|
|
7c79b72544
|
feat(affinity-P2): admin API — Rule/Level CRUD + Setting + Logs + Stats + Devices + Adjust (P2-06~P2-12)
新增 admin 管理端完整 API,挂载在 /api/v1/admin/affinity/ 路径下:
- serializers.py:9 个序列化器
- AffinityRuleSerializer / AffinityLevelSerializer / AffinitySettingSerializer
含跨字段 validate(min/max 关系、区间重叠、衰减区间、companion_time 字段必填等)
- AffinityLogSerializer 只读 + 关联字段展开(user_username/device_code/rule_name)
- UserDeviceAffinitySerializer 含 device_code/mac/status/level_name
- AffinityAdjust + AffinityAdjustBatch 用 Serializer 而非 ModelSerializer
- permissions.py 中 IsAdminUserStaff 复用,所有 view 默认 RedisTokenAuthentication + IsAdminUserStaff
- views.py:7 个视图
- AffinityRuleAdminViewSet (P2-06):ModelViewSet + 软删 (is_deleted+is_enabled=False) + restore action;?include_deleted=true 返回全集
- AffinityLevelAdminViewSet (P2-07):同上软删;serializer 跨字段校验区间重叠
- AffinitySettingView (P2-08):APIView 单例 GET/PUT/PATCH;pk=1 硬约束
- AffinityLogListView (P2-09):过滤 user_id/device_id/rule_key/source/date_from/date_to;分页 page_size 上限 200;select_related 防 N+1
- AffinityStatsView (P2-10):avg/max/top_count/active_7d/total_devices/today_interactions/today_change_sum/rule_freq_top/level_distribution;全部基于 UserDevice.active 聚合;今日按 AffinitySetting.timezone 取 local date
- UserAffinityDevicesView (P2-11):?user_id= 必传 + 404 校验;?include_unbound=true 含历史;默认仅 is_bound=True
- AffinityAdjustView + AffinityAdjustBatchView (P2-12):委托 AffinityService.admin_adjust;批量遍历 UserDevice.active 逐台调用,返回 per-device 结果数组
- urls.py:DRF DefaultRouter 注册 rules/levels CRUD + 5 个独立 path 挂 settings/logs/stats/devices/adjust*
- admin_urls.py:引入 include 并新增 path('affinity/', include('userapp.affinity.urls'))
Django check 通过,6 URL reverse 全部解析正确:
/api/v1/admin/affinity/settings/
/api/v1/admin/affinity/logs/
/api/v1/admin/affinity/stats/
/api/v1/admin/affinity/devices/
/api/v1/admin/affinity/adjust/
/api/v1/admin/affinity/adjust-batch/
旧的 /api/user/affinity-rules/ 与 /affinity-levels/ 暂保留兼容,前端切到 admin 后即可清理。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-05-14 09:36:11 +08:00 |
|
|
|
f26e78c545
|
feat(affinity-P2): service 层落地 — 唯一写入入口 + Redis 计数器 + 等级映射 + 跨级奖励 + WS 推送 (P2-01~P2-05)
新增 6 个模块,把好感度变化的全部副作用收敛到一个调用入口:
- counters.py (P2-02):Redis 三类计数器
- affinity💿{device_id}:{rule_key} 冷却
- affinity:daily:{device_id}:{rule_key}:{YYYYMMDD} 单规则日上限
- affinity:daily:{device_id}:_global:{YYYYMMDD} 全局正向日上限
- 自然日按 AffinitySetting.timezone (Asia/Shanghai 默认) 通过 zoneinfo 计算
- cache.add + cache.incr 实现 set-if-not-exists + atomic-incr 语义,TTL 48h
- event_id 60s 去重防客户端重复上报
- levels.py (P2-03):等级映射
- map_value_to_level / update_device_level / progress_to_next_level
- update_device_level 仅 level 变化时 save(update_fields=['affinity_level'])
- ws.py (P2-05):WebSocket 推送 helper
- 3 类事件 affinity_update / level_up / level_down
- asgiref.async_to_sync 包装 channel_layer.group_send
- 推送故障 fire-and-forget 仅日志记录,不阻塞主流程
- rewards.py (P2-04):跨级奖励发放(A3 方案 B)
- grant_levels(user_device, from_level, to_level) 逐级独立事务
- UserLevelRewardGrant 唯一约束保证幂等(决策 11:衰减回升不补发)
- _dispatch_reward_to_external_systems 是 STUB,P3/P4 接虚拟货币/道具 app 时实现
- services.py (P2-01):AffinityService 主入口
- apply(user_id, device_id, rule_key, source, event_id, metadata, operator_admin_id, reason)
- 10 步流水线 [event_id 去重 → 取规则 → 冷却 → 取 UserDevice.active → 计算 + single_cap 钳位 → 规则日上限 → 全局日上限 → 原子写库 → Redis 累加 → 奖励 → WS 推送]
- admin_adjust 绕过 rule 与冷却,但走 [0, max_affinity] 钳位 + log + 等级缓存 + 奖励 + WS
- 返回 ApplyResult dataclass 含 ApplyOutcome 枚举(applied / noop_no_rule / noop_cooldown / noop_*_daily_cap / noop_event_duplicate / noop_value_boundary / error)
- permissions.py:IsAdminUserStaff 复用 IsAuthenticated + is_staff 检查
Smoke test 6 项全 PASS:no_rule / chat applied / event_id 去重 / 冷却拦截 / admin_adjust / max_affinity 钳位。
AffinityLog 写库 / UserLevelRewardGrant 幂等 / level 缓存更新 均经事务原子保证。
设计依据:docs/好感度系统功能与规则设计.md §4.3 触发流程 + §6 等级规则 + §9 数据契约。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-05-14 09:35:53 +08:00 |
|