CommentController.cs 14 KB

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