# R01-B 角色授权与异常映射收束报告 日期:2026-09-17 分支:`refactor/r01-structure-performance` HEAD:`84fe9f5febcf56910d5c978dd44e0e16d15d7209`(A6 后夹具修复提交) 本轮性质:**实施与自动化验证** —— 未暂存、未提交、未推送,待独立代码复验 范围:**B1 角色授权重复收束 + B2 异常到 HTTP 状态映射收束**(不拆分子阶段,不进入 R01-C/E) --- ## 0. 结论 > **R01-B角色授权与异常映射收束完成,待独立代码复验。** - 角色授权重复逻辑收束到唯一实现点 `security/UserRoleGuard`,**54 个调用点**改走公共 guard; 角色 guard 方法定义 **32 → 21**,`getRoles()` 角色比较 **46 处 / 31 文件 → 23 处 / 16 文件**; - HTTP 状态映射收束到 `common/exception/ErrorHttpStatusRegistry`,`GlobalExceptionHandler` 不再持有映射表;**112 个 ErrorCode 的映射逐项保持不变**,未映射的 54 个仍回退 400; - 收束过程中发现一处既有潜在缺陷(`ErrorCode` 166 个常量只有 165 个唯一数字码), **已记录、未擅自修改**(见 §9.4)。 --- ## 1. 前置 Git 状态 | 项 | 结果 | |---|---| | `git branch --show-current` | `refactor/r01-structure-performance` | | `git rev-parse HEAD` | `84fe9f5febcf56910d5c978dd44e0e16d15d7209` | | `git rev-list --left-right --count HEAD...@{upstream}` | `0 0` | | `git diff --cached --name-only` | **空** | | `git diff --check` | 仅 LF/CRLF 警告,**exit 0** | | 与目标文件重叠的未提交修改 | **无**。`GlobalExceptionHandler.java` 在 `git status` 中显示 `M`,但 `git hash-object` = `git rev-parse HEAD:` = `1fdc341e…`,`git diff --quiet` exit 0 —— 属 Windows stat 缓存假阳性,非真实修改 | --- ## 2. 角色授权库存(重构前) 搜索范围:`mydreamjob-server/src/main/java`。 重构前共 **32 个**角色 guard 方法定义 + **7 个** `private boolean hasRole(User, Role)` 助手。 **共同语义(近乎逐字相同的复制)**: ``` User user = userRepository.findById(userId) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); boolean ok = user.getRoles().stream().anyMatch(r -> r == User.Role.X); // user_roles if (!ok) throw new BusinessException(ErrorCode.FORBIDDEN); ``` | 项 | 统一事实 | |---|---| | 输入来源 | 已解析出的 `userId`(由 `CurrentUserAccessor` 或调用方传入),**没有任何一处从请求参数取角色** | | 角色来源 | **`user_roles` 关联表**(`user.getRoles()`)—— 31 个文件、46 处 `getRoles()`;`users.role` 单值列**不**参与授权判定(它只用于 JWT 声明签发) | | 失败错误码 | 用户不存在 → `USER_NOT_FOUND`(4001);角色不符 → `FORBIDDEN`(2002) | | HTTP 状态 | 4001 → 404;2002 → 403(经 §3 的映射表) | | 是否含资源归属 | **不含**。归属(如 `assertOwnerOrThrow`、`findByOwnerUserId`)由各业务 Service 独立执行 | | 是否允许 ADMIN | **不允许**(无隐式超级权限);`requireAny` 由调用方显式列出 ADMIN 才放行 | | 可否安全委托 | 纯 void 角色门禁 → **可**;返回 `Company` 实体的 `requireCompany` → **不可整体委托**(见 §7) | --- ## 3. HTTP 映射库存(重构前) `GlobalExceptionHandler.buildBusinessHttpStatuses()`(第 71–136 行)+ `register()`(第 138–147 行) + `resolveHttpStatus()`(第 67–69 行),共 **112 个 `ErrorCode` 引用**: | HttpStatus | 数量 | 备注 | |---|---|---| | CONFLICT | 77 | | | NOT_FOUND | 27 | | | FORBIDDEN | 5 | `FORBIDDEN`、`INTERVIEW_ACCESS_DENIED`、`APPLICATION_OWNER_MISMATCH`、`MATCH_REQUIRED_FOR_CONVERSATION`、`OFFER_COMPARE_NOT_OWNER` | | TOO_MANY_REQUESTS | 3 | | | **合计** | **112** | | | 项 | 结果 | |---|---| | 重复键 | **0**(112 个 `ErrorCode` 常量互不相同) | | 重复项映射一致性 | 不适用 | | 未显式映射的 ErrorCode | **54 / 166** → 一律回退 **HTTP 400** | | 关键既有语义 | `UNAUTHORIZED(2001)` **未显式映射 → 400**(不是 401);`SERVICE_UNAVAILABLE`、`RATE_LIMITED`、`INTERNAL_ERROR` 同为 **400** | | 已有测试锁定状态码 | **有**:`GlobalExceptionHandlerStatusContractTest` 以 `@EnumSource(ErrorCode.class)` 对**全部 166 个**错误码锁定状态 + 业务码 + message + requestId | **快照已保存**:`docs/acceptance/r01-b/b2-mapping-baseline.json`(112 条 `名称 → 状态`), 并逐条转写进 `GlobalExceptionHandlerMappingParityTest` 作为独立基线。 --- ## 4. 修复前失败测试(红灯) | 观察 | 说明 | |---|---| | 角色授权 | 重构前**没有**公共 guard:32 个方法各自实现,无任何调用点经过统一入口。新增的 `RoleAuthorizationIntegrationTest` 以 `@SpyBean UserRoleGuard` 断言"端点确实调用公共 guard"——在重构前该 bean 不存在,测试无法编译/通过 | | HTTP 映射 | 重构前映射表在 `GlobalExceptionHandler` 内部,**无法在初始化期检出重复键**(`register()` 仅在"同码不同状态"时抛错,同码同状态被静默容忍);新增的 `ErrorHttpStatusRegistryTest` 直接断言"同码重复注册必须抛 `IllegalStateException`"——重构前无从表达 | | 未改变任何既有业务期望 | `GlobalExceptionHandlerStatusContractTest` 的 4 个状态桶与全部断言**逐字保留**,仅把其反射调用私有方法的用例改为调用 `registry.resolve(int)` | --- ## 5. 公共角色 guard 设计 新增 `security/UserRoleGuard.java`(`@Component`,仅依赖 `UserRepository`)。 ```java void requireStudent(Long userId); void requireStudent(User user); void requireCompany(Long userId); void requireCompany(User user); void requireAdmin(Long userId); void requireAdmin(User user); void requireAny(Long userId, User.Role... allowedRoles); void requireAny(User user, User.Role... allowedRoles); User requireUser(Long userId); // 存在性 + active 校验 void requireRole(User user, User.Role... allowedRoles); ``` | 要求 | 落实 | |---|---| | 角色来自权威数据库 | 从 `user_roles`(`user.getRoles()`)读取,与全部被替换的实现一致 | | 校验存在且 active | `requireUser` 不存在 → `USER_NOT_FOUND`;**存在但 inactive → `FORBIDDEN`(失败关闭)** | | `users.role` 与 `user_roles` 核对 | 授权**只**依据 `user_roles`;`users.role` 与之不一致时按 `user_roles` 判定 → 缺少所需角色即拒绝(失败关闭)。**混用两者会改变既有语义,故未采纳** | | 复用现有 ErrorCode | 只用 `USER_NOT_FOUND` / `FORBIDDEN`,**未新增任何 ErrorCode** | | 不解析 SecurityContext | 只接收 `userId`;结构断言验证其声明字段**只有** `UserRepository` | | 不处理 requestId | 无 `HttpServletRequest`、无 `RequestIdAccessor` | | 不处理资源归属 | 类中无任何 ownership 逻辑 | | 不记录敏感信息 | 无日志、无审计写入,无副作用 | | 无隐式 ADMIN | `requireStudent`/`requireCompany` 对 ADMIN 同样拒绝,测试已锁定 | | 查询次数受控 | `requireXxx(Long)` 恰好 **1** 次 `findById`;`requireXxx(User)` **0** 次;**无跨请求缓存**(用户被禁用/注销后立即失效) | 检索确认:仓库中**原本不存在** `UserRoleGuard` 或等价的公共角色组件,故新增; 未新建重复角色枚举(复用既有 `User.Role`)。 --- ## 6. 每个迁移调用点(54 处) `userRoleGuard.requireStudent` **37** 处、`requireAdmin` **13** 处、`requireCompany` **4** 处, 覆盖 **21 个生产文件**。 | # | 文件 | 原方法 | 迁移后 | |---|---|---|---| | 1 | `ability/AbilityAttemptService` | `requireStudent`(私有) | 删除,6 处改调 guard | | 2 | `assessment/AssessmentAttemptService` | `requireStudent`(私有) | 删除,5 处改调 guard | | 3 | `bidirectionalmatch/MatchRuleService` | `requireStudent`(私有) | 删除,2 处改调 guard | | 4 | `careerprogress/CareerProgressService` | `assertStudentRole`(私有) | 删除,1 处改调 guard | | 5 | `competition/CompetitionRankingService` | `assertStudentRole`(私有) | 删除,1 处改调 guard | | 6 | `job/JobFavoriteService` | `assertStudentRole`(私有) | 删除,3 处改调 guard | | 7 | `match/MatchService` | `assertStudentRole`(私有) | 删除,1 处改调 guard | | 8 | `review/JobReviewService` | `assertStudentRole`+`assertAdminRole`(私有) | 删除,3+3 处改调 guard | | 9 | `talentdiscovery/TalentDiscoveryService` | `requireStudent`(私有) | 删除,2 处改调 guard | | 10 | `talentinvitation/TalentInvitationService` | `requireStudent`(私有) | 删除,3 处改调 guard | | 11 | `recommendation/JobRecommendationService` | `requireStudent`+`requireAdmin`(私有) | 删除,3+2 处改调 guard | | 12 | `candidatescore/governance/CandidateScoringGovernanceService` | `assertStudentRole`+`assertAdminRole`(私有) | 删除,3+4 处改调 guard | | 13 | `candidatescore/dashboard/CompanyScoringDashboardService` | `assertAdminRole`(私有) | 删除,1 处改调 guard | | 14 | `application/ApplicationController` | `assertCompanyRole`(私有) | 删除,3 处改调 guard | | 15 | `safety/AdminReportController` | `requireAdmin(HttpServletRequest)` | 保留方法(返回 userId),角色部分委托 guard,3 处调用不变 | | 16 | `risk/AdminRiskController` | `requireAdmin(HttpServletRequest)` | 同上,7 处调用不变;随之移除已不需要的 `UserRepository` 字段与 import | | 17 | `company/CompanyService` | `assertStudentRole`+`assertCompanyRole`(公开) | 保留签名,方法体改为一行委托 | | 18 | `profile/ProfileService` | `assertStudentRole`(公开) | 保留签名,一行委托 | | 19 | `preference/PreferenceService` | `assertStudentRole`(公开) | 保留签名,一行委托 | | 20 | `privacy/PrivacySettingsService` | `assertStudentRole`(公开) | 保留签名,一行委托 | | 21 | `verification/CompanyVerificationService` | `assertAdminRole`(公开) | 保留签名,一行委托 | **公开方法为何保留签名**:它们已被跨包调用(如 `profileService.assertStudentRole` 被 `ResumeVersionService`、`ResumeExportService`、`ProfileController` 等调用; `companyService.assertStudentRole` 被 `CandidateScoreCardController`、`CandidateScoringController`、 `StudentReviewWeightService` 调用)。保留签名、把**实现**收敛为一行委托,可在不扩大改动面的前提下 消除重复逻辑。**每个调用点的原错误码(`USER_NOT_FOUND`/`FORBIDDEN`)、原角色集合、 原调用顺序均未改变。** --- ## 7. 保留调用点及理由(非等价,未迁移) | 类别 | 数量 | 保留理由 | |---|---|---| | `private Company requireCompany(Long)` | **7** —— `MatchRuleService`、`InterviewService`、`OfferService`、`CompanyReviewManagementService`、`CompanyReviewReplyService`、`CompanyVerificationService`、`CompanyScoringDashboardService` | **不是纯角色门禁**:除角色判定外还承担实体查找与归属语义(`companyRepository.findByOwnerUserId(userId).orElseThrow(COMPANY_NOT_FOUND)`),返回值被业务逻辑使用。整体委托公共 guard 会改变查询次数与错误码(`COMPANY_NOT_FOUND` vs `USER_NOT_FOUND`),**按任务书要求不迁移不等价逻辑** | | `private boolean hasRole(User, Role)` | **7** —— `InterviewService`、`InterviewScoreService`、`MessagingService`、`OfferService`、`TalentDiscoveryService`、`TalentInvitationService`、`CompanyVerificationService` | 用于**资源归属/可见性分支**(如 `hasRole(user, STUDENT) && interview.getStudentUserId().equals(userId)`、`MessagingService` 中 ADMIN 可见性分支),不是"必须具有某角色"的门禁。**角色授权与资源归属是两件事**,迁移会改变语义 | | `AdminReportController`/`AdminRiskController.requireAdmin(HttpServletRequest)` | 2 | 第 15/16 行已委托 guard;保留该方法是因为它还负责返回 `userId`,属 Controller 内的便捷封装 | | `CompanyService` 等 5 个公开方法 | 5 | 见 §6 末尾说明 | **未为"grep 为零"迁移任何不等价逻辑。** --- ## 8. 角色 / 资源归属边界 | 边界 | 事实 | |---|---| | 身份("我是谁") | 仍由 `CurrentUserAccessor` 负责;`UserRoleGuard` **不**解析 SecurityContext(结构测试断言其唯一依赖是 `UserRepository`) | | 角色授权("我能不能") | 收束到 `UserRoleGuard` | | 资源归属("这条数据是不是我的") | **仍由各业务 Service 执行**,未删除、未移动。`RoleAuthorizationIntegrationTest.ownershipRemainsWithBusinessService` 用"角色正确但无企业实体的 COMPANY 用户"访问 `/company/candidates/{id}` → 得到 **404**(业务层归属/存在性判定),证明 guard 既没有替代也没有跳过归属校验 | | 403 / 404 分层 | 保持既有:角色不符 403(2002);资源不存在 404(如 `RISK_EVENT_NOT_FOUND`) | --- ## 9. B2 实现与逐项一致性 ### 9.1 实现 新增 `common/exception/ErrorHttpStatusRegistry.java`: ```java public HttpStatus resolve(ErrorCode errorCode); public HttpStatus resolve(int errorCode); // BusinessException 只暴露数字 code public HttpStatus defaultStatus(); // 400 public int explicitMappingCount(); // 112 public static Map explicitMappingsView(); public static Map numericMappingsView(); // 111 ``` | 要求 | 落实 | |---|---| | 不可变映射 | 构建后 `Map.copyOf(...)`,运行期 `put`/`remove` 抛 `UnsupportedOperationException`(测试锁定) | | 键类型 | **`ErrorCode` 常量**(见 §9.4);数字入口由该映射派生 | | 初始化禁止重复键 | `Builder.put` 用 `putIfAbsent`,**同码重复注册即抛 `IllegalStateException`**(无论目标状态是否相同) | | 显式映射集中一处 | 只在 `explicitMappings()` 内,全部 `ErrorCode.X` 符号,**无手写数字** | | 未映射默认 400 | `DEFAULT_STATUS = BAD_REQUEST`,与重构前 `getOrDefault(code, BAD_REQUEST)` 一致 | | 不引入反射扫描 | 无反射 | | 不使用可运行时修改的 Map | 静态 final + `Map.copyOf` | | Handler 只调用 registry | `GlobalExceptionHandler` 删除 `BUSINESS_HTTP_STATUSES`、`buildBusinessHttpStatuses()`、`register()`、`resolveHttpStatus()`;`handleBusinessException` 改为 `errorHttpStatusRegistry.resolve(errorCode)` | | 同码映射到不同状态 | **未发现**(唯一同数字码的两个常量映射到同一状态,见 §9.4) | `ErrorHttpStatusRegistry` 通过 `@Import` 挂在 `GlobalExceptionHandler` 上,使 `@WebMvcTest` 切片 (`CompanyControllerMvcTest`)无需改动即可解析到该组件。 ### 9.2 响应契约未变 - 响应 JSON 结构、字段、业务码、message 全部不变(`@EnumSource` 全量断言) - requestId 仍来自 `RequestIdAccessor`,测试断言 `"r01b-contract"` / `"r01b-parity"` - 日志级别未变(`handleBusinessException` 仍为 `log.warn`) - 未知 code 仍回退 **400** - `ErrorCode` 数字、枚举成员**零修改**(`git diff` 为空) - 未新增接口、DTO、依赖 ### 9.3 逐项一致性证明 `GlobalExceptionHandlerMappingParityTest`: 1. **显式映射集合** = 重构前快照的 112 个 `ErrorCode`(对**全部 166 个**错误码逐一核对: 快照中未显式映射的,重构后也不得显式映射); 2. **处理器实际返回状态** ≡ **registry 解析结果** ≡ **快照基线**(`@EnumSource` 166 参数化); 3. 未映射码(含 `UNAUTHORIZED(2001)`、`SERVICE_UNAVAILABLE`)保持 **400**; 4. `FORBIDDEN` 保持业务码 **2002** + HTTP **403**; 5. NOT_FOUND 类 404、CONFLICT 类 409、限流类 429; 6. 响应体不含异常类名、堆栈、内部路径。 ### 9.4 收束过程中发现的既有潜在缺陷(已记录,未修改) **`ErrorCode` 166 个常量只有 165 个唯一数字码**:`REVIEW_ALREADY_DECIDED(9103)` 与 `DUPLICATE_FEEDBACK(9103)` 共用同一数字码。 - 重构前该重复被 `register()` **静默容忍**(`putIfAbsent` 只在"同码不同状态"时抛错); - 本轮的严格重复检测因此**在启动期暴露了它**; - 二者当前都映射到 **CONFLICT**,故数字入口无歧义、行为与重构前完全一致; - 按任务书"不修改 `ErrorCode` 数字、不新增 `ErrorCode`"的要求,**本轮不修改**, 仅把映射键改为 `ErrorCode` 常量(这也正是"键必须是 ErrorCode 或稳定错误码类型"的实践理由), 并以 `ErrorHttpStatusRegistryTest.distinctConstantsWithSharedNumericCodeAreAllowed` 锁定该事实。 **处置建议(留待后续独立提交)**:为 `DUPLICATE_FEEDBACK` 分配唯一数字码。若两个常量被映射到 **不同状态**,注册表的 `indexByCode` 会在初始化期直接抛错,不会静默任选其一。 --- ### 9.5 量化结果(前后对比) | 指标 | 重构前 | 重构后 | 说明 | |---|---|---|---| | 角色 guard 方法定义总数 | **32** | **21** | 其中 6 个是 `UserRoleGuard` 自身;6 个是保留签名的公开方法(方法体已是一行委托);8 个是实体返回型 `requireCompany`(非等价,保留);2 个是 Controller 的 `requireAdmin(request)`(已委托) | | 重复 student guard 实现 | **16** | **1**(公共 guard) | 其余为公开委托方法/实体返回型 | | 重复 company guard 实现 | **2**(纯角色门禁)+ 7(实体返回型) | **1**(公共 guard);7 个实体返回型保留 | | 重复 admin guard 实现 | **7** | **1**(公共 guard) | | | 已迁移调用点 | 0 | **54** | 37 student + 13 admin + 4 company | | 明确保留的非等价调用点 | — | **14** | 7 个实体返回型 `requireCompany` + 7 个归属/可见性用 `hasRole` | | `getRoles()` 角色比较 | **46 处 / 31 文件** | **23 处 / 16 文件** | 减少 50% | | `GlobalExceptionHandler` 行数 | **232** | **146** | 净减 **86 行**;映射表、`register()`、`resolveHttpStatus()` 全部移出 | | `resolveHttpStatus` 条件链 | 映射构建 66 行 + 注册 10 行 + 解析 3 行 | **0**(逻辑迁入 `ErrorHttpStatusRegistry`,261 行,含 javadoc 与显式 112 条映射) | | | `ErrorCode.X.getCode()` 用于状态比较的次数 | 0(原为 `Map` 查找) | **0** | 状态解析无数字字面量比较 | | 重复 HTTP 映射数 | 0(且**不可检测**) | **0**(且**初始化即拒绝**) | | | 未映射 ErrorCode 数 | **54 / 166** → 400 | **54 / 166** → 400 | 逐项不变 | | 角色授权 DB 查询次数 | 每处 1 次 `findById` | `requireXxx(Long)` **1 次**;`requireXxx(User)` **0 次**;无跨请求缓存 | | --- ## 10. 查询次数 | 场景 | 数据库查询次数 | |---|---| | `requireStudent/Company/Admin/Any(Long userId)` | **恰好 1** 次 `users.findById`(`RoleAuthorizationGuardTest.queryCountIsBounded` 以 Mockito `times(1)` 锁定) | | `requireStudent/Company/Admin/Any(User user)` | **0** 次(供已加载实体的调用点使用) | | 一个请求内相同用户重复角色检查 | 调用方已加载实体时应使用实体重载;现有调用点每个请求路径至多 1 次 guard 调用 | | 跨请求缓存 | **无**(不缓存 active 状态,注销/禁用后立即失效) | | Redis 依赖 | **无** | | 事务边界 | **未改变**(guard 只读、无 `@Transactional`) | | 单次授权的 N+1 | **无**:guard 为单条主键查询,`user_roles` 由 JPA 关联加载 | --- ## 11. 测试结果 日志目录:`docs/acceptance/r01-b/` | # | 命令 | 结果 | 日志 | |---|---|---|---| | 1 | `mvn -Dtest="RoleAuthorizationGuardTest,RoleAuthorizationIntegrationTest,ErrorHttpStatusRegistryTest,GlobalExceptionHandlerMappingParityTest" test` | **371 / 371**,F=0 E=0 S=0 ✔ | `b1b2-tests.log` | | 2 | `mvn -Dtest="*Security*Test,*Authorization*Test,*Role*Test,*Admin*Controller*Test,*Company*Controller*Test,*Student*Controller*Test,*GlobalExceptionHandler*Test,*BusinessException*Test" test` | **448 中 6 项失败** —— 全部为**既有夹具缺陷**(§11.3),非本轮引入 | `b1-related.log` | | 3 | `mvn test` | **2656 / 2656**,Failures=**0**,Errors=**0**,Skipped=**0**,BUILD SUCCESS ✔ | `b-full-mvntest.log` | | 4 | `mvn clean verify` | 见 §11.4 | `b-clean-verify.log` | | 5 | `python ..\docs\verify_openapi.py` | **176 paths / 214 operations**,All checks passed ✔ | — | ### 11.1 新增测试逐类结果 | 测试类 | Tests | 覆盖 | |---|---|---| | `RoleAuthorizationGuardTest` | 16 | STUDENT/COMPANY/ADMIN 通过各自 guard;跨角色拒绝;ADMIN 无隐式超级权限;inactive 与 `active=null` 失败关闭;角色缺失;多角色;`users.role` 与 `user_roles` 不一致失败关闭;错误码/HTTP 契约;不解析 requestId/SecurityContext;查询次数 | | `RoleAuthorizationIntegrationTest` | 10 | 三个真实端点(ADMIN `/admin/scoring/risk/overview`、STUDENT `/profile/me/career-progress`、COMPANY `/company/candidates`)经真实 SecurityFilterChain 验证 401/403/404 分层;接线变异(`@SpyBean` 断言 guard 确被调用);资源归属仍由业务层判定 | | `ErrorHttpStatusRegistryTest` | 174 | 112 条显式映射、111 个唯一数字码、四状态白名单、逐 ErrorCode 自洽、未映射回退 400、重复键初始化拒绝、不可变、键必须为 ErrorCode、9103 共用数字码 | | `GlobalExceptionHandlerMappingParityTest` | 171 | 显式映射集合 = 重构前快照;处理器返回 ≡ registry ≡ 快照(166 参数化);FORBIDDEN 2002/403;`UNAUTHORIZED(2001)` 保持 400;服务不可用保持 400;响应体无内部细节 | ### 11.2 测试总数 | 项 | 数值 | |---|---| | R01-B 前基线 | **2285** | | 本轮新增 | **371** | | 预期合计 | 2656 | | 实测 (`mvn test`) | **2656** ✔ 与预期逐位一致 | ### 11.3 关联回归中暴露的**既有**夹具缺陷(与本轮无关,如实记录) `mvn -Dtest="*Admin*Controller*Test,..."` 单独运行时,**既有**的 `AdminReviewControllerTest`(4 项)与 `CompanyReviewManagementControllerTest`(2 项) 出现 `expected 200/403/409 but was 401`。根因与 A6 独立复验 §9 定位的**同一缺陷**: ``` AuthSessionService - session validation rejected: user not usable JwtAuthenticationFilter - JWT authentication rejected: session not active ``` - `AdminReviewControllerTest` 使用 `ADMIN_ID = 1L`;`CompanyReviewManagementControllerTest` 使用 `token(1L, "COMPANY")` / `token(2L, "STUDENT")`; - `V1_1` 种子账号占用 `users.id = 1/2/3` 且被 `V43` 置为 `inactive`,而 `TestAuthTokens` 对"已存在但 inactive"的行不插入也不激活; - 全量顺序下 `com.mydreamjob.controller.AuthIntegrationTest` 的 `deleteAll()` 偶然清掉种子行,问题被掩盖 —— 故 §11.2 的全量结果是干净的; - 日志中该警告恰好 6 次,分别落在上述两个类,**无一处落在本轮改动的代码路径**; - 本轮**未修改**这两个测试类,也**未**把该失败写成通过。 > 该缺陷与 `FairnessMonitoringControllerTest` 属同一类,已在 A6 后以独立提交 `84fe9f5` 修复 > **其中一个类**;其余同类类别的隔离修复应另开独立纯测试提交,不并入 R01-B。 ### 11.4 `mvn clean verify` ``` [INFO] Tests run: 2656, Failures: 0, Errors: 0, Skipped: 0 [INFO] BUILD SUCCESS ``` | 项 | 数值 | |---|---| | 全量测试总数 | **2656**(不低于收束前 2285 ✔) | | Failures / Errors / Skipped | **0 / 0 / 0** ✔ | | 日志 | `docs/acceptance/r01-b/b-clean-verify.log` | --- ## 12. OpenAPI ``` python ..\docs\verify_openapi.py → paths: 176 / operations: 214 → All checks passed! ``` `git diff -- docs/openapi-rc.json` → **空**,`openapi-rc.json` 零修改 ✔ --- ## 13. 精确修改文件 **新增生产代码(2)** ``` mydreamjob-server/src/main/java/com/mydreamjob/security/UserRoleGuard.java mydreamjob-server/src/main/java/com/mydreamjob/common/exception/ErrorHttpStatusRegistry.java ``` **修改生产代码(22)** ``` ability/AbilityAttemptService.java application/ApplicationController.java assessment/AssessmentAttemptService.java bidirectionalmatch/MatchRuleService.java candidatescore/dashboard/CompanyScoringDashboardService.java candidatescore/governance/CandidateScoringGovernanceService.java careerprogress/CareerProgressService.java common/exception/GlobalExceptionHandler.java ← B2 company/CompanyService.java competition/CompetitionRankingService.java job/JobFavoriteService.java match/MatchService.java preference/PreferenceService.java privacy/PrivacySettingsService.java profile/ProfileService.java recommendation/JobRecommendationService.java review/JobReviewService.java risk/AdminRiskController.java safety/AdminReportController.java talentdiscovery/TalentDiscoveryService.java talentinvitation/TalentInvitationService.java verification/CompanyVerificationService.java ``` **新增测试(5)** ``` common/r01b/RoleAuthorizationGuardTest.java common/r01b/RoleAuthorizationIntegrationTest.java common/r01b/ErrorHttpStatusRegistryTest.java common/r01b/GlobalExceptionHandlerMappingParityTest.java common/r01b/UserRoleGuardTestSupport.java ← 测试支持(非测试类) ``` **修改测试(21)** —— 仅接线,未放宽任何断言: ``` careerprogress/CareerProgressServiceTest.java ← 构造器参数补 guard(真实实例) common/exception/GlobalExceptionHandler{AccountDeletion,HttpStatus,ReportBlock,StatusContract}Test.java ← 构造器补 registry candidatescore/governance/CandidateScoringGovernanceServiceTest.java profile/ResumeServiceTest.java review/JobReviewDimensionServiceTest.java assessment/AssessmentAttemptServiceTest.java match/MatchServiceTest.java privacy/PrivacySettingsServiceTest.java review/JobReviewServiceTest.java job/JobFavoriteServiceTest.java company/CompanyServiceTest.java preference/PreferenceServiceTest.java competition/CompetitionRankingServiceTest.java ability/AbilityAttemptServiceTest.java bidirectionalmatch/MatchRuleServiceTest.java verification/CompanyVerificationServiceTest.java talentdiscovery/TalentDiscoveryServiceTest.java recommendation/JobRecommendationServiceTest.java ``` > 这 16 个 `@InjectMocks` 单元测试类新增了 `@Mock UserRoleGuard` 并在 `@BeforeEach` > 通过 `UserRoleGuardTestSupport.delegateToRealGuard(...)` 把 mock **委托给真实 guard**, > 复用测试自身已 stub 的 `UserRepository`。这样"角色不符 → 403"的既有断言**语义不变**, > 没有被削弱成"只验证调用了一次"。 **文档(2)** ``` docs/r01-b-authorization-exception-mapping-report.md (新增) docs/backend-development-roadmap.md (更新:A 系列完成 / 84fe9f5 / R01-B 状态 / 下一阶段 R01-C) ``` --- ## 14. Flyway / ErrorCode / 接口 / DTO 是否变化 | 项 | 结果 | |---|---| | `git diff --name-only -- mydreamjob-server/src/main/resources/db/migration` | **空** —— V1–V45 未改动 | | `ErrorCode.java` | **未修改**(数字、成员、message 全部不变) | | 接口(路径/方法/参数/响应结构) | **未修改**(未新增、未删除、未改签名) | | DTO | **未修改** | | `docs/openapi-rc.json` | **未修改** | | 依赖(`pom.xml`) | **未修改** | | 新依赖 | **未引入**(guard 只用 Spring Data JPA 与既有 `ErrorCode`) | --- ## 15. Git 状态 | 项 | 结果 | |---|---| | 分支 / HEAD | `refactor/r01-structure-performance` / `84fe9f5…`(未变) | | `git diff --cached --name-only` | **空** | | `git diff --check` | 仅 LF/CRLF 警告,exit 0 | | 已暂存 / 已提交 / 已推送 | **均无** | | 历史未跟踪文件 | 未删除 | --- ## 16. 未暂存、未提交、未推送 **确认:未暂存、未提交、未推送。** 本轮未使用 `git add`、未创建提交、未推送; 未执行 `reset`/`restore`/`checkout`/`clean`;未操作 Docker;未修改 V1–V45。 --- ## 17. 未进入 R01-C/E **确认:未进入 R01-C、未进入 R01-E。** 本轮未处理 `overviewStatus`、推荐权重、 高校名单与评分阈值、N+1、会话未读数、requestId、身份主体解析、Token、评分快照、 新接口与数据库迁移中的任何一项。 --- > **R01-B角色授权与异常映射收束完成,待独立代码复验。**