四条读出路径(列表/详情/统计/导出)与管理动作行级把关共用 AnswerScopeQueryBuilder 一个谓词构造器(契约 §8 红线);数据范围按 SysRoleEnum 矩阵×audience 解析 + customId=fc:scope:all 扫描授权模块树,多来源取并集、全不命中回落仅本人提交。 四铁律落地:两段式列表禁 SELECT values_json、keyset 仅提交时间序(游标对外是 uuid)、 覆盖索引化(新增 idx_answer_form_time)、统计缓存键含 form_uuid+version 且仅升版失效+短 TTL。 T3 验收裁决前置三项:P25 answer.form_uuid 精确钉版溯源(删平台级回退启发式)、 P33 代填 subject 存在性+同校(SubjectDirectory 策略接口,生产走 /oapi/user/byId)、 P34 附件 key 与 store_key 一致性比对;P21 FormAdminGuard 整体替换为"授权模块集"口径 (ModuleTreeSource 策略接口,与 scope 解析共用同一棵树)。 新增对外读接口 GET /oapi/form/schema(内联 options+valueSchema)与 /oapi/form/answer (钉住版本 schema+值文档);导出 easyexcel 流式同步响应,选项列显 label、附件列显 fileName。 自验:mvn -B -o clean package 177 条全绿(本轮新增 56,23 条打真实 MySQL), 覆盖 V9/V11/V23/V27/V28/V37/V42 + V30 读侧半边 + V39 范围半边; V9 以"发布时补建索引行"实现(方案未指定机制,记 P36)。详见 docs/T4-report.md(问题单 P35~P46)。
22 KiB
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)
三处顺序是有意为之,不是随手排的:
- 幂等在限次之前:重放的那一条本来就占着名额,先查限次会把"双击重试"误判成"超过次数";
- 限次用"事务内插入 + uk 撞键即拒"(方案 §5.4 v1.4),不做"先 count 再插"——count 在并发下靠不住;
fill_quota与answer同事务,答卷插入失败则名额一起回滚,不留"名额被占但无答卷"的脏状态; - 值校验用未展开的 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(两个不同教师代填同一学生→第二条"限填一次";换学生仍可填)、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(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 提交(只改本文,不动任何构建输入),其后同流水线再跑一次。
—— 全文完 ——