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(...);      // 通知被回复者

[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(访客)
  ↓
返回更新后的评论列表

💡 设计亮点

  1. 邮箱验证可配置:通过配置文件控制是否启用
  2. 异步消息队列:邮件发送不阻塞主流程
  3. 错误容错:邮件发送失败只记录日志,不影响评论发布
  4. 缓存优化:使用 FusionCache 存储邮箱验证状态
  5. 用户体验:自动保存评论者信息到 Cookie

🎯 适用场景

  • 博客评论系统
  • 产品评价功能
  • 论坛回复功能
  • 任何需要用户互动的场景
评论加载中...