duxingchen
9693e9b388
feat(product): 产品序列号在同一规格型号下唯一
用户手填的「产品序列号」此前完全没有唯一性校验,填重复值静默通过 —— 而它是要跟 MOM 设备身份对上的业务号,重号会让对账与追溯失去锚点。
口径按业务确认:同一规格型号下不允许重复,不同规格型号之间允许相同;不填(空/纯空白)不参与校验。落地为部分唯一表达式索引 COALESCE(spec_model,'') + btrim(external_serial),WHERE deleted_at IS NULL AND external_serial IS NOT NULL AND btrim(external_serial) <> '':
· COALESCE 让 spec_model 为空的产品也归入同一组 —— 裸列比较下 NULL≠NULL 会把它们漏放,等于开一个唯一性漏斗;
· 显式排除空值:空串在索引里是实在的值会互相冲突,NULL 才天然互不相等,只有统一成 NULL,「不填」才能真正做到任意多个都放行;
· deleted_at IS NULL 与既有的 ix_products_serial_number 同口径,软删行不占位,支持「删掉重贴」。
服务层查重与索引逐字同口径,并捕获 IntegrityError 兜住并发穿透 —— 否则冲突会落到兜底 handler 变成 500。restore_product 补了 external_serial 冲突的专门文案:它原来的提示只提 16 位序列号,加上新约束后会把人引向错误的排查方向。
迁移刻意不做数据回填:存量若有重复,CREATE INDEX 会直接失败并整段回滚(本地已实测)。配套的只读扫描脚本 report_external_serial_dupes 就是这条「先报告、人工清、再迁移」流程的量尺。