using Dpz.Core.Entity.Base;
using Dpz.Core.MessageQueue.Abstractions;
using Dpz.Core.Public.ViewModel.Messages;
using Microsoft.AspNetCore.RateLimiting;

namespace Dpz.Core.WebApi.Controllers;

/// <summary>
/// 邮箱验证码控制器
/// </summary>
[ApiController, Route("api/[controller]")]
public class EmailVerifyController(
    IConfiguration configuration,
    IMessagePublisher<SendEmailVerifyCodeMessage> verifyCodePublisher,
    IFusionCache fusionCache,
    ILogger<EmailVerifyController> logger
) : ControllerBase
{
    private readonly bool _emailVerifyEnabled =
        configuration.GetSection("EmailVerifyServiceOpen").Get<bool?>() ?? false;

    /// <summary>
    /// 验证码缓存key前缀
    /// </summary>
    private const string VerifyCodeCachePrefix = "EmailVerifyCode:";

    /// <summary>
    /// 发送时间缓存key前缀
    /// </summary>
    private const string SendTimeCachePrefix = "EmailVerifySendTime:";

    /// <summary>
    /// 发送间隔限制(秒)
    /// </summary>
    private const int SendIntervalSeconds = 60;

    /// <summary>
    /// 验证码有效期
    /// </summary>
    private static readonly TimeSpan VerifyCodeExpiration = TimeSpan.FromMinutes(30);

    /// <summary>
    /// 验证通过后免验证时长
    /// </summary>
    private static readonly TimeSpan VerifyPassedExpiration = TimeSpan.FromDays(3);

    /// <summary>
    /// 获取邮箱验证服务状态
    /// </summary>
    /// <returns></returns>
    [HttpGet("status")]
    public ActionResult<ResponseResult<bool>> GetStatus()
    {
        return Ok(ResponseResult<bool>.Ok(_emailVerifyEnabled));
    }

    /// <summary>
    /// 检查当前请求是否需要邮箱验证
    /// </summary>
    /// <param name="email"></param>
    /// <returns></returns>
    [HttpGet("check-need-verify")]
    public async Task<ActionResult<ResponseResult<bool>>> CheckNeedVerify(
        [FromQuery] string? email = null
    )
    {
        if (User.Identity?.IsAuthenticated == true)
        {
            return Ok(ResponseResult<bool>.Ok(false));
        }

        if (!_emailVerifyEnabled)
        {
            return Ok(ResponseResult<bool>.Ok(false));
        }

        if (string.IsNullOrWhiteSpace(email))
        {
            return Ok(ResponseResult<bool>.Ok(true));
        }

        var verifyPassedCacheKey = Request.BuildEmailVerifyPassedCacheKey(email);
        var verified = await fusionCache.GetOrDefaultAsync<bool>(verifyPassedCacheKey);
        return Ok(ResponseResult<bool>.Ok(!verified));
    }

    /// <summary>
    /// 发送验证码
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    [HttpPost("send"), EnableRateLimiting("comment")]
    [ProducesResponseType<ResponseResult<int>>(StatusCodes.Status200OK)]
    [ProducesResponseType<ResponseResult>(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> SendVerifyCode([FromBody] SendVerifyCodeRequest request)
    {
        if (!_emailVerifyEnabled)
        {
            return BadRequest(ResponseResult.Fail("邮箱验证服务未开启"));
        }

        if (string.IsNullOrWhiteSpace(request.Email))
        {
            return BadRequest(ResponseResult.Fail("邮箱不能为空"));
        }

        // 检查是否在冷却时间内
        var sendTimeCacheKey = $"{SendTimeCachePrefix}{request.Email.ToLower()}";
        var lastSendTime = await fusionCache.GetOrDefaultAsync<DateTime?>(sendTimeCacheKey);

        if (lastSendTime.HasValue)
        {
            var elapsed = (DateTime.Now - lastSendTime.Value).TotalSeconds;
            if (elapsed < SendIntervalSeconds)
            {
                var remainingSeconds = (int)(SendIntervalSeconds - elapsed);
                return BadRequest(ResponseResult.Fail($"请等待 {remainingSeconds} 秒后再试"));
            }
        }

        // 生成6位数字验证码
        var verifyCode = Random.Shared.Next(100000, 999999).ToString();

        try
        {
            // 发布验证码发送消息
            await verifyCodePublisher.PublishAsync(
                new SendEmailVerifyCodeMessage
                {
                    Email = request.Email,
                    VerifyCode = verifyCode,
                    Purpose = "评论验证",
                }
            );

            // 记录发送时间
            await fusionCache.SetAsync(
                sendTimeCacheKey,
                DateTime.Now,
                options => options.SetDuration(TimeSpan.FromSeconds(SendIntervalSeconds))
            );

            logger.LogInformation("验证码发送成功: Email={Email}", request.Email);

            return Ok(
                ResponseResult<int>.Ok((int)VerifyCodeExpiration.TotalMinutes, "验证码已发送")
            );
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "验证码发送失败: Email={Email}", request.Email);
            return BadRequest(ResponseResult.Fail("验证码发送失败,请稍后重试"));
        }
    }

    /// <summary>
    /// 验证验证码
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    [HttpPost("verify")]
    [ProducesResponseType<ResponseResult>(StatusCodes.Status200OK)]
    [ProducesResponseType<ResponseResult>(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> VerifyCode([FromBody] VerifyCodeRequest request)
    {
        if (!_emailVerifyEnabled)
        {
            return BadRequest(ResponseResult.Fail("邮箱验证服务未开启"));
        }

        if (string.IsNullOrWhiteSpace(request.Email))
        {
            return BadRequest(ResponseResult.Fail("邮箱不能为空"));
        }

        if (string.IsNullOrWhiteSpace(request.Code))
        {
            return BadRequest(ResponseResult.Fail("验证码不能为空"));
        }

        var cacheKey = $"{VerifyCodeCachePrefix}{request.Email.ToLower()}";
        var cachedCode = await fusionCache.GetOrDefaultAsync<string>(cacheKey);

        if (string.IsNullOrEmpty(cachedCode))
        {
            return BadRequest(ResponseResult.Fail("验证码已过期或不存在"));
        }

        if (cachedCode != request.Code)
        {
            return BadRequest(ResponseResult.Fail("验证码错误"));
        }

        // 验证成功后删除验证码(一次性使用)
        await fusionCache.RemoveAsync(cacheKey);

        // 记录验证通过状态,3天内免验证
        await fusionCache.SetAsync(
            Request.BuildEmailVerifyPassedCacheKey(request.Email),
            true,
            options => options.SetDuration(VerifyPassedExpiration)
        );

        logger.LogInformation("验证码验证成功: Email={Email}", request.Email);

        return Ok(ResponseResult.Ok("验证成功"));
    }
}

/// <summary>
/// 发送验证码请求
/// </summary>
public class SendVerifyCodeRequest
{
    /// <summary>
    /// 邮箱地址
    /// </summary>
    public required string Email { get; set; }
}

/// <summary>
/// 验证验证码请求
/// </summary>
public class VerifyCodeRequest
{
    /// <summary>
    /// 邮箱地址
    /// </summary>
    public required string Email { get; set; }

    /// <summary>
    /// 验证码
    /// </summary>
    public required string Code { get; set; }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个ASP.NET Core Web API控制器,用于处理邮箱验证码的相关功能。让我详细解释一下代码的各个部分:

类概述

EmailVerifyController 是一个邮箱验证码控制器,提供验证码的发送、验证等功能。

构造函数依赖注入

public class EmailVerifyController(
    IConfiguration configuration,                              // 配置服务
    IMessagePublisher<SendEmailVerifyCodeMessage> verifyCodePublisher,  // 消息发布者
    IFusionCache fusionCache,                                 // 缓存服务
    ILogger<EmailVerifyController> logger                     // 日志服务
) : ControllerBase

常量和配置

  • _emailVerifyEnabled: 从配置中读取邮箱验证服务是否开启
  • VerifyCodeCachePrefix: 验证码缓存键前缀
  • SendTimeCachePrefix: 发送时间缓存键前缀
  • SendIntervalSeconds: 发送间隔限制(60秒)
  • VerifyCodeExpiration: 验证码有效期(30分钟)
  • VerifyPassedExpiration: 验证通过后免验证时长(3天)

主要功能接口

1. 获取服务状态 GET /api/emailverify/status

[HttpGet("status")]
public ActionResult<ResponseResult<bool>> GetStatus()

返回邮箱验证服务是否启用的状态。

2. 检查是否需要验证 GET /api/emailverify/check-need-verify

[HttpGet("check-need-verify")]
public async Task<ActionResult<ResponseResult<bool>>> CheckNeedVerify(string? email = null)

逻辑:

  • 如果用户已认证,返回 false(不需要验证)
  • 如果验证服务未启用,返回 false
  • 如果邮箱为空,返回 true(需要验证)
  • 检查缓存中该邮箱是否已通过验证,如果已验证则返回 false

3. 发送验证码 POST /api/emailverify/send

[HttpPost("send"), EnableRateLimiting("comment")]

功能流程:

  1. 检查邮箱验证服务是否开启
  2. 验证邮箱地址不为空
  3. 检查发送冷却时间(60秒限制)
  4. 生成6位随机数字验证码
  5. 通过消息队列发布验证码发送消息
  6. 记录发送时间到缓存
  7. 返回验证码有效期(30分钟)

4. 验证验证码 POST /api/emailverify/verify

[HttpPost("verify")]

功能流程:

  1. 检查服务状态和参数有效性
  2. 从缓存中获取该邮箱的验证码
  3. 比对验证码是否正确
  4. 验证成功后删除验证码(确保一次性使用)
  5. 设置验证通过状态到缓存(3天内免验证)

请求/响应模型

SendVerifyCodeRequest

public class SendVerifyCodeRequest
{
    public required string Email { get; set; }  // 邮箱地址
}

VerifyCodeRequest

public class VerifyCodeRequest
{
    public required string Email { get; set; }  // 邮箱地址
    public required string Code { get; set; }   // 验证码
}

关键特性

  1. 限流控制: 使用 [EnableRateLimiting("comment")] 防止恶意频繁请求
  2. 缓存机制: 使用 FusionCache 存储验证码、发送时间和验证状态
  3. 消息队列: 通过消息发布者异步发送邮件,解耦发送逻辑
  4. 安全机制:
    • 验证码一次性使用
    • 发送间隔限制
    • 验证码过期机制
  5. 用户体验: 验证通过后3天内免验证

这个控制器设计得比较完善,考虑了安全性、性能和用户体验等多个方面。

评论加载中...