Files
form-center-service/sql/t3_answer_domain.sql
T
chenxing 7af6a98fc0 fix(post-p95): 跨版本编辑必挂/编辑不吃截止闸/自填 subject 归一/报名号并发重号
P95 落地后的独立修复批,四条存量逻辑硬伤(填报入口层不动):

- 跨版本编辑必挂:客户端按当前发布版渲染、服务端按钉住旧版校验,升版后编辑永远
  400(含未知字段/缺必填)。改为编辑态吃 /my/detail 的钉住版 groups。
- 编辑不吃截止/停用闸:先占位、截止后随便改可绕过 deadline。editBySubmitter 补
  截止/停用/已删三闸,与 submit 同口径。
- 自填 subject 落 NULL:subject_id 为空的自填行进不了"关于我的"(/my/about 与
  ABOUT_ME 谓词都按 subject 查),V18"含代填"实际退化成"仅代填"。落库归一为
  coalesce(subjectId, submitterId);限次键本就是 coalesce,不受影响。
- answer_no 并发重号:count+1 的 count 与 insert 之间有并发窗口,报名号是对外凭证。
  加 uk_answer_no 兜底 + 撞号重算序号重试(3 次封顶),按异常消息列名区分撞的是
  报名号还是幂等键(MySQL/H2 通用)。

语义变更备案:自填行 subject 由 NULL 变本人,读矩阵"学生x面向学生=关于我"这一格
此后含自填行,可见集变大(DataScopeService/矩阵规则一字未动,变的是数据形状)。
三处断言随行为更新:AdminScopeIT、AnswerReadIT、MyAnswerIT(+ReadFixture 注释)。

验证:mvn 261 绿(+2 新用例:并发报名号不重复、编辑闸三态)。
2026-10-16 15:04:20 +08:00

131 lines
9.4 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 逐列比对一致。
-- T4 变更(本轮):①answer 增列 `form_uuid VARCHAR(32) NOT NULL`(P25 裁决落地,钉版溯源精确坐标);
-- ②answer 增索引 `idx_answer_form_time`(方案 §5.5 铁律③,四读出路径 keyset 游标的覆盖索引)。
-- 开发库 answer 当时零行 → NOT NULL 无需回填;已建库由 ddl-auto=update 追加列与索引,
-- 生产(ddl-auto=none)升级即照本脚本这两条 ALTER/CREATE INDEX 执行。
-- ============================================================
CREATE TABLE `answer` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`uuid` VARCHAR(32) NOT NULL COMMENT '数据面运行时身份(详情 URL/嵌入地址/L2 引用,契约 §1)',
`form_uuid` VARCHAR(32) NOT NULL COMMENT '提交时解析命中的表单 uuid——钉版溯源的精确坐标(P25 裁决:遮蔽歧义根治,读改路径一律按它取版本行)',
`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`),
-- Bug#4 变更(本轮):报名号并发重号兜底。answer_no 是 count+1(当日已有号数)生成,
-- 同表单同日并发提交会算出同一个号;号是对外凭证,库级唯一才拦得住。
-- null 不参与唯一判定(未开报名号的表单该列恒 null,多行共存不受影响)。
UNIQUE KEY `uk_answer_no` (`sch_id`, `app_code`, `code`, `answer_no`),
KEY `idx_answer_form` (`sch_id`, `code`, `status`, `id`),
KEY `idx_answer_submitter` (`sch_id`, `submitter_id`),
KEY `idx_answer_subject` (`subject_id`),
KEY `idx_answer_form_time` (`sch_id`, `app_code`, `code`, `is_deleted`, `id`) COMMENT 'T4 铁律③:单表单列表/导出/计数 keyset 游标的覆盖索引'
) 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)';