# 幸运礼物 Flutter 对接

本文只保留 App 对接需要的信息：地址、参数、返回值、IM 字段。服务端实现细节看 [幸运礼物实现文档](../幸运礼物实现文档.md)，礼物面板通用字段看 [礼物 Tab Flutter 对接](礼物TabFlutter对接.md)。

## 1. 送礼前检查

地址：

```http
POST /api/v1/activities/lucky-gifts/check
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```

请求参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID。 |
| `gift_id` | string | 是 | 礼物 ID。 |
| `pool_id` | string | 是 | 幸运礼物奖池 ID，例如 `lucky`、`super_lucky` 或后台配置值。 |

请求示例：

```json
{
  "room_id": "lalu_room_001",
  "gift_id": "rose",
  "pool_id": "super_lucky"
}
```

返回值：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `enabled` | bool | 是否可参与幸运礼物抽奖。 |
| `reason` | string | 不可参与原因；可为空。 |
| `room_id` | string | 房间 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `pool_id` | string | 奖池 ID。 |
| `gift_price` | int64 | 当前规则参考礼物价格，只展示用，扣费以后端送礼结果为准。 |
| `rule_version` | int64 | 命中的幸运礼物规则版本。 |
| `target_rtp_ppm` | int64 | 目标 RTP，ppm 口径；`950000` 表示 95%。 |
| `experience_pool` | string | 当前用户体验池，例如 `novice`、`normal`、`advanced`。 |

返回示例：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_abc",
  "data": {
    "enabled": true,
    "reason": "",
    "room_id": "lalu_room_001",
    "gift_id": "rose",
    "pool_id": "super_lucky",
    "gift_price": 100,
    "rule_version": 12,
    "target_rtp_ppm": 950000,
    "experience_pool": "novice"
  }
}
```

## 2. 发送幸运礼物

地址：

```http
POST /api/v1/rooms/gift/send
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```

请求参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID。 |
| `command_id` | string | 是 | 本次送礼幂等键；同一次重试必须复用。 |
| `target_type` | string | 否 | 当前固定传 `user`；为空后端按 `user`。 |
| `target_user_ids` | int64[] | 是 | 收礼用户列表；当前只传 1 个用户。 |
| `target_user_id` | int64 | 否 | 旧单目标兼容字段，新代码优先用 `target_user_ids`。 |
| `gift_id` | string | 是 | 礼物 ID。 |
| `gift_count` | int32 | 是 | 礼物数量，必须大于 0；批量送礼会聚合返回一次中奖结果。 |
| `pool_id` | string | 是 | 幸运礼物奖池 ID；普通礼物不传。 |

请求示例：

```json
{
  "room_id": "lalu_room_001",
  "command_id": "gift_1779259000000_10001_rose_01",
  "target_type": "user",
  "target_user_ids": [10002],
  "gift_id": "rose",
  "gift_count": 9,
  "pool_id": "super_lucky"
}
```

返回值：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `result.applied` | bool | 本次命令是否实际落地；重复提交同一 `command_id` 可能为 `false`。 |
| `result.room_version` | int64 | 房间版本。 |
| `result.server_time_ms` | int64 | 服务端时间。 |
| `billing_receipt_id` | string | 送礼扣费回执。 |
| `room_heat` | int64 | 送礼后的房间热度。 |
| `gift_rank` | array | 当前房间礼物榜。 |
| `room` | object | 房间快照。 |
| `rocket` | object | 房间火箭状态。 |
| `lucky_gift` | object | 幸运礼物抽奖结果；普通礼物可为空。 |

`lucky_gift` 返回字段：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `enabled` | bool | 本次是否触发幸运礼物。 |
| `draw_id` | string | 聚合抽奖 ID。 |
| `command_id` | string | 本次送礼命令 ID。 |
| `pool_id` | string | 奖池 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `rule_version` | int64 | 规则版本。 |
| `experience_pool` | string | 用户体验池。 |
| `selected_tier_id` | string | 命中奖档 ID。 |
| `multiplier_ppm` | int64 | 倍率 ppm；`2000000` 表示 2x。 |
| `base_reward_coins` | int64 | 基础奖池返奖金币。 |
| `room_atmosphere_reward_coins` | int64 | 房间气氛池返奖金币。 |
| `activity_subsidy_coins` | int64 | 活动补贴返奖金币。 |
| `effective_reward_coins` | int64 | 实际中奖金币；0 表示未中奖。 |
| `reward_status` | string | `granted` 已入账；`pending`/`granting` 到账处理中；`failed` 需要人工处理。 |
| `stage_feedback` | bool | 是否有阶段反馈表现。 |
| `high_multiplier` | bool | 是否高倍率中奖。 |
| `created_at_ms` | int64 | 抽奖创建时间。 |
| `wallet_transaction_id` | string | 返奖钱包交易 ID；已入账时有值。 |
| `coin_balance_after` | int64 | 返奖入账后的 COIN 可用余额。 |

返回示例：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_abc",
  "data": {
    "result": {
      "applied": true,
      "room_version": 14,
      "server_time_ms": 1779259000000
    },
    "billing_receipt_id": "receipt-lucky",
    "room_heat": 123456,
    "gift_rank": [],
    "room": {},
    "rocket": {},
    "lucky_gift": {
      "enabled": true,
      "draw_id": "lucky_draw_test",
      "command_id": "gift_1779259000000_10001_rose_01",
      "pool_id": "super_lucky",
      "gift_id": "rose",
      "rule_version": 12,
      "experience_pool": "novice",
      "selected_tier_id": "novice_2x",
      "multiplier_ppm": 2000000,
      "base_reward_coins": 1800,
      "room_atmosphere_reward_coins": 0,
      "activity_subsidy_coins": 0,
      "effective_reward_coins": 1800,
      "reward_status": "granted",
      "stage_feedback": false,
      "high_multiplier": false,
      "created_at_ms": 1779259000000,
      "wallet_transaction_id": "wallet_tx_lucky",
      "coin_balance_after": 8800
    }
  }
}
```

