# 好感度系统功能与规则设计

> 文档范围：本文档描述**洛天依管理系统**中「好感度系统」的全部功能模块与规则设计，用于产品对齐、联调对接与后续服务端落地。
>
> 当前状态：管理后台 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) 第 79–115 行），但前端尚未实际调用真实接口，设备端/手机端事件尚未接入好感度逻辑。

---

## 一、系统定位

好感度系统用于刻画**每一台洛天依设备与用户**之间的亲密度关系。其核心价值：

- 让用户对每台设备的互动行为产生**持续、可感知的数值反馈**
- 通过**等级 + 解锁内容**形成长期陪伴动机
- 对不活跃设备通过**衰减机制**保留流失预警与召回空间

**关键设计：好感度是「设备级」而不是「用户级」。**

- 一个用户可以绑定多台设备
- 每台设备维护自己独立的好感度值、等级、解锁内容
- 同一用户在不同设备上的好感度互不影响
- 数值范围 `[0, max_affinity]`（默认上限 100），落库到 `UserDevice.favorability`

---

## 二、功能模块总览

管理后台分 4 个标签页，对应 5 块功能：

| 模块 | 页签 | 功能说明 |
|---|---|---|
| 系统概览 | 系统概览 | 关键指标卡片 + 全局基础参数设置 |
| 互动规则 | 互动规则 | 管理各类互动行为的好感度变化规则 |
| 衰减规则 | 互动规则页下半 | 配置不活跃设备的好感度衰减策略 |
| 等级奖励 | 等级奖励 | 管理好感度等级划分与奖励发放 |
| 数据统计 | 数据统计 | 好感度分布、互动分析、趋势监控 |

---

## 三、模块 1 — 系统概览与全局参数

### 3.1 关键指标卡片

所有指标的**统计基本单位是「设备」**（每条 `UserDevice` 绑定算一份）。

| 指标 | 含义 | 数据源 |
|---|---|---|
| 平均好感度 | 所有「用户-设备绑定」的 `favorability` 平均值 | 聚合 `UserDevice` |
| 最高好感度 | 达到 `max_affinity` 上限的**设备数** | `UserDevice.favorability = max_affinity` 计数 |
| 互动次数/日 | 当天触发的所有规则次数总和 | 聚合 `AffinityLog` |
| 活跃用户比例 | 近 N 天有互动的**设备**占比 | 聚合 `UserDevice.last_active_at` |

### 3.2 全局参数（AffinitySetting，单例）

| 字段 | 默认值 | 说明 |
|---|---|---|
| `initial_affinity` | 10 | 新建 `UserDevice` 绑定时的初始好感度 |
| `max_affinity` | 100 | 好感度上限（管理员手动调整也不能突破） |
| `daily_cap` | 20 | **每台设备**每日好感度净增长上限（跨规则汇总） |
| `decay_rate` | 2 点/天 | 全局默认衰减速率（衰减规则可覆盖） |
| `decay_threshold` | 3 天 | 设备多少天不互动后开始衰减 |
| `enable_notify` | true | 好感度变化是否推送通知 |
| `enable_rewards` | true | 是否启用等级奖励发放 |
| `timezone` | `Asia/Shanghai` | 自然日 0 点重置基准（全用户统一） |

**规则**：
- `initial_affinity` 只影响**新建的设备绑定**，不回溯修改已有绑定
- `max_affinity` 调低后，**已超过的设备保留原值**，但不再增加
- `daily_cap` 是**每设备**跨所有正向规则的顶层日增长上限，触达后当日所有正向增益无效（衰减不受此限）
- `timezone` 决定"今天"的边界 — 默认 `Asia/Shanghai`，0 点（北京时间）重置所有日上限计数器

---

## 四、模块 2 — 互动规则

### 4.1 规则字段定义

| 字段 | 类型 | 示例 | 说明 |
|---|---|---|---|
| `id` | string | `rule-2` | 规则唯一 ID |
| `name` | string | `对话` | 规则展示名 |
| `type` / `rule_key` | enum | `chat` | **代码标识**，服务端事件通过它匹配规则，不可重复 |
| `description` | string | `与洛天依进行对话` | 展示描述 |
| `minChange` | int | 1 | 单次好感度变化最小值 |
| `maxChange` | int | 5 | 单次好感度变化最大值 |
| `singleCap` | int | 5 | 单次变化绝对值上限（保护性钳位，防止数据异常） |
| `dailyCap` | int | 15 | **本规则在每台设备**每日累计变化上限（绝对值） |
| `isNegative` | bool | false | 是否负向规则。正向用正数，负向用负数 |
| `isEnabled` | bool | true | 是否启用，禁用后事件不触发 |

