28 KiB
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; - 收束过程中发现一处既有潜在缺陷(
ErrorCode166 个常量只有 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:<file> = 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)。
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:
public HttpStatus resolve(ErrorCode errorCode);
public HttpStatus resolve(int errorCode); // BusinessException 只暴露数字 code
public HttpStatus defaultStatus(); // 400
public int explicitMappingCount(); // 112
public static Map<ErrorCode, HttpStatus> explicitMappingsView();
public static Map<Integer, HttpStatus> 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:
- 显式映射集合 = 重构前快照的 112 个
ErrorCode(对全部 166 个错误码逐一核对: 快照中未显式映射的,重构后也不得显式映射); - 处理器实际返回状态 ≡ registry 解析结果 ≡ 快照基线(
@EnumSource166 参数化); - 未映射码(含
UNAUTHORIZED(2001)、SERVICE_UNAVAILABLE)保持 400; FORBIDDEN保持业务码 2002 + HTTP 403;- NOT_FOUND 类 404、CONFLICT 类 409、限流类 429;
- 响应体不含异常类名、堆栈、内部路径。
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角色授权与异常映射收束完成,待独立代码复验。