# 新版 VIP 权限策略 Flutter 客户端对接文档

## 1. VIP 权限策略

### 1.1 权限来源

- VIP 等级只用于套餐名称、价格、有效期和等级展示。
- Flutter 只使用 `GET /api/v1/vip/me` 返回的 `data.state.effective_benefits` 判断当前用户是否具有某项权益。
- Flutter 不得使用 `VIP等级 >= N`、`unlock_level`、`app_code` 或本地权益表推导权限。
- `/api/v1/vip/packages` 中的 `packages[].benefits` 是该套餐当前配置的完整权益集合，只用于套餐页和升级引导，不能作为用户已拥有权益。
- 写操作最终以对应业务接口的服务端鉴权结果为准。Flutter 的权益判断只用于入口显隐、锁态和交互预判。

### 1.2 Flutter 权益状态

```dart
class VipEntitlementState {
  final VipProgramConfig program;
  final VipIdentity effectiveVip;
  final String effectiveSource; // paid / trial / none
  final Map<String, VipBenefit> benefitsByCode;
  final VipUserSettings settings;
  final int evaluatedAtMs;

  bool has(String benefitCode) {
    return program.status == 'active' &&
        effectiveVip.active &&
        benefitsByCode[benefitCode]?.status == 'active';
  }

  bool allowsRoomEntryNotice() {
    return has('room_entry_notice') && settings.roomEntryNoticeEnabled;
  }

  bool allowsOnlineGlobalNotice() {
    return has('online_global_notice') &&
        settings.onlineGlobalNoticeEnabled;
  }

  bool allowsHideProfileData() {
    return has('hide_profile_data') && settings.hideProfileDataEnabled;
  }

  bool allowsAnonymousProfileVisit() {
    return has('anonymous_profile_visit') &&
        settings.anonymousProfileVisitEnabled;
  }

  bool allowsLeaderboardInvisible() {
    return has('leaderboard_invisible') &&
        settings.leaderboardInvisibleEnabled;
  }
}
```

`benefitsByCode` 必须由 `state.effective_benefits` 转换。未知 `benefit_code` 保留在状态中但不自动开放功能，不得导致解析失败或页面崩溃。

### 1.3 权益刷新

| 场景 | Flutter 处理 |
|---|---|
| 登录成功、切换账号、切换 App | 清空原 VIP 状态，调用 `/vip/me` |
| App 冷启动、回到前台 | 调用 `/vip/me` 校准过期、体验卡回落和后台配置变更 |
| 进入 VIP 页面 | 并发调用 `/vip/packages` 和 `/vip/me` |
| 购买、佩戴体验卡、卸下体验卡成功 | 使用响应中的 `state` 原子替换本地状态 |
| 收到 `vip_notice` | 按 `event_id` 去重，再调用 `/vip/me` |
| `config_version` 不一致 | 用户权限以 `/vip/me.state` 为准，重新请求 `/vip/packages`，不得合并不同版本的权益数组 |
| VIP 到期倒计时结束 | 可更新页面显示，但执行功能前仍以服务端接口结果为准 |

购买、佩戴体验卡、卸下体验卡成功时，`data.state` 与 `GET /api/v1/vip/me` 的 `data.state` **完全同构**，不是局部补丁。Flutter 必须整体替换 VIP store，禁止按字段合并。若灰度期间收到缺少下文任一顶层字段的旧节点响应，应放弃该响应中的 `state` 并立即重拉 `/vip/me`。

### 1.4 Fami 初始等级权益

下表只用于识别初始配置。Flutter 实际必须渲染 `/vip/packages` 返回的数据。

| 等级 | 本级初始新增权益 |
|---|---|
| VIP1 | `vip_badge_identity`、`vip_medal`、`avatar_frame`、`vip_title` |
| VIP2 | `chat_bubble`、`vip_gift`、`entry_effect`、`visitor_history` |
| VIP3 | `voice_wave`、`profile_card`、`custom_room_background`、`room_image_message` |
| VIP4 | `animated_avatar`、`animated_room_cover`、`mic_skin`、`room_border`、`vehicle` |
| VIP5 | `colored_room_name`、`colored_nickname`、`colored_id`、`gift_tray_skin` |
| VIP6 | `hide_profile_data`、`custom_avatar_frame`、`leaderboard_invisible`、`daily_coin_rebate` |
| VIP7 | `custom_profile_card`、`anonymous_profile_visit`、`custom_gift` |
| VIP8 | `room_entry_notice`、`custom_pretty_id`、`custom_vehicle` |
| VIP9 | `anti_kick`、`anti_mute`、`online_global_notice` |

## 2. 通用协议

- API 前缀：`/api/v1`。
- 请求头：`Authorization: Bearer <access_token>`。
- `app_code`、`user_id` 从登录态解析，Flutter 不传。
- 时间字段统一使用 Unix epoch milliseconds。
- 业务切日使用 UTC，Flutter 仅在展示时转换本地时间。
- 成功响应统一格式：

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

- `request_id` 只用于链路追踪。
- 带 `command_id` 的接口，网络重试必须复用原 `command_id`；新的业务动作必须生成新值。

### 2.1 `VipState` 唯一结构

`GET /vip/me`、`GET /vip/packages`、购买、佩戴体验卡和卸下体验卡返回的 `state` 使用同一结构，固定包含以下 8 个顶层字段：

