上周一家教培客户报障:同一个孩子,系统里躺着两份档案。妈妈买的课时扣在一份上,爸爸约的课记在另一份上,作业交了两份,老师点名时点到两个"同名"的孩子。
一路排查下来,根因并不在谁操作失误,而在数据模型:系统只按"操作人手机号"判唯一,没有按"孩子"判重。本文复盘这次重复档案从产生、定位到合并的全过程,给一套多家长联系人加学员主档的建模和合并防重方案。
一、事故现场:一个孩子怎么变成两份档案
先看现象。花名册按姓名一搜,同一个名字出两条记录;课时余额对不上,妈妈购买的十节课时在 A 档,爸爸预约的课程却挂在 B 档;作业和打卡各有一份;点名时一个班出现两个相同名字。
根因是注册和建档只认操作人的手机号。爸爸、妈妈各自注册账号,又都给同一个孩子建立档案,于是产生两条学员记录,每条各挂一个家长。系统之所以没拦住,是因为两个手机号确实不同,看起来像两个不同用户;而姓名相同本就允许重名,单凭名字无法判定为同一人。
二、先定位:怎么找出"其实是同一个孩子"的档案
判重不能只靠姓名,同名孩子很常见。要按特征强弱组合识别:
- 强特征:姓名加出生日期、证件号、班级加姓名。
- 弱特征:性别、联系电话是否重叠、家庭地址、紧急联系人。
先用一条 SQL 把高疑似对筛出来:
SELECT name, birth_date, COUNT(*) AS cnt, GROUP_CONCAT(student_id) AS ids
FROM student
GROUP BY name, birth_date
HAVING COUNT(*) > 1;
命中后必须人工确认,不能自动合并,因为确实存在姓名、出生日期都相同却并非同一人的情况。系统只负责列出疑似对和各自的班级、联系人,由教务核对后再决定。
三、关键:把"学员"和"家长联系人"拆成两个实体
重复的根源,是模型把家长账号和学员绑成了一条记录。正确的做法是学员主档与联系人分开,并用关系表连接:
CREATE TABLE student (
student_id BIGINT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
birth_date DATE,
gender VARCHAR(4),
class_id BIGINT,
status VARCHAR(20) DEFAULT 'active' -- active/merged
);
CREATE TABLE contact (
contact_id BIGINT PRIMARY KEY,
name VARCHAR(50),
phone VARCHAR(20),
account_id BIGINT
);
CREATE TABLE student_contact (
student_id BIGINT NOT NULL,
contact_id BIGINT NOT NULL,
relation VARCHAR(10), -- 父亲/母亲/其他
is_primary TINYINT,
PRIMARY KEY (student_id, contact_id)
);
这样爸爸、妈妈是两个联系人,却指向同一个学员主档;一个联系人也能关联多个学员,覆盖兄弟姐妹的场景。建档流程也要随之调整:家长添加孩子时,先按姓名加出生日期或班级做疑似匹配,提示"该学员可能已存在,是否关联到已有档案",而不是直接新建。
四、合并重复档案:数据怎么迁、冲突怎么解
合并的思路是保留一条主档,把另一条的关联数据迁过来,副档做归档标记。
迁移要分对象处理。联系人方面,两边的联系人都挂到主档,同一手机号只保留一条。课时资产要特别谨慎,两个账户购买的课包应按原始订单逐包核对后保留或合并,不能把数字简单相加,否则容易把已退、赠送的课时也算进去。约课、消课、作业、打卡、成绩、点名记录,全部改挂到主档 ID。
冲突按下面的规则解:
- 同一节课两边都预约了,只保留一条有效预约,另一条按重复预约释放。
- 出生日期等字段两版不一致时,以有证件、信息更完整的一版为准,差异记录留痕。
整个合并要在一个事务内完成,副档不物理删除,而是打上 merged 标记并记录并入的主档 ID,便于历史追溯:
START TRANSACTION;
UPDATE booking SET student_id = #{masterId} WHERE student_id = #{dupId};
UPDATE homework SET student_id = #{masterId} WHERE student_id = #{dupId};
UPDATE student_contact SET student_id = #{masterId} WHERE student_id = #{dupId};
UPDATE student SET status = 'merged', merged_into = #{masterId}
WHERE student_id = #{dupId};
COMMIT;
五、怎么防止再次产生
入口要前置查重。家长端、教务端建档时,按姓名加出生日期、手机号、班级做疑似比对,命中后要求选择"关联已有学员"或"确认新建(确为重名)"。批量导入学员时先跑一遍重复比对,生成待确认清单,不直接插入。
数据库层只对强标识加约束,例如证件号、班级内业务学号加唯一索引;姓名这类弱标识不加唯一约束,以免真正重名的孩子无法建档,弱标识靠应用层提示来兜底。
六、踩坑清单
- 把家长手机号当成学员唯一键,多个家长必然建出重复档,学员与联系人要拆开。
- 只按姓名判重,会误合并同名孩子,要用多特征加人工确认。
- 合并时课时直接相加,容易把已退、赠送课时算错,要按订单逐包核对。
- 物理删除副档,会让历史约课、订单失去关联,应打标记归档。
- 合并不在事务内,迁一半失败会产生孤儿数据。
- 给姓名加唯一索引,真重名无法建档,弱标识不建唯一。
七、工程落地建议
重复治理分两步。先一次性跑全量比对,由人工确认后批量合并存量;再把疑似查重前置到所有建档和导入入口,形成长期防线。
合并操作建议做成"可预览的合并工单":系统先列出将迁移的联系人、课包、记录和冲突项,教务确认后在一个事务内执行,工单保留以便回滚和审计。
八、复盘清单
- 学员主档与联系人是否拆成多对多关系。
- 判重是否使用多特征并由人工确认。
- 课时是否按原始订单逐包核对。
- 副档是否归档保留,合并是否在事务内完成。
- 建档、导入入口是否都前置了查重。
常见问题
同一个孩子被父母各注册一遍、出现两份档案,怎么处理?
先按姓名加出生日期、班级等特征确认确为同一人,再保留一条主档,把两边的联系人、课包、约课和作业迁到主档,副档标记合并归档,课时按原始订单逐包核对。
只按姓名判重,会不会误合并同名孩子?
会。重名很常见,姓名只能作为弱参考,要结合出生日期、班级、证件等强特征,命中后由人工确认,不能自动合并。
怎么从源头避免一个孩子出现多份档案?
把学员主档和家长联系人拆成多对多关系,家长添加孩子时先做全局疑似匹配并提示关联,导入和建档入口统一前置查重。
学员档案看似是简单的增删改查,但一旦和家长账号、课时资产纠缠在一起,重复数据就会直接影响课时账和教学记录。本文为工程实践分享,具体规则以各机构实际业务为准。