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

402 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 等级与成就体系架构
本文定义用户等级、普通成就和活动限时成就的服务边界、事实来源、数据模型、接口和落地顺序。等级/成就属于用户成长读模型,不能把进度判断塞进房间、钱包、用户资料或游戏主链路。
## 目标
- 支持三条首版等级轨道:财富、游戏、魅力。
- 支持普通成就:关注/被关注、送礼/收礼、麦上时长、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` MySQLRedis 只可做短期缓存。
- 奖励通过 `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 outboxroom 事件继续走已有 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。