ChouBox/ChouBox.Code/Other/SMSBLL.cs
2025-04-24 01:45:31 +08:00

194 lines
7.1 KiB
C#
Raw Permalink 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.

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;
/// <summary>
/// 发送验证码服务
/// </summary>
public class SMSBLL : ChouBoxCodeBase
{
private const string RETRY_COUNT_SUFFIX = ":RetryCount";
private const string LOCK_SUFFIX = ":Lock";
public SMSBLL(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
/// <summary>
/// 发送验证码
/// </summary>
/// <param name="phone"></param>
/// <param name="codeType">验证码类型1-注册2-登录3-找回密码4-修改手机5-修改邮箱6-其他</param>
/// <param name="userId">用户ID可为空表示未登录用户</param>
/// <returns>返回下一次可请求的等待秒数,-1表示当天不能再请求</returns>
/// <exception cref="CustomException"></exception>
/// <exception cref="Exception"></exception>
public async Task<int> 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<int?>(dailyCountKey) ?? 0;
// 获取重试次数
var retryCountKey = $"VerificationCode:{phone}{RETRY_COUNT_SUFFIX}";
var retryCount = RedisCache.StringGet<int?>(retryCountKey) ?? 0;
var resendLockKey = $"VerificationCode:{phone}{LOCK_SUFFIX}";
var resendLock = RedisCache.StringGet<string>(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);
}
// 发送成功存入Redis5分钟有效期
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;
}
}
/// <summary>
/// 根据重试次数获取等待时间
/// </summary>
/// <param name="retryCount">重试次数</param>
/// <returns>等待秒数,-1表示不允许再次请求</returns>
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次则不允许再次请求
};
}
}