# 好感度系统 — 开发任务清单

> 配套文档：[好感度系统功能与规则设计.md](./好感度系统功能与规则设计.md)
>
> 维护说明：每完成一个子任务，把状态标记为 ✅ 并附上 commit hash。需求变更时直接修订本文，决策点的最新结论以《设计》文档「决策记录」为准。

---

## 总览

| 阶段 | 目标 | 子任务数 | 依赖 | 关联待敲定项 |
|---|---|---|---|---|
| **P1** | 数据模型扩展 + 迁移 | 10 | — | — |
| **P2** | Service 层 + 管理端 API | 12 | P1 | A3（跨级事务，建模时用默认） |
| **P3** | 管理端前端接通（拆 mock） | 8 | P2 | — |
| **P4** | 触发点埋点（设备/手机事件接入 service） | 7 | P2 | — |
| **P5** | 客户端 API + 衰减定时任务 | 6 | P2、P4 | A1（解绑重绑）、A4（通知通道） |
| **P6** | 观测、性能、并发加固 | 5 | P4 | — |

**子任务合计：48 项**

每个子任务独立可提交、可回滚（atomic commit 粒度）。

---

## P1 数据模型扩展 + 迁移

**目标**：所有 schema 变更落地，迁移脚本能跑通存量数据，**不写业务逻辑**。

| # | 子任务 | 产出物 | 验收标准 | 状态 |
|---|---|---|---|---|
| P1-01 | `AffinityRule` 字段扩展 | 新增字段：`rule_key`、`trigger_type`、`min_change`、`max_change`、`single_cap`、`daily_cap`、`cooldown_seconds`、`is_negative`、`is_enabled`、`is_deleted` | migration 通过；`rule_key` 唯一索引；旧字段 `points`/`daily_limit` 标记 deprecated 并保留一个版本 | ✅ |
| P1-02 | `AffinityRule` 陪伴时长字段 | 同表加字段 `min_continuous_minutes`、`max_count_per_day`（仅 `trigger_type=companion_time` 时使用） | migration 通过；nullable | ✅ |
| P1-03 | `AffinityLevel` 字段扩展 | 新增 `min_affinity`、`max_affinity`、`unlock_content`、`reward_type`、`reward_currency`、`reward_items`(JSON)、`is_enabled`、`is_deleted` | migration 通过；旧字段 `required_points` 保留 | ✅ |
| P1-04 | 新建 `AffinitySetting`（单例表） | 字段：`initial_affinity`、`max_affinity`、`daily_cap`、`decay_rate`、`decay_threshold`、`enable_notify`、`enable_rewards`、`timezone`、衰减相关字段 | migration 通过；初始化一行默认数据 | ✅ |
| P1-05 | 新建 `AffinityLog` | 字段：`user`、`device`、`rule`(SET NULL)、`rule_key`(冗余文本)、`change_value`、`before_value`、`after_value`、`source`、`event_id`、`operator_admin_id`、`reason`、`metadata`(JSON)、`created_at` | migration 通过；`(device, created_at)` 索引；`event_id` 唯一去重 | ✅ |
| P1-06 | 新建 `UserAffinityDailyCounter`（DB 兜底） | 字段：`device`、`rule`、`date`、`accumulated_change`、`trigger_count`，唯一 `(device, rule, date)` | migration 通过；热路径仍走 Redis，每晚凌晨从 Redis 落库 | ✅ |
| P1-07 | 新建 `UserLevelRewardGrant` | 字段：`device`、`level`、`granted_at`、`reward_snapshot`(JSON)，唯一 `(device, level)` | migration 通过 | ✅ |
| P1-08 | `UserDevice` 加字段 | 新增 `favorability`(int, default=10)、`affinity_level`(int, default=1)、`last_active_at`(datetime)、`is_active`(bool, default=true) | migration 通过；保留软删除选项 | ✅ |
| P1-09 | 数据迁移脚本 | management command：把 `ParadiseUser.favorability` 写到该用户主设备 `UserDevice.favorability`；无主设备时写到第一台 | 脚本幂等；存量用户 0 丢失；产出迁移报告 | ✅ |
| P1-10 | 默认数据 seed | management command：初始化 8 条默认规则、5 个等级、`AffinitySetting` 默认值 | 命令重复执行不重复插入；与设计文档默认值一致 | ✅ |

**完工里程碑**：`python manage.py migrate` 通过，`python manage.py seed_affinity` 写入默认数据，admin 后台 ORM 层能看到所有表。

---

## P2 Service 层 + 管理端 API

**目标**：业务逻辑统一入口建好，管理端所有接口可用（前端先用 Postman 跑）。

