Pārlūkot izejas kodu

1.升级.NET8
2.性能优化

懒得勤快 1 gadu atpakaļ
vecāks
revīzija
8ea7a14fa8
24 mainītis faili ar 2311 papildinājumiem un 2259 dzēšanām
  1. 279 282
      src/Masuit.MyBlogs.Core/Common/UserAgent.cs
  2. 73 73
      src/Masuit.MyBlogs.Core/Configs/MappingProfile.cs
  3. 58 59
      src/Masuit.MyBlogs.Core/Controllers/CategoryController.cs
  4. 8 3
      src/Masuit.MyBlogs.Core/Controllers/HomeController.cs
  5. 1 1
      src/Masuit.MyBlogs.Core/Controllers/MsgController.cs
  6. 196 196
      src/Masuit.MyBlogs.Core/Controllers/NoticeController.cs
  7. 3 3
      src/Masuit.MyBlogs.Core/Controllers/UploadController.cs
  8. 6 25
      src/Masuit.MyBlogs.Core/Infrastructure/DataContext.cs
  9. 138 138
      src/Masuit.MyBlogs.Core/Infrastructure/Drive/DriveAccountService.cs
  10. 71 69
      src/Masuit.MyBlogs.Core/Infrastructure/Drive/TokenService.cs
  11. 556 556
      src/Masuit.MyBlogs.Core/Infrastructure/Repository/BaseRepository.cs
  12. 121 121
      src/Masuit.MyBlogs.Core/Infrastructure/Services/AdvertisementService.cs
  13. 7 7
      src/Masuit.MyBlogs.Core/Masuit.MyBlogs.Core.csproj
  14. 1 1
      src/Masuit.MyBlogs.Core/Models/DTO/CategoryDto.cs
  15. 53 39
      src/Masuit.MyBlogs.Core/Models/Entity/Category.cs
  16. 112 99
      src/Masuit.MyBlogs.Core/Models/Entity/Comment.cs
  17. 93 79
      src/Masuit.MyBlogs.Core/Models/Entity/LeaveMessage.cs
  18. 47 47
      src/Masuit.MyBlogs.Core/Models/Entity/Menu.cs
  19. 284 262
      src/Masuit.MyBlogs.Core/Models/Entity/Post.cs
  20. 46 46
      src/Masuit.MyBlogs.Core/Models/ViewModel/LeaveMessageViewModel.cs
  21. 1 1
      src/Masuit.MyBlogs.Core/Properties/PublishProfiles/FolderProfile.pubxml
  22. 129 129
      src/Masuit.MyBlogs.Core/Startup.cs
  23. 13 8
      src/Masuit.MyBlogs.Core/Views/Home/Category.cshtml
  24. 15 15
      src/Masuit.MyBlogs.Core/Views/Msg/Index.cshtml

+ 279 - 282
src/Masuit.MyBlogs.Core/Common/UserAgent.cs

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

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

@@ -7,76 +7,76 @@ namespace Masuit.MyBlogs.Core.Configs;
 /// </summary>
 public sealed class MappingProfile : Profile
 {
-	public MappingProfile()
-	{
-		CreateMap<CategoryCommand, Category>().ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
-		CreateMap<Category, CategoryDto_P>().ReverseMap();
-		CreateMap<Category, CategoryDto>().ReverseMap();
-		CreateMap<CategoryCommand, CategoryDto>().ReverseMap();
-
-		CreateMap<CommentCommand, Comment>().ForMember(c => c.Status, e => e.MapFrom(c => Status.Pending)).ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
-		CreateMap<Comment, CommentDto>().ReverseMap();
-		CreateMap<CommentCommand, CommentDto>().ReverseMap();
-		CreateMap<Comment, CommentViewModel>().ForMember(c => c.CommentDate, e => e.MapFrom(c => c.CommentDate.ToString("yyyy-MM-dd HH:mm:ss"))).ReverseMap();
-
-		CreateMap<LeaveMessageCommand, LeaveMessage>().ForMember(c => c.Status, e => e.MapFrom(c => Status.Pending)).ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
-		CreateMap<LeaveMessage, LeaveMessageDto>().ReverseMap();
-		CreateMap<LeaveMessageCommand, LeaveMessageDto>().ReverseMap();
-		CreateMap<LeaveMessage, LeaveMessageViewModel>().ForMember(l => l.PostDate, e => e.MapFrom(l => l.PostDate.ToString("yyyy-MM-dd HH:mm:ss"))).ReverseMap();
-
-		CreateMap<Links, LinksDto>().ForMember(e => e.Loopbacks, e => e.MapFrom(m => m.Loopbacks.GroupBy(e =>
-			e.IP).Count())).ReverseMap();
-
-		CreateMap<MenuCommand, Menu>().ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
-		CreateMap<Menu, MenuDto>().ForMember(m => m.Children, e => e.MapFrom(m => m.Children.OrderBy(c => c.Sort).ToList())).ReverseMap();
-
-		CreateMap<Misc, MiscDto>().ReverseMap();
-
-		CreateMap<Notice, NoticeDto>().ReverseMap();
-
-		CreateMap<PostCommand, Post>().ReverseMap();
-		CreateMap<Post, PostModelBase>();
-		CreateMap<Post, PostHistoryVersion>().ForMember(p => p.Id, e => e.Ignore()).ForMember(v => v.PostId, e => e.MapFrom(p => p.Id));
-		CreateMap<Post, PostDto>().ForMember(p => p.CategoryName, e => e.MapFrom(p => p.Category.Name)).ForMember(p => p.LimitMode, e => e.MapFrom(p => p.LimitMode ?? RegionLimitMode.All)).ForMember(p => p.Category, e => e.Ignore()).ReverseMap();
-		CreateMap<PostCommand, PostDto>().ReverseMap();
-		CreateMap<PostHistoryVersion, PostDto>().ForMember(p => p.CategoryName, e => e.MapFrom(p => p.Category.Name)).ReverseMap();
-		CreateMap<Post, PostDataModel>().ForMember(p => p.ModifyDate, e => e.MapFrom(p => p.ModifyDate))
-			.ForMember(p => p.PostDate, e => e.MapFrom(p => p.PostDate))
-			.ForMember(p => p.Status, e => e.MapFrom(p => p.Status.GetDisplay()))
-			.ForMember(p => p.ModifyCount, e => e.MapFrom(p => p.PostHistoryVersion.Count))
-			.ForMember(p => p.ViewCount, e => e.MapFrom(p => p.TotalViewCount))
-			.ForMember(p => p.Seminars, e => e.MapFrom(p => p.Seminar.Select(s => s.Id).ToArray()))
-			.ForMember(p => p.LimitDesc, e => e.MapFrom(p => p.LimitMode > RegionLimitMode.All ? string.Format(p.LimitMode.GetDescription(), p.Regions, p.ExceptRegions) : "无限制"));
-
-		CreateMap<SearchDetails, SearchDetailsDto>().ReverseMap();
-
-		CreateMap<UserInfo, UserInfoDto>();
-		CreateMap<UserInfoDto, UserInfo>()
-			.ForMember(u => u.Id, e => e.Ignore())
-			.ForMember(u => u.Password, e => e.Ignore())
-			.ForMember(u => u.SaltKey, e => e.Ignore());
-
-		CreateMap<LoginRecord, LoginRecordViewModel>().ReverseMap();
-
-		CreateMap<Seminar, SeminarDto>().ReverseMap();
-
-		CreateMap<PostMergeRequestCommandBase, PostMergeRequest>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.MergeState, e => e.Ignore()).ReverseMap();
-		CreateMap<PostMergeRequestCommand, PostMergeRequest>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.MergeState, e => e.Ignore()).ReverseMap();
-		CreateMap<PostMergeRequestCommand, Post>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.Status, e => e.Ignore()).ReverseMap();
-		CreateMap<PostMergeRequest, PostMergeRequestDtoBase>().ForMember(p => p.PostTitle, e => e.MapFrom(r => r.Post.Title));
-		CreateMap<PostMergeRequest, PostMergeRequestDto>().ForMember(p => p.PostTitle, e => e.MapFrom(r => r.Post.Title));
-		CreateMap<PostMergeRequest, Post>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.Status, e => e.Ignore()).ReverseMap();
-		CreateMap<Post, PostMergeRequestDto>().ReverseMap();
-
-		CreateMap<Advertisement, AdvertisementViewModel>()
-			.ForMember(m => m.AverageViewCount, e => e.MapFrom(a => a.ClickRecords.Where(o => o.Time >= DateTime.Today.AddMonths(-1)).GroupBy(r => r.Time.Date).Select(g => g.Count()).DefaultIfEmpty().Average()))
-			.ForMember(m => m.ViewCount, e => e.MapFrom(a => a.ClickRecords.Count(o => o.Time >= DateTime.Today.AddMonths(-1))));
-		CreateMap<AdvertisementDto, Advertisement>().ForMember(a => a.ClickRecords, e => e.Ignore()).ForMember(a => a.Status, e => e.Ignore()).ForMember(a => a.UpdateTime, e => e.MapFrom(a => DateTime.Now)).ReverseMap();
-
-		CreateMap<Donate, DonateDto>();
-
-		CreateMap<PostVisitRecord, PostVisitRecordViewModel>().ForMember(m => m.Time, e => e.MapFrom(m => m.Time.ToString("yyyy-MM-dd HH:mm:ss")));
-
-		CreateMap<AdvertisementClickRecord, AdvertisementClickRecordViewModel>().ForMember(m => m.Time, e => e.MapFrom(m => m.Time.ToString("yyyy-MM-dd HH:mm:ss")));
-	}
-}
+    public MappingProfile()
+    {
+        CreateMap<CategoryCommand, Category>().ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
+        CreateMap<Category, CategoryDto_P>().ReverseMap();
+        CreateMap<Category, CategoryDto>().ReverseMap();
+        CreateMap<CategoryCommand, CategoryDto>().ReverseMap();
+
+        CreateMap<CommentCommand, Comment>().ForMember(c => c.Status, e => e.MapFrom(c => Status.Pending)).ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
+        CreateMap<Comment, CommentDto>().ReverseMap();
+        CreateMap<CommentCommand, CommentDto>().ReverseMap();
+        CreateMap<Comment, CommentViewModel>().ForMember(c => c.CommentDate, e => e.MapFrom(c => c.CommentDate.ToString("yyyy-MM-dd HH:mm:ss"))).ReverseMap();
+
+        CreateMap<LeaveMessageCommand, LeaveMessage>().ForMember(c => c.Status, e => e.MapFrom(c => Status.Pending)).ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
+        CreateMap<LeaveMessage, LeaveMessageDto>().ReverseMap();
+        CreateMap<LeaveMessageCommand, LeaveMessageDto>().ReverseMap();
+        CreateMap<LeaveMessage, LeaveMessageViewModel>().ForMember(l => l.PostDate, e => e.MapFrom(l => l.PostDate.ToString("yyyy-MM-dd HH:mm:ss"))).ReverseMap();
+
+        CreateMap<Links, LinksDto>().ForMember(e => e.Loopbacks, e => e.MapFrom(m => m.Loopbacks.GroupBy(e =>
+            e.IP).Count())).ReverseMap();
+
+        CreateMap<MenuCommand, Menu>().ForMember(c => c.ParentId, e => e.MapFrom(c => c.ParentId > 0 ? c.ParentId : null)).ReverseMap();
+        CreateMap<Menu, MenuDto>().ForMember(m => m.Children, e => e.MapFrom(m => m.Children.OrderBy(c => c.Sort).ToList())).ReverseMap();
+
+        CreateMap<Misc, MiscDto>().ReverseMap();
+
+        CreateMap<Notice, NoticeDto>().ReverseMap();
+
+        CreateMap<PostCommand, Post>().ReverseMap();
+        CreateMap<Post, PostModelBase>();
+        CreateMap<Post, PostHistoryVersion>().ForMember(p => p.Id, e => e.Ignore()).ForMember(v => v.PostId, e => e.MapFrom(p => p.Id));
+        CreateMap<Post, PostDto>().ForMember(p => p.CategoryName, e => e.MapFrom(p => p.Category.Name)).ForMember(p => p.LimitMode, e => e.MapFrom(p => p.LimitMode ?? RegionLimitMode.All)).ForMember(p => p.Category, e => e.Ignore()).ReverseMap();
+        CreateMap<PostCommand, PostDto>().ReverseMap();
+        CreateMap<PostHistoryVersion, PostDto>().ForMember(p => p.CategoryName, e => e.MapFrom(p => p.Category.Name)).ReverseMap();
+        CreateMap<Post, PostDataModel>().ForMember(p => p.ModifyDate, e => e.MapFrom(p => p.ModifyDate))
+            .ForMember(p => p.PostDate, e => e.MapFrom(p => p.PostDate))
+            .ForMember(p => p.Status, e => e.MapFrom(p => p.Status.GetDisplay()))
+            .ForMember(p => p.ModifyCount, e => e.MapFrom(p => p.PostHistoryVersion.Count))
+            .ForMember(p => p.ViewCount, e => e.MapFrom(p => p.TotalViewCount))
+            .ForMember(p => p.Seminars, e => e.MapFrom(p => p.Seminar.Select(s => s.Id).ToArray()))
+            .ForMember(p => p.LimitDesc, e => e.MapFrom(p => p.LimitMode > RegionLimitMode.All ? string.Format(p.LimitMode.GetDescription(), p.Regions, p.ExceptRegions) : "无限制"));
+
+        CreateMap<SearchDetails, SearchDetailsDto>().ReverseMap();
+
+        CreateMap<UserInfo, UserInfoDto>();
+        CreateMap<UserInfoDto, UserInfo>()
+            .ForMember(u => u.Id, e => e.Ignore())
+            .ForMember(u => u.Password, e => e.Ignore())
+            .ForMember(u => u.SaltKey, e => e.Ignore());
+
+        CreateMap<LoginRecord, LoginRecordViewModel>().ReverseMap();
+
+        CreateMap<Seminar, SeminarDto>().ReverseMap();
+
+        CreateMap<PostMergeRequestCommandBase, PostMergeRequest>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.MergeState, e => e.Ignore()).ReverseMap();
+        CreateMap<PostMergeRequestCommand, PostMergeRequest>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.MergeState, e => e.Ignore()).ReverseMap();
+        CreateMap<PostMergeRequestCommand, Post>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.Status, e => e.Ignore()).ReverseMap();
+        CreateMap<PostMergeRequest, PostMergeRequestDtoBase>().ForMember(p => p.PostTitle, e => e.MapFrom(r => r.Post.Title));
+        CreateMap<PostMergeRequest, PostMergeRequestDto>().ForMember(p => p.PostTitle, e => e.MapFrom(r => r.Post.Title));
+        CreateMap<PostMergeRequest, Post>().ForMember(p => p.Id, e => e.Ignore()).ForMember(p => p.Status, e => e.Ignore()).ReverseMap();
+        CreateMap<Post, PostMergeRequestDto>().ReverseMap();
+
+        CreateMap<Advertisement, AdvertisementViewModel>()
+            .ForMember(m => m.AverageViewCount, e => e.MapFrom(a => a.ClickRecords.Where(o => o.Time >= DateTime.Today.AddMonths(-1)).GroupBy(r => r.Time.Date).Select(g => g.Count()).DefaultIfEmpty().Average()))
+            .ForMember(m => m.ViewCount, e => e.MapFrom(a => a.ClickRecords.Count(o => o.Time >= DateTime.Today.AddMonths(-1))));
+        CreateMap<AdvertisementDto, Advertisement>().ForMember(a => a.ClickRecords, e => e.Ignore()).ForMember(a => a.Status, e => e.Ignore()).ForMember(a => a.UpdateTime, e => e.MapFrom(a => DateTime.Now)).ReverseMap();
+
+        CreateMap<Donate, DonateDto>();
+
+        CreateMap<PostVisitRecord, PostVisitRecordViewModel>().ForMember(m => m.Time, e => e.MapFrom(m => m.Time.ToString("yyyy-MM-dd HH:mm:ss")));
+
+        CreateMap<AdvertisementClickRecord, AdvertisementClickRecordViewModel>().ForMember(m => m.Time, e => e.MapFrom(m => m.Time.ToString("yyyy-MM-dd HH:mm:ss")));
+    }
+}

