# 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` 模块与迁移脚本（仅新增模块与测试）

