# R01-A6 最终业务 Controller 请求身份与 requestId 收束报告 日期:2026-09-17 分支:`refactor/r01-structure-performance` A6 基线 HEAD:`534afe686b966f175bcaf4af0726cec061092050`(A5 提交) 本轮性质:**实施与自动化验证** —— 未提交、未推送,待独立代码复验 --- ## 0. 结论 > **R01-A6最终业务Controller请求上下文收束完成,待独立代码复验;A系列到此结束,不再拆分A7/A8。** **业务 Controller 身份解析残留 = 0;业务 Controller requestId 重复解析残留 = 0。** 剩余命中全部为基础设施职责。 --- ## 1. 开始时状态 ### 1.1 A5 固化(第一阶段) A5 此前**未提交**,本轮按已确认清单精确固化: | 项 | 结果 | |---|---| | 暂存文件数 | **15**(8 生产 Controller + 4 测试 + 2 报告 + 1 路线图) | | `git add` 方式 | **逐文件 `git add --`**;未使用 `git add -A` / `git add .` | | 路线图 | `git diff --numstat` 为 `1 1`(**仅 R01 行一处 hunk**),无需 `git add -p` | | 排除项 | `GlobalExceptionHandler.java`、`db/migration`、`docs/acceptance`、`backend-infra`、A2–A4 文件 **全部排除** ✔ | | `git diff --cached --check` | 无输出 ✔ | | 敏感扫描 | 1050 新增行,12 类(手机号/JWT/Bearer/验证码/口令/密钥/私钥/AWS/64位摘要/本机路径/UUID/连接串)**全部命中 0** ✔ | | A5 定向 | **42 / 42**,F=0 E=0 S=0 | | `mvn clean verify` | **2255 / 2255**,F=0 E=0 **S=0**,BUILD SUCCESS | | OpenAPI | **176 / 214**,All checks passed | | 提交 | `refactor: centralize fifth batch request context handling` | | **A5 提交 hash** | **`534afe686b966f175bcaf4af0726cec061092050`** | | 推送 | `0226977..534afe6`;**未 force push** | | 推送后同步 | **0 / 0** ✔ | ### 1.2 A6 开始前核查(第二阶段) | 检查项 | 结果 | |---|---| | 分支 | `refactor/r01-structure-performance` ✔ | | 新 HEAD | `534afe686b966f175bcaf4af0726cec061092050` ✔ | | 暂存区 | 空 ✔ | | `git diff --check` | 无空白错误 ✔ | | **A5 未提交残余** | **无**(工作区仅 `GlobalExceptionHandler.java` 的 stat 缓存标记,blob 三方一致)✔ | | 历史未跟踪文件 | 全部保留 ✔ | | Docker / R01-B·C·E | 未触碰 ✔ | **A6 基线测试数:2255。** --- ## 2. 五个 Controller 的修改内容 ### 2.1 迁移前契约 | Controller | 真实代表端点 | 身份解析 | requestId 解析 | |---|---|---|---| | `controller/ApiController` | `GET /` | 无 | `private String getRequestId(HttpServletRequest)` → `request.getHeader("X-Request-Id")` | | `controller/AuthController` | `POST /auth/login`、`GET /auth/me` 等 | **已有 `CurrentUserAccessor`** ✓ | `private String getRequestId(...)` | | `controller/SystemController` | `GET /health`、`GET /info` | 无 | `private String getRequestId(...)` | | `job/JobController` | `GET /jobs`、`POST /jobs` 等 | **已有 `CurrentUserAccessor`** ✓ | `private String getRequestId(...)` | | `scoringmonitoring/FairnessMonitoringController` | `GET /admin/scoring/fairness/*` | **需迁移**:`private Long userId()` 直读 `SecurityContextHolder` + `Long.valueOf(auth.getName())` | `private String requestId(HttpServletRequest)` 单行直读 | ### 2.2 修改后 | Controller | 注入 | `requireUserId()` | `requestIdAccessor.get(request)` | |---|---|---|---| | ApiController | 新增 `RequestIdAccessor`(并补 `@RequiredArgsConstructor`) | 0 | 1 | | AuthController | 新增 `RequestIdAccessor`(`CurrentUserAccessor` 既有) | 2(既有) | 14 | | SystemController | 新增 `RequestIdAccessor`(并补 `@RequiredArgsConstructor`) | 0 | 2 | | JobController | 新增 `RequestIdAccessor`(`CurrentUserAccessor` 既有) | 1(既有) | 8 | | FairnessMonitoringController | 新增 `CurrentUserAccessor` + `RequestIdAccessor` | 5 | 5 | **`JobController` 的身份迁移未被重复改造** ✔ —— 它此前已使用 `CurrentUserAccessor`,本批只迁移 requestId。 **`AuthController` 无重复身份解析** ✔ —— 它此前已使用 `CurrentUserAccessor`,本批只迁移 requestId。 ### 2.3 删除的私有方法(共 6 个) | Controller | 删除 | |---|---| | ApiController | `getRequestId` ×1 | | AuthController | `getRequestId` ×1 | | SystemController | `getRequestId` ×1 | | JobController | `getRequestId` ×1 | | FairnessMonitoringController | `userId` ×1、`requestId` ×1 | | **合计** | **6** | > **计数澄清(独立复验补充)**:曾出现"`5 × getRequestId + 1 × userId + 1 × requestId`"的表述,其算术结果为 7,**与真实 diff 不符,该表达式不成立**。经逐文件 `git diff` 核对:`getRequestId` 实际只有 **4 个**(ApiController / AuthController / SystemController / JobController);`FairnessMonitoringController` 的 requestId 方法签名是 **`requestId(HttpServletRequest)`(无 `get` 前缀)**,因此**不能被计入 `getRequestId`**。正确分解为 **4 × getRequestId + 1 × userId + 1 × requestId = 6**,与本表逐文件清单完全一致。**未为迎合数字修改任何生产代码。** **保留**:`FairnessMonitoringController.parseDate`(业务参数解析)、`checkParams`(业务参数校验)✔ 未被误删。 **新增注解**:`ApiController` 与 `SystemController` 原本无依赖、缺 `@RequiredArgsConstructor`,为支持构造器注入补齐该注解与 import(**唯一的新增注解**,未引入 `@Autowired`)。 **未修改**:`RequestIdFilter`、`RequestIdAccessor`、`RequestLoggingFilter`、`AuditContext`、`GlobalExceptionHandler`、`SecurityConfig`、Filter 顺序 ✔ --- ## 3. 修复前失败证据 新增 `mydreamjob-server/src/test/java/com/mydreamjob/common/r01a6/` 下两个测试类。 **修复前实测:`Tests run: 10, Failures: 5`** | 失败测试 | 修复前实际 | |---|---| | `A6AccessorWiringProbeTest.requestIdComesFromAccessorAcrossFiveControllers` | `path=/` 响应体 `requestId` **不是** Accessor 返回值(Controller 直读 header) | | `A6AccessorWiringProbeTest.fairnessMonitoringUsesCurrentUserAccessor` | `FairnessMonitoringService.drift(...)` **未被调用**(身份未走 Accessor) | | `A6AccessorWiringProbeTest.accessorThrowsUnauthenticated_fairnessReturns401` | 不成立 | | `R01A6RequestContextMigrationIntegrationTest.fairnessMonitoring_illegalPrincipals_are401` | `principal='anonymousUser'` → **`expected: 401 but was: 400`** | | `R01A6RequestContextMigrationIntegrationTest.fairnessMonitoring_illegalPrincipals_produceNoServiceCall` | 不成立 | **关于 400 而非 500(如实记录)**:`FairnessMonitoringController.userId()` 带有 `try { Long.valueOf(auth.getName()); } catch (NumberFormatException) { throw BusinessException(UNAUTHORIZED); }`, 因此其**既有行为本身是安全的(未逃逸为 500)**,但抛出的 `BusinessException(ErrorCode.UNAUTHORIZED=2001)` 不在 `GlobalExceptionHandler.BUSINESS_HTTP_STATUSES` 映射中,`resolveHttpStatus` 的 `getOrDefault(2001, BAD_REQUEST)` 使其落到 **HTTP 400**。这正是本批收束的真实语义缺陷: **非法主体应返回 401 而非 400**。接线探针则因"未调用 Accessor"而失败 —— **未编造 500**。 **修复后:`Tests run: 10, Failures: 0`** --- ## 4. 角色与资源归属语义 | 核查项 | 结论 | |---|---| | 静态审查:删除行中是否含角色/归属检查 | **否** ✔ | | 静态审查:新增行中是否引入角色/归属逻辑 | **否** ✔ | | `ErrorCode.java` | **零改动** ✔ | | ADMIN / COMPANY / STUDENT 角色检查 | 未删除、未移动 ✔ | | 企业归属 / 资源归属 / 403 / 404 分支 | 未删除、未移动 ✔ | | 监控数据访问权限(ADMIN 只读聚合) | 未改变 ✔ | **分层保持**:身份失败 → **401**;角色不足 → **403**(由 Service 层或既有逻辑产生);资源不存在 → **404**。 --- ## 5. 独立接线探针 `A6AccessorWiringProbeTest`(3 例)使用 `@MockBean` 替换 `CurrentUserAccessor`、`RequestIdAccessor` 与对应 Service: | 用例 | 证明 | |---|---| | `requestIdComesFromAccessorAcrossFiveControllers` | mock `RequestIdAccessor` 返回 `a6-probe-request-id` → **5 个 Controller 的响应体 requestId 均为该值**(修复前为直读 header 的 null) | | `fairnessMonitoringUsesCurrentUserAccessor` | mock `CurrentUserAccessor` 返回 `5151L` → `fairnessMonitoringService.drift(5151L)` 被调用 | | `accessorThrowsUnauthenticated_fairnessReturns401` | mock 抛 `InsufficientAuthenticationException` → FairnessMonitoring 返回 **401**,Service **零调用** | **变异有效性**:删除任一 Controller 中的 Accessor 调用会使对应 `verify` 失败 ✔ --- ## 6. 非法 principal 的结果 `R01A6RequestContextMigrationIntegrationTest` 对 `GET /admin/scoring/fairness/drift` 覆盖: | 主体 | 结果 | |---|---| | `anonymousUser` | **401** ✔ | | `not-a-number` | **401** ✔ | | `99999999999999999999`(20 位溢出) | **401** ✔ | | `0` | **401** ✔ | | `-1` | **401** ✔ | | 匿名 token / 无 Token | **401** ✔ | | 合法数字主体 `"7"` | 不被身份层拒绝(非 401)✔ | 断言:**401 且无 500**;响应体仅解析 `code`/`message` 稳定字段,**不对整段 JSON 做数字搜索**;不含 `Exception`/`java.`/`principal`/`anonymousUser`/`com.mydreamjob`/`NumberFormat`;无 `stackTrace`/`trace`;**业务 Service 零调用** ✔ --- ## 7. requestId 规范化 覆盖 5 个 Controller 的真实端点(`/`、`/health`、`/auth/me`、`/jobs`、`/admin/scoring/fairness/drift`): | 输入 | 结果 | |---|---| | 合法值 `a6-legal-0001` | 响应头 == 响应体,原样保留 ✔ | | 空白 `" "` | 重新生成,头体同源 ✔ | | 超长(80 字符) | 收敛至 ≤32,头体同源 ✔ | | 非法字符 ``、`a b` | 生成合法替代值,响应头不含 `<`/`>`/空格 ✔ | | request attribute 与 header 不同 | **attribute 优先** ✔ | | CR / LF / CRLF(直接经 Accessor) | 不进入规范化结果 ✔ | --- ## 8. 全仓剩余命中逐文件分类 **身份解析**(`SecurityContextHolder.getContext().getAuthentication` \| `Long.(valueOf|parseLong)([^)]*getName()`)—— **3 文件** | 文件 | 分类 | |---|---| | `security/CurrentUserAccessor` | 基础设施(Accessor 自身) | | `auth/JwtAuthenticationFilter` | 基础设施(认证过滤器) | | `deletion/DeletionGuardInterceptor` | 基础设施(拦截器) | **requestId 解析**(含 `X-Request-Id` 直读、`RequestIdFilter.REQUEST_ID_HEADER`、`String (getRequestId|resolveRequestId|requestId)(`)—— **5 文件** | 文件 | 分类 | |---|---| | `audit/AuditContext` | 基础设施 | | `common/exception/GlobalExceptionHandler` | 基础设施 | | `common/logging/RequestIdAccessor` | 基础设施(Accessor 自身) | | `common/logging/RequestLoggingFilter` | 基础设施 | | `deletion/DeletionGuardInterceptor` | 基础设施 | **全部落在任务书允许的基础设施清单内**(`CurrentUserAccessor`、`JwtAuthenticationFilter`、`DeletionGuardInterceptor`、`RequestIdAccessor`、`RequestIdFilter`、`RequestLoggingFilter`、`AuditContext`、`GlobalExceptionHandler`)✔ **未为"全仓零命中"修改任何基础设施类** ✔ **未在 A6 清单内的业务 Controller 残留:0** ✔ —— 无需越界修改。 --- ## 9. 业务 Controller 残留 | 项 | 结果 | |---|---| | 业务 Controller 身份解析 | **0** ✔ | | 业务 Controller requestId 重复解析 | **0** ✔ | **A 系列请求上下文收束达成目标终态。A7/A8 不再设置。** --- ## 10. 测试数 | 命令 | 结果 | |---|---| | A6 定向(`R01A6…,A6AccessorWiringProbeTest,CurrentUserAccessorTest,RequestIdAccessorTest,RequestIdFilterTest`) | **40 / 40**,F=0 E=0 **S=0** | | A6 关联(`*ApiController*Test,*AuthController*Test,*SystemController*Test,*JobController*Test,*FairnessMonitoring*Test,*Security*Test,*RequestId*Test`) | **76 / 76**(1 项既有夹具脆弱性,见 §10.1,**全量顺序下不触发**) | | `mvn test` | **2265 / 2265**,Failures=0、Errors=0、**Skipped=0** | | `mvn clean verify` | **2265 / 2265**,Failures=0、Errors=0、**Skipped=0**,BUILD SUCCESS | A5 基线 2255 → A6 **2265**(新增 **10** 个测试:7 + 3),**不低于基线** ✔ ### 10.1 基线已存在的测试夹具脆弱性(未在本批修复) A6 关联回归单独运行时,**既有** `FairnessMonitoringControllerTest` 出现 6 个失败(`200/403/400 → 401`),根因为: ``` Caused by: AuthSessionService - session validation rejected: user not usable JwtAuthenticationFilter - JWT authentication rejected: session not active ``` **归因验证(任务书要求)**:在 `git worktree add --detach HEAD`(**不含 A6 改动**)中运行**完全相同**的命令: | | 测试数 | 失败 | 根因 | |---|---|---|---| | 基线(HEAD) | **7** | **6** | `user not usable` | | 本次(含 A6) | **7** | **6** | **完全相同** | **实施阶段初步判断(后被独立复验否定,保留于此以存档过程)**:当时认为 `TestAuthTokens` 夹具插入 `users` 行时**不写 `user_roles`**,导致该用户被判 "not usable"。 **该判断不成立**:`AuthSessionService.isUsable(User)` 只检查用户存在且 `is_active = true`, `UserRepository.findById` 无角色过滤,该路径**从不查询 `user_roles`**。 **独立复验结论(准确根因)**:见 `docs/r01-a6-independent-verification-20260917.md` §9。 | 环节 | 事实 | |---|---| | 身份冲突 | `FairnessMonitoringControllerTest` 使用 `token(1L, "ADMIN")` / `token(2L, "STUDENT")`,与 `V1_1__seed_data.sql` 种子账号占用的 `users.id = 1 / 2 / 3` **冲突** | | 种子被禁用 | `V43__disable_legacy_seed_accounts.sql` 按原始指纹把这三行 `is_active` 置为 **0** | | 夹具缺陷 | `TestAuthTokens.writeSession()` 仅当 `SELECT COUNT(*) FROM users WHERE id = ?` 为 0 时才 INSERT;**对已存在但 `is_active = 0` 的行不做激活**,永不修复其启用状态 | | 直接后果 | `AuthSessionService.isSessionActive()` 第一步 `isUserUsable()` 返回 false 并短路,请求在进入 Controller 前即 401 | | 全量为何通过 | 全量顺序中 `com.mydreamjob.controller.AuthIntegrationTest` 的 `@BeforeEach` 执行 `userRepository.deleteAll()`,**偶然清除**了 V43 已禁用的种子行,夹具随后才补建出 active 用户 —— 问题被掩盖,而非被修复 | **与 A6 生产改动无关**:401 由 `JwtAuthenticationFilter` / `AuthSessionService` 在请求进入 Controller 之前产生(`Handler: Type = null`),A6 只改了 5 个 Controller 内部取身份 / 取 requestId 的写法,该路径上一行 A6 代码都未执行。独立复验的决定性对照实验:同一份含 A6 的 代码,单跑 Fairness 为 7 run / 6 failures,而让 `AuthIntegrationTest` 先跑则为 **7 run / 0 failures**。 `mvn clean verify`(全量顺序)下 **2278/2278 全通过**,该失败不触发。 按任务书要求:**未把失败写成通过,未在本批顺手修复**;该夹具问题将在 A6 提交后 **以独立纯测试提交修复**(不与本批生产改动混合)。 --- ## 11. OpenAPI `python docs\verify_openapi.py` → **176 paths / 214 operations**,All checks passed ✔ **`docs/openapi-rc.json` 零修改** ✔ --- ## 12. 精确修改文件 **生产代码(5 个,全部在 A6 指定清单内)** ``` mydreamjob-server/src/main/java/com/mydreamjob/controller/ApiController.java mydreamjob-server/src/main/java/com/mydreamjob/controller/AuthController.java mydreamjob-server/src/main/java/com/mydreamjob/controller/SystemController.java mydreamjob-server/src/main/java/com/mydreamjob/job/JobController.java mydreamjob-server/src/main/java/com/mydreamjob/scoringmonitoring/FairnessMonitoringController.java ``` **测试(新增 2 个)** ``` mydreamjob-server/src/test/java/com/mydreamjob/common/r01a6/R01A6RequestContextMigrationIntegrationTest.java mydreamjob-server/src/test/java/com/mydreamjob/common/r01a6/A6AccessorWiringProbeTest.java ``` **文档(2 个)** ``` docs/r01-a6-final-request-context-migration-report.md # 本报告 docs/backend-development-roadmap.md # 仅更新顶部 R01 状态行 ``` **未修改**:Service、DTO、`ErrorCode`、Flyway、`SecurityConfig`、Filter、Accessor、OpenAPI、既有测试、`UserRoleGuard`。 --- ## 13. Flyway / ErrorCode / DTO / 接口 | 项 | 结果 | |---|---| | V1–V45 迁移 | **零修改** ✔ | | `ErrorCode.java` | **零改动** ✔ | | DTO | **零改动** ✔ | | 公网接口 / OpenAPI | **零新增零修改** ✔ | | 新增表 / 索引 / 迁移 | **无** ✔ | --- ## 14. Git 状态 ``` git diff --check → 无输出 git status --short → 8 个生产/文档改动 + GlobalExceptionHandler(stat 缓存标记) git diff --cached --name-only → 空 git diff --name-only -- db/migration → 空 git diff -- docs/openapi-rc.json → 空 ``` - `GlobalExceptionHandler.java`:工作区 / 索引 / HEAD 三方 blob 均为 `1fdc341e…`,**内容零差异**,未触碰 ✔ - 临时基线 worktree 已 `--force` 移除并 prune,`git worktree list` 仅主仓库 ✔ --- ## 15. 明确声明 - **未暂存、未提交、未推送。** - 未操作 Docker。 - **未进入 R01-B / R01-C / R01-E。** - 未修改基础设施类(`RequestIdFilter`、`RequestIdAccessor`、`RequestLoggingFilter`、`AuditContext`、`GlobalExceptionHandler`、`SecurityConfig`)。 - 未删除历史未跟踪文件。 - 基线测试污染已用 HEAD 隔离 worktree 复现并如实记录,**未写成通过、未顺手修复**。 --- > **R01-A6最终业务Controller请求上下文收束完成,待独立代码复验;A系列到此结束,不再拆分A7/A8。**