关联模块:
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)
| 概览 | 内容 |
|---|---|
| 功能 | 从 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 读取内容 |
| 字段 | 类型 | 说明 |
|---|---|---|
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_at由document_service.py在每次更新时手动赋值datetime.now(timezone.utc),而非依赖数据库onupdate触发,确保时间准确。
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())
POST /api/v1/documents
触发时机:用户在 Chat 中点击"编辑"按钮时。
处理流程:
userId、fileUrl、sessionIdfileUrl 指向的 Word 文档(.doc / .docx)python-docx 解析文档内容,转换为 Markdown 格式documents 表documentId、format、createdAt注意:
{
"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 解析失败 |
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 不存在 |
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 同时传入 |
DELETE /api/v1/documents/{sessionId}
触发时机:删除对应会话时,联动删除该会话关联的所有文档记录。
硬删除,数据库记录直接移除,不支持撤销。如果 sessionId 下没有文档,也会返回成功。
路径参数:sessionId
{
"code": 0,
"message": "Deleted 3 document(s) successfully"
}
sessionId 下的所有文档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
}
}
}
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 }
_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 后补末尾换行,返回新内容
document_service.py)| 方法 | 说明 |
|---|---|
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) |
静态方法,局部块替换核心算法 |
| 异常类 | 触发条件 | HTTP 状态码 |
|---|---|---|
DocumentNotFoundError |
get_document() 查不到记录 |
404 |
app/schemas/document.py)所有请求字段使用 camelCase 别名接收,响应字段使用 serialization_alias 输出 camelCase。
| Schema 类 | 用途 |
|---|---|
CreateDocumentRequest |
创建请求,含 userId、fileUrl、sessionId |
UpdateDocumentRequest |
更新请求,含 content / blocks 二选一校验(model_validator) |
BlockUpdate |
局部块更新单元,含 level、index、content |
DocumentResponse |
详情响应,含完整字段 |
DocumentListItem |
列表项响应,不含 content |
Pagination |
分页信息 |
| 文件 | 职责 |
|---|---|
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 |
建表迁移脚本 |
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