using AutoMapper; using ChouBox.Code.AppExtend; using ChouBox.Code.TencentCloudExtend; using ChouBox.Model.Entities; using HuanMeng.DotNetCore.Base; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ChouBox.Code.Other; /// /// 发送验证码服务 /// public class SMSBLL : ChouBoxCodeBase { private const string RETRY_COUNT_SUFFIX = ":RetryCount"; private const string LOCK_SUFFIX = ":Lock"; public SMSBLL(IServiceProvider serviceProvider) : base(serviceProvider) { } /// /// 发送验证码 /// /// /// 验证码类型:1-注册,2-登录,3-找回密码,4-修改手机,5-修改邮箱,6-其他 /// 用户ID,可为空表示未登录用户 /// 返回下一次可请求的等待秒数,-1表示当天不能再请求 /// /// public async Task SendPhoneAsync(string phone, sbyte codeType = 2, ulong? userId = null) { if (string.IsNullOrEmpty(phone)) { throw new ArgumentNullException("手机号不能为空"); } var smsConfig = await this.GetTencentSMSConfigAsync(); if (smsConfig == null) { throw new ArgumentNullException("暂未开放发送验证码!"); } // 检查当天发送次数限制 var dailyCountKey = $"VerificationCodeDailyCount:{phone}:{DateTime.Now:yyyyMMdd}"; var dailyCount = RedisCache.StringGet(dailyCountKey) ?? 0; // 获取重试次数 var retryCountKey = $"VerificationCode:{phone}{RETRY_COUNT_SUFFIX}"; var retryCount = RedisCache.StringGet(retryCountKey) ?? 0; var resendLockKey = $"VerificationCode:{phone}{LOCK_SUFFIX}"; var resendLock = RedisCache.StringGet(resendLockKey); // 计算本次等待时间 int waitSeconds = GetWaitSeconds(retryCount); if (resendLock != null && !string.IsNullOrEmpty(resendLock)) { // 获取锁的剩余过期时间(秒) var remainingSeconds = RedisCache.KeyTimeToLive(resendLockKey)?.TotalSeconds ?? 0; remainingSeconds = Math.Ceiling(remainingSeconds); // 向上取整 string message; if (remainingSeconds <= 60) { message = $"验证码发送过于频繁,请{remainingSeconds}秒后再试。"; } else { int remainingMinutes = (int)Math.Floor(remainingSeconds / 60); message = $"验证码发送过于频繁,请{remainingMinutes}分钟后再试。"; } throw new CustomException(message); } // 设置每天最大发送次数为5次 const int maxDailyCount = 5; if (dailyCount >= maxDailyCount) { // 记录达到每日上限日志 Logger.LogWarning($"手机号{phone}当天验证码发送次数已达上限({maxDailyCount}次)"); throw new CustomException($"当天验证码发送次数已达上限,请明天再试"); } Random random = new Random(); var verificationCode = random.Next(1000, 9999); var redisKey = $"VerificationCode:{phone}"; // 获取客户端真实IP地址 string ipAddress = GetRealIpAddress(); try { TencentSMSSendVerificationCode tencentSMSSendVerificationCode = new TencentSMSSendVerificationCode(smsConfig, phone); bool smsSucceeded = true; string errorMessage = "验证码发送失败"; try { var result = await tencentSMSSendVerificationCode.SendVerificationCode(verificationCode.ToString(), 5); if (!result) { smsSucceeded = false; } } catch (Exception smsEx) { smsSucceeded = false; errorMessage = smsEx.Message; // 记录发送失败日志 Logger.LogError($"验证码发送异常,手机号:{phone},IP:{ipAddress},错误:{smsEx.Message}"); } // 无论成功失败都更新重试次数和锁定时间 retryCount++; var nextWaitSeconds = GetWaitSeconds(retryCount); // 设置不能重发的锁 await RedisCache.StringSetAsync(resendLockKey, "1", TimeSpan.FromSeconds(waitSeconds)); // 更新重试次数,5分钟后过期 await RedisCache.StringSetAsync(retryCountKey, retryCount.ToString(), TimeSpan.FromMinutes(5)); // 更新当天发送次数计数并设置过期时间为当天剩余时间 dailyCount++; var endOfDay = DateTime.Today.AddDays(1).AddSeconds(-1); // 当天23:59:59 var expireTime = endOfDay - DateTime.Now; await RedisCache.StringSetAsync(dailyCountKey, dailyCount.ToString(), expireTime); // 如果短信发送失败,抛出异常 if (!smsSucceeded) { throw new CustomException(errorMessage); } // 发送成功,存入Redis,5分钟有效期 await RedisCache.StringSetAsync(redisKey, verificationCode.ToString(), TimeSpan.FromMinutes(5)); // 保存到数据库 var verificationRecord = new UserVerificationCodes { UserId = userId, Account = phone, Code = verificationCode.ToString(), CodeType = codeType, Channel = "sms", IpAddress = ipAddress, Status = 0, // 未使用 CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now, ExpiredAt = DateTime.Now.AddMinutes(5) }; await Dao.Context.UserVerificationCodes.AddAsync(verificationRecord); await Dao.Context.SaveChangesAsync(); return nextWaitSeconds; } catch (Exception ex) { // 记录异常日志 Logger.LogError($"发送验证码异常:{ex.Message},手机号:{phone},IP:{ipAddress}"); throw; } } /// /// 根据重试次数获取等待时间 /// /// 重试次数 /// 等待秒数,-1表示不允许再次请求 private int GetWaitSeconds(int retryCount) { return retryCount switch { 0 => 30, // 第1次请求后等待30秒 1 => 60, // 第2次请求后等待60秒 2 => 60, // 第3次请求后等待60秒 3 => 300, // 第4次请求后等待300秒(5分钟) 4 => 300, // 第5次请求后等待300秒(5分钟) _ => -1 // 超过5次则不允许再次请求 }; } }