export-doc-content-mapping.md 19 KB

导出 API 设计文档

关联模块:app/api/v1/export.py · app/api/v1/export_records.py · app/services/export_service.py · app/services/export_record_service.py · app/services/storage_monitor.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/export/records · GET /api/v1/export/records/{recordId}/download · DELETE /api/v1/export/records/{recordId} · GET /api/v1/admin/storage
核心依赖 python-docx 1.1.2 · mistune 3.0.2
文件存储 服务器本地 ./tmp/{user_id}/{YYYY-MM-DD}/ 目录,按用户、按天分区,永久保留
下载链接 永久有效,无过期时间
默认样式文件 ./tmp/default.json(阶段 0 固定使用;阶段 1 起支持用户选择)
磁盘监控 后台定时任务每 30 分钟检查一次;每次导出后也触发实时检查

2. 端点详情

2.1 导出 .doc 文件

POST /api/v1/export/doc

用户在编辑器填写文件名、选择格式并确认后触发。后端将 Markdown 内容转换为 .doc 文件,按用户和日期分区存储,写入下载记录,返回永久下载链接。

请求

Headers

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

Body

{
  "userId": "user-xyz",
  "fileName": "2026年Q2季度报告",
  "format": "doc",
  "content": "# 2026年Q2季度报告\n\n## 一、背景\n\n本季度整体营收同比增长15%。\n\n...",
  "documentId": "doc-abc123",
  "styleId": null
}
字段 类型 必填 说明
userId string 当前用户 ID,用于文件分区存储和配额检查
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": {
    "recordId": "rec-abc123",
    "downloadUrl": "http://192.168.0.200:8000/api/v1/export/records/rec-abc123/download",
    "fileName": "2026年Q2季度报告_XnjI2ABA1kI.doc",
    "styleId": "default",
    "warning": null
  }
}

200 成功(存储超限)

{
  "code": 0,
  "message": "Success",
  "data": {
    "recordId": "rec-abc123",
    "downloadUrl": "http://192.168.0.200:8000/api/v1/export/records/rec-abc123/download",
    "fileName": "2026年Q2季度报告_XnjI2ABA1kI.doc",
    "styleId": "default",
    "warning": "您的存储空间已超出限额,请删除旧文件释放空间"
  }
}
字段 类型 说明
recordId string 下载记录 ID,可用于重新下载或删除
downloadUrl string 永久下载链接
fileName string 含扩展名的完整文件名
styleId string 实际使用的样式 ID;阶段 0 固定返回 "default"
warning string | null 存储超限时附带警告信息,正常时为 null

错误响应

HTTP 状态码 code 触发条件
500 500 Markdown 解析失败 / 文件写入失败

2.2 获取下载记录列表

GET /api/v1/export/records

分页查询当前用户的全部导出记录。

请求

参数 类型 必填 说明
userId string 当前用户 ID(Query 参数)
page integer 页码,默认 1
pageSize integer 每页数量,默认 20,最大 100
sortOrder string asc / desc,默认 desc

响应

