hyapp-server/docs/等级体系架构.md
2026-05-14 17:44:16 +08:00

18 KiB
Raw Permalink Blame History

等级体系架构

本文只定义等级体系,不包含成就体系。等级体系用于表达用户在财富、游戏和魅力三条成长轨道上的长期身份,并在达到指定等级或等级段时发放永久资源奖励。

目标

  • 财富等级:按用户送出礼物价值累计。
  • 游戏等级:按用户游戏消耗累计。
  • 魅力等级:按用户收到礼物价值累计。
  • 后台可配置每条轨道的等级阈值、等级段展示、等级奖励资源组。
  • 每到指定等级或等级段,系统自动发放头像框、徽章、礼物等永久资源。
  • 等级段资源支持替换展示:例如 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-serviceWalletGiftDebited,取 sender 侧实际消耗 coin
游戏等级 game 游戏消耗 wallet-service 的游戏扣费成功 outbox取用户实际消耗 coin
魅力等级 charm 收到礼物价值 wallet-serviceWalletGiftDebited,取 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.GrantResourceGroupgrant_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 增加展示偏好。

展示投影:

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

返回用户等级奖励发放状态,用于展示“待发放/已发放/失败重试中”。首版奖励自动发放,不需要用户领取。

后台配置

后台建议放在活动管理或用户成长管理下:

用户成长
  - 等级轨道
  - 等级规则
  - 等级段奖励
  - 等级奖励记录

等级规则页:

  • 选择轨道:财富/游戏/魅力。
  • 配置每一级 levelrequired_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-servicewallet-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 不生效。