using Dispose.Scope; using Hangfire; using Masuit.MyBlogs.Core.Common.Mails; using Masuit.MyBlogs.Core.Extensions; using Masuit.Tools.Html; using Masuit.Tools.Logging; using Microsoft.Net.Http.Headers; using System.Text; using System.Text.RegularExpressions; using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode; namespace Masuit.MyBlogs.Core.Controllers; /// /// 评论管理 /// public sealed class CommentController : BaseController { public ICommentService CommentService { get; set; } public IPostService PostService { get; set; } public IWebHostEnvironment HostEnvironment { get; set; } /// /// 发表评论 /// /// /// /// /// [HttpPost, ValidateAntiForgeryToken] public async Task Submit([FromServices] IInternalMessageService messageService, [FromServices] IEmailBlocklistService blocklistService, CommentCommand cmd) { var match = Regex.Match(cmd.NickName + cmd.Content.RemoveHtmlTag(), CommonHelper.BanRegex); if (match.Success) { LogManager.Info($"提交内容:{cmd.NickName}/{cmd.Content},敏感词:{match.Value}"); return ResultData(null, false, "您提交的内容包含敏感词,被禁止发表,请检查您的内容后尝试重新提交!"); } var error = await ValidateEmailCode(blocklistService, cmd.Email, cmd.Code); if (!string.IsNullOrEmpty(error)) { return ResultData(null, false, error); } if (cmd.ParentId > 0 && DateTime.Now - CommentService[cmd.ParentId.Value, c => c.CommentDate] > TimeSpan.FromDays(180)) { return ResultData(null, false, "当前评论过于久远,不再允许回复!"); } var post = await PostService.GetByIdAsync(cmd.PostId) ?? throw new NotFoundException("评论失败,文章未找到"); CheckPermission(post); if (post.DisableComment) { return ResultData(null, false, "本文已禁用评论功能,不允许任何人回复!"); } cmd.Content = cmd.Content.Trim().Replace("


", string.Empty); var ip = ClientIP.ToString(); if (!CurrentUser.IsAdmin) { if (await RedisHelper.SAddAsync("Comments:" + ip, cmd.Content) == 0) { await RedisHelper.ExpireAsync("Comments:" + ip, TimeSpan.FromMinutes(2)); return ResultData(null, false, "您已发表了相同的评论内容,请稍后再发表吧!"); } if (await RedisHelper.SCardAsync("Comments:" + ip) > 2) { await RedisHelper.ExpireAsync("Comments:" + ip, TimeSpan.FromMinutes(2)); return ResultData(null, false, "您的发言频率过快,请稍后再发表吧!"); } } var comment = Mapper.Map(cmd); if (cmd.ParentId > 0) { comment.GroupTag = CommentService.GetQuery(c => c.Id == cmd.ParentId).Select(c => c.GroupTag).FirstOrDefault(); comment.Path = (CommentService.GetQuery(c => c.Id == cmd.ParentId).Select(c => c.Path).FirstOrDefault() + "," + cmd.ParentId).Trim(','); } else { comment.GroupTag = SnowFlake.NewId; comment.Path = SnowFlake.NewId; } if (cmd.Email == post.Email || cmd.Email == post.ModifierEmail || Regex.Match(cmd.NickName + cmd.Content, CommonHelper.ModRegex).Length <= 0) { comment.Status = Status.Published; } comment.CommentDate = DateTime.Now; var user = HttpContext.Session.Get(SessionKey.UserInfo); if (user != null) { comment.NickName = user.NickName; comment.Email = user.Email; if (user.IsAdmin) { comment.Status = Status.Published; comment.IsMaster = true; } } comment.Content = await cmd.Content.HtmlSanitizerStandard().ClearImgAttributes(); comment.Browser = cmd.Browser ?? Request.Headers[HeaderNames.UserAgent]; comment.IP = ip; comment.Location = Request.Location(); comment = CommentService.AddEntitySaved(comment); if (comment == null) { return ResultData(null, false, "评论失败"); } Response.Cookies.Append("NickName", comment.NickName, new CookieOptions() { Expires = DateTimeOffset.Now.AddYears(1), SameSite = SameSiteMode.Lax }); WriteEmailKeyCookie(cmd.Email); await RedisHelper.ExpireAsync("Comments:" + comment.IP, TimeSpan.FromMinutes(1)); var emails = new HashSet(); var email = CommonHelper.SystemSettings["ReceiveEmail"]; //站长邮箱 emails.Add(email); var content = new Template(await new FileInfo(HostEnvironment.WebRootPath + "/template/notify.html").ShareReadWrite().ReadAllTextAsync(Encoding.UTF8)) .Set("title", post.Title) .Set("time", DateTime.Now.ToTimeZoneF(HttpContext.Session.Get(SessionKey.TimeZone))) .Set("nickname", comment.NickName) .Set("content", comment.Content); Response.Cookies.Append("Comment_" + post.Id, "1", new CookieOptions() { Expires = DateTimeOffset.Now.AddDays(2), SameSite = SameSiteMode.Lax, MaxAge = TimeSpan.FromDays(2), Secure = true }); if (comment.Status == Status.Published) { if (!comment.IsMaster) { await messageService.AddEntitySavedAsync(new InternalMessage() { Title = $"来自【{comment.NickName}】在文章《{post.Title}》的新评论", Content = comment.Content, Link = Url.Action("Details", "Post", new { id = comment.PostId, cid = comment.Id }) + "#comment" }); } if (comment.ParentId == null) { emails.Add(post.Email); emails.Add(post.ModifierEmail); //新评论,只通知博主和楼主 foreach (var s in emails) { BackgroundJob.Enqueue(sender => sender.Send(Request.Host + "|博客文章新评论:", content.Set("link", Url.Action("Details", "Post", new { id = comment.PostId, cid = comment.Id }, Request.Scheme) + "#comment").Render(false), s, comment.IP)); } } else { //通知博主和所有关联的评论访客 emails.AddRange(await CommentService.GetQuery(c => c.GroupTag == comment.GroupTag).Select(c => c.Email).Distinct().ToArrayAsync()); emails.AddRange(post.Email, post.ModifierEmail); emails.Remove(comment.Email); string link = Url.Action("Details", "Post", new { id = comment.PostId, cid = comment.Id }, Request.Scheme) + "#comment"; foreach (var s in emails) { BackgroundJob.Enqueue(sender => sender.Send($"{Request.Host}{CommonHelper.SystemSettings["Title"]}文章评论回复:", content.Set("link", link).Render(false), s, comment.IP)); } } return ResultData(null, true, "评论发表成功,服务器正在后台处理中,这会有一定的延迟,稍后将显示到评论列表中"); } foreach (var s in emails) { BackgroundJob.Enqueue(sender => sender.Send(Request.Host + "|博客文章新评论(待审核):", content.Set("link", Url.Action("Details", "Post", new { id = comment.PostId, cid = comment.Id }, Request.Scheme) + "#comment").Render(false) + "

(待审核)

", s, comment.IP)); } return ResultData(null, true, "评论成功,待审核通过以后显示"); } /// /// 评论投票 /// /// /// [HttpPost] public async Task CommentVote(int id) { if (HttpContext.Session.Get("cm" + id) != null) { return ResultData(null, false, "您刚才已经投过票了,感谢您的参与!"); } var cm = await CommentService.GetAsync(c => c.Id == id && c.Status == Status.Published) ?? throw new NotFoundException("评论不存在!"); cm.VoteCount++; bool b = await CommentService.SaveChangesAsync() > 0; if (b) { HttpContext.Session.Set("cm" + id, id.GetBytes()); } return ResultData(null, b, b ? "投票成功" : "投票失败"); } /// /// 获取评论 /// /// /// /// /// /// public async Task GetComments(int? id, [Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15, int? cid = null) { if (cid > 0) { var comment = await CommentService.GetByIdAsync(cid.Value) ?? throw new NotFoundException("评论未找到"); var layer = CommentService.GetQueryNoTracking(c => c.GroupTag == comment.GroupTag).ToPooledListScope(); foreach (var c in layer) { c.CommentDate = c.CommentDate.ToTimeZone(HttpContext.Session.Get(SessionKey.TimeZone)); c.IsAuthor = c.Email == comment.Post.Email || c.Email == comment.Post.ModifierEmail; if (!CurrentUser.IsAdmin) { c.Email = null; c.IP = null; c.Location = null; } } return ResultData(new { total = 1, parentTotal = 1, page, size, rows = Mapper.Map>(layer.ToTree(c => c.Id, c => c.ParentId)) }); } var parent = await CommentService.GetPagesAsync(page, size, c => c.PostId == id && c.ParentId == null && (c.Status == Status.Published || CurrentUser.IsAdmin), c => c.CommentDate, false); if (!parent.Data.Any()) { return ResultData(null, false, "没有评论"); } int total = parent.TotalCount; //总条数,用于前台分页 var tags = parent.Data.Select(c => c.GroupTag).ToArray(); var comments = CommentService.GetQuery(c => tags.Contains(c.GroupTag)).Include(c => c.Post).AsNoTracking().ToPooledListScope(); comments.ForEach(c => { c.CommentDate = c.CommentDate.ToTimeZone(HttpContext.Session.Get(SessionKey.TimeZone)); c.IsAuthor = c.Email == c.Post.Email || c.Email == c.Post.ModifierEmail; if (!CurrentUser.IsAdmin) { c.Email = null; c.IP = null; c.Location = null; } }); if (total > 0) { return ResultData(new { total, parentTotal = total, page, size, rows = Mapper.Map>(comments.OrderByDescending(c => c.CommentDate).ToTree(c => c.Id, c => c.ParentId)) }); } return ResultData(null, false, "没有评论"); } /// /// 审核评论 /// /// /// [MyAuthorize] public async Task Pass(int id) { var comment = await CommentService.GetByIdAsync(id) ?? throw new NotFoundException("评论不存在!"); comment.Status = Status.Published; Post post = await PostService.GetByIdAsync(comment.PostId); bool b = await CommentService.SaveChangesAsync() > 0; if (b) { var content = new Template(await new FileInfo(Path.Combine(HostEnvironment.WebRootPath, "template", "notify.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8)) .Set("title", post.Title) .Set("time", DateTime.Now.ToTimeZoneF(HttpContext.Session.Get(SessionKey.TimeZone))) .Set("nickname", comment.NickName) .Set("content", comment.Content); var emails = CommentService.GetQuery(c => c.GroupTag == comment.GroupTag).Select(c => c.Email).Distinct().AsEnumerable().Append(post.ModifierEmail).Except(new List { comment.Email, CurrentUser.Email }).ToPooledSetScope(); var link = Url.Action("Details", "Post", new { id = comment.PostId, cid = id }, Request.Scheme) + "#comment"; foreach (var email in emails) { BackgroundJob.Enqueue(sender => sender.Send($"{Request.Host}{CommonHelper.SystemSettings["Title"]}文章评论回复:", content.Set("link", link).Render(false), email, ClientIP.ToString())); } return ResultData(null, true, "审核通过!"); } return ResultData(null, false, "审核失败!"); } /// /// 删除评论 /// /// /// [MyAuthorize] public ActionResult Delete(int id) { var b = CommentService.DeleteById(id); return ResultData(null, b, b ? "删除成功!" : "删除失败!"); } /// /// 获取未审核的评论 /// /// [MyAuthorize] public async Task GetPendingComments([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15) { var pages = await CommentService.GetPagesAsync(page, size, c => c.Status == Status.Pending, c => c.CommentDate, false); foreach (var item in pages.Data) { item.CommentDate = item.CommentDate.ToTimeZone(HttpContext.Session.Get(SessionKey.TimeZone)); } return Ok(pages); } }