Browse Source

5s盾支持按地区开启

懒得勤快 2 years ago
parent
commit
30917c4fce

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

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

+ 173 - 174
src/Masuit.MyBlogs.Core/Extensions/MiddlewareExtension.cs

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

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

@@ -15,7 +15,7 @@ public class DataContext : DbContext
     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
     {
         optionsBuilder.UseExceptionProcessor();
-        optionsBuilder.EnableDetailedErrors().UseLazyLoadingProxies().UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll).ConfigureWarnings(builder => builder.Ignore(CoreEventId.DetachedLazyLoadingWarning));
+        optionsBuilder.EnableDetailedErrors().UseLazyLoadingProxies().UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll).ConfigureWarnings(builder => builder.Ignore(CoreEventId.DetachedLazyLoadingWarning, CoreEventId.LazyLoadOnDisposedContextWarning));
     }
 
     protected override void OnModelCreating(ModelBuilder modelBuilder)

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

@@ -130,17 +130,19 @@ public sealed partial class AdvertisementService : BaseService<Advertisement>, I
 			}
 
 			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))
+			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)))
-				.Where(a => array.Contains(a.Id)).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();
+				.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))
+				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))
-				.Where(a => array.Contains(a.Id)).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));
+				.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();
@@ -148,4 +150,4 @@ public sealed partial class AdvertisementService : BaseService<Advertisement>, I
 			return list;
 		});
 	}
-}
+}

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

@@ -51,7 +51,7 @@
         <PackageReference Include="Dispose.Scope.AspNetCore" Version="0.0.3" />
         <PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="6.0.3" />
         <PackageReference Include="FreeRedis" Version="1.1.5" />
-        <PackageReference Include="Hangfire" Version="1.8.3" />
+        <PackageReference Include="Hangfire" Version="1.8.4" />
         <PackageReference Include="Hangfire.MemoryStorage" Version="1.8.0" />
         <PackageReference Include="htmldiff.net" Version="1.4.1" />
         <PackageReference Include="Karambolo.AspNetCore.Bundling.NUglify" Version="3.6.1" />

+ 128 - 127
src/Masuit.MyBlogs.Core/Startup.cs

@@ -21,6 +21,7 @@ using Microsoft.Extensions.Primitives;
 using Newtonsoft.Json;
 using SixLabors.ImageSharp.Web.DependencyInjection;
 using System.Text.RegularExpressions;
+using Masuit.Tools.AspNetCore.ModelBinder;
 
 namespace Masuit.MyBlogs.Core;
 
@@ -29,142 +30,142 @@ 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>
+	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.AddDbContext<DataContext>(opt => opt.UseNpgsql(AppConfig.ConnString, builder => builder.EnableRetryOnFailure(10)).EnableSensitiveDataLogging()); //配置数据库
-        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.AddDbContext<DataContext>(opt => opt.UseNpgsql(AppConfig.ConnString, builder => builder.EnableRetryOnFailure(10)).EnableSensitiveDataLogging()); //配置数据库
+		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>(p => 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().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("网站启动完成");
+	}
 }

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

@@ -8,390 +8,379 @@
 @model Masuit.MyBlogs.Core.Models.Entity.Post
 
 @{
-	ViewBag.Title = Model.Title;
-	Layout = "~/Views/Shared/_Layout.cshtml";
-	string[] colors = { "success", "info", "primary", "warning", "danger", "default", "primary" };
-	var cid = Context.Request.RouteValues["cid"]??Context.Request.Query["cid"];
-	string hidden = string.IsNullOrEmpty(Context.Request.Cookies["ValidateKey"]) ? "" : "hidden";
-	AdvertisementDto ad = ViewBag.Ads;
+    ViewBag.Title = Model.Title;
+    Layout = "~/Views/Shared/_Layout.cshtml";
+    string[] colors = { "success", "info", "primary", "warning", "danger", "default", "primary" };
+    var cid = Context.Request.RouteValues["cid"]??Context.Request.Query["cid"];
+    string hidden = string.IsNullOrEmpty(Context.Request.Cookies["ValidateKey"]) ? "" : "hidden";
+    AdvertisementDto ad = ViewBag.Ads;
 }
 
 <environment names="Development">
-	<link href="~/Assets/jquery.tocify/jquery.tocify.css" rel="stylesheet" />
-	<link href="~/Assets/UEditor/third-party/SyntaxHighlighter/styles/shCore.css" rel="stylesheet" />
-	<script src="/Assets/UEditor/third-party/SyntaxHighlighter/scripts/shCore.js"></script>
-	<script src="/Assets/UEditor/third-party/SyntaxHighlighter/scripts/bundle.min.js"></script>
-	<script src="/Assets/jquery.tocify/jquery.tocify.js"></script>
-	<script src="/Scripts/global/article.js"></script>
+    <link href="~/Assets/jquery.tocify/jquery.tocify.css" rel="stylesheet" />
+    <link href="~/Assets/UEditor/third-party/SyntaxHighlighter/styles/shCore.css" rel="stylesheet" />
+    <script src="/Assets/UEditor/third-party/SyntaxHighlighter/scripts/shCore.js"></script>
+    <script src="/Assets/UEditor/third-party/SyntaxHighlighter/scripts/bundle.min.js"></script>
+    <script src="/Assets/jquery.tocify/jquery.tocify.js"></script>
+    <script src="/Scripts/global/article.js"></script>
 </environment>
 <environment names="Stage,Production">
-	@await Styles.RenderAsync("/bundles/article.css")
-	@await Scripts.RenderAsync("/bundles/article.js")
+    @await Styles.RenderAsync("/bundles/article.css")
+    @await Scripts.RenderAsync("/bundles/article.js")
 </environment>
 <div class="container">
