# 用户装扮 Flutter 对接文档

## 通用说明

所有需要登录的接口都需要带：

```http
Authorization: Bearer <token>
```

统一返回外壳：

```json
{
  "code": "OK",
  "message": "OK",
  "request_id": "xxx",
  "data": {}
}
```

下文只描述 `data` 内部字段。

## 1. 获取我的装扮聚合

地址：

```http
GET /api/v1/appearance
GET /api/v1/users/me/appearance
```

参数：无。

返回值：

```json
{
  "user_id": "42",
  "avatar_frame": {
    "resource_id": 8101,
    "resource_code": "frame_gold",
    "resource_type": "avatar_frame",
    "name": "Gold Frame",
    "asset_url": "https://cdn.example/frame.png",
    "preview_url": "",
    "animation_url": "https://cdn.example/frame.svga",
    "metadata_json": "{}",
    "entitlement_id": "ent-frame",
    "expires_at_ms": 1999999999999
  },
  "profile_card": {},
  "vehicle": {
    "resource_id": 8102,
    "resource_code": "vehicle_gold",
    "resource_type": "vehicle",
    "name": "Gold Vehicle",
    "asset_url": "https://cdn.example/vehicle.png",
    "preview_url": "",
    "animation_url": "https://cdn.example/vehicle.svga",
    "metadata_json": "{}",
    "entitlement_id": "ent-vehicle",
    "expires_at_ms": 1999999999999
  },
  "mic_seat_icon": {
    "resource_id": 8107,
    "resource_code": "mic_seat_icon_gold",
    "resource_type": "mic_seat_icon",
    "name": "Gold Mic Icon",
    "asset_url": "https://cdn.example/mic-seat-icon.png",
    "preview_url": "",
    "animation_url": "",
    "metadata_json": "{}",
    "entitlement_id": "ent-mic-seat-icon",
    "expires_at_ms": 1999999999999
  },
  "mic_seat_animation": {
    "resource_id": 8106,
    "resource_code": "mic_seat_wave_gold",
    "resource_type": "mic_seat_animation",
    "name": "Gold Mic Wave",
    "asset_url": "https://cdn.example/mic-seat.png",
    "preview_url": "",
    "animation_url": "https://cdn.example/mic-seat.svga",
    "metadata_json": "{}",
    "entitlement_id": "ent-mic-seat-wave",
    "expires_at_ms": 1999999999999
  },
  "equipped_badges": [],
  "badges": [
    {
      "slot": "strip",
      "position": 1,
      "badge_form": "icon",
      "resource_id": 8103,
      "resource": {
        "resource_id": 8103,
        "resource_code": "badge_host",
        "resource_type": "badge",
        "name": "Host Badge",
        "asset_url": "https://cdn.example/badge.png",
        "preview_url": "",
        "animation_url": "",
        "metadata_json": "{}",
        "entitlement_id": "",
        "expires_at_ms": 0
      },
      "entitlement_id": "ent-badge",
      "source_type": "achievement",
      "source_id": "ach-host",
      "pin_mode": "auto",
      "updated_at_ms": 1778256000000
    }
  ],
  "badge_profile": {
    "strip_badges": [],
    "profile_tile_badges": [],
    "honor_badges": [],
    "server_time_ms": 1778256000000
  },
  "resources": [
    {
      "resource_id": 8107,
      "resource_code": "mic_seat_icon_gold",
      "resource_type": "mic_seat_icon",
      "name": "Gold Mic Icon",
      "asset_url": "https://cdn.example/mic-seat-icon.png",
      "preview_url": "",
      "animation_url": "",
      "metadata_json": "{}",
      "entitlement_id": "ent-mic-seat-icon",
      "expires_at_ms": 1999999999999
    },
    {
      "resource_id": 8106,
      "resource_code": "mic_seat_wave_gold",
      "resource_type": "mic_seat_animation",
      "name": "Gold Mic Wave",
      "asset_url": "https://cdn.example/mic-seat.png",
      "preview_url": "",
      "animation_url": "https://cdn.example/mic-seat.svga",
      "metadata_json": "{}",
      "entitlement_id": "ent-mic-seat-wave",
      "expires_at_ms": 1999999999999
    }
  ],
  "server_time_ms": 1778256000000
}
```

字段说明：

