懒得勤快 2 年之前
父节点
当前提交
c036b52a3c
共有 77 个文件被更改,包括 2619 次插入2648 次删除
  1. 1 1
      src/Masuit.MyBlogs.Core/Common/ImagebedClient.cs
  2. 1 1
      src/Masuit.MyBlogs.Core/Common/Mails/MailgunSender.cs
  3. 33 34
      src/Masuit.MyBlogs.Core/Common/Mails/SmtpSender.cs
  4. 4 4
      src/Masuit.MyBlogs.Core/Common/PerfCounter.cs
  5. 1 1
      src/Masuit.MyBlogs.Core/Common/UserAgent.cs
  6. 1 1
      src/Masuit.MyBlogs.Core/Configs/AutofacModule.cs
  7. 1 1
      src/Masuit.MyBlogs.Core/Configs/GitlabConfig.cs
  8. 1 1
      src/Masuit.MyBlogs.Core/Configs/MappingProfile.cs
  9. 1 1
      src/Masuit.MyBlogs.Core/Controllers/AdvertisementController.cs
  10. 1 1
      src/Masuit.MyBlogs.Core/Controllers/CategoryController.cs
  11. 1 1
      src/Masuit.MyBlogs.Core/Controllers/CommentController.cs
  12. 1 1
      src/Masuit.MyBlogs.Core/Controllers/DashboardController.cs
  13. 1 1
      src/Masuit.MyBlogs.Core/Controllers/DefaultController.cs
  14. 1 1
      src/Masuit.MyBlogs.Core/Controllers/DonateController.cs
  15. 1 1
      src/Masuit.MyBlogs.Core/Controllers/Drive/AdminController.cs
  16. 1 1
      src/Masuit.MyBlogs.Core/Controllers/Drive/DriveController.cs
  17. 1 1
      src/Masuit.MyBlogs.Core/Controllers/Drive/SitesController.cs
  18. 1 1
      src/Masuit.MyBlogs.Core/Controllers/Drive/UserController.cs
  19. 1 1
      src/Masuit.MyBlogs.Core/Controllers/ErrorController.cs
  20. 1 1
      src/Masuit.MyBlogs.Core/Controllers/FileController.cs
  21. 1 1
      src/Masuit.MyBlogs.Core/Controllers/FirewallController.cs
  22. 1 1
      src/Masuit.MyBlogs.Core/Controllers/HomeController.cs
  23. 1 1
      src/Masuit.MyBlogs.Core/Controllers/LinksController.cs
  24. 1 1
      src/Masuit.MyBlogs.Core/Controllers/LoginController.cs
  25. 1 1
      src/Masuit.MyBlogs.Core/Controllers/MenuController.cs
  26. 1 1
      src/Masuit.MyBlogs.Core/Controllers/MergeController.cs
  27. 1 1
      src/Masuit.MyBlogs.Core/Controllers/MiscController.cs
  28. 1 1
      src/Masuit.MyBlogs.Core/Controllers/MsgController.cs
  29. 1 1
      src/Masuit.MyBlogs.Core/Controllers/NoticeController.cs
  30. 211 211
      src/Masuit.MyBlogs.Core/Controllers/PassportController.cs
  31. 1 1
      src/Masuit.MyBlogs.Core/Controllers/PostController.cs
  32. 1 1
      src/Masuit.MyBlogs.Core/Controllers/SearchController.cs
  33. 1 1
      src/Masuit.MyBlogs.Core/Controllers/SeminarController.cs
  34. 1 1
      src/Masuit.MyBlogs.Core/Controllers/ShareController.cs
  35. 1 1
      src/Masuit.MyBlogs.Core/Controllers/ShortController.cs
  36. 1 1
      src/Masuit.MyBlogs.Core/Controllers/SubscribeController.cs
  37. 389 389
      src/Masuit.MyBlogs.Core/Controllers/SystemController.cs
  38. 1 1
      src/Masuit.MyBlogs.Core/Controllers/ToolsController.cs
  39. 262 264
      src/Masuit.MyBlogs.Core/Controllers/UploadController.cs
  40. 130 131
      src/Masuit.MyBlogs.Core/Controllers/UserController.cs
  41. 27 28
      src/Masuit.MyBlogs.Core/Controllers/ValidateController.cs
  42. 25 26
      src/Masuit.MyBlogs.Core/Controllers/ValuesController.cs
  43. 68 69
      src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/OneDriveConfiguration.cs
  44. 63 64
      src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/ProtectedApiCallHelper.cs
  45. 31 33
      src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/TokenCacheHelper.cs
  46. 10 11
      src/Masuit.MyBlogs.Core/Extensions/Firewall/AllowAccessFirewallAttribute.cs
  47. 1 1
      src/Masuit.MyBlogs.Core/Extensions/Firewall/CloudflareRepoter.cs
  48. 1 1
      src/Masuit.MyBlogs.Core/Extensions/Firewall/DefaultFirewallRepoter.cs
  49. 1 1
      src/Masuit.MyBlogs.Core/Extensions/Firewall/FirewallAttribute.cs
  50. 6 7
      src/Masuit.MyBlogs.Core/Extensions/Firewall/TempDenyException.cs
  51. 1 1
      src/Masuit.MyBlogs.Core/Extensions/Hangfire/HangfireActivator.cs
  52. 1 1
      src/Masuit.MyBlogs.Core/Extensions/Hangfire/HangfireBackJob.cs
  53. 63 64
      src/Masuit.MyBlogs.Core/Extensions/MyAuthorizeAttribute.cs
  54. 65 66
      src/Masuit.MyBlogs.Core/Extensions/TranslateMiddleware.cs
  55. 126 127
      src/Masuit.MyBlogs.Core/Infrastructure/DataContext.cs
  56. 141 142
      src/Masuit.MyBlogs.Core/Infrastructure/Drive/DriveAccountService.cs
  57. 171 172
      src/Masuit.MyBlogs.Core/Infrastructure/Drive/DriveService.cs
  58. 59 60
      src/Masuit.MyBlogs.Core/Infrastructure/Drive/SettingService.cs
  59. 71 72
      src/Masuit.MyBlogs.Core/Infrastructure/Drive/TokenService.cs
  60. 15 16
      src/Masuit.MyBlogs.Core/Infrastructure/DriveContext.cs
  61. 1 1
      src/Masuit.MyBlogs.Core/Infrastructure/LoggerDbContext.cs
  62. 1 1
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/CommentRepository.cs
  63. 1 1
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/LeaveMessageRepository.cs
  64. 1 1
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/MenuRepository.cs
  65. 1 1
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/PostRepository.cs
  66. 20 20
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/Repositories.cs
  67. 1 1
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/SearchDetailsRepository.cs
  68. 1 1
      src/Masuit.MyBlogs.Core/Infrastructure/Services/AdvertisementService.cs
  69. 39 40
      src/Masuit.MyBlogs.Core/Infrastructure/Services/CategoryService.cs
  70. 7 8
      src/Masuit.MyBlogs.Core/Infrastructure/Services/CommentService.cs
  71. 6 7
      src/Masuit.MyBlogs.Core/Infrastructure/Services/LeaveMessageService.cs
  72. 7 8
      src/Masuit.MyBlogs.Core/Infrastructure/Services/MenuService.cs
  73. 233 234
      src/Masuit.MyBlogs.Core/Infrastructure/Services/PostService.cs
  74. 17 18
      src/Masuit.MyBlogs.Core/Infrastructure/Services/SearchDetailsService.cs
  75. 119 120
      src/Masuit.MyBlogs.Core/Infrastructure/Services/Services.cs
  76. 131 132
      src/Masuit.MyBlogs.Core/Infrastructure/Services/UserInfoService.cs
  77. 24 25
      src/Masuit.MyBlogs.Core/Models/Command/CategoryCommand.cs

+ 1 - 1
src/Masuit.MyBlogs.Core/Common/ImagebedClient.cs

@@ -13,7 +13,7 @@ namespace Masuit.MyBlogs.Core.Common
     /// <summary>
     /// 图床客户端
     /// </summary>
-    public class ImagebedClient
+    public sealed class ImagebedClient
     {
         private readonly HttpClient _httpClient;
         private readonly IConfiguration _config;

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

@@ -6,7 +6,7 @@ using System.Text;
 
 namespace Masuit.MyBlogs.Core.Common.Mails
 {
-    public class MailgunSender : IMailSender
+    public sealed class MailgunSender : IMailSender
     {
         private readonly HttpClient _httpClient;
         private readonly IConfiguration _configuration;

+ 33 - 34
src/Masuit.MyBlogs.Core/Common/Mails/SmtpSender.cs

@@ -2,41 +2,40 @@
 using Masuit.Tools.Models;
 using System.Text;
 
-namespace Masuit.MyBlogs.Core.Common.Mails
+namespace Masuit.MyBlogs.Core.Common.Mails;
+
+public sealed class SmtpSender : IMailSender
 {
-    public class SmtpSender : IMailSender
-    {
-        public void Send(string title, string content, string tos)
-        {
-            new Email()
-            {
-                EnableSsl = bool.Parse(CommonHelper.SystemSettings.GetOrAdd("EnableSsl", "true")),
-                Body = content,
-                SmtpServer = CommonHelper.SystemSettings["SMTP"],
-                Username = CommonHelper.SystemSettings["EmailFrom"],
-                Password = CommonHelper.SystemSettings["EmailPwd"],
-                SmtpPort = CommonHelper.SystemSettings["SmtpPort"].ToInt32(),
-                Subject = title,
-                Tos = tos
-            }.Send();
-        }
+	public void Send(string title, string content, string tos)
+	{
+		new Email()
+		{
+			EnableSsl = bool.Parse(CommonHelper.SystemSettings.GetOrAdd("EnableSsl", "true")),
+			Body = content,
+			SmtpServer = CommonHelper.SystemSettings["SMTP"],
+			Username = CommonHelper.SystemSettings["EmailFrom"],
+			Password = CommonHelper.SystemSettings["EmailPwd"],
+			SmtpPort = CommonHelper.SystemSettings["SmtpPort"].ToInt32(),
+			Subject = title,
+			Tos = tos
+		}.Send();
+	}
 
-        public List<string> GetBounces()
-        {
-            return File.ReadAllText(Path.Combine(AppContext.BaseDirectory + "App_Data", "email-bounces.txt"), Encoding.UTF8).Split(',').ToList();
-        }
+	public List<string> GetBounces()
+	{
+		return File.ReadAllText(Path.Combine(AppContext.BaseDirectory + "App_Data", "email-bounces.txt"), Encoding.UTF8).Split(',').ToList();
+	}
 
-        public string AddRecipient(string email)
-        {
-            var bounces = GetBounces();
-            bounces.Add(email);
-            File.WriteAllText(Path.Combine(AppContext.BaseDirectory + "App_Data", "email-bounces.txt"), bounces.Join(","));
-            return "添加成功";
-        }
+	public string AddRecipient(string email)
+	{
+		var bounces = GetBounces();
+		bounces.Add(email);
+		File.WriteAllText(Path.Combine(AppContext.BaseDirectory + "App_Data", "email-bounces.txt"), bounces.Join(","));
+		return "添加成功";
+	}
 
-        public bool HasBounced(string address)
-        {
-            return GetBounces().Contains(address);
-        }
-    }
-}
+	public bool HasBounced(string address)
+	{
+		return GetBounces().Contains(address);
+	}
+}

+ 4 - 4
src/Masuit.MyBlogs.Core/Common/PerfCounter.cs

@@ -76,7 +76,7 @@ public interface IPerfCounter
     void Process();
 }
 
-public class DefaultPerfCounter : IPerfCounter
+public sealed class DefaultPerfCounter : IPerfCounter
 {
     static DefaultPerfCounter()
     {
@@ -92,7 +92,7 @@ public class DefaultPerfCounter : IPerfCounter
     }
 }
 
-public class PerfCounterInDatabase : IPerfCounter
+public sealed class PerfCounterInDatabase : IPerfCounter
 {
     public static ConcurrentLimitedQueue<PerformanceCounter> List { get; } = new(50000);
 
@@ -128,7 +128,7 @@ public class PerfCounterInDatabase : IPerfCounter
     }
 }
 
