| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- // Package norm 标识归一化纯函数(P0-06 §2/§3.3 设计稿落地)。
- //
- // 职责:把 legacy 原始标识收敛为目标态 canonical 形态。
- // 设计铁律:
- // - **纯函数**:无副作用、确定(同一输入恒同输出),可独立单测;
- // - **幂等**:归一(归一(x)) == 归一(x)(对已归一值再归一不变);
- // - **先归一后匹配**:归一失败绝不写脏 canonical(调用方据此跳过 t_identity 写入)。
- //
- // 四类标识归一力度(P0-06 §1):
- // - phone:强归一(trim → 去分隔符 → 剥国际前缀 → 11 位校验)——主力 key
- // - wechat:不做字符串变换(openid 已唯一),仅 trim;必须带 appid(见调用方)
- // - email:轻归一(trim + 小写 + 正则校验)——不作归一 key
- // - username:轻归一(trim + 小写)——绝不作自然人归一 key
- package norm
- import (
- "regexp"
- "strings"
- )
- // 大陆手机号正则(11 位:1 + 3-9 起始号段 + 9 位)
- var cnPhoneRe = regexp.MustCompile(`^1[3-9]\d{9}$`)
- // 分隔符:空白、横线、点(P0-06 §2.1 步 2)
- var sepRe = regexp.MustCompile(`[\s\-\.]`)
- // 邮箱最小可判定正则(P0-06 §2.3 步 4,不强求 RFC 5322 全量)
- var emailRe = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`)
- // Phone 归一:trim → 去分隔符 → 剥国际前缀 → 11 位大陆号校验。
- //
- // 规则(P0-06 §2.1 顺序执行):
- // 1. trim 去首尾空白
- // 2. 去掉所有空格、横线、点(sepRe)
- // 3. 去 + 号前缀
- // 4. 剥国际前缀(仅当剥后 11 位匹配大陆号段才剥,P0-06 §2.1 步 3):
- // - 前导 "0086"(4 位)
- // - 前导 "86"(2 位,须后接 11 位大陆号)
- // 5. 11 位大陆号校验:正则 ^1[3-9]\d{9}$
- // 6. 否则 → ok=false(phone_invalid,转 P0-08 人工)
- //
- // 入参:
- // - raw string:legacy t_user.phone_number 原始值(可能含分隔符/国际前缀/空白)。
- //
- // 出参:
- // - canonical string:归一后的 11 位大陆号(如 "13800000000");ok=false 时为空串。
- // - ok bool:true 表示归一成功(可写 t_identity.phone);false 表示 phone_invalid。
- //
- // 幂等验证:对已归一值 "13800000000" 再调用 → ("13800000000", true),不变。
- func Phone(raw string) (canonical string, ok bool) {
- s := strings.TrimSpace(raw)
- s = sepRe.ReplaceAllString(s, "")
- s = strings.TrimPrefix(s, "+")
- // 剥 0086 前缀(须后接 11 位大陆号,避免误剥正常号段)
- if strings.HasPrefix(s, "0086") {
- rest := s[4:]
- if cnPhoneRe.MatchString(rest) {
- return rest, true
- }
- }
- // 剥 86 前缀(+86 已去 +,此处剩 "86"+11 位)
- if strings.HasPrefix(s, "86") && len(s) == 13 {
- rest := s[2:]
- if cnPhoneRe.MatchString(rest) {
- return rest, true
- }
- }
- if cnPhoneRe.MatchString(s) {
- return s, true
- }
- return "", false
- }
- // WechatOpenID 归一:仅 trim(微信侧已清洗,不做字符串变换)。
- //
- // 入参:
- // - raw string:legacy t_weixin_user.openid 原始值。
- //
- // 出参:
- // - canonical string:trim 后的 openid;空串返回 ""。
- // - ok bool:true 表示非空可落 t_identity.wechat。
- func WechatOpenID(raw string) (canonical string, ok bool) {
- s := strings.TrimSpace(raw)
- if s == "" {
- return "", false
- }
- return s, true
- }
- // Email 归一:trim + 小写 + 正则校验(P0-06 §2.3)。
- //
- // 保留 +tag(P0-06 §5 拍板:量仅 167 条,保守不破坏既有邮箱)。
- //
- // 入参:
- // - raw string:legacy t_user.email 原始值。
- //
- // 出参:
- // - canonical string:小写 email;ok=false 时为空串。
- // - ok bool:true 表示通过最小正则校验(可写 t_identity.email)。
- func Email(raw string) (canonical string, ok bool) {
- s := strings.ToLower(strings.TrimSpace(raw))
- if !emailRe.MatchString(s) {
- return "", false
- }
- return s, true
- }
- // Username 归一:trim + 小写(P0-06 §2.4)。
- //
- // 红线:username **绝不作自然人归一 key**——仅作登录账号落 t_identity.username。
- // 本函数无 ok 返回值(username 始终可归一,空串也合法——legacy 100% 有值)。
- //
- // 入参:
- // - raw string:legacy t_user.username 原始值。
- //
- // 出参:
- // - canonical string:小写 username;空串输入返回 ""。
- func Username(raw string) string {
- return strings.ToLower(strings.TrimSpace(raw))
- }
|