# Flutter 金币流水对接

本文只描述 Flutter App 读取当前登录用户金币流水的接口。金币余额和流水事实由 `wallet-service` 维护；客户端只展示 gateway 返回的分录投影，不在本地推算余额。

## 接口

`GET /api/v1/wallet/coin-transactions`

登录接口，需要 `Authorization: Bearer <access_token>`。接口固定查询当前登录用户的 `COIN` 资产流水，不接受客户端传 `user_id` 或 `asset_type`。

请求头：

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

Query：

| 字段 | 必填 | 默认 | 说明 |
| --- | --- | --- | --- |
| `page` | 否 | `1` | 页码，从 1 开始。 |
| `page_size` | 否 | `30` | 每页数量。首屏不传时返回最近 30 条。 |

排序固定为 `created_at_ms DESC, entry_id DESC`，即最新流水在前。分页字段使用通用 `page` / `page_size`，不要使用业务专属分页名。

示例：

```http
GET /api/v1/wallet/coin-transactions?page=1&page_size=30
Authorization: Bearer <access_token>
X-App-Code: lalu
```

成功响应：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_abc",
  "data": {
    "items": [
      {
        "entry_id": 101,
        "transaction_id": "wtx_abc",
        "biz_type": "coin_seller_transfer",
        "asset_type": "COIN",
        "available_delta": 3000,
        "frozen_delta": 0,
        "available_after": 12800,
        "frozen_after": 0,
        "counterparty_user_id": 90001,
        "room_id": "",
        "created_at_ms": 1710000000000
      }
    ],
    "total": 48,
    "page": 1,
    "page_size": 30
  }
}
```

字段规则：

| 字段 | 说明 |
| --- | --- |
| `available_delta` | 本次可用金币变化；正数是收入，负数是支出。 |
| `available_after` | 本条流水落账后的可用金币余额，余额展示以该字段为准。 |
| `biz_type` | 业务类型，例如 `coin_seller_transfer`、`gift_debit`、`task_reward`、`game_credit`、`game_debit`、`game_refund`、`vip_purchase`。客户端可本地映射展示文案。 |
| `counterparty_user_id` | 对手用户长 ID；没有对手方时为 `0`。 |
| `room_id` | 房间相关流水的房间 ID；非房间流水为空字符串。 |
| `created_at_ms` | Unix epoch milliseconds；展示层按用户本地时区格式化。 |

## Flutter 解析示例

```dart
class CoinTransactionPage {
  CoinTransactionPage({
    required this.items,
    required this.total,
    required this.page,
    required this.pageSize,
  });

  final List<CoinTransaction> items;
  final int total;
  final int page;
  final int pageSize;

  factory CoinTransactionPage.fromJson(Map<String, dynamic> json) {
    return CoinTransactionPage(
      items: (json['items'] as List<dynamic>? ?? const [])
          .map((item) => CoinTransaction.fromJson(item as Map<String, dynamic>))
          .toList(),
      total: (json['total'] as num?)?.toInt() ?? 0,
      page: (json['page'] as num?)?.toInt() ?? 1,
      pageSize: (json['page_size'] as num?)?.toInt() ?? 30,
    );
  }
}

class CoinTransaction {
  CoinTransaction({
    required this.entryId,
    required this.transactionId,
    required this.bizType,
    required this.availableDelta,
    required this.availableAfter,
    required this.createdAtMs,
  });

  final int entryId;
  final String transactionId;
  final String bizType;
  final int availableDelta;
  final int availableAfter;
  final int createdAtMs;

  bool get isIncome => availableDelta > 0;
  int get amount => availableDelta.abs();

  factory CoinTransaction.fromJson(Map<String, dynamic> json) {
    return CoinTransaction(
      entryId: (json['entry_id'] as num).toInt(),
      transactionId: json['transaction_id'] as String? ?? '',
      bizType: json['biz_type'] as String? ?? '',
      availableDelta: (json['available_delta'] as num?)?.toInt() ?? 0,
      availableAfter: (json['available_after'] as num?)?.toInt() ?? 0,
      createdAtMs: (json['created_at_ms'] as num?)?.toInt() ?? 0,
    );
  }
}
```

接口调用时，先解析外层 envelope：只有 `code == "OK"` 时读取 `data`；其他 `code` 按登录态、参数错误或服务错误处理，并把 `request_id` 记录到客户端日志。
