# Client API 前端联调文档 文档版本: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`