toc-complete-guide.md 16 KB

TOC (目录) 功能完整指南

最后更新: 2026-07-10
文档版本: v2.0
状态: ✅ 已完成并上线


目录

  1. 功能概述
  2. 快速开始
  3. 功能需求与设计
  4. 实现细节
  5. 页码编号机制
  6. 自动更新功能
  7. 测试验证
  8. 已知问题与限制
  9. 故障排查

功能概述

背景问题

原系统在解析 Word 文档时完全跳过了 SDT 目录控件(Structured Document Tag),导致:

  • 目录信息完全丢失
  • 用户在前端编辑器中看不到目录
  • 导出时无法生成目录

解决方案

实现了完整的 TOC Block 支持:

  • 解析识别 - 正确识别 Word SDT 目录控件
  • 数据存储 - 保存目录配置(层级、超链接、页码等)
  • 前端展示 - 只读占位符,不可编辑但可删除
  • 智能导出 - 插入 TOC 域代码,自动生成目录
  • 页码控制 - 目录单独一页,正文从第 1 页开始
  • 自动更新 - 使用 WPS/Word API 自动更新域

用户价值

  1. 文档完整性 - 保留原始文档的目录结构
  2. 编辑便利性 - 在编辑器中看到目录位置
  3. 导出一致性 - 导出文档自动包含目录
  4. 开箱即用 - 下载的文档已生成目录,无需手动更新

快速开始

1. 上传包含目录的文档

curl -X POST http://localhost:8000/api/v1/documents \
  -F "file=@document_with_toc.docx"

2. 查看解析结果

{
  "blocks": [
    {
      "id": "toc-001",
      "type": "toc",
      "content": {"title": "目录"},
      "metadata": {
        "toc_config": {
          "levels": "1-3",
          "use_hyperlinks": true,
          "use_page_numbers": true
        },
        "readonly": true,
        "deletable": true
      }
    }
  ]
}

3. 导出文档

curl -X POST http://localhost:8000/api/v1/documents/{id}/export/doc \
  -o output.docx

生成的文档会:

  • 自动包含目录(已生成,无需更新)
  • 目录单独一页
  • 页码从正文第一页开始(第 1 页)
  • 支持超链接点击跳转

功能需求与设计

TOC Block 数据结构

{
  "id": "toc-001",
  "blockOrder": 0,
  "type": "toc",
  "level": 0,
  "index": 0,
  "content": {
    "title": "目录"
  },
  "wordStyle": "TOC",
  "metadata": {
    "toc_config": {
      "levels": "1-3",              // 包含的标题层级
      "use_hyperlinks": true,       // 是否使用超链接
      "use_page_numbers": true,     // 是否显示页码
      "hide_page_numbers_in_web": true,
      "use_outline_levels": true,
      "show_leader_dots": true,
      "leader_char": "."
    },
    "is_auto_generated": true,      // 标记为自动生成
    "readonly": true,                // 前端不可编辑
    "deletable": true                // 前端可删除
  }
}

核心功能需求

功能 需求 状态
FR-1 解析 Word 文档目录
FR-2 存储目录配置
FR-3 前端只读展示
FR-4 删除目录
FR-5 导出包含目录
FR-6 禁止编辑内容
FR-7 自动更新域
FR-8 页码从正文开始

实现细节

1. 解析阶段 (word_parser.py)

检测 SDT 目录控件

# 遍历 body 元素,检测 SDT
for child in body:
    tag = child.tag
    if tag.endswith('sdt'):
        # 提取 TOC 信息
        toc_block = _extract_toc_from_sdt(child, block_order, parent_stack)
        if toc_block:
            blocks.append(toc_block)

提取目录配置

