document-management-design.md 12 KB

文档管理设计文档

关联模块:app/api/v1/documents.py · app/services/document_service.py · app/models/document.py · app/schemas/document.py 关联章节:text-editor-backend-api.md § 2. 文档管理 API(阶段 0)


1. 概览

概览 内容
功能 从 Word 文档链接解析 Markdown 内容并持久化存储,提供文档 CRUD 接口供编辑器使用
引入阶段 阶段 0
Base URL http://192.168.0.200:8000
涉及端点 POST /api/v1/documents · GET /api/v1/documents · GET /api/v1/documents/{documentId} · PUT /api/v1/documents/{documentId} · DELETE /api/v1/documents/{sessionId}
存储方式 PostgreSQL,内容以 Markdown 文本直接存储,不限制大小
关联功能 文档创建后可关联会话 ID;导出时通过 documentId 读取内容

2. 数据模型

2.1 documents 表

字段 类型 说明
id VARCHAR(64) 主键,格式 doc-{uuid[:12]},自动生成
content TEXT Markdown 内容,从 Word 文档解析而来
format VARCHAR(32) 内容格式,固定为 markdown
session_id VARCHAR(128) 关联聊天会话 ID,必填
created_by VARCHAR(128) 创建者用户 ID
created_at TIMESTAMPTZ 创建时间,服务端自动生成
updated_at TIMESTAMPTZ 最后更新时间,每次 PUT 手动赋值刷新

updated_atdocument_service.py 在每次更新时手动赋值 datetime.now(timezone.utc),而非依赖数据库 onupdate 触发,确保时间准确。

