hyapp-server/docs/成就和徽章体系架构.md
2026-05-14 17:44:16 +08:00

24 KiB
Raw Permalink Blame History

成就和徽章体系架构

本文单独定义成就、徽章、长徽章、大徽章、系统赠送徽章和资料页展示的产品/后端边界。等级体系、VIP 体系和资源组体系已有独立文档,本文只说明它们和徽章展示的衔接方式。

目标

  • 成就可以由服务端任务完成、事实指标达标或活动条件达成后自动解锁。
  • 成就解锁后可以发放徽章,也可以只记录成就不发徽章。
  • 系统可以直接赠送徽章,不要求生成一个伪造成就。
  • 徽章分为长徽章和大徽章:
    • 长徽章用于身份、等级、VIP、机构、职业、官方认证等横向标识。
    • 大徽章用于成就、活动、荣誉墙等方形或近方形展示。
  • App 可以在资料页顶部展示多个长徽章,在 Honor/荣誉区域展示多个大徽章。
  • 房间资料、麦位、公屏和用户主页都只能读取服务端返回的徽章投影,客户端不能自报徽章。
  • 后台可以配置成就条件、成就奖励徽章、徽章样式、徽章有效期、系统赠送和展示优先级。

非目标

  • 不把成就体系和每日任务体系合并。每日任务是短周期任务和领奖;成就是长期或活动荣誉。
  • 不把徽章配置写散到用户资料表。徽章仍然是 wallet-service 的资源权益。
  • 不允许客户端提交“我完成了某成就”或“我拥有某徽章”。
  • 不让 room-servicewallet-serviceuser-servicegame-service 在主链路同步判断成就。
  • 首版不做徽章交易、徽章合成、徽章回收兑换。

核心概念

概念 含义 Owner
成就 achievement 一套服务端条件和解锁事实,例如完成指定任务、送礼达到 10 万、活动内抽奖 10 次 activity-service
徽章资源 badge resource 用户可以持有、过期、展示或佩戴的资源,resources.resource_type=badge wallet-service
长徽章 strip badge 横向胶囊/铭牌样式用于等级、VIP、身份、机构、认证等 wallet-service 配置,来源状态由对应服务提供
大徽章 tile badge 方形或近方形荣誉图标,用于成就、活动赠送、系统荣誉 wallet-service 配置,成就来源由 activity-service 触发
徽章持有 entitlement 用户已经获得某个徽章资源,可能永久,也可能限时 wallet-service
徽章展示 display slot 用户资料页、荣誉墙、房间资料等位置上展示哪些徽章和排序 wallet-service 或展示投影

关键关系:

  • 成就解锁不等于徽章持有。成就是荣誉事实,徽章是资源权益。
  • 徽章持有不等于正在展示。用户可以拥有很多大徽章,但资料页只展示前几个。
  • 系统赠送徽章不等于成就。运营活动可以直接发一个大徽章,不必污染成就列表。
  • 长徽章优先表达“当前身份状态”,大徽章优先表达“历史荣誉沉淀”。

徽章分类

按获取来源

来源 source_type 示例 是否生成成就
成就奖励 achievement 完成送礼达人成就后获得方形徽章
系统赠送 system_grant 运营活动、节日、补偿、人工授予徽章
等级身份 level 财富 M16、游戏 53、魅力 18 不一定,通常由等级投影计算
VIP 身份 vip VIP1/VIP7 长徽章 否,由 VIP 状态决定
官方身份 identity 主播、公会、代理、币商、官方认证 否,由对应身份系统决定
关系身份 relationship CP、守护、家族关系等 可选,通常由关系事实决定

按展示形态

形态 badge_form 展示位置 设计规则
长徽章 strip 昵称下方、资料卡顶部、房间小卡、麦位名称旁 横向,图标 + 短文本或等级数字,优先级排序,数量受限
大徽章 tile Honor 页、荣誉墙、资料页精选荣誉 方形/近方形,强调图形资产,可被用户精选展示

