# Flutter 礼物墙对接

本文描述 Flutter App 查询用户礼物墙的 HTTP 接口。礼物墙事实由 `wallet-service` 在送礼扣费成功后通过异步投影聚合，客户端只展示 gateway 返回的当前投影，不从房间消息或本地缓存推算收礼数量。

## 接口

`GET /api/v1/users/me/gift-wall`

`GET /api/v1/users/{user_id}/gift-wall`

登录接口，需要 `Authorization: Bearer <access_token>`。

- `GET /api/v1/users/me/gift-wall` 查询当前登录用户。
- `GET /api/v1/users/{user_id}/gift-wall` 查询资料卡目标用户，`user_id` 是路径参数，必须是正整数。

一致性边界：礼物墙是最终一致读模型，送礼成功后允许延迟数秒到几十秒才出现在接口结果中。客户端不要用该接口判断刚刚送礼是否成功；送礼成功状态仍以送礼接口响应和房间 IM 消息为准。

请求头：

| Header | 必填 | 说明 |
| --- | --- | --- |
| `Authorization` | 是 | App 登录 access token。 |
| `X-App-Code` | 否 | App 编码，默认 `lalu`。 |

示例：

```http
GET /api/v1/users/me/gift-wall
Authorization: Bearer <access_token>
X-App-Code: lalu
```

```http
GET /api/v1/users/10002/gift-wall
Authorization: Bearer <access_token>
X-App-Code: lalu
```

成功响应：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_abc",
  "data": {
    "items": [
      {
        "gift_id": "rose",
        "gift_name": "Rose",
        "resource_id": 11,
        "resource": {
          "resource_id": 11,
          "resource_code": "gift_rose",
          "resource_type": "gift",
          "name": "Rose",
          "status": "active",
          "grantable": true,
          "grant_strategy": "increase_quantity",
          "usage_scopes": ["gift"],
          "asset_url": "https://cdn.example/rose.png",
          "preview_url": "",
          "animation_url": "",
          "metadata_json": "{}",
          "sort_order": 10,
          "created_at_ms": 1710000000000,
          "updated_at_ms": 1710000000000
        },
        "resource_snapshot_json": "{\"ResourceID\":11}",
        "gift_type_code": "normal",
        "presentation_json": "{}",
        "gift_count": 5,
        "total_value": 50,
        "total_coin_value": 50,
        "total_diamond_value": 0,
        "total_gift_point": 50,
        "total_heat_value": 50,
        "charge_asset_type": "COIN",
        "last_price_version": "v1",
        "first_received_at_ms": 1710000000000,
        "last_received_at_ms": 1710000005000,
        "sort_order": 10
      }
    ],
    "gift_kind_count": 1,
    "gift_total_count": 5,
    "total_value": 50,
    "total_coin_value": 50,
    "total_diamond_value": 0,
    "total_gift_point": 50,
    "total_heat_value": 50
  }
}
```

字段规则：

| 字段 | 说明 |
| --- | --- |
| `items` | 按 `gift_id` 聚合后的礼物列表，固定按后台礼物排序、价值和最近收礼时间排序。 |
| `gift_count` | 当前用户收到该礼物的累计个数。 |
| `gift_kind_count` | 当前用户收到过的礼物种类数，等于 `items.length`。 |
| `gift_total_count` | 当前用户收到的所有礼物数量总和。 |
| `total_value` | 礼物总价值，按送礼时服务端结算的 `charge_amount` 累加。 |
| `total_coin_value` / `total_diamond_value` | 按收费资产拆分的总价值；当前普通礼物通常只读 `total_coin_value`。 |
| `total_gift_point` | 主播礼物积分累计值，不等同于可提现余额。 |
| `total_heat_value` | 房间热度累计值，可用于展示贡献热度，不要反推价格。 |
| `charge_asset_type` | 该礼物历史上只用一种资产收费时为 `COIN` 或 `DIAMOND`；发生过资产切换时为 `MIXED`。 |
| `resource` | 礼物展示资源快照，客户端优先用 `asset_url` / `preview_url` / `animation_url` 渲染。 |
| `resource_snapshot_json` | 原始资源快照 JSON，主要用于客户端兜底和日志排查。 |
| `*_ms` | Unix epoch milliseconds；展示层按用户本地时区格式化。 |

## Flutter 解析示例

```dart
class GiftWall {
  GiftWall({
    required this.items,
    required this.giftKindCount,
    required this.giftTotalCount,
    required this.totalValue,
  });

  final List<GiftWallItem> items;
  final int giftKindCount;
  final int giftTotalCount;
  final int totalValue;

  factory GiftWall.fromJson(Map<String, dynamic> json) {
    return GiftWall(
      items: (json['items'] as List<dynamic>? ?? const [])
          .map((item) => GiftWallItem.fromJson(item as Map<String, dynamic>))
          .toList(),
      giftKindCount: (json['gift_kind_count'] as num?)?.toInt() ?? 0,
      giftTotalCount: (json['gift_total_count'] as num?)?.toInt() ?? 0,
      totalValue: (json['total_value'] as num?)?.toInt() ?? 0,
    );
  }
}

class GiftWallItem {
  GiftWallItem({
    required this.giftId,
    required this.giftName,
    required this.giftCount,
    required this.totalValue,
    required this.assetUrl,
  });

  final String giftId;
  final String giftName;
  final int giftCount;
  final int totalValue;
  final String assetUrl;

  factory GiftWallItem.fromJson(Map<String, dynamic> json) {
    final resource = json['resource'] as Map<String, dynamic>? ?? const {};
    return GiftWallItem(
      giftId: json['gift_id'] as String? ?? '',
      giftName: json['gift_name'] as String? ?? '',
      giftCount: (json['gift_count'] as num?)?.toInt() ?? 0,
      totalValue: (json['total_value'] as num?)?.toInt() ?? 0,
      assetUrl: resource['asset_url'] as String? ?? '',
    );
  }
}
```

调用时先解析外层 envelope：只有 `code == "OK"` 时读取 `data`；其他错误按统一网关错误处理，并把 `request_id` 写入客户端日志。
