# Content Repository(候选查询与数据访问层)|Tasks > 对应计划:`spec_kit/Personalized Reco/modules/content-repository/plan.md` > > 本清单已对齐确认点: > > - `text` 按客户端 **locale** 输出(请求携带) > - **不允许语言回退**(缺少目标语言文本的内容直接过滤,不返回) > - 需要做 **MySQL(dev)DB 集成测试** > - 代码与其他推荐子模块放同一目录:`server/app/features/personalized_reco/` > - `fetch_contents_by_ids` 返回顺序必须与输入 `content_ids` **一致** > > 执行说明: > > - 本次已在 dev MySQL 环境跑通 `pytest`,且测试不做破坏性操作: > - 不清表(不执行 DELETE/TRUNCATE) > - 每个用例使用事务并在结束时 rollback > - 默认不执行 Alembic 迁移(如需自动迁移需显式设置 `ALLOW_SCHEMA_MIGRATION=1`) --- ## 0. 任务标记规则 - 用勾选框标记执行状态: - `[ ]` 未开始 - `[x]` 已完成 - 每个任务都要求可独立验收(有明确产出/可运行的检查方式)。 --- ## 1. 文档对齐(先把口径写死,避免实现漂移) - [x] 1.1 更新 `modules/content-repository/spec.md`,加入 locale 相关契约 - **变更点**: - 在输入中新增 `locale`(例如 `en`/`en-US`/`tc`/`zh-TW`/`zh-HK`),声明由客户端请求携带并透传至 repository - 在输出字段 `text` 补充语言选择规则: - `locale=en*`:必须从 `text_en` 产出;若缺失则该内容不可返回(过滤) - `locale=zh-TW|zh-HK`:必须从 `text_tc` 产出;若缺失则该内容不可返回(过滤) - 说明:当前仅支持 EN/TC(不做繁转简);若未来新增 `zh-CN` 再单独设计转换策略 - 明确:`fetch_contents_by_ids` **返回顺序与入参一致** - **验收**:`spec.md` 中输入/输出与接口签名不再缺少 locale,且 `text` 的来源与“不允许语言回退”规则清晰。 - [x] 1.2 更新 `modules/content-repository/plan.md`,补齐 locale 选文与“繁转简”技术方案 - **变更点**: - 在“读取层规范化(Normalization)”新增 `text` 规范化章节:语言选择 + 简中转换 - 说明:当前仅支持 EN/TC(不做繁转简);若未来新增 `zh-CN` 再单独设计转换策略 - 明确策略:**不允许语言回退**;缺少目标语言文本的内容视为不可用,必须在候选/按 ID 获取时过滤掉 - **验收**:`plan.md` 有明确依赖与落地策略(含依赖包/转换时机/兜底策略),不留二义性。 --- ## 2. 目录与骨架(与推荐子模块同级) - [x] 2.1 新建目录 `server/app/features/personalized_reco/content_repository/` - **包含**: - `__init__.py` - `types.py`(DTO:`ContentProfile`、locale 类型等) - `interface.py`(`ContentRepository` Protocol/ABC) - `normalization.py`(解析与兜底:suitability、risk_flags、power、text) - `sqlalchemy_repo.py`(SQLAlchemy 实现) - **验收**:可被 `app.features.personalized_reco.content_repository.*` 正常 import。 - [ ] 2.2 依赖补齐(如采用 OpenCC) - **说明**:当前仅支持 EN/TC,此任务可跳过;若未来新增 `zh-CN` 并需要繁转简,再引入 OpenCC。 --- ## 3. 数据结构与接口(面向引擎注入) - [x] 3.1 定义 `ContentProfile` DTO(稳定字段契约) - **字段**:对齐 `modules/content-repository/spec.md`,并补齐 `review_confidence` 输出兜底为 `0.7` - **注意**:`text` 为最终对外输出文本(已按 locale 选择/转换) - **验收**:DTO 字段齐全;类型清晰;不暴露 ORM 模型。 - [x] 3.2 定义 `ContentRepository` 接口(含 locale) - **建议签名**(示例,最终以 spec 为准): - `fetch_candidates(scene, user_profile, fallback_level, limit, locale, exclude_content_ids=None) -> List[ContentProfile]` - `fetch_contents_by_ids(content_ids, locale) -> List[ContentProfile]` - **验收**:推荐引擎可以仅依赖该接口,不依赖 SQLAlchemy/FastAPI Depends。 --- ## 4. 规范化工具函数(可单测) - [x] 4.1 suitability 解析与兜底 - **规则**: - 缺失/NULL/解析失败 → 全 0.5(固定 key 集合) - 部分 key 缺失 → 对缺失 key 补 0.5 - 非法值 → 兜底 0.5 - **验收**:单元测试覆盖缺失/部分缺失/非法值。 - [x] 4.2 risk_flags 映射、去重与排序 - **规则**:旧→新映射对齐 spec;去重;稳定排序(例如字典序) - **验收**:单元测试断言输出不含旧命名且顺序稳定。 - [x] 4.3 personalization_power 映射 - **规则**:`0/5/10 -> 0.0/0.5/1.0`;非法值 -> 0.0 - **验收**:单元测试覆盖正常/异常值。 - [x] 4.4 text 选择与简中转换 - **输入**:`text_en`、`text_tc`、`locale` - **规则**:按 1.1/1.2 写死的策略执行(当前仅支持 EN/TC,不做繁转简) - **验收**:单元测试覆盖: - `en` 取英文 - `zh-TW` 取繁中 - 缺失目标语言文本时的行为:返回“不可用”(例如返回空字符串 + 上层过滤,或直接返回 `None` 由调用方过滤;实现中必须一致) --- ## 5. SQLAlchemy 实现(无 N+1、顺序可控) > 说明:当前 DB 模型为: > > - `contents`:`text_en` / `text_tc` / `author_id` / `template_id` > - `content_profiles`:JSON、power、stage、is_safe_pool、review_confidence > - `content_risk_flags`:关联表(1:N) - [x] 5.1 实现 `fetch_contents_by_ids(content_ids, locale)` - **实现要点**: - 输入去重,但输出必须按原始输入顺序重排(并忽略不存在的 id 或明确行为:不存在则跳过) - 两段式查询避免行膨胀: 1) `contents` JOIN `content_profiles` 批量取主体与画像 2) `content_risk_flags` 批量取 flags,再按 `content_id` 聚合 - 组装 DTO 时执行 normalization(含 text locale 规则) - **验收**: - 返回顺序与输入一致 - 缺失目标语言文本的 content_id 不返回(跳过,不做语言回退) - 不产生按 id 循环查 flags 的查询(查询次数为常数级) - [x] 5.2 实现 `fetch_candidates(scene, user_profile, fallback_level, limit, locale, exclude_content_ids)` - **实现要点**: - 计算 `effective_fallback_level`: - 若画像 need/context/emotion 任一缺失,则 `effective_fallback_level = max(fallback_level, 1)` - DB 粗过滤对齐 plan: - L1:`personalization_power <= 5` - L2:`personalization_power = 0` 且 `stage = general` - L3:`is_safe_pool = true` 且 `stage = general` 且 `personalization_power = 0`(如需更严格可在此明确) - 排序(V1):按 `content_profiles.updated_at DESC` 或 `contents.updated_at DESC`(择一写死并记录) - 两段式候选: 1) 先查候选 id(`LIMIT limit * multiplier`) 2) 调用 `fetch_contents_by_ids` 补全字段 - locale 文本存在性过滤: - `locale=en*`:`contents.text_en IS NOT NULL` - `locale=zh-*`:`contents.text_tc IS NOT NULL` - 输出顺序: - 返回顺序按候选 id 列表顺序(用于后续引擎打分/重排);最终截断至 `limit` - **验收**: - 在不同 `effective_fallback_level` 下能返回候选 - 不返回缺少目标语言文本的内容(不做语言回退) - 查询次数为常数级(不随 `limit` 线性增长) --- ## 6. DB 集成测试(dev MySQL) - [x] 6.1 建立测试目录与 pytest 配置 - **目标**:在 `server/` 内新增 `tests/`(或 `app/**/__tests__/`,但建议统一为 `server/tests/`) - **内容**: - `server/tests/conftest.py`:提供 AsyncEngine/AsyncSession、清库策略、query count 统计工具 - 测试运行约定:通过 `DATABASE_URL` 指向 dev 测试库(建议单独库名,例如 `mindfulness_dev_test`) - **验收**:`pytest` 可在 `server/` 下运行并发现测试。 - [x] 6.2 测试库 schema 初始化(用 Alembic) - **策略**(二选一写死): - A:测试启动时 `alembic upgrade head`(确保 schema 最新) - B:在 CI/本地提前准备库,仅在测试中清表 - **验收**:测试运行前 schema 可用,且不会污染开发主库数据(推荐使用独立 test 库)。 - [x] 6.3 集成测试用例:`fetch_contents_by_ids` - **准备数据**:插入最小内容 2~3 条(覆盖 text_en/text_tc 缺失组合)、profiles、flags(含旧 flag) - **断言**: - 返回顺序与输入一致 - `en` 不返回 `text_en` 缺失的内容(不回退 `text_tc`) - risk_flags 映射后不含旧命名 - `review_confidence` NULL → 0.7 - **验收**:测试稳定通过。 - [x] 6.4 集成测试用例:`fetch_candidates` - **准备数据**:覆盖 `personalization_power` 0/5/10、`is_safe_pool` true/false、不同 stage - **断言**: - L1/L2/L3 粗过滤生效 - `exclude_content_ids` 生效 - 查询次数为常数级(用 before_cursor_execute 计数) - **验收**:测试稳定通过。 --- ## 7. 最终自检清单(合入前) - [x] 7.1 文档一致性检查 - `spec.md` / `plan.md` / 实现接口签名三者一致(尤其是 `locale` 与 `text` 输出规则) - [x] 7.2 性能检查(最小) - `fetch_contents_by_ids` / `fetch_candidates` 查询次数断言通过(无 N+1) - [x] 7.3 回归检查 - 不影响现有 `user_profile_scoring` 模块与迁移脚本(仅新增模块与测试)