export.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. """export.py — POST /api/v1/export/doc"""
  2. from pathlib import Path
  3. from fastapi import APIRouter, Depends
  4. from sqlalchemy.ext.asyncio import AsyncSession
  5. from app.api.v1 import ok
  6. from app.config import settings
  7. from app.core.dependencies import get_db
  8. from app.core.exceptions import ExportError
  9. from app.schemas.export import ExportDocRequest, ExportDocResponse
  10. from app.services.content_db import ContentDB
  11. from app.services.document_service import DocumentService
  12. from app.services.export_record_service import ExportRecordService
  13. from app.services.export_service import (
  14. build_style_map,
  15. load_style_file,
  16. blocks_to_docx_bytes,
  17. _make_filename,
  18. update_document_fields,
  19. )
  20. from app.services.storage_monitor import check_quota
  21. router = APIRouter(tags=["Export"])
  22. @router.post("/export/doc", summary="导出 .doc 文件")
  23. async def export_document(
  24. body: ExportDocRequest,
  25. db: AsyncSession = Depends(get_db),
  26. ) -> dict:
  27. doc_svc = DocumentService(db)
  28. rec_svc = ExportRecordService(db)
  29. # 1. 读取文档
  30. doc = await doc_svc.get_document(body.document_id)
  31. user_id = doc.created_by or "default-user"
  32. # 2. 检查是否有最新记录可复用
  33. latest = await rec_svc.get_latest_record(body.document_id)
  34. if latest and doc.updated_at <= latest.created_at:
  35. # 文档未更新,直接复用已有记录
  36. warning = check_quota(user_id)
  37. return ok(ExportDocResponse(
  38. record_id=latest.id,
  39. download_url=latest.download_url,
  40. file_name=latest.file_name,
  41. style_id=latest.style_id,
  42. warning=warning,
  43. ).model_dump(by_alias=True))
  44. # 3. 加载样式
  45. style_data = load_style_file(body.style_id)
  46. style_map = build_style_map(style_data)
  47. actual_style_id = body.style_id or "default"
  48. # 4. 从 SQLite 读取 blocks 并生成 .doc 字节流
  49. try:
  50. with ContentDB(doc.content_db_path) as content_db:
  51. blocks = content_db.get_blocks()
  52. # Blocks → Word
  53. doc_bytes = blocks_to_docx_bytes(blocks, style_map, style_data)
  54. except Exception as exc:
  55. raise ExportError(f"文档转换失败: {exc}") from exc
  56. # 5. 写入文件:./tmp/{user_id}/{YYYY-MM-DD}/{name}.doc
  57. from datetime import date
  58. file_stem = _make_filename(blocks)
  59. file_name = f"{file_stem}.doc"
  60. today = date.today().strftime("%Y-%m-%d")
  61. user_dir = Path(settings.temp_dir) / user_id / today
  62. user_dir.mkdir(parents=True, exist_ok=True)
  63. file_path = user_dir / file_name
  64. try:
  65. file_path.write_bytes(doc_bytes)
  66. except OSError as exc:
  67. raise ExportError(f"文件写入失败: {exc}") from exc
  68. # 5.5. 如果文档包含 TOC,使用 WPS/Word 更新域(目录和页码)
  69. has_toc = any(block.get('type') == 'toc' for block in blocks)
  70. if has_toc:
  71. update_success = update_document_fields(str(file_path))
  72. if update_success:
  73. # 域更新成功后,重新读取文件大小(可能略有变化)
  74. file_size = file_path.stat().st_size
  75. # 获取最终文件大小
  76. file_size = file_path.stat().st_size
  77. # 6. 写入数据库记录(先占位 download_url,再回写)
  78. record = await rec_svc.create_record(
  79. user_id=user_id,
  80. file_name=file_name,
  81. file_path=str(file_path),
  82. file_size=file_size,
  83. download_url="",
  84. document_id=doc.id,
  85. style_id=actual_style_id,
  86. )
  87. download_url = (
  88. f"{settings.base_url.rstrip('/')}/api/v1/export/records/{record.id}/download"
  89. f"?userId={user_id}"
  90. )
  91. record.download_url = download_url
  92. await db.commit()
  93. # 7. 配额检查
  94. warning = check_quota(user_id)
  95. return ok(ExportDocResponse(
  96. record_id=record.id,
  97. download_url=download_url,
  98. file_name=file_name,
  99. style_id=actual_style_id,
  100. warning=warning,
  101. ).model_dump(by_alias=True))