| 字段 | 说明 |
|---|---|
| user_id | 用户 ID，字符串 |
| avatar_frame | 当前佩戴头像框，未佩戴时为 `{}` |
| profile_card | 当前佩戴背景卡，未佩戴时为 `{}` |
| vehicle | 当前佩戴入场座驾，未佩戴时为 `{}` |
| mic_seat_icon | 当前佩戴麦位中心图标，未佩戴时为 `{}` |
| mic_seat_animation | 当前佩戴麦位声波，未佩戴时为 `{}` |
| equipped_badges | wallet 当前佩戴的 badge 权益 |
| badges | 房间、公屏等高频展示用徽章，取 `strip_badges` |
| badge_profile | 完整徽章展示配置 |
| resources | 当前佩戴资源的完整列表 |
| server_time_ms | 服务端时间戳 |

说明：

- 过期佩戴不会返回。
- 头像框、背景卡、入场座驾、麦位中心图标和麦位声波来自 wallet 当前佩戴。
- `mic_seat_icon` 与 `mic_seat_animation` 是两个独立装备槽；Flutter 只能用前者替换麦位中心图标，后者继续渲染 Voice Wave，二者不能互相覆盖或兜底。
- 徽章展示配置来自 activity。
- 客户端渲染动画优先使用 `animation_url`，没有动画时用 `asset_url` 或 `preview_url` 兜底。

## 2. 获取我的资源背包

地址：

```http
GET /api/v1/users/me/resources
```

参数：

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| resource_type | string | 否 | `avatar_frame` / `profile_card` / `vehicle` / `badge` / `chat_bubble` / `mic_seat_icon` / `mic_seat_animation` |

示例：

```http
GET /api/v1/users/me/resources?resource_type=vehicle
```

返回值：

```json
{
  "items": [
    {
      "entitlement_id": "ent-vehicle",
      "resource_id": 8102,
      "resource": {
        "resource_id": 8102,
        "resource_code": "vehicle_gold",
        "resource_type": "vehicle",
        "name": "Gold Vehicle",
        "status": "active",
        "usage_scopes": ["room_entry"],
        "asset_url": "https://cdn.example/vehicle.png",
        "preview_url": "",
        "animation_url": "https://cdn.example/vehicle.svga",
        "metadata_json": "{}"
      },
      "status": "active",
      "quantity": 1,
      "remaining_quantity": 1,
      "effective_at_ms": 1778256000000,
      "expires_at_ms": 1999999999999,
      "source_grant_id": "grant-1",
      "created_at_ms": 1778256000000,
      "updated_at_ms": 1778256000000,
      "equipped": true
    }
  ],
  "total": 1
}
```

字段说明：

| 字段 | 说明 |
|---|---|
| entitlement_id | 用户权益 ID，佩戴时建议传这个值 |
| resource_id | 资源 ID |
| resource | 资源素材和基础信息 |
| status | 权益状态 |
| quantity | 总数量 |
| remaining_quantity | 剩余数量 |
| effective_at_ms | 生效时间 |
| expires_at_ms | 过期时间，0 表示不过期 |
| equipped | 是否当前佩戴 |

## 3. 佩戴资源

地址：

```http
POST /api/v1/users/me/resources/{resource_id}/equip
```

路径参数：

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| resource_id | int64 | 是 | 资源 ID |

Body：

```json
{
  "entitlement_id": "ent-vehicle"
}
```

Body 参数：

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| entitlement_id | string | 否 | 用户权益 ID，建议传 |

返回值：

```json
{
  "entitlement_id": "ent-vehicle",
  "resource_id": 8102,
  "resource": {},
  "status": "active",
  "quantity": 1,
  "remaining_quantity": 1,
  "effective_at_ms": 1778256000000,
  "expires_at_ms": 1999999999999,
  "equipped": true
}
```

说明：

- 可佩戴类型：`avatar_frame`、`profile_card`、`vehicle`、`chat_bubble`、`badge`、`mic_seat_icon`、`mic_seat_animation`。
- `badge`、`mic_seat_animation` 发放成功后服务端默认佩戴。
- 过期资源不能佩戴。
- 不传 `entitlement_id` 时，服务端会选择当前有效权益。

## 4. 取消佩戴

地址：

```http
DELETE /api/v1/users/me/resources/{resource_type}/unequip
```

路径参数：

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| resource_type | string | 是 | `avatar_frame` / `profile_card` / `vehicle` / `chat_bubble` / `badge` / `mic_seat_icon` / `mic_seat_animation` |

返回值：

```json
{
  "resource_type": "vehicle",
  "unequipped": true,
  "updated_at_ms": 1778256000000
}
```

说明：

- 重复取消不会报错。
- 用户本来没佩戴该类型时，`unequipped=false`。

