# T3 执行说明 · 后端·答卷核心 > 任务书:《表单中心任务书-Qoder-20261014.md》v1.1 §T3|执行日期:2026-10-14|执行方:Qoder > 输入权威:方案 v1.4(§4.3②/§5.2/§5.4/§7.7)+ 契约 v1.0(§3/§5/**§2.2 实现期补充定稿**/§6 **错误码四段定稿**/§10)+ 手册 §5 裁决 **P5/P10/P12/P14/P18/P19/P20/P21/P22** > 前置:T1/T2 已验收。本轮对 T2 已验收文件只做**三处裁决驱动的扩展**(见 §1.6),未重构其行为;T2/T1 的 67 条测试原样通过。 --- ## 1. 做了什么 ### 1.1 新增文件(答卷域:6 实体 + 6 仓库 + 4 服务 + 2 控制器 + 1 校验器 + 1 DDL 脚本 + 7 测试类) | 包 | 新增 | 承担 | | --- | --- | --- | | `entity` | `Answer` / `AnswerIndex` / `StatusLog` / `AnswerOpLog` / `FillQuota` / `FormFile` | 契约 §10 答卷域五表(`MEDIUMTEXT`、`v_str(191)`、`DECIMAL(20,6)`、`uk_answer_uuid`/`uk_answer_idem`/`uk_quota` 逐项对齐)+ 附件落档行(P24) | | `repository` | 六个同名 `*Repository` | 全部 JPQL/派生查询,**零 native SQL**(P10);**answer 数据无物理删除通道**(P22/V30)——`AnswerRepository` 不提供删行方法;`FillQuotaRepository.releaseQuota` 的删除=**释放名额**(方案 §5.4 点名行为,V32 后半),不是答卷数据删除〔措辞按 T3 验收报告 §4.1 于 T4 更正〕 | | `common` | `FormValueValidator` | 契约 §3 值形状 ×11 放行类的提交期校验;附件值只认 `{id,fileName,key}` 且拒绝对 URL;字典候选由服务层注入(保持可脱库单测) | | `service` | `AnswerService`(提交编排)/ `AnswerIndexService`(索引写/重建/清理)/ `AnswerAdminService`(编辑/软删/改状态/批量)/ `FormFileService`(上传) | 见 §1.2–§1.4 | | `model` | `AnswerModels` | 提交/编辑/批量/上传出入参;请求体**没有** `schId/submitterId` 字段,响应只出现 `answerId`(uuid) | | `controller/api` | `FormAnswerController`(2 端点)+ `FormAnswerAdminController`(5 端点) | 见 §1.5 | | `sql` | `t3_answer_domain.sql` | 答卷域六表 MySQL 5.7 兼容 DDL(评审权威 + 生产 `ddl-auto=none` 升级底稿) | | `local-debug` | `t3-answer-smoke.sh` | 手册 §4.2 | ### 1.2 提交链路(契约 §5;顺序即语义,写在 `AnswerService` 类注释里) ``` 钉版解析 → 截止 → 幂等命中 → subject 授权 → 值校验(按钉住版本) → 附件同校归属 → fill_quota 占位(撞 uk 即拒) → 落 answer → 写 answer_index(只写 searchable) ``` 三处顺序是有意为之,不是随手排的: 1. **幂等在限次之前**:重放的那一条本来就占着名额,先查限次会把"双击重试"误判成"超过次数"; 2. **限次用"事务内插入 + uk 撞键即拒"**(方案 §5.4 v1.4),不做"先 count 再插"——count 在并发下靠不住; `fill_quota` 与 `answer` 同事务,答卷插入失败则名额一起回滚,不留"名额被占但无答卷"的脏状态; 3. **值校验用未展开的 schema 原文 + `allowedValues`(含软删条目)**,而不是 resolve 的内联结果: 展开后就看不出字段引用了哪个字典,"软删条目仍放行"(契约 §2.1/V38)无法表达。 **钉版**:`FormDefService.resolvePublished` 与填报页 resolve 走**同一条遮蔽实现**(本轮把它从 `resolve()` 里提出来复用),提交时解析到的 `current_version` 即加载版;配合 P20(升版只开草稿行、指针不动),"升版瞬间在途提交按旧版安全落地"天然成立。请求体**不接收** version(契约 §5 请求字段是闭集)。 **身份**(V17):`sch_id`/`submitter_id` 只从会话 `BoaUser` 取,`SubmitRequest` 里没有这两个字段(客户端即便多传也被 Jackson 忽略);`subject_id` 非空且 ≠ 本人 → 要求管理权(P21 闸门),否则 FORBIDDEN。 ### 1.3 索引与状态(方案 §5.2/§5.3) - `AnswerIndexService`:只写显式 `searchable` 字段;checkbox/multiselect **一值一行**;`number→v_num`、`date→v_date`、文本/选项 `→v_str(截 191)`;`upload/image` 无检索算子定义 → 不写行只 warn(P28)。 - 三个调用点:提交写入 / 编辑 `rebuild` / 软删 `clear`。**不存在**"按新版本 schema 清理旧字段行"的入口——那会打掉 V8 的历史可筛。 - 初始状态 = 表单 `status_defs` 里第一个未废弃 code(未配置即 `pending`)。 ### 1.4 上传模块(平台标准版,未抄 notice FileController) - 存储根读取形 `${FILE_PATH:${FILE_UPLOAD_PATH:${file.upload-path:/data/upload}}}`(yml 注释里定死的那条链;启动日志打印 `store_root=`,IT 断言兜底链生效);**yml 的 notice 教训注释一字未动**。 - `key = form/yyyy/MM/{SecureRandom 32hex}.{ext}`:存储名与用户文件名完全无关 → 同名不覆盖、路径穿越被吸收成展示名、伪装双扩展名按最后一段判定。 - 黑名单 = 平台同源 15 项(jsp/jspx/html/htm/js/exe/sh/bat/php/asp/aspx/jar/war/class/htaccess)**+ `svg`**(P27);扩展名形态限 `^[a-z0-9]{1,15}$`。 - 返回值字段闭集 `{id, fileName, key}`;库里也只存相对 `store_key`(varchar(191),本列有唯一键,避开 5.7 的 767 字节单列索引上限)。 - **P12 已配**:`spring.servlet.multipart.max-file-size/max-request-size=50MB`(默认 1MB 会直接挡掉合法附件),服务侧再判一次 ≤50MB(容器上限与业务上限分离,报错信息可读)。 ### 1.5 接口清单(本轮 7 个新端点,累计 28) - 用户面:`POST /api/form/submit`(幂等命中仍 200+原 `answerId`)、`POST /api/form/upload`(单文件)。 - 数据管理面 `POST /api/form/admin/data/*`:`edit`、`softDelete`、`changeStatus`、`batch/softDelete`、`batch/changeStatus`。 - **本轮刻意不建**:列表/详情/统计/导出读接口——方案 §6.4 红线要求四条读出路径共用同一个 scope 构造器,那是 T4 的建成物;先写一个无范围过滤的详情接口就是给 T4 留越权后门。 - **也不存在**填表人可用的编辑/撤回端点(V29 的"404/403"里,404 由端点不存在保证、403 由守卫保证)。 ### 1.6 对 T2 已验收产出的三处改动(全部裁决驱动,非重构) | 文件 | 改了什么 | 依据 | | --- | --- | --- | | `FormAdminGuard` | 校级角色集 `{2,5,7}` → **`{2,7}`**;新增 `canManage()`(不抛异常版,供 subject 授权分支);平台级拒绝判定前移(原因说准) | **P21 收紧**(局领导属查看角色,不给定义/数据管理权)+ 契约 §5 代填分支 | | `FormDefService` | `resolve()` 抽出 `resolvePublished()`(返回头表+版本行,供提交钉版复用同一条遮蔽实现);新增 `pinnedForData()`(数据面解析,不校验生命周期、含软删表单、遮蔽歧义时退平台级行);`answerNoEnabled=1` 时校验 `code ≤49`(`answer_no varchar(64)` 装得下号) | 契约 §5 钉版、§5.4 数据读改不受停用/软删影响、§10 列宽 | | `FormRepository` | 新增 `findDataDomainCandidates`(不排除软删表单的取域查询) | 方案 §5.4"软删是唯一切除形态、无物理删除"→ 答卷溯源不能断 | | `application.yml` | 只加 `spring.servlet.multipart` 两行 | **P12** | ### 1.7 环境动作(本机副作用) - 复用本地 MySQL 8.0.45(3306)+ 库 `bsch_formcenter`;`ddl-auto=update` 新建答卷域六表(`form_file.store_key` 后由 255 手工 `ALTER` 到 191 以对齐实体,表当时为空,无数据风险)。 - 为验证 `sql/t3_answer_domain.sql` 可执行,临时建库 `fc_t3_ddlcheck` 实跑(`mysql_exit=0`、6 表齐),**验证完已 DROP**,`SHOW DATABASES LIKE 'fc_t3%'` 为空。 - 上传测试写入 `service/target/tmp-upload/`(`mvn clean` 带走;`target/` 已被 .gitignore 排除),**未写 `D:/data/upload`**——冒烟启动时未注入 env,兜底键生效才写到那里,本轮该路径下只有脚本外无手工写入(T1 起该目录即本地上传兜底根)。 - IT 数据零残留:`answer/answer_index/fill_quota/answer_op_log/status_log/form_file/form` 计数全为 0(并发 IT 非回滚,靠 `@AfterEach` 定点清理,已验证)。 - 未启动 CAS/UserBase/Redis;未触碰 160 共享环境。 --- ## 2. 自验证据(逐 V) **门禁**:`mvn -B -o clean package` → **BUILD SUCCESS,Tests run: 121, Failures: 0, Errors: 0, Skipped: 0**,产物 `target/form-center.jar`。 其中 **T3 新增 54 条**(`FormValueValidatorTest` 12 + `FormAdminGuardTest` 5 + `AnswerSubmitIT` 17 + `AnswerAdminIT` 11 + `FormFileServiceIT` 7 + `AnswerConcurrencyIT` 2),**39 条打真实 MySQL**;T2 的 67 条一条未改仍全绿。 | V | 断言(方案 §9 原文) | 证据 | | --- | --- | --- | | **V17** | `submitter_id`/`sch_id` 由会话推导、接口不接受外部传入;越权构造被拒;**普通用户把 `subject_id` 构造成他人被拒(仅可=自己,管理员代填不受此限)** | `AnswerSubmitIT.subjectAuthorization`(构造成他人→"无权为他人填报";=自己→放行且 `submitter=subject`)、`adminCanFillOnBehalf`(管理员代填:`submitter=教师`、`subject=学生`两键分列)、`sessionRequired`(未登录/无学校归属各自被拒)、`idempotencyKeyOwnedByAnotherAccountIsRejected`(跨账号撞库级幂等键不给他人 answerId);权限层 `FormAdminGuardTest`(5 条,含 P21 的 roleId=5 被移出)。请求体无 `schId/submitterId` 字段=结构性不可能外部传入 | | **V29** | 填表人无编辑/撤回接口可用(404/403);管理员编辑成功且写 `answer_op_log` | 404:`FormAnswerController` 只有 `submit`/`upload` 两个端点(结构即证据)+ `FormAnswerAdminController` 的守卫;403:`FormAdminGuardTest.onlyRoleIdsTwoAndSevenManage`。`AnswerAdminIT.adminEditWritesOpLogAndRebuildsIndex`:编辑成功、`values_json` 更新、`op_log` 恰一行 `op=edit`+操作人正确、**身份列与状态列不动**、索引按钉住版本重建(`v_num` 变成 88);`editStillEnforcesValueContract` 证明被拒时**不落** op_log、原值不被破坏 | | **V30** | 软删后列表/详情/统计/导出均不可见,索引行同步删除;无物理删除接口存在 | `AnswerAdminIT.softDeleteIsTheOnlyDeletion`:`is_deleted=1`、`answer_index` 行清空、写 `op_log`、`answerRepository.findById` 仍在(物理行还在)、重复删被拒。"四读出路径不可见"依赖 T4 读接口(本轮刻意不建,见 §1.5);**无物理删除**=两个仓库都不提供 delete 方法、控制器无端点(`AnswerRepository` javadoc 成文)。`batchSoftDeletePerItemLogging` 覆盖批量同口径 | | **V31** | 同幂等键重复提交只产生一条答卷 | `AnswerSubmitIT.idempotencyReturnsOriginalAnswer`:两次同键 → 库里 1 行、第二次 `answerId` 相同且 `replayed=true`、**原值不被第二次的值覆盖**;`idempotencyKeyRequired`(必填/≤64/不同键各成一条);并发同键见 V32 | | **V32** | 限一次:subject 非空按 subject 限(两老师代填同一学生第二条被拒)、subject 空按 submitter 限(同人第二次被拒)——**两种 limitScope 各验一条**;**并发同键双提交恰一条成功(fill_quota 唯一键兜底)**;**软删后可重填(名额已释放)**;不限次表单不受影响 | 功能面:`onceLimitIsPerSubjectWhenSubjectPresent`(**两个不同教师**(实为两个 roleId=7 操作者——教师(1) 按 P21 无代填权)代填同一学生→第二条"限填一次";换学生仍可填)、`onceLimitFallsBackToSubmitter`(同人第二次被拒 + 不限次表单两条都过)、`softDeleteReleasesQuota`(软删后同 subject 可重填)。**并发面**:`AnswerConcurrencyIT.concurrentSubmitsAgainstOnceLimitProduceExactlyOneAnswer`(真 MySQL 双线程同时放开:恰 1 条 `answer`、恰一条成功另一个拿到"限填一次"、`fill_quota` 恰 1 行 `limit_key=提交人id`、成功那条索引照常);`concurrentSubmitsWithSameIdempotencyKeyProduceOneAnswer`(同幂等键并发:恰 1 行,两个返回值指向同一 uuid——即"撞 uk 后回读原答卷"的并发结局) | | **V33** | 截止后填报页只读、提交接口拒绝;停用表单入口失效但数据管理可见 | `deadlineBlocksSubmit`(2020 年截止→"已过收集截止时间";改 2099→放行,顺带证"只选日期补 23:59:59")、`disabledFormClosesFillEntryOnly`(停用后提交被拒"已停用",`enable` 后同表单同数据恢复可填)、`AnswerAdminIT.domainScopingAndSoftDeletedFormStillOwnsAnswers`(表单软删后其存量答卷仍可编辑=数据侧可见可管理) | | **V14**(编排补充①) | 填表人改状态被拒(接口层) | `AnswerAdminIT.submitterCannotChangeStatus`:对学生(3)/教师(1) 身份调控制器实际走的 `adminGuard.requireAdmin` → FORBIDDEN"无表单管理权限",对管理员放行;数据面不存在"我改我这条的状态"的端点 | | **V15**(编排补充①) | 改状态写 `status_log`;系统不存在任何流转规则校验 | `statusChangeHasNoTransitionRules`:`pending → back → pending → done`(含倒退)全部放行,`status_log` 三行按序、操作人正确;`statusMustBeLiveDefinition` 证明唯一约束是"目标值必须是未废弃定义",**不是**转移规则 | | **V24**(编排补充②,本地可断言部分) | 非白名单扩展名被拒;答卷值中不含绝对 URL;返回只含 `{id,fileName,key}` | `FormFileServiceIT.dangerousExtensionsRejected`(15 项逐个 + `svg` + 缺扩展名 + 非法扩展名形态 + `report.pdf.jsp` 伪装双扩展名 + 放行 `ok.docx`)、`uploadReturnsContractTripleAndStoresUnderRoot`(响应字段闭集恰是三件、`key` 匹配 `^form/\d{4}/\d{2}/[0-9a-f]{32}\.jpg$`、库里也存相对 key)、`fileSizeCap`(>50MB 拒且不留行;空文件拒)、`submittedAttachmentIdsMustBeOwned`(提交值里 `key` 给 `http://cdn...` 被拒"相对存储路径";伪造附件 id 被拒)。**"经 FILE_WEB_PATH 可访问"按编排留给 T9 部署环境**;`FILE_PATH` 兜底链由 `storeRootFollowsDocumentedPlaceholderChain` 断言 | | **V1/V3/V8/V9 相邻面** | 值形状按契约 §3 全类型接受;索引只写 searchable;升版字段身份 | `FormValueValidatorTest` 12 条(含 14 类里 11 类的示例值、附件形状、字典软删放行、上限 9);`AnswerSubmitIT.indexWritesSearchableOnly`(`remark` 不声明就不进索引、250 字符截到 191、`v_date`/`v_num` 类型正确)、`submitPinsCurrentlyPublishedVersion`(v2 字段在 v1 期被拒"未知字段",发布后按 v2 钉版) | **其它取证** | 项 | 结果 | | --- | --- | | MySQL 5.7 复核(P10) | ①答卷域**零 native SQL**;②`sql/t3_answer_domain.sql` 在 `fc_t3_ddlcheck` 实跑 `exit=0`、6 表齐、随后 DROP;③`SHOW CREATE TABLE` 逐列对齐契约 §10(`mediumtext`/`v_str(191)`/`decimal(20,6)`/`date`/两个 uk + `uk_quota` + 四个 answer_index 复合索引);④`form_file.store_key` 由 255 主动收到 **191**(该列有唯一键,避开 5.7 单列索引 767 字节上限);⑤answer_index 复合索引键长约 1292B,依赖 5.7 默认 `ROW_FORMAT=DYNAMIC`+`innodb_large_prefix=ON` → 记 **P31** 待 160 核 | | 启动冒烟 | `bash local-debug/t3-answer-smoke.sh` → **7/7 PASS**:health=200;`store_root=D:/data/upload`(兜底链生效);`/api/form/submit`、`/api/form/upload`、`/api/form/admin/data/edit`、`.../batch/changeStatus` 无票均 302→CAS;`/oapi/*` 仍 401 | | 错误码口径(P14 定稿) | 全线沿用四段:值形状/未知字段/附件伪造/未找到=`PARAM_ERROR(2)`;未登录=`UNAUTHORIZED(4)`;subject 越权/无管理权=`FORBIDDEN(3)`;限次/截止/未发布/已停用/无草稿=`CUSTOM(5)`。本轮未引入任何 VALIDATE/NOT_FOUND 字样 | | 登录态冒烟(P1 遗留) | 仍无本地 CAS/UserBase,接口以"墙后 302 + 54 条服务层直连真库"替代取证;缺口沿 P1 归 T5 联调轮 | --- ## 3. 问题单(本轮登记,供执行手册 §5 裁决;编号接续 P22) | # | 问题 | 本轮取值(最保守解读) | 影响面 | | --- | --- | --- | --- | | P23 | 契约 §10 `uk_answer_idem(idempotency_key)` 是**库级**唯一键(不含 `sch_id`/`submitter_id`),跨账号撞同键的语义契约未写 | 命中同键且归属(会话校+提交人)一致 → 200+原 `answerId`;**归属不一致 → FORBIDDEN**(不把他人 answerId 回给请求方,uuid 不可枚举但同键命中本身就是探测面);并发撞键由编排在事务外回读原答卷。若裁决要"键只需表单内唯一",把 uk 改 `(sch_id, code, idempotency_key)` | 幂等语义与并发测试 | | P24 | 契约 §3 附件值形状含 `id`,但 §10 九表**没有附件表**——`id` 无处解析 | 建最小 `form_file`(`id/sch_id/file_name/store_key/file_size/ext`,`uk(store_key)`),上传成功即落档;值里的 `id` 即本表主键,提交时做**同校归属校验**。若裁决"中心不落附件档、`id` 由消费方自造",本表与归属校验一起退掉 | 上传模块 + 二期 GC/鉴权下载接口 | | P25 | 契约 §1 校域不变式使 `answer.sch_id` 恒=会话校,于是答卷的钉版坐标 `(校, app_code, code, version)` **不含"表单归属校"**:平台级表单被本校后建的同 `(app_code,code)` 表单遮蔽后,历史答卷版本行解析有歧义窗口 | `pinnedForData()` 在本校域取不到该版本行时,**退回平台级同 code 表单的行**(历史数据仍按原 schema 校验,不静默失败)。根治=`answer` 增列 `form_uuid`(或契约明确"平台级表单答卷必须存平台级归属") | 数据面溯源;T4 读详情/导出同样要走这条解析 | | P26 | `answer_no` 当日序号取"当日行数+1",**并发下可重号**;契约 §10 未给 `answer_no` 任何唯一键 | 按 DDL 草案执行:不加索引、不改格式,序号含软删行(软删不回收号)。要根治需 `uk(sch_id, code, answer_no)` + 撞号重试,或独立取号表——属加语义,等裁决 | 报名号场景(第一天就会并发);T9 压测可暴露 | | P27 | 上传黑名单:平台同源清单(svc-hall/UserBase/Notice 那 15 项)不含 `svg`,而一期"key=不可猜测随机串即访问控制"(方案 §7.7)对 svg 失效(同源直访 + 内嵌脚本=存储型 XSS 面) | **在平台 15 项上追加 `svg`**,代码注释写明理由。若确有图标上传需求,需二期的鉴权下载接口(U11 族)才能安全放行 | 安全边界;前端 T7/T8 若允许 svg 需同步 | | P28 | 附件字段勾了 `searchable` 时如何建索引,契约 §2 未给附件的检索算子(算子只定义了 等值/区间/前缀 三类) | **不写索引行**并 warn(不发明算子、不拿 fileName 冒充可筛值)。若要"N 个文件"可筛,需先定算子 | V3/V9 一致性;T4 筛选候选列表 | | P29 | 批量操作单次条数上限,方案 §5.4 只写"批量逐条走单条校验与留痕" | 加安全阈值 **200**(逐条各走独立事务,一次请求挂几千条等于把接口变成 DoS 面);超限 `PARAM_ERROR`。批量与单条同形:`op` 用 `batch_*` 前缀(契约 §10 注释的 `edit/soft_delete/batch_…` 形态)、一条答卷一行日志、summary 带批次上下文 | 前端 T8 分页勾选即可满足 | | P30 | 数据管理操作的行级范围(契约 §8 矩阵 / `fc:scope:all`)本轮未接 | 仍走 P21 过渡角色闸门 + `(域, uuid)` 定位(跨校答卷直接查不到);**行级 scope 与"编辑入口按数据范围收敛"归 T4**(V27/V39 的断言点)。本轮不做"半个 scope",避免与 T4 的构造器双份实现 | T4 收紧点 | | P31 | `answer_index` 三条复合索引键长约 1292 字节,超过 5.7 旧行格式的单索引 767 字节上限 | 依赖 5.7 默认 `ROW_FORMAT=DYNAMIC` + `innodb_large_prefix=ON`(上限 3072);`sql/t3_answer_domain.sql` 顶部把该前提与降级方案(`v_str` 降 `varchar(64)`,前缀匹配语义不变)写死。**160 生产行格式/版本待核**(P10 的延伸,方案 §7.7 记档项) | 建表能否成功;T9 部署前必查 | | P32 | 编辑/改状态是否受"截止时间"与"表单软删"约束,契约只写了填报入口 | 编辑与改状态**不看截止**(截止只挡填报,方案 §5.4);表单软删后其存量答卷**仍可管理**(数据面取域不排除软删表单,新增 `findDataDomainCandidates`)。两条都按"停用/软删只断填报入口、数据读改不变"外推 | T4/T8 读路径需保持同一外推口径 | | P33 | `subject_id` 的**存在性/归属**未校验:管理员代填可把 `subject_id` 写成任意整数(不存在的用户、或别校的用户 id),污染"关于我的"视图(V18 的主体可见性依赖 id 真实) | 本轮只校验**授权**(普通用户只能=自己,V17)与**校域**(`sch_id` 恒=会话校),不校验 id 是否存在于本校——需要 UserBase 查人(`by-ids`/组树),与 T4 的 SDK 调用链一起做。`subject_id` 因此当前是"未经证实的引用",**T4 接入时必须补一道 by-ids 校验**(越权面不大但数据会脏) | V18;T4 scope 与"关于我的"视图 | --- ## 4. 推送与 CI | 仓 | 提交 | 推送 | CI run | 结论 | | --- | --- | --- | --- | --- | | form-center-service | `bff0fb1`(33 条:29 新增 + 4 处裁决驱动改动,行数见 §1.6) | `90f6b0a..bff0fb1 main -> main` | [run 1481](http://192.168.10.160:3000/form-center/form-center-service/actions/runs/1481)(`build.yml@refs/heads/main`,head_sha `bff0fb1…`) | **status=completed / conclusion=success**(Gitea Actions API 实测,poll1 即终态) | | form-center-front | 无改动 | 未推送 | — | T3 是纯后端任务 | - 推送方式:常规 `git push origin main`;暂存**按显式路径逐条 `git add`**(33 条路径,提交前 `git status` 核对:无 `add -A`、无敏感文件、`target/` 与 `local-debug/` 不在本仓工作树)。 - CI 取证方式:`git credential fill` 取本机已存凭据后调 `GET /api/v1/repos/.../actions/runs[/{id}]`,凭据不回显、不落盘、不入库。 - CI 只跑 `mvn -B -DskipTests clean package`(**不跑单测**,方案 §7.2),所以 CI 绿只证明产物链; 答卷的行为证据在本文 §2 的本地 `mvn test`(121 条,其中 39 条打真实 MySQL、2 条真并发取证)。 - 本节的回填本身是一次 docs-only 提交(只改本文,不动任何构建输入),其后同流水线再跑一次。 —— 全文完 ——