长徽章不建议放进 Honor 网格里堆叠;大徽章不建议塞到昵称行里,否则会挤压昵称、短号和国家标识。

总体架构

flowchart LR
  Task["activity task completed"] --> AchievementConsumer["activity achievement consumer"]
  WalletFact["wallet/user/room/game facts"] --> AchievementConsumer
  AchievementConsumer --> AchievementDB[("activity achievement_*")]
  AchievementDB --> RewardJob["achievement reward job"]
  RewardJob --> WalletGrant["wallet-service GrantResourceGroup"]

  Admin["server/admin"] --> SystemGrant["wallet-service GrantResource/Group"]
  WalletGrant --> BadgeEntitlement[("user_resource_entitlements")]
  SystemGrant --> BadgeEntitlement

  BadgeEntitlement --> BadgeDisplay["badge display slots / projection"]
  Level["activity level read model"] --> BadgeDisplay
  VIP["wallet VIP state"] --> BadgeDisplay
  Identity["user/host/agency identity"] --> BadgeDisplay

  App["App profile / Honor"] --> Gateway["gateway-service"]
  Gateway --> BadgeDisplay

处理原则:

  • 成就只消费服务端事实事件或服务端任务完成事件。
  • 徽章发放统一走 wallet-service 资源或资源组发放能力。
  • 长徽章里的等级/VIP/身份可以是“计算型展示”,不一定要求每次状态变化都发一个新资源。
  • 大徽章必须有明确的徽章资源和持有记录,方便 Honor 页展示、过期过滤和后台审计。
  • 高频房间资料展示需要轻量投影,不能实时扫用户全部资源和全部成就。

成就设计

成就分两类:

类型 achievement_type 示例 周期
普通成就 ordinary 完成 10 个每日任务、累计送礼 10 万、累计上麦 100 小时 lifetime
活动成就 activity_limited 活动期内完成指定游戏挑战、送指定礼物、登录满 7 天 activity:{collection_id}

成就触发来源:

触发来源 metric_type 示例 说明
每日/专属任务完成 task_completed_countspecific_task_completed task module 写出 TaskCompleted 服务端事件,成就系统消费
送礼/收礼 gift_spend_coingift_received_value 以 wallet 账务事实为准
游戏 game_spend_coingame_win_coingame_round_count 以游戏结算或钱包扣费事实为准
关系/社交 follow_countfollowed_countrelationship_days 以 user-service 关系事实为准
活跃 active_day_countconsecutive_active_days 以服务端活跃事实为准

成就解锁规则

  • 单条件成就:一个指标达到目标值后解锁。
  • 多条件成就:多个条件全部达到后解锁,首版只支持 AND。
  • 活动成就只统计 occurred_at_ms >= start_ms && occurred_at_ms < end_ms 的事件。
  • 普通成就 cycle_key=lifetime;活动成就 cycle_key=activity:{collection_id}
  • 退款、撤销、解除关系等首版不自动回滚已解锁成就,后续通过风控或后台补偿处理。

成就奖励徽章

成就定义里不直接写图片 URL只绑定资源组或主徽章资源

字段 说明
primary_badge_resource_id 该成就主展示徽章,通常是大徽章
reward_resource_group_id 解锁后发放的资源组,可以包含徽章、头像框、金币等
auto_pin_policy 是否自动放入资料页精选展示,例如 none / if_empty / always
honor_visibility 是否进入 Honor 列表,默认进入

如果 reward_resource_group_id 内包含大徽章,必须保证对应资源的 badge_form=tile。如果成就只用于记录不发徽章,primary_badge_resource_id=0reward_resource_group_id=0

系统赠送徽章

系统赠送用于运营或后台直接授予徽章:

