Appearance
前端联调 · 页面直调(3 个端点)
归属:前端联调 API 手册 → 页面直调(不走
src/api/*.ts,无 TS 泛型锚) 端点:3 个 ——myTodayHealth·getTodayOverview·upload?type=avatar为什么单列:这 3 条是自动化提取器的盲区(生成器只扫src/api/*.ts),2026-09-20 人工全审矩阵补录 ⚠️ 无 TS 泛型锚 → 本页字段表以后端源码为契约基准,前端 TS 核对后如有偏差请回填 取值语义:取值速查 · stats(profileComplete=false意味着预算是默认画像算出来的)· 取值速查 · upload(type非白名单静默兜底misc、localFallback与source的降级链)· 取值速查 · device(myTodayHealth块状结构)
本页 curl 变量
bash
BASE_URL=https://api.qingmulife.cn # 本地:http://localhost:3000
ACCESS=<access_token>
ENV=Prd # Dev | Prd —— 全程必须一致端点一览
| # | 方法 | 路径 | action | 页面 | 鉴权 |
|---|---|---|---|---|---|
| 1 | POST | /api/v1/device | myTodayHealth | 首页健康卡(index.vue) | access |
| 2 | POST | /api/v1/stats | getTodayOverview | 首页热量 / 连续天数卡 | access |
| 3 | POST | /api/v1/upload?type=avatar | — | 头像上传(multipart) | access(手动注入) |
myTodayHealth
一句话:首页健康卡数据 —— 步数 / 活动 / 心率 / 血氧 / 睡眠一屏聚合(块状结构)。
curl
bash
curl -s "$BASE_URL/api/v1/device" \
-H 'Content-Type: application/json' \
-H 'X-Env: Prd' \
-H "Authorization: Bearer $ACCESS" \
-d '{"action":"myTodayHealth","payload":{}}'请求
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | string | ✔ | 固定 "myTodayHealth"(device 端点 6+2 action 分发) |
payload | object | ✖ | 忽略(无入参) |
Authorization / X-Env | header | ✔ |
响应 data ⚠️ 块状结构(不是扁平字段)
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
date | string | 否 | CN 今日 'YYYY-MM-DD' |
steps | {value: number, date: string} | null | 是 | 步数块 |
todayActivity | {totalCalories: number, totalDistanceKm: number} | null | 是 | 当日活动块 |
hr | {value: number, date: string} | null | 是 | 心率块 |
spo2 | null(恒 null) | 是 | 血氧块 —— V2 sync 暂无血氧来源;前端按 ?? undefined 容忍 |
sleep | {durationHours, deepHours, lightHours, remHours, score: null, date} | null | 是 | 睡眠块 |
sources | {steps?: string, hr?: string, sleep?: string} | 否 | 厂商来源映射(「数据来自:XX」小字);无数据时为 {},键按有无数据出现 |
⚠️ 顶层就这 7 个键(
date/steps/todayActivity/hr/spo2/sleep/sources)—— 后端没有sleepXiaomi键。前端const sleep = h.sleepXiaomi ?? h.sleep里的sleepXiaomi是 V1 遗留分支,永远取不到(??右侧恒生效)。无害但误导 —— 见 KB-04 台账,接线时勿照抄。
⚠️ 待确认 #2 —— 粒度差异:前端矩阵文档口径写的是简写
{steps, distance, hr, spo2, sleep…},但后端顶层没有distance—— 对应物是todayActivity.totalDistanceKm。同样steps/hr都是{value, date}对象而非数字。 前端 TS 核对后如有偏差请升级为契约差异回填台账。
错误码
| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
40403002 | 401 | 未登录 / token 无效 | 走拦截器 401 分档 |
40403001 | 400 | 未知 action | 检查 action 名 |
getTodayOverview
一句话:首页热量 / 连续天数卡 —— 今日热量预算 + 摄入 + 消耗 + 打卡统计。
🔴 坑位(历史漂移,已修,留档防回归):响应是嵌套的。 后端自始返回
data.calorie.budgetKcal,前端曾读顶层data.budgetKcal→ 首页热量目标恒为默认值。 前端已在api分支24902e9修正为按嵌套路径消费。别再写回顶层。
curl
bash
curl -s "$BASE_URL/api/v1/stats" \
-H 'Content-Type: application/json' \
-H 'X-Env: Prd' \
-H "Authorization: Bearer $ACCESS" \
-d '{"action":"getTodayOverview","payload":{}}'请求
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | string | ✔ | 固定 "getTodayOverview"(stats 端点 15 action 分发) |
payload | object | ✖ | 忽略(无入参) |
Authorization / X-Env | header | ✔ |
响应 data —— 🔴 两层嵌套
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
date | string | 否 | CN 今日 'YYYY-MM-DD' |
calorie.budgetKcal | number | ✔ | int —— 热量预算(BMR Mifflin-St Jeor × 1.3 − 400,下限 1500 / 女性 1200)。画像缺失 = null |
calorie.intakeKcal | number | 否 | float —— 摄入(当日无记录 = 0) |
calorie.burnKcal | number | 否 | float —— 消耗 = 跑步 + 力量(无记录 = 0) |
calorie.remainingKcal | number | ✔ | int —— 剩余额度(budget + burn − intake,向零截断)。画像缺失 = null |
calorie.profileComplete | boolean | 否 | false = 画像缺失 |
checkin.count | number | 否 | int —— 当日打卡数 |
checkin.distanceKm | number | 否 | float —— 无打卡为 0 |
🔴
profileComplete:false时后端budgetKcal/remainingKcal一律返回null—— 不编数(2026-09-20 用户拍板)。此前返的是「默认资料 30 岁 / 165cm / 60kg 算出来的值」, 看着像真数据,前端无法区分有无。看到null请渲染--,不要再兜一个默认值 (本仓 KB-06 的教训:?? 0把「不知道」变成可证伪的假数)。
✅ 正确读法:
res.data.calorie.budgetKcal❌ 错误读法:res.data.budgetKcal(恒undefined→ 前端回退默认值 → 需求「热量目标」静默失效)
⚠️ 本端点没有
basis字段(2026-09-22 按源码核实并移除 —— 本表此前误列)。basis属于stats.calorieBalance(另一个 action),与getTodayOverview无关。 首页 17 个端点的完整入参 / 出参表见 首页 API 接口参考。
错误码
| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
40422002 | 401 | 未登录 / token 无效 | 走拦截器 401 分档 |
40422001 | 400 | 入参非法 | 检查调用点 |
40422005 | 200 | 样本不足(关联分析不下结论) | 按缺省文案展示 |
⚠️ 待确认 #1 —— 错误码段归属:stats 源码用
40422xxx(src/app/stats/domain/exceptions.py), 但错误码注册表把40422xxx登记为 ranking 段 —— 疑似撞段。 本页按源码写实际值;段归属待确认后回填。
upload
一句话:头像上传 —— 🔴 multipart,不是 action 协议,header 要手动注入 token + X-Env。
🔴 坑位:这条不走
request.ts拦截器 → 没有自动 rotate(token 过期时不会自动续期,直接失败)。 上传前请自查 token 有效性;失败重试需自行处理。
curl
bash
# --form 让 curl 自动生成 multipart boundary + filename
curl -s "$BASE_URL/api/v1/upload?type=avatar" \
-H 'X-Env: Prd' \
-H "Authorization: Bearer $ACCESS" \
-F 'file=@/path/to/avatar.jpg'
# 强制本地 fallback(应急 / 调试)
curl -s "$BASE_URL/api/v1/upload?type=avatar&localFallback=1" \
-H 'X-Env: Prd' \
-H "Authorization: Bearer $ACCESS" \
-F 'file=@/path/to/avatar.jpg'请求
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type (query) | string | ✔ | 头像传 'avatar'。净化正则 /^[a-z0-9_-]{1,32}$/(下划线合法)→ 不符静默兜底 'misc'(不报错) |
file (multipart) | File | ✔ | 字段名必须叫 file;stdlib 解析单 file 字段 |
Authorization | header | ✔ | Bearer <access> —— 页面手动注入 |
X-Env | header | ✔ | 页面手动注入(漏了会落到默认库) |
localFallback (query) | string | ✖ | '1' = 强制本地存储(应急/调试);缺省 COS 优先 |
x-upload-password | header | ✖ | 仅 xiaomi_zip 用(不进 URL 日志),头像不需要 |
约束:
| 项 | 值 |
|---|---|
| 大小上限 | 50MB(CZ_UPLOAD_MAX_SIZE) |
| MIME 白名单 | image/jpeg · image/png · image/webp · application/zip · application/octet-stream |
| 存储派发 | COS 配置齐全 → COS(优先 CDN cos-cdn.qingmulife.cn);配置缺失 / putObject 抛错 → 静默 fallback 本地 |
响应 data
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
url | string | 否 | COS/CDN URL 或本地 /uploads/{type}/... 路径(依环境配置)—— 前端消费这个做头像回显 |
size | number | 否 | int,字节数(后端返回,前端未声明) |
mime | string | 否 | 回显上传 MIME(后端返回,前端未声明) |
source | string | 否 | 'cos' | 'local'(后端返回,前端未声明) |
uploadRecordId | string | 条件出现 | 仅 type ∈ RECORD_TYPES(7 个数据包/截图 type)且 source='cos' —— avatar 不出现 |
错误码(段 40419xxx)
| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
40419003 | 400 | multipart 里没有 file 字段(NO_FILE) | 检查字段名是否叫 file |
40419004 | 400 | MIME 不在白名单(UNSUPPORTED_MIME) | 前置校验图片类型 |
40419005 | 413 | 文件 > 50MB(FILE_TOO_LARGE) | 前置压缩 / 提示超限 |
40419002 | 401 | 未登录 / token 无效 | 无自动 rotate —— 手动刷新 token 后重试 |
40419001 | 400 | 参数非法 | 检查 type |
40419006 | 404 | UploadRecord 不存在(worker 侧) | 用户无感 |
本页合同要点速记
| 端点 | 最容易踩的点 |
|---|---|
myTodayHealth | 块状结构 —— steps.value / todayActivity.totalDistanceKm;顶层无 distance;spo2 恒 null |
getTodayOverview | 嵌套两层 —— data.calorie.budgetKcal,不是 data.budgetKcal |
upload | multipart + 手动 header + 无自动 rotate + 字段名必须 file |