add sms authorization

This commit is contained in:
liangzai 2025-06-03 12:24:15 +08:00
parent bebabce346
commit acd0dd5b9c
13 changed files with 252 additions and 90 deletions

View File

@ -165,12 +165,16 @@ service main {
@doc "第三方拒绝授权" @doc "第三方拒绝授权"
@handler rejectAuthorization @handler rejectAuthorization
post /rejectAuthorization (RejectAuthorizationReq) returns (RejectAuthorizationResp) post /rejectAuthorization (RejectAuthorizationReq) returns (RejectAuthorizationResp)
@doc "短信授权"
@handler smsAuthorization
post /smsAuthorization (SmsAuthorizationReq) returns (SmsAuthorizationResp)
} }
type ( type (
sendSmsReq { sendSmsReq {
Mobile string `json:"mobile" validate:"required,mobile"` Mobile string `json:"mobile" validate:"required,mobile"`
ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply bindMobile realName"` ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply bindMobile realName authorization"`
} }
// 发起人脸认证请求 // 发起人脸认证请求
@ -202,6 +206,16 @@ type (
} }
RejectAuthorizationResp { RejectAuthorizationResp {
} }
SmsAuthorizationReq {
OrderNo string `json:"order_no" validate:"required"` // 订单号
Mobile string `json:"mobile" validate:"required,mobile"`
Code string `json:"code" validate:"required"`
}
SmsAuthorizationResp {
Passed bool `json:"passed"` // 是否通过
OrderID int64 `json:"order_id"`
}
) )
//============================> notification v1 <============================ //============================> notification v1 <============================
@server ( @server (

View File

@ -0,0 +1,29 @@
package auth
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"qnc-server/app/user/cmd/api/internal/logic/auth"
"qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types"
"qnc-server/common/result"
"qnc-server/pkg/lzkit/validator"
)
func SmsAuthorizationHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SmsAuthorizationReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := auth.NewSmsAuthorizationLogic(r.Context(), svcCtx)
resp, err := l.SmsAuthorization(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@ -173,6 +173,12 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/sendSms", Path: "/sendSms",
Handler: auth.SendSmsHandler(serverCtx), Handler: auth.SendSmsHandler(serverCtx),
}, },
{
// 短信授权
Method: http.MethodPost,
Path: "/smsAuthorization",
Handler: auth.SmsAuthorizationHandler(serverCtx),
},
}, },
rest.WithPrefix("/api/v1/auth"), rest.WithPrefix("/api/v1/auth"),
) )

View File

@ -64,25 +64,23 @@ func (l *InitFaceVerifyLogic) InitFaceVerify(req *types.InitFaceVerifyReq) (resp
if decodeErr != nil { if decodeErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询授权信息失败: %v", decodeErr) return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询授权信息失败: %v", decodeErr)
} }
if authorization.GrantType == "face" { authorizationFaceBuilder := l.svcCtx.AuthorizationFaceModel.SelectBuilder().
authorizationFaceBuilder := l.svcCtx.AuthorizationFaceModel.SelectBuilder(). Where("authorization_id = ? AND status = ? AND certify_id != '' AND certify_url != ''",
Where("authorization_id = ? AND status = ? AND certify_id != '' AND certify_url != ''", authorization.Id, model.AuthorizationStatusPending).
authorization.Id, model.AuthorizationStatusPending). OrderBy("create_time DESC").
OrderBy("create_time DESC"). Limit(1)
Limit(1) existingFaces, err := l.svcCtx.AuthorizationFaceModel.FindAll(l.ctx, authorizationFaceBuilder, "")
existingFaces, err := l.svcCtx.AuthorizationFaceModel.FindAll(l.ctx, authorizationFaceBuilder, "") if err != nil {
if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询人脸认证记录失败: %v", err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询人脸认证记录失败: %v", err)
}
if len(existingFaces) > 0 {
// 如果存在记录,直接返回最新的认证信息
return &types.InitFaceVerifyResp{
CertifyId: existingFaces[0].CertifyId,
CertifyUrl: existingFaces[0].CertifyUrl,
}, nil
}
} }
if len(existingFaces) > 0 {
// 如果存在记录,直接返回最新的认证信息
return &types.InitFaceVerifyResp{
CertifyId: existingFaces[0].CertifyId,
CertifyUrl: existingFaces[0].CertifyUrl,
}, nil
}
outerOrderNo := genOuterOrderNo() outerOrderNo := genOuterOrderNo()
name, err := crypto.AesDecrypt(authorization.TargetName, key) name, err := crypto.AesDecrypt(authorization.TargetName, key)
if err != nil { if err != nil {
@ -106,6 +104,10 @@ func (l *InitFaceVerifyLogic) InitFaceVerify(req *types.InitFaceVerifyReq) (resp
Int64: req.AuthType, Int64: req.AuthType,
Valid: true, Valid: true,
} }
authorization.GrantType = sql.NullString{
String: model.AuthorizationGrantTypeFace,
Valid: true,
}
_, err = l.svcCtx.AuthorizationModel.Update(l.ctx, session, authorization) _, err = l.svcCtx.AuthorizationModel.Update(l.ctx, session, authorization)
if err != nil { if err != nil {
return err return err

View File

@ -0,0 +1,147 @@
package auth
import (
"context"
"database/sql"
"encoding/hex"
"fmt"
"qnc-server/app/user/cmd/api/internal/service"
"qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types"
"qnc-server/app/user/model"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/bytedance/sonic"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type SmsAuthorizationLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewSmsAuthorizationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SmsAuthorizationLogic {
return &SmsAuthorizationLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *SmsAuthorizationLogic) SmsAuthorization(req *types.SmsAuthorizationReq) (resp *types.SmsAuthorizationResp, err error) {
order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", err)
}
if order.Status != "paid" {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "订单未支付")
}
authorization, err := l.svcCtx.AuthorizationModel.FindOneByOrderId(l.ctx, order.Id)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询授权信息失败: %v", err)
}
if authorization.Status != "pending" {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "授权信息状态不正确")
}
key, decodeErr := hex.DecodeString(l.svcCtx.Config.Encrypt.SecretKey)
if decodeErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询授权信息失败: %v", decodeErr)
}
name, err := crypto.AesDecrypt(authorization.TargetName, key)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解密姓名失败: %v", err)
}
idCard, err := crypto.AesDecrypt(authorization.TargetIdcard, key)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解密身份证号失败: %v", err)
}
threeFactorVerificationResp, err := l.svcCtx.VerificationService.ThreeFactorVerification(service.ThreeFactorVerificationRequest{
Mobile: req.Mobile,
Name: string(name),
IDCard: string(idCard),
})
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "三要素验证失败: %v", err)
}
if !threeFactorVerificationResp.Passed {
return nil, errors.Wrapf(xerr.NewErrMsg("该手机号码不是被查询人的手机号"), "三要素验证失败")
}
secretKey := l.svcCtx.Config.Encrypt.SecretKey
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err)
}
// 检查手机号是否在一分钟内已发送过验证码
redisKey := fmt.Sprintf("%s:%s", "authorization", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "验证码过期: %s", encryptedMobile)
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "读取验证码redis缓存失败, mobile: %s, err: %+v", encryptedMobile, err)
}
if cacheCode != req.Code {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "验证码不正确: %s", encryptedMobile)
}
err = l.svcCtx.AuthorizationModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
order, err := l.svcCtx.OrderModel.FindOne(ctx, authorization.OrderId)
if err != nil {
return errors.Wrapf(err, "查询订单失败")
}
redisKey := fmt.Sprintf(types.QueryCacheKey, order.UserId, order.OrderNo)
cache, cacheErr := l.svcCtx.Redis.GetCtx(ctx, redisKey)
if cacheErr != nil {
return fmt.Errorf("获取缓存内容失败: %+v", cacheErr)
}
var data types.QueryCacheLoad
err = sonic.Unmarshal([]byte(cache), &data)
if err != nil {
return fmt.Errorf("解析缓存内容失败: %+v", err)
}
// 插入新queryModel
query := &model.Query{
OrderId: authorization.OrderId,
UserId: authorization.UserId,
ProductId: order.ProductId,
QueryParams: data.Params,
QueryState: "pending",
}
_, insertQueryErr := l.svcCtx.QueryModel.Insert(ctx, session, query)
if insertQueryErr != nil {
return errors.Wrapf(insertQueryErr, "保存查询失败")
}
authorization.GrantType = sql.NullString{
String: model.AuthorizationGrantTypeSms,
Valid: true,
}
// 更新主授权状态
authorization.Status = model.AuthorizationStatusSuccess
_, err = l.svcCtx.AuthorizationModel.Update(ctx, session, authorization)
if err != nil {
return errors.Wrapf(err, "更新授权状态失败")
}
if asyncErr := l.svcCtx.AsynqService.SendQueryTask(authorization.OrderId); asyncErr != nil {
logx.Errorf("异步任务调度失败: %v", asyncErr)
return asyncErr
}
return nil
})
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "更新授权信息失败: %v", err)
}
return &types.SmsAuthorizationResp{
OrderID: authorization.OrderId,
Passed: true,
}, nil
}

View File

@ -135,7 +135,6 @@ func (l *AlipayCallbackLogic) handleQueryOrderPayment(w http.ResponseWriter, not
UserId: order.UserId, UserId: order.UserId,
TargetName: encryptName, TargetName: encryptName,
TargetIdcard: encryptIdcard, TargetIdcard: encryptIdcard,
GrantType: model.GrantTypeFace,
Status: model.AuthorizationStatusPending, Status: model.AuthorizationStatusPending,
}) })
if err != nil { if err != nil {

View File

@ -205,7 +205,6 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
UserId: order.UserId, UserId: order.UserId,
TargetName: encryptName, TargetName: encryptName,
TargetIdcard: encryptIdcard, TargetIdcard: encryptIdcard,
GrantType: model.GrantTypeFace,
Status: model.AuthorizationStatusPending, Status: model.AuthorizationStatusPending,
}) })
if err != nil { if err != nil {

View File

@ -132,7 +132,6 @@ func (l *WechatPayCallbackLogic) handleQueryOrderPayment(w http.ResponseWriter,
UserId: order.UserId, UserId: order.UserId,
TargetName: encryptName, TargetName: encryptName,
TargetIdcard: encryptIdcard, TargetIdcard: encryptIdcard,
GrantType: model.GrantTypeFace,
Status: model.AuthorizationStatusPending, Status: model.AuthorizationStatusPending,
}) })
if err != nil { if err != nil {
@ -233,51 +232,6 @@ func (l *WechatPayCallbackLogic) handleRefund(order *model.AgentMembershipRechar
if refundErr != nil { if refundErr != nil {
return refundErr return refundErr
} }
} else {
refund, refundErr := l.svcCtx.AlipayService.AliRefund(ctx, order.OrderNo, order.Amount)
if refundErr != nil {
return refundErr
}
if refund.IsSuccess() {
redisKey := fmt.Sprintf(types.QueryCacheKey, order.UserId, order.OrderNo)
cache, cacheErr := l.svcCtx.Redis.Get(redisKey)
if cacheErr != nil {
return fmt.Errorf("获取缓存内容失败: %+v", cacheErr)
}
var data types.QueryCacheLoad
err := json.Unmarshal([]byte(cache), &data)
if err != nil {
return fmt.Errorf("解析缓存内容失败: %+v", err)
}
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, decodeErr := hex.DecodeString(secretKey)
if decodeErr != nil {
return fmt.Errorf("获取AES密钥失败: %+v", decodeErr)
}
decryptData, aesdecryptErr := crypto.AesDecrypt(data.Params, key)
if aesdecryptErr != nil {
return fmt.Errorf("解密参数失败: %+v", aesdecryptErr)
}
var paramsMap map[string]string
if err := json.Unmarshal([]byte(decryptData), &paramsMap); err != nil {
return fmt.Errorf("解析参数失败: %+v", err)
}
_, err = l.svcCtx.AuthorizationModel.Insert(l.ctx, nil, &model.Authorization{
OrderId: order.Id,
UserId: order.UserId,
TargetName: paramsMap["name"],
TargetIdcard: paramsMap["id_card"],
GrantType: model.GrantTypeFace,
Status: model.AuthorizationStatusPending,
})
if err != nil {
logx.Errorf("支付宝支付回调,插入授权信息失败: %+v", err)
return fmt.Errorf("插入授权信息失败: %+v", err)
}
} else {
logx.Errorf("支付宝退款失败:%v", refundErr)
return refundErr
}
} }
return nil return nil
} }

View File

@ -553,6 +553,17 @@ type SaveAgentMembershipUserConfigReq struct {
PriceRatio float64 `json:"price_ratio"` PriceRatio float64 `json:"price_ratio"`
} }
type SmsAuthorizationReq struct {
OrderNo string `json:"order_no" validate:"required"` // 订单号
Mobile string `json:"mobile" validate:"required,mobile"`
Code string `json:"code" validate:"required"`
}
type SmsAuthorizationResp struct {
Passed bool `json:"passed"` // 是否通过
OrderID int64 `json:"order_id"`
}
type TimeRangeReport struct { type TimeRangeReport struct {
Commission float64 `json:"commission"` // 佣金 Commission float64 `json:"commission"` // 佣金
Report int `json:"report"` // 报告量 Report int `json:"report"` // 报告量
@ -627,5 +638,5 @@ type GetAppVersionResp struct {
type SendSmsReq struct { type SendSmsReq struct {
Mobile string `json:"mobile" validate:"required,mobile"` Mobile string `json:"mobile" validate:"required,mobile"`
ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply bindMobile realName"` ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply bindMobile realName authorization"`
} }

View File

@ -56,19 +56,19 @@ type (
} }
Authorization struct { Authorization struct {
Id int64 `db:"id"` Id int64 `db:"id"`
OrderId int64 `db:"order_id"` OrderId int64 `db:"order_id"`
GrantType string `db:"grant_type"` // 授权类型face人脸后续可扩展 GrantType sql.NullString `db:"grant_type"` // 授权类型face人脸sms短信
AuthType sql.NullInt64 `db:"auth_type"` // 1本人2他人 AuthType sql.NullInt64 `db:"auth_type"` // 1本人2他人
UserId int64 `db:"user_id"` UserId int64 `db:"user_id"`
TargetName string `db:"target_name"` TargetName string `db:"target_name"`
TargetIdcard string `db:"target_idcard"` TargetIdcard string `db:"target_idcard"`
Status string `db:"status"` // 授权状态pending待授权、success已授权、expired已失效、revoked已撤销 Status string `db:"status"` // 授权状态pending待授权、success已授权、expired已失效、revoked已撤销、rejected被拒绝
CreateTime time.Time `db:"create_time"` CreateTime time.Time `db:"create_time"`
UpdateTime time.Time `db:"update_time"` UpdateTime time.Time `db:"update_time"`
DeleteTime sql.NullTime `db:"delete_time"` // 删除时间 DeleteTime sql.NullTime `db:"delete_time"` // 删除时间
DelState int64 `db:"del_state"` DelState int64 `db:"del_state"`
Version int64 `db:"version"` // 版本号 Version int64 `db:"version"` // 版本号
} }
) )

View File

@ -48,7 +48,8 @@ const (
) )
const ( const (
GrantTypeFace string = "face" AuthorizationGrantTypeFace = "face"
AuthorizationGrantTypeSms = "sms"
) )
const ( const (
AuthorizationStatusPending = "pending" AuthorizationStatusPending = "pending"

View File

@ -21,10 +21,10 @@ const config = {
targetDir: '../../app/user/model', targetDir: '../../app/user/model',
// 表名列表 // 表名列表
tables: [ tables: [
'agent', // 'agent',
// 'agent_active_stat', // 'agent_active_stat',
'agent_audit', // 'agent_audit',
'agent_real_name' // 'agent_real_name'
// 'agent_closure', // 'agent_closure',
// 'agent_commission', // 'agent_commission',
// 'agent_commission_deduction', // 'agent_commission_deduction',
@ -47,7 +47,7 @@ const config = {
// 'user', // 'user',
// 'user_auth', // 'user_auth',
// 'example', // 'example',
// 'authorization', 'authorization',
// 'authorization_face' // 'authorization_face'
] ]
}; };

View File

@ -49,10 +49,10 @@ $TARGET_DIR = "../../app/user/model"
# 表名列表 - 每个元素后必须有逗号分隔 # 表名列表 - 每个元素后必须有逗号分隔
$tables = @( $tables = @(
"agent", # "agent",
# "agent_active_stat", # "agent_active_stat",
"agent_audit", # "agent_audit",
"agent_real_name" # "agent_real_name"
# "agent_closure", # "agent_closure",
# "agent_commission", # "agent_commission",
# "agent_commission_deduction", # "agent_commission_deduction",
@ -75,7 +75,7 @@ $tables = @(
# "user", # "user",
# "user_auth", # "user_auth",
# "example", # "example",
# "authorization", "authorization"
# "authorization_face" # "authorization_face"
) )