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

604 lines
24 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.

# 成就和徽章体系架构
本文单独定义成就、徽章、长徽章、大徽章、系统赠送徽章和资料页展示的产品/后端边界。等级体系、VIP 体系和资源组体系已有独立文档,本文只说明它们和徽章展示的衔接方式。
## 目标
- 成就可以由服务端任务完成、事实指标达标或活动条件达成后自动解锁。
- 成就解锁后可以发放徽章,也可以只记录成就不发徽章。
- 系统可以直接赠送徽章,不要求生成一个伪造成就。
- 徽章分为长徽章和大徽章:
- 长徽章用于身份、等级、VIP、机构、职业、官方认证等横向标识。
- 大徽章用于成就、活动、荣誉墙等方形或近方形展示。
- App 可以在资料页顶部展示多个长徽章,在 Honor/荣誉区域展示多个大徽章。
- 房间资料、麦位、公屏和用户主页都只能读取服务端返回的徽章投影,客户端不能自报徽章。
- 后台可以配置成就条件、成就奖励徽章、徽章样式、徽章有效期、系统赠送和展示优先级。
## 非目标
- 不把成就体系和每日任务体系合并。每日任务是短周期任务和领奖;成就是长期或活动荣誉。
- 不把徽章配置写散到用户资料表。徽章仍然是 `wallet-service` 的资源权益。
- 不允许客户端提交“我完成了某成就”或“我拥有某徽章”。
- 不让 `room-service``wallet-service``user-service``game-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 网格里堆叠;大徽章不建议塞到昵称行里,否则会挤压昵称、短号和国家标识。
## 总体架构
```mermaid
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_count``specific_task_completed` | task module 写出 `TaskCompleted` 服务端事件,成就系统消费 |
| 送礼/收礼 | `gift_spend_coin``gift_received_value` | 以 wallet 账务事实为准 |
| 游戏 | `game_spend_coin``game_win_coin``game_round_count` | 以游戏结算或钱包扣费事实为准 |
| 关系/社交 | `follow_count``followed_count``relationship_days` | 以 user-service 关系事实为准 |
| 活跃 | `active_day_count``consecutive_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=0``reward_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_tile``honor_wall`
- Honor 页展示用户拥有的全部有效大徽章,按获得时间倒序或后台排序展示。
- 资料页顶部精选只展示 3 到 6 个,用户可以手动置顶;未设置时按稀有度、优先级、获得时间自动选择。
- 已过期的大徽章默认不展示;如果产品要展示历史已过期活动徽章,需要给资源配置 `historical_visible=true`,但不能再作为有效权益使用。
- 大徽章点击后可以进入详情页,展示来源、获得时间、有效期、对应成就进度或系统赠送原因。
## 数据模型
### 钱包资源侧
徽章本体继续复用 `resources`
```sql
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。
建议新增徽章展示规格表:
```sql
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`;但后台要按长徽章/大徽章筛选、排序和校验时,独立表更清晰。
### 成就侧
```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,
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_equipment``resource_type=badge` 只能保存一个装备项,无法满足多个长徽章和多个大徽章同时展示。建议不要直接复用它做徽章墙,而是新增多槽位表:
```sql
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_tile``honor_wall` 只允许 `badge_form=tile`
- 资源型徽章必须校验用户持有有效 `user_resource_entitlements`
- 计算型长徽章可以 `entitlement_id=''`,但必须能从等级/VIP/身份状态验证。
- 同一槽位同一 `resource_id` 不应重复展示。
### 资料展示投影
高频资料展示建议维护轻量投影:
```sql
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`。完整详情仍回源到成就和资源接口。
## 发放流程
### 成就获得大徽章
```mermaid
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}`
### 系统直接赠送徽章
```mermaid
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`、操作人、原因、目标用户、有效期和资源快照,方便客服和审计。
### 长徽章刷新
长徽章不一定经过资源发放:
```mermaid
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 接口
### 查询我的成就
```http
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` | 活动成就结束时间 |
### 查询我的徽章
```http
GET /api/v1/badges/me
```
返回:
```json
{
"strip_badges": [],
"profile_tile_badges": [],
"honor_badges": [],
"server_time_ms": 1777996800000
}
```
`strip_badges` 是资料顶部长徽章;`profile_tile_badges` 是资料页精选大徽章;`honor_badges` 是 Honor 页全部大徽章。
### 设置资料页精选大徽章
```http
POST /api/v1/badges/display
```
请求:
```json
{
"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 返回幂等冲突。
### 查询他人荣誉
```http
GET /api/v1/users/{user_id}/honors
```
返回用户公开可见的大徽章、成就摘要和精选展示。隐私或风控隐藏的徽章不返回。
### 房间资料展示
现有接口可继续承载:
```http
GET /api/v1/users/room-display-profiles:batch?user_ids=10001,10002
```
`badges` 字段建议返回已裁剪的展示数组,不返回用户全部徽章:
```json
{
"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 }
]
}
```
## 后台管理
后台建议放在活动管理和资源管理两个区域:
```text
活动管理
- 成就配置
- 活动成就
资源管理
- 徽章资源
- 徽章赠送
- 徽章展示配置
```
### 成就配置
字段:
| 字段 | 说明 |
| --- | --- |
| 成就名称、描述 | 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_form``badge_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` 隔离。