---
url: /docs/guide/uniapp/api.md
---
# uniapp API 调用契约 · 总览与分工边界

> 本目录是 **uniapp 前端调用视角**的 API 文档 —— 回答「哪个页面在什么时机调、响应怎么读、失败时前端做什么」。
> **不回答**「这个字段是什么类型、错误码是几、curl 怎么写」—— 那些一律去 [前端联调 API 手册](/api-docs/v1/frontend/)。

## 一、分工边界（端到端 · 先看这张表再看任何一页）

四套文档并存，**接触面只有"锚点"一个** —— 接触面越窄，越不会互相矛盾。

```
提问                                  → 去哪
────────────────────────────────────────────────────────────────────
某个字段的类型 / 必填 / 可空            → 手册（契约权威）
某个错误码的码值 / HTTP / 触发条件      → 手册
想直接 curl 一条试试                    → 手册
某页面在哪个生命周期调了哪个函数        → 本目录 api/*.md
响应应该从哪一层读（嵌套 vs 顶层）      → 本目录 api/*.md   ★ 历史 bug 高发区
失败了前端该做什么（提示？回退？登出？） → 本目录 api/*.md
从零把登录链搭起来（SDK/基座/manifest） → OAuth 落地手册
架构 / 分域 / 部署 / 运维               → internal-wiki
```

