125 lines
2.5 KiB
Go
125 lines
2.5 KiB
Go
|
|
package services
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"log/slog"
|
|||
|
|
"math/rand"
|
|||
|
|
"platform/init/rds"
|
|||
|
|
"strconv"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/redis/go-redis/v9"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var Verifier = &verifierService{}
|
|||
|
|
|
|||
|
|
type verifierService struct {
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type VerifierServiceError string
|
|||
|
|
|
|||
|
|
func (e VerifierServiceError) Error() string {
|
|||
|
|
return string(e)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var (
|
|||
|
|
ErrVerifierServiceInvalid = VerifierServiceError("验证码错误")
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type VerifierServiceSendLimitErr int
|
|||
|
|
|
|||
|
|
func (e VerifierServiceSendLimitErr) Error() string {
|
|||
|
|
return "发送频率过快"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type VerifierSmsPurpose int
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
Login VerifierSmsPurpose = iota
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func smsKey(phone string, purpose VerifierSmsPurpose) string {
|
|||
|
|
return fmt.Sprintf("verify:sms:%d:%s", purpose, phone)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *verifierService) SendSms(ctx context.Context, phone string, purpose VerifierSmsPurpose) error {
|
|||
|
|
key := smsKey(phone, purpose)
|
|||
|
|
keyLock := key + ":lock"
|
|||
|
|
|
|||
|
|
// 生成验证码
|
|||
|
|
code := rand.Intn(900000) + 100000 // 6-digit code between 100000-999999
|
|||
|
|
|
|||
|
|
// 检查发送频率,1 分钟内只能发送一次
|
|||
|
|
err := rds.Client.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 := rds.Client.Pipeline()
|
|||
|
|
pipe.Set(ctx, key, code, 10*time.Minute)
|
|||
|
|
pipe.Set(ctx, keyLock, "", 1*time.Minute)
|
|||
|
|
_, err = pipe.Exec(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}, keyLock)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TODO: 发送短信验证码
|
|||
|
|
slog.Debug("发送验证码", slog.String("phone", phone), slog.String("code", strconv.Itoa(code)))
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *verifierService) VerifySms(ctx context.Context, phone, code string) (bool, error) {
|
|||
|
|
key := smsKey(phone, Login)
|
|||
|
|
keyLock := key + ":lock"
|
|||
|
|
|
|||
|
|
err := rds.Client.Watch(ctx, func(tx *redis.Tx) error {
|
|||
|
|
|
|||
|
|
// 检查验证码
|
|||
|
|
val, err := rds.Client.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 {
|
|||
|
|
if errors.Is(err, ErrVerifierServiceInvalid) {
|
|||
|
|
return false, nil
|
|||
|
|
}
|
|||
|
|
return false, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true, nil
|
|||
|
|
}
|