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