export.py 3.3 KB

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