18 KiB
等级与成就体系架构
本文定义用户等级、普通成就和活动限时成就的服务边界、事实来源、数据模型、接口和落地顺序。等级/成就属于用户成长读模型,不能把进度判断塞进房间、钱包、用户资料或游戏主链路。
目标
- 支持三条首版等级轨道:财富、游戏、魅力。
- 支持普通成就:关注/被关注、送礼/收礼、麦上时长、App 活跃天数等长期身份象征。
- 支持活动限时成就:锁定活动、抽奖、特定游戏、特定礼物、指定时间窗内的专属成就。
- 达到等级或解锁成就后,可以发放头像框、徽章、座驾、称号等资源组奖励。
- App 可以读取自己的成长总览、等级详情、成就列表和对外展示身份标识。
- 后台可以配置等级阈值、积分政策、成就条件、活动时间窗和奖励资源组。
非目标
- 不让客户端上报等级经验或成就进度。
- 不把等级体系和 VIP 体系合并。VIP 仍是
wallet-service的付费会员权益;等级是由事实事件派生的成长身份。 - 不在
room-service.SendGift、上麦、关注、游戏改账等主链路里同步调用成长系统。 - 首版不做扣回降级。退款、撤销、关系解除等后续事实进入风控和补偿,不直接回滚已解锁身份。
- 首版不做历史兼容;当前开发阶段可以按最新事实直接调整 proto、表结构和配置。
服务边界
| 模块 | 责任 | 不负责 |
|---|---|---|
activity-service |
拥有等级账户、等级配置、成就定义、成就进度、解锁事实、奖励发放状态和事件幂等 | 不拥有钱包余额、房间状态、用户资料和游戏订单事实 |
wallet-service |
发放等级/成就奖励资源组,保持资源权益和钱包资产入账一致 | 不判断用户是否达到等级或成就 |
user-service |
拥有用户资料、社交关系、麦上时长、登录审计等基础事实,并通过 outbox 暴露事实 | 不计算等级/成就 |
room-service |
继续只拥有 Room Cell、房间状态、麦位、送礼房间表现和 room outbox | 不保存成长进度 |
game-service |
拥有游戏订单和游戏结算事实;金币改账继续走 wallet-service |
不保存成长进度 |
gateway-service |
暴露 App HTTP API,鉴权、生成 request_id、透传到 activity gRPC |
不查成长数据库 |
server/admin |
后台配置页面和审计,通过 hyapp.local/api 访问 activity/wallet |
不 import app 服务内部包 |
首版建议在 activity-service 内新增 growth module,而不是新建服务。原因是现有每日任务已经在 activity-service 内实现了“事实事件 -> 用户进度 -> 奖励领取”的主模式,成就和等级可以复用事件消费、后台配置、inbox 和活动能力。后续只有当成长系统吞吐、团队边界或部署频率独立膨胀时,再拆 growth-service。
总体架构
flowchart LR
Room["room-service outbox"] --> EventBus["durable event relay"]
Wallet["wallet-service outbox"] --> EventBus
User["user-service user_outbox"] --> EventBus
Game["game-service outbox"] --> EventBus
EventBus --> GrowthConsumer["activity-service growth consumer"]
GrowthConsumer --> GrowthDB[("activity MySQL growth_* / achievement_*")]
GrowthConsumer --> GrowthOutbox["activity outbox / reward jobs"]
GrowthOutbox --> WalletGrant["wallet-service GrantResourceGroup"]
GrowthOutbox --> Inbox["activity inbox optional unlock notice"]
App["App"] --> Gateway["gateway-service /api/v1"]
Gateway --> GrowthQuery["activity-service GrowthService"]
Admin["server/admin"] --> AdminGrowth["activity-service AdminGrowthService"]
核心原则:
- 等级和成就只消费已经提交的服务端事实事件。
- 每个事实事件必须有稳定
event_id;每个派生指标也必须有稳定幂等键。 - 成长状态保存在
activity-serviceMySQL,Redis 只可做短期缓存。 - 奖励通过
wallet-service.GrantResourceGroup发放。资源组可以包含装扮权益,也可以包含金币类 wallet asset item。 - 自动奖励走 activity 侧 reward job/outbox,不阻塞事件消费主事务。
事实事件模型
对外不要让每个来源服务理解等级和成就规则。来源服务只发布事实,activity-service 把事实映射为成长指标。
统一内部事件结构:
| 字段 | 说明 |
|---|---|
event_id |
来源事实事件 ID;派生多条指标时追加 :metric:{metric_type}:{role} |
source_service |
wallet / user / room / game |
event_type |
来源事件类型,例如 WalletGiftDebited |
user_id |
本条指标归属用户 |
metric_type |
成长指标,例如 gift_spend_coin |
value |
正向增量,单位由指标定义决定 |
occurred_at_ms |
来源事实发生时间,统一 UTC epoch milliseconds |
dimensions_json |
可选过滤维度,例如 gift_id/game_id/room_id/region_id/activity_id |
首版指标映射:
| 事实来源 | 派生指标 | 用户归属 | 用途 |
|---|---|---|---|
WalletGiftDebited |
gift_spend_coin |
送礼人 | 财富等级、送礼成就 |
WalletGiftDebited 或 RoomGiftSent |
gift_received_value |
收礼人 | 魅力等级、收礼成就 |
WalletRechargeRecorded |
recharge_coin |
充值用户 | 财富等级、充值成就 |
ApplyGameCoinChange 成功账务事件 |
game_spend_coin / game_win_coin |
游戏用户 | 游戏等级、游戏成就 |
UserMicSessionClosed |
mic_online_ms |
上麦用户 | 魅力等级、麦上成就 |
UserFollowed |
follow_count / followed_count |
关注人/被关注人 | 普通成就 |
UserDailyActive |
active_day_count |
活跃用户 | App 天数成就 |
如果一个来源事件会给多个人增加进度,必须拆成多条派生指标并使用不同幂等键。例如送礼同时影响送礼人的财富等级和收礼人的魅力等级。
等级体系
首版等级轨道:
| 轨道 | 主要指标 | 产品含义 |
|---|---|---|
wealth 财富 |
充值、送礼消费、活动消费 | 付费贡献和消费身份 |
game 游戏 |
游戏金币消耗、有效局数、中奖额 | 游戏参与和游戏贡献 |
charm 魅力 |
收礼价值、麦上在线、被关注 | 被认可、被互动和内容活跃 |
积分政策
等级积分不要写死在事件消费者里。后台配置把指标转换为某条轨道的积分:
| 字段 | 说明 |
|---|---|
track |
wealth / game / charm |
metric_type |
输入指标 |
point_numerator / point_denominator |
转换比例,避免浮点数 |
daily_cap |
可选每日上限,0 表示不限 |
dimension_filter_json |
可选过滤条件,例如只统计指定礼物、指定游戏 |
effective_from_ms / effective_to_ms |
政策生效窗口 |
事件消费时先记录原始指标,再按当时有效政策写入积分流水。政策调整只影响新事件;如果运营要求重算,必须走后台重算任务,不在查询路径临时扫描历史事件。
数据模型
growth_level_tracks(
app_code, track, name, status, sort_order,
display_config_json, created_at_ms, updated_at_ms
);
growth_point_policies(
app_code, policy_id, track, metric_type, status,
point_numerator, point_denominator, daily_cap,
dimension_filter_json, effective_from_ms, effective_to_ms,
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
);
growth_level_rules(
app_code, track, level, name, required_points, reward_resource_group_id,
display_config_json, status, sort_order, created_at_ms, updated_at_ms
);
user_growth_accounts(
app_code, user_id, track, total_points, current_level,
level_started_at_ms, level_updated_at_ms, version, updated_at_ms,
PRIMARY KEY(app_code, user_id, track)
);
growth_point_events(
app_code, point_event_id, source_event_id, source_service, source_event_type,
user_id, track, metric_type, raw_value, point_delta, policy_id,
occurred_at_ms, created_at_ms,
PRIMARY KEY(app_code, point_event_id)
);
user_growth_level_history(
app_code, history_id, user_id, track, previous_level, new_level,
total_points, point_event_id, reward_job_id, created_at_ms
);
等级升级规则:
current_level由total_points命中growth_level_rules.required_points计算。- 同一事件必须在一个 activity MySQL 事务内完成幂等记录、积分流水、账户更新和升级历史。
- 允许一次事件跨多级升级;每个新等级可以生成一条 reward job。
- 首版不降级。政策重算如果导致等级下降,必须通过后台批处理显式执行并写审计。
成就体系
成就不是每日任务的简单改名。现有 task_definitions 适合 daily/exclusive 任务和金币领取;成就需要公开身份展示、多阶段解锁、活动时间窗、资源奖励和可佩戴标识,因此建议独立建模。
成就分类:
| 类型 | 周期 | 示例 | 处理 |
|---|---|---|---|
ordinary 普通成就 |
lifetime | 关注 100 人、收到 10 万礼物价值、麦上 100 小时、活跃 30 天 | 永久解锁,可公开展示 |
activity_limited 活动成就 |
[start_ms,end_ms) |
指定活动内抽奖 10 次、发送指定礼物、完成某游戏挑战 | 窗口内累计,窗口外不再增长 |
成就条件
首版支持两类条件:
- 单指标阈值:
metric_type + target_value + optional dimension_filter_json。 - 多条件 AND:每个条件独立累计,全部达到后解锁。
不支持在客户端表达条件,不支持按本地时区切日。活动窗口统一使用 [start_ms, end_ms) UTC 毫秒区间。
数据模型
achievement_collections(
app_code, collection_id, collection_type, title, status,
effective_from_ms, effective_to_ms, sort_order, display_config_json,
created_at_ms, updated_at_ms
);
achievement_definitions(
app_code, achievement_id, collection_id, category, title, description,
status, version, unlock_mode, reward_resource_group_id,
display_resource_id, badge_resource_id, effective_from_ms, effective_to_ms,
sort_order, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
);
achievement_conditions(
app_code, condition_id, achievement_id, version,
metric_type, target_value, target_unit, dimension_filter_json,
created_at_ms
);
user_achievement_progress(
app_code, user_id, achievement_id, condition_id, cycle_key,
progress_value, target_value, status, updated_at_ms,
PRIMARY KEY(app_code, user_id, achievement_id, condition_id, cycle_key)
);
user_achievement_unlocks(
app_code, user_id, achievement_id, cycle_key, version,
status, unlocked_at_ms, reward_job_id, displayed, updated_at_ms,
PRIMARY KEY(app_code, user_id, achievement_id, cycle_key)
);
achievement_event_consumption(
app_code, event_id, source_service, source_event_type, user_id,
status, skip_reason, consumed_at_ms, created_at_ms,
PRIMARY KEY(app_code, event_id)
);
cycle_key 对普通成就固定为 lifetime。活动限时成就使用活动集合 ID 或 activity:{collection_id},不能使用本地日期。
解锁与奖励
- 自动解锁:事件消费发现条件全部满足时写
user_achievement_unlocks。 - 奖励发放:如果配置了
reward_resource_group_id,写growth_reward_jobs,异步调用wallet-service.GrantResourceGroup。 - 展示身份:
display_resource_id或badge_resource_id只表示成就身份标识;真正的资源权益仍以 wallet resource entitlement 为准。 - 活动成就如果奖励限时资源,资源组 item 使用
duration_ms控制有效期;成就解锁事实本身可以永久保留。
奖励发放
等级和成就都使用同一张奖励任务表,避免事件消费事务跨服务调用 wallet:
growth_reward_jobs(
app_code, reward_job_id, reward_type, source_id, user_id,
resource_group_id, wallet_command_id, wallet_grant_id,
status, attempt_count, next_retry_at_ms, failure_reason,
created_at_ms, updated_at_ms,
PRIMARY KEY(app_code, reward_job_id),
UNIQUE KEY uk_growth_reward_source(app_code, reward_type, source_id, user_id)
);
发放规则:
wallet_command_id必须稳定,例如growth_reward:{reward_job_id}。wallet-service需要允许grant_source=growth,用于区分后台发放和成长系统自动发放。- 钱包发放成功后回写
wallet_grant_id,重复执行必须返回同一结果。 - 发放失败不回滚等级/成就解锁,依靠 reward job 重试和后台排障。
App 接口
成长总览
GET /api/v1/growth/me/overview
返回:
{
"tracks": [
{
"track": "wealth",
"level": 8,
"total_points": 125000,
"current_level_points": 100000,
"next_level_points": 150000,
"reward_claim_status": "granted"
}
],
"achievement_summary": {
"unlocked_count": 12,
"displayed_achievement_ids": ["ach_gift_100k"]
},
"server_time_ms": 1777996800000
}
等级详情
GET /api/v1/growth/levels
返回当前用户每条轨道、等级阈值、已获得奖励和下一等级差值。客户端只展示服务端返回的状态,不自行计算是否升级。
成就列表
GET /api/v1/achievements?collection_type=ordinary
GET /api/v1/achievements?collection_id=act_202605
每项返回 achievement_id/title/category/progress/target/status/unlocked_at_ms/reward_status/displayed/effective_to_ms。
展示身份设置
POST /api/v1/achievements/display
请求:
{
"achievement_ids": ["ach_gift_100k", "ach_mic_100h"],
"command_id": "cmd_display_550e8400"
}
activity-service 校验这些成就是当前用户已解锁且可展示,再保存展示偏好。房间和资料页只读取投影,不实时扫描成就列表。
对外展示读模型
需要一个轻量投影给资料卡、房间成员列表和我的页使用:
user_growth_display_profiles(
app_code, user_id,
wealth_level, game_level, charm_level,
displayed_achievement_ids_json,
displayed_badge_resource_ids_json,
updated_at_ms,
PRIMARY KEY(app_code, user_id)
);
gateway-service 的 GET /api/v1/users/me/overview 当前已经有 level 入口,但没有成长摘要。首版可以保持入口不变,在等级页进入后调用成长接口;如果设计稿需要首屏直接展示等级,则 gateway 并行调用 activity-service 的轻量 summary,并允许降级为 unavailable=true。
后台管理
后台新增在活动管理下:
活动管理
- 等级配置
- 成就配置
等级配置:
- 等级轨道:财富、游戏、魅力的名称、排序、状态、展示配置。
- 积分政策:指标、转换比例、每日上限、过滤维度、生效时间。
- 等级规则:等级、阈值、奖励资源组、展示配置。
成就配置:
- 成就集合:普通成就或活动限时成就,配置
[start_ms,end_ms)。 - 成就定义:标题、分类、展示资源、奖励资源组、排序、状态。
- 成就条件:单指标或多条件 AND,条件必须由服务端支持的
metric_type组成。
后台启用 active 配置时只影响新事件;需要追溯时创建 growth_rebuild_jobs,由 cron-service 触发 activity-service 批处理。cron 只负责调度,重算逻辑、幂等和状态推进仍由 activity-service owner 完成。
与现有每日任务的关系
已有每日任务表和接口继续保留:
task_definitions:daily/exclusive 任务,领取金币奖励。user_task_progress:任务页进度。ClaimTaskReward:已完成任务领奖。
等级/成就新增独立模型,但共用同一套事实事件源。不要把永久成就硬塞进 task_definitions,否则会把“任务奖励领取状态”和“公开身份解锁状态”混在一起,后续活动成就、徽章展示和多阶段成就会变得难以维护。
异常与补偿
- 事件重复:
growth_event_consumption、growth_point_events、achievement_event_consumption都以派生事件 ID 做幂等。 - 事件乱序:等级按正向累计处理;成就只判断达到阈值,不依赖事件顺序。
- 钱包奖励失败:只标记 reward job failed/retryable,不回滚等级或成就。
- 来源 outbox 延迟:成长进度允许短暂延迟,App 展示服务端当前事实。
- 活动窗口边界:只统计
occurred_at_ms >= start_ms && occurred_at_ms < end_ms的事件。 - 重算任务:必须有
rebuild_job_id、游标、锁、重试和审计,不允许查询接口临时重扫全量事件。
落地顺序
- 在
activity-service增加 growth/achievement domain、MySQL 表、repository、service 和单元测试。 - 在
api/proto/activity/v1/activity.proto增加GrowthService、AchievementService和后台管理 RPC,运行make proto。 - 在
wallet-service扩展资源发放来源,支持grant_source=growth,并补发放幂等测试。 - 建立事实事件 relay:优先接 wallet/user/game outbox,room 事件继续走已有 room outbox consumer,不用 cron 从头扫 outbox。
- 实现等级事件消费:积分政策、账户更新、跨级升级、reward job、展示投影。
- 实现成就事件消费:条件进度、多条件解锁、活动窗口过滤、reward job、展示设置。
- 接入 gateway App API 和 server/admin 配置页。
- 补充 Docker 配置和本地 schema,运行
make test;涉及 proto 时额外运行make proto && go test ./...。
验收点
- 重复投递同一送礼事件,财富等级、魅力等级和成就进度都只增加一次。
- 一个大额送礼事件跨多个财富等级时,等级历史和奖励任务数量正确。
- 活动成就只统计
[start_ms,end_ms)内的事件,结束边界毫秒不计入。 - 奖励资源组发放失败后,等级/成就已解锁,reward job 可重试并幂等。
room-service、wallet-service、user-service和game-service主链路不依赖 activity-service 可用性。- 所有业务时间和窗口使用 UTC epoch milliseconds。