-
zhangwei
2 天以前 89b94f3cc1aa492b3223b97f3312d8eca004032b
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
 
#if NET9_0_OR_GREATER
 
using Microsoft.AspNetCore.Builder;
using XiHan.Framework.Utils.Logging;
using XiHan.Framework.Utils.Reflections;
 
namespace Admin.NET.Core.Update;
 
/// <summary>
/// 自动版本更新中间件拓展
/// </summary>
/// <remarks>
/// 使用方法
/// 1.在 Admin.NET.Web.Core 的 Startup.cs 中的 Configure 方法中调用 app.UseAutoVersionUpdate()。
/// 2.在入口项目 Admin.NET.Web.Entry 的根目录下创建一个名为 UpdateScripts 的文件夹,并在其中放置 .sql 后缀的脚本文件
/// 3.脚本文件命名格式为版本号,例如 1.0.0.sql、1.0.1.sql 等,版本号应符合语义化版本规范。
/// 4.脚本的属性:复制到输出目录,设置为:始终复制。
/// 5.设置主节点的 Admin.NET.Application 的 Configuration/App.json 的 WorkerId 为 1。
/// 6.设置入口项目 Admin.NET.Web.Entry.csproj 的 Version。
/// ==================================================
/// 更新新版本时
/// 1.需在 UpdateScripts 文件夹中添加新的脚本文件,脚本文件名应为新版本号。
/// 2.设置入口项目 Admin.NET.Web.Entry.csproj 的 Version
/// </remarks>
[SuppressSniffer]
public static class AutoVersionUpdate
{
    /// <summary>
    /// 使用自动版本更新中间件
    /// </summary>
    /// <param name="app"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseAutoVersionUpdate(this IApplicationBuilder app)
    {
        ConsoleLogger.Info("AutoVersionUpdate 中间件运行");
 
        var snowIdOpt = App.GetConfig<SnowIdOptions>("SnowId", true);
        if (snowIdOpt.WorkerId != 1)
        {
            ConsoleLogger.Handle("非主节点,不执行脚本");
            return app;
        }
 
        var currentVersion = GetEntryAssemblyCurrentVersion();
        ConsoleLogger.Handle($"当前版本:{currentVersion}");
 
        var historyVersionInfo = GetEntryAssemblyHistoryVersionInfo();
        var historyVersion = historyVersionInfo.Version;
        var historyDate = historyVersionInfo.Date;
        var historyIsRunScript = historyVersionInfo.IsRunScript;
 
        ConsoleLogger.Handle($"历史版本:{historyVersion},更新时间:{historyDate},是否已执行{historyIsRunScript}");
 
        // 历史版本为空、版本号相同,不执行脚本
        if (historyVersion == string.Empty)
        {
            ConsoleLogger.Handle("历史版本为空,默认为最新版本,不执行脚本");
 
            // 保存当前版本信息
            SetEntryAssemblyCurrentVersion(currentVersion, true);
 
            return app;
        }
        else if (currentVersion.CompareTo(historyVersion) <= 0 && historyIsRunScript)
        {
            ConsoleLogger.Handle("当前版本号与历史版本号相同,且已执行过脚本,不再执行");
 
            // 保存当前版本信息
            SetEntryAssemblyCurrentVersion(currentVersion, false);
 
            return app;
        }
        else
        {
            ConsoleLogger.Handle("当前版本号与历史版本号不同,或版本号相同但未执行过脚本,开始执行脚本");
 
            var scriptSqlVersions = GetScriptSqlVersions();
 
            // 若不存在当前版本的脚本,则只保存当前版本信息,不执行脚本
            if (scriptSqlVersions.All(s => s.Version.CompareTo(currentVersion) < 0))
            {
                ConsoleLogger.Handle("不存在当前版本的脚本,只保存当前版本信息,不执行脚本");
 
                // 保存当前版本信息
                SetEntryAssemblyCurrentVersion(currentVersion, false);
 
                return app;
            }
 
            // 执行脚本
            foreach (var sqlFileInfo in scriptSqlVersions)
            {
                var sqlVersion = sqlFileInfo.Version;
 
                // 只执行大于历史版本的脚本,或者当前版本但未执行过
                if (sqlVersion.CompareTo(historyVersion) < 0)
                {
                    ConsoleLogger.Handle($"版本{sqlVersion}低于历史版本,跳过");
                    continue;
                }
                if (sqlVersion == historyVersion && historyIsRunScript)
                {
                    ConsoleLogger.Handle($"版本{sqlVersion}等于历史版本,且已执行过脚本,跳过");
                    continue;
                }
 
                // 执行脚本
                var sql = File.ReadAllText(sqlFileInfo.FilePath);
                if (sql != null)
                {
                    ConsoleLogger.Handle($"执行版本{sqlVersion}脚本");
 
                    HandleSqlScript(app, sql, sqlVersion);
                }
            }
        }
 
        ConsoleLogger.Success("AutoVersionUpdate 中间件结束");
 
        return app;
    }
 