场景 推荐方式
活动参与奖 后台批量赠送资源组,grant_source=activity_campaign
官方认证 授予长徽章,grant_source=identity_verify,通常限时或直到身份取消
补偿/人工荣誉 后台赠送大徽章,grant_source=admin
节日限定 赠送限时大徽章,资源组 item 配置 duration_ms

系统赠送只写资源授予事实,不写 user_achievement_unlocks。如果产品希望用户在 Honor 页看到“活动赠送”的记录,可以从 wallet 的 ResourceGroupGranted/UserResourceChanged 事件生成 user_badge_grants 投影,来源标记为 system_grant

长徽章设计

长徽章用于资料头部的身份行,类似截图中昵称下方的 M16、等级数字、身份标识、VIP 等。

长徽章来源

类型 示例 展示来源
等级长徽章 财富 M16、游戏 53、魅力 18 activity-service 等级摘要
VIP 长徽章 VIP1/VIP7 wallet-service VIP 当前状态
身份长徽章 主播、代理、币商、官方、家族 对应身份系统或后台授予
关系长徽章 CP 天数、守护关系 user-service 关系事实

长徽章有两种实现方式:

方式 使用场景 说明
计算型长徽章 等级、VIP 这类随状态变化的标识 不需要每级发一个资源,查询时根据状态和配置生成展示项
资源型长徽章 官方认证、代理、币商等运营授予标识 通过 badge 资源发放,有有效期和审计

等级和 VIP 不建议每次升级都发一个新长徽章资源,否则会产生大量过期或废弃权益。更稳妥的做法是保存等级/VIP 状态,按 badge_display_specs 的模板渲染,例如 M{level}VIP{level}

展示规则

  • 长徽章展示槽位建议为 profile_strip
  • 默认最多展示 6 个,超过数量按 priority 和业务权威排序截断。
  • 推荐排序:官方/身份认证 > VIP > 等级 > 关系 > 活动临时身份。
  • 长徽章文字必须短,避免昵称区域换行挤压。
  • 可过期长徽章在 expires_at_ms <= now 后自动从展示投影移除。
  • 用户不应能手动伪造等级/VIP/认证长徽章,只能在允许的资源型长徽章中选择是否展示。

大徽章设计

大徽章用于 Honor 页和资料页精选荣誉,类似截图中一排方形金色徽章。

大徽章来源

类型 示例 获取
成就大徽章 送礼达人、麦上之星、连续活跃 30 天 成就解锁后发放
活动大徽章 生日活动、节日活动、游戏赛季 活动成就或系统赠送
官方大徽章 官方精选、赛事冠军、年度荣誉 系统赠送

展示规则

  • 大徽章展示槽位建议为 profile_tilehonor_wall
  • Honor 页展示用户拥有的全部有效大徽章,按获得时间倒序或后台排序展示。
  • 资料页顶部精选只展示 3 到 6 个,用户可以手动置顶;未设置时按稀有度、优先级、获得时间自动选择。
  • 已过期的大徽章默认不展示;如果产品要展示历史已过期活动徽章,需要给资源配置 historical_visible=true,但不能再作为有效权益使用。
  • 大徽章点击后可以进入详情页,展示来源、获得时间、有效期、对应成就进度或系统赠送原因。

数据模型

钱包资源侧

徽章本体继续复用 resources

resources(
  app_code, resource_id, resource_code, resource_type,
  name, status, grantable, grant_strategy,
  asset_url, preview_url, animation_url, metadata_json,
  created_at_ms, updated_at_ms
);

约束:

  • 徽章资源必须 resource_type=badge
  • 大徽章推荐 grant_strategy=set_active_flag,永久持有。
  • 限时活动徽章可以使用 expires_at_ms 控制有效期。
  • 徽章图片、动画和客户端样式从资源库读取,不允许成就配置直接填散落 URL。

建议新增徽章展示规格表:

