feat: restore source parity and harden agent runtime

This commit is contained in:
2026-06-22 11:17:26 +08:00
parent e33f08277b
commit 0793eb82d6
596 changed files with 168879 additions and 290 deletions
@@ -0,0 +1,388 @@
#!/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))