# Integration（FastAPI API + Celery Worker）｜Plan

> 对应规范：`spec_kit/Personalized Reco/modules/integration-api-worker/spec.md`
>
> 依赖（已实现）：
>
> - `server/app/features/personalized_reco/reco_engine/`：统一引擎入口 `recommend(...)`
> - `server/app/features/personalized_reco/content_repository/`：`SqlAlchemyContentRepository`
> - `server/app/db/session.py`：`get_db` / `AsyncSessionLocal`
> - `server/app/worker.py`：`celery_app`
>
> 本计划已按确认项固化：
>
> - API 路由：按场景拆分（`/v1/reco/feed`、`/v1/reco/push`、`/v1/reco/widget`）
> - locale：从 `Accept-Language` 解析并映射到 `en/tc`，缺省 `en`
> - 限流：**按客户端 IP**，**1 分钟 10 次**
> - Celery：实现 `tasks.reco.generate` + `tasks.reco.push_once`
> - Celery 内调用 async：使用 `asyncio.run(...)`
> - now 注入：支持 Header `X-Now`（方案 B），并保留请求体 `now`（spec 已定义）

---

## 1. 目标与交付物

### 1.1 目标

- 对外提供推荐能力：
  - **FastAPI**：客户端同步获取推荐结果。
  - **Celery**：后台任务式生成推荐（Push/Widget 的定时/批处理）。
- **不复制推荐逻辑**：API 与任务均只调用同一 `Reco Engine`。
- 提供基础可用的 **IP 限流**（1 分钟 10 次）。
- 支持 **now 注入** 以实现确定性回归测试。
- 多语言仅支持 **EN/TC**，不允许语言回退。

### 1.2 交付物（tasks 阶段落地）

- 新增 API 路由文件（建议）：
  - `server/app/api/v1/reco.py`
- `main.py` 注册路由：
  - `app.include_router(reco_router)`
- 新增 Celery 任务：
  - `server/app/tasks/reco.py`（包含 `tasks.reco.generate`、`tasks.reco.push_once`）
- 新增限流中间件/依赖：
  - `server/app/api/limits.py`（或 `server/app/core/ratelimit.py`）
- 单元/集成测试（至少）：
  - API schema 校验（请求/响应模型）
  - 限流行为（同 IP 超过阈值返回 429）
  - Celery 任务能跑通 `ping -> reco.generate`

---

## 2. FastAPI 设计

### 2.1 路由与接口

新增推荐路由：

- `POST /v1/reco/feed`
- `POST /v1/reco/push`
- `POST /v1/reco/widget`

说明：

- 每个路由内部将 `scene` 固定为对应场景，避免客户端传错。
- `k` 若未传：按引擎默认（建议：feed=30，push/widget=1；此默认可在 API 层写死或由调用方显式传入）。

### 2.2 请求/响应模型（建议）

请求体 `RecoRequest`（pydantic）：

- `k: Optional[int]`
- `user_profile: UserProfileV1_2`
- `already_recommended_ids: list[str|int] = []`
- `touched_or_viewed_ids: list[str|int] = []`
- `now: Optional[datetime] = None`（用于测试；生产通常不传）

响应体 `RecoResponse`（pydantic）：

- `items: list[RecommendedItem]`
- `meta: RecoMeta`

> 说明：可以直接复用引擎的 `RecoEngineResult`/`RecommendedItem`/`RecoMeta` 作为 response_model，减少重复。

### 2.3 now 注入优先级（确定性）

支持两种注入：

- Header：`X-Now`（ISO8601 字符串，如 `2026-02-02T12:00:00Z`）
- Body：`now`

建议优先级：

1. 若 `X-Now` 存在且可解析 → 使用 header 的时间
2. 否则若 body.now 存在 → 使用 body.now
3. 否则 → 使用服务端 `datetime.now(timezone.utc)`

### 2.4 locale 获取与映射（EN/TC）

来源：HTTP Header `Accept-Language`

建议解析规则（无需额外依赖）：

