Appearance
前端联调 API 手册(v1)
归属:API Docs → v1 → 前端联调手册 覆盖:30 个端点 — 前端
src/api/*.ts(6 文件)+src/utils/request.ts拦截器 + 3 条页面直调 字段事实源:tests/reports/contract-matrix.json(2026-09-20 前后端逐字段比对) 错误码权威:错误码注册表 配套文档:API 联通矩阵 · 联调问题台账
本手册按前端源码文件分组 —— 你打开哪个 .ts,就看哪一页。每个端点四块:摘要 / curl / 请求字段表 / 响应字段表 + 错误码表。
30 端点总表
| 页 | 对应前端文件 | 端点数 |
|---|---|---|
| login.md | src/api/login.ts + src/utils/request.ts | 7 |
| user.md | src/api/user.ts | 7 |
| device.md | src/api/device.ts | 4 |
| training.md | src/api/training.ts | 5 |
| splash.md | src/api/splash.ts | 2 |
| agreement.md | src/api/agreement.ts | 2 |
| page-direct.md | 页面直调(不走 src/api/*.ts) | 3 |
| 合计 | 30 |
🔢 字段「值」速查:values.md · 取值速查 —— 本手册各页回答「字段叫什么、什么类型、必填吗」; 那个页回答「填这个值会发生什么」(每个枚举/边界值的业务后果 + 缺省行为)。 起因:2026-09-20 真实联调 BUG ——
habits.exerciseFreq只写「0–7」,前端不知0是有效值(写 0 就通了),卡住联调。根因不是代码,是文档只给范围不给语义。
⭐ 另有 19 个新接口(不在上面 30 个内,但你可能要用)
2026-09-20 新增 —— 为当时还是硬编码假数据的页面补的数据源。 接线前必读:接线必读 · 7 条兜底 —— 不做兜底会白屏(withDefaults 只补 undefined 不补 null)。
| 批次 | 数量 | 端点 | 覆盖页面 |
|---|---|---|---|
| device 健康详情 | 10 | POST /api/v1/device:sleepDetail / heartRateDetail / stressDetail / oxygenDetail / stepsDetail / caloriesDetail / strengthDetail / weightDetail / activitySummary / recordDates | detail/ 14 页 + index/ 9 组件 |
| sport 运动会话 | 6 | POST /api/v1/sport:startRun / reportRunCoordinate / finishRun / myRunRecords / runSessionDetail / addWeightRecord | run/ + mine/sports/ + 加体重表单 |
| health_archive 健康档案 | 3 | POST /api/v1/health-archive:archive / saveHealthIncident / saveHeartRateZones | mine/info/ 7 组件 |
| simulation 仿真数据 | 3 | POST /api/v1/simulation:simulateData / clearSimulatedData / status | Dev 库专用(Prd 会拒) |
📖 Dev / Prd 两个库有什么区别 → 双库对照(结论:契约完全一致,区别只在 Dev 有仿真数据) 📋 逐字段契约证据 →
tests/reports/contract-audit-*.md(1036 行,19 个 action 每个字段的对照)
| # | 方法 | 路径 | action | 所在页 |
|---|---|---|---|---|
| 1 | POST | /api/v1/auth/oauth | captcha | login |
| 2 | POST | /api/v1/auth/oauth | sms-send | login |
| 3 | POST | /api/v1/auth/oauth | sms-login | login |
| 4 | POST | /api/v1/auth/oauth | rotate | login |
| 5 | POST | /api/v1/auth/oauth | mySessions | login |
| 6 | POST | /api/v1/auth/oauth | revokeSession | login |
| 7 | POST | /api/v1/auth/oauth | logout | login |
| 8 | POST | /api/v1/user | updateBasicProfile | user |
| 9 | POST | /api/v1/user | submitOnboarding | user |
| 10 | POST | /api/v1/user | onboardingSuggestion | user |
| 11 | POST | /api/v1/user | adoptPlanOption | user |
| 12 | POST | /api/v1/user | completeOnboarding | user |
| 13 | POST | /api/v1/user | myProfile | user |
| 14 | POST | /api/v1/user | updateProfile | user |
| 15 | GET | /api/v1/device/vendors | — | device |
| 16 | POST | /api/v1/device/vendors/{id}/auth-url | — | device |
| 17 | POST | /api/v1/device | myBindings | device |
| 18 | POST | /api/v1/device | unbind | device |
| 19 | GET | /api/v1/training/plans | — | training |
| 20 | POST | /api/v1/training/enroll | — | training |
| 21 | GET | /api/v1/training/tasks | — | training |
| 22 | POST | /api/v1/training/task/checkin | — | training |
| 23 | GET | /api/v1/training/reminders | — | training |
| 24 | GET | /api/v1/splash-screen | — | splash |
| 25 | POST | /api/v1/splash-screen/track | — | splash |
| 26 | GET | /api/v1/content/agreements | — | agreement |
| 27 | GET | /api/v1/content/agreements/{type} | — | agreement |
| 28 | POST | /api/v1/device | myTodayHealth | page-direct |
| 29 | POST | /api/v1/stats | getTodayOverview | page-direct |
| 30 | POST | /api/v1/upload?type=avatar | — | page-direct |
通用约定(7 条 · 所有端点共用)
G1 · 统一包络 —— request.ts 不剥壳
json
{ "code": "0", "msg": "ok", "data": { /* 业务字段全在这里 */ } }- 前端
resolve(body)拿到的是完整 body(含code/msg/data),业务字段一律在body.data之下 → 调用点写res.data.xxx。 - 业务码是字符串:
"0"成功、"40101012"失败。前端用String(body?.code) === '0'判断,不要用=== 0。
G2 请求头
| header | 值 | 说明 |
|---|---|---|
X-Env | Dev | Prd(大小写不敏感) | 双库路由。全程必须一致 —— token 绑 X-Env,跨库即失效。主请求 + rotate 都要带 |
Authorization | Bearer <access> | 默认档位;rotate 例外(见下) |
Content-Type | application/json | action 协议与 REST body 都走 JSON;upload 例外(multipart) |
⚠️ 本手册所有示例统一用
X-Env: Prd—— 因为 App 产线包就是 Prd (qm-frontend-api/src/config/index.ts的X_ENV = "Prd",build-prod.sh构建期硬断言)。 照着示例调 = 和 App 打同一个库,排查时不会出现「我调的结果和 App 不一样」的环境错位。 两个值都合法:Dev用于联调准备期的独立库(数据与 Prd 隔离),Prd是 App 实际用的。
- GET 请求前端会自动附加
cacheTimequery(防缓存击穿)—— 后端忽略,无影响。
G3 · 鉴权档位
| 档位 | 端点 |
|---|---|
| 无(免登录) | captcha · sms-send · sms-login · training/plans · splash-screen 全部 · content/agreements 全部 |
Bearer <access> | 其余全部(含 3 条页面直调) |
Bearer <refresh> ⚠️ | 只有 rotate —— 传 access 会 40101011 |
G4 两套错误契约并存
| 域 | HTTP | 业务码 | 说明 |
|---|---|---|---|
oauth | 200 | 40101xxx | 业务失败也是 HTTP 200(例外:40101013 封禁 → 403、40101011/12/14/15 → 401) |
user / device / training / upload / stats | 400 / 401 / 404(+413) | 40301xxx / 40403xxx / 40406xxx / 40419xxx / 40422xxx | HTTP 状态直接表达错误类别;msg 可直接 toast |
agreement | 恒 200 | 无业务码 | 非法入参走 data: null,不报错 |
前端 401 分档(request.ts 现行策略):
| 业务码 | 处理 |
|---|---|
40101015 | 业务 401 → toast,不续期(目标会话不存在) |
40101012 / 40101014 | 强登出(重放 / 设备信任过期) |
| 其他 401 | 触发 rotate 单飞重放一次 |
G5 · rotate 由拦截器自动调用 —— 不是业务直调
request.ts 的 rotateOnce 单飞拦截器在 token 将过期 / 收到 401 时自动发起;curl 调试时要自己带 refresh token。详见 login.md · 4 rotate。
G6 · 命名风格
| 域 | 风格 |
|---|---|
user / device / training / splash | camelCase |
agreement | snake_case(updated_at —— 全项目唯一,前端已对齐,勿改) |
oauth | camelCase,孤儿键 token_expiring 例外 |
G7 · 响应侧几乎无 schema(重要的排障前提)
30 个端点里只有 captcha 有 Pydantic 响应模型(CaptchaOutput)。其余 29 个的响应是手组 dict / dataclass 序列化 / repo dict 直出 —— OpenAPI 看不到响应结构、字段改动没有任何守卫挡。所以本手册的字段表就是事实防线,字段以本手册为准。
🔴 头号坑位速查(联调前必读,踩了会浪费半天)
| # | 坑 | 症状 | 详见 |
|---|---|---|---|
| 1 | clientId 复合格式只允许一个冒号({类型}:{设备UUID}) | 写成 android:a:b 被拒,且返回的是通用异常码 40201007("服务异常")而非参数错误 → 排障方向被误导 | login · 台账 #3 |
| 2 | X-Env 全程必须一致 | token 绑 X-Env,跨库即失效;rotate 漏带 → 查不到绑定行 | G2 |
| 3 | 两套错误契约并存 | 拦截器只处理 HTTP 200 或只处理 HTTP 4xx,会漏一半 | G4 |
| 4 | rotate 用 refresh token 做 Bearer | 传 access → 40101011 | login#4 |
| 5 | enroll 失败也返回 HTTP 200 + code 0 | data: {ok:false, reason} 不是幂等 upsert;后端规则「一次只允许一个进行中的计划」。必须自查 data.ok | training#2 |
| 6 | checkin 的 taskId 不存在/非本人 → data: null(HTTP 200 不报错) | 前端 await 后本地置 status='done' → 假成功 | training#4 |
| 7 | logout 有已知缺陷 | 前端现状不传 clientId → 后端吊销 oauth:default(不存在的会话)→ 真实会话未吊销,refresh 仍可 rotate。决策:联调期再修 | login#7 · 台账 |
| 8 | upload?type=avatar 是 multipart | 不走 action 协议、不走 request.ts 拦截器 → token + X-Env 要手动注入,且无自动 rotate | page-direct#3 |
| 9 | stats/getTodayOverview 响应是嵌套的 | 读顶层 data.budgetKcal → 恒 undefined(历史漂移:首页热量目标恒为默认值)。正解 data.calorie.budgetKcal | page-direct#2 |
| 10 | device/vendors 的 health.detail 后端不存在 | 前端声明了 detail(optional)→ 展示时恒 undefined。对应物是 health.message | device#1 |
已修复(2026-09-20 · v2.0.49 生产验证)
| 项 | 原问题 | 现状 |
|---|---|---|
rotate 缺 expiresAt | 前端 tokenExpires 恒被覆盖为 {}(与 sms-login 路径不对称) | ✅ 已补 —— rotate 现返回 expiresAt.{access,refresh},与 sms-login 同口径 |
updateProfile.nickname | 只写画像表,不同步 oauth 主表 → 重登显示旧昵称 | ✅ 已双写 —— 提交 nickname 时同步 users 主表 |
错误码段总览(本手册涉及)
| 段 | 域 | 覆盖端点 |
|---|---|---|
40101xxx | oauth | login 全部 7 个 |
40001xxx | oauth 参数段 | 同上(40001001 = HTTP 400 入参校验失败) |
40301xxx | user | user 全部 7 个 + splash track(复用 UserError) |
40403xxx | device | device 全部 4 个 + myTodayHealth |
40406xxx | training | training 全部 5 个 |
40419xxx | upload | upload?type=avatar |
40422xxx | stats(注册表当前登记为 ranking —— 撞段待确认,见下) | getTodayOverview |
50001001 | oauth 兜底 | 所有 oauth 端点的服务异常 |
全量码表见 → 错误码权威注册表。
关联阅读
- OAuth 登录认证(v1) —— 认证链的深入说明(验证码三定律、限流三层、clientId 语义、状态机)。本手册只留接口级摘要,认证细节请点过去
- OAuth 9 Action 字段表 · OAuth 错误码矩阵
- User API · Device 设备线 · training API · upload API
- 联调问题台账 —— 差异 + 决策 + 状态的单一台账
- 开发测试台(免登录观测台)
- Swagger 调试:
{BASE_URL}/docs
待确认清单(本手册中标注不确定的条目)
| # | 条目 | 待确认内容 |
|---|---|---|
| 1 | stats/getTodayOverview 错误码段 | ✅ 已确认(2026-09-20 契约审计):40422xxx 是 ranking 与 stats 的事实共享段,两域 40422001/002/004 逐码重号且语义完全相同 → 前端按码分支行为一致,实际无危害。遗留风险:任一方将来新增码会真冲突(建议下次动这两个域的错误码时把一方迁走)。已据实登记进注册表 |
| 2 | device/myTodayHealth 响应粒度 | 前端矩阵文档口径是简写 {steps,distance,hr,spo2,sleep…},后端实际是块状结构(steps={value,date}、todayActivity={totalCalories,totalDistanceKm});顶层 distance 后端不存在。前端 TS 核对后如有偏差需升级处理 |
| 3 | device/myTodayHealth 的 spo2 | 后端恒返回 null(V2 sync 暂无血氧来源),前端按 ?? undefined 容忍;接血氧源后自动转有值 |
| 4 | user/myProfile 完整键集 | 动态 dict(画像行 24 键 + account 5 键),无 schema 无法静态枚举;且双仓储字段集不一致(PG 有 updatedAt,InMemory 无)。前端类型是 Record<string, any> —— 只能按字段存在性消费,不要全等断言 |
| 5 | user/updateProfile 响应 user 行 | 画像行 dict 直出全列(远超前端声明的 7 键)。前端声明的 7 键都在,其余键未定义 |
| 6 | logout 修复归属 | 前端补 clientId / 后端改语义(无 clientId → 吊销全部会话)—— 两条路线联调期决策,见台账 |