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 }