关联模块:
app/api/v1/export.py·app/services/export_service.py·app/schemas/export.py关联章节:text-editor-backend-api.md § 3. 导出 API(阶段 0)
| 项目 | 内容 |
|---|---|
| 功能 | 将编辑器中的 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 起支持用户选择) |
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
}
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 | 文件不存在或已被清理 |
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
default.json)| 项目 | 内容 |
|---|---|
| 文件路径 | ./tmp/default.json(由 DEFAULT_STYLE_FILE 配置) |
| 格式 | 样式 JSON,与用户上传文档提取的格式完全相同 |
| 来源 | 从内置标准 Word 文档提取,随服务部署一同发布 |
| 阶段 0 行为 | 所有导出请求固定使用此文件,不支持切换 |
| 阶段 1 行为 | 用户未传 styleId 时作为兜底;用户可上传文档生成新样式并选用 |
{
"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 数据结构。
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"]}
导出时,DocxRenderer 按以下规则从样式映射中读取属性并应用到 python-docx 对象:
| 优先级 | 规则 |
|---|---|
| 1 | 使用样式 JSON 中对应 style_id 的字体和段落格式 |
| 2 | JSON 中字段为 null 时,保留 python-docx 内置 Word 样式默认值 |
| 3 | 标题颜色:JSON 有 color_rgb 时使用,否则强制黑色(覆盖 Word 内置蓝色) |
| 4 | 找不到对应 style_id 时,回退到 Normal 样式 |
| 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 样式的蓝色主题色。
| Markdown | 语义 | python-docx 处理 |
|---|---|---|
**文字** |
粗体 | run.bold = True |
*文字* / _文字_ |
斜体 | run.italic = True |
~~文字~~ |
删除线 | run.font.strike = True |
`代码` |
行内代码 | run.font.name = "Courier New" |
| 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") |
| Markdown | Word 样式 | python-docx 处理 |
|---|---|---|
> 文字 |
Quote | doc.add_paragraph(style="Quote") |
代码块 |
No Spacing | 字体 Courier New 10pt,颜色 #333333 |
--- / *** |
段落底部边框 | OxmlElement 注入 w:pBdr |
\| 列 \| 列 \| |
Table Grid | doc.add_table(),表头行自动加粗 |
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_row;table_body下才有table_row -> table_cell两层结构。
| 项目 | 内容 |
|---|---|
| 存储路径 | ./tmp/(由 TEMP_DIR 配置) |
| 文件命名 | {safeName}_{randomToken}.doc,随机后缀防碰撞 |
| 当前清理机制 | 无自动清理,文件永久保留 |
expiresAt 字段 |
仅作为提示告知前端链接预期有效期,后端不做过期校验 |
待完善(后续迭代):
export_cache 表,BYTEA 存储),支持多实例部署| 环境变量 | 默认值 | 说明 |
|---|---|---|
TEMP_DIR |
./tmp |
临时文件存储目录 |
EXPORT_LINK_EXPIRES |
3600 |
下载链接有效期(秒) |
BASE_URL |
http://192.168.0.200:8000 |
服务对外地址,用于拼接 downloadUrl |
DEFAULT_STYLE_FILE |
./tmp/default.json |
默认样式文件路径;阶段 0 固定使用,阶段 1 作为兜底 |
| 文件 | 职责 |
|---|---|
app/api/v1/export.py |
路由层:参数接收、可选草稿同步、响应封装 |
app/schemas/export.py |
请求/响应 Pydantic 模型,字段别名(camelCase) |
app/services/export_service.py |
核心:DocxRenderer、load_style_file()、markdown_to_docx_bytes()、文件写入、链接生成 |
app/services/document_service.py |
可选路径:documentId 存在时调用 update_document() |
app/config.py |
temp_dir、base_url、export_link_expires、default_style_file 配置读取 |
app/core/exceptions.py |
ExportError 定义及全局 handler(返回 HTTP 500) |
tmp/default.json |
默认样式文件,阶段 0 固定使用 |
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