You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

31 KiB

R01-B 独立代码复验报告(角色授权与异常映射收束)

日期:2026-09-18 分支:refactor/r01-structure-performance HEAD:84fe9f5febcf56910d5c978dd44e0e16d15d7209(A6 后夹具修复提交,复验全程未变) 复验对象:docs/r01-b-authorization-exception-mapping-report.md 性质:仅复验 —— 未修改生产代码、未放宽既有断言、未暂存、未提交、未推送


0. 结论

R01-B独立代码复验通过,可以进入精确暂存与独立提交检查点;两个Controller测试夹具缺陷须在R01-B提交后以独立纯测试提交修复。

  • 角色授权:54 个调用点全部真实存在且仅经 HTTP 入口可达;保留的 14 个点确为不等价; 未发现角色/归属/错误码/事务语义变化。
  • HTTP 映射:从 HEAD 独立提取的 166 项基线逐项一致(0 处不一致);112 显式、54 默认 400; UNAUTHORIZED 仍 400、FORBIDDEN 仍 403、SERVICE_UNAVAILABLE 仍 400。
  • 9103 重复码:运行时确实无法区分(BusinessException 只保存 int)。未夸大为"已解决身份歧义"; 已证明两者状态相同故 HTTP 行为一致,且"同数字不同状态"会初始化失败而非静默任选。
  • 测试委托模式:真实。变异实验(把委托改为无条件下放行)令 63 个负向用例中 50 个失败。
  • 仅有两个既有测试夹具缺陷(AdminReviewControllerTest、CompanyReviewManagementControllerTest), 已用证据证明其失败发生在安全链认证阶段、未进入 R01-B 代码。
  • 一处需如实说明的环境偏差:本轮 mvn clean verify 为 2681 / F=0 / E=0 / Skipped=26。 26 个跳过全部是 @Testcontainers(disabledWithoutDocker = true) 的 MySQL 用例, 因本机 Docker 守护进程未运行而按设计跳过;上一轮同代码全量为 Skipped=0 (这些用例执行并通过)。不声称 Skipped=0,原因与自洽性见 §14.1。 不构成 R01-B 阻断项。

1. 分支、HEAD 与 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 exit 0,无空白错误 ✔
R01-B 改动位置 全部仍在工作区(22 生产 + 21 测试 + 1 文档)
R01-B 之外的新生产改动 无(逐文件比对允许清单,无越界)

2. 角色库存独立计数

独立复验者自行从 HEAD 与工作区分别提取(正则按方法签名匹配,不采信实施报告数字):

项 HEAD 工作区
角色 guard 方法定义总数 32 21
private boolean hasRole(User, Role) 助手 7 7
getRoles() 出现次数 / 文件数 46 / 31 23 / 17
调用公共 guard 的点位 0 54

工作区 21 个定义的构成(逐条列出于下):

类别 数量 明细
UserRoleGuard 自身 6 requireStudent/Company/Admin × (Long, User)
公开兼容委托(方法体一行) 6 CompanyService.assertCompanyRole、CompanyService.assertStudentRole、PreferenceService.assertStudentRole、PrivacySettingsService.assertStudentRole、ProfileService.assertStudentRole、CompanyVerificationService.assertAdminRole
实体返回型 requireCompany(非等价,保留) 7 MatchRuleService、CompanyScoringDashboardService、InterviewService、OfferService、CompanyReviewManagementService、CompanyReviewReplyService、CompanyVerificationService
Controller 便捷包装(返回 userId,已委托 guard) 2 AdminRiskController.requireAdmin(HttpServletRequest)、AdminReportController.requireAdmin(HttpServletRequest)

6 + 6 + 7 + 2 = 21 ✔ 与工作区实测一致。

与实施报告的一处非语义差异:实施报告 §9.5 写 getRoles() 为「23 处 / 16 文件」, 独立实测为「23 处 / 17 文件」(出现次数一致,文件计数差 1)。 该差异不影响任何结论,仅记录以备核对。


3. 54 个迁移调用点对账

逐调用点清单(文件 / 方法 / 旧 guard / 新 guard / 角色 / 错误码 / 归属 / 查询次数)已落盘: docs/acceptance/r01-b/iv-role-callsites.tsv(54 行 + 表头)。

按文件汇总:

文件 调用点
ability/AbilityAttemptService 6
assessment/AssessmentAttemptService 5
candidatescore/governance/CandidateScoringGovernanceService 7
review/JobReviewService 6
recommendation/JobRecommendationService 5
application/ApplicationController 3
job/JobFavoriteService 3
talentinvitation/TalentInvitationService 3
bidirectionalmatch/MatchRuleService 2
company/CompanyService 2
talentdiscovery/TalentDiscoveryService 2
candidatescore/dashboard/CompanyScoringDashboardService 1
careerprogress/CareerProgressService 1
competition/CompetitionRankingService 1
match/MatchService 1
preference/PreferenceService 1
privacy/PrivacySettingsService 1
profile/ProfileService 1
risk/AdminRiskController 1
safety/AdminReportController 1
verification/CompanyVerificationService 1
合计 54
独立核对项 结果
迁移调用点总数 54 ✔
student / admin / company 分布 37 / 13 / 4 ✔(与实施报告一致)
重复统计 无(同一行不会被两个正则重复计入)
32 → 21 方法数 ✔ 逐方法差异已列出:HEAD 独有 17 个定义被删除,工作区新增 UserRoleGuard 的 6 个
是否删除角色检查 否 —— 被删方法的 HEAD 调用点数合计 46,对应迁移后 54 个调用点(含新增的 MatchRuleService 2 处与委托内调用),无一处丢失
是否扩大 ADMIN 权限 否 —— 未引入任何 requireAny(..., ADMIN);ADMIN 仍被 STUDENT/COMPANY 门禁拒绝(HTTP 实测 403)
是否把 404/业务不存在改成 403 否 —— USER_NOT_FOUND 仍映射 404;COMPANY_NOT_FOUND 等业务码未变;实测验端点返回 404 而非 403

4. 14 个保留点及其不等价性核验

类别 数量 独立核验结论
private Company requireCompany(Long) 7 确为不等价:除角色判定外还执行 companyRepository.findByOwnerUserId(...) 并抛 COMPANY_NOT_FOUND,返回值被业务使用。用 void guard 替换会丢失实体与错误码语义,且改变查询次数
private boolean hasRole(User, Role) 7 确为不等价:返回布尔值用于分支选择(如 listMyInterviews 按 COMPANY/STUDENT 走不同查询;listOffers 还要先取 company 实体),并含归属判定(hasRole(user, STUDENT) && interview.getStudentUserId().equals(userId))。void guard 无法表达"值"

已逐处读取源码确认(例:InterviewService.listMyInterviews、OfferService.listOffers 的 if (hasRole(...)) {...} if (hasRole(...)) {...} else 403 分支结构)。 14 与 21 的关系:21 个方法 = 6(guard 自身)+ 6(一行委托)+ 7(实体返回型)+ 2(Controller 包装); 其中真正"不等价保留"的是 7 个实体返回型 requireCompany + 7 个 hasRole 助手 = 14。

一处残留观察(非阻断):ApplicationController.getCompanyCandidateDetail 先调 requireCompany(userId),随后调用的 companyService.findCompanyByOwnerUserId(userId) 内部又调 assertCompanyRole(userId) —— 同一请求 2 次角色检查。经与 HEAD 逐行比对,该重复在收束前就存在 (HEAD 的 assertCompanyRole 同样被 Controller 与 findCompanyByOwnerUserId 各调一次), 非本轮引入,本轮未改变查询次数。可作为后续独立优化点。


5. inactive 语义分析

项 结论
用户不存在 USER_NOT_FOUND(4001) → 404(沿用既有错误码,未新增)
用户存在但 inactive FORBIDDEN(2002) → 403(新增的失败关闭;既有实现无此校验)
active 为 null 同样拒绝(isActive 要求 Boolean.TRUE)
角色不足 FORBIDDEN(2002) → 403(与既有完全一致)
与既有实现的差异面 仅"inactive 用户"这一种输入:收束前会通过,收束后拒绝

HTTP 入口的 inactive 用户在 guard 之前已被拒绝(证据链,非推测):

  1. JwtAuthenticationFilter.authenticate 在建立认证前调用 authSessionService.isSessionActive(...);
  2. AuthSessionService.isSessionActive 第一步 isUserUsable(userId) → user.isPresent() && isUsable(user),而 isUsable 只检查 user.getActive();
  3. 为 false 时 SecurityContextHolder.clearContext() 并直接返回,请求根本不会进入 Controller;
  4. 因此对 HTTP 入口而言,guard 的 active 检查是第二道,观察不到行为差异。

6. 非 HTTP 入口分析

对全部 54 个迁移调用点逐个追溯调用链,结论:全部仅经 HTTP 入口可达。

入口类型 独立核验
HTTP Controller 链路 54/54 调用点的宿主方法均由 @RestController 直达(21 个文件逐一确认)
定时任务 全仓 @Scheduled 类共 8 个:MatchNotificationScheduler、CandidateScoringRefreshScheduler、PersonalDataExportWorker、AccountDeletionService、NotificationConsumer、OfferExpiryScheduler、RiskEventDetector、RiskEventScanScheduler —— 无一个注入或调用任何被迁移的服务或 UserRoleGuard
Worker 同上(PersonalDataExportWorker、CandidateScoringRefreshWorker、AccountDeletionCompletionWorker 的依赖已逐一列出核对)
消息消费者 NotificationConsumer 仅依赖 notification 三件套,未触碰
内部 Service 调用 非 Controller 的调用者仅 4 处:ResumeVersionService/ResumeExportService/StudentReviewWeightService(均只被 Controller 调用)、MatchService.diagnose(唯一调用者是 MatchController)。均为 HTTP 链路
注销/导出/补偿流程 AccountDeletionService、PersonalDataExportWorker、补偿扫描器均未触碰迁移点
管理后台任务 AdminRiskController/AdminReportController 的 requireAdmin 只被各自 HTTP 端点调用

因此:不存在"合法的历史数据处理、注销收尾或后台任务需要对 inactive 用户执行受保护动作"的场景。 独立变量验证:MatchService.diagnose 的唯一调用方是 MatchController(grep "\.diagnose(" 全仓仅 1 处)。


7. 查询次数

场景 独立实测
requireXxx(Long userId) 恰好 1 次 users.findById(Mockito times(1) + verifyNoMoreInteractions 锁定)
requireXxx(User user) 0 次(verifyNoMoreInteractions 覆盖)
guard 写库 0(never()).save/deleteById/delete)
单次授权 N+1 无(单条主键查询;user_roles 由 JPA 关联加载)
跨请求缓存 无(guard 无状态字段,唯一依赖 UserRepository;用户禁用后立即失效)
Redis / SecurityContext / requestId 均未触碰(结构断言 + 行为断言双重证明)
每请求角色检查次数 多数路径 1 次;/company/candidates/{id} 为 2 次 —— 与 HEAD 相同,非回归(见 §4)

8. 测试委托模式是否真实(含变异验证)

8.1 静态核验

项 结果
使用 delegateToRealGuard 的测试类 16 个(另 1 个为支持类本身)
每个类是否同时声明 @Mock private UserRoleGuard userRoleGuard; 是(16/16,各 1 处)
委托是否使用真实 UserRoleGuard 实现 是:new UserRoleGuard(userRepository)
是否使用每个测试自己 stub 的 UserRepository 是:delegateToRealGuard(userRoleGuard, userRepository) 传入该测试类的 @Mock UserRepository
是否使用宽泛 doNothing() 否:UserRoleGuardTestSupport 内 0 处 doNothing();模块内 7 处均为 lenient().doAnswer(...) 显式转发
是否把所有 guard 调用默认放行 否:四个方法均逐字转发到真实 guard;lenient() 只用于消除"未被该用例触发"的多余桩告警,不改变行为
本轮是否新增 doNothing 否:全仓 10 处 doNothing() 全部位于未被 R01-B 改动的文件(git status 为空),且目标是其他 mock(favoriteRepository、messagingService、被 mock 的 companyService 等)

8.2 变异验证(决定性)

把 UserRoleGuardTestSupport.delegateToRealGuard 临时改为空实现(即 mock 的所有 void 方法无条件放行), 运行 5 个含负向断言的测试类:

mvn -Dtest="CompanyServiceTest,JobFavoriteServiceTest,PrivacySettingsServiceTest,MatchServiceTest,JobReviewServiceTest" test
→ Tests run: 63, Failures: 13, Errors: 37   ← 50/63 失败

失败样例:

CompanyServiceTest.findCompany_asStudent_shouldReturn403:71
    Expected BusinessException to be thrown, but nothing was thrown.
CompanyServiceTest.updateCompany_asStudent_shouldReturn403:83
    expected: <2002> but was: <5001>
JobFavoriteServiceTest.favorite_asCompany_shouldReturn403:177
    Unexpected exception type thrown ... but was: java.lang.NullPointerException
MatchServiceTest.diagnose_asAdmin_shouldReturn403:91
    Unexpected exception type thrown ... but was: java.lang.NullPointerException

判据成立:若负向测试在"无条件放行"下仍通过,说明测试未覆盖授权;实测 50 个用例立即失败, 证明这些测试真的执行了角色判断,委托模式未被削弱。 变异后已把文件按原 SHA256 还原(02f25b74cf5d2192882a77930ec1c748a1049509323bf02668dc5b04822c82a1,逐字节一致), 临时备份已删除。日志:docs/acceptance/r01-b/iv-mutation.log。


9. HTTP 映射 166 项对账(独立提取)

基线来源:git show HEAD:…GlobalExceptionHandler.java 的 buildBusinessHttpStatuses(), 独立解析后写入 docs/acceptance/r01-b/reconcile.tsv(166 行)与 mydreamjob-server/src/test/resources/r01b/error-code-http-status-baseline.tsv(测试资源)。 未复用工作区实现的任何映射表,不存在同源假证明。

项 HEAD 工作区 一致
枚举常量总数 166 166 ✔
显式映射常量数 112 112 ✔
显式映射覆盖的唯一数字码 111 111 ✔
默认 400 项 54 54 ✔
逐项状态不一致数 — — 0 ✔
HEAD 中重复注册 无 无 ✔

关键项逐项核对:

ErrorCode 数字码 HEAD 工作区
UNAUTHORIZED 2001 400(未登记默认) 400 ✔
FORBIDDEN 2002 403 403 ✔
SERVICE_UNAVAILABLE 9001 400(未登记默认) 400 ✔
USER_NOT_FOUND 4001 404 404 ✔
APPLICATION_DUPLICATE 8001 409 409 ✔
ABILITY_RETAKE_LIMIT 9811 429 429 ✔
REVIEW_ALREADY_DECIDED 9103 409 409 ✔
DUPLICATE_FEEDBACK 9103 409 409 ✔

(上表数字码与状态均取自独立提取的 docs/acceptance/r01-b/reconcile.tsv。)

响应 JSON、requestId、日志行为未变:GlobalExceptionHandler.handleBusinessException 仍为 log.warn、仍用 ApiResponse.error(errorCode, e.getMessage(), requestIdAccessor.get(request)), 仅把状态解析改为 errorHttpStatusRegistry.resolve(errorCode)。


10. 112 显式 / 54 默认统计(独立复核)

项 实施报告 独立复核
显式映射 112 112 ✔
未映射(默认 400) 54 54 ✔
唯一数字码 111(未声明) 111 ✔(9103 共用)

11. 9103 重复码的真实运行时语义

11.1 逐层查清

层 事实
BusinessException 保存什么 private final int errorCode; —— 只保存数字,枚举身份在构造时即丢失;无 ErrorCode 字段,无 fromCode 反查
GlobalExceptionHandler 拿到什么 int errorCode = e.getErrorCode(); → int
ErrorHttpStatusRegistry.resolve(...) 以什么为键 resolve(ErrorCode) 用枚举键表 EXPLICIT;运行时路径 resolve(int) 用派生数字表 BY_CODE
是否使用 ErrorCode.values() / fromCode() / 首次匹配 全部没有(registry 与 handler 中 values()/valueOf/ordinal/stream() 命中为 0;ErrorCode 亦无 fromCode 辅助方法)
相同数字码不同枚举是否可能被错误归类 运行时不可能区分 —— 两者都是 9103,resolve(9103) 只有一个结果
映射是否依赖枚举声明顺序 不依赖。EXPLICIT 按注册顺序构建(与枚举声明顺序无关),BY_CODE 由它派生并用 putIfAbsent 去重;顺序只影响"谁先写入",而冲突时抛错而非取先者

11.2 明确声明(不夸大)

运行时无法区分 REVIEW_ALREADY_DECIDED 与 DUPLICATE_FEEDBACK:BusinessException 只携带 int 9103,Handler 也只拿到该数字。因此不能声称"以枚举为键已彻底解决身份歧义"; 准确表述是:两者当前都映射为 409 CONFLICT,故 HTTP 行为一致; 枚举键表的价值在于配置期可审计与可去重,而不是运行期消歧。

11.3 独立测试对六项要求的覆盖

# 要求 覆盖用例
1 两个枚举常量都显式注册 bothSharedNumericCodeConstantsAreExplicitlyMappedTo409
2 两者分别解析为 409 同上
3 resolve(9103) 行为明确 runtimeCannotDistinguishTheTwoSharedConstants(= CONFLICT,并断言两个异常的业务码相同)
4 改成不同状态时初始化/解析必须失败 conflictingStatusForSharedNumericCodeFailsFast(构造 9103 → CONFLICT + 9103 → NOT_FOUND,调用数字索引构建必须抛 IllegalStateException,消息含 9103;同时反证状态相同时允许)
5 不依赖 ErrorCode.values() 顺序 mappingDoesNotDependOnEnumDeclarationOrder(正序/逆序注册结果相同 + 逐项比对基线)
6 未映射 code 仍为 400 unknownCodeFallsBackTo400AndResponseIsClean

12. 独立新增测试

测试类 Tests 结果
common/r01b/R01BIndependentRoleVerificationTest 11 通过
common/r01b/R01BIndependentHttpMappingParityTest 9 通过
common/r01b/R01BWiringMutationTest 5 通过
合计 25 25 / 25,F=0 E=0 S=0 ✔

日志:docs/acceptance/r01-b/iv-1-independent.log

覆盖要点:

  • 角色:active 三角色各自通过;三类交叉全部拒绝;ADMIN 不自动通过 STUDENT/COMPANY; 用户不存在(USER_NOT_FOUND);inactive 与 active=null(FORBIDDEN);roles 为空; 多角色;users.role 与 user_roles 不一致时按 user_roles 失败关闭; requireXxx(Long) 恰好 1 次查询、requireXxx(User) 零查询;无写库; 不触碰 SecurityContext(在上下文里放 ADMIN 仍按传入 userId 判定)/requestId/Redis; 三个真实 HTTP 端点验证 401 / 403 / 404 分层。
  • 接线变异:guard 拒绝 → 403 且下游业务方法零调用;guard 放行 → STUDENT 也能通过 ADMIN 端点 (若端点仍保留自有角色比较,STUDENT 不可能通过 → 该断言即变异检测器); guard 收到正确 userId;归属失败仍由业务层返回 404。
  • 映射:166 项按名称逐项比对 HEAD 基线(不作数字去重)、112/54 计数、 9103 六项、未知码 400、响应体无堆栈与内部类名。

13. 两个 Controller 测试单跑结果(如实记录,未写成通过)

命令 结果
mvn -Dtest="AdminReviewControllerTest" test 11 run / 4 failures / 0 errors / 0 skipped,BUILD FAILURE
mvn -Dtest="CompanyReviewManagementControllerTest" test 3 run / 2 failures / 0 errors / 0 skipped,BUILD FAILURE

失败断言:expected 200/400/409 but was 401(6 项合计)。

根因证据链(非按日志文本猜测):

证据 AdminReview CompanyReviewMgmt
Handler: Type = null 次数 4 2
session validation rejected: user not usable 4 2
JWT authentication rejected: session not active 4 2
401 响应体 {"code":401,"message":"not authenticated","timestamp":""} 同
测试使用的用户 ID ADMIN_ID = 1L token(1L,"COMPANY") / token(2L,"STUDENT")
是否在 R01-B 改动清单内 否 否

判定:

  1. Handler: Type = null 证明请求从未进入任何 Controller,JobReviewService / UserRoleGuard / GlobalExceptionHandler 均未被调用 —— 因此与 R01-B 新逻辑无关;
  2. 401 由 SecurityConfig 的认证入口产出(body 形状 {"code":401,"message":"not authenticated"}), 不是 GlobalExceptionHandler 的 ApiResponse 形状(后者会带业务码);
  3. 拒绝发生在 AuthSessionService.isUserUsable —— 用户 id=1/2 正是 V1_1 种子账号、 被 V43 置为 inactive;
  4. 全量顺序下 com.mydreamjob.controller.AuthIntegrationTest 的 deleteAll() 偶然清除种子行, 问题被掩盖,故 §14 全量为绿。

处置:未修改这两个测试;未写成通过;不让它们阻断对 R01-B 生产代码的判断。 R01-B 提交后须以独立纯测试提交修复(与 A6 后 84fe9f5 修复 FairnessMonitoringControllerTest 同法)。


14. 全量测试

命令 结果 日志
mvn -Dtest="RoleAuthorizationGuardTest,RoleAuthorizationIntegrationTest,ErrorHttpStatusRegistryTest,GlobalExceptionHandlerMappingParityTest" test 371 / 371,F=0 E=0 S=0 ✔ iv-2-impl.log
mvn -Dtest="*Security*Test,*Authorization*Test,*Role*Test,*GlobalExceptionHandler*Test,*BusinessException*Test" test 425 / 425,F=0 E=0 S=0 ✔(该命令不含两个已知夹具类,已单独记录于 §13) iv-3-related.log
mvn clean verify 见 §14.1 iv-4-clean-verify.log

14.1 mvn clean verify

[INFO] Tests run: 2681, Failures: 0, Errors: 0, Skipped: 26
[INFO] BUILD SUCCESS
项 数值
测试总数 2681(= 前次 2656 + 本轮新增独立测试 25)✔ 不低于 2656
Failures 0 ✔
Errors 0 ✔
Skipped 26 —— 未达"0"要求,原因如下(环境性,非代码回归)

Skipped=26 的准确原因(如实记录,不掩饰、不写成 0)

26 个跳过全部来自 4 个 MySQL / Testcontainers 测试类:

测试类 Skipped
job.JobSearchSpecificationMySqlIntegrationTest 15
candidatescore.refresh.CandidateScoringRefreshMySqlIntegrationTest 6
job.JobSearchQuerySideEffectMySqlIntegrationTest 4
job.JobSearchQueryExplainMySqlIntegrationTest 1
合计 26

根因:这些类标注 @Testcontainers(disabledWithoutDocker = true)(见 JobSearchSpecificationMySqlIntegrationTest:44 等),Testcontainers 在 Docker 守护进程不可用时 按设计环境性跳过。本机实测:

docker info → failed to connect to the docker API at npipe:////./pipe/dockerDesktopLinuxEngine;
              check if the path is correct and if the daemon is running

为何与 R01-B 无关:

  1. 本轮全程未操作 Docker(任务书明令禁止),Docker 守护进程不在运行属环境状态;
  2. 紧邻的上一次全量 mvn clean verify(R01-B 实施阶段)为 2656 / F=0 / E=0 / Skipped=0 —— 同一份生产代码下这 4 个类执行并通过,证明它们未被 R01-B 破坏;
  3. 本轮相对该次运行,生产代码零改动(仅新增 3 个测试类 + 1 个测试资源 + 1 份文档);
  4. 计数自洽:2656 − 26(改为跳过)+ 25(本轮新增)= 2655 执行 + 26 跳过 = 2681, 即除这 26 个 Docker 条件用例外的全部测试仍执行且全部通过。

结论:该偏差为环境条件,不构成 R01-B 的阻断项;但按任务书"跳过必须是 0"的字面要求, 本环境无法满足,故不声称 Skipped=0。若需在 Docker 可用环境下复核, 重新执行 mvn clean verify 即可(无需任何代码改动)。


15. OpenAPI

python ..\docs\verify_openapi.py
→ paths: 176 / operations: 214
→ All checks passed!

git diff -- docs/openapi-rc.json → 空 ✔


16. 精确文件范围

R01-B 生产改动(22 修改 + 2 新增)

修改: ability/AbilityAttemptService、application/ApplicationController、assessment/AssessmentAttemptService、
      bidirectionalmatch/MatchRuleService、candidatescore/dashboard/CompanyScoringDashboardService、
      candidatescore/governance/CandidateScoringGovernanceService、careerprogress/CareerProgressService、
      common/exception/GlobalExceptionHandler、company/CompanyService、competition/CompetitionRankingService、
      job/JobFavoriteService、match/MatchService、preference/PreferenceService、
      privacy/PrivacySettingsService、profile/ProfileService、recommendation/JobRecommendationService、
      review/JobReviewService、risk/AdminRiskController、safety/AdminReportController、
      talentdiscovery/TalentDiscoveryService、talentinvitation/TalentInvitationService、
      verification/CompanyVerificationService
新增: security/UserRoleGuard、common/exception/ErrorHttpStatusRegistry

测试改动(21 修改 + 4 新增 + 1 支持类 + 1 基线资源)、文档(1 修改 + 1 新增)。 本轮独立复验仅新增:

mydreamjob-server/src/test/java/com/mydreamjob/common/r01b/R01BIndependentRoleVerificationTest.java
mydreamjob-server/src/test/java/com/mydreamjob/common/r01b/R01BIndependentHttpMappingParityTest.java
mydreamjob-server/src/test/java/com/mydreamjob/common/r01b/R01BWiringMutationTest.java
mydreamjob-server/src/test/resources/r01b/error-code-http-status-baseline.tsv
docs/r01-b-independent-verification-20260918.md
docs/acceptance/r01-b/*(验收日志与对账产物,被 .gitignore 覆盖)

未修改任何生产代码 ✔(git diff 生产文件数与复验开始前一致,均为 R01-B 实施者留下的改动)。


17. Git 边界

检查 结果
git diff --check exit 0,无空白错误 ✔
git diff --cached --name-only 空 ✔
git diff --name-only -- …/db/migration 空 —— V1–V45 未改 ✔
git diff -- docs/openapi-rc.json 空 ✔
git diff -- mydreamjob-server/pom.xml 空 ✔
ErrorCode.java 未修改 ✔
接口 / DTO 未修改 ✔
历史未跟踪文件 完整保留 ✔
Java / Maven 残留进程 无 ✔
HEAD / upstream 84fe9f5… / 0 0 ✔

18. 是否存在阻断

无阻断。

复核项 结论
角色语义变化 无(仅新增 inactive 失败关闭,且 HTTP 入口不可观测、非 HTTP 入口不存在)
归属语义变化 无(归属仍由原 Service 执行,实测 404 未被改写为 403)
错误码变化 无(USER_NOT_FOUND/FORBIDDEN 沿用,未新增 ErrorCode)
HTTP 映射变化 无(166 项逐项一致)
事务语义变化 无(guard 只读、无 @Transactional,未改变任何事务边界)
ADMIN 权限扩大 无
测试削弱 无(变异实验 50/63 失败)
阻断项 0

19. 未暂存、未提交、未推送

确认:未暂存、未提交、未推送。 本轮未执行 git add、未创建提交、未推送; 未执行 reset/restore/checkout/clean;首轮复验未操作 Docker,随后在负责人授权的 补充复验中仅启动本机既有 Docker Desktop 并由 Testcontainers 创建临时 MySQL 容器, 未重建项目容器、未删除 volume;未删除历史未跟踪文件。 变异实验对测试支持文件的临时修改已按原 SHA256 逐字节还原并清理备份。


20. 未进入 R01-C/E

确认:未进入 R01-C、未进入 R01-E。 本轮未处理 overviewStatus、推荐权重、 高校名单与评分阈值、N+1、会话未读数、requestId、身份主体解析、Token、评分快照、 新接口与数据库迁移中的任何一项。


R01-B独立代码复验通过,可以进入精确暂存与独立提交检查点;两个Controller测试夹具缺陷须在R01-B提交后以独立纯测试提交修复。


21. Docker / Testcontainers 补充复验(2026-09-18)

21.1 首轮 SKIP 记录保留

首次独立复验时 Docker 守护进程不可用,以下 4 个测试类合计 26 个 MySQL/Testcontainers 用例按 disabledWithoutDocker = true 跳过;该事实及原因分析保留在本报告第 14 节,未删除、 未改写成首次即通过:

测试类 Tests 首轮 Skipped
CandidateScoringRefreshMySqlIntegrationTest 6 6
JobSearchQueryExplainMySqlIntegrationTest 1 1
JobSearchQuerySideEffectMySqlIntegrationTest 4 4
JobSearchSpecificationMySqlIntegrationTest 15 15
合计 26 26

21.2 环境恢复

经负责人授权启动本机既有 Docker Desktop:

Docker Server: 29.6.2
Testcontainers: 1.21.3
MySQL image: mysql:8.4

未重建项目容器、未删除或修改既有 volume。Testcontainers 实际连接 Windows npipe Docker Engine,并成功启动 Ryuk 与临时 MySQL 8.4 容器。

21.3 26 个 MySQL 用例定向补验

执行:

mvn -Dtest="CandidateScoringRefreshMySqlIntegrationTest,JobSearchQueryExplainMySqlIntegrationTest,JobSearchQuerySideEffectMySqlIntegrationTest,JobSearchSpecificationMySqlIntegrationTest" test

结果:

Tests run: 26, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

4 个类均真实启动 MySQL 8.4 Testcontainer,未使用 H2 代替,未通过修改 disabledWithoutDocker 或测试选择规则规避执行。

21.4 完整干净构建

随后执行:

mvn clean verify

结果:

Tests run: 2681, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

并重新执行 OpenAPI 校验:

paths: 176
operations: 214
Valid JSON: True
All checks passed!

本次补验未修改任何生产代码、测试代码、Flyway、ErrorCode、DTO、接口、依赖或 openapi-rc.json。

21.5 最终冻结结论

首轮因环境导致的 26 个 SKIP 已由真实 Docker/Testcontainers 执行闭环,最终硬性门禁为 Failures=0 / Errors=0 / Skipped=0。

R01-B独立代码复验及Docker/Testcontainers补充验证全部通过,可以进入精确暂存与独立提交检查点;两个Controller测试夹具缺陷须在R01-B提交后以独立纯测试提交修复。