## 5. 批量获取房间展示资料

地址：

```http
GET /api/v1/users/room-display-profiles:batch
```

参数：

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| user_ids | string | 是 | 多个用户 ID，逗号分隔 |
| user_id | string | 否 | 也支持重复传多个 `user_id` |

示例：

```http
GET /api/v1/users/room-display-profiles:batch?user_ids=42,43
```

返回值：

```json
{
  "profiles": [
    {
      "user_id": "42",
      "username": "Tom",
      "avatar": "https://cdn.example/avatar.png",
      "display_user_id": "100001",
      "gender": "male",
      "age": 22,
      "country": "CN",
      "country_name": "China",
      "country_display_name": "China",
      "country_flag": "CN",
      "vip": {},
      "level": {},
      "badges": [],
      "avatar_frame": {
        "resource_id": 8101,
        "resource_code": "frame_gold",
        "resource_type": "avatar_frame",
        "name": "Gold Frame",
        "asset_url": "https://cdn.example/frame.png",
        "preview_url": "",
        "animation_url": "https://cdn.example/frame.svga",
        "metadata_json": "{}",
        "entitlement_id": "ent-frame",
        "expires_at_ms": 1999999999999
      },
      "profile_card": {
        "resource_id": 8104,
        "resource_code": "profile_card_gold",
        "resource_type": "profile_card",
        "name": "Gold Profile Card",
        "asset_url": "https://cdn.example/profile-card.png",
        "preview_url": "",
        "animation_url": "https://cdn.example/profile-card.mp4",
        "metadata_json": "{\"profile_card_layout\":{\"source_width\":1136,\"source_height\":1680,\"content_top\":143,\"content_height\":1525}}",
        "entitlement_id": "ent-profile-card",
        "expires_at_ms": 1999999999999
      },
      "vehicle": {
        "resource_id": 8102,
        "resource_code": "vehicle_gold",
        "resource_type": "vehicle",
        "name": "Gold Vehicle",
        "asset_url": "https://cdn.example/vehicle.png",
        "preview_url": "",
        "animation_url": "https://cdn.example/vehicle.svga",
        "metadata_json": "{}",
        "entitlement_id": "ent-vehicle",
        "expires_at_ms": 1999999999999
      },
      "chat_bubble": {
        "resource_id": 8103,
        "resource_code": "bubble_gold",
        "resource_type": "chat_bubble",
        "name": "Gold Chat Bubble",
        "asset_url": "https://cdn.example/chat-bubble.png",
        "preview_url": "",
        "animation_url": "",
        "metadata_json": "{}",
        "entitlement_id": "ent-chat-bubble",
        "expires_at_ms": 1999999999999
      },
      "mic_seat_icon": {
        "resource_id": 8107,
        "resource_code": "mic_seat_icon_gold",
        "resource_type": "mic_seat_icon",
        "name": "Gold Mic Icon",
        "asset_url": "https://cdn.example/mic-seat-icon.png",
        "preview_url": "",
        "animation_url": "",
        "metadata_json": "{}",
        "entitlement_id": "ent-mic-seat-icon",
        "expires_at_ms": 1999999999999
      },
      "mic_seat_animation": {
        "resource_id": 8106,
        "resource_code": "mic_seat_wave_gold",
        "resource_type": "mic_seat_animation",
        "name": "Gold Mic Wave",
        "asset_url": "https://cdn.example/mic-seat.png",
        "preview_url": "",
        "animation_url": "https://cdn.example/mic-seat.svga",
        "metadata_json": "{}",
        "entitlement_id": "ent-mic-seat-wave",
        "expires_at_ms": 1999999999999
      },
      "charm": 0
    }
  ]
}
```

说明：

- 房间、公屏、在线列表展示头像框用 `avatar_frame`。
- 房间资料卡展示用 `profile_card`，从 `metadata_json.profile_card_layout` 解析 `content_top` 和 `content_height`。
- 入场座驾展示用 `vehicle`。
- 公屏聊天气泡用 `chat_bubble`；未佩戴时固定返回空对象，普通文本消息按发送人 ID 批量补齐该字段。
- VIP 装扮只在当前有效 VIP 权限覆盖对应等级时视为已佩戴；VIP 失效后背包可保留历史权益，但 `equipped=false`，客户端不得用资源列表兜底继续渲染。
- 麦位中心图标展示用 `mic_seat_icon`；在有用户的麦位用其 `asset_url` 替换默认中心图标，未佩戴时使用客户端默认麦位图标。
- 麦位声波展示用 `mic_seat_animation`。
- 同一资料同时返回两者时必须叠加渲染：`mic_seat_icon` 管中心图标，`mic_seat_animation` 管 Voice Wave；不能因为图标存在而隐藏声波。
- 徽章条展示用 `badges`。
- 过期佩戴不会返回。

