Files
gongxue-base/docs/refactor/content-import-contract.md
2026-06-29 14:02:38 +08:00

403 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 内容导入契约
更新时间2026-06-29
## 结论
内容导入的最终规范化、校验、租户隔离、幂等和审计必须由后端负责。
前端只负责:
- 上传或粘贴 JSON/Excel/CSV。
- 做轻量格式预检查,减少明显错误。
- 展示后端 preview 返回的 `job/items/issues`
- 让运营人员修正数据后再确认导入。
迁移脚本只负责:
- 从 PocketBase 导出或旧 JSON 中抽取数据。
- 转换成新架构推荐格式。
- 调用后端 preview/import API。
不建议迁移脚本直接绕过后端写业务表,除非是一次性内控迁移,并且必须额外跑导入后校验。
## 已实现导入 API
```text
POST /api/tenant-content/imports/preview/questions
POST /api/tenant-content/imports/questions
POST /api/tenant-content/imports/preview/vocabulary
POST /api/tenant-content/imports/vocabulary
POST /api/tenant-content/imports/preview/handbook
POST /api/tenant-content/imports/handbook
POST /api/tenant-content/imports/preview/scoreline
POST /api/tenant-content/imports/scoreline
POST /api/tenant-content/imports/preview/videos
POST /api/tenant-content/imports/videos
GET /api/tenant-content/imports
GET /api/tenant-content/imports/issues
GET /api/tenant-content/imports/field-mapping
GET /api/tenant-content/imports/templates
POST /api/tenant-content/imports/post-check
GET /api/tenant-content/imports/post-check
```
所有导入都会写入:
- `content_import_jobs`
- `content_import_items`
- `content_import_issues`
- `audit_logs`
## 格式支持
当前题目、单词、知识手册、分数线和视频导入均支持:
- `sourceFormat=json`:直接提交规范 JSON 或兼容旧模板。
- `sourceFormat=csv`:提交 `csvText``fileContent``payload``fileBase64`,后端按表头归一化。
- `sourceFormat=excel`:提交 `.xlsx``fileBase64`,可选 `sheetName``sheetIndex`;多 Sheet 会被解析进同一个 `content_import_jobs`
- 同步导入默认在 API 请求内完成;大批量导入可在确认导入时传 `executionMode=async`API 会把 job 置为 `pending`,由 imports worker 后台消费。
表格导入安全边界:
- 单文件最大 8MB。
- 单次最多解析 5000 行、160 列。
- 单个单元格最多保留 100000 字符。
- 后端只保存解析后的原始行和规范化 payload不把 `fileBase64` 长期写入数据库。
- 任务列表会返回 `sourceFormat``executionMode``parserMetadata`前端可展示解析器、Sheet、行数等信息。
异步导入调用方式:
```json
{
"previewJobId": "uuid",
"executionMode": "async",
"allowPartial": false
}
```
异步导入状态流:
```text
preview -> pending -> importing -> completed
preview -> pending -> importing -> completed_with_errors
preview -> pending -> failed
```
worker 命令:
```bash
npm --workspace @tiku-saas/worker run imports:once
```
前端提交异步导入后不要重复同步执行同一 job只需要轮询 `GET /api/tenant-content/imports` 并用 `GET /api/tenant-content/imports/issues` 展示问题行。worker 会按 `attempt_count/max_attempts` 记录重试,失败时写入 `errorMessage` 和审计日志。
## 模板、字段映射和导入后复检
租户后台前端不要把导入字段写死在页面里。导入页初始化时先读取字段映射,下载模板时调用模板接口:
```text
GET /api/tenant-content/imports/field-mapping?importType=questions
GET /api/tenant-content/imports/templates?importType=questions&format=csv
```
`importType` 支持:
```text
questions | vocabulary | handbook | scoreline | videos
```
`templates` 返回:
```json
{
"item": {
"importType": "questions",
"format": "csv",
"fileName": "questions-import-template.csv",
"mimeType": "text/csv; charset=utf-8",
"contentBase64": "...",
"fields": []
}
}
```
前端可用 `contentBase64` 生成下载文件,或用 `contentPreview` 做在线预览。`fields` 包含规范字段、中文别名、是否必填和示例,适合做字段映射 UI。
CSV/Excel 支持本次导入字段别名覆盖,字段名必须是后端支持的规范字段。后端会按导入类型做目标字段白名单校验,并拒绝 `__proto__``constructor``prototype` 等危险对象键;前端不能把字段映射当成绕过后端 schema 的扩展机制。JSON 导入应直接提交规范字段,通常不需要 `fieldMappingOverrides`
字段别名覆盖示例:
```json
{
"sourceFormat": "csv",
"sourceName": "questions-custom-headers.csv",
"csvText": "旧编号,自定义题干,左选项,右选项,正确项\nq1,题干,A,B,B",
"fieldMappingOverrides": {
"legacyId": ["旧编号"],
"content": ["自定义题干"],
"optionA": ["左选项"],
"optionB": ["右选项"],
"answer": ["正确项"]
}
}
```
导入完成后,租户后台应主动触发复检:
```json
POST /api/tenant-content/imports/post-check
{
"jobId": "uuid"
}
```
复检会确认:
- job 行数、有效行、已处理行是否一致。
- 处理成功的 item 是否都有 `target_id`
- 题目是否存在当前版本,且已绑定目标集合。
- 单词单元是否存在且有单词。
- 知识手册是否存在章节/条目。
- 分数线字段、院校、专业、记录是否都落到对应表。
- 视频是否存在题目绑定。
复检结果会写入 `content_import_jobs.summary.importPostCheck`,也可以通过:
```text
GET /api/tenant-content/imports/post-check?jobId=<jobId>
```
读取。异步导入未完成时触发复检会返回 `IMPORT_JOB_NOT_COMPLETED`
推荐 CSV/Excel 表头:
| 类型 | 常用表头 |
| --- | --- |
| 题目 | `legacyId``题型/type``题干/content``选项A`-`选项H``答案``解析``难度``标签` |
| 单词 | `unitLegacyId``unitName``wordLegacyId``word``phonetic``meaning``example``difficulty``tags` |
| 知识手册 | `subjectLegacyId``subjectName``chapterLegacyId``chapterName``sectionName``entryLegacyId``title``content``summary``tags` |
| 分数线 | 多 Sheet 推荐 `fields``schools``majors``records`;记录 Sheet 可包含任意动态字段列 |
| 视频 | `legacyId``title``videoUrl``thumbnailUrl``durationSeconds``subjectId``questionId``legacyQuestionId``accessMode` |
## 单词导入
推荐新格式:
```json
{
"regionId": "uuid",
"entryId": "uuid",
"contentNodeId": "uuid",
"units": [
{
"legacyId": "unit-1",
"name": "Unit 1 - 高频核心词",
"description": "专升本考试高频词汇",
"order": 1,
"words": [
{
"legacyId": "word-abandon",
"word": "abandon",
"phonetic": "/əˈbændən/",
"meaning": "v. 放弃,抛弃",
"example": "He had to abandon his car in the snow.",
"exampleTranslation": "他不得不把车丢弃在雪地里。",
"difficulty": 3,
"tags": ["高频词"],
"order": 1
}
]
}
]
}
```
兼容旧模板:
- `vocabulary_units_示例数据`
- `vocabulary_示例数据`
后端会把旧模板归一化为 `vocabulary_units/vocabulary_words`,并可自动挂到 `content_entries/content_nodes`
## 知识手册导入
推荐新格式:
```json
{
"regionId": "uuid",
"entryId": "uuid",
"contentNodeId": "uuid",
"subjects": [
{
"legacyId": "handbook-chinese",
"name": "大学语文",
"type": "guide",
"chapters": [
{
"legacyId": "chapter-outline",
"name": "一、语文考纲",
"sections": [
{
"legacyId": "section-outline",
"name": "考纲解读",
"entries": [
{
"legacyId": "entry-outline",
"title": "2024年天津专升本语文考试大纲",
"summary": "全面解读语文考试要求",
"content": "Markdown 内容",
"tags": ["考纲"]
}
]
}
]
}
]
}
]
}
```
映射规则:
- 手册入口:`content_entries.entry_type = handbook`
- 书籍/科目:`handbook_subjects`,并生成或绑定一个 `content_nodes`
- 章节:`handbook_chapters`,并生成章节节点
- 小节:默认进入 `content_nodes`,作为知识点的目录节点
- 知识点:`handbook_entries`,通过 `content_node_id` 归属到小节或章节
## 题目导入
题目继续兼容旧题库 JSON 数组格式,并支持 Markdown、KaTeX、图片、表格、阅读理解子题等字段。导入时可以传
- `subjectId`
- `categoryId`
- `entryId`
- `contentNodeId`
- `collectionId`
这样题目会同时落到旧兼容表和新内容导航/题目集合。
## 分数线导入
分数线导入支持字段、院校、专业、年份记录一起提交,适合把旧题库地区分数线 JSON 转成新结构后统一 preview/import。
推荐格式:
```json
{
"regionId": "uuid",
"sourceName": "scoreline-tianjin-2025.json",
"fields": [
{
"legacyId": "tj-min-score",
"fieldKey": "minScore",
"fieldName": "最低分",
"fieldType": "number",
"unit": "分",
"isFilter": true,
"isTrend": true,
"order": 1
}
],
"schools": [
{
"legacyId": "school-a",
"name": "天津测试学院",
"shortName": "测试学院",
"schoolType": "public",
"isHot": true
}
],
"majors": [
{
"legacyId": "major-a",
"schoolLegacyId": "school-a",
"name": "软件工程"
}
],
"records": [
{
"legacyId": "record-a-2025",
"schoolLegacyId": "school-a",
"majorLegacyId": "major-a",
"year": 2025,
"fieldValues": {
"minScore": 188,
"planCount": 60
}
}
]
}
```
校验规则:
- `kind` 可为 `field/school/major/record`;使用 `fields/schools/majors/records` 分桶时后端会自动补。
- `record` 必须能通过 `schoolId``schoolLegacyId``schoolName` 定位院校。
- `major` 必须能通过 `schoolId``schoolLegacyId``schoolName` 定位院校。
- `fieldValues` 保存动态字段值,前端筛选和趋势图应先读取 `/api/scoreline/fields`
- 相同 `legacyId` 再导入会幂等更新;内容 hash 未变化时计入 `skipped`
## 视频导入
视频导入支持视频基础信息和题目绑定一起提交。付费视频不要在列表页暴露可播放 URL播放仍走 `POST /api/videos/play` 做权益和签名校验。
推荐格式:
```json
{
"sourceName": "question-videos.json",
"videos": [
{
"legacyId": "video-001",
"title": "函数极限精讲",
"description": "题目解析视频",
"videoUrl": "https://example.com/private/video-001.mp4",
"thumbnailUrl": "https://example.com/thumb/video-001.jpg",
"durationSeconds": 120,
"knowledgeTags": ["高数", "极限"],
"subjectId": "uuid",
"accessMode": "svip",
"freePreviewSeconds": 15,
"bindings": [
{
"legacyId": "video-001-question-001",
"questionId": "uuid",
"videoType": "specific",
"order": 1
}
]
}
]
}
```
校验规则:
- `title` 必填。
- `accessMode` 可为 `free``svip``video_quota`
- 绑定题目必须提供 `questionId``legacyQuestionId`,且题目必须属于当前租户。
- `assetId` 可以绑定 `content_assets` 台账资源;生产建议优先用资源台账和签名播放,不让前端长期持有私有视频 URL。
- 导入成功会写 `question_videos`,并把题目 `has_video_explanation` 标记为 true。
## 幂等规则
- 优先使用 `legacyId` 作为跨迁移稳定标识。
- 没有 `legacyId` 时,后端按导入 job 和行号生成内部标识。
- 相同 `legacyId` 再导入会更新。
- 内容 hash 未变化时标记为 `skipped`
## 下一步
- 已补 `npm run pb:import:dry-run` 静态报告工具;拿到真实 PocketBase 全量导出数据后做多轮 dry-run并把 dry-run 报告、正式导入报告和复检报告作为上线验收材料。
- Taro 租户内容页已接模板下载、可视化字段映射第一版、导入 job 详情/轮询、逐行 issue 展示和复检结果面板;继续补完整目标入口/集合选择表单。
- 后续按大租户数据量补导入任务分页预览、抽样校验和导入性能压测。