|
@@ -0,0 +1,277 @@
|
|
|
|
|
+# CFC 小程序后端「接口契约」评审报告
|
|
|
|
|
+
|
|
|
|
|
+> 评审角色:集成·接口级评审专家(柯砚)
|
|
|
|
|
+> 代码仓库:`D:/cfc` · 后端:`D:/cfc/cfc-backend`(Spring Boot 2.7,端口 **9082**,统一 `@PostMapping`,JWT Bearer 鉴权)
|
|
|
|
|
+> 评审方式:全程只读,仅新增本文件,未修改/删除/提交任何源码
|
|
|
|
|
+> 目标:为把空体冒烟测试升级为业务级测试(带真实参数、多角色 token)提供契约基线,逐步达到 100% 通过
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 0. 前置结论(先读这段)
|
|
|
|
|
+
|
|
|
|
|
+1. **统一响应结构**:`Result<T>{code,message,data}`,位于 `common/Result.java`。`code=200` 成功;其余为非 0 失败码。
|
|
|
|
|
+2. **鉴权**:`JwtInterceptor` 拦截 `/api/**`,白名单见 §2。**token 通过 `Authorization: Bearer <token>` 头传递**(也可走 `?token=` 参数)。token 字段名是 `token`(在 `LoginResultDTO` 里,不是 `accessToken`)。
|
|
|
|
|
+3. **既有冒烟 0% 的根因**:`INTERFACE_TEST_REPORT.md` 显示全部 `NON_JSON`,目标域填的是 `cfc.iwintrue.com`(外网不可达,返回 HTML 而非 JSON)。**测试应改为指向 `http://localhost:9082`**。同时基线里大量路径写错(如 `/api/user/switch-*`、实际是 `/api/family/user/switch-role`),导致 404/错路由。
|
|
|
|
|
+4. **没有 `mock-login` 接口**。`auto-login` 已废弃(返回 410)。多角色 token 的获取姿势见 §3、§4。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 1. 统一响应结构 `Result<T>` 与 code 取值约定
|
|
|
|
|
+
|
|
|
|
|
+**定义位置**:`src/main/java/com/etotem/cfc/common/Result.java:8`
|
|
|
|
|
+
|
|
|
|
|
+字段:
|
|
|
|
|
+
|
|
|
|
|
+| 字段 | 类型 | 说明 |
|
|
|
|
|
+|------|------|------|
|
|
|
|
|
+| `code` | `Integer` | 业务码,200=成功 |
|
|
|
|
|
+| `message` | `String` | 提示文案 |
|
|
|
|
|
+| `data` | `T` | 业务数据,可 null |
|
|
|
|
|
+
|
|
|
|
|
+**code 取值约定(证据均在 `Result.java`)**:
|
|
|
|
|
+
|
|
|
|
|
+| 取值 | 含义 | 出处 / 触发位置 |
|
|
|
|
|
+|------|------|----------------|
|
|
|
|
|
+| `200` | 成功 | `Result.java:16,23,31`(`success()` 系列统一写 200) |
|
|
|
|
|
+| `500` | 通用失败(参数错误、业务异常、未登录等混用) | `Result.java:39`(`error(String)` 写死 500) |
|
|
|
|
|
+| `5001` | 无家庭(请先创建或加入家庭) | `Result.java:45-50`(`noFamily()`) |
|
|
|
|
|
+| `401` | 未登录 / token 失效 | 见 §2 `JwtInterceptor.writeUnauthorized`;业务层 `AuthController.verifyToken` `AuthController.java:226,238,246`、`sso-ticket` `:353` 用 `Result.error(401,...)` |
|
|
|
|
|
+| `403` | 无权限(越权访问家庭数据 / 组织权限) | `FamilyAccessInterceptor.java:139`(`{"code":403,...}`);`common/AdminOrgAccess.java:40,43,46,52,61` |
|
|
|
|
|
+| `410` | 接口已废弃(Gone) | `AuthController.java:139`(auto-login)、`FamilyController.java:42,52`、`FamilyUserController.java:97,124,131`(switch-mode/switch-to-child/switch-back-to-parent) |
|
|
|
|
|
+| `4001` | 业务自定义失败(如“请先购买健康启航计划体验包”) | `HealthReportController.java:216` |
|
|
|
|
|
+| 其他任意非 0 | 各业务自定义错误码 | 全代码库散落大量 `Result.error(NNNN, msg)`,测试需**按接口逐个核对实际码**,不能假设只有上面几种 |
|
|
|
|
|
+
|
|
|
|
|
+**对编写用例的建议**:
|
|
|
|
|
+- 断言不要只判 `200`。把「业务成功」定义为 `code==200 && data!=null`(或 data 符合契约);「失败」定义为 `code!=200` 且断言 `message` 非空。
|
|
|
|
|
+- 必须记录每个接口返回的**实际错误码**(尤其是自定义码如 4001),避免把“预期失败”误判为“通过”或“崩溃”。
|
|
|
|
|
+- 注意:**未带 token 访问非白名单接口,不一定返回 401**。`JwtInterceptor` 对“无 token 且非强制登录路径”会注入匿名用户(`userId=-1, role=anonymous`)并放行,下游 Controller 通常返回 `code=500` 的 `Result.error("请先登录")`/`"未登录"`,HTTP 状态仍是 200。**只有 token 格式非法/过期、或命中 `LOGIN_REQUIRED_PATHS` 才会真正返回 HTTP 401**。用例要区分这两种“未登录”表现。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 2. 鉴权拦截器 `JwtInterceptor` 与公开路径白名单
|
|
|
|
|
+
|
|
|
|
|
+**实现位置**:`src/main/java/com/etotem/cfc/config/JwtInterceptor.java:21`
|
|
|
|
|
+**注册位置**:`src/main/java/com/etotem/cfc/config/WebConfig.java:95-137`(拦截 `/api/**`,并在 `excludePathPatterns` 中再排除一批)
|
|
|
|
|
+
|
|
|
|
|
+### 2.1 白名单 `PUBLIC_PATHS`(`JwtInterceptor.java:32-77`,精确匹配 + `prefix/` 前缀匹配)
|
|
|
|
|
+
|
|
|
|
|
+以下路径**完全跳过 token 校验**(匿名即可访问):
|
|
|
|
|
+
|
|
|
|
|
+- 认证类:`/api/auth/send-code`、`/api/auth/phone-login`、`/api/auth/wechat-phone-login`、`/api/auth/silent-login`、`/api/auth/sso-login`、`/api/auth/auto-login`(已废弃)、`/api/auth/register-with-idcard`、`/api/auth/wechat-login`、`/api/auth/verify`、`/api/auth/check-phone`、`/api/auth/direct-register`、`/api/auth/register-with-invite`、`/api/auth/wechat-web/qr`、`/api/auth/wechat-web/poll`、`/api/auth/internal/`
|
|
|
|
|
+- 商品/商城:`/api/product/list`、`/api/product/detail`、`/api/special-price/info`、`/api/product/type-list`、`/api/product/related-products`、`/api/mini-game/list`
|
|
|
|
|
+- 文章:`/api/articles/list`、`/api/articles/detail`、`/api/articles/categories`、`/api/articles/featured`、`/api/articles/record-read`、`/api/articles/related-products`、`/api/articles/related`、`/api/articles/related-by-product`、`/api/content/related-items`
|
|
|
|
|
+- 成长/活动:`/api/growth/**`、`/api/activity/list`、`/api/activity/detail`、`/api/activity/order/notify`、`/api/content-sections/visible`
|
|
|
|
|
+- 心智:`/api/mind/alert/active`、`/api/mind/alert/checkin-alerts`、`/api/mind/traditional/mirror`、`/api/mind/traditional/compatibility`
|
|
|
|
|
+- 其他:`/api/admin-auth/send-code`、`/api/admin-auth/login`、`/api/admin-auth/login-by-password`、`/api/admin-auth/account-roles`、`/api/mcp/**`
|
|
|
|
|
+
|
|
|
|
|
+> 匹配规则见 `JwtInterceptor.java:93-99`:`path.equals(publicPath)` 或 `path.startsWith(boundaryPath + "/")`(`/**` 仅取前缀)。
|
|
|
|
|
+
|
|
|
|
|
+### 2.2 强制登录路径 `LOGIN_REQUIRED_PATHS`(`JwtInterceptor.java:80-83`)
|
|
|
|
|
+
|
|
|
|
|
+无 token 直接 **HTTP 401**:`/api/share/qrcode`、`/api/auth/verify-password`。
|
|
|
|
|
+
|
|
|
|
|
+### 2.3 token 解析与请求属性注入(`JwtInterceptor.java:102-191`)
|
|
|
|
|
+
|
|
|
|
|
+- 取 token:`Authorization` 头,去 `Bearer ` 前缀(`:130`);空则取 `?token=` 参数(`:106`)。
|
|
|
|
|
+- 解析成功后注入请求属性(下游 `@RequestAttribute` 取值):
|
|
|
|
|
+ - `userId`(`Long`,`:148`)
|
|
|
|
|
+ - `role`(`String`,`:149`)
|
|
|
|
|
+ - `roles`(`List<String>`,`:150`)
|
|
|
|
|
+ - `familyId`(`Long`,`:167`,user.family_id 为空时从 `family_members` 反查)
|
|
|
|
|
+ - `currentMemberId` / `familyMemberId`(`Long`,`:184-185`,当 familyId 非空时查家庭成员)
|
|
|
|
|
+- 无 token 且非强制登录:注入**匿名用户** `userId=-1, role="anonymous"`,放行(`:211-217`)。
|
|
|
|
|
+
|
|
|
|
|
+### 2.4 第二个拦截器:家庭越权 `FamilyAccessInterceptor`(`config/FamilyAccessInterceptor.java:33`)
|
|
|
|
|
+
|
|
|
|
|
+- 作用:校验请求里出现的 `familyId`(query/path/body 递归)是否属于当前用户自己或 `teacherFamilyIds`。
|
|
|
|
|
+- `admin/**` 跳过(`:51`)。
|
|
|
|
|
+- 默认 **logging 模式**(只打 warn,不拦截);**enforcement 模式**(`-Dfamily.access.mode=enforcement`)越权返回 **HTTP 403 + code 403**(`:135-140`)。
|
|
|
|
|
+- ⚠️ 测试环境若开了 enforcement,跨家庭访问会被 403;业务级用例需确保每个角色的 token 只访问自己家庭的数据。
|
|
|
|
|
+
|
|
|
|
|
+### 2.5 其它拦截器(顺序见 `WebConfig.java:94-179`)
|
|
|
|
|
+
|
|
|
|
|
+`operationLogInterceptor`、`rateLimitInterceptor` 也拦截 `/api/**`,并对白名单批量 exclude。限流可能让高频测试偶发失败,必要时在测试侧限速或识别限流返回。
|
|
|
|
|
+
|
|
|
|
|
+**对编写用例的建议**:
|
|
|
|
|
+- 把白名单接口标记为「免登录可测」,优先拿它们做“空体/最小参数”探活。
|
|
|
|
|
+- 非白名单接口必须先拿 token;**拿 token 的最小成本路径是 `/api/auth/direct-register`**(见 §3),无需验证码。
|
|
|
|
|
+- 注意公开接口(白名单)内部大多用 `@RequestAttribute(value="userId", required=false)`(如 `ActivityController.java:29`),匿名时 `userId` 为 null,不应断言“未登录 401”,而应断言业务正常返回(活动列表/详情本就是公开读)。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 3. 登录相关接口(请求参数 + 返回 token 字段)
|
|
|
|
|
+
|
|
|
|
|
+**控制器**:`src/main/java/com/etotem/cfc/controller/auth/AuthController.java`
|
|
|
|
|
+
|
|
|
|
|
+> 所有登录/注册接口均返回 `Result<LoginResultDTO>`。token 字段名 = **`token`**(`LoginResultDTO`,见 `src/main/java/com/etotem/cfc/dto/LoginResultDTO.java`),响应体取值 `data.token`。前端/测试用 `Authorization: Bearer <data.token>` 携带。`LoginResultDTO` 还含 `userId, role, roles, familyId, nickname, phone, currentChildId, isNewUser, needsRoleSelect, needsFamily, teacherStatus, hasTeacherRole, vendorType, activityProviderStatus` 等,可用来驱动后续断言。
|
|
|
|
|
+
|
|
|
|
|
+| 接口 | 行号 | 请求体(必填项) | 说明 / 返回 |
|
|
|
|
|
+|------|------|------------------|------------|
|
|
|
|
|
+| `POST /api/auth/wechat-login` | `AuthController.java:175` | `{code}`(`WechatLoginDTO.code`) | 微信 code 登录;测试环境若无真实微信 code 不易用 |
|
|
|
|
|
+| `POST /api/auth/phone-login` | `:67` | `{phone, code}` | 需先 `send-code` 拿验证码并落库,流程重 |
|
|
|
|
|
+| `POST /api/auth/direct-register` | `:278` | `{phone, nickname?, avatar?, role?, birthday?, gender?, inviteCode?}`(`DirectRegisterDTO`) | **测试首选**:跳过验证码直接注册/登录;`phone` 必填且需 `^1[3-9]\d{9}$`;已注册则返回“该手机号已注册” |
|
|
|
|
|
+| `POST /api/auth/register-with-idcard` | `:146` | `{phone, code, idCard, realName}` | 需验证码,身份证格式校验 |
|
|
|
|
|
+| `POST /api/auth/sso-login` | `:376` | `{ticket}`(公开) | 跨小程序 SSO:先 `/api/auth/sso-ticket` 签发,再消费换登录态 |
|
|
|
|
|
+| `POST /api/auth/sso-ticket` | `:349` | `{target:"train"}`(需登录) | 返回 `{ticket,target}` |
|
|
|
|
|
+| `POST /api/auth/auto-login` | `:136` | — | **已废弃 → `Result.error(410,...)`**,不要再用 |
|
|
|
|
|
+| `POST /api/auth/verify` | `:222` | 仅 `Authorization` 头 | 校验 token,返回 `{userId,role}`;token 非法/用户不存在 → `code=401` |
|
|
|
|
|
+| `POST /api/auth/wechat-phone-login` | `:90` | `{code, phoneCode}` | 需微信能力 |
|
|
|
|
|
+| `POST /api/auth/silent-login` | `:107` | `{code}` | 已废弃的 openid 自动登录替代方案(仍需微信 code) |
|
|
|
|
|
+
|
|
|
|
|
+**关于 mock-login**:**全代码库无 `mock-login` 接口**(已 grep 确认)。多角色 token 的推荐获取方式:
|
|
|
|
|
+1. `direct-register` 时直接带 `role`(值 `parent`/`teacher`)拿对应角色 token;
|
|
|
|
|
+2. 登录后用 `switch-role` 切换(见 §4)。
|
|
|
|
|
+
|
|
|
|
|
+**对编写用例的建议**:
|
|
|
|
|
+- 业务级用例的“登录前置”统一封装为:`POST /api/auth/direct-register` → 提取 `data.token` → 后续请求带 `Authorization: Bearer <token>`。注意 `direct-register` 会校验手机号格式,使用测试专用号段即可,但**同一手机号二次调用会报“已注册”**,用例需做号段/清理或复用已注册 token。
|
|
|
|
|
+- `needsFamily=true`(familyId 为空/0)时,要先建/加入家庭再调家庭/健康/任务类接口,否则易触发 `code=5001` 或 downstream 的 `familyId不能为空`。
|
|
|
|
|
+- 校验未登录判定:带**非法/过期 token** 才稳定得到 HTTP 401;不带 token 访问非白名单接口是匿名放行 + 业务 `code=500`,不要混为一谈。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 4. 角色模型与切换角色接口现状
|
|
|
|
|
+
|
|
|
|
|
+### 4.1 角色如何区分
|
|
|
|
|
+
|
|
|
|
|
+**实体**:`src/main/java/com/etotem/cfc/entity/User.java`
|
|
|
|
|
+- `role`(`User.java:30`):当前活跃角色,取值 `parent` / `child` / `teacher` / `admin`。
|
|
|
|
|
+- `roles`(`User.java:34`):拥有的全部角色列表,JSON。
|
|
|
|
|
+- `familyRole`(`User.java:99`):家长身份(爸爸/妈妈/爷爷…),区别于 `role`。
|
|
|
|
|
+- `vendorType`(`User.java`):服务商类型 `planner` / `activity_provider` / `product_supplier`(配合 `vendorStatus`)。
|
|
|
|
|
+
|
|
|
|
|
+**角色语义(基于 `UserService` 与控制器)**:
|
|
|
|
|
+- **parent(家长)**:常规登录用户,可建/加入家庭,给孩子派任务、看健康报告。
|
|
|
|
|
+- **child(孩子)**:**通常不作为独立登录账号**——孩子以 `family_members` 表的成员记录存在,由家长以 `currentMemberId` 切换“成员视图”来代操作。`User.role="child"` 在代码里也存在(`UserService.java` 有 `setRole("child")` 分支),但小程序端孩子一般通过家长切换成员视图访问,而非自己登录拿 token。
|
|
|
|
|
+- **teacher(成长规划师)**:可登录,受 `teacherStatus`(pending/approved/rejected)审核;`teacherFamilyIds` 决定可访问哪些家庭。部分接口要求 `teacher` 角色(如 `FamilyController.guideBindFamily` `:68` 校验 `"teacher".equals(guide.getRole())`)。
|
|
|
|
|
+- **admin**:**禁止登录小程序**(`UserService.ssoLogin` `:701` 抛“该账号不允许登录小程序”;`switchRoleForLogin` `:635` 抛“管理员角色不允许切换”)。admin 走独立的 `/api/admin-auth/*` 体系。
|
|
|
|
|
+
|
|
|
|
|
+### 4.2 切换角色接口现状(重点:此前冒烟 `/api/user/switch-*` 返回 404 的原因)
|
|
|
|
|
+
|
|
|
|
|
+经核对,**不存在 `/api/user/switch-*` 路由**,真实路由如下(`grep` 证据):
|
|
|
|
|
+
|
|
|
|
|
+| 真实路由 | 行号 | 说明 |
|
|
|
|
|
+|----------|------|------|
|
|
|
|
|
+| `POST /api/auth/switch-role` | `AuthController.java:330` | 切换**活跃登录角色** `parent/teacher`,重新签发 token(返回 `LoginResultDTO`)。需登录。底层 `UserService.switchRoleForLogin` `:637`:非 teacher 不能切到 teacher |
|
|
|
|
|
+| `POST /api/family/user/switch-role` | `FamilyUserController.java:101-105` | 切 `parent/child/teacher`,body `{role}`,返回 `Result<String>` |
|
|
|
|
|
+| `POST /api/family/member/switch` | `FamilyMembersController.java:84` | **切换成员视图**(家长切到某个孩子 `memberId`),body `{memberId}`,返回 `SwitchMemberVO`。这是替代一堆废弃接口的新路由 |
|
|
|
|
|
+| `POST /api/family/user/switch-mode` | `FamilyUserController.java:94` | **已废弃 → `code=410`** |
|
|
|
|
|
+| `POST /api/family/user/switch-to-child` | `FamilyUserController.java:121` | **已废弃 → `code=410`** |
|
|
|
|
|
+| `POST /api/family/user/switch-back-to-parent` | `FamilyUserController.java:129` | **已废弃 → `code=410`** |
|
|
|
|
|
+
|
|
|
|
|
+**结论**:此前冒烟 `/api/user/switch-*` 的 404,是因为路径写错(少了 `/family/` 前缀,或混淆了废弃路由)。正确做法是:切换登录角色用 `/api/auth/switch-role`,切换成员视图用 `/api/family/member/switch`。不要再用任何 `switch-mode/switch-to-child/switch-back-to-parent`(都返回 410)。
|
|
|
|
|
+
|
|
|
|
|
+**对编写用例的建议**:
|
|
|
|
|
+- 多角色 token 准备:用 `direct-register` 带 `role=parent` 注册家长;另注册一个 `role=teacher` 且 `teacherStatus=approved` 的账号作规划师(注意 `switchRoleForLogin` 要求该用户 `roles` 里真含 teacher,否则切不过去)。
|
|
|
|
|
+- 验证 teacher 越权场景:用家长 token 调 `POST /api/family/guide-bind`(要求 teacher 角色)应得“只有成长规划师才能使用此功能”。
|
|
|
|
|
+- 孩子视角用例:家长登录 → `POST /api/family/member/switch {memberId}` 拿到成员视图 → 用返回的 `currentMemberId` 调健康/任务接口。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 5. 代表性业务接口契约(优先此前冒烟大量 500 的 health / family / activity)
|
|
|
|
|
+
|
|
|
|
|
+> 通用约定:全部 `@PostMapping`;请求体多为 `Map<String,Object>` 或实体;缺参通常 `Result.error("xxx不能为空")` → `code=500`。以下“常见错误码”含业务自定义码。
|
|
|
|
|
+
|
|
|
|
|
+### 5.1 活动 Activity —— `controller/ActivityController.java`(`@RequestMapping("/api/activity")`)
|
|
|
|
|
+此前冒烟 500 多因未登录 / 缺 `id` / 缺 `memberId`。注意 `/api/activity/*` 中仅 `list`、`detail`、`order/notify` 在白名单,其余需登录。
|
|
|
|
|
+
|
|
|
|
|
+| 接口 | 行号 | 必填参数 | 正常返回 / 常见错误 |
|
|
|
|
|
+|------|------|----------|--------------------|
|
|
|
|
|
+| `POST /api/activity/list` | `:28` | 无(公开)`dimensionCode? page? size?` | `Map`(列表+分页);匿名可访问 |
|
|
|
|
|
+| `POST /api/activity/detail` | `:36` | `id`(必填) | `ActivityDTO`;缺 `id`→`"id不能为空"` |
|
|
|
|
|
+| `POST /api/activity/register` | `:123` | `id`(活动ID);单报需 `memberId`(缺→`"孩子ID不能为空"`) | `Map`;批量用 `registrants[]` |
|
|
|
|
|
+| `POST /api/activity/create` | `:44` | 活动实体;普通用户需登录(未登录且非管理员→`"普通用户需登录后创建活动"`) | `ActivityDTO` |
|
|
|
|
|
+| `POST /api/activity/publish` | `:71` | `id`;需 `activity_manager/admin` 角色 | 字符串;无权限→`"只有管理员或活动管理员可以发布活动"` |
|
|
|
|
|
+| `POST /api/activity/checkin` | `:167` | `id`, `memberId` | `Map`;含 `error` 键时 `Result.error(...)` |
|
|
|
|
|
+| `POST /api/activity/submit-review` | `:106` | `id`;须创建人或管理员 | `"已提交审核"` |
|
|
|
|
|
+
|
|
|
|
|
+活动管理角色判定:`hasActivityManageRole` `:23-26`:`role=="admin"||"activity_admin"` 或 `roles` 含 `activity_manager`。
|
|
|
|
|
+
|
|
|
|
|
+### 5.2 家庭 Family —— `controller/family/FamilyController.java`(`/api/family`)+ 成员
|
|
|
|
|
+此前 500 多因 `familyId` 缺失/无家庭。
|
|
|
|
|
+
|
|
|
|
|
+| 接口 | 行号 | 必填参数 | 正常返回 / 常见错误 |
|
|
|
|
|
+|------|------|----------|--------------------|
|
|
|
|
|
+| `POST /api/family/guide-bind` | `:59` | `inviteCode`;需 `teacher` 角色 | `"申请已提交,请等待家长审批"`;非 teacher→`"只有成长规划师才能使用此功能"`;无效码→`"邀请码无效"` |
|
|
|
|
|
+| `POST /api/family/member/switch` | `:84` | `memberId` | `SwitchMemberVO`;缺 `memberId`→`"缺少成员ID"` |
|
|
|
|
|
+| `POST /api/family/user/switch-role` | `FamilyUserController.java:101` | `{role}` | `String`;无该角色→`Result.error("切换失败...")` |
|
|
|
|
|
+| `POST /api/family/invite-code` / `generate` | `:40 / :50` | — | **已废弃 → `code=410`**,应改用 `/api/family/invite/generate`(邀请令牌) |
|
|
|
|
|
+
|
|
|
|
|
+### 5.3 健康现状 HealthStatus —— `controller/HealthStatusController.java`(`/api/health-status`)
|
|
|
|
|
+需登录(`userId` 必填,非白名单)。
|
|
|
|
|
+
|
|
|
|
|
+| 接口 | 行号 | 必填参数 | 正常返回 / 常见错误 |
|
|
|
|
|
+|------|------|----------|--------------------|
|
|
|
|
|
+| `POST /api/health-status/get` | `:28` | 无(`userId` 由拦截器注入);`memberId?` | `HealthStatus`(账号级或成员级) |
|
|
|
|
|
+| `POST /api/health-status/save` | `:48` | 至少填一项(全空→`"请至少填写一项"`);`heightCm/weightKg/bloodPressure/bloodGlucose` 有范围校验 | `HealthStatus`;身高 30-250、体重 3-300、血压 `\d{2,3}/\d{2,3}`、血糖 1-30,越界→对应中文错误 |
|
|
|
|
|
+| `POST /api/health-status/signature` | `:79` | `signature`(base64 PNG data URL) | `Void`;空→`"请完成手签名"` |
|
|
|
|
|
+
|
|
|
|
|
+### 5.4 健康报告 HealthReport —— `controller/HealthReportController.java`(`/api/health-report`)
|
|
|
|
|
+**此前 500 的高发区**。根因:报告创建依赖“已购买健康启航计划体验包”且需 `memberId`/`reportType`。
|
|
|
|
|
+
|
|
|
|
|
+| 接口 | 行号 | 必填参数 | 正常返回 / 常见错误 |
|
|
|
|
|
+|------|------|----------|--------------------|
|
|
|
|
|
+| `POST /api/health-report/report/create` | `:206` | `userId`(注入)、`memberId`、`reportType`(`physical_exam`/`gut_flora`)、且**需先购计划** | `HealthReport`;未购→`code=4001 "请先购买健康启航计划体验包"`;缺 `memberId`→`"memberId不能为空"`;缺 `reportType`→`"reportType不能为空: ..."` |
|
|
|
|
|
+| `POST /api/health-report/report/latest` | `:295` | `memberId` | `HealthReport`;缺→`"memberId不能为空"` |
|
|
|
|
|
+| `POST /api/health-report/report/list` | `:316` | `userId` | `List<HealthReport>` |
|
|
|
|
|
+| `POST /api/health-report/indicators/current` | `:541` | `memberId` | `List`;缺→`"memberId不能为空"` |
|
|
|
|
|
+| `POST /api/health-report/indicators/rebuild` | `:516` | `memberId`;**需 `role==admin`** | 否则→`"权限不足"` |
|
|
|
|
|
+
|
|
|
|
|
+> 推论:此前 health 模块 500,很可能是测试未建家庭/未购计划/未传 `memberId`。业务级用例需先完成“建家庭→(必要时购计划)→拿 `memberId`”的前置链。
|
|
|
|
|
+
|
|
|
|
|
+### 5.5 任务 Task —— `controller/task/TaskController.java`(`/api/task`)
|
|
|
|
|
+需登录;`/today` 等需要 `familyId`。
|
|
|
|
|
+
|
|
|
|
|
+| 接口 | 行号 | 必填参数 | 正常返回 / 常见错误 |
|
|
|
|
|
+|------|------|----------|--------------------|
|
|
|
|
|
+| `POST /api/task/today` | `:81` | `familyId`(缺→`"familyId不能为空"`) | `List<Task>` |
|
|
|
|
|
+| `POST /api/task/create` | `:73` | `userId`(注入) | `Long`(任务ID) |
|
|
|
|
|
+| `POST /api/task/{id}/complete` | `:109` | 路径 `id`;`currentMemberId` | `Map`;无权/不存在→`"任务不存在、无权完成或状态已变更"` |
|
|
|
|
|
+| `POST /api/task/pending-review` | `:189` | `userId` | `List<Task>` |
|
|
|
|
|
+| `POST /api/task/batch-complete` | `:255` | `taskIds`(缺→`"taskIds不能为空"`) | `Boolean` |
|
|
|
|
|
+
|
|
|
|
|
+### 5.6 商城/商品 Product(白名单只读)
|
|
|
|
|
+- `POST /api/product/list`(`WebConfig` 白名单、`JwtInterceptor` 白名单):公开,无必填。
|
|
|
|
|
+- `POST /api/product/detail`:公开,需 `id`。
|
|
|
|
|
+- `POST /api/special-price/info`:公开。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 6. 业务级用例编写总建议(落地清单)
|
|
|
|
|
+
|
|
|
|
|
+1. **测试目标改 localhost**:`http://localhost:9082`(端口 `application.yml:2`),不要打外网域名,否则全 `NON_JSON`。
|
|
|
|
|
+2. **统一登录封装**:用 `POST /api/auth/direct-register`(带 `phone`+`role`)拿 `data.token`;每个角色(parent/teacher)+ 每个成员视图各准备一个 token 池,避免“已注册”冲突(可维护一批测试手机号)。
|
|
|
|
|
+3. **前置链路**:parent 注册 →(建/加家庭拿到 `familyId`)→(如测健康报告需购计划)→ 通过 `/api/family/member/switch` 拿 `memberId`。把这条链路做成用例 fixture。
|
|
|
|
|
+4. **断言策略**:成功 = `code==200`;失败 = `code!=200` 且记录真实 `message`/自定义码(如 4001/5001/410/403)。不要假设失败码只有 500。
|
|
|
|
|
+5. **未登录断言双形态**:非法 token → 真 HTTP 401(`/api/auth/verify` 等);不带头访问非白名单 → 匿名放行 + 业务 `code=500`,HTTP 仍 200。
|
|
|
|
|
+6. **角色/权限用例**:
|
|
|
|
|
+ - 家长调 `teacher`-only 接口(如 `/api/family/guide-bind`)→ 期望“只有成长规划师才能使用此功能”。
|
|
|
|
|
+ - 非 admin 调 `indicators/rebuild` → 期望“权限不足”。
|
|
|
|
|
+ - 跨家庭访问(若开 enforcement)→ 期望 403。
|
|
|
|
|
+7. **废弃接口不要测通过**:`auto-login`、`switch-mode`、`switch-to-child`、`switch-back-to-parent`、`/api/family/invite-code` 等均返回 410,用例应预期 410 而非 200/404。
|
|
|
|
|
+8. **修正错误路径**:把旧基线里的 `/api/user/switch-*` 改成 `/api/family/user/switch-role` 或 `/api/auth/switch-role`;`/api/activities/hot` 等需核对真实路由(活动列表是 `/api/activity/list`)。
|
|
|
|
|
+9. **限流**:`rateLimitInterceptor` 可能让高频批量测试偶发失败,建议在测试客户端加节流或识别限流返回后退避重试。
|
|
|
|
|
+10. **参数校验覆盖**:对 `health-status/save` 等做边界用例(身高 <30、体重 >300、血压格式错误、血糖越界)验证中文错误码;对 `activity/detail`、`report/latest` 等做缺 `id`/`memberId` 的负向用例。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 附:核心证据文件索引
|
|
|
|
|
+
|
|
|
|
|
+| 主题 | 文件:行号 |
|
|
|
|
|
+|------|-----------|
|
|
|
|
|
+| 响应结构 / code 约定 | `common/Result.java:8,16,39,45,52` |
|
|
|
|
|
+| 401 未登录写出 | `config/JwtInterceptor.java:205-209` |
|
|
|
|
|
+| 403 越权写出 | `config/FamilyAccessInterceptor.java:135-140`;`common/AdminOrgAccess.java` |
|
|
|
|
|
+| 拦截注册 / 白名单 exclude | `config/WebConfig.java:94-179` |
|
|
|
|
|
+| 公开白名单 PUBLIC_PATHS | `config/JwtInterceptor.java:32-77` |
|
|
|
|
|
+| token 取用与属性注入 | `config/JwtInterceptor.java:102-191` |
|
|
|
|
|
+| JWT 签发(claims=userId/role/roles) | `config/JwtConfig.java:46-68` |
|
|
|
|
|
+| 登录/注册/切换接口 | `controller/auth/AuthController.java:67,136,175,222,278,330,349,376` |
|
|
|
|
|
+| 登录返回 DTO(token 字段) | `dto/LoginResultDTO.java` |
|
|
|
|
|
+| 角色字段 | `entity/User.java:30,34,99` |
|
|
|
|
|
+| 切换角色(真实路由) | `AuthController.java:330`;`FamilyUserController.java:101`;`FamilyMembersController.java:84`;废弃 `FamilyUserController.java:94,121,129` |
|
|
|
|
|
+| 活动接口 | `controller/ActivityController.java:28,36,44,71,123,167` |
|
|
|
|
|
+| 家庭/成员切换 | `controller/family/FamilyController.java:59`;`FamilyMembersController.java:84` |
|
|
|
|
|
+| 健康现状 | `controller/HealthStatusController.java:28,48,79` |
|
|
|
|
|
+| 健康报告(4001) | `controller/HealthReportController.java:206,216,295,541` |
|
|
|
|
|
+| 任务 | `controller/task/TaskController.java:73,81,109,189,255` |
|
|
|
|
|
+| 端口 | `src/main/resources/application.yml:2` |
|