懒得勤快 2 years ago
parent
commit
e7c567f554
100 changed files with 9521 additions and 9885 deletions
  1. 0 2
      src/Masuit.MyBlogs.Core/Common/CommonHelper.cs
  2. 51 54
      src/Masuit.MyBlogs.Core/Common/HttpContextExtension.cs
  3. 165 167
      src/Masuit.MyBlogs.Core/Common/ImagebedClient.cs
  4. 8 9
      src/Masuit.MyBlogs.Core/Common/Mails/IMailSender.cs
  5. 17 18
      src/Masuit.MyBlogs.Core/Common/Mails/MailServiceCollectionExt.cs
  6. 58 59
      src/Masuit.MyBlogs.Core/Common/Mails/MailgunSender.cs
  7. 1 2
      src/Masuit.MyBlogs.Core/Common/Mails/SmtpSender.cs
  8. 175 178
      src/Masuit.MyBlogs.Core/Common/PerfCounter.cs
  9. 313 314
      src/Masuit.MyBlogs.Core/Common/QQWrySearcher.cs
  10. 51 54
      src/Masuit.MyBlogs.Core/Common/TrackData.cs
  11. 285 288
      src/Masuit.MyBlogs.Core/Common/UserAgent.cs
  12. 30 31
      src/Masuit.MyBlogs.Core/Configs/AppConfig.cs
  13. 9 10
      src/Masuit.MyBlogs.Core/Configs/AutofacModule.cs
  14. 9 10
      src/Masuit.MyBlogs.Core/Configs/GitlabConfig.cs
  15. 81 88
      src/Masuit.MyBlogs.Core/Configs/MappingProfile.cs
  16. 59 66
      src/Masuit.MyBlogs.Core/Controllers/AdminController.cs
  17. 0 9
      src/Masuit.MyBlogs.Core/Controllers/AdvertisementController.cs
  18. 252 321
      src/Masuit.MyBlogs.Core/Controllers/BaseController.cs
  19. 63 70
      src/Masuit.MyBlogs.Core/Controllers/CategoryController.cs
  20. 261 272
      src/Masuit.MyBlogs.Core/Controllers/CommentController.cs
  21. 101 105
      src/Masuit.MyBlogs.Core/Controllers/DashboardController.cs
  22. 52 55
      src/Masuit.MyBlogs.Core/Controllers/DonateController.cs
  23. 240 241
      src/Masuit.MyBlogs.Core/Controllers/Drive/AdminController.cs
  24. 19 20
      src/Masuit.MyBlogs.Core/Controllers/Drive/DriveController.cs
  25. 237 241
      src/Masuit.MyBlogs.Core/Controllers/Drive/SitesController.cs
  26. 49 55
      src/Masuit.MyBlogs.Core/Controllers/Drive/UserController.cs
  27. 207 212
      src/Masuit.MyBlogs.Core/Controllers/ErrorController.cs
  28. 267 271
      src/Masuit.MyBlogs.Core/Controllers/FileController.cs
  29. 148 153
      src/Masuit.MyBlogs.Core/Controllers/FirewallController.cs
  30. 0 11
      src/Masuit.MyBlogs.Core/Controllers/HomeController.cs
  31. 195 200
      src/Masuit.MyBlogs.Core/Controllers/LinksController.cs
  32. 19 21
      src/Masuit.MyBlogs.Core/Controllers/LoginController.cs
  33. 67 74
      src/Masuit.MyBlogs.Core/Controllers/MenuController.cs
  34. 147 160
      src/Masuit.MyBlogs.Core/Controllers/MergeController.cs
  35. 187 195
      src/Masuit.MyBlogs.Core/Controllers/MiscController.cs
  36. 299 310
      src/Masuit.MyBlogs.Core/Controllers/MsgController.cs
  37. 207 214
      src/Masuit.MyBlogs.Core/Controllers/NoticeController.cs
  38. 182 192
      src/Masuit.MyBlogs.Core/Controllers/PassportController.cs
  39. 20 22
      src/Masuit.MyBlogs.Core/Controllers/PostController.cs
  40. 102 112
      src/Masuit.MyBlogs.Core/Controllers/SearchController.cs
  41. 166 176
      src/Masuit.MyBlogs.Core/Controllers/SeminarController.cs
  42. 57 60
      src/Masuit.MyBlogs.Core/Controllers/ShareController.cs
  43. 18 20
      src/Masuit.MyBlogs.Core/Controllers/ShortController.cs
  44. 290 300
      src/Masuit.MyBlogs.Core/Controllers/SubscribeController.cs
  45. 330 336
      src/Masuit.MyBlogs.Core/Controllers/SystemController.cs
  46. 157 160
      src/Masuit.MyBlogs.Core/Controllers/ToolsController.cs
  47. 0 4
      src/Masuit.MyBlogs.Core/Controllers/UploadController.cs
  48. 1 4
      src/Masuit.MyBlogs.Core/Controllers/UserController.cs
  49. 0 1
      src/Masuit.MyBlogs.Core/Controllers/ValidateController.cs
  50. 1 3
      src/Masuit.MyBlogs.Core/Controllers/ValuesController.cs
  51. 1 2
      src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/ProtectedApiCallHelper.cs
  52. 14 15
      src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/ServiceCollectionExtension.cs
  53. 6 7
      src/Masuit.MyBlogs.Core/Extensions/Firewall/AccessDenyException.cs
  54. 46 47
      src/Masuit.MyBlogs.Core/Extensions/Firewall/CloudflareRepoter.cs
  55. 20 21
      src/Masuit.MyBlogs.Core/Extensions/Firewall/DefaultFirewallRepoter.cs
  56. 243 235
      src/Masuit.MyBlogs.Core/Extensions/Firewall/FirewallAttribute.cs
  57. 22 23
      src/Masuit.MyBlogs.Core/Extensions/Firewall/FirewallServiceCollectionExt.cs
  58. 15 16
      src/Masuit.MyBlogs.Core/Extensions/Firewall/IFirewallRepoter.cs
  59. 123 112
      src/Masuit.MyBlogs.Core/Extensions/Firewall/IRequestLogger.cs
  60. 12 13
      src/Masuit.MyBlogs.Core/Extensions/Firewall/IpIntercepter.cs
  61. 113 106
      src/Masuit.MyBlogs.Core/Extensions/Firewall/RequestInterceptMiddleware.cs
  62. 9 9
      src/Masuit.MyBlogs.Core/Extensions/Hangfire/HangfireActivator.cs
  63. 250 259
      src/Masuit.MyBlogs.Core/Extensions/Hangfire/HangfireBackJob.cs
  64. 20 21
      src/Masuit.MyBlogs.Core/Extensions/Hangfire/HangfireJobInit.cs
  65. 52 57
      src/Masuit.MyBlogs.Core/Extensions/Hangfire/IHangfireBackJob.cs
  66. 181 182
      src/Masuit.MyBlogs.Core/Extensions/MiddlewareExtension.cs
  67. 0 5
      src/Masuit.MyBlogs.Core/Extensions/MyAuthorizeAttribute.cs
  68. 6 7
      src/Masuit.MyBlogs.Core/Extensions/NotFoundException.cs
  69. 0 1
      src/Masuit.MyBlogs.Core/Extensions/TranslateMiddleware.cs
  70. 21 22
      src/Masuit.MyBlogs.Core/Extensions/UEditor/ConfigHandler.cs
  71. 93 95
      src/Masuit.MyBlogs.Core/Extensions/UEditor/CrawlerHandler.cs
  72. 23 25
      src/Masuit.MyBlogs.Core/Extensions/UEditor/Handler.cs
  73. 86 87
      src/Masuit.MyBlogs.Core/Extensions/UEditor/ListFileManager.cs
  74. 16 17
      src/Masuit.MyBlogs.Core/Extensions/UEditor/NotSupportedHandler.cs
  75. 37 38
      src/Masuit.MyBlogs.Core/Extensions/UEditor/PathFormatter.cs
  76. 23 24
      src/Masuit.MyBlogs.Core/Extensions/UEditor/UeditorConfig.cs
  77. 185 187
      src/Masuit.MyBlogs.Core/Extensions/UEditor/UploadHandler.cs
  78. 1 2
      src/Masuit.MyBlogs.Core/Infrastructure/DataContext.cs
  79. 50 51
      src/Masuit.MyBlogs.Core/Infrastructure/Drive/IDriveAccountService.cs
  80. 8 9
      src/Masuit.MyBlogs.Core/Infrastructure/Drive/IDriveService.cs
  81. 12 13
      src/Masuit.MyBlogs.Core/Infrastructure/LoggerDbContext.cs
  82. 558 561
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/BaseRepository.cs
  83. 0 1
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/CommentRepository.cs
  84. 463 466
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/Interface/IBaseRepository.cs
  85. 9 12
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/Interface/ISearchDetailsRepository.cs
  86. 0 1
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/LeaveMessageRepository.cs
  87. 0 1
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/MenuRepository.cs
  88. 0 2
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/PostRepository.cs
  89. 0 1
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/Repositories.cs
  90. 9 10
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/SearchDetailsRepository.cs
  91. 0 7
      src/Masuit.MyBlogs.Core/Infrastructure/Services/AdvertisementService.cs
  92. 658 661
      src/Masuit.MyBlogs.Core/Infrastructure/Services/BaseService.cs
  93. 0 2
      src/Masuit.MyBlogs.Core/Infrastructure/Services/CategoryService.cs
  94. 0 2
      src/Masuit.MyBlogs.Core/Infrastructure/Services/CommentService.cs
  95. 3 6
      src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/IAdvertisementService.cs
  96. 464 466
      src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/IBaseService.cs
  97. 9 12
      src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/ICategoryService.cs
  98. 2 5
      src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/ICommentService.cs
  99. 2 5
      src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/ILeaveMessageService.cs
  100. 3 6
      src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/IMenuService.cs

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

@@ -5,10 +5,8 @@ using AutoMapper;
 using FreeRedis;
 using Hangfire;
 using Masuit.MyBlogs.Core.Common.Mails;
-using Masuit.Tools;
 using Masuit.Tools.Media;
 using Masuit.Tools.Models;
-using Masuit.Tools.Security;
 using Masuit.Tools.Systems;
 using MaxMind.GeoIP2;
 using MaxMind.GeoIP2.Responses;

+ 51 - 54
src/Masuit.MyBlogs.Core/Common/HttpContextExtension.cs

@@ -1,62 +1,59 @@
 using DnsClient;
-using Masuit.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools;
 using Microsoft.Net.Http.Headers;
 using Polly;
 
-namespace Masuit.MyBlogs.Core.Common
+namespace Masuit.MyBlogs.Core.Common;
+
+public static class HttpContextExtension
 {
-    public static class HttpContextExtension
-    {
-        /// <summary>
-        /// 地理位置信息
-        /// </summary>
-        /// <param name="request"></param>
-        /// <returns></returns>
-        public static IPLocation Location(this HttpRequest request)
-        {
-            return (IPLocation)request.HttpContext.Items.GetOrAdd("ip.location", () => request.HttpContext.Connection.RemoteIpAddress.GetIPLocation());
-        }
+	/// <summary>
+	/// 地理位置信息
+	/// </summary>
+	/// <param name="request"></param>
+	/// <returns></returns>
+	public static IPLocation Location(this HttpRequest request)
+	{
+		return (IPLocation)request.HttpContext.Items.GetOrAdd("ip.location", () => request.HttpContext.Connection.RemoteIpAddress.GetIPLocation());
+	}
 
-        public static int[] GetHideCategories(this HttpRequest request)
-        {
-            return request.Cookies[SessionKey.HideCategories]?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => s.ToInt32()).ToArray() ?? request.Query[SessionKey.SafeMode].ToString().Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => s.ToInt32()).ToArray();
-        }
+	public static int[] GetHideCategories(this HttpRequest request)
+	{
+		return request.Cookies[SessionKey.HideCategories]?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => s.ToInt32()).ToArray() ?? request.Query[SessionKey.SafeMode].ToString().Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => s.ToInt32()).ToArray();
+	}
 
-        /// <summary>
-        /// 是否是搜索引擎访问
-        /// </summary>
-        /// <param name="req"></param>
-        /// <returns></returns>
-        public static bool IsRobot(this HttpRequest req)
-        {
-            if (UserAgent.Parse(req.Headers[HeaderNames.UserAgent].ToString()).IsRobot || req.Location().Contains("Spider", "蜘蛛"))
-            {
-                var nslookup = new LookupClient();
-                var fallbackPolicy = Policy<bool>.Handle<Exception>().FallbackAsync(false);
-                var retryPolicy = Policy<bool>.Handle<Exception>().RetryAsync(3);
-                return Policy.WrapAsync(fallbackPolicy, retryPolicy).ExecuteAsync(async () =>
-                {
-                    using var cts = new CancellationTokenSource(1000);
-                    var query = await nslookup.QueryReverseAsync(req.HttpContext.Connection.RemoteIpAddress, cts.Token);
-                    return query.Answers.Any(r => r.ToString().Trim('.').EndsWith(new[]
-                    {
-                        "baidu.com",
-                        "google.com",
-                        "googlebot.com",
-                        "googleusercontent.com",
-                        "bing.com",
-                        "search.msn.com",
-                        "sogou.com",
-                        "soso.com",
-                        "yandex.com",
-                        "apple.com",
-                        "sm.cn"
-                    }));
-                }).Result;
-            }
+	/// <summary>
+	/// 是否是搜索引擎访问
+	/// </summary>
+	/// <param name="req"></param>
+	/// <returns></returns>
+	public static bool IsRobot(this HttpRequest req)
+	{
+		if (UserAgent.Parse(req.Headers[HeaderNames.UserAgent].ToString()).IsRobot || req.Location().Contains("Spider", "蜘蛛"))
+		{
+			var nslookup = new LookupClient();
+			var fallbackPolicy = Policy<bool>.Handle<Exception>().FallbackAsync(false);
+			var retryPolicy = Policy<bool>.Handle<Exception>().RetryAsync(3);
+			return Policy.WrapAsync(fallbackPolicy, retryPolicy).ExecuteAsync(async () =>
+			{
+				using var cts = new CancellationTokenSource(1000);
+				var query = await nslookup.QueryReverseAsync(req.HttpContext.Connection.RemoteIpAddress, cts.Token);
+				return query.Answers.Any(r => r.ToString().Trim('.').EndsWith(new[]
+				{
+					"baidu.com",
+					"google.com",
+					"googlebot.com",
+					"googleusercontent.com",
+					"bing.com",
+					"search.msn.com",
+					"sogou.com",
+					"soso.com",
+					"yandex.com",
+					"apple.com",
+					"sm.cn"
+				}));
+			}).Result;
+		}
 
-            return false;
-        }
-    }
-}
+		return false;
+	}
+}

+ 165 - 167
src/Masuit.MyBlogs.Core/Common/ImagebedClient.cs

@@ -1,178 +1,176 @@
 using Hangfire;
 using Masuit.MyBlogs.Core.Configs;
-using Masuit.Tools;
 using Masuit.Tools.Html;
 using Masuit.Tools.Logging;
-using Masuit.Tools.Systems;
 using System.Net.Http.Headers;
 using System.Text.RegularExpressions;
 using System.Web;
 
 namespace Masuit.MyBlogs.Core.Common
 {
-    /// <summary>
-    /// 图床客户端
-    /// </summary>
-    public sealed class ImagebedClient
-    {
-        private readonly HttpClient _httpClient;
-        private readonly IConfiguration _config;
-
-        /// <summary>
-        /// 图床客户端
-        /// </summary>
-        /// <param name="httpClient"></param>
-        /// <param name="config"></param>
-        public ImagebedClient(HttpClient httpClient, IConfiguration config)
-        {
-            _config = config;
-            _httpClient = httpClient;
-        }
-
-        private readonly List<string> _failedList = new();
-
-        /// <summary>
-        /// 上传图片
-        /// </summary>
-        /// <param name="stream"></param>
-        /// <param name="file"></param>
-        /// <returns></returns>
-        public Task<(string url, bool success)> UploadImage(Stream stream, string file, CancellationToken cancellationToken)
-        {
-            if (stream.Length < 51200)
-            {
-                return Task.FromResult<(string, bool)>((null, false));
-            }
-
-            file = Regex.Replace(Path.GetFileName(file), @"\p{P}|\p{S}", "");
-            var gitlabs = AppConfig.GitlabConfigs.Where(c => c.FileLimitSize >= stream.Length && !_failedList.Contains(c.ApiUrl)).OrderByRandom().ToList();
-            if (gitlabs.Count > 0)
-            {
-                var gitlab = gitlabs[0];
-                if (gitlab.ApiUrl.Contains("api.github.com"))
-                {
-                    return UploadGithub(gitlab, stream, file, cancellationToken);
-                }
-
-                return UploadGitlab(gitlab, stream, file, cancellationToken);
-            }
-
-            return Task.FromResult<(string, bool)>((null, false));
-        }
-
-        /// <summary>
-        /// github图床
-        /// </summary>
-        /// <param name="config"></param>
-        /// <param name="stream"></param>
-        /// <param name="file"></param>
-        /// <returns></returns>
-        private Task<(string url, bool success)> UploadGithub(GitlabConfig config, Stream stream, string file, CancellationToken cancellationToken)
-        {
-            var path = $"{DateTime.Now:yyyy\\/MM\\/dd}/{file}";
-            _httpClient.DefaultRequestHeaders.UserAgent.Add(ProductInfoHeaderValue.Parse("Awesome-Octocat-App"));
-            _httpClient.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse("token " + config.AccessToken);
-            return _httpClient.PutAsJsonAsync(config.ApiUrl + HttpUtility.UrlEncode(path), new
-            {
-                message = SnowFlake.NewId,
-                committer = new
-                {
-                    name = SnowFlake.NewId,
-                    email = "[email protected]"
-                },
-                content = Convert.ToBase64String(stream.ToArray())
-            }, cancellationToken).ContinueWith(t =>
-            {
-                if (t.IsCompletedSuccessfully)
-                {
-                    using var resp = t.Result;
-                    using var content = resp.Content;
-                    if (resp.IsSuccessStatusCode)
-                    {
-                        return (config.RawUrl.Split(',').OrderByRandom().FirstOrDefault() + path, true);
-                    }
-                }
-
-                LogManager.Info("图片上传到gitee失败。");
-                return (null, false);
-            });
-        }
-
-        /// <summary>
-        /// github图床
-        /// </summary>
-        /// <param name="config"></param>
-        /// <param name="stream"></param>
-        /// <param name="file"></param>
-        /// <returns></returns>
-        private Task<(string url, bool success)> UploadGitlab(GitlabConfig config, Stream stream, string file, CancellationToken cancellationToken)
-        {
-            var path = $"{DateTime.Now:yyyy\\/MM\\/dd}/{file}";
-            _httpClient.DefaultRequestHeaders.Add("PRIVATE-TOKEN", config.AccessToken);
-            return _httpClient.PostAsJsonAsync(config.ApiUrl.Contains("/v3/") ? config.ApiUrl : config.ApiUrl + HttpUtility.UrlEncode(path), new
-            {
-                file_path = path,
-                branch_name = config.Branch,
-                branch = config.Branch,
-                author_email = CommonHelper.SystemSettings["ReceiveEmail"],
-                author_name = SnowFlake.NewId,
-                encoding = "base64",
-                content = Convert.ToBase64String(stream.ToArray()),
-                commit_message = SnowFlake.NewId
-            }, cancellationToken).ContinueWith(t =>
-            {
-                if (t.IsCompletedSuccessfully)
-                {
-                    using var resp = t.Result;
-                    using var content = resp.Content;
-                    if (resp.IsSuccessStatusCode || content.ReadAsStringAsync().Result.Contains("already exists"))
-                    {
-                        return (config.RawUrl + path, true);
-                    }
-                }
-
-                LogManager.Info($"图片上传到gitlab({config.ApiUrl})失败。");
-                _failedList.Add(config.ApiUrl);
-                return (null, false);
-            });
-        }
-
-        /// <summary>
-        /// 替换img标签的src属性
-        /// </summary>
-        /// <param name="content"></param>
-        /// <returns></returns>
-        public async Task<string> ReplaceImgSrc(string content, CancellationToken cancellationToken)
-        {
-            if (bool.TryParse(_config["Imgbed:EnableLocalStorage"], out var b) && b)
-            {
-                return content;
-            }
-
-            var srcs = content.MatchImgSrcs();
-            foreach (var src in srcs)
-            {
-                if (src.StartsWith("http"))
-                {
-                    continue;
-                }
-
-                var path = Path.Combine(AppContext.BaseDirectory + "wwwroot", src.Replace("/", @"\")[1..]);
-                if (!File.Exists(path))
-                {
-                    continue;
-                }
-
-                await using var stream = File.OpenRead(path);
-                var (url, success) = await UploadImage(stream, path, cancellationToken);
-                if (success)
-                {
-                    content = content.Replace(src, url);
-                    BackgroundJob.Enqueue(() => File.Delete(path));
-                }
-            }
-
-            return content;
-        }
-    }
+	/// <summary>
+	/// 图床客户端
+	/// </summary>
+	public sealed class ImagebedClient
+	{
+		private readonly HttpClient _httpClient;
+		private readonly IConfiguration _config;
+
+		/// <summary>
+		/// 图床客户端
+		/// </summary>
+		/// <param name="httpClient"></param>
+		/// <param name="config"></param>
+		public ImagebedClient(HttpClient httpClient, IConfiguration config)
+		{
+			_config = config;
+			_httpClient = httpClient;
+		}
+
+		private readonly List<string> _failedList = new();
+
+		/// <summary>
+		/// 上传图片
+		/// </summary>
+		/// <param name="stream"></param>
+		/// <param name="file"></param>
+		/// <returns></returns>
+		public Task<(string url, bool success)> UploadImage(Stream stream, string file, CancellationToken cancellationToken)
+		{
+			if (stream.Length < 51200)
+			{
+				return Task.FromResult<(string, bool)>((null, false));
+			}
+
+			file = Regex.Replace(Path.GetFileName(file), @"\p{P}|\p{S}", "");
+			var gitlabs = AppConfig.GitlabConfigs.Where(c => c.FileLimitSize >= stream.Length && !_failedList.Contains(c.ApiUrl)).OrderByRandom().ToList();
+			if (gitlabs.Count > 0)
+			{
+				var gitlab = gitlabs[0];
+				if (gitlab.ApiUrl.Contains("api.github.com"))
+				{
+					return UploadGithub(gitlab, stream, file, cancellationToken);
+				}
+
+				return UploadGitlab(gitlab, stream, file, cancellationToken);
+			}
+
+			return Task.FromResult<(string, bool)>((null, false));
+		}
+
+		/// <summary>
+		/// github图床
+		/// </summary>
+		/// <param name="config"></param>
+		/// <param name="stream"></param>
+		/// <param name="file"></param>
+		/// <returns></returns>
+		private Task<(string url, bool success)> UploadGithub(GitlabConfig config, Stream stream, string file, CancellationToken cancellationToken)
+		{
+			var path = $"{DateTime.Now:yyyy\\/MM\\/dd}/{file}";
+			_httpClient.DefaultRequestHeaders.UserAgent.Add(ProductInfoHeaderValue.Parse("Awesome-Octocat-App"));
+			_httpClient.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse("token " + config.AccessToken);
+			return _httpClient.PutAsJsonAsync(config.ApiUrl + HttpUtility.UrlEncode(path), new
+			{
+				message = SnowFlake.NewId,
+				committer = new
+				{
+					name = SnowFlake.NewId,
+					email = "[email protected]"
+				},
+				content = Convert.ToBase64String(stream.ToArray())
+			}, cancellationToken).ContinueWith(t =>
+			{
+				if (t.IsCompletedSuccessfully)
+				{
+					using var resp = t.Result;
+					using var content = resp.Content;
+					if (resp.IsSuccessStatusCode)
+					{
+						return (config.RawUrl.Split(',').OrderByRandom().FirstOrDefault() + path, true);
+					}
+				}
+
+				LogManager.Info("图片上传到gitee失败。");
+				return (null, false);
+			});
+		}
+
+		/// <summary>
+		/// github图床
+		/// </summary>
+		/// <param name="config"></param>
+		/// <param name="stream"></param>
+		/// <param name="file"></param>
+		/// <returns></returns>
+		private Task<(string url, bool success)> UploadGitlab(GitlabConfig config, Stream stream, string file, CancellationToken cancellationToken)
+		{
+			var path = $"{DateTime.Now:yyyy\\/MM\\/dd}/{file}";
+			_httpClient.DefaultRequestHeaders.Add("PRIVATE-TOKEN", config.AccessToken);
+			return _httpClient.PostAsJsonAsync(config.ApiUrl.Contains("/v3/") ? config.ApiUrl : config.ApiUrl + HttpUtility.UrlEncode(path), new
+			{
+				file_path = path,
+				branch_name = config.Branch,
+				branch = config.Branch,
+				author_email = CommonHelper.SystemSettings["ReceiveEmail"],
+				author_name = SnowFlake.NewId,
+				encoding = "base64",
+				content = Convert.ToBase64String(stream.ToArray()),
+				commit_message = SnowFlake.NewId
+			}, cancellationToken).ContinueWith(t =>
+			{
+				if (t.IsCompletedSuccessfully)
+				{
+					using var resp = t.Result;
+					using var content = resp.Content;
+					if (resp.IsSuccessStatusCode || content.ReadAsStringAsync().Result.Contains("already exists"))
+					{
+						return (config.RawUrl + path, true);
+					}
+				}
+
+				LogManager.Info($"图片上传到gitlab({config.ApiUrl})失败。");
+				_failedList.Add(config.ApiUrl);
+				return (null, false);
+			});
+		}
+
+		/// <summary>
+		/// 替换img标签的src属性
+		/// </summary>
+		/// <param name="content"></param>
+		/// <returns></returns>
+		public async Task<string> ReplaceImgSrc(string content, CancellationToken cancellationToken)
+		{
+			if (bool.TryParse(_config["Imgbed:EnableLocalStorage"], out var b) && b)
+			{
+				return content;
+			}
+
+			var srcs = content.MatchImgSrcs();
+			foreach (var src in srcs)
+			{
+				if (src.StartsWith("http"))
+				{
+					continue;
+				}
+
+				var path = Path.Combine(AppContext.BaseDirectory + "wwwroot", src.Replace("/", @"\")[1..]);
+				if (!File.Exists(path))
+				{
+					continue;
+				}
+
+				await using var stream = File.OpenRead(path);
+				var (url, success) = await UploadImage(stream, path, cancellationToken);
+				if (success)
+				{
+					content = content.Replace(src, url);
+					BackgroundJob.Enqueue(() => File.Delete(path));
+				}
+			}
+
+			return content;
+		}
+	}
 }

+ 8 - 9
src/Masuit.MyBlogs.Core/Common/Mails/IMailSender.cs

@@ -1,13 +1,12 @@
-namespace Masuit.MyBlogs.Core.Common.Mails
+namespace Masuit.MyBlogs.Core.Common.Mails;
+
+public interface IMailSender
 {
-    public interface IMailSender
-    {
-        void Send(string title, string content, string tos);
+	void Send(string title, string content, string tos);
 
-        List<string> GetBounces();
+	List<string> GetBounces();
 
-        string AddRecipient(string email);
+	string AddRecipient(string email);
 
-        public bool HasBounced(string address);
-    }
-}
+	public bool HasBounced(string address);
+}

+ 17 - 18
src/Masuit.MyBlogs.Core/Common/Mails/MailServiceCollectionExt.cs

@@ -1,21 +1,20 @@
-namespace Masuit.MyBlogs.Core.Common.Mails
+namespace Masuit.MyBlogs.Core.Common.Mails;
+
+public static class MailServiceCollectionExt
 {
-    public static class MailServiceCollectionExt
-    {
-        public static IServiceCollection AddMailSender(this IServiceCollection services, IConfiguration configuration)
-        {
-            switch (configuration["MailSender"])
-            {
-                case "Mailgun":
-                    services.AddHttpClient<IMailSender, MailgunSender>();
-                    break;
+	public static IServiceCollection AddMailSender(this IServiceCollection services, IConfiguration configuration)
+	{
+		switch (configuration["MailSender"])
+		{
+			case "Mailgun":
+				services.AddHttpClient<IMailSender, MailgunSender>();
+				break;
 
-                default:
-                    services.AddSingleton<IMailSender, SmtpSender>();
-                    break;
-            }
+			default:
+				services.AddSingleton<IMailSender, SmtpSender>();
+				break;
+		}
 
-            return services;
-        }
-    }
-}
+		return services;
+	}
+}

+ 58 - 59
src/Masuit.MyBlogs.Core/Common/Mails/MailgunSender.cs

@@ -4,68 +4,67 @@ using Newtonsoft.Json.Linq;
 using System.Net.Http.Headers;
 using System.Text;
 
-namespace Masuit.MyBlogs.Core.Common.Mails
+namespace Masuit.MyBlogs.Core.Common.Mails;
+
+public sealed class MailgunSender : IMailSender
 {
-    public sealed class MailgunSender : IMailSender
-    {
-        private readonly HttpClient _httpClient;
-        private readonly IConfiguration _configuration;
-        private readonly ICacheManager<List<string>> _cacheManager;
-        private readonly ICacheManager<bool> _bouncedCacheManager;
+	private readonly HttpClient _httpClient;
+	private readonly IConfiguration _configuration;
+	private readonly ICacheManager<List<string>> _cacheManager;
+	private readonly ICacheManager<bool> _bouncedCacheManager;
 
-        public MailgunSender(HttpClient httpClient, IConfiguration configuration, ICacheManager<List<string>> cacheManager, ICacheManager<bool> bouncedCacheManager)
-        {
-            _configuration = configuration;
-            _cacheManager = cacheManager;
-            _bouncedCacheManager = bouncedCacheManager;
-            _httpClient = httpClient;
-            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"api:{_configuration["MailgunConfig:apikey"]}")));
-        }
+	public MailgunSender(HttpClient httpClient, IConfiguration configuration, ICacheManager<List<string>> cacheManager, ICacheManager<bool> bouncedCacheManager)
+	{
+		_configuration = configuration;
+		_cacheManager = cacheManager;
+		_bouncedCacheManager = bouncedCacheManager;
+		_httpClient = httpClient;
+		_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"api:{_configuration["MailgunConfig:apikey"]}")));
+	}
 
-        public void Send(string title, string content, string tos)
-        {
-            EmailAddress email = _configuration["MailgunConfig:from"];
-            using var form = new MultipartFormDataContent
-            {
-                { new StringContent(email,Encoding.UTF8), "from" },
-                { new StringContent(tos,Encoding.UTF8), "to" },
-                { new StringContent(title,Encoding.UTF8), "subject" },
-                { new StringContent(content,Encoding.UTF8), "html" }
-            };
-            _httpClient.PostAsync($"https://api.mailgun.net/v3/{email.Domain}/messages", form).Wait();
-        }
+	public void Send(string title, string content, string tos)
+	{
+		EmailAddress email = _configuration["MailgunConfig:from"];
+		using var form = new MultipartFormDataContent
+		{
+			{ new StringContent(email,Encoding.UTF8), "from" },
+			{ new StringContent(tos,Encoding.UTF8), "to" },
+			{ new StringContent(title,Encoding.UTF8), "subject" },
+			{ new StringContent(content,Encoding.UTF8), "html" }
+		};
+		_httpClient.PostAsync($"https://api.mailgun.net/v3/{email.Domain}/messages", form).Wait();
+	}
 
-        public List<string> GetBounces()
-        {
-            EmailAddress email = _configuration["MailgunConfig:from"];
-            return _cacheManager.GetOrAdd("email-bounces", _ => _httpClient.GetStringAsync($"https://api.mailgun.net/v3/{email.Domain}/bounces").ContinueWith(t =>
-             {
-                 return t.IsCompletedSuccessfully ? ((JArray)JObject.Parse(t.Result)["items"])?.Select(x => (string)x["address"]).ToList() : new List<string>();
-             }).Result);
-        }
+	public List<string> GetBounces()
+	{
+		EmailAddress email = _configuration["MailgunConfig:from"];
+		return _cacheManager.GetOrAdd("email-bounces", _ => _httpClient.GetStringAsync($"https://api.mailgun.net/v3/{email.Domain}/bounces").ContinueWith(t =>
+		{
+			return t.IsCompletedSuccessfully ? ((JArray)JObject.Parse(t.Result)["items"])?.Select(x => (string)x["address"]).ToList() : new List<string>();
+		}).Result);
+	}
 
-        public bool HasBounced(string address)
-        {
-            EmailAddress email = _configuration["MailgunConfig:from"];
-            return _bouncedCacheManager.GetOrAdd("email-bounced", _ => _httpClient.GetStringAsync($"https://api.mailgun.net/v3/{email.Domain}/bounces/{address}").ContinueWith(t => t.IsCompletedSuccessfully && JObject.Parse(t.Result).ContainsKey("error")).Result);
-        }
+	public bool HasBounced(string address)
+	{
+		EmailAddress email = _configuration["MailgunConfig:from"];
+		return _bouncedCacheManager.GetOrAdd("email-bounced", _ => _httpClient.GetStringAsync($"https://api.mailgun.net/v3/{email.Domain}/bounces/{address}").ContinueWith(t => t.IsCompletedSuccessfully && JObject.Parse(t.Result).ContainsKey("error")).Result);
+	}
 
-        public string AddRecipient(string email)
-        {
-            EmailAddress mail = _configuration["MailgunConfig:from"];
-            return _httpClient.PostAsync($"https://api.mailgun.net/v3/{mail.Domain}/bounces", new MultipartFormDataContent
-            {
-                { new StringContent(email,Encoding.UTF8), "address" },
-                { new StringContent("黑名单邮箱",Encoding.UTF8), "error" }
-            }).ContinueWith(t =>
-            {
-                var resp = t.Result;
-                if (resp.IsSuccessStatusCode)
-                {
-                    return (string)JObject.Parse(resp.Content.ReadAsStringAsync().Result)["message"];
-                }
-                return "添加失败";
-            }).Result;
-        }
-    }
-}
+	public string AddRecipient(string email)
+	{
+		EmailAddress mail = _configuration["MailgunConfig:from"];
+		return _httpClient.PostAsync($"https://api.mailgun.net/v3/{mail.Domain}/bounces", new MultipartFormDataContent
+		{
+			{ new StringContent(email,Encoding.UTF8), "address" },
+			{ new StringContent("黑名单邮箱",Encoding.UTF8), "error" }
+		}).ContinueWith(t =>
+		{
+			var resp = t.Result;
+			if (resp.IsSuccessStatusCode)
+			{
+				return (string)JObject.Parse(resp.Content.ReadAsStringAsync().Result)["message"];
+			}
+			return "添加失败";
+		}).Result;
+	}
+}

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

@@ -1,5 +1,4 @@
-using Masuit.Tools;
-using Masuit.Tools.Models;
+using Masuit.Tools.Models;
 using System.Text;
 
 namespace Masuit.MyBlogs.Core.Common.Mails;

+ 175 - 178
src/Masuit.MyBlogs.Core/Common/PerfCounter.cs

@@ -1,9 +1,6 @@
-using Masuit.MyBlogs.Core.Infrastructure;
-using Masuit.Tools;
-using Masuit.Tools.DateTimeExt;
+using Masuit.Tools.DateTimeExt;
 using Masuit.Tools.Hardware;
 using Masuit.Tools.Logging;
-using Masuit.Tools.Systems;
 using Microsoft.Extensions.DependencyInjection.Extensions;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
@@ -14,159 +11,159 @@ namespace Masuit.MyBlogs.Core.Common;
 
 public interface IPerfCounter
 {
-    public static ConcurrentLimitedQueue<PerformanceCounter> List { get; } = new(50000);
-
-    public static readonly DateTime StartTime = DateTime.Now;
-
-    public static void Init()
-    {
-        Task.Run(() =>
-        {
-            int errorCount = 0;
-            while (true)
-            {
-                try
-                {
-                    List.Enqueue(GetCurrentPerformanceCounter());
-                }
-                catch (Exception e)
-                {
-                    if (errorCount > 20)
-                    {
-                        LogManager.Error(e);
-                        break;
-                    }
-
-                    Console.ForegroundColor = ConsoleColor.Red;
-                    Console.WriteLine(e.Message);
-                    Console.ForegroundColor = ConsoleColor.White;
-                    errorCount++;
-                }
-                Thread.Sleep(5000);
-            }
-        });
-    }
-
-    public static PerformanceCounter GetCurrentPerformanceCounter()
-    {
-        var time = DateTime.Now.GetTotalMilliseconds();
-        var load = SystemInfo.CpuLoad;
-        var mem = (1 - SystemInfo.MemoryAvailable.ConvertTo<float>() / SystemInfo.PhysicalMemory.ConvertTo<float>()) * 100;
-
-        var read = SystemInfo.GetDiskData(DiskData.Read) / 1024f;
-        var write = SystemInfo.GetDiskData(DiskData.Write) / 1024;
-
-        var up = SystemInfo.GetNetData(NetData.Received) / 1024;
-        var down = SystemInfo.GetNetData(NetData.Sent) / 1024;
-        return new PerformanceCounter()
-        {
-            Time = time,
-            CpuLoad = load,
-            MemoryUsage = mem,
-            DiskRead = read,
-            DiskWrite = write,
-            Download = down,
-            Upload = up,
-            ServerIP = SystemInfo.GetLocalUsedIP(AddressFamily.InterNetwork).ToString()
-        };
-    }
-
-    IQueryable<PerformanceCounter> CreateDataSource();
-
-    void Process();
+	public static ConcurrentLimitedQueue<PerformanceCounter> List { get; } = new(50000);
+
+	public static readonly DateTime StartTime = DateTime.Now;
+
+	public static void Init()
+	{
+		Task.Run(() =>
+		{
+			int errorCount = 0;
+			while (true)
+			{
+				try
+				{
+					List.Enqueue(GetCurrentPerformanceCounter());
+				}
+				catch (Exception e)
+				{
+					if (errorCount > 20)
+					{
+						LogManager.Error(e);
+						break;
+					}
+
+					Console.ForegroundColor = ConsoleColor.Red;
+					Console.WriteLine(e.Message);
+					Console.ForegroundColor = ConsoleColor.White;
+					errorCount++;
+				}
+				Thread.Sleep(5000);
+			}
+		});
+	}
+
+	public static PerformanceCounter GetCurrentPerformanceCounter()
+	{
+		var time = DateTime.Now.GetTotalMilliseconds();
+		var load = SystemInfo.CpuLoad;
+		var mem = (1 - SystemInfo.MemoryAvailable.ConvertTo<float>() / SystemInfo.PhysicalMemory.ConvertTo<float>()) * 100;
+
+		var read = SystemInfo.GetDiskData(DiskData.Read) / 1024f;
+		var write = SystemInfo.GetDiskData(DiskData.Write) / 1024;
+
+		var up = SystemInfo.GetNetData(NetData.Received) / 1024;
+		var down = SystemInfo.GetNetData(NetData.Sent) / 1024;
+		return new PerformanceCounter()
+		{
+			Time = time,
+			CpuLoad = load,
+			MemoryUsage = mem,
+			DiskRead = read,
+			DiskWrite = write,
+			Download = down,
+			Upload = up,
+			ServerIP = SystemInfo.GetLocalUsedIP(AddressFamily.InterNetwork).ToString()
+		};
+	}
+
+	IQueryable<PerformanceCounter> CreateDataSource();
+
+	void Process();
 }
 
 public sealed class DefaultPerfCounter : IPerfCounter
 {
-    static DefaultPerfCounter()
-    {
-    }
-
-    public IQueryable<PerformanceCounter> CreateDataSource()
-    {
-        return IPerfCounter.List.AsQueryable();
-    }
-
-    public void Process()
-    {
-    }
+	static DefaultPerfCounter()
+	{
+	}
+
+	public IQueryable<PerformanceCounter> CreateDataSource()
+	{
+		return IPerfCounter.List.AsQueryable();
+	}
+
+	public void Process()
+	{
+	}
 }
 
 public sealed class PerfCounterInDatabase : IPerfCounter
 {
-    public static ConcurrentLimitedQueue<PerformanceCounter> List { get; } = new(50000);
-
-    private readonly LoggerDbContext _dbContext;
-
-    public PerfCounterInDatabase(LoggerDbContext dbContext)
-    {
-        _dbContext = dbContext;
-    }
-
-    public IQueryable<PerformanceCounter> CreateDataSource()
-    {
-        return _dbContext.Set<PerformanceCounter>();
-    }
-
-    public void Process()
-    {
-        if (Debugger.IsAttached)
-        {
-            return;
-        }
-
-        while (IPerfCounter.List.TryDequeue(out var result))
-        {
-            _dbContext.Add(result);
-        }
-
-        if (_dbContext.SaveChanges() > 0)
-        {
-            var start = DateTime.Now.AddMonths(-1).GetTotalMilliseconds();
-            _dbContext.Set<PerformanceCounter>().Where(e => e.Time < start).DeleteFromQuery();
-        }
-    }
+	public static ConcurrentLimitedQueue<PerformanceCounter> List { get; } = new(50000);
+
+	private readonly LoggerDbContext _dbContext;
+
+	public PerfCounterInDatabase(LoggerDbContext dbContext)
+	{
+		_dbContext = dbContext;
+	}
+
+	public IQueryable<PerformanceCounter> CreateDataSource()
+	{
+		return _dbContext.Set<PerformanceCounter>();
+	}
+
+	public void Process()
+	{
+		if (Debugger.IsAttached)
+		{
+			return;
+		}
+
+		while (IPerfCounter.List.TryDequeue(out var result))
+		{
+			_dbContext.Add(result);
+		}
+
+		if (_dbContext.SaveChanges() > 0)
+		{
+			var start = DateTime.Now.AddMonths(-1).GetTotalMilliseconds();
+			_dbContext.Set<PerformanceCounter>().Where(e => e.Time < start).DeleteFromQuery();
+		}
+	}
 }
 
 public sealed class PerfCounterBackService : ScheduledService
 {
-    private readonly IServiceScopeFactory _serviceScopeFactory;
-
-    public PerfCounterBackService(IServiceScopeFactory serviceScopeFactory) : base(TimeSpan.FromSeconds(5))
-    {
-        _serviceScopeFactory = serviceScopeFactory;
-    }
-
-    protected override Task ExecuteAsync()
-    {
-        using var scope = _serviceScopeFactory.CreateAsyncScope();
-        var counter = scope.ServiceProvider.GetRequiredService<IPerfCounter>();
-        counter.Process();
-        return Task.CompletedTask;
-    }
+	private readonly IServiceScopeFactory _serviceScopeFactory;
+
+	public PerfCounterBackService(IServiceScopeFactory serviceScopeFactory) : base(TimeSpan.FromSeconds(5))
+	{
+		_serviceScopeFactory = serviceScopeFactory;
+	}
+
+	protected override Task ExecuteAsync()
+	{
+		using var scope = _serviceScopeFactory.CreateAsyncScope();
+		var counter = scope.ServiceProvider.GetRequiredService<IPerfCounter>();
+		counter.Process();
+		return Task.CompletedTask;
+	}
 }
 
 public static class PerfCounterServiceExtension
 {
-    public static IServiceCollection AddPerfCounterManager(this IServiceCollection services, IConfiguration configuration)
-    {
-        IPerfCounter.Init();
-        switch (configuration["PerfCounterStorage"])
-        {
-            case "database":
-                services.AddScoped<IPerfCounter, PerfCounterInDatabase>();
-                services.TryAddScoped<PerfCounterInDatabase>();
-                break;
-
-            default:
-                services.AddSingleton<IPerfCounter, DefaultPerfCounter>();
-                break;
-        }
-
-        services.Configure<HostOptions>(options => options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore);
-        services.AddHostedService<PerfCounterBackService>();
-        return services;
-    }
+	public static IServiceCollection AddPerfCounterManager(this IServiceCollection services, IConfiguration configuration)
+	{
+		IPerfCounter.Init();
+		switch (configuration["PerfCounterStorage"])
+		{
+			case "database":
+				services.AddScoped<IPerfCounter, PerfCounterInDatabase>();
+				services.TryAddScoped<PerfCounterInDatabase>();
+				break;
+
+			default:
+				services.AddSingleton<IPerfCounter, DefaultPerfCounter>();
+				break;
+		}
+
+		services.Configure<HostOptions>(options => options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore);
+		services.AddHostedService<PerfCounterBackService>();
+		return services;
+	}
 }
 
 /// <summary>
@@ -175,41 +172,41 @@ public static class PerfCounterServiceExtension
 [Table(nameof(PerformanceCounter))]
 public sealed class PerformanceCounter
 {
-    [StringLength(128)]
-    public string ServerIP { get; set; }
-
-    /// <summary>
-    /// 当前时间戳
-    /// </summary>
-    public long Time { get; set; }
-
-    /// <summary>
-    /// CPU当前负载
-    /// </summary>
-    public float CpuLoad { get; set; }
-
-    /// <summary>
-    /// 内存使用率
-    /// </summary>
-    public float MemoryUsage { get; set; }
-
-    /// <summary>
-    /// 磁盘读
-    /// </summary>
-    public float DiskRead { get; set; }
-
-    /// <summary>
-    /// 磁盘写
-    /// </summary>
-    public float DiskWrite { get; set; }
-
-    /// <summary>
-    /// 网络上行
-    /// </summary>
-    public float Upload { get; set; }
-
-    /// <summary>
-    /// 网络下行
-    /// </summary>
-    public float Download { get; set; }
+	[StringLength(128)]
+	public string ServerIP { get; set; }
+
+	/// <summary>
+	/// 当前时间戳
+	/// </summary>
+	public long Time { get; set; }
+
+	/// <summary>
+	/// CPU当前负载
+	/// </summary>
+	public float CpuLoad { get; set; }
+
+	/// <summary>
+	/// 内存使用率
+	/// </summary>
+	public float MemoryUsage { get; set; }
+
+	/// <summary>
+	/// 磁盘读
+	/// </summary>
+	public float DiskRead { get; set; }
+
+	/// <summary>
+	/// 磁盘写
+	/// </summary>
+	public float DiskWrite { get; set; }
+
+	/// <summary>
+	/// 网络上行
+	/// </summary>
+	public float Upload { get; set; }
+
+	/// <summary>
+	/// 网络下行
+	/// </summary>
+	public float Download { get; set; }
 }

+ 313 - 314
src/Masuit.MyBlogs.Core/Common/QQWrySearcher.cs

@@ -1,4 +1,3 @@
-using Masuit.Tools;
 using System.Net;
 using System.Text;
 
@@ -9,317 +8,317 @@ namespace Masuit.MyBlogs.Core.Common;
 /// </summary>
 public sealed class QQWrySearcher : IDisposable
 {
-    private readonly SemaphoreSlim _initLock = new SemaphoreSlim(initialCount: 1, maxCount: 1);
-
-    private object _versionLock = new object();
-
-    private static readonly Encoding Gb2312Encoding;
-
-    /// <summary>
-    /// 数据库 缓存
-    /// </summary>
-    private byte[] _qqwryDbBytes;
-
-    /// <summary>
-    /// Ip索引 缓存
-    /// </summary>
-    private long[] _ipIndexCache;
-
-    /// <summary>
-    /// 起始定位
-    /// </summary>
-    private long _startPosition;
-
-    /// <summary>
-    /// 是否初始化
-    /// </summary>
-    private bool? _init;
-
-    private readonly string _dbPath;
-
-    private int? _ipCount;
-
-    private string _version;
-
-    /// <summary>
-    /// 记录总数
-    /// </summary>
-    public int IpCount
-    {
-        get
-        {
-            _ipCount ??= _ipIndexCache.Length;
-            return _ipCount.Value;
-        }
-    }
-
-    /// <summary>
-    /// 版本信息
-    /// </summary>
-    public string Version
-    {
-        get
-        {
-            if (!string.IsNullOrWhiteSpace(_version))
-            {
-                return _version;
-            }
-            lock (_versionLock)
-            {
-                if (!string.IsNullOrWhiteSpace(_version))
-                {
-                    return _version;
-                }
-                _version = GetIpLocation(IPAddress.Parse("255.255.255.255")).Network;
-                return _version;
-            }
-        }
-    }
-
-    static QQWrySearcher()
-    {
-        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
-        Gb2312Encoding = Encoding.GetEncoding("gb2312");
-    }
-
-    public QQWrySearcher(string dbPath)
-    {
-        _dbPath = dbPath;
-        Init();
-    }
-
-    /// <summary>
-    /// 初始化
-    /// </summary>
-    /// <returns></returns>
-    public bool Init(bool getNewDb = false)
-    {
-        if (_init != null && !getNewDb)
-        {
-            return _init.Value;
-        }
-        _initLock.Wait();
-        try
-        {
-            if (_init != null && !getNewDb)
-            {
-                return _init.Value;
-            }
-
-            _qqwryDbBytes = File.ReadAllBytes(_dbPath);
-            _ipIndexCache = BlockToArray(ReadIpBlock(_qqwryDbBytes, out _startPosition));
-            _ipCount = null;
-            _version = null;
-            _init = true;
-        }
-        finally
-        {
-            _initLock.Release();
-        }
-
-        if (_qqwryDbBytes == null)
-        {
-            throw new InvalidOperationException("无法打开IP数据库" + _dbPath + "!");
-        }
-
-        return true;
-
-    }
-
-    /// <summary>
-    ///  获取指定IP所在地理位置
-    /// </summary>
-    /// <param name="ip">要查询的IP地址</param>
-    /// <returns></returns>
-    public (string City, string Network) GetIpLocation(IPAddress ip)
-    {
-        if (ip.IsPrivateIP())
-        {
-            return ("内网", "内网");
-        }
-        var ipnum = IpToLong(ip);
-        return ReadLocation(ipnum, _startPosition, _ipIndexCache, _qqwryDbBytes);
-    }
-
-    /// <inheritdoc />
-    /// <summary>
-    /// 释放
-    /// </summary>
-    public void Dispose()
-    {
-        _initLock?.Dispose();
-        _versionLock = null;
-        _qqwryDbBytes = null;
-        _qqwryDbBytes = null;
-        _ipIndexCache = null;
-        _init = null;
-    }
-
-    ///<summary>
-    /// 将字符串形式的IP转换位long
-    ///</summary>
-    ///<param name="ip"></param>
-    ///<returns></returns>
-    private static long IpToLong(IPAddress ip)
-    {
-        var bytes = ip.GetAddressBytes();
-        var ipBytes = new byte[8];
-        for (var i = 0; i < 4; i++)
-        {
-            ipBytes[i] = bytes[3 - i];
-        }
-
-        return BitConverter.ToInt64(ipBytes);
-    }
-
-    ///<summary>
-    /// 将索引区字节块中的起始IP转换成Long数组
-    ///</summary>
-    ///<param name="ipBlock"></param>
-    private static long[] BlockToArray(byte[] ipBlock)
-    {
-        var ipArray = new long[ipBlock.Length / 7];
-        var ipIndex = 0;
-        var temp = new byte[8];
-        for (var i = 0; i < ipBlock.Length; i += 7)
-        {
-            Array.Copy(ipBlock, i, temp, 0, 4);
-            ipArray[ipIndex] = BitConverter.ToInt64(temp, 0);
-            ipIndex++;
-        }
-        return ipArray;
-    }
-
-    /// <summary>
-    ///  从IP数组中搜索指定IP并返回其索引
-    /// </summary>
-    /// <param name="ip"></param>
-    /// <param name="ipArray">IP数组</param>
-    /// <param name="start">指定搜索的起始位置</param>
-    /// <param name="end">指定搜索的结束位置</param>
-    /// <returns></returns>
-    private static int SearchIp(long ip, long[] ipArray, int start, int end)
-    {
-        while (true)
-        {
-            //计算中间索引
-            var middle = (start + end) / 2;
-            if (middle == start)
-            {
-                return middle;
-            }
-
-            if (ip < ipArray[middle])
-            {
-                end = middle;
-            }
-            else
-            {
-                start = middle;
-            }
-        }
-    }
-
-    ///<summary>
-    /// 读取IP文件中索引区块
-    ///</summary>
-    ///<returns></returns>
-    private static byte[] ReadIpBlock(byte[] bytes, out long startPosition)
-    {
-        long offset = 0;
-        startPosition = ReadLongX(bytes, offset, 4);
-        offset += 4;
-        var endPosition = ReadLongX(bytes, offset, 4);
-        offset = startPosition;
-        var count = (endPosition - startPosition) / 7 + 1;//总记录数
-        var ipBlock = new byte[count * 7];
-        for (var i = 0; i < ipBlock.Length; i++)
-        {
-            ipBlock[i] = bytes[offset + i];
-        }
-
-        return ipBlock;
-    }
-
-    /// <summary>
-    ///  从IP文件中读取指定字节并转换位long
-    /// </summary>
-    /// <param name="bytes"></param>
-    /// <param name="offset"></param>
-    /// <param name="bytesCount">需要转换的字节数,主意不要超过8字节</param>
-    /// <returns></returns>
-    private static long ReadLongX(byte[] bytes, long offset, int bytesCount)
-    {
-        var cBytes = new byte[8];
-        for (var i = 0; i < bytesCount; i++)
-        {
-            cBytes[i] = bytes[offset + i];
-        }
-
-        return BitConverter.ToInt64(cBytes, 0);
-    }
-
-    /// <summary>
-    ///  从IP文件中读取字符串
-    /// </summary>
-    /// <param name="bytes"></param>
-    /// <param name="flag">转向标志</param>
-    /// <param name="offset"></param>
-    /// <returns></returns>
-    private static string ReadString(byte[] bytes, int flag, ref long offset)
-    {
-        if (flag == 1 || flag == 2)//转向标志
-        {
-            offset = ReadLongX(bytes, offset, 3);
-        }
-        else
-        {
-            offset -= 1;
-        }
-
-        var list = new List<byte>();
-        var b = bytes[offset];
-        offset += 1;
-        while (b > 0)
-        {
-            list.Add(b);
-            b = bytes[offset];
-            offset += 1;
-        }
-
-        return Gb2312Encoding.GetString(list.ToArray());
-    }
-
-    private static (string City, string Network) ReadLocation(long ip, long startPosition, long[] ipIndex, byte[] qqwryDbBytes)
-    {
-        long offset = SearchIp(ip, ipIndex, 0, ipIndex.Length) * 7 + 4;
-
-        //偏移
-        var arrayOffset = startPosition + offset;
-        //跳过结束IP
-        arrayOffset = ReadLongX(qqwryDbBytes, arrayOffset, 3) + 4;
-        //读取标志
-        var flag = qqwryDbBytes[arrayOffset];
-        arrayOffset += 1;
-        //表示国家和地区被转向
-        if (flag == 1)
-        {
-            arrayOffset = ReadLongX(qqwryDbBytes, arrayOffset, 3);
-            //再读标志
-            flag = qqwryDbBytes[arrayOffset];
-            arrayOffset += 1;
-        }
-        var countryOffset = arrayOffset;
-        var city = ReadString(qqwryDbBytes, flag, ref arrayOffset);
-
-        if (flag == 2)
-        {
-            arrayOffset = countryOffset + 3;
-        }
-
-        flag = qqwryDbBytes[arrayOffset];
-        arrayOffset += 1;
-        var network = ReadString(qqwryDbBytes, flag, ref arrayOffset);
-        return (city, network);
-    }
+	private readonly SemaphoreSlim _initLock = new SemaphoreSlim(initialCount: 1, maxCount: 1);
+
+	private object _versionLock = new object();
+
+	private static readonly Encoding Gb2312Encoding;
+
+	/// <summary>
+	/// 数据库 缓存
+	/// </summary>
+	private byte[] _qqwryDbBytes;
+
+	/// <summary>
+	/// Ip索引 缓存
+	/// </summary>
+	private long[] _ipIndexCache;
+
+	/// <summary>
+	/// 起始定位
+	/// </summary>
+	private long _startPosition;
+
+	/// <summary>
+	/// 是否初始化
+	/// </summary>
+	private bool? _init;
+
+	private readonly string _dbPath;
+
+	private int? _ipCount;
+
+	private string _version;
+
+	/// <summary>
+	/// 记录总数
+	/// </summary>
+	public int IpCount
+	{
+		get
+		{
+			_ipCount ??= _ipIndexCache.Length;
+			return _ipCount.Value;
+		}
+	}
+
+	/// <summary>
+	/// 版本信息
+	/// </summary>
+	public string Version
+	{
+		get
+		{
+			if (!string.IsNullOrWhiteSpace(_version))
+			{
+				return _version;
+			}
+			lock (_versionLock)
+			{
+				if (!string.IsNullOrWhiteSpace(_version))
+				{
+					return _version;
+				}
+				_version = GetIpLocation(IPAddress.Parse("255.255.255.255")).Network;
+				return _version;
+			}
+		}
+	}
+
+	static QQWrySearcher()
+	{
+		Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+		Gb2312Encoding = Encoding.GetEncoding("gb2312");
+	}
+
+	public QQWrySearcher(string dbPath)
+	{
+		_dbPath = dbPath;
+		Init();
+	}
+
+	/// <summary>
+	/// 初始化
+	/// </summary>
+	/// <returns></returns>
+	public bool Init(bool getNewDb = false)
+	{
+		if (_init != null && !getNewDb)
+		{
+			return _init.Value;
+		}
+		_initLock.Wait();
+		try
+		{
+			if (_init != null && !getNewDb)
+			{
+				return _init.Value;
+			}
+
+			_qqwryDbBytes = File.ReadAllBytes(_dbPath);
+			_ipIndexCache = BlockToArray(ReadIpBlock(_qqwryDbBytes, out _startPosition));
+			_ipCount = null;
+			_version = null;
+			_init = true;
+		}
+		finally
+		{
+			_initLock.Release();
+		}
+
+		if (_qqwryDbBytes == null)
+		{
+			throw new InvalidOperationException("无法打开IP数据库" + _dbPath + "!");
+		}
+
+		return true;
+
+	}
+
+	/// <summary>
+	///  获取指定IP所在地理位置
+	/// </summary>
+	/// <param name="ip">要查询的IP地址</param>
+	/// <returns></returns>
+	public (string City, string Network) GetIpLocation(IPAddress ip)
+	{
+		if (ip.IsPrivateIP())
+		{
+			return ("内网", "内网");
+		}
+		var ipnum = IpToLong(ip);
+		return ReadLocation(ipnum, _startPosition, _ipIndexCache, _qqwryDbBytes);
+	}
+
+	/// <inheritdoc />
+	/// <summary>
+	/// 释放
+	/// </summary>
+	public void Dispose()
+	{
+		_initLock?.Dispose();
+		_versionLock = null;
+		_qqwryDbBytes = null;
+		_qqwryDbBytes = null;
+		_ipIndexCache = null;
+		_init = null;
+	}
+
+	///<summary>
+	/// 将字符串形式的IP转换位long
+	///</summary>
+	///<param name="ip"></param>
+	///<returns></returns>
+	private static long IpToLong(IPAddress ip)
+	{
+		var bytes = ip.GetAddressBytes();
+		var ipBytes = new byte[8];
+		for (var i = 0; i < 4; i++)
+		{
+			ipBytes[i] = bytes[3 - i];
+		}
+
+		return BitConverter.ToInt64(ipBytes);
+	}
+
+	///<summary>
+	/// 将索引区字节块中的起始IP转换成Long数组
+	///</summary>
+	///<param name="ipBlock"></param>
+	private static long[] BlockToArray(byte[] ipBlock)
+	{
+		var ipArray = new long[ipBlock.Length / 7];
+		var ipIndex = 0;
+		var temp = new byte[8];
+		for (var i = 0; i < ipBlock.Length; i += 7)
+		{
+			Array.Copy(ipBlock, i, temp, 0, 4);
+			ipArray[ipIndex] = BitConverter.ToInt64(temp, 0);
+			ipIndex++;
+		}
+		return ipArray;
+	}
+
+	/// <summary>
+	///  从IP数组中搜索指定IP并返回其索引
+	/// </summary>
+	/// <param name="ip"></param>
+	/// <param name="ipArray">IP数组</param>
+	/// <param name="start">指定搜索的起始位置</param>
+	/// <param name="end">指定搜索的结束位置</param>
+	/// <returns></returns>
+	private static int SearchIp(long ip, long[] ipArray, int start, int end)
+	{
+		while (true)
+		{
+			//计算中间索引
+			var middle = (start + end) / 2;
+			if (middle == start)
+			{
+				return middle;
+			}
+
+			if (ip < ipArray[middle])
+			{
+				end = middle;
+			}
+			else
+			{
+				start = middle;
+			}
+		}
+	}
+
+	///<summary>
+	/// 读取IP文件中索引区块
+	///</summary>
+	///<returns></returns>
+	private static byte[] ReadIpBlock(byte[] bytes, out long startPosition)
+	{
+		long offset = 0;
+		startPosition = ReadLongX(bytes, offset, 4);
+		offset += 4;
+		var endPosition = ReadLongX(bytes, offset, 4);
+		offset = startPosition;
+		var count = (endPosition - startPosition) / 7 + 1;//总记录数
+		var ipBlock = new byte[count * 7];
+		for (var i = 0; i < ipBlock.Length; i++)
+		{
+			ipBlock[i] = bytes[offset + i];
+		}
+
+		return ipBlock;
+	}
+
+	/// <summary>
+	///  从IP文件中读取指定字节并转换位long
+	/// </summary>
+	/// <param name="bytes"></param>
+	/// <param name="offset"></param>
+	/// <param name="bytesCount">需要转换的字节数,主意不要超过8字节</param>
+	/// <returns></returns>
+	private static long ReadLongX(byte[] bytes, long offset, int bytesCount)
+	{
+		var cBytes = new byte[8];
+		for (var i = 0; i < bytesCount; i++)
+		{
+			cBytes[i] = bytes[offset + i];
+		}
+
+		return BitConverter.ToInt64(cBytes, 0);
+	}
+
+	/// <summary>
+	///  从IP文件中读取字符串
+	/// </summary>
+	/// <param name="bytes"></param>
+	/// <param name="flag">转向标志</param>
+	/// <param name="offset"></param>
+	/// <returns></returns>
+	private static string ReadString(byte[] bytes, int flag, ref long offset)
+	{
+		if (flag == 1 || flag == 2)//转向标志
+		{
+			offset = ReadLongX(bytes, offset, 3);
+		}
+		else
+		{
+			offset -= 1;
+		}
+
+		var list = new List<byte>();
+		var b = bytes[offset];
+		offset += 1;
+		while (b > 0)
+		{
+			list.Add(b);
+			b = bytes[offset];
+			offset += 1;
+		}
+
+		return Gb2312Encoding.GetString(list.ToArray());
+	}
+
+	private static (string City, string Network) ReadLocation(long ip, long startPosition, long[] ipIndex, byte[] qqwryDbBytes)
+	{
+		long offset = SearchIp(ip, ipIndex, 0, ipIndex.Length) * 7 + 4;
+
+		//偏移
+		var arrayOffset = startPosition + offset;
+		//跳过结束IP
+		arrayOffset = ReadLongX(qqwryDbBytes, arrayOffset, 3) + 4;
+		//读取标志
+		var flag = qqwryDbBytes[arrayOffset];
+		arrayOffset += 1;
+		//表示国家和地区被转向
+		if (flag == 1)
+		{
+			arrayOffset = ReadLongX(qqwryDbBytes, arrayOffset, 3);
+			//再读标志
+			flag = qqwryDbBytes[arrayOffset];
+			arrayOffset += 1;
+		}
+		var countryOffset = arrayOffset;
+		var city = ReadString(qqwryDbBytes, flag, ref arrayOffset);
+
+		if (flag == 2)
+		{
+			arrayOffset = countryOffset + 3;
+		}
+
+		flag = qqwryDbBytes[arrayOffset];
+		arrayOffset += 1;
+		var network = ReadString(qqwryDbBytes, flag, ref arrayOffset);
+		return (city, network);
+	}
 }

+ 51 - 54
src/Masuit.MyBlogs.Core/Common/TrackData.cs

@@ -1,59 +1,56 @@
-using Masuit.Tools;
-using Newtonsoft.Json;
+using Newtonsoft.Json;
 using System.Collections.Concurrent;
 using System.Text;
-using Masuit.Tools.Systems;
 
-namespace Masuit.MyBlogs.Core.Common
+namespace Masuit.MyBlogs.Core.Common;
+
+public static class TrackData
 {
-    public static class TrackData
-    {
-        /// <summary>
-        /// 请求日志
-        /// </summary>
-        public static ConcurrentDictionary<string, RequestLog> RequestLogs { get; } = new();
-
-        /// <summary>
-        /// 刷写日志
-        /// </summary>
-        public static void DumpLog()
-        {
-            if (RequestLogs.Count > 0)
-            {
-                var logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "req.txt").CreateFileIfNotExist();
-                File.WriteAllLines(logPath, RequestLogs.Values.SelectMany(g => g.RequestUrls).GroupBy(s => s).ToDictionary(x => x.Key, x => x.Count()).OrderBy(x => x.Key).ThenByDescending(x => x.Value).Select(g => g.Value + "\t" + g.Key), Encoding.UTF8);
-                File.AppendAllLines(logPath, new[] { "", $"累计处理请求数:{RequestLogs.Sum(kv => kv.Value.Count)}" });
-
-                logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "ua.txt").CreateFileIfNotExist();
-                File.WriteAllLines(logPath, RequestLogs.Values.SelectMany(g => g.UserAgents).Where(s => !string.IsNullOrEmpty(s)).Select(UserAgent.Parse).Where(ua => !(ua.IsBrowser || ua.IsMobile)).GroupBy(s => s.ToString()).ToDictionary(x => x.Key, x => x.Count()).OrderBy(x => x.Key).ThenByDescending(x => x.Value).Select(g => g.Value + "\t" + g.Key), Encoding.UTF8);
-
-                logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "raw.json").CreateFileIfNotExist();
-                File.WriteAllText(logPath, RequestLogs.ToJsonString(new JsonSerializerSettings() { Formatting = Formatting.Indented }), Encoding.UTF8);
-
-                logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "ip.txt").CreateFileIfNotExist();
-                File.WriteAllLines(logPath, RequestLogs.Keys.Select(s => new { s, loc = s.GetIPLocation().ToString() }).OrderBy(x => x.loc).Select(g => g.s + "\t" + g.loc), Encoding.UTF8);
-                RequestLogs.Clear();
-            }
-        }
-
-        private static string CreateFileIfNotExist(this string filepath)
-        {
-            var fileInfo = new FileInfo(filepath);
-            if (!fileInfo.Exists)
-            {
-                fileInfo.Directory.Create();
-            }
-
-            return filepath;
-        }
-    }
-
-    public class RequestLog
-    {
-        public ConcurrentHashSet<string> UserAgents { get; } = new();
-
-        public ConcurrentHashSet<string> RequestUrls { get; } = new();
-
-        public int Count { get; set; }
-    }
+	/// <summary>
+	/// 请求日志
+	/// </summary>
+	public static ConcurrentDictionary<string, RequestLog> RequestLogs { get; } = new();
+
+	/// <summary>
+	/// 刷写日志
+	/// </summary>
+	public static void DumpLog()
+	{
+		if (RequestLogs.Count > 0)
+		{
+			var logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "req.txt").CreateFileIfNotExist();
+			File.WriteAllLines(logPath, RequestLogs.Values.SelectMany(g => g.RequestUrls).GroupBy(s => s).ToDictionary(x => x.Key, x => x.Count()).OrderBy(x => x.Key).ThenByDescending(x => x.Value).Select(g => g.Value + "\t" + g.Key), Encoding.UTF8);
+			File.AppendAllLines(logPath, new[] { "", $"累计处理请求数:{RequestLogs.Sum(kv => kv.Value.Count)}" });
+
+			logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "ua.txt").CreateFileIfNotExist();
+			File.WriteAllLines(logPath, RequestLogs.Values.SelectMany(g => g.UserAgents).Where(s => !string.IsNullOrEmpty(s)).Select(UserAgent.Parse).Where(ua => !(ua.IsBrowser || ua.IsMobile)).GroupBy(s => s.ToString()).ToDictionary(x => x.Key, x => x.Count()).OrderBy(x => x.Key).ThenByDescending(x => x.Value).Select(g => g.Value + "\t" + g.Key), Encoding.UTF8);
+
+			logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "raw.json").CreateFileIfNotExist();
+			File.WriteAllText(logPath, RequestLogs.ToJsonString(new JsonSerializerSettings() { Formatting = Formatting.Indented }), Encoding.UTF8);
+
+			logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "ip.txt").CreateFileIfNotExist();
+			File.WriteAllLines(logPath, RequestLogs.Keys.Select(s => new { s, loc = s.GetIPLocation().ToString() }).OrderBy(x => x.loc).Select(g => g.s + "\t" + g.loc), Encoding.UTF8);
+			RequestLogs.Clear();
+		}
+	}
+
+	private static string CreateFileIfNotExist(this string filepath)
+	{
+		var fileInfo = new FileInfo(filepath);
+		if (!fileInfo.Exists)
+		{
+			fileInfo.Directory.Create();
+		}
+
+		return filepath;
+	}
 }
+
+public class RequestLog
+{
+	public ConcurrentHashSet<string> UserAgents { get; } = new();
+
+	public ConcurrentHashSet<string> RequestUrls { get; } = new();
+
+	public int Count { get; set; }
+}

+ 285 - 288
src/Masuit.MyBlogs.Core/Common/UserAgent.cs

@@ -1,315 +1,312 @@
 using Microsoft.Extensions.Caching.Memory;
 using System.Text.RegularExpressions;
 
-namespace Masuit.MyBlogs.Core.Common
+namespace Masuit.MyBlogs.Core.Common;
+
+public sealed class UserAgent
 {
-    public sealed class UserAgent
-    {
-        private static readonly IMemoryCache Cache = new MemoryCache(new MemoryCacheOptions());
+	private static readonly IMemoryCache Cache = new MemoryCache(new MemoryCacheOptions());
 
-        internal static readonly Dictionary<string, string> Platforms = new Dictionary<string, string>() {
-            {"windows nt 10.0", "Windows 10/11"},
-            {"windows nt 6.3", "Windows 8.1"},
-            {"windows nt 6.2", "Windows 8"},
-            {"windows nt 6.1", "Windows 7"},
-            {"windows nt 6.0", "Windows Vista"},
-            {"windows nt 5.2", "Windows 2003"},
-            {"windows nt 5.1", "Windows XP"},
-            {"windows nt 5.0", "Windows 2000"},
-            {"windows nt 4.0", "Windows NT 4.0"},
-            {"winnt4.0", "Windows NT 4.0"},
-            {"winnt 4.0", "Windows NT"},
-            {"winnt", "Windows NT"},
-            {"windows 98", "Windows 98"},
-            {"win98", "Windows 98"},
-            {"windows 95", "Windows 95"},
-            {"win95", "Windows 95"},
-            {"windows phone", "Windows Phone"},
-            {"windows", "Unknown Windows OS"},
-            {"android", "Android"},
-            {"blackberry", "BlackBerry"},
-            {"iphone", "iOS"},
-            {"ipad", "iOS"},
-            {"ipod", "iOS"},
-            {"os x", "Mac OS X"},
-            {"ppc mac", "Power PC Mac"},
-            {"freebsd", "FreeBSD"},
-            {"ppc", "Macintosh"},
-            {"linux", "Linux"},
-            {"debian", "Debian"},
-            {"sunos", "Sun Solaris"},
-            {"beos", "BeOS"},
-            {"apachebench", "ApacheBench"},
-            {"aix", "AIX"},
-            {"irix", "Irix"},
-            {"osf", "DEC OSF"},
-            {"hp-ux", "HP-UX"},
-            {"netbsd", "NetBSD"},
-            {"bsdi", "BSDi"},
-            {"openbsd", "OpenBSD"},
-            {"gnu", "GNU/Linux"},
-            {"unix", "Unknown Unix OS"},
-            {"symbian", "Symbian OS"},
-        };
+	internal static readonly Dictionary<string, string> Platforms = new() {
+		{"windows nt 10.0", "Windows 10/11"},
+		{"windows nt 6.3", "Windows 8.1"},
+		{"windows nt 6.2", "Windows 8"},
+		{"windows nt 6.1", "Windows 7"},
+		{"windows nt 6.0", "Windows Vista"},
+		{"windows nt 5.2", "Windows 2003"},
+		{"windows nt 5.1", "Windows XP"},
+		{"windows nt 5.0", "Windows 2000"},
+		{"windows nt 4.0", "Windows NT 4.0"},
+		{"winnt4.0", "Windows NT 4.0"},
+		{"winnt 4.0", "Windows NT"},
+		{"winnt", "Windows NT"},
+		{"windows 98", "Windows 98"},
+		{"win98", "Windows 98"},
+		{"windows 95", "Windows 95"},
+		{"win95", "Windows 95"},
+		{"windows phone", "Windows Phone"},
+		{"windows", "Unknown Windows OS"},
+		{"android", "Android"},
+		{"blackberry", "BlackBerry"},
+		{"iphone", "iOS"},
+		{"ipad", "iOS"},
+		{"ipod", "iOS"},
+		{"os x", "Mac OS X"},
+		{"ppc mac", "Power PC Mac"},
+		{"freebsd", "FreeBSD"},
+		{"ppc", "Macintosh"},
+		{"linux", "Linux"},
+		{"debian", "Debian"},
+		{"sunos", "Sun Solaris"},
+		{"beos", "BeOS"},
+		{"apachebench", "ApacheBench"},
+		{"aix", "AIX"},
+		{"irix", "Irix"},
+		{"osf", "DEC OSF"},
+		{"hp-ux", "HP-UX"},
+		{"netbsd", "NetBSD"},
+		{"bsdi", "BSDi"},
+		{"openbsd", "OpenBSD"},
+		{"gnu", "GNU/Linux"},
+		{"unix", "Unknown Unix OS"},
+		{"symbian", "Symbian OS"},
+	};
 
-        internal static readonly Dictionary<string, string> Browsers = new Dictionary<string, string>()
-        {
-            {"OPR", "Opera"},
-            {"Flock", "Flock"},
-            {"Edge", "Spartan"},
-            {"MQQ", "手机QQ浏览器"},
-            {"QQ", "QQ浏览器"},
-            {"MicroMessenger", "微信内置浏览器"},
-            {"Baidu", "百度浏览器"},
-            {"Chrome", "Chrome"},
-            {"Opera.*?Version", "Opera"},
-            {"Opera", "Opera"},
-            {"MSIE", "Internet Explorer"},
-            {"Internet Explorer", "Internet Explorer"},
-            {"Trident.* rv" , "Internet Explorer"},
-            {"Shiira", "Shiira"},
-            {"Firefox", "Firefox"},
-            {"Chimera", "Chimera"},
-            {"Phoenix", "Phoenix"},
-            {"Firebird", "Firebird"},
-            {"Camino", "Camino"},
-            {"Netscape", "Netscape"},
-            {"OmniWeb", "OmniWeb"},
-            {"Safari", "Safari"},
-            {"Mozilla", "Mozilla"},
-            {"Konqueror", "Konqueror"},
-            {"icab", "iCab"},
-            {"Lynx", "Lynx"},
-            {"Links", "Links"},
-            {"hotjava", "HotJava"},
-            {"amaya", "Amaya"},
-            {"IBrowse", "IBrowse"},
-            {"Maxthon", "Maxthon"},
-            {"Ubuntu", "Ubuntu Web Browser"},
-        };
+	internal static readonly Dictionary<string, string> Browsers = new()
+	{
+		{"OPR", "Opera"},
+		{"Flock", "Flock"},
+		{"Edge", "Spartan"},
+		{"MQQ", "手机QQ浏览器"},
+		{"QQ", "QQ浏览器"},
+		{"MicroMessenger", "微信内置浏览器"},
+		{"Baidu", "百度浏览器"},
+		{"Chrome", "Chrome"},
+		{"Opera.*?Version", "Opera"},
+		{"Opera", "Opera"},
+		{"MSIE", "Internet Explorer"},
+		{"Internet Explorer", "Internet Explorer"},
+		{"Trident.* rv" , "Internet Explorer"},
+		{"Shiira", "Shiira"},
+		{"Firefox", "Firefox"},
+		{"Chimera", "Chimera"},
+		{"Phoenix", "Phoenix"},
+		{"Firebird", "Firebird"},
+		{"Camino", "Camino"},
+		{"Netscape", "Netscape"},
+		{"OmniWeb", "OmniWeb"},
+		{"Safari", "Safari"},
+		{"Mozilla", "Mozilla"},
+		{"Konqueror", "Konqueror"},
+		{"icab", "iCab"},
+		{"Lynx", "Lynx"},
+		{"Links", "Links"},
+		{"hotjava", "HotJava"},
+		{"amaya", "Amaya"},
+		{"IBrowse", "IBrowse"},
+		{"Maxthon", "Maxthon"},
+		{"Ubuntu", "Ubuntu Web Browser"},
+	};
 
-        internal static readonly Dictionary<string, string> Mobiles = new Dictionary<string, string>()
-        {
-            // Legacy
-            {"mobileexplorer", "Mobile Explorer"},
-            {"palmsource", "Palm"},
-            {"palmscape", "Palmscape"},
-            // Phones and Manufacturers
-            {"motorola", "Motorola"},
-            {"nokia", "Nokia"},
-            {"palm", "Palm"},
-            {"iphone", "Apple iPhone"},
-            {"ipad", "iPad"},
-            {"ipod", "Apple iPod Touch"},
-            {"sony", "Sony Ericsson"},
-            {"ericsson", "Sony Ericsson"},
-            {"blackberry", "BlackBerry"},
-            {"cocoon", "O2 Cocoon"},
-            {"blazer", "Treo"},
-            {"lg", "LG"},
-            {"amoi", "Amoi"},
-            {"xda", "XDA"},
-            {"mda", "MDA"},
-            {"vario", "Vario"},
-            {"htc", "HTC"},
-            {"samsung", "Samsung"},
-            {"sharp", "Sharp"},
-            {"sie-", "Siemens"},
-            {"alcatel", "Alcatel"},
-            {"benq", "BenQ"},
-            {"ipaq", "HP iPaq"},
-            {"mot-", "Motorola"},
-            {"playstation portable", "PlayStation Portable"},
-            {"playstation 3", "PlayStation 3"},
-            {"playstation vita", "PlayStation Vita"},
-            {"hiptop", "Danger Hiptop"},
-            {"nec-", "NEC"},
-            {"panasonic", "Panasonic"},
-            {"philips", "Philips"},
-            {"sagem", "Sagem"},
-            {"sanyo", "Sanyo"},
-            {"spv", "SPV"},
-            {"zte", "ZTE"},
-            {"sendo", "Sendo"},
-            {"nintendo dsi", "Nintendo DSi"},
-            {"nintendo ds", "Nintendo DS"},
-            {"nintendo 3ds", "Nintendo 3DS"},
-            {"wii", "Nintendo Wii"},
-            {"open web", "Open Web"},
-            {"openweb", "OpenWeb"},
-            {"vivo", "Vivo"},
-            {"oppo", "OPPO"},
-            {"xiaomi", "小米"},
-            {"miui", "小米"},
-            {"SKR-", "小米黑鲨"},
-            {"huawei", "华为"},
-            {"HONOR", "华为荣耀"},
-            {"ONEPLUS", "一加"},
-            {"GM19", "一加"},
-            {"Nexus", "Nexus"},
-            {"ASUS", "ASUS"},
+	internal static readonly Dictionary<string, string> Mobiles = new()
+	{
+		// Legacy
+		{"mobileexplorer", "Mobile Explorer"},
+		{"palmsource", "Palm"},
+		{"palmscape", "Palmscape"},
+		// Phones and Manufacturers
+		{"motorola", "Motorola"},
+		{"nokia", "Nokia"},
+		{"palm", "Palm"},
+		{"iphone", "Apple iPhone"},
+		{"ipad", "iPad"},
+		{"ipod", "Apple iPod Touch"},
+		{"sony", "Sony Ericsson"},
+		{"ericsson", "Sony Ericsson"},
+		{"blackberry", "BlackBerry"},
+		{"cocoon", "O2 Cocoon"},
+		{"blazer", "Treo"},
+		{"lg", "LG"},
+		{"amoi", "Amoi"},
+		{"xda", "XDA"},
+		{"mda", "MDA"},
+		{"vario", "Vario"},
+		{"htc", "HTC"},
+		{"samsung", "Samsung"},
+		{"sharp", "Sharp"},
+		{"sie-", "Siemens"},
+		{"alcatel", "Alcatel"},
+		{"benq", "BenQ"},
+		{"ipaq", "HP iPaq"},
+		{"mot-", "Motorola"},
+		{"playstation portable", "PlayStation Portable"},
+		{"playstation 3", "PlayStation 3"},
+		{"playstation vita", "PlayStation Vita"},
+		{"hiptop", "Danger Hiptop"},
+		{"nec-", "NEC"},
+		{"panasonic", "Panasonic"},
+		{"philips", "Philips"},
+		{"sagem", "Sagem"},
+		{"sanyo", "Sanyo"},
+		{"spv", "SPV"},
+		{"zte", "ZTE"},
+		{"sendo", "Sendo"},
+		{"nintendo dsi", "Nintendo DSi"},
+		{"nintendo ds", "Nintendo DS"},
+		{"nintendo 3ds", "Nintendo 3DS"},
+		{"wii", "Nintendo Wii"},
+		{"open web", "Open Web"},
+		{"openweb", "OpenWeb"},
+		{"vivo", "Vivo"},
+		{"oppo", "OPPO"},
+		{"xiaomi", "小米"},
+		{"miui", "小米"},
+		{"SKR-", "小米黑鲨"},
+		{"huawei", "华为"},
+		{"HONOR", "华为荣耀"},
+		{"ONEPLUS", "一加"},
+		{"GM19", "一加"},
+		{"Nexus", "Nexus"},
+		{"ASUS", "ASUS"},
 
-            // Operating Systems
-            {"android", "Android"},
-            {"symbian", "Symbian"},
-            {"SymbianOS", "SymbianOS"},
-            {"elaine", "Palm"},
-            {"series60", "Symbian S60"},
-            {"windows ce", "Windows CE"},
-            // Browsers
-            {"obigo", "Obigo"},
-            {"netfront", "Netfront Browser"},
-            {"openwave", "Openwave Browser"},
-            {"mobilexplorer", "Mobile Explorer"},
-            {"operamini", "Opera Mini"},
-            {"opera mini", "Opera Mini"},
-            {"opera mobi", "Opera Mobile"},
-            {"fennec", "Firefox Mobile"},
-            // Other
-            {"digital paths", "Digital Paths"},
-            {"avantgo", "AvantGo"},
-            {"xiino", "Xiino"},
-            {"novarra", "Novarra Transcoder"},
-            {"vodafone", "Vodafone"},
-            {"docomo", "NTT DoCoMo"},
-            {"o2", "O2"},
-            // Fallback
-            {"mobile", "Generic Mobile"},
-            {"wireless", "Generic Mobile"},
-            {"j2me", "Generic Mobile"},
-            {"midp", "Generic Mobile"},
-            {"cldc", "Generic Mobile"},
-            {"up.link", "Generic Mobile"},
-            {"up.browser", "Generic Mobile"},
-            {"smartphone", "Generic Mobile"},
-            {"cellphone", "Generic Mobile"},
-        };
+		// Operating Systems
+		{"android", "Android"},
+		{"symbian", "Symbian"},
+		{"SymbianOS", "SymbianOS"},
+		{"elaine", "Palm"},
+		{"series60", "Symbian S60"},
+		{"windows ce", "Windows CE"},
+		// Browsers
+		{"obigo", "Obigo"},
+		{"netfront", "Netfront Browser"},
+		{"openwave", "Openwave Browser"},
+		{"mobilexplorer", "Mobile Explorer"},
+		{"operamini", "Opera Mini"},
+		{"opera mini", "Opera Mini"},
+		{"opera mobi", "Opera Mobile"},
+		{"fennec", "Firefox Mobile"},
+		// Other
+		{"digital paths", "Digital Paths"},
+		{"avantgo", "AvantGo"},
+		{"xiino", "Xiino"},
+		{"novarra", "Novarra Transcoder"},
+		{"vodafone", "Vodafone"},
+		{"docomo", "NTT DoCoMo"},
+		{"o2", "O2"},
+		// Fallback
+		{"mobile", "Generic Mobile"},
+		{"wireless", "Generic Mobile"},
+		{"j2me", "Generic Mobile"},
+		{"midp", "Generic Mobile"},
+		{"cldc", "Generic Mobile"},
+		{"up.link", "Generic Mobile"},
+		{"up.browser", "Generic Mobile"},
+		{"smartphone", "Generic Mobile"},
+		{"cellphone", "Generic Mobile"},
+	};
 
-        internal static readonly Dictionary<string, string> Robots = new Dictionary<string, string>()
-        {
-            {"googlebot", "Googlebot"},
-            {"applebot", "AppleBot"},
-            {"msnbot", "MSNBot"},
-            {"baiduspider", "Baiduspider"},
-            {"bingbot", "Bing"},
-            {"slurp", "Inktomi Slurp"},
-            {"yahoo", "Yahoo!"},
-            {"ask jeeves", "Ask Jeeves"},
-            {"fastcrawler", "FastCrawler"},
-            {"infoseek", "InfoSeek Robot 1.0"},
-            {"lycos", "Lycos"},
-            {"yandex", "YandexBot"},
-            {"mediapartners-google", "MediaPartners Google"},
-            {"CRAZYWEBCRAWLER", "Crazy Webcrawler"},
-            {"adsbot-google", "AdsBot Google"},
-            {"feedfetcher-google", "Feedfetcher Google"},
-            {"curious george", "Curious George"},
-            {"ia_archiver", "Alexa Crawler"},
-            {"MJ12bot", "Majestic-12"},
-            {"Uptimebot", "Uptimebot"},
-            {"Sogou web spider", "Sogou Web Spider"},
-            {"TelegramBot", "Telegram Bot"},
-            {"DNSPod", "DNSPod"}
-        };
+	internal static readonly Dictionary<string, string> Robots = new()
+	{
+		{"googlebot", "Googlebot"},
+		{"applebot", "AppleBot"},
+		{"msnbot", "MSNBot"},
+		{"baiduspider", "Baiduspider"},
+		{"bingbot", "Bing"},
+		{"slurp", "Inktomi Slurp"},
+		{"yahoo", "Yahoo!"},
+		{"ask jeeves", "Ask Jeeves"},
+		{"fastcrawler", "FastCrawler"},
+		{"infoseek", "InfoSeek Robot 1.0"},
+		{"lycos", "Lycos"},
+		{"yandex", "YandexBot"},
+		{"mediapartners-google", "MediaPartners Google"},
+		{"CRAZYWEBCRAWLER", "Crazy Webcrawler"},
+		{"adsbot-google", "AdsBot Google"},
+		{"feedfetcher-google", "Feedfetcher Google"},
+		{"curious george", "Curious George"},
+		{"ia_archiver", "Alexa Crawler"},
+		{"MJ12bot", "Majestic-12"},
+		{"Uptimebot", "Uptimebot"},
+		{"Sogou web spider", "Sogou Web Spider"},
+		{"TelegramBot", "Telegram Bot"},
+		{"DNSPod", "DNSPod"}
+	};
 
-        protected string agent;
+	protected string agent;
 
-        public bool IsBrowser { get; set; }
+	public bool IsBrowser { get; set; }
 
-        public bool IsRobot { get; set; }
+	public bool IsRobot { get; set; }
 
-        public bool IsMobile { get; set; }
+	public bool IsMobile { get; set; }
 
-        // Current values
-        public string Platform { get; set; } = "";
+	// Current values
+	public string Platform { get; set; } = "";
 
-        public string Browser { get; set; } = "";
+	public string Browser { get; set; } = "";
 
-        public string BrowserVersion { get; set; } = "";
+	public string BrowserVersion { get; set; } = "";
 
-        public string Mobile { get; set; } = "";
+	public string Mobile { get; set; } = "";
 
-        public string Robot { get; set; } = "";
+	public string Robot { get; set; } = "";
 
-        internal UserAgent(string userAgentString = null)
-        {
-            if (userAgentString != null)
-            {
-                agent = userAgentString.Length > 512 ? userAgentString[..512] : userAgentString;
-                SetPlatform();
-                if (SetRobot()) return;
-                if (SetBrowser()) return;
-            }
-        }
+	internal UserAgent(string userAgentString = null)
+	{
+		if (userAgentString != null)
+		{
+			agent = userAgentString.Length > 512 ? userAgentString[..512] : userAgentString;
+			SetPlatform();
+			if (SetRobot()) return;
+			if (SetBrowser()) return;
+		}
+	}
 
-        internal bool SetPlatform()
-        {
-            foreach (var item in Platforms.Where(item => Regex.IsMatch(agent, $"{Regex.Escape(item.Key)}", RegexOptions.IgnoreCase)))
-            {
-                Platform = item.Value;
-                return true;
-            }
-            Platform = "Unknown Platform";
-            return false;
-        }
+	internal bool SetPlatform()
+	{
+		foreach (var item in Platforms.Where(item => Regex.IsMatch(agent, $"{Regex.Escape(item.Key)}", RegexOptions.IgnoreCase)))
+		{
+			Platform = item.Value;
+			return true;
+		}
+		Platform = "Unknown Platform";
+		return false;
+	}
 
-        internal bool SetBrowser()
-        {
-            foreach (var item in Browsers)
-            {
-                var match = Regex.Match(agent, $@"{item.Key}.*?([0-9\.]+)", RegexOptions.IgnoreCase);
-                if (match.Success)
-                {
-                    IsBrowser = true;
-                    BrowserVersion = match.Groups[1].Value;
-                    Browser = item.Value;
-                    SetMobile();
-                    return true;
-                }
-            }
-            return false;
-        }
+	internal bool SetBrowser()
+	{
+		foreach (var item in Browsers)
+		{
+			var match = Regex.Match(agent, $@"{item.Key}.*?([0-9\.]+)", RegexOptions.IgnoreCase);
+			if (match.Success)
+			{
+				IsBrowser = true;
+				BrowserVersion = match.Groups[1].Value;
+				Browser = item.Value;
+				SetMobile();
+				return true;
+			}
+		}
+		return false;
+	}
 
-        internal bool SetRobot()
-        {
-            foreach (var item in Robots.Where(item => Regex.IsMatch(agent, $"{Regex.Escape(item.Key)}", RegexOptions.IgnoreCase)))
-            {
-                IsRobot = true;
-                Robot = item.Value;
-                SetMobile();
-                return true;
-            }
+	internal bool SetRobot()
+	{
+		foreach (var item in Robots.Where(item => Regex.IsMatch(agent, $"{Regex.Escape(item.Key)}", RegexOptions.IgnoreCase)))
+		{
+			IsRobot = true;
+			Robot = item.Value;
+			SetMobile();
+			return true;
+		}
 
-            return false;
-        }
+		return false;
+	}
 
-        internal bool SetMobile()
-        {
-            foreach (var item in Mobiles.Where(item => agent.IndexOf(item.Key, StringComparison.OrdinalIgnoreCase) != -1))
-            {
-                IsMobile = true;
-                Mobile = item.Value;
-                return true;
-            }
+	internal bool SetMobile()
+	{
+		foreach (var item in Mobiles.Where(item => agent.IndexOf(item.Key, StringComparison.OrdinalIgnoreCase) != -1))
+		{
+			IsMobile = true;
+			Mobile = item.Value;
+			return true;
+		}
 
-            return false;
-        }
+		return false;
+	}
 
-        /// <summary>Returns a string that represents the current object.</summary>
-        /// <returns>A string that represents the current object.</returns>
-        public override string ToString()
-        {
-            return agent;
-        }
+	public override string ToString()
+	{
+		return agent;
+	}
 
-        public static UserAgent Parse(string userAgentString)
-        {
-            return Cache.GetOrCreate(userAgentString, entry =>
-            {
-                entry.SlidingExpiration = TimeSpan.FromHours(1);
-                entry.Size = 1;
-                return new UserAgent(userAgentString);
-            });
-        }
-    }
-}
+	public static UserAgent Parse(string userAgentString)
+	{
+		return Cache.GetOrCreate(userAgentString, entry =>
+		{
+			entry.SlidingExpiration = TimeSpan.FromHours(1);
+			entry.Size = 1;
+			return new UserAgent(userAgentString);
+		});
+	}
+}

+ 30 - 31
src/Masuit.MyBlogs.Core/Configs/AppConfig.cs

@@ -1,39 +1,38 @@
-namespace Masuit.MyBlogs.Core.Configs
+namespace Masuit.MyBlogs.Core.Configs;
+
+/// <summary>
+/// 应用程序配置
+/// </summary>
+public class AppConfig
 {
-    /// <summary>
-    /// 应用程序配置
-    /// </summary>
-    public class AppConfig
-    {
-        /// <summary>
-        /// 数据库连接字符串
-        /// </summary>
-        public static string ConnString { get; set; }
+	/// <summary>
+	/// 数据库连接字符串
+	/// </summary>
+	public static string ConnString { get; set; }
 
-        /// <summary>
-        /// 百度AK
-        /// </summary>
-        public static string BaiduAK { get; set; }
+	/// <summary>
+	/// 百度AK
+	/// </summary>
+	public static string BaiduAK { get; set; }
 
-        /// <summary>
-        /// Redis连接字符串
-        /// </summary>
-        public static string Redis { get; set; }
+	/// <summary>
+	/// Redis连接字符串
+	/// </summary>
+	public static string Redis { get; set; }
 
-        /// <summary>
-        /// gitlab图床配置
-        /// </summary>
-        public static List<GitlabConfig> GitlabConfigs { get; set; } = new List<GitlabConfig>();
+	/// <summary>
+	/// gitlab图床配置
+	/// </summary>
+	public static List<GitlabConfig> GitlabConfigs { get; set; } = new List<GitlabConfig>();
 
-        /// <summary>
-        /// 真实客户端IP转发标头
-        /// </summary>
-        public static string TrueClientIPHeader { get; set; }
+	/// <summary>
+	/// 真实客户端IP转发标头
+	/// </summary>
+	public static string TrueClientIPHeader { get; set; }
 
-        /// <summary>
-        /// 是否允许IP直接访问 
-        /// </summary>
-        public static bool EnableIPDirect { get; set; }
+	/// <summary>
+	/// 是否允许IP直接访问 
+	/// </summary>
+	public static bool EnableIPDirect { get; set; }
 
-    }
 }

+ 9 - 10
src/Masuit.MyBlogs.Core/Configs/AutofacModule.cs

@@ -2,14 +2,13 @@
 using Masuit.MyBlogs.Core.Extensions.Hangfire;
 using System.Reflection;
 
-namespace Masuit.MyBlogs.Core.Configs
+namespace Masuit.MyBlogs.Core.Configs;
+
+public sealed class AutofacModule : Autofac.Module
 {
-    public sealed class AutofacModule : Autofac.Module
-    {
-        protected override void Load(ContainerBuilder builder)
-        {
-            builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()).AsImplementedInterfaces().Where(t => t.Name.EndsWith("Repository") || t.Name.EndsWith("Service") || t.Name.EndsWith("Controller") || t.Name.EndsWith("Attribute")).PropertiesAutowired().AsSelf().InstancePerDependency();
-            builder.RegisterType<HangfireBackJob>().As<IHangfireBackJob>().InstancePerDependency();
-        }
-    }
-}
+	protected override void Load(ContainerBuilder builder)
+	{
+		builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()).AsImplementedInterfaces().Where(t => t.Name.EndsWith("Repository") || t.Name.EndsWith("Service") || t.Name.EndsWith("Controller") || t.Name.EndsWith("Attribute")).PropertiesAutowired().AsSelf().InstancePerDependency();
+		builder.RegisterType<HangfireBackJob>().As<IHangfireBackJob>().InstancePerDependency();
+	}
+}

+ 9 - 10
src/Masuit.MyBlogs.Core/Configs/GitlabConfig.cs

@@ -1,13 +1,12 @@
-namespace Masuit.MyBlogs.Core.Configs
+namespace Masuit.MyBlogs.Core.Configs;
+
+public sealed class GitlabConfig
 {
-    public sealed class GitlabConfig
-    {
-        public bool Enabled { get; set; }
+	public bool Enabled { get; set; }
 
-        public string ApiUrl { get; set; }
-        public string RawUrl { get; set; }
-        public string AccessToken { get; set; }
-        public string Branch { get; set; }
-        public int FileLimitSize { get; set; }
-    }
+	public string ApiUrl { get; set; }
+	public string RawUrl { get; set; }
+	public string AccessToken { get; set; }
+	public string Branch { get; set; }
+	public int FileLimitSize { get; set; }
 }

+ 81 - 88
src/Masuit.MyBlogs.Core/Configs/MappingProfile.cs

@@ -1,90 +1,83 @@
 using AutoMapper;
-using Masuit.MyBlogs.Core.Models.Command;
-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.Tools.Systems;
-
-namespace Masuit.MyBlogs.Core.Configs
+
+namespace Masuit.MyBlogs.Core.Configs;
+
+/// <summary>
+/// 注册automapper
+/// </summary>
+public sealed class MappingProfile : Profile
 {
-    /// <summary>
-    /// 注册automapper
-    /// </summary>
-    public sealed class MappingProfile : Profile
-    {
-        public MappingProfile()
-        {
-            CreateMap<CategoryCommand, Category>().ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
-            CreateMap<Category, CategoryDto_P>().ReverseMap();
-            CreateMap<Category, CategoryDto>().ReverseMap();
-            CreateMap<CategoryCommand, CategoryDto>().ReverseMap();
-
-            CreateMap<CommentCommand, Comment>().ForMember(c => c.Status, e => e.MapFrom(c => Status.Pending)).ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
-            CreateMap<Comment, CommentDto>().ReverseMap();
-            CreateMap<CommentCommand, CommentDto>().ReverseMap();
-            CreateMap<Comment, CommentViewModel>().ForMember(c => c.CommentDate, e => e.MapFrom(c => c.CommentDate.ToString("yyyy-MM-dd HH:mm:ss"))).ReverseMap();
-
-            CreateMap<LeaveMessageCommand, LeaveMessage>().ForMember(c => c.Status, e => e.MapFrom(c => Status.Pending)).ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
-            CreateMap<LeaveMessage, LeaveMessageDto>().ReverseMap();
-            CreateMap<LeaveMessageCommand, LeaveMessageDto>().ReverseMap();
-            CreateMap<LeaveMessage, LeaveMessageViewModel>().ForMember(l => l.PostDate, e => e.MapFrom(l => l.PostDate.ToString("yyyy-MM-dd HH:mm:ss"))).ReverseMap();
-
-            CreateMap<Links, LinksDto>().ForMember(e => e.Loopbacks, e => e.MapFrom(m => m.Loopbacks.GroupBy(e =>
-                e.IP).Count())).ReverseMap();
-
-            CreateMap<MenuCommand, Menu>().ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
-            CreateMap<Menu, MenuDto>().ForMember(m => m.Children, e => e.MapFrom(m => m.Children.OrderBy(c => c.Sort).ToList())).ReverseMap();
-
-            CreateMap<Misc, MiscDto>().ReverseMap();
-
-            CreateMap<Notice, NoticeDto>().ReverseMap();
-
-            CreateMap<PostCommand, Post>().ReverseMap();
-            CreateMap<Post, PostModelBase>();
-            CreateMap<Post, PostHistoryVersion>().ForMember(p => p.Id, e => e.Ignore()).ForMember(v => v.PostId, e => e.MapFrom(p => p.Id));
-            CreateMap<Post, PostDto>().ForMember(p => p.CategoryName, e => e.MapFrom(p => p.Category.Name)).ForMember(p => p.LimitMode, e => e.MapFrom(p => p.LimitMode ?? RegionLimitMode.All)).ForMember(p => p.Category, e => e.Ignore()).ReverseMap();
-            CreateMap<PostCommand, PostDto>().ReverseMap();
-            CreateMap<PostHistoryVersion, PostDto>().ForMember(p => p.CategoryName, e => e.MapFrom(p => p.Category.Name)).ReverseMap();
-            CreateMap<Post, PostDataModel>().ForMember(p => p.ModifyDate, e => e.MapFrom(p => p.ModifyDate))
-                .ForMember(p => p.PostDate, e => e.MapFrom(p => p.PostDate))
-                .ForMember(p => p.Status, e => e.MapFrom(p => p.Status.GetDisplay()))
-                .ForMember(p => p.ModifyCount, e => e.MapFrom(p => p.PostHistoryVersion.Count))
-                .ForMember(p => p.ViewCount, e => e.MapFrom(p => p.TotalViewCount))
-                .ForMember(p => p.AverageViewCount, e => e.MapFrom(p => p.PostVisitRecordStats.Sum(t => t.Count) / (p.PostVisitRecordStats.Max(s => s.Date) - p.PostVisitRecordStats.Min(s => s.Date)).TotalDays))
-                .ForMember(p => p.Seminars, e => e.MapFrom(p => p.Seminar.Select(s => s.Id).ToArray()))
-                .ForMember(p => p.LimitDesc, e => e.MapFrom(p => p.LimitMode > RegionLimitMode.All ? string.Format(p.LimitMode.GetDescription(), p.Regions, p.ExceptRegions) : "无限制"));
-
-            CreateMap<SearchDetails, SearchDetailsDto>().ReverseMap();
-
-            CreateMap<UserInfo, UserInfoDto>();
-            CreateMap<UserInfoDto, UserInfo>()
-                .ForMember(u => u.Id, e => e.Ignore())
-                .ForMember(u => u.Password, e => e.Ignore())
-                .ForMember(u => u.SaltKey, e => e.Ignore());
-
-            CreateMap<LoginRecord, LoginRecordViewModel>().ReverseMap();
-
-            CreateMap<Seminar, SeminarDto>().ReverseMap();
-
-            CreateMap<PostMergeRequestCommandBase, PostMergeRequest>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.MergeState, e => e.Ignore()).ReverseMap();
-            CreateMap<PostMergeRequestCommand, PostMergeRequest>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.MergeState, e => e.Ignore()).ReverseMap();
-            CreateMap<PostMergeRequestCommand, Post>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.Status, e => e.Ignore()).ReverseMap();
-            CreateMap<PostMergeRequest, PostMergeRequestDtoBase>().ForMember(p => p.PostTitle, e => e.MapFrom(r => r.Post.Title));
-            CreateMap<PostMergeRequest, PostMergeRequestDto>().ForMember(p => p.PostTitle, e => e.MapFrom(r => r.Post.Title));
-            CreateMap<PostMergeRequest, Post>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.Status, e => e.Ignore()).ReverseMap();
-            CreateMap<Post, PostMergeRequestDto>().ReverseMap();
-
-            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)).ReverseMap();
-
-            CreateMap<Donate, DonateDto>();
-
-            CreateMap<PostVisitRecord, PostVisitRecordViewModel>().ForMember(m => m.Time, e => e.MapFrom(m => m.Time.ToString("yyyy-MM-dd HH:mm:ss")));
-
-            CreateMap<AdvertisementClickRecord, AdvertisementClickRecordViewModel>().ForMember(m => m.Time, e => e.MapFrom(m => m.Time.ToString("yyyy-MM-dd HH:mm:ss")));
-        }
-    }
-}
+	public MappingProfile()
+	{
+		CreateMap<CategoryCommand, Category>().ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
+		CreateMap<Category, CategoryDto_P>().ReverseMap();
+		CreateMap<Category, CategoryDto>().ReverseMap();
+		CreateMap<CategoryCommand, CategoryDto>().ReverseMap();
+
+		CreateMap<CommentCommand, Comment>().ForMember(c => c.Status, e => e.MapFrom(c => Status.Pending)).ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
+		CreateMap<Comment, CommentDto>().ReverseMap();
+		CreateMap<CommentCommand, CommentDto>().ReverseMap();
+		CreateMap<Comment, CommentViewModel>().ForMember(c => c.CommentDate, e => e.MapFrom(c => c.CommentDate.ToString("yyyy-MM-dd HH:mm:ss"))).ReverseMap();
+
+		CreateMap<LeaveMessageCommand, LeaveMessage>().ForMember(c => c.Status, e => e.MapFrom(c => Status.Pending)).ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
+		CreateMap<LeaveMessage, LeaveMessageDto>().ReverseMap();
+		CreateMap<LeaveMessageCommand, LeaveMessageDto>().ReverseMap();
+		CreateMap<LeaveMessage, LeaveMessageViewModel>().ForMember(l => l.PostDate, e => e.MapFrom(l => l.PostDate.ToString("yyyy-MM-dd HH:mm:ss"))).ReverseMap();
+
+		CreateMap<Links, LinksDto>().ForMember(e => e.Loopbacks, e => e.MapFrom(m => m.Loopbacks.GroupBy(e =>
+			e.IP).Count())).ReverseMap();
+
+		CreateMap<MenuCommand, Menu>().ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
+		CreateMap<Menu, MenuDto>().ForMember(m => m.Children, e => e.MapFrom(m => m.Children.OrderBy(c => c.Sort).ToList())).ReverseMap();
+
+		CreateMap<Misc, MiscDto>().ReverseMap();
+
+		CreateMap<Notice, NoticeDto>().ReverseMap();
+
+		CreateMap<PostCommand, Post>().ReverseMap();
+		CreateMap<Post, PostModelBase>();
+		CreateMap<Post, PostHistoryVersion>().ForMember(p => p.Id, e => e.Ignore()).ForMember(v => v.PostId, e => e.MapFrom(p => p.Id));
+		CreateMap<Post, PostDto>().ForMember(p => p.CategoryName, e => e.MapFrom(p => p.Category.Name)).ForMember(p => p.LimitMode, e => e.MapFrom(p => p.LimitMode ?? RegionLimitMode.All)).ForMember(p => p.Category, e => e.Ignore()).ReverseMap();
+		CreateMap<PostCommand, PostDto>().ReverseMap();
+		CreateMap<PostHistoryVersion, PostDto>().ForMember(p => p.CategoryName, e => e.MapFrom(p => p.Category.Name)).ReverseMap();
+		CreateMap<Post, PostDataModel>().ForMember(p => p.ModifyDate, e => e.MapFrom(p => p.ModifyDate))
+			.ForMember(p => p.PostDate, e => e.MapFrom(p => p.PostDate))
+			.ForMember(p => p.Status, e => e.MapFrom(p => p.Status.GetDisplay()))
+			.ForMember(p => p.ModifyCount, e => e.MapFrom(p => p.PostHistoryVersion.Count))
+			.ForMember(p => p.ViewCount, e => e.MapFrom(p => p.TotalViewCount))
+			.ForMember(p => p.AverageViewCount, e => e.MapFrom(p => p.PostVisitRecordStats.Sum(t => t.Count) / (p.PostVisitRecordStats.Max(s => s.Date) - p.PostVisitRecordStats.Min(s => s.Date)).TotalDays))
+			.ForMember(p => p.Seminars, e => e.MapFrom(p => p.Seminar.Select(s => s.Id).ToArray()))
+			.ForMember(p => p.LimitDesc, e => e.MapFrom(p => p.LimitMode > RegionLimitMode.All ? string.Format(p.LimitMode.GetDescription(), p.Regions, p.ExceptRegions) : "无限制"));
+
+		CreateMap<SearchDetails, SearchDetailsDto>().ReverseMap();
+
+		CreateMap<UserInfo, UserInfoDto>();
+		CreateMap<UserInfoDto, UserInfo>()
+			.ForMember(u => u.Id, e => e.Ignore())
+			.ForMember(u => u.Password, e => e.Ignore())
+			.ForMember(u => u.SaltKey, e => e.Ignore());
+
+		CreateMap<LoginRecord, LoginRecordViewModel>().ReverseMap();
+
+		CreateMap<Seminar, SeminarDto>().ReverseMap();
+
+		CreateMap<PostMergeRequestCommandBase, PostMergeRequest>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.MergeState, e => e.Ignore()).ReverseMap();
+		CreateMap<PostMergeRequestCommand, PostMergeRequest>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.MergeState, e => e.Ignore()).ReverseMap();
+		CreateMap<PostMergeRequestCommand, Post>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.Status, e => e.Ignore()).ReverseMap();
+		CreateMap<PostMergeRequest, PostMergeRequestDtoBase>().ForMember(p => p.PostTitle, e => e.MapFrom(r => r.Post.Title));
+		CreateMap<PostMergeRequest, PostMergeRequestDto>().ForMember(p => p.PostTitle, e => e.MapFrom(r => r.Post.Title));
+		CreateMap<PostMergeRequest, Post>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.Status, e => e.Ignore()).ReverseMap();
+		CreateMap<Post, PostMergeRequestDto>().ReverseMap();
+
+		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)).ReverseMap();
+
+		CreateMap<Donate, DonateDto>();
+
+		CreateMap<PostVisitRecord, PostVisitRecordViewModel>().ForMember(m => m.Time, e => e.MapFrom(m => m.Time.ToString("yyyy-MM-dd HH:mm:ss")));
+
+		CreateMap<AdvertisementClickRecord, AdvertisementClickRecordViewModel>().ForMember(m => m.Time, e => e.MapFrom(m => m.Time.ToString("yyyy-MM-dd HH:mm:ss")));
+	}
+}

+ 59 - 66
src/Masuit.MyBlogs.Core/Controllers/AdminController.cs

@@ -1,91 +1,84 @@
 using AutoMapper;
 using FreeRedis;
-using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Configs;
 using Masuit.MyBlogs.Core.Extensions;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.DTO;
-using Masuit.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools.Core.Net;
-using Masuit.Tools.Security;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.Filters;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 管理页的父控制器
+/// </summary>
+[MyAuthorize, ApiExplorerSettings(IgnoreApi = true)]
+public class AdminController : Controller
 {
 	/// <summary>
-	/// 管理页的父控制器
+	/// UserInfoService
 	/// </summary>
-	[MyAuthorize, ApiExplorerSettings(IgnoreApi = true)]
-	public class AdminController : Controller
-	{
-		/// <summary>
-		/// UserInfoService
-		/// </summary>
-		public IUserInfoService UserInfoService { get; set; }
-		public IRedisClient RedisHelper { get; set; }
+	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)
+	/// <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
 		{
-			return Ok(new
-			{
-				IsLogin = isLogin,
-				Success = success,
-				Message = message,
-				Data = data
-			});
-		}
+			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);
 #endif
-			if (user == null && Request.Cookies.Any(x => x.Key == "username" || x.Key == "password")) //执行自动登录
+		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)
 			{
-				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
 				{
-					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);
-				}
+					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())
+		}
+		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++)
 			{
-				for (var i = 0; i < errmsgs.Count; i++)
-				{
-					errmsgs[i] = i + 1 + ". " + errmsgs[i];
-				}
+				errmsgs[i] = i + 1 + ". " + errmsgs[i];
 			}
-			filterContext.Result = ResultData(null, false, "数据校验失败,错误信息:" + string.Join(" | ", errmsgs));
 		}
+		filterContext.Result = ResultData(null, false, "数据校验失败,错误信息:" + string.Join(" | ", errmsgs));
 	}
-}
+}

+ 0 - 9
src/Masuit.MyBlogs.Core/Controllers/AdvertisementController.cs

@@ -1,24 +1,15 @@
 using AutoMapper.QueryableExtensions;
-using Lucene.Net.Support;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
-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.MyBlogs.Core.Models.ViewModel;
 using Masuit.Tools.AspNetCore.Mime;
 using Masuit.Tools.AspNetCore.ModelBinder;
 using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
-using Masuit.Tools.Core.Net;
 using Masuit.Tools.Excel;
-using Masuit.Tools.Linq;
 using Masuit.Tools.Models;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Net.Http.Headers;
 using System.ComponentModel.DataAnnotations;
-using System.Linq.Expressions;
 using System.Net;
 using System.Text.RegularExpressions;
 

+ 252 - 321
src/Masuit.MyBlogs.Core/Controllers/BaseController.cs

@@ -5,406 +5,337 @@ using Masuit.MyBlogs.Core.Common.Mails;
 using Masuit.MyBlogs.Core.Configs;
 using Masuit.MyBlogs.Core.Extensions;
 using Masuit.MyBlogs.Core.Extensions.Firewall;
-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.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools;
-using Masuit.Tools.Core.Net;
-using Masuit.Tools.Linq;
-using Masuit.Tools.Security;
-using Masuit.Tools.Strings;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.Filters;
 using Microsoft.Net.Http.Headers;
-using System.Linq.Expressions;
 using System.Net;
 using System.Text.RegularExpressions;
 using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 基本父控制器
+/// </summary>
+[ApiExplorerSettings(IgnoreApi = true), ServiceFilter(typeof(FirewallAttribute))]
+public class BaseController : Controller
 {
-	/// <summary>
-	/// 基本父控制器
-	/// </summary>
-	[ApiExplorerSettings(IgnoreApi = true), ServiceFilter(typeof(FirewallAttribute))]
-	public class BaseController : Controller
-	{
-		public IUserInfoService UserInfoService { get; set; }
+	public IUserInfoService UserInfoService { get; set; }
 
-		public ILinksService LinksService { get; set; }
+	public ILinksService LinksService { get; set; }
 
-		public IAdvertisementService AdsService { get; set; }
+	public IAdvertisementService AdsService { get; set; }
 
-		public IVariablesService VariablesService { get; set; }
+	public IVariablesService VariablesService { get; set; }
 
-		public IMapper Mapper { 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();
+	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>
+	/// 客户端的真实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"]);
 
-		/// <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();
 
-		public int[] HideCategories => Request.GetHideCategories();
+	public bool SafeMode => !Request.Cookies.ContainsKey("Nsfw") || Request.Cookies["Nsfw"] == "1";
 
-		/// <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)
+	/// <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
 		{
-			return Ok(new
-			{
-				IsLogin = isLogin,
-				Success = success,
-				Message = message,
-				Data = data,
-				code
-			});
-		}
+			IsLogin = isLogin,
+			Success = success,
+			Message = message,
+			Data = data,
+			code
+		});
+	}
 
-		protected string ReplaceVariables(string text, int depth = 0)
+	protected string ReplaceVariables(string text, int depth = 0)
+	{
+		if (string.IsNullOrEmpty(text))
 		{
-			if (string.IsNullOrEmpty(text))
-			{
-				return 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 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 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)
 			{
-				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)
 				{
-					string valve = value;
-					if (Regex.IsMatch(valve, pattern) && depth < 32)
-					{
-						valve = ReplaceVariables(valve, depth++);
-					}
-
-					template.Set(key, valve);
+					valve = ReplaceVariables(valve, depth++);
 				}
-			}
 
-			return template.Render();
+				template.Set(key, valve);
+			}
 		}
 
-		public override Task OnActionExecutionAsync(ActionExecutingContext filterContext, ActionExecutionDelegate next)
-		{
-			ViewBag.Desc = CommonHelper.SystemSettings["Description"];
-			var user = filterContext.HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
+		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);
+			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();
-			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(" | "))
-			};
+		if (CommonHelper.SystemSettings.GetOrAdd("CloseSite", "false") == "true" && user?.IsAdmin != true)
+		{
+			filterContext.Result = RedirectToAction("ComingSoon", "Error");
 			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 (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))))
 		{
-			if (CurrentUser.IsAdmin)
-			{
-				return string.Empty; ;
-			}
+			filterContext.Result = ResultData("网站当前处于数据写保护状态,无法提交任何数据,如有疑问请联系网站管理员!", false, "网站当前处于数据写保护状态,无法提交任何数据,如有疑问请联系网站管理员!", user != null, HttpStatusCode.BadRequest);
+			return Task.CompletedTask;
+		}
 
-			if (string.IsNullOrEmpty(Request.Cookies["ValidateKey"]))
+		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)
 			{
-				if (string.IsNullOrEmpty(code))
+				Response.Cookies.Append("username", name, new CookieOptions
 				{
-					return "请输入验证码!";
-				}
-				if (RedisHelper.Get("code:" + email) != code)
+					Expires = DateTime.Now.AddYears(1),
+					SameSite = SameSiteMode.Lax
+				});
+				Response.Cookies.Append("password", pwd, new CookieOptions
 				{
-					return "验证码错误!";
-				}
-			}
-			else if (Request.Cookies["ValidateKey"].DesDecrypt(AppConfig.BaiduAK) != email)
-			{
-				Response.Cookies.Delete("Email");
-				Response.Cookies.Delete("NickName");
-				Response.Cookies.Delete("ValidateKey");
-				return "邮箱验证信息已失效,请刷新页面后重新评论!";
+					Expires = DateTime.Now.AddYears(1),
+					SameSite = SameSiteMode.Lax
+				});
+				filterContext.HttpContext.Session.Set(SessionKey.UserInfo, userInfo);
 			}
+		}
 
-			if (mailSender.HasBounced(email))
-			{
-				Response.Cookies.Delete("Email");
-				Response.Cookies.Delete("NickName");
-				Response.Cookies.Delete("ValidateKey");
-				return "邮箱地址错误,请刷新页面后重新使用有效的邮箱地址!";
-			}
+		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;
+	}
 
-			return string.Empty;
+	/// <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; ;
 		}
 
-		internal void WriteEmailKeyCookie(string email)
+		if (string.IsNullOrEmpty(Request.Cookies["ValidateKey"]))
 		{
-			Response.Cookies.Append("Email", email, new CookieOptions()
+			if (string.IsNullOrEmpty(code))
 			{
-				Expires = DateTimeOffset.Now.AddYears(1),
-				SameSite = SameSiteMode.Lax
-			});
-			Response.Cookies.Append("ValidateKey", email.DesEncrypt(AppConfig.BaiduAK), new CookieOptions()
+				return "请输入验证码!";
+			}
+			if (RedisHelper.Get("code:" + email) != code)
 			{
-				Expires = DateTimeOffset.Now.AddYears(1),
-				SameSite = SameSiteMode.Lax
-			});
+				return "验证码错误!";
+			}
+		}
+		else if (Request.Cookies["ValidateKey"].DesDecrypt(AppConfig.BaiduAK) != email)
+		{
+			Response.Cookies.Delete("Email");
+			Response.Cookies.Delete("NickName");
+			Response.Cookies.Delete("ValidateKey");
+			return "邮箱验证信息已失效,请刷新页面后重新评论!";
 		}
 
-		protected void CheckPermission(List<PostDto> posts)
+		if (mailSender.HasBounced(email))
 		{
-			if (CurrentUser.IsAdmin || Request.IsRobot())
-			{
-				return;
-			}
+			Response.Cookies.Delete("Email");
+			Response.Cookies.Delete("NickName");
+			Response.Cookies.Delete("ValidateKey");
+			return "邮箱地址错误,请刷新页面后重新使用有效的邮箱地址!";
+		}
 
-			posts.RemoveAll(p => p.LimitMode == RegionLimitMode.OnlyForSearchEngine);
-			if (VisitorTokenValid || CommonHelper.IPWhiteList.Contains(ClientIP))
-			{
-				return;
-			}
+		return string.Empty;
+	}
 
-			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();
-				}
-			}
+	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
+		});
+	}
 
-			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;
 		}
 
-		protected Expression<Func<Post, bool>> PostBaseWhere()
+		Expression<Func<Post, bool>> where = p => p.Status == Status.Published && p.LimitMode != RegionLimitMode.OnlyForSearchEngine;
+		where = where.AndIf(HideCategories.Length > 0, p => !HideCategories.Contains(p.CategoryId)).AndIf(SafeMode, p => !p.IsNsfw);
+		if (VisitorTokenValid || CommonHelper.IPWhiteList.Contains(ClientIP))
 		{
-			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));
-			}
+			return where;
+		}
 
-			if (VisitorTokenValid || CommonHelper.IPWhiteList.Contains(ClientIP))
+		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)
 			{
-				return where;
+				location += "|" + s.GetIPLocation();
 			}
+		}
 
-			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));
+	}
 
-			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;
 		}
 
-		protected void CheckPermission(Post post)
+		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)
 		{
-			if (CurrentUser.IsAdmin || VisitorTokenValid || Request.IsRobot() || CommonHelper.IPWhiteList.Contains(ClientIP))
+			var s = rawip.Base64Decrypt();
+			if (ClientIP != s)
 			{
-				return;
+				location += "|" + s.GetIPLocation();
 			}
+		}
 
-			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)
+		switch (post.LimitMode)
+		{
+			case RegionLimitMode.OnlyForSearchEngine:
+				Disallow(post);
+				break;
+
+			case RegionLimitMode.AllowRegion:
+				if (!Regex.IsMatch(location, post.Regions, RegexOptions.IgnoreCase))
 				{
-					location += "|" + s.GetIPLocation();
+					Disallow(post);
 				}
-			}
 
-			switch (post.LimitMode)
-			{
-				case RegionLimitMode.OnlyForSearchEngine:
-					Disallow(post);
-					break;
+				break;
 
-				case RegionLimitMode.AllowRegion:
-					if (!Regex.IsMatch(location, post.Regions, RegexOptions.IgnoreCase))
-					{
-						Disallow(post);
-					}
+			case RegionLimitMode.ForbidRegion:
+				if (Regex.IsMatch(location, post.Regions, RegexOptions.IgnoreCase))
+				{
+					Disallow(post);
+				}
 
-					break;
+				break;
 
-				case RegionLimitMode.ForbidRegion:
-					if (Regex.IsMatch(location, post.Regions, 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;
+				}
 
-				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;
-			}
+				goto case RegionLimitMode.ForbidRegion;
+		}
 
-			if (HideCategories.Contains(post.CategoryId))
-			{
-				throw new NotFoundException("文章未找到");
-			}
+		if (HideCategories.Contains(post.CategoryId) || (SafeMode && post.IsNsfw))
+		{
+			throw new NotFoundException("文章未找到");
 		}
+	}
 
-		private void Disallow(Post post)
+	private void Disallow(Post post)
+	{
+		var remark = "无权限查看该文章";
+		if (Request.Cookies.TryGetValue(SessionKey.RawIP, out var rawip) && ClientIP != rawip.Base64Decrypt())
 		{
-			var remark = "无权限查看该文章";
-			if (Request.Cookies.TryGetValue(SessionKey.RawIP, out var rawip) && ClientIP != rawip.Base64Decrypt())
-			{
-				remark += ",发生了IP切换,原始IP:" + rawip.Base64Decrypt();
-			}
+			remark += ",发生了IP切换,原始IP:" + rawip.Base64Decrypt();
+		}
 
-			RedisHelper.IncrBy("interceptCount", 1);
-			RedisHelper.LPush("intercept", new IpIntercepter()
+		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
 			{
-				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("文章未找到");
-		}
+				Request.Protocol,
+				Request.Headers
+			}.ToJsonString()
+		});
+		throw new NotFoundException("文章未找到");
 	}
-}
+}

+ 63 - 70
src/Masuit.MyBlogs.Core/Controllers/CategoryController.cs

@@ -1,88 +1,81 @@
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.Command;
-using Masuit.MyBlogs.Core.Models.DTO;
-using Masuit.MyBlogs.Core.Models.Entity;
-using Masuit.MyBlogs.Core.Models.Enum;
 using Masuit.Tools.AspNetCore.ModelBinder;
 using Masuit.Tools.Models;
-using Masuit.Tools.Systems;
 using Microsoft.AspNetCore.Mvc;
 using Z.EntityFramework.Plus;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 文章分类
+/// </summary>
+public sealed class CategoryController : BaseController
 {
 	/// <summary>
-	/// 文章分类
+	/// CategoryService
+	/// </summary>
+	public ICategoryService CategoryService { get; set; }
+
+	/// <summary>
+	/// 获取所有分类
 	/// </summary>
-	public sealed class CategoryController : BaseController
+	/// <returns></returns>
+	public ActionResult GetCategories()
 	{
-		/// <summary>
-		/// CategoryService
-		/// </summary>
-		public ICategoryService CategoryService { get; set; }
+		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>
-		/// <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)
+	/// <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 model = await CategoryService.GetByIdAsync(id) ?? throw new NotFoundException("分类不存在!");
-			return ResultData(model.Mapper<CategoryDto>());
+			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 ? "分类删除成功" : "分类删除失败");
 	}
-}
+}

+ 261 - 272
src/Masuit.MyBlogs.Core/Controllers/CommentController.cs

@@ -3,19 +3,9 @@ using Hangfire;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Common.Mails;
 using Masuit.MyBlogs.Core.Extensions;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.Command;
-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.Tools;
-using Masuit.Tools.Core.Net;
 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;
@@ -24,324 +14,323 @@ using System.Text;
 using System.Text.RegularExpressions;
 using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 评论管理
+/// </summary>
+public sealed class CommentController : BaseController
 {
-	/// <summary>
-	/// 评论管理
-	/// </summary>
-	public sealed class CommentController : BaseController
-	{
-		public ICommentService CommentService { get; set; }
+	public ICommentService CommentService { get; set; }
 
-		public IPostService PostService { get; set; }
+	public IPostService PostService { get; set; }
 
-		public IWebHostEnvironment HostEnvironment { get; set; }
+	public IWebHostEnvironment HostEnvironment { get; set; }
 
-		public ICacheManager<int> CommentFeq { get; set; }
+	public ICacheManager<int> CommentFeq { get; set; }
 
-		/// <summary>
-		/// 发表评论
-		/// </summary>
-		/// <param name="messageService"></param>
-		/// <param name="mailSender"></param>
-		/// <param name="cmd"></param>
-		/// <returns></returns>
-		[HttpPost, ValidateAntiForgeryToken]
-		public async Task<ActionResult> Submit([FromServices] IInternalMessageService messageService, [FromServices] IMailSender mailSender, CommentCommand cmd)
+	/// <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)
 		{
-			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);
-			}
+			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.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)
+		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)
 			{
-				comment.NickName = user.NickName;
-				comment.Email = user.Email;
-				if (user.IsAdmin)
+				await messageService.AddEntitySavedAsync(new InternalMessage()
 				{
-					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, "评论失败");
+					Title = $"来自【{comment.NickName}】在文章《{post.Title}》的新评论",
+					Content = comment.Content,
+					Link = Url.Action("Details", "Post", new { id = comment.PostId, cid = comment.Id }) + "#comment"
+				});
 			}
-
-			Response.Cookies.Append("NickName", comment.NickName, new CookieOptions()
+			if (comment.ParentId == null)
 			{
-				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);
+				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
+				//新评论,只通知博主和楼主
+				foreach (var s in emails)
 				{
-					//通知博主和所有关联的评论访客
-					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));
-					}
+					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));
 				}
-				return ResultData(null, true, "评论发表成功,服务器正在后台处理中,这会有一定的延迟,稍后将显示到评论列表中");
 			}
-
-			foreach (var s in emails)
+			else
 			{
-				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));
+				//通知博主和所有关联的评论访客
+				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, "评论成功,待站长审核通过以后将显示");
+			return ResultData(null, true, "评论发表成功,服务器正在后台处理中,这会有一定的延迟,稍后将显示到评论列表中");
 		}
 
-		/// <summary>
-		/// 评论投票
-		/// </summary>
-		/// <param name="id"></param>
-		/// <returns></returns>
-		[HttpPost]
-		public async Task<ActionResult> CommentVote(int id)
+		foreach (var s in emails)
 		{
-			if (HttpContext.Session.Get("cm" + id) != null)
-			{
-				return ResultData(null, false, "您刚才已经投过票了,感谢您的参与!");
-			}
+			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));
+		}
 
-			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, true, "评论成功,待站长审核通过以后将显示");
+	}
 
-			return ResultData(null, b, b ? "投票成功" : "投票失败");
+	/// <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>
-		/// <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)
+		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)
 		{
-			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;
-					}
-				}
+			HttpContext.Session.Set("cm" + id, id.GetBytes());
+		}
 
-				return ResultData(new
-				{
-					total = 1,
-					parentTotal = 1,
-					page,
-					size,
-					rows = layer.ToTree(c => c.Id, c => c.ParentId).Mapper<IList<CommentViewModel>>()
-				});
-			}
+		return ResultData(null, b, b ? "投票成功" : "投票失败");
+	}
 
-			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 =>
+	/// <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 == c.Post.Email || c.Email == c.Post.ModifierEmail;
+				c.IsAuthor = c.Email == comment.Post.Email || c.Email == comment.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(new
+			{
+				total = 1,
+				parentTotal = 1,
+				page,
+				size,
+				rows = layer.ToTree(c => c.Id, c => c.ParentId).Mapper<IList<CommentViewModel>>()
+			});
 		}
 
-		/// <summary>
-		/// 审核评论
-		/// </summary>
-		/// <param name="id"></param>
-		/// <returns></returns>
-		[MyAuthorize]
-		public async Task<ActionResult> Pass(int id)
+		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())
 		{
-			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)
+			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)
 			{
-				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, "审核通过!");
+				c.Email = null;
+				c.IP = null;
+				c.Location = null;
 			}
-
-			return ResultData(null, false, "审核失败!");
-		}
-
-		/// <summary>
-		/// 删除评论
-		/// </summary>
-		/// <param name="id"></param>
-		/// <returns></returns>
-		[MyAuthorize]
-		public ActionResult Delete(int id)
+		});
+		if (total > 0)
 		{
-			var b = CommentService.DeleteById(id);
-			return ResultData(null, b, b ? "删除成功!" : "删除失败!");
+			return ResultData(new
+			{
+				total,
+				parentTotal = total,
+				page,
+				size,
+				rows = comments.OrderByDescending(c => c.CommentDate).ToTree(c => c.Id, c => c.ParentId).Mapper<IList<CommentViewModel>>()
+			});
 		}
 
-		/// <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)
+		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 pages = await CommentService.GetPagesAsync<DateTime, CommentDto>(page, size, c => c.Status == Status.Pending, c => c.CommentDate, false);
-			foreach (var item in pages.Data)
+			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)
 			{
-				item.CommentDate = item.CommentDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+				BackgroundJob.Enqueue(() => CommonHelper.SendMail($"{Request.Host}{CommonHelper.SystemSettings["Title"]}文章评论回复:", content.Set("link", link).Render(false), email, ClientIP));
 			}
 
-			return Ok(pages);
+			return ResultData(null, true, "审核通过!");
 		}
+
+		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>
+	/// <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);
 	}
-}
+}

+ 101 - 105
src/Masuit.MyBlogs.Core/Controllers/DashboardController.cs

@@ -1,117 +1,113 @@
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.Enum;
-using Masuit.Tools;
-using Masuit.Tools.AspNetCore.ModelBinder;
+using Masuit.Tools.AspNetCore.ModelBinder;
 using Masuit.Tools.Logging;
 using Microsoft.AspNetCore.Mvc;
 using Polly;
 using System.Text;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 控制面板
+/// </summary>
+public sealed class DashboardController : AdminController
 {
-    /// <summary>
-    /// 控制面板
-    /// </summary>
-    public sealed class DashboardController : AdminController
-    {
-        /// <summary>
-        /// 控制面板
-        /// </summary>
-        /// <returns></returns>
-        [Route("dashboard"), ResponseCache(Duration = 60, VaryByHeader = "Cookie")]
-        public ActionResult Index()
-        {
-            Response.Cookies.Append("lang", "zh-cn", new CookieOptions()
-            {
-                Expires = DateTime.Now.AddYears(1),
-            });
-            return View();
-        }
+	/// <summary>
+	/// 控制面板
+	/// </summary>
+	/// <returns></returns>
+	[Route("dashboard"), ResponseCache(Duration = 60, VaryByHeader = "Cookie")]
+	public ActionResult Index()
+	{
+		Response.Cookies.Append("lang", "zh-cn", new CookieOptions()
+		{
+			Expires = DateTime.Now.AddYears(1),
+		});
+		return View();
+	}
 
-        [Route("counter")]
-        public ActionResult Counter()
-        {
-            return View();
-        }
+	[Route("counter")]
+	public ActionResult Counter()
+	{
+		return View();
+	}
 
-        /// <summary>
-        /// 获取站内消息
-        /// </summary>
-        /// <returns></returns>
-        public ActionResult GetMessages([FromServices] IPostService postService, [FromServices] ILeaveMessageService leaveMessageService, [FromServices] ICommentService commentService)
-        {
-            var post = postService.GetQuery(p => p.Status == Status.Pending).Select(p => new
-            {
-                p.Id,
-                p.Title,
-                p.PostDate,
-                p.Author
-            }).ToList();
-            var msgs = leaveMessageService.GetQuery(m => m.Status == Status.Pending).Select(p => new
-            {
-                p.Id,
-                p.PostDate,
-                p.NickName
-            }).ToList();
-            var comments = commentService.GetQuery(c => c.Status == Status.Pending).Select(p => new
-            {
-                p.Id,
-                p.CommentDate,
-                p.PostId,
-                p.NickName
-            }).ToList();
-            return ResultData(new
-            {
-                post,
-                msgs,
-                comments
-            });
-        }
+	/// <summary>
+	/// 获取站内消息
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult GetMessages([FromServices] IPostService postService, [FromServices] ILeaveMessageService leaveMessageService, [FromServices] ICommentService commentService)
+	{
+		var post = postService.GetQuery(p => p.Status == Status.Pending).Select(p => new
+		{
+			p.Id,
+			p.Title,
+			p.PostDate,
+			p.Author
+		}).ToList();
+		var msgs = leaveMessageService.GetQuery(m => m.Status == Status.Pending).Select(p => new
+		{
+			p.Id,
+			p.PostDate,
+			p.NickName
+		}).ToList();
+		var comments = commentService.GetQuery(c => c.Status == Status.Pending).Select(p => new
+		{
+			p.Id,
+			p.CommentDate,
+			p.PostId,
+			p.NickName
+		}).ToList();
+		return ResultData(new
+		{
+			post,
+			msgs,
+			comments
+		});
+	}
 
-        /// <summary>
-        /// 获取日志文件列表
-        /// </summary>
-        /// <returns></returns>
-        public ActionResult GetLogfiles()
-        {
-            List<string> files = Directory.GetFiles(LogManager.LogDirectory).OrderByDescending(s => s).Select(Path.GetFileName).ToList();
-            return ResultData(files);
-        }
+	/// <summary>
+	/// 获取日志文件列表
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult GetLogfiles()
+	{
+		List<string> files = Directory.GetFiles(LogManager.LogDirectory).OrderByDescending(s => s).Select(Path.GetFileName).ToList();
+		return ResultData(files);
+	}
 
-        /// <summary>
-        /// 查看日志
-        /// </summary>
-        /// <param name="filename"></param>
-        /// <returns></returns>
-        public ActionResult Catlog([FromBodyOrDefault] string filename)
-        {
-            if (System.IO.File.Exists(Path.Combine(LogManager.LogDirectory, filename)))
-            {
-                string text = new FileInfo(Path.Combine(LogManager.LogDirectory, filename)).ShareReadWrite().ReadAllText(Encoding.UTF8);
-                return ResultData(text);
-            }
-            return ResultData(null, false, "文件不存在!");
-        }
+	/// <summary>
+	/// 查看日志
+	/// </summary>
+	/// <param name="filename"></param>
+	/// <returns></returns>
+	public ActionResult Catlog([FromBodyOrDefault] string filename)
+	{
+		if (System.IO.File.Exists(Path.Combine(LogManager.LogDirectory, filename)))
+		{
+			string text = new FileInfo(Path.Combine(LogManager.LogDirectory, filename)).ShareReadWrite().ReadAllText(Encoding.UTF8);
+			return ResultData(text);
+		}
+		return ResultData(null, false, "文件不存在!");
+	}
 
-        /// <summary>
-        /// 删除文件
-        /// </summary>
-        /// <param name="filename"></param>
-        /// <returns></returns>
-        public ActionResult DeleteFile([FromBodyOrDefault] string filename)
-        {
-            Policy.Handle<IOException>().WaitAndRetry(5, i => TimeSpan.FromSeconds(1)).Execute(() => System.IO.File.Delete(Path.Combine(LogManager.LogDirectory, filename)));
-            return ResultData(null, message: "文件删除成功!");
-        }
+	/// <summary>
+	/// 删除文件
+	/// </summary>
+	/// <param name="filename"></param>
+	/// <returns></returns>
+	public ActionResult DeleteFile([FromBodyOrDefault] string filename)
+	{
+		Policy.Handle<IOException>().WaitAndRetry(5, i => TimeSpan.FromSeconds(1)).Execute(() => System.IO.File.Delete(Path.Combine(LogManager.LogDirectory, filename)));
+		return ResultData(null, message: "文件删除成功!");
+	}
 
-        /// <summary>
-        /// 资源管理器
-        /// </summary>
-        /// <returns></returns>
-        [Route("filemanager"), ResponseCache(Duration = 60, VaryByHeader = "Cookie")]
-        public ActionResult FileManager()
-        {
-            return View();
-        }
-    }
-}
+	/// <summary>
+	/// 资源管理器
+	/// </summary>
+	/// <returns></returns>
+	[Route("filemanager"), ResponseCache(Duration = 60, VaryByHeader = "Cookie")]
+	public ActionResult FileManager()
+	{
+		return View();
+	}
+}

+ 52 - 55
src/Masuit.MyBlogs.Core/Controllers/DonateController.cs

@@ -1,65 +1,62 @@
 using Masuit.MyBlogs.Core.Extensions;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.Entity;
 using Masuit.Tools.AspNetCore.ModelBinder;
 using Microsoft.AspNetCore.Mvc;
 using System.ComponentModel.DataAnnotations;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 打赏管理
+/// </summary>
+public sealed class DonateController : AdminController
 {
-    /// <summary>
-    /// 打赏管理
-    /// </summary>
-    public sealed class DonateController : AdminController
-    {
-        /// <summary>
-        /// DonateService
-        /// </summary>
-        public IDonateService DonateService { get; set; }
+	/// <summary>
+	/// DonateService
+	/// </summary>
+	public IDonateService DonateService { get; set; }
 
-        /// <summary>
-        /// 分页数据
-        /// </summary>
-        /// <param name="page"></param>
-        /// <param name="size"></param>
-        /// <returns></returns>
-        public ActionResult GetPageData([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
-        {
-            var list = DonateService.GetPages(page, size, d => true, d => d.DonateTime, false);
-            return Ok(list);
-        }
+	/// <summary>
+	/// 分页数据
+	/// </summary>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <returns></returns>
+	public ActionResult GetPageData([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
+	{
+		var list = DonateService.GetPages(page, size, d => true, d => d.DonateTime, false);
+		return Ok(list);
+	}
 
-        /// <summary>
-        /// 详情
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        public async Task<ActionResult> Get(int id)
-        {
-            Donate donate = await DonateService.GetByIdAsync(id) ?? throw new NotFoundException("条目不存在!");
-            return ResultData(donate);
-        }
+	/// <summary>
+	/// 详情
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	public async Task<ActionResult> Get(int id)
+	{
+		Donate donate = await DonateService.GetByIdAsync(id) ?? throw new NotFoundException("条目不存在!");
+		return ResultData(donate);
+	}
 
-        /// <summary>
-        /// 保存数据
-        /// </summary>
-        /// <param name="donate"></param>
-        /// <returns></returns>
-        public async Task<ActionResult> Save([FromBodyOrDefault] Donate donate)
-        {
-            var b = await DonateService.AddOrUpdateSavedAsync(d => d.Id, donate) > 0;
-            return ResultData(null, b, b ? "保存成功!" : "保存失败!");
-        }
+	/// <summary>
+	/// 保存数据
+	/// </summary>
+	/// <param name="donate"></param>
+	/// <returns></returns>
+	public async Task<ActionResult> Save([FromBodyOrDefault] Donate donate)
+	{
+		var b = await DonateService.AddOrUpdateSavedAsync(d => d.Id, donate) > 0;
+		return ResultData(null, b, b ? "保存成功!" : "保存失败!");
+	}
 
-        /// <summary>
-        /// 删除
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        public ActionResult Delete(int id)
-        {
-            bool b = DonateService - id;
-            return ResultData(null, b, b ? "删除成功!" : "删除失败!");
-        }
-    }
-}
+	/// <summary>
+	/// 删除
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	public ActionResult Delete(int id)
+	{
+		bool b = DonateService - id;
+		return ResultData(null, b, b ? "删除成功!" : "删除失败!");
+	}
+}

+ 240 - 241
src/Masuit.MyBlogs.Core/Controllers/Drive/AdminController.cs

@@ -7,270 +7,269 @@ using Microsoft.AspNetCore.Mvc;
 using Newtonsoft.Json;
 using Newtonsoft.Json.Serialization;
 
-namespace Masuit.MyBlogs.Core.Controllers.Drive
+namespace Masuit.MyBlogs.Core.Controllers.Drive;
+
+[MyAuthorize]
+[ApiController]
+[Route("api/[controller]")]
+public sealed class AdminController : Controller
 {
-    [MyAuthorize]
-    [ApiController]
-    [Route("api/[controller]")]
-    public sealed class AdminController : Controller
-    {
-        private readonly IDriveAccountService _driveAccount;
-        private readonly SettingService _setting;
-        private readonly TokenService _tokenService;
+	private readonly IDriveAccountService _driveAccount;
+	private readonly SettingService _setting;
+	private readonly TokenService _tokenService;
 
-        public AdminController(IDriveAccountService driveAccount, SettingService setting, TokenService tokenService)
-        {
-            _driveAccount = driveAccount;
-            _setting = setting;
-            _tokenService = tokenService;
-        }
+	public AdminController(IDriveAccountService driveAccount, SettingService setting, TokenService tokenService)
+	{
+		_driveAccount = driveAccount;
+		_setting = setting;
+		_tokenService = tokenService;
+	}
 
-        /// <summary>
-        /// 重定向到 M$ 的 Oauth
-        /// </summary>
-        /// <returns></returns>
-        [AllowAnonymous]
-        [HttpGet("bind/url")]
-        public async Task<RedirectResult> RedirectToBinding()
-        {
-            string url = await _driveAccount.GetAuthorizationRequestUrl();
-            var result = new RedirectResult(url);
-            return result;
-        }
+	/// <summary>
+	/// 重定向到 M$ 的 Oauth
+	/// </summary>
+	/// <returns></returns>
+	[AllowAnonymous]
+	[HttpGet("bind/url")]
+	public async Task<RedirectResult> RedirectToBinding()
+	{
+		string url = await _driveAccount.GetAuthorizationRequestUrl();
+		var result = new RedirectResult(url);
+		return result;
+	}
 
-        /// <summary>
-        /// 从 Oauth 重定向的url
-        /// </summary>
-        /// <returns></returns>
-        [AllowAnonymous]
-        [HttpGet("bind/new")]
-        public async Task<IActionResult> NewBinding(string code)
-        {
-            try
-            {
-                var result = await _tokenService.Authorize(code);
-                if (result.AccessToken != null)
-                {
-                    await _setting.Set("AccountStatus", "已认证");
-                    return Redirect("/#/admin");
-                }
-            }
-            catch (Exception ex)
-            {
-                return StatusCode(500, new ErrorResponse()
-                {
-                    message = ex.Message
-                });
-            }
-            return StatusCode(500, new ErrorResponse()
-            {
-                message = "未知错误"
-            });
-        }
+	/// <summary>
+	/// 从 Oauth 重定向的url
+	/// </summary>
+	/// <returns></returns>
+	[AllowAnonymous]
+	[HttpGet("bind/new")]
+	public async Task<IActionResult> NewBinding(string code)
+	{
+		try
+		{
+			var result = await _tokenService.Authorize(code);
+			if (result.AccessToken != null)
+			{
+				await _setting.Set("AccountStatus", "已认证");
+				return Redirect("/#/admin");
+			}
+		}
+		catch (Exception ex)
+		{
+			return StatusCode(500, new ErrorResponse()
+			{
+				message = ex.Message
+			});
+		}
+		return StatusCode(500, new ErrorResponse()
+		{
+			message = "未知错误"
+		});
+	}
 
-        /// <summary>
-        /// 添加 SharePoint Site
-        /// </summary>
-        /// <returns></returns>
-        [HttpPost("sites")]
-        public async Task<IActionResult> AddSite(AddSiteModel model)
-        {
-            try
-            {
-                await _driveAccount.AddSiteId(model.siteName, model.nickName);
-            }
-            catch (Exception ex)
-            {
-                return StatusCode(500, new ErrorResponse()
-                {
-                    message = ex.Message
-                });
-            }
-            return StatusCode(201);
-        }
+	/// <summary>
+	/// 添加 SharePoint Site
+	/// </summary>
+	/// <returns></returns>
+	[HttpPost("sites")]
+	public async Task<IActionResult> AddSite(AddSiteModel model)
+	{
+		try
+		{
+			await _driveAccount.AddSiteId(model.siteName, model.nickName);
+		}
+		catch (Exception ex)
+		{
+			return StatusCode(500, new ErrorResponse()
+			{
+				message = ex.Message
+			});
+		}
+		return StatusCode(201);
+	}
 
-        /// <summary>
-        /// 获取基本内容
-        /// </summary>
-        /// <returns></returns>
-        [HttpGet("info")]
-        public async Task<IActionResult> GetInfo()
-        {
-            try
-            {
-                var driveInfo = new List<DriveAccountService.DriveInfo>();
-                if (_setting.Get("AccountStatus") == "已认证")
-                {
-                    driveInfo = await _driveAccount.GetDriveInfo();
-                }
-                return Json(new
-                {
-                    officeName = OneDriveConfiguration.AccountName,
-                    officeType = Enum.GetName(typeof(OneDriveConfiguration.OfficeType), OneDriveConfiguration.Type),
-                    driveInfo,
-                    appName = _setting.Get("AppName"),
-                    webName = _setting.Get("WebName"),
-                    defaultDrive = _setting.Get("DefaultDrive"),
-                    accountStatus = _setting.Get("AccountStatus"),
-                    readme = _setting.Get("Readme"),
-                    footer = _setting.Get("Footer"),
-                    allowAnonymouslyUpload = !string.IsNullOrEmpty(_setting.Get("AllowAnonymouslyUpload")) && Convert.ToBoolean(_setting.Get("AllowAnonymouslyUpload")),
-                    uploadPassword = _setting.Get("UploadPassword"),
-                }, new JsonSerializerSettings()
-                {
-                    ContractResolver = new CamelCasePropertyNamesContractResolver()
-                });
-            }
-            catch (Exception ex)
-            {
-                return StatusCode(500, new ErrorResponse()
-                {
-                    message = ex.Message
-                });
-            }
-        }
+	/// <summary>
+	/// 获取基本内容
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("info")]
+	public async Task<IActionResult> GetInfo()
+	{
+		try
+		{
+			var driveInfo = new List<DriveAccountService.DriveInfo>();
+			if (_setting.Get("AccountStatus") == "已认证")
+			{
+				driveInfo = await _driveAccount.GetDriveInfo();
+			}
+			return Json(new
+			{
+				officeName = OneDriveConfiguration.AccountName,
+				officeType = Enum.GetName(typeof(OneDriveConfiguration.OfficeType), OneDriveConfiguration.Type),
+				driveInfo,
+				appName = _setting.Get("AppName"),
+				webName = _setting.Get("WebName"),
+				defaultDrive = _setting.Get("DefaultDrive"),
+				accountStatus = _setting.Get("AccountStatus"),
+				readme = _setting.Get("Readme"),
+				footer = _setting.Get("Footer"),
+				allowAnonymouslyUpload = !string.IsNullOrEmpty(_setting.Get("AllowAnonymouslyUpload")) && Convert.ToBoolean(_setting.Get("AllowAnonymouslyUpload")),
+				uploadPassword = _setting.Get("UploadPassword"),
+			}, new JsonSerializerSettings()
+			{
+				ContractResolver = new CamelCasePropertyNamesContractResolver()
+			});
+		}
+		catch (Exception ex)
+		{
+			return StatusCode(500, new ErrorResponse()
+			{
+				message = ex.Message
+			});
+		}
+	}
 
-        /// <summary>
-        /// 设置readme
-        /// </summary>
-        /// <param name="model"></param>
-        /// <returns></returns>
-        [HttpPost("readme")]
-        public async Task<IActionResult> UpdateReadme(ReadmeModel model)
-        {
-            try
-            {
-                await _setting.Set("Readme", model.text);
-            }
-            catch (Exception e)
-            {
-                return StatusCode(500, new ErrorResponse()
-                {
-                    message = e.Message
-                });
-            };
-            return StatusCode(204);
-        }
+	/// <summary>
+	/// 设置readme
+	/// </summary>
+	/// <param name="model"></param>
+	/// <returns></returns>
+	[HttpPost("readme")]
+	public async Task<IActionResult> UpdateReadme(ReadmeModel model)
+	{
+		try
+		{
+			await _setting.Set("Readme", model.text);
+		}
+		catch (Exception e)
+		{
+			return StatusCode(500, new ErrorResponse()
+			{
+				message = e.Message
+			});
+		};
+		return StatusCode(204);
+	}
 
-        /// <summary>
-        /// 更新
-        /// </summary>
-        /// <param name="settings"></param>
-        /// <returns></returns>
-        [HttpPost("settings")]
-        public async Task<IActionResult> UpdateSetting(UpdateSettings toSaveSetting)
-        {
-            try
-            {
-                await _setting.Set("AppName", toSaveSetting.appName);
-                await _setting.Set("WebName", toSaveSetting.webName);
-                await _setting.Set("DefaultDrive", toSaveSetting.defaultDrive);
-                await _setting.Set("Footer", toSaveSetting.footer);
-                await _setting.Set("UploadPassword", toSaveSetting.uploadPassword);
-                await _setting.Set("AllowAnonymouslyUpload", toSaveSetting.allowAnonymouslyUpload.ToString());
-            }
-            catch (Exception e)
-            {
-                return StatusCode(500, new ErrorResponse()
-                {
-                    message = e.Message
-                });
-            };
-            return StatusCode(204);
-        }
+	/// <summary>
+	/// 更新
+	/// </summary>
+	/// <param name="settings"></param>
+	/// <returns></returns>
+	[HttpPost("settings")]
+	public async Task<IActionResult> UpdateSetting(UpdateSettings toSaveSetting)
+	{
+		try
+		{
+			await _setting.Set("AppName", toSaveSetting.appName);
+			await _setting.Set("WebName", toSaveSetting.webName);
+			await _setting.Set("DefaultDrive", toSaveSetting.defaultDrive);
+			await _setting.Set("Footer", toSaveSetting.footer);
+			await _setting.Set("UploadPassword", toSaveSetting.uploadPassword);
+			await _setting.Set("AllowAnonymouslyUpload", toSaveSetting.allowAnonymouslyUpload.ToString());
+		}
+		catch (Exception e)
+		{
+			return StatusCode(500, new ErrorResponse()
+			{
+				message = e.Message
+			});
+		};
+		return StatusCode(204);
+	}
 
-        /// <summary>
-        /// 解除绑定
-        /// </summary>
-        /// <param name="nickName"></param>
-        /// <returns></returns>
-        [HttpDelete("sites")]
-        public async Task<IActionResult> Unbind(string nickName)
-        {
-            try
-            {
-                await _driveAccount.Unbind(nickName);
-            }
-            catch (Exception e)
-            {
-                return StatusCode(500, new ErrorResponse()
-                {
-                    message = e.Message
-                });
-            };
-            return StatusCode(204);
-        }
+	/// <summary>
+	/// 解除绑定
+	/// </summary>
+	/// <param name="nickName"></param>
+	/// <returns></returns>
+	[HttpDelete("sites")]
+	public async Task<IActionResult> Unbind(string nickName)
+	{
+		try
+		{
+			await _driveAccount.Unbind(nickName);
+		}
+		catch (Exception e)
+		{
+			return StatusCode(500, new ErrorResponse()
+			{
+				message = e.Message
+			});
+		};
+		return StatusCode(204);
+	}
 
-        /// <summary>
-        /// 更新站点设置
-        /// </summary>
-        /// <returns></returns>
-        [HttpPost("sites/settings")]
-        public async Task<IActionResult> UpdateSiteSettings(SiteSettingsModel model)
-        {
-            try
-            {
-                var site = _driveAccount.SiteContext.Sites.SingleOrDefault(site => site.Name == model.siteName);
-                if (site != null)
-                {
-                    site.NickName = model.nickName;
-                    site.HiddenFolders = model.hiddenFolders.Split(',');
-                    _driveAccount.SiteContext.Sites.Update(site);
-                    await _driveAccount.SiteContext.SaveChangesAsync();
-                }
-            }
-            catch (Exception ex)
-            {
-                return StatusCode(500, new ErrorResponse()
-                {
-                    message = ex.Message
-                });
-            }
-            return StatusCode(204);
-        }
+	/// <summary>
+	/// 更新站点设置
+	/// </summary>
+	/// <returns></returns>
+	[HttpPost("sites/settings")]
+	public async Task<IActionResult> UpdateSiteSettings(SiteSettingsModel model)
+	{
+		try
+		{
+			var site = _driveAccount.SiteContext.Sites.SingleOrDefault(site => site.Name == model.siteName);
+			if (site != null)
+			{
+				site.NickName = model.nickName;
+				site.HiddenFolders = model.hiddenFolders.Split(',');
+				_driveAccount.SiteContext.Sites.Update(site);
+				await _driveAccount.SiteContext.SaveChangesAsync();
+			}
+		}
+		catch (Exception ex)
+		{
+			return StatusCode(500, new ErrorResponse()
+			{
+				message = ex.Message
+			});
+		}
+		return StatusCode(204);
+	}
 
-        #region 接收表单模型
+	#region 接收表单模型
 
-        public class UpdateSettings
-        {
-            public string appName { get; set; }
+	public class UpdateSettings
+	{
+		public string appName { get; set; }
 
-            public string webName { get; set; }
+		public string webName { get; set; }
 
-            public string navImg { get; set; }
+		public string navImg { get; set; }
 
-            public string defaultDrive { get; set; }
+		public string defaultDrive { get; set; }
 
-            public string readme { get; set; }
+		public string readme { get; set; }
 
-            public string footer { get; set; }
+		public string footer { get; set; }
 
-            public bool allowAnonymouslyUpload { get; set; }
+		public bool allowAnonymouslyUpload { get; set; }
 
-            public string uploadPassword { get; set; }
-        }
+		public string uploadPassword { get; set; }
+	}
 
-        public class AddSiteModel
-        {
-            public string siteName { get; set; }
+	public class AddSiteModel
+	{
+		public string siteName { get; set; }
 
-            public string nickName { get; set; }
-        }
+		public string nickName { get; set; }
+	}
 
-        public class SiteSettingsModel
-        {
-            public string siteName { get; set; }
+	public class SiteSettingsModel
+	{
+		public string siteName { get; set; }
 
-            public string nickName { get; set; }
+		public string nickName { get; set; }
 
-            public string hiddenFolders { get; set; }
-        }
+		public string hiddenFolders { get; set; }
+	}
 
-        public class ReadmeModel
-        {
-            public string text { get; set; }
-        }
+	public class ReadmeModel
+	{
+		public string text { get; set; }
+	}
 
-        #endregion 接收表单模型
-    }
-}
+	#endregion 接收表单模型
+}

+ 19 - 20
src/Masuit.MyBlogs.Core/Controllers/Drive/DriveController.cs

@@ -3,25 +3,24 @@ using Masuit.MyBlogs.Core.Extensions.Firewall;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.Filters;
 
-namespace Masuit.MyBlogs.Core.Controllers.Drive
+namespace Masuit.MyBlogs.Core.Controllers.Drive;
+
+[ServiceFilter(typeof(FirewallAttribute))]
+public sealed class DriveController : Controller
 {
-    [ServiceFilter(typeof(FirewallAttribute))]
-    public sealed class DriveController : Controller
-    {
-        [HttpGet("/drive")]
-        public IActionResult Index()
-        {
-            return View();
-        }
+	[HttpGet("/drive")]
+	public IActionResult Index()
+	{
+		return View();
+	}
 
-        public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
-        {
-            if (CommonHelper.SystemSettings.GetOrAdd("CloseSite", "false") == "true")
-            {
-                context.Result = RedirectToAction("ComingSoon", "Error");
-                return Task.CompletedTask;
-            }
-            return next();
-        }
-    }
-}
+	public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+	{
+		if (CommonHelper.SystemSettings.GetOrAdd("CloseSite", "false") == "true")
+		{
+			context.Result = RedirectToAction("ComingSoon", "Error");
+			return Task.CompletedTask;
+		}
+		return next();
+	}
+}

+ 237 - 241
src/Masuit.MyBlogs.Core/Controllers/Drive/SitesController.cs

@@ -3,261 +3,257 @@ using Masuit.MyBlogs.Core.Extensions.DriveHelpers;
 using Masuit.MyBlogs.Core.Extensions.Firewall;
 using Masuit.MyBlogs.Core.Infrastructure.Drive;
 using Masuit.MyBlogs.Core.Models.Drive;
-using Masuit.MyBlogs.Core.Models.DTO;
-using Masuit.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools.Core.Net;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.Filters;
 using Newtonsoft.Json;
 using Newtonsoft.Json.Serialization;
 
-namespace Masuit.MyBlogs.Core.Controllers.Drive
+namespace Masuit.MyBlogs.Core.Controllers.Drive;
+
+[ApiController]
+[ServiceFilter(typeof(FirewallAttribute))]
+[Route("api/")]
+public sealed class SitesController : Controller
 {
-    [ApiController]
-    [ServiceFilter(typeof(FirewallAttribute))]
-    [Route("api/")]
-    public sealed class SitesController : Controller
-    {
-        private readonly IDriveAccountService _siteService;
-        private readonly IDriveService _driveService;
-        private readonly SettingService _setting;
+	private readonly IDriveAccountService _siteService;
+	private readonly IDriveService _driveService;
+	private readonly SettingService _setting;
 
-        public UserInfoDto CurrentUser => HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo) ?? new UserInfoDto();
+	public UserInfoDto CurrentUser => HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo) ?? new UserInfoDto();
 
-        public SitesController(IDriveAccountService siteService, IDriveService driveService, SettingService setting)
-        {
-            this._siteService = siteService;
-            this._driveService = driveService;
-            this._setting = setting;
-        }
+	public SitesController(IDriveAccountService siteService, IDriveService driveService, SettingService setting)
+	{
+		this._siteService = siteService;
+		this._driveService = driveService;
+		this._setting = setting;
+	}
 
-        /// <summary>
-        /// 返回所有sites
-        /// </summary>
-        /// <returns></returns>
-        [HttpGet("sites"), ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)]
-        public IActionResult GetSites()
-        {
-            return Json(_siteService.GetSites(), new JsonSerializerSettings()
-            {
-                ContractResolver = new CamelCasePropertyNamesContractResolver()
-            });
-        }
+	/// <summary>
+	/// 返回所有sites
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("sites"), ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)]
+	public IActionResult GetSites()
+	{
+		return Json(_siteService.GetSites(), new JsonSerializerSettings()
+		{
+			ContractResolver = new CamelCasePropertyNamesContractResolver()
+		});
+	}
 
-        /// <summary>
-        /// 根据路径获取文件夹内容
-        /// </summary>
-        /// <returns></returns>
-        [HttpGet("sites/{siteName}/{**path}"), ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)]
-        public async Task<IActionResult> GetDirectory(string siteName, string path)
-        {
-            if (string.IsNullOrEmpty(siteName))
-            {
-                return NotFound(new ErrorResponse()
-                {
-                    message = "找不到请求的 Site Name"
-                });
-            }
-            if (string.IsNullOrEmpty(path))
-            {
-                try
-                {
-                    var result = await _driveService.GetRootItems(siteName, CurrentUser.IsAdmin);
-                    return Json(result, new JsonSerializerSettings()
-                    {
-                        ContractResolver = new CamelCasePropertyNamesContractResolver()
-                    });
-                }
-                catch (Exception e)
-                {
-                    return StatusCode(500, e.Message);
-                }
-            }
-            else
-            {
-                try
-                {
-                    var result = await _driveService.GetDriveItemsByPath(path, siteName, CurrentUser.IsAdmin);
-                    if (result == null)
-                    {
-                        return NotFound(new ErrorResponse()
-                        {
-                            message = $"路径{path}不存在"
-                        });
-                    }
-                    return Json(result, new JsonSerializerSettings()
-                    {
-                        ContractResolver = new CamelCasePropertyNamesContractResolver()
-                    });
-                }
-                catch
-                {
-                    return NotFound(new ErrorResponse()
-                    {
-                        message = $"路径{path}不存在"
-                    });
-                }
-            }
-        }
+	/// <summary>
+	/// 根据路径获取文件夹内容
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("sites/{siteName}/{**path}"), ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)]
+	public async Task<IActionResult> GetDirectory(string siteName, string path)
+	{
+		if (string.IsNullOrEmpty(siteName))
+		{
+			return NotFound(new ErrorResponse()
+			{
+				message = "找不到请求的 Site Name"
+			});
+		}
+		if (string.IsNullOrEmpty(path))
+		{
+			try
+			{
+				var result = await _driveService.GetRootItems(siteName, CurrentUser.IsAdmin);
+				return Json(result, new JsonSerializerSettings()
+				{
+					ContractResolver = new CamelCasePropertyNamesContractResolver()
+				});
+			}
+			catch (Exception e)
+			{
+				return StatusCode(500, e.Message);
+			}
+		}
+		else
+		{
+			try
+			{
+				var result = await _driveService.GetDriveItemsByPath(path, siteName, CurrentUser.IsAdmin);
+				if (result == null)
+				{
+					return NotFound(new ErrorResponse()
+					{
+						message = $"路径{path}不存在"
+					});
+				}
+				return Json(result, new JsonSerializerSettings()
+				{
+					ContractResolver = new CamelCasePropertyNamesContractResolver()
+				});
+			}
+			catch
+			{
+				return NotFound(new ErrorResponse()
+				{
+					message = $"路径{path}不存在"
+				});
+			}
+		}
+	}
 
-        /// <summary>
-        /// 下载文件
-        /// </summary>
-        /// <param name="path"></param>
-        /// <returns></returns>
-        [HttpGet("files/{siteName}/{**path}"), ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)]
-        public async Task<IActionResult> Download(string siteName, string path)
-        {
-            try
-            {
-                var result = await _driveService.GetDriveItemByPath(path, siteName);
-                if (result != null)
-                {
-                    return Redirect(result.DownloadUrl);
-                }
+	/// <summary>
+	/// 下载文件
+	/// </summary>
+	/// <param name="path"></param>
+	/// <returns></returns>
+	[HttpGet("files/{siteName}/{**path}"), ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)]
+	public async Task<IActionResult> Download(string siteName, string path)
+	{
+		try
+		{
+			var result = await _driveService.GetDriveItemByPath(path, siteName);
+			if (result != null)
+			{
+				return Redirect(result.DownloadUrl);
+			}
 
-                return NotFound(new ErrorResponse()
-                {
-                    message = $"所求的{path}不存在"
-                });
-            }
-            catch (Exception e)
-            {
-                return StatusCode(500, e.Message);
-            }
-        }
+			return NotFound(new ErrorResponse()
+			{
+				message = $"所求的{path}不存在"
+			});
+		}
+		catch (Exception e)
+		{
+			return StatusCode(500, e.Message);
+		}
+	}
 
-        /// <summary>
-        /// 获取基本信息
-        /// </summary>
-        /// <returns></returns>
-        [HttpGet("info"), ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)]
-        public IActionResult GetInfo()
-        {
-            bool isAollowAnonymous = !string.IsNullOrEmpty(_setting.Get("AllowAnonymouslyUpload")) && Convert.ToBoolean(_setting.Get("AllowAnonymouslyUpload"));
-            return Json(new
-            {
-                appName = _setting.Get("AppName"),
-                webName = _setting.Get("WebName"),
-                defaultDrive = _setting.Get("DefaultDrive"),
-                readme = _setting.Get("Readme"),
-                footer = _setting.Get("Footer"),
-                allowUpload = isAollowAnonymous
-            }, new JsonSerializerSettings()
-            {
-                ContractResolver = new CamelCasePropertyNamesContractResolver()
-            });
-        }
+	/// <summary>
+	/// 获取基本信息
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("info"), ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)]
+	public IActionResult GetInfo()
+	{
+		bool isAollowAnonymous = !string.IsNullOrEmpty(_setting.Get("AllowAnonymouslyUpload")) && Convert.ToBoolean(_setting.Get("AllowAnonymouslyUpload"));
+		return Json(new
+		{
+			appName = _setting.Get("AppName"),
+			webName = _setting.Get("WebName"),
+			defaultDrive = _setting.Get("DefaultDrive"),
+			readme = _setting.Get("Readme"),
+			footer = _setting.Get("Footer"),
+			allowUpload = isAollowAnonymous
+		}, new JsonSerializerSettings()
+		{
+			ContractResolver = new CamelCasePropertyNamesContractResolver()
+		});
+	}
 
-        /// <summary>
-        /// 获得readme
-        /// </summary>
-        /// <returns></returns>
-        [HttpGet("readme"), ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)]
-        public IActionResult GetReadme()
-        {
-            return Json(new
-            {
-                readme = _setting.Get("Readme")
-            }, new JsonSerializerSettings()
-            {
-                ContractResolver = new CamelCasePropertyNamesContractResolver()
-            });
-        }
+	/// <summary>
+	/// 获得readme
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("readme"), ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)]
+	public IActionResult GetReadme()
+	{
+		return Json(new
+		{
+			readme = _setting.Get("Readme")
+		}, new JsonSerializerSettings()
+		{
+			ContractResolver = new CamelCasePropertyNamesContractResolver()
+		});
+	}
 
-        /// <summary>
-        /// 获取文件分片上传路径
-        /// </summary>
-        /// <returns></returns>
-        [HttpGet("upload/{siteName}/{**fileName}"), ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)]
-        public async Task<IActionResult> GetUploadUrl(string siteName, string fileName)
-        {
-            bool isAollowAnonymous = !string.IsNullOrEmpty(_setting.Get("AllowAnonymouslyUpload")) && Convert.ToBoolean(_setting.Get("AllowAnonymouslyUpload"));
-            if (!isAollowAnonymous)
-            {
-                if (Request.Headers.ContainsKey("Authorization"))
-                {
-                    if (!CurrentUser.IsAdmin)
-                    {
-                        return Unauthorized(new ErrorResponse()
-                        {
-                            message = "未经授权的访问"
-                        });
-                    }
-                }
-                else
-                {
-                    return Unauthorized(new ErrorResponse()
-                    {
-                        message = "未经授权的访问"
-                    });
-                }
-            }
-            string path = Path.Combine($"upload/{Guid.NewGuid()}", fileName);
-            try
-            {
-                var result = await _driveService.GetUploadUrl(path, siteName);
-                return Json(new
-                {
-                    requestUrl = result,
-                    fileUrl = $"{OneDriveConfiguration.BaseUri}/api/files/{siteName}/{path}"
-                }, new JsonSerializerSettings()
-                {
-                    ContractResolver = new CamelCasePropertyNamesContractResolver()
-                });
-            }
-            catch (Exception e)
-            {
-                return StatusCode(500, e.Message);
-            }
-        }
+	/// <summary>
+	/// 获取文件分片上传路径
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("upload/{siteName}/{**fileName}"), ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)]
+	public async Task<IActionResult> GetUploadUrl(string siteName, string fileName)
+	{
+		bool isAollowAnonymous = !string.IsNullOrEmpty(_setting.Get("AllowAnonymouslyUpload")) && Convert.ToBoolean(_setting.Get("AllowAnonymouslyUpload"));
+		if (!isAollowAnonymous)
+		{
+			if (Request.Headers.ContainsKey("Authorization"))
+			{
+				if (!CurrentUser.IsAdmin)
+				{
+					return Unauthorized(new ErrorResponse()
+					{
+						message = "未经授权的访问"
+					});
+				}
+			}
+			else
+			{
+				return Unauthorized(new ErrorResponse()
+				{
+					message = "未经授权的访问"
+				});
+			}
+		}
+		string path = Path.Combine($"upload/{Guid.NewGuid()}", fileName);
+		try
+		{
+			var result = await _driveService.GetUploadUrl(path, siteName);
+			return Json(new
+			{
+				requestUrl = result,
+				fileUrl = $"{OneDriveConfiguration.BaseUri}/api/files/{siteName}/{path}"
+			}, new JsonSerializerSettings()
+			{
+				ContractResolver = new CamelCasePropertyNamesContractResolver()
+			});
+		}
+		catch (Exception e)
+		{
+			return StatusCode(500, e.Message);
+		}
+	}
 
-        /// <summary>
-        /// 获取文件分片上传路径
-        /// </summary>
-        /// <returns></returns>
-        [HttpGet("cli/upload/{siteName}/:/{**path}")]
-        public async Task<IActionResult> GetUploadUrl(string siteName, string path, string uploadPassword)
-        {
-            if (uploadPassword != _setting.Get("UploadPassword"))
-            {
-                return Unauthorized(new ErrorResponse()
-                {
-                    message = "上传密码错误"
-                });
-            }
-            if (string.IsNullOrEmpty(path))
-            {
-                return BadRequest(new ErrorResponse()
-                {
-                    message = "必须存在上传路径"
-                });
-            }
-            try
-            {
-                var result = await _driveService.GetUploadUrl(path, siteName);
-                return Json(new
-                {
-                    requestUrl = result
-                }, new JsonSerializerSettings()
-                {
-                    ContractResolver = new CamelCasePropertyNamesContractResolver()
-                });
-            }
-            catch (Exception e)
-            {
-                return StatusCode(500, e.Message);
-            }
-        }
+	/// <summary>
+	/// 获取文件分片上传路径
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("cli/upload/{siteName}/:/{**path}")]
+	public async Task<IActionResult> GetUploadUrl(string siteName, string path, string uploadPassword)
+	{
+		if (uploadPassword != _setting.Get("UploadPassword"))
+		{
+			return Unauthorized(new ErrorResponse()
+			{
+				message = "上传密码错误"
+			});
+		}
+		if (string.IsNullOrEmpty(path))
+		{
+			return BadRequest(new ErrorResponse()
+			{
+				message = "必须存在上传路径"
+			});
+		}
+		try
+		{
+			var result = await _driveService.GetUploadUrl(path, siteName);
+			return Json(new
+			{
+				requestUrl = result
+			}, new JsonSerializerSettings()
+			{
+				ContractResolver = new CamelCasePropertyNamesContractResolver()
+			});
+		}
+		catch (Exception e)
+		{
+			return StatusCode(500, e.Message);
+		}
+	}
 
-        public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
-        {
-            if (CommonHelper.SystemSettings.GetOrAdd("CloseSite", "false") == "true")
-            {
-                context.Result = new BadRequestObjectResult(new { code = 403 });
-                return Task.CompletedTask;
-            }
-            return next();
-        }
-    }
-}
+	public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+	{
+		if (CommonHelper.SystemSettings.GetOrAdd("CloseSite", "false") == "true")
+		{
+			context.Result = new BadRequestObjectResult(new { code = 403 });
+			return Task.CompletedTask;
+		}
+		return next();
+	}
+}

+ 49 - 55
src/Masuit.MyBlogs.Core/Controllers/Drive/UserController.cs

@@ -1,67 +1,61 @@
 using Hangfire;
 using Masuit.MyBlogs.Core.Configs;
 using Masuit.MyBlogs.Core.Extensions.Hangfire;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
 using Masuit.MyBlogs.Core.Models.Drive;
-using Masuit.MyBlogs.Core.Models.Enum;
-using Masuit.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools.Core.Net;
-using Masuit.Tools.Security;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using System.Web;
 
-namespace Masuit.MyBlogs.Core.Controllers.Drive
+namespace Masuit.MyBlogs.Core.Controllers.Drive;
+
+[ApiController]
+[Route("api/[controller]")]
+public sealed class UserController : Controller
 {
-    [ApiController]
-    [Route("api/[controller]")]
-    public sealed class UserController : Controller
-    {
-        public IUserInfoService UserInfoService { get; set; }
+	public IUserInfoService UserInfoService { get; set; }
 
-        /// <summary>
-        /// 验证 返回Token
-        /// </summary>
-        /// <param name="model"></param>
-        /// <returns></returns>
-        [AllowAnonymous]
-        [HttpPost("authenticate")]
-        public IActionResult Authenticate(AuthenticateModel model)
-        {
-            var user = UserInfoService.Login(model.Username, model.Password);
-            if (user is null)
-            {
-                return BadRequest(new ErrorResponse()
-                {
-                    message = "错误的用户名或密码"
-                });
-            }
-            Response.Cookies.Append("username", HttpUtility.UrlEncode(model.Username.Trim()), new CookieOptions()
-            {
-                Expires = DateTime.Now.AddYears(1),
-                SameSite = SameSiteMode.Lax
-            });
-            Response.Cookies.Append("password", model.Password.Trim().DesEncrypt(AppConfig.BaiduAK), new CookieOptions()
-            {
-                Expires = DateTime.Now.AddYears(1),
-                SameSite = SameSiteMode.Lax
-            });
-            HttpContext.Session.Set(SessionKey.UserInfo, user);
-            BackgroundJob.Enqueue<IHangfireBackJob>(job => job.LoginRecord(user, HttpContext.Connection.RemoteIpAddress.ToString(), LoginType.Default));
-            return Ok(new
-            {
-                error = false,
-                id = model.Username,
-                username = model.Username,
-                token = model.Username
-            });
-        }
-    }
+	/// <summary>
+	/// 验证 返回Token
+	/// </summary>
+	/// <param name="model"></param>
+	/// <returns></returns>
+	[AllowAnonymous]
+	[HttpPost("authenticate")]
+	public IActionResult Authenticate(AuthenticateModel model)
+	{
+		var user = UserInfoService.Login(model.Username, model.Password);
+		if (user is null)
+		{
+			return BadRequest(new ErrorResponse()
+			{
+				message = "错误的用户名或密码"
+			});
+		}
+		Response.Cookies.Append("username", HttpUtility.UrlEncode(model.Username.Trim()), new CookieOptions()
+		{
+			Expires = DateTime.Now.AddYears(1),
+			SameSite = SameSiteMode.Lax
+		});
+		Response.Cookies.Append("password", model.Password.Trim().DesEncrypt(AppConfig.BaiduAK), new CookieOptions()
+		{
+			Expires = DateTime.Now.AddYears(1),
+			SameSite = SameSiteMode.Lax
+		});
+		HttpContext.Session.Set(SessionKey.UserInfo, user);
+		BackgroundJob.Enqueue<IHangfireBackJob>(job => job.LoginRecord(user, HttpContext.Connection.RemoteIpAddress.ToString(), LoginType.Default));
+		return Ok(new
+		{
+			error = false,
+			id = model.Username,
+			username = model.Username,
+			token = model.Username
+		});
+	}
+}
 
-    public class AuthenticateModel
-    {
-        public string Username { get; set; }
+public class AuthenticateModel
+{
+	public string Username { get; set; }
 
-        public string Password { get; set; }
-    }
-}
+	public string Password { get; set; }
+}

+ 207 - 212
src/Masuit.MyBlogs.Core/Controllers/ErrorController.cs

@@ -3,13 +3,9 @@ using Hangfire;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Configs;
 using Masuit.MyBlogs.Core.Extensions.Firewall;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
 using Masuit.Tools.AspNetCore.Mime;
 using Masuit.Tools.Core.Validator;
 using Masuit.Tools.Logging;
-using Masuit.Tools.Security;
-using Masuit.Tools.Strings;
-using Masuit.Tools.Systems;
 using Microsoft.AspNetCore.Diagnostics;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
@@ -19,213 +15,212 @@ using System.Text;
 using System.Web;
 using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 错误页
+/// </summary>
+[ApiExplorerSettings(IgnoreApi = true)]
+public sealed class ErrorController : Controller
 {
-    /// <summary>
-    /// 错误页
-    /// </summary>
-    [ApiExplorerSettings(IgnoreApi = true)]
-    public sealed class ErrorController : Controller
-    {
-        public IRedisClient RedisClient { get; set; }
-
-        /// <summary>
-        /// 404
-        /// </summary>
-        /// <returns></returns>
-        [Route("error"), Route("{*url}", Order = 99999), ResponseCache(Duration = 36000)]
-        public ActionResult Index()
-        {
-            Response.StatusCode = 404;
-            string accept = Request.Headers[HeaderNames.Accept] + "";
-            return true switch
-            {
-                _ when accept.StartsWith("image") => File("/Assets/images/404/4044.jpg", ContentType.Jpeg),
-                _ when Request.HasJsonContentType() || Request.Method == HttpMethods.Post => Json(new
-                {
-                    StatusCode = 404,
-                    Success = false,
-                    Message = "页面未找到!"
-                }),
-                _ => View("Index")
-            };
-        }
-
-        /// <summary>
-        /// 503
-        /// </summary>
-        /// <returns></returns>
-        [Route("ServiceUnavailable")]
-        public async Task<ActionResult> ServiceUnavailable()
-        {
-            var feature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
-            if (feature != null)
-            {
-                string err;
-                var ip = HttpContext.Connection.RemoteIpAddress;
-                switch (feature.Error)
-                {
-                    case DbUpdateConcurrencyException ex:
-                        err = $"数据库并发更新异常,更新表:{ex.Entries.Select(e => e.Metadata.Name)},请求路径({Request.Method}):{Request.Scheme}://{Request.Host}{HttpUtility.UrlDecode(feature.Path)}{Request.QueryString},客户端用户代理:{Request.Headers[HeaderNames.UserAgent]},客户端IP:{ip}\t{ex.InnerException?.Message},请求参数:\n{await GetRequestBody(Request)}\n堆栈信息:";
-                        LogManager.Error(err, ex.Demystify());
-                        break;
-
-                    case DbUpdateException ex:
-                        err = $"数据库更新时异常,更新表:{ex.Entries.Select(e => e.Metadata.Name)},请求路径({Request.Method}):{Request.Scheme}://{Request.Host}{HttpUtility.UrlDecode(feature.Path)}{Request.QueryString} ,客户端用户代理:{Request.Headers[HeaderNames.UserAgent]},客户端IP:{ip}\t{ex.InnerException?.Message},请求参数:\n{await GetRequestBody(Request)}\n堆栈信息:";
-                        LogManager.Error(err, ex.Demystify());
-                        break;
-
-                    case AggregateException ex:
-                        LogManager.Debug("↓↓↓" + ex.Message + "↓↓↓");
-                        ex.Flatten().Handle(e =>
-                        {
-                            LogManager.Error($"异常源:{e.Source},异常类型:{e.GetType().Name},请求路径({Request.Method}):{Request.Scheme}://{Request.Host}{HttpUtility.UrlDecode(feature.Path)}{Request.QueryString} ,客户端用户代理:{Request.Headers[HeaderNames.UserAgent]},客户端IP:{ip}\t", e.Demystify());
-                            return true;
-                        });
-                        var body = await GetRequestBody(Request);
-                        if (!string.IsNullOrEmpty(body))
-                        {
-                            LogManager.Debug("↑↑↑请求参数:\n" + body);
-                        }
-                        break;
-
-                    case AccessDenyException:
-                        var entry = ip.GetIPLocation();
-                        var tips = Template.Create(CommonHelper.SystemSettings.GetOrAdd("AccessDenyTips", @"<h4>遇到了什么问题?</h4>
+	public IRedisClient RedisClient { get; set; }
+
+	/// <summary>
+	/// 404
+	/// </summary>
+	/// <returns></returns>
+	[Route("error"), Route("{*url}", Order = 99999), ResponseCache(Duration = 36000)]
+	public ActionResult Index()
+	{
+		Response.StatusCode = 404;
+		string accept = Request.Headers[HeaderNames.Accept] + "";
+		return true switch
+		{
+			_ when accept.StartsWith("image") => File("/Assets/images/404/4044.jpg", ContentType.Jpeg),
+			_ when Request.HasJsonContentType() || Request.Method == HttpMethods.Post => Json(new
+			{
+				StatusCode = 404,
+				Success = false,
+				Message = "页面未找到!"
+			}),
+			_ => View("Index")
+		};
+	}
+
+	/// <summary>
+	/// 503
+	/// </summary>
+	/// <returns></returns>
+	[Route("ServiceUnavailable")]
+	public async Task<ActionResult> ServiceUnavailable()
+	{
+		var feature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
+		if (feature != null)
+		{
+			string err;
+			var ip = HttpContext.Connection.RemoteIpAddress;
+			switch (feature.Error)
+			{
+				case DbUpdateConcurrencyException ex:
+					err = $"数据库并发更新异常,更新表:{ex.Entries.Select(e => e.Metadata.Name)},请求路径({Request.Method}):{Request.Scheme}://{Request.Host}{HttpUtility.UrlDecode(feature.Path)}{Request.QueryString},客户端用户代理:{Request.Headers[HeaderNames.UserAgent]},客户端IP:{ip}\t{ex.InnerException?.Message},请求参数:\n{await GetRequestBody(Request)}\n堆栈信息:";
+					LogManager.Error(err, ex.Demystify());
+					break;
+
+				case DbUpdateException ex:
+					err = $"数据库更新时异常,更新表:{ex.Entries.Select(e => e.Metadata.Name)},请求路径({Request.Method}):{Request.Scheme}://{Request.Host}{HttpUtility.UrlDecode(feature.Path)}{Request.QueryString} ,客户端用户代理:{Request.Headers[HeaderNames.UserAgent]},客户端IP:{ip}\t{ex.InnerException?.Message},请求参数:\n{await GetRequestBody(Request)}\n堆栈信息:";
+					LogManager.Error(err, ex.Demystify());
+					break;
+
+				case AggregateException ex:
+					LogManager.Debug("↓↓↓" + ex.Message + "↓↓↓");
+					ex.Flatten().Handle(e =>
+					{
+						LogManager.Error($"异常源:{e.Source},异常类型:{e.GetType().Name},请求路径({Request.Method}):{Request.Scheme}://{Request.Host}{HttpUtility.UrlDecode(feature.Path)}{Request.QueryString} ,客户端用户代理:{Request.Headers[HeaderNames.UserAgent]},客户端IP:{ip}\t", e.Demystify());
+						return true;
+					});
+					var body = await GetRequestBody(Request);
+					if (!string.IsNullOrEmpty(body))
+					{
+						LogManager.Debug("↑↑↑请求参数:\n" + body);
+					}
+					break;
+
+				case AccessDenyException:
+					var entry = ip.GetIPLocation();
+					var tips = Template.Create(CommonHelper.SystemSettings.GetOrAdd("AccessDenyTips", @"<h4>遇到了什么问题?</h4>
                 <h4>基于主观因素考虑,您所在的地区暂时不允许访问本站,如有疑问,请联系站长!或者请联系站长开通本站的访问权限!</h4>")).Set("clientip", ip.ToString()).Set("location", entry.Address).Set("network", entry.Network).Render();
-                        Response.StatusCode = 403;
-                        return View("AccessDeny", tips);
-
-                    case TempDenyException:
-                        Response.StatusCode = 429;
-                        return Request.HasJsonContentType() || Request.Method == HttpMethods.Post ? Json(new
-                        {
-                            StatusCode = 429,
-                            Success = false,
-                            Message = $"检测到您的IP({ip})访问过于频繁,已被本站暂时禁止访问,请稍后再试!"
-                        }) : View("TempDeny");
-
-                    default:
-                        LogManager.Error($"异常源:{feature.Error.Source},异常类型:{feature.Error.GetType().Name},请求路径({Request.Method}):{Request.Scheme}://{Request.Host}{HttpUtility.UrlDecode(feature.Path)}{Request.QueryString} ,客户端用户代理:{Request.Headers[HeaderNames.UserAgent]},客户端IP:{ip},请求参数:\n{await GetRequestBody(Request)}\n堆栈信息:", feature.Error.Demystify());
-                        break;
-                }
-            }
-
-            Response.StatusCode = 503;
-            return Request.HasJsonContentType() || Request.Method == HttpMethods.Post ? Json(new
-            {
-                StatusCode = 503,
-                Success = false,
-                Message = "服务器发生错误!"
-            }) : View();
-        }
-
-        private static async Task<string> GetRequestBody(HttpRequest req)
-        {
-            if (req.ContentLength > 5120)
-            {
-                return "请求体超长";
-            }
-
-            req.Body.Seek(0, SeekOrigin.Begin);
-            using var sr = new StreamReader(req.Body, Encoding.UTF8, false);
-            var body = await sr.ReadToEndAsync();
-            body = HttpUtility.UrlDecode(body);
-            req.Body.Position = 0;
-            return body;
-        }
-
-        /// <summary>
-        /// 网站升级中
-        /// </summary>
-        /// <returns></returns>
-        [Route("ComingSoon"), ResponseCache(Duration = 360000)]
-        public ActionResult ComingSoon()
-        {
-            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 = RedisClient.Get("token:" + email);
-            if (!token.Equals(s))
-            {
-                return ResultData(null, false, "访问密码不正确!");
-            }
-
-            Response.Cookies.Append("Email", email, new CookieOptions
-            {
-                Expires = DateTime.Now.AddYears(1),
-                SameSite = SameSiteMode.Lax
-            });
-            Response.Cookies.Append("FullAccessToken", email.MDString(AppConfig.BaiduAK), new CookieOptions
-            {
-                Expires = DateTime.Now.AddYears(1),
-                SameSite = SameSiteMode.Lax
-            });
-            return ResultData(null);
-        }
-
-        /// <summary>
-        /// 检查授权邮箱
-        /// </summary>
-        /// <param name="userInfoService"></param>
-        /// <param name="email"></param>
-        /// <returns></returns>
-        [HttpPost, ValidateAntiForgeryToken, AllowAccessFirewall, ResponseCache(Duration = 100, VaryByQueryKeys = new[] { "email" })]
-        public ActionResult GetViewToken([FromServices] IUserInfoService userInfoService, string email)
-        {
-            var validator = new IsEmailAttribute();
-            if (!validator.IsValid(email))
-            {
-                return ResultData(null, false, validator.ErrorMessage);
-            }
-
-            if (RedisClient.Exists("get:" + email))
-            {
-                RedisClient.Expire("get:" + email, 120);
-                return ResultData(null, false, "发送频率限制,请在2分钟后重新尝试发送邮件!请检查你的邮件,若未收到,请检查你的邮箱地址或邮件垃圾箱!");
-            }
-
-            if (!userInfoService.Any(b => b.Email == email))
-            {
-                return ResultData(null, false, "您目前没有权限访问这个链接,请联系站长开通访问权限!");
-            }
-
-            var token = SnowFlake.GetInstance().GetUniqueShortId(6);
-            RedisClient.Set("token:" + email, token, 86400);
-            BackgroundJob.Enqueue(() => CommonHelper.SendMail(Request.Host + "博客访问验证码", $"{Request.Host}本次验证码是:<span style='color:red'>{token}</span>,有效期为24h,请按时使用!", email, HttpContext.Connection.RemoteIpAddress.ToString()));
-            RedisClient.Set("get:" + email, token, 120);
-            return ResultData(null);
-        }
-
-        /// <summary>
-        /// 响应数据
-        /// </summary>
-        /// <param name="data">数据</param>
-        /// <param name="success">响应状态</param>
-        /// <param name="message">响应消息</param>
-        /// <returns></returns>
-        public ActionResult ResultData(object data, bool success = true, string message = "")
-        {
-            return Ok(new
-            {
-                Success = success,
-                Message = message,
-                Data = data
-            });
-        }
-    }
-}
+					Response.StatusCode = 403;
+					return View("AccessDeny", tips);
+
+				case TempDenyException:
+					Response.StatusCode = 429;
+					return Request.HasJsonContentType() || Request.Method == HttpMethods.Post ? Json(new
+					{
+						StatusCode = 429,
+						Success = false,
+						Message = $"检测到您的IP({ip})访问过于频繁,已被本站暂时禁止访问,请稍后再试!"
+					}) : View("TempDeny");
+
+				default:
+					LogManager.Error($"异常源:{feature.Error.Source},异常类型:{feature.Error.GetType().Name},请求路径({Request.Method}):{Request.Scheme}://{Request.Host}{HttpUtility.UrlDecode(feature.Path)}{Request.QueryString} ,客户端用户代理:{Request.Headers[HeaderNames.UserAgent]},客户端IP:{ip},请求参数:\n{await GetRequestBody(Request)}\n堆栈信息:", feature.Error.Demystify());
+					break;
+			}
+		}
+
+		Response.StatusCode = 503;
+		return Request.HasJsonContentType() || Request.Method == HttpMethods.Post ? Json(new
+		{
+			StatusCode = 503,
+			Success = false,
+			Message = "服务器发生错误!"
+		}) : View();
+	}
+
+	private static async Task<string> GetRequestBody(HttpRequest req)
+	{
+		if (req.ContentLength > 5120)
+		{
+			return "请求体超长";
+		}
+
+		req.Body.Seek(0, SeekOrigin.Begin);
+		using var sr = new StreamReader(req.Body, Encoding.UTF8, false);
+		var body = await sr.ReadToEndAsync();
+		body = HttpUtility.UrlDecode(body);
+		req.Body.Position = 0;
+		return body;
+	}
+
+	/// <summary>
+	/// 网站升级中
+	/// </summary>
+	/// <returns></returns>
+	[Route("ComingSoon"), ResponseCache(Duration = 360000)]
+	public ActionResult ComingSoon()
+	{
+		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 = RedisClient.Get("token:" + email);
+		if (!token.Equals(s))
+		{
+			return ResultData(null, false, "访问密码不正确!");
+		}
+
+		Response.Cookies.Append("Email", email, new CookieOptions
+		{
+			Expires = DateTime.Now.AddYears(1),
+			SameSite = SameSiteMode.Lax
+		});
+		Response.Cookies.Append("FullAccessToken", email.MDString(AppConfig.BaiduAK), new CookieOptions
+		{
+			Expires = DateTime.Now.AddYears(1),
+			SameSite = SameSiteMode.Lax
+		});
+		return ResultData(null);
+	}
+
+	/// <summary>
+	/// 检查授权邮箱
+	/// </summary>
+	/// <param name="userInfoService"></param>
+	/// <param name="email"></param>
+	/// <returns></returns>
+	[HttpPost, ValidateAntiForgeryToken, AllowAccessFirewall, ResponseCache(Duration = 100, VaryByQueryKeys = new[] { "email" })]
+	public ActionResult GetViewToken([FromServices] IUserInfoService userInfoService, string email)
+	{
+		var validator = new IsEmailAttribute();
+		if (!validator.IsValid(email))
+		{
+			return ResultData(null, false, validator.ErrorMessage);
+		}
+
+		if (RedisClient.Exists("get:" + email))
+		{
+			RedisClient.Expire("get:" + email, 120);
+			return ResultData(null, false, "发送频率限制,请在2分钟后重新尝试发送邮件!请检查你的邮件,若未收到,请检查你的邮箱地址或邮件垃圾箱!");
+		}
+
+		if (!userInfoService.Any(b => b.Email == email))
+		{
+			return ResultData(null, false, "您目前没有权限访问这个链接,请联系站长开通访问权限!");
+		}
+
+		var token = SnowFlake.GetInstance().GetUniqueShortId(6);
+		RedisClient.Set("token:" + email, token, 86400);
+		BackgroundJob.Enqueue(() => CommonHelper.SendMail(Request.Host + "博客访问验证码", $"{Request.Host}本次验证码是:<span style='color:red'>{token}</span>,有效期为24h,请按时使用!", email, HttpContext.Connection.RemoteIpAddress.ToString()));
+		RedisClient.Set("get:" + email, token, 120);
+		return ResultData(null);
+	}
+
+	/// <summary>
+	/// 响应数据
+	/// </summary>
+	/// <param name="data">数据</param>
+	/// <param name="success">响应状态</param>
+	/// <param name="message">响应消息</param>
+	/// <returns></returns>
+	public ActionResult ResultData(object data, bool success = true, string message = "")
+	{
+		return Ok(new
+		{
+			Success = success,
+			Message = message,
+			Data = data
+		});
+	}
+}

+ 267 - 271
src/Masuit.MyBlogs.Core/Controllers/FileController.cs

@@ -1,306 +1,302 @@
 using FreeRedis;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
-using Masuit.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools;
 using Masuit.Tools.AspNetCore.ModelBinder;
 using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
 using Masuit.Tools.Files;
 using Masuit.Tools.Logging;
-using Masuit.Tools.Security;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using Polly;
 using System.Diagnostics;
 using System.Text;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 资源管理器
+/// </summary>
+[Route("[controller]/[action]")]
+public sealed class FileController : AdminController
 {
-    /// <summary>
-    /// 资源管理器
-    /// </summary>
-    [Route("[controller]/[action]")]
-    public sealed class FileController : AdminController
-    {
-        public IWebHostEnvironment HostEnvironment { get; set; }
+	public IWebHostEnvironment HostEnvironment { get; set; }
 
-        /// <summary>
-        ///
-        /// </summary>
-        public ISevenZipCompressor SevenZipCompressor { get; set; }
+	/// <summary>
+	///
+	/// </summary>
+	public ISevenZipCompressor SevenZipCompressor { get; set; }
 
-        public IRedisClient RedisClient { get; set; }
+	public IRedisClient RedisClient { get; set; }
 
-        /// <summary>
-        /// 获取文件列表
-        /// </summary>
-        /// <param name="path"></param>
-        /// <returns></returns>
-        public ActionResult GetFiles([FromBodyOrDefault] string path)
-        {
-            var files = Directory.GetFiles(HostEnvironment.WebRootPath + path).OrderByDescending(s => s).Select(s => new
-            {
-                filename = Path.GetFileName(s),
-                path = s
-            });
-            return ResultData(files);
-        }
+	/// <summary>
+	/// 获取文件列表
+	/// </summary>
+	/// <param name="path"></param>
+	/// <returns></returns>
+	public ActionResult GetFiles([FromBodyOrDefault] string path)
+	{
+		var files = Directory.GetFiles(HostEnvironment.WebRootPath + path).OrderByDescending(s => s).Select(s => new
+		{
+			filename = Path.GetFileName(s),
+			path = s
+		});
+		return ResultData(files);
+	}
 
-        /// <summary>
-        /// 读取文件内容
-        /// </summary>
-        /// <param name="filename"></param>
-        /// <returns></returns>
-        public ActionResult Read([FromBodyOrDefault] string filename)
-        {
-            if (System.IO.File.Exists(filename))
-            {
-                string text = new FileInfo(filename).ShareReadWrite().ReadAllText(Encoding.UTF8);
-                return ResultData(text);
-            }
-            return ResultData(null, false, "文件不存在!");
-        }
+	/// <summary>
+	/// 读取文件内容
+	/// </summary>
+	/// <param name="filename"></param>
+	/// <returns></returns>
+	public ActionResult Read([FromBodyOrDefault] string filename)
+	{
+		if (System.IO.File.Exists(filename))
+		{
+			string text = new FileInfo(filename).ShareReadWrite().ReadAllText(Encoding.UTF8);
+			return ResultData(text);
+		}
+		return ResultData(null, false, "文件不存在!");
+	}
 
-        /// <summary>
-        /// 保存文件
-        /// </summary>
-        /// <param name="filename"></param>
-        /// <param name="content"></param>
-        /// <returns></returns>
-        public ActionResult Save([FromBodyOrDefault] string filename, [FromBodyOrDefault] string content)
-        {
-            try
-            {
-                new FileInfo(filename).ShareReadWrite().WriteAllText(content, Encoding.UTF8);
-                return ResultData(null, message: "保存成功");
-            }
-            catch (IOException e)
-            {
-                LogManager.Error(GetType(), e.Demystify());
-                return ResultData(null, false, "保存失败");
-            }
-        }
+	/// <summary>
+	/// 保存文件
+	/// </summary>
+	/// <param name="filename"></param>
+	/// <param name="content"></param>
+	/// <returns></returns>
+	public ActionResult Save([FromBodyOrDefault] string filename, [FromBodyOrDefault] string content)
+	{
+		try
+		{
+			new FileInfo(filename).ShareReadWrite().WriteAllText(content, Encoding.UTF8);
+			return ResultData(null, message: "保存成功");
+		}
+		catch (IOException e)
+		{
+			LogManager.Error(GetType(), e.Demystify());
+			return ResultData(null, false, "保存失败");
+		}
+	}
 
-        /// <summary>
-        /// 上传文件
-        /// </summary>
-        /// <returns></returns>
-        [HttpPost]
-        public async Task<ActionResult> Upload([FromBodyOrDefault] string destination)
-        {
-            var form = await Request.ReadFormAsync();
-            foreach (var t in form.Files)
-            {
-                string path = Path.Combine(HostEnvironment.ContentRootPath, CommonHelper.SystemSettings["PathRoot"].TrimStart('\\', '/'), destination.TrimStart('\\', '/'), t.FileName);
-                await using var fs = new FileStream(path, FileMode.Create, FileAccess.ReadWrite);
-                await t.CopyToAsync(fs);
-            }
-            return Json(new
-            {
-                result = new List<object>()
-            });
-        }
+	/// <summary>
+	/// 上传文件
+	/// </summary>
+	/// <returns></returns>
+	[HttpPost]
+	public async Task<ActionResult> Upload([FromBodyOrDefault] string destination)
+	{
+		var form = await Request.ReadFormAsync();
+		foreach (var t in form.Files)
+		{
+			string path = Path.Combine(HostEnvironment.ContentRootPath, CommonHelper.SystemSettings["PathRoot"].TrimStart('\\', '/'), destination.TrimStart('\\', '/'), t.FileName);
+			await using var fs = new FileStream(path, FileMode.Create, FileAccess.ReadWrite);
+			await t.CopyToAsync(fs);
+		}
+		return Json(new
+		{
+			result = new List<object>()
+		});
+	}
 
-        /// <summary>
-        /// 操作文件
-        /// </summary>
-        /// <param name="req"></param>
-        /// <returns></returns>
-        [HttpPost]
-        public ActionResult Handle([FromBody] FileRequest req)
-        {
-            var list = new List<object>();
-            var root = Path.Combine(HostEnvironment.ContentRootPath, CommonHelper.SystemSettings["PathRoot"].TrimStart('\\', '/'));
-            switch (req.Action)
-            {
-                case "list":
-                    var path = Path.Combine(root, req.Path.TrimStart('\\', '/'));
-                    var dirs = Directory.GetDirectories(path);
-                    var files = Directory.GetFiles(path);
-                    list.AddRange(dirs.Select(s => new DirectoryInfo(s)).Select(dirinfo => new FileList
-                    {
-                        date = dirinfo.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss"),
-                        name = dirinfo.Name,
-                        size = 0,
-                        type = "dir"
-                    }).Union(files.Select(s => new FileInfo(s)).Select(info => new FileList
-                    {
-                        name = info.Name,
-                        size = info.Length,
-                        date = info.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss"),
-                        type = "file"
-                    })));
-                    break;
+	/// <summary>
+	/// 操作文件
+	/// </summary>
+	/// <param name="req"></param>
+	/// <returns></returns>
+	[HttpPost]
+	public ActionResult Handle([FromBody] FileRequest req)
+	{
+		var list = new List<object>();
+		var root = Path.Combine(HostEnvironment.ContentRootPath, CommonHelper.SystemSettings["PathRoot"].TrimStart('\\', '/'));
+		switch (req.Action)
+		{
+			case "list":
+				var path = Path.Combine(root, req.Path.TrimStart('\\', '/'));
+				var dirs = Directory.GetDirectories(path);
+				var files = Directory.GetFiles(path);
+				list.AddRange(dirs.Select(s => new DirectoryInfo(s)).Select(dirinfo => new FileList
+				{
+					date = dirinfo.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss"),
+					name = dirinfo.Name,
+					size = 0,
+					type = "dir"
+				}).Union(files.Select(s => new FileInfo(s)).Select(info => new FileList
+				{
+					name = info.Name,
+					size = info.Length,
+					date = info.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss"),
+					type = "file"
+				})));
+				break;
 
-                case "remove":
-                    req.Items.ForEach(s =>
-                    {
-                        s = Path.Combine(root, s.TrimStart('\\', '/'));
-                        try
-                        {
-                            Policy.Handle<IOException>().WaitAndRetry(5, i => TimeSpan.FromSeconds(1)).Execute(() => System.IO.File.Delete(s));
-                        }
-                        catch
-                        {
-                            Policy.Handle<IOException>().WaitAndRetry(5, i => TimeSpan.FromSeconds(1)).Execute(() => Directory.Delete(s, true));
-                        }
-                    });
-                    list.Add(new
-                    {
-                        success = "true"
-                    });
-                    break;
+			case "remove":
+				req.Items.ForEach(s =>
+				{
+					s = Path.Combine(root, s.TrimStart('\\', '/'));
+					try
+					{
+						Policy.Handle<IOException>().WaitAndRetry(5, i => TimeSpan.FromSeconds(1)).Execute(() => System.IO.File.Delete(s));
+					}
+					catch
+					{
+						Policy.Handle<IOException>().WaitAndRetry(5, i => TimeSpan.FromSeconds(1)).Execute(() => Directory.Delete(s, true));
+					}
+				});
+				list.Add(new
+				{
+					success = "true"
+				});
+				break;
 
-                case "rename":
-                case "move":
-                    string newpath;
-                    if (!string.IsNullOrEmpty(req.Item))
-                    {
-                        newpath = Path.Combine(root, req.NewItemPath?.TrimStart('\\', '/'));
-                        path = Path.Combine(root, req.Item.TrimStart('\\', '/'));
-                        try
-                        {
-                            System.IO.File.Move(path, newpath);
-                        }
-                        catch
-                        {
-                            Directory.Move(path, newpath);
-                        }
-                    }
-                    else
-                    {
-                        newpath = Path.Combine(root, req.NewPath.TrimStart('\\', '/'));
-                        req.Items.ForEach(s =>
-                        {
-                            try
-                            {
-                                System.IO.File.Move(Path.Combine(root, s.TrimStart('\\', '/')), Path.Combine(newpath, Path.GetFileName(s)));
-                            }
-                            catch
-                            {
-                                Directory.Move(Path.Combine(root, s.TrimStart('\\', '/')), Path.Combine(newpath, Path.GetFileName(s)));
-                            }
-                        });
-                    }
-                    list.Add(new
-                    {
-                        success = "true"
-                    });
-                    break;
+			case "rename":
+			case "move":
+				string newpath;
+				if (!string.IsNullOrEmpty(req.Item))
+				{
+					newpath = Path.Combine(root, req.NewItemPath?.TrimStart('\\', '/'));
+					path = Path.Combine(root, req.Item.TrimStart('\\', '/'));
+					try
+					{
+						System.IO.File.Move(path, newpath);
+					}
+					catch
+					{
+						Directory.Move(path, newpath);
+					}
+				}
+				else
+				{
+					newpath = Path.Combine(root, req.NewPath.TrimStart('\\', '/'));
+					req.Items.ForEach(s =>
+					{
+						try
+						{
+							System.IO.File.Move(Path.Combine(root, s.TrimStart('\\', '/')), Path.Combine(newpath, Path.GetFileName(s)));
+						}
+						catch
+						{
+							Directory.Move(Path.Combine(root, s.TrimStart('\\', '/')), Path.Combine(newpath, Path.GetFileName(s)));
+						}
+					});
+				}
+				list.Add(new
+				{
+					success = "true"
+				});
+				break;
 
-                case "copy":
-                    if (!string.IsNullOrEmpty(req.Item))
-                    {
-                        System.IO.File.Copy(Path.Combine(root, req.Item.TrimStart('\\', '/')), Path.Combine(root, req.NewItemPath.TrimStart('\\', '/')), true);
-                    }
-                    else
-                    {
-                        newpath = Path.Combine(root, req.NewPath.TrimStart('\\', '/'));
-                        req.Items.ForEach(s => System.IO.File.Copy(Path.Combine(root, s.TrimStart('\\', '/')), !string.IsNullOrEmpty(req.SingleFilename) ? Path.Combine(newpath, req.SingleFilename) : Path.Combine(newpath, Path.GetFileName(s)), true));
-                    }
-                    list.Add(new
-                    {
-                        success = "true"
-                    });
-                    break;
+			case "copy":
+				if (!string.IsNullOrEmpty(req.Item))
+				{
+					System.IO.File.Copy(Path.Combine(root, req.Item.TrimStart('\\', '/')), Path.Combine(root, req.NewItemPath.TrimStart('\\', '/')), true);
+				}
+				else
+				{
+					newpath = Path.Combine(root, req.NewPath.TrimStart('\\', '/'));
+					req.Items.ForEach(s => System.IO.File.Copy(Path.Combine(root, s.TrimStart('\\', '/')), !string.IsNullOrEmpty(req.SingleFilename) ? Path.Combine(newpath, req.SingleFilename) : Path.Combine(newpath, Path.GetFileName(s)), true));
+				}
+				list.Add(new
+				{
+					success = "true"
+				});
+				break;
 
-                case "edit":
-                    new FileInfo(Path.Combine(root, req.Item.TrimStart('\\', '/'))).ShareReadWrite().WriteAllText(req.Content, Encoding.UTF8);
-                    list.Add(new
-                    {
-                        success = "true"
-                    });
-                    break;
+			case "edit":
+				new FileInfo(Path.Combine(root, req.Item.TrimStart('\\', '/'))).ShareReadWrite().WriteAllText(req.Content, Encoding.UTF8);
+				list.Add(new
+				{
+					success = "true"
+				});
+				break;
 
-                case "getContent":
-                    return Json(new
-                    {
-                        result = new FileInfo(Path.Combine(root, req.Item.TrimStart('\\', '/'))).ShareReadWrite().ReadAllText(Encoding.UTF8)
-                    });
+			case "getContent":
+				return Json(new
+				{
+					result = new FileInfo(Path.Combine(root, req.Item.TrimStart('\\', '/'))).ShareReadWrite().ReadAllText(Encoding.UTF8)
+				});
 
-                case "createFolder":
-                    list.Add(new
-                    {
-                        success = Directory.CreateDirectory(Path.Combine(root, req.NewPath.TrimStart('\\', '/'))).Exists.ToString()
-                    });
-                    break;
+			case "createFolder":
+				list.Add(new
+				{
+					success = Directory.CreateDirectory(Path.Combine(root, req.NewPath.TrimStart('\\', '/'))).Exists.ToString()
+				});
+				break;
 
-                case "changePermissions":
+			case "changePermissions":
 
-                    //todo:文件权限修改
-                    break;
+				//todo:文件权限修改
+				break;
 
-                case "compress":
-                    var filename = Path.Combine(Path.Combine(root, req.Destination.TrimStart('\\', '/')), Path.GetFileNameWithoutExtension(req.CompressedFilename) + ".zip");
-                    SevenZipCompressor.Zip(req.Items.Select(s => Path.Combine(root, s.TrimStart('\\', '/'))), filename);
-                    list.Add(new
-                    {
-                        success = "true"
-                    });
-                    break;
+			case "compress":
+				var filename = Path.Combine(Path.Combine(root, req.Destination.TrimStart('\\', '/')), Path.GetFileNameWithoutExtension(req.CompressedFilename) + ".zip");
+				SevenZipCompressor.Zip(req.Items.Select(s => Path.Combine(root, s.TrimStart('\\', '/'))), filename);
+				list.Add(new
+				{
+					success = "true"
+				});
+				break;
 
-                case "extract":
-                    var folder = Path.Combine(Path.Combine(root, req.Destination.TrimStart('\\', '/')), req.FolderName.Trim('/', '\\'));
-                    var zip = Path.Combine(root, req.Item.TrimStart('\\', '/'));
-                    SevenZipCompressor.Decompress(zip, folder);
-                    list.Add(new
-                    {
-                        success = "true"
-                    });
-                    break;
-            }
-            return Json(new
-            {
-                result = list
-            });
-        }
+			case "extract":
+				var folder = Path.Combine(Path.Combine(root, req.Destination.TrimStart('\\', '/')), req.FolderName.Trim('/', '\\'));
+				var zip = Path.Combine(root, req.Item.TrimStart('\\', '/'));
+				SevenZipCompressor.Decompress(zip, folder);
+				list.Add(new
+				{
+					success = "true"
+				});
+				break;
+		}
+		return Json(new
+		{
+			result = list
+		});
+	}
 
-        /// <summary>
-        /// 下载文件
-        /// </summary>
-        /// <param name="path"></param>
-        /// <param name="items"></param>
-        /// <param name="toFilename"></param>
-        /// <returns></returns>
-        [HttpGet]
-        public ActionResult Handle(string path, string[] items, string toFilename)
-        {
-            path = path?.TrimStart('\\', '/') ?? "";
-            var token = Guid.NewGuid().ToString().MDString(Guid.NewGuid().ToString()).FromBinaryBig(16).ToBinary(62);
-            RedisClient.Set("FileManager:Token:" + token, "1");
-            RedisClient.Expire("FileManager:Token:" + token, TimeSpan.FromDays(1));
-            return RedirectToAction("Download", "File", new { path, items, toFilename, token });
-        }
+	/// <summary>
+	/// 下载文件
+	/// </summary>
+	/// <param name="path"></param>
+	/// <param name="items"></param>
+	/// <param name="toFilename"></param>
+	/// <returns></returns>
+	[HttpGet]
+	public ActionResult Handle(string path, string[] items, string toFilename)
+	{
+		path = path?.TrimStart('\\', '/') ?? "";
+		var token = Guid.NewGuid().ToString().MDString(Guid.NewGuid().ToString()).FromBinaryBig(16).ToBinary(62);
+		RedisClient.Set("FileManager:Token:" + token, "1");
+		RedisClient.Expire("FileManager:Token:" + token, TimeSpan.FromDays(1));
+		return RedirectToAction("Download", "File", new { path, items, toFilename, token });
+	}
 
-        /// <summary>
-        /// 下载文件
-        /// </summary>
-        /// <param name="path"></param>
-        /// <param name="items"></param>
-        /// <param name="toFilename"></param>
-        /// <param name="token">访问token</param>
-        /// <returns></returns>
-        [HttpGet("{**path}"), AllowAnonymous]
-        public ActionResult Download(string path, string[] items, string toFilename, string token)
-        {
-            if (RedisClient.Exists("FileManager:Token:" + token))
-            {
-                var root = CommonHelper.SystemSettings["PathRoot"].TrimStart('\\', '/');
-                if (items.Length > 0)
-                {
-                    using var ms = SevenZipCompressor.ZipStream(items.Select(s => Path.Combine(HostEnvironment.ContentRootPath, root, s.TrimStart('\\', '/'))));
-                    var buffer = ms.ToArray();
-                    return this.ResumeFile(buffer, Path.GetFileName(toFilename));
-                }
+	/// <summary>
+	/// 下载文件
+	/// </summary>
+	/// <param name="path"></param>
+	/// <param name="items"></param>
+	/// <param name="toFilename"></param>
+	/// <param name="token">访问token</param>
+	/// <returns></returns>
+	[HttpGet("{**path}"), AllowAnonymous]
+	public ActionResult Download(string path, string[] items, string toFilename, string token)
+	{
+		if (RedisClient.Exists("FileManager:Token:" + token))
+		{
+			var root = CommonHelper.SystemSettings["PathRoot"].TrimStart('\\', '/');
+			if (items.Length > 0)
+			{
+				using var ms = SevenZipCompressor.ZipStream(items.Select(s => Path.Combine(HostEnvironment.ContentRootPath, root, s.TrimStart('\\', '/'))));
+				var buffer = ms.ToArray();
+				return this.ResumeFile(buffer, Path.GetFileName(toFilename));
+			}
 
-                var file = Path.Combine(HostEnvironment.ContentRootPath, root, path);
-                if (System.IO.File.Exists(file))
-                {
-                    return this.ResumePhysicalFile(file, Path.GetFileName(file));
-                }
-            }
+			var file = Path.Combine(HostEnvironment.ContentRootPath, root, path);
+			if (System.IO.File.Exists(file))
+			{
+				return this.ResumePhysicalFile(file, Path.GetFileName(file));
+			}
+		}
 
-            throw new NotFoundException("文件未找到");
-        }
-    }
-}
+		throw new NotFoundException("文件未找到");
+	}
+}

+ 148 - 153
src/Masuit.MyBlogs.Core/Controllers/FirewallController.cs

@@ -3,13 +3,8 @@ using FreeRedis;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Configs;
 using Masuit.MyBlogs.Core.Extensions.Firewall;
-using Masuit.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools;
 using Masuit.Tools.AspNetCore.Mime;
 using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
-using Masuit.Tools.Core.Net;
-using Masuit.Tools.Security;
-using Masuit.Tools.Strings;
 using Microsoft.AspNetCore.Http.Extensions;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Net.Http.Headers;
@@ -20,167 +15,167 @@ namespace Masuit.MyBlogs.Core.Controllers;
 
 public sealed class FirewallController : Controller
 {
-    public IRedisClient RedisClient { get; set; }
-    private readonly HttpClient _httpClient;
+	public IRedisClient RedisClient { get; set; }
+	private readonly HttpClient _httpClient;
 
-    public FirewallController(IHttpClientFactory httpClientFactory)
-    {
-        _httpClient = httpClientFactory.CreateClient();
-    }
+	public FirewallController(IHttpClientFactory httpClientFactory)
+	{
+		_httpClient = httpClientFactory.CreateClient();
+	}
 
-    /// <summary>
-    /// JS挑战,5秒盾
-    /// </summary>
-    /// <param name="token"></param>
-    /// <returns></returns>
-    [HttpPost("/challenge"), AutoValidateAntiforgeryToken]
-    public ActionResult JsChallenge()
-    {
-        try
-        {
-            HttpContext.Session.Set("js-challenge", 1);
-            Response.Cookies.Append(SessionKey.ChallengeBypass, DateTime.Now.AddSeconds(new Random().Next(60, 86400)).ToString("yyyy-MM-dd HH:mm:ss").AESEncrypt(AppConfig.BaiduAK), new CookieOptions()
-            {
-                SameSite = SameSiteMode.Lax,
-                Expires = DateTime.Now.AddDays(1)
-            });
-            return Ok();
-        }
-        catch
-        {
-            return BadRequest();
-        }
-    }
+	/// <summary>
+	/// JS挑战,5秒盾
+	/// </summary>
+	/// <param name="token"></param>
+	/// <returns></returns>
+	[HttpPost("/challenge"), AutoValidateAntiforgeryToken]
+	public ActionResult JsChallenge()
+	{
+		try
+		{
+			HttpContext.Session.Set("js-challenge", 1);
+			Response.Cookies.Append(SessionKey.ChallengeBypass, DateTime.Now.AddSeconds(new Random().Next(60, 86400)).ToString("yyyy-MM-dd HH:mm:ss").AESEncrypt(AppConfig.BaiduAK), new CookieOptions()
+			{
+				SameSite = SameSiteMode.Lax,
+				Expires = DateTime.Now.AddDays(1)
+			});
+			return Ok();
+		}
+		catch
+		{
+			return BadRequest();
+		}
+	}
 
-    /// <summary>
-    /// 验证码
-    /// </summary>
-    /// <returns></returns>
-    [HttpGet("/challenge-captcha.jpg")]
-    [ResponseCache(NoStore = true, Duration = 0)]
-    public ActionResult CaptchaChallenge()
-    {
-        string code = ValidateCode.CreateValidateCode(6);
-        HttpContext.Session.Set("challenge-captcha", code);
-        var buffer = HttpContext.CreateValidateGraphic(code);
-        return this.ResumeFile(buffer, ContentType.Jpeg, "验证码.jpg");
-    }
+	/// <summary>
+	/// 验证码
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("/challenge-captcha.jpg")]
+	[ResponseCache(NoStore = true, Duration = 0)]
+	public ActionResult CaptchaChallenge()
+	{
+		string code = ValidateCode.CreateValidateCode(6);
+		HttpContext.Session.Set("challenge-captcha", code);
+		var buffer = HttpContext.CreateValidateGraphic(code);
+		return this.ResumeFile(buffer, ContentType.Jpeg, "验证码.jpg");
+	}
 
-    /// <summary>
-    /// 验证码挑战
-    /// </summary>
-    /// <param name="code"></param>
-    /// <returns></returns>
-    [HttpPost("/captcha"), AutoValidateAntiforgeryToken]
-    public ActionResult CaptchaChallenge(string code)
-    {
-        if (string.IsNullOrEmpty(code) || code.Length < 4)
-        {
-            return BadRequest("验证码无效");
-        }
+	/// <summary>
+	/// 验证码挑战
+	/// </summary>
+	/// <param name="code"></param>
+	/// <returns></returns>
+	[HttpPost("/captcha"), AutoValidateAntiforgeryToken]
+	public ActionResult CaptchaChallenge(string code)
+	{
+		if (string.IsNullOrEmpty(code) || code.Length < 4)
+		{
+			return BadRequest("验证码无效");
+		}
 
-        if (code.Equals(HttpContext.Session.Get<string>("challenge-captcha"), StringComparison.CurrentCultureIgnoreCase))
-        {
-            HttpContext.Session.Set("js-challenge", 1);
-            HttpContext.Session.Remove("challenge-captcha");
-            Response.Cookies.Append(SessionKey.ChallengeBypass, DateTime.Now.AddSeconds(new Random().Next(60, 86400)).ToString("yyyy-MM-dd HH:mm:ss").AESEncrypt(AppConfig.BaiduAK), new CookieOptions()
-            {
-                SameSite = SameSiteMode.Lax,
-                Expires = DateTime.Now.AddDays(1)
-            });
-        }
+		if (code.Equals(HttpContext.Session.Get<string>("challenge-captcha"), StringComparison.CurrentCultureIgnoreCase))
+		{
+			HttpContext.Session.Set("js-challenge", 1);
+			HttpContext.Session.Remove("challenge-captcha");
+			Response.Cookies.Append(SessionKey.ChallengeBypass, DateTime.Now.AddSeconds(new Random().Next(60, 86400)).ToString("yyyy-MM-dd HH:mm:ss").AESEncrypt(AppConfig.BaiduAK), new CookieOptions()
+			{
+				SameSite = SameSiteMode.Lax,
+				Expires = DateTime.Now.AddDays(1)
+			});
+		}
 
-        return Redirect(Request.Headers[HeaderNames.Referer]);
-    }
+		return Redirect(Request.Headers[HeaderNames.Referer]);
+	}
 
-    /// <summary>
-    /// CloudflareTurnstile验证
-    /// </summary>
-    /// <returns></returns>
-    [HttpPost("/turnstile-handler"), AutoValidateAntiforgeryToken]
-    public async Task<ActionResult> CloudflareTurnstileHandler()
-    {
-        var form = await Request.ReadFormAsync();
-        if (form.ContainsKey("cf-turnstile-response"))
-        {
-            var token = form["cf-turnstile-response"][0];
-            const string url = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
-            using var encodedContent = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
-            {
-                new("secret",CommonHelper.SystemSettings["TurnstileSecretKey"]),
-                new("response",token),
-                new("remoteip",HttpContext.Connection.RemoteIpAddress.ToString()),
-            });
-            var resp = await _httpClient.PostAsync(url, encodedContent);
-            var result = await resp.Content.ReadFromJsonAsync<TurnstileResult>();
-            if (result.Success)
-            {
-                HttpContext.Session.Set("js-challenge", 1);
-                Response.Cookies.Append(SessionKey.ChallengeBypass, DateTime.Now.AddSeconds(new Random().Next(60, 86400)).ToString("yyyy-MM-dd HH:mm:ss").AESEncrypt(AppConfig.BaiduAK), new CookieOptions()
-                {
-                    SameSite = SameSiteMode.Lax,
-                    Expires = DateTime.Now.AddDays(1)
-                });
-            }
-        }
+	/// <summary>
+	/// CloudflareTurnstile验证
+	/// </summary>
+	/// <returns></returns>
+	[HttpPost("/turnstile-handler"), AutoValidateAntiforgeryToken]
+	public async Task<ActionResult> CloudflareTurnstileHandler()
+	{
+		var form = await Request.ReadFormAsync();
+		if (form.ContainsKey("cf-turnstile-response"))
+		{
+			var token = form["cf-turnstile-response"][0];
+			const string url = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
+			using var encodedContent = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
+			{
+				new("secret",CommonHelper.SystemSettings["TurnstileSecretKey"]),
+				new("response",token),
+				new("remoteip",HttpContext.Connection.RemoteIpAddress.ToString()),
+			});
+			var resp = await _httpClient.PostAsync(url, encodedContent);
+			var result = await resp.Content.ReadFromJsonAsync<TurnstileResult>();
+			if (result.Success)
+			{
+				HttpContext.Session.Set("js-challenge", 1);
+				Response.Cookies.Append(SessionKey.ChallengeBypass, DateTime.Now.AddSeconds(new Random().Next(60, 86400)).ToString("yyyy-MM-dd HH:mm:ss").AESEncrypt(AppConfig.BaiduAK), new CookieOptions()
+				{
+					SameSite = SameSiteMode.Lax,
+					Expires = DateTime.Now.AddDays(1)
+				});
+			}
+		}
 
-        return Redirect(Request.Headers[HeaderNames.Referer]);
-    }
+		return Redirect(Request.Headers[HeaderNames.Referer]);
+	}
 
-    /// <summary>
-    /// 反爬虫检测
-    /// </summary>
-    /// <param name="id"></param>
-    /// <param name="cacheManager"></param>
-    /// <param name="env"></param>
-    /// <returns></returns>
-    [HttpGet("/craw/{id}")]
-    [ServiceFilter(typeof(FirewallAttribute))]
-    public IActionResult AntiCrawler(string id, [FromServices] ICacheManager<int> cacheManager, [FromServices] IWebHostEnvironment env)
-    {
-        if (Request.IsRobot())
-        {
-            return Ok();
-        }
+	/// <summary>
+	/// 反爬虫检测
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="cacheManager"></param>
+	/// <param name="env"></param>
+	/// <returns></returns>
+	[HttpGet("/craw/{id}")]
+	[ServiceFilter(typeof(FirewallAttribute))]
+	public IActionResult AntiCrawler(string id, [FromServices] ICacheManager<int> cacheManager, [FromServices] IWebHostEnvironment env)
+	{
+		if (Request.IsRobot())
+		{
+			return Ok();
+		}
 
-        var ip = HttpContext.Connection.RemoteIpAddress.ToString();
-        RedisClient.LPush("intercept", new IpIntercepter()
-        {
-            IP = ip,
-            RequestUrl = Request.GetDisplayUrl(),
-            Time = DateTime.Now,
-            Referer = Request.Headers[HeaderNames.Referer],
-            UserAgent = Request.Headers[HeaderNames.UserAgent],
-            Remark = "检测到异常爬虫行为",
-            Address = Request.Location(),
-            HttpVersion = Request.Protocol,
-            Headers = new
-            {
-                Request.Protocol,
-                Request.Headers
-            }.ToJsonString()
-        });
-        cacheManager.AddOrUpdate("AntiCrawler:" + ip, 1, i => i + 1, 5);
-        cacheManager.Expire("AntiCrawler:" + ip, ExpirationMode.Sliding, TimeSpan.FromMinutes(10));
-        if (cacheManager.Get<int>("AntiCrawler:" + ip) > 3)
-        {
-            Response.StatusCode = 429;
-            return Content("");
-        }
+		var ip = HttpContext.Connection.RemoteIpAddress.ToString();
+		RedisClient.LPush("intercept", new IpIntercepter()
+		{
+			IP = ip,
+			RequestUrl = Request.GetDisplayUrl(),
+			Time = DateTime.Now,
+			Referer = Request.Headers[HeaderNames.Referer],
+			UserAgent = Request.Headers[HeaderNames.UserAgent],
+			Remark = "检测到异常爬虫行为",
+			Address = Request.Location(),
+			HttpVersion = Request.Protocol,
+			Headers = new
+			{
+				Request.Protocol,
+				Request.Headers
+			}.ToJsonString()
+		});
+		cacheManager.AddOrUpdate("AntiCrawler:" + ip, 1, i => i + 1, 5);
+		cacheManager.Expire("AntiCrawler:" + ip, ExpirationMode.Sliding, TimeSpan.FromMinutes(10));
+		if (cacheManager.Get<int>("AntiCrawler:" + ip) > 3)
+		{
+			Response.StatusCode = 429;
+			return Content("");
+		}
 
-        var sitemap = Path.Combine(env.WebRootPath, "sitemap.txt");
-        return System.IO.File.Exists(sitemap) ? Redirect(System.IO.File.ReadLines(sitemap).OrderByRandom().FirstOrDefault() ?? "/") : Redirect("/");
-    }
+		var sitemap = Path.Combine(env.WebRootPath, "sitemap.txt");
+		return System.IO.File.Exists(sitemap) ? Redirect(System.IO.File.ReadLines(sitemap).OrderByRandom().FirstOrDefault() ?? "/") : Redirect("/");
+	}
 }
 
 public class TurnstileResult
 {
-    public bool Success { get; set; }
-    [JsonProperty("error-codes")]
-    public string[] ErrorCodes { get; set; }
-    [JsonProperty("challenge_ts")]
-    public DateTime ChallengeTime { get; set; }
-    public string Hostname { get; set; }
-    public string Action { get; set; }
-    public string Cdata { get; set; }
+	public bool Success { get; set; }
+	[JsonProperty("error-codes")]
+	public string[] ErrorCodes { get; set; }
+	[JsonProperty("challenge_ts")]
+	public DateTime ChallengeTime { get; set; }
+	public string Hostname { get; set; }
+	public string Action { get; set; }
+	public string Cdata { get; set; }
 }

+ 0 - 11
src/Masuit.MyBlogs.Core/Controllers/HomeController.cs

@@ -2,25 +2,14 @@
 using AutoMapper.QueryableExtensions;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
-using Masuit.MyBlogs.Core.Infrastructure.Repository;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
 using Masuit.MyBlogs.Core.Models;
-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.Tools;
 using Masuit.Tools.AspNetCore.Mime;
-using Masuit.Tools.Core.Net;
-using Masuit.Tools.Linq;
 using Masuit.Tools.Models;
-using Masuit.Tools.Systems;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Net.Http.Headers;
 using System.ComponentModel.DataAnnotations;
 using System.Linq.Dynamic.Core;
-using System.Linq.Expressions;
 using System.Runtime.InteropServices;
 using System.Text;
 using System.Text.RegularExpressions;

+ 195 - 200
src/Masuit.MyBlogs.Core/Controllers/LinksController.cs

@@ -1,210 +1,205 @@
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
 using Masuit.MyBlogs.Core.Extensions.Firewall;
-using Masuit.MyBlogs.Core.Models.DTO;
-using Masuit.MyBlogs.Core.Models.Entity;
-using Masuit.MyBlogs.Core.Models.Enum;
-using Masuit.Tools;
 using Masuit.Tools.AspNetCore.ModelBinder;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
 using System.Text;
 using Z.EntityFramework.Plus;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 友情链接管理
+/// </summary>
+public sealed class LinksController : BaseController
 {
-    /// <summary>
-    /// 友情链接管理
-    /// </summary>
-    public sealed class LinksController : BaseController
-    {
-        public IHttpClientFactory HttpClientFactory { get; set; }
-        public IConfiguration Configuration { get; set; }
-        private HttpClient HttpClient => HttpClientFactory.CreateClient();
-
-        /// <summary>
-        /// 友情链接页
-        /// </summary>
-        /// <returns></returns>
-        [Route("links"), ResponseCache(Duration = 600, VaryByHeader = "Cookie"), AllowAccessFirewall]
-        public async Task<ActionResult> Index([FromServices] IWebHostEnvironment hostEnvironment)
-        {
-            var list = LinksService.GetQueryFromCache<bool, LinksDto>(l => l.Status == Status.Available, l => l.Recommend, false);
-            var html = await new FileInfo(Path.Combine(hostEnvironment.WebRootPath, "template", "links.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8);
-            ViewBag.Html = ReplaceVariables(html);
-            ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location());
-            return CurrentUser.IsAdmin ? View("Index_Admin", list) : View(list);
-        }
-
-        /// <summary>
-        /// 申请友链
-        /// </summary>
-        /// <param name="link"></param>
-        /// <param name="cancellationToken"></param>
-        /// <returns></returns>
-        public async Task<ActionResult> Apply(Links link, CancellationToken cancellationToken)
-        {
-            if (!link.Url.MatchUrl() || link.Url.Contains(Request.Host.Host))
-            {
-                return ResultData(null, false, "添加失败!链接非法!");
-            }
-
-            if (link.Url.Contains(new[] { "?", "&", "=" }))
-            {
-                return ResultData(null, false, "添加失败!请移除链接中的查询字符串后再试!如遇特殊情况,请联系站长进行处理。");
-            }
-
-            if (!link.Url.Contains(link.UrlBase))
-            {
-                return ResultData(null, false, "站点主页和友链地址不匹配,请检查");
-            }
-
-            var host = new Uri(link.Url).Host;
-            if (LinksService.Any(l => l.Url.Contains(host)))
-            {
-                return ResultData(null, false, "添加失败!检测到您的网站已经是本站的友情链接了!");
-            }
-
-            HttpClient.DefaultRequestHeaders.UserAgent.TryParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47");
-            HttpClient.DefaultRequestHeaders.Referrer = new Uri(Request.Scheme + "://" + Request.Host);
-            HttpClient.DefaultRequestHeaders.Add("X-Forwarded-For", "1.1.1.1");
-            HttpClient.DefaultRequestHeaders.Add("X-Forwarded-Host", "1.1.1.1");
-            HttpClient.DefaultRequestHeaders.Add("X-Real-IP", "1.1.1.1");
-            HttpClient.DefaultRequestVersion = new Version(2, 0);
-            return await HttpClient.GetAsync(Configuration["HttpClientProxy:UriPrefix"] + link.Url, cancellationToken).ContinueWith(t =>
-            {
-                if (t.IsFaulted || t.IsCanceled)
-                {
-                    return ResultData(null, false, "添加失败!检测到您的网站疑似挂了,或者连接到你网站的时候超时,请检查下!");
-                }
-
-                var res = t.Result;
-                if (!res.IsSuccessStatusCode)
-                {
-                    return ResultData(null, false, "添加失败!检测到您的网站疑似挂了!返回状态码为:" + res.StatusCode);
-                }
-
-                using var httpContent = res.Content;
-                var s = httpContent.ReadAsStringAsync().Result;
-                if (!s.Contains(Request.Host.Host))
-                {
-                    return ResultData(null, false, $"添加失败!检测到您的网站上未将本站设置成友情链接,请先将本站主域名:{Request.Host}在您的网站设置为友情链接,并且能够展示后,再次尝试添加即可!");
-                }
-
-                var b = LinksService.AddEntitySaved(link) != null;
-                QueryCacheManager.ExpireType<Links>();
-                return ResultData(null, b, b ? "添加成功!这可能有一定的延迟,如果没有看到您的链接,请稍等几分钟后刷新页面即可,如有疑问,请联系站长。" : "添加失败!这可能是由于网站服务器内部发生了错误,如有疑问,请联系站长。");
-            });
-        }
-
-        /// <summary>
-        /// 添加友链
-        /// </summary>
-        /// <param name="links"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> Save([FromBodyOrDefault] Links links)
-        {
-            bool b = await LinksService.AddOrUpdateSavedAsync(l => l.Id, links) > 0;
-            QueryCacheManager.ExpireType<Links>();
-            return b ? ResultData(null, message: "添加成功!") : ResultData(null, false, "添加失败!");
-        }
-
-        /// <summary>
-        /// 检测回链
-        /// </summary>
-        /// <param name="link"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public Task<ActionResult> Check([FromBodyOrDefault] string link)
-        {
-            HttpClient.DefaultRequestHeaders.UserAgent.TryParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47");
-            HttpClient.DefaultRequestHeaders.Add("X-Forwarded-For", "1.1.1.1");
-            HttpClient.DefaultRequestHeaders.Add("X-Forwarded-Host", "1.1.1.1");
-            HttpClient.DefaultRequestHeaders.Add("X-Real-IP", "1.1.1.1");
-            HttpClient.DefaultRequestVersion = new Version(2, 0);
-            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
-            return HttpClient.GetAsync(Configuration["HttpClientProxy:UriPrefix"] + link, cts.Token).ContinueWith(t =>
-            {
-                if (t.IsFaulted || t.IsCanceled)
-                {
-                    return ResultData(null, false, link + " 似乎挂了!错误信息:" + t.Exception?.Flatten().InnerException?.Message);
-                }
-
-                using var res = t.Result;
-                if (!res.IsSuccessStatusCode)
-                {
-                    return ResultData(null, false, link + " 对方网站返回错误的状态码!http响应码为:" + res.StatusCode);
-                }
-
-                using var httpContent = res.Content;
-                var s = httpContent.ReadAsStringAsync().Result;
-                return s.Contains(CommonHelper.SystemSettings["Domain"].Split("|")) ? ResultData(null, true, "友情链接正常!") : ResultData(null, false, link + " 对方似乎没有本站的友情链接!");
-            });
-        }
-
-        /// <summary>
-        /// 删除友链
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> Delete(int id)
-        {
-            bool b = await LinksService.DeleteByIdAsync(id) > 0;
-            QueryCacheManager.ExpireType<Links>();
-            return ResultData(null, b, b ? "删除成功!" : "删除失败!");
-        }
-
-        /// <summary>
-        /// 所有的友情链接
-        /// </summary>
-        /// <returns></returns>
-        [MyAuthorize]
-        public ActionResult Get()
-        {
-            var list = LinksService.GetAll<LinksDto>().OrderBy(p => p.Status).ThenByDescending(p => p.Recommend).ThenByDescending(p => p.Id).ToList();
-            return ResultData(list);
-        }
-
-        /// <summary>
-        /// 切换友情链接的白名单状态
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [HttpPost]
-        [MyAuthorize]
-        public async Task<ActionResult> ToggleWhitelist(int id)
-        {
-            var b = await LinksService.GetQuery(m => m.Id == id).ExecuteUpdateAsync(e => e.SetProperty(c => c.Except, c => !c.Except)) > 0;
-            return ResultData(null, b, b ? "切换成功!" : "切换失败!");
-        }
-
-        /// <summary>
-        /// 切换友情链接的推荐状态
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [HttpPost]
-        [MyAuthorize]
-        public async Task<ActionResult> ToggleRecommend(int id)
-        {
-            var b = await LinksService.GetQuery(m => m.Id == id).ExecuteUpdateAsync(e => e.SetProperty(c => c.Recommend, c => !c.Recommend)) > 0;
-            return ResultData(null, b, b ? "切换成功!" : "切换失败!");
-        }
-
-        /// <summary>
-        /// 切换友情链接可用状态
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [HttpPost]
-        [MyAuthorize]
-        public async Task<ActionResult> Toggle(int id)
-        {
-            var b = await LinksService.GetQuery(m => m.Id == id).ExecuteUpdateAsync(s => s.SetProperty(e => e.Status, m => m.Status == Status.Unavailable ? Status.Available : Status.Unavailable)) > 0;
-            QueryCacheManager.ExpireType<Links>();
-            return ResultData(null, b, b ? "切换成功!" : "切换失败!");
-        }
-    }
-}
+	public IHttpClientFactory HttpClientFactory { get; set; }
+	public IConfiguration Configuration { get; set; }
+	private HttpClient HttpClient => HttpClientFactory.CreateClient();
+
+	/// <summary>
+	/// 友情链接页
+	/// </summary>
+	/// <returns></returns>
+	[Route("links"), ResponseCache(Duration = 600, VaryByHeader = "Cookie"), AllowAccessFirewall]
+	public async Task<ActionResult> Index([FromServices] IWebHostEnvironment hostEnvironment)
+	{
+		var list = LinksService.GetQueryFromCache<bool, LinksDto>(l => l.Status == Status.Available, l => l.Recommend, false);
+		var html = await new FileInfo(Path.Combine(hostEnvironment.WebRootPath, "template", "links.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8);
+		ViewBag.Html = ReplaceVariables(html);
+		ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location());
+		return CurrentUser.IsAdmin ? View("Index_Admin", list) : View(list);
+	}
+
+	/// <summary>
+	/// 申请友链
+	/// </summary>
+	/// <param name="link"></param>
+	/// <param name="cancellationToken"></param>
+	/// <returns></returns>
+	public async Task<ActionResult> Apply(Links link, CancellationToken cancellationToken)
+	{
+		if (!link.Url.MatchUrl() || link.Url.Contains(Request.Host.Host))
+		{
+			return ResultData(null, false, "添加失败!链接非法!");
+		}
+
+		if (link.Url.Contains(new[] { "?", "&", "=" }))
+		{
+			return ResultData(null, false, "添加失败!请移除链接中的查询字符串后再试!如遇特殊情况,请联系站长进行处理。");
+		}
+
+		if (!link.Url.Contains(link.UrlBase))
+		{
+			return ResultData(null, false, "站点主页和友链地址不匹配,请检查");
+		}
+
+		var host = new Uri(link.Url).Host;
+		if (LinksService.Any(l => l.Url.Contains(host)))
+		{
+			return ResultData(null, false, "添加失败!检测到您的网站已经是本站的友情链接了!");
+		}
+
+		HttpClient.DefaultRequestHeaders.UserAgent.TryParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47");
+		HttpClient.DefaultRequestHeaders.Referrer = new Uri(Request.Scheme + "://" + Request.Host);
+		HttpClient.DefaultRequestHeaders.Add("X-Forwarded-For", "1.1.1.1");
+		HttpClient.DefaultRequestHeaders.Add("X-Forwarded-Host", "1.1.1.1");
+		HttpClient.DefaultRequestHeaders.Add("X-Real-IP", "1.1.1.1");
+		HttpClient.DefaultRequestVersion = new Version(2, 0);
+		return await HttpClient.GetAsync(Configuration["HttpClientProxy:UriPrefix"] + link.Url, cancellationToken).ContinueWith(t =>
+		{
+			if (t.IsFaulted || t.IsCanceled)
+			{
+				return ResultData(null, false, "添加失败!检测到您的网站疑似挂了,或者连接到你网站的时候超时,请检查下!");
+			}
+
+			var res = t.Result;
+			if (!res.IsSuccessStatusCode)
+			{
+				return ResultData(null, false, "添加失败!检测到您的网站疑似挂了!返回状态码为:" + res.StatusCode);
+			}
+
+			using var httpContent = res.Content;
+			var s = httpContent.ReadAsStringAsync().Result;
+			if (!s.Contains(Request.Host.Host))
+			{
+				return ResultData(null, false, $"添加失败!检测到您的网站上未将本站设置成友情链接,请先将本站主域名:{Request.Host}在您的网站设置为友情链接,并且能够展示后,再次尝试添加即可!");
+			}
+
+			var b = LinksService.AddEntitySaved(link) != null;
+			QueryCacheManager.ExpireType<Links>();
+			return ResultData(null, b, b ? "添加成功!这可能有一定的延迟,如果没有看到您的链接,请稍等几分钟后刷新页面即可,如有疑问,请联系站长。" : "添加失败!这可能是由于网站服务器内部发生了错误,如有疑问,请联系站长。");
+		});
+	}
+
+	/// <summary>
+	/// 添加友链
+	/// </summary>
+	/// <param name="links"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Save([FromBodyOrDefault] Links links)
+	{
+		bool b = await LinksService.AddOrUpdateSavedAsync(l => l.Id, links) > 0;
+		QueryCacheManager.ExpireType<Links>();
+		return b ? ResultData(null, message: "添加成功!") : ResultData(null, false, "添加失败!");
+	}
+
+	/// <summary>
+	/// 检测回链
+	/// </summary>
+	/// <param name="link"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public Task<ActionResult> Check([FromBodyOrDefault] string link)
+	{
+		HttpClient.DefaultRequestHeaders.UserAgent.TryParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47");
+		HttpClient.DefaultRequestHeaders.Add("X-Forwarded-For", "1.1.1.1");
+		HttpClient.DefaultRequestHeaders.Add("X-Forwarded-Host", "1.1.1.1");
+		HttpClient.DefaultRequestHeaders.Add("X-Real-IP", "1.1.1.1");
+		HttpClient.DefaultRequestVersion = new Version(2, 0);
+		using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+		return HttpClient.GetAsync(Configuration["HttpClientProxy:UriPrefix"] + link, cts.Token).ContinueWith(t =>
+		{
+			if (t.IsFaulted || t.IsCanceled)
+			{
+				return ResultData(null, false, link + " 似乎挂了!错误信息:" + t.Exception?.Flatten().InnerException?.Message);
+			}
+
+			using var res = t.Result;
+			if (!res.IsSuccessStatusCode)
+			{
+				return ResultData(null, false, link + " 对方网站返回错误的状态码!http响应码为:" + res.StatusCode);
+			}
+
+			using var httpContent = res.Content;
+			var s = httpContent.ReadAsStringAsync().Result;
+			return s.Contains(CommonHelper.SystemSettings["Domain"].Split("|")) ? ResultData(null, true, "友情链接正常!") : ResultData(null, false, link + " 对方似乎没有本站的友情链接!");
+		});
+	}
+
+	/// <summary>
+	/// 删除友链
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Delete(int id)
+	{
+		bool b = await LinksService.DeleteByIdAsync(id) > 0;
+		QueryCacheManager.ExpireType<Links>();
+		return ResultData(null, b, b ? "删除成功!" : "删除失败!");
+	}
+
+	/// <summary>
+	/// 所有的友情链接
+	/// </summary>
+	/// <returns></returns>
+	[MyAuthorize]
+	public ActionResult Get()
+	{
+		var list = LinksService.GetAll<LinksDto>().OrderBy(p => p.Status).ThenByDescending(p => p.Recommend).ThenByDescending(p => p.Id).ToList();
+		return ResultData(list);
+	}
+
+	/// <summary>
+	/// 切换友情链接的白名单状态
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpPost]
+	[MyAuthorize]
+	public async Task<ActionResult> ToggleWhitelist(int id)
+	{
+		var b = await LinksService.GetQuery(m => m.Id == id).ExecuteUpdateAsync(e => e.SetProperty(c => c.Except, c => !c.Except)) > 0;
+		return ResultData(null, b, b ? "切换成功!" : "切换失败!");
+	}
+
+	/// <summary>
+	/// 切换友情链接的推荐状态
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpPost]
+	[MyAuthorize]
+	public async Task<ActionResult> ToggleRecommend(int id)
+	{
+		var b = await LinksService.GetQuery(m => m.Id == id).ExecuteUpdateAsync(e => e.SetProperty(c => c.Recommend, c => !c.Recommend)) > 0;
+		return ResultData(null, b, b ? "切换成功!" : "切换失败!");
+	}
+
+	/// <summary>
+	/// 切换友情链接可用状态
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpPost]
+	[MyAuthorize]
+	public async Task<ActionResult> Toggle(int id)
+	{
+		var b = await LinksService.GetQuery(m => m.Id == id).ExecuteUpdateAsync(s => s.SetProperty(e => e.Status, m => m.Status == Status.Unavailable ? Status.Available : Status.Unavailable)) > 0;
+		QueryCacheManager.ExpireType<Links>();
+		return ResultData(null, b, b ? "切换成功!" : "切换失败!");
+	}
+}

+ 19 - 21
src/Masuit.MyBlogs.Core/Controllers/LoginController.cs

@@ -1,31 +1,29 @@
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.ViewModel;
-using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc;
 
 namespace Masuit.MyBlogs.Core.Controllers;
 
 [Route("login")]
 public sealed class LoginController : AdminController
 {
-    public ILoginRecordService LoginRecordService { get; set; }
+	public ILoginRecordService LoginRecordService { get; set; }
 
-    [Route("delete/{id:int}/{ids}")]
-    public async Task<ActionResult> Delete(int id, string ids)
-    {
-        if (!string.IsNullOrWhiteSpace(ids))
-        {
-            bool b = await LoginRecordService.DeleteEntitySavedAsync(r => r.UserInfoId == id && ids.Contains(r.Id.ToString())) > 0;
-            return ResultData(null, b, b ? "删除成功!" : "删除失败");
-        }
+	[Route("delete/{id:int}/{ids}")]
+	public async Task<ActionResult> Delete(int id, string ids)
+	{
+		if (!string.IsNullOrWhiteSpace(ids))
+		{
+			bool b = await LoginRecordService.DeleteEntitySavedAsync(r => r.UserInfoId == id && ids.Contains(r.Id.ToString())) > 0;
+			return ResultData(null, b, b ? "删除成功!" : "删除失败");
+		}
 
-        return ResultData(null, false, "数据不合法");
-    }
+		return ResultData(null, false, "数据不合法");
+	}
 
-    [Route("getrecent/{id:int}")]
-    public ActionResult GetRecentRecord(int id)
-    {
-        var time = DateTime.Now.AddMonths(-1);
-        var list = LoginRecordService.GetQueryFromCache<DateTime, LoginRecordViewModel>(r => r.UserInfoId == id && r.LoginTime >= time, r => r.LoginTime, false);
-        return ResultData(list);
-    }
+	[Route("getrecent/{id:int}")]
+	public ActionResult GetRecentRecord(int id)
+	{
+		var time = DateTime.Now.AddMonths(-1);
+		var list = LoginRecordService.GetQueryFromCache<DateTime, LoginRecordViewModel>(r => r.UserInfoId == id && r.LoginTime >= time, r => r.LoginTime, false);
+		return ResultData(list);
+	}
 }

+ 67 - 74
src/Masuit.MyBlogs.Core/Controllers/MenuController.cs

@@ -1,94 +1,87 @@
 using Masuit.MyBlogs.Core.Common;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.Command;
-using Masuit.MyBlogs.Core.Models.DTO;
-using Masuit.MyBlogs.Core.Models.Entity;
-using Masuit.MyBlogs.Core.Models.Enum;
 using Masuit.Tools.AspNetCore.ModelBinder;
 using Masuit.Tools.Models;
-using Masuit.Tools.Systems;
 using Microsoft.AspNetCore.Mvc;
 using Z.EntityFramework.Plus;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 菜单管理
+/// </summary>
+public sealed class MenuController : AdminController
 {
 	/// <summary>
-	/// 菜单管理
+	/// 菜单数据服务
 	/// </summary>
-	public sealed class MenuController : AdminController
-	{
-		/// <summary>
-		/// 菜单数据服务
-		/// </summary>
-		public IMenuService MenuService { get; set; }
+	public IMenuService MenuService { get; set; }
 
-		/// <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>
+	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()
+	/// <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)
 		{
-			var array = Enum.GetValues(typeof(MenuType));
-			var list = new List<object>();
-			foreach (Enum e in array)
+			list.Add(new
 			{
-				list.Add(new
-				{
-					e,
-					name = e.GetDisplay()
-				});
-			}
-			return ResultData(list);
+				e,
+				name = e.GetDisplay()
+			});
 		}
+		return ResultData(list);
+	}
 
-		/// <summary>
-		/// 删除菜单
-		/// </summary>
-		/// <param name="id"></param>
-		/// <returns></returns>
-		public async Task<ActionResult> Delete(int id)
+	/// <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("/"))
 		{
-			var menus = MenuService[id].Flatten();
-			bool b = await MenuService.DeleteEntitiesSavedAsync(menus) > 0;
-			return ResultData(null, b, b ? "删除成功" : "删除失败");
+			model.Icon = null;
 		}
-
-		/// <summary>
-		/// 保持菜单
-		/// </summary>
-		/// <param name="model"></param>
-		/// <returns></returns>
-		public async Task<ActionResult> Save([FromBodyOrDefault] MenuCommand model)
+		var m = await MenuService.GetByIdAsync(model.Id);
+		if (m == null)
 		{
-			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 ? "修改成功" : "修改失败");
+			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 ? "修改成功" : "修改失败");
 	}
-}
+}

+ 147 - 160
src/Masuit.MyBlogs.Core/Controllers/MergeController.cs

@@ -2,170 +2,157 @@
 using Hangfire;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
-using Masuit.MyBlogs.Core.Infrastructure.Repository;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.Command;
-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.Tools;
 using Masuit.Tools.AspNetCore.ModelBinder;
-using Masuit.Tools.Core.Net;
-using Masuit.Tools.Linq;
-using Masuit.Tools.Strings;
 using Microsoft.AspNetCore.Mvc;
 using System.ComponentModel.DataAnnotations;
-using System.Linq.Expressions;
 using System.Text;
 using System.Text.RegularExpressions;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+[Route("merge/")]
+public sealed class MergeController : AdminController
 {
-    [Route("merge/")]
-    public sealed class MergeController : AdminController
-    {
-        public IPostMergeRequestService PostMergeRequestService { get; set; }
-
-        public IWebHostEnvironment HostEnvironment { get; set; }
-
-        public MapperConfiguration MapperConfig { get; set; }
-
-        /// <summary>
-        /// 获取合并详情
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [HttpGet("{id}")]
-        public async Task<ActionResult> Get(int id)
-        {
-            var p = Mapper.Map<PostMergeRequestDto>(await PostMergeRequestService.GetByIdAsync(id));
-            if (p != null)
-            {
-                p.SubmitTime = p.SubmitTime.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-            }
-
-            return ResultData(p);
-        }
-
-        /// <summary>
-        /// 获取分页数据
-        /// </summary>
-        /// <param name="page"></param>
-        /// <param name="size"></param>
-        /// <param name="kw"></param>
-        /// <returns></returns>
-        [HttpGet]
-        public ActionResult GetPageData(int page = 1, int size = 10, string kw = "")
-        {
-            Expression<Func<PostMergeRequest, bool>> where = r => true;
-            if (!string.IsNullOrEmpty(kw))
-            {
-                where = where.And(r => r.Title.Contains(kw) || r.Content.Contains(kw) || r.Modifier.Contains(kw) || r.ModifierEmail.Contains(kw));
-            }
-
-            var list = PostMergeRequestService.GetQuery(where).OrderByDescending(d => d.MergeState == MergeStatus.Pending).ThenByDescending(r => r.Id).ToPagedList<PostMergeRequest, PostMergeRequestDtoBase>(page, size, MapperConfig);
-            foreach (var item in list.Data)
-            {
-                item.SubmitTime = item.SubmitTime.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-            }
-
-            return Ok(list);
-        }
-
-        /// <summary>
-        /// 版本对比
-        /// </summary>
-        /// <param name="mid"></param>
-        /// <returns></returns>
-        [HttpGet("compare/{mid}")]
-        public async Task<IActionResult> MergeCompare(int mid)
-        {
-            var newer = await PostMergeRequestService.GetByIdAsync(mid) ?? throw new NotFoundException("待合并文章未找到");
-            var old = newer.Post;
-            var diffHelper = new HtmlDiff.HtmlDiff(old.Content, newer.Content);
-            string diffOutput = diffHelper.Build();
-            old.Content = Regex.Replace(Regex.Replace(diffOutput, "<ins.+?</ins>", string.Empty), @"<\w+></\w+>", string.Empty);
-            newer.Content = Regex.Replace(Regex.Replace(diffOutput, "<del.+?</del>", string.Empty), @"<\w+></\w+>", string.Empty);
-            return ResultData(new { old = old.Mapper<PostMergeRequestDto>(), newer = newer.Mapper<PostMergeRequestDto>() });
-        }
-
-        /// <summary>
-        /// 直接合并
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [HttpPost("{id}")]
-        public async Task<IActionResult> Merge(int id)
-        {
-            var merge = await PostMergeRequestService.GetByIdAsync(id) ?? throw new NotFoundException("待合并文章未找到");
-            var history = merge.Post.Mapper<PostHistoryVersion>();
-            history.Id = 0;
-            merge.Post = Mapper.Map(merge, merge.Post);
-            merge.Post.PostHistoryVersion.Add(history);
-            merge.Post.ModifyDate = DateTime.Now;
-            merge.MergeState = MergeStatus.Merged;
-            var b = await PostMergeRequestService.SaveChangesAsync() > 0;
-            if (!b)
-            {
-                return ResultData(null, false, "文章合并失败!");
-            }
-
-            string link = Request.Scheme + "://" + Request.Host + "/" + merge.Post.Id;
-            string content = new Template(await new FileInfo(HostEnvironment.WebRootPath + "/template/merge-pass.html").ShareReadWrite().ReadAllTextAsync(Encoding.UTF8)).Set("link", link).Set("title", merge.Post.Title).Render();
-            BackgroundJob.Enqueue(() => CommonHelper.SendMail(CommonHelper.SystemSettings["Title"] + "博客你提交的修改已通过", content, merge.ModifierEmail, "127.0.0.1"));
-            return ResultData(null, true, "文章合并完成!");
-        }
-
-        /// <summary>
-        /// 编辑并合并
-        /// </summary>
-        /// <param name="dto"></param>
-        /// <returns></returns>
-        [HttpPost]
-        public async Task<IActionResult> Merge([FromBodyOrDefault] PostMergeRequestCommandBase dto)
-        {
-            var merge = await PostMergeRequestService.GetByIdAsync(dto.Id) ?? throw new NotFoundException("待合并文章未找到");
-            Mapper.Map(dto, merge);
-            var b = await PostMergeRequestService.SaveChangesAsync() > 0;
-            return b ? await Merge(merge.Id) : ResultData(null, false, "文章合并失败!");
-        }
-
-        /// <summary>
-        /// 拒绝合并
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="reason"></param>
-        /// <returns></returns>
-        [HttpPost("reject/{id}")]
-        public async Task<ActionResult> Reject(int id, [Required(ErrorMessage = "请填写拒绝理由"), FromBodyOrDefault] string reason)
-        {
-            var merge = await PostMergeRequestService.GetByIdAsync(id) ?? throw new NotFoundException("待合并文章未找到");
-            merge.MergeState = MergeStatus.Reject;
-            var b = await PostMergeRequestService.SaveChangesAsync() > 0;
-            if (!b)
-            {
-                return ResultData(null, false, "操作失败!");
-            }
-
-            var link = Request.Scheme + "://" + Request.Host + "/" + merge.Post.Id + "/merge/" + id;
-            var content = new Template(await new FileInfo(HostEnvironment.WebRootPath + "/template/merge-reject.html").ShareReadWrite().ReadAllTextAsync(Encoding.UTF8)).Set("link", link).Set("title", merge.Post.Title).Set("reason", reason).Render();
-            BackgroundJob.Enqueue(() => CommonHelper.SendMail(CommonHelper.SystemSettings["Title"] + "博客你提交的修改已被拒绝", content, merge.ModifierEmail, "127.0.0.1"));
-            return ResultData(null, true, "合并已拒绝!");
-        }
-
-        /// <summary>
-        /// 标记为恶意修改
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [HttpPost("block/{id}")]
-        public async Task<ActionResult> Block(int id)
-        {
-            var merge = await PostMergeRequestService.GetByIdAsync(id) ?? throw new NotFoundException("待合并文章未找到");
-            merge.MergeState = MergeStatus.Block;
-            var b = await PostMergeRequestService.SaveChangesAsync() > 0;
-            return b ? ResultData(null, true, "操作成功!") : ResultData(null, false, "操作失败!");
-        }
-    }
-}
+	public IPostMergeRequestService PostMergeRequestService { get; set; }
+
+	public IWebHostEnvironment HostEnvironment { get; set; }
+
+	public MapperConfiguration MapperConfig { get; set; }
+
+	/// <summary>
+	/// 获取合并详情
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpGet("{id}")]
+	public async Task<ActionResult> Get(int id)
+	{
+		var p = Mapper.Map<PostMergeRequestDto>(await PostMergeRequestService.GetByIdAsync(id));
+		if (p != null)
+		{
+			p.SubmitTime = p.SubmitTime.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		}
+
+		return ResultData(p);
+	}
+
+	/// <summary>
+	/// 获取分页数据
+	/// </summary>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <param name="kw"></param>
+	/// <returns></returns>
+	[HttpGet]
+	public ActionResult GetPageData(int page = 1, int size = 10, string kw = "")
+	{
+		Expression<Func<PostMergeRequest, bool>> where = r => true;
+		if (!string.IsNullOrEmpty(kw))
+		{
+			where = where.And(r => r.Title.Contains(kw) || r.Content.Contains(kw) || r.Modifier.Contains(kw) || r.ModifierEmail.Contains(kw));
+		}
+
+		var list = PostMergeRequestService.GetQuery(where).OrderByDescending(d => d.MergeState == MergeStatus.Pending).ThenByDescending(r => r.Id).ToPagedList<PostMergeRequest, PostMergeRequestDtoBase>(page, size, MapperConfig);
+		foreach (var item in list.Data)
+		{
+			item.SubmitTime = item.SubmitTime.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		}
+
+		return Ok(list);
+	}
+
+	/// <summary>
+	/// 版本对比
+	/// </summary>
+	/// <param name="mid"></param>
+	/// <returns></returns>
+	[HttpGet("compare/{mid}")]
+	public async Task<IActionResult> MergeCompare(int mid)
+	{
+		var newer = await PostMergeRequestService.GetByIdAsync(mid) ?? throw new NotFoundException("待合并文章未找到");
+		var old = newer.Post;
+		var diffHelper = new HtmlDiff.HtmlDiff(old.Content, newer.Content);
+		string diffOutput = diffHelper.Build();
+		old.Content = Regex.Replace(Regex.Replace(diffOutput, "<ins.+?</ins>", string.Empty), @"<\w+></\w+>", string.Empty);
+		newer.Content = Regex.Replace(Regex.Replace(diffOutput, "<del.+?</del>", string.Empty), @"<\w+></\w+>", string.Empty);
+		return ResultData(new { old = old.Mapper<PostMergeRequestDto>(), newer = newer.Mapper<PostMergeRequestDto>() });
+	}
+
+	/// <summary>
+	/// 直接合并
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpPost("{id}")]
+	public async Task<IActionResult> Merge(int id)
+	{
+		var merge = await PostMergeRequestService.GetByIdAsync(id) ?? throw new NotFoundException("待合并文章未找到");
+		var history = merge.Post.Mapper<PostHistoryVersion>();
+		history.Id = 0;
+		merge.Post = Mapper.Map(merge, merge.Post);
+		merge.Post.PostHistoryVersion.Add(history);
+		merge.Post.ModifyDate = DateTime.Now;
+		merge.MergeState = MergeStatus.Merged;
+		var b = await PostMergeRequestService.SaveChangesAsync() > 0;
+		if (!b)
+		{
+			return ResultData(null, false, "文章合并失败!");
+		}
+
+		string link = Request.Scheme + "://" + Request.Host + "/" + merge.Post.Id;
+		string content = new Template(await new FileInfo(HostEnvironment.WebRootPath + "/template/merge-pass.html").ShareReadWrite().ReadAllTextAsync(Encoding.UTF8)).Set("link", link).Set("title", merge.Post.Title).Render();
+		BackgroundJob.Enqueue(() => CommonHelper.SendMail(CommonHelper.SystemSettings["Title"] + "博客你提交的修改已通过", content, merge.ModifierEmail, "127.0.0.1"));
+		return ResultData(null, true, "文章合并完成!");
+	}
+
+	/// <summary>
+	/// 编辑并合并
+	/// </summary>
+	/// <param name="dto"></param>
+	/// <returns></returns>
+	[HttpPost]
+	public async Task<IActionResult> Merge([FromBodyOrDefault] PostMergeRequestCommandBase dto)
+	{
+		var merge = await PostMergeRequestService.GetByIdAsync(dto.Id) ?? throw new NotFoundException("待合并文章未找到");
+		Mapper.Map(dto, merge);
+		var b = await PostMergeRequestService.SaveChangesAsync() > 0;
+		return b ? await Merge(merge.Id) : ResultData(null, false, "文章合并失败!");
+	}
+
+	/// <summary>
+	/// 拒绝合并
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="reason"></param>
+	/// <returns></returns>
+	[HttpPost("reject/{id}")]
+	public async Task<ActionResult> Reject(int id, [Required(ErrorMessage = "请填写拒绝理由"), FromBodyOrDefault] string reason)
+	{
+		var merge = await PostMergeRequestService.GetByIdAsync(id) ?? throw new NotFoundException("待合并文章未找到");
+		merge.MergeState = MergeStatus.Reject;
+		var b = await PostMergeRequestService.SaveChangesAsync() > 0;
+		if (!b)
+		{
+			return ResultData(null, false, "操作失败!");
+		}
+
+		var link = Request.Scheme + "://" + Request.Host + "/" + merge.Post.Id + "/merge/" + id;
+		var content = new Template(await new FileInfo(HostEnvironment.WebRootPath + "/template/merge-reject.html").ShareReadWrite().ReadAllTextAsync(Encoding.UTF8)).Set("link", link).Set("title", merge.Post.Title).Set("reason", reason).Render();
+		BackgroundJob.Enqueue(() => CommonHelper.SendMail(CommonHelper.SystemSettings["Title"] + "博客你提交的修改已被拒绝", content, merge.ModifierEmail, "127.0.0.1"));
+		return ResultData(null, true, "合并已拒绝!");
+	}
+
+	/// <summary>
+	/// 标记为恶意修改
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpPost("block/{id}")]
+	public async Task<ActionResult> Block(int id)
+	{
+		var merge = await PostMergeRequestService.GetByIdAsync(id) ?? throw new NotFoundException("待合并文章未找到");
+		merge.MergeState = MergeStatus.Block;
+		var b = await PostMergeRequestService.SaveChangesAsync() > 0;
+		return b ? ResultData(null, true, "操作成功!") : ResultData(null, false, "操作失败!");
+	}
+}

+ 187 - 195
src/Masuit.MyBlogs.Core/Controllers/MiscController.cs

@@ -1,202 +1,194 @@
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
-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.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools;
 using Masuit.Tools.AspNetCore.ModelBinder;
-using Masuit.Tools.Core.Net;
 using Microsoft.AspNetCore.Mvc;
 using System.Text;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 杂项页
+/// </summary>
+public sealed class MiscController : BaseController
 {
-    /// <summary>
-    /// 杂项页
-    /// </summary>
-    public sealed class MiscController : BaseController
-    {
-        /// <summary>
-        /// MiscService
-        /// </summary>
-        public IMiscService MiscService { get; set; }
-
-        public IWebHostEnvironment HostEnvironment { get; set; }
-
-        public ImagebedClient ImagebedClient { get; set; }
-
-        /// <summary>
-        /// 杂项页
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [Route("misc/{id:int}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id" }, VaryByHeader = "Cookie")]
-        public async Task<ActionResult> Index(int id)
-        {
-            var misc = await MiscService.GetFromCacheAsync(m => m.Id == id) ?? throw new NotFoundException("页面未找到");
-            misc.ModifyDate = misc.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-            misc.PostDate = misc.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-            misc.Content = await ReplaceVariables(misc.Content).Next(s => Request.IsRobot() ? Task.FromResult(s) : s.InjectFingerprint());
-            return View(misc);
-        }
-
-        /// <summary>
-        /// 打赏
-        /// </summary>
-        /// <returns></returns>
-        [Route("donate")]
-        public async Task<ActionResult> Donate()
-        {
-            var ads = AdsService.GetsByWeightedPrice(2, AdvertiseType.InPage, Request.Location());
-            if (bool.Parse(CommonHelper.SystemSettings.GetOrAdd("EnableDonate", "true")))
-            {
-                ViewBag.Ads = ads;
-                var text = await new FileInfo(Path.Combine(HostEnvironment.WebRootPath, "template", "donate.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8);
-                return CurrentUser.IsAdmin ? View("Donate_Admin", text) : View(model: text);
-            }
-
-            return Redirect(ads.FirstOrDefault()?.Url ?? "/");
-        }
-
-        /// <summary>
-        /// 打赏列表
-        /// </summary>
-        /// <param name="donateService"></param>
-        /// <param name="page"></param>
-        /// <param name="size"></param>
-        /// <returns></returns>
-        [Route("donatelist")]
-        public ActionResult DonateList([FromServices] IDonateService donateService, int page = 1, int size = 10)
-        {
-            if (bool.Parse(CommonHelper.SystemSettings.GetOrAdd("EnableDonate", "true")))
-            {
-                var list = donateService.GetPagesFromCache<DateTime, DonateDto>(page, size, d => true, d => d.DonateTime, false);
-                if (!CurrentUser.IsAdmin)
-                {
-                    foreach (var item in list.Data.Where(item => !(item.QQorWechat + item.Email).Contains("匿名")))
-                    {
-                        item.QQorWechat = item.QQorWechat?.Mask();
-                        item.Email = item.Email?.MaskEmail();
-                    }
-                }
-
-                return Ok(list);
-            }
-
-            return Ok();
-        }
-
-        /// <summary>
-        /// 关于
-        /// </summary>
-        /// <returns></returns>
-        [Route("about"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
-        public async Task<ActionResult> About()
-        {
-            var text = await new FileInfo(Path.Combine(HostEnvironment.WebRootPath, "template", "about.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8);
-            return View(model: text);
-        }
-
-        /// <summary>
-        /// 评论及留言须知
-        /// </summary>
-        /// <returns></returns>
-        [Route("agreement"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
-        public async Task<ActionResult> Agreement()
-        {
-            var text = await new FileInfo(Path.Combine(HostEnvironment.WebRootPath, "template", "agreement.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8);
-            return View(model: text);
-        }
-
-        /// <summary>
-        /// 声明
-        /// </summary>
-        /// <returns></returns>
-        [Route("disclaimer"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
-        public async Task<ActionResult> Disclaimer()
-        {
-            var text = await new FileInfo(Path.Combine(HostEnvironment.WebRootPath, "template", "disclaimer.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8);
-            return View(model: text);
-        }
-
-        /// <summary>
-        /// 创建页面
-        /// </summary>
-        /// <param name="model"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> Write([FromBodyOrDefault] Misc model, CancellationToken cancellationToken)
-        {
-            model.Content = await ImagebedClient.ReplaceImgSrc(await model.Content.Trim().ClearImgAttributes(), cancellationToken);
-            var e = MiscService.AddEntitySaved(model);
-            return e != null ? ResultData(null, message: "发布成功") : ResultData(null, false, "发布失败");
-        }
-
-        /// <summary>
-        /// 删除页面
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> Delete(int id)
-        {
-            bool b = await MiscService.DeleteByIdAsync(id) > 0;
-            return ResultData(null, b, b ? "删除成功" : "删除失败");
-        }
-
-        /// <summary>
-        /// 编辑页面
-        /// </summary>
-        /// <param name="misc"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> Edit([FromBodyOrDefault] Misc misc, CancellationToken cancellationToken)
-        {
-            var entity = await MiscService.GetByIdAsync(misc.Id) ?? throw new NotFoundException("杂项页未找到");
-            entity.ModifyDate = DateTime.Now;
-            entity.Title = misc.Title;
-            entity.Content = await ImagebedClient.ReplaceImgSrc(await misc.Content.ClearImgAttributes(), cancellationToken);
-            bool b = await MiscService.SaveChangesAsync() > 0;
-            return ResultData(null, b, b ? "修改成功" : "修改失败");
-        }
-
-        /// <summary>
-        /// 分页数据
-        /// </summary>
-        /// <param name="page"></param>
-        /// <param name="size"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public ActionResult GetPageData(int page = 1, int size = 10)
-        {
-            var list = MiscService.GetPages(page, size, n => true, n => n.ModifyDate, false);
-            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));
-            }
-
-            return Ok(list);
-        }
-
-        /// <summary>
-        /// 详情
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> Get(int id)
-        {
-            var misc = await MiscService.GetByIdAsync(id);
-            if (misc != null)
-            {
-                misc.ModifyDate = misc.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-                misc.PostDate = misc.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-            }
-
-            return ResultData(misc.Mapper<MiscDto>());
-        }
-    }
-}
+	/// <summary>
+	/// MiscService
+	/// </summary>
+	public IMiscService MiscService { get; set; }
+
+	public IWebHostEnvironment HostEnvironment { get; set; }
+
+	public ImagebedClient ImagebedClient { get; set; }
+
+	/// <summary>
+	/// 杂项页
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[Route("misc/{id:int}"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id" }, VaryByHeader = "Cookie")]
+	public async Task<ActionResult> Index(int id)
+	{
+		var misc = await MiscService.GetFromCacheAsync(m => m.Id == id) ?? throw new NotFoundException("页面未找到");
+		misc.ModifyDate = misc.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		misc.PostDate = misc.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		misc.Content = ReplaceVariables(misc.Content);
+		return View(misc);
+	}
+
+	/// <summary>
+	/// 打赏
+	/// </summary>
+	/// <returns></returns>
+	[Route("donate")]
+	public async Task<ActionResult> Donate()
+	{
+		var ads = AdsService.GetsByWeightedPrice(2, AdvertiseType.InPage, Request.Location());
+		if (bool.Parse(CommonHelper.SystemSettings.GetOrAdd("EnableDonate", "true")))
+		{
+			ViewBag.Ads = ads;
+			var text = await new FileInfo(Path.Combine(HostEnvironment.WebRootPath, "template", "donate.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8);
+			return CurrentUser.IsAdmin ? View("Donate_Admin", text) : View(model: text);
+		}
+
+		return Redirect(ads.FirstOrDefault()?.Url ?? "/");
+	}
+
+	/// <summary>
+	/// 打赏列表
+	/// </summary>
+	/// <param name="donateService"></param>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <returns></returns>
+	[Route("donatelist")]
+	public ActionResult DonateList([FromServices] IDonateService donateService, int page = 1, int size = 10)
+	{
+		if (bool.Parse(CommonHelper.SystemSettings.GetOrAdd("EnableDonate", "true")))
+		{
+			var list = donateService.GetPagesFromCache<DateTime, DonateDto>(page, size, d => true, d => d.DonateTime, false);
+			if (!CurrentUser.IsAdmin)
+			{
+				foreach (var item in list.Data.Where(item => !(item.QQorWechat + item.Email).Contains("匿名")))
+				{
+					item.QQorWechat = item.QQorWechat?.Mask();
+					item.Email = item.Email?.MaskEmail();
+				}
+			}
+
+			return Ok(list);
+		}
+
+		return Ok();
+	}
+
+	/// <summary>
+	/// 关于
+	/// </summary>
+	/// <returns></returns>
+	[Route("about"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
+	public async Task<ActionResult> About()
+	{
+		var text = await new FileInfo(Path.Combine(HostEnvironment.WebRootPath, "template", "about.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8);
+		return View(model: text);
+	}
+
+	/// <summary>
+	/// 评论及留言须知
+	/// </summary>
+	/// <returns></returns>
+	[Route("agreement"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
+	public async Task<ActionResult> Agreement()
+	{
+		var text = await new FileInfo(Path.Combine(HostEnvironment.WebRootPath, "template", "agreement.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8);
+		return View(model: text);
+	}
+
+	/// <summary>
+	/// 声明
+	/// </summary>
+	/// <returns></returns>
+	[Route("disclaimer"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
+	public async Task<ActionResult> Disclaimer()
+	{
+		var text = await new FileInfo(Path.Combine(HostEnvironment.WebRootPath, "template", "disclaimer.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8);
+		return View(model: text);
+	}
+
+	/// <summary>
+	/// 创建页面
+	/// </summary>
+	/// <param name="model"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Write([FromBodyOrDefault] Misc model, CancellationToken cancellationToken)
+	{
+		model.Content = await ImagebedClient.ReplaceImgSrc(await model.Content.Trim().ClearImgAttributes(), cancellationToken);
+		var e = MiscService.AddEntitySaved(model);
+		return e != null ? ResultData(null, message: "发布成功") : ResultData(null, false, "发布失败");
+	}
+
+	/// <summary>
+	/// 删除页面
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Delete(int id)
+	{
+		bool b = await MiscService.DeleteByIdAsync(id) > 0;
+		return ResultData(null, b, b ? "删除成功" : "删除失败");
+	}
+
+	/// <summary>
+	/// 编辑页面
+	/// </summary>
+	/// <param name="misc"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Edit([FromBodyOrDefault] Misc misc, CancellationToken cancellationToken)
+	{
+		var entity = await MiscService.GetByIdAsync(misc.Id) ?? throw new NotFoundException("杂项页未找到");
+		entity.ModifyDate = DateTime.Now;
+		entity.Title = misc.Title;
+		entity.Content = await ImagebedClient.ReplaceImgSrc(await misc.Content.ClearImgAttributes(), cancellationToken);
+		bool b = await MiscService.SaveChangesAsync() > 0;
+		return ResultData(null, b, b ? "修改成功" : "修改失败");
+	}
+
+	/// <summary>
+	/// 分页数据
+	/// </summary>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public ActionResult GetPageData(int page = 1, int size = 10)
+	{
+		var list = MiscService.GetPages(page, size, n => true, n => n.ModifyDate, false);
+		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));
+		}
+
+		return Ok(list);
+	}
+
+	/// <summary>
+	/// 详情
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Get(int id)
+	{
+		var misc = await MiscService.GetByIdAsync(id);
+		if (misc != null)
+		{
+			misc.ModifyDate = misc.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+			misc.PostDate = misc.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		}
+
+		return ResultData(misc.Mapper<MiscDto>());
+	}
+}

+ 299 - 310
src/Masuit.MyBlogs.Core/Controllers/MsgController.cs

@@ -3,19 +3,9 @@ using Hangfire;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Common.Mails;
 using Masuit.MyBlogs.Core.Extensions;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.Command;
-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.Tools;
-using Masuit.Tools.Core.Net;
 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;
@@ -24,82 +14,53 @@ using System.Text;
 using System.Text.RegularExpressions;
 using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 留言板和站内信
+/// </summary>
+public sealed class MsgController : BaseController
 {
 	/// <summary>
-	/// 留言板和站内信
+	/// 留言
 	/// </summary>
-	public sealed class MsgController : BaseController
-	{
-		/// <summary>
-		/// 留言
-		/// </summary>
-		public ILeaveMessageService LeaveMessageService { get; set; }
-
-		/// <summary>
-		/// 站内信
-		/// </summary>
-		public IInternalMessageService MessageService { get; set; }
-
-		public IWebHostEnvironment HostEnvironment { get; set; }
-
-		public ICacheManager<int> MsgFeq { get; set; }
-
-		/// <summary>
-		/// 留言板
-		/// </summary>
-		/// <returns></returns>
-		[Route("msg"), Route("msg/{cid:int}"), ResponseCache(Duration = 600, VaryByHeader = "Cookie")]
-		public async Task<ActionResult> Index()
-		{
-			ViewBag.TotalCount = LeaveMessageService.Count(m => m.ParentId == null && m.Status == Status.Published);
-			var text = await new FileInfo(Path.Combine(HostEnvironment.WebRootPath, "template", "agreement.html")).ShareReadWrite().ReadAllTextAsync(Encoding.UTF8);
-			return CurrentUser.IsAdmin ? View("Index_Admin", text) : View(model: text);
-		}
+	public ILeaveMessageService LeaveMessageService { get; set; }
 
-		/// <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;
-					}
-				}
+	/// <summary>
+	/// 站内信
+	/// </summary>
+	public IInternalMessageService MessageService { get; set; }
 
-				return ResultData(new
-				{
-					total = 1,
-					parentTotal = 1,
-					page,
-					size,
-					rows = layer.ToTree(e => e.Id, e => e.ParentId).Mapper<IList<LeaveMessageViewModel>>()
-				});
-			}
+	public IWebHostEnvironment HostEnvironment { get; set; }
 
-			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 =>
+	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)
@@ -108,280 +69,308 @@ namespace Masuit.MyBlogs.Core.Controllers
 					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, "没有留言");
+			return ResultData(new
+			{
+				total = 1,
+				parentTotal = 1,
+				page,
+				size,
+				rows = layer.ToTree(e => e.Id, e => e.ParentId).Mapper<IList<LeaveMessageViewModel>>()
+			});
 		}
 
-		/// <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 parent = await LeaveMessageService.GetPagesAsync(page, size, m => m.ParentId == null && (m.Status == Status.Published || CurrentUser.IsAdmin), m => m.PostDate, false);
+		if (!parent.Data.Any())
 		{
-			var match = Regex.Match(cmd.NickName + cmd.Content.RemoveHtmlTag(), CommonHelper.BanRegex);
-			if (match.Success)
+			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)
 			{
-				LogManager.Info($"提交内容:{cmd.NickName}/{cmd.Content},敏感词:{match.Value}");
-				return ResultData(null, false, "您提交的内容包含敏感词,被禁止发表,请检查您的内容后尝试重新提交!");
+				m.Email = null;
+				m.IP = null;
+				m.Location = null;
 			}
-
-			var error = ValidateEmailCode(mailSender, cmd.Email, cmd.Code);
-			if (!string.IsNullOrEmpty(error))
+		});
+		if (total > 0)
+		{
+			return ResultData(new
 			{
-				return ResultData(null, false, error);
-			}
+				total,
+				parentTotal = total,
+				page,
+				size,
+				rows = messages.OrderByDescending(c => c.PostDate).ToTree(c => c.Id, c => c.ParentId).Mapper<IList<LeaveMessageViewModel>>()
+			});
+		}
 
-			if (cmd.ParentId > 0 && DateTime.Now - LeaveMessageService[cmd.ParentId.Value, m => m.PostDate] > TimeSpan.FromDays(180))
-			{
-				return ResultData(null, false, "当前留言过于久远,不再允许回复!");
-			}
+		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, "您的发言频率过快,请稍后再发表吧!");
-			}
+	/// <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 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;
-			}
+		var error = ValidateEmailCode(mailSender, cmd.Email, cmd.Code);
+		if (!string.IsNullOrEmpty(error))
+		{
+			return ResultData(null, false, error);
+		}
 
-			if (Regex.Match(cmd.NickName + cmd.Content, CommonHelper.ModRegex).Length <= 0)
-			{
-				msg.Status = Status.Published;
-			}
+		if (cmd.ParentId > 0 && DateTime.Now - LeaveMessageService[cmd.ParentId.Value, m => m.PostDate] > TimeSpan.FromDays(180))
+		{
+			return ResultData(null, false, "当前留言过于久远,不再允许回复!");
+		}
 
-			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;
-				}
-			}
+		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, "您的发言频率过快,请稍后再发表吧!");
+		}
 
-			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, "留言发表失败!");
-			}
+		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;
+		}
 
-			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 (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)
 			{
-				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, "留言发表成功,服务器正在后台处理中,这会有一定的延迟,稍后将会显示到列表中!");
+				msg.Status = Status.Published;
+				msg.IsMaster = 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, "留言发表成功,待站长审核通过以后将显示到列表中!");
+		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, "留言发表失败!");
 		}
 
-		/// <summary>
-		/// 审核
-		/// </summary>
-		/// <param name="id"></param>
-		/// <returns></returns>
-		[MyAuthorize]
-		public async Task<ActionResult> Pass(int id)
+		Response.Cookies.Append("NickName", msg.NickName, new CookieOptions()
 		{
-			var msg = await LeaveMessageService.GetByIdAsync(id);
-			msg.Status = Status.Published;
-			bool b = await LeaveMessageService.SaveChangesAsync() > 0;
-			if (b)
+			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 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);
+				//通知博主和上层所有关联的评论访客
+				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, b, b ? "审核通过!" : "审核失败!");
+			return ResultData(null, true, "留言发表成功,服务器正在后台处理中,这会有一定的延迟,稍后将会显示到列表中!");
 		}
 
-		/// <summary>
-		/// 删除留言
-		/// </summary>
-		/// <param name="id"></param>
-		/// <returns></returns>
-		[MyAuthorize]
-		public ActionResult Delete(int id)
+		BackgroundJob.Enqueue(() => CommonHelper.SendMail(Request.Host + "|博客新留言(待审核):", content.Set("link", Url.Action("Index", "Msg", new
 		{
-			var b = LeaveMessageService.DeleteById(id);
-			return ResultData(null, b, b ? "删除成功!" : "删除失败!");
-		}
+			cid = msg.Id
+		}, Request.Scheme)).Render(false) + "<p style='color:red;'>(待审核)</p>", email, ClientIP));
+		return ResultData(null, true, "留言发表成功,待站长审核通过以后将显示到列表中!");
+	}
 
-		/// <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)
+	/// <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 list = await LeaveMessageService.GetPagesAsync<DateTime, LeaveMessageDto>(page, size, m => m.Status == Status.Pending, l => l.PostDate, false);
-			foreach (var m in list.Data)
+			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)
 			{
-				m.PostDate = m.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+				BackgroundJob.Enqueue(() => CommonHelper.SendMail($"{Request.Host}{CommonHelper.SystemSettings["Title"]} 留言回复:", content.Set("link", link).Render(false), s, ClientIP));
 			}
-
-			return Ok(list);
 		}
 
-		#region 站内消息
+		return ResultData(null, b, b ? "审核通过!" : "审核失败!");
+	}
 
-		/// <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 ActionResult Delete(int id)
+	{
+		var b = LeaveMessageService.DeleteById(id);
+		return ResultData(null, b, b ? "删除成功!" : "删除失败!");
+	}
 
-		/// <summary>
-		/// 标记为未读
-		/// </summary>
-		/// <param name="id"></param>
-		/// <returns></returns>
-		[MyAuthorize]
-		public async Task<ActionResult> Unread(int id)
+	/// <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)
 		{
-			await MessageService.GetQuery(m => m.Id == id).ExecuteUpdateAsync(s => s.SetProperty(m => m.Read, false));
-			return Content("ok");
+			m.PostDate = m.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
 		}
 
-		/// <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);
-		}
+		return Ok(list);
+	}
 
-		/// <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 ? "站内消息删除成功!" : "站内消息删除失败!");
-		}
+	#region 站内消息
 
-		/// <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>
+	/// <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>
-		/// <returns></returns>
-		[MyAuthorize]
-		public ActionResult GetUnreadMsgs()
-		{
-			var msgs = MessageService.GetQueryNoTracking(m => !m.Read, m => m.Time, false).ToList();
-			return ResultData(msgs);
-		}
+	/// <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>
-		/// <returns></returns>
-		[MyAuthorize]
-		public async Task<ActionResult> ClearMsgs()
-		{
-			await MessageService.DeleteEntitySavedAsync(m => m.Read);
-			return ResultData(null, true, "站内消息清除成功!");
-		}
+	/// <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);
+	}
 
-		#endregion 站内消息
+	/// <summary>
+	/// 清除站内信
+	/// </summary>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> ClearMsgs()
+	{
+		await MessageService.DeleteEntitySavedAsync(m => m.Read);
+		return ResultData(null, true, "站内消息清除成功!");
 	}
-}
+
+	#endregion 站内消息
+}

+ 207 - 214
src/Masuit.MyBlogs.Core/Controllers/NoticeController.cs

@@ -1,224 +1,217 @@
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
 using Masuit.MyBlogs.Core.Extensions.Firewall;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
 using Masuit.MyBlogs.Core.Models;
-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.Tools.AspNetCore.ModelBinder;
-using Masuit.Tools.Core.Net;
 using Microsoft.AspNetCore.Mvc;
 using System.ComponentModel.DataAnnotations;
 using Z.EntityFramework.Plus;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 网站公告
+/// </summary>
+public sealed class NoticeController : BaseController
 {
-    /// <summary>
-    /// 网站公告
-    /// </summary>
-    public sealed class NoticeController : BaseController
-    {
-        /// <summary>
-        /// 公告
-        /// </summary>
-        public INoticeService NoticeService { get; set; }
-
-        public ImagebedClient ImagebedClient { get; set; }
-
-        /// <summary>
-        /// 公告列表
-        /// </summary>
-        /// <param name="page"></param>
-        /// <param name="size"></param>
-        /// <returns></returns>
-        [Route("notice"), AllowAccessFirewall, Route("n", Order = 1), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size" }, VaryByHeader = "Cookie")]
-        public ActionResult Index([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
-        {
-            var list = NoticeService.GetPagesFromCache<DateTime, NoticeDto>(page, size, n => n.NoticeStatus == NoticeStatus.Normal, n => n.ModifyDate, false);
-            ViewData["page"] = new Pagination(page, size, list.TotalCount);
-            foreach (var n in list.Data)
-            {
-                n.ModifyDate = n.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-                n.PostDate = n.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-                n.Content = ReplaceVariables(n.Content);
-            }
-
-            ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location());
-            return CurrentUser.IsAdmin ? View("Index_Admin", list.Data) : View(list.Data);
-        }
-
-        /// <summary>
-        /// 公告详情
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [Route("notice/{id:int}"), AllowAccessFirewall, ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id" }, VaryByHeader = "Cookie")]
-        public async Task<ActionResult> Details(int id)
-        {
-            var notice = await NoticeService.GetByIdAsync(id) ?? throw new NotFoundException("页面未找到");
-            if (!HttpContext.Session.TryGetValue("notice" + id, out _))
-            {
-                notice.ViewCount += 1;
-                await NoticeService.SaveChangesAsync();
-                HttpContext.Session.Set("notice" + id, notice.Title);
-            }
-
-            notice.ModifyDate = notice.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-            notice.PostDate = notice.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-            notice.Content = ReplaceVariables(notice.Content);
-            ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location());
-            return View(notice);
-        }
-
-        /// <summary>
-        /// 发布公告
-        /// </summary>
-        /// <param name="notice"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> Write([FromBodyOrDefault] Notice notice, CancellationToken cancellationToken)
-        {
-            notice.Content = await ImagebedClient.ReplaceImgSrc(await notice.Content.ClearImgAttributes(), cancellationToken);
-            if (notice.StartTime.HasValue && notice.EndTime.HasValue && notice.StartTime >= notice.EndTime)
-            {
-                return ResultData(null, false, "开始时间不能小于结束时间");
-            }
-
-            notice.NoticeStatus = NoticeStatus.Normal;
-            if (DateTime.Now < notice.StartTime)
-            {
-                notice.NoticeStatus = NoticeStatus.UnStart;
-            }
-
-            var e = NoticeService.AddEntitySaved(notice);
-            QueryCacheManager.ExpireType<Notice>();
-            return e != null ? ResultData(null, message: "发布成功") : ResultData(null, false, "发布失败");
-        }
-
-        /// <summary>
-        /// 删除公告
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> Delete(int id)
-        {
-            bool b = await NoticeService.DeleteByIdAsync(id) > 0;
-            QueryCacheManager.ExpireType<Notice>();
-            return ResultData(null, b, b ? "删除成功" : "删除失败");
-        }
-
-        /// <summary>
-        /// 公告上下架
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> ChangeState(int id)
-        {
-            var notice = await NoticeService.GetByIdAsync(id) ?? throw new NotFoundException("公告未找到");
-            notice.NoticeStatus = notice.NoticeStatus == NoticeStatus.Normal ? NoticeStatus.Expired : NoticeStatus.Normal;
-            var b = await NoticeService.SaveChangesAsync() > 0;
-            QueryCacheManager.ExpireType<Notice>();
-            return ResultData(null, b, notice.NoticeStatus == NoticeStatus.Normal ? $"【{notice.Title}】已上架!" : $"【{notice.Title}】已下架!");
-        }
-
-        /// <summary>
-        /// 编辑公告
-        /// </summary>
-        /// <param name="notice"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> Edit([FromBodyOrDefault] NoticeDto notice, CancellationToken cancellationToken)
-        {
-            var entity = await NoticeService.GetByIdAsync(notice.Id) ?? throw new NotFoundException("公告已经被删除!");
-            if (notice.StartTime.HasValue && notice.EndTime.HasValue && notice.StartTime >= notice.EndTime)
-            {
-                return ResultData(null, false, "开始时间不能小于结束时间");
-            }
-
-            if (DateTime.Now < notice.StartTime)
-            {
-                entity.NoticeStatus = NoticeStatus.UnStart;
-            }
-
-            entity.ModifyDate = DateTime.Now;
-            entity.StartTime = notice.StartTime;
-            entity.EndTime = notice.EndTime;
-            entity.Title = notice.Title;
-            entity.StrongAlert = notice.StrongAlert;
-            entity.Content = await ImagebedClient.ReplaceImgSrc(await notice.Content.ClearImgAttributes(), cancellationToken);
-            bool b = await NoticeService.SaveChangesAsync() > 0;
-            QueryCacheManager.ExpireType<Notice>();
-            return ResultData(null, b, b ? "修改成功" : "修改失败");
-        }
-
-        /// <summary>
-        /// 公告分页数据
-        /// </summary>
-        /// <param name="page"></param>
-        /// <param name="size"></param>
-        /// <returns></returns>
-        public ActionResult GetPageData([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
-        {
-            var list = NoticeService.GetPagesNoTracking(page, size, n => true, n => n.ModifyDate, false);
-            foreach (var n in list.Data)
-            {
-                n.ModifyDate = n.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-                n.PostDate = n.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-            }
-
-            return Ok(list);
-        }
-
-        /// <summary>
-        /// 公告详情
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public ActionResult Get(int id)
-        {
-            var notice = NoticeService.Get<NoticeDto>(n => n.Id == id);
-            if (notice != null)
-            {
-                notice.ModifyDate = notice.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-                notice.PostDate = notice.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-                notice.Content = ReplaceVariables(notice.Content);
-            }
-
-            return ResultData(notice);
-        }
-
-        /// <summary>
-        /// 最近一条公告
-        /// </summary>
-        /// <returns></returns>
-        [ResponseCache(Duration = 600, VaryByHeader = "Cookie"), AllowAccessFirewall]
-        public async Task<ActionResult> Last()
-        {
-            var notice = await NoticeService.GetAsync(n => n.NoticeStatus == NoticeStatus.Normal, n => n.ModifyDate, false);
-            if (notice == null)
-            {
-                return ResultData(null, false);
-            }
-
-            if (Request.Cookies.TryGetValue("last-notice", out var id) && notice.Id.ToString() == id)
-            {
-                return ResultData(null, false);
-            }
-
-            notice.ViewCount += 1;
-            await NoticeService.SaveChangesAsync();
-            var dto = notice.Mapper<NoticeDto>();
-            Response.Cookies.Append("last-notice", dto.Id.ToString(), new CookieOptions()
-            {
-                Expires = DateTime.Now.AddYears(1),
-                SameSite = SameSiteMode.Lax
-            });
-            return ResultData(dto);
-        }
-    }
-}
+	/// <summary>
+	/// 公告
+	/// </summary>
+	public INoticeService NoticeService { get; set; }
+
+	public ImagebedClient ImagebedClient { get; set; }
+
+	/// <summary>
+	/// 公告列表
+	/// </summary>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <returns></returns>
+	[Route("notice"), AllowAccessFirewall, Route("n", Order = 1), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size" }, VaryByHeader = "Cookie")]
+	public ActionResult Index([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
+	{
+		var list = NoticeService.GetPagesFromCache<DateTime, NoticeDto>(page, size, n => n.NoticeStatus == NoticeStatus.Normal, n => n.ModifyDate, false);
+		ViewData["page"] = new Pagination(page, size, list.TotalCount);
+		foreach (var n in list.Data)
+		{
+			n.ModifyDate = n.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+			n.PostDate = n.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+			n.Content = ReplaceVariables(n.Content);
+		}
+
+		ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location());
+		return CurrentUser.IsAdmin ? View("Index_Admin", list.Data) : View(list.Data);
+	}
+
+	/// <summary>
+	/// 公告详情
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[Route("notice/{id:int}"), AllowAccessFirewall, ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "id" }, VaryByHeader = "Cookie")]
+	public async Task<ActionResult> Details(int id)
+	{
+		var notice = await NoticeService.GetByIdAsync(id) ?? throw new NotFoundException("页面未找到");
+		if (!HttpContext.Session.TryGetValue("notice" + id, out _))
+		{
+			notice.ViewCount += 1;
+			await NoticeService.SaveChangesAsync();
+			HttpContext.Session.Set("notice" + id, notice.Title);
+		}
+
+		notice.ModifyDate = notice.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		notice.PostDate = notice.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		notice.Content = ReplaceVariables(notice.Content);
+		ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.InPage, Request.Location());
+		return View(notice);
+	}
+
+	/// <summary>
+	/// 发布公告
+	/// </summary>
+	/// <param name="notice"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Write([FromBodyOrDefault] Notice notice, CancellationToken cancellationToken)
+	{
+		notice.Content = await ImagebedClient.ReplaceImgSrc(await notice.Content.ClearImgAttributes(), cancellationToken);
+		if (notice.StartTime.HasValue && notice.EndTime.HasValue && notice.StartTime >= notice.EndTime)
+		{
+			return ResultData(null, false, "开始时间不能小于结束时间");
+		}
+
+		notice.NoticeStatus = NoticeStatus.Normal;
+		if (DateTime.Now < notice.StartTime)
+		{
+			notice.NoticeStatus = NoticeStatus.UnStart;
+		}
+
+		var e = NoticeService.AddEntitySaved(notice);
+		QueryCacheManager.ExpireType<Notice>();
+		return e != null ? ResultData(null, message: "发布成功") : ResultData(null, false, "发布失败");
+	}
+
+	/// <summary>
+	/// 删除公告
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Delete(int id)
+	{
+		bool b = await NoticeService.DeleteByIdAsync(id) > 0;
+		QueryCacheManager.ExpireType<Notice>();
+		return ResultData(null, b, b ? "删除成功" : "删除失败");
+	}
+
+	/// <summary>
+	/// 公告上下架
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> ChangeState(int id)
+	{
+		var notice = await NoticeService.GetByIdAsync(id) ?? throw new NotFoundException("公告未找到");
+		notice.NoticeStatus = notice.NoticeStatus == NoticeStatus.Normal ? NoticeStatus.Expired : NoticeStatus.Normal;
+		var b = await NoticeService.SaveChangesAsync() > 0;
+		QueryCacheManager.ExpireType<Notice>();
+		return ResultData(null, b, notice.NoticeStatus == NoticeStatus.Normal ? $"【{notice.Title}】已上架!" : $"【{notice.Title}】已下架!");
+	}
+
+	/// <summary>
+	/// 编辑公告
+	/// </summary>
+	/// <param name="notice"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Edit([FromBodyOrDefault] NoticeDto notice, CancellationToken cancellationToken)
+	{
+		var entity = await NoticeService.GetByIdAsync(notice.Id) ?? throw new NotFoundException("公告已经被删除!");
+		if (notice.StartTime.HasValue && notice.EndTime.HasValue && notice.StartTime >= notice.EndTime)
+		{
+			return ResultData(null, false, "开始时间不能小于结束时间");
+		}
+
+		if (DateTime.Now < notice.StartTime)
+		{
+			entity.NoticeStatus = NoticeStatus.UnStart;
+		}
+
+		entity.ModifyDate = DateTime.Now;
+		entity.StartTime = notice.StartTime;
+		entity.EndTime = notice.EndTime;
+		entity.Title = notice.Title;
+		entity.StrongAlert = notice.StrongAlert;
+		entity.Content = await ImagebedClient.ReplaceImgSrc(await notice.Content.ClearImgAttributes(), cancellationToken);
+		bool b = await NoticeService.SaveChangesAsync() > 0;
+		QueryCacheManager.ExpireType<Notice>();
+		return ResultData(null, b, b ? "修改成功" : "修改失败");
+	}
+
+	/// <summary>
+	/// 公告分页数据
+	/// </summary>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <returns></returns>
+	public ActionResult GetPageData([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
+	{
+		var list = NoticeService.GetPagesNoTracking(page, size, n => true, n => n.ModifyDate, false);
+		foreach (var n in list.Data)
+		{
+			n.ModifyDate = n.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+			n.PostDate = n.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		}
+
+		return Ok(list);
+	}
+
+	/// <summary>
+	/// 公告详情
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public ActionResult Get(int id)
+	{
+		var notice = NoticeService.Get<NoticeDto>(n => n.Id == id);
+		if (notice != null)
+		{
+			notice.ModifyDate = notice.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+			notice.PostDate = notice.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+			notice.Content = ReplaceVariables(notice.Content);
+		}
+
+		return ResultData(notice);
+	}
+
+	/// <summary>
+	/// 最近一条公告
+	/// </summary>
+	/// <returns></returns>
+	[ResponseCache(Duration = 600, VaryByHeader = "Cookie"), AllowAccessFirewall]
+	public async Task<ActionResult> Last()
+	{
+		var notice = await NoticeService.GetAsync(n => n.NoticeStatus == NoticeStatus.Normal, n => n.ModifyDate, false);
+		if (notice == null)
+		{
+			return ResultData(null, false);
+		}
+
+		if (Request.Cookies.TryGetValue("last-notice", out var id) && notice.Id.ToString() == id)
+		{
+			return ResultData(null, false);
+		}
+
+		notice.ViewCount += 1;
+		await NoticeService.SaveChangesAsync();
+		var dto = notice.Mapper<NoticeDto>();
+		Response.Cookies.Append("last-notice", dto.Id.ToString(), new CookieOptions()
+		{
+			Expires = DateTime.Now.AddYears(1),
+			SameSite = SameSiteMode.Lax
+		});
+		return ResultData(dto);
+	}
+}

+ 182 - 192
src/Masuit.MyBlogs.Core/Controllers/PassportController.cs

@@ -1,85 +1,103 @@
 using CacheManager.Core;
 using Hangfire;
-using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Configs;
 using Masuit.MyBlogs.Core.Extensions.Firewall;
 using Masuit.MyBlogs.Core.Extensions.Hangfire;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.DTO;
-using Masuit.MyBlogs.Core.Models.Enum;
-using Masuit.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools;
 using Masuit.Tools.AspNetCore.Mime;
 using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
-using Masuit.Tools.Core.Net;
 using Masuit.Tools.Logging;
-using Masuit.Tools.Security;
-using Masuit.Tools.Strings;
 using Microsoft.AspNetCore.Mvc;
 using System.Net;
 using System.Web;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 登录授权
+/// </summary>
+[ApiExplorerSettings(IgnoreApi = true), ServiceFilter(typeof(FirewallAttribute))]
+public sealed class PassportController : Controller
 {
 	/// <summary>
-	/// 登录授权
+	/// 用户
 	/// </summary>
-	[ApiExplorerSettings(IgnoreApi = true), ServiceFilter(typeof(FirewallAttribute))]
-	public sealed class PassportController : Controller
-	{
-		/// <summary>
-		/// 用户
-		/// </summary>
-		public IUserInfoService UserInfoService { get; set; }
+	public IUserInfoService UserInfoService { get; set; }
 
-		public IFirewallRepoter FirewallRepoter { get; set; }
+	public IFirewallRepoter FirewallRepoter { get; set; }
 
-		/// <summary>
-		/// 客户端的真实IP
-		/// </summary>
-		public string ClientIP => HttpContext.Connection.RemoteIpAddress.ToString();
+	/// <summary>
+	/// 客户端的真实IP
+	/// </summary>
+	public string ClientIP => HttpContext.Connection.RemoteIpAddress.ToString();
 
-		/// <summary>
-		///
-		/// </summary>
-		/// <param name="data"></param>
-		/// <param name="isTrue"></param>
-		/// <param name="message"></param>
-		/// <returns></returns>
-		public ActionResult ResultData(object data, bool isTrue = true, string message = "")
+	/// <summary>
+	///
+	/// </summary>
+	/// <param name="data"></param>
+	/// <param name="isTrue"></param>
+	/// <param name="message"></param>
+	/// <returns></returns>
+	public ActionResult ResultData(object data, bool isTrue = true, string message = "")
+	{
+		return Json(new
+		{
+			Success = isTrue,
+			Message = message,
+			Data = data
+		});
+	}
+
+	/// <summary>
+	/// 登录页
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult Login()
+	{
+		var keys = RsaCrypt.GenerateRsaKeys(RsaKeyType.PKCS1);
+		Response.Cookies.Append(nameof(keys.PublicKey), keys.PublicKey, new CookieOptions()
 		{
-			return Json(new
+			SameSite = SameSiteMode.Lax
+		});
+		HttpContext.Session.Set(nameof(keys.PrivateKey), keys.PrivateKey);
+		string from = Request.Query["from"];
+		if (!string.IsNullOrEmpty(from))
+		{
+			from = HttpUtility.UrlDecode(from);
+			Response.Cookies.Append("refer", from, new CookieOptions()
 			{
-				Success = isTrue,
-				Message = message,
-				Data = data
+				SameSite = SameSiteMode.Lax
 			});
 		}
 
-		/// <summary>
-		/// 登录页
-		/// </summary>
-		/// <returns></returns>
-		public ActionResult Login()
+		if (HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo) != null)
 		{
-			var keys = RsaCrypt.GenerateRsaKeys(RsaKeyType.PKCS1);
-			Response.Cookies.Append(nameof(keys.PublicKey), keys.PublicKey, new CookieOptions()
+			if (string.IsNullOrEmpty(from))
 			{
-				SameSite = SameSiteMode.Lax
-			});
-			HttpContext.Session.Set(nameof(keys.PrivateKey), keys.PrivateKey);
-			string from = Request.Query["from"];
-			if (!string.IsNullOrEmpty(from))
+				return RedirectToAction("Index", "Home");
+			}
+
+			return LocalRedirect(from);
+		}
+
+		if (Request.Cookies.Count > 2)
+		{
+			string name = Request.Cookies["username"];
+			string pwd = Request.Cookies["password"]?.DesDecrypt(AppConfig.BaiduAK);
+			var userInfo = UserInfoService.Login(name, pwd);
+			if (userInfo != null)
 			{
-				from = HttpUtility.UrlDecode(from);
-				Response.Cookies.Append("refer", from, new CookieOptions()
+				Response.Cookies.Append("username", name, new CookieOptions()
 				{
+					Expires = DateTime.Now.AddYears(1),
 					SameSite = SameSiteMode.Lax
 				});
-			}
-
-			if (HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo) != null)
-			{
+				Response.Cookies.Append("password", Request.Cookies["password"], new CookieOptions()
+				{
+					Expires = DateTime.Now.AddYears(1),
+					SameSite = SameSiteMode.Lax
+				});
+				HttpContext.Session.Set(SessionKey.UserInfo, userInfo);
+				BackgroundJob.Enqueue<IHangfireBackJob>(job => job.LoginRecord(userInfo, ClientIP, LoginType.Default));
 				if (string.IsNullOrEmpty(from))
 				{
 					return RedirectToAction("Index", "Home");
@@ -87,172 +105,144 @@ namespace Masuit.MyBlogs.Core.Controllers
 
 				return LocalRedirect(from);
 			}
+		}
 
-			if (Request.Cookies.Count > 2)
-			{
-				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
-					});
-					HttpContext.Session.Set(SessionKey.UserInfo, userInfo);
-					BackgroundJob.Enqueue<IHangfireBackJob>(job => job.LoginRecord(userInfo, ClientIP, LoginType.Default));
-					if (string.IsNullOrEmpty(from))
-					{
-						return RedirectToAction("Index", "Home");
-					}
-
-					return LocalRedirect(from);
-				}
-			}
+		return View();
+	}
 
-			return View();
+	/// <summary>
+	/// 登陆检查
+	/// </summary>
+	/// <param name="username"></param>
+	/// <param name="password"></param>
+	/// <param name="valid"></param>
+	/// <param name="remem"></param>
+	/// <returns></returns>
+	[HttpPost, ValidateAntiForgeryToken]
+	public ActionResult Login([FromServices] ICacheManager<int> cacheManager, string username, string password, string valid, string remem)
+	{
+		string validSession = HttpContext.Session.Get<string>("valid") ?? string.Empty; //将验证码从Session中取出来,用于登录验证比较
+		if (string.IsNullOrEmpty(validSession) || !valid.Trim().Equals(validSession, StringComparison.InvariantCultureIgnoreCase))
+		{
+			return ResultData(null, false, "验证码错误");
 		}
 
-		/// <summary>
-		/// 登陆检查
-		/// </summary>
-		/// <param name="username"></param>
-		/// <param name="password"></param>
-		/// <param name="valid"></param>
-		/// <param name="remem"></param>
-		/// <returns></returns>
-		[HttpPost, ValidateAntiForgeryToken]
-		public ActionResult Login([FromServices] ICacheManager<int> cacheManager, string username, string password, string valid, string remem)
+		HttpContext.Session.Remove("valid"); //验证成功就销毁验证码Session,非常重要
+		if (string.IsNullOrEmpty(username.Trim()) || string.IsNullOrEmpty(password.Trim()))
 		{
-			string validSession = HttpContext.Session.Get<string>("valid") ?? string.Empty; //将验证码从Session中取出来,用于登录验证比较
-			if (string.IsNullOrEmpty(validSession) || !valid.Trim().Equals(validSession, StringComparison.InvariantCultureIgnoreCase))
-			{
-				return ResultData(null, false, "验证码错误");
-			}
-
-			HttpContext.Session.Remove("valid"); //验证成功就销毁验证码Session,非常重要
-			if (string.IsNullOrEmpty(username.Trim()) || string.IsNullOrEmpty(password.Trim()))
-			{
-				return ResultData(null, false, "用户名或密码不能为空");
-			}
+			return ResultData(null, false, "用户名或密码不能为空");
+		}
 
-			try
-			{
-				var privateKey = HttpContext.Session.Get<string>(nameof(RsaKey.PrivateKey));
-				password = password.RSADecrypt(privateKey);
-			}
-			catch (Exception)
-			{
-				LogManager.Info("登录失败,私钥:" + HttpContext.Session.Get<string>(nameof(RsaKey.PrivateKey)));
-				throw;
-			}
-			var userInfo = UserInfoService.Login(username, password);
-			if (userInfo == null)
+		try
+		{
+			var privateKey = HttpContext.Session.Get<string>(nameof(RsaKey.PrivateKey));
+			password = password.RSADecrypt(privateKey);
+		}
+		catch (Exception)
+		{
+			LogManager.Info("登录失败,私钥:" + HttpContext.Session.Get<string>(nameof(RsaKey.PrivateKey)));
+			throw;
+		}
+		var userInfo = UserInfoService.Login(username, password);
+		if (userInfo == null)
+		{
+			var times = cacheManager.AddOrUpdate("LoginError:" + ClientIP, 1, i => i + 1, 5);
+			if (times > 30)
 			{
-				var times = cacheManager.AddOrUpdate("LoginError:" + ClientIP, 1, i => i + 1, 5);
-				if (times > 30)
-				{
-					FirewallRepoter.ReportAsync(IPAddress.Parse(ClientIP)).ContinueWith(_ => LogManager.Info($"多次登录用户名或密码错误,疑似爆破行为,已上报IP{ClientIP}至:" + FirewallRepoter.ReporterName));
-				}
-
-				return ResultData(null, false, "用户名或密码错误");
+				FirewallRepoter.ReportAsync(IPAddress.Parse(ClientIP)).ContinueWith(_ => LogManager.Info($"多次登录用户名或密码错误,疑似爆破行为,已上报IP{ClientIP}至:" + FirewallRepoter.ReporterName));
 			}
 
-			HttpContext.Session.Set(SessionKey.UserInfo, userInfo);
-			if (remem.Trim().Contains(new[] { "on", "true" })) //是否记住登录
-			{
-				Response.Cookies.Append("username", HttpUtility.UrlEncode(username.Trim()), new CookieOptions()
-				{
-					Expires = DateTime.Now.AddYears(1),
-					SameSite = SameSiteMode.Lax
-				});
-				Response.Cookies.Append("password", password.Trim().DesEncrypt(AppConfig.BaiduAK), new CookieOptions()
-				{
-					Expires = DateTime.Now.AddYears(1),
-					SameSite = SameSiteMode.Lax
-				});
-			}
+			return ResultData(null, false, "用户名或密码错误");
+		}
 
-			BackgroundJob.Enqueue<IHangfireBackJob>(job => job.LoginRecord(userInfo, ClientIP, LoginType.Default));
-			string refer = Request.Cookies["refer"];
-			Response.Cookies.Delete(nameof(RsaKey.PublicKey), new CookieOptions()
+		HttpContext.Session.Set(SessionKey.UserInfo, userInfo);
+		if (remem.Trim().Contains(new[] { "on", "true" })) //是否记住登录
+		{
+			Response.Cookies.Append("username", HttpUtility.UrlEncode(username.Trim()), new CookieOptions()
 			{
+				Expires = DateTime.Now.AddYears(1),
 				SameSite = SameSiteMode.Lax
 			});
-			Response.Cookies.Delete("refer", new CookieOptions()
+			Response.Cookies.Append("password", password.Trim().DesEncrypt(AppConfig.BaiduAK), new CookieOptions()
 			{
+				Expires = DateTime.Now.AddYears(1),
 				SameSite = SameSiteMode.Lax
 			});
-			HttpContext.Session.Remove(nameof(RsaKey.PrivateKey));
-			return ResultData(null, true, string.IsNullOrEmpty(refer) ? "/" : refer);
 		}
 
-		/// <summary>
-		/// 生成验证码
-		/// </summary>
-		/// <returns></returns>
-		public ActionResult ValidateCode()
+		BackgroundJob.Enqueue<IHangfireBackJob>(job => job.LoginRecord(userInfo, ClientIP, LoginType.Default));
+		string refer = Request.Cookies["refer"];
+		Response.Cookies.Delete(nameof(RsaKey.PublicKey), new CookieOptions()
 		{
-			string code = Tools.Strings.ValidateCode.CreateValidateCode(6);
-			HttpContext.Session.Set("valid", code); //将验证码生成到Session中
-			var buffer = HttpContext.CreateValidateGraphic(code);
-			return this.ResumeFile(buffer, ContentType.Jpeg, "验证码.jpg");
-		}
-
-		/// <summary>
-		/// 检查验证码
-		/// </summary>
-		/// <param name="code"></param>
-		/// <returns></returns>
-		[HttpPost]
-		public ActionResult CheckValidateCode(string code)
+			SameSite = SameSiteMode.Lax
+		});
+		Response.Cookies.Delete("refer", new CookieOptions()
 		{
-			string validSession = HttpContext.Session.Get<string>("valid");
-			if (string.IsNullOrEmpty(validSession) || !code.Trim().Equals(validSession, StringComparison.InvariantCultureIgnoreCase))
-			{
-				return ResultData(null, false, "验证码错误");
-			}
+			SameSite = SameSiteMode.Lax
+		});
+		HttpContext.Session.Remove(nameof(RsaKey.PrivateKey));
+		return ResultData(null, true, string.IsNullOrEmpty(refer) ? "/" : refer);
+	}
 
-			return ResultData(null, false, "验证码正确");
-		}
+	/// <summary>
+	/// 生成验证码
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult ValidateCode()
+	{
+		string code = Tools.Strings.ValidateCode.CreateValidateCode(6);
+		HttpContext.Session.Set("valid", code); //将验证码生成到Session中
+		var buffer = HttpContext.CreateValidateGraphic(code);
+		return this.ResumeFile(buffer, ContentType.Jpeg, "验证码.jpg");
+	}
 
-		/// <summary>
-		/// 获取用户信息
-		/// </summary>
-		/// <returns></returns>
-		public ActionResult GetUserInfo()
+	/// <summary>
+	/// 检查验证码
+	/// </summary>
+	/// <param name="code"></param>
+	/// <returns></returns>
+	[HttpPost]
+	public ActionResult CheckValidateCode(string code)
+	{
+		string validSession = HttpContext.Session.Get<string>("valid");
+		if (string.IsNullOrEmpty(validSession) || !code.Trim().Equals(validSession, StringComparison.InvariantCultureIgnoreCase))
 		{
-			var user = HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
+			return ResultData(null, false, "验证码错误");
+		}
+
+		return ResultData(null, false, "验证码正确");
+	}
+
+	/// <summary>
+	/// 获取用户信息
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult GetUserInfo()
+	{
+		var user = HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
 #if DEBUG
 			user = UserInfoService.GetByUsername("masuit").Mapper<UserInfoDto>();
 #endif
 
-			return ResultData(user);
-		}
+		return ResultData(user);
+	}
 
-		/// <summary>
-		/// 注销登录
-		/// </summary>
-		/// <returns></returns>
-		public ActionResult Logout()
+	/// <summary>
+	/// 注销登录
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult Logout()
+	{
+		HttpContext.Session.Remove(SessionKey.UserInfo);
+		Response.Cookies.Delete("username", new CookieOptions()
 		{
-			HttpContext.Session.Remove(SessionKey.UserInfo);
-			Response.Cookies.Delete("username", new CookieOptions()
-			{
-				SameSite = SameSiteMode.Lax
-			});
-			Response.Cookies.Delete("password", new CookieOptions()
-			{
-				SameSite = SameSiteMode.Lax
-			});
-			HttpContext.Session.Clear();
-			return Request.Method.Equals(HttpMethods.Get) ? RedirectToAction("Index", "Home") : ResultData(null, message: "注销成功!");
-		}
+			SameSite = SameSiteMode.Lax
+		});
+		Response.Cookies.Delete("password", new CookieOptions()
+		{
+			SameSite = SameSiteMode.Lax
+		});
+		HttpContext.Session.Clear();
+		return Request.Method.Equals(HttpMethods.Get) ? RedirectToAction("Index", "Home") : ResultData(null, message: "注销成功!");
 	}
-}
+}

+ 20 - 22
src/Masuit.MyBlogs.Core/Controllers/PostController.cs

@@ -7,36 +7,20 @@ using Masuit.MyBlogs.Core.Configs;
 using Masuit.MyBlogs.Core.Extensions;
 using Masuit.MyBlogs.Core.Extensions.Firewall;
 using Masuit.MyBlogs.Core.Extensions.Hangfire;
-using Masuit.MyBlogs.Core.Infrastructure;
-using Masuit.MyBlogs.Core.Infrastructure.Repository;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.Command;
-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;
-using Masuit.Tools;
 using Masuit.Tools.AspNetCore.Mime;
 using Masuit.Tools.AspNetCore.ModelBinder;
 using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
-using Masuit.Tools.Core.Net;
 using Masuit.Tools.Core.Validator;
 using Masuit.Tools.Excel;
 using Masuit.Tools.Html;
-using Masuit.Tools.Linq;
 using Masuit.Tools.Logging;
 using Masuit.Tools.Models;
-using Masuit.Tools.Security;
-using Masuit.Tools.Strings;
-using Masuit.Tools.Systems;
 using Microsoft.AspNetCore.Http.Extensions;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Net.Http.Headers;
 using System.ComponentModel.DataAnnotations;
 using System.Linq.Dynamic.Core;
-using System.Linq.Expressions;
 using System.Net;
 using System.Text;
 using System.Text.RegularExpressions;
@@ -127,8 +111,8 @@ public sealed class PostController : BaseController
 		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 = await ReplaceVariables(post.Content).Next(s => notRobot ? s.InjectFingerprint() : Task.FromResult(s));
-		post.ProtectContent = await ReplaceVariables(post.ProtectContent).Next(s => notRobot ? s.InjectFingerprint() : Task.FromResult(s));
+		post.Content = await ReplaceVariables(post.Content).Next(s => notRobot && post.DisableCopy ? s.InjectFingerprint() : Task.FromResult(s));
+		post.ProtectContent = await ReplaceVariables(post.ProtectContent).Next(s => notRobot && post.DisableCopy ? s.InjectFingerprint() : Task.FromResult(s));
 
 		if (CurrentUser.IsAdmin)
 		{
@@ -183,8 +167,8 @@ public sealed class PostController : BaseController
 	{
 		var history = await PostHistoryVersionService.GetAsync(v => v.Id == hid && (v.Post.Status == Status.Published || CurrentUser.IsAdmin)) ?? throw new NotFoundException("文章未找到");
 		CheckPermission(history.Post);
-		history.Content = await ReplaceVariables(history.Content).Next(s => Request.IsRobot() ? Task.FromResult(s) : s.InjectFingerprint());
-		history.ProtectContent = await ReplaceVariables(history.ProtectContent).Next(s => Request.IsRobot() ? Task.FromResult(s) : s.InjectFingerprint());
+		history.Content = await ReplaceVariables(history.Content).Next(s => CurrentUser.IsAdmin || Request.IsRobot() ? Task.FromResult(s) : s.InjectFingerprint());
+		history.ProtectContent = await ReplaceVariables(history.ProtectContent).Next(s => CurrentUser.IsAdmin || Request.IsRobot() ? Task.FromResult(s) : s.InjectFingerprint());
 		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);
@@ -216,9 +200,9 @@ public sealed class PostController : BaseController
 		main.Id = id;
 		var diff = new HtmlDiff.HtmlDiff(right.Content, left.Content);
 		var diffOutput = diff.Build();
-		right.Content = await ReplaceVariables(Regex.Replace(Regex.Replace(diffOutput, "<ins.+?</ins>", string.Empty), @"<\w+></\w+>", string.Empty)).Next(s => Request.IsRobot() ? Task.FromResult(s) : s.InjectFingerprint());
+		right.Content = await ReplaceVariables(Regex.Replace(Regex.Replace(diffOutput, "<ins.+?</ins>", string.Empty), @"<\w+></\w+>", string.Empty)).Next(s => CurrentUser.IsAdmin || Request.IsRobot() ? Task.FromResult(s) : s.InjectFingerprint());
 		right.ModifyDate = right.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-		left.Content = await ReplaceVariables(Regex.Replace(Regex.Replace(diffOutput, "<del.+?</del>", string.Empty), @"<\w+></\w+>", string.Empty)).Next(s => Request.IsRobot() ? Task.FromResult(s) : s.InjectFingerprint());
+		left.Content = await ReplaceVariables(Regex.Replace(Regex.Replace(diffOutput, "<del.+?</del>", string.Empty), @"<\w+></\w+>", string.Empty)).Next(s => CurrentUser.IsAdmin || Request.IsRobot() ? Task.FromResult(s) : s.InjectFingerprint());
 		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;
@@ -994,6 +978,20 @@ public sealed class PostController : BaseController
 		return ResultData(null, await PostService.SaveChangesAsync() > 0, post.DisableCopy ? $"已开启【{post.Title}】这篇文章的防复制功能!" : $"已关闭【{post.Title}】这篇文章的防复制功能!");
 	}
 
+	/// <summary>
+	/// 禁用或开启NSFW
+	/// </summary>
+	/// <param name="id">文章id</param>
+	/// <returns></returns>
+	[MyAuthorize]
+	[HttpPost("post/{id}/nsfw")]
+	public async Task<ActionResult> Nsfw(int id)
+	{
+		var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
+		post.IsNsfw = !post.IsNsfw;
+		return ResultData(null, await PostService.SaveChangesAsync() > 0, post.IsNsfw ? $"已将文章【{post.Title}】标记为不安全内容!" : $"已将文章【{post.Title}】取消标记为不安全内容!");
+	}
+
 	/// <summary>
 	/// 修改分类
 	/// </summary>

+ 102 - 112
src/Masuit.MyBlogs.Core/Controllers/SearchController.cs

@@ -1,131 +1,121 @@
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
-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.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools;
-using Masuit.Tools.Core.Net;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Caching.Memory;
 using Microsoft.International.Converters.TraditionalChineseToSimplifiedConverter;
 using System.ComponentModel.DataAnnotations;
-using System.Linq.Expressions;
 using Z.EntityFramework.Plus;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 站内搜索
+/// </summary>
+public sealed class SearchController : BaseController
 {
-    /// <summary>
-    /// 站内搜索
-    /// </summary>
-    public sealed class SearchController : BaseController
-    {
-        public ISearchDetailsService SearchDetailsService { get; set; }
+	public ISearchDetailsService SearchDetailsService { get; set; }
 
-        /// <summary>
-        /// 搜索页
-        /// </summary>
-        /// <param name="postService"></param>
-        /// <param name="wd"></param>
-        /// <param name="page"></param>
-        /// <param name="size"></param>
-        /// <returns></returns>
-        [HttpGet("search/{**wd:maxlength(64)}"), HttpGet("search", Order = 2), HttpGet("s/{**wd:maxlength(64)}", Order = 3), HttpGet("s", Order = 4)]
-        public async Task<ActionResult> Search([FromServices] IPostService postService, string wd = "", [Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
-        {
-            wd = ChineseConverter.Convert(wd?.Trim() ?? "", ChineseConversionDirection.TraditionalToSimplified);
-            if (wd.Length > 128)
-            {
-                wd = wd[..128];
-            }
+	/// <summary>
+	/// 搜索页
+	/// </summary>
+	/// <param name="postService"></param>
+	/// <param name="wd"></param>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <returns></returns>
+	[HttpGet("search/{**wd:maxlength(64)}"), HttpGet("search", Order = 2), HttpGet("s/{**wd:maxlength(64)}", Order = 3), HttpGet("s", Order = 4)]
+	public async Task<ActionResult> Search([FromServices] IPostService postService, string wd = "", [Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
+	{
+		wd = ChineseConverter.Convert(wd?.Trim() ?? "", ChineseConversionDirection.TraditionalToSimplified);
+		if (wd.Length > 128)
+		{
+			wd = wd[..128];
+		}
 
-            ViewBag.PageSize = size;
-            ViewBag.Keyword = wd;
-            if (!wd.IsNullOrEmpty())
-            {
-                var posts = postService.SearchPage(page, size, wd);
-                CheckPermission(posts.Results);
-                if (posts.Results.Count > 1)
-                {
-                    ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location(), keywords: wd);
-                }
+		ViewBag.PageSize = size;
+		ViewBag.Keyword = wd;
+		if (!wd.IsNullOrEmpty())
+		{
+			var posts = postService.SearchPage(PostBaseWhere(), page, size, wd);
+			if (posts.Results.Count > 1)
+			{
+				ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location(), keywords: wd);
+			}
 
-                ViewBag.hotSearches = new List<KeywordsRank>();
-                if (posts.Total > size)
-                {
-                    ViewBag.RelateKeywords = SearchDetailsService.GetQuery(s => s.Keywords.Contains(wd) && s.Keywords != wd).Select(s => s.Keywords).GroupBy(s => s).OrderByDescending(g => g.Count()).Select(g => g.Key).Take(10).FromCache(new MemoryCacheEntryOptions()
-                    {
-                        AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
-                    }).ToList();
-                }
+			ViewBag.hotSearches = new List<KeywordsRank>();
+			if (posts.Total > size)
+			{
+				ViewBag.RelateKeywords = SearchDetailsService.GetQuery(s => s.Keywords.Contains(wd) && s.Keywords != wd).Select(s => s.Keywords).GroupBy(s => s).OrderByDescending(g => g.Count()).Select(g => g.Key).Take(10).FromCache(new MemoryCacheEntryOptions()
+				{
+					AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
+				}).ToList();
+			}
 
-                if (!HttpContext.Session.TryGetValue("search:" + wd, out _) && !Request.IsRobot())
-                {
-                    SearchDetailsService.AddEntity(new SearchDetails
-                    {
-                        Keywords = wd,
-                        SearchTime = DateTime.Now,
-                        IP = ClientIP,
-                        Elapsed = posts.Elapsed,
-                        ResultCount = posts.Total
-                    });
-                    await SearchDetailsService.SaveChangesAsync();
-                    HttpContext.Session.Set("search:" + wd, wd);
-                }
+			if (!HttpContext.Session.TryGetValue("search:" + wd, out _) && !Request.IsRobot())
+			{
+				SearchDetailsService.AddEntity(new SearchDetails
+				{
+					Keywords = wd,
+					SearchTime = DateTime.Now,
+					IP = ClientIP,
+					Elapsed = posts.Elapsed,
+					ResultCount = posts.Total
+				});
+				await SearchDetailsService.SaveChangesAsync();
+				HttpContext.Session.Set("search:" + wd, wd);
+			}
 
-                if (page == 1 && posts.Results.Count == 1)
-                {
-                    return RedirectToAction("Details", "Post", new { id = posts.Results[0].Id, kw = wd });
-                }
+			if (page == 1 && posts.Results.Count == 1)
+			{
+				return RedirectToAction("Details", "Post", new { id = posts.Results[0].Id, kw = wd });
+			}
 
-                return View(posts);
-            }
+			return View(posts);
+		}
 
-            ViewBag.hotSearches = RedisHelper.Get<List<KeywordsRank>>("SearchRank:Week").Take(10).ToList();
-            return View(new SearchResult<PostDto>());
-        }
+		ViewBag.hotSearches = RedisHelper.Get<List<KeywordsRank>>("SearchRank:Week").Take(10).ToList();
+		return View(new SearchResult<PostDto>());
+	}
 
-        /// <summary>
-        /// 关键词搜索记录
-        /// </summary>
-        /// <param name="page"></param>
-        /// <param name="size"></param>
-        /// <param name="search"></param>
-        /// <returns></returns>
-        [MyAuthorize, HttpPost("search/SearchList"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size", "search" })]
-        public ActionResult SearchList([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 200, ErrorMessage = "页大小必须在0到50之间")] int size = 15, string search = "")
-        {
-            var where = string.IsNullOrEmpty(search) ? (Expression<Func<SearchDetails, bool>>)(s => true) : s => s.Keywords.Contains(search);
-            var pages = SearchDetailsService.GetPages<DateTime, SearchDetailsDto>(page, size, where, s => s.SearchTime, false);
-            return Ok(pages);
-        }
+	/// <summary>
+	/// 关键词搜索记录
+	/// </summary>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <param name="search"></param>
+	/// <returns></returns>
+	[MyAuthorize, HttpPost("search/SearchList"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size", "search" })]
+	public ActionResult SearchList([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 200, ErrorMessage = "页大小必须在0到50之间")] int size = 15, string search = "")
+	{
+		var where = string.IsNullOrEmpty(search) ? (Expression<Func<SearchDetails, bool>>)(s => true) : s => s.Keywords.Contains(search);
+		var pages = SearchDetailsService.GetPages<DateTime, SearchDetailsDto>(page, size, where, s => s.SearchTime, false);
+		return Ok(pages);
+	}
 
-        /// <summary>
-        /// 热词
-        /// </summary>
-        /// <returns></returns>
-        [MyAuthorize, Route("search/HotKey"), ResponseCache(Duration = 600)]
-        public ActionResult HotKey()
-        {
-            return ResultData(new
-            {
-                month = RedisHelper.Get<List<KeywordsRank>>("SearchRank:Month"),
-                week = RedisHelper.Get<List<KeywordsRank>>("SearchRank:Week"),
-                today = RedisHelper.Get<List<KeywordsRank>>("SearchRank:Today")
-            });
-        }
+	/// <summary>
+	/// 热词
+	/// </summary>
+	/// <returns></returns>
+	[MyAuthorize, Route("search/HotKey"), ResponseCache(Duration = 600)]
+	public ActionResult HotKey()
+	{
+		return ResultData(new
+		{
+			month = RedisHelper.Get<List<KeywordsRank>>("SearchRank:Month"),
+			week = RedisHelper.Get<List<KeywordsRank>>("SearchRank:Week"),
+			today = RedisHelper.Get<List<KeywordsRank>>("SearchRank:Today")
+		});
+	}
 
-        /// <summary>
-        /// 删除搜索记录
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [HttpPost, MyAuthorize]
-        public async Task<ActionResult> Delete(int id)
-        {
-            bool b = await SearchDetailsService.DeleteByIdAsync(id) > 0;
-            return ResultData(null, b, b ? "删除成功!" : "删除失败!");
-        }
-    }
-}
+	/// <summary>
+	/// 删除搜索记录
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpPost, MyAuthorize]
+	public async Task<ActionResult> Delete(int id)
+	{
+		bool b = await SearchDetailsService.DeleteByIdAsync(id) > 0;
+		return ResultData(null, b, b ? "删除成功!" : "删除失败!");
+	}
+}

+ 166 - 176
src/Masuit.MyBlogs.Core/Controllers/SeminarController.cs

@@ -1,186 +1,176 @@
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
-using Masuit.MyBlogs.Core.Infrastructure.Repository;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
 using Masuit.MyBlogs.Core.Models;
-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.Tools.AspNetCore.ModelBinder;
-using Masuit.Tools.Core.Net;
-using Masuit.Tools.Linq;
-using Masuit.Tools.Systems;
 using Microsoft.AspNetCore.Mvc;
 using System.ComponentModel.DataAnnotations;
 using System.Linq.Dynamic.Core;
 using System.Runtime.InteropServices;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 专题页
+/// </summary>
+public sealed class SeminarController : BaseController
 {
-    /// <summary>
-    /// 专题页
-    /// </summary>
-    public sealed class SeminarController : BaseController
-    {
-        /// <summary>
-        /// 专题
-        /// </summary>
-        public ISeminarService SeminarService { get; set; }
-
-        /// <summary>
-        /// 文章
-        /// </summary>
-        public IPostService PostService { get; set; }
-
-        /// <summary>
-        /// 专题页
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="page"></param>
-        /// <param name="size"></param>
-        /// <param name="orderBy"></param>
-        /// <returns></returns>
-        [Route("special/{id:int}"), Route("c/{id:int}", Order = 1), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size", "orderBy" }, VaryByHeader = "Cookie")]
-        public async Task<ActionResult> Index(int id, [Optional] OrderBy? orderBy, [Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
-        {
-            var s = await SeminarService.GetByIdAsync(id) ?? throw new NotFoundException("专题未找到");
-            var h24 = DateTime.Today.AddDays(-1);
-            var posts = orderBy switch
-            {
-                OrderBy.Trending => await PostService.GetQuery(PostBaseWhere().And(p => p.Seminar.Any(x => x.Id == id))).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 => p.Seminar.Any(x => x.Id == id))).OrderBy($"{nameof(Post.IsFixedTop)} desc,{(orderBy ?? OrderBy.ModifyDate).GetDisplay()} desc").ToPagedListAsync<Post, PostDto>(page, size, MapperConfig)
-            };
-            ViewBag.Id = s.Id;
-            ViewBag.Title = s.Title;
-            ViewBag.Desc = s.Description;
-            ViewBag.SubTitle = s.SubTitle;
-            ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location(), keywords: s.Title);
-            ViewData["page"] = new Pagination(page, size, posts.TotalCount, orderBy);
-            PostService.SolvePostsCategory(posts.Data);
-            foreach (var item in posts.Data)
-            {
-                item.PostDate = item.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-                item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
-            }
-
-            return View(posts);
-        }
-
-        #region 管理端
-
-        /// <summary>
-        /// 保存专题
-        /// </summary>
-        /// <param name="seminar"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public ActionResult Save([FromBodyOrDefault] Seminar seminar)
-        {
-            if (seminar.Id > 0 ? SeminarService.Any(s => s.Id != seminar.Id && s.Title == seminar.Title) : SeminarService.Any(s => s.Title == seminar.Title))
-            {
-                return ResultData(null, false, $"{seminar.Title} 已经存在了");
-            }
-
-            var entry = SeminarService.GetById(seminar.Id);
-            bool b;
-            if (entry is null)
-            {
-                b = SeminarService.AddEntitySaved(seminar) != null;
-            }
-            else
-            {
-                entry.Description = seminar.Description;
-                entry.Title = seminar.Title;
-                entry.SubTitle = seminar.SubTitle;
-                b = SeminarService.SaveChanges() > 0;
-            }
-
-            return ResultData(null, b, b ? "保存成功" : "保存失败");
-        }
-
-        /// <summary>
-        /// 删除专题
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> Delete(int id)
-        {
-            bool b = await SeminarService.DeleteByIdAsync(id) > 0;
-            return ResultData(null, b, b ? "删除成功" : "删除失败");
-        }
-
-        /// <summary>
-        /// 获取专题详情
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> Get(int id)
-        {
-            Seminar seminar = await SeminarService.GetByIdAsync(id);
-            return ResultData(seminar.Mapper<SeminarDto>());
-        }
-
-        /// <summary>
-        /// 专题分页列表
-        /// </summary>
-        /// <param name="page"></param>
-        /// <param name="size"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public ActionResult GetPageData([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
-        {
-            var list = SeminarService.GetPages<int, SeminarDto>(page, size, s => true, s => s.Id, false);
-            return Ok(list);
-        }
-
-        /// <summary>
-        /// 获取所有专题
-        /// </summary>
-        /// <returns></returns>
-        [MyAuthorize]
-        public ActionResult GetAll()
-        {
-            var list = SeminarService.GetAll<string, SeminarDto>(s => s.Title).ToList();
-            return ResultData(list);
-        }
-
-        /// <summary>
-        /// 给专题添加文章
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="pid"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> AddPost(int id, int pid)
-        {
-            Seminar seminar = await SeminarService.GetByIdAsync(id);
-            Post post = await PostService.GetByIdAsync(pid);
-            seminar.Post.Add(post);
-            bool b = await SeminarService.SaveChangesAsync() > 0;
-            return ResultData(null, b, b ? $"已成功将【{post.Title}】添加到专题【{seminar.Title}】" : "添加失败!");
-        }
-
-        /// <summary>
-        /// 移除文章
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="pid"></param>
-        /// <returns></returns>
-        [MyAuthorize]
-        public async Task<ActionResult> RemovePost(int id, int pid)
-        {
-            Seminar seminar = await SeminarService.GetByIdAsync(id);
-            Post post = await PostService.GetByIdAsync(pid);
-
-            //bool b = await seminarPostService.DeleteEntitySavedAsync(s => s.SeminarId == id && s.PostId == pid) > 0;
-            seminar.Post.Remove(post);
-            var b = await SeminarService.SaveChangesAsync() > 0;
-            return ResultData(null, b, b ? $"已成功将【{post.Title}】从专题【{seminar.Title}】移除" : "添加失败!");
-        }
-
-        #endregion 管理端
-    }
-}
+	/// <summary>
+	/// 专题
+	/// </summary>
+	public ISeminarService SeminarService { get; set; }
+
+	/// <summary>
+	/// 文章
+	/// </summary>
+	public IPostService PostService { get; set; }
+
+	/// <summary>
+	/// 专题页
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <param name="orderBy"></param>
+	/// <returns></returns>
+	[Route("special/{id:int}"), Route("c/{id:int}", Order = 1), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "page", "size", "orderBy" }, VaryByHeader = "Cookie")]
+	public async Task<ActionResult> Index(int id, [Optional] OrderBy? orderBy, [Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
+	{
+		var s = await SeminarService.GetByIdAsync(id) ?? throw new NotFoundException("专题未找到");
+		var h24 = DateTime.Today.AddDays(-1);
+		var posts = orderBy switch
+		{
+			OrderBy.Trending => await PostService.GetQuery(PostBaseWhere().And(p => p.Seminar.Any(x => x.Id == id))).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 => p.Seminar.Any(x => x.Id == id))).OrderBy($"{nameof(Post.IsFixedTop)} desc,{(orderBy ?? OrderBy.ModifyDate).GetDisplay()} desc").ToPagedListAsync<Post, PostDto>(page, size, MapperConfig)
+		};
+		ViewBag.Id = s.Id;
+		ViewBag.Title = s.Title;
+		ViewBag.Desc = s.Description;
+		ViewBag.SubTitle = s.SubTitle;
+		ViewBag.Ads = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location(), keywords: s.Title);
+		ViewData["page"] = new Pagination(page, size, posts.TotalCount, orderBy);
+		PostService.SolvePostsCategory(posts.Data);
+		foreach (var item in posts.Data)
+		{
+			item.PostDate = item.PostDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+			item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
+		}
+
+		return View(posts);
+	}
+
+	#region 管理端
+
+	/// <summary>
+	/// 保存专题
+	/// </summary>
+	/// <param name="seminar"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public ActionResult Save([FromBodyOrDefault] Seminar seminar)
+	{
+		if (seminar.Id > 0 ? SeminarService.Any(s => s.Id != seminar.Id && s.Title == seminar.Title) : SeminarService.Any(s => s.Title == seminar.Title))
+		{
+			return ResultData(null, false, $"{seminar.Title} 已经存在了");
+		}
+
+		var entry = SeminarService.GetById(seminar.Id);
+		bool b;
+		if (entry is null)
+		{
+			b = SeminarService.AddEntitySaved(seminar) != null;
+		}
+		else
+		{
+			entry.Description = seminar.Description;
+			entry.Title = seminar.Title;
+			entry.SubTitle = seminar.SubTitle;
+			b = SeminarService.SaveChanges() > 0;
+		}
+
+		return ResultData(null, b, b ? "保存成功" : "保存失败");
+	}
+
+	/// <summary>
+	/// 删除专题
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Delete(int id)
+	{
+		bool b = await SeminarService.DeleteByIdAsync(id) > 0;
+		return ResultData(null, b, b ? "删除成功" : "删除失败");
+	}
+
+	/// <summary>
+	/// 获取专题详情
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> Get(int id)
+	{
+		Seminar seminar = await SeminarService.GetByIdAsync(id);
+		return ResultData(seminar.Mapper<SeminarDto>());
+	}
+
+	/// <summary>
+	/// 专题分页列表
+	/// </summary>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public ActionResult GetPageData([Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")] int page = 1, [Range(1, 50, ErrorMessage = "页大小必须在0到50之间")] int size = 15)
+	{
+		var list = SeminarService.GetPages<int, SeminarDto>(page, size, s => true, s => s.Id, false);
+		return Ok(list);
+	}
+
+	/// <summary>
+	/// 获取所有专题
+	/// </summary>
+	/// <returns></returns>
+	[MyAuthorize]
+	public ActionResult GetAll()
+	{
+		var list = SeminarService.GetAll<string, SeminarDto>(s => s.Title).ToList();
+		return ResultData(list);
+	}
+
+	/// <summary>
+	/// 给专题添加文章
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="pid"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> AddPost(int id, int pid)
+	{
+		Seminar seminar = await SeminarService.GetByIdAsync(id);
+		Post post = await PostService.GetByIdAsync(pid);
+		seminar.Post.Add(post);
+		bool b = await SeminarService.SaveChangesAsync() > 0;
+		return ResultData(null, b, b ? $"已成功将【{post.Title}】添加到专题【{seminar.Title}】" : "添加失败!");
+	}
+
+	/// <summary>
+	/// 移除文章
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="pid"></param>
+	/// <returns></returns>
+	[MyAuthorize]
+	public async Task<ActionResult> RemovePost(int id, int pid)
+	{
+		Seminar seminar = await SeminarService.GetByIdAsync(id);
+		Post post = await PostService.GetByIdAsync(pid);
+
+		//bool b = await seminarPostService.DeleteEntitySavedAsync(s => s.SeminarId == id && s.PostId == pid) > 0;
+		seminar.Post.Remove(post);
+		var b = await SeminarService.SaveChangesAsync() > 0;
+		return ResultData(null, b, b ? $"已成功将【{post.Title}】从专题【{seminar.Title}】移除" : "添加失败!");
+	}
+
+	#endregion 管理端
+}

+ 57 - 60
src/Masuit.MyBlogs.Core/Controllers/ShareController.cs

@@ -1,69 +1,66 @@
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.Entity;
-using Masuit.Tools.AspNetCore.ModelBinder;
+using Masuit.Tools.AspNetCore.ModelBinder;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
 using Z.EntityFramework.Plus;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 快速分享
+/// </summary>
+public sealed class ShareController : AdminController
 {
-    /// <summary>
-    /// 快速分享
-    /// </summary>
-    public sealed class ShareController : AdminController
-    {
-        /// <summary>
-        /// 快速分享
-        /// </summary>
-        public IFastShareService FastShareService { get; set; }
+	/// <summary>
+	/// 快速分享
+	/// </summary>
+	public IFastShareService FastShareService { get; set; }
 
-        /// <summary>
-        /// 快速分享
-        /// </summary>
-        /// <returns></returns>
-        public ActionResult Index()
-        {
-            var shares = FastShareService.GetAll(s => s.Sort).ToList();
-            return ResultData(shares);
-        }
+	/// <summary>
+	/// 快速分享
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult Index()
+	{
+		var shares = FastShareService.GetAll(s => s.Sort).ToList();
+		return ResultData(shares);
+	}
 
-        /// <summary>
-        /// 添加快速分享
-        /// </summary>
-        /// <param name="share"></param>
-        /// <returns></returns>
-        [HttpPost]
-        public ActionResult Add([FromBodyOrDefault] FastShare share)
-        {
-            bool b = FastShareService.AddEntitySaved(share) != null;
-            QueryCacheManager.ExpireType<FastShare>();
-            return ResultData(null, b, b ? "添加成功" : "添加失败");
-        }
+	/// <summary>
+	/// 添加快速分享
+	/// </summary>
+	/// <param name="share"></param>
+	/// <returns></returns>
+	[HttpPost]
+	public ActionResult Add([FromBodyOrDefault] FastShare share)
+	{
+		bool b = FastShareService.AddEntitySaved(share) != null;
+		QueryCacheManager.ExpireType<FastShare>();
+		return ResultData(null, b, b ? "添加成功" : "添加失败");
+	}
 
-        /// <summary>
-        /// 移除快速分享
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [HttpPost]
-        public async Task<ActionResult> Remove(int id)
-        {
-            bool b = await FastShareService.DeleteByIdAsync(id) > 0;
-            QueryCacheManager.ExpireType<FastShare>();
-            return ResultData(null, b, b ? "删除成功" : "删除失败");
-        }
+	/// <summary>
+	/// 移除快速分享
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpPost]
+	public async Task<ActionResult> Remove(int id)
+	{
+		bool b = await FastShareService.DeleteByIdAsync(id) > 0;
+		QueryCacheManager.ExpireType<FastShare>();
+		return ResultData(null, b, b ? "删除成功" : "删除失败");
+	}
 
-        /// <summary>
-        /// 更新快速分享
-        /// </summary>
-        /// <param name="model"></param>
-        /// <returns></returns>
-        [HttpPost]
-        public async Task<ActionResult> Update([FromBodyOrDefault] FastShare model)
-        {
-            var b = await FastShareService.GetQuery(s => s.Id == model.Id).ExecuteUpdateAsync(s => s.SetProperty(e => e.Title, model.Title).SetProperty(e => e.Link, model.Link).SetProperty(e => e.Sort, model.Sort)) > 0;
-            QueryCacheManager.ExpireType<FastShare>();
-            return ResultData(null, b, b ? "更新成功" : "更新失败");
-        }
-    }
-}
+	/// <summary>
+	/// 更新快速分享
+	/// </summary>
+	/// <param name="model"></param>
+	/// <returns></returns>
+	[HttpPost]
+	public async Task<ActionResult> Update([FromBodyOrDefault] FastShare model)
+	{
+		var b = await FastShareService.GetQuery(s => s.Id == model.Id).ExecuteUpdateAsync(s => s.SetProperty(e => e.Title, model.Title).SetProperty(e => e.Link, model.Link).SetProperty(e => e.Sort, model.Sort)) > 0;
+		QueryCacheManager.ExpireType<FastShare>();
+		return ResultData(null, b, b ? "更新成功" : "更新失败");
+	}
+}

+ 18 - 20
src/Masuit.MyBlogs.Core/Controllers/ShortController.cs

@@ -1,28 +1,26 @@
 using FreeRedis;
 using Masuit.MyBlogs.Core.Extensions;
 using Masuit.MyBlogs.Core.Extensions.Firewall;
-using Masuit.Tools;
 using Microsoft.AspNetCore.Mvc;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+public sealed class ShortController : Controller
 {
-    public sealed class ShortController : Controller
-    {
-        public IRedisClient RedisHelper { get; set; }
-        [HttpGet("short"), MyAuthorize, AllowAccessFirewall]
-        public IActionResult Short(string key, string url, int? expire)
-        {
-            expire ??= -1;
-            var id = string.IsNullOrEmpty(key) ? url.Crc32().FromBinary(16).ToBinary(62) : key;
-            RedisHelper.Set("shorturl:" + id, url, expire.Value);
-            return Ok(id);
-        }
+	public IRedisClient RedisHelper { get; set; }
+	[HttpGet("short"), MyAuthorize, AllowAccessFirewall]
+	public IActionResult Short(string key, string url, int? expire)
+	{
+		expire ??= -1;
+		var id = string.IsNullOrEmpty(key) ? url.Crc32().FromBinary(16).ToBinary(62) : key;
+		RedisHelper.Set("shorturl:" + id, url, expire.Value);
+		return Ok(id);
+	}
 
-        [HttpGet("{key}", Order = 100), AllowAccessFirewall]
-        public ActionResult RedirectTo(string key)
-        {
-            var url = RedisHelper.Get("shorturl:" + key) ?? throw new NotFoundException("链接未找到");
-            return Redirect(url);
-        }
-    }
+	[HttpGet("{key}", Order = 100), AllowAccessFirewall]
+	public ActionResult RedirectTo(string key)
+	{
+		var url = RedisHelper.Get("shorturl:" + key) ?? throw new NotFoundException("链接未找到");
+		return Redirect(url);
+	}
 }

+ 290 - 300
src/Masuit.MyBlogs.Core/Controllers/SubscribeController.cs

@@ -2,369 +2,359 @@
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions;
 using Masuit.MyBlogs.Core.Extensions.Firewall;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.Entity;
-using Masuit.MyBlogs.Core.Models.Enum;
-using Masuit.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools;
 using Masuit.Tools.AspNetCore.Mime;
-using Masuit.Tools.Core.Net;
-using Masuit.Tools.Linq;
 using Masuit.Tools.Models;
-using Masuit.Tools.Systems;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Net.Http.Headers;
-using System.Linq.Expressions;
 using System.Text;
 using System.Text.RegularExpressions;
 using WilderMinds.RssSyndication;
 using Z.EntityFramework.Plus;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 订阅服务
+/// </summary>
+public sealed class SubscribeController : Controller
 {
+	public IPostService PostService { get; set; }
+
+	public IAdvertisementService AdvertisementService { get; set; }
+	public IRedisClient RedisClient { get; set; }
+
 	/// <summary>
-	/// 订阅服务
+	/// RSS订阅
 	/// </summary>
-	public sealed class SubscribeController : Controller
+	/// <returns></returns>
+	[Route("/rss"), ResponseCache(Duration = 3600)]
+	public async Task<IActionResult> Rss()
 	{
-		public IPostService PostService { 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")
 		{
-			if (CommonHelper.SystemSettings.GetOrAdd("EnableRss", "true") != "true")
-			{
-				throw new NotFoundException("不允许订阅");
-			}
+			throw new NotFoundException("不允许订阅");
+		}
 
-			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 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()
 			{
-				var summary = await p.Content.GetSummary(300, 50);
-				return new Item()
+				Author = new Author
 				{
-					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
+					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 rss = feed.Serialize(new SerializeOption()
-			{
-				Encoding = Encoding.UTF8
-			});
-			return Content(rss, ContentType.Xml);
-		}
+		});
+		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 = "")
+	private void InsertAdvertisement(IList<Item> posts, int? cid = null, string keywords = "")
+	{
+		if (posts.Count > 2)
 		{
-			if (posts.Count > 2)
+			var ad = AdvertisementService.GetByWeightedPrice((AdvertiseType)(DateTime.Now.Second % 4 + 1), Request.Location(), cid, keywords);
+			if (ad is not null)
 			{
-				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()
 				{
-					posts.Insert(new Random().Next(1, posts.Count), new Item()
+					Author = new Author()
 					{
-						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 })
-					});
-				}
+						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)
+	/// <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")
 		{
-			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();
-			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);
+			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)
+		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()
 		{
-			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("专题未找到");
-			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 =>
+			AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
+		}).ToList();
+		var data = await raw.SelectAsync(async p =>
+		{
+			var summary = await p.Content.GetSummary(300, 50);
+			return new Item()
 			{
-				var summary = await p.Content.GetSummary(300, 50);
-				return new Item()
+				Author = new Author
 				{
-					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
+					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 rss = feed.Serialize(new SerializeOption()
-			{
-				Encoding = Encoding.UTF8
-			});
-			return Content(rss, ContentType.Xml);
-		}
+		});
+		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("/{id}/rss"), ResponseCache(Duration = 3600)]
-		public async Task<IActionResult> PostRss(int id)
+	/// <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")
 		{
-			if (CommonHelper.SystemSettings.GetOrAdd("EnableRss", "true") != "true")
-			{
-				throw new NotFoundException("不允许订阅");
-			}
+			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()
+		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 = post.Modifier
+					Name = p.Modifier
 				},
 				Body = summary,
 				Categories = new List<string>
 				{
-					post.Category.Path()
+					p.Category.Name
 				},
-				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(),
+				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 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);
-		}
+		});
+		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);
+	}
 
-		protected Expression<Func<Post, bool>> PostBaseWhere()
+	/// <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")
 		{
-			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));
+			throw new NotFoundException("不允许订阅");
 		}
 
-		private void CheckPermission(Post post)
+		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()
 		{
-			var ipLocation = Request.Location();
-			var location = ipLocation + ipLocation.Coodinate + "|" + Request.Headers[HeaderNames.Referer] + "|" + Request.Headers[HeaderNames.UserAgent];
-			switch (post.LimitMode)
+			Author = new Author
 			{
-				case RegionLimitMode.OnlyForSearchEngine:
-					Disallow(post);
-					break;
+				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);
+	}
 
-				case RegionLimitMode.AllowRegion:
-					if (!Regex.IsMatch(location, post.Regions, RegexOptions.IgnoreCase) && !Request.IsRobot())
-					{
-						Disallow(post);
-					}
+	private 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));
+	}
 
-					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.ForbidRegion:
-					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.AllowRegionExceptForbidRegion:
-					if (Regex.IsMatch(location, post.ExceptRegions, RegexOptions.IgnoreCase))
-					{
-						Disallow(post);
-					}
+			case RegionLimitMode.ForbidRegion:
+				if (Regex.IsMatch(location, post.Regions, RegexOptions.IgnoreCase) && !Request.IsRobot())
+				{
+					Disallow(post);
+				}
 
-					goto case RegionLimitMode.AllowRegion;
-				case RegionLimitMode.ForbidRegionExceptAllowRegion:
-					if (Regex.IsMatch(location, post.ExceptRegions, RegexOptions.IgnoreCase))
-					{
-						break;
-					}
+				break;
 
-					goto case RegionLimitMode.ForbidRegion;
-			}
+			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;
 		}
+	}
 
-		private void Disallow(Post post)
+	private void Disallow(Post post)
+	{
+		RedisClient.IncrBy("interceptCount", 1);
+		RedisClient.LPush("intercept", new IpIntercepter()
 		{
-			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
 			{
-				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("文章未找到");
-		}
+				Request.Protocol,
+				Request.Headers
+			}.ToJsonString()
+		});
+		throw new NotFoundException("文章未找到");
 	}
-}
+}

+ 330 - 336
src/Masuit.MyBlogs.Core/Controllers/SystemController.cs

@@ -2,16 +2,11 @@
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Common.Mails;
 using Masuit.MyBlogs.Core.Extensions.Firewall;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.Entity;
-using Masuit.MyBlogs.Core.Models.Enum;
-using Masuit.Tools;
 using Masuit.Tools.AspNetCore.ModelBinder;
 using Masuit.Tools.DateTimeExt;
 using Masuit.Tools.Hardware;
 using Masuit.Tools.Logging;
 using Masuit.Tools.Models;
-using Masuit.Tools.Systems;
 using Microsoft.AspNetCore.Mvc;
 using Newtonsoft.Json.Linq;
 using System.ComponentModel.DataAnnotations;
@@ -21,394 +16,393 @@ using System.Net.Sockets;
 using System.Text;
 using PerformanceCounter = Masuit.MyBlogs.Core.Common.PerformanceCounter;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 系统设置
+/// </summary>
+public sealed class SystemController : AdminController
 {
 	/// <summary>
 	/// 系统设置
 	/// </summary>
-	public sealed class SystemController : AdminController
-	{
-		/// <summary>
-		/// 系统设置
-		/// </summary>
-		public ISystemSettingService SystemSettingService { get; set; }
+	public ISystemSettingService SystemSettingService { get; set; }
 
-		public IPerfCounter PerfCounter { get; set; }
+	public IPerfCounter PerfCounter { get; set; }
 
-		public ActionResult GetServers()
-		{
-			var servers = PerfCounter.CreateDataSource().Select(c => c.ServerIP).Distinct().ToArray();
-			return Ok(servers);
-		}
+	public ActionResult GetServers()
+	{
+		var servers = PerfCounter.CreateDataSource().Select(c => c.ServerIP).Distinct().ToArray();
+		return Ok(servers);
+	}
 
-		/// <summary>
-		/// 获取历史性能计数器
-		/// </summary>
-		/// <returns></returns>
-		public IActionResult GetCounterHistory(string ip = null)
+	/// <summary>
+	/// 获取历史性能计数器
+	/// </summary>
+	/// <returns></returns>
+	public IActionResult GetCounterHistory(string ip = null)
+	{
+		ip = ip.IfNullOrEmpty(() => SystemInfo.GetLocalUsedIP(AddressFamily.InterNetwork).ToString());
+		var time = DateTime.Now.AddDays(-15).GetTotalMilliseconds();
+		var counters = PerfCounter.CreateDataSource().Where(c => c.ServerIP == ip && c.Time >= time);
+		var count = counters.Count();
+		var ticks = count switch
+		{
+			<= 5000 => count,
+			> 5000 and <= 10000 => 3,
+			> 10000 and <= 20000 => 6,
+			> 20000 and <= 50000 => 12,
+			> 50000 and <= 100000 => 24,
+			> 100000 and <= 200000 => 48,
+			_ => 72
+		} * 10000;
+
+		var list = count < 5000 ? counters.OrderBy(c => c.Time).ToList() : counters.GroupBy(c => c.Time / ticks).Select(g => new PerformanceCounter
 		{
-			ip = ip.IfNullOrEmpty(() => SystemInfo.GetLocalUsedIP(AddressFamily.InterNetwork).ToString());
-			var time = DateTime.Now.AddDays(-15).GetTotalMilliseconds();
-			var counters = PerfCounter.CreateDataSource().Where(c => c.ServerIP == ip && c.Time >= time);
-			var count = counters.Count();
-			var ticks = count switch
+			Time = g.Key * ticks,
+			CpuLoad = g.Average(c => c.CpuLoad),
+			DiskRead = g.Average(c => c.DiskRead),
+			DiskWrite = g.Average(c => c.DiskWrite),
+			Download = g.Average(c => c.Download),
+			Upload = g.Average(c => c.Upload),
+			MemoryUsage = g.Average(c => c.MemoryUsage)
+		}).OrderBy(c => c.Time).ToList();
+		return Ok(new
+		{
+			cpu = list.Select(c => new[]
 			{
-				<= 5000 => count,
-				> 5000 and <= 10000 => 3,
-				> 10000 and <= 20000 => 6,
-				> 20000 and <= 50000 => 12,
-				> 50000 and <= 100000 => 24,
-				> 100000 and <= 200000 => 48,
-				_ => 72
-			} * 10000;
-
-			var list = count < 5000 ? counters.OrderBy(c => c.Time).ToList() : counters.GroupBy(c => c.Time / ticks).Select(g => new PerformanceCounter
+				c.Time,
+				c.CpuLoad.ToDecimal(2)
+			}),
+			mem = list.Select(c => new[]
 			{
-				Time = g.Key * ticks,
-				CpuLoad = g.Average(c => c.CpuLoad),
-				DiskRead = g.Average(c => c.DiskRead),
-				DiskWrite = g.Average(c => c.DiskWrite),
-				Download = g.Average(c => c.Download),
-				Upload = g.Average(c => c.Upload),
-				MemoryUsage = g.Average(c => c.MemoryUsage)
-			}).OrderBy(c => c.Time).ToList();
-			return Ok(new
+				c.Time,
+				c.MemoryUsage.ToDecimal(2)
+			}),
+			read = list.Select(c => new[]
 			{
-				cpu = list.Select(c => new[]
-				{
-					c.Time,
-					c.CpuLoad.ToDecimal(2)
-				}),
-				mem = list.Select(c => new[]
-				{
-					c.Time,
-					c.MemoryUsage.ToDecimal(2)
-				}),
-				read = list.Select(c => new[]
-				{
-					c.Time,
-					c.DiskRead.ToDecimal(2)
-				}),
-				write = list.Select(c => new[]
-				{
-					c.Time,
-					c.DiskWrite.ToDecimal(2)
-				}),
-				down = list.Select(c => new[]
-				{
-					c.Time,
-					c.Download.ToDecimal(2)
-				}),
-				up = list.Select(c => new[]
-				{
-					c.Time,
-					c.Upload.ToDecimal(2)
-				})
-			});
-		}
-
-		/// <summary>
-		/// 获取设置信息
-		/// </summary>
-		/// <returns></returns>
-		public ActionResult GetSettings()
-		{
-			var list = SystemSettingService.GetAll().Select(s => new
+				c.Time,
+				c.DiskRead.ToDecimal(2)
+			}),
+			write = list.Select(c => new[]
 			{
-				s.Name,
-				s.Value
-			}).ToList();
-			return ResultData(list);
-		}
+				c.Time,
+				c.DiskWrite.ToDecimal(2)
+			}),
+			down = list.Select(c => new[]
+			{
+				c.Time,
+				c.Download.ToDecimal(2)
+			}),
+			up = list.Select(c => new[]
+			{
+				c.Time,
+				c.Upload.ToDecimal(2)
+			})
+		});
+	}
 
-		/// <summary>
-		/// 保存设置
-		/// </summary>
-		/// <param name="settings"></param>
-		/// <returns></returns>
-		public async Task<ActionResult> Save([FromBody] List<SystemSetting> settings)
+	/// <summary>
+	/// 获取设置信息
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult GetSettings()
+	{
+		var list = SystemSettingService.GetAll().Select(s => new
 		{
-			var b = await SystemSettingService.AddOrUpdateSavedAsync(s => s.Name, settings) > 0;
-			var dic = settings.ToDictionary(s => s.Name, s => s.Value); //同步设置
-			foreach (var (key, value) in dic)
-			{
-				CommonHelper.SystemSettings[key] = value;
-			}
+			s.Name,
+			s.Value
+		}).ToList();
+		return ResultData(list);
+	}
 
-			return ResultData(null, b, b ? "设置保存成功!" : "设置保存失败!");
+	/// <summary>
+	/// 保存设置
+	/// </summary>
+	/// <param name="settings"></param>
+	/// <returns></returns>
+	public async Task<ActionResult> Save([FromBody] List<SystemSetting> settings)
+	{
+		var b = await SystemSettingService.AddOrUpdateSavedAsync(s => s.Name, settings) > 0;
+		var dic = settings.ToDictionary(s => s.Name, s => s.Value); //同步设置
+		foreach (var (key, value) in dic)
+		{
+			CommonHelper.SystemSettings[key] = value;
 		}
 
-		/// <summary>
-		/// 获取状态
-		/// </summary>
-		/// <returns></returns>
-		public ActionResult GetStatus()
+		return ResultData(null, b, b ? "设置保存成功!" : "设置保存失败!");
+	}
+
+	/// <summary>
+	/// 获取状态
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult GetStatus()
+	{
+		Array array = Enum.GetValues(typeof(Status));
+		var list = new List<object>();
+		foreach (Enum e in array)
 		{
-			Array array = Enum.GetValues(typeof(Status));
-			var list = new List<object>();
-			foreach (Enum e in array)
+			list.Add(new
 			{
-				list.Add(new
-				{
-					e,
-					name = e.GetDisplay()
-				});
-			}
-
-			return ResultData(list);
+				e,
+				name = e.GetDisplay()
+			});
 		}
 
-		/// <summary>
-		/// 邮件测试
-		/// </summary>
-		/// <param name="smtp"></param>
-		/// <param name="user"></param>
-		/// <param name="pwd"></param>
-		/// <param name="port"></param>
-		/// <param name="to"></param>
-		/// <returns></returns>
-		public ActionResult MailTest([FromBodyOrDefault] string smtp, [FromBodyOrDefault] string user, [FromBodyOrDefault] string pwd, [FromBodyOrDefault] int port, [FromBodyOrDefault] string to, [FromBodyOrDefault] bool ssl)
+		return ResultData(list);
+	}
+
+	/// <summary>
+	/// 邮件测试
+	/// </summary>
+	/// <param name="smtp"></param>
+	/// <param name="user"></param>
+	/// <param name="pwd"></param>
+	/// <param name="port"></param>
+	/// <param name="to"></param>
+	/// <returns></returns>
+	public ActionResult MailTest([FromBodyOrDefault] string smtp, [FromBodyOrDefault] string user, [FromBodyOrDefault] string pwd, [FromBodyOrDefault] int port, [FromBodyOrDefault] string to, [FromBodyOrDefault] bool ssl)
+	{
+		try
 		{
-			try
+			new Email()
 			{
-				new Email()
-				{
-					EnableSsl = ssl,
-					Body = "发送成功,网站邮件配置正确!",
-					SmtpServer = smtp,
-					Username = user,
-					Password = pwd,
-					SmtpPort = port,
-					Subject = "网站测试邮件",
-					Tos = to
-				}.Send();
-				return ResultData(null, true, "测试邮件发送成功,网站邮件配置正确!");
-			}
-			catch (Exception e)
-			{
-				return ResultData(null, false, "邮件配置测试失败!错误信息:\r\n" + e.Message + "\r\n\r\n详细堆栈跟踪:\r\n" + e.StackTrace);
-			}
+				EnableSsl = ssl,
+				Body = "发送成功,网站邮件配置正确!",
+				SmtpServer = smtp,
+				Username = user,
+				Password = pwd,
+				SmtpPort = port,
+				Subject = "网站测试邮件",
+				Tos = to
+			}.Send();
+			return ResultData(null, true, "测试邮件发送成功,网站邮件配置正确!");
 		}
-
-		/// <summary>
-		/// 发送一封系统邮件
-		/// </summary>
-		/// <param name="tos"></param>
-		/// <param name="title"></param>
-		/// <param name="content"></param>
-		/// <returns></returns>
-		public ActionResult SendMail([Required(ErrorMessage = "收件人不能为空"), FromBodyOrDefault] string tos, [Required(ErrorMessage = "邮件标题不能为空"), FromBodyOrDefault] string title, [Required(ErrorMessage = "邮件内容不能为空"), FromBodyOrDefault] string content)
+		catch (Exception e)
 		{
-			BackgroundJob.Enqueue(() => CommonHelper.SendMail(title, content + "<p style=\"color: red\">本邮件由系统自动发出,请勿回复本邮件!</p>", tos, "127.0.0.1"));
-			return Ok();
+			return ResultData(null, false, "邮件配置测试失败!错误信息:\r\n" + e.Message + "\r\n\r\n详细堆栈跟踪:\r\n" + e.StackTrace);
 		}
+	}
 
-		/// <summary>
-		/// 路径测试
-		/// </summary>
-		/// <param name="path"></param>
-		/// <returns></returns>
-		public ActionResult PathTest([FromBodyOrDefault] string path)
-		{
-			if (!(path.EndsWith("/") || path.EndsWith("\\")))
-			{
-				return ResultData(null, false, "路径不存在");
-			}
-
-			if (path.Equals("/") || path.Equals("\\"))
-			{
-				return ResultData(null, true, "根路径正确");
-			}
+	/// <summary>
+	/// 发送一封系统邮件
+	/// </summary>
+	/// <param name="tos"></param>
+	/// <param name="title"></param>
+	/// <param name="content"></param>
+	/// <returns></returns>
+	public ActionResult SendMail([Required(ErrorMessage = "收件人不能为空"), FromBodyOrDefault] string tos, [Required(ErrorMessage = "邮件标题不能为空"), FromBodyOrDefault] string title, [Required(ErrorMessage = "邮件内容不能为空"), FromBodyOrDefault] string content)
+	{
+		BackgroundJob.Enqueue(() => CommonHelper.SendMail(title, content + "<p style=\"color: red\">本邮件由系统自动发出,请勿回复本邮件!</p>", tos, "127.0.0.1"));
+		return Ok();
+	}
 
-			try
-			{
-				bool b = Directory.Exists(path);
-				return ResultData(null, b, b ? "根路径正确" : "路径不存在");
-			}
-			catch (Exception e)
-			{
-				LogManager.Error(GetType(), e.Demystify());
-				return ResultData(null, false, "路径格式不正确!错误信息:\r\n" + e.Message + "\r\n\r\n详细堆栈跟踪:\r\n" + e.StackTrace);
-			}
+	/// <summary>
+	/// 路径测试
+	/// </summary>
+	/// <param name="path"></param>
+	/// <returns></returns>
+	public ActionResult PathTest([FromBodyOrDefault] string path)
+	{
+		if (!(path.EndsWith("/") || path.EndsWith("\\")))
+		{
+			return ResultData(null, false, "路径不存在");
 		}
 
-		/// <summary>
-		/// 发件箱记录
-		/// </summary>
-		/// <returns></returns>
-		public ActionResult<List<JObject>> SendBox()
+		if (path.Equals("/") || path.Equals("\\"))
 		{
-			return RedisHelper.SUnion(RedisHelper.Keys("Email:*")).Select(JObject.Parse).OrderByDescending(o => o["time"]).ToList();
+			return ResultData(null, true, "根路径正确");
 		}
 
-		public ActionResult BounceEmail([FromServices] IMailSender mailSender, [FromBodyOrDefault] string email)
+		try
 		{
-			var msg = mailSender.AddRecipient(email);
-			return Ok(new
-			{
-				msg
-			});
+			bool b = Directory.Exists(path);
+			return ResultData(null, b, b ? "根路径正确" : "路径不存在");
 		}
-
-		#region 网站防火墙
-
-		/// <summary>
-		/// 获取全局IP黑名单
-		/// </summary>
-		/// <returns></returns>
-		public ActionResult IpBlackList()
+		catch (Exception e)
 		{
-			return ResultData(CommonHelper.DenyIP);
+			LogManager.Error(GetType(), e.Demystify());
+			return ResultData(null, false, "路径格式不正确!错误信息:\r\n" + e.Message + "\r\n\r\n详细堆栈跟踪:\r\n" + e.StackTrace);
 		}
+	}
+
+	/// <summary>
+	/// 发件箱记录
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult<List<JObject>> SendBox()
+	{
+		return RedisHelper.SUnion(RedisHelper.Keys("Email:*")).Select(JObject.Parse).OrderByDescending(o => o["time"]).ToList();
+	}
 
-		/// <summary>
-		/// 获取IP地址段黑名单
-		/// </summary>
-		/// <returns></returns>
-		public ActionResult GetIPRangeBlackList()
+	public ActionResult BounceEmail([FromServices] IMailSender mailSender, [FromBodyOrDefault] string email)
+	{
+		var msg = mailSender.AddRecipient(email);
+		return Ok(new
 		{
-			return ResultData(new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "DenyIPRange.txt")).ShareReadWrite().ReadAllText(Encoding.UTF8));
-		}
+			msg
+		});
+	}
 
-		/// <summary>
-		/// 设置IP地址段黑名单
-		/// </summary>
-		/// <returns></returns>
-		public async Task<ActionResult> SetIPRangeBlackList([FromBodyOrDefault] string content)
+	#region 网站防火墙
+
+	/// <summary>
+	/// 获取全局IP黑名单
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult IpBlackList()
+	{
+		return ResultData(CommonHelper.DenyIP);
+	}
+
+	/// <summary>
+	/// 获取IP地址段黑名单
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult GetIPRangeBlackList()
+	{
+		return ResultData(new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "DenyIPRange.txt")).ShareReadWrite().ReadAllText(Encoding.UTF8));
+	}
+
+	/// <summary>
+	/// 设置IP地址段黑名单
+	/// </summary>
+	/// <returns></returns>
+	public async Task<ActionResult> SetIPRangeBlackList([FromBodyOrDefault] string content)
+	{
+		var file = new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "DenyIPRange.txt")).ShareReadWrite();
+		await file.WriteAllTextAsync(content, Encoding.UTF8, false);
+		CommonHelper.DenyIPRange.Clear();
+		var lines = (await file.ReadAllLinesAsync(Encoding.UTF8)).Where(s => s.Split(' ').Length > 2);
+		foreach (var line in lines)
 		{
-			var file = new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "DenyIPRange.txt")).ShareReadWrite();
-			await file.WriteAllTextAsync(content, Encoding.UTF8, false);
-			CommonHelper.DenyIPRange.Clear();
-			var lines = (await file.ReadAllLinesAsync(Encoding.UTF8)).Where(s => s.Split(' ').Length > 2);
-			foreach (var line in lines)
+			try
+			{
+				var strs = line.Split(' ');
+				CommonHelper.DenyIPRange[strs[0]] = strs[1];
+			}
+			catch (IndexOutOfRangeException)
 			{
-				try
-				{
-					var strs = line.Split(' ');
-					CommonHelper.DenyIPRange[strs[0]] = strs[1];
-				}
-				catch (IndexOutOfRangeException)
-				{
-				}
 			}
-
-			return ResultData(null);
 		}
 
-		/// <summary>
-		/// 全局IP白名单
-		/// </summary>
-		/// <returns></returns>
-		public ActionResult IpWhiteList()
-		{
-			return ResultData(new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "whitelist.txt")).ShareReadWrite().ReadAllText(Encoding.UTF8));
-		}
+		return ResultData(null);
+	}
 
-		/// <summary>
-		/// 设置IP黑名单
-		/// </summary>
-		/// <param name="content"></param>
-		/// <returns></returns>
-		public async Task<ActionResult> SetIpBlackList([FromBodyOrDefault] string content)
-		{
-			CommonHelper.DenyIP = content + "";
-			await new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "denyip.txt")).ShareReadWrite().WriteAllTextAsync(CommonHelper.DenyIP, Encoding.UTF8);
-			return ResultData(null);
-		}
+	/// <summary>
+	/// 全局IP白名单
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult IpWhiteList()
+	{
+		return ResultData(new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "whitelist.txt")).ShareReadWrite().ReadAllText(Encoding.UTF8));
+	}
 
-		/// <summary>
-		/// 设置IP白名单
-		/// </summary>
-		/// <param name="content"></param>
-		/// <returns></returns>
-		public async Task<ActionResult> SetIpWhiteList([FromBodyOrDefault] string content)
-		{
-			await new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "whitelist.txt")).ShareReadWrite().WriteAllTextAsync(content, Encoding.UTF8);
-			CommonHelper.IPWhiteList.Add(content);
-			return ResultData(null);
-		}
+	/// <summary>
+	/// 设置IP黑名单
+	/// </summary>
+	/// <param name="content"></param>
+	/// <returns></returns>
+	public async Task<ActionResult> SetIpBlackList([FromBodyOrDefault] string content)
+	{
+		CommonHelper.DenyIP = content + "";
+		await new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "denyip.txt")).ShareReadWrite().WriteAllTextAsync(CommonHelper.DenyIP, Encoding.UTF8);
+		return ResultData(null);
+	}
 
-		/// <summary>
-		/// 获取拦截日志
-		/// </summary>
-		/// <returns></returns>
-		public ActionResult InterceptLog()
+	/// <summary>
+	/// 设置IP白名单
+	/// </summary>
+	/// <param name="content"></param>
+	/// <returns></returns>
+	public async Task<ActionResult> SetIpWhiteList([FromBodyOrDefault] string content)
+	{
+		await new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "whitelist.txt")).ShareReadWrite().WriteAllTextAsync(content, Encoding.UTF8);
+		CommonHelper.IPWhiteList.Add(content);
+		return ResultData(null);
+	}
+
+	/// <summary>
+	/// 获取拦截日志
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult InterceptLog()
+	{
+		var list = RedisHelper.LRange<IpIntercepter>("intercept", 0, -1);
+		return ResultData(new
 		{
-			var list = RedisHelper.LRange<IpIntercepter>("intercept", 0, -1);
-			return ResultData(new
+			interceptCount = RedisHelper.Get("interceptCount"),
+			list,
+			ranking = list.GroupBy(i => i.IP).Where(g => g.Count() > 1).Select(g =>
 			{
-				interceptCount = RedisHelper.Get("interceptCount"),
-				list,
-				ranking = list.GroupBy(i => i.IP).Where(g => g.Count() > 1).Select(g =>
+				var start = g.Min(t => t.Time);
+				var end = g.Max(t => t.Time);
+				return new
 				{
-					var start = g.Min(t => t.Time);
-					var end = g.Max(t => t.Time);
-					return new
-					{
-						g.Key,
-						g.First().Address,
-						Start = start,
-						End = end,
-						Continue = start.GetDiffTime(end),
-						Count = g.Count()
-					};
-				}).OrderByDescending(a => a.Count).Take(30)
-			});
-		}
+					g.Key,
+					g.First().Address,
+					Start = start,
+					End = end,
+					Continue = start.GetDiffTime(end),
+					Count = g.Count()
+				};
+			}).OrderByDescending(a => a.Count).Take(30)
+		});
+	}
 
-		/// <summary>
-		/// 清除拦截日志
-		/// </summary>
-		/// <returns></returns>
-		public ActionResult ClearInterceptLog()
-		{
-			bool b = RedisHelper.Del("intercept") > 0;
-			return ResultData(null, b, b ? "拦截日志清除成功!" : "拦截日志清除失败!");
-		}
+	/// <summary>
+	/// 清除拦截日志
+	/// </summary>
+	/// <returns></returns>
+	public ActionResult ClearInterceptLog()
+	{
+		bool b = RedisHelper.Del("intercept") > 0;
+		return ResultData(null, b, b ? "拦截日志清除成功!" : "拦截日志清除失败!");
+	}
 
-		/// <summary>
-		/// 将IP添加到白名单
-		/// </summary>
-		/// <param name="ip"></param>
-		/// <returns></returns>
-		public async Task<ActionResult> AddToWhiteList([FromBodyOrDefault] string ip)
+	/// <summary>
+	/// 将IP添加到白名单
+	/// </summary>
+	/// <param name="ip"></param>
+	/// <returns></returns>
+	public async Task<ActionResult> AddToWhiteList([FromBodyOrDefault] string ip)
+	{
+		if (!ip.MatchInetAddress())
 		{
-			if (!ip.MatchInetAddress())
-			{
-				return ResultData(null, false);
-			}
-
-			var fs = new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "whitelist.txt")).ShareReadWrite();
-			string ips = await fs.ReadAllTextAsync(Encoding.UTF8, false);
-			var list = ips.Split(',').Where(s => !string.IsNullOrEmpty(s)).ToList();
-			list.Add(ip);
-			await fs.WriteAllTextAsync(string.Join(",", list.Distinct()), Encoding.UTF8);
-			CommonHelper.IPWhiteList = list;
-			return ResultData(null);
+			return ResultData(null, false);
 		}
 
-		/// <summary>
-		/// 将IP添加到黑名单
-		/// </summary>
-		/// <param name="firewallRepoter"></param>
-		/// <param name="ip"></param>
-		/// <returns></returns>
-		public async Task<ActionResult> AddToBlackList([FromServices] IFirewallRepoter firewallRepoter, [FromBodyOrDefault] string ip)
-		{
-			if (!ip.MatchInetAddress())
-			{
-				return ResultData(null, false);
-			}
+		var fs = new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "whitelist.txt")).ShareReadWrite();
+		string ips = await fs.ReadAllTextAsync(Encoding.UTF8, false);
+		var list = ips.Split(',').Where(s => !string.IsNullOrEmpty(s)).ToList();
+		list.Add(ip);
+		await fs.WriteAllTextAsync(string.Join(",", list.Distinct()), Encoding.UTF8);
+		CommonHelper.IPWhiteList = list;
+		return ResultData(null);
+	}
 
-			CommonHelper.DenyIP += "," + ip;
-			var basedir = AppDomain.CurrentDomain.BaseDirectory;
-			await new FileInfo(Path.Combine(basedir, "App_Data", "denyip.txt")).ShareReadWrite().WriteAllTextAsync(CommonHelper.DenyIP, Encoding.UTF8);
-			CommonHelper.IPWhiteList.Remove(ip);
-			await new FileInfo(Path.Combine(basedir, "App_Data", "whitelist.txt")).ShareReadWrite().WriteAllTextAsync(string.Join(",", CommonHelper.IPWhiteList.Distinct()), Encoding.UTF8);
-			await firewallRepoter.ReportAsync(IPAddress.Parse(ip));
-			return ResultData(null);
+	/// <summary>
+	/// 将IP添加到黑名单
+	/// </summary>
+	/// <param name="firewallRepoter"></param>
+	/// <param name="ip"></param>
+	/// <returns></returns>
+	public async Task<ActionResult> AddToBlackList([FromServices] IFirewallRepoter firewallRepoter, [FromBodyOrDefault] string ip)
+	{
+		if (!ip.MatchInetAddress())
+		{
+			return ResultData(null, false);
 		}
 
-		#endregion 网站防火墙
+		CommonHelper.DenyIP += "," + ip;
+		var basedir = AppDomain.CurrentDomain.BaseDirectory;
+		await new FileInfo(Path.Combine(basedir, "App_Data", "denyip.txt")).ShareReadWrite().WriteAllTextAsync(CommonHelper.DenyIP, Encoding.UTF8);
+		CommonHelper.IPWhiteList.Remove(ip);
+		await new FileInfo(Path.Combine(basedir, "App_Data", "whitelist.txt")).ShareReadWrite().WriteAllTextAsync(string.Join(",", CommonHelper.IPWhiteList.Distinct()), Encoding.UTF8);
+		await firewallRepoter.ReportAsync(IPAddress.Parse(ip));
+		return ResultData(null);
 	}
-}
+
+	#endregion 网站防火墙
+}

+ 157 - 160
src/Masuit.MyBlogs.Core/Controllers/ToolsController.cs

@@ -1,8 +1,6 @@
 using DnsClient;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Configs;
-using Masuit.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools;
 using Masuit.Tools.AspNetCore.Mime;
 using Masuit.Tools.Core.Validator;
 using Masuit.Tools.Models;
@@ -15,171 +13,170 @@ using Polly;
 using System.Net;
 using TimeZoneConverter;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 黑科技
+/// </summary>
+[Route("tools")]
+public sealed class ToolsController : BaseController
 {
-    /// <summary>
-    /// 黑科技
-    /// </summary>
-    [Route("tools")]
-    public sealed class ToolsController : BaseController
-    {
-        private readonly HttpClient _httpClient;
-
-        public ToolsController(IHttpClientFactory httpClientFactory)
-        {
-            _httpClient = httpClientFactory.CreateClient();
-        }
-
-        /// <summary>
-        /// 获取ip地址详细信息
-        /// </summary>
-        /// <param name="ip"></param>
-        /// <returns></returns>
-        [Route("ip"), Route("ip/{ip?}", Order = 1), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "ip" })]
-        public async Task<ActionResult> GetIpInfo([IsIPAddress] string ip)
-        {
-            if (string.IsNullOrEmpty(ip))
-            {
-                ip = ClientIP;
-            }
-
-            if (ip.IsPrivateIP())
-            {
-                return Ok("内网IP");
-            }
-
-            var ipAddress = IPAddress.Parse(ip);
-            ViewBag.IP = ip;
-            var loc = ipAddress.GetIPLocation();
-            var asn = ipAddress.GetIPAsn();
-            var nslookup = new LookupClient();
-            using var cts = new CancellationTokenSource(2000);
-            var domain = await nslookup.QueryReverseAsync(ipAddress, cts.Token).ContinueWith(t => t.IsCompletedSuccessfully ? t.Result.Answers.Select(r => r.ToString()).Join("; ") : "无");
-            var address = new IpInfo
-            {
-                Location = loc.Coodinate,
-                Address = loc.Address,
-                Address2 = loc.Address2,
-                Network = new NetworkInfo
-                {
-                    Asn = asn.AutonomousSystemNumber,
-                    Router = asn.Network + "",
-                    Organization = loc.ISP
-                },
-                TimeZone = loc.Coodinate.TimeZone + $"  UTC{TZConvert.GetTimeZoneInfo(loc.Coodinate.TimeZone ?? "Asia/Shanghai").BaseUtcOffset.Hours:+#;-#;0}",
-                IsProxy = loc.Network.Contains(new[] { "cloud", "Compute", "Serv", "Tech", "Solution", "Host", "云", "Datacenter", "Data Center", "Business", "ASN" }) || domain.Length > 1 || await ipAddress.IsProxy(cts.Token),
-                Domain = domain
-            };
-            if (Request.Method.Equals(HttpMethods.Get) || (Request.Headers[HeaderNames.Accept] + "").StartsWith(ContentType.Json))
-            {
-                return View(address);
-            }
-
-            return Json(address);
-        }
-
-        /// <summary>
-        /// 根据经纬度获取详细地理信息
-        /// </summary>
-        /// <param name="lat"></param>
-        /// <param name="lng"></param>
-        /// <returns></returns>
-        [HttpGet("pos"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "lat", "lng" })]
-        public async Task<ActionResult> Position(string lat, string lng)
-        {
-            if (string.IsNullOrEmpty(lat) || string.IsNullOrEmpty(lng))
-            {
-                var ip = ClientIP;
+	private readonly HttpClient _httpClient;
+
+	public ToolsController(IHttpClientFactory httpClientFactory)
+	{
+		_httpClient = httpClientFactory.CreateClient();
+	}
+
+	/// <summary>
+	/// 获取ip地址详细信息
+	/// </summary>
+	/// <param name="ip"></param>
+	/// <returns></returns>
+	[Route("ip"), Route("ip/{ip?}", Order = 1), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "ip" })]
+	public async Task<ActionResult> GetIpInfo([IsIPAddress] string ip)
+	{
+		if (string.IsNullOrEmpty(ip))
+		{
+			ip = ClientIP;
+		}
+
+		if (ip.IsPrivateIP())
+		{
+			return Ok("内网IP");
+		}
+
+		var ipAddress = IPAddress.Parse(ip);
+		ViewBag.IP = ip;
+		var loc = ipAddress.GetIPLocation();
+		var asn = ipAddress.GetIPAsn();
+		var nslookup = new LookupClient();
+		using var cts = new CancellationTokenSource(2000);
+		var domain = await nslookup.QueryReverseAsync(ipAddress, cts.Token).ContinueWith(t => t.IsCompletedSuccessfully ? t.Result.Answers.Select(r => r.ToString()).Join("; ") : "无");
+		var address = new IpInfo
+		{
+			Location = loc.Coodinate,
+			Address = loc.Address,
+			Address2 = loc.Address2,
+			Network = new NetworkInfo
+			{
+				Asn = asn.AutonomousSystemNumber,
+				Router = asn.Network + "",
+				Organization = loc.ISP
+			},
+			TimeZone = loc.Coodinate.TimeZone + $"  UTC{TZConvert.GetTimeZoneInfo(loc.Coodinate.TimeZone ?? "Asia/Shanghai").BaseUtcOffset.Hours:+#;-#;0}",
+			IsProxy = loc.Network.Contains(new[] { "cloud", "Compute", "Serv", "Tech", "Solution", "Host", "云", "Datacenter", "Data Center", "Business", "ASN" }) || domain.Length > 1 || await ipAddress.IsProxy(cts.Token),
+			Domain = domain
+		};
+		if (Request.Method.Equals(HttpMethods.Get) || (Request.Headers[HeaderNames.Accept] + "").StartsWith(ContentType.Json))
+		{
+			return View(address);
+		}
+
+		return Json(address);
+	}
+
+	/// <summary>
+	/// 根据经纬度获取详细地理信息
+	/// </summary>
+	/// <param name="lat"></param>
+	/// <param name="lng"></param>
+	/// <returns></returns>
+	[HttpGet("pos"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "lat", "lng" })]
+	public async Task<ActionResult> Position(string lat, string lng)
+	{
+		if (string.IsNullOrEmpty(lat) || string.IsNullOrEmpty(lng))
+		{
+			var ip = ClientIP;
 #if DEBUG
                 var r = new Random();
                 ip = $"{r.Next(210)}.{r.Next(255)}.{r.Next(255)}.{r.Next(255)}";
 #endif
-                var location = Policy<CityResponse>.Handle<AddressNotFoundException>().Fallback(() => new CityResponse()).Execute(() => CommonHelper.MaxmindReader.City(ip));
-                var address = new PhysicsAddress()
-                {
-                    Status = 0,
-                    AddressResult = new AddressResult()
-                    {
-                        FormattedAddress = IPAddress.Parse(ip).GetIPLocation().Address,
-                        Location = new Location()
-                        {
-                            Lng = (decimal)location.Location.Longitude.GetValueOrDefault(),
-                            Lat = (decimal)location.Location.Latitude.GetValueOrDefault()
-                        }
-                    }
-                };
-                return View(address);
-            }
-
-            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
-            var s = await _httpClient.GetStringAsync($"http://api.map.baidu.com/geocoder/v2/?location={lat},{lng}&output=json&pois=1&ak={AppConfig.BaiduAK}", cts.Token).ContinueWith(t =>
-             {
-                 if (t.IsCompletedSuccessfully)
-                 {
-                     return JsonConvert.DeserializeObject<PhysicsAddress>(t.Result);
-                 }
-
-                 return new PhysicsAddress();
-             });
-
-            return View(s);
-        }
-
-        /// <summary>
-        /// 详细地理信息转经纬度
-        /// </summary>
-        /// <param name="addr"></param>
-        /// <returns></returns>
-        [Route("addr"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "addr" })]
-        public async Task<ActionResult> Address(string addr)
-        {
-            if (string.IsNullOrEmpty(addr))
-            {
-                var ip = ClientIP;
+			var location = Policy<CityResponse>.Handle<AddressNotFoundException>().Fallback(() => new CityResponse()).Execute(() => CommonHelper.MaxmindReader.City(ip));
+			var address = new PhysicsAddress()
+			{
+				Status = 0,
+				AddressResult = new AddressResult()
+				{
+					FormattedAddress = IPAddress.Parse(ip).GetIPLocation().Address,
+					Location = new Location()
+					{
+						Lng = (decimal)location.Location.Longitude.GetValueOrDefault(),
+						Lat = (decimal)location.Location.Latitude.GetValueOrDefault()
+					}
+				}
+			};
+			return View(address);
+		}
+
+		using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+		var s = await _httpClient.GetStringAsync($"http://api.map.baidu.com/geocoder/v2/?location={lat},{lng}&output=json&pois=1&ak={AppConfig.BaiduAK}", cts.Token).ContinueWith(t =>
+		{
+			if (t.IsCompletedSuccessfully)
+			{
+				return JsonConvert.DeserializeObject<PhysicsAddress>(t.Result);
+			}
+
+			return new PhysicsAddress();
+		});
+
+		return View(s);
+	}
+
+	/// <summary>
+	/// 详细地理信息转经纬度
+	/// </summary>
+	/// <param name="addr"></param>
+	/// <returns></returns>
+	[Route("addr"), ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "addr" })]
+	public async Task<ActionResult> Address(string addr)
+	{
+		if (string.IsNullOrEmpty(addr))
+		{
+			var ip = ClientIP;
 #if DEBUG
                 Random r = new Random();
                 ip = $"{r.Next(210)}.{r.Next(255)}.{r.Next(255)}.{r.Next(255)}";
 #endif
-                var location = Policy<CityResponse>.Handle<AddressNotFoundException>().Fallback(() => new CityResponse()).Execute(() => CommonHelper.MaxmindReader.City(ip));
-                var address = new PhysicsAddress()
-                {
-                    Status = 0,
-                    AddressResult = new AddressResult()
-                    {
-                        FormattedAddress = IPAddress.Parse(ip).GetIPLocation().Address,
-                        Location = new Location()
-                        {
-                            Lng = (decimal)location.Location.Longitude.GetValueOrDefault(),
-                            Lat = (decimal)location.Location.Latitude.GetValueOrDefault()
-                        }
-                    }
-                };
-                ViewBag.Address = address.AddressResult.FormattedAddress;
-                if (Request.Method.Equals(HttpMethods.Get) || (Request.Headers[HeaderNames.Accept] + "").StartsWith(ContentType.Json))
-                {
-                    return View(address.AddressResult.Location);
-                }
-
-                return Json(address.AddressResult.Location);
-            }
-
-            ViewBag.Address = addr;
-            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
-            var physicsAddress = await _httpClient.GetStringAsync($"http://api.map.baidu.com/geocoder/v2/?output=json&address={addr}&ak={AppConfig.BaiduAK}", cts.Token).ContinueWith(t =>
-             {
-                 if (t.IsCompletedSuccessfully)
-                 {
-                     return JsonConvert.DeserializeObject<PhysicsAddress>(t.Result);
-                 }
-
-                 return new PhysicsAddress();
-             });
-            if (Request.Method.Equals(HttpMethods.Get) || (Request.Headers[HeaderNames.Accept] + "").StartsWith(ContentType.Json))
-            {
-                return View(physicsAddress?.AddressResult?.Location);
-            }
-
-            return Json(physicsAddress?.AddressResult?.Location);
-        }
-    }
+			var location = Policy<CityResponse>.Handle<AddressNotFoundException>().Fallback(() => new CityResponse()).Execute(() => CommonHelper.MaxmindReader.City(ip));
+			var address = new PhysicsAddress()
+			{
+				Status = 0,
+				AddressResult = new AddressResult()
+				{
+					FormattedAddress = IPAddress.Parse(ip).GetIPLocation().Address,
+					Location = new Location()
+					{
+						Lng = (decimal)location.Location.Longitude.GetValueOrDefault(),
+						Lat = (decimal)location.Location.Latitude.GetValueOrDefault()
+					}
+				}
+			};
+			ViewBag.Address = address.AddressResult.FormattedAddress;
+			if (Request.Method.Equals(HttpMethods.Get) || (Request.Headers[HeaderNames.Accept] + "").StartsWith(ContentType.Json))
+			{
+				return View(address.AddressResult.Location);
+			}
+
+			return Json(address.AddressResult.Location);
+		}
+
+		ViewBag.Address = addr;
+		using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+		var physicsAddress = await _httpClient.GetStringAsync($"http://api.map.baidu.com/geocoder/v2/?output=json&address={addr}&ak={AppConfig.BaiduAK}", cts.Token).ContinueWith(t =>
+		{
+			if (t.IsCompletedSuccessfully)
+			{
+				return JsonConvert.DeserializeObject<PhysicsAddress>(t.Result);
+			}
+
+			return new PhysicsAddress();
+		});
+		if (Request.Method.Equals(HttpMethods.Get) || (Request.Headers[HeaderNames.Accept] + "").StartsWith(ContentType.Json))
+		{
+			return View(physicsAddress?.AddressResult?.Location);
+		}
+
+		return Json(physicsAddress?.AddressResult?.Location);
+	}
 }

+ 0 - 4
src/Masuit.MyBlogs.Core/Controllers/UploadController.cs

@@ -3,14 +3,10 @@ using DocumentFormat.OpenXml.Packaging;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Extensions.Firewall;
 using Masuit.MyBlogs.Core.Extensions.UEditor;
-using Masuit.MyBlogs.Core.Models.DTO;
-using Masuit.MyBlogs.Core.Models.ViewModel;
 using Masuit.Tools.AspNetCore.Mime;
 using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
-using Masuit.Tools.Core.Net;
 using Masuit.Tools.Html;
 using Masuit.Tools.Logging;
-using Masuit.Tools.Systems;
 using Microsoft.AspNetCore.Mvc;
 using OpenXmlPowerTools;
 using Polly;

+ 1 - 4
src/Masuit.MyBlogs.Core/Controllers/UserController.cs

@@ -1,8 +1,5 @@
-using Masuit.MyBlogs.Core.Models.DTO;
-using Masuit.MyBlogs.Core.Models.Entity;
-using Masuit.Tools.AspNetCore.ModelBinder;
+using Masuit.Tools.AspNetCore.ModelBinder;
 using Microsoft.AspNetCore.Mvc;
-using System.Linq.Expressions;
 using System.Net;
 
 namespace Masuit.MyBlogs.Core.Controllers;

+ 0 - 1
src/Masuit.MyBlogs.Core/Controllers/ValidateController.cs

@@ -1,7 +1,6 @@
 using Hangfire;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.Tools.Core.Validator;
-using Masuit.Tools.Systems;
 using Microsoft.AspNetCore.Mvc;
 
 namespace Masuit.MyBlogs.Core.Controllers;

+ 1 - 3
src/Masuit.MyBlogs.Core/Controllers/ValuesController.cs

@@ -1,6 +1,4 @@
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.Entity;
-using Masuit.Tools.AspNetCore.ModelBinder;
+using Masuit.Tools.AspNetCore.ModelBinder;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
 using Z.EntityFramework.Plus;

+ 1 - 2
src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/ProtectedApiCallHelper.cs

@@ -18,8 +18,7 @@ public sealed class ProtectedApiCallHelper
 		HttpClient = httpClient;
 	}
 
-	protected HttpClient HttpClient { get; }
-
+	private HttpClient HttpClient { get; }
 
 	/// <summary>
 	/// Calls the protected Web API and processes the result

+ 14 - 15
src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/ServiceCollectionExtension.cs

@@ -1,19 +1,18 @@
-using Masuit.MyBlogs.Core.Infrastructure;
-using Masuit.MyBlogs.Core.Infrastructure.Drive;
+using Masuit.MyBlogs.Core.Infrastructure.Drive;
 
 namespace Masuit.MyBlogs.Core.Extensions.DriveHelpers
 {
-    public static class ServiceCollectionExtension
-    {
-        public static IServiceCollection AddOneDrive(this IServiceCollection services)
-        {
-            services.AddDbContext<DriveContext>(ServiceLifetime.Scoped);
-            //不要被 CG Token获取采用单一实例
-            services.AddSingleton(new TokenService());
-            services.AddTransient<IDriveAccountService, DriveAccountService>();
-            services.AddTransient<IDriveService, DriveService>();
-            services.AddScoped<SettingService>();
-            return services;
-        }
-    }
+	public static class ServiceCollectionExtension
+	{
+		public static IServiceCollection AddOneDrive(this IServiceCollection services)
+		{
+			services.AddDbContext<DriveContext>(ServiceLifetime.Scoped);
+			//不要被 CG Token获取采用单一实例
+			services.AddSingleton(new TokenService());
+			services.AddTransient<IDriveAccountService, DriveAccountService>();
+			services.AddTransient<IDriveService, DriveService>();
+			services.AddScoped<SettingService>();
+			return services;
+		}
+	}
 }

+ 6 - 7
src/Masuit.MyBlogs.Core/Extensions/Firewall/AccessDenyException.cs

@@ -1,9 +1,8 @@
-namespace Masuit.MyBlogs.Core.Extensions.Firewall
+namespace Masuit.MyBlogs.Core.Extensions.Firewall;
+
+public class AccessDenyException : Exception
 {
-    public class AccessDenyException : Exception
-    {
-        public AccessDenyException(string msg) : base(msg)
-        {
-        }
-    }
+	public AccessDenyException(string msg) : base(msg)
+	{
+	}
 }

+ 46 - 47
src/Masuit.MyBlogs.Core/Extensions/Firewall/CloudflareRepoter.cs

@@ -4,57 +4,56 @@ using Polly;
 using System.Net;
 using System.Net.Sockets;
 
-namespace Masuit.MyBlogs.Core.Extensions.Firewall
+namespace Masuit.MyBlogs.Core.Extensions.Firewall;
+
+public sealed class CloudflareRepoter : IFirewallRepoter
 {
-    public sealed class CloudflareRepoter : IFirewallRepoter
-    {
-        private readonly HttpClient _httpClient;
-        private readonly IConfiguration _configuration;
+	private readonly IConfiguration _configuration;
+	private readonly HttpClient _httpClient;
 
-        public string ReporterName { get; set; } = "cloudflare";
 
+	public CloudflareRepoter(HttpClient httpClient, IConfiguration configuration)
+	{
+		_httpClient = httpClient;
+		_configuration = configuration;
+	}
 
-        public CloudflareRepoter(HttpClient httpClient, IConfiguration configuration)
-        {
-            _httpClient = httpClient;
-            _configuration = configuration;
-        }
+	public string ReporterName { get; set; } = "cloudflare";
 
-        public void Report(IPAddress ip)
-        {
-            ReportAsync(ip).Wait();
-        }
+	public void Report(IPAddress ip)
+	{
+		ReportAsync(ip).Wait();
+	}
 
-        public Task ReportAsync(IPAddress ip)
-        {
-            var scope = _configuration["FirewallService:Cloudflare:Scope"];
-            var zoneid = _configuration["FirewallService:Cloudflare:ZoneId"];
-            var fallbackPolicy = Policy.HandleInner<HttpRequestException>().FallbackAsync(_ =>
-            {
-                LogManager.Info($"cloudflare请求出错,{ip}上报失败!");
-                return Task.CompletedTask;
-            });
-            var retryPolicy = Policy.HandleInner<HttpRequestException>().RetryAsync(3);
-            return fallbackPolicy.WrapAsync(retryPolicy).ExecuteAsync(() => _httpClient.PostAsJsonAsync($"https://api.cloudflare.com/client/v4/{scope}/{zoneid}/firewall/access_rules/rules", new
-            {
-                mode = "block",
-                notes = $"恶意请求IP{ip.GetIPLocation()}",
-                configuration = new
-                {
-                    target = ip.AddressFamily switch
-                    {
-                        AddressFamily.InterNetworkV6 => "ip6",
-                        _ => "ip"
-                    },
-                    value = ip.ToString()
-                }
-            }).ContinueWith(t =>
-            {
-                if (!t.Result.IsSuccessStatusCode)
-                {
-                    throw new HttpRequestException("请求失败");
-                }
-            }));
-        }
-    }
+	public Task ReportAsync(IPAddress ip)
+	{
+		var scope = _configuration["FirewallService:Cloudflare:Scope"];
+		var zoneid = _configuration["FirewallService:Cloudflare:ZoneId"];
+		var fallbackPolicy = Policy.HandleInner<HttpRequestException>().FallbackAsync(_ =>
+		{
+			LogManager.Info($"cloudflare请求出错,{ip}上报失败!");
+			return Task.CompletedTask;
+		});
+		var retryPolicy = Policy.HandleInner<HttpRequestException>().RetryAsync(3);
+		return fallbackPolicy.WrapAsync(retryPolicy).ExecuteAsync(() => _httpClient.PostAsJsonAsync($"https://api.cloudflare.com/client/v4/{scope}/{zoneid}/firewall/access_rules/rules", new
+		{
+			mode = "block",
+			notes = $"恶意请求IP{ip.GetIPLocation()}",
+			configuration = new
+			{
+				target = ip.AddressFamily switch
+				{
+					AddressFamily.InterNetworkV6 => "ip6",
+					_ => "ip"
+				},
+				value = ip.ToString()
+			}
+		}).ContinueWith(t =>
+		{
+			if (!t.Result.IsSuccessStatusCode)
+			{
+				throw new HttpRequestException("请求失败");
+			}
+		}));
+	}
 }

+ 20 - 21
src/Masuit.MyBlogs.Core/Extensions/Firewall/DefaultFirewallRepoter.cs

@@ -1,27 +1,26 @@
 using System.Net;
 
-namespace Masuit.MyBlogs.Core.Extensions.Firewall
+namespace Masuit.MyBlogs.Core.Extensions.Firewall;
+
+public sealed class DefaultFirewallRepoter : IFirewallRepoter
 {
-    public sealed class DefaultFirewallRepoter : IFirewallRepoter
-    {
-        public string ReporterName { get; set; }
+	public string ReporterName { get; set; }
 
-        /// <summary>
-        /// 上报IP
-        /// </summary>
-        /// <param name="ip"></param>
-        public void Report(IPAddress ip)
-        {
-        }
+	/// <summary>
+	/// 上报IP
+	/// </summary>
+	/// <param name="ip"></param>
+	public void Report(IPAddress ip)
+	{
+	}
 
-        /// <summary>
-        /// 上报IP
-        /// </summary>
-        /// <param name="ip"></param>
-        /// <returns></returns>
-        public Task ReportAsync(IPAddress ip)
-        {
-            return Task.CompletedTask;
-        }
-    }
+	/// <summary>
+	/// 上报IP
+	/// </summary>
+	/// <param name="ip"></param>
+	/// <returns></returns>
+	public Task ReportAsync(IPAddress ip)
+	{
+		return Task.CompletedTask;
+	}
 }

+ 243 - 235
src/Masuit.MyBlogs.Core/Extensions/Firewall/FirewallAttribute.cs

@@ -1,250 +1,258 @@
-using CacheManager.Core;
+using System.Net;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Web;
+using CacheManager.Core;
 using FreeRedis;
 using Markdig;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Configs;
 using Masuit.MyBlogs.Core.Controllers;
-using Masuit.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools;
 using Masuit.Tools.AspNetCore.Mime;
-using Masuit.Tools.Core.Net;
 using Masuit.Tools.Logging;
-using Masuit.Tools.Security;
-using Masuit.Tools.Strings;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.Filters;
 using Microsoft.Extensions.Caching.Memory;
-using System.Net;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Web;
-using HeaderNames = Microsoft.Net.Http.Headers.HeaderNames;
+using Microsoft.Net.Http.Headers;
+using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
 
 namespace Masuit.MyBlogs.Core.Extensions.Firewall;
 
 public sealed class FirewallAttribute : IAsyncActionFilter
 {
-    public ICacheManager<int> CacheManager { get; set; }
-
-    public IFirewallRepoter FirewallRepoter { get; set; }
-
-    public IMemoryCache MemoryCache { get; set; }
-    public IRedisClient RedisClient { get; set; }
-
-    public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
-    {
-        var request = context.HttpContext.Request;
-        if (CommonHelper.SystemSettings.TryGetValue("BlockHeaderValues", out var v) && v.Length > 0)
-        {
-            var strs = v.Split("|", StringSplitOptions.RemoveEmptyEntries);
-            if (request.Headers.Values.Any(values => strs.Any(s => values.Contains(s))))
-            {
-                context.Result = new NotFoundResult();
-                return Task.CompletedTask;
-            }
-        }
-
-        request.Headers.Values.Contains("");
-        var ip = context.HttpContext.Connection.RemoteIpAddress.ToString();
-        var tokenValid = request.Cookies.ContainsKey("FullAccessToken") && request.Cookies["Email"].MDString(AppConfig.BaiduAK).Equals(request.Cookies["FullAccessToken"]);
-
-        //黑名单
-        if (ip.IsDenyIpAddress() && !tokenValid)
-        {
-            AccessDeny(ip, request, "黑名单IP地址");
-            context.Result = new BadRequestObjectResult("您当前所在的网络环境不支持访问本站!");
-            return Task.CompletedTask;
-        }
-
-        //bypass
-        if (CommonHelper.SystemSettings.GetOrAdd("FirewallEnabled", "true") == "false" || context.ActionDescriptor.EndpointMetadata.Any(o => o is MyAuthorizeAttribute or AllowAccessFirewallAttribute) || tokenValid)
-        {
-            return next();
-        }
-
-        //UserAgent
-        var ua = request.Headers[HeaderNames.UserAgent] + "";
-        var blocked = CommonHelper.SystemSettings.GetOrAdd("UserAgentBlocked", "").Split(new[] { ',', '|' }, StringSplitOptions.RemoveEmptyEntries);
-        if (ua.Contains(blocked))
-        {
-            var agent = UserAgent.Parse(ua);
-            AccessDeny(ip, request, $"UA黑名单({agent.Browser} {agent.BrowserVersion}/{agent.Platform})");
-            var msg = CommonHelper.SystemSettings.GetOrAdd("UserAgentBlockedMsg", "当前浏览器不支持访问本站");
-            context.Result = new ContentResult()
-            {
-                Content = Markdown.ToHtml(Template.Create(msg).Set("browser", agent.Browser + " " + agent.BrowserVersion).Set("os", agent.Platform).Render()),
-                ContentType = ContentType.Html + "; charset=utf-8",
-                StatusCode = 403
-            };
-            return Task.CompletedTask;
-        }
-
-        //搜索引擎
-        if (Regex.IsMatch(request.Method, "OPTIONS|HEAD", RegexOptions.IgnoreCase) || request.IsRobot())
-        {
-            return next();
-        }
-
-        // 反爬虫
-        if (CacheManager.GetOrAdd(nameof(FirewallController.AntiCrawler) + ":" + ip, 0) > 3)
-        {
-            context.Result = new ContentResult
-            {
-                ContentType = ContentType.Html + "; charset=utf-8",
-                StatusCode = 429,
-                Content = "检测到访问异常,请在10分钟后再试!"
-            };
-            return Task.CompletedTask;
-        }
-
-        //安全模式
-        if (request.Query[SessionKey.SafeMode].Count > 0)
-        {
-            request.Cookies.TryGetValue(SessionKey.HideCategories, out var s);
-            context.HttpContext.Response.Cookies.Append(SessionKey.HideCategories, request.Query[SessionKey.SafeMode] + "," + s, new CookieOptions
-            {
-                Expires = DateTime.Now.AddYears(1),
-                SameSite = SameSiteMode.Lax
-            });
-        }
-
-        //白名单地区
-        var ipLocation = ip.GetIPLocation();
-        var (location, network, pos) = ipLocation;
-        pos += ipLocation.Coodinate;
-        var allowedAreas = CommonHelper.SystemSettings.GetOrAdd("AllowedArea", "").Split(new[] { ',', ',' }, StringSplitOptions.RemoveEmptyEntries);
-        if (allowedAreas.Any() && pos.Contains(allowedAreas))
-        {
-            return next();
-        }
-
-        //黑名单地区
-        var denyAreas = CommonHelper.SystemSettings.GetOrAdd("DenyArea", "").Split(new[] { ',', ',' }, StringSplitOptions.RemoveEmptyEntries);
-        if (denyAreas.Any())
-        {
-            if (string.IsNullOrWhiteSpace(location) || string.IsNullOrWhiteSpace(network) || pos.Contains(denyAreas) || denyAreas.Intersect(pos.Split("|")).Any()) // 未知地区的,未知网络的,禁区的
-            {
-                AccessDeny(ip, request, "访问地区限制");
-                throw new AccessDenyException("访问地区限制");
-            }
-        }
-
-        //挑战模式
-        if (context.HttpContext.Session.TryGetValue("js-challenge", out _) || request.Path.ToUriComponent().Contains("."))
-        {
-            return next();
-        }
-
-        try
-        {
-            if (request.Cookies.TryGetValue(SessionKey.ChallengeBypass, out var time) && time.AESDecrypt(AppConfig.BaiduAK).ToDateTime() > DateTime.Now)
-            {
-                context.HttpContext.Session.Set("js-challenge", 1);
-                return next();
-            }
-        }
-        catch
-        {
-            context.HttpContext.Response.Cookies.Delete(SessionKey.ChallengeBypass);
-        }
-
-        if (Challenge(context, out var completedTask))
-        {
-            return completedTask;
-        }
-
-        //限流
-        return ThrottleLimit(ip, request, next);
-    }
-
-    private static bool Challenge(ActionExecutingContext context, out Task completedTask)
-    {
-        var mode = CommonHelper.SystemSettings.GetOrAdd(SessionKey.ChallengeMode, "");
-        if (mode == SessionKey.JSChallenge)
-        {
-            context.Result = new ViewResult()
-            {
-                ViewName = "/Views/Shared/JSChallenge.cshtml"
-            };
-            completedTask = Task.CompletedTask;
-            return true;
-        }
-
-        if (mode == SessionKey.CaptchaChallenge)
-        {
-            context.Result = new ViewResult()
-            {
-                ViewName = "/Views/Shared/CaptchaChallenge.cshtml"
-            };
-            completedTask = Task.CompletedTask;
-            return true;
-        }
-
-        if (mode == SessionKey.CloudflareTurnstileChallenge)
-        {
-            context.Result = new ViewResult()
-            {
-                ViewName = "/Views/Shared/CloudflareTurnstileChallenge.cshtml"
-            };
-            completedTask = Task.CompletedTask;
-            return true;
-        }
-
-        completedTask = Task.CompletedTask;
-        return false;
-    }
-
-    private Task ThrottleLimit(string ip, HttpRequest request, ActionExecutionDelegate next)
-    {
-        var times = CacheManager.AddOrUpdate("Frequency:" + ip, 1, i => i + 1, 5);
-        CacheManager.Expire("Frequency:" + ip, ExpirationMode.Absolute, TimeSpan.FromSeconds(CommonHelper.SystemSettings.GetOrAdd("LimitIPFrequency", "60").ToInt32()));
-        var limit = CommonHelper.SystemSettings.GetOrAdd("LimitIPRequestTimes", "90").ToInt32();
-        if (times <= limit)
-        {
-            return next();
-        }
-
-        if (times > limit * 1.2)
-        {
-            CacheManager.Expire("Frequency:" + ip, TimeSpan.FromMinutes(CommonHelper.SystemSettings.GetOrAdd("BanIPTimespan", "10").ToInt32()));
-            AccessDeny(ip, request, "访问频次限制");
-            throw new TempDenyException("访问频次限制");
-        }
-
-        return next();
-    }
-
-    private async void AccessDeny(string ip, HttpRequest request, string remark)
-    {
-        var path = HttpUtility.UrlDecode(request.Path + request.QueryString, Encoding.UTF8);
-        RedisClient.IncrBy("interceptCount", 1);
-        RedisClient.LPush("intercept", new IpIntercepter()
-        {
-            IP = ip,
-            RequestUrl = HttpUtility.UrlDecode(request.Scheme + "://" + request.Host + path),
-            Time = DateTime.Now,
-            Referer = request.Headers[HeaderNames.Referer],
-            UserAgent = request.Headers[HeaderNames.UserAgent],
-            Remark = remark,
-            Address = request.Location(),
-            HttpVersion = request.Protocol,
-            Headers = new
-            {
-                request.Protocol,
-                request.Headers
-            }.ToJsonString()
-        });
-        var limit = CommonHelper.SystemSettings.GetOrAdd("LimitIPInterceptTimes", "30").ToInt32();
-        var list = RedisClient.LRange<IpIntercepter>("intercept", 0, -1);
-        var key = "FirewallRepoter:" + FirewallRepoter.ReporterName + ":" + ip;
-        if (list.Count(x => x.IP == ip) >= limit && !MemoryCache.TryGetValue(key, out _))
-        {
-            LogManager.Info($"准备上报IP{ip}到{FirewallRepoter.ReporterName}");
-            await FirewallRepoter.ReportAsync(IPAddress.Parse(ip)).ContinueWith(_ =>
-            {
-                MemoryCache.Set(key, 1, TimeSpan.FromDays(1));
-                LogManager.Info($"访问频次限制,已上报IP{ip}至:" + FirewallRepoter.ReporterName);
-            });
-        }
-    }
-}
+	public ICacheManager<int> CacheManager { get; set; }
+
+	public IFirewallRepoter FirewallRepoter { get; set; }
+
+	public IMemoryCache MemoryCache { get; set; }
+	public IRedisClient RedisClient { get; set; }
+
+	public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+	{
+		var request = context.HttpContext.Request;
+		if (CommonHelper.SystemSettings.TryGetValue("BlockHeaderValues", out var v) && v.Length > 0)
+		{
+			var strs = v.Split("|", StringSplitOptions.RemoveEmptyEntries);
+			if (request.Headers.Values.Any(values => strs.Any(s => values.Contains(s))))
+			{
+				context.Result = new NotFoundResult();
+				return Task.CompletedTask;
+			}
+		}
+
+		request.Headers.Values.Contains("");
+		var ip = context.HttpContext.Connection.RemoteIpAddress.ToString();
+		var tokenValid = request.Cookies.ContainsKey("FullAccessToken") && request.Cookies["Email"].MDString(AppConfig.BaiduAK).Equals(request.Cookies["FullAccessToken"]);
+
+		//黑名单
+		if (ip.IsDenyIpAddress() && !tokenValid)
+		{
+			AccessDeny(ip, request, "黑名单IP地址");
+			context.Result = new BadRequestObjectResult("您当前所在的网络环境不支持访问本站!");
+			return Task.CompletedTask;
+		}
+
+		//bypass
+		if (CommonHelper.SystemSettings.GetOrAdd("FirewallEnabled", "true") == "false" || context.ActionDescriptor.EndpointMetadata.Any(o => o is MyAuthorizeAttribute or AllowAccessFirewallAttribute) || tokenValid)
+		{
+			return next();
+		}
+
+		//UserAgent
+		var ua = request.Headers[HeaderNames.UserAgent] + "";
+		var blocked = CommonHelper.SystemSettings.GetOrAdd("UserAgentBlocked", "").Split(new[]
+		{
+			',',
+			'|'
+		}, StringSplitOptions.RemoveEmptyEntries);
+		if (ua.Contains(blocked))
+		{
+			var agent = UserAgent.Parse(ua);
+			AccessDeny(ip, request, $"UA黑名单({agent.Browser} {agent.BrowserVersion}/{agent.Platform})");
+			var msg = CommonHelper.SystemSettings.GetOrAdd("UserAgentBlockedMsg", "当前浏览器不支持访问本站");
+			context.Result = new ContentResult
+			{
+				Content = Markdown.ToHtml(Template.Create(msg).Set("browser", agent.Browser + " " + agent.BrowserVersion).Set("os", agent.Platform).Render()),
+				ContentType = ContentType.Html + "; charset=utf-8",
+				StatusCode = 403
+			};
+			return Task.CompletedTask;
+		}
+
+		//搜索引擎
+		if (Regex.IsMatch(request.Method, "OPTIONS|HEAD", RegexOptions.IgnoreCase) || request.IsRobot())
+		{
+			return next();
+		}
+
+		// 反爬虫
+		if (CacheManager.GetOrAdd(nameof(FirewallController.AntiCrawler) + ":" + ip, 0) > 3)
+		{
+			context.Result = new ContentResult
+			{
+				ContentType = ContentType.Html + "; charset=utf-8",
+				StatusCode = 429,
+				Content = "检测到访问异常,请在10分钟后再试!"
+			};
+			return Task.CompletedTask;
+		}
+
+		//安全模式
+		if (request.Query[SessionKey.SafeMode].Count > 0)
+		{
+			request.Cookies.TryGetValue(SessionKey.HideCategories, out var s);
+			context.HttpContext.Response.Cookies.Append(SessionKey.HideCategories, request.Query[SessionKey.SafeMode] + "," + s, new CookieOptions
+			{
+				Expires = DateTime.Now.AddYears(1),
+				SameSite = SameSiteMode.Lax
+			});
+		}
+
+		//白名单地区
+		var ipLocation = ip.GetIPLocation();
+		var (location, network, pos) = ipLocation;
+		pos += ipLocation.Coodinate;
+		var allowedAreas = CommonHelper.SystemSettings.GetOrAdd("AllowedArea", "").Split(new[]
+		{
+			',',
+			','
+		}, StringSplitOptions.RemoveEmptyEntries);
+		if (allowedAreas.Any() && pos.Contains(allowedAreas))
+		{
+			return next();
+		}
+
+		//黑名单地区
+		var denyAreas = CommonHelper.SystemSettings.GetOrAdd("DenyArea", "").Split(new[]
+		{
+			',',
+			','
+		}, StringSplitOptions.RemoveEmptyEntries);
+		if (denyAreas.Any())
+		{
+			if (string.IsNullOrWhiteSpace(location) || string.IsNullOrWhiteSpace(network) || pos.Contains(denyAreas) || denyAreas.Intersect(pos.Split("|")).Any()) // 未知地区的,未知网络的,禁区的
+			{
+				AccessDeny(ip, request, "访问地区限制");
+				throw new AccessDenyException("访问地区限制");
+			}
+		}
+
+		//挑战模式
+		if (context.HttpContext.Session.TryGetValue("js-challenge", out _) || request.Path.ToUriComponent().Contains("."))
+		{
+			return next();
+		}
+
+		try
+		{
+			if (request.Cookies.TryGetValue(SessionKey.ChallengeBypass, out var time) && time.AESDecrypt(AppConfig.BaiduAK).ToDateTime() > DateTime.Now)
+			{
+				context.HttpContext.Session.Set("js-challenge", 1);
+				return next();
+			}
+		}
+		catch
+		{
+			context.HttpContext.Response.Cookies.Delete(SessionKey.ChallengeBypass);
+		}
+
+		if (Challenge(context, out var completedTask))
+		{
+			return completedTask;
+		}
+
+		//限流
+		return ThrottleLimit(ip, request, next);
+	}
+
+	private static bool Challenge(ActionExecutingContext context, out Task completedTask)
+	{
+		var mode = CommonHelper.SystemSettings.GetOrAdd(SessionKey.ChallengeMode, "");
+		if (mode == SessionKey.JSChallenge)
+		{
+			context.Result = new ViewResult
+			{
+				ViewName = "/Views/Shared/JSChallenge.cshtml"
+			};
+			completedTask = Task.CompletedTask;
+			return true;
+		}
+
+		if (mode == SessionKey.CaptchaChallenge)
+		{
+			context.Result = new ViewResult
+			{
+				ViewName = "/Views/Shared/CaptchaChallenge.cshtml"
+			};
+			completedTask = Task.CompletedTask;
+			return true;
+		}
+
+		if (mode == SessionKey.CloudflareTurnstileChallenge)
+		{
+			context.Result = new ViewResult
+			{
+				ViewName = "/Views/Shared/CloudflareTurnstileChallenge.cshtml"
+			};
+			completedTask = Task.CompletedTask;
+			return true;
+		}
+
+		completedTask = Task.CompletedTask;
+		return false;
+	}
+
+	private Task ThrottleLimit(string ip, HttpRequest request, ActionExecutionDelegate next)
+	{
+		var times = CacheManager.AddOrUpdate("Frequency:" + ip, 1, i => i + 1, 5);
+		CacheManager.Expire("Frequency:" + ip, ExpirationMode.Absolute, TimeSpan.FromSeconds(CommonHelper.SystemSettings.GetOrAdd("LimitIPFrequency", "60").ToInt32()));
+		var limit = CommonHelper.SystemSettings.GetOrAdd("LimitIPRequestTimes", "90").ToInt32();
+		if (times <= limit)
+		{
+			return next();
+		}
+
+		if (times > limit * 1.2)
+		{
+			CacheManager.Expire("Frequency:" + ip, TimeSpan.FromMinutes(CommonHelper.SystemSettings.GetOrAdd("BanIPTimespan", "10").ToInt32()));
+			AccessDeny(ip, request, "访问频次限制");
+			throw new TempDenyException("访问频次限制");
+		}
+
+		return next();
+	}
+
+	private async void AccessDeny(string ip, HttpRequest request, string remark)
+	{
+		var path = HttpUtility.UrlDecode(request.Path + request.QueryString, Encoding.UTF8);
+		RedisClient.IncrBy("interceptCount", 1);
+		RedisClient.LPush("intercept", new IpIntercepter
+		{
+			IP = ip,
+			RequestUrl = HttpUtility.UrlDecode(request.Scheme + "://" + request.Host + path),
+			Time = DateTime.Now,
+			Referer = request.Headers[HeaderNames.Referer],
+			UserAgent = request.Headers[HeaderNames.UserAgent],
+			Remark = remark,
+			Address = request.Location(),
+			HttpVersion = request.Protocol,
+			Headers = new
+			{
+				request.Protocol,
+				request.Headers
+			}.ToJsonString()
+		});
+		var limit = CommonHelper.SystemSettings.GetOrAdd("LimitIPInterceptTimes", "30").ToInt32();
+		var list = RedisClient.LRange<IpIntercepter>("intercept", 0, -1);
+		var key = "FirewallRepoter:" + FirewallRepoter.ReporterName + ":" + ip;
+		if (list.Count(x => x.IP == ip) >= limit && !MemoryCache.TryGetValue(key, out _))
+		{
+			LogManager.Info($"准备上报IP{ip}到{FirewallRepoter.ReporterName}");
+			await FirewallRepoter.ReportAsync(IPAddress.Parse(ip)).ContinueWith(_ =>
+			{
+				MemoryCache.Set(key, 1, TimeSpan.FromDays(1));
+				LogManager.Info($"访问频次限制,已上报IP{ip}至:" + FirewallRepoter.ReporterName);
+			});
+		}
+	}
+}

+ 22 - 23
src/Masuit.MyBlogs.Core/Extensions/Firewall/FirewallServiceCollectionExt.cs

@@ -1,26 +1,25 @@
-namespace Masuit.MyBlogs.Core.Extensions.Firewall
+namespace Masuit.MyBlogs.Core.Extensions.Firewall;
+
+public static class FirewallServiceCollectionExt
 {
-    public static class FirewallServiceCollectionExt
-    {
-        public static IServiceCollection AddFirewallReporter(this IServiceCollection services, IConfiguration configuration)
-        {
-            switch (configuration["FirewallService:type"])
-            {
-                case "Cloudflare":
-                case "cloudflare":
-                case "cf":
-                    services.AddHttpClient<IFirewallRepoter, CloudflareRepoter>().ConfigureHttpClient(c =>
-                    {
-                        c.DefaultRequestHeaders.Add("X-Auth-Email", configuration["FirewallService:Cloudflare:AuthEmail"]);
-                        c.DefaultRequestHeaders.Add("X-Auth-Key", configuration["FirewallService:Cloudflare:AuthKey"]);
-                    });
-                    break;
-                default:
-                    services.AddSingleton<IFirewallRepoter, DefaultFirewallRepoter>();
-                    break;
-            }
+	public static IServiceCollection AddFirewallReporter(this IServiceCollection services, IConfiguration configuration)
+	{
+		switch (configuration["FirewallService:type"])
+		{
+			case "Cloudflare":
+			case "cloudflare":
+			case "cf":
+				services.AddHttpClient<IFirewallRepoter, CloudflareRepoter>().ConfigureHttpClient(c =>
+				{
+					c.DefaultRequestHeaders.Add("X-Auth-Email", configuration["FirewallService:Cloudflare:AuthEmail"]);
+					c.DefaultRequestHeaders.Add("X-Auth-Key", configuration["FirewallService:Cloudflare:AuthKey"]);
+				});
+				break;
+			default:
+				services.AddSingleton<IFirewallRepoter, DefaultFirewallRepoter>();
+				break;
+		}
 
-            return services;
-        }
-    }
+		return services;
+	}
 }

+ 15 - 16
src/Masuit.MyBlogs.Core/Extensions/Firewall/IFirewallRepoter.cs

@@ -1,22 +1,21 @@
 using System.Net;
 
-namespace Masuit.MyBlogs.Core.Extensions.Firewall
+namespace Masuit.MyBlogs.Core.Extensions.Firewall;
+
+public interface IFirewallRepoter
 {
-    public interface IFirewallRepoter
-    {
-        string ReporterName { get; set; }
+	string ReporterName { get; set; }
 
-        /// <summary>
-        /// 上报IP
-        /// </summary>
-        /// <param name="ip"></param>
-        void Report(IPAddress ip);
+	/// <summary>
+	/// 上报IP
+	/// </summary>
+	/// <param name="ip"></param>
+	void Report(IPAddress ip);
 
-        /// <summary>
-        /// 上报IP
-        /// </summary>
-        /// <param name="ip"></param>
-        /// <returns></returns>
-        Task ReportAsync(IPAddress ip);
-    }
+	/// <summary>
+	/// 上报IP
+	/// </summary>
+	/// <param name="ip"></param>
+	/// <returns></returns>
+	Task ReportAsync(IPAddress ip);
 }

+ 123 - 112
src/Masuit.MyBlogs.Core/Extensions/Firewall/IRequestLogger.cs

@@ -1,140 +1,151 @@
-using Masuit.MyBlogs.Core.Common;
-using Masuit.MyBlogs.Core.Infrastructure;
-using Masuit.MyBlogs.Core.Models.Entity;
-using Masuit.Tools;
-using Microsoft.Extensions.DependencyInjection.Extensions;
-using System.Collections.Concurrent;
+using System.Collections.Concurrent;
 using System.Diagnostics;
+using Masuit.MyBlogs.Core.Common;
+using Microsoft.Extensions.DependencyInjection.Extensions;
 
 namespace Masuit.MyBlogs.Core.Extensions.Firewall;
 
 public interface IRequestLogger
 {
-    void Log(string ip, string url, string userAgent, string traceid);
+	void Log(string ip, string url, string userAgent, string traceid);
 
-    void Process();
+	void Process();
 }
 
 public class RequestNoneLogger : IRequestLogger
 {
-    public void Log(string ip, string url, string userAgent, string traceid)
-    {
-    }
+	public void Log(string ip, string url, string userAgent, string traceid)
+	{
+	}
 
-    public void Process()
-    {
-    }
+	public void Process()
+	{
+	}
 }
 
 public class RequestFileLogger : IRequestLogger
 {
-    public void Log(string ip, string url, string userAgent, string traceid)
-    {
-        TrackData.RequestLogs.AddOrUpdate(ip, new RequestLog()
-        {
-            Count = 1,
-            RequestUrls = { url },
-            UserAgents = { userAgent }
-        }, (_, i) =>
-        {
-            i.UserAgents.Add(userAgent);
-            i.RequestUrls.Add(url);
-            i.Count++;
-            return i;
-        });
-    }
-
-    public void Process()
-    {
-    }
+	public void Log(string ip, string url, string userAgent, string traceid)
+	{
+		TrackData.RequestLogs.AddOrUpdate(ip, new RequestLog
+		{
+			Count = 1,
+			RequestUrls =
+			{
+				url
+			},
+			UserAgents =
+			{
+				userAgent
+			}
+		}, (_, i) =>
+		{
+			i.UserAgents.Add(userAgent);
+			i.RequestUrls.Add(url);
+			i.Count++;
+			return i;
+		});
+	}
+
+	public void Process()
+	{
+	}
 }
 
 public class RequestDatabaseLogger : IRequestLogger
 {
-    private readonly LoggerDbContext _dataContext;
-    private static readonly ConcurrentQueue<RequestLogDetail> Queue = new();
-
-    public RequestDatabaseLogger(LoggerDbContext dataContext)
-    {
-        _dataContext = dataContext;
-    }
-
-    public void Log(string ip, string url, string userAgent, string traceid)
-    {
-        Queue.Enqueue(new RequestLogDetail
-        {
-            Time = DateTime.Now,
-            UserAgent = userAgent,
-            RequestUrl = url,
-            IP = ip,
-            TraceId = traceid
-        });
-    }
-
-    public void Process()
-    {
-        if (Debugger.IsAttached)
-        {
-            return;
-        }
-
-        while (Queue.TryDequeue(out var result))
-        {
-            var location = result.IP.GetIPLocation();
-            result.Location = location;
-            result.Country = new[] { location.Country, location.Address2.Country }.Where(s => !string.IsNullOrEmpty(s)).Distinct().Join("|");
-            result.City = new[] { location.City, location.Address2.City }.Where(s => !string.IsNullOrEmpty(s)).Distinct().Join("|");
-            result.Network = location.Network;
-            _dataContext.Add(result);
-        }
-
-        if (_dataContext.SaveChanges() > 0)
-        {
-            var start = DateTime.Now.AddMonths(-6);
-            _dataContext.Set<RequestLogDetail>().Where(e => e.Time < start).DeleteFromQuery();
-        }
-    }
+	private static readonly ConcurrentQueue<RequestLogDetail> Queue = new();
+	private readonly LoggerDbContext _dataContext;
+
+	public RequestDatabaseLogger(LoggerDbContext dataContext)
+	{
+		_dataContext = dataContext;
+	}
+
+	public void Log(string ip, string url, string userAgent, string traceid)
+	{
+		Queue.Enqueue(new RequestLogDetail
+		{
+			Time = DateTime.Now,
+			UserAgent = userAgent,
+			RequestUrl = url,
+			IP = ip,
+			TraceId = traceid
+		});
+	}
+
+	public void Process()
+	{
+		if (Debugger.IsAttached)
+		{
+			return;
+		}
+
+		while (Queue.TryDequeue(out var result))
+		{
+			var location = result.IP.GetIPLocation();
+			result.Location = location;
+			result.Country = new[]
+			{
+				location.Country,
+				location.Address2.Country
+			}.Where(s => !string.IsNullOrEmpty(s)).Distinct().Join("|");
+			result.City = new[]
+			{
+				location.City,
+				location.Address2.City
+			}.Where(s => !string.IsNullOrEmpty(s)).Distinct().Join("|");
+			result.Network = location.Network;
+			_dataContext.Add(result);
+		}
+
+		if (_dataContext.SaveChanges() > 0)
+		{
+			var start = DateTime.Now.AddMonths(-6);
+			_dataContext.Set<RequestLogDetail>().Where(e => e.Time < start).DeleteFromQuery();
+		}
+	}
 }
 
 public class RequestLoggerBackService : ScheduledService
 {
-    private readonly IServiceScopeFactory _scopeFactory;
-
-    public RequestLoggerBackService(IServiceScopeFactory scopeFactory) : base(TimeSpan.FromMinutes(5))
-    {
-        _scopeFactory = scopeFactory;
-    }
-
-    protected override Task ExecuteAsync()
-    {
-        using var scope = _scopeFactory.CreateAsyncScope();
-        var logger = scope.ServiceProvider.GetRequiredService<IRequestLogger>();
-        logger.Process();
-        return Task.CompletedTask;
-    }
+	private readonly IServiceScopeFactory _scopeFactory;
+
+	public RequestLoggerBackService(IServiceScopeFactory scopeFactory) : base(TimeSpan.FromMinutes(5))
+	{
+		_scopeFactory = scopeFactory;
+	}
+
+	protected override Task ExecuteAsync()
+	{
+		using var scope = _scopeFactory.CreateAsyncScope();
+		var logger = scope.ServiceProvider.GetRequiredService<IRequestLogger>();
+		logger.Process();
+		return Task.CompletedTask;
+	}
 }
 
 public static class RequestLoggerServiceExtension
 {
-    public static IServiceCollection AddRequestLogger(this IServiceCollection services, IConfiguration configuration)
-    {
-        switch (configuration["RequestLogStorage"])
-        {
-            case "database":
-                services.AddScoped<IRequestLogger, RequestDatabaseLogger>();
-                services.TryAddScoped<RequestDatabaseLogger>();
-                break;
-
-            case "file":
-                services.AddSingleton<IRequestLogger, RequestFileLogger>();
-                break;
-
-            default:
-                services.AddSingleton<IRequestLogger, RequestNoneLogger>();
-                break;
-        }
-
-        services.AddHostedService<RequestLoggerBackService>();
-        return services;
-    }
-}
+	public static IServiceCollection AddRequestLogger(this IServiceCollection services, IConfiguration configuration)
+	{
+		switch (configuration["RequestLogStorage"])
+		{
+			case "database":
+				services.AddScoped<IRequestLogger, RequestDatabaseLogger>();
+				services.TryAddScoped<RequestDatabaseLogger>();
+				break;
+
+			case "file":
+				services.AddSingleton<IRequestLogger, RequestFileLogger>();
+				break;
+
+			default:
+				services.AddSingleton<IRequestLogger, RequestNoneLogger>();
+				break;
+		}
+
+		services.AddHostedService<RequestLoggerBackService>();
+		return services;
+	}
+}

+ 12 - 13
src/Masuit.MyBlogs.Core/Extensions/Firewall/IpIntercepter.cs

@@ -1,15 +1,14 @@
-namespace Masuit.MyBlogs.Core.Extensions.Firewall
+namespace Masuit.MyBlogs.Core.Extensions.Firewall;
+
+public class IpIntercepter
 {
-    public class IpIntercepter
-    {
-        public string IP { get; set; }
-        public string RequestUrl { get; set; }
-        public string Referer { get; set; }
-        public string Address { get; set; }
-        public string UserAgent { get; set; }
-        public DateTime Time { get; set; }
-        public string Remark { get; set; }
-        public string HttpVersion { get; set; }
-        public string Headers { get; set; }
-    }
+	public string IP { get; set; }
+	public string RequestUrl { get; set; }
+	public string Referer { get; set; }
+	public string Address { get; set; }
+	public string UserAgent { get; set; }
+	public DateTime Time { get; set; }
+	public string Remark { get; set; }
+	public string HttpVersion { get; set; }
+	public string Headers { get; set; }
 }

+ 113 - 106
src/Masuit.MyBlogs.Core/Extensions/Firewall/RequestInterceptMiddleware.cs

@@ -1,16 +1,13 @@
-using FreeRedis;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Web;
+using FreeRedis;
 using Hangfire;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Configs;
 using Masuit.MyBlogs.Core.Extensions.Hangfire;
-using Masuit.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools;
-using Masuit.Tools.Core.Net;
-using Masuit.Tools.Security;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Web;
-using HeaderNames = Microsoft.Net.Http.Headers.HeaderNames;
+using Microsoft.Net.Http.Headers;
+using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
 
 namespace Masuit.MyBlogs.Core.Extensions.Firewall;
 
@@ -19,109 +16,119 @@ namespace Masuit.MyBlogs.Core.Extensions.Firewall;
 /// </summary>
 public class RequestInterceptMiddleware
 {
-    private readonly RequestDelegate _next;
-    private readonly IRequestLogger _requestLogger;
-    private readonly IRedisClient _redisClient;
-    /// <summary>
-    /// 构造函数
-    /// </summary>
-    /// <param name="next"></param>
-    public RequestInterceptMiddleware(RequestDelegate next, IRequestLogger requestLogger, IRedisClient redisClient)
-    {
-        _next = next;
-        _requestLogger = requestLogger;
-        _redisClient = redisClient;
-    }
+	private readonly RequestDelegate _next;
+	private readonly IRedisClient _redisClient;
+	private readonly IRequestLogger _requestLogger;
+
+	/// <summary>
+	/// 构造函数
+	/// </summary>
+	/// <param name="next"></param>
+	public RequestInterceptMiddleware(RequestDelegate next, IRequestLogger requestLogger, IRedisClient redisClient)
+	{
+		_next = next;
+		_requestLogger = requestLogger;
+		_redisClient = redisClient;
+	}
+
+	public Task Invoke(HttpContext context)
+	{
+		var request = context.Request;
 
-    public Task Invoke(HttpContext context)
-    {
-        var request = context.Request;
+		//启用读取request
+		request.EnableBuffering();
+		if (!AppConfig.EnableIPDirect && request.Host.Host.MatchInetAddress() && !request.Host.Host.IsPrivateIP())
+		{
+			context.Response.Redirect("https://www.baidu.com", true);
 
-        //启用读取request
-        request.EnableBuffering();
-        if (!AppConfig.EnableIPDirect && request.Host.Host.MatchInetAddress() && !request.Host.Host.IsPrivateIP())
-        {
-            context.Response.Redirect("https://www.baidu.com", true);
+			//context.Response.StatusCode = 404;
+			return Task.CompletedTask;
+		}
 
-            //context.Response.StatusCode = 404;
-            return Task.CompletedTask;
-        }
-        var ip = context.Connection.RemoteIpAddress!.ToString();
-        var path = HttpUtility.UrlDecode(request.Path + request.QueryString, Encoding.UTF8);
-        var requestUrl = HttpUtility.UrlDecode(request.Scheme + "://" + request.Host + path);
-        var match = Regex.Match(path ?? "", CommonHelper.BanRegex);
-        if (match.Length > 0)
-        {
-            _redisClient.IncrBy("interceptCount", 1);
-            _redisClient.LPush("intercept", new IpIntercepter()
-            {
-                IP = ip,
-                RequestUrl = requestUrl,
-                Time = DateTime.Now,
-                Referer = request.Headers[HeaderNames.Referer],
-                UserAgent = request.Headers[HeaderNames.UserAgent],
-                Remark = $"检测到敏感词拦截:{match.Value}",
-                Address = request.Location(),
-                HttpVersion = request.Protocol,
-                Headers = new
-                {
-                    request.Protocol,
-                    request.Headers
-                }.ToJsonString()
-            });
-            context.Response.StatusCode = 404;
-            context.Response.ContentType = "text/html; charset=utf-8";
-            return context.Response.WriteAsync("参数不合法!", Encoding.UTF8);
-        }
+		var ip = context.Connection.RemoteIpAddress!.ToString();
+		var path = HttpUtility.UrlDecode(request.Path + request.QueryString, Encoding.UTF8);
+		var requestUrl = HttpUtility.UrlDecode(request.Scheme + "://" + request.Host + path);
+		var match = Regex.Match(path ?? "", CommonHelper.BanRegex);
+		if (match.Length > 0)
+		{
+			_redisClient.IncrBy("interceptCount", 1);
+			_redisClient.LPush("intercept", new IpIntercepter
+			{
+				IP = ip,
+				RequestUrl = requestUrl,
+				Time = DateTime.Now,
+				Referer = request.Headers[HeaderNames.Referer],
+				UserAgent = request.Headers[HeaderNames.UserAgent],
+				Remark = $"检测到敏感词拦截:{match.Value}",
+				Address = request.Location(),
+				HttpVersion = request.Protocol,
+				Headers = new
+				{
+					request.Protocol,
+					request.Headers
+				}.ToJsonString()
+			});
+			context.Response.StatusCode = 404;
+			context.Response.ContentType = "text/html; charset=utf-8";
+			return context.Response.WriteAsync("参数不合法!", Encoding.UTF8);
+		}
 
-        if (!context.Session.TryGetValue("session", out _) && !context.Request.IsRobot())
-        {
-            context.Session.Set("session", 0);
-            var referer = context.Request.Headers[HeaderNames.Referer].ToString();
-            if (!string.IsNullOrEmpty(referer))
-            {
-                try
-                {
-                    new Uri(referer);//判断是不是一个合法的referer
-                    if (!referer.Contains(context.Request.Host.Value) && !referer.Contains(new[] { "baidu.com", "google", "sogou", "so.com", "bing.com", "sm.cn" }))
-                    {
-                        BackgroundJob.Enqueue<IHangfireBackJob>(job => job.UpdateLinkWeight(referer, ip));
-                    }
-                }
-                catch
-                {
-                    context.Response.StatusCode = 405;
-                    context.Response.ContentType = "text/html; charset=utf-8";
-                    return context.Response.WriteAsync("您的浏览器不支持访问本站!", Encoding.UTF8);
-                }
-            }
-        }
+		if (!context.Session.TryGetValue("session", out _) && !context.Request.IsRobot())
+		{
+			context.Session.Set("session", 0);
+			var referer = context.Request.Headers[HeaderNames.Referer].ToString();
+			if (!string.IsNullOrEmpty(referer))
+			{
+				try
+				{
+					new Uri(referer); //判断是不是一个合法的referer
+					if (!referer.Contains(context.Request.Host.Value) && !referer.Contains(new[]
+						{
+							"baidu.com",
+							"google",
+							"sogou",
+							"so.com",
+							"bing.com",
+							"sm.cn"
+						}))
+					{
+						BackgroundJob.Enqueue<IHangfireBackJob>(job => job.UpdateLinkWeight(referer, ip));
+					}
+				}
+				catch
+				{
+					context.Response.StatusCode = 405;
+					context.Response.ContentType = "text/html; charset=utf-8";
+					return context.Response.WriteAsync("您的浏览器不支持访问本站!", Encoding.UTF8);
+				}
+			}
+		}
 
-        if (!context.Request.IsRobot())
-        {
-            if (request.QueryString.HasValue && request.QueryString.Value.Contains("="))
-            {
-                var q = request.QueryString.Value.Trim('?');
-                requestUrl = requestUrl.Replace(q, q.Split('&').Where(s => !s.StartsWith("cid") && !s.StartsWith("uid")).Join("&"));
-            }
+		if (!context.Request.IsRobot())
+		{
+			if (request.QueryString.HasValue && request.QueryString.Value.Contains("="))
+			{
+				var q = request.QueryString.Value.Trim('?');
+				requestUrl = requestUrl.Replace(q, q.Split('&').Where(s => !s.StartsWith("cid") && !s.StartsWith("uid")).Join("&"));
+			}
 
-            _requestLogger.Log(ip, requestUrl, context.Request.Headers[HeaderNames.UserAgent], context.Session.Id);
-        }
+			_requestLogger.Log(ip, requestUrl, context.Request.Headers[HeaderNames.UserAgent], context.Session.Id);
+		}
 
-        if (string.IsNullOrEmpty(context.Session.Get<string>(SessionKey.TimeZone)))
-        {
-            context.Session.Set(SessionKey.TimeZone, context.Connection.RemoteIpAddress.GetClientTimeZone());
-        }
+		if (string.IsNullOrEmpty(context.Session.Get<string>(SessionKey.TimeZone)))
+		{
+			context.Session.Set(SessionKey.TimeZone, context.Connection.RemoteIpAddress.GetClientTimeZone());
+		}
 
-        if (!context.Request.Cookies.ContainsKey(SessionKey.RawIP))
-        {
-            context.Response.Cookies.Append(SessionKey.RawIP, ip.Base64Encrypt(), new CookieOptions()
-            {
-                Expires = DateTimeOffset.Now.AddDays(1),
-                SameSite = SameSiteMode.Lax
-            });
-        }
+		if (!context.Request.Cookies.ContainsKey(SessionKey.RawIP))
+		{
+			context.Response.Cookies.Append(SessionKey.RawIP, ip.Base64Encrypt(), new CookieOptions
+			{
+				Expires = DateTimeOffset.Now.AddDays(1),
+				SameSite = SameSiteMode.Lax
+			});
+		}
 
-        return _next(context);
-    }
-}
+		return _next(context);
+	}
+}

+ 9 - 9
src/Masuit.MyBlogs.Core/Extensions/Hangfire/HangfireActivator.cs

@@ -4,15 +4,15 @@ namespace Masuit.MyBlogs.Core.Extensions.Hangfire;
 
 public sealed class HangfireActivator : JobActivator
 {
-    private readonly IServiceProvider _serviceProvider;
+	private readonly IServiceProvider _serviceProvider;
 
-    public HangfireActivator(IServiceProvider serviceProvider)
-    {
-        _serviceProvider = serviceProvider;
-    }
+	public HangfireActivator(IServiceProvider serviceProvider)
+	{
+		_serviceProvider = serviceProvider;
+	}
 
-    public override object ActivateJob(Type type)
-    {
-        return _serviceProvider.GetService(type);
-    }
+	public override object ActivateJob(Type type)
+	{
+		return _serviceProvider.GetService(type);
+	}
 }

+ 250 - 259
src/Masuit.MyBlogs.Core/Extensions/Hangfire/HangfireBackJob.cs

@@ -1,309 +1,300 @@
 using FreeRedis;
 using Masuit.LuceneEFCore.SearchEngine.Interfaces;
 using Masuit.MyBlogs.Core.Common;
-using Masuit.MyBlogs.Core.Infrastructure;
-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;
 using Masuit.Tools.Logging;
-using Masuit.Tools.Strings;
-using Masuit.Tools.Systems;
 using Microsoft.EntityFrameworkCore;
 using System.Net;
 
-namespace Masuit.MyBlogs.Core.Extensions.Hangfire
+namespace Masuit.MyBlogs.Core.Extensions.Hangfire;
+
+/// <summary>
+/// hangfire后台任务
+/// </summary>
+public sealed class HangfireBackJob : Disposable, IHangfireBackJob
 {
+	private readonly IHttpClientFactory _httpClientFactory;
+	private readonly IWebHostEnvironment _hostEnvironment;
+	private readonly IServiceScope _serviceScope;
+	private readonly IRedisClient _redisClient;
+	private readonly IConfiguration _configuration;
+
 	/// <summary>
 	/// hangfire后台任务
 	/// </summary>
-	public sealed class HangfireBackJob : Disposable, IHangfireBackJob
+	public HangfireBackJob(IServiceProvider serviceProvider, IHttpClientFactory httpClientFactory, IWebHostEnvironment hostEnvironment, IRedisClient redisClient, IConfiguration configuration)
 	{
-		private readonly IHttpClientFactory _httpClientFactory;
-		private readonly IWebHostEnvironment _hostEnvironment;
-		private readonly IServiceScope _serviceScope;
-		private readonly IRedisClient _redisClient;
-		private readonly IConfiguration _configuration;
+		_httpClientFactory = httpClientFactory;
+		_hostEnvironment = hostEnvironment;
+		_redisClient = redisClient;
+		_configuration = configuration;
+		_serviceScope = serviceProvider.CreateScope();
+	}
 
-		/// <summary>
-		/// hangfire后台任务
-		/// </summary>
-		public HangfireBackJob(IServiceProvider serviceProvider, IHttpClientFactory httpClientFactory, IWebHostEnvironment hostEnvironment, IRedisClient redisClient, IConfiguration configuration)
+	/// <summary>
+	/// 登录记录
+	/// </summary>
+	/// <param name="userInfo"></param>
+	/// <param name="ip"></param>
+	/// <param name="type"></param>
+	public void LoginRecord(UserInfoDto userInfo, string ip, LoginType type)
+	{
+		var record = new LoginRecord()
 		{
-			_httpClientFactory = httpClientFactory;
-			_hostEnvironment = hostEnvironment;
-			_redisClient = redisClient;
-			_configuration = configuration;
-			_serviceScope = serviceProvider.CreateScope();
-		}
+			IP = ip,
+			LoginTime = DateTime.Now,
+			LoginType = type,
+			PhysicAddress = ip.GetIPLocation()
+		};
+		var userInfoService = _serviceScope.ServiceProvider.GetRequiredService<IUserInfoService>();
+		var settingService = _serviceScope.ServiceProvider.GetRequiredService<ISystemSettingService>();
+		var u = userInfoService.GetByUsername(userInfo.Username);
+		u.LoginRecord.Add(record);
+		userInfoService.SaveChanges();
+		var content = new Template(File.ReadAllText(Path.Combine(_hostEnvironment.WebRootPath, "template", "login.html")))
+			.Set("name", u.Username)
+			.Set("time", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))
+			.Set("ip", record.IP)
+			.Set("address", record.PhysicAddress).Render();
+		CommonHelper.SendMail(settingService.Get(s => s.Name.Equals("Title")).Value + "账号登录通知", content, settingService.Get(s => s.Name.Equals("ReceiveEmail")).Value, "127.0.0.1");
+	}
 
-		/// <summary>
-		/// 登录记录
-		/// </summary>
-		/// <param name="userInfo"></param>
-		/// <param name="ip"></param>
-		/// <param name="type"></param>
-		public void LoginRecord(UserInfoDto userInfo, string ip, LoginType type)
+	/// <summary>
+	/// 文章定时发布
+	/// </summary>
+	/// <param name="p"></param>
+	public void PublishPost(Post p)
+	{
+		p.Status = Status.Published;
+		p.PostDate = DateTime.Now;
+		p.ModifyDate = DateTime.Now;
+		var postService = _serviceScope.ServiceProvider.GetRequiredService<IPostService>();
+		var post = postService.GetById(p.Id);
+		if (post is null)
 		{
-			var record = new LoginRecord()
-			{
-				IP = ip,
-				LoginTime = DateTime.Now,
-				LoginType = type,
-				PhysicAddress = ip.GetIPLocation()
-			};
-			var userInfoService = _serviceScope.ServiceProvider.GetRequiredService<IUserInfoService>();
-			var settingService = _serviceScope.ServiceProvider.GetRequiredService<ISystemSettingService>();
-			var u = userInfoService.GetByUsername(userInfo.Username);
-			u.LoginRecord.Add(record);
-			userInfoService.SaveChanges();
-			var content = new Template(File.ReadAllText(Path.Combine(_hostEnvironment.WebRootPath, "template", "login.html")))
-				.Set("name", u.Username)
-				.Set("time", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))
-				.Set("ip", record.IP)
-				.Set("address", record.PhysicAddress).Render();
-			CommonHelper.SendMail(settingService.Get(s => s.Name.Equals("Title")).Value + "账号登录通知", content, settingService.Get(s => s.Name.Equals("ReceiveEmail")).Value, "127.0.0.1");
+			postService.AddEntitySaved(p);
 		}
-
-		/// <summary>
-		/// 文章定时发布
-		/// </summary>
-		/// <param name="p"></param>
-		public void PublishPost(Post p)
+		else
 		{
-			p.Status = Status.Published;
-			p.PostDate = DateTime.Now;
-			p.ModifyDate = DateTime.Now;
-			var postService = _serviceScope.ServiceProvider.GetRequiredService<IPostService>();
-			var post = postService.GetById(p.Id);
-			if (post is null)
-			{
-				postService.AddEntitySaved(p);
-			}
-			else
-			{
-				post.Status = Status.Published;
-				post.PostDate = DateTime.Now;
-				post.ModifyDate = DateTime.Now;
-				postService.SaveChanges();
-			}
+			post.Status = Status.Published;
+			post.PostDate = DateTime.Now;
+			post.ModifyDate = DateTime.Now;
+			postService.SaveChanges();
 		}
+	}
 
-		/// <summary>
-		/// 文章访问记录
-		/// </summary>
-		/// <param name="pid"></param>
-		/// <param name="ip"></param>
-		/// <param name="refer"></param>
-		/// <param name="url"></param>
-		public void RecordPostVisit(int pid, string ip, string refer, string url)
+	/// <summary>
+	/// 文章访问记录
+	/// </summary>
+	/// <param name="pid"></param>
+	/// <param name="ip"></param>
+	/// <param name="refer"></param>
+	/// <param name="url"></param>
+	public void RecordPostVisit(int pid, string ip, string refer, string url)
+	{
+		var lastQuarter = DateTime.Now.AddMonths(-6);
+		var last3Year = DateTime.Now.AddYears(-3);
+		var recordService = _serviceScope.ServiceProvider.GetRequiredService<IPostVisitRecordService>();
+		var recordStatsService = _serviceScope.ServiceProvider.GetRequiredService<IPostVisitRecordStatsService>();
+		var postService = _serviceScope.ServiceProvider.GetRequiredService<IPostService>();
+		recordService.GetQuery(b => b.Time < lastQuarter).DeleteFromQuery();
+		recordStatsService.GetQuery(b => b.Date < last3Year).DeleteFromQuery();
+		var post = postService.GetById(pid);
+		if (post == null)
 		{
-			var lastQuarter = DateTime.Now.AddMonths(-6);
-			var last3Year = DateTime.Now.AddYears(-3);
-			var recordService = _serviceScope.ServiceProvider.GetRequiredService<IPostVisitRecordService>();
-			var recordStatsService = _serviceScope.ServiceProvider.GetRequiredService<IPostVisitRecordStatsService>();
-			var postService = _serviceScope.ServiceProvider.GetRequiredService<IPostService>();
-			recordService.GetQuery(b => b.Time < lastQuarter).DeleteFromQuery();
-			recordStatsService.GetQuery(b => b.Date < last3Year).DeleteFromQuery();
-			var post = postService.GetById(pid);
-			if (post == null)
-			{
-				return;
-			}
+			return;
+		}
 
-			post.TotalViewCount += 1;
-			post.AverageViewCount = recordService.GetQuery(e => e.PostId == pid).GroupBy(r => r.Time.Date).Select(g => g.Count()).DefaultIfEmpty().Average();
-			recordService.AddEntity(new PostVisitRecord()
+		post.TotalViewCount += 1;
+		post.AverageViewCount = recordService.GetQuery(e => e.PostId == pid).GroupBy(r => r.Time.Date).Select(g => g.Count()).DefaultIfEmpty().Average();
+		recordService.AddEntity(new PostVisitRecord()
+		{
+			IP = ip,
+			Referer = refer,
+			Location = ip.GetIPLocation(),
+			Time = DateTime.Now,
+			RequestUrl = url,
+			PostId = pid
+		});
+		var stats = recordStatsService.Get(e => e.PostId == pid && e.Date >= DateTime.Today);
+		if (stats != null)
+		{
+			stats.Count = recordService.Count(e => e.PostId == pid & e.Time >= DateTime.Today) + 1;
+			stats.UV = recordService.GetQuery(e => e.PostId == pid & e.Time >= DateTime.Today).Select(e => e.IP).Distinct().Count() + 1;
+		}
+		else
+		{
+			recordStatsService.AddEntity(new PostVisitRecordStats()
 			{
-				IP = ip,
-				Referer = refer,
-				Location = ip.GetIPLocation(),
-				Time = DateTime.Now,
-				RequestUrl = url,
+				Count = 1,
+				UV = 1,
+				Date = DateTime.Today,
 				PostId = pid
 			});
-			var stats = recordStatsService.Get(e => e.PostId == pid && e.Date >= DateTime.Today);
-			if (stats != null)
-			{
-				stats.Count = recordService.Count(e => e.PostId == pid & e.Time >= DateTime.Today) + 1;
-				stats.UV = recordService.GetQuery(e => e.PostId == pid & e.Time >= DateTime.Today).Select(e => e.IP).Distinct().Count() + 1;
-			}
-			else
-			{
-				recordStatsService.AddEntity(new PostVisitRecordStats()
-				{
-					Count = 1,
-					UV = 1,
-					Date = DateTime.Today,
-					PostId = pid
-				});
-			}
-
-			postService.SaveChanges();
 		}
 
-		/// <summary>
-		/// 每天的任务
-		/// </summary>
-		public void EverydayJob()
-		{
-			CommonHelper.IPErrorTimes.RemoveWhere(kv => kv.Value < 100); //将访客访问出错次数少于100的移开
-			var time = DateTime.Now.AddMonths(-1);
-			var searchDetailsService = _serviceScope.ServiceProvider.GetRequiredService<ISearchDetailsService>();
-			var advertisementService = _serviceScope.ServiceProvider.GetRequiredService<IAdvertisementService>();
-			var noticeService = _serviceScope.ServiceProvider.GetRequiredService<INoticeService>();
-			var postService = _serviceScope.ServiceProvider.GetRequiredService<IPostService>();
-			searchDetailsService.DeleteEntitySaved(s => s.SearchTime < time);
-			TrackData.DumpLog();
-			advertisementService.GetQuery(a => DateTime.Now >= a.ExpireTime).ExecuteUpdate(s => s.SetProperty(a => a.Status, Status.Unavailable));
-			noticeService.GetQuery(n => n.NoticeStatus == NoticeStatus.UnStart && n.StartTime < DateTime.Now).ExecuteUpdate(s => s.SetProperty(e => e.NoticeStatus, NoticeStatus.Normal).SetProperty(e => e.PostDate, DateTime.Now).SetProperty(e => e.ModifyDate, DateTime.Now));
-			noticeService.GetQuery(n => n.NoticeStatus == NoticeStatus.Normal && n.EndTime < DateTime.Now).ExecuteUpdate(s => s.SetProperty(e => e.NoticeStatus, NoticeStatus.Expired).SetProperty(e => e.ModifyDate, DateTime.Now));
-			postService.GetQuery(p => p.ExpireAt < DateTime.Now && p.Status == Status.Published).ExecuteUpdate(s => s.SetProperty(p => p.Status, Status.Takedown));
-		}
+		postService.SaveChanges();
+	}
 
-		/// <summary>
-		/// 每月的任务
-		/// </summary>
-		public void EverymonthJob()
-		{
-			var advertisementService = _serviceScope.ServiceProvider.GetRequiredService<IAdvertisementService>();
-			advertisementService.GetAll().ExecuteUpdate(s => s.SetProperty(a => a.DisplayCount, 0));
-		}
+	/// <summary>
+	/// 每天的任务
+	/// </summary>
+	public void EverydayJob()
+	{
+		CommonHelper.IPErrorTimes.RemoveWhere(kv => kv.Value < 100); //将访客访问出错次数少于100的移开
+		var time = DateTime.Now.AddMonths(-1);
+		var searchDetailsService = _serviceScope.ServiceProvider.GetRequiredService<ISearchDetailsService>();
+		var advertisementService = _serviceScope.ServiceProvider.GetRequiredService<IAdvertisementService>();
+		var noticeService = _serviceScope.ServiceProvider.GetRequiredService<INoticeService>();
+		var postService = _serviceScope.ServiceProvider.GetRequiredService<IPostService>();
+		searchDetailsService.DeleteEntitySaved(s => s.SearchTime < time);
+		TrackData.DumpLog();
+		advertisementService.GetQuery(a => DateTime.Now >= a.ExpireTime).ExecuteUpdate(s => s.SetProperty(a => a.Status, Status.Unavailable));
+		noticeService.GetQuery(n => n.NoticeStatus == NoticeStatus.UnStart && n.StartTime < DateTime.Now).ExecuteUpdate(s => s.SetProperty(e => e.NoticeStatus, NoticeStatus.Normal).SetProperty(e => e.PostDate, DateTime.Now).SetProperty(e => e.ModifyDate, DateTime.Now));
+		noticeService.GetQuery(n => n.NoticeStatus == NoticeStatus.Normal && n.EndTime < DateTime.Now).ExecuteUpdate(s => s.SetProperty(e => e.NoticeStatus, NoticeStatus.Expired).SetProperty(e => e.ModifyDate, DateTime.Now));
+		postService.GetQuery(p => p.ExpireAt < DateTime.Now && p.Status == Status.Published).ExecuteUpdate(s => s.SetProperty(p => p.Status, Status.Takedown));
+	}
+
+	/// <summary>
+	/// 每月的任务
+	/// </summary>
+	public void EverymonthJob()
+	{
+		var advertisementService = _serviceScope.ServiceProvider.GetRequiredService<IAdvertisementService>();
+		advertisementService.GetAll().ExecuteUpdate(s => s.SetProperty(a => a.DisplayCount, 0));
+	}
 
-		public void CheckAdvertisements()
+	public void CheckAdvertisements()
+	{
+		var advertisementService = _serviceScope.ServiceProvider.GetRequiredService<IAdvertisementService>();
+		var client = _httpClientFactory.CreateClient();
+		client.DefaultRequestHeaders.UserAgent.TryParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47");
+		client.DefaultRequestHeaders.Add("X-Forwarded-For", "114.114.114.114");
+		client.DefaultRequestHeaders.Add("X-Forwarded-Host", "114.114.114.114");
+		client.DefaultRequestHeaders.Add("X-Real-IP", "114.114.114.114");
+		client.DefaultRequestHeaders.Referrer = new Uri("https://baidu.com");
+		client.Timeout = TimeSpan.FromSeconds(10);
+		var baseAddr = bool.Parse(_configuration["Https:Enabled"]) ? $"https://127.1:{_configuration["Https:Port"]}" : $"http://127.1:{_configuration["Port"]}";
+		advertisementService.GetQuery(a => a.Status == Status.Available).AsParallel().ForAll(e =>
 		{
-			var advertisementService = _serviceScope.ServiceProvider.GetRequiredService<IAdvertisementService>();
-			var client = _httpClientFactory.CreateClient();
-			client.DefaultRequestHeaders.UserAgent.TryParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47");
-			client.DefaultRequestHeaders.Add("X-Forwarded-For", "114.114.114.114");
-			client.DefaultRequestHeaders.Add("X-Forwarded-Host", "114.114.114.114");
-			client.DefaultRequestHeaders.Add("X-Real-IP", "114.114.114.114");
-			client.DefaultRequestHeaders.Referrer = new Uri("https://baidu.com");
-			client.Timeout = TimeSpan.FromSeconds(10);
-			var baseAddr = bool.Parse(_configuration["Https:Enabled"]) ? $"https://127.1:{_configuration["Https:Port"]}" : $"http://127.1:{_configuration["Port"]}";
-			advertisementService.GetQuery(a => a.Status == Status.Available).AsParallel().ForAll(e =>
+			var url = e.Url;
+			if (e.Url.StartsWith("/"))
+			{
+				url = baseAddr + e.Url;
+			}
+
+			using var cts = new CancellationTokenSource(client.Timeout);
+			client.GetAsync(url, cts.Token).ContinueWith(t =>
 			{
-				var url = e.Url;
-				if (e.Url.StartsWith("/"))
+				if (t.IsCanceled || t.IsFaulted)
 				{
-					url = baseAddr + e.Url;
+					LogManager.Info($"广告【[{e.Id}] {e.Title}】因访问超时被自动下架!");
+					e.Status = Status.Unavailable;
 				}
 
-				using var cts = new CancellationTokenSource(client.Timeout);
-				client.GetAsync(url, cts.Token).ContinueWith(t =>
+				if (t.Result.StatusCode == HttpStatusCode.NotFound)
 				{
-					if (t.IsCanceled || t.IsFaulted)
-					{
-						LogManager.Info($"广告【[{e.Id}] {e.Title}】因访问超时被自动下架!");
-						e.Status = Status.Unavailable;
-					}
-
-					if (t.Result.StatusCode == HttpStatusCode.NotFound)
-					{
-						LogManager.Info($"广告【[{e.Id}] {e.Title}】因广告链接404被自动下架!");
-						e.Status = Status.Unavailable;
-					}
-				}).Wait();
-			});
-			advertisementService.SaveChanges();
-		}
+					LogManager.Info($"广告【[{e.Id}] {e.Title}】因广告链接404被自动下架!");
+					e.Status = Status.Unavailable;
+				}
+			}).Wait();
+		});
+		advertisementService.SaveChanges();
+	}
 
-		/// <summary>
-		/// 检查友链
-		/// </summary>
-		public void CheckLinks()
+	/// <summary>
+	/// 检查友链
+	/// </summary>
+	public void CheckLinks()
+	{
+		var client = _httpClientFactory.CreateClient();
+		client.DefaultRequestHeaders.UserAgent.TryParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47");
+		client.DefaultRequestHeaders.Add("X-Forwarded-For", "1.1.1.1");
+		client.DefaultRequestHeaders.Add("X-Forwarded-Host", "1.1.1.1");
+		client.DefaultRequestHeaders.Add("X-Real-IP", "1.1.1.1");
+		client.DefaultRequestHeaders.Referrer = new Uri("https://google.com");
+		client.Timeout = TimeSpan.FromSeconds(10);
+		var linksService = _serviceScope.ServiceProvider.GetRequiredService<ILinksService>();
+		linksService.GetQuery(l => !l.Except).AsParallel().ForAll(link =>
 		{
-			var client = _httpClientFactory.CreateClient();
-			client.DefaultRequestHeaders.UserAgent.TryParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47");
-			client.DefaultRequestHeaders.Add("X-Forwarded-For", "1.1.1.1");
-			client.DefaultRequestHeaders.Add("X-Forwarded-Host", "1.1.1.1");
-			client.DefaultRequestHeaders.Add("X-Real-IP", "1.1.1.1");
-			client.DefaultRequestHeaders.Referrer = new Uri("https://google.com");
-			client.Timeout = TimeSpan.FromSeconds(10);
-			var linksService = _serviceScope.ServiceProvider.GetRequiredService<ILinksService>();
-			linksService.GetQuery(l => !l.Except).AsParallel().ForAll(link =>
+			var prev = link.Status;
+			using var cts = new CancellationTokenSource(client.Timeout);
+			client.GetStringAsync(_configuration["HttpClientProxy:UriPrefix"] + link.Url, cts.Token).ContinueWith(t =>
 			{
-				var prev = link.Status;
-				using var cts = new CancellationTokenSource(client.Timeout);
-				client.GetStringAsync(_configuration["HttpClientProxy:UriPrefix"] + link.Url, cts.Token).ContinueWith(t =>
+				if (t.IsCanceled || t.IsFaulted)
 				{
-					if (t.IsCanceled || t.IsFaulted)
-					{
-						link.Status = Status.Unavailable;
-					}
-					else
-					{
-						link.Status = !t.Result.Contains(CommonHelper.SystemSettings["Domain"].Split("|")) ? Status.Unavailable : Status.Available;
-					}
-
-					if (link.Status != prev)
-					{
-						link.UpdateTime = DateTime.Now;
-					}
-				}).Wait();
-			});
-			linksService.SaveChanges();
-		}
+					link.Status = Status.Unavailable;
+				}
+				else
+				{
+					link.Status = !t.Result.Contains(CommonHelper.SystemSettings["Domain"].Split("|")) ? Status.Unavailable : Status.Available;
+				}
 
-		/// <summary>
-		/// 更新友链权重
-		/// </summary>
-		/// <param name="referer"></param>
-		/// <param name="ip"></param>
-		public void UpdateLinkWeight(string referer, string ip)
-		{
-			var linksService = _serviceScope.ServiceProvider.GetRequiredService<ILinksService>();
-			var loopbackService = _serviceScope.ServiceProvider.GetRequiredService<ILinkLoopbackService>();
-			var list = linksService.GetQuery(l => referer.Contains(l.UrlBase)).ToList();
-			foreach (var link in list)
-			{
-				link.Loopbacks.Add(new LinkLoopback()
+				if (link.Status != prev)
 				{
-					IP = ip,
-					Referer = referer,
-					Time = DateTime.Now
-				});
-			}
-			var time = DateTime.Now.AddMonths(-1);
-			loopbackService.GetQuery(b => b.Time < time).DeleteFromQuery();
-			linksService.SaveChanges();
-		}
+					link.UpdateTime = DateTime.Now;
+				}
+			}).Wait();
+		});
+		linksService.SaveChanges();
+	}
 
-		/// <summary>
-		/// 重建Lucene索引库
-		/// </summary>
-		public void CreateLuceneIndex()
+	/// <summary>
+	/// 更新友链权重
+	/// </summary>
+	/// <param name="referer"></param>
+	/// <param name="ip"></param>
+	public void UpdateLinkWeight(string referer, string ip)
+	{
+		var linksService = _serviceScope.ServiceProvider.GetRequiredService<ILinksService>();
+		var loopbackService = _serviceScope.ServiceProvider.GetRequiredService<ILinkLoopbackService>();
+		var list = linksService.GetQuery(l => referer.Contains(l.UrlBase)).ToList();
+		foreach (var link in list)
 		{
-			var searchEngine = _serviceScope.ServiceProvider.GetRequiredService<ISearchEngine<DataContext>>();
-			var postService = _serviceScope.ServiceProvider.GetRequiredService<IPostService>();
-			searchEngine.LuceneIndexer.DeleteAll();
-			searchEngine.CreateIndex(new List<string>()
+			link.Loopbacks.Add(new LinkLoopback()
 			{
-				nameof(DataContext.Post),
+				IP = ip,
+				Referer = referer,
+				Time = DateTime.Now
 			});
-			var list = postService.GetQuery(p => p.Status != Status.Published || p.LimitMode == RegionLimitMode.OnlyForSearchEngine).ToList();
-			searchEngine.LuceneIndexer.Delete(list);
 		}
+		var time = DateTime.Now.AddMonths(-1);
+		loopbackService.GetQuery(b => b.Time < time).DeleteFromQuery();
+		linksService.SaveChanges();
+	}
 
-		/// <summary>
-		/// 搜索统计
-		/// </summary>
-		public void StatisticsSearchKeywords()
+	/// <summary>
+	/// 重建Lucene索引库
+	/// </summary>
+	public void CreateLuceneIndex()
+	{
+		var searchEngine = _serviceScope.ServiceProvider.GetRequiredService<ISearchEngine<DataContext>>();
+		var postService = _serviceScope.ServiceProvider.GetRequiredService<IPostService>();
+		searchEngine.LuceneIndexer.DeleteAll();
+		searchEngine.CreateIndex(new List<string>()
 		{
-			var searchDetailsService = _serviceScope.ServiceProvider.GetRequiredService<ISearchDetailsService>();
-			_redisClient.Set("SearchRank:Month", searchDetailsService.GetRanks(DateTime.Today.AddMonths(-1)));
-			_redisClient.Set("SearchRank:Week", searchDetailsService.GetRanks(DateTime.Today.AddDays(-7)));
-			_redisClient.Set("SearchRank:Today", searchDetailsService.GetRanks(DateTime.Today));
-		}
+			nameof(DataContext.Post),
+		});
+		var list = postService.GetQuery(p => p.Status != Status.Published || p.LimitMode == RegionLimitMode.OnlyForSearchEngine).ToList();
+		searchEngine.LuceneIndexer.Delete(list);
+	}
 
-		/// <summary>
-		/// 释放
-		/// </summary>
-		/// <param name="disposing"></param>
-		public override void Dispose(bool disposing)
-		{
-			_serviceScope.Dispose();
-		}
+	/// <summary>
+	/// 搜索统计
+	/// </summary>
+	public void StatisticsSearchKeywords()
+	{
+		var searchDetailsService = _serviceScope.ServiceProvider.GetRequiredService<ISearchDetailsService>();
+		_redisClient.Set("SearchRank:Month", searchDetailsService.GetRanks(DateTime.Today.AddMonths(-1)));
+		_redisClient.Set("SearchRank:Week", searchDetailsService.GetRanks(DateTime.Today.AddDays(-7)));
+		_redisClient.Set("SearchRank:Today", searchDetailsService.GetRanks(DateTime.Today));
+	}
+
+	/// <summary>
+	/// 释放
+	/// </summary>
+	/// <param name="disposing"></param>
+	public override void Dispose(bool disposing)
+	{
+		_serviceScope.Dispose();
 	}
-}
+}

+ 20 - 21
src/Masuit.MyBlogs.Core/Extensions/Hangfire/HangfireJobInit.cs

@@ -1,24 +1,23 @@
 using Hangfire;
 
-namespace Masuit.MyBlogs.Core.Extensions.Hangfire
+namespace Masuit.MyBlogs.Core.Extensions.Hangfire;
+
+/// <summary>
+/// hangfire配置
+/// </summary>
+public static class HangfireJobInit
 {
-    /// <summary>
-    /// hangfire配置
-    /// </summary>
-    public static class HangfireJobInit
-    {
-        /// <summary>
-        /// hangfire初始化
-        /// </summary>
-        public static void Start()
-        {
-            RecurringJob.AddOrUpdate<IHangfireBackJob>(job => job.CheckLinks(), "0 */5 * * *"); //每5h检查友链
-            RecurringJob.AddOrUpdate<IHangfireBackJob>(job => job.CheckAdvertisements(), Cron.Daily); //每5h检查友链
-            RecurringJob.AddOrUpdate<IHangfireBackJob>(job => job.EverydayJob(), Cron.Daily(5), TimeZoneInfo.Local); //每天的任务
-            RecurringJob.AddOrUpdate<IHangfireBackJob>(job => job.CreateLuceneIndex(), Cron.Weekly(DayOfWeek.Monday, 5), TimeZoneInfo.Local); //每周的任务
-            RecurringJob.AddOrUpdate<IHangfireBackJob>(job => job.EverymonthJob(), Cron.Monthly(1, 0, 0), TimeZoneInfo.Local); //每月的任务
-            RecurringJob.AddOrUpdate<IHangfireBackJob>(job => job.StatisticsSearchKeywords(), Cron.Hourly); //每小时的任务
-            BackgroundJob.Enqueue<IHangfireBackJob>(job => job.StatisticsSearchKeywords());
-        }
-    }
-}
+	/// <summary>
+	/// hangfire初始化
+	/// </summary>
+	public static void Start()
+	{
+		RecurringJob.AddOrUpdate<IHangfireBackJob>(job => job.CheckLinks(), "0 */5 * * *"); //每5h检查友链
+		RecurringJob.AddOrUpdate<IHangfireBackJob>(job => job.CheckAdvertisements(), Cron.Daily); //每5h检查友链
+		RecurringJob.AddOrUpdate<IHangfireBackJob>(job => job.EverydayJob(), Cron.Daily(5), TimeZoneInfo.Local); //每天的任务
+		RecurringJob.AddOrUpdate<IHangfireBackJob>(job => job.CreateLuceneIndex(), Cron.Weekly(DayOfWeek.Monday, 5), TimeZoneInfo.Local); //每周的任务
+		RecurringJob.AddOrUpdate<IHangfireBackJob>(job => job.EverymonthJob(), Cron.Monthly(1, 0, 0), TimeZoneInfo.Local); //每月的任务
+		RecurringJob.AddOrUpdate<IHangfireBackJob>(job => job.StatisticsSearchKeywords(), Cron.Hourly); //每小时的任务
+		BackgroundJob.Enqueue<IHangfireBackJob>(job => job.StatisticsSearchKeywords());
+	}
+}

+ 52 - 57
src/Masuit.MyBlogs.Core/Extensions/Hangfire/IHangfireBackJob.cs

@@ -1,69 +1,64 @@
-using Masuit.MyBlogs.Core.Models.DTO;
-using Masuit.MyBlogs.Core.Models.Entity;
-using Masuit.MyBlogs.Core.Models.Enum;
+namespace Masuit.MyBlogs.Core.Extensions.Hangfire;
 
-namespace Masuit.MyBlogs.Core.Extensions.Hangfire
+/// <summary>
+/// hangfire后台任务
+/// </summary>
+public interface IHangfireBackJob
 {
-    /// <summary>
-    /// hangfire后台任务
-    /// </summary>
-    public interface IHangfireBackJob
-    {
-        /// <summary>
-        /// 登陆记录
-        /// </summary>
-        /// <param name="userInfo"></param>
-        /// <param name="ip"></param>
-        /// <param name="type"></param>
-        void LoginRecord(UserInfoDto userInfo, string ip, LoginType type);
+	/// <summary>
+	/// 登陆记录
+	/// </summary>
+	/// <param name="userInfo"></param>
+	/// <param name="ip"></param>
+	/// <param name="type"></param>
+	void LoginRecord(UserInfoDto userInfo, string ip, LoginType type);
 
-        /// <summary>
-        /// 文章定时发表
-        /// </summary>
-        /// <param name="p"></param>
-        void PublishPost(Post p);
+	/// <summary>
+	/// 文章定时发表
+	/// </summary>
+	/// <param name="p"></param>
+	void PublishPost(Post p);
 
-        /// <summary>
-        /// 文章访问记录
-        /// </summary>
-        /// <param name="pid"></param>
-        /// <param name="ip"></param>
-        /// <param name="refer"></param>
-        /// <param name="url"></param><param name="headers"></param>
-        void RecordPostVisit(int pid, string ip, string refer, string url);
+	/// <summary>
+	/// 文章访问记录
+	/// </summary>
+	/// <param name="pid"></param>
+	/// <param name="ip"></param>
+	/// <param name="refer"></param>
+	/// <param name="url"></param><param name="headers"></param>
+	void RecordPostVisit(int pid, string ip, string refer, string url);
 
-        /// <summary>
-        /// 每日任务
-        /// </summary>
-        void EverydayJob();
+	/// <summary>
+	/// 每日任务
+	/// </summary>
+	void EverydayJob();
 
-        /// <summary>
-        /// 每月的任务
-        /// </summary>
-        void EverymonthJob();
+	/// <summary>
+	/// 每月的任务
+	/// </summary>
+	void EverymonthJob();
 
-        /// <summary>
-        /// 友链检查
-        /// </summary>
-        void CheckLinks();
+	/// <summary>
+	/// 友链检查
+	/// </summary>
+	void CheckLinks();
 
-        void CheckAdvertisements();
+	void CheckAdvertisements();
 
-        /// <summary>
-        /// 更新友链权重
-        /// </summary>
-        /// <param name="referer"></param>
-        /// <param name="ip"></param>
-        void UpdateLinkWeight(string referer, string ip);
+	/// <summary>
+	/// 更新友链权重
+	/// </summary>
+	/// <param name="referer"></param>
+	/// <param name="ip"></param>
+	void UpdateLinkWeight(string referer, string ip);
 
-        /// <summary>
-        /// 重建Lucene索引库
-        /// </summary>
-        void CreateLuceneIndex();
+	/// <summary>
+	/// 重建Lucene索引库
+	/// </summary>
+	void CreateLuceneIndex();
 
-        /// <summary>
-        /// 搜索统计
-        /// </summary>
-        void StatisticsSearchKeywords();
-    }
+	/// <summary>
+	/// 搜索统计
+	/// </summary>
+	void StatisticsSearchKeywords();
 }

+ 181 - 182
src/Masuit.MyBlogs.Core/Extensions/MiddlewareExtension.cs

@@ -17,192 +17,191 @@ using System.Text.Encodings.Web;
 using System.Text.Unicode;
 using ConfigurationBuilder = CacheManager.Core.ConfigurationBuilder;
 
-namespace Masuit.MyBlogs.Core.Extensions
-{
-    public static class MiddlewareExtension
-    {
-        /// <summary>
-        /// 缓存配置
-        /// </summary>
-        /// <param name="services"></param>
-        /// <returns></returns>
-        public static IServiceCollection AddCacheConfig(this IServiceCollection services)
-        {
-            var jss = new JsonSerializerSettings
-            {
-                NullValueHandling = NullValueHandling.Ignore,
-                ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
-                TypeNameHandling = TypeNameHandling.Auto
-            };
-            services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>));
-            services.AddSingleton(new ConfigurationBuilder().WithRedisConfiguration("redis", AppConfig.Redis).WithJsonSerializer(jss, jss).WithMaxRetries(5).WithRetryTimeout(100).WithRedisCacheHandle("redis").WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(5))
-                .Build());
-            return services;
-        }
+namespace Masuit.MyBlogs.Core.Extensions;
 
-        /// <summary>
-        /// automapper
-        /// </summary>
-        /// <param name="services"></param>
-        /// <returns></returns>
-        public static IServiceCollection AddMapper(this IServiceCollection services)
-        {
-            var mc = new MapperConfiguration(cfg => cfg.AddExpressionMapping().AddProfile(new MappingProfile()));
-            services.AddAutoMapper(cfg => cfg.AddExpressionMapping().AddProfile(new MappingProfile()), Assembly.GetExecutingAssembly());
-            services.AddSingleton(mc);
-            return services;
-        }
+public static class MiddlewareExtension
+{
+	/// <summary>
+	/// 缓存配置
+	/// </summary>
+	/// <param name="services"></param>
+	/// <returns></returns>
+	public static IServiceCollection AddCacheConfig(this IServiceCollection services)
+	{
+		var jss = new JsonSerializerSettings
+		{
+			NullValueHandling = NullValueHandling.Ignore,
+			ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
+			TypeNameHandling = TypeNameHandling.Auto
+		};
+		services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>));
+		services.AddSingleton(new ConfigurationBuilder().WithRedisConfiguration("redis", AppConfig.Redis).WithJsonSerializer(jss, jss).WithMaxRetries(5).WithRetryTimeout(100).WithRedisCacheHandle("redis").WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(5))
+			.Build());
+		return services;
+	}
 
-        /// <summary>
-        /// mvc
-        /// </summary>
-        /// <param name="services"></param>
-        /// <returns></returns>
-        public static IServiceCollection AddMyMvc(this IServiceCollection services)
-        {
-            services.AddMvc(options =>
-            {
-                options.ModelBinderProviders.InsertBodyOrDefaultBinding();
-                options.ReturnHttpNotAcceptable = true;
-                options.Filters.Add<ExceptionFilter>();
-            }).AddNewtonsoftJson(options =>
-            {
-                options.SerializerSettings.ContractResolver = new DefaultContractResolver();
-                options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local;
-            }).AddControllersAsServices().AddViewComponentsAsServices().AddTagHelpersAsServices(); // MVC
-            services.Configure<WebEncoderOptions>(options =>
-            {
-                options.TextEncoderSettings = new TextEncoderSettings(UnicodeRanges.All);
-            }); //解决razor视图中中文被编码的问题
-            return services;
-        }
+	/// <summary>
+	/// automapper
+	/// </summary>
+	/// <param name="services"></param>
+	/// <returns></returns>
+	public static IServiceCollection AddMapper(this IServiceCollection services)
+	{
+		var mc = new MapperConfiguration(cfg => cfg.AddExpressionMapping().AddProfile(new MappingProfile()));
+		services.AddAutoMapper(cfg => cfg.AddExpressionMapping().AddProfile(new MappingProfile()), Assembly.GetExecutingAssembly());
+		services.AddSingleton(mc);
+		return services;
+	}
 
-        /// <summary>
-        /// 输出缓存
-        /// </summary>
-        /// <param name="services"></param>
-        /// <returns></returns>
-        public static IServiceCollection AddResponseCache(this IServiceCollection services)
-        {
-            services.AddResponseCaching(); //注入响应缓存
-            services.Configure<BrotliCompressionProviderOptions>(options =>
-            {
-                options.Level = CompressionLevel.Fastest;
-            }).Configure<GzipCompressionProviderOptions>(options =>
-            {
-                options.Level = CompressionLevel.Optimal;
-            }).AddResponseCompression(options =>
-            {
-                options.EnableForHttps = true;
-                options.Providers.Add<BrotliCompressionProvider>();
-                options.Providers.Add<GzipCompressionProvider>();
-                options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
-                {
-                    "text/html; charset=utf-8",
-                    "application/xhtml+xml",
-                    "application/atom+xml",
-                    "image/svg+xml"
-                });
-            });
-            return services;
-        }
+	/// <summary>
+	/// mvc
+	/// </summary>
+	/// <param name="services"></param>
+	/// <returns></returns>
+	public static IServiceCollection AddMyMvc(this IServiceCollection services)
+	{
+		services.AddMvc(options =>
+		{
+			options.ModelBinderProviders.InsertBodyOrDefaultBinding();
+			options.ReturnHttpNotAcceptable = true;
+			options.Filters.Add<ExceptionFilter>();
+		}).AddNewtonsoftJson(options =>
+		{
+			options.SerializerSettings.ContractResolver = new DefaultContractResolver();
+			options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local;
+		}).AddControllersAsServices().AddViewComponentsAsServices().AddTagHelpersAsServices(); // MVC
+		services.Configure<WebEncoderOptions>(options =>
+		{
+			options.TextEncoderSettings = new TextEncoderSettings(UnicodeRanges.All);
+		}); //解决razor视图中中文被编码的问题
+		return services;
+	}
 
-        /// <summary>
-        /// 添加静态资源打包
-        /// </summary>
-        /// <param name="app"></param>
-        /// <returns></returns>
-        public static IApplicationBuilder UseBundles(this IApplicationBuilder app)
-        {
-            app.UseBundling(bundles =>
-            {
-                bundles.AddCss("/main.css")
-                    .Include("/fonts/icomoon.css")
-                    .Include("/Content/jquery.paging.css")
-                    .Include("/Content/common/reset.css")
-                    .Include("/Content/common/loading.css")
-                    .Include("/Content/common/style.css")
-                    .Include("/Content/common/articlestyle.css")
-                    .Include("/Content/common/leaderboard.css")
-                    .Include("/Assets/breadcrumb/style.css")
-                    .Include("/Assets/nav/css/style.css");
-                bundles.AddCss("/filemanager.css")
-                    .Include("/Content/bootswatch.min.css")
-                    .Include("/fonts/icomoon.css")
-                    .Include("/ng-views/filemanager/css/animations.css")
-                    .Include("/ng-views/filemanager/css/dialogs.css")
-                    .Include("/ng-views/filemanager/css/main.css")
-                    .Include("/Content/common/loading.min.css");
-                bundles.AddCss("/dashboard.css")
-                    .Include("/fonts/icomoon.css")
-                    .Include("/Assets/fileupload/filestyle.css")
-                    .Include("/Content/common/loading.min.css")
-                    .Include("/Content/checkbox.min.css")
-                    .Include("/ng-views/css/app.css");
-                bundles.AddCss("/article.css")
-                    .Include("/Assets/jquery.tocify/jquery.tocify.css")
-                    .Include("/Assets/UEditor/third-party/SyntaxHighlighter/styles/shCore.css")
-                    .Include("/Assets/highlight/css/highlight.css");
+	/// <summary>
+	/// 输出缓存
+	/// </summary>
+	/// <param name="services"></param>
+	/// <returns></returns>
+	public static IServiceCollection AddResponseCache(this IServiceCollection services)
+	{
+		services.AddResponseCaching(); //注入响应缓存
+		services.Configure<BrotliCompressionProviderOptions>(options =>
+		{
+			options.Level = CompressionLevel.Fastest;
+		}).Configure<GzipCompressionProviderOptions>(options =>
+		{
+			options.Level = CompressionLevel.Optimal;
+		}).AddResponseCompression(options =>
+		{
+			options.EnableForHttps = true;
+			options.Providers.Add<BrotliCompressionProvider>();
+			options.Providers.Add<GzipCompressionProvider>();
+			options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
+			{
+				"text/html; charset=utf-8",
+				"application/xhtml+xml",
+				"application/atom+xml",
+				"image/svg+xml"
+			});
+		});
+		return services;
+	}
 
-                bundles.AddJs("/main.js")
-                    .Include("/Scripts/jquery.query.js")
-                    .Include("/Scripts/jquery.paging.js")
-                    .Include("/Scripts/ripplet.js")
-                    .Include("/Scripts/global/functions.js")
-                    .Include("/Scripts/global/scripts.js")
-                    .Include("/Scripts/platform.js")
-                    .Include("/Assets/newsbox/jquery.bootstrap.newsbox.js")
-                    .Include("/Assets/tagcloud/js/tagcloud.js")
-                    .Include("/Assets/scrolltop/js/scrolltop.js")
-                    .Include("/Assets/nav/js/main.js");
-                bundles.AddJs("/filemanager.js")
-                    .Include("/Scripts/ng-file-upload.min.js")
-                    .Include("/ng-views/filemanager/js/app.js")
-                    .Include("/ng-views/filemanager/js/directives/directives.js")
-                    .Include("/ng-views/filemanager/js/filters/filters.js")
-                    .Include("/ng-views/filemanager/js/providers/config.js")
-                    .Include("/ng-views/filemanager/js/entities/chmod.js")
-                    .Include("/ng-views/filemanager/js/entities/item.js")
-                    .Include("/ng-views/filemanager/js/services/apihandler.js")
-                    .Include("/ng-views/filemanager/js/services/apimiddleware.js")
-                    .Include("/ng-views/filemanager/js/services/filenavigator.js")
-                    .Include("/ng-views/filemanager/js/providers/translations.js")
-                    .Include("/ng-views/filemanager/js/controllers/main.js")
-                    .Include("/ng-views/filemanager/js/controllers/selector-controller.js");
-                bundles.AddJs("/article.js")
-                    .Include("/Assets/UEditor/third-party/SyntaxHighlighter/scripts/shCore.js")
-                    .Include("/Assets/UEditor/third-party/SyntaxHighlighter/scripts/bundle.min.js")
-                    .Include("/Assets/jquery.tocify/jquery.tocify.js")
-                    .Include("/Scripts/global/article.js");
-            });
-            return app;
-        }
-    }
+	/// <summary>
+	/// 添加静态资源打包
+	/// </summary>
+	/// <param name="app"></param>
+	/// <returns></returns>
+	public static IApplicationBuilder UseBundles(this IApplicationBuilder app)
+	{
+		app.UseBundling(bundles =>
+		{
+			bundles.AddCss("/main.css")
+				.Include("/fonts/icomoon.css")
+				.Include("/Content/jquery.paging.css")
+				.Include("/Content/common/reset.css")
+				.Include("/Content/common/loading.css")
+				.Include("/Content/common/style.css")
+				.Include("/Content/common/articlestyle.css")
+				.Include("/Content/common/leaderboard.css")
+				.Include("/Assets/breadcrumb/style.css")
+				.Include("/Assets/nav/css/style.css");
+			bundles.AddCss("/filemanager.css")
+				.Include("/Content/bootswatch.min.css")
+				.Include("/fonts/icomoon.css")
+				.Include("/ng-views/filemanager/css/animations.css")
+				.Include("/ng-views/filemanager/css/dialogs.css")
+				.Include("/ng-views/filemanager/css/main.css")
+				.Include("/Content/common/loading.min.css");
+			bundles.AddCss("/dashboard.css")
+				.Include("/fonts/icomoon.css")
+				.Include("/Assets/fileupload/filestyle.css")
+				.Include("/Content/common/loading.min.css")
+				.Include("/Content/checkbox.min.css")
+				.Include("/ng-views/css/app.css");
+			bundles.AddCss("/article.css")
+				.Include("/Assets/jquery.tocify/jquery.tocify.css")
+				.Include("/Assets/UEditor/third-party/SyntaxHighlighter/styles/shCore.css")
+				.Include("/Assets/highlight/css/highlight.css");
 
-    public class ExceptionFilter : IExceptionFilter
-    {
-        public void OnException(ExceptionContext context)
-        {
-            if (context.Exception is NotFoundException)
-            {
-                context.HttpContext.Response.StatusCode = 404;
-                string accept = context.HttpContext.Request.Headers[HeaderNames.Accept] + "";
-                context.Result = true switch
-                {
-                    _ when accept.StartsWith("image") => new VirtualFileResult("/Assets/images/404/4044.jpg", ContentType.Jpeg),
-                    _ when context.HttpContext.Request.HasJsonContentType() || context.HttpContext.Request.Method == HttpMethods.Post => new JsonResult(new
-                    {
-                        StatusCode = 404,
-                        Success = false,
-                        Message = "页面未找到!"
-                    }),
-                    _ => new ViewResult()
-                    {
-                        ViewName = "/Views/Error/Index.cshtml"
-                    }
-                };
-                context.ExceptionHandled = true;
-            }
-        }
-    }
+			bundles.AddJs("/main.js")
+				.Include("/Scripts/jquery.query.js")
+				.Include("/Scripts/jquery.paging.js")
+				.Include("/Scripts/ripplet.js")
+				.Include("/Scripts/global/functions.js")
+				.Include("/Scripts/global/scripts.js")
+				.Include("/Scripts/platform.js")
+				.Include("/Assets/newsbox/jquery.bootstrap.newsbox.js")
+				.Include("/Assets/tagcloud/js/tagcloud.js")
+				.Include("/Assets/scrolltop/js/scrolltop.js")
+				.Include("/Assets/nav/js/main.js");
+			bundles.AddJs("/filemanager.js")
+				.Include("/Scripts/ng-file-upload.min.js")
+				.Include("/ng-views/filemanager/js/app.js")
+				.Include("/ng-views/filemanager/js/directives/directives.js")
+				.Include("/ng-views/filemanager/js/filters/filters.js")
+				.Include("/ng-views/filemanager/js/providers/config.js")
+				.Include("/ng-views/filemanager/js/entities/chmod.js")
+				.Include("/ng-views/filemanager/js/entities/item.js")
+				.Include("/ng-views/filemanager/js/services/apihandler.js")
+				.Include("/ng-views/filemanager/js/services/apimiddleware.js")
+				.Include("/ng-views/filemanager/js/services/filenavigator.js")
+				.Include("/ng-views/filemanager/js/providers/translations.js")
+				.Include("/ng-views/filemanager/js/controllers/main.js")
+				.Include("/ng-views/filemanager/js/controllers/selector-controller.js");
+			bundles.AddJs("/article.js")
+				.Include("/Assets/UEditor/third-party/SyntaxHighlighter/scripts/shCore.js")
+				.Include("/Assets/UEditor/third-party/SyntaxHighlighter/scripts/bundle.min.js")
+				.Include("/Assets/jquery.tocify/jquery.tocify.js")
+				.Include("/Scripts/global/article.js");
+		});
+		return app;
+	}
 }
+
+public class ExceptionFilter : IExceptionFilter
+{
+	public void OnException(ExceptionContext context)
+	{
+		if (context.Exception is NotFoundException)
+		{
+			context.HttpContext.Response.StatusCode = 404;
+			string accept = context.HttpContext.Request.Headers[HeaderNames.Accept] + "";
+			context.Result = true switch
+			{
+				_ when accept.StartsWith("image") => new VirtualFileResult("/Assets/images/404/4044.jpg", ContentType.Jpeg),
+				_ when context.HttpContext.Request.HasJsonContentType() || context.HttpContext.Request.Method == HttpMethods.Post => new JsonResult(new
+				{
+					StatusCode = 404,
+					Success = false,
+					Message = "页面未找到!"
+				}),
+				_ => new ViewResult()
+				{
+					ViewName = "/Views/Error/Index.cshtml"
+				}
+			};
+			context.ExceptionHandled = true;
+		}
+	}
+}

+ 0 - 5
src/Masuit.MyBlogs.Core/Extensions/MyAuthorizeAttribute.cs

@@ -1,9 +1,4 @@
 using Masuit.MyBlogs.Core.Configs;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.DTO;
-using Masuit.MyBlogs.Core.Models.ViewModel;
-using Masuit.Tools.Core.Net;
-using Masuit.Tools.Security;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.Filters;

+ 6 - 7
src/Masuit.MyBlogs.Core/Extensions/NotFoundException.cs

@@ -1,9 +1,8 @@
-namespace Masuit.MyBlogs.Core.Extensions
+namespace Masuit.MyBlogs.Core.Extensions;
+
+public class NotFoundException : Exception
 {
-    public class NotFoundException : Exception
-    {
-        public NotFoundException(string msg) : base(msg)
-        {
-        }
-    }
+	public NotFoundException(string msg) : base(msg)
+	{
+	}
 }

+ 0 - 1
src/Masuit.MyBlogs.Core/Extensions/TranslateMiddleware.cs

@@ -1,5 +1,4 @@
 using Masuit.MyBlogs.Core.Common;
-using Masuit.Tools;
 using Masuit.Tools.AspNetCore.Mime;
 using Microsoft.International.Converters.TraditionalChineseToSimplifiedConverter;
 using System.Text;

+ 21 - 22
src/Masuit.MyBlogs.Core/Extensions/UEditor/ConfigHandler.cs

@@ -1,25 +1,24 @@
-namespace Masuit.MyBlogs.Core.Extensions.UEditor
+namespace Masuit.MyBlogs.Core.Extensions.UEditor;
+
+/// <summary>
+/// Config 的摘要说明
+/// </summary>
+public class ConfigHandler : Handler
 {
-    /// <summary>
-    /// Config 的摘要说明
-    /// </summary>
-    public class ConfigHandler : Handler
-    {
-        /// <summary>
-        /// 
-        /// </summary>
-        /// <param name="context"></param>
-        public ConfigHandler(HttpContext context) : base(context)
-        {
-        }
+	/// <summary>
+	/// 
+	/// </summary>
+	/// <param name="context"></param>
+	public ConfigHandler(HttpContext context) : base(context)
+	{
+	}
 
-        /// <summary>
-        /// 
-        /// </summary>
-        /// <returns></returns>
-        public override Task<string> Process()
-        {
-            return Task.FromResult(WriteJson(UeditorConfig.Items));
-        }
-    }
+	/// <summary>
+	/// 
+	/// </summary>
+	/// <returns></returns>
+	public override Task<string> Process()
+	{
+		return Task.FromResult(WriteJson(UeditorConfig.Items));
+	}
 }

+ 93 - 95
src/Masuit.MyBlogs.Core/Extensions/UEditor/CrawlerHandler.cs

@@ -1,5 +1,4 @@
 using Masuit.MyBlogs.Core.Common;
-using Masuit.Tools;
 using Masuit.Tools.AspNetCore.Mime;
 using Masuit.Tools.Logging;
 using SixLabors.ImageSharp;
@@ -7,127 +6,126 @@ using System.Diagnostics;
 using System.Net;
 using System.Text.RegularExpressions;
 
-namespace Masuit.MyBlogs.Core.Extensions.UEditor
+namespace Masuit.MyBlogs.Core.Extensions.UEditor;
+
+/// <summary>
+/// Crawler 的摘要说明
+/// </summary>
+public class CrawlerHandler : Handler
 {
-	/// <summary>
-	/// Crawler 的摘要说明
-	/// </summary>
-	public class CrawlerHandler : Handler
-	{
-		private readonly HttpClient _httpClient;
-		private readonly IConfiguration _configuration;
+	private readonly HttpClient _httpClient;
+	private readonly IConfiguration _configuration;
 
-		public CrawlerHandler(HttpContext context) : base(context)
-		{
-			_httpClient = context.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient();
-			_configuration = context.RequestServices.GetRequiredService<IConfiguration>();
-		}
+	public CrawlerHandler(HttpContext context) : base(context)
+	{
+		_httpClient = context.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient();
+		_configuration = context.RequestServices.GetRequiredService<IConfiguration>();
+	}
 
-		public override async Task<string> Process()
+	public override async Task<string> Process()
+	{
+		var form = await Request.ReadFormAsync();
+		string[] sources = form["source[]"];
+		if (sources?.Length > 0 || sources?.Length <= 10)
 		{
-			var form = await Request.ReadFormAsync();
-			string[] sources = form["source[]"];
-			if (sources?.Length > 0 || sources?.Length <= 10)
+			using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+			return WriteJson(new
 			{
-				using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
-				return WriteJson(new
+				state = "SUCCESS",
+				list = (await sources.SelectAsync(s =>
 				{
-					state = "SUCCESS",
-					list = (await sources.SelectAsync(s =>
+					return new Crawler(s, _httpClient, _configuration, Context).Fetch(cts.Token).ContinueWith(t => new
 					{
-						return new Crawler(s, _httpClient, _configuration, Context).Fetch(cts.Token).ContinueWith(t => new
-						{
-							state = t.Result.State,
-							source = t.Result.SourceUrl,
-							url = t.Result.ServerUrl
-						});
-					}))
-				});
-			}
-
-			return WriteJson(new
-			{
-				state = "参数错误:没有指定抓取源"
+						state = t.Result.State,
+						source = t.Result.SourceUrl,
+						url = t.Result.ServerUrl
+					});
+				}))
 			});
 		}
+
+		return WriteJson(new
+		{
+			state = "参数错误:没有指定抓取源"
+		});
 	}
+}
 
-	public class Crawler
-	{
-		public string SourceUrl { get; set; }
+public class Crawler
+{
+	public string SourceUrl { get; set; }
+
+	public string ServerUrl { get; set; }
 
-		public string ServerUrl { get; set; }
+	public string State { get; set; }
 
-		public string State { get; set; }
+	private readonly HttpClient _httpClient;
+	private readonly IConfiguration _configuration;
+	private readonly HttpContext _httpContext;
+	public Crawler(string sourceUrl, HttpClient httpClient, IConfiguration configuration, HttpContext httpContext)
+	{
+		SourceUrl = sourceUrl;
+		_httpClient = httpClient;
+		_configuration = configuration;
+		_httpContext = httpContext;
+	}
 
-		private readonly HttpClient _httpClient;
-		private readonly IConfiguration _configuration;
-		private readonly HttpContext _httpContext;
-		public Crawler(string sourceUrl, HttpClient httpClient, IConfiguration configuration, HttpContext httpContext)
+	public async Task<Crawler> Fetch(CancellationToken token)
+	{
+		if (!SourceUrl.IsExternalAddress())
 		{
-			SourceUrl = sourceUrl;
-			_httpClient = httpClient;
-			_configuration = configuration;
-			_httpContext = httpContext;
+			State = "INVALID_URL";
+			return this;
 		}
-
-		public async Task<Crawler> Fetch(CancellationToken token)
+		try
 		{
-			if (!SourceUrl.IsExternalAddress())
+			_httpClient.DefaultRequestHeaders.Referrer = new Uri(SourceUrl);
+			using var response = await _httpClient.GetAsync(_configuration["HttpClientProxy:UriPrefix"] + SourceUrl);
+			if (response.StatusCode != HttpStatusCode.OK)
 			{
-				State = "INVALID_URL";
+				State = "远程地址返回了错误的状态吗:" + response.StatusCode;
 				return this;
 			}
-			try
-			{
-				_httpClient.DefaultRequestHeaders.Referrer = new Uri(SourceUrl);
-				using var response = await _httpClient.GetAsync(_configuration["HttpClientProxy:UriPrefix"] + SourceUrl);
-				if (response.StatusCode != HttpStatusCode.OK)
-				{
-					State = "远程地址返回了错误的状态吗:" + response.StatusCode;
-					return this;
-				}
-
-				ServerUrl = PathFormatter.Format(Path.GetFileNameWithoutExtension(SourceUrl), CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload") + UeditorConfig.GetString("catcherPathFormat")) + MimeMapper.ExtTypes[response.Content.Headers.ContentType?.MediaType ?? "image/jpeg"];
-				var stream = await response.Content.ReadAsStreamAsync();
-				var format = await Image.DetectFormatAsync(stream).ContinueWith(t => t.IsCompletedSuccessfully ? t.Result : null);
-				stream.Position = 0;
-				if (format != null)
-				{
-					ServerUrl = ServerUrl.Replace(Path.GetExtension(ServerUrl), "." + format.Name.ToLower());
-					if (!Regex.IsMatch(format.Name, "JPEG|PNG|Webp|GIF", RegexOptions.IgnoreCase))
-					{
-						using var image = await Image.LoadAsync(stream);
-						var memoryStream = new MemoryStream();
-						await image.SaveAsJpegAsync(memoryStream);
-						await stream.DisposeAsync();
-						stream = memoryStream;
-						ServerUrl = ServerUrl.Replace(Path.GetExtension(ServerUrl), ".jpg");
-					}
-				}
 
-				var savePath = AppContext.BaseDirectory + "wwwroot" + ServerUrl;
-				var (url, success) = await _httpContext.RequestServices.GetRequiredService<ImagebedClient>().UploadImage(stream, savePath, token);
-				if (success)
-				{
-					ServerUrl = url;
-				}
-				else
+			ServerUrl = PathFormatter.Format(Path.GetFileNameWithoutExtension(SourceUrl), CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload") + UeditorConfig.GetString("catcherPathFormat")) + MimeMapper.ExtTypes[response.Content.Headers.ContentType?.MediaType ?? "image/jpeg"];
+			var stream = await response.Content.ReadAsStreamAsync();
+			var format = await Image.DetectFormatAsync(stream).ContinueWith(t => t.IsCompletedSuccessfully ? t.Result : null);
+			stream.Position = 0;
+			if (format != null)
+			{
+				ServerUrl = ServerUrl.Replace(Path.GetExtension(ServerUrl), "." + format.Name.ToLower());
+				if (!Regex.IsMatch(format.Name, "JPEG|PNG|Webp|GIF", RegexOptions.IgnoreCase))
 				{
-					Directory.CreateDirectory(Path.GetDirectoryName(savePath));
-					await File.WriteAllBytesAsync(savePath, await stream.ToArrayAsync());
+					using var image = await Image.LoadAsync(stream);
+					var memoryStream = new MemoryStream();
+					await image.SaveAsJpegAsync(memoryStream);
+					await stream.DisposeAsync();
+					stream = memoryStream;
+					ServerUrl = ServerUrl.Replace(Path.GetExtension(ServerUrl), ".jpg");
 				}
+			}
 
-				await stream.DisposeAsync();
-				State = "SUCCESS";
+			var savePath = AppContext.BaseDirectory + "wwwroot" + ServerUrl;
+			var (url, success) = await _httpContext.RequestServices.GetRequiredService<ImagebedClient>().UploadImage(stream, savePath, token);
+			if (success)
+			{
+				ServerUrl = url;
 			}
-			catch (Exception e)
+			else
 			{
-				State = "抓取错误:" + e.Message;
-				LogManager.Error(e.Demystify());
+				Directory.CreateDirectory(Path.GetDirectoryName(savePath));
+				await File.WriteAllBytesAsync(savePath, await stream.ToArrayAsync());
 			}
 
-			return this;
+			await stream.DisposeAsync();
+			State = "SUCCESS";
+		}
+		catch (Exception e)
+		{
+			State = "抓取错误:" + e.Message;
+			LogManager.Error(e.Demystify());
 		}
+
+		return this;
 	}
-}
+}

+ 23 - 25
src/Masuit.MyBlogs.Core/Extensions/UEditor/Handler.cs

@@ -1,32 +1,30 @@
 using Newtonsoft.Json;
 
-namespace Masuit.MyBlogs.Core.Extensions.UEditor
+namespace Masuit.MyBlogs.Core.Extensions.UEditor;
+
+/// <summary>
+/// Handler 的摘要说明
+/// </summary>
+public abstract class Handler
 {
-    /// <summary>
-    /// Handler 的摘要说明
-    /// </summary>
-    public abstract class Handler
-    {
-        protected Handler(HttpContext context)
-        {
-            this.Request = context.Request;
-            this.Response = context.Response;
-            this.Context = context;
-            //this.Server = context.Server;
-        }
+	protected Handler(HttpContext context)
+	{
+		this.Request = context.Request;
+		this.Response = context.Response;
+		this.Context = context;
+		//this.Server = context.Server;
+	}
 
-        public abstract Task<string> Process();
+	public abstract Task<string> Process();
 
-        protected string WriteJson(object response)
-        {
-            string jsonpCallback = Request.Query["callback"];
-            string json = JsonConvert.SerializeObject(response);
-            return string.IsNullOrWhiteSpace(jsonpCallback) ? json : $"{jsonpCallback}({json});";
-        }
+	protected string WriteJson(object response)
+	{
+		string jsonpCallback = Request.Query["callback"];
+		string json = JsonConvert.SerializeObject(response);
+		return string.IsNullOrWhiteSpace(jsonpCallback) ? json : $"{jsonpCallback}({json});";
+	}
 
-        public HttpRequest Request { get; }
-        public HttpResponse Response { get; }
-        public HttpContext Context { get; }
-        //public HttpServerUtility Server { get; private set; }
-    }
+	public HttpRequest Request { get; }
+	public HttpResponse Response { get; }
+	public HttpContext Context { get; }
 }

+ 86 - 87
src/Masuit.MyBlogs.Core/Extensions/UEditor/ListFileManager.cs

@@ -1,94 +1,93 @@
-namespace Masuit.MyBlogs.Core.Extensions.UEditor
+namespace Masuit.MyBlogs.Core.Extensions.UEditor;
+
+/// <summary>
+/// FileManager 的摘要说明
+/// </summary>
+public class ListFileManager : Handler
 {
-    /// <summary>
-    /// FileManager 的摘要说明
-    /// </summary>
-    public class ListFileManager : Handler
-    {
-        private enum ResultState
-        {
-            Success,
-            InvalidParam,
-            AuthorizError,
-            IOError,
-            PathNotFound
-        }
+	private enum ResultState
+	{
+		Success,
+		InvalidParam,
+		AuthorizError,
+		IOError,
+		PathNotFound
+	}
 
-        private int _start;
-        private int _size;
-        private int _total;
-        private ResultState _state;
-        private readonly string _pathToList;
-        private string[] _fileList;
-        private readonly string[] _searchExtensions;
+	private int _start;
+	private int _size;
+	private int _total;
+	private ResultState _state;
+	private readonly string _pathToList;
+	private string[] _fileList;
+	private readonly string[] _searchExtensions;
 
-        public ListFileManager(HttpContext context, string pathToList, string[] searchExtensions) : base(context)
-        {
-            _searchExtensions = searchExtensions.Select(x => x.ToLower()).ToArray();
-            _pathToList = pathToList;
-        }
+	public ListFileManager(HttpContext context, string pathToList, string[] searchExtensions) : base(context)
+	{
+		_searchExtensions = searchExtensions.Select(x => x.ToLower()).ToArray();
+		_pathToList = pathToList;
+	}
 
-        public override Task<string> Process()
-        {
-            try
-            {
-                _start = string.IsNullOrEmpty(Request.Query["start"]) ? 0 : Convert.ToInt32(Request.Query["start"]);
-                _size = string.IsNullOrEmpty(Request.Query["size"]) ? UeditorConfig.GetInt("imageManagerListSize") : Convert.ToInt32(Request.Query["size"]);
-            }
-            catch (FormatException)
-            {
-                _state = ResultState.InvalidParam;
-                return Task.FromResult(WriteResult());
-            }
-            var buildingList = new List<string>();
-            try
-            {
-                var localPath = AppContext.BaseDirectory + "wwwroot" + _pathToList;
-                buildingList.AddRange(Directory.GetFiles(localPath, "*", SearchOption.AllDirectories).Where(x => _searchExtensions.Contains(Path.GetExtension(x).ToLower())).Select(x => _pathToList + x.Substring(localPath.Length).Replace("\\", "/")));
-                _total = buildingList.Count;
-                _fileList = buildingList.OrderBy(x => x).Skip(_start).Take(_size).ToArray();
-            }
-            catch (UnauthorizedAccessException)
-            {
-                _state = ResultState.AuthorizError;
-            }
-            catch (DirectoryNotFoundException)
-            {
-                _state = ResultState.PathNotFound;
-            }
-            catch (IOException)
-            {
-                _state = ResultState.IOError;
-            }
-            return Task.FromResult(WriteResult());
-        }
+	public override Task<string> Process()
+	{
+		try
+		{
+			_start = string.IsNullOrEmpty(Request.Query["start"]) ? 0 : Convert.ToInt32(Request.Query["start"]);
+			_size = string.IsNullOrEmpty(Request.Query["size"]) ? UeditorConfig.GetInt("imageManagerListSize") : Convert.ToInt32(Request.Query["size"]);
+		}
+		catch (FormatException)
+		{
+			_state = ResultState.InvalidParam;
+			return Task.FromResult(WriteResult());
+		}
+		var buildingList = new List<string>();
+		try
+		{
+			var localPath = AppContext.BaseDirectory + "wwwroot" + _pathToList;
+			buildingList.AddRange(Directory.GetFiles(localPath, "*", SearchOption.AllDirectories).Where(x => _searchExtensions.Contains(Path.GetExtension(x).ToLower())).Select(x => _pathToList + x.Substring(localPath.Length).Replace("\\", "/")));
+			_total = buildingList.Count;
+			_fileList = buildingList.OrderBy(x => x).Skip(_start).Take(_size).ToArray();
+		}
+		catch (UnauthorizedAccessException)
+		{
+			_state = ResultState.AuthorizError;
+		}
+		catch (DirectoryNotFoundException)
+		{
+			_state = ResultState.PathNotFound;
+		}
+		catch (IOException)
+		{
+			_state = ResultState.IOError;
+		}
+		return Task.FromResult(WriteResult());
+	}
 
-        private string WriteResult()
-        {
-            return WriteJson(new
-            {
-                state = GetStateString(),
-                list = _fileList?.Select(x => new
-                {
-                    url = x
-                }),
-                start = _start,
-                size = _size,
-                total = _total
-            });
-        }
+	private string WriteResult()
+	{
+		return WriteJson(new
+		{
+			state = GetStateString(),
+			list = _fileList?.Select(x => new
+			{
+				url = x
+			}),
+			start = _start,
+			size = _size,
+			total = _total
+		});
+	}
 
-        private string GetStateString()
-        {
-            return _state switch
-            {
-                ResultState.Success => "SUCCESS",
-                ResultState.InvalidParam => "参数不正确",
-                ResultState.PathNotFound => "路径不存在",
-                ResultState.AuthorizError => "文件系统权限不足",
-                ResultState.IOError => "文件系统读取错误",
-                _ => "未知错误"
-            };
-        }
-    }
+	private string GetStateString()
+	{
+		return _state switch
+		{
+			ResultState.Success => "SUCCESS",
+			ResultState.InvalidParam => "参数不正确",
+			ResultState.PathNotFound => "路径不存在",
+			ResultState.AuthorizError => "文件系统权限不足",
+			ResultState.IOError => "文件系统读取错误",
+			_ => "未知错误"
+		};
+	}
 }

+ 16 - 17
src/Masuit.MyBlogs.Core/Extensions/UEditor/NotSupportedHandler.cs

@@ -1,20 +1,19 @@
-namespace Masuit.MyBlogs.Core.Extensions.UEditor
+namespace Masuit.MyBlogs.Core.Extensions.UEditor;
+
+/// <summary>
+/// NotSupportedHandler 的摘要说明
+/// </summary>
+public class NotSupportedHandler : Handler
 {
-    /// <summary>
-    /// NotSupportedHandler 的摘要说明
-    /// </summary>
-    public class NotSupportedHandler : Handler
-    {
-        public NotSupportedHandler(HttpContext context) : base(context)
-        {
-        }
+	public NotSupportedHandler(HttpContext context) : base(context)
+	{
+	}
 
-        public override Task<string> Process()
-        {
-            return Task.FromResult(WriteJson(new
-            {
-                state = "action 参数为空或者 action 不被支持。"
-            }));
-        }
-    }
+	public override Task<string> Process()
+	{
+		return Task.FromResult(WriteJson(new
+		{
+			state = "action 参数为空或者 action 不被支持。"
+		}));
+	}
 }

+ 37 - 38
src/Masuit.MyBlogs.Core/Extensions/UEditor/PathFormatter.cs

@@ -1,48 +1,47 @@
 using Masuit.Tools.DateTimeExt;
 using System.Text.RegularExpressions;
 
-namespace Masuit.MyBlogs.Core.Extensions.UEditor
+namespace Masuit.MyBlogs.Core.Extensions.UEditor;
+
+/// <summary>
+/// PathFormater 的摘要说明
+/// </summary>
+public static class PathFormatter
 {
-    /// <summary>
-    /// PathFormater 的摘要说明
-    /// </summary>
-    public static class PathFormatter
-    {
-        public static string Format(string originFileName, string pathFormat)
-        {
-            if (string.IsNullOrWhiteSpace(pathFormat))
-            {
-                pathFormat = "{filename}_{rand:6}";
-            }
+	public static string Format(string originFileName, string pathFormat)
+	{
+		if (string.IsNullOrWhiteSpace(pathFormat))
+		{
+			pathFormat = "{filename}_{rand:6}";
+		}
 
-            var invalidPattern = new Regex(@"[\\\/\:\*\?\042\<\>\|]");
-            originFileName = invalidPattern.Replace(originFileName, "");
+		var invalidPattern = new Regex(@"[\\\/\:\*\?\042\<\>\|]");
+		originFileName = invalidPattern.Replace(originFileName, "");
 
-            string extension = Path.GetExtension(originFileName);
-            string filename = Path.GetFileNameWithoutExtension(originFileName);
+		string extension = Path.GetExtension(originFileName);
+		string filename = Path.GetFileNameWithoutExtension(originFileName);
 
-            pathFormat = pathFormat.Replace("{filename}", filename);
-            pathFormat = new Regex(@"\{rand(\:?)(\d+)\}").Replace(pathFormat, match =>
-            {
-                var digit = 6;
-                if (match.Groups.Count > 2)
-                {
-                    digit = Convert.ToInt32(match.Groups[2].Value);
-                }
-                var rand = new Random();
-                return rand.Next((int)Math.Pow(10, digit), (int)Math.Pow(10, digit + 1)).ToString();
-            });
+		pathFormat = pathFormat.Replace("{filename}", filename);
+		pathFormat = new Regex(@"\{rand(\:?)(\d+)\}").Replace(pathFormat, match =>
+		{
+			var digit = 6;
+			if (match.Groups.Count > 2)
+			{
+				digit = Convert.ToInt32(match.Groups[2].Value);
+			}
+			var rand = new Random();
+			return rand.Next((int)Math.Pow(10, digit), (int)Math.Pow(10, digit + 1)).ToString();
+		});
 
-            pathFormat = pathFormat.Replace("{time}", DateTime.Now.GetTotalMilliseconds().ToString());
-            pathFormat = pathFormat.Replace("{yyyy}", DateTime.Now.Year.ToString());
-            pathFormat = pathFormat.Replace("{yy}", (DateTime.Now.Year % 100).ToString("D2"));
-            pathFormat = pathFormat.Replace("{mm}", DateTime.Now.Month.ToString("D2"));
-            pathFormat = pathFormat.Replace("{dd}", DateTime.Now.Day.ToString("D2"));
-            pathFormat = pathFormat.Replace("{hh}", DateTime.Now.Hour.ToString("D2"));
-            pathFormat = pathFormat.Replace("{ii}", DateTime.Now.Minute.ToString("D2"));
-            pathFormat = pathFormat.Replace("{ss}", DateTime.Now.Second.ToString("D2"));
+		pathFormat = pathFormat.Replace("{time}", DateTime.Now.GetTotalMilliseconds().ToString());
+		pathFormat = pathFormat.Replace("{yyyy}", DateTime.Now.Year.ToString());
+		pathFormat = pathFormat.Replace("{yy}", (DateTime.Now.Year % 100).ToString("D2"));
+		pathFormat = pathFormat.Replace("{mm}", DateTime.Now.Month.ToString("D2"));
+		pathFormat = pathFormat.Replace("{dd}", DateTime.Now.Day.ToString("D2"));
+		pathFormat = pathFormat.Replace("{hh}", DateTime.Now.Hour.ToString("D2"));
+		pathFormat = pathFormat.Replace("{ii}", DateTime.Now.Minute.ToString("D2"));
+		pathFormat = pathFormat.Replace("{ss}", DateTime.Now.Second.ToString("D2"));
 
-            return pathFormat + extension;
-        }
-    }
+		return pathFormat + extension;
+	}
 }

+ 23 - 24
src/Masuit.MyBlogs.Core/Extensions/UEditor/UeditorConfig.cs

@@ -1,32 +1,31 @@
 using Newtonsoft.Json.Linq;
 
-namespace Masuit.MyBlogs.Core.Extensions.UEditor
+namespace Masuit.MyBlogs.Core.Extensions.UEditor;
+
+/// <summary>
+/// Config 的摘要说明
+/// </summary>
+public static class UeditorConfig
 {
-    /// <summary>
-    /// Config 的摘要说明
-    /// </summary>
-    public static class UeditorConfig
-    {
-        public static JObject Items => JObject.Parse(File.ReadAllText(AppContext.BaseDirectory + "App_Data/ueconfig.json"));
+	public static JObject Items => JObject.Parse(File.ReadAllText(AppContext.BaseDirectory + "App_Data/ueconfig.json"));
 
-        public static T GetValue<T>(string key)
-        {
-            return Items[key].Value<T>();
-        }
+	public static T GetValue<T>(string key)
+	{
+		return Items[key].Value<T>();
+	}
 
-        public static String[] GetStringList(string key)
-        {
-            return Items[key].Select(x => x.Value<String>()).ToArray();
-        }
+	public static String[] GetStringList(string key)
+	{
+		return Items[key].Select(x => x.Value<String>()).ToArray();
+	}
 
-        public static String GetString(string key)
-        {
-            return GetValue<String>(key);
-        }
+	public static String GetString(string key)
+	{
+		return GetValue<String>(key);
+	}
 
-        public static int GetInt(string key)
-        {
-            return GetValue<int>(key);
-        }
-    }
+	public static int GetInt(string key)
+	{
+		return GetValue<int>(key);
+	}
 }

+ 185 - 187
src/Masuit.MyBlogs.Core/Extensions/UEditor/UploadHandler.cs

@@ -1,195 +1,193 @@
 using Masuit.MyBlogs.Core.Common;
-using Masuit.Tools;
 using Masuit.Tools.Logging;
 using SixLabors.ImageSharp;
 using System.Diagnostics;
 using System.Text.RegularExpressions;
 
-namespace Masuit.MyBlogs.Core.Extensions.UEditor
+namespace Masuit.MyBlogs.Core.Extensions.UEditor;
+
+/// <summary>
+/// UploadHandler 的摘要说明
+/// </summary>
+public class UploadHandler : Handler
+{
+	public UploadConfig UploadConfig { get; }
+
+	public UploadResult Result { get; }
+
+	public UploadHandler(HttpContext context, UploadConfig config) : base(context)
+	{
+		UploadConfig = config;
+		Result = new UploadResult()
+		{
+			State = UploadState.Unknown
+		};
+	}
+
+	public override async Task<string> Process()
+	{
+		var form = await Request.ReadFormAsync();
+		var files = form.Files;
+		foreach (var file in files)
+		{
+			var uploadFileName = file.FileName;
+			if (!CheckFileType(uploadFileName))
+			{
+				Result.State = UploadState.TypeNotAllow;
+				return WriteResult();
+			}
+			if (!CheckFileSize(file.Length))
+			{
+				Result.State = UploadState.SizeLimitExceed;
+				return WriteResult();
+			}
+
+			Result.OriginFileName = uploadFileName;
+			var savePath = PathFormatter.Format(uploadFileName, UploadConfig.PathFormat);
+			var cts = new CancellationTokenSource(20000);
+			var stream = file.OpenReadStream();
+			try
+			{
+				var stream2 = stream.AddWatermark();
+				var format = await Image.DetectFormatAsync(stream2).ContinueWith(t => t.IsCompletedSuccessfully ? t.Result : null);
+				stream2.Position = 0;
+				if (format != null && !Regex.IsMatch(format.Name, "JPEG|PNG|Webp|GIF", RegexOptions.IgnoreCase))
+				{
+					using var image = await Image.LoadAsync(stream2);
+					var memoryStream = new MemoryStream();
+					await image.SaveAsJpegAsync(memoryStream);
+					await stream2.DisposeAsync();
+					stream2 = memoryStream;
+					savePath = savePath.Replace(Path.GetExtension(savePath), ".jpg");
+				}
+
+				var localPath = AppContext.BaseDirectory + "wwwroot" + savePath;
+				var (url, success) = await Context.RequestServices.GetRequiredService<ImagebedClient>().UploadImage(stream2, localPath, cts.Token);
+				if (success)
+				{
+					Result.Url = url;
+				}
+				else
+				{
+					Directory.CreateDirectory(Path.GetDirectoryName(localPath));
+					await File.WriteAllBytesAsync(localPath, await stream2.ToArrayAsync());
+					Result.Url = savePath;
+				}
+
+				Result.State = UploadState.Success;
+			}
+			catch (Exception e)
+			{
+				Result.State = UploadState.FileAccessError;
+				Result.ErrorMessage = e.Message;
+				LogManager.Error(e.Demystify());
+			}
+			finally
+			{
+				cts.Dispose();
+				stream.Close();
+				await stream.DisposeAsync();
+			}
+		}
+
+		return WriteResult();
+	}
+
+	private string WriteResult()
+	{
+		return WriteJson(new
+		{
+			state = GetStateMessage(Result.State),
+			url = Result.Url,
+			title = Result.OriginFileName,
+			original = Result.OriginFileName,
+			error = Result.ErrorMessage
+		});
+	}
+
+	private static string GetStateMessage(UploadState state)
+	{
+		switch (state)
+		{
+			case UploadState.Success:
+				return "SUCCESS";
+
+			case UploadState.FileAccessError:
+				return "文件访问出错,请检查写入权限";
+
+			case UploadState.SizeLimitExceed:
+				return "文件大小超出服务器限制";
+
+			case UploadState.TypeNotAllow:
+				return "不允许的文件格式";
+
+			case UploadState.NetworkError:
+				return "网络错误";
+		}
+		return "未知错误";
+	}
+
+	private bool CheckFileType(string filename)
+	{
+		return UploadConfig.AllowExtensions.Any(x => x.Equals(Path.GetExtension(filename), StringComparison.CurrentCultureIgnoreCase));
+	}
+
+	private bool CheckFileSize(long size)
+	{
+		return size < UploadConfig.SizeLimit;
+	}
+}
+
+public class UploadConfig
+{
+	/// <summary>
+	/// 文件命名规则
+	/// </summary>
+	public string PathFormat { get; set; }
+
+	/// <summary>
+	/// 上传表单域名称
+	/// </summary>
+	public string UploadFieldName { get; set; }
+
+	/// <summary>
+	/// 上传大小限制
+	/// </summary>
+	public int SizeLimit { get; set; }
+
+	/// <summary>
+	/// 上传允许的文件格式
+	/// </summary>
+	public string[] AllowExtensions { get; set; }
+
+	/// <summary>
+	/// 文件是否以 Base64 的形式上传
+	/// </summary>
+	public bool Base64 { get; set; }
+
+	/// <summary>
+	/// Base64 字符串所表示的文件名
+	/// </summary>
+	public string Base64Filename { get; set; }
+}
+
+public class UploadResult
 {
-    /// <summary>
-    /// UploadHandler 的摘要说明
-    /// </summary>
-    public class UploadHandler : Handler
-    {
-        public UploadConfig UploadConfig { get; }
-
-        public UploadResult Result { get; }
-
-        public UploadHandler(HttpContext context, UploadConfig config) : base(context)
-        {
-            UploadConfig = config;
-            Result = new UploadResult()
-            {
-                State = UploadState.Unknown
-            };
-        }
-
-        public override async Task<string> Process()
-        {
-            var form = await Request.ReadFormAsync();
-            var files = form.Files;
-            foreach (var file in files)
-            {
-                var uploadFileName = file.FileName;
-                if (!CheckFileType(uploadFileName))
-                {
-                    Result.State = UploadState.TypeNotAllow;
-                    return WriteResult();
-                }
-                if (!CheckFileSize(file.Length))
-                {
-                    Result.State = UploadState.SizeLimitExceed;
-                    return WriteResult();
-                }
-
-                Result.OriginFileName = uploadFileName;
-                var savePath = PathFormatter.Format(uploadFileName, UploadConfig.PathFormat);
-                var cts = new CancellationTokenSource(20000);
-                var stream = file.OpenReadStream();
-                try
-                {
-                    var stream2 = stream.AddWatermark();
-                    var format = await Image.DetectFormatAsync(stream2).ContinueWith(t => t.IsCompletedSuccessfully ? t.Result : null);
-                    stream2.Position = 0;
-                    if (format != null && !Regex.IsMatch(format.Name, "JPEG|PNG|Webp|GIF", RegexOptions.IgnoreCase))
-                    {
-                        using var image = await Image.LoadAsync(stream2);
-                        var memoryStream = new MemoryStream();
-                        await image.SaveAsJpegAsync(memoryStream);
-                        await stream2.DisposeAsync();
-                        stream2 = memoryStream;
-                        savePath = savePath.Replace(Path.GetExtension(savePath), ".jpg");
-                    }
-
-                    var localPath = AppContext.BaseDirectory + "wwwroot" + savePath;
-                    var (url, success) = await Context.RequestServices.GetRequiredService<ImagebedClient>().UploadImage(stream2, localPath, cts.Token);
-                    if (success)
-                    {
-                        Result.Url = url;
-                    }
-                    else
-                    {
-                        Directory.CreateDirectory(Path.GetDirectoryName(localPath));
-                        await File.WriteAllBytesAsync(localPath, await stream2.ToArrayAsync());
-                        Result.Url = savePath;
-                    }
-
-                    Result.State = UploadState.Success;
-                }
-                catch (Exception e)
-                {
-                    Result.State = UploadState.FileAccessError;
-                    Result.ErrorMessage = e.Message;
-                    LogManager.Error(e.Demystify());
-                }
-                finally
-                {
-                    cts.Dispose();
-                    stream.Close();
-                    await stream.DisposeAsync();
-                }
-            }
-
-            return WriteResult();
-        }
-
-        private string WriteResult()
-        {
-            return WriteJson(new
-            {
-                state = GetStateMessage(Result.State),
-                url = Result.Url,
-                title = Result.OriginFileName,
-                original = Result.OriginFileName,
-                error = Result.ErrorMessage
-            });
-        }
-
-        private static string GetStateMessage(UploadState state)
-        {
-            switch (state)
-            {
-                case UploadState.Success:
-                    return "SUCCESS";
-
-                case UploadState.FileAccessError:
-                    return "文件访问出错,请检查写入权限";
-
-                case UploadState.SizeLimitExceed:
-                    return "文件大小超出服务器限制";
-
-                case UploadState.TypeNotAllow:
-                    return "不允许的文件格式";
-
-                case UploadState.NetworkError:
-                    return "网络错误";
-            }
-            return "未知错误";
-        }
-
-        private bool CheckFileType(string filename)
-        {
-            return UploadConfig.AllowExtensions.Any(x => x.Equals(Path.GetExtension(filename), StringComparison.CurrentCultureIgnoreCase));
-        }
-
-        private bool CheckFileSize(long size)
-        {
-            return size < UploadConfig.SizeLimit;
-        }
-    }
-
-    public class UploadConfig
-    {
-        /// <summary>
-        /// 文件命名规则
-        /// </summary>
-        public string PathFormat { get; set; }
-
-        /// <summary>
-        /// 上传表单域名称
-        /// </summary>
-        public string UploadFieldName { get; set; }
-
-        /// <summary>
-        /// 上传大小限制
-        /// </summary>
-        public int SizeLimit { get; set; }
-
-        /// <summary>
-        /// 上传允许的文件格式
-        /// </summary>
-        public string[] AllowExtensions { get; set; }
-
-        /// <summary>
-        /// 文件是否以 Base64 的形式上传
-        /// </summary>
-        public bool Base64 { get; set; }
-
-        /// <summary>
-        /// Base64 字符串所表示的文件名
-        /// </summary>
-        public string Base64Filename { get; set; }
-    }
-
-    public class UploadResult
-    {
-        public UploadState State { get; set; }
-
-        public string Url { get; set; }
-
-        public string OriginFileName { get; set; }
-
-        public string ErrorMessage { get; set; }
-    }
-
-    public enum UploadState
-    {
-        NetworkError = -4,
-        FileAccessError = -3,
-        TypeNotAllow = -2,
-        SizeLimitExceed = -1,
-        Success = 0,
-        Unknown = 1
-    }
+	public UploadState State { get; set; }
+
+	public string Url { get; set; }
+
+	public string OriginFileName { get; set; }
+
+	public string ErrorMessage { get; set; }
 }
+
+public enum UploadState
+{
+	NetworkError = -4,
+	FileAccessError = -3,
+	TypeNotAllow = -2,
+	SizeLimitExceed = -1,
+	Success = 0,
+	Unknown = 1
+}

+ 1 - 2
src/Masuit.MyBlogs.Core/Infrastructure/DataContext.cs

@@ -1,5 +1,4 @@
-using Masuit.MyBlogs.Core.Models.Entity;
-using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Diagnostics;
 
 namespace Masuit.MyBlogs.Core.Infrastructure;

+ 50 - 51
src/Masuit.MyBlogs.Core/Infrastructure/Drive/IDriveAccountService.cs

@@ -1,54 +1,53 @@
 using Masuit.MyBlogs.Core.Models.Drive;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Drive
+namespace Masuit.MyBlogs.Core.Infrastructure.Drive;
+
+public interface IDriveAccountService
 {
-    public interface IDriveAccountService
-    {
-        public DriveContext SiteContext { get; set; }
-
-        /// <summary>
-        /// 返回 Oauth 验证url
-        /// </summary>
-        /// <returns></returns>
-        public Task<string> GetAuthorizationRequestUrl();
-
-        /// <summary>
-        /// 添加 SharePoint Site-ID
-        /// </summary>
-        /// <param name="siteName"></param>
-        /// <param name="dominName"></param>
-        /// <returns></returns>
-        public Task AddSiteId(string siteName, string nickName);
-
-        /// <summary>
-        /// Graph实例
-        /// </summary>
-        /// <value></value>
-        public Microsoft.Graph.GraphServiceClient Graph { get; set; }
-
-        /// <summary>
-        /// 返回所有 sharepoint site
-        /// </summary>
-        /// <returns></returns>
-        public List<Site> GetSites();
-
-        /// <summary>
-        /// 获取驱动器信息
-        /// </summary>
-        /// <returns></returns>
-        public Task<List<DriveAccountService.DriveInfo>> GetDriveInfo();
-
-        /// <summary>
-        /// 解除绑定
-        /// </summary>
-        /// <param name="nickName"></param>
-        /// <returns></returns>
-        public Task Unbind(string nickName);
-
-        /// <summary>
-        /// 获得账户 Token
-        /// </summary>
-        /// <returns></returns>
-        public string GetToken();
-    }
-}
+	public DriveContext SiteContext { get; set; }
+
+	/// <summary>
+	/// 返回 Oauth 验证url
+	/// </summary>
+	/// <returns></returns>
+	public Task<string> GetAuthorizationRequestUrl();
+
+	/// <summary>
+	/// 添加 SharePoint Site-ID
+	/// </summary>
+	/// <param name="siteName"></param>
+	/// <param name="dominName"></param>
+	/// <returns></returns>
+	public Task AddSiteId(string siteName, string nickName);
+
+	/// <summary>
+	/// Graph实例
+	/// </summary>
+	/// <value></value>
+	public Microsoft.Graph.GraphServiceClient Graph { get; set; }
+
+	/// <summary>
+	/// 返回所有 sharepoint site
+	/// </summary>
+	/// <returns></returns>
+	public List<Site> GetSites();
+
+	/// <summary>
+	/// 获取驱动器信息
+	/// </summary>
+	/// <returns></returns>
+	public Task<List<DriveAccountService.DriveInfo>> GetDriveInfo();
+
+	/// <summary>
+	/// 解除绑定
+	/// </summary>
+	/// <param name="nickName"></param>
+	/// <returns></returns>
+	public Task Unbind(string nickName);
+
+	/// <summary>
+	/// 获得账户 Token
+	/// </summary>
+	/// <returns></returns>
+	public string GetToken();
+}

+ 8 - 9
src/Masuit.MyBlogs.Core/Infrastructure/Drive/IDriveService.cs

@@ -1,15 +1,14 @@
 using Masuit.MyBlogs.Core.Models.Drive;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Drive
+namespace Masuit.MyBlogs.Core.Infrastructure.Drive;
+
+public interface IDriveService
 {
-    public interface IDriveService
-    {
-        public Task<List<DriveFile>> GetRootItems(string siteName, bool showHiddenFolders);
+	public Task<List<DriveFile>> GetRootItems(string siteName, bool showHiddenFolders);
 
-        public Task<List<DriveFile>> GetDriveItemsByPath(string path, string siteName, bool showHiddenFolders);
+	public Task<List<DriveFile>> GetDriveItemsByPath(string path, string siteName, bool showHiddenFolders);
 
-        public Task<DriveFile> GetDriveItemByPath(string path, string siteName);
+	public Task<DriveFile> GetDriveItemByPath(string path, string siteName);
 
-        public Task<string> GetUploadUrl(string path, string siteName = "onedrive");
-    }
-}
+	public Task<string> GetUploadUrl(string path, string siteName = "onedrive");
+}

+ 12 - 13
src/Masuit.MyBlogs.Core/Infrastructure/LoggerDbContext.cs

@@ -1,25 +1,24 @@
 using Masuit.MyBlogs.Core.Common;
-using Masuit.MyBlogs.Core.Models.Entity;
 using Microsoft.EntityFrameworkCore;
 
 namespace Masuit.MyBlogs.Core.Infrastructure;
 
 public sealed class LoggerDbContext : DbContext
 {
-    public LoggerDbContext(DbContextOptions<LoggerDbContext> options) : base(options)
-    {
-    }
+	public LoggerDbContext(DbContextOptions<LoggerDbContext> options) : base(options)
+	{
+	}
 
-    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
-    {
-        optionsBuilder.EnableDetailedErrors().UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
-    }
+	protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+	{
+		optionsBuilder.EnableDetailedErrors().UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
+	}
 
-    protected override void OnModelCreating(ModelBuilder modelBuilder)
-    {
-        modelBuilder.Entity<RequestLogDetail>().HasKey(e => new { e.Id, e.Time });
-        modelBuilder.Entity<PerformanceCounter>().HasKey(e => new { e.ServerIP, e.Time });
-    }
+	protected override void OnModelCreating(ModelBuilder modelBuilder)
+	{
+		modelBuilder.Entity<RequestLogDetail>().HasKey(e => new { e.Id, e.Time });
+		modelBuilder.Entity<PerformanceCounter>().HasKey(e => new { e.ServerIP, e.Time });
+	}
 }
 
 //[AttributeUsage(AttributeTargets.Property)]

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

@@ -4,569 +4,566 @@ using Masuit.LuceneEFCore.SearchEngine;
 using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
 using Masuit.Tools.Core.AspNetCore;
 using Masuit.Tools.Models;
-using Masuit.Tools.Systems;
 using Microsoft.EntityFrameworkCore;
-using System.Linq.Expressions;
 using Z.EntityFramework.Plus;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Repository
+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 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>
-	/// 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);
-	}
-}
+	/// 获取第一条数据(不跟踪实体)
+	/// </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);
+}

+ 0 - 1
src/Masuit.MyBlogs.Core/Infrastructure/Repository/CommentRepository.cs

@@ -1,5 +1,4 @@
 using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
-using Masuit.MyBlogs.Core.Models.Entity;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Repository;
 

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

@@ -1,486 +1,483 @@
 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
+namespace Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
+
+public interface IBaseRepository<T> : IDisposable where T : LuceneIndexableBaseEntity
 {
-	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>
-	{ }
+	/// <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 ILinkLoopbackRepository : IBaseRepository<LinkLoopback>
-	{ }
-
-	public partial interface ILoginRecordRepository : IBaseRepository<LoginRecord>
-	{ }
+public partial interface ICategoryRepository : IBaseRepository<Category>
+{ }
 
-	public partial interface IMenuRepository : IBaseRepository<Menu>
-	{ }
+public partial interface ICommentRepository : IBaseRepository<Comment>
+{ }
 
-	public partial interface IMiscRepository : IBaseRepository<Misc>
-	{ }
+public partial interface IDonateRepository : IBaseRepository<Donate>
+{ }
 
-	public partial interface INoticeRepository : IBaseRepository<Notice>
-	{ }
+public partial interface IFastShareRepository : IBaseRepository<FastShare>
+{ }
 
-	public partial interface IPostRepository : IBaseRepository<Post>
-	{ }
+public partial interface IInternalMessageRepository : IBaseRepository<InternalMessage>
+{ }
 
-	public partial interface IPostHistoryVersionRepository : IBaseRepository<PostHistoryVersion>
-	{ }
+public partial interface ILeaveMessageRepository : IBaseRepository<LeaveMessage>
+{ }
 
-	public partial interface ISeminarRepository : IBaseRepository<Seminar>
-	{ }
+public partial interface ILinksRepository : IBaseRepository<Links>
+{ }
 
-	public partial interface ISystemSettingRepository : IBaseRepository<SystemSetting>
-	{ }
+public partial interface ILinkLoopbackRepository : IBaseRepository<LinkLoopback>
+{ }
 
-	public partial interface IUserInfoRepository : IBaseRepository<UserInfo>
-	{ }
+public partial interface ILoginRecordRepository : IBaseRepository<LoginRecord>
+{ }
 
-	public partial interface IPostMergeRequestRepository : IBaseRepository<PostMergeRequest>
-	{ }
+public partial interface IMenuRepository : IBaseRepository<Menu>
+{ }
 
-	public partial interface IAdvertisementRepository : IBaseRepository<Advertisement>
-	{ }
+public partial interface IMiscRepository : IBaseRepository<Misc>
+{ }
 
-	public partial interface IAdvertisementClickRecordRepository : IBaseRepository<AdvertisementClickRecord>
-	{ }
+public partial interface INoticeRepository : IBaseRepository<Notice>
+{ }
 
-	public partial interface IVariablesRepository : IBaseRepository<Variables>
-	{ }
+public partial interface IPostRepository : IBaseRepository<Post>
+{ }
 
-	public partial interface IPostVisitRecordRepository : IBaseRepository<PostVisitRecord>
-	{ }
+public partial interface IPostHistoryVersionRepository : IBaseRepository<PostHistoryVersion>
+{ }
 
-	public partial interface IPostVisitRecordStatsRepository : IBaseRepository<PostVisitRecordStats>
-	{ }
+public partial interface ISeminarRepository : IBaseRepository<Seminar>
+{ }
 
-	public partial interface IPostTagsRepository : IBaseRepository<PostTag>
-	{ }
-}
+public partial interface ISystemSettingRepository : IBaseRepository<SystemSetting>
+{ }
+
+public partial interface IUserInfoRepository : IBaseRepository<UserInfo>
+{ }
+
+public partial interface IPostMergeRequestRepository : IBaseRepository<PostMergeRequest>
+{ }
+
+public partial interface IAdvertisementRepository : IBaseRepository<Advertisement>
+{ }
+
+public partial interface IAdvertisementClickRecordRepository : IBaseRepository<AdvertisementClickRecord>
+{ }
+
+public partial interface IVariablesRepository : IBaseRepository<Variables>
+{ }
+
+public partial interface IPostVisitRecordRepository : IBaseRepository<PostVisitRecord>
+{ }
+
+public partial interface IPostVisitRecordStatsRepository : IBaseRepository<PostVisitRecordStats>
+{ }
+
+public partial interface IPostTagsRepository : IBaseRepository<PostTag>
+{ }

+ 9 - 12
src/Masuit.MyBlogs.Core/Infrastructure/Repository/Interface/ISearchDetailsRepository.cs

@@ -1,14 +1,11 @@
-using Masuit.MyBlogs.Core.Models.Entity;
+namespace Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Repository.Interface
+public partial interface ISearchDetailsRepository : IBaseRepository<SearchDetails>
 {
-    public partial interface ISearchDetailsRepository : IBaseRepository<SearchDetails>
-    {
-        /// <summary>
-        /// 搜索统计
-        /// </summary>
-        /// <param name="start"></param>
-        /// <returns></returns>
-        List<SearchRank> GetRanks(DateTime start);
-    }
-}
+	/// <summary>
+	/// 搜索统计
+	/// </summary>
+	/// <param name="start"></param>
+	/// <returns></returns>
+	List<SearchRank> GetRanks(DateTime start);
+}

+ 0 - 1
src/Masuit.MyBlogs.Core/Infrastructure/Repository/LeaveMessageRepository.cs

@@ -1,5 +1,4 @@
 using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
-using Masuit.MyBlogs.Core.Models.Entity;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Repository;
 

+ 0 - 1
src/Masuit.MyBlogs.Core/Infrastructure/Repository/MenuRepository.cs

@@ -1,5 +1,4 @@
 using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
-using Masuit.MyBlogs.Core.Models.Entity;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Repository;
 

+ 0 - 2
src/Masuit.MyBlogs.Core/Infrastructure/Repository/PostRepository.cs

@@ -1,7 +1,5 @@
 using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
-using Masuit.MyBlogs.Core.Models.Entity;
 using Microsoft.EntityFrameworkCore;
-using System.Linq.Expressions;
 using Z.EntityFramework.Plus;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Repository;

+ 0 - 1
src/Masuit.MyBlogs.Core/Infrastructure/Repository/Repositories.cs

@@ -1,5 +1,4 @@
 using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
-using Masuit.MyBlogs.Core.Models.Entity;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Repository;
 

+ 9 - 10
src/Masuit.MyBlogs.Core/Infrastructure/Repository/SearchDetailsRepository.cs

@@ -1,17 +1,16 @@
 using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
-using Masuit.MyBlogs.Core.Models.Entity;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Repository;
 
 public sealed partial class SearchDetailsRepository : BaseRepository<SearchDetails>, ISearchDetailsRepository
 {
-    /// <summary>
-    /// 热词统计
-    /// </summary>
-    /// <param name="start"></param>
-    /// <returns></returns>
-    public List<SearchRank> GetRanks(DateTime start)
-    {
-        return DataContext.SearchDetails.Where(s => s.SearchTime > start).Select(s => new { s.IP, s.Keywords }).Distinct().GroupBy(s => s.Keywords).Select(g => new SearchRank { Keywords = g.Key, Count = g.Count() }).OrderByDescending(s => s.Count).Take(30).ToList();
-    }
+	/// <summary>
+	/// 热词统计
+	/// </summary>
+	/// <param name="start"></param>
+	/// <returns></returns>
+	public List<SearchRank> GetRanks(DateTime start)
+	{
+		return DataContext.SearchDetails.Where(s => s.SearchTime > start).Select(s => new { s.IP, s.Keywords }).Distinct().GroupBy(s => s.Keywords).Select(g => new SearchRank { Keywords = g.Key, Count = g.Count() }).OrderByDescending(s => s.Count).Take(30).ToList();
+	}
 }

+ 0 - 7
src/Masuit.MyBlogs.Core/Infrastructure/Services/AdvertisementService.cs

@@ -2,15 +2,8 @@
 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;
-using Masuit.Tools.Linq;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Caching.Memory;
-using System.Linq.Expressions;
 using System.Text.RegularExpressions;
 using Z.EntityFramework.Plus;
 

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

@@ -1,670 +1,667 @@
 using Masuit.LuceneEFCore.SearchEngine;
 using Masuit.LuceneEFCore.SearchEngine.Interfaces;
 using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
 using Masuit.Tools.Models;
-using System.Linq.Expressions;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Services
+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 List<T> GetAllFromCache<TS>(Expression<Func<T, TS>> @orderby, bool isAsc = true)
+	{
+		return BaseDal.GetAllFromCache(orderby, isAsc);
+	}
+
 	/// <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);
+	/// 获取所有实体
+	/// </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);
 }

+ 0 - 2
src/Masuit.MyBlogs.Core/Infrastructure/Services/CategoryService.cs

@@ -1,7 +1,5 @@
 using Masuit.LuceneEFCore.SearchEngine.Interfaces;
 using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.Entity;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Services;
 

+ 0 - 2
src/Masuit.MyBlogs.Core/Infrastructure/Services/CommentService.cs

@@ -1,7 +1,5 @@
 using Masuit.LuceneEFCore.SearchEngine.Interfaces;
 using Masuit.MyBlogs.Core.Infrastructure.Repository.Interface;
-using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
-using Masuit.MyBlogs.Core.Models.Entity;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Services;
 

+ 3 - 6
src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/IAdvertisementService.cs

@@ -1,12 +1,9 @@
 using Masuit.MyBlogs.Core.Common;
-using Masuit.MyBlogs.Core.Models.DTO;
-using Masuit.MyBlogs.Core.Models.Entity;
-using Masuit.MyBlogs.Core.Models.Enum;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Services.Interface
 {
-    public partial interface IAdvertisementService : IBaseService<Advertisement>
-    {
+	public partial interface IAdvertisementService : IBaseService<Advertisement>
+	{
 		/// <summary>
 		/// 按价格随机筛选一个元素
 		/// </summary>
@@ -27,5 +24,5 @@ namespace Masuit.MyBlogs.Core.Infrastructure.Services.Interface
 		/// <param name="keywords"></param>
 		/// <returns></returns>
 		List<AdvertisementDto> GetsByWeightedPrice(int count, AdvertiseType type, IPLocation location, int? cid = null, string keywords = "");
-    }
+	}
 }

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

@@ -1,470 +1,468 @@
 using Masuit.LuceneEFCore.SearchEngine;
 using Masuit.Tools.Models;
-using System.Linq.Expressions;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Services.Interface
+namespace Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
+
+public interface IBaseService<T> where T : LuceneIndexableBaseEntity
 {
-	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);
-	}
-}
+	/// <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);
+}

+ 9 - 12
src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/ICategoryService.cs

@@ -1,15 +1,12 @@
-using Masuit.MyBlogs.Core.Models.Entity;
+namespace Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Services.Interface
+public partial interface ICategoryService : IBaseService<Category>
 {
-    public partial interface ICategoryService : IBaseService<Category>
-    {
-        /// <summary>
-        /// 删除分类,并将该分类下的文章移动到新分类下
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="mid"></param>
-        /// <returns></returns>
-        Task<bool> Delete(int id, int mid);
-    }
+	/// <summary>
+	/// 删除分类,并将该分类下的文章移动到新分类下
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="mid"></param>
+	/// <returns></returns>
+	Task<bool> Delete(int id, int mid);
 }

+ 2 - 5
src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/ICommentService.cs

@@ -1,8 +1,5 @@
-using Masuit.MyBlogs.Core.Models.Entity;
+namespace Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Services.Interface
+public partial interface ICommentService : IBaseService<Comment>
 {
-    public partial interface ICommentService : IBaseService<Comment>
-    {
-    }
 }

+ 2 - 5
src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/ILeaveMessageService.cs

@@ -1,8 +1,5 @@
-using Masuit.MyBlogs.Core.Models.Entity;
+namespace Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Services.Interface
+public partial interface ILeaveMessageService : IBaseService<LeaveMessage>
 {
-    public partial interface ILeaveMessageService : IBaseService<LeaveMessage>
-    {
-    }
 }

+ 3 - 6
src/Masuit.MyBlogs.Core/Infrastructure/Services/Interface/IMenuService.cs

@@ -1,8 +1,5 @@
-using Masuit.MyBlogs.Core.Models.Entity;
+namespace Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Services.Interface
+public partial interface IMenuService : IBaseService<Menu>
 {
-    public partial interface IMenuService : IBaseService<Menu>
-    {
-    }
-}
+}

Some files were not shown because too many files changed in this diff