-public class PerfCounterBackService : ScheduledService
+public sealed class PerfCounterBackService : ScheduledService
 {
     private readonly IServiceScopeFactory _serviceScopeFactory;
 
@@ -173,7 +173,7 @@ public static class PerfCounterServiceExtension
 /// 性能计数器
 /// </summary>
 [Table(nameof(PerformanceCounter))]
-public class PerformanceCounter
+public sealed class PerformanceCounter
 {
     [StringLength(128)]
     public string ServerIP { get; set; }

+ 1 - 1
src/Masuit.MyBlogs.Core/Common/UserAgent.cs

@@ -3,7 +3,7 @@ using System.Text.RegularExpressions;
 
 namespace Masuit.MyBlogs.Core.Common
 {
-    public class UserAgent
+    public sealed class UserAgent
     {
         private static readonly IMemoryCache Cache = new MemoryCache(new MemoryCacheOptions());
 

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

@@ -4,7 +4,7 @@ using System.Reflection;
 
 namespace Masuit.MyBlogs.Core.Configs
 {
-    public class AutofacModule : Autofac.Module
+    public sealed class AutofacModule : Autofac.Module
     {
         protected override void Load(ContainerBuilder builder)
         {

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

@@ -1,6 +1,6 @@
 namespace Masuit.MyBlogs.Core.Configs
 {
-    public class GitlabConfig
+    public sealed class GitlabConfig
     {
         public bool Enabled { get; set; }
 

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

@@ -11,7 +11,7 @@ namespace Masuit.MyBlogs.Core.Configs
     /// <summary>
     /// 注册automapper
     /// </summary>
-    public class MappingProfile : Profile
+    public sealed class MappingProfile : Profile
     {
         public MappingProfile()
         {

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

@@ -25,7 +25,7 @@ using System.Text.RegularExpressions;
 namespace Masuit.MyBlogs.Core.Controllers;
 
 [Route("partner/[action]")]
-public class AdvertisementController : BaseController
+public sealed class AdvertisementController : BaseController
 {
 	public IAdvertisementClickRecordService ClickRecordService { get; set; }
 

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

@@ -16,7 +16,7 @@ namespace Masuit.MyBlogs.Core.Controllers
 	/// <summary>
 	/// 文章分类
 	/// </summary>
-	public class CategoryController : BaseController
+	public sealed class CategoryController : BaseController
 	{
 		/// <summary>
 		/// CategoryService

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

@@ -29,7 +29,7 @@ namespace Masuit.MyBlogs.Core.Controllers
 	/// <summary>
 	/// 评论管理
 	/// </summary>
-	public class CommentController : BaseController
+	public sealed class CommentController : BaseController
 	{
 		public ICommentService CommentService { get; set; }
 

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

@@ -12,7 +12,7 @@ namespace Masuit.MyBlogs.Core.Controllers
     /// <summary>
     /// 控制面板
     /// </summary>
-    public class DashboardController : AdminController
+    public sealed class DashboardController : AdminController
     {
         /// <summary>
         /// 控制面板

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

@@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc;
 
 namespace Masuit.MyBlogs.Core.Controllers;
 
-public class DefaultController : Controller
+public sealed class DefaultController : Controller
 {
     /// <summary>
     /// 设置cookie

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

@@ -10,7 +10,7 @@ namespace Masuit.MyBlogs.Core.Controllers
     /// <summary>
     /// 打赏管理
     /// </summary>
-    public class DonateController : AdminController
+    public sealed class DonateController : AdminController
     {
         /// <summary>
         /// DonateService

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

@@ -12,7 +12,7 @@ namespace Masuit.MyBlogs.Core.Controllers.Drive
     [MyAuthorize]
     [ApiController]
     [Route("api/[controller]")]
-    public class AdminController : Controller
+    public sealed class AdminController : Controller
     {
         private readonly IDriveAccountService _driveAccount;
         private readonly SettingService _setting;

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

@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
 namespace Masuit.MyBlogs.Core.Controllers.Drive
 {
     [ServiceFilter(typeof(FirewallAttribute))]
-    public class DriveController : Controller
+    public sealed class DriveController : Controller
     {
         [HttpGet("/drive")]
         public IActionResult Index()

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

@@ -16,7 +16,7 @@ namespace Masuit.MyBlogs.Core.Controllers.Drive
     [ApiController]
     [ServiceFilter(typeof(FirewallAttribute))]
     [Route("api/")]
-    public class SitesController : Controller
+    public sealed class SitesController : Controller
     {
         private readonly IDriveAccountService _siteService;
         private readonly IDriveService _driveService;

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

@@ -15,7 +15,7 @@ namespace Masuit.MyBlogs.Core.Controllers.Drive
 {
     [ApiController]
     [Route("api/[controller]")]
-    public class UserController : Controller
+    public sealed class UserController : Controller
     {
         public IUserInfoService UserInfoService { get; set; }
 

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

@@ -25,7 +25,7 @@ namespace Masuit.MyBlogs.Core.Controllers
     /// 错误页
     /// </summary>
     [ApiExplorerSettings(IgnoreApi = true)]
-    public class ErrorController : Controller
+    public sealed class ErrorController : Controller
     {
         public IRedisClient RedisClient { get; set; }
 

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

@@ -20,7 +20,7 @@ namespace Masuit.MyBlogs.Core.Controllers
     /// 资源管理器
     /// </summary>
     [Route("[controller]/[action]")]
-    public class FileController : AdminController
+    public sealed class FileController : AdminController
     {
         public IWebHostEnvironment HostEnvironment { get; set; }
 

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

@@ -18,7 +18,7 @@ using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
 
 namespace Masuit.MyBlogs.Core.Controllers;
 
-public class FirewallController : Controller
+public sealed class FirewallController : Controller
 {
     public IRedisClient RedisClient { get; set; }
     private readonly HttpClient _httpClient;

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

@@ -31,7 +31,7 @@ namespace Masuit.MyBlogs.Core.Controllers;
 /// <summary>
 /// 首页
 /// </summary>
-public class HomeController : BaseController
+public sealed class HomeController : BaseController
 {
 	/// <summary>
 	/// 文章

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

@@ -16,7 +16,7 @@ namespace Masuit.MyBlogs.Core.Controllers
     /// <summary>
     /// 友情链接管理
     /// </summary>
-    public class LinksController : BaseController
+    public sealed class LinksController : BaseController
     {
         public IHttpClientFactory HttpClientFactory { get; set; }
         public IConfiguration Configuration { get; set; }

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

@@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Mvc;
 namespace Masuit.MyBlogs.Core.Controllers;
 
 [Route("login")]
-public class LoginController : AdminController
+public sealed class LoginController : AdminController
 {
     public ILoginRecordService LoginRecordService { get; set; }
 

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

@@ -15,7 +15,7 @@ namespace Masuit.MyBlogs.Core.Controllers
 	/// <summary>
 	/// 菜单管理
 	/// </summary>
-	public class MenuController : AdminController
+	public sealed class MenuController : AdminController
 	{
 		/// <summary>
 		/// 菜单数据服务

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

@@ -23,7 +23,7 @@ using System.Text.RegularExpressions;
 namespace Masuit.MyBlogs.Core.Controllers
 {
     [Route("merge/")]
-    public class MergeController : AdminController
+    public sealed class MergeController : AdminController
     {
         public IPostMergeRequestService PostMergeRequestService { get; set; }
 

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

@@ -16,7 +16,7 @@ namespace Masuit.MyBlogs.Core.Controllers
     /// <summary>
     /// 杂项页
     /// </summary>
-    public class MiscController : BaseController
+    public sealed class MiscController : BaseController
     {
         /// <summary>
         /// MiscService

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

@@ -29,7 +29,7 @@ namespace Masuit.MyBlogs.Core.Controllers
 	/// <summary>
 	/// 留言板和站内信
 	/// </summary>
-	public class MsgController : BaseController
+	public sealed class MsgController : BaseController
 	{
 		/// <summary>
 		/// 留言

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

@@ -18,7 +18,7 @@ namespace Masuit.MyBlogs.Core.Controllers
     /// <summary>
     /// 网站公告
     /// </summary>
-    public class NoticeController : BaseController
+    public sealed class NoticeController : BaseController
     {
         /// <summary>
         /// 公告

+ 211 - 211
src/Masuit.MyBlogs.Core/Controllers/PassportController.cs

@@ -21,238 +21,238 @@ using System.Web;
 
 namespace Masuit.MyBlogs.Core.Controllers
 {
-    /// <summary>
-    /// 登录授权
-    /// </summary>
-    [ApiExplorerSettings(IgnoreApi = true), ServiceFilter(typeof(FirewallAttribute))]
-    public class PassportController : Controller
-    {
-        /// <summary>
-        /// 用户
-        /// </summary>
-        public IUserInfoService UserInfoService { get; set; }
+	/// <summary>
+	/// 登录授权
+	/// </summary>
+	[ApiExplorerSettings(IgnoreApi = true), ServiceFilter(typeof(FirewallAttribute))]
+	public sealed class PassportController : Controller
+	{
+		/// <summary>
+		/// 用户
+		/// </summary>
+		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 = "")
-        {
-            return Json(new
-            {
-                Success = isTrue,
-                Message = message,
-                Data = data
-            });
-        }
+		/// <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()
-            {
-                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()
-                {
-                    SameSite = SameSiteMode.Lax
-                });
-            }
+		/// <summary>
+		/// 登录页
+		/// </summary>
+		/// <returns></returns>
+		public ActionResult Login()
+		{
+			var keys = RsaCrypt.GenerateRsaKeys(RsaKeyType.PKCS1);
+			Response.Cookies.Append(nameof(keys.PublicKey), keys.PublicKey, new CookieOptions()
+			{
+				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()
+				{
+					SameSite = SameSiteMode.Lax
+				});
+			}
 
-            if (HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo) != null)
-            {
-                if (string.IsNullOrEmpty(from))
-                {
-                    return RedirectToAction("Index", "Home");
-                }
+			if (HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo) != null)
+			{
+				if (string.IsNullOrEmpty(from))
+				{
+					return RedirectToAction("Index", "Home");
+				}
 
-                return LocalRedirect(from);
-            }
+				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");
-                    }
+			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 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)
+		{
+			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, "用户名或密码不能为空");
-            }
+			HttpContext.Session.Remove("valid"); //验证成功就销毁验证码Session,非常重要
+			if (string.IsNullOrEmpty(username.Trim()) || string.IsNullOrEmpty(password.Trim()))
+			{
+				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)
-            {
-                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));
-                }
+			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)
+				{
+					FirewallRepoter.ReportAsync(IPAddress.Parse(ClientIP)).ContinueWith(_ => LogManager.Info($"多次登录用户名或密码错误,疑似爆破行为,已上报IP{ClientIP}至:" + FirewallRepoter.ReporterName));
+				}
 
-                return ResultData(null, false, "用户名或密码错误");
-            }
+				return ResultData(null, false, "用户名或密码错误");
+			}
 
-            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
-                });
-            }
+			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
+				});
+			}
 
-            BackgroundJob.Enqueue<IHangfireBackJob>(job => job.LoginRecord(userInfo, ClientIP, LoginType.Default));
-            string refer = Request.Cookies["refer"];
-            Response.Cookies.Delete(nameof(RsaKey.PublicKey), new CookieOptions()
-            {
-                SameSite = SameSiteMode.Lax
-            });
-            Response.Cookies.Delete("refer", new CookieOptions()
-            {
-                SameSite = SameSiteMode.Lax
-            });
-            HttpContext.Session.Remove(nameof(RsaKey.PrivateKey));
-            return ResultData(null, true, string.IsNullOrEmpty(refer) ? "/" : refer);
-        }
+			BackgroundJob.Enqueue<IHangfireBackJob>(job => job.LoginRecord(userInfo, ClientIP, LoginType.Default));
+			string refer = Request.Cookies["refer"];
+			Response.Cookies.Delete(nameof(RsaKey.PublicKey), new CookieOptions()
+			{
+				SameSite = SameSiteMode.Lax
+			});
+			Response.Cookies.Delete("refer", new CookieOptions()
+			{
+				SameSite = SameSiteMode.Lax
+			});
+			HttpContext.Session.Remove(nameof(RsaKey.PrivateKey));
+			return ResultData(null, true, string.IsNullOrEmpty(refer) ? "/" : refer);
+		}
 
-        /// <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 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>
-        /// <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))
-            {
-                return ResultData(null, false, "验证码错误");
-            }
+		/// <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))
+			{
+				return ResultData(null, false, "验证码错误");
+			}
 
-            return ResultData(null, false, "验证码正确");
-        }
+			return ResultData(null, false, "验证码正确");
+		}
 
-        /// <summary>
-        /// 获取用户信息
-        /// </summary>
-        /// <returns></returns>
-        public ActionResult GetUserInfo()
-        {
-            var user = HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
+		/// <summary>
+		/// 获取用户信息
+		/// </summary>
+		/// <returns></returns>
+		public ActionResult GetUserInfo()
+		{
+			var user = HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
 #if DEBUG
-            user = UserInfoService.GetByUsername("masuit").Mapper<UserInfoDto>();
+			user = UserInfoService.GetByUsername("masuit").Mapper<UserInfoDto>();
 #endif
 
-            return ResultData(user);
-        }
+			return ResultData(user);
+		}
 
-        /// <summary>
-        /// 注销登录
-        /// </summary>
-        /// <returns></returns>
-        public ActionResult Logout()
-        {
-            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: "注销成功!");
-        }
-    }
+		/// <summary>
+		/// 注销登录
+		/// </summary>
+		/// <returns></returns>
+		public ActionResult Logout()
+		{
+			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: "注销成功!");
+		}
+	}
 }

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

@@ -48,7 +48,7 @@ namespace Masuit.MyBlogs.Core.Controllers;
 /// <summary>
 /// 文章管理
 /// </summary>
-public class PostController : BaseController
+public sealed class PostController : BaseController
 {
 	public IPostService PostService { get; set; }
 

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

@@ -19,7 +19,7 @@ namespace Masuit.MyBlogs.Core.Controllers
     /// <summary>
     /// 站内搜索
     /// </summary>
-    public class SearchController : BaseController
+    public sealed class SearchController : BaseController
     {
         public ISearchDetailsService SearchDetailsService { get; set; }
 

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

@@ -21,7 +21,7 @@ namespace Masuit.MyBlogs.Core.Controllers
     /// <summary>
     /// 专题页
     /// </summary>
-    public class SeminarController : BaseController
+    public sealed class SeminarController : BaseController
     {
         /// <summary>
         /// 专题

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

@@ -10,7 +10,7 @@ namespace Masuit.MyBlogs.Core.Controllers
     /// <summary>
     /// 快速分享
     /// </summary>
-    public class ShareController : AdminController
+    public sealed class ShareController : AdminController
     {
         /// <summary>
         /// 快速分享

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

@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc;
 
 namespace Masuit.MyBlogs.Core.Controllers
 {
-    public class ShortController : Controller
+    public sealed class ShortController : Controller
     {
         public IRedisClient RedisHelper { get; set; }
         [HttpGet("short"), MyAuthorize, AllowAccessFirewall]

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

@@ -27,7 +27,7 @@ namespace Masuit.MyBlogs.Core.Controllers
 	/// <summary>
 	/// 订阅服务
 	/// </summary>
-	public class SubscribeController : Controller
+	public sealed class SubscribeController : Controller
 	{
 		public IPostService PostService { get; set; }
 

+ 389 - 389
src/Masuit.MyBlogs.Core/Controllers/SystemController.cs

@@ -8,6 +8,7 @@ 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;
@@ -18,397 +19,396 @@ using System.Diagnostics;
 using System.Net;
 using System.Net.Sockets;
 using System.Text;
-using Masuit.Tools.Hardware;
 using PerformanceCounter = Masuit.MyBlogs.Core.Common.PerformanceCounter;
 
 namespace Masuit.MyBlogs.Core.Controllers
 {
-    /// <summary>
-    /// 系统设置
-    /// </summary>
-    public class SystemController : AdminController
-    {
-        /// <summary>
-        /// 系统设置
-        /// </summary>
-        public ISystemSettingService SystemSettingService { get; set; }
-
-        public IPerfCounter PerfCounter { get; set; }
-
-        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)
-        {
-            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
-            {
-                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[]
-                {
-                    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
-            {
-                s.Name,
-                s.Value
-            }).ToList();
-            return ResultData(list);
-        }
-
-        /// <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;
-            }
-
-            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)
-            {
-                list.Add(new
-                {
-                    e,
-                    name = e.GetDisplay()
-                });
-            }
-
-            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
-            {
-                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);
-            }
-        }
-
-        /// <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();
-        }
-
-        /// <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, "根路径正确");
-            }
-
-            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>
-        /// <returns></returns>
-        public ActionResult<List<JObject>> SendBox()
-        {
-            return RedisHelper.SUnion(RedisHelper.Keys("Email:*")).Select(JObject.Parse).OrderByDescending(o => o["time"]).ToList();
-        }
-
-        public ActionResult BounceEmail([FromServices] IMailSender mailSender, [FromBodyOrDefault] string email)
-        {
-            var msg = mailSender.AddRecipient(email);
-            return Ok(new
-            {
-                msg
-            });
-        }
-
-        #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)
-            {
-                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));
-        }
-
-        /// <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>
-        /// <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
-            {
-                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
-                    {
-                        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>
-        /// 将IP添加到白名单
-        /// </summary>
-        /// <param name="ip"></param>
-        /// <returns></returns>
-        public async Task<ActionResult> AddToWhiteList([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);
-        }
-
-        /// <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);
-            }
-
-            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 网站防火墙
-    }
+	/// <summary>
+	/// 系统设置
+	/// </summary>
+	public sealed class SystemController : AdminController
+	{
+		/// <summary>
+		/// 系统设置
+		/// </summary>
+		public ISystemSettingService SystemSettingService { get; set; }
+
+		public IPerfCounter PerfCounter { get; set; }
+
+		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)
+		{
+			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
+			{
+				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[]
+				{
+					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
+			{
+				s.Name,
+				s.Value
+			}).ToList();
+			return ResultData(list);
+		}
+
+		/// <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;
+			}
+
+			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)
+			{
+				list.Add(new
+				{
+					e,
+					name = e.GetDisplay()
+				});
+			}
+
+			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
+			{
+				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);
+			}
+		}
+
+		/// <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();
+		}
+
+		/// <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, "根路径正确");
+			}
+
+			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>
+		/// <returns></returns>
+		public ActionResult<List<JObject>> SendBox()
+		{
+			return RedisHelper.SUnion(RedisHelper.Keys("Email:*")).Select(JObject.Parse).OrderByDescending(o => o["time"]).ToList();
+		}
+
+		public ActionResult BounceEmail([FromServices] IMailSender mailSender, [FromBodyOrDefault] string email)
+		{
+			var msg = mailSender.AddRecipient(email);
+			return Ok(new
+			{
+				msg
+			});
+		}
+
+		#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)
+			{
+				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));
+		}
+
+		/// <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>
+		/// <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
+			{
+				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
+					{
+						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>
+		/// 将IP添加到白名单
+		/// </summary>
+		/// <param name="ip"></param>
+		/// <returns></returns>
+		public async Task<ActionResult> AddToWhiteList([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);
+		}
+
+		/// <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);
+			}
+
+			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 网站防火墙
+	}
 }

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

@@ -21,7 +21,7 @@ namespace Masuit.MyBlogs.Core.Controllers
     /// 黑科技
     /// </summary>
     [Route("tools")]
-    public class ToolsController : BaseController
+    public sealed class ToolsController : BaseController
     {
         private readonly HttpClient _httpClient;
 

+ 262 - 264
src/Masuit.MyBlogs.Core/Controllers/UploadController.cs

@@ -12,7 +12,6 @@ using Masuit.Tools.Html;
 using Masuit.Tools.Logging;
 using Masuit.Tools.Systems;
 using Microsoft.AspNetCore.Mvc;
-using Newtonsoft.Json;
 using OpenXmlPowerTools;
 using Polly;
 using System.ComponentModel.DataAnnotations;
@@ -20,290 +19,289 @@ using System.Diagnostics;
 using System.Text.RegularExpressions;
 using System.Xml.Linq;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 文件上传
+/// </summary>
+[ApiExplorerSettings(IgnoreApi = true)]
+[ServiceFilter(typeof(FirewallAttribute))]
+public sealed class UploadController : Controller
 {
-    /// <summary>
-    /// 文件上传
-    /// </summary>
-    [ApiExplorerSettings(IgnoreApi = true)]
-    [ServiceFilter(typeof(FirewallAttribute))]
-    public class UploadController : Controller
-    {
-        public IWebHostEnvironment HostEnvironment { get; set; }
+	public IWebHostEnvironment HostEnvironment { get; set; }
 
-        public ActionResult ResultData(object data, bool isTrue = true, string message = "")
-        {
-            return Json(new
-            {
-                Success = isTrue,
-                Message = message,
-                Data = data
-            });
-        }
+	public ActionResult ResultData(object data, bool isTrue = true, string message = "")
+	{
+		return Json(new
+		{
+			Success = isTrue,
+			Message = message,
+			Data = data
+		});
+	}
 
-        #region Word上传转码
+	#region Word上传转码
 
-        /// <summary>
-        /// 上传Word转码
-        /// </summary>
-        /// <returns></returns>
-        [HttpPost]
-        public async Task<ActionResult> UploadWord(CancellationToken cancellationToken)
-        {
-            var form = await Request.ReadFormAsync(cancellationToken);
-            var files = form.Files;
-            if (files.Count <= 0)
-            {
-                return ResultData(null, false, "请先选择您需要上传的文件!");
-            }
+	/// <summary>
+	/// 上传Word转码
+	/// </summary>
+	/// <returns></returns>
+	[HttpPost]
+	public async Task<ActionResult> UploadWord(CancellationToken cancellationToken)
+	{
+		var form = await Request.ReadFormAsync(cancellationToken);
+		var files = form.Files;
+		if (files.Count <= 0)
+		{
+			return ResultData(null, false, "请先选择您需要上传的文件!");
+		}
 
-            var file = files[0];
-            string fileName = file.FileName;
-            if (!Regex.IsMatch(Path.GetExtension(fileName), "doc|docx"))
-            {
-                return ResultData(null, false, "文件格式不支持,只能上传doc或者docx的文档!");
-            }
+		var file = files[0];
+		string fileName = file.FileName;
+		if (!Regex.IsMatch(Path.GetExtension(fileName), "doc|docx"))
+		{
+			return ResultData(null, false, "文件格式不支持,只能上传doc或者docx的文档!");
+		}
 
-            var html = await SaveAsHtml(file);
-            if (html.Length < 10)
-            {
-                return ResultData(null, false, "读取文件内容失败,请检查文件的完整性,建议另存后重新上传!");
-            }
+		var html = await SaveAsHtml(file);
+		if (html.Length < 10)
+		{
+			return ResultData(null, false, "读取文件内容失败,请检查文件的完整性,建议另存后重新上传!");
+		}
 
-            if (html.Length > 1000000)
-            {
-                return ResultData(null, false, "文档内容超长,服务器拒绝接收,请优化文档内容后再尝试重新上传!");
-            }
+		if (html.Length > 1000000)
+		{
+			return ResultData(null, false, "文档内容超长,服务器拒绝接收,请优化文档内容后再尝试重新上传!");
+		}
 
-            return ResultData(new
-            {
-                Title = Path.GetFileNameWithoutExtension(fileName),
-                Content = html
-            });
-        }
+		return ResultData(new
+		{
+			Title = Path.GetFileNameWithoutExtension(fileName),
+			Content = html
+		});
+	}
 
-        private static async Task<string> ConvertToHtml(IFormFile file)
-        {
-            var docfile = Path.Combine(Environment.GetEnvironmentVariable("temp") ?? "upload", file.FileName);
-            try
-            {
-                await using var ms = file.OpenReadStream();
-                await using var fs = System.IO.File.Create(docfile, 1024, FileOptions.DeleteOnClose);
-                await ms.CopyToAsync(fs);
-                using var doc = WordprocessingDocument.Open(fs, true);
-                var pageTitle = file.FileName;
-                var part = doc.CoreFilePropertiesPart;
-                if (part != null)
-                {
-                    pageTitle ??= (string)part.GetXDocument().Descendants(DC.title).FirstOrDefault();
-                }
+	private static async Task<string> ConvertToHtml(IFormFile file)
+	{
+		var docfile = Path.Combine(Environment.GetEnvironmentVariable("temp") ?? "upload", file.FileName);
+		try
+		{
+			await using var ms = file.OpenReadStream();
+			await using var fs = System.IO.File.Create(docfile, 1024, FileOptions.DeleteOnClose);
+			await ms.CopyToAsync(fs);
+			using var doc = WordprocessingDocument.Open(fs, true);
+			var pageTitle = file.FileName;
+			var part = doc.CoreFilePropertiesPart;
+			if (part != null)
+			{
+				pageTitle ??= (string)part.GetXDocument().Descendants(DC.title).FirstOrDefault();
+			}
 
-                var settings = new HtmlConverterSettings()
-                {
-                    PageTitle = pageTitle,
-                    FabricateCssClasses = false,
-                    RestrictToSupportedLanguages = false,
-                    RestrictToSupportedNumberingFormats = false,
-                    ImageHandler = imageInfo =>
-                    {
-                        var stream = new MemoryStream();
-                        imageInfo.Bitmap.Save(stream, imageInfo.Bitmap.RawFormat);
-                        var base64String = Convert.ToBase64String(stream.ToArray());
-                        return new XElement(Xhtml.img, new XAttribute(NoNamespace.src, $"data:{imageInfo.ContentType};base64," + base64String), imageInfo.ImgStyleAttribute, imageInfo.AltText != null ? new XAttribute(NoNamespace.alt, imageInfo.AltText) : null);
-                    }
-                };
-                var htmlElement = HtmlConverter.ConvertToHtml(doc, settings);
-                var html = new XDocument(new XDocumentType("html", null, null, null), htmlElement);
-                var htmlString = html.ToString(SaveOptions.DisableFormatting);
-                return htmlString;
-            }
-            finally
-            {
-                if (System.IO.File.Exists(docfile))
-                {
-                    Policy.Handle<IOException>().WaitAndRetry(5, i => TimeSpan.FromSeconds(1)).Execute(() => System.IO.File.Delete(docfile));
-                }
-            }
-        }
+			var settings = new HtmlConverterSettings()
+			{
+				PageTitle = pageTitle,
+				FabricateCssClasses = false,
+				RestrictToSupportedLanguages = false,
+				RestrictToSupportedNumberingFormats = false,
+				ImageHandler = imageInfo =>
+				{
+					var stream = new MemoryStream();
+					imageInfo.Bitmap.Save(stream, imageInfo.Bitmap.RawFormat);
+					var base64String = Convert.ToBase64String(stream.ToArray());
+					return new XElement(Xhtml.img, new XAttribute(NoNamespace.src, $"data:{imageInfo.ContentType};base64," + base64String), imageInfo.ImgStyleAttribute, imageInfo.AltText != null ? new XAttribute(NoNamespace.alt, imageInfo.AltText) : null);
+				}
+			};
+			var htmlElement = HtmlConverter.ConvertToHtml(doc, settings);
+			var html = new XDocument(new XDocumentType("html", null, null, null), htmlElement);
+			var htmlString = html.ToString(SaveOptions.DisableFormatting);
+			return htmlString;
+		}
+		finally
+		{
+			if (System.IO.File.Exists(docfile))
+			{
+				Policy.Handle<IOException>().WaitAndRetry(5, i => TimeSpan.FromSeconds(1)).Execute(() => System.IO.File.Delete(docfile));
+			}
+		}
+	}
 
-        private async Task<string> SaveAsHtml(IFormFile file)
-        {
-            var html = await ConvertToHtml(file);
-            var context = BrowsingContext.New(Configuration.Default);
-            var doc = context.OpenAsync(req => req.Content(html)).Result;
-            var body = doc.Body;
-            var nodes = body.GetElementsByTagName("img");
-            foreach (var img in nodes)
-            {
-                var attr = img.Attributes["src"].Value;
-                var strs = attr.Split(",");
-                var base64 = strs[1];
-                var bytes = Convert.FromBase64String(base64);
-                var ext = strs[0].Split(";")[0].Split("/")[1];
-                await using var image = new MemoryStream(bytes);
-                var imgFile = $"{SnowFlake.NewId}.{ext}";
-                var path = Path.Combine(HostEnvironment.WebRootPath, CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload").Trim('/', '\\'), "images", imgFile);
-                var dir = Path.GetDirectoryName(path);
-                Directory.CreateDirectory(dir);
-                await using var fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
-                await image.CopyToAsync(fs);
-                img.Attributes["src"].Value = path[HostEnvironment.WebRootPath.Length..].Replace("\\", "/");
-            }
+	private async Task<string> SaveAsHtml(IFormFile file)
+	{
+		var html = await ConvertToHtml(file);
+		var context = BrowsingContext.New(Configuration.Default);
+		var doc = context.OpenAsync(req => req.Content(html)).Result;
+		var body = doc.Body;
+		var nodes = body.GetElementsByTagName("img");
+		foreach (var img in nodes)
+		{
+			var attr = img.Attributes["src"].Value;
+			var strs = attr.Split(",");
+			var base64 = strs[1];
+			var bytes = Convert.FromBase64String(base64);
+			var ext = strs[0].Split(";")[0].Split("/")[1];
+			await using var image = new MemoryStream(bytes);
+			var imgFile = $"{SnowFlake.NewId}.{ext}";
+			var path = Path.Combine(HostEnvironment.WebRootPath, CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload").Trim('/', '\\'), "images", imgFile);
+			var dir = Path.GetDirectoryName(path);
+			Directory.CreateDirectory(dir);
+			await using var fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
+			await image.CopyToAsync(fs);
+			img.Attributes["src"].Value = path[HostEnvironment.WebRootPath.Length..].Replace("\\", "/");
+		}
 
-            return body.InnerHtml.HtmlSantinizerStandard().HtmlSantinizerCustom(attributes: new[] { "dir", "lang" });
-        }
+		return body.InnerHtml.HtmlSantinizerStandard().HtmlSantinizerCustom(attributes: new[] { "dir", "lang" });
+	}
 
-        private static async Task SaveFile(IFormFile file, string path)
-        {
-            var dir = Path.GetDirectoryName(path);
-            Directory.CreateDirectory(dir);
-            await using var fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
-            await file.CopyToAsync(fs);
-        }
+	private static async Task SaveFile(IFormFile file, string path)
+	{
+		var dir = Path.GetDirectoryName(path);
+		Directory.CreateDirectory(dir);
+		await using var fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
+		await file.CopyToAsync(fs);
+	}
 
-        #endregion Word上传转码
+	#endregion Word上传转码
 
-        /// <summary>
-        /// 文件下载
-        /// </summary>
-        /// <param name="path"></param>
-        /// <returns></returns>
-        [HttpGet("download/{**path}")]
-        public ActionResult Download([FromServices] IMimeMapper mimeMapper, [Required] string path)
-        {
-            if (string.IsNullOrEmpty(path)) return Content("null");
-            var file = Path.Combine(HostEnvironment.WebRootPath, CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload").Trim('/', '\\'), path.Trim('.', '/', '\\'));
-            if (System.IO.File.Exists(file))
-            {
-                return this.ResumePhysicalFile(file, mimeMapper.GetMimeFromPath(file), Path.GetFileName(file));
-            }
+	/// <summary>
+	/// 文件下载
+	/// </summary>
+	/// <param name="path"></param>
+	/// <returns></returns>
+	[HttpGet("download/{**path}")]
+	public ActionResult Download([FromServices] IMimeMapper mimeMapper, [Required] string path)
+	{
+		if (string.IsNullOrEmpty(path)) return Content("null");
+		var file = Path.Combine(HostEnvironment.WebRootPath, CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload").Trim('/', '\\'), path.Trim('.', '/', '\\'));
+		if (System.IO.File.Exists(file))
+		{
+			return this.ResumePhysicalFile(file, mimeMapper.GetMimeFromPath(file), Path.GetFileName(file));
+		}
 
-            return Content("null");
-        }
+		return Content("null");
+	}
 
-        /// <summary>
-        /// UEditor文件上传处理
-        /// </summary>
-        /// <returns></returns>
-        [Route("fileuploader")]
-        public async Task<ActionResult> UeditorFileUploader()
-        {
-            UserInfoDto user = HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo) ?? new UserInfoDto();
-            var action = Request.Query["action"].ToString() switch //通用
-            {
-                "config" => (Handler)new ConfigHandler(HttpContext),
-                "uploadimage" => new UploadHandler(HttpContext, new UploadConfig()
-                {
-                    AllowExtensions = UeditorConfig.GetStringList("imageAllowFiles"),
-                    PathFormat = "/" + CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload").Trim('/', '\\') + UeditorConfig.GetString("imagePathFormat"),
-                    SizeLimit = UeditorConfig.GetInt("imageMaxSize"),
-                    UploadFieldName = UeditorConfig.GetString("imageFieldName")
-                }),
-                "uploadscrawl" => new UploadHandler(HttpContext, new UploadConfig()
-                {
-                    AllowExtensions = new[]
-                    {
-                        ".png"
-                    },
-                    PathFormat = "/" + CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload").Trim('/', '\\') + UeditorConfig.GetString("scrawlPathFormat"),
-                    SizeLimit = UeditorConfig.GetInt("scrawlMaxSize"),
-                    UploadFieldName = UeditorConfig.GetString("scrawlFieldName"),
-                    Base64 = true,
-                    Base64Filename = "scrawl.png"
-                }),
-                "catchimage" => new CrawlerHandler(HttpContext),
-                _ => new NotSupportedHandler(HttpContext)
-            };
+	/// <summary>
+	/// UEditor文件上传处理
+	/// </summary>
+	/// <returns></returns>
+	[Route("fileuploader")]
+	public async Task<ActionResult> UeditorFileUploader()
+	{
+		UserInfoDto user = HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo) ?? new UserInfoDto();
+		var action = Request.Query["action"].ToString() switch //通用
+		{
+			"config" => (Handler)new ConfigHandler(HttpContext),
+			"uploadimage" => new UploadHandler(HttpContext, new UploadConfig()
+			{
+				AllowExtensions = UeditorConfig.GetStringList("imageAllowFiles"),
+				PathFormat = "/" + CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload").Trim('/', '\\') + UeditorConfig.GetString("imagePathFormat"),
+				SizeLimit = UeditorConfig.GetInt("imageMaxSize"),
+				UploadFieldName = UeditorConfig.GetString("imageFieldName")
+			}),
+			"uploadscrawl" => new UploadHandler(HttpContext, new UploadConfig()
+			{
+				AllowExtensions = new[]
+				{
+					".png"
+				},
+				PathFormat = "/" + CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload").Trim('/', '\\') + UeditorConfig.GetString("scrawlPathFormat"),
+				SizeLimit = UeditorConfig.GetInt("scrawlMaxSize"),
+				UploadFieldName = UeditorConfig.GetString("scrawlFieldName"),
+				Base64 = true,
+				Base64Filename = "scrawl.png"
+			}),
+			"catchimage" => new CrawlerHandler(HttpContext),
+			_ => new NotSupportedHandler(HttpContext)
+		};
 
-            if (user.IsAdmin)
-            {
-                switch (Request.Query["action"])//管理员用
-                {
-                    //case "uploadvideo":
-                    //    action = new UploadHandler(context, new UploadConfig()
-                    //    {
-                    //        AllowExtensions = UeditorConfig.GetStringList("videoAllowFiles"),
-                    //        PathFormat =  "/" + CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload") + UeditorConfig.GetString("videoPathFormat"),
-                    //        SizeLimit = UeditorConfig.GetInt("videoMaxSize"),
-                    //        UploadFieldName = UeditorConfig.GetString("videoFieldName")
-                    //    });
-                    //    break;
-                    case "uploadfile":
-                        action = new UploadHandler(HttpContext, new UploadConfig()
-                        {
-                            AllowExtensions = UeditorConfig.GetStringList("fileAllowFiles"),
-                            PathFormat = "/" + CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload").Trim('/', '\\') + UeditorConfig.GetString("filePathFormat"),
-                            SizeLimit = UeditorConfig.GetInt("fileMaxSize"),
-                            UploadFieldName = UeditorConfig.GetString("fileFieldName")
-                        });
-                        break;
+		if (user.IsAdmin)
+		{
+			switch (Request.Query["action"])//管理员用
+			{
+				//case "uploadvideo":
+				//    action = new UploadHandler(context, new UploadConfig()
+				//    {
+				//        AllowExtensions = UeditorConfig.GetStringList("videoAllowFiles"),
+				//        PathFormat =  "/" + CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload") + UeditorConfig.GetString("videoPathFormat"),
+				//        SizeLimit = UeditorConfig.GetInt("videoMaxSize"),
+				//        UploadFieldName = UeditorConfig.GetString("videoFieldName")
+				//    });
+				//    break;
+				case "uploadfile":
+					action = new UploadHandler(HttpContext, new UploadConfig()
+					{
+						AllowExtensions = UeditorConfig.GetStringList("fileAllowFiles"),
+						PathFormat = "/" + CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload").Trim('/', '\\') + UeditorConfig.GetString("filePathFormat"),
+						SizeLimit = UeditorConfig.GetInt("fileMaxSize"),
+						UploadFieldName = UeditorConfig.GetString("fileFieldName")
+					});
+					break;
 
-                        //case "listimage":
-                        //    action = new ListFileManager(context, CommonHelper.SystemSettings.GetOrAdd("UploadPath", "/upload") + UeditorConfig.GetString("imageManagerListPath"), UeditorConfig.GetStringList("imageManagerAllowFiles"));
-                        //    break;
-                        //case "listfile":
-                        //    action = new ListFileManager(context, CommonHelper.SystemSettings.GetOrAdd("UploadPath", "/upload") + UeditorConfig.GetString("fileManagerListPath"), UeditorConfig.GetStringList("fileManagerAllowFiles"));
-                        //    break;
-                }
-            }
+					//case "listimage":
+					//    action = new ListFileManager(context, CommonHelper.SystemSettings.GetOrAdd("UploadPath", "/upload") + UeditorConfig.GetString("imageManagerListPath"), UeditorConfig.GetStringList("imageManagerAllowFiles"));
+					//    break;
+					//case "listfile":
+					//    action = new ListFileManager(context, CommonHelper.SystemSettings.GetOrAdd("UploadPath", "/upload") + UeditorConfig.GetString("fileManagerListPath"), UeditorConfig.GetStringList("fileManagerAllowFiles"));
+					//    break;
+			}
+		}
 
-            string result = await action.Process();
-            return Content(result, ContentType.Json);
-        }
+		string result = await action.Process();
+		return Content(result, ContentType.Json);
+	}
 
-        /// <summary>
-        /// 上传文件
-        /// </summary>
-        /// <param name="imagebedClient"></param>
-        /// <param name="file"></param>
-        /// <param name="cancellationToken"></param>
-        /// <returns></returns>
-        [HttpPost("upload"), ApiExplorerSettings(IgnoreApi = false)]
-        public async Task<ActionResult> UploadFile([FromServices] ImagebedClient imagebedClient, IFormFile file, CancellationToken cancellationToken)
-        {
-            string path;
-            string filename = SnowFlake.GetInstance().GetUniqueId() + Path.GetExtension(file.FileName);
-            var pathBase = CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload").Trim('/', '\\');
-            switch (file.ContentType)
-            {
-                case var _ when file.ContentType.StartsWith("image"):
-                    {
-                        await using var stream = file.OpenReadStream();
-                        var (url, success) = await imagebedClient.UploadImage(stream, file.FileName, cancellationToken);
-                        if (success)
-                        {
-                            return ResultData(url);
-                        }
+	/// <summary>
+	/// 上传文件
+	/// </summary>
+	/// <param name="imagebedClient"></param>
+	/// <param name="file"></param>
+	/// <param name="cancellationToken"></param>
+	/// <returns></returns>
+	[HttpPost("upload"), ApiExplorerSettings(IgnoreApi = false)]
+	public async Task<ActionResult> UploadFile([FromServices] ImagebedClient imagebedClient, IFormFile file, CancellationToken cancellationToken)
+	{
+		string path;
+		string filename = SnowFlake.GetInstance().GetUniqueId() + Path.GetExtension(file.FileName);
+		var pathBase = CommonHelper.SystemSettings.GetOrAdd("UploadPath", "upload").Trim('/', '\\');
+		switch (file.ContentType)
+		{
+			case var _ when file.ContentType.StartsWith("image"):
+				{
+					await using var stream = file.OpenReadStream();
+					var (url, success) = await imagebedClient.UploadImage(stream, file.FileName, cancellationToken);
+					if (success)
+					{
+						return ResultData(url);
+					}
 
-                        path = Path.Combine(HostEnvironment.WebRootPath, pathBase, "images", filename);
-                        var dir = Path.GetDirectoryName(path);
-                        Directory.CreateDirectory(dir);
-                        await using var fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
-                        await file.CopyToAsync(fs, CancellationToken.None);
-                        break;
-                    }
-                case var _ when file.ContentType.StartsWith("audio") || file.ContentType.StartsWith("video"):
-                    path = Path.Combine(HostEnvironment.WebRootPath, pathBase, "media", filename);
-                    break;
+					path = Path.Combine(HostEnvironment.WebRootPath, pathBase, "images", filename);
+					var dir = Path.GetDirectoryName(path);
+					Directory.CreateDirectory(dir);
+					await using var fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
+					await file.CopyToAsync(fs, CancellationToken.None);
+					break;
+				}
+			case var _ when file.ContentType.StartsWith("audio") || file.ContentType.StartsWith("video"):
+				path = Path.Combine(HostEnvironment.WebRootPath, pathBase, "media", filename);
+				break;
 
-                case var _ when file.ContentType.StartsWith("text") || (ContentType.Doc + "," + ContentType.Xls + "," + ContentType.Ppt + "," + ContentType.Pdf).Contains(file.ContentType):
-                    path = Path.Combine(HostEnvironment.WebRootPath, pathBase, "docs", filename);
-                    break;
+			case var _ when file.ContentType.StartsWith("text") || (ContentType.Doc + "," + ContentType.Xls + "," + ContentType.Ppt + "," + ContentType.Pdf).Contains(file.ContentType):
+				path = Path.Combine(HostEnvironment.WebRootPath, pathBase, "docs", filename);
+				break;
 
-                default:
-                    path = Path.Combine(HostEnvironment.WebRootPath, pathBase, "files", filename);
-                    break;
-            }
-            try
-            {
-                await SaveFile(file, path);
-                return ResultData(path[HostEnvironment.WebRootPath.Length..].Replace("\\", "/"));
-            }
-            catch (Exception e)
-            {
-                LogManager.Error(e.Demystify());
-                return ResultData(null, false, "文件上传失败!");
-            }
-        }
-    }
-}
+			default:
+				path = Path.Combine(HostEnvironment.WebRootPath, pathBase, "files", filename);
+				break;
+		}
+		try
+		{
+			await SaveFile(file, path);
+			return ResultData(path[HostEnvironment.WebRootPath.Length..].Replace("\\", "/"));
+		}
+		catch (Exception e)
+		{
+			LogManager.Error(e.Demystify());
+			return ResultData(null, false, "文件上传失败!");
+		}
+	}
+}

+ 130 - 131
src/Masuit.MyBlogs.Core/Controllers/UserController.cs

@@ -5,146 +5,145 @@ using Microsoft.AspNetCore.Mvc;
 using System.Linq.Expressions;
 using System.Net;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+/// <summary>
+/// 用户管理
+/// </summary>
+public sealed class UserController : AdminController
 {
-    /// <summary>
-    /// 用户管理
-    /// </summary>
-    public class UserController : AdminController
-    {
-        /// <summary>
-        /// 修改用户名
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="username"></param>
-        /// <returns></returns>
-        public async Task<ActionResult> ChangeUsername([FromBodyOrDefault] int id, [FromBodyOrDefault] string username)
-        {
-            UserInfo userInfo = await UserInfoService.GetByIdAsync(id);
-            if (!username.Equals(userInfo.Username) && UserInfoService.UsernameExist(username))
-            {
-                return ResultData(null, false, $"用户名{username}已经存在,请尝试更换其他用户名!");
-            }
+	/// <summary>
+	/// 修改用户名
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="username"></param>
+	/// <returns></returns>
+	public async Task<ActionResult> ChangeUsername([FromBodyOrDefault] int id, [FromBodyOrDefault] string username)
+	{
+		UserInfo userInfo = await UserInfoService.GetByIdAsync(id);
+		if (!username.Equals(userInfo.Username) && UserInfoService.UsernameExist(username))
+		{
+			return ResultData(null, false, $"用户名{username}已经存在,请尝试更换其他用户名!");
+		}
 
-            userInfo.Username = username;
-            bool b = await UserInfoService.SaveChangesAsync() > 0;
-            return ResultData(Mapper.Map<UserInfoDto>(userInfo), b, b ? $"用户名修改成功,新用户名为{username}。" : "用户名修改失败!");
-        }
+		userInfo.Username = username;
+		bool b = await UserInfoService.SaveChangesAsync() > 0;
+		return ResultData(Mapper.Map<UserInfoDto>(userInfo), b, b ? $"用户名修改成功,新用户名为{username}。" : "用户名修改失败!");
+	}
 
-        /// <summary>
-        /// 修改昵称
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="username"></param>
-        /// <returns></returns>
-        public async Task<ActionResult> ChangeNickName([FromBodyOrDefault] int id, [FromBodyOrDefault] string username)
-        {
-            UserInfo userInfo = await UserInfoService.GetByIdAsync(id);
-            userInfo.NickName = username;
-            bool b = await UserInfoService.SaveChangesAsync() > 0;
-            return ResultData(Mapper.Map<UserInfoDto>(userInfo), b, b ? $"昵称修改成功,新昵称为{username}。" : "昵称修改失败!");
-        }
+	/// <summary>
+	/// 修改昵称
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="username"></param>
+	/// <returns></returns>
+	public async Task<ActionResult> ChangeNickName([FromBodyOrDefault] int id, [FromBodyOrDefault] string username)
+	{
+		UserInfo userInfo = await UserInfoService.GetByIdAsync(id);
+		userInfo.NickName = username;
+		bool b = await UserInfoService.SaveChangesAsync() > 0;
+		return ResultData(Mapper.Map<UserInfoDto>(userInfo), b, b ? $"昵称修改成功,新昵称为{username}。" : "昵称修改失败!");
+	}
 
-        /// <summary>
-        /// 修改密码
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="old"></param>
-        /// <param name="pwd"></param>
-        /// <param name="pwd2"></param>
-        /// <returns></returns>
-        public ActionResult ChangePassword([FromBodyOrDefault] int id, [FromBodyOrDefault] string old, [FromBodyOrDefault] string pwd, [FromBodyOrDefault] string pwd2)
-        {
-            if (pwd.Equals(pwd2))
-            {
-                bool b = UserInfoService.ChangePassword(id, old, pwd);
-                return ResultData(null, b, b ? $"密码修改成功,新密码为:{pwd}!" : "密码修改失败,可能是原密码不正确!");
-            }
+	/// <summary>
+	/// 修改密码
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="old"></param>
+	/// <param name="pwd"></param>
+	/// <param name="pwd2"></param>
+	/// <returns></returns>
+	public ActionResult ChangePassword([FromBodyOrDefault] int id, [FromBodyOrDefault] string old, [FromBodyOrDefault] string pwd, [FromBodyOrDefault] string pwd2)
+	{
+		if (pwd.Equals(pwd2))
+		{
+			bool b = UserInfoService.ChangePassword(id, old, pwd);
+			return ResultData(null, b, b ? $"密码修改成功,新密码为:{pwd}!" : "密码修改失败,可能是原密码不正确!");
+		}
 
-            return ResultData(null, false, "两次输入的密码不一致!");
-        }
+		return ResultData(null, false, "两次输入的密码不一致!");
+	}
 
-        /// <summary>
-        /// 重置密码
-        /// </summary>
-        /// <param name="name">用户名</param>
-        /// <param name="pwd"></param>
-        /// <returns></returns>
-        public ActionResult ResetPassword([FromBodyOrDefault] string name, [FromBodyOrDefault] string pwd)
-        {
-            bool b = UserInfoService.ResetPassword(name, pwd);
-            return ResultData(null, b, b ? $"密码重置成功,新密码为:{pwd}!" : "密码重置失败!");
-        }
+	/// <summary>
+	/// 重置密码
+	/// </summary>
+	/// <param name="name">用户名</param>
+	/// <param name="pwd"></param>
+	/// <returns></returns>
+	public ActionResult ResetPassword([FromBodyOrDefault] string name, [FromBodyOrDefault] string pwd)
+	{
+		bool b = UserInfoService.ResetPassword(name, pwd);
+		return ResultData(null, b, b ? $"密码重置成功,新密码为:{pwd}!" : "密码重置失败!");
+	}
 
-        /// <summary>
-        /// 修改头像
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="path"></param>
-        /// <returns></returns>
-        public async Task<ActionResult> ChangeAvatar([FromBodyOrDefault] int id, [FromBodyOrDefault] string path)
-        {
-            UserInfo userInfo = await UserInfoService.GetByIdAsync(id);
-            userInfo.Avatar = path;
-            bool b = await UserInfoService.SaveChangesAsync() > 0;
-            return ResultData(Mapper.Map<UserInfoDto>(userInfo), b, b ? "头像修改成功。" : "头像修改失败!");
-        }
+	/// <summary>
+	/// 修改头像
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="path"></param>
+	/// <returns></returns>
+	public async Task<ActionResult> ChangeAvatar([FromBodyOrDefault] int id, [FromBodyOrDefault] string path)
+	{
+		UserInfo userInfo = await UserInfoService.GetByIdAsync(id);
+		userInfo.Avatar = path;
+		bool b = await UserInfoService.SaveChangesAsync() > 0;
+		return ResultData(Mapper.Map<UserInfoDto>(userInfo), b, b ? "头像修改成功。" : "头像修改失败!");
+	}
 
-        /// <summary>
-        /// 手动添加或修改用户
-        /// </summary>
-        /// <param name="model"></param>
-        /// <returns></returns>
-        [HttpPost]
-        [ProducesResponseType((int)HttpStatusCode.OK)]
-        public async Task<IActionResult> Save([FromBodyOrDefault] UserInfoDto model)
-        {
-            var userInfo = UserInfoService.GetByUsername(model.Username);
-            if (userInfo is null)
-            {
-                userInfo = Mapper.Map<UserInfo>(model);
-                userInfo.Password = "123456";
-                UserInfoService.Register(userInfo);
-                return ResultData(null, true, "用户保存成功");
-            }
+	/// <summary>
+	/// 手动添加或修改用户
+	/// </summary>
+	/// <param name="model"></param>
+	/// <returns></returns>
+	[HttpPost]
+	[ProducesResponseType((int)HttpStatusCode.OK)]
+	public async Task<IActionResult> Save([FromBodyOrDefault] UserInfoDto model)
+	{
+		var userInfo = UserInfoService.GetByUsername(model.Username);
+		if (userInfo is null)
+		{
+			userInfo = Mapper.Map<UserInfo>(model);
+			userInfo.Password = "123456";
+			UserInfoService.Register(userInfo);
+			return ResultData(null, true, "用户保存成功");
+		}
 
-            Mapper.Map(model, userInfo);
-            var b = await UserInfoService.SaveChangesAsync() > 0;
-            return ResultData(null, b, b ? "用户保存成功" : "用户保存失败");
-        }
+		Mapper.Map(model, userInfo);
+		var b = await UserInfoService.SaveChangesAsync() > 0;
+		return ResultData(null, b, b ? "用户保存成功" : "用户保存失败");
+	}
 
-        /// <summary>
-        /// 删除用户
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [HttpPost]
-        [ProducesResponseType((int)HttpStatusCode.OK)]
-        public async Task<IActionResult> Delete([FromBodyOrDefault] int id)
-        {
-            await UserInfoService.DeleteByIdAsync(id);
-            return ResultData(null);
-        }
+	/// <summary>
+	/// 删除用户
+	/// </summary>
+	/// <param name="id"></param>
+	/// <returns></returns>
+	[HttpPost]
+	[ProducesResponseType((int)HttpStatusCode.OK)]
+	public async Task<IActionResult> Delete([FromBodyOrDefault] int id)
+	{
+		await UserInfoService.DeleteByIdAsync(id);
+		return ResultData(null);
+	}
 
-        /// <summary>
-        /// 获取用户列表
-        /// </summary>
-        /// <param name="page"></param>
-        /// <param name="size"></param>
-        /// <param name="search"></param>
-        /// <returns></returns>
-        [HttpGet]
-        [ProducesResponseType((int)HttpStatusCode.OK)]
-        public IActionResult GetUsers(int page, int size, string search)
-        {
-            Expression<Func<UserInfo, bool>> where = info => true;
-            if (!string.IsNullOrEmpty(search))
-            {
-                where = u => u.Username.Contains(search) || u.NickName.Contains(search) || u.Email.Contains(search) || u.QQorWechat.Contains(search);
-            }
+	/// <summary>
+	/// 获取用户列表
+	/// </summary>
+	/// <param name="page"></param>
+	/// <param name="size"></param>
+	/// <param name="search"></param>
+	/// <returns></returns>
+	[HttpGet]
+	[ProducesResponseType((int)HttpStatusCode.OK)]
+	public IActionResult GetUsers(int page, int size, string search)
+	{
+		Expression<Func<UserInfo, bool>> where = info => true;
+		if (!string.IsNullOrEmpty(search))
+		{
+			where = u => u.Username.Contains(search) || u.NickName.Contains(search) || u.Email.Contains(search) || u.QQorWechat.Contains(search);
+		}
 
-            var pages = UserInfoService.GetPages<int, UserInfoDto>(page, size, where, u => u.Id, false);
-            return Ok(pages);
-        }
-    }
-}
+		var pages = UserInfoService.GetPages<int, UserInfoDto>(page, size, where, u => u.Id, false);
+		return Ok(pages);
+	}
+}

+ 27 - 28
src/Masuit.MyBlogs.Core/Controllers/ValidateController.cs

@@ -4,39 +4,38 @@ using Masuit.Tools.Core.Validator;
 using Masuit.Tools.Systems;
 using Microsoft.AspNetCore.Mvc;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+public sealed class ValidateController : BaseController
 {
-    public class ValidateController : BaseController
-    {
-        /// <summary>
-        /// 发送验证码
-        /// </summary>
-        /// <param name="email"></param>
-        /// <returns></returns>
-        [HttpPost, ValidateAntiForgeryToken, ResponseCache(Duration = 115, VaryByQueryKeys = new[] { "email" })]
-        public ActionResult SendCode(string email)
-        {
-            var validator = new IsEmailAttribute();
-            if (!validator.IsValid(email))
-            {
-                return ResultData(null, false, validator.ErrorMessage);
-            }
+	/// <summary>
+	/// 发送验证码
+	/// </summary>
+	/// <param name="email"></param>
+	/// <returns></returns>
+	[HttpPost, ValidateAntiForgeryToken, ResponseCache(Duration = 115, VaryByQueryKeys = new[] { "email" })]
+	public ActionResult SendCode(string email)
+	{
+		var validator = new IsEmailAttribute();
+		if (!validator.IsValid(email))
+		{
+			return ResultData(null, false, validator.ErrorMessage);
+		}
 
-            if (RedisHelper.Exists("get:" + email))
-            {
-                RedisHelper.Expire("get:" + email, 120);
-                return ResultData(null, false, "发送频率限制,请在2分钟后重新尝试发送邮件!请检查你的邮件,若未收到,请检查你的邮箱地址或邮件垃圾箱!");
-            }
+		if (RedisHelper.Exists("get:" + email))
+		{
+			RedisHelper.Expire("get:" + email, 120);
+			return ResultData(null, false, "发送频率限制,请在2分钟后重新尝试发送邮件!请检查你的邮件,若未收到,请检查你的邮箱地址或邮件垃圾箱!");
+		}
 
-            string code = SnowFlake.GetInstance().GetUniqueShortId(6);
-            RedisHelper.Set("code:" + email, code, 86400);
-            BackgroundJob.Enqueue(() => CommonHelper.SendMail(Request.Host + "博客验证码", $"{Request.Host}本次验证码是:<span style='color:red'>{code}</span>,有效期为24h,请按时使用!", email, ClientIP));
-            RedisHelper.Set("get:" + email, code, 120);
+		string code = SnowFlake.GetInstance().GetUniqueShortId(6);
+		RedisHelper.Set("code:" + email, code, 86400);
+		BackgroundJob.Enqueue(() => CommonHelper.SendMail(Request.Host + "博客验证码", $"{Request.Host}本次验证码是:<span style='color:red'>{code}</span>,有效期为24h,请按时使用!", email, ClientIP));
+		RedisHelper.Set("get:" + email, code, 120);
 #if !DEBUG
-            return ResultData(null, true, "验证码发送成功!");
+		return ResultData(null, true, "验证码发送成功!");
 #else
             return ResultData(null, true, "验证码:" + code);
 #endif
-        }
-    }
+	}
 }

+ 25 - 26
src/Masuit.MyBlogs.Core/Controllers/ValuesController.cs

@@ -5,33 +5,32 @@ using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
 using Z.EntityFramework.Plus;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+[Route("values")]
+public sealed class ValuesController : AdminController
 {
-    [Route("values")]
-    public class ValuesController : AdminController
-    {
-        public IVariablesService VariablesService { get; set; }
+	public IVariablesService VariablesService { get; set; }
 
-        [HttpGet("list")]
-        public async Task<ActionResult> GetAll()
-        {
-            return ResultData(await VariablesService.GetAllNoTracking().ToListAsync());
-        }
+	[HttpGet("list")]
+	public async Task<ActionResult> GetAll()
+	{
+		return ResultData(await VariablesService.GetAllNoTracking().ToListAsync());
+	}
 
-        [HttpPost]
-        public async Task<ActionResult> Save([FromBodyOrDefault] Variables model)
-        {
-            var b = await VariablesService.AddOrUpdateSavedAsync(v => v.Key, model) > 0;
-            QueryCacheManager.ExpireType<Variables>();
-            return ResultData(null, b, b ? "保存成功" : "保存失败");
-        }
+	[HttpPost]
+	public async Task<ActionResult> Save([FromBodyOrDefault] Variables model)
+	{
+		var b = await VariablesService.AddOrUpdateSavedAsync(v => v.Key, model) > 0;
+		QueryCacheManager.ExpireType<Variables>();
+		return ResultData(null, b, b ? "保存成功" : "保存失败");
+	}
 
-        [HttpPost("{id:int}")]
-        public ActionResult Delete(int id)
-        {
-            var b = VariablesService - id;
-            QueryCacheManager.ExpireType<Variables>();
-            return ResultData(null, b, b ? "删除成功" : "保存失败");
-        }
-    }
-}
+	[HttpPost("{id:int}")]
+	public ActionResult Delete(int id)
+	{
+		var b = VariablesService - id;
+		QueryCacheManager.ExpireType<Variables>();
+		return ResultData(null, b, b ? "删除成功" : "保存失败");
+	}
+}

+ 68 - 69
src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/OneDriveConfiguration.cs

@@ -1,86 +1,85 @@
 using Microsoft.Extensions.Primitives;
 
-namespace Masuit.MyBlogs.Core.Extensions.DriveHelpers
+namespace Masuit.MyBlogs.Core.Extensions.DriveHelpers;
+
+public sealed class OneDriveConfiguration
 {
-    public class OneDriveConfiguration
-    {
-        private static IConfigurationRoot ConfigurationRoot;
-        static OneDriveConfiguration()
-        {
-            void BindConfig()
-            {
-                var builder = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json", true, true);
-                ConfigurationRoot = builder.Build();
-            }
+	private static IConfigurationRoot ConfigurationRoot;
+	static OneDriveConfiguration()
+	{
+		void BindConfig()
+		{
+			var builder = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json", true, true);
+			ConfigurationRoot = builder.Build();
+		}
 
-            BindConfig();
-            ChangeToken.OnChange(ConfigurationRoot.GetReloadToken, BindConfig);
-        }
+		BindConfig();
+		ChangeToken.OnChange(ConfigurationRoot.GetReloadToken, BindConfig);
+	}
 
-        /// <summary>
-        /// 数据库连接字符串
-        /// </summary>
-        public static string ConnectionString => ConfigurationRoot["OneDrive:ConnectionString"];
+	/// <summary>
+	/// 数据库连接字符串
+	/// </summary>
+	public static string ConnectionString => ConfigurationRoot["OneDrive:ConnectionString"];
 
-        /// <summary>
-        /// Graph连接 ClientID
-        /// </summary>
-        public static string ClientId => ConfigurationRoot["OneDrive:ClientId"];
+	/// <summary>
+	/// Graph连接 ClientID
+	/// </summary>
+	public static string ClientId => ConfigurationRoot["OneDrive:ClientId"];
 
-        /// <summary>
-        /// Graph连接 ClientSecret
-        /// </summary>
-        public static string ClientSecret => ConfigurationRoot["OneDrive:ClientSecret"];
+	/// <summary>
+	/// Graph连接 ClientSecret
+	/// </summary>
+	public static string ClientSecret => ConfigurationRoot["OneDrive:ClientSecret"];
 
-        /// <summary>
-        /// Binding 回调 Url
-        /// </summary>
-        public static string BaseUri => ConfigurationRoot["OneDrive:BaseUri"];
+	/// <summary>
+	/// Binding 回调 Url
+	/// </summary>
+	public static string BaseUri => ConfigurationRoot["OneDrive:BaseUri"];
 
-        /// <summary>
-        /// 返回 Scopes
-        /// </summary>
-        /// <value></value>
-        public static string[] Scopes => new[] { "Sites.ReadWrite.All", "Files.ReadWrite.All" };
+	/// <summary>
+	/// 返回 Scopes
+	/// </summary>
+	/// <value></value>
+	public static string[] Scopes => new[] { "Sites.ReadWrite.All", "Files.ReadWrite.All" };
 
-        /// <summary>
-        /// 代理路径
-        /// </summary>
-        public static string Proxy => ConfigurationRoot["OneDrive:Proxy"];
+	/// <summary>
+	/// 代理路径
+	/// </summary>
+	public static string Proxy => ConfigurationRoot["OneDrive:Proxy"];
 
-        /// <summary>
-        /// 账户名称
-        /// </summary>
-        public static string AccountName => ConfigurationRoot["OneDrive:AccountName"];
+	/// <summary>
+	/// 账户名称
+	/// </summary>
+	public static string AccountName => ConfigurationRoot["OneDrive:AccountName"];
 
-        /// <summary>
-        /// 域名
-        /// </summary>
-        public static string DominName => ConfigurationRoot["OneDrive:DominName"];
+	/// <summary>
+	/// 域名
+	/// </summary>
+	public static string DominName => ConfigurationRoot["OneDrive:DominName"];
 
-        /// <summary>
-        /// Office 类型
-        /// </summary>
-        /// <param name="="></param>
-        /// <returns></returns>
-        public static OfficeType Type => (ConfigurationRoot["OneDrive:Type"] == "China") ? OfficeType.China : OfficeType.Global;
+	/// <summary>
+	/// Office 类型
+	/// </summary>
+	/// <param name="="></param>
+	/// <returns></returns>
+	public static OfficeType Type => (ConfigurationRoot["OneDrive:Type"] == "China") ? OfficeType.China : OfficeType.Global;
 
-        /// <summary>
-        /// Graph Api
-        /// </summary>
-        /// <param name="="></param>
-        /// <returns></returns>
-        public static string GraphApi => (ConfigurationRoot["OneDrive:Type"] == "China") ? "https://microsoftgraph.chinacloudapi.cn" : "https://graph.microsoft.com";
+	/// <summary>
+	/// Graph Api
+	/// </summary>
+	/// <param name="="></param>
+	/// <returns></returns>
+	public static string GraphApi => (ConfigurationRoot["OneDrive:Type"] == "China") ? "https://microsoftgraph.chinacloudapi.cn" : "https://graph.microsoft.com";
 
-        /// <summary>
-        /// CDN URL
-        /// </summary>
-        public static string[] CDNUrls => ConfigurationRoot.GetSection("OneDrive:CDNUrls").GetChildren().Select(x => x.Value).ToArray();
+	/// <summary>
+	/// CDN URL
+	/// </summary>
+	public static string[] CDNUrls => ConfigurationRoot.GetSection("OneDrive:CDNUrls").GetChildren().Select(x => x.Value).ToArray();
 
-        public enum OfficeType
-        {
-            Global,
-            China
-        }
-    }
+	public enum OfficeType
+	{
+		Global,
+		China
+	}
 }

+ 63 - 64
src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/ProtectedApiCallHelper.cs

@@ -2,72 +2,71 @@ using Newtonsoft.Json;
 using Newtonsoft.Json.Linq;
 using System.Net.Http.Headers;
 
-namespace Masuit.MyBlogs.Core.Extensions.DriveHelpers
+namespace Masuit.MyBlogs.Core.Extensions.DriveHelpers;
+
+/// <summary>
+/// Helper class to call a protected API and process its result
+/// </summary>
+public sealed class ProtectedApiCallHelper
 {
-    /// <summary>
-    /// Helper class to call a protected API and process its result
-    /// </summary>
-    public class ProtectedApiCallHelper
-    {
-        /// <summary>
-        /// Constructor
-        /// </summary>
-        /// <param name="httpClient">HttpClient used to call the protected API</param>
-        public ProtectedApiCallHelper(HttpClient httpClient)
-        {
-            HttpClient = httpClient;
-        }
+	/// <summary>
+	/// Constructor
+	/// </summary>
+	/// <param name="httpClient">HttpClient used to call the protected API</param>
+	public ProtectedApiCallHelper(HttpClient httpClient)
+	{
+		HttpClient = httpClient;
+	}
 
-        protected HttpClient HttpClient { get; }
+	protected HttpClient HttpClient { get; }
 
 
-        /// <summary>
-        /// Calls the protected Web API and processes the result
-        /// </summary>
-        /// <param name="webApiUrl">Url of the Web API to call (supposed to return Json)</param>
-        /// <param name="accessToken">Access token used as a bearer security token to call the Web API</param>
-        /// <param name="processResult">Callback used to process the result of the call to the Web API</param>
-        public async Task CallWebApiAndProcessResultASync(string webApiUrl, string accessToken, Action<JObject> processResult, Method method = Method.Get, HttpContent sendContent = null)
-        {
-            if (!string.IsNullOrEmpty(accessToken))
-            {
-                var defaultRequetHeaders = HttpClient.DefaultRequestHeaders;
-                if (defaultRequetHeaders.Accept.All(m => m.MediaType != "application/json"))
-                {
-                    HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
-                }
-                defaultRequetHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
-                var response = method switch
-                {
-                    Method.Get => await HttpClient.GetAsync(webApiUrl),
-                    Method.Post => await HttpClient.PostAsync(webApiUrl, sendContent),
-                    Method.Put => await HttpClient.PutAsync(webApiUrl, sendContent),
-                    _ => new HttpResponseMessage()
-                };
-                if (response.IsSuccessStatusCode)
-                {
-                    string json = await response.Content.ReadAsStringAsync();
-                    JObject result = JsonConvert.DeserializeObject(json) as JObject;
-                    processResult(result);
-                }
-                else
-                {
-                    string content = await response.Content.ReadAsStringAsync();
-                    JObject result = JsonConvert.DeserializeObject(content) as JObject;
-                    throw new Exception(result.Property("error").Value["message"].ToString());
-                }
-            }
-            else
-            {
-                throw new Exception("未提供Token");
-            }
-        }
+	/// <summary>
+	/// Calls the protected Web API and processes the result
+	/// </summary>
+	/// <param name="webApiUrl">Url of the Web API to call (supposed to return Json)</param>
+	/// <param name="accessToken">Access token used as a bearer security token to call the Web API</param>
+	/// <param name="processResult">Callback used to process the result of the call to the Web API</param>
+	public async Task CallWebApiAndProcessResultASync(string webApiUrl, string accessToken, Action<JObject> processResult, Method method = Method.Get, HttpContent sendContent = null)
+	{
+		if (!string.IsNullOrEmpty(accessToken))
+		{
+			var defaultRequetHeaders = HttpClient.DefaultRequestHeaders;
+			if (defaultRequetHeaders.Accept.All(m => m.MediaType != "application/json"))
+			{
+				HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+			}
+			defaultRequetHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
+			var response = method switch
+			{
+				Method.Get => await HttpClient.GetAsync(webApiUrl),
+				Method.Post => await HttpClient.PostAsync(webApiUrl, sendContent),
+				Method.Put => await HttpClient.PutAsync(webApiUrl, sendContent),
+				_ => new HttpResponseMessage()
+			};
+			if (response.IsSuccessStatusCode)
+			{
+				string json = await response.Content.ReadAsStringAsync();
+				JObject result = JsonConvert.DeserializeObject(json) as JObject;
+				processResult(result);
+			}
+			else
+			{
+				string content = await response.Content.ReadAsStringAsync();
+				JObject result = JsonConvert.DeserializeObject(content) as JObject;
+				throw new Exception(result.Property("error").Value["message"].ToString());
+			}
+		}
+		else
+		{
+			throw new Exception("未提供Token");
+		}
+	}
 
-        public enum Method
-        {
-            Post,
-            Get,
-            Put
-        }
-    }
-}
+	public enum Method
+	{
+		Post,
+		Get,
+		Put
+	}
+}

+ 31 - 33
src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/TokenCacheHelper.cs

@@ -1,38 +1,36 @@
 using Microsoft.Identity.Client;
 
-namespace Masuit.MyBlogs.Core.Extensions.DriveHelpers
-{
-    /// <summary>
-    /// 缓存 Token
-    /// </summary>
-    static class TokenCacheHelper
-    {
-        /// <summary>
-        /// Path to the token cache
-        /// </summary>
-        public static readonly string CacheFilePath = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "TokenCache.bin");
+namespace Masuit.MyBlogs.Core.Extensions.DriveHelpers;
 
-        private static readonly object FileLock = new();
-        public static void EnableSerialization(ITokenCache tokenCache)
-        {
-            tokenCache.SetBeforeAccess(args =>
-            {
-                lock (FileLock)
-                {
-                    args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath) ? File.ReadAllBytes(CacheFilePath) : null);
-                }
-            });
-            tokenCache.SetAfterAccess(args =>
-            {
-                if (args.HasStateChanged)
-                {
-                    lock (FileLock)
-                    {
-                        File.WriteAllBytes(CacheFilePath, args.TokenCache.SerializeMsalV3());
-                    }
-                }
-            });
-        }
+/// <summary>
+/// 缓存 Token
+/// </summary>
+internal static class TokenCacheHelper
+{
+	/// <summary>
+	/// Path to the token cache
+	/// </summary>
+	public static readonly string CacheFilePath = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "TokenCache.bin");
 
-    }
+	private static readonly object FileLock = new();
+	public static void EnableSerialization(ITokenCache tokenCache)
+	{
+		tokenCache.SetBeforeAccess(args =>
+		{
+			lock (FileLock)
+			{
+				args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath) ? File.ReadAllBytes(CacheFilePath) : null);
+			}
+		});
+		tokenCache.SetAfterAccess(args =>
+		{
+			if (args.HasStateChanged)
+			{
+				lock (FileLock)
+				{
+					File.WriteAllBytes(CacheFilePath, args.TokenCache.SerializeMsalV3());
+				}
+			}
+		});
+	}
 }