-	<ol class="cd-breadcrumb triangle">
-		<li><a asp-controller="Home" asp-action="Index">首页</a></li>
-		<li><a asp-controller="Home" asp-action="Post">文章列表</a></li>
-		@if(Model.Category.ParentId>0) {
-		    if(Model.Category.Parent.ParentId>0) {
-			    <li><a asp-controller="Home" asp-action="Category" asp-route-id="@Model.Category.Parent.ParentId">@Model.Category.Parent.Parent.Name</a></li>
-		    }
-			<li><a asp-controller="Home" asp-action="Category" asp-route-id="@Model.Category.ParentId">@Model.Category.Parent.Name</a></li>
-		}
-		<li><a asp-controller="Home" asp-action="Category" asp-route-id="@Model.CategoryId">@Model.Category.Name</a></li>
-		<li class="current"><em>@ViewBag.Title</em></li>
-	</ol>
-	<div class="wrapper-content article">
-		<div class="ibox">
-			<div class="ibox-content animated fadeIn">
-				<main>
-					<section class="article-content">
-						<header class="page-header">
-							<div class="text-center">
-								<a>
-									<h2 class="padding-bot10">
-										@Html.Raw(Model.Title)
-									</h2>
-								</a>
-							</div>
-							<div class="row">
-								<div class="col-sm-8">
-									<div class="padding-bot10">
-										<span class="label label-info">
-											<a asp-controller="Home" asp-action="Author" asp-route-author="@Model.Author">@Model.Author</a>
-										</span>发表于<a asp-controller="Home" asp-action="Archieve" asp-route-yyyy="@Model.PostDate.Year" asp-route-mm="@Model.PostDate.Month" asp-route-dd="@Model.PostDate.Day" asp-route-mode="@nameof(Model.PostDate)"><time class="text-info">@Model.PostDate.ToString("yyyy-MM-dd")</time></a> |
-										<span class="label label-info">
-											<a asp-controller="Home" asp-action="Author" asp-route-author="@Model.Modifier">@Model.Modifier</a>
-										</span>最后修改于<a asp-controller="Home" asp-action="Archieve" asp-route-yyyy="@Model.ModifyDate.Year" asp-route-mm="@Model.ModifyDate.Month" asp-route-dd="@Model.ModifyDate.Day" asp-route-mode="@nameof(Model.ModifyDate)"><time class="text-success">@Model.ModifyDate.ToString("yyyy-MM-dd")</time></a>
-									</div>
-								</div>
-								@{
-									if (!string.IsNullOrEmpty(Model.Label))
-									{
-										<div class="pull-right margin-right20">
-											<div>
-												@foreach (string s in Model.Label.Split(new []{',', ','},StringSplitOptions.RemoveEmptyEntries))
-												{
-													<a asp-controller="Home" asp-action="Tag" asp-route-tag="@s">
-														<span class="label label-@colors[new Random().Next() % colors.Length]">@s</span>
-													</a>
-												}
-											</div>
-										</div>
-									}
-								}
-							</div>
-							<div class="row">
-								<div class="col-md-12 line-height24">
-									分类:<i class="icon-map-pin"></i>
-									@{
-										await Html.RenderPartialAsync("CategoryPath", Model.Category);
-									}
-									| 评论总数:<span class="text-danger">@ViewBag.CommentsCount</span>条 | @(await Html.RenderComponentAsync<PostOnline>(RenderMode.ServerPrerendered, new{ Model.Id,IP=Context.Connection.RemoteIpAddress.ToString()}))
-									@if (Model.Seminar.Any())
-									{
-										<span> | 所属专题:</span>
-										var seminars = Model.Seminar;
-										foreach (var s in seminars)
-										{
-											<a asp-controller="Seminar" asp-action="Index" asp-route-id="@s.Id" class="label label-info">@s.Title</a>
-											<text> </text>
-										}
-									}
-									@if (!Model.Locked) {
-										<div class="pull-right">
-											<a asp-controller="Post" asp-action="PushMerge" asp-route-id="@Model.Id" class="btn btn-danger">我要参与编辑</a>
-										</div>
-									}
-								</div>
-							</div>
-						</header>
-						<article class="article" id="article">
-							@if (DateTime.Now - Model.ModifyDate > TimeSpan.FromDays(365))
-							{
-								<p class="text-focus text-center">该文章已经超过1年未更新,可能无法为您提供及时准确的资讯,请根据当下实际情况,酌情参考本文内容。</p>
-							}
-							@if(!Model.DisableCopy||Context.Request.IsRobot()) {
-								@Html.Raw(await Model.Content.ReplaceImgAttribute(Model.Title))
-							}
-							else {
-								@Html.Raw(Regex.Replace(await Model.Content.ReplaceImgAttribute(Model.Title), @"[\u4e00-\u9fa5]", m => HtmlEncoder.Default.Encode(m.Value)))
-							}
-							@{
-								await Html.RenderPartialAsync("/Views/Post/ProtectContent.cshtml",Model);
-							}
-						</article>
-					</section>
-				</main>
-				<hr class="margin-top10 marginbot10" />
-				@if (ad != null)
-				{
-					await Html.RenderPartialAsync("_ArticleListAdvertisement", ad);
-				}
-				<section class="wow row padding-top40 padding-bot20 animated fadeIn">
-					<div class="col-xs-6">
-						<div class="btn-group">
-							<button type="button" id="voteup" class="btn btn-success btn-lg">
-								<i class="icon-thumbsup"></i><span>@Model.VoteUpCount</span>
-							</button>
-							<button type="button" id="votedown" class="btn btn-danger btn-lg">
-								<i class="icon-thumbsdown"></i><span>@Model.VoteDownCount</span>
-							</button>
-						</div>
-					</div>
-					@if (Model.Rss&&CommonHelper.SystemSettings.GetOrAdd("EnableRss", "true") == "true") {
-						<div class="col-xs-6 text-right">
-							<div class="btn-group">
-								<a class="btn btn-danger btn-lg" asp-controller="Subscribe" asp-action="PostRss" asp-route-id="@Model.Id" target="_blank">
-									<i class="icon-rss4"></i><span>订阅本文更新</span>
-								</a>
-							</div>
-						</div>
-					}
-					<div class="col-xs-12">
-						<p class="text-red">如果你喜欢这篇文章,欢迎点赞、评论、分享本文!</p>
-					</div>
-				</section>
-				<section class="wow fadeInUp row size16">
-					<div class="col-xs-6">
-						上一篇:
-						@{
-							PostModelBase prev = ViewBag.Prev;
-							if (prev != null)
-							{
-								<a asp-controller="Post" asp-action="Details" asp-route-id="@prev.Id">@prev.Title</a>
-							}
-							else
-							{
-								<a>没有了</a>
-							}
-						}
-					</div>
-					<div class="col-xs-6 text-right">
-						下一篇:
-						@{
-							PostModelBase next = ViewBag.Next;
-							if (next != null)
-							{
-								<a asp-controller="Post" asp-action="Details" asp-route-id="@next.Id">@next.Title</a>
-							}
-							else
-							{
-								<a>没有了</a>
-							}
-						}
-					</div>
-				</section>
-				@if (ViewBag.HistoryCount>0)
-				{
-					<section class="wow margintop20 animated fadeIn">
-						<h3>文章历史版本:</h3>
-						<p>
-							修改次数:@ViewBag.HistoryCount 次
-							<a asp-controller="Post" asp-action="History" asp-route-id="@Model.Id">查看历史版本</a>
-						</p>
-					</section>
-				}
-				@{
-					var posts = (Dictionary<int,string>)ViewBag.Related;
-					if (posts.Count>1)
-					{
-						<section class="wow margintop20 animated fadeIn">
-							<h3>相关推荐:</h3>
-							<table class="table table-condensed size16">
-								@for (int i = 1; i < posts.Count; i += 2)
-								{
-									<tr>
-										<td style="width: 50%"><a asp-controller="Post" asp-action="Details" asp-route-id="@posts.ElementAt(i - 1).Key">@posts.ElementAt(i - 1).Value</a></td>
-										<td><a asp-controller="Post" asp-action="Details" asp-route-id="@posts.ElementAt(i).Key">@posts.ElementAt(i).Value</a></td>
-									</tr>
-								}
-							</table>
-						</section>
-					}
-				}
-				<section class="wow padding-bot20 animated fadeIn">
-					<h3>版权声明:</h3>
-					<p class="text-red size16">
-						@if (Model.DisableCopy)
-						{
-							<text>🈲⚠本文为作者原创,仅用于本站访客学习、研究和交流目的,未经授权禁止转载。️⚠🈲 </text>
-						}
-						else
-						{
-							<text>本文仅用于学习、研究和交流目的,欢迎非商业性质转载。本文链接:<a asp-controller="Post" asp-action="Details" asp-route-id="@Model.Id">@HttpUtility.UrlDecode(Context.Request.Scheme + "://" + Context.Request.Host + Context.Request.Path)</a>。</text>
-							@Html.Raw(CommonHelper.SystemSettings["Disclaimer"])
-						}
-					</p>
-				</section>
-				<section class="wow row animated fadeIn">
-					<div class="col-lg-12">
-						<h3>评论区:</h3>
-						@if (!Model.DisableComment)
-						{
-							<form class="form-horizontal animated pulse" id="comment" method="post">
-								@Html.AntiForgeryToken()
-								<input type="hidden" name="PostId" value="@Model.Id" />
-								<input type="hidden" id="OperatingSystem" name="OperatingSystem" />
-								<input type="hidden" name="Browser" id="Browser" />
+    <ol class="cd-breadcrumb triangle">
+        <li><a asp-controller="Home" asp-action="Index">首页</a></li>
+        <li><a asp-controller="Home" asp-action="Post">文章列表</a></li>
+        @if(Model.Category.ParentId>0) {
+            if(Model.Category.Parent.ParentId>0) {
+                <li><a asp-controller="Home" asp-action="Category" asp-route-id="@Model.Category.Parent.ParentId">@Model.Category.Parent.Parent.Name</a></li>
+            }
+            <li><a asp-controller="Home" asp-action="Category" asp-route-id="@Model.Category.ParentId">@Model.Category.Parent.Name</a></li>
+        }
+        <li><a asp-controller="Home" asp-action="Category" asp-route-id="@Model.CategoryId">@Model.Category.Name</a></li>
+        <li class="current"><em>@ViewBag.Title</em></li>
+    </ol>
+    <div class="wrapper-content article">
+        <div class="ibox">
+            <div class="ibox-content animated fadeIn">
+                <main>
+                    <section class="article-content">
+                        <header class="page-header">
+                            <div class="text-center">
+                                <a>
+                                    <h2 class="padding-bot10">
+                                        @Html.Raw(Model.Title)
+                                    </h2>
+                                </a>
+                            </div>
+                            <div class="row">
+                                <div class="col-sm-8">
+                                    <div class="padding-bot10">
+                                        <span class="label label-info">
+                                            <a asp-controller="Home" asp-action="Author" asp-route-author="@Model.Author">@Model.Author</a>
+                                        </span>发表于<a asp-controller="Home" asp-action="Archieve" asp-route-yyyy="@Model.PostDate.Year" asp-route-mm="@Model.PostDate.Month" asp-route-dd="@Model.PostDate.Day" asp-route-mode="@nameof(Model.PostDate)"><time class="text-info">@Model.PostDate.ToString("yyyy-MM-dd")</time></a> |
+                                        <span class="label label-info">
+                                            <a asp-controller="Home" asp-action="Author" asp-route-author="@Model.Modifier">@Model.Modifier</a>
+                                        </span>最后修改于<a asp-controller="Home" asp-action="Archieve" asp-route-yyyy="@Model.ModifyDate.Year" asp-route-mm="@Model.ModifyDate.Month" asp-route-dd="@Model.ModifyDate.Day" asp-route-mode="@nameof(Model.ModifyDate)"><time class="text-success">@Model.ModifyDate.ToString("yyyy-MM-dd")</time></a>
+                                    </div>
+                                </div>
+                                @{
+                                    if (!string.IsNullOrEmpty(Model.Label))
+                                    {
+                                        <div class="pull-right margin-right20">
+                                            <div>
+                                                @foreach (string s in Model.Label.Split(new []{',', ','},StringSplitOptions.RemoveEmptyEntries))
+                                                {
+                                                    <a asp-controller="Home" asp-action="Tag" asp-route-tag="@s">
+                                                        <span class="label label-@colors[new Random().Next() % colors.Length]">@s</span>
+                                                    </a>
+                                                }
+                                            </div>
+                                        </div>
+                                    }
+                                }
+                            </div>
+                            <div class="row">
+                                <div class="col-md-12 line-height24">
+                                    分类:<i class="icon-map-pin"></i>
+                                    @{
+                                        await Html.RenderPartialAsync("CategoryPath", Model.Category);
+                                    }
+                                    | 评论总数:<span class="text-danger">@ViewBag.CommentsCount</span>条 | @(await Html.RenderComponentAsync<PostOnline>(RenderMode.ServerPrerendered, new{ Model.Id,IP=Context.Connection.RemoteIpAddress.ToString()}))
+                                    @if (Model.Seminar.Any())
+                                    {
+                                        <span> | 所属专题:</span>
+                                        var seminars = Model.Seminar;
+                                        foreach (var s in seminars)
+                                        {
+                                            <a asp-controller="Seminar" asp-action="Index" asp-route-id="@s.Id" class="label label-info">@s.Title</a>
+                                            <text> </text>
+                                        }
+                                    }
+                                    @if (!Model.Locked) {
+                                        <div class="pull-right">
+                                            <a asp-controller="Post" asp-action="PushMerge" asp-route-id="@Model.Id" class="btn btn-danger">我要参与编辑</a>
+                                        </div>
+                                    }
+                                </div>
+                            </div>
+                        </header>
+                        <article class="article" id="article">
+                            @if (DateTime.Now - Model.ModifyDate > TimeSpan.FromDays(365))
+                            {
+                                <p class="text-focus text-center">该文章已经超过1年未更新,可能无法为您提供及时准确的资讯,请根据当下实际情况,酌情参考本文内容。</p>
+                            }
+                            @if(!Model.DisableCopy||Context.Request.IsRobot()) {
+                                @Html.Raw(await Model.Content.ReplaceImgAttribute(Model.Title))
+                            }
+                            else {
+                                @Html.Raw(Regex.Replace(await Model.Content.ReplaceImgAttribute(Model.Title), @"[\u4e00-\u9fa5]", m => HtmlEncoder.Default.Encode(m.Value)))
+                            }
+                            @{
+                                await Html.RenderPartialAsync("/Views/Post/ProtectContent.cshtml",Model);
+                            }
+                        </article>
+                    </section>
+                </main>
+                <hr class="margin-top10 marginbot10" />
+                @if (ad != null)
+                {
+                    await Html.RenderPartialAsync("_ArticleListAdvertisement", ad);
+                }
+                <section class="wow row padding-top40 padding-bot20 animated fadeIn">
+                    <div class="col-xs-6">
+                        <div class="btn-group">
+                            <button type="button" id="voteup" class="btn btn-success btn-lg">
+                                <i class="icon-thumbsup"></i><span>@Model.VoteUpCount</span>
+                            </button>
+                            <button type="button" id="votedown" class="btn btn-danger btn-lg">
+                                <i class="icon-thumbsdown"></i><span>@Model.VoteDownCount</span>
+                            </button>
+                        </div>
+                    </div>
+                    @if (Model.Rss&&CommonHelper.SystemSettings.GetOrAdd("EnableRss", "true") == "true") {
+                        <div class="col-xs-6 text-right">
+                            <div class="btn-group">
+                                <a class="btn btn-danger btn-lg" asp-controller="Subscribe" asp-action="PostRss" asp-route-id="@Model.Id" target="_blank">
+                                    <i class="icon-rss4"></i><span>订阅本文更新</span>
+                                </a>
+                            </div>
+                        </div>
+                    }
+                    <div class="col-xs-12">
+                        <p class="text-red">如果你喜欢这篇文章,欢迎点赞、评论、分享本文!</p>
+                    </div>
+                </section>
+                <section class="wow fadeInUp row size16">
+                @{
+                    PostModelBase prev = ViewBag.Prev;
+                    if (prev != null)
+                    {
+                        <div class="col-xs-6">
+                            上一篇:
+                            <a asp-controller="Post" asp-action="Details" asp-route-id="@prev.Id">@prev.Title</a>
+                        </div>
+                    }
+                    PostModelBase next = ViewBag.Next;
+                    if (next != null)
+                    {
+                        <div class="col-xs-6 text-right">
+                            下一篇:<a asp-controller="Post" asp-action="Details" asp-route-id="@next.Id">@next.Title</a>
+                        </div>
+                    }
+                }
+                </section>
+                @if (ViewBag.HistoryCount>0)
+                {
+                    <section class="wow margintop20 animated fadeIn">
+                        <h3>文章历史版本:</h3>
+                        <p>
+                            修改次数:@ViewBag.HistoryCount 次
+                            <a asp-controller="Post" asp-action="History" asp-route-id="@Model.Id">查看历史版本</a>
+                        </p>
+                    </section>
+                }
+                @{
+                    var posts = (Dictionary<int,string>)ViewBag.Related;
+                    if (posts.Count>1)
+                    {
+                        <section class="wow margintop20 animated fadeIn">
+                            <h3>相关推荐:</h3>
+                            <table class="table table-condensed size16">
+                                @for (int i = 1; i < posts.Count; i += 2)
+                                {
+                                    <tr>
+                                        <td style="width: 50%"><a asp-controller="Post" asp-action="Details" asp-route-id="@posts.ElementAt(i - 1).Key">@posts.ElementAt(i - 1).Value</a></td>
+                                        <td><a asp-controller="Post" asp-action="Details" asp-route-id="@posts.ElementAt(i).Key">@posts.ElementAt(i).Value</a></td>
+                                    </tr>
+                                }
+                            </table>
+                        </section>
+                    }
+                }
+                <section class="wow padding-bot20 animated fadeIn">
+                    <h3>版权声明:</h3>
+                    <p class="text-red size16">
+                        @if (Model.DisableCopy)
+                        {
+                            <text>🈲⚠本文为作者原创,仅用于本站访客学习、研究和交流目的,未经授权禁止转载。️⚠🈲 </text>
+                        }
+                        else
+                        {
+                            <text>本文仅用于学习、研究和交流目的,欢迎非商业性质转载。本文链接:<a asp-controller="Post" asp-action="Details" asp-route-id="@Model.Id">@HttpUtility.UrlDecode(Context.Request.Scheme + "://" + Context.Request.Host + Context.Request.Path)</a>。</text>
+                            @Html.Raw(CommonHelper.SystemSettings["Disclaimer"])
+                        }
+                    </p>
+                </section>
+                <section class="wow row animated fadeIn">
+                    <div class="col-lg-12">
+                        <h3>评论区:</h3>
+                        @if (!Model.DisableComment)
+                        {
+                            <form class="form-horizontal animated pulse" id="comment" method="post">
+                                @Html.AntiForgeryToken()
+                                <input type="hidden" name="PostId" value="@Model.Id" />
+                                <input type="hidden" id="OperatingSystem" name="OperatingSystem" />
+                                <input type="hidden" name="Browser" id="Browser" />
                                 @if(hidden==nameof(hidden)) {
                                     <p>@Context.Request.Cookies["NickName"] 您好!</p>
                                 }
-								<div class="row @hidden">
-									<div class="col-md-4">
-										<div class="input-group">
-											<span class="input-group-addon">
-												<label for="name">昵称:</label>
-											</span>
-											<input type="text" class="form-control" name="NickName" id="name" required placeholder="昵称" value="@Context.Request.Cookies["NickName"]">
-										</div>
-									</div>
-									<div class="col-md-5">
-										<div class="input-group">
-											<span class="input-group-addon">
-												<label for="email">邮箱:</label>
-											</span>
-											<input type="email" class="form-control" name="Email" id="email" required placeholder="留下您的真实邮箱,以方便接收回复" value="@Context.Request.Cookies["Email"]">
-											<span class="input-group-btn">
-												<button type="button" id="getcode" class="btn btn-danger">
-													获取验证码
-												</button>
-											</span>
-										</div>
-									</div>
-									<div class="col-md-3">
-										<div class="input-group">
-											<span class="input-group-addon">验证码:</span>
-											<input type="text" class="form-control" name="Code" placeholder="验证码">
-										</div>
-									</div>
-								</div>
-								<div class="form-group overlay animated fadeInDown" style="margin-bottom: 0px !important;">
-									<textarea class="layui-textarea" id="layedit" name="Content" style="height: 100px"></textarea>
-								</div>
-								<label><input type="checkbox" name="Agree" value="true" required/>我已阅读<a asp-controller="Misc" asp-action="agreement" target="_blank">《评论须知》</a></label>
-								<div class="form-group">
-									<div class="col-xs-6">
-										<button type="submit" class="btn btn-info btn-lg">提交</button>
-										<a class="text-red">评论框可粘贴网络图片</a>
-									</div>
-								</div>
-							</form>
-						}
-						else
-						{
-							<h4>本篇文章已禁用评论和回复功能</h4>
-						}
-						<ul class="wow media-list"></ul>
-						@if (ViewBag.CommentsCount>0)
-						{
-							<div class="row">
-								<div class="col-md-12 text-center">
-									<div id="pageToolbar"></div>
-								</div>
-							</div>
-						}
-					</div>
-				</section>
-			</div>
-		</div>
-	</div>
+                                <div class="row @hidden">
+                                    <div class="col-md-4">
+                                        <div class="input-group">
+                                            <span class="input-group-addon">
+                                                <label for="name">昵称:</label>
+                                            </span>
+                                            <input type="text" class="form-control" name="NickName" id="name" required placeholder="昵称" value="@Context.Request.Cookies["NickName"]">
+                                        </div>
+                                    </div>
+                                    <div class="col-md-5">
+                                        <div class="input-group">
+                                            <span class="input-group-addon">
+                                                <label for="email">邮箱:</label>
+                                            </span>
+                                            <input type="email" class="form-control" name="Email" id="email" required placeholder="留下您的真实邮箱,以方便接收回复" value="@Context.Request.Cookies["Email"]">
+                                            <span class="input-group-btn">
+                                                <button type="button" id="getcode" class="btn btn-danger">
+                                                    获取验证码
+                                                </button>
+                                            </span>
+                                        </div>
+                                    </div>
+                                    <div class="col-md-3">
+                                        <div class="input-group">
+                                            <span class="input-group-addon">验证码:</span>
+                                            <input type="text" class="form-control" name="Code" placeholder="验证码">
+                                        </div>
+                                    </div>
+                                </div>
+                                <div class="form-group overlay animated fadeInDown" style="margin-bottom: 0px !important;">
+                                    <textarea class="layui-textarea" id="layedit" name="Content" style="height: 100px"></textarea>
+                                </div>
+                                <label><input type="checkbox" name="Agree" value="true" required/>我已阅读<a asp-controller="Misc" asp-action="agreement" target="_blank">《评论须知》</a></label>
+                                <div class="form-group">
+                                    <div class="col-xs-6">
+                                        <button type="submit" class="btn btn-info btn-lg">提交</button>
+                                        <a class="text-red">评论框可粘贴网络图片</a>
+                                    </div>
+                                </div>
+                            </form>
+                        }
+                        else
+                        {
+                            <h4>本篇文章已禁用评论和回复功能</h4>
+                        }
+                        <ul class="wow media-list"></ul>
+                        @if (ViewBag.CommentsCount>0)
+                        {
+                            <div class="row">
+                                <div class="col-md-12 text-center">
+                                    <div id="pageToolbar"></div>
+                                </div>
+                            </div>
+                        }
+                    </div>
+                </section>
+            </div>
+        </div>
+    </div>
 </div>
 <div style="position: absolute; left: -20000px; bottom: 0;">
