export.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  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. )
  19. from app.services.storage_monitor import check_quota
  20. router = APIRouter(tags=["Export"])
  21. @router.post("/export/doc", summary="导出 .doc 文件")
  22. async def export_document(
  23. body: ExportDocRequest,
  24. db: AsyncSession = Depends(get_db),
  25. ) -> dict:
  26. doc_svc = DocumentService(db)
  27. rec_svc = ExportRecordService(db)
  28. # 1. 读取文档
  29. doc = await doc_svc.get_document(body.document_id)
  30. user_id = doc.created_by or "default-user"
  31. # 2. 检查是否有最新记录可复用
  32. latest = await rec_svc.get_latest_record(body.document_id)
  33. if latest and doc.updated_at <= latest.created_at:
  34. # 文档未更新,直接复用已有记录
  35. warning = check_quota(user_id)
  36. return ok(ExportDocResponse(
  37. record_id=latest.id,
  38. download_url=latest.download_url,
  39. file_name=latest.file_name,
  40. style_id=latest.style_id,
  41. warning=warning,
  42. ).model_dump(by_alias=True))
  43. # 3. 加载样式
  44. style_data = load_style_file(body.style_id)
  45. style_map = build_style_map(style_data)
  46. actual_style_id = body.style_id or "default"
  47. # 4. 从 SQLite 读取 blocks 并生成 .doc 字节流
  48. try:
  49. with ContentDB(doc.content_db_path) as content_db:
  50. blocks = content_db.get_blocks()
  51. # Blocks → Word
  52. doc_bytes = blocks_to_docx_bytes(blocks, style_map, style_data)
  53. except Exception as exc:
  54. raise ExportError(f"文档转换失败: {exc}") from exc
  55. # 5. 写入文件:./tmp/{user_id}/{YYYY-MM-DD}/{name}.doc
  56. from datetime import date
  57. file_stem = _make_filename(blocks)
  58. file_name = f"{file_stem}.doc"
  59. today = date.today().strftime("%Y-%m-%d")
  60. user_dir = Path(settings.temp_dir) / user_id / today
  61. user_dir.mkdir(parents=True, exist_ok=True)
  62. file_path = user_dir / file_name
  63. try:
  64. file_path.write_bytes(doc_bytes)
  65. except OSError as exc:
  66. raise ExportError(f"文件写入失败: {exc}") from exc
  67. file_size = file_path.stat().st_size
  68. # 6. 写入数据库记录(先占位 download_url,再回写)
  69. record = await rec_svc.create_record(
  70. user_id=user_id,
  71. file_name=file_name,
  72. file_path=str(file_path),
  73. file_size=file_size,
  74. download_url="",
  75. document_id=doc.id,
  76. style_id=actual_style_id,
  77. )
  78. download_url = (
  79. f"{settings.base_url.rstrip('/')}/api/v1/export/records/{record.id}/download"
  80. f"?userId={user_id}"
  81. )
  82. record.download_url = download_url
  83. await db.commit()
  84. # 7. 配额检查
  85. warning = check_quota(user_id)
  86. return ok(ExportDocResponse(
  87. record_id=record.id,
  88. download_url=download_url,
  89. file_name=file_name,
  90. style_id=actual_style_id,
  91. warning=warning,
  92. ).model_dump(by_alias=True))