| 内容 | 唯一归属 | 另一侧怎么写 |
|---|---|---|
| 请求/响应**字段表**（类型、必填、可空） | [前端联调 API 手册](/api-docs/v1/frontend/) | 本目录只写**读法路径**（`res.data.calorie.budgetKcal`），**不写类型** |
| **错误码具体码值** + HTTP + 触发条件 | 手册 | 本目录只写**前端动作**（「提示并回退重拉」）+ 指锚点 |
| 全部 **curl** / `BASE_URL` 变量块 | 手册 | 本目录**不出现 curl** |
| **字段取值语义**（每个值意味着什么 / 缺省行为 / 边界值） | [取值速查](/api-docs/v1/frontend/values) | 本目录只在踩坑处指锚点，不复制取值表。⚠️ 2026-09-20 新增 —— 起因是一条真实联调 BUG：`habits.exerciseFreq` 文档只写「0–7」，前端**不知道 0 是有效值**（写 0 就通），卡了一轮 |
| **通用约定 G1-G7** | 手册 [通用约定](/api-docs/v1/frontend/#通用约定-7-条-·-所有端点共用) | 本目录写一句「与手册 G2 一致」 |
| **10 条头号坑位** | 手册 [头号坑位速查](/api-docs/v1/frontend/#🔴-头号坑位速查-联调前必读-踩了会浪费半天) + [联调台账](/internal-wiki/operations/frontend-integration-issues) | 本目录只在**自己页面确实踩到**时指锚点 |
| 真机/框架坑（编译器锁版本、`t.__f__` 崩溃、`www` 缓存、`adb logcat`、验证码渲染） | [guide/uniapp 铁律](/guide/uniapp/) | 手册一字未提 —— 那是 guide 的**独占资产** |
| 页面 ↔ API 映射、调用链时序、`storage` 键名 | 本目录 `api/*.md` + `pages/*.md` | 手册只在 `page-direct` 涉及 3 端点 |
| OAuth 从零施工（拷 SDK / 自定义基座 / 微信登录 manifest） | [OAuth 登录链落地手册](/api-docs/v1/oauth/uniapp-dev-guide) | 该页页首已声明「契约见手册」 |

> ⚠️ **一致性的兜底是脚本，不是自觉**：`scripts/check_uniapp_api_docs.py`（pre-commit 钩子）机械校验
> ① 矩阵 30 端点覆盖 ② 已证伪字面量（历史错误契约）③ 字段路径层级 ④ 跨目录死链。
> 已知边界：该脚本抓不到**语义坑**（字段名对但含义错）与**可空性/枚举值域/常量** —— 那些只能靠人工对拍矩阵。

### 本目录已知的读法坑（扩展前必读）

* **`request` 不解包**：`const res = await xxxApi()` 拿到的是**完整响应体**（含 `code`/`msg`/`data`），业务字段在 `res.data` 之下
* **嵌套 vs 平铺**：响应里的分组对象（如 `data.calorie.*` / `data.checkin.*`）**不是**顶层字段。写错层级 ⇒ 恒 `undefined` ⇒ `?? 默认值` 兜底 ⇒ **需求静默失效**。
  同类事故已出现 **2 次**（首页热量目标恒 400 / 健康卡距离字段）
* **业务失败可能不是 reject**：部分端点失败仍返回 `HTTP 200 + code "0"`，只是 `data` 为 `null` 或 `ok: false` —— **必须判空、必须自查 `ok`**，否则出现"假成功"
* **两套错误契约并存**：`oauth` 是「HTTP 200 + `40101xxx`」；`user`/`device`/`training`/`stats`/`upload` 是「**HTTP 400/401/404 + `code`**」。
  按其中一套写拦截器会漏掉另一套 —— 细节见 [手册 G4](/api-docs/v1/frontend/#g4-两套错误契约并存)

## 二、阅读顺序

按「从登录到业务」的实际施工顺序读，每一步都建立在前一步的产物之上：

| 步 | 页面 | 解决什么 |
|---|---|---|
| 1 | **本页** | 分工边界 + 已知读法坑（不读这页，后面每一页都会读错层次） |
| 2 | [api/oauth](./oauth.md) | **登录链** —— token 从哪来、怎么续（rotate）、怎么登出。**所有后续请求的前提** |
| 3 | [api/user](./user.md) | 新用户向导链（三页向导的调用顺序、画像与习惯字段） |
| 4 | [api/splash](./splash.md) | 开机信息屏（启动即拉，独立于登录态） |
| 5 | [api/device](./device.md) | 设备线（厂商列表 / 授权 URL / 绑定与解绑） |
| 6 | [api/training](./training.md) | 训练计划（报名 / 任务 / 打卡 / 提醒） |
| 7 | [api/stats](./stats.md) | 首页今日概览 —— **含嵌套读法防回归块**（历史 bug 现场） |
| 8 | [api/agreement](./agreement.md) | 用户协议 / 隐私政策（免登录，登录页就要用） |
| 9 | [api/page-direct](./page-direct.md) | **页面直调**（不走 `src/api/*.ts` 的 3 个端点：首页健康卡 / 热量卡 / 头像上传） |

> 只想查一个端点 → 直接跳 [第四节 反查表](#四、端点-→-文档-反查表)（锚点由 VitePress 从标题生成）。

## 三、本目录文件一览

| 文件 | 覆盖端点 | 覆盖的前端文件 / 触发方 |
|---|---|---|
| [api/oauth](./oauth.md) | 7 | `src/api/login.ts` + `src/utils/request.ts`（rotate 拦截器） |
| [api/user](./user.md) | 7 | `src/api/user.ts` |
| [api/splash](./splash.md) | 2 | `src/api/splash.ts` |
| [api/device](./device.md) | 4 | `src/api/device.ts` |
| [api/training](./training.md) | 5 | `src/api/training.ts` |
| [api/stats](./stats.md) | 1 | **页面直调** `src/pages/index/index.vue` |
| [api/agreement](./agreement.md) | 2 | `src/api/agreement.ts` |
| [api/page-direct](./page-direct.md) | 3 | **页面直调**（`index.vue` ×2 + `personal-form.vue` ×1） |
| | **30** | 与 [手册 30 端点总表](/api-docs/v1/frontend/#_30-端点总表) 同源（`tests/reports/contract-matrix.json`） |

> `training` 另有 1 个**前端未封装**的端点（`POST /api/v1/training/leave`，**不在 30 端点矩阵内**）——
> 已单列在 [training](./training.md) 并标注「契约未审计」。

## 四、端点 → 文档 反查表

> 对齐 [手册 30 端点总表](/api-docs/v1/frontend/#_30-端点总表) 的逐条编号（`#` 列两侧同号）。
> 「前端函数」列给出**消费视角的入口**（业务直调 / 拦截器 / 页面直调）—— **字段与码值以手册为准**。
> ⚠️ 本表只证明端点**被索引**，不证明**被写透** —— 每页的完成度看该页自身。

| # | 方法 | 路径 | 前端函数 / 触发方 | 消费视角文档 |
|---|---|---|---|---|
| 1 | POST | `/api/v1/auth/oauth` | `getCaptcha()` | [oauth](./oauth.md) |
| 2 | POST | `/api/v1/auth/oauth` | `sendSmsCode()` | [oauth](./oauth.md) |
| 3 | POST | `/api/v1/auth/oauth` | `smsLogin()` | [oauth](./oauth.md) |
| 4 | POST | `/api/v1/auth/oauth` | `rotateOnce()`（**拦截器自动**，非业务直调） | [oauth](./oauth.md) |
| 5 | POST | `/api/v1/auth/oauth` | `listSessions()` | [oauth](./oauth.md) |
| 6 | POST | `/api/v1/auth/oauth` | `revokeSession()` | [oauth](./oauth.md) |
| 7 | POST | `/api/v1/auth/oauth` | `logout()` | [oauth](./oauth.md) |
| 8 | POST | `/api/v1/user` | `updateBasicProfile()` | [user](./user.md) |
| 9 | POST | `/api/v1/user` | `submitOnboarding()` | [user](./user.md) |
| 10 | POST | `/api/v1/user` | `getOnboardingSuggestion()` | [user](./user.md) |
| 11 | POST | `/api/v1/user` | `adoptPlanOption()` | [user](./user.md) |
| 12 | POST | `/api/v1/user` | `completeOnboarding()` | [user](./user.md) |
| 13 | POST | `/api/v1/user` | `myProfile()` | [user](./user.md) |
| 14 | POST | `/api/v1/user` | `updateProfile()` | [user](./user.md) |
| 15 | GET | `/api/v1/device/vendors` | `listVendors()` | [device](./device.md) |
| 16 | POST | `/api/v1/device/vendors/{id}/auth-url` | `getAuthUrl()` | [device](./device.md) |
| 17 | POST | `/api/v1/device` | `listMyBindings()` | [device](./device.md) |
| 18 | POST | `/api/v1/device` | `unbindVendor()` | [device](./device.md) |
| 19 | GET | `/api/v1/training/plans` | `listTrainingPlans()` | [training](./training.md) |
| 20 | POST | `/api/v1/training/enroll` | `enrollTrainingPlan()` | [training](./training.md) |
| 21 | GET | `/api/v1/training/tasks` | `listMyTasks()` | [training](./training.md) |
| 22 | POST | `/api/v1/training/task/checkin` | `checkinTask()` | [training](./training.md) |
| 23 | GET | `/api/v1/training/reminders` | `listMyReminders()` | [training](./training.md) |
| 24 | GET | `/api/v1/splash-screen` | `getSplashScreen()` | [splash](./splash.md) |
| 25 | POST | `/api/v1/splash-screen/track` | `trackSplashEvent()` | [splash](./splash.md) |
| 26 | GET | `/api/v1/content/agreements` | `listAgreements()` | [agreement](./agreement.md) |
| 27 | GET | `/api/v1/content/agreements/{type}` | `getAgreement()` | [agreement](./agreement.md) |
| 28 | POST | `/api/v1/device` | **页面直调** `index.vue`（健康卡） | [page-direct](./page-direct.md) |
| 29 | POST | `/api/v1/stats` | **页面直调** `index.vue`（热量卡） | [page-direct](./page-direct.md) |
| 30 | POST | `/api/v1/upload?type=avatar` | **页面直调** `personal-form.vue`（头像） | [page-direct](./page-direct.md) |

> 反查表逆用：知道自己在改哪个 `.ts` → 找上表「前端函数」列 → 点进对应页。
> 注意 **`/api/v1/user` 与 `/api/v1/device` 是多 action 共用一个路径**：路径相同、`action` 不同，
> 光看 URL 无法区分端点是哪一个（`action` 名与入参见 [手册总表](/api-docs/v1/frontend/#_30-端点总表)）。
