package services import ( "context" "encoding/json" "errors" "fmt" "log/slog" "math/rand" "platform/pkg/env" "platform/pkg/rds" "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 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 ( VerifierSmsPurposeLogin VerifierSmsPurpose = iota ) type verifierService struct { } func (s *verifierService) SendSms(ctx context.Context, phone string, purpose VerifierSmsPurpose) error { key := smsKey(phone, purpose) keyLock := key + ":lock" // 检查发送频率,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, 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 // 发送短信验证码 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 { _ = rds.Client.Del(ctx, key, keyLock).Err() return err } if response.Body.Code == nil || *response.Body.Code != "OK" { _ = rds.Client.Del(ctx, key, keyLock).Err() return VerifierServiceError("验证码发送失败") } // 设置验证码 err = rds.Client.Set(ctx, key, code, 5*time.Minute).Err() if err != nil { _ = rds.Client.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 := 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 { return err } return nil } func smsKey(phone string, purpose VerifierSmsPurpose) string { return fmt.Sprintf("verify:sms:%d:%s", purpose, phone) }