package user import ( "os" "sort" "strings" "time" "yunyan/comm" "yunyan/lego/core" "yunyan/lego/core/cbase" "yunyan/lego/sys/log" "yunyan/lego/sys/mysql" "yunyan/lego/sys/postgres" "yunyan/pb" "yunyan/sys/idverify" ) // 实名认证的数据访问 + 服务解析。 // // 两个库都要碰: // - useridverify / user 在**业务库 mysql**(随部署,按应用分离); // - svc_config 在 **console 共享库 postgres**(后台「第三方服务配置」维护)。 // idHashSaltEnv 身份证号指纹的盐。见 idverify.HashIdCard 的说明:不加盐等于明文。 const idHashSaltEnv = "ID_HASH_SALT" // idVerifyFailWindow / idVerifyFailLimit 限流:同一账号在窗口内失败达上限即拒绝。 // 服务商按次计费,不限流的话一个脚本就能把额度刷干净。 const ( idVerifyFailWindow = 10 * time.Minute idVerifyFailLimit = 5 ) type modelIdVerifyComp struct { cbase.ModuleCompBase module *User } func (this *modelIdVerifyComp) Init(service core.IService, module core.IModule, comp core.IModuleComp, opt core.IModuleOptions) (err error) { this.ModuleCompBase.Init(service, module, comp, opt) this.module = module.(*User) if err = mysql.CreateTable(comm.TableUserIdVerify, &pb.DBUserIdVerify{}); err != nil { this.module.Errorln(err) } return } // find 取某账号的实名记录;无记录返回 nil,nil。 func (this *modelIdVerifyComp) find(uid string) (*pb.DBUserIdVerify, error) { model := &pb.DBUserIdVerify{} err := mysql.FindOne(comm.TableUserIdVerify, model, "uid=?", uid) if err == mysql.ErrNoDocuments { return nil, nil } if err != nil { return nil, err } return model, nil } // save 落库实名记录(有则更新、无则插入)。 func (this *modelIdVerifyComp) save(model *pb.DBUserIdVerify) error { now := time.Now().Unix() model.Updatetime = now if model.Createtime == 0 { model.Createtime = now return mysql.Insert(comm.TableUserIdVerify, model) } return mysql.Save(comm.TableUserIdVerify, model) } // markUserVerified 把 user 表的实名标识打上(与 useridverify 明细表配套, // 便于列表/后台按 idverified 直接过滤,不用每次 join 明细表)。 // gender>0 时一并回写真实性别(身份证第 17 位奇男偶女),0 表示解析不出、保留用户原选择。 func (this *modelIdVerifyComp) markUserVerified(uid string, at int64, gender int32) error { cols := map[string]interface{}{ "idverified": true, "idverifiedtime": at, } if gender > 0 { cols["gender"] = gender } return mysql.Table(comm.TableUser).Where("uid=?", uid).UpdateColumns(cols).Error } // salt 读取指纹盐。 func (this *modelIdVerifyComp) salt() string { return os.Getenv(idHashSaltEnv) } // resolveVerifier 从「第三方服务配置」里解析出本应用可用的实名核验服务。 // // 口径与 resolveThirdSvcs 的作用域一致:应用行(app_name=)优先于全局行(app_name=''), // 只取启用的、类别含 comm.SvcCatIdVerify 的服务。返回 svcId 供落库记录用哪家核验的。 // // ⚠️ 这类服务不走 resolveThirdSvcs(那是客户端下发口,已按 IsServerOnlySvc 把它们拦掉了), // 凭据只在这里、服务端进程内解密使用。 func (this *modelIdVerifyComp) resolveVerifier() (svcId string, v idverify.Verifier, err error) { app := comm.AppName() svcs := make([]*comm.ThirdSvcConfig, 0) if err = postgres.Find(comm.TableSvcConfig, &svcs, "(app_name=? OR app_name='')", app); err != nil { if err == postgres.ErrNoDocuments { err = nil } else { return } } // 候选 = 启用的、类别含实名核验的服务。 candidates := make([]*comm.ThirdSvcConfig, 0, len(svcs)) for _, s := range svcs { if !s.Enable || !comm.CategoriesHas(s.Categories, comm.SvcCatIdVerify) { continue } candidates = append(candidates, s) } if len(candidates) == 0 { return "", nil, nil // 未配置:由调用方回 IdVerifyNotConfigured } // 应用行覆盖全局行;同作用域内多条同时启用属于配置错误—— // 这里按 Id 字典序取第一条,保证**每次重启选的是同一家**。 // 原先是「取遍历到的第一条」,而 Find 没有 ORDER BY,同作用域两条都启用时 // 选谁取决于 Postgres 的返回顺序,可能在重启后悄悄换一家服务商(还各自计费)。 sort.SliceStable(candidates, func(i, j int) bool { ai := candidates[i].AppName == app && app != "" aj := candidates[j].AppName == app && app != "" if ai != aj { return ai // 应用行排前面 } return candidates[i].Id < candidates[j].Id }) picked := candidates[0] // 同作用域内还有别的启用项时必须喊出来:运营多半是想换一家却忘了把旧的停用, // 静默择一会让「已启用的新服务商」看起来完全没生效。 if len(candidates) > 1 { others := make([]string, 0, len(candidates)-1) for _, c := range candidates[1:] { if (c.AppName == app && app != "") == (picked.AppName == app && app != "") { others = append(others, c.Id) } } if len(others) > 0 { this.module.Warn("实名核验服务有多条同时启用,已按 Id 取第一条;请在后台只保留一条启用", log.Field{Key: "picked", Value: picked.Id}, log.Field{Key: "provider", Value: picked.Provider}, log.Field{Key: "ignored", Value: strings.Join(others, ",")}) } } fields, ferr := comm.ResolveSvcPlainFields(picked, nil, os.Getenv("FIELD_ENCRYPT_KEY")) if ferr != nil { return picked.Id, nil, ferr } v, err = idverify.New(picked.Provider, fields) return picked.Id, v, err }