# CFC 小程序后端「接口契约」评审报告 > 评审角色:集成·接口级评审专家(柯砚) > 代码仓库:`D:/cfc` · 后端:`D:/cfc/cfc-backend`(Spring Boot 2.7,端口 **9082**,统一 `@PostMapping`,JWT Bearer 鉴权) > 评审方式:全程只读,仅新增本文件,未修改/删除/提交任何源码 > 目标:为把空体冒烟测试升级为业务级测试(带真实参数、多角色 token)提供契约基线,逐步达到 100% 通过 --- ## 0. 前置结论(先读这段) 1. **统一响应结构**:`Result{code,message,data}`,位于 `common/Result.java`。`code=200` 成功;其余为非 0 失败码。 2. **鉴权**:`JwtInterceptor` 拦截 `/api/**`,白名单见 §2。**token 通过 `Authorization: Bearer ` 头传递**(也可走 `?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` 与 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`,`: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`。token 字段名 = **`token`**(`LoginResultDTO`,见 `src/main/java/com/etotem/cfc/dto/LoginResultDTO.java`),响应体取值 `data.token`。前端/测试用 `Authorization: Bearer ` 携带。`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 `。注意 `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` | | `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` 或实体;缺参通常 `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` | | `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` | | `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` | | `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` |