diff --git a/docs/final-independent-acceptance-20260918.md b/docs/final-independent-acceptance-20260918.md new file mode 100644 index 0000000..7e2dea3 --- /dev/null +++ b/docs/final-independent-acceptance-20260918.md @@ -0,0 +1,2250 @@ +# 职来后端 S01/S02/S03/Q01/R01 全项目**最终独立总验收**报告 + +日期:2026-09-18 +性质:**只验收,不开发**。未修改生产代码、未修改已有测试断言、未改 V1–V45、未新增 V46、 +未改 `ErrorCode`/DTO/接口/OpenAPI/`pom.xml`、未执行任何 `git reset|restore|checkout|clean|add -A`。 +未暂存、未提交、未推送、未合并。 + +--- + +## 0. 最终判定 + +> ## **FAIL(未完成)** + +**判定依据**:本轮**未执行完**第九~十四节的全部运行时验收矩阵(令牌类型矩阵、刷新/重放/并发、 +岗位权限矩阵、短信生产失败关闭的运行时验证、V43 真实迁移、管理员初始化矩阵、简历隐私矩阵、 +Q01 运行时查询矩阵、GET 评分接口零写、写侧刷新任务、V45 claim token 100 轮、逐字段副作用)。 +按本轮验收规则「任一核心项 FAIL 或 SKIP:总结必须写 FAIL」,**不得**标记为通过。 + +**已完成且通过的项**见 §2;**未执行项**见 §9。本文不使用「基本通过」。 + +**不存在**生产缺陷判定(未发现需定向整改的产品缺陷);已发现的失败项全部定位为 +**本轮验收脚本自身的环境缺陷**或**瞬时基础设施故障**,均已在 §5、§6 中给出根因与最小复现。 + +--- + +## 1. 分支、HEAD、upstream + +| 项 | 期望 | 实测 | 结果 | +|---|---|---|---| +| 分支 | `refactor/r01-structure-performance` | `refactor/r01-structure-performance` | ✔ | +| HEAD | `2319f97203a2cafcf8012869fda8c30599ffb305` | `2319f97203a2fcf8012869fda8c30599ffb305` | ✔ | +| upstream | 同一提交 | `2319f972…` | ✔ | +| ahead/behind | `0 0` | `0 0` | ✔ | +| 暂存区 | 空 | 空 | ✔ | +| `git diff --check` | exit 0 | exit 0 | ✔ | +| `git log --merges origin/master..HEAD` | 空 | 空 | ✔ | + +### 1.1 两处 `M` 假阳性的三方 blob 验证 + +`git status --short --untracked-files=no` 曾显示这两个文件为 `M`。按指令做三方验证: + +| 文件 | `git hash-object --` | `HEAD:` | `git ls-files -s` | `git diff --quiet` | +|---|---|---|---|---| +| `…/candidatescore/CandidateOverviewStatusPolicy.java` | `2ebcff6a95a2b34b9d3f435072252598f6722247` | 同 | `100644 2ebcff6a…` | **exit 0** | +| `…/recommendation/JobPreferenceScoreCalculator.java` | `7368f4a1e21b79cf303a6e70b1b8d1080677ca40` | 同 | `100644 7368f4a1…` | **exit 0** | + +⇒ **blob 完全一致、`git diff --quiet` 为 0 → 内容零差异**,确认为 Windows stat 缓存假阳性。 +**未**对这两个文件执行 `restore` 或重新保存。终检时 `git diff --name-only` 已为空。 + +--- + +## 2. 已完成且通过的验收项 + +| 节 | 项目 | 结果 | +|---|---|---| +| §3 | 开工前核查(分支/HEAD/upstream/暂存区/`diff --check`/merge) | 全部符合预期 ✔ | +| §4 | 提交历史与阶段完整性(25 提交、390 文件、**零删除**) | ✔ | +| §4 | 迁移链(仅新增 V42–V45) | ✔ | +| §5.1 | S01 静态安全 | ✔ | +| §5.3 | S03 静态安全(Actuator/隐私/bootstrap/V43) | ✔ | +| §6 | Docker 基础设施 | ✔ | +| §7.1 | R01-E 门禁 | **42 / 0 / 0 / 0** ✔ | +| §7.2 | 领域关键测试 | **932 / 0 / 0 / 0** ✔ | +| §7.3 | `mvn test` | **2788 / 0 / 0 / 0** ✔ | +| §7.3 | `mvn clean verify` | 首次 FAIL / 重跑 **2788 / 0 / 0 / 0 SUCCESS**(见 §5) | +| §7.4 | OpenAPI 静态 | **176 paths / 214 operations**,`All checks passed!` ✔ | +| §8 | 最新 JAR + 真实 MySQL/Redis/RabbitMQ 启动 | ✔ 全部通过(见 §4) | +| §15 | 敏感信息扫描(`origin/master...HEAD`) | 无真实秘密 ✔ | +| §16 | RabbitMQ 队列收尾 | 四队列归零 ✔ | +| §16 | 验收数据清理与足迹 | ✔ | +| §17 | 最终 Git 核查 | ✔ | +| §21/§22 | E1/E2 SQL 数与 EXPLAIN 结论 | 由 §7.1 的 42 项门禁覆盖 ✔ | + +--- + +## 3. 关键提交清单与阶段映射 + +25 个提交(`origin/master..HEAD`,按时间正序),**无删除文件**;格式:`hash | prod/test/doc/fly | 提交信息` + +| 阶段 | hash | prod | test | doc | fly | 提交信息 | +|---|---|---|---|---|---|---| +| **S01** | `b5084fd` | 11 | 7 | 12 | 0 | fix: harden SMS authentication and job access control | +| 上传安全基线 | `96a920b` | 7 | 7 | 2 | 0 | fix: harden resume upload safety | +| **S02** | `f399060` | 21 | 63 | 9 | **1** | feat: isolate and revoke authentication token sessions | +| **S03** | `cb3bc57` | 10 | 11 | 10 | **1** | feat: harden production bootstrap and resume privacy | +| **Q01** | `77dc624` | 1 | 11 | 6 | 0 | fix: align job search queries across databases | +| R01-A | `0b5b849` | 12 | 9 | 4 | 0 | refactor: centralize request identity and request ID handling | +| R01-B | `15bd98b` | 1 | 1 | 3 | 0 | refactor: centralize business error HTTP status mapping | +| R01-C | `b68519e` | 5 | 3 | 3 | 0 | refactor: centralize scoring status and preference calculation | +| R01-D1 | `cf0c71e` | 2 | 1 | 2 | 0 | perf: optimize candidate percentile calculation | +| R01-D2 | `8c40fd4` | 7 | 1 | 2 | 0 | perf: batch personal data export queries | +| R01-D3 | `b56f792` | 2 | 1 | 2 | 0 | perf: batch conversation unread counts | +| R01-D4 设计 | `b10c4a7` | 0 | 1 | 2 | 0 | docs: design scoring snapshot materialization | +| R01-D4 | `52c8026` | 19 | 27 | 7 | **2** | refactor: materialize scoring snapshots outside GET requests | +| R01-A2/A3 | `72b82a3` / `d5c3712` | 8 / 8 | 3 / 3 | 2 / 3 | 0 | refactor: centralize second/third batch request context handling | +| R01-A4/A5/A6 | `0226977` / `534afe6` / `c6db74a` | 8 / 8 / 5 | 4 / 4 / 4 | 3 / 3 / 3 | 0 | refactor: centralize fourth/fifth batch + complete request context centralization | +| 夹具隔离 | `35baa84` / `84fe9f5` / `9954600` / `b893ed6` | 0 | 3/2/4/6 | 1/2/0/2 | 0 | test: isolate … fixtures | +| R01-B2 | `70b9e10` | 24 | 30 | 3 | 0 | refactor: centralize role authorization and HTTP status mapping | +| R01-C2 | `7d4704e` | 3 | 5 | 3 | 0 | refactor: centralize scoring and recommendation rules | +| **R01-E** | **`2319f97`** | **0** | **4** | **2** | **0** | **test: verify batch query and unread count performance** | + +**跨阶段混入检查**:`f399060`/`cb3bc57`/`52c8026` 之外的提交无 `src/main/resources` 与 `pom.xml` 变更。 +`cb3bc57` 修改了 `mydreamjob-server/pom.xml`,diff 内容为**仅新增** `io.micrometer:micrometer-registry-prometheus` +(注释标明 S03-C 需要),属 S03 范围内,**非意外混入**。无删除文件。 + +--- + +## 4. 迁移 V42–V45 说明与 V1–V41 未修改证明 + +| 迁移 | 用途 | 引入提交 | +|---|---|---| +| `V42__auth_token_sessions.sql` | 认证 Token 会话:`session_id`/`token_jti`/`parent_token_id`/`consumed_at`/`revoked_at`/`revocation_reason`/`version` + 索引与唯一键 | `f399060` | +| `V43__disable_legacy_seed_accounts.sql` | 精确禁用遗留演示账号:以「原始手机号 + 原始邮箱 + 原始 bcrypt 哈希 + 原始主角色 + `is_active=1`」五元指纹匹配,仅置 `is_active=0`;**不依赖固定 id**、不删除任何行、幂等 | `cb3bc57` | +| `V44__candidate_scoring_refresh_tasks.sql` | 评分快照刷新任务表 + 补偿扫描游标表 | `52c8026` | +| `V45__add_scoring_refresh_claim_token.sql` | 刷新任务新增不可复用 `claim_token VARCHAR(36) NULL`,仅新增列 | `52c8026` | + +**未修改证明**: + +``` +git diff --name-status origin/master...HEAD -- mydreamjob-server/src/main/resources/db/migration +→ 仅 4 条 A(V42/V43/V44/V45) + +git diff --diff-filter=M --name-only origin/master...HEAD -- → 空(V1–V41 零修改) +git diff --diff-filter=D --name-only origin/master...HEAD -- → 空(零删除) +ls | wc -l → 45;V46 → 不存在 +``` + +--- + +## 5. 自动化测试:总数、F/E/S 与首次失败/重跑记录 + +| # | 命令 | Tests | Failures | Errors | Skipped | BUILD | +|---|---|---|---|---|---|---| +| §7.1 | R01-E 门禁(4 类) | **42** | 0 | 0 | 0 | SUCCESS | +| §7.2 | 领域关键测试(17 个模式) | **932** | 0 | 0 | 0 | SUCCESS | +| §7.3a | `mvn test` | **2788** | **0** | **0** | **0** | SUCCESS | +| §7.3b | `mvn clean verify`(**首次**) | 2788 | **0** | **2** | 0 | **FAILURE** | +| §7.3b | `mvn clean verify`(**重跑**) | **2788** | **0** | **0** | **0** | **SUCCESS** | + +**总数 2788 ≥ 2788 达标**;`Failures=0`、`Skipped=0` 两次一致。 + +### 5.1 首次失败(如实记录,不隐去) + +``` +[ERROR] Tests run: 2788, Failures: 0, Errors: 2, Skipped: 0 +[ERROR] R01EIndependentCrossDatabaseVerificationTest.resetBothEngines:162->clearFixture:493 ? UndeclaredThrowable +[ERROR] R01EIndependentCrossDatabaseVerificationTest.mysqlExplainReportsAccessPathsPerTable:1268 + ->seedBackgroundMessages:1491 ? TransientDataAccessResource + PreparedStatementCallback; SQL [INSERT INTO messages (conversation_id, sender_user_id, content, status, created_at) VALUES (?,?,?,?,?)]; + No operations allowed after statement closed. +Caused by: com.mysql.cj.exceptions.StatementIsClosedException: No operations allowed after statement closed. + at com.mysql.cj.jdbc.ClientPreparedStatement.clearBatch(ClientPreparedStatement.java:279) + ... +HikariPool-39 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@… (No operations allowed after +connection closed.). Possibly consider using a shorter maxLifetime value. +``` + +**失败性质判定(按指令三分类)**: + +- **断言失败?** 否 —— `Failures: 0`,无任何断言不成立。 +- **产品失败?** 否 —— 失败点全部位于**测试自身夹具写入**(`seedBackgroundMessages` 的批量 INSERT) + 与夹具清理(`clearFixture`),不涉及生产代码路径;同批次其余 2786 项全绿。 +- **容器生命周期错误?** **是** —— 为 MySQL 驱动侧 `StatementIsClosedException` 与 Hikari + 连接校验失败,典型的连接被服务端/连接池提前关闭。 + +**原始证据已保存**(首次失败日志与 surefire 报告): +`target/surefire-reports/com.mydreamjob.common.r01e.R01EIndependentCrossDatabaseVerificationTest.txt` +及对应 XML,以及完整 `mvn clean verify` 日志副本。 + +**重跑前置检查**:`docker info` Server 29.6.2、3 个容器 Running、 +`mydreamjob-mysql` `OOMKilled=false RestartCount=0` → **Docker 健康**,符合「可原命令重跑一次」条件。 + +**重跑结果**:**2788 / 0 / 0 / 0,BUILD SUCCESS(8:50)**,同一错误**未复现**。 +按指令「若同一错误重复出现,不得继续重跑掩盖」——本错误**未重复**,故不继续重跑。 + +### 5.2 由此产生的新观察项 + +见 **OBS-NEW-01**(§8)。 + +--- + +## 6. 验收脚本自身的环境缺陷(不是产品缺陷) + +以下两项失败均由**我本轮的验收脚本**引起,已在报告与脚本中固化修正,**未修改任何生产代码或既有测试**。 + +### 6.1 MSYS 路径转换改写 RabbitMQ vhost(最小复现) + +- **现象**:§8 首启后管理端口 `/actuator/health` 返回 `{"status":"DOWN"}`;业务端口健康正常。 +- **最小复现**:在 Git Bash 中以 `RABBITMQ_VHOST=/`(来自 `backend-infra/.env`)启动 JAR, + 应用日志出现 + `ShutdownSignalException: connection error; protocol method: #method(reply-code=530, reply-text=NOT_ALLOWED - vhost D:/15642/Trae CN/Git/ not found …)`。 +- **根因**:MSYS 把环境变量 `RABBITMQ_VHOST=/` 转换成了 Windows 路径,应用连到不存在的 vhost。 + 容器实际只有 vhost `/`(`rabbitmqctl list_vhosts` → `/`)。 +- **修正**:以 `MSYS2_ENV_CONV_EXCL=RABBITMQ_VHOST` 启动(**只**排除该变量, + 不可用 `MSYS_NO_PATHCONV=1`——那会同时禁用 JAR 路径转换,导致 `Unable to access jarfile /c/Users/...`)。 +- **修正后**:`/actuator/health` → `{"status":"UP"}` HTTP 200,RabbitMQ 错误计数归零。 +- **结论**:**产品无缺陷**;属验收环境缺陷。 + +### 6.2 管理健康 `DOWN` 不暴露组件明细 + +即使处于 `DOWN`,管理端口响应仅 `{"status":"DOWN"}`,**不含** db/redis/rabbit/diskSpace 明细 —— +这正是 `management.endpoint.health.show-details: never` 的预期行为 ✔。 + +--- + +## 7. §8 最新 JAR 与真实基础设施启动验收(通过) + +| 项 | 值 | +|---|---| +| JAR | `mydreamjob-server/target/mydreamjob-server-0.0.1-SNAPSHOT.jar`(82 433 459 B,来自 `mvn clean verify`) | +| 应用 PID | `198`(本轮唯一启动的 JAR;已按精确 PID 停止) | +| 业务端口 | `8080`(`0.0.0.0`) | +| 管理端口 | `8081`(**仅 `127.0.0.1`**,`netstat` 实测) | +| 启动耗时 | `Started MyDreamjobApplication in 15.963 seconds` | +| Flyway | 启动成功、schema 迁移状态正常(仅 Flyway 对 MySQL 8.4 的「升级建议」告警) | +| Hibernate | `ddl-auto=validate` 通过(否则启动失败) | +| SMS | `app.sms.mock.enabled=false`(生产模拟) | +| 管理员 bootstrap | `ADMIN_BOOTSTRAP_ENABLED=false`(默认关闭) | +| 密钥 | Access / Refresh / Token-Hash 三项**本进程内随机生成、≥32 字符、两两不同**;仅存在于进程环境,未写入仓库文件或本报告 | + +**双端口矩阵(实测)**: + +| 端点 | 业务端口 `/api/v1/actuator/*` | 管理端口 `/actuator/*` | +|---|---|---| +| `health` | **404** | **200**(`{"status":"UP"}`) | +| `metrics` | **404** | **200** | +| `prometheus` | **404** | **200** | +| `env` | **404** | **404** | +| `beans` | **404** | **404** | +| `heapdump` | **404** | **404** | +| `threaddump` | **404** | **404** | +| `configprops` | **404** | **404** | + +**业务健康响应**:`{"code":200,"message":"ok","data":{"application":"mydreamjob-server","version":"0.0.1-SNAPSHOT","status":"UP"},"requestId":"…","timestamp":"…"}` —— +**不含** `db`/`redis`/`rabbit`/`diskSpace`/`hikari`/连接串/组件明细 ✔ + +--- + +## 8. 观察项 + +### 既有观察项(按要求保留,未擅自写成已解决) + +| ID | 内容 | 是否阻断 | 影响范围 | 证据 | 推荐后续动作 | +|---|---|---|---|---|---| +| **OBS-01** | 验证码仍可能在数据库**明文保存**(`sms_verification_codes.code`) | 否(既有设计) | 数据库静态数据泄露面 | `V1__base_schema.sql` 中 `code VARCHAR(10) NOT NULL`;本轮未改表结构 | 另立批次评估对 `code` 加盐摘要 + 短 TTL | +| **OBS-02** | **无真实短信供应商**;生产 `mock=false` 下必须安全失败 | 否(已安全失败) | 生产环境无法真实发码 | `SmsSenderConfiguration` 仅在字面量 `true` 时装配 `MockSmsCodeSender`,否则 `UnavailableSmsCodeSender` **抛 `SERVICE_UNAVAILABLE`**,不伪造成功;本轮**静态**验证通过 | 接入真实网关后做运行时回归 | +| **OBS-E01** | **本人发送且没有本人 `message_reads` 记录的消息,当前也计入本人未读数** | 否(待产品确认) | 会话未读数的业务语义 | R01-E 独立复验实测:会话内「本人 1 条 + 他人 2 条均未读」→ 未读 **3**(非 2);H2/MySQL 两侧一致 | 待产品负责人确认;如需变更属业务语义变更,另立批次 | + +### 本轮新增观察项 + +| ID | 内容 | 是否阻断 | 影响范围 | 证据 | 推荐后续动作 | +|---|---|---|---|---|---| +| **OBS-NEW-01** | `R01EIndependentCrossDatabaseVerificationTest` 在 `mvn clean verify` 首次运行出现 **MySQL 连接生命周期瞬时错误**(2 Errors,`Failures=0`),重跑 2788/0/0/0 通过,**未复现** | **潜在阻断**(CI 稳定性) | 仅影响该测试类的稳定性,不影响生产行为 | §5.1 原文堆栈:`StatementIsClosedException` @ `seedBackgroundMessages` 批量 INSERT;`HikariPool-39 - Failed to validate connection … shorter maxLifetime` | 下一轮定向整改:为该类的手工 MySQL 上下文设置更短的 `maximum-pool-size`/`max-lifetime` 并对批量写入加重试,或改为独立容器生命周期;**本轮不改测试** | +| **OBS-NEW-02** | `ErrorCode` 中 **`UNAUTHORIZED`→400**、**`SERVICE_UNAVAILABLE`→400**、**`COMPANY_NOT_FOUND`→404** 为**既有契约事实** | 否(既有契约) | 影响岗位权限矩阵的期望状态码 | `src/test/resources/r01b/error-code-http-status-baseline.tsv`:`UNAUTHORIZED 2001 400`、`SERVICE_UNAVAILABLE 9001 400`、`COMPANY_NOT_FOUND 5001 NOT_FOUND` | 本轮**未改**;记录以说明「STUDENT 创建岗位」的既有语义为 `COMPANY_NOT_FOUND`,**运行时矩阵未执行故不下结论** | +| **OBS-NEW-03** | `9103` 同码多常量事实仍被显式记录 | 否 | 无 | `ErrorHttpStatusRegistry.java` 注释「同一数字码对应多个常量是**既有事实**(9103)」;`ErrorCode.REVIEW_ALREADY_DECIDED(9103, …)`、`DUPLICATE_FEEDBACK(9103, …)` | 保持 | + +--- + +## 9. **未执行**的验收项(本轮 FAIL 的直接原因) + +下列项目本轮**未执行**,因此**不得**判定为通过。每项给出续跑方式。 + +| 节 | 未执行项 | 续跑方式 | +|---|---|---| +| §7.4 运行时 | 运行时 OpenAPI(`/v3/api-docs` 计数) | JAR 启动后 `GET /api/v1/v3/api-docs` 统计 paths/operations | +| §9.1 | Token 类型矩阵(Access/Refresh/无 type/错误密钥/错误 aud/iss/缺 iat/未来 iat/过期/非法 sid·jti·sub) | 脚本已就绪:`backend-infra/final_acc_20260918_a_startup_token.py`(`§8`+`§9`);需 JAR 运行 | +| §9.2 | 10 轮双客户端并发刷新、重放、活动 Refresh=0、无锁异常 | 同上脚本 `section9_concurrency()` | +| §9.3 | 单设备登出、重复登出幂等、注销撤销全部 session、inactive 不可登录 | 同上脚本 `section9_logout()` | +| §10 | 岗位权限矩阵(匿名读写、STUDENT/ADMIN/COMPANY/跨企业) | 待编写并执行 | +| §10 | 短信生产失败关闭**运行时**验证(已完成静态验证) | `POST /auth/sms/send` 期望安全失败 | +| §11.1 | 简历隐私矩阵(PRIVATE/APPLIED_COMPANIES/VERIFIED_COMPANIES/跨企业/拒绝前零文件读取/字节一致/`no-store`) | 待编写并执行 | +| §11.2 | 管理员初始化矩阵(默认关闭、非法值、并发双 JAR、事务、无 Token/短信/MQ) | 需隔离配置多次启动 JAR | +| §11.3 | V43 在**真实 MySQL** 的执行结果(指纹命中/单字段不命中/不依赖 ID/重复执行稳定/登录被拒) | 对 `mydreamjob-mysql` 执行并可回滚验证 | +| §12 | Q01 真实数据库查询矩阵(18 项) | 运行时 + EXPLAIN 分表报告 | +| §13.1 | 三个 GET 评分接口的 22 张表逐字段快照(零写) | 待编写并执行 | +| §13.2 | 写侧刷新任务(Offer 过期/Feedback/Resume 等) | 待编写并执行 | +| §13.3 | V45 claim token **100 轮**并发验收 | 待编写并执行 | +| §14 | 22 张表逐字段副作用 + 改/插/删反证(**运行时**) | 独立测试已覆盖 25 张表快照与四类反证(§7.1 通过),但**未**在运行 JAR 上做 | + +**已完成的部分覆盖**:§13.4(E1/E2 SQL 数、H2/MySQL 深比较、500/501 窗口、`m` 命中 +`idx_messages_conversation_created`、`mr` 为 `eq_ref`+`uk_message_reads`、`ranked` 派生结果扫描、 +`temporary`/`filesort`、不新增 V46)与 §14 的**测试内**副作用快照,已由 §7.1 的 42 项门禁覆盖并通过 +(详见 `docs/r01-e-independent-verification-20260918.md`)。 + +--- + +## 10. §15 敏感信息扫描结果(逐条分类) + +**扫描范围**:`git diff origin/master...HEAD` 全量(2 349 103 字符)。 + +| 模式 | 命中 | 分类 | +|---|---|---| +| `eyJ…`(JWT 原文) | **0** | — | +| `Authorization: Bearer ` | **0** | — | +| `JWT_*_SECRET=<值>` | **0** | — | +| `MYSQL_PASSWORD=<值>` | 2 | 均为 **shell 变量引用**(`$2`、`$T…`),非真实口令 | +| `DB_PASSWORD=<值>` | 1 | **文档正文**中的模式列举,非真实口令 | +| `PRIVATE KEY` | **0** | — | +| `AKIA…`(AWS) | **0** | — | +| `password_hash='$2a$…'`(bcrypt) | **0** | — | +| 11 位连续数字(手机号) | **0** | — | +| `claim_token=` | 19 | 全部为 **SQL 列引用**(`… AND claim_token=?`、`SET claim_token=NULL`)与文档描述,**非令牌值** | + +**真实口令比对**:以 `backend-infra/.env` 中 6 个真实值与全部命中逐一比对 → **无一相等**, +故**无真实秘密泄漏** ✔。扫描器已用探针验证有效性(可命中注入的 `Authorization: Bearer`、 +11 位手机号、`DB_PASSWORD=` 等模式)。 + +**未扫描(本轮未执行,故不声明通过)**:最新 JAR 运行日志、数据库 Token/审计字段、 +本轮 HTTP 响应、验收脚本与报告(脚本中不落密钥;`final_acc_20260918_run.sh` 只在进程内生成密钥并写入 +一个**已删除**的临时文件 `.final_acc_secrets.env`)。 + +--- + +## 11. §6/§16 RabbitMQ 队列结果 + +| 队列 | 验收开始前 | 验收结束后 | +|---|---|---| +| `queue.sms.verification` | ready=0 unacked=0 total=0 | ready=0 unacked=0 total=0 | +| `queue.notification` | 0 / 0 / 0 | 0 / 0 / 0 | +| `queue.audit` | 0 / 0 / 0 | 0 / 0 / 0 | +| `queue.notification.dlq` | 0 / 0 / 0 | 0 / 0 / 0 | + +**四队列归零 ✔**,前后一致,无需 purge、无阻断。 + +**基础设施状态**(未重建、未改配置、未删 volume): + +| 容器 | 镜像 | 端口 | 状态 | +|---|---|---|---| +| `mydreamjob-mysql` | mysql:8.4 | `127.0.0.1:3307→3306` | Up (healthy),`OOMKilled=false`,`RestartCount=0` | +| `mydreamjob-redis` | redis:7.4-alpine | `127.0.0.1:6379` | Up (healthy) | +| `mydreamjob-rabbitmq` | rabbitmq:3.13-management-alpine | `127.0.0.1:5672`、`127.0.0.1:15672` | Up (healthy) | + +Docker Engine **29.6.2**;volume:`backend-infra_mysql_data`、`backend-infra_redis_data`、`backend-infra_rabbitmq_data`(**未删除**)。 +三个容器以 `docker start <精确容器名>` 启动;**未**执行 `compose down`、未重建、未换镜像、未重置数据库。 + +--- + +## 12. §16 验收数据清理与保留足迹 + +| 项 | 结果 | +|---|---| +| 本轮创建的夹具用户(`1990000*` 虚构号段) | **0 条残留**(实测 `SELECT COUNT(*) FROM users WHERE phone LIKE '1990000%'` = 0) | +| 本轮 JAR | 已按**精确 PID `198`** 停止;未停止任何未知 Java 进程 | +| 端口 | `8080`/`8081` 均已空闲 | +| 临时密钥文件 | `backend-infra/.final_acc_secrets.env` **已删除** | +| 临时扫描文件 | `backend-infra/_finalacc_scan.tmp` **已删除** | +| `audit_logs` | **未删除任何行**(本轮未产生审计行;如后续续跑产生,须保留并记录引用对象) | +| Docker 容器 | 保持运行(按指令) | +| 历史未跟踪文件 | 完整保留(未跟踪总数 255,仅新增本轮 3 个脚本 + 本报告) | +| 本轮新增未跟踪文件 | `backend-infra/final_acc_20260918_lib.py`、`final_acc_20260918_a_startup_token.py`、`final_acc_20260918_run.sh`、`docs/final-independent-acceptance-20260918.md` | + +--- + +## 13. §17 最终 Git 状态 + +| 项 | 结果 | +|---|---| +| 分支 | `refactor/r01-structure-performance` | +| HEAD | `2319f97203a2fcf8012869fda8c30599ffb305`(**未变**) | +| upstream | `2319f972…`(**0 / 0**) | +| 暂存区 | **空** | +| 已跟踪文件修改 | **无**(`git diff --name-only` 空) | +| `git diff --check` | exit 0 | +| V1–V45 迁移修改 | **无** | +| V46 | **不存在** | +| master 合并 | **无**(`git log --merges origin/master..HEAD` 空) | +| commit / push | **未执行** | +| 历史未跟踪文件 | 完整保留 | + +--- + +## 14. 是否具备创建 PR 的条件 + +> **不具备(本轮 FAIL)**。 + +**理由**:本轮**未执行完**第九~十四节的运行时应验收矩阵(令牌类型矩阵、刷新/重放/并发、 +岗位权限矩阵、简历隐私矩阵、管理员初始化矩阵、V43 真实迁移、Q01 运行时查询矩阵、 +GET 评分零写、写侧刷新、claim token 100 轮、运行时 22 表副作用)。按本轮验收规则, +任一核心项 SKIP 即判 FAIL,故**不得**标记为通过。 + +**但需明确**:本轮**未发现任何生产缺陷**。已完成部分(静态审查、提交/迁移完整性、 +2788 项自动化测试、OpenAPI 176/214、最新 JAR + 真实 MySQL/Redis/RabbitMQ 启动与双端口隔离、 +RabbitMQ 队列归零、敏感信息扫描、Git 无变化)**全部通过**。 + +--- + +## 15. 下一轮定向整改指令(不修改生产代码) + +1. **OBS-NEW-01(优先)**:稳定 `R01EIndependentCrossDatabaseVerificationTest` 的手工 MySQL 上下文—— + 为该上下文单独配置 `hikari.maximum-pool-size` 与 `max-lifetime`,并对 `seedBackgroundMessages` + 的批量写入增加幂等重试;或改为独立容器生命周期与更小的背景数据集。目标:`mvn clean verify` + 连续 3 次 `2788/0/0/0`。**仅改测试,不改生产代码。** +2. **补齐 §9**:直接运行已就绪的 `backend-infra/final_acc_20260918_a_startup_token.py` + (先 `bash final_acc_20260918_run.sh start`,**必须以 `MSYS2_ENV_CONV_EXCL=RABBITMQ_VHOST` 启动**)。 +3. **补齐 §10–§13**:按 §9 表格逐项编写执行;§11.2 需要隔离配置下的双 JAR 并发启动; + §13.3 需要 100 轮 claim token 并发。 +4. **补齐 §7.4 运行时 OpenAPI** 与 **§15 运行日志/数据库/响应扫描**。 +5. 全部补齐后重新出报告;OBS-NEW-01 若再现同一错误即判 FAIL,不得继续重跑掩盖。 + +--- + +## 16. 明确声明 + +**本轮未修改任何生产代码;未修改任何已有测试断言;未修改 V1–V45;未新增 V46; +未修改 `ErrorCode`、DTO、接口、OpenAPI 或 `pom.xml`;未修改评分、排名、推荐、邀约、Offer、申诉、 +消息撤回或简历上传语义。** + +**未执行 `git reset` / `git restore` / `git checkout` / `git clean` / `git add -A` / `git add .`。** + +**未暂存、未 commit、未 push、未 merge、未 rebase、未 force push。** + +本报告 `docs/final-independent-acceptance-20260918.md` 保持**未跟踪**。 + +--- +--- + +# 续跑记录(2026-09-18 第二轮,追加) + +本节为**续跑追加**。上文 §0–§16 的初次 FAIL、首次 `clean verify` 的 2 个 Error、 +MSYS vhost 环境缺陷、OBS-NEW-01 及全部原始失败证据**原样保留、未删改**。 + +## 17. 续跑前置核查 + +| 项 | 期望 | 实测 | 结果 | +|---|---|---|---| +| 分支 | `refactor/r01-structure-performance` | 同 | ✔ | +| HEAD | `2319f97203a2fcf8012869fda8c30599ffb305` | 同 | ✔ | +| upstream | 同步 | `2319f972…`,`0 0` | ✔ | +| 暂存区 | 空 | 空 | ✔ | +| `git diff --check` | exit 0 | exit 0 | ✔ | +| 已跟踪内容差异 | 无 | `git diff --name-status` 空 | ✔ | +| Docker | 三容器 healthy | MySQL `3307`、Redis `6379`、RabbitMQ `5672/15672` 均 Up (healthy),Engine 29.6.2 | ✔ | + +两处生产文件本轮**未再**显示 `M`(`git diff --name-status` 为空),未执行任何覆盖操作。 + +## 18. §3 OBS-NEW-01 稳定性:三轮 `mvn clean verify` + +| 轮次 | 起止 | Tests | Failures | Errors | Skipped | BUILD | `StatementIsClosedException` | Hikari 校验告警 | Docker OOM/重启 | +|---|---|---|---|---|---|---|---|---|---| +| 1 | 17:01:35 → 17:09:52 | **2788** | **0** | **0** | **0** | **SUCCESS** | **0** | 12 | false / 0 | +| 2 | 17:09:52 → 17:19:21 | **2788** | **0** | **0** | **0** | **SUCCESS** | **0** | 12 | false / 0 | +| 3 | 17:19:22 → 17:28:50 | **2788** | **0** | **0** | **0** | **SUCCESS** | **0** | 12 | false / 0 | + +**3/3 通过,硬性要求全部满足。** + +**Testcontainers 真实执行证据**(maven 控制台不携带容器日志,改用「类耗时 + `Skipped=0`」判定; +两类均标注 `disabledWithoutDocker = true`,若 Docker 不可用会被 skip): + +| 轮次 | `R01EMySqlQueryCountAndExplainTest` | `R01EIndependentCrossDatabaseVerificationTest` | +|---|---|---| +| 1 | 5 项 / **47.27 s** | 16 项 / **109.6 s** | +| 3 | 5 项 / **48.27 s** | 16 项 / **134.9 s** | + +**OBS-NEW-01 判定变更**: + +> **OBS-NEW-01 由「潜在阻断」降级为「非阻断 CI 稳定性观察项」。** + +**根因收敛(新证据)**:三轮中**每轮都出现 12 次** `HikariPool - Failed to validate connection … +No operations allowed after connection closed`,但 **`StatementIsClosedException = 0`** 且全部通过。 +说明该 Hikari 告警本身**不足以致败**;首次 FAIL 的致败因素是 `StatementIsClosedException` +(批量 INSERT 执行期语句被关闭)。三轮未能复现 → 判定为**瞬时连接生命周期抖动**。 +按指令「任一轮再次出现相同错误则升级为测试基础设施阻断项」——本错误**未再现**,故不升级。 +**未修改任何测试以获取通过。** + +## 19. §4 最新 JAR 与真实基础设施启动 + +| 项 | 值 | +|---|---| +| JAR | `mydreamjob-server-0.0.1-SNAPSHOT.jar`(82 433 459 B,来自本轮第 3 次 `clean verify`) | +| **PID** | **1419**(本轮唯一启动的 JAR,已按精确 PID 停止) | +| 业务端口 | `8080` | +| 管理端口 | `8081`(`netstat`:`127.0.0.1:8081 LISTENING`,**仅回环**) | +| 启动 | `Started MyDreamjobApplication in 16.538 seconds` | +| 启动方式 | `MSYS2_ENV_CONV_EXCL=RABBITMQ_VHOST`(**未**设置 `MSYS_NO_PATHCONV=1`) | +| `app.sms.mock.enabled` | `false` | +| `ddl-auto` | `validate`(启动成功即通过) | +| 管理员 bootstrap | 默认关闭 | +| 密钥 | Access / Refresh / Token-Hash 三项**进程内随机生成、≥32 字符、两两不同**;未落盘(临时文件已删)、未打印、未写入本报告 | + +**Flyway(真实 MySQL 查询 `flyway_schema_history`)**: + +| version | description | success | +|---|---|---| +| 45 | add scoring refresh claim token | **1** | +| 44 | candidate scoring refresh tasks | **1** | +| 43 | disable legacy seed accounts | **1** | +| 42 | auth token sessions | **1** | + +`SELECT COUNT(*) … WHERE success = 0` → **0**(**failed migration = 0 ✔**)。 + +**双端口矩阵(实测)**: + +| 端点 | 业务 `/api/v1/actuator/*` | 管理 `/actuator/*` | +|---|---|---| +| `health` | **404** | **200**(`{"status":"UP"}`) | +| `metrics` | **404** | **200** | +| `prometheus` | **404** | **200** | +| `env` / `beans` / `heapdump` / `threaddump` / `configprops` | **404** | **404** | + +业务健康响应仅含 `application` / `version` / `status`,**不含** db、redis、rabbit、diskSpace、 +hikari、连接串或任何组件明细 ✔ + +## 20. §5 运行时 OpenAPI 与静态文件**逐集合**比对 + +| 项 | 静态 `docs/openapi-rc.json` | 运行时 `/api/v1/v3/api-docs` | +|---|---|---| +| paths | **176** | **176** | +| operations | **214** | **214** | +| 路径集合是否完全一致 | — | **True**(`only_static=[]`、`only_runtime=[]`) | +| operation 集合是否完全一致 | — | **True**(`only_static=[]`、`only_runtime=[]`) | + +**RUNTIME_OPENAPI_RESULT = PASS**(**非**仅比较数量:逐路径、逐 `METHOD path` 做集合相等判定)。 +`docs/openapi-rc.json` **未修改**。 + +## 21. §6 Token 与会话矩阵(真实 HTTP) + +**结果:36 / 36 全部 PASS。** + +| 分段 | 项数 | 结果 | +|---|---|---| +| §8 端口与健康(复跑) | 15 | **15/15 PASS** | +| §9.1 Token 类型矩阵 | 13 | **13/13 PASS** | +| §9.2 并发刷新与重放 | 3 | **3/3 PASS** | +| §9.3 登出与会话注销 | 5 | **5/5 PASS** | + +### 21.1 类型矩阵(每格均为实测) + +| Token | 普通接口 `/auth/me` | 刷新接口 `/auth/refresh` | +|---|---|---| +| Access | **200** | **401** | +| Refresh | **401** | **200** | +| 无 `type` | 401 | 401 | +| 错误密钥 | 401 | 401 | +| 错误 `aud` | 401 | 401 | +| 错误 `iss` | 401 | 401 | +| 缺 `iat` | 401 | 401 | +| 未来 `iat` | 401 | 401 | +| 已过期 | 401 | 401 | +| 非法 `jti` | 401 | 401 | +| 非法 `sid` | 401 | 401 | +| 非法 `sub`(非正数) | 401 | 401 | + +**拒绝响应泄漏扫描**:Token 原文 / `principal` / `claim` / 异常类 / 堆栈 / SQL / 本地路径 +→ **全部 0 命中** ✔ + +### 21.2 并发刷新与重放(10 轮 × 2 独立客户端) + +| 断言 | 结果 | +|---|---| +| 每轮最多 1 个成功 | **10/10 轮** ✔ | +| 失败方只能 401,不得 500 | ✔ | +| 该 session 活动 Refresh **严格为 0** | `act=0`(每轮实测)✔ | +| 原 Access 失效 | `old=401`(每轮实测)✔ | +| 无锁异常 / 无 `could not take locks` | ✔ | +| 数据库不存 JWT 原文 | `auth_tokens` 中 `token_hash LIKE 'eyJ%'` → **0** ✔ | + +### 21.3 登出与会话 + +| 断言 | 结果 | +|---|---| +| 单设备登出 | 200 ✔ | +| 只撤销当前 session,其他设备保持有效 | `A=0 B=1`,B 的 `/auth/me` → 200 ✔ | +| 重复登出幂等 | 已撤销的 Access → 401 ✔ | +| inactive 用户不能登录 | 拒绝(HTTP 400,<500,非 500)✔ | + +### 21.4 本轮两项**脚本缺陷**(非产品缺陷,已修正) + +| # | 现象 | 判定 | 修正 | +|---|---|---|---| +| S-1 | 夹具手机号 `1990000`(7 位)+5 位 = **12 位**,登录被拒 `phone must contain exactly 11 digits` | **脚本缺陷**;产品校验**正确生效** | 前缀改为 `199000`(6 位)+5 位 = 11 位 | +| S-2 | 「业务健康不泄露组件明细」误报,`act=db` | **脚本缺陷**:对整段 JSON 做子串匹配,随机 `requestId` 十六进制串恰好含 `db` | 改为仅解析 `data` 对象的键并做集合判定 | +| S-3 | 控制台 GBK 无法输出 `⊆` | 脚本缺陷 | 改为 ASCII 文案 | + +## 22. §12 运行时敏感扫描 + +**扫描范围**:最新应用日志、本轮 HTTP 响应、`auth_tokens`、`audit_logs`、`notifications`、`messages`、验收脚本输出。 + +| 目标 | 模式 | 命中 | 分类 | +|---|---|---|---| +| 应用日志 | `eyJ…`(JWT 原文) | **0** | — | +| 应用日志 | `Bearer ` | **0** | — | +| 应用日志 | 11 位手机号 | **0** | — | +| 应用日志 | `authorization` | **0** | — | +| 应用日志 | `password` | 2 | Spring Boot 默认 `Using generated security password=`;`SecurityConfig` **未启用** `httpBasic`/`formLogin`/`UserDetailsService`,该用户**惰性**。见 OBS-NEW-05 | +| HTTP 响应 | Token/principal/claim/异常类/堆栈/SQL/本地路径 | **0** | 见 §21.1 | +| `auth_tokens` | JWT 原文 | **0** | 仅存摘要 ✔ | +| `audit_logs` | `eyJ…` | **0** | — | +| `notifications` / `messages` | `eyJ…` | **0 / 0** | — | +| `audit_logs` | 11 位数字 | **818** | 见 **OBS-NEW-04**(**历史遗留**) | +| `audit_logs` | `claim%` | **0** | — | + +**扫描器探针有效性**:以注入式探针(`Authorization: Bearer`、11 位手机号、`DB_PASSWORD=`)验证后清理; +`audit_logs` 的 818 命中即为扫描器**确实能命中**的实证。 + +## 23. 本轮**新增**观察项 + +| ID | 内容 | 是否阻断 | 影响范围 | 证据 | 推荐后续动作 | +|---|---|---|---|---|---| +| **OBS-NEW-04** | `audit_logs` 中 `action='SMS_SEND'` 的 **818 行** `details` 含**完整手机号**(文本形态 `Mock SMS sent to <11 位号码> for login`,全部匹配真实移动号段 `1[3-9]…`) | **否**(不阻断本分支) | 本地验收数据库**历史数据**;生产审计表的 PII 留存面 | 时间范围 **2026-08-06 17:21 → 2026-09-17 14:46**,**全部早于**本分支 S01 加固;**本轮贡献 0 行**(本轮 `mock=false`,不产生该链路);`grep "Mock SMS sent to" src/main` → **当前代码已无该写入路径**(S01 已移除) | ① 属**数据留存**问题而非代码缺陷;② 生产侧需制定审计 PII 保留/脱敏策略;③ 如需清理须按数据治理流程单独执行,**本轮未删除任何 `audit_logs` 行** | +| **OBS-NEW-05** | 启动日志在 INFO 级输出 `Using generated security password=` | **否** | 日志噪声 + 潜在的惰性默认账号 | 应用日志第 21/23 行;`SecurityConfig` 中 `httpBasic`/`formLogin`/`UserDetailsService` **均无命中**,故无认证入口可到达该账号 | 排除 `UserDetailsServiceAutoConfiguration`(或在生产 profile 关闭)以消除该日志与默认账号 | + +## 24. §13 收尾:清理、保留足迹、RabbitMQ 终态 + +| 项 | 结果 | +|---|---| +| 本轮夹具用户(`199000%` 号段) | 清理前 **12** → 清理后 **0** ✔ | +| `sms_verification_codes`(`1990%`) | **0** ✔ | +| **`audit_logs`** | **未删除任何行**;验收开始前 **2868** 行 → 结束后 **2868** 行(**保留足迹,数量不变**)✔ | +| 应用 PID | 精确停止 **1419**;未停止任何未知 Java 进程 | +| `8080` / `8081` | 均已空闲 ✔ | +| 临时密钥文件 | `backend-infra/.final_acc_secrets.env` **已删除** | +| Docker 容器 | 保持运行(`mydreamjob-mysql`/`redis`/`rabbitmq`),未重建、未删 volume | +| 历史未跟踪文件 | 完整保留 | +| 本轮新增未跟踪文件 | 4 个验收脚本 + 3 个 JSON/日志产物 + 本报告 | + +**RabbitMQ 四队列终态**: + +| 队列 | ready | unacked | +|---|---|---| +| `queue.sms.verification` | **0** | **0** | +| `queue.notification` | **0** | **0** | +| `queue.audit` | **0** | **0** | +| `queue.notification.dlq` | **0** | **0** | + +**四队列归零 ✔**,与验收开始前一致;**未执行 purge**。 + +## 25. 续跑后的逐项状态(PASS / FAIL / SKIP) + +| 节 | 项目 | 状态 | +|---|---|---| +| §1 | 分支/HEAD/upstream/暂存区/两处 `M` 三方核验 | **PASS** | +| §2 | 提交历史与阶段完整性、迁移 V42–V45、V1–V41 未修改 | **PASS** | +| §2 | S01/S03 静态安全 | **PASS** | +| §3 | 三轮 `mvn clean verify` | **PASS**(3/3) | +| §4 | 最新 JAR + 真实 MySQL/Redis/RabbitMQ + 双端口 + Flyway V45 | **PASS** | +| §5 | 运行时 OpenAPI 逐集合比对 | **PASS** | +| §6 | Token 类型矩阵 | **PASS**(13/13) | +| §6 | 并发刷新与重放(10 轮) | **PASS**(3/3) | +| §6 | 登出与会话注销 | **PASS**(5/5) | +| §12 | 运行时敏感扫描(日志/响应/DB) | **PASS**,附 OBS-NEW-04/05 | +| §13 | 收尾清理与队列终态 | **PASS** | +| **§7** | **S01 岗位权限矩阵 + 短信失败关闭运行时** | **SKIP(未执行)** | +| **§8** | **S03 简历隐私矩阵** | **SKIP(未执行)** | +| **§8** | **管理员初始化矩阵** | **SKIP(未执行)** | +| **§8** | **V43 真实迁移验证** | **SKIP(未执行)** | +| **§9** | **Q01 真实数据库查询矩阵** | **SKIP(未执行)** | +| **§10** | **三个 GET 评分接口 22 表零写快照** | **SKIP(未执行)** | +| **§10** | **写侧刷新任务链路** | **SKIP(未执行)** | +| **§10** | **claim token 100 轮并发** | **SKIP(未执行)** | +| **§11** | **运行时 22 张表逐字段副作用 + 四类反证** | **SKIP(未执行)** | + +## 26. 续跑后最终 Git 状态 + +| 项 | 结果 | +|---|---| +| HEAD | `2319f97203a2fcf8012869fda8c30599ffb305`(**未变**) | +| upstream | `2319f972…`,ahead/behind **0 / 0** | +| 暂存区 | **空** | +| 已跟踪文件修改 | **无**(`git diff --name-status` 空) | +| `git diff --check` | exit 0 | +| V1–V45 迁移修改 | **无**;**无 V46** | +| master 合并 | **无** | +| commit / push / merge / rebase / force push | **均未执行** | + +## 27. 续跑后最终判定 + +> ## **FAIL(仍未通过)** + +**原因**:§25 中标为 **SKIP** 的 9 项(§7 岗位权限与短信、§8 简历隐私/管理员初始化/V43、 +§9 Q01 运行时、§10 评分零写/写侧刷新/claim token 100 轮、§11 运行时 22 表副作用)**本轮未执行**。 +按验收规则「任一核心项 FAIL 或 SKIP:总结必须写 FAIL」,**不得**标记为通过,**不得**使用「基本通过」。 + +**本轮新增的通过项**(均已在 §17–§24 给出实测证据): +三轮 `clean verify` 2788/0/0/0 稳定、最新 JAR + 真实三中间件启动与双端口隔离、 +运行时 OpenAPI 逐集合一致、Token 类型矩阵与并发重放登出共 36/36、运行时敏感扫描、 +队列归零与夹具零残留。 + +**未发现任何产品缺陷**。本轮发现的 3 项脚本缺陷与 1 项历史数据观察(OBS-NEW-04)均已如实分类, +未现场修改任何生产代码或既有测试。 + +**下一轮定向整改指令(承接 §15,不修改生产代码)**: + +1. 补齐 §7 岗位权限矩阵与短信失败关闭运行时(注意 `COMPANY_NOT_FOUND`→404 的既有契约,见 OBS-NEW-02)。 +2. 补齐 §8 简历隐私矩阵(含拒绝路径 storage read = 0)、管理员初始化隔离矩阵、V43 真实迁移验证。 +3. 补齐 §9 Q01 真实数据库查询矩阵与 EXPLAIN 分表报告。 +4. 补齐 §10 三 GET 零写 22 表快照、写侧刷新链路、claim token 100 轮。 +5. 补齐 §11 运行时 22 表逐字段副作用与改/插/删/单列四类反证。 +6. 就 **OBS-NEW-04** 制定审计 PII 留存策略(数据治理,非代码改动);就 **OBS-NEW-05** 排除默认安全用户自动配置。 + +--- +--- + +# 续跑记录(2026-09-18 第三轮,追加) + +上文全部内容(初次 FAIL、首次 `clean verify` 的 2 个 Error、MSYS vhost 缺陷、OBS-NEW-01、 +第二轮三轮构建结果与全部原始证据)**原样保留、未删改**。 + +> ### ⚠ 本轮发生一次**验收脚本误操作历史数据**事件,已复原并如实记录(见 §32)。 + +## 28. §7 S01 岗位权限矩阵(真实 HTTP):**20/20 PASS** + +| 用例 | 期望 | 实测 | +|---|---|---| +| 匿名 GET `/jobs`(列表保持公开) | 200 | **200** ✔ | +| 匿名 GET `/jobs/public/{id}`(公开详情) | 200 | **200** ✔ | +| 匿名 GET `/jobs/{id}`(企业端详情) | 401 | **401** ✔ | +| 匿名 POST `/jobs` | 401 | **401** ✔ | +| 匿名 PUT `/jobs/{id}` | 401 | **401** ✔ | +| 匿名 PATCH `/jobs/{id}/status`(发布) | 401 | **401** ✔ | +| 匿名 DELETE `/jobs/{id}` | 401 | **401** ✔ | +| COMPANY 创建岗位 | 200/201 | **成功** ✔ | +| COMPANY 发布岗位 | 200 | **200** ✔ | +| STUDENT 创建岗位 | 拒绝且非 500 | **403 / code=2002** ✔ | +| ADMIN 创建岗位 | 拒绝且非 500 | **403 / code=2002** ✔ | +| 其他企业修改他人岗位 | 非 200 且非 500 | **400** ✔ | +| 失败请求不写入岗位表 | jobCount 不变 | **27 → 27** ✔ | +| 异常主体 `anonymousUser` | 401 | **401** ✔ | +| 异常主体 非数字 | 401 | **401** ✔ | +| 异常主体 20 位 Long 溢出 | 401 | **401** ✔ | +| 异常主体 零值 `sub` | 401 | **401** ✔ | +| 生产 `mock=false` 短信安全失败 | 非 200 且非 5xx | **400** ✔ | +| 不伪造发送成功 | 响应无成功文案 | **无** ✔ | +| 失败响应不含手机号/验证码/Token | 无泄漏 | **无** ✔ | + +**说明**:STUDENT/ADMIN 创建岗位返回 **403 / code=2002(FORBIDDEN)**,与指令期望一致; +该项在本轮**实测**,修正了第二轮基于静态基线的推断(见 OBS-NEW-02)。 + +## 29. §9 Q01 岗位搜索真实数据库矩阵:**13/15**,发现 **DEF-01** + +### 29.1 通过项(13) + +空条件返回结果、标题关键词、描述关键词、大小写不敏感、特殊字符(`Delta_100%`)、 +城市过滤(`city=上海` → 1)、最低薪资下限、最高薪资上限、边界相等、无结果、 +分页 `size=1`、非公开(DRAFT)岗位不进入公开搜索 —— 全部符合预期。 + +### 29.2 **DEF-01:`/jobs` 与 `/jobs/search` 关键词语义分歧(产品行为缺陷)** + +**最小复现(真实 HTTP,已保存)**: + +``` +GET /api/v1/jobs?keyword=%20AlphaEngineer%20 → items=0 (期望 1:应 trim 后命中) +GET /api/v1/jobs?keyword=%20%20%20 → items=0 (期望 20:空白应等同未传) +GET /api/v1/jobs/search?keyword=%20AlphaEngineer%20 → items=1 (正确) +GET /api/v1/jobs/search?keyword=%20%20%20 → items=20 (正确) +GET /api/v1/jobs → items=20 +GET /api/v1/jobs?keyword=AlphaEngineer → items=1 +``` + +**根因(静态定位,未修改代码)**: + +| 端点 | 调用链 | 关键词处理 | trim | 空白关键词 | +|---|---|---|---|---| +| `GET /jobs` | `JobController:97` → `JobService.listPublishedJobs` → `JobRepository:26`(JPQL `LIKE %:keyword%`) | 仅判 `:keyword IS NULL` | **无** | 空白串**非 NULL** → 谓词生效 → **0 条** | +| `GET /jobs/search` | `JobController:134` → `JobService.searchJobs` → `JobSearchSpecification:53` | `"%" + keyword.trim() + "%"` + `isBlank()` 判空 | **有** | 空白被 `isBlank()` 跳过 → **全部** | + +**判定**:指令 §12 明确列出 **trim** 与 **空白关键词** 两个验收用例。`/jobs` 端点两条均不满足 +(trim 未生效、空白关键词返回 0 而非全部),且与同一产品的另一公开端点 `/jobs/search` 行为不一致。 +按「产品缺陷直接 FAIL,不现场修复」→ **判定为 FAIL 项**,**未修改任何生产代码**。 + +**定向整改指令(下一轮,不属本轮范围)**: +统一 `GET /jobs` 与 `GET /jobs/search` 的关键词语义 —— 使 `listPublishedJobs` 路径同样对 +`keyword` 执行 `trim` + `isBlank` 判空(或将 `/jobs` 直接委托给 `JobSearchSpecification`), +并补充「带前后空白的关键词」与「纯空白关键词」两条 HTTP 回归用例。注意 +`JobRepository:26` 与 `JobSearchSpecification` 是**两套并行实现**,整改时需评估是否合并。 + +### 29.3 本轮 Q01 未覆盖项 + +排序(`sort`)、H2/MySQL 逐项对账(ID 与顺序一致)、EXPLAIN 分表报告 —— **SKIP(未执行)**。 + +## 30. §7/§9 本轮脚本缺陷(非产品缺陷,已修正) + +| # | 现象 | 判定 | 修正 | +|---|---|---|---| +| S-4 | 匿名公开详情断言 401(期望 200) | **脚本缺陷**:调用了企业端 `/jobs/{id}`;公开端点是 `/jobs/public/{id}` | 改用正确端点,并**新增**一条「企业端详情匿名 401」断言 | +| S-5 | 中文城市与含空格关键词全部 0 条 | **脚本缺陷**:查询参数**未做 URL 编码** | 改用 `urllib.parse.quote` | +| S-6 | 清理时 `fk_jobs_company` 外键失败 | **脚本缺陷**:未按外键顺序删除 | 改为 applications → jobs → companies → users | + +## 31. 第三轮收尾 + +| 项 | 结果 | +|---|---| +| 本轮夹具残留(`199000%` 用户 / `FINALACC_20260918_%` 公司·岗位 / 短信码) | **全部 0** ✔ | +| **`audit_logs`** | **未删除任何行**;本轮开始前 **2868** → 第三轮开始前 **2888** → 结束后 **2888**(本轮 HTTP 产生的审计行**保留**,未删) | +| 应用 PID | 精确停止 **1438** | +| `8080` / `8081` | 均已空闲 ✔ | +| 临时密钥文件 | `.final_acc_secrets.env` **已删除** | +| RabbitMQ 四队列 | 全部 `ready=0 unacked=0` ✔,**未 purge** | +| Docker | 三容器保持运行,未重建、未删 volume | + +## 32. ⚠ 验收脚本误操作历史数据事件(已复原,如实记录) + +**经过**:第三轮首次运行 `final_acc_20260918_b_jobs_search.py` 时,夹具清理使用了**过宽谓词** +`phone LIKE '1990%'`(本意是匹配本轮 `199000 + 5 位` 夹具),导致两条**既有历史用户** +(`users.id = 7938 / 7939`,创建于 **2026-09-14 09:48**,非本轮产生)的 `user_roles` 行被删除。 + +**发现**:紧随其后的 `DELETE FROM users WHERE phone LIKE '1990%'` 因 +`fk_applications_student` 外键失败而中止,据此定位到该前缀下存在非本轮用户。 + +**影响面(已逐项核实)**: + +| 对象 | 状态 | +|---|---| +| `users` 7938 / 7939 | **未删除**(删除语句因外键失败,未生效)✔ | +| `user_roles` 7938 / 7939 | **被误删** ✗ → **已复原** | +| `applications`(学生 7938/7939,共 2 条) | **未受影响** ✔ | +| `auth_tokens`(7938/7939) | 原本为 0,未受影响 ✔ | +| 其它表 | 未触及 | + +**复原操作与依据**:`INSERT INTO user_roles (user_id, role) VALUES (7938,'STUDENT'),(7939,'STUDENT');` +依据:`users.role` 两行均为 `STUDENT`,且库内 **256** 条 STUDENT 用户的 `user_roles` **全部**为 +`('STUDENT')`(1:1 形态)。复原后已核验两行存在且角色正确。 + +**同类错误未再次发生**:脚本已改为精确前缀 `199000%` 并补全外键删除顺序;第三轮后续运行未再触碰历史行。 + +**教训与建议**:验收夹具前缀应避免使用过短的数字前缀(`1990` 会与既有测试号段冲突), +建议改用更长且唯一的号段,并在任何 DELETE 前先 `SELECT COUNT(*)` 输出命中范围。 + +## 33. 第三轮后的逐项状态 + +| 节 | 项目 | 状态 | +|---|---|---| +| §7 S01 岗位权限矩阵(20 项) | 真实 HTTP 全量执行 | **PASS** | +| §7 S01 短信生产失败关闭 | 真实 HTTP | **PASS** | +| §9 Q01 搜索矩阵(15 项) | 真实 HTTP + MySQL | **13 PASS / 2 FAIL(DEF-01)** | +| §9 Q01 排序 / H2-MySQL 对账 / EXPLAIN | — | **SKIP** | +| §8 S03 简历隐私矩阵 | — | **SKIP** | +| §8 S03 管理员初始化矩阵 | — | **SKIP** | +| §8 S03 V43 真实迁移验证 | — | **SKIP** | +| §10 R01 三 GET 零写 22 表快照 | — | **SKIP** | +| §10 R01 写侧刷新链路 | — | **SKIP** | +| §10 R01 claim token 100 轮 | — | **SKIP** | +| §11 运行时 22 表逐字段副作用 + 四类反证 | — | **SKIP** | +| §12 运行时敏感扫描(日志/响应/DB) | 已在第二轮执行 | **PASS**(附 OBS-NEW-04/05) | + +## 34. 第三轮后最终 Git 状态 + +| 项 | 结果 | +|---|---| +| HEAD | `2319f97203a2cafcf8012869fda8c30599ffb305`(**未变**) | +| upstream | 同提交,ahead/behind **0 / 0** | +| 暂存区 | **空** | +| 已跟踪文件修改 | **无** | +| `git diff --check` | exit 0 | +| V1–V45 迁移修改 | **无**;**无 V46** | +| master 合并 / commit / push / merge / rebase / force push | **均未执行** | + +## 35. 第三轮后最终判定 + +> ## **FAIL(仍未通过)** + +**新增 FAIL 项:DEF-01**(`/jobs` 关键词 trim 与空白关键词行为不符合 Q01 验收判据, +与 `/jobs/search` 语义分歧)—— 属**产品行为缺陷**,已给出最小复现与定向整改指令,**未修改生产代码**。 + +**仍为 SKIP 的项**:§8 全部(简历隐私矩阵、管理员初始化矩阵、V43 真实迁移)、 +§9 排序与 H2/MySQL 对账与 EXPLAIN、§10 全部(三 GET 零写、写侧刷新、claim token 100 轮)、 +§11 运行时 22 表副作用与四类反证。 + +**第三轮新增通过项**:S01 岗位权限矩阵 20/20(真实 HTTP)、短信生产失败关闭、 +Q01 搜索矩阵 13/15(真实 HTTP + MySQL)、夹具零残留与审计保留足迹、队列归零。 + +**下一轮定向整改指令**: + +1. **DEF-01(优先,产品缺陷)**:统一 `GET /jobs` 与 `GET /jobs/search` 的关键词语义 + (`trim` + `isBlank` 判空),并补充 HTTP 回归用例;评估 `JobRepository:26` 与 + `JobSearchSpecification` 两套并行实现是否合并。 +2. 补齐 §8 简历隐私矩阵、管理员初始化隔离矩阵、V43 真实迁移验证。 +3. 补齐 §9 排序、H2/MySQL 逐项对账与 EXPLAIN 分表报告。 +4. 补齐 §10 三 GET 零写 22 表快照、写侧刷新链路、claim token 100 轮。 +5. 补齐 §11 运行时 22 表逐字段副作用与改/插/删/单列四类反证。 +6. 验收夹具改用更长唯一号段;任何 DELETE 前先输出命中范围(见 §32)。 + +--- +--- + +# 续跑记录(2026-09-18 第四轮,追加) + +上文全部内容(初次 FAIL、首次 2 个 Error、MSYS vhost 缺陷、OBS-NEW-01、第二/三轮结果、 +§32 历史数据误操作事件)**原样保留、未删改**。 + +## 36. §8 S03 —— V43 真实 MySQL 验证:**PASS** + +在 `mydreamjob-mysql`(真实 MySQL 8.4,V43 已随 Flyway 执行、`success=1`)上逐项验证: + +| 断言 | 期望 | 实测 | +|---|---|---| +| 完整五要素指纹命中并禁用 | 三个演示账号 `is_active=0` | `13800000001 ADMIN 0`、`13800000002 STUDENT 0`、`13800000003 COMPANY 0` ✔ | +| `users` / `user_roles` **保留**(不删除) | 3 / 3 | **3 / 3** ✔ | +| 重复执行稳定(幂等) | 重跑三段 UPDATE 受影响行数 = 0 | **0 / 0 / 0** ✔ | +| **任意单字段改变不命中** | 改 phone / email / password_hash / role 各自 0 行 | **0 / 0 / 0 / 0** ✔(证明五个合取项全部生效) | +| 不依赖固定 ID | V43 的 `WHERE` 无 `id =` 谓词 | 静态核对 V43 SQL:仅用 phone+email+password_hash+role+is_active 五元指纹 ✔ | +| 执行后演示账号未被误改 | 仍为禁用且字段不变 | 复核 `1/2/3` 三行 `is_active=0` ✔ | + +**未覆盖**:V43「原演示账号登录被拒绝」的**运行时**验证 —— **SKIP**(需构造 `sms_verification_codes` 并调用登录, +本轮未执行;其等价语义已由 S01 的「inactive 用户不能登录」HTTP 用例覆盖并通过,见 §21.3)。 + +## 37. §10 三个 GET 评分接口零写(24 张表逐字段快照):**PASS(7/7)** + +| 断言 | 结果 | +|---|---| +| `GET /profile/me/candidate-scoring`(综合评分) | **200** ✔ | +| `GET /profile/me/candidate-scoring/dimensions/EDUCATION`(单维度) | **200** ✔ | +| `GET /company/candidates/{applicationId}/scoring`(企业评分) | **200** ✔ | +| **三个 GET 前后 24 张表逐字段零变化** | `changed=none` ✔ | +| GET 不新增 `candidate_scoring_refresh_tasks` | `0 -> 0` ✔ | +| `candidate_dimension_scores` 行数与 `version` 不推进 | `rows 1->1`、`ver 0->0` ✔ | +| 无快照学生请求后仍为 **0 行** | `0` ✔ | + +**快照覆盖的 24 张表**:users、user_roles、student_profiles、companies、jobs、applications、 +resume_versions、auth_tokens、audit_logs、notifications、messages、message_reads、conversations、 +conversation_members、candidate_dimension_scores、candidate_scoring_refresh_tasks、 +candidate_scoring_appeals、offers、interviews、candidate_feedback、talent_invitations、 +scoring_risk_events、scoring_risk_actions、personal_data_export_tasks。 +每张表**显式列出稳定字段**(含 `version` / `updated_at` 等),按主键排序,逐字段深比较 —— +**未**只比较 COUNT / SUM / CRC。 + +## 38. §11 副作用快照比较器的四类反证:**PASS(4/4)** + +每条反证都要求**变更真正命中 1 行**且**被比较器检出**(避免「没改到任何行」造成的假通过): + +| 反证 | 变更语句 | 受影响行数 | 是否检出 | +|---|---|---|---| +| 改一行 | `UPDATE users SET nickname=…` | **1** | **True** ✔ | +| 插一行 | `INSERT INTO candidate_scoring_refresh_tasks …` | **1** | **True** ✔ | +| 改单列 | `UPDATE candidate_dimension_scores SET status='PENDING' …` | **1** | **True** ✔ | +| 删一行 | `DELETE FROM candidate_dimension_scores …` | **1** | **True** ✔ | + +⇒ 快照比较器**确实具备检出能力**,§37 的「零变化」不是「没在比」。 + +## 39. 第四轮脚本缺陷(非产品缺陷,已修正) + +| # | 现象 | 判定 | 修正 | +|---|---|---|---| +| S-7 | 反证「插一行 / 删一行 / 改单列」未检出 | **脚本缺陷**:变更语句**未命中任何行**(`scoring_risk_events` 无 `student_user_id` 列而静默失败;`candidate_feedback` 无对应行;`dimension_scores` 已被前序步骤删除) | 改用确定存在的行,并断言 `ROW_COUNT()=1` | +| S-8 | 快照列名错误 | **脚本缺陷**:`scoring_risk_events` 实际列为 `subject_user_id`,非 `student_user_id` | 按 `information_schema` 实测列名修正 | +| S-9 | 收尾清理报 `Unknown column` | 同 S-8 | 同上 | + +## 40. 第四轮收尾 + +| 项 | 结果 | +|---|---| +| 夹具残留(用户 / 公司 / 岗位 / 短信码 / refresh task) | **全部 0** ✔ | +| **`audit_logs`** | **未删除任何行**;本轮 HTTP 产生的审计行**保留**,第三轮结束 2888 → 本轮结束 **2900** | +| 历史用户 `7938 / 7939` | `users.role` 与 `user_roles.role` 均为 `STUDENT`,**完好** ✔ | +| 应用 PID | 精确停止 **1452** | +| `8080` / `8081` | 空闲 ✔ | +| 临时密钥文件 | 已删除 | +| RabbitMQ 四队列 | 全部 `ready=0 unacked=0` ✔,未 purge | +| Docker | 三容器保持运行,未重建、未删 volume | + +## 41. 第四轮后逐项状态 + +| 节 | 项目 | 状态 | +|---|---|---| +| §8 S03 V43 真实 MySQL 验证 | — | **PASS** | +| §10 三 GET 零写(24 表逐字段) | — | **PASS(7/7)** | +| §11 四类反证 | — | **PASS(4/4)** | +| §8 S03 简历隐私矩阵 | — | **SKIP** | +| §8 S03 管理员初始化矩阵 | — | **SKIP** | +| §8 S03 V43 登录拒绝(运行时) | — | **SKIP** | +| §9 Q01 排序 / H2-MySQL 对账 / EXPLAIN | — | **SKIP** | +| §10 写侧刷新链路 | — | **SKIP** | +| §10 claim token 100 轮 | — | **SKIP** | + +## 42. 第四轮后最终 Git 状态 + +| 项 | 结果 | +|---|---| +| HEAD | `2319f97203a2cafcf8012869fda8c30599ffb305`(**未变**) | +| upstream | 同提交,**0 / 0** | +| 暂存区 | **空** | +| 已跟踪文件修改 | **无** | +| V1–V45 修改 / V46 | **无 / 不存在** | +| master 合并 / commit / push / merge / rebase / force push | **均未执行** | + +## 43. 全轮次累计判定 + +> ## **FAIL(仍未通过)** + +**累计 FAIL 项**:**DEF-01**(`/jobs` 关键词 trim 与空白关键词不符合 Q01 判据,与 `/jobs/search` 语义分歧;产品缺陷,已给最小复现与整改指令,未修改生产代码)。 + +**累计 SKIP 项**:§8 简历隐私矩阵、管理员初始化矩阵、V43 运行时登录拒绝;§9 排序 / H2-MySQL 逐项对账 / EXPLAIN 分表报告;§10 写侧刷新链路、claim token 100 轮。 + +**累计 PASS 项**:开工核查与两处 `M` 三方核验、提交与迁移完整性、S01/S03 静态审查、 +三轮 `clean verify` 2788/0/0/0、最新 JAR + 真实三中间件 + 双端口 + Flyway V45、 +运行时 OpenAPI 逐集合一致、Token/会话矩阵 36/36、S01 岗位权限矩阵 20/20、 +S03 V43 真实 MySQL 验证、Q01 搜索矩阵 13/15、三 GET 零写 24 表快照 7/7、 +四类反证 4/4、运行时敏感扫描、夹具零残留与审计保留足迹、队列归零。 + +**最终 Git**:HEAD 未变、upstream 0/0、暂存区空、无已跟踪修改、未提交、未推送、未合并。 + +--- +--- + +# 续跑记录(2026-09-18 第五轮,追加) + +上文全部内容(初次 FAIL、首次 2 个 Error、MSYS vhost 缺陷、OBS-NEW-01、第二/三/四轮结果、 +§32 历史数据误操作事件)**原样保留、未删改**。 + +## 44. §8 S03 —— V43 运行时登录拒绝:**PASS(4/4)** + +对 V43 禁用的两个 Flyway 演示账号,注入合法验证码后走真实登录链路: + +| 断言 | 期望 | 实测 | +|---|---|---| +| 已禁用演示 **ADMIN**(`13800000001`)登录 | 非 200 且无 Token | **HTTP 400,无 `accessToken`** ✔ | +| 已禁用演示 **STUDENT**(`13800000002`)登录 | 非 200 且无 Token | **HTTP 400,无 `accessToken`** ✔ | +| 被拒登录未产生会话行 | `auth_tokens` = 0 | **0** ✔ | +| 演示账号行未被删除 | 2 行存在 | **2** ✔ | + +⇒ V43 的**目标语义**(禁用后无法取得会话)在真实运行实例上成立;结合 §36 的真实 MySQL 指纹验证, +V43 的静态 + 数据 + 运行时三层证据齐备。 + +## 45. §9 Q01 排序:**部分 PASS,排序语义 SKIP** + +| 用例 | 结果 | +|---|---| +| 白名单校验:未知 `sort` 值一律 400 | `salary_desc` / `salary_asc` / `salaryDesc` / `salaryAsc` → **400** ✔(**正确的白名单拒绝**) | +| `sort=latest` | **200** ✔ | +| `sort=salary` | **200** ✔(白名单内) | +| `sort` 省略 | **200** ✔ | +| **`sort=salary` 的升降序与 NULL 排序语义** | **SKIP** —— 未用夹具验证顺序 | +| **`sort=relevance` 的相关度语义** | **SKIP** | + +**白名单事实(静态)**:`JobService:201` `ALLOWED_SORTS = Set.of("latest", "salary", "relevance")`; +`JobService:216` 非白名单值抛 `BusinessException(BAD_REQUEST)` → 400。 +`JobService:207` 注释说明 `sort=salary` 用 CriteriaQuery 的 CASE 排序使 `salaryMax=NULL` 排在最后。 + +## 46. 第五轮脚本缺陷(非产品缺陷,已修正) + +| # | 现象 | 判定 | 修正 | +|---|---|---|---| +| S-10 | 排序用例 4 项 400 | **脚本缺陷**:使用了**不存在的** `sort` 值(`salary_desc`/`salaryAsc` 等);产品白名单只有 `latest`/`salary`/`relevance`,返回 400 是**正确行为** | 记录真实白名单;顺序语义因缺夹具未验证,标记 SKIP | +| S-11 | 排序段 `TypeError: int() … NoneType` | **脚本缺陷**:只登录创建了 STUDENT 用户,未建公司夹具 | 补 SQL 公司夹具 | + +## 47. 第五轮收尾 + +| 项 | 结果 | +|---|---| +| 夹具残留(用户 / 公司 / 岗位 / 短信码) | **全部 0** ✔ | +| **`audit_logs`** | **未删除任何行**;第四轮 2900 → 本轮 **2904**(本轮产生的审计行保留) | +| Flyway 演示账号 | 仍为 `13800000001=0, 13800000002=0, 13800000003=0`(**禁用状态未被改动**)✔ | +| 历史用户 `7938 / 7939` 的 `user_roles` | **2 行存在**(§32 复原成果保持)✔ | +| 应用 PID | 精确停止 **1791** | +| `8080` / `8081` | 空闲 ✔ | +| 临时密钥文件 | 已删除 | +| RabbitMQ 四队列 | 全部 `ready=0 unacked=0` ✔,未 purge | + +## 48. 全轮次最终状态汇总 + +| 节 | 项目 | 状态 | +|---|---|---| +| §1 | 分支/HEAD/upstream/暂存区/两处 `M` 三方核验 | **PASS** | +| §2 | 提交与迁移完整性、V1–V41 未修改、S01/S03 静态审查 | **PASS** | +| §3 | 三轮 `mvn clean verify` | **PASS(3/3)** | +| §4 | 最新 JAR + 真实 MySQL/Redis/RabbitMQ + 双端口 + Flyway V45 | **PASS** | +| §5 | 运行时 OpenAPI 逐集合一致 | **PASS** | +| §6 | Token/会话矩阵(类型 + 并发重放 + 登出) | **PASS(36/36)** | +| §7 | S01 岗位权限矩阵 + 短信失败关闭 | **PASS(20/20)** | +| §8 | S03 V43 真实 MySQL 指纹验证 | **PASS** | +| §8 | S03 V43 运行时登录拒绝 | **PASS(4/4)** | +| §9 | Q01 搜索矩阵 | **13 PASS / 2 FAIL(DEF-01)** | +| §9 | Q01 排序白名单 | **PASS** | +| §9 | Q01 排序语义 / H2-MySQL 逐项对账 / EXPLAIN 分表 | **SKIP** | +| §10 | 三 GET 零写(24 表逐字段) | **PASS(7/7)** | +| §10 | 写侧刷新链路 | **SKIP** | +| §10 | claim token 100 轮 | **SKIP** | +| §11 | 四类反证 | **PASS(4/4)** | +| §12 | 运行时敏感扫描 | **PASS**(附 OBS-NEW-04/05) | +| §13 | 收尾清理、队列归零、审计保留足迹 | **PASS** | +| §14 | 简历隐私矩阵 | **SKIP** | +| §14 | 管理员初始化矩阵 | **SKIP** | + +## 49. 最终判定(全轮次) + +> ## **FAIL(仍未通过)** + +**FAIL 项(1)**:**DEF-01** —— `GET /jobs` 的关键词 `trim` 与空白关键词行为不符合 Q01 验收判据, +且与 `GET /jobs/search` 语义分歧(`/jobs?keyword=%20%20%20` → 0 条;`/jobs/search?keyword=%20%20%20` → 20 条)。 +**产品行为缺陷**,已给出最小复现与定向整改指令,**未修改任何生产代码**。 + +**SKIP 项(5)**:§14 简历隐私矩阵、§14 管理员初始化矩阵、§9 排序语义与 H2/MySQL 逐项对账与 EXPLAIN 分表、 +§10 写侧刷新链路、§10 claim token 100 轮。 + +**观察项(不阻断)**:OBS-01、OBS-02、OBS-E01、OBS-NEW-01(已降级)、OBS-NEW-02、OBS-NEW-03、 +OBS-NEW-04(审计历史 PII)、OBS-NEW-05(默认安全口令日志)。 + +**产品缺陷数**:**1**(DEF-01)。**未发现其它产品缺陷**;本轮其余失败全部定位为**验收脚本/环境缺陷**(S-1…S-11)。 + +**最终 Git 状态**: + +| 项 | 结果 | +|---|---| +| HEAD | `2319f97203a2cafcf8012869fda8c30599ffb305`(**未变**) | +| upstream | `2319f972…`,ahead/behind **0 / 0** | +| 暂存区 | **空** | +| 已跟踪文件修改 | **无** | +| `git diff --check` | exit 0 | +| V1–V45 修改 / V46 | **无 / 不存在** | +| master 合并 / commit / push / merge / rebase / force push | **均未执行** | +| 历史未跟踪文件 | 完整保留 | +| 本报告 | 保持**未跟踪** | + +**结论**:**不具备创建 PR 的条件** —— 需先修复 **DEF-01** 并补齐上述 5 项 SKIP 验收。 + +--- +--- + +# 续跑记录(2026-09-18 第六轮,追加) + +上文全部内容(初次 FAIL、首次 2 个 Error、MSYS vhost 缺陷、OBS-NEW-01、第二~五轮结果、 +§32 历史数据误操作事件)**原样保留、未删改**。 + +## 50. §10 V45 claim_token 真实 MySQL **100 轮**所有权验证:**PASS(7/7)** + +**方法**:忠实复现 `CandidateScoringRefreshTaskExecutor` 的五条 SQL(claim / recoverStale / +complete / pendingRollback / recordFailure),在真实 MySQL 上直接驱动 —— **未修改任何生产代码**。 + +| 断言 | 期望 | 实测 | +|---|---|---| +| **同 `processing_started_at` 的两次领取 token 必须不同** | 100/100 | **100/100** ✔ | +| A 的迟到 `COMPLETED` / `PENDING` 回退 / `RETRY`·`FAILED` 均更新 **0 行** | 100/100 | **100/100** ✔ | +| 且 B 的 status / version / token 不变 | 100/100 | **100/100** ✔ | +| 伪造 / 空 / 旧 token 的更新均 **0 行** | 100/100 | **100/100** ✔ | +| 离开 `PROCESSING` 后 token 被清空 | 100/100 | **100/100** ✔ | +| 无永久 `PROCESSING` | 100/100 | **100/100** ✔ | +| `recoverStale` 后 token 清空 | token=`` | **``** ✔ | +| `claim_token` **不进入**审计 / 通知 / 消息 / 快照 JSON | 全 0 | `{audit_logs:0, notifications:0, messages:0, snapshot_history:0}` ✔ | + +**关键设计**:每轮刻意让两次领取写入**完全相同**的 `processing_started_at`(秒级 `DATETIME`,固定为 +`2026-09-18 12:00:00`)—— 这正是 V45 修复前「新旧持有者取得相同所有权标识」的触发条件。 +实测 100/100 轮生成**不同** `claim_token`,且旧持有者的全部迟到写入均 **0 行**,证明 +**V45 的 claim fence 在真实 MySQL 上确实封堵了该竞态**。 + +## 51. 第六轮脚本缺陷(非产品缺陷,已修正) + +| # | 现象 | 判定 | 修正 | +|---|---|---|---| +| S-12 | 100 轮全部短路,计数器 0/100 | **脚本缺陷**:`recoverStale` 的边界传入了与领取**相同**的 `T`,`processing_started_at < T` 恒假 → 回收 0 行 → 后续第二次领取不满足 `status IN ('PENDING','RETRY')` | 边界改为晚于 `T` 的值 | +| S-13 | `KeyError: JWT_ACCESS_SECRET` | **脚本缺陷**:lib 在 import 时读取密钥,而本轮脚本不需要 JWT | 运行期注入占位值 | + +## 52. 第六轮收尾 + +| 项 | 结果 | +|---|---| +| 夹具残留(用户 / refresh task / 公司 / 短信码) | **全部 0** ✔ | +| `audit_logs` | **未删除任何行**,维持 **2904** | +| 历史用户 `7938 / 7939` 的 `user_roles` | **2 行存在** ✔ | +| `8080` / `8081` | 空闲(无监听)✔ | +| RabbitMQ 四队列 | **4/4 均为 `0 0`** ✔,未 purge | +| Docker | 三容器保持运行,未重建、未删 volume | + +## 53. 全部轮次最终状态 + +| 节 | 项目 | 状态 | +|---|---|---| +| §1 开工核查与两处 `M` 三方核验 | — | **PASS** | +| §2 提交与迁移完整性、S01/S03 静态审查 | — | **PASS** | +| §3 三轮 `mvn clean verify` | — | **PASS(3/3,2788/0/0/0)** | +| §4 最新 JAR + 真实三中间件 + 双端口 + Flyway V45 | — | **PASS** | +| §5 运行时 OpenAPI 逐集合一致 | — | **PASS(176/214)** | +| §6 Token/会话矩阵 | — | **PASS(36/36)** | +| §7 S01 岗位权限矩阵 + 短信失败关闭 | — | **PASS(20/20)** | +| §8 S03 V43 真实 MySQL 指纹验证 | — | **PASS** | +| §8 S03 V43 运行时登录拒绝 | — | **PASS(4/4)** | +| §9 Q01 搜索矩阵 | — | **13 PASS / 2 FAIL(DEF-01)** | +| §9 Q01 排序白名单 | — | **PASS** | +| §9 Q01 排序语义 / H2-MySQL 对账 / EXPLAIN 分表 | — | **SKIP** | +| §10 三 GET 零写(24 表逐字段) | — | **PASS(7/7)** | +| §10 **V45 claim_token 100 轮** | — | **PASS(7/7,各项 100/100)** | +| §10 写侧刷新链路 | — | **SKIP** | +| §11 四类反证 | — | **PASS(4/4)** | +| §12 运行时敏感扫描 | — | **PASS**(附 OBS-NEW-04/05) | +| §13 收尾清理 / 队列归零 / 审计保留足迹 | — | **PASS** | +| §14/§8 简历隐私矩阵 | — | **SKIP** | +| §14/§8 管理员初始化矩阵 | — | **SKIP** | + +## 54. 最终判定 + +> ## **FAIL(仍未通过)** + +**FAIL 项(1)**:**DEF-01** —— `GET /jobs` 的关键词 `trim` 与空白关键词行为不符合 Q01 验收判据, +且与 `GET /jobs/search` 语义分歧(`/jobs?keyword=%20%20%20` → 0 条;`/jobs/search?keyword=%20%20%20` → 20 条)。 +**产品行为缺陷**,已给出最小复现与定向整改指令,**未修改任何生产代码**。 + +**SKIP 项(4)**:简历隐私矩阵、管理员初始化矩阵、Q01 排序语义/H2-MySQL 对账/EXPLAIN 分表、 +写侧刷新链路。 + +**产品缺陷数:1**(DEF-01)。其余全部失败均定位为**验收脚本/环境缺陷**(S-1…S-13),非产品缺陷。 + +**最终 Git 状态**:HEAD `2319f97203a2cafcf8012869fda8c30599ffb305`(未变); +upstream **0 / 0**;暂存区**空**;已跟踪**零修改**;`git diff --check` exit 0; +V1–V45 未修改、**无 V46**;master **未合并**;**未提交、未推送**;历史未跟踪文件完整保留; +本报告保持**未跟踪**。 + +**结论**:**不具备创建 PR 的条件** —— 需先修复 **DEF-01**(产品缺陷)并补齐上述 4 项 SKIP 验收。 + +--- +--- + +# 续跑记录(2026-09-18 第七轮,追加)—— 环境阻断 + +上文全部内容**原样保留、未删改**。 + +## 55. 第七轮:Docker 守护进程不可用,剩余 SKIP 项**被环境阻断** + +**现象**: + +``` +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 +docker ps -a → 同一错误 +``` + +**判定**:**Docker Desktop 守护进程已停止**(并非容器 Exited)。这与指令允许的 +「`docker start <精确容器名>`」情形不同 —— 指令允许的是在**守护进程正常**时启动已停止的具名容器; +本轮的阻断发生在**更下一层**,启动 Docker Desktop 属主机级运维动作,不在本轮允许范围内, +故**未尝试启动**,也未对容器/volume 做任何操作。 + +**连带影响**: + +| 项 | 影响 | +|---|---| +| 应用启动 | 我启动的最新 JAR(PID 542)因连不上 MySQL 而**启动失败**(`Communications link failure`),未监听 8080/8081 | +| `mysql()` 夹具通道 | 依赖 `docker exec mydreamjob-mysql`,随之不可用 | +| 第四~六轮已完成的验证 | **不受影响**(证据在 Docker 停止前已采集并写入本报告) | + +**第七轮收尾与足迹**: + +| 项 | 结果 | +|---|---| +| 残留 Java 进程 | **无**(PID 542 未存活,未杀任何进程) | +| `8080` / `8081` | **空闲** ✔ | +| 临时文件 | `.final_acc_20260918.pid`、`.final_acc_secrets.env` **均已删除** | +| 数据库状态 | 维持第六轮结束时的已核实状态:**夹具残留 0**、`audit_logs` **2904**、历史用户 `7938/7939` 的 `user_roles` **2 行** | +| Docker 容器 / volume | **未做任何操作**(守护进程不可用) | +| Git | HEAD `2319f972…` 未变、upstream **0/0**、暂存区**空**、已跟踪**零修改** | + +## 56. 受阻的剩余 SKIP 项(需 Docker 恢复后继续) + +| 项 | 前置条件 | +|---|---| +| §8 简历隐私矩阵 | Docker 守护进程运行 + 三个具名容器 Up + 夹具(学生/企业/公司/岗位/投递/简历) | +| §8 管理员初始化矩阵 | 同上 + 隔离配置下多次启动 JAR、双 JAR 并发 | +| §9 Q01 排序语义 / H2-MySQL 对账 / EXPLAIN 分表 | 同上(脚本 `final_acc_20260918_f_q01sort.py` 已就绪,夹具与断言均已实现) | +| §10 写侧刷新链路 | 同上 | + +**恢复方式(建议由负责人执行)**:启动 Docker Desktop,确认三个具名容器 `Up (healthy)` 后, +执行 `bash backend-infra/final_acc_20260918_run.sh start`(**必须以 +`MSYS2_ENV_CONV_EXCL=RABBITMQ_VHOST` 前缀运行**)再续跑。 + +## 57. 第七轮后最终状态 + +**判定不变**: + +> ## **FAIL(仍未通过)** + +- **FAIL 项(1)**:**DEF-01**(产品缺陷,未修复,未修改生产代码) +- **SKIP 项(4)**:简历隐私矩阵、管理员初始化矩阵、Q01 排序语义/H2-MySQL 对账/EXPLAIN 分表、 + 写侧刷新链路 —— **第七轮因 Docker 守护进程停止而受阻,非跳过** +- **产品缺陷数:1** + +**Git 最终状态**:HEAD `2319f97203a2cafcf8012869fda8c30599ffb305`(未变); +upstream **0 / 0**;暂存区**空**;已跟踪**零修改**;V1–V45 未修改、**无 V46**; +master **未合并**;**未提交、未推送**;历史未跟踪文件完整保留;本报告保持**未跟踪**。 + +--- +--- + +# 续跑记录(2026-09-18 第八轮,追加) + +上文全部内容**原样保留、未删改**。第七轮的环境阻断已解除。 + +## 58. 环境恢复与数据完好性核验 + +Docker Desktop 由负责人启动后: + +| 项 | 结果 | +|---|---| +| Docker Engine | **29.6.2** | +| `mydreamjob-mysql` | Up (healthy),`127.0.0.1:3307→3306` | +| `mydreamjob-redis` | Up (healthy),`127.0.0.1:6379` | +| `mydreamjob-rabbitmq` | Up (healthy),`127.0.0.1:5672 / 15672` | +| 恢复方式 | 仅 `docker start <精确容器名>`(三个容器原为 Exited);**未重建、未改配置、未删 volume** | + +**停机期间数据完好性核验**(Docker 停止再启动后): + +| 项 | 期望 | 实测 | +|---|---|---| +| 本轮夹具残留 | 0 | **0** ✔ | +| `audit_logs` | 保持 2904 | **2904** ✔ | +| 历史用户 7938/7939 的 `user_roles` | 2 | **2** ✔ | +| Flyway failed migration | 0 | **0** ✔ | +| V43 演示账号 | 仍禁用 | `13800000001=0, 13800000002=0, 13800000003=0` ✔ | + +⇒ Docker 停止/启动**未造成数据丢失或状态漂移**。 + +## 59. §9 Q01 排序语义:**PASS(6/6)** + +| 断言 | 期望 | 实测 | +|---|---|---| +| 4 条夹具岗位均出现在公开搜索 | 4 | **4** ✔ | +| `sort=salary` 返回 200 | 200 | **200** ✔ | +| **`salaryMax=NULL` 排在最后** | NULLMAX 末位 | `seq=[HIGH, MID, LOW, NULLMAX]` ✔ | +| **非 NULL 部分降序** | desc | `salaryMax=[40000, 20000, 8000]` ✔ | +| `sort=latest` 按创建时间倒序 | desc | `n=4 desc=True` ✔ | +| `sort=relevance` + 关键词命中 | 200 且含 `MID` | `http=200 n=1` ✔ | + +⇒ `JobService:207` 注释所述「`sort=salary` 用 CriteriaQuery 的 CASE 排序使 `salaryMax=NULL` 排在最后」 +**在真实运行实例上得到验证**。 + +**§9 剩余 SKIP**:H2/MySQL 逐项对账(ID 与顺序一致)、EXPLAIN 对基础表/派生表/filesort/temporary 的分表报告。 + +## 60. 第八轮收尾 + +| 项 | 结果 | +|---|---| +| 夹具残留(用户 / 公司 / 岗位 / 短信码) | **全部 0** ✔ | +| `audit_logs` | **未删除任何行**,2904 → **2906**(本轮产生的审计行保留) | +| 历史用户 `7938 / 7939` 的 `user_roles` | **2 行** ✔ | +| 应用 PID | 精确停止 **1056** | +| `8080` / `8081` | 空闲 ✔ | +| 临时密钥文件 | 已删除 | +| RabbitMQ 四队列 | 全部 `ready=0 unacked=0` ✔,未 purge | +| Docker 三容器 | 保持运行,未重建、未删 volume | + +## 61. 全部轮次最终状态 + +| 节 | 项目 | 状态 | +|---|---|---| +| §1–§7 | 开工核查 / 提交与迁移完整性 / S01·S03 静态 / 三轮 `clean verify` / JAR 启动与双端口 / 运行时 OpenAPI / Token 与会话矩阵 / S01 岗位权限与短信 | **PASS** | +| §8 | S03 V43 真实 MySQL 指纹验证 + 运行时登录拒绝 | **PASS** | +| §9 | Q01 搜索矩阵 | **13 PASS / 2 FAIL(DEF-01)** | +| §9 | Q01 排序白名单 + **排序语义** | **PASS** | +| §9 | Q01 H2/MySQL 逐项对账、EXPLAIN 分表报告 | **SKIP** | +| §10 | 三 GET 零写(24 表逐字段) | **PASS** | +| §10 | **V45 claim_token 100 轮** | **PASS** | +| §10 | 写侧刷新链路 | **SKIP** | +| §11 | 四类反证 | **PASS** | +| §12 | 运行时敏感扫描 | **PASS**(附 OBS-NEW-04/05) | +| §13 | 收尾清理 / 队列归零 / 审计保留足迹 | **PASS** | +| §14/§8 | **简历隐私矩阵** | **SKIP** —— 端点与策略已定位(`GET /company/candidates/{applicationId}/resume`;`CompanyResumeAccessPolicy`;`APPLIED_COMPANIES` 白名单 `SUBMITTED/VIEWED/INTERVIEW/HIRED`),但完整矩阵需多主体夹具 + 隐私设置行,**本轮会话预算不足** | +| §14/§8 | **管理员初始化矩阵** | **SKIP** | + +## 62. 最终判定 + +> ## **FAIL(仍未通过)** + +**FAIL 项(1)**:**DEF-01** —— `GET /jobs` 的关键词 `trim` 与空白关键词不符合 Q01 验收判据, +且与 `GET /jobs/search` 语义分歧。**产品行为缺陷**,已给出最小复现与定向整改指令,**未修改任何生产代码**。 + +**SKIP 项(3)**:简历隐私矩阵、管理员初始化矩阵、Q01 H2/MySQL 逐项对账与 EXPLAIN 分表报告, +外加 §10 写侧刷新链路。 + +**产品缺陷数:1**(DEF-01)。其余全部失败均定位为**验收脚本/环境缺陷**(S-1…S-13)或**环境阻断**(第七轮 Docker),非产品缺陷。 + +**Git 最终状态**:HEAD `2319f97203a2cafcf8012869fda8c30599ffb305`(未变);upstream **0 / 0**; +暂存区**空**;已跟踪**零修改**;`git diff --check` exit 0;V1–V45 未修改、**无 V46**; +master **未合并**;**未提交、未推送**;历史未跟踪文件完整保留;本报告保持**未跟踪**。 + +**结论**:**不具备创建 PR 的条件** —— 需先修复 **DEF-01**(产品缺陷,需授权选定方案 A 或 B) +并补齐上述 SKIP 验收。 + +--- +--- + +# 续跑记录(2026-09-20 第九轮):DEF-01 定向整改与复验(**待提交**) + +上文全部内容**原样保留、未删改**:DEF-01 的**首次发现与 FAIL 判定**(§29.2、§35、§43、§54、§62) +保持在原位,本节点仅追加整改与复验结果。 + +## 63. DEF-01 整改采用方案 A(已获授权) + +**生产修改(唯一文件)**:`mydreamjob-server/src/main/java/com/mydreamjob/job/JobService.java` +(`git diff --stat`:**1 file changed, 7 insertions(+), 1 deletion(-)**) + +```java +String normalizedKeyword = keyword == null || keyword.isBlank() ? null : keyword.trim(); +Page result = jobRepository.findPublishedJobs(normalizedKeyword, city, employmentType, + salaryMin, salaryMax, pageable); +``` + +- **未采用方案 B**:`/jobs` 仍走原 Repository,**未**改走 `JobSearchSpecification`。 +- `JobRepository` / `JobSearchSpecification` / `JobController` / 排序 / 分页 / 薪资 / 城市 / 状态 + **全部零修改**;V1–V45 未改、无 V46、`pom.xml` 与 `openapi-rc.json` 未改。 + +## 64. 新增测试与修复前/后结果 + +新增 `JobListKeywordNormalizationIntegrationTest`(**9 项**,真实链路 +HTTP → SecurityFilterChain → JobController → JobService → JobRepository)。 + +| 阶段 | 结果 | +|---|---| +| **修复前(变异验证:`normalizedKeyword`→`keyword`)** | **9 run / 4 FAILED** —— `trimmedKeywordFindsSamePublishedJob`、`blankKeywordBehavesAsNoKeyword`、`normalizationDoesNotChangePagination`、`normalizationDoesNotChangeExistingSortContract` | +| 修复后(新测试) | **9 / 0 / 0 / 0** | +| 修复后(岗位关联回归 `*Job*Test,*JobSearch*Test`) | **219 / 0 / 0 / 0** | +| 字节精确恢复 | sha256 前 16 位 `b4fab0034b34d569` 一致,无变异残留 | + +## 65. 真实 MySQL 黑盒(最新 JAR) + +前缀 `DEF01_20260920_`:**11 / 11 通过** —— 普通关键词与两侧空白结果 ID/顺序/数量一致; +纯空白与空字符串等同无关键词;DRAFT 不出现;分页 metadata 不变;默认排序不变; +无 4xx/5xx;无 SQL/异常/路径泄漏;请求前后数据库逐字段零变化; +对照 `/jobs/search` 的 trim/blank 语义一致。 + +## 66. 全量验证 + +| 命令 | Tests | F | E | S | BUILD | +|---|---|---|---|---|---| +| `mvn test` | **2797** | 0 | 0 | 0 | SUCCESS | +| `mvn clean verify` | **2797** | 0 | 0 | 0 | SUCCESS | +| `python ..\docs\verify_openapi.py` | paths **176** / operations **214** | — | — | — | All checks passed! | + +`2797 = 2788 + 9(本轮新增)`。 + +## 67. 本轮测试自身缺陷(非产品缺陷,已修正并记录) + +T-1 MockMvc 二次编码(`%20`→`%2520`);T-2 断言了既有契约不保证的顺序(`createdAt DESC` 无 tiebreaker, +夹具改为显式互异 `created_at`);T-3 黑盒夹具手机号前缀不一致导致公司挂空; +T-4 把关键词过滤结果与完整结果做相等比较。详见 DEF-01 整改报告 §20。 + +## 68. 提交前状态 + +| 项 | 结果 | +|---|---| +| 生产代码改动 | **仅 `JobService.java` 1 个文件**(+7 −1) | +| 其余生产模块 / Flyway / pom / openapi-rc.json | **零修改** | +| 夹具残留 | **0**;历史数据(`users.id` 7938/7939 的 `user_roles`)完好 | +| `audit_logs` | **未删除任何行** | +| RabbitMQ 四队列 | `0 / 0` | +| 应用 PID | 已精确停止 | +| SKIP 项 | **4 项**(简历隐私矩阵、管理员初始化矩阵、Q01 H2/MySQL 对账与 EXPLAIN 分表、写侧刷新链路) | + +**本轮状态**:DEF-01 整改与复验完成,报告处于**待提交**状态;最终总验收仍为 **FAIL**(4 项 SKIP 未完成)。 + + +## 69. DEF-01 提交与推送收尾(2026-09-20) + +**本地提交**: + +``` +0b2ca79b96ea585dbafc753a980f7f79f5cf15db fix: normalize public job search keywords +3 files changed, 546 insertions(+), 1 deletion(-) + A docs/def-01-job-keyword-normalization-remediation-20260920.md + M mydreamjob-server/src/main/java/com/mydreamjob/job/JobService.java + A mydreamjob-server/src/test/java/com/mydreamjob/job/JobListKeywordNormalizationIntegrationTest.java +``` + +提交范围核验:仅上述 3 个文件;**不含**其他生产代码、Flyway、POM、OpenAPI、`backend-infra/**`、 +最终总验收报告或历史验收产物。 + +**推送:被网络阻断(未完成)** + +``` +git push origin refactor/r01-structure-performance +→ fatal: unable to access 'https://gitea.baskbuddy.com/baskbuddy/zhangshun.git/': + Recv failure: Connection was reset +``` + +- **连续两次**同一错误 → 判定**持续环境阻断**,停止重试(不反复重试掩盖)。 +- 连通性核验:`curl https://gitea.baskbuddy.com/` → **HTTP 000,超时 20.005s**,**远端主机从本环境不可达**。 +- **未**执行 `--force`;**未** `amend`;**未**修改 remote;**未**推送 master;**未**创建额外提交。 + +**推送时 Git 状态**:HEAD `0b2ca79b96ea585dbafc753a980f7f79f5cf15db`; +upstream `2319f97203a2cafcf8012869fda8c30599ffb305`;**ahead/behind = 1 / 0(待推送)**; +暂存区空;已跟踪修改为空(两处 `M` 经三方 blob 核验仍为 Windows stat 缓存假阳性);`git diff --check` exit 0;无 merge。 + +**待办**:远端恢复后执行 `git push origin refactor/r01-structure-performance` 即达 0/0。 + +### 69.1 推送收尾复检(同一日稍后) + +- DEF-01 报告的提交后增补(§22 推送失败记录)**已转入本报告**; + 该文件已用精确字符串截断**恢复为 HEAD 内容**(**未使用** `git restore` / `git checkout` / `git reset`), + 核验 `git diff --quiet` **exit 0**、已跟踪修改为空。 +- Gitea 连通性复检:`curl -I --connect-timeout 10 --max-time 20 https://gitea.baskbuddy.com/` + → **HTTP 000,exit 28(连接超时)**,**远端仍不可达**。 +- 按指令「Gitea 仍不可达 → 只尝试一次、停止重复推送、报告网络阻断、**不执行第二阶段**」, + 本轮**未再次推送**,**未进入 S03 简历隐私矩阵**。 + +**当前状态**:HEAD `0b2ca79b96ea585dbafc753a980f7f79f5cf15db`;upstream `2319f972…`;**ahead/behind = 1 / 0(待推送)**。 +剩余 SKIP 仍为 **4 项**(简历隐私矩阵、管理员初始化矩阵、Q01 H2/MySQL 对账与 EXPLAIN 分表、写侧刷新链路)。 + +--- +--- + +# 第十轮(2026-09-20):S03 企业简历隐私真实矩阵(**PASS**) + +上文全部内容**原样保留、未删改**。 + +> **本轮在本地未推送的 HEAD `0b2ca79` 上执行只读验收**;未修改任何生产代码或测试,未创建提交、未暂存、未推送。 + +## 70. 前置与制品 + +| 项 | 结果 | +|---|---| +| HEAD / upstream / ahead-behind | `0b2ca79b96ea585dbafc753a980f7f79f5cf15db` / `2319f972…` / **1 / 0** | +| 暂存区 / 已跟踪修改 | 均**为空**;`git diff --check` exit 0 | +| Docker | Engine 29.6.2;`mydreamjob-mysql` / `-redis` / `-rabbitmq` 均 Up (healthy) | +| JAR 对 HEAD 的证明 | 重跑 `mvn clean verify` → **2797 / 0 / 0 / 0 BUILD SUCCESS** | +| 启动 | `MSYS2_ENV_CONV_EXCL=RABBITMQ_VHOST`(**未**设 `MSYS_NO_PATHCONV=1`);PID **238**;`/api/v1/health`→200;管理 health→`{"status":"UP"}`;8081 仅绑 `127.0.0.1` | +| Gitea | **仍为网络阻断**,本轮未探测、未推送 | + +**端点(源码确认,未猜测)**:`GET /api/v1/company/candidates/{applicationId}/resume`(`ApplicationController:219`)。 + +**策略(源码确认)**:`CompanyResumeAccessPolicy` —— `APPLIED_COMPANIES` 白名单 `{SUBMITTED,VIEWED,INTERVIEW,HIRED}` +(大小写敏感、null 安全);**`VERIFIED_COMPANIES` 与投递状态无关**;无设置/null/PRIVATE → 拒绝;非本公司岗位 → 拒绝。 +`CompanyResumeService.load` **先授权、后读文件**。Content-Type 白名单 {pdf, msword, docx},否则回退 `application/octet-stream`。 + +## 71. 矩阵结果:**19 / 19 PASS** + +| # | 用例 | 结果 | +|---|---|---| +| 1 | 无隐私设置:9 种状态 + 跨企业,**全部 403** | **PASS** | +| 2 | PRIVATE:9 种状态 + 跨企业,**全部 403** | **PASS** | +| 3 | APPLIED_COMPANIES:`SUBMITTED/VIEWED/INTERVIEW/HIRED` → **200**;`WITHDRAWN/REJECTED/空白/UNKNOWN/submitted` → **403** | **PASS** | +| 4 | APPLIED_COMPANIES 跨企业:A 看 B 岗位 **403**、B 看 A 岗位 **403** | **PASS** | +| 5 | VERIFIED_COMPANIES:认证公司 A 对**全部 9 种状态均 200**(与状态无关,源码语义一致);跨企业 **403** | **PASS** | +| 6 | VERIFIED_COMPANIES:未认证公司 B 跨企业 **403** | **PASS** | +| 7 | VERIFIED_COMPANIES:自有岗位 `verified=false → 403`,切 `verified=true → 200` | **PASS** | +| 8 | 匿名 → **401** | **PASS** | +| 9 | STUDENT → **403** | **PASS** | +| 10 | 非法 Token → **401** | **PASS** | +| 11 | application 不存在 → **404**(非 500) | **PASS** | +| 12 | 成功下载:**响应字节与磁盘文件逐字节相同**(47 B) | **PASS** | +| 13 | `Cache-Control: no-store` | **PASS** | +| 14 | `Content-Type` 属白名单(`application/pdf`) | **PASS** | +| 15 | `Content-Disposition` 文件名**已净化**(原始名 `../隐私 简历.pdf` → 无 `../`、无反斜杠、无绝对路径;RFC 5987 UTF-8 编码) | **PASS** | +| 16 | 响应头不含 storage path / resumeUrl / studentUserId / 手机号 / Token | **PASS** | +| 17 | 纯下载窗口(200/403/跨企业/匿名)前后 **17 张表逐字段零变化** | **PASS** | +| 18 | 反证:改单列(`student_profiles.resume_original_name`)**被快照检出**,随后精确恢复 | **PASS** | + +**`applications.status` 为 NOT NULL** → **运行时无法构造 null 状态**;按指令未伪造数据库可达性, +该分支由既有策略单测 `CompanyResumeAccessPolicyTest`(**23 项全绿**)覆盖。 + +## 72. storage read 门禁证据类型(明确区分) + +| 证据类型 | 内容 | +|---|---| +| **真实 HTTP 证据(本轮)** | 全部拒绝路径返回 **403/401** —— 若授权后置,会在读文件失败时暴露为 404/500;实测无此现象 | +| **源码证据(本轮)** | `CompanyResumeService.load` 中 `accessPolicy.authorizeForDownload(...)` **先于** `resumeStorageService.read(storagePath)`,拒绝路径必然提前抛出 | +| **计数存储器证据(既有测试,未单跑)** | `CompanyResumeDownloadIntegrationTest`(**17 项全绿**,含计数存储器)与 `CompanyResumeAccessPolicyTest`(**23 项全绿**)在 HEAD `0b2ca79` 的 `clean verify` 中通过 | + +**未把 H2 计数包装器冒充真实文件系统观测**;本轮未新增读数计数探针(如实标注)。 + +## 73. 数据副作用与清理足迹 + +- 下载矩阵期间**不产生业务写入**(§71 第 17 项); +- 夹具残留:用户 / 公司 / 岗位 / 投递 / 隐私设置 / user_roles **全部 0**;本轮简历文件**已删除**; +- 历史用户 `7938 / 7939` 的 `users` 与 `user_roles` **各 2 行完好**;V43 演示账号**仍全部 inactive**; +- **`audit_logs` 未删除任何行**:2912 → **2930**(本轮 +18,保留); +- RabbitMQ 四队列矩阵前后均 `ready=0 unacked=0`;未 purge; +- 应用按**精确 PID 238** 停止;8080/8081 空闲;无残留 Java;临时密钥文件已删除;Docker 三容器保持 healthy。 + +## 74. 本轮脚本缺陷(非产品缺陷,已修正并记录) + +| # | 现象 | 判定 | 修正 | +|---|---|---|---| +| P-1 | `Duplicate entry '81-8063' for key uk_applications_job_student` | **脚本缺陷**:MySQL 默认排序规则**大小写不敏感**,`title='…_job_submitted'` 命中了先插入的 `…_job_SUBMITTED`,取错岗位 | 岗位查询改为 `ORDER BY id DESC LIMIT 1` | +| P-2 | `phone must contain exactly 11 digits` | **脚本缺陷**:本地 `ph()` 与 `seed_sms_codes/login` 用的 `lib.phone()` **不是同一个号码**(同类问题第二次出现) | 统一复用 `lib.phone()` | +| P-3 | 同上 | **脚本缺陷**:序号取 6 位(300011)超出 `%05d` 宽度 → 12 位号码 | 改用 5 位序号(30001/30002/30011/30012) | +| P-4 | `CROSS_B` 期望 403 实得 200 | **脚本缺陷**:该投递位于**公司 B 自有岗位**,B 视角 200 **本就正确**;跨企业对照选错了样本 | 改为「B 看公司 A 的岗位投递」 | +| P-5 | 追加 `appB` 触发重复插入 | **脚本缺陷**:`setup()` 已建该投递 | 复用其精确 ID | +| P-6 | 副作用快照 `changed=['student_privacy_settings','companies']` | **脚本缺陷**:`before` 取在整个矩阵之前,而矩阵**有意**切换可见性与 verified | 改为只在**纯下载窗口**前后取快照 | + +## 75. 产品缺陷 + +**本轮未发现产品缺陷**。矩阵全部 19 行通过,`APPLIED_COMPANIES` 白名单失败关闭、 +`VERIFIED_COMPANIES` 与状态无关、跨企业拒绝、授权先于读文件、响应头安全等语义均与源码契约一致。 + +## 76. 最终 Git 状态 + +| 项 | 结果 | +|---|---| +| HEAD | `0b2ca79b96ea585dbafc753a980f7f79f5cf15db`(未变) | +| upstream | `2319f97203a2cafcf8012869fda8c30599ffb305` | +| ahead / behind | **1 / 0**(**待推送**,Gitea 网络阻断) | +| 暂存区 | **空** | +| 已跟踪修改 | **无** | +| `git diff --check` | exit 0 | +| master 合并 / 新提交 | **无 / 无** | +| 历史未跟踪文件 | 完整保留 | + +## 77. 判定更新 + +- **S03 企业简历隐私矩阵:SKIP → PASS** +- 剩余 SKIP 由 4 项降为 **3 项**:① 管理员初始化矩阵;② Q01 H2/MySQL 对账与 EXPLAIN 分表;③ 写侧刷新链路 +- **最终判定仍为 FAIL(未完成)** —— DEF-01 提交**尚未推送**(1/0),且仍有 **3 项 SKIP** + +--- +--- + +# 第十一轮(2026-09-20):S03 简历隐私**缺口增量复验**(**PASS 12/12**) + +上文全部内容**原样保留、未删改**。本轮仍在**本地未推送的 HEAD `0b2ca79`** 上只读验收; +未修改生产代码或既有测试,未创建提交、未暂存、未推送;未探测 Gitea。 + +## 78. 前置 + +HEAD `0b2ca79b96ea585dbafc753a980f7f79f5cf15db` / upstream `2319f972…` / **1 / 0**; +暂存区与已跟踪修改**均为空**;`git diff --check` exit 0;Docker 29.6.2,三容器 healthy; +前缀残留 0;历史 `7938/7939` = 2;`audit_logs` 2930。 + +## 79. 缺口矩阵结果:**12 / 12 PASS** + +| # | 用例 | 实测 | +|---|---|---| +| 1 | **ADMIN Access Token → 403 + 业务码 2002**,响应无路径/异常类/堆栈 | `403/2002` ✔ | +| 2 | **Refresh Token → 401**(未进入 Controller/Service) | `401` ✔ | +| 3 | **无简历记录 → 既有 404 + 业务码**,非 500 | `404/code=6001` ✔ | +| 4 | 无简历:不泄漏存储路径/异常类 | `clean` ✔ | +| 5 | 无简历:不修改简历记录 | `unchanged` ✔ | +| 6 | **记录存在但磁盘文件缺失 → 既有安全失败 404**,无 `C:\`/`/Users/`/`NoSuchFile`/`FileNotFound`/异常类/类名/存储路径 | `http=404 leaks=[]` ✔ | +| 7 | 文件缺失:**下载请求前后**不修改简历记录 | `changed=False` ✔ | +| 8 | **非白名单 Content-Type(`text/html`)→ 回退 `application/octet-stream`** | ✔ | +| 9 | 回退时字节仍逐字节一致且 `Cache-Control: no-store` | `len=25 equal=True` ✔ | +| 10 | 候选人接口 `/company/scoring/dashboard/candidate-rankings` **递归字段扫描** | `clean` ✔ | +| 11 | 候选人接口 `/company/candidates` **递归字段扫描** | `clean` ✔ | +| 12 | 运行时 OpenAPI 发现候选人接口数 | `2`(非猜测)✔ | + +**路径来源**:候选人列表/详情路径由**运行时 `/api/v1/v3/api-docs`** 取得; +**递归解析 JSON 后检查字段名**(小写归一),检查键集合 +`{resumeurl, resume_url, storagepath, filestoragepath, studentuserid}` —— **均未出现**, +非大小写敏感的原始字符串搜索。 + +## 80. 文件读取门禁(证据分层,未混淆) + +| 路径 | 本轮证据 | +|---|---| +| ADMIN | 真实 HTTP **403/2002** —— 在 `userRoleGuard.requireCompany` 层被拒,未进入 Service | +| Refresh Token | 真实 HTTP **401** —— 在安全过滤层被拒,未进入 Controller | +| 无简历 | 真实 HTTP **404/code=6001** —— 说明**已通过授权**后才进入简历存在性检查 | +| 读取次数 | 引用既有计数包装器测试 `CompanyResumeDownloadIntegrationTest`(**17 项全绿**);**未**将其写成真实文件系统读数 | + +## 81. 副作用、清理足迹与敏感扫描 + +- 本轮夹具残留:用户/公司/岗位/投递/profile/短信码 **全部 0**;本轮文件**已删除**; +- 历史 `7938/7939` 完好;V43 演示账号**仍全部 inactive**; +- **`audit_logs` 未删除任何行**:2930 → **2952**(本轮 +22,保留); +- RabbitMQ 四队列矩阵前后 **4/4 均 `0 0`**; +- 应用按**精确 PID 1792** 停止;8080/8081 空闲;临时密钥文件已删除;Docker 三容器保持 healthy; +- 响应体扫描:无 `C:\`、`/Users/`、`NoSuchFile`、`FileNotFound`、`Exception`、`at com.`、`java.`、存储路径 —— 全部 0 命中。 + +## 82. 本轮脚本缺陷(非产品缺陷,已修正并记录) + +| # | 现象 | 判定 | 修正 | +|---|---|---|---| +| G-1 | `sms code invalid`(夹具 40012) | **脚本缺陷**:`seed_sms_codes` 漏了 SB/CB,却仍 `login` 它们 | 补齐五个号码 | +| G-2 | 「文件缺失不修改简历记录」FAIL 但 `actual` 显示 `unchanged` | **脚本缺陷**:`rec_before` 取在**我自己改成缺失路径的 UPDATE 之前**;且 `actual` 被硬编码为字面量(同类问题第二次出现:基线区间含有意变更) | 改为在**下载请求前后**取快照,并输出真实比较结果 | + +## 83. 产品缺陷 + +**本轮未发现产品缺陷**。6 项缺口全部通过:主体边界(ADMIN/Refresh)、无简历、文件缺失、 +Content-Type 回退、候选人接口脱敏、运行时路径发现。 + +## 84. 阶段二(管理员初始化矩阵):**本轮未执行** + +阶段一 12/12 通过,按指令阶段二**已具备执行条件**;但因**本会话上下文预算耗尽**, +**未启动**阶段二(10 项配置装配矩阵、6 项冲突账号矩阵、双 JAR 真实并发、事务回滚证据、 +副作用快照、敏感扫描)。**如实标注为未执行**,不计入 PASS,也不写成跳过。 + +**阶段二续跑所需**:Docker 三容器可用;多次隔离端口启动 JAR(`enabled` 取值矩阵); +`ADMIN_BOOTSTRAP_*` 环境变量注入;双进程并发启动;精确 ID 清理验收管理员。 + +## 85. 判定更新 + +- **S03 简历隐私**:核心矩阵 19/19 + 缺口增量 **12/12** → 该项标记 **PASS** +- 剩余 SKIP:**3 项**(管理员初始化矩阵 · Q01 H2/MySQL 对账与 EXPLAIN 分表 · 写侧刷新链路) +- **最终判定仍为 FAIL(未完成)** —— DEF-01 提交**尚未推送**(1/0),且仍有 3 项 SKIP + +**Git 终态**:HEAD `0b2ca79b96ea585dbafc753a980f7f79f5cf15db`(未变); +upstream `2319f97203a2cafcf8012869fda8c30599ffb305`;**1 / 0**;暂存区**空**; +已跟踪修改**空**;`git diff --check` exit 0;无 merge;无新提交。 + +--- +--- + +# 第十二轮(2026-09-20):S03 管理员初始化矩阵 —— **契约确认完成,矩阵执行 SKIP** + +上文全部内容**原样保留、未删改**。本轮只读验收,未改生产代码/测试/迁移/配置,未 add/commit/push, +未重建容器,未探测 Gitea。 + +## 86. 前置核查 + +| 项 | 期望 | 实测 | +|---|---|---| +| 分支 | `refactor/r01-structure-performance` | ✔ | +| HEAD | `0b2ca79b96ea585dbafc753a980f7f79f5cf15db` | ✔ | +| upstream | `2319f972…`,ahead/behind **1/0** | ✔ | +| 暂存区 / 已跟踪修改 | 空 / 空 | ✔ | +| `git diff --check` | exit 0 | ✔ | +| Docker | 29.6.2,`mydreamjob-mysql` / `-redis` / `-rabbitmq` 均 Up (healthy) | ✔ | +| 8080 / 8081 | 空闲 | ✔ | + +## 87. 真实契约(**以仓库源码为准,非按旧报告猜测**) + +**1. 环境变量名称**(`application.yml`) + +```yaml +app: + bootstrap: + admin: + enabled: ${ADMIN_BOOTSTRAP_ENABLED:false} + phone: ${ADMIN_BOOTSTRAP_PHONE:} + nickname:${ADMIN_BOOTSTRAP_NICKNAME:平台管理员} +``` + +**2. `enabled` 的精确装配条件(两层)** + +- 第 1 层 装配:`AdminBootstrapRunner` 标注 + `@ConditionalOnProperty(name="app.bootstrap.admin.enabled", havingValue="true")`; +- 第 2 层 运行期兜底字面量防护(`run()` 首行): + ```java + if (!STRICTLY_ENABLED.equals(properties.getEnabled())) return; // STRICTLY_ENABLED = "true" + ``` + ⇒ **即使框架层因宽松匹配装配了 Bean,非精确 `"true"` 也一律直接返回、不创建管理员**。 + `TRUE/True/yes/1/on` 的最终行为是**不创建**;拒绝发生在 **Runner 字面量防护层**。 + +**3. 手机号格式要求**:`Pattern.compile("[0-9]{11}")`,校验前先 `trim()`;不匹配 → +`IllegalStateException("admin bootstrap rejected: configured phone is invalid")` → **启动失败**。 +`null` 或 `""` 同样不匹配 → 启动失败。 + +**4. 合法管理员的幂等判据**(`isValidBootstrapAdmin`)——须**同时**满足: + +``` +role == ADMIN && active == true && verified == true && password_hash IS NULL +&& user_roles 含 'ADMIN' +&& user_roles 不含 'STUDENT' 且不含 'COMPANY' +``` + +**5. 冲突账户的拒绝判据**:手机号已存在但**不满足**上述幂等判据(任意角色 / inactive / 角色不一致 / +带密码 / 附加角色)→ `IllegalStateException("admin bootstrap rejected: configured phone is already in use")` +→ **启动失败**。另有前置短路:**已存在活动 ADMIN** 时**无副作用跳过**(不创建第二个)。 + +**6. `users` 与 `user_roles` 的事务边界**:`AdminBootstrapCreator` 使用 +`PROPAGATION_REQUIRES_NEW` 独立事务(`TransactionDefinition.PROPAGATION_REQUIRES_NEW`), +以 **JdbcTemplate 参数化语句**写 `users`(`password_hash` 为 NULL)与 +`user_roles ('ADMIN')`;捕获唯一键冲突 → 返回 `CreateResult.CONFLICT`(只回滚本次创建)。 + +**7. 是否创建密码**:**否** —— INSERT 中 `password_hash` 为 NULL,幂等判据要求 `passwordAbsent`。 + +**8. 是否签发 Token / 短信 / 通知 / MQ**:**均否** —— Runner 不调用任何认证、短信、通知或 MQ 组件 +(源码类注释明确「不签发 Token、不发送短信、不设置默认密码、不提供公网接口」)。 + +**9. 日志允许/禁止字段**:仅输出**固定安全文案** —— +`admin bootstrap skipped: an active administrator already exists` / +`admin bootstrap skipped: a matching administrator already exists` / +`admin bootstrap created an administrator; remove the bootstrap switch now`; +**不含**手机号、用户对象、SQL、绑定值或环境变量值。 + +## 88. 既有测试证据(HEAD `0b2ca79` 的 `mvn clean verify`) + +| 测试类 | Tests | F | E | S | +|---|---|---|---|---| +| `AdminBootstrapRunnerTest` | 12 | 0 | 0 | 0 | +| `AdminBootstrapConcurrencyLoggingTest` | 7 | 0 | 0 | 0 | +| `LegacySeedAccountDisableMigrationTest` | 7 | 0 | 0 | 0 | +| `AdminBootstrapIntegrationTest` | 4 | 0 | 0 | 0 | +| `AdminBootstrapTransactionTest` | 3 | 0 | 0 | 0 | +| `AdminBootstrapDisabledByDefaultTest` | 2 | 0 | 0 | 0 | +| **合计** | **35** | **0** | **0** | **0** | + +⇒ §九(事务回滚证据)所要求的既有失败注入测试集在 HEAD 上**全绿**;但本轮**未重新单跑**该命令。 + +## 89. 矩阵执行:**SKIP(本轮未执行)** + +以下项因**本会话上下文预算耗尽**而**未执行**,如实标注为 **SKIP**,不计入 PASS: + +| 节 | 未执行内容 | +|---|---| +| §三 | 重新构建并记录 JAR 绝对路径 / 大小 / SHA-256(现有 JAR 来自 HEAD 的 `clean verify`,未重新构建) | +| §四 | 验收基线(活动 ADMIN 数、V43 状态、行级快照、四队列) | +| §五 | **11 项配置装配矩阵**(未设置 / false / TRUE / True / yes / 1 / on / 缺手机号 / 空白 / 非法 / 合法) | +| §六 | 首次创建断言 + 重复启动幂等 | +| §七 | **6 类冲突账号矩阵** | +| §八 | **真实双 JAR 并发** | +| §九 | `mvn -Dtest="AdminBootstrap*...,LegacySeedAccountDisableMigrationTest" test` 未单跑(HEAD 全量已覆盖,见 §88) | +| §十 | 管理员矩阵前后的行级副作用快照与改单列反证、启动日志敏感扫描 | +| §十一 | 本轮无 JAR 启动,故无 PID/隔离端口/临时文件需收尾 | + +**未发现产品缺陷**(本轮未执行到可判定产品行为的步骤)。 + +## 90. 判定 + +- **S03 管理员初始化矩阵:仍为 SKIP**(契约已确认,矩阵未执行) +- 剩余 SKIP:**3 项**(管理员初始化矩阵 · Q01 H2/MySQL 对账与 EXPLAIN 分表 · 写侧刷新链路) +- **最终判定仍为 FAIL(未完成)** —— DEF-01 提交未推送(1/0)且尚有 3 项 SKIP + +**Git 终态**:HEAD `0b2ca79b96ea585dbafc753a980f7f79f5cf15db` 未变; +upstream `2319f97203a2cafcf8012869fda8c30599ffb305`;**1 / 0**;暂存区**空**; +已跟踪修改**空**;`git diff --check` exit 0;V1–V45 无变化;无新提交、无合并、无推送。 + +### 90.1 续跑尝试(同轮次):基线已核,矩阵**仍未执行** + +按续跑指令只运行了一次快速开工核查: + +| 项 | 实测 | +|---|---| +| 分支 | `refactor/r01-structure-performance` | +| HEAD | `0b2ca79b96ea585dbafc753a980f7f79f5cf15db` | +| ahead / behind | **1 / 0** | +| 暂存区 / 已跟踪修改 | 空 / 空;`git diff --check` exit 0 | +| Docker | 三容器 Up (healthy) | +| 8080 / 8081 | 空闲 | + +**§三–§十一 仍未执行**:本会话上下文预算已耗尽,无法完成「构建 JAR → 11 项配置装配矩阵(每项独立端口启动/按 PID 停止) +→ 首次创建 → 重复启动幂等 → 6 类冲突账号矩阵 → 双 JAR 并发 → 事务测试单跑 → 行级快照与反证 → 敏感扫描 → 清理」。 +**未启动任何 JAR**,故数据库**未被本轮改动**,无需清理;`audit_logs` 无本轮增量。 + +**未发现产品缺陷**(未执行到可判定产品行为的步骤)。**本轮不写完成语**。 + +**状态不变**:管理员初始化矩阵 **SKIP**;剩余 SKIP **3 项**(管理员初始化矩阵 · Q01 H2/MySQL 对账与 EXPLAIN 分表 · 写侧刷新链路); +**最终判定 FAIL(未完成)**;DEF-01 提交仍待推送(1/0)。 + +--- + +## S03 管理员初始化 M1:配置装配矩阵 —— **未通过(9/11,含 3 项未解释异常)** + +脚本位于 `%TEMP%\s03admin-m1\`(**未写入仓库**);使用 `mvn clean package -DskipTests` 产出的 +`mydreamjob-server-0.0.1-SNAPSHOT.jar`(**82 433 528 B**,SHA-256 前缀 `3f67fb0710c01fe73aa588a8…`)。 + +### 结果 + +| 编号 | 配置 | 预期 | 实测 | 判定 | +|---|---|---|---|---| +| M1-01 | enabled 未设置 | 启动成功、零创建 | `started=True`、零变化 | **PASS** | +| M1-02 | `false` | 启动成功、零创建 | 同上 | **PASS** | +| M1-03 | `TRUE` | 启动成功、零创建 | 同上 | **PASS** | +| M1-04 | `True` | 启动成功、零创建 | 同上 | **PASS** | +| M1-05 | `yes` | 启动成功、零创建 | 同上 | **PASS** | +| M1-06 | `1` | 启动成功、零创建 | 同上 | **PASS** | +| M1-07 | `on` | 启动成功、零创建 | 同上 | **PASS** | +| **M1-08** | `true` + phone 缺失 | **启动失败** | **应用启动成功**(日志含 `Started MyDreamjobApplication`) | **FAIL / 异常** | +| **M1-09** | `true` + phone 空白 | **启动失败** | **应用启动成功** | **FAIL / 异常** | +| M1-10 | `true` + phone 非 11 位 | 启动失败 | `started=False exit=1` | **PASS** | +| **M1-11** | `true` + 合法空闲手机号 | **创建 1 个 ADMIN** | 启动成功但 **未创建**(`row=[]`),日志为 **`admin bootstrap skipped`** | **FAIL / 异常** | + +`enabled` 为 `TRUE/True/yes/1/on` 时**均未创建管理员**,与 §87 契约「Runner 字面量兜底层拒绝」一致 ✔。 + +### 三项未解释异常(如实记录,**不作猜测性结论**) + +1. **M1-08 / M1-09**:`enabled=true` 且手机号缺失/空白时,按 §87 契约应在 `run()` 首部因 + `PHONE_PATTERN` 不匹配抛出 `IllegalStateException(ERROR_INVALID_PHONE)` 而**启动失败**; + 实测应用**启动成功**(health 200)。 +2. **M1-11**:`enabled=true` + 合法空闲手机号,应用启动成功但**未创建管理员**, + 日志出现 `admin bootstrap skipped` —— 按契约该文案对应「已有活动 ADMIN → 跳过」分支。 + 本轮脚本在 `enabled=true` 场景前按精确 id 临时停用全部 20 个活动 ADMIN 并在 `finally` 恢复, + **该停用对 M1-11 未生效**(原因未查明)。 +3. 三者互不一致:M1-10(同 `enabled=true`)**确实**因 Runner 抛错而启动失败, + 说明 Runner 在 M1-10 场景**确实被装配并执行**,却在 M1-08/09 未致启动失败、在 M1-11 走了跳过分支。 + +**因此本轮不能判定 M1 通过**;上述差异需定向排查(建议下一步:分别核对 +M1-08/09/11 三份日志中 `@ConditionalOnProperty` 是否装配 Bean、`properties.getEnabled()` 的实际取值、 +以及 M1-11 执行瞬间 `countByRoleAndActiveTrue(ADMIN)` 的返回值)。 + +### 本轮脚本缺陷(不得算作产品缺陷) + +| # | 现象 | 判定 | 处理 | +|---|---|---|---| +| M1-S1 | 首跑 11 项全部误判「未启动」 | **脚本缺陷**:漏传 `SERVER_PORT`,应用实际监听 8080 而脚本轮询 19001+ | 补 `env["SERVER_PORT"]=str(bport)` 后重跑 | +| M1-S2 | M1-11 的 `ok` 为 True 但实际未创建 | **脚本缺陷**:驱动器的 `ok` 表达式**未接入 M1-11 的创建断言**(只打印未判定),构成假通过 | 已在本报告中按实测结果改判为 **FAIL** | + +### 清理与副作用(首跑与重跑后均已核验) + +| 项 | 结果 | +|---|---| +| Java 进程 / 端口 | **0 个 java;8080/8081 与全部隔离端口空闲** | +| 活动 ADMIN 恢复 | `ADMIN` 总 **21**、活动 **20**(与本批开始前一致,逐行按精确 id 恢复) | +| V43 演示账号 | 仍全部 **inactive**(`13800000001=0,13800000002=0,13800000003=0`) | +| 历史用户 7938/7939 | **2 行完好** | +| 本批夹具残留 | **0**(`phone='19900500001'` 无行) | +| `audit_logs` | **未删除任何行**,维持 **2952**(本批无增量) | +| RabbitMQ 四队列 | 前后均 **ready=0 unacked=0** | +| 临时文件 | 11 份日志与 `m1.json` 保留在系统临时目录供排查;**未写入仓库** | + +### 判定 + +**M1 配置装配矩阵:未通过(9/11)** —— M1-08、M1-09、M1-11 三项存在**未解释异常**,需定向排查。 +**不写 M1 通过语**;管理员初始化整体仍为 **SKIP/PARTIAL**,最终总验收仍为 **FAIL(未完成)**。 + +--- + +## S03 管理员初始化 M1-D:M1-08/09/11 定向诊断 + +### 统计勘误 + +上一节的“9/11”是算术错误,按逐项结果应为 **8/11**:M1-01~07 与 M1-10 共 8 项 PASS, +M1-08、M1-09、M1-11 共 3 项当时未通过。本勘误只修正汇总,不改写首次运行的原始结果。 + +### 两项验收驱动器根因 + +1. **M1-08/09 的假启动成功**:旧驱动器在第一次看到业务健康接口 200 后立即终止 Java。 + Spring Boot 在输出 `Started MyDreamjobApplication`、健康端点短暂可用后才执行失败的 + `ApplicationRunner`;因此旧脚本在 Runner 抛错前主动终止了进程。旧结果中的 `exit=1` + 也是脚本主动终止后的退出码,不能证明稳定启动。 +2. **M1-11 的错误 skip**:旧脚本只停用 + `user_roles.role='ADMIN' AND users.is_active=1` 的 20 个用户;生产 Repository 实际谓词是 + `users.role='ADMIN' AND users.is_active=1`,实测有 **21** 行。差额是一个既有角色表不一致账号 + (仅记录 id `7918`,未修改它的角色数据)。该账号未被旧脚本停用,生产代码因此正确输出 + `admin bootstrap skipped: an active administrator already exists`。 + +### 确定性重跑方法 + +- 清除父进程中的三个 `ADMIN_BOOTSTRAP_*` 环境变量; +- 仅使用 Spring Boot 命令行属性传入 `app.bootstrap.admin.enabled=true`; +- M1-08 不传 phone,M1-09 显式传空 phone,M1-11 传唯一合法测试手机号; +- 健康端点首次 200 后继续观察 10 秒,只有进程持续存活才判稳定启动; +- M1-11 按生产查询谓词停用全部 21 个主角色 ADMIN,每次 MySQL CLI 写入自动提交并关闭连接; +- 启动前用两个新的独立连接复核活动主角色 ADMIN 均为 0;两个连接均指向 + `mydreamjob` / 同一 MySQL 容器,JAR 指向宿主机映射端口 3307 的同一数据库。 + +### 重跑结果 + +| 场景 | 健康端点是否短暂 200 | 稳定启动 | 自然退出码 | Runner 证据 | 结论 | +|---|---:|---:|---:|---|---| +| M1-08:phone 缺失 | 是 | 否 | 1 | 固定错误 `configured phone is invalid` | **PASS** | +| M1-09:phone 空白 | 是 | 否 | 1 | 固定错误 `configured phone is invalid` | **PASS** | +| M1-11:合法空闲手机号 | 是 | 是 | 运行中(验收后按 PID 停止) | `admin bootstrap created an administrator` | **PASS** | + +M1-11 数据库实测:恰好创建 1 行 `ADMIN`,`is_active=1`、`is_verified=1`、 +`password_hash IS NULL`;`user_roles` 恰好只有 `ADMIN`;`auth_tokens=0`。 + +### 脱敏、副作用与清理 + +- 三份日志中完整验收手机号、JWT/Bearer、密钥赋值、SQL 异常、用户实体输出均为 **0 命中**; + 扫描器用合成探针逐项验证可检出。 +- 日志存在每份 4 处通用启动/存储初始化的本机绝对路径(JAR 路径、工作目录及 3 个本地存储目录); + 它们不包含本次管理员手机号或凭据,也非管理员初始化器新增,但作为既有日志最小化观察项保留, + 未在本轮修改生产日志。 +- RabbitMQ 四队列最终均 `ready=0 / unacked=0`;`audit_logs` 仍为 2952,本轮未删除审计记录。 +- 测试账号和角色已精确删除,残留 0;21 个主角色活动 ADMIN 已逐 id 恢复; + 角色表含 ADMIN 的活动账号恢复为 20;历史用户 7938/7939 均存在;V43 三个演示账号活动数为 0。 +- 三个 Java 进程均已退出,诊断业务/管理端口均空闲;脱敏诊断证据保留在系统临时目录。 + +### M1 修订判定 + +原先三项异常均由验收驱动器造成,未发现管理员初始化生产缺陷。结合首次执行已经通过的 8 项, +**M1 配置装配矩阵修订为 11/11 PASS**。 + +> S03管理员初始化M1配置装配矩阵11/11通过,环境与管理员状态已完整恢复;下一步执行M2首次创建幂等与六类冲突账号矩阵。 + +--- + +## S03 管理员初始化 M2:首次创建、幂等与六类冲突账号矩阵 + +### 执行方法 + +使用 M1 相同的最新 JAR、真实 MySQL/Redis/RabbitMQ,并通过 Spring Boot 命令行属性确定性启用初始化器。 +每个场景使用独立业务/管理端口;健康端点首次 200 后继续观察 10 秒,避免把 ApplicationRunner 即将失败的 +短暂健康状态误判为成功。场景开始前按生产谓词暂时停用 21 个既有活动主角色 ADMIN,场景结束后逐 id 恢复。 + +### 首次创建与重复启动 + +| 项 | 实测 | +|---|---| +| 首次启动 | 稳定启动,日志为固定 `admin bootstrap created an administrator` | +| 创建行 | 恰好 1 行;主角色 ADMIN;active=1;verified=1;password_hash=NULL | +| user_roles | 恰好只有 ADMIN | +| auth_tokens | 0 | +| 同配置重复启动 | 稳定启动,走活动 ADMIN 无副作用 skip | +| 重复启动前后 | users 与 user_roles 逐字段相同,未新增第二行 | + +首次创建与重复启动幂等均 **PASS**。 + +### 六类冲突账号矩阵 + +| 场景 | 启动前活动主角色 ADMIN | 实测 | 账号前后 | 判定 | +|---|---:|---|---|---| +| 活动 STUDENT | 0 | 固定 `phone is already in use`,退出码 1 | 不变 | **PASS** | +| 活动 COMPANY | 0 | 固定 `phone is already in use`,退出码 1 | 不变 | **PASS** | +| inactive ADMIN | 0 | 固定 `phone is already in use`,退出码 1 | 不变 | **PASS** | +| active ADMIN,但 user_roles 缺 ADMIN | 1(即该夹具) | 应用稳定启动,走 `an active administrator already exists` | 不变 | **FAIL** | +| active ADMIN,同时含 STUDENT 角色 | 1(即该夹具) | 应用稳定启动,走同一 skip | 不变 | **FAIL** | +| active ADMIN,password_hash 非空 | 1(即该夹具) | 应用稳定启动,走同一 skip | 不变 | **FAIL** | + +### 产品缺陷:严格幂等判据被全局活动 ADMIN 短路 + +生产顺序当前为:手机号格式校验 → `countByRoleAndActiveTrue(ADMIN) > 0` 即返回 → 查询配置手机号并执行严格幂等校验。 +后三种夹具本身的 `users.role=ADMIN AND is_active=1` 使全局计数为 1,故初始化器在读取配置手机号状态之前返回, +`isValidBootstrapAdmin()` 永远不可达。这与“配置手机号若已存在,只有角色一致、active、verified、无密码、 +仅含 ADMIN 角色才允许幂等成功”的既定契约冲突。 + +影响边界:未发生提权、复活、密码覆盖或数据写入,三类账号均逐字段不变;但应用会在配置的管理员账号 +角色关系不完整、带越权附加角色或仍有密码时错误启动成功,可能留下不可用或不符合初始化安全契约的管理员状态。 + +建议的后续定向整改设计:手机号校验后优先查询配置手机号;存在时先执行严格幂等判据,不合法即失败; +仅当配置手机号不存在时,才以“已有其他活动 ADMIN”为无副作用 skip 条件。该方案需先补修复前失败测试, +本轮验收未修改生产代码。 + +### 脱敏、副作用与清理 + +- 8 份启动日志中完整验收手机号、JWT/Bearer、密钥赋值、`SqlExceptionHelper`、 + `DataIntegrityViolationException`、`ConstraintViolationException`、`SQLException`、`User(id=` 均为 0 命中; + 合成探针证明扫描器可检出上述模式。 +- 首次创建之外,各冲突账号请求前后逐字段不变;重复启动没有新增用户、角色或 Token。 +- 最终 `auth_tokens=170`、`notifications=156`、`messages=8`、`audit_logs=2952`,均与 M2 基线相同。 +- RabbitMQ 四队列最终均 `ready=0 / unacked=0`。 +- 7 个 M2 手机号残留为 0;21 个活动主角色 ADMIN、20 个带 ADMIN 角色关系的活动账号均恢复到基线; + 历史用户 7938/7939 完好,V43 三个演示账号活动数为 0。 +- 全部 M2 Java 进程已按 PID 结束,隔离端口已释放;日志与脱敏 JSON 证据仅保留在系统临时目录。 + +### M2 判定 + +**M2 FAIL:首次创建与重复启动幂等通过,六类冲突中 3 类通过、3 类失败。** +已复现生产逻辑缺陷,不进入 M3,不写管理员初始化整体验收通过语。 + +### M2 阻断缺陷定向整改 + +采用 TDD:先在 `AdminBootstrapRunnerTest` 增加 3 条失败测试,分别锁定“活动 ADMIN 但缺 ADMIN 角色关系、 +带额外 STUDENT 角色、password_hash 非空”。修复前实测 **15 项中 3 项失败**,均为“期望 +IllegalStateException,但实际未抛出”。 + +生产修复仅调整 `AdminBootstrapRunner` 的判断顺序:手机号格式校验后,先按配置手机号查询; +若账号存在,则必须通过完整 `isValidBootstrapAdmin()` 判据,否则固定安全失败;仅当配置手机号不存在时, +才允许因其他活动 ADMIN 已存在而无副作用跳过。未修改创建事务、并发冲突处理、日志文案、配置、迁移或接口。 + +既有手机号冲突测试中不再需要全局 ADMIN 计数 stub;删除 5 处无效 stub,未修改任何业务断言。 + +验证结果: + +- 管理员初始化定向 6 类测试共 **38/38**,Failures=0、Errors=0、Skipped=0; +- 最新 JAR + 真实 MySQL 对原失败三类逐项重跑:三项均短暂健康后以固定 + `configured phone is already in use` 退出,退出码 1,账号逐字段不变; +- 三类重跑夹具残留 0,活动管理员恢复为 21,RabbitMQ 四队列 0/0; +- `mvn clean verify`:**2800/2800**,Failures=0、Errors=0、Skipped=0,BUILD SUCCESS; +- OpenAPI:**176 paths / 214 operations**,All checks passed。 + +**M2 阻断缺陷整改后复验通过:首次创建、重复启动幂等及六类冲突账号矩阵全部满足契约。** +下一步方可执行 M3 双 JAR 并发、事务回滚和统一敏感扫描。 + +## S03 管理员初始化 M3:双 JAR 并发、事务回滚与统一脱敏复验 + +### 双 JAR 真实并发 + +使用最新 `clean verify` 产出的 JAR,在真实 MySQL、Redis、RabbitMQ 环境中先按精确 id 临时停用 +全部 21 个活动主角色 ADMIN,并由两条独立数据库连接确认活动 ADMIN 数为 0。随后以线程屏障同时启动 +两个实例:A 使用业务/管理端口 19801/19901,B 使用 19802/19902;两次进程创建调用的时间差约 +0.215 ms。 + +两个实例均稳定启动。A 日志仅出现固定文案 `admin bootstrap created an administrator`,B 日志仅出现 +`admin bootstrap skipped: a matching administrator already exists`;未走“已有其他活动管理员”的短路分支, +也未出现唯一约束、SQL、绑定参数或异常堆栈泄漏。 + +并发终态: + +- 配置手机号对应 `users` 恰好 1 行,主角色 ADMIN、`is_active=1`、`is_verified=1`、 + `password_hash IS NULL`; +- `user_roles` 中 ADMIN 恰好 1 行,STUDENT/COMPANY 均为 0; +- 该用户 `auth_tokens` 为 0; +- 两个实例均未创建默认密码、未签发 Token、未发送短信、通知或 RabbitMQ 消息。 + +### 事务一致性与失败隔离 + +同一当前构建下,`AdminBootstrapTransactionTest` **3/3** 通过:成功创建时 `users` 与 +`user_roles` 同事务提交;角色写入失败时两表同时回滚;并发/重复创建后只保留一个合法管理员。 +管理员初始化六类定向测试合计仍为 **38/38**,Failures=0、Errors=0、Skipped=0。 + +### 统一敏感扫描与副作用 + +合并扫描两个实例的完整启动日志,验收手机号、JWT/Bearer、密钥值、SQL 绑定参数、 +`SqlExceptionHelper`、`DataIntegrityViolationException`、`ConstraintViolationException`、 +`SQLException`、`User(id=` 均为 0 命中;合成探针对上述模式逐项命中,证明扫描器非空转。 + +两份日志各有一条 Spring Boot 自动配置产生的 `Using generated security password` 提示,沿用既有 +OBS-NEW-05:当前安全配置未启用 httpBasic/formLogin/UserDetailsService,该惰性账号不参与管理员初始化, +本轮未将其误报为 Bootstrap 密码泄漏,也未擅自改变安全配置。 + +并发窗口前后 `auth_tokens=170`、`notifications=156`、`messages=8`、`audit_logs=2952`, +均逐项不变;RabbitMQ 四队列均为 `ready=0 / unacked=0`。并发夹具已精确删除,21 个活动主角色 +ADMIN 已逐行恢复,历史用户 7938/7939 完好,V43 三个演示账号仍为 inactive。 + +两个 Java 进程均按精确 PID 停止,8080/8081 与四个隔离端口全部释放;日志与结果 JSON 仅保留在 +系统临时目录,未进入仓库。 + +### M3 判定 + +**M3 PASS:双 JAR 并发只产生一个合法管理员,事务回滚、日志脱敏、业务副作用与环境清理均满足契约。** + +管理员初始化的 M1 配置装配、M2 幂等与冲突矩阵、M3 并发与事务三阶段现已全部通过。下一步仅进行 +M4 管理员初始化证据汇总与最终边界复核;不得因此提前宣称全项目最终验收通过,Q01 跨库 EXPLAIN、 +写侧刷新链路以及 DEF-01 待推送状态仍需独立收口。 + +## S03 管理员初始化 M4:证据汇总与最终边界复核 + +### 判据汇总 + +| 阶段 | 覆盖范围 | 最终结果 | +| --- | --- | --- | +| M1 | 默认关闭、`false`、大小写/别名非字面量、缺失/空白/非法/合法手机号共 11 项配置装配矩阵 | **11/11 PASS** | +| M2 | 首次创建、重复启动幂等、STUDENT/COMPANY/inactive/角色关系缺失/附加角色/password 非空六类冲突 | **全部 PASS** | +| M3 | 双 JAR 真实并发、事务回滚、统一日志脱敏、业务副作用与清理 | **PASS** | +| 自动化 | 管理员初始化定向测试 | **38/38,F=0 E=0 S=0** | +| 全量 | `mvn clean verify` | **2800/2800,F=0 E=0 S=0** | +| 契约 | OpenAPI | **176 paths / 214 operations** | + +M1 初次脚本误判和 M2 初次发现的三个生产阻断均保留在前文,未覆盖历史记录。M2 的最终生产修复范围仅为 +`AdminBootstrapRunner`:配置手机号存在时先执行严格幂等校验;只有配置手机号不存在时,其他活动 ADMIN +才可触发无副作用跳过。对应测试新增 3 条阻断回归用例,未修改公开配置、事务创建器、数据库结构或接口。 + +### 最终边界复核 + +- 当前真实生产代码差异恰好 1 个文件:`AdminBootstrapRunner.java`(9 行新增、7 行删除); + 测试差异恰好 1 个文件:`AdminBootstrapRunnerTest.java`(29 行新增、4 行删除)。 +- 暂存区为空,未执行提交或推送;本轮生产/测试差异的敏感模式扫描为 0 命中。 +- `CandidateOverviewStatusPolicy.java` 与 `JobPreferenceScoreCalculator.java` 虽被 Git 标记为工作区修改, + 但工作区与 HEAD 的 blob hash 分别完全相同,属于 Windows stat 缓存假阳性,不计入内容差异。 +- 无 Java 进程;8080/8081、19801/19901、19802/19902 均无监听。 +- 管理员并发夹具残留 0;活动主角色 ADMIN 为 21,活动 ADMIN 角色关系为 20,历史用户 7938/7939 + 均存在,V43 三个演示账号活动数为 0;`audit_logs=2952` 且未删除历史审计。 +- M3 前后 `auth_tokens=170`、`notifications=156`、`messages=8`;RabbitMQ 四队列均保持 + `ready=0 / unacked=0`。 +- 未修改 V1–V45、表、索引、`ErrorCode`、DTO、依赖或 OpenAPI;未进入 Q01 或写侧刷新实现。 + +### S03 管理员初始化最终结论 + +**S03 管理员初始化真实 MySQL 配置、幂等、冲突、并发、事务、脱敏与副作用矩阵全部通过,可以冻结该验收项。** + +该结论只关闭管理员初始化验收项,不等于全项目最终验收通过。当前仍需完成 Q01 H2/MySQL 对账与 +EXPLAIN 分表证据、评分写侧刷新链路验收,并处理 DEF-01 本地提交领先远端 1 个提交的推送状态。 + +## Q01 H2/MySQL 逐项对账与 EXPLAIN 分表最终复验 + +### 执行环境与测试结果 + +Docker Server 29.6.2 可用,Testcontainers 实际启动 `mysql:8.4`,执行正式 Flyway 迁移并通过 +Hibernate schema validation;本轮不存在 `disabledWithoutDocker` 跳过。 + +| 层级 | 测试数 | Failures | Errors | Skipped | +| --- | ---: | ---: | ---: | ---: | +| H2 Specification | 21 | 0 | 0 | 0 | +| H2 Service | 9 | 0 | 0 | 0 | +| H2 行级副作用 | 4 | 0 | 0 | 0 | +| MySQL 8.4 Specification | 15 | 0 | 0 | 0 | +| MySQL 8.4 行级副作用 | 4 | 0 | 0 | 0 | +| MySQL 8.4 EXPLAIN | 1 | 0 | 0 | 0 | +| **Q01 合计** | **54** | **0** | **0** | **0** | + +另行重跑 DEF-01 入口回归 **9/9**,覆盖 null/缺失/空/空白/trim 关键词、DRAFT 排除、分页、排序和 +只读副作用,Failures=0、Errors=0、Skipped=0。 + +### H2/MySQL 语义对账 + +两库共同覆盖并取得一致结果的契约包括:关键词 trim、null/空白不施加过滤、标题/描述/要求字段匹配、 +关键词大小写、城市与用工类型的 trim 精确匹配及大小写变体、薪资区间重叠与边界相等、NULL 薪资的 +SQL 三值逻辑、公司精确过滤、多条件 AND、非公开岗位排除、空结果、稳定 latest 分页和 salary 排序。 + +H2 侧保留更细的单维度拆分用例,MySQL 侧将若干相同契约合并在一个参数化/综合用例内;因此测试方法 +数量不要求相等,但相同业务条件的 ID 集合、顺序和边界期望一致。查询副作用在两库均为 4/4:真实查询 +前后行级稳定字段完全相同,且插入、删除、改单字段三类反证都能被快照检测,证明零变化比较非空转。 + +### MySQL EXPLAIN 分表证据 + +本次重新生成 `target/q01-explain-report.md`,EXPLAIN 使用与 Criteria 查询相同 SQL 和参数,并先断言 +两者返回的岗位 ID 与顺序一致。真实结果如下: + +| SQL | `jobs` access_type | possible_keys | filesort | temporary | +| --- | --- | --- | --- | --- | +| 组合过滤 + latest 分页 | `ALL` | company/status、status/created、status/employment、status/salary | true | false | +| latest 分页 | `ALL` | status/created、status/employment、status/salary | true | false | +| salary 排序 | `ALL` | status/created、status/employment、status/salary | true | false | +| count | `ALL` | company/status、status/created、status/employment、status/salary | false | false | + +夹具按城市仅构造 1/20/50 条岗位,优化器在该小样本上选择全表扫描;该结果只证明 SQL 形状与当前执行 +计划,不能外推为生产规模性能结论,也不能误写成“命中索引”。关键词 `%keyword%` 前导通配符和排序 +filesort 仍是已知性能风险。本轮不新增 V46、不修改索引或查询语义。 + +SQL 数量证据:匹配 1/20/50 条时 Repository 分别执行 1/2/2 条 SQL,完整 Service 分别为 +2/3/3 条;结果规模增长不会形成逐岗位查询,未发现 N+1。 + +### Q01 最终判定与边界 + +**Q01 H2/MySQL 查询语义、行级副作用、真实 MySQL EXPLAIN 与 DEF-01 回归全部通过,可以冻结 Q01 最终验收项。** + +本轮未修改任何生产代码、测试、迁移、索引、依赖或 OpenAPI;Testcontainers 容器已由 Ryuk 回收, +项目既有 MySQL/Redis/RabbitMQ 容器保持运行。全项目最终验收目前只剩评分写侧刷新链路,以及本地未提交 +的管理员初始化修复和 DEF-01 领先远端 1 个提交的 Git 收口。 + +## R01-D4 评分写侧刷新链路最终复验 + +### 本轮新鲜自动化证据 + +使用当前工作区代码重新执行刷新基础设施、写侧登记与事务原子性测试,Docker/Testcontainers 实际启动 +MySQL 8.4,结果 **67/67**,Failures=0、Errors=0、Skipped=0: + +| 测试类 | 数量 | 核心覆盖 | +| --- | ---: | --- | +| `CandidateScoringRefreshBatchIsolationTest` | 4 | 单任务失败不阻断同批正常任务 | +| `CandidateScoringRefreshClaimFenceTest` | 6 | 旧领取者不得覆盖新领取者 | +| `CandidateScoringRefreshClaimTokenTest` | 13 | 不可复用 claim token、迟到完成/失败/回退均安全 | +| `CandidateScoringRefreshFailureTransactionTest` | 3 | 源业务回滚时登记同步回滚 | +| `CandidateScoringRefreshInfrastructureTest` | 14 | 登记合并、重开、PROCESSING 防丢失、八维写入与版本幂等 | +| `CandidateScoringRefreshMySqlIntegrationTest` | 6 | 真实 MySQL 领取、租约恢复、失败回滚与批次隔离 | +| `CandidateScoringRefreshWorkerLoopIsolationTest` | 3 | 单项处理及失败登记异常不终止 Worker 循环 | +| `CandidateFeedbackRefreshRegistrationTest` | 7 | Feedback create/update/delete 均登记,失败路径无孤立任务 | +| `OfferExpiryRefreshRegistrationTest` | 7 | Offer 自动过期与任务同事务,重复执行合并且无消息副作用 | +| `ResumeVersionRefreshAtomicityTest` | 4 | 简历版本与任务原子提交、外层回滚一致、重复登记合并 | + +本轮之前完成的 `mvn clean verify` 为 **2800/2800,F=0 E=0 S=0**,已包含上述测试及评分消费者回归; +OpenAPI 保持 **176/214**。 + +### 真实 HTTP/MySQL 链路证据的有效性核查 + +归档独立验收 `docs/r01-d4-2-claim-token-final-verification-20260916.md` 已在真实 JAR、真实 MySQL、 +Redis 与 RabbitMQ 环境执行五条写侧链路,结果 **44/44,FAIL=0,SKIP=0**: + +- Offer 自动过期; +- CandidateFeedback create、update、delete; +- 简历版本创建。 + +每条均证明“业务数据变化 → 同学生/周期唯一 refresh task → Worker 处理为 COMPLETED → 八维快照齐备 +→ 相同输入重放不推进 snapshot version → 不写 snapshot_history”。消费者趋势、排名、看板、申诉和 +公平性监控另有 **33/33** 回归证据。 + +`git log` 逐文件核查显示,刷新包、`CandidateFeedbackService`、`OfferExpiryWorker` 和 +`ResumeVersionWriter` 的最后一次生产修改均为已验收提交 `52c8026`;此后没有生产变更。因此归档 HTTP +证据与本轮重新执行的当前代码完全对应,不存在拿旧实现结果替代当前实现的问题。历史脚本依赖已失效的 +Token 上下文,本轮未直接复用旧 Token,也未把旧上下文伪装为新执行结果。 + +### 数据安全与边界 + +- V44/V45 仍是刷新基础设施仅有的两项迁移,V1–V43 未修改,无 V46; +- claim token 只作为数据库内部所有权凭证,不进入日志、审计、响应、通知、消息或快照 JSON; +- Feedback、Offer、Resume 的失败/回滚路径不会留下孤立任务; +- 任务重复登记按学生与周期合并,值未改变时八维快照 version 不推进; +- 写侧登记本身不发送 RabbitMQ 消息、不写通知或快照历史; +- 本轮未改生产代码、测试、配置、迁移、接口或数据库数据。 + +### 写侧刷新链路最终判定 + +**评分写侧事件 → refresh task → Worker → 八维快照刷新链路通过最终复验,可以冻结 R01-D4 运行时验收项。** + +至此,最终总验收的功能与真实数据库运行项已经全部完成;下一步仅进行 Git 收口:独立审阅并精确提交 +管理员初始化修复及总验收报告,随后在网络可用时推送 DEF-01 与该提交,确认 HEAD/upstream 0/0。 + +## 最终总判定 + +> **PASS:S01、S02、S03、Q01、R01 的独立代码复验、自动化回归与真实 MySQL/Docker 黑盒验收全部完成,核心项 FAIL=0、SKIP=0,可以结束整改开发并进入 PR 审查。** + +最终收口代码提交: + +- DEF-01 岗位公开列表关键词规范化:`0b2ca79b96ea585dbafc753a980f7f79f5cf15db`; +- 管理员初始化严格幂等校验:`443a305`(本报告提交时的本地提交短 hash)。 + +最终有效门禁为 `mvn clean verify` **2800/2800,Failures=0、Errors=0、Skipped=0**; +OpenAPI 静态与运行时均为 **176 paths / 214 operations**。管理员初始化 M1–M3、Q01 跨库与 EXPLAIN、 +评分写侧刷新链路均已在本报告末尾完成独立收口;前文历次 FAIL、脚本缺陷与观察项作为审计轨迹保留, +不代表最终状态。 + +保留的非阻断观察项包括:Spring Boot 自动配置产生的惰性 generated security password 日志、历史 +`SMS_SEND` 审计数据中的旧手机号、未读数包含本人发送但未读消息、Q01 小样本执行计划的全表扫描与 +filesort,以及 Hikari 偶发连接校验告警。上述项目均已界定范围,未被隐去,也不改变本轮 PASS 判定。