- 若 header 缺失/空 → `"en"`
- 若包含 `zh-TW`/`zh-HK`/`tc` → `"tc"`
- 否则默认 `"en"`

随后调用 `content_repository.types.normalize_locale(locale)` 做严格校验（保证只出 `en/tc`）。

### 2.5 依赖注入与数据库会话

API 层使用 `Depends(get_db)` 注入 `AsyncSession`：

- 在 handler 内创建 `SqlAlchemyContentRepository(session)`
- 调用 `reco_engine.recommend(repo=..., scene=..., ...)`

### 2.6 限流（按 IP：1 分钟 10 次）

实现方式（V1 推荐：无外部依赖、内存版）：

- 在 FastAPI 层添加一个依赖或中间件：
  - 从 `Request.client.host` 取 IP（若有反代需后续支持 `X-Forwarded-For`，V1 先不做）
  - 使用滑动窗口或固定窗口计数（推荐固定窗口：按分钟 bucket）
  - 超过阈值：返回 `HTTP 429`，响应体包含 `detail="rate_limited"`

注意与取舍：

- 内存限流在多进程/多实例下不共享（V1 可接受）；后续可升级为 Redis 限流。

---

## 3. Celery Worker 设计

### 3.1 任务列表

- `tasks.reco.generate`
  - 输入：与 API 等价，但建议 payload 小（user_profile + ids + scene + 可选 now/locale）
  - 输出：默认忽略结果（worker 已配置 `task_ignore_result`），但函数可返回 `items/meta` 用于调试
- `tasks.reco.push_once`
  - 输入：尽量只包含 push 需要字段（user_profile + ids + 可选 now/locale）
  - 行为：内部调用 `tasks.reco.generate(scene="push")`，并预留“写入下游”的占位函数（V1 不接真实推送系统）

### 3.2 任务内调用推荐引擎（async → sync）

由于 `Reco Engine` 为 async，Celery task 为 sync，采用：

- `asyncio.run(_run_reco_async(...))`

其中 `_run_reco_async` 负责：

- `async with AsyncSessionLocal() as session:`
- `repo = SqlAlchemyContentRepository(session)`
- `await recommend(repo=repo, ...)`

说明：

- Celery 环境通常没有运行中的事件循环，`asyncio.run` 可用。
- 若未来引入 async worker/或在已有 loop 环境中调用，再考虑改为“可复用事件循环工具”。

### 3.3 locale 与 now

- locale：
  - Celery 输入可直接传 `"en"/"tc"`，缺省 `"en"`
  - 仍通过 `normalize_locale` 严格校验
- now：
  - 任务输入支持传入 `now`（用于回归测试/离线批处理），否则用服务端当前时间

---

## 4. 一致性策略（API vs Celery）

必须保证：

- API 与任务都只调用 `reco_engine.recommend`
- 对同一份输入（含固定 now/locale），输出 items 结果一致（允许浮点微差）

建议做一个对比测试：

- 在测试中构造固定 `now`，用同样的 repo/同样的输入分别走 API handler 与 Celery 的 `_run_reco_async`，断言 `content_id` 列表一致。

---

## 5. 错误处理与返回规范

### 5.1 API 错误处理

- 请求体校验失败：FastAPI 422
- locale 不支持：返回 200 但 items 为空（由引擎兜底）或 400（可选）
  - V1 建议：保持与引擎一致，返回空 items + meta，并在 meta.config_snapshot 记录错误 stage
- 限流触发：429

### 5.2 Celery 错误处理

- 任务内部捕获异常并记录日志
- 默认不回写结果，避免 Redis 占用
- 必要时将错误信息写入任务日志或后续的可观测系统（V2）

---

## 6. 安全与性能（V1）

- 限流：按 IP 10/min，保护服务与数据库
- Payload 控制：
  - Celery 输入避免传大数组；历史集合若过大，后续演进为“传引用 ID”
- 多语言：只支持 EN/TC，不做语言回退（与 repository 口径一致）

