lty/docs/好感度系统功能与规则设计.md
pmc 3b7c5c85f5
All checks were successful
Build and Deploy LTY / build-and-deploy (push) Successful in 9m14s
feat: update device interaction views, docs, and CLAUDE.md
- Update device_interaction views
- Update admin README and CLAUDE.md
- Add affinity system design doc
- Add device chat record subtitle storage scheme doc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 17:06:21 +08:00

314 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 好感度系统功能与规则设计
> 文档范围:本文档描述**洛天依管理系统**中「好感度系统」的全部功能模块与规则设计,用于产品对齐、联调对接与后续服务端落地。
>
> 当前状态:管理后台 UI 已完成(见 [qy-lty-admin/app/affinity/page.tsx](../qy-lty-admin/app/affinity/page.tsx)),后端模型骨架已就绪(见 [qy_lty/userapp/models.py](../qy_lty/userapp/models.py) 第 79115 行),但前端尚未实际调用真实接口,设备端/手机端事件尚未接入好感度逻辑。
---
## 一、系统定位
好感度系统用于刻画**用户与洛天依**之间的亲密度关系。其核心价值:
- 让用户的互动行为产生**持续、可感知的数值反馈**
- 通过**等级 + 解锁内容**形成长期陪伴动机
- 对不活跃用户通过**衰减机制**保留流失预警与召回空间
好感度是一个 `[0, max_affinity]` 区间的整数(默认上限 100每个用户独立维护记录在 `ParadiseUser.favorability` 字段。
---
## 二、功能模块总览
管理后台分 4 个标签页,对应 5 块功能:
| 模块 | 页签 | 功能说明 |
|---|---|---|
| 系统概览 | 系统概览 | 关键指标卡片 + 全局基础参数设置 |
| 互动规则 | 互动规则 | 管理各类互动行为的好感度变化规则 |
| 衰减规则 | 互动规则页下半 | 配置不活跃用户的好感度衰减策略 |
| 等级奖励 | 等级奖励 | 管理好感度等级划分与奖励发放 |
| 数据统计 | 数据统计 | 好感度分布、互动分析、趋势监控 |
---
## 三、模块 1 — 系统概览与全局参数
### 3.1 关键指标卡片
| 指标 | 含义 | 数据源 |
|---|---|---|
| 平均好感度 | 所有用户 `favorability` 平均值 | 聚合 `ParadiseUser` |
| 最高好感度 | 系统中达到上限的用户数 | `favorability = max_affinity` 计数 |
| 互动次数/日 | 当天触发的所有规则次数总和 | 聚合 `AffinityLog`(待建) |
| 活跃用户比例 | 近 N 天有互动的用户占比 | 聚合用户活跃状态 |
### 3.2 全局参数AffinitySetting
| 字段 | 默认值 | 说明 |
|---|---|---|
| `initial_affinity` | 10 | 新用户创建时的初始好感度 |
| `max_affinity` | 100 | 好感度上限(所有规则累计不会超过此值) |
| `daily_cap` | 20 | 单用户每日好感度**净增长**上限(跨规则汇总) |
| `decay_rate` | 2 点/天 | 全局默认衰减速率(衰减规则可覆盖) |
| `decay_threshold` | 3 天 | 不互动多少天后开始衰减 |
| `enable_notify` | true | 好感度变化是否推送通知 |
| `enable_rewards` | true | 是否启用等级奖励发放 |
**规则**
- `initial_affinity` 只影响**新用户**,不回溯修改已有用户
- `max_affinity` 调低后,**已超过的用户保留原值**,但不再增加
- `daily_cap` 是**跨所有正向规则**的顶层限制,触达后当日所有增益无效(衰减不受此限)
---
## 四、模块 2 — 互动规则
### 4.1 规则字段定义
| 字段 | 类型 | 示例 | 说明 |
|---|---|---|---|
| `id` | string | `rule-2` | 规则唯一 ID |
| `name` | string | `对话` | 规则展示名 |
| `type` / `rule_key` | enum | `chat` | **代码标识**,服务端事件通过它匹配规则,不可重复 |
| `description` | string | `与洛天依进行对话` | 展示描述 |
| `minChange` | int | 1 | 单次好感度变化最小值 |
| `maxChange` | int | 5 | 单次好感度变化最大值(最终取 `[min, max]` 的随机整数) |
| `singleCap` | int | 5 | 单次变化绝对值上限(保护性钳位,防止数据异常) |
| `dailyCap` | int | 15 | **本规则**每日累计变化上限(绝对值) |
| `isNegative` | bool | false | 是否负向规则。正向用正数,负向用负数 |
| `isEnabled` | bool | true | 是否启用,禁用后事件不触发 |
### 4.2 默认规则集8 条)
| rule_key | 名称 | 范围 | 单次/日 | 正负 | 触发来源 |
|---|---|---|---|---|---|
| `card` | 使用卡片 | +1 ~ +3 | 3 / 10 | 正 | 手机端(使用卡片 API |
| `chat` | 对话 | +1 ~ +5 | 5 / 15 | 正 | 设备端 + 手机端(聊天消息) |
| `feed` | 喂食 | +2 ~ +8 | 8 / 16 | 正 | 手机端(喂食动作) |
| `touch` | 抚摸 | +1 ~ +3 | 3 / 9 | 正 | 手机端 / 设备端(抚摸信号) |
| `dress` | 换装 | +2 ~ +6 | 6 / 12 | 正 | 手机端(换装 API |
| `prop` | 使用道具 | +1 ~ +4 | 4 / 12 | 正 | 手机端(道具使用) |
| `gift` | 送礼物 | +5 ~ +15 | 15 / 20 | 正 | 手机端(赠礼 API |
| `decay` | 无互动衰减 | 1 ~ 3 | 3 / 5 | 负 | 定时任务 |
### 4.3 触发计算流程
```
收到事件 (user_id, rule_key, 来源上下文)
① 取规则 → 若 is_enabled=false丢弃
② 冷却检查Redisaffinity:cd:{user}:{rule_key}
│ 未到冷却 → 丢弃
③ 本规则日上限检查affinity:daily:{user}:{rule_key}:{date}
│ 已满 → 丢弃
④ 全局日上限检查(正向事件才检查)
│ 已满 → 丢弃
⑤ 计算变化值 = random(min, max)
↓ 按 single_cap 钳位
⑥ 原子更新 ParadiseUser.favorability
↓ 钳位到 [0, max_affinity]
⑦ 写 AffinityLog、更新计数器
⑧ 判断是否跨越等级边界
│ 是 → 触发等级变更事件(发奖励 + 推通知)
⑨ 通过 WebSocket 向用户的所有在线端推送 affinity_update
```
### 4.4 规则设计约定
- **单一写入入口**:所有好感度变化必须经由服务端统一入口,客户端不能直接增减。
- **rule_key 即契约**:客户端事件不携带分值,只报「我触发了 `gift` 规则」,具体加多少由服务端按规则算。规则可被管理员随时调整,客户端无需改动。
- **幂等防护**:同一 `rule_key` + 同一设备事件 ID 在冷却窗口内只生效一次,防抖防重复。
- **禁用规则的兜底**:管理员禁用某规则后,客户端若继续上报该事件,服务端静默丢弃(不报错、不扣冷却)。
---
## 五、模块 3 — 衰减规则
衰减是**本质为 `rule_key=decay` 的负向规则**,但由于业务语义特殊,在后台独立配置。
### 5.1 衰减字段
| 字段 | 默认值 | 说明 |
|---|---|---|
| `decay_start_days` | 3 | 不互动多少天后开始衰减 |
| `decay_rate_per_day` | 2 点/天 | 平均每日衰减点数 |
| `min_decay` | 1 | 单日衰减最小值 |
| `max_decay` | 3 | 单日衰减最大值 |
| `decay_cap` | 5 点/天 | 单日衰减上限(保护性) |
| `min_floor` | 0 | **衰减下限**,好感度不会低于此值 |
| `notify_decay` | true | 是否通知用户「好感度下降了」 |
### 5.2 衰减执行
- **频次**:每日 00:30 由定时任务统一跑一次
- **命中对象**`last_active_at < now - decay_start_days` 的用户
- **落库**:衰减也写 `AffinityLog``source='system_decay'`
- **下限保护**:若用户当前好感度 ≤ `min_floor` 则跳过
- **与互动的关系**:用户当天有互动即**重置不活跃计数**,次日不衰减
### 5.3 设计权衡
- 衰减**不占用** `daily_cap` 全局日上限(因为它是扣减,不是增益)
- 衰减日志会产生大量记录,可考虑按天合并写一条,减少 `AffinityLog` 膨胀
- 若「当日互动」和「当日衰减」同时命中,先执行衰减再执行互动(让用户感受到「我回来了 → 好感度止跌回升」)
---
## 六、模块 4 — 等级奖励
### 6.1 等级字段
| 字段 | 示例 | 说明 |
|---|---|---|
| `level` | 3 | 等级序号,唯一 |
| `name` | 熟悉 | 等级名 |
| `minAffinity` / `maxAffinity` | 41 / 60 | 等级好感度区间(闭区间) |
| `unlockContent` | `更多服装、特殊对话` | 文案描述,前端展示 |
| `rewardType` | `unlock` / `item` / `currency` / `mixed` | 奖励类型 |
| `rewardCurrency` | 100 | 虚拟货币数量rewardType 含 currency 时生效) |
| `rewardItems` | `[{item_id, qty}]` | 道具列表rewardType 含 item 时生效) |
| `isEnabled` | true | 是否启用该等级 |
### 6.2 默认等级5 档)
| 等级 | 名称 | 区间 | 解锁内容 |
|---|---|---|---|
| 1 | 初识 | 0 ~ 20 | 基础对话功能 |
| 2 | 相识 | 21 ~ 40 | 基础服装、道具使用 |
| 3 | 熟悉 | 41 ~ 60 | 更多服装、特殊对话 |
| 4 | 亲密 | 61 ~ 80 | 限定服装、特殊互动 |
| 5 | 挚友 | 81 ~ 100 | 专属内容、特殊剧情 |
### 6.3 等级变化规则
- 等级由好感度区间**自动映射**,不是独立字段
- **跨级判定**:每次好感度变动后,取当前值所属区间,与上一次等级比较
- 升级 → 发放目标等级的奖励(**只发最终落点等级**,跳级不补发中间等级)
- 降级(衰减导致) → 不追回奖励,但取消解锁内容访问权限
- **奖励幂等**:同一用户同一等级的「首次达到奖励」只发一次;降级后再升级不重复发放
### 6.4 区间约束
- 区间**不得重叠**,管理端保存时做校验
- 区间**不得有空隙**(如等级 2 的 max=40则等级 3 的 min 必须是 41
- 最低等级 `min=0`,最高等级 `max=max_affinity`
---
## 七、模块 5 — 数据统计
### 7.1 概览指标
- 平均好感度 / 中位数 / 最高好感度
- 活跃用户数(近 7 日有互动)
- 今日互动次数、今日新增好感度总量
### 7.2 分布分析
- 好感度区间分布0-20、21-40…
- 各等级用户数占比
- 各互动规则触发频次 Top N
### 7.3 趋势分析
- 日/周/月的平均好感度变化曲线
- 日互动量趋势
- 衰减命中用户数趋势
### 7.4 用户级查询
- 按用户 ID 查询其好感度当前值、等级、近期变化日志
- 管理员手动调整(加减好感度,走 `source='admin_adjust'` 记 log
---
## 八、多端触发点一览
好感度变化可由以下端点触发,所有端都走**同一个服务端入口**
| 触发来源 | 规则 | 通道 | 位置参考 |
|---|---|---|---|
| 设备端上报「用户发起对话」 | `chat` | WebSocket | [device_interaction/consumers.py](../qy_lty/device_interaction/consumers.py) `chat_message` |
| 设备端对话结束(陪伴时长) | `chat` 或独立 `companion_time` | WebSocket | `conversation_status` 的 begin/end |
| 手机端点击「唱歌/跳舞/抚摸」 | 对应 rule_key | WebSocket | consumers.py `sing` / `dance` / `touch` |
| 手机端赠礼 / 喂食 / 换装 / 用道具 | 对应 rule_key | HTTP | 对应业务 ViewSet 钩子 |
| 管理员手动调整 | 无 rule | HTTP Admin API | 管理后台 |
| 衰减定时任务 | `decay` | 后台任务 | 定时调度 |
**身份识别**
- 设备端MAC 登录获取 token → 服务端通过 `UserDevice` 找到 `user_id`
- 手机端Redis token 认证 → 直接拿到 `user_id`
- 服务端只认 `user_id`,与触发端无关
---
## 九、数据契约(接口层摘要)
### 9.1 管理端
| 接口 | 方法 | 用途 |
|---|---|---|
| `/api/admin/affinity/rules/` | GET/POST/PATCH/DELETE | 互动规则 CRUD |
| `/api/admin/affinity/levels/` | GET/POST/PATCH/DELETE | 等级 CRUD |
| `/api/admin/affinity/settings/` | GET/PUT | 全局参数(单例) |
| `/api/admin/affinity/logs/` | GET | 变化日志查询(可按 user、rule、时间过滤 |
| `/api/admin/affinity/stats/` | GET | 统计聚合 |
| `/api/admin/affinity/adjust/` | POST | 管理员手动调整(必须留审计) |
### 9.2 客户端(手机端 / 设备端共用)
| 接口 | 方法 | 用途 |
|---|---|---|
| `/api/user/me/affinity/` | GET | 当前好感度、等级、下一级进度、近期变化 |
| `/api/user/me/affinity/claim-reward/` | POST | 领取等级奖励 |
### 9.3 WebSocket 实时推送
| 事件 | 方向 | payload |
|---|---|---|
| `affinity_update` | 服务端 → 用户 | `{change, before, after, rule_key, source}` |
| `level_up` | 服务端 → 用户 | `{old_level, new_level, reward}` |
| `level_down` | 服务端 → 用户 | `{old_level, new_level}`(衰减导致降级) |
---
## 十、待开发拼板
| 模块 | 前端 | 后端模型 | 后端接口 | 触发埋点 |
|---|---|---|---|---|
| 互动规则 CRUD | ✅ UI 完成 | ⚠️ 缺字段 | ⚠️ 需扩展 | — |
| 等级 CRUD | ✅ UI 完成 | ⚠️ 缺字段 | ⚠️ 需扩展 | — |
| 全局设置 | ✅ UI 完成 | ❌ 无表 | ❌ 无接口 | — |
| 衰减配置 | ✅ UI 完成 | ❌ 无表 | ❌ 无接口 | ❌ 无定时任务 |
| 变化日志 | ❌ 无 UI | ❌ 无表 | ❌ 无接口 | — |
| 数据统计 | ⚠️ Mock 展示 | — | ❌ 无聚合接口 | — |
| 客户端查询 | — | — | ❌ 无接口 | — |
| WS 实时推送 | — | — | — | ❌ 未接入 |
| 设备/手机事件埋点 | — | — | — | ❌ 未接入 |
图例:✅ 已完成 ⚠️ 部分完成 ❌ 未开始
---
## 十一、术语表
| 术语 | 含义 |
|---|---|
| rule_key | 互动规则的代码级标识(如 `chat`),客户端事件通过它匹配规则 |
| single_cap | 单次变化绝对值上限(保护性钳位) |
| daily_cap | 单规则每日累计变化上限 |
| 全局日上限 | 跨所有正向规则的顶层日增长上限(`AffinitySetting.daily_cap` |
| 冷却 | 同一用户同一规则的最小触发间隔 |
| source | 变化来源:`device_event` / `mobile_event` / `system_decay` / `admin_adjust` |
| 跨级 | 好感度变化使得用户从一个等级区间移动到另一个 |