using System.Net;
using Dpz.Core.EnumLibrary;
using Dpz.Core.MessageQueue.Abstractions;
using Dpz.Core.Public.ViewModel.Messages;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.RateLimiting;
namespace Dpz.Core.Web.Controllers;
public class CommentController(
ICommentService commentService,
IMessagePublisher<SendMasterEmailMessage> masterEmailPublisher,
IMessagePublisher<SendReplyEmailMessage> replyEmailPublisher,
IConfiguration configuration,
IFusionCache fusionCache,
ILogger<CommentController> logger,
IHttpCurrentUserService currentUserService
) : Controller
{
private readonly bool _emailVerifyEnabled =
configuration.GetSection("EmailVerifyServiceOpen").Get<bool?>() ?? true;
[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 (_emailVerifyEnabled)
{
if (string.IsNullOrWhiteSpace(comment.Email))
{
return Json(ResultInfo.ToFail("邮箱不能为空"));
}
var verified = await fusionCache.GetOrDefaultAsync<bool>(
Request.BuildEmailVerifyPassedCacheKey(comment.Email)
);
if (!verified)
{
return Json(ResultInfo.ToFail("请先完成邮箱验证后再提交评论"));
}
}
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);
// 发布邮件消息
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, "评论邮件消息发送失败");
}
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 = await currentUserService.GetRequiredUserInfoAsync();
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))
{
try
{
await PublishReplyEmailMessageAsync(
comment.ReplyId,
comment.User?.Name ?? "匿名用户",
comment.CommentText,
null
);
}
catch (Exception e)
{
logger.LogError(e, "评论邮件消息发送失败");
}
}
ViewData["node"] = comment.Node;
ViewData["relation"] = comment.Relation;
return PartialView("_CommentListPartial", model);
}
[NonAction]
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 MVC 的评论控制器(CommentController),用于处理网站的评论功能。以下是详细的代码分析:
📋 整体架构
依赖注入的服务
控制器通过构造函数注入了以下服务:
ICommentService:评论业务逻辑服务IMessagePublisher:消息队列发布器(用于发送邮件通知)IConfiguration:配置服务IFusionCache:缓存服务ILogger:日志服务IHttpCurrentUserService:当前用户服务
🔧 核心功能
1. Index 方法 - 获取评论列表
[HttpGet("Comment/{node}/{relation}")]
- 功能:分页加载评论列表
- 参数:
node:评论节点(如文章、产品等)relation:关联ID(URL编码)pageIndex/pageSize:分页参数
- 返回:部分视图(PartialView)和评论总数(通过响应头)
2. Page 方法 - 分页加载
[HttpGet("Comment/Page/{node}/{relation}")]
- 与
Index方法类似,但返回的是_CommentListPartial部分视图 - 用于异步加载更多评论
3. Publish 方法 - 发布评论(访客)
[HttpPost, ValidateAntiForgeryToken, EnableRateLimiting("comment")]
核心流程:
a. 邮箱验证检查
if (_emailVerifyEnabled) {
var verified = await fusionCache.GetOrDefaultAsync<bool>(
Request.BuildEmailVerifyPassedCacheKey(comment.Email)
);
}
- 如果启用邮箱验证,检查缓存中是否存在已验证的标记
b. 模型验证
if (!ModelState.IsValid) {
// 提取所有错误消息并返回
}
c. 发布评论
- 调用
commentService.PublishCommentAsync保存评论 - 设置 Cookie(昵称、邮箱、网站)以便下次自动填充
d. 发送邮件通知
await masterEmailPublisher.PublishAsync(...); // 通知站长
await PublishReplyEmailMessageAsync(...); // 通知被回复者
4. SetCookie 方法 - Cookie管理
[NonAction]
private void SetCookie(string nickname, string email, string? site)
- 目的:记住用户的评论信息(昵称、邮箱、网站)
- 特性:
HttpOnly:防止 JavaScript 访问Secure:仅 HTTPS 传输IsEssential:GDPR 豁免- 有效期:1年
5. Send 方法 - 发布评论(会员)
[HttpPost, ValidateAntiForgeryToken, CheckAuthorize]
- 与
Publish方法类似,但用于已登录用户 - 从
currentUserService获取用户信息 - 不需要邮箱验证
6. PublishReplyEmailMessageAsync - 发送回复通知
[NonAction]
private Task PublishReplyEmailMessageAsync(...)
- 封装了发送回复邮件的逻辑
- 将消息发布到消息队列
🔒 安全特性
| 特性 | 说明 |
|---|---|
ValidateAntiForgeryToken | 防止 CSRF 攻击 |
EnableRateLimiting("comment") | 限制评论频率 |
CheckAuthorize | 验证用户登录状态 |
| URL解码 | 防止 URL 编码注入 |
| Cookie安全设置 | HttpOnly + Secure |
📊 数据流
用户提交评论
↓
验证邮箱(访客)/ 验证登录(会员)
↓
模型验证
↓
保存到数据库
↓
发送邮件消息到队列
↓
设置Cookie(访客)
↓
返回更新后的评论列表
💡 设计亮点
- 邮箱验证可配置:通过配置文件控制是否启用
- 异步消息队列:邮件发送不阻塞主流程
- 错误容错:邮件发送失败只记录日志,不影响评论发布
- 缓存优化:使用 FusionCache 存储邮箱验证状态
- 用户体验:自动保存评论者信息到 Cookie
🎯 适用场景
- 博客评论系统
- 产品评价功能
- 论坛回复功能
- 任何需要用户互动的场景
AI 正在分析代码…
评论加载中...