# R01-A4 第四批:请求身份与 requestId 收束报告 日期:2026-09-17 分支:`refactor/r01-structure-performance` HEAD:`35baa8416d31e71dd2c2556877f9d2e1c93b14a1` 范围:**仅 8 个 Controller**(CandidateFeedbackController、ApplicationMatchController、MatchConversationController、MatchRuleController、TalentDiscoveryController、CareerProgressController、MatchController、TalentInvitationController) --- ## 0. 结论 > **R01-A4第四批请求身份与requestId收束完成,待独立代码复验。** 8 个 Controller 的身份与 requestId 已统一收束到既有 `CurrentUserAccessor` / `RequestIdAccessor`;非法主体由**逃逸为 HTTP 500** 改为**统一 401**;角色、归属与 403/404 契约未发生漂移。 --- ## 1. 基线 | 检查项 | 结果 | |---|---| | 分支 | `refactor/r01-structure-performance` ✔ | | HEAD | `35baa8416d31e71dd2c2556877f9d2e1c93b14a1` ✔ | | 与上游同步 | `0 / 0` ✔ | | 暂存区 | 空 ✔ | | `git diff --check` | 无空白错误 ✔ | | 8 个目标文件开始前状态 | **均未改动**(无本批以外修改)✔ | | `GlobalExceptionHandler.java` | 仍为 stat 缓存标记(内容零差异),未处理、未暂存 ✔ | | 未跟踪文件 | 247 个,全部保护 ✔ | --- ## 2. 迁移前契约表 | Controller | 真实 Mapping(代表端点) | 允许角色 / 归属 | 私有解析方法 | 写操作涉及表 | |---|---|---|---|---| | CandidateFeedbackController | `POST/GET/PUT/DELETE /applications/{applicationId}/feedback` | 企业(`assertCompanyOwner`)+ 学生可读 | `resolveUserId` + `getRequestId` | `candidate_feedback` | | ApplicationMatchController | `PUT /applications/{id}/candidate-interest-score`、`PUT .../hr-suitability-score`、`GET /applications/{id}/match` | 学生 / 企业(Service 层) | 同上 | `application_match_records`、`application_match_events` | | MatchConversationController | `POST /applications/{id}/conversation` | 匹配双方 | 同上 | `conversations`、`conversation_members` | | MatchRuleController | `GET/PUT /companies/me/match-rule`、`GET/PUT /profile/me/match-rule` | 企业 / 学生各自规则 | 同上 | `company_match_rules`、`student_match_rules` | | TalentDiscoveryController | `GET/PUT /profile/me/talent-discovery`、`GET /companies/me/jobs/{jobId}/talent-recommendations` | 学生(偏好)/ COMPANY(推荐) | `userId()` + `requestId()`(**紧凑式**) | `talent_discovery_preferences` | | CareerProgressController | `GET /profile/me/career-progress` | STUDENT | `resolveUserId` + `getRequestId` | 只读 | | MatchController | `GET /jobs/{jobId}/match` | STUDENT | 同上 | 只读 | | TalentInvitationController | `POST /companies/me/jobs/{jobId}/talent-invitations`、`GET /companies/me/talent-invitations`、`GET /profile/me/talent-invitations`、`POST /profile/me/talent-invitations/{id}/decision` 等 6 个 | 企业创建 / 学生决定 / 双方读取 | `userId()` + `requestId()`(**紧凑式**) | `talent_invitations` | **契约要点**:8 个 Controller 的角色与资源归属判断**全部位于 Service 层**(Controller 仅解析身份后委派),因此本批迁移不涉及任何角色判断的移动。 **迁移前状态码**:非法/匿名主体因 `Long.valueOf(auth.getName())` 抛 `NumberFormatException` → **HTTP 500**(见 §3 实测)。 --- ## 3. 修复前失败测试(TDD 证据) 新增 `R01A4RequestContextMigrationIntegrationTest`(`mydreamjob-server/src/test/java/com/mydreamjob/common/r01a4/`),真实 `SecurityFilterChain` + `MockMvc`。 **修复前实测(仅加入测试、未改生产代码):`Tests run: 9, Failures: 3`** | 失败测试 | 修复前实际 | |---|---| | `illegalPrincipals_areAlways401` | `path=/applications/1/feedback principal='anonymousUser'` → **`expected: 401 but was: 500`** | | `conversationEndpoint_illegalPrincipals_are401` | `principal='anonymousUser'` → **`expected: 401 but was: 500`** | | `errorBody_isSafe` | 同路径 → **500** | **修复后:`Tests run: 9, Failures: 0`** --- ## 4. 生产修改 8 个 Controller 统一构造器注入(均为 `private final`): ```java /** R01-A4:身份只来自安全上下文,不接受请求参数传入的 userId。 */ private final com.mydreamjob.security.CurrentUserAccessor currentUserAccessor; /** R01-A4:requestId 只来自过滤器规范化后的唯一产出点。 */ private final com.mydreamjob.common.logging.RequestIdAccessor requestIdAccessor; ``` | Controller | `requireUserId()` | `requestIdAccessor.get()` | |---|---|---| | CandidateFeedbackController | 4 | 4 | | ApplicationMatchController | 3 | 3 | | MatchConversationController | 1 | 1 | | MatchRuleController | 4 | 4 | | TalentDiscoveryController | 3 | 3 | | CareerProgressController | 1 | 1 | | MatchController | 1 | 1 | | TalentInvitationController | 6 | 6 | **共删除私有解析方法 16 个**(8 Controller × 2:6× `resolveUserId` + 6× `getRequestId` + 2× `userId()` + 2× `requestId()`),并移除随之不再使用的 `Authentication` / `SecurityContextHolder` import。 **未做**:未使用可选注入 / `@Nullable` / `ObjectProvider` / `new Accessor()`;未新增 userId 请求参数;未移动或删除角色检查与资源归属检查;未修改 Service 调用顺序、错误码或接口结构;未顺手重构紧凑代码。 --- ## 5. 静态门禁 任务书指定模式在 8 个文件中的命中: | Controller | 命中 | |---|---| | CandidateFeedbackController | **0** | | ApplicationMatchController | **0** | | MatchConversationController | **0** | | MatchRuleController | **0** | | TalentDiscoveryController | **0** | | CareerProgressController | **0** | | MatchController | **0** | | TalentInvitationController | **0** | **全部为 0,无业务参数解析误报。身份/requestId 私有解析残留为 0。** ### 全仓剩余统计 **身份解析**(`SecurityContextHolder.getContext().getAuthentication` \| `Long.(valueOf|parseLong)([^)]*getName()`) | | A3 后 | A4 后 | 预期 | |---|---|---|---| | 全仓文件 | 20 | **12** | 12 ✔ | | —— 基础设施 | 3 | **3**(`security/CurrentUserAccessor`、`auth/JwtAuthenticationFilter`、`deletion/DeletionGuardInterceptor`) | — | | —— **业务 Controller** | 17 | **9** | 9 ✔ | | —— 误报 | 0 | **0** | — | **requestId 解析**(`request.getHeader("X-Request-Id")` \| `request.getHeader(RequestIdFilter.REQUEST_ID_HEADER)` \| `String (getRequestId|resolveRequestId|requestId)(`) | | A3 后 | A4 后 | 预期 | |---|---|---|---| | 全仓文件(**本轮完整模式**) | 25 | **18** | 17 | | 全仓文件(**A3 口径**,不含 `REQUEST_ID_HEADER` 项) | 25 | **17** | 17 ✔ | | —— 基础设施(完整模式) | 4 | **5** | — | | —— **业务 Controller** | 21 | **13** | 13 ✔ | | —— Service / 非请求入口 | 0 | **0** | — | | —— 误报 | 0 | **0** | — | **数量差异的逐文件解释(不修改代码迁就数字)**:本轮模式比 A3 口径多含 `request.getHeader(RequestIdFilter.REQUEST_ID_HEADER)` 一项,该模式额外命中的**唯一文件**是 `common/logging/RequestIdAccessor.java`(第 35 行的兜底读取 `request.getHeader(RequestIdFilter.REQUEST_ID_HEADER)`)—— 这是 **Accessor 自身的合法实现**,属基础设施,非业务入口。因此:完整模式 18 = 基础设施 5 + 业务 13;A3 口径 17 = 基础设施 4 + 业务 13。**业务 Controller 数量与预期完全一致(13)**。 --- ## 6. 角色与副作用验证 ### 6.1 既有测试回归(角色/归属语义证据) | 命令 | 结果 | |---|---| | A4 关联回归(`*CandidateFeedback*Test,*ApplicationMatch*Test,*MatchConversation*Test,*MatchRule*Test,*TalentDiscovery*Test,*CareerProgress*Test,*MatchController*Test,*TalentInvitation*Test,*RequestContext*Test,*RequestId*Test`) | **163 / 163**,F=0 E=0 S=0,BUILD SUCCESS | 既有测试覆盖:反馈仅企业可写、匹配双方可见性、会话归属、匹配规则角色、人才发现 COMPANY 权限、职业进度 STUDENT 权限、邀约企业创建/学生决定/双方读取、其他企业与其他学生的 403/404。 ### 6.2 真实 HTTP 验证 只读实例 PID 7648,验证后已停止。 | 场景 | 结果 | |---|---| | 无 token / 畸形 Bearer / 纯数字 Bearer × 7 端点 | 全部 **401** ✔ | | STUDENT → `GET /companies/me/match-rule` | **403** ✔ | | COMPANY → `GET /companies/me/match-rule` | **403**(该账号无企业资料,既有权属语义)✔ | | STUDENT → `GET /profile/me/career-progress` | **200** ✔ | | COMPANY → `GET /profile/me/career-progress` | **200**(该账号注册时自带 STUDENT 角色,属既有语义)✔ | | STUDENT → `GET /profile/me/talent-discovery` | **200** ✔ | | ADMIN → `GET /profile/me/talent-discovery` | **403** ✔ | | STUDENT → `GET /profile/me/talent-invitations` | **200** ✔ | | COMPANY → `GET /companies/me/talent-invitations` | **403**(无企业资料)✔ | ### 6.3 副作用快照 上述**全部请求前后 22 张表逐行稳定排序的逐字段快照比对 → 全部零变化** ✔ - 覆盖表含 `candidate_feedback`、`application_match_records`、`application_match_events`、`conversations`、`conversation_members`、`company_match_rules`、`student_match_rules`、`talent_discovery_preferences`、`talent_invitations`、`notifications`、`messages`、`audit_logs` 等 - **未使用** COUNT/SUM 代替内容快照;采用显式列 + 稳定 `ORDER BY` 的逐字段比较 --- ## 7. 独立验证辅助测试(实施证据) 新增 `A4AccessorWiringProbeTest`(4 例),证明接线真实存在: | 用例 | 断言 | |---|---| | `accessorThrowsUnauthenticated_endpointsReturn401` | mock Accessor 抛认证异常 → 7 端点全部 **401**(若未接线则不可能) | | `serviceReceivesAccessorUserId` | mock 返回 `4242L` → `careerProgressService.getCareerProgress(4242L, …)`、`talentInvitationService.listCompany(4242L, …)` 收到该值 | | `requestIdComesFromAccessorBean` | mock 的 requestId 必须出现在响应体 | | `noToken_is401` | 无 Token → Security 链 401 | **删除 Accessor 调用会使本测试失败** ✔(属实施证据,不代替后续独立复验) --- ## 8. requestId 同源证据 | 输入 | 结果 | |---|---| | 合法值 `a4-legal-0001` | 响应头 == 响应体 `requestId`,原样保留 ✔ | | 空白 `" "` | 重新生成非空值,头体同源 ✔ | | 超长(80 字符) | 收敛至 ≤32,头体同源 ✔ | | 非法字符 ``、`a b` | 生成合法替代值;响应头不含 `<`/`>`/空格 ✔ | | request attribute 与 header 不同 | **attribute 优先** ✔ | | CR / LF / CRLF(直接经 Accessor) | 不进入规范化结果 ✔ | 覆盖 8 个端点(含 POST 端点)。 --- ## 9. 测试数量与 OpenAPI | 命令 | 结果 | |---|---| | A4 定向(`R01A4…,A4AccessorWiringProbeTest,CurrentUserAccessorTest,RequestIdAccessorTest,RequestIdFilterTest`) | **43 / 43**,F=0 E=0 **S=0** | | A4 关联回归 | **163 / 163**,F=0 E=0 **S=0** | | `mvn test` | **2238 / 2238**,Failures=0、Errors=0、**Skipped=0** | | `mvn clean verify` | **2238 / 2238**,Failures=0、Errors=0、**Skipped=0**,BUILD SUCCESS(≥ 2225 ✔) | | `python docs\verify_openapi.py` | **176 paths / 214 operations**,All checks passed | - 较上轮(2225)新增 **13** 个测试(9 + 4) - **`docs/openapi-rc.json` 零修改** ✔ - **V1–V45 零修改** ✔ - **未新增接口、表、索引、迁移或 ErrorCode** ✔ - **未发现新的基线测试污染**(上一轮的账号注销夹具隔离修复已生效)✔ --- ## 10. 精确修改文件 **生产代码(8 个,全部在任务书指定清单内)** ``` mydreamjob-server/src/main/java/com/mydreamjob/feedback/CandidateFeedbackController.java mydreamjob-server/src/main/java/com/mydreamjob/bidirectionalmatch/ApplicationMatchController.java mydreamjob-server/src/main/java/com/mydreamjob/bidirectionalmatch/MatchConversationController.java mydreamjob-server/src/main/java/com/mydreamjob/bidirectionalmatch/MatchRuleController.java mydreamjob-server/src/main/java/com/mydreamjob/talentdiscovery/TalentDiscoveryController.java mydreamjob-server/src/main/java/com/mydreamjob/careerprogress/CareerProgressController.java mydreamjob-server/src/main/java/com/mydreamjob/match/MatchController.java mydreamjob-server/src/main/java/com/mydreamjob/talentinvitation/TalentInvitationController.java ``` **测试(新增 2 个)** ``` mydreamjob-server/src/test/java/com/mydreamjob/common/r01a4/R01A4RequestContextMigrationIntegrationTest.java mydreamjob-server/src/test/java/com/mydreamjob/common/r01a4/A4AccessorWiringProbeTest.java ``` **文档(1 个)** ``` docs/backend-development-roadmap.md # 仅更新顶部 R01 状态行,历史章节未动 ``` **未修改**:Service、DTO、`ErrorCode`、Flyway、`SecurityConfig`、Filter 顺序、OpenAPI、既有测试、`UserRoleGuard`、"匿名 401 缺 requestId"观察项。 --- ## 11. Git 状态 ``` 分支:refactor/r01-structure-performance HEAD:35baa8416d31e71dd2c2556877f9d2e1c93b14a1(未变) 暂存区:空 git add -A / commit / push:均未执行 Docker:未操作 历史未跟踪文件:247 个完整保留 ``` 验收临时用户(a4vstu / a4vcom)已按精确主键清理,`users` 残留 0;`audit_logs` 4 行 append-only 保留并登记。 --- ## 12. 明确声明 - **未暂存、未提交、未推送。** - 未进入 `UserRoleGuard`;未处理"匿名 401 缺 requestId"观察项。 - 未使用 `@Autowired(required=false)`、`@Nullable`、`ObjectProvider` 或 `new Accessor()` 回退;测试切片使用真实 `SecurityFilterChain` 与真实 Accessor Bean。 - 全仓剩余身份/requestId 数量已**逐文件分类解释**,未修改代码迁就数字。 --- > **R01-A4第四批请求身份与requestId收束完成,待独立代码复验。**