    #region 辅助方法
 
    /// <summary>
    /// 获取入口程序集当前版本信息
    /// </summary>
    /// <returns></returns>
    private static string GetEntryAssemblyCurrentVersion()
    {
        var entryAssemblyVersion = AssemblyHelper.GetEntryAssemblyVersion();
        return entryAssemblyVersion.ToString(3);
    }
 
    /// <summary>
    /// 设置入口程序集当前版本信息
    /// </summary>
    /// <param name="version"></param>
    /// <param name="isRunScript"></param>
    private static void SetEntryAssemblyCurrentVersion(string version, bool isRunScript)
    {
        var path = Path.Combine(AppContext.BaseDirectory, "version.txt");
        var now = DateTime.Now;
        File.WriteAllText(path, $"{version}^{now:yyyy-MM-dd HH:mm:ss}^{isRunScript}");
    }
 
    /// <summary>
    /// 获取入口程序集上一次运行版本信息
    /// </summary>
    /// <returns></returns>
    private static HistoryVersionInfo GetEntryAssemblyHistoryVersionInfo()
    {
        var path = Path.Combine(AppContext.BaseDirectory, "version.txt");
 
        // 检查文件是否存在
        if (File.Exists(path))
        {
            // 文件存在时读取内容
            var info = File.ReadAllText(path);
 
            if (info.Contains('^'))
            {
                var parts = info.Split('^');
                var version = parts.Length > 0 ? parts[0].ToString() : string.Empty;
                var date = parts.Length > 1 ? parts[1] : string.Empty;
                var isRunScript = parts.Length > 2 ? parts[2].ToBoolean() : false;
 
                return new HistoryVersionInfo(version, date, isRunScript);
            }
        }
 
        // 文件不存在或内容格式不正确时返回默认值
        return new HistoryVersionInfo(string.Empty, string.Empty, false);
    }
 
    /// <summary>
    /// 获取程序目录下的脚本 SQL 文件版本
    /// </summary>
    /// <returns></returns>
    private static List<SqlFileInfo> GetScriptSqlVersions()
    {
        // 获取所有脚本文件
        var path = Path.Combine(AppContext.BaseDirectory, "UpdateScripts");
        var scriptFiles = Directory.GetFiles(path, "*.sql").ToList();
 
        var sqlVersions = scriptFiles
            .Select(s => new SqlFileInfo(Path.GetFileNameWithoutExtension(s), s))
            .OrderBy(s => s.Version).ToList();
        return sqlVersions;
    }
 
    /// <summary>
    /// 保存当前版本信息
    /// </summary>
    /// <param name="app"></param>
    /// <param name="sql"></param>
    /// <param name="sqlVersion"></param>
    private static void HandleSqlScript(IApplicationBuilder app, string sql, string sqlVersion)
    {
        using var scope = App.GetRequiredService<IServiceScopeFactory>().CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
 
        var isSuccess = false;
 
        try
        {
            // 开启事务
            dbContext.Ado.BeginTran();
            dbContext.Ado.ExecuteCommand(sql);
            dbContext.Ado.CommitTran();
            isSuccess = true;
        }
        catch (Exception ex)
        {
            dbContext.Ado.RollbackTran();
            ConsoleLogger.Error($"AutoVersionUpdate 执行 SQL 脚本出错,版本:{sqlVersion},错误:{ex.Message}");
        }
        finally
        {
            if (isSuccess)
            {
                // 保存当前版本信息
                SetEntryAssemblyCurrentVersion(sqlVersion, true);
            }
        }
    }
 
    #endregion 辅助方法
}
 
public record SqlFileInfo(string Version, string FilePath);
public record HistoryVersionInfo(string Version, string Date, bool IsRunScript);
 
#endif // NET9_0_OR_GREATER