从 SDT XML 中提取:

  • 目录标题(第一个段落的文本)
  • TOC 域代码参数(TOC \o "1-3" \h \z \u
  • 层级、超链接、页码等配置

2. 导出阶段 (export_service.py)

主要函数

函数 功能 位置
_render_toc_block() 渲染 TOC block Line 1275
_create_toc_field() 创建 TOC 域代码 Line 1315
_set_update_fields_on_open() 设置自动更新 Line 1176
_add_page_number_footer_with_restart() 添加页码(重启编号) Line 1443
update_document_fields() 使用 WPS/Word API 更新域 Line 17

渲染流程

1. 检测 TOC block → 设置自动更新域
   ↓
2. 在目录前添加分页符
   ↓
3. 添加目录标题(居中,Normal 样式)
   ↓
4. 插入 TOC 域代码
   ↓
5. 添加分节符(目录后开始新节)
   ↓
6. 为新节添加页码页脚(从 1 开始)
   ↓
7. 渲染其他 blocks(heading, paragraph 等)
   ↓
8. 保存文档
   ↓
9. 使用 WPS/Word API 自动更新域(可选)

3. Word TOC 域结构

<w:p>
  <w:r>
    <!-- 域开始 -->
    <w:fldChar w:fldCharType="begin" w:dirty="1"/>
    
    <!-- 域指令 -->
    <w:instrText xml:space="preserve">TOC \o "1-3" \h \z \u</w:instrText>
    
    <!-- 域分隔符 -->
    <w:fldChar w:fldCharType="separate"/>
    
    <!-- 占位符(更新后会被目录内容替换) -->
    <w:r><w:t>占位文本</w:t></w:r>
    
    <!-- 域结束 -->
    <w:fldChar w:fldCharType="end"/>
  </w:r>
</w:p>

TOC 域参数说明

参数 说明 示例
\o "1-3" 包含标题层级 1-3 一、二、三级标题
\h 使用超链接 可点击跳转
\z Web 视图中隐藏页码 -
\u 使用 Unicode 支持中文

页码编号机制

问题:目录计入页码

修改前

文档(单节)
├── 目录 → 第 1 页
├── 正文 → 第 2 页  ✗ 不符合需求

解决方案:使用分节符

修改后

文档
├── [第一节:目录]
│   ├── 目录内容
│   └── 页脚:无页码
│
├── [分节符 - 下一页]
│
└── [第二节:正文]
    ├── 一、基本数据 → 第 1 页  ✓
    ├── 正文内容...
    └── 页脚:第 X 页 / 共 Y 页

关键技术

1. 创建新节

from docx.enum.section import WD_SECTION

# 添加分节符(下一页开始新节)
new_section = doc.add_section(WD_SECTION.NEW_PAGE)

2. 断开页脚链接

# 新节的页脚不链接到前一节
footer = new_section.footer
footer.is_linked_to_previous = False

3. 设置页码从 1 开始

# 通过 XML 设置页码起始值
sectPr = section._sectPr
pgNumType = OxmlElement('w:pgNumType')
pgNumType.set(qn('w:start'), '1')
sectPr.append(pgNumType)

4. 使用 SECTIONPAGES 域

修改前(错误):

add_field(r4._r, ' NUMPAGES ')  # 整个文档总页数(包括目录)

修改后(正确):

add_field(r4._r, ' SECTIONPAGES ')  # 当前节总页数(只计算正文)

效果对比

域代码 计算范围 示例结果
NUMPAGES 整个文档 共 23 页(含目录)
SECTIONPAGES 当前节(正文) 共 20 页(不含目录)

页码格式

页脚内容

第 {PAGE} 页 / 共 {SECTIONPAGES} 页

实际显示

第 1 页 / 共 20 页
第 2 页 / 共 20 页
...
第 20 页 / 共 20 页

自动更新功能

功能说明

导出后使用 WPS/Word COM API 自动更新文档域,确保用户下载的文档已包含完整目录,无需手动更新。

实现原理

参考 update_toc.py 的逻辑:

# 1. 启动 WPS/Word
app = win32com.client.Dispatch("Kwps.Application")  # 或 Word.Application
app.Visible = False
app.DisplayAlerts = False

# 2. 打开文档
doc = app.Documents.Open(file_path)

# 3. 更新所有域
doc.Fields.Update()

# 4. 再次更新 TOC 域(确保页码正确)
for field in doc.Fields:
    if field.Type == 13:  # wdFieldTOC
        field.Update()

# 5. 保存并关闭
doc.Save()
doc.Close()
app.Quit()

集成到导出流程

# 在 export.py 中
# 5. 写入文件
file_path.write_bytes(doc_bytes)

# 5.5. 如果文档包含 TOC,自动更新域
has_toc = any(block.get('type') == 'toc' for block in blocks)
if has_toc:
    update_success = update_document_fields(str(file_path))
    if update_success:
        # 域更新成功,重新读取文件大小
        file_size = file_path.stat().st_size

# 6. 返回下载链接(已包含更新后的文档)

容错机制

场景 处理方式 结果
非 Windows 平台 跳过更新 文档仍可用,用户打开时自动更新
未安装 pywin32 跳过更新 文档仍可用,用户打开时自动更新
WPS/Word 不可用 跳过更新 文档仍可用,用户打开时自动更新
更新过程出错 跳过更新 文档仍可用,用户打开时自动更新

性能影响

操作 耗时
启动 WPS ~1-2秒
打开文档 ~0.5秒
更新域 ~0.5秒
保存文档 ~0.5秒
总计 ~2-3秒

文件大小优化:更新后文件减小约 20%(去除冗余 XML)

依赖安装

# 安装 pywin32(Windows COM API)
pip install pywin32

# 系统要求
# - Windows 操作系统
# - WPS Office 或 Microsoft Word

测试验证

自动化测试

1. 基础导出测试

python test_toc_export.py

预期结果

  • ✅ 生成 tmp/test_toc_export.docx
  • ✅ 文档大小约 45KB
  • ✅ 无错误日志

2. 自动更新测试

python test_toc_update.py

预期结果

✓ 使用 WPS 成功更新文档域
✓ 域更新成功!
✓ 最终文件大小: 35,373 字节
→ 文件大小变化: -9,774 字节

手动验证清单

打开生成的 tmp/test_toc_with_update.docx,检查:

目录页

  • 目录单独占一页
  • 目录标题居中显示
  • 目录已自动生成(无需手动更新)
  • 目录包含所有一级标题
  • 目录项页码正确

正文页

  • 正文从新的一页开始
  • 第一个一级标题显示"第 1 页"
  • 页码连续递增
  • 页脚显示"第 X 页 / 共 Y 页"
  • "共 Y 页"只计算正文(不包括目录)

交互功能

  • 点击目录项可跳转到对应标题
  • 修改标题后右键"更新域"可刷新目录
  • 页码自动更新

已知问题与限制

1. 仅支持 SDT 目录

问题:只识别由 Word"插入目录"功能生成的目录
影响:手动创建的目录表格不会被识别
解决方案:提示用户使用 Word 的标准目录功能

2. 目录项不存储

问题:不存储目录中的具体项(标题文本、页码等)
原因:目录项由 Word 自动生成,存储后可能不一致
影响:前端无法预览目录内容
解决方案:前端可从 heading blocks 动态生成预览

3. 复杂配置支持有限

问题:高级配置(自定义样式、特殊开关)可能丢失
影响:复杂目录的某些格式可能不完全保留
解决方案:优先支持常用配置,复杂配置在后续版本迭代

4. WPS vs Microsoft Word

软件 自动更新 手动更新
WPS Office ✅ 提示更新 ✅ F9 或右键
Microsoft Word ⚠️ 可能需手动 ✅ F9 或右键
LibreOffice ⚠️ 部分支持 ✅ 手动更新

5. 非 Windows 平台

问题:Linux/macOS 无法使用 COM API 自动更新
影响:导出文档需用户手动更新域
解决方案:文档已设置自动更新标记,打开时会提示


故障排查

问题 1:目录未生成

症状:打开文档后目录区域是空的

排查步骤

  1. 检查是否自动更新:WPS 通常会提示"是否更新域"
  2. 手动更新:右键点击目录区域 → 选择"更新域"
  3. 按 F9 键强制更新所有域

常见原因

  • Word 安全设置禁用自动更新
  • 文档标题未使用标准样式(Heading 1, 2, 3)

问题 2:页码错误(从 2 开始)

症状:正文第一页显示"第 2 页"而不是"第 1 页"

排查步骤

  1. 检查文档是否使用分节符(不是简单分页符)
  2. 检查新节的 pgNumType 是否设置 start="1"
  3. 检查页脚是否断开链接(is_linked_to_previous = False

解决方案

  • 确保使用最新版本的代码(已修复)
  • 重新导出文档

问题 3:"共 X 页"包括目录页

症状:页脚显示"共 23 页",但实际正文只有 20 页

排查步骤

  1. 检查是否使用 SECTIONPAGES 域(不是 NUMPAGES
  2. 查看页脚域代码:右键 → 切换域代码

解决方案

  • 确保使用最新版本(已使用 SECTIONPAGES
  • 重新导出文档

问题 4:自动更新失败

症状:测试日志显示"域更新失败"

排查步骤

  1. 检查是否安装 pywin32:pip list | grep pywin32
  2. 检查是否安装 WPS/Word
  3. 检查文件路径是否为绝对路径
  4. 检查文件权限

解决方案

# 安装 pywin32
pip install pywin32

# 检查 WPS 是否可用
python -c "import win32com.client; app = win32com.client.Dispatch('Kwps.Application'); print('WPS OK')"

# 检查 Word 是否可用
python -c "import win32com.client; app = win32com.client.Dispatch('Word.Application'); print('Word OK')"

问题 5:目录超链接无法跳转

症状:点击目录项无反应

排查步骤

  1. 检查 TOC 域是否包含 \h 参数
  2. 检查标题是否有书签(自动生成)
  3. 更新域后重新测试

解决方案

  • 确保 toc_config.use_hyperlinks = true
  • 重新导出文档
  • 在 Word 中手动更新域(F9)

附录

A. 相关文件清单

核心代码文件

文件 说明 关键函数
app/services/export_service.py 导出服务 _render_toc_block, update_document_fields
app/api/v1/export.py 导出 API export_document
app/schemas/block.py Block Schema TOCBlock, TOCConfig

测试文件

文件 说明
test_toc_export.py 基础导出测试
test_toc_update.py 自动更新测试

文档文件

文件 说明
docs/features/toc-complete-guide.md 本文档(综合指南)
update_toc.py 参考实现

B. API 参考

创建文档(包含目录)

请求:

POST /api/v1/documents
Content-Type: multipart/form-data

file: <Word文档二进制>

响应:

{
  "code": 0,
  "data": {
    "id": "doc-123",
    "name": "document.docx",
    "created_at": "2026-07-10T10:00:00Z"
  }
}

获取 Blocks(含 TOC)

请求:

GET /api/v1/documents/{documentId}/blocks

响应:

{
  "code": 0,
  "data": {
    "blocks": [
      {
        "id": "toc-001",
        "type": "toc",
        "content": {"title": "目录"},
        "metadata": {
          "toc_config": {...},
          "readonly": true
        }
      }
    ]
  }
}

导出文档

请求:

POST /api/v1/export/doc
Content-Type: application/json

{
  "document_id": "doc-123",
  "style_id": null
}

响应:

{
  "code": 0,
  "data": {
    "record_id": "export-456",
    "download_url": "http://localhost:8000/api/v1/export/records/export-456/download",
    "file_name": "document_1720598400000.doc"
  }
}

C. Word 域代码参考

域类型 域代码 说明
目录 TOC \o "1-3" \h \z \u 生成目录
当前页码 PAGE 第 X 页
总页数 NUMPAGES 整个文档页数
节页数 SECTIONPAGES 当前节页数
日期 DATE \@ "yyyy-MM-dd" 当前日期

D. 联系方式

如有问题或建议,请联系:


文档结束