CommentController.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. using CacheManager.Core;
  2. using Hangfire;
  3. using Masuit.MyBlogs.Core.Common;
  4. using Masuit.MyBlogs.Core.Common.Mails;
  5. using Masuit.MyBlogs.Core.Extensions;
  6. using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
  7. using Masuit.MyBlogs.Core.Models.Command;
  8. using Masuit.MyBlogs.Core.Models.DTO;
  9. using Masuit.MyBlogs.Core.Models.Entity;
  10. using Masuit.MyBlogs.Core.Models.Enum;
  11. using Masuit.MyBlogs.Core.Models.ViewModel;
  12. using Masuit.Tools;
  13. using Masuit.Tools.Core.Net;
  14. using Masuit.Tools.Html;
  15. using Masuit.Tools.Logging;
  16. using Masuit.Tools.Models;
  17. using Masuit.Tools.Strings;
  18. using Microsoft.AspNetCore.Hosting;
  19. using Microsoft.AspNetCore.Http;
  20. using Microsoft.AspNetCore.Mvc;
  21. using Microsoft.Net.Http.Headers;
  22. using System;
  23. using System.Collections.Generic;
  24. using System.ComponentModel.DataAnnotations;
  25. using System.IO;
  26. using System.Linq;
  27. using System.Text.RegularExpressions;
  28. using System.Threading.Tasks;
  29. using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
  30. namespace Masuit.MyBlogs.Core.Controllers
  31. {
  32. /// <summary>
  33. /// 评论管理
  34. /// </summary>
  35. public class CommentController : BaseController
  36. {
  37. public ICommentService CommentService { get; set; }
  38. public IPostService PostService { get; set; }
  39. public IWebHostEnvironment HostEnvironment { get; set; }
  40. public ICacheManager<int> CommentFeq { get; set; }
  41. /// <summary>
  42. /// 发表评论
  43. /// </summary>
  44. /// <param name="messageService"></param>
  45. /// <param name="mailSender"></param>
  46. /// <param name="dto"></param>
  47. /// <returns></returns>
  48. [HttpPost, ValidateAntiForgeryToken]
  49. public async Task<ActionResult> Submit([FromServices] IInternalMessageService messageService, [FromServices] IMailSender mailSender, CommentCommand dto)
  50. {
  51. var match = Regex.Match(dto.NickName + dto.Content.RemoveHtmlTag(), CommonHelper.BanRegex);
  52. if (match.Success)
  53. {
  54. LogManager.Info($"提交内容:{dto.NickName}/{dto.Content},敏感词:{match.Value}");
  55. return ResultData(null, false, "您提交的内容包含敏感词,被禁止发表,请检查您的内容后尝试重新提交!");
  56. }
  57. var error = await ValidateEmailCode(mailSender, dto.Email, dto.Code);
  58. if (!string.IsNullOrEmpty(error))
  59. {
  60. return ResultData(null, false, error);
  61. }
  62. Post post = await PostService.GetByIdAsync(dto.PostId) ?? throw new NotFoundException("评论失败,文章未找到");
  63. if (post.DisableComment)
  64. {
  65. return ResultData(null, false, "本文已禁用评论功能,不允许任何人回复!");
  66. }
  67. dto.Content = dto.Content.Trim().Replace("<p><br></p>", string.Empty);
  68. if (CommentFeq.GetOrAdd("Comments:" + ClientIP, 1) > 2)
  69. {
  70. CommentFeq.Expire("Comments:" + ClientIP, TimeSpan.FromMinutes(1));
  71. return ResultData(null, false, "您的发言频率过快,请稍后再发表吧!");
  72. }
  73. var comment = dto.Mapper<Comment>();
  74. if (Regex.Match(dto.NickName + dto.Content, CommonHelper.ModRegex).Length <= 0)
  75. {
  76. comment.Status = Status.Published;
  77. }
  78. comment.CommentDate = DateTime.Now;
  79. var user = HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
  80. if (user != null)
  81. {
  82. comment.NickName = user.NickName;
  83. comment.Email = user.Email;
  84. if (user.IsAdmin)
  85. {
  86. comment.Status = Status.Published;
  87. comment.IsMaster = true;
  88. }
  89. }
  90. comment.Content = dto.Content.HtmlSantinizerStandard().ClearImgAttributes();
  91. comment.Browser = dto.Browser ?? Request.Headers[HeaderNames.UserAgent];
  92. comment.IP = ClientIP;
  93. comment.Location = Request.Location();
  94. comment = CommentService.AddEntitySaved(comment);
  95. if (comment == null)
  96. {
  97. return ResultData(null, false, "评论失败");
  98. }
  99. Response.Cookies.Append("NickName", comment.NickName, new CookieOptions()
  100. {
  101. Expires = DateTimeOffset.Now.AddYears(1),
  102. SameSite = SameSiteMode.Lax
  103. });
  104. WriteEmailKeyCookie(dto.Email);
  105. CommentFeq.AddOrUpdate("Comments:" + ClientIP, 1, i => i + 1, 5);
  106. CommentFeq.Expire("Comments:" + ClientIP, TimeSpan.FromMinutes(1));
  107. var emails = new HashSet<string>();
  108. var email = CommonHelper.SystemSettings["ReceiveEmail"]; //站长邮箱
  109. emails.Add(email);
  110. var content = new Template(await System.IO.File.ReadAllTextAsync(HostEnvironment.WebRootPath + "/template/notify.html"))
  111. .Set("title", post.Title)
  112. .Set("time", DateTime.Now.ToTimeZoneF(HttpContext.Session.Get<string>(SessionKey.TimeZone)))
  113. .Set("nickname", comment.NickName)
  114. .Set("content", comment.Content);
  115. if (comment.Status == Status.Published)
  116. {
  117. if (!comment.IsMaster)
  118. {
  119. await messageService.AddEntitySavedAsync(new InternalMessage()
  120. {
  121. Title = $"来自【{comment.NickName}】在文章《{post.Title}》的新评论",
  122. Content = comment.Content,
  123. Link = Url.Action("Details", "Post", new { id = comment.PostId, cid = comment.Id }) + "#comment"
  124. });
  125. }
  126. if (comment.ParentId == 0)
  127. {
  128. emails.Add(post.Email);
  129. emails.Add(post.ModifierEmail);
  130. //新评论,只通知博主和楼主
  131. foreach (var s in emails)
  132. {
  133. BackgroundJob.Enqueue(() => CommonHelper.SendMail(Request.Host + "|博客文章新评论:", content.Set("link", Url.Action("Details", "Post", new { id = comment.PostId, cid = comment.Id }, Request.Scheme) + "#comment").Render(false), s, ClientIP));
  134. }
  135. }
  136. else
  137. {
  138. //通知博主和上层所有关联的评论访客
  139. var parent = await CommentService.GetByIdAsync(comment.ParentId);
  140. emails.AddRange(parent.Root().Flatten().Select(c => c.Email).ToArray());
  141. emails.AddRange(post.Email, post.ModifierEmail);
  142. emails.Remove(comment.Email);
  143. string link = Url.Action("Details", "Post", new { id = comment.PostId, cid = comment.Id }, Request.Scheme) + "#comment";
  144. foreach (var s in emails)
  145. {
  146. BackgroundJob.Enqueue(() => CommonHelper.SendMail($"{Request.Host}{CommonHelper.SystemSettings["Title"]}文章评论回复:", content.Set("link", link).Render(false), s, ClientIP));
  147. }
  148. }
  149. return ResultData(null, true, "评论发表成功,服务器正在后台处理中,这会有一定的延迟,稍后将显示到评论列表中");
  150. }
  151. foreach (var s in emails)
  152. {
  153. BackgroundJob.Enqueue(() => CommonHelper.SendMail(Request.Host + "|博客文章新评论(待审核):", content.Set("link", Url.Action("Details", "Post", new { id = comment.PostId, cid = comment.Id }, Request.Scheme) + "#comment").Render(false) + "<p style='color:red;'>(待审核)</p>", s, ClientIP));
  154. }
  155. return ResultData(null, true, "评论成功,待站长审核通过以后将显示");
  156. }
  157. /// <summary>
  158. /// 评论投票
  159. /// </summary>
  160. /// <param name="id"></param>
  161. /// <returns></returns>
  162. [HttpPost]
  163. public async Task<ActionResult> CommentVote(int id)
  164. {
  165. if (HttpContext.Session.Get("cm" + id) != null)
  166. {
  167. return ResultData(null, false, "您刚才已经投过票了,感谢您的参与!");
  168. }
  169. var cm = await CommentService.GetAsync(c => c.Id == id && c.Status == Status.Published) ?? throw new NotFoundException("评论不存在!");
  170. cm.VoteCount++;
  171. bool b = await CommentService.SaveChangesAsync() > 0;
  172. if (b)
  173. {
  174. HttpContext.Session.Set("cm" + id, id.GetBytes());
  175. }
  176. return ResultData(null, b, b ? "投票成功" : "投票失败");
  177. }
  178. /// <summary>
  179. /// 获取评论
  180. /// </summary>
  181. /// <param name="id"></param>
  182. /// <param name="page"></param>
  183. /// <param name="size"></param>
  184. /// <param name="cid"></param>
  185. /// <returns></returns>
  186. public async Task<ActionResult> GetComments(int? id, [Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15, int cid = 0)
  187. {
  188. if (cid != 0)
  189. {
  190. var comment = await CommentService.GetByIdAsync(cid) ?? throw new NotFoundException("评论未找到");
  191. var single = new[] { comment.Root() };
  192. foreach (var c in single.Flatten())
  193. {
  194. c.CommentDate = c.CommentDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
  195. }
  196. return ResultData(new
  197. {
  198. total = 1,
  199. parentTotal = 1,
  200. page,
  201. size,
  202. rows = single.Mapper<IList<CommentViewModel>>()
  203. });
  204. }
  205. var parent = await CommentService.GetPagesAsync(page, size, c => c.PostId == id && c.ParentId == 0 && (c.Status == Status.Published || CurrentUser.IsAdmin), c => c.CommentDate, false);
  206. if (!parent.Data.Any())
  207. {
  208. return ResultData(null, false, "没有评论");
  209. }
  210. int total = parent.TotalCount; //总条数,用于前台分页
  211. parent.Data.Flatten().ForEach(c =>
  212. {
  213. c.CommentDate = c.CommentDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
  214. });
  215. if (total > 0)
  216. {
  217. return ResultData(new
  218. {
  219. total,
  220. parentTotal = total,
  221. page,
  222. size,
  223. rows = parent.Data.Mapper<IList<CommentViewModel>>()
  224. });
  225. }
  226. return ResultData(null, false, "没有评论");
  227. }
  228. /// <summary>
  229. /// 审核评论
  230. /// </summary>
  231. /// <param name="id"></param>
  232. /// <returns></returns>
  233. [MyAuthorize]
  234. public async Task<ActionResult> Pass(int id)
  235. {
  236. Comment comment = await CommentService.GetByIdAsync(id) ?? throw new NotFoundException("评论不存在!");
  237. comment.Status = Status.Published;
  238. Post post = await PostService.GetByIdAsync(comment.PostId);
  239. bool b = await CommentService.SaveChangesAsync() > 0;
  240. if (b)
  241. {
  242. var content = new Template(await System.IO.File.ReadAllTextAsync(Path.Combine(HostEnvironment.WebRootPath, "template", "notify.html")))
  243. .Set("title", post.Title)
  244. .Set("time", DateTime.Now.ToTimeZoneF(HttpContext.Session.Get<string>(SessionKey.TimeZone)))
  245. .Set("nickname", comment.NickName)
  246. .Set("content", comment.Content);
  247. var root = comment.Root();
  248. var emails = root.Flatten().Select(c => c.Email).Append(post.ModifierEmail).Except(new List<string> { comment.Email, CurrentUser.Email }).ToHashSet();
  249. var link = Url.Action("Details", "Post", new
  250. {
  251. id = comment.PostId,
  252. cid = root.Id
  253. }, Request.Scheme) + "#comment";
  254. foreach (var email in emails)
  255. {
  256. BackgroundJob.Enqueue(() => CommonHelper.SendMail($"{Request.Host}{CommonHelper.SystemSettings["Title"]}文章评论回复:", content.Set("link", link).Render(false), email, ClientIP));
  257. }
  258. return ResultData(null, true, "审核通过!");
  259. }
  260. return ResultData(null, false, "审核失败!");
  261. }
  262. /// <summary>
  263. /// 删除评论
  264. /// </summary>
  265. /// <param name="id"></param>
  266. /// <returns></returns>
  267. [MyAuthorize]
  268. public ActionResult Delete(int id)
  269. {
  270. var b = CommentService.DeleteByIdSaved(id);
  271. return ResultData(null, b, b ? "删除成功!" : "删除失败!");
  272. }
  273. /// <summary>
  274. /// 获取未审核的评论
  275. /// </summary>
  276. /// <returns></returns>
  277. [MyAuthorize]
  278. public async Task<ActionResult> GetPendingComments([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
  279. {
  280. var pages = await CommentService.GetPagesAsync<DateTime, CommentDto>(page, size, c => c.Status == Status.Pending, c => c.CommentDate, false);
  281. foreach (var item in pages.Data)
  282. {
  283. item.CommentDate = item.CommentDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
  284. }
  285. return Ok(pages);
  286. }
  287. }
  288. }