18 KiB
等级体系架构
本文只定义等级体系,不包含成就体系。等级体系用于表达用户在财富、游戏和魅力三条成长轨道上的长期身份,并在达到指定等级或等级段时发放永久资源奖励。
目标
- 财富等级:按用户送出礼物价值累计。
- 游戏等级:按用户游戏消耗累计。
- 魅力等级:按用户收到礼物价值累计。
- 后台可配置每条轨道的等级阈值、等级段展示、等级奖励资源组。
- 每到指定等级或等级段,系统自动发放头像框、徽章、礼物等永久资源。
- 等级段资源支持替换展示:例如 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 可作为房间表现、活动播报或对账补充,但不能替代账务事实作为价值来源。
事件流
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
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
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
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 |
奖励任务表:
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-10 级时首次进入 tier A,系统发放 A 头像框/A 徽章永久权益。
- 用户 11 级进入 tier B,系统发放 B 头像框/B 徽章永久权益。
- 等级展示投影切换为 tier B 的头像框/徽章。
- A 头像框/A 徽章仍在用户背包里,用户资源事实不被删除。
默认展示有两种实现选择,首版建议使用等级投影优先:
| 方案 | 处理 | 适用 |
|---|---|---|
| 等级投影优先 | user_level_display_profiles 保存当前等级段头像框/徽章,资料和房间展示优先读它 |
等级段身份必须强制跟随等级 |
| 自动装备钱包资源 | reward job 成功后调用或扩展 wallet-service 装备逻辑,更新 user_resource_equipment |
产品允许等级奖励直接改变用户当前装备 |
首版推荐“等级投影优先”,原因是 user_resource_equipment 表表示用户主动装备的当前资源,同一资源类型只有一条记录;等级段身份是系统身份展示,和用户手动装备头像框/徽章不是同一个语义。后续如果产品要求用户可以手动隐藏等级段徽章,再在 activity-service 增加展示偏好。
展示投影:
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 接口
我的等级总览
GET /api/v1/levels/me/overview
返回:
{
"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
}
等级规则列表
GET /api/v1/levels/tracks/{track}
返回指定轨道的等级阈值、等级段、奖励资源组摘要和当前用户进度。客户端只用于展示,不自行判断升级。
等级奖励记录
GET /api/v1/levels/rewards
返回用户等级奖励发放状态,用于展示“待发放/已发放/失败重试中”。首版奖励自动发放,不需要用户领取。
后台配置
后台建议放在活动管理或用户成长管理下:
用户成长
- 等级轨道
- 等级规则
- 等级段奖励
- 等级奖励记录
等级规则页:
- 选择轨道:财富/游戏/魅力。
- 配置每一级
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事件只作为游戏局详情、风控或对账输入。
升级计算
事件消费事务内流程:
- 插入
growth_level_value_events,重复则直接返回。 - 锁定
user_growth_level_accounts当前轨道行。 - 累加
total_value。 - 根据 active
growth_level_rules计算新等级。 - 如果等级或等级段变化,写
user_growth_level_history。 - 为跨过的具体等级和首次进入的新等级段创建
growth_level_reward_jobs。 - 更新
user_level_display_profiles。 - 提交事务。
补偿与重算
- 事件消费者失败:依赖来源 outbox 或 event relay 重试,activity 侧幂等。
- 奖励发放失败:reward job 重试,不回滚等级。
- 后台规则改错:创建
growth_level_rebuild_jobs,由cron-service调度 activity-service 批处理重算。 - 重算必须写操作人、原因、规则版本和影响用户数;不能在查询接口临时扫描全量流水。
- 所有窗口和时间字段统一使用 UTC epoch milliseconds,字段名保持
*_ms。
落地顺序
- 在
activity-service增加 growth level domain、repository、service、MySQL 表和单元测试。 - 在
api/proto/activity/v1/activity.proto增加等级查询和后台配置 RPC,运行make proto。 - 在
wallet-service允许资源发放来源growth_level,补充GrantResourceGroup幂等测试。 - 接入 wallet 游戏/礼物 outbox 到 activity growth consumer,先实现财富和魅力,再实现游戏。
- 实现 reward worker,调用
wallet-service.GrantResourceGroup发放等级和等级段资源组。 - 实现 gateway App 接口和 server/admin 配置页。
- 补充 Docker/local initdb schema,运行
make test;涉及 proto 时运行make proto && go test ./...。
验收点
- 同一送礼 outbox 重复投递,财富和魅力等级只增加一次。
- sender 财富和 target 魅力使用同一账务事实拆成不同幂等键。
- 用户从 10 级升到 11 级时,旧等级段资源仍在背包,新等级段头像框/徽章成为当前等级展示。
- 用户一次大额送礼跨多级时,账户等级、升级历史、具体等级奖励、等级段奖励都正确。
- 奖励资源组发放失败后,等级已升级,reward job 可重试且不重复发永久资源。
- gateway 返回的等级展示资源来自服务端投影,客户端自报资源 ID 不生效。