badge_display_specs(
  app_code VARCHAR(32) NOT NULL,
  resource_id BIGINT NOT NULL,
  badge_form VARCHAR(32) NOT NULL,          -- strip / tile
  badge_kind VARCHAR(32) NOT NULL,          -- level / vip / achievement / system / identity
  default_slot VARCHAR(32) NOT NULL,        -- profile_strip / profile_tile / honor_wall
  display_name VARCHAR(128) NOT NULL,
  priority INT NOT NULL DEFAULT 0,
  rarity VARCHAR(32) NOT NULL DEFAULT '',
  text_template VARCHAR(128) NOT NULL DEFAULT '',
  display_config_json JSON NULL,
  status VARCHAR(32) NOT NULL,
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY(app_code, resource_id),
  KEY idx_badge_specs_form(app_code, badge_form, status, priority)
);

如果首版不想加表,可以先把这些字段放进 resources.metadata_json;但后台要按长徽章/大徽章筛选、排序和校验时,独立表更清晰。

成就侧

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,
  achievement_type, title, description,
  status, version,
  primary_badge_resource_id,
  reward_resource_group_id,
  auto_pin_policy,
  honor_visibility,
  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,
  created_at_ms, updated_at_ms,
  PRIMARY KEY(app_code, user_id, achievement_id, cycle_key)
);

徽章展示槽位

现有 user_resource_equipmentresource_type=badge 只能保存一个装备项,无法满足多个长徽章和多个大徽章同时展示。建议不要直接复用它做徽章墙,而是新增多槽位表:

user_badge_display_slots(
  app_code VARCHAR(32) NOT NULL,
  user_id BIGINT NOT NULL,
  slot VARCHAR(32) NOT NULL,                -- profile_strip / profile_tile / honor_wall / room_card
  position INT NOT NULL,
  badge_form VARCHAR(32) NOT NULL,          -- strip / tile
  resource_id BIGINT NOT NULL,
  entitlement_id VARCHAR(96) NOT NULL DEFAULT '',
  source_type VARCHAR(32) NOT NULL,         -- achievement / system_grant / level / vip / identity
  source_id VARCHAR(128) NOT NULL DEFAULT '',
  pin_mode VARCHAR(32) NOT NULL DEFAULT '', -- auto / user / system
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY(app_code, user_id, slot, position),
  KEY idx_badge_display_user(app_code, user_id, slot, updated_at_ms),
  KEY idx_badge_display_resource(app_code, resource_id)
);

校验:

  • profile_strip 只允许 badge_form=strip
  • profile_tilehonor_wall 只允许 badge_form=tile
  • 资源型徽章必须校验用户持有有效 user_resource_entitlements
  • 计算型长徽章可以 entitlement_id='',但必须能从等级/VIP/身份状态验证。
  • 同一槽位同一 resource_id 不应重复展示。

资料展示投影

高频资料展示建议维护轻量投影:

user_badge_display_profiles(
  app_code VARCHAR(32) NOT NULL,
  user_id BIGINT NOT NULL,
  strip_badges_json JSON NOT NULL,
  tile_badges_json JSON NOT NULL,
  honor_summary_json JSON NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY(app_code, user_id)
);

投影内容只放展示所需字段,例如 resource_id/name/badge_form/source_type/asset_url/text/level/expires_at_ms/priority。完整详情仍回源到成就和资源接口。

发放流程

成就获得大徽章

sequenceDiagram
  autonumber
  participant Source as Task/Wallet/User/Game Fact
  participant Act as activity-service
  participant DB as activity MySQL
  participant Wallet as wallet-service
  participant App as App

  Source->>Act: TaskCompleted or metric event
  Act->>DB: update achievement progress idempotently
  Act->>DB: insert user_achievement_unlocks
  Act->>DB: insert reward job
  Act->>Wallet: GrantResourceGroup(command_id)
  Wallet->>Wallet: create badge entitlement
  Wallet-->>Act: grant_id
  Act->>DB: mark reward granted
  App->>Act: query achievements / badges

