402 lines
18 KiB
Markdown
402 lines
18 KiB
Markdown
# 等级与成就体系架构
|
||
|
||
本文定义用户等级、普通成就和活动限时成就的服务边界、事实来源、数据模型、接口和落地顺序。等级/成就属于用户成长读模型,不能把进度判断塞进房间、钱包、用户资料或游戏主链路。
|
||
|
||
## 目标
|
||
|
||
- 支持三条首版等级轨道:财富、游戏、魅力。
|
||
- 支持普通成就:关注/被关注、送礼/收礼、麦上时长、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`。
|
||
|
||
## 总体架构
|
||
|
||
```mermaid
|
||
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-service` MySQL,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` | 政策生效窗口 |
|
||
|
||
事件消费时先记录原始指标,再按当时有效政策写入积分流水。政策调整只影响新事件;如果运营要求重算,必须走后台重算任务,不在查询路径临时扫描历史事件。
|
||
|
||
### 数据模型
|
||
|
||
```sql
|
||
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 毫秒区间。
|
||
|
||
### 数据模型
|
||
|
||
```sql
|
||
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:
|
||
|
||
```sql
|
||
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 接口
|
||
|
||
### 成长总览
|
||
|
||
```http
|
||
GET /api/v1/growth/me/overview
|
||
```
|
||
|
||
返回:
|
||
|
||
```json
|
||
{
|
||
"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
|
||
}
|
||
```
|
||
|
||
### 等级详情
|
||
|
||
```http
|
||
GET /api/v1/growth/levels
|
||
```
|
||
|
||
返回当前用户每条轨道、等级阈值、已获得奖励和下一等级差值。客户端只展示服务端返回的状态,不自行计算是否升级。
|
||
|
||
### 成就列表
|
||
|
||
```http
|
||
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`。
|
||
|
||
### 展示身份设置
|
||
|
||
```http
|
||
POST /api/v1/achievements/display
|
||
```
|
||
|
||
请求:
|
||
|
||
```json
|
||
{
|
||
"achievement_ids": ["ach_gift_100k", "ach_mic_100h"],
|
||
"command_id": "cmd_display_550e8400"
|
||
}
|
||
```
|
||
|
||
activity-service 校验这些成就是当前用户已解锁且可展示,再保存展示偏好。房间和资料页只读取投影,不实时扫描成就列表。
|
||
|
||
## 对外展示读模型
|
||
|
||
需要一个轻量投影给资料卡、房间成员列表和我的页使用:
|
||
|
||
```sql
|
||
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`。
|
||
|
||
## 后台管理
|
||
|
||
后台新增在活动管理下:
|
||
|
||
```text
|
||
活动管理
|
||
- 等级配置
|
||
- 成就配置
|
||
```
|
||
|
||
等级配置:
|
||
|
||
- 等级轨道:财富、游戏、魅力的名称、排序、状态、展示配置。
|
||
- 积分政策:指标、转换比例、每日上限、过滤维度、生效时间。
|
||
- 等级规则:等级、阈值、奖励资源组、展示配置。
|
||
|
||
成就配置:
|
||
|
||
- 成就集合:普通成就或活动限时成就,配置 `[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`、游标、锁、重试和审计,不允许查询接口临时重扫全量事件。
|
||
|
||
## 落地顺序
|
||
|
||
1. 在 `activity-service` 增加 growth/achievement domain、MySQL 表、repository、service 和单元测试。
|
||
2. 在 `api/proto/activity/v1/activity.proto` 增加 `GrowthService`、`AchievementService` 和后台管理 RPC,运行 `make proto`。
|
||
3. 在 `wallet-service` 扩展资源发放来源,支持 `grant_source=growth`,并补发放幂等测试。
|
||
4. 建立事实事件 relay:优先接 wallet/user/game outbox,room 事件继续走已有 room outbox consumer,不用 cron 从头扫 outbox。
|
||
5. 实现等级事件消费:积分政策、账户更新、跨级升级、reward job、展示投影。
|
||
6. 实现成就事件消费:条件进度、多条件解锁、活动窗口过滤、reward job、展示设置。
|
||
7. 接入 gateway App API 和 server/admin 配置页。
|
||
8. 补充 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。
|