+ 58 - 59
src/Masuit.MyBlogs.Core/Controllers/CategoryController.cs

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

+ 8 - 3
src/Masuit.MyBlogs.Core/Controllers/HomeController.cs

@@ -236,7 +236,7 @@ public sealed class HomeController : BaseController
     {
         page = Math.Max(1, page);
         var cat = await CategoryService.GetByIdAsync(id) ?? throw new NotFoundException("文章分类未找到");
-        var cids = cat.Flatten().Select(c => c.Id).ToArray();
+        var cids = CategoryService.GetQuery(c => c.Status == Status.Available && c.Path.StartsWith(cat.Path + ",")).Select(c => c.Id).Cacheable().AsEnumerable().Append(id).ToArray();
         var h24 = DateTime.Today.AddDays(-1);
         var posts = orderBy switch
         {
@@ -245,7 +245,6 @@ public sealed class HomeController : BaseController
         };
         var viewModel = GetIndexPageViewModel();
         viewModel.Posts = posts;
-        ViewBag.Category = cat;
         viewModel.PageParams = new Pagination(page, size, posts.TotalCount, orderBy);
         viewModel.SidebarAds = AdsService.GetsByWeightedPrice(2, AdvertiseType.SideBar, Request.Location(), id);
         viewModel.ListAdvertisement = AdsService.GetByWeightedPrice(AdvertiseType.ListItem, Request.Location(), id);
@@ -255,6 +254,7 @@ public sealed class HomeController : BaseController
             item.ModifyDate = item.ModifyDate.ToTimeZone(HttpContext.Session.Get<string>(SessionKey.TimeZone));
         }
 
+        ViewBag.Category = cat;
         return View(viewModel);
     }
 
@@ -316,7 +316,12 @@ public sealed class HomeController : BaseController
     {
         var postsQuery = PostService.GetQuery<PostDto>(PostBaseWhere()); //准备文章的查询
         var notices = NoticeService.GetPagesFromCache<DateTime, NoticeDto>(1, 5, n => n.NoticeStatus == NoticeStatus.Normal, n => n.ModifyDate, false); //加载前5条公告
-        var cats = CategoryService.GetQuery(c => c.Status == Status.Available && c.Post.Count > 0).Include(c => c.Parent).OrderBy(c => c.Name).ThenBy(c => c.Path).AsNoTracking().Cacheable().ToPooledListScope(); //加载分类目录
+        var cats = CategoryService.GetQuery(c => c.Status == Status.Available && c.Post.Count > 0).OrderBy(c => c.Name).ThenBy(c => c.Path).AsNoTracking().Select(c => new Category
+        {
+            Id = c.Id,
+            Name = c.Name,
+            ParentId = c.ParentId,
+        }).Cacheable().ToPooledListScope(); //加载分类目录
         var hotSearches = RedisHelper.Get<List<KeywordsRank>>("SearchRank:Week").AsNotNull().Take(10).ToPooledListScope(); //热词统计
         var hot5Post = postsQuery.OrderBy((new Random().Next() % 3) switch
         {

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

@@ -108,7 +108,7 @@ public sealed class MsgController : BaseController
                 parentTotal = total,
                 page,
                 size,
-                rows = Mapper.Map<IList<LeaveMessageViewModel>>(messages.OrderByDescending(c => c.PostDate).ToTree(c => c.Id, c => c.ParentId))
+                rows = Mapper.Map<List<LeaveMessageViewModel>>(messages.OrderByDescending(c => c.PostDate).ToTree(c => c.Id, c => c.ParentId))
             });
         }
 

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

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

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

@@ -129,9 +129,9 @@ public sealed class UploadController : Controller
         var doc = context.OpenAsync(req => req.Content(html)).Result;
         var body = doc.Body;
         var nodes = body.GetElementsByTagName("img");
-        foreach (var img in nodes)
+        foreach (var img in nodes.Select(x => x.Attributes["src"]))
         {
-            var attr = img.Attributes["src"].Value;
+            var attr = img.Value;
             var strs = attr.Split(",");
             var base64 = strs[1];
             var bytes = Convert.FromBase64String(base64);
@@ -143,7 +143,7 @@ public sealed class UploadController : Controller
             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("\\", "/");
+            img.Value = path[HostEnvironment.WebRootPath.Length..].Replace("\\", "/");
         }
 
         return body.InnerHtml.HtmlSanitizerCustom(attributes: new[] { "dir", "lang" });

+ 6 - 25
src/Masuit.MyBlogs.Core/Infrastructure/DataContext.cs

@@ -1,4 +1,5 @@
-using EntityFramework.Exceptions.Common;
+using System.Reflection;
+using EntityFramework.Exceptions.Common;
 using EntityFramework.Exceptions.PostgreSQL;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Diagnostics;
@@ -15,37 +16,21 @@ public class DataContext : DbContext
     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
     {
         optionsBuilder.UseExceptionProcessor();
-        optionsBuilder.EnableDetailedErrors().UseLazyLoadingProxies().UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll).ConfigureWarnings(builder => builder.Ignore(CoreEventId.DetachedLazyLoadingWarning, CoreEventId.LazyLoadOnDisposedContextWarning));
+        optionsBuilder.EnableDetailedErrors().UseLazyLoadingProxies(builder => builder.IgnoreNonVirtualNavigations()).UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll).ConfigureWarnings(builder => builder.Ignore(CoreEventId.DetachedLazyLoadingWarning, CoreEventId.LazyLoadOnDisposedContextWarning));
     }
 
     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.ApplyConfigurationsFromAssembly(Assembly.GetAssembly(GetType()));
         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<Menu>().Navigation(p => p.Children).EnableLazyLoading(false);
+        modelBuilder.Entity<Menu>().Navigation(p => p.Parent).EnableLazyLoading(false);
 
         modelBuilder.Entity<Links>().HasMany(e => e.Loopbacks).WithOne(l => l.Links).IsRequired().HasForeignKey(e => e.LinkId).OnDelete(DeleteBehavior.Cascade);
 
@@ -63,10 +48,6 @@ public class DataContext : DbContext
         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);

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

@@ -6,141 +6,141 @@ namespace Masuit.MyBlogs.Core.Infrastructure.Drive;
 
 public sealed 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; }
+    }
+}

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

@@ -7,80 +7,82 @@ namespace Masuit.MyBlogs.Core.Infrastructure.Drive;
 
 public sealed class TokenService
 {
-	public AuthorizationCodeProvider authProvider;
+    private 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 readonly IConfidentialClientApplication App;
 
-	public IConfidentialClientApplication app;
+    [Obsolete("Obsolete")]
+    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);
 
-		//缓存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);
-		}
+        //这里要传入一个 Scope 否则默认使用 https://graph.microsoft.com/.default
+        //而导致无法使用世纪互联版本
+        var authProvider = new AuthorizationCodeProvider(App, OneDriveConfiguration.Scopes);
 
-		//启用代理
-		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
+        if (File.Exists(TokenCacheHelper.CacheFilePath))
+        {
+            _authorizeResult = authProvider.ClientApplication.AcquireTokenSilent(OneDriveConfiguration.Scopes, OneDriveConfiguration.AccountName).ExecuteAsync().Result;
 
-		//定时更新Token
-		_ = new Timer(_ =>
-		{
-			if (File.Exists(TokenCacheHelper.CacheFilePath))
-			{
-				authorizeResult = authProvider.ClientApplication.AcquireTokenSilent(OneDriveConfiguration.Scopes, OneDriveConfiguration.AccountName).ExecuteAsync().Result;
-			}
-		}, null, TimeSpan.Zero, TimeSpan.FromHours(1));
-	}
+            //Debug.WriteLine(authorizeResult.AccessToken);
+        }
 
-	/// <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;
-	}
-}
+        //启用代理
+        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));
+    }
+
+    /// <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;
+    }
+}

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

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

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

