"""export.py — POST /api/v1/export/doc""" from pathlib import Path from fastapi import APIRouter, Depends from sqlalchemy.ext.asyncio import AsyncSession from app.api.v1 import ok from app.config import settings from app.core.dependencies import get_db from app.core.exceptions import ExportError from app.schemas.export import ExportDocRequest, ExportDocResponse from app.services.content_db import ContentDB from app.services.document_service import DocumentService from app.services.export_record_service import ExportRecordService from app.services.export_service import ( build_style_map, load_style_file, blocks_to_docx_bytes, _make_filename, update_document_fields, ) from app.services.storage_monitor import check_quota router = APIRouter(tags=["Export"]) @router.post("/export/doc", summary="导出 .doc 文件") async def export_document( body: ExportDocRequest, db: AsyncSession = Depends(get_db), ) -> dict: doc_svc = DocumentService(db) rec_svc = ExportRecordService(db) # 1. 读取文档 doc = await doc_svc.get_document(body.document_id) user_id = doc.created_by or "default-user" # 2. 检查是否有最新记录可复用 latest = await rec_svc.get_latest_record(body.document_id) if latest and doc.updated_at <= latest.created_at: # 文档未更新,直接复用已有记录 warning = check_quota(user_id) return ok(ExportDocResponse( record_id=latest.id, download_url=latest.download_url, file_name=latest.file_name, style_id=latest.style_id, warning=warning, ).model_dump(by_alias=True)) # 3. 加载样式 style_data = load_style_file(body.style_id) style_map = build_style_map(style_data) actual_style_id = body.style_id or "default" # 4. 从 SQLite 读取 blocks 并生成 .doc 字节流 try: with ContentDB(doc.content_db_path) as content_db: blocks = content_db.get_blocks() # Blocks → Word doc_bytes = blocks_to_docx_bytes(blocks, style_map, style_data) except Exception as exc: raise ExportError(f"文档转换失败: {exc}") from exc # 5. 写入文件:./tmp/{user_id}/{YYYY-MM-DD}/{name}.doc from datetime import date file_stem = _make_filename(blocks) file_name = f"{file_stem}.doc" today = date.today().strftime("%Y-%m-%d") user_dir = Path(settings.temp_dir) / user_id / today user_dir.mkdir(parents=True, exist_ok=True) file_path = user_dir / file_name try: file_path.write_bytes(doc_bytes) except OSError as exc: raise ExportError(f"文件写入失败: {exc}") from exc # 5.5. 如果文档包含 TOC,使用 WPS/Word 更新域(目录和页码) has_toc = any(block.get('type') == 'toc' for block in blocks) if has_toc: update_success = update_document_fields(str(file_path)) if update_success: # 域更新成功后,重新读取文件大小(可能略有变化) file_size = file_path.stat().st_size # 获取最终文件大小 file_size = file_path.stat().st_size # 6. 写入数据库记录(先占位 download_url,再回写) record = await rec_svc.create_record( user_id=user_id, file_name=file_name, file_path=str(file_path), file_size=file_size, download_url="", document_id=doc.id, style_id=actual_style_id, ) download_url = ( f"{settings.base_url.rstrip('/')}/api/v1/export/records/{record.id}/download" f"?userId={user_id}" ) record.download_url = download_url await db.commit() # 7. 配额检查 warning = check_quota(user_id) return ok(ExportDocResponse( record_id=record.id, download_url=download_url, file_name=file_name, style_id=actual_style_id, warning=warning, ).model_dump(by_alias=True))