match_test.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. package migrate
  2. import "testing"
  3. // TestPlannerDecide 验证置信分级 + 冲突检测(P0-07 §2.2 匹配优先级)。
  4. func TestPlannerDecide(t *testing.T) {
  5. // 构造测试数据:
  6. // - user 1: phone 13800000001(唯一)+ 现映射种子 → 应判 seed/100
  7. // - user 2: phone 13800000002(唯一,无种子)→ 应判 phone/90
  8. // - user 3: phone 13900000003(撞号,user 4 同号)→ 应判 conflict/0
  9. // - user 4: phone 13900000003(撞号)→ 应判 conflict/0
  10. // - user 5: 无 phone,有微信 unionid → 应判 unionid/70
  11. // - user 6: 无 phone、无微信 → 应判 unmapped/50
  12. // - user 7: phone 归一失败(非大陆号)→ 应判 unmapped/50 + PhoneInvalid
  13. users := []LegacyUser{
  14. {ID: 1, PhoneNumber: ptr("13800000001")},
  15. {ID: 2, PhoneNumber: ptr("13800000002")},
  16. {ID: 3, PhoneNumber: ptr("13900000003")},
  17. {ID: 4, PhoneNumber: ptr("13900000003")},
  18. {ID: 5},
  19. {ID: 6},
  20. {ID: 7, PhoneNumber: ptr("12345")},
  21. }
  22. assoc := []LegacyAccountAssociation{
  23. {UserID: 1, MarkingSystemUserID: ptr[int32](999)}, // user 1 有现映射
  24. }
  25. wx := []LegacyWeixinUser{
  26. {OpenID: "openid5", UserID: 5, UnionID: ptr("union5"), AppID: ptr("appid5")},
  27. }
  28. p := NewPlanner(users, assoc, wx)
  29. t.Run("seed 优先", func(t *testing.T) {
  30. d := p.Decide(1, users[0].PhoneNumber)
  31. if d.MatchSource != MatchSourceSeed || d.Confidence != ConfidenceSeed {
  32. t.Fatalf("user1 期望 seed/100, 得 %s/%d", d.MatchSource, d.Confidence)
  33. }
  34. })
  35. t.Run("phone 唯一", func(t *testing.T) {
  36. d := p.Decide(2, users[1].PhoneNumber)
  37. if d.MatchSource != MatchSourcePhone || d.Confidence != ConfidencePhone {
  38. t.Fatalf("user2 期望 phone/90, 得 %s/%d", d.MatchSource, d.Confidence)
  39. }
  40. })
  41. t.Run("phone 撞号冲突", func(t *testing.T) {
  42. for _, id := range []int32{3, 4} {
  43. raw := users[id-1].PhoneNumber
  44. d := p.Decide(id, raw)
  45. if d.MatchSource != MatchSourceConflict || !d.PhoneConflict || d.Confidence != ConfidenceConflict {
  46. t.Fatalf("user%d 期望 conflict/0+PhoneConflict, 得 %s/%d conflict=%v",
  47. id, d.MatchSource, d.Confidence, d.PhoneConflict)
  48. }
  49. }
  50. })
  51. t.Run("unionid 锚点", func(t *testing.T) {
  52. d := p.Decide(5, nil)
  53. if d.MatchSource != MatchSourceUnionid || d.Confidence != ConfidenceUnionid || !d.HasWechat {
  54. t.Fatalf("user5 期望 unionid/70+HasWechat, 得 %s/%d wechat=%v", d.MatchSource, d.Confidence, d.HasWechat)
  55. }
  56. })
  57. t.Run("unmapped", func(t *testing.T) {
  58. d := p.Decide(6, nil)
  59. if d.MatchSource != MatchSourceUnmapped || d.Confidence != ConfidenceUnmapped {
  60. t.Fatalf("user6 期望 unmapped/50, 得 %s/%d", d.MatchSource, d.Confidence)
  61. }
  62. })
  63. t.Run("phone 归一失败", func(t *testing.T) {
  64. d := p.Decide(7, users[6].PhoneNumber)
  65. if !d.PhoneInvalid {
  66. t.Fatalf("user7 期望 PhoneInvalid=true, 得 false")
  67. }
  68. })
  69. // 撞号组:仅 13900000003 一组(user 3,4)
  70. groups := p.PhoneConflictGroups()
  71. if len(groups) != 1 {
  72. t.Fatalf("期望 1 组撞号, 得 %d 组", len(groups))
  73. }
  74. if g, ok := groups["13900000003"]; !ok || len(g) != 2 {
  75. t.Fatalf("撞号组 13900000003 应含 2 账号, 得 %v", g)
  76. }
  77. }
  78. // TestPlannerSeedPriorityOverPhone 验证现映射种子优先级高于 phone(即使 phone 也撞号)。
  79. func TestPlannerSeedPriorityOverPhone(t *testing.T) {
  80. users := []LegacyUser{
  81. {ID: 10, PhoneNumber: ptr("13700000000")},
  82. {ID: 11, PhoneNumber: ptr("13700000000")}, // 撞号
  83. }
  84. assoc := []LegacyAccountAssociation{{UserID: 10, MarkingSystemUserID: ptr[int32](1)}} // user 10 有种子
  85. p := NewPlanner(users, assoc, nil)
  86. d := p.Decide(10, users[0].PhoneNumber)
  87. // 即使 phone 撞号,user 10 有现映射种子 → seed/100(优先级最高)
  88. if d.MatchSource != MatchSourceSeed || d.Confidence != ConfidenceSeed {
  89. t.Fatalf("user10 期望 seed/100(种子优先), 得 %s/%d", d.MatchSource, d.Confidence)
  90. }
  91. // user 11 无种子 → conflict
  92. d2 := p.Decide(11, users[1].PhoneNumber)
  93. if d2.MatchSource != MatchSourceConflict {
  94. t.Fatalf("user11 期望 conflict, 得 %s", d2.MatchSource)
  95. }
  96. }
  97. // TestPlannerEmptyPhoneSkipsIndex 验证空/无效 phone 不进撞号索引。
  98. func TestPlannerEmptyPhoneSkipsIndex(t *testing.T) {
  99. users := []LegacyUser{
  100. {ID: 1, PhoneNumber: nil},
  101. {ID: 2, PhoneNumber: ptr("")},
  102. {ID: 3, PhoneNumber: ptr(" ")},
  103. }
  104. p := NewPlanner(users, nil, nil)
  105. if len(p.phoneIndex) != 0 {
  106. t.Fatalf("空 phone 不进索引, 得 %d 项", len(p.phoneIndex))
  107. }
  108. }
  109. func ptr[T any](v T) *T { return &v }