// Package common 提供各业务端共用的鉴权、资源、钱包和账户范围能力。 package common import ( "fmt" "regexp" "strings" "time" "git.apinb.com/bsm-sdk/core/crypto/token" "git.apinb.com/bsm-sdk/core/env" "git.apinb.com/bsm-sdk/core/errcode" "git.apinb.com/bsm-sdk/core/infra" sdkmiddleware "git.apinb.com/bsm-sdk/core/middleware" "git.apinb.com/heqiapp/platforms/backend/api/internal/config" "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/gin-gonic/gin" ) var phonePattern = regexp.MustCompile(`^1[3-9]\d{9}$`) var verificationPurposes = map[string]struct{}{ "login": {}, "register": {}, "reset_login_password": {}, "set_payment_password": {}, "reset_payment_password": {}, "bind_bank": {}, "unbind_bank": {}, } type verificationValue struct { Code string `json:"code"` Phone string `json:"phone"` Purpose string `json:"purpose"` Client string `json:"client"` } // ValidPhone 判断手机号是否符合中国大陆手机号格式。 func ValidPhone(phone string) bool { return phonePattern.MatchString(strings.TrimSpace(phone)) } // SendVerificationCode 创建一次性验证码。Mock 模式的验证码只保存在 Redis,不返回给客户端。 func SendVerificationCode(client string) gin.HandlerFunc { return func(ctx *gin.Context) { var request struct { Phone string `json:"phone" binding:"required"` Purpose string `json:"purpose" binding:"required"` } if ctx.ShouldBindJSON(&request) != nil || !ValidPhone(request.Phone) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } if _, ok := verificationPurposes[request.Purpose]; !ok { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } phone := strings.TrimSpace(request.Phone) throttleKey := impl.RedisService.BuildKey("client-verification-throttle", client, phone) var sent bool if impl.RedisService.Get(throttleKey, &sent) == nil && sent { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } requestIdentity := models.NewIdentity() value := verificationValue{Code: config.Spec.Global.MockVerificationCode, Phone: phone, Purpose: request.Purpose, Client: client} ttl := time.Duration(config.Spec.Global.VerificationTTLSeconds) * time.Second if err := impl.RedisService.Set(verificationKey(requestIdentity), value, ttl); err != nil { infra.Response.Error(ctx, err) return } _ = impl.RedisService.Set(throttleKey, true, time.Duration(config.Spec.Global.VerificationSendIntervalSeconds)*time.Second) infra.Response.Success(ctx, gin.H{"request_identity": requestIdentity, "expires_in": config.Spec.Global.VerificationTTLSeconds}) } } // VerifyCode 校验并消费验证码。 func VerifyCode(client, phone, purpose, requestIdentity, code string) bool { if !config.Spec.Global.MockVerificationEnabled || requestIdentity == "" || code == "" { return false } key := verificationKey(requestIdentity) var value verificationValue if impl.RedisService.Get(key, &value) != nil { return false } if value.Client != client || value.Phone != strings.TrimSpace(phone) || value.Purpose != purpose || value.Code != code { return false } return impl.RedisService.Delete(key) == nil } func verificationKey(identity string) string { return impl.RedisService.BuildKey("client-verification", identity) } // IssueToken 签发严格区分 user_app 和 service_app 的 JWT。 func IssueToken(identity, client, role string, extend map[string]string) (string, error) { return token.New(env.Runtime.JwtSecretKey).GenerateJwt(0, identity, client, role, nil, extend) } // RequireClient 验证客户端种类,阻止后台令牌跨端调用。 func RequireClient(client string) gin.HandlerFunc { return func(ctx *gin.Context) { claims, err := sdkmiddleware.ParseAuth(ctx) if err != nil || claims.Client != client { infra.Response.Error(ctx, errcode.ErrPermissionDenied) ctx.Abort() return } ctx.Next() } } // UserAccount 返回当前启用的用户账户。 func UserAccount(ctx *gin.Context) (models.UserAccount, bool) { claims, err := sdkmiddleware.ParseAuth(ctx) if err != nil { infra.Response.Error(ctx, err) return models.UserAccount{}, false } var account models.UserAccount if impl.DBService.Where("identity = ? AND status = ?", claims.Identity, 1).First(&account).Error != nil { infra.Response.Error(ctx, errcode.ErrPermissionDenied) return models.UserAccount{}, false } return account, true } // StaffAccount 返回当前启用的工作人员账户。 func StaffAccount(ctx *gin.Context) (models.StaffAccount, bool) { claims, err := sdkmiddleware.ParseAuth(ctx) if err != nil { infra.Response.Error(ctx, err) return models.StaffAccount{}, false } var account models.StaffAccount if impl.DBService.Where("identity = ? AND status = ?", claims.Identity, 1).First(&account).Error != nil { infra.Response.Error(ctx, errcode.ErrPermissionDenied) return models.StaffAccount{}, false } return account, true } // RecordNo 生成便于检索的业务流水号。 func RecordNo(prefix string) string { return fmt.Sprintf("%s%d", prefix, time.Now().UnixNano()) }