-	<div id="reply" class="container-fluid">
-		<form class="form-horizontal" id="reply-form" method="post">
-			@Html.AntiForgeryToken()
-			<input type="hidden" name="PostId" id="postId" value="@Model.Id" />
-			<input type="hidden" name="OperatingSystem" id="OperatingSystem2" />
-			<input type="hidden" name="Browser" id="Browser2" />
+    <div id="reply" class="container-fluid">
+        <form class="form-horizontal" id="reply-form" method="post">
+            @Html.AntiForgeryToken()
+            <input type="hidden" name="PostId" id="postId" value="@Model.Id" />
+            <input type="hidden" name="OperatingSystem" id="OperatingSystem2" />
+            <input type="hidden" name="Browser" id="Browser2" />
             @if(hidden==nameof(hidden)) {
                 <p>@Context.Request.Cookies["NickName"] 您好!</p>
             }
-			<div class="@hidden">
-				<div class="input-group">
-					<span class="input-group-addon">
-						<label for="name">昵称:</label>
-					</span>
-					<input type="text" class="form-control" name="NickName" id="name2" required placeholder="昵称" value="@Context.Request.Cookies["NickName"]">
-				</div>
-				<div class="input-group">
-					<span class="input-group-addon">
-						<label for="email">邮箱:</label>
-					</span>
-					<input type="email" class="form-control" name="Email" id="email2" required placeholder="留下您的真实邮箱,以方便接收回复" value="@Context.Request.Cookies["Email"]">
-					<span class="input-group-btn">
-						<button type="button" id="getcode-reply" class="btn btn-danger">
-							获取验证码
-						</button>
-					</span>
-				</div>
-				<div class="input-group">
-					<span class="input-group-addon">验证码:</span>
-					<input type="text" class="form-control" name="Code" placeholder="验证码">
-				</div>
-			</div>
-			<input type="hidden" value="" id="uid" name="ParentId" />
-			<div class="form-group overlay animated fadeInDown">
-				<textarea class="layui-textarea" id="layedit2" name="Content" style="height: 80px"></textarea>
-			</div>
-			<label><input type="checkbox" name="Agree" value="true" required/>我已阅读<a asp-controller="Misc" asp-action="agreement" target="_blank">《评论须知》</a></label>
-			<div class="form-group">
-				<div class="col-xs-12">
-					<div class="btn-group">
-						<button type="submit" class="btn btn-info">
-							提交
-						</button>
-						<button type="button" class="btn-cancel btn btn-danger">
-							取消
-						</button>
-					</div>
-				</div>
-			</div>
-		</form>
-	</div>
+            <div class="@hidden">
+                <div class="input-group">
+                    <span class="input-group-addon">
+                        <label for="name">昵称:</label>
+                    </span>
+                    <input type="text" class="form-control" name="NickName" id="name2" required placeholder="昵称" value="@Context.Request.Cookies["NickName"]">
+                </div>
+                <div class="input-group">
+                    <span class="input-group-addon">
+                        <label for="email">邮箱:</label>
+                    </span>
+                    <input type="email" class="form-control" name="Email" id="email2" required placeholder="留下您的真实邮箱,以方便接收回复" value="@Context.Request.Cookies["Email"]">
+                    <span class="input-group-btn">
+                        <button type="button" id="getcode-reply" class="btn btn-danger">
+                            获取验证码
+                        </button>
+                    </span>
+                </div>
+                <div class="input-group">
+                    <span class="input-group-addon">验证码:</span>
+                    <input type="text" class="form-control" name="Code" placeholder="验证码">
+                </div>
+            </div>
+            <input type="hidden" value="" id="uid" name="ParentId" />
+            <div class="form-group overlay animated fadeInDown">
+                <textarea class="layui-textarea" id="layedit2" name="Content" style="height: 80px"></textarea>
+            </div>
+            <label><input type="checkbox" name="Agree" value="true" required/>我已阅读<a asp-controller="Misc" asp-action="agreement" target="_blank">《评论须知》</a></label>
+            <div class="form-group">
+                <div class="col-xs-12">
+                    <div class="btn-group">
+                        <button type="submit" class="btn btn-info">
+                            提交
+                        </button>
+                        <button type="button" class="btn-cancel btn btn-danger">
+                            取消
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </form>
+    </div>
 </div>
 <script>