@@ -11,137 +11,137 @@ namespace Masuit.MyBlogs.Core.Infrastructure.Services;
 
 public sealed partial class AdvertisementService : BaseService<Advertisement>, IAdvertisementService
 {
-	public ICacheManager<List<AdvertisementDto>> CacheManager { get; set; }
+    public ICacheManager<List<AdvertisementDto>> CacheManager { get; set; }
 
-	public ICategoryRepository CategoryRepository { get; set; }
+    public ICategoryRepository CategoryRepository { get; set; }
 
-	private readonly ILuceneIndexSearcher _luceneIndexSearcher;
+    private readonly ILuceneIndexSearcher _luceneIndexSearcher;
 
-	public AdvertisementService(IBaseRepository<Advertisement> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-	{
-		_luceneIndexSearcher = searcher;
-	}
+    public AdvertisementService(IBaseRepository<Advertisement> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+    {
+        _luceneIndexSearcher = searcher;
+    }
 
-	/// <summary>
-	/// 按价格随机筛选一个元素
-	/// </summary>
-	/// <param name="type">广告类型</param>
-	/// <param name="location"></param>
-	/// <param name="cid">分类id</param>
-	/// <param name="keywords"></param>
-	/// <returns></returns>
-	public AdvertisementDto GetByWeightedPrice(AdvertiseType type, IPLocation location, int? cid = null, string keywords = "")
-	{
-		return GetsByWeightedPrice(1, type, location, cid, keywords).FirstOrDefault();
-	}
+    /// <summary>
+    /// 按价格随机筛选一个元素
+    /// </summary>
+    /// <param name="type">广告类型</param>
+    /// <param name="location"></param>
+    /// <param name="cid">分类id</param>
+    /// <param name="keywords"></param>
+    /// <returns></returns>
+    public AdvertisementDto GetByWeightedPrice(AdvertiseType type, IPLocation location, int? cid = null, string keywords = "")
+    {
+        return GetsByWeightedPrice(1, type, location, cid, keywords).FirstOrDefault();
+    }
 
-	/// <summary>
-	/// 按价格随机筛选一个元素
-	/// </summary>
-	/// <param name="count">数量</param>
-	/// <param name="type">广告类型</param>
-	/// <param name="ipinfo"></param>
-	/// <param name="cid">分类id</param>
-	/// <param name="keywords"></param>
-	/// <returns></returns>
-	public List<AdvertisementDto> GetsByWeightedPrice(int count, AdvertiseType type, IPLocation ipinfo, int? cid = null, string keywords = "")
-	{
-		if (Count(a => a.Status == Status.Available) >= 200)
-		{
-			return GetsByWeightedPriceExternal(count, type, ipinfo, cid, keywords);
-		}
-		return GetsByWeightedPriceMemory(count, type, ipinfo, cid, keywords);
-	}
+    /// <summary>
+    /// 按价格随机筛选一个元素
+    /// </summary>
+    /// <param name="count">数量</param>
+    /// <param name="type">广告类型</param>
+    /// <param name="ipinfo"></param>
+    /// <param name="cid">分类id</param>
+    /// <param name="keywords"></param>
+    /// <returns></returns>
+    public List<AdvertisementDto> GetsByWeightedPrice(int count, AdvertiseType type, IPLocation ipinfo, int? cid = null, string keywords = "")
+    {
+        if (Count(a => a.Status == Status.Available) >= 200)
+        {
+            return GetsByWeightedPriceExternal(count, type, ipinfo, cid, keywords);
+        }
+        return GetsByWeightedPriceMemory(count, type, ipinfo, cid, keywords);
+    }
 
-	/// <summary>
-	/// 按价格随机筛选一个元素
-	/// </summary>
-	/// <param name="count">数量</param>
-	/// <param name="type">广告类型</param>
-	/// <param name="ipinfo"></param>
-	/// <param name="cid">分类id</param>
-	/// <param name="keywords"></param>
-	/// <returns></returns>
-	public List<AdvertisementDto> GetsByWeightedPriceExternal(int count, AdvertiseType type, IPLocation ipinfo, int? cid = null, string keywords = "")
-	{
-		var (location, _, _) = ipinfo;
-		return CacheManager.GetOrAdd($"Advertisement:{location.Crc32()}:{type}:{count}-{cid}-{keywords}", _ =>
-		{
-			var atype = type.ToString("D");
-			Expression<Func<Advertisement, bool>> where = a => a.Types.Contains(atype) && a.Status == Status.Available;
-			var catCount = CategoryRepository.Count(_ => true);
-			where = where.And(a => a.RegionMode == RegionLimitMode.All || (a.RegionMode == RegionLimitMode.AllowRegion ? Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase) : !Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase)));
-			if (cid.HasValue)
-			{
-				var pids = CategoryRepository.GetQuery(c => c.Id == cid).Select(c => string.Concat(c.ParentId, "|", c.Parent.ParentId).Trim('|')).Distinct().Cacheable(CacheExpirationMode.Absolute, TimeSpan.FromHours(5)).ToList();
-				var scid = pids.Append(cid + "").Join("|");
-				if (Any(a => Regex.IsMatch(a.CategoryIds, scid)))
-				{
-					where = where.And(a => Regex.IsMatch(a.CategoryIds, scid) || string.IsNullOrEmpty(a.CategoryIds));
-				}
-			}
+    /// <summary>
+    /// 按价格随机筛选一个元素
+    /// </summary>
+    /// <param name="count">数量</param>
+    /// <param name="type">广告类型</param>
+    /// <param name="ipinfo"></param>
+    /// <param name="cid">分类id</param>
+    /// <param name="keywords"></param>
+    /// <returns></returns>
+    public List<AdvertisementDto> GetsByWeightedPriceExternal(int count, AdvertiseType type, IPLocation ipinfo, int? cid = null, string keywords = "")
+    {
+        var (location, _, _) = ipinfo;
+        return CacheManager.GetOrAdd($"Advertisement:{location.Crc32()}:{type}:{count}-{cid}-{keywords}", _ =>
+        {
+            var atype = type.ToString("D");
+            Expression<Func<Advertisement, bool>> where = a => a.Types.Contains(atype) && a.Status == Status.Available;
+            var catCount = CategoryRepository.Count(_ => true);
+            where = where.And(a => a.RegionMode == RegionLimitMode.All || (a.RegionMode == RegionLimitMode.AllowRegion ? Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase) : !Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase)));
+            if (cid.HasValue)
+            {
+                var pids = CategoryRepository.GetQuery(c => c.Id == cid).Select(c => string.Concat(c.ParentId, "|", c.Parent.ParentId).Trim('|')).Distinct().Cacheable(CacheExpirationMode.Absolute, TimeSpan.FromHours(5)).ToList();
+                var scid = pids.Append(cid + "").Join("|");
+                if (Any(a => Regex.IsMatch(a.CategoryIds, scid)))
+                {
+                    where = where.And(a => Regex.IsMatch(a.CategoryIds, scid) || string.IsNullOrEmpty(a.CategoryIds));
+                }
+            }
 
-			if (!keywords.IsNullOrEmpty())
-			{
-				var regex = _luceneIndexSearcher.CutKeywords(keywords).Select(Regex.Escape).Join("|");
-				where = where.And(a => Regex.IsMatch(a.Title + a.Description, regex));
-			}
+            if (!keywords.IsNullOrEmpty())
+            {
+                var regex = _luceneIndexSearcher.CutKeywords(keywords).Select(Regex.Escape).Join("|");
+                where = where.And(a => Regex.IsMatch(a.Title + a.Description, regex));
+            }
 
-			var array = GetQuery(a => a.Status == Status.Available).GroupBy(a => a.Merchant).Select(g => g.OrderBy(_ => EF.Functions.Random()).FirstOrDefault().Id).Take(50).ToArray();
-			var list = GetQuery<AdvertisementDto>(where).Where(a => array.Contains(a.Id)).OrderBy(a => -Math.Log(EF.Functions.Random()) / ((double)a.Price / a.Types.Length * catCount / (string.IsNullOrEmpty(a.CategoryIds) ? catCount : (a.CategoryIds.Length + 1)))).Take(count).ToList();
-			if (list.Count == 0 && keywords is { Length: > 0 })
-			{
-				return GetsByWeightedPriceExternal(count, type, ipinfo, cid);
-			}
+            var array = GetQuery(a => a.Types.Contains(atype) && a.Status == Status.Available).GroupBy(a => a.Merchant).Select(g => g.OrderBy(_ => EF.Functions.Random()).FirstOrDefault().Id).Take(50).ToArray();
+            var list = GetQuery<AdvertisementDto>(where).Where(a => array.Contains(a.Id)).OrderBy(a => -Math.Log(EF.Functions.Random()) / ((double)a.Price / a.Types.Length * catCount / (string.IsNullOrEmpty(a.CategoryIds) ? catCount : (a.CategoryIds.Length + 1)))).Take(count).ToList();
+            if (list.Count == 0 && keywords is { Length: > 0 })
+            {
+                return GetsByWeightedPriceExternal(count, type, ipinfo, cid);
+            }
 
-			var ids = list.Select(a => a.Id).ToArray();
-			GetQuery(a => ids.Contains(a.Id)).ExecuteUpdate(p => p.SetProperty(a => a.DisplayCount, a => a.DisplayCount + 1));
-			return list;
-		});
-	}
+            var ids = list.Select(a => a.Id).ToArray();
+            GetQuery(a => ids.Contains(a.Id)).ExecuteUpdate(p => p.SetProperty(a => a.DisplayCount, a => a.DisplayCount + 1));
+            return list;
+        });
+    }
 
-	/// <summary>
-	/// 按价格随机筛选一个元素
-	/// </summary>
-	/// <param name="count">数量</param>
-	/// <param name="type">广告类型</param>
-	/// <param name="ipinfo"></param>
-	/// <param name="cid">分类id</param>
-	/// <param name="keywords"></param>
-	/// <returns></returns>
-	public List<AdvertisementDto> GetsByWeightedPriceMemory(int count, AdvertiseType type, IPLocation ipinfo, int? cid = null, string keywords = "")
-	{
-		var (location, _, _) = ipinfo;
-		var all = CacheManager.GetOrAdd("Advertisement:all", _ => GetQueryFromCache<AdvertisementDto>(a => a.Status == Status.Available).ToList());
-		return CacheManager.GetOrAdd($"Advertisement:{location.Crc32()}:{type}:{count}-{cid}-{keywords}", _ =>
-		{
-			var atype = type.ToString("D");
-			var catCount = CategoryRepository.Count(_ => true);
-			string scid = "";
-			if (cid.HasValue)
-			{
-				scid = CategoryRepository.GetQuery(c => c.Id == cid).Select(c => string.Concat(c.ParentId, "|", c.Parent.ParentId).Trim('|')).Distinct().Cacheable(CacheExpirationMode.Absolute, TimeSpan.FromHours(5)).AsEnumerable().Append(cid + "").Join("|");
-			}
+    /// <summary>
+    /// 按价格随机筛选一个元素
+    /// </summary>
+    /// <param name="count">数量</param>
+    /// <param name="type">广告类型</param>
+    /// <param name="ipinfo"></param>
+    /// <param name="cid">分类id</param>
+    /// <param name="keywords"></param>
+    /// <returns></returns>
+    public List<AdvertisementDto> GetsByWeightedPriceMemory(int count, AdvertiseType type, IPLocation ipinfo, int? cid = null, string keywords = "")
+    {
+        var (location, _, _) = ipinfo;
+        var all = CacheManager.GetOrAdd("Advertisement:all", _ => GetQuery<AdvertisementDto>(a => a.Status == Status.Available).ToList());
+        return CacheManager.GetOrAdd($"Advertisement:{location.Crc32()}:{type}:{count}-{cid}-{keywords}", _ =>
+        {
+            var atype = type.ToString("D");
+            var catCount = CategoryRepository.Count(_ => true);
+            string scid = "";
+            if (cid.HasValue)
+            {
+                scid = CategoryRepository.GetQuery(c => c.Id == cid).Select(c => string.Concat(c.ParentId, "|", c.Parent.ParentId).Trim('|')).Distinct().Cacheable(CacheExpirationMode.Absolute, TimeSpan.FromHours(5)).AsEnumerable().Append(cid + "").Join("|");
+            }
 
-			var array = all.GroupBy(a => a.Merchant).Select(g => g.OrderByRandom().FirstOrDefault().Id).Take(50).ToArray();
-			var list = all.Where(a => a.Types.Contains(atype) && array.Contains(a.Id))
-				.Where(a => a.RegionMode == RegionLimitMode.All || (a.RegionMode == RegionLimitMode.AllowRegion ? Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase) : !Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase)))
-				.WhereIf(cid.HasValue, a => Regex.IsMatch(a.CategoryIds + "", scid) || string.IsNullOrEmpty(a.CategoryIds))
-				.WhereIf(!keywords.IsNullOrEmpty(), a => (a.Title + a.Description).Contains(_luceneIndexSearcher.CutKeywords(keywords)))
-				.OrderBy(a => -Math.Log(Random.Shared.NextDouble()) / ((double)a.Price / a.Types.Length * catCount / (string.IsNullOrEmpty(a.CategoryIds) ? catCount : (a.CategoryIds.Length + 1))))
-				.Take(count).ToList();
-			if (list.Count == 0 && keywords is { Length: > 0 })
-			{
-				list.AddRange(all.Where(a => a.Types.Contains(atype) && array.Contains(a.Id))
-				.Where(a => a.RegionMode == RegionLimitMode.All || (a.RegionMode == RegionLimitMode.AllowRegion ? Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase) : !Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase)))
-				.WhereIf(cid.HasValue, a => Regex.IsMatch(a.CategoryIds + "", scid) || string.IsNullOrEmpty(a.CategoryIds))
-				.OrderBy(a => -Math.Log(Random.Shared.NextDouble()) / ((double)a.Price / a.Types.Length * catCount / (string.IsNullOrEmpty(a.CategoryIds) ? catCount : (a.CategoryIds.Length + 1))))
-				.Take(count));
-			}
+            var array = all.Where(a => a.Types.Contains(atype)).GroupBy(a => a.Merchant).Select(g => g.OrderByRandom().FirstOrDefault().Id).Take(50).ToArray();
+            var list = all.Where(a => a.Types.Contains(atype) && array.Contains(a.Id))
+                .Where(a => a.RegionMode == RegionLimitMode.All || (a.RegionMode == RegionLimitMode.AllowRegion ? Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase) : !Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase)))
+                .WhereIf(cid.HasValue, a => Regex.IsMatch(a.CategoryIds + "", scid) || string.IsNullOrEmpty(a.CategoryIds))
+                .WhereIf(!keywords.IsNullOrEmpty(), a => (a.Title + a.Description).Contains(_luceneIndexSearcher.CutKeywords(keywords)))
+                .OrderBy(a => -Math.Log(Random.Shared.NextDouble()) / ((double)a.Price / a.Types.Length * catCount / (string.IsNullOrEmpty(a.CategoryIds) ? catCount : (a.CategoryIds.Length + 1))))
+                .Take(count).ToList();
+            if (list.Count == 0 && keywords is { Length: > 0 })
+            {
+                list.AddRange(all.Where(a => a.Types.Contains(atype) && array.Contains(a.Id))
+                .Where(a => a.RegionMode == RegionLimitMode.All || (a.RegionMode == RegionLimitMode.AllowRegion ? Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase) : !Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase)))
+                .WhereIf(cid.HasValue, a => Regex.IsMatch(a.CategoryIds + "", scid) || string.IsNullOrEmpty(a.CategoryIds))
+                .OrderBy(a => -Math.Log(Random.Shared.NextDouble()) / ((double)a.Price / a.Types.Length * catCount / (string.IsNullOrEmpty(a.CategoryIds) ? catCount : (a.CategoryIds.Length + 1))))
+                .Take(count));
+            }
 
