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次则不允许再次请求
};
}
}