+ 10 - 11
src/Masuit.MyBlogs.Core/Extensions/Firewall/AllowAccessFirewallAttribute.cs

@@ -1,17 +1,16 @@
 using Microsoft.AspNetCore.Mvc.Filters;
 
-namespace Masuit.MyBlogs.Core.Extensions.Firewall
+namespace Masuit.MyBlogs.Core.Extensions.Firewall;
+
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
+public sealed class AllowAccessFirewallAttribute : Attribute, IFilterFactory, IOrderedFilter
 {
-    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
-    public class AllowAccessFirewallAttribute : Attribute, IFilterFactory, IOrderedFilter
-    {
-        public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
-        {
-            return new AllowAccessFirewallAttribute();
-        }
+	public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
+	{
+		return new AllowAccessFirewallAttribute();
+	}
 
-        public bool IsReusable => true;
+	public bool IsReusable => true;
 
-        public int Order { get; }
-    }
+	public int Order { get; }
 }

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

@@ -6,7 +6,7 @@ using System.Net.Sockets;
 
 namespace Masuit.MyBlogs.Core.Extensions.Firewall
 {
-    public class CloudflareRepoter : IFirewallRepoter
+    public sealed class CloudflareRepoter : IFirewallRepoter
     {
         private readonly HttpClient _httpClient;
         private readonly IConfiguration _configuration;

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

@@ -2,7 +2,7 @@
 
 namespace Masuit.MyBlogs.Core.Extensions.Firewall
 {
-    public class DefaultFirewallRepoter : IFirewallRepoter
+    public sealed class DefaultFirewallRepoter : IFirewallRepoter
     {
         public string ReporterName { get; set; }
 

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

@@ -22,7 +22,7 @@ using HeaderNames = Microsoft.Net.Http.Headers.HeaderNames;
 
 namespace Masuit.MyBlogs.Core.Extensions.Firewall;
 
-public class FirewallAttribute : IAsyncActionFilter
+public sealed class FirewallAttribute : IAsyncActionFilter
 {
     public ICacheManager<int> CacheManager { get; set; }
 

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

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

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

@@ -2,7 +2,7 @@
 
 namespace Masuit.MyBlogs.Core.Extensions.Hangfire;
 
-public class HangfireActivator : JobActivator
+public sealed class HangfireActivator : JobActivator
 {
     private readonly IServiceProvider _serviceProvider;
 

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

@@ -18,7 +18,7 @@ namespace Masuit.MyBlogs.Core.Extensions.Hangfire
 	/// <summary>
 	/// hangfire后台任务
 	/// </summary>
-	public class HangfireBackJob : Disposable, IHangfireBackJob
+	public sealed class HangfireBackJob : Disposable, IHangfireBackJob
 	{
 		private readonly IHttpClientFactory _httpClientFactory;
 		private readonly IWebHostEnvironment _hostEnvironment;

+ 63 - 64
src/Masuit.MyBlogs.Core/Extensions/MyAuthorizeAttribute.cs

@@ -9,73 +9,72 @@ using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.Filters;
 using System.Web;
 
-namespace Masuit.MyBlogs.Core.Extensions
+namespace Masuit.MyBlogs.Core.Extensions;
+
+/// <summary>
+/// 授权验证过滤器
+/// </summary>
+public sealed class MyAuthorizeAttribute : ActionFilterAttribute
 {
-    /// <summary>
-    /// 授权验证过滤器
-    /// </summary>
-    public class MyAuthorizeAttribute : ActionFilterAttribute
-    {
-        /// <summary>在执行操作方法之前由 ASP.NET MVC 框架调用。</summary>
-        /// <param name="filterContext">筛选器上下文。</param>
-        public override void OnActionExecuting(ActionExecutingContext filterContext)
-        {
-            if (filterContext.ActionDescriptor.EndpointMetadata.OfType<AllowAnonymousAttribute>().Any())
-            {
-                return;
-            }
+	/// <summary>在执行操作方法之前由 ASP.NET MVC 框架调用。</summary>
+	/// <param name="filterContext">筛选器上下文。</param>
+	public override void OnActionExecuting(ActionExecutingContext filterContext)
+	{
+		if (filterContext.ActionDescriptor.EndpointMetadata.OfType<AllowAnonymousAttribute>().Any())
+		{
+			return;
+		}
 #if !DEBUG
-            var user = filterContext.HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
-            if (user?.IsAdmin == true)
-            {
-                return;
-            }
+		var user = filterContext.HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo);
+		if (user?.IsAdmin == true)
+		{
+			return;
+		}
 
-            //先尝试自动登录
-            if (filterContext.HttpContext.Request.Cookies.Any(x => x.Key == "username" || x.Key == "password"))
-            {
-                string name = filterContext.HttpContext.Request.Cookies["username"] ?? "";
-                string pwd = filterContext.HttpContext.Request.Cookies["password"]?.DesDecrypt(AppConfig.BaiduAK) ?? "";
+		//先尝试自动登录
+		if (filterContext.HttpContext.Request.Cookies.Any(x => x.Key == "username" || x.Key == "password"))
+		{
+			string name = filterContext.HttpContext.Request.Cookies["username"] ?? "";
+			string pwd = filterContext.HttpContext.Request.Cookies["password"]?.DesDecrypt(AppConfig.BaiduAK) ?? "";
 
-                var userInfo = (Startup.ServiceProvider.GetRequiredService<IUserInfoService>()).Login(name, pwd);
-                if (userInfo != null)
-                {
-                    filterContext.HttpContext.Response.Cookies.Append("username", name, new CookieOptions()
-                    {
-                        Expires = DateTime.Now.AddYears(1),
-                        SameSite = SameSiteMode.Lax
-                    });
-                    filterContext.HttpContext.Response.Cookies.Append("password", filterContext.HttpContext.Request.Cookies["password"], new CookieOptions()
-                    {
-                        Expires = DateTime.Now.AddYears(1),
-                        SameSite = SameSiteMode.Lax
-                    });
-                    filterContext.HttpContext.Session.Set(SessionKey.UserInfo, userInfo);
-                }
-                else
-                {
-                    if (filterContext.HttpContext.Request.Method.Equals(HttpMethods.Get))
-                    {
-                        filterContext.Result = new RedirectResult("/passport/login?from=" + HttpUtility.UrlEncode(filterContext.HttpContext.Request.Path.ToString())?.Replace("#", "%23"));
-                    }
-                    else
-                    {
-                        filterContext.Result = new UnauthorizedObjectResult(new { StatusCode = 401, Success = false, IsLogin = false, Message = "未登录系统,请先登录!" });
-                    }
-                }
-            }
-            else
-            {
-                if (filterContext.HttpContext.Request.Method.Equals(HttpMethods.Get))
-                {
-                    filterContext.Result = new RedirectResult("/passport/login?from=" + HttpUtility.UrlEncode(filterContext.HttpContext.Request.Path.ToString()));
-                }
-                else
-                {
-                    filterContext.Result = new UnauthorizedObjectResult(new { StatusCode = 401, Success = false, IsLogin = false, Message = "未登录系统,请先登录!" });
-                }
-            }
+			var userInfo = (Startup.ServiceProvider.GetRequiredService<IUserInfoService>()).Login(name, pwd);
+			if (userInfo != null)
+			{
+				filterContext.HttpContext.Response.Cookies.Append("username", name, new CookieOptions()
+				{
+					Expires = DateTime.Now.AddYears(1),
+					SameSite = SameSiteMode.Lax
+				});
+				filterContext.HttpContext.Response.Cookies.Append("password", filterContext.HttpContext.Request.Cookies["password"], new CookieOptions()
+				{
+					Expires = DateTime.Now.AddYears(1),
+					SameSite = SameSiteMode.Lax
+				});
+				filterContext.HttpContext.Session.Set(SessionKey.UserInfo, userInfo);
+			}
+			else
+			{
+				if (filterContext.HttpContext.Request.Method.Equals(HttpMethods.Get))
+				{
+					filterContext.Result = new RedirectResult("/passport/login?from=" + HttpUtility.UrlEncode(filterContext.HttpContext.Request.Path.ToString())?.Replace("#", "%23"));
+				}
+				else
+				{
+					filterContext.Result = new UnauthorizedObjectResult(new { StatusCode = 401, Success = false, IsLogin = false, Message = "未登录系统,请先登录!" });
+				}
+			}
+		}
+		else
+		{
+			if (filterContext.HttpContext.Request.Method.Equals(HttpMethods.Get))
+			{
+				filterContext.Result = new RedirectResult("/passport/login?from=" + HttpUtility.UrlEncode(filterContext.HttpContext.Request.Path.ToString()));
+			}
+			else
+			{
+				filterContext.Result = new UnauthorizedObjectResult(new { StatusCode = 401, Success = false, IsLogin = false, Message = "未登录系统,请先登录!" });
+			}
+		}
 #endif
-        }
-    }
+	}
 }

+ 65 - 66
src/Masuit.MyBlogs.Core/Extensions/TranslateMiddleware.cs

@@ -4,78 +4,77 @@ using Masuit.Tools.AspNetCore.Mime;
 using Microsoft.International.Converters.TraditionalChineseToSimplifiedConverter;
 using System.Text;
 
-namespace Masuit.MyBlogs.Core.Extensions
+namespace Masuit.MyBlogs.Core.Extensions;
+
+/// <summary>
+/// 简繁转换拦截器
+/// </summary>
+public sealed class TranslateMiddleware
 {
-    /// <summary>
-    /// 简繁转换拦截器
-    /// </summary>
-    public class TranslateMiddleware
-    {
-        private readonly RequestDelegate _next;
+	private readonly RequestDelegate _next;
 
-        /// <summary>
-        /// 构造函数
-        /// </summary>
-        /// <param name="next"></param>
-        public TranslateMiddleware(RequestDelegate next)
-        {
-            _next = next;
-        }
+	/// <summary>
+	/// 构造函数
+	/// </summary>
+	/// <param name="next"></param>
+	public TranslateMiddleware(RequestDelegate next)
+	{
+		_next = next;
+	}
 
-        public Task Invoke(HttpContext context)
-        {
-            var path = context.Request.Path.Value ?? "";
-            if (path.StartsWith("/_blazor") || path.StartsWith("/api") || path.StartsWith("/file") || path.StartsWith("/download") || context.Request.IsRobot())
-            {
-                return _next(context);
-            }
+	public Task Invoke(HttpContext context)
+	{
+		var path = context.Request.Path.Value ?? "";
+		if (path.StartsWith("/_blazor") || path.StartsWith("/api") || path.StartsWith("/file") || path.StartsWith("/download") || context.Request.IsRobot())
+		{
+			return _next(context);
+		}
 
-            string lang = context.Request.Query["lang"];
-            lang ??= context.Request.Cookies["lang"];
-            if (string.IsNullOrEmpty(lang))
-            {
-                if (context.Request.Location().Address.Contains(new[] { "台湾", "香港", "澳门", "Taiwan", "TW", "HongKong", "HK" }))
-                {
-                    return Traditional(context);
-                }
+		string lang = context.Request.Query["lang"];
+		lang ??= context.Request.Cookies["lang"];
+		if (string.IsNullOrEmpty(lang))
+		{
+			if (context.Request.Location().Address.Contains(new[] { "台湾", "香港", "澳门", "Taiwan", "TW", "HongKong", "HK" }))
+			{
+				return Traditional(context);
+			}
 
-                return _next(context);
-            }
-            if (lang == "zh-cn")
-            {
-                return _next(context);
-            }
+			return _next(context);
+		}
+		if (lang == "zh-cn")
+		{
+			return _next(context);
+		}
 
-            return Traditional(context);
-        }
+		return Traditional(context);
+	}
 
-        private async Task Traditional(HttpContext context)
-        {
-            var accept = context.Request.Headers["Accept"].ToString();
-            if (accept.StartsWith("text") || accept.Contains(ContentType.Json))
-            {
-                //设置stream存放ResponseBody
-                var responseOriginalBody = context.Response.Body;
-                using var memStream = new MemoryStream();
-                context.Response.Body = memStream;
+	private async Task Traditional(HttpContext context)
+	{
+		var accept = context.Request.Headers["Accept"].ToString();
+		if (accept.StartsWith("text") || accept.Contains(ContentType.Json))
+		{
+			//设置stream存放ResponseBody
+			var responseOriginalBody = context.Response.Body;
+			using var memStream = new MemoryStream();
+			context.Response.Body = memStream;
 
-                // 执行其他中间件
-                await _next(context);
+			// 执行其他中间件
+			await _next(context);
 
-                //处理执行其他中间件后的ResponseBody
-                memStream.Seek(0, SeekOrigin.Begin);
-                using var responseReader = new StreamReader(memStream, Encoding.UTF8);
-                var responseBody = await responseReader.ReadToEndAsync();
-                memStream.Seek(0, SeekOrigin.Begin);
-                await memStream.WriteAsync(Encoding.UTF8.GetBytes(ChineseConverter.Convert(responseBody, ChineseConversionDirection.SimplifiedToTraditional)).AsMemory());
-                memStream.Seek(0, SeekOrigin.Begin);
-                await memStream.CopyToAsync(responseOriginalBody);
-                context.Response.Body = responseOriginalBody;
-            }
-            else
-            {
-                await _next(context);
-            }
-        }
-    }
-}
+			//处理执行其他中间件后的ResponseBody
+			memStream.Seek(0, SeekOrigin.Begin);
+			using var responseReader = new StreamReader(memStream, Encoding.UTF8);
+			var responseBody = await responseReader.ReadToEndAsync();
+			memStream.Seek(0, SeekOrigin.Begin);
+			await memStream.WriteAsync(Encoding.UTF8.GetBytes(ChineseConverter.Convert(responseBody, ChineseConversionDirection.SimplifiedToTraditional)).AsMemory());
+			memStream.Seek(0, SeekOrigin.Begin);
+			await memStream.CopyToAsync(responseOriginalBody);
+			context.Response.Body = responseOriginalBody;
+		}
+		else
+		{
+			await _next(context);
+		}
+	}
+}

+ 126 - 127
src/Masuit.MyBlogs.Core/Infrastructure/DataContext.cs

@@ -2,166 +2,165 @@
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Diagnostics;
 
-namespace Masuit.MyBlogs.Core.Infrastructure
+namespace Masuit.MyBlogs.Core.Infrastructure;
+
+public class DataContext : DbContext
 {
-	public class DataContext : DbContext
+	public DataContext(DbContextOptions<DataContext> options) : base(options)
 	{
-		public DataContext(DbContextOptions<DataContext> options) : base(options)
-		{
-		}
+	}
 
-		protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
-		{
-			optionsBuilder.EnableDetailedErrors().UseLazyLoadingProxies().UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll).ConfigureWarnings(builder => builder.Ignore(CoreEventId.DetachedLazyLoadingWarning));
-		}
+	protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+	{
+		optionsBuilder.EnableDetailedErrors().UseLazyLoadingProxies().UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll).ConfigureWarnings(builder => builder.Ignore(CoreEventId.DetachedLazyLoadingWarning));
+	}
 
-		protected override void OnModelCreating(ModelBuilder modelBuilder)
-		{
-			base.OnModelCreating(modelBuilder);
-			modelBuilder.Entity<Category>().HasMany(e => e.Post).WithOne(e => e.Category).OnDelete(DeleteBehavior.Cascade);
-			modelBuilder.Entity<Category>().HasMany(e => e.PostHistoryVersion).WithOne(e => e.Category).HasForeignKey(r => r.CategoryId).OnDelete(DeleteBehavior.Cascade);
-			modelBuilder.Entity<Category>().HasMany(e => e.Children).WithOne(c => c.Parent).IsRequired(false).HasForeignKey(c => c.ParentId).OnDelete(DeleteBehavior.Cascade);
-			modelBuilder.Entity<Category>().Property(c => c.Path).IsRequired();
-
-			modelBuilder.Entity<Post>().HasMany(e => e.Comment).WithOne(e => e.Post).HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
-			modelBuilder.Entity<Post>().HasMany(e => e.PostHistoryVersion).WithOne(e => e.Post).HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
-			modelBuilder.Entity<Post>().HasMany(e => e.Seminar).WithMany(s => s.Post).UsingEntity(builder => builder.ToTable("SeminarPost"));
-			modelBuilder.Entity<Post>().HasMany(e => e.PostMergeRequests).WithOne(s => s.Post).HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
-			modelBuilder.Entity<Post>().HasMany(e => e.PostVisitRecords).WithOne().HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
-			modelBuilder.Entity<Post>().HasMany(e => e.PostVisitRecordStats).WithOne().HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
-			modelBuilder.Entity<PostHistoryVersion>().HasMany(e => e.Seminar).WithMany(s => s.PostHistoryVersion).UsingEntity(builder => builder.ToTable("SeminarPostHistoryVersion"));
-
-			modelBuilder.Entity<UserInfo>().HasMany(e => e.LoginRecord).WithOne(e => e.UserInfo).OnDelete(DeleteBehavior.Cascade);
-
-			modelBuilder.Entity<Menu>().HasMany(e => e.Children).WithOne(m => m.Parent).IsRequired(false).OnDelete(DeleteBehavior.Cascade);
-			modelBuilder.Entity<Menu>().Property(c => c.Path).IsRequired();
-
-			modelBuilder.Entity<Comment>().HasMany(e => e.Children).WithOne(c => c.Parent).HasForeignKey(c => c.ParentId).IsRequired(false).OnDelete(DeleteBehavior.Cascade);
-			modelBuilder.Entity<Comment>().Property(c => c.Path).IsRequired();
-			modelBuilder.Entity<Comment>().Property(c => c.GroupTag).IsRequired();
-
-			modelBuilder.Entity<LeaveMessage>().HasMany(e => e.Children).WithOne(c => c.Parent).HasForeignKey(c => c.ParentId).IsRequired(false).OnDelete(DeleteBehavior.Cascade);
-			modelBuilder.Entity<LeaveMessage>().Property(c => c.Path).IsRequired();
-			modelBuilder.Entity<LeaveMessage>().Property(c => c.GroupTag).IsRequired();
-
-			modelBuilder.Entity<Links>().HasMany(e => e.Loopbacks).WithOne(l => l.Links).IsRequired().HasForeignKey(e => e.LinkId).OnDelete(DeleteBehavior.Cascade);
-
-			modelBuilder.Entity<Advertisement>().HasMany(e => e.ClickRecords).WithOne().HasForeignKey(e => e.AdvertisementId).IsRequired().OnDelete(DeleteBehavior.Cascade);
-
-			modelBuilder.Entity<Advertisement>().HasIndex(a => a.Price).IsDescending();
-			modelBuilder.Entity<AdvertisementClickRecord>().HasIndex(a => a.Time);
-			modelBuilder.Entity<AdvertisementClickRecord>().HasIndex(a => a.AdvertisementId);
-			modelBuilder.Entity<Category>().HasIndex(a => a.ParentId);
-			modelBuilder.Entity<Comment>().HasIndex(a => a.PostId);
-			modelBuilder.Entity<LeaveMessage>().HasIndex(a => a.PostDate).IsDescending();
-			modelBuilder.Entity<LinkLoopback>().HasIndex(a => a.LinkId);
-			modelBuilder.Entity<Links>().HasIndex(a => a.Recommend);
-			modelBuilder.Entity<LoginRecord>().HasIndex(a => a.UserInfoId);
-			modelBuilder.Entity<Menu>().HasIndex(a => a.Sort);
-			modelBuilder.Entity<Menu>().HasIndex(a => a.ParentId);
-			modelBuilder.Entity<Notice>().HasIndex(a => a.ModifyDate).IsDescending();
-			modelBuilder.Entity<Post>().HasIndex(a => a.CategoryId);
-			modelBuilder.Entity<Post>().HasIndex(a => a.ModifyDate).IsDescending();
-			modelBuilder.Entity<Post>().HasIndex(a => a.AverageViewCount).IsDescending();
-			modelBuilder.Entity<Post>().HasIndex(a => a.TotalViewCount).IsDescending();
-			modelBuilder.Entity<PostHistoryVersion>().HasIndex(a => a.CategoryId);
-			modelBuilder.Entity<PostHistoryVersion>().HasIndex(a => a.PostId);
-			modelBuilder.Entity<PostMergeRequest>().HasIndex(a => a.PostId);
-			modelBuilder.Entity<PostVisitRecord>().HasIndex(a => a.PostId);
-			modelBuilder.Entity<PostVisitRecord>().HasIndex(a => a.Time).IsDescending();
-			modelBuilder.Entity<PostVisitRecordStats>().HasIndex(a => a.Date).IsDescending();
-			modelBuilder.Entity<PostVisitRecordStats>().HasIndex(a => a.PostId);
-			modelBuilder.Entity<SearchDetails>().HasIndex(a => a.SearchTime).IsDescending();
-		}
+	protected override void OnModelCreating(ModelBuilder modelBuilder)
+	{
+		base.OnModelCreating(modelBuilder);
+		modelBuilder.Entity<Category>().HasMany(e => e.Post).WithOne(e => e.Category).OnDelete(DeleteBehavior.Cascade);
+		modelBuilder.Entity<Category>().HasMany(e => e.PostHistoryVersion).WithOne(e => e.Category).HasForeignKey(r => r.CategoryId).OnDelete(DeleteBehavior.Cascade);
+		modelBuilder.Entity<Category>().HasMany(e => e.Children).WithOne(c => c.Parent).IsRequired(false).HasForeignKey(c => c.ParentId).OnDelete(DeleteBehavior.Cascade);
+		modelBuilder.Entity<Category>().Property(c => c.Path).IsRequired();
+
+		modelBuilder.Entity<Post>().HasMany(e => e.Comment).WithOne(e => e.Post).HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
+		modelBuilder.Entity<Post>().HasMany(e => e.PostHistoryVersion).WithOne(e => e.Post).HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
+		modelBuilder.Entity<Post>().HasMany(e => e.Seminar).WithMany(s => s.Post).UsingEntity(builder => builder.ToTable("SeminarPost"));
+		modelBuilder.Entity<Post>().HasMany(e => e.PostMergeRequests).WithOne(s => s.Post).HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
+		modelBuilder.Entity<Post>().HasMany(e => e.PostVisitRecords).WithOne().HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
+		modelBuilder.Entity<Post>().HasMany(e => e.PostVisitRecordStats).WithOne().HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
+		modelBuilder.Entity<PostHistoryVersion>().HasMany(e => e.Seminar).WithMany(s => s.PostHistoryVersion).UsingEntity(builder => builder.ToTable("SeminarPostHistoryVersion"));
+
+		modelBuilder.Entity<UserInfo>().HasMany(e => e.LoginRecord).WithOne(e => e.UserInfo).OnDelete(DeleteBehavior.Cascade);
+
+		modelBuilder.Entity<Menu>().HasMany(e => e.Children).WithOne(m => m.Parent).IsRequired(false).OnDelete(DeleteBehavior.Cascade);
+		modelBuilder.Entity<Menu>().Property(c => c.Path).IsRequired();
+
+		modelBuilder.Entity<Comment>().HasMany(e => e.Children).WithOne(c => c.Parent).HasForeignKey(c => c.ParentId).IsRequired(false).OnDelete(DeleteBehavior.Cascade);
+		modelBuilder.Entity<Comment>().Property(c => c.Path).IsRequired();
+		modelBuilder.Entity<Comment>().Property(c => c.GroupTag).IsRequired();
+
+		modelBuilder.Entity<LeaveMessage>().HasMany(e => e.Children).WithOne(c => c.Parent).HasForeignKey(c => c.ParentId).IsRequired(false).OnDelete(DeleteBehavior.Cascade);
+		modelBuilder.Entity<LeaveMessage>().Property(c => c.Path).IsRequired();
+		modelBuilder.Entity<LeaveMessage>().Property(c => c.GroupTag).IsRequired();
+
+		modelBuilder.Entity<Links>().HasMany(e => e.Loopbacks).WithOne(l => l.Links).IsRequired().HasForeignKey(e => e.LinkId).OnDelete(DeleteBehavior.Cascade);
+
+		modelBuilder.Entity<Advertisement>().HasMany(e => e.ClickRecords).WithOne().HasForeignKey(e => e.AdvertisementId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+
+		modelBuilder.Entity<Advertisement>().HasIndex(a => a.Price).IsDescending();
+		modelBuilder.Entity<AdvertisementClickRecord>().HasIndex(a => a.Time);
+		modelBuilder.Entity<AdvertisementClickRecord>().HasIndex(a => a.AdvertisementId);
+		modelBuilder.Entity<Category>().HasIndex(a => a.ParentId);
+		modelBuilder.Entity<Comment>().HasIndex(a => a.PostId);
+		modelBuilder.Entity<LeaveMessage>().HasIndex(a => a.PostDate).IsDescending();
+		modelBuilder.Entity<LinkLoopback>().HasIndex(a => a.LinkId);
+		modelBuilder.Entity<Links>().HasIndex(a => a.Recommend);
+		modelBuilder.Entity<LoginRecord>().HasIndex(a => a.UserInfoId);
+		modelBuilder.Entity<Menu>().HasIndex(a => a.Sort);
+		modelBuilder.Entity<Menu>().HasIndex(a => a.ParentId);
+		modelBuilder.Entity<Notice>().HasIndex(a => a.ModifyDate).IsDescending();
+		modelBuilder.Entity<Post>().HasIndex(a => a.CategoryId);
+		modelBuilder.Entity<Post>().HasIndex(a => a.ModifyDate).IsDescending();
+		modelBuilder.Entity<Post>().HasIndex(a => a.AverageViewCount).IsDescending();
+		modelBuilder.Entity<Post>().HasIndex(a => a.TotalViewCount).IsDescending();
+		modelBuilder.Entity<PostHistoryVersion>().HasIndex(a => a.CategoryId);
+		modelBuilder.Entity<PostHistoryVersion>().HasIndex(a => a.PostId);
+		modelBuilder.Entity<PostMergeRequest>().HasIndex(a => a.PostId);
+		modelBuilder.Entity<PostVisitRecord>().HasIndex(a => a.PostId);
+		modelBuilder.Entity<PostVisitRecord>().HasIndex(a => a.Time).IsDescending();
+		modelBuilder.Entity<PostVisitRecordStats>().HasIndex(a => a.Date).IsDescending();
+		modelBuilder.Entity<PostVisitRecordStats>().HasIndex(a => a.PostId);
+		modelBuilder.Entity<SearchDetails>().HasIndex(a => a.SearchTime).IsDescending();
+	}
 
-		public override int SaveChanges()
+	public override int SaveChanges()
+	{
+		DbUpdateConcurrencyException ex = null;
+		for (int i = 0; i < 5; i++)
 		{
-			DbUpdateConcurrencyException ex = null;
-			for (int i = 0; i < 5; i++)
+			try
 			{
-				try
-				{
-					return base.SaveChanges();
-				}
-				catch (DbUpdateConcurrencyException e)
-				{
-					ex = e;
-					var entry = e.Entries.Single();
-					var databaseValues = entry.GetDatabaseValues();
-					var resolvedValues = databaseValues.Clone();
-					entry.OriginalValues.SetValues(databaseValues);
-					entry.CurrentValues.SetValues(resolvedValues);
-				}
+				return base.SaveChanges();
+			}
+			catch (DbUpdateConcurrencyException e)
+			{
+				ex = e;
+				var entry = e.Entries.Single();
+				var databaseValues = entry.GetDatabaseValues();
+				var resolvedValues = databaseValues.Clone();
+				entry.OriginalValues.SetValues(databaseValues);
+				entry.CurrentValues.SetValues(resolvedValues);
 			}
-
-			throw ex;
 		}
 
-		public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new())
+		throw ex;
+	}
+
+	public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new())
+	{
+		DbUpdateConcurrencyException ex = null;
+		for (int i = 0; i < 5; i++)
 		{
-			DbUpdateConcurrencyException ex = null;
-			for (int i = 0; i < 5; i++)
+			try
 			{
-				try
-				{
-					return await base.SaveChangesAsync(cancellationToken);
-				}
-				catch (DbUpdateConcurrencyException e)
-				{
-					ex = e;
-					var entry = e.Entries.Single();
-					var databaseValues = await entry.GetDatabaseValuesAsync(cancellationToken);
-					var resolvedValues = databaseValues.Clone();
-					entry.OriginalValues.SetValues(databaseValues);
-					entry.CurrentValues.SetValues(resolvedValues);
-				}
+				return await base.SaveChangesAsync(cancellationToken);
+			}
+			catch (DbUpdateConcurrencyException e)
+			{
+				ex = e;
+				var entry = e.Entries.Single();
+				var databaseValues = await entry.GetDatabaseValuesAsync(cancellationToken);
+				var resolvedValues = databaseValues.Clone();
+				entry.OriginalValues.SetValues(databaseValues);
+				entry.CurrentValues.SetValues(resolvedValues);
 			}
-
-			throw ex;
 		}
 
-		public virtual DbSet<Category> Category { get; set; }
+		throw ex;
+	}
 
-		public virtual DbSet<Comment> Comment { get; set; }
+	public virtual DbSet<Category> Category { get; set; }
 
-		public virtual DbSet<LeaveMessage> LeaveMessage { get; set; }
+	public virtual DbSet<Comment> Comment { get; set; }
 
-		public virtual DbSet<Links> Links { get; set; }
+	public virtual DbSet<LeaveMessage> LeaveMessage { get; set; }
 
-		public virtual DbSet<Menu> Menu { get; set; }
+	public virtual DbSet<Links> Links { get; set; }
 
-		public virtual DbSet<Misc> Misc { get; set; }
+	public virtual DbSet<Menu> Menu { get; set; }
 
-		public virtual DbSet<Notice> Notice { get; set; }
+	public virtual DbSet<Misc> Misc { get; set; }
 
-		public virtual DbSet<Post> Post { get; set; }
+	public virtual DbSet<Notice> Notice { get; set; }
 
-		public virtual DbSet<PostHistoryVersion> PostHistoryVersion { get; set; }
+	public virtual DbSet<Post> Post { get; set; }
 
-		public virtual DbSet<SearchDetails> SearchDetails { get; set; }
+	public virtual DbSet<PostHistoryVersion> PostHistoryVersion { get; set; }
 
-		public virtual DbSet<SystemSetting> SystemSetting { get; set; }
+	public virtual DbSet<SearchDetails> SearchDetails { get; set; }
 
-		public virtual DbSet<UserInfo> UserInfo { get; set; }
+	public virtual DbSet<SystemSetting> SystemSetting { get; set; }
 
-		public virtual DbSet<LoginRecord> LoginRecord { get; set; }
+	public virtual DbSet<UserInfo> UserInfo { get; set; }
 
-		public virtual DbSet<Donate> Donate { get; set; }
+	public virtual DbSet<LoginRecord> LoginRecord { get; set; }
 
-		public virtual DbSet<Seminar> Seminar { get; set; }
+	public virtual DbSet<Donate> Donate { get; set; }
 
-		public virtual DbSet<InternalMessage> InternalMessage { get; set; }
+	public virtual DbSet<Seminar> Seminar { get; set; }
 
-		public virtual DbSet<FastShare> FastShare { get; set; }
+	public virtual DbSet<InternalMessage> InternalMessage { get; set; }
 
-		public virtual DbSet<PostMergeRequest> PostMergeRequests { get; set; }
+	public virtual DbSet<FastShare> FastShare { get; set; }
 
-		public virtual DbSet<Advertisement> Advertisements { get; set; }
+	public virtual DbSet<PostMergeRequest> PostMergeRequests { get; set; }
 
-		public virtual DbSet<Variables> Variables { get; set; }
+	public virtual DbSet<Advertisement> Advertisements { get; set; }
 
-		public virtual DbSet<LinkLoopback> LinkLoopbacks { get; set; }
+	public virtual DbSet<Variables> Variables { get; set; }
 
-		public virtual DbSet<PostTag> PostTags { get; set; }
-	}
-}
+	public virtual DbSet<LinkLoopback> LinkLoopbacks { get; set; }
+
+	public virtual DbSet<PostTag> PostTags { get; set; }
+}

