---
url: /docs/api-docs/v1/user/actions.md
---
# user —— C 端用户模块（15 action）

> 📘 前端联调速查（30 端点手册）：[frontend/user.md](/api-docs/v1/frontend/user)
> 统一入口：`POST /api/v1/user`
> body = `{ "action": "<15种之一>", "payload": { ... } }`
> 鉴权：**全部 action 需已登录** —— `Authorization: Bearer <access_token>`（OAuth 登录所得）
> 契约权威：`src/app/user/interface/api/user_actions.py`（V1 action 协议 100% 兼容 — 客户端契约不变）

## 15 个 Action 一览

| Action | 鉴权 | 场景 | 详细 |
|---|---|---|---|
| `completeOnboarding` | Bearer | 完成新手引导（onboardingDone=true） | [→ §1](#1-completeonboarding) |
| `resetOnboarding` | Bearer | 重新走引导（onboardingDone=false） | [→ §2](#2-resetonboarding) |
| `bindApps` | Bearer | 绑定手机/邮箱/密码/用户名 | [→ §3](#3-bindapps) |
| `bindInviter` | Bearer | 绑定邀请人（双方奖励） | [→ §4](#4-bindinviter) |
| `checkReportQuota` | Bearer | 报告免费周配额查询 | [→ §5](#5-checkreportquota) |
| `redeemMember` | Bearer | 积分兑换会员时长 | [→ §6](#6-redeemmember) |
| `submitOnboarding` | Bearer | 引导步骤 1-2：存资料 + 画像 | [→ §7](#7-submitonboarding) |
| `onboardingSuggestion` | Bearer | 引导步骤 4：目标 + 计划方案 | [→ §8](#8-onboardingsuggestion) |
| `adoptPlanOption` | Bearer | 采纳计划方案 | [→ §9](#9-adoptplanoption) |
| `precheckDeleteAccount` | Bearer | 注销前置检查 | [→ §10](#10-precheckdeleteaccount) |
| `deleteAccount` | Bearer | 账号注销（不可逆） | [→ §11](#11-deleteaccount) |
| `updateBasicProfile` | Bearer | 新用户弹窗：基础资料 4 维（V2 扩展） | [→ §12](#12-updatebasicprofile) |
| `myProfile` | Bearer | 读全量画像 + 账号概况（我的页回显） | [→ §13](#13-myprofile) |
| `updateProfile` | Bearer | 画像编辑（昵称/头像/签名/运动偏好） | [→ §14](#14-updateprofile) |
| `fitnessProfile` | Bearer | GIS 配速参数读（App 运动页初始化） | [→ §15](#15-fitnessprofile) |

## 公共约定

### 统一信封（V2 全局）

* **成功**：`{ "code": "0", "msg": "ok", "data": { ... } }`
* **失败**：`{ "code": "40301xxx", "msg": "<字段路径>: <原因>" }`
* **响应 data 为 camelCase**（V1 客户端契约，如 `memberExpireAt` / `onboardingDone` / `weeklyUsed`）

### HTTP 状态映射（⚠️ 与 admin 的「默认 200」不同 — V1 行为 100% 兼容）

| 层 | HTTP | 说明 |
|---|---|---|
| 参数校验失败 | **400** | payload 格式错（pydantic 手动校验，msg 含字段路径） |
| 业务失败（默认） | **400** | 40301xxx（除下列特例） |
| 未登录 | **401** | `40301002` |
| 用户不存在 | **404** | `40301004` |
| 未知 action | **400** | `40301001`（msg=`unknown action: <x>`） |

### 错误码矩阵（40301xxx）

| code | 语义 | HTTP |
|---|---|---|
| `40301001` | 请求参数非法（payload 校验/字段缺失/未知 action） | 400 |
| `40301002` | 未登录 / token 无效 | 401 |
| `40301004` | 用户不存在 | 404 |
| `40301005` | phone/email/username 已被其他账号绑定 | 400 |
| `40301006` | 积分不足（兑换扣减防双花） | 400 |
| `40301007` | 兑换套餐无效（days 不在白名单） | 400 |
| `40301008` | 邀请码无效 / 不能邀请自己 / 已绑定邀请人 | 400 |
| `40301009` | 邀请奖励时长已达上限（90 天） | 400 |
| `40301010` | 基础资料未完成（缺 height/weight/age） | 400 |
| `40301011` | 方案不存在或已不适用 | 400 |
| `40301012` | 注销前置检查被拦截（积分/会员） | 400 |
| `40301013` | 年龄超出合理范围 \[5, 120] | 400 |

***

## 1. completeOnboarding

完成新用户激活向导：`onboardingDone = true` + 失效 me 缓存。

**请求**：`{ "action": "completeOnboarding", "payload": {} }`（无需 payload）

**响应 data**：`{ "ok": true }`

**错误码**：`40301004`（404）· `40301002`（401）

## 2. resetOnboarding

重新激活：`onboardingDone = false`（前端重走向导填资料/授权微信运动）。

**请求**：`{ "action": "resetOnboarding", "payload": {} }`

**响应 data**：`{ "ok": true }`

**错误码**：`40301004`（404）

## 3. bindApps

多方式认证绑定（微信登录用户补手机号/邮箱/密码/用户名，之后可用绑定方式登录同一账号）。

**请求 payload**：

| 字段 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `phone` | string | ⬜ | `^1[3-9]\d{9}$` | 手机号（全局唯一） |
| `email` | string | ⬜ | 邮箱格式 | 邮箱（全局唯一） |
| `password` | string | ⬜ | 6~64 字符 | 密码（scrypt 哈希落库） |
| `username` | string | ⬜ | 3~32 字符 | 用户名（全局唯一） |

**至少提供一个字段**，否则 `40301001`。

**响应 data**：`{ "user": { ...UserDTO（25 字段，camelCase）} }`

**错误码**：`40301005` 三字段任一被其他账号占用 · `40301001` 全空 / 格式错 · `40301004`（404）

## 4. bindInviter

邀请裂变：绑定邀请人（双方奖励）—— 邀请人 +7 天会员（封顶 90 天）+50 积分；被邀人 +3 天体验 +20 积分。

**请求 payload**：

| 字段 | 类型 | 必填 | 约束 |
|---|---|---|---|
| `inviterCode` | string | ✅ | 4~16 字符 |

**响应 data**：`{ "ok": true, "inviterId": "<id>" }`

**错误码**：`40301008` 邀请码无效 / 邀请自己 / 已绑定邀请人 · `40301009` 邀请人奖励封顶（90 天）· `40301004`（404）

## 5. checkReportQuota

报告免费周配额：会员不限，免费每周 1 次全文（ISO 自然周）。

**请求**：`{ "action": "checkReportQuota", "payload": {} }`

**响应 data**：

| 字段 | 类型 | 说明 |
|---|---|---|
| `canView` | bool | 本周是否还能看全文 |
| `weeklyUsed` | int | 本周已用次数（会员恒 0） |
| `quota` | int | `-1` = 不限（会员）；`1` = 每周 1 次（免费） |

**错误码**：`40301004`（404）

## 6. redeemMember

积分兑换会员时长（飞轮消费出口：扣积分 + 续期）。

**请求 payload**：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `days` | literal `7` | `30` | ✅ | 套餐（7 天=100 积分 / 30 天=300 积分） |

**响应 data**：`{ "ok": true, "days": <n>, "pointsCost": <n> }`

**错误码**：`40301007` 套餐无效 · `40301006` 积分不足 · `40301004`（404）

## 7. submitOnboarding

引导步骤 1-2：存资料 + 判定人群分型（audience）+ 返画像（**不标 done**）。

**请求 payload**：

| 字段 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `nickname` | string | ⬜ | ≤32 | 昵称（truthy 才写） |
| `gender` | string | ✅ | `male` | `female` | `unknown` | 性别 |
| `age` | int | ✅ | 5~120 | 年龄（服务端二次校验 → `40301013`） |
| `zodiac` | string | ⬜ | 12 星座名 | 星座（truthy 才写 — 脱敏画像） |
| `height` | float | ✅ | 50~250 | 身高 cm |
| `weight` | float | ✅ | 10~300 | 体重 kg |
| `bodyFat` | float | ⬜ | 0~80 | 体脂 % |
| `habits` | object | ✅ | 见下 | 习惯问卷 |

`habits` 子字段：

| 字段 | 类型 | 必填 | 约束 |
|---|---|---|---|
| `exerciseFreq` | int | ✅ | 0~7 |
| `exercisePrefs` | string\[] | ✅ | `run` | `cycling` | `walk` | `strength` | `other`（多选 — 画像 P0 由 V1 单值 `exercisePref` 改） |
| `exercisePrefRemark` | string | ⬜ | ≤200（含 `other` 时必填） |
| `takeoutFreq` | int | ⬜ | 0~21 |
| `sugarDrink` | int | ⬜ | 0~20 |
| `lateDinner` | bool | ⬜ | — |

**响应 data**（ProfileSummary）：

| 字段 | 类型 | 说明 |
|---|---|---|
| `audience` | string | 人群分型（A/B/C 类） |
| `bmi` | float | BMI |
| `bmiLabel` | string | BMI 文案标签 |
| `bmr` | int | 基础代谢 |
| `tags` | string\[] | 画像标签 |
| `summary` | string | 画像摘要 |
| `minorMode` | bool | 未成年模式 |

**错误码**：`40301013` 年龄越界 · `40301001` 格式错

**⚠️ 先跑通阶段语义（2026-09-17）**：同 `updateBasicProfile` —— 读不到用户时**按需建档**（oauth 用户库与 user 模块 InMemory 分离 / 多 worker 约束），不返回 404；落库阶段统一用户存储后改回严格 404。

## 8. onboardingSuggestion

引导步骤 4：健康目标区间 + 2~3 套计划方案（供用户选择，**不自动采纳**）。

**请求**：`{ "action": "onboardingSuggestion", "payload": {} }`

**响应 data**（OnboardingSuggestion）：

| 字段 | 类型 | 说明 |
|---|---|---|
| `audience` | string | 人群分型（存储优先 — 与 V1 口径一致） |
| `minorMode` | bool | 未成年模式（age < 18） |
| `goal` | object | null | `{targetWeightMin, targetWeightMax, weeks, maxWeeklyLossPct, note}` |
| `plans` | array | `[{id, name, sessionsPerWeek, deficitKcal, composition, reason, recommended}]` |
| `safetyNote` | string | null | 安全提示（按 BMI / 未成年） |

**错误码**：`40301010` 基础资料未完成（缺 height/weight/age）· `40301004`（404）

## 9. adoptPlanOption

采纳计划方案（服务端**二次校验防伪造 deficit** — 重生成建议取该方案值）。

**请求 payload**：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `planId` | literal `"gentle"` | `"standard"` | `"intense"` | ✅ | 方案 id |

**响应 data**：`{ "planOption": "<id>", "dailyDeficitKcal": <int>, "sessionsPerWeek": <int> }`

**错误码**：`40301011` 方案不存在或已不适用 · `40301010` 资料未完成 · `40301004`（404）

## 10. precheckDeleteAccount

注销前置检查（积分/会员余额拦截 + 数据概览）。

**请求**：`{ "action": "precheckDeleteAccount", "payload": {} }`

**响应 data**：

| 字段 | 类型 | 说明 |
|---|---|---|
| `canDelete` | bool | 是否可注销（blockers 为空） |
| `blockers` | string\[] | 拦截原因（如「还有 N 积分余额,请先消耗或兑换」「会员尚未到期…」） |
| `summary` | object | 数据概览 `{bodyCompositions, teamMemberships}` |

**错误码**：`40301004`（404）

## 11. deleteAccount

账号注销（个保法 + 苹果 5.1.1(v) 强制，**不可逆**）。内部重跑 precheck 兜底 + 级联清理 + 清缓存。

**请求**：`{ "action": "deleteAccount", "payload": {} }`

**响应 data**：`{ "deleted": true }`

**错误码**：`40301012` 前置检查被拦截（积分/会员 — 与 precheck 同源）· `40301004`（404）

**客户端约定**：必须先走 `precheckDeleteAccount` + 用户二次确认，再调本 action。

## 12. updateBasicProfile

新用户「完善基础资料」弹窗提交（**V2 扩展** — 向导 step1 的轻量提交口；V1 无此 action，V1 该场景由 `submitOnboarding` 承担）。仅 upsert 4 维基础字段，**不触碰** `onboardingDone` / habits / 画像。

**调用时机**：登录响应 `user.isNewUser === true`（注册当次）后弹窗收集 → 提交本 action。

**请求**：

| 字段 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `gender` | `"male"` | `"female"` | ✅ | — | 性别（UI 显示男/女，提交映射） |
| `age` | int | ✅ | `5~120` | 年龄（服务端二次校验同口径） |
| `height` | float | ✅ | `50~250` | 身高 cm（落库取整 — 与 submitOnboarding 同口径） |
| `weight` | float | ✅ | `10~300` | 体重 kg |

**响应 data**：`{ "gender": "<male|female>", "age": <int>, "height": <float>, "weight": <float> }`（camelCase 契约）

**错误码**：`40301013` 年龄超出合理范围（AGE\_INVALID）· 参数校验失败 → `40301xxx`（`<字段路径>: <原因>`）

**⚠️ 先跑通阶段语义（2026-09-17）**：user 模块当前为 InMemory（与 oauth 用户库分离）→ 新注册用户在本存储不存在时**按需建档**（openid = `o-{userId}`），不返回 404；落库阶段统一用户存储后改回严格 404。

**缓存**：写后失效 `user:me:{userId}`（与全部写 action 同约定）。

## 13. myProfile

读全量画像 + 账号概况（personal-form / 个人设置页一站式回显）。合并两源：`user_profile` 画像行（0011-0013）+ 账号段（积分/会员/成长等级 — 0016 用户主表）。无画像行 → **惰性建档**（不 404）。

**请求**：`{ "action": "myProfile", "payload": {} }`（无需 payload — 身份以 token 为准）

**响应 data**：⚠️ **动态 dict（画像行 + account 段合并，按字段存在性消费 — 勿做全等断言）**：

| 分组 | 字段（camelCase） | 说明 |
|---|---|---|
| 画像 · 身份 | `userId` / `nickname` / `avatarUrl` / `signature` / `signatureSource` | `signatureSource` = `custom`（用户手写）| `agent`（AI 分析） |
| 画像 · 偏好 | `exercisePrefs` / `exercisePrefRemark` / `profileTags` | jsonb → list（无 → `[]`） |
| 画像 · 资料 | `gender` / `age` / `height` / `weight` / `zodiac` / `audience` | 向导/agent 未填时可能为 null |
| 画像 · 计划 | `onboardingDone` / `habits` / `planOption` / `dailyDeficitKcal` / `sessionsPerWeek` | 同上 |
| 画像 · fitness | `fitness` / `agentAnalyzedAt` | `fitness` = `{level, sport, paceHintMinKm, weeklyTargetKm}` |
| account 段 | `points` / `totalPointsEarned` / `memberExpireAt` / `growthLevel` / `onboardingDone` | user\_core 未装配/异常时 5 键整体缺席 |
| 时间戳 | `updatedAt` | ⚠️ **仅 PG 实现**（行含 updated\_at）；InMemory 路径无此键 |

**错误码**：`40301001` 未装配（画像仓储未接 PG）· `40301002`（401）

## 14. updateProfile

画像编辑（昵称 / 头像 / 签名 / 运动偏好）—— **全字段可选，只提交要改的维度**。

**请求 payload**：

| 字段 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `nickname` | string | ⬜ | ≤32 | ⭐ 双写 oauth users 主表（登录读源 — 2026-09-20 契约修复） |
| `avatarUrl` | string | ⬜ | — | 头像 URL |
| `signature` | string | ⬜ | ≤60 | 写非空值时 `signatureSource='custom'`（agent 分析不覆盖） |
| `exercisePrefs` | string\[] | ⬜ | 1~4 项 · `run` | `cycling` | `walk` | `strength` | `other` | 运动偏好多选（空数组 → `40301001`） |
| `exercisePrefRemark` | string | ⬜ | ≤200 | `exercisePrefs` 含 `other` 时必填 |

**⚠️ 语义（仅非 None 字段写）**：字段传 `null` / 不传 = **不修改** —— 无法用它清空字段（「清空签名」需另立约定）。

**响应 data**：`{ "user": <画像行 dict（camelCase）> }` —— 回写后整行（字段集同 §13 画像段；PG 路径含 `updatedAt`）。

**错误码**：`40301001` payload 校验 / `other` 缺 remark / 未装配 · `40301002`（401）

## 15. fitnessProfile

GIS 高德配速参数读（0013 — agent 每日回写，App 运动页初始化消费）。

**请求**：`{ "action": "fitnessProfile", "payload": {} }`

**响应 data**：

| 字段 | 类型 | 说明 |
|---|---|---|
| `fitness` | object | null | `{level, sport, paceHintMinKm, weeklyTargetKm}`；**agent 未分析 → null**（App 端降级默认参数，不报错） |
| `profileTags` | string\[] | 画像标签（无 → `[]`） |
| `agentAnalyzedAt` | string | null | agent 最近分析时间（ISO） |

**错误码**：`40301001` 未装配（画像仓储未接 PG）· `40301002`（401）

***

## 实现参考

* 契约权威：`src/app/user/interface/api/user_actions.py`（pydantic payload schema + 15 分支）
* 模块结构：`src/app/user/CLAUDE.md`