-	@if(Model.DisableCopy) {
-		<text>
-	GlobalCopyrightProtect();
-		</text>
-	}
-	window.onload = function() {
-		loading();
-		$('#pageToolbar').Paging({ //异步加载评论
-			pagesize: 10,
-			count: @ViewBag.CommentsCount,
-			toolbar: true,
-			callback: function(page, size, count) {
-				window.get("/comment/getcomments?id=" [email protected]+"&page=" + page + "&size=" + size, function(data) {
-					data = data.Data;
-					if (data) {
-						document.querySelector(".media-list").innerHTML = loadParentComments(data);
-						bindReplyBtn();
-						commentVoteBind(); //评论投票
-					}
-				});
-			}
-		});
-		loadingDone();
-		@if(Model.DisableComment) {
-			<text>
-				$(".msg-list article .panel-body a").remove();
-			</text>
-		}
-	};
+    @if(Model.DisableCopy) {
+        <text>
+    GlobalCopyrightProtect();
+        </text>
+    }
+    window.onload = function() {
+        loading();
+        $('#pageToolbar').Paging({ //异步加载评论
+            pagesize: 10,
+            count: @ViewBag.CommentsCount,
+            toolbar: true,
+            callback: function(page, size, count) {
+                window.get("/comment/getcomments?id=" [email protected]+"&page=" + page + "&size=" + size, function(data) {
+                    data = data.Data;
+                    if (data) {
+                        document.querySelector(".media-list").innerHTML = loadParentComments(data);
+                        bindReplyBtn();
+                        commentVoteBind(); //评论投票
+                    }
+                });
+            }
+        });
+        loadingDone();
+        @if(Model.DisableComment) {
+            <text>
+                $(".msg-list article .panel-body a").remove();
+            </text>
+        }
+    };
 
-	/**
+    /**
    * 获取评论
    */
-	function getcomments() {
-		window.get("/comment/getcomments?id=" + $("#postId").val()+"&page=1&size=10&cid=@cid", function(data) {
-			data = data.Data;
-			if (data) {
-				document.querySelector(".media-list").innerHTML = loadParentComments(data);
-				bindReplyBtn();
-				commentVoteBind(); //评论投票
-			}
-		});
-	}
+    function getcomments() {
+        window.get("/comment/getcomments?id=" + $("#postId").val()+"&page=1&size=10&cid=@cid", function(data) {
+            data = data.Data;
+            if (data) {
+                document.querySelector(".media-list").innerHTML = loadParentComments(data);
+                bindReplyBtn();
+                commentVoteBind(); //评论投票
+            }
+        });
+    }
 </script>

+ 487 - 498
src/Masuit.MyBlogs.Core/Views/Post/Details_Admin.cshtml

@@ -8,521 +8,510 @@
 @using Masuit.Tools.Systems
 @model Masuit.MyBlogs.Core.Models.Entity.Post
 @{
-	ViewBag.Title = Model.Title;
-	Layout = "~/Views/Shared/_Layout.cshtml";
-	string[] colors = { "success", "info", "primary", "warning", "danger", "default", "primary" };
-	UserInfoDto user = Context.Session.Get<UserInfoDto>(SessionKey.UserInfo);
-	var cid = Context.Request.RouteValues["cid"] ?? Context.Request.Query["cid"];
-	AdvertisementDto ad = ViewBag.Ads;
+    ViewBag.Title = Model.Title;
+    Layout = "~/Views/Shared/_Layout.cshtml";
+    string[] colors = { "success", "info", "primary", "warning", "danger", "default", "primary" };
+    UserInfoDto user = Context.Session.Get<UserInfoDto>(SessionKey.UserInfo);
+    var cid = Context.Request.RouteValues["cid"] ?? Context.Request.Query["cid"];
+    AdvertisementDto ad = ViewBag.Ads;
 }
 <script src="https://cdn.staticfile.org/jqueryui/1.12.1/jquery-ui.min.js" async defer></script>
 <environment names="Development">
-	<link href="~/Assets/jquery.tocify/jquery.tocify.css" rel="stylesheet" />
-	<link href="~/Assets/UEditor/third-party/SyntaxHighlighter/styles/shCore.css" rel="stylesheet" />
-	<script src="/Assets/UEditor/third-party/SyntaxHighlighter/scripts/shCore.js"></script>
-	<script src="/Assets/UEditor/third-party/SyntaxHighlighter/scripts/bundle.min.js"></script>
-	<script src="/Assets/jquery.tocify/jquery.tocify.js"></script>
-	<script src="/Scripts/global/article.js"></script>
+    <link href="~/Assets/jquery.tocify/jquery.tocify.css" rel="stylesheet" />
+    <link href="~/Assets/UEditor/third-party/SyntaxHighlighter/styles/shCore.css" rel="stylesheet" />
+    <script src="/Assets/UEditor/third-party/SyntaxHighlighter/scripts/shCore.js"></script>
+    <script src="/Assets/UEditor/third-party/SyntaxHighlighter/scripts/bundle.min.js"></script>
+    <script src="/Assets/jquery.tocify/jquery.tocify.js"></script>
+    <script src="/Scripts/global/article.js"></script>
 </environment>
 <environment names="Stage,Production">
-	@await Styles.RenderAsync("/bundles/article.css")
-	@await Scripts.RenderAsync("/bundles/article.js")
+    @await Styles.RenderAsync("/bundles/article.css")
+    @await Scripts.RenderAsync("/bundles/article.js")
 </environment>
 <div class="container">
