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

18 KiB
Raw Permalink Blame History

等级与成就体系架构

本文定义用户等级、普通成就和活动限时成就的服务边界、事实来源、数据模型、接口和落地顺序。等级/成就属于用户成长读模型,不能把进度判断塞进房间、钱包、用户资料或游戏主链路。

目标

  • 支持三条首版等级轨道:财富、游戏、魅力。
  • 支持普通成就:关注/被关注、送礼/收礼、麦上时长、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-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 送礼人 财富等级、送礼成就
WalletGiftDebitedRoomGiftSent 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_leveltotal_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_idbadge_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-serviceGET /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_definitionsdaily/exclusive 任务,领取金币奖励。
  • user_task_progress:任务页进度。
  • ClaimTaskReward:已完成任务领奖。

等级/成就新增独立模型,但共用同一套事实事件源。不要把永久成就硬塞进 task_definitions,否则会把“任务奖励领取状态”和“公开身份解锁状态”混在一起,后续活动成就、徽章展示和多阶段成就会变得难以维护。

异常与补偿

  • 事件重复:growth_event_consumptiongrowth_point_eventsachievement_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 增加 GrowthServiceAchievementService 和后台管理 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-servicewallet-serviceuser-servicegame-service 主链路不依赖 activity-service 可用性。
  • 所有业务时间和窗口使用 UTC epoch milliseconds。