diff --git a/docs/好感度系统-开发任务清单.md b/docs/好感度系统-开发任务清单.md index 0f17945..29b07ee 100644 --- a/docs/好感度系统-开发任务清单.md +++ b/docs/好感度系统-开发任务清单.md @@ -50,18 +50,18 @@ | # | 子任务 | 产出物 | 验收标准 | 状态 | |---|---|---|---|---| -| 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 | ⬜ | +| P2-01 | Service 层骨架 | `userapp/affinity/services.py` — `AffinityService.apply()` + `admin_adjust()` 单一入口;10 步流水线含 event_id 去重 → 取规则 → 冷却 → 取设备 → 计算 + 钳位 → 规则日上限 → 全局日上限 → 原子写库 → Redis 累加 → 奖励 → WS 推送 | smoke test 6 项全 PASS(chat/dup/cooldown/admin_adjust/clamp/no_rule) | ✅ | +| P2-02 | Redis 计数器工具 | `userapp/affinity/counters.py` — 三类 key (cd / daily rule / daily global) + event_id 去重;Asia/Shanghai 自然日通过 `zoneinfo.ZoneInfo` 计算;cache.add+incr 原子语义;TTL 48h | smoke test 验证冷却 + event_id 去重命中 | ✅ | +| P2-03 | 等级映射 + 缓存更新 | `userapp/affinity/levels.py` — `map_value_to_level` / `update_device_level` / `progress_to_next_level`;仅 level 变化时 save(update_fields=) | smoke test 验证跨级更新 affinity_level 缓存 | ✅ | +| P2-04 | 跨级奖励发放(A3:方案 B) | `userapp/affinity/rewards.py` — `grant_levels(user_device, from, to)` 逐级独立事务;UserLevelRewardGrant 唯一约束防重;外部派发 hook STUB(P3/P4 接外部) | smoke test 验证 +80 跨 4 级写入 4 条 UserLevelRewardGrant | ✅ | +| P2-05 | AffinityLog 写入 + WS 推送钩子 | `userapp/affinity/ws.py` — 3 类事件 (affinity_update / level_up / level_down);asgiref async_to_sync 包装 channel_layer.group_send;fire-and-forget 故障不阻塞 | smoke test 后 AffinityLog 写入 3 行 | ✅ | +| P2-06 | `AffinityRule` ViewSet 重写 | `userapp/affinity/views.py` AffinityRuleAdminViewSet — ModelViewSet + 软删 perform_destroy (is_deleted+is_enabled=False) + `restore` action;?include_deleted=true 显示全集 | URL reverse + permission IsAdminUserStaff 验证 | ✅ | +| P2-07 | `AffinityLevel` ViewSet | AffinityLevelAdminViewSet — 同上软删;serializer 跨字段校验区间重叠 | URL reverse | ✅ | +| P2-08 | `AffinitySetting` GET/PUT | AffinitySettingView APIView — GET/PUT/PATCH 单例;pk=1 硬约束;衰减区间跨字段校验 | URL `/api/v1/admin/affinity/settings/` reverse | ✅ | +| P2-09 | `AffinityLog` 查询接口 | AffinityLogListView — 过滤 user/device/rule/source/date_range;自实现分页 page_size 上限 200;select_related 防 N+1 | URL `/api/v1/admin/affinity/logs/` reverse | ✅ | +| P2-10 | 数据统计接口 | AffinityStatsView — avg/max/top_count/active_7d/total_devices/today_interactions/today_change_sum/rule_freq_top/level_distribution;全部基于 UserDevice.active | URL reverse;返回结构与 §7.1 对齐 | ✅ | +| P2-11 | 用户设备好感度查询(admin) | UserAffinityDevicesView — `?user_id=` 必传 + 404 校验;?include_unbound=true 含历史;CR-001 默认仅 is_bound=True | URL reverse | ✅ | +| P2-12 | 管理员手动调整接口 | AffinityAdjustView / AffinityAdjustBatchView — 委托 AffinityService.admin_adjust;批量遍历 UserDevice.active 逐台调用;返回 per-device 结果数组 | smoke test 验证 +5 + 钳位到 max_affinity | ✅ | **完工里程碑**:所有 admin 接口可在 Postman 调通;service.apply 写入 log + 推 WS + 算等级。 @@ -143,3 +143,6 @@ |---|---|---| | 2026-04-24 | 初版创建 | — | | 2026-04-24 | P1-01 ~ P1-10 全部完成;models/migrations/seed 命令落地,等待 `migrate` 应用到数据库 | Claude | +| 2026-05-13 | P1 代码审查 (REVIEW-affinity-P1.md):3 Critical + 9 Warning + 6 Info = 18 项 finding | Claude (gsd-code-reviewer) | +| 2026-05-13 | P1 审查修复 (REVIEW-affinity-P1-FIX-REPORT.md):17 FIXED + 1 PARTIAL (WR-008) + 0 SKIPPED;4 commits A/B/C/D + 5 新迁移 + 1 重写;migrate / seed 验证通过;CHECK 约束 DB 层 smoke test PASS。P1 可以收尾,进入 P2。 | Claude (gsd-code-fixer) | +| 2026-05-13 | P2-01 ~ P2-12 全部完成:service 层(counters/levels/ws/rewards/services)+ admin API(rules/levels/settings/logs/stats/devices/adjust×2)落地;userapp/admin_urls.py 挂载 `/api/v1/admin/affinity/...`;Django check 通过;6 URL reverse 解析正确;service 6 项 smoke test 全 PASS(applied/no_rule/cooldown/event_dup/admin_adjust/clamp);存量数据未污染(测试设备已重置)。可进入 P3 管理端前端接通。 | Claude | diff --git a/qy_lty/docs/修改记录.md b/qy_lty/docs/修改记录.md index d7b0a67..031614f 100644 --- a/qy_lty/docs/修改记录.md +++ b/qy_lty/docs/修改记录.md @@ -23,6 +23,47 @@ +### [2026-05-13] 好感度系统 P2 阶段 — Service 层 + 管理端 API 落地 + +配套设计文档:[../../docs/好感度系统功能与规则设计.md](../../docs/好感度系统功能与规则设计.md) +配套任务清单:[../../docs/好感度系统-开发任务清单.md](../../docs/好感度系统-开发任务清单.md)(P2-01 ~ P2-12 全部完成) + +本次完成「好感度系统服务层 + 管理端 API」整体落地,所有好感度变化收敛到 `AffinityService.apply()` 单一入口,管理端 admin 后台具备完整 CRUD + 数据统计 + 设备查询 + 手动调整能力。Service 层 6 项 smoke test 全 PASS(含正常 apply / event_id 去重 / 冷却拦截 / admin_adjust / 钳位)。 + +- **文件路径**: + - `userapp/affinity/counters.py`(**新建** — P2-02 Redis 计数器:冷却 / 单规则日上限 / 全局日上限 / event_id 去重;Asia/Shanghai 自然日基准;TTL 48h;用 django-redis cache.add+incr 原子语义) + - `userapp/affinity/levels.py`(**新建** — P2-03 等级映射:map_value_to_level / progress_to_next_level / update_device_level;区间匹配按 -level desc 优先,重叠场景配合 P1 clean() 校验拦截) + - `userapp/affinity/ws.py`(**新建** — P2-05 WS 推送:push_affinity_update / push_level_up / push_level_down;asgiref async_to_sync 包装 channel_layer.group_send,向 device_{user_id} 分组广播;故障 fire-and-forget 不阻塞主流程) + - `userapp/affinity/rewards.py`(**新建** — P2-04 跨级奖励发放,A3 方案 B:每级独立事务,UserLevelRewardGrant 唯一约束保证幂等;外部派发 hook _dispatch_reward_to_external_systems 暂为 STUB,P3/P4 接虚拟货币/道具 app 时实现) + - `userapp/affinity/services.py`(**新建** — P2-01 AffinityService.apply() 主入口:10 步流水线 [event_id 去重 → 取规则 → 冷却 → 取 UserDevice → 计算变化 + single_cap 钳位 → 规则日上限 → 全局日上限 → 原子写 favorability + log + counter + 等级缓存 → Redis 计数器累加 → 奖励发放 → WS 推送];admin_adjust 专用入口绕过 rule 但仍走钳位 + 日志 + 等级 + 奖励 + WS) + - `userapp/affinity/serializers.py`(**新建** — 9 个序列化器:Rule/Level/Setting 三个 ModelSerializer 含跨字段校验,AffinityLogSerializer 只读 + 关联字段展开,UserDeviceAffinitySerializer 含 device_code/mac/level_name,AffinityAdjust 与 AffinityAdjustBatch 用 Serializer 而非 ModelSerializer) + - `userapp/affinity/permissions.py`(**新建** — IsAdminUserStaff 复用 IsAuthenticated 并加 is_staff 检查) + - `userapp/affinity/views.py`(**新建** — 7 个视图:AffinityRuleAdminViewSet / AffinityLevelAdminViewSet ModelViewSet + 软删 perform_destroy + restore action;AffinitySettingView APIView 单例 GET/PUT/PATCH;AffinityLogListView 含 user/device/rule/source/date_range/分页过滤;AffinityStatsView 聚合 avg/max/top_count/active_7d/today_interactions/rule_freq_top/level_distribution;UserAffinityDevicesView 按 user_id 展开设备列表,CR-001 默认仅返回 is_bound=True;AffinityAdjustView + AffinityAdjustBatchView 委托 AffinityService.admin_adjust) + - `userapp/affinity/urls.py`(**新建** — DRF DefaultRouter 注册 rules/levels CRUD + 5 个独立 path 挂 settings/logs/stats/devices/adjust*) + - `userapp/admin_urls.py`(修改 — 引入 include 并新增 `path('affinity/', include('userapp.affinity.urls'))`) +- **修改类型**: 新增 + 重构 +- **修改内容**: + - **P2-01 Service 层骨架**:唯一写入入口 `AffinityService.apply(user_id, device_id, rule_key, source, event_id, metadata, operator_admin_id, reason)` + `admin_adjust(user_id, device_id, delta, operator_admin_id, reason, batch)`;返回 `ApplyResult` dataclass 含 outcome 枚举 + change/before/after/level 信息 + - **P2-02 Redis 计数器**:6 类操作(is_in_cooldown / set_cooldown / get/incr_rule_daily / get/incr_global_daily / event_already_processed / mark_event_processed),用 `cache.add+incr` 实现 set-if-not-exists+atomic-increment 语义;Asia/Shanghai 时区自然日字符串通过 `zoneinfo.ZoneInfo` 计算 + - **P2-03 等级映射**:`map_value_to_level(value)` 按 (min, max) 区间匹配;`update_device_level(user_device)` 仅在 level 变化时调 save(update_fields=['affinity_level']) + - **P2-04 跨级奖励发放**:`grant_levels(user_device, from_level, to_level)` 逐级独立事务 + UniqueConstraint 防重;返回 RewardGrantResult(granted, skipped_duplicate, failed);失败的级别不影响其他级别(A3 方案 B 核心特性) + - **P2-05 WS 推送**:3 类事件(affinity_update / level_up / level_down);channel_layer 故障静默吞掉但日志记录 + - **P2-06 AffinityRule admin CRUD**:默认列表过滤 `is_deleted=False`,?include_deleted=true 显示全集;DELETE 走软删 `is_deleted=True+is_enabled=False`;POST/restore 自定义 action 恢复软删 + - **P2-07 AffinityLevel admin CRUD**:同上软删;serializer 跨字段校验区间重叠(与启用中其他等级不冲突) + - **P2-08 AffinitySetting GET/PUT/PATCH**:单例,pk=1 硬约束;跨字段校验衰减区间 + 初始/上限关系 + - **P2-09 AffinityLog 查询**:select_related user/rule/device.device 避免 N+1;过滤 user_id/device_id/rule_key/source/date_from/date_to;自实现分页(page/page_size,page_size 上限 200) + - **P2-10 stats**:所有指标基于 `UserDevice.active`(is_bound=True)聚合;今日数据按 AffinitySetting.timezone 取 local date;rule_freq_top 取近 7 日 Top 10 + - **P2-11 devices**:?user_id= 必传 + 404 校验;?include_unbound=true 才返回历史;默认按 is_primary desc, bound_at desc 排序 + - **P2-12 adjust / adjust-batch**:单台调整必传 user_id+device_id+delta+reason;批量调整对 user 名下所有 active 设备各调一次,逐台独立调用 service,返回 per-device 结果数组 + - **挂载位置**:`/api/v1/admin/affinity/{rules,levels,settings,logs,stats,devices,adjust,adjust-batch}/`;旧的 `/api/user/affinity-rules/` 与 `/affinity-levels/` 暂保留兼容(前端切到 admin 后即可清理) +- **修改原因**: P1 数据层就绪后必须落地服务层 + admin API,否则数据模型只是空壳;管理后台前端(P3)需要这套 admin API 才能拆 mock 接通;触发点埋点(P4)和客户端 API(P5)都依赖 service 层的 apply() 入口 +- **跨项目联动**: + - 管理后台前端 P3 阶段接入:`lib/api/affinity.ts` 需要切到 `/api/v1/admin/affinity/...` 新路径并对齐新字段集(cooldown_seconds, min_continuous_minutes 等) + - 设备/手机端 P4 阶段在 `device_interaction/consumers.py` 的 chat_message / sing / dance / touch / conversation_status 处调 `AffinityService.apply(rule_key=...)`;事件需带 `event_id`(UUID) + - 客户端 P5 阶段查询 `/api/user/me/affinity/` 暂未实现(P5 任务),P2 仅落地 admin 端 + +--- + ### [2026-05-13] 好感度系统 P1 审查修复 D — WR-002~WR-009 + IN-001~IN-006 综合改进 配套审查报告:[docs/REVIEW-affinity-P1.md](REVIEW-affinity-P1.md)(WR-002 ~ WR-009 + IN-001 ~ IN-006)