-			var ids = list.Select(a => a.Id).ToArray();
-			GetQuery(a => ids.Contains(a.Id)).ExecuteUpdate(p => p.SetProperty(a => a.DisplayCount, a => a.DisplayCount + 1));
-			return list;
-		});
-	}
+            var ids = list.Select(a => a.Id).ToArray();
+            GetQuery(a => ids.Contains(a.Id)).ExecuteUpdate(p => p.SetProperty(a => a.DisplayCount, a => a.DisplayCount + 1));
+            return list;
+        });
+    }
 }

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
 
     <PropertyGroup>
-        <TargetFramework>net7.0</TargetFramework>
+        <TargetFramework>net8.0</TargetFramework>
         <ServerGarbageCollection>false</ServerGarbageCollection>
         <ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
         <TieredPGO>true</TieredPGO>
@@ -51,22 +51,22 @@
         <PackageReference Include="Dispose.Scope.AspNetCore" Version="0.0.3" />
         <PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="4.0.0" />
         <PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="6.0.3" />
-        <PackageReference Include="FreeRedis" Version="1.1.12" />
+        <PackageReference Include="FreeRedis" Version="1.2.5" />
         <PackageReference Include="Hangfire" Version="1.8.6" />
         <PackageReference Include="Hangfire.MemoryStorage" Version="1.8.0" />
         <PackageReference Include="htmldiff.net" Version="1.4.2" />
         <PackageReference Include="Karambolo.AspNetCore.Bundling.NUglify" Version="3.7.0" />
         <PackageReference Include="Markdig" Version="0.33.0" />
         <PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
-        <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.13" />
-        <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.13" />
-        <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.13" />
-        <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="7.0.13" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
+        <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.0" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.0" />
+        <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.0" />
         <PackageReference Include="Microsoft.Graph" Version="4.54.0" />
         <PackageReference Include="Microsoft.Graph.Auth" Version="1.0.0-preview.7" />
         <PackageReference Include="Microsoft.NETCore.Platforms" Version="7.0.4" />
         <PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
-        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.11" />
+        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0-rc.2" />
         <PackageReference Include="OpenXmlPowerTools-NetStandard" Version="4.6.23-alpha4" />
         <PackageReference Include="MiniProfiler.EntityFrameworkCore" Version="4.3.8" />
         <PackageReference Include="PanGu.HighLight" Version="1.0.0" />

+ 1 - 1
src/Masuit.MyBlogs.Core/Models/DTO/CategoryDto.cs

@@ -5,7 +5,7 @@ namespace Masuit.MyBlogs.Core.Models.DTO;
 /// <summary>
 /// 文章分类输出模型
 /// </summary>
-public class CategoryDto : BaseDto, ITreeChildren<CategoryDto>
+public class CategoryDto : BaseDto, ITreeEntity<CategoryDto, int>
 {
     /// <summary>
     /// 分类名

+ 53 - 39
src/Masuit.MyBlogs.Core/Models/Entity/Category.cs

@@ -1,6 +1,8 @@
 using Masuit.Tools.Models;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
 
 namespace Masuit.MyBlogs.Core.Models.Entity;
 
@@ -8,43 +10,55 @@ namespace Masuit.MyBlogs.Core.Models.Entity;
 /// 文章分类
 /// </summary>
 [Table("Category")]
-public class Category : BaseEntity, ITree<Category>
+public class Category : BaseEntity, ITree<Category>, IEntityTypeConfiguration<Category>, ITreeEntity<Category, int>
 {
-	public Category()
-	{
-		Post = new HashSet<Post>();
-		Status = Status.Available;
-	}
-
-	/// <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>
-	/// 父级id
-	/// </summary>
-	public int? ParentId { get; set; }
-
-	public string Path { get; set; }
-
-	public virtual ICollection<Post> Post { get; set; }
-
-	public virtual ICollection<PostHistoryVersion> PostHistoryVersion { get; set; }
-
-	/// <summary>
-	/// 父节点
-	/// </summary>
-	public virtual Category Parent { get; set; }
-
-	/// <summary>
-	/// 子级
-	/// </summary>
-	public virtual ICollection<Category> Children { get; set; }
-}
+    public Category()
+    {
+        Post = new HashSet<Post>();
+        Status = Status.Available;
+    }
+
+    /// <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>
+    /// 父级id
+    /// </summary>
+    public int? ParentId { get; set; }
+
+    public string Path { get; set; }
+
+    public virtual ICollection<Post> Post { get; set; }
+
+    public virtual ICollection<PostHistoryVersion> PostHistoryVersion { get; set; }
+
+    /// <summary>
+    /// 父节点
+    /// </summary>
+    public virtual Category Parent { get; set; }
+
+    /// <summary>
+    /// 子级
+    /// </summary>
+    public virtual ICollection<Category> Children { get; set; }
+
+    /// <summary>
+    ///     Configures the entity of type <typeparamref name="TEntity" />.
+    /// </summary>
+    /// <param name="builder">The builder to be used to configure the entity type.</param>
+    public void Configure(EntityTypeBuilder<Category> builder)
+    {
+        builder.HasMany(e => e.Post).WithOne(e => e.Category).IsRequired(false).OnDelete(DeleteBehavior.Cascade);
+        builder.HasMany(e => e.PostHistoryVersion).WithOne(e => e.Category).HasForeignKey(r => r.CategoryId).OnDelete(DeleteBehavior.Cascade);
+        builder.HasMany(e => e.Children).WithOne(c => c.Parent).IsRequired(false).HasForeignKey(c => c.ParentId).OnDelete(DeleteBehavior.Cascade);
+        builder.Property(c => c.Path).IsRequired();
+    }
+}

+ 112 - 99
src/Masuit.MyBlogs.Core/Models/Entity/Comment.cs

@@ -3,6 +3,8 @@ using Masuit.Tools.Models;
 using System.ComponentModel;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
 
 namespace Masuit.MyBlogs.Core.Models.Entity;
 
@@ -10,103 +12,114 @@ namespace Masuit.MyBlogs.Core.Models.Entity;
 /// 评论表
 /// </summary>
 [Table("Comment")]
-public class Comment : BaseEntity, ITreeChildren<Comment>, ITreeParent<Comment>
+public class Comment : BaseEntity, ITreeEntity<Comment, int>, ITreeParent<Comment>, IEntityTypeConfiguration<Comment>
 {
-	public Comment()
-	{
-		Status = Status.Pending;
-		IsMaster = false;
-		Children = new List<Comment>();
-	}
-
-	/// <summary>
-	/// 昵称
-	/// </summary>
-	[Required(ErrorMessage = "既然要评论,不留名怎么行呢!"), MaxLength(24, ErrorMessage = "别闹,你这名字太长了吧!"), MinLength(2, ErrorMessage = "昵称至少2个字!")]
-	public string NickName { get; set; }
-
-	/// <summary>
-	/// 邮箱
-	/// </summary>
-	[IsEmail]
-	public string Email { get; set; }
-
-	/// <summary>
-	/// 评论内容
-	/// </summary>
-	[Required(ErrorMessage = "评论内容不能为空!")]
-	public string Content { get; set; }
-
-	/// <summary>
-	/// 父级ID
-	/// </summary>
-	public int? ParentId { get; set; }
-
-	/// <summary>
-	/// 文章ID
-	/// </summary>
-	public int PostId { get; set; }
-
-	/// <summary>
-	/// 发表时间
-	/// </summary>
-	public DateTime CommentDate { get; set; }
-
-	/// <summary>
-	/// 浏览器版本
-	/// </summary>
-	[StringLength(255)]
-	public string Browser { get; set; }
-
-	/// <summary>
-	/// 操作系统版本
-	/// </summary>
-	[StringLength(255)]
-	public string OperatingSystem { get; set; }
-
-	/// <summary>
-	/// 是否是博主
-	/// </summary>
-	[DefaultValue(false)]
-	public bool IsMaster { get; set; }
-
-	[NotMapped]
-	public bool IsAuthor { get; set; }
-
-	/// <summary>
-	/// 支持数
-	/// </summary>
-	public int VoteCount { get; set; }
-
-	/// <summary>
-	/// 反对数
-	/// </summary>
-	public int AgainstCount { get; set; }
-
-	/// <summary>
-	/// 提交人IP地址
-	/// </summary>
-	public string IP { get; set; }
-
-	/// <summary>
-	/// 地理信息
-	/// </summary>
-	public string Location { get; set; }
-
-	public string GroupTag { get; set; }
-
-	public string Path { get; set; }
-
-	[ForeignKey("PostId")]
-	public virtual Post Post { get; set; }
-
-	/// <summary>
-	/// 子级
-	/// </summary>
-	public virtual ICollection<Comment> Children { get; set; }
-
-	/// <summary>
-	/// 父节点
-	/// </summary>
-	public virtual Comment Parent { get; set; }
-}
+    public Comment()
+    {
+        Status = Status.Pending;
+        IsMaster = false;
+        Children = new List<Comment>();
+    }
+
+    /// <summary>
+    /// 昵称
+    /// </summary>
+    [Required(ErrorMessage = "既然要评论,不留名怎么行呢!"), MaxLength(24, ErrorMessage = "别闹,你这名字太长了吧!"), MinLength(2, ErrorMessage = "昵称至少2个字!")]
+    public string NickName { get; set; }
+
+    /// <summary>
+    /// 邮箱
+    /// </summary>
+    [IsEmail]
+    public string Email { get; set; }
+
+    /// <summary>
+    /// 评论内容
+    /// </summary>
+    [Required(ErrorMessage = "评论内容不能为空!")]
+    public string Content { get; set; }
+
+    /// <summary>
+    /// 父级ID
+    /// </summary>
+    public int? ParentId { get; set; }
+
+    /// <summary>
+    /// 文章ID
+    /// </summary>
+    public int PostId { get; set; }
+
+    /// <summary>
+    /// 发表时间
+    /// </summary>
+    public DateTime CommentDate { get; set; }
+
+    /// <summary>
+    /// 浏览器版本
+    /// </summary>
+    [StringLength(255)]
+    public string Browser { get; set; }
+
+    /// <summary>
+    /// 操作系统版本
+    /// </summary>
+    [StringLength(255)]
+    public string OperatingSystem { get; set; }
+
+    /// <summary>
+    /// 是否是博主
+    /// </summary>
+    [DefaultValue(false)]
+    public bool IsMaster { get; set; }
+
+    [NotMapped]
+    public bool IsAuthor { get; set; }
+
+    /// <summary>
+    /// 支持数
+    /// </summary>
+    public int VoteCount { get; set; }
+
+    /// <summary>
+    /// 反对数
+    /// </summary>
+    public int AgainstCount { get; set; }
+
+    /// <summary>
+    /// 提交人IP地址
+    /// </summary>
+    public string IP { get; set; }
+
+    /// <summary>
+    /// 地理信息
+    /// </summary>
+    public string Location { get; set; }
+
+    public string GroupTag { get; set; }
+
+    public string Path { get; set; }
+
+    [ForeignKey("PostId")]
+    public virtual Post Post { get; set; }
+
+    /// <summary>
+    /// 子级
+    /// </summary>
+    public ICollection<Comment> Children { get; set; }
+
+    /// <summary>
+    /// 父节点
+    /// </summary>
+    public Comment Parent { get; set; }
+
+    /// <summary>
+    ///     Configures the entity of type <typeparamref name="TEntity" />.
+    /// </summary>
+    /// <param name="builder">The builder to be used to configure the entity type.</param>
+    public void Configure(EntityTypeBuilder<Comment> builder)
+    {
+        builder.HasMany(e => e.Children).WithOne(c => c.Parent).HasForeignKey(c => c.ParentId).IsRequired(false).OnDelete(DeleteBehavior.Cascade);
+        builder.Property(c => c.Path).IsRequired();
+        builder.Property(c => c.GroupTag).IsRequired();
+    }
+}

