feat: restore source parity and harden agent runtime
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
大屏设计器模块
|
||||
"""
|
||||
from online_dev.screen_design.screen_api import router as screen_router
|
||||
from online_dev.screen_design.material_api import router as material_router
|
||||
|
||||
__all__ = ['screen_router', 'material_router']
|
||||
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
大屏素材 API
|
||||
素材分类和素材管理
|
||||
"""
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from online_dev.screen_design.material_schema import (
|
||||
MaterialCategoryCreateIn,
|
||||
MaterialCategoryUpdateIn,
|
||||
MaterialCategoryOut,
|
||||
MaterialCreateIn,
|
||||
MaterialUpdateIn,
|
||||
MaterialOut,
|
||||
)
|
||||
from online_dev.screen_design.material_service import MaterialCategoryService, MaterialService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/screen/material', tags=['大屏素材'])
|
||||
|
||||
|
||||
# ============ 素材分类 API ============
|
||||
|
||||
@router.get('/categories', response_model=List[MaterialCategoryOut], summary='素材分类列表')
|
||||
async def list_material_categories(db: AsyncSession = Depends(get_db)):
|
||||
"""获取所有素材分类"""
|
||||
categories = await MaterialCategoryService.get_all(db)
|
||||
|
||||
result = []
|
||||
for cat in categories:
|
||||
count = await MaterialCategoryService.get_material_count(db, cat.id)
|
||||
result.append(MaterialCategoryOut.from_orm_model(cat, material_count=count))
|
||||
return result
|
||||
|
||||
|
||||
@router.post('/categories', response_model=MaterialCategoryOut, summary='创建素材分类')
|
||||
async def create_material_category(
|
||||
data: MaterialCategoryCreateIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""创建素材分类"""
|
||||
# 检查编码是否已存在
|
||||
if await MaterialCategoryService.check_code_exists(db, data.code):
|
||||
raise HTTPException(status_code=400, detail=f"分类编码 {data.code} 已存在")
|
||||
|
||||
category = await MaterialCategoryService.create(db, data)
|
||||
await db.commit()
|
||||
|
||||
return MaterialCategoryOut.from_orm_model(category, material_count=0)
|
||||
|
||||
|
||||
@router.put('/categories/{category_id}', response_model=MaterialCategoryOut, summary='更新素材分类')
|
||||
async def update_material_category(
|
||||
category_id: str,
|
||||
data: MaterialCategoryUpdateIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更新素材分类"""
|
||||
category = await MaterialCategoryService.get_by_id(db, category_id)
|
||||
if not category:
|
||||
raise HTTPException(status_code=404, detail="分类不存在")
|
||||
|
||||
category = await MaterialCategoryService.update(db, category_id, data)
|
||||
await db.commit()
|
||||
|
||||
count = await MaterialCategoryService.get_material_count(db, category.id)
|
||||
return MaterialCategoryOut.from_orm_model(category, material_count=count)
|
||||
|
||||
|
||||
@router.delete('/categories/{category_id}', summary='删除素材分类')
|
||||
async def delete_material_category(
|
||||
category_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""删除素材分类(同时删除分类下的所有素材)"""
|
||||
category = await MaterialCategoryService.get_by_id(db, category_id)
|
||||
if not category:
|
||||
raise HTTPException(status_code=404, detail="分类不存在")
|
||||
|
||||
await MaterialCategoryService.delete_with_materials(db, category_id)
|
||||
await db.commit()
|
||||
return {'success': True}
|
||||
|
||||
|
||||
# ============ 素材 API ============
|
||||
|
||||
async def _get_file_info(db: AsyncSession, file_id: str):
|
||||
"""获取文件信息"""
|
||||
file_url = ''
|
||||
file_type = 'image'
|
||||
file_size = 0
|
||||
|
||||
if file_id:
|
||||
try:
|
||||
from core.file_manager.model import FileManager
|
||||
stmt = select(FileManager).where(FileManager.id == file_id)
|
||||
file_result = await db.execute(stmt)
|
||||
file_obj = file_result.scalar_one_or_none()
|
||||
if file_obj:
|
||||
file_url = file_obj.url or ''
|
||||
file_size = file_obj.size or 0
|
||||
file_ext = (file_obj.file_ext or '').lower()
|
||||
file_type = 'svg' if file_ext == 'svg' or file_ext == '.svg' else 'image'
|
||||
except Exception as e:
|
||||
logger.warning(f"获取文件信息失败: {e}")
|
||||
|
||||
return file_url, file_type, file_size
|
||||
|
||||
|
||||
@router.get('/list', response_model=List[MaterialOut], summary='素材列表')
|
||||
async def list_materials(
|
||||
category_id: str = Query(default=None, alias="categoryId", description="分类ID"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取素材列表"""
|
||||
materials = await MaterialService.get_list_by_category(db, category_id)
|
||||
|
||||
result = []
|
||||
for material in materials:
|
||||
# 获取分类名称
|
||||
category_name = ''
|
||||
if material.category_id:
|
||||
category = await MaterialCategoryService.get_by_id(db, material.category_id)
|
||||
if category:
|
||||
category_name = category.name
|
||||
|
||||
# 获取文件信息
|
||||
file_url, file_type, file_size = await _get_file_info(db, material.file_id)
|
||||
|
||||
result.append(MaterialOut.from_orm_model(
|
||||
material,
|
||||
category_name=category_name,
|
||||
file_url=file_url,
|
||||
file_type=file_type,
|
||||
file_size=file_size,
|
||||
))
|
||||
return result
|
||||
|
||||
|
||||
@router.post('/create', response_model=MaterialOut, summary='创建素材')
|
||||
async def create_material(
|
||||
data: MaterialCreateIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""创建素材(关联已上传的文件)"""
|
||||
# 验证分类(category_id 为空时表示无分类素材)
|
||||
category = None
|
||||
if data.category_id:
|
||||
category = await MaterialCategoryService.get_by_id(db, data.category_id)
|
||||
if not category:
|
||||
raise HTTPException(status_code=400, detail="分类不存在")
|
||||
|
||||
# 验证文件
|
||||
try:
|
||||
from core.file_manager.model import FileManager
|
||||
stmt = select(FileManager).where(FileManager.id == data.file_id)
|
||||
file_result = await db.execute(stmt)
|
||||
file_obj = file_result.scalar_one_or_none()
|
||||
if not file_obj:
|
||||
raise HTTPException(status_code=400, detail="文件不存在")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"验证文件失败: {e}")
|
||||
|
||||
# 创建素材
|
||||
material = await MaterialService.create(db, data)
|
||||
await db.commit()
|
||||
|
||||
# 获取文件信息
|
||||
file_url = file_obj.url or ''
|
||||
file_size = file_obj.size or 0
|
||||
file_ext = (file_obj.file_ext or '').lower()
|
||||
file_type = 'svg' if file_ext == 'svg' or file_ext == '.svg' else 'image'
|
||||
|
||||
return MaterialOut.from_orm_model(
|
||||
material,
|
||||
category_name=category.name if category else '',
|
||||
file_url=file_url,
|
||||
file_type=file_type,
|
||||
file_size=file_size,
|
||||
)
|
||||
|
||||
|
||||
@router.put('/{material_id}', response_model=MaterialOut, summary='更新素材')
|
||||
async def update_material(
|
||||
material_id: str,
|
||||
data: MaterialUpdateIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更新素材信息"""
|
||||
material = await MaterialService.get_by_id(db, material_id)
|
||||
if not material:
|
||||
raise HTTPException(status_code=404, detail="素材不存在")
|
||||
|
||||
# 如果更新分类,验证分类存在
|
||||
if data.category_id:
|
||||
category = await MaterialCategoryService.get_by_id(db, data.category_id)
|
||||
if not category:
|
||||
raise HTTPException(status_code=400, detail="分类不存在")
|
||||
|
||||
material = await MaterialService.update(db, material_id, data)
|
||||
await db.commit()
|
||||
|
||||
# 获取分类名称
|
||||
category_name = ''
|
||||
if material.category_id:
|
||||
category = await MaterialCategoryService.get_by_id(db, material.category_id)
|
||||
if category:
|
||||
category_name = category.name
|
||||
|
||||
# 获取文件信息
|
||||
file_url, file_type, file_size = await _get_file_info(db, material.file_id)
|
||||
|
||||
return MaterialOut.from_orm_model(
|
||||
material,
|
||||
category_name=category_name,
|
||||
file_url=file_url,
|
||||
file_type=file_type,
|
||||
file_size=file_size,
|
||||
)
|
||||
|
||||
|
||||
@router.delete('/{material_id}', summary='删除素材')
|
||||
async def delete_material(
|
||||
material_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""删除素材"""
|
||||
material = await MaterialService.get_by_id(db, material_id)
|
||||
if not material:
|
||||
raise HTTPException(status_code=404, detail="素材不存在")
|
||||
|
||||
await MaterialService.delete(db, material_id)
|
||||
await db.commit()
|
||||
return {'success': True}
|
||||
|
||||
|
||||
@router.delete('/batch', summary='批量删除素材')
|
||||
async def batch_delete_materials(
|
||||
ids: List[str] = Query(..., description="素材ID列表"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""批量删除素材"""
|
||||
count = await MaterialService.batch_delete(db, ids)
|
||||
await db.commit()
|
||||
return {'count': count}
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
大屏素材数据模型
|
||||
"""
|
||||
from sqlalchemy import Column, String
|
||||
|
||||
from app.base_model import BaseModel
|
||||
|
||||
|
||||
class ScreenMaterialCategory(BaseModel):
|
||||
"""大屏素材分类"""
|
||||
__tablename__ = "screen_material_category"
|
||||
|
||||
name = Column(String(50), nullable=False, comment="分类名称")
|
||||
code = Column(String(50), unique=True, index=True, nullable=False, comment="分类编码")
|
||||
icon = Column(String(50), default='Image', comment="图标名称")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ScreenMaterialCategory {self.name}>"
|
||||
|
||||
|
||||
class ScreenMaterial(BaseModel):
|
||||
"""大屏素材"""
|
||||
__tablename__ = "screen_material"
|
||||
|
||||
category_id = Column(String(32), nullable=True, index=True, comment="分类ID")
|
||||
name = Column(String(100), nullable=False, comment="素材名称")
|
||||
file_id = Column(String(32), nullable=True, comment="文件ID(关联文件管理器)")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ScreenMaterial {self.name}>"
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
大屏素材 Schema 定义
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
# ============ 素材分类 Schema ============
|
||||
|
||||
class MaterialCategoryCreateIn(BaseModel):
|
||||
"""创建素材分类请求"""
|
||||
name: str = Field(..., description="分类名称")
|
||||
code: str = Field(..., description="分类编码")
|
||||
icon: str = Field(default='Image', description="图标名称")
|
||||
sort: int = Field(default=0, description="排序")
|
||||
|
||||
|
||||
class MaterialCategoryUpdateIn(BaseModel):
|
||||
"""更新素材分类请求"""
|
||||
name: Optional[str] = Field(default=None, description="分类名称")
|
||||
icon: Optional[str] = Field(default=None, description="图标名称")
|
||||
sort: Optional[int] = Field(default=None, description="排序")
|
||||
|
||||
|
||||
class MaterialCategoryOut(BaseModel):
|
||||
"""素材分类输出"""
|
||||
id: str
|
||||
name: str
|
||||
code: str
|
||||
icon: str
|
||||
sort: int
|
||||
material_count: int = Field(default=0, description="素材数量")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def from_orm_model(cls, obj, material_count: int = 0):
|
||||
"""从ORM模型转换"""
|
||||
return cls(
|
||||
id=str(obj.id),
|
||||
name=obj.name,
|
||||
code=obj.code,
|
||||
icon=obj.icon or 'Image',
|
||||
sort=obj.sort or 0,
|
||||
material_count=material_count,
|
||||
)
|
||||
|
||||
|
||||
# ============ 素材 Schema ============
|
||||
|
||||
class MaterialCreateIn(BaseModel):
|
||||
"""创建素材请求"""
|
||||
category_id: Optional[str] = Field(default=None, description="分类ID(为空表示无分类)")
|
||||
name: str = Field(..., description="素材名称")
|
||||
file_id: str = Field(..., description="文件ID(来自文件管理器)")
|
||||
|
||||
|
||||
class MaterialUpdateIn(BaseModel):
|
||||
"""更新素材请求"""
|
||||
name: Optional[str] = Field(default=None, description="素材名称")
|
||||
category_id: Optional[str] = Field(default=None, description="分类ID")
|
||||
sort: Optional[int] = Field(default=None, description="排序")
|
||||
|
||||
|
||||
class MaterialOut(BaseModel):
|
||||
"""素材输出"""
|
||||
id: str
|
||||
name: str
|
||||
category_id: str
|
||||
category_name: str = Field(default='', description="分类名称")
|
||||
file_id: Optional[str] = Field(default=None, description="文件ID")
|
||||
file_url: str = Field(default='', description="文件URL")
|
||||
file_type: str = Field(default='image', description="文件类型")
|
||||
file_size: int = Field(default=0, description="文件大小")
|
||||
sort: int = 0
|
||||
sys_create_datetime: str
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def from_orm_model(
|
||||
cls,
|
||||
obj,
|
||||
category_name: str = '',
|
||||
file_url: str = '',
|
||||
file_type: str = 'image',
|
||||
file_size: int = 0,
|
||||
):
|
||||
"""从ORM模型转换"""
|
||||
return cls(
|
||||
id=str(obj.id),
|
||||
name=obj.name,
|
||||
category_id=str(obj.category_id) if obj.category_id else '',
|
||||
category_name=category_name,
|
||||
file_id=str(obj.file_id) if obj.file_id else None,
|
||||
file_url=file_url,
|
||||
file_type=file_type,
|
||||
file_size=file_size,
|
||||
sort=obj.sort or 0,
|
||||
sys_create_datetime=obj.sys_create_datetime.strftime('%Y-%m-%d %H:%M:%S') if obj.sys_create_datetime else '',
|
||||
)
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
大屏素材服务
|
||||
"""
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy import select, func, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.base_service import BaseService
|
||||
from online_dev.screen_design.material_model import ScreenMaterialCategory, ScreenMaterial
|
||||
from online_dev.screen_design.material_schema import (
|
||||
MaterialCategoryCreateIn, MaterialCategoryUpdateIn,
|
||||
MaterialCreateIn, MaterialUpdateIn,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MaterialCategoryService(BaseService[ScreenMaterialCategory, MaterialCategoryCreateIn, MaterialCategoryUpdateIn]):
|
||||
"""素材分类服务"""
|
||||
|
||||
model = ScreenMaterialCategory
|
||||
|
||||
@classmethod
|
||||
async def get_all(cls, db: AsyncSession) -> List[ScreenMaterialCategory]:
|
||||
"""获取所有素材分类"""
|
||||
stmt = select(ScreenMaterialCategory).where(
|
||||
ScreenMaterialCategory.is_deleted == False
|
||||
).order_by(ScreenMaterialCategory.sort.asc(), ScreenMaterialCategory.sys_create_datetime.desc())
|
||||
result = await db.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def check_code_exists(cls, db: AsyncSession, code: str, exclude_id: str = None) -> bool:
|
||||
"""检查编码是否存在"""
|
||||
stmt = select(ScreenMaterialCategory).where(
|
||||
ScreenMaterialCategory.code == code,
|
||||
ScreenMaterialCategory.is_deleted == False,
|
||||
)
|
||||
if exclude_id:
|
||||
stmt = stmt.where(ScreenMaterialCategory.id != exclude_id)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
@classmethod
|
||||
async def get_material_count(cls, db: AsyncSession, category_id: str) -> int:
|
||||
"""获取分类下的素材数量"""
|
||||
stmt = select(func.count()).where(
|
||||
ScreenMaterial.category_id == category_id,
|
||||
ScreenMaterial.is_deleted == False,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar() or 0
|
||||
|
||||
@classmethod
|
||||
async def delete_with_materials(cls, db: AsyncSession, category_id: str) -> bool:
|
||||
"""删除分类及其下的所有素材"""
|
||||
# 删除分类下的所有素材(硬删除)
|
||||
await db.execute(
|
||||
delete(ScreenMaterial).where(ScreenMaterial.category_id == category_id)
|
||||
)
|
||||
|
||||
# 删除分类(硬删除)
|
||||
await db.execute(
|
||||
delete(ScreenMaterialCategory).where(ScreenMaterialCategory.id == category_id)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class MaterialService(BaseService[ScreenMaterial, MaterialCreateIn, MaterialUpdateIn]):
|
||||
"""素材服务"""
|
||||
|
||||
model = ScreenMaterial
|
||||
|
||||
@classmethod
|
||||
async def get_list_by_category(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
category_id: str = None,
|
||||
) -> List[ScreenMaterial]:
|
||||
"""获取素材列表"""
|
||||
stmt = select(ScreenMaterial).where(ScreenMaterial.is_deleted == False)
|
||||
|
||||
if category_id:
|
||||
stmt = stmt.where(ScreenMaterial.category_id == category_id)
|
||||
|
||||
stmt = stmt.order_by(ScreenMaterial.sort.asc(), ScreenMaterial.sys_create_datetime.desc())
|
||||
result = await db.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def batch_delete(cls, db: AsyncSession, material_ids: List[str]) -> int:
|
||||
"""批量删除素材(硬删除)"""
|
||||
stmt = delete(ScreenMaterial).where(ScreenMaterial.id.in_(material_ids))
|
||||
result = await db.execute(stmt)
|
||||
return result.rowcount
|
||||
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
大屏设计器 API
|
||||
大屏项目的 CRUD、发布、复制、导入导出
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.config import settings
|
||||
from app.base_schema import PaginatedResponse
|
||||
from online_dev.screen_design.screen_model import ScreenProject
|
||||
from online_dev.screen_design.screen_schema import (
|
||||
ScreenProjectCreateIn,
|
||||
ScreenProjectUpdateIn,
|
||||
ScreenProjectOut,
|
||||
ScreenProjectListOut,
|
||||
ScreenPublishIn,
|
||||
ScreenSetPasswordIn,
|
||||
ScreenImportCheckIn,
|
||||
ScreenImportCheckOut,
|
||||
ScreenImportIn,
|
||||
)
|
||||
from online_dev.screen_design.screen_service import ScreenProjectService, ScreenServiceException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix='/screen', tags=['大屏设计器'])
|
||||
|
||||
|
||||
@router.get('/list', response_model=PaginatedResponse[ScreenProjectListOut], summary='大屏项目列表')
|
||||
async def list_projects(
|
||||
page: int = Query(default=1, ge=1, description="页码"),
|
||||
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"),
|
||||
name: str = Query(default=None, description="项目名称"),
|
||||
code: str = Query(default=None, description="项目编码"),
|
||||
status: str = Query(default=None, description="状态"),
|
||||
application_id: str = Query(default=None, alias="applicationId", description="应用ID"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""分页查询大屏项目列表(自动应用数据权限)"""
|
||||
# 构建过滤条件
|
||||
filters = []
|
||||
|
||||
# 应用过滤
|
||||
if application_id is not None:
|
||||
if application_id == "":
|
||||
filters.append(ScreenProject.application_id.is_(None))
|
||||
else:
|
||||
filters.append(ScreenProject.application_id == application_id)
|
||||
|
||||
if name:
|
||||
filters.append(ScreenProject.name.contains(name))
|
||||
if code:
|
||||
filters.append(ScreenProject.code.contains(code))
|
||||
if status:
|
||||
filters.append(ScreenProject.status == status)
|
||||
|
||||
# 使用带数据权限的列表查询
|
||||
items, total = await ScreenProjectService.get_list_with_data_scope(
|
||||
db, page=page, page_size=page_size, filters=filters
|
||||
)
|
||||
# 转换为输出格式
|
||||
result_items = [ScreenProjectListOut.from_orm_model(item) for item in items]
|
||||
return PaginatedResponse(items=result_items, total=total)
|
||||
|
||||
|
||||
@router.get('/code/{code}', response_model=ScreenProjectOut, summary='根据编码获取大屏项目')
|
||||
async def get_project_by_code(
|
||||
code: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""根据编码获取大屏项目详情"""
|
||||
project = await ScreenProjectService.get_by_code(db, code)
|
||||
if not project:
|
||||
raise HTTPException(status_code=400, detail=f"大屏项目不存在: {code}")
|
||||
return ScreenProjectOut.from_orm_model(project)
|
||||
|
||||
|
||||
@router.get('/{project_id}', response_model=ScreenProjectOut, summary='大屏项目详情')
|
||||
async def get_project(
|
||||
project_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取大屏项目详情"""
|
||||
project = await ScreenProjectService.get_by_id(db, project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=400, detail="大屏项目不存在")
|
||||
return ScreenProjectOut.from_orm_model(project)
|
||||
|
||||
|
||||
@router.post('', response_model=ScreenProjectOut, summary='创建大屏项目')
|
||||
async def create_project(
|
||||
data: ScreenProjectCreateIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""创建大屏项目"""
|
||||
# 检查编码唯一性
|
||||
if await ScreenProjectService.check_code_exists(db, data.code):
|
||||
raise HTTPException(status_code=400, detail=f"项目编码已存在: {data.code}")
|
||||
|
||||
project = await ScreenProjectService.create(db, data)
|
||||
await db.commit()
|
||||
logger.info(f"大屏项目创建成功: {project.code}")
|
||||
return ScreenProjectOut.from_orm_model(project)
|
||||
|
||||
|
||||
@router.put('/{project_id}', response_model=ScreenProjectOut, summary='更新大屏项目')
|
||||
async def update_project(
|
||||
project_id: str,
|
||||
data: ScreenProjectUpdateIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更新大屏项目"""
|
||||
project = await ScreenProjectService.update(db, project_id, data)
|
||||
if not project:
|
||||
raise HTTPException(status_code=400, detail="大屏项目不存在")
|
||||
await db.commit()
|
||||
logger.info(f"大屏项目更新成功: {project.code}")
|
||||
return ScreenProjectOut.from_orm_model(project)
|
||||
|
||||
|
||||
@router.delete('/{project_id}', response_model=ScreenProjectOut, summary='删除大屏项目')
|
||||
async def delete_project(
|
||||
project_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""删除大屏项目"""
|
||||
project = await ScreenProjectService.get_by_id(db, project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=400, detail="大屏项目不存在")
|
||||
|
||||
result = ScreenProjectOut.from_orm_model(project)
|
||||
await ScreenProjectService.delete(db, project_id)
|
||||
await db.commit()
|
||||
return result
|
||||
|
||||
|
||||
@router.delete('/batch', summary='批量删除大屏项目')
|
||||
async def batch_delete_projects(
|
||||
ids: List[str] = Query(..., description="项目ID列表"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""批量删除大屏项目"""
|
||||
count = await ScreenProjectService.batch_delete(db, ids)
|
||||
await db.commit()
|
||||
return {'count': count}
|
||||
|
||||
|
||||
@router.post('/{project_id}/publish', response_model=ScreenProjectOut, summary='发布大屏')
|
||||
async def publish_project(
|
||||
project_id: str,
|
||||
data: ScreenPublishIn = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""发布大屏"""
|
||||
try:
|
||||
access_password = data.access_password if data else None
|
||||
project = await ScreenProjectService.publish(db, project_id, access_password)
|
||||
await db.commit()
|
||||
return ScreenProjectOut.from_orm_model(project)
|
||||
except ScreenServiceException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post('/{project_id}/unpublish', response_model=ScreenProjectOut, summary='取消发布')
|
||||
async def unpublish_project(
|
||||
project_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""取消发布大屏"""
|
||||
try:
|
||||
project = await ScreenProjectService.unpublish(db, project_id)
|
||||
await db.commit()
|
||||
return ScreenProjectOut.from_orm_model(project)
|
||||
except ScreenServiceException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post('/{project_id}/set-password', response_model=ScreenProjectOut, summary='设置访问密码')
|
||||
async def set_access_password(
|
||||
project_id: str,
|
||||
data: ScreenSetPasswordIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""设置或清除大屏访问密码"""
|
||||
try:
|
||||
project = await ScreenProjectService.set_password(db, project_id, data.access_password)
|
||||
await db.commit()
|
||||
return ScreenProjectOut.from_orm_model(project)
|
||||
except ScreenServiceException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post('/{project_id}/verify-password', summary='验证访问密码')
|
||||
async def verify_access_password(
|
||||
project_id: str,
|
||||
data: ScreenSetPasswordIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""验证大屏访问密码"""
|
||||
try:
|
||||
is_valid = await ScreenProjectService.verify_password(db, project_id, data.access_password)
|
||||
return {'valid': is_valid}
|
||||
except ScreenServiceException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post('/{project_id}/copy', response_model=ScreenProjectOut, summary='复制大屏项目')
|
||||
async def copy_project(
|
||||
project_id: str,
|
||||
new_code: str = Query(..., alias="newCode", description="新项目编码"),
|
||||
new_name: str = Query(default=None, alias="newName", description="新项目名称"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""复制大屏项目"""
|
||||
try:
|
||||
project = await ScreenProjectService.copy(db, project_id, new_code, new_name)
|
||||
await db.commit()
|
||||
return ScreenProjectOut.from_orm_model(project)
|
||||
except ScreenServiceException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get('/{project_id}/export', summary='导出大屏配置')
|
||||
async def export_project_config(
|
||||
project_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""导出大屏配置为 JSON"""
|
||||
try:
|
||||
config = await ScreenProjectService.export_config(db, project_id)
|
||||
|
||||
content = json.dumps(config, ensure_ascii=False, indent=2)
|
||||
|
||||
return StreamingResponse(
|
||||
iter([content]),
|
||||
media_type='application/json',
|
||||
headers={
|
||||
'Content-Disposition': f'attachment; filename="{config["code"]}.json"'
|
||||
}
|
||||
)
|
||||
except ScreenServiceException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post('/import/check', response_model=ScreenImportCheckOut, summary='导入预检查')
|
||||
async def check_import_project_config(
|
||||
data: ScreenImportCheckIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""导入预检查:检查项目编码是否冲突"""
|
||||
try:
|
||||
return await ScreenProjectService.check_import(db, data.code)
|
||||
except ScreenServiceException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post('/import', response_model=ScreenProjectOut, summary='导入大屏配置')
|
||||
async def import_project_config(
|
||||
data: ScreenImportIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""导入大屏配置"""
|
||||
try:
|
||||
project = await ScreenProjectService.import_config(db, data.model_dump())
|
||||
await db.commit()
|
||||
return ScreenProjectOut.from_orm_model(project)
|
||||
except ScreenServiceException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
大屏设计器数据模型
|
||||
"""
|
||||
from sqlalchemy import Column, String, Text, Integer, JSON, DateTime
|
||||
|
||||
from app.base_model import BaseModel
|
||||
|
||||
|
||||
class ScreenProject(BaseModel):
|
||||
"""大屏项目"""
|
||||
__tablename__ = "screen_project"
|
||||
|
||||
# 所属应用(逻辑外键关联 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, index=True, nullable=False, comment="项目编码")
|
||||
description = Column(Text, default='', comment="项目描述")
|
||||
status = Column(String(20), default='draft', comment="状态: draft/published")
|
||||
version = Column(Integer, default=1, comment="版本号")
|
||||
|
||||
# 缩略图
|
||||
thumbnail = Column(Text, default='', comment="缩略图(Base64或URL)")
|
||||
|
||||
# 大屏配置(存储完整的设计器配置)
|
||||
screen_config = Column(JSON, default=dict, comment="大屏设计配置")
|
||||
|
||||
# 发布相关
|
||||
access_password = Column(String(100), default='', comment="访问密码")
|
||||
published_at = Column(DateTime, nullable=True, comment="发布时间")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ScreenProject {self.name} ({self.code})>"
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
大屏设计器 Schema 定义
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class ScreenProjectBase(BaseModel):
|
||||
"""大屏项目基础信息"""
|
||||
application_id: Optional[str] = Field(None, description="所属应用ID")
|
||||
name: str = Field(..., description="项目名称")
|
||||
code: str = Field(..., pattern=r"^[a-zA-Z][a-zA-Z0-9_]*$", description="项目编码(字母开头,只能包含字母、数字和下划线)")
|
||||
description: str = Field(default='', description="项目描述")
|
||||
sort: int = Field(default=0, description="排序")
|
||||
|
||||
|
||||
class ScreenProjectCreateIn(ScreenProjectBase):
|
||||
"""创建大屏项目请求"""
|
||||
screen_config: Dict[str, Any] = Field(default_factory=dict, description="大屏设计配置")
|
||||
thumbnail: str = Field(default='', description="缩略图")
|
||||
|
||||
|
||||
class ScreenProjectUpdateIn(BaseModel):
|
||||
"""更新大屏项目请求"""
|
||||
name: Optional[str] = Field(default=None, description="项目名称")
|
||||
description: Optional[str] = Field(default=None, description="项目描述")
|
||||
sort: Optional[int] = Field(default=None, description="排序")
|
||||
screen_config: Optional[Dict[str, Any]] = Field(default=None, description="大屏设计配置")
|
||||
thumbnail: Optional[str] = Field(default=None, description="缩略图")
|
||||
|
||||
|
||||
class ScreenProjectOut(BaseModel):
|
||||
"""大屏项目详情输出"""
|
||||
id: str
|
||||
application_id: Optional[str] = None
|
||||
name: str
|
||||
code: str
|
||||
description: str
|
||||
status: str
|
||||
version: int
|
||||
thumbnail: str
|
||||
screen_config: Dict[str, Any]
|
||||
sort: int
|
||||
access_password: str = Field(default='', description="访问密码")
|
||||
published_at: Optional[str] = Field(default=None, description="发布时间")
|
||||
sys_create_datetime: str
|
||||
sys_update_datetime: str
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def from_orm_model(cls, obj):
|
||||
"""从ORM模型转换"""
|
||||
return cls(
|
||||
id=str(obj.id),
|
||||
application_id=obj.application_id,
|
||||
name=obj.name,
|
||||
code=obj.code,
|
||||
description=obj.description or '',
|
||||
status=obj.status or 'draft',
|
||||
version=obj.version or 1,
|
||||
thumbnail=obj.thumbnail or '',
|
||||
screen_config=obj.screen_config or {},
|
||||
sort=obj.sort or 0,
|
||||
access_password=obj.access_password or '',
|
||||
published_at=obj.published_at.strftime('%Y-%m-%d %H:%M:%S') if obj.published_at else None,
|
||||
sys_create_datetime=obj.sys_create_datetime.strftime('%Y-%m-%d %H:%M:%S') if obj.sys_create_datetime else '',
|
||||
sys_update_datetime=obj.sys_update_datetime.strftime('%Y-%m-%d %H:%M:%S') if obj.sys_update_datetime else '',
|
||||
)
|
||||
|
||||
|
||||
class ScreenProjectListOut(BaseModel):
|
||||
"""大屏项目列表输出(不含screen_config)"""
|
||||
id: str
|
||||
application_id: Optional[str] = None
|
||||
name: str
|
||||
code: str
|
||||
description: str
|
||||
status: str
|
||||
version: int
|
||||
thumbnail: str
|
||||
sort: int
|
||||
access_password: str = Field(default='', description="访问密码")
|
||||
published_at: Optional[str] = Field(default=None, description="发布时间")
|
||||
sys_create_datetime: str
|
||||
sys_update_datetime: str
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def from_orm_model(cls, obj):
|
||||
"""从ORM模型转换"""
|
||||
return cls(
|
||||
id=str(obj.id),
|
||||
application_id=obj.application_id,
|
||||
name=obj.name,
|
||||
code=obj.code,
|
||||
description=obj.description or '',
|
||||
status=obj.status or 'draft',
|
||||
version=obj.version or 1,
|
||||
thumbnail=obj.thumbnail or '',
|
||||
sort=obj.sort or 0,
|
||||
access_password=obj.access_password or '',
|
||||
published_at=obj.published_at.strftime('%Y-%m-%d %H:%M:%S') if obj.published_at else None,
|
||||
sys_create_datetime=obj.sys_create_datetime.strftime('%Y-%m-%d %H:%M:%S') if obj.sys_create_datetime else '',
|
||||
sys_update_datetime=obj.sys_update_datetime.strftime('%Y-%m-%d %H:%M:%S') if obj.sys_update_datetime else '',
|
||||
)
|
||||
|
||||
|
||||
class ScreenExportOut(BaseModel):
|
||||
"""大屏配置导出"""
|
||||
name: str
|
||||
code: str
|
||||
description: str
|
||||
screen_config: Dict[str, Any]
|
||||
|
||||
|
||||
class ScreenImportCheckIn(BaseModel):
|
||||
"""大屏导入预检查请求"""
|
||||
code: str = Field(..., description="项目编码")
|
||||
|
||||
|
||||
class ScreenImportCheckOut(BaseModel):
|
||||
"""大屏导入预检查结果"""
|
||||
code_exists: bool = Field(..., description="项目编码是否已存在")
|
||||
can_import: bool = Field(..., description="是否可以直接导入(编码不冲突)")
|
||||
|
||||
|
||||
class ScreenImportIn(BaseModel):
|
||||
"""大屏配置导入"""
|
||||
application_id: Optional[str] = Field(None, description="所属应用ID")
|
||||
name: str = Field(..., description="项目名称")
|
||||
code: str = Field(..., description="项目编码")
|
||||
description: str = Field(default='', description="项目描述")
|
||||
screen_config: Dict[str, Any] = Field(default_factory=dict, description="大屏设计配置")
|
||||
|
||||
|
||||
class ScreenPublishIn(BaseModel):
|
||||
"""发布大屏请求"""
|
||||
access_password: Optional[str] = Field(default=None, description="访问密码")
|
||||
|
||||
|
||||
class ScreenSetPasswordIn(BaseModel):
|
||||
"""设置访问密码请求"""
|
||||
access_password: str = Field(default='', description="访问密码,空字符串表示清除密码")
|
||||
@@ -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