---
url: /docs/guide/uniapp/api/stats.md
---
# Stats 今日概览（前端调用视角）

> 后端契约：[手册 · page-direct#gettodayoverview](/api-docs/v1/frontend/page-direct#gettodayoverview)（字段类型 / curl / 错误码 —— 本页零复述）。
> 调用现状：**页面直调**（未收敛为 `src/api/*.ts`）—— 首页 `src/pages/index/index.vue:105-146` `loadToday()`。消费页：[pages/index](../pages/index.md)

## getTodayOverview（页面直调 · 首页热量 / 连续天数卡）

**一句话**：首页 `loadToday()` 在 `onShow` 时拉一次 —— 给三环提供**目标值**（预算热量）与当日打卡统计；「实际值」（步数 / 消耗 / 距离）由 `myTodayHealth` 提供（见 [device · myTodayHealth](./device.md)）。

**前端函数签名**

| 项 | 值 |
|---|---|
| 封装 | **页面直调**（未收敛）—— `src/pages/index/index.vue:105-146` `loadToday()` |
| 触发时机 | 首页 `onShow`（`index.vue:147`）；与 `myTodayHealth` 并行（同一 `Promise.all`） |
| 鉴权 | `Bearer access`（`request.ts` 默认档） |

**必需 payload 键**

| 键 | 从哪来 | 必填 |
|---|---|---|
| `action` | 字面量 `'getTodayOverview'` | ✔ |
| `payload` | 该 action 无入参 —— 前端连 `payload` 键都没传，后端按忽略处理 | ✖ |

**响应读法**（⚠️ **两层嵌套** —— 曾读顶层导致「热量目标恒 400」，前端已修，勿写回）

```ts
const res = await request({ url: 'api/v1/stats', method: 'post', data: { action: 'getTodayOverview' } });
const c = res?.data?.calorie;                          // 中文注解：先取 calorie 块 —— 它是嵌套对象，不是顶层字段
calorieGoal.value = c?.budgetKcal ?? 400;              // 中文注解：⚠️ budgetKcal 现可能为 null（画像不完整）—— `?? 400` 会把空值伪装成有数据，应改为判空分支
const doneCount = res?.data?.checkin?.count ?? 0;      // 中文注解：连续打卡天数卡
const doneKm    = res?.data?.checkin?.distanceKm ?? 0; // 中文注解：同日打卡距离合计
```

> ✅ **正确读法**：`res.data.calorie.budgetKcal`（先 `calorie` 再取键）
> ❌ **错误读法**：`res.data.budgetKcal` —— 恒 `undefined` → `?? 400` 兜底生效 → **热量目标静默恒为默认 400**。
> **历史（防回归）**：这个 bug 在生产真实存在过（前端 `api` 分支 `24902e9`，2026-09-20 修复）；
> 根因是文档把 `data.calorie.budgetKcal` 简写成扁平的 `{budgetKcal, ...}`，与后端自始的嵌套结构不一致 ——
> **本页现在写的是修正后的真值，勿改回。**
> ⚠️ 另注：`400` 是 `index.vue:87` 的**渲染初值**，**不是契约值**；修复前生产实测 `calorie.budgetKcal = 1423`
> —— 那个 1423 是**后端画像没读到、用默认资料（30岁/165cm/60kg）编出来的假数**（2026-09-20 定位）。
> 现契约：画像完整 → 真实资料算出的值；**画像缺失 → `null`（不再编数）** —— 见下表。

**响应真值（嵌套两层）**

| 路径 | 前端拿它干什么 |
|---|---|
| `data.date` | 今日（CN 时区 `YYYY-MM-DD`）—— 与设备侧 `myTodayHealth.date` 同口径，可对拍 |
| `data.calorie.budgetKcal` / `.remainingKcal` | 三环**目标值** / 剩余额度（服务端按 BMR 算；**画像不完整时为 `null`** —— 2026-09-20 起不编默认值） |
| `data.calorie.intakeKcal` / `.burnKcal` | 摄入 / 消耗（无数据为 `0`，不是 null —— 判空时别把 `0` 当"没数据"） |
| `data.calorie.profileComplete` | 画像是否完整；`false` ⇒ `budgetKcal`/`remainingKcal` 为 **`null`**（不编数）→ 前端**必须判空**（提示补全资料 / 隐藏三环），**禁止** `?? 400` 之类兜底把空值伪装成有数据（`400` 只是渲染初值） |
| `data.checkin.count` | 当日打卡数（连续天数卡） |
| `data.checkin.distanceKm` | 当日打卡距离 |

> 字段类型 / 可空性 / 错误码见 [手册 · page-direct#gettodayoverview](/api-docs/v1/frontend/page-direct#gettodayoverview)。

**失败时的前端动作**

| 情形 | 前端动作 |
|---|---|
| 任意业务失败 | `catch` 内 `console.error`，UI 保默认值 —— **不弹 toast**（首页首屏不打扰；`request.ts` 的非 200 分支仍会 toast，属兜底） |
| ⚠️ **`Promise.all` 一损俱损** | `loadToday()` 是 `Promise.all([stats, myTodayHealth])`：**stats 失败会连带丢弃 `myTodayHealth` 已经成功拿到的数据**（两者走同一个 `catch`）。旧表述「单接口失败不阻塞首页」**只对了"首页不崩"**，漏了这层耦合 —— 结果是「热量卡无目标 + 健康卡明明有数据却也不更新」。要让两者各自降级需改前端为 `Promise.allSettled`（前端改动，非本轮文档范围） |
| 401 | `request.ts` 拦截器 rotate 单飞续期并重放一次（与 [手册 G5](/api-docs/v1/frontend/#g5-·-rotate-由拦截器自动调用-——-不是业务直调) 一致）；失败则强登出回登录页 —— 页面 `catch` 不额外处理 |

**⚠️ 坑位**

* **嵌套 vs 平铺**：同类问题已出现 **2 次**（本端点把 `calorie.budgetKcal` 读成顶层；`myTodayHealth` 顶层 `distance` vs `todayActivity.totalDistanceKm`）。
  写新代码前**先把响应 `console.log` 出来看一眼层级**，别照平铺猜 —— 后端 29/30 端点无 Pydantic 响应模型，读错层级不会有任何报错。
* **`40422xxx` 与 `ranking` 硬撞车**：stats 源码用 `40422xxx`，而 [错误码注册表](/ai-contexts/error-code-registry) 把该段登记为 `ranking` ——
  前端**无法靠前缀区分**两者异常。属台账 #6 的根因，见 [联调台账](/internal-wiki/operations/frontend-integration-issues)。
* `X-Env` 全程一致（与 [手册 G2](/api-docs/v1/frontend/#g2-请求头) 一致）；App 产线包 `X_ENV = "Prd"`（`src/config/index.ts` + `build-prod.sh` 构建期硬断言）。