| 字段 | 含义 |
|---|---|
| `paid_vip` | 当前付费 VIP；没有时为 `null` |
| `equipped_trial_card` | 当前佩戴的体验卡；没有时为 `null` |
| `effective_vip` | 付费 VIP 与体验卡合并后的当前展示身份 |
| `effective_source` | `paid`、`trial`、`none` |
| `effective_benefits` | 服务端计算后的最终权益集合 |
| `evaluated_at_ms` | 本次状态计算时间 |
| `program_config` | 本次计算使用的完整 App VIP 配置 |
| `user_settings` | 当前 App 下完整的 5 项用户开关 |

`paid_vip`、`effective_vip` 非空时都返回完整 `UserVip` 字段：`user_id`、`level`、`name`、`active`、`started_at_ms`、`expires_at_ms`、`updated_at_ms`、`program_type`、`config_version`。`program_config` 返回完整字段，不允许写接口只返回 `app_code/program_type` 的裁剪对象。

### 2.2 特权展示协议

每项 `VipBenefit` 除执行字段外提供结构化 `presentation`，Flutter 不解析 `metadata_json`，也不按 `benefit_code` 维护隐形展示配置：

```json
{
  "description": "进入房间时展示动态背景和用户头像",
  "preview_items": [
    {
      "preview_id": "room_background_gold",
      "title": "鎏金房间背景",
      "media_type": "animation",
      "asset_url": "https://cdn.example/vip/room-bg.png",
      "preview_url": "https://cdn.example/vip/room-bg-preview.png",
      "animation_url": "https://cdn.example/vip/room-bg.svga",
      "composition_type": "user_avatar_center",
      "sort_order": 10
    }
  ],
  "numeric_reward": null
}
```

- `media_type`：`image` 或 `animation`；动态资源加载失败时依次使用 `preview_url`、`asset_url`。
- `composition_type`：`none`、`user_avatar_center`、`user_avatar_waveform`，分别表示无合成、头像居中、头像与声波纹合成。
- `preview_items` 可返回多项，按 `sort_order` 升序、`preview_id` 去重。
- `numeric_reward` 用于数值权益，字段为 `label/value/unit/period`，其中 `period` 为 `once/daily/monthly`。该字段只用于展示，结算仍以后端接口为准。
- 没有富预览时 `presentation` 为 `null`；Flutter 回退展示权益名称和通用图标。

## 3. VIP 套餐

### 接口地址

`GET /api/v1/vip/packages`

### 参数

无。

### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_packages",
  "data": {
    "current_vip": {
      "level": 3,
      "name": "VIP3",
      "active": true,
      "started_at_ms": 1780000000000,
      "expires_at_ms": 1782592000000,
      "program_type": "tiered_privilege_v1",
      "config_version": 7
    },
    "packages": [
      {
        "level": 4,
        "name": "VIP4",
        "status": "active",
        "price_coin": 4000,
        "duration_ms": 2592000000,
        "reward_resource_group_id": 0,
        "reward_items": [],
        "can_purchase": true,
        "sort_order": 40,
        "recharge_gate_required": false,
        "required_recharge_coin_amount": 0,
        "user_recharge_coin_amount": 0,
        "purchase_locked_reason": "",
        "benefits": [
          {
            "benefit_code": "custom_room_background",
            "name": "自定义房间背景",
            "benefit_type": "function",
            "unlock_level": 3,
            "status": "active",
            "trial_enabled": true,
            "resource_id": 0,
            "resource_type": "",
            "execution_scope": "room",
            "auto_equip": false,
            "sort_order": 110,
            "metadata_json": "",
            "presentation": {
              "description": "进入房间时展示动态背景和用户头像",
              "preview_items": [
                {
                  "preview_id": "room_background_gold",
                  "title": "鎏金房间背景",
                  "media_type": "animation",
                  "asset_url": "https://cdn.example/vip/room-bg.png",
                  "preview_url": "https://cdn.example/vip/room-bg-preview.png",
                  "animation_url": "https://cdn.example/vip/room-bg.svga",
                  "composition_type": "user_avatar_center",
                  "sort_order": 10
                }
              ],
              "numeric_reward": null
            }
          }
        ],
        "config_version": 7
      }
    ],
    "program_config": {
      "app_code": "fami",
      "program_type": "tiered_privilege_v1",
      "level_count": 9,
      "same_level_expiry_policy": "extend_remaining",
      "upgrade_expiry_policy": "replace_from_now",
      "downgrade_purchase_policy": "reject",
      "benefit_inheritance_policy": "target_only",
      "grant_mode": "trial_card",
      "trial_card_enabled": true,
      "status": "active",
      "config_version": 7
    },
    "state": {
      "paid_vip": {
        "user_id": 10001,
        "level": 3,
        "name": "VIP3",
        "active": true,
        "started_at_ms": 1780000000000,
        "expires_at_ms": 1782592000000,
        "updated_at_ms": 1780000000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "equipped_trial_card": null,
      "effective_vip": {
        "user_id": 10001,
        "level": 3,
        "name": "VIP3",
        "active": true,
        "started_at_ms": 1780000000000,
        "expires_at_ms": 1782592000000,
        "updated_at_ms": 1780000000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "effective_source": "paid",
      "effective_benefits": [
        {
          "benefit_code": "custom_room_background",
          "status": "active"
        }
      ],
      "evaluated_at_ms": 1780000000000,
      "program_config": {
        "app_code": "fami",
        "program_type": "tiered_privilege_v1",
        "level_count": 9,
        "same_level_expiry_policy": "extend_remaining",
        "upgrade_expiry_policy": "replace_from_now",
        "downgrade_purchase_policy": "reject",
        "benefit_inheritance_policy": "target_only",
        "grant_mode": "trial_card",
        "trial_card_enabled": true,
        "status": "active",
        "config_version": 7
      },
      "user_settings": {
        "app_code": "fami",
        "user_id": 10001,
        "room_entry_notice_enabled": true,
        "online_global_notice_enabled": true,
        "hide_profile_data_enabled": false,
        "anonymous_profile_visit_enabled": false,
        "leaderboard_invisible_enabled": false,
        "updated_at_ms": 0
      }
    }
  }
}
```

## 4. 当前 VIP 权限状态

### 接口地址

`GET /api/v1/vip/me`

### 参数

无。

### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_vip_me",
  "data": {
    "level": 8,
    "name": "VIP8",
    "active": true,
    "started_at_ms": 1780100000000,
    "expires_at_ms": 1782692000000,
    "program_type": "tiered_privilege_v1",
    "config_version": 7,
    "program_config": {
      "app_code": "fami",
      "program_type": "tiered_privilege_v1",
      "level_count": 9,
      "same_level_expiry_policy": "extend_remaining",
      "upgrade_expiry_policy": "replace_from_now",
      "downgrade_purchase_policy": "reject",
      "benefit_inheritance_policy": "target_only",
      "grant_mode": "trial_card",
      "trial_card_enabled": true,
      "status": "active",
      "config_version": 7
    },
    "state": {
      "paid_vip": {
        "user_id": 10001,
        "level": 3,
        "name": "VIP3",
        "active": true,
        "started_at_ms": 1780000000000,
        "expires_at_ms": 1782592000000,
        "updated_at_ms": 1780000000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "equipped_trial_card": {
        "trial_card_id": "vip_trial_card_xxx",
        "entitlement_id": "ent_xxx",
        "resource_id": 108,
        "user_id": 10001,
        "level": 8,
        "name": "VIP8",
        "status": "active",
        "equipped": true,
        "duration_ms": 2592000000,
        "effective_at_ms": 1780100000000,
        "expires_at_ms": 1782692000000,
        "remaining_duration_ms": 2591000000,
        "grant_source": "admin",
        "source_grant_id": "grant_xxx",
        "created_at_ms": 1780100000000,
        "updated_at_ms": 1780100000000
      },
      "effective_vip": {
        "user_id": 10001,
        "level": 8,
        "name": "VIP8",
        "active": true,
        "started_at_ms": 1780100000000,
        "expires_at_ms": 1782692000000,
        "updated_at_ms": 1780100000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "effective_source": "trial",
      "effective_benefits": [
        {
          "benefit_code": "room_entry_notice",
          "name": "进房高亮通知",
          "benefit_type": "function",
          "status": "active",
          "trial_enabled": true,
          "resource_id": 0,
          "resource_type": "",
          "execution_scope": "room",
          "auto_equip": false,
          "sort_order": 290,
          "metadata_json": "",
          "presentation": null
        }
      ],
      "evaluated_at_ms": 1780101000000,
      "program_config": {
        "app_code": "fami",
        "program_type": "tiered_privilege_v1",
        "level_count": 9,
        "same_level_expiry_policy": "extend_remaining",
        "upgrade_expiry_policy": "replace_from_now",
        "downgrade_purchase_policy": "reject",
        "benefit_inheritance_policy": "target_only",
        "grant_mode": "trial_card",
        "trial_card_enabled": true,
        "status": "active",
        "config_version": 7
      },
      "user_settings": {
        "app_code": "fami",
        "user_id": 10001,
        "room_entry_notice_enabled": true,
        "online_global_notice_enabled": true,
        "hide_profile_data_enabled": false,
        "anonymous_profile_visit_enabled": false,
        "leaderboard_invisible_enabled": false,
        "updated_at_ms": 0
      }
    }
  }
}
```

- `effective_source`：`paid`、`trial`、`none`。
- 当前展示身份只读 `state.effective_vip`。
- 当前权限只读 `state.effective_benefits`。
- `daily_coin_rebate` 不会因体验卡进入 `effective_benefits`。

## 5. 购买、续期、升级

### 接口地址

`POST /api/v1/vip/purchase`

### 参数

```json
{
  "command_id": "vip_purchase_10001_4_1780100000000",
  "level": 4
}
```

| 字段 | 必填 | 说明 |
|---|---|---|
| `command_id` | 是 | 最长 128 字节，同一次购买重试必须复用 |
| `level` | 是 | 必须来自 `/vip/packages` |

### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_purchase",
  "data": {
    "order_id": "vip_order_xxx",
    "transaction_id": "tx_xxx",
    "vip": {
      "level": 4,
      "name": "VIP4",
      "active": true,
      "started_at_ms": 1780100000000,
      "expires_at_ms": 1782692000000,
      "program_type": "tiered_privilege_v1",
      "config_version": 7
    },
    "coin_spent": 4000,
    "coin_balance_after": 6000,
    "coin_balance": {
      "asset_type": "COIN",
      "available_amount": 6000,
      "frozen_amount": 0,
      "version": 18
    },
    "reward_items": [],
    "program_config": {
      "app_code": "fami",
      "program_type": "tiered_privilege_v1",
      "level_count": 9,
      "same_level_expiry_policy": "extend_remaining",
      "upgrade_expiry_policy": "replace_from_now",
      "downgrade_purchase_policy": "reject",
      "benefit_inheritance_policy": "target_only",
      "grant_mode": "trial_card",
      "trial_card_enabled": true,
      "status": "active",
      "config_version": 7
    },
    "state": {
      "paid_vip": {
        "user_id": 10001,
        "level": 4,
        "name": "VIP4",
        "active": true,
        "started_at_ms": 1780100000000,
        "expires_at_ms": 1782692000000,
        "updated_at_ms": 1780100000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "equipped_trial_card": null,
      "effective_vip": {
        "user_id": 10001,
        "level": 4,
        "name": "VIP4",
        "active": true,
        "started_at_ms": 1780100000000,
        "expires_at_ms": 1782692000000,
        "updated_at_ms": 1780100000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "effective_source": "paid",
      "effective_benefits": [
        {
          "benefit_code": "custom_room_background",
          "status": "active"
        }
      ],
      "evaluated_at_ms": 1780100000000,
      "program_config": {
        "app_code": "fami",
        "program_type": "tiered_privilege_v1",
        "level_count": 9,
        "same_level_expiry_policy": "extend_remaining",
        "upgrade_expiry_policy": "replace_from_now",
        "downgrade_purchase_policy": "reject",
        "benefit_inheritance_policy": "target_only",
        "grant_mode": "trial_card",
        "trial_card_enabled": true,
        "status": "active",
        "config_version": 7
      },
      "user_settings": {
        "app_code": "fami",
        "user_id": 10001,
        "room_entry_notice_enabled": true,
        "online_global_notice_enabled": true,
        "hide_profile_data_enabled": false,
        "anonymous_profile_visit_enabled": false,
        "leaderboard_invisible_enabled": false,
        "updated_at_ms": 0
      }
    }
  }
}
```

- 同级购买：从原截止时间继续累加。
- Fami 高等级购买：立即替换低等级，从购买时间重新计时，低等级剩余时间丢弃。
- 低等级购买：服务端拒绝。
- 购买时佩戴体验卡：目标等级大于等于体验等级时，体验卡及其体验资源会被回收，响应为真实付费 VIP；目标等级低于体验等级时，`paid_vip` 独立计时，`effective_vip` 继续来自体验卡。两种情况都必须整体使用响应 `state`。
- 钱包缓存只使用 `coin_balance`。应用时比较 `version`，低版本响应不得覆盖本地高版本余额；`coin_balance_after` 仅保留旧客户端兼容。
- 历史幂等订单在升级前没有记录版本时可能返回 `coin_balance.version=0`，Flutter 必须调用现有钱包余额接口刷新，不得把版本 0 写入余额 store。

## 6. VIP 体验卡

### 6.1 体验卡列表

#### 接口地址

`GET /api/v1/users/me/resources?resource_type=vip_trial_card`

#### 参数

| 参数 | 位置 | 必填 | 值 |
|---|---|---|---|
| `resource_type` | query | 是 | `vip_trial_card` |

#### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_trial_cards",
  "data": {
    "items": [
      {
        "entitlement_id": "ent_card_a",
        "resource_id": 101,
        "resource": {
          "resource_id": 101,
          "resource_code": "vip_trial_card_3",
          "resource_type": "vip_trial_card",
          "name": "VIP3体验卡",
          "status": "active",
          "asset_url": "",
          "preview_url": "",
          "animation_url": "",
          "metadata_json": "{\"vip_level\":3}",
          "sort_order": 30
        },
        "status": "active",
        "quantity": 1,
        "remaining_quantity": 1,
        "effective_at_ms": 1780100000000,
        "expires_at_ms": 1781309600000,
        "source_grant_id": "grant_a",
        "created_at_ms": 1780100000000,
        "updated_at_ms": 1780100000000,
        "equipped": false
      }
    ],
    "total": 1
  }
}
```

### 6.2 佩戴体验卡

#### 接口地址

`POST /api/v1/vip/trial-cards/{entitlement_id}/equip`

#### 参数

| 参数 | 位置 | 必填 | 说明 |
|---|---|---|---|
| `entitlement_id` | path | 是 | 使用体验卡列表中的实例 ID |

无 body。

#### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_trial_equip",
  "data": {
    "trial_card": {
      "trial_card_id": "vip_trial_card_b",
      "entitlement_id": "ent_card_b",
      "resource_id": 102,
      "user_id": 10001,
      "level": 4,
      "name": "VIP4",
      "status": "active",
      "equipped": true,
      "duration_ms": 1728000000,
      "effective_at_ms": 1780200000000,
      "expires_at_ms": 1781928000000,
      "remaining_duration_ms": 1700000000,
      "grant_source": "admin",
      "source_grant_id": "grant_b",
      "created_at_ms": 1780200000000,
      "updated_at_ms": 1780200000000
    },
    "program_config": {
      "app_code": "fami",
      "program_type": "tiered_privilege_v1",
      "level_count": 9,
      "same_level_expiry_policy": "extend_remaining",
      "upgrade_expiry_policy": "replace_from_now",
      "downgrade_purchase_policy": "reject",
      "benefit_inheritance_policy": "target_only",
      "grant_mode": "trial_card",
      "trial_card_enabled": true,
      "status": "active",
      "config_version": 7
    },
    "state": {
      "paid_vip": {
        "user_id": 10001,
        "level": 3,
        "name": "VIP3",
        "active": true,
        "started_at_ms": 1780000000000,
        "expires_at_ms": 1782592000000,
        "updated_at_ms": 1780000000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "equipped_trial_card": {
        "trial_card_id": "vip_trial_card_b",
        "entitlement_id": "ent_card_b",
        "resource_id": 102,
        "user_id": 10001,
        "level": 4,
        "name": "VIP4",
        "status": "active",
        "equipped": true,
        "duration_ms": 1728000000,
        "effective_at_ms": 1780200000000,
        "expires_at_ms": 1781928000000,
        "remaining_duration_ms": 1700000000,
        "grant_source": "admin",
        "source_grant_id": "grant_b",
        "created_at_ms": 1780200000000,
        "updated_at_ms": 1780200000000
      },
      "effective_vip": {
        "user_id": 10001,
        "level": 4,
        "name": "VIP4",
        "active": true,
        "started_at_ms": 1780200000000,
        "expires_at_ms": 1781928000000,
        "updated_at_ms": 1780200000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "effective_source": "trial",
      "effective_benefits": [],
      "evaluated_at_ms": 1780228000000,
      "program_config": {
        "app_code": "fami",
        "program_type": "tiered_privilege_v1",
        "level_count": 9,
        "same_level_expiry_policy": "extend_remaining",
        "upgrade_expiry_policy": "replace_from_now",
        "downgrade_purchase_policy": "reject",
        "benefit_inheritance_policy": "target_only",
        "grant_mode": "trial_card",
        "trial_card_enabled": true,
        "status": "active",
        "config_version": 7
      },
      "user_settings": {
        "app_code": "fami",
        "user_id": 10001,
        "room_entry_notice_enabled": true,
        "online_global_notice_enabled": true,
        "hide_profile_data_enabled": false,
        "anonymous_profile_visit_enabled": false,
        "leaderboard_invisible_enabled": false,
        "updated_at_ms": 0
      }
    },
    "server_time_ms": 1780228000000
  }
}
```

### 6.3 卸下体验卡

#### 接口地址

`DELETE /api/v1/vip/trial-cards/equipped`

#### 参数

无。

#### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_trial_unequip",
  "data": {
    "unequipped": true,
    "program_config": {
      "app_code": "fami",
      "program_type": "tiered_privilege_v1",
      "level_count": 9,
      "same_level_expiry_policy": "extend_remaining",
      "upgrade_expiry_policy": "replace_from_now",
      "downgrade_purchase_policy": "reject",
      "benefit_inheritance_policy": "target_only",
      "grant_mode": "trial_card",
      "trial_card_enabled": true,
      "status": "active",
      "config_version": 7
    },
    "state": {
      "paid_vip": {
        "user_id": 10001,
        "level": 3,
        "name": "VIP3",
        "active": true,
        "started_at_ms": 1780000000000,
        "expires_at_ms": 1782592000000,
        "updated_at_ms": 1780000000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "equipped_trial_card": null,
      "effective_vip": {
        "user_id": 10001,
        "level": 3,
        "name": "VIP3",
        "active": true,
        "started_at_ms": 1780000000000,
        "expires_at_ms": 1782592000000,
        "updated_at_ms": 1780000000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "effective_source": "paid",
      "effective_benefits": [],
      "evaluated_at_ms": 1780229000000,
      "program_config": {
        "app_code": "fami",
        "program_type": "tiered_privilege_v1",
        "level_count": 9,
        "same_level_expiry_policy": "extend_remaining",
        "upgrade_expiry_policy": "replace_from_now",
        "downgrade_purchase_policy": "reject",
        "benefit_inheritance_policy": "target_only",
        "grant_mode": "trial_card",
        "trial_card_enabled": true,
        "status": "active",
        "config_version": 7
      },
      "user_settings": {
        "app_code": "fami",
        "user_id": 10001,
        "room_entry_notice_enabled": true,
        "online_global_notice_enabled": true,
        "hide_profile_data_enabled": false,
        "anonymous_profile_visit_enabled": false,
        "leaderboard_invisible_enabled": false,
        "updated_at_ms": 0
      }
    },
    "server_time_ms": 1780229000000
  }
}
```

- 体验卡不能把当前 paid/trial 最终等级切低；同级或更高级卡仍按 `entitlement_id` 直接切换。
- 普通切换和卸下不会暂停、延长或删除体验卡；购买等级大于等于当前体验等级时会回收当前卡片及其体验资源。
- 每张卡的剩余时间按绝对 `expires_at_ms` 计算。
- 佩戴和卸下成功后立即使用响应 `state` 替换本地 VIP 状态。

## 7. VIP 功能开关

### 7.1 查询开关

#### 接口地址

`GET /api/v1/vip/settings`

#### 参数

无。

#### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_settings_get",
  "data": {
    "settings": {
      "app_code": "fami",
      "user_id": 10001,
      "room_entry_notice_enabled": true,
      "online_global_notice_enabled": false,
      "hide_profile_data_enabled": true,
      "anonymous_profile_visit_enabled": true,
      "leaderboard_invisible_enabled": true,
      "updated_at_ms": 1780300000000
    },
    "evaluated_at_ms": 1780300001000
  }
}
```

### 7.2 修改开关

#### 接口地址

`PATCH /api/v1/vip/settings`

#### 参数

局部更新，至少传一个字段：

```json
{
  "room_entry_notice_enabled": false,
  "online_global_notice_enabled": true,
  "hide_profile_data_enabled": false,
  "anonymous_profile_visit_enabled": true,
  "leaderboard_invisible_enabled": true
}
```

#### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_settings_patch",
  "data": {
    "settings": {
      "app_code": "fami",
      "user_id": 10001,
      "room_entry_notice_enabled": false,
      "online_global_notice_enabled": true,
      "hide_profile_data_enabled": false,
      "anonymous_profile_visit_enabled": true,
      "leaderboard_invisible_enabled": true,
      "updated_at_ms": 1780300010000
    },
    "server_time_ms": 1780300010000
  }
}
```

5 个字段都使用 PATCH 语义：不传表示保持原值，显式 `false` 表示关闭。没有持久记录时，进房/上线通知默认 `true`，隐藏个人数据/匿名访问/榜单隐身默认 `false`；设置只限制用户主动启用对应效果，不会单独授予 VIP 权益。服务端执行时使用“当前有效权益 AND 用户设置”判定，Flutter 不能只看开关。

匿名访问主页生效时，`POST /api/v1/users/{user_id}/visit` 返回 `recorded=false`，后端不新增、不刷新访客记录，也不增加目标用户的 `visitors_count`；客户端不能再自行发送第二次访问记录请求。

## 8. 每日 VIP 金币返现

### 8.1 查询当前返现

#### 接口地址

`GET /api/v1/vip/coin-rebates/current`

#### 参数

无。

#### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_rebate_current",
  "data": {
    "found": true,
    "rebate": {
      "rebate_id": "vip_rebate_xxx",
      "user_id": "10001",
      "task_day": "2026-07-15",
      "vip_level": 6,
      "vip_name": "VIP6",
      "coin_amount": 100,
      "status": "claimable",
      "available_at_ms": 1784073600000,
      "expires_at_ms": 1784160000000,
      "config_version": 8,
      "claimed_at_ms": 0,
      "wallet_transaction_id": "",
      "created_at_ms": 1784073605000,
      "updated_at_ms": 1784073605000
    },
    "server_time_ms": 1784073610000
  }
}
```

无资格时 `found=false`、`rebate=null`。

### 8.2 批量查询返现状态

#### 接口地址

`POST /api/v1/vip/coin-rebates/statuses`

#### 参数

```json
{
  "rebate_ids": ["vip_rebate_xxx", "vip_rebate_yyy"],
  "page": 1,
  "page_size": 20
}
```

- `rebate_ids` 最多 100 个。
- 不传 `rebate_ids` 时按 `page/page_size` 查询历史。
- `page_size` 最大 100。

#### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_rebate_statuses",
  "data": {
    "rebates": [
      {
        "rebate_id": "vip_rebate_xxx",
        "task_day": "2026-07-15",
        "vip_level": 6,
        "vip_name": "VIP6",
        "coin_amount": 100,
        "status": "claimable",
        "available_at_ms": 1784073600000,
        "expires_at_ms": 1784160000000,
        "claimed_at_ms": 0,
        "wallet_transaction_id": ""
      }
    ],
    "total": 1,
    "server_time_ms": 1784073610000
  }
}
```

### 8.3 领取返现

#### 接口地址

`POST /api/v1/vip/coin-rebates/{rebate_id}/claim`

#### 参数

| 参数 | 位置 | 必填 | 说明 |
|---|---|---|---|
| `rebate_id` | path | 是 | 从 current、statuses 或系统消息取得 |

```json
{
  "command_id": "vip_rebate_claim_10001_20260715"
}
```

#### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_rebate_claim",
  "data": {
    "rebate": {
      "rebate_id": "vip_rebate_xxx",
      "task_day": "2026-07-15",
      "vip_level": 6,
      "vip_name": "VIP6",
      "coin_amount": 100,
      "status": "claimed",
      "claimed_at_ms": 1784073700000,
      "wallet_transaction_id": "wallet_tx_xxx"
    },
    "transaction_id": "wallet_tx_xxx",
    "coin_balance": {
      "asset_type": "COIN",
      "available_amount": 20100,
      "frozen_amount": 0,
      "version": 18
    },
    "server_time_ms": 1784073700000
  }
}
```

## 9. 上线全服通知

### 接口地址

`POST /api/v1/vip/online-notice`

### 参数

```json
{
  "command_id": "vip_online_<process_boot_id>_<session_nonce>_2026-07-15"
}
```

- `command_id` 最长 128 字节。
- 同一登录会话同一 UTC 日复用同一个 `command_id`。
- `process_boot_id` 和 `session_nonce` 只保存在内存中，不写磁盘；退出登录或会话终态失效时必须轮换 `session_nonce`。

### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_online_notice",
  "data": {
    "event_id": "vip_online_notice:10001:sha256_digest",
    "group_id": "hy_fami_bc_g_v2",
    "status": "pending",
    "created": true,
    "evaluated_at_ms": 1784073610000
  }
}
```