+ 141 - 142
src/Masuit.MyBlogs.Core/Infrastructure/Drive/DriveAccountService.cs

@@ -2,146 +2,145 @@ using Masuit.MyBlogs.Core.Extensions.DriveHelpers;
 using Masuit.MyBlogs.Core.Models.Drive;
 using Microsoft.Identity.Client;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Drive
+namespace Masuit.MyBlogs.Core.Infrastructure.Drive;
+
+public sealed class DriveAccountService : IDriveAccountService
 {
-    public class DriveAccountService : IDriveAccountService
-    {
-        private readonly IConfidentialClientApplication _app;
-
-        public DriveContext SiteContext { get; set; }
-
-        /// <summary>
-        /// Graph实例
-        /// </summary>
-        /// <value></value>
-        public Microsoft.Graph.GraphServiceClient Graph { get; set; }
-
-        public DriveAccountService(DriveContext siteContext, TokenService tokenService)
-        {
-            SiteContext = siteContext;
-            _app = tokenService.app;
-            Graph = tokenService.Graph;
-        }
-
-        /// <summary>
-        /// 返回 Oauth 验证url
-        /// </summary>
-        /// <returns></returns>
-        public async Task<string> GetAuthorizationRequestUrl()
-        {
-            var redirectUrl = await _app.GetAuthorizationRequestUrl(OneDriveConfiguration.Scopes).ExecuteAsync();
-            return redirectUrl.AbsoluteUri;
-        }
-
-        /// <summary>
-        /// 添加 SharePoint Site-ID 到数据库
-        /// </summary>
-        /// <param name="siteName"></param>
-        /// <param name="nickName"></param>
-        /// <returns></returns>
-        public async Task AddSiteId(string siteName, string nickName)
-        {
-            Site site = new();
-
-            //使用 Onedrive
-            if (siteName == "onedrive")
-            {
-                site.Name = siteName;
-                site.NickName = nickName;
-            }
-            else
-            {
-                using var httpClient = new HttpClient
-                {
-                    Timeout = TimeSpan.FromSeconds(20)
-                };
-                var apiCaller = new ProtectedApiCallHelper(httpClient);
-                await apiCaller.CallWebApiAndProcessResultASync($"{OneDriveConfiguration.GraphApi}/v1.0/sites/{OneDriveConfiguration.DominName}:/sites/{siteName}", GetToken(), result =>
-                {
-                    site.SiteId = result.Properties().Single((prop) => prop.Name == "id").Value.ToString();
-                    site.Name = result.Properties().Single((prop) => prop.Name == "name").Value.ToString();
-                    site.NickName = nickName;
-                });
-            }
-            if (!SiteContext.Sites.Any(s => s.SiteId == site.SiteId))
-            {
-                //若是首次添加则设置为默认的驱动器
-                using (var setting = new SettingService(new DriveContext()))
-                {
-                    if (!SiteContext.Sites.Any())
-                    {
-                        await setting.Set("DefaultDrive", site.Name);
-                    }
-                }
-                await SiteContext.Sites.AddAsync(site);
-                await SiteContext.SaveChangesAsync();
-            }
-            else
-            {
-                throw new Exception("站点已被创建");
-            }
-        }
-
-        public List<Site> GetSites()
-        {
-            return SiteContext.Sites.ToList();
-        }
-
-        /// <summary>
-        /// 获取 Drive Info
-        /// </summary>
-        /// <returns></returns>
-        public async Task<List<DriveInfo>> GetDriveInfo()
-        {
-            var drivesInfo = new List<DriveInfo>();
-            foreach (var item in SiteContext.Sites.ToArray())
-            {
-                Microsoft.Graph.Drive drive;
-
-                //Onedrive
-                if (string.IsNullOrEmpty(item.SiteId))
-                {
-                    drive = await Graph.Me.Drive.Request().GetAsync();
-                }
-                else
-                {
-                    drive = await Graph.Sites[item.SiteId].Drive.Request().GetAsync();
-                }
-                drivesInfo.Add(new DriveInfo()
-                {
-                    Quota = drive.Quota,
-                    NickName = item.NickName,
-                    Name = item.Name,
-                    HiddenFolders = item.HiddenFolders
-                });
-            }
-            return drivesInfo;
-        }
-
-        public async Task Unbind(string nickName)
-        {
-            SiteContext.Sites.Remove(SiteContext.Sites.Single(site => site.NickName == nickName));
-            await SiteContext.SaveChangesAsync();
-        }
-
-        /// <summary>
-        /// 获取 Token
-        /// </summary>
-        /// <returns></returns>
-        public string GetToken()
-        {
-            return _app.AcquireTokenSilent(OneDriveConfiguration.Scopes, OneDriveConfiguration.AccountName).ExecuteAsync().Result.AccessToken;
-        }
-
-        public class DriveInfo
-        {
-            public Microsoft.Graph.Quota Quota { get; set; }
-
-            public string NickName { get; set; }
-
-            public string Name { get; set; }
-
-            public string[] HiddenFolders { get; set; }
-        }
-    }
-}
+	private readonly IConfidentialClientApplication _app;
+
+	public DriveContext SiteContext { get; set; }
+
+	/// <summary>
+	/// Graph实例
+	/// </summary>
+	/// <value></value>
+	public Microsoft.Graph.GraphServiceClient Graph { get; set; }
+
+	public DriveAccountService(DriveContext siteContext, TokenService tokenService)
+	{
+		SiteContext = siteContext;
+		_app = tokenService.app;
+		Graph = tokenService.Graph;
+	}
+
+	/// <summary>
+	/// 返回 Oauth 验证url
+	/// </summary>
+	/// <returns></returns>
+	public async Task<string> GetAuthorizationRequestUrl()
+	{
+		var redirectUrl = await _app.GetAuthorizationRequestUrl(OneDriveConfiguration.Scopes).ExecuteAsync();
+		return redirectUrl.AbsoluteUri;
+	}
+
+	/// <summary>
+	/// 添加 SharePoint Site-ID 到数据库
+	/// </summary>
+	/// <param name="siteName"></param>
+	/// <param name="nickName"></param>
+	/// <returns></returns>
+	public async Task AddSiteId(string siteName, string nickName)
+	{
+		Site site = new();
+
+		//使用 Onedrive
+		if (siteName == "onedrive")
+		{
+			site.Name = siteName;
+			site.NickName = nickName;
+		}
+		else
+		{
+			using var httpClient = new HttpClient
+			{
+				Timeout = TimeSpan.FromSeconds(20)
+			};
+			var apiCaller = new ProtectedApiCallHelper(httpClient);
+			await apiCaller.CallWebApiAndProcessResultASync($"{OneDriveConfiguration.GraphApi}/v1.0/sites/{OneDriveConfiguration.DominName}:/sites/{siteName}", GetToken(), result =>
+			{
+				site.SiteId = result.Properties().Single((prop) => prop.Name == "id").Value.ToString();
+				site.Name = result.Properties().Single((prop) => prop.Name == "name").Value.ToString();
+				site.NickName = nickName;
+			});
+		}
+		if (!SiteContext.Sites.Any(s => s.SiteId == site.SiteId))
+		{
+			//若是首次添加则设置为默认的驱动器
+			using (var setting = new SettingService(new DriveContext()))
+			{
+				if (!SiteContext.Sites.Any())
+				{
+					await setting.Set("DefaultDrive", site.Name);
+				}
+			}
+			await SiteContext.Sites.AddAsync(site);
+			await SiteContext.SaveChangesAsync();
+		}
+		else
+		{
+			throw new Exception("站点已被创建");
+		}
+	}
+
+	public List<Site> GetSites()
+	{
+		return SiteContext.Sites.ToList();
+	}
+
+	/// <summary>
+	/// 获取 Drive Info
+	/// </summary>
+	/// <returns></returns>
+	public async Task<List<DriveInfo>> GetDriveInfo()
+	{
+		var drivesInfo = new List<DriveInfo>();
+		foreach (var item in SiteContext.Sites.ToArray())
+		{
+			Microsoft.Graph.Drive drive;
+
+			//Onedrive
+			if (string.IsNullOrEmpty(item.SiteId))
+			{
+				drive = await Graph.Me.Drive.Request().GetAsync();
+			}
+			else
+			{
+				drive = await Graph.Sites[item.SiteId].Drive.Request().GetAsync();
+			}
+			drivesInfo.Add(new DriveInfo()
+			{
+				Quota = drive.Quota,
+				NickName = item.NickName,
+				Name = item.Name,
+				HiddenFolders = item.HiddenFolders
+			});
+		}
+		return drivesInfo;
+	}
+
+	public async Task Unbind(string nickName)
+	{
+		SiteContext.Sites.Remove(SiteContext.Sites.Single(site => site.NickName == nickName));
+		await SiteContext.SaveChangesAsync();
+	}
+
+	/// <summary>
+	/// 获取 Token
+	/// </summary>
+	/// <returns></returns>
+	public string GetToken()
+	{
+		return _app.AcquireTokenSilent(OneDriveConfiguration.Scopes, OneDriveConfiguration.AccountName).ExecuteAsync().Result.AccessToken;
+	}
+
+	public class DriveInfo
+	{
+		public Microsoft.Graph.Quota Quota { get; set; }
+
+		public string NickName { get; set; }
+
+		public string Name { get; set; }
+
+		public string[] HiddenFolders { get; set; }
+	}
+}

