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 之前已被拒绝(证据链,非推测):
JwtAuthenticationFilter.authenticate在建立认证前调用authSessionService.isSessionActive(...);AuthSessionService.isSessionActive第一步isUserUsable(userId)→user.isPresent() && isUsable(user),而isUsable只检查user.getActive();- 为 false 时
SecurityContextHolder.clearContext()并直接返回,请求根本不会进入 Controller; - 因此对 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 改动清单内 | 否 | 否 |
判定:
Handler: Type = null证明请求从未进入任何 Controller,JobReviewService/UserRoleGuard/GlobalExceptionHandler均未被调用 —— 因此与 R01-B 新逻辑无关;- 401 由
SecurityConfig的认证入口产出(body 形状{"code":401,"message":"not authenticated"}), 不是GlobalExceptionHandler的ApiResponse形状(后者会带业务码); - 拒绝发生在
AuthSessionService.isUserUsable—— 用户 id=1/2 正是V1_1种子账号、 被V43置为 inactive; - 全量顺序下
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 无关:
- 本轮全程未操作 Docker(任务书明令禁止),Docker 守护进程不在运行属环境状态;
- 紧邻的上一次全量
mvn clean verify(R01-B 实施阶段)为2656 / F=0 / E=0 / Skipped=0—— 同一份生产代码下这 4 个类执行并通过,证明它们未被 R01-B 破坏; - 本轮相对该次运行,生产代码零改动(仅新增 3 个测试类 + 1 个测试资源 + 1 份文档);
- 计数自洽:
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提交后以独立纯测试提交修复。