zhaolei
9 days ago 921de2254ff5712a44ed8575ee8efe34252f6603
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
using Bootstrap.DataAccess;
using Bootstrap.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Task = System.Threading.Tasks.Task;
 
namespace Bootstrap.Admin.HealthChecks
{
    /// <summary>
    /// 数据库检查类
    /// </summary>
    public class DBHealthCheck : IHealthCheck
    {
        private readonly IConfiguration _configuration;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private static readonly Func<IConfiguration, string, string?> ConnectionStringResolve = (c, name) => string.IsNullOrEmpty(name)
            ? c.GetSection("ConnectionStrings").GetChildren().FirstOrDefault()?.Value
            : c.GetConnectionString(name);
 
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="configuration"></param>
        /// <param name="httpContextAccessor"></param>
        public DBHealthCheck(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
        {
            _configuration = configuration;
            _httpContextAccessor = httpContextAccessor;
        }
 
        /// <summary>
        /// 异步检查方法
        /// </summary>
        /// <param name="context"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task<HealthCheckResult> 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 = Prow.Data.DatabaseProviderType.SqlServer.ToString(),
                    Widget = typeof(User).Assembly.FullName,
                    ConnectionString = Prow.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<string, object?>()
            {
                { "ConnectionString", db.ConnectionString },
                { "Reference", DbContextManager.Create<Dict>()?.GetType().Assembly.FullName ?? db.Widget },
                { "DbType", db?.ProviderName },
                { "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));
        }
 
        private class DbOption
        {
            public bool Enabled { get; set; }
            public string? ProviderName { get; set; }
            public string? Widget { get; set; }
            public string? ConnectionString { get; set; }
        }
    }
}