+ 93 - 79
src/Masuit.MyBlogs.Core/Models/Entity/LeaveMessage.cs

@@ -4,6 +4,9 @@ using Masuit.Tools.Models;
 using System.ComponentModel;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using System.Reflection.Emit;
 
 namespace Masuit.MyBlogs.Core.Models.Entity;
 
@@ -11,83 +14,94 @@ namespace Masuit.MyBlogs.Core.Models.Entity;
 /// 留言板
 /// </summary>
 [Table("LeaveMessage")]
-public class LeaveMessage : BaseEntity, ITreeParent<LeaveMessage>, ITreeChildren<LeaveMessage>
+public class LeaveMessage : BaseEntity, ITreeParent<LeaveMessage>, ITreeChildren<LeaveMessage>, IEntityTypeConfiguration<LeaveMessage>
 {
-	public LeaveMessage()
-	{
-		PostDate = DateTime.Now;
-		Status = Status.Pending;
-		IsMaster = false;
-		Children = new List<LeaveMessage>();
-	}
-
-	/// <summary>
-	/// 昵称
-	/// </summary>
-	[Required(ErrorMessage = "昵称不能为空!")]
-	public string NickName { get; set; }
-
-	/// <summary>
-	/// 内容
-	/// </summary>
-	[Required(ErrorMessage = "留言内容不能为空!"), SubmitCheck]
-	public string Content { get; set; }
-
-	/// <summary>
-	/// 发表时间
-	/// </summary>
-	public DateTime PostDate { get; set; }
-
-	/// <summary>
-	/// 邮箱
-	/// </summary>
-	[IsEmail]
-	public string Email { get; set; }
-
-	/// <summary>
-	/// 父级ID
-	/// </summary>
-	public int? ParentId { get; set; }
-
-	/// <summary>
-	/// 浏览器版本
-	/// </summary>
-	[StringLength(255)]
-	public string Browser { get; set; }
-
-	/// <summary>
-	/// 操作系统版本
-	/// </summary>
-	[StringLength(255)]
-	public string OperatingSystem { get; set; }
-
-	/// <summary>
-	/// 是否是博主
-	/// </summary>
-	[DefaultValue(false)]
-	public bool IsMaster { get; set; }
-
-	/// <summary>
-	/// 提交人IP
-	/// </summary>
-	public string IP { get; set; }
-
-	/// <summary>
-	/// 地理信息
-	/// </summary>
-	public string Location { get; set; }
-
-	public string GroupTag { get; set; }
-
-	public string Path { get; set; }
-
-	/// <summary>
-	/// 父节点
-	/// </summary>
-	public virtual LeaveMessage Parent { get; set; }
-
-	/// <summary>
-	/// 子级
-	/// </summary>
-	public virtual ICollection<LeaveMessage> Children { get; set; }
-}
+    public LeaveMessage()
+    {
+        PostDate = DateTime.Now;
+        Status = Status.Pending;
+        IsMaster = false;
+        Children = new List<LeaveMessage>();
+    }
+
+    /// <summary>
+    /// 昵称
+    /// </summary>
+    [Required(ErrorMessage = "昵称不能为空!")]
+    public string NickName { get; set; }
+
+    /// <summary>
+    /// 内容
+    /// </summary>
+    [Required(ErrorMessage = "留言内容不能为空!"), SubmitCheck]
+    public string Content { get; set; }
+
+    /// <summary>
+    /// 发表时间
+    /// </summary>
+    public DateTime PostDate { get; set; }
+
+    /// <summary>
+    /// 邮箱
+    /// </summary>
+    [IsEmail]
+    public string Email { get; set; }
+
+    /// <summary>
+    /// 父级ID
+    /// </summary>
+    public int? ParentId { get; set; }
+
+    /// <summary>
+    /// 浏览器版本
+    /// </summary>
+    [StringLength(255)]
+    public string Browser { get; set; }
+
+    /// <summary>
+    /// 操作系统版本
+    /// </summary>
+    [StringLength(255)]
+    public string OperatingSystem { get; set; }
+
+    /// <summary>
+    /// 是否是博主
+    /// </summary>
+    [DefaultValue(false)]
+    public bool IsMaster { get; set; }
+
+    /// <summary>
+    /// 提交人IP
+    /// </summary>
+    public string IP { get; set; }
+
+    /// <summary>
+    /// 地理信息
+    /// </summary>
+    public string Location { get; set; }
+
+    public string GroupTag { get; set; }
+
+    public string Path { get; set; }
+
+    /// <summary>
+    /// 父节点
+    /// </summary>
+    public LeaveMessage Parent { get; set; }
+
+    /// <summary>
+    /// 子级
+    /// </summary>
+    public ICollection<LeaveMessage> Children { get; set; }
+
+    /// <summary>
+    ///     Configures the entity of type <typeparamref name="TEntity" />.
+    /// </summary>
+    /// <param name="builder">The builder to be used to configure the entity type.</param>
+    public void Configure(EntityTypeBuilder<LeaveMessage> builder)
+    {
+        builder.HasMany(e => e.Children).WithOne(c => c.Parent).HasForeignKey(c => c.ParentId).IsRequired(false).OnDelete(DeleteBehavior.Cascade);
+        builder.Property(c => c.Path).IsRequired();
+        builder.Property(c => c.GroupTag).IsRequired();
+    }
+}

+ 47 - 47
src/Masuit.MyBlogs.Core/Models/Entity/Menu.cs

@@ -8,61 +8,61 @@ namespace Masuit.MyBlogs.Core.Models.Entity;
 /// 导航菜单
 /// </summary>
 [Table("Menu")]
-public class Menu : BaseEntity, ITree<Menu>
+public class Menu : BaseEntity, ITree<Menu>, ITreeEntity<Menu, int>
 {
-	public Menu()
-	{
-		ParentId = 0;
-		Status = Status.Available;
-		Children = new List<Menu>();
-	}
+    public Menu()
+    {
+        ParentId = 0;
+        Status = Status.Available;
+        Children = new List<Menu>();
+    }
 
-	/// <summary>
-	/// 名字
-	/// </summary>
-	[Required(ErrorMessage = "菜单名不能为空!")]
-	public string Name { get; set; }
+    /// <summary>
+    /// 名字
+    /// </summary>
+    [Required(ErrorMessage = "菜单名不能为空!")]
+    public string Name { get; set; }
 
-	/// <summary>
-	/// 父节点
-	/// </summary>
-	public virtual Menu Parent { get; set; }
+    /// <summary>
+    /// 父节点
+    /// </summary>
+    public Menu Parent { get; set; }
 
-	/// <summary>
-	/// 子级
-	/// </summary>
-	public virtual ICollection<Menu> Children { get; set; }
+    /// <summary>
+    /// 子级
+    /// </summary>
+    public ICollection<Menu> Children { get; set; }
 
-	/// <summary>
-	/// 图标
-	/// </summary>
-	public string Icon { get; set; }
+    /// <summary>
+    /// 图标
+    /// </summary>
+    public string Icon { get; set; }
 
-	/// <summary>
-	/// URL
-	/// </summary>
-	[Required(ErrorMessage = "菜单的URL不能为空!")]
-	public string Url { get; set; }
+    /// <summary>
+    /// URL
+    /// </summary>
+    [Required(ErrorMessage = "菜单的URL不能为空!")]
+    public string Url { get; set; }
 
-	/// <summary>
-	/// 排序号
-	/// </summary>
-	public int Sort { get; set; }
+    /// <summary>
+    /// 排序号
+    /// </summary>
+    public int Sort { get; set; }
 
-	/// <summary>
-	/// 父级ID
-	/// </summary>
-	public int? ParentId { get; set; }
+    /// <summary>
+    /// 父级ID
+    /// </summary>
+    public int? ParentId { get; set; }
 
-	/// <summary>
-	/// 菜单类型
-	/// </summary>
-	public virtual MenuType MenuType { get; set; }
+    /// <summary>
+    /// 菜单类型
+    /// </summary>
+    public virtual MenuType MenuType { get; set; }
 
-	/// <summary>
-	/// 是否在新标签页打开
-	/// </summary>
-	public bool NewTab { get; set; }
+    /// <summary>
+    /// 是否在新标签页打开
+    /// </summary>
+    public bool NewTab { get; set; }
 
-	public string Path { get; set; }
-}
+    public string Path { get; set; }
+}

+ 284 - 262
src/Masuit.MyBlogs.Core/Models/Entity/Post.cs

@@ -3,6 +3,9 @@ using Masuit.MyBlogs.Core.Models.Validation;
 using System.ComponentModel;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using System.Reflection.Emit;
 
 namespace Masuit.MyBlogs.Core.Models.Entity;
 
@@ -10,230 +13,249 @@ namespace Masuit.MyBlogs.Core.Models.Entity;
 /// 文章
 /// </summary>
 [Table("Post")]
