package idverify import ( "crypto/sha256" "encoding/hex" "strings" ) // 身份证号的本地处理:格式校验、掩码、加盐指纹。 // // 本地校验的意义不只是"友好提示"——服务商核验是**按次计费**的,把明显不合法的号码 // (位数不对、校验位算不上)挡在调用之前,既省额度也避免把垃圾请求算进失败次数。 // 加权因子与校验码表(GB 11643-1999 附录A,ISO 7064:1983 MOD 11-2)。 var ( idWeights = [17]int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2} idCheckCode = [11]byte{'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'} ) // ValidIdCard 校验 18 位二代身份证号:前 17 位为数字,末位为数字或 X/x,且校验位正确。 // 只支持二代证——阿里云 Id2MetaVerify 与腾讯云 IdCardVerification 都只认二代证。 func ValidIdCard(no string) bool { no = strings.TrimSpace(no) if len(no) != 18 { return false } sum := 0 for i := 0; i < 17; i++ { c := no[i] if c < '0' || c > '9' { return false } sum += int(c-'0') * idWeights[i] } last := no[17] if last == 'x' { last = 'X' } return last == idCheckCode[sum%11] } // MaskIdCard 生成用于展示与落库的掩码:保留前 4 位与后 4 位,中间一律 *。 // 例:440301199001011234 -> 4403**********1234 // 非 18 位的输入原样返回掩码化的兜底(全 *),避免意外把原文写进库。 func MaskIdCard(no string) string { no = strings.TrimSpace(no) if len(no) != 18 { return strings.Repeat("*", len(no)) } return no[:4] + strings.Repeat("*", 10) + no[14:] } // HashIdCard 生成加盐 SHA-256 指纹(小写十六进制),用于后台排查"同一证件绑了多个账号"。 // // 必须加盐:身份证号空间有限(约 10^17,且前 6 位地区码、中间 8 位生日高度可枚举), // 裸 SHA-256 可以被彻底反查,等同于明文存储。salt 由 ID_HASH_SALT 环境变量提供。 // salt 为空时返回空串——调用方据此跳过写指纹,绝不退化成裸哈希。 func HashIdCard(no, salt string) string { no = strings.TrimSpace(no) if no == "" || salt == "" { return "" } sum := sha256.Sum256([]byte(salt + "|" + strings.ToUpper(no))) return hex.EncodeToString(sum[:]) } // GenderFromIdCard 从身份证号解析性别:第 17 位(顺序码末位)奇数为男、偶数为女。 // 返回值对齐 pb.DBUser.Gender 的口径:1=男,2=女,0=解析不出。 func GenderFromIdCard(no string) int32 { no = strings.TrimSpace(no) if len(no) != 18 { return 0 } c := no[16] if c < '0' || c > '9' { return 0 } if (c-'0')%2 == 1 { return 1 } return 2 }