-	<ol class="cd-breadcrumb triangle">
-		<li><a asp-controller="Home" asp-action="Index">首页</a></li>
-		<li><a asp-controller="Home" asp-action="Post">文章列表</a></li>
-		@if(Model.Category.ParentId>0) {
-		    if(Model.Category.Parent.ParentId>0) {
-			    <li><a asp-controller="Home" asp-action="Category" asp-route-id="@Model.Category.Parent.ParentId">@Model.Category.Parent.Parent.Name</a></li>
-		    }
-			<li><a asp-controller="Home" asp-action="Category" asp-route-id="@Model.Category.ParentId">@Model.Category.Parent.Name</a></li>
-		}
-		<li><a asp-controller="Home" asp-action="Category" asp-route-id="@Model.CategoryId">@Model.Category.Name</a></li>
-		<li class="current"><em>@ViewBag.Title</em></li>
-	</ol>
-	<div class="wrapper-content article">
-		<div class="ibox">
-			<div class="ibox-content animated fadeIn">
-				<main>
-					<section>
-						<header class="page-header">
-							<div class="text-center">
-								<a>
-									<h2 class="padding-bot10">
-										@Html.Raw(Model.Title)
-									</h2>
-								</a>
-							</div>
-							<div class="row">
-								<div class="col-sm-8">
-									<div class="padding-bot10">
-										<span class="label label-info">
-											<a asp-controller="Home" asp-action="Author" asp-route-author="@Model.Author">@Model.Author</a>
-										 </span>发表于<a asp-controller="Home" asp-action="Archieve" asp-route-yyyy="@Model.PostDate.Year" asp-route-mm="@Model.PostDate.Month" asp-route-dd="@Model.PostDate.Day" asp-route-mode="@nameof(Model.PostDate)"><time class="text-info">@Model.PostDate.ToString("yyyy-MM-dd HH:mm:ss")</time></a> |
-										<span class="label label-info">
-											<a asp-controller="Home" asp-action="Author" asp-route-author="@Model.Modifier">@Model.Modifier</a>
-										</span>最后修改于<a asp-controller="Home" asp-action="Archieve" asp-route-yyyy="@Model.ModifyDate.Year" asp-route-mm="@Model.ModifyDate.Month" asp-route-dd="@Model.ModifyDate.Day" asp-route-mode="@nameof(Model.ModifyDate)"><time class="text-success">@Model.ModifyDate.ToString("yyyy-MM-dd HH:mm")</time></a>
-									</div>
-								</div>
-								@{
-									if (!string.IsNullOrEmpty(Model.Label))
-									{
-										<div class="pull-right margin-right20">
-											<div>
-												@foreach (string s in Model.Label.Split(new[] { ',', ',' }, StringSplitOptions.RemoveEmptyEntries))
-												{
-													<a asp-controller="Home" asp-action="Tag" asp-route-tag="@s">
-														<span class="label label-@colors[new Random().Next() % colors.Length]">@s</span>
-													</a>
-												}
-											</div>
-										</div>
-									}
-								}
-							</div>
-							<div class="row">
-								<div class="col-md-12 line-height24">
-									分类:<i class="icon-map-pin"></i>
-									@{
-										await Html.RenderPartialAsync("CategoryPath", Model.Category);
-									}
-									| 评论总数:<span class="text-danger">@ViewBag.CommentsCount</span>条 | 热度:<span class="text-danger">@Model.TotalViewCount</span>℃ | @(await Html.RenderComponentAsync<PostOnline>(RenderMode.ServerPrerendered, new { Model.Id,IP=Context.Connection.RemoteIpAddress.ToString(),IsAdmin=true })) | 状态:@Model.Status.GetDisplay()
-									@if (Model.Seminar.Any())
-									{
-										<span> | 所属专题:</span>
-										var seminars = Model.Seminar;
-										foreach (var s in seminars)
-										{
-											<a asp-controller="Seminar" asp-action="Index" asp-route-id="@s.Id" class="label label-info">@s.Title</a><text> </text>
-										}
-									}
-								</div>
-								@if (Model.LimitMode > 0)
-								{
-									<div class="col-md-6 col-sm-12 text-red margin-top10">
-										本文 @Model.LimitMode.GetDescription(Model.Regions, Model.ExceptRegions)
-									</div>
-								}
-								<div class="col-md-6 col-sm-12 pull-right">
-									<div class="btn-group pull-right">
-										<a asp-controller="Post" asp-action="Refresh" asp-route-id="@Model.Id" class="btn btn-info">刷新</a>
-										@if (Model.IsFixedTop)
-										{
-											<button class="btn btn-success" id="pin">取消置顶</button>
-										}
-										else
-										{
-											<button class="btn btn-success" id="pin">置顶</button>
-										}
-										@if (Model.Status == Status.Pending)
-										{
-											<button class="btn btn-primary" id="pass">通过</button>
-										}
-										<a asp-controller="Dashboard" asp-action="Index" asp-fragment="/post/[email protected]" class="btn btn-primary" target="_blank">修改</a>
-										<a asp-controller="Dashboard" asp-action="Index" asp-fragment="/[email protected]" class="btn btn-info" target="_blank">复制</a>
-										<button class="btn btn-danger" id="takedown">下架</button>
-										<a asp-controller="Post" asp-action="PostVisitRecordInsight" asp-route-id="@Model.Id" class="btn btn-primary" target="_blank">洞察</a>
-									</div>
-								</div>
-							</div>
-						</header>
-						<article class="article" id="article">
-							@if (DateTime.Now - Model.ModifyDate > TimeSpan.FromDays(365))
-							{
-								<p class="text-focus text-center">该文章已经超过1年未更新,可能无法为您提供及时准确的资讯,请根据当下实际情况,酌情参考本文内容。</p>
-							}
-							@Html.Raw(Model.Content)
-							@Html.Raw(Model.ProtectContent)
-						</article>
-					</section>
-				</main>
-				<hr class="margin-top10 marginbot10" />
-				@if (ad != null)
-				{
-					await Html.RenderPartialAsync("_ArticleListAdvertisement", ad);
-				}
-				<section class="wow fadeIn row padding-top40 padding-bot20">
-					<div class="col-xs-6">
-						<div class="btn-group">
-							<button type="button" id="voteup" class="btn btn-success btn-lg">
-								<i class="icon-thumbsup"></i><span>@Model.VoteUpCount</span>
-							</button>
-							<button type="button" id="votedown" class="btn btn-danger btn-lg">
-								<i class="icon-thumbsdown"></i><span>@Model.VoteDownCount</span>
-							</button>
-						</div>
-					</div>
-				</section>
-				<section class="wow row size16 animated fadeIn">
-					<div class="col-xs-6">
-						上一篇:
-						@{
-							PostModelBase prev = ViewBag.Prev;
-							if (prev != null)
-							{
-								<a asp-controller="Post" asp-action="Details" asp-route-id="@prev.Id">@prev.Title</a>
-							}
-							else
-							{
-								<a>没有了</a>
-							}
-						}
-					</div>
-					<div class="col-xs-6 text-right">
-						下一篇:
-						@{
-							PostModelBase next = ViewBag.Next;
-							if (next != null)
-							{
-								<a asp-controller="Post" asp-action="Details" asp-route-id="@next.Id">@next.Title</a>
-							}
-							else
-							{
-								<a>没有了</a>
-							}
-						}
-					</div>
-				</section>
-				@if (ViewBag.HistoryCount>0)
-				{
-					<section class="wow margintop20 animated fadeIn">
-						<h3>文章历史版本:</h3>
-						<p>
-							修改次数:@ViewBag.HistoryCount 次
-							<a asp-controller="Post" asp-action="History" asp-route-id="@Model.Id">查看历史版本</a>
-						</p>
-					</section>
-				}
-				@{
-					var posts = (Dictionary<int, string>)ViewBag.Related;
-					if (posts.Count > 1)
-					{
-						<section class="wow margintop20 animated fadeIn">
-							<h3>相关推荐:</h3>
-							<table class="table table-condensed size16">
-								@for (int i = 1; i < posts.Count; i += 2)
-								{
-									<tr>
-										<td style="width: 50%"><a asp-controller="Post" asp-action="Details" asp-route-id="@posts.ElementAt(i - 1).Key">@posts.ElementAt(i - 1).Value</a></td>
-										<td><a asp-controller="Post" asp-action="Details" asp-route-id="@posts.ElementAt(i).Key">@posts.ElementAt(i).Value</a></td>
-									</tr>
-								}
-							</table>
-						</section>
-					}
-				}
-				<section class="wow padding-bot20 animated fadeIn">
-					<h3>版权声明:</h3>
-					<p class="text-red size16">
-						@if (Model.DisableCopy)
-						{
-							<text>🈲⚠本文为作者原创,仅用于本站访客学习、研究和交流目的,未经授权禁止转载。️⚠🈲</text>
-						}
-						else
-						{
-							<text>本文仅用于学习、研究和交流目的,欢迎非商业性质转载。本文链接:<a asp-controller="Post" asp-action="Details" asp-route-id="@Model.Id">@HttpUtility.UrlDecode(Context.Request.Scheme + "://" + Context.Request.Host + Context.Request.Path)</a>。</text>
-							@Html.Raw(CommonHelper.SystemSettings["Disclaimer"])
-						}
-					</p>
-				</section>
-				<section class="wow row animated fadeIn">
-					<div class="col-lg-12">
-						<h3>评论区:</h3>
-						<form class="form-horizontal animated pulse" id="comment" method="post">
-							@Html.AntiForgeryToken()
-							<input type="hidden" name="PostId" value="@Model.Id" />
-							<input type="hidden" id="OperatingSystem" name="OperatingSystem" />
-							<input type="hidden" name="Browser" id="Browser" />
-							<input type="hidden" value="@user.NickName" class="form-control" name="NickName" id="name">
-							<input type="hidden" value="@user.Email" class="form-control" name="Email" id="email">
-							<div class="form-group overlay animated fadeInDown" style="margin-bottom: 0px !important;">
-								<textarea class="layui-textarea" id="layedit" name="Content" style="height: 100px"></textarea>
-							</div>
-							<input type="hidden" name="Agree" value="true" />
-							<div class="form-group">
-								<div class="col-xs-12">
-									<button type="submit" class="btn btn-info btn-lg">提交</button>
-									<a class="text-red">评论框可粘贴网络图片</a>
-								</div>
-							</div>
-						</form>
-						<ul class="wow media-list"></ul>
-						@if (ViewBag.CommentsCount>0)
-						{
-							<div class="row">
-								<div class="col-md-12 text-center">
-									<div id="pageToolbar"></div>
-								</div>
-							</div>
-						}
-						else
-						{
-							<h4>还没有评论哦,赶紧来写评论吧</h4>
-						}
-					</div>
-				</section>
-			</div>
-		</div>
-	</div>
+    <ol class="cd-breadcrumb triangle">
+        <li><a asp-controller="Home" asp-action="Index">首页</a></li>
+        <li><a asp-controller="Home" asp-action="Post">文章列表</a></li>
+        @if(Model.Category.ParentId>0) {
+            if(Model.Category.Parent.ParentId>0) {
+                <li><a asp-controller="Home" asp-action="Category" asp-route-id="@Model.Category.Parent.ParentId">@Model.Category.Parent.Parent.Name</a></li>
+            }
+            <li><a asp-controller="Home" asp-action="Category" asp-route-id="@Model.Category.ParentId">@Model.Category.Parent.Name</a></li>
+        }
+        <li><a asp-controller="Home" asp-action="Category" asp-route-id="@Model.CategoryId">@Model.Category.Name</a></li>
+        <li class="current"><em>@ViewBag.Title</em></li>
+    </ol>
+    <div class="wrapper-content article">
+        <div class="ibox">
+            <div class="ibox-content animated fadeIn">
+                <main>
+                    <section>
+                        <header class="page-header">
+                            <div class="text-center">
+                                <a>
+                                    <h2 class="padding-bot10">
+                                        @Html.Raw(Model.Title)
+                                    </h2>
+                                </a>
+                            </div>
+                            <div class="row">
+                                <div class="col-sm-8">
+                                    <div class="padding-bot10">
+                                        <span class="label label-info">
+                                            <a asp-controller="Home" asp-action="Author" asp-route-author="@Model.Author">@Model.Author</a>
+                                         </span>发表于<a asp-controller="Home" asp-action="Archieve" asp-route-yyyy="@Model.PostDate.Year" asp-route-mm="@Model.PostDate.Month" asp-route-dd="@Model.PostDate.Day" asp-route-mode="@nameof(Model.PostDate)"><time class="text-info">@Model.PostDate.ToString("yyyy-MM-dd HH:mm:ss")</time></a> |
+                                        <span class="label label-info">
+                                            <a asp-controller="Home" asp-action="Author" asp-route-author="@Model.Modifier">@Model.Modifier</a>
+                                        </span>最后修改于<a asp-controller="Home" asp-action="Archieve" asp-route-yyyy="@Model.ModifyDate.Year" asp-route-mm="@Model.ModifyDate.Month" asp-route-dd="@Model.ModifyDate.Day" asp-route-mode="@nameof(Model.ModifyDate)"><time class="text-success">@Model.ModifyDate.ToString("yyyy-MM-dd HH:mm")</time></a>
+                                    </div>
+                                </div>
+                                @{
+                                    if (!string.IsNullOrEmpty(Model.Label))
+                                    {
+                                        <div class="pull-right margin-right20">
+                                            <div>
+                                                @foreach (string s in Model.Label.Split(new[] { ',', ',' }, StringSplitOptions.RemoveEmptyEntries))
+                                                {
+                                                    <a asp-controller="Home" asp-action="Tag" asp-route-tag="@s">
+                                                        <span class="label label-@colors[new Random().Next() % colors.Length]">@s</span>
+                                                    </a>
+                                                }
+                                            </div>
+                                        </div>
+                                    }
+                                }
+                            </div>
+                            <div class="row">
+                                <div class="col-md-12 line-height24">
+                                    分类:<i class="icon-map-pin"></i>
+                                    @{
+                                        await Html.RenderPartialAsync("CategoryPath", Model.Category);
+                                    }
+                                    | 评论总数:<span class="text-danger">@ViewBag.CommentsCount</span>条 | 热度:<span class="text-danger">@Model.TotalViewCount</span>℃ | @(await Html.RenderComponentAsync<PostOnline>(RenderMode.ServerPrerendered, new { Model.Id,IP=Context.Connection.RemoteIpAddress.ToString(),IsAdmin=true })) | 状态:@Model.Status.GetDisplay()
+                                    @if (Model.Seminar.Any())
+                                    {
+                                        <span> | 所属专题:</span>
+                                        var seminars = Model.Seminar;
+                                        foreach (var s in seminars)
+                                        {
+                                            <a asp-controller="Seminar" asp-action="Index" asp-route-id="@s.Id" class="label label-info">@s.Title</a><text> </text>
+                                        }
+                                    }
+                                </div>
+                                @if (Model.LimitMode > 0)
+                                {
+                                    <div class="col-md-6 col-sm-12 text-red margin-top10">
+                                        本文 @Model.LimitMode.GetDescription(Model.Regions, Model.ExceptRegions)
+                                    </div>
+                                }
+                                <div class="col-md-6 col-sm-12 pull-right">
+                                    <div class="btn-group pull-right">
+                                        <a asp-controller="Post" asp-action="Refresh" asp-route-id="@Model.Id" class="btn btn-info">刷新</a>
+                                        @if (Model.IsFixedTop)
+                                        {
+                                            <button class="btn btn-success" id="pin">取消置顶</button>
+                                        }
+                                        else
+                                        {
+                                            <button class="btn btn-success" id="pin">置顶</button>
+                                        }
+                                        @if (Model.Status == Status.Pending)
+                                        {
+                                            <button class="btn btn-primary" id="pass">通过</button>
+                                        }
+                                        <a asp-controller="Dashboard" asp-action="Index" asp-fragment="/post/[email protected]" class="btn btn-primary" target="_blank">修改</a>
+                                        <a asp-controller="Dashboard" asp-action="Index" asp-fragment="/[email protected]" class="btn btn-info" target="_blank">复制</a>
+                                        <button class="btn btn-danger" id="takedown">下架</button>
+                                        <a asp-controller="Post" asp-action="PostVisitRecordInsight" asp-route-id="@Model.Id" class="btn btn-primary" target="_blank">洞察</a>
+                                    </div>
+                                </div>
+                            </div>
+                        </header>
+                        <article class="article" id="article">
+                            @if (DateTime.Now - Model.ModifyDate > TimeSpan.FromDays(365))
+                            {
+                                <p class="text-focus text-center">该文章已经超过1年未更新,可能无法为您提供及时准确的资讯,请根据当下实际情况,酌情参考本文内容。</p>
+                            }
+                            @Html.Raw(Model.Content)
+                            @Html.Raw(Model.ProtectContent)
+                        </article>
+                    </section>
+                </main>
+                <hr class="margin-top10 marginbot10" />
+                @if (ad != null)
+                {
+                    await Html.RenderPartialAsync("_ArticleListAdvertisement", ad);
+                }
+                <section class="wow fadeIn row padding-top40 padding-bot20">
+                    <div class="col-xs-6">
+                        <div class="btn-group">
+                            <button type="button" id="voteup" class="btn btn-success btn-lg">
+                                <i class="icon-thumbsup"></i><span>@Model.VoteUpCount</span>
+                            </button>
+                            <button type="button" id="votedown" class="btn btn-danger btn-lg">
+                                <i class="icon-thumbsdown"></i><span>@Model.VoteDownCount</span>
+                            </button>
+                        </div>
+                    </div>
+                </section>
+                <section class="wow row size16 animated fadeIn">
+                @{
+                    PostModelBase prev = ViewBag.Prev;
+                    if (prev != null)
+                    {
+                        <div class="col-xs-6">
+                            上一篇:
+                            <a asp-controller="Post" asp-action="Details" asp-route-id="@prev.Id">@prev.Title</a>
+                        </div>
+                    }
+                    PostModelBase next = ViewBag.Next;
+                    if (next != null)
+                    {
+                        <div class="col-xs-6 text-right">
+                            下一篇:<a asp-controller="Post" asp-action="Details" asp-route-id="@next.Id">@next.Title</a>
+                        </div>
+                    }
+                }
+                </section>
+                @if (ViewBag.HistoryCount>0)
+                {
+                    <section class="wow margintop20 animated fadeIn">
+                        <h3>文章历史版本:</h3>
+                        <p>
+                            修改次数:@ViewBag.HistoryCount 次
+                            <a asp-controller="Post" asp-action="History" asp-route-id="@Model.Id">查看历史版本</a>
+                        </p>
+                    </section>
+                }
+                @{
+                    var posts = (Dictionary<int, string>)ViewBag.Related;
+                    if (posts.Count > 1)
+                    {
+                        <section class="wow margintop20 animated fadeIn">
+                            <h3>相关推荐:</h3>
+                            <table class="table table-condensed size16">
+                                @for (int i = 1; i < posts.Count; i += 2)
+                                {
+                                    <tr>
+                                        <td style="width: 50%"><a asp-controller="Post" asp-action="Details" asp-route-id="@posts.ElementAt(i - 1).Key">@posts.ElementAt(i - 1).Value</a></td>
+                                        <td><a asp-controller="Post" asp-action="Details" asp-route-id="@posts.ElementAt(i).Key">@posts.ElementAt(i).Value</a></td>
+                                    </tr>
+                                }
+                            </table>
+                        </section>
+                    }
+                }
+                <section class="wow padding-bot20 animated fadeIn">
+                    <h3>版权声明:</h3>
+                    <p class="text-red size16">
+                        @if (Model.DisableCopy)
+                        {
+                            <text>🈲⚠本文为作者原创,仅用于本站访客学习、研究和交流目的,未经授权禁止转载。️⚠🈲</text>
+                        }
+                        else
+                        {
+                            <text>本文仅用于学习、研究和交流目的,欢迎非商业性质转载。本文链接:<a asp-controller="Post" asp-action="Details" asp-route-id="@Model.Id">@HttpUtility.UrlDecode(Context.Request.Scheme + "://" + Context.Request.Host + Context.Request.Path)</a>。</text>
+                            @Html.Raw(CommonHelper.SystemSettings["Disclaimer"])
+                        }
+                    </p>
+                </section>
+                <section class="wow row animated fadeIn">
+                    <div class="col-lg-12">
+                        <h3>评论区:</h3>
+                        <form class="form-horizontal animated pulse" id="comment" method="post">
+                            @Html.AntiForgeryToken()
+                            <input type="hidden" name="PostId" value="@Model.Id" />
+                            <input type="hidden" id="OperatingSystem" name="OperatingSystem" />
+                            <input type="hidden" name="Browser" id="Browser" />
+                            <input type="hidden" value="@user.NickName" class="form-control" name="NickName" id="name">
+                            <input type="hidden" value="@user.Email" class="form-control" name="Email" id="email">
+                            <div class="form-group overlay animated fadeInDown" style="margin-bottom: 0px !important;">
+                                <textarea class="layui-textarea" id="layedit" name="Content" style="height: 100px"></textarea>
+                            </div>
+                            <input type="hidden" name="Agree" value="true" />
+                            <div class="form-group">
+                                <div class="col-xs-12">
+                                    <button type="submit" class="btn btn-info btn-lg">提交</button>
+                                    <a class="text-red">评论框可粘贴网络图片</a>
+                                </div>
+                            </div>
+                        </form>
+                        <ul class="wow media-list"></ul>
+                        @if (ViewBag.CommentsCount>0)
+                        {
+                            <div class="row">
+                                <div class="col-md-12 text-center">
+                                    <div id="pageToolbar"></div>
+                                </div>
+                            </div>
+                        }
+                        else
+                        {
+                            <h4>还没有评论哦,赶紧来写评论吧</h4>
+                        }
+                    </div>
+                </section>
+            </div>
+        </div>
+    </div>
 </div>
 <div style="position: absolute; left: -20000px; bottom: 0;">