-public class Post : BaseEntity
+public class Post : BaseEntity, IEntityTypeConfiguration<Post>
 {
-	public Post()
-	{
-		Comment = new HashSet<Comment>();
-		PostDate = DateTime.Now;
-		ModifyDate = DateTime.Now;
-		IsFixedTop = false;
-		Status = Status.Pending;
-		Seminar = new HashSet<Seminar>();
-		PostMergeRequests = new HashSet<PostMergeRequest>();
-		PostVisitRecordStats = new List<PostVisitRecordStats>();
-		PostVisitRecords = new List<PostVisitRecord>();
-	}
-
-	/// <summary>
-	/// 标题
-	/// </summary>
-	[Required(ErrorMessage = "文章标题不能为空!"), LuceneIndex]
-	public string Title { get; set; }
-
-	/// <summary>
-	/// 作者
-	/// </summary>
-	[Required, MaxLength(24, ErrorMessage = "作者名最长支持24个字符!"), LuceneIndex]
-	public string Author { get; set; }
-
-	/// <summary>
-	/// 内容
-	/// </summary>
-	[Required(ErrorMessage = "文章内容不能为空!"), SubmitCheck(20, 1000000, false), LuceneIndex(IsHtml = true)]
-	public string Content { get; set; }
-
-	/// <summary>
-	/// 受保护的内容
-	/// </summary>
-	[LuceneIndex(IsHtml = true)]
-	public string ProtectContent { get; set; }
-
-	/// <summary>
-	/// 受保护内容模式
-	/// </summary>
-	public ProtectContentMode ProtectContentMode { get; set; }
-
-	/// <summary>
-	/// 发表时间
-	/// </summary>
-	public DateTime PostDate { get; set; }
-
-	/// <summary>
-	/// 修改时间
-	/// </summary>
-	[LuceneIndex]
-	public DateTime ModifyDate { get; set; }
-
-	/// <summary>
-	/// 是否置顶
-	/// </summary>
-	[DefaultValue(false)]
-	public bool IsFixedTop { get; set; }
-
-	/// <summary>
-	/// 分类id
-	/// </summary>
-	public int CategoryId { get; set; }
-
-	/// <summary>
-	/// 作者邮箱
-	/// </summary>
-	[Required(ErrorMessage = "作者邮箱不能为空!"), EmailAddress, LuceneIndex]
-	public string Email { get; set; }
-
-	/// <summary>
-	/// 修改人名字
-	/// </summary>
-	[LuceneIndex]
-	public string Modifier { get; set; }
-
-	/// <summary>
-	/// 修改人邮箱
-	/// </summary>
-	[LuceneIndex]
-	public string ModifierEmail { get; set; }
-
-	/// <summary>
-	/// 标签
-	/// </summary>
-	[StringLength(256, ErrorMessage = "标签最大允许255个字符"), LuceneIndex]
-	public string Label { get; set; }
-
-	/// <summary>
-	/// 文章关键词
-	/// </summary>
-	[StringLength(256, ErrorMessage = "文章关键词最大允许255个字符"), LuceneIndex]
-	public string Keyword { get; set; }
-
-	/// <summary>
-	/// 支持数
-	/// </summary>
-	[DefaultValue(0), ConcurrencyCheck]
-	public int VoteUpCount { get; set; }
-
-	/// <summary>
-	/// 反对数
-	/// </summary>
-	[DefaultValue(0), ConcurrencyCheck]
-	public int VoteDownCount { get; set; }
-
-	/// <summary>
-	/// 每日平均访问量
-	/// </summary>
-	[ConcurrencyCheck]
-	public double AverageViewCount { get; set; }
-
-	/// <summary>
-	/// 总访问量
-	/// </summary>
-	[ConcurrencyCheck]
-	public int TotalViewCount { get; set; }
-
-	/// <summary>
-	/// 提交人IP地址
-	/// </summary>
-	public string IP { get; set; }
-
-	/// <summary>
-	/// 禁止评论
-	/// </summary>
-	public bool DisableComment { get; set; }
-
-	/// <summary>
-	/// 禁止转载
-	/// </summary>
-	public bool DisableCopy { get; set; }
-
-	/// <summary>
-	/// 限制模式
-	/// </summary>
-	public RegionLimitMode? LimitMode { get; set; }
-
-	/// <summary>
-	/// 限制地区,竖线分隔
-	/// </summary>
-	public string Regions { get; set; }
-
-	/// <summary>
-	/// 限制排除地区,竖线分隔
-	/// </summary>
-	public string ExceptRegions { get; set; }
-
-	/// <summary>
-	/// 限制模式
-	/// </summary>
-	public RegionLimitMode? ProtectContentLimitMode { get; set; }
-
-	/// <summary>
-	/// 限制地区,竖线分隔
-	/// </summary>
-	public string ProtectContentRegions { get; set; }
-
-	/// <summary>
-	/// 保护密码
-	/// </summary>
-	public string ProtectPassword { get; set; }
-
-	/// <summary>
-	/// 开启rss订阅
-	/// </summary>
-	public bool Rss { get; set; }
-
-	/// <summary>
-	/// 锁定编辑
-	/// </summary>
-	public bool Locked { get; set; }
-
-	/// <summary>
-	/// 重定向到第三方链接
-	/// </summary>
-	public string Redirect { get; set; }
-
-	/// <summary>
-	/// 过期时间
-	/// </summary>
-	public DateTime? ExpireAt { get; set; }
-
-	/// <summary>
-	/// 是否是不安全内容
-	/// </summary>
-	public bool IsNsfw { get; set; }
-
-	/// <summary>
-	/// 分类
-	/// </summary>
-	public virtual Category Category { get; set; }
-
-	/// <summary>
-	/// 评论
-	/// </summary>
-	public virtual ICollection<Comment> Comment { get; set; }
-
-	/// <summary>
-	/// 专题
-	/// </summary>
-	public virtual ICollection<Seminar> Seminar { get; set; }
-
-	/// <summary>
-	/// 文章历史版本
-	/// </summary>
-	public virtual ICollection<PostHistoryVersion> PostHistoryVersion { get; set; }
-
-	/// <summary>
-	/// 文章修改请求
-	/// </summary>
-	public virtual ICollection<PostMergeRequest> PostMergeRequests { get; set; }
-
-	/// <summary>
-	/// 访问记录
-	/// </summary>
-	public virtual ICollection<PostVisitRecord> PostVisitRecords { get; set; }
-
-	/// <summary>
-	/// 访问记录统计
-	/// </summary>
-	public virtual ICollection<PostVisitRecordStats> PostVisitRecordStats { get; set; }
+    public Post()
+    {
+        Comment = new HashSet<Comment>();
+        PostDate = DateTime.Now;
+        ModifyDate = DateTime.Now;
+        IsFixedTop = false;
+        Status = Status.Pending;
+        Seminar = new HashSet<Seminar>();
+        PostMergeRequests = new HashSet<PostMergeRequest>();
+        PostVisitRecordStats = new List<PostVisitRecordStats>();
+        PostVisitRecords = new List<PostVisitRecord>();
+    }
+
+    /// <summary>
+    /// 标题
+    /// </summary>
+    [Required(ErrorMessage = "文章标题不能为空!"), LuceneIndex]
+    public string Title { get; set; }
+
+    /// <summary>
+    /// 作者
+    /// </summary>
+    [Required, MaxLength(24, ErrorMessage = "作者名最长支持24个字符!"), LuceneIndex]
+    public string Author { get; set; }
+
+    /// <summary>
+    /// 内容
+    /// </summary>
+    [Required(ErrorMessage = "文章内容不能为空!"), SubmitCheck(20, 1000000, false), LuceneIndex(IsHtml = true)]
+    public string Content { get; set; }
+
+    /// <summary>
+    /// 受保护的内容
+    /// </summary>
+    [LuceneIndex(IsHtml = true)]
+    public string ProtectContent { get; set; }
+
+    /// <summary>
+    /// 受保护内容模式
+    /// </summary>
+    public ProtectContentMode ProtectContentMode { get; set; }
+
+    /// <summary>
+    /// 发表时间
+    /// </summary>
+    public DateTime PostDate { get; set; }
+
+    /// <summary>
+    /// 修改时间
+    /// </summary>
+    [LuceneIndex]
+    public DateTime ModifyDate { get; set; }
+
+    /// <summary>
+    /// 是否置顶
+    /// </summary>
+    [DefaultValue(false)]
+    public bool IsFixedTop { get; set; }
+
+    /// <summary>
+    /// 分类id
+    /// </summary>
+    public int CategoryId { get; set; }
+
+    /// <summary>
+    /// 作者邮箱
+    /// </summary>
+    [Required(ErrorMessage = "作者邮箱不能为空!"), EmailAddress, LuceneIndex]
+    public string Email { get; set; }
+
+    /// <summary>
+    /// 修改人名字
+    /// </summary>
+    [LuceneIndex]
+    public string Modifier { get; set; }
+
+    /// <summary>
+    /// 修改人邮箱
+    /// </summary>
+    [LuceneIndex]
+    public string ModifierEmail { get; set; }
+
+    /// <summary>
+    /// 标签
+    /// </summary>
+    [StringLength(256, ErrorMessage = "标签最大允许255个字符"), LuceneIndex]
+    public string Label { get; set; }
+
+    /// <summary>
+    /// 文章关键词
+    /// </summary>
+    [StringLength(256, ErrorMessage = "文章关键词最大允许255个字符"), LuceneIndex]
+    public string Keyword { get; set; }
+
+    /// <summary>
+    /// 支持数
+    /// </summary>
+    [DefaultValue(0), ConcurrencyCheck]
+    public int VoteUpCount { get; set; }
+
+    /// <summary>
+    /// 反对数
+    /// </summary>
+    [DefaultValue(0), ConcurrencyCheck]
+    public int VoteDownCount { get; set; }
+
+    /// <summary>
+    /// 每日平均访问量
+    /// </summary>
+    [ConcurrencyCheck]
+    public double AverageViewCount { get; set; }
+
+    /// <summary>
+    /// 总访问量
+    /// </summary>
+    [ConcurrencyCheck]
+    public int TotalViewCount { get; set; }
+
+    /// <summary>
+    /// 提交人IP地址
+    /// </summary>
+    public string IP { get; set; }
+
+    /// <summary>
+    /// 禁止评论
+    /// </summary>
+    public bool DisableComment { get; set; }
+
+    /// <summary>
+    /// 禁止转载
+    /// </summary>
+    public bool DisableCopy { get; set; }
+
+    /// <summary>
+    /// 限制模式
+    /// </summary>
+    public RegionLimitMode? LimitMode { get; set; }
+
+    /// <summary>
+    /// 限制地区,竖线分隔
+    /// </summary>
+    public string Regions { get; set; }
+
+    /// <summary>
+    /// 限制排除地区,竖线分隔
+    /// </summary>
+    public string ExceptRegions { get; set; }
+
+    /// <summary>
+    /// 限制模式
+    /// </summary>
+    public RegionLimitMode? ProtectContentLimitMode { get; set; }
+
+    /// <summary>
+    /// 限制地区,竖线分隔
+    /// </summary>
+    public string ProtectContentRegions { get; set; }
+
+    /// <summary>
+    /// 保护密码
+    /// </summary>
+    public string ProtectPassword { get; set; }
+
+    /// <summary>
+    /// 开启rss订阅
+    /// </summary>
+    public bool Rss { get; set; }
+
+    /// <summary>
+    /// 锁定编辑
+    /// </summary>
+    public bool Locked { get; set; }
+
+    /// <summary>
+    /// 重定向到第三方链接
+    /// </summary>
+    public string Redirect { get; set; }
+
+    /// <summary>
+    /// 过期时间
+    /// </summary>
+    public DateTime? ExpireAt { get; set; }
+
+    /// <summary>
+    /// 是否是不安全内容
+    /// </summary>
+    public bool IsNsfw { get; set; }
+
+    /// <summary>
+    /// 分类
+    /// </summary>
+    public virtual Category Category { get; set; }
+
+    /// <summary>
+    /// 评论
+    /// </summary>
+    public virtual ICollection<Comment> Comment { get; set; }
+
+    /// <summary>
+    /// 专题
+    /// </summary>
+    public virtual ICollection<Seminar> Seminar { get; set; }
+
+    /// <summary>
+    /// 文章历史版本
+    /// </summary>
+    public virtual ICollection<PostHistoryVersion> PostHistoryVersion { get; set; }
+
+    /// <summary>
+    /// 文章修改请求
+    /// </summary>
+    public virtual ICollection<PostMergeRequest> PostMergeRequests { get; set; }
+
+    /// <summary>
+    /// 访问记录
+    /// </summary>
+    public virtual ICollection<PostVisitRecord> PostVisitRecords { get; set; }
+
+    /// <summary>
+    /// 访问记录统计
+    /// </summary>
+    public virtual ICollection<PostVisitRecordStats> PostVisitRecordStats { get; set; }
+
+    /// <summary>
+    ///     Configures the entity of type <typeparamref name="TEntity" />.
+    /// </summary>
+    /// <param name="builder">The builder to be used to configure the entity type.</param>
+    public void Configure(EntityTypeBuilder<Post> builder)
+    {
+        builder.HasMany(e => e.Comment).WithOne(e => e.Post).HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
+        builder.HasMany(e => e.PostHistoryVersion).WithOne(e => e.Post).HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
+        builder.HasMany(e => e.Seminar).WithMany(s => s.Post).UsingEntity(builder => builder.ToTable("SeminarPost"));
+        builder.HasMany(e => e.PostMergeRequests).WithOne(s => s.Post).HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
+        builder.HasMany(e => e.PostVisitRecords).WithOne().HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
+        builder.HasMany(e => e.PostVisitRecordStats).WithOne().HasForeignKey(r => r.PostId).OnDelete(DeleteBehavior.Cascade);
+
+        builder.HasIndex(a => a.CategoryId);
+        builder.HasIndex(a => a.ModifyDate).IsDescending();
+        builder.HasIndex(a => a.AverageViewCount).IsDescending();
+        builder.HasIndex(a => a.TotalViewCount).IsDescending();
+    }
 }
 
 /// <summary>