客户端处理：

- `reward_status=granted` 且 `effective_reward_coins > 0`：立即展示中奖，并用 `coin_balance_after` 覆盖本地金币余额。
- `reward_status=pending` 或 `granting`：展示“到账处理中”，不要本地加金币，不要生成新 `command_id` 重发。
- `effective_reward_coins=0`：未中奖，只展示普通送礼表现。
- 同一用户同一房间连续发送幸运礼物时，客户端需要本地排队；重试必须复用原 `command_id`。

## 3. IM 字段

### 3.1 送礼房间 IM

用途：房间内播放普通送礼动效、同步热度和礼物榜。幸运礼物也会先产生这条送礼 IM。

| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `room_gift_sent` |
| `Data.event_type` | `room_gift_sent` |

`Data` 核心字段：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | string | 房间事件 ID，用于去重。 |
| `room_id` | string | 房间 ID。 |
| `room_version` | int64 | 房间版本。 |
| `occurred_at_ms` | int64 | 事件发生时间。 |
| `actor_user_id` | string/int64 | 送礼用户 ID。 |
| `target_user_id` | string/int64 | 收礼用户 ID。 |
| `gift_value` | int64 | 本次送礼房间价值/热度口径。 |
| `room_heat` | int64 | 送礼后的房间热度。 |
| `sender_vip_level` | string | 送礼人发送瞬间的生效 VIP 等级；`0` 或缺失表示无有效 VIP。 |
| `attributes.gift_id` | string | 礼物 ID。 |
| `attributes.pool_id` | string | 幸运礼物奖池 ID；普通礼物可为空。 |
| `attributes.gift_count` | string | 礼物数量，字符串格式。 |
| `attributes.billing_receipt_id` | string | 钱包扣费回执。 |
| `attributes.coin_spent` | string | 本次实际消耗金币，字符串格式。 |
| `attributes.gift_point_added` | string | 主播礼物积分增量，字符串格式。 |
| `attributes.price_version` | string | 价格版本。 |

示例：

```json
{
  "event_type": "room_gift_sent",
  "event_id": "room_event_xxx",
  "room_id": "lalu_room_001",
  "room_version": 14,
  "occurred_at_ms": 1779259000000,
  "actor_user_id": "10001",
  "target_user_id": "10002",
  "gift_value": 900,
  "room_heat": 123456,
  "sender_vip_level": "7",
  "attributes": {
    "gift_id": "rose",
    "pool_id": "super_lucky",
    "gift_count": "9",
    "billing_receipt_id": "receipt-lucky",
    "coin_spent": "900",
    "gift_point_added": "900",
    "price_version": "v1",
    "sender_vip_level": "7"
  }
}
```

`RoomEvent` 会把 `attributes` 展示字段同时铺到顶层。Flutter 优先解析顶层 `sender_vip_level`，兼容回退到 `attributes.sender_vip_level`，使用 `int.tryParse` 转为等级；后端不下发 VIP 色值。

### 3.2 幸运礼物开奖 IM

用途：房间公屏展示幸运礼物中奖结果、其他用户同步中奖表现。当前用户自己的余额以 HTTP 返回的 `coin_balance_after` 为准，不等这条 IM。

| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `lucky_gift_drawn` |
| `Data.event_type` | `lucky_gift_drawn` |

`Data` 核心字段：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | string | 开奖事件 ID，用于去重。 |
| `app_code` | string | App Code。 |
| `draw_id` | string | 聚合抽奖 ID。 |
| `command_id` | string | 送礼命令 ID。 |
| `pool_id` | string | 奖池 ID。 |
| `room_id` | string | 房间 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `gift_count` | int32 | 礼物数量。 |
| `user_id` | int64 | 中奖用户 ID。 |
| `sender_user_id` | int64 | 送礼用户 ID。 |
| `target_user_id` | int64 | 收礼用户 ID。 |
| `visible_region_id` | int64 | 展示区域 ID。 |
| `coin_spent` | int64 | 本次送礼消耗金币。 |
| `rule_version` | int64 | 规则版本。 |
| `experience_pool` | string | 用户体验池。 |
| `selected_tier_id` | string | 命中奖档 ID。 |
| `multiplier_ppm` | int64 | 命中倍率 ppm。 |
| `base_reward_coins` | int64 | 基础奖池返奖金币。 |
| `room_atmosphere_reward_coins` | int64 | 房间气氛池返奖金币。 |
| `activity_subsidy_coins` | int64 | 活动补贴返奖金币。 |
| `effective_reward_coins` | int64 | 实际中奖金币。 |
| `stage_feedback` | bool | 是否有阶段反馈表现。 |
| `high_multiplier` | bool | 是否高倍率中奖。 |
| `created_at_ms` | int64 | 抽奖创建时间。 |

示例：

```json
{
  "event_type": "lucky_gift_drawn",
  "event_id": "lucky_gift_drawn:lucky_draw_test",
  "app_code": "lalu",
  "draw_id": "lucky_draw_test",
  "command_id": "gift_1779259000000_10001_rose_01",
  "pool_id": "super_lucky",
  "room_id": "lalu_room_001",
  "gift_id": "rose",
  "gift_count": 9,
  "user_id": 10001,
  "sender_user_id": 10001,
  "target_user_id": 10002,
  "visible_region_id": 688,
  "coin_spent": 900,
  "rule_version": 12,
  "experience_pool": "novice",
  "selected_tier_id": "novice_2x",
  "multiplier_ppm": 2000000,
  "base_reward_coins": 1800,
  "room_atmosphere_reward_coins": 0,
  "activity_subsidy_coins": 0,
  "effective_reward_coins": 1800,
  "stage_feedback": false,
  "high_multiplier": false,
  "created_at_ms": 1779259000000
}
```

## 4. 状态处理

| 场景 | App 处理 |
| --- | --- |
| `lucky_gift` 为空 | 当普通礼物处理。 |
| `effective_reward_coins=0` | 未中奖，不更新中奖余额。 |
| `reward_status=granted` | 已入账；用 `coin_balance_after` 更新余额。 |
| `reward_status=pending` / `granting` | 到账处理中；等待钱包通知、刷新余额或后端补偿。 |
| `reward_status=failed` | 展示异常状态，记录 `request_id/draw_id/command_id` 方便排查。 |
| HTTP 超时 | 保留原 `command_id` 重试，不生成新命令。 |
