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

7.0 KiB
Raw Blame History

内容导入契约

更新时间2026-06-29

结论

内容导入的最终规范化、校验、租户隔离、幂等和审计必须由后端负责。

前端只负责:

  • 上传或粘贴 JSON/Excel/CSV。
  • 做轻量格式预检查,减少明显错误。
  • 展示后端 preview 返回的 job/items/issues
  • 让运营人员修正数据后再确认导入。

迁移脚本只负责:

  • 从 PocketBase 导出或旧 JSON 中抽取数据。
  • 转换成新架构推荐格式。
  • 调用后端 preview/import API。

不建议迁移脚本直接绕过后端写业务表,除非是一次性内控迁移,并且必须额外跑导入后校验。

已实现导入 API

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

单词导入

推荐新格式:

{
  "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

知识手册导入

推荐新格式:

{
  "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。

推荐格式:

{
  "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 必须能通过 schoolIdschoolLegacyIdschoolName 定位院校。
  • major 必须能通过 schoolIdschoolLegacyIdschoolName 定位院校。
  • fieldValues 保存动态字段值,前端筛选和趋势图应先读取 /api/scoreline/fields
  • 相同 legacyId 再导入会幂等更新;内容 hash 未变化时计入 skipped

视频导入

视频导入支持视频基础信息和题目绑定一起提交。付费视频不要在列表页暴露可播放 URL播放仍走 POST /api/videos/play 做权益和签名校验。

推荐格式:

{
  "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 可为 freesvipvideo_quota
  • 绑定题目必须提供 questionIdlegacyQuestionId,且题目必须属于当前租户。
  • assetId 可以绑定 content_assets 台账资源;生产建议优先用资源台账和签名播放,不让前端长期持有私有视频 URL。
  • 导入成功会写 question_videos,并把题目 has_video_explanation 标记为 true。

幂等规则

  • 优先使用 legacyId 作为跨迁移稳定标识。
  • 没有 legacyId 时,后端按导入 job 和行号生成内部标识。
  • 相同 legacyId 再导入会更新。
  • 内容 hash 未变化时标记为 skipped

下一步

  • 增加 Excel/CSV 解析入口,但解析后仍进入同一套 preview/import 管线。
  • 增加异步 worker处理大批量导入、重试和导入后校验。