# Client API 前端联调文档
> 文档版本：v1.0
> 最后更新：2026-04-02 08:28:05


文档版本：v1.0.0
最后更新：2026-03-31
状态：联调中

## 1. 文档说明

本文档面向前端联调，描述当前 `client-api` 在代码中真实可用的接口。

约定：

- 本文档优先级高于产品阶段的总草案；前端联调以本文档为准
- 本文档只覆盖 `client-api`
- 每个接口会标记当前状态：
  - `已实现，已联调`
  - `已实现，未联调`
  - `预留未就绪`

## 2. 通用约定

### 2.1 Base Path

- `client-api`：`/client/v1`

### 2.2 成功响应

```json
{
  "request_id": "req_xxx",
  "data": {}
}
```

### 2.3 失败响应

```json
{
  "request_id": "req_xxx",
  "error": {
    "code": "invalid_request",
    "message": "xxx",
    "details": {}
  }
}
```

### 2.4 鉴权说明

- 登录后接口使用：`Authorization: Bearer {access_token}`
- `launch` 成功后会返回 `session_token`
- 但当前版本下游 `session` 相关接口尚未开放，因此前端暂时只需要保存 `session_token`

### 2.5 枚举说明

`client_type`

- `app`
- `wechat`

`body_profile_status`

- `pending`
- `completed`

## 3. 已实现接口

### 3.1 `POST /client/v1/auth/sms/send`

状态：`已实现，已联调`

接口介绍：

- 发送短信验证码
- 当前已接阿里云短信

请求参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `scene` | `string` | 是 | 当前已使用 `client_login` |
| `country_code` | `string` | 是 | 国家码，如 `86` |
| `mobile` | `string` | 是 | 手机号 |
| `client_type` | `string` | 是 | `app` / `wechat` |
| `device_id` | `string` | 是 | 设备唯一标识 |

请求示例：

```json
{
  "scene": "client_login",
  "country_code": "86",
  "mobile": "15168870729",
  "client_type": "app",
  "device_id": "dev_iphone_001"
}
```

返回参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `cooldown_sec` | `int` | 重发冷却时间 |
| `expires_in_sec` | `int` | 验证码有效期 |

### 3.2 `POST /client/v1/auth/login/sms`

状态：`已实现，已联调`

接口介绍：

- 短信登录
- 如果手机号首次登录，后端会自动创建：
  - `users`
  - `login_identities`
  - 默认 `user_body_profiles`
  - 首条 `user_body_profile_versions`

请求参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `login_request_id` | `string` | 是 | 登录幂等号 |
| `country_code` | `string` | 是 | 国家码 |
| `mobile` | `string` | 是 | 手机号 |
| `sms_code` | `string` | 是 | 短信验证码 |
| `client_type` | `string` | 是 | `app` / `wechat` |
| `device_id` | `string` | 是 | 设备唯一标识 |

返回参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `user_id` | `string` | 用户公开 ID |
| `access_token` | `string` | 登录态 token |
| `refresh_token` | `string` | refresh token |
| `login_method` | `string` | 当前为 `sms` |
| `body_profile_status` | `string` | `pending` / `completed` |
| `expires_in_sec` | `int` | `access_token` 有效期 |

### 3.3 `POST /client/v1/auth/login/wechat`

状态：`已实现，未联调`

接口介绍：

- 微信登录入口已预留
- 当前仓库里 provider 仍以 mock 为主，尚未做真实联调

请求参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `login_request_id` | `string` | 是 | 登录幂等号 |
| `wechat_login_code` | `string` | 是 | 微信登录 code |
| `client_type` | `string` | 是 | 建议传 `wechat` |
| `device_id` | `string` | 是 | 设备唯一标识 |

返回参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `user_id` | `string` | 用户公开 ID |
| `access_token` | `string` | 登录态 token |
| `refresh_token` | `string` | refresh token |
| `login_method` | `string` | 当前为 `wechat` |
| `body_profile_status` | `string` | `pending` / `completed` |
| `expires_in_sec` | `int` | `access_token` 有效期 |

### 3.4 `POST /client/v1/auth/refresh`

状态：`已实现，已联调`

接口介绍：

- 使用 refresh token 刷新 `access_token`

请求参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `refresh_token` | `string` | 是 | refresh token |
| `client_type` | `string` | 是 | `app` / `wechat` |

返回参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `access_token` | `string` | 新的登录态 token |
| `expires_in_sec` | `int` | 有效期 |

### 3.5 `PUT /client/v1/me/body-profile`

状态：`已实现，未联调`

接口介绍：

- 更新身体数据
- 成功后会更新当前档案，并追加历史版本

认证：

- 需要 `access_token`

