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.
177 lines
6.9 KiB
177 lines
6.9 KiB
package user
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"yunyan/comm"
|
|
"yunyan/lego/sys/log"
|
|
"yunyan/pb"
|
|
"yunyan/sys/idverify"
|
|
)
|
|
|
|
// @Summary 实名认证(身份证二要素核验)
|
|
// @Description 提交姓名+身份证号,由服务端调用后台配置的核验服务商(阿里云/腾讯云/创蓝)核验
|
|
// @Tags User
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param user body pb.UserIdVerifyReq true "实名认证"
|
|
// @Success 200 {object} comm.HttpResult{data=pb.UserIdVerifyResp} "成功返回"
|
|
// @Router /api/home/user_idverify [post]
|
|
//
|
|
// 落库口径(合规):身份证号**全文不落库**,只存掩码与加盐 SHA-256 指纹。
|
|
// 一证可绑多号(不拦),指纹用于后台排查同一证件下的账号。
|
|
func (this *apiComp) IdVerify(session comm.IUserSession, req *pb.UserIdVerifyReq) (resp *pb.UserIdVerifyResp, errdata *pb.ErrorData) {
|
|
uid := session.GetUserId()
|
|
realname := strings.TrimSpace(req.Realname)
|
|
idcardno := strings.ToUpper(strings.TrimSpace(req.Idcardno))
|
|
|
|
// 1) 本地校验:挡在计费调用之前。
|
|
if realname == "" || len([]rune(realname)) > 32 || !idverify.ValidIdCard(idcardno) {
|
|
errdata = &pb.ErrorData{Code: pb.ErrorCode_IdVerifyParamInvalid, Message: "姓名或身份证号格式不合法"}
|
|
return
|
|
}
|
|
|
|
record, err := this.module.idverifymodel.find(uid)
|
|
if err != nil {
|
|
errdata = &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()}
|
|
return
|
|
}
|
|
|
|
// 2) 已实名 + 提交的还是同一个人:直接返回,不再走一次计费核验。
|
|
// 注意不能对「已实名」一律早返回 —— 客户端的「修改认证」就是靠再次提交换人,
|
|
// 早返回会让改绑永远改不动(表现为点了确定但姓名没变)。
|
|
salt := this.module.idverifymodel.salt()
|
|
if record != nil && record.Verifytime > 0 &&
|
|
record.Realname == realname && sameIdCard(record, idcardno, salt) {
|
|
resp = &pb.UserIdVerifyResp{
|
|
Verified: true, Realname: record.Realname,
|
|
Idcardmask: record.Idcardmask, Verifytime: record.Verifytime,
|
|
}
|
|
return
|
|
}
|
|
|
|
// 3) 限流:窗口内失败次数达上限即拒。
|
|
now := time.Now().Unix()
|
|
if record != nil && record.Failcount >= idVerifyFailLimit &&
|
|
now-record.Lastfailtime < int64(idVerifyFailWindow.Seconds()) {
|
|
errdata = &pb.ErrorData{Code: pb.ErrorCode_IdVerifyTooFrequent, Message: "校验过于频繁,请稍后再试"}
|
|
return
|
|
}
|
|
// 窗口已过则清零,重新计数。
|
|
if record != nil && now-record.Lastfailtime >= int64(idVerifyFailWindow.Seconds()) {
|
|
record.Failcount = 0
|
|
}
|
|
|
|
// 4) 解析后台配置的核验服务。
|
|
svcId, verifier, err := this.module.idverifymodel.resolveVerifier()
|
|
if err != nil {
|
|
this.module.Error("IdVerify: 核验服务解析失败",
|
|
log.Field{Key: "uid", Value: uid}, log.Field{Key: "svc", Value: svcId}, log.Field{Key: "err", Value: err.Error()})
|
|
errdata = &pb.ErrorData{Code: pb.ErrorCode_IdVerifyNotConfigured, Message: "实名认证服务不可用"}
|
|
return
|
|
}
|
|
if verifier == nil {
|
|
errdata = &pb.ErrorData{Code: pb.ErrorCode_IdVerifyNotConfigured, Message: "未配置实名认证服务"}
|
|
return
|
|
}
|
|
|
|
// 5) 调服务商。
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
result, err := verifier.Verify(ctx, realname, idcardno)
|
|
|
|
if record == nil {
|
|
record = &pb.DBUserIdVerify{Uid: uid}
|
|
}
|
|
record.Realname = realname
|
|
record.Idcardmask = idverify.MaskIdCard(idcardno)
|
|
record.Idcardhash = idverify.HashIdCard(idcardno, this.module.idverifymodel.salt())
|
|
record.Provider = verifier.Provider()
|
|
record.Svcid = svcId
|
|
|
|
// 调用失败 ≠ 不一致:结论未知,不写 bizcode、不算作用户填错。
|
|
if err != nil {
|
|
record.Failcount++
|
|
record.Lastfailtime = now
|
|
_ = this.module.idverifymodel.save(record)
|
|
this.module.Error("IdVerify: 服务商调用失败",
|
|
log.Field{Key: "uid", Value: uid}, log.Field{Key: "provider", Value: verifier.Provider()},
|
|
log.Field{Key: "svc", Value: svcId}, log.Field{Key: "err", Value: err.Error()})
|
|
code := pb.ErrorCode_IdVerifyProviderError
|
|
if errors.Is(err, idverify.ErrMissingCredential) || errors.Is(err, idverify.ErrUnsupportedProvider) {
|
|
code = pb.ErrorCode_IdVerifyNotConfigured
|
|
}
|
|
errdata = &pb.ErrorData{Code: code, Message: "实名认证服务暂时不可用,请稍后再试"}
|
|
return
|
|
}
|
|
|
|
record.Bizcode = result.BizCode
|
|
if !result.Matched {
|
|
record.Failcount++
|
|
record.Lastfailtime = now
|
|
_ = this.module.idverifymodel.save(record)
|
|
errdata = &pb.ErrorData{Code: pb.ErrorCode_IdVerifyMismatch, Message: "姓名与身份证号不一致"}
|
|
return
|
|
}
|
|
|
|
// 6) 通过:写明细 + 打 user 表标识 + 回写真实性别。
|
|
// 性别取身份证第 17 位(奇男偶女),比用户自己选的更可信,核验通过后以它为准。
|
|
record.Verifytime = now
|
|
record.Failcount = 0
|
|
if err = this.module.idverifymodel.save(record); err != nil {
|
|
errdata = &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()}
|
|
return
|
|
}
|
|
gender := idverify.GenderFromIdCard(idcardno)
|
|
if err = this.module.idverifymodel.markUserVerified(uid, now, gender); err != nil {
|
|
// 明细已落库,标识没打上:不让用户重复走一次计费核验,只记日志由运维补。
|
|
this.module.Error("IdVerify: user 表实名标识回写失败",
|
|
log.Field{Key: "uid", Value: uid}, log.Field{Key: "err", Value: err.Error()})
|
|
}
|
|
|
|
resp = &pb.UserIdVerifyResp{
|
|
Verified: true, Realname: record.Realname,
|
|
Idcardmask: record.Idcardmask, Verifytime: record.Verifytime,
|
|
Gender: gender,
|
|
}
|
|
return
|
|
}
|
|
|
|
// sameIdCard 判断提交的号码是否就是记录里那张证件。
|
|
// 有盐时比指纹(最准);没配盐时指纹为空,退而比掩码——掩码只保留首尾各 4 位,
|
|
// 中间 10 位不同的两张证件会被误判成同一张,所以这只是降级兜底,生产务必配 ID_HASH_SALT。
|
|
func sameIdCard(record *pb.DBUserIdVerify, idcardno, salt string) bool {
|
|
if h := idverify.HashIdCard(idcardno, salt); h != "" && record.Idcardhash != "" {
|
|
return h == record.Idcardhash
|
|
}
|
|
return record.Idcardmask == idverify.MaskIdCard(idcardno)
|
|
}
|
|
|
|
// @Summary 查询实名状态
|
|
// @Description 查询本账号是否已通过身份证实名校验
|
|
// @Tags User
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param user body pb.UserGetIdVerifyReq true "查询实名状态"
|
|
// @Success 200 {object} comm.HttpResult{data=pb.UserGetIdVerifyResp} "成功返回"
|
|
// @Router /api/home/user_getidverify [post]
|
|
func (this *apiComp) GetIdVerify(session comm.IUserSession, req *pb.UserGetIdVerifyReq) (resp *pb.UserGetIdVerifyResp, errdata *pb.ErrorData) {
|
|
record, err := this.module.idverifymodel.find(session.GetUserId())
|
|
if err != nil {
|
|
errdata = &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()}
|
|
return
|
|
}
|
|
resp = &pb.UserGetIdVerifyResp{}
|
|
if record != nil && record.Verifytime > 0 {
|
|
resp.Verified = true
|
|
resp.Realname = record.Realname
|
|
resp.Idcardmask = record.Idcardmask
|
|
resp.Verifytime = record.Verifytime
|
|
}
|
|
return
|
|
}
|
|
|