@@ -241,23 +263,23 @@ public class Post : BaseEntity
 /// </summary>
 public enum RegionLimitMode
 {
-	[Description("不限")]
-	All,
+    [Description("不限")]
+    All,
 
-	[Description("指定地区可见:{0}")]
-	AllowRegion,
+    [Description("指定地区可见:{0}")]
+    AllowRegion,
 
-	[Description("指定地区不可见:{0}")]
-	ForbidRegion,
+    [Description("指定地区不可见:{0}")]
+    ForbidRegion,
 
-	[Description("可见地区:{0},排除地区:{1}")]
-	AllowRegionExceptForbidRegion,
+    [Description("可见地区:{0},排除地区:{1}")]
+    AllowRegionExceptForbidRegion,
 
-	[Description("不可见地区:{0},排除地区:{1}")]
-	ForbidRegionExceptAllowRegion,
+    [Description("不可见地区:{0},排除地区:{1}")]
+    ForbidRegionExceptAllowRegion,
 
-	[Description("仅搜索引擎可见")]
-	OnlyForSearchEngine,
+    [Description("仅搜索引擎可见")]
+    OnlyForSearchEngine,
 }
 
 /// <summary>
@@ -265,30 +287,30 @@ public enum RegionLimitMode
 /// </summary>
 public enum ProtectContentMode
 {
-	None,
-
-	/// <summary>
-	/// 评论可见
-	/// </summary>
-	CommentVisiable,
-
-	/// <summary>
-	/// 地区可见
-	/// </summary>
-	Regions,
-
-	/// <summary>
-	/// 授权可见
-	/// </summary>
-	AuthorizeVisiable,
-
-	/// <summary>
-	/// 密码可见
-	/// </summary>
-	Password,
-
-	/// <summary>
-	/// 仅搜索引擎可见
-	/// </summary>
-	OnlyForSearchEngine,
-}
+    None,
+
+    /// <summary>
+    /// 评论可见
+    /// </summary>
+    CommentVisiable,
+
+    /// <summary>
+    /// 地区可见
+    /// </summary>
+    Regions,
+
+    /// <summary>
+    /// 授权可见
+    /// </summary>
+    AuthorizeVisiable,
+
+    /// <summary>
+    /// 密码可见
+    /// </summary>
+    Password,
+
+    /// <summary>
+    /// 仅搜索引擎可见
+    /// </summary>
+    OnlyForSearchEngine,
+}

+ 46 - 46
src/Masuit.MyBlogs.Core/Models/ViewModel/LeaveMessageViewModel.cs

@@ -5,60 +5,60 @@ namespace Masuit.MyBlogs.Core.Models.ViewModel;
 /// <summary>
 /// 留言板视图模型
 /// </summary>
-public class LeaveMessageViewModel : BaseEntity, ITreeChildren<LeaveMessageViewModel>
+public class LeaveMessageViewModel : BaseEntity, ITreeEntity<LeaveMessageViewModel, int>
 {
-	/// <summary>
-	/// 昵称
-	/// </summary>
-	public string NickName { get; set; }
+    /// <summary>
+    /// 昵称
+    /// </summary>
+    public string NickName { get; set; }
 
-	/// <summary>
-	/// 内容
-	/// </summary>
-	public string Content { get; set; }
+    /// <summary>
+    /// 内容
+    /// </summary>
+    public string Content { get; set; }
 
-	/// <summary>
-	/// 发表时间
-	/// </summary>
-	public string PostDate { get; set; }
+    /// <summary>
+    /// 发表时间
+    /// </summary>
+    public string PostDate { get; set; }
 
-	/// <summary>
-	/// 邮箱
-	/// </summary>
-	public string Email { get; set; }
+    /// <summary>
+    /// 邮箱
+    /// </summary>
+    public string Email { get; set; }
 
-	/// <summary>
-	/// 父级ID
-	/// </summary>
-	public int ParentId { get; set; }
+    /// <summary>
+    /// 父级ID
+    /// </summary>
+    public int? ParentId { get; set; }
 
-	/// <summary>
-	/// 浏览器版本
-	/// </summary>
-	public string Browser { get; set; }
+    /// <summary>
+    /// 浏览器版本
+    /// </summary>
+    public string Browser { get; set; }
 
-	/// <summary>
-	/// 操作系统版本
-	/// </summary>
-	public string OperatingSystem { get; set; }
+    /// <summary>
+    /// 操作系统版本
+    /// </summary>
+    public string OperatingSystem { get; set; }
 
-	/// <summary>
-	/// 是否是博主
-	/// </summary>
-	public bool IsMaster { get; set; }
+    /// <summary>
+    /// 是否是博主
+    /// </summary>
+    public bool IsMaster { get; set; }
 
-	/// <summary>
-	/// 提交人IP地址
-	/// </summary>
-	public string IP { get; set; }
+    /// <summary>
+    /// 提交人IP地址
+    /// </summary>
+    public string IP { get; set; }
 
-	/// <summary>
-	/// 地理信息
-	/// </summary>
-	public string Location { get; set; }
+    /// <summary>
+    /// 地理信息
+    /// </summary>
+    public string Location { get; set; }
 
-	/// <summary>
-	/// 子级
-	/// </summary>
-	public ICollection<LeaveMessageViewModel> Children { get; set; }
-}
+    /// <summary>
+    /// 子级
+    /// </summary>
+    public ICollection<LeaveMessageViewModel> Children { get; set; }
+}

+ 1 - 1
src/Masuit.MyBlogs.Core/Properties/PublishProfiles/FolderProfile.pubxml

@@ -12,7 +12,7 @@
     <SiteUrlToLaunchAfterPublish />
     <LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
     <ExcludeApp_Data>false</ExcludeApp_Data>
-    <TargetFramework>net7.0</TargetFramework>
+    <TargetFramework>net8.0</TargetFramework>
     <ProjectGuid>2f8270e4-5e57-4ce4-ab5f-8008f9fc8c7c</ProjectGuid>
     <SelfContained>false</SelfContained>
     <publishUrl>bin\Release\publish\</publishUrl>

+ 129 - 129
src/Masuit.MyBlogs.Core/Startup.cs

@@ -23,7 +23,6 @@ using SixLabors.ImageSharp.Web.DependencyInjection;
 using System.Text.RegularExpressions;
 using Masuit.Tools.AspNetCore.ModelBinder;
 using EFCoreSecondLevelCacheInterceptor;
-using Microsoft.Extensions.DependencyInjection;
 
 namespace Masuit.MyBlogs.Core;
 
@@ -32,143 +31,144 @@ namespace Masuit.MyBlogs.Core;
 /// </summary>
 public class Startup
 {
-	/// <summary>
-	/// 配置中心
-	/// </summary>
-	public IConfiguration Configuration { get; set; }
+    /// <summary>
+    /// 配置中心
+    /// </summary>
+    public IConfiguration Configuration { get; set; }
 
-	private readonly IWebHostEnvironment _env;
+    private readonly IWebHostEnvironment _env;
 
-	/// <summary>
-	/// asp.net core核心配置
-	/// </summary>
-	/// <param name="configuration"></param>
-	public Startup(IConfiguration configuration, IWebHostEnvironment env)
-	{
-		_env = env;
+    /// <summary>
+    /// asp.net core核心配置
+    /// </summary>
+    /// <param name="configuration"></param>
+    /// <param name="env"></param>
+    public Startup(IConfiguration configuration, IWebHostEnvironment env)
+    {
+        _env = env;
 
-		void BindConfig()
-		{
-			Configuration = configuration;
-			AppConfig.ConnString = configuration["Database:" + nameof(AppConfig.ConnString)];
-			AppConfig.BaiduAK = configuration[nameof(AppConfig.BaiduAK)];
-			AppConfig.Redis = configuration[nameof(AppConfig.Redis)];
-			AppConfig.TrueClientIPHeader = configuration[nameof(AppConfig.TrueClientIPHeader)] ?? "CF-Connecting-IP";
-			AppConfig.EnableIPDirect = bool.Parse(configuration[nameof(AppConfig.EnableIPDirect)] ?? "false");
-			configuration.Bind("Imgbed:Gitlabs", AppConfig.GitlabConfigs);
-			configuration.AddToMasuitTools();
-		}
+        void BindConfig()
+        {
+            Configuration = configuration;
+            AppConfig.ConnString = configuration["Database:" + nameof(AppConfig.ConnString)];
+            AppConfig.BaiduAK = configuration[nameof(AppConfig.BaiduAK)];
+            AppConfig.Redis = configuration[nameof(AppConfig.Redis)];
+            AppConfig.TrueClientIPHeader = configuration[nameof(AppConfig.TrueClientIPHeader)] ?? "CF-Connecting-IP";
+            AppConfig.EnableIPDirect = bool.Parse(configuration[nameof(AppConfig.EnableIPDirect)] ?? "false");
+            configuration.Bind("Imgbed:Gitlabs", AppConfig.GitlabConfigs);
+            configuration.AddToMasuitTools();
+        }
 
-		ChangeToken.OnChange(configuration.GetReloadToken, BindConfig);
-		BindConfig();
-	}
+        ChangeToken.OnChange(configuration.GetReloadToken, BindConfig);
+        BindConfig();
+    }
 
-	/// <summary>
-	/// ConfigureServices
-	/// </summary>
-	/// <param name="services"></param>
-	/// <returns></returns>
-	public void ConfigureServices(IServiceCollection services)
-	{
-		services.AddEFSecondLevelCache(options => options.UseCacheManagerCoreProvider(CacheExpirationMode.Absolute, TimeSpan.FromMinutes(5)).DisableLogging(true).UseCacheKeyPrefix("EFCache:"));
-		services.AddDbContext<DataContext>((serviceProvider, opt) => opt.UseNpgsql(AppConfig.ConnString, builder => builder.EnableRetryOnFailure(10)).EnableSensitiveDataLogging().AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>())); //配置数据库
-		services.AddDbContext<LoggerDbContext>(opt => opt.UseNpgsql(AppConfig.ConnString)); //配置数据库
-		services.ConfigureOptions();
-		services.AddHttpsRedirection(options =>
-		{
-			options.RedirectStatusCode = StatusCodes.Status301MovedPermanently;
-		});
-		services.AddSession().AddAntiforgery(); //注入Session
-		services.AddResponseCache().AddCacheConfig();
-		services.AddHangfireServer().AddHangfire((serviceProvider, configuration) =>
-		{
-			configuration.UseActivator(new HangfireActivator(serviceProvider));
-			configuration.UseFilter(new AutomaticRetryAttribute());
-			configuration.UseMemoryStorage();
-		}); //配置hangfire
+    /// <summary>
+    /// ConfigureServices
+    /// </summary>
+    /// <param name="services"></param>
+    /// <returns></returns>
+    public void ConfigureServices(IServiceCollection services)
+    {
+        services.AddEFSecondLevelCache(options => options.UseCacheManagerCoreProvider(CacheExpirationMode.Absolute, TimeSpan.FromMinutes(5)).DisableLogging(true).UseCacheKeyPrefix("EFCache:"));
+        services.AddDbContext<DataContext>((serviceProvider, opt) => opt.UseNpgsql(AppConfig.ConnString, builder => builder.EnableRetryOnFailure(10)).EnableSensitiveDataLogging().AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>())); //配置数据库
+        services.AddDbContext<LoggerDbContext>(opt => opt.UseNpgsql(AppConfig.ConnString)); //配置数据库
+        services.ConfigureOptions();
+        services.AddHttpsRedirection(options =>
+        {
+            options.RedirectStatusCode = StatusCodes.Status301MovedPermanently;
+        });
+        services.AddSession().AddAntiforgery(); //注入Session
+        services.AddResponseCache().AddCacheConfig();
+        services.AddHangfireServer().AddHangfire((serviceProvider, configuration) =>
+        {
+            configuration.UseActivator(new HangfireActivator(serviceProvider));
+            configuration.UseFilter(new AutomaticRetryAttribute());
+            configuration.UseMemoryStorage();
+        }); //配置hangfire
 
-		services.AddSevenZipCompressor().AddResumeFileResult().AddSearchEngine<DataContext>(new LuceneIndexerOptions()
-		{
-			Path = "lucene"
-		}); // 配置7z和断点续传和Redis和Lucene搜索引擎
+        services.AddSevenZipCompressor().AddResumeFileResult().AddSearchEngine<DataContext>(new LuceneIndexerOptions()
+        {
+            Path = "lucene"
+        }); // 配置7z和断点续传和Redis和Lucene搜索引擎
 
-		services.SetupHttpClients(Configuration);
-		services.AddMailSender(Configuration).AddFirewallReporter(Configuration).AddRequestLogger(Configuration).AddPerfCounterManager(Configuration);
-		services.AddBundling().UseDefaults(_env).UseNUglify().EnableMinification().EnableChangeDetection().EnableCacheHeader(TimeSpan.FromHours(1));
-		services.AddSingleton<IRedisClient>(new RedisClient(AppConfig.Redis)
-		{
-			Serialize = JsonConvert.SerializeObject,
-			Deserialize = JsonConvert.DeserializeObject
-		});
-		services.SetupMiniProfile();
-		services.AddSingleton<IMimeMapper, MimeMapper>(p => new MimeMapper());
-		services.AddOneDrive();
-		services.AutoRegisterServices();
-		services.AddRazorPages();
-		services.AddServerSideBlazor();
-		services.AddMapper().AddMyMvc().AddHealthChecks();
-		services.SetupImageSharp();
-		services.AddHttpContextAccessor();
-	}
+        services.SetupHttpClients(Configuration);
+        services.AddMailSender(Configuration).AddFirewallReporter(Configuration).AddRequestLogger(Configuration).AddPerfCounterManager(Configuration);
+        services.AddBundling().UseDefaults(_env).UseNUglify().EnableMinification().EnableChangeDetection().EnableCacheHeader(TimeSpan.FromHours(1));
+        services.AddSingleton<IRedisClient>(new RedisClient(AppConfig.Redis)
+        {
+            Serialize = JsonConvert.SerializeObject,
+            Deserialize = JsonConvert.DeserializeObject
+        });
+        services.SetupMiniProfile();
+        services.AddSingleton<IMimeMapper, MimeMapper>(_ => new MimeMapper());
+        services.AddOneDrive();
+        services.AutoRegisterServices();
+        services.AddRazorPages();
+        services.AddServerSideBlazor();
+        services.AddMapper().AddMyMvc().AddHealthChecks();
+        services.SetupImageSharp();
+        services.AddHttpContextAccessor();
+    }
 