**变化值算法**：每次触发，服务端在 `[minChange, maxChange]` 闭区间内**随机取整数**作为本次变化值。如果两个值相等就是固定值，不等则随机。

### 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, device_id, rule_key, 来源上下文)
  │
  ▼
① 校验：device_id 必须属于 user_id（UserDevice 存在）
  │
  ▼
② 取规则 → 若 is_enabled=false，丢弃
  │
  ▼
③ 冷却检查（Redis：affinity:cd:{device_id}:{rule_key}）
  │  未到冷却 → 丢弃
  ▼
④ 本规则日上限检查（affinity:daily:{device_id}:{rule_key}:{date}）
  │  已满 → 丢弃
  ▼
⑤ 全局日上限检查（仅正向，affinity:daily:{device_id}:_global:{date}）
  │  已满 → 丢弃
  ▼
⑥ 计算变化值 = random(min, max)
     ↓ 按 single_cap 钳位
  │
  ▼
⑦ 原子更新 UserDevice.favorability
     ↓ 钳位到 [0, max_affinity]
  │
  ▼
⑧ 写 AffinityLog、更新计数器、刷新 UserDevice.last_active_at
  │
  ▼
⑨ 判断是否跨越等级边界
  │  是 → 触发等级变更事件（发奖励 + 推通知，详见 6.3）
  ▼
⑩ 通过 WebSocket 向用户的所有在线端推送 affinity_update（payload 含 device_id）
```

### 4.4 规则设计约定

- **单一写入入口**：所有好感度变化必须经由服务端统一入口，客户端不能直接增减
- **rule_key 即契约**：客户端事件不携带分值，只报「设备 X 触发了 `gift` 规则」，具体加多少由服务端按规则算
- **幂等防护**：同一 `device_id` + 同一 `rule_key` + 同一事件 ID 在冷却窗口内只生效一次
- **禁用规则的兜底**：管理员禁用某规则后，客户端若继续上报该事件，服务端静默丢弃

### 4.5 陪伴时长规则（特殊规则）

陪伴时长有两个独立配置字段，不同于普通规则：

| 字段 | 默认 | 说明 |
|---|---|---|
| `min_continuous_minutes` | 5 | 单次会话需达到的**最小连续分钟数**才计一次 |
| `max_count_per_day` | 6 | **每台设备每日**最多触发次数 |
| `change_per_count` | +3 | 每次满足后获得的好感度（在 [min, max] 区间随机） |

**计算方式**：
- 服务端在 `conversation_status` 收到 `begin` 时启动会话计时器
- 收到 `end` 时计算时长 = `end_at − start_at`
- 触发次数 = `floor(时长_分钟 / min_continuous_minutes)`，受当日剩余次数（`max_count_per_day` 减去已触发数）限制
- 每触发一次，按规则计算变化值并记一条 `AffinityLog`
- 中断（断线、强制结束）只算到中断时刻

**示例**：`min_continuous_minutes=5, max_count_per_day=6`
- 用户聊了 17 分钟 → `floor(17/5)=3` 次 → 加 3 次好感度
- 用户当天已聊过 5 次（已触发 5 次），又聊 17 分钟 → 只能再触发 1 次（剩余 1 次）

---

## 五、模块 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（Asia/Shanghai）由定时任务统一跑一次
- **命中对象**：`UserDevice.last_active_at < now - decay_start_days` 的**设备**
- **每台设备独立判断**：同一用户多设备只衰减不活跃的那几台
- **落库**：每条衰减都写 `AffinityLog`，`source='system_decay'`，`device_id` 必填
- **下限保护**：若设备当前好感度 ≤ `min_floor` 则跳过
- **与互动的关系**：设备当天有互动即**重置不活跃计数**（`last_active_at` 刷新），次日不衰减

### 5.3 设计权衡

- 衰减**不占用**全局日上限（`AffinitySetting.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 等级变化规则

**等级是「设备级」概念**：每台 `UserDevice` 各自维护当前等级，与同用户其他设备无关。

- 等级由好感度区间**自动映射**，不是独立字段（缓存到 `UserDevice.affinity_level`）
- **跨级判定**：每次好感度变动后，取当前值所属区间，与上一次等级比较
  - **升级**：从 N 级到 M 级（M > N）→ **逐级发放** N+1、N+2、…、M 每一级的奖励（一次性发完）
  - **降级**（衰减导致）：不追回奖励，但取消该设备对应等级的解锁内容访问权限
- **奖励幂等**：用 `UserLevelRewardGrant(device_id, level)` 表记录"已发放"标记
  - 同一设备同一等级**永久只发一次**
  - 降级回升再次跨过同等级 → **不再发放**（已发标记保留，不清除）
  - 标记跨衰减保留：用户从 5 级衰减到 4 级，再升回 5 级，5 级奖励不重发
- **新增等级的存量处理**：管理员新增等级时（如新增 6 级），已经处于符合条件区间的设备**不立即触发**，需要下次好感度变化时才判定

### 6.4 跨设备解锁

**解锁内容仅在达到该等级的那台设备上有效**：

- 设备 A 升到 4 级解锁了限定服装 → 限定服装只能在设备 A 上使用
- 同一用户拿设备 B 登录手机端 → 看不到/用不了设备 A 解锁的内容
- 客户端展示要明确按 `device_id` 过滤已解锁内容

### 6.5 区间约束

- 区间**不得重叠**，管理端保存时做校验
- 区间**不得有空隙**（如等级 2 的 max=40，则等级 3 的 min 必须是 41）
- 最低等级 `min=0`，最高等级 `max=max_affinity`

---

## 七、模块 5 — 数据统计

### 7.1 概览指标（按设备聚合）
- 平均好感度 / 中位数 / 最高好感度
- 活跃设备数（近 7 日有互动的 `UserDevice` 数）
- 今日互动次数、今日新增好感度总量

### 7.2 分布分析
- 好感度区间分布（0-20、21-40…），按设备数统计
- 各等级设备数占比
- 各互动规则触发频次 Top N

### 7.3 趋势分析
- 日/周/月的设备平均好感度变化曲线
- 日互动量趋势
- 衰减命中设备数趋势

### 7.4 用户级查询
- 按用户 ID 查询：返回该用户名下**所有设备**的好感度列表（device_id、设备名、当前值、等级、最近互动时间）
- 单设备维度：可点击展开看具体某台设备的近期变化日志
- 管理员手动调整：必须**指定 `device_id`**，不能给"用户"加好感度（详见 9.1）

---

## 八、多端触发点一览

好感度变化可由以下端点触发，所有事件**必须携带 `device_id`**，最终走同一个服务端入口：

| 触发来源 | 规则 | 通道 | device_id 来源 | 位置参考 |
|---|---|---|---|---|
| 设备端上报「用户发起对话」 | `chat` | WebSocket | 设备登录态自带 | [device_interaction/consumers.py](../qy_lty/device_interaction/consumers.py) `chat_message` |
| 设备端对话结束（陪伴时长） | `companion_time` | WebSocket | 设备登录态自带 | `conversation_status` 的 begin/end |
| 手机端点击「唱歌/跳舞/抚摸」 | 对应 rule_key | WebSocket | 手机端选定的当前设备 | consumers.py `sing` / `dance` / `touch` |
| 手机端赠礼 / 喂食 / 换装 / 用道具 | 对应 rule_key | HTTP | 请求体显式传 device_id | 对应业务 ViewSet 钩子 |
| 管理员手动调整 | 无 rule | HTTP Admin API | 请求体显式传 device_id | 管理后台 |
| 衰减定时任务 | `decay` | 后台任务 | 任务遍历 UserDevice 时取 | 定时调度 |

**身份识别**：
- 设备端：MAC 登录获取 token → 服务端通过 `UserDevice` 反查 `user_id` 和 `device_id`，事件自动绑定到该设备
- 手机端：Redis token 认证拿到 `user_id`，**事件必须显式指定要操作哪台设备**（用户在 App 里可能切换设备）
- 服务端只认 `(user_id, device_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 / device / rule / 时间过滤） |
| `/api/admin/affinity/stats/` | GET | 统计聚合（设备维度） |
| `/api/admin/affinity/devices/` | GET | 按 user_id 列出该用户所有设备及好感度 |
| `/api/admin/affinity/adjust/` | POST | 手动调整**单台设备**好感度，必传 `device_id`，钳位到 `[0, max_affinity]` |
| `/api/admin/affinity/adjust-batch/` | POST | 批量给某用户**名下所有设备**各 ±X（用于群体补偿） |

### 9.2 客户端（手机端 / 设备端共用）

| 接口 | 方法 | 用途 |
|---|---|---|
| `/api/user/me/affinity/` | GET | 返回**当前用户名下所有设备**的好感度列表（含 device_id、当前值、等级、下一级进度） |
| `/api/user/me/affinity/?device_id=xxx` | GET | 查询指定设备的详情（近期变化、已解锁内容） |
| `/api/user/me/affinity/claim-reward/` | POST | 领取奖励（必传 `device_id` + `level`） |

### 9.3 WebSocket 实时推送

所有 WS 事件 payload 必须包含 `device_id`，客户端按 device_id 路由到对应 UI：

| 事件 | 方向 | payload |
|---|---|---|
| `affinity_update` | 服务端 → 用户的所有在线端（手机+设备） | `{device_id, change, before, after, rule_key, source}` |
| `level_up` | 服务端 → 用户的所有在线端 | `{device_id, old_level, new_level, rewards: [...]}`（rewards 为本次跨级一次性发放的所有等级奖励列表） |
| `level_down` | 服务端 → 用户的所有在线端 | `{device_id, old_level, new_level}`（衰减导致降级） |

**多设备并行**：用户绑了 3 台设备，同时在线时收到 3 条独立的 `affinity_update`，前端按 `device_id` 显示在不同卡片。

---

## 十、待开发拼板

| 模块 | 前端 | 后端模型 | 后端接口 | 触发埋点 |
|---|---|---|---|---|
| 互动规则 CRUD | ✅ UI 完成 | ⚠️ 缺字段 | ⚠️ 需扩展 | — |
| 等级 CRUD | ✅ UI 完成 | ⚠️ 缺字段 | ⚠️ 需扩展 | — |
| 全局设置 | ✅ UI 完成 | ❌ 无表 | ❌ 无接口 | — |
| 衰减配置 | ✅ UI 完成 | ❌ 无表 | ❌ 无接口 | ❌ 无定时任务 |
| **设备级好感度迁移** | — | ❌ `UserDevice.favorability` 字段缺 | — | — |
| 变化日志 | ❌ 无 UI | ❌ 无表（需带 device 维度） | ❌ 无接口 | — |
| 数据统计 | ⚠️ Mock 展示 | — | ❌ 无聚合接口 | — |
| 客户端查询（多设备列表） | — | — | ❌ 无接口 | — |
| 等级奖励发放标记 | — | ❌ `UserLevelRewardGrant` 表缺 | — | — |
| 陪伴时长规则 | ⚠️ UI 没字段 | ❌ 规则字段缺 | — | ❌ 计时器未实现 |
| WS 实时推送 | — | — | — | ❌ 未接入 |
| 设备/手机事件埋点 | — | — | — | ❌ 未接入 |

图例：✅ 已完成　⚠️ 部分完成　❌ 未开始

---

## 十一、术语表

| 术语 | 含义 |
|---|---|
| rule_key | 互动规则的代码级标识（如 `chat`），客户端事件通过它匹配规则 |
| single_cap | 单次变化绝对值上限（保护性钳位） |
| daily_cap（规则） | 单设备单规则每日累计变化上限 |
| 全局日上限 | 单设备跨所有正向规则的顶层日增长上限（`AffinitySetting.daily_cap`） |
| 冷却 | 同一设备同一规则的最小触发间隔 |
| source | 变化来源：`device_event` / `mobile_event` / `system_decay` / `admin_adjust_single` / `admin_adjust_batch` |
| 跨级 | 好感度变化使得设备从一个等级区间移动到另一个 |
| UserDevice | 用户与设备的绑定关系，好感度落在这一行 |
| 已发奖励标记 | `UserLevelRewardGrant(device_id, level)` 唯一记录，永久保留 |

---

## 十二、决策记录

以下决策已确认，作为开发依据：

| # | 决策点 | 结论 |
|---|---|---|
| 1 | 规则值域 | `min_change` 和 `max_change` 之间**随机取整数** |
| 2 | 陪伴时长 | 配两个字段：`min_continuous_minutes`（最小连续分钟数）+ `max_count_per_day`（每日最多次数）。算法：满 X 分钟触发 1 次，封顶 Y 次 |
| 3 | 跨级奖励 | 升级时**逐级发放**经过的每一级；降级回升后**不再补发**（永久幂等） |
| 4 | 衰减频次 | 每天 00:30 一次（Asia/Shanghai） |
| 5 | 日上限重置 | 自然日 0 点重置（Asia/Shanghai 时区） |
| 6 | 管理员钳位 | 手动调整**不能突破** `max_affinity` 上限和 `0` 下限 |
| 7 | 推送范围 | `affinity_update` / `level_up` 推送给手机端**和**设备端（用户的所有在线端） |
| 8 | 好感度归属 | **设备级**：好感度落在 `UserDevice.favorability`，每台设备独立。等级、解锁内容、衰减、日上限**全部按设备独立计算** |
| 8.A | 好感度字段 | 新增 `UserDevice.favorability`，原 `ParadiseUser.favorability` 废弃（迁移时把已有值写到主设备绑定上） |
| 8.B | 等级判定 | 每台设备独立判定 |
| 8.C | 跨设备解锁 | 解锁内容**只在解锁那台设备**上可用，不跨设备共享 |
| 8.D | 客户端查询 | `/api/user/me/affinity/` 默认返回**全部设备列表**，可带 `device_id` 查指定设备 |
| 8.E | 衰减判定 | 按每台设备的 `last_active_at` 各自判断 |
| 8.F | 规则日上限 | 每台设备各自计 |
| 8.G | 统计口径 | 平均好感度等指标按**设备**聚合；用户查询页展开每台设备 |
| 9 | 跨级新增等级 | 新增等级后已在范围内的设备**不立即触发**，下次变化时再判 |
| 10 | 修改已有等级奖励 | 已发过的设备**不补发** |
| 11 | 已发标记跨衰减 | 衰减导致降级**不清除**已发标记 |
| 12 | 管理员调整归属 | 必须指定 `device_id`；额外提供「批量给名下所有设备各加 X」的接口 |
| 13 | 冷却字段 | `cooldown_seconds` **暴露给管理员**配置，作为 `AffinityRule` 的字段 |
| 14 | 客户端首屏同步 | 设备/手机端登录后**主动调用** `/api/user/me/affinity/?device_id=xxx` 拉一次最新值，避免 WS 离线期间漏推 |

---

## 十三、待敲定细节（不阻塞 P1/P2，按建议默认方案先走，需要时再定）

### 13.1 必须在指定阶段前敲定

| # | 决策点 | 默认方案（建模阶段保留选项的兜底做法） | 真正阻塞阶段 |
|---|---|---|---|
| A1 | 设备解绑后重新绑定的好感度 | `UserDevice` 加 `is_active` 软删除字段，数据保留。具体行为（恢复 / 归零）可后期切换 | P5（客户端绑定接口） |
| A2 | 设备转移给新用户 | 旧 `UserDevice` 标记 `is_active=false` 归档；新用户新建一条 `UserDevice` 从 `initial_affinity` 开始 | 仅当产品确认需要转移流程时 |
| A3 | 跨级奖励的事务边界 | 方案 B：每级独立事务，失败的标记为待补发，后台重试 | P3（service 层奖励发放） |
| A4 | 好感度变化通知投递通道 | 先实现 WebSocket 实时推送一种通道；站内信、APP 推送后续叠加 | P5（客户端通知 UI） |

### 13.2 非阻塞，待数据/运营反馈后再定

| # | 决策点 | 默认方案 | 备注 |
|---|---|---|---|
| B1 | 等级 / 规则删除语义 | 软删除（`is_deleted=true`），日志保留完整。`AffinityLog.rule` 用 SET NULL，冗余 `rule_key` 文本字段保留可读性 | 永远不必硬定 |
| B2 | `AffinityLog` 保留期限 | 暂永久保留；衰减日志按"设备×天"合并降低增长。日志表上线半年后再决定归档策略 | 数据量起来再看 |
| B3 | 通知聚合频次 | 每次变化都推 WS（前端按需做防抖） | 前端体验出来再调 |

### 13.3 接受默认方案，非紧急（如有异议再讨论）

| # | 决策点 | 默认方案 |
|---|---|---|
| C1 | 首次绑定是否触发等级 1 奖励 | 触发。`initial_affinity=10` 落在等级 1 区间，发等级 1 奖励 |
| C2 | 等级名称是否允许改 | 允许。改名只影响展示，等级序号不变，不重发奖励 |
| C3 | 管理员操作审计 | `AffinityLog` 加 `operator_admin_id`（仅 admin 来源时填）+ `reason` 文本字段（备注） |
| C4 | 客户端缓存策略 | 服务端为准；客户端只缓存展示，WS 收不到时主动拉接口 |
| C5 | `max_affinity` 调整对存量等级的影响 | 调高：自动扩展最高等级 max；调低：超出设备保留原值，下次变化时按新边界判等级 |
| C6 | 进度条算法 | `(value - level.min) / (level.max - level.min) × 100%` |
| C7 | 多账号防刷 | 同一 MAC 在 7 天内最多绑定 3 个不同用户（`UserDevice.bound_at` 限制） |
| C8 | 跨规则同时触发顺序 | 严格按服务端接收顺序串行处理（`(device_id, rule_key)` 维度的 Redis 锁） |
| C9 | 同一事件 ID 防重 | 客户端事件携带 `event_id`（UUID），服务端缓存 60s 去重 |
| C10 | 设备登录后客户端首屏 | 拉一次 `/api/user/me/affinity/?device_id=当前设备` 同步最新值（已敲定，13 列决策） |

---
