export-doc-content-mapping.md 14 KB

导出 API 设计文档

关联模块:app/api/v1/export.py · app/services/export_service.py · app/schemas/export.py 关联章节:text-editor-backend-api.md § 3. 导出 API(阶段 0)


1. 概览

项目 内容
功能 将编辑器中的 Markdown 内容转换为 .doc 文件并提供下载
引入阶段 阶段 0
Base URL http://192.168.0.200:8000
涉及端点 POST /api/v1/export/doc · GET /api/v1/files/{filename}
核心依赖 python-docx 1.1.2 · mistune 3.0.2
文件存储 服务器本地 ./tmp/ 目录(临时缓存,当前不自动清理)
下载链接有效期 默认 3600 秒(1 小时),由 EXPORT_LINK_EXPIRES 配置
默认样式文件 ./tmp/default.json(阶段 0 固定使用;阶段 1 起支持用户选择)

2. 端点详情

2.1 导出 .doc 文件

POST /api/v1/export/doc

用户在编辑器填写文件名、选择格式并确认后触发。后端将 Markdown 内容转换为 .doc 文件,存入临时目录,返回下载链接。

请求

Headers

Key Value
Content-Type application/json
Authorization Bearer <token>(复用宿主应用认证)

Body

{
  "fileName": "2026年Q2季度报告",
  "format": "doc",
  "content": "# 2026年Q2季度报告\n\n## 一、背景\n\n本季度整体营收同比增长15%。\n\n...",
  "documentId": "doc-abc123",
  "styleId": null
}
字段 类型 必填 说明
fileName string 文件名,不含扩展名,最大 255 字符
format string 阶段 0 固定为 "doc"
content string 文档当前 Markdown 内容,最大 200KB
documentId string 关联文档 ID,传入时后端同步更新草稿记录
styleId string 样式 ID;阶段 0 暂不生效,后端固定使用 default.json;阶段 1 起传入有效 ID 则使用用户上传的样式

响应

200 成功

{
  "code": 0,
  "message": "Success",
  "data": {
    "downloadUrl": "http://192.168.0.200:8000/api/v1/files/2026年Q2季度报告_XnjI2ABA1kI.doc",
    "fileName": "2026年Q2季度报告.doc",
    "expiresAt": 1749999999000,
    "styleId": "default"
  }
}
字段 类型 说明
downloadUrl string 临时下载链接,有效期由 expiresAt 标注
fileName string 含扩展名的完整文件名
expiresAt number 链接过期时间,Unix 毫秒时间戳
styleId string 实际使用的样式 ID;阶段 0 固定返回 "default"

错误响应

HTTP 状态码 code 触发条件
500 500 Markdown 解析失败 / 文件写入失败
{
  "code": 500,
  "message": "Export failed: Markdown 转换失败: ...",
  "data": null
}

2.2 下载导出文件

GET /api/v1/files/{filename}

前端通过 downloadUrl 跳转或 <a href> 触发,后端从临时目录读取文件返回二进制流。此端点不在 Swagger 文档中展示(include_in_schema=False)。

请求

GET /api/v1/files/2026年Q2季度报告_XnjI2ABA1kI.doc
路径参数 说明
filename 完整文件名(含随机后缀和扩展名)

响应

200 成功

Content-Type: application/msword
Content-Disposition: attachment; filename="2026年Q2季度报告_XnjI2ABA1kI.doc"

<二进制文件内容>

错误响应

HTTP 状态码 code 触发条件
500 500 文件不存在或已被清理

3. 内部处理流程

POST /api/v1/export/doc
        |
        v
参数校验(ExportDocRequest)
  - fileName 最大 255 字符
  - format 仅接受 "doc"
  - content 不为空
        |
        v
  documentId 存在?
    |           |
   Yes          No
    |           |
    v           |
同步更新草稿记录  |
PUT documents/{id}
(失败不阻断导出)
    |           |
    └─────┬─────┘
          |
          v
  ┌─────────────────────────────┐
  │        样式文件解析           │
  │                             │
  │  styleId 传入且有效?         │
  │    Yes → 从样式库加载对应     │
  │          JSON 文件           │
  │    No  → 加载默认样式文件     │
  │          ./tmp/default.json  │
  │                             │
  │  解析 JSON,提取各样式定义    │
  │  (Normal、Heading1-6、      │
  │   List Bullet/Number 等)    │
  └─────────────┬───────────────┘
                │
                v