幂等键建议:

  • 成就消费:source_event_id:achievement:{achievement_id}:{user_id}:{cycle_key}
  • 奖励发放:achievement_reward:{user_id}:{achievement_id}:{cycle_key}
  • 钱包命令:wallet_grant:{reward_job_id}

系统直接赠送徽章

sequenceDiagram
  autonumber
  participant Admin as server/admin
  participant Wallet as wallet-service
  participant Projection as badge projection

  Admin->>Wallet: GrantResource/GrantResourceGroup(command_id, badge resource)
  Wallet->>Wallet: write resource_grants and user_resource_entitlements
  Wallet->>Projection: UserResourceChanged outbox
  Projection->>Projection: update user_badge_display_profiles

系统赠送必须保留 grant_source、操作人、原因、目标用户、有效期和资源快照,方便客服和审计。

长徽章刷新

长徽章不一定经过资源发放:

flowchart LR
  LevelChanged["GrowthLevelChanged"] --> Projection["badge display projection"]
  VipChanged["VipMembershipChanged"] --> Projection
  IdentityChanged["Host/Agency/Verify Changed"] --> Projection
  Projection --> Profile["profile strip badges"]

例如用户财富等级从 M15 升到 M16只需要等级投影变化后刷新 strip_badges_json 中的文本和样式,不需要给用户发一个新的 M16 徽章资源。

App 接口

查询我的成就

GET /api/v1/achievements?collection_type=ordinary
GET /api/v1/achievements?collection_id=act_202605

每个成就返回:

字段 说明
achievement_id 成就 ID
title / description 展示文案
status locked / in_progress / unlocked / expired
progress_value / target_value 当前进度和目标
unlocked_at_ms 解锁时间
primary_badge 主大徽章资源摘要
reward_status none / pending / granted / failed
effective_to_ms 活动成就结束时间

查询我的徽章

GET /api/v1/badges/me

返回:

{
  "strip_badges": [],
  "profile_tile_badges": [],
  "honor_badges": [],
  "server_time_ms": 1777996800000
}

strip_badges 是资料顶部长徽章;profile_tile_badges 是资料页精选大徽章;honor_badges 是 Honor 页全部大徽章。

设置资料页精选大徽章

POST /api/v1/badges/display

请求:

{
  "slot": "profile_tile",
  "items": [
    { "resource_id": 101, "entitlement_id": "ent_1", "position": 1 },
    { "resource_id": 102, "entitlement_id": "ent_2", "position": 2 }
  ],
  "command_id": "cmd_badge_display_550e8400"
}

规则:

  • 只能设置当前用户自己有效持有的大徽章。
  • profile_tile 最多 6 个,具体数量由 App 设计配置决定。
  • 长徽章中的等级、VIP、认证类标识不允许用户伪造或任意排序只允许隐藏非强制展示的资源型长徽章。
  • 重复 command_id + 相同 payload 返回同一结果;不同 payload 返回幂等冲突。

查询他人荣誉

GET /api/v1/users/{user_id}/honors

返回用户公开可见的大徽章、成就摘要和精选展示。隐私或风控隐藏的徽章不返回。

房间资料展示

现有接口可继续承载:

GET /api/v1/users/room-display-profiles:batch?user_ids=10001,10002

badges 字段建议返回已裁剪的展示数组,不返回用户全部徽章:

{
  "badges": [
    { "badge_form": "strip", "source_type": "vip", "text": "VIP7" },
    { "badge_form": "strip", "source_type": "level", "text": "M16" },
    { "badge_form": "tile", "source_type": "achievement", "resource_id": 101 }
  ]
}

后台管理

后台建议放在活动管理和资源管理两个区域:

活动管理
  - 成就配置
  - 活动成就

资源管理
  - 徽章资源
  - 徽章赠送
  - 徽章展示配置

成就配置

字段:

