Files
form-center-service/sql/t3_answer_domain.sql
T
chenxing bff0fb15fa
build / build-push (push) Successful in 52s
feat(answer): T3 后端·答卷核心——提交/幂等/限次/索引/软删留痕 + 平台标准版上传
答卷域是"校验只写一遍、索引只有一份"这条选型(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
2026-10-14 16:50:07 +08:00

121 lines
8.2 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ============================================================
-- 表单中心 T3 · 答卷域 DDL(MySQL 5.7 兼容复核版)
-- 范围:契约 §10 九表中的答卷域五表 = answer / answer_index / status_log / answer_op_log / fill_quota
-- 另加 form_file(中心上传附件落档行;契约 §10 未列,但 §3 值形状含 id → 见 T3 报告问题单 P24)
-- 用途:评审权威 + 生产 ddl-auto=none 时的升级脚本底稿;开发期仍由 ddl-auto=update 建表。
-- 5.7 复核要点:①无 CTE/WITH、无函数索引、无生成列;②utf8mb4(本脚本显式写 CHARSET/COLLATE);
-- ③InnoDB、无物理外键(方案 §7.7 逻辑外键约定);④TEXT/MEDIUMTEXT/LONGTEXT 一律不写 DEFAULT(5.7 不允许);
-- ⑤索引键长:单列 uk/普通列的 utf8mb4 上限 767 字节=191 字符(v_str、store_key 均按此定宽),
-- answer_index 三条复合索引键长约 1292 字节,依赖 5.7 默认 ROW_FORMAT=DYNAMIC + innodb_large_prefix=ON
-- (上限 3072)——160 生产 MySQL 版本与行格式待核(方案 §7.7/P10 记档项),
-- 若为 COMPACT 或 large_prefix=OFF,需把 v_str 降到 varchar(64)(前缀匹配语义不变,可筛选更短)。
-- P5/P15 裁决:实体继承 BaseEntity,实表=契约草案 + create_by/update_by 两列;
-- 审计列在基类未标 nullable=false,故按建表实态写 DEFAULT NULL(Listener 恒写 now(),业务上不为空)。
-- 本机取证:MySQL 8.0.45 / bsch_formcenter 由 ddl-auto=update 建表后 SHOW CREATE TABLE 逐列比对一致。
-- ============================================================
CREATE TABLE `answer` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`uuid` VARCHAR(32) NOT NULL COMMENT '数据面运行时身份(详情 URL/嵌入地址/L2 引用,契约 §1)',
`sch_id` BIGINT NOT NULL COMMENT '恒=会话校(校域不变式:跨校提交在解析层即不可能)',
`app_code` VARCHAR(64) NOT NULL,
`code` VARCHAR(64) NOT NULL,
`version` INT NOT NULL COMMENT '钉住的 schema 版本(提交时解析到的发布版,升版不回改)',
`submitter_id` INT NOT NULL COMMENT '谁提交的(CAS 会话推导,不可外部传入)',
`subject_id` INT DEFAULT NULL COMMENT '关于谁(代填时=被代填人;普通用户只能=自己)',
`status` VARCHAR(64) NOT NULL COMMENT '轻状态位 code,取值域=form.status_defs(一等列,方案 §5.3②)',
`values_json` MEDIUMTEXT NOT NULL COMMENT '值文档 {key:value}(TEXT 的 64KB 会被长文本答卷顶爆)',
`answer_no` VARCHAR(64) DEFAULT NULL COMMENT '报名号 {code}-{yyyyMMdd}-{当日序号},表单级开关默认关',
`idempotency_key` VARCHAR(64) NOT NULL COMMENT '客户端幂等键;库级唯一——防双击只有 DB 唯一索引靠得住',
`is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删=唯一删除形态(无物理删除接口);同步删索引行+释放名额',
`create_by` VARCHAR(255) DEFAULT NULL,
`create_time` DATETIME DEFAULT NULL,
`update_by` VARCHAR(255) DEFAULT NULL,
`update_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_answer_uuid` (`uuid`),
UNIQUE KEY `uk_answer_idem` (`idempotency_key`),
KEY `idx_answer_form` (`sch_id`, `code`, `status`, `id`),
KEY `idx_answer_submitter` (`sch_id`, `submitter_id`),
KEY `idx_answer_subject` (`subject_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='答卷(方案 §5.1:定义与答卷物理分离)';
CREATE TABLE `answer_index` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`answer_id` BIGINT NOT NULL COMMENT '逻辑外键 → answer.id',
`sch_id` BIGINT NOT NULL,
`code` VARCHAR(64) NOT NULL,
`field_key` VARCHAR(64) NOT NULL COMMENT '只写显式声明 searchable 的字段(V3/V9)',
`v_str` VARCHAR(191) DEFAULT NULL COMMENT '文本/选项值;utf8mb4 下 767 字节索引上限,超长截断=前缀匹配',
`v_num` DECIMAL(20,6) DEFAULT NULL COMMENT '数值值(precision 白名单上界 6 与此同口径)',
`v_date` DATE DEFAULT NULL COMMENT '日期值(禁 TEXT 字典序比日期,方案 §4.3④/V4)',
`create_by` VARCHAR(255) DEFAULT NULL,
`create_time` DATETIME DEFAULT NULL,
`update_by` VARCHAR(255) DEFAULT NULL,
`update_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_ai_str` (`sch_id`, `code`, `field_key`, `v_str`, `answer_id`),
KEY `idx_ai_num` (`sch_id`, `code`, `field_key`, `v_num`, `answer_id`),
KEY `idx_ai_date` (`sch_id`, `code`, `field_key`, `v_date`, `answer_id`),
KEY `idx_ai_answer` (`answer_id`) COMMENT '软删同步删行用'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='索引值表:一 searchable 字段值一行(多值多行),同时支撑筛选与分组统计';
CREATE TABLE `status_log` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`answer_id` BIGINT NOT NULL,
`status_code` VARCHAR(64) NOT NULL,
`operator_id` INT NOT NULL COMMENT '操作人;填表人改状态在接口层即被拒(V14)',
`create_by` VARCHAR(255) DEFAULT NULL,
`create_time` DATETIME DEFAULT NULL,
`update_by` VARCHAR(255) DEFAULT NULL,
`update_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_sl_answer` (`answer_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='状态留痕:只记"谁把这条标成什么",系统内不存在任何转移规则校验(V15)';
CREATE TABLE `answer_op_log` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`answer_id` BIGINT NOT NULL,
`op` VARCHAR(32) NOT NULL COMMENT 'edit/soft_delete/change_status/batch_soft_delete/batch_change_status',
`operator_id` INT NOT NULL,
`summary` VARCHAR(500) DEFAULT NULL COMMENT '改动摘要/批次上下文(批量逐条一行,与单条同形)',
`create_by` VARCHAR(255) DEFAULT NULL,
`create_time` DATETIME DEFAULT NULL,
`update_by` VARCHAR(255) DEFAULT NULL,
`update_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_ol_answer` (`answer_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='操作留痕(方案 §5.4)';
CREATE TABLE `fill_quota` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`form_id` BIGINT NOT NULL COMMENT '限次是表单级(跨版本共享名额)',
`limit_key` VARCHAR(128) NOT NULL COMMENT 'coalesce(subject_id, submitter_id) 字符串化',
`create_by` VARCHAR(255) DEFAULT NULL,
`create_time` DATETIME DEFAULT NULL,
`update_by` VARCHAR(255) DEFAULT NULL,
`update_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_quota` (`form_id`, `limit_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='限填登记:提交事务内插入、撞键即拒(并发兜底);软删答卷同步删行=释放名额';
CREATE TABLE `form_file` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`sch_id` BIGINT NOT NULL,
`file_name` VARCHAR(255) NOT NULL COMMENT '用户上传时的展示名(已剥路径分隔符与上跳)',
`store_key` VARCHAR(191) NOT NULL COMMENT '相对存储 key=form/yyyy/MM/{32hex}.{ext};不可猜测串即访问控制(方案 §7.7)。191 而非 255:本列有唯一键,utf8mb4 下单列索引 767 字节上限',
`file_size` BIGINT NOT NULL COMMENT '字节数(≤50MB 全局兜底在上传时判)',
`ext` VARCHAR(16) NOT NULL COMMENT '归一化小写扩展名(黑名单判定结果留档)',
`create_by` VARCHAR(255) DEFAULT NULL,
`create_time` DATETIME DEFAULT NULL,
`update_by` VARCHAR(255) DEFAULT NULL,
`update_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_form_file_key` (`store_key`),
KEY `idx_form_file_sch` (`sch_id`, `id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='中心上传附件落档(值形状 {id,fileName,key} 里的 id 由本表提供;绝不存绝对 URL)';