- `status=pending` 表示事件已进入异步投递链路。
- 幂等命中时 `created=false`，不是失败。
- 同一进程每天触发一次；杀掉进程后新进程当天可以再次触发。

## 10. 进房 VIP 通知

### 接口地址

`POST /api/v1/rooms/join`

### 参数

```json
{
  "room_id": "room_1001",
  "command_id": "join_10001_room_1001_1784073600000",
  "role": "audience",
  "password": "",
  "response_mode": "full"
}
```

### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_join",
  "data": {
    "result": {
      "applied": true,
      "room_version": 18,
      "server_time_ms": 1784073600000
    },
    "room": {},
    "viewer": {},
    "seats": [],
    "profiles": [],
    "im": {
      "group_id": "room_1001",
      "need_join_group": true
    },
    "effective_vip": {
      "program_type": "tiered_privilege_v1",
      "level": 8,
      "name": "VIP8",
      "room_entry_notice_enabled": true
    },
    "server_time_ms": 1784073600000
  }
}
```

本人进房首屏使用 HTTP 返回的 `data.effective_vip`。其他用户的进房效果使用当前房间 IM 消息。

## 11. VIP 装扮资源

### 11.1 我的资源列表

#### 接口地址

`GET /api/v1/users/me/resources?resource_type={resource_type}`

#### 参数

| 参数 | 位置 | 必填 | 说明 |
|---|---|---|---|
| `resource_type` | query | 否 | `avatar_frame`、`profile_card`、`vehicle`、`chat_bubble` 等资源类型 |

#### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_resources",
  "data": {
    "items": [
      {
        "entitlement_id": "ent_xxx",
        "resource_id": 108,
        "resource": {
          "resource_id": 108,
          "resource_code": "frame_vip9",
          "resource_type": "avatar_frame",
          "name": "VIP头像框",
          "status": "active",
          "asset_url": "https://cdn.example/frame.png",
          "preview_url": "https://cdn.example/frame-preview.png",
          "animation_url": "https://cdn.example/frame.svga",
          "metadata_json": "{}",
          "sort_order": 10
        },
        "status": "active",
        "expires_at_ms": 1786665600000,
        "equipped": true
      }
    ],
    "total": 1
  }
}
```

