820 lines
30 KiB
Python
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
|