## 6. 进房

地址：

```http
POST /api/v1/rooms/join
```

Body：

```json
{
  "room_id": "room_1001",
  "command_id": "cmd-join-1001",
  "role": "audience",
  "password": "1234"
}
```

参数：

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| room_id | string | 是 | 房间 ID |
| command_id | string | 是 | 幂等 ID，Flutter 每次进房动作生成一个 |
| role | string | 否 | 默认 `audience` |
| password | string | 否 | 锁房密码 |

返回值：

```json
{
  "result": {
    "applied": true,
    "room_version": 9,
    "server_time_ms": 1778256000000
  },
  "room": {},
  "viewer": {},
  "seats": [],
  "contribution_rank": [],
  "profiles": [
    {
      "user_id": "42",
      "display_user_id": "100001",
      "username": "Tom",
      "avatar": "https://cdn.example/avatar.png"
    }
  ],
  "im": {
    "group_id": "room_1001",
    "need_join_group": true
  },
  "rtc": {
    "need_token": true,
    "available": true,
    "token": {}
  },
  "server_time_ms": 1778256000000
}
```

说明：

- Flutter 不需要传入场座驾。
- 服务端会在进房时自动读取当前佩戴的 `vehicle`，写入房间 IM。
- 完整模式下 `join` 返回里的 `profiles` 与 `/users/room-display-profiles:batch` 使用同一聚合结构，直接包含 `mic_seat_icon` 和 `mic_seat_animation`；lite 模式不保证返回增强装扮。
- 后续增量用户用 `/users/room-display-profiles:batch`，或使用房间详情、在线用户列表里内嵌的 `profile` 字段补齐。

## 7. 相关 IM

### 用户进房

腾讯 IM 群自定义消息：

```json
{
  "event_id": "evt_xxx",
  "room_id": "room_1001",
  "event_type": "room_user_joined",
  "actor_user_id": 42,
  "target_user_id": 42,
  "room_version": 9,
  "entry_vehicle": {
    "resource_id": 8102,
    "resource_code": "vehicle_gold",
    "name": "Gold Vehicle",
    "asset_url": "https://cdn.example/vehicle.png",
    "preview_url": "",
    "animation_url": "https://cdn.example/vehicle.svga",
    "metadata_json": "{}",
    "entitlement_id": "ent-vehicle",
    "expires_at_ms": 1999999999999
  },
  "attributes": {
    "role": "audience"
  }
}
```

Flutter 处理：

| 字段 | 说明 |
|---|---|
| event_type | `room_user_joined` 表示用户进入房间 |
| actor_user_id | 进房用户 ID |
| target_user_id | 进房用户 ID |
| room_version | 房间版本 |
| entry_vehicle | 入场座驾快照 |
| attributes.role | 用户进房角色，仅用于展示 |

处理规则：

- `entry_vehicle.resource_id > 0`：播放入场座驾。
- `entry_vehicle.animation_url`：优先用于动画播放。
- `entry_vehicle.asset_url` / `entry_vehicle.preview_url`：动画不可用时兜底展示。
- `entry_vehicle` 不存在或为空：普通进房，不播放座驾。
- 不要收到进房 IM 后再实时请求 `/appearance` 查询座驾，直接使用 IM 中的 `entry_vehicle` 快照。

## 8. Flutter 调用建议

1. 个人装扮页进入时调 `/api/v1/appearance`。
2. 背包页调 `/api/v1/users/me/resources?resource_type=xxx`。
3. 点击佩戴调 `POST /api/v1/users/me/resources/{resource_id}/equip`，成功后刷新 `/appearance`。
4. 点击取消佩戴调 `DELETE /api/v1/users/me/resources/{resource_type}/unequip`，成功后刷新 `/appearance`。
5. 个人主页信息卡背景使用个人资料返回的 `profile_bg_img`；该字段是用户资料字段，和 `profile_card` 装扮资源无关。
6. 房间、公屏、在线用户资料优先使用 `room-display-profiles:batch` 的 `avatar_frame`、`profile_card`、`vehicle`、`chat_bubble`、`mic_seat_icon`、`mic_seat_animation`、`badges`。
7. 收到 `room_user_joined` IM 时，只按 `entry_vehicle` 播放入场座驾。
