---
url: /docs/api-docs/v1/frontend/user.md
---
# 前端联调 · 用户资料（user.ts）

> 归属：[前端联调 API 手册](./) → `src/api/user.ts`
> 端点：**7 个** · 全部为 user 域 **action 协议**（`POST /api/v1/user`）
> 错误码段：`40301xxx`（全表见 [错误码注册表](/ai-contexts/error-code-registry)）
> 错误契约：**HTTP 400 / 401 / 404 + `{code, msg, data}`**（与 oauth 域不同 —— 不走 HTTP 200）
> 取值语义：**[取值速查 · user](./values#user)** —— `habits.*` 每个取值打什么画像标签 / 怎么影响人群分型、`audience` A/B/C 的判据、`planId` 的**人群相关合法集**、各字段「不传 vs 传 0 vs 传 null」的区别
> 深入说明：[User API Docs](/api-docs/v1/user/actions)

## 本页 curl 变量

```bash
BASE_URL=https://api.qingmulife.cn   # 本地：http://localhost:3000
ACCESS=<access_token>
ENV=Prd                              # Dev | Prd —— 全程必须一致
USER_ID=<登录响应 data.user.id>       # 仅前端本地使用，请求体不需要（身份以 token 为准）
```

> 全部端点需 `Bearer access` + `X-Env`。请求体统一 `{"action": "...", "payload": {...}}`。

## 端点一览

| # | action | 场景 |
|---|---|---|
| [1](#updatebasicprofile) | `updateBasicProfile` | 向导 step1：性别/年龄/身高/体重 4 维独立提交 |
| [2](#submitonboarding) | `submitOnboarding` | 向导 step2：生活习惯 → 返画像摘要（**不标 done**） |
| [3](#onboardingsuggestion) | `onboardingSuggestion` | 向导 step3：方案推荐（目标 + 3 档计划 + 安全提示） |
| [4](#adoptplanoption) | `adoptPlanOption` | 采用某档计划（服务端重算 deficit） |
| [5](#completeonboarding) | `completeOnboarding` | 向导收尾：标记 `onboardingDone=true` |
| [6](#myprofile) | `myProfile` | 我的页 / 个人资料回显 |
| [7](#updateprofile) | `updateProfile` | 个人资料编辑保存 |

***

## updateBasicProfile

**一句话**：向导第一步 —— 4 维基础资料独立提交（可先跑通这一段再进下一步）。

### curl

```bash
curl -s "$BASE_URL/api/v1/user" \
  -H 'Content-Type: application/json' \
  -H 'X-Env: Prd' \
  -H "Authorization: Bearer $ACCESS" \
  -d '{"action":"updateBasicProfile","payload":{
        "gender":"male","age":30,"height":175,"weight":70
      }}'
```

### 请求

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `gender` | `'male'` | `'female'` | ✔ | Literal 两值，其他值 → `40301001` |
| `age` | number | ✔ | int，**5 ≤ age ≤ 120** |
| `height` | number | ✔ | float，**50 ≤ height ≤ 250**（写入时 `cz_js_round` 取整） |
| `weight` | number | ✔ | float，**10 ≤ weight ≤ 300** |

### 响应 `data`

| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `gender` | string | 否 | 回显 |
| `age` | number | 否 | int |
| `height` | number | 否 | float —— **取整后的值**（如 `175.0`） |
| `weight` | number | 否 | float |

### 错误码

| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
| `40301001` | 400 | 任一字段超范围 / `gender` 非两值 | 表单即时校验，提示具体字段 |
| `40301002` | 401 | 未登录 / token 无效 | 走拦截器 401 分档 |
| `40301013` | 400 | 年龄非法（专用码） | 提示年龄范围 |

***

## submitOnboarding

**一句话**：向导第二步 —— 提交生活习惯，返画像摘要（**这一步不标 done**，收尾靠 `completeOnboarding`）。

### curl

```bash
curl -s "$BASE_URL/api/v1/user" \
  -H 'Content-Type: application/json' \
  -H 'X-Env: Prd' \
  -H "Authorization: Bearer $ACCESS" \
  -d '{"action":"submitOnboarding","payload":{
        "gender":"male","age":30,"height":175,"weight":70,
        "habits":{
          "exerciseFreq":3,
          "exercisePrefs":["run","walk"],
          "exercisePrefRemark":"每周三次慢跑",
          "takeoutFreq":5,
          "sugarDrink":2,
          "lateDinner":false
        }
      }}'
```

### 请求

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `gender` | `'male'` | `'female'` | `'unknown'` | ✔ | 后端比前端更宽（含 `unknown`） |
| `age` | number | ✔ | int，5–120 |
| `height` | number | ✔ | float，50–250 |
| `weight` | number | ✔ | float，10–300 |
| `habits.exerciseFreq` | number | ✖ | int，**0–7**（每周运动次数）。**`0` 是有效值**= 不运动（久坐）→ tag「久坐少动」+ 分型走 A 类；`1~3` 偶尔运动；`≥4` 运动规律；**`≥3` 才有「运动基础」**（分型判据）。**不传 → 后端按 0**（2026-09-20 起改可选 — 前端不再采集，后端按打卡数据校正）。→ [取值语义](./values#user) |
| `habits.exercisePrefs` | string\[] | ✔ | 值域 `'run'` | `'cycling'` | `'walk'` | `'strength'` | `'other'`。⚠️ **本 action 无 1–4 长度限制**（`CzHabitsPayload` 无 min/max）—— 空数组 `[]` 合法；1–4 的限制在 `updateProfile` |
| `habits.exercisePrefRemark` | string | ✖ | 含 `other` 时**必填**（≤200）—— 含 `other` 却为空 → `40301001` |
| `habits.takeoutFreq` | number | ✖ | int，0–21 |
| `habits.sugarDrink` | number | ✖ | int，0–20 |
| `habits.lateDinner` | boolean | ✖ | 是否常吃宵夜 |
| `nickname` / `zodiac` / `bodyFat` | — | ✖ | schema 支持但向导链路不收集 |

### 响应 `data`

| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `audience` | string | 否 | 人群分型 |
| `bmi` | number | 否 | float |
| `bmiLabel` | string | 否 | BMI 文案标签 |
| `bmr` | number | 否 | int，基础代谢 |
| `tags` | string\[] | 否 | 画像标签 |
| `summary` | string | 否 | 画像摘要文案 |
| `minorMode` | boolean | 否 | 未成年模式 |

### 错误码

| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
| `40301001` | 400 | 字段超范围 / `exercisePrefs` 含 `other` 但无 `exercisePrefRemark` | 表单即时校验（前端注释已对齐此规则 ✅） |
| `40301002` | 401 | 未登录 | 走拦截器 401 分档 |

***

## onboardingSuggestion

**一句话**：向导第三步 —— 拉取目标建议 + 3 档计划选项 + 安全提示。

### curl

```bash
curl -s "$BASE_URL/api/v1/user" \
  -H 'Content-Type: application/json' \
  -H 'X-Env: Prd' \
  -H "Authorization: Bearer $ACCESS" \
  -d '{"action":"onboardingSuggestion","payload":{}}'
```

### 请求

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `payload` | object | ✖ | 忽略 —— `user_id` 来自 token |

### 响应 `data`

| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `audience` | string | 否 | 人群分型 |
| `minorMode` | boolean | 否 | 未成年模式 |
| `goal` | object | **null** | **是** | 目标建议块；null 时前端需兜底展示 |
| `goal.targetWeightMin` / `targetWeightMax` | number | 否 | float |
| `goal.weeks` | number | 否 | int |
| `goal.maxWeeklyLossPct` | number | 否 | float，周最大减重比例 |
| `goal.note` | string | 否 | 说明文案 |
| `plans[]` | object\[] | 否 | 3 档计划（`gentle` / `standard` / `intense`） |
| `plans[].id` | string | 否 | `gentle` | `standard` | `intense` —— 原样回传给 `adoptPlanOption` |
| `plans[].name` / `composition` / `reason` | string | 否 | 展示文案 |
| `plans[].sessionsPerWeek` / `deficitKcal` | number | 否 | int |
| `plans[].recommended` | boolean | 否 | 是否推荐档 |
| `safetyNote` | string | **null** | **是** | 安全提示；null 时不展示该区块 |

### 错误码

| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
| `40301002` | 401 | 未登录 | 走拦截器 401 分档 |
| `40301010` | 400 | 画像不完整（未先走 step1/2） | 引导回向导前一步 |

***

## adoptPlanOption

**一句话**：采用某档计划 —— 服务端**重算** deficit，不信任前端传的数值。

### curl

```bash
curl -s "$BASE_URL/api/v1/user" \
  -H 'Content-Type: application/json' \
  -H 'X-Env: Prd' \
  -H "Authorization: Bearer $ACCESS" \
  -d '{"action":"adoptPlanOption","payload":{"planId":"standard"}}'
```

### 请求

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `planId` | string | ✔ | Literal `'gentle'` | `'standard'` | `'intense'`（TS 标注为 `string`，实际值域受限） |

### 响应 `data`

| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `planOption` | string | 否 | 回显采用的档位 |
| `dailyDeficitKcal` | number | 否 | **服务端重算** —— 不是前端传的值 |
| `sessionsPerWeek` | number | 否 | int |

### 错误码

| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
| `40301011` | 400 | `planId` 非法（PLAN\_OPTION\_INVALID） | 用 `onboardingSuggestion` 返回的 `plans[].id`，别硬编码 |
| `40301010` | 400 | 画像不完整 | 引导回向导 |

***

## completeOnboarding

**一句话**：向导收尾 —— 标记 `onboardingDone = true`（新用户下次登录不再弹向导）。

### curl

```bash
curl -s "$BASE_URL/api/v1/user" \
  -H 'Content-Type: application/json' \
  -H 'X-Env: Prd' \
  -H "Authorization: Bearer $ACCESS" \
  -d '{"action":"completeOnboarding","payload":{}}'
```

### 请求

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `payload` | object | ✖ | 忽略 |

### 响应 `data`

| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `ok` | boolean | 否 | 恒 `true` |

### 错误码

| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
| `40301002` | 401 | 未登录 | 走拦截器 401 分档 |

***

## myProfile

**一句话**：拉当前用户完整画像 —— 我的页 / 个人资料页回显。

### curl

```bash
curl -s "$BASE_URL/api/v1/user" \
  -H 'Content-Type: application/json' \
  -H 'X-Env: Prd' \
  -H "Authorization: Bearer $ACCESS" \
  -d '{"action":"myProfile","payload":{}}'
```

### 请求

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `payload` | object | ✖ | 忽略（`user_id` 来自 token） |

### 响应 `data` ⚠️ 动态 dict（**无 schema**）

响应 = **画像行 dict 合并 account 段动态组出**，键集无法静态枚举。已知分组：

| 分组 | 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|---|
| 画像 · 身份 | `userId` / `nickname` / `avatarUrl` / `signature` / `signatureSource` | string | 是 | `signatureSource` = `'custom'` | `'agent'` |
| 画像 · 偏好 | `exercisePrefs` / `exercisePrefRemark` / `profileTags` | array | 是 | jsonb → list，默认 `[]` |
| 画像 · 4 维 | `gender` / `age` / `height` / `weight` / `audience` / `zodiac` | — | **是** | agent 未分析时**可能全为 null** |
| 画像 · 计划 | `habits` / `planOption` / `dailyDeficitKcal` / `sessionsPerWeek` / `fitness` / `agentAnalyzedAt` | — | 是 | 同上 |
| **account 段** | `points` / `totalPointsEarned` / `memberExpireAt` / `growthLevel` / `onboardingDone` | — | 是 | **user\_core 未装配 / 异常时 5 键整体消失** |
| 时间戳 | `updatedAt` | string ISO | 是 | ⚠️ **仅 PG 实现有；InMemory 默认建档无此键** |

> ⚠️ **待确认 #4**：无 schema，**只能按字段存在性消费**，不要做全等断言。前端类型是 `Record<string, any>` —— 读 `p.exercisePrefs` 这类画像段字段是安全的（account 段缺席不影响）。

### 错误码

| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
| `40301002` | 401 | 未登录 | 走拦截器 401 分档 |
| `40301004` | 404 | 用户不存在 | 强登出 |

***

## updateProfile

**一句话**：个人资料编辑保存（昵称 / 头像 / 签名 / 运动偏好）。

### curl

```bash
curl -s "$BASE_URL/api/v1/user" \
  -H 'Content-Type: application/json' \
  -H 'X-Env: Prd' \
  -H "Authorization: Bearer $ACCESS" \
  -d '{"action":"updateProfile","payload":{
        "nickname":"前端E2E用户",
        "signature":"跑起来就对了",
        "exercisePrefs":["run","cycling"],
        "exercisePrefRemark":"每周 4 跑 1 骑"
      }}'
```

### 请求

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `nickname` | string | ✖ | ≤ 32。✅ 2026-09-20 起**双写** oauth 主表（此前只写画像表 → 重登显示旧昵称） |
| `avatarUrl` | string | **null** | ✖ | 🔴 **传 `null` = 不修改（无法置空）** —— 后端「仅非 None 字段写」 |
| `signature` | string | **null** | ✖ | ≤ 60。🔴 **传 `null` = 不修改（无法清空）**；写非空值时 `signatureSource='custom'` |
| `exercisePrefs` | string\[] | ✖ | **min 1 / max 4** —— ⚠️ **空数组 → `40301001`**（前端已规避：页面空数组时不传） |
| `exercisePrefRemark` | string | null | ✖ | ≤ 200；`exercisePrefs` 含 `other` 时必填 |

> 🔴 **坑位 · 传 `null` 无法清空**：前端 TS 类型写的是 `string \| null`（暗示可清空），但后端语义是「仅非 None 字段写」→ 传 `null` 被**静默忽略**。
> 当前调用点用 `undefined`（不传）所以没踩；**一旦实现「清空签名」传 `null` 就会静默失效**。[台账 · 潜在问题 #3](/internal-wiki/operations/frontend-integration-issues)

### 响应 `data`

| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `user.nickname` | string | null | 是 | |
| `user.avatarUrl` | string | null | 是 | |
| `user.signature` | string | null | 是 | |
| `user.signatureSource` | `'custom'` | `'agent'` | 否 | PG server\_default 保证非 null |
| `user.exercisePrefs` | string\[] | 是 | jsonb default `[]` |
| `user.exercisePrefRemark` | string | null | 是 | |
| `user.profileTags` | string\[] | 是 | jsonb default `[]` |

> ⚠️ **待确认 #5**：响应 `user` 是**画像行 dict 直出**（含 `userId` / `gender` / `age` / … / `updatedAt` 全列，远超上面声明的 7 键）。上面 7 键都在 ✅，其余键未定义。

### 错误码

| 业务码 | HTTP | 触发条件 | 前端建议处置 |
|---|---|---|---|
| `40301001` | 400 | `nickname` > 32 / `signature` > 60 / **`exercisePrefs` 空数组** / 含 `other` 无 remark | 表单即时校验；空数组**不传**该字段 |
| `40301002` | 401 | 未登录 | 走拦截器 401 分档 |