字段 说明
成就名称、描述 App 展示
成就类型 普通 / 活动限时
条件类型 指定任务完成 / 指标累计 / 多条件 AND
指标和目标值 例如 task_completed_count >= 10
活动窗口 活动成就必填
主徽章资源 一般选择大徽章
奖励资源组 可选,解锁后发放
排序和状态 draft / active / paused / archived

徽章资源配置

字段:

字段 说明
徽章形态 长徽章 strip / 大徽章 tile
徽章来源提示 achievement / system / level / vip / identity
资源素材 icon、preview、animation
文本模板 长徽章可用,例如 M{level}VIP{level}
默认槽位 profile_strip / profile_tile / honor_wall
展示优先级 数值越高越靠前
稀有度 common / rare / epic / legendary 等展示字段
有效期策略 永久、固定天数、跟随 VIP/身份有效期

系统赠送

字段:

字段 说明
目标用户 单个用户或批量导入
徽章或资源组 从资源库选择
有效期 永久或指定天数
赠送原因 必填,进入审计
grant_source admin / activity_campaign / identity_verify
command_id 前端按提交动作生成,重试复用

展示建议

资料页顶部可以按这个结构展示:

  1. 头像、昵称、国家和基础身份。
  2. 长徽章行VIP、等级、身份、机构等最多 6 个。
  3. 精选大徽章行:用户置顶或系统自动挑选 3 到 6 个。
  4. 数据行:访客、关注、粉丝等。
  5. TabHonor / Relationship / Moment。
  6. Honor 页:按分类展示全部大徽章和成就详情。

展示策略:

  • 长徽章越靠近昵称,越应该是强身份和实时状态。
  • 大徽章越靠近 Honor越应该是历史荣誉和活动纪念。
  • 资料页首屏只展示精选,不展示全部,避免像资源背包一样拥挤。
  • Honor 页要能区分“成就获得”和“系统赠送”,否则用户不知道徽章来源。

异常与补偿

  • 重复事件:成就进度和解锁必须用派生事件 ID 幂等。
  • 钱包发放失败:成就仍保持已解锁,奖励任务重试,后台可手动补发。
  • 徽章过期:展示投影按有效期过滤,不删除历史授予记录。
  • 系统赠送撤销:首版建议新增后台禁用/撤销权益命令,不直接删表。
  • 成就配置修改active 成就修改条件必须生成新版本;历史是否重算走后台 rebuild job。
  • 展示投影延迟:房间资料允许短暂延迟,但不能返回客户端自报徽章。
  • 多 App 隔离:所有成就、资源、展示槽位必须带 app_code

落地顺序

  1. 在资源后台为 badge 增加 badge_formbadge_kind、默认槽位和优先级配置。
  2. activity-service 增加 achievement domain定义、条件、进度、解锁、奖励任务。
  3. 扩展 task module 事件,完成任务后写出 TaskCompleted 服务端事实,供成就消费。
  4. wallet-service.GrantResourceGroup 发放成就徽章和系统徽章。
  5. 新增 user_badge_display_slots,支持多个长徽章和多个大徽章展示。
  6. 新增 App 接口:成就列表、我的徽章、设置精选大徽章、他人荣誉。
  7. 接入 room-display-profiles:batch,返回裁剪后的长徽章和精选大徽章。
  8. 补后台:成就配置、徽章资源配置、系统赠送、授予记录查询。

验收点

  • 用户完成指定任务后,成就只解锁一次,徽章奖励只发放一次。
  • 系统直接赠送大徽章后,用户 Honor 页能看到该徽章,但成就列表不会多出伪造成就。
  • 同一用户可以同时展示多个长徽章和多个大徽章。
  • 等级/VIP 变化后,长徽章展示自动变化,不产生无意义的旧等级徽章堆积。
  • 用户不能把未持有、已过期或形态不匹配的徽章设置到资料页。
  • 资料页和房间资料只返回服务端投影,客户端不能自报徽章。
  • 徽章资源、成就定义、系统赠送和展示设置都按 app_code 隔离。