package migrate import "testing" // TestPlannerDecide 验证置信分级 + 冲突检测(P0-07 §2.2 匹配优先级)。 func TestPlannerDecide(t *testing.T) { // 构造测试数据: // - user 1: phone 13800000001(唯一)+ 现映射种子 → 应判 seed/100 // - user 2: phone 13800000002(唯一,无种子)→ 应判 phone/90 // - user 3: phone 13900000003(撞号,user 4 同号)→ 应判 conflict/0 // - user 4: phone 13900000003(撞号)→ 应判 conflict/0 // - user 5: 无 phone,有微信 unionid → 应判 unionid/70 // - user 6: 无 phone、无微信 → 应判 unmapped/50 // - user 7: phone 归一失败(非大陆号)→ 应判 unmapped/50 + PhoneInvalid users := []LegacyUser{ {ID: 1, PhoneNumber: ptr("13800000001")}, {ID: 2, PhoneNumber: ptr("13800000002")}, {ID: 3, PhoneNumber: ptr("13900000003")}, {ID: 4, PhoneNumber: ptr("13900000003")}, {ID: 5}, {ID: 6}, {ID: 7, PhoneNumber: ptr("12345")}, } assoc := []LegacyAccountAssociation{ {UserID: 1, MarkingSystemUserID: ptr[int32](999)}, // user 1 有现映射 } wx := []LegacyWeixinUser{ {OpenID: "openid5", UserID: 5, UnionID: ptr("union5"), AppID: ptr("appid5")}, } p := NewPlanner(users, assoc, wx) t.Run("seed 优先", func(t *testing.T) { d := p.Decide(1, users[0].PhoneNumber) if d.MatchSource != MatchSourceSeed || d.Confidence != ConfidenceSeed { t.Fatalf("user1 期望 seed/100, 得 %s/%d", d.MatchSource, d.Confidence) } }) t.Run("phone 唯一", func(t *testing.T) { d := p.Decide(2, users[1].PhoneNumber) if d.MatchSource != MatchSourcePhone || d.Confidence != ConfidencePhone { t.Fatalf("user2 期望 phone/90, 得 %s/%d", d.MatchSource, d.Confidence) } }) t.Run("phone 撞号冲突", func(t *testing.T) { for _, id := range []int32{3, 4} { raw := users[id-1].PhoneNumber d := p.Decide(id, raw) if d.MatchSource != MatchSourceConflict || !d.PhoneConflict || d.Confidence != ConfidenceConflict { t.Fatalf("user%d 期望 conflict/0+PhoneConflict, 得 %s/%d conflict=%v", id, d.MatchSource, d.Confidence, d.PhoneConflict) } } }) t.Run("unionid 锚点", func(t *testing.T) { d := p.Decide(5, nil) if d.MatchSource != MatchSourceUnionid || d.Confidence != ConfidenceUnionid || !d.HasWechat { t.Fatalf("user5 期望 unionid/70+HasWechat, 得 %s/%d wechat=%v", d.MatchSource, d.Confidence, d.HasWechat) } }) t.Run("unmapped", func(t *testing.T) { d := p.Decide(6, nil) if d.MatchSource != MatchSourceUnmapped || d.Confidence != ConfidenceUnmapped { t.Fatalf("user6 期望 unmapped/50, 得 %s/%d", d.MatchSource, d.Confidence) } }) t.Run("phone 归一失败", func(t *testing.T) { d := p.Decide(7, users[6].PhoneNumber) if !d.PhoneInvalid { t.Fatalf("user7 期望 PhoneInvalid=true, 得 false") } }) // 撞号组:仅 13900000003 一组(user 3,4) groups := p.PhoneConflictGroups() if len(groups) != 1 { t.Fatalf("期望 1 组撞号, 得 %d 组", len(groups)) } if g, ok := groups["13900000003"]; !ok || len(g) != 2 { t.Fatalf("撞号组 13900000003 应含 2 账号, 得 %v", g) } } // TestPlannerSeedPriorityOverPhone 验证现映射种子优先级高于 phone(即使 phone 也撞号)。 func TestPlannerSeedPriorityOverPhone(t *testing.T) { users := []LegacyUser{ {ID: 10, PhoneNumber: ptr("13700000000")}, {ID: 11, PhoneNumber: ptr("13700000000")}, // 撞号 } assoc := []LegacyAccountAssociation{{UserID: 10, MarkingSystemUserID: ptr[int32](1)}} // user 10 有种子 p := NewPlanner(users, assoc, nil) d := p.Decide(10, users[0].PhoneNumber) // 即使 phone 撞号,user 10 有现映射种子 → seed/100(优先级最高) if d.MatchSource != MatchSourceSeed || d.Confidence != ConfidenceSeed { t.Fatalf("user10 期望 seed/100(种子优先), 得 %s/%d", d.MatchSource, d.Confidence) } // user 11 无种子 → conflict d2 := p.Decide(11, users[1].PhoneNumber) if d2.MatchSource != MatchSourceConflict { t.Fatalf("user11 期望 conflict, 得 %s", d2.MatchSource) } } // TestPlannerEmptyPhoneSkipsIndex 验证空/无效 phone 不进撞号索引。 func TestPlannerEmptyPhoneSkipsIndex(t *testing.T) { users := []LegacyUser{ {ID: 1, PhoneNumber: nil}, {ID: 2, PhoneNumber: ptr("")}, {ID: 3, PhoneNumber: ptr(" ")}, } p := NewPlanner(users, nil, nil) if len(p.phoneIndex) != 0 { t.Fatalf("空 phone 不进索引, 得 %d 项", len(p.phoneIndex)) } } func ptr[T any](v T) *T { return &v }