import asyncio import json import os import uuid from datetime import datetime from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import PlainTextResponse, RedirectResponse from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from config import settings from database import get_db from models.document import Document from models.generation_log import GenerationLog from models.paragraph import Paragraph from models.template import Template from models.template_block import TemplateBlock from services.document_export import export_document_bytes, export_document_bytes_from_blocks from services.minio_client import ( download_object_bytes, get_presigned_url, split_bucket_path, upload_bytes, ) router = APIRouter() @router.get("/{document_id}/docx") async def export_docx(document_id: int, db: AsyncSession = Depends(get_db)): document = await db.get(Document, document_id) if document is None: raise HTTPException(status_code=404, detail="生成记录不存在") template = await db.get(Template, document.template_id) if template is None: raise HTTPException(status_code=404, detail="模板不存在") template_bucket, template_object = split_bucket_path(template.file_path) template_bytes = await asyncio.to_thread(download_object_bytes, template_bucket, template_object) result = await db.execute( select(GenerationLog, Paragraph) .join(Paragraph, Paragraph.id == GenerationLog.paragraph_id) .where(GenerationLog.document_id == document_id) .order_by(Paragraph.sort_index.asc(), Paragraph.id.asc()) ) logs = [] for log, paragraph in result.all(): logs.append( { "anchor_title": paragraph.anchor_title or paragraph.title, "title": paragraph.title, "write_mode": paragraph.write_mode, "content": json.loads(log.content) if log.content else {"content": []}, } ) exported_bytes = await asyncio.to_thread(export_document_bytes, template_bytes, logs) object_name = f"{datetime.now().strftime('%Y%m%d')}/{uuid.uuid4().hex}.docx" await asyncio.to_thread( upload_bytes, settings.MINIO_BUCKET_OUTPUTS, object_name, exported_bytes, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ) document.file_path = f"{settings.MINIO_BUCKET_OUTPUTS}/{object_name}" await db.commit() return RedirectResponse( url=get_presigned_url(settings.MINIO_BUCKET_OUTPUTS, object_name), status_code=307, ) @router.get("/{document_id}/docx-v2") async def export_docx_v2(document_id: int, db: AsyncSession = Depends(get_db)): """基于块结构的 Word 导出。""" document = await db.get(Document, document_id) if document is None: raise HTTPException(status_code=404, detail="生成记录不存在") template = await db.get(Template, document.template_id) if template is None: raise HTTPException(status_code=404, detail="模板不存在") template_bucket, template_object = split_bucket_path(template.file_path) template_bytes = await asyncio.to_thread(download_object_bytes, template_bucket, template_object) # 查询块和生成日志 blocks_result = await db.execute( select(TemplateBlock) .where(TemplateBlock.template_id == document.template_id) .order_by(TemplateBlock.sort_index.asc(), TemplateBlock.id.asc()) ) blocks = blocks_result.scalars().all() logs_result = await db.execute( select(GenerationLog) .where(GenerationLog.document_id == document_id) ) logs = logs_result.scalars().all() block_dicts = [ { "id": b.id, "sort_index": b.sort_index, "anchor_start_index": b.anchor_start_index, "anchor_end_index": b.anchor_end_index, "edit_mode": b.edit_mode, "block_type": b.block_type, "title": b.title, } for b in blocks ] log_dicts = [ { "id": log.id, "block_id": log.block_id, "status": log.status, "content": json.loads(log.content) if log.content else {"content": []}, } for log in logs ] exported_bytes = await asyncio.to_thread( export_document_bytes_from_blocks, template_bytes, block_dicts, log_dicts ) object_name = f"{datetime.now().strftime('%Y%m%d')}/{uuid.uuid4().hex}.docx" await asyncio.to_thread( upload_bytes, settings.MINIO_BUCKET_OUTPUTS, object_name, exported_bytes, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ) document.file_path = f"{settings.MINIO_BUCKET_OUTPUTS}/{object_name}" await db.commit() return RedirectResponse( url=get_presigned_url(settings.MINIO_BUCKET_OUTPUTS, object_name), status_code=307, ) @router.get("/{document_id}/pdf") async def export_pdf(document_id: int): return PlainTextResponse( f"文档 {document_id} 的 PDF 导出功能正在开发中,当前版本请先使用预览页查看结果。", media_type="text/plain; charset=utf-8", )