{
  "code": 0,
  "data": {
    "records": [
      {
        "recordId": "rec-abc123",
        "userId": "user-xyz",
        "fileName": "2026年Q2季度报告_XnjI2ABA1kI.doc",
        "fileSize": 204800,
        "downloadUrl": "http://192.168.0.200:8000/api/v1/export/records/rec-abc123/download",
        "documentId": "doc-abc123",
        "styleId": "default",
        "createdAt": 1680000000000
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 20,
      "total": 5,
      "totalPages": 1
    }
  }
}

2.3 重新下载文件

GET /api/v1/export/records/{recordId}/download

根据记录 ID 查找本地文件,触发浏览器下载。后端查询时同时校验 recordId + userId,查不到一律返回 404。

请求

参数 类型 必填 说明
recordId string 路径参数
userId string 当前用户 ID(Query 参数)

响应

200 成功

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

<二进制文件内容>

错误响应

HTTP 状态码 code 触发条件
404 4041 记录不存在、文件已被删除,或 recordId 与 userId 不匹配

2.4 删除下载记录

DELETE /api/v1/export/records/{recordId}

同步删除磁盘文件和数据库记录(硬删除)。后端查询时同时校验 recordId + userId,查不到一律返回 404。

请求

参数 类型 必填 说明
recordId string 路径参数
userId string 当前用户 ID(Query 参数)

响应

{
  "code": 0,
  "message": "Record deleted successfully"
}

错误响应

HTTP 状态码 code 触发条件
404 4041 记录不存在,或 recordId 与 userId 不匹配

2.5 管理端存储查询

GET /api/v1/admin/storage

查看 tmp/ 目录总占用、各用户占用及配额情况。

响应

{
  "code": 0,
  "data": {
    "diskTotalBytes": 107374182400,
    "diskUsedBytes": 53687091200,
    "tmpTotalBytes": 10737418240,
    "quotaBytes": 53687091200,
    "quotaExceeded": false,
    "activeUsers": 5,
    "perUserQuotaBytes": 10737418240,
    "users": [
      {
        "userId": "user-xyz",
        "usedBytes": 204800,
        "quotaExceeded": false
      }
    ]
  }
}
字段 说明
diskTotalBytes 服务器磁盘总容量(字节)
diskUsedBytes 磁盘已使用容量(字节)
tmpTotalBytes tmp/ 目录当前占用(字节)
quotaBytes tmp/ 目录配额上限,即磁盘总量的 50%
quotaExceeded tmp/ 总占用是否超过配额
activeUsers tmp/ 下实际有文件的用户数
perUserQuotaBytes 每人均分配额,即 quotaBytes ÷ activeUsers
users[].usedBytes 该用户目录占用(字节)
users[].quotaExceeded 该用户是否超过均分配额

3. 内部处理流程

3.1 导出流程

POST /api/v1/export/doc
        |
        v
参数校验(ExportDocRequest)
  - userId 不为空
  - fileName 最大 255 字符
  - format 仅接受 "doc"
  - content 不为空
        |
        v
  documentId 存在?
    |           |
   Yes          No
    |           |
    v           |
同步更新草稿记录  |
PUT documents/{id}
(失败不阻断导出)
    |           |
    └─────┬─────┘
          |
          v
  ┌─────────────────────────────┐
  │        样式文件解析           │
  │                             │
  │  styleId 传入且有效?         │
  │    Yes → 从样式库加载对应     │
  │          JSON 文件           │
  │    No  → 加载默认样式文件     │
  │          ./tmp/default.json  │
  └─────────────┬───────────────┘
                │
                v
export_service.export_doc()
  1. load_style_definitions(style_file)
  2. markdown_to_docx_bytes(content, styles)
  3. _safe_filename(fileName)
  4. 写入 ./tmp/{user_id}/{YYYY-MM-DD}/{name}_{token}.doc
  5. 写入 export_records 数据库记录
          |
          v
storage_monitor.check_after_export(user_id)
  检查 tmp/ 总占用 > 磁盘 50%?
    → 是:给所有有文件的用户发警告 + 记录管理员日志
  检查该用户目录 > 均分配额?
    → 是:响应附带 warning 字段
          |
          v
返回 ExportDocResponse
  { recordId, downloadUrl, fileName, styleId, warning }

3.2 磁盘监控流程

应用启动
    │
    ├── 后台定时任务(每 30 分钟)
    │       │
    │       v
    │   扫描 tmp/ 总占用
    │       │
    │       ├── 超 50%?→ 给所有有文件用户发警告 + 管理员日志
    │       │
    │       └── 遍历各用户目录
    │               │
    │               └── 超均分额度?→ 给该用户发警告
    │
    └── 每次导出后触发一次实时检查(同上逻辑)

3.3 样式加载优先级

请求携带 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/{user_id}/{YYYY-MM-DD}/(按用户、按天分区)
文件命名 {safeName}_{randomToken}.doc,随机后缀防碰撞
清理机制 用户主动删除记录时同步删除磁盘文件(硬删除)
配额限制 tmp/ 总占用不超过磁盘 50%;单用户不超过均分额度(50% ÷ 活跃用户数)
超限行为 导出仍然成功,响应附带 warning 字段;同时触发用户和管理员通知

7. 配置项

环境变量 默认值 说明
TEMP_DIR ./tmp 文件存储根目录
BASE_URL http://192.168.0.200:8000 服务对外地址,用于拼接 downloadUrl
DEFAULT_STYLE_FILE ./tmp/default.json 默认样式文件路径;阶段 0 固定使用,阶段 1 作为兜底
DISK_QUOTA_RATIO 0.5 tmp/ 目录占磁盘总量的配额比例

8. 涉及代码文件

文件 职责
app/api/v1/export.py 路由层:导出端点,参数接收、可选草稿同步、响应封装
app/api/v1/export_records.py 路由层:下载记录列表、重新下载、删除记录
app/schemas/export.py 请求/响应 Pydantic 模型,字段别名(camelCase)
app/services/export_service.py 核心:DocxRendererload_style_file()markdown_to_docx_bytes()、文件写入
app/services/export_record_service.py 下载记录 CRUD(写入、查询、硬删除)
app/services/storage_monitor.py 磁盘配额检查、后台定时任务、警告触发逻辑
app/services/document_service.py 可选路径:documentId 存在时调用 update_document()
app/models/export_record.py ExportRecord ORM 模型
app/config.py temp_dirbase_urldefault_style_filedisk_quota_ratio 配置读取
app/core/exceptions.py ExportError 定义及全局 handler(返回 HTTP 500)
tmp/default.json 默认样式文件,阶段 0 固定使用

9. 请求示例

导出文件

curl -X POST http://192.168.0.200:8000/api/v1/export/doc \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user-xyz",
    "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
  }'

预期响应

{
  "code": 0,
  "message": "Success",
  "data": {
    "recordId": "rec-abc123",
    "downloadUrl": "http://192.168.0.200:8000/api/v1/export/records/rec-abc123/download",
    "fileName": "2026年Q2季度报告_XnjI2ABA1kI.doc",
    "styleId": "default",
    "warning": null
  }
}

获取下载记录列表

curl "http://192.168.0.200:8000/api/v1/export/records?userId=user-xyz&page=1&pageSize=20"

重新下载

curl -O "http://192.168.0.200:8000/api/v1/export/records/rec-abc123/download?userId=user-xyz"

删除记录

curl -X DELETE "http://192.168.0.200:8000/api/v1/export/records/rec-abc123?userId=user-xyz"

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