508 lines
16 KiB
Python
508 lines
16 KiB
Python
#!/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()]
|