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