+ 171 - 172
src/Masuit.MyBlogs.Core/Infrastructure/Drive/DriveService.cs

@@ -2,176 +2,175 @@ using Masuit.MyBlogs.Core.Extensions.DriveHelpers;
 using Masuit.MyBlogs.Core.Models.Drive;
 using Microsoft.Graph;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Drive
+namespace Masuit.MyBlogs.Core.Infrastructure.Drive;
+
+public sealed class DriveService : IDriveService
 {
-    public class DriveService : IDriveService
-    {
-        private readonly IDriveAccountService _accountService;
-        private readonly GraphServiceClient _graph;
-        private readonly DriveContext _driveContext;
-
-        public DriveService(IDriveAccountService accountService, DriveContext driveContext)
-        {
-            _accountService = accountService;
-            _graph = accountService.Graph;
-            _driveContext = driveContext;
-        }
-
-        /// <summary>
-        /// 获取根目录的所有项目
-        /// </summary>
-        /// <returns></returns>
-        public async Task<List<DriveFile>> GetRootItems(string siteName, bool showHiddenFolders)
-        {
-            var drive = siteName != "onedrive" ? _graph.Sites[GetSiteId(siteName)].Drive : _graph.Me.Drive;
-            var request = drive.Root.Children.Request();
-            var result = await request.GetAsync();
-            var files = await GetItems(result, siteName, showHiddenFolders);
-            return files.OrderByDescending(f => f.CreatedTime).ToList();
-        }
-
-        /// <summary>
-        /// 根据路径获取文件夹下所有项目
-        /// </summary>
-        /// <param name="path"></param>
-        /// <param name="siteName"></param>
-        /// <param name="showHiddenFolders"></param>
-        /// <returns></returns>
-        public async Task<List<DriveFile>> GetDriveItemsByPath(string path, string siteName, bool showHiddenFolders)
-        {
-            var drive = siteName != "onedrive" ? _graph.Sites[GetSiteId(siteName)].Drive : _graph.Me.Drive;
-            var result = await drive.Root.ItemWithPath(path).Children.Request().GetAsync();
-            var files = await GetItems(result, siteName, showHiddenFolders);
-            files = files.OrderByDescending(f => f.CreatedTime).ToList();
-            return files;
-        }
-
-        /// <summary>
-        /// 根据路径获取项目
-        /// </summary>
-        /// <param name="path"></param>
-        /// <returns></returns>
-        public async Task<DriveFile> GetDriveItemByPath(string path, string siteName = "onedrive")
-        {
-            string[] imgArray = { ".png", ".jpg", ".jpeg", ".bmp", ".webp" };
-            var extension = Path.GetExtension(path);
-            var drive = siteName != "onedrive" ? _graph.Sites[GetSiteId(siteName)].Drive : _graph.Me.Drive;
-
-            //这么写是因为:分块上传图片后直接获取会报错。
-            if (imgArray.Contains(extension))
-            {
-                await drive.Root.ItemWithPath(path).Thumbnails.Request().GetAsync();
-            }
-            var result = await drive.Root.ItemWithPath(path).Request().GetAsync();
-            return GetItem(result);
-        }
-
-        /// <summary>
-        /// 获得上传url
-        /// </summary>
-        /// <param name="path"></param>
-        /// <param name="siteName"></param>
-        /// <returns></returns>
-        public async Task<string> GetUploadUrl(string path, string siteName = "onedrive")
-        {
-            var drive = siteName != "onedrive" ? _graph.Sites[GetSiteId(siteName)].Drive : _graph.Me.Drive;
-            var requestUrl = drive.Root.ItemWithPath(path).CreateUploadSession().Request().RequestUrl;
-            var apiCallHelper = new ProtectedApiCallHelper(new HttpClient());
-            var uploadUrl = "";
-            await apiCallHelper.CallWebApiAndProcessResultASync(requestUrl, _accountService.GetToken(), o =>
-            {
-                uploadUrl = o["uploadUrl"].ToString();
-            }, ProtectedApiCallHelper.Method.Post);
-            return uploadUrl;
-        }
-
-        #region PrivateMethod
-
-        private DriveFile GetItem(DriveItem result)
-        {
-            var file = new DriveFile()
-            {
-                CreatedTime = result.CreatedDateTime,
-                Name = result.Name,
-                Size = result.Size,
-                Id = result.Id
-            };
-            if (result.AdditionalData != null)
-            {
-                //可能是文件夹
-                if (result.AdditionalData.TryGetValue("@microsoft.graph.downloadUrl", out var downloadUrl))
-                {
-                    file.DownloadUrl = ReplaceCDNUrls(downloadUrl.ToString());
-                }
-            }
-
-            return file;
-        }
-
-        private async Task<List<DriveFile>> GetItems(IDriveItemChildrenCollectionPage result, string siteName = "onedrive", bool showHiddenFolders = false)
-        {
-            var files = new List<DriveFile>();
-            foreach (var item in result)
-            {
-                //要隐藏文件
-                if (!showHiddenFolders)
-                {
-                    //跳过隐藏的文件
-                    var hiddenFolders = _driveContext.Sites.Single(site => site.Name == siteName).HiddenFolders;
-                    if (hiddenFolders != null)
-                    {
-                        if (hiddenFolders.Any(str => str == item.Name))
-                        {
-                            continue;
-                        }
-                    }
-                }
-                var file = new DriveFile()
-                {
-                    CreatedTime = item.CreatedDateTime,
-                    Name = item.Name,
-                    Size = item.Size,
-                    Id = item.Id
-                };
-                if (item.AdditionalData != null)
-                {
-                    //可能是文件夹
-                    if (item.AdditionalData.TryGetValue("@microsoft.graph.downloadUrl", out var downloadUrl))
-                    {
-                        file.DownloadUrl = ReplaceCDNUrls(downloadUrl.ToString());
-                    }
-                }
-                files.Add(file);
-            }
-
-            if (result.Count == 200)
-            {
-                files.AddRange(await GetItems(await result.NextPageRequest.GetAsync(), siteName, showHiddenFolders));
-            }
-
-            return files;
-        }
-
-        /// <summary>
-        /// 根据名称返回siteid
-        /// </summary>
-        /// <returns></returns>
-        private string GetSiteId(string siteName)
-        {
-            var site = _driveContext.Sites.SingleOrDefault(s => s.Name == siteName);
-            return site?.SiteId;
-        }
-
-        private string ReplaceCDNUrls(string downloadUrl)
-        {
-            if (OneDriveConfiguration.CDNUrls.Length > 0)
-            {
-                return OneDriveConfiguration.CDNUrls.Select(item => item.Split(";", StringSplitOptions.RemoveEmptyEntries)).Where(strings => strings.Length > 1).Aggregate(downloadUrl, (current, strings) => current.Replace(strings[0], strings[1..][DateTime.Now.Second % (strings.Length - 1)]));
-            }
-
-            return downloadUrl;
-        }
-
-        #endregion PrivateMethod
-    }
-}
+	private readonly IDriveAccountService _accountService;
+	private readonly GraphServiceClient _graph;
+	private readonly DriveContext _driveContext;
+
+	public DriveService(IDriveAccountService accountService, DriveContext driveContext)
+	{
+		_accountService = accountService;
+		_graph = accountService.Graph;
+		_driveContext = driveContext;
+	}
+
+	/// <summary>
+	/// 获取根目录的所有项目
+	/// </summary>
+	/// <returns></returns>
+	public async Task<List<DriveFile>> GetRootItems(string siteName, bool showHiddenFolders)
+	{
+		var drive = siteName != "onedrive" ? _graph.Sites[GetSiteId(siteName)].Drive : _graph.Me.Drive;
+		var request = drive.Root.Children.Request();
+		var result = await request.GetAsync();
+		var files = await GetItems(result, siteName, showHiddenFolders);
+		return files.OrderByDescending(f => f.CreatedTime).ToList();
+	}
+
+	/// <summary>
+	/// 根据路径获取文件夹下所有项目
+	/// </summary>
+	/// <param name="path"></param>
+	/// <param name="siteName"></param>
+	/// <param name="showHiddenFolders"></param>
+	/// <returns></returns>
+	public async Task<List<DriveFile>> GetDriveItemsByPath(string path, string siteName, bool showHiddenFolders)
+	{
+		var drive = siteName != "onedrive" ? _graph.Sites[GetSiteId(siteName)].Drive : _graph.Me.Drive;
+		var result = await drive.Root.ItemWithPath(path).Children.Request().GetAsync();
+		var files = await GetItems(result, siteName, showHiddenFolders);
+		files = files.OrderByDescending(f => f.CreatedTime).ToList();
+		return files;
+	}
+
+	/// <summary>
+	/// 根据路径获取项目
+	/// </summary>
+	/// <param name="path"></param>
+	/// <returns></returns>
+	public async Task<DriveFile> GetDriveItemByPath(string path, string siteName = "onedrive")
+	{
+		string[] imgArray = { ".png", ".jpg", ".jpeg", ".bmp", ".webp" };
+		var extension = Path.GetExtension(path);
+		var drive = siteName != "onedrive" ? _graph.Sites[GetSiteId(siteName)].Drive : _graph.Me.Drive;
+
+		//这么写是因为:分块上传图片后直接获取会报错。
+		if (imgArray.Contains(extension))
+		{
+			await drive.Root.ItemWithPath(path).Thumbnails.Request().GetAsync();
+		}
+		var result = await drive.Root.ItemWithPath(path).Request().GetAsync();
+		return GetItem(result);
+	}
+
+	/// <summary>
+	/// 获得上传url
+	/// </summary>
+	/// <param name="path"></param>
+	/// <param name="siteName"></param>
+	/// <returns></returns>
+	public async Task<string> GetUploadUrl(string path, string siteName = "onedrive")
+	{
+		var drive = siteName != "onedrive" ? _graph.Sites[GetSiteId(siteName)].Drive : _graph.Me.Drive;
+		var requestUrl = drive.Root.ItemWithPath(path).CreateUploadSession().Request().RequestUrl;
+		var apiCallHelper = new ProtectedApiCallHelper(new HttpClient());
+		var uploadUrl = "";
+		await apiCallHelper.CallWebApiAndProcessResultASync(requestUrl, _accountService.GetToken(), o =>
+		{
+			uploadUrl = o["uploadUrl"].ToString();
+		}, ProtectedApiCallHelper.Method.Post);
+		return uploadUrl;
+	}
+
+	#region PrivateMethod
+
+	private DriveFile GetItem(DriveItem result)
+	{
+		var file = new DriveFile()
+		{
+			CreatedTime = result.CreatedDateTime,
+			Name = result.Name,
+			Size = result.Size,
+			Id = result.Id
+		};
+		if (result.AdditionalData != null)
+		{
+			//可能是文件夹
+			if (result.AdditionalData.TryGetValue("@microsoft.graph.downloadUrl", out var downloadUrl))
+			{
+				file.DownloadUrl = ReplaceCDNUrls(downloadUrl.ToString());
+			}
+		}
+
+		return file;
+	}
+
+	private async Task<List<DriveFile>> GetItems(IDriveItemChildrenCollectionPage result, string siteName = "onedrive", bool showHiddenFolders = false)
+	{
+		var files = new List<DriveFile>();
+		foreach (var item in result)
+		{
+			//要隐藏文件
+			if (!showHiddenFolders)
+			{
+				//跳过隐藏的文件
+				var hiddenFolders = _driveContext.Sites.Single(site => site.Name == siteName).HiddenFolders;
+				if (hiddenFolders != null)
+				{
+					if (hiddenFolders.Any(str => str == item.Name))
+					{
+						continue;
+					}
+				}
+			}
+			var file = new DriveFile()
+			{
+				CreatedTime = item.CreatedDateTime,
+				Name = item.Name,
+				Size = item.Size,
+				Id = item.Id
+			};
+			if (item.AdditionalData != null)
+			{
+				//可能是文件夹
+				if (item.AdditionalData.TryGetValue("@microsoft.graph.downloadUrl", out var downloadUrl))
+				{
+					file.DownloadUrl = ReplaceCDNUrls(downloadUrl.ToString());
+				}
+			}
+			files.Add(file);
+		}
+
+		if (result.Count == 200)
+		{
+			files.AddRange(await GetItems(await result.NextPageRequest.GetAsync(), siteName, showHiddenFolders));
+		}
+
+		return files;
+	}
+
+	/// <summary>
+	/// 根据名称返回siteid
+	/// </summary>
+	/// <returns></returns>
+	private string GetSiteId(string siteName)
+	{
+		var site = _driveContext.Sites.SingleOrDefault(s => s.Name == siteName);
+		return site?.SiteId;
+	}
+
+	private string ReplaceCDNUrls(string downloadUrl)
+	{
+		if (OneDriveConfiguration.CDNUrls.Length > 0)
+		{
+			return OneDriveConfiguration.CDNUrls.Select(item => item.Split(";", StringSplitOptions.RemoveEmptyEntries)).Where(strings => strings.Length > 1).Aggregate(downloadUrl, (current, strings) => current.Replace(strings[0], strings[1..][DateTime.Now.Second % (strings.Length - 1)]));
+		}
+
+		return downloadUrl;
+	}
+
+	#endregion PrivateMethod
+}

