You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

102 lines
4.1 KiB

package user
import (
"regexp"
"strings"
"yunyan/pb"
)
// 用户账号 ↔ 实名记录 的绑定口径,集中在这一个文件里(2026-09-19 梳理)。
//
// 两张表:
// - useridverify(明细,一 uid 一行)是**权威**:客户端闸门 user_getidverify 只读它,
// 计费核验的结论也只落在它上面;
// - user.idverified / idverifiedtime 是明细的**派生缓存**,只为后台列表按实名筛选,
// 方向永远是「明细 → 标识」,标识不能反过来推出明细。
//
// 这里放的都是不碰数据库的纯逻辑,好写测试;落库的在 model_idverify.go。
// idVerifyIdentity 一次提交的身份三元组(姓名 + 掩码 + 指纹)。
type idVerifyIdentity struct {
realname string
idcardmask string
idcardhash string
}
// sameIdentity 提交的是不是记录上那个人。指纹优先(最准),没配盐时指纹为空、退回比掩码
// (掩码只留首尾各 4 位,会碰撞,所以生产务必配 ID_HASH_SALT)。
func sameIdentity(record *pb.DBUserIdVerify, id idVerifyIdentity) bool {
if record.Realname != id.realname {
return false
}
if id.idcardhash != "" && record.Idcardhash != "" {
return id.idcardhash == record.Idcardhash
}
return record.Idcardmask == id.idcardmask
}
// applyIdVerifyAttempt 把一次核验的结果合并进记录,返回被顶掉的旧身份(换人时非 nil,供留痕)。
//
// ⚠️ 失败时**不能改写已实名记录上的身份字段**。原实现是先无条件把姓名/掩码/指纹
// 换成本次提交的,再看结果——于是已实名的人「修改认证」填错一次,明细行就变成
// 「verifytime 还是通过、身份却是那个没通过的人」:user_getidverify 回的是未核验的姓名,
// 后台看到的也是;再提交原来那个人又被当成换人、再计费一次。
// 未实名的记录(verifytime==0)记下最后一次尝试的身份是可以的,排查刷接口时有用。
//
// bizcode 为空表示服务商没给结论(调用失败),不覆盖已有的结果码。
func applyIdVerifyAttempt(record *pb.DBUserIdVerify, matched bool, id idVerifyIdentity,
provider, svcId, bizcode string, now int64) (replaced *pb.DBUserIdVerify) {
if !matched {
record.Failcount++
record.Lastfailtime = now
if record.Verifytime > 0 {
return nil
}
record.Realname, record.Idcardmask, record.Idcardhash = id.realname, id.idcardmask, id.idcardhash
record.Provider, record.Svcid = provider, svcId
if bizcode != "" {
record.Bizcode = bizcode
}
return nil
}
if record.Verifytime > 0 && !sameIdentity(record, id) {
// 只复制留痕要用的身份字段,别整体拷 pb 结构(内含 mutex,vet 会报 copylocks)。
replaced = &pb.DBUserIdVerify{
Uid: record.Uid, Realname: record.Realname,
Idcardmask: record.Idcardmask, Idcardhash: record.Idcardhash,
Verifytime: record.Verifytime,
}
}
record.Realname, record.Idcardmask, record.Idcardhash = id.realname, id.idcardmask, id.idcardhash
record.Provider, record.Svcid, record.Bizcode = provider, svcId, bizcode
record.Verifytime = now
record.Failcount = 0
return replaced
}
// scrubCancelledUserIdVerify 注销时把 user 行上由实名派生出来的三个字段一并清掉。
//
// 注销删的是明细行(见 modelUserComp.cancel),标识若不同步清,这个账号在后台列表里
// 仍是「已实名」、详情却没有姓名证件;性别也还是上一张身份证回写的值。
// 复活(api_sgin 的 Cancelled 分支)不会再碰这三个字段,所以只能在这里清。
func scrubCancelledUserIdVerify(user *pb.DBUser) {
user.Idverified = false
user.Idverifiedtime = 0
user.Gender = 0
}
// collationCharset 由排序规则名推字符集名(utf8mb4_0900_ai_ci → utf8mb4)。
// 名字来自 information_schema,仍按白名单字符校验一次再拼进 DDL。
var collationNameRe = regexp.MustCompile(`^[a-z0-9_]+$`)
func collationCharset(collation string) (charset string, ok bool) {
if !collationNameRe.MatchString(collation) {
return "", false
}
i := strings.IndexByte(collation, '_')
if i <= 0 {
return "", false
}
return collation[:i], true
}