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.
 
 
 
 
 
 

157 lines
5.5 KiB

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>)优先于全局行(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
}