+ 59 - 60
src/Masuit.MyBlogs.Core/Infrastructure/Drive/SettingService.cs

@@ -1,67 +1,66 @@
 using Masuit.MyBlogs.Core.Models.Drive;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Drive
+namespace Masuit.MyBlogs.Core.Infrastructure.Drive;
+
+public sealed class SettingService : IDisposable
 {
-    public class SettingService : IDisposable
-    {
-        private readonly DriveContext _context;
-        public SettingService(DriveContext context)
-        {
-            _context = context;
-        }
+	private readonly DriveContext _context;
+	public SettingService(DriveContext context)
+	{
+		_context = context;
+	}
 
-        /// <summary>
-        /// 获取设置
-        /// </summary>
-        /// <param name="key"></param>
-        /// <returns></returns>
-        public string Get(string key)
-        {
-            //判断是否存在
-            if (!_context.Settings.Any(setting => setting.Key == key))
-            {
-                return null;
-            }
-            var result = _context.Settings.SingleOrDefault(setting => setting.Key == key).Value;
-            return result;
-        }
+	/// <summary>
+	/// 获取设置
+	/// </summary>
+	/// <param name="key"></param>
+	/// <returns></returns>
+	public string Get(string key)
+	{
+		//判断是否存在
+		if (!_context.Settings.Any(setting => setting.Key == key))
+		{
+			return null;
+		}
+		var result = _context.Settings.SingleOrDefault(setting => setting.Key == key).Value;
+		return result;
+	}
 
-        /// <summary>
-        /// 创建或更新设置
-        /// </summary>
-        /// <param name="key"></param>
-        /// <param name="value"></param>
-        /// <returns></returns>
-        public async Task Set(string key, string value)
-        {
-            if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(key))
-            {
-                throw new Exception("键值对为空");
-            }
-            if (string.IsNullOrEmpty(value))
-                value = "";
-            //已经存在
-            if (_context.Settings.Any(setting => setting.Key == key))
-            {
-                Setting setting = _context.Settings.Single(setting => setting.Key == key);
-                setting.Value = value;
-                _context.Settings.Update(setting);
-            }
-            else
-            {
-                Setting setting = new Setting()
-                {
-                    Key = key,
-                    Value = value
-                };
-                await _context.Settings.AddAsync(setting);
-            }
-            await _context.SaveChangesAsync();
-        }
+	/// <summary>
+	/// 创建或更新设置
+	/// </summary>
+	/// <param name="key"></param>
+	/// <param name="value"></param>
+	/// <returns></returns>
+	public async Task Set(string key, string value)
+	{
+		if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(key))
+		{
+			throw new Exception("键值对为空");
+		}
+		if (string.IsNullOrEmpty(value))
+			value = "";
+		//已经存在
+		if (_context.Settings.Any(setting => setting.Key == key))
+		{
+			Setting setting = _context.Settings.Single(setting => setting.Key == key);
+			setting.Value = value;
+			_context.Settings.Update(setting);
+		}
+		else
+		{
+			Setting setting = new Setting()
+			{
+				Key = key,
+				Value = value
+			};
+			await _context.Settings.AddAsync(setting);
+		}
+		await _context.SaveChangesAsync();
+	}
 
