// 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)) }