# S01 最终真实 MySQL / Docker 增量复验报告(脱敏版) - **验收日期**:2026-09-14 - **分支**:`fix/resume-upload-safety` - **范围**:只验证 S01 第四轮整改(F6:`expires_at` → `expired_at`)后的**真实运行路径**;本轮**未修改任何代码** - **结论**:**PASS** - **本报告不含**:验证码值、完整手机号、JWT、`Authorization` 头、数据库口令、`.env` 内容 > 本轮为此前因 Docker Linux Engine 未运行而 BLOCKED 的复验。本次 Docker Engine 可用,全部真实黑盒已执行完成。 --- ## 1. 基线与边界 | 项 | 实测 | |---|---| | 分支 | `fix/resume-upload-safety` | | 暂存区 | **空**(`git diff --cached --name-only` 无输出) | | `git diff --check` | 无空白错误(仅 CRLF 提示) | | 迁移改动 | **无**(`git diff --name-only -- db/migration` 无输出;41 个文件,最大 `V41__message_recall.sql`) | | 已跟踪改动 | 24 个(**全部来自此前几轮**:7 个 docs + 11 个 java main + 6 个 java test),本轮 **0 新增** | | 删除文件 | **0** | | 历史未跟踪文件 | 未删除、未移动、未覆盖 | 本轮**未修改**:生产代码、现有测试、`docs/smoke-test.sh`、`V1–V41`;**未新增** Flyway/表/索引/`ErrorCode`/接口;**未恢复**验证码日志。 ## 2. Docker 环境 | 项 | 值 | |---|---| | Docker Engine | 29.6.2(Docker Desktop 4.84.0,linux/amd64) | | `mydreamjob-mysql` | **healthy**(mysql:8.4,127.0.0.1:3307→3306) | | `mydreamjob-redis` | **healthy**(redis:7.4-alpine) | | `mydreamjob-rabbitmq` | **healthy**(rabbitmq:3.13-management-alpine) | 进入本轮时三个容器均为 `Exited`(2 天前停止)。经负责人明确授权后,对**已存在的具名容器**执行 `docker start`:**未重建容器、未改动配置、未触碰 volume(`mysql_data`/`redis_data`/`rabbitmq_data` 均保留)**。验收结束后容器保持运行,由负责人决定是否停止。 ## 3. 真实 Schema 对账 真实 MySQL 执行 `SHOW COLUMNS FROM sms_verification_codes`(**应用账号 + `MYSQL_PWD`,非 root**): ``` id phone code purpose attempts max_attempts expired_at used created_at ``` 三方对账**完全一致**: | 来源 | 列集合 | |---|---| | 真实 MySQL | `attempts code created_at expired_at id max_attempts phone purpose used` | | 实体 `@Column` | 同上 | | `V1__base_schema.sql` | 同上 | - **包含 `expired_at`** ✅ - **不包含 `expires_at`** ✅ **反证**:`SELECT expires_at FROM sms_verification_codes LIMIT 1;` → `ERROR 1054 (42S22) Unknown column 'expires_at' in 'field list'`(退出码 1)。 ## 4. 真实取码 SQL 修复后的等价只读查询在真实 MySQL 执行: ```sql SELECT code FROM sms_verification_codes WHERE phone = ? AND purpose = ? AND used = 0 AND expired_at > NOW() ORDER BY created_at DESC, id DESC LIMIT 1; ``` **退出码 = 0,无 `ERROR 1054`**。(查询返回行数按参数而定,本报告不展示任何取回的 `code`。) ## 5. 最新 JAR 启动 | 项 | 值 | |---|---| | JAR | `target/mydreamjob-server-0.0.1-SNAPSHOT.jar`(`mvn clean verify` 产物) | | 启动参数 | `--spring.jpa.hibernate.ddl-auto=validate`、`--app.sms.mock.enabled=true`、`--server.port=8080` | | 启动结果 | **`Started MyDreamjobApplication`(14.9 s)** | | Flyway | 无错误(仅一条 MySQL 8.4 版本提示 WARN) | | Schema validation | **无错误** | | `GET /api/v1/health` | **HTTP 200,`status=UP`** | | 运行时 OpenAPI | **173 paths / 211 operations** | 端口 8080 在启动前空闲,无未知进程被停止。日志写入本轮新目录 `docs/acceptance/s01-20260914/`,未覆盖历史日志。 ## 6. 真实完整冒烟链 以唯一虚构前缀 `1990914xxxx` 与隔离 requestId 执行: ```bash SMS_MOCK_ENABLED=true ALLOW_TEST_OTP_DB_READ=true bash docs/smoke-test.sh ``` 结果:**PASS=10 FAIL=0 SKIP=1,exit=0** | 步骤 | 结果 | |---|---| | `GET /health` | PASS(200) | | `POST /auth/sms/send` + `POST /auth/login` | **PASS**(200 / 200) | | `GET /jobs?size=5` | PASS | | `GET /jobs/public/{id}` | PASS | | `GET /jobs/{id}/match` | PASS | | `POST /applications` | PASS | | `PATCH /applications/{id}/status` | **SKIP**(未提供 `COMPANY_TOKEN`,属脚本既有语义) | | `GET /notifications` | PASS | | `GET /notifications/unread-count` | PASS | | `POST /notifications/{id}/read` | PASS | | `POST /applications/{id}/withdraw` | PASS | 逐项确认: 1. 发码成功 ✅ 2. `smoke-test.sh` 经**真实 `docker exec`** 连真实 MySQL ✅ 3. 使用应用账号 `MYSQL_USER`,**未使用 root** ✅ 4. mysql argv **不含密码** ✅(见 §7) 5. SQL 使用 `expired_at` ✅ 6. 成功取得可用验证码,**未显示其值** ✅ 7. 真实 `POST /auth/login` 成功 ✅ 8. 获得 Access Token,**未输出** ✅ 9. 后续岗位/投递/通知步骤按夹具条件执行 ✅ 10. **未出现 `ERROR 1054`** ✅ 11. 未出现空验证码 ✅ 12. 未从日志读取验证码 ✅ 13. stdout/stderr 不含验证码、完整手机号、JWT、`Authorization`、数据库口令 ✅ **核心链路无 SKIP**:唯一 SKIP 是企业状态步骤,取码与登录核心链路均实际执行并通过。 **真实库侧证据**:该手机号在 `sms_verification_codes` 中产生 **1 行、`used=1`、`code` 长度 6** —— 真实 MySQL 中确实生成了 6 位验证码并被真实登录消费。 > **关键推论**:登录成功即证明真实执行的 SQL 使用了**有效列**。若仍是 `expires_at`,查询会报 `ERROR 1054`、取不到码,登录必然失败。 ### `bash -x` 复验 以 `bash -x` 执行(新手机号):**exit=0,PASS=10 FAIL=0 SKIP=1**,捕获 stdout+stderr 后扫描,敏感命中数: | 标记 | 命中 | |---|---| | 实际验证码值 | **0** | | 完整手机号 | **0** | | `Authorization: Bearer` | **0** | | `LOGIN_BODY=` | **0** | | JWT 形态(`eyJ…`) | **0** | | `ERROR 1054` | **0** | ## 7. F5 真实环境 argv 证据 在真实查询执行期间,读取**容器内 mysql 进程的实际 argv**: ``` mysql -umydreamjob -N -B mydreamjob ``` - 使用**应用账号**(非 root)✅ - **不含 `-p<密码>`** ✅ - **不含 `--password`** ✅ - 密码经 `MYSQL_PWD` 环境变量传递 ✅ ## 8. mock=false 真实 MySQL 回滚 以 `--app.sms.mock.enabled=false` 重启后调用 `POST /auth/sms/send`: | 项 | 结果 | |---|---| | 响应 | `HTTP 400` + `{"code":400,"message":"sms sender unavailable"}` | | 是否虚假成功 | **否** ✅ | | 验证码表前后状态 | **完全一致**(该手机号 0 行;全局 `max_id` 994→994;总数 918→918) ✅ | | 残留 `used=0` 验证码 | **无** ✅ | | 新增虚假发送成功审计 | **0** 行 ✅ | | 日志含手机号/验证码 | **0** 命中 ✅ | 响应与上一轮(`docs/acceptance/s01-20260911/d2-sms-response.txt`)逐字一致。 ## 9. 配置缺失 / 空值 / 非法值启动 | 用例 | 启动 | Bean 缺失错误 | 发码响应 | 是否落库 | |---|---|---|---|---| | 配置**缺失** | `Started` ×1 | **0** | 400 `sms sender unavailable` | 未落库(994→994) ✅ | | `SMS_MOCK_ENABLED=`(**空值**) | `Started` ×1 | **0** | 400 `sms sender unavailable` | 未落库(994→994) ✅ | | `SMS_MOCK_ENABLED=invalid-value`(**非法值**) | `Started` ×1 | **0** | 400 `sms sender unavailable` | 未落库(994→994) ✅ | **「恰好一个 `SmsCodeSender` 且为 `UnavailableSmsCodeSender`」在真实环境按排除法成立**: - 0 个 Bean → 启动会以 `NoSuchBeanDefinition` 失败;≥2 个 → 会以 `NoUniqueBeanDefinition` 失败;Mock 生效 → 发码成功且落库。 - 实测:**启动成功** + **发码不可用** + **未落库**,三者互斥地锁定「恰好一个 `UnavailableSmsCodeSender`」。 (Bean 数量与类型的严格断言另由 `SmsSenderConfigurationTest`(真实 `ApplicationContextRunner`)在 §10 覆盖。) 每次只停止本轮明确记录 PID 的 JAR,未触碰任何未知进程。 ## 10. 日志脱敏扫描 扫描对象:**本轮全部新日志**(6 个应用日志 + stdout)**+ 冒烟 stdout/stderr**。 | 禁止项 | 命中 | |---|---| | 实际验证码(本轮真实产生的 2 个,逐个比对) | **0 / 0** | | 完整手机号(6 个测试号逐个比对) | **0** | | `Authorization: Bearer` | **0** | | JWT 形态(`eyJ…`) | **0** | | `MYSQL_PASSWORD` 实际值 / 数据库口令 | **0** | | `LOGIN_BODY` | **0** | | `MOCK SMS CODE` | **0** | | `NumberFormatException` | **0** | | `ERROR 1054` | **0** | **敏感标记命中合计 = 0。** 扫描器有效性对照:注入探针(`MyDreamJob backend smoke test`)命中 **2**,证明扫描器在工作,而非空转的 0。 ## 11. OpenAPI 与回归 | 项 | 结果 | |---|---| | `python docs/verify_openapi.py` | **All checks passed!** | | 运行时 `/v3/api-docs` | **173 paths / 211 operations** | | 静态 `docs/openapi-rc.json` | **173 paths / 211 operations** | | 静/运行时一致 | **YES**(路径差集:静态独有 = 无,运行独有 = 无) | | 是否新增取码接口 | **无**(含 "code" 的唯一路径是 `/admin/scoring/risk/rules/{ruleCode}`,为风控规则码,与验证码无关) | | 接口结构变化 | **无** | Maven 安全专项: ``` mvn -Dtest="SmsSenderConfigurationTest,SmsVerificationSecurityTest,SmsVerificationServiceTest,CurrentUserAccessorTest,AuthenticationBoundaryIntegrationTest,SmsEmptyPropertyStartupIntegrationTest,SmsUnavailableSenderRollbackIntegrationTest" test ``` → **Tests run: 61, Failures: 0, Errors: 0, Skipped: 0 — BUILD SUCCESS** ## 12. 数据清理 本轮产生的数据可按唯一虚构前缀 `1990914` 精确圈定(时间戳均为 2026-09-14): | 表 | 行数 | |---|---| | `users` | 2(验收虚构号码:`199****0101`、`199****0102`) | | `sms_verification_codes` | 2 | | `applications` | 2(均为 `WITHDRAWN`) | | `notifications` | 4 | | `user_roles` | 2 | | `audit_logs` | 8 | **决策:保留本轮数据,不执行删除。** 理由: 1. `audit_logs` 有 **8 行** `user_id` 指向这两个用户,且该表**无外键约束**;删除用户会使审计行成为孤儿。 2. 本轮任务边界明确**禁止删除 `audit_logs` 行**。 3. 保留可维持审计链一致;边界亦明确允许「无法安全确认时保留数据并记录」。 未执行任何模糊 `DELETE`、未清空表、未重置自增、未修改真实账号。上述足迹为精确记录,供后续人工处置。 ## 13. 最终 Git 检查 | 项 | 结果 | |---|---| | `git status --short` | 24 个已跟踪改动(**与本轮开始时同一批**)+ 98 个未跟踪(既有残骸 + 本轮验收产物) | | `git diff --cached --name-only` | **空**(未暂存) | | `git diff --check` | 无空白错误 | | `git diff --name-only -- db/migration` | **空**(V1–V41 未修改) | | 删除文件 | **0** | | commit / push | **未执行** | | S02 | **未进入** | ## 14. 验收结论 | # | 判据 | 结果 | |---|---|---| | 1 | Docker/MySQL 可用 | ✅ | | 2 | 真实表包含 `expired_at` | ✅ | | 3 | 真实取码 SQL 执行成功 | ✅ | | 4 | 完整学生登录链成功 | ✅ | | 5 | `bash -x` 无敏感泄露 | ✅ | | 6 | mock=false 真实 MySQL 完整回滚 | ✅ | | 7 | 缺失/空/非法配置安全回退 | ✅ | | 8 | 运行时 OpenAPI 为 173/211 | ✅ | | 9 | 无日志泄露 | ✅ | | 10 | 核心项无 SKIP | ✅ | | 11 | FAIL = 0 | ✅ | **S01独立代码复验和真实MySQL/Docker黑盒全部通过,可以进入精确暂存检查点。** --- ## 遗留与观察(不阻断本轮结论) 1. **验证码仍为数据库明文存储**(未改表结构,属已知遗留)。 2. **真实短信供应商未接入**;生产 `mock=false` 下发码一律安全失败,需产品确认上线策略。 3. **本轮测试账号保留在库**(见 §12),与其审计记录一致;如需清理应由人工按精确 ID 处置。 4. `/api/v1/api-docs` 返回 500(`/api/v1/v3/api-docs` 正常 200)。该行为与 S01 无关,非本轮引入,记录待查。 5. 三个容器在验收结束后**保持运行**(本轮仅 `docker start`,未重建、未删除 volume),是否停止由负责人决定。