请求参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `gender` | `string` | 否 | `male` / `female` / `other` / `unknown` |
| `birth_date` | `string` | 否 | `YYYY-MM-DD` |
| `height_cm` | `number` | 否 | 身高，厘米 |
| `weight_kg` | `number` | 否 | 体重，千克 |
| `resting_heart_rate_bpm` | `int` | 否 | 静息心率 |
| `max_heart_rate_bpm` | `int` | 否 | 最大心率 |

返回参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `user_id` | `string` | 用户公开 ID |
| `body_profile_status` | `string` | `pending` / `completed` |
| `completed_at` | `string` | 首次补全时间，未补全时可能为空 |

### 3.6 `GET /client/v1/cards`

状态：`已实现，已联调`

接口介绍：

- 首页卡片列表

查询参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `region_code` | `string` | 否 | 地区编码，当前可空 |

返回参数：

`data.items[]`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `card_id` | `string` | 卡片公开 ID |
| `card_type` | `string` | 例如 `competition_card` |
| `display_name` | `string` | 卡片展示名称 |
| `competition_id` | `string` | 关联赛事 ID，非赛事卡可能为空 |
| `html_url` | `string` | H5 地址，可空 |
| `cover_url` | `string` | 封面地址，可空 |
| `display_slot` | `string` | 展示槽位 |
| `display_priority` | `int` | 展示优先级 |

### 3.7 `GET /client/v1/competitions/{competition_id}`

状态：`已实现，已联调`

接口介绍：

- 赛事详情页
- 如果带 `access_token`，会返回当前用户的 `registration_status`

认证：

- 可匿名访问
- 建议前端在登录后带上 `access_token`

Path 参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `competition_id` | `string` | 赛事公开 ID |

返回参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `competition_id` | `string` | 赛事公开 ID |
| `display_name` | `string` | 赛事名称 |
| `competition_status` | `string` | 当前状态 |
| `registration_enabled` | `bool` | 是否允许报名 |
| `registration_status` | `string` | 当前用户报名状态，匿名访问时可能为空 |
| `competition_start_at` | `string` | 赛事开始时间 |
| `competition_end_at` | `string` | 赛事结束时间 |
| `leaderboard_enabled` | `bool` | 是否展示排行榜 |
| `realtime_board_enabled` | `bool` | 是否启用实时榜 |

`data.events[]`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | `string` | Event 公开 ID |
| `display_name` | `string` | Event 名称 |
| `is_default` | `bool` | 是否默认 Event |
| `current_release_id` | `string` | 当前发布版 ID |
| `manifest_url` | `string` | manifest 下载地址 |
| `manifest_checksum_sha256` | `string` | manifest 校验值 |
| `relation_status` | `string` | 关联状态 |

### 3.8 `GET /client/v1/competitions/{competition_id}/events/{event_id}`

状态：`已实现，已联调`

接口介绍：

- 赛事上下文下的 Event 详情
- 前端应在这个页面预加载 manifest，并完成路线预览

认证：

- 可匿名访问
- 建议前端在登录后带上 `access_token`

Path 参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `competition_id` | `string` | 赛事公开 ID |
| `event_id` | `string` | Event 公开 ID |

返回参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `competition_id` | `string` | 赛事公开 ID |
| `event_id` | `string` | Event 公开 ID |
| `display_name` | `string` | Event 名称 |
| `current_release_id` | `string` | 当前发布版 ID |
| `manifest_url` | `string` | manifest 下载地址 |
| `manifest_checksum_sha256` | `string` | manifest 校验值 |
| `direct_entry_enabled` | `bool` | 是否支持地图直入 |
| `playfield_version_id` | `string` | 场地版本 ID |
| `playfield_kind` | `string` | 如 `course` |

`data.competition_context`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `competition_id` | `string` | 赛事公开 ID |
| `display_name` | `string` | 赛事名称 |
| `competition_status` | `string` | 赛事状态 |
| `registration_status` | `string` | 当前用户报名状态 |
| `leaderboard_enabled` | `bool` | 是否显示排行榜 |
| `realtime_board_enabled` | `bool` | 是否启用实时榜 |

`data.map_summary`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `map_id` | `string` | 地图公开 ID |
| `display_name` | `string` | 地图名称 |
| `cover_url` | `string` | 封面图，可空 |
| `scale_text` | `string` | 比例尺，可空 |

`data.preview_summary`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `control_count` | `int` | 控制点数量 |
| `route_count` | `int` | 路线数量 |
| `playfield_kind` | `string` | 场地类型 |

### 3.9 `POST /client/v1/competitions/{competition_id}/registrations`

状态：`已实现，未联调`

接口介绍：

- 赛事报名

认证：

