#!/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