恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
MySQL 唯一索引遇到空手机号,后台会员表该怎么处理?
首页
资讯中心
/
MySQL 唯一索引遇到空手机号,后台会员表该怎么处理?
MySQL 唯一索引遇到空手机号,后台会员表该怎么处理?
发布时间:2026/9/5 2:54:25
后台会员手机号如果允许不填数据库里更稳的是存NULL不要存空字符串。空字符串是一个真实字段值唯一索引会把第二个空字符串当成重复NULL表示未知值MySQL 和 PostgreSQL 的唯一索引都允许多行NULL。排查时别只看页面表单有没有填手机号要把数据库字段、历史数据、导入接口、编辑 API 和唯一索引一起验收。XYGo Admin 在 v1.4.6 里处理过这个问题会员mobile从NOT NULL DEFAULT 改为可空历史空字符串统一归一为NULL并补了 PostgreSQL 的手机号唯一索引。这篇按 2026-09-04 的仓库状态写只把它当成源码样本不把任何项目说成通用银弹。先复现为什么第二个空手机号会报唯一键冲突很多后台一开始会把手机号字段写成这样CREATE TABLE xy_member ( id bigint PRIMARY KEY AUTO_INCREMENT, username varchar(64) NOT NULL, mobile varchar(20) NOT NULL DEFAULT , created_at datetime, updated_at datetime, UNIQUE KEY uk_mobile (mobile) );看起来没问题手机号有唯一索引页面允许不填后端没有收到手机号时就落一个空字符串。问题要到第二个无手机号会员才暴露。INSERT INTO xy_member (id, username, mobile) VALUES (1, u001, ); INSERT INTO xy_member (id, username, mobile) VALUES (2, u002, ); -- Duplicate entry for key uk_mobile这不是 MySQL 抽风。不是“没有值”它是一个长度为 0 的字符串。唯一索引看到两行都是同一个值就必须拦住第二行。后台页面上看起来都是“未填写”数据库看到的是两个相同的字符串。如果字段改成可空再插入两行NULL语义就不一样了CREATE TABLE member_nullable ( id bigint PRIMARY KEY AUTO_INCREMENT, username varchar(64) NOT NULL, mobile varchar(20) DEFAULT NULL, UNIQUE KEY uk_mobile (mobile) ); INSERT INTO member_nullable (id, username, mobile) VALUES (1, u001, NULL); INSERT INTO member_nullable (id, username, mobile) VALUES (2, u002, NULL); -- 两条都能插入这里的判断很简单如果业务说“手机号可选但用户填了就不能重复”数据库字段应该允许NULL并把空字符串清洗掉。不要在页面层面把“没填”偷换成。迁移不能只改字段要先清洗历史空字符串线上表已经存在数据时不能只执行一条ALTER TABLE。老数据里的还在导入任务、批量编辑或旧接口也可能继续写空串。正确顺序是先明确目标语义再改字段再清洗历史值最后补接口验收。MySQL 版可以这样处理-- 1. 手机号字段允许 NULL ALTER TABLE xy_member MODIFY COLUMN mobile varchar(20) DEFAULT NULL COMMENT 手机号; -- 2. 历史空字符串归一为 NULL UPDATE xy_member SET mobile NULL WHERE mobile ; -- 3. 唯一索引 uk_mobile 保持不变 -- MySQL 唯一索引允许多个 NULL不需要为了 NULL 重建索引XYGo Admin 的server/cmd_tools/migrate/1.4.6_member_mobile_nullable.mysql.sql也是这个方向。脚本注释写得很直mobile原来是NOT NULL DEFAULT 并且有唯一索引uk_mobile多个无手机号会员写入空字符串会触发唯一键冲突处理方式是改为可空空值统一存NULL。我会额外加一条排查 SQL确认线上还没有残留空串SELECT COUNT(*) AS empty_mobile_count FROM xy_member WHERE mobile ; SELECT mobile, COUNT(*) AS c FROM xy_member WHERE mobile IS NOT NULL AND mobile GROUP BY mobile HAVING c 1;第一条查空字符串残留第二条查真实手机号重复。迁移前如果第二条已经有数据别急着执行 DDL。那是业务脏数据需要先人工合并、作废或改成更完整的唯一约束规则。PostgreSQL 也允许多个 NULL但迁移脚本不完全一样容易被忽略的是 PostgreSQL。很多后台项目支持 MySQL 和 PostgreSQL表结构迁移不能只复制一份 MySQL DDL。PostgreSQL 版通常要拆成几步ALTER TABLE xy_member ALTER COLUMN mobile DROP NOT NULL; ALTER TABLE xy_member ALTER COLUMN mobile DROP DEFAULT; UPDATE xy_member SET mobile NULL WHERE mobile ; CREATE UNIQUE INDEX IF NOT EXISTS uk_member_mobile ON xy_member (mobile);XYGo Admin 的server/cmd_tools/migrate/1.4.6_member_mobile_nullable.pgsql.sql做了这几个动作放宽mobile的NOT NULL去掉默认空串把历史空串改为NULL再补uk_member_mobile唯一索引。这个差异值得放进 code review。MySQL 侧原来已有uk_mobilePostgreSQL 侧如果缺索引只改字段可空仍挡不住真实手机号重复。如果你的项目维护两套迁移脚本我建议每次改“可选但唯一”的字段时都查这几项# 迁移脚本是否同时覆盖 MySQL / PostgreSQL # 这里用 rg 举例CI 里也可以换成固定路径检查 rg mobile server/cmd_tools/migrate rg UNIQUE|unique|uk_mobile|uk_member_mobile server/cmd_tools/migrate rg DEFAULT |DROP DEFAULT|DEFAULT NULL server/cmd_tools/migrateAI 或代码生成器最容易漏的不是语法而是“两个数据库的约束语义是否一起改了”。页面能新增不代表导入、编辑和跨库迁移都没问题。后台接口怎么验收新增、编辑、导入要分开测数据库改完以后接口还要跟上。否则页面新增存NULL批量导入还在写几天后还是会报错。我通常按三条链路验收。第一条是新增会员。空手机号应该落NULL不是。curl -i -X POST https://admin.example.com/admin/member/create \ -H Authorization: Bearer token \ -H Content-Type: application/json \ -d {username:u001,mobile:} curl -i -X POST https://admin.example.com/admin/member/create \ -H Authorization: Bearer token \ -H Content-Type: application/json \ -d {username:u002}第二条是编辑会员。把已有手机号清空时也应该变成NULL。SELECT id, username, mobile, mobile IS NULL AS mobile_is_null FROM xy_member WHERE username IN (u001, u002);第三条是导入会员。CSV、Excel、后台导入脚本经常绕过表单校验必须单独测。导入两条手机号为空的数据后再查一次空串残留SELECT COUNT(*) AS empty_mobile_count FROM xy_member WHERE mobile ;如果这条不是 0说明还有入口在写旧语义。别只在前端组件里改requiredfalse后端入参归一、导入解析和数据库迁移要一起改。权限和日志也要看一眼不然排障会断线手机号迁移看起来是数据库问题但后台系统里还牵涉权限和审计。低权限账号不应该能触发会员导入或迁移入口失败日志要能看到是唯一键冲突、字段约束还是导入数据本身脏。可以用这类验收命令看接口边界# 未登录应该是 401 curl -i -X POST https://admin.example.com/admin/member/import \ -F filemembers.csv # 登录但无导入权限应该是 403 curl -i -X POST https://admin.example.com/admin/member/import \ -H Authorization: Bearer low_permission_token \ -F filemembers.csv # 有权限账号才进入导入逻辑 curl -i -X POST https://admin.example.com/admin/member/import \ -H Authorization: Bearer admin_token \ -F filemembers.csvXYGo Admin 本身是 GoFrame Vue3 后台项目带 RBAC 和 CRUD 生成器。这里可参考的不是“直接套项目”而是它把会员手机号可空这个修复放进 Release v1.4.6 和迁移脚本里同时仓库里有server/internal/middleware/admin_permission.go这类后台权限中间件。数据库约束、接口权限和代码生成器输出要放在同一轮 review 里看才不容易只修了页面。规范来源和源码入口放一个就够了https://github.com/z312193608/xygo-admin。核验时点是 2026-09-04GitHub 仓库公开最新 tag 是 v1.4.9GitHub Releases API 的 latest 对象仍是 v1.4.6所以本文只把 v1.4.6 当作公开 Release 证据不把 v1.4.9 写成 Release。适用场景这套处理适合“可选但填了必须唯一”的字段比如会员手机号、客户手机号、备用邮箱、企业统一社会信用代码、第三方账号 unionid。它的共同点是业务允许暂时没有值但一旦填了就要能定位唯一主体。也适合 AI 生成后台或代码生成器改表后的 review。AI 很可能把页面必填、数据库默认值、唯一索引和导入模板分开处理看上去每个文件都能编译但放到真实数据里就撞唯一键。不适用场景不要把所有唯一字段都改成可空。登录账号、订单号、支付流水号、租户编码这类字段通常必须有值而且不能靠多个NULL混过去。手机号如果已经是登录凭证还要考虑账号合并、短信验证、脱敏展示和风控规则不能只靠一条 DDL 解决。还有一种情况是业务要求“没有手机号也只能有一个特殊占位账号”。那就不要用NULL表示未知值应该单独设计状态字段或占位记录。数据库能允许多个NULL不代表业务一定需要多个空手机号用户。上线前我会保留这张检查表检查项要看的地方期望结果字段定义MySQL / PostgreSQL 迁移脚本mobile可空不再默认空字符串历史数据WHERE mobile 结果为 0唯一索引SHOW INDEX或pg_indexes真实手机号仍唯一新增接口后台 create API空手机号落NULL编辑接口后台 update API清空手机号后落NULL导入接口CSV / Excel 导入不再写入权限接口导入、批量编辑、迁移入口未登录 401低权限 403日志操作日志和错误日志能看到唯一键冲突或脏数据原因这类问题的坑不在“NULL 和空字符串谁更高级”而在语义有没有统一。页面说可选数据库却把空值当成真实值MySQL 改了PostgreSQL 没改新增接口修了导入还在写旧值。只要这几条链路一起验会员手机号可空就不会变成下一次线上导入事故。