norm.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. // Package norm 标识归一化纯函数(P0-06 §2/§3.3 设计稿落地)。
  2. //
  3. // 职责:把 legacy 原始标识收敛为目标态 canonical 形态。
  4. // 设计铁律:
  5. // - **纯函数**:无副作用、确定(同一输入恒同输出),可独立单测;
  6. // - **幂等**:归一(归一(x)) == 归一(x)(对已归一值再归一不变);
  7. // - **先归一后匹配**:归一失败绝不写脏 canonical(调用方据此跳过 t_identity 写入)。
  8. //
  9. // 四类标识归一力度(P0-06 §1):
  10. // - phone:强归一(trim → 去分隔符 → 剥国际前缀 → 11 位校验)——主力 key
  11. // - wechat:不做字符串变换(openid 已唯一),仅 trim;必须带 appid(见调用方)
  12. // - email:轻归一(trim + 小写 + 正则校验)——不作归一 key
  13. // - username:轻归一(trim + 小写)——绝不作自然人归一 key
  14. package norm
  15. import (
  16. "regexp"
  17. "strings"
  18. )
  19. // 大陆手机号正则(11 位:1 + 3-9 起始号段 + 9 位)
  20. var cnPhoneRe = regexp.MustCompile(`^1[3-9]\d{9}$`)
  21. // 分隔符:空白、横线、点(P0-06 §2.1 步 2)
  22. var sepRe = regexp.MustCompile(`[\s\-\.]`)
  23. // 邮箱最小可判定正则(P0-06 §2.3 步 4,不强求 RFC 5322 全量)
  24. var emailRe = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`)
  25. // Phone 归一:trim → 去分隔符 → 剥国际前缀 → 11 位大陆号校验。
  26. //
  27. // 规则(P0-06 §2.1 顺序执行):
  28. // 1. trim 去首尾空白
  29. // 2. 去掉所有空格、横线、点(sepRe)
  30. // 3. 去 + 号前缀
  31. // 4. 剥国际前缀(仅当剥后 11 位匹配大陆号段才剥,P0-06 §2.1 步 3):
  32. // - 前导 "0086"(4 位)
  33. // - 前导 "86"(2 位,须后接 11 位大陆号)
  34. // 5. 11 位大陆号校验:正则 ^1[3-9]\d{9}$
  35. // 6. 否则 → ok=false(phone_invalid,转 P0-08 人工)
  36. //
  37. // 入参:
  38. // - raw string:legacy t_user.phone_number 原始值(可能含分隔符/国际前缀/空白)。
  39. //
  40. // 出参:
  41. // - canonical string:归一后的 11 位大陆号(如 "13800000000");ok=false 时为空串。
  42. // - ok bool:true 表示归一成功(可写 t_identity.phone);false 表示 phone_invalid。
  43. //
  44. // 幂等验证:对已归一值 "13800000000" 再调用 → ("13800000000", true),不变。
  45. func Phone(raw string) (canonical string, ok bool) {
  46. s := strings.TrimSpace(raw)
  47. s = sepRe.ReplaceAllString(s, "")
  48. s = strings.TrimPrefix(s, "+")
  49. // 剥 0086 前缀(须后接 11 位大陆号,避免误剥正常号段)
  50. if strings.HasPrefix(s, "0086") {
  51. rest := s[4:]
  52. if cnPhoneRe.MatchString(rest) {
  53. return rest, true
  54. }
  55. }
  56. // 剥 86 前缀(+86 已去 +,此处剩 "86"+11 位)
  57. if strings.HasPrefix(s, "86") && len(s) == 13 {
  58. rest := s[2:]
  59. if cnPhoneRe.MatchString(rest) {
  60. return rest, true
  61. }
  62. }
  63. if cnPhoneRe.MatchString(s) {
  64. return s, true
  65. }
  66. return "", false
  67. }
  68. // WechatOpenID 归一:仅 trim(微信侧已清洗,不做字符串变换)。
  69. //
  70. // 入参:
  71. // - raw string:legacy t_weixin_user.openid 原始值。
  72. //
  73. // 出参:
  74. // - canonical string:trim 后的 openid;空串返回 ""。
  75. // - ok bool:true 表示非空可落 t_identity.wechat。
  76. func WechatOpenID(raw string) (canonical string, ok bool) {
  77. s := strings.TrimSpace(raw)
  78. if s == "" {
  79. return "", false
  80. }
  81. return s, true
  82. }
  83. // Email 归一:trim + 小写 + 正则校验(P0-06 §2.3)。
  84. //
  85. // 保留 +tag(P0-06 §5 拍板:量仅 167 条,保守不破坏既有邮箱)。
  86. //
  87. // 入参:
  88. // - raw string:legacy t_user.email 原始值。
  89. //
  90. // 出参:
  91. // - canonical string:小写 email;ok=false 时为空串。
  92. // - ok bool:true 表示通过最小正则校验(可写 t_identity.email)。
  93. func Email(raw string) (canonical string, ok bool) {
  94. s := strings.ToLower(strings.TrimSpace(raw))
  95. if !emailRe.MatchString(s) {
  96. return "", false
  97. }
  98. return s, true
  99. }
  100. // Username 归一:trim + 小写(P0-06 §2.4)。
  101. //
  102. // 红线:username **绝不作自然人归一 key**——仅作登录账号落 t_identity.username。
  103. // 本函数无 ok 返回值(username 始终可归一,空串也合法——legacy 100% 有值)。
  104. //
  105. // 入参:
  106. // - raw string:legacy t_user.username 原始值。
  107. //
  108. // 出参:
  109. // - canonical string:小写 username;空串输入返回 ""。
  110. func Username(raw string) string {
  111. return strings.ToLower(strings.TrimSpace(raw))
  112. }