--- todos: - id: "finalize-article-schema" content: "course_article 增加 content_json(含 nameList/skipWords);正文不再依赖 course_article_sentence" status: pending - id: "nce-import-command" content: "NCE_*.json → course + course_article;导入时写入 skipWords/nameList(人名/地名免敲)" status: pending - id: "article-api" content: "GET 课文 API:content_json 直出 TypeWords Article DTO(nameList 供默写/听写跳过)" status: pending - id: "article-vocab-api" content: "课文生词 API:course_article_vocab(词头)+ vocab_sense + course_word_sense,与正文分离" status: pending - id: "content-patch-admin" content: "低频勘误:PATCH content_json 的 text/translate/skipWords,或单课重导" status: pending isProject: false --- # 课文内容存储方案建议(修订 v2) ## 结论 - **课文正文**:`course_article.content_json`(几乎不改,偶发勘误) - **课文生词**:关系表(`course_article_vocab` + `course_word_sense`) - **人名/地名免敲**:写在 `content_json` 的 **`skipWords`**(API 层映射为 TypeWords 的 `nameList`) --- ## 人名 / 地名:练习时不用敲 ### 需求 默写或听写课文时,**人名、地名**应预先标注,练习流程 **自动跳过**,不要求学生输入(如 `Silbury`、`Pinhurst`、`James`、`Scott`)。 ### TypeWords 已有能力(可直接复用) [`TypingArticle.vue`](TypeWords/packages/core/src/components/article/TypingArticle.vue) 已支持 `article.nameList`: - 命中 `nameList` 的 token 在 `next()` 时 **自动跳过** - 内置跳过:`Mr` / `Mrs` / `Ms` / `Dr` / `Miss` - 多词条目 `"James Scott"` 会拆成 `james`、`scott` 分别匹配 **不需要**在正文里加 `{Silbury}` 这类 inline 标记;**维护一份免敲词表**即可。 ### 在 `content_json` 里怎么存 推荐 **两层字段**(存库清晰 + 对接 TypeWords 零改动): ```json { "title": "No wrong numbers", "text": "Mr.James Scott has a garage in Silbury ...", "textTranslate": "...", "audioSrc": "/sound/article/nce2-1/No wrong numbers.mp3", "lrcPosition": [[15.45, 24.87]], "skipWords": [ { "token": "James", "type": "person" }, { "token": "Scott", "type": "person" }, { "token": "Silbury", "type": "place" }, { "token": "Pinhurst", "type": "place" } ] } ``` | 字段 | 用途 | |------|------| | `skipWords[]` | collin **权威存储**;`type`: `person` / `place`(可扩展 `org`) | | `nameList` | **API 输出** 时由 `skipWords[].token` 去重生成为 TypeWords 字段(可不落库重复存) | Lesson5 示例映射: ``` skipWords → nameList: ["James", "Scott", "Silbury", "Pinhurst"] (Mr 已由 TypeWords 内置跳过) ``` ### 练习端行为 ```mermaid flowchart LR API["GET 课文 API"] Map["skipWords → nameList"] TW["TypeWords TypingArticle"] Skip["isNameWord 自动 next"] API --> Map --> TW --> Skip ``` - **默写 / 听写**:同一套 `nameList` 逻辑 - **UI 可选增强**(后续):按 `skipWords.type` 在课文预览里给人名/地名不同样式(不影响是否跳过) ### 导入时谁维护 skipWords 1. **人工标注**(推荐 MVP):导入 NCE 时在源 JSON 或 Excel 补一列「免敲词」 2. **半自动**:导入脚本从 `text` 匹配已知的本课 `course_article_vocab` 之外的专名词表(准确度有限) 3. **勘误**:只改 `content_json.skipWords`,`content_version + 1` **不要**单独建 `course_article_skip_token` 表 — 与「几乎不改」的 JSON 方案一致,免敲词随课文打包即可。 --- ## 课文正文:content_json(不变) | 层级 | 存储 | |------|------| | 正文 | `course_article.content_json` | | 元数据 | `course_article` 列:`lesson_no`, `title`, `audio_url`, `status` | | 生词 | 关系表(一词一行 + 义项) | | 词典 | `word` / `dict` | `content_json` **不含** `newWords`;**含** `skipWords`(人名/地名)。 完整示例: ```json { "title": "A private conversation", "titleTranslate": "私人谈话", "text": "...", "textTranslate": "...", "audioSrc": "...", "lrcPosition": [], "skipWords": [ { "token": "theatre", "type": "place" } ], "quote": null, "question": null } ``` --- ## 与 TypeWords 字段对照 | TypeWords `Article` | collin 来源 | |---------------------|-------------| | `title`, `text`, `textTranslate`, `lrcPosition`, `audioSrc` | `content_json` | | `nameList` | API 由 `skipWords[].token` 生成 | | `sections` | 前端 `genArticleSectionData(text)` | | 生词 | 独立 API,不进 JSON | --- ## 对迁移 / API 的调整 1. `course_article.content_json` 文档化 **`skipWords`** 结构 2. `ArticleApiService`:读 JSON → 填充 `nameList` → 返回 TypeWords DTO 3. NCE 导入 Command:支持从源文件 `nameList` 迁移,或从 sidecar 配置读 `skipWords` 4. 废弃 `course_article_sentence`(正文不再用句子表) --- ## 实施优先级 1. 定稿 `content_json` schema(含 `skipWords`) 2. NCE 导入 + 首批课文手工标注免敲词 3. GET 课文 API(`skipWords` → `nameList`) 4. 生词 API(与正文分离) 5. 勘误:PATCH `text` / `translate` / `skipWords`