using System.Net;
using Dpz.Core.EnumLibrary;
using Dpz.Core.Hangfire;
using Hangfire;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.RateLimiting;
namespace Dpz.Core.Web.Controllers;
public class CommentController(ICommentService commentService) : Controller
{
[HttpGet("Comment/{node}/{relation}")]
public async Task<IActionResult> Index(
CommentNode node,
string relation,
int pageIndex = 1,
int pageSize = 5
)
{
relation = WebUtility.UrlDecode(relation);
ViewData["node"] = node;
ViewData["relation"] = relation;
var list = await commentService.GetCommentsAsync(node, relation, pageIndex, pageSize);
var count = await commentService.GetCommentCountAsync(node, relation);
var model = new CommentPage { Page = list, Count = count };
Response.Headers.Append("CommentCount", count.ToString());
return PartialView(model);
}
[HttpGet("Comment/Page/{node}/{relation}")]
public async Task<IActionResult> Page(
CommentNode node,
string relation,
int pageIndex = 1,
int pageSize = 5
)
{
relation = WebUtility.UrlDecode(relation);
ViewData["node"] = node;
ViewData["relation"] = relation;
var list = await commentService.GetCommentsAsync(node, relation, pageIndex, pageSize);
var count = await commentService.GetCommentCountAsync(node, relation);
var model = new CommentPage { Page = list, Count = count };
Response.Headers.Append("CommentCount", count.ToString());
return PartialView("_CommentListPartial", model);
}
[HttpPost, ValidateAntiForgeryToken, EnableRateLimiting("comment")]
public async Task<IActionResult> Publish(VmPublishComment comment)
{
if (!ModelState.IsValid)
{
var errors = ModelState
.SelectMany(x => x.Value?.Errors ?? new ModelErrorCollection())
.Select(x => x.ErrorMessage)
.Where(x => !string.IsNullOrEmpty(x))
.ToArray();
return Json(new ResultInfo(string.Join("\n", errors)));
}
await commentService.PublishCommentAsync(comment);
var list = await commentService.GetCommentsAsync(comment.Node, comment.Relation, 1, 5);
var count = await commentService.GetCommentCountAsync(comment.Node, comment.Relation);
var model = new CommentPage { Page = list, Count = count };
ViewData["node"] = comment.Node;
ViewData["relation"] = comment.Relation;
SetCookie(comment.NickName, comment.Email, comment.Site);
BackgroundJob.Enqueue<EmailSenderActivator>(x => x.SendMasterAsync(comment));
if (!string.IsNullOrEmpty(comment.ReplyId))
{
BackgroundJob.Enqueue<EmailSenderActivator>(x => x.SendCommenterAsync(comment));
}
ViewData["node"] = comment.Node;
ViewData["relation"] = comment.Relation;
Response.Headers.Append("CommentCount", count.ToString());
return PartialView("_CommentListPartial", model);
}
[NonAction]
private void SetCookie(string nickname, string email, string? site)
{
var cookieNickname = Request.Cookies[nameof(nickname)];
if (
string.IsNullOrEmpty(cookieNickname)
|| WebUtility.UrlDecode(cookieNickname) != nickname
)
{
Response.Cookies.Append(
nameof(nickname),
WebUtility.UrlEncode(nickname),
new CookieOptions
{
HttpOnly = true,
Secure = true,
IsEssential = true,
Expires = DateTimeOffset.Now.AddYears(1),
}
);
}
var cookieEmail = Request.Cookies[nameof(email)];
if (string.IsNullOrEmpty(cookieEmail) || WebUtility.UrlDecode(cookieEmail) != email)
{
Response.Cookies.Append(
nameof(email),
WebUtility.UrlEncode(email),
new CookieOptions
{
HttpOnly = true,
Secure = true,
IsEssential = true,
Expires = DateTimeOffset.Now.AddYears(1),
}
);
}
var cookieSite = Request.Cookies[nameof(site)];
if (
!string.IsNullOrEmpty(site)
&& (string.IsNullOrEmpty(cookieSite) || WebUtility.UrlDecode(cookieSite) != site)
)
{
Response.Cookies.Append(
nameof(site),
WebUtility.UrlEncode(site),
new CookieOptions
{
HttpOnly = true,
Secure = true,
IsEssential = true,
Expires = DateTimeOffset.Now.AddYears(1),
}
);
}
}
[HttpPost, ValidateAntiForgeryToken, CheckAuthorize]
public async Task<IActionResult> Send(MembleComment comment)
{
if (!ModelState.IsValid)
{
var errors = ModelState
.SelectMany(x => x.Value?.Errors ?? new ModelErrorCollection())
.Select(x => x.ErrorMessage)
.Where(x => !string.IsNullOrEmpty(x))
.ToArray();
return Json(new ResultInfo(string.Join("\n", errors)));
}
comment.User = User.RequiredUserInfo;
await commentService.PublishCommentAsync(comment);
var list = await commentService.GetCommentsAsync(comment.Node, comment.Relation, 1, 5);
var count = await commentService.GetCommentCountAsync(comment.Node, comment.Relation);
var model = new CommentPage { Page = list, Count = count };
Response.Headers.Append("CommentCount", count.ToString());
if (!string.IsNullOrEmpty(comment.ReplyId))
{
BackgroundJob.Enqueue<EmailSenderActivator>(x => x.SendCommenterAsync(comment));
}
ViewData["node"] = comment.Node;
ViewData["relation"] = comment.Relation;
return PartialView("_CommentListPartial", model);
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
这段代码是一个 ASP.NET Core MVC 控制器 CommentController,用于处理评论的显示与提交。下面分模块说明其功能与要点(中文):
概览
- 控制器通过构造器注入了 ICommentService(用于读取/发布评论)。
- 使用了 Hangfire 的 BackgroundJob 来异步推送邮件(EmailSenderActivator)。
- 在一些动作上使用了防伪、频率限制和自定义授权特性(ValidateAntiForgeryToken、EnableRateLimiting("comment")、CheckAuthorize)。
路由与动作(主要接口)
- Index (GET /Comment/{node}/{relation})
- 参数:CommentNode node(枚举)、string relation、pageIndex(默认1)、pageSize(默认5)。
- 先对 relation 做 URL 解码,调用 commentService 获取指定页的评论列表和评论总数。
- 将 node/relation 放到 ViewData,构造 CommentPage(包含 Page 与 Count),把 Count 写到响应头 CommentCount,然后返回一个 PartialView(默认视图名,可能为视图目录下的 Comment/Index 部分视图)。
- Page (GET /Comment/Page/{node}/{relation})
- 与 Index 类似,但返回指定的部分视图 "_CommentListPartial"(用于分页局部刷新)。
- Publish (POST)
- 接收 VmPublishComment,带有 [ValidateAntiForgeryToken] 与限流 [EnableRateLimiting("comment")]。
- 如果 ModelState 无效,收集所有错误并以 JSON(ResultInfo) 返回错误信息。
- 否则调用 commentService.PublishCommentAsync 发布评论,然后重新拉取第一页评论与总数,构造 CommentPage。
- 设置 ViewData,调用 SetCookie 将昵称/邮箱/站点写入 Cookie(1 年,HttpOnly、Secure、IsEssential)。
- 使用 Hangfire BackgroundJob.Enqueue 异步发送邮件:总是给站长(SendMasterAsync),如果是回复则也给被回复者(SendCommenterAsync)。
- 将最新评论数放到响应头,返回局部视图 "_CommentListPartial"(用于前端更新评论列表)。
- Send (POST)
- 接收 MembleComment,带有 [ValidateAntiForgeryToken] 且需要授权 [CheckAuthorize](自定义检查)。
- 同样进行 ModelState 校验与错误返回。
- 将 comment.User 设为当前用户信息(User.RequiredUserInfo),再发布评论并拉取首页评论与计数。
- 若为回复则异步发送给被回复者的邮件(通过 Hangfire)。
- 返回局部视图 "_CommentListPartial" 与更新后的评论。
辅助方法
- SetCookie(string nickname, string email, string? site)(NonAction)
- 读取当前请求中的相应 cookie,如果不存在或与当前值不同则写入新的 cookie。
- 写入选项:HttpOnly = true、Secure = true、IsEssential = true、Expires = 1 年后,值用 URL 编码保存。
通用/实现细节与注意点
- 所有读取 relation 时都做了 WebUtility.UrlDecode,避免 URL 编码导致匹配失败。
- 使用 Response.Headers.Append("CommentCount", count.ToString()) 把评论总数返回给前端,便于前端知道总数(例如显示计数或分页)。
- 使用部分视图返回最新评论列表,便于前端做局部刷新(AJAX)。
- Publish 使用了请求速率限制(EnableRateLimiting)和防伪(ValidateAntiForgeryToken)来防止滥发与 CSRF。
- Send 要求登录/授权(CheckAuthorize),并把当前用户信息关联到评论上。
- 使用 Hangfire 异步发送邮件,避免阻塞请求/影响响应速度。
- Cookie 采用 HttpOnly + Secure,增强安全性(但这也意味着前端 JS 无法直接读取这些 cookie)。
潜在小提示/可改进点
- Publish 中对 ViewData 的设置出现了两次重复设置(无害但冗余)。
- 将用户可见的昵称/邮箱存为 HttpOnly cookie 会使前端 JS 无法读取或自动填充表单;根据需求决定是否应去掉 HttpOnly。
- 返回错误时用 ResultInfo 包装 JSON,需确保前端能正确解析并显示换行(代码中用 "\n" 连接错误消息)。
- 若 relation 或其它参数包含特殊字符,注意路由匹配与长度限制问题。
总结 该控制器负责:显示评论分页(Index、Page)、处理前端访客发布的评论(Publish,带限流与防伪)、处理已登录用户发布(Send,带授权),并在发布后通过 Hangfire 异步发送通知邮件、更新评论列表并通过部分视图返回给前端,同时把评论总数写入响应头。
评论加载中...