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 新用例:并发报名号不重复、编辑闸三态)。
131 lines
9.4 KiB
SQL
131 lines
9.4 KiB
SQL
-- ============================================================
|
||
-- 表单中心 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)';
|