-        public void Dispose()
-        {
-            _context.Dispose();
-        }
-    }
+	public void Dispose()
+	{
+		_context.Dispose();
+	}
 }

+ 71 - 72
src/Masuit.MyBlogs.Core/Infrastructure/Drive/TokenService.cs

@@ -3,85 +3,84 @@ using Microsoft.Graph.Auth;
 using Microsoft.Identity.Client;
 using System.Net;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Drive
+namespace Masuit.MyBlogs.Core.Infrastructure.Drive;
+
+public sealed class TokenService
 {
-    public class TokenService
-    {
-        public AuthorizationCodeProvider authProvider;
+	public AuthorizationCodeProvider authProvider;
 
-        public AuthenticationResult authorizeResult;
+	public AuthenticationResult authorizeResult;
 
-        /// <summary>
-        /// Graph实例
-        /// </summary>
-        /// <value></value>
-        public Microsoft.Graph.GraphServiceClient Graph { get; set; }
+	/// <summary>
+	/// Graph实例
+	/// </summary>
+	/// <value></value>
+	public Microsoft.Graph.GraphServiceClient Graph { get; set; }
 
-        public IConfidentialClientApplication app;
+	public IConfidentialClientApplication app;
 
-        public TokenService()
-        {
-            if (OneDriveConfiguration.Type == OneDriveConfiguration.OfficeType.China)
-            {
-                app = ConfidentialClientApplicationBuilder.Create(OneDriveConfiguration.ClientId).WithClientSecret(OneDriveConfiguration.ClientSecret).WithRedirectUri(OneDriveConfiguration.BaseUri + "/api/admin/bind/new").WithAuthority(AzureCloudInstance.AzureChina, "common").Build();
-            }
-            else
-            {
-                app = ConfidentialClientApplicationBuilder.Create(OneDriveConfiguration.ClientId).WithClientSecret(OneDriveConfiguration.ClientSecret).WithRedirectUri(OneDriveConfiguration.BaseUri + "/api/admin/bind/new").WithAuthority(AzureCloudInstance.AzurePublic, "common").Build();
-            }
+	public TokenService()
+	{
+		if (OneDriveConfiguration.Type == OneDriveConfiguration.OfficeType.China)
+		{
+			app = ConfidentialClientApplicationBuilder.Create(OneDriveConfiguration.ClientId).WithClientSecret(OneDriveConfiguration.ClientSecret).WithRedirectUri(OneDriveConfiguration.BaseUri + "/api/admin/bind/new").WithAuthority(AzureCloudInstance.AzureChina, "common").Build();
+		}
+		else
+		{
+			app = ConfidentialClientApplicationBuilder.Create(OneDriveConfiguration.ClientId).WithClientSecret(OneDriveConfiguration.ClientSecret).WithRedirectUri(OneDriveConfiguration.BaseUri + "/api/admin/bind/new").WithAuthority(AzureCloudInstance.AzurePublic, "common").Build();
+		}
 
-            //缓存Token
-            TokenCacheHelper.EnableSerialization(app.UserTokenCache);
-            //这里要传入一个 Scope 否则默认使用 https://graph.microsoft.com/.default
-            //而导致无法使用世纪互联版本
-            authProvider = new AuthorizationCodeProvider(app, OneDriveConfiguration.Scopes);
-            //获取Token
-            if (File.Exists(TokenCacheHelper.CacheFilePath))
-            {
-                authorizeResult = authProvider.ClientApplication.AcquireTokenSilent(OneDriveConfiguration.Scopes, OneDriveConfiguration.AccountName).ExecuteAsync().Result;
-                //Debug.WriteLine(authorizeResult.AccessToken);
-            }
+		//缓存Token
+		TokenCacheHelper.EnableSerialization(app.UserTokenCache);
+		//这里要传入一个 Scope 否则默认使用 https://graph.microsoft.com/.default
+		//而导致无法使用世纪互联版本
+		authProvider = new AuthorizationCodeProvider(app, OneDriveConfiguration.Scopes);
+		//获取Token
+		if (File.Exists(TokenCacheHelper.CacheFilePath))
+		{
+			authorizeResult = authProvider.ClientApplication.AcquireTokenSilent(OneDriveConfiguration.Scopes, OneDriveConfiguration.AccountName).ExecuteAsync().Result;
+			//Debug.WriteLine(authorizeResult.AccessToken);
+		}
 
-            //启用代理
-            if (!string.IsNullOrEmpty(OneDriveConfiguration.Proxy))
-            {
-                // Configure your proxy
-                var httpClientHandler = new HttpClientHandler
-                {
-                    Proxy = new WebProxy(OneDriveConfiguration.Proxy),
-                    UseDefaultCredentials = true
-                };
-                var httpProvider = new Microsoft.Graph.HttpProvider(httpClientHandler, false)
-                {
-                    OverallTimeout = TimeSpan.FromSeconds(10)
-                };
-                Graph = new Microsoft.Graph.GraphServiceClient($"{OneDriveConfiguration.GraphApi}/v1.0", authProvider, httpProvider);
-            }
-            else
-            {
-                Graph = new Microsoft.Graph.GraphServiceClient($"{OneDriveConfiguration.GraphApi}/v1.0", authProvider);
-            }
+		//启用代理
+		if (!string.IsNullOrEmpty(OneDriveConfiguration.Proxy))
+		{
+			// Configure your proxy
+			var httpClientHandler = new HttpClientHandler
+			{
+				Proxy = new WebProxy(OneDriveConfiguration.Proxy),
+				UseDefaultCredentials = true
+			};
+			var httpProvider = new Microsoft.Graph.HttpProvider(httpClientHandler, false)
+			{
+				OverallTimeout = TimeSpan.FromSeconds(10)
+			};
+			Graph = new Microsoft.Graph.GraphServiceClient($"{OneDriveConfiguration.GraphApi}/v1.0", authProvider, httpProvider);
+		}
+		else
+		{
+			Graph = new Microsoft.Graph.GraphServiceClient($"{OneDriveConfiguration.GraphApi}/v1.0", authProvider);
+		}
 
-            //定时更新Token
-            _ = new Timer(_ =>
-              {
-                  if (File.Exists(TokenCacheHelper.CacheFilePath))
-                  {
-                      authorizeResult = authProvider.ClientApplication.AcquireTokenSilent(OneDriveConfiguration.Scopes, OneDriveConfiguration.AccountName).ExecuteAsync().Result;
-                  }
-              }, null, TimeSpan.Zero, TimeSpan.FromHours(1));
-        }
+		//定时更新Token
+		_ = new Timer(_ =>
+		{
+			if (File.Exists(TokenCacheHelper.CacheFilePath))
+			{
+				authorizeResult = authProvider.ClientApplication.AcquireTokenSilent(OneDriveConfiguration.Scopes, OneDriveConfiguration.AccountName).ExecuteAsync().Result;
+			}
+		}, null, TimeSpan.Zero, TimeSpan.FromHours(1));
+	}
 
-        /// <summary>
-        /// 验证
-        /// </summary>
-        /// <param name="code"></param>
-        /// <returns></returns>
-        public async Task<AuthenticationResult> Authorize(string code)
-        {
-            var authorizationCodeProvider = new AuthorizationCodeProvider(app);
-            authorizeResult = await authorizationCodeProvider.ClientApplication.AcquireTokenByAuthorizationCode(OneDriveConfiguration.Scopes, code).ExecuteAsync();
-            return authorizeResult;
-        }
-    }
+	/// <summary>
+	/// 验证
+	/// </summary>
+	/// <param name="code"></param>
+	/// <returns></returns>
+	public async Task<AuthenticationResult> Authorize(string code)
+	{
+		var authorizationCodeProvider = new AuthorizationCodeProvider(app);
+		authorizeResult = await authorizationCodeProvider.ClientApplication.AcquireTokenByAuthorizationCode(OneDriveConfiguration.Scopes, code).ExecuteAsync();
+		return authorizeResult;
+	}
 }

+ 15 - 16
src/Masuit.MyBlogs.Core/Infrastructure/DriveContext.cs

@@ -3,23 +3,22 @@ using Masuit.MyBlogs.Core.Models.Drive;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 
-namespace Masuit.MyBlogs.Core.Infrastructure
+namespace Masuit.MyBlogs.Core.Infrastructure;
+
+public sealed class DriveContext : DbContext
 {
-    public class DriveContext : DbContext
-    {
-        public DbSet<Setting> Settings { get; set; }
+	public DbSet<Setting> Settings { get; set; }
 
-        public DbSet<Site> Sites { get; set; }
+	public DbSet<Site> Sites { get; set; }
 
-        protected override void OnConfiguring(DbContextOptionsBuilder builder)
-        {
-            builder.UseSqlite(OneDriveConfiguration.ConnectionString);
-        }
+	protected override void OnConfiguring(DbContextOptionsBuilder builder)
+	{
+		builder.UseSqlite(OneDriveConfiguration.ConnectionString);
+	}
 
-        protected override void OnModelCreating(ModelBuilder modelBuilder)
-        {
-            var converter = new ValueConverter<string[], string>(model => string.Join(',', model), data => data.Split(',', StringSplitOptions.None));
-            modelBuilder.Entity<Site>().Property(s => s.HiddenFolders).HasConversion(converter);
-        }
-    }
-}
+	protected override void OnModelCreating(ModelBuilder modelBuilder)
+	{
+		var converter = new ValueConverter<string[], string>(model => string.Join(',', model), data => data.Split(',', StringSplitOptions.None));
+		modelBuilder.Entity<Site>().Property(s => s.HiddenFolders).HasConversion(converter);
+	}
+}

+ 1 - 1
src/Masuit.MyBlogs.Core/Infrastructure/LoggerDbContext.cs

@@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore;
 
 namespace Masuit.MyBlogs.Core.Infrastructure;
 
