diff --git a/src/blazor/admin/BootstrapAdmin.Web/Controllers/AccountController.cs b/src/blazor/admin/BootstrapAdmin.Web/Controllers/AccountController.cs
index 13d43263..7f046f88 100644
--- a/src/blazor/admin/BootstrapAdmin.Web/Controllers/AccountController.cs
+++ b/src/blazor/admin/BootstrapAdmin.Web/Controllers/AccountController.cs
@@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using System.Security.Claims;
-namespace Bootstrap.Admin.Controllers
+namespace BootstrapAdmin.Web.Controllers
{
///
/// Account controller.
diff --git a/src/blazor/admin/BootstrapAdmin.Web/Controllers/api/GiteeController.cs b/src/blazor/admin/BootstrapAdmin.Web/Controllers/api/GiteeController.cs
new file mode 100644
index 00000000..afc31131
--- /dev/null
+++ b/src/blazor/admin/BootstrapAdmin.Web/Controllers/api/GiteeController.cs
@@ -0,0 +1,109 @@
+using BootstrapAdmin.Web.HealthChecks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using System.Text.RegularExpressions;
+using RouteAttribute = Microsoft.AspNetCore.Mvc.RouteAttribute;
+
+namespace BootstrapAdmin.Web.Controllers.Api
+{
+ ///
+ /// Gitee 网站信息接口类
+ ///
+ [Route("api/[controller]/[action]")]
+ [ApiController]
+ [AllowAnonymous]
+ public class GiteeController : ControllerBase
+ {
+ ///
+ /// 获取 Gitee 网站 Issues 信息
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ [HttpGet]
+ public async Task Issues([FromServices] GiteeHttpClient client, [FromQuery] string? userName = "LongbowEnterprise", [FromQuery] string? repoName = "BootstrapAdmin", [FromQuery] string? label = "custom badge", [FromQuery] string? color = "orange")
+ {
+ var content = await client.HttpClient.GetStringAsync($"https://gitee.com/{userName}/{repoName}/issues");
+ var regex = Regex.Matches(content, "([\\d]+)
", RegexOptions.IgnoreCase);
+ var labels = new string[] { "open", "progressing", "closed", "rejected" };
+ var result = string.IsNullOrEmpty(content) ? new string[] { "unknown" } : regex.Select((m, i) => $"{labels[i]} {m.Groups[1].Value}");
+ var msg = string.Join(" ", result);
+ color = msg.StartsWith("open 0 progressing 0", StringComparison.OrdinalIgnoreCase) ? "success" : color;
+ return new JsonResult(new { schemaVersion = 1, label, message = msg, color });
+ }
+
+ ///
+ /// 获取 Gitee 网站 Pulls 信息
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ [HttpGet]
+ public async Task Pulls([FromServices] GiteeHttpClient client, [FromQuery] string? userName = "LongbowEnterprise", [FromQuery] string? repoName = "BootstrapAdmin", [FromQuery] string? label = "custom badge", [FromQuery] string? color = "orange")
+ {
+ var content = await client.HttpClient.GetStringAsync($"https://gitee.com/{userName}/{repoName}/pulls") ?? "";
+ var regex = Regex.Matches(content, "([\\d]+)
", RegexOptions.IgnoreCase);
+ var labels = new string[] { "open", "merged", "closed" };
+ var result = string.IsNullOrEmpty(content) ? new string[] { "unknown" } : regex.Select((m, i) => $"{labels[i]} {m.Groups[1].Value}");
+ var msg = string.Join(" ", result);
+ return new JsonResult(new { schemaVersion = 1, label, message = msg, color });
+ }
+
+ ///
+ /// 获取 Gitee 网站 Releases 信息
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ [HttpGet]
+ public async Task Releases([FromServices] GiteeHttpClient client, [FromQuery] string? userName = "LongbowEnterprise", [FromQuery] string? repoName = "BootstrapAdmin", [FromQuery] string? label = "custom badge", [FromQuery] string? color = "orange")
+ {
+ var content = await client.HttpClient.GetStringAsync($"https://gitee.com/{userName}/{repoName}/releases") ?? "";
+ var regex = Regex.Match(content, $"", RegexOptions.IgnoreCase);
+ var msg = regex?.Groups[1].Value ?? "unknown";
+ return new JsonResult(new { schemaVersion = 1, label, message = msg, color });
+ }
+
+ ///
+ /// 获取 Gitee 网站 Builds 信息
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ [HttpGet]
+ public async Task Builds([FromServices] GiteeHttpClient client, [FromQuery] string? userName = "ArgoZhang", [FromQuery] string? projName = "bootstrapadmin", [FromQuery] string? branchName = "master", [FromQuery] string? label = "custom badge", [FromQuery] string? color = "orange")
+ {
+ var content = await client.HttpClient.GetFromJsonAsync($"https://ci.appveyor.com/api/projects/{userName}/{projName}/branch/{branchName}", new CancellationTokenSource(10000).Token);
+ return new JsonResult(new { schemaVersion = 1, label, message = content?.Build.Version ?? "unknown", color });
+ }
+
+ private class AppveyorBuildResult
+ {
+ ///
+ /// Appveyor 编译版本实例
+ ///
+ public Build Build { get; set; } = new Build();
+ }
+
+ private class Build
+ {
+ ///
+ /// Build 版本信息
+ ///
+ public string Version { get; set; } = "";
+ }
+ }
+}
diff --git a/src/blazor/admin/BootstrapAdmin.Web/Extensions/ApplicationBuilderExtensions.cs b/src/blazor/admin/BootstrapAdmin.Web/Extensions/ApplicationBuilderExtensions.cs
index eddd456e..c177c7fc 100644
--- a/src/blazor/admin/BootstrapAdmin.Web/Extensions/ApplicationBuilderExtensions.cs
+++ b/src/blazor/admin/BootstrapAdmin.Web/Extensions/ApplicationBuilderExtensions.cs
@@ -10,8 +10,11 @@
///
///
///
- public static IApplicationBuilder UseBootstrapBlazorAdmin(this IApplicationBuilder builder)
+ public static WebApplication UseBootstrapBlazorAdmin(this WebApplication builder)
{
+ // 开启健康检查
+ builder.MapBootstrapHealthChecks();
+
builder.UseAuthentication();
builder.UseAuthorization();
diff --git a/src/blazor/admin/BootstrapAdmin.Web/Extensions/ServicesExtensions.cs b/src/blazor/admin/BootstrapAdmin.Web/Extensions/ServicesExtensions.cs
index 9460ed5c..d2a1e943 100644
--- a/src/blazor/admin/BootstrapAdmin.Web/Extensions/ServicesExtensions.cs
+++ b/src/blazor/admin/BootstrapAdmin.Web/Extensions/ServicesExtensions.cs
@@ -4,6 +4,8 @@ using BootstrapAdmin.Web.Services.SMS.Tencent;
using BootstrapAdmin.Web.Utils;
//using Microsoft.EntityFrameworkCore;
using System.Text;
+using System.Text.Encodings.Web;
+using System.Text.Unicode;
namespace Microsoft.Extensions.DependencyInjection
{
@@ -19,16 +21,21 @@ namespace Microsoft.Extensions.DependencyInjection
public static IServiceCollection AddBootstrapBlazorAdmin(this IServiceCollection services)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
-
services.AddLogging(logging => logging.AddFileLogger().AddCloudLogger().AddDBLogger(ExceptionsHelper.Log));
services.AddCors();
services.AddResponseCompression();
+ // 增加编码服务
+ services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
+
+ // 增加 健康检查服务
+ services.AddAdminHealthChecks();
+
// 增加 BootstrapBlazor 组件
services.AddBootstrapBlazor();
// 增加手机短信服务
- services.AddScoped();
+ services.AddSingleton();
// 增加认证授权服务
services.AddBootstrapAdminSecurity();
diff --git a/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/DBHealthCheck.cs b/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/DBHealthCheck.cs
new file mode 100644
index 00000000..d8ad5175
--- /dev/null
+++ b/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/DBHealthCheck.cs
@@ -0,0 +1,125 @@
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace BootstrapAdmin.Web.HealthChecks;
+
+///
+/// 数据库检查类
+///
+class DBHealthCheck : IHealthCheck
+{
+ private readonly IConfiguration _configuration;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private static readonly Func ConnectionStringResolve = (c, name) => string.IsNullOrEmpty(name)
+ ? c.GetSection("ConnectionStrings").GetChildren().FirstOrDefault()?.Value
+ : c.GetConnectionString(name);
+
+ ///
+ /// 构造函数
+ ///
+ ///
+ ///
+ public DBHealthCheck(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
+ {
+ _configuration = configuration;
+ _httpContextAccessor = httpContextAccessor;
+ }
+
+ ///
+ /// 异步检查方法
+ ///
+ ///
+ ///
+ ///
+ public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ //var db = _configuration.GetSection("DB").GetChildren()
+ // .Select(config => new DbOption()
+ // {
+ // Enabled = bool.TryParse(config["Enabled"], out var en) ? en : false,
+ // ProviderName = config["ProviderName"],
+ // Widget = config["Widget"],
+ // ConnectionString = ConnectionStringResolve(config.GetSection("ConnectionStrings").Exists() ? config : _configuration, string.Empty)
+ // }).FirstOrDefault(i => i.Enabled) ?? new DbOption()
+ // {
+ // Enabled = true,
+ // ProviderName = Longbow.Data.DatabaseProviderType.SqlServer.ToString(),
+ // Widget = typeof(User).Assembly.FullName,
+ // ConnectionString = Longbow.Data.DbManager.GetConnectionString()
+ // };
+
+ //// 检查 当前用户 账户权限
+ //var loginUser = _httpContextAccessor.HttpContext?.User.Identity?.Name;
+ //var userName = loginUser ?? "Admin";
+ //var dictsCount = 0;
+ //var menusCount = 0;
+ //var roles = string.Empty;
+ //var displayName = string.Empty;
+ //var healths = false;
+ //Exception? error = null;
+ //try
+ //{
+ // var user = UserHelper.RetrieveUserByUserName(userName);
+ // displayName = user?.DisplayName ?? string.Empty;
+ // roles = string.Join(",", RoleHelper.RetrievesByUserName(userName) ?? new string[0]);
+ // menusCount = MenuHelper.RetrieveMenusByUserName(userName)?.Count() ?? 0;
+ // dictsCount = DictHelper.RetrieveDicts()?.Count() ?? 0;
+ // healths = user != null && !string.IsNullOrEmpty(roles) && menusCount > 0 && dictsCount > 0;
+
+ // // 检查数据库是否可写
+ // var dict = new BootstrapDict()
+ // {
+ // Category = "DB-Check",
+ // Name = "WriteTest",
+ // Code = "1"
+ // };
+ // if (DictHelper.Save(dict) && !string.IsNullOrEmpty(dict.Id)) DictHelper.Delete(new string[] { dict.Id });
+ //}
+ //catch (Exception ex)
+ //{
+ // error = ex;
+ //}
+ //var data = new Dictionary()
+ //{
+ // { "ConnectionString", db.ConnectionString ?? string.Empty },
+ // { "Reference", DbContextManager.Create()?.GetType().Assembly.FullName ?? db.Widget ?? string.Empty },
+ // { "DbType", db?.ProviderName ?? string.Empty },
+ // { "Dicts", dictsCount },
+ // { "LoginName", userName },
+ // { "DisplayName", displayName },
+ // { "Roles", roles },
+ // { "Navigations", menusCount }
+ //};
+
+ //if (string.IsNullOrEmpty(db?.ConnectionString))
+ //{
+ // // 未启用连接字符串
+ // data["ConnectionString"] = "未配置数据库连接字符串";
+ // return Task.FromResult(HealthCheckResult.Unhealthy("Error", null, data));
+ //}
+
+ //if (DbContextManager.Exception != null) error = DbContextManager.Exception;
+ //if (error != null)
+ //{
+ // data.Add("Exception", error.Message);
+
+ // if (error.Message.Contains("SQLite Error 8: 'attempt to write a readonly database'.")) data.Add("解决办法", "更改数据库文件为可读,并授予进程可写权限");
+ // if (error.Message.Contains("Could not load", StringComparison.OrdinalIgnoreCase)) data.Add("解决办法", "Nuget 引用相对应的数据库驱动 dll");
+
+ // // UNDONE: Json 序列化循环引用导致异常 NET 5.0 修复此问题
+ // // 目前使用 new Exception() 临时解决
+ // return Task.FromResult(HealthCheckResult.Unhealthy("Error", new Exception(error.Message), data));
+ //}
+
+ //return healths ? Task.FromResult(HealthCheckResult.Healthy("Ok", data)) : Task.FromResult(HealthCheckResult.Degraded("Failed", null, data));
+
+ return Task.FromResult(HealthCheckResult.Healthy("Ok"));
+ }
+
+ private class DbOption
+ {
+ public bool Enabled { get; set; }
+ public string? ProviderName { get; set; }
+ public string? Widget { get; set; }
+ public string? ConnectionString { get; set; }
+ }
+}
diff --git a/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/GiteeHttpClient.cs b/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/GiteeHttpClient.cs
new file mode 100644
index 00000000..d3b54366
--- /dev/null
+++ b/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/GiteeHttpClient.cs
@@ -0,0 +1,23 @@
+namespace BootstrapAdmin.Web.HealthChecks;
+
+///
+/// Gitee HttpClient 操作类
+///
+public class GiteeHttpClient
+{
+ ///
+ /// HttpClient 实例
+ ///
+ public HttpClient HttpClient { get; private set; }
+
+ ///
+ /// 构造函数
+ ///
+ ///
+ public GiteeHttpClient(HttpClient client)
+ {
+ client.Timeout = TimeSpan.FromSeconds(10);
+ client.DefaultRequestHeaders.Connection.Add("keep-alive");
+ HttpClient = client;
+ }
+}
diff --git a/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/GiteeHttpHealthCheck.cs b/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/GiteeHttpHealthCheck.cs
new file mode 100644
index 00000000..65196565
--- /dev/null
+++ b/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/GiteeHttpHealthCheck.cs
@@ -0,0 +1,51 @@
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using System.Diagnostics;
+
+namespace BootstrapAdmin.Web.HealthChecks;
+
+///
+/// Gitee 接口检查器
+///
+class GiteeHttpHealthCheck : IHealthCheck
+{
+ private readonly GiteeHttpClient _client;
+ ///
+ /// 构造函数
+ ///
+ ///
+ ///
+ public GiteeHttpHealthCheck(GiteeHttpClient client, IHttpContextAccessor accessor)
+ {
+ _client = client;
+ _client.HttpClient.BaseAddress = new Uri($"{accessor.HttpContext!.Request.Scheme}://{accessor.HttpContext?.Request.Host}{accessor.HttpContext?.Request.PathBase}");
+ }
+
+ ///
+ /// 异步检查方法
+ ///
+ ///
+ ///
+ ///
+ public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ var urls = new string[] { "Issues", "Pulls", "Releases", "Builds" };
+ var data = new Dictionary();
+
+ Task.WaitAll(urls.Select(url => Task.Run(async () =>
+ {
+ var sw = Stopwatch.StartNew();
+ Exception? error = null;
+ object? result = null;
+ try
+ {
+ result = await _client.HttpClient.GetFromJsonAsync