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

320 lines
9.4 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
```
所有导入都会写入:
- `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` 和审计日志。
推荐 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`
## 下一步
- 增加导入后校验/复检,确认导入行数、目标表记录、题目集合绑定和视频绑定一致。
- 增加导入模板下载接口和后台可视化字段映射器。