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() ident := idVerifyIdentity{ realname: realname, idcardmask: idverify.MaskIdCard(idcardno), idcardhash: idverify.HashIdCard(idcardno, salt), } if record != nil && record.Verifytime > 0 && sameIdentity(record, ident) { 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} } // 调用失败 ≠ 不一致:结论未知,不写 bizcode、不算作用户填错。 // 已实名记录上的身份字段在失败时一律不动(见 applyIdVerifyAttempt)。 if err != nil { applyIdVerifyAttempt(record, false, ident, verifier.Provider(), svcId, "", 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 } if !result.Matched { applyIdVerifyAttempt(record, false, ident, verifier.Provider(), svcId, result.BizCode, now) _ = this.module.idverifymodel.save(record) errdata = &pb.ErrorData{Code: pb.ErrorCode_IdVerifyMismatch, Message: "姓名与身份证号不一致"} return } // 6) 通过:写明细 + 打 user 表标识 + 回写真实性别。 // 性别取身份证第 17 位(奇男偶女),比用户自己选的更可信,核验通过后以它为准。 // 换人(已实名账号改绑成另一张证)时留一条 rebind 痕迹,主表不保留历史。 replaced := applyIdVerifyAttempt(record, true, ident, verifier.Provider(), svcId, result.BizCode, now) if err = this.module.idverifymodel.save(record); err != nil { errdata = &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()} return } if replaced != nil { if lerr := this.module.idverifymodel.logRebind(uid, replaced, record, now); lerr != nil { this.module.Error("IdVerify: 换人留痕失败", log.Field{Key: "uid", Value: uid}, log.Field{Key: "err", Value: lerr.Error()}) } } 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 } // @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 }