// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
#if NET10_0_OR_GREATER
using System.Data;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Builder;
using XiHan.Framework.Utils.Logging;
using XiHan.Framework.Utils.Reflections;
namespace Admin.NET.Core.Update;
///
/// 自动版本更新中间件拓展
///
[SuppressSniffer]
public static class AutoVersionUpdate
{
private const string MigrationLogTable = "sys_db_migration_log";
private const string StatusRunning = "Running";
private const string StatusSuccess = "Success";
private const string StatusFailed = "Failed";
private const string StatusSkipped = "Skipped";
private const string VerifyNotConfigured = "NotConfigured";
private const string VerifySuccess = "Success";
private const string VerifyFailed = "Failed";
///
/// 使用自动版本更新中间件
///
public static IApplicationBuilder UseAutoVersionUpdate(this IApplicationBuilder app)
{
LogHelper.Info("AutoVersionUpdate 中间件运行");
var snowIdOpt = App.GetConfig("SnowId", true);
if (snowIdOpt.WorkerId != 1)
{
LogHelper.Handle("非主节点,不执行脚本");
return app;
}
var stopOnFailure = App.GetConfig("AutoVersionUpdate:StopApplicationOnFailure") ?? true;
try
{
RunPendingMigrations(app);
LogHelper.Success("AutoVersionUpdate 中间件结束");
}
catch (Exception ex)
{
LogHelper.Error($"AutoVersionUpdate 执行失败:{ex.Message}");
if (stopOnFailure)
throw;
}
return app;
}
private static void RunPendingMigrations(IApplicationBuilder app)
{
using var scope = App.GetRequiredService().CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
EnsureMigrationLogTable(db);
var currentVersionText = GetEntryAssemblyCurrentVersion();
var currentVersion = ParseVersion(currentVersionText);
var historyFromTxt = GetEntryAssemblyHistoryVersionInfo();
var databaseName = GetDatabaseName(db);
LogHelper.Handle($"当前版本:{currentVersionText},目标数据库:{databaseName ?? "(unknown)"}");
var scripts = LoadMigrationScripts();
var migrationRows = LoadMigrationLogs(db);
var successVersions = migrationRows
.Where(x => string.Equals(x.Status, StatusSuccess, StringComparison.OrdinalIgnoreCase))
.Select(x => ParseVersion(x.Version))
.ToHashSet();
Version? legacyBoundary = null;
if (migrationRows.Count == 0 &&
!string.IsNullOrWhiteSpace(historyFromTxt.Version) &&
historyFromTxt.IsRunScript &&
Version.TryParse(historyFromTxt.Version, out var legacyVersion))
{
legacyBoundary = legacyVersion;
LogHelper.Handle(
$"检测到 {MigrationLogTable} 为空但 version.txt 存在,legacy 跳过边界:{legacyBoundary}");
}
var pending = scripts
.Where(s => s.ParsedVersion <= currentVersion)
.Where(s => !ShouldSkipScript(s, migrationRows, legacyBoundary))
.OrderBy(s => s.ParsedVersion)
.ToList();
LogHelper.Handle($"发现脚本 {scripts.Count} 个,已成功 {successVersions.Count} 个,待执行 {pending.Count} 个");
if (pending.Count == 0)
{
SetEntryAssemblyCurrentVersion(currentVersionText, true);
return;
}
foreach (var script in pending)
{
ExecuteOneMigrationScript(db, script, databaseName);
}
SetEntryAssemblyCurrentVersion(currentVersionText, true);
}
private static bool ShouldSkipScript(
MigrationScript script,
List migrationRows,
Version? legacyBoundary)
{
var row = migrationRows.FirstOrDefault(x =>
string.Equals(x.Version, script.Version, StringComparison.OrdinalIgnoreCase));
if (row != null)
{
if (string.Equals(row.Status, StatusSuccess, StringComparison.OrdinalIgnoreCase))
{
if (!string.Equals(row.FileHash, script.Hash, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
$"版本脚本 {script.FileName} 的 SHA256 与已成功记录不一致,禁止静默覆盖。请新建更高版本脚本。");
}
LogHelper.Handle($"版本 {script.Version} 已成功且 hash 未变,跳过");
return true;
}
return false;
}
if (legacyBoundary != null && script.ParsedVersion <= legacyBoundary)
{
LogHelper.Handle($"版本 {script.Version} 处于 legacy 边界内,跳过");
return true;
}
return false;
}
private static void ExecuteOneMigrationScript(ISqlSugarClient db, MigrationScript script, string? databaseName)
{
var sql = File.ReadAllText(script.FilePath);
if (string.IsNullOrWhiteSpace(sql))
{
LogHelper.Handle($"版本 {script.Version} 脚本为空,记录 Skipped");
UpsertMigrationLog(db, script, databaseName, StatusSkipped, 0, 0, VerifyNotConfigured, null, 0);
return;
}
if (SqlScriptSplitter.ContainsDelimiterDirective(sql))
{
throw new InvalidOperationException(
$"版本脚本 {script.FileName} 包含 DELIMITER 指令,当前执行器不支持,请改为普通 SQL 或手工执行。");
}
var startedAt = DateTime.Now;
var sw = Stopwatch.StartNew();
UpsertMigrationLog(db, script, databaseName, StatusRunning, 0, 0, VerifyNotConfigured, null, 0);
try
{
LogHelper.Handle($"执行版本 {script.Version} 脚本 {script.FileName},SHA256={script.Hash}");
var executeResult = SqlScriptSplitter.Execute(db, sql);
var verifyStatus = RunVerifyScriptIfExists(db, script);
sw.Stop();
UpsertMigrationLog(
db,
script,
databaseName,
StatusSuccess,
executeResult.StatementCount,
executeResult.AffectedRows,
verifyStatus,
null,
sw.ElapsedMilliseconds);
LogHelper.Handle(
$"版本 {script.Version} 成功:语句 {executeResult.StatementCount} 条,影响行 {executeResult.AffectedRows},校验 {verifyStatus},耗时 {sw.ElapsedMilliseconds}ms");
}
catch (Exception ex)
{
sw.Stop();
var message = BuildExceptionMessage(ex);
UpsertMigrationLog(
db,
script,
databaseName,
StatusFailed,
0,
0,
VerifyFailed,
message,
sw.ElapsedMilliseconds);
LogHelper.Error($"AutoVersionUpdate 版本 {script.Version} 失败:{message}");
throw new InvalidOperationException($"AutoVersionUpdate 版本 {script.Version} 执行失败:{message}", ex);
}
}
private static string RunVerifyScriptIfExists(ISqlSugarClient db, MigrationScript script)
{
var verifyPath = Path.ChangeExtension(script.FilePath, ".verify.sql");
if (!File.Exists(verifyPath))
return VerifyNotConfigured;
var verifySql = File.ReadAllText(verifyPath);
var statements = SqlScriptSplitter.Split(verifySql);
if (statements.Count == 0)
return VerifyNotConfigured;
for (var i = 0; i < statements.Count; i++)
{
var statement = statements[i];
var result = db.Ado.SqlQuerySingle