- 需要 `access_token`

Path 参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `competition_id` | `string` | 赛事公开 ID |

请求参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `group_id` | `string` | 否 | 队伍或分组 ID |
| `form_payload` | `object` | 否 | 附加报名表单 |

返回参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `registration_id` | `string` | 报名记录 ID |
| `status` | `string` | 当前报名状态 |

### 3.10 `GET /client/v1/maps`

状态：`已实现，已联调`

接口介绍：

- 地图列表页

查询参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `region_code` | `string` | 否 | 地区编码 |
| `page` | `int` | 否 | 默认 `1` |
| `page_size` | `int` | 否 | 默认 `20`，最大 `50` |

返回参数：

`data.items[]`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `map_id` | `string` | 地图公开 ID |
| `display_name` | `string` | 地图名称 |
| `cover_url` | `string` | 封面图，可空 |
| `scale_text` | `string` | 比例尺，可空 |
| `distance_from_user_km` | `number` | 距离，可空 |

说明：

- 当前响应只返回 `items`，不回显 `page/page_size/total`

### 3.11 `GET /client/v1/maps/{map_id}`

状态：`已实现，已联调`

接口介绍：

- 地图详情页
- 同时返回当前地图下允许直入的 Event 列表

Path 参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `map_id` | `string` | 地图公开 ID |

返回参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `map_id` | `string` | 地图公开 ID |
| `display_name` | `string` | 地图名称 |

`data.map_summary`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `cover_url` | `string` | 封面图，可空 |
| `scale_text` | `string` | 比例尺，可空 |
| `updated_date` | `string` | 更新时间，可空 |

`data.events[]`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | `string` | Event 公开 ID |
| `display_name` | `string` | Event 名称 |
| `preview_image_url` | `string` | 预览图，可空 |
| `control_count` | `int` | 控制点数量 |
| `route_count` | `int` | 路线数量 |
| `direct_entry_enabled` | `bool` | 是否允许地图直入 |
| `current_release_id` | `string` | 当前发布版 ID |
| `manifest_url` | `string` | manifest 下载地址 |
| `manifest_checksum_sha256` | `string` | manifest 校验值 |
| `playfield_kind` | `string` | 如 `course` |

### 3.12 `GET /client/v1/events`

状态：`已实现，已联调`

接口介绍：

- 地图直入链路下的 Event 列表

查询参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `map_id` | `string` | 否 | 按地图筛选 |
| `page` | `int` | 否 | 默认 `1` |
| `page_size` | `int` | 否 | 默认 `20`，最大 `50` |

返回参数：

`data.items[]`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | `string` | Event 公开 ID |
| `display_name` | `string` | Event 名称 |
| `map_id` | `string` | 地图公开 ID |
| `map_display_name` | `string` | 地图名称 |
| `preview_image_url` | `string` | 预览图，可空 |
| `control_count` | `int` | 控制点数量 |
| `route_count` | `int` | 路线数量 |
| `direct_entry_enabled` | `bool` | 是否允许地图直入 |
| `current_release_id` | `string` | 当前发布版 ID |
| `manifest_url` | `string` | manifest 下载地址 |
| `manifest_checksum_sha256` | `string` | manifest 校验值 |
| `playfield_kind` | `string` | 如 `course` |

说明：

- 当前响应只返回 `items`，不回显 `page/page_size/total`

### 3.13 `GET /client/v1/events/{event_id}`

状态：`已实现，已联调`

接口介绍：

- 地图直入上下文下的 Event 详情
- 与赛事入口页共用同一套 Event 详情视图

Path 参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | `string` | Event 公开 ID |

返回参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | `string` | Event 公开 ID |
| `display_name` | `string` | Event 名称 |
| `direct_entry_enabled` | `bool` | 是否允许地图直入 |
| `current_release_id` | `string` | 当前发布版 ID |
| `manifest_url` | `string` | manifest 下载地址 |
| `manifest_checksum_sha256` | `string` | manifest 校验值 |
| `playfield_version_id` | `string` | 场地版本 ID |
| `playfield_kind` | `string` | 如 `course` |

`data.map_summary`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `map_id` | `string` | 地图公开 ID |
| `display_name` | `string` | 地图名称 |
| `cover_url` | `string` | 封面图，可空 |
| `scale_text` | `string` | 比例尺，可空 |

`data.event_summary`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `control_count` | `int` | 控制点数量 |
| `route_count` | `int` | 路线数量 |
| `playfield_kind` | `string` | 场地类型 |

### 3.14 `POST /client/v1/competitions/{competition_id}/events/{event_id}/launch`

状态：`已实现，已联调`

接口介绍：

- 赛事入口 `launch`
- 会校验赛事时间窗、报名状态、`release_id`
- 成功后创建 session，并返回 `session_token`

认证：

- 需要 `access_token`

Path 参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `competition_id` | `string` | 赛事公开 ID |
| `event_id` | `string` | Event 公开 ID |

请求参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `launch_request_id` | `string` | 是 | 启动幂等号 |
| `device_id` | `string` | 是 | 设备唯一标识 |
| `client_type` | `string` | 是 | 必须与 `access_token` 内声明一致 |
| `release_id` | `string` | 是 | 前端当前持有的发布版 ID |
| `route_code` | `string` | 否 | 当前选中的路线编码 |

返回参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `session_id` | `string` | 会话 ID |
| `session_token` | `string` | 会话 token |
| `session_token_expires_at` | `string` | 会话 token 过期时间 |
| `participant_id` | `string` | 参赛身份 ID |
| `competition_id` | `string` | 赛事公开 ID |
| `event_id` | `string` | Event 公开 ID |
| `event_release_id` | `string` | 实际冻结的发布版 ID |
| `playfield_version_id` | `string` | 场地版本 ID |
| `route_code` | `string` | 当前冻结的路线编码 |
| `realtime_endpoint` | `string` | realtime-center 地址 |
| `realtime_token` | `string` | 当前版本通常为空 |

当前注意：

- 若前端传入的 `release_id` 已过期，会返回 `EVENT_RELEASE_STALE`
- 当前后端会冻结并回传 `route_code`，但还没有对 manifest 内路线做严格校验

### 3.15 `POST /client/v1/events/{event_id}/launch`

状态：`已实现，已联调`

接口介绍：

- 地图直入 `launch`
- 不校验赛事报名资格
- 成功后创建 session，并返回 `session_token`

认证：

- 需要 `access_token`

Path 参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | `string` | Event 公开 ID |

请求参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `launch_request_id` | `string` | 是 | 启动幂等号 |
| `device_id` | `string` | 是 | 设备唯一标识 |
| `client_type` | `string` | 是 | 必须与 `access_token` 内声明一致 |
| `release_id` | `string` | 是 | 前端当前持有的发布版 ID |
| `route_code` | `string` | 否 | 当前选中的路线编码 |

返回参数：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `session_id` | `string` | 会话 ID |
| `session_token` | `string` | 会话 token |
| `session_token_expires_at` | `string` | 会话 token 过期时间 |
| `participant_id` | `string` | 参赛身份 ID |
| `event_id` | `string` | Event 公开 ID |
| `event_release_id` | `string` | 实际冻结的发布版 ID |
| `playfield_version_id` | `string` | 场地版本 ID |
| `route_code` | `string` | 当前冻结的路线编码 |
| `realtime_endpoint` | `string` | realtime-center 地址 |
| `realtime_token` | `string` | 当前版本通常为空 |

当前注意：

- 若前端传入的 `release_id` 已过期，会返回 `EVENT_RELEASE_STALE`
- 当前后端会冻结并回传 `route_code`，但还没有对 manifest 内路线做严格校验

## 4. 预留未就绪接口

以下接口当前在路由中已占位，但实际会直接返回 `not implemented`：

| 接口 | 当前错误码 |
| --- | --- |
| `POST /client/v1/session-uploads` | `session_upload_not_ready` |
| `POST /client/v1/punches` | `session_punch_not_ready` |
| `POST /client/v1/sessions/{session_id}/finish` | `session_finish_not_ready` |
| `GET /client/v1/sessions/{session_id}/result` | `session_result_not_ready` |
| `GET /client/v1/sessions/{session_id}/replay-summary` | `session_replay_summary_not_ready` |
| `GET /client/v1/sessions/{session_id}/gps-track` | `session_gps_track_not_ready` |
| `GET /client/v1/sessions/{session_id}/heart-rate` | `session_heart_rate_not_ready` |
| `GET /client/v1/me/sessions` | `session_history_not_ready` |

说明：

- 所以当前不是“只有 GPS / 心率上报没测”
- 而是整条 `session` 下游链路都还没开放
- 当前已实测闭环停在 `launch` 成功返回 `session_token`

## 5. 当前测试数据

以下数据可直接用于本地联调：

| 类型 | ID |
| --- | --- |
| `card_id` | `crd_classic_demo_001` |
| `competition_id` | `cmp_classic_demo_001` |
| `map_id` | `lxcb-001` |
| `event_id` | `sample-classic-001` |
| `release_id` | `sample-classic-001-rel-1` |
| `route_code` | `classic-001` |

说明：

- 已存在测试赛事卡片、地图、Event、manifest 绑定关系
- 已存在一个已批准报名的测试用户，可直接验证赛事入口 `launch`


