forked from wangziqi/gongxue-base
267 lines
7.0 KiB
Markdown
267 lines
7.0 KiB
Markdown
# 内容导入契约
|
||
|
||
更新时间: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`
|
||
|
||
## 单词导入
|
||
|
||
推荐新格式:
|
||
|
||
```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`。
|
||
|
||
## 下一步
|
||
|
||
- 增加 Excel/CSV 解析入口,但解析后仍进入同一套 preview/import 管线。
|
||
- 增加异步 worker,处理大批量导入、重试和导入后校验。
|