Appearance
user —— C 端用户模块(15 action)
📘 前端联调速查(30 端点手册):frontend/user.md 统一入口:
POST /api/v1/userbody ={ "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 |
resetOnboarding | Bearer | 重新走引导(onboardingDone=false) | → §2 |
bindApps | Bearer | 绑定手机/邮箱/密码/用户名 | → §3 |
bindInviter | Bearer | 绑定邀请人(双方奖励) | → §4 |
checkReportQuota | Bearer | 报告免费周配额查询 | → §5 |
redeemMember | Bearer | 积分兑换会员时长 | → §6 |
submitOnboarding | Bearer | 引导步骤 1-2:存资料 + 画像 | → §7 |
onboardingSuggestion | Bearer | 引导步骤 4:目标 + 计划方案 | → §8 |
adoptPlanOption | Bearer | 采纳计划方案 | → §9 |
precheckDeleteAccount | Bearer | 注销前置检查 | → §10 |
deleteAccount | Bearer | 账号注销(不可逆) | → §11 |
updateBasicProfile | Bearer | 新用户弹窗:基础资料 4 维(V2 扩展) | → §12 |
myProfile | Bearer | 读全量画像 + 账号概况(我的页回显) | → §13 |
updateProfile | Bearer | 画像编辑(昵称/头像/签名/运动偏好) | → §14 |
fitnessProfile | Bearer | GIS 配速参数读(App 运动页初始化) | → §15 |
公共约定
统一信封(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