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