-public class LoggerDbContext : DbContext
+public sealed class LoggerDbContext : DbContext
 {
     public LoggerDbContext(DbContextOptions<LoggerDbContext> options) : base(options)
     {

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

@@ -3,6 +3,6 @@ using Masuit.MyBlogs.Core.Models.Entity;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Repository;
 
-public partial class CommentRepository : BaseRepository<Comment>, ICommentRepository
+public sealed partial class CommentRepository : BaseRepository<Comment>, ICommentRepository
 {
 }

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

@@ -3,6 +3,6 @@ using Masuit.MyBlogs.Core.Models.Entity;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Repository;
 
-public partial class LeaveMessageRepository : BaseRepository<LeaveMessage>, ILeaveMessageRepository
+public sealed partial class LeaveMessageRepository : BaseRepository<LeaveMessage>, ILeaveMessageRepository
 {
 }

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

@@ -3,6 +3,6 @@ using Masuit.MyBlogs.Core.Models.Entity;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Repository;
 
-public partial class MenuRepository : BaseRepository<Menu>, IMenuRepository
+public sealed partial class MenuRepository : BaseRepository<Menu>, IMenuRepository
 {
 }

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

@@ -6,7 +6,7 @@ using Z.EntityFramework.Plus;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Repository;
 
-public partial class PostRepository : BaseRepository<Post>, IPostRepository
+public sealed partial class PostRepository : BaseRepository<Post>, IPostRepository
 {
 	/// <summary>
 	/// 获取第一条数据,优先从缓存读取

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

@@ -3,82 +3,82 @@ using Masuit.MyBlogs.Core.Models.Entity;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Repository;
 
-public partial class CategoryRepository : BaseRepository<Category>, ICategoryRepository
+public sealed partial class CategoryRepository : BaseRepository<Category>, ICategoryRepository
 {
 }
 
-public partial class DonateRepository : BaseRepository<Donate>, IDonateRepository
+public sealed partial class DonateRepository : BaseRepository<Donate>, IDonateRepository
 {
 }
 
-public partial class FastShareRepository : BaseRepository<FastShare>, IFastShareRepository
+public sealed partial class FastShareRepository : BaseRepository<FastShare>, IFastShareRepository
 {
 }
 
-public partial class InternalMessageRepository : BaseRepository<InternalMessage>, IInternalMessageRepository
+public sealed partial class InternalMessageRepository : BaseRepository<InternalMessage>, IInternalMessageRepository
 {
 }
 
-public partial class LinksRepository : BaseRepository<Links>, ILinksRepository
+public sealed partial class LinksRepository : BaseRepository<Links>, ILinksRepository
 {
 }
 
-public partial class LinkLoopbackRepository : BaseRepository<LinkLoopback>, ILinkLoopbackRepository
+public sealed partial class LinkLoopbackRepository : BaseRepository<LinkLoopback>, ILinkLoopbackRepository
 {
 }
 
-public partial class LoginRecordRepository : BaseRepository<LoginRecord>, ILoginRecordRepository
+public sealed partial class LoginRecordRepository : BaseRepository<LoginRecord>, ILoginRecordRepository
 {
 }
 
-public partial class MiscRepository : BaseRepository<Misc>, IMiscRepository
+public sealed partial class MiscRepository : BaseRepository<Misc>, IMiscRepository
 {
 }
 
-public partial class NoticeRepository : BaseRepository<Notice>, INoticeRepository
+public sealed partial class NoticeRepository : BaseRepository<Notice>, INoticeRepository
 {
 }
 
-public partial class PostHistoryVersionRepository : BaseRepository<PostHistoryVersion>, IPostHistoryVersionRepository
+public sealed partial class PostHistoryVersionRepository : BaseRepository<PostHistoryVersion>, IPostHistoryVersionRepository
 {
 }
 
-public partial class SeminarRepository : BaseRepository<Seminar>, ISeminarRepository
+public sealed partial class SeminarRepository : BaseRepository<Seminar>, ISeminarRepository
 {
 }
 
-public partial class SystemSettingRepository : BaseRepository<SystemSetting>, ISystemSettingRepository
+public sealed partial class SystemSettingRepository : BaseRepository<SystemSetting>, ISystemSettingRepository
 {
 }
 
-public partial class UserInfoRepository : BaseRepository<UserInfo>, IUserInfoRepository
+public sealed partial class UserInfoRepository : BaseRepository<UserInfo>, IUserInfoRepository
 {
 }
 
-public partial class PostMergeRequestRepository : BaseRepository<PostMergeRequest>, IPostMergeRequestRepository
+public sealed partial class PostMergeRequestRepository : BaseRepository<PostMergeRequest>, IPostMergeRequestRepository
 {
 }
 
-public partial class AdvertisementRepository : BaseRepository<Advertisement>, IAdvertisementRepository
+public sealed partial class AdvertisementRepository : BaseRepository<Advertisement>, IAdvertisementRepository
 {
 }
 
-public partial class AdvertisementClickRecordRepository : BaseRepository<AdvertisementClickRecord>, IAdvertisementClickRecordRepository
+public sealed partial class AdvertisementClickRecordRepository : BaseRepository<AdvertisementClickRecord>, IAdvertisementClickRecordRepository
 {
 }
 
-public partial class VariablesRepository : BaseRepository<Variables>, IVariablesRepository
+public sealed partial class VariablesRepository : BaseRepository<Variables>, IVariablesRepository
 {
 }
 
-public partial class PostVisitRecordRepository : BaseRepository<PostVisitRecord>, IPostVisitRecordRepository
+public sealed partial class PostVisitRecordRepository : BaseRepository<PostVisitRecord>, IPostVisitRecordRepository
 {
 }
 
-public partial class PostVisitRecordStatsRepository : BaseRepository<PostVisitRecordStats>, IPostVisitRecordStatsRepository
+public sealed partial class PostVisitRecordStatsRepository : BaseRepository<PostVisitRecordStats>, IPostVisitRecordStatsRepository
 {
 }
 
-public partial class PostTagsRepository : BaseRepository<PostTag>, IPostTagsRepository
+public sealed partial class PostTagsRepository : BaseRepository<PostTag>, IPostTagsRepository
 {
 }

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

@@ -3,7 +3,7 @@ using Masuit.MyBlogs.Core.Models.Entity;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Repository;
 
-public partial class SearchDetailsRepository : BaseRepository<SearchDetails>, ISearchDetailsRepository
+public sealed partial class SearchDetailsRepository : BaseRepository<SearchDetails>, ISearchDetailsRepository
 {
     /// <summary>
     /// 热词统计

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

@@ -16,7 +16,7 @@ using Z.EntityFramework.Plus;
 
 namespace Masuit.MyBlogs.Core.Infrastructure.Services;
 
-public partial class AdvertisementService : BaseService<Advertisement>, IAdvertisementService
+public sealed partial class AdvertisementService : BaseService<Advertisement>, IAdvertisementService
 {
 	public ICacheManager<List<AdvertisementDto>> CacheManager { get; set; }
 

+ 39 - 40
src/Masuit.MyBlogs.Core/Infrastructure/Services/CategoryService.cs

@@ -3,47 +3,46 @@ 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
+namespace Masuit.MyBlogs.Core.Infrastructure.Services;
+
+public sealed partial class CategoryService : BaseService<Category>, ICategoryService
 {
-    public partial class CategoryService : BaseService<Category>, ICategoryService
-    {
-        public CategoryService(IBaseRepository<Category> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
+	public CategoryService(IBaseRepository<Category> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
 
-        /// <summary>
-        /// 删除分类,并将该分类下的文章移动到新分类下
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="mid"></param>
-        /// <returns></returns>
-        public async Task<bool> Delete(int id, int mid)
-        {
-            var category = await GetByIdAsync(id);
-            var categories = GetQuery(c => c.Path.StartsWith(category.Path)).ToList();
-            var moveCat = await GetByIdAsync(mid);
-            foreach (var c in categories)
-            {
-                for (var j = 0; j < c.Post.Count; j++)
-                {
-                    var p = c.Post.ElementAt(j);
-                    moveCat.Post.Add(p);
-                    for (var i = 0; i < p.PostHistoryVersion.Count; i++)
-                    {
-                        moveCat.PostHistoryVersion.Add(p.PostHistoryVersion.ElementAt(i));
-                    }
-                }
+	/// <summary>
+	/// 删除分类,并将该分类下的文章移动到新分类下
+	/// </summary>
+	/// <param name="id"></param>
+	/// <param name="mid"></param>
+	/// <returns></returns>
+	public async Task<bool> Delete(int id, int mid)
+	{
+		var category = await GetByIdAsync(id);
+		var categories = GetQuery(c => c.Path.StartsWith(category.Path)).ToList();
+		var moveCat = await GetByIdAsync(mid);
+		foreach (var c in categories)
+		{
+			for (var j = 0; j < c.Post.Count; j++)
+			{
+				var p = c.Post.ElementAt(j);
+				moveCat.Post.Add(p);
+				for (var i = 0; i < p.PostHistoryVersion.Count; i++)
+				{
+					moveCat.PostHistoryVersion.Add(p.PostHistoryVersion.ElementAt(i));
+				}
+			}
 
-                for (var i = 0; i < c.PostHistoryVersion.Count; i++)
-                {
-                    var p = c.PostHistoryVersion.ElementAt(i);
-                    p.CategoryId = moveCat.Id;
-                    moveCat.PostHistoryVersion.Add(p);
-                }
-            }
+			for (var i = 0; i < c.PostHistoryVersion.Count; i++)
+			{
+				var p = c.PostHistoryVersion.ElementAt(i);
+				p.CategoryId = moveCat.Id;
+				moveCat.PostHistoryVersion.Add(p);
+			}
+		}
 
-            bool b = await DeleteByIdAsync(id) > 0;
-            return b;
-        }
-    }
-}
+		bool b = await DeleteByIdAsync(id) > 0;
+		return b;
+	}
+}

+ 7 - 8
src/Masuit.MyBlogs.Core/Infrastructure/Services/CommentService.cs

@@ -3,12 +3,11 @@ 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
+namespace Masuit.MyBlogs.Core.Infrastructure.Services;
+
+public sealed partial class CommentService : BaseService<Comment>, ICommentService
 {
-    public partial class CommentService : BaseService<Comment>, ICommentService
-    {
-        public CommentService(IBaseRepository<Comment> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-}
+	public CommentService(IBaseRepository<Comment> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}

+ 6 - 7
src/Masuit.MyBlogs.Core/Infrastructure/Services/LeaveMessageService.cs

@@ -3,12 +3,11 @@ 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
+namespace Masuit.MyBlogs.Core.Infrastructure.Services;
+
+public sealed partial class LeaveMessageService : BaseService<LeaveMessage>, ILeaveMessageService
 {
-    public partial class LeaveMessageService : BaseService<LeaveMessage>, ILeaveMessageService
-    {
-        public LeaveMessageService(IBaseRepository<LeaveMessage> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
+	public LeaveMessageService(IBaseRepository<LeaveMessage> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
 }

+ 7 - 8
src/Masuit.MyBlogs.Core/Infrastructure/Services/MenuService.cs

@@ -3,12 +3,11 @@ 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
+namespace Masuit.MyBlogs.Core.Infrastructure.Services;
+
+public sealed partial class MenuService : BaseService<Menu>, IMenuService
 {
-    public partial class MenuService : BaseService<Menu>, IMenuService
-    {
-        public MenuService(IBaseRepository<Menu> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-}
+	public MenuService(IBaseRepository<Menu> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}

+ 233 - 234
src/Masuit.MyBlogs.Core/Infrastructure/Services/PostService.cs

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

+ 17 - 18
src/Masuit.MyBlogs.Core/Infrastructure/Services/SearchDetailsService.cs

@@ -3,25 +3,24 @@ 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
+namespace Masuit.MyBlogs.Core.Infrastructure.Services;
+
+public sealed partial class SearchDetailsService : BaseService<SearchDetails>, ISearchDetailsService
 {
-    public partial class SearchDetailsService : BaseService<SearchDetails>, ISearchDetailsService
-    {
-        private readonly ISearchDetailsRepository _searchDetailsRepository;
+	private readonly ISearchDetailsRepository _searchDetailsRepository;
 
-        public SearchDetailsService(IBaseRepository<SearchDetails> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher, ISearchDetailsRepository searchDetailsRepository) : base(repository, searchEngine, searcher)
-        {
-            _searchDetailsRepository = searchDetailsRepository;
-        }
+	public SearchDetailsService(IBaseRepository<SearchDetails> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher, ISearchDetailsRepository searchDetailsRepository) : base(repository, searchEngine, searcher)
+	{
+		_searchDetailsRepository = searchDetailsRepository;
+	}
 
-        /// <summary>
-        /// 搜索统计
-        /// </summary>
-        /// <param name="start"></param>
-        /// <returns></returns>
-        public List<SearchRank> GetRanks(DateTime start)
-        {
-            return _searchDetailsRepository.GetRanks(start);
-        }
-    }
+	/// <summary>
+	/// 搜索统计
+	/// </summary>
+	/// <param name="start"></param>
+	/// <returns></returns>
+	public List<SearchRank> GetRanks(DateTime start)
+	{
+		return _searchDetailsRepository.GetRanks(start);
+	}
 }

+ 119 - 120
src/Masuit.MyBlogs.Core/Infrastructure/Services/Services.cs

@@ -3,124 +3,123 @@ 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
-{
-    public partial class DonateService : BaseService<Donate>, IDonateService
-    {
-        public DonateService(IBaseRepository<Donate> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class FastShareService : BaseService<FastShare>, IFastShareService
-    {
-        public FastShareService(IBaseRepository<FastShare> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class InternalMessageService : BaseService<InternalMessage>, IInternalMessageService
-    {
-        public InternalMessageService(IBaseRepository<InternalMessage> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class LinksService : BaseService<Links>, ILinksService
-    {
-        public LinksService(IBaseRepository<Links> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class LinkLoopbackService : BaseService<LinkLoopback>, ILinkLoopbackService
-    {
-        public LinkLoopbackService(IBaseRepository<LinkLoopback> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class LoginRecordService : BaseService<LoginRecord>, ILoginRecordService
-    {
-        public LoginRecordService(IBaseRepository<LoginRecord> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class MiscService : BaseService<Misc>, IMiscService
-    {
-        public MiscService(IBaseRepository<Misc> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class NoticeService : BaseService<Notice>, INoticeService
-    {
-        public NoticeService(IBaseRepository<Notice> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class PostHistoryVersionService : BaseService<PostHistoryVersion>, IPostHistoryVersionService
-    {
-        public PostHistoryVersionService(IBaseRepository<PostHistoryVersion> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class SeminarService : BaseService<Seminar>, ISeminarService
-    {
-        public SeminarService(IBaseRepository<Seminar> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class SystemSettingService : BaseService<SystemSetting>, ISystemSettingService
-    {
-        public SystemSettingService(IBaseRepository<SystemSetting> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class PostMergeRequestService : BaseService<PostMergeRequest>, IPostMergeRequestService
-    {
-        public PostMergeRequestService(IBaseRepository<PostMergeRequest> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class VariablesService : BaseService<Variables>, IVariablesService
-    {
-        public VariablesService(IBaseRepository<Variables> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class PostVisitRecordService : BaseService<PostVisitRecord>, IPostVisitRecordService
-    {
-        public PostVisitRecordService(IBaseRepository<PostVisitRecord> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class PostVisitRecordStatsService : BaseService<PostVisitRecordStats>, IPostVisitRecordStatsService
-    {
-        public PostVisitRecordStatsService(IBaseRepository<PostVisitRecordStats> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class AdvertisementClickRecordService : BaseService<AdvertisementClickRecord>, IAdvertisementClickRecordService
-    {
-        public AdvertisementClickRecordService(IBaseRepository<AdvertisementClickRecord> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
-
-    public partial class PostTagService : BaseService<PostTag>, IPostTagService
-    {
-        public PostTagService(IBaseRepository<PostTag> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
+namespace Masuit.MyBlogs.Core.Infrastructure.Services;
+
+public sealed partial class DonateService : BaseService<Donate>, IDonateService
+{
+	public DonateService(IBaseRepository<Donate> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
 }
+
+public sealed partial class FastShareService : BaseService<FastShare>, IFastShareService
+{
+	public FastShareService(IBaseRepository<FastShare> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class InternalMessageService : BaseService<InternalMessage>, IInternalMessageService
+{
+	public InternalMessageService(IBaseRepository<InternalMessage> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class LinksService : BaseService<Links>, ILinksService
+{
+	public LinksService(IBaseRepository<Links> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class LinkLoopbackService : BaseService<LinkLoopback>, ILinkLoopbackService
+{
+	public LinkLoopbackService(IBaseRepository<LinkLoopback> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class LoginRecordService : BaseService<LoginRecord>, ILoginRecordService
+{
+	public LoginRecordService(IBaseRepository<LoginRecord> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class MiscService : BaseService<Misc>, IMiscService
+{
+	public MiscService(IBaseRepository<Misc> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class NoticeService : BaseService<Notice>, INoticeService
+{
+	public NoticeService(IBaseRepository<Notice> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class PostHistoryVersionService : BaseService<PostHistoryVersion>, IPostHistoryVersionService
+{
+	public PostHistoryVersionService(IBaseRepository<PostHistoryVersion> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class SeminarService : BaseService<Seminar>, ISeminarService
+{
+	public SeminarService(IBaseRepository<Seminar> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class SystemSettingService : BaseService<SystemSetting>, ISystemSettingService
+{
+	public SystemSettingService(IBaseRepository<SystemSetting> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class PostMergeRequestService : BaseService<PostMergeRequest>, IPostMergeRequestService
+{
+	public PostMergeRequestService(IBaseRepository<PostMergeRequest> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class VariablesService : BaseService<Variables>, IVariablesService
+{
+	public VariablesService(IBaseRepository<Variables> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class PostVisitRecordService : BaseService<PostVisitRecord>, IPostVisitRecordService
+{
+	public PostVisitRecordService(IBaseRepository<PostVisitRecord> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class PostVisitRecordStatsService : BaseService<PostVisitRecordStats>, IPostVisitRecordStatsService
+{
+	public PostVisitRecordStatsService(IBaseRepository<PostVisitRecordStats> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class AdvertisementClickRecordService : BaseService<AdvertisementClickRecord>, IAdvertisementClickRecordService
+{
+	public AdvertisementClickRecordService(IBaseRepository<AdvertisementClickRecord> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}
+
+public sealed partial class PostTagService : BaseService<PostTag>, IPostTagService
+{
+	public PostTagService(IBaseRepository<PostTag> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
+}

+ 131 - 132
src/Masuit.MyBlogs.Core/Infrastructure/Services/UserInfoService.cs

@@ -8,145 +8,144 @@ using Masuit.Tools;
 using Masuit.Tools.DateTimeExt;
 using Masuit.Tools.Security;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Services
+namespace Masuit.MyBlogs.Core.Infrastructure.Services;
+
+public sealed partial class UserInfoService : BaseService<UserInfo>, IUserInfoService
 {
-    public partial class UserInfoService : BaseService<UserInfo>, IUserInfoService
-    {
-        /// <summary>
-        /// 根据用户名获取
-        /// </summary>
-        /// <param name="name"></param>
-        /// <returns></returns>
-        public UserInfo GetByUsername(string name)
-        {
-            return Get(u => u.Username.Equals(name) || u.Email.Equals(name));
-        }
+	/// <summary>
+	/// 根据用户名获取
+	/// </summary>
+	/// <param name="name"></param>
+	/// <returns></returns>
+	public UserInfo GetByUsername(string name)
+	{
+		return Get(u => u.Username.Equals(name) || u.Email.Equals(name));
+	}
 
-        /// <summary>
-        /// 登录
-        /// </summary>
-        /// <param name="username"></param>
-        /// <param name="password"></param>
-        /// <returns></returns>
-        public UserInfoDto Login(string username, string password)
-        {
-            UserInfo userInfo = GetByUsername(username);
-            if (userInfo != null)
-            {
-                UserInfoDto user = userInfo.Mapper<UserInfoDto>();
-                string key = userInfo.SaltKey;
-                string pwd = userInfo.Password;
-                password = password.MDString3(key);
-                if (pwd.Equals(password))
-                {
-                    return user;
-                }
-            }
-            return null;
-        }
+	/// <summary>
+	/// 登录
+	/// </summary>
+	/// <param name="username"></param>
+	/// <param name="password"></param>
+	/// <returns></returns>
+	public UserInfoDto Login(string username, string password)
+	{
+		UserInfo userInfo = GetByUsername(username);
+		if (userInfo != null)
+		{
+			UserInfoDto user = userInfo.Mapper<UserInfoDto>();
+			string key = userInfo.SaltKey;
+			string pwd = userInfo.Password;
+			password = password.MDString3(key);
+			if (pwd.Equals(password))
+			{
+				return user;
+			}
+		}
+		return null;
+	}
 
-        /// <summary>
-        /// 注册
-        /// </summary>
-        /// <param name="userInfo"></param>
-        /// <returns></returns>
-        public UserInfo Register(UserInfo userInfo)
-        {
-            UserInfo exist = Get(u => u.Username.Equals(userInfo.Username) || u.Email.Equals(userInfo.Email));
-            if (exist is null)
-            {
-                var salt = $"{new Random().StrictNext()}{DateTime.Now.GetTotalMilliseconds()}".MDString2(Guid.NewGuid().ToString()).SHA256();
-                userInfo.Password = userInfo.Password.MDString3(salt);
-                userInfo.SaltKey = salt;
-                UserInfo added = AddEntity(userInfo);
-                int line = SaveChanges();
-                return line > 0 ? added : null;
-            }
-            return null;
-        }
+	/// <summary>
+	/// 注册
+	/// </summary>
+	/// <param name="userInfo"></param>
+	/// <returns></returns>
+	public UserInfo Register(UserInfo userInfo)
+	{
+		UserInfo exist = Get(u => u.Username.Equals(userInfo.Username) || u.Email.Equals(userInfo.Email));
+		if (exist is null)
+		{
+			var salt = $"{new Random().StrictNext()}{DateTime.Now.GetTotalMilliseconds()}".MDString2(Guid.NewGuid().ToString()).SHA256();
+			userInfo.Password = userInfo.Password.MDString3(salt);
+			userInfo.SaltKey = salt;
+			UserInfo added = AddEntity(userInfo);
+			int line = SaveChanges();
+			return line > 0 ? added : null;
+		}
+		return null;
+	}
 
-        /// <summary>
-        /// 检查用户名是否存在
-        /// </summary>
-        /// <param name="name"></param>
-        /// <returns></returns>
-        public bool UsernameExist(string name)
-        {
-            UserInfo userInfo = GetByUsername(name);
-            return userInfo != null;
-        }
+	/// <summary>
+	/// 检查用户名是否存在
+	/// </summary>
+	/// <param name="name"></param>
+	/// <returns></returns>
+	public bool UsernameExist(string name)
+	{
+		UserInfo userInfo = GetByUsername(name);
+		return userInfo != null;
+	}
 
-        /// <summary>
-        /// 检查邮箱是否存在
-        /// </summary>
-        /// <param name="email"></param>
-        /// <returns></returns>
-        public bool EmailExist(string email) => GetNoTracking(u => u.Email.Equals(email)) != null;
+	/// <summary>
+	/// 检查邮箱是否存在
+	/// </summary>
+	/// <param name="email"></param>
+	/// <returns></returns>
+	public bool EmailExist(string email) => GetNoTracking(u => u.Email.Equals(email)) != null;
 
-        /// <summary>
-        /// 修改密码
-        /// </summary>
-        /// <param name="name">用户名,邮箱或者电话号码</param>
-        /// <param name="oldPwd">旧密码</param>
-        /// <param name="newPwd">新密码</param>
-        /// <returns></returns>
-        public bool ChangePassword(string name, string oldPwd, string newPwd)
-        {
-            UserInfo userInfo = GetByUsername(name);
-            if (userInfo != null)
-            {
-                string key = userInfo.SaltKey;
-                string pwd = userInfo.Password;
-                oldPwd = oldPwd.MDString3(key);
-                if (pwd.Equals(oldPwd))
-                {
-                    var salt = $"{new Random().StrictNext()}{DateTime.Now.GetTotalMilliseconds()}".MDString2(Guid.NewGuid().ToString()).SHA256();
-                    userInfo.Password = newPwd.MDString3(salt);
-                    userInfo.SaltKey = salt;
-                    return SaveChanges() > 0;
-                }
-            }
-            return false;
-        }
+	/// <summary>
+	/// 修改密码
+	/// </summary>
+	/// <param name="name">用户名,邮箱或者电话号码</param>
+	/// <param name="oldPwd">旧密码</param>
+	/// <param name="newPwd">新密码</param>
+	/// <returns></returns>
+	public bool ChangePassword(string name, string oldPwd, string newPwd)
+	{
+		UserInfo userInfo = GetByUsername(name);
+		if (userInfo != null)
+		{
+			string key = userInfo.SaltKey;
+			string pwd = userInfo.Password;
+			oldPwd = oldPwd.MDString3(key);
+			if (pwd.Equals(oldPwd))
+			{
+				var salt = $"{new Random().StrictNext()}{DateTime.Now.GetTotalMilliseconds()}".MDString2(Guid.NewGuid().ToString()).SHA256();
+				userInfo.Password = newPwd.MDString3(salt);
+				userInfo.SaltKey = salt;
+				return SaveChanges() > 0;
+			}
+		}
+		return false;
+	}
 
-        public bool ChangePassword(int id, string oldPwd, string newPwd)
-        {
-            UserInfo userInfo = GetById(id);
-            if (userInfo != null)
-            {
-                string key = userInfo.SaltKey;
-                string pwd = userInfo.Password;
-                oldPwd = oldPwd.MDString3(key);
-                if (pwd.Equals(oldPwd))
-                {
-                    var salt = $"{new Random().StrictNext()}{DateTime.Now.GetTotalMilliseconds()}".MDString2(Guid.NewGuid().ToString()).SHA256();
-                    userInfo.Password = newPwd.MDString3(salt);
-                    userInfo.SaltKey = salt;
-                    return SaveChanges() > 0;
-                }
-            }
-            return false;
-        }
+	public bool ChangePassword(int id, string oldPwd, string newPwd)
+	{
+		UserInfo userInfo = GetById(id);
+		if (userInfo != null)
+		{
+			string key = userInfo.SaltKey;
+			string pwd = userInfo.Password;
+			oldPwd = oldPwd.MDString3(key);
+			if (pwd.Equals(oldPwd))
+			{
+				var salt = $"{new Random().StrictNext()}{DateTime.Now.GetTotalMilliseconds()}".MDString2(Guid.NewGuid().ToString()).SHA256();
+				userInfo.Password = newPwd.MDString3(salt);
+				userInfo.SaltKey = salt;
+				return SaveChanges() > 0;
+			}
+		}
+		return false;
+	}
 
-        /// <summary>
-        /// 重置密码
-        /// </summary>
-        /// <returns></returns>
-        public bool ResetPassword(string name, string newPwd = "123456")
-        {
-            UserInfo userInfo = GetByUsername(name);
-            if (userInfo != null)
-            {
-                var salt = $"{new Random().StrictNext()}{DateTime.Now.GetTotalMilliseconds()}".MDString2(Guid.NewGuid().ToString()).SHA256();
-                userInfo.Password = newPwd.MDString3(salt);
-                userInfo.SaltKey = salt;
-                return SaveChanges() > 0;
-            }
-            return false;
-        }
+	/// <summary>
+	/// 重置密码
+	/// </summary>
+	/// <returns></returns>
+	public bool ResetPassword(string name, string newPwd = "123456")
+	{
+		UserInfo userInfo = GetByUsername(name);
+		if (userInfo != null)
+		{
+			var salt = $"{new Random().StrictNext()}{DateTime.Now.GetTotalMilliseconds()}".MDString2(Guid.NewGuid().ToString()).SHA256();
+			userInfo.Password = newPwd.MDString3(salt);
+			userInfo.SaltKey = salt;
+			return SaveChanges() > 0;
+		}
+		return false;
+	}
 
-        public UserInfoService(IBaseRepository<UserInfo> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-        }
-    }
+	public UserInfoService(IBaseRepository<UserInfo> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+	{
+	}
 }

+ 24 - 25
src/Masuit.MyBlogs.Core/Models/Command/CategoryCommand.cs

@@ -2,32 +2,31 @@ using Masuit.MyBlogs.Core.Models.Entity;
 using Masuit.MyBlogs.Core.Models.Enum;
 using System.ComponentModel.DataAnnotations;
 
-namespace Masuit.MyBlogs.Core.Models.Command
+namespace Masuit.MyBlogs.Core.Models.Command;
+
+/// <summary>
+/// 文章分类输入模型
+/// </summary>
+public class CategoryCommand : BaseEntity
 {
-    /// <summary>
-    /// 文章分类输入模型
-    /// </summary>
-    public class CategoryCommand : BaseEntity
-    {
-        public CategoryCommand()
-        {
-            Status = Status.Available;
-        }
+	public CategoryCommand()
+	{
+		Status = Status.Available;
+	}
 
-        /// <summary>
-        /// 分类名
-        /// </summary>
-        [Required(ErrorMessage = "分类名不能为空"), MaxLength(64, ErrorMessage = "分类名最大允许64个字符"), MinLength(2, ErrorMessage = "分类名至少2个字符")]
-        public string Name { get; set; }
+	/// <summary>
+	/// 分类名
+	/// </summary>
+	[Required(ErrorMessage = "分类名不能为空"), MaxLength(64, ErrorMessage = "分类名最大允许64个字符"), MinLength(2, ErrorMessage = "分类名至少2个字符")]
+	public string Name { get; set; }
 
-        /// <summary>
-        /// 分类描述
-        /// </summary>
-        public string Description { get; set; }
+	/// <summary>
+	/// 分类描述
+	/// </summary>
+	public string Description { get; set; }
 
-        /// <summary>
-        /// 父级id
-        /// </summary>
-        public int? ParentId { get; set; }
-    }
-}
+	/// <summary>
+	/// 父级id
+	/// </summary>
+	public int? ParentId { get; set; }
+}