feat: restore source parity and harden agent runtime
This commit is contained in:
@@ -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))
|
||||
Reference in New Issue
Block a user