| # | 子任务 | 产出物 | 验收标准 | 状态 |
|---|---|---|---|---|
| P2-01 | Service 层骨架 | 新建 `qy_lty/affinity/services.py`，定义 `AffinityService.apply(user_id, device_id, rule_key, source, event_id, metadata)` 单一入口 | 单元测试覆盖正常路径 | ⬜ |
| P2-02 | Redis 计数器工具 | 冷却 / 单规则日上限 / 全局日上限三类 key，含 Asia/Shanghai 自然日切换 | `cd:{device}:{rule}`、`daily:{device}:{rule}:{YYYYMMDD}`、`daily:{device}:_global:{YYYYMMDD}` 命名一致 | ⬜ |
| P2-03 | 等级映射 + 缓存更新 | 根据 `favorability` 计算 `affinity_level`，写回 `UserDevice` | 区间边界正确（含闭区间） | ⬜ |
| P2-04 | 跨级奖励发放（A3：方案 B） | 升级时逐级发放，每级一个独立事务，失败的入重试队列；写 `UserLevelRewardGrant` 防重 | 同设备同等级不重发；外部失败不影响等级提升 | ⬜ |
| P2-05 | AffinityLog 写入 + WS 推送钩子 | service 末尾发 channel layer 消息到 `device_{user_id}` group | 单设备变化推送到该用户所有在线端 | ⬜ |
| P2-06 | `AffinityRule` ViewSet 重写 | CRUD + `rule_key` 唯一校验 + 软删除 | admin 角色才可写；普通管理员 403 | ⬜ |
| P2-07 | `AffinityLevel` ViewSet | CRUD + 区间不重叠不空隙校验 | 校验失败返回 400 + 明确错误信息 | ⬜ |
| P2-08 | `AffinitySetting` GET/PUT | 单例接口 | PUT 后立即生效（清缓存） | ⬜ |
| P2-09 | `AffinityLog` 查询接口 | `/api/admin/affinity/logs/` 支持过滤 user/device/rule/时间 | 分页 + 排序 + 性能（10w 行 < 500ms） | ⬜ |
| P2-10 | 数据统计接口 | `/api/admin/affinity/stats/` 返回平均/最高/活跃/今日互动等指标 | 按设备聚合，与设计文档 §7.1 一致 | ⬜ |
| P2-11 | 用户设备好感度查询（admin） | `/api/admin/affinity/devices/?user_id=` 列出该用户所有设备及好感度/等级 | 含已解绑（is_active=false）的归档项标记 | ⬜ |
| P2-12 | 管理员手动调整接口 | `/api/admin/affinity/adjust/`（必传 device_id）+ `/api/admin/affinity/adjust-batch/`（用户名下所有设备各加 X） | 钳位 [0, max_affinity]；写 log + operator_admin_id + reason | ⬜ |

**完工里程碑**：所有 admin 接口可在 Postman 调通；service.apply 写入 log + 推 WS + 算等级。

---

## P3 管理端前端接通（拆 mock）

**目标**：前端 [page.tsx](../qy-lty-admin/app/affinity/page.tsx) 全面去 mock，所有读写走真实接口。

