# 成就和徽章体系架构

本文单独定义成就、徽章、长徽章、大徽章、系统赠送徽章和资料页展示的产品/后端边界。等级体系、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. Tab：Honor / 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` 隔离。
