tokenFactory/common/sms_verification.go

157 lines
4.8 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 common
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"time"
)
var mainlandChinaPhoneRegexp = regexp.MustCompile(`^1[3-9]\d{9}$`)
// NormalizePhone 标准化手机号(去空格)。
func NormalizePhone(phone string) string {
normalized := strings.TrimSpace(phone)
normalized = strings.ReplaceAll(normalized, " ", "")
normalized = strings.ReplaceAll(normalized, "-", "")
normalized = strings.ReplaceAll(normalized, "(", "")
normalized = strings.ReplaceAll(normalized, ")", "")
if strings.HasPrefix(normalized, "+86") {
normalized = strings.TrimPrefix(normalized, "+86")
} else if strings.HasPrefix(normalized, "0086") {
normalized = strings.TrimPrefix(normalized, "0086")
} else if len(normalized) == 13 && strings.HasPrefix(normalized, "86") {
normalized = strings.TrimPrefix(normalized, "86")
}
return normalized
}
// ValidateMainlandChinaPhone 校验中国大陆 11 位手机号格式。
func ValidateMainlandChinaPhone(phone string) bool {
return mainlandChinaPhoneRegexp.MatchString(NormalizePhone(phone))
}
// SMSVerificationCodeKey 返回短信验证码 Redis Key。
func SMSVerificationCodeKey(phone string) string {
return "sms:code:" + NormalizePhone(phone)
}
// SMSVerificationCooldownKey 返回短信冷却 Redis Key。
func SMSVerificationCooldownKey(phone string) string {
return "sms:cooldown:" + NormalizePhone(phone)
}
// SMSVerificationDailyCountKey 返回短信日计数 Redis Key。
func SMSVerificationDailyCountKey(phone string, now time.Time) string {
return "sms:daily:" + NormalizePhone(phone) + ":" + now.Format("20060102")
}
// EnsureRedisEnabledForSMS 短信验证码依赖 Redis未启用时返回错误。
func EnsureRedisEnabledForSMS() error {
if !RedisEnabled || RDB == nil {
return fmt.Errorf("短信验证码服务未启用,请先配置 Redis")
}
return nil
}
// IsSMSPhoneBlacklisted 判断手机号是否在短信黑名单中。
func IsSMSPhoneBlacklisted(phone string) bool {
phone = NormalizePhone(phone)
for _, blocked := range SMSPhoneBlacklist {
if NormalizePhone(blocked) == phone {
return true
}
}
return false
}
// CheckSMSCanSend 校验手机号是否满足发送频率限制。
func CheckSMSCanSend(phone string) error {
if err := EnsureRedisEnabledForSMS(); err != nil {
return err
}
phone = NormalizePhone(phone)
ctx := context.Background()
cooldownKey := SMSVerificationCooldownKey(phone)
exists, err := RDB.Exists(ctx, cooldownKey).Result()
if err != nil {
return fmt.Errorf("读取短信冷却状态失败: %w", err)
}
if exists > 0 {
return fmt.Errorf("发送过于频繁,请 %d 分钟后再试", SMSCodeCooldownMinutes)
}
dailyKey := SMSVerificationDailyCountKey(phone, time.Now())
countStr, err := RDB.Get(ctx, dailyKey).Result()
if err == nil {
count, parseErr := strconv.Atoi(countStr)
if parseErr == nil && count >= SMSCodeDailyLimit {
return fmt.Errorf("该手机号今日发送次数已达上限(%d 次)", SMSCodeDailyLimit)
}
}
return nil
}
// RecordSMSSend 成功发送短信后,记录冷却与当日计数。
func RecordSMSSend(phone string) error {
if err := EnsureRedisEnabledForSMS(); err != nil {
return err
}
phone = NormalizePhone(phone)
ctx := context.Background()
cooldownKey := SMSVerificationCooldownKey(phone)
if err := RDB.Set(ctx, cooldownKey, "1", time.Duration(SMSCodeCooldownMinutes)*time.Minute).Err(); err != nil {
return fmt.Errorf("写入短信冷却状态失败: %w", err)
}
now := time.Now()
dailyKey := SMSVerificationDailyCountKey(phone, now)
count, err := RDB.Incr(ctx, dailyKey).Result()
if err != nil {
return fmt.Errorf("更新短信日计数失败: %w", err)
}
if count == 1 {
nextDay := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location())
expire := time.Until(nextDay)
if expire <= 0 {
expire = 24 * time.Hour
}
if err := RDB.Expire(ctx, dailyKey, expire).Err(); err != nil {
return fmt.Errorf("设置短信日计数过期失败: %w", err)
}
}
return nil
}
// StoreSMSVerificationCode 保存短信验证码,默认 5 分钟过期。
func StoreSMSVerificationCode(phone, code string) error {
if err := EnsureRedisEnabledForSMS(); err != nil {
return err
}
ctx := context.Background()
key := SMSVerificationCodeKey(phone)
return RDB.Set(ctx, key, code, time.Duration(SMSCodeValidMinutes)*time.Minute).Err()
}
// VerifyAndConsumeSMSCode 校验短信验证码并在成功后删除,避免重复使用。
func VerifyAndConsumeSMSCode(phone, code string) bool {
if err := EnsureRedisEnabledForSMS(); err != nil {
return false
}
ctx := context.Background()
key := SMSVerificationCodeKey(phone)
val, err := RDB.Get(ctx, key).Result()
if err != nil || strings.TrimSpace(val) == "" {
return false
}
if strings.TrimSpace(val) != strings.TrimSpace(code) {
return false
}
_ = RDB.Del(ctx, key).Err()
return true
}