feat: restore source parity and harden agent runtime
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
大屏设计器服务
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from sqlalchemy import select, func, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.base_service import BaseService
|
||||
from online_dev.screen_design.screen_model import ScreenProject
|
||||
from online_dev.screen_design.screen_schema import ScreenProjectCreateIn, ScreenProjectUpdateIn
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ScreenServiceException(Exception):
|
||||
"""大屏服务异常"""
|
||||
pass
|
||||
|
||||
|
||||
class ScreenProjectService(BaseService[ScreenProject, ScreenProjectCreateIn, ScreenProjectUpdateIn]):
|
||||
"""
|
||||
大屏项目服务
|
||||
|
||||
数据权限:
|
||||
- 使用 get_list_with_data_scope() 自动应用数据权限
|
||||
- 支持本人、本部门、本部门及下级、全部等数据范围
|
||||
"""
|
||||
|
||||
model = ScreenProject
|
||||
|
||||
# 资源类型(用于数据权限配置)
|
||||
RESOURCE_TYPE = "screen_project"
|
||||
RESOURCE_DISPLAY_NAME = "大屏管理"
|
||||
|
||||
@classmethod
|
||||
async def get_list(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
name: str = None,
|
||||
code: str = None,
|
||||
status: str = None,
|
||||
application_id: str = None,
|
||||
) -> Tuple[List[ScreenProject], int]:
|
||||
"""获取大屏项目列表"""
|
||||
stmt = select(ScreenProject).where(ScreenProject.is_deleted == False)
|
||||
|
||||
# 应用过滤
|
||||
if application_id is not None:
|
||||
if application_id == "":
|
||||
stmt = stmt.where(ScreenProject.application_id.is_(None))
|
||||
else:
|
||||
stmt = stmt.where(ScreenProject.application_id == application_id)
|
||||
|
||||
if name:
|
||||
stmt = stmt.where(ScreenProject.name.contains(name))
|
||||
if code:
|
||||
stmt = stmt.where(ScreenProject.code.contains(code))
|
||||
if status:
|
||||
stmt = stmt.where(ScreenProject.status == status)
|
||||
|
||||
# 计算总数
|
||||
count_stmt = select(func.count()).select_from(stmt.subquery())
|
||||
total_result = await db.execute(count_stmt)
|
||||
total = total_result.scalar() or 0
|
||||
|
||||
# 分页
|
||||
stmt = stmt.order_by(ScreenProject.sort.asc(), ScreenProject.sys_create_datetime.desc())
|
||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
items = list(result.scalars().all())
|
||||
|
||||
return items, total
|
||||
|
||||
@classmethod
|
||||
async def get_by_code(cls, db: AsyncSession, code: str) -> Optional[ScreenProject]:
|
||||
"""根据编码获取大屏项目"""
|
||||
stmt = select(ScreenProject).where(
|
||||
ScreenProject.code == code,
|
||||
ScreenProject.is_deleted == False,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@classmethod
|
||||
async def check_code_exists(cls, db: AsyncSession, code: str, exclude_id: str = None) -> bool:
|
||||
"""检查编码是否存在"""
|
||||
stmt = select(ScreenProject).where(
|
||||
ScreenProject.code == code,
|
||||
ScreenProject.is_deleted == False,
|
||||
)
|
||||
if exclude_id:
|
||||
stmt = stmt.where(ScreenProject.id != exclude_id)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
@classmethod
|
||||
async def publish(cls, db: AsyncSession, project_id: str, access_password: str = None) -> ScreenProject:
|
||||
"""发布大屏"""
|
||||
project = await cls.get_by_id(db, project_id)
|
||||
if not project:
|
||||
raise ScreenServiceException(f"大屏项目不存在: {project_id}")
|
||||
|
||||
if project.status == 'published':
|
||||
raise ScreenServiceException("大屏已发布")
|
||||
|
||||
project.status = 'published'
|
||||
project.version += 1
|
||||
project.published_at = datetime.now()
|
||||
|
||||
if access_password is not None:
|
||||
project.access_password = access_password
|
||||
|
||||
db.add(project)
|
||||
await db.flush()
|
||||
await db.refresh(project)
|
||||
|
||||
logger.info(f"大屏发布成功: {project.code}, version={project.version}")
|
||||
return project
|
||||
|
||||
@classmethod
|
||||
async def unpublish(cls, db: AsyncSession, project_id: str) -> ScreenProject:
|
||||
"""取消发布大屏"""
|
||||
project = await cls.get_by_id(db, project_id)
|
||||
if not project:
|
||||
raise ScreenServiceException(f"大屏项目不存在: {project_id}")
|
||||
|
||||
if project.status == 'draft':
|
||||
raise ScreenServiceException("大屏未发布")
|
||||
|
||||
project.status = 'draft'
|
||||
db.add(project)
|
||||
await db.flush()
|
||||
await db.refresh(project)
|
||||
|
||||
logger.info(f"大屏取消发布: {project.code}")
|
||||
return project
|
||||
|
||||
@classmethod
|
||||
async def set_password(cls, db: AsyncSession, project_id: str, password: str) -> ScreenProject:
|
||||
"""设置或清除访问密码"""
|
||||
project = await cls.get_by_id(db, project_id)
|
||||
if not project:
|
||||
raise ScreenServiceException(f"大屏项目不存在: {project_id}")
|
||||
|
||||
project.access_password = password
|
||||
db.add(project)
|
||||
await db.flush()
|
||||
await db.refresh(project)
|
||||
|
||||
logger.info(f"大屏密码已{'设置' if password else '清除'}: {project.code}")
|
||||
return project
|
||||
|
||||
@classmethod
|
||||
async def verify_password(cls, db: AsyncSession, project_id: str, password: str) -> bool:
|
||||
"""验证访问密码"""
|
||||
project = await cls.get_by_id(db, project_id)
|
||||
if not project:
|
||||
raise ScreenServiceException(f"大屏项目不存在: {project_id}")
|
||||
|
||||
# 如果没有设置密码,直接返回 True
|
||||
if not project.access_password:
|
||||
return True
|
||||
|
||||
return project.access_password == password
|
||||
|
||||
@classmethod
|
||||
async def copy(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
project_id: str,
|
||||
new_code: str,
|
||||
new_name: str = None,
|
||||
) -> ScreenProject:
|
||||
"""复制大屏项目"""
|
||||
source = await cls.get_by_id(db, project_id)
|
||||
if not source:
|
||||
raise ScreenServiceException(f"大屏项目不存在: {project_id}")
|
||||
|
||||
# 检查新编码唯一性
|
||||
if await cls.check_code_exists(db, new_code):
|
||||
raise ScreenServiceException(f"项目编码已存在: {new_code}")
|
||||
|
||||
new_project = ScreenProject(
|
||||
application_id=source.application_id,
|
||||
name=new_name or f"{source.name}_副本",
|
||||
code=new_code,
|
||||
description=source.description,
|
||||
status='draft',
|
||||
version=1,
|
||||
thumbnail=source.thumbnail,
|
||||
screen_config=source.screen_config,
|
||||
sort=source.sort,
|
||||
)
|
||||
db.add(new_project)
|
||||
await db.flush()
|
||||
await db.refresh(new_project)
|
||||
|
||||
logger.info(f"大屏项目复制成功: {source.code} -> {new_code}")
|
||||
return new_project
|
||||
|
||||
@classmethod
|
||||
async def export_config(cls, db: AsyncSession, project_id: str) -> Dict[str, Any]:
|
||||
"""导出大屏配置"""
|
||||
project = await cls.get_by_id(db, project_id)
|
||||
if not project:
|
||||
raise ScreenServiceException(f"大屏项目不存在: {project_id}")
|
||||
|
||||
return {
|
||||
'name': project.name,
|
||||
'code': project.code,
|
||||
'description': project.description,
|
||||
'screen_config': project.screen_config,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def check_import(cls, db: AsyncSession, code: str) -> Dict[str, Any]:
|
||||
"""导入预检查:编码是否冲突"""
|
||||
code_exists = False
|
||||
if code:
|
||||
code_exists = await cls.check_code_exists(db, code)
|
||||
|
||||
return {
|
||||
'code_exists': code_exists,
|
||||
'can_import': not code_exists,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def import_config(cls, db: AsyncSession, data: Dict[str, Any]) -> ScreenProject:
|
||||
"""导入大屏配置"""
|
||||
required_fields = ['name', 'code']
|
||||
for field in required_fields:
|
||||
if not data.get(field):
|
||||
raise ScreenServiceException(f"缺少必要字段: {field}")
|
||||
|
||||
if await cls.check_code_exists(db, data['code']):
|
||||
raise ScreenServiceException(f"项目编码已存在: {data['code']}")
|
||||
|
||||
project = ScreenProject(
|
||||
application_id=data.get('application_id'),
|
||||
name=data['name'],
|
||||
code=data['code'],
|
||||
description=data.get('description', ''),
|
||||
screen_config=data.get('screen_config', {}),
|
||||
)
|
||||
db.add(project)
|
||||
await db.flush()
|
||||
await db.refresh(project)
|
||||
|
||||
logger.info(f"大屏项目导入成功: {project.code}")
|
||||
return project
|
||||
|
||||
@classmethod
|
||||
async def batch_delete(cls, db: AsyncSession, project_ids: List[str]) -> int:
|
||||
"""批量删除大屏项目"""
|
||||
stmt = (
|
||||
update(ScreenProject)
|
||||
.where(ScreenProject.id.in_(project_ids), ScreenProject.is_deleted == False)
|
||||
.values(is_deleted=True)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
count = result.rowcount
|
||||
|
||||
logger.info(f"批量删除大屏项目成功: {count} 个")
|
||||
return count
|
||||
Reference in New Issue
Block a user