Files
ai-agent-admin/backend-fastapi/online_dev/page_manager/service.py
T
2026-06-09 21:18:33 +08:00

508 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
页面元数据管理服务(异步版本)
"""
import logging
from typing import Any, Dict, List
from sqlalchemy import select, update, delete, func, and_
from sqlalchemy.ext.asyncio import AsyncSession
from online_dev.page_manager.model import PageMeta
from app.data_scope_utils import get_data_scope_filter, apply_data_scope_to_conditions
logger = logging.getLogger(__name__)
# 资源类型(用于数据权限配置)
RESOURCE_TYPE = "page"
RESOURCE_DISPLAY_NAME = "页面管理"
class PageServiceException(Exception):
"""页面服务异常"""
pass
class PageService:
"""
页面元数据管理服务
数据权限:
- 使用 list_with_data_scope() 自动应用数据权限
- 支持本人、本部门、本部门及下级、全部等数据范围
"""
# ============ 查询 ============
@staticmethod
async def list(
db: AsyncSession,
page: int = 1,
page_size: int = 20,
application_id: str = None,
name: str = None,
code: str = None,
category: str = None,
status: str = None
) -> Dict[str, Any]:
"""分页查询页面列表"""
conditions = [PageMeta.is_deleted == False]
# 应用过滤
if application_id:
conditions.append(PageMeta.application_id == application_id)
else:
# 如果没有指定 application_id,只返回主应用的页面(application_id 为 NULL
conditions.append(PageMeta.application_id.is_(None))
if name:
conditions.append(PageMeta.name.ilike(f"%{name}%"))
if code:
conditions.append(PageMeta.code.ilike(f"%{code}%"))
if category:
conditions.append(PageMeta.category == category)
if status:
conditions.append(PageMeta.status == status)
# 获取总数
count_stmt = select(func.count(PageMeta.id)).where(and_(*conditions))
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# 获取列表
offset = (page - 1) * page_size
stmt = select(PageMeta).where(and_(*conditions)).order_by(
PageMeta.sort, PageMeta.sys_create_datetime.desc()
).offset(offset).limit(page_size)
result = await db.execute(stmt)
items = list(result.scalars().all())
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size
}
@staticmethod
async def list_with_data_scope(
db: AsyncSession,
page: int = 1,
page_size: int = 20,
application_id: str = None,
name: str = None,
code: str = None,
category: str = None,
status: str = None
) -> Dict[str, Any]:
"""
分页查询页面列表(带数据权限过滤)
自动从上下文获取当前用户信息,应用数据权限过滤
"""
conditions = [PageMeta.is_deleted == False]
# 应用过滤
if application_id:
conditions.append(PageMeta.application_id == application_id)
else:
conditions.append(PageMeta.application_id.is_(None))
if name:
conditions.append(PageMeta.name.ilike(f"%{name}%"))
if code:
conditions.append(PageMeta.code.ilike(f"%{code}%"))
if category:
conditions.append(PageMeta.category == category)
if status:
conditions.append(PageMeta.status == status)
# 获取数据权限过滤条件并应用
data_scope_filter = await get_data_scope_filter(db, RESOURCE_TYPE)
scope_conditions = apply_data_scope_to_conditions(PageMeta, data_scope_filter)
conditions.extend(scope_conditions)
# 获取总数
count_stmt = select(func.count(PageMeta.id)).where(and_(*conditions))
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# 获取列表
offset = (page - 1) * page_size
stmt = select(PageMeta).where(and_(*conditions)).order_by(
PageMeta.sort, PageMeta.sys_create_datetime.desc()
).offset(offset).limit(page_size)
result = await db.execute(stmt)
items = list(result.scalars().all())
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size
}
@staticmethod
async def get(db: AsyncSession, page_id: str) -> PageMeta:
"""获取页面详情"""
stmt = select(PageMeta).where(
PageMeta.id == page_id,
PageMeta.is_deleted == False
)
result = await db.execute(stmt)
page = result.scalar_one_or_none()
if not page:
raise PageServiceException(f"页面不存在: {page_id}")
return page
@staticmethod
async def get_by_code(db: AsyncSession, code: str) -> PageMeta:
"""根据编码获取页面"""
stmt = select(PageMeta).where(
PageMeta.code == code,
PageMeta.is_deleted == False
)
result = await db.execute(stmt)
page = result.scalar_one_or_none()
if not page:
raise PageServiceException(f"页面不存在: {code}")
return page
# ============ 创建 ============
@staticmethod
async def create(
db: AsyncSession,
data: Dict[str, Any],
user_id: str = None
) -> PageMeta:
"""创建页面"""
code = data.get("code")
# 检查编码唯一性
stmt = select(PageMeta).where(
PageMeta.code == code,
PageMeta.is_deleted == False
)
result = await db.execute(stmt)
if result.scalar_one_or_none():
raise PageServiceException(f"页面编码已存在: {code}")
# 从上下文获取用户信息
from utils.context import get_current_user_info_from_context
user_info = get_current_user_info_from_context()
page = PageMeta(
application_id=data.get("application_id"),
name=data.get("name"),
code=code,
category=data.get("category", ""),
description=data.get("description", ""),
page_config=data.get("page_config", {}),
sort=data.get("sort", 0),
sys_creator_id=user_id or (user_info.get('user_id') if user_info else None),
sys_modifier_id=user_id or (user_info.get('user_id') if user_info else None),
)
# 自动填充部门ID
if user_info and user_info.get('dept_id'):
page.sys_dept_id = user_info.get('dept_id')
db.add(page)
await db.commit()
await db.refresh(page)
logger.info(f"页面创建成功: {page.code}")
return page
# ============ 更新 ============
@staticmethod
async def update(
db: AsyncSession,
page_id: str,
data: Dict[str, Any],
user_id: str = None
) -> PageMeta:
"""更新页面"""
page = await PageService.get(db, page_id)
# 更新基本字段
if "name" in data and data["name"] is not None:
page.name = data["name"]
if "category" in data and data["category"] is not None:
page.category = data["category"]
if "description" in data and data["description"] is not None:
page.description = data["description"]
if "sort" in data and data["sort"] is not None:
page.sort = data["sort"]
if "page_config" in data and data["page_config"] is not None:
page.page_config = data["page_config"]
page.sys_modifier_id = user_id
await db.commit()
await db.refresh(page)
logger.info(f"页面更新成功: {page.code}")
return page
# ============ 删除 ============
@staticmethod
async def _cleanup_page_publish_resources(db: AsyncSession, page: PageMeta) -> None:
"""清理页面发布产生的菜单(取消发布、删除时共用)"""
from core.menu.model import Menu
delete_menu_stmt = delete(Menu).where(
Menu.path == f"/page-render/{page.code}"
)
result = await db.execute(delete_menu_stmt)
if result.rowcount > 0:
logger.info(
"物理删除页面菜单: %s, 删除数量: %s",
page.code,
result.rowcount,
)
@staticmethod
async def delete(db: AsyncSession, page_id: str) -> bool:
"""删除页面(软删除)"""
from core.menu.service import MenuService
page = await PageService.get(db, page_id)
await PageService._cleanup_page_publish_resources(db, page)
page.is_deleted = True
page.status = "draft"
await db.commit()
await MenuService.invalidate_cache()
logger.info("页面删除成功: %s", page.code)
return True
@staticmethod
async def batch_delete(db: AsyncSession, page_ids: List[str]) -> int:
"""批量删除页面"""
from core.menu.service import MenuService
stmt = select(PageMeta).where(
PageMeta.id.in_(page_ids),
PageMeta.is_deleted == False,
)
pages = list((await db.execute(stmt)).scalars().all())
for page in pages:
await PageService._cleanup_page_publish_resources(db, page)
update_stmt = update(PageMeta).where(
PageMeta.id.in_(page_ids),
PageMeta.is_deleted == False,
).values(is_deleted=True, status="draft")
result = await db.execute(update_stmt)
await db.commit()
count = result.rowcount
if count > 0:
await MenuService.invalidate_cache()
logger.info("批量删除页面成功: %s", count)
return count
# ============ 发布/取消发布 ============
@staticmethod
async def publish(
db: AsyncSession,
page_id: str,
publish_config: Dict[str, Any] = None
) -> PageMeta:
"""发布页面并创建菜单"""
from core.menu.model import Menu
from core.menu.service import MenuService
page = await PageService.get(db, page_id)
if page.status == "published":
raise PageServiceException("页面已发布")
# 更新页面状态
page.status = "published"
page.version += 1
# 创建或更新菜单
if publish_config:
menu_parent_id = publish_config.get("menu_parent_id")
menu_path = f"/page-render/{page.code}"
# 检查是否已存在该页面的菜单
menu_stmt = select(Menu).where(
Menu.path == menu_path
)
menu_result = await db.execute(menu_stmt)
existing_menu = menu_result.scalar_one_or_none()
if existing_menu:
# 更新现有菜单
existing_menu.name = publish_config.get("menu_name", page.name)
existing_menu.title = publish_config.get("menu_name", page.name)
existing_menu.parent_id = menu_parent_id
existing_menu.icon = publish_config.get("menu_icon", "lucide:layout-dashboard")
existing_menu.order = publish_config.get("menu_order", 0)
existing_menu.type = "online_page"
existing_menu.application_id = page.application_id
logger.info(f"更新页面菜单: {page.code}")
else:
# 创建新菜单
new_menu = Menu(
application_id=page.application_id,
name=publish_config.get("menu_name", page.name),
title=publish_config.get("menu_name", page.name),
path=menu_path,
component="online-dev/page-render/index",
type="online_page",
parent_id=menu_parent_id,
icon=publish_config.get("menu_icon", "lucide:layout-dashboard"),
order=publish_config.get("menu_order", 0),
)
db.add(new_menu)
logger.info(f"创建页面菜单: {page.code}")
await db.commit()
await db.refresh(page)
# 清空菜单缓存
await MenuService.invalidate_cache()
logger.info("已清空菜单缓存")
logger.info(f"页面发布成功: {page.code}, version={page.version}")
return page
@staticmethod
async def unpublish(db: AsyncSession, page_id: str) -> PageMeta:
"""取消发布页面并物理删除菜单"""
from core.menu.service import MenuService
page = await PageService.get(db, page_id)
if page.status == "draft":
raise PageServiceException("页面未发布")
page.status = "draft"
await PageService._cleanup_page_publish_resources(db, page)
await db.commit()
await db.refresh(page)
# 清空菜单缓存
await MenuService.invalidate_cache()
logger.info("已清空菜单缓存")
logger.info("页面取消发布: %s", page.code)
return page
# ============ 复制 ============
@staticmethod
async def copy(
db: AsyncSession,
page_id: str,
new_code: str,
new_name: str = None,
user_id: str = None
) -> PageMeta:
"""复制页面"""
source = await PageService.get(db, page_id)
# 检查新编码唯一性
stmt = select(PageMeta).where(
PageMeta.code == new_code,
PageMeta.is_deleted == False
)
result = await db.execute(stmt)
if result.scalar_one_or_none():
raise PageServiceException(f"页面编码已存在: {new_code}")
new_page = PageMeta(
application_id=source.application_id,
name=new_name or f"{source.name}_副本",
code=new_code,
category=source.category,
description=source.description,
status="draft",
version=1,
page_config=source.page_config,
sort=source.sort,
sys_creator_id=user_id,
sys_modifier_id=user_id,
)
db.add(new_page)
await db.commit()
await db.refresh(new_page)
logger.info(f"页面复制成功: {source.code} -> {new_code}")
return new_page
# ============ 导入/导出 ============
@staticmethod
async def export_config(db: AsyncSession, page_id: str) -> Dict[str, Any]:
"""导出页面配置"""
page = await PageService.get(db, page_id)
return {
"name": page.name,
"code": page.code,
"category": page.category,
"description": page.description,
"page_config": page.page_config,
}
@staticmethod
async def check_import(db: AsyncSession, code: str) -> Dict[str, Any]:
"""导入预检查:编码是否冲突"""
code_exists = False
if code:
stmt = select(PageMeta).where(
PageMeta.code == code,
PageMeta.is_deleted == False,
)
result = await db.execute(stmt)
code_exists = result.scalar_one_or_none() is not None
return {
"code_exists": code_exists,
"can_import": not code_exists,
}
@staticmethod
async def import_config(
db: AsyncSession,
data: Dict[str, Any],
user_id: str = None
) -> PageMeta:
"""导入页面配置"""
required_fields = ["name", "code"]
for field in required_fields:
if not data.get(field):
raise PageServiceException(f"缺少必要字段: {field}")
return await PageService.create(db, data, user_id)
# ============ 获取分类列表 ============
@staticmethod
async def get_categories(db: AsyncSession) -> List[str]:
"""获取所有分类"""
stmt = select(PageMeta.category).where(
PageMeta.is_deleted == False,
PageMeta.category != ""
).distinct()
result = await db.execute(stmt)
return [row[0] for row in result.fetchall()]