# 子模块：Content Repository（候选查询与数据访问层）｜Spec

## 1. 目标描述

提供推荐算法可注入的、与 ORM/SQL 解耦的数据访问接口：

- 按场景与用户画像拉取候选内容画像（Cᵢ）。
- 按 `content_id` 批量获取内容画像（去重/重排/补字段）。
- 对 risk_flags、suitability JSON 等“存储形态”做统一解析与兼容（对上提供稳定结构）。

> 规则口径：risk_flags 命名与语义严格按 `句子文案打分规则`；若历史数据存在旧 flag，需在读取层做一次映射（避免语义漂移）。
>
> 存储形态对齐 `modules/db-design/plan.md`：读取层需要将 `contents` + `content_profiles` + `content_risk_flags` 组装为上层稳定的 `ContentProfile` 结构。

---

## 2. 输入 / 输出定义

### 2.1 输入

- `scene`: `feed | push | widget`
- `user_profile`: 客户端问卷画像（V1.2；字段允许缺失）
- `locale`：客户端语言（由请求携带并透传至推荐模块；**当前仅支持 EN/TC**，例如 `en` / `en-US` / `tc` / `zh-TW` / `zh-HK`）
- `fallback_level`: `0|1|2|3`
- `limit`: 候选条数上限（由引擎配置）
- （可选）排除集合：`exclude_content_ids`（用于 DB 层先排一部分，减少传输；类型为 `List[int]`，与 MySQL 自增 `content_id` 对齐）
- 数据库会话/连接（实现层使用 `AsyncSession` 注入）

### 2.2 输出

- `List[ContentProfile]`（稳定字段契约）：
  - `content_id`：`int`（MySQL 自增主键）
  - `text`：按 `locale` 输出的文案文本（**不允许语言回退**）
    - `locale=en*`：必须从 `contents.text_en` 产出；若该 content 无 `text_en`，则该 content 不可返回（在候选/按 ID 获取时过滤）
    - `locale=tc/zh-TW/zh-HK`：必须从 `contents.text_tc` 产出；若无 `text_tc`，则该 content 不可返回
  - `stage`：`general | expecting | parenting | unknown`
  - `emotion_score`：`float | None`（约定：`None` 表示 general）
  - `context_suitability`：`Dict[str, float]`（见 4.1 的 key 集合；值为 `0/0.5/1`）
  - `need_suitability`：`Dict[str, float]`（见 4.1 的 key 集合；值为 `0/0.5/1`）
  - `personalization_power`：`float`（对上稳定口径为 `0/0.5/1`；若 DB 存 `0/5/10`，读取层需映射）
  - `risk_flags`：`List[str]`（已做旧→新映射、去重；命名只允许 `unsafe_for_* / block_* / soft_*`）
  - 可选：`author_id`、`template_id`、`review_confidence`

### 2.3 数据存储形态（对齐 DB 设计）

> 对齐：`spec_kit/Personalized Reco/modules/db-design/plan.md`

- `contents`：提供 `content_id`、`text`、（可选）`author_id`、`template_id`
- `content_profiles`：提供 `stage`、`emotion_score`、`context_suitability_json`、`need_suitability_json`、`personalization_power`（推荐存 `0/5/10`）、（可选）`review_confidence`
- `content_risk_flags`：通过关联表提供风险标记集合（同一 content 下按 `uniq_content_flag(content_id, flag)` 去重）

---

## 3. 接口（建议）

推荐模块对该子模块只依赖抽象接口（Python Protocol/ABC 均可）：

- `fetch_candidates(scene, user_profile, fallback_level, limit, exclude_content_ids=None) -> List[ContentProfile]`
- `fetch_contents_by_ids(content_ids: List[int]) -> List[ContentProfile]`

---

## 4. 关键规则与实现约束

### 4.1 字段缺失与默认值

- 若 `review_confidence` 缺失：输出时默认按 `0.7`（对齐 `句子文案打分规则` V1.2 约定）。
- `context_suitability/need_suitability` 若缺失：**读取层必须补齐为“全 0.5 的通用可推”结构**（稳定输出，避免上层分支判断）。
  - `context_suitability` 必须包含 5 个 key：`family/work/relationship/friends/health`
  - `need_suitability` 必须包含 5 个 key：`emotional_support/parenting_pressure/self_worth/anxiety_relief/rest_balance`
  - 补齐时上述 key 的默认值均为 `0.5`
- `personalization_power`：若 DB 采用 `0/5/10` 存储，读取层必须映射为 `0.0/0.5/1.0` 对上输出。

### 4.2 risk_flags 兼容映射（若存在历史旧数据）

对齐 `句子文案打分规则` 的旧→新映射：

- `block_stage_unknown` → `unsafe_for_stage_unknown`
- `block_stage_parenting` → `unsafe_for_stage_parenting`
- `block_emotion_low` → `unsafe_for_emotion_low`
- `block_health_sensitive` → `block_health_medical`（读取层默认采用更保守的硬拦截映射；除非未来引入可判定的细分字段再放宽为 `soft_health_sensitive`）

输出约束：

- 输出的 flags 必须已去重，且不得包含任何旧命名。

### 4.3 性能约束

- 不得产生 N+1 查询：候选与字段必须一次或少量批量查询获取。
- `fetch_candidates` 必须支持 limit，并在 DB 层尽量过滤（减少应用层扫描）。
- `fetch_contents_by_ids` / `fetch_candidates` 推荐查询形态：
  - `contents` JOIN `content_profiles`，再 LEFT JOIN `content_risk_flags`（或先批量取 profiles，再批量取 flags 并在应用层聚合），避免按 `content_id` 循环查 flags。

---

## 5. 验收标准（可验证）

- `fetch_contents_by_ids`：
  - 输入任意 `content_id` 列表，返回包含完整字段的 `ContentProfile` 列表（无重复、可缺省字段按约定兜底）。
- `fetch_candidates`：
  - 在不同 `scene` 与 `fallback_level` 下能返回候选（即便画像缺失也不报错）。
  - risk_flags 映射正确：输出的 flag 名称集合只包含新命名（`unsafe_for_* / block_* / soft_*`）。
- 性能：
  - 单次调用不出现按 content_id 循环查库的行为（可通过日志/测试断言查询次数）。

