179 lines
6.6 KiB
C#
179 lines
6.6 KiB
C#
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);
|
||
var result = await tencentSMSSendVerificationCode.SendVerificationCode(verificationCode.ToString(), 5);
|
||
if (!result)
|
||
{
|
||
// 记录发送失败日志
|
||
Logger.LogError($"验证码发送失败,手机号:{phone},IP:{ipAddress}");
|
||
throw new CustomException("验证码发送失败");
|
||
}
|
||
|
||
// 发送成功,存入Redis,5分钟有效期
|
||
await RedisCache.StringSetAsync(redisKey, verificationCode.ToString(), TimeSpan.FromMinutes(5));
|
||
|
||
// 更新重试次数和锁定时间
|
||
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);
|
||
|
||
// 保存到数据库
|
||
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次则不允许再次请求
|
||
};
|
||
}
|
||
}
|