Files
ai-agent-admin/backend-fastapi/online_dev/document_generator/api.py
T

820 lines
30 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
文档生成API接口
"""
from typing import Optional, List
from datetime import datetime
import json
import logging
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
import io
from app.database import get_db
from app.config import settings
from app.base_schema import PaginatedResponse, ResponseModel
from online_dev.document_generator.schema import (
DocumentTemplateCreate, DocumentTemplateUpdate, DocumentTemplateOut, DocumentTemplateListOut,
DocumentTemplateImportCheckIn, DocumentTemplateImportCheckOut, DocumentTemplateImportIn,
GeneratedDocumentOut, GenerateDocumentIn, BatchGenerateDocumentIn, PreviewDocumentIn,
TemplateCategory,
)
from online_dev.document_generator.service import (
DocumentTemplateService, GeneratedDocumentService
)
from online_dev.document_generator.generator import pdf_generator
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/document-generator", tags=["文档生成"])
# ==================== 文档模板 ====================
@router.get("/templates", response_model=PaginatedResponse[DocumentTemplateListOut], summary="获取模板列表")
async def get_template_list(
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"),
application_id: Optional[str] = Query(default=None, alias="applicationId", description="应用ID"),
category: Optional[str] = Query(default=None, description="分类"),
status: Optional[str] = Query(default=None, description="状态"),
form_code: Optional[str] = Query(default=None, alias="formCode", description="表单编码"),
workflow_code: Optional[str] = Query(default=None, alias="workflowCode", description="流程编码"),
keyword: Optional[str] = Query(default=None, description="关键词"),
is_builtin: Optional[bool] = Query(default=None, alias="isBuiltin", description="是否内置"),
db: AsyncSession = Depends(get_db),
):
"""获取文档模板列表"""
items, total = await DocumentTemplateService.get_list(
db,
page=page,
page_size=page_size,
application_id=application_id,
category=category,
status=status,
form_code=form_code,
workflow_code=workflow_code,
keyword=keyword,
is_builtin=is_builtin,
)
return PaginatedResponse(items=items, total=total)
@router.get("/templates/categories", response_model=List[TemplateCategory], summary="获取模板分类")
async def get_template_categories(
application_id: Optional[str] = Query(default=None, alias="applicationId"),
db: AsyncSession = Depends(get_db),
):
"""获取模板分类统计"""
return await DocumentTemplateService.get_categories(db, application_id)
@router.get("/templates/builtin", response_model=List[DocumentTemplateListOut], summary="获取内置模板")
async def get_builtin_templates(
db: AsyncSession = Depends(get_db),
):
"""获取内置模板列表"""
items, _ = await DocumentTemplateService.get_list(db, page=1, page_size=100, is_builtin=True)
return items
@router.get("/templates/by-form/{form_code}", response_model=List[DocumentTemplateListOut], summary="根据表单编码获取模板")
async def get_templates_by_form_code(
form_code: str,
db: AsyncSession = Depends(get_db),
):
"""根据表单编码获取绑定的已发布单据模板列表"""
templates = await DocumentTemplateService.get_by_form_code(db, form_code)
return templates
@router.get("/templates/check-form/{form_code}", response_model=ResponseModel, summary="检查表单是否绑定模板")
async def check_form_has_templates(
form_code: str,
db: AsyncSession = Depends(get_db),
):
"""检查表单是否绑定了已发布的单据模板"""
has_templates = await DocumentTemplateService.has_templates_by_form_code(db, form_code)
return ResponseModel(message="success", data={"hasTemplates": has_templates})
@router.post("/templates", response_model=DocumentTemplateOut, summary="创建模板")
async def create_template(
data: DocumentTemplateCreate,
db: AsyncSession = Depends(get_db),
):
"""创建文档模板"""
# 检查编码唯一性
existing = await DocumentTemplateService.get_by_code(db, data.code)
if existing:
raise HTTPException(status_code=400, detail="模板编码已存在")
template = await DocumentTemplateService.create(db, data)
return template
@router.get("/templates/{template_id}", response_model=DocumentTemplateOut, summary="获取模板详情")
async def get_template(
template_id: str,
db: AsyncSession = Depends(get_db),
):
"""获取模板详情"""
template = await DocumentTemplateService.get_by_id(db, template_id)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
return template
@router.get("/templates/code/{code}", response_model=DocumentTemplateOut, summary="根据编码获取模板")
async def get_template_by_code(
code: str,
db: AsyncSession = Depends(get_db),
):
"""根据编码获取模板"""
template = await DocumentTemplateService.get_by_code(db, code)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
return template
@router.put("/templates/{template_id}", response_model=DocumentTemplateOut, summary="更新模板")
async def update_template(
template_id: str,
data: DocumentTemplateUpdate,
db: AsyncSession = Depends(get_db),
):
"""更新模板"""
template = await DocumentTemplateService.update(db, template_id, data)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
return template
@router.delete("/templates/{template_id}", response_model=ResponseModel, summary="删除模板")
async def delete_template(
template_id: str,
db: AsyncSession = Depends(get_db),
):
"""删除模板"""
template = await DocumentTemplateService.get_by_id(db, template_id)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
if template.is_builtin:
raise HTTPException(status_code=400, detail="内置模板不能删除")
await DocumentTemplateService.delete(db, template_id)
return ResponseModel(message="删除成功")
@router.post("/templates/{template_id}/publish", response_model=DocumentTemplateOut, summary="发布模板")
async def publish_template(
template_id: str,
db: AsyncSession = Depends(get_db),
):
"""发布模板"""
template = await DocumentTemplateService.publish(db, template_id)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
return template
@router.post("/templates/{template_id}/unpublish", response_model=DocumentTemplateOut, summary="取消发布")
async def unpublish_template(
template_id: str,
db: AsyncSession = Depends(get_db),
):
"""取消发布模板"""
template = await DocumentTemplateService.unpublish(db, template_id)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
return template
@router.post("/templates/{template_id}/copy", response_model=DocumentTemplateOut, summary="复制模板")
async def copy_template(
template_id: str,
new_code: str = Query(..., alias="newCode", description="新编码"),
new_name: str = Query(..., alias="newName", description="新名称"),
db: AsyncSession = Depends(get_db),
):
"""复制模板"""
# 检查新编码唯一性
existing = await DocumentTemplateService.get_by_code(db, new_code)
if existing:
raise HTTPException(status_code=400, detail="模板编码已存在")
template = await DocumentTemplateService.copy(db, template_id, new_code, new_name)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
return template
@router.get("/templates/{template_id}/export", summary="导出单据模板配置")
async def export_template_config(
template_id: str,
db: AsyncSession = Depends(get_db),
):
"""导出单据模板配置为 JSON 文件"""
config = await DocumentTemplateService.export_config(db, template_id)
if not config:
raise HTTPException(status_code=404, detail="模板不存在")
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"'
},
)
@router.post(
"/templates/import/check",
response_model=DocumentTemplateImportCheckOut,
summary="单据模板导入预检查",
)
async def check_import_template_config(
data: DocumentTemplateImportCheckIn,
db: AsyncSession = Depends(get_db),
):
"""导入预检查:检查模板编码是否冲突"""
return await DocumentTemplateService.check_import(db, data.code)
@router.post(
"/templates/import",
response_model=DocumentTemplateOut,
summary="导入单据模板配置",
)
async def import_template_config(
data: DocumentTemplateImportIn,
db: AsyncSession = Depends(get_db),
):
"""导入单据模板配置"""
try:
return await DocumentTemplateService.import_config(db, data.model_dump())
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# ==================== 文档生成 ====================
@router.post("/generate", response_model=GeneratedDocumentOut, summary="生成文档")
async def generate_document(
data: GenerateDocumentIn,
db: AsyncSession = Depends(get_db),
):
"""生成文档"""
from core.file_manager.service import FileManagerService
# 获取模板
template = await DocumentTemplateService.get_by_id(db, data.template_id)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
if template.status != "published":
raise HTTPException(status_code=400, detail="模板未发布")
# 删除同模板+同表单数据的旧单据(每个模板只保留一个单据)
if data.form_data_id:
existing_docs = await GeneratedDocumentService.get_by_template_and_form_data(
db, data.template_id, data.form_data_id
)
for old_doc in existing_docs:
# 删除关联的文件
if old_doc.file_id:
try:
await FileManagerService.delete_item(db, old_doc.file_id, hard=True, is_superuser=True)
except Exception as e:
logger.warning(f"删除旧单据文件失败: {e}")
# 删除单据记录
await GeneratedDocumentService.delete(db, old_doc.id, hard=True, auto_commit=False)
# 加载表单数据
form_data = {}
if data.form_data_id and template.form_code:
form_data = await _load_form_data(db, template.form_code, data.form_data_id)
# 加载流程实例数据
if data.instance_id:
instance_data = await _load_instance_data(db, data.instance_id)
form_data.update(instance_data)
# 添加签名文件URL
if data.signature_file_ids:
for field_name, file_id in data.signature_file_ids.items():
# 获取文件URL
file_info = await FileManagerService.get_by_id(db, file_id)
if file_info:
form_data[field_name] = file_info.url or ""
# 执行计算规则(聚合字段和计算字段)
template_data = dict(form_data)
if template.calculation_rules:
try:
from online_dev.document_generator.calculation_engine import CalculationEngine
logger.info(f"开始执行计算规则: {template.calculation_rules}")
calculated_values = await CalculationEngine.calculate_all(
template.calculation_rules,
form_data
)
# 将计算结果合并到模板数据中
template_data.update(calculated_values)
logger.info(f"生成文档计算完成,计算结果: {calculated_values}")
except Exception as calc_error:
logger.error(f"生成文档计算规则执行失败: {calc_error}", exc_info=True)
# 生成 PDF
try:
page_config = {
"size": template.page_size,
"orientation": template.page_orientation,
"margin": template.page_margin or {"top": 20, "right": 20, "bottom": 20, "left": 20},
}
logger.info(
f"[生成文档] 开始生成PDF | template_id={template.id}, "
f"template_type={template.template_type}, data_keys={list(template_data.keys())}, "
f"page_config={page_config}"
)
pdf_bytes = pdf_generator.generate(
template_type=template.template_type,
template_content=template.template_content or "",
data=template_data,
css=template.template_css,
page_config=page_config,
)
logger.info(f"[生成文档] PDF生成成功 | template_id={template.id}, 大小={len(pdf_bytes)} bytes")
except Exception as e:
logger.error(
f"[生成文档] PDF生成失败 | template_id={template.id}, "
f"template_type={template.template_type}, error={type(e).__name__}: {e}",
exc_info=True,
)
raise HTTPException(status_code=500, detail=f"PDF生成失败: {str(e)}")
# 获取页数
page_count = pdf_generator.get_page_count(pdf_bytes)
# 保存文件
document_name = data.document_name or f"{template.name}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
filename = f"{document_name}.pdf"
file_record = await FileManagerService.upload_file(
db=db,
file_content=pdf_bytes,
filename=filename,
file_size=len(pdf_bytes),
parent_id=None,
is_public=False,
source="document",
)
# 创建文档记录
document = await GeneratedDocumentService.create_document(
db=db,
template=template,
file_id=file_record.id,
file_size=len(pdf_bytes),
page_count=page_count,
document_name=document_name,
form_data_id=data.form_data_id,
instance_id=data.instance_id,
generate_type="manual",
)
return document
@router.post("/preview", summary="预览文档")
async def preview_document(
data: PreviewDocumentIn,
db: AsyncSession = Depends(get_db),
):
"""预览文档(返回 PDF 流)
支持两种方式:
1. 传入 template_id,从数据库读取模板
2. 传入 template_content,直接使用传入的 JSON 配置(无需保存)
"""
template_content = ""
template_type = "designer"
template_css = None
page_config = {"top": 20, "right": 20, "bottom": 20, "left": 20}
if data.template_content:
# 直接使用传入的 JSON 配置
template_content = data.template_content
# 从 JSON 中解析页面配置
try:
import json
config = json.loads(data.template_content)
page_config = {
"size": config.get("pageSize", "A4"),
"orientation": config.get("pageOrientation", "portrait"),
"margin": config.get("pageMargin", {"top": 20, "right": 20, "bottom": 20, "left": 20}),
"customPageWidth": config.get("customPageWidth"),
"customPageHeight": config.get("customPageHeight"),
"showPageNumber": config.get("showPageNumber"),
"pageNumberPosition": config.get("pageNumberPosition"),
"pageNumberAlign": config.get("pageNumberAlign"),
"pageNumberFormat": config.get("pageNumberFormat"),
"pageNumberFontSize": config.get("pageNumberFontSize"),
"pageNumberColor": config.get("pageNumberColor"),
}
except json.JSONDecodeError:
pass
elif data.template_id:
# 从数据库读取模板
template = await DocumentTemplateService.get_by_id(db, data.template_id)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
template_content = template.template_content or ""
template_type = template.template_type
template_css = template.template_css
page_config = {
"size": template.page_size,
"orientation": template.page_orientation,
"margin": template.page_margin or {"top": 20, "right": 20, "bottom": 20, "left": 20},
}
else:
raise HTTPException(status_code=400, detail="请提供 template_id 或 template_content")
# 加载数据
form_data = data.test_data or {}
if data.template_id:
template = await DocumentTemplateService.get_by_id(db, data.template_id)
if not form_data and data.form_data_id and template and template.form_code:
form_data = await _load_form_data(db, template.form_code, data.form_data_id)
if data.instance_id:
instance_data = await _load_instance_data(db, data.instance_id)
form_data.update(instance_data)
# 执行计算规则
template_data = dict(form_data)
calculation_rules = None
# 优先使用请求中的 calculation_rules(用于未保存的模板预览)
if data.calculation_rules:
calculation_rules = data.calculation_rules
logger.info(f"使用请求中的计算规则: {calculation_rules}")
# 否则从模板中获取
elif data.template_id:
template = await DocumentTemplateService.get_by_id(db, data.template_id)
if template and template.calculation_rules:
calculation_rules = template.calculation_rules
logger.info(f"使用模板中的计算规则: {calculation_rules}")
# 执行计算
if calculation_rules:
try:
from online_dev.document_generator.calculation_engine import CalculationEngine
logger.info(f"开始执行计算规则")
calculated_values = await CalculationEngine.calculate_all(
calculation_rules,
form_data
)
# 将计算结果合并到模板数据中
template_data.update(calculated_values)
logger.info(f"预览计算完成,计算结果: {calculated_values}")
except Exception as calc_error:
logger.error(f"预览计算规则执行失败: {calc_error}", exc_info=True)
else:
logger.info(f"没有计算规则需要执行")
# 生成 PDF
try:
logger.info(
f"[预览文档] 开始生成PDF | template_id={data.template_id}, "
f"template_type={template_type}, has_template_content={bool(data.template_content)}, "
f"data_keys={list(template_data.keys())}, has_css={template_css is not None}, "
f"page_config={page_config}"
)
pdf_bytes = pdf_generator.generate(
template_type=template_type,
template_content=template_content,
data=template_data,
css=template_css,
page_config=page_config,
)
logger.info(f"[预览文档] PDF生成成功 | 大小={len(pdf_bytes)} bytes")
except Exception as e:
logger.error(
f"[预览文档] PDF生成失败 | template_id={data.template_id}, "
f"template_type={template_type}, error={type(e).__name__}: {e}",
exc_info=True,
)
raise HTTPException(status_code=500, detail=f"PDF生成失败: {str(e)}")
return StreamingResponse(
io.BytesIO(pdf_bytes),
media_type="application/pdf",
headers={"Content-Disposition": f"inline; filename=preview.pdf"}
)
@router.post("/preview-html", summary="预览HTML")
async def preview_html(
data: PreviewDocumentIn,
db: AsyncSession = Depends(get_db),
):
"""预览文档HTML(返回 HTML 字符串)
支持两种方式:
1. 传入 template_id,从数据库读取模板
2. 传入 template_content,直接使用传入的 JSON 配置(无需保存)
"""
template_content = ""
template_type = "designer"
template_css = None
if data.template_content:
# 直接使用传入的 JSON 配置
template_content = data.template_content
elif data.template_id:
# 从数据库读取模板
template = await DocumentTemplateService.get_by_id(db, data.template_id)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
template_content = template.template_content or ""
template_type = template.template_type
template_css = template.template_css
else:
raise HTTPException(status_code=400, detail="请提供 template_id 或 template_content")
# 加载数据
form_data = data.test_data or {}
if data.template_id:
template = await DocumentTemplateService.get_by_id(db, data.template_id)
if not form_data and data.form_data_id and template and template.form_code:
form_data = await _load_form_data(db, template.form_code, data.form_data_id)
if data.instance_id:
instance_data = await _load_instance_data(db, data.instance_id)
form_data.update(instance_data)
# 执行计算规则
template_data = dict(form_data)
calculation_rules = None
# 优先使用请求中的 calculation_rules(用于未保存的模板预览)
if data.calculation_rules:
calculation_rules = data.calculation_rules
# 否则从模板中获取
elif data.template_id:
template = await DocumentTemplateService.get_by_id(db, data.template_id)
if template and template.calculation_rules:
calculation_rules = template.calculation_rules
# 执行计算
if calculation_rules:
try:
from online_dev.document_generator.calculation_engine import CalculationEngine
calculated_values = await CalculationEngine.calculate_all(
calculation_rules,
form_data
)
# 将计算结果合并到模板数据中
template_data.update(calculated_values)
logger.info(f"预览HTML计算完成,计算结果: {calculated_values}")
except Exception as calc_error:
logger.error(f"预览HTML计算规则执行失败: {calc_error}", exc_info=True)
# 生成 HTML
try:
from online_dev.document_generator.generator import DesignerTemplateRenderer
if template_type == "designer":
html_content = DesignerTemplateRenderer.render(
template_content,
template_data,
template_css,
)
else:
from online_dev.document_generator.generator import TemplateEngine
engine = TemplateEngine()
html_content = engine.render_html(template_content, template_data)
return {"html": html_content}
except Exception as e:
raise HTTPException(status_code=500, detail=f"HTML生成失败: {str(e)}")
@router.post("/batch-generate", response_model=ResponseModel, summary="批量生成文档")
async def batch_generate_documents(
data: BatchGenerateDocumentIn,
db: AsyncSession = Depends(get_db),
):
"""批量生成文档"""
template = await DocumentTemplateService.get_by_id(db, data.template_id)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
if template.status != "published":
raise HTTPException(status_code=400, detail="模板未发布")
success_count = 0
fail_count = 0
for form_data_id in data.form_data_ids:
try:
await generate_document(
GenerateDocumentIn(template_id=data.template_id, form_data_id=form_data_id),
db=db,
)
success_count += 1
except Exception:
fail_count += 1
return ResponseModel(
message=f"批量生成完成: 成功 {success_count} 个, 失败 {fail_count}",
data={"success": success_count, "fail": fail_count}
)
# ==================== 生成的文档 ====================
@router.get("/documents", response_model=PaginatedResponse[GeneratedDocumentOut], summary="获取文档列表")
async def get_document_list(
page: int = Query(default=1, ge=1),
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize"),
template_id: Optional[str] = Query(default=None, alias="templateId"),
form_code: Optional[str] = Query(default=None, alias="formCode"),
form_data_id: Optional[str] = Query(default=None, alias="formDataId"),
instance_id: Optional[str] = Query(default=None, alias="instanceId"),
keyword: Optional[str] = Query(default=None),
db: AsyncSession = Depends(get_db),
):
"""获取生成的文档列表"""
items, total = await GeneratedDocumentService.get_list(
db,
page=page,
page_size=page_size,
template_id=template_id,
form_code=form_code,
form_data_id=form_data_id,
instance_id=instance_id,
keyword=keyword,
)
return PaginatedResponse(items=items, total=total)
@router.get("/documents/{document_id}", response_model=GeneratedDocumentOut, summary="获取文档详情")
async def get_document(
document_id: str,
db: AsyncSession = Depends(get_db),
):
"""获取文档详情"""
document = await GeneratedDocumentService.get_by_id(db, document_id)
if not document:
raise HTTPException(status_code=404, detail="文档不存在")
return document
@router.get("/documents/{document_id}/download", summary="下载文档")
async def download_document(
document_id: str,
db: AsyncSession = Depends(get_db),
):
"""下载文档"""
from core.file_manager.service import FileManagerService
document = await GeneratedDocumentService.get_by_id(db, document_id)
if not document:
raise HTTPException(status_code=404, detail="文档不存在")
# 获取文件内容
file_content = await FileManagerService.get_file_content(db, document.file_id)
if not file_content:
raise HTTPException(status_code=404, detail="文件不存在")
# 增加下载次数
await GeneratedDocumentService.increment_download_count(db, document_id)
filename = f"{document.document_name}.pdf"
return StreamingResponse(
io.BytesIO(file_content),
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
@router.delete("/documents/{document_id}", response_model=ResponseModel, summary="删除文档")
async def delete_document(
document_id: str,
db: AsyncSession = Depends(get_db),
):
"""删除文档"""
document = await GeneratedDocumentService.get_by_id(db, document_id)
if not document:
raise HTTPException(status_code=404, detail="文档不存在")
await GeneratedDocumentService.delete(db, document_id)
return ResponseModel(message="删除成功")
@router.post("/documents/{document_id}/regenerate", response_model=GeneratedDocumentOut, summary="重新生成文档")
async def regenerate_document(
document_id: str,
db: AsyncSession = Depends(get_db),
):
"""重新生成文档"""
document = await GeneratedDocumentService.get_by_id(db, document_id)
if not document:
raise HTTPException(status_code=404, detail="文档不存在")
# 使用原文档的参数重新生成
new_document = await generate_document(
GenerateDocumentIn(
template_id=document.template_id,
form_data_id=document.form_data_id,
instance_id=document.instance_id,
document_name=document.document_name,
),
db=db,
)
# 删除旧文档
await GeneratedDocumentService.delete(db, document_id)
return new_document
# ==================== 辅助函数 ====================
async def _load_form_data(db: AsyncSession, form_code: str, form_data_id: str) -> dict:
"""加载表单数据(含关联选择器的显示名称)"""
try:
from online_dev.form_data_manager.service import FormDataService
service = await FormDataService.create_service(db, form_code)
data = await service.get(db, form_data_id)
return data or {}
except Exception as e:
logger.error(f"加载表单数据失败 form_code={form_code}, id={form_data_id}: {e}")
return {}
async def _load_instance_data(db: AsyncSession, instance_id: str) -> dict:
"""加载流程实例数据"""
try:
from online_dev.workflow.model import WorkflowInstance, WorkflowLog
from sqlalchemy import select
# 获取实例
stmt = select(WorkflowInstance).where(WorkflowInstance.id == instance_id)
result = await db.execute(stmt)
instance = result.scalar_one_or_none()
if not instance:
return {}
# 获取审批日志
log_stmt = select(WorkflowLog).where(
WorkflowLog.instance_id == instance_id
).order_by(WorkflowLog.sys_create_datetime)
log_result = await db.execute(log_stmt)
logs = list(log_result.scalars().all())
return {
"_instance": {
"id": instance.id,
"instance_no": instance.instance_no,
"title": instance.title,
"status": instance.status,
"started_at": instance.started_at,
"completed_at": instance.completed_at,
},
"_logs": [
{
"node_name": log.node_name,
"action": log.action,
"comment": log.comment,
"created_at": log.sys_create_datetime,
}
for log in logs
],
}
except Exception:
return {}
# ==================== 系统字体 ====================
@router.get("/fonts", summary="获取服务器支持的字体列表")
async def get_available_fonts():
"""获取服务器上可用的字体列表"""
from online_dev.document_generator.font_service import FontService
fonts = FontService.get_available_fonts()
return fonts