| # | 子任务 | 产出物 | 验收标准 | 状态 |
|---|---|---|---|---|
| P3-01 | `lib/api/affinity.ts` 字段对齐 | 与 P2 字段同步：增 cooldown_seconds、min/max_change 等 | TypeScript 类型与后端 serializer 一致 | ⬜ |
| P3-02 | 互动规则页拆 mock | `page.tsx` `initialRules` 删除，改 useEffect 拉 `getAffinityRules()` | 刷新页面看到的是数据库数据 | ⬜ |
| P3-03 | `AffinityRuleDialog` 接通真接口 | 替换 [affinity-rule-dialog.tsx:112](../qy-lty-admin/components/affinity/affinity-rule-dialog.tsx#L112) 的 setTimeout，调 create/update | 保存成功后表格自动刷新 | ⬜ |
| P3-04 | 等级页拆 mock + Dialog 接通 | 同上对 levels | 同上 | ⬜ |
| P3-05 | 系统设置面板接通 | `/api/admin/affinity/settings/` GET/PUT | 修改后刷新仍生效 | ⬜ |
| P3-06 | 衰减规则面板接通 | 同 settings 接口 | 同上 | ⬜ |
| P3-07 | 数据统计页接通 stats 接口 | 4 个指标卡片 + 分布图表换真数据 | 和 admin 后台 `/stats/` 返回一致 | ⬜ |
| P3-08 | 新建变化日志查询页 | 新页面 `/affinity/logs`，支持 user/device/规则/时间过滤 | 分页可用；单条点击展开 metadata | ⬜ |

**完工里程碑**：管理后台所有功能用真实数据，无任何 setTimeout / 本地 useState mock。

---

## P4 触发点埋点（设备/手机事件接入 service）

**目标**：用户在设备/App 上的真实操作触发好感度变化。

| # | 子任务 | 产出物 | 验收标准 | 状态 |
|---|---|---|---|---|
| P4-01 | `chat_message` 接入 service | [device_interaction/consumers.py](../qy_lty/device_interaction/consumers.py) `chat_message` 处理函数末尾调 `apply(rule_key='chat')` | 设备发对话消息后 favorability 增加 | ⬜ |
| P4-02 | `sing` / `dance` / `touch` 接入 | 同上对三个动作消息 | 三种动作各自独立 rule 触发 | ⬜ |
| P4-03 | 陪伴时长计时器 | `conversation_status` begin 启动 Redis timer，end 计算时长 → `floor(duration / min_continuous_minutes)` 次 `apply(rule_key='companion_time')` | 聊够 X 分钟正确加分；中途断线只算到中断时刻 | ⬜ |
| P4-04 | 喂食 / 送礼 / 换装 / 道具接入 | 对应 HTTP ViewSet 的 `perform_create` 调 service（必须带 device_id） | 手机端动作触发好感度 | ⬜ |
| P4-05 | WS 推送实现 | channel layer consumer 处理 `affinity_update`、`level_up`、`level_down` 三类事件 | 用户多端在线全部收到 | ⬜ |
| P4-06 | `event_id` 去重 | 客户端事件携带 UUID，服务端 60s Redis 缓存防重复 | 同一 event_id 重复发送只生效一次 | ⬜ |
| P4-07 | 串行锁 | `(device_id, rule_key)` Redis 锁，避免并发导致计数器/上限判断失误 | 100 并发触发同 rule，最终值与日志一致 | ⬜ |

**完工里程碑**：实际拿设备聊天 / 用 App 抚摸等，能在 admin 日志页看到记录。

---

## P5 客户端 API + 衰减定时任务

**目标**：手机端和设备端能查询、能领奖；不活跃设备每日自动衰减。

| # | 子任务 | 产出物 | 验收标准 | 状态 |
|---|---|---|---|---|
| P5-01 | `/api/user/me/affinity/`（多设备列表） | 默认返回当前用户所有 `is_active=true` 设备的好感度、等级、下一级进度 | 返回数据结构与 §9.2 一致 | ⬜ |
| P5-02 | `/api/user/me/affinity/?device_id=` | 单设备详情：当前值、等级、近期 N 条变化、已解锁内容 | 含进度百分比 | ⬜ |
| P5-03 | `/api/user/me/affinity/claim-reward/` | 领奖接口（必传 device_id + level） | 重复领取返回幂等成功；未达等级返回 403 | ⬜ |
| P5-04 | 衰减定时任务 | 新增 management command + celery/django-q beat，每天 00:30 跑 | 不活跃设备次日有衰减日志；活跃设备无衰减 | ⬜ |
| P5-05 | 设备解绑 / 重绑处理（A1 决策点） | 解绑置 `is_active=false`；重绑读默认方案 | 待 A1 拍板后确认是否恢复历史值 | ⬜ |
| P5-06 | 客户端首屏同步 | 文档约定：设备/手机登录后调一次 `/api/user/me/affinity/?device_id=` | 客户端 SDK 含此调用（前端实现侧） | ⬜ |

**完工里程碑**：用户在 App 看到所有设备好感度；离线一段时间登录后看到衰减后的值。

---

## P6 观测、性能、并发加固

**目标**：上线前的观察手段、压测、数据修复能力。

| # | 子任务 | 产出物 | 验收标准 | 状态 |
|---|---|---|---|---|
| P6-01 | 结构化日志 | structlog 在 service.apply 入口埋点（user/device/rule/source/result） | grep 一个 device_id 能拼出完整轨迹 | ⬜ |
| P6-02 | Prometheus 指标 | `apply_total{rule, source, result}`、`cooldown_hit`、`daily_cap_hit`、`decay_runs`、`reward_grant_failed` | Grafana 面板 4 张图 | ⬜ |
| P6-03 | 并发压测 | 单设备 100 并发触发同 rule 的脚本 | 最终 favorability、log 数、计数器三者一致 | ⬜ |
| P6-04 | Redis ↔ DB 一致性校验 | 离线脚本：对比 Redis 当日计数器与 DB `UserAffinityDailyCounter` | 偏差告警 | ⬜ |
| P6-05 | 数据修复工具 | management command：扫描 favorability 越界的设备并钳位修复 | 可 dry-run + 实际修复模式 | ⬜ |

**完工里程碑**：监控面板可观测；压测无数据竞争；越界数据可一键修复。

---

## 变更记录

| 日期 | 变更 | 操作人 |
|---|---|---|
| 2026-04-24 | 初版创建 | — |
| 2026-04-24 | P1-01 ~ P1-10 全部完成；models/migrations/seed 命令落地，等待 `migrate` 应用到数据库 | Claude |
