feat(distribution): 支持分站独立短信验证码通道
- 新增分销站点独立短信服务商配置(阿里云),登录验证码按 site_code/domain 解析专属通道,未启用或解析失败时回退主站默认通道 - 密钥经 crypto.Encryptor 加密落库,模型字段 json 序列化隔离,DTO 仅回传掩码 - provider 白名单校验,mock 仅在非生产环境放行;启用时强制校验必填凭据 - 配置读写/测试接口限管理员权限,测试发送增加 60s 频控(Redis,缺省进程内) - 解密失败记告警并优雅回退主站,避免把密文当明文密钥使用 - 两套前端新增短信配置入口、必填与手机号校验、发送冷却 - 新增迁移 000071 及加解密、白名单、频控、解析回退等单测
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/captcha"
|
||||
@@ -48,12 +49,18 @@ const (
|
||||
loginMaxFailureCount = 5
|
||||
)
|
||||
|
||||
// SiteSMSProviderResolver 用于根据分销站点标识解析专属 SMS Provider
|
||||
type SiteSMSProviderResolver interface {
|
||||
ResolveSMSProvider(ctx context.Context, siteCode, domain string) (smsprovider.Provider, error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
users *UserRepository
|
||||
redis *redis.Client
|
||||
jwt *JWTManager
|
||||
sms smsprovider.Provider
|
||||
log *zap.Logger
|
||||
users *UserRepository
|
||||
redis *redis.Client
|
||||
jwt *JWTManager
|
||||
sms smsprovider.Provider
|
||||
smsResolver SiteSMSProviderResolver
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
type LoginResult struct {
|
||||
@@ -65,6 +72,10 @@ func NewService(users *UserRepository, redis *redis.Client, jwt *JWTManager, sms
|
||||
return &Service{users: users, redis: redis, jwt: jwt, sms: sms, log: log}
|
||||
}
|
||||
|
||||
func (s *Service) SetSiteSMSResolver(resolver SiteSMSProviderResolver) {
|
||||
s.smsResolver = resolver
|
||||
}
|
||||
|
||||
func (s *Service) Captcha(ctx context.Context) (*captcha.Item, error) {
|
||||
if s.redis == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
@@ -76,7 +87,7 @@ func (s *Service) Captcha(ctx context.Context) (*captcha.Item, error) {
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *Service) SendSMSCode(ctx context.Context, phone string, captchaID string, captchaCode string) error {
|
||||
func (s *Service) SendSMSCode(ctx context.Context, phone string, captchaID string, captchaCode string, siteContext ...string) error {
|
||||
if s.redis == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
@@ -117,14 +128,39 @@ func (s *Service) SendSMSCode(ctx context.Context, phone string, captchaID strin
|
||||
return err
|
||||
}
|
||||
|
||||
if s.sms == nil {
|
||||
provider := s.sms
|
||||
siteCode := ""
|
||||
domain := ""
|
||||
if len(siteContext) > 0 {
|
||||
siteCode = strings.TrimSpace(siteContext[0])
|
||||
}
|
||||
if len(siteContext) > 1 {
|
||||
domain = strings.TrimSpace(siteContext[1])
|
||||
}
|
||||
if s.smsResolver != nil && (siteCode != "" || domain != "") {
|
||||
customProvider, resolveErr := s.smsResolver.ResolveSMSProvider(ctx, siteCode, domain)
|
||||
if resolveErr != nil {
|
||||
if s.log != nil {
|
||||
s.log.Warn("解析分销站点独立短信服务商失败,将回退至主站默认通道",
|
||||
zap.String("site_code", siteCode),
|
||||
zap.String("domain", domain),
|
||||
zap.Error(resolveErr),
|
||||
)
|
||||
}
|
||||
} else if customProvider != nil {
|
||||
provider = customProvider
|
||||
}
|
||||
}
|
||||
|
||||
if provider == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
if err := s.sms.SendLoginCode(ctx, phone, code); err != nil {
|
||||
if err := provider.SendLoginCode(ctx, phone, code); err != nil {
|
||||
if s.log != nil {
|
||||
fields := []zap.Field{
|
||||
zap.String("module", "auth"),
|
||||
zap.String("phone", PublicPhone(phone)),
|
||||
zap.String("site_code", siteCode),
|
||||
zap.Error(err),
|
||||
}
|
||||
if requestID := logging.RequestIDFromContext(ctx); requestID != "" {
|
||||
|
||||
Reference in New Issue
Block a user