document_service.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. """document_service.py — 文档 CRUD(基于 SQLite Block 存储)"""
  2. import tempfile
  3. from datetime import date, datetime, timezone
  4. from pathlib import Path
  5. import httpx
  6. from sqlalchemy import func, select
  7. from sqlalchemy.ext.asyncio import AsyncSession
  8. from app.config import settings
  9. from app.core.exceptions import DocumentNotFoundError, DocumentParseError
  10. from app.models.document import Document
  11. from app.schemas.document import CreateDocumentRequest
  12. from app.services.content_db import ContentDB
  13. from app.services.word_parser import parse_word_to_blocks
  14. # ------------------------------------------------------------------ #
  15. # Word 文档下载
  16. # ------------------------------------------------------------------ #
  17. async def _download_word(file_url: str) -> Path:
  18. """下载 Word 文档到临时文件"""
  19. suffix = Path(file_url.split("?")[0]).suffix.lower() or ".docx"
  20. if suffix not in (".doc", ".docx"):
  21. raise DocumentParseError(f"不支持的文件格式: {suffix}")
  22. try:
  23. async with httpx.AsyncClient(timeout=60) as client:
  24. resp = await client.get(file_url)
  25. resp.raise_for_status()
  26. except httpx.HTTPError as exc:
  27. raise DocumentParseError(f"文件下载失败: {exc}") from exc
  28. with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
  29. tmp_path = Path(tmp.name)
  30. tmp_path.write_bytes(resp.content)
  31. return tmp_path
  32. # ------------------------------------------------------------------ #
  33. # 服务类
  34. # ------------------------------------------------------------------ #
  35. class DocumentService:
  36. def __init__(self, db: AsyncSession) -> None:
  37. self.db = db
  38. async def create_document(self, data: CreateDocumentRequest) -> Document:
  39. """创建文档:下载 Word → 解析为 Blocks → 存入 SQLite"""
  40. # 1. 下载 Word 文档
  41. tmp_path = await _download_word(data.file_url)
  42. try:
  43. # 2. 解析为 Blocks
  44. blocks = parse_word_to_blocks(tmp_path)
  45. # 3. 创建 SQLite 数据库
  46. user_id = data.user_id or "default-user"
  47. today = date.today().strftime("%Y-%m-%d")
  48. db_dir = Path(settings.temp_dir) / user_id / "sqlite"
  49. db_dir.mkdir(parents=True, exist_ok=True)
  50. # 生成文档 ID
  51. import uuid
  52. doc_id = f"doc-{uuid.uuid4().hex[:12]}"
  53. db_path = db_dir / f"{doc_id}.db"
  54. # 4. 写入 Blocks
  55. with ContentDB(str(db_path)) as content_db:
  56. content_db.create_tables()
  57. content_db.insert_blocks(blocks)
  58. # 5. 创建文档记录
  59. doc = Document(
  60. id=doc_id,
  61. content_db_path=str(db_path),
  62. session_id=data.session_id,
  63. created_by=data.user_id,
  64. )
  65. self.db.add(doc)
  66. await self.db.commit()
  67. await self.db.refresh(doc)
  68. return doc
  69. finally:
  70. # 6. 清理临时文件
  71. tmp_path.unlink(missing_ok=True)
  72. async def get_document(self, document_id: str, include_blocks: bool = False) -> Document:
  73. """获取文档详情"""
  74. result = await self.db.execute(
  75. select(Document).where(Document.id == document_id)
  76. )
  77. doc = result.scalar_one_or_none()
  78. if doc is None:
  79. raise DocumentNotFoundError(document_id)
  80. # 可选:加载 blocks
  81. if include_blocks:
  82. with ContentDB(doc.content_db_path) as content_db:
  83. blocks = content_db.get_blocks()
  84. # 动态添加 blocks 属性
  85. doc.blocks = blocks
  86. return doc
  87. async def list_documents(
  88. self,
  89. user_id: str,
  90. page: int = 1,
  91. page_size: int = 20,
  92. session_id: str | None = None,
  93. sort_by: str = "updated_at",
  94. sort_order: str = "desc",
  95. ) -> tuple[list[Document], int]:
  96. """获取文档列表"""
  97. query = select(Document).where(Document.created_by == user_id)
  98. if session_id:
  99. query = query.where(Document.session_id == session_id)
  100. sort_col = getattr(Document, sort_by, Document.updated_at)
  101. query = query.order_by(sort_col.asc() if sort_order == "asc" else sort_col.desc())
  102. count_q = select(func.count()).select_from(query.subquery())
  103. total: int = (await self.db.execute(count_q)).scalar_one()
  104. offset = (page - 1) * page_size
  105. result = await self.db.execute(query.offset(offset).limit(page_size))
  106. return list(result.scalars().all()), total
  107. async def delete_document(self, document_id: str) -> None:
  108. """删除文档(同时删除 SQLite 文件)"""
  109. doc = await self.get_document(document_id)
  110. # 删除 SQLite 文件
  111. db_path = Path(doc.content_db_path)
  112. if db_path.exists():
  113. db_path.unlink()
  114. # 删除数据库记录
  115. await self.db.delete(doc)
  116. await self.db.commit()
  117. async def delete_documents_by_session(self, session_id: str) -> int:
  118. """删除指定 sessionId 的所有文档(同时删除 SQLite 文件)"""
  119. result = await self.db.execute(
  120. select(Document).where(Document.session_id == session_id)
  121. )
  122. docs = list(result.scalars().all())
  123. for doc in docs:
  124. # 删除 SQLite 文件
  125. db_path = Path(doc.content_db_path)
  126. if db_path.exists():
  127. db_path.unlink()
  128. await self.db.delete(doc)
  129. await self.db.commit()
  130. return len(docs)
  131. async def update_document_timestamp(self, document_id: str) -> Document:
  132. """更新文档的 updated_at 时间戳"""
  133. doc = await self.get_document(document_id)
  134. doc.updated_at = datetime.now(timezone.utc)
  135. await self.db.commit()
  136. await self.db.refresh(doc)
  137. return doc