### 11.2 佩戴资源

#### 接口地址

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

#### 参数

| 参数 | 位置 | 必填 | 说明 |
|---|---|---|---|
| `resource_id` | path | 是 | 资源 ID |

无 body。

### 11.3 卸下资源

#### 接口地址

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

#### 参数

| 参数 | 位置 | 必填 | 说明 |
|---|---|---|---|
| `resource_type` | path | 是 | 资源类型 |

无 body。

### 11.4 当前装扮

#### 接口地址

- `GET /api/v1/appearance`
- `GET /api/v1/users/by-id/{user_id}/appearance`

#### 参数

第二个接口使用 path 参数 `user_id`。

#### 返回值

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_appearance",
  "data": {
    "user_id": "10001",
    "avatar_frame": {},
    "profile_card": {},
    "vehicle": {},
    "mic_seat_icon": {},
    "mic_seat_animation": {},
    "equipped_badges": [],
    "badges": [],
    "resources": [],
    "server_time_ms": 1784073600000
  }
}
```

## 12. 相关 IM

### 12.1 VIP 激活通知

通道：腾讯云 IM C2C `TIMCustomElem`。

```json
{
  "Ext": "vip_notice",
  "Desc": "VipActivated",
  "Data": "{\"event_type\":\"VipActivated\",\"event_id\":\"evt_xxx\",\"app_code\":\"fami\",\"transaction_id\":\"tx_xxx\",\"command_id\":\"vip_purchase_10001_4_1780100000000\",\"user_id\":\"10001\",\"source\":\"vip_purchase\",\"action\":\"upgrade\",\"previous_level\":3,\"previous_expires_at_ms\":1781396000000,\"level\":4,\"vip_name\":\"VIP4\",\"started_at_ms\":1780100000000,\"expires_at_ms\":1782692000000,\"program_type\":\"tiered_privilege_v1\",\"config_version\":7,\"expiry_policy\":\"replace_from_now\",\"created_at_ms\":1780100000000}"
}
```

Flutter 按 `event_id` 去重，然后调用 `GET /api/v1/vip/me`，不能直接使用 IM 内容生成权限。

### 12.2 进房通知

通道：当前房间腾讯云 IM 群 `TIMCustomElem`，不发送到全服群。

```json
{
  "Ext": "room_system_message",
  "Desc": "room_user_joined",
  "Data": "{\"event_id\":\"evt_join_xxx\",\"room_id\":\"room_1001\",\"event_type\":\"room_user_joined\",\"actor_user_id\":10001,\"target_user_id\":10001,\"room_version\":18,\"role\":\"audience\",\"nickname\":\"Nina\",\"avatar\":\"https://cdn.example/avatar.png\",\"display_user_id\":\"100042\",\"pretty_display_user_id\":\"888042\",\"vip_program_type\":\"tiered_privilege_v1\",\"effective_vip_level\":\"8\",\"effective_vip_name\":\"VIP8\",\"room_entry_notice_enabled\":\"true\"}"
}
```

仅当 `event_type=room_user_joined` 且 `room_entry_notice_enabled=true` 时展示 VIP 进房效果。使用 `event_id` 去重。

### 12.3 上线全服通知

通道：全局播报群 `TIMCustomElem`。

| 字段 | 值 |
|---|---|
| `Ext` | `im_broadcast` |
| `Desc` | `vip_online_notice` |
| `Data` | 下方 JSON 字符串 |
| `CloudCustomData` | 与 `Data` 相同 |

```json
{
  "event_id": "vip_online_notice:10001:sha256_digest",
  "broadcast_type": "vip_online_notice",
  "scope": "global",
  "app_code": "fami",
  "user_id": 10001,
  "sender_user_id": 10001,
  "nickname": "Nina",
  "avatar": "https://cdn.example/avatar.png",
  "avatar_frame_url": "https://cdn.example/frame.png",
  "avatar_frame_resource_id": 108,
  "avatar_frame_code": "frame_vip9",
  "avatar_frame_asset_url": "https://cdn.example/frame.png",
  "avatar_frame_preview_url": "https://cdn.example/frame-preview.png",
  "avatar_frame_animation_url": "https://cdn.example/frame-animation.svga",
  "display_user_id": "888042",
  "raw_display_user_id": "100042",
  "pretty_display_user_id": "888042",
  "vip_program_type": "tiered_privilege_v1",
  "vip_level": 9,
  "vip_name": "VIP9",
  "message": "欢迎进入Fami，祝你有美好的一天",
  "action": {
    "type": "none"
  },
  "sent_at_ms": 1784073610500
}
```

Flutter 使用 `event_id` 去重，直接使用事件内的用户、VIP 和头像框快照渲染。

### 12.4 金币返现系统消息

接口：`GET /api/v1/messages?section=system&page_size=20&page_token=`。

```json
{
  "message_id": "msg_vip_rebate_xxx",
  "title": "VIP Coin Cashback",
  "summary": "Your daily VIP cashback of 100 coins is ready. Claim it before it expires.",
  "body": "Your daily VIP cashback of 100 coins is ready. Claim it before it expires.",
  "icon_url": "",
  "image_url": "",
  "action_type": "vip_coin_rebate_claim",
  "action_param": "{\"coin_amount\":100,\"expires_at_ms\":1784160000000,\"rebate_id\":\"vip_rebate_xxx\",\"task_day\":\"2026-07-15\"}",
  "read": false,
  "sent_at_ms": 1784073600000
}
```

Flutter 将 `action_param` 解析为 JSON，打开页面后调用 `/vip/coin-rebates/statuses` 获取真实领取状态。

### 12.5 IM 登录与全局群

接口：`GET /api/v1/im/usersig`。

Flutter 使用返回的 UserSig 登录腾讯云 IM，并加入响应 `join_groups` 中 `type=global_broadcast` 的群组后，再消费 `vip_online_notice`。

## 13. 稳定业务错误码

失败仍使用统一 envelope，Flutter 只判断 `code`，不得解析 `message`：

```json
{
  "code": "VIP_INSUFFICIENT_COIN",
  "message": "insufficient coin balance",
  "request_id": "req_purchase",
  "data": null
}
```

| HTTP | `code` | 场景 | Flutter 处理 |
|---:|---|---|---|
| 409 | `VIP_PROGRAM_INACTIVE` | 当前 App 的 VIP 体系未启用 | 关闭购买/体验卡操作并刷新配置 |
| 409 | `VIP_PACKAGE_NOT_PURCHASABLE` | 套餐不存在、停用或不在当前体系范围 | 刷新 `/vip/packages` |
| 409 | `VIP_DOWNGRADE_NOT_ALLOWED` | 购买等级低于当前付费等级 | 提示不可降级，刷新 `/vip/me` |
| 409 | `VIP_RECHARGE_REQUIRED` | 未达到套餐充值门槛 | 展示充值引导 |
| 409 | `VIP_INSUFFICIENT_COIN` | 购买金币余额不足 | 展示金币充值入口 |
| 404 | `VIP_TRIAL_CARD_NOT_FOUND` | 卡不存在、不属于本人或背包权益已撤销 | 刷新体验卡列表和 `/vip/me` |
| 409 | `VIP_TRIAL_CARD_EXPIRED` | 卡或背包权益已越过绝对截止时间 | 刷新体验卡列表和 `/vip/me` |
| 403 | `VIP_BENEFIT_REQUIRED` | 操作需要的权益未生效或被用户关闭 | 刷新 `/vip/me`，保持入口锁态 |
| 403 | `VIP_ANTI_KICK` | 普通房主/管理员踢人被目标用户防踢拦截 | 提示目标不可被踢出，不改变房间本地状态 |
| 403 | `VIP_ANTI_MUTE` | 普通房主/管理员禁言被目标用户防禁言拦截 | 提示目标不可被禁言，不改变房间本地状态 |
| 404 | `VIP_COIN_REBATE_NOT_FOUND` | 返现不存在或不属于当前用户/App | 刷新返现列表 |
| 409 | `VIP_COIN_REBATE_EXPIRED` | 已越过 UTC 次日 0 点的领取结束边界 | 标记过期并刷新返现列表 |
| 409 | `IDEMPOTENCY_CONFLICT` | 同一 `command_id` 被用于不同业务参数 | 停止重试，为新动作生成新 ID |
| 409 | `LEDGER_CONFLICT` | 钱包并发版本冲突 | 刷新余额和 `/vip/me` 后由用户重新发起 |

返现重复领取采用幂等成功语义：相同 `command_id` 重试返回首次成功回执，不返回 `VIP_REBATE_ALREADY_CLAIMED`。因此客户端不应实现该错误分支。
