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)。

路由与动作(主要接口)

  1. 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 部分视图)。
  1. Page (GET /Comment/Page/{node}/{relation})
  • 与 Index 类似,但返回指定的部分视图 "_CommentListPartial"(用于分页局部刷新)。
  1. 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"(用于前端更新评论列表)。
  1. 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 异步发送通知邮件、更新评论列表并通过部分视图返回给前端,同时把评论总数写入响应头。

评论加载中...