#!/usr/bin/env python # -*- coding: utf-8 -*- """报表管理 API""" import json import logging from typing import List from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi.responses import StreamingResponse from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from core.application.model import Application from core.menu.model import Menu from app.base_schema import PaginatedResponse from online_dev.report_manager.exceptions import ReportServiceException from online_dev.report_manager.schema import ( ReportImportCheckIn, ReportImportCheckOut, ReportImportIn, ReportPublishIn, ReportSaveIn, ReportSaveOut, ReportTemplateCreateIn, ReportTemplateListOut, ReportTemplateOut, ReportTemplateUpdateIn, ReportVersionListOut, ReportVersionOut, ) from online_dev.report_manager.service import ReportService from online_dev.report_manager.version_service import ReportVersionService from online_dev.report_manager.dataset_bridge import ReportDatasetBridge logger = logging.getLogger(__name__) router = APIRouter(prefix="/report", tags=["报表管理"]) def _fmt_dt(dt) -> str: return dt.strftime("%Y-%m-%d %H:%M:%S") if dt else "" def _build_template_out(tpl, active_version_id: str = None) -> dict: return { "id": str(tpl.id), "application_id": tpl.application_id, "name": tpl.name, "code": tpl.code, "category": tpl.category or "", "description": tpl.description or "", "status": tpl.status, "allow_export": bool(tpl.allow_export), "allow_print": bool(tpl.allow_print), "allow_watermark": bool(tpl.allow_watermark), "watermark_config": tpl.watermark_config or {}, "sort": tpl.sort or 0, "active_version_id": active_version_id, "sys_create_datetime": _fmt_dt(tpl.sys_create_datetime), "sys_update_datetime": _fmt_dt(tpl.sys_update_datetime), } def _build_version_out(version, datasets: list = None) -> dict: return { "id": str(version.id), "template_id": version.template_id, "version": version.version, "state": int(version.state), "snapshot": version.snapshot or {}, "cells": version.cells or {}, "query_list": version.query_list or [], "sort_list": version.sort_list or [], "column_list": version.column_list or [], "fence_list": version.fence_list or [], "convert_config": version.convert_config or {}, "datasets": datasets or [], "sys_create_datetime": _fmt_dt(version.sys_create_datetime), "sys_update_datetime": _fmt_dt(version.sys_update_datetime), } @router.get("/list", response_model=PaginatedResponse[ReportTemplateListOut], summary="报表列表") async def list_reports( application_id: str = Query(None, alias="applicationId"), name: str = Query(None), code: str = Query(None), category: str = Query(None), status: str = Query(None), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=500, alias="pageSize"), db: AsyncSession = Depends(get_db), ): result = await ReportService.list_with_data_scope( db, page=page, page_size=page_size, application_id=application_id, name=name, code=code, category=category, status=status, ) items = result["items"] app_ids = list({t.application_id for t in items if t.application_id}) app_map = {} if app_ids: rows = await db.execute( select(Application.id, Application.name, Application.code).where(Application.id.in_(app_ids)) ) app_map = {r.id: {"name": r.name, "code": r.code} for r in rows} menu_paths: set[str] = set() if items: paths = [f"/report-render/{t.code}" for t in items] menu_rows = await db.execute(select(Menu.path).where(Menu.path.in_(paths))) menu_paths = set(menu_rows.scalars().all()) out = [] for t in items: info = app_map.get(t.application_id, {}) out.append({ "id": str(t.id), "application_id": t.application_id, "application_name": info.get("name", ""), "application_code": info.get("code", ""), "name": t.name, "code": t.code, "category": t.category or "", "description": t.description or "", "status": t.status, "has_release_menu": f"/report-render/{t.code}" in menu_paths, "sort": t.sort or 0, "sys_create_datetime": _fmt_dt(t.sys_create_datetime), "sys_update_datetime": _fmt_dt(t.sys_update_datetime), }) return PaginatedResponse(items=out, total=result["total"]) @router.get("/categories", response_model=List[str], summary="分类列表") async def get_categories( application_id: str = Query(None, alias="applicationId"), db: AsyncSession = Depends(get_db), ): return await ReportService.get_categories(db, application_id) @router.get("/code/{code}", response_model=ReportTemplateOut, summary="按编码获取模板") async def get_by_code(code: str, db: AsyncSession = Depends(get_db)): try: tpl = await ReportService.get_by_code(db, code) vid = await ReportService.get_active_version_id(db, tpl.id) return _build_template_out(tpl, vid) except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.get("/{template_id}", response_model=ReportTemplateOut, summary="模板详情") async def get_template(template_id: str, db: AsyncSession = Depends(get_db)): try: tpl = await ReportService.get(db, template_id) vid = await ReportService.get_active_version_id(db, template_id) return _build_template_out(tpl, vid) except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("", response_model=ReportTemplateOut, summary="创建报表") async def create_template( request: Request, data: ReportTemplateCreateIn, db: AsyncSession = Depends(get_db), ): try: tpl = await ReportService.create(db, data.model_dump(), request.state.user_id) ver = await ReportVersionService.get_designing_or_latest(db, tpl.id) return _build_template_out(tpl, ver.id if ver else None) except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.put("/{template_id}", response_model=ReportTemplateOut, summary="更新模板") async def update_template( request: Request, template_id: str, data: ReportTemplateUpdateIn, db: AsyncSession = Depends(get_db), ): try: tpl = await ReportService.update( db, template_id, data.model_dump(exclude_none=True), request.state.user_id ) vid = await ReportService.get_active_version_id(db, template_id) return _build_template_out(tpl, vid) except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.delete("/batch", summary="批量删除") async def batch_delete(ids: List[str] = Query(...), db: AsyncSession = Depends(get_db)): count = await ReportService.batch_delete(db, ids) return {"count": count} @router.delete("/{template_id}", response_model=ReportTemplateOut, summary="删除报表") async def delete_template(template_id: str, db: AsyncSession = Depends(get_db)): try: tpl = await ReportService.get(db, template_id) out = _build_template_out(tpl) await ReportService.delete(db, template_id) return out except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.get("/{template_id}/versions", response_model=List[ReportVersionListOut], summary="版本列表") async def list_versions(template_id: str, db: AsyncSession = Depends(get_db)): try: await ReportService.get(db, template_id) versions = await ReportVersionService.list_by_template(db, template_id) return [ { "id": str(v.id), "template_id": v.template_id, "version": v.version, "state": int(v.state), "sys_create_datetime": _fmt_dt(v.sys_create_datetime), "sys_update_datetime": _fmt_dt(v.sys_update_datetime), } for v in versions ] except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.get("/version/{version_id}", response_model=ReportVersionOut, summary="版本详情") async def get_version(version_id: str, db: AsyncSession = Depends(get_db)): try: detail = await ReportVersionService.get_version_detail(db, version_id) return _build_version_out(detail["version"], detail["datasets"]) except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/save", response_model=ReportSaveOut, summary="保存版本") async def save_version( request: Request, data: ReportSaveIn, db: AsyncSession = Depends(get_db), ): try: payload = data.model_dump(by_alias=False) result = await ReportVersionService.save(db, payload, request.state.user_id) return result except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.delete("/version/{version_id}", summary="删除版本") async def delete_version(version_id: str, db: AsyncSession = Depends(get_db)): try: await ReportVersionService.delete_version(db, version_id) return {"success": True} except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/version/{version_id}/copy", response_model=ReportSaveOut, summary="复制版本") async def copy_version( request: Request, version_id: str, db: AsyncSession = Depends(get_db), ): """复制版本为新的「设计中」版本(对标 JNPF POST /Report/Info/{versionId})""" try: new_ver = await ReportVersionService.duplicate_version( db, version_id, request.state.user_id ) return { "template_id": new_ver.template_id, "version_id": str(new_ver.id), "state": int(new_ver.state), } except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/{template_id}/copy", response_model=ReportTemplateOut, summary="复制报表") async def copy_template( request: Request, template_id: str, new_code: str = Query(..., alias="newCode"), new_name: str = Query(None, alias="newName"), db: AsyncSession = Depends(get_db), ): try: tpl = await ReportService.copy(db, template_id, new_code, new_name, request.state.user_id) ver = await ReportVersionService.get_designing_or_latest(db, tpl.id) return _build_template_out(tpl, ver.id if ver else None) except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.get("/{template_id}/export", summary="导出报表配置") async def export_report_config( template_id: str, db: AsyncSession = Depends(get_db), ): try: config = await ReportService.export_config(db, template_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 ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/import/check", response_model=ReportImportCheckOut, summary="导入预检查") async def check_import(data: ReportImportCheckIn, db: AsyncSession = Depends(get_db)): return await ReportService.check_import(db, data.code) @router.post("/import", response_model=ReportTemplateOut, summary="导入报表配置") async def import_report_config( request: Request, data: ReportImportIn, db: AsyncSession = Depends(get_db), ): try: tpl = await ReportService.import_config( db, data.model_dump(), request.state.user_id, ) ver = await ReportVersionService.get_designing_or_latest(db, tpl.id) return _build_template_out(tpl, ver.id if ver else None) except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.get("/selector", summary="分类树形选择器") async def report_selector( application_id: str = Query(None, alias="applicationId"), db: AsyncSession = Depends(get_db), ): return await ReportService.get_selector(db, application_id) @router.get("/{template_id}/release-menu", summary="已发布菜单信息") async def get_release_menu(template_id: str, db: AsyncSession = Depends(get_db)): try: return await ReportService.get_release_menu(db, template_id) except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/{template_id}/publish", response_model=ReportTemplateOut, summary="发布到菜单") async def publish_template( template_id: str, data: ReportPublishIn, db: AsyncSession = Depends(get_db), ): try: tpl = await ReportService.publish( db, template_id, { "menu_name": data.menu_name, "menu_parent_id": data.menu_parent_id, "menu_icon": data.menu_icon, "menu_order": data.menu_order, }, ) ver = await ReportVersionService.get_active(db, tpl.id) return _build_template_out(tpl, ver.id if ver else None) except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/{template_id}/unpublish", response_model=ReportTemplateOut, summary="取消发布") async def unpublish_template(template_id: str, db: AsyncSession = Depends(get_db)): try: tpl = await ReportService.unpublish(db, template_id) ver = await ReportVersionService.get_designing_or_latest(db, tpl.id) return _build_template_out(tpl, ver.id if ver else None) except ReportServiceException as e: raise HTTPException(status_code=400, detail=str(e))