Restore AI workflow design nodes
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .service import PageService, PageServiceException
|
||||
|
||||
__all__ = ["PageService", "PageServiceException"]
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
页面管理数据模型
|
||||
"""
|
||||
from sqlalchemy import Column, String, Text, Integer, JSON
|
||||
|
||||
from app.base_model import BaseModel
|
||||
|
||||
|
||||
class PageMeta(BaseModel):
|
||||
"""页面元数据"""
|
||||
__tablename__ = "page_meta"
|
||||
|
||||
# 所属应用(逻辑外键关联 core_application)
|
||||
application_id = Column(String(21), nullable=True, index=True, comment="所属应用ID")
|
||||
|
||||
name = Column(String(100), nullable=False, comment="页面名称")
|
||||
code = Column(String(100), unique=True, nullable=False, comment="页面编码")
|
||||
category = Column(String(50), default="", comment="分类")
|
||||
description = Column(Text, default="", comment="描述")
|
||||
status = Column(String(20), default="draft", index=True, comment="状态: draft/published")
|
||||
version = Column(Integer, default=1, comment="版本号")
|
||||
|
||||
# 页面配置(存储 dashboard-design 的配置)
|
||||
page_config = Column(JSON, default=dict, comment="页面设计配置")
|
||||
@@ -0,0 +1,507 @@
|
||||
#!/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()]
|
||||
Reference in New Issue
Block a user