Files
platform/web/services/verifier.go

169 lines
3.6 KiB
Go
Raw Normal View History

2025-03-18 17:57:07 +08:00
package services
import (
"context"
2025-04-18 17:47:54 +08:00
"encoding/json"
2025-03-18 17:57:07 +08:00
"errors"
"fmt"
"log/slog"
"math/rand"
2025-04-18 17:47:54 +08:00
"platform/pkg/env"
"platform/pkg/u"
g "platform/web/globals"
2025-03-18 17:57:07 +08:00
"strconv"
"time"
2025-04-18 17:47:54 +08:00
"github.com/alibabacloud-go/dysmsapi-20170525/v4/client"
2025-03-18 17:57:07 +08:00
"github.com/redis/go-redis/v9"
)
var Verifier = &verifierService{}
type verifierService struct {
2025-03-18 17:57:07 +08:00
}
func (s *verifierService) SendSms(ctx context.Context, phone string, purpose VerifierSmsPurpose) error {
key := smsKey(phone, purpose)
keyLock := key + ":lock"
// 检查发送频率1 分钟内只能发送一次
err := g.Redis.Watch(ctx, func(tx *redis.Tx) error {
2025-03-18 17:57:07 +08:00
result, err := tx.TTL(ctx, keyLock).Result()
if err != nil {
return err
}
if result > 0 {
return VerifierServiceSendLimitErr(result.Seconds())
}
if result != -2 {
return VerifierServiceError("验证码检查异常")
}
pipe := g.Redis.Pipeline()
2025-03-18 17:57:07 +08:00
pipe.Set(ctx, keyLock, "", 1*time.Minute)
_, err = pipe.Exec(ctx)
if err != nil {
return err
}
return nil
}, keyLock)
if err != nil {
return err
}
2025-04-18 17:47:54 +08:00
// 生成验证码
code := rand.Intn(900000) + 100000 // 6-digit code between 100000-999999
2025-03-18 17:57:07 +08:00
2025-04-18 17:47:54 +08:00
// 发送短信验证码
if env.DebugExternalChange {
params, err := json.Marshal(map[string]string{
"code": strconv.Itoa(code),
})
if err != nil {
return err
}
response, err := g.Aliyun.Sms.SendSms(&client.SendSmsRequest{
PhoneNumbers: &phone,
SignName: &env.AliyunSmsSignature,
TemplateCode: &env.AliyunSmsTemplateLogin,
TemplateParam: u.P(string(params)),
})
if err != nil {
_ = g.Redis.Del(ctx, key, keyLock).Err()
return err
}
if response.Body.Code == nil || *response.Body.Code != "OK" {
_ = g.Redis.Del(ctx, key, keyLock).Err()
return VerifierServiceError("验证码发送失败")
}
2025-04-18 17:47:54 +08:00
}
// 设置验证码
err = g.Redis.Set(ctx, key, code, 5*time.Minute).Err()
2025-04-18 17:47:54 +08:00
if err != nil {
_ = g.Redis.Del(ctx, key, keyLock).Err()
2025-04-18 17:47:54 +08:00
return err
}
slog.Debug("发送验证码", slog.String("phone", phone), slog.String("code", strconv.Itoa(code)))
2025-03-18 17:57:07 +08:00
return nil
}
func (s *verifierService) VerifySms(ctx context.Context, phone, code string) error {
key := smsKey(phone, VerifierSmsPurposeLogin)
2025-03-18 17:57:07 +08:00
keyLock := key + ":lock"
err := g.Redis.Watch(ctx, func(tx *redis.Tx) error {
2025-03-18 17:57:07 +08:00
// 检查验证码
val, err := g.Redis.Get(ctx, key).Result()
2025-03-18 17:57:07 +08:00
if err != nil && !errors.Is(err, redis.Nil) {
slog.Error("验证码获取失败", slog.Any("err", err))
return err
}
if val != code {
return ErrVerifierServiceInvalid
}
// 删除验证码
_, err = tx.Pipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.Del(ctx, key)
pipe.Del(ctx, keyLock)
return nil
})
if err != nil {
slog.Error("验证码删除失败", slog.Any("err", err))
return err
}
return nil
}, key)
if err != nil {
return err
2025-03-18 17:57:07 +08:00
}
return nil
}
func (s *verifierService) GetSms(ctx context.Context, phone string) (string, error) {
key := smsKey(phone, VerifierSmsPurposeLogin)
val, err := g.Redis.Get(ctx, key).Result()
if err != nil {
return "", fmt.Errorf("验证码获取失败: %w", err)
}
return val, nil
}
func smsKey(phone string, purpose VerifierSmsPurpose) string {
return fmt.Sprintf("verify:sms:%d:%s", purpose, phone)
2025-03-18 17:57:07 +08:00
}
// region 短信目的
type VerifierSmsPurpose int
const (
VerifierSmsPurposeLogin VerifierSmsPurpose = iota // 登录
)
// region 服务异常
type VerifierServiceError string
func (e VerifierServiceError) Error() string {
return string(e)
}
var (
ErrVerifierServiceInvalid = VerifierServiceError("验证码错误")
)
type VerifierServiceSendLimitErr int
func (e VerifierServiceSendLimitErr) Error() string {
return "发送频率过快"
}