-	public void ConfigureContainer(ContainerBuilder builder)
-	{
-		builder.RegisterModule(new AutofacModule());
-	}
+    public void ConfigureContainer(ContainerBuilder builder)
+    {
+        builder.RegisterModule(new AutofacModule());
+    }
 
-	/// <summary>
-	/// Configure
-	/// </summary>
-	/// <param name="app"></param>
-	/// <param name="env"></param>
-	/// <param name="hangfire"></param>
-	/// <param name="luceneIndexerOptions"></param>
-	/// <param name="maindb"></param>
-	/// <param name="loggerdb"></param>
-	public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHangfireBackJob hangfire, LuceneIndexerOptions luceneIndexerOptions, DataContext maindb, LoggerDbContext loggerdb)
-	{
-		maindb.Database.EnsureCreated();
-		loggerdb.Database.EnsureCreated();
-		app.InitSettings();
-		app.UseDisposeScope();
-		app.UseLuceneSearch(env, hangfire, luceneIndexerOptions);
-		app.UseForwardedHeaders().UseCertificateForwarding(); // X-Forwarded-For
-		if (env.IsDevelopment())
-		{
-			app.UseDeveloperExceptionPage();
-		}
-		else
-		{
-			app.UseExceptionHandler("/ServiceUnavailable");
-		}
+    /// <summary>
+    /// Configure
+    /// </summary>
+    /// <param name="app"></param>
+    /// <param name="env"></param>
+    /// <param name="hangfire"></param>
+    /// <param name="luceneIndexerOptions"></param>
+    /// <param name="maindb"></param>
+    /// <param name="loggerdb"></param>
+    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHangfireBackJob hangfire, LuceneIndexerOptions luceneIndexerOptions, DataContext maindb, LoggerDbContext loggerdb)
+    {
+        maindb.Database.EnsureCreated();
+        loggerdb.Database.EnsureCreated();
+        app.InitSettings();
+        app.UseDisposeScope();
+        app.UseLuceneSearch(env, hangfire, luceneIndexerOptions);
+        app.UseForwardedHeaders().UseCertificateForwarding(); // X-Forwarded-For
+        if (env.IsDevelopment())
+        {
+            app.UseDeveloperExceptionPage();
+        }
+        else
+        {
+            app.UseExceptionHandler("/ServiceUnavailable");
+        }
 
-		app.UseBundles();
-		app.SetupHttpsRedirection(Configuration);
-		app.UseDefaultFiles().UseWhen(c => Regex.IsMatch(c.Request.Path.Value + "", @"(\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.tiff|\.pbm)$", RegexOptions.IgnoreCase), builder => builder.UseImageSharp()).UseStaticFiles();
-		app.UseSession().UseCookiePolicy(); //注入Session
-		app.UseWhen(c => c.Session.Get<UserInfoDto>(SessionKey.UserInfo)?.IsAdmin == true, builder =>
-		{
-			builder.UseMiniProfiler();
-			builder.UseCLRStatsDashboard();
-		});
-		app.UseWhen(c => !c.Request.Path.StartsWithSegments("/_blazor"), builder => builder.UseMiddleware<RequestInterceptMiddleware>()); //启用网站请求拦截
-		app.SetupHangfire();
-		app.UseResponseCaching().UseResponseCompression(); //启动Response缓存
-		app.UseMiddleware<TranslateMiddleware>();
-		app.UseRouting().UseBodyOrDefaultModelBinder().UseEndpoints(endpoints =>
-		{
-			endpoints.MapBlazorHub(options =>
-			{
-				options.ApplicationMaxBufferSize = 4194304;
-				options.LongPolling.PollTimeout = TimeSpan.FromSeconds(10);
-				options.TransportMaxBufferSize = 8388608;
-			});
-			endpoints.MapHealthChecks("/health");
-			endpoints.MapControllers(); // 属性路由
-			endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); // 默认路由
-			endpoints.MapFallbackToController("Index", "Error");
-		});
+        app.UseBundles();
+        app.SetupHttpsRedirection(Configuration);
+        app.UseDefaultFiles().UseWhen(c => Regex.IsMatch(c.Request.Path.Value + "", @"(\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.tiff|\.pbm)$", RegexOptions.IgnoreCase), builder => builder.UseImageSharp()).UseStaticFiles();
+        app.UseSession().UseCookiePolicy(); //注入Session
+        app.UseWhen(c => c.Session.Get<UserInfoDto>(SessionKey.UserInfo)?.IsAdmin == true, builder =>
+        {
+            builder.UseMiniProfiler();
+            builder.UseCLRStatsDashboard();
+        });
+        app.UseWhen(c => !c.Request.Path.StartsWithSegments("/_blazor"), builder => builder.UseMiddleware<RequestInterceptMiddleware>()); //启用网站请求拦截
+        app.SetupHangfire();
+        app.UseResponseCaching().UseResponseCompression(); //启动Response缓存
+        app.UseMiddleware<TranslateMiddleware>();
+        app.UseRouting().UseBodyOrDefaultModelBinder().UseEndpoints(endpoints =>
+        {
+            endpoints.MapBlazorHub(options =>
+            {
+                options.ApplicationMaxBufferSize = 4194304;
+                options.LongPolling.PollTimeout = TimeSpan.FromSeconds(10);
+                options.TransportMaxBufferSize = 8388608;
+            });
+            endpoints.MapHealthChecks("/health");
+            endpoints.MapControllers(); // 属性路由
+            endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); // 默认路由
+            endpoints.MapFallbackToController("Index", "Error");
+        });
 
-		Console.WriteLine("网站启动完成");
-	}
+        Console.WriteLine("网站启动完成");
+    }
 }

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

@@ -6,12 +6,18 @@
 @using Masuit.MyBlogs.Core.Infrastructure.Services.Interface
 @using Masuit.MyBlogs.Core.Models.Enum
 @using Masuit.Tools.Models
-@using Dispose.Scope
 @using EFCoreSecondLevelCacheInterceptor
 @model Masuit.MyBlogs.Core.Models.ViewModel.HomePageViewModel
 @inject ICategoryService CategoryService
 @{
-    Category cat = ViewBag.Category;
+    var all = CategoryService.GetQuery(c => c.Status==Status.Available).Select(c => new Category() {
+        Id = c.Id,
+        ParentId = c.ParentId,
+        Name = c.Name,
+        Description = c.Description
+    }).Cacheable().ToList();
+    var alllist = all.ToTree();
+    var cat = all.FirstOrDefault(c => c.Id == ViewBag.Category.Id);
     ViewBag.Title = "分类_" + cat.Path();
     Layout = "~/Views/Shared/_Layout.cshtml";
     var level = cat.Level();
@@ -20,19 +26,18 @@
     var parentId = cat.ParentId;
     switch (level) {
         case 1:
-            children2.AddRange(cat.Children.Where(c => c.Status == Status.Available).OrderBy(c => c.Id).ToPooledListScope());
+            children2.AddRange(cat.Children.OrderBy(c => c.Id));
             break;
         case 2:
-            children2.AddRange(CategoryService.GetQuery(c => c.Status == Status.Available && c.ParentId == parentId, c => c.Name).Cacheable().ToPooledListScope());
-            children3.AddRange(cat.Children.Where(c => c.Status == Status.Available).OrderBy(c => c.Id).ToPooledListScope());
+            children2.AddRange(all.Where(c => c.ParentId == parentId).OrderBy(c=> c.Name));
+            children3.AddRange(cat.Children.OrderBy(c => c.Name));
             break;
         case 3:
             var topid = cat.Parent.ParentId;
-            children2.AddRange(CategoryService.GetQuery(c => c.Status == Status.Available && c.ParentId == topid, c => c.Name).Cacheable().ToPooledListScope());
-            children3.AddRange(CategoryService.GetQuery(c => c.Status == Status.Available && c.ParentId == parentId, c => c.Name).Cacheable().ToPooledListScope());
+            children2.AddRange(all.Where(c => c.ParentId == topid).OrderBy(c => c.Name));
+            children3.AddRange(all.Where(c => c.ParentId == parentId).OrderBy(c => c.Name));
             break;
     }
-    var alllist = CategoryService.GetQuery(c => c.Status == Status.Available && c.ParentId == null, c => c.Name).Select(c => new{c.Id,c.Name}).Cacheable().ToPooledListScope();
 }
 <style>
     .bg-title {

+ 15 - 15
src/Masuit.MyBlogs.Core/Views/Msg/Index.cshtml

@@ -129,35 +129,35 @@
 </div>
 <script src="~/Scripts/global/leavemsg.js"></script>
 <script>
-	window.onload = function() {
+    window.onload = function() {
         loading();
-		$('#pageToolbar').Paging({ //异步加载评论
-			pagesize: 10,
-			count: @ViewBag.TotalCount,
-			toolbar: true,
-			callback: function(page, size, count) {
-				$.get("/msg/getmsgs?page="+page+"&size="+size, function (data){
+        $('#pageToolbar').Paging({ //异步加载评论
+            pagesize: 10,
+            count: @ViewBag.TotalCount,
+            toolbar: true,
+            callback: function(page, size, count) {
+                $.get("/msg/getmsgs?page="+page+"&size="+size, function (data){
                     data = data.Data;
                     if (data) {
                         document.querySelector(".media-list").innerHTML = loadParentMsgs(data);
                         bindReplyBtn();
-					}
-				});
-			}
-		});
+                    }
+                });
+            }
+        });
         loadingDone();
-	};
+    };
 
-	/**
+    /**
    * 获取留言
    */
     function getmsgs() {
         $.get("/msg/getmsgs?page=1&size=10&cid=@cid", function(data){
             data = data.Data;
-			if (data) {
+            if (data) {
                 document.querySelector(".media-list").innerHTML = loadParentMsgs(data);
                 bindReplyBtn();
-			}
+            }
         });
     }
 </script>