export_service.export_doc()
  1. load_style_definitions(style_file)
     - 读取样式 JSON(默认或用户指定)
     - 构建 style_id -> 样式属性 映射表

  2. markdown_to_docx_bytes(content, styles)
     - DocxRenderer 初始化,注入样式映射
       └── 应用 Normal 样式的字体和字号
           (默认:宋体 12pt)
     - mistune 解析 Markdown -> AST
       plugins: table, strikethrough, url
     - 遍历 token,按映射表应用对应样式

  3. _safe_filename(fileName)
     └── 过滤非法字符,规范化 Unicode

  4. 写入 ./tmp/{name}_{token}.doc

  5. 生成 downloadUrl + expiresAt
          |
          v
返回 ExportDocResponse
  { downloadUrl, fileName, expiresAt, styleId }

样式加载优先级

请求携带 styleId(阶段 1)
    └─ styleId 有效 → 使用用户样式库中对应的 JSON 文件
    └─ styleId 无效 → 返回 404,不降级

请求未携带 styleId(阶段 0 / 默认)
    └─ 加载 ./tmp/default.json(由 DEFAULT_STYLE_FILE 配置)
    └─ default.json 不存在 → 返回 500

4. 默认样式文件(default.json

4.1 文件位置与用途

项目 内容
文件路径 ./tmp/default.json(由 DEFAULT_STYLE_FILE 配置)
格式 样式 JSON,与用户上传文档提取的格式完全相同
来源 从内置标准 Word 文档提取,随服务部署一同发布
阶段 0 行为 所有导出请求固定使用此文件,不支持切换
阶段 1 行为 用户未传 styleId 时作为兜底;用户可上传文档生成新样式并选用

4.2 文件结构

{
  "source_file": "default.doc",
  "total_styles": 164,
  "styles": [
    {
      "name": "Normal",
      "style_id": "Normal",
      "type": "PARAGRAPH (1)",
      "builtin": true,
      "hidden": false,
      "quick_style": true,
      "priority": null,
      "base_style": null,
      "next_paragraph_style": "Normal",
      "font": {
        "name": "宋体",
        "size_pt": 12.0,
        "bold": null,
        "italic": null,
        "underline": null,
        "color_rgb": null,
        "strike": null,
        "all_caps": null,
        "small_caps": null
      },
      "paragraph_format": {
        "alignment": null,
        "left_indent_pt": null,
        "right_indent_pt": null,
        "first_line_indent_pt": null,
        "space_before_pt": null,
        "space_after_pt": null,
        "line_spacing": null,
        "keep_together": null,
        "keep_with_next": null,
        "page_break_before": null
      }
    },
    {
      "name": "Heading 1",
      "style_id": "Heading1",
      "type": "PARAGRAPH (1)",
      "font": { "size_pt": 14.0, "bold": true, "color_rgb": "365F91" },
      "paragraph_format": { "space_before_pt": 24.0, "space_after_pt": 0.0 }
    }
    // ... 其余 162 个样式定义
  ]
}

字段说明见 text-editor-backend-api.md §4 数据结构

4.3 后端加载逻辑(伪代码)

def load_style_file(style_id: str | None) -> dict:
    """
    阶段 0:style_id 始终为 None,返回 default.json
    阶段 1:style_id 有值时从样式库查找对应 JSON 路径
    """
    if style_id is None:
        path = settings.default_style_file   # ./tmp/default.json
    else:
        # 阶段 1:从数据库查路径,找不到抛 404
        path = style_repo.get_file_path(style_id)

    with open(path, encoding="utf-8") as f:
        return json.load(f)


def build_style_map(style_data: dict) -> dict[str, dict]:
    """将样式列表转换为 style_id -> 样式属性 的映射,加速查询"""
    return {s["style_id"]: s for s in style_data["styles"]}

4.4 样式应用规则

导出时,DocxRenderer 按以下规则从样式映射中读取属性并应用到 python-docx 对象:

优先级 规则
1 使用样式 JSON 中对应 style_id 的字体和段落格式
2 JSON 中字段为 null 时,保留 python-docx 内置 Word 样式默认值
3 标题颜色:JSON 有 color_rgb 时使用,否则强制黑色(覆盖 Word 内置蓝色)
4 找不到对应 style_id 时,回退到 Normal 样式

5. Markdown 与 Word 样式映射

5.1 标题

Markdown Word 样式 python-docx 调用
# 文字 Heading 1 doc.add_heading(text, level=1)
## 文字 Heading 2 doc.add_heading(text, level=2)
### 文字 Heading 3 doc.add_heading(text, level=3)
#### 文字 Heading 4 doc.add_heading(text, level=4)
普通段落 Normal doc.add_paragraph(text)

所有标题颜色强制设为黑色,覆盖 Word 内置 Heading 样式的蓝色主题色。

5.2 内联样式

Markdown 语义 python-docx 处理
**文字** 粗体 run.bold = True
*文字* / _文字_ 斜体 run.italic = True
~~文字~~ 删除线 run.font.strike = True
`代码` 行内代码 run.font.name = "Courier New"

5.3 列表

Markdown Word 样式 python-docx 调用
- 文字 / * 文字 List Bullet doc.add_paragraph(style="List Bullet")
1. 文字 List Number doc.add_paragraph(style="List Number")
二级无序缩进 List Bullet 2 doc.add_paragraph(style="List Bullet 2")
二级有序缩进 List Number 2 doc.add_paragraph(style="List Number 2")

5.4 块级元素

Markdown Word 样式 python-docx 处理
> 文字 Quote doc.add_paragraph(style="Quote")
代码块 No Spacing 字体 Courier New 10pt,颜色 #333333
--- / *** 段落底部边框 OxmlElement 注入 w:pBdr
\| 列 \| 列 \| Table Grid doc.add_table(),表头行自动加粗

5.5 表格 AST 结构说明

mistune 3.x 解析表格后的 token 层级(需启用 table 插件):

table
  ├── table_head
  │     ├── table_cell { head: true }   <- 直接是 table_cell,无 table_row 层
  │     └── table_cell { head: true }
  └── table_body
        └── table_row
              ├── table_cell { head: false }
              └── table_cell { head: false }

table_head 下直接是 table_cell,不经过 table_rowtable_body 下才有 table_row -> table_cell 两层结构。


6. 临时文件说明

项目 内容
存储路径 ./tmp/(由 TEMP_DIR 配置)
文件命名 {safeName}_{randomToken}.doc,随机后缀防碰撞
当前清理机制 无自动清理,文件永久保留
expiresAt 字段 仅作为提示告知前端链接预期有效期,后端不做过期校验

待完善(后续迭代):

  • 下载端点增加过期时间校验,过期返回 410
  • 服务启动时扫描清理过期文件
  • 或迁移为数据库缓存(export_cache 表,BYTEA 存储),支持多实例部署

7. 配置项

环境变量 默认值 说明
TEMP_DIR ./tmp 临时文件存储目录
EXPORT_LINK_EXPIRES 3600 下载链接有效期(秒)
BASE_URL http://192.168.0.200:8000 服务对外地址,用于拼接 downloadUrl
DEFAULT_STYLE_FILE ./tmp/default.json 默认样式文件路径;阶段 0 固定使用,阶段 1 作为兜底

8. 涉及代码文件

文件 职责
app/api/v1/export.py 路由层:参数接收、可选草稿同步、响应封装
app/schemas/export.py 请求/响应 Pydantic 模型,字段别名(camelCase)
app/services/export_service.py 核心:DocxRendererload_style_file()markdown_to_docx_bytes()、文件写入、链接生成
app/services/document_service.py 可选路径:documentId 存在时调用 update_document()
app/config.py temp_dirbase_urlexport_link_expiresdefault_style_file 配置读取
app/core/exceptions.py ExportError 定义及全局 handler(返回 HTTP 500)
tmp/default.json 默认样式文件,阶段 0 固定使用

9. 请求示例

curl

curl -X POST http://192.168.0.200:8000/api/v1/export/doc \
  -H "Content-Type: application/json" \
  -d '{
    "fileName": "2026年Q2季度报告",
    "format": "doc",
    "content": "# 2026年Q2季度报告\n\n## 一、背景\n\n本季度整体营收同比增长15%。\n\n## 二、核心数据\n\n### 2.1 收入\n\n| 月份 | 收入(万元) |\n|------|------|\n| 4月 | 120 |\n| 5月 | 135 |\n\n### 2.2 用户增长\n\n- 新增用户:12,000\n- 月活用户:85,000\n\n## 三、下季度计划\n\n1. 推进产品 A 上线\n2. 扩充销售团队\n",
    "styleId": null
  }'

阶段 0 传 null 或不传 styleId,后端均使用 default.json

预期响应

{
  "code": 0,
  "message": "Success",
  "data": {
    "downloadUrl": "http://192.168.0.200:8000/api/v1/files/2026年Q2季度报告_XnjI2ABA1kI.doc",
    "fileName": "2026年Q2季度报告.doc",
    "expiresAt": 1749999999000,
    "styleId": "default"
  }
}

下载文件

# 浏览器直接访问 downloadUrl,或:
curl -O "http://192.168.0.200:8000/api/v1/files/2026年Q2季度报告_XnjI2ABA1kI.doc"

文档版本:v2.2 更新日期:2026-06-15 关联文档text-editor-backend-api.md · text-editor-architecture.md · environment-setup.md