2.2 ORM 模型(app/models/document.py

class Document(Base):
    __tablename__ = "documents"

    id: Mapped[str] = mapped_column(
        String(64), primary_key=True,
        default=lambda: f"doc-{uuid.uuid4().hex[:12]}"
    )
    content: Mapped[str] = mapped_column(Text, nullable=False, default="")
    format: Mapped[str] = mapped_column(String(32), nullable=False, default="markdown")
    session_id: Mapped[str] = mapped_column(String(128), nullable=False)
    created_by: Mapped[str | None] = mapped_column(String(128), nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

3. 端点详情

3.1 创建文档

POST /api/v1/documents

触发时机:用户在 Chat 中点击"编辑"按钮时。

处理流程

  1. 接收 userIdfileUrlsessionId
  2. 后端下载 fileUrl 指向的 Word 文档(.doc / .docx)
  3. 使用 python-docx 解析文档内容,转换为 Markdown 格式
  4. 将 Markdown 内容写入 documents
  5. 删除临时下载文件
  6. 返回 documentIdformatcreatedAt

注意

  • 不限制文件大小
  • Word 文档下载完成后自动删除,不留临时文件
  • 此接口只做解析和持久化,不生成任何导出文件

请求

{
  "userId": "user-xyz",
  "fileUrl": "https://example.com/files/report.docx",
  "sessionId": "chat-session-123"
}
字段 类型 必填 说明
userId string 当前用户 ID,写入 created_by 字段
fileUrl string Word 文档的可访问链接,后端下载后解析为 Markdown
sessionId string 关联聊天会话 ID,必填

响应

{
  "code": 0,
  "data": {
    "documentId": "doc-abc123",
    "format": "markdown",
    "createdAt": 1680000000000
  }
}

错误响应

HTTP 状态码 code 触发条件
400 400 fileUrl 无法访问或文件格式不支持
500 500 Word 解析失败

3.2 获取文档详情

GET /api/v1/documents/{documentId}

触发时机:编辑器 Modal 打开后,根据 documentId 加载文档内容。

请求

路径参数:documentId

响应

{
  "code": 0,
  "data": {
    "id": "doc-abc123",
    "content": "# 标题\n\n这是文档内容...",
    "format": "markdown",
    "sessionId": "chat-session-123",
    "userId": "user-xyz",
    "createdAt": 1680000000000,
    "updatedAt": 1680000100000
  }
}

错误响应

HTTP 状态码 code 触发条件
404 404 documentId 不存在

3.3 更新文档

PUT /api/v1/documents/{documentId}

触发时机:用户在编辑器中点击"保存"按钮时。

支持两种模式,二选一,不可同时传入

  • 全量更新:传入 content,直接替换整篇内容
  • 局部块更新:传入 blocks,按标题级别和顺序下标定位到具体块后替换,其余内容不变

局部块更新说明

块的定义:以标题行(# / ## / ...)为分割点,每个标题行及其下属正文构成一个块。

定位方式:level(标题等级)+ index(该等级在全文中的出现顺序,从 0 开始)。

示例:文档有两个 ## 标题,修改第二个时传 { "level": 2, "index": 1, "content": "..." }

请求(全量更新)

{
  "content": "# 更新后的完整内容\n\n..."
}

请求(局部块更新)

{
  "blocks": [
    {
      "level": 2,
      "index": 1,
      "content": "## 1.2 目标\n\n更新后的段落内容..."
    }
  ]
}
字段 类型 必填 说明
content string 全量替换内容,与 blocks 二选一
blocks array 局部块更新列表,与 content 二选一
blocks[].level integer 标题等级,1 = H1,2 = H2,以此类推(1-6)
blocks[].index integer 该等级标题在全文中的顺序下标,从 0 开始
blocks[].content string 该块的新内容,须包含标题行本身

响应

{
  "code": 0,
  "data": {
    "documentId": "doc-abc123",
    "updatedAt": 1680000200000
  }
}

错误响应

HTTP 状态码 code 触发条件
404 404 documentId 不存在
422 422 content 和 blocks 同时传入

3.4 删除会话的所有文档

DELETE /api/v1/documents/{sessionId}

触发时机:删除对应会话时,联动删除该会话关联的所有文档记录。

硬删除,数据库记录直接移除,不支持撤销。如果 sessionId 下没有文档,也会返回成功。

请求

路径参数:sessionId

响应

{
  "code": 0,
  "message": "Deleted 3 document(s) successfully"
}

说明

  • 删除指定 sessionId 下的所有文档
  • 返回消息中包含实际删除的文档数量
  • 如果该 sessionId 下没有文档,返回 "Deleted 0 document(s) successfully"

3.5 获取文档列表

GET /api/v1/documents

分页查询当前用户的文档记录,列表项不含 content,减少传输量。

请求参数

参数 类型 必填 说明
userId string 当前用户 ID
page integer 页码,默认 1
pageSize integer 每页数量,默认 20,最大 100
sessionId string 按聊天会话过滤
sortBy string createdAt / updatedAt,默认 updatedAt
sortOrder string asc / desc,默认 desc

响应

{
  "code": 0,
  "data": {
    "documents": [
      {
        "id": "doc-abc123",
        "sessionId": "chat-session-123",
        "createdAt": 1680000000000,
        "updatedAt": 1680000100000
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 20,
      "total": 100,
      "totalPages": 5
    }
  }
}

4. 内部处理流程

4.1 创建文档流程

POST /api/v1/documents
        |
        v
参数校验(userId, fileUrl 不为空)
        |
        v
下载 fileUrl 文件到临时目录
  - 下载失败 → 返回 400
        |
        v
python-docx 解析 Word 文档 → Markdown 内容
  - 解析失败 → 返回 500
        |
        v
写入 documents 表
  - id:doc-{uuid[:12]}
  - content:解析后的 Markdown
  - format:markdown
  - session_id:sessionId(必填)
  - created_by:userId
        |
        v
删除临时下载文件
        |
        v
返回 { documentId, format, createdAt }

4.2 局部块更新流程(_apply_block_updates

接收 content(当前全文)+ blocks(待替换块列表)
        |
        v
按行扫描文档,用正则 ^(#{1,6})\s+ 找出所有标题行位置
  → 得到 [(line_idx, level), ...]
        |
        v
统计每个 level 的出现顺序,生成带 index 的标题信息
  → [(line_idx, level, index), ...]
        |
        v
按标题行位置将全文切分为若干 chunk
  → chunk[0]:首个标题前的前置内容
  → chunk[1..N]:各标题块(含标题行 + 下属正文)
  → 同时构建 (level, index) → chunk_idx 的 O(1) 映射字典
        |
        v
遍历 blocks 列表,按 (level, index) 定位 chunk,替换内容
        |
        v
将所有 chunk 重新拼接,strip 后补末尾换行,返回新内容

5. 服务层核心逻辑(document_service.py

5.1 关键方法

方法 说明
create_document(data, user_id) 创建文档,写入数据库
get_document(document_id) 查询单条,不存在抛 DocumentNotFoundError
list_documents(page, page_size, session_id, sort_by, sort_order) 分页查询,使用 SELECT COUNT(*) FROM subquery 计总数
update_document(document_id, data) 全量或局部更新,手动刷新 updated_at
delete_document(document_id) 硬删除
_apply_block_updates(content, blocks) 静态方法,局部块替换核心算法

5.2 异常处理

异常类 触发条件 HTTP 状态码
DocumentNotFoundError get_document() 查不到记录 404

6. Schema 说明(app/schemas/document.py

所有请求字段使用 camelCase 别名接收,响应字段使用 serialization_alias 输出 camelCase。

Schema 类 用途
CreateDocumentRequest 创建请求,含 userIdfileUrlsessionId
UpdateDocumentRequest 更新请求,含 content / blocks 二选一校验(model_validator
BlockUpdate 局部块更新单元,含 levelindexcontent
DocumentResponse 详情响应,含完整字段
DocumentListItem 列表项响应,不含 content
Pagination 分页信息

7. 涉及代码文件

文件 职责
app/api/v1/documents.py 路由层:五个端点的参数接收、服务调用、响应封装
app/services/document_service.py 业务逻辑:CRUD、局部块更新算法
app/models/document.py ORM 模型:Document 表定义
app/schemas/document.py 请求/响应 Pydantic 模型
app/core/exceptions.py DocumentNotFoundError 定义及全局 handler
migrations/versions/001_init_documents.py 建表迁移脚本

8. 请求示例

创建文档

curl -X POST http://192.168.0.200:8000/api/v1/documents \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user-xyz",
    "fileUrl": "https://example.com/files/report.docx",
    "sessionId": "chat-session-123"
  }'

获取文档详情

curl "http://192.168.0.200:8000/api/v1/documents/doc-abc123"

全量更新文档

curl -X PUT http://192.168.0.200:8000/api/v1/documents/doc-abc123 \
  -H "Content-Type: application/json" \
  -d '{"content": "# 新标题\n\n新的完整内容..."}'

局部块更新

curl -X PUT http://192.168.0.200:8000/api/v1/documents/doc-abc123 \
  -H "Content-Type: application/json" \
  -d '{
    "blocks": [
      {
        "level": 2,
        "index": 0,
        "content": "## 一、背景\n\n更新后的背景描述..."
      }
    ]
  }'

删除会话的所有文档

curl -X DELETE "http://192.168.0.200:8000/api/v1/documents/chat-session-123"

获取文档列表

curl "http://192.168.0.200:8000/api/v1/documents?userId=user-xyz&page=1&pageSize=20&sessionId=chat-session-123"

文档版本:v1.0 创建日期:2026-06-24 关联文档text-editor-backend-api.md · export-doc-content-mapping.md · text-editor-architecture.md