Files
cmr-mini/tmp/Client-API.md

699 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Client API 前端联调文档
> 文档版本v1.0
> 最后更新2026-04-02
文档版本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`