-	<div id="reply" class="container-fluid">
-		<form class="form-horizontal" id="reply-form" method="post">
-			@Html.AntiForgeryToken()
-			<input type="hidden" name="PostId" id="postId" value="@Model.Id" />
-			<input type="hidden" name="OperatingSystem" id="OperatingSystem2" />
-			<input type="hidden" name="Browser" id="Browser2" />
-			<input type="hidden" value="@user.NickName" class="form-control" name="NickName" id="name2">
-			<input type="hidden" value="@user.Email" class="form-control" name="Email" id="email2">
-			<input type="hidden" value="" id="uid" name="ParentId" />
-			<div class="form-group overlay animated fadeInDown">
-				<textarea class="layui-textarea" id="layedit2" name="Content" style="height: 80px"></textarea>
-			</div>
-			<input type="hidden" name="Agree" value="true" />
-			<div class="form-group">
-				<div class="col-xs-12">
-					<div class="btn-group">
-						<button type="submit" class="btn btn-info btn-lg">
-							提交回复
-						</button>
-						<button type="button" class="btn-cancel btn btn-danger btn-lg">
-							取消回复
-						</button>
-					</div>
-				</div>
-			</div>
-		</form>
-	</div>
+    <div id="reply" class="container-fluid">
+        <form class="form-horizontal" id="reply-form" method="post">
+            @Html.AntiForgeryToken()
+            <input type="hidden" name="PostId" id="postId" value="@Model.Id" />
+            <input type="hidden" name="OperatingSystem" id="OperatingSystem2" />
+            <input type="hidden" name="Browser" id="Browser2" />
+            <input type="hidden" value="@user.NickName" class="form-control" name="NickName" id="name2">
+            <input type="hidden" value="@user.Email" class="form-control" name="Email" id="email2">
+            <input type="hidden" value="" id="uid" name="ParentId" />
+            <div class="form-group overlay animated fadeInDown">
+                <textarea class="layui-textarea" id="layedit2" name="Content" style="height: 80px"></textarea>
+            </div>
+            <input type="hidden" name="Agree" value="true" />
+            <div class="form-group">
+                <div class="col-xs-12">
+                    <div class="btn-group">
+                        <button type="submit" class="btn btn-info btn-lg">
+                            提交回复
+                        </button>
+                        <button type="button" class="btn-cancel btn btn-danger btn-lg">
+                            取消回复
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </form>
+    </div>
 </div>
 <script>
-	function getcomments() {
-		window.get("/comment/getcomments?id=" + $("#postId").val()+"&page=1&size=10&cid=@cid",function(data) {
-			data = data.Data;
-			document.querySelector(".media-list").innerHTML = loadParentComments(data);
-			bindReplyBtn();
-			commentVoteBind(); //评论投票
-		});
-	}
-	window.onload = function() {
-		loading();
-		$('#pageToolbar').Paging({ //异步加载评论
-			pagesize: 10,
-			count: @ViewBag.CommentsCount,
-			toolbar: true,
-			callback: function(page, size, count) {
-				window.get("/comment/getcomments?id=" [email protected]+"&page=" + page + "&size=" + size, function (data) {
-					data = data.Data;
-					if (data) {
-						document.querySelector(".media-list").innerHTML = loadParentComments(data);
-						bindReplyBtn();
-						commentVoteBind(); //评论投票
-					}
-				});
-			}
-		});
-		$("#takedown").on("click", function (e) {
-			swal({
-				title: "确认下架这篇文章吗?",
-				text: '@Model.Title',
-				showCancelButton: true,
-				confirmButtonColor: "#DD6B55",
-				confirmButtonText: "确定",
-				cancelButtonText: "取消",
-				showLoaderOnConfirm: true,
-				animation: true,
-				allowOutsideClick: false
-			}).then(function () {
-				window.fetch("/post/takedown/"[email protected], {
-		            credentials: 'include',
-		            method: 'POST',
-		            mode: 'cors'
-	            }).then(function(response) {
-		            return response.json();
-	            }).then(function(data) {
-		            window.notie.alert({
-						type: 1,
-						text: data.Message,
-						time: 4
-					});
-	            }).catch(function(e) {
-		            loadingDone();
-		            window.notie.alert({
-						type: 3,
-						text: "请求失败,请稍候再试!",
-						time: 4
-					});
-	            });
-			}, function () {
-			});
-		});
-		$("#pass").on("click", function (e) {
-			window.post("/post/pass", { id:@Model.Id}, function (data) {
-				window.notie.alert({
-					type: 1,
-					text: data.Message,
-					time: 4
-				}, () => {
-					window.notie.alert({
-						type: 3,
-						text: "请求失败,请稍候再试!",
-						time: 4
-					});
-				});
-				location.reload();
-			});
-		});
-		$("#pin").on("click", function (e) {
-			window.post("/post/Fixtop", { id: @Model.Id }, function (data) {
-				window.notie.alert({
-					type: 1,
-					text: data.Message,
-					time: 4
-				}, () => {
-					window.notie.alert({
-						type: 3,
-						text: "请求失败,请稍候再试!",
-						time: 4
-					});
-				});
-				location.reload();
-			});
-		});
-		loadingDone();
-	};
-	//递归加载评论
-	//加载父楼层
-	function loadParentComments(data) {
-		loading();
-		var html = '';
-		if (data) {
-			var rows = data.rows;
-			var page = data.page;
-			var size = data.size;
-			var maxPage = Math.ceil(data.total / size);
-			page = page > maxPage ? maxPage : page;
-			page = page < 1 ? 1 : page;
-			var startfloor = data.parentTotal - (page - 1) * size;
-			for (let i = 0; i < rows.length; i++) {
-				html += `<li class="msg-list media animated fadeInRight" id='${rows[i].Id}'>
-							<div class="media-body">
-								<article class="panel panel-info">
-									<header class="panel-heading ${rows[i].IsMaster ? "text-red" : ""} ${rows[i].IsAuthor ? "text-bold" : ""}">${startfloor--}# ${rows[i].IsMaster ? `<i class="icon icon-user"></i>` : ""}${rows[i].NickName}${rows[i].IsMaster ? `(管理员)` : ""} | ${rows[i].CommentDate}
-										<span class="pull-right" style="font-size: 10px;">${rows[i].Status == 4 ? `<a class="label label-success" onclick="pass(${rows[i].Id})">通过</a> |` : ""} <a class="label label-danger" onclick="del(${rows[i].Id})">删除</a> | ${GetOperatingSystem(rows[i].OperatingSystem) + " | " + GetBrowser(rows[i].Browser)}</span>
-									</header>
-									<div class="panel-body line-height24">
-										${rows[i].Content}
-										<span class="cmvote label label-info" data-id="${rows[i].Id}"><i class="icon-thumbsup"></i>(<span class="count">${rows[i].VoteCount}</span>)</span>
-										<a class="label label-info" href="?uid=${rows[i].Id}"><i class="icon-comment"></i></a><div class="margin-top10"></div>
-										<div class="pull-left">
-											<span class="label label-success">${rows[i].IP}</span>
-											<span class="label label-primary">${rows[i].Location}</span>
-										</div>
-										<div class="pull-right">
-											<span class="label label-success" onclick="bounceEmail('${rows[i].Email}')">${rows[i].Email}</span>
-										</div><br/>
-										${loadComments(rows[i].Children)}
-									</div>
-								</article>
-							</div>
-						</li>`;
-			}
-		}
-		loadingDone();
-		return html;
-	}
+    function getcomments() {
+        window.get("/comment/getcomments?id=" + $("#postId").val()+"&page=1&size=10&cid=@cid",function(data) {
+            data = data.Data;
+            document.querySelector(".media-list").innerHTML = loadParentComments(data);
+            bindReplyBtn();
+            commentVoteBind(); //评论投票
+        });
+    }
+    window.onload = function() {
+        loading();
+        $('#pageToolbar').Paging({ //异步加载评论
+            pagesize: 10,
+            count: @ViewBag.CommentsCount,
+            toolbar: true,
+            callback: function(page, size, count) {
+                window.get("/comment/getcomments?id=" [email protected]+"&page=" + page + "&size=" + size, function (data) {
+                    data = data.Data;
+                    if (data) {
+                        document.querySelector(".media-list").innerHTML = loadParentComments(data);
+                        bindReplyBtn();
+                        commentVoteBind(); //评论投票
+                    }
+                });
+            }
+        });
+        $("#takedown").on("click", function (e) {
+            swal({
+                title: "确认下架这篇文章吗?",
+                text: '@Model.Title',
+                showCancelButton: true,
+                confirmButtonColor: "#DD6B55",
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                showLoaderOnConfirm: true,
+                animation: true,
+                allowOutsideClick: false
+            }).then(function () {
+                window.fetch("/post/takedown/"[email protected], {
+                    credentials: 'include',
+                    method: 'POST',
+                    mode: 'cors'
+                }).then(function(response) {
+                    return response.json();
+                }).then(function(data) {
+                    window.notie.alert({
+                        type: 1,
+                        text: data.Message,
+                        time: 4
+                    });
+                }).catch(function(e) {
+                    loadingDone();
+                    window.notie.alert({
+                        type: 3,
+                        text: "请求失败,请稍候再试!",
+                        time: 4
+                    });
+                });
+            }, function () {
+            });
+        });
+        $("#pass").on("click", function (e) {
+            window.post("/post/pass", { id:@Model.Id}, function (data) {
+                window.notie.alert({
+                    type: 1,
+                    text: data.Message,
+                    time: 4
+                }, () => {
+                    window.notie.alert({
+                        type: 3,
+                        text: "请求失败,请稍候再试!",
+                        time: 4
+                    });
+                });
+                location.reload();
+            });
+        });
+        $("#pin").on("click", function (e) {
+            window.post("/post/Fixtop", { id: @Model.Id }, function (data) {
+                window.notie.alert({
+                    type: 1,
+                    text: data.Message,
+                    time: 4
+                }, () => {
+                    window.notie.alert({
+                        type: 3,
+                        text: "请求失败,请稍候再试!",
+                        time: 4
+                    });
+                });
+                location.reload();
+            });
+        });
+        loadingDone();
+    };
+    //递归加载评论
+    //加载父楼层
+    function loadParentComments(data) {
+        loading();
+        var html = '';
+        if (data) {
+            var rows = data.rows;
+            var page = data.page;
+            var size = data.size;
+            var maxPage = Math.ceil(data.total / size);
+            page = page > maxPage ? maxPage : page;
+            page = page < 1 ? 1 : page;
+            var startfloor = data.parentTotal - (page - 1) * size;
+            for (let i = 0; i < rows.length; i++) {
+                html += `<li class="msg-list media animated fadeInRight" id='${rows[i].Id}'>
+                            <div class="media-body">
+                                <article class="panel panel-info">
+                                    <header class="panel-heading ${rows[i].IsMaster ? "text-red" : ""} ${rows[i].IsAuthor ? "text-bold" : ""}">${startfloor--}# ${rows[i].IsMaster ? `<i class="icon icon-user"></i>` : ""}${rows[i].NickName}${rows[i].IsMaster ? `(管理员)` : ""} | ${rows[i].CommentDate}
+                                        <span class="pull-right" style="font-size: 10px;">${rows[i].Status == 4 ? `<a class="label label-success" onclick="pass(${rows[i].Id})">通过</a> |` : ""} <a class="label label-danger" onclick="del(${rows[i].Id})">删除</a> | ${GetOperatingSystem(rows[i].OperatingSystem) + " | " + GetBrowser(rows[i].Browser)}</span>
+                                    </header>
+                                    <div class="panel-body line-height24">
+                                        ${rows[i].Content}
+                                        <span class="cmvote label label-info" data-id="${rows[i].Id}"><i class="icon-thumbsup"></i>(<span class="count">${rows[i].VoteCount}</span>)</span>
+                                        <a class="label label-info" href="?uid=${rows[i].Id}"><i class="icon-comment"></i></a><div class="margin-top10"></div>
+                                        <div class="pull-left">
+                                            <span class="label label-success">${rows[i].IP}</span>
+                                            <span class="label label-primary">${rows[i].Location}</span>
+                                        </div>
+                                        <div class="pull-right">
+                                            <span class="label label-success" onclick="bounceEmail('${rows[i].Email}')">${rows[i].Email}</span>
+                                        </div><br/>
+                                        ${loadComments(rows[i].Children)}
+                                    </div>
+                                </article>
+                            </div>
+                        </li>`;
+            }
+        }
+        loadingDone();
+        return html;
+    }
 
