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

namespace Dpz.Core.WebApi.Controllers;

/// <summary>
/// 评论
/// </summary>
[ApiController, Route("api/[controller]")]
public class CommentController(
    ICommentService commentService,
    IMessagePublisher<SendMasterEmailMessage> masterEmailPublisher,
    IMessagePublisher<SendReplyEmailMessage> replyEmailPublisher,
    IConfiguration configuration,
    IFusionCache fusionCache,
    ILogger<CommentController> logger
) : ControllerBase
{
    private readonly bool _emailVerifyEnabled =
        configuration.GetSection("EmailVerifyServiceOpen").Get<bool?>() ?? false;

    /// <summary>
    /// 获取评论
    /// </summary>
    /// <returns></returns>
    [HttpGet, Authorize(Policy = "System")]
    [ProducesResponseType<List<VmCommentFlat>>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> GetComments([FromQuery] CommentQueryDto query)
    {
        var list = await commentService.GetPageAsync(
            query.Node,
            query.Relation,
            query.PageIndex,
            query.PageSize
        );
        list.AddPaginationMetadata(Response.Headers);
        return Ok(list);
    }

    /// <summary>
    /// 评论分页信息
    /// </summary>
    /// <param name="query"></param>
    /// <returns></returns>
    [HttpGet("page")]
    [ProducesResponseType<List<CommentViewModel>>(StatusCodes.Status200OK)]
    public async Task<IActionResult> GetCommentPage([FromQuery] CommentPageQuery query)
    {
        var list = await commentService.GetCommentsAsync(
            query.Node,
            query.Relation,
            query.PageIndex,
            query.PageSize
        );
        list.AddPaginationMetadata(Response.Headers);
        return Ok(list);
    }

    /// <summary>
    /// 匿名发送评论
    /// </summary>
    /// <param name="comment"></param>
    /// <param name="pageSize"></param>
    /// <returns></returns>
    [HttpPost, EnableRateLimiting("comment")]
    [ProducesResponseType<List<CommentViewModel>>(StatusCodes.Status200OK)]
    public async Task<IActionResult> SendComment(
        [FromBody] VmPublishComment comment,
        [FromQuery] int pageSize = 5
    )
    {
        if (pageSize > 100)
        {
            pageSize = 100;
        }

        if (_emailVerifyEnabled)
        {
            if (string.IsNullOrWhiteSpace(comment.Email))
            {
                return BadRequest(new { message = "邮箱不能为空" });
            }

            var verified = await fusionCache.GetOrDefaultAsync<bool>(
                Request.BuildEmailVerifyPassedCacheKey(comment.Email)
            );

            if (!verified)
            {
                return BadRequest(new { message = "请先完成邮箱验证后再提交评论" });
            }
        }

        try
        {
            await commentService.PublishCommentAsync(comment);
        }
        catch (Exception e)
        {
            return BadRequest(e.Message);
        }

        // 发布邮件消息
        try
        {
            // 发送给管理员
            await masterEmailPublisher.PublishAsync(
                new SendMasterEmailMessage
                {
                    NickName = comment.NickName,
                    Email = comment.Email,
                    CommentText = comment.CommentText,
                    SendTime = comment.SendTime,
                    Source = nameof(CommentController),
                }
            );

            // 如果是回复,发送给被回复者
            if (!string.IsNullOrEmpty(comment.ReplyId))
            {
                await PublishReplyEmailMessageAsync(
                    comment.ReplyId,
                    comment.NickName,
                    comment.CommentText,
                    comment.Email
                );
            }
        }
        catch (Exception e)
        {
            logger.LogError(e, "评论邮件消息发送失败");
        }

        var list = await commentService.GetCommentsAsync(
            comment.Node,
            comment.Relation,
            1,
            pageSize
        );
        list.AddPaginationMetadata(Response.Headers);
        return Ok(list);
    }

    /// <summary>
    /// 文章关联
    /// </summary>
    /// <returns></returns>
    [HttpGet("relation/article"), Authorize(Policy = "System")]
    public async Task<ActionResult<List<CommentRelationResponse>>> GetArticleRelationTitle()
    {
        var dic = await commentService.HasCommentForArticleAsync();
        return Ok(dic);
    }

    /// <summary>
    /// 源码关联
    /// </summary>
    /// <returns></returns>
    [HttpGet("relation/code"), Authorize(Policy = "System")]
    public async Task<ActionResult<List<CommentRelationResponse>>> CodeRelation()
    {
        var dic = await commentService.CodeRelationAsync();
        return Ok(dic);
    }

    /// <summary>
    /// 其他关联
    /// </summary>
    /// <returns></returns>
    [HttpGet("relation/other"), Authorize(Policy = "System")]
    public async Task<ActionResult<List<CommentRelationResponse>>> OtherRelation()
    {
        var dic = await commentService.OtherRelationAsync();
        return Ok(dic);
    }

    /// <summary>
    /// 逻辑删除
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [HttpDelete("{id}"), Authorize(Policy = "System")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> Delete(string id)
    {
        await commentService.ClearAsync(id);
        return NoContent();
    }

    /// <summary>
    /// 物理删除
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [HttpDelete("{id}/physical"), Authorize(Policy = "System")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> PhysicalDelete(string id)
    {
        await commentService.DeleteAsync(id);
        return NoContent();
    }

    private Task PublishReplyEmailMessageAsync(
        string targetCommentId,
        string replierNickname,
        string replyText,
        string? replierEmail
    ) =>
        replyEmailPublisher.PublishAsync(
            new SendReplyEmailMessage
            {
                TargetCommentId = targetCommentId,
                ReplierNickname = replierNickname,
                ReplierEmail = replierEmail,
                ReplyText = replyText,
                Source = nameof(CommentController),
            }
        );
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

下面是对这段 ASP.NET Core 控制器代码(CommentController)功能的逐条解释与要点说明,便于你快速理解各个路由、行为和实现细节。

总体

  • 控制器路由为 api/comment([Route("api/[controller]")])。
  • 通过构造函数注入 ICommentService 和 ILogger
  • 使用了授权、速率限制和 Hangfire 后台任务等机制。
  • 主要职责:查询评论、分页获取评论、发布(匿名)评论、获取与评论相关的实体列表(文章/源码/其它)、逻辑删除和物理删除评论。

各接口说明

  1. GET api/comment
  • 方法名:GetComments
  • 授权:需要 Authorize(Policy = "System")(只有满足 System 策略的用户可调用)。
  • 输入:从查询字符串接收 CommentQueryDto(包含 Node、Relation、PageIndex、PageSize 等)。
  • 行为:调用 commentService.GetPageAsync(...) 获取分页扁平化评论(VmCommentFlat 列表);通过 list.AddPaginationMetadata(Response.Headers) 把分页元数据放到响应头。
  • 返回:200 OK + 列表;401 Unauthorized(如果未授权)。
  1. GET api/comment/page
  • 方法名:GetCommentPage
  • 授权:没有标注 Authorize(通常是公开接口)。
  • 输入:CommentPageQuery(Node、Relation、PageIndex、PageSize)。
  • 行为:调用 commentService.GetCommentsAsync(...) 获取 CommentViewModel 列表;同样添加分页元数据到响应头。
  • 返回:200 OK + 列表。
  1. POST api/comment
  • 方法名:SendComment
  • 功能:匿名发送/发布评论(前端提交评论)。
  • 限流:[EnableRateLimiting("comment")](使用名为 "comment" 的速率限制策略)。
  • 输入:请求体 VmPublishComment;可选查询参数 pageSize(默认 5,最大 100)。
  • 行为流程:
    1. 限幅 pageSize(>100 时设置为 100)。
    2. 调用 commentService.PublishCommentAsync(comment) 发布评论;如果抛异常,返回 400 BadRequest,内容为异常消息。
    3. 发送邮件通知(通过 Hangfire 后台任务):
      • BackgroundJob.Enqueue(x => x.SendMasterAsync(comment));
      • 若 comment.ReplyId 不为空,则 enqueue SendCommenterAsync 通知被回复者。
      • 邮件发送异常只记录日志(logger.LogError),不影响主流程。
    4. 发布成功后,重新读取第一页评论(pageIndex = 1,pageSize 为前面限制后的值),并把分页信息放到响应头,然后返回 200 OK + 列表。
  • 返回:200 OK + 最新评论页;如果发布失败返回 400。
  1. GET api/comment/relation/article
  • 方法名:GetArticleRelationTitle
  • 授权:Require System 策略。
  • 功能:获取与文章关联并有评论的文章列表(返回 List),内部调用 commentService.HasCommentForArticleAsync()。
  • 返回:200 OK + 列表。
  1. GET api/comment/relation/code
  • 方法名:CodeRelation
  • 授权:Require System 策略。
  • 功能:获取与源码(code)相关的评论关联列表,调用 commentService.CodeRelationAsync()。
  • 返回:200 OK + 列表。
  1. GET api/comment/relation/other
  • 方法名:OtherRelation
  • 授权:Require System 策略。
  • 功能:获取其它类型的评论关联(调用 commentService.OtherRelationAsync())。
  • 返回:200 OK + 列表。
  1. DELETE api/comment/{id}
  • 方法名:Delete
  • 授权:Require System 策略。
  • 功能:逻辑删除(软删)评论,调用 commentService.ClearAsync(id)。
  • 返回:204 No Content(成功),或 401 Unauthorized(未授权)。
  1. DELETE api/comment/{id}/physical
  • 方法名:PhysicalDelete
  • 授权:Require System 策略。
  • 功能:物理删除评论(真正从存储删除),调用 commentService.DeleteAsync(id)。
  • 返回:204 No Content(成功)。

实现细节与注意点

  • AddPaginationMetadata(Response.Headers):代码中使用了一个扩展方法把分页信息放到响应头中(通常是总页数、总条数、当前页等),便于前端处理分页。
  • 异常处理:
    • 发布评论时(PublishCommentAsync)若抛异常直接返回 BadRequest 并包含异常消息。
    • 邮件发送异常不会阻塞请求:使用 Hangfire Enqueue 将发送工作推入后台队列,并在捕获异常时记录日志。
  • Hangfire:使用 BackgroundJob.Enqueue 来创建火并行任务,EmailSenderActivator 应该是一个包含 SendMasterAsync/SendCommenterAsync 的服务/激活器,用于发送邮件通知。
  • 授权与公开接口:部分管理/统计接口受 System 策略保护(仅管理员或系统用户),而评论分页获取(GetCommentPage)和发布评论(SendComment)通常是公开的(SendComment 通过速率限制保护滥用)。
  • Rate limiting:SendComment 被标注了 EnableRateLimiting("comment"),需要在应用中配置名为 "comment" 的速率限制策略(例如每个IP每分钟多少次)。
  • 参数验证与安全:示例中没有对输入的 DTO 做显式模型验证(例如 ModelState),但若启用了 ApiController 特性,框架会自动进行模型验证并返回 400。发布时直接把异常消息返回给客户端,需注意不要泄露敏感信息。

总结 该控制器实现了评论的常见 CRUD(查询、分页、发布、逻辑/物理删除)和关联信息查询,同时使用授权策略保护管理接口、用速率限制保护发布接口、并通过 Hangfire 将邮件通知异步化以提升响应性能。

评论加载中...