442 lines
18 KiB
Markdown
442 lines
18 KiB
Markdown
# 等级体系架构
|
||
|
||
本文只定义等级体系,不包含成就体系。等级体系用于表达用户在财富、游戏和魅力三条成长轨道上的长期身份,并在达到指定等级或等级段时发放永久资源奖励。
|
||
|
||
## 目标
|
||
|
||
- 财富等级:按用户送出礼物价值累计。
|
||
- 游戏等级:按用户游戏消耗累计。
|
||
- 魅力等级:按用户收到礼物价值累计。
|
||
- 后台可配置每条轨道的等级阈值、等级段展示、等级奖励资源组。
|
||
- 每到指定等级或等级段,系统自动发放头像框、徽章、礼物等永久资源。
|
||
- 等级段资源支持替换展示:例如 1-10 级展示 A 头像框/A 徽章,11-20 级展示 B 头像框/B 徽章;A/B 都是用户永久持有资源,但当前等级段只展示 B。
|
||
- App 和房间资料只读服务端等级摘要与展示资源,客户端不能自报等级、经验或徽章。
|
||
|
||
## 非目标
|
||
|
||
- 不做历史兼容;当前开发阶段按最新等级规则直接调整 proto、表结构和接口。
|
||
- 首版不做降级。退款、撤销、风控处罚等后续事实只进入补偿或人工处理,不自动扣回等级。
|
||
- 不把等级和 VIP 混合。VIP 仍由 `wallet-service` 作为付费会员权益 owner;等级由 `activity-service` 消费事实事件派生。
|
||
- 不在送礼、游戏扣费、房间命令主链路里同步调用等级系统。
|
||
- 不允许客户端提交“完成等级进度”或“切换等级段资源”的业务事实。
|
||
|
||
## 服务边界
|
||
|
||
| 模块 | 责任 | 不负责 |
|
||
| --- | --- | --- |
|
||
| `activity-service` | 等级轨道、等级规则、等级段、用户等级账户、等级事件幂等、升级历史、等级奖励任务、展示投影 | 不拥有钱包余额、资源权益、房间状态、游戏订单 |
|
||
| `wallet-service` | 礼物扣费/收礼账务事实、资源组发放、永久资源权益、资源装备表 | 不判断等级是否升级 |
|
||
| `game-service` | 游戏订单和游戏消耗事实;金币改账继续走 `wallet-service` | 不保存等级进度 |
|
||
| `room-service` | 只拥有 Room Cell 和房间表现;送礼成功后写 room outbox | 不计算财富/魅力等级 |
|
||
| `gateway-service` | App HTTP 入口、鉴权、`request_id`、调用 activity gRPC | 不查等级数据库 |
|
||
| `server/admin` | 等级后台配置和审计,通过 protobuf client 访问 app 后端 | 不 import app 服务内部包 |
|
||
|
||
等级体系首版放在 `activity-service/internal/domain/growth`。原因是现有每日任务已经在 `activity-service` 内实现了事实事件消费、用户进度、后台配置和钱包奖励发放模式,等级是同类低频派生状态。
|
||
|
||
## 等级轨道
|
||
|
||
| 轨道 | 代码 | 权威指标 | 来源事实 | 单位 |
|
||
| --- | --- | --- | --- | --- |
|
||
| 财富等级 | `wealth` | 送出礼物价值 | `wallet-service` 的 `WalletGiftDebited`,取 sender 侧实际消耗 | coin |
|
||
| 游戏等级 | `game` | 游戏消耗 | `wallet-service` 的游戏扣费成功 outbox,取用户实际消耗 | coin |
|
||
| 魅力等级 | `charm` | 收到礼物价值 | `wallet-service` 的 `WalletGiftDebited`,取 target 侧收到的礼物结算价值 | coin |
|
||
|
||
财富和魅力默认以钱包账务事实为准,因为礼物价格、扣费和收礼积分都由 `wallet-service` owner。`RoomGiftSent` 可作为房间表现、活动播报或对账补充,但不能替代账务事实作为价值来源。
|
||
|
||
## 事件流
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
Wallet["wallet-service wallet_outbox"] --> Relay["durable event relay"]
|
||
Game["game-service/game wallet events"] --> Relay
|
||
Relay --> Consumer["activity-service growth consumer"]
|
||
Consumer --> DB[("activity MySQL growth tables")]
|
||
DB --> RewardJob["growth_reward_jobs"]
|
||
RewardJob --> WalletGrant["wallet-service GrantResourceGroup"]
|
||
DB --> Projection["user_level_display_profiles"]
|
||
App["App/gateway"] --> Query["activity-service GrowthService"]
|
||
Query --> DB
|
||
```
|
||
|
||
处理原则:
|
||
|
||
- 来源服务只发布事实,不理解等级规则。
|
||
- activity-service 用 `event_id + metric_type + user_id + track` 生成派生事件幂等键。
|
||
- 同一事件可以同时影响多人或多轨道。例如一次送礼会给 sender 增加财富等级进度,给 target 增加魅力等级进度。
|
||
- 等级奖励发放不阻塞事件消费;先写 `growth_reward_jobs`,再异步调用 `wallet-service.GrantResourceGroup`。
|
||
|
||
## 后台配置模型
|
||
|
||
### 等级轨道
|
||
|
||
`growth_level_tracks`
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| `track` | `wealth` / `game` / `charm` |
|
||
| `name` | 后台和 App 展示名 |
|
||
| `status` | `active` / `disabled` |
|
||
| `sort_order` | 展示排序 |
|
||
| `display_config_json` | 颜色、图标、说明等展示配置,不参与计算 |
|
||
|
||
### 等级规则
|
||
|
||
`growth_level_rules`
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| `track` | 等级轨道 |
|
||
| `level` | 等级数字,从 1 开始 |
|
||
| `required_value` | 达到该等级需要的累计值 |
|
||
| `name` | 等级展示名 |
|
||
| `status` | `active` / `disabled` |
|
||
| `reward_resource_group_id` | 达到该具体等级时发放的资源组,0 表示无奖励 |
|
||
| `sort_order` | 后台排序 |
|
||
| `display_config_json` | 单等级展示配置 |
|
||
|
||
约束:
|
||
|
||
- 同一 `track + level` 唯一。
|
||
- 同一轨道内 `required_value` 必须随 `level` 严格递增。
|
||
- 已 active 的等级规则如果修改 `required_value` 或奖励资源组,后台必须生成新版本;是否重算历史由单独重算任务决定。
|
||
|
||
### 等级段展示
|
||
|
||
`growth_level_tiers`
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| `tier_id` | 等级段 ID |
|
||
| `track` | 等级轨道 |
|
||
| `min_level` | 等级段起始等级,包含 |
|
||
| `max_level` | 等级段结束等级,包含 |
|
||
| `name` | 等级段展示名,例如 `财富 1-10` |
|
||
| `display_avatar_frame_resource_id` | 当前等级段默认展示头像框资源 |
|
||
| `display_badge_resource_id` | 当前等级段默认展示徽章资源 |
|
||
| `reward_resource_group_id` | 首次进入该等级段时发放的永久资源组 |
|
||
| `status` | `active` / `disabled` |
|
||
| `display_config_json` | 颜色、角标、客户端样式配置 |
|
||
|
||
等级段用于解决“1-10 级一套头像框/徽章,11-20 级换一套”的需求。资源本身仍由 `wallet-service` 永久发放;当前展示由等级投影决定,不删除旧资源。
|
||
|
||
约束:
|
||
|
||
- 同一轨道 active 等级段不能重叠。
|
||
- `min_level <= max_level`。
|
||
- `display_avatar_frame_resource_id` 必须是 active 且可发放的 `avatar_frame` 资源。
|
||
- `display_badge_resource_id` 必须是 active 且可发放的 `badge` 资源。
|
||
- `reward_resource_group_id` 对应资源组里的等级段头像框/徽章通常配置 `duration_ms=0`,表示永久权益。
|
||
|
||
## 用户等级数据
|
||
|
||
### 用户等级账户
|
||
|
||
`user_growth_level_accounts`
|
||
|
||
```sql
|
||
user_growth_level_accounts(
|
||
app_code VARCHAR(32) NOT NULL,
|
||
user_id BIGINT NOT NULL,
|
||
track VARCHAR(32) NOT NULL,
|
||
total_value BIGINT NOT NULL DEFAULT 0,
|
||
current_level INT NOT NULL DEFAULT 0,
|
||
current_tier_id BIGINT NOT NULL DEFAULT 0,
|
||
level_updated_at_ms BIGINT NOT NULL DEFAULT 0,
|
||
tier_updated_at_ms BIGINT NOT NULL DEFAULT 0,
|
||
version BIGINT NOT NULL DEFAULT 0,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
PRIMARY KEY(app_code, user_id, track)
|
||
);
|
||
```
|
||
|
||
`total_value` 是等级判断唯一累计值:
|
||
|
||
- 财富:累计送出礼物实际消耗 coin。
|
||
- 游戏:累计游戏实际消耗 coin。
|
||
- 魅力:累计收到礼物价值 coin。
|
||
|
||
### 等级事件流水
|
||
|
||
`growth_level_value_events`
|
||
|
||
```sql
|
||
growth_level_value_events(
|
||
app_code VARCHAR(32) NOT NULL,
|
||
value_event_id VARCHAR(160) NOT NULL,
|
||
source_event_id VARCHAR(128) NOT NULL,
|
||
source_service VARCHAR(64) NOT NULL,
|
||
source_event_type VARCHAR(96) NOT NULL,
|
||
user_id BIGINT NOT NULL,
|
||
track VARCHAR(32) NOT NULL,
|
||
metric_type VARCHAR(64) NOT NULL,
|
||
value_delta BIGINT NOT NULL,
|
||
occurred_at_ms BIGINT NOT NULL,
|
||
dimensions_json JSON NULL,
|
||
created_at_ms BIGINT NOT NULL,
|
||
PRIMARY KEY(app_code, value_event_id),
|
||
KEY idx_growth_value_user_track(app_code, user_id, track, occurred_at_ms)
|
||
);
|
||
```
|
||
|
||
`value_event_id` 示例:
|
||
|
||
- `wallet_outbox_id:wealth:sender:{sender_user_id}`
|
||
- `wallet_outbox_id:charm:target:{target_user_id}`
|
||
- `game_order_id:game:{user_id}`
|
||
|
||
同一幂等键重复投递直接返回已消费,不再次增加等级值。
|
||
|
||
### 升级历史
|
||
|
||
`user_growth_level_history`
|
||
|
||
```sql
|
||
user_growth_level_history(
|
||
app_code VARCHAR(32) NOT NULL,
|
||
history_id VARCHAR(96) NOT NULL,
|
||
user_id BIGINT NOT NULL,
|
||
track VARCHAR(32) NOT NULL,
|
||
previous_level INT NOT NULL,
|
||
new_level INT NOT NULL,
|
||
previous_tier_id BIGINT NOT NULL DEFAULT 0,
|
||
new_tier_id BIGINT NOT NULL DEFAULT 0,
|
||
total_value BIGINT NOT NULL,
|
||
value_event_id VARCHAR(160) NOT NULL,
|
||
created_at_ms BIGINT NOT NULL,
|
||
PRIMARY KEY(app_code, history_id),
|
||
KEY idx_growth_history_user_track(app_code, user_id, track, created_at_ms)
|
||
);
|
||
```
|
||
|
||
一次大额事件可以跨多级。账户只保存最终等级;历史可以选择写一条 `previous_level -> new_level`,奖励任务按“跨过的等级”和“首次进入的等级段”逐条生成。
|
||
|
||
## 等级奖励
|
||
|
||
等级奖励分两类:
|
||
|
||
| 类型 | 触发 | 例子 | 是否永久 |
|
||
| --- | --- | --- | --- |
|
||
| 具体等级奖励 | 达到某个 level | 到 10 级送礼物包 | 由资源组 item `duration_ms` 决定 |
|
||
| 等级段奖励 | 首次进入等级段 | 1-10 头像框/徽章,11-20 新头像框/徽章 | 通常永久,`duration_ms=0` |
|
||
|
||
奖励任务表:
|
||
|
||
```sql
|
||
growth_level_reward_jobs(
|
||
app_code VARCHAR(32) NOT NULL,
|
||
reward_job_id VARCHAR(96) NOT NULL,
|
||
user_id BIGINT NOT NULL,
|
||
track VARCHAR(32) NOT NULL,
|
||
reward_source_type VARCHAR(32) NOT NULL,
|
||
reward_source_id VARCHAR(96) NOT NULL,
|
||
resource_group_id BIGINT NOT NULL,
|
||
wallet_command_id VARCHAR(128) NOT NULL,
|
||
wallet_grant_id VARCHAR(96) NOT NULL DEFAULT '',
|
||
status VARCHAR(32) NOT NULL,
|
||
attempt_count INT NOT NULL DEFAULT 0,
|
||
next_retry_at_ms BIGINT NOT NULL DEFAULT 0,
|
||
failure_reason VARCHAR(255) NOT NULL DEFAULT '',
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
PRIMARY KEY(app_code, reward_job_id),
|
||
UNIQUE KEY uk_growth_level_reward_once(app_code, user_id, track, reward_source_type, reward_source_id)
|
||
);
|
||
```
|
||
|
||
`reward_source_type`:
|
||
|
||
- `level`: `reward_source_id = level:{level}`
|
||
- `tier`: `reward_source_id = tier:{tier_id}`
|
||
|
||
发放规则:
|
||
|
||
- activity-service 只创建 reward job,不在事件消费事务里调用 wallet。
|
||
- reward worker 调用 `wallet-service.GrantResourceGroup`,`grant_source=growth_level`。
|
||
- `wallet_command_id` 必须稳定,例如 `growth_level_reward:{reward_job_id}`。
|
||
- 资源组发放成功后写回 `wallet_grant_id`。
|
||
- 失败只影响奖励发放状态,不回滚用户等级。
|
||
- 重试必须复用同一个 `wallet_command_id`,避免重复发放永久资源。
|
||
|
||
## 等级段资源替换展示
|
||
|
||
用户升到新等级段时,旧等级段资源不删除:
|
||
|
||
1. 用户 1-10 级时首次进入 tier A,系统发放 A 头像框/A 徽章永久权益。
|
||
2. 用户 11 级进入 tier B,系统发放 B 头像框/B 徽章永久权益。
|
||
3. 等级展示投影切换为 tier B 的头像框/徽章。
|
||
4. A 头像框/A 徽章仍在用户背包里,用户资源事实不被删除。
|
||
|
||
默认展示有两种实现选择,首版建议使用等级投影优先:
|
||
|
||
| 方案 | 处理 | 适用 |
|
||
| --- | --- | --- |
|
||
| 等级投影优先 | `user_level_display_profiles` 保存当前等级段头像框/徽章,资料和房间展示优先读它 | 等级段身份必须强制跟随等级 |
|
||
| 自动装备钱包资源 | reward job 成功后调用或扩展 `wallet-service` 装备逻辑,更新 `user_resource_equipment` | 产品允许等级奖励直接改变用户当前装备 |
|
||
|
||
首版推荐“等级投影优先”,原因是 `user_resource_equipment` 表表示用户主动装备的当前资源,同一资源类型只有一条记录;等级段身份是系统身份展示,和用户手动装备头像框/徽章不是同一个语义。后续如果产品要求用户可以手动隐藏等级段徽章,再在 activity-service 增加展示偏好。
|
||
|
||
展示投影:
|
||
|
||
```sql
|
||
user_level_display_profiles(
|
||
app_code VARCHAR(32) NOT NULL,
|
||
user_id BIGINT NOT NULL,
|
||
wealth_level INT NOT NULL DEFAULT 0,
|
||
wealth_tier_id BIGINT NOT NULL DEFAULT 0,
|
||
wealth_avatar_frame_resource_id BIGINT NOT NULL DEFAULT 0,
|
||
wealth_badge_resource_id BIGINT NOT NULL DEFAULT 0,
|
||
game_level INT NOT NULL DEFAULT 0,
|
||
game_tier_id BIGINT NOT NULL DEFAULT 0,
|
||
game_avatar_frame_resource_id BIGINT NOT NULL DEFAULT 0,
|
||
game_badge_resource_id BIGINT NOT NULL DEFAULT 0,
|
||
charm_level INT NOT NULL DEFAULT 0,
|
||
charm_tier_id BIGINT NOT NULL DEFAULT 0,
|
||
charm_avatar_frame_resource_id BIGINT NOT NULL DEFAULT 0,
|
||
charm_badge_resource_id BIGINT NOT NULL DEFAULT 0,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
PRIMARY KEY(app_code, user_id)
|
||
);
|
||
```
|
||
|
||
如果某个等级段没有配置头像框或徽章,对应字段返回 0,客户端不展示该类等级段资源。
|
||
|
||
## App 接口
|
||
|
||
### 我的等级总览
|
||
|
||
```http
|
||
GET /api/v1/levels/me/overview
|
||
```
|
||
|
||
返回:
|
||
|
||
```json
|
||
{
|
||
"tracks": [
|
||
{
|
||
"track": "wealth",
|
||
"level": 12,
|
||
"tier_id": 2,
|
||
"total_value": 230000,
|
||
"current_level_required_value": 220000,
|
||
"next_level": 13,
|
||
"next_level_required_value": 260000,
|
||
"display_avatar_frame_resource_id": 91001,
|
||
"display_badge_resource_id": 92001,
|
||
"reward_pending_count": 0
|
||
}
|
||
],
|
||
"server_time_ms": 1777996800000
|
||
}
|
||
```
|
||
|
||
### 等级规则列表
|
||
|
||
```http
|
||
GET /api/v1/levels/tracks/{track}
|
||
```
|
||
|
||
返回指定轨道的等级阈值、等级段、奖励资源组摘要和当前用户进度。客户端只用于展示,不自行判断升级。
|
||
|
||
### 等级奖励记录
|
||
|
||
```http
|
||
GET /api/v1/levels/rewards
|
||
```
|
||
|
||
返回用户等级奖励发放状态,用于展示“待发放/已发放/失败重试中”。首版奖励自动发放,不需要用户领取。
|
||
|
||
## 后台配置
|
||
|
||
后台建议放在活动管理或用户成长管理下:
|
||
|
||
```text
|
||
用户成长
|
||
- 等级轨道
|
||
- 等级规则
|
||
- 等级段奖励
|
||
- 等级奖励记录
|
||
```
|
||
|
||
等级规则页:
|
||
|
||
- 选择轨道:财富/游戏/魅力。
|
||
- 配置每一级 `level` 和 `required_value`。
|
||
- 可选配置该级 `reward_resource_group_id`。
|
||
- active 规则调整阈值或奖励时生成新版本。
|
||
|
||
等级段页:
|
||
|
||
- 选择轨道。
|
||
- 配置 `min_level/max_level`。
|
||
- 配置头像框资源、徽章资源、奖励资源组。
|
||
- 校验同轨道 active 等级段不能重叠。
|
||
- 校验头像框/徽章资源 active、grantable,并且资源类型正确。
|
||
|
||
奖励记录页:
|
||
|
||
- 按用户、轨道、等级、等级段、状态查询。
|
||
- 支持失败 reward job 人工重试。
|
||
- 不支持人工删除用户等级历史。
|
||
|
||
## 事件处理细节
|
||
|
||
### 送礼事件
|
||
|
||
`WalletGiftDebited` 派生两条等级事件:
|
||
|
||
| 轨道 | 用户 | 值 |
|
||
| --- | --- | --- |
|
||
| `wealth` | sender | `coin_spent` |
|
||
| `charm` | target | `received_gift_value`,首版按该 target 对应的礼物结算 coin 价值累计 |
|
||
|
||
如果一次礼物支持多 target,必须按 wallet 实际拆单结果给每个 target 生成独立 charm 事件,不能用客户端目标列表估算。当前单 target 礼物的 `received_gift_value` 等于本次礼物账务结算的 `coin_spent`。
|
||
|
||
### 游戏事件
|
||
|
||
游戏等级只统计成功扣费或已结算的真实消耗:
|
||
|
||
- `game_spend_coin` 只取用户实际消耗的 COIN。
|
||
- 退款、回滚、补单不在首版自动扣减等级。
|
||
- 如果 `game-service` 与 `wallet-service` 都能发布游戏事件,activity-service 只消费钱包游戏扣费 outbox,避免同一游戏订单被双计。`game-service` 事件只作为游戏局详情、风控或对账输入。
|
||
|
||
### 升级计算
|
||
|
||
事件消费事务内流程:
|
||
|
||
1. 插入 `growth_level_value_events`,重复则直接返回。
|
||
2. 锁定 `user_growth_level_accounts` 当前轨道行。
|
||
3. 累加 `total_value`。
|
||
4. 根据 active `growth_level_rules` 计算新等级。
|
||
5. 如果等级或等级段变化,写 `user_growth_level_history`。
|
||
6. 为跨过的具体等级和首次进入的新等级段创建 `growth_level_reward_jobs`。
|
||
7. 更新 `user_level_display_profiles`。
|
||
8. 提交事务。
|
||
|
||
## 补偿与重算
|
||
|
||
- 事件消费者失败:依赖来源 outbox 或 event relay 重试,activity 侧幂等。
|
||
- 奖励发放失败:reward job 重试,不回滚等级。
|
||
- 后台规则改错:创建 `growth_level_rebuild_jobs`,由 `cron-service` 调度 activity-service 批处理重算。
|
||
- 重算必须写操作人、原因、规则版本和影响用户数;不能在查询接口临时扫描全量流水。
|
||
- 所有窗口和时间字段统一使用 UTC epoch milliseconds,字段名保持 `*_ms`。
|
||
|
||
## 落地顺序
|
||
|
||
1. 在 `activity-service` 增加 growth level domain、repository、service、MySQL 表和单元测试。
|
||
2. 在 `api/proto/activity/v1/activity.proto` 增加等级查询和后台配置 RPC,运行 `make proto`。
|
||
3. 在 `wallet-service` 允许资源发放来源 `growth_level`,补充 `GrantResourceGroup` 幂等测试。
|
||
4. 接入 wallet 游戏/礼物 outbox 到 activity growth consumer,先实现财富和魅力,再实现游戏。
|
||
5. 实现 reward worker,调用 `wallet-service.GrantResourceGroup` 发放等级和等级段资源组。
|
||
6. 实现 gateway App 接口和 server/admin 配置页。
|
||
7. 补充 Docker/local initdb schema,运行 `make test`;涉及 proto 时运行 `make proto && go test ./...`。
|
||
|
||
## 验收点
|
||
|
||
- 同一送礼 outbox 重复投递,财富和魅力等级只增加一次。
|
||
- sender 财富和 target 魅力使用同一账务事实拆成不同幂等键。
|
||
- 用户从 10 级升到 11 级时,旧等级段资源仍在背包,新等级段头像框/徽章成为当前等级展示。
|
||
- 用户一次大额送礼跨多级时,账户等级、升级历史、具体等级奖励、等级段奖励都正确。
|
||
- 奖励资源组发放失败后,等级已升级,reward job 可重试且不重复发永久资源。
|
||
- gateway 返回的等级展示资源来自服务端投影,客户端自报资源 ID 不生效。
|