# Explore tab Flutter App 对接文档

本文档定义 App 端 Explore 页 H5 tab 的读取接口。后台在 `HYApp 管理平台 / APP配置 / Explore配置` 维护 tab 名称和 H5 链接；App 只读取已启用配置并按服务端返回顺序展示。

## 基础约定

本地网关地址：

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

所有 `/api/v1` 业务接口使用统一响应外壳：

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

客户端成功判断：

```text
HTTP 200 && code == "OK"
```

失败时保留 `request_id`，用于后端排查日志。

## App 标识 Header

Explore tab 按 App 隔离，接口没有 query/body 形式的 `app_code` 参数。gateway 通过 Header 解析 App：

| Header | 必填 | 说明 |
| --- | --- | --- |
| `X-App-Code` | 推荐 | 内部 App code，例如 `lalu`。 |
| `X-HY-App-Code` | 否 | `X-App-Code` 的兼容别名。 |
| `X-App-Package` | 推荐二选一 | App 包名，例如 `com.org.laluparty`。传包名时 gateway 可解析到内部 `app_code`。 |
| `X-App-Platform` | 包名解析时推荐 | 平台，例如 `android`、`ios`。 |

当前开发默认 `app_code` 是 `lalu`。Flutter 仍建议显式传 `X-App-Code` 或 `X-App-Package`，避免多 App 环境串配置。

## 获取 Explore tab 列表

接口地址：

```http
GET /api/v1/app/explore-tabs
```

完整本地地址：

```text
http://127.0.0.1:13000/api/v1/app/explore-tabs
```

鉴权：

```text
不需要 Bearer token
```

服务端行为：

- 只返回当前 App 已启用的 Explore tabs。
- 后台关闭的 tab 不下发给 App。
- 返回顺序为后台 `sort_order` 升序，同排序时按较新的记录优先。

### 请求 Header

```http
X-App-Code: lalu
```

或：

```http
X-App-Package: com.org.laluparty
X-App-Platform: android
```

### Query 参数

无。

### Request Body

无。

### curl 示例

```bash
curl -X GET 'http://127.0.0.1:13000/api/v1/app/explore-tabs' \
  -H 'X-App-Code: lalu'
```

包名解析方式：

```bash
curl -X GET 'http://127.0.0.1:13000/api/v1/app/explore-tabs' \
  -H 'X-App-Package: com.org.laluparty' \
  -H 'X-App-Platform: android'
```

### 成功响应

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {
    "items": [
      {
        "id": 7,
        "app_code": "lalu",
        "tab": "Farm",
        "h5_url": "https://www.baidu.com",
        "enabled": true,
        "sort_order": 1,
        "updated_at_ms": 1700000000000
      }
    ],
    "total": 1
  }
}
```

### `data` 字段

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `items` | array | Explore tab 列表。 |
| `total` | int | 本次返回数量。 |

### `items[]` 字段

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | int | 后台配置记录 ID，只用于排查或本地缓存 key，不作为业务路由。 |
| `app_code` | string | tab 所属 App，例如 `lalu`。 |
| `tab` | string | App 展示的 tab 文案。 |
| `h5_url` | string | 点击 tab 后加载的 H5 地址。 |
| `enabled` | bool | 是否启用。App 接口只返回 `true` 的记录。 |
| `sort_order` | int | 展示排序，数值越大越靠前。 |
| `updated_at_ms` | int | 更新时间，Unix epoch milliseconds。 |

### 空列表响应

当后台没有启用的 Explore tab 时，返回空数组，不是错误：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {
    "items": [],
    "total": 0
  }
}
```

### 错误响应

配置读取异常时：

```json
{
  "code": "UPSTREAM_ERROR",
  "message": "upstream service error",
  "request_id": "req_xxx"
}
```

客户端处理建议：

- `UPSTREAM_ERROR`：展示空态或保留上一次本地缓存。
- 非 `OK`：不要使用 `data`。
- 记录 `request_id` 到客户端日志，便于问题排查。

## Flutter 数据模型

```dart
class ExploreTabList {
  final List<ExploreTabItem> items;
  final int total;

  ExploreTabList({
    required this.items,
    required this.total,
  });

  factory ExploreTabList.fromJson(Map<String, dynamic> json) {
    final rawItems = json['items'] as List<dynamic>? ?? const [];
    return ExploreTabList(
      items: rawItems
          .map((e) => ExploreTabItem.fromJson(e as Map<String, dynamic>))
          .toList(),
      total: json['total'] as int? ?? rawItems.length,
    );
  }
}

class ExploreTabItem {
  final int id;
  final String appCode;
  final String tab;
  final String h5Url;
  final bool enabled;
  final int sortOrder;
  final int updatedAtMs;

  ExploreTabItem({
    required this.id,
    required this.appCode,
    required this.tab,
    required this.h5Url,
    required this.enabled,
    required this.sortOrder,
    required this.updatedAtMs,
  });

  factory ExploreTabItem.fromJson(Map<String, dynamic> json) {
    return ExploreTabItem(
      id: json['id'] as int? ?? 0,
      appCode: json['app_code'] as String? ?? '',
      tab: json['tab'] as String? ?? '',
      h5Url: json['h5_url'] as String? ?? '',
      enabled: json['enabled'] as bool? ?? false,
      sortOrder: json['sort_order'] as int? ?? 0,
      updatedAtMs: json['updated_at_ms'] as int? ?? 0,
    );
  }
}
```

## Flutter Dio 示例

```dart
Future<ExploreTabList> fetchExploreTabs(Dio dio) async {
  final response = await dio.get(
    '/api/v1/app/explore-tabs',
    options: Options(
      headers: {
        'X-App-Code': 'lalu',
      },
    ),
  );

  final body = response.data as Map<String, dynamic>;
  if (body['code'] != 'OK') {
    throw Exception('${body['code']}: ${body['message']} request_id=${body['request_id']}');
  }

  return ExploreTabList.fromJson(body['data'] as Map<String, dynamic>);
}
```

## 展示和跳转规则

- App 按 `items` 返回顺序展示，不需要二次排序。
- `tab` 为空或 `h5_url` 为空时，建议 App 跳过该项，避免展示不可点击入口。
- 点击 tab 后打开 `h5_url`，由 H5 页面自身处理内部导航。
- 如果接口失败，App 可以使用本地缓存的上一版 tabs；没有缓存时展示空态。

