Jelajahi Sumber

优化广告呈现性能

懒得勤快 3 tahun lalu
induk
melakukan
4bbc645466
48 mengubah file dengan 6142 tambahan dan 6181 penghapusan
  1. 1 2
      src/Masuit.MyBlogs.Core/Common/CommonHelper.cs
  2. 2 3
      src/Masuit.MyBlogs.Core/Common/ImagebedClient.cs
  3. 1 1
      src/Masuit.MyBlogs.Core/Configs/MappingProfile.cs
  4. 70 71
      src/Masuit.MyBlogs.Core/Controllers/AdminController.cs
  5. 275 276
      src/Masuit.MyBlogs.Core/Controllers/AdvertisementController.cs
  6. 373 374
      src/Masuit.MyBlogs.Core/Controllers/BaseController.cs
  7. 68 69
      src/Masuit.MyBlogs.Core/Controllers/CategoryController.cs
  8. 293 294
      src/Masuit.MyBlogs.Core/Controllers/CommentController.cs
  9. 288 289
      src/Masuit.MyBlogs.Core/Controllers/HomeController.cs
  10. 75 76
      src/Masuit.MyBlogs.Core/Controllers/MenuController.cs
  11. 358 359
      src/Masuit.MyBlogs.Core/Controllers/MsgController.cs
  12. 1254 1255
      src/Masuit.MyBlogs.Core/Controllers/PostController.cs
  13. 324 325
      src/Masuit.MyBlogs.Core/Controllers/SubscribeController.cs
  14. 558 559
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/BaseRepository.cs
  15. 463 464
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/Interface/IBaseRepository.cs
  16. 31 32
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/PostRepository.cs
  17. 66 65
      src/Masuit.MyBlogs.Core/Infrastructure/Services/AdvertisementService.cs
  18. 661 662
      src/Masuit.MyBlogs.Core/Infrastructure/Services/BaseService.cs
  19. 20 19
      src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/IAdvertisementService.cs
  20. 464 465
      src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/IBaseService.cs
  21. 259 260
      src/Masuit.MyBlogs.Core/Infrastructure/Services/PostService.cs
  22. 0 1
      src/Masuit.MyBlogs.Core/Masuit.MyBlogs.Core.csproj
  23. 51 65
      src/Masuit.MyBlogs.Core/Models/ViewModel/HomePageViewModel.cs
  24. 143 146
      src/Masuit.MyBlogs.Core/PrepareStartup.cs
  25. 2 1
      src/Masuit.MyBlogs.Core/Startup.cs
  26. 1 1
      src/Masuit.MyBlogs.Core/Views/Advertisement/ClickRecordsInsight.cshtml
  27. 1 2
      src/Masuit.MyBlogs.Core/Views/Dashboard/Counter.razor
  28. 8 9
      src/Masuit.MyBlogs.Core/Views/Home/Category.cshtml
  29. 1 2
      src/Masuit.MyBlogs.Core/Views/Home/Index.cshtml
  30. 2 2
      src/Masuit.MyBlogs.Core/Views/Links/Index.cshtml
  31. 2 3
      src/Masuit.MyBlogs.Core/Views/Links/Index_Admin.cshtml
  32. 2 2
      src/Masuit.MyBlogs.Core/Views/Misc/Donate.cshtml
  33. 2 2
      src/Masuit.MyBlogs.Core/Views/Misc/Donate_Admin.cshtml
  34. 1 2
      src/Masuit.MyBlogs.Core/Views/Notice/Details.cshtml
  35. 2 2
      src/Masuit.MyBlogs.Core/Views/Notice/Index.cshtml
  36. 2 2
      src/Masuit.MyBlogs.Core/Views/Notice/Index_Admin.cshtml
  37. 2 1
      src/Masuit.MyBlogs.Core/Views/Post/CompareVersion.cshtml
  38. 2 2
      src/Masuit.MyBlogs.Core/Views/Post/Details.cshtml
  39. 1 2
      src/Masuit.MyBlogs.Core/Views/Post/Details_Admin.cshtml
  40. 2 1
      src/Masuit.MyBlogs.Core/Views/Post/History.cshtml
  41. 2 1
      src/Masuit.MyBlogs.Core/Views/Post/HistoryVersion.cshtml
  42. 2 1
      src/Masuit.MyBlogs.Core/Views/Post/HistoryVersion_Admin.cshtml
  43. 2 2
      src/Masuit.MyBlogs.Core/Views/Search/Search.cshtml
  44. 1 2
      src/Masuit.MyBlogs.Core/Views/Seminar/Index.cshtml
  45. 1 1
      src/Masuit.MyBlogs.Core/Views/Shared/_ArticleListAdvertisement.cshtml
  46. 1 2
      src/Masuit.MyBlogs.Core/Views/Shared/_ArticleListPartial.cshtml
  47. 1 2
      src/Masuit.MyBlogs.Core/Views/Shared/_ArticleListPartial_Admin.cshtml
  48. 1 2
      src/Masuit.MyBlogs.Core/Views/Shared/_Layout.cshtml

+ 1 - 2
src/Masuit.MyBlogs.Core/Common/CommonHelper.cs

@@ -2,7 +2,6 @@
 using AngleSharp.Css.Dom;
 using AngleSharp.Dom;
 using AutoMapper;
-using Collections.Pooled;
 using FreeRedis;
 using Hangfire;
 using IP2Region;
@@ -311,7 +310,7 @@ namespace Masuit.MyBlogs.Core.Common
             }
 
             var elements = doc.DocumentElement.QuerySelectorAll("p,br");
-            using var els = elements.OrderByRandom().Take(Math.Max(elements.Length / 5, 3)).ToPooledList();
+            var els = elements.OrderByRandom().Take(Math.Max(elements.Length / 5, 3)).ToList();
             var href = "https://" + SystemSettings["Domain"].Split('|').OrderByRandom().FirstOrDefault();
             foreach (var el in els)
             {

+ 2 - 3
src/Masuit.MyBlogs.Core/Common/ImagebedClient.cs

@@ -1,5 +1,4 @@
-using Collections.Pooled;
-using Hangfire;
+using Hangfire;
 using Masuit.MyBlogs.Core.Configs;
 using Masuit.Tools;
 using Masuit.Tools.Html;
@@ -46,7 +45,7 @@ namespace Masuit.MyBlogs.Core.Common
             }
 
             file = Regex.Replace(Path.GetFileName(file), @"\p{P}|\p{S}", "");
-            using var gitlabs = AppConfig.GitlabConfigs.Where(c => c.FileLimitSize >= stream.Length && !_failedList.Contains(c.ApiUrl)).OrderByRandom().ToPooledList();
+            var gitlabs = AppConfig.GitlabConfigs.Where(c => c.FileLimitSize >= stream.Length && !_failedList.Contains(c.ApiUrl)).OrderByRandom().ToList();
             if (gitlabs.Count > 0)
             {
                 var gitlab = gitlabs[0];

+ 1 - 1
src/Masuit.MyBlogs.Core/Configs/MappingProfile.cs

@@ -77,7 +77,7 @@ namespace Masuit.MyBlogs.Core.Configs
             CreateMap<Advertisement, AdvertisementViewModel>()
                 .ForMember(m => m.AverageViewCount, e => e.MapFrom(a => a.ClickRecords.Where(o => o.Time >= DateTime.Today.AddMonths(-1)).GroupBy(r => r.Time.Date).Select(g => g.Count()).DefaultIfEmpty().Average()))
                 .ForMember(m => m.ViewCount, e => e.MapFrom(a => a.ClickRecords.Count(o => o.Time >= DateTime.Today.AddMonths(-1))));
-            CreateMap<AdvertisementDto, Advertisement>().ForMember(a => a.ClickRecords, e => e.Ignore()).ForMember(a => a.Status, e => e.Ignore()).ForMember(a => a.UpdateTime, e => e.MapFrom(a => DateTime.Now));
+            CreateMap<AdvertisementDto, Advertisement>().ForMember(a => a.ClickRecords, e => e.Ignore()).ForMember(a => a.Status, e => e.Ignore()).ForMember(a => a.UpdateTime, e => e.MapFrom(a => DateTime.Now)).ReverseMap();
 
             CreateMap<Donate, DonateDto>();
 

+ 70 - 71
src/Masuit.MyBlogs.Core/Controllers/AdminController.cs

@@ -1,5 +1,4 @@
 using AutoMapper;
-using Collections.Pooled;
 using FreeRedis;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Configs;
@@ -14,79 +13,79 @@ using Microsoft.AspNetCore.Mvc.Filters;
 
 namespace Masuit.MyBlogs.Core.Controllers
 {
-    /// <summary>
-    /// 管理页的父控制器
-    /// </summary>
-    [MyAuthorize, ApiExplorerSettings(IgnoreApi = true)]
-    public class AdminController : Controller
-    {
-        /// <summary>
-        /// UserInfoService
-        /// </summary>
-        public IUserInfoService UserInfoService { get; set; }
-        public IRedisClient RedisHelper { get; set; }
+	/// <summary>
+	/// 管理页的父控制器
+	/// </summary>
+	[MyAuthorize, ApiExplorerSettings(IgnoreApi = true)]
+	public class AdminController : Controller
+	{
+		/// <summary>
+		/// UserInfoService
+		/// </summary>
+		public IUserInfoService UserInfoService { get; set; }
+		public IRedisClient RedisHelper { get; set; }
 
-        public IMapper Mapper { get; set; }
+		public IMapper Mapper { get; set; }
 
-        /// <summary>
-        /// 返回结果json
-        /// </summary>
-        /// <param name="data">响应数据</param>
-        /// <param name="success">响应状态</param>
-        /// <param name="message">响应消息</param>
-        /// <param name="isLogin">登录状态</param>
-        /// <returns></returns>
-        public ActionResult ResultData(object data, bool success = true, string message = "", bool isLogin = true)
-        {
-            return Ok(new
-            {
-                IsLogin = isLogin,
-                Success = success,
-                Message = message,
-                Data = data
-            });
-        }
+		/// <summary>
+		/// 返回结果json
+		/// </summary>
+		/// <param name="data">响应数据</param>
+		/// <param name="success">响应状态</param>
+		/// <param name="message">响应消息</param>
+		/// <param name="isLogin">登录状态</param>
+		/// <returns></returns>
+		public ActionResult ResultData(object data, bool success = true, string message = "", bool isLogin = true)
+		{
+			return Ok(new
+			{
+				IsLogin = isLogin,
+				Success = success,
+				Message = message,
+				Data = data
+			});
+		}
 
-        /// <summary>在调用操作方法前调用。</summary>
-        /// <param name="filterContext">有关当前请求和操作的信息。</param>
-        public override void OnActionExecuting(ActionExecutingContext filterContext)
-        {
-            base.OnActionExecuting(filterContext);
-            var user = filterContext.HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
+		/// <summary>在调用操作方法前调用。</summary>
+		/// <param name="filterContext">有关当前请求和操作的信息。</param>
+		public override void OnActionExecuting(ActionExecutingContext filterContext)
+		{
+			base.OnActionExecuting(filterContext);
+			var user = filterContext.HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
 #if DEBUG
-            user = UserInfoService.GetByUsername("masuit").Mapper<UserInfoDto>();
-            filterContext.HttpContext.Session.Set(SessionKey.UserInfo, user);
+			user = UserInfoService.GetByUsername("masuit").Mapper<UserInfoDto>();
+			filterContext.HttpContext.Session.Set(SessionKey.UserInfo, user);
 #endif
-            if (user == null && Request.Cookies.Any(x => x.Key == "username" || x.Key == "password")) //执行自动登录
-            {
-                string name = Request.Cookies["username"];
-                string pwd = Request.Cookies["password"]?.DesDecrypt(AppConfig.BaiduAK);
-                var userInfo = UserInfoService.Login(name, pwd);
-                if (userInfo != null)
-                {
-                    Response.Cookies.Append("username", name, new CookieOptions
-                    {
-                        Expires = DateTime.Now.AddYears(1),
-                        SameSite = SameSiteMode.Lax
-                    });
-                    Response.Cookies.Append("password", Request.Cookies["password"], new CookieOptions
-                    {
-                        Expires = DateTime.Now.AddYears(1),
-                        SameSite = SameSiteMode.Lax
-                    });
-                    filterContext.HttpContext.Session.Set(SessionKey.UserInfo, userInfo);
-                }
-            }
-            if (ModelState.IsValid) return;
-            using var errmsgs = ModelState.SelectMany(kv => kv.Value.Errors.Select(e => e.ErrorMessage)).ToPooledList();
-            if (errmsgs.Any())
-            {
-                for (var i = 0; i < errmsgs.Count; i++)
-                {
-                    errmsgs[i] = i + 1 + ". " + errmsgs[i];
-                }
-            }
-            filterContext.Result = ResultData(null, false, "数据校验失败,错误信息:" + string.Join(" | ", errmsgs));
-        }
-    }
+			if (user == null && Request.Cookies.Any(x => x.Key == "username" || x.Key == "password")) //执行自动登录
+			{
+				string name = Request.Cookies["username"];
+				string pwd = Request.Cookies["password"]?.DesDecrypt(AppConfig.BaiduAK);
+				var userInfo = UserInfoService.Login(name, pwd);
+				if (userInfo != null)
+				{
+					Response.Cookies.Append("username", name, new CookieOptions
+					{
+						Expires = DateTime.Now.AddYears(1),
+						SameSite = SameSiteMode.Lax
+					});
+					Response.Cookies.Append("password", Request.Cookies["password"], new CookieOptions
+					{
+						Expires = DateTime.Now.AddYears(1),
+						SameSite = SameSiteMode.Lax
+					});
+					filterContext.HttpContext.Session.Set(SessionKey.UserInfo, userInfo);
+				}
+			}
+			if (ModelState.IsValid) return;
+			var errmsgs = ModelState.SelectMany(kv => kv.Value.Errors.Select(e => e.ErrorMessage)).ToList();
+			if (errmsgs.Any())
+			{
+				for (var i = 0; i < errmsgs.Count; i++)
+				{
+					errmsgs[i] = i + 1 + ". " + errmsgs[i];
+				}
+			}
+			filterContext.Result = ResultData(null, false, "数据校验失败,错误信息:" + string.Join(" | ", errmsgs));
+		}
+	}
 }

+ 275 - 276
src/Masuit.MyBlogs.Core/Controllers/AdvertisementController.cs

@@ -1,5 +1,4 @@
 using AutoMapper.QueryableExtensions;
-using Collections.Pooled;
 using Lucene.Net.Support;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
@@ -28,304 +27,304 @@ namespace Masuit.MyBlogs.Core.Controllers;
 [Route("partner/[action]")]
 public class AdvertisementController : BaseController
 {
-    public IAdvertisementClickRecordService ClickRecordService { get; set; }
+	public IAdvertisementClickRecordService ClickRecordService { get; set; }
 
-    /// <summary>
-    /// 前往
-    /// </summary>
-    /// <param name="id">广告id</param>
-    /// <returns></returns>
-    [HttpGet("/p{id:int}"), HttpGet("{id:int}", Order = 1), ResponseCache(Duration = 3600)]
-    public async Task<IActionResult> Redirect(int id)
-    {
-        var ad = await AdsService.GetByIdAsync(id) ?? throw new NotFoundException("推广链接不存在");
-        if (!Request.IsRobot() && string.IsNullOrEmpty(HttpContext.Session.Get<string>("ads" + id)))
-        {
-            HttpContext.Session.Set("ads" + id, id.ToString());
-            ClickRecordService.AddEntity(new AdvertisementClickRecord()
-            {
-                IP = ClientIP,
-                Location = ClientIP.GetIPLocation(),
-                Referer = Request.Headers[HeaderNames.Referer].ToString(),
-                Time = DateTime.Now,
-                AdvertisementId = id
-            });
-            await ClickRecordService.SaveChangesAsync();
-            var start = DateTime.Today.AddYears(-1);
-            await ClickRecordService.GetQuery(a => a.Time < start).DeleteFromQueryAsync();
-        }
+	/// <summary>
+	/// 前往
+	/// </summary>
+	/// <param name="id">广告id</param>
+	/// <returns></returns>
+	[HttpGet("/p{id:int}"), HttpGet("{id:int}", Order = 1), ResponseCache(Duration = 3600)]
+	public async Task<IActionResult> Redirect(int id)
+	{
+		var ad = await AdsService.GetByIdAsync(id) ?? throw new NotFoundException("推广链接不存在");
+		if (!Request.IsRobot() && string.IsNullOrEmpty(HttpContext.Session.Get<string>("ads" + id)))
+		{
+			HttpContext.Session.Set("ads" + id, id.ToString());
+			ClickRecordService.AddEntity(new AdvertisementClickRecord()
+			{
+				IP = ClientIP,
+				Location = ClientIP.GetIPLocation(),
+				Referer = Request.Headers[HeaderNames.Referer].ToString(),
+				Time = DateTime.Now,
+				AdvertisementId = id
+			});
+			await ClickRecordService.SaveChangesAsync();
+			var start = DateTime.Today.AddYears(-1);
+			await ClickRecordService.GetQuery(a => a.Time < start).DeleteFromQueryAsync();
+		}
 
-        return Redirect(ad.Url);
-    }
+		return Redirect(ad.Url);
+	}
 
-    /// <summary>
-    /// 获取分页
-    /// </summary>
-    /// <returns></returns>
-    [MyAuthorize]
-    public ActionResult GetPageData(int page = 1, [Range(1, int.MaxValue, ErrorMessage = "页大小必须大于0")] int size = 10, string kw = "")
-    {
-        Expression<Func<Advertisement, bool>> where = p => true;
-        if (!string.IsNullOrEmpty(kw))
-        {
-            kw = Regex.Escape(kw);
-            where = where.And(p => Regex.IsMatch(p.Title + p.Description + p.Url, kw, RegexOptions.IgnoreCase));
-        }
+	/// <summary>
+	/// 获取分页
+	/// </summary>
+	/// <returns></returns>
+	[MyAuthorize]
+	public ActionResult GetPageData(int page = 1, [Range(1, int.MaxValue, ErrorMessage = "页大小必须大于0")] int size = 10, string kw = "")
+	{
+		Expression<Func<Advertisement, bool>> where = p => true;
+		if (!string.IsNullOrEmpty(kw))
+		{
+			kw = Regex.Escape(kw);
+			where = where.And(p => Regex.IsMatch(p.Title + p.Description + p.Url, kw, RegexOptions.IgnoreCase));
+		}
 
-        var list = AdsService.GetQuery(where).OrderByDescending(p => p.Status == Status.Available).ThenByDescending(a => a.Price).ThenByDescending(a => a.Id).ProjectTo<AdvertisementViewModel>(MapperConfig).ToPagedList(page, size);
-        return Ok(list);
-    }
+		var list = AdsService.GetQuery(where).OrderByDescending(p => p.Status == Status.Available).ThenByDescending(a => a.Price).ThenByDescending(a => a.Id).ProjectTo<AdvertisementViewModel>(MapperConfig).ToPagedList(page, size);
+		return Ok(list);
+	}
 
-    /// <summary>
-    /// 保存广告
-    /// </summary>
-    /// <param name="model"></param>
-    /// <returns></returns>
-    [HttpPost, MyAuthorize]
-    public async Task<IActionResult> Save([FromBodyOrDefault] AdvertisementDto model)
-    {
-        var entity = AdsService[model.Id] ?? new Advertisement();
-        model.CategoryIds = model.CategoryIds?.Replace("null", "");
-        model.Regions = Regex.Replace(model.Regions ?? "", @"(\p{P}|\p{Z}|\p{S})+", "|");
-        if (model.RegionMode == RegionLimitMode.All)
-        {
-            model.Regions = null;
-        }
+	/// <summary>
+	/// 保存广告
+	/// </summary>
+	/// <param name="model"></param>
+	/// <returns></returns>
+	[HttpPost, MyAuthorize]
+	public async Task<IActionResult> Save([FromBodyOrDefault] AdvertisementDto model)
+	{
+		var entity = AdsService[model.Id] ?? new Advertisement();
+		model.CategoryIds = model.CategoryIds?.Replace("null", "");
+		model.Regions = Regex.Replace(model.Regions ?? "", @"(\p{P}|\p{Z}|\p{S})+", "|");
+		if (model.RegionMode == RegionLimitMode.All)
+		{
+			model.Regions = null;
+		}
 
-        if (model.Types.Contains(AdvertiseType.Banner.ToString("D")) && string.IsNullOrEmpty(model.ImageUrl))
-        {
-            return ResultData(null, false, "宣传大图不能为空");
-        }
+		if (model.Types.Contains(AdvertiseType.Banner.ToString("D")) && string.IsNullOrEmpty(model.ImageUrl))
+		{
+			return ResultData(null, false, "宣传大图不能为空");
+		}
 
-        if (model.Types.Length > 3 && string.IsNullOrEmpty(model.ThumbImgUrl))
-        {
-            return ResultData(null, false, "宣传小图不能为空");
-        }
+		if (model.Types.Length > 3 && string.IsNullOrEmpty(model.ThumbImgUrl))
+		{
+			return ResultData(null, false, "宣传小图不能为空");
+		}
 
-        Mapper.Map(model, entity);
-        var b = await AdsService.AddOrUpdateSavedAsync(a => a.Id, entity) > 0;
-        return ResultData(null, b, b ? "保存成功" : "保存失败");
-    }
+		Mapper.Map(model, entity);
+		var b = await AdsService.AddOrUpdateSavedAsync(a => a.Id, entity) > 0;
+		return ResultData(null, b, b ? "保存成功" : "保存失败");
+	}
 
-    /// <summary>
-    /// 删除广告
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    [HttpPost("{id}"), HttpGet("{id}"), MyAuthorize]
-    public async Task<IActionResult> Delete(int id)
-    {
-        bool b = await AdsService.DeleteByIdAsync(id) > 0;
-        return ResultData(null, b, b ? "删除成功" : "删除失败");
-    }
+	/// <summary>
+	/// 删除广告
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpPost("{id}"), HttpGet("{id}"), MyAuthorize]
+	public async Task<IActionResult> Delete(int id)
+	{
+		bool b = await AdsService.DeleteByIdAsync(id) > 0;
+		return ResultData(null, b, b ? "删除成功" : "删除失败");
+	}
 
-    /// <summary>
-    /// 广告上下架
-    /// </summary>
-    /// <param name="id">文章id</param>
-    /// <returns></returns>
-    [MyAuthorize, HttpPost("{id}")]
-    public async Task<ActionResult> ChangeState(int id)
-    {
-        var ad = await AdsService.GetByIdAsync(id) ?? throw new NotFoundException("广告不存在!");
-        ad.Status = ad.Status == Status.Available ? Status.Unavailable : Status.Available;
-        return ResultData(null, await AdsService.SaveChangesAsync() > 0, ad.Status == Status.Available ? $"【{ad.Title}】已上架!" : $"【{ad.Title}】已下架!");
-    }
+	/// <summary>
+	/// 广告上下架
+	/// </summary>
+	/// <param name="id">文章id</param>
+	/// <returns></returns>
+	[MyAuthorize, HttpPost("{id}")]
+	public async Task<ActionResult> ChangeState(int id)
+	{
+		var ad = await AdsService.GetByIdAsync(id) ?? throw new NotFoundException("广告不存在!");
+		ad.Status = ad.Status == Status.Available ? Status.Unavailable : Status.Available;
+		return ResultData(null, await AdsService.SaveChangesAsync() > 0, ad.Status == Status.Available ? $"【{ad.Title}】已上架!" : $"【{ad.Title}】已下架!");
+	}
 
-    /// <summary>
-    /// 随机前往一个广告
-    /// </summary>
-    /// <returns></returns>
-    [HttpGet("/partner-random")]
-    public async Task<ActionResult> RandomGo()
-    {
-        var ad = AdsService.GetByWeightedPrice((AdvertiseType)new Random().Next(1, 4), Request.Location());
-        if (!Request.IsRobot() && string.IsNullOrEmpty(HttpContext.Session.Get<string>("ads" + ad.Id)))
-        {
-            HttpContext.Session.Set("ads" + ad.Id, ad.Id.ToString());
-            ad.ClickRecords.Add(new AdvertisementClickRecord()
-            {
-                IP = ClientIP,
-                Location = ClientIP.GetIPLocation(),
-                Referer = Request.Headers[HeaderNames.Referer].ToString(),
-                Time = DateTime.Now
-            });
-            await AdsService.SaveChangesAsync();
-            var start = DateTime.Today.AddMonths(-1);
-            await ClickRecordService.GetQuery(a => a.Time < start).DeleteFromQueryAsync();
-        }
+	/// <summary>
+	/// 随机前往一个广告
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("/partner-random")]
+	public async Task<ActionResult> RandomGo()
+	{
+		var ad = AdsService.GetByWeightedPrice((AdvertiseType)new Random().Next(1, 4), Request.Location());
+		if (!Request.IsRobot() && string.IsNullOrEmpty(HttpContext.Session.Get<string>("ads" + ad.Id)))
+		{
+			HttpContext.Session.Set("ads" + ad.Id, ad.Id.ToString());
+			ClickRecordService.AddEntity(new AdvertisementClickRecord()
+			{
+				IP = ClientIP,
+				Location = ClientIP.GetIPLocation(),
+				Referer = Request.Headers[HeaderNames.Referer].ToString(),
+				Time = DateTime.Now
+			});
+			await AdsService.SaveChangesAsync();
+			var start = DateTime.Today.AddMonths(-1);
+			await ClickRecordService.GetQuery(a => a.Time < start).DeleteFromQueryAsync();
+		}
 
-        return Redirect(ad.Url);
-    }
+		return Redirect(ad.Url);
+	}
 
-    /// <summary>
-    /// 广告访问记录
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="page"></param>
-    /// <param name="size"></param>
-    /// <returns></returns>
-    [HttpGet("/partner/{id}/records"), MyAuthorize]
-    public async Task<IActionResult> ClickRecords(int id, int page = 1, int size = 15, string kw = "")
-    {
-        Expression<Func<AdvertisementClickRecord, bool>> where = e => e.AdvertisementId == id;
-        if (!string.IsNullOrEmpty(kw))
-        {
-            kw = Regex.Escape(kw);
-            where = where.And(e => Regex.IsMatch(e.IP + e.Location + e.Referer, kw));
-        }
+	/// <summary>
+	/// 广告访问记录
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <returns></returns>
+	[HttpGet("/partner/{id}/records"), MyAuthorize]
+	public async Task<IActionResult> ClickRecords(int id, int page = 1, int size = 15, string kw = "")
+	{
+		Expression<Func<AdvertisementClickRecord, bool>> where = e => e.AdvertisementId == id;
+		if (!string.IsNullOrEmpty(kw))
+		{
+			kw = Regex.Escape(kw);
+			where = where.And(e => Regex.IsMatch(e.IP + e.Location + e.Referer, kw));
+		}
 
-        var pages = await ClickRecordService.GetPagesAsync<DateTime, AdvertisementClickRecordViewModel>(page, size, where, e => e.Time, false);
-        return Ok(pages);
-    }
+		var pages = await ClickRecordService.GetPagesAsync<DateTime, AdvertisementClickRecordViewModel>(page, size, where, e => e.Time, false);
+		return Ok(pages);
+	}
 
-    /// <summary>
-    /// 导出广告访问记录
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    [HttpGet("/partner/{id}/records-export"), MyAuthorize]
-    public IActionResult ExportClickRecords(int id)
-    {
-        using var list = ClickRecordService.GetQuery<DateTime, AdvertisementClickRecordViewModel>(e => e.AdvertisementId == id, e => e.Time, false).ToPooledList();
-        using var ms = list.ToExcel();
-        var advertisement = AdsService[id];
-        return this.ResumeFile(ms.ToArray(), ContentType.Xlsx, advertisement.Title + "访问记录.xlsx");
-    }
+	/// <summary>
+	/// 导出广告访问记录
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpGet("/partner/{id}/records-export"), MyAuthorize]
+	public IActionResult ExportClickRecords(int id)
+	{
+		var list = ClickRecordService.GetQuery<DateTime, AdvertisementClickRecordViewModel>(e => e.AdvertisementId == id, e => e.Time, false).ToList();
+		using var ms = list.ToExcel();
+		var advertisement = AdsService[id];
+		return this.ResumeFile(ms.ToArray(), ContentType.Xlsx, advertisement.Title + "访问记录.xlsx");
+	}
 
-    /// <summary>
-    /// 广告访问记录图表
-    /// </summary>
-    /// <returns></returns>
-    [HttpGet("/partner/{id}/records-chart"), MyAuthorize]
-    [ProducesResponseType((int)HttpStatusCode.OK)]
-    public async Task<IActionResult> ClickRecordsChart(int id, bool compare, uint period, CancellationToken cancellationToken)
-    {
-        if (compare)
-        {
-            var start1 = DateTime.Today.AddDays(-period);
-            var list1 = await ClickRecordService.GetQuery(e => e.AdvertisementId == id && e.Time >= start1).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
-            {
-                Date = g.Key,
-                Count = g.Count()
-            }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
-            if (list1.Count == 0)
-            {
-                return Ok(Array.Empty<int>());
-            }
+	/// <summary>
+	/// 广告访问记录图表
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("/partner/{id}/records-chart"), MyAuthorize]
+	[ProducesResponseType((int)HttpStatusCode.OK)]
+	public async Task<IActionResult> ClickRecordsChart(int id, bool compare, uint period, CancellationToken cancellationToken)
+	{
+		if (compare)
+		{
+			var start1 = DateTime.Today.AddDays(-period);
+			var list1 = await ClickRecordService.GetQuery(e => e.AdvertisementId == id && e.Time >= start1).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
+			{
+				Date = g.Key,
+				Count = g.Count()
+			}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
+			if (list1.Count == 0)
+			{
+				return Ok(Array.Empty<int>());
+			}
 
-            var start2 = start1.AddDays(-period - 1);
-            var list2 = await ClickRecordService.GetQuery(e => e.AdvertisementId == id && e.Time >= start2 && e.Time < start1).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
-            {
-                Date = g.Key,
-                Count = g.Count()
-            }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
+			var start2 = start1.AddDays(-period - 1);
+			var list2 = await ClickRecordService.GetQuery(e => e.AdvertisementId == id && e.Time >= start2 && e.Time < start1).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
+			{
+				Date = g.Key,
+				Count = g.Count()
+			}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
 
-            // 将数据填充成连续的数据
-            for (var i = start1; i <= DateTime.Today; i = i.AddDays(1))
-            {
-                if (list1.All(a => a.Date != i))
-                {
-                    list1.Add(new { Date = i, Count = 0 });
-                }
-            }
-            for (var i = start2; i < start1; i = i.AddDays(1))
-            {
-                if (list2.All(a => a.Date != i))
-                {
-                    list2.Add(new { Date = i, Count = 0 });
-                }
-            }
-            return Ok(new[] { list1.OrderBy(a => a.Date), list2.OrderBy(a => a.Date) });
-        }
+			// 将数据填充成连续的数据
+			for (var i = start1; i <= DateTime.Today; i = i.AddDays(1))
+			{
+				if (list1.All(a => a.Date != i))
+				{
+					list1.Add(new { Date = i, Count = 0 });
+				}
+			}
+			for (var i = start2; i < start1; i = i.AddDays(1))
+			{
+				if (list2.All(a => a.Date != i))
+				{
+					list2.Add(new { Date = i, Count = 0 });
+				}
+			}
+			return Ok(new[] { list1.OrderBy(a => a.Date), list2.OrderBy(a => a.Date) });
+		}
 
-        var list = await ClickRecordService.GetQuery(e => e.AdvertisementId == id).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
-        {
-            Date = g.Key,
-            Count = g.Count()
-        }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
-        return Ok(new[] { list });
-    }
+		var list = await ClickRecordService.GetQuery(e => e.AdvertisementId == id).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
+		{
+			Date = g.Key,
+			Count = g.Count()
+		}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
+		return Ok(new[] { list });
+	}
 
-    /// <summary>
-    /// 广告访问记录图表
-    /// </summary>
-    /// <returns></returns>
-    [HttpGet("/partner/records-chart"), MyAuthorize]
-    [ProducesResponseType((int)HttpStatusCode.OK)]
-    public async Task<IActionResult> ClickRecordsChart(bool compare, uint period, CancellationToken cancellationToken)
-    {
-        if (compare)
-        {
-            var start1 = DateTime.Today.AddDays(-period);
-            var list1 = await ClickRecordService.GetQuery(e => e.Time >= start1).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
-            {
-                Date = g.Key,
-                Count = g.Count()
-            }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
-            if (list1.Count == 0)
-            {
-                return Ok(Array.Empty<int>());
-            }
+	/// <summary>
+	/// 广告访问记录图表
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("/partner/records-chart"), MyAuthorize]
+	[ProducesResponseType((int)HttpStatusCode.OK)]
+	public async Task<IActionResult> ClickRecordsChart(bool compare, uint period, CancellationToken cancellationToken)
+	{
+		if (compare)
+		{
+			var start1 = DateTime.Today.AddDays(-period);
+			var list1 = await ClickRecordService.GetQuery(e => e.Time >= start1).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
+			{
+				Date = g.Key,
+				Count = g.Count()
+			}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
+			if (list1.Count == 0)
+			{
+				return Ok(Array.Empty<int>());
+			}
 
-            var start2 = start1.AddDays(-period - 1);
-            var list2 = await ClickRecordService.GetQuery(e => e.Time >= start2 && e.Time < start1).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
-            {
-                Date = g.Key,
-                Count = g.Count()
-            }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
+			var start2 = start1.AddDays(-period - 1);
+			var list2 = await ClickRecordService.GetQuery(e => e.Time >= start2 && e.Time < start1).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
+			{
+				Date = g.Key,
+				Count = g.Count()
+			}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
 
-            // 将数据填充成连续的数据
-            for (var i = start1; i <= DateTime.Today; i = i.AddDays(1))
-            {
-                if (list1.All(a => a.Date != i))
-                {
-                    list1.Add(new { Date = i, Count = 0 });
-                }
-            }
-            for (var i = start2; i < start1; i = i.AddDays(1))
-            {
-                if (list2.All(a => a.Date != i))
-                {
-                    list2.Add(new { Date = i, Count = 0 });
-                }
-            }
-            return Ok(new[] { list1.OrderBy(a => a.Date), list2.OrderBy(a => a.Date) });
-        }
+			// 将数据填充成连续的数据
+			for (var i = start1; i <= DateTime.Today; i = i.AddDays(1))
+			{
+				if (list1.All(a => a.Date != i))
+				{
+					list1.Add(new { Date = i, Count = 0 });
+				}
+			}
+			for (var i = start2; i < start1; i = i.AddDays(1))
+			{
+				if (list2.All(a => a.Date != i))
+				{
+					list2.Add(new { Date = i, Count = 0 });
+				}
+			}
+			return Ok(new[] { list1.OrderBy(a => a.Date), list2.OrderBy(a => a.Date) });
+		}
 
-        var start = DateTime.Now.AddMonths(-1);
-        var list = await ClickRecordService.GetQuery(e => e.Time >= start).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
-        {
-            Date = g.Key,
-            Count = g.Count()
-        }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
-        return Ok(new[] { list });
-    }
+		var start = DateTime.Now.AddMonths(-1);
+		var list = await ClickRecordService.GetQuery(e => e.Time >= start).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
+		{
+			Date = g.Key,
+			Count = g.Count()
+		}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
+		return Ok(new[] { list });
+	}
 
-    /// <summary>
-    /// 广告访问记录分析
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    [HttpGet("/partner/{id}/insight"), MyAuthorize]
-    public IActionResult ClickRecordsInsight(int id)
-    {
-        return View(AdsService[id]);
-    }
+	/// <summary>
+	/// 广告访问记录分析
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpGet("/partner/{id}/insight"), MyAuthorize]
+	public IActionResult ClickRecordsInsight(int id)
+	{
+		return View(Mapper.Map<AdvertisementViewModel>(AdsService[id]));
+	}
 
-    /// <summary>
-    /// 设置分类
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="cids"></param>
-    /// <returns></returns>
-    /// <exception cref="NotFoundException"></exception>
-    [HttpPost("/partner/{id}/categories")]
-    public async Task<ActionResult> SetCategories(int id, [FromBodyOrDefault] string cids)
-    {
-        var entity = await AdsService.GetByIdAsync(id) ?? throw new NotFoundException("广告未找到");
-        entity.CategoryIds = cids;
-        await AdsService.SaveChangesAsync();
-        return Ok();
-    }
+	/// <summary>
+	/// 设置分类
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="cids"></param>
+	/// <returns></returns>
+	/// <exception cref="NotFoundException"></exception>
+	[HttpPost("/partner/{id}/categories")]
+	public async Task<ActionResult> SetCategories(int id, [FromBodyOrDefault] string cids)
+	{
+		var entity = await AdsService.GetByIdAsync(id) ?? throw new NotFoundException("广告未找到");
+		entity.CategoryIds = cids;
+		await AdsService.SaveChangesAsync();
+		return Ok();
+	}
 }

+ 373 - 374
src/Masuit.MyBlogs.Core/Controllers/BaseController.cs

@@ -1,5 +1,4 @@
 using AutoMapper;
-using Collections.Pooled;
 using FreeRedis;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Common.Mails;
@@ -26,381 +25,381 @@ using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
 
 namespace Masuit.MyBlogs.Core.Controllers
 {
-    /// <summary>
-    /// 基本父控制器
-    /// </summary>
-    [ApiExplorerSettings(IgnoreApi = true), ServiceFilter(typeof(FirewallAttribute))]
-    public class BaseController : Controller
-    {
-        public IUserInfoService UserInfoService { get; set; }
-
-        public ILinksService LinksService { get; set; }
-
-        public IAdvertisementService AdsService { get; set; }
-
-        public IVariablesService VariablesService { get; set; }
-
-        public IMapper Mapper { get; set; }
-
-        public MapperConfiguration MapperConfig { get; set; }
-        public IRedisClient RedisHelper { get; set; }
-        public UserInfoDto CurrentUser => HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo) ?? new UserInfoDto();
-
-        /// <summary>
-        /// 客户端的真实IP
-        /// </summary>
-        public string ClientIP => HttpContext.Connection.RemoteIpAddress.ToString();
-
-        /// <summary>
-        /// 普通访客是否token合法
-        /// </summary>
-        public bool VisitorTokenValid => Request.Cookies.ContainsKey("FullAccessToken") && Request.Cookies["Email"].MDString(AppConfig.BaiduAK).Equals(Request.Cookies["FullAccessToken"]);
-
-        public int[] HideCategories => Request.GetHideCategories();
-
-        /// <summary>
-        /// 响应数据
-        /// </summary>
-        /// <param name="data">数据</param>
-        /// <param name="success">响应状态</param>
-        /// <param name="message">响应消息</param>
-        /// <param name="isLogin">登录状态</param>
-        /// <param name="code">http响应码</param>
-        /// <returns></returns>
-        public ActionResult ResultData(object data, bool success = true, string message = "", bool isLogin = true, HttpStatusCode code = HttpStatusCode.OK)
-        {
-            return Ok(new
-            {
-                IsLogin = isLogin,
-                Success = success,
-                Message = message,
-                Data = data,
-                code
-            });
-        }
-
-        protected string ReplaceVariables(string text, int depth = 0)
-        {
-            if (string.IsNullOrEmpty(text))
-            {
-                return text;
-            }
-
-            var location = Request.Location();
-            var template = Template.Create(text).Set("clientip", ClientIP).Set("location", location.Address).Set("network", location.Network).Set("domain", Request.Host.Host).Set("path", Request.Path.ToUriComponent());
-            if (text.Contains("{{browser}}") || text.Contains("{{os}}"))
-            {
-                var agent = UserAgent.Parse(Request.Headers[HeaderNames.UserAgent] + "");
-                template.Set("browser", agent.Browser + " " + agent.BrowserVersion).Set("os", agent.Platform);
-            }
-
-            var pattern = @"\{\{[\w._-]+\}\}";
-            var keys = Regex.Matches(template.Render(), pattern).Select(m => m.Value.Trim('{', '}')).ToArray();
-            if (keys.Length > 0)
-            {
-                var dic = VariablesService.GetQueryFromCache(v => keys.Contains(v.Key)).ToDictionary(v => v.Key, v => v.Value);
-                foreach (var (key, value) in dic)
-                {
-                    string valve = value;
-                    if (Regex.IsMatch(valve, pattern) && depth < 32)
-                    {
-                        valve = ReplaceVariables(valve, depth++);
-                    }
-
-                    template.Set(key, valve);
-                }
-            }
-
-            return template.Render();
-        }
-
-        public override Task OnActionExecutionAsync(ActionExecutingContext filterContext, ActionExecutionDelegate next)
-        {
-            ViewBag.Desc = CommonHelper.SystemSettings["Description"];
-            var user = filterContext.HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
+	/// <summary>
+	/// 基本父控制器
+	/// </summary>
+	[ApiExplorerSettings(IgnoreApi = true), ServiceFilter(typeof(FirewallAttribute))]
+	public class BaseController : Controller
+	{
+		public IUserInfoService UserInfoService { get; set; }
+
+		public ILinksService LinksService { get; set; }
+
+		public IAdvertisementService AdsService { get; set; }
+
+		public IVariablesService VariablesService { get; set; }
+
+		public IMapper Mapper { get; set; }
+
+		public MapperConfiguration MapperConfig { get; set; }
+		public IRedisClient RedisHelper { get; set; }
+		public UserInfoDto CurrentUser => HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo) ?? new UserInfoDto();
+
+		/// <summary>
+		/// 客户端的真实IP
+		/// </summary>
+		public string ClientIP => HttpContext.Connection.RemoteIpAddress.ToString();
+
+		/// <summary>
+		/// 普通访客是否token合法
+		/// </summary>
+		public bool VisitorTokenValid => Request.Cookies.ContainsKey("FullAccessToken") && Request.Cookies["Email"].MDString(AppConfig.BaiduAK).Equals(Request.Cookies["FullAccessToken"]);
+
+		public int[] HideCategories => Request.GetHideCategories();
+
+		/// <summary>
+		/// 响应数据
+		/// </summary>
+		/// <param name="data">数据</param>
+		/// <param name="success">响应状态</param>
+		/// <param name="message">响应消息</param>
+		/// <param name="isLogin">登录状态</param>
+		/// <param name="code">http响应码</param>
+		/// <returns></returns>
+		public ActionResult ResultData(object data, bool success = true, string message = "", bool isLogin = true, HttpStatusCode code = HttpStatusCode.OK)
+		{
+			return Ok(new
+			{
+				IsLogin = isLogin,
+				Success = success,
+				Message = message,
+				Data = data,
+				code
+			});
+		}
+
+		protected string ReplaceVariables(string text, int depth = 0)
+		{
+			if (string.IsNullOrEmpty(text))
+			{
+				return text;
+			}
+
+			var location = Request.Location();
+			var template = Template.Create(text).Set("clientip", ClientIP).Set("location", location.Address).Set("network", location.Network).Set("domain", Request.Host.Host).Set("path", Request.Path.ToUriComponent());
+			if (text.Contains("{{browser}}") || text.Contains("{{os}}"))
+			{
+				var agent = UserAgent.Parse(Request.Headers[HeaderNames.UserAgent] + "");
+				template.Set("browser", agent.Browser + " " + agent.BrowserVersion).Set("os", agent.Platform);
+			}
+
+			var pattern = @"\{\{[\w._-]+\}\}";
+			var keys = Regex.Matches(template.Render(), pattern).Select(m => m.Value.Trim('{', '}')).ToArray();
+			if (keys.Length > 0)
+			{
+				var dic = VariablesService.GetQueryFromCache(v => keys.Contains(v.Key)).ToDictionary(v => v.Key, v => v.Value);
+				foreach (var (key, value) in dic)
+				{
+					string valve = value;
+					if (Regex.IsMatch(valve, pattern) && depth < 32)
+					{
+						valve = ReplaceVariables(valve, depth++);
+					}
+
+					template.Set(key, valve);
+				}
+			}
+
+			return template.Render();
+		}
+
+		public override Task OnActionExecutionAsync(ActionExecutingContext filterContext, ActionExecutionDelegate next)
+		{
+			ViewBag.Desc = CommonHelper.SystemSettings["Description"];
+			var user = filterContext.HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
 #if DEBUG
             user = UserInfoService.GetByUsername("masuit").Mapper<UserInfoDto>();
             filterContext.HttpContext.Session.Set(SessionKey.UserInfo, user);
 #endif
-            if (CommonHelper.SystemSettings.GetOrAdd("CloseSite", "false") == "true" && user?.IsAdmin != true)
-            {
-                filterContext.Result = RedirectToAction("ComingSoon", "Error");
-                return Task.CompletedTask;
-            }
-
-            if (Request.Method == HttpMethods.Post && !Request.Path.Value.Contains("get", StringComparison.InvariantCultureIgnoreCase) && CommonHelper.SystemSettings.GetOrAdd("DataReadonly", "false") == "true" && !filterContext.Filters.Any(m => m.ToString().Contains(nameof(MyAuthorizeAttribute))))
-            {
-                filterContext.Result = ResultData("网站当前处于数据写保护状态,无法提交任何数据,如有疑问请联系网站管理员!", false, "网站当前处于数据写保护状态,无法提交任何数据,如有疑问请联系网站管理员!", user != null, HttpStatusCode.BadRequest);
-                return Task.CompletedTask;
-            }
-
-            if (user == null && Request.Cookies.ContainsKey("username") && Request.Cookies.ContainsKey("password")) //执行自动登录
-            {
-                var name = Request.Cookies["username"];
-                var pwd = Request.Cookies["password"];
-                var userInfo = UserInfoService.Login(name, pwd.DesDecrypt(AppConfig.BaiduAK));
-                if (userInfo != null)
-                {
-                    Response.Cookies.Append("username", name, new CookieOptions
-                    {
-                        Expires = DateTime.Now.AddYears(1),
-                        SameSite = SameSiteMode.Lax
-                    });
-                    Response.Cookies.Append("password", pwd, new CookieOptions
-                    {
-                        Expires = DateTime.Now.AddYears(1),
-                        SameSite = SameSiteMode.Lax
-                    });
-                    filterContext.HttpContext.Session.Set(SessionKey.UserInfo, userInfo);
-                }
-            }
-
-            if (ModelState.IsValid) return next();
-            using var errmsgs = ModelState.SelectMany(kv => kv.Value.Errors.Select(e => e.ErrorMessage)).Select((s, i) => $"{i + 1}. {s}").ToPooledList();
-            filterContext.Result = true switch
-            {
-                _ when Request.HasJsonContentType() || Request.Method == HttpMethods.Post => ResultData(errmsgs, false, "数据校验失败,错误信息:" + errmsgs.Join(" | "), user != null, HttpStatusCode.BadRequest),
-                _ => base.BadRequest("参数错误:" + errmsgs.Join(" | "))
-            };
-            return Task.CompletedTask;
-        }
-
-        /// <summary>
-        /// 验证邮箱验证码
-        /// </summary>
-        /// <param name="mailSender"></param>
-        /// <param name="email">邮箱地址</param>
-        /// <param name="code">验证码</param>
-        /// <returns></returns>
-        internal string ValidateEmailCode(IMailSender mailSender, string email, string code)
-        {
-            if (CurrentUser.IsAdmin)
-            {
-                return string.Empty; ;
-            }
-
-            if (string.IsNullOrEmpty(Request.Cookies["ValidateKey"]))
-            {
-                if (string.IsNullOrEmpty(code))
-                {
-                    return "请输入验证码!";
-                }
-                if (RedisHelper.Get("code:" + email) != code)
-                {
-                    return "验证码错误!";
-                }
-            }
-            else if (Request.Cookies["ValidateKey"].DesDecrypt(AppConfig.BaiduAK) != email)
-            {
-                Response.Cookies.Delete("Email");
-                Response.Cookies.Delete("NickName");
-                Response.Cookies.Delete("ValidateKey");
-                return "邮箱验证信息已失效,请刷新页面后重新评论!";
-            }
-
-            if (mailSender.HasBounced(email))
-            {
-                Response.Cookies.Delete("Email");
-                Response.Cookies.Delete("NickName");
-                Response.Cookies.Delete("ValidateKey");
-                return "邮箱地址错误,请刷新页面后重新使用有效的邮箱地址!";
-            }
-
-            return string.Empty;
-        }
-
-        internal void WriteEmailKeyCookie(string email)
-        {
-            Response.Cookies.Append("Email", email, new CookieOptions()
-            {
-                Expires = DateTimeOffset.Now.AddYears(1),
-                SameSite = SameSiteMode.Lax
-            });
-            Response.Cookies.Append("ValidateKey", email.DesEncrypt(AppConfig.BaiduAK), new CookieOptions()
-            {
-                Expires = DateTimeOffset.Now.AddYears(1),
-                SameSite = SameSiteMode.Lax
-            });
-        }
-
-        protected void CheckPermission(List<PostDto> posts)
-        {
-            if (CurrentUser.IsAdmin || Request.IsRobot())
-            {
-                return;
-            }
-
-            posts.RemoveAll(p => p.LimitMode == RegionLimitMode.OnlyForSearchEngine);
-            if (VisitorTokenValid || CommonHelper.IPWhiteList.Contains(ClientIP))
-            {
-                return;
-            }
-
-            var ipLocation = Request.Location();
-            var location = ipLocation + ipLocation.Coodinate + "|" + Request.Headers[HeaderNames.Referer] + "|" + Request.Headers[HeaderNames.UserAgent];
-            if (Request.Cookies.TryGetValue(SessionKey.RawIP, out var rawip))
-            {
-                var s = rawip.Base64Decrypt();
-                if (ClientIP != s)
-                {
-                    location += "|" + s.GetIPLocation();
-                }
-            }
-
-            posts.RemoveAll(p =>
-            {
-                switch (p.LimitMode)
-                {
-                    case RegionLimitMode.AllowRegion:
-                        return !Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase);
-
-                    case RegionLimitMode.ForbidRegion:
-                        return Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase);
-
-                    case RegionLimitMode.AllowRegionExceptForbidRegion:
-                        if (Regex.IsMatch(location, p.ExceptRegions, RegexOptions.IgnoreCase))
-                        {
-                            return true;
-                        }
-
-                        goto case RegionLimitMode.AllowRegion;
-                    case RegionLimitMode.ForbidRegionExceptAllowRegion:
-                        if (Regex.IsMatch(location, p.ExceptRegions, RegexOptions.IgnoreCase))
-                        {
-                            return false;
-                        }
-
-                        goto case RegionLimitMode.ForbidRegion;
-                    default:
-                        return false;
-                }
-            });
-            posts.RemoveAll(p => HideCategories.Contains(p.CategoryId));
-        }
-
-        protected Expression<Func<Post, bool>> PostBaseWhere()
-        {
-            if (CurrentUser.IsAdmin || Request.IsRobot())
-            {
-                return _ => true;
-            }
-
-            Expression<Func<Post, bool>> where = p => p.Status == Status.Published && p.LimitMode != RegionLimitMode.OnlyForSearchEngine;
-            if (HideCategories.Length > 0)
-            {
-                where = where.And(p => !HideCategories.Contains(p.CategoryId));
-            }
-
-            if (VisitorTokenValid || CommonHelper.IPWhiteList.Contains(ClientIP))
-            {
-                return where;
-            }
-
-            var ipLocation = Request.Location();
-            var location = ipLocation + ipLocation.Coodinate + "|" + Request.Headers[HeaderNames.Referer] + "|" + Request.Headers[HeaderNames.UserAgent];
-            if (Request.Cookies.TryGetValue(SessionKey.RawIP, out var rawip) && ClientIP != rawip)
-            {
-                var s = rawip.Base64Decrypt();
-                if (ClientIP != s)
-                {
-                    location += "|" + s.GetIPLocation();
-                }
-            }
-
-            return where.And(p => p.LimitMode == null || p.LimitMode == RegionLimitMode.All ? true :
-                   p.LimitMode == RegionLimitMode.AllowRegion ? Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) :
-                   p.LimitMode == RegionLimitMode.ForbidRegion ? !Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) :
-                   p.LimitMode == RegionLimitMode.AllowRegionExceptForbidRegion ? Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) && !Regex.IsMatch(location, p.ExceptRegions, RegexOptions.IgnoreCase) : !Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) || Regex.IsMatch(location, p.ExceptRegions, RegexOptions.IgnoreCase));
-        }
-
-        protected void CheckPermission(Post post)
-        {
-            if (CurrentUser.IsAdmin || VisitorTokenValid || Request.IsRobot() || CommonHelper.IPWhiteList.Contains(ClientIP))
-            {
-                return;
-            }
-
-            var ipLocation = Request.Location();
-            var location = ipLocation + ipLocation.Coodinate + "|" + Request.Headers[HeaderNames.Referer] + "|" + Request.Headers[HeaderNames.UserAgent];
-            if (Request.Cookies.TryGetValue(SessionKey.RawIP, out var rawip) && ClientIP != rawip)
-            {
-                var s = rawip.Base64Decrypt();
-                if (ClientIP != s)
-                {
-                    location += "|" + s.GetIPLocation();
-                }
-            }
-
-            switch (post.LimitMode)
-            {
-                case RegionLimitMode.OnlyForSearchEngine:
-                    Disallow(post);
-                    break;
-
-                case RegionLimitMode.AllowRegion:
-                    if (!Regex.IsMatch(location, post.Regions, RegexOptions.IgnoreCase))
-                    {
-                        Disallow(post);
-                    }
-
-                    break;
-
-                case RegionLimitMode.ForbidRegion:
-                    if (Regex.IsMatch(location, post.Regions, RegexOptions.IgnoreCase))
-                    {
-                        Disallow(post);
-                    }
-
-                    break;
-
-                case RegionLimitMode.AllowRegionExceptForbidRegion:
-                    if (Regex.IsMatch(location, post.ExceptRegions, RegexOptions.IgnoreCase))
-                    {
-                        Disallow(post);
-                    }
-
-                    goto case RegionLimitMode.AllowRegion;
-                case RegionLimitMode.ForbidRegionExceptAllowRegion:
-                    if (Regex.IsMatch(location, post.ExceptRegions, RegexOptions.IgnoreCase))
-                    {
-                        break;
-                    }
-
-                    goto case RegionLimitMode.ForbidRegion;
-            }
-
-            if (HideCategories.Contains(post.CategoryId))
-            {
-                throw new NotFoundException("文章未找到");
-            }
-        }
-
-        private void Disallow(Post post)
-        {
-            var remark = "无权限查看该文章";
-            if (Request.Cookies.TryGetValue(SessionKey.RawIP, out var rawip) && ClientIP != rawip.Base64Decrypt())
-            {
-                remark += ",发生了IP切换,原始IP:" + rawip.Base64Decrypt();
-            }
-
-            RedisHelper.IncrBy("interceptCount", 1);
-            RedisHelper.LPush("intercept", new IpIntercepter()
-            {
-                IP = ClientIP,
-                RequestUrl = $"//{Request.Host}/{post.Id}",
-                Referer = Request.Headers[HeaderNames.Referer],
-                Time = DateTime.Now,
-                UserAgent = Request.Headers[HeaderNames.UserAgent],
-                Remark = remark,
-                Address = Request.Location(),
-                HttpVersion = Request.Protocol,
-                Headers = new
-                {
-                    Request.Protocol,
-                    Request.Headers
-                }.ToJsonString()
-            });
-            throw new NotFoundException("文章未找到");
-        }
-    }
+			if (CommonHelper.SystemSettings.GetOrAdd("CloseSite", "false") == "true" && user?.IsAdmin != true)
+			{
+				filterContext.Result = RedirectToAction("ComingSoon", "Error");
+				return Task.CompletedTask;
+			}
+
+			if (Request.Method == HttpMethods.Post && !Request.Path.Value.Contains("get", StringComparison.InvariantCultureIgnoreCase) && CommonHelper.SystemSettings.GetOrAdd("DataReadonly", "false") == "true" && !filterContext.Filters.Any(m => m.ToString().Contains(nameof(MyAuthorizeAttribute))))
+			{
+				filterContext.Result = ResultData("网站当前处于数据写保护状态,无法提交任何数据,如有疑问请联系网站管理员!", false, "网站当前处于数据写保护状态,无法提交任何数据,如有疑问请联系网站管理员!", user != null, HttpStatusCode.BadRequest);
+				return Task.CompletedTask;
+			}
+
+			if (user == null && Request.Cookies.ContainsKey("username") && Request.Cookies.ContainsKey("password")) //执行自动登录
+			{
+				var name = Request.Cookies["username"];
+				var pwd = Request.Cookies["password"];
+				var userInfo = UserInfoService.Login(name, pwd.DesDecrypt(AppConfig.BaiduAK));
+				if (userInfo != null)
+				{
+					Response.Cookies.Append("username", name, new CookieOptions
+					{
+						Expires = DateTime.Now.AddYears(1),
+						SameSite = SameSiteMode.Lax
+					});
+					Response.Cookies.Append("password", pwd, new CookieOptions
+					{
+						Expires = DateTime.Now.AddYears(1),
+						SameSite = SameSiteMode.Lax
+					});
+					filterContext.HttpContext.Session.Set(SessionKey.UserInfo, userInfo);
+				}
+			}
+
+			if (ModelState.IsValid) return next();
+			var errmsgs = ModelState.SelectMany(kv => kv.Value.Errors.Select(e => e.ErrorMessage)).Select((s, i) => $"{i + 1}. {s}").ToList();
+			filterContext.Result = true switch
+			{
+				_ when Request.HasJsonContentType() || Request.Method == HttpMethods.Post => ResultData(errmsgs, false, "数据校验失败,错误信息:" + errmsgs.Join(" | "), user != null, HttpStatusCode.BadRequest),
+				_ => base.BadRequest("参数错误:" + errmsgs.Join(" | "))
+			};
+			return Task.CompletedTask;
+		}
+
+		/// <summary>
+		/// 验证邮箱验证码
+		/// </summary>
+		/// <param name="mailSender"></param>
+		/// <param name="email">邮箱地址</param>
+		/// <param name="code">验证码</param>
+		/// <returns></returns>
+		internal string ValidateEmailCode(IMailSender mailSender, string email, string code)
+		{
+			if (CurrentUser.IsAdmin)
+			{
+				return string.Empty; ;
+			}
+
+			if (string.IsNullOrEmpty(Request.Cookies["ValidateKey"]))
+			{
+				if (string.IsNullOrEmpty(code))
+				{
+					return "请输入验证码!";
+				}
+				if (RedisHelper.Get("code:" + email) != code)
+				{
+					return "验证码错误!";
+				}
+			}
+			else if (Request.Cookies["ValidateKey"].DesDecrypt(AppConfig.BaiduAK) != email)
+			{
+				Response.Cookies.Delete("Email");
+				Response.Cookies.Delete("NickName");
+				Response.Cookies.Delete("ValidateKey");
+				return "邮箱验证信息已失效,请刷新页面后重新评论!";
+			}
+
+			if (mailSender.HasBounced(email))
+			{
+				Response.Cookies.Delete("Email");
+				Response.Cookies.Delete("NickName");
+				Response.Cookies.Delete("ValidateKey");
+				return "邮箱地址错误,请刷新页面后重新使用有效的邮箱地址!";
+			}
+
+			return string.Empty;
+		}
+
+		internal void WriteEmailKeyCookie(string email)
+		{
+			Response.Cookies.Append("Email", email, new CookieOptions()
+			{
+				Expires = DateTimeOffset.Now.AddYears(1),
+				SameSite = SameSiteMode.Lax
+			});
+			Response.Cookies.Append("ValidateKey", email.DesEncrypt(AppConfig.BaiduAK), new CookieOptions()
+			{
+				Expires = DateTimeOffset.Now.AddYears(1),
+				SameSite = SameSiteMode.Lax
+			});
+		}
+
+		protected void CheckPermission(List<PostDto> posts)
+		{
+			if (CurrentUser.IsAdmin || Request.IsRobot())
+			{
+				return;
+			}
+
+			posts.RemoveAll(p => p.LimitMode == RegionLimitMode.OnlyForSearchEngine);
+			if (VisitorTokenValid || CommonHelper.IPWhiteList.Contains(ClientIP))
+			{
+				return;
+			}
+
+			var ipLocation = Request.Location();
+			var location = ipLocation + ipLocation.Coodinate + "|" + Request.Headers[HeaderNames.Referer] + "|" + Request.Headers[HeaderNames.UserAgent];
+			if (Request.Cookies.TryGetValue(SessionKey.RawIP, out var rawip))
+			{
+				var s = rawip.Base64Decrypt();
+				if (ClientIP != s)
+				{
+					location += "|" + s.GetIPLocation();
+				}
+			}
+
+			posts.RemoveAll(p =>
+			{
+				switch (p.LimitMode)
+				{
+					case RegionLimitMode.AllowRegion:
+						return !Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase);
+
+					case RegionLimitMode.ForbidRegion:
+						return Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase);
+
+					case RegionLimitMode.AllowRegionExceptForbidRegion:
+						if (Regex.IsMatch(location, p.ExceptRegions, RegexOptions.IgnoreCase))
+						{
+							return true;
+						}
+
+						goto case RegionLimitMode.AllowRegion;
+					case RegionLimitMode.ForbidRegionExceptAllowRegion:
+						if (Regex.IsMatch(location, p.ExceptRegions, RegexOptions.IgnoreCase))
+						{
+							return false;
+						}
+
+						goto case RegionLimitMode.ForbidRegion;
+					default:
+						return false;
+				}
+			});
+			posts.RemoveAll(p => HideCategories.Contains(p.CategoryId));
+		}
+
+		protected Expression<Func<Post, bool>> PostBaseWhere()
+		{
+			if (CurrentUser.IsAdmin || Request.IsRobot())
+			{
+				return _ => true;
+			}
+
+			Expression<Func<Post, bool>> where = p => p.Status == Status.Published && p.LimitMode != RegionLimitMode.OnlyForSearchEngine;
+			if (HideCategories.Length > 0)
+			{
+				where = where.And(p => !HideCategories.Contains(p.CategoryId));
+			}
+
+			if (VisitorTokenValid || CommonHelper.IPWhiteList.Contains(ClientIP))
+			{
+				return where;
+			}
+
+			var ipLocation = Request.Location();
+			var location = ipLocation + ipLocation.Coodinate + "|" + Request.Headers[HeaderNames.Referer] + "|" + Request.Headers[HeaderNames.UserAgent];
+			if (Request.Cookies.TryGetValue(SessionKey.RawIP, out var rawip) && ClientIP != rawip)
+			{
+				var s = rawip.Base64Decrypt();
+				if (ClientIP != s)
+				{
+					location += "|" + s.GetIPLocation();
+				}
+			}
+
+			return where.And(p => p.LimitMode == null || p.LimitMode == RegionLimitMode.All ? true :
+				   p.LimitMode == RegionLimitMode.AllowRegion ? Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) :
+				   p.LimitMode == RegionLimitMode.ForbidRegion ? !Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) :
+				   p.LimitMode == RegionLimitMode.AllowRegionExceptForbidRegion ? Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) && !Regex.IsMatch(location, p.ExceptRegions, RegexOptions.IgnoreCase) : !Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) || Regex.IsMatch(location, p.ExceptRegions, RegexOptions.IgnoreCase));
+		}
+
+		protected void CheckPermission(Post post)
+		{
+			if (CurrentUser.IsAdmin || VisitorTokenValid || Request.IsRobot() || CommonHelper.IPWhiteList.Contains(ClientIP))
+			{
+				return;
+			}
+
+			var ipLocation = Request.Location();
+			var location = ipLocation + ipLocation.Coodinate + "|" + Request.Headers[HeaderNames.Referer] + "|" + Request.Headers[HeaderNames.UserAgent];
+			if (Request.Cookies.TryGetValue(SessionKey.RawIP, out var rawip) && ClientIP != rawip)
+			{
+				var s = rawip.Base64Decrypt();
+				if (ClientIP != s)
+				{
+					location += "|" + s.GetIPLocation();
+				}
+			}
+
+			switch (post.LimitMode)
+			{
+				case RegionLimitMode.OnlyForSearchEngine:
+					Disallow(post);
+					break;
+
+				case RegionLimitMode.AllowRegion:
+					if (!Regex.IsMatch(location, post.Regions, RegexOptions.IgnoreCase))
+					{
+						Disallow(post);
+					}
+
+					break;
+
+				case RegionLimitMode.ForbidRegion:
+					if (Regex.IsMatch(location, post.Regions, RegexOptions.IgnoreCase))
+					{
+						Disallow(post);
+					}
+
+					break;
+
+				case RegionLimitMode.AllowRegionExceptForbidRegion:
+					if (Regex.IsMatch(location, post.ExceptRegions, RegexOptions.IgnoreCase))
+					{
+						Disallow(post);
+					}
+
+					goto case RegionLimitMode.AllowRegion;
+				case RegionLimitMode.ForbidRegionExceptAllowRegion:
+					if (Regex.IsMatch(location, post.ExceptRegions, RegexOptions.IgnoreCase))
+					{
+						break;
+					}
+
+					goto case RegionLimitMode.ForbidRegion;
+			}
+
+			if (HideCategories.Contains(post.CategoryId))
+			{
+				throw new NotFoundException("文章未找到");
+			}
+		}
+
+		private void Disallow(Post post)
+		{
+			var remark = "无权限查看该文章";
+			if (Request.Cookies.TryGetValue(SessionKey.RawIP, out var rawip) && ClientIP != rawip.Base64Decrypt())
+			{
+				remark += ",发生了IP切换,原始IP:" + rawip.Base64Decrypt();
+			}
+
+			RedisHelper.IncrBy("interceptCount", 1);
+			RedisHelper.LPush("intercept", new IpIntercepter()
+			{
+				IP = ClientIP,
+				RequestUrl = $"//{Request.Host}/{post.Id}",
+				Referer = Request.Headers[HeaderNames.Referer],
+				Time = DateTime.Now,
+				UserAgent = Request.Headers[HeaderNames.UserAgent],
+				Remark = remark,
+				Address = Request.Location(),
+				HttpVersion = Request.Protocol,
+				Headers = new
+				{
+					Request.Protocol,
+					Request.Headers
+				}.ToJsonString()
+			});
+			throw new NotFoundException("文章未找到");
+		}
+	}
 }

+ 68 - 69
src/Masuit.MyBlogs.Core/Controllers/CategoryController.cs

@@ -1,5 +1,4 @@
-using Collections.Pooled;
-using Masuit.MyBlogs.Core.Common;
+using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
 using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
 using Masuit.MyBlogs.Core.Models.Command;
@@ -14,76 +13,76 @@ using Z.EntityFramework.Plus;
 
 namespace Masuit.MyBlogs.Core.Controllers
 {
-    /// <summary>
-    /// 文章分类
-    /// </summary>
-    public class CategoryController : BaseController
-    {
-        /// <summary>
-        /// CategoryService
-        /// </summary>
-        public ICategoryService CategoryService { get; set; }
+	/// <summary>
+	/// 文章分类
+	/// </summary>
+	public class CategoryController : BaseController
+	{
+		/// <summary>
+		/// CategoryService
+		/// </summary>
+		public ICategoryService CategoryService { get; set; }
 
-        /// <summary>
-        /// 获取所有分类
-        /// </summary>
-        /// <returns></returns>
-        public ActionResult GetCategories()
-        {
-            using var categories = CategoryService.GetQueryNoTracking(c => c.Status == Status.Available, c => c.Name).ToPooledList();
-            var list = categories.ToTree(c => c.Id, c => c.ParentId);
-            return ResultData(list.Mapper<List<CategoryDto>>());
-        }
+		/// <summary>
+		/// 获取所有分类
+		/// </summary>
+		/// <returns></returns>
+		public ActionResult GetCategories()
+		{
+			var categories = CategoryService.GetQueryNoTracking(c => c.Status == Status.Available, c => c.Name).ToList();
+			var list = categories.ToTree(c => c.Id, c => c.ParentId);
+			return ResultData(list.Mapper<List<CategoryDto>>());
+		}
 
-        /// <summary>
-        /// 获取分类详情
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        public async Task<ActionResult> Get(int id)
-        {
-            var model = await CategoryService.GetByIdAsync(id) ?? throw new NotFoundException("分类不存在!");
-            return ResultData(model.Mapper<CategoryDto>());
-        }
+		/// <summary>
+		/// 获取分类详情
+		/// </summary>
+		/// <param name="id"></param>
+		/// <returns></returns>
+		public async Task<ActionResult> Get(int id)
+		{
+			var model = await CategoryService.GetByIdAsync(id) ?? throw new NotFoundException("分类不存在!");
+			return ResultData(model.Mapper<CategoryDto>());
+		}
 
-        /// <summary>
-        /// 保存分类
-        /// </summary>
-        /// <param name="cmd"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> Save([FromBodyOrDefault] CategoryCommand cmd)
-        {
-            var cat = await CategoryService.GetByIdAsync(cmd.Id);
-            if (cat == null)
-            {
-                var category = Mapper.Map<Category>(cmd);
-                category.Path = cmd.ParentId > 0 ? (CategoryService[cmd.ParentId.Value].Path + "," + cmd.ParentId).Trim(',') : SnowFlake.NewId;
-                var b1 = await CategoryService.AddEntitySavedAsync(category) > 0;
-                return ResultData(null, b1, b1 ? "分类添加成功!" : "分类添加失败!");
-            }
+		/// <summary>
+		/// 保存分类
+		/// </summary>
+		/// <param name="cmd"></param>
+		/// <returns></returns>
+		[MyAuthorize]
+		public async Task<ActionResult> Save([FromBodyOrDefault] CategoryCommand cmd)
+		{
+			var cat = await CategoryService.GetByIdAsync(cmd.Id);
+			if (cat == null)
+			{
+				var category = Mapper.Map<Category>(cmd);
+				category.Path = cmd.ParentId > 0 ? (CategoryService[cmd.ParentId.Value].Path + "," + cmd.ParentId).Trim(',') : SnowFlake.NewId;
+				var b1 = await CategoryService.AddEntitySavedAsync(category) > 0;
+				return ResultData(null, b1, b1 ? "分类添加成功!" : "分类添加失败!");
+			}
 
-            cat.Name = cmd.Name;
-            cat.Description = cmd.Description;
-            cat.ParentId = cmd.ParentId;
-            cat.Path = cmd.ParentId > 0 ? (CategoryService[cmd.ParentId.Value].Path + "," + cmd.ParentId).Trim(',') : SnowFlake.NewId;
-            bool b = await CategoryService.SaveChangesAsync() > 0;
-            QueryCacheManager.ExpireType<Category>();
-            return ResultData(null, b, b ? "分类修改成功!" : "分类修改失败!");
-        }
+			cat.Name = cmd.Name;
+			cat.Description = cmd.Description;
+			cat.ParentId = cmd.ParentId;
+			cat.Path = cmd.ParentId > 0 ? (CategoryService[cmd.ParentId.Value].Path + "," + cmd.ParentId).Trim(',') : SnowFlake.NewId;
+			bool b = await CategoryService.SaveChangesAsync() > 0;
+			QueryCacheManager.ExpireType<Category>();
+			return ResultData(null, b, b ? "分类修改成功!" : "分类修改失败!");
+		}
 
-        /// <summary>
-        /// 删除分类
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="cid"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> Delete(int id, int cid = 1)
-        {
-            bool b = await CategoryService.Delete(id, cid);
-            QueryCacheManager.ExpireType<Category>();
-            return ResultData(null, b, b ? "分类删除成功" : "分类删除失败");
-        }
-    }
+		/// <summary>
+		/// 删除分类
+		/// </summary>
+		/// <param name="id"></param>
+		/// <param name="cid"></param>
+		/// <returns></returns>
+		[MyAuthorize]
+		public async Task<ActionResult> Delete(int id, int cid = 1)
+		{
+			bool b = await CategoryService.Delete(id, cid);
+			QueryCacheManager.ExpireType<Category>();
+			return ResultData(null, b, b ? "分类删除成功" : "分类删除失败");
+		}
+	}
 }

+ 293 - 294
src/Masuit.MyBlogs.Core/Controllers/CommentController.cs

@@ -15,334 +15,333 @@ using Masuit.Tools.Html;
 using Masuit.Tools.Logging;
 using Masuit.Tools.Models;
 using Masuit.Tools.Strings;
+using Masuit.Tools.Systems;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
 using Microsoft.Net.Http.Headers;
 using System.ComponentModel.DataAnnotations;
 using System.Text;
 using System.Text.RegularExpressions;
-using Masuit.Tools.Systems;
-using Microsoft.EntityFrameworkCore;
 using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
-using Collections.Pooled;
 
 namespace Masuit.MyBlogs.Core.Controllers
 {
-    /// <summary>
-    /// 评论管理
-    /// </summary>
-    public class CommentController : BaseController
-    {
-        public ICommentService CommentService { get; set; }
+	/// <summary>
+	/// 评论管理
+	/// </summary>
+	public class CommentController : BaseController
+	{
+		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, "当前评论过于久远,不再允许回复!");
+			}
 
-            Post post = await PostService.GetByIdAsync(cmd.PostId) ?? throw new NotFoundException("评论失败,文章未找到");
-            if (post.DisableComment)
-            {
-                return ResultData(null, false, "本文已禁用评论功能,不允许任何人回复!");
-            }
+			Post post = await PostService.GetByIdAsync(cmd.PostId) ?? throw new NotFoundException("评论失败,文章未找到");
+			if (post.DisableComment)
+			{
+				return ResultData(null, false, "本文已禁用评论功能,不允许任何人回复!");
+			}
 
-            cmd.Content = cmd.Content.Trim().Replace("<p><br></p>", string.Empty);
-            if (CommentFeq.GetOrAdd("Comments:" + ClientIP, 1) > 2)
-            {
-                CommentFeq.Expire("Comments:" + ClientIP, TimeSpan.FromMinutes(1));
-                return ResultData(null, false, "您的发言频率过快,请稍后再发表吧!");
-            }
+			cmd.Content = cmd.Content.Trim().Replace("<p><br></p>", string.Empty);
+			if (CommentFeq.GetOrAdd("Comments:" + ClientIP, 1) > 2)
+			{
+				CommentFeq.Expire("Comments:" + ClientIP, TimeSpan.FromMinutes(1));
+				return ResultData(null, false, "您的发言频率过快,请稍后再发表吧!");
+			}
 
-            var comment = cmd.Mapper<Comment>();
-            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 = cmd.Mapper<Comment>();
+			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 = ClientIP;
-            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.HtmlSantinizerStandard().ClearImgAttributes();
+			comment.Browser = cmd.Browser ?? Request.Headers[HeaderNames.UserAgent];
+			comment.IP = ClientIP;
+			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:" + ClientIP, 1, i => i + 1, 5);
-            CommentFeq.Expire("Comments:" + ClientIP, 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:" + ClientIP, 1, i => i + 1, 5);
+			CommentFeq.Expire("Comments:" + ClientIP, 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(() => 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));
-                    }
-                }
-                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(() => CommonHelper.SendMail($"{Request.Host}{CommonHelper.SystemSettings["Title"]}文章评论回复:", content.Set("link", link).Render(false), s, ClientIP));
-                    }
-                }
-                return ResultData(null, true, "评论发表成功,服务器正在后台处理中,这会有一定的延迟,稍后将显示到评论列表中");
-            }
+					//新评论,只通知博主和楼主
+					foreach (var s in emails)
+					{
+						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));
+					}
+				}
+				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(() => CommonHelper.SendMail($"{Request.Host}{CommonHelper.SystemSettings["Title"]}文章评论回复:", content.Set("link", link).Render(false), s, ClientIP));
+					}
+				}
+				return ResultData(null, true, "评论发表成功,服务器正在后台处理中,这会有一定的延迟,稍后将显示到评论列表中");
+			}
 
-            foreach (var s in emails)
-            {
-                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));
-            }
+			foreach (var s in emails)
+			{
+				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));
+			}
 
-            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("评论未找到");
-                using var layer = CommentService.GetQueryNoTracking(c => c.GroupTag == comment.GroupTag).ToPooledList();
-                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).ToList();
+				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 = layer.ToTree(c => c.Id, c => c.ParentId).Mapper<IList<CommentViewModel>>()
-                });
-            }
+				return ResultData(new
+				{
+					total = 1,
+					parentTotal = 1,
+					page,
+					size,
+					rows = layer.ToTree(c => c.Id, c => c.ParentId).Mapper<IList<CommentViewModel>>()
+				});
+			}
 
-            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();
-            using var comments = CommentService.GetQuery(c => tags.Contains(c.GroupTag)).Include(c => c.Post).AsNoTracking().ToPooledList();
-            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 = comments.OrderByDescending(c => c.CommentDate).ToTree(c => c.Id, c => c.ParentId).Mapper<IList<CommentViewModel>>()
-                });
-            }
+			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().ToList();
+			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 = comments.OrderByDescending(c => c.CommentDate).ToTree(c => c.Id, c => c.ParentId).Mapper<IList<CommentViewModel>>()
+				});
+			}
 
-            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);
-                using var emails = CommentService.GetQuery(c => c.GroupTag == comment.GroupTag).Select(c => c.Email).Distinct().ToPooledList().Append(post.ModifierEmail).Except(new List<string> { comment.Email, CurrentUser.Email }).ToPooledSet();
-                var link = Url.Action("Details", "Post", new
-                {
-                    id = comment.PostId,
-                    cid = id
-                }, Request.Scheme) + "#comment";
-                foreach (var email in emails)
-                {
-                    BackgroundJob.Enqueue(() => CommonHelper.SendMail($"{Request.Host}{CommonHelper.SystemSettings["Title"]}文章评论回复:", content.Set("link", link).Render(false), email, ClientIP));
-                }
+		/// <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().ToList().Append(post.ModifierEmail).Except(new List<string> { comment.Email, CurrentUser.Email }).ToHashSet();
+				var link = Url.Action("Details", "Post", new
+				{
+					id = comment.PostId,
+					cid = id
+				}, Request.Scheme) + "#comment";
+				foreach (var email in emails)
+				{
+					BackgroundJob.Enqueue(() => CommonHelper.SendMail($"{Request.Host}{CommonHelper.SystemSettings["Title"]}文章评论回复:", content.Set("link", link).Render(false), email, ClientIP));
+				}
 
-                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);
+		}
+	}
 }

+ 288 - 289
src/Masuit.MyBlogs.Core/Controllers/HomeController.cs

@@ -1,6 +1,5 @@
 using AngleSharp;
 using AutoMapper.QueryableExtensions;
-using Collections.Pooled;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
 using Masuit.MyBlogs.Core.Infrastructure.Repository;
@@ -34,315 +33,315 @@ namespace Masuit.MyBlogs.Core.Controllers;
 /// </summary>
 public class HomeController : BaseController
 {
-    /// <summary>
-    /// 文章
-    /// </summary>
-    public IPostService PostService { get; set; }
+	/// <summary>
+	/// 文章
+	/// </summary>
+	public IPostService PostService { get; set; }
 
-    /// <summary>
-    /// 分类
-    /// </summary>
-    public ICategoryService CategoryService { get; set; }
+	/// <summary>
+	/// 分类
+	/// </summary>
+	public ICategoryService CategoryService { get; set; }
 
-    /// <summary>
-    /// 网站公告
-    /// </summary>
-    public INoticeService NoticeService { get; set; }
+	/// <summary>
+	/// 网站公告
+	/// </summary>
+	public INoticeService NoticeService { get; set; }
 
-    /// <summary>
-    /// 首页
-    /// </summary>
-    /// <returns></returns>
-    [ResponseCache(Duration = 600, VaryByHeader = nameof(HeaderNames.Cookie))]
-    public async Task<ActionResult> Index([FromServices] IFastShareService fastShareService)
-    {
-        var banners = AdsService.GetsByWeightedPrice(8, AdvertiseType.Banner, Request.Location()).OrderByRandom().ToList();
-        var fastShares = fastShareService.GetAllFromCache(s => s.Sort);
-        var postsQuery = PostService.GetQuery(PostBaseWhere()); //准备文章的查询
-        var posts = await postsQuery.Where(p => !p.IsFixedTop).OrderBy(OrderBy.ModifyDate.GetDisplay() + " desc").ToPagedListAsync<Post, PostDto>(1, 15, MapperConfig);
-        posts.Data.InsertRange(0, postsQuery.Where(p => p.IsFixedTop).OrderByDescending(p => p.ModifyDate).ProjectTo<PostDto>(MapperConfig).FromCache().ToList());
-        var viewModel = GetIndexPageViewModel();
-        viewModel.Banner = banners;
-        viewModel.Posts = posts;
-        ViewBag.FastShare = fastShares;
-        viewModel.PageParams = new Pagination(1, 15, posts.TotalCount, OrderBy.ModifyDate);
-        viewModel.SidebarAds = AdsService.GetsByWeightedPrice(2, AdvertiseType.SideBar, Request.Location());
-        viewModel.ListAdvertisement = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location());
-        PostService.SolvePostsCategory(posts.Data);
-        foreach (var item in posts.Data)
-        {
-            item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-        }
+	/// <summary>
+	/// 首页
+	/// </summary>
+	/// <returns></returns>
+	[ResponseCache(Duration = 600, VaryByHeader = nameof(HeaderNames.Cookie))]
+	public async Task<ActionResult> Index([FromServices] IFastShareService fastShareService)
+	{
+		var banners = AdsService.GetsByWeightedPrice(8, AdvertiseType.Banner, Request.Location()).OrderByRandom().ToList();
+		var fastShares = fastShareService.GetAllFromCache(s => s.Sort);
+		var postsQuery = PostService.GetQuery(PostBaseWhere()); //准备文章的查询
+		var posts = await postsQuery.Where(p => !p.IsFixedTop).OrderBy(OrderBy.ModifyDate.GetDisplay() + " desc").ToPagedListAsync<Post, PostDto>(1, 15, MapperConfig);
+		posts.Data.InsertRange(0, postsQuery.Where(p => p.IsFixedTop).OrderByDescending(p => p.ModifyDate).ProjectTo<PostDto>(MapperConfig).FromCache().ToList());
+		var viewModel = GetIndexPageViewModel();
+		viewModel.Banner = banners;
+		viewModel.Posts = posts;
+		ViewBag.FastShare = fastShares;
+		viewModel.PageParams = new Pagination(1, 15, posts.TotalCount, OrderBy.ModifyDate);
+		viewModel.SidebarAds = AdsService.GetsByWeightedPrice(2, AdvertiseType.SideBar, Request.Location());
+		viewModel.ListAdvertisement = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location());
+		PostService.SolvePostsCategory(posts.Data);
+		foreach (var item in posts.Data)
+		{
+			item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		}
 
-        return View(viewModel);
-    }
+		return View(viewModel);
+	}
 
-    /// <summary>
-    /// 文章列表页
-    /// </summary>
-    /// <param name="page"></param>
-    /// <param name="size"></param>
-    /// <param name="orderBy"></param>
-    /// <returns></returns>
-    [Route("posts"), Route("p", Order = 1), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size", "orderBy" }, VaryByHeader = nameof(HeaderNames.Cookie))]
-    public async Task<ActionResult> Post([Optional] OrderBy? orderBy, int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
-    {
-        page = Math.Max(1, page);
-        var viewModel = GetIndexPageViewModel();
-        var postsQuery = PostService.GetQuery(PostBaseWhere()); //准备文章的查询
-        var h24 = DateTime.Today.AddDays(-1);
-        var posts = orderBy switch
-        {
-            OrderBy.Trending => await postsQuery.Where(p => !p.IsFixedTop).OrderByDescending(p => p.PostVisitRecordStats.Where(e => e.Date >= h24).Sum(t => t.Count)).ToPagedListAsync<Post, PostDto>(page, size, MapperConfig),
-            _ => await postsQuery.Where(p => !p.IsFixedTop).OrderBy((orderBy ?? OrderBy.ModifyDate).GetDisplay() + " desc").ToPagedListAsync<Post, PostDto>(page, size, MapperConfig)
-        };
-        if (page == 1)
-        {
-            posts.Data.InsertRange(0, postsQuery.Where(p => p.IsFixedTop).OrderByDescending(p => p.ModifyDate).ProjectTo<PostDto>(MapperConfig).ToList());
-        }
+	/// <summary>
+	/// 文章列表页
+	/// </summary>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <param name="orderBy"></param>
+	/// <returns></returns>
+	[Route("posts"), Route("p", Order = 1), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size", "orderBy" }, VaryByHeader = nameof(HeaderNames.Cookie))]
+	public async Task<ActionResult> Post([Optional] OrderBy? orderBy, int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
+	{
+		page = Math.Max(1, page);
+		var viewModel = GetIndexPageViewModel();
+		var postsQuery = PostService.GetQuery(PostBaseWhere()); //准备文章的查询
+		var h24 = DateTime.Today.AddDays(-1);
+		var posts = orderBy switch
+		{
+			OrderBy.Trending => await postsQuery.Where(p => !p.IsFixedTop).OrderByDescending(p => p.PostVisitRecordStats.Where(e => e.Date >= h24).Sum(t => t.Count)).ToPagedListAsync<Post, PostDto>(page, size, MapperConfig),
+			_ => await postsQuery.Where(p => !p.IsFixedTop).OrderBy((orderBy ?? OrderBy.ModifyDate).GetDisplay() + " desc").ToPagedListAsync<Post, PostDto>(page, size, MapperConfig)
+		};
+		if (page == 1)
+		{
+			posts.Data.InsertRange(0, postsQuery.Where(p => p.IsFixedTop).OrderByDescending(p => p.ModifyDate).ProjectTo<PostDto>(MapperConfig).ToList());
+		}
 
-        viewModel.Posts = posts;
-        viewModel.PageParams = new Pagination(page, size, posts.TotalCount, orderBy);
-        viewModel.SidebarAds = AdsService.GetsByWeightedPrice(2, AdvertiseType.SideBar, Request.Location());
-        viewModel.ListAdvertisement = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location());
-        PostService.SolvePostsCategory(posts.Data);
-        foreach (var item in posts.Data)
-        {
-            item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-        }
+		viewModel.Posts = posts;
+		viewModel.PageParams = new Pagination(page, size, posts.TotalCount, orderBy);
+		viewModel.SidebarAds = AdsService.GetsByWeightedPrice(2, AdvertiseType.SideBar, Request.Location());
+		viewModel.ListAdvertisement = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location());
+		PostService.SolvePostsCategory(posts.Data);
+		foreach (var item in posts.Data)
+		{
+			item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		}
 
-        return View(viewModel);
-    }
+		return View(viewModel);
+	}
 
-    /// <summary>
-    /// 标签文章页
-    /// </summary>
-    /// <param name="tag"></param>
-    /// <param name="page"></param>
-    /// <param name="size"></param>
-    /// <param name="orderBy"></param>
-    /// <returns></returns>
-    [Route("tag/{tag}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size", "orderBy" }, VaryByHeader = nameof(HeaderNames.Cookie))]
-    public async Task<ActionResult> Tag(string tag, [Optional] OrderBy? orderBy, int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
-    {
-        page = Math.Max(1, page);
-        if (string.IsNullOrWhiteSpace(tag))
-        {
-            throw new NotFoundException("");
-        }
+	/// <summary>
+	/// 标签文章页
+	/// </summary>
+	/// <param name="tag"></param>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <param name="orderBy"></param>
+	/// <returns></returns>
+	[Route("tag/{tag}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size", "orderBy" }, VaryByHeader = nameof(HeaderNames.Cookie))]
+	public async Task<ActionResult> Tag(string tag, [Optional] OrderBy? orderBy, int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
+	{
+		page = Math.Max(1, page);
+		if (string.IsNullOrWhiteSpace(tag))
+		{
+			throw new NotFoundException("");
+		}
 
-        var where = PostBaseWhere();
-        var queryable = PostService.GetQuery(tag.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(s => Regex.Escape(s.Trim())).Aggregate(where, (current, s) => current.And(p => Regex.IsMatch(p.Label, s, RegexOptions.IgnoreCase))));
-        var h24 = DateTime.Today.AddDays(-1);
-        var posts = orderBy switch
-        {
-            OrderBy.Trending => await queryable.OrderByDescending(p => p.PostVisitRecordStats.Where(e => e.Date >= h24).Sum(e => e.Count)).ToPagedListAsync<Post, PostDto>(page, size, MapperConfig),
-            _ => await queryable.OrderBy($"{nameof(PostDto.IsFixedTop)} desc,{(orderBy ?? OrderBy.ModifyDate).GetDisplay()} desc").ToPagedListAsync<Post, PostDto>(page, size, MapperConfig)
-        };
-        var viewModel = GetIndexPageViewModel();
-        ViewBag.Tag = tag;
-        viewModel.Posts = posts;
-        viewModel.PageParams = new Pagination(page, size, posts.TotalCount, orderBy);
-        viewModel.SidebarAds = AdsService.GetsByWeightedPrice(2, AdvertiseType.SideBar, Request.Location(), keywords: tag);
-        viewModel.ListAdvertisement = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location(), keywords: tag);
-        PostService.SolvePostsCategory(posts.Data);
-        foreach (var item in posts.Data)
-        {
-            item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-        }
+		var where = PostBaseWhere();
+		var queryable = PostService.GetQuery(tag.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(s => Regex.Escape(s.Trim())).Aggregate(where, (current, s) => current.And(p => Regex.IsMatch(p.Label, s, RegexOptions.IgnoreCase))));
+		var h24 = DateTime.Today.AddDays(-1);
+		var posts = orderBy switch
+		{
+			OrderBy.Trending => await queryable.OrderByDescending(p => p.PostVisitRecordStats.Where(e => e.Date >= h24).Sum(e => e.Count)).ToPagedListAsync<Post, PostDto>(page, size, MapperConfig),
+			_ => await queryable.OrderBy($"{nameof(PostDto.IsFixedTop)} desc,{(orderBy ?? OrderBy.ModifyDate).GetDisplay()} desc").ToPagedListAsync<Post, PostDto>(page, size, MapperConfig)
+		};
+		var viewModel = GetIndexPageViewModel();
+		ViewBag.Tag = tag;
+		viewModel.Posts = posts;
+		viewModel.PageParams = new Pagination(page, size, posts.TotalCount, orderBy);
+		viewModel.SidebarAds = AdsService.GetsByWeightedPrice(2, AdvertiseType.SideBar, Request.Location(), keywords: tag);
+		viewModel.ListAdvertisement = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location(), keywords: tag);
+		PostService.SolvePostsCategory(posts.Data);
+		foreach (var item in posts.Data)
+		{
+			item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		}
 
-        return View(viewModel);
-    }
+		return View(viewModel);
+	}
 
-    /// <summary>
-    /// 存档文章页
-    /// </summary>
-    /// <param name="yyyy"></param>
-    /// <param name="mm"></param>
-    /// <param name="dd"></param>
-    /// <param name="page"></param>
-    /// <param name="size"></param>
-    /// <param name="orderBy"></param>
-    /// <returns></returns>
-    [Route("{yyyy:int}/{mm:int}/{dd:int}/{mode}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size", "orderBy" }, VaryByHeader = nameof(HeaderNames.Cookie))]
-    public async Task<ActionResult> Archieve([Range(2010, 2099)] int yyyy, [Range(1, 12)] int mm, [Range(1, 31)] int dd, [Optional] OrderBy? orderBy, int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15, string mode = nameof(Models.Entity.Post.ModifyDate))
-    {
-        page = Math.Max(1, page);
-        if (!DateTime.TryParse(yyyy + "-" + mm + "-" + dd, out var date))
-        {
-            date = DateTime.Today;
-        }
+	/// <summary>
+	/// 存档文章页
+	/// </summary>
+	/// <param name="yyyy"></param>
+	/// <param name="mm"></param>
+	/// <param name="dd"></param>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <param name="orderBy"></param>
+	/// <returns></returns>
+	[Route("{yyyy:int}/{mm:int}/{dd:int}/{mode}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size", "orderBy" }, VaryByHeader = nameof(HeaderNames.Cookie))]
+	public async Task<ActionResult> Archieve([Range(2010, 2099)] int yyyy, [Range(1, 12)] int mm, [Range(1, 31)] int dd, [Optional] OrderBy? orderBy, int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15, string mode = nameof(Models.Entity.Post.ModifyDate))
+	{
+		page = Math.Max(1, page);
+		if (!DateTime.TryParse(yyyy + "-" + mm + "-" + dd, out var date))
+		{
+			date = DateTime.Today;
+		}
 
-        var where = mode switch
-        {
-            nameof(Models.Entity.Post.PostDate) => PostBaseWhere().And(p => p.PostDate.Date == date),
-            _ => PostBaseWhere().And(p => p.ModifyDate.Date == date),
-        };
-        var queryable = PostService.GetQuery(where);
-        var h24 = DateTime.Today.AddDays(-1);
-        var posts = orderBy switch
-        {
-            OrderBy.Trending => await queryable.OrderByDescending(p => p.PostVisitRecordStats.Where(e => e.Date >= h24).Sum(e => e.Count)).ToPagedListAsync<Post, PostDto>(page, size, MapperConfig),
-            _ => await queryable.OrderBy($"{nameof(PostDto.IsFixedTop)} desc,{(orderBy ?? OrderBy.ModifyDate).GetDisplay()} desc").ToPagedListAsync<Post, PostDto>(page, size, MapperConfig)
-        };
-        var viewModel = GetIndexPageViewModel();
-        viewModel.Posts = posts;
-        viewModel.PageParams = new Pagination(page, size, posts.TotalCount, orderBy);
-        viewModel.SidebarAds = AdsService.GetsByWeightedPrice(2, AdvertiseType.SideBar, Request.Location());
-        viewModel.ListAdvertisement = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location());
-        PostService.SolvePostsCategory(posts.Data);
-        foreach (var item in posts.Data)
-        {
-            item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-        }
+		var where = mode switch
+		{
+			nameof(Models.Entity.Post.PostDate) => PostBaseWhere().And(p => p.PostDate.Date == date),
+			_ => PostBaseWhere().And(p => p.ModifyDate.Date == date),
+		};
+		var queryable = PostService.GetQuery(where);
+		var h24 = DateTime.Today.AddDays(-1);
+		var posts = orderBy switch
+		{
+			OrderBy.Trending => await queryable.OrderByDescending(p => p.PostVisitRecordStats.Where(e => e.Date >= h24).Sum(e => e.Count)).ToPagedListAsync<Post, PostDto>(page, size, MapperConfig),
+			_ => await queryable.OrderBy($"{nameof(PostDto.IsFixedTop)} desc,{(orderBy ?? OrderBy.ModifyDate).GetDisplay()} desc").ToPagedListAsync<Post, PostDto>(page, size, MapperConfig)
+		};
+		var viewModel = GetIndexPageViewModel();
+		viewModel.Posts = posts;
+		viewModel.PageParams = new Pagination(page, size, posts.TotalCount, orderBy);
+		viewModel.SidebarAds = AdsService.GetsByWeightedPrice(2, AdvertiseType.SideBar, Request.Location());
+		viewModel.ListAdvertisement = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location());
+		PostService.SolvePostsCategory(posts.Data);
+		foreach (var item in posts.Data)
+		{
+			item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		}
 
-        return View(viewModel);
-    }
+		return View(viewModel);
+	}
 
-    /// <summary>
-    /// 作者文章页
-    /// </summary>
-    /// <param name="author"></param>
-    /// <param name="page"></param>
-    /// <param name="size"></param>
-    /// <param name="orderBy"></param>
-    /// <returns></returns>
-    [Route("author/{author}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size", "orderBy" }, VaryByHeader = nameof(HeaderNames.Cookie))]
-    public async Task<ActionResult> Author(string author, [Optional] OrderBy? orderBy, int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
-    {
-        page = Math.Max(1, page);
-        Expression<Func<Post, bool>> where = PostBaseWhere().And(p => p.Author.Equals(author) || p.Modifier.Equals(author) || p.Email.Equals(author) || p.PostHistoryVersion.Any(v => v.Modifier.Equals(author) || v.ModifierEmail.Equals(author)));
-        var h24 = DateTime.Today.AddDays(-1);
-        var posts = orderBy switch
-        {
-            OrderBy.Trending => await PostService.GetQuery(where).OrderByDescending(p => p.PostVisitRecordStats.Where(e => e.Date >= h24).Sum(e => e.Count)).ToPagedListAsync<Post, PostDto>(page, size, MapperConfig),
-            _ => await PostService.GetQuery(where).OrderBy($"{nameof(PostDto.IsFixedTop)} desc,{(orderBy ?? OrderBy.ModifyDate).GetDisplay()} desc").ToPagedListAsync<Post, PostDto>(page, size, MapperConfig)
-        };
-        var viewModel = GetIndexPageViewModel();
-        ViewBag.Author = author;
-        viewModel.Posts = posts;
-        viewModel.PageParams = new Pagination(page, size, posts.TotalCount, orderBy);
-        viewModel.SidebarAds = AdsService.GetsByWeightedPrice(2, AdvertiseType.SideBar, Request.Location());
-        viewModel.ListAdvertisement = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location());
-        PostService.SolvePostsCategory(posts.Data);
-        foreach (var item in posts.Data)
-        {
-            item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-        }
+	/// <summary>
+	/// 作者文章页
+	/// </summary>
+	/// <param name="author"></param>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <param name="orderBy"></param>
+	/// <returns></returns>
+	[Route("author/{author}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size", "orderBy" }, VaryByHeader = nameof(HeaderNames.Cookie))]
+	public async Task<ActionResult> Author(string author, [Optional] OrderBy? orderBy, int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
+	{
+		page = Math.Max(1, page);
+		Expression<Func<Post, bool>> where = PostBaseWhere().And(p => p.Author.Equals(author) || p.Modifier.Equals(author) || p.Email.Equals(author) || p.PostHistoryVersion.Any(v => v.Modifier.Equals(author) || v.ModifierEmail.Equals(author)));
+		var h24 = DateTime.Today.AddDays(-1);
+		var posts = orderBy switch
+		{
+			OrderBy.Trending => await PostService.GetQuery(where).OrderByDescending(p => p.PostVisitRecordStats.Where(e => e.Date >= h24).Sum(e => e.Count)).ToPagedListAsync<Post, PostDto>(page, size, MapperConfig),
+			_ => await PostService.GetQuery(where).OrderBy($"{nameof(PostDto.IsFixedTop)} desc,{(orderBy ?? OrderBy.ModifyDate).GetDisplay()} desc").ToPagedListAsync<Post, PostDto>(page, size, MapperConfig)
+		};
+		var viewModel = GetIndexPageViewModel();
+		ViewBag.Author = author;
+		viewModel.Posts = posts;
+		viewModel.PageParams = new Pagination(page, size, posts.TotalCount, orderBy);
+		viewModel.SidebarAds = AdsService.GetsByWeightedPrice(2, AdvertiseType.SideBar, Request.Location());
+		viewModel.ListAdvertisement = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location());
+		PostService.SolvePostsCategory(posts.Data);
+		foreach (var item in posts.Data)
+		{
+			item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		}
 
-        return View(viewModel);
-    }
+		return View(viewModel);
+	}
 
-    /// <summary>
-    /// 分类文章页
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="page"></param>
-    /// <param name="size"></param>
-    /// <param name="orderBy"></param>
-    /// <returns></returns>
-    [Route("cat/{id:int}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size", "orderBy" }, VaryByHeader = nameof(HeaderNames.Cookie))]
-    public async Task<ActionResult> Category(int id, [Optional] OrderBy? orderBy, int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
-    {
-        page = Math.Max(1, page);
-        var cat = await CategoryService.GetByIdAsync(id) ?? throw new NotFoundException("文章分类未找到");
-        var cids = cat.Flatten().Select(c => c.Id).ToArray();
-        var h24 = DateTime.Today.AddDays(-1);
-        var posts = orderBy switch
-        {
-            OrderBy.Trending => await PostService.GetQuery(PostBaseWhere().And(p => cids.Contains(p.CategoryId))).OrderByDescending(p => p.PostVisitRecordStats.Where(e => e.Date >= h24).Sum(e => e.Count)).ToPagedListAsync<Post, PostDto>(page, size, MapperConfig),
-            _ => await PostService.GetQuery(PostBaseWhere().And(p => cids.Contains(p.CategoryId))).OrderBy($"{nameof(PostDto.IsFixedTop)} desc,{(orderBy ?? OrderBy.ModifyDate).GetDisplay()} desc").ToPagedListAsync<Post, PostDto>(page, size, MapperConfig)
-        };
-        var viewModel = GetIndexPageViewModel();
-        viewModel.Posts = posts;
-        ViewBag.Category = cat;
-        viewModel.PageParams = new Pagination(page, size, posts.TotalCount, orderBy);
-        viewModel.SidebarAds = AdsService.GetsByWeightedPrice(2, AdvertiseType.SideBar, Request.Location(), id);
-        viewModel.ListAdvertisement = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location(), id);
-        PostService.SolvePostsCategory(posts.Data);
-        foreach (var item in posts.Data)
-        {
-            item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-        }
+	/// <summary>
+	/// 分类文章页
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <param name="orderBy"></param>
+	/// <returns></returns>
+	[Route("cat/{id:int}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size", "orderBy" }, VaryByHeader = nameof(HeaderNames.Cookie))]
+	public async Task<ActionResult> Category(int id, [Optional] OrderBy? orderBy, int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
+	{
+		page = Math.Max(1, page);
+		var cat = await CategoryService.GetByIdAsync(id) ?? throw new NotFoundException("文章分类未找到");
+		var cids = cat.Flatten().Select(c => c.Id).ToArray();
+		var h24 = DateTime.Today.AddDays(-1);
+		var posts = orderBy switch
+		{
+			OrderBy.Trending => await PostService.GetQuery(PostBaseWhere().And(p => cids.Contains(p.CategoryId))).OrderByDescending(p => p.PostVisitRecordStats.Where(e => e.Date >= h24).Sum(e => e.Count)).ToPagedListAsync<Post, PostDto>(page, size, MapperConfig),
+			_ => await PostService.GetQuery(PostBaseWhere().And(p => cids.Contains(p.CategoryId))).OrderBy($"{nameof(PostDto.IsFixedTop)} desc,{(orderBy ?? OrderBy.ModifyDate).GetDisplay()} desc").ToPagedListAsync<Post, PostDto>(page, size, MapperConfig)
+		};
+		var viewModel = GetIndexPageViewModel();
+		viewModel.Posts = posts;
+		ViewBag.Category = cat;
+		viewModel.PageParams = new Pagination(page, size, posts.TotalCount, orderBy);
+		viewModel.SidebarAds = AdsService.GetsByWeightedPrice(2, AdvertiseType.SideBar, Request.Location(), id);
+		viewModel.ListAdvertisement = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location(), id);
+		PostService.SolvePostsCategory(posts.Data);
+		foreach (var item in posts.Data)
+		{
+			item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		}
 
-        return View(viewModel);
-    }
+		return View(viewModel);
+	}
 
-    /// <summary>
-    /// 切换语言
-    /// </summary>
-    /// <param name="lang"></param>
-    /// <returns></returns>
-    [Route("lang/{lang}")]
-    public ActionResult SetLang(string lang)
-    {
-        Response.Cookies.Append("lang", lang, new CookieOptions()
-        {
-            Expires = DateTime.Now.AddYears(1),
-        });
-        var referer = Request.Headers[HeaderNames.Referer].ToString();
-        return Redirect(string.IsNullOrEmpty(referer) ? "/" : referer);
-    }
+	/// <summary>
+	/// 切换语言
+	/// </summary>
+	/// <param name="lang"></param>
+	/// <returns></returns>
+	[Route("lang/{lang}")]
+	public ActionResult SetLang(string lang)
+	{
+		Response.Cookies.Append("lang", lang, new CookieOptions()
+		{
+			Expires = DateTime.Now.AddYears(1),
+		});
+		var referer = Request.Headers[HeaderNames.Referer].ToString();
+		return Redirect(string.IsNullOrEmpty(referer) ? "/" : referer);
+	}
 
-    /// <summary>
-    /// 站点地图
-    /// </summary>
-    /// <param name="env"></param>
-    /// <param name="ext"></param>
-    /// <returns></returns>
-    [HttpGet("/sitemaps.{ext}")]
-    public async Task<ActionResult> Sitemap([FromServices] IWebHostEnvironment env, string ext)
-    {
-        var sitemap = Path.Combine(env.WebRootPath, "sitemap." + ext);
-        if (System.IO.File.Exists(sitemap))
-        {
-            var fs = new FileInfo(sitemap).ShareReadWrite();
-            switch (ext)
-            {
-                case "txt":
-                    return Content((await fs.ReadAllLinesAsync(Encoding.UTF8)).Select(s => Request.Scheme + "://" + Request.Host.Host + new Uri(s).GetComponents(UriComponents.PathAndQuery, UriFormat.UriEscaped)).Join("\r\n"), ContentType.Txt);
+	/// <summary>
+	/// 站点地图
+	/// </summary>
+	/// <param name="env"></param>
+	/// <param name="ext"></param>
+	/// <returns></returns>
+	[HttpGet("/sitemaps.{ext}")]
+	public async Task<ActionResult> Sitemap([FromServices] IWebHostEnvironment env, string ext)
+	{
+		var sitemap = Path.Combine(env.WebRootPath, "sitemap." + ext);
+		if (System.IO.File.Exists(sitemap))
+		{
+			var fs = new FileInfo(sitemap).ShareReadWrite();
+			switch (ext)
+			{
+				case "txt":
+					return Content((await fs.ReadAllLinesAsync(Encoding.UTF8)).Select(s => Request.Scheme + "://" + Request.Host.Host + new Uri(s).GetComponents(UriComponents.PathAndQuery, UriFormat.UriEscaped)).Join("\r\n"), ContentType.Txt);
 
-                case "html":
-                    var context = BrowsingContext.New(Configuration.Default);
-                    var doc = await context.OpenAsync(req => req.Content(fs.ReadAllText(Encoding.UTF8)));
-                    foreach (var e in doc.Body.QuerySelectorAll("li a"))
-                    {
-                        e.SetAttribute("href", Request.Scheme + "://" + Request.Host.Host + new Uri(e.GetAttribute("href")).GetComponents(UriComponents.PathAndQuery, UriFormat.UriEscaped));
-                    }
+				case "html":
+					var context = BrowsingContext.New(Configuration.Default);
+					var doc = await context.OpenAsync(req => req.Content(fs.ReadAllText(Encoding.UTF8)));
+					foreach (var e in doc.Body.QuerySelectorAll("li a"))
+					{
+						e.SetAttribute("href", Request.Scheme + "://" + Request.Host.Host + new Uri(e.GetAttribute("href")).GetComponents(UriComponents.PathAndQuery, UriFormat.UriEscaped));
+					}
 
-                    return Content(doc.DocumentElement.OuterHtml, ContentType.Html);
-            }
-            return File("/sitemap." + ext, new MimeMapper().GetMimeFromExtension("." + ext));
-        }
+					return Content(doc.DocumentElement.OuterHtml, ContentType.Html);
+			}
+			return File("/sitemap." + ext, new MimeMapper().GetMimeFromExtension("." + ext));
+		}
 
-        return NotFound();
-    }
+		return NotFound();
+	}
 
-    /// <summary>
-    /// 获取页面视图模型
-    /// </summary>
-    /// <returns></returns>
-    private HomePageViewModel GetIndexPageViewModel()
-    {
-        var postsQuery = PostService.GetQuery<PostDto>(PostBaseWhere()); //准备文章的查询
-        var notices = NoticeService.GetPagesFromCache<DateTime, NoticeDto>(1, 5, n => n.NoticeStatus == NoticeStatus.Normal, n => n.ModifyDate, false); //加载前5条公告
-        using var cats = CategoryService.GetQuery(c => c.Status == Status.Available && c.Post.Count > 0).Include(c => c.Parent).OrderBy(c => c.Name).ThenBy(c => c.Path).AsNoTracking().FromCache().ToPooledList(); //加载分类目录
-        var hotSearches = RedisHelper.Get<PooledList<KeywordsRank>>("SearchRank:Week").AsNotNull().Take(10).ToPooledList(); //热词统计
-        var hot5Post = postsQuery.OrderBy((new Random().Next() % 3) switch
-        {
-            1 => nameof(OrderBy.VoteUpCount),
-            2 => nameof(OrderBy.AverageViewCount),
-            _ => nameof(OrderBy.TotalViewCount)
-        } + " desc").Skip(0).Take(5).FromCache().ToPooledList(); //热门文章
-        var tagdic = PostService.GetTags().OrderByRandom().Take(20).ToDictionary(x => x.Key, x => Math.Min(x.Value + 12, 32)); //统计标签
-        return new HomePageViewModel
-        {
-            Categories = Mapper.Map<PooledList<CategoryDto_P>>(cats.ToTree(c => c.Id, c => c.ParentId).Flatten().ToPooledList()),
-            HotSearch = hotSearches,
-            Notices = notices.Data,
-            Tags = tagdic,
-            Top5Post = hot5Post,
-            PostsQueryable = postsQuery
-        };
-    }
+	/// <summary>
+	/// 获取页面视图模型
+	/// </summary>
+	/// <returns></returns>
+	private HomePageViewModel GetIndexPageViewModel()
+	{
+		var postsQuery = PostService.GetQuery<PostDto>(PostBaseWhere()); //准备文章的查询
+		var notices = NoticeService.GetPagesFromCache<DateTime, NoticeDto>(1, 5, n => n.NoticeStatus == NoticeStatus.Normal, n => n.ModifyDate, false); //加载前5条公告
+		var cats = CategoryService.GetQuery(c => c.Status == Status.Available && c.Post.Count > 0).Include(c => c.Parent).OrderBy(c => c.Name).ThenBy(c => c.Path).AsNoTracking().FromCache().ToList(); //加载分类目录
+		var hotSearches = RedisHelper.Get<List<KeywordsRank>>("SearchRank:Week").AsNotNull().Take(10).ToList(); //热词统计
+		var hot5Post = postsQuery.OrderBy((new Random().Next() % 3) switch
+		{
+			1 => nameof(OrderBy.VoteUpCount),
+			2 => nameof(OrderBy.AverageViewCount),
+			_ => nameof(OrderBy.TotalViewCount)
+		} + " desc").Skip(0).Take(5).FromCache().ToList(); //热门文章
+		var tagdic = PostService.GetTags().OrderByRandom().Take(20).ToDictionary(x => x.Key, x => Math.Min(x.Value + 12, 32)); //统计标签
+		return new HomePageViewModel
+		{
+			Categories = Mapper.Map<List<CategoryDto_P>>(cats.ToTree(c => c.Id, c => c.ParentId).Flatten().ToList()),
+			HotSearch = hotSearches,
+			Notices = notices.Data,
+			Tags = tagdic,
+			Top5Post = hot5Post,
+			PostsQueryable = postsQuery
+		};
+	}
 }

+ 75 - 76
src/Masuit.MyBlogs.Core/Controllers/MenuController.cs

@@ -1,5 +1,4 @@
-using Collections.Pooled;
-using Masuit.MyBlogs.Core.Common;
+using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
 using Masuit.MyBlogs.Core.Models.Command;
 using Masuit.MyBlogs.Core.Models.DTO;
@@ -13,83 +12,83 @@ using Z.EntityFramework.Plus;
 
 namespace Masuit.MyBlogs.Core.Controllers
 {
-    /// <summary>
-    /// 菜单管理
-    /// </summary>
-    public class MenuController : AdminController
-    {
-        /// <summary>
-        /// 菜单数据服务
-        /// </summary>
-        public IMenuService MenuService { get; set; }
+	/// <summary>
+	/// 菜单管理
+	/// </summary>
+	public class MenuController : AdminController
+	{
+		/// <summary>
+		/// 菜单数据服务
+		/// </summary>
+		public IMenuService MenuService { get; set; }
 
-        /// <summary>
-        /// 获取菜单
-        /// </summary>
-        /// <returns></returns>
-        public ActionResult GetMenus()
-        {
-            using var list = MenuService.GetAllNoTracking(m => m.Sort).ToPooledList();
-            var menus = list.ToTree(m => m.Id, m => m.ParentId);
-            return ResultData(Mapper.Map<List<MenuDto>>(menus));
-        }
+		/// <summary>
+		/// 获取菜单
+		/// </summary>
+		/// <returns></returns>
+		public ActionResult GetMenus()
+		{
+			var list = MenuService.GetAllNoTracking(m => m.Sort).ToList();
+			var menus = list.ToTree(m => m.Id, m => m.ParentId);
+			return ResultData(Mapper.Map<List<MenuDto>>(menus));
+		}
 
-        /// <summary>
-        /// 获取菜单类型
-        /// </summary>
-        /// <returns></returns>
-        [ResponseCache(Duration = 86400)]
-        public ActionResult GetMenuType()
-        {
-            var array = Enum.GetValues(typeof(MenuType));
-            var list = new List<object>();
-            foreach (Enum e in array)
-            {
-                list.Add(new
-                {
-                    e,
-                    name = e.GetDisplay()
-                });
-            }
-            return ResultData(list);
-        }
+		/// <summary>
+		/// 获取菜单类型
+		/// </summary>
+		/// <returns></returns>
+		[ResponseCache(Duration = 86400)]
+		public ActionResult GetMenuType()
+		{
+			var array = Enum.GetValues(typeof(MenuType));
+			var list = new List<object>();
+			foreach (Enum e in array)
+			{
+				list.Add(new
+				{
+					e,
+					name = e.GetDisplay()
+				});
+			}
+			return ResultData(list);
+		}
 
-        /// <summary>
-        /// 删除菜单
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        public async Task<ActionResult> Delete(int id)
-        {
-            var menus = MenuService[id].Flatten();
-            bool b = await MenuService.DeleteEntitiesSavedAsync(menus) > 0;
-            return ResultData(null, b, b ? "删除成功" : "删除失败");
-        }
+		/// <summary>
+		/// 删除菜单
+		/// </summary>
+		/// <param name="id"></param>
+		/// <returns></returns>
+		public async Task<ActionResult> Delete(int id)
+		{
+			var menus = MenuService[id].Flatten();
+			bool b = await MenuService.DeleteEntitiesSavedAsync(menus) > 0;
+			return ResultData(null, b, b ? "删除成功" : "删除失败");
+		}
 
-        /// <summary>
-        /// 保持菜单
-        /// </summary>
-        /// <param name="model"></param>
-        /// <returns></returns>
-        public async Task<ActionResult> Save([FromBodyOrDefault] MenuCommand model)
-        {
-            if (string.IsNullOrEmpty(model.Icon) || !model.Icon.Contains("/"))
-            {
-                model.Icon = null;
-            }
-            var m = await MenuService.GetByIdAsync(model.Id);
-            if (m == null)
-            {
-                var menu = model.Mapper<Menu>();
-                menu.Path = model.ParentId > 0 ? (MenuService[model.ParentId.Value].Path + "," + model.ParentId).Trim(',') : SnowFlake.NewId;
-                return await MenuService.AddEntitySavedAsync(menu) > 0 ? ResultData(model, true, "添加成功") : ResultData(null, false, "添加失败");
-            }
+		/// <summary>
+		/// 保持菜单
+		/// </summary>
+		/// <param name="model"></param>
+		/// <returns></returns>
+		public async Task<ActionResult> Save([FromBodyOrDefault] MenuCommand model)
+		{
+			if (string.IsNullOrEmpty(model.Icon) || !model.Icon.Contains("/"))
+			{
+				model.Icon = null;
+			}
+			var m = await MenuService.GetByIdAsync(model.Id);
+			if (m == null)
+			{
+				var menu = model.Mapper<Menu>();
+				menu.Path = model.ParentId > 0 ? (MenuService[model.ParentId.Value].Path + "," + model.ParentId).Trim(',') : SnowFlake.NewId;
+				return await MenuService.AddEntitySavedAsync(menu) > 0 ? ResultData(model, true, "添加成功") : ResultData(null, false, "添加失败");
+			}
 
-            Mapper.Map(model, m);
-            m.Path = model.ParentId > 0 ? (MenuService[model.ParentId.Value].Path + "," + model.ParentId).Trim(',') : SnowFlake.NewId;
-            bool b = await MenuService.SaveChangesAsync() > 0;
-            QueryCacheManager.ExpireType<Menu>();
-            return ResultData(null, b, b ? "修改成功" : "修改失败");
-        }
-    }
+			Mapper.Map(model, m);
+			m.Path = model.ParentId > 0 ? (MenuService[model.ParentId.Value].Path + "," + model.ParentId).Trim(',') : SnowFlake.NewId;
+			bool b = await MenuService.SaveChangesAsync() > 0;
+			QueryCacheManager.ExpireType<Menu>();
+			return ResultData(null, b, b ? "修改成功" : "修改失败");
+		}
+	}
 }

+ 358 - 359
src/Masuit.MyBlogs.Core/Controllers/MsgController.cs

@@ -1,5 +1,4 @@
 using CacheManager.Core;
-using Collections.Pooled;
 using Hangfire;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Common.Mails;
@@ -27,362 +26,362 @@ using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
 
 namespace Masuit.MyBlogs.Core.Controllers
 {
-    /// <summary>
-    /// 留言板和站内信
-    /// </summary>
-    public 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("留言未找到");
-                using var layer = LeaveMessageService.GetQueryNoTracking(e => e.GroupTag == message.GroupTag).ToPooledList();
-                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 = layer.ToTree(e => e.Id, e => e.ParentId).Mapper<IList<LeaveMessageViewModel>>()
-                });
-            }
-
-            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();
-            using var messages = LeaveMessageService.GetQueryNoTracking(c => tags.Contains(c.GroupTag)).ToPooledList();
-            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 = messages.OrderByDescending(c => c.PostDate).ToTree(c => c.Id, c => c.ParentId).Mapper<IList<LeaveMessageViewModel>>()
-                });
-            }
-
-            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);
-            if (MsgFeq.GetOrAdd("Comments:" + ClientIP, 1) > 2)
-            {
-                MsgFeq.Expire("Comments:" + ClientIP, TimeSpan.FromMinutes(1));
-                return ResultData(null, false, "您的发言频率过快,请稍后再发表吧!");
-            }
-
-            var msg = cmd.Mapper<LeaveMessage>();
-            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 = ClientIP;
-            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:" + ClientIP, 1, i => i + 1, 5);
-            MsgFeq.Expire("Comments:" + ClientIP, 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(() => CommonHelper.SendMail(Request.Host + "|博客新留言:", content.Set("link", Url.Action("Index", "Msg", new { cid = msg.Id }, Request.Scheme)).Render(false), email, ClientIP));
-                }
-                else
-                {
-                    //通知博主和上层所有关联的评论访客
-                    using var emails = LeaveMessageService.GetQuery(e => e.GroupTag == msg.GroupTag).Select(c => c.Email).Distinct().AsEnumerable().Append(email).Except(new[] { msg.Email }).ToPooledSet();
-                    string link = Url.Action("Index", "Msg", new { cid = msg.Id }, Request.Scheme);
-                    foreach (var s in emails)
-                    {
-                        BackgroundJob.Enqueue(() => CommonHelper.SendMail($"{Request.Host}{CommonHelper.SystemSettings["Title"]} 留言回复:", content.Set("link", link).Render(false), s, ClientIP));
-                    }
-                }
-                return ResultData(null, true, "留言发表成功,服务器正在后台处理中,这会有一定的延迟,稍后将会显示到列表中!");
-            }
-
-            BackgroundJob.Enqueue(() => CommonHelper.SendMail(Request.Host + "|博客新留言(待审核):", content.Set("link", Url.Action("Index", "Msg", new
-            {
-                cid = msg.Id
-            }, Request.Scheme)).Render(false) + "<p style='color:red;'>(待审核)</p>", email, ClientIP));
-            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);
-                using var emails = LeaveMessageService.GetQuery(m => m.GroupTag == msg.GroupTag).Select(m => m.Email).Distinct().ToPooledList().Except(new List<string> { msg.Email, CurrentUser.Email }).ToPooledSet();
-                var link = Url.Action("Index", "Msg", new { cid = id }, Request.Scheme);
-                foreach (var s in emails)
-                {
-                    BackgroundJob.Enqueue(() => CommonHelper.SendMail($"{Request.Host}{CommonHelper.SystemSettings["Title"]} 留言回复:", content.Set("link", link).Render(false), s, ClientIP));
-                }
-            }
-
-            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).ToList();
-            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 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).ToList();
+				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 = layer.ToTree(e => e.Id, e => e.ParentId).Mapper<IList<LeaveMessageViewModel>>()
+				});
+			}
+
+			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)).ToList();
+			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 = messages.OrderByDescending(c => c.PostDate).ToTree(c => c.Id, c => c.ParentId).Mapper<IList<LeaveMessageViewModel>>()
+				});
+			}
+
+			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);
+			if (MsgFeq.GetOrAdd("Comments:" + ClientIP, 1) > 2)
+			{
+				MsgFeq.Expire("Comments:" + ClientIP, TimeSpan.FromMinutes(1));
+				return ResultData(null, false, "您的发言频率过快,请稍后再发表吧!");
+			}
+
+			var msg = cmd.Mapper<LeaveMessage>();
+			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 = ClientIP;
+			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:" + ClientIP, 1, i => i + 1, 5);
+			MsgFeq.Expire("Comments:" + ClientIP, 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(() => CommonHelper.SendMail(Request.Host + "|博客新留言:", content.Set("link", Url.Action("Index", "Msg", new { cid = msg.Id }, Request.Scheme)).Render(false), email, ClientIP));
+				}
+				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(() => CommonHelper.SendMail($"{Request.Host}{CommonHelper.SystemSettings["Title"]} 留言回复:", content.Set("link", link).Render(false), s, ClientIP));
+					}
+				}
+				return ResultData(null, true, "留言发表成功,服务器正在后台处理中,这会有一定的延迟,稍后将会显示到列表中!");
+			}
+
+			BackgroundJob.Enqueue(() => CommonHelper.SendMail(Request.Host + "|博客新留言(待审核):", content.Set("link", Url.Action("Index", "Msg", new
+			{
+				cid = msg.Id
+			}, Request.Scheme)).Render(false) + "<p style='color:red;'>(待审核)</p>", email, ClientIP));
+			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().ToList().Except(new List<string> { msg.Email, CurrentUser.Email }).ToHashSet();
+				var link = Url.Action("Index", "Msg", new { cid = id }, Request.Scheme);
+				foreach (var s in emails)
+				{
+					BackgroundJob.Enqueue(() => CommonHelper.SendMail($"{Request.Host}{CommonHelper.SystemSettings["Title"]} 留言回复:", content.Set("link", link).Render(false), s, ClientIP));
+				}
+			}
+
+			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).ToList();
+			return ResultData(msgs);
+		}
+
+		/// <summary>
+		/// 清除站内信
+		/// </summary>
+		/// <returns></returns>
+		[MyAuthorize]
+		public async Task<ActionResult> ClearMsgs()
+		{
+			await MessageService.DeleteEntitySavedAsync(m => m.Read);
+			return ResultData(null, true, "站内消息清除成功!");
+		}
+
+		#endregion 站内消息
+	}
 }

+ 1254 - 1255
src/Masuit.MyBlogs.Core/Controllers/PostController.cs

@@ -1,5 +1,4 @@
 using CacheManager.Core;
-using Collections.Pooled;
 using Hangfire;
 using Masuit.LuceneEFCore.SearchEngine;
 using Masuit.LuceneEFCore.SearchEngine.Interfaces;
@@ -51,1258 +50,1258 @@ namespace Masuit.MyBlogs.Core.Controllers;
 /// </summary>
 public class PostController : BaseController
 {
-    public IPostService PostService { get; set; }
-
-    public ICategoryService CategoryService { get; set; }
-
-    public ISeminarService SeminarService { get; set; }
-
-    public IPostHistoryVersionService PostHistoryVersionService { get; set; }
-
-    public IWebHostEnvironment HostEnvironment { get; set; }
-
-    public ISearchEngine<DataContext> SearchEngine { get; set; }
-
-    public ImagebedClient ImagebedClient { get; set; }
-
-    public IPostVisitRecordService PostVisitRecordService { get; set; }
-
-    public ICommentService CommentService { get; set; }
-
-    public IPostTagService PostTagService { get; set; }
-
-    /// <summary>
-    /// 文章详情页
-    /// </summary>
-    /// <returns></returns>
-    [Route("{id:int}"), Route("{id:int}/comments/{cid:int}"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
-    public async Task<ActionResult> Details(int id, string kw, int cid, string t)
-    {
-        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 });
-        }
-
-        var post = await PostService.GetQuery(p => p.Id == id && (p.Status == Status.Published || CurrentUser.IsAdmin)).Include(p => p.Seminar).AsNoTracking().FirstOrDefaultAsync() ?? throw new NotFoundException("文章未找到");
-        CheckPermission(post);
-        if (!string.IsNullOrEmpty(post.Redirect))
-        {
-            if (notRobot && string.IsNullOrEmpty(HttpContext.Session.Get<string>("post" + id)))
-            {
-                BackgroundJob.Enqueue<IHangfireBackJob>(job => job.RecordPostVisit(id, ClientIP, Request.Headers[HeaderNames.Referer].ToString(), Request.GetDisplayUrl()));
-                HttpContext.Session.Set("post" + id, id.ToString());
-            }
-
-            return Redirect(post.Redirect);
-        }
-
-        post.Category = CategoryService[post.CategoryId];
-        ViewBag.CommentsCount = CommentService.Count(c => c.PostId == id && c.ParentId == null && c.Status == Status.Published);
-        ViewBag.HistoryCount = PostHistoryVersionService.Count(c => c.PostId == id);
-        ViewBag.Keyword = post.Keyword + "," + post.Label;
-        if (Request.Query.ContainsKey("share"))
-        {
-            ViewBag.Desc = await post.Content.GetSummary(200);
-        }
-        else
-        {
-            ViewBag.Desc = "若页面无法访问,可通过搜索引擎网页快照进行浏览。" + await post.Content.GetSummary(200);
-        }
-
-        var modifyDate = post.ModifyDate;
-        ViewBag.Next = await PostService.GetFromCacheAsync<DateTime, PostModelBase>(p => p.ModifyDate > modifyDate && (p.LimitMode ?? 0) == RegionLimitMode.All && (p.Status == Status.Published || CurrentUser.IsAdmin), p => p.ModifyDate);
-        ViewBag.Prev = await PostService.GetFromCacheAsync<DateTime, PostModelBase>(p => p.ModifyDate < modifyDate && (p.LimitMode ?? 0) == RegionLimitMode.All && (p.Status == Status.Published || CurrentUser.IsAdmin), p => p.ModifyDate, false);
-        ViewData[nameof(post.Author)] = post.Author;
-        ViewData[nameof(post.PostDate)] = post.PostDate;
-        ViewData[nameof(post.ModifyDate)] = post.ModifyDate;
-        ViewData["cover"] = post.Content.MatchFirstImgSrc();
-        if (!string.IsNullOrEmpty(kw))
-        {
-            await PostService.Highlight(post, kw);
-        }
-
-        var regex = SearchEngine.LuceneIndexSearcher.CutKeywords(string.IsNullOrWhiteSpace(post.Keyword + post.Label) ? post.Title : post.Keyword + post.Label).Select(Regex.Escape).Join("|");
-        ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location(), post.CategoryId, regex);
-        var related = PostService.GetQuery(PostBaseWhere().And(p => p.Id != id && Regex.IsMatch(p.Title + (p.Keyword ?? "") + (p.Label ?? ""), regex, RegexOptions.IgnoreCase)), p => p.AverageViewCount, false).Take(10).Select(p => new { p.Id, p.Title }).FromCache().ToDictionary(p => p.Id, p => p.Title);
-        ViewBag.Related = related;
-        post.ModifyDate = post.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-        post.PostDate = post.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-        post.Content = ReplaceVariables(post.Content);
-        post.ProtectContent = ReplaceVariables(post.ProtectContent);
-
-        if (CurrentUser.IsAdmin)
-        {
-            return View("Details_Admin", post);
-        }
-
-        if (notRobot && string.IsNullOrEmpty(HttpContext.Session.Get<string>("post" + id)))
-        {
-            BackgroundJob.Enqueue<IHangfireBackJob>(job => job.RecordPostVisit(id, ClientIP, Request.Headers[HeaderNames.Referer].ToString(), Request.GetDisplayUrl()));
-            HttpContext.Session.Set("post" + id, id.ToString());
-        }
-
-        if (post.LimitMode == RegionLimitMode.OnlyForSearchEngine)
-        {
-            BackgroundJob.Enqueue<IHangfireBackJob>(job => job.RecordPostVisit(id, ClientIP, Request.Headers[HeaderNames.Referer].ToString(), Request.GetDisplayUrl()));
-        }
-
-        return View(post);
-    }
-
-    /// <summary>
-    /// 文章历史版本
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="page"></param>
-    /// <param name="size"></param>
-    /// <returns></returns>
-    [Route("{id:int}/history"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id", "page", "size" }, VaryByHeader = "Cookie")]
-    public async Task<ActionResult> History(int id, [Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 20)
-    {
-        var post = await PostService.GetAsync(p => p.Id == id && (p.Status == Status.Published || CurrentUser.IsAdmin)) ?? throw new NotFoundException("文章未找到");
-        CheckPermission(post);
-        ViewBag.Primary = post;
-        var list = await PostHistoryVersionService.GetPagesAsync(page, size, v => v.PostId == id, v => v.ModifyDate, false);
-        foreach (var item in list.Data)
-        {
-            item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-        }
-
-        ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location(), post.CategoryId, post.Keyword + "," + post.Label);
-        return View(list);
-    }
-
-    /// <summary>
-    /// 文章历史版本
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="hid"></param>
-    /// <returns></returns>
-    [Route("{id:int}/history/{hid:int}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id", "hid" }, VaryByHeader = "Cookie")]
-    public async Task<ActionResult> HistoryVersion(int id, int hid)
-    {
-        var history = await PostHistoryVersionService.GetAsync(v => v.Id == hid && (v.Post.Status == Status.Published || CurrentUser.IsAdmin)) ?? throw new NotFoundException("文章未找到");
-        CheckPermission(history.Post);
-        history.Content = ReplaceVariables(history.Content);
-        history.ProtectContent = ReplaceVariables(history.ProtectContent);
-        history.ModifyDate = history.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-        var next = await PostHistoryVersionService.GetAsync(p => p.PostId == id && p.ModifyDate > history.ModifyDate, p => p.ModifyDate);
-        var prev = await PostHistoryVersionService.GetAsync(p => p.PostId == id && p.ModifyDate < history.ModifyDate, p => p.ModifyDate, false);
-        ViewBag.Next = next;
-        ViewBag.Prev = prev;
-        ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location(), history.CategoryId, history.Label);
-        ViewData[nameof(history.Post.Author)] = history.Post.Author;
-        ViewData[nameof(history.Post.PostDate)] = history.Post.PostDate;
-        ViewData[nameof(history.ModifyDate)] = history.ModifyDate;
-        ViewData["cover"] = history.Content.MatchFirstImgSrc();
-        return CurrentUser.IsAdmin ? View("HistoryVersion_Admin", history) : View(history);
-    }
-
-    /// <summary>
-    /// 版本对比
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="v1"></param>
-    /// <param name="v2"></param>
-    /// <returns></returns>
-    [Route("{id:int}/history/{v1:int}-{v2:int}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id", "v1", "v2" }, VaryByHeader = "Cookie")]
-    public async Task<ActionResult> CompareVersion(int id, int v1, int v2)
-    {
-        var post = await PostService.GetAsync(p => p.Id == id && (p.Status == Status.Published || CurrentUser.IsAdmin));
-        var main = post.Mapper<PostHistoryVersion>() ?? throw new NotFoundException("文章未找到");
-        CheckPermission(post);
-        var left = v1 <= 0 ? main : await PostHistoryVersionService.GetAsync(v => v.Id == v1) ?? throw new NotFoundException("文章未找到");
-        var right = v2 <= 0 ? main : await PostHistoryVersionService.GetAsync(v => v.Id == v2) ?? throw new NotFoundException("文章未找到");
-        main.Id = id;
-        var diff = new HtmlDiff.HtmlDiff(right.Content, left.Content);
-        var diffOutput = diff.Build();
-        right.Content = ReplaceVariables(Regex.Replace(Regex.Replace(diffOutput, "<ins.+?</ins>", string.Empty), @"<\w+></\w+>", string.Empty));
-        right.ModifyDate = right.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-        left.Content = ReplaceVariables(Regex.Replace(Regex.Replace(diffOutput, "<del.+?</del>", string.Empty), @"<\w+></\w+>", string.Empty));
-        left.ModifyDate = left.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-        ViewBag.Ads = AdsService.GetsByWeightedPrice(2, AdvertiseType.InPage, Request.Location(), main.CategoryId, main.Label);
-        ViewBag.DisableCopy = post.DisableCopy;
-        return View(new[] { main, left, right });
-    }
-
-    /// <summary>
-    /// 反对
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    public async Task<ActionResult> VoteDown(int id)
-    {
-        if (HttpContext.Session.Get("post-vote" + id) != null)
-        {
-            return ResultData(null, false, "您刚才已经投过票了,感谢您的参与!");
-        }
-
-        var b = await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.VoteDownCount, m => m.VoteDownCount + 1)) > 0;
-        if (b)
-        {
-            HttpContext.Session.Set("post-vote" + id, id.GetBytes());
-        }
-
-        return ResultData(null, b, b ? "投票成功!" : "投票失败!");
-    }
-
-    /// <summary>
-    /// 支持
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    public async Task<ActionResult> VoteUp(int id)
-    {
-        if (HttpContext.Session.Get("post-vote" + id) != null)
-        {
-            return ResultData(null, false, "您刚才已经投过票了,感谢您的参与!");
-        }
-
-        var b = await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.VoteUpCount, m => m.VoteUpCount + 1)) > 0;
-        if (b)
-        {
-            HttpContext.Session.Set("post-vote" + id, id.GetBytes());
-        }
-
-        return ResultData(null, b, b ? "投票成功!" : "投票失败!");
-    }
-
-    /// <summary>
-    /// 投稿页
-    /// </summary>
-    /// <returns></returns>
-    public ActionResult Publish()
-    {
-        return View();
-    }
-
-    /// <summary>
-    /// 发布投稿
-    /// </summary>
-    /// <param name="post"></param>
-    /// <param name="code"></param>
-    /// <param name="cancellationToken"></param>
-    /// <returns></returns>
-    [HttpPost, ValidateAntiForgeryToken]
-    public async Task<ActionResult> Publish(PostCommand post, [Required(ErrorMessage = "验证码不能为空")] string code, CancellationToken cancellationToken)
-    {
-        if (RedisHelper.Get("code:" + post.Email) != code)
-        {
-            return ResultData(null, false, "验证码错误!");
-        }
-
-        if (PostService.Any(p => p.Status == Status.Forbidden && p.Email == post.Email))
-        {
-            return ResultData(null, false, "由于您曾经恶意投稿,该邮箱已经被标记为黑名单,无法进行投稿,如有疑问,请联系网站管理员进行处理。");
-        }
-
-        var match = Regex.Match(post.Title + post.Author + post.Content, CommonHelper.BanRegex);
-        if (match.Success)
-        {
-            LogManager.Info($"提交内容:{post.Title}/{post.Author}/{post.Content},敏感词:{match.Value}");
-            return ResultData(null, false, "您提交的内容包含敏感词,被禁止发表,请检查您的内容后尝试重新提交!");
-        }
-
-        if (!CategoryService.Any(c => c.Id == post.CategoryId))
-        {
-            return ResultData(null, message: "请选择一个分类");
-        }
-
-        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 p = post.Mapper<Post>();
-        p.IP = ClientIP;
-        p.Modifier = p.Author;
-        p.ModifierEmail = p.Email;
-        p.DisableCopy = true;
-        p.Rss = true;
-        PostTagService.AddOrUpdate(t => t.Name, p.Label.AsNotNull().Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => new PostTag()
-        {
-            Name = s,
-            Count = PostService.Count(t => t.Label.Contains(s))
-        }));
-        p = PostService.AddEntitySaved(p);
-        if (p == null)
-        {
-            return ResultData(null, false, "文章发表失败!");
-        }
-
-        RedisHelper.Expire("code:" + p.Email, 1);
-        var content = new Template(await new FileInfo(HostEnvironment.WebRootPath + "/template/publish.html").ShareReadWrite().ReadAllTextAsync(Encoding.UTF8))
-            .Set("link", Url.Action("Details", "Post", new { id = p.Id }, Request.Scheme))
-            .Set("time", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))
-            .Set("title", p.Title).Render();
-        BackgroundJob.Enqueue(() => CommonHelper.SendMail(CommonHelper.SystemSettings["Title"] + "有访客投稿:", content, CommonHelper.SystemSettings["ReceiveEmail"], ClientIP));
-        return ResultData(p.Mapper<PostDto>(), message: "文章发表成功,待站长审核通过以后将显示到列表中!");
-    }
-
-    /// <summary>
-    /// 获取标签
-    /// </summary>
-    /// <returns></returns>
-    [ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
-    public ActionResult GetTag()
-    {
-        return ResultData(PostService.GetTags().Select(x => x.Key).OrderBy(s => s));
-    }
-
-    /// <summary>
-    /// 标签云
-    /// </summary>
-    /// <returns></returns>
-    [Route("all"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
-    public async Task<ActionResult> All()
-    {
-        ViewBag.tags = new Dictionary<string, int>(PostService.GetTags().Where(x => x.Value > 1).OrderBy(x => x.Key));
-        ViewBag.cats = await CategoryService.GetQuery(c => c.Post.Count > 0, c => c.Post.Count, false).Include(c => c.Parent).ThenInclude(c => c.Parent).AsNoTracking().ToDictionaryAsync(c => c.Id, c => c.Path()); //category
-        ViewBag.seminars = await SeminarService.GetAll(c => c.Post.Count, false).AsNoTracking().ToDictionaryAsync(c => c.Id, c => c.Title); //seminars
-        return View();
-    }
-
-    /// <summary>
-    /// 检查访问密码
-    /// </summary>
-    /// <param name="email"></param>
-    /// <param name="token"></param>
-    /// <returns></returns>
-    [HttpPost, ValidateAntiForgeryToken, AllowAccessFirewall]
-    public ActionResult CheckViewToken(string email, string token)
-    {
-        if (string.IsNullOrEmpty(token))
-        {
-            return ResultData(null, false, "请输入访问密码!");
-        }
-
-        var s = RedisHelper.Get("token:" + email);
-        if (token.Equals(s))
-        {
-            HttpContext.Session.Set("AccessViewToken", token);
-            Response.Cookies.Append("Email", email, new CookieOptions
-            {
-                Expires = DateTime.Now.AddYears(1),
-                SameSite = SameSiteMode.Lax
-            });
-            Response.Cookies.Append("PostAccessToken", email.MDString3(AppConfig.BaiduAK), new CookieOptions
-            {
-                Expires = DateTime.Now.AddYears(1),
-                SameSite = SameSiteMode.Lax
-            });
-            return ResultData(null);
-        }
-
-        return ResultData(null, false, "访问密码不正确!");
-    }
-
-    /// <summary>
-    /// 检查授权邮箱
-    /// </summary>
-    /// <param name="email"></param>
-    /// <returns></returns>
-    [HttpPost, ValidateAntiForgeryToken, AllowAccessFirewall]
-    public ActionResult GetViewToken(string email)
-    {
-        var validator = new IsEmailAttribute();
-        if (!validator.IsValid(email))
-        {
-            return ResultData(null, false, validator.ErrorMessage);
-        }
-
-        if (RedisHelper.Exists("get:" + email))
-        {
-            RedisHelper.Expire("get:" + email, 120);
-            return ResultData(null, false, "发送频率限制,请在2分钟后重新尝试发送邮件!请检查你的邮件,若未收到,请检查你的邮箱地址或邮件垃圾箱!");
-        }
-
-        if (!UserInfoService.Any(b => b.Email.Equals(email)))
-        {
-            return ResultData(null, false, "您目前没有权限访问这个链接,请联系站长开通访问权限!");
-        }
-
-        var token = SnowFlake.GetInstance().GetUniqueShortId(6);
-        RedisHelper.Set("token:" + email, token, 86400);
-        BackgroundJob.Enqueue(() => CommonHelper.SendMail(Request.Host + "博客访问验证码", $"{Request.Host}本次验证码是:<span style='color:red'>{token}</span>,有效期为24h,请按时使用!", email, ClientIP));
-        RedisHelper.Set("get:" + email, token, 120);
-        return ResultData(null);
-    }
-
-    /// <summary>
-    /// 文章合并
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    [HttpGet("{id}/merge")]
-    public async Task<ActionResult> PushMerge(int id)
-    {
-        var post = await PostService.GetAsync(p => p.Id == id && p.Status == Status.Published && !p.Locked) ?? throw new NotFoundException("文章未找到");
-        CheckPermission(post);
-        return View(post);
-    }
-
-    /// <summary>
-    /// 文章合并
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="mid"></param>
-    /// <returns></returns>
-    [HttpGet("{id}/merge/{mid}")]
-    public async Task<ActionResult> RepushMerge(int id, int mid)
-    {
-        var post = await PostService.GetAsync(p => p.Id == id && p.Status == Status.Published && !p.Locked) ?? throw new NotFoundException("文章未找到");
-        CheckPermission(post);
-        var merge = post.PostMergeRequests.FirstOrDefault(p => p.Id == mid && p.MergeState != MergeStatus.Merged) ?? throw new NotFoundException("待合并文章未找到");
-        return View(merge);
-    }
-
-    /// <summary>
-    /// 文章合并
-    /// </summary>
-    /// <param name="messageService"></param>
-    /// <param name="postMergeRequestService"></param>
-    /// <param name="dto"></param>
-    /// <returns></returns>
-    [HttpPost("{id}/pushmerge")]
-    public async Task<ActionResult> PushMerge([FromServices] IInternalMessageService messageService, [FromServices] IPostMergeRequestService postMergeRequestService, PostMergeRequestCommand dto)
-    {
-        if (RedisHelper.Get("code:" + dto.ModifierEmail) != dto.Code)
-        {
-            return ResultData(null, false, "验证码错误!");
-        }
-
-        var post = await PostService.GetAsync(p => p.Id == dto.PostId && p.Status == Status.Published && !p.Locked) ?? throw new NotFoundException("文章未找到");
-        if (post.Title.Equals(dto.Title) && post.Content.HammingDistance(dto.Content) <= 1)
-        {
-            return ResultData(null, false, "内容未被修改或修改的内容过少(无意义修改)!");
-        }
-
-        #region 合并验证
-
-        if (postMergeRequestService.Any(p => p.ModifierEmail == dto.ModifierEmail && p.MergeState == MergeStatus.Block))
-        {
-            return ResultData(null, false, "由于您曾经多次恶意修改文章,已经被标记为黑名单,无法修改任何文章,如有疑问,请联系网站管理员进行处理。");
-        }
-
-        if (post.PostMergeRequests.Any(p => p.ModifierEmail == dto.ModifierEmail && p.MergeState == MergeStatus.Pending))
-        {
-            return ResultData(null, false, "您已经提交过一次修改请求正在待处理,暂不能继续提交修改请求!");
-        }
-
-        #endregion 合并验证
-
-        #region 直接合并
-
-        if (post.Email.Equals(dto.ModifierEmail))
-        {
-            var history = post.Mapper<PostHistoryVersion>();
-            Mapper.Map(dto, post);
-            post.PostHistoryVersion.Add(history);
-            post.ModifyDate = DateTime.Now;
-            return await PostService.SaveChangesAsync() > 0 ? ResultData(null, true, "你是文章原作者,无需审核,文章已自动更新并在首页展示!") : ResultData(null, false, "操作失败!");
-        }
-
-        #endregion 直接合并
-
-        var merge = post.PostMergeRequests.FirstOrDefault(r => r.Id == dto.Id && r.MergeState != MergeStatus.Merged);
-        if (merge != null)
-        {
-            Mapper.Map(dto, merge);
-            merge.SubmitTime = DateTime.Now;
-            merge.MergeState = MergeStatus.Pending;
-        }
-        else
-        {
-            merge = Mapper.Map<PostMergeRequest>(dto);
-            merge.SubmitTime = DateTime.Now;
-            post.PostMergeRequests.Add(merge);
-        }
-        merge.IP = ClientIP;
-        var b = await PostService.SaveChangesAsync() > 0;
-        if (!b)
-        {
-            return ResultData(null, false, "操作失败!");
-        }
-
-        RedisHelper.Expire("code:" + dto.ModifierEmail, 1);
-        await messageService.AddEntitySavedAsync(new InternalMessage()
-        {
-            Title = $"来自【{dto.Modifier}】对文章《{post.Title}》的修改请求",
-            Content = dto.Title,
-            Link = "#/merge/compare?id=" + merge.Id
-        });
-
-        var htmlDiff = new HtmlDiff.HtmlDiff(post.Content.RemoveHtmlTag(), dto.Content.RemoveHtmlTag());
-        var diff = htmlDiff.Build();
-        var content = new Template(await new FileInfo(HostEnvironment.WebRootPath + "/template/merge-request.html").ShareReadWrite().ReadAllTextAsync(Encoding.UTF8))
-            .Set("title", post.Title)
-            .Set("link", Url.Action("Index", "Dashboard", new { }, Request.Scheme) + "#/merge/compare?id=" + merge.Id)
-            .Set("diff", diff)
-            .Set("host", "//" + Request.Host)
-            .Set("id", merge.Id.ToString())
-            .Render();
-        BackgroundJob.Enqueue(() => CommonHelper.SendMail("博客文章修改请求:", content, CommonHelper.SystemSettings["ReceiveEmail"], ClientIP));
-        return ResultData(null, true, "您的修改请求已提交,已进入审核状态,感谢您的参与!");
-    }
-
-    #region 后端管理
-
-    /// <summary>
-    /// 固顶
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    public async Task<ActionResult> Fixtop(int id)
-    {
-        Post post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
-        post.IsFixedTop = !post.IsFixedTop;
-        bool b = await PostService.SaveChangesAsync() > 0;
-        return b ? ResultData(null, true, post.IsFixedTop ? "置顶成功!" : "取消置顶成功!") : ResultData(null, false, "操作失败!");
-    }
-
-    /// <summary>
-    /// 审核
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    public async Task<ActionResult> Pass(int id)
-    {
-        var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
-        post.Status = Status.Published;
-        post.ModifyDate = DateTime.Now;
-        post.PostDate = DateTime.Now;
-        var b = await PostService.SaveChangesAsync() > 0;
-        if (!b)
-        {
-            return ResultData(null, false, "审核失败!");
-        }
-
-        (post.Keyword + "," + post.Label).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(KeywordsManager.AddWords);
-        SearchEngine.LuceneIndexer.Add(post);
-        return ResultData(null, true, "审核通过!");
-    }
-
-    /// <summary>
-    /// 删除
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    public async Task<ActionResult> Delete(int id)
-    {
-        var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
-        post.Status = Status.Deleted;
-        bool b = await PostService.SaveChangesAsync(true) > 0;
-        SearchEngine.LuceneIndexer.Delete(post);
-        return ResultData(null, b, b ? "删除成功!" : "删除失败!");
-    }
-
-    /// <summary>
-    /// 还原版本
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    public async Task<ActionResult> Restore(int id)
-    {
-        var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
-        post.Status = Status.Published;
-        bool b = await PostService.SaveChangesAsync() > 0;
-        SearchEngine.LuceneIndexer.Add(post);
-        return ResultData(null, b, b ? "恢复成功!" : "恢复失败!");
-    }
-
-    /// <summary>
-    /// 彻底删除文章
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    public ActionResult Truncate(int id)
-    {
-        bool b = PostService - id;
-        return ResultData(null, b, b ? "删除成功!" : "删除失败!");
-    }
-
-    /// <summary>
-    /// 获取文章
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    public ActionResult Get(int id)
-    {
-        Post post = PostService[id] ?? throw new NotFoundException("文章未找到");
-        PostDto model = post.Mapper<PostDto>();
-        model.Seminars = post.Seminar.Select(s => s.Id).Join(",");
-        return ResultData(model);
-    }
-
-    /// <summary>
-    /// 获取文章分页
-    /// </summary>
-    /// <returns></returns>
-    [MyAuthorize]
-    public async Task<ActionResult> GetPageData([FromServices] ICacheManager<HashSet<string>> cacheManager, int page = 1, [Range(1, 200, ErrorMessage = "页大小必须介于{1}-{2}")] int size = 10, OrderBy orderby = OrderBy.ModifyDate, string kw = "", int? cid = null)
-    {
-        Expression<Func<Post, bool>> where = p => true;
-        if (cid.HasValue)
-        {
-            where = where.And(p => p.CategoryId == cid.Value || p.Category.ParentId == cid.Value || p.Category.Parent.ParentId == cid.Value);
-        }
-
-        if (!string.IsNullOrEmpty(kw))
-        {
-            kw = Regex.Escape(kw);
-            where = where.And(p => Regex.IsMatch(p.Title + p.Author + p.Email + p.Content, kw, RegexOptions.IgnoreCase));
-        }
-
-        var list = orderby switch
-        {
-            OrderBy.Trending => await PostService.GetQuery(where).OrderByDescending(p => p.Status).ThenByDescending(p => p.IsFixedTop).ThenByDescending(p => p.PostVisitRecordStats.Sum(t => t.Count) / p.PostVisitRecordStats.Count).ToPagedListAsync<Post, PostDataModel>(page, size, MapperConfig),
-            _ => await PostService.GetQuery(where).OrderBy($"{nameof(Post.Status)} desc,{nameof(Post.IsFixedTop)} desc,{orderby.GetDisplay()} desc").ToPagedListAsync<Post, PostDataModel>(page, size, MapperConfig)
-        };
-        foreach (var item in list.Data)
-        {
-            item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-            item.PostDate = item.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-            item.Online = cacheManager.Get(nameof(PostOnline) + ":" + item.Id)?.Count ?? 0;
-        }
-
-        return Ok(list);
-    }
-
-    /// <summary>
-    /// 获取未审核文章
-    /// </summary>
-    /// <param name="page"></param>
-    /// <param name="size"></param>
-    /// <param name="search"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    public async Task<ActionResult> GetPending([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15, string search = "")
-    {
-        Expression<Func<Post, bool>> where = p => p.Status == Status.Pending;
-        if (!string.IsNullOrEmpty(search))
-        {
-            where = where.And(p => p.Title.Contains(search) || p.Author.Contains(search) || p.Email.Contains(search) || p.Label.Contains(search));
-        }
-
-        var pages = await PostService.GetQuery(where).OrderByDescending(p => p.IsFixedTop).ThenByDescending(p => p.ModifyDate).ToPagedListAsync<Post, PostDataModel>(page, size, MapperConfig);
-        foreach (var item in pages.Data)
-        {
-            item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-            item.PostDate = item.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-        }
-
-        return Ok(pages);
-    }
-
-    /// <summary>
-    /// 编辑
-    /// </summary>
-    /// <param name="post"></param>
-    /// <param name="cancellationToken"></param>
-    /// <returns></returns>
-    [HttpPost, MyAuthorize]
-    public async Task<ActionResult> Edit([FromBodyOrDefault] PostCommand post, CancellationToken cancellationToken = default)
-    {
-        post.Content = await ImagebedClient.ReplaceImgSrc(await post.Content.Trim().ClearImgAttributes(), cancellationToken);
-        if (!ValidatePost(post, out var resultData))
-        {
-            return resultData;
-        }
-
-        Post p = await PostService.GetByIdAsync(post.Id);
-        if (post.Reserve && p.Status == Status.Published)
-        {
-            if (p.Content.HammingDistance(post.Content) > 0)
-            {
-                var history = p.Mapper<PostHistoryVersion>();
-                p.PostHistoryVersion.Add(history);
-            }
-
-            if (p.Title.HammingDistance(post.Title) > 10 && CommentService.Any(c => c.PostId == p.Id && c.ParentId == null))
-            {
-                p.Comment.Add(new Comment
-                {
-                    Status = Status.Published,
-                    NickName = "系统自动评论",
-                    Email = p.Email,
-                    Content = $"<p style=\"color:red\">温馨提示:由于文章发生了重大更新,本条评论之前的所有评论仅作为原文《{p.Title}》的历史评论保留,不作为本文的最新评论参考,请知悉!了解更多信息,请查阅本文的历史修改记录。</p>",
-                    PostId = p.Id,
-                    CommentDate = DateTime.Now,
-                    IsMaster = true,
-                    IsAuthor = true,
-                    IP = "127.0.0.1",
-                    Location = "内网",
-                    GroupTag = SnowFlake.NewId,
-                    Path = SnowFlake.NewId,
-                });
-            }
-
-            p.ModifyDate = DateTime.Now;
-            var user = HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
-            post.Modifier = string.IsNullOrEmpty(post.Modifier) ? user.NickName : post.Modifier;
-            post.ModifierEmail = string.IsNullOrEmpty(post.ModifierEmail) ? user.Email : post.ModifierEmail;
-        }
-
-        Mapper.Map(post, p);
-        p.IP = ClientIP;
-        p.Seminar.Clear();
-        if (!string.IsNullOrEmpty(post.Seminars))
-        {
-            var tmp = post.Seminars.Split(',', StringSplitOptions.RemoveEmptyEntries).Distinct().Select(int.Parse).ToArray();
-            var seminars = SeminarService.GetQuery(s => tmp.Contains(s.Id)).ToList();
-            p.Seminar.AddRange(seminars);
-        }
-
-        (p.Keyword + "," + p.Label).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(KeywordsManager.AddWords);
-        PostTagService.AddOrUpdate(t => t.Name, p.Label.AsNotNull().Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => new PostTag()
-        {
-            Name = s,
-            Count = PostService.Count(t => t.Label.Contains(s))
-        }));
-        bool b = await SearchEngine.SaveChangesAsync() > 0;
-        if (!b)
-        {
-            return ResultData(null, false, "文章修改失败!");
-        }
-
-        if (p.LimitMode == RegionLimitMode.OnlyForSearchEngine)
-        {
-            SearchEngine.LuceneIndexer.Delete(p);
-        }
-        return ResultData(p.Mapper<PostDto>(), message: "文章修改成功!");
-    }
-
-    /// <summary>
-    /// 发布
-    /// </summary>
-    /// <param name="post"></param>
-    /// <param name="timespan"></param>
-    /// <param name="schedule"></param>
-    /// <param name="cancellationToken"></param>
-    /// <returns></returns>
-    [MyAuthorize, HttpPost]
-    public async Task<ActionResult> Write([FromBodyOrDefault] PostCommand post, [FromBodyOrDefault] DateTime? timespan, [FromBodyOrDefault] bool schedule = false, CancellationToken cancellationToken = default)
-    {
-        post.Content = await ImagebedClient.ReplaceImgSrc(await post.Content.Trim().ClearImgAttributes(), cancellationToken);
-        if (!ValidatePost(post, out var resultData))
-        {
-            return resultData;
-        }
-
-        post.Status = Status.Published;
-        Post p = post.Mapper<Post>();
-        p.Modifier = p.Author;
-        p.ModifierEmail = p.Email;
-        p.IP = ClientIP;
-        p.Rss = p.LimitMode is null or RegionLimitMode.All;
-        if (!string.IsNullOrEmpty(post.Seminars))
-        {
-            var tmp = post.Seminars.Split(',').Distinct().Select(int.Parse).ToArray();
-            p.Seminar.AddRange(SeminarService[s => tmp.Contains(s.Id)]);
-        }
-
-        if (schedule)
-        {
-            if (!timespan.HasValue || timespan.Value <= DateTime.Now)
-            {
-                return ResultData(null, false, "如果要定时发布,请选择正确的一个将来时间点!");
-            }
-
-            p.Status = Status.Schedule;
-            p.PostDate = timespan.Value.ToUniversalTime();
-            p.ModifyDate = timespan.Value.ToUniversalTime();
-            BackgroundJob.Enqueue<IHangfireBackJob>(job => job.PublishPost(p));
-            return ResultData(p.Mapper<PostDto>(), message: $"文章于{timespan.Value:yyyy-MM-dd HH:mm:ss}将会自动发表!");
-        }
-
-        PostService.AddEntity(p);
-        (p.Keyword + "," + p.Label).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(KeywordsManager.AddWords);
-        PostTagService.AddOrUpdate(t => t.Name, p.Label.AsNotNull().Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => new PostTag()
-        {
-            Name = s,
-            Count = PostService.Count(t => t.Label.Contains(s))
-        }));
-        bool b = await SearchEngine.SaveChangesAsync() > 0;
-        if (!b)
-        {
-            return ResultData(null, false, "文章发表失败!");
-        }
-
-        if (p.LimitMode == RegionLimitMode.OnlyForSearchEngine)
-        {
-            SearchEngine.LuceneIndexer.Delete(p);
-        }
-
-        return ResultData(null, true, "文章发表成功!");
-    }
-
-    private bool ValidatePost(PostCommand post, out ActionResult resultData)
-    {
-        if (!CategoryService.Any(c => c.Id == post.CategoryId && c.Status == Status.Available))
-        {
-            resultData = ResultData(null, false, "请选择一个分类");
-            return false;
-        }
-
-        switch (post.LimitMode)
-        {
-            case RegionLimitMode.AllowRegion:
-            case RegionLimitMode.ForbidRegion:
-                if (string.IsNullOrEmpty(post.Regions))
-                {
-                    resultData = ResultData(null, false, "请输入限制的地区");
-                    return false;
-                }
-
-                post.Regions = post.Regions.Replace(",", "|").Replace(",", "|");
-                break;
-
-            case RegionLimitMode.AllowRegionExceptForbidRegion:
-            case RegionLimitMode.ForbidRegionExceptAllowRegion:
-                if (string.IsNullOrEmpty(post.ExceptRegions))
-                {
-                    resultData = ResultData(null, false, "请输入排除的地区");
-                    return false;
-                }
-
-                post.ExceptRegions = post.ExceptRegions.Replace(",", "|").Replace(",", "|");
-                goto case RegionLimitMode.AllowRegion;
-        }
-
-        if (string.IsNullOrEmpty(post.Label?.Trim()) || post.Label.Equals("null"))
-        {
-            post.Label = null;
-        }
-        else if (post.Label.Trim().Length > 50)
-        {
-            post.Label = post.Label.Replace(",", ",");
-            post.Label = post.Label.Trim().Substring(0, 50);
-        }
-        else
-        {
-            post.Label = post.Label.Replace(",", ",");
-        }
-
-        if (string.IsNullOrEmpty(post.ProtectContent?.RemoveHtmlTag()) || post.ProtectContent.Equals("null"))
-        {
-            post.ProtectContent = null;
-        }
-
-        resultData = null;
-        return true;
-    }
-
-    /// <summary>
-    /// 添加专题
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="sid"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    public async Task<ActionResult> AddSeminar(int id, int sid)
-    {
-        var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
-        Seminar seminar = await SeminarService.GetByIdAsync(sid) ?? throw new NotFoundException("专题未找到");
-        post.Seminar.Add(seminar);
-        bool b = await PostService.SaveChangesAsync() > 0;
-        return ResultData(null, b, b ? $"已将文章【{post.Title}】添加到专题【{seminar.Title}】" : "添加失败");
-    }
-
-    /// <summary>
-    /// 移除专题
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="sid"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    public async Task<ActionResult> RemoveSeminar(int id, int sid)
-    {
-        var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
-        Seminar seminar = await SeminarService.GetByIdAsync(sid) ?? throw new NotFoundException("专题未找到");
-        post.Seminar.Remove(seminar);
-        bool b = await PostService.SaveChangesAsync() > 0;
-        return ResultData(null, b, b ? $"已将文章【{post.Title}】从【{seminar.Title}】专题移除" : "添加失败");
-    }
-
-    /// <summary>
-    /// 删除历史版本
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    public async Task<ActionResult> DeleteHistory(int id)
-    {
-        bool b = await PostHistoryVersionService.DeleteByIdAsync(id) > 0;
-        return ResultData(null, b, b ? "历史版本文章删除成功!" : "历史版本文章删除失败!");
-    }
-
-    /// <summary>
-    /// 还原版本
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    public async Task<ActionResult> Revert(int id)
-    {
-        var history = await PostHistoryVersionService.GetByIdAsync(id) ?? throw new NotFoundException("版本不存在");
-        history.Post.Category = history.Category;
-        history.Post.CategoryId = history.CategoryId;
-        history.Post.Content = history.Content;
-        history.Post.Title = history.Title;
-        history.Post.Label = history.Label;
-        history.Post.ModifyDate = history.ModifyDate;
-        history.Post.Seminar.Clear();
-        foreach (var s in history.Seminar)
-        {
-            history.Post.Seminar.Add(s);
-        }
-        bool b = await SearchEngine.SaveChangesAsync() > 0;
-        await PostHistoryVersionService.DeleteByIdAsync(id);
-        return ResultData(null, b, b ? "回滚成功" : "回滚失败");
-    }
-
-    /// <summary>
-    /// 禁用或开启文章评论
-    /// </summary>
-    /// <param name="id">文章id</param>
-    /// <returns></returns>
-    [MyAuthorize]
-    [HttpPost("post/{id}/DisableComment")]
-    public async Task<ActionResult> DisableComment(int id)
-    {
-        var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
-        post.DisableComment = !post.DisableComment;
-        return ResultData(null, await PostService.SaveChangesAsync() > 0, post.DisableComment ? $"已禁用【{post.Title}】这篇文章的评论功能!" : $"已启用【{post.Title}】这篇文章的评论功能!");
-    }
-
-    /// <summary>
-    /// 禁用或开启文章评论
-    /// </summary>
-    /// <param name="id">文章id</param>
-    /// <returns></returns>
-    [MyAuthorize]
-    [HttpPost("post/{id}/DisableCopy")]
-    public async Task<ActionResult> DisableCopy(int id)
-    {
-        var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
-        post.DisableCopy = !post.DisableCopy;
-        return ResultData(null, await PostService.SaveChangesAsync() > 0, post.DisableCopy ? $"已开启【{post.Title}】这篇文章的防复制功能!" : $"已关闭【{post.Title}】这篇文章的防复制功能!");
-    }
-
-    /// <summary>
-    /// 修改分类
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="cid"></param>
-    /// <returns></returns>
-    [HttpPost("post/{id}/ChangeCategory/{cid}")]
-    public async Task<ActionResult> ChangeCategory(int id, int cid)
-    {
-        await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(p => p.CategoryId, cid));
-        return Ok();
-    }
-
-    /// <summary>
-    /// 修改专题
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="sids"></param>
-    /// <returns></returns>
-    [HttpPost("post/{id}/ChangeSeminar/{sids}")]
-    public async Task<ActionResult> ChangeSeminar(int id, string sids)
-    {
-        var post = PostService[id] ?? throw new NotFoundException("文章不存在");
-        var ids = sids.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
-        post.Seminar.Clear();
-        post.Seminar.AddRange(SeminarService[s => ids.Contains(s.Id)]);
-        await PostService.SaveChangesAsync();
-        return Ok();
-    }
-
-    /// <summary>
-    /// 刷新文章
-    /// </summary>
-    /// <param name="id">文章id</param>
-    /// <param name="cancellationToken"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    public async Task<ActionResult> Refresh(int id, CancellationToken cancellationToken = default)
-    {
-        await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.ModifyDate, DateTime.Now), cancellationToken: cancellationToken);
-        return RedirectToAction("Details", new { id });
-    }
-
-    /// <summary>
-    /// 标记为恶意修改
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="cancellationToken"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    [HttpPost("post/block/{id}")]
-    public async Task<ActionResult> Block(int id, CancellationToken cancellationToken = default)
-    {
-        var b = await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Status, Status.Forbidden), cancellationToken: cancellationToken) > 0;
-        return b ? ResultData(null, true, "操作成功!") : ResultData(null, false, "操作失败!");
-    }
-
-    /// <summary>
-    /// 切换允许rss订阅
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="cancellationToken"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    [HttpPost("post/{id}/rss-switch")]
-    public async Task<ActionResult> RssSwitch(int id, CancellationToken cancellationToken = default)
-    {
-        await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Rss, p => !p.Rss), cancellationToken: cancellationToken);
-        return ResultData(null, message: "操作成功");
-    }
-
-    /// <summary>
-    /// 切换锁定编辑
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="cancellationToken"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    [HttpPost("post/{id}/locked-switch")]
-    public async Task<ActionResult> LockedSwitch(int id, CancellationToken cancellationToken = default)
-    {
-        await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Locked, p => !p.Locked), cancellationToken: cancellationToken);
-        return ResultData(null, message: "操作成功");
-    }
-
-    /// <summary>
-    /// 文章统计
-    /// </summary>
-    /// <returns></returns>
-    [MyAuthorize]
-    public async Task<IActionResult> Statistic(CancellationToken cancellationToken = default)
-    {
-        var keys = RedisHelper.Keys(nameof(PostOnline) + ":*");
-        var sets = keys.Select(s => (Id: s.Split(':')[1].ToInt32(), Clients: RedisHelper.HGet<HashSet<string>>(s, "value")));
-        var ids = sets.Where(t => t.Clients?.Count > 0).OrderByDescending(t => t.Clients.Count).Take(10).Select(t => t.Id).ToArray();
-        var mostHots = await PostService.GetQuery<PostModelBase>(p => ids.Contains(p.Id)).ToListAsync().ContinueWith(t =>
-        {
-            foreach (var item in t.Result)
-            {
-                item.ViewCount = sets.FirstOrDefault(x => x.Id == item.Id).Clients.Count;
-            }
-
-            return t.Result.OrderByDescending(p => p.ViewCount);
-        });
-        var postsQuery = PostService.GetQuery(p => p.Status == Status.Published);
-        var mostView = await postsQuery.OrderByDescending(p => p.TotalViewCount).Take(10).Select(p => new PostModelBase()
-        {
-            Id = p.Id,
-            Title = p.Title,
-            ViewCount = p.TotalViewCount
-        }).ToListAsync(cancellationToken);
-        var mostAverage = await postsQuery.OrderByDescending(p => p.AverageViewCount).Take(10).Select(p => new PostModelBase()
-        {
-            Id = p.Id,
-            Title = p.Title,
-            ViewCount = (int)p.AverageViewCount
-        }).ToListAsync(cancellationToken);
-        var yesterday = DateTime.Now.AddDays(-1);
-        var trending = await postsQuery.Select(p => new PostModelBase()
-        {
-            Id = p.Id,
-            Title = p.Title,
-            ViewCount = p.PostVisitRecords.Count(t => t.Time >= yesterday)
-        }).OrderByDescending(p => p.ViewCount).Take(10).ToListAsync(cancellationToken);
-        var readCount = PostVisitRecordService.Count(e => e.Time >= yesterday);
-        return ResultData(new
-        {
-            mostHots,
-            mostView,
-            mostAverage,
-            trending,
-            readCount
-        });
-    }
-
-    /// <summary>
-    /// 文章访问记录
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="page"></param>
-    /// <param name="size"></param>
-    /// <returns></returns>
-    [HttpGet("/{id}/records"), MyAuthorize]
-    [ProducesResponseType(typeof(PagedList<PostVisitRecordViewModel>), (int)HttpStatusCode.OK)]
-    public async Task<IActionResult> PostVisitRecords(int id, int page = 1, int size = 15, string kw = "")
-    {
-        Expression<Func<PostVisitRecord, bool>> where = e => e.PostId == id;
-        if (!string.IsNullOrEmpty(kw))
-        {
-            kw = Regex.Escape(kw);
-            where = where.And(e => Regex.IsMatch(e.IP + e.Location + e.Referer + e.RequestUrl, kw, RegexOptions.IgnoreCase));
-        }
-
-        var pages = await PostVisitRecordService.GetPagesAsync<DateTime, PostVisitRecordViewModel>(page, size, where, e => e.Time, false);
-        return Ok(pages);
-    }
-
-    /// <summary>
-    /// 导出文章访问记录
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    [HttpGet("/{id}/records-export"), MyAuthorize]
-    [ProducesResponseType(typeof(PagedList<PostVisitRecordViewModel>), (int)HttpStatusCode.OK)]
-    public IActionResult ExportPostVisitRecords(int id)
-    {
-        using var list = PostVisitRecordService.GetQuery<DateTime, PostVisitRecordViewModel>(e => e.PostId == id, e => e.Time, false).ToPooledList();
-        using var ms = list.ToExcel();
-        var post = PostService[id];
-        return this.ResumeFile(ms.ToArray(), ContentType.Xlsx, post.Title + "访问记录.xlsx");
-    }
-
-    /// <summary>
-    /// 文章访问记录图表
-    /// </summary>
-    /// <returns></returns>
-    [HttpGet("/{id}/records-chart"), MyAuthorize]
-    [ProducesResponseType((int)HttpStatusCode.OK)]
-    public async Task<IActionResult> PostVisitRecordChart([FromServices] IPostVisitRecordStatsService statsService, int id, bool compare, uint period, CancellationToken cancellationToken)
-    {
-        if (compare)
-        {
-            var start1 = DateTime.Today.AddDays(-period);
-            var list1 = await statsService.GetQuery(e => e.PostId == id && e.Date >= start1).GroupBy(t => t.Date).Select(g => new
-            {
-                Date = g.Key,
-                Count = g.Sum(t => t.Count),
-                UV = g.Sum(t => t.UV)
-            }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
-            if (list1.Count == 0)
-            {
-                return Ok(Array.Empty<int>());
-            }
-
-            var start2 = start1.AddDays(-period - 1);
-            var list2 = await statsService.GetQuery(e => e.PostId == id && e.Date >= start2 && e.Date < start1).GroupBy(t => t.Date).Select(g => new
-            {
-                Date = g.Key,
-                Count = g.Sum(t => t.Count),
-                UV = g.Sum(t => t.UV)
-            }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
-
-            // 将数据填充成连续的数据
-            for (var i = start1; i <= DateTime.Today; i = i.AddDays(1))
-            {
-                if (list1.All(a => a.Date != i))
-                {
-                    list1.Add(new { Date = i, Count = 0, UV = 0 });
-                }
-            }
-            for (var i = start2; i < start1; i = i.AddDays(1))
-            {
-                if (list2.All(a => a.Date != i))
-                {
-                    list2.Add(new { Date = i, Count = 0, UV = 0 });
-                }
-            }
-            return Ok(new[] { list1.OrderBy(a => a.Date), list2.OrderBy(a => a.Date) });
-        }
-
-        var list = await statsService.GetQuery(e => e.PostId == id).GroupBy(t => t.Date).Select(g => new
-        {
-            Date = g.Key,
-            Count = g.Sum(t => t.Count),
-            UV = g.Sum(t => t.UV)
-        }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
-        return Ok(new[] { list });
-    }
-
-    /// <summary>
-    /// 文章访问记录图表
-    /// </summary>
-    /// <returns></returns>
-    [HttpGet("/post/records-chart"), MyAuthorize]
-    [ProducesResponseType((int)HttpStatusCode.OK)]
-    public async Task<IActionResult> PostVisitRecordChart(bool compare, uint period, CancellationToken cancellationToken)
-    {
-        if (compare)
-        {
-            var start1 = DateTime.Today.AddDays(-period);
-            var list1 = await PostVisitRecordService.GetQuery(e => e.Time >= start1).Select(e => new { e.Time.Date, e.IP }).GroupBy(t => t.Date).Select(g => new
-            {
-                Date = g.Key,
-                Count = g.Count(),
-                UV = g.Select(e => e.IP).Distinct().Count()
-            }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
-            if (list1.Count == 0)
-            {
-                return Ok(Array.Empty<int>());
-            }
-
-            var start2 = start1.AddDays(-period - 1);
-            var list2 = await PostVisitRecordService.GetQuery(e => e.Time >= start2 && e.Time < start1).Select(e => new { e.Time.Date, e.IP }).GroupBy(t => t.Date).Select(g => new
-            {
-                Date = g.Key,
-                Count = g.Count(),
-                UV = g.Select(e => e.IP).Distinct().Count()
-            }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
-
-            // 将数据填充成连续的数据
-            for (var i = start1; i <= DateTime.Today; i = i.AddDays(1))
-            {
-                if (list1.All(a => a.Date != i))
-                {
-                    list1.Add(new { Date = i, Count = 0, UV = 0 });
-                }
-            }
-            for (var i = start2; i < start1; i = i.AddDays(1))
-            {
-                if (list2.All(a => a.Date != i))
-                {
-                    list2.Add(new { Date = i, Count = 0, UV = 0 });
-                }
-            }
-            return Ok(new[] { list1.OrderBy(a => a.Date), list2.OrderBy(a => a.Date) });
-        }
-
-        var list = await PostVisitRecordService.GetAll().Select(e => new { e.Time.Date, e.IP }).GroupBy(t => t.Date).Select(g => new
-        {
-            Date = g.Key,
-            Count = g.Count(),
-            UV = g.Select(e => e.IP).Distinct().Count()
-        }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
-        return Ok(new[] { list });
-    }
-
-    /// <summary>
-    /// 文章访问记录分析
-    /// </summary>
-    /// <param name="id"></param>
-    /// <returns></returns>
-    [HttpGet("/{id}/insight"), MyAuthorize]
-    [ProducesResponseType(typeof(PagedList<PostVisitRecordViewModel>), (int)HttpStatusCode.OK)]
-    public IActionResult PostVisitRecordInsight(int id)
-    {
-        return View(PostService[id]);
-    }
-
-    /// <summary>
-    /// 获取地区集
-    /// </summary>
-    /// <param name="name"></param>
-    /// <returns></returns>
-    [MyAuthorize]
-    [ProducesResponseType(typeof(List<string>), (int)HttpStatusCode.OK)]
-    public async Task<IActionResult> GetRegions(string name)
-    {
-        return ResultData(await PostService.GetAll().Select(p => EF.Property<string>(p, name)).Distinct().ToListAsync());
-    }
-
-    #endregion 后端管理
+	public IPostService PostService { get; set; }
+
+	public ICategoryService CategoryService { get; set; }
+
+	public ISeminarService SeminarService { get; set; }
+
+	public IPostHistoryVersionService PostHistoryVersionService { get; set; }
+
+	public IWebHostEnvironment HostEnvironment { get; set; }
+
+	public ISearchEngine<DataContext> SearchEngine { get; set; }
+
+	public ImagebedClient ImagebedClient { get; set; }
+
+	public IPostVisitRecordService PostVisitRecordService { get; set; }
+
+	public ICommentService CommentService { get; set; }
+
+	public IPostTagService PostTagService { get; set; }
+
+	/// <summary>
+	/// 文章详情页
+	/// </summary>
+	/// <returns></returns>
+	[Route("{id:int}"), Route("{id:int}/comments/{cid:int}"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
+	public async Task<ActionResult> Details(int id, string kw, int cid, string t)
+	{
+		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 });
+		}
+
+		var post = await PostService.GetQuery(p => p.Id == id && (p.Status == Status.Published || CurrentUser.IsAdmin)).Include(p => p.Seminar).AsNoTracking().FirstOrDefaultAsync() ?? throw new NotFoundException("文章未找到");
+		CheckPermission(post);
+		if (!string.IsNullOrEmpty(post.Redirect))
+		{
+			if (notRobot && string.IsNullOrEmpty(HttpContext.Session.Get<string>("post" + id)))
+			{
+				BackgroundJob.Enqueue<IHangfireBackJob>(job => job.RecordPostVisit(id, ClientIP, Request.Headers[HeaderNames.Referer].ToString(), Request.GetDisplayUrl()));
+				HttpContext.Session.Set("post" + id, id.ToString());
+			}
+
+			return Redirect(post.Redirect);
+		}
+
+		post.Category = CategoryService[post.CategoryId];
+		ViewBag.CommentsCount = CommentService.Count(c => c.PostId == id && c.ParentId == null && c.Status == Status.Published);
+		ViewBag.HistoryCount = PostHistoryVersionService.Count(c => c.PostId == id);
+		ViewBag.Keyword = post.Keyword + "," + post.Label;
+		if (Request.Query.ContainsKey("share"))
+		{
+			ViewBag.Desc = await post.Content.GetSummary(200);
+		}
+		else
+		{
+			ViewBag.Desc = "若页面无法访问,可通过搜索引擎网页快照进行浏览。" + await post.Content.GetSummary(200);
+		}
+
+		var modifyDate = post.ModifyDate;
+		ViewBag.Next = await PostService.GetFromCacheAsync<DateTime, PostModelBase>(p => p.ModifyDate > modifyDate && (p.LimitMode ?? 0) == RegionLimitMode.All && (p.Status == Status.Published || CurrentUser.IsAdmin), p => p.ModifyDate);
+		ViewBag.Prev = await PostService.GetFromCacheAsync<DateTime, PostModelBase>(p => p.ModifyDate < modifyDate && (p.LimitMode ?? 0) == RegionLimitMode.All && (p.Status == Status.Published || CurrentUser.IsAdmin), p => p.ModifyDate, false);
+		ViewData[nameof(post.Author)] = post.Author;
+		ViewData[nameof(post.PostDate)] = post.PostDate;
+		ViewData[nameof(post.ModifyDate)] = post.ModifyDate;
+		ViewData["cover"] = post.Content.MatchFirstImgSrc();
+		if (!string.IsNullOrEmpty(kw))
+		{
+			await PostService.Highlight(post, kw);
+		}
+
+		var regex = SearchEngine.LuceneIndexSearcher.CutKeywords(string.IsNullOrWhiteSpace(post.Keyword + post.Label) ? post.Title : post.Keyword + post.Label).Select(Regex.Escape).Join("|");
+		ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location(), post.CategoryId, regex);
+		var related = PostService.GetQuery(PostBaseWhere().And(p => p.Id != id && Regex.IsMatch(p.Title + (p.Keyword ?? "") + (p.Label ?? ""), regex, RegexOptions.IgnoreCase)), p => p.AverageViewCount, false).Take(10).Select(p => new { p.Id, p.Title }).FromCache().ToDictionary(p => p.Id, p => p.Title);
+		ViewBag.Related = related;
+		post.ModifyDate = post.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		post.PostDate = post.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		post.Content = ReplaceVariables(post.Content);
+		post.ProtectContent = ReplaceVariables(post.ProtectContent);
+
+		if (CurrentUser.IsAdmin)
+		{
+			return View("Details_Admin", post);
+		}
+
+		if (notRobot && string.IsNullOrEmpty(HttpContext.Session.Get<string>("post" + id)))
+		{
+			BackgroundJob.Enqueue<IHangfireBackJob>(job => job.RecordPostVisit(id, ClientIP, Request.Headers[HeaderNames.Referer].ToString(), Request.GetDisplayUrl()));
+			HttpContext.Session.Set("post" + id, id.ToString());
+		}
+
+		if (post.LimitMode == RegionLimitMode.OnlyForSearchEngine)
+		{
+			BackgroundJob.Enqueue<IHangfireBackJob>(job => job.RecordPostVisit(id, ClientIP, Request.Headers[HeaderNames.Referer].ToString(), Request.GetDisplayUrl()));
+		}
+
+		return View(post);
+	}
+
+	/// <summary>
+	/// 文章历史版本
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <returns></returns>
+	[Route("{id:int}/history"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id", "page", "size" }, VaryByHeader = "Cookie")]
+	public async Task<ActionResult> History(int id, [Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 20)
+	{
+		var post = await PostService.GetAsync(p => p.Id == id && (p.Status == Status.Published || CurrentUser.IsAdmin)) ?? throw new NotFoundException("文章未找到");
+		CheckPermission(post);
+		ViewBag.Primary = post;
+		var list = await PostHistoryVersionService.GetPagesAsync(page, size, v => v.PostId == id, v => v.ModifyDate, false);
+		foreach (var item in list.Data)
+		{
+			item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		}
+
+		ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location(), post.CategoryId, post.Keyword + "," + post.Label);
+		return View(list);
+	}
+
+	/// <summary>
+	/// 文章历史版本
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="hid"></param>
+	/// <returns></returns>
+	[Route("{id:int}/history/{hid:int}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id", "hid" }, VaryByHeader = "Cookie")]
+	public async Task<ActionResult> HistoryVersion(int id, int hid)
+	{
+		var history = await PostHistoryVersionService.GetAsync(v => v.Id == hid && (v.Post.Status == Status.Published || CurrentUser.IsAdmin)) ?? throw new NotFoundException("文章未找到");
+		CheckPermission(history.Post);
+		history.Content = ReplaceVariables(history.Content);
+		history.ProtectContent = ReplaceVariables(history.ProtectContent);
+		history.ModifyDate = history.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		var next = await PostHistoryVersionService.GetAsync(p => p.PostId == id && p.ModifyDate > history.ModifyDate, p => p.ModifyDate);
+		var prev = await PostHistoryVersionService.GetAsync(p => p.PostId == id && p.ModifyDate < history.ModifyDate, p => p.ModifyDate, false);
+		ViewBag.Next = next;
+		ViewBag.Prev = prev;
+		ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location(), history.CategoryId, history.Label);
+		ViewData[nameof(history.Post.Author)] = history.Post.Author;
+		ViewData[nameof(history.Post.PostDate)] = history.Post.PostDate;
+		ViewData[nameof(history.ModifyDate)] = history.ModifyDate;
+		ViewData["cover"] = history.Content.MatchFirstImgSrc();
+		return CurrentUser.IsAdmin ? View("HistoryVersion_Admin", history) : View(history);
+	}
+
+	/// <summary>
+	/// 版本对比
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="v1"></param>
+	/// <param name="v2"></param>
+	/// <returns></returns>
+	[Route("{id:int}/history/{v1:int}-{v2:int}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id", "v1", "v2" }, VaryByHeader = "Cookie")]
+	public async Task<ActionResult> CompareVersion(int id, int v1, int v2)
+	{
+		var post = await PostService.GetAsync(p => p.Id == id && (p.Status == Status.Published || CurrentUser.IsAdmin));
+		var main = post.Mapper<PostHistoryVersion>() ?? throw new NotFoundException("文章未找到");
+		CheckPermission(post);
+		var left = v1 <= 0 ? main : await PostHistoryVersionService.GetAsync(v => v.Id == v1) ?? throw new NotFoundException("文章未找到");
+		var right = v2 <= 0 ? main : await PostHistoryVersionService.GetAsync(v => v.Id == v2) ?? throw new NotFoundException("文章未找到");
+		main.Id = id;
+		var diff = new HtmlDiff.HtmlDiff(right.Content, left.Content);
+		var diffOutput = diff.Build();
+		right.Content = ReplaceVariables(Regex.Replace(Regex.Replace(diffOutput, "<ins.+?</ins>", string.Empty), @"<\w+></\w+>", string.Empty));
+		right.ModifyDate = right.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		left.Content = ReplaceVariables(Regex.Replace(Regex.Replace(diffOutput, "<del.+?</del>", string.Empty), @"<\w+></\w+>", string.Empty));
+		left.ModifyDate = left.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		ViewBag.Ads = AdsService.GetsByWeightedPrice(2, AdvertiseType.InPage, Request.Location(), main.CategoryId, main.Label);
+		ViewBag.DisableCopy = post.DisableCopy;
+		return View(new[] { main, left, right });
+	}
+
+	/// <summary>
+	/// 反对
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	public async Task<ActionResult> VoteDown(int id)
+	{
+		if (HttpContext.Session.Get("post-vote" + id) != null)
+		{
+			return ResultData(null, false, "您刚才已经投过票了,感谢您的参与!");
+		}
+
+		var b = await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.VoteDownCount, m => m.VoteDownCount + 1)) > 0;
+		if (b)
+		{
+			HttpContext.Session.Set("post-vote" + id, id.GetBytes());
+		}
+
+		return ResultData(null, b, b ? "投票成功!" : "投票失败!");
+	}
+
+	/// <summary>
+	/// 支持
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	public async Task<ActionResult> VoteUp(int id)
+	{
+		if (HttpContext.Session.Get("post-vote" + id) != null)
+		{
+			return ResultData(null, false, "您刚才已经投过票了,感谢您的参与!");
+		}
+
+		var b = await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.VoteUpCount, m => m.VoteUpCount + 1)) > 0;
+		if (b)
+		{
+			HttpContext.Session.Set("post-vote" + id, id.GetBytes());
+		}
+
+		return ResultData(null, b, b ? "投票成功!" : "投票失败!");
+	}
+
+	/// <summary>
+	/// 投稿页
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult Publish()
+	{
+		return View();
+	}
+
+	/// <summary>
+	/// 发布投稿
+	/// </summary>
+	/// <param name="post"></param>
+	/// <param name="code"></param>
+	/// <param name="cancellationToken"></param>
+	/// <returns></returns>
+	[HttpPost, ValidateAntiForgeryToken]
+	public async Task<ActionResult> Publish(PostCommand post, [Required(ErrorMessage = "验证码不能为空")] string code, CancellationToken cancellationToken)
+	{
+		if (RedisHelper.Get("code:" + post.Email) != code)
+		{
+			return ResultData(null, false, "验证码错误!");
+		}
+
+		if (PostService.Any(p => p.Status == Status.Forbidden && p.Email == post.Email))
+		{
+			return ResultData(null, false, "由于您曾经恶意投稿,该邮箱已经被标记为黑名单,无法进行投稿,如有疑问,请联系网站管理员进行处理。");
+		}
+
+		var match = Regex.Match(post.Title + post.Author + post.Content, CommonHelper.BanRegex);
+		if (match.Success)
+		{
+			LogManager.Info($"提交内容:{post.Title}/{post.Author}/{post.Content},敏感词:{match.Value}");
+			return ResultData(null, false, "您提交的内容包含敏感词,被禁止发表,请检查您的内容后尝试重新提交!");
+		}
+
+		if (!CategoryService.Any(c => c.Id == post.CategoryId))
+		{
+			return ResultData(null, message: "请选择一个分类");
+		}
+
+		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 p = post.Mapper<Post>();
+		p.IP = ClientIP;
+		p.Modifier = p.Author;
+		p.ModifierEmail = p.Email;
+		p.DisableCopy = true;
+		p.Rss = true;
+		PostTagService.AddOrUpdate(t => t.Name, p.Label.AsNotNull().Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => new PostTag()
+		{
+			Name = s,
+			Count = PostService.Count(t => t.Label.Contains(s))
+		}));
+		p = PostService.AddEntitySaved(p);
+		if (p == null)
+		{
+			return ResultData(null, false, "文章发表失败!");
+		}
+
+		RedisHelper.Expire("code:" + p.Email, 1);
+		var content = new Template(await new FileInfo(HostEnvironment.WebRootPath + "/template/publish.html").ShareReadWrite().ReadAllTextAsync(Encoding.UTF8))
+			.Set("link", Url.Action("Details", "Post", new { id = p.Id }, Request.Scheme))
+			.Set("time", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))
+			.Set("title", p.Title).Render();
+		BackgroundJob.Enqueue(() => CommonHelper.SendMail(CommonHelper.SystemSettings["Title"] + "有访客投稿:", content, CommonHelper.SystemSettings["ReceiveEmail"], ClientIP));
+		return ResultData(p.Mapper<PostDto>(), message: "文章发表成功,待站长审核通过以后将显示到列表中!");
+	}
+
+	/// <summary>
+	/// 获取标签
+	/// </summary>
+	/// <returns></returns>
+	[ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
+	public ActionResult GetTag()
+	{
+		return ResultData(PostService.GetTags().Select(x => x.Key).OrderBy(s => s));
+	}
+
+	/// <summary>
+	/// 标签云
+	/// </summary>
+	/// <returns></returns>
+	[Route("all"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
+	public async Task<ActionResult> All()
+	{
+		ViewBag.tags = new Dictionary<string, int>(PostService.GetTags().Where(x => x.Value > 1).OrderBy(x => x.Key));
+		ViewBag.cats = await CategoryService.GetQuery(c => c.Post.Count > 0, c => c.Post.Count, false).Include(c => c.Parent).ThenInclude(c => c.Parent).AsNoTracking().ToDictionaryAsync(c => c.Id, c => c.Path()); //category
+		ViewBag.seminars = await SeminarService.GetAll(c => c.Post.Count, false).AsNoTracking().ToDictionaryAsync(c => c.Id, c => c.Title); //seminars
+		return View();
+	}
+
+	/// <summary>
+	/// 检查访问密码
+	/// </summary>
+	/// <param name="email"></param>
+	/// <param name="token"></param>
+	/// <returns></returns>
+	[HttpPost, ValidateAntiForgeryToken, AllowAccessFirewall]
+	public ActionResult CheckViewToken(string email, string token)
+	{
+		if (string.IsNullOrEmpty(token))
+		{
+			return ResultData(null, false, "请输入访问密码!");
+		}
+
+		var s = RedisHelper.Get("token:" + email);
+		if (token.Equals(s))
+		{
+			HttpContext.Session.Set("AccessViewToken", token);
+			Response.Cookies.Append("Email", email, new CookieOptions
+			{
+				Expires = DateTime.Now.AddYears(1),
+				SameSite = SameSiteMode.Lax
+			});
+			Response.Cookies.Append("PostAccessToken", email.MDString3(AppConfig.BaiduAK), new CookieOptions
+			{
+				Expires = DateTime.Now.AddYears(1),
+				SameSite = SameSiteMode.Lax
+			});
+			return ResultData(null);
+		}
+
+		return ResultData(null, false, "访问密码不正确!");
+	}
+
+	/// <summary>
+	/// 检查授权邮箱
+	/// </summary>
+	/// <param name="email"></param>
+	/// <returns></returns>
+	[HttpPost, ValidateAntiForgeryToken, AllowAccessFirewall]
+	public ActionResult GetViewToken(string email)
+	{
+		var validator = new IsEmailAttribute();
+		if (!validator.IsValid(email))
+		{
+			return ResultData(null, false, validator.ErrorMessage);
+		}
+
+		if (RedisHelper.Exists("get:" + email))
+		{
+			RedisHelper.Expire("get:" + email, 120);
+			return ResultData(null, false, "发送频率限制,请在2分钟后重新尝试发送邮件!请检查你的邮件,若未收到,请检查你的邮箱地址或邮件垃圾箱!");
+		}
+
+		if (!UserInfoService.Any(b => b.Email.Equals(email)))
+		{
+			return ResultData(null, false, "您目前没有权限访问这个链接,请联系站长开通访问权限!");
+		}
+
+		var token = SnowFlake.GetInstance().GetUniqueShortId(6);
+		RedisHelper.Set("token:" + email, token, 86400);
+		BackgroundJob.Enqueue(() => CommonHelper.SendMail(Request.Host + "博客访问验证码", $"{Request.Host}本次验证码是:<span style='color:red'>{token}</span>,有效期为24h,请按时使用!", email, ClientIP));
+		RedisHelper.Set("get:" + email, token, 120);
+		return ResultData(null);
+	}
+
+	/// <summary>
+	/// 文章合并
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpGet("{id}/merge")]
+	public async Task<ActionResult> PushMerge(int id)
+	{
+		var post = await PostService.GetAsync(p => p.Id == id && p.Status == Status.Published && !p.Locked) ?? throw new NotFoundException("文章未找到");
+		CheckPermission(post);
+		return View(post);
+	}
+
+	/// <summary>
+	/// 文章合并
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="mid"></param>
+	/// <returns></returns>
+	[HttpGet("{id}/merge/{mid}")]
+	public async Task<ActionResult> RepushMerge(int id, int mid)
+	{
+		var post = await PostService.GetAsync(p => p.Id == id && p.Status == Status.Published && !p.Locked) ?? throw new NotFoundException("文章未找到");
+		CheckPermission(post);
+		var merge = post.PostMergeRequests.FirstOrDefault(p => p.Id == mid && p.MergeState != MergeStatus.Merged) ?? throw new NotFoundException("待合并文章未找到");
+		return View(merge);
+	}
+
+	/// <summary>
+	/// 文章合并
+	/// </summary>
+	/// <param name="messageService"></param>
+	/// <param name="postMergeRequestService"></param>
+	/// <param name="dto"></param>
+	/// <returns></returns>
+	[HttpPost("{id}/pushmerge")]
+	public async Task<ActionResult> PushMerge([FromServices] IInternalMessageService messageService, [FromServices] IPostMergeRequestService postMergeRequestService, PostMergeRequestCommand dto)
+	{
+		if (RedisHelper.Get("code:" + dto.ModifierEmail) != dto.Code)
+		{
+			return ResultData(null, false, "验证码错误!");
+		}
+
+		var post = await PostService.GetAsync(p => p.Id == dto.PostId && p.Status == Status.Published && !p.Locked) ?? throw new NotFoundException("文章未找到");
+		if (post.Title.Equals(dto.Title) && post.Content.HammingDistance(dto.Content) <= 1)
+		{
+			return ResultData(null, false, "内容未被修改或修改的内容过少(无意义修改)!");
+		}
+
+		#region 合并验证
+
+		if (postMergeRequestService.Any(p => p.ModifierEmail == dto.ModifierEmail && p.MergeState == MergeStatus.Block))
+		{
+			return ResultData(null, false, "由于您曾经多次恶意修改文章,已经被标记为黑名单,无法修改任何文章,如有疑问,请联系网站管理员进行处理。");
+		}
+
+		if (post.PostMergeRequests.Any(p => p.ModifierEmail == dto.ModifierEmail && p.MergeState == MergeStatus.Pending))
+		{
+			return ResultData(null, false, "您已经提交过一次修改请求正在待处理,暂不能继续提交修改请求!");
+		}
+
+		#endregion 合并验证
+
+		#region 直接合并
+
+		if (post.Email.Equals(dto.ModifierEmail))
+		{
+			var history = post.Mapper<PostHistoryVersion>();
+			Mapper.Map(dto, post);
+			post.PostHistoryVersion.Add(history);
+			post.ModifyDate = DateTime.Now;
+			return await PostService.SaveChangesAsync() > 0 ? ResultData(null, true, "你是文章原作者,无需审核,文章已自动更新并在首页展示!") : ResultData(null, false, "操作失败!");
+		}
+
+		#endregion 直接合并
+
+		var merge = post.PostMergeRequests.FirstOrDefault(r => r.Id == dto.Id && r.MergeState != MergeStatus.Merged);
+		if (merge != null)
+		{
+			Mapper.Map(dto, merge);
+			merge.SubmitTime = DateTime.Now;
+			merge.MergeState = MergeStatus.Pending;
+		}
+		else
+		{
+			merge = Mapper.Map<PostMergeRequest>(dto);
+			merge.SubmitTime = DateTime.Now;
+			post.PostMergeRequests.Add(merge);
+		}
+		merge.IP = ClientIP;
+		var b = await PostService.SaveChangesAsync() > 0;
+		if (!b)
+		{
+			return ResultData(null, false, "操作失败!");
+		}
+
+		RedisHelper.Expire("code:" + dto.ModifierEmail, 1);
+		await messageService.AddEntitySavedAsync(new InternalMessage()
+		{
+			Title = $"来自【{dto.Modifier}】对文章《{post.Title}》的修改请求",
+			Content = dto.Title,
+			Link = "#/merge/compare?id=" + merge.Id
+		});
+
+		var htmlDiff = new HtmlDiff.HtmlDiff(post.Content.RemoveHtmlTag(), dto.Content.RemoveHtmlTag());
+		var diff = htmlDiff.Build();
+		var content = new Template(await new FileInfo(HostEnvironment.WebRootPath + "/template/merge-request.html").ShareReadWrite().ReadAllTextAsync(Encoding.UTF8))
+			.Set("title", post.Title)
+			.Set("link", Url.Action("Index", "Dashboard", new { }, Request.Scheme) + "#/merge/compare?id=" + merge.Id)
+			.Set("diff", diff)
+			.Set("host", "//" + Request.Host)
+			.Set("id", merge.Id.ToString())
+			.Render();
+		BackgroundJob.Enqueue(() => CommonHelper.SendMail("博客文章修改请求:", content, CommonHelper.SystemSettings["ReceiveEmail"], ClientIP));
+		return ResultData(null, true, "您的修改请求已提交,已进入审核状态,感谢您的参与!");
+	}
+
+	#region 后端管理
+
+	/// <summary>
+	/// 固顶
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Fixtop(int id)
+	{
+		Post post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
+		post.IsFixedTop = !post.IsFixedTop;
+		bool b = await PostService.SaveChangesAsync() > 0;
+		return b ? ResultData(null, true, post.IsFixedTop ? "置顶成功!" : "取消置顶成功!") : ResultData(null, false, "操作失败!");
+	}
+
+	/// <summary>
+	/// 审核
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Pass(int id)
+	{
+		var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
+		post.Status = Status.Published;
+		post.ModifyDate = DateTime.Now;
+		post.PostDate = DateTime.Now;
+		var b = await PostService.SaveChangesAsync() > 0;
+		if (!b)
+		{
+			return ResultData(null, false, "审核失败!");
+		}
+
+		(post.Keyword + "," + post.Label).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(KeywordsManager.AddWords);
+		SearchEngine.LuceneIndexer.Add(post);
+		return ResultData(null, true, "审核通过!");
+	}
+
+	/// <summary>
+	/// 删除
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Delete(int id)
+	{
+		var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
+		post.Status = Status.Deleted;
+		bool b = await PostService.SaveChangesAsync(true) > 0;
+		SearchEngine.LuceneIndexer.Delete(post);
+		return ResultData(null, b, b ? "删除成功!" : "删除失败!");
+	}
+
+	/// <summary>
+	/// 还原版本
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Restore(int id)
+	{
+		var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
+		post.Status = Status.Published;
+		bool b = await PostService.SaveChangesAsync() > 0;
+		SearchEngine.LuceneIndexer.Add(post);
+		return ResultData(null, b, b ? "恢复成功!" : "恢复失败!");
+	}
+
+	/// <summary>
+	/// 彻底删除文章
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public ActionResult Truncate(int id)
+	{
+		bool b = PostService - id;
+		return ResultData(null, b, b ? "删除成功!" : "删除失败!");
+	}
+
+	/// <summary>
+	/// 获取文章
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public ActionResult Get(int id)
+	{
+		Post post = PostService[id] ?? throw new NotFoundException("文章未找到");
+		PostDto model = post.Mapper<PostDto>();
+		model.Seminars = post.Seminar.Select(s => s.Id).Join(",");
+		return ResultData(model);
+	}
+
+	/// <summary>
+	/// 获取文章分页
+	/// </summary>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> GetPageData([FromServices] ICacheManager<HashSet<string>> cacheManager, int page = 1, [Range(1, 200, ErrorMessage = "页大小必须介于{1}-{2}")] int size = 10, OrderBy orderby = OrderBy.ModifyDate, string kw = "", int? cid = null)
+	{
+		Expression<Func<Post, bool>> where = p => true;
+		if (cid.HasValue)
+		{
+			where = where.And(p => p.CategoryId == cid.Value || p.Category.ParentId == cid.Value || p.Category.Parent.ParentId == cid.Value);
+		}
+
+		if (!string.IsNullOrEmpty(kw))
+		{
+			kw = Regex.Escape(kw);
+			where = where.And(p => Regex.IsMatch(p.Title + p.Author + p.Email + p.Content, kw, RegexOptions.IgnoreCase));
+		}
+
+		var list = orderby switch
+		{
+			OrderBy.Trending => await PostService.GetQuery(where).OrderByDescending(p => p.Status).ThenByDescending(p => p.IsFixedTop).ThenByDescending(p => p.PostVisitRecordStats.Sum(t => t.Count) / p.PostVisitRecordStats.Count).ToPagedListAsync<Post, PostDataModel>(page, size, MapperConfig),
+			_ => await PostService.GetQuery(where).OrderBy($"{nameof(Post.Status)} desc,{nameof(Post.IsFixedTop)} desc,{orderby.GetDisplay()} desc").ToPagedListAsync<Post, PostDataModel>(page, size, MapperConfig)
+		};
+		foreach (var item in list.Data)
+		{
+			item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+			item.PostDate = item.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+			item.Online = cacheManager.Get(nameof(PostOnline) + ":" + item.Id)?.Count ?? 0;
+		}
+
+		return Ok(list);
+	}
+
+	/// <summary>
+	/// 获取未审核文章
+	/// </summary>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <param name="search"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> GetPending([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15, string search = "")
+	{
+		Expression<Func<Post, bool>> where = p => p.Status == Status.Pending;
+		if (!string.IsNullOrEmpty(search))
+		{
+			where = where.And(p => p.Title.Contains(search) || p.Author.Contains(search) || p.Email.Contains(search) || p.Label.Contains(search));
+		}
+
+		var pages = await PostService.GetQuery(where).OrderByDescending(p => p.IsFixedTop).ThenByDescending(p => p.ModifyDate).ToPagedListAsync<Post, PostDataModel>(page, size, MapperConfig);
+		foreach (var item in pages.Data)
+		{
+			item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+			item.PostDate = item.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		}
+
+		return Ok(pages);
+	}
+
+	/// <summary>
+	/// 编辑
+	/// </summary>
+	/// <param name="post"></param>
+	/// <param name="cancellationToken"></param>
+	/// <returns></returns>
+	[HttpPost, MyAuthorize]
+	public async Task<ActionResult> Edit([FromBodyOrDefault] PostCommand post, CancellationToken cancellationToken = default)
+	{
+		post.Content = await ImagebedClient.ReplaceImgSrc(await post.Content.Trim().ClearImgAttributes(), cancellationToken);
+		if (!ValidatePost(post, out var resultData))
+		{
+			return resultData;
+		}
+
+		Post p = await PostService.GetByIdAsync(post.Id);
+		if (post.Reserve && p.Status == Status.Published)
+		{
+			if (p.Content.HammingDistance(post.Content) > 0)
+			{
+				var history = p.Mapper<PostHistoryVersion>();
+				p.PostHistoryVersion.Add(history);
+			}
+
+			if (p.Title.HammingDistance(post.Title) > 10 && CommentService.Any(c => c.PostId == p.Id && c.ParentId == null))
+			{
+				p.Comment.Add(new Comment
+				{
+					Status = Status.Published,
+					NickName = "系统自动评论",
+					Email = p.Email,
+					Content = $"<p style=\"color:red\">温馨提示:由于文章发生了重大更新,本条评论之前的所有评论仅作为原文《{p.Title}》的历史评论保留,不作为本文的最新评论参考,请知悉!了解更多信息,请查阅本文的历史修改记录。</p>",
+					PostId = p.Id,
+					CommentDate = DateTime.Now,
+					IsMaster = true,
+					IsAuthor = true,
+					IP = "127.0.0.1",
+					Location = "内网",
+					GroupTag = SnowFlake.NewId,
+					Path = SnowFlake.NewId,
+				});
+			}
+
+			p.ModifyDate = DateTime.Now;
+			var user = HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
+			post.Modifier = string.IsNullOrEmpty(post.Modifier) ? user.NickName : post.Modifier;
+			post.ModifierEmail = string.IsNullOrEmpty(post.ModifierEmail) ? user.Email : post.ModifierEmail;
+		}
+
+		Mapper.Map(post, p);
+		p.IP = ClientIP;
+		p.Seminar.Clear();
+		if (!string.IsNullOrEmpty(post.Seminars))
+		{
+			var tmp = post.Seminars.Split(',', StringSplitOptions.RemoveEmptyEntries).Distinct().Select(int.Parse).ToArray();
+			var seminars = SeminarService.GetQuery(s => tmp.Contains(s.Id)).ToList();
+			p.Seminar.AddRange(seminars);
+		}
+
+		(p.Keyword + "," + p.Label).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(KeywordsManager.AddWords);
+		PostTagService.AddOrUpdate(t => t.Name, p.Label.AsNotNull().Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => new PostTag()
+		{
+			Name = s,
+			Count = PostService.Count(t => t.Label.Contains(s))
+		}));
+		bool b = await SearchEngine.SaveChangesAsync() > 0;
+		if (!b)
+		{
+			return ResultData(null, false, "文章修改失败!");
+		}
+
+		if (p.LimitMode == RegionLimitMode.OnlyForSearchEngine)
+		{
+			SearchEngine.LuceneIndexer.Delete(p);
+		}
+		return ResultData(p.Mapper<PostDto>(), message: "文章修改成功!");
+	}
+
+	/// <summary>
+	/// 发布
+	/// </summary>
+	/// <param name="post"></param>
+	/// <param name="timespan"></param>
+	/// <param name="schedule"></param>
+	/// <param name="cancellationToken"></param>
+	/// <returns></returns>
+	[MyAuthorize, HttpPost]
+	public async Task<ActionResult> Write([FromBodyOrDefault] PostCommand post, [FromBodyOrDefault] DateTime? timespan, [FromBodyOrDefault] bool schedule = false, CancellationToken cancellationToken = default)
+	{
+		post.Content = await ImagebedClient.ReplaceImgSrc(await post.Content.Trim().ClearImgAttributes(), cancellationToken);
+		if (!ValidatePost(post, out var resultData))
+		{
+			return resultData;
+		}
+
+		post.Status = Status.Published;
+		Post p = post.Mapper<Post>();
+		p.Modifier = p.Author;
+		p.ModifierEmail = p.Email;
+		p.IP = ClientIP;
+		p.Rss = p.LimitMode is null or RegionLimitMode.All;
+		if (!string.IsNullOrEmpty(post.Seminars))
+		{
+			var tmp = post.Seminars.Split(',').Distinct().Select(int.Parse).ToArray();
+			p.Seminar.AddRange(SeminarService[s => tmp.Contains(s.Id)]);
+		}
+
+		if (schedule)
+		{
+			if (!timespan.HasValue || timespan.Value <= DateTime.Now)
+			{
+				return ResultData(null, false, "如果要定时发布,请选择正确的一个将来时间点!");
+			}
+
+			p.Status = Status.Schedule;
+			p.PostDate = timespan.Value.ToUniversalTime();
+			p.ModifyDate = timespan.Value.ToUniversalTime();
+			BackgroundJob.Enqueue<IHangfireBackJob>(job => job.PublishPost(p));
+			return ResultData(p.Mapper<PostDto>(), message: $"文章于{timespan.Value:yyyy-MM-dd HH:mm:ss}将会自动发表!");
+		}
+
+		PostService.AddEntity(p);
+		(p.Keyword + "," + p.Label).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(KeywordsManager.AddWords);
+		PostTagService.AddOrUpdate(t => t.Name, p.Label.AsNotNull().Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => new PostTag()
+		{
+			Name = s,
+			Count = PostService.Count(t => t.Label.Contains(s))
+		}));
+		bool b = await SearchEngine.SaveChangesAsync() > 0;
+		if (!b)
+		{
+			return ResultData(null, false, "文章发表失败!");
+		}
+
+		if (p.LimitMode == RegionLimitMode.OnlyForSearchEngine)
+		{
+			SearchEngine.LuceneIndexer.Delete(p);
+		}
+
+		return ResultData(null, true, "文章发表成功!");
+	}
+
+	private bool ValidatePost(PostCommand post, out ActionResult resultData)
+	{
+		if (!CategoryService.Any(c => c.Id == post.CategoryId && c.Status == Status.Available))
+		{
+			resultData = ResultData(null, false, "请选择一个分类");
+			return false;
+		}
+
+		switch (post.LimitMode)
+		{
+			case RegionLimitMode.AllowRegion:
+			case RegionLimitMode.ForbidRegion:
+				if (string.IsNullOrEmpty(post.Regions))
+				{
+					resultData = ResultData(null, false, "请输入限制的地区");
+					return false;
+				}
+
+				post.Regions = post.Regions.Replace(",", "|").Replace(",", "|");
+				break;
+
+			case RegionLimitMode.AllowRegionExceptForbidRegion:
+			case RegionLimitMode.ForbidRegionExceptAllowRegion:
+				if (string.IsNullOrEmpty(post.ExceptRegions))
+				{
+					resultData = ResultData(null, false, "请输入排除的地区");
+					return false;
+				}
+
+				post.ExceptRegions = post.ExceptRegions.Replace(",", "|").Replace(",", "|");
+				goto case RegionLimitMode.AllowRegion;
+		}
+
+		if (string.IsNullOrEmpty(post.Label?.Trim()) || post.Label.Equals("null"))
+		{
+			post.Label = null;
+		}
+		else if (post.Label.Trim().Length > 50)
+		{
+			post.Label = post.Label.Replace(",", ",");
+			post.Label = post.Label.Trim().Substring(0, 50);
+		}
+		else
+		{
+			post.Label = post.Label.Replace(",", ",");
+		}
+
+		if (string.IsNullOrEmpty(post.ProtectContent?.RemoveHtmlTag()) || post.ProtectContent.Equals("null"))
+		{
+			post.ProtectContent = null;
+		}
+
+		resultData = null;
+		return true;
+	}
+
+	/// <summary>
+	/// 添加专题
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="sid"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> AddSeminar(int id, int sid)
+	{
+		var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
+		Seminar seminar = await SeminarService.GetByIdAsync(sid) ?? throw new NotFoundException("专题未找到");
+		post.Seminar.Add(seminar);
+		bool b = await PostService.SaveChangesAsync() > 0;
+		return ResultData(null, b, b ? $"已将文章【{post.Title}】添加到专题【{seminar.Title}】" : "添加失败");
+	}
+
+	/// <summary>
+	/// 移除专题
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="sid"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> RemoveSeminar(int id, int sid)
+	{
+		var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
+		Seminar seminar = await SeminarService.GetByIdAsync(sid) ?? throw new NotFoundException("专题未找到");
+		post.Seminar.Remove(seminar);
+		bool b = await PostService.SaveChangesAsync() > 0;
+		return ResultData(null, b, b ? $"已将文章【{post.Title}】从【{seminar.Title}】专题移除" : "添加失败");
+	}
+
+	/// <summary>
+	/// 删除历史版本
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> DeleteHistory(int id)
+	{
+		bool b = await PostHistoryVersionService.DeleteByIdAsync(id) > 0;
+		return ResultData(null, b, b ? "历史版本文章删除成功!" : "历史版本文章删除失败!");
+	}
+
+	/// <summary>
+	/// 还原版本
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Revert(int id)
+	{
+		var history = await PostHistoryVersionService.GetByIdAsync(id) ?? throw new NotFoundException("版本不存在");
+		history.Post.Category = history.Category;
+		history.Post.CategoryId = history.CategoryId;
+		history.Post.Content = history.Content;
+		history.Post.Title = history.Title;
+		history.Post.Label = history.Label;
+		history.Post.ModifyDate = history.ModifyDate;
+		history.Post.Seminar.Clear();
+		foreach (var s in history.Seminar)
+		{
+			history.Post.Seminar.Add(s);
+		}
+		bool b = await SearchEngine.SaveChangesAsync() > 0;
+		await PostHistoryVersionService.DeleteByIdAsync(id);
+		return ResultData(null, b, b ? "回滚成功" : "回滚失败");
+	}
+
+	/// <summary>
+	/// 禁用或开启文章评论
+	/// </summary>
+	/// <param name="id">文章id</param>
+	/// <returns></returns>
+	[MyAuthorize]
+	[HttpPost("post/{id}/DisableComment")]
+	public async Task<ActionResult> DisableComment(int id)
+	{
+		var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
+		post.DisableComment = !post.DisableComment;
+		return ResultData(null, await PostService.SaveChangesAsync() > 0, post.DisableComment ? $"已禁用【{post.Title}】这篇文章的评论功能!" : $"已启用【{post.Title}】这篇文章的评论功能!");
+	}
+
+	/// <summary>
+	/// 禁用或开启文章评论
+	/// </summary>
+	/// <param name="id">文章id</param>
+	/// <returns></returns>
+	[MyAuthorize]
+	[HttpPost("post/{id}/DisableCopy")]
+	public async Task<ActionResult> DisableCopy(int id)
+	{
+		var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
+		post.DisableCopy = !post.DisableCopy;
+		return ResultData(null, await PostService.SaveChangesAsync() > 0, post.DisableCopy ? $"已开启【{post.Title}】这篇文章的防复制功能!" : $"已关闭【{post.Title}】这篇文章的防复制功能!");
+	}
+
+	/// <summary>
+	/// 修改分类
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="cid"></param>
+	/// <returns></returns>
+	[HttpPost("post/{id}/ChangeCategory/{cid}")]
+	public async Task<ActionResult> ChangeCategory(int id, int cid)
+	{
+		await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(p => p.CategoryId, cid));
+		return Ok();
+	}
+
+	/// <summary>
+	/// 修改专题
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="sids"></param>
+	/// <returns></returns>
+	[HttpPost("post/{id}/ChangeSeminar/{sids}")]
+	public async Task<ActionResult> ChangeSeminar(int id, string sids)
+	{
+		var post = PostService[id] ?? throw new NotFoundException("文章不存在");
+		var ids = sids.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
+		post.Seminar.Clear();
+		post.Seminar.AddRange(SeminarService[s => ids.Contains(s.Id)]);
+		await PostService.SaveChangesAsync();
+		return Ok();
+	}
+
+	/// <summary>
+	/// 刷新文章
+	/// </summary>
+	/// <param name="id">文章id</param>
+	/// <param name="cancellationToken"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Refresh(int id, CancellationToken cancellationToken = default)
+	{
+		await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.ModifyDate, DateTime.Now), cancellationToken: cancellationToken);
+		return RedirectToAction("Details", new { id });
+	}
+
+	/// <summary>
+	/// 标记为恶意修改
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="cancellationToken"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	[HttpPost("post/block/{id}")]
+	public async Task<ActionResult> Block(int id, CancellationToken cancellationToken = default)
+	{
+		var b = await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Status, Status.Forbidden), cancellationToken: cancellationToken) > 0;
+		return b ? ResultData(null, true, "操作成功!") : ResultData(null, false, "操作失败!");
+	}
+
+	/// <summary>
+	/// 切换允许rss订阅
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="cancellationToken"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	[HttpPost("post/{id}/rss-switch")]
+	public async Task<ActionResult> RssSwitch(int id, CancellationToken cancellationToken = default)
+	{
+		await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Rss, p => !p.Rss), cancellationToken: cancellationToken);
+		return ResultData(null, message: "操作成功");
+	}
+
+	/// <summary>
+	/// 切换锁定编辑
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="cancellationToken"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	[HttpPost("post/{id}/locked-switch")]
+	public async Task<ActionResult> LockedSwitch(int id, CancellationToken cancellationToken = default)
+	{
+		await PostService.GetQuery(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Locked, p => !p.Locked), cancellationToken: cancellationToken);
+		return ResultData(null, message: "操作成功");
+	}
+
+	/// <summary>
+	/// 文章统计
+	/// </summary>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<IActionResult> Statistic(CancellationToken cancellationToken = default)
+	{
+		var keys = RedisHelper.Keys(nameof(PostOnline) + ":*");
+		var sets = keys.Select(s => (Id: s.Split(':')[1].ToInt32(), Clients: RedisHelper.HGet<HashSet<string>>(s, "value")));
+		var ids = sets.Where(t => t.Clients?.Count > 0).OrderByDescending(t => t.Clients.Count).Take(10).Select(t => t.Id).ToArray();
+		var mostHots = await PostService.GetQuery<PostModelBase>(p => ids.Contains(p.Id)).ToListAsync().ContinueWith(t =>
+		{
+			foreach (var item in t.Result)
+			{
+				item.ViewCount = sets.FirstOrDefault(x => x.Id == item.Id).Clients.Count;
+			}
+
+			return t.Result.OrderByDescending(p => p.ViewCount);
+		});
+		var postsQuery = PostService.GetQuery(p => p.Status == Status.Published);
+		var mostView = await postsQuery.OrderByDescending(p => p.TotalViewCount).Take(10).Select(p => new PostModelBase()
+		{
+			Id = p.Id,
+			Title = p.Title,
+			ViewCount = p.TotalViewCount
+		}).ToListAsync(cancellationToken);
+		var mostAverage = await postsQuery.OrderByDescending(p => p.AverageViewCount).Take(10).Select(p => new PostModelBase()
+		{
+			Id = p.Id,
+			Title = p.Title,
+			ViewCount = (int)p.AverageViewCount
+		}).ToListAsync(cancellationToken);
+		var yesterday = DateTime.Now.AddDays(-1);
+		var trending = await postsQuery.Select(p => new PostModelBase()
+		{
+			Id = p.Id,
+			Title = p.Title,
+			ViewCount = p.PostVisitRecords.Count(t => t.Time >= yesterday)
+		}).OrderByDescending(p => p.ViewCount).Take(10).ToListAsync(cancellationToken);
+		var readCount = PostVisitRecordService.Count(e => e.Time >= yesterday);
+		return ResultData(new
+		{
+			mostHots,
+			mostView,
+			mostAverage,
+			trending,
+			readCount
+		});
+	}
+
+	/// <summary>
+	/// 文章访问记录
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <returns></returns>
+	[HttpGet("/{id}/records"), MyAuthorize]
+	[ProducesResponseType(typeof(PagedList<PostVisitRecordViewModel>), (int)HttpStatusCode.OK)]
+	public async Task<IActionResult> PostVisitRecords(int id, int page = 1, int size = 15, string kw = "")
+	{
+		Expression<Func<PostVisitRecord, bool>> where = e => e.PostId == id;
+		if (!string.IsNullOrEmpty(kw))
+		{
+			kw = Regex.Escape(kw);
+			where = where.And(e => Regex.IsMatch(e.IP + e.Location + e.Referer + e.RequestUrl, kw, RegexOptions.IgnoreCase));
+		}
+
+		var pages = await PostVisitRecordService.GetPagesAsync<DateTime, PostVisitRecordViewModel>(page, size, where, e => e.Time, false);
+		return Ok(pages);
+	}
+
+	/// <summary>
+	/// 导出文章访问记录
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpGet("/{id}/records-export"), MyAuthorize]
+	[ProducesResponseType(typeof(PagedList<PostVisitRecordViewModel>), (int)HttpStatusCode.OK)]
+	public IActionResult ExportPostVisitRecords(int id)
+	{
+		var list = PostVisitRecordService.GetQuery<DateTime, PostVisitRecordViewModel>(e => e.PostId == id, e => e.Time, false).ToList();
+		using var ms = list.ToExcel();
+		var post = PostService[id];
+		return this.ResumeFile(ms.ToArray(), ContentType.Xlsx, post.Title + "访问记录.xlsx");
+	}
+
+	/// <summary>
+	/// 文章访问记录图表
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("/{id}/records-chart"), MyAuthorize]
+	[ProducesResponseType((int)HttpStatusCode.OK)]
+	public async Task<IActionResult> PostVisitRecordChart([FromServices] IPostVisitRecordStatsService statsService, int id, bool compare, uint period, CancellationToken cancellationToken)
+	{
+		if (compare)
+		{
+			var start1 = DateTime.Today.AddDays(-period);
+			var list1 = await statsService.GetQuery(e => e.PostId == id && e.Date >= start1).GroupBy(t => t.Date).Select(g => new
+			{
+				Date = g.Key,
+				Count = g.Sum(t => t.Count),
+				UV = g.Sum(t => t.UV)
+			}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
+			if (list1.Count == 0)
+			{
+				return Ok(Array.Empty<int>());
+			}
+
+			var start2 = start1.AddDays(-period - 1);
+			var list2 = await statsService.GetQuery(e => e.PostId == id && e.Date >= start2 && e.Date < start1).GroupBy(t => t.Date).Select(g => new
+			{
+				Date = g.Key,
+				Count = g.Sum(t => t.Count),
+				UV = g.Sum(t => t.UV)
+			}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
+
+			// 将数据填充成连续的数据
+			for (var i = start1; i <= DateTime.Today; i = i.AddDays(1))
+			{
+				if (list1.All(a => a.Date != i))
+				{
+					list1.Add(new { Date = i, Count = 0, UV = 0 });
+				}
+			}
+			for (var i = start2; i < start1; i = i.AddDays(1))
+			{
+				if (list2.All(a => a.Date != i))
+				{
+					list2.Add(new { Date = i, Count = 0, UV = 0 });
+				}
+			}
+			return Ok(new[] { list1.OrderBy(a => a.Date), list2.OrderBy(a => a.Date) });
+		}
+
+		var list = await statsService.GetQuery(e => e.PostId == id).GroupBy(t => t.Date).Select(g => new
+		{
+			Date = g.Key,
+			Count = g.Sum(t => t.Count),
+			UV = g.Sum(t => t.UV)
+		}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
+		return Ok(new[] { list });
+	}
+
+	/// <summary>
+	/// 文章访问记录图表
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("/post/records-chart"), MyAuthorize]
+	[ProducesResponseType((int)HttpStatusCode.OK)]
+	public async Task<IActionResult> PostVisitRecordChart(bool compare, uint period, CancellationToken cancellationToken)
+	{
+		if (compare)
+		{
+			var start1 = DateTime.Today.AddDays(-period);
+			var list1 = await PostVisitRecordService.GetQuery(e => e.Time >= start1).Select(e => new { e.Time.Date, e.IP }).GroupBy(t => t.Date).Select(g => new
+			{
+				Date = g.Key,
+				Count = g.Count(),
+				UV = g.Select(e => e.IP).Distinct().Count()
+			}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
+			if (list1.Count == 0)
+			{
+				return Ok(Array.Empty<int>());
+			}
+
+			var start2 = start1.AddDays(-period - 1);
+			var list2 = await PostVisitRecordService.GetQuery(e => e.Time >= start2 && e.Time < start1).Select(e => new { e.Time.Date, e.IP }).GroupBy(t => t.Date).Select(g => new
+			{
+				Date = g.Key,
+				Count = g.Count(),
+				UV = g.Select(e => e.IP).Distinct().Count()
+			}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
+
+			// 将数据填充成连续的数据
+			for (var i = start1; i <= DateTime.Today; i = i.AddDays(1))
+			{
+				if (list1.All(a => a.Date != i))
+				{
+					list1.Add(new { Date = i, Count = 0, UV = 0 });
+				}
+			}
+			for (var i = start2; i < start1; i = i.AddDays(1))
+			{
+				if (list2.All(a => a.Date != i))
+				{
+					list2.Add(new { Date = i, Count = 0, UV = 0 });
+				}
+			}
+			return Ok(new[] { list1.OrderBy(a => a.Date), list2.OrderBy(a => a.Date) });
+		}
+
+		var list = await PostVisitRecordService.GetAll().Select(e => new { e.Time.Date, e.IP }).GroupBy(t => t.Date).Select(g => new
+		{
+			Date = g.Key,
+			Count = g.Count(),
+			UV = g.Select(e => e.IP).Distinct().Count()
+		}).OrderBy(a => a.Date).ToListAsync(cancellationToken);
+		return Ok(new[] { list });
+	}
+
+	/// <summary>
+	/// 文章访问记录分析
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpGet("/{id}/insight"), MyAuthorize]
+	[ProducesResponseType(typeof(PagedList<PostVisitRecordViewModel>), (int)HttpStatusCode.OK)]
+	public IActionResult PostVisitRecordInsight(int id)
+	{
+		return View(PostService[id]);
+	}
+
+	/// <summary>
+	/// 获取地区集
+	/// </summary>
+	/// <param name="name"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	[ProducesResponseType(typeof(List<string>), (int)HttpStatusCode.OK)]
+	public async Task<IActionResult> GetRegions(string name)
+	{
+		return ResultData(await PostService.GetAll().Select(p => EF.Property<string>(p, name)).Distinct().ToListAsync());
+	}
+
+	#endregion 后端管理
 }

+ 324 - 325
src/Masuit.MyBlogs.Core/Controllers/SubscribeController.cs

@@ -1,5 +1,4 @@
-using Collections.Pooled;
-using FreeRedis;
+using FreeRedis;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
 using Masuit.MyBlogs.Core.Extensions.Firewall;
@@ -25,347 +24,347 @@ using Z.EntityFramework.Plus;
 
 namespace Masuit.MyBlogs.Core.Controllers
 {
-    /// <summary>
-    /// 订阅服务
-    /// </summary>
-    public class SubscribeController : Controller
-    {
-        public IPostService PostService { get; set; }
+	/// <summary>
+	/// 订阅服务
+	/// </summary>
+	public class SubscribeController : Controller
+	{
+		public IPostService PostService { get; set; }
 
-        public IAdvertisementService AdvertisementService { get; set; }
-        public IRedisClient RedisClient { get; set; }
+		public IAdvertisementService AdvertisementService { get; set; }
+		public IRedisClient RedisClient { get; set; }
 
-        /// <summary>
-        /// RSS订阅
-        /// </summary>
-        /// <returns></returns>
-        [Route("/rss"), ResponseCache(Duration = 3600)]
-        public async Task<IActionResult> Rss()
-        {
-            if (CommonHelper.SystemSettings.GetOrAdd("EnableRss", "true") != "true")
-            {
-                throw new NotFoundException("不允许订阅");
-            }
+		/// <summary>
+		/// RSS订阅
+		/// </summary>
+		/// <returns></returns>
+		[Route("/rss"), ResponseCache(Duration = 3600)]
+		public async Task<IActionResult> Rss()
+		{
+			if (CommonHelper.SystemSettings.GetOrAdd("EnableRss", "true") != "true")
+			{
+				throw new NotFoundException("不允许订阅");
+			}
 
-            var time = DateTime.Today.AddDays(-1);
-            string scheme = Request.Scheme;
-            var host = Request.Host;
-            using var raw = PostService.GetQuery(PostBaseWhere().And(p => p.Rss && p.ModifyDate >= time), p => p.ModifyDate, false).Include(p => p.Category).AsNoTracking().FromCache(new MemoryCacheEntryOptions()
-            {
-                AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
-            }).ToPooledList();
-            var data = await raw.SelectAsync(async p =>
-            {
-                var summary = await p.Content.GetSummary(300, 50);
-                return new Item()
-                {
-                    Author = new Author
-                    {
-                        Name = p.Modifier
-                    },
-                    Body = summary,
-                    Categories = new List<string>
-                    {
-                        p.Category.Name
-                    },
-                    Link = new Uri(scheme + "://" + host + "/" + p.Id),
-                    PublishDate = p.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone)),
-                    Title = p.Title,
-                    Permalink = scheme + "://" + host + "/" + p.Id,
-                    Guid = p.Id.ToString(),
-                    FullHtmlContent = summary
-                };
-            });
-            var posts = data.ToList();
-            InsertAdvertisement(posts);
-            var feed = new Feed()
-            {
-                Title = CommonHelper.SystemSettings["Title"],
-                Description = CommonHelper.SystemSettings["Description"],
-                Link = new Uri(scheme + "://" + host + "/rss"),
-                Copyright = CommonHelper.SystemSettings["Title"],
-                Language = "zh-cn",
-                Items = posts
-            };
-            var rss = feed.Serialize(new SerializeOption()
-            {
-                Encoding = Encoding.UTF8
-            });
-            return Content(rss, ContentType.Xml);
-        }
+			var time = DateTime.Today.AddDays(-1);
+			string scheme = Request.Scheme;
+			var host = Request.Host;
+			var raw = PostService.GetQuery(PostBaseWhere().And(p => p.Rss && p.ModifyDate >= time), p => p.ModifyDate, false).Include(p => p.Category).AsNoTracking().FromCache(new MemoryCacheEntryOptions()
+			{
+				AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
+			}).ToList();
+			var data = await raw.SelectAsync(async p =>
+			{
+				var summary = await p.Content.GetSummary(300, 50);
+				return new Item()
+				{
+					Author = new Author
+					{
+						Name = p.Modifier
+					},
+					Body = summary,
+					Categories = new List<string>
+					{
+						p.Category.Name
+					},
+					Link = new Uri(scheme + "://" + host + "/" + p.Id),
+					PublishDate = p.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone)),
+					Title = p.Title,
+					Permalink = scheme + "://" + host + "/" + p.Id,
+					Guid = p.Id.ToString(),
+					FullHtmlContent = summary
+				};
+			});
+			var posts = data.ToList();
+			InsertAdvertisement(posts);
+			var feed = new Feed()
+			{
+				Title = CommonHelper.SystemSettings["Title"],
+				Description = CommonHelper.SystemSettings["Description"],
+				Link = new Uri(scheme + "://" + host + "/rss"),
+				Copyright = CommonHelper.SystemSettings["Title"],
+				Language = "zh-cn",
+				Items = posts
+			};
+			var rss = feed.Serialize(new SerializeOption()
+			{
+				Encoding = Encoding.UTF8
+			});
+			return Content(rss, ContentType.Xml);
+		}
 
-        private void InsertAdvertisement(IList<Item> posts, int? cid = null, string keywords = "")
-        {
-            if (posts.Count > 2)
-            {
-                var ad = AdvertisementService.GetByWeightedPrice((AdvertiseType)(DateTime.Now.Second % 4 + 1), Request.Location(), cid, keywords);
-                if (ad is not null)
-                {
-                    posts.Insert(new Random().Next(1, posts.Count), new Item()
-                    {
-                        Author = new Author()
-                        {
-                            Name = ad.Title
-                        },
-                        Body = ad.Description,
-                        Title = ad.Title,
-                        FullHtmlContent = ad.Description,
-                        Guid = SnowFlake.NewId,
-                        PublishDate = DateTime.UtcNow,
-                        Link = new Uri(Url.ActionLink("Redirect", "Advertisement", new { id = ad.Id })),
-                        Permalink = Url.ActionLink("Redirect", "Advertisement", new { id = ad.Id })
-                    });
-                }
-            }
-        }
+		private void InsertAdvertisement(IList<Item> posts, int? cid = null, string keywords = "")
+		{
+			if (posts.Count > 2)
+			{
+				var ad = AdvertisementService.GetByWeightedPrice((AdvertiseType)(DateTime.Now.Second % 4 + 1), Request.Location(), cid, keywords);
+				if (ad is not null)
+				{
+					posts.Insert(new Random().Next(1, posts.Count), new Item()
+					{
+						Author = new Author()
+						{
+							Name = ad.Title
+						},
+						Body = ad.Description,
+						Title = ad.Title,
+						FullHtmlContent = ad.Description,
+						Guid = SnowFlake.NewId,
+						PublishDate = DateTime.UtcNow,
+						Link = new Uri(Url.ActionLink("Redirect", "Advertisement", new { id = ad.Id })),
+						Permalink = Url.ActionLink("Redirect", "Advertisement", new { id = ad.Id })
+					});
+				}
+			}
+		}
 
-        /// <summary>
-        /// RSS分类订阅
-        /// </summary>
-        /// <returns></returns>
-        [Route("/cat/{id}/rss"), ResponseCache(Duration = 3600)]
-        public async Task<IActionResult> CategoryRss([FromServices] ICategoryService categoryService, int id)
-        {
-            if (CommonHelper.SystemSettings.GetOrAdd("EnableRss", "true") != "true")
-            {
-                throw new NotFoundException("不允许订阅");
-            }
+		/// <summary>
+		/// RSS分类订阅
+		/// </summary>
+		/// <returns></returns>
+		[Route("/cat/{id}/rss"), ResponseCache(Duration = 3600)]
+		public async Task<IActionResult> CategoryRss([FromServices] ICategoryService categoryService, int id)
+		{
+			if (CommonHelper.SystemSettings.GetOrAdd("EnableRss", "true") != "true")
+			{
+				throw new NotFoundException("不允许订阅");
+			}
 
-            var time = DateTime.Today.AddDays(-1);
-            string scheme = Request.Scheme;
-            var host = Request.Host;
-            var category = await categoryService.GetByIdAsync(id) ?? throw new NotFoundException("分类未找到");
-            var cids = category.Flatten().Select(c => c.Id).ToArray();
-            using var raw = PostService.GetQuery(PostBaseWhere().And(p => p.Rss && cids.Contains(p.CategoryId) && p.ModifyDate >= time), p => p.ModifyDate, false).Include(p => p.Category).AsNoTracking().FromCache(new MemoryCacheEntryOptions()
-            {
-                AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
-            }).ToPooledList();
-            var data = await raw.SelectAsync(async p =>
-            {
-                var summary = await p.Content.GetSummary(300, 50);
-                return new Item()
-                {
-                    Author = new Author
-                    {
-                        Name = p.Modifier
-                    },
-                    Body = summary,
-                    Categories = new List<string>
-                    {
-                        p.Category.Name
-                    },
-                    Link = new Uri(scheme + "://" + host + "/" + p.Id),
-                    PublishDate = p.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone)),
-                    Title = p.Title,
-                    Permalink = scheme + "://" + host + "/" + p.Id,
-                    Guid = p.Id.ToString(),
-                    FullHtmlContent = summary
-                };
-            });
-            var posts = data.ToList();
-            InsertAdvertisement(posts, id, category.Name);
-            var feed = new Feed()
-            {
-                Title = Request.Host + $":分类{category.Name}文章订阅",
-                Description = category.Description,
-                Link = new Uri(scheme + "://" + host + "/rss"),
-                Copyright = CommonHelper.SystemSettings["Title"],
-                Language = "zh-cn",
-                Items = posts
-            };
-            var rss = feed.Serialize(new SerializeOption()
-            {
-                Encoding = Encoding.UTF8
-            });
-            return Content(rss, ContentType.Xml);
-        }
+			var time = DateTime.Today.AddDays(-1);
+			string scheme = Request.Scheme;
+			var host = Request.Host;
+			var category = await categoryService.GetByIdAsync(id) ?? throw new NotFoundException("分类未找到");
+			var cids = category.Flatten().Select(c => c.Id).ToArray();
+			var raw = PostService.GetQuery(PostBaseWhere().And(p => p.Rss && cids.Contains(p.CategoryId) && p.ModifyDate >= time), p => p.ModifyDate, false).Include(p => p.Category).AsNoTracking().FromCache(new MemoryCacheEntryOptions()
+			{
+				AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
+			}).ToList();
+			var data = await raw.SelectAsync(async p =>
+			{
+				var summary = await p.Content.GetSummary(300, 50);
+				return new Item()
+				{
+					Author = new Author
+					{
+						Name = p.Modifier
+					},
+					Body = summary,
+					Categories = new List<string>
+					{
+						p.Category.Name
+					},
+					Link = new Uri(scheme + "://" + host + "/" + p.Id),
+					PublishDate = p.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone)),
+					Title = p.Title,
+					Permalink = scheme + "://" + host + "/" + p.Id,
+					Guid = p.Id.ToString(),
+					FullHtmlContent = summary
+				};
+			});
+			var posts = data.ToList();
+			InsertAdvertisement(posts, id, category.Name);
+			var feed = new Feed()
+			{
+				Title = Request.Host + $":分类{category.Name}文章订阅",
+				Description = category.Description,
+				Link = new Uri(scheme + "://" + host + "/rss"),
+				Copyright = CommonHelper.SystemSettings["Title"],
+				Language = "zh-cn",
+				Items = posts
+			};
+			var rss = feed.Serialize(new SerializeOption()
+			{
+				Encoding = Encoding.UTF8
+			});
+			return Content(rss, ContentType.Xml);
+		}
 
-        /// <summary>
-        /// RSS专题订阅
-        /// </summary>
-        /// <returns></returns>
-        [Route("/special/{id}/rss"), ResponseCache(Duration = 3600)]
-        public async Task<IActionResult> SeminarRss([FromServices] ISeminarService seminarService, int id)
-        {
-            if (CommonHelper.SystemSettings.GetOrAdd("EnableRss", "true") != "true")
-            {
-                throw new NotFoundException("不允许订阅");
-            }
+		/// <summary>
+		/// RSS专题订阅
+		/// </summary>
+		/// <returns></returns>
+		[Route("/special/{id}/rss"), ResponseCache(Duration = 3600)]
+		public async Task<IActionResult> SeminarRss([FromServices] ISeminarService seminarService, int id)
+		{
+			if (CommonHelper.SystemSettings.GetOrAdd("EnableRss", "true") != "true")
+			{
+				throw new NotFoundException("不允许订阅");
+			}
 
-            var time = DateTime.Today.AddDays(-1);
-            string scheme = Request.Scheme;
-            var host = Request.Host;
-            var seminar = await seminarService.GetByIdAsync(id) ?? throw new NotFoundException("专题未找到");
-            using var raw = PostService.GetQuery(PostBaseWhere().And(p => p.Rss && p.Seminar.Any(s => s.Id == id) && p.ModifyDate >= time), p => p.ModifyDate, false).Include(p => p.Category).AsNoTracking().FromCache(new MemoryCacheEntryOptions()
-            {
-                AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
-            }).ToPooledList();
-            var data = await raw.SelectAsync(async p =>
-            {
-                var summary = await p.Content.GetSummary(300, 50);
-                return new Item()
-                {
-                    Author = new Author
-                    {
-                        Name = p.Modifier
-                    },
-                    Body = summary,
-                    Categories = new List<string>
-                    {
-                        p.Category.Name
-                    },
-                    Link = new Uri(scheme + "://" + host + "/" + p.Id),
-                    PublishDate = p.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone)),
-                    Title = p.Title,
-                    Permalink = scheme + "://" + host + "/" + p.Id,
-                    Guid = p.Id.ToString(),
-                    FullHtmlContent = summary
-                };
-            });
-            var posts = data.ToList();
-            InsertAdvertisement(posts, id, seminar.Title);
-            var feed = new Feed()
-            {
-                Title = Request.Host + $":专题{seminar.Title}文章订阅",
-                Description = seminar.Description,
-                Link = new Uri(scheme + "://" + host + "/rss"),
-                Copyright = CommonHelper.SystemSettings["Title"],
-                Language = "zh-cn",
-                Items = posts
-            };
-            var rss = feed.Serialize(new SerializeOption()
-            {
-                Encoding = Encoding.UTF8
-            });
-            return Content(rss, ContentType.Xml);
-        }
+			var time = DateTime.Today.AddDays(-1);
+			string scheme = Request.Scheme;
+			var host = Request.Host;
+			var seminar = await seminarService.GetByIdAsync(id) ?? throw new NotFoundException("专题未找到");
+			var raw = PostService.GetQuery(PostBaseWhere().And(p => p.Rss && p.Seminar.Any(s => s.Id == id) && p.ModifyDate >= time), p => p.ModifyDate, false).Include(p => p.Category).AsNoTracking().FromCache(new MemoryCacheEntryOptions()
+			{
+				AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
+			}).ToList();
+			var data = await raw.SelectAsync(async p =>
+			{
+				var summary = await p.Content.GetSummary(300, 50);
+				return new Item()
+				{
+					Author = new Author
+					{
+						Name = p.Modifier
+					},
+					Body = summary,
+					Categories = new List<string>
+					{
+						p.Category.Name
+					},
+					Link = new Uri(scheme + "://" + host + "/" + p.Id),
+					PublishDate = p.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone)),
+					Title = p.Title,
+					Permalink = scheme + "://" + host + "/" + p.Id,
+					Guid = p.Id.ToString(),
+					FullHtmlContent = summary
+				};
+			});
+			var posts = data.ToList();
+			InsertAdvertisement(posts, id, seminar.Title);
+			var feed = new Feed()
+			{
+				Title = Request.Host + $":专题{seminar.Title}文章订阅",
+				Description = seminar.Description,
+				Link = new Uri(scheme + "://" + host + "/rss"),
+				Copyright = CommonHelper.SystemSettings["Title"],
+				Language = "zh-cn",
+				Items = posts
+			};
+			var rss = feed.Serialize(new SerializeOption()
+			{
+				Encoding = Encoding.UTF8
+			});
+			return Content(rss, ContentType.Xml);
+		}
 
-        /// <summary>
-        /// RSS文章订阅
-        /// </summary>
-        /// <returns></returns>
-        [Route("/{id}/rss"), ResponseCache(Duration = 3600)]
-        public async Task<IActionResult> PostRss(int id)
-        {
-            if (CommonHelper.SystemSettings.GetOrAdd("EnableRss", "true") != "true")
-            {
-                throw new NotFoundException("不允许订阅");
-            }
+		/// <summary>
+		/// RSS文章订阅
+		/// </summary>
+		/// <returns></returns>
+		[Route("/{id}/rss"), ResponseCache(Duration = 3600)]
+		public async Task<IActionResult> PostRss(int id)
+		{
+			if (CommonHelper.SystemSettings.GetOrAdd("EnableRss", "true") != "true")
+			{
+				throw new NotFoundException("不允许订阅");
+			}
 
-            string scheme = Request.Scheme;
-            var host = Request.Host;
-            var post = await PostService.GetAsync(p => p.Rss && p.Status == Status.Published && p.Id == id) ?? throw new NotFoundException("文章未找到");
-            CheckPermission(post);
-            var summary = await post.Content.GetSummary(300, 50);
-            var item = new Item()
-            {
-                Author = new Author
-                {
-                    Name = post.Modifier
-                },
-                Body = summary,
-                Categories = new List<string>
-                {
-                    post.Category.Path()
-                },
-                Link = new Uri(scheme + "://" + host + "/" + post.Id),
-                PublishDate = post.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone)),
-                Title = post.Title,
-                Permalink = scheme + "://" + host + "/" + post.Id,
-                Guid = post.Id.ToString(),
-                FullHtmlContent = summary
-            };
-            var feed = new Feed()
-            {
-                Title = Request.Host + $":文章【{post.Title}】更新订阅",
-                Description = summary,
-                Link = new Uri(scheme + "://" + host + "/rss/" + id),
-                Copyright = CommonHelper.SystemSettings["Title"],
-                Language = "zh-cn",
-                Items = new List<Item>() { item }
-            };
-            var rss = feed.Serialize(new SerializeOption()
-            {
-                Encoding = Encoding.UTF8
-            });
-            return Content(rss, ContentType.Xml);
-        }
+			string scheme = Request.Scheme;
+			var host = Request.Host;
+			var post = await PostService.GetAsync(p => p.Rss && p.Status == Status.Published && p.Id == id) ?? throw new NotFoundException("文章未找到");
+			CheckPermission(post);
+			var summary = await post.Content.GetSummary(300, 50);
+			var item = new Item()
+			{
+				Author = new Author
+				{
+					Name = post.Modifier
+				},
+				Body = summary,
+				Categories = new List<string>
+				{
+					post.Category.Path()
+				},
+				Link = new Uri(scheme + "://" + host + "/" + post.Id),
+				PublishDate = post.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone)),
+				Title = post.Title,
+				Permalink = scheme + "://" + host + "/" + post.Id,
+				Guid = post.Id.ToString(),
+				FullHtmlContent = summary
+			};
+			var feed = new Feed()
+			{
+				Title = Request.Host + $":文章【{post.Title}】更新订阅",
+				Description = summary,
+				Link = new Uri(scheme + "://" + host + "/rss/" + id),
+				Copyright = CommonHelper.SystemSettings["Title"],
+				Language = "zh-cn",
+				Items = new List<Item>() { item }
+			};
+			var rss = feed.Serialize(new SerializeOption()
+			{
+				Encoding = Encoding.UTF8
+			});
+			return Content(rss, ContentType.Xml);
+		}
 
-        protected Expression<Func<Post, bool>> PostBaseWhere()
-        {
-            var ipLocation = Request.Location();
-            var location = ipLocation + ipLocation.Coodinate + "|" + Request.Headers[HeaderNames.Referer] + "|" + Request.Headers[HeaderNames.UserAgent];
-            return p => p.Status == Status.Published && p.LimitMode != RegionLimitMode.OnlyForSearchEngine
-                && (p.LimitMode == null || p.LimitMode == RegionLimitMode.All ? true :
-                    p.LimitMode == RegionLimitMode.AllowRegion ? Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) :
-                    p.LimitMode == RegionLimitMode.ForbidRegion ? !Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) :
-                    p.LimitMode == RegionLimitMode.AllowRegionExceptForbidRegion ? Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) && !Regex.IsMatch(location, p.ExceptRegions, RegexOptions.IgnoreCase) :
-                    !Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) || Regex.IsMatch(location, p.ExceptRegions, RegexOptions.IgnoreCase));
-        }
+		protected Expression<Func<Post, bool>> PostBaseWhere()
+		{
+			var ipLocation = Request.Location();
+			var location = ipLocation + ipLocation.Coodinate + "|" + Request.Headers[HeaderNames.Referer] + "|" + Request.Headers[HeaderNames.UserAgent];
+			return p => p.Status == Status.Published && p.LimitMode != RegionLimitMode.OnlyForSearchEngine
+				&& (p.LimitMode == null || p.LimitMode == RegionLimitMode.All ? true :
+					p.LimitMode == RegionLimitMode.AllowRegion ? Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) :
+					p.LimitMode == RegionLimitMode.ForbidRegion ? !Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) :
+					p.LimitMode == RegionLimitMode.AllowRegionExceptForbidRegion ? Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) && !Regex.IsMatch(location, p.ExceptRegions, RegexOptions.IgnoreCase) :
+					!Regex.IsMatch(location, p.Regions, RegexOptions.IgnoreCase) || Regex.IsMatch(location, p.ExceptRegions, RegexOptions.IgnoreCase));
+		}
 
-        private void CheckPermission(Post post)
-        {
-            var ipLocation = Request.Location();
-            var location = ipLocation + ipLocation.Coodinate + "|" + Request.Headers[HeaderNames.Referer] + "|" + Request.Headers[HeaderNames.UserAgent];
-            switch (post.LimitMode)
-            {
-                case RegionLimitMode.OnlyForSearchEngine:
-                    Disallow(post);
-                    break;
+		private void CheckPermission(Post post)
+		{
+			var ipLocation = Request.Location();
+			var location = ipLocation + ipLocation.Coodinate + "|" + Request.Headers[HeaderNames.Referer] + "|" + Request.Headers[HeaderNames.UserAgent];
+			switch (post.LimitMode)
+			{
+				case RegionLimitMode.OnlyForSearchEngine:
+					Disallow(post);
+					break;
 
-                case RegionLimitMode.AllowRegion:
-                    if (!Regex.IsMatch(location, post.Regions, RegexOptions.IgnoreCase) && !Request.IsRobot())
-                    {
-                        Disallow(post);
-                    }
+				case RegionLimitMode.AllowRegion:
+					if (!Regex.IsMatch(location, post.Regions, RegexOptions.IgnoreCase) && !Request.IsRobot())
+					{
+						Disallow(post);
+					}
 
-                    break;
+					break;
 
-                case RegionLimitMode.ForbidRegion:
-                    if (Regex.IsMatch(location, post.Regions, RegexOptions.IgnoreCase) && !Request.IsRobot())
-                    {
-                        Disallow(post);
-                    }
+				case RegionLimitMode.ForbidRegion:
+					if (Regex.IsMatch(location, post.Regions, RegexOptions.IgnoreCase) && !Request.IsRobot())
+					{
+						Disallow(post);
+					}
 
-                    break;
+					break;
 
-                case RegionLimitMode.AllowRegionExceptForbidRegion:
-                    if (Regex.IsMatch(location, post.ExceptRegions, RegexOptions.IgnoreCase))
-                    {
-                        Disallow(post);
-                    }
+				case RegionLimitMode.AllowRegionExceptForbidRegion:
+					if (Regex.IsMatch(location, post.ExceptRegions, RegexOptions.IgnoreCase))
+					{
+						Disallow(post);
+					}
 
-                    goto case RegionLimitMode.AllowRegion;
-                case RegionLimitMode.ForbidRegionExceptAllowRegion:
-                    if (Regex.IsMatch(location, post.ExceptRegions, RegexOptions.IgnoreCase))
-                    {
-                        break;
-                    }
+					goto case RegionLimitMode.AllowRegion;
+				case RegionLimitMode.ForbidRegionExceptAllowRegion:
+					if (Regex.IsMatch(location, post.ExceptRegions, RegexOptions.IgnoreCase))
+					{
+						break;
+					}
 
-                    goto case RegionLimitMode.ForbidRegion;
-            }
-        }
+					goto case RegionLimitMode.ForbidRegion;
+			}
+		}
 
-        private void Disallow(Post post)
-        {
-            RedisClient.IncrBy("interceptCount", 1);
-            RedisClient.LPush("intercept", new IpIntercepter()
-            {
-                IP = HttpContext.Connection.RemoteIpAddress.ToString(),
-                RequestUrl = $"//{Request.Host}/{post.Id}",
-                Referer = Request.Headers[HeaderNames.Referer],
-                Time = DateTime.Now,
-                UserAgent = Request.Headers[HeaderNames.UserAgent],
-                Remark = "无权限查看该文章",
-                Address = Request.Location(),
-                HttpVersion = Request.Protocol,
-                Headers = new
-                {
-                    Request.Protocol,
-                    Request.Headers
-                }.ToJsonString()
-            });
-            throw new NotFoundException("文章未找到");
-        }
-    }
+		private void Disallow(Post post)
+		{
+			RedisClient.IncrBy("interceptCount", 1);
+			RedisClient.LPush("intercept", new IpIntercepter()
+			{
+				IP = HttpContext.Connection.RemoteIpAddress.ToString(),
+				RequestUrl = $"//{Request.Host}/{post.Id}",
+				Referer = Request.Headers[HeaderNames.Referer],
+				Time = DateTime.Now,
+				UserAgent = Request.Headers[HeaderNames.UserAgent],
+				Remark = "无权限查看该文章",
+				Address = Request.Location(),
+				HttpVersion = Request.Protocol,
+				Headers = new
+				{
+					Request.Protocol,
+					Request.Headers
+				}.ToJsonString()
+			});
+			throw new NotFoundException("文章未找到");
+		}
+	}
 }

+ 558 - 559
src/Masuit.MyBlogs.Core/Infrastructure/Repository/BaseRepository.cs

@@ -1,6 +1,5 @@
 using AutoMapper;
 using AutoMapper.QueryableExtensions;
-using Collections.Pooled;
 using Masuit.LuceneEFCore.SearchEngine;
 using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
 using Masuit.Tools.Core.AspNetCore;
@@ -12,562 +11,562 @@ using Z.EntityFramework.Plus;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Repository
 {
-    /// <summary>
-    /// DAL基类
-    /// </summary>
-    /// <typeparam name="T">实体类型</typeparam>
-    public abstract class BaseRepository<T> : Disposable, IBaseRepository<T> where T : LuceneIndexableBaseEntity
-    {
-        public virtual DataContext DataContext { get; set; }
-
-        public MapperConfiguration MapperConfig { get; set; }
-
-        /// <summary>
-        /// 获取所有实体
-        /// </summary>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<T> GetAll()
-        {
-            return DataContext.Set<T>();
-        }
-
-        /// <summary>
-        /// 获取所有实体(不跟踪)
-        /// </summary>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<T> GetAllNoTracking()
-        {
-            return DataContext.Set<T>().AsNoTracking();
-        }
-
-        /// <summary>
-        /// 获取所有实体(不跟踪)
-        /// </summary>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<TDto> GetAll<TDto>() where TDto : class
-        {
-            return DataContext.Set<T>().AsNoTracking().ProjectTo<TDto>(MapperConfig);
-        }
-
-        /// <summary>
-        /// 获取所有实体
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IOrderedQueryable<T> GetAll<TS>(Expression<Func<T, TS>> orderby, bool isAsc = true)
-        {
-            return isAsc ? DataContext.Set<T>().OrderBy(orderby) : DataContext.Set<T>().OrderByDescending(orderby);
-        }
-
-        /// <summary>
-        /// 获取所有实体(不跟踪)
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IOrderedQueryable<T> GetAllNoTracking<TS>(Expression<Func<T, TS>> orderby, bool isAsc = true)
-        {
-            return isAsc ? DataContext.Set<T>().AsNoTracking().OrderBy(orderby) : DataContext.Set<T>().AsNoTracking().OrderByDescending(orderby);
-        }
-
-        /// <summary>
-        /// 从二级缓存获取所有实体
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns></returns>
-        public virtual PooledList<T> GetAllFromCache<TS>(Expression<Func<T, TS>> orderby, bool isAsc = true)
-        {
-            return GetAllNoTracking(orderby, isAsc).FromCache().ToPooledList();
-        }
-
-        /// <summary>
-        /// 获取所有实体
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<TDto> GetAll<TS, TDto>(Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class
-        {
-            return GetAllNoTracking(orderby, isAsc).ProjectTo<TDto>(MapperConfig);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<T> GetQuery(Expression<Func<T, bool>> where)
-        {
-            return DataContext.Set<T>().Where(where);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IOrderedQueryable<T> GetQuery<TS>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true)
-        {
-            return isAsc ? DataContext.Set<T>().Where(where).OrderBy(orderby) : DataContext.Set<T>().Where(where).OrderByDescending(orderby);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合,优先从二级缓存读取
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns></returns>
-        public virtual PooledList<T> GetQueryFromCache(Expression<Func<T, bool>> where)
-        {
-            return DataContext.Set<T>().Where(where).AsNoTracking().FromCache().ToPooledList();
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合,优先从二级缓存读取
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序方式</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns></returns>
-        public virtual PooledList<T> GetQueryFromCache<TS>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true)
-        {
-            return GetQueryNoTracking(where, orderby, isAsc).FromCache().ToPooledList();
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合(不跟踪实体)
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<T> GetQueryNoTracking(Expression<Func<T, bool>> where)
-        {
-            return DataContext.Set<T>().Where(where).AsNoTracking();
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合(不跟踪实体)
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序方式</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IOrderedQueryable<T> GetQueryNoTracking<TS>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true)
-        {
-            return isAsc ? DataContext.Set<T>().Where(where).AsNoTracking().OrderBy(orderby) : DataContext.Set<T>().Where(where).AsNoTracking().OrderByDescending(orderby);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个被AutoMapper映射后的集合(不跟踪实体)
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<TDto> GetQuery<TDto>(Expression<Func<T, bool>> where) where TDto : class
-        {
-            return DataContext.Set<T>().Where(where).AsNoTracking().ProjectTo<TDto>(MapperConfig);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个被AutoMapper映射后的集合(不跟踪实体)
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <typeparam name="TDto">输出类型</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序方式</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<TDto> GetQuery<TS, TDto>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class
-        {
-            return GetQueryNoTracking(where, orderby, isAsc).ProjectTo<TDto>(MapperConfig);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个被AutoMapper映射后的集合,优先从二级缓存读取(不跟踪实体)
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <typeparam name="TDto">输出类型</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序方式</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns></returns>
-        public virtual PooledList<TDto> GetQueryFromCache<TS, TDto>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class
-        {
-            return GetQuery(where, orderby, isAsc).ProjectTo<TDto>(MapperConfig).FromCache().ToPooledList();
-        }
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        public virtual T Get(Expression<Func<T, bool>> where)
-        {
-            return EF.CompileQuery((DataContext ctx) => ctx.Set<T>().FirstOrDefault(where))(DataContext);
-        }
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        public Task<T> GetFromCacheAsync(Expression<Func<T, bool>> @where)
-        {
-            return DataContext.Set<T>().Where(where).AsNoTracking().DeferredFirstOrDefault().ExecuteAsync();
-        }
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>实体</returns>
-        public virtual T Get<TS>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true)
-        {
-            return isAsc ? EF.CompileQuery((DataContext ctx) => ctx.Set<T>().OrderBy(orderby).FirstOrDefault(where))(DataContext) : EF.CompileQuery((DataContext ctx) => ctx.Set<T>().OrderByDescending(orderby).FirstOrDefault(where))(DataContext);
-        }
-
-        /// <summary>
-        /// 获取第一条被AutoMapper映射后的数据(不跟踪)
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>映射实体</returns>
-        public Task<TDto> GetAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
-        {
-            return isAsc ? DataContext.Set<T>().Where(where).OrderBy(orderby).AsNoTracking().ProjectTo<TDto>(MapperConfig).FirstOrDefaultAsync() : DataContext.Set<T>().Where(where).OrderByDescending(orderby).AsNoTracking().ProjectTo<TDto>(MapperConfig).FirstOrDefaultAsync();
-        }
-
-        /// <summary>
-        /// 获取第一条被AutoMapper映射后的数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>映射实体</returns>
-        public Task<TDto> GetFromCacheAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
-        {
-            return isAsc ? DataContext.Set<T>().Where(where).OrderBy(orderby).ProjectTo<TDto>(MapperConfig).DeferredFirstOrDefault().ExecuteAsync() : DataContext.Set<T>().Where(where).OrderByDescending(orderby).ProjectTo<TDto>(MapperConfig).DeferredFirstOrDefault().ExecuteAsync();
-        }
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        public virtual Task<T> GetAsync(Expression<Func<T, bool>> where)
-        {
-            return EF.CompileAsyncQuery((DataContext ctx) => ctx.Set<T>().FirstOrDefault(where))(DataContext);
-        }
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>实体</returns>
-        public virtual Task<T> GetAsync<TS>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true)
-        {
-            return isAsc ? EF.CompileAsyncQuery((DataContext ctx) => ctx.Set<T>().OrderBy(orderby).FirstOrDefault(where))(DataContext) : EF.CompileAsyncQuery((DataContext ctx) => ctx.Set<T>().OrderByDescending(orderby).FirstOrDefault(where))(DataContext);
-        }
-
-        /// <summary>
-        /// 获取第一条数据(不跟踪实体)
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        public virtual T GetNoTracking(Expression<Func<T, bool>> where)
-        {
-            return EF.CompileQuery((DataContext ctx) => ctx.Set<T>().AsNoTracking().FirstOrDefault(where))(DataContext);
-        }
-
-        /// <summary>
-        /// 获取第一条被AutoMapper映射后的数据(不跟踪实体)
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        public virtual TDto Get<TDto>(Expression<Func<T, bool>> where) where TDto : class
-        {
-            return DataContext.Set<T>().Where(where).AsNoTracking().ProjectTo<TDto>(MapperConfig).FirstOrDefault();
-        }
-
-        /// <summary>
-        /// 获取第一条被AutoMapper映射后的数据(不跟踪实体)
-        /// </summary>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>实体</returns>
-        public virtual TDto Get<TS, TDto>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class
-        {
-            return isAsc ? DataContext.Set<T>().Where(where).OrderBy(orderby).AsNoTracking().ProjectTo<TDto>(MapperConfig).FirstOrDefault() : DataContext.Set<T>().Where(where).OrderByDescending(orderby).AsNoTracking().ProjectTo<TDto>(MapperConfig).FirstOrDefault();
-        }
-
-        /// <summary>
-        /// 根据ID找实体
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>实体</returns>
-        public virtual T GetById(int id)
-        {
-            return EF.CompileQuery((DataContext ctx, int xid) => ctx.Set<T>().FirstOrDefault(t => t.Id == xid))(DataContext, id);
-        }
-
-        /// <summary>
-        /// 根据ID找实体(异步)
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>实体</returns>
-        public virtual Task<T> GetByIdAsync(int id)
-        {
-            return EF.CompileAsyncQuery((DataContext ctx, int xid) => ctx.Set<T>().FirstOrDefault(t => t.Id == xid))(DataContext, id);
-        }
-
-        /// <summary>
-        /// 标准分页查询方法
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        public virtual PagedList<T> GetPages<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc)
-        {
-            return isAsc ? DataContext.Set<T>().Where(where).OrderBy(orderby).ToPagedList(pageIndex, pageSize) : DataContext.Set<T>().Where(where).OrderByDescending(orderby).ToPagedList(pageIndex, pageSize);
-        }
-
-        /// <summary>
-        /// 标准分页查询方法
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        public virtual Task<PagedList<T>> GetPagesAsync<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> orderby, bool isAsc)
-        {
-            return isAsc ? DataContext.Set<T>().Where(where).OrderBy(orderby).ToPagedListAsync(pageIndex, pageSize) : DataContext.Set<T>().Where(where).OrderByDescending(orderby).ToPagedListAsync(pageIndex, pageSize);
-        }
-
-        /// <summary>
-        /// 标准分页查询方法(不跟踪实体)
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        public virtual PagedList<T> GetPagesNoTracking<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true)
-        {
-            return isAsc ? DataContext.Set<T>().Where(where).AsNoTracking().OrderBy(orderby).ToPagedList(pageIndex, pageSize) : DataContext.Set<T>().Where(where).AsNoTracking().OrderByDescending(orderby).ToPagedList(pageIndex, pageSize);
-        }
-
-        /// <summary>
-        /// 标准分页查询方法,取出被AutoMapper映射后的数据集合(不跟踪实体)
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <typeparam name="TDto"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        public virtual PagedList<TDto> GetPages<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class
-        {
-            return isAsc ? DataContext.Set<T>().Where(where).AsNoTracking().OrderBy(orderby).ToPagedList<T, TDto>(pageIndex, pageSize, MapperConfig) : DataContext.Set<T>().Where(where).AsNoTracking().OrderByDescending(orderby).ToPagedList<T, TDto>(pageIndex, pageSize, MapperConfig);
-        }
-
-        /// <summary>
-        /// 标准分页查询方法,取出被AutoMapper映射后的数据集合
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <typeparam name="TDto"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        public Task<PagedList<TDto>> GetPagesAsync<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc) where TDto : class
-        {
-            return isAsc ? DataContext.Set<T>().Where(where).AsNoTracking().OrderBy(orderby).ToPagedListAsync<T, TDto>(pageIndex, pageSize, MapperConfig) : DataContext.Set<T>().Where(where).AsNoTracking().OrderByDescending(orderby).ToPagedListAsync<T, TDto>(pageIndex, pageSize, MapperConfig);
-        }
-
-        /// <summary>
-        /// 标准分页查询方法,取出被AutoMapper映射后的数据集合,优先从缓存读取(不跟踪实体)
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <typeparam name="TDto"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        public virtual PagedList<TDto> GetPagesFromCache<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class
-        {
-            var temp = DataContext.Set<T>().Where(where).AsNoTracking();
-            return isAsc ? temp.OrderBy(orderby).ToCachedPagedList<T, TDto>(pageIndex, pageSize, MapperConfig) : temp.OrderByDescending(orderby).ToCachedPagedList<T, TDto>(pageIndex, pageSize, MapperConfig);
-        }
-
-        /// <summary>
-        /// 根据ID删除实体
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>删除成功</returns>
-        public virtual bool DeleteById(int id)
-        {
-            return DataContext.Set<T>().Where(t => t.Id == id).ExecuteDelete() > 0;
-        }
-
-        /// <summary>
-        /// 根据ID删除实体
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>删除成功</returns>
-        public virtual Task<int> DeleteByIdAsync(int id)
-        {
-            return DataContext.Set<T>().Where(t => t.Id == id).ExecuteDeleteAsync();
-        }
-
-        /// <summary>
-        /// 删除实体
-        /// </summary>
-        /// <param name="t">需要删除的实体</param>
-        /// <returns>删除成功</returns>
-        public virtual bool DeleteEntity(T t)
-        {
-            DataContext.Entry(t).State = EntityState.Unchanged;
-            DataContext.Entry(t).State = EntityState.Deleted;
-            DataContext.Remove(t);
-            return true;
-        }
-
-        /// <summary>
-        /// 根据条件删除实体
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>删除成功</returns>
-        public virtual int DeleteEntity(Expression<Func<T, bool>> where)
-        {
-            return DataContext.Set<T>().Where(where).ExecuteDelete();
-        }
-
-        /// <summary>
-        /// 添加实体
-        /// </summary>
-        /// <param name="t">需要添加的实体</param>
-        /// <returns>添加成功</returns>
-        public T AddEntity(T t)
-        {
-            DataContext.Add(t);
-            return t;
-        }
-
-        /// <summary>
-        /// 添加或更新实体
-        /// </summary>
-        /// <param name="key">更新键规则</param>
-        /// <param name="t">需要保存的实体</param>
-        /// <returns>保存成功</returns>
-        public T AddOrUpdate<TKey>(Expression<Func<T, TKey>> key, T t)
-        {
-            DataContext.Set<T>().AddOrUpdate(key, t);
-            return t;
-        }
-
-        /// <summary>
-        /// 添加或更新实体
-        /// </summary>
-        /// <param name="key">更新键规则</param>
-        /// <param name="entities">需要保存的实体</param>
-        /// <returns>保存成功</returns>
-        public void AddOrUpdate<TKey>(Expression<Func<T, TKey>> key, IEnumerable<T> entities)
-        {
-            DataContext.Set<T>().AddOrUpdate(key, entities);
-        }
-
-        /// <summary>
-        /// 统一保存数据
-        /// </summary>
-        /// <returns>受影响的行数</returns>
-        public virtual int SaveChanges()
-        {
-            return DataContext.SaveChanges();
-        }
-
-        /// <summary>
-        /// 统一保存数据(异步)
-        /// </summary>
-        /// <returns>受影响的行数</returns>
-        public virtual Task<int> SaveChangesAsync()
-        {
-            return DataContext.SaveChangesAsync();
-        }
-
-        /// <summary>
-        /// 判断实体是否在数据库中存在
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>是否存在</returns>
-        public virtual bool Any(Expression<Func<T, bool>> where)
-        {
-            return EF.CompileQuery((DataContext ctx) => ctx.Set<T>().Any(where))(DataContext);
-        }
-
-        /// <summary>
-        /// 符合条件的个数
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>是否存在</returns>
-        public virtual int Count(Expression<Func<T, bool>> where)
-        {
-            return EF.CompileQuery((DataContext ctx) => ctx.Set<T>().Count(where))(DataContext);
-        }
-
-        /// <summary>
-        /// 删除多个实体
-        /// </summary>
-        /// <param name="list">实体集合</param>
-        /// <returns>删除成功</returns>
-        public virtual bool DeleteEntities(IEnumerable<T> list)
-        {
-            DataContext.RemoveRange(list);
-            return true;
-        }
-
-        public override void Dispose(bool disposing)
-        {
-        }
-
-        public T this[int id] => GetById(id);
-    }
+	/// <summary>
+	/// DAL基类
+	/// </summary>
+	/// <typeparam name="T">实体类型</typeparam>
+	public abstract class BaseRepository<T> : Disposable, IBaseRepository<T> where T : LuceneIndexableBaseEntity
+	{
+		public virtual DataContext DataContext { get; set; }
+
+		public MapperConfiguration MapperConfig { get; set; }
+
+		/// <summary>
+		/// 获取所有实体
+		/// </summary>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<T> GetAll()
+		{
+			return DataContext.Set<T>();
+		}
+
+		/// <summary>
+		/// 获取所有实体(不跟踪)
+		/// </summary>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<T> GetAllNoTracking()
+		{
+			return DataContext.Set<T>().AsNoTracking();
+		}
+
+		/// <summary>
+		/// 获取所有实体(不跟踪)
+		/// </summary>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<TDto> GetAll<TDto>() where TDto : class
+		{
+			return DataContext.Set<T>().AsNoTracking().ProjectTo<TDto>(MapperConfig);
+		}
+
+		/// <summary>
+		/// 获取所有实体
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IOrderedQueryable<T> GetAll<TS>(Expression<Func<T, TS>> orderby, bool isAsc = true)
+		{
+			return isAsc ? DataContext.Set<T>().OrderBy(orderby) : DataContext.Set<T>().OrderByDescending(orderby);
+		}
+
+		/// <summary>
+		/// 获取所有实体(不跟踪)
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IOrderedQueryable<T> GetAllNoTracking<TS>(Expression<Func<T, TS>> orderby, bool isAsc = true)
+		{
+			return isAsc ? DataContext.Set<T>().AsNoTracking().OrderBy(orderby) : DataContext.Set<T>().AsNoTracking().OrderByDescending(orderby);
+		}
+
+		/// <summary>
+		/// 从二级缓存获取所有实体
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns></returns>
+		public virtual List<T> GetAllFromCache<TS>(Expression<Func<T, TS>> orderby, bool isAsc = true)
+		{
+			return GetAllNoTracking(orderby, isAsc).FromCache().ToList();
+		}
+
+		/// <summary>
+		/// 获取所有实体
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<TDto> GetAll<TS, TDto>(Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class
+		{
+			return GetAllNoTracking(orderby, isAsc).ProjectTo<TDto>(MapperConfig);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<T> GetQuery(Expression<Func<T, bool>> where)
+		{
+			return DataContext.Set<T>().Where(where);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IOrderedQueryable<T> GetQuery<TS>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true)
+		{
+			return isAsc ? DataContext.Set<T>().Where(where).OrderBy(orderby) : DataContext.Set<T>().Where(where).OrderByDescending(orderby);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合,优先从二级缓存读取
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns></returns>
+		public virtual List<T> GetQueryFromCache(Expression<Func<T, bool>> where)
+		{
+			return DataContext.Set<T>().Where(where).AsNoTracking().FromCache().ToList();
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合,优先从二级缓存读取
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序方式</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns></returns>
+		public virtual List<T> GetQueryFromCache<TS>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true)
+		{
+			return GetQueryNoTracking(where, orderby, isAsc).FromCache().ToList();
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合(不跟踪实体)
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<T> GetQueryNoTracking(Expression<Func<T, bool>> where)
+		{
+			return DataContext.Set<T>().Where(where).AsNoTracking();
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合(不跟踪实体)
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序方式</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IOrderedQueryable<T> GetQueryNoTracking<TS>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true)
+		{
+			return isAsc ? DataContext.Set<T>().Where(where).AsNoTracking().OrderBy(orderby) : DataContext.Set<T>().Where(where).AsNoTracking().OrderByDescending(orderby);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个被AutoMapper映射后的集合(不跟踪实体)
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<TDto> GetQuery<TDto>(Expression<Func<T, bool>> where) where TDto : class
+		{
+			return DataContext.Set<T>().Where(where).AsNoTracking().ProjectTo<TDto>(MapperConfig);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个被AutoMapper映射后的集合(不跟踪实体)
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <typeparam name="TDto">输出类型</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序方式</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<TDto> GetQuery<TS, TDto>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class
+		{
+			return GetQueryNoTracking(where, orderby, isAsc).ProjectTo<TDto>(MapperConfig);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个被AutoMapper映射后的集合,优先从二级缓存读取(不跟踪实体)
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <typeparam name="TDto">输出类型</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序方式</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns></returns>
+		public virtual List<TDto> GetQueryFromCache<TS, TDto>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class
+		{
+			return GetQuery(where, orderby, isAsc).ProjectTo<TDto>(MapperConfig).FromCache().ToList();
+		}
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		public virtual T Get(Expression<Func<T, bool>> where)
+		{
+			return EF.CompileQuery((DataContext ctx) => ctx.Set<T>().FirstOrDefault(where))(DataContext);
+		}
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		public Task<T> GetFromCacheAsync(Expression<Func<T, bool>> @where)
+		{
+			return DataContext.Set<T>().Where(where).AsNoTracking().DeferredFirstOrDefault().ExecuteAsync();
+		}
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>实体</returns>
+		public virtual T Get<TS>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true)
+		{
+			return isAsc ? EF.CompileQuery((DataContext ctx) => ctx.Set<T>().OrderBy(orderby).FirstOrDefault(where))(DataContext) : EF.CompileQuery((DataContext ctx) => ctx.Set<T>().OrderByDescending(orderby).FirstOrDefault(where))(DataContext);
+		}
+
+		/// <summary>
+		/// 获取第一条被AutoMapper映射后的数据(不跟踪)
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>映射实体</returns>
+		public Task<TDto> GetAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
+		{
+			return isAsc ? DataContext.Set<T>().Where(where).OrderBy(orderby).AsNoTracking().ProjectTo<TDto>(MapperConfig).FirstOrDefaultAsync() : DataContext.Set<T>().Where(where).OrderByDescending(orderby).AsNoTracking().ProjectTo<TDto>(MapperConfig).FirstOrDefaultAsync();
+		}
+
+		/// <summary>
+		/// 获取第一条被AutoMapper映射后的数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>映射实体</returns>
+		public Task<TDto> GetFromCacheAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
+		{
+			return isAsc ? DataContext.Set<T>().Where(where).OrderBy(orderby).ProjectTo<TDto>(MapperConfig).DeferredFirstOrDefault().ExecuteAsync() : DataContext.Set<T>().Where(where).OrderByDescending(orderby).ProjectTo<TDto>(MapperConfig).DeferredFirstOrDefault().ExecuteAsync();
+		}
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		public virtual Task<T> GetAsync(Expression<Func<T, bool>> where)
+		{
+			return EF.CompileAsyncQuery((DataContext ctx) => ctx.Set<T>().FirstOrDefault(where))(DataContext);
+		}
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>实体</returns>
+		public virtual Task<T> GetAsync<TS>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true)
+		{
+			return isAsc ? EF.CompileAsyncQuery((DataContext ctx) => ctx.Set<T>().OrderBy(orderby).FirstOrDefault(where))(DataContext) : EF.CompileAsyncQuery((DataContext ctx) => ctx.Set<T>().OrderByDescending(orderby).FirstOrDefault(where))(DataContext);
+		}
+
+		/// <summary>
+		/// 获取第一条数据(不跟踪实体)
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		public virtual T GetNoTracking(Expression<Func<T, bool>> where)
+		{
+			return EF.CompileQuery((DataContext ctx) => ctx.Set<T>().AsNoTracking().FirstOrDefault(where))(DataContext);
+		}
+
+		/// <summary>
+		/// 获取第一条被AutoMapper映射后的数据(不跟踪实体)
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		public virtual TDto Get<TDto>(Expression<Func<T, bool>> where) where TDto : class
+		{
+			return DataContext.Set<T>().Where(where).AsNoTracking().ProjectTo<TDto>(MapperConfig).FirstOrDefault();
+		}
+
+		/// <summary>
+		/// 获取第一条被AutoMapper映射后的数据(不跟踪实体)
+		/// </summary>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>实体</returns>
+		public virtual TDto Get<TS, TDto>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class
+		{
+			return isAsc ? DataContext.Set<T>().Where(where).OrderBy(orderby).AsNoTracking().ProjectTo<TDto>(MapperConfig).FirstOrDefault() : DataContext.Set<T>().Where(where).OrderByDescending(orderby).AsNoTracking().ProjectTo<TDto>(MapperConfig).FirstOrDefault();
+		}
+
+		/// <summary>
+		/// 根据ID找实体
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>实体</returns>
+		public virtual T GetById(int id)
+		{
+			return EF.CompileQuery((DataContext ctx, int xid) => ctx.Set<T>().FirstOrDefault(t => t.Id == xid))(DataContext, id);
+		}
+
+		/// <summary>
+		/// 根据ID找实体(异步)
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>实体</returns>
+		public virtual Task<T> GetByIdAsync(int id)
+		{
+			return EF.CompileAsyncQuery((DataContext ctx, int xid) => ctx.Set<T>().FirstOrDefault(t => t.Id == xid))(DataContext, id);
+		}
+
+		/// <summary>
+		/// 标准分页查询方法
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		public virtual PagedList<T> GetPages<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc)
+		{
+			return isAsc ? DataContext.Set<T>().Where(where).OrderBy(orderby).ToPagedList(pageIndex, pageSize) : DataContext.Set<T>().Where(where).OrderByDescending(orderby).ToPagedList(pageIndex, pageSize);
+		}
+
+		/// <summary>
+		/// 标准分页查询方法
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		public virtual Task<PagedList<T>> GetPagesAsync<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> orderby, bool isAsc)
+		{
+			return isAsc ? DataContext.Set<T>().Where(where).OrderBy(orderby).ToPagedListAsync(pageIndex, pageSize) : DataContext.Set<T>().Where(where).OrderByDescending(orderby).ToPagedListAsync(pageIndex, pageSize);
+		}
+
+		/// <summary>
+		/// 标准分页查询方法(不跟踪实体)
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		public virtual PagedList<T> GetPagesNoTracking<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true)
+		{
+			return isAsc ? DataContext.Set<T>().Where(where).AsNoTracking().OrderBy(orderby).ToPagedList(pageIndex, pageSize) : DataContext.Set<T>().Where(where).AsNoTracking().OrderByDescending(orderby).ToPagedList(pageIndex, pageSize);
+		}
+
+		/// <summary>
+		/// 标准分页查询方法,取出被AutoMapper映射后的数据集合(不跟踪实体)
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <typeparam name="TDto"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		public virtual PagedList<TDto> GetPages<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class
+		{
+			return isAsc ? DataContext.Set<T>().Where(where).AsNoTracking().OrderBy(orderby).ToPagedList<T, TDto>(pageIndex, pageSize, MapperConfig) : DataContext.Set<T>().Where(where).AsNoTracking().OrderByDescending(orderby).ToPagedList<T, TDto>(pageIndex, pageSize, MapperConfig);
+		}
+
+		/// <summary>
+		/// 标准分页查询方法,取出被AutoMapper映射后的数据集合
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <typeparam name="TDto"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		public Task<PagedList<TDto>> GetPagesAsync<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc) where TDto : class
+		{
+			return isAsc ? DataContext.Set<T>().Where(where).AsNoTracking().OrderBy(orderby).ToPagedListAsync<T, TDto>(pageIndex, pageSize, MapperConfig) : DataContext.Set<T>().Where(where).AsNoTracking().OrderByDescending(orderby).ToPagedListAsync<T, TDto>(pageIndex, pageSize, MapperConfig);
+		}
+
+		/// <summary>
+		/// 标准分页查询方法,取出被AutoMapper映射后的数据集合,优先从缓存读取(不跟踪实体)
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <typeparam name="TDto"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		public virtual PagedList<TDto> GetPagesFromCache<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class
+		{
+			var temp = DataContext.Set<T>().Where(where).AsNoTracking();
+			return isAsc ? temp.OrderBy(orderby).ToCachedPagedList<T, TDto>(pageIndex, pageSize, MapperConfig) : temp.OrderByDescending(orderby).ToCachedPagedList<T, TDto>(pageIndex, pageSize, MapperConfig);
+		}
+
+		/// <summary>
+		/// 根据ID删除实体
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>删除成功</returns>
+		public virtual bool DeleteById(int id)
+		{
+			return DataContext.Set<T>().Where(t => t.Id == id).ExecuteDelete() > 0;
+		}
+
+		/// <summary>
+		/// 根据ID删除实体
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>删除成功</returns>
+		public virtual Task<int> DeleteByIdAsync(int id)
+		{
+			return DataContext.Set<T>().Where(t => t.Id == id).ExecuteDeleteAsync();
+		}
+
+		/// <summary>
+		/// 删除实体
+		/// </summary>
+		/// <param name="t">需要删除的实体</param>
+		/// <returns>删除成功</returns>
+		public virtual bool DeleteEntity(T t)
+		{
+			DataContext.Entry(t).State = EntityState.Unchanged;
+			DataContext.Entry(t).State = EntityState.Deleted;
+			DataContext.Remove(t);
+			return true;
+		}
+
+		/// <summary>
+		/// 根据条件删除实体
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>删除成功</returns>
+		public virtual int DeleteEntity(Expression<Func<T, bool>> where)
+		{
+			return DataContext.Set<T>().Where(where).ExecuteDelete();
+		}
+
+		/// <summary>
+		/// 添加实体
+		/// </summary>
+		/// <param name="t">需要添加的实体</param>
+		/// <returns>添加成功</returns>
+		public T AddEntity(T t)
+		{
+			DataContext.Add(t);
+			return t;
+		}
+
+		/// <summary>
+		/// 添加或更新实体
+		/// </summary>
+		/// <param name="key">更新键规则</param>
+		/// <param name="t">需要保存的实体</param>
+		/// <returns>保存成功</returns>
+		public T AddOrUpdate<TKey>(Expression<Func<T, TKey>> key, T t)
+		{
+			DataContext.Set<T>().AddOrUpdate(key, t);
+			return t;
+		}
+
+		/// <summary>
+		/// 添加或更新实体
+		/// </summary>
+		/// <param name="key">更新键规则</param>
+		/// <param name="entities">需要保存的实体</param>
+		/// <returns>保存成功</returns>
+		public void AddOrUpdate<TKey>(Expression<Func<T, TKey>> key, IEnumerable<T> entities)
+		{
+			DataContext.Set<T>().AddOrUpdate(key, entities);
+		}
+
+		/// <summary>
+		/// 统一保存数据
+		/// </summary>
+		/// <returns>受影响的行数</returns>
+		public virtual int SaveChanges()
+		{
+			return DataContext.SaveChanges();
+		}
+
+		/// <summary>
+		/// 统一保存数据(异步)
+		/// </summary>
+		/// <returns>受影响的行数</returns>
+		public virtual Task<int> SaveChangesAsync()
+		{
+			return DataContext.SaveChangesAsync();
+		}
+
+		/// <summary>
+		/// 判断实体是否在数据库中存在
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>是否存在</returns>
+		public virtual bool Any(Expression<Func<T, bool>> where)
+		{
+			return EF.CompileQuery((DataContext ctx) => ctx.Set<T>().Any(where))(DataContext);
+		}
+
+		/// <summary>
+		/// 符合条件的个数
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>是否存在</returns>
+		public virtual int Count(Expression<Func<T, bool>> where)
+		{
+			return EF.CompileQuery((DataContext ctx) => ctx.Set<T>().Count(where))(DataContext);
+		}
+
+		/// <summary>
+		/// 删除多个实体
+		/// </summary>
+		/// <param name="list">实体集合</param>
+		/// <returns>删除成功</returns>
+		public virtual bool DeleteEntities(IEnumerable<T> list)
+		{
+			DataContext.RemoveRange(list);
+			return true;
+		}
+
+		public override void Dispose(bool disposing)
+		{
+		}
+
+		public T this[int id] => GetById(id);
+	}
 }

+ 463 - 464
src/Masuit.MyBlogs.Core/Infrastructure/Repository/Interface/IBaseRepository.cs

@@ -1,487 +1,486 @@
-using Collections.Pooled;
-using Masuit.LuceneEFCore.SearchEngine;
+using Masuit.LuceneEFCore.SearchEngine;
 using Masuit.MyBlogs.Core.Models.Entity;
 using Masuit.Tools.Models;
 using System.Linq.Expressions;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Repository.Interface
 {
-    public interface IBaseRepository<T> : IDisposable where T : LuceneIndexableBaseEntity
-    {
-        /// <summary>
-        /// 获取所有实体
-        /// </summary>
-        /// <returns>还未执行的SQL语句</returns>
-        IQueryable<T> GetAll();
-
-        /// <summary>
-        /// 获取所有实体(不跟踪)
-        /// </summary>
-        /// <returns>还未执行的SQL语句</returns>
-        IQueryable<T> GetAllNoTracking();
-
-        /// <summary>
-        /// 获取所有实体
-        /// </summary>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <returns>还未执行的SQL语句</returns>
-        IQueryable<TDto> GetAll<TDto>() where TDto : class;
-
-        /// <summary>
-        /// 获取所有实体
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IOrderedQueryable<T> GetAll<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 获取所有实体(不跟踪)
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IOrderedQueryable<T> GetAllNoTracking<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 从二级缓存获取所有实体
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns></returns>
-        PooledList<T> GetAllFromCache<TS>(Expression<Func<T, TS>> orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 获取所有实体
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IQueryable<TDto> GetAll<TS, TDto>(Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IQueryable<T> GetQuery(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IOrderedQueryable<T> GetQuery<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 基本查询方法,获取一个被AutoMapper映射后的集合
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IQueryable<TDto> GetQuery<TDto>(Expression<Func<T, bool>> @where) where TDto : class;
-
-        /// <summary>
-        /// 基本查询方法,获取一个被AutoMapper映射后的集合
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <typeparam name="TDto">输出类型</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        /// <returns></returns>
-        IQueryable<TDto> GetQuery<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合,优先从二级缓存读取
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns></returns>
-        PooledList<T> GetQueryFromCache(Expression<Func<T, bool>> where);
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合,优先从二级缓存读取
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序方式</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns></returns>
-        PooledList<T> GetQueryFromCache<TS>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 基本查询方法,获取一个被AutoMapper映射后的集合,优先从二级缓存读取
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <typeparam name="TDto">输出类型</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序方式</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns></returns>
-        PooledList<TDto> GetQueryFromCache<TS, TDto>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class;
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合(不跟踪实体)
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IQueryable<T> GetQueryNoTracking(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合(不跟踪实体)
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序方式</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IOrderedQueryable<T> GetQueryNoTracking<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        T Get(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 从二级缓存获取第一条数据
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        Task<T> GetFromCacheAsync(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>实体</returns>
-        T Get<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 获取第一条被AutoMapper映射后的数据
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        TDto Get<TDto>(Expression<Func<T, bool>> @where) where TDto : class;
-
-        /// <summary>
-        /// 获取第一条被AutoMapper映射后的数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>映射实体</returns>
-        TDto Get<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
-
-        /// <summary>
-        /// 获取第一条被AutoMapper映射后的数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>映射实体</returns>
-        Task<TDto> GetAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
-
-        /// <summary>
-        /// 从二级缓存获取第一条被AutoMapper映射后的数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>映射实体</returns>
-        Task<TDto> GetFromCacheAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        Task<T> GetAsync(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>实体</returns>
-        Task<T> GetAsync<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 获取第一条数据(不跟踪实体)
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        T GetNoTracking(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 根据ID找实体
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>实体</returns>
-        T GetById(int id);
-
-        /// <summary>
-        /// 根据ID找实体(异步)
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>实体</returns>
-        Task<T> GetByIdAsync(int id);
-
-        /// <summary>
-        /// 标准分页查询方法
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        PagedList<T> GetPages<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc);
-
-        /// <summary>
-        /// 标准分页查询方法,取出被AutoMapper映射后的数据集合
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <typeparam name="TDto"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        PagedList<TDto> GetPages<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc) where TDto : class;
-
-        /// <summary>
-        /// 标准分页查询方法
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        Task<PagedList<T>> GetPagesAsync<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc);
-
-        /// <summary>
-        /// 标准分页查询方法,取出被AutoMapper映射后的数据集合
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <typeparam name="TDto"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        Task<PagedList<TDto>> GetPagesAsync<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc) where TDto : class;
-
-        /// <summary>
-        /// 标准分页查询方法,优先从二级缓存读取,取出被AutoMapper映射后的数据集合
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <typeparam name="TDto"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        PagedList<TDto> GetPagesFromCache<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc) where TDto : class;
-
-        /// <summary>
-        /// 标准分页查询方法(不跟踪实体)
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        PagedList<T> GetPagesNoTracking<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 根据ID删除实体
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>删除成功</returns>
-        bool DeleteById(int id);
-
-        /// <summary>
-        /// 根据ID删除实体
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>删除成功</returns>
-        Task<int> DeleteByIdAsync(int id);
-
-        /// <summary>
-        /// 删除实体
-        /// </summary>
-        /// <param name="t">需要删除的实体</param>
-        /// <returns>删除成功</returns>
-        bool DeleteEntity(T t);
-
-        /// <summary>
-        /// 根据条件删除实体
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>删除成功</returns>
-        int DeleteEntity(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 添加实体
-        /// </summary>
-        /// <param name="t">需要添加的实体</param>
-        /// <returns>添加成功</returns>
-        T AddEntity(T t);
-
-        /// <summary>
-        /// 添加或更新实体
-        /// </summary>
-        /// <param name="key">更新键规则</param>
-        /// <param name="t">需要保存的实体</param>
-        /// <returns>保存成功</returns>
-        T AddOrUpdate<TKey>(Expression<Func<T, TKey>> key, T t);
-
-        /// <summary>
-        /// 添加或更新实体
-        /// </summary>
-        /// <param name="key">更新键规则</param>
-        /// <param name="entities">需要保存的实体</param>
-        /// <returns>保存成功</returns>
-        void AddOrUpdate<TKey>(Expression<Func<T, TKey>> key, IEnumerable<T> entities);
-
-        /// <summary>
-        /// 统一保存数据
-        /// </summary>
-        /// <returns>受影响的行数</returns>
-        int SaveChanges();
-
-        /// <summary>
-        /// 统一保存数据(异步)
-        /// </summary>
-        /// <returns>受影响的行数</returns>
-        Task<int> SaveChangesAsync();
-
-        /// <summary>
-        /// 判断实体是否在数据库中存在
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>是否存在</returns>
-        bool Any(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 符合条件的个数
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>是否存在</returns>
-        int Count(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 删除多个实体
-        /// </summary>
-        /// <param name="list">实体集合</param>
-        /// <returns>删除成功</returns>
-        bool DeleteEntities(IEnumerable<T> list);
-
-        void Dispose(bool disposing);
-
-        T this[int id] => GetById(id);
-
-        PooledList<T> this[Expression<Func<T, bool>> where] => GetQuery(where).ToPooledList();
-    }
-
-    public partial interface ICategoryRepository : IBaseRepository<Category>
-    { }
-
-    public partial interface ICommentRepository : IBaseRepository<Comment>
-    { }
-
-    public partial interface IDonateRepository : IBaseRepository<Donate>
-    { }
-
-    public partial interface IFastShareRepository : IBaseRepository<FastShare>
-    { }
-
-    public partial interface IInternalMessageRepository : IBaseRepository<InternalMessage>
-    { }
-
-    public partial interface ILeaveMessageRepository : IBaseRepository<LeaveMessage>
-    { }
-
-    public partial interface ILinksRepository : IBaseRepository<Links>
-    { }
+	public interface IBaseRepository<T> : IDisposable where T : LuceneIndexableBaseEntity
+	{
+		/// <summary>
+		/// 获取所有实体
+		/// </summary>
+		/// <returns>还未执行的SQL语句</returns>
+		IQueryable<T> GetAll();
+
+		/// <summary>
+		/// 获取所有实体(不跟踪)
+		/// </summary>
+		/// <returns>还未执行的SQL语句</returns>
+		IQueryable<T> GetAllNoTracking();
+
+		/// <summary>
+		/// 获取所有实体
+		/// </summary>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <returns>还未执行的SQL语句</returns>
+		IQueryable<TDto> GetAll<TDto>() where TDto : class;
+
+		/// <summary>
+		/// 获取所有实体
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IOrderedQueryable<T> GetAll<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 获取所有实体(不跟踪)
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IOrderedQueryable<T> GetAllNoTracking<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 从二级缓存获取所有实体
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns></returns>
+		List<T> GetAllFromCache<TS>(Expression<Func<T, TS>> orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 获取所有实体
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IQueryable<TDto> GetAll<TS, TDto>(Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IQueryable<T> GetQuery(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IOrderedQueryable<T> GetQuery<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 基本查询方法,获取一个被AutoMapper映射后的集合
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IQueryable<TDto> GetQuery<TDto>(Expression<Func<T, bool>> @where) where TDto : class;
+
+		/// <summary>
+		/// 基本查询方法,获取一个被AutoMapper映射后的集合
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <typeparam name="TDto">输出类型</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		/// <returns></returns>
+		IQueryable<TDto> GetQuery<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合,优先从二级缓存读取
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns></returns>
+		List<T> GetQueryFromCache(Expression<Func<T, bool>> where);
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合,优先从二级缓存读取
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序方式</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns></returns>
+		List<T> GetQueryFromCache<TS>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 基本查询方法,获取一个被AutoMapper映射后的集合,优先从二级缓存读取
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <typeparam name="TDto">输出类型</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序方式</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns></returns>
+		List<TDto> GetQueryFromCache<TS, TDto>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class;
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合(不跟踪实体)
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IQueryable<T> GetQueryNoTracking(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合(不跟踪实体)
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序方式</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IOrderedQueryable<T> GetQueryNoTracking<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		T Get(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 从二级缓存获取第一条数据
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		Task<T> GetFromCacheAsync(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>实体</returns>
+		T Get<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 获取第一条被AutoMapper映射后的数据
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		TDto Get<TDto>(Expression<Func<T, bool>> @where) where TDto : class;
+
+		/// <summary>
+		/// 获取第一条被AutoMapper映射后的数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>映射实体</returns>
+		TDto Get<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
+
+		/// <summary>
+		/// 获取第一条被AutoMapper映射后的数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>映射实体</returns>
+		Task<TDto> GetAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
+
+		/// <summary>
+		/// 从二级缓存获取第一条被AutoMapper映射后的数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>映射实体</returns>
+		Task<TDto> GetFromCacheAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		Task<T> GetAsync(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>实体</returns>
+		Task<T> GetAsync<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 获取第一条数据(不跟踪实体)
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		T GetNoTracking(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 根据ID找实体
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>实体</returns>
+		T GetById(int id);
+
+		/// <summary>
+		/// 根据ID找实体(异步)
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>实体</returns>
+		Task<T> GetByIdAsync(int id);
+
+		/// <summary>
+		/// 标准分页查询方法
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		PagedList<T> GetPages<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc);
+
+		/// <summary>
+		/// 标准分页查询方法,取出被AutoMapper映射后的数据集合
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <typeparam name="TDto"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		PagedList<TDto> GetPages<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc) where TDto : class;
+
+		/// <summary>
+		/// 标准分页查询方法
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		Task<PagedList<T>> GetPagesAsync<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc);
+
+		/// <summary>
+		/// 标准分页查询方法,取出被AutoMapper映射后的数据集合
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <typeparam name="TDto"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		Task<PagedList<TDto>> GetPagesAsync<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc) where TDto : class;
+
+		/// <summary>
+		/// 标准分页查询方法,优先从二级缓存读取,取出被AutoMapper映射后的数据集合
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <typeparam name="TDto"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		PagedList<TDto> GetPagesFromCache<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc) where TDto : class;
+
+		/// <summary>
+		/// 标准分页查询方法(不跟踪实体)
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		PagedList<T> GetPagesNoTracking<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 根据ID删除实体
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>删除成功</returns>
+		bool DeleteById(int id);
+
+		/// <summary>
+		/// 根据ID删除实体
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>删除成功</returns>
+		Task<int> DeleteByIdAsync(int id);
+
+		/// <summary>
+		/// 删除实体
+		/// </summary>
+		/// <param name="t">需要删除的实体</param>
+		/// <returns>删除成功</returns>
+		bool DeleteEntity(T t);
+
+		/// <summary>
+		/// 根据条件删除实体
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>删除成功</returns>
+		int DeleteEntity(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 添加实体
+		/// </summary>
+		/// <param name="t">需要添加的实体</param>
+		/// <returns>添加成功</returns>
+		T AddEntity(T t);
+
+		/// <summary>
+		/// 添加或更新实体
+		/// </summary>
+		/// <param name="key">更新键规则</param>
+		/// <param name="t">需要保存的实体</param>
+		/// <returns>保存成功</returns>
+		T AddOrUpdate<TKey>(Expression<Func<T, TKey>> key, T t);
+
+		/// <summary>
+		/// 添加或更新实体
+		/// </summary>
+		/// <param name="key">更新键规则</param>
+		/// <param name="entities">需要保存的实体</param>
+		/// <returns>保存成功</returns>
+		void AddOrUpdate<TKey>(Expression<Func<T, TKey>> key, IEnumerable<T> entities);
+
+		/// <summary>
+		/// 统一保存数据
+		/// </summary>
+		/// <returns>受影响的行数</returns>
+		int SaveChanges();
+
+		/// <summary>
+		/// 统一保存数据(异步)
+		/// </summary>
+		/// <returns>受影响的行数</returns>
+		Task<int> SaveChangesAsync();
+
+		/// <summary>
+		/// 判断实体是否在数据库中存在
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>是否存在</returns>
+		bool Any(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 符合条件的个数
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>是否存在</returns>
+		int Count(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 删除多个实体
+		/// </summary>
+		/// <param name="list">实体集合</param>
+		/// <returns>删除成功</returns>
+		bool DeleteEntities(IEnumerable<T> list);
+
+		void Dispose(bool disposing);
+
+		T this[int id] => GetById(id);
+
+		List<T> this[Expression<Func<T, bool>> where] => GetQuery(where).ToList();
+	}
+
+	public partial interface ICategoryRepository : IBaseRepository<Category>
+	{ }
+
+	public partial interface ICommentRepository : IBaseRepository<Comment>
+	{ }
+
+	public partial interface IDonateRepository : IBaseRepository<Donate>
+	{ }
+
+	public partial interface IFastShareRepository : IBaseRepository<FastShare>
+	{ }
+
+	public partial interface IInternalMessageRepository : IBaseRepository<InternalMessage>
+	{ }
+
+	public partial interface ILeaveMessageRepository : IBaseRepository<LeaveMessage>
+	{ }
+
+	public partial interface ILinksRepository : IBaseRepository<Links>
+	{ }
 
-    public partial interface ILinkLoopbackRepository : IBaseRepository<LinkLoopback>
-    { }
-
-    public partial interface ILoginRecordRepository : IBaseRepository<LoginRecord>
-    { }
+	public partial interface ILinkLoopbackRepository : IBaseRepository<LinkLoopback>
+	{ }
+
+	public partial interface ILoginRecordRepository : IBaseRepository<LoginRecord>
+	{ }
 
-    public partial interface IMenuRepository : IBaseRepository<Menu>
-    { }
+	public partial interface IMenuRepository : IBaseRepository<Menu>
+	{ }
 
-    public partial interface IMiscRepository : IBaseRepository<Misc>
-    { }
+	public partial interface IMiscRepository : IBaseRepository<Misc>
+	{ }
 
-    public partial interface INoticeRepository : IBaseRepository<Notice>
-    { }
+	public partial interface INoticeRepository : IBaseRepository<Notice>
+	{ }
 
-    public partial interface IPostRepository : IBaseRepository<Post>
-    { }
+	public partial interface IPostRepository : IBaseRepository<Post>
+	{ }
 
-    public partial interface IPostHistoryVersionRepository : IBaseRepository<PostHistoryVersion>
-    { }
+	public partial interface IPostHistoryVersionRepository : IBaseRepository<PostHistoryVersion>
+	{ }
 
-    public partial interface ISeminarRepository : IBaseRepository<Seminar>
-    { }
+	public partial interface ISeminarRepository : IBaseRepository<Seminar>
+	{ }
 
-    public partial interface ISystemSettingRepository : IBaseRepository<SystemSetting>
-    { }
+	public partial interface ISystemSettingRepository : IBaseRepository<SystemSetting>
+	{ }
 
-    public partial interface IUserInfoRepository : IBaseRepository<UserInfo>
-    { }
+	public partial interface IUserInfoRepository : IBaseRepository<UserInfo>
+	{ }
 
-    public partial interface IPostMergeRequestRepository : IBaseRepository<PostMergeRequest>
-    { }
+	public partial interface IPostMergeRequestRepository : IBaseRepository<PostMergeRequest>
+	{ }
 
-    public partial interface IAdvertisementRepository : IBaseRepository<Advertisement>
-    { }
+	public partial interface IAdvertisementRepository : IBaseRepository<Advertisement>
+	{ }
 
-    public partial interface IAdvertisementClickRecordRepository : IBaseRepository<AdvertisementClickRecord>
-    { }
+	public partial interface IAdvertisementClickRecordRepository : IBaseRepository<AdvertisementClickRecord>
+	{ }
 
-    public partial interface IVariablesRepository : IBaseRepository<Variables>
-    { }
+	public partial interface IVariablesRepository : IBaseRepository<Variables>
+	{ }
 
-    public partial interface IPostVisitRecordRepository : IBaseRepository<PostVisitRecord>
-    { }
+	public partial interface IPostVisitRecordRepository : IBaseRepository<PostVisitRecord>
+	{ }
 
-    public partial interface IPostVisitRecordStatsRepository : IBaseRepository<PostVisitRecordStats>
-    { }
+	public partial interface IPostVisitRecordStatsRepository : IBaseRepository<PostVisitRecordStats>
+	{ }
 
-    public partial interface IPostTagsRepository : IBaseRepository<PostTag>
-    { }
+	public partial interface IPostTagsRepository : IBaseRepository<PostTag>
+	{ }
 }

+ 31 - 32
src/Masuit.MyBlogs.Core/Infrastructure/Repository/PostRepository.cs

@@ -1,5 +1,4 @@
-using Collections.Pooled;
-using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
+using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
 using Masuit.MyBlogs.Core.Models.Entity;
 using Microsoft.EntityFrameworkCore;
 using System.Linq.Expressions;
@@ -9,36 +8,36 @@ namespace Masuit.MyBlogs.Core.Infrastructure.Repository;
 
 public partial class PostRepository : BaseRepository<Post>, IPostRepository
 {
-    /// <summary>
-    /// 获取第一条数据,优先从缓存读取
-    /// </summary>
-    /// <param name="where">查询条件</param>
-    /// <returns>实体</returns>
-    public override Task<Post> GetAsync(Expression<Func<Post, bool>> @where)
-    {
-        return EF.CompileAsyncQuery((DataContext ctx) => ctx.Post.Include(p => p.Category).Include(p => p.Seminar).FirstOrDefault(@where))(DataContext);
-    }
+	/// <summary>
+	/// 获取第一条数据,优先从缓存读取
+	/// </summary>
+	/// <param name="where">查询条件</param>
+	/// <returns>实体</returns>
+	public override Task<Post> GetAsync(Expression<Func<Post, bool>> @where)
+	{
+		return EF.CompileAsyncQuery((DataContext ctx) => ctx.Post.Include(p => p.Category).Include(p => p.Seminar).FirstOrDefault(@where))(DataContext);
+	}
 
-    /// <summary>
-    /// 基本查询方法,获取一个集合,优先从二级缓存读取
-    /// </summary>
-    /// <param name="where">查询条件</param>
-    /// <returns>还未执行的SQL语句</returns>
-    public override PooledList<Post> GetQueryFromCache(Expression<Func<Post, bool>> where)
-    {
-        return DataContext.Post.Include(p => p.Category).Where(where).FromCache().ToPooledList();
-    }
+	/// <summary>
+	/// 基本查询方法,获取一个集合,优先从二级缓存读取
+	/// </summary>
+	/// <param name="where">查询条件</param>
+	/// <returns>还未执行的SQL语句</returns>
+	public override List<Post> GetQueryFromCache(Expression<Func<Post, bool>> where)
+	{
+		return DataContext.Post.Include(p => p.Category).Where(where).FromCache().ToList();
+	}
 
-    /// <summary>
-    /// 基本查询方法,获取一个集合
-    /// </summary>
-    /// <typeparam name="TS">排序</typeparam>
-    /// <param name="where">查询条件</param>
-    /// <param name="orderby">排序字段</param>
-    /// <param name="isAsc">是否升序</param>
-    /// <returns>还未执行的SQL语句</returns>
-    public override IOrderedQueryable<Post> GetQuery<TS>(Expression<Func<Post, bool>> @where, Expression<Func<Post, TS>> @orderby, bool isAsc = true)
-    {
-        return isAsc ? DataContext.Post.Include(p => p.Category).Where(where).OrderBy(orderby) : DataContext.Post.Include(p => p.Category).Where(where).OrderByDescending(orderby);
-    }
+	/// <summary>
+	/// 基本查询方法,获取一个集合
+	/// </summary>
+	/// <typeparam name="TS">排序</typeparam>
+	/// <param name="where">查询条件</param>
+	/// <param name="orderby">排序字段</param>
+	/// <param name="isAsc">是否升序</param>
+	/// <returns>还未执行的SQL语句</returns>
+	public override IOrderedQueryable<Post> GetQuery<TS>(Expression<Func<Post, bool>> @where, Expression<Func<Post, TS>> @orderby, bool isAsc = true)
+	{
+		return isAsc ? DataContext.Post.Include(p => p.Category).Where(where).OrderBy(orderby) : DataContext.Post.Include(p => p.Category).Where(where).OrderByDescending(orderby);
+	}
 }

+ 66 - 65
src/Masuit.MyBlogs.Core/Infrastructure/Services/AdvertisementService.cs

@@ -3,6 +3,7 @@ using Masuit.LuceneEFCore.SearchEngine.Interfaces;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
 using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
+using Masuit.MyBlogs.Core.Models.DTO;
 using Masuit.MyBlogs.Core.Models.Entity;
 using Masuit.MyBlogs.Core.Models.Enum;
 using Masuit.Tools;
@@ -17,77 +18,77 @@ namespace Masuit.MyBlogs.Core.Infrastructure.Services;
 
 public partial class AdvertisementService : BaseService<Advertisement>, IAdvertisementService
 {
-    public ICacheManager<List<Advertisement>> CacheManager { get; set; }
+	public ICacheManager<List<AdvertisementDto>> CacheManager { get; set; }
 
-    public ICategoryRepository CategoryRepository { get; set; }
+	public ICategoryRepository CategoryRepository { get; set; }
 
-    private readonly ILuceneIndexSearcher _luceneIndexSearcher;
+	private readonly ILuceneIndexSearcher _luceneIndexSearcher;
 
-    public AdvertisementService(IBaseRepository<Advertisement> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-    {
-        _luceneIndexSearcher = searcher;
-    }
+	public AdvertisementService(IBaseRepository<Advertisement> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+		_luceneIndexSearcher = searcher;
+	}
 
-    /// <summary>
-    /// 按价格随机筛选一个元素
-    /// </summary>
-    /// <param name="type">广告类型</param>
-    /// <param name="location"></param>
-    /// <param name="cid">分类id</param>
-    /// <param name="keywords"></param>
-    /// <returns></returns>
-    public Advertisement GetByWeightedPrice(AdvertiseType type, IPLocation location, int? cid = null, string keywords = "")
-    {
-        return GetsByWeightedPrice(1, type, location, cid, keywords).FirstOrDefault();
-    }
+	/// <summary>
+	/// 按价格随机筛选一个元素
+	/// </summary>
+	/// <param name="type">广告类型</param>
+	/// <param name="location"></param>
+	/// <param name="cid">分类id</param>
+	/// <param name="keywords"></param>
+	/// <returns></returns>
+	public AdvertisementDto GetByWeightedPrice(AdvertiseType type, IPLocation location, int? cid = null, string keywords = "")
+	{
+		return GetsByWeightedPrice(1, type, location, cid, keywords).FirstOrDefault();
+	}
 
-    /// <summary>
-    /// 按价格随机筛选一个元素
-    /// </summary>
-    /// <param name="count">数量</param>
-    /// <param name="type">广告类型</param>
-    /// <param name="ipinfo"></param>
-    /// <param name="cid">分类id</param>
-    /// <param name="keywords"></param>
-    /// <returns></returns>
-    public List<Advertisement> GetsByWeightedPrice(int count, AdvertiseType type, IPLocation ipinfo, int? cid = null, string keywords = "")
-    {
-        var (location, _, _) = ipinfo;
-        return CacheManager.GetOrAdd($"Advertisement:{location.Crc32()}:{type}:{count}-{cid}-{keywords}", _ =>
-        {
-            var atype = type.ToString("D");
-            Expression<Func<Advertisement, bool>> where = a => a.Types.Contains(atype) && a.Status == Status.Available;
-            var catCount = CategoryRepository.Count(_ => true);
-            where = where.And(a => a.RegionMode == RegionLimitMode.All || (a.RegionMode == RegionLimitMode.AllowRegion ? Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase) : !Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase)));
-            if (cid.HasValue)
-            {
-                var pids = CategoryRepository.GetQuery(c => c.Id == cid).Select(c => c.ParentId + "|" + c.Parent.ParentId).FromCache(new MemoryCacheEntryOptions()
-                {
-                    AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(5)
-                }).ToArray();
-                var scid = pids.Select(s => s.Trim('|')).Where(s => !string.IsNullOrEmpty(s)).Append(cid + "").Join("|");
-                if (Any(a => Regex.IsMatch(a.CategoryIds, scid)))
-                {
-                    where = where.And(a => Regex.IsMatch(a.CategoryIds, scid) || string.IsNullOrEmpty(a.CategoryIds));
-                }
-            }
+	/// <summary>
+	/// 按价格随机筛选一个元素
+	/// </summary>
+	/// <param name="count">数量</param>
+	/// <param name="type">广告类型</param>
+	/// <param name="ipinfo"></param>
+	/// <param name="cid">分类id</param>
+	/// <param name="keywords"></param>
+	/// <returns></returns>
+	public List<AdvertisementDto> GetsByWeightedPrice(int count, AdvertiseType type, IPLocation ipinfo, int? cid = null, string keywords = "")
+	{
+		var (location, _, _) = ipinfo;
+		return CacheManager.GetOrAdd($"Advertisement:{location.Crc32()}:{type}:{count}-{cid}-{keywords}", _ =>
+		{
+			var atype = type.ToString("D");
+			Expression<Func<Advertisement, bool>> where = a => a.Types.Contains(atype) && a.Status == Status.Available;
+			var catCount = CategoryRepository.Count(_ => true);
+			where = where.And(a => a.RegionMode == RegionLimitMode.All || (a.RegionMode == RegionLimitMode.AllowRegion ? Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase) : !Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase)));
+			if (cid.HasValue)
+			{
+				var pids = CategoryRepository.GetQuery(c => c.Id == cid).Select(c => c.ParentId + "|" + c.Parent.ParentId).FromCache(new MemoryCacheEntryOptions()
+				{
+					AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(5)
+				}).ToArray();
+				var scid = pids.Select(s => s.Trim('|')).Where(s => !string.IsNullOrEmpty(s)).Append(cid + "").Join("|");
+				if (Any(a => Regex.IsMatch(a.CategoryIds, scid)))
+				{
+					where = where.And(a => Regex.IsMatch(a.CategoryIds, scid) || string.IsNullOrEmpty(a.CategoryIds));
+				}
+			}
 
-            if (!keywords.IsNullOrEmpty())
-            {
-                var regex = _luceneIndexSearcher.CutKeywords(keywords).Select(Regex.Escape).Join("|");
-                where = where.And(a => Regex.IsMatch(a.Title + a.Description, regex));
-            }
+			if (!keywords.IsNullOrEmpty())
+			{
+				var regex = _luceneIndexSearcher.CutKeywords(keywords).Select(Regex.Escape).Join("|");
+				where = where.And(a => Regex.IsMatch(a.Title + a.Description, regex));
+			}
 
-            var array = GetQuery(a => a.Status == Status.Available).GroupBy(a => a.Merchant).Select(g => g.OrderBy(_ => EF.Functions.Random()).FirstOrDefault().Id).Take(50).ToArray();
-            var list = GetQuery(where).Where(a => array.Contains(a.Id)).OrderBy(a => -Math.Log(EF.Functions.Random()) / ((double)a.Price / a.Types.Length * catCount / (string.IsNullOrEmpty(a.CategoryIds) ? catCount : (a.CategoryIds.Length + 1)))).Take(count).AsNoTracking().ToList();
-            if (list.Count == 0 && keywords is { Length: > 0 })
-            {
-                return GetsByWeightedPrice(count, type, ipinfo, cid);
-            }
+			var array = GetQuery(a => a.Status == Status.Available).GroupBy(a => a.Merchant).Select(g => g.OrderBy(_ => EF.Functions.Random()).FirstOrDefault().Id).Take(50).ToArray();
+			var list = GetQuery<AdvertisementDto>(where).Where(a => array.Contains(a.Id)).OrderBy(a => -Math.Log(EF.Functions.Random()) / ((double)a.Price / a.Types.Length * catCount / (string.IsNullOrEmpty(a.CategoryIds) ? catCount : (a.CategoryIds.Length + 1)))).Take(count).ToList();
+			if (list.Count == 0 && keywords is { Length: > 0 })
+			{
+				return GetsByWeightedPrice(count, type, ipinfo, cid);
+			}
 
-            var ids = list.Select(a => a.Id).ToArray();
-            GetQuery(a => ids.Contains(a.Id)).ExecuteUpdate(a => a.SetProperty(a => a.DisplayCount, a => a.DisplayCount + 1));
-            return list;
-        });
-    }
+			var ids = list.Select(a => a.Id).ToArray();
+			GetQuery(a => ids.Contains(a.Id)).ExecuteUpdate(a => a.SetProperty(a => a.DisplayCount, a => a.DisplayCount + 1));
+			return list;
+		});
+	}
 }

+ 661 - 662
src/Masuit.MyBlogs.Core/Infrastructure/Services/BaseService.cs

@@ -1,5 +1,4 @@
-using Collections.Pooled;
-using Masuit.LuceneEFCore.SearchEngine;
+using Masuit.LuceneEFCore.SearchEngine;
 using Masuit.LuceneEFCore.SearchEngine.Interfaces;
 using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
 using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
@@ -8,664 +7,664 @@ using System.Linq.Expressions;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Services
 {
-    /// <summary>
-    /// 业务层基类
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    public class BaseService<T> : IBaseService<T> where T : LuceneIndexableBaseEntity
-    {
-        public virtual IBaseRepository<T> BaseDal { get; set; }
-        protected readonly ISearchEngine<DataContext> SearchEngine;
-        protected readonly ILuceneIndexSearcher Searcher;
-
-        public BaseService(IBaseRepository<T> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher)
-        {
-            BaseDal = repository;
-            SearchEngine = searchEngine;
-            Searcher = searcher;
-        }
-
-        /// <summary>
-        /// 获取所有实体
-        /// </summary>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<T> GetAll()
-        {
-            return BaseDal.GetAll();
-        }
-
-        /// <summary>
-        /// 获取所有实体(不跟踪)
-        /// </summary>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<T> GetAllNoTracking()
-        {
-            return BaseDal.GetAllNoTracking();
-        }
-
-        /// <summary>
-        /// 获取所有实体
-        /// </summary>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<TDto> GetAll<TDto>() where TDto : class
-        {
-            return BaseDal.GetAll<TDto>();
-        }
-
-        /// <summary>
-        /// 获取所有实体
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IOrderedQueryable<T> GetAll<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true)
-        {
-            return BaseDal.GetAll(orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 获取所有实体(不跟踪)
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IOrderedQueryable<T> GetAllNoTracking<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true)
-        {
-            return BaseDal.GetAllNoTracking(orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 从二级缓存获取所有实体
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual PooledList<T> GetAllFromCache<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true)
-        {
-            return BaseDal.GetAllFromCache(orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 获取所有实体
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<TDto> GetAll<TS, TDto>(Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
-        {
-            return BaseDal.GetAll<TS, TDto>(orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<T> GetQuery(Expression<Func<T, bool>> @where)
-        {
-            return BaseDal.GetQuery(where);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IOrderedQueryable<T> IBaseService<T>.GetQuery<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc)
-        {
-            return BaseDal.GetQuery(where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个被AutoMapper映射后的集合
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<TDto> GetQuery<TDto>(Expression<Func<T, bool>> @where) where TDto : class
-        {
-            return BaseDal.GetQuery<TDto>(where);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个被AutoMapper映射后的集合
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <typeparam name="TDto">输出类型</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        /// <returns></returns>
-        public virtual IQueryable<TDto> GetQuery<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
-        {
-            return BaseDal.GetQuery<TS, TDto>(where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合,优先从二级缓存读取
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IEnumerable<T> GetQueryFromCache(Expression<Func<T, bool>> @where)
-        {
-            return BaseDal.GetQueryFromCache(where);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合,优先从二级缓存读取
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序方式</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IEnumerable<T> GetQueryFromCache<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true)
-        {
-            return BaseDal.GetQueryFromCache(where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个被AutoMapper映射后的集合,优先从二级缓存读取
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <typeparam name="TDto">输出类型</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序方式</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual PooledList<TDto> GetQueryFromCache<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
-        {
-            return BaseDal.GetQueryFromCache<TS, TDto>(where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合(不跟踪实体)
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IQueryable<T> GetQueryNoTracking(Expression<Func<T, bool>> @where)
-        {
-            return BaseDal.GetQueryNoTracking(where);
-        }
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合(不跟踪实体)
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序方式</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual IOrderedQueryable<T> GetQueryNoTracking<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true)
-        {
-            return BaseDal.GetQueryNoTracking(where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        public virtual T Get(Expression<Func<T, bool>> @where)
-        {
-            return BaseDal.Get(where);
-        }
-
-        /// <summary>
-        /// 从二级缓存获取第一条数据
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        public Task<T> GetFromCacheAsync(Expression<Func<T, bool>> @where)
-        {
-            return BaseDal.GetFromCacheAsync(where);
-        }
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>实体</returns>
-        public virtual T Get<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true)
-        {
-            return BaseDal.Get(where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 获取第一条被AutoMapper映射后的数据
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        public virtual TDto Get<TDto>(Expression<Func<T, bool>> @where) where TDto : class
-        {
-            return BaseDal.Get<TDto>(where);
-        }
-
-        /// <summary>
-        /// 获取第一条被AutoMapper映射后的数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>映射实体</returns>
-        public virtual TDto Get<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
-        {
-            return BaseDal.Get<TS, TDto>(where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 获取第一条被AutoMapper映射后的数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>映射实体</returns>
-        public Task<TDto> GetAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
-        {
-            return BaseDal.GetAsync<TS, TDto>(where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 从二级缓存获取第一条被AutoMapper映射后的数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>映射实体</returns>
-        public Task<TDto> GetFromCacheAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
-        {
-            return BaseDal.GetFromCacheAsync<TS, TDto>(where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>实体</returns>
-        public virtual Task<T> GetAsync<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true)
-        {
-            return BaseDal.GetAsync(where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 获取第一条数据,优先从缓存读取
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        public virtual Task<T> GetAsync(Expression<Func<T, bool>> @where)
-        {
-            return BaseDal.GetAsync(where);
-        }
-
-        /// <summary>
-        /// 获取第一条数据(不跟踪实体)
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        public virtual T GetNoTracking(Expression<Func<T, bool>> @where)
-        {
-            return BaseDal.GetNoTracking(where);
-        }
-
-        /// <summary>
-        /// 根据ID找实体
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>实体</returns>
-        public virtual T GetById(int id)
-        {
-            return BaseDal.GetById(id);
-        }
-
-        /// <summary>
-        /// 根据ID找实体(异步)
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>实体</returns>
-        public virtual Task<T> GetByIdAsync(int id)
-        {
-            return BaseDal.GetByIdAsync(id);
-        }
-
-        /// <summary>
-        /// 标准分页查询方法
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual PagedList<T> GetPages<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true)
-        {
-            return BaseDal.GetPages(pageIndex, pageSize, where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 标准分页查询方法,取出被AutoMapper映射后的数据集合
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual PagedList<TDto> GetPages<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc) where TDto : class
-        {
-            return BaseDal.GetPages<TS, TDto>(pageIndex, pageSize, where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 标准分页查询方法
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public Task<PagedList<T>> GetPagesAsync<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc)
-        {
-            return BaseDal.GetPagesAsync(pageIndex, pageSize, where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 标准分页查询方法,取出被AutoMapper映射后的数据集合
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <typeparam name="TDto"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public Task<PagedList<TDto>> GetPagesAsync<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc) where TDto : class
-        {
-            return BaseDal.GetPagesAsync<TS, TDto>(pageIndex, pageSize, where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 标准分页查询方法,优先从二级缓存读取,取出被AutoMapper映射后的数据集合
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual PagedList<TDto> GetPagesFromCache<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc) where TDto : class
-        {
-            return BaseDal.GetPagesFromCache<TS, TDto>(pageIndex, pageSize, where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 标准分页查询方法(不跟踪实体)
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        public virtual PagedList<T> GetPagesNoTracking<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true)
-        {
-            return BaseDal.GetPagesNoTracking(pageIndex, pageSize, where, orderby, isAsc);
-        }
-
-        /// <summary>
-        /// 根据ID删除实体
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>删除成功</returns>
-        public virtual bool DeleteById(int id)
-        {
-            return BaseDal.DeleteById(id);
-        }
-
-        /// <summary>
-        /// 根据ID删除实体并保存(异步)
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>删除成功</returns>
-        public virtual Task<int> DeleteByIdAsync(int id)
-        {
-            return BaseDal.DeleteByIdAsync(id);
-        }
-
-        /// <summary>
-        /// 删除实体并保存
-        /// </summary>
-        /// <param name="t">需要删除的实体</param>
-        /// <returns>删除成功</returns>
-        public virtual bool DeleteEntity(T t)
-        {
-            return BaseDal.DeleteEntity(t);
-        }
-
-        /// <summary>
-        /// 根据条件删除实体
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>删除成功</returns>
-        public virtual int DeleteEntity(Expression<Func<T, bool>> @where)
-        {
-            return BaseDal.DeleteEntity(where);
-        }
-
-        /// <summary>
-        /// 根据条件删除实体
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>删除成功</returns>
-        public virtual int DeleteEntitySaved(Expression<Func<T, bool>> @where)
-        {
-            BaseDal.DeleteEntity(where);
-            return SaveChanges();
-        }
-
-        /// <summary>
-        /// 根据条件删除实体
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>删除成功</returns>
-        public virtual Task<int> DeleteEntitySavedAsync(Expression<Func<T, bool>> @where)
-        {
-            BaseDal.DeleteEntity(where);
-            return SaveChangesAsync();
-        }
-
-        /// <summary>
-        /// 删除实体并保存
-        /// </summary>
-        /// <param name="t">需要删除的实体</param>
-        /// <returns>删除成功</returns>
-        public virtual bool DeleteEntitySaved(T t)
-        {
-            BaseDal.DeleteEntity(t);
-            return SaveChanges() > 0;
-        }
-
-        /// <summary>
-        /// 添加实体
-        /// </summary>
-        /// <param name="t">需要添加的实体</param>
-        /// <returns>添加成功</returns>
-        public virtual T AddEntity(T t)
-        {
-            return BaseDal.AddEntity(t);
-        }
-
-        /// <summary>
-        /// 添加或更新实体
-        /// </summary>
-        /// <param name="key">更新键规则</param>
-        /// <param name="t">需要保存的实体</param>
-        /// <returns>保存成功</returns>
-        public T AddOrUpdate<TKey>(Expression<Func<T, TKey>> key, T t)
-        {
-            return BaseDal.AddOrUpdate(key, t);
-        }
-
-        /// <summary>
-        /// 添加或更新实体
-        /// </summary>
-        /// <param name="key">更新键规则</param>
-        /// <param name="entities">需要保存的实体</param>
-        /// <returns>保存成功</returns>
-        public void AddOrUpdate<TKey>(Expression<Func<T, TKey>> key, IEnumerable<T> entities)
-        {
-            BaseDal.AddOrUpdate(key, entities);
-        }
-
-        /// <summary>
-        /// 添加实体并保存
-        /// </summary>
-        /// <param name="t">需要添加的实体</param>
-        /// <returns>添加成功</returns>
-        public virtual T AddEntitySaved(T t)
-        {
-            T entity = BaseDal.AddEntity(t);
-            bool b = SaveChanges() > 0;
-            return b ? entity : null;
-        }
-
-        /// <summary>
-        /// 添加或更新实体
-        /// </summary>
-        /// <param name="key">更新键规则</param>
-        /// <param name="t">需要保存的实体</param>
-        /// <returns>保存成功</returns>
-        public Task<int> AddOrUpdateSavedAsync<TKey>(Expression<Func<T, TKey>> key, T t)
-        {
-            AddOrUpdate(key, t);
-            return SaveChangesAsync();
-        }
-
-        /// <summary>
-        /// 添加或更新实体
-        /// </summary>
-        /// <param name="key">更新键规则</param>
-        /// <param name="entities">需要保存的实体</param>
-        /// <returns>保存成功</returns>
-        public Task<int> AddOrUpdateSavedAsync<TKey>(Expression<Func<T, TKey>> key, IEnumerable<T> entities)
-        {
-            AddOrUpdate(key, entities);
-            return SaveChangesAsync();
-        }
-
-        /// <summary>
-        /// 添加实体并保存(异步)
-        /// </summary>
-        /// <param name="t">需要添加的实体</param>
-        /// <returns>添加成功</returns>
-        public virtual Task<int> AddEntitySavedAsync(T t)
-        {
-            BaseDal.AddEntity(t);
-            return SaveChangesAsync();
-        }
-
-        /// <summary>
-        /// 统一保存的方法
-        /// </summary>
-        /// <returns>受影响的行数</returns>
-        public virtual int SaveChanges()
-        {
-            return BaseDal.SaveChanges();
-        }
-
-        /// <summary>
-        /// 统一保存数据
-        /// </summary>
-        /// <returns>受影响的行数</returns>
-        public virtual Task<int> SaveChangesAsync()
-        {
-            return BaseDal.SaveChangesAsync();
-        }
-
-        /// <summary>
-        /// 判断实体是否在数据库中存在
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>是否存在</returns>
-        public virtual bool Any(Expression<Func<T, bool>> @where)
-        {
-            return BaseDal.Any(where);
-        }
-
-        /// <summary>
-        /// 统计符合条件的个数
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns></returns>
-        public int Count(Expression<Func<T, bool>> @where)
-        {
-            return BaseDal.Count(where);
-        }
-
-        /// <summary>
-        /// 删除多个实体
-        /// </summary>
-        /// <param name="list">实体集合</param>
-        /// <returns>删除成功</returns>
-        public virtual bool DeleteEntities(IEnumerable<T> list)
-        {
-            return BaseDal.DeleteEntities(list);
-        }
-
-        /// <summary>
-        /// 删除多个实体并保存(异步)
-        /// </summary>
-        /// <param name="list">实体集合</param>
-        /// <returns>删除成功</returns>
-        public virtual Task<int> DeleteEntitiesSavedAsync(IEnumerable<T> list)
-        {
-            BaseDal.DeleteEntities(list);
-            return SaveChangesAsync();
-        }
-
-        public virtual T this[int id]
-        {
-            get => GetById(id);
-            set => AddEntity(value);
-        }
-
-        public virtual string this[int id, Expression<Func<T, string>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
-        public virtual int this[int id, Expression<Func<T, int>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
-        public virtual DateTime this[int id, Expression<Func<T, DateTime>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
-        public virtual long this[int id, Expression<Func<T, long>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
-        public virtual decimal this[int id, Expression<Func<T, decimal>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
-
-        public static T operator +(BaseService<T> left, T right) => left.AddEntity(right);
-        public static bool operator -(BaseService<T> left, T right) => left.DeleteEntity(right);
-        public static bool operator -(BaseService<T> left, int id) => left.DeleteById(id);
-    }
+	/// <summary>
+	/// 业务层基类
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	public class BaseService<T> : IBaseService<T> where T : LuceneIndexableBaseEntity
+	{
+		public virtual IBaseRepository<T> BaseDal { get; set; }
+		protected readonly ISearchEngine<DataContext> SearchEngine;
+		protected readonly ILuceneIndexSearcher Searcher;
+
+		public BaseService(IBaseRepository<T> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher)
+		{
+			BaseDal = repository;
+			SearchEngine = searchEngine;
+			Searcher = searcher;
+		}
+
+		/// <summary>
+		/// 获取所有实体
+		/// </summary>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<T> GetAll()
+		{
+			return BaseDal.GetAll();
+		}
+
+		/// <summary>
+		/// 获取所有实体(不跟踪)
+		/// </summary>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<T> GetAllNoTracking()
+		{
+			return BaseDal.GetAllNoTracking();
+		}
+
+		/// <summary>
+		/// 获取所有实体
+		/// </summary>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<TDto> GetAll<TDto>() where TDto : class
+		{
+			return BaseDal.GetAll<TDto>();
+		}
+
+		/// <summary>
+		/// 获取所有实体
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IOrderedQueryable<T> GetAll<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true)
+		{
+			return BaseDal.GetAll(orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 获取所有实体(不跟踪)
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IOrderedQueryable<T> GetAllNoTracking<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true)
+		{
+			return BaseDal.GetAllNoTracking(orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 从二级缓存获取所有实体
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual List<T> GetAllFromCache<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true)
+		{
+			return BaseDal.GetAllFromCache(orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 获取所有实体
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<TDto> GetAll<TS, TDto>(Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
+		{
+			return BaseDal.GetAll<TS, TDto>(orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<T> GetQuery(Expression<Func<T, bool>> @where)
+		{
+			return BaseDal.GetQuery(where);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IOrderedQueryable<T> IBaseService<T>.GetQuery<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc)
+		{
+			return BaseDal.GetQuery(where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个被AutoMapper映射后的集合
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<TDto> GetQuery<TDto>(Expression<Func<T, bool>> @where) where TDto : class
+		{
+			return BaseDal.GetQuery<TDto>(where);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个被AutoMapper映射后的集合
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <typeparam name="TDto">输出类型</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		/// <returns></returns>
+		public virtual IQueryable<TDto> GetQuery<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
+		{
+			return BaseDal.GetQuery<TS, TDto>(where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合,优先从二级缓存读取
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IEnumerable<T> GetQueryFromCache(Expression<Func<T, bool>> @where)
+		{
+			return BaseDal.GetQueryFromCache(where);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合,优先从二级缓存读取
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序方式</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IEnumerable<T> GetQueryFromCache<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true)
+		{
+			return BaseDal.GetQueryFromCache(where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个被AutoMapper映射后的集合,优先从二级缓存读取
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <typeparam name="TDto">输出类型</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序方式</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual List<TDto> GetQueryFromCache<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
+		{
+			return BaseDal.GetQueryFromCache<TS, TDto>(where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合(不跟踪实体)
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IQueryable<T> GetQueryNoTracking(Expression<Func<T, bool>> @where)
+		{
+			return BaseDal.GetQueryNoTracking(where);
+		}
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合(不跟踪实体)
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序方式</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual IOrderedQueryable<T> GetQueryNoTracking<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true)
+		{
+			return BaseDal.GetQueryNoTracking(where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		public virtual T Get(Expression<Func<T, bool>> @where)
+		{
+			return BaseDal.Get(where);
+		}
+
+		/// <summary>
+		/// 从二级缓存获取第一条数据
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		public Task<T> GetFromCacheAsync(Expression<Func<T, bool>> @where)
+		{
+			return BaseDal.GetFromCacheAsync(where);
+		}
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>实体</returns>
+		public virtual T Get<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true)
+		{
+			return BaseDal.Get(where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 获取第一条被AutoMapper映射后的数据
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		public virtual TDto Get<TDto>(Expression<Func<T, bool>> @where) where TDto : class
+		{
+			return BaseDal.Get<TDto>(where);
+		}
+
+		/// <summary>
+		/// 获取第一条被AutoMapper映射后的数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>映射实体</returns>
+		public virtual TDto Get<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
+		{
+			return BaseDal.Get<TS, TDto>(where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 获取第一条被AutoMapper映射后的数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>映射实体</returns>
+		public Task<TDto> GetAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
+		{
+			return BaseDal.GetAsync<TS, TDto>(where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 从二级缓存获取第一条被AutoMapper映射后的数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>映射实体</returns>
+		public Task<TDto> GetFromCacheAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class
+		{
+			return BaseDal.GetFromCacheAsync<TS, TDto>(where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>实体</returns>
+		public virtual Task<T> GetAsync<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true)
+		{
+			return BaseDal.GetAsync(where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 获取第一条数据,优先从缓存读取
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		public virtual Task<T> GetAsync(Expression<Func<T, bool>> @where)
+		{
+			return BaseDal.GetAsync(where);
+		}
+
+		/// <summary>
+		/// 获取第一条数据(不跟踪实体)
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		public virtual T GetNoTracking(Expression<Func<T, bool>> @where)
+		{
+			return BaseDal.GetNoTracking(where);
+		}
+
+		/// <summary>
+		/// 根据ID找实体
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>实体</returns>
+		public virtual T GetById(int id)
+		{
+			return BaseDal.GetById(id);
+		}
+
+		/// <summary>
+		/// 根据ID找实体(异步)
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>实体</returns>
+		public virtual Task<T> GetByIdAsync(int id)
+		{
+			return BaseDal.GetByIdAsync(id);
+		}
+
+		/// <summary>
+		/// 标准分页查询方法
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual PagedList<T> GetPages<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true)
+		{
+			return BaseDal.GetPages(pageIndex, pageSize, where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 标准分页查询方法,取出被AutoMapper映射后的数据集合
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual PagedList<TDto> GetPages<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc) where TDto : class
+		{
+			return BaseDal.GetPages<TS, TDto>(pageIndex, pageSize, where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 标准分页查询方法
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public Task<PagedList<T>> GetPagesAsync<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc)
+		{
+			return BaseDal.GetPagesAsync(pageIndex, pageSize, where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 标准分页查询方法,取出被AutoMapper映射后的数据集合
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <typeparam name="TDto"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public Task<PagedList<TDto>> GetPagesAsync<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc) where TDto : class
+		{
+			return BaseDal.GetPagesAsync<TS, TDto>(pageIndex, pageSize, where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 标准分页查询方法,优先从二级缓存读取,取出被AutoMapper映射后的数据集合
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual PagedList<TDto> GetPagesFromCache<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc) where TDto : class
+		{
+			return BaseDal.GetPagesFromCache<TS, TDto>(pageIndex, pageSize, where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 标准分页查询方法(不跟踪实体)
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		public virtual PagedList<T> GetPagesNoTracking<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true)
+		{
+			return BaseDal.GetPagesNoTracking(pageIndex, pageSize, where, orderby, isAsc);
+		}
+
+		/// <summary>
+		/// 根据ID删除实体
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>删除成功</returns>
+		public virtual bool DeleteById(int id)
+		{
+			return BaseDal.DeleteById(id);
+		}
+
+		/// <summary>
+		/// 根据ID删除实体并保存(异步)
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>删除成功</returns>
+		public virtual Task<int> DeleteByIdAsync(int id)
+		{
+			return BaseDal.DeleteByIdAsync(id);
+		}
+
+		/// <summary>
+		/// 删除实体并保存
+		/// </summary>
+		/// <param name="t">需要删除的实体</param>
+		/// <returns>删除成功</returns>
+		public virtual bool DeleteEntity(T t)
+		{
+			return BaseDal.DeleteEntity(t);
+		}
+
+		/// <summary>
+		/// 根据条件删除实体
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>删除成功</returns>
+		public virtual int DeleteEntity(Expression<Func<T, bool>> @where)
+		{
+			return BaseDal.DeleteEntity(where);
+		}
+
+		/// <summary>
+		/// 根据条件删除实体
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>删除成功</returns>
+		public virtual int DeleteEntitySaved(Expression<Func<T, bool>> @where)
+		{
+			BaseDal.DeleteEntity(where);
+			return SaveChanges();
+		}
+
+		/// <summary>
+		/// 根据条件删除实体
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>删除成功</returns>
+		public virtual Task<int> DeleteEntitySavedAsync(Expression<Func<T, bool>> @where)
+		{
+			BaseDal.DeleteEntity(where);
+			return SaveChangesAsync();
+		}
+
+		/// <summary>
+		/// 删除实体并保存
+		/// </summary>
+		/// <param name="t">需要删除的实体</param>
+		/// <returns>删除成功</returns>
+		public virtual bool DeleteEntitySaved(T t)
+		{
+			BaseDal.DeleteEntity(t);
+			return SaveChanges() > 0;
+		}
+
+		/// <summary>
+		/// 添加实体
+		/// </summary>
+		/// <param name="t">需要添加的实体</param>
+		/// <returns>添加成功</returns>
+		public virtual T AddEntity(T t)
+		{
+			return BaseDal.AddEntity(t);
+		}
+
+		/// <summary>
+		/// 添加或更新实体
+		/// </summary>
+		/// <param name="key">更新键规则</param>
+		/// <param name="t">需要保存的实体</param>
+		/// <returns>保存成功</returns>
+		public T AddOrUpdate<TKey>(Expression<Func<T, TKey>> key, T t)
+		{
+			return BaseDal.AddOrUpdate(key, t);
+		}
+
+		/// <summary>
+		/// 添加或更新实体
+		/// </summary>
+		/// <param name="key">更新键规则</param>
+		/// <param name="entities">需要保存的实体</param>
+		/// <returns>保存成功</returns>
+		public void AddOrUpdate<TKey>(Expression<Func<T, TKey>> key, IEnumerable<T> entities)
+		{
+			BaseDal.AddOrUpdate(key, entities);
+		}
+
+		/// <summary>
+		/// 添加实体并保存
+		/// </summary>
+		/// <param name="t">需要添加的实体</param>
+		/// <returns>添加成功</returns>
+		public virtual T AddEntitySaved(T t)
+		{
+			T entity = BaseDal.AddEntity(t);
+			bool b = SaveChanges() > 0;
+			return b ? entity : null;
+		}
+
+		/// <summary>
+		/// 添加或更新实体
+		/// </summary>
+		/// <param name="key">更新键规则</param>
+		/// <param name="t">需要保存的实体</param>
+		/// <returns>保存成功</returns>
+		public Task<int> AddOrUpdateSavedAsync<TKey>(Expression<Func<T, TKey>> key, T t)
+		{
+			AddOrUpdate(key, t);
+			return SaveChangesAsync();
+		}
+
+		/// <summary>
+		/// 添加或更新实体
+		/// </summary>
+		/// <param name="key">更新键规则</param>
+		/// <param name="entities">需要保存的实体</param>
+		/// <returns>保存成功</returns>
+		public Task<int> AddOrUpdateSavedAsync<TKey>(Expression<Func<T, TKey>> key, IEnumerable<T> entities)
+		{
+			AddOrUpdate(key, entities);
+			return SaveChangesAsync();
+		}
+
+		/// <summary>
+		/// 添加实体并保存(异步)
+		/// </summary>
+		/// <param name="t">需要添加的实体</param>
+		/// <returns>添加成功</returns>
+		public virtual Task<int> AddEntitySavedAsync(T t)
+		{
+			BaseDal.AddEntity(t);
+			return SaveChangesAsync();
+		}
+
+		/// <summary>
+		/// 统一保存的方法
+		/// </summary>
+		/// <returns>受影响的行数</returns>
+		public virtual int SaveChanges()
+		{
+			return BaseDal.SaveChanges();
+		}
+
+		/// <summary>
+		/// 统一保存数据
+		/// </summary>
+		/// <returns>受影响的行数</returns>
+		public virtual Task<int> SaveChangesAsync()
+		{
+			return BaseDal.SaveChangesAsync();
+		}
+
+		/// <summary>
+		/// 判断实体是否在数据库中存在
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>是否存在</returns>
+		public virtual bool Any(Expression<Func<T, bool>> @where)
+		{
+			return BaseDal.Any(where);
+		}
+
+		/// <summary>
+		/// 统计符合条件的个数
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns></returns>
+		public int Count(Expression<Func<T, bool>> @where)
+		{
+			return BaseDal.Count(where);
+		}
+
+		/// <summary>
+		/// 删除多个实体
+		/// </summary>
+		/// <param name="list">实体集合</param>
+		/// <returns>删除成功</returns>
+		public virtual bool DeleteEntities(IEnumerable<T> list)
+		{
+			return BaseDal.DeleteEntities(list);
+		}
+
+		/// <summary>
+		/// 删除多个实体并保存(异步)
+		/// </summary>
+		/// <param name="list">实体集合</param>
+		/// <returns>删除成功</returns>
+		public virtual Task<int> DeleteEntitiesSavedAsync(IEnumerable<T> list)
+		{
+			BaseDal.DeleteEntities(list);
+			return SaveChangesAsync();
+		}
+
+		public virtual T this[int id]
+		{
+			get => GetById(id);
+			set => AddEntity(value);
+		}
+
+		public virtual string this[int id, Expression<Func<T, string>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
+		public virtual int this[int id, Expression<Func<T, int>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
+		public virtual DateTime this[int id, Expression<Func<T, DateTime>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
+		public virtual long this[int id, Expression<Func<T, long>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
+		public virtual decimal this[int id, Expression<Func<T, decimal>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
+
+		public static T operator +(BaseService<T> left, T right) => left.AddEntity(right);
+		public static bool operator -(BaseService<T> left, T right) => left.DeleteEntity(right);
+		public static bool operator -(BaseService<T> left, int id) => left.DeleteById(id);
+	}
 }

+ 20 - 19
src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/IAdvertisementService.cs

@@ -1,4 +1,5 @@
 using Masuit.MyBlogs.Core.Common;
+using Masuit.MyBlogs.Core.Models.DTO;
 using Masuit.MyBlogs.Core.Models.Entity;
 using Masuit.MyBlogs.Core.Models.Enum;
 
@@ -6,25 +7,25 @@ namespace Masuit.MyBlogs.Core.Infrastructure.Services.Interface
 {
     public partial interface IAdvertisementService : IBaseService<Advertisement>
     {
-        /// <summary>
-        /// 按价格随机筛选一个元素
-        /// </summary>
-        /// <param name="type">广告类型</param>
-        /// <param name="location"></param>
-        /// <param name="cid">分类id</param>
-        /// <param name="keywords"></param>
-        /// <returns></returns>
-        Advertisement GetByWeightedPrice(AdvertiseType type, IPLocation location, int? cid = null, string keywords = null);
+		/// <summary>
+		/// 按价格随机筛选一个元素
+		/// </summary>
+		/// <param name="type">广告类型</param>
+		/// <param name="location"></param>
+		/// <param name="cid">分类id</param>
+		/// <param name="keywords"></param>
+		/// <returns></returns>
+		AdvertisementDto GetByWeightedPrice(AdvertiseType type, IPLocation location, int? cid = null, string keywords = "");
 
-        /// <summary>
-        /// 按价格随机筛选多个元素
-        /// </summary>
-        /// <param name="count">数量</param>
-        /// <param name="type">广告类型</param>
-        /// <param name="location"></param>
-        /// <param name="cid">分类id</param>
-        /// <param name="keywords"></param>
-        /// <returns></returns>
-        List<Advertisement> GetsByWeightedPrice(int count, AdvertiseType type, IPLocation location, int? cid = null, string keywords = null);
+		/// <summary>
+		/// 按价格随机筛选多个元素
+		/// </summary>
+		/// <param name="count">数量</param>
+		/// <param name="type">广告类型</param>
+		/// <param name="location"></param>
+		/// <param name="cid">分类id</param>
+		/// <param name="keywords"></param>
+		/// <returns></returns>
+		List<AdvertisementDto> GetsByWeightedPrice(int count, AdvertiseType type, IPLocation location, int? cid = null, string keywords = "");
     }
 }

+ 464 - 465
src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/IBaseService.cs

@@ -1,471 +1,470 @@
-using Collections.Pooled;
-using Masuit.LuceneEFCore.SearchEngine;
+using Masuit.LuceneEFCore.SearchEngine;
 using Masuit.Tools.Models;
 using System.Linq.Expressions;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Services.Interface
 {
-    public interface IBaseService<T> where T : LuceneIndexableBaseEntity
-    {
-        /// <summary>
-        /// 获取所有实体
-        /// </summary>
-        /// <returns>还未执行的SQL语句</returns>
-        IQueryable<T> GetAll();
-
-        /// <summary>
-        /// 获取所有实体(不跟踪)
-        /// </summary>
-        /// <returns>还未执行的SQL语句</returns>
-        IQueryable<T> GetAllNoTracking();
-
-        /// <summary>
-        /// 获取所有实体
-        /// </summary>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <returns>还未执行的SQL语句</returns>
-        IQueryable<TDto> GetAll<TDto>() where TDto : class;
-
-        /// <summary>
-        /// 获取所有实体
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IOrderedQueryable<T> GetAll<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 获取所有实体(不跟踪)
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IOrderedQueryable<T> GetAllNoTracking<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 从二级缓存获取所有实体
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns></returns>
-        PooledList<T> GetAllFromCache<TS>(Expression<Func<T, TS>> orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 获取所有实体Dto
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IQueryable<TDto> GetAll<TS, TDto>(Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IQueryable<T> GetQuery(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IOrderedQueryable<T> GetQuery<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 基本查询方法,获取一个被AutoMapper映射后的集合
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IQueryable<TDto> GetQuery<TDto>(Expression<Func<T, bool>> @where) where TDto : class;
-
-        /// <summary>
-        /// 基本查询方法,获取一个被AutoMapper映射后的集合
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <typeparam name="TDto">输出类型</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        /// <returns></returns>
-        IQueryable<TDto> GetQuery<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合,优先从二级缓存读取
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns></returns>
-        IEnumerable<T> GetQueryFromCache(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合,优先从二级缓存读取
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序方式</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns></returns>
-        IEnumerable<T> GetQueryFromCache<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 基本查询方法,获取一个被AutoMapper映射后的集合,优先从二级缓存读取
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <typeparam name="TDto">输出类型</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序方式</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns></returns>
-        PooledList<TDto> GetQueryFromCache<TS, TDto>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class;
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合(不跟踪实体)
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IQueryable<T> GetQueryNoTracking(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 基本查询方法,获取一个集合(不跟踪实体)
-        /// </summary>
-        /// <typeparam name="TS">排序字段</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序方式</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>还未执行的SQL语句</returns>
-        IOrderedQueryable<T> GetQueryNoTracking<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        T Get(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 从二级缓存获取第一条数据
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        Task<T> GetFromCacheAsync(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>实体</returns>
-        T Get<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 获取第一条被AutoMapper映射后的数据
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        TDto Get<TDto>(Expression<Func<T, bool>> @where) where TDto : class;
-
-        /// <summary>
-        /// 获取第一条被AutoMapper映射后的数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>映射实体</returns>
-        TDto Get<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
-
-        /// <summary>
-        /// 获取第一条被AutoMapper映射后的数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>映射实体</returns>
-        Task<TDto> GetAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
-
-        /// <summary>
-        /// 从二级缓存获取第一条被AutoMapper映射后的数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <typeparam name="TDto">映射实体</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>映射实体</returns>
-        Task<TDto> GetFromCacheAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        Task<T> GetAsync(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 获取第一条数据
-        /// </summary>
-        /// <typeparam name="TS">排序</typeparam>
-        /// <param name="where">查询条件</param>
-        /// <param name="orderby">排序字段</param>
-        /// <param name="isAsc">是否升序</param>
-        /// <returns>实体</returns>
-        Task<T> GetAsync<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 获取第一条数据(不跟踪实体)
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>实体</returns>
-        T GetNoTracking(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 根据ID找实体
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>实体</returns>
-        T GetById(int id);
-
-        /// <summary>
-        /// 根据ID找实体(异步)
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>实体</returns>
-        Task<T> GetByIdAsync(int id);
-
-        /// <summary>
-        /// 标准分页查询方法
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        PagedList<T> GetPages<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc);
-
-        /// <summary>
-        /// 标准分页查询方法,取出被AutoMapper映射后的数据集合
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <typeparam name="TDto"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        PagedList<TDto> GetPages<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc) where TDto : class;
-
-        /// <summary>
-        /// 标准分页查询方法
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        Task<PagedList<T>> GetPagesAsync<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc);
-
-        /// <summary>
-        /// 标准分页查询方法,取出被AutoMapper映射后的数据集合
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <typeparam name="TDto"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        Task<PagedList<TDto>> GetPagesAsync<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc) where TDto : class;
-
-        /// <summary>
-        /// 标准分页查询方法,优先从二级缓存读取,取出被AutoMapper映射后的数据集合
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <typeparam name="TDto"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        PagedList<TDto> GetPagesFromCache<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc) where TDto : class;
-
-        /// <summary>
-        /// 标准分页查询方法(不跟踪实体)
-        /// </summary>
-        /// <typeparam name="TS"></typeparam>
-        /// <param name="pageIndex">第几页</param>
-        /// <param name="pageSize">每页大小</param>
-        /// <param name="where">where Lambda条件表达式</param>
-        /// <param name="orderby">orderby Lambda条件表达式</param>
-        /// <param name="isAsc">升序降序</param>
-        /// <returns></returns>
-        PagedList<T> GetPagesNoTracking<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
-
-        /// <summary>
-        /// 根据ID删除实体
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>删除成功</returns>
-        bool DeleteById(int id);
-
-        /// <summary>
-        /// 根据ID删除实体并保存(异步)
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>删除成功</returns>
-        Task<int> DeleteByIdAsync(int id);
-
-        /// <summary>
-        /// 删除实体并保存
-        /// </summary>
-        /// <param name="t">需要删除的实体</param>
-        /// <returns>删除成功</returns>
-        bool DeleteEntity(T t);
-
-        /// <summary>
-        /// 根据条件删除实体
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>删除成功</returns>
-        int DeleteEntity(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 根据条件删除实体
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>删除成功</returns>
-        int DeleteEntitySaved(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 根据条件删除实体
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>删除成功</returns>
-        Task<int> DeleteEntitySavedAsync(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 删除实体并保存
-        /// </summary>
-        /// <param name="t">需要删除的实体</param>
-        /// <returns>删除成功</returns>
-        bool DeleteEntitySaved(T t);
-
-        /// <summary>
-        /// 添加实体
-        /// </summary>
-        /// <param name="t">需要添加的实体</param>
-        /// <returns>添加成功</returns>
-        T AddEntity(T t);
-
-        /// <summary>
-        /// 添加或更新实体
-        /// </summary>
-        /// <param name="key">更新键规则</param>
-        /// <param name="entities">需要保存的实体</param>
-        /// <returns>保存成功</returns>
-        void AddOrUpdate<TKey>(Expression<Func<T, TKey>> key, IEnumerable<T> entities);
-
-        /// <summary>
-        /// 添加实体并保存
-        /// </summary>
-        /// <param name="t">需要添加的实体</param>
-        /// <returns>添加成功</returns>
-        T AddEntitySaved(T t);
-
-        /// <summary>
-        /// 添加或更新实体
-        /// </summary>
-        /// <param name="key">更新键规则</param>
-        /// <param name="t">需要保存的实体</param>
-        /// <returns>保存成功</returns>
-        Task<int> AddOrUpdateSavedAsync<TKey>(Expression<Func<T, TKey>> key, T t);
-
-        /// <summary>
-        /// 添加或更新实体
-        /// </summary>
-        /// <param name="key">更新键规则</param>
-        /// <param name="entities">需要保存的实体</param>
-        /// <returns>保存成功</returns>
-        Task<int> AddOrUpdateSavedAsync<TKey>(Expression<Func<T, TKey>> key, IEnumerable<T> entities);
-
-        /// <summary>
-        /// 添加实体并保存(异步)
-        /// </summary>
-        /// <param name="t">需要添加的实体</param>
-        /// <returns>添加成功</returns>
-        Task<int> AddEntitySavedAsync(T t);
-
-        /// <summary>
-        /// 统一保存的方法
-        /// </summary>
-        /// <returns>受影响的行数</returns>
-        int SaveChanges();
-
-        /// <summary>
-        /// 统一保存数据
-        /// </summary>
-        /// <returns>受影响的行数</returns>
-        Task<int> SaveChangesAsync();
-
-        /// <summary>
-        /// 判断实体是否在数据库中存在
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>是否存在</returns>
-        bool Any(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 统计符合条件的个数
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns></returns>
-        int Count(Expression<Func<T, bool>> @where);
-
-        /// <summary>
-        /// 删除多个实体并保存(异步)
-        /// </summary>
-        /// <param name="list">实体集合</param>
-        /// <returns>删除成功</returns>
-        Task<int> DeleteEntitiesSavedAsync(IEnumerable<T> list);
-
-        T this[int id] => GetById(id);
-
-        string this[int id, Expression<Func<T, string>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
-
-        int this[int id, Expression<Func<T, int>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
-
-        DateTime this[int id, Expression<Func<T, DateTime>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
-
-        long this[int id, Expression<Func<T, long>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
-
-        decimal this[int id, Expression<Func<T, decimal>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
-
-        PooledList<T> this[Expression<Func<T, bool>> where] => GetQuery(where).ToPooledList();
-
-        public static T operator +(IBaseService<T> left, T right) => left.AddEntitySaved(right);
-
-        public static bool operator -(IBaseService<T> left, T right) => left.DeleteEntitySaved(right);
-
-        public static bool operator -(IBaseService<T> left, int id) => left.DeleteById(id);
-    }
+	public interface IBaseService<T> where T : LuceneIndexableBaseEntity
+	{
+		/// <summary>
+		/// 获取所有实体
+		/// </summary>
+		/// <returns>还未执行的SQL语句</returns>
+		IQueryable<T> GetAll();
+
+		/// <summary>
+		/// 获取所有实体(不跟踪)
+		/// </summary>
+		/// <returns>还未执行的SQL语句</returns>
+		IQueryable<T> GetAllNoTracking();
+
+		/// <summary>
+		/// 获取所有实体
+		/// </summary>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <returns>还未执行的SQL语句</returns>
+		IQueryable<TDto> GetAll<TDto>() where TDto : class;
+
+		/// <summary>
+		/// 获取所有实体
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IOrderedQueryable<T> GetAll<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 获取所有实体(不跟踪)
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IOrderedQueryable<T> GetAllNoTracking<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 从二级缓存获取所有实体
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns></returns>
+		List<T> GetAllFromCache<TS>(Expression<Func<T, TS>> orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 获取所有实体Dto
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IQueryable<TDto> GetAll<TS, TDto>(Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IQueryable<T> GetQuery(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IOrderedQueryable<T> GetQuery<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 基本查询方法,获取一个被AutoMapper映射后的集合
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IQueryable<TDto> GetQuery<TDto>(Expression<Func<T, bool>> @where) where TDto : class;
+
+		/// <summary>
+		/// 基本查询方法,获取一个被AutoMapper映射后的集合
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <typeparam name="TDto">输出类型</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		/// <returns></returns>
+		IQueryable<TDto> GetQuery<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合,优先从二级缓存读取
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns></returns>
+		IEnumerable<T> GetQueryFromCache(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合,优先从二级缓存读取
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序方式</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns></returns>
+		IEnumerable<T> GetQueryFromCache<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 基本查询方法,获取一个被AutoMapper映射后的集合,优先从二级缓存读取
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <typeparam name="TDto">输出类型</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序方式</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns></returns>
+		List<TDto> GetQueryFromCache<TS, TDto>(Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc = true) where TDto : class;
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合(不跟踪实体)
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IQueryable<T> GetQueryNoTracking(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 基本查询方法,获取一个集合(不跟踪实体)
+		/// </summary>
+		/// <typeparam name="TS">排序字段</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序方式</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>还未执行的SQL语句</returns>
+		IOrderedQueryable<T> GetQueryNoTracking<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		T Get(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 从二级缓存获取第一条数据
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		Task<T> GetFromCacheAsync(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>实体</returns>
+		T Get<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 获取第一条被AutoMapper映射后的数据
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		TDto Get<TDto>(Expression<Func<T, bool>> @where) where TDto : class;
+
+		/// <summary>
+		/// 获取第一条被AutoMapper映射后的数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>映射实体</returns>
+		TDto Get<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
+
+		/// <summary>
+		/// 获取第一条被AutoMapper映射后的数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>映射实体</returns>
+		Task<TDto> GetAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
+
+		/// <summary>
+		/// 从二级缓存获取第一条被AutoMapper映射后的数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <typeparam name="TDto">映射实体</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>映射实体</returns>
+		Task<TDto> GetFromCacheAsync<TS, TDto>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true) where TDto : class;
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		Task<T> GetAsync(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 获取第一条数据
+		/// </summary>
+		/// <typeparam name="TS">排序</typeparam>
+		/// <param name="where">查询条件</param>
+		/// <param name="orderby">排序字段</param>
+		/// <param name="isAsc">是否升序</param>
+		/// <returns>实体</returns>
+		Task<T> GetAsync<TS>(Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 获取第一条数据(不跟踪实体)
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>实体</returns>
+		T GetNoTracking(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 根据ID找实体
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>实体</returns>
+		T GetById(int id);
+
+		/// <summary>
+		/// 根据ID找实体(异步)
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>实体</returns>
+		Task<T> GetByIdAsync(int id);
+
+		/// <summary>
+		/// 标准分页查询方法
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		PagedList<T> GetPages<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc);
+
+		/// <summary>
+		/// 标准分页查询方法,取出被AutoMapper映射后的数据集合
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <typeparam name="TDto"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		PagedList<TDto> GetPages<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc) where TDto : class;
+
+		/// <summary>
+		/// 标准分页查询方法
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		Task<PagedList<T>> GetPagesAsync<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc);
+
+		/// <summary>
+		/// 标准分页查询方法,取出被AutoMapper映射后的数据集合
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <typeparam name="TDto"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		Task<PagedList<TDto>> GetPagesAsync<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> where, Expression<Func<T, TS>> orderby, bool isAsc) where TDto : class;
+
+		/// <summary>
+		/// 标准分页查询方法,优先从二级缓存读取,取出被AutoMapper映射后的数据集合
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <typeparam name="TDto"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		PagedList<TDto> GetPagesFromCache<TS, TDto>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc) where TDto : class;
+
+		/// <summary>
+		/// 标准分页查询方法(不跟踪实体)
+		/// </summary>
+		/// <typeparam name="TS"></typeparam>
+		/// <param name="pageIndex">第几页</param>
+		/// <param name="pageSize">每页大小</param>
+		/// <param name="where">where Lambda条件表达式</param>
+		/// <param name="orderby">orderby Lambda条件表达式</param>
+		/// <param name="isAsc">升序降序</param>
+		/// <returns></returns>
+		PagedList<T> GetPagesNoTracking<TS>(int pageIndex, int pageSize, Expression<Func<T, bool>> @where, Expression<Func<T, TS>> @orderby, bool isAsc = true);
+
+		/// <summary>
+		/// 根据ID删除实体
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>删除成功</returns>
+		bool DeleteById(int id);
+
+		/// <summary>
+		/// 根据ID删除实体并保存(异步)
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>删除成功</returns>
+		Task<int> DeleteByIdAsync(int id);
+
+		/// <summary>
+		/// 删除实体并保存
+		/// </summary>
+		/// <param name="t">需要删除的实体</param>
+		/// <returns>删除成功</returns>
+		bool DeleteEntity(T t);
+
+		/// <summary>
+		/// 根据条件删除实体
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>删除成功</returns>
+		int DeleteEntity(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 根据条件删除实体
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>删除成功</returns>
+		int DeleteEntitySaved(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 根据条件删除实体
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>删除成功</returns>
+		Task<int> DeleteEntitySavedAsync(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 删除实体并保存
+		/// </summary>
+		/// <param name="t">需要删除的实体</param>
+		/// <returns>删除成功</returns>
+		bool DeleteEntitySaved(T t);
+
+		/// <summary>
+		/// 添加实体
+		/// </summary>
+		/// <param name="t">需要添加的实体</param>
+		/// <returns>添加成功</returns>
+		T AddEntity(T t);
+
+		/// <summary>
+		/// 添加或更新实体
+		/// </summary>
+		/// <param name="key">更新键规则</param>
+		/// <param name="entities">需要保存的实体</param>
+		/// <returns>保存成功</returns>
+		void AddOrUpdate<TKey>(Expression<Func<T, TKey>> key, IEnumerable<T> entities);
+
+		/// <summary>
+		/// 添加实体并保存
+		/// </summary>
+		/// <param name="t">需要添加的实体</param>
+		/// <returns>添加成功</returns>
+		T AddEntitySaved(T t);
+
+		/// <summary>
+		/// 添加或更新实体
+		/// </summary>
+		/// <param name="key">更新键规则</param>
+		/// <param name="t">需要保存的实体</param>
+		/// <returns>保存成功</returns>
+		Task<int> AddOrUpdateSavedAsync<TKey>(Expression<Func<T, TKey>> key, T t);
+
+		/// <summary>
+		/// 添加或更新实体
+		/// </summary>
+		/// <param name="key">更新键规则</param>
+		/// <param name="entities">需要保存的实体</param>
+		/// <returns>保存成功</returns>
+		Task<int> AddOrUpdateSavedAsync<TKey>(Expression<Func<T, TKey>> key, IEnumerable<T> entities);
+
+		/// <summary>
+		/// 添加实体并保存(异步)
+		/// </summary>
+		/// <param name="t">需要添加的实体</param>
+		/// <returns>添加成功</returns>
+		Task<int> AddEntitySavedAsync(T t);
+
+		/// <summary>
+		/// 统一保存的方法
+		/// </summary>
+		/// <returns>受影响的行数</returns>
+		int SaveChanges();
+
+		/// <summary>
+		/// 统一保存数据
+		/// </summary>
+		/// <returns>受影响的行数</returns>
+		Task<int> SaveChangesAsync();
+
+		/// <summary>
+		/// 判断实体是否在数据库中存在
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>是否存在</returns>
+		bool Any(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 统计符合条件的个数
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns></returns>
+		int Count(Expression<Func<T, bool>> @where);
+
+		/// <summary>
+		/// 删除多个实体并保存(异步)
+		/// </summary>
+		/// <param name="list">实体集合</param>
+		/// <returns>删除成功</returns>
+		Task<int> DeleteEntitiesSavedAsync(IEnumerable<T> list);
+
+		T this[int id] => GetById(id);
+
+		string this[int id, Expression<Func<T, string>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
+
+		int this[int id, Expression<Func<T, int>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
+
+		DateTime this[int id, Expression<Func<T, DateTime>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
+
+		long this[int id, Expression<Func<T, long>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
+
+		decimal this[int id, Expression<Func<T, decimal>> selector] => GetQuery(t => t.Id == id).Select(selector).FirstOrDefault();
+
+		List<T> this[Expression<Func<T, bool>> where] => GetQuery(where).ToList();
+
+		public static T operator +(IBaseService<T> left, T right) => left.AddEntitySaved(right);
+
+		public static bool operator -(IBaseService<T> left, T right) => left.DeleteEntitySaved(right);
+
+		public static bool operator -(IBaseService<T> left, int id) => left.DeleteById(id);
+	}
 }

+ 259 - 260
src/Masuit.MyBlogs.Core/Infrastructure/Services/PostService.cs

@@ -3,7 +3,6 @@ using AngleSharp.Dom;
 using AngleSharp.Html.Parser;
 using AutoMapper;
 using CacheManager.Core;
-using Collections.Pooled;
 using Masuit.LuceneEFCore.SearchEngine;
 using Masuit.LuceneEFCore.SearchEngine.Interfaces;
 using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
@@ -24,286 +23,286 @@ using Z.EntityFramework.Plus;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Services
 {
-    public class PostService : BaseService<Post>, IPostService
-    {
-        private readonly ICacheManager<SearchResult<PostDto>> _cacheManager;
-        private readonly ICategoryRepository _categoryRepository;
-        private readonly IMapper _mapper;
-        private readonly IPostTagsRepository _postTagsRepository;
+	public class PostService : BaseService<Post>, IPostService
+	{
+		private readonly ICacheManager<SearchResult<PostDto>> _cacheManager;
+		private readonly ICategoryRepository _categoryRepository;
+		private readonly IMapper _mapper;
+		private readonly IPostTagsRepository _postTagsRepository;
 
-        public PostService(IPostRepository repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher, ICacheManager<SearchResult<PostDto>> cacheManager, ICategoryRepository categoryRepository, IMapper mapper, IPostTagsRepository postTagsRepository) : base(repository, searchEngine, searcher)
-        {
-            _cacheManager = cacheManager;
-            _categoryRepository = categoryRepository;
-            _mapper = mapper;
-            _postTagsRepository = postTagsRepository;
-        }
+		public PostService(IPostRepository repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher, ICacheManager<SearchResult<PostDto>> cacheManager, ICategoryRepository categoryRepository, IMapper mapper, IPostTagsRepository postTagsRepository) : base(repository, searchEngine, searcher)
+		{
+			_cacheManager = cacheManager;
+			_categoryRepository = categoryRepository;
+			_mapper = mapper;
+			_postTagsRepository = postTagsRepository;
+		}
 
-        /// <summary>
-        /// 文章高亮关键词处理
-        /// </summary>
-        /// <param name="p"></param>
-        /// <param name="keyword"></param>
-        public async Task Highlight(Post p, string keyword)
-        {
-            try
-            {
-                var simpleHtmlFormatter = new SimpleHTMLFormatter("<span style='color:red;background-color:yellow;font-size: 1.1em;font-weight:700;'>", "</span>");
-                var highlighter = new Highlighter(simpleHtmlFormatter, new Segment()) { FragmentSize = int.MaxValue };
-                keyword = Regex.Replace(keyword, @"<|>|\(|\)|\{|\}|\[|\]", " ");
-                var keywords = Searcher.CutKeywords(keyword);
-                var context = BrowsingContext.New(Configuration.Default);
-                var document = await context.OpenAsync(req => req.Content(p.Content));
-                var elements = document.DocumentElement.GetElementsByTagName("p");
-                foreach (var e in elements)
-                {
-                    for (var index = 0; index < e.ChildNodes.Length; index++)
-                    {
-                        var node = e.ChildNodes[index];
-                        bool handled = false;
-                        foreach (var s in keywords)
-                        {
-                            string frag;
-                            if (handled == false && node.TextContent.Contains(s, StringComparison.CurrentCultureIgnoreCase) && !string.IsNullOrEmpty(frag = highlighter.GetBestFragment(s, node.TextContent)))
-                            {
-                                switch (node)
-                                {
-                                    case IElement el:
-                                        el.InnerHtml = frag;
-                                        handled = true;
-                                        break;
+		/// <summary>
+		/// 文章高亮关键词处理
+		/// </summary>
+		/// <param name="p"></param>
+		/// <param name="keyword"></param>
+		public async Task Highlight(Post p, string keyword)
+		{
+			try
+			{
+				var simpleHtmlFormatter = new SimpleHTMLFormatter("<span style='color:red;background-color:yellow;font-size: 1.1em;font-weight:700;'>", "</span>");
+				var highlighter = new Highlighter(simpleHtmlFormatter, new Segment()) { FragmentSize = int.MaxValue };
+				keyword = Regex.Replace(keyword, @"<|>|\(|\)|\{|\}|\[|\]", " ");
+				var keywords = Searcher.CutKeywords(keyword);
+				var context = BrowsingContext.New(Configuration.Default);
+				var document = await context.OpenAsync(req => req.Content(p.Content));
+				var elements = document.DocumentElement.GetElementsByTagName("p");
+				foreach (var e in elements)
+				{
+					for (var index = 0; index < e.ChildNodes.Length; index++)
+					{
+						var node = e.ChildNodes[index];
+						bool handled = false;
+						foreach (var s in keywords)
+						{
+							string frag;
+							if (handled == false && node.TextContent.Contains(s, StringComparison.CurrentCultureIgnoreCase) && !string.IsNullOrEmpty(frag = highlighter.GetBestFragment(s, node.TextContent)))
+							{
+								switch (node)
+								{
+									case IElement el:
+										el.InnerHtml = frag;
+										handled = true;
+										break;
 
-                                    case IText t:
-                                        var parser = new HtmlParser();
-                                        var parseDoc = parser.ParseDocument(frag).Body;
-                                        e.ReplaceChild(parseDoc, t);
-                                        handled = true;
-                                        break;
-                                }
-                            }
-                        }
-                    }
-                }
-                p.Content = document.Body.InnerHtml;
-            }
-            catch (Exception e)
-            {
-                Console.WriteLine(e);
-            }
-        }
+									case IText t:
+										var parser = new HtmlParser();
+										var parseDoc = parser.ParseDocument(frag).Body;
+										e.ReplaceChild(parseDoc, t);
+										handled = true;
+										break;
+								}
+							}
+						}
+					}
+				}
+				p.Content = document.Body.InnerHtml;
+			}
+			catch (Exception e)
+			{
+				Console.WriteLine(e);
+			}
+		}
 
-        public SearchResult<PostDto> SearchPage(int page, int size, string keyword)
-        {
-            var cacheKey = $"search:{keyword}:{page}:{size}";
-            var result = _cacheManager.GetOrAdd(cacheKey, _ =>
-            {
-                var searchResult = SearchEngine.ScoredSearch<Post>(BuildSearchOptions(page, size, keyword));
-                using var entities = searchResult.Results.Where(s => s.Entity.Status == Status.Published).DistinctBy(s => s.Entity.Id).ToPooledList();
-                var ids = entities.Select(s => s.Entity.Id).ToArray();
-                var dic = GetQuery<PostDto>(p => ids.Contains(p.Id) && p.LimitMode != RegionLimitMode.OnlyForSearchEngine).ToDictionary(p => p.Id);
-                var posts = entities.Where(s => dic.ContainsKey(s.Entity.Id)).Select(s => dic[s.Entity.Id]).ToList();
-                var simpleHtmlFormatter = new SimpleHTMLFormatter("<span style='color:red;background-color:yellow;font-size: 1.1em;font-weight:700;'>", "</span>");
-                var highlighter = new Highlighter(simpleHtmlFormatter, new Segment()) { FragmentSize = 200 };
-                var keywords = Searcher.CutKeywords(keyword);
-                HighlightSegment(posts, keywords, highlighter);
-                SolvePostsCategory(posts);
-                return new SearchResult<PostDto>()
-                {
-                    Results = posts,
-                    Elapsed = searchResult.Elapsed,
-                    Total = searchResult.TotalHits
-                };
-            });
-            return result;
-        }
+		public SearchResult<PostDto> SearchPage(int page, int size, string keyword)
+		{
+			var cacheKey = $"search:{keyword}:{page}:{size}";
+			var result = _cacheManager.GetOrAdd(cacheKey, _ =>
+			{
+				var searchResult = SearchEngine.ScoredSearch<Post>(BuildSearchOptions(page, size, keyword));
+				var entities = searchResult.Results.Where(s => s.Entity.Status == Status.Published).DistinctBy(s => s.Entity.Id).ToList();
+				var ids = entities.Select(s => s.Entity.Id).ToArray();
+				var dic = GetQuery<PostDto>(p => ids.Contains(p.Id) && p.LimitMode != RegionLimitMode.OnlyForSearchEngine).ToDictionary(p => p.Id);
+				var posts = entities.Where(s => dic.ContainsKey(s.Entity.Id)).Select(s => dic[s.Entity.Id]).ToList();
+				var simpleHtmlFormatter = new SimpleHTMLFormatter("<span style='color:red;background-color:yellow;font-size: 1.1em;font-weight:700;'>", "</span>");
+				var highlighter = new Highlighter(simpleHtmlFormatter, new Segment()) { FragmentSize = 200 };
+				var keywords = Searcher.CutKeywords(keyword);
+				HighlightSegment(posts, keywords, highlighter);
+				SolvePostsCategory(posts);
+				return new SearchResult<PostDto>()
+				{
+					Results = posts,
+					Elapsed = searchResult.Elapsed,
+					Total = searchResult.TotalHits
+				};
+			});
+			return result;
+		}
 
-        public void SolvePostsCategory(IList<PostDto> posts)
-        {
-            var cids = posts.Select(p => p.CategoryId).Distinct().ToArray();
-            var categories = _categoryRepository.GetQuery(c => cids.Contains(c.Id)).Include(c => c.Parent).ToDictionary(c => c.Id);
-            posts.ForEach(p => p.Category = _mapper.Map<CategoryDto_P>(categories[p.CategoryId]));
-        }
+		public void SolvePostsCategory(IList<PostDto> posts)
+		{
+			var cids = posts.Select(p => p.CategoryId).Distinct().ToArray();
+			var categories = _categoryRepository.GetQuery(c => cids.Contains(c.Id)).Include(c => c.Parent).ToDictionary(c => c.Id);
+			posts.ForEach(p => p.Category = _mapper.Map<CategoryDto_P>(categories[p.CategoryId]));
+		}
 
-        /// <summary>
-        /// 高亮截取处理
-        /// </summary>
-        /// <param name="posts"></param>
-        /// <param name="keywords"></param>
-        /// <param name="highlighter"></param>
-        private static void HighlightSegment(IList<PostDto> posts, List<string> keywords, Highlighter highlighter)
-        {
-            foreach (var p in posts)
-            {
-                p.Content = p.Content.RemoveHtmlTag();
-                foreach (var s in keywords)
-                {
-                    string frag;
-                    if (p.Title.Contains(s) && !string.IsNullOrEmpty(frag = highlighter.GetBestFragment(s, p.Title)))
-                    {
-                        p.Title = frag;
-                        break;
-                    }
-                }
+		/// <summary>
+		/// 高亮截取处理
+		/// </summary>
+		/// <param name="posts"></param>
+		/// <param name="keywords"></param>
+		/// <param name="highlighter"></param>
+		private static void HighlightSegment(IList<PostDto> posts, List<string> keywords, Highlighter highlighter)
+		{
+			foreach (var p in posts)
+			{
+				p.Content = p.Content.RemoveHtmlTag();
+				foreach (var s in keywords)
+				{
+					string frag;
+					if (p.Title.Contains(s) && !string.IsNullOrEmpty(frag = highlighter.GetBestFragment(s, p.Title)))
+					{
+						p.Title = frag;
+						break;
+					}
+				}
 
-                bool handled = false;
-                foreach (var s in keywords)
-                {
-                    string frag;
-                    if (p.Content.Contains(s) && !string.IsNullOrEmpty(frag = highlighter.GetBestFragment(s, p.Content)))
-                    {
-                        p.Content = frag;
-                        handled = true;
-                        break;
-                    }
-                }
+				bool handled = false;
+				foreach (var s in keywords)
+				{
+					string frag;
+					if (p.Content.Contains(s) && !string.IsNullOrEmpty(frag = highlighter.GetBestFragment(s, p.Content)))
+					{
+						p.Content = frag;
+						handled = true;
+						break;
+					}
+				}
 
-                if (p.Content.Length > 200 && !handled)
-                {
-                    p.Content = p.Content[..200];
-                }
-            }
-        }
+				if (p.Content.Length > 200 && !handled)
+				{
+					p.Content = p.Content[..200];
+				}
+			}
+		}
 
-        private static SearchOptions BuildSearchOptions(int page, int size, string keyword)
-        {
-            keyword = Regex.Replace(keyword, @":\s+", ":");
-            var fields = new List<string>();
-            var newkeywords = new List<string>();
-            foreach (var item in keyword.Split(' ', ' ').Where(s => s.Contains(new[] { ":", ":" })))
-            {
-                var part = item.Split(':', ':');
-                var field = typeof(Post).GetProperty(part[0], BindingFlags.IgnoreCase)?.Name;
-                if (!string.IsNullOrEmpty(field))
-                {
-                    fields.Add(field);
-                }
+		private static SearchOptions BuildSearchOptions(int page, int size, string keyword)
+		{
+			keyword = Regex.Replace(keyword, @":\s+", ":");
+			var fields = new List<string>();
+			var newkeywords = new List<string>();
+			foreach (var item in keyword.Split(' ', ' ').Where(s => s.Contains(new[] { ":", ":" })))
+			{
+				var part = item.Split(':', ':');
+				var field = typeof(Post).GetProperty(part[0], BindingFlags.IgnoreCase)?.Name;
+				if (!string.IsNullOrEmpty(field))
+				{
+					fields.Add(field);
+				}
 
-                newkeywords.Add(part[1]);
-            }
+				newkeywords.Add(part[1]);
+			}
 
-            var searchOptions = fields.Any() ? new SearchOptions(newkeywords.Join(" "), page, size, fields.Join(",")) : new SearchOptions(keyword, page, size, typeof(Post));
-            if (keyword.Contains(new[] { " ", ",", ";" }))
-            {
-                searchOptions.Score = 0.3f;
-            }
+			var searchOptions = fields.Any() ? new SearchOptions(newkeywords.Join(" "), page, size, fields.Join(",")) : new SearchOptions(keyword, page, size, typeof(Post));
+			if (keyword.Contains(new[] { " ", ",", ";" }))
+			{
+				searchOptions.Score = 0.3f;
+			}
 
-            return searchOptions;
-        }
+			return searchOptions;
+		}
 
-        /// <summary>
-        /// 文章所有tag
-        /// </summary>
-        /// <returns></returns>
-        public Dictionary<string, int> GetTags()
-        {
-            return _postTagsRepository.GetAll(t => t.Count, false).FromCache().ToDictionary(g => g.Name, g => g.Count);
-        }
+		/// <summary>
+		/// 文章所有tag
+		/// </summary>
+		/// <returns></returns>
+		public Dictionary<string, int> GetTags()
+		{
+			return _postTagsRepository.GetAll(t => t.Count, false).FromCache().ToDictionary(g => g.Name, g => g.Count);
+		}
 
-        /// <summary>
-        /// 添加实体并保存
-        /// </summary>
-        /// <param name="t">需要添加的实体</param>
-        /// <returns>添加成功</returns>
-        public override Post AddEntitySaved(Post t)
-        {
-            t = base.AddEntity(t);
-            SearchEngine.SaveChanges(t.Status == Status.Published);
-            return t;
-        }
+		/// <summary>
+		/// 添加实体并保存
+		/// </summary>
+		/// <param name="t">需要添加的实体</param>
+		/// <returns>添加成功</returns>
+		public override Post AddEntitySaved(Post t)
+		{
+			t = base.AddEntity(t);
+			SearchEngine.SaveChanges(t.Status == Status.Published);
+			return t;
+		}
 
-        /// <summary>
-        /// 添加实体并保存(异步)
-        /// </summary>
-        /// <param name="t">需要添加的实体</param>
-        /// <returns>添加成功</returns>
-        public override Task<int> AddEntitySavedAsync(Post t)
-        {
-            base.AddEntity(t);
-            return SearchEngine.SaveChangesAsync(t.Status == Status.Published);
-        }
+		/// <summary>
+		/// 添加实体并保存(异步)
+		/// </summary>
+		/// <param name="t">需要添加的实体</param>
+		/// <returns>添加成功</returns>
+		public override Task<int> AddEntitySavedAsync(Post t)
+		{
+			base.AddEntity(t);
+			return SearchEngine.SaveChangesAsync(t.Status == Status.Published);
+		}
 
-        /// <summary>
-        /// 根据ID删除实体并保存
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>删除成功</returns>
-        public override bool DeleteById(int id)
-        {
-            DeleteEntity(GetById(id));
-            return SearchEngine.SaveChanges() > 0;
-        }
+		/// <summary>
+		/// 根据ID删除实体并保存
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>删除成功</returns>
+		public override bool DeleteById(int id)
+		{
+			DeleteEntity(GetById(id));
+			return SearchEngine.SaveChanges() > 0;
+		}
 
-        /// <summary>
-        /// 根据ID删除实体并保存(异步)
-        /// </summary>
-        /// <param name="id">实体id</param>
-        /// <returns>删除成功</returns>
-        public override Task<int> DeleteByIdAsync(int id)
-        {
-            base.DeleteById(id);
-            return SearchEngine.SaveChangesAsync();
-        }
+		/// <summary>
+		/// 根据ID删除实体并保存(异步)
+		/// </summary>
+		/// <param name="id">实体id</param>
+		/// <returns>删除成功</returns>
+		public override Task<int> DeleteByIdAsync(int id)
+		{
+			base.DeleteById(id);
+			return SearchEngine.SaveChangesAsync();
+		}
 
-        /// <summary>
-        /// 删除多个实体并保存(异步)
-        /// </summary>
-        /// <param name="list">实体集合</param>
-        /// <returns>删除成功</returns>
-        public override Task<int> DeleteEntitiesSavedAsync(IEnumerable<Post> list)
-        {
-            base.DeleteEntities(list);
-            return SearchEngine.SaveChangesAsync();
-        }
+		/// <summary>
+		/// 删除多个实体并保存(异步)
+		/// </summary>
+		/// <param name="list">实体集合</param>
+		/// <returns>删除成功</returns>
+		public override Task<int> DeleteEntitiesSavedAsync(IEnumerable<Post> list)
+		{
+			base.DeleteEntities(list);
+			return SearchEngine.SaveChangesAsync();
+		}
 
-        /// <summary>
-        /// 根据条件删除实体
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>删除成功</returns>
-        public override int DeleteEntitySaved(Expression<Func<Post, bool>> where)
-        {
-            base.DeleteEntity(where);
-            return SearchEngine.SaveChanges();
-        }
+		/// <summary>
+		/// 根据条件删除实体
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>删除成功</returns>
+		public override int DeleteEntitySaved(Expression<Func<Post, bool>> where)
+		{
+			base.DeleteEntity(where);
+			return SearchEngine.SaveChanges();
+		}
 
-        /// <summary>
-        /// 删除实体并保存
-        /// </summary>
-        /// <param name="t">需要删除的实体</param>
-        /// <returns>删除成功</returns>
-        public override bool DeleteEntitySaved(Post t)
-        {
-            base.DeleteEntity(t);
-            return SearchEngine.SaveChanges() > 0;
-        }
+		/// <summary>
+		/// 删除实体并保存
+		/// </summary>
+		/// <param name="t">需要删除的实体</param>
+		/// <returns>删除成功</returns>
+		public override bool DeleteEntitySaved(Post t)
+		{
+			base.DeleteEntity(t);
+			return SearchEngine.SaveChanges() > 0;
+		}
 
-        /// <summary>
-        /// 根据条件删除实体
-        /// </summary>
-        /// <param name="where">查询条件</param>
-        /// <returns>删除成功</returns>
-        public override Task<int> DeleteEntitySavedAsync(Expression<Func<Post, bool>> where)
-        {
-            base.DeleteEntity(where);
-            return SearchEngine.SaveChangesAsync();
-        }
+		/// <summary>
+		/// 根据条件删除实体
+		/// </summary>
+		/// <param name="where">查询条件</param>
+		/// <returns>删除成功</returns>
+		public override Task<int> DeleteEntitySavedAsync(Expression<Func<Post, bool>> where)
+		{
+			base.DeleteEntity(where);
+			return SearchEngine.SaveChangesAsync();
+		}
 
-        /// <summary>
-        /// 统一保存的方法
-        /// </summary>
-        /// <returns>受影响的行数</returns>
-        public int SaveChanges(bool flushIndex)
-        {
-            return flushIndex ? SearchEngine.SaveChanges() : base.SaveChanges();
-        }
+		/// <summary>
+		/// 统一保存的方法
+		/// </summary>
+		/// <returns>受影响的行数</returns>
+		public int SaveChanges(bool flushIndex)
+		{
+			return flushIndex ? SearchEngine.SaveChanges() : base.SaveChanges();
+		}
 
-        /// <summary>
-        /// 统一保存数据
-        /// </summary>
-        /// <returns>受影响的行数</returns>
-        public async Task<int> SaveChangesAsync(bool flushIndex)
-        {
-            return flushIndex ? await SearchEngine.SaveChangesAsync() : await base.SaveChangesAsync();
-        }
-    }
+		/// <summary>
+		/// 统一保存数据
+		/// </summary>
+		/// <returns>受影响的行数</returns>
+		public async Task<int> SaveChangesAsync(bool flushIndex)
+		{
+			return flushIndex ? await SearchEngine.SaveChangesAsync() : await base.SaveChangesAsync();
+		}
+	}
 }

+ 0 - 1
src/Masuit.MyBlogs.Core/Masuit.MyBlogs.Core.csproj

@@ -45,7 +45,6 @@
         <PackageReference Include="CacheManager.StackExchange.Redis" Version="1.2.0" />
         <PackageReference Include="CHTCHSConv" Version="1.0.0" />
         <PackageReference Include="CLRStats" Version="1.0.0" />
-        <PackageReference Include="Collections.Pooled" Version="1.0.82" />
         <PackageReference Include="FreeRedis" Version="1.0.3" />
         <PackageReference Include="Hangfire" Version="1.7.31" />
         <PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" />

+ 51 - 65
src/Masuit.MyBlogs.Core/Models/ViewModel/HomePageViewModel.cs

@@ -1,80 +1,66 @@
-using Collections.Pooled;
-using Masuit.MyBlogs.Core.Models.DTO;
-using Masuit.MyBlogs.Core.Models.Entity;
+using Masuit.MyBlogs.Core.Models.DTO;
 using Masuit.Tools.Models;
-using Masuit.Tools.Systems;
 
 namespace Masuit.MyBlogs.Core.Models.ViewModel
 {
-    /// <summary>
-    /// 首页视图模型
-    /// </summary>
-    public class HomePageViewModel : Disposable
-    {
-        /// <summary>
-        /// 文章列表
-        /// </summary>
-        public PagedList<PostDto> Posts { get; set; }
+	/// <summary>
+	/// 首页视图模型
+	/// </summary>
+	public class HomePageViewModel
+	{
+		/// <summary>
+		/// 文章列表
+		/// </summary>
+		public PagedList<PostDto> Posts { get; set; }
 
-        /// <summary>
-        /// 网站公告列表
-        /// </summary>
-        public List<NoticeDto> Notices { get; set; }
+		/// <summary>
+		/// 网站公告列表
+		/// </summary>
+		public List<NoticeDto> Notices { get; set; }
 
-        /// <summary>
-        /// 分类列表
-        /// </summary>
-        public PooledList<CategoryDto_P> Categories { get; set; }
+		/// <summary>
+		/// 分类列表
+		/// </summary>
+		public List<CategoryDto_P> Categories { get; set; }
 
-        /// <summary>
-        /// 标签列表
-        /// </summary>
-        public IDictionary<string, int> Tags { get; set; }
+		/// <summary>
+		/// 标签列表
+		/// </summary>
+		public IDictionary<string, int> Tags { get; set; }
 
-        /// <summary>
-        /// 近期热搜
-        /// </summary>
-        public PooledList<KeywordsRank> HotSearch { get; set; }
+		/// <summary>
+		/// 近期热搜
+		/// </summary>
+		public List<KeywordsRank> HotSearch { get; set; }
 
-        /// <summary>
-        /// 热门文章
-        /// </summary>
-        public PooledList<PostDto> Top5Post { get; set; }
+		/// <summary>
+		/// 热门文章
+		/// </summary>
+		public List<PostDto> Top5Post { get; set; }
 
-        /// <summary>
-        /// 文章列表查询
-        /// </summary>
-        public IQueryable<PostDto> PostsQueryable { get; set; }
+		/// <summary>
+		/// 文章列表查询
+		/// </summary>
+		public IQueryable<PostDto> PostsQueryable { get; set; }
 
-        /// <summary>
-        /// banner文章
-        /// </summary>
-        public List<Advertisement> Banner { get; set; }
+		/// <summary>
+		/// banner文章
+		/// </summary>
+		public List<AdvertisementDto> Banner { get; set; }
 
-        /// <summary>
-        /// 边栏广告
-        /// </summary>
-        public List<Advertisement> SidebarAds { get; set; }
+		/// <summary>
+		/// 边栏广告
+		/// </summary>
+		public List<AdvertisementDto> SidebarAds { get; set; }
 
-        /// <summary>
-        /// 列表内广告
-        /// </summary>
-        public Advertisement ListAdvertisement { get; set; }
+		/// <summary>
+		/// 列表内广告
+		/// </summary>
+		public AdvertisementDto ListAdvertisement { get; set; }
 
-        /// <summary>
-        /// 分页参数
-        /// </summary>
-        public Pagination PageParams { get; set; }
-
-        /// <summary>
-        /// 释放
-        /// </summary>
-        /// <param name="disposing"></param>
-        public override void Dispose(bool disposing)
-        {
-            Categories.Dispose();
-            HotSearch.Dispose();
-            Top5Post.Dispose();
-        }
-    }
+		/// <summary>
+		/// 分页参数
+		/// </summary>
+		public Pagination PageParams { get; set; }
+	}
 }

+ 143 - 146
src/Masuit.MyBlogs.Core/PrepareStartup.cs

@@ -1,6 +1,5 @@
 using Hangfire;
 using Hangfire.Dashboard;
-using JiebaNet.Segmenter;
 using Masuit.LuceneEFCore.SearchEngine;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Configs;
@@ -21,167 +20,165 @@ using Microsoft.Net.Http.Headers;
 using StackExchange.Profiling;
 using System.Text.RegularExpressions;
 using System.Web;
-using Collections.Pooled;
 using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
-using Microsoft.AspNetCore.Mvc;
 
 namespace Masuit.MyBlogs.Core
 {
-    public static class PrepareStartup
-    {
-        /// <summary>
-        /// 初始化系统设置参数
-        /// </summary>
-        /// <param name="app"></param>
-        internal static void InitSettings(this IApplicationBuilder app)
-        {
-            var dic = app.ApplicationServices.GetRequiredService<DataContext>().SystemSetting.ToDictionary(s => s.Name, s => s.Value);
-            CommonHelper.SystemSettings.AddOrUpdate(dic);
-        }
+	public static class PrepareStartup
+	{
+		/// <summary>
+		/// 初始化系统设置参数
+		/// </summary>
+		/// <param name="app"></param>
+		internal static void InitSettings(this IApplicationBuilder app)
+		{
+			var dic = app.ApplicationServices.GetRequiredService<DataContext>().SystemSetting.ToDictionary(s => s.Name, s => s.Value);
+			CommonHelper.SystemSettings.AddOrUpdate(dic);
+		}
 
-        internal static void UseLuceneSearch(this IApplicationBuilder app, IHostEnvironment env, IHangfireBackJob hangfire, LuceneIndexerOptions luceneIndexerOptions)
-        {
-            var are = new AutoResetEvent(false);
-            Task.Run(() =>
-            {
-                Console.WriteLine("正在导入自定义词库...");
-                double time = HiPerfTimer.Execute(() =>
-                {
-                    var db = app.ApplicationServices.GetRequiredService<DataContext>();
-                    using var set = db.Post.Select(p => $"{p.Title},{p.Label},{p.Keyword}").AsParallel().SelectMany(s => Regex.Split(s, @"\p{P}(?<!\.|#)|\p{Z}|\p{S}")).Where(s => s.Length > 1).ToPooledSet();
-                    var lines = File.ReadAllLines(Path.Combine(env.ContentRootPath, "App_Data", "CustomKeywords.txt")).Union(set);
-                    KeywordsManager.AddWords(lines);
-                    KeywordsManager.AddSynonyms(File.ReadAllLines(Path.Combine(env.ContentRootPath, "App_Data", "CustomSynonym.txt")).Where(s => s.Contains(" ")).Select(s =>
-                    {
-                        var arr = Regex.Split(s, "\\s");
-                        return (arr[0], arr[1]);
-                    }));
-                });
-                Console.WriteLine($"导入自定义词库完成,耗时{time}s");
-                Windows.ClearMemorySilent();
-                are.Set();
-            });
+		internal static void UseLuceneSearch(this IApplicationBuilder app, IHostEnvironment env, IHangfireBackJob hangfire, LuceneIndexerOptions luceneIndexerOptions)
+		{
+			var are = new AutoResetEvent(false);
+			Task.Run(() =>
+			{
+				Console.WriteLine("正在导入自定义词库...");
+				double time = HiPerfTimer.Execute(() =>
+				{
+					var db = app.ApplicationServices.GetRequiredService<DataContext>();
+					var set = db.Post.Select(p => $"{p.Title},{p.Label},{p.Keyword}").AsParallel().SelectMany(s => Regex.Split(s, @"\p{P}(?<!\.|#)|\p{Z}|\p{S}")).Where(s => s.Length > 1).ToHashSet();
+					var lines = File.ReadAllLines(Path.Combine(env.ContentRootPath, "App_Data", "CustomKeywords.txt")).Union(set);
+					KeywordsManager.AddWords(lines);
+					KeywordsManager.AddSynonyms(File.ReadAllLines(Path.Combine(env.ContentRootPath, "App_Data", "CustomSynonym.txt")).Where(s => s.Contains(" ")).Select(s =>
+					{
+						var arr = Regex.Split(s, "\\s");
+						return (arr[0], arr[1]);
+					}));
+				});
+				Console.WriteLine($"导入自定义词库完成,耗时{time}s");
+				Windows.ClearMemorySilent();
+				are.Set();
+			});
 
-            string lucenePath = Path.Combine(env.ContentRootPath, luceneIndexerOptions.Path);
-            if (!Directory.Exists(lucenePath) || Directory.GetFiles(lucenePath).Length < 1)
-            {
-                are.WaitOne();
-                Console.WriteLine("索引库不存在,开始自动创建Lucene索引库...");
-                hangfire.CreateLuceneIndex();
-                Console.WriteLine("索引库创建完成!");
-            }
-        }
+			string lucenePath = Path.Combine(env.ContentRootPath, luceneIndexerOptions.Path);
+			if (!Directory.Exists(lucenePath) || Directory.GetFiles(lucenePath).Length < 1)
+			{
+				are.WaitOne();
+				Console.WriteLine("索引库不存在,开始自动创建Lucene索引库...");
+				hangfire.CreateLuceneIndex();
+				Console.WriteLine("索引库创建完成!");
+			}
+		}
 
-        public static void SetupHangfire(this IApplicationBuilder app)
-        {
-            app.UseHangfireDashboard("/taskcenter", new DashboardOptions()
-            {
-                Authorization = new[]
-                {
-                    new MyRestrictiveAuthorizationFilter()
-                }
-            }); //配置hangfire
-            HangfireJobInit.Start(); //初始化定时任务
-        }
+		public static void SetupHangfire(this IApplicationBuilder app)
+		{
+			app.UseHangfireDashboard("/taskcenter", new DashboardOptions()
+			{
+				Authorization = new[]
+				{
+					new MyRestrictiveAuthorizationFilter()
+				}
+			}); //配置hangfire
+			HangfireJobInit.Start(); //初始化定时任务
+		}
 
-        public static void SetupHttpsRedirection(this IApplicationBuilder app, IConfiguration config)
-        {
-            if (bool.Parse(config["Https:Enabled"]))
-            {
-                app.UseHttpsRedirection();
-            }
+		public static void SetupHttpsRedirection(this IApplicationBuilder app, IConfiguration config)
+		{
+			if (bool.Parse(config["Https:Enabled"]))
+			{
+				app.UseHttpsRedirection();
+			}
 
-            var options = new RewriteOptions().Add(c =>
-            {
-                if (c.HttpContext.Request.Path.Equals("/tag") && c.HttpContext.Request.Query.ContainsKey("tag"))
-                {
-                    c.Result = RuleResult.EndResponse;
-                    c.HttpContext.Response.Redirect("/tag/" + HttpUtility.UrlEncode(c.HttpContext.Request.Query["tag"]), true);
-                }
+			var options = new RewriteOptions().Add(c =>
+			{
+				if (c.HttpContext.Request.Path.Equals("/tag") && c.HttpContext.Request.Query.ContainsKey("tag"))
+				{
+					c.Result = RuleResult.EndResponse;
+					c.HttpContext.Response.Redirect("/tag/" + HttpUtility.UrlEncode(c.HttpContext.Request.Query["tag"]), true);
+				}
 
-                if ((c.HttpContext.Request.Path.Equals("/search") || c.HttpContext.Request.Path.Equals("/s")) && c.HttpContext.Request.Query.ContainsKey("wd"))
-                {
-                    c.Result = RuleResult.EndResponse;
-                    c.HttpContext.Response.Redirect("/search/" + HttpUtility.UrlEncode(c.HttpContext.Request.Query["wd"]).Replace("+", "%20"), true);
-                }
-            }).AddRewrite(@"\w+/_blazor(.*)", "_blazor$1", false);
-            switch (config["UseRewriter"])
-            {
-                case "NonWww":
-                    options.AddRedirectToNonWww(301); // URL重写
-                    break;
+				if ((c.HttpContext.Request.Path.Equals("/search") || c.HttpContext.Request.Path.Equals("/s")) && c.HttpContext.Request.Query.ContainsKey("wd"))
+				{
+					c.Result = RuleResult.EndResponse;
+					c.HttpContext.Response.Redirect("/search/" + HttpUtility.UrlEncode(c.HttpContext.Request.Query["wd"]).Replace("+", "%20"), true);
+				}
+			}).AddRewrite(@"\w+/_blazor(.*)", "_blazor$1", false);
+			switch (config["UseRewriter"])
+			{
+				case "NonWww":
+					options.AddRedirectToNonWww(301); // URL重写
+					break;
 
-                case "WWW":
-                    options.AddRedirectToWww(301); // URL重写
-                    break;
-            }
+				case "WWW":
+					options.AddRedirectToWww(301); // URL重写
+					break;
+			}
 
-            app.UseRewriter(options);
-        }
+			app.UseRewriter(options);
+		}
 
-        public static void SetupMiniProfile(this IServiceCollection services)
-        {
-            services.AddMiniProfiler(options =>
-            {
-                options.RouteBasePath = "/profiler";
-                options.EnableServerTimingHeader = true;
-                options.ResultsAuthorize = req => req.HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo)?.IsAdmin == true;
-                options.ResultsListAuthorize = options.ResultsAuthorize;
-                options.IgnoredPaths.AddRange("/Assets/", "/Content/", "/fonts/", "/images/", "/ng-views/", "/Scripts/", "/static/", "/template/", "/cloud10.png", "/favicon.ico", "/_blazor");
-                options.PopupRenderPosition = RenderPosition.BottomLeft;
-                options.PopupShowTimeWithChildren = true;
-                options.PopupShowTrivial = true;
-            }).AddEntityFramework();
-        }
+		public static void SetupMiniProfile(this IServiceCollection services)
+		{
+			services.AddMiniProfiler(options =>
+			{
+				options.RouteBasePath = "/profiler";
+				options.EnableServerTimingHeader = true;
+				options.ResultsAuthorize = req => req.HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo)?.IsAdmin == true;
+				options.ResultsListAuthorize = options.ResultsAuthorize;
+				options.IgnoredPaths.AddRange("/Assets/", "/Content/", "/fonts/", "/images/", "/ng-views/", "/Scripts/", "/static/", "/template/", "/cloud10.png", "/favicon.ico", "/_blazor");
+				options.PopupRenderPosition = RenderPosition.BottomLeft;
+				options.PopupShowTimeWithChildren = true;
+				options.PopupShowTrivial = true;
+			}).AddEntityFramework();
+		}
 
-        public static void ConfigureOptions(this IServiceCollection services)
-        {
-            services.Configure<CookiePolicyOptions>(options =>
-            {
-                options.MinimumSameSitePolicy = SameSiteMode.Lax;
-            }); //配置Cookie策略
-            services.Configure<FormOptions>(options =>
-            {
-                options.MultipartBodyLengthLimit = 104857600; // 100MB
-            }); //配置请求长度
-            services.Configure<ForwardedHeadersOptions>(options => // X-Forwarded-For
-            {
-                options.ForwardLimit = null;
-                options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
-                options.ForwardedForHeaderName = AppConfig.TrueClientIPHeader;
-                options.KnownNetworks.Clear();
-                options.KnownProxies.Clear();
-            });
-            services.Configure<StaticFileOptions>(options =>
-            {
-                options.OnPrepareResponse = context =>
-                {
-                    context.Context.Response.Headers[HeaderNames.CacheControl] = "public,no-cache";
-                    context.Context.Response.Headers[HeaderNames.Expires] = DateTime.Now.AddDays(7).ToString("R");
-                };
-                options.ContentTypeProvider = new FileExtensionContentTypeProvider(MimeMapper.MimeTypes);
-                options.HttpsCompression = HttpsCompressionMode.Compress;
-            }); // 配置静态资源文件类型和缓存
-        }
-    }
+		public static void ConfigureOptions(this IServiceCollection services)
+		{
+			services.Configure<CookiePolicyOptions>(options =>
+			{
+				options.MinimumSameSitePolicy = SameSiteMode.Lax;
+			}); //配置Cookie策略
+			services.Configure<FormOptions>(options =>
+			{
+				options.MultipartBodyLengthLimit = 104857600; // 100MB
+			}); //配置请求长度
+			services.Configure<ForwardedHeadersOptions>(options => // X-Forwarded-For
+			{
+				options.ForwardLimit = null;
+				options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
+				options.ForwardedForHeaderName = AppConfig.TrueClientIPHeader;
+				options.KnownNetworks.Clear();
+				options.KnownProxies.Clear();
+			});
+			services.Configure<StaticFileOptions>(options =>
+			{
+				options.OnPrepareResponse = context =>
+				{
+					context.Context.Response.Headers[HeaderNames.CacheControl] = "public,no-cache";
+					context.Context.Response.Headers[HeaderNames.Expires] = DateTime.Now.AddDays(7).ToString("R");
+				};
+				options.ContentTypeProvider = new FileExtensionContentTypeProvider(MimeMapper.MimeTypes);
+				options.HttpsCompression = HttpsCompressionMode.Compress;
+			}); // 配置静态资源文件类型和缓存
+		}
+	}
 
-    /// <summary>
-    /// hangfire授权拦截器
-    /// </summary>
-    public class MyRestrictiveAuthorizationFilter : IDashboardAuthorizationFilter
-    {
-        /// <summary>
-        /// 授权校验
-        /// </summary>
-        /// <param name="context"></param>
-        /// <returns></returns>
-        public bool Authorize(DashboardContext context)
-        {
+	/// <summary>
+	/// hangfire授权拦截器
+	/// </summary>
+	public class MyRestrictiveAuthorizationFilter : IDashboardAuthorizationFilter
+	{
+		/// <summary>
+		/// 授权校验
+		/// </summary>
+		/// <param name="context"></param>
+		/// <returns></returns>
+		public bool Authorize(DashboardContext context)
+		{
 #if DEBUG
             return true;
 #endif
-            var user = context.GetHttpContext().Session.Get<UserInfoDto>(SessionKey.UserInfo) ?? new UserInfoDto();
-            return user.IsAdmin;
-        }
-    }
+			var user = context.GetHttpContext().Session.Get<UserInfoDto>(SessionKey.UserInfo) ?? new UserInfoDto();
+			return user.IsAdmin;
+		}
+	}
 }

+ 2 - 1
src/Masuit.MyBlogs.Core/Startup.cs

@@ -169,12 +169,13 @@ namespace Masuit.MyBlogs.Core
         /// <param name="env"></param>
         /// <param name="hangfire"></param>
         /// <param name="luceneIndexerOptions"></param>
+        /// <param name="maindb"></param>
+        /// <param name="loggerdb"></param>
         public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHangfireBackJob hangfire, LuceneIndexerOptions luceneIndexerOptions, DataContext maindb, LoggerDbContext loggerdb)
         {
             ServiceProvider = app.ApplicationServices;
             maindb.Database.EnsureCreated();
             loggerdb.Database.EnsureCreated();
-            maindb.Advertisements.Where(a => a.Id == 159).ExecuteUpdate(e => e.SetProperty(a => a.DisplayCount, a => a.DisplayCount + 1));
             app.InitSettings();
             app.UseLuceneSearch(env, hangfire, luceneIndexerOptions);
             app.UseForwardedHeaders().UseCertificateForwarding(); // X-Forwarded-For

+ 1 - 1
src/Masuit.MyBlogs.Core/Views/Advertisement/ClickRecordsInsight.cshtml

@@ -1,4 +1,4 @@
-@model Masuit.MyBlogs.Core.Models.Entity.Advertisement
+@model Masuit.MyBlogs.Core.Models.ViewModel.AdvertisementViewModel
 
 @{
     Layout = null;

+ 1 - 2
src/Masuit.MyBlogs.Core/Views/Dashboard/Counter.razor

@@ -4,7 +4,6 @@
 @using Masuit.MyBlogs.Core.Common
 @using Masuit.Tools
 @using System.IO
-@using Collections.Pooled
 @using Masuit.Tools.Logging
 @using PerformanceCounter = Masuit.MyBlogs.Core.Common.PerformanceCounter
 @inject IPerfCounter PerfCounter;
@@ -166,7 +165,7 @@
     
     public Dictionary<string, dynamic> GetCounterPercent()
     {
-        using var counters = PerfCounter.CreateDataSource().Where(c => c.ServerIP==ip).OrderByRandom().Take(5000).ToPooledList();
+        var counters = PerfCounter.CreateDataSource().Where(c => c.ServerIP==ip).OrderByRandom().Take(5000).ToList();
         var count = counters.Count();
         return new()
         {

+ 8 - 9
src/Masuit.MyBlogs.Core/Views/Home/Category.cshtml

@@ -6,7 +6,6 @@
 @using Masuit.MyBlogs.Core.Infrastructure.Services.Interface
 @using Masuit.MyBlogs.Core.Models.Enum
 @using Masuit.Tools.Models
-@using Collections.Pooled
 @using Z.EntityFramework.Plus
 @model Masuit.MyBlogs.Core.Models.ViewModel.HomePageViewModel
 @inject ICategoryService CategoryService
@@ -15,24 +14,24 @@
     ViewBag.Title = "分类_" + cat.Path();
     Layout = "~/Views/Shared/_Layout.cshtml";
     var level = cat.Level();
-    using var children2 = new PooledList<Category>();
-    using var children3 = new PooledList<Category>();
+    var children2 = new List<Category>();
+    var children3 = new List<Category>();
     var parentId = cat.ParentId;
     switch (level) {
         case 1:
-            children2.AddRange(cat.Children.Where(c => c.Status == Status.Available).OrderBy(c => c.Id).ToPooledList());
+            children2.AddRange(cat.Children.Where(c => c.Status == Status.Available).OrderBy(c => c.Id).ToList());
             break;
         case 2:
-            children2.AddRange(CategoryService.GetQuery(c => c.Status == Status.Available && c.ParentId == parentId, c => c.Name).FromCache().ToPooledList());
-            children3.AddRange(cat.Children.Where(c => c.Status == Status.Available).OrderBy(c => c.Id).ToPooledList());
+            children2.AddRange(CategoryService.GetQuery(c => c.Status == Status.Available && c.ParentId == parentId, c => c.Name).FromCache().ToList());
+            children3.AddRange(cat.Children.Where(c => c.Status == Status.Available).OrderBy(c => c.Id).ToList());
             break;
         case 3:
             var topid = cat.Parent.ParentId;
-            children2.AddRange(CategoryService.GetQuery(c => c.Status == Status.Available && c.ParentId == topid, c => c.Name).FromCache().ToPooledList());
-            children3.AddRange(CategoryService.GetQuery(c => c.Status == Status.Available && c.ParentId == parentId, c => c.Name).FromCache().ToPooledList());
+            children2.AddRange(CategoryService.GetQuery(c => c.Status == Status.Available && c.ParentId == topid, c => c.Name).FromCache().ToList());
+            children3.AddRange(CategoryService.GetQuery(c => c.Status == Status.Available && c.ParentId == parentId, c => c.Name).FromCache().ToList());
             break;
     }
-    var alllist = CategoryService.GetQuery(c => c.Status == Status.Available && c.ParentId == null, c => c.Name).Select(c => new{c.Id,c.Name}).FromCache().ToPooledList();
+    var alllist = CategoryService.GetQuery(c => c.Status == Status.Available && c.ParentId == null, c => c.Name).Select(c => new{c.Id,c.Name}).FromCache().ToList();
 }
 <style>
     .bg-title {

+ 1 - 2
src/Masuit.MyBlogs.Core/Views/Home/Index.cshtml

@@ -1,5 +1,4 @@
 @using System.Diagnostics
-@using Collections.Pooled
 @using Masuit.MyBlogs.Core.Common
 @using Masuit.MyBlogs.Core.Models.DTO
 @using Masuit.MyBlogs.Core.Models.Entity
@@ -11,7 +10,7 @@
 @{
     ViewBag.Title = "首页";
     Layout = "~/Views/Shared/_Layout.cshtml";
-    PooledList<FastShare> shares = ViewBag.FastShare;
+    List<FastShare> shares = ViewBag.FastShare;
     Context.Request.Path = new PathString("/posts");
     var sliderid = Stopwatch.GetTimestamp().ToBinary(62);
 }

+ 2 - 2
src/Masuit.MyBlogs.Core/Views/Links/Index.cshtml

@@ -1,6 +1,6 @@
 @using Masuit.MyBlogs.Core.Models.DTO
 @using Masuit.MyBlogs.Core.Models.Entity
-@model Collections.Pooled.PooledList<Masuit.MyBlogs.Core.Models.DTO.LinksDto>
+@model List<Masuit.MyBlogs.Core.Models.DTO.LinksDto>
 
 @{
     ViewBag.Title = "友情链接大全";
@@ -29,7 +29,7 @@
     </section>
     <hr class="margin-top10 marginbot10" />
     @{
-        await Html.RenderPartialAsync("_ArticleListAdvertisement", (Advertisement)ViewBag.Ads);
+        await Html.RenderPartialAsync("_ArticleListAdvertisement", (AdvertisementDto)ViewBag.Ads);
     }
 </div>
 <script>

+ 2 - 3
src/Masuit.MyBlogs.Core/Views/Links/Index_Admin.cshtml

@@ -1,6 +1,5 @@
 @using Masuit.MyBlogs.Core.Models.DTO
-@using Masuit.MyBlogs.Core.Models.Entity
-@model Collections.Pooled.PooledList<Masuit.MyBlogs.Core.Models.DTO.LinksDto>
+@model List<Masuit.MyBlogs.Core.Models.DTO.LinksDto>
 
 @{
     ViewBag.Title = "友情链接大全";
@@ -29,7 +28,7 @@
     </section>
     <hr class="margin-top10 marginbot10" />
     @{
-        await Html.RenderPartialAsync("_ArticleListAdvertisement", (Advertisement)ViewBag.Ads);
+        await Html.RenderPartialAsync("_ArticleListAdvertisement", (AdvertisementDto)ViewBag.Ads);
     }
 </div>
 <script>

+ 2 - 2
src/Masuit.MyBlogs.Core/Views/Misc/Donate.cshtml

@@ -1,11 +1,11 @@
 @model string
-@using Masuit.MyBlogs.Core.Models.Entity
+@using Masuit.MyBlogs.Core.Models.DTO
 
 @{
     ViewBag.Title = "网站打赏";
     Layout = "~/Views/Shared/_Layout.cshtml";
     Random r = new Random();
-    List<Advertisement> ads = ViewBag.Ads;
+    List<AdvertisementDto> ads = ViewBag.Ads;
 }
 <style>
     .bg-title {

+ 2 - 2
src/Masuit.MyBlogs.Core/Views/Misc/Donate_Admin.cshtml

@@ -1,11 +1,11 @@
 @model string
-@using Masuit.MyBlogs.Core.Models.Entity
+@using Masuit.MyBlogs.Core.Models.DTO
 
 @{
     ViewBag.Title = "网站打赏";
     Layout = "~/Views/Shared/_Layout.cshtml";
     Random r = new Random();
-    List<Advertisement> ads = ViewBag.Ads;
+    List<AdvertisementDto> ads = ViewBag.Ads;
 }
 <style>
     .bg-title {

+ 1 - 2
src/Masuit.MyBlogs.Core/Views/Notice/Details.cshtml

@@ -1,6 +1,5 @@
 @using Masuit.MyBlogs.Core.Common
 @using Masuit.MyBlogs.Core.Models.DTO
-@using Masuit.MyBlogs.Core.Models.Entity
 @using Masuit.MyBlogs.Core.Models.ViewModel
 @using Masuit.Tools.Core.Net
 @model Masuit.MyBlogs.Core.Models.Entity.Notice
@@ -74,6 +73,6 @@
         </div>
     </div>
     @{
-        await Html.RenderPartialAsync("_ArticleListAdvertisement", (Advertisement)ViewBag.Ads);
+        await Html.RenderPartialAsync("_ArticleListAdvertisement", (AdvertisementDto)ViewBag.Ads);
     }
 </div>

+ 2 - 2
src/Masuit.MyBlogs.Core/Views/Notice/Index.cshtml

@@ -1,4 +1,4 @@
-@using Masuit.MyBlogs.Core.Models.Entity
+@using Masuit.MyBlogs.Core.Models.DTO
 @model IList<Masuit.MyBlogs.Core.Models.DTO.NoticeDto>
 @{
     ViewBag.Title = "网站公告栏";
@@ -30,6 +30,6 @@
     </div>
     @{
         await Html.RenderPartialAsync("_Pagination", ViewData["page"]);
-        await Html.RenderPartialAsync("_ArticleListAdvertisement", (Advertisement)ViewBag.Ads);
+        await Html.RenderPartialAsync("_ArticleListAdvertisement", (AdvertisementDto)ViewBag.Ads);
     }
 </div>

+ 2 - 2
src/Masuit.MyBlogs.Core/Views/Notice/Index_Admin.cshtml

@@ -1,4 +1,4 @@
-@using Masuit.MyBlogs.Core.Models.Entity
+@using Masuit.MyBlogs.Core.Models.DTO
 @model IList<Masuit.MyBlogs.Core.Models.DTO.NoticeDto>
 @{
     ViewBag.Title = "网站公告栏";
@@ -35,7 +35,7 @@
     </div>
     @{
         await Html.RenderPartialAsync("_Pagination", ViewData["page"]);
-        await Html.RenderPartialAsync("_ArticleListAdvertisement", (Advertisement)ViewBag.Ads);
+        await Html.RenderPartialAsync("_ArticleListAdvertisement", (AdvertisementDto)ViewBag.Ads);
     }
 </div>
 <script>

+ 2 - 1
src/Masuit.MyBlogs.Core/Views/Post/CompareVersion.cshtml

@@ -1,6 +1,7 @@
 @using System.Text.Encodings.Web
 @using System.Text.RegularExpressions
 @using Masuit.MyBlogs.Core.Common
+@using Masuit.MyBlogs.Core.Models.DTO
 @using Masuit.MyBlogs.Core.Models.Entity
 @using Masuit.MyBlogs.Core.Models.ViewModel
 @using Masuit.Tools.Core.Net
@@ -9,7 +10,7 @@
 	ViewBag.Title = Model[0].Title + "版本对比";
 	Layout = "~/Views/Shared/_Layout.cshtml";
 	string[] colors = { "success", "info", "primary", "warning", "danger", "default", "primary" };
-	IList<Advertisement> ads = ViewBag.Ads;
+	IList<AdvertisementDto> ads = ViewBag.Ads;
 }
 <style>
 	ins {

+ 2 - 2
src/Masuit.MyBlogs.Core/Views/Post/Details.cshtml

@@ -2,7 +2,7 @@
 @using System.Text.RegularExpressions
 @using System.Web
 @using Masuit.MyBlogs.Core.Common
-@using Masuit.MyBlogs.Core.Models.Entity
+@using Masuit.MyBlogs.Core.Models.DTO
 @using Masuit.MyBlogs.Core.Models.ViewModel
 @using Masuit.MyBlogs.Core.Views.Post
 @model Masuit.MyBlogs.Core.Models.Entity.Post
@@ -13,7 +13,7 @@
 	string[] colors = { "success", "info", "primary", "warning", "danger", "default", "primary" };
 	var cid = Context.Request.RouteValues["cid"]??Context.Request.Query["cid"];
 	string hidden = string.IsNullOrEmpty(Context.Request.Cookies["ValidateKey"]) ? "" : "hidden";
-	Advertisement ad = ViewBag.Ads;
+	AdvertisementDto ad = ViewBag.Ads;
 }
 
 <environment names="Development">

+ 1 - 2
src/Masuit.MyBlogs.Core/Views/Post/Details_Admin.cshtml

@@ -1,7 +1,6 @@
 @using System.Web
 @using Masuit.MyBlogs.Core.Common
 @using Masuit.MyBlogs.Core.Models.DTO
-@using Masuit.MyBlogs.Core.Models.Entity
 @using Masuit.MyBlogs.Core.Models.Enum
 @using Masuit.MyBlogs.Core.Models.ViewModel
 @using Masuit.MyBlogs.Core.Views.Post
@@ -14,7 +13,7 @@
 	string[] colors = { "success", "info", "primary", "warning", "danger", "default", "primary" };
 	UserInfoDto user = Context.Session.Get<UserInfoDto>(SessionKey.UserInfo);
 	var cid = Context.Request.RouteValues["cid"] ?? Context.Request.Query["cid"];
-	Advertisement ad = ViewBag.Ads;
+	AdvertisementDto ad = ViewBag.Ads;
 }
 <script src="https://cdn.staticfile.org/jqueryui/1.12.1/jquery-ui.min.js" async defer></script>
 <environment names="Development">

+ 2 - 1
src/Masuit.MyBlogs.Core/Views/Post/History.cshtml

@@ -1,11 +1,12 @@
 @using Masuit.MyBlogs.Core.Models
+@using Masuit.MyBlogs.Core.Models.DTO
 @using Masuit.MyBlogs.Core.Models.Entity
 @model Masuit.Tools.Models.PagedList<Masuit.MyBlogs.Core.Models.Entity.PostHistoryVersion>
 @{
 	var post = (Post)ViewBag.Primary;
 	ViewBag.Title = post.Title + "的历史版本";
 	Layout = "~/Views/Shared/_Layout.cshtml";
-	Advertisement ad = ViewBag.Ads;
+	AdvertisementDto ad = ViewBag.Ads;
 }
 <div class="container" style="min-height: 70vh">
 	<ol class="cd-breadcrumb triangle">

+ 2 - 1
src/Masuit.MyBlogs.Core/Views/Post/HistoryVersion.cshtml

@@ -1,6 +1,7 @@
 @using System.Text.Encodings.Web
 @using System.Text.RegularExpressions
 @using Masuit.MyBlogs.Core.Common
+@using Masuit.MyBlogs.Core.Models.DTO
 @using Masuit.MyBlogs.Core.Models.Entity
 @using Masuit.MyBlogs.Core.Models.ViewModel
 @using Masuit.Tools.Core.Net
@@ -10,7 +11,7 @@
 	ViewBag.Title = Model.Post.Title + "于" + Model.ModifyDate.ToTimeZoneF(Context.Session.Get<string>(SessionKey.TimeZone)) + "的历史版本:" + Model.Title;
 	Layout = "~/Views/Shared/_Layout.cshtml";
 	string[] colors = { "success", "info", "primary", "warning", "danger", "default", "primary" };
-	Advertisement ad = ViewBag.Ads;
+	AdvertisementDto ad = ViewBag.Ads;
 }
 <environment names="Development">
 	<link href="~/Assets/jquery.tocify/jquery.tocify.css" rel="stylesheet" />

+ 2 - 1
src/Masuit.MyBlogs.Core/Views/Post/HistoryVersion_Admin.cshtml

@@ -1,4 +1,5 @@
 @using Masuit.MyBlogs.Core.Common
+@using Masuit.MyBlogs.Core.Models.DTO
 @using Masuit.MyBlogs.Core.Models.Entity
 @using Masuit.MyBlogs.Core.Models.ViewModel
 @using Masuit.Tools.Core.Net
@@ -7,7 +8,7 @@
 	ViewBag.Title = Model.Post.Title + "于" + Model.ModifyDate.ToTimeZoneF(Context.Session.Get<string>(SessionKey.TimeZone)) + "的历史版本:" + Model.Title;
 	Layout = "~/Views/Shared/_Layout.cshtml";
 	string[] colors = { "success", "info", "primary", "warning", "danger", "default", "primary" };
-	Advertisement ad = ViewBag.Ads;
+	AdvertisementDto ad = ViewBag.Ads;
 }
 <environment names="Development">
 	<link href="~/Assets/jquery.tocify/jquery.tocify.css" rel="stylesheet" />

+ 2 - 2
src/Masuit.MyBlogs.Core/Views/Search/Search.cshtml

@@ -1,5 +1,5 @@
 @using Masuit.MyBlogs.Core.Common
-@using Masuit.MyBlogs.Core.Models.Entity
+@using Masuit.MyBlogs.Core.Models.DTO
 @using Masuit.MyBlogs.Core.Models.ViewModel
 @using Masuit.Tools
 @using Masuit.Tools.Core.Net
@@ -10,7 +10,7 @@
 	Layout = "~/Views/Shared/_Layout.cshtml";
 	IList<KeywordsRank> hotSearches = ViewBag.hotSearches;
 	IList<string> relateKeywords = ViewBag.RelateKeywords;
-	Advertisement ad = ViewBag.Ads;
+	AdvertisementDto ad = ViewBag.Ads;
 }
 <div class="container min-height610">
 	<ol class="cd-breadcrumb triangle">

+ 1 - 2
src/Masuit.MyBlogs.Core/Views/Seminar/Index.cshtml

@@ -1,5 +1,4 @@
 @using Masuit.MyBlogs.Core.Models.DTO
-@using Masuit.MyBlogs.Core.Models.Entity
 @using Masuit.MyBlogs.Core.Models.ViewModel
 @using Masuit.Tools.Core.Net
 @using Masuit.MyBlogs.Core.Models
@@ -13,7 +12,7 @@
     Layout = "~/Views/Shared/_Layout.cshtml";
     Random r = new Random();
     UserInfoDto user = Context.Session.Get<UserInfoDto>(SessionKey.UserInfo) ?? new UserInfoDto();
-    Advertisement ad = ViewBag.Ads;
+    AdvertisementDto ad = ViewBag.Ads;
     Pagination page = ViewData["page"] as Pagination;
 }
 <style>

+ 1 - 1
src/Masuit.MyBlogs.Core/Views/Shared/_ArticleListAdvertisement.cshtml

@@ -1,5 +1,5 @@
 @using System.Diagnostics
-@model Masuit.MyBlogs.Core.Models.Entity.Advertisement
+@model Masuit.MyBlogs.Core.Models.DTO.AdvertisementDto
 <div class="ibox wow fadeIn" id="@Stopwatch.GetTimestamp()">
     <div class="ibox-content" id="@Stopwatch.GetTimestamp()">
         <a asp-controller="Advertisement" asp-action="Redirect" asp-route-id="@Model.Id" target="_blank" id="@Stopwatch.GetTimestamp()">

+ 1 - 2
src/Masuit.MyBlogs.Core/Views/Shared/_ArticleListPartial.cshtml

@@ -3,7 +3,6 @@
 @using Masuit.MyBlogs.Core.Infrastructure.Services.Interface
 @using Masuit.MyBlogs.Core.Models.Enum
 @using Microsoft.AspNetCore.Mvc.TagHelpers
-@using Collections.Pooled
 @using Masuit.Tools.Core.AspNetCore
 @model Masuit.MyBlogs.Core.Models.ViewModel.HomePageViewModel
 @inject IPostService PostService
@@ -26,7 +25,7 @@
         }
         else
         {
-            using var list = Model.PostsQueryable.OrderByRandom().Take(5).ToPooledList();
+            var list = Model.PostsQueryable.OrderByRandom().Take(5).ToList();
             if (list.Any())
             {
                 if (Model.Posts.CurrentPage == 1)

+ 1 - 2
src/Masuit.MyBlogs.Core/Views/Shared/_ArticleListPartial_Admin.cshtml

@@ -4,7 +4,6 @@
 @using Masuit.MyBlogs.Core.Infrastructure.Services.Interface
 @using Masuit.MyBlogs.Core.Models.Enum
 @using Microsoft.AspNetCore.Mvc.TagHelpers
-@using Collections.Pooled
 @using Masuit.Tools.Core.AspNetCore
 @model Masuit.MyBlogs.Core.Models.ViewModel.HomePageViewModel
 @inject IPostService PostService
@@ -27,7 +26,7 @@
         }
         else
         {
-            using var list = Model.PostsQueryable.OrderByRandom().Take(5).ToPooledList();
+            var list = Model.PostsQueryable.OrderByRandom().Take(5).ToList();
             if (list.Any())
             {
                 <div class="page-header">

+ 1 - 2
src/Masuit.MyBlogs.Core/Views/Shared/_Layout.cshtml

@@ -13,7 +13,6 @@
 @using Microsoft.AspNetCore.Http.Extensions
 @using Microsoft.AspNetCore.Mvc.TagHelpers
 @using StackExchange.Profiling
-@using Collections.Pooled
 @using Masuit.Tools.AspNetCore.Extensions
 @using Z.EntityFramework.Plus
 
@@ -21,7 +20,7 @@
     string[] colors = { "success", "info", "warning", "danger", "default" };
     var menus = MenuService.GetQueryFromCache(m => m.Status == Status.Available,m => m.Sort).ToTree(m => m.Id,m => m.ParentId);
     var user = Context.Session.Get<UserInfoDto>(SessionKey.UserInfo) ?? new UserInfoDto();
-    using var links = LinksService.GetQuery(l => l.Status == Status.Available).OrderByDescending(l => l.Recommend).ThenByDescending(l => l.Loopbacks.GroupBy(x => x.IP).Count()).Take(30).Select(e => new{e.Url,e.Name}).FromCache().ToPooledList();
+    var links = LinksService.GetQuery(l => l.Status == Status.Available).OrderByDescending(l => l.Recommend).ThenByDescending(l => l.Loopbacks.GroupBy(x => x.IP).Count()).Take(30).Select(e => new{e.Url,e.Name}).FromCache().ToList();
 }
 
 <!DOCTYPE html>