Browse Source

升级包

懒得勤快 2 years ago
parent
commit
3ccef6c07a

+ 287 - 287
src/Masuit.MyBlogs.Core/Controllers/CommentController.cs

@@ -22,318 +22,318 @@ namespace Masuit.MyBlogs.Core.Controllers;
 /// </summary>
 public sealed class CommentController : BaseController
 {
-	public ICommentService CommentService { get; set; }
+    public ICommentService CommentService { get; set; }
 
-	public IPostService PostService { get; set; }
+    public IPostService PostService { get; set; }
 
-	public IWebHostEnvironment HostEnvironment { get; set; }
+    public IWebHostEnvironment HostEnvironment { get; set; }
 
-	public ICacheManager<int> CommentFeq { get; set; }
+    public ICacheManager<int> CommentFeq { get; set; }
 
-	/// <summary>
-	/// 发表评论
-	/// </summary>
-	/// <param name="messageService"></param>
-	/// <param name="mailSender"></param>
-	/// <param name="cmd"></param>
-	/// <returns></returns>
-	[HttpPost, ValidateAntiForgeryToken]
-	public async Task<ActionResult> Submit([FromServices] IInternalMessageService messageService, [FromServices] IMailSender mailSender, 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 = ValidateEmailCode(mailSender, cmd.Email, cmd.Code);
-		if (!string.IsNullOrEmpty(error))
-		{
-			return ResultData(null, false, error);
-		}
+    /// <summary>
+    /// 发表评论
+    /// </summary>
+    /// <param name="messageService"></param>
+    /// <param name="mailSender"></param>
+    /// <param name="cmd"></param>
+    /// <returns></returns>
+    [HttpPost, ValidateAntiForgeryToken]
+    public async Task<ActionResult> Submit([FromServices] IInternalMessageService messageService, [FromServices] IMailSender mailSender, 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 = ValidateEmailCode(mailSender, 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, "当前评论过于久远,不再允许回复!");
-		}
+        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, "本文已禁用评论功能,不允许任何人回复!");
-		}
+        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("<p><br></p>", string.Empty);
-		var ip = ClientIP.ToString();
-		if (CommentFeq.GetOrAdd("Comments:" + ip, 1) > 2)
-		{
-			CommentFeq.Expire("Comments:" + ip, TimeSpan.FromMinutes(1));
-			return ResultData(null, false, "您的发言频率过快,请稍后再发表吧!");
-		}
+        cmd.Content = cmd.Content.Trim().Replace("<p><br></p>", string.Empty);
+        var ip = ClientIP.ToString();
+        if (CommentFeq.GetOrAdd("Comments:" + ip, 1) > 2)
+        {
+            CommentFeq.Expire("Comments:" + ip, TimeSpan.FromMinutes(1));
+            return ResultData(null, false, "您的发言频率过快,请稍后再发表吧!");
+        }
 
-		var comment = Mapper.Map<Comment>(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;
-		}
+        var comment = Mapper.Map<Comment>(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;
-		}
+        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<UserInfoDto>(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.HtmlSantinizerStandard().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, "评论失败");
-		}
+        comment.CommentDate = DateTime.Now;
+        var user = HttpContext.Session.Get<UserInfoDto>(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);
-		CommentFeq.AddOrUpdate("Comments:" + comment.IP, 1, i => i + 1, 5);
-		CommentFeq.Expire("Comments:" + comment.IP, TimeSpan.FromMinutes(1));
-		var emails = new HashSet<string>();
-		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<string>(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);
+        Response.Cookies.Append("NickName", comment.NickName, new CookieOptions()
+        {
+            Expires = DateTimeOffset.Now.AddYears(1),
+            SameSite = SameSiteMode.Lax
+        });
+        WriteEmailKeyCookie(cmd.Email);
+        CommentFeq.AddOrUpdate("Comments:" + comment.IP, 1, i => i + 1, 5);
+        CommentFeq.Expire("Comments:" + comment.IP, TimeSpan.FromMinutes(1));
+        var emails = new HashSet<string>();
+        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<string>(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<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));
-				}
-			}
-			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<IMailSender>(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<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));
+                }
+            }
+            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<IMailSender>(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<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));
-		}
+        foreach (var s in emails)
+        {
+            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));
+        }
 
-		return ResultData(null, true, "评论成功,待站长审核通过以后将显示");
-	}
+        return ResultData(null, true, "评论成功,待站长审核通过以后将显示");
+    }
 
-	/// <summary>
-	/// 评论投票
-	/// </summary>
-	/// <param name="id"></param>
-	/// <returns></returns>
-	[HttpPost]
-	public async Task<ActionResult> CommentVote(int id)
-	{
-		if (HttpContext.Session.Get("cm" + id) != null)
-		{
-			return ResultData(null, false, "您刚才已经投过票了,感谢您的参与!");
-		}
+    /// <summary>
+    /// 评论投票
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpPost]
+    public async Task<ActionResult> 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());
-		}
+        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 ? "投票成功" : "投票失败");
-	}
+        return ResultData(null, b, b ? "投票成功" : "投票失败");
+    }
 
-	/// <summary>
-	/// 获取评论
-	/// </summary>
-	/// <param name="id"></param>
-	/// <param name="page"></param>
-	/// <param name="size"></param>
-	/// <param name="cid"></param>
-	/// <returns></returns>
-	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)
-	{
-		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<string>(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;
-				}
-			}
+    /// <summary>
+    /// 获取评论
+    /// </summary>
+    /// <param name="id"></param>
+    /// <param name="page"></param>
+    /// <param name="size"></param>
+    /// <param name="cid"></param>
+    /// <returns></returns>
+    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)
+    {
+        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<string>(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<IList<CommentViewModel>>(layer.ToTree(c => c.Id, c => c.ParentId))
-			});
-		}
+            return ResultData(new
+            {
+                total = 1,
+                parentTotal = 1,
+                page,
+                size,
+                rows = Mapper.Map<IList<CommentViewModel>>(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<string>(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<IList<CommentViewModel>>(comments.OrderByDescending(c => c.CommentDate).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<string>(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<IList<CommentViewModel>>(comments.OrderByDescending(c => c.CommentDate).ToTree(c => c.Id, c => c.ParentId))
+            });
+        }
 
-		return ResultData(null, false, "没有评论");
-	}
+        return ResultData(null, false, "没有评论");
+    }
 
-	/// <summary>
-	/// 审核评论
-	/// </summary>
-	/// <param name="id"></param>
-	/// <returns></returns>
-	[MyAuthorize]
-	public async Task<ActionResult> 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<string>(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<string> { 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<IMailSender>(sender => sender.Send($"{Request.Host}{CommonHelper.SystemSettings["Title"]}文章评论回复:", content.Set("link", link).Render(false), email, ClientIP.ToString()));
-			}
+    /// <summary>
+    /// 审核评论
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [MyAuthorize]
+    public async Task<ActionResult> 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<string>(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<string> { 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<IMailSender>(sender => sender.Send($"{Request.Host}{CommonHelper.SystemSettings["Title"]}文章评论回复:", content.Set("link", link).Render(false), email, ClientIP.ToString()));
+            }
 
-			return ResultData(null, true, "审核通过!");
-		}
+            return ResultData(null, true, "审核通过!");
+        }
 
-		return ResultData(null, false, "审核失败!");
-	}
+        return ResultData(null, false, "审核失败!");
+    }
 
-	/// <summary>
-	/// 删除评论
-	/// </summary>
-	/// <param name="id"></param>
-	/// <returns></returns>
-	[MyAuthorize]
-	public ActionResult Delete(int id)
-	{
-		var b = CommentService.DeleteById(id);
-		return ResultData(null, b, b ? "删除成功!" : "删除失败!");
-	}
+    /// <summary>
+    /// 删除评论
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [MyAuthorize]
+    public ActionResult Delete(int id)
+    {
+        var b = CommentService.DeleteById(id);
+        return ResultData(null, b, b ? "删除成功!" : "删除失败!");
+    }
 
-	/// <summary>
-	/// 获取未审核的评论
-	/// </summary>
-	/// <returns></returns>
-	[MyAuthorize]
-	public async Task<ActionResult> GetPendingComments([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
-	{
-		var pages = await CommentService.GetPagesAsync<DateTime, CommentDto>(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<string>(SessionKey.TimeZone));
-		}
+    /// <summary>
+    /// 获取未审核的评论
+    /// </summary>
+    /// <returns></returns>
+    [MyAuthorize]
+    public async Task<ActionResult> GetPendingComments([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
+    {
+        var pages = await CommentService.GetPagesAsync<DateTime, CommentDto>(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<string>(SessionKey.TimeZone));
+        }
 
-		return Ok(pages);
-	}
+        return Ok(pages);
+    }
 }

+ 354 - 354
src/Masuit.MyBlogs.Core/Controllers/MsgController.cs

@@ -22,357 +22,357 @@ namespace Masuit.MyBlogs.Core.Controllers;
 /// </summary>
 public sealed class MsgController : BaseController
 {
-	/// <summary>
-	/// 留言
-	/// </summary>
-	public ILeaveMessageService LeaveMessageService { get; set; }
-
-	/// <summary>
-	/// 站内信
-	/// </summary>
-	public IInternalMessageService MessageService { get; set; }
-
-	public IWebHostEnvironment HostEnvironment { get; set; }
-
-	public ICacheManager<int> MsgFeq { get; set; }
-
-	/// <summary>
-	/// 留言板
-	/// </summary>
-	/// <returns></returns>
-	[Route("msg"), Route("msg/{cid:int}"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
-	public async Task<ActionResult> Index()
-	{
-		ViewBag.TotalCount = LeaveMessageService.Count(m => m.ParentId == null && m.Status == Status.Published);
-		var text = await new FileInfo(Path.Combine(HostEnvironment.WebRootPath, "template", "agreement.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8);
-		return CurrentUser.IsAdmin ? View("Index_Admin", text) : View(model: text);
-	}
-
-	/// <summary>
-	/// 获取留言
-	/// </summary>
-	/// <param name="page"></param>
-	/// <param name="size"></param>
-	/// <param name="cid"></param>
-	/// <returns></returns>
-	public async Task<ActionResult> GetMsgs([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 message = await LeaveMessageService.GetByIdAsync(cid.Value) ?? throw new NotFoundException("留言未找到");
-			var layer = LeaveMessageService.GetQueryNoTracking(e => e.GroupTag == message.GroupTag).ToPooledListScope();
-			foreach (var m in layer)
-			{
-				m.PostDate = m.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-				if (!CurrentUser.IsAdmin)
-				{
-					m.Email = null;
-					m.IP = null;
-					m.Location = null;
-				}
-			}
-
-			return ResultData(new
-			{
-				total = 1,
-				parentTotal = 1,
-				page,
-				size,
-				rows = Mapper.Map<IList<LeaveMessageViewModel>>(layer.ToTree(e => e.Id, e => e.ParentId))
-			});
-		}
-
-		var parent = await LeaveMessageService.GetPagesAsync(page, size, m => m.ParentId == null && (m.Status == Status.Published || CurrentUser.IsAdmin), m => m.PostDate, false);
-		if (!parent.Data.Any())
-		{
-			return ResultData(null, false, "没有留言");
-		}
-		var total = parent.TotalCount;
-		var tags = parent.Data.Select(c => c.GroupTag).ToArray();
-		var messages = LeaveMessageService.GetQueryNoTracking(c => tags.Contains(c.GroupTag)).ToPooledListScope();
-		messages.ForEach(m =>
-		{
-			m.PostDate = m.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-			if (!CurrentUser.IsAdmin)
-			{
-				m.Email = null;
-				m.IP = null;
-				m.Location = null;
-			}
-		});
-		if (total > 0)
-		{
-			return ResultData(new
-			{
-				total,
-				parentTotal = total,
-				page,
-				size,
-				rows = Mapper.Map<IList<LeaveMessageViewModel>>(messages.OrderByDescending(c => c.PostDate).ToTree(c => c.Id, c => c.ParentId))
-			});
-		}
-
-		return ResultData(null, false, "没有留言");
-	}
-
-	/// <summary>
-	/// 发表留言
-	/// </summary>
-	/// <param name="mailSender"></param>
-	/// <param name="cmd"></param>
-	/// <returns></returns>
-	[HttpPost, ValidateAntiForgeryToken]
-	public async Task<ActionResult> Submit([FromServices] IMailSender mailSender, LeaveMessageCommand 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 = ValidateEmailCode(mailSender, cmd.Email, cmd.Code);
-		if (!string.IsNullOrEmpty(error))
-		{
-			return ResultData(null, false, error);
-		}
-
-		if (cmd.ParentId > 0 && DateTime.Now - LeaveMessageService[cmd.ParentId.Value, m => m.PostDate] > TimeSpan.FromDays(180))
-		{
-			return ResultData(null, false, "当前留言过于久远,不再允许回复!");
-		}
-
-		cmd.Content = cmd.Content.Trim().Replace("<p><br></p>", string.Empty);
-		var ip = ClientIP.ToString();
-		if (MsgFeq.GetOrAdd("Comments:" + ip, 1) > 2)
-		{
-			MsgFeq.Expire("Comments:" + ip, TimeSpan.FromMinutes(1));
-			return ResultData(null, false, "您的发言频率过快,请稍后再发表吧!");
-		}
-
-		var msg = Mapper.Map<LeaveMessage>(cmd);
-		if (cmd.ParentId > 0)
-		{
-			msg.GroupTag = LeaveMessageService.GetQuery(c => c.Id == cmd.ParentId).Select(c => c.GroupTag).FirstOrDefault();
-			msg.Path = (LeaveMessageService.GetQuery(c => c.Id == cmd.ParentId).Select(c => c.Path).FirstOrDefault() + "," + cmd.ParentId).Trim(',');
-		}
-		else
-		{
-			msg.GroupTag = SnowFlake.NewId;
-			msg.Path = SnowFlake.NewId;
-		}
-
-		if (Regex.Match(cmd.NickName + cmd.Content, CommonHelper.ModRegex).Length <= 0)
-		{
-			msg.Status = Status.Published;
-		}
-
-		msg.PostDate = DateTime.Now;
-		var user = HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
-		if (user != null)
-		{
-			msg.NickName = user.NickName;
-			msg.Email = user.Email;
-			if (user.IsAdmin)
-			{
-				msg.Status = Status.Published;
-				msg.IsMaster = true;
-			}
-		}
-
-		msg.Content = await cmd.Content.HtmlSantinizerStandard().ClearImgAttributes();
-		msg.Browser = cmd.Browser ?? Request.Headers[HeaderNames.UserAgent];
-		msg.IP = ip;
-		msg.Location = Request.Location();
-		msg = LeaveMessageService.AddEntitySaved(msg);
-		if (msg == null)
-		{
-			return ResultData(null, false, "留言发表失败!");
-		}
-
-		Response.Cookies.Append("NickName", msg.NickName, new CookieOptions()
-		{
-			Expires = DateTimeOffset.Now.AddYears(1),
-			SameSite = SameSiteMode.Lax
-		});
-		WriteEmailKeyCookie(cmd.Email);
-		MsgFeq.AddOrUpdate("Comments:" + ip, 1, i => i + 1, 5);
-		MsgFeq.Expire("Comments:" + ip, TimeSpan.FromMinutes(1));
-		var email = CommonHelper.SystemSettings["ReceiveEmail"];
-		var content = new Template(await new FileInfo(HostEnvironment.WebRootPath + "/template/notify.html").ShareReadWrite().ReadAllTextAsync(Encoding.UTF8)).Set("title", "网站留言板").Set("time", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")).Set("nickname", msg.NickName).Set("content", msg.Content);
-		if (msg.Status == Status.Published)
-		{
-			if (!msg.IsMaster)
-			{
-				await MessageService.AddEntitySavedAsync(new InternalMessage()
-				{
-					Title = $"来自【{msg.NickName}】的新留言",
-					Content = msg.Content,
-					Link = Url.Action("Index", "Msg", new { cid = msg.Id })
-				});
-			}
-			if (msg.ParentId == null)
-			{
-				//新评论,只通知博主
-				BackgroundJob.Enqueue<IMailSender>(sender => sender.Send(Request.Host + "|博客新留言:", content.Set("link", Url.Action("Index", "Msg", new { cid = msg.Id }, Request.Scheme)).Render(false), email, ip));
-			}
-			else
-			{
-				//通知博主和上层所有关联的评论访客
-				var emails = LeaveMessageService.GetQuery(e => e.GroupTag == msg.GroupTag).Select(c => c.Email).Distinct().AsEnumerable().Append(email).Except(new[] { msg.Email }).ToHashSet();
-				string link = Url.Action("Index", "Msg", new { cid = msg.Id }, Request.Scheme);
-				foreach (var s in emails)
-				{
-					BackgroundJob.Enqueue<IMailSender>(sender => sender.Send($"{Request.Host}{CommonHelper.SystemSettings["Title"]} 留言回复:", content.Set("link", link).Render(false), s, ip));
-				}
-			}
-			return ResultData(null, true, "留言发表成功,服务器正在后台处理中,这会有一定的延迟,稍后将会显示到列表中!");
-		}
-
-		BackgroundJob.Enqueue<IMailSender>(sender => sender.Send(Request.Host + "|博客新留言(待审核):", content.Set("link", Url.Action("Index", "Msg", new
-		{
-			cid = msg.Id
-		}, Request.Scheme)).Render(false) + "<p style='color:red;'>(待审核)</p>", email, ip));
-		return ResultData(null, true, "留言发表成功,待站长审核通过以后将显示到列表中!");
-	}
-
-	/// <summary>
-	/// 审核
-	/// </summary>
-	/// <param name="id"></param>
-	/// <returns></returns>
-	[MyAuthorize]
-	public async Task<ActionResult> Pass(int id)
-	{
-		var msg = await LeaveMessageService.GetByIdAsync(id);
-		msg.Status = Status.Published;
-		bool b = await LeaveMessageService.SaveChangesAsync() > 0;
-		if (b)
-		{
-			var content = new Template(await new FileInfo(Path.Combine(HostEnvironment.WebRootPath, "template", "notify.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8)).Set("time", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")).Set("nickname", msg.NickName).Set("content", msg.Content);
-			var emails = LeaveMessageService.GetQuery(m => m.GroupTag == msg.GroupTag).Select(m => m.Email).Distinct().AsEnumerable().Except(new List<string> { msg.Email, CurrentUser.Email }).ToPooledSetScope();
-			var link = Url.Action("Index", "Msg", new { cid = id }, Request.Scheme);
-			foreach (var s in emails)
-			{
-				BackgroundJob.Enqueue<IMailSender>(sender => sender.Send($"{Request.Host}{CommonHelper.SystemSettings["Title"]} 留言回复:", content.Set("link", link).Render(false), s, ClientIP.ToString()));
-			}
-		}
-
-		return ResultData(null, b, b ? "审核通过!" : "审核失败!");
-	}
-
-	/// <summary>
-	/// 删除留言
-	/// </summary>
-	/// <param name="id"></param>
-	/// <returns></returns>
-	[MyAuthorize]
-	public ActionResult Delete(int id)
-	{
-		var b = LeaveMessageService.DeleteById(id);
-		return ResultData(null, b, b ? "删除成功!" : "删除失败!");
-	}
-
-	/// <summary>
-	/// 获取待审核的留言
-	/// </summary>
-	/// <returns></returns>
-	[MyAuthorize]
-	public async Task<ActionResult> GetPendingMsgs([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
-	{
-		var list = await LeaveMessageService.GetPagesAsync<DateTime, LeaveMessageDto>(page, size, m => m.Status == Status.Pending, l => l.PostDate, false);
-		foreach (var m in list.Data)
-		{
-			m.PostDate = m.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-		}
-
-		return Ok(list);
-	}
-
-	#region 站内消息
-
-	/// <summary>
-	/// 已读站内信
-	/// </summary>
-	/// <param name="id"></param>
-	/// <returns></returns>
-	[MyAuthorize]
-	public async Task<ActionResult> Read(int id)
-	{
-		await MessageService.GetQuery(m => m.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Read, true));
-		return Content("ok");
-	}
-
-	/// <summary>
-	/// 标记为未读
-	/// </summary>
-	/// <param name="id"></param>
-	/// <returns></returns>
-	[MyAuthorize]
-	public async Task<ActionResult> Unread(int id)
-	{
-		await MessageService.GetQuery(m => m.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Read, false));
-		return Content("ok");
-	}
-
-	/// <summary>
-	/// 标记为已读
-	/// </summary>
-	/// <param name="id"></param>
-	/// <returns></returns>
-	[MyAuthorize]
-	public async Task<ActionResult> MarkRead(int id)
-	{
-		await MessageService.GetQuery(m => m.Id <= id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Read, true));
-		return ResultData(null);
-	}
-
-	/// <summary>
-	/// 删除站内信
-	/// </summary>
-	/// <param name="id"></param>
-	/// <returns></returns>
-	[MyAuthorize]
-	public async Task<ActionResult> DeleteMsg(int id)
-	{
-		bool b = await MessageService.DeleteByIdAsync(id) > 0;
-		return ResultData(null, b, b ? "站内消息删除成功!" : "站内消息删除失败!");
-	}
-
-	/// <summary>
-	/// 获取站内信
-	/// </summary>
-	/// <param name="page"></param>
-	/// <param name="size"></param>
-	/// <returns></returns>
-	[MyAuthorize]
-	public ActionResult GetInternalMsgs([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
-	{
-		var msgs = MessageService.GetPagesNoTracking(page, size, m => true, m => m.Time, false);
-		return Ok(msgs);
-	}
-
-	/// <summary>
-	/// 获取未读消息
-	/// </summary>
-	/// <returns></returns>
-	[MyAuthorize]
-	public ActionResult GetUnreadMsgs()
-	{
-		var msgs = MessageService.GetQueryNoTracking(m => !m.Read, m => m.Time, false).ToPooledListScope();
-		return ResultData(msgs);
-	}
-
-	/// <summary>
-	/// 清除站内信
-	/// </summary>
-	/// <returns></returns>
-	[MyAuthorize]
-	public async Task<ActionResult> ClearMsgs()
-	{
-		await MessageService.DeleteEntitySavedAsync(m => m.Read);
-		return ResultData(null, true, "站内消息清除成功!");
-	}
-
-	#endregion 站内消息
-}
+    /// <summary>
+    /// 留言
+    /// </summary>
+    public ILeaveMessageService LeaveMessageService { get; set; }
+
+    /// <summary>
+    /// 站内信
+    /// </summary>
+    public IInternalMessageService MessageService { get; set; }
+
+    public IWebHostEnvironment HostEnvironment { get; set; }
+
+    public ICacheManager<int> MsgFeq { get; set; }
+
+    /// <summary>
+    /// 留言板
+    /// </summary>
+    /// <returns></returns>
+    [Route("msg"), Route("msg/{cid:int}"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
+    public async Task<ActionResult> Index()
+    {
+        ViewBag.TotalCount = LeaveMessageService.Count(m => m.ParentId == null && m.Status == Status.Published);
+        var text = await new FileInfo(Path.Combine(HostEnvironment.WebRootPath, "template", "agreement.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8);
+        return CurrentUser.IsAdmin ? View("Index_Admin", text) : View(model: text);
+    }
+
+    /// <summary>
+    /// 获取留言
+    /// </summary>
+    /// <param name="page"></param>
+    /// <param name="size"></param>
+    /// <param name="cid"></param>
+    /// <returns></returns>
+    public async Task<ActionResult> GetMsgs([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 message = await LeaveMessageService.GetByIdAsync(cid.Value) ?? throw new NotFoundException("留言未找到");
+            var layer = LeaveMessageService.GetQueryNoTracking(e => e.GroupTag == message.GroupTag).ToPooledListScope();
+            foreach (var m in layer)
+            {
+                m.PostDate = m.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+                if (!CurrentUser.IsAdmin)
+                {
+                    m.Email = null;
+                    m.IP = null;
+                    m.Location = null;
+                }
+            }
+
+            return ResultData(new
+            {
+                total = 1,
+                parentTotal = 1,
+                page,
+                size,
+                rows = Mapper.Map<IList<LeaveMessageViewModel>>(layer.ToTree(e => e.Id, e => e.ParentId))
+            });
+        }
+
+        var parent = await LeaveMessageService.GetPagesAsync(page, size, m => m.ParentId == null && (m.Status == Status.Published || CurrentUser.IsAdmin), m => m.PostDate, false);
+        if (!parent.Data.Any())
+        {
+            return ResultData(null, false, "没有留言");
+        }
+        var total = parent.TotalCount;
+        var tags = parent.Data.Select(c => c.GroupTag).ToArray();
+        var messages = LeaveMessageService.GetQueryNoTracking(c => tags.Contains(c.GroupTag)).ToPooledListScope();
+        messages.ForEach(m =>
+        {
+            m.PostDate = m.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+            if (!CurrentUser.IsAdmin)
+            {
+                m.Email = null;
+                m.IP = null;
+                m.Location = null;
+            }
+        });
+        if (total > 0)
+        {
+            return ResultData(new
+            {
+                total,
+                parentTotal = total,
+                page,
+                size,
+                rows = Mapper.Map<IList<LeaveMessageViewModel>>(messages.OrderByDescending(c => c.PostDate).ToTree(c => c.Id, c => c.ParentId))
+            });
+        }
+
+        return ResultData(null, false, "没有留言");
+    }
+
+    /// <summary>
+    /// 发表留言
+    /// </summary>
+    /// <param name="mailSender"></param>
+    /// <param name="cmd"></param>
+    /// <returns></returns>
+    [HttpPost, ValidateAntiForgeryToken]
+    public async Task<ActionResult> Submit([FromServices] IMailSender mailSender, LeaveMessageCommand 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 = ValidateEmailCode(mailSender, cmd.Email, cmd.Code);
+        if (!string.IsNullOrEmpty(error))
+        {
+            return ResultData(null, false, error);
+        }
+
+        if (cmd.ParentId > 0 && DateTime.Now - LeaveMessageService[cmd.ParentId.Value, m => m.PostDate] > TimeSpan.FromDays(180))
+        {
+            return ResultData(null, false, "当前留言过于久远,不再允许回复!");
+        }
+
+        cmd.Content = cmd.Content.Trim().Replace("<p><br></p>", string.Empty);
+        var ip = ClientIP.ToString();
+        if (MsgFeq.GetOrAdd("Comments:" + ip, 1) > 2)
+        {
+            MsgFeq.Expire("Comments:" + ip, TimeSpan.FromMinutes(1));
+            return ResultData(null, false, "您的发言频率过快,请稍后再发表吧!");
+        }
+
+        var msg = Mapper.Map<LeaveMessage>(cmd);
+        if (cmd.ParentId > 0)
+        {
+            msg.GroupTag = LeaveMessageService.GetQuery(c => c.Id == cmd.ParentId).Select(c => c.GroupTag).FirstOrDefault();
+            msg.Path = (LeaveMessageService.GetQuery(c => c.Id == cmd.ParentId).Select(c => c.Path).FirstOrDefault() + "," + cmd.ParentId).Trim(',');
+        }
+        else
+        {
+            msg.GroupTag = SnowFlake.NewId;
+            msg.Path = SnowFlake.NewId;
+        }
+
+        if (Regex.Match(cmd.NickName + cmd.Content, CommonHelper.ModRegex).Length <= 0)
+        {
+            msg.Status = Status.Published;
+        }
+
+        msg.PostDate = DateTime.Now;
+        var user = HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
+        if (user != null)
+        {
+            msg.NickName = user.NickName;
+            msg.Email = user.Email;
+            if (user.IsAdmin)
+            {
+                msg.Status = Status.Published;
+                msg.IsMaster = true;
+            }
+        }
+
+        msg.Content = await cmd.Content.HtmlSanitizerStandard().ClearImgAttributes();
+        msg.Browser = cmd.Browser ?? Request.Headers[HeaderNames.UserAgent];
+        msg.IP = ip;
+        msg.Location = Request.Location();
+        msg = LeaveMessageService.AddEntitySaved(msg);
+        if (msg == null)
+        {
+            return ResultData(null, false, "留言发表失败!");
+        }
+
+        Response.Cookies.Append("NickName", msg.NickName, new CookieOptions()
+        {
+            Expires = DateTimeOffset.Now.AddYears(1),
+            SameSite = SameSiteMode.Lax
+        });
+        WriteEmailKeyCookie(cmd.Email);
+        MsgFeq.AddOrUpdate("Comments:" + ip, 1, i => i + 1, 5);
+        MsgFeq.Expire("Comments:" + ip, TimeSpan.FromMinutes(1));
+        var email = CommonHelper.SystemSettings["ReceiveEmail"];
+        var content = new Template(await new FileInfo(HostEnvironment.WebRootPath + "/template/notify.html").ShareReadWrite().ReadAllTextAsync(Encoding.UTF8)).Set("title", "网站留言板").Set("time", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")).Set("nickname", msg.NickName).Set("content", msg.Content);
+        if (msg.Status == Status.Published)
+        {
+            if (!msg.IsMaster)
+            {
+                await MessageService.AddEntitySavedAsync(new InternalMessage()
+                {
+                    Title = $"来自【{msg.NickName}】的新留言",
+                    Content = msg.Content,
+                    Link = Url.Action("Index", "Msg", new { cid = msg.Id })
+                });
+            }
+            if (msg.ParentId == null)
+            {
+                //新评论,只通知博主
+                BackgroundJob.Enqueue<IMailSender>(sender => sender.Send(Request.Host + "|博客新留言:", content.Set("link", Url.Action("Index", "Msg", new { cid = msg.Id }, Request.Scheme)).Render(false), email, ip));
+            }
+            else
+            {
+                //通知博主和上层所有关联的评论访客
+                var emails = LeaveMessageService.GetQuery(e => e.GroupTag == msg.GroupTag).Select(c => c.Email).Distinct().AsEnumerable().Append(email).Except(new[] { msg.Email }).ToHashSet();
+                string link = Url.Action("Index", "Msg", new { cid = msg.Id }, Request.Scheme);
+                foreach (var s in emails)
+                {
+                    BackgroundJob.Enqueue<IMailSender>(sender => sender.Send($"{Request.Host}{CommonHelper.SystemSettings["Title"]} 留言回复:", content.Set("link", link).Render(false), s, ip));
+                }
+            }
+            return ResultData(null, true, "留言发表成功,服务器正在后台处理中,这会有一定的延迟,稍后将会显示到列表中!");
+        }
+
+        BackgroundJob.Enqueue<IMailSender>(sender => sender.Send(Request.Host + "|博客新留言(待审核):", content.Set("link", Url.Action("Index", "Msg", new
+        {
+            cid = msg.Id
+        }, Request.Scheme)).Render(false) + "<p style='color:red;'>(待审核)</p>", email, ip));
+        return ResultData(null, true, "留言发表成功,待站长审核通过以后将显示到列表中!");
+    }
+
+    /// <summary>
+    /// 审核
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [MyAuthorize]
+    public async Task<ActionResult> Pass(int id)
+    {
+        var msg = await LeaveMessageService.GetByIdAsync(id);
+        msg.Status = Status.Published;
+        bool b = await LeaveMessageService.SaveChangesAsync() > 0;
+        if (b)
+        {
+            var content = new Template(await new FileInfo(Path.Combine(HostEnvironment.WebRootPath, "template", "notify.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8)).Set("time", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")).Set("nickname", msg.NickName).Set("content", msg.Content);
+            var emails = LeaveMessageService.GetQuery(m => m.GroupTag == msg.GroupTag).Select(m => m.Email).Distinct().AsEnumerable().Except(new List<string> { msg.Email, CurrentUser.Email }).ToPooledSetScope();
+            var link = Url.Action("Index", "Msg", new { cid = id }, Request.Scheme);
+            foreach (var s in emails)
+            {
+                BackgroundJob.Enqueue<IMailSender>(sender => sender.Send($"{Request.Host}{CommonHelper.SystemSettings["Title"]} 留言回复:", content.Set("link", link).Render(false), s, ClientIP.ToString()));
+            }
+        }
+
+        return ResultData(null, b, b ? "审核通过!" : "审核失败!");
+    }
+
+    /// <summary>
+    /// 删除留言
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [MyAuthorize]
+    public ActionResult Delete(int id)
+    {
+        var b = LeaveMessageService.DeleteById(id);
+        return ResultData(null, b, b ? "删除成功!" : "删除失败!");
+    }
+
+    /// <summary>
+    /// 获取待审核的留言
+    /// </summary>
+    /// <returns></returns>
+    [MyAuthorize]
+    public async Task<ActionResult> GetPendingMsgs([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
+    {
+        var list = await LeaveMessageService.GetPagesAsync<DateTime, LeaveMessageDto>(page, size, m => m.Status == Status.Pending, l => l.PostDate, false);
+        foreach (var m in list.Data)
+        {
+            m.PostDate = m.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+        }
+
+        return Ok(list);
+    }
+
+    #region 站内消息
+
+    /// <summary>
+    /// 已读站内信
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [MyAuthorize]
+    public async Task<ActionResult> Read(int id)
+    {
+        await MessageService.GetQuery(m => m.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Read, true));
+        return Content("ok");
+    }
+
+    /// <summary>
+    /// 标记为未读
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [MyAuthorize]
+    public async Task<ActionResult> Unread(int id)
+    {
+        await MessageService.GetQuery(m => m.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Read, false));
+        return Content("ok");
+    }
+
+    /// <summary>
+    /// 标记为已读
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [MyAuthorize]
+    public async Task<ActionResult> MarkRead(int id)
+    {
+        await MessageService.GetQuery(m => m.Id <= id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Read, true));
+        return ResultData(null);
+    }
+
+    /// <summary>
+    /// 删除站内信
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [MyAuthorize]
+    public async Task<ActionResult> DeleteMsg(int id)
+    {
+        bool b = await MessageService.DeleteByIdAsync(id) > 0;
+        return ResultData(null, b, b ? "站内消息删除成功!" : "站内消息删除失败!");
+    }
+
+    /// <summary>
+    /// 获取站内信
+    /// </summary>
+    /// <param name="page"></param>
+    /// <param name="size"></param>
+    /// <returns></returns>
+    [MyAuthorize]
+    public ActionResult GetInternalMsgs([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
+    {
+        var msgs = MessageService.GetPagesNoTracking(page, size, m => true, m => m.Time, false);
+        return Ok(msgs);
+    }
+
+    /// <summary>
+    /// 获取未读消息
+    /// </summary>
+    /// <returns></returns>
+    [MyAuthorize]
+    public ActionResult GetUnreadMsgs()
+    {
+        var msgs = MessageService.GetQueryNoTracking(m => !m.Read, m => m.Time, false).ToPooledListScope();
+        return ResultData(msgs);
+    }
+
+    /// <summary>
+    /// 清除站内信
+    /// </summary>
+    /// <returns></returns>
+    [MyAuthorize]
+    public async Task<ActionResult> ClearMsgs()
+    {
+        await MessageService.DeleteEntitySavedAsync(m => m.Read);
+        return ResultData(null, true, "站内消息清除成功!");
+    }
+
+    #endregion 站内消息
+}

+ 2 - 2
src/Masuit.MyBlogs.Core/Controllers/PostController.cs

@@ -66,7 +66,7 @@ public sealed class PostController : BaseController
         var notRobot = !Request.IsRobot();
         if (string.IsNullOrEmpty(t) && notRobot)
         {
-            return RedirectToAction("Details", cid > 0 ? new { id, kw, cid, t = SnowFlake.NewId } : new { id, kw, t = SnowFlake.NewId });
+            return RedirectToAction("Details", cid > 0 ? new { id, kw, cid, t = HttpContext.Connection.Id } : new { id, kw, t = HttpContext.Connection.Id });
         }
 
         var post = await PostService.GetQuery(p => p.Id == id && (p.Status == Status.Published || CurrentUser.IsAdmin)).Include(p => p.Seminar).AsNoTracking().FirstOrDefaultAsync() ?? throw new NotFoundException("文章未找到");
@@ -298,7 +298,7 @@ public sealed class PostController : BaseController
 
         post.Label = string.IsNullOrEmpty(post.Label?.Trim()) ? null : post.Label.Replace(",", ",");
         post.Status = Status.Pending;
-        post.Content = await ImagebedClient.ReplaceImgSrc(await post.Content.HtmlSantinizerStandard().ClearImgAttributes(), cancellationToken);
+        post.Content = await ImagebedClient.ReplaceImgSrc(await post.Content.HtmlSanitizerStandard().ClearImgAttributes(), cancellationToken);
         Post p = Mapper.Map<Post>(post);
         p.IP = ClientIP.ToString();
         p.Modifier = p.Author;

+ 1 - 1
src/Masuit.MyBlogs.Core/Controllers/UploadController.cs

@@ -146,7 +146,7 @@ public sealed class UploadController : Controller
             img.Attributes["src"].Value = path[HostEnvironment.WebRootPath.Length..].Replace("\\", "/");
         }
 
-        return body.InnerHtml.HtmlSantinizerStandard().HtmlSantinizerCustom(attributes: new[] { "dir", "lang" });
+        return body.InnerHtml.HtmlSanitizerCustom(attributes: new[] { "dir", "lang" });
     }
 
     private static async Task SaveFile(IFormFile file, string path)

+ 2 - 2
src/Masuit.MyBlogs.Core/Extensions/PerfCounterFilterAttribute.cs

@@ -15,7 +15,7 @@ public class PerfCounterFilterAttribute : ActionFilterAttribute
     /// <inheritdoc />
     public override void OnActionExecuted(ActionExecutedContext context)
     {
-        context.HttpContext.Response.Headers.Add("X-Action-Time", Stopwatch.ElapsedMilliseconds + "ms");
+        context.HttpContext.Response.Headers.AddOrUpdate("X-Action-Time", Stopwatch.ElapsedMilliseconds + "ms", Stopwatch.ElapsedMilliseconds + "ms");
     }
 
     /// <inheritdoc />
@@ -24,7 +24,7 @@ public class PerfCounterFilterAttribute : ActionFilterAttribute
         Stopwatch.Restart();
         context.HttpContext.Response.OnStarting(() =>
         {
-            context.HttpContext.Response.Headers.Add("X-Result-Time", Stopwatch.ElapsedMilliseconds + "ms");
+            context.HttpContext.Response.Headers.AddOrUpdate("X-Result-Time", Stopwatch.ElapsedMilliseconds + "ms", Stopwatch.ElapsedMilliseconds + "ms");
             return Task.CompletedTask;
         });
     }

+ 4 - 4
src/Masuit.MyBlogs.Core/Masuit.MyBlogs.Core.csproj

@@ -49,8 +49,8 @@
         <PackageReference Include="CHTCHSConv" Version="1.0.0" />
         <PackageReference Include="CLRStats" Version="1.0.0" />
         <PackageReference Include="Dispose.Scope.AspNetCore" Version="0.0.3" />
-        <PackageReference Include="FreeRedis" Version="1.0.8" />
-        <PackageReference Include="Hangfire" Version="1.7.34" />
+        <PackageReference Include="FreeRedis" Version="1.0.10" />
+        <PackageReference Include="Hangfire" Version="1.8.0" />
         <PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" />
         <PackageReference Include="htmldiff.net" Version="1.4.0" />
         <PackageReference Include="Karambolo.AspNetCore.Bundling.NUglify" Version="3.6.1" />
@@ -64,7 +64,7 @@
         <PackageReference Include="Microsoft.Graph.Auth" Version="1.0.0-preview.7" />
         <PackageReference Include="Microsoft.NETCore.Platforms" Version="7.0.2" />
         <PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.2.22" />
-        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.3" />
+        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
         <PackageReference Include="OpenXmlPowerTools-NetStandard" Version="4.6.23-alpha4" />
         <PackageReference Include="MiniProfiler.EntityFrameworkCore" Version="4.2.22" />
         <PackageReference Include="PanGu.HighLight" Version="1.0.0" />
@@ -72,7 +72,7 @@
         <PackageReference Include="System.Linq.Dynamic.Core" Version="1.3.2" />
         <PackageReference Include="TimeZoneConverter" Version="6.1.0" />
         <PackageReference Include="WilderMinds.RssSyndication" Version="1.7.0" />
-        <PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="7.20.0" />
+        <PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="7.21.0" />
     </ItemGroup>
     <ItemGroup>
         <Content Update="appsettings.json">