答卷域是"校验只写一遍、索引只有一份"这条选型(D2 模型 2b)的兑现处,本轮把它一次落齐。 - 提交链路按语义排死顺序:钉版解析→截止→幂等命中→subject 授权→值校验→附件同校归属 →fill_quota 占位→落 answer→写索引。幂等必须在限次之前(重放那条本就占着名额), 限次不做先 count 再插而是事务内撞 uk 即拒(并发下 count 靠不住,方案 §5.4) - 钉版与定义域共用同一条 resolve 遮蔽实现;升版窗口旧版仍可填并钉旧版(P20), 请求体不含 version/schId/submitterId——身份结构性不可注入(V17) - FormValueValidator 按契约 §3 逐类校验值形状:date 严格 YYYY-MM-DD 且必须是真实日期、 number 空值只认 null、选项值取内联 ∪ allowedValues(含软删条目)、 附件只认 {id,fileName,key} 且拒绝对 URL/跨目录 key - answer_index 只写显式 searchable 字段、多值一行一个、v_str 截 191;编辑按钉住版本重建, 软删同步删行且释放 fill_quota 名额;留痕只增不改,改状态无任何转移规则校验(V15) - 上传模块自写平台标准版:FILE_PATH 三层兜底链、随机存储名、平台 15 项黑名单+svg、 ≤50MB(并按 P12 显式配 spring.servlet.multipart,默认 1MB 会挡掉合法附件) - 数据面刻意不建列表/详情/统计/导出读接口:四条读出路径必须共用 T4 的 scope 构造器, 现在写一个无范围过滤的详情接口就是给 T4 留后门 - T2 已验收文件仅按裁决改动:FormAdminGuard 收紧为 {2,7}(P21)并加 canManage; FormDefService 抽出 resolvePublished/新增 pinnedForData(数据面溯源含软删表单); FormRepository 加数据面取域查询;application.yml 只加 multipart 两行 自验:mvn -B -o clean package 绿(121 测试,本轮新增 54 条含 39 条打真实 MySQL, V32 并发面用双线程非回滚 IT 取证),逐 V 证据与问题单 P23~P32 见 docs/T3-report.md
定义与答卷物理分离的第一步:把"契约"落进可执行代码,让后续答卷域只需消费不必再解释。 - form/form_version 头表+版本行:已发布行零写路径(改版=v+1 新行,老行逐字节冻结), 升版期间头表指针不动,使契约 §5 的钉版与 §3.2 的两层缓存声明天然成立;uuid 建草稿即生成 - FormSchemaValidator:19 项属性白名单、14/11 控件、options 只认 {label,value} 数组或 dict:{code} 引用(字符串数组被拒)、searchable 只出显式声明字段、跨版本同 key 不得换 type (否则 answer_index 分类型落值的语义与"删字段后历史仍可筛"当场失效) - 选项字典两级:管理面同层精确匹配(校级改不到平台级)、解析面 {会话校,0} 且本校遮蔽不回落; value 物理不可变而 label 可改;软删条目仍进提交放行集;引用是活引用、只在读路径内联展开 - 表单级配置(audience/fill_limit/deadline 补 23:59:59/报名号/状态定义)留在头表,不随版本快照 - 另存为副本:新 code+新 uuid+version 1、schema 深拷贝且字典引用随行,源表只读 - 出入参不出现库内 id;手写 DDL 落 sql/t2_definition_domain.sql 并按 MySQL 5.7 复核(无 native SQL) 自验:mvn -B -o clean package 绿(67 测试,含 27 条打真实 MySQL 的 IT),逐 V 证据与 9 条 实现期问题单见 docs/T2-report.md