-- 编号让位(2026-09-17):本文件原为 V150,与远端已推送的 V150__merchant_hero_image 撞号,本地让号改为 V153。 -- 商户端手输 6 位数字核销码的失败计数与锁定。 -- -- 核销码本来就下发一个 6 位数字码给顾客(order_dynamic_verification_code.display_code),但系统里 -- 没有任何地方能输入它:相机不好用或屏幕反光时,商户只能让顾客反复刷新二维码。补上手输入口的同时 -- 必须限次——数字码只有 100 万空间,不限次就是一个可以暴力枚举的核销入口。 -- -- 计数按「谁在输」记(商户 + 门店 + 操作员):连续输错到上限就锁定一段时间,锁定期间连校验都不做, -- 否则攻击者仍然可以用「这次报错是无效码还是已锁定」把枚举继续下去。 -- -- 幂等:`create table if not exists` 在 MySQL 5.7 与 8.0 上都能直接重复执行,所以这份迁移也可以 -- 用 scripts/apply-sql-file.ps1 直接落到开发库(开发库 Flyway 关闭、结构不由迁移维护)。 create table if not exists merchant_numeric_code_attempt ( id bigint not null auto_increment, merchant_id bigint not null, store_id bigint not null, operator_id bigint not null, consecutive_failures int not null default 0 comment '连续输错次数,成功或锁定时归零', locked_until datetime default null comment '锁定到期时间,未锁定为 NULL', last_attempt_at datetime not null default current_timestamp, primary key (id), unique key uk_merchant_numeric_code_attempt (merchant_id, store_id, operator_id) ) engine=InnoDB default charset=utf8mb4 comment='商户端手输数字核销码的失败计数与锁定';