# 成就徽章 Flutter 对接

## 地址

Base URL：

```text
https://<gateway-host>
```

本地开发：

```text
http://127.0.0.1:13000
```

| 功能 | Method | Path |
| --- | --- | --- |
| 我的成就列表 | `GET` | `/api/v1/achievements` |
| 我的徽章展示 | `GET` | `/api/v1/badges/me` |
| 设置方形徽章展示槽 | `POST` | `/api/v1/badges/display` |

## 通用请求

Headers：

| Header | 必填 | 说明 |
| --- | --- | --- |
| `Authorization` | 是 | `Bearer <access_token>` |
| `Content-Type` | POST 必填 | `application/json` |
| `X-App-Code` | 否 | App 编码，默认 `lalu` |
| `X-App-Package` | 否 | 包名解析 App 时使用 |
| `X-App-Platform` | 否 | `android` / `ios` |

统一返回：

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

失败返回：

```json
{
  "code": "INVALID_ARGUMENT",
  "message": "invalid argument",
  "request_id": "req_xxx"
}
```

## 请求参数

### `GET /api/v1/achievements`

Query：

| 字段 | 必填 | 默认 | 说明 |
| --- | --- | --- | --- |
| `collection_type` | 否 | 全部 | 普通成就传 `ordinary` |
| `collection_id` | 否 | 全部 | 成就集合 ID |
| `page` | 否 | `1` | 页码，从 1 开始 |
| `page_size` | 否 | `20` | 每页数量 |

### `GET /api/v1/badges/me`

无 query。

### `POST /api/v1/badges/display`

Body：

```json
{
  "slot": "profile_tile",
  "command_id": "cmd_badge_display_1710000000000",
  "items": [
    {
      "position": 1,
      "resource_id": 8001,
      "entitlement_id": "ent_xxx"
    }
  ]
}
```

| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `slot` | 是 | 当前只允许 `profile_tile` |
| `command_id` | 是 | 客户端本次操作幂等 ID；同一次重试使用同一个值 |
| `items` | 是 | 展示槽完整替换；最多 6 个 |
| `items[].position` | 否 | 从 1 开始；不传按数组顺序生成 |
| `items[].resource_id` | 是 | 必须来自用户已获得的可展示徽章 |
| `items[].entitlement_id` | 否 | 有则透传 |

## 返回值

以下 JSON 均为统一 envelope 的 `data`。

### 我的成就列表

```json
{
  "achievements": [
    {
      "definition": {
        "achievement_id": "ach_first_gift",
        "collection_id": "ordinary",
        "collection_type": "ordinary",
        "achievement_type": "ordinary",
        "title": "First Gift",
        "description": "",
        "status": "active",
        "version": 1,
        "primary_badge_resource_id": 8001,
        "primary_badge_resource": {
          "resource_id": 8001,
          "resource_code": "badge_first_gift",
          "resource_type": "badge",
          "name": "First Gift",
          "status": "active",
          "grantable": true,
          "grant_strategy": "new_entitlement",
          "usage_scopes": ["badge"],
          "asset_url": "https://cdn.example/badge.png",
          "preview_url": "",
          "animation_url": "",
          "metadata_json": "{\"badge_form\":\"tile\"}",
          "sort_order": 10,
          "created_at_ms": 1710000000000,
          "updated_at_ms": 1710000000000
        },
        "reward_resource_group_id": 9001,
        "reward_resource_group": {
          "group_id": 9001,
          "group_code": "ach_first_gift_reward",
          "name": "First Gift Reward",
          "status": "active",
          "description": "",
          "sort_order": 10,
          "items": [
            {
              "group_item_id": 1,
              "item_type": "resource",
              "resource_id": 8001,
              "resource": {
                "resource_id": 8001,
                "resource_code": "badge_first_gift",
                "resource_type": "badge",
                "name": "First Gift",
                "status": "active",
                "grantable": true,
                "grant_strategy": "new_entitlement",
                "usage_scopes": ["badge"],
                "asset_url": "https://cdn.example/badge.png",
                "preview_url": "",
                "animation_url": "",
                "metadata_json": "{\"badge_form\":\"tile\"}",
                "sort_order": 10,
                "created_at_ms": 1710000000000,
                "updated_at_ms": 1710000000000
              },
              "quantity": 1,
              "duration_ms": 0,
              "sort_order": 10
            }
          ],
          "created_at_ms": 1710000000000,
          "updated_at_ms": 1710000000000
        },
        "auto_pin_policy": "if_empty",
        "honor_visibility": true,
        "effective_from_ms": 0,
        "effective_to_ms": 0,
        "sort_order": 10,
        "display_config_json": "{}",
        "conditions": [
          {
            "condition_id": "cond_xxx",
            "achievement_id": "ach_first_gift",
            "version": 1,
            "metric_type": "send_gift_count",
            "target_value": 1,
            "target_unit": "count",
            "dimension_filter_json": "{}"
          }
        ],
        "created_by_admin_id": 1,
        "updated_by_admin_id": 1,
        "created_at_ms": 1710000000000,
        "updated_at_ms": 1710000000000
      },
      "cycle_key": "lifetime",
      "progress_value": 1,
      "target_value": 1,
      "user_status": "unlocked",
      "unlocked_at_ms": 1710000000000,
      "reward_status": "granted"
    }
  ],
  "total": 1,
  "page": 1,
  "page_size": 20,
  "server_time_ms": 1710000000000
}
```

字段值：

| 字段 | 值 |
| --- | --- |
| `achievement_type` | `ordinary` / `activity_limited` |
| `collection_type` | `ordinary` |
| `user_status` | 空字符串表示未完成；`unlocked` 表示已达成 |
| `reward_status` | 空字符串 / `pending` / `running` / `granted` / `failed` |
| `auto_pin_policy` | `none` / `if_empty` / `always` |
| `primary_badge_resource` | 成就主徽章素材，可直接渲染 |
| `reward_resource_group.items[].resource` | 成就奖励资源素材，可直接渲染 |

### 我的徽章展示

```json
{
  "strip_badges": [
    {
      "slot": "profile_strip",
      "position": 1,
      "badge_form": "strip",
      "resource_id": 8101,
      "resource": {
        "resource_id": 8101,
        "resource_code": "vip_gold_strip",
        "resource_type": "badge",
        "name": "VIP Gold",
        "status": "active",
        "grantable": true,
        "grant_strategy": "new_entitlement",
        "usage_scopes": ["badge"],
        "asset_url": "https://cdn.example/vip-strip.png",
        "preview_url": "",
        "animation_url": "",
        "metadata_json": "{\"badge_form\":\"strip\"}",
        "sort_order": 10,
        "created_at_ms": 1710000000000,
        "updated_at_ms": 1710000000000
      },
      "entitlement_id": "ent_strip_1",
      "source_type": "vip",
      "source_id": "vip_gold",
      "pin_mode": "system",
      "updated_at_ms": 1710000000000
    }
  ],
  "profile_tile_badges": [
    {
      "slot": "profile_tile",
      "position": 1,
      "badge_form": "tile",
      "resource_id": 8001,
      "resource": {
        "resource_id": 8001,
        "resource_code": "badge_first_gift",
        "resource_type": "badge",
        "name": "First Gift",
        "status": "active",
        "grantable": true,
        "grant_strategy": "new_entitlement",
        "usage_scopes": ["badge"],
        "asset_url": "https://cdn.example/badge.png",
        "preview_url": "",
        "animation_url": "",
        "metadata_json": "{\"badge_form\":\"tile\"}",
        "sort_order": 10,
        "created_at_ms": 1710000000000,
        "updated_at_ms": 1710000000000
      },
      "entitlement_id": "ent_tile_1",
      "source_type": "achievement",
      "source_id": "ach_first_gift",
      "pin_mode": "user",
      "updated_at_ms": 1710000000000
    }
  ],
  "honor_badges": [
    {
      "slot": "honor_wall",
      "position": 1,
      "badge_form": "tile",
      "resource_id": 8001,
      "resource": {
        "resource_id": 8001,
        "resource_code": "badge_first_gift",
        "resource_type": "badge",
        "name": "First Gift",
        "status": "active",
        "grantable": true,
        "grant_strategy": "new_entitlement",
        "usage_scopes": ["badge"],
        "asset_url": "https://cdn.example/badge.png",
        "preview_url": "",
        "animation_url": "",
        "metadata_json": "{\"badge_form\":\"tile\"}",
        "sort_order": 10,
        "created_at_ms": 1710000000000,
        "updated_at_ms": 1710000000000
      },
      "entitlement_id": "ent_tile_1",
      "source_type": "achievement",
      "source_id": "ach_first_gift",
      "pin_mode": "auto",
      "updated_at_ms": 1710000000000
    }
  ],
  "server_time_ms": 1710000000000
}
```

字段值：

| 字段 | 说明 |
| --- | --- |
| `strip_badges` | 长徽章；身份、等级、VIP 等系统展示 |
| `profile_tile_badges` | 用户主页方形展示槽；用户可通过 `POST /badges/display` 调整 |
| `honor_badges` | 荣誉墙方形徽章；成就或系统赠送 |
| `badge_form` | `strip` 长徽章 / `tile` 方形徽章 |
| `slot` | `profile_strip` / `profile_tile` / `honor_wall` |
| `source_type` | `achievement` / `system_grant` / `level` / `vip` / `identity` |
| `resource` | 徽章素材，可直接渲染 |

## 错误处理

| HTTP | code | 处理 |
| --- | --- | --- |
| `200` | `OK` | 读取 `data` |
| `400` | `INVALID_JSON` | 请求体不是合法 JSON |
| `400` | `INVALID_ARGUMENT` | 参数错误；不自动重试 |
| `401` | `UNAUTHORIZED` | 刷新 token；失败则重新登录 |
| `403` | `PROFILE_REQUIRED` | 跳转资料补全 |
| `403` | `PERMISSION_DENIED` | 停止当前操作 |
| `404` | `NOT_FOUND` | 资源不存在或徽章不可展示 |
| `429` | `RATE_LIMITED` | 延迟重试 |
| `500` | `INTERNAL_ERROR` | 提示重试，记录 `request_id` |
| `502` | `UPSTREAM_ERROR` | 提示重试，记录 `request_id` |

`POST /api/v1/badges/display`：

| 场景 | 处理 |
| --- | --- |
| 网络超时 | 使用同一个 `command_id` 重试 |
| 用户重新调整排序 | 生成新的 `command_id` |
| `NOT_FOUND` | 当前用户未获得该 `resource_id` 对应徽章，刷新 `/api/v1/badges/me` |
| `INVALID_ARGUMENT` | 检查 `slot`、`items.length <= 6`、`resource_id > 0`、重复 `resource_id` |