-	//加载子楼层
-	function loadComments(comments, depth = 0) {
-		comments.sort(function(x, y) {
-			return x.Id - y.Id
-		});
+    //加载子楼层
+    function loadComments(comments, depth = 0) {
+        comments.sort(function(x, y) {
+            return x.Id - y.Id
+        });
 
-		var colors = ["info", "success", "primary", "warning", "danger"];
-		var floor = 1;
-		depth++;
-		var html = '';
-		for (let item of comments) {
-			var color = colors[depth % 5];
-			html += `<article id="${item.Id}" class="panel panel-${color}">
-							<div class="panel-heading ${item.IsMaster ? "text-red" : ""} ${item.IsAuthor ? "text-bold" : ""}">
-								${depth}-${floor++}# ${item.IsMaster ? `<i class="icon icon-user"></i>` : ""}${item.NickName}${item.IsMaster ? `(管理员)` : ""} | ${item.CommentDate}
-								<span class="pull-right" style="font-size: 10px;">${item.Status == 4 ? `<a class="label label-success" onclick="pass(${item.Id})">通过</a> |` : ""} <a class="label label-danger" onclick="del(${item.Id})">删除</a> | ${GetOperatingSystem(item.OperatingSystem) + " | " + GetBrowser(item.Browser)}</span>
-							</div>
-							<div class="panel-body line-height24">
-								${item.Content}
-								<span class="cmvote label label-${color}" data-id="${item.Id}"><i class="icon-thumbsup"></i>(<span class="count">${item.VoteCount}</span>)</span>
-								<a class="label label-${color}" href="?uid=${item.Id}"><i class="icon-comment"></i></a>
-								<div class="margin-top10"></div>
-								<div class="pull-left">
-									<span class="label label-success">${item.IP}</span>
-									<span class="label label-primary">${item.Location}</span>
-								</div>
-								<div class="pull-right">
-									<span class="label label-success" onclick="bounceEmail('${item.Email}')">${item.Email}</span>
-								</div><br/>
-								${loadComments(item.Children, depth)}
-							</div>
-						</article>`;
-		}
-		return html;
-	}
+        var colors = ["info", "success", "primary", "warning", "danger"];
+        var floor = 1;
+        depth++;
+        var html = '';
+        for (let item of comments) {
+            var color = colors[depth % 5];
+            html += `<article id="${item.Id}" class="panel panel-${color}">
+                            <div class="panel-heading ${item.IsMaster ? "text-red" : ""} ${item.IsAuthor ? "text-bold" : ""}">
+                                ${depth}-${floor++}# ${item.IsMaster ? `<i class="icon icon-user"></i>` : ""}${item.NickName}${item.IsMaster ? `(管理员)` : ""} | ${item.CommentDate}
+                                <span class="pull-right" style="font-size: 10px;">${item.Status == 4 ? `<a class="label label-success" onclick="pass(${item.Id})">通过</a> |` : ""} <a class="label label-danger" onclick="del(${item.Id})">删除</a> | ${GetOperatingSystem(item.OperatingSystem) + " | " + GetBrowser(item.Browser)}</span>
+                            </div>
+                            <div class="panel-body line-height24">
+                                ${item.Content}
+                                <span class="cmvote label label-${color}" data-id="${item.Id}"><i class="icon-thumbsup"></i>(<span class="count">${item.VoteCount}</span>)</span>
+                                <a class="label label-${color}" href="?uid=${item.Id}"><i class="icon-comment"></i></a>
+                                <div class="margin-top10"></div>
+                                <div class="pull-left">
+                                    <span class="label label-success">${item.IP}</span>
+                                    <span class="label label-primary">${item.Location}</span>
+                                </div>
+                                <div class="pull-right">
+                                    <span class="label label-success" onclick="bounceEmail('${item.Email}')">${item.Email}</span>
+                                </div><br/>
+                                ${loadComments(item.Children, depth)}
+                            </div>
+                        </article>`;
+        }
+        return html;
+    }
 
-	function pass(id) {
-		window.post("/comment/pass", { id: id }, function (res) {
-			swal(res.Message, "", res.Success ? "success" : "error");
-			getcomments();
-		}, () => {
-			swal("操作失败,请稍候再试", "", "error");
-		});
-	}
+    function pass(id) {
+        window.post("/comment/pass", { id: id }, function (res) {
+            swal(res.Message, "", res.Success ? "success" : "error");
+            getcomments();
+        }, () => {
+            swal("操作失败,请稍候再试", "", "error");
+        });
+    }
 
-	function bounceEmail(email) {
-		swal({
-			title: "确定将这个邮箱添加到黑名单吗?",
-			showCancelButton: true,
-			confirmButtonColor: "#DD6B55",
-			confirmButtonText: "确定",
-			cancelButtonText: "取消",
-			showLoaderOnConfirm: true,
-			animation: true,
-			allowOutsideClick: false
-		}).then(function () {
-			window.post("/system/BounceEmail", { email: email }, function () {
-				swal("邮箱添加到黑名单成功", "", "success");
-			}, (e) => {
-				swal("操作失败,请稍候再试", "", "error");
-			});
-		}, function () { });
-	}
+    function bounceEmail(email) {
+        swal({
+            title: "确定将这个邮箱添加到黑名单吗?",
+            showCancelButton: true,
+            confirmButtonColor: "#DD6B55",
+            confirmButtonText: "确定",
+            cancelButtonText: "取消",
+            showLoaderOnConfirm: true,
+            animation: true,
+            allowOutsideClick: false
+        }).then(function () {
+            window.post("/system/BounceEmail", { email: email }, function () {
+                swal("邮箱添加到黑名单成功", "", "success");
+            }, (e) => {
+                swal("操作失败,请稍候再试", "", "error");
+            });
+        }, function () { });
+    }
 
-	function del(id) {
-		swal({
-			title: "确定删除这条评论吗?",
-			showCancelButton: true,
-			confirmButtonColor: "#DD6B55",
-			confirmButtonText: "确定",
-			cancelButtonText: "取消",
-			showLoaderOnConfirm: true,
-			animation: true,
-			allowOutsideClick: false
-		}).then(function () {
-			$.post("/comment/delete/"+id, function (res) {
-				swal(res.Message, "", res.Success ? "success" : "error");
-				getcomments();
-			});
-		}, function () { });
-	}
+    function del(id) {
+        swal({
+            title: "确定删除这条评论吗?",
+            showCancelButton: true,
+            confirmButtonColor: "#DD6B55",
+            confirmButtonText: "确定",
+            cancelButtonText: "取消",
+            showLoaderOnConfirm: true,
+            animation: true,
+            allowOutsideClick: false
+        }).then(function () {
+            $.post("/comment/delete/"+id, function (res) {
+                swal(res.Message, "", res.Success ? "success" : "error");
+                getcomments();
+            });
+        }, function () { });
+    }
 
 function showViewer(data) {
-	var html="";
-	for (let item of data) {
-		html+=`<p><a href="/tools/ip/${item.key}" target="_blank">${item.key}</a>:${item.value}</p>`;
-	}
-	layer.open({
-	  type: 1,
-	  area: ['800px',"60vh"], //宽高
-	  content: html
-	});
+    var html="";
+    for (let item of data) {
+        html+=`<p><a href="/tools/ip/${item.key}" target="_blank">${item.key}</a>:${item.value}</p>`;
+    }
+    layer.open({
+      type: 1,
+      area: ['800px',"60vh"], //宽高
+      content: html
+    });
 }
 </script>

+ 1 - 1
src/Masuit.MyBlogs.Core/wwwroot/Scripts/global/article.js

@@ -6,7 +6,7 @@
 	$(".tocify>.close").on("click", function(e) {
 		$(this).parent().hide();
 	});		
-	$('article img').click(function(){
+	$('article p>img').click(function(){
 		window.open($(this).attr("src"));
 	});
 

+ 26 - 6
src/Masuit.MyBlogs.Core/wwwroot/ng-views/views/system/firewall.html

@@ -36,19 +36,19 @@
                     <div class="input-group">
                         <span class="input-group-addon">单IP访问频次:每</span>
                         <div class="fg-line">
-                            <input class="form-control" ng-model="Settings.LimitIPFrequency" type="text" />
+                            <input class="form-control" ng-model="Settings.LimitIPFrequency" type="text" style="width:32px"/>
                         </div>
                         <span class="input-group-addon">秒内,最大请求</span>
                         <div class="fg-line">
-                            <input class="form-control" ng-model="Settings.LimitIPRequestTimes" type="text" />
+                            <input class="form-control" ng-model="Settings.LimitIPRequestTimes" type="text" style="width:32px" />
                         </div>
                         <span class="input-group-addon">次,冻结该IP:</span>
                         <div class="fg-line">
-                            <input class="form-control" ng-model="Settings.BanIPTimespan" type="text" />
+                            <input class="form-control" ng-model="Settings.BanIPTimespan" type="text" style="width:24px" />
                         </div>
                         <span class="input-group-addon">分钟,拦截次数达到</span>
                         <div class="fg-line">
-                            <input class="form-control" ng-model="Settings.LimitIPInterceptTimes" type="text" />
+                            <input class="form-control" ng-model="Settings.LimitIPInterceptTimes" type="text" style="width:32px" />
                         </div>
                         <span class="input-group-addon">次,上报防火墙永久冻结该IP。</span>
                     </div>
@@ -57,7 +57,7 @@
                     <div class="input-group">
                         <span class="input-group-addon">挑战模式:</span>
                         <select ng-model="Settings.ChallengeMode" ng-init="Settings.ChallengeMode = ' '" class="form-control">
-                            <option value=" ">无</option>
+                            <option value="">无</option>
                             <option value="JSChallenge">JS挑战</option>
                             <option value="CaptchaChallenge">验证码挑战</option>
                             <option value="CloudflareTurnstileChallenge">CloudflareTurnstile</option>
@@ -66,7 +66,7 @@
                     <div class="input-group" ng-if="Settings.ChallengeMode=='CloudflareTurnstileChallenge'">
                         <span class="input-group-addon">客户端公钥:</span>
                         <div class="fg-line">
-                            <input class="form-control" ng-model="Settings.TurnstileClientKey" placeholder="0x4AAAAAAAAu4dr4wC-ZVpPT" type="text" style="min-width: 220px"/>
+                            <input class="form-control" ng-model="Settings.TurnstileClientKey" placeholder="0x4AAAAAAAAu4dr4wC-ZVpPT" type="text" style="min-width: 220px" />
                         </div>
                     </div>
                     <div class="input-group" ng-if="Settings.ChallengeMode=='CloudflareTurnstileChallenge'">
@@ -76,6 +76,26 @@
                         </div>
                         <span class="input-group-addon text-red" ng-if="(Settings.TurnstileSecretKey+Settings.TurnstileClientKey).length==0">温馨提示:请先到<a href="https://dash.cloudflare.com/" target="_blank">Cloudflare Turnstile</a>生成站点密钥。</span>
                     </div>
+                    <small class="form-group form-inline" ng-if="Settings.ChallengeMode">
+                        <div class="input-group">
+                            <span class="input-group-addon">启用规则:</span>
+                        </div>
+                        <div class="input-group">
+                            <div class="fg-line">
+                                <select ng-model="Settings.ChallengeRule" class="form-control">
+                                    <option value="All">所有</option>
+                                    <option value="Region">在地区:</option>
+                                </select>
+                                <span ng-if="Settings.ChallengeRule=='Region'">
+                                    <input type="text" class="form-control" id="region" placeholder="竖线分隔,支持国家、地区、城市、运营商、ASN" ng-model="Settings.ChallengeRegions" style="width:300px">
+                                    <select ng-model="Settings.ChallengeRegionLimitMode" ng-init="Settings.ChallengeRegionLimitMode=Settings.ChallengeRegionLimitMode||'2'" class="form-control">
+                                        <option value="1">以内</option>
+                                        <option value="2">以外</option>
+                                    </select>
+                                </span>
+                            </div>
+                        </div>
+                    </small>
                 </div>
                 <div class="col-md-12">
                     <div class="input-group">