-- ============================================================================ -- 智能飞禽物联平台 · 项目自有补丁脚本(PROJECT PATCH,非框架基线) -- 文件:script/sql/bm_patch.sql -- 说明:本文件承载「项目自有的库表变更」,与框架基线 ry_vue.sql 分离维护, -- 以便将来框架(RuoYi-Vue-Plus)升级时按补丁重放、避免与基线冲突。 -- 【请勿修改 ry_vue.sql 以保持与上游基线一致】。 -- 执行方式:连接业务库(bird_monitoring)后整体执行本文件。 -- ============================================================================ -- ---------------------------------------------------------------------------- -- 补丁 1:为 sys_user.phone_number 增加唯一索引(uk_sys_user_phone_number) -- ---------------------------------------------------------------------------- -- 背景: -- 框架基线 ry_vue.sql 中 sys_user.phone_number 仅有普通索引 -- key idx_sys_user_phone (phone_number) -- 非唯一 -- 无法在 DB 层阻止「同一手机号注册多个账号」。 -- APP 账号闭环(迭代 I2)的注册为「先查后插」,存在并发同号双写(TOCTOU)风险; -- 应用层已用「按手机号加 Redisson 锁 + 判重」将其降到接近零(见 AppAuthServiceImpl#register), -- 但 **DB 唯一键才是最终防线**,故以本补丁补充该约束;应用层锁保留作为双保险。 -- -- ★ 执行前必读 · 前置检查(务必先跑 SELECT,确认无重复再执行 ALTER) -- 1) 检查是否存在重复的「非空」手机号: -- SELECT phone_number, COUNT(*) AS cnt -- FROM sys_user -- WHERE phone_number IS NOT NULL AND phone_number <> '' -- GROUP BY phone_number -- HAVING COUNT(*) > 1; -- → 期望返回 0 行。若返回了行,须先人工清理重复数据(每个手机号仅保留一条), -- 再执行本补丁;否则第 2 步 ALTER 会因重复键失败(1062)。 -- -- 2) 空字符串归一化说明: -- phone_number 列默认值为 ''(可存在多行同为 ''),而 MySQL 唯一索引 -- **不允许多个 ''(空串)**、但**允许多个 NULL**(NULL 不参与唯一性判定)。 -- 因此本补丁先将 '' 归一化为 NULL,使唯一约束只作用于「非空手机号」。 -- -- ★ 执行前已核验(2026-09-16 实测线上库 bird_monitoring.sys_user) -- - 总行数:3; -- - 非空 phone_number:仅 1 行(属 sys_user); -- - 重复手机号:无(前置检查 SELECT 返回 0 行); -- - app_user 记录:0 条。 -- => 满足前置条件,可安全执行。执行后如需复核: -- SHOW INDEX FROM sys_user WHERE Key_name = 'uk_sys_user_phone_number'; -- -- ★ MySQL 限制说明(不要用存储过程硬凑 IF NOT EXISTS) -- MySQL 不支持 "ALTER TABLE ... ADD UNIQUE KEY IF NOT EXISTS"。 -- 本补丁**未**用存储过程模拟幂等;若重复执行本文件,将出现如下**预期报错**(可忽略): -- ERROR 1061 (42000): Duplicate key name 'uk_sys_user_phone_number' -- 看到该报错即表示索引已存在,无需再次创建。 -- -- ★ 影响面提示(重要) -- 该唯一索引作用于整张 sys_user 表(含 sys_user 与 app_user 两类 user_type), -- 即执行后**手机号在全表范围内唯一**(非空手机号不可在「后台用户」与「APP 用户」间重复)。 -- 这与迭代 I2 早期「两类用户手机号互不冲突」的表述不同:现在由 DB 强制全局唯一。 -- 若后台新增用户时用到已被 APP 占用的手机号,将因该唯一键报 1062(属预期约束)。 -- -- ★ 回滚(如需撤销) -- ALTER TABLE sys_user DROP INDEX uk_sys_user_phone_number; -- ---------------------------------------------------------------------------- -- 步骤 1:将空字符串手机号归一化为 NULL(唯一索引允许多个 NULL) UPDATE sys_user SET phone_number = NULL WHERE phone_number = ''; -- 步骤 2:新增唯一索引 ALTER TABLE sys_user ADD UNIQUE KEY uk_sys_user_phone_number (phone_number); -- ---------------------------------------------------------------------------- -- 补丁 2:为 sys_user.email 增加唯一索引(uk_sys_user_email) -- ---------------------------------------------------------------------------- -- 背景: -- 迭代「邮箱注册」新增邮箱账号入口后,email 成为与 phone_number 并列的账号标识, -- 同样需要「先查后插」之外的第二道防线(DB 唯一键)。 -- 框架基线 ry_vue.sql 中 sys_user.email 无任何索引。 -- -- ★ 执行前必读 · 前置检查 -- 1) 重复非空邮箱检查: -- SELECT email, COUNT(*) AS cnt FROM sys_user -- WHERE email IS NOT NULL AND email <> '' -- GROUP BY email HAVING COUNT(*) > 1; -- → 期望返回 0 行。 -- 2) 空串归一化:email 列默认值为 ''(实测线上 9 行中 8 行为 ''), -- 而 MySQL 唯一索引允许多个 NULL 但**不允许多个 ''**,故必须先归一化。 -- -- ★ 执行前已核验(2026-09-17 实测线上库 bird_monitoring.sys_user) -- - 总行数 9;非空 email 仅 1 行(admin 的 crazyLionLi@163.com); -- - 重复邮箱:无(前置检查 SELECT 返回 0 行); -- - app_user 记录 8 条,email 全为 ''。 -- => 满足前置条件,可安全执行。 -- -- ★ 影响面提示 -- 与手机号同一口径:该唯一索引作用于整张 sys_user 表(含 sys_user 与 app_user), -- 即邮箱在全表范围内唯一。若后台新增用户填写了已被 APP 占用的邮箱,将报 1062(属预期约束)。 -- -- ★ 重复执行本文件时的预期报错(可忽略) -- ERROR 1061 (42000): Duplicate key name 'uk_sys_user_email' -- -- ★ 回滚 -- ALTER TABLE sys_user DROP INDEX uk_sys_user_email; -- ---------------------------------------------------------------------------- -- 步骤 1:将空字符串邮箱归一化为 NULL UPDATE sys_user SET email = NULL WHERE email = ''; -- 步骤 2:新增唯一索引 ALTER TABLE sys_user ADD UNIQUE KEY uk_sys_user_email (email); -- ---------------------------------------------------------------------------- -- 补丁 3:将 sys_user.phone_number / email 的列默认值 '' 改为 NULL -- ---------------------------------------------------------------------------- -- ★ 本补丁是「邮箱注册」能否落地的关键前提,不可省略。 -- -- 背景(两个坑叠加才会暴露): -- 1) 本项目 MyBatis-Plus 全局字段策略为 insertStrategy: NOT_NULL(见 -- ruoyi-common-mybatis/src/main/resources/common-mybatis.yml)—— -- 实体字段为 null 时,该列会被**整个从 INSERT 语句中省略**,而不是写入 NULL; -- 2) 此时生效的是**列默认值**,而 phone_number / email 的基线默认值都是 ''。 -- -- 于是:邮箱注册创建的账号不填手机号 ⇒ INSERT 省略 phone_number ⇒ 落库为 '' -- ⇒ 第二个邮箱注册账号再落 '' ⇒ 撞 uk_sys_user_phone_number 报 1062。 -- 同一问题对称存在于 email:手机号注册的账号不填邮箱 ⇒ 落 '' ⇒ 第二个撞 uk_sys_user_email。 -- -- 修法:把两列的默认值改为 NULL(NULL 不参与唯一性判定,可多行共存)。 -- 这样「字段为 null ⇒ INSERT 省略该列 ⇒ 落库 NULL」成为一致且唯一的行为, -- 无需修改框架实体 SysUser(不改框架文件的铁律得以保持)。 -- -- ★ 影响面提示(有意为之的正向变化) -- 后台「新增用户」不填邮箱/手机号时,落库由 '' 变为 NULL。 -- 应用层所有比较均已用 StringUtils.isBlank 处理('' 与 null 等价), -- 实测已确认无逻辑依赖 '' 这一具体取值。 -- -- ★ 回滚(恢复到基线默认值,回滚前须先处理已落 NULL 的行) -- ALTER TABLE sys_user MODIFY COLUMN phone_number varchar(11) DEFAULT '' COMMENT '手机号码'; -- ALTER TABLE sys_user MODIFY COLUMN email varchar(50) DEFAULT '' COMMENT '用户邮箱'; -- ---------------------------------------------------------------------------- -- 步骤 1:手机号列默认值改为 NULL ALTER TABLE sys_user MODIFY COLUMN phone_number varchar(11) DEFAULT NULL COMMENT '手机号码'; -- 步骤 2:邮箱列默认值改为 NULL ALTER TABLE sys_user MODIFY COLUMN email varchar(50) DEFAULT NULL COMMENT '用户邮箱';