Files
platform/web/services/verifier.go

169 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"math/rand"
"platform/pkg/env"
"platform/pkg/u"
g "platform/web/globals"
"strconv"
"time"
"github.com/alibabacloud-go/dysmsapi-20170525/v4/client"
"github.com/redis/go-redis/v9"
)
var Verifier = &verifierService{}
type verifierService struct {
}
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 {
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()
pipe.Set(ctx, keyLock, "", 1*time.Minute)
_, err = pipe.Exec(ctx)
if err != nil {
return err
}
return nil
}, keyLock)
if err != nil {
return err
}
// 生成验证码
code := rand.Intn(900000) + 100000 // 6-digit code between 100000-999999
// 发送短信验证码
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("验证码发送失败")
}
}
// 设置验证码
err = g.Redis.Set(ctx, key, code, 5*time.Minute).Err()
if err != nil {
_ = g.Redis.Del(ctx, key, keyLock).Err()
return err
}
slog.Debug("发送验证码", slog.String("phone", phone), slog.String("code", strconv.Itoa(code)))
return nil
}
func (s *verifierService) VerifySms(ctx context.Context, phone, code string) error {
key := smsKey(phone, VerifierSmsPurposeLogin)
keyLock := key + ":lock"
err := g.Redis.Watch(ctx, func(tx *redis.Tx) error {
// 检查验证码
val, err := g.Redis.Get(ctx, key).Result()
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
}
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)
}
// 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 "发送频率过快"
}