#!/usr/bin/env python # -*- coding: utf-8 -*- """报表版本管理服务""" import copy import json import logging from typing import Any, Dict, List, Optional from sqlalchemy import select, update, func, and_ from sqlalchemy.ext.asyncio import AsyncSession from online_dev.report_manager.model import ReportTemplate, ReportVersion, ReportDataset from online_dev.report_manager.enums import ReportVersionState from online_dev.report_manager.exceptions import ReportServiceException from online_dev.report_manager.constants import default_snapshot, default_cells from online_dev.report_manager.dataset_bridge import ReportDatasetBridge logger = logging.getLogger(__name__) class ReportVersionService: @staticmethod async def get(db: AsyncSession, version_id: str) -> ReportVersion: stmt = select(ReportVersion).where( ReportVersion.id == version_id, ReportVersion.is_deleted == False, ) ver = (await db.execute(stmt)).scalar_one_or_none() if not ver: raise ReportServiceException(f"版本不存在: {version_id}") return ver @staticmethod async def list_by_template(db: AsyncSession, template_id: str) -> List[ReportVersion]: stmt = ( select(ReportVersion) .where( ReportVersion.template_id == template_id, ReportVersion.is_deleted == False, ) .order_by(ReportVersion.version.desc()) ) return list((await db.execute(stmt)).scalars().all()) @staticmethod async def get_active(db: AsyncSession, template_id: str) -> Optional[ReportVersion]: stmt = select(ReportVersion).where( ReportVersion.template_id == template_id, ReportVersion.state == ReportVersionState.ACTIVE, ReportVersion.is_deleted == False, ) return (await db.execute(stmt)).scalar_one_or_none() @staticmethod async def get_designing_or_latest(db: AsyncSession, template_id: str) -> Optional[ReportVersion]: stmt = select(ReportVersion).where( ReportVersion.template_id == template_id, ReportVersion.is_deleted == False, ).order_by( ReportVersion.state.asc(), ReportVersion.version.desc(), ) versions = list((await db.execute(stmt)).scalars().all()) for v in versions: if v.state == ReportVersionState.DESIGNING: return v return versions[0] if versions else None @staticmethod def _parse_json_field(value: Any, default: Any) -> Any: if value is None: return default if isinstance(value, str): try: return json.loads(value) except json.JSONDecodeError: return default return value @staticmethod async def save(db: AsyncSession, data: Dict[str, Any], user_id: str = None) -> Dict[str, Any]: from online_dev.report_manager.service import ReportService template_id = data.get("id") tpl = await ReportService.get(db, template_id) version_id = data.get("version_id") save_type = data.get("type", 0) snapshot = ReportVersionService._parse_json_field(data.get("snapshot"), default_snapshot()) cells = ReportVersionService._parse_json_field(data.get("cells"), default_cells()) query_list = ReportVersionService._parse_json_field(data.get("query_list"), []) sort_list = ReportVersionService._parse_json_field(data.get("sort_list"), []) column_list = ReportVersionService._parse_json_field(data.get("column_list"), []) fence_list = ReportVersionService._parse_json_field(data.get("fence_list"), []) convert_config = ReportVersionService._parse_json_field(data.get("convert_config"), {}) data_set_list = data.get("data_set_list") or [] if version_id: version = await ReportVersionService.get(db, version_id) if version.template_id != template_id: raise ReportServiceException("版本与模板不匹配") if version.state != ReportVersionState.DESIGNING: raise ReportServiceException("只能编辑设计中版本") else: designing = await ReportVersionService.get_designing_or_latest(db, template_id) if designing and designing.state == ReportVersionState.DESIGNING: version = designing else: max_ver = ( await db.execute( select(func.max(ReportVersion.version)).where( ReportVersion.template_id == template_id, ReportVersion.is_deleted == False, ) ) ).scalar() or 0 version = ReportVersion( template_id=template_id, version=max_ver + 1, state=ReportVersionState.DESIGNING, snapshot=default_snapshot(), cells=default_cells(), sys_creator_id=user_id, sys_modifier_id=user_id, ) db.add(version) await db.flush() version.snapshot = snapshot version.cells = cells version.query_list = query_list version.sort_list = sort_list version.column_list = column_list version.fence_list = fence_list version.convert_config = convert_config version.sys_modifier_id = user_id await ReportDatasetBridge.sync_datasets(db, version.id, data_set_list) if save_type == 1: stmt = update(ReportVersion).where( ReportVersion.template_id == template_id, ReportVersion.state == ReportVersionState.ACTIVE, ReportVersion.is_deleted == False, ).values(state=ReportVersionState.ARCHIVED) await db.execute(stmt) version.state = ReportVersionState.ACTIVE await db.flush() max_ver = ( await db.execute( select(func.max(ReportVersion.version)).where( ReportVersion.template_id == template_id, ReportVersion.is_deleted == False, ) ) ).scalar() or version.version new_designing = ReportVersion( template_id=template_id, version=max_ver + 1, state=ReportVersionState.DESIGNING, snapshot=copy.deepcopy(version.snapshot), cells=copy.deepcopy(version.cells), query_list=copy.deepcopy(version.query_list), sort_list=copy.deepcopy(version.sort_list), column_list=copy.deepcopy(version.column_list), fence_list=copy.deepcopy(version.fence_list), convert_config=copy.deepcopy(version.convert_config), sys_creator_id=user_id, sys_modifier_id=user_id, ) db.add(new_designing) await db.flush() await ReportDatasetBridge.copy_datasets(db, version.id, new_designing.id) version = new_designing else: version.state = ReportVersionState.DESIGNING await db.commit() await db.refresh(version) return { "template_id": template_id, "version_id": version.id, "state": int(version.state), } @staticmethod async def get_version_detail(db: AsyncSession, version_id: str) -> Dict[str, Any]: version = await ReportVersionService.get(db, version_id) datasets = await ReportDatasetBridge.list_by_version(db, version_id) return { "version": version, "datasets": datasets, } @staticmethod async def delete_version(db: AsyncSession, version_id: str) -> bool: version = await ReportVersionService.get(db, version_id) if version.state in (ReportVersionState.ACTIVE, ReportVersionState.ARCHIVED): raise ReportServiceException("不能删除启用中或已归档版本") count_stmt = select(func.count(ReportVersion.id)).where( ReportVersion.template_id == version.template_id, ReportVersion.is_deleted == False, ) if (await db.execute(count_stmt)).scalar() <= 1: raise ReportServiceException("不能删除最后一个版本") version.is_deleted = True await ReportDatasetBridge.delete_by_version(db, version_id) await db.commit() return True @staticmethod async def copy_version_content( db: AsyncSession, src_version_id: str, new_template_id: str, user_id: str = None, ) -> None: src = await ReportVersionService.get(db, src_version_id) new_ver = ( await db.execute( select(ReportVersion).where( ReportVersion.template_id == new_template_id, ReportVersion.is_deleted == False, ) ) ).scalar_one_or_none() if not new_ver: return new_ver.snapshot = src.snapshot new_ver.cells = src.cells new_ver.query_list = src.query_list new_ver.sort_list = src.sort_list new_ver.column_list = src.column_list new_ver.fence_list = src.fence_list new_ver.convert_config = src.convert_config await ReportDatasetBridge.copy_datasets(db, src_version_id, new_ver.id) await db.commit() @staticmethod async def duplicate_version( db: AsyncSession, src_version_id: str, user_id: str = None, ) -> ReportVersion: """复制版本为新的「设计中」版本(同模板)。""" src = await ReportVersionService.get(db, src_version_id) template_id = src.template_id await db.execute( update(ReportVersion).where( ReportVersion.template_id == template_id, ReportVersion.state == ReportVersionState.DESIGNING, ReportVersion.is_deleted == False, ).values(state=ReportVersionState.ARCHIVED) ) max_ver = ( await db.execute( select(func.max(ReportVersion.version)).where( ReportVersion.template_id == template_id, ReportVersion.is_deleted == False, ) ) ).scalar() or 0 new_ver = ReportVersion( template_id=template_id, version=max_ver + 1, state=ReportVersionState.DESIGNING, snapshot=copy.deepcopy(src.snapshot), cells=copy.deepcopy(src.cells), query_list=copy.deepcopy(src.query_list), sort_list=copy.deepcopy(src.sort_list), column_list=copy.deepcopy(src.column_list), fence_list=copy.deepcopy(src.fence_list), convert_config=copy.deepcopy(src.convert_config), sys_creator_id=user_id, sys_modifier_id=user_id, ) db.add(new_ver) await db.flush() await ReportDatasetBridge.copy_datasets(db, src.id, new_ver.id) await db.commit() await db.refresh(new_ver) return new_ver