12 KiB
R01-A3 独立代码复验报告(第三批:8 个 Controller 请求身份与 requestId 收束)
日期:2026-09-17
分支:refactor/r01-structure-performance
HEAD:52c8026b2e56dae7dbfae6a828a9efe23e180445(复验全程未变)
复验对象:docs/r01-a3-request-context-migration-report.md
性质:仅复验 —— 未修改生产代码、未修改既有测试、未暂存、未提交、未推送
0. 结论
R01-A3独立代码复验通过;A2与A3当前仍共存于未提交工作区,下一步必须按文件和hunk拆分为两个独立提交,不得合并提交。
FAIL = 0(A3 范围内);SKIP = 0。 另发现基线已存在的非 A3 阻断缺陷 1 项(§7),已用基线 worktree 证伪归因。
1. 边界确认
| 检查项 | 结果 |
|---|---|
| 分支 | refactor/r01-structure-performance ✔ |
| HEAD | 52c8026b2e56dae7dbfae6a828a9efe23e180445 ✔ |
| 暂存区 | 空 ✔ |
git diff --check |
无空白错误 ✔ |
| 工作区构成 | A2(8 个 Controller)+ A3(8 个 Controller) + 路线图 + GlobalExceptionHandler(stat 缓存,内容零差异)✔ |
| 归因纪律 | 本报告的 A3 证据仅取自 A3 的 8 个文件路径;A2 的 8 个文件未计入 A3 证据 ✔ |
| 未进入 A4 | ✔ |
| 未修改既有污染测试 | ✔ |
2. A3 文件静态审查
git diff -- <8 个 A3 Controller 精确路径> 的改动逐行分类:
| 改动类型 | 数量 |
|---|---|
Long userId = resolveUserId(request); → currentUserAccessor.requireUserId() |
21 |
新增 private final CurrentUserAccessor currentUserAccessor; |
8 |
新增 private final RequestIdAccessor requestIdAccessor; |
8 |
删除 import ...SecurityContextHolder; |
8 |
删除 import ...Authentication; |
7 |
删除 return request.getHeader("X-Request-Id"); |
7 |
删除 return Long.valueOf(auth.getName()); |
7 |
getRequestId(request) / requestId(request) → requestIdAccessor.get(request) |
其余全部调用点 |
riskAuditContext 内 header 直读 → requestIdAccessor.get(request) |
1 |
2.1 私有方法删除清单 —— 实际为 16 个(实施报告写 17,属笔误)
逐条核对(git diff | grep '^-.*private .*(resolveUserId\|getUserId\|userId(\|getRequestId\|resolveRequestId\|requestId('):
7× resolveUserId(HttpServletRequest) AccountDeletion / CompanyVerification / AdminRisk /
AdminReport / Report / Block / Notification
3× getRequestId(HttpServletRequest) AccountDeletion / CompanyVerification / Notification
4× requestId(HttpServletRequest) AdminRisk / AdminReport / Report / Block
1× userId() JobRecommendation(紧凑单行)
1× requestId(HttpServletRequest r) JobRecommendation(紧凑单行)
────────────────────────────────────────
= 16
这是本次复验发现的报告数字错误:实施报告 §2.1 称「共删除私有解析方法 17 个」,实际为 16 个(8 个 Controller × 2)。行为本身正确(8 个文件门禁命中均为 0),仅报告计数有误。
JobRecommendationController 原压缩单行身份方法已完整删除:
- private Long userId(){ Authentication a=SecurityContextHolder.getContext().getAuthentication(); ... }
- private String requestId(HttpServletRequest r){return r.getHeader("X-Request-Id");}
其 9 处调用点全部改为 currentUserAccessor.requireUserId() / requestIdAccessor.get(request) ✔
2.2 注入方式
8 个 Controller 均为 private final 构造器注入;无 @Autowired(required=false)、@Nullable、ObjectProvider、new CurrentUserAccessor()、new RequestIdAccessor() ✔
2.3 未漂移项
| 核查项 | 结论 |
|---|---|
| 角色检查位置 | 未移动 —— verificationService.assertAdminRole(userId)×4、AdminRisk/AdminReport/Report 内的 userRepository.findById → getRoles → FORBIDDEN、JobRecommendation 的 Service 层裁决,全部原样保留 ✔ |
| 资源归属检查 | 未移动 ✔ |
| Service 调用顺序 | 未变 ✔ |
| DTO / 错误码 / 响应结构 | 未变(ErrorCode、SecurityConfig、Flyway 均零修改)✔ |
是否混入 UserRoleGuard 或其他重构 |
否 ✔ |
3. 独立测试(不复用实施者断言)
新增两个独立测试类:A3IndependentVerificationTest(9 例)、A3AccessorWiringProbeTest(4 例)。
| 覆盖 | 结果 |
|---|---|
8 端点 × 8 非法主体(anonymousUser/空/空白/非数字/abc123/20 位溢出/0/-1) |
全部 401,无 500 ✔ |
| 匿名 token 与无 Token | 401 ✔(无 Token 由 Security 链提前返回,按要求只断言 401、不要求补 requestId) |
| 错误体安全 | code=401;message ∈ {authentication required, not authenticated};无类名/堆栈/principal;非数字标记不回显(纯数字主体不参与该断言,避免与状态码/时间戳碰撞)✔ |
| 溢出主体 | 无 NumberFormatException;解析 JSON 后仅检查业务字段,未做整响应数字搜索 ✔ |
| 合法主体 | 不被身份层拒绝 ✔ |
| 角色语义未漂移 | 显式 stub 角色层抛 FORBIDDEN → 403(非 401);AdminRiskController 桩造非 ADMIN 用户 → 403 ✔ |
| request attribute 优先于 header | ✔ |
| 响应 body 使用规范化 requestId(与响应头同源) | 5 种输入 × 8 端点 ✔ |
| CR/LF | 不进入规范化结果(直接经 Accessor 验证)✔ |
| Accessor 接线变异检测 | mock 抛未登录 → 8 端点必须 401;mock requestId 必须出现在响应体;verify(notificationService).list(42L, …) —— 删除 Accessor 调用会使该测试失败 ✔ |
4. 统计口径重新核对(任务书第四节)
使用更完整的模式(含 requestId(...) 方法名与 X-Request-Id 直读):
rg -l 'request\.getHeader\("X-Request-Id"\)|String\s+(getRequestId|resolveRequestId|requestId)\s*\(' \
mydreamjob-server/src/main/java -g "*.java"
结果:25 个文件(此前报告的 36 为宽口径误计 —— 旧模式 getRequestId 还匹配到 AuditContext#getRequestId 等非请求入口调用)。
| 分类 | 数量 | 明细 |
|---|---|---|
| 合理基础设施 | 4 | audit/AuditContext.java、common/exception/GlobalExceptionHandler.java、common/logging/RequestLoggingFilter.java、deletion/DeletionGuardInterceptor.java |
| 尚未迁移的业务 Controller | 21 | bidirectionalmatch/*×3、candidatescore/governance/、careerprogress/、controller/*×3、feedback/、interviewscore/、job/*×2、match/、review/*×5、scoringmonitoring/、talentdiscovery/、talentinvitation/ |
| Service 或非请求入口 | 0 | — |
| 误报 | 0 | — |
真实业务剩余数 = 21 个业务 Controller(非「36 个文件」)。
身份解析:
rg -l 'SecurityContextHolder\.getContext\(\)\.getAuthentication|Long\.(valueOf|parseLong)\([^)]*getName\(\)' \
mydreamjob-server/src/main/java -g "*.java"
结果:20 个文件 = 基础设施 3(security/CurrentUserAccessor、auth/JwtAuthenticationFilter、deletion/DeletionGuardInterceptor)+ 业务 Controller 17 + 误报 0。
真实业务剩余数 = 17 个业务 Controller。
5. 验证结果
| 命令 | 结果 |
|---|---|
mvn -Dtest="R01A3…,A3IndependentVerificationTest,A3AccessorWiringProbeTest,CurrentUserAccessorTest,RequestIdAccessorTest,RequestIdFilterTest" test |
51 / 51,F=0 E=0 S=0 |
关联回归(*Deletion*Test,*Verification*Test,*Risk*Test,*Report*Test,*Block*Test,*Notification*Test,*Recommendation*Test,*RequestContext*Test,*RequestId*Test) |
260 / 260,F=0 E=1,BUILD FAILURE —— 全部为基线已存在的非 A3 缺陷(§7) |
mvn clean verify |
2220 / 2220,Failures=0、Errors=0、Skipped=0,BUILD SUCCESS |
python docs\verify_openapi.py |
176 paths / 214 operations,All checks passed |
- 较实施轮次(2207)新增 13 个测试,全部为本次独立复验新增的两个测试类
docs/openapi-rc.json零修改;ErrorCode/ Flyway /SecurityConfig零修改 ✔
6. 独立真实 HTTP 验证
只读实例 PID 16604,验证后已停止。
| 场景 | 结果 |
|---|---|
| 无 token / 畸形 Bearer / 纯数字 Bearer × 8 端点 | 全部 401 ✔ |
| STUDENT → ADMIN 风险概览 / 举报 / 认证审核 | 403 ✔ |
| ADMIN → 风险概览(合法) | 200 ✔ |
| STUDENT → 通知列表(合法) | 200 ✔ |
| 上述全部请求前后 22 张表逐字段比较 | 全部零变化 ✔ |
7. 基线已存在的非 A3 阻断缺陷(如实记录,未修复)
关联回归命令触发 1 个 Error:
AccountDeletionControllerTest.lazyTransition_firstGet_returnsCompleted_andConsistent:228->createUser:75
DataIntegrityViolation: USERS(PHONE) 唯一键冲突 '930000000'
隔离复现(任务书要求):在 git worktree add --detach HEAD(不含 A2/A3 改动)中用完全相同的命令运行:
| 测试数 | 失败签名 | |
|---|---|---|
| 基线(HEAD) | 223 | AccountDeletionControllerTest...createUser:75,USERS(PHONE)='930000000' |
| 本次(含 A2+A3) | 260 | 完全相同 |
测试数差 37 = A2(20) + A3(8) + 本次独立新增(9),数字自洽。
判定:基线已存在的非 A3 阻断缺陷,与 A2/A3 无关。
- 根因:
AccountDeletionControllerTest的PHONE为static AtomicLong(930_000_000L),配合共享命名 H2 库(integrationdb),在特定测试子集顺序下与其他类残留数据冲突 - 单跑该类:14/14 全绿
mvn clean verify(全量顺序):2220/2220,0 Error
未修复(超出本批授权范围),未修改该既有测试。建议后续单独处理。
8. 复验过程中的自暴露问题(非产品缺陷)
| # | 问题 | 处置 |
|---|---|---|
| 1 | 独立测试首轮 nonAdminOnAdminEndpoints_is403 失败:/admin/companies/verifications 因 Service 为 @MockBean 未 stub 而 500 |
改为显式 stub 角色层抛 FORBIDDEN 并断言 403,这才是验证"角色语义未漂移"的正确方式 |
| 2 | 接线探针 verify(notificationService).list(42L) 参数个数错误(实际为 3 参) |
改为 list(eq(42L), anyInt(), anyInt()) |
9. 结论与下一步约束
R01-A3独立代码复验通过;A2与A3当前仍共存于未提交工作区,下一步必须按文件和hunk拆分为两个独立提交,不得合并提交。
必须遵守的下一步约束:
- 按文件与 hunk 精确拆分为两个独立提交:
- 提交 1(A2):
ApplicationController、CompanyController、MessagingController、OfferController、InterviewController、CandidateScoringController、CandidateScoreCardController、CompanyScoringDashboardController(8 个)+ A2 测试 + A2 报告 - 提交 2(A3):
AccountDeletionController、CompanyVerificationController、AdminRiskController、AdminReportController、ReportController、BlockController、NotificationController、JobRecommendationController(8 个)+ A3 测试 + A3 报告 docs/backend-development-roadmap.md同时被两批修改,必须用git add -p拆分 hunk
- 提交 1(A2):
- 不得合并提交。
GlobalExceptionHandler.java仅 stat 缓存标记(内容零差异),不得提交。- 既有污染测试缺陷不属 A2/A3,若修复应作为独立提交,不得混入。
复验完成前,未暂存、未提交、未推送、未进入 A4。