# 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: ```text 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 用例定向补验 执行: ```text mvn -Dtest="CandidateScoringRefreshMySqlIntegrationTest,JobSearchQueryExplainMySqlIntegrationTest,JobSearchQuerySideEffectMySqlIntegrationTest,JobSearchSpecificationMySqlIntegrationTest" test ``` 结果: ```text Tests run: 26, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ``` 4 个类均真实启动 MySQL 8.4 Testcontainer,未使用 H2 代替,未通过修改 `disabledWithoutDocker` 或测试选择规则规避执行。 ### 21.4 完整干净构建 随后执行: ```text mvn clean verify ``` 结果: ```text Tests run: 2681, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ``` 并重新执行 OpenAPI 校验: ```text 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提交后以独立纯测试提交修复。**