944 lines
40 KiB
C#
944 lines
40 KiB
C#
using Dm.util;
|
||
using Mapster;
|
||
using Microsoft.Data.Sqlite;
|
||
using Microsoft.Extensions.Configuration;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using NewLife;
|
||
using Prism.Ioc;
|
||
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Data;
|
||
using System.Diagnostics;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Linq.Dynamic.Core;
|
||
using System.Reflection;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using Yitter.IdGenerator;
|
||
using YY.Admin.Core;
|
||
using YY.Admin.Core.Extension;
|
||
using YY.Admin.Core.Option;
|
||
using YY.Admin.Core.SeedData;
|
||
using YY.Admin.Core.Session;
|
||
using YY.Admin.Core.Util;
|
||
using DbType = SqlSugar.DbType;
|
||
|
||
namespace YY.Admin.Core.SqlSugar
|
||
{
|
||
public static class SqlSugarSetup
|
||
{
|
||
// 多租户实例
|
||
public static ITenant ITenant { get; set; }
|
||
|
||
|
||
// 是否正在处理种子数据
|
||
private static bool _isHandlingSeedData = false;
|
||
public static void AddSqlSugar(this IContainerRegistry containerRegistry,IConfiguration configuration)
|
||
{
|
||
//var _logger = Container.Resolve<ILoggerService>();
|
||
// 直接读取配置
|
||
var dbOptions = configuration.GetSection("DbConnection").Get<DbConnectionOptions>();
|
||
// 注册雪花Id
|
||
var snowIdOpt = new IdGeneratorOptions
|
||
{
|
||
// 扩展 WorkerId 位数至 12 位 (0-4095)
|
||
WorkerIdBitLength = 12,
|
||
|
||
// 减少序列号位数保持 64 位总长
|
||
SeqBitLength = 8,
|
||
|
||
// 调整时间戳位数 (仍可支持 68 年)
|
||
BaseTime = DateTime.Now.AddYears(-30)
|
||
};
|
||
YitIdHelper.SetIdGenerator(snowIdOpt);
|
||
// 自定义 SqlSugar 雪花ID算法
|
||
SnowFlakeSingle.WorkId = snowIdOpt.WorkerId;
|
||
StaticConfig.CustomSnowFlakeFunc = () =>
|
||
{
|
||
return YitIdHelper.NextId();
|
||
};
|
||
// 动态表达式 SqlFunc 支持,https://www.donet5.com/Home/Doc?typeId=2569
|
||
StaticConfig.DynamicExpressionParserType = typeof(DynamicExpressionParser);
|
||
StaticConfig.DynamicExpressionParsingConfig = new ParsingConfig
|
||
{
|
||
CustomTypeProvider = new SqlSugarTypeProvider()
|
||
};
|
||
dbOptions!.ConnectionConfigs = SetDbConfig(dbOptions.ConnectionConfigs);
|
||
|
||
SqlSugarScope sqlSugar = new(dbOptions.ConnectionConfigs.Cast<ConnectionConfig>().ToList(), db =>
|
||
{
|
||
foreach(var config in dbOptions.ConnectionConfigs)
|
||
{
|
||
var dbProvider = db.GetConnectionScope(config.ConfigId);
|
||
SetDbAop(dbProvider, config, dbOptions.EnableConsoleSql, dbOptions.SuperAdminIgnoreIDeletedFilter);
|
||
}
|
||
});
|
||
ITenant = sqlSugar;
|
||
// 注册为单例服务(Prism 方式)
|
||
containerRegistry.RegisterInstance<ISqlSugarClient>(sqlSugar);
|
||
foreach (var config in dbOptions.ConnectionConfigs)
|
||
{
|
||
InitDatabase(sqlSugar, config);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 配置Aop
|
||
/// </summary>
|
||
/// <param name="db"></param>
|
||
/// <param name="enableConsoleSql"></param>
|
||
/// <param name="superAdminIgnoreIDeletedFilter"></param>
|
||
public static void SetDbAop(SqlSugarScopeProvider db, DbConnectionConfig config, bool enableConsoleSql, bool superAdminIgnoreIDeletedFilter)
|
||
{
|
||
// 设置超时时间
|
||
db.Ado.CommandTimeOut = 30;
|
||
|
||
// 打印SQL语句
|
||
if (enableConsoleSql)
|
||
{
|
||
db.Aop.OnLogExecuting = (sql, pars) =>
|
||
{
|
||
//var log = $"【{DateTime.Now}——执行SQL】\r\n{UtilMethods.GetNativeSql(sql, pars)}\r\n";
|
||
var log = $"【{DateTime.Now}——执行SQL】\r\n{UtilMethods.GetSqlString(dbType: config.DbType, sql, pars)}\r\n";
|
||
var originColor = Console.ForegroundColor;
|
||
if (sql.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase))
|
||
Console.ForegroundColor = ConsoleColor.Green;
|
||
if (sql.StartsWith("UPDATE", StringComparison.OrdinalIgnoreCase) || sql.StartsWith("INSERT", StringComparison.OrdinalIgnoreCase))
|
||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||
if (sql.StartsWith("DELETE", StringComparison.OrdinalIgnoreCase))
|
||
Console.ForegroundColor = ConsoleColor.Red;
|
||
Debug.WriteLine(log);
|
||
Console.ForegroundColor = originColor;
|
||
};
|
||
}
|
||
db.Aop.OnError = ex =>
|
||
{
|
||
if (ex.Parametres == null) return;
|
||
var log = $"【{DateTime.Now}——错误SQL】\r\n{UtilMethods.GetSqlString(config.DbType, ex.Sql, (SugarParameter[])ex.Parametres)}\r\n";
|
||
Debug.WriteLine(log);
|
||
// logger.Error(log, ex);
|
||
};
|
||
db.Aop.OnLogExecuted = (sql, pars) =>
|
||
{
|
||
// 执行时间超过5秒时
|
||
if (!(db.Ado.SqlExecutionTime.TotalSeconds > 5)) return;
|
||
|
||
var fileName = db.Ado.SqlStackTrace.FirstFileName; // 文件名
|
||
var fileLine = db.Ado.SqlStackTrace.FirstLine; // 行号
|
||
var firstMethodName = db.Ado.SqlStackTrace.FirstMethodName; // 方法名
|
||
var log = $"【{DateTime.Now}——超时SQL】\r\n【所在文件名】:{fileName}\r\n【代码行数】:{fileLine}\r\n【方法名】:{firstMethodName}\r\n" + $"【SQL语句】:{UtilMethods.GetNativeSql(sql, pars)}";
|
||
Debug.WriteLine(log);
|
||
// logger.Warning(log);
|
||
};
|
||
var currentUser = AppSession.CurrentUser;
|
||
|
||
var isSuperAdmin = currentUser?.AccountType == AccountTypeEnum.SuperAdmin;
|
||
|
||
// 配置假删除过滤器,如果当前用户是超级管理员并且允许忽略软删除过滤器则不会应用
|
||
if (!isSuperAdmin || !superAdminIgnoreIDeletedFilter)
|
||
db.QueryFilter.AddTableFilter<IDeletedFilter>(u => u.IsDelete == false);
|
||
|
||
// 超级管理员排除其他过滤器
|
||
if (isSuperAdmin) return;
|
||
|
||
// 配置租户过滤器
|
||
var tenantId = currentUser?.TenantId ?? 0;
|
||
if (tenantId > 0)
|
||
db.QueryFilter.AddTableFilter<ITenantIdFilter>(u => u.TenantId == tenantId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 配置连接属性
|
||
/// </summary>
|
||
/// <param name="config"></param>
|
||
/// <returns></returns>
|
||
public static List<DbConnectionConfig> SetDbConfig(List<DbConnectionConfig> configs)
|
||
{
|
||
foreach (var config in configs)
|
||
{
|
||
if (config.DbSettings.EnableConnStringEncrypt)
|
||
{
|
||
config.ConnectionString = CryptogramUtil.Decrypt(config.ConnectionString);
|
||
}
|
||
|
||
// SQLite 相对路径会解析到 LocalApplicationData\Data(见 ResolveSqliteConnectionToAbsolutePath)
|
||
if (config.DbType == DbType.Sqlite && !string.IsNullOrWhiteSpace(config.ConnectionString))
|
||
{
|
||
// 安装包随带的 Admin.NET.db / Slave.db 在安装目录;运行时代码只读写可写目录。首启若无用户库则从安装目录复制模板。
|
||
TrySeedSqliteFromInstallDirectory(config.ConnectionString);
|
||
config.ConnectionString = ResolveSqliteConnectionToAbsolutePath(config.ConnectionString);
|
||
}
|
||
|
||
var configureExternalServices = new ConfigureExternalServices
|
||
{
|
||
EntityNameService = (type, entity) => // 处理表
|
||
{
|
||
entity.IsDisabledDelete = true; // 禁止删除非 sqlsugar 创建的列
|
||
// 只处理贴了特性[SugarTable]表
|
||
if (!type.GetCustomAttributes<SugarTable>().Any())
|
||
return;
|
||
if (config.DbSettings.EnableUnderLine && !entity.DbTableName.Contains('_'))
|
||
entity.DbTableName = UtilMethods.ToUnderLine(entity.DbTableName); // 驼峰转下划线
|
||
},
|
||
EntityService = (type, column) => // 处理列
|
||
{
|
||
// 只处理贴了特性[SugarColumn]列
|
||
if (!type.GetCustomAttributes<SugarColumn>().Any())
|
||
{
|
||
return;
|
||
}
|
||
if (new NullabilityInfoContext().Create(type).WriteState is NullabilityState.Nullable)
|
||
{
|
||
|
||
column.IsNullable = true;
|
||
}
|
||
if (config.DbSettings.EnableUnderLine && !column.IsIgnore && !column.DbColumnName.Contains('_'))
|
||
{
|
||
|
||
column.DbColumnName = UtilMethods.ToUnderLine(column.DbColumnName); // 驼峰转下划线
|
||
}
|
||
},
|
||
};
|
||
config.ConfigureExternalServices = configureExternalServices;
|
||
config.InitKeyType = InitKeyType.Attribute;
|
||
config.IsAutoCloseConnection = true;
|
||
config.MoreSettings = new ConnMoreSettings
|
||
{
|
||
IsAutoRemoveDataCache = true, // 启用自动删除缓存,所有增删改会自动调用.RemoveDataCache()
|
||
IsAutoDeleteQueryFilter = true, // 启用删除查询过滤器
|
||
IsAutoUpdateQueryFilter = true, // 启用更新查询过滤器
|
||
SqlServerCodeFirstNvarchar = true // 采用Nvarchar
|
||
};
|
||
|
||
// 若库类型是人大金仓则默认设置PG模式
|
||
if (config.DbType == DbType.Kdbndp)
|
||
config.MoreSettings.DatabaseModel = DbType.PostgreSQL; // 配置PG模式主要是兼容系统表差异
|
||
|
||
// 若库类型是Oracle则默认主键名字和参数名字最大长度
|
||
if (config.DbType == DbType.Oracle)
|
||
config.MoreSettings.MaxParameterNameLength = 30;
|
||
}
|
||
|
||
return configs;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化数据库
|
||
/// </summary>
|
||
/// <param name="db">SqlSugarScope 实例</param>
|
||
/// <param name="config">数据库连接配置</param>
|
||
private static void InitDatabase(SqlSugarScope db, DbConnectionConfig config)
|
||
{
|
||
var dbProvider = db.GetConnectionScope(config.ConfigId);
|
||
|
||
// 等待数据库连接就绪
|
||
WaitForDatabaseReady(dbProvider);
|
||
|
||
// 初始化数据库
|
||
if (config.DbSettings.EnableInitDb)
|
||
{
|
||
//Log.Information($"初始化数据库 {config.DbType} - {config.ConfigId} - {config.ConnectionString}");
|
||
if (config.DbType != DbType.Oracle) dbProvider.DbMaintenance.CreateDatabase();
|
||
}
|
||
|
||
// 初始化表结构
|
||
if (config.TableSettings.EnableInitTable)
|
||
{
|
||
//Log.Information($"初始化表结构 {config.DbType} - {config.ConfigId}");
|
||
var entityTypes = GetEntityTypesForInit(config);
|
||
InitializeTables(dbProvider, entityTypes, config);
|
||
}
|
||
|
||
// 兼容旧库:曾关闭表初始化或升级前库无 Jeecg 盐列时补齐,避免脱网按 Jeecg 规则验密时读列失败
|
||
EnsureSysUserJeecgPasswordSaltColumn(dbProvider, config);
|
||
// 兼容旧库:菜单表增加「桌面默认首页」标记列
|
||
EnsureSysMenuDefaultDesktopHomeColumn(dbProvider, config);
|
||
// 兜底:确保 Jeecg 用户同构表存在(登录页“同步 Jeecg 用户”写入此表)
|
||
EnsureJeecgSysUserMirrorTable(dbProvider, config);
|
||
// 兜底:确保 Jeecg 数据字典同构表存在(数据字典页面“同步数据字典”写入此表)
|
||
EnsureJeecgSysDictItemMirrorTable(dbProvider, config);
|
||
//// 初始化视图
|
||
//if (config.DbSettings.EnableInitView) InitView(dbProvider);
|
||
// 初始化种子数据
|
||
if (config.SeedSettings.EnableInitSeed) InitSeedData(db, config);
|
||
// 关闭全量种子时首启可能无菜单数据;补一份基准菜单,避免打包版本左侧空白
|
||
EnsureBaselineSysMenuSeed(db, config);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 首次启动时:若用户数据目录中尚无该 SQLite 文件,且安装目录(exe 旁)存在同名库,则复制一份作为初始模板。
|
||
/// 否则安装包内随带的菜单/配置永远不会被用到(实际始终读写 %LocalAppData%\YY.Admin\Data\)。
|
||
/// </summary>
|
||
private static void TrySeedSqliteFromInstallDirectory(string connectionString)
|
||
{
|
||
try
|
||
{
|
||
var builder = new SqliteConnectionStringBuilder(connectionString);
|
||
var ds = builder.DataSource;
|
||
if (string.IsNullOrWhiteSpace(ds) || Path.IsPathFullyQualified(ds))
|
||
return;
|
||
|
||
var fileName = Path.GetFileName(ds.TrimStart('.', '/', '\\'));
|
||
if (string.IsNullOrWhiteSpace(fileName))
|
||
fileName = "Admin.NET.db";
|
||
|
||
var dataDir = AppWritablePaths.EnsureDirectoryExists(AppWritablePaths.DataDirectory);
|
||
var targetPath = Path.Combine(dataDir, fileName);
|
||
if (File.Exists(targetPath))
|
||
return;
|
||
|
||
var bundledPath = Path.Combine(AppContext.BaseDirectory, fileName);
|
||
if (!File.Exists(bundledPath))
|
||
return;
|
||
|
||
File.Copy(bundledPath, targetPath, overwrite: false);
|
||
}
|
||
catch
|
||
{
|
||
// 忽略:后续仍可按空库走建表/基准种子
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将 SQLite 连接串中的相对 DataSource 解析为 %LocalAppData%\YY.Admin\Data\ 下绝对路径(适配 Program Files 只读安装)。
|
||
/// </summary>
|
||
private static string ResolveSqliteConnectionToAbsolutePath(string connectionString)
|
||
{
|
||
try
|
||
{
|
||
var builder = new SqliteConnectionStringBuilder(connectionString);
|
||
var ds = builder.DataSource;
|
||
if (string.IsNullOrWhiteSpace(ds))
|
||
{
|
||
return connectionString;
|
||
}
|
||
|
||
if (Path.IsPathFullyQualified(ds))
|
||
{
|
||
return connectionString;
|
||
}
|
||
|
||
// Program Files 等安装目录对普通用户只读;运行期 SQLite 放到 LocalApplicationData(首启可由 TrySeedSqliteFromInstallDirectory 从安装目录拷贝模板)
|
||
var dataDir = AppWritablePaths.EnsureDirectoryExists(AppWritablePaths.DataDirectory);
|
||
var fileName = Path.GetFileName(ds.TrimStart('.', '/', '\\'));
|
||
if (string.IsNullOrWhiteSpace(fileName))
|
||
{
|
||
fileName = "Admin.NET.db";
|
||
}
|
||
|
||
builder.DataSource = Path.Combine(dataDir, fileName);
|
||
return builder.ConnectionString;
|
||
}
|
||
catch
|
||
{
|
||
return connectionString;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 未启用 EnableInitSeed 且 sys_menu 为空时,写入 SysMenu 种子,避免发布后界面无功能菜单。
|
||
/// </summary>
|
||
private static void EnsureBaselineSysMenuSeed(SqlSugarScope db, DbConnectionConfig config)
|
||
{
|
||
try
|
||
{
|
||
if (config.SeedSettings.EnableInitSeed)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!string.Equals(config.ConfigId.ToString(), SqlSugarConst.MainConfigId, StringComparison.Ordinal))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (config.DbType != DbType.Sqlite)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var dbProvider = db.GetConnectionScope(config.ConfigId);
|
||
var menuEntityInfo = dbProvider.EntityMaintenance.GetEntityInfo(typeof(SysMenu));
|
||
if (!dbProvider.DbMaintenance.IsAnyTable(menuEntityInfo.DbTableName, false))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var cnt = dbProvider.Queryable<SysMenu>().ClearFilter().Count();
|
||
if (cnt > 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var seedType = typeof(SysMenuSeedData);
|
||
var seedData = GetSeedData(seedType)?.ToList();
|
||
if (seedData == null || seedData.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
AdjustSeedDataIds(seedData, config);
|
||
var progress = 0;
|
||
InsertOrUpdateSeedData(dbProvider, seedType, typeof(SysMenu), seedData, config, ref progress, 1);
|
||
}
|
||
catch
|
||
{
|
||
// 启动阶段不因兜底种子失败而阻断
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 若 sys_menu 缺少桌面默认首页标记列,则 ALTER 补齐(与实体 SysMenu.IsDefaultDesktopHome 一致)
|
||
/// </summary>
|
||
private static void EnsureSysMenuDefaultDesktopHomeColumn(SqlSugarScopeProvider dbProvider, DbConnectionConfig config)
|
||
{
|
||
try
|
||
{
|
||
var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(typeof(SysMenu));
|
||
var tableName = entityInfo.DbTableName;
|
||
if (!dbProvider.DbMaintenance.IsAnyTable(tableName, false))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var col = entityInfo.Columns.FirstOrDefault(c => c.PropertyName == nameof(SysMenu.IsDefaultDesktopHome));
|
||
if (col == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var columns = dbProvider.DbMaintenance.GetColumnInfosByTableName(tableName, false);
|
||
if (columns != null &&
|
||
columns.Any(c => string.Equals(c.DbColumnName, col.DbColumnName, StringComparison.OrdinalIgnoreCase)))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var cName = col.DbColumnName;
|
||
var sql = config.DbType switch
|
||
{
|
||
DbType.Sqlite => $"ALTER TABLE {tableName} ADD COLUMN {cName} INTEGER NOT NULL DEFAULT 0;",
|
||
DbType.MySql => $"ALTER TABLE `{tableName}` ADD COLUMN `{cName}` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '桌面端默认首页';",
|
||
DbType.PostgreSQL => $"ALTER TABLE \"{tableName}\" ADD COLUMN IF NOT EXISTS {cName} BOOLEAN NOT NULL DEFAULT FALSE;",
|
||
DbType.SqlServer => $"ALTER TABLE [{tableName}] ADD [{cName}] BIT NOT NULL DEFAULT 0;",
|
||
DbType.Kdbndp => $"ALTER TABLE \"{tableName}\" ADD COLUMN IF NOT EXISTS {cName} BOOLEAN NOT NULL DEFAULT FALSE;",
|
||
DbType.Dm => $"ALTER TABLE {tableName} ADD {cName} NUMBER(1) DEFAULT 0 NOT NULL;",
|
||
_ => (string?)null
|
||
};
|
||
|
||
if (string.IsNullOrEmpty(sql))
|
||
{
|
||
return;
|
||
}
|
||
|
||
Retry(() => dbProvider.Ado.ExecuteCommand(sql), maxRetry: 3, retryIntervalMs: 1000);
|
||
}
|
||
catch
|
||
{
|
||
// 无权限、从库只读等场景不阻断启动
|
||
}
|
||
}
|
||
|
||
private static void EnsureSysUserJeecgPasswordSaltColumn(SqlSugarScopeProvider dbProvider, DbConnectionConfig config)
|
||
{
|
||
try
|
||
{
|
||
var tableName = dbProvider.EntityMaintenance.GetEntityInfo(typeof(SysUser)).DbTableName;
|
||
if (!dbProvider.DbMaintenance.IsAnyTable(tableName, false))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var columns = dbProvider.DbMaintenance.GetColumnInfosByTableName(tableName);
|
||
if (columns != null && columns.Any(c => string.Equals(c.DbColumnName, "jeecg_password_salt", StringComparison.OrdinalIgnoreCase)))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var sql = config.DbType switch
|
||
{
|
||
DbType.Sqlite => $"ALTER TABLE {tableName} ADD COLUMN jeecg_password_salt TEXT NULL;",
|
||
DbType.MySql => $"ALTER TABLE `{tableName}` ADD COLUMN `jeecg_password_salt` VARCHAR(64) NULL COMMENT 'Jeecg密码盐';",
|
||
DbType.PostgreSQL => $"ALTER TABLE \"{tableName}\" ADD COLUMN IF NOT EXISTS jeecg_password_salt VARCHAR(64) NULL;",
|
||
DbType.SqlServer => $"ALTER TABLE [{tableName}] ADD [jeecg_password_salt] NVARCHAR(64) NULL;",
|
||
DbType.Kdbndp => $"ALTER TABLE \"{tableName}\" ADD COLUMN IF NOT EXISTS jeecg_password_salt VARCHAR(64) NULL;",
|
||
DbType.Dm => $"ALTER TABLE {tableName} ADD jeecg_password_salt VARCHAR(64) NULL;",
|
||
_ => (string?)null
|
||
};
|
||
|
||
if (string.IsNullOrEmpty(sql))
|
||
{
|
||
return;
|
||
}
|
||
|
||
Retry(() => dbProvider.Ado.ExecuteCommand(sql), maxRetry: 3, retryIntervalMs: 1000);
|
||
}
|
||
catch
|
||
{
|
||
// 无权限、从库只读等场景不阻断启动
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 若缺少 Jeecg 同构用户表,则启动时强制创建。
|
||
/// 说明:用于规避某些环境 AppDomain 扫描不到新实体导致 CodeFirst 漏建的问题。
|
||
/// </summary>
|
||
private static void EnsureJeecgSysUserMirrorTable(SqlSugarScopeProvider dbProvider, DbConnectionConfig config)
|
||
{
|
||
try
|
||
{
|
||
var tableName = dbProvider.EntityMaintenance.GetEntityInfo(typeof(JeecgSysUser)).DbTableName;
|
||
if (dbProvider.DbMaintenance.IsAnyTable(tableName, false))
|
||
{
|
||
return;
|
||
}
|
||
|
||
Retry(() =>
|
||
{
|
||
dbProvider.CodeFirst.InitTables(typeof(JeecgSysUser));
|
||
}, maxRetry: 3, retryIntervalMs: 1000);
|
||
|
||
// CodeFirst 仍未创建时,针对 SQLite 做 SQL 级兜底,确保表可见
|
||
if (!dbProvider.DbMaintenance.IsAnyTable(tableName, false) && config.DbType == DbType.Sqlite)
|
||
{
|
||
const string createSql = @"
|
||
CREATE TABLE IF NOT EXISTS jeecg_sys_user (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
username TEXT NULL,
|
||
realname TEXT NULL,
|
||
password TEXT NULL,
|
||
salt TEXT NULL,
|
||
avatar TEXT NULL,
|
||
birthday TEXT NULL,
|
||
sex INTEGER NULL,
|
||
email TEXT NULL,
|
||
phone TEXT NULL,
|
||
org_code TEXT NULL,
|
||
login_tenant_id INTEGER NULL,
|
||
status INTEGER NULL,
|
||
del_flag INTEGER NULL,
|
||
work_no TEXT NULL,
|
||
telephone TEXT NULL,
|
||
create_by TEXT NULL,
|
||
create_time TEXT NULL,
|
||
update_by TEXT NULL,
|
||
update_time TEXT NULL,
|
||
activiti_sync INTEGER NULL,
|
||
user_identity INTEGER NULL,
|
||
depart_ids TEXT NULL,
|
||
client_id TEXT NULL,
|
||
bpm_status TEXT NULL,
|
||
sign TEXT NULL,
|
||
sign_enable INTEGER NULL,
|
||
main_dep_post_id TEXT NULL,
|
||
position_type TEXT NULL,
|
||
last_pwd_update_time TEXT NULL,
|
||
sort INTEGER NULL,
|
||
iz_hide_contact TEXT NULL
|
||
);";
|
||
Retry(() => dbProvider.Ado.ExecuteCommand(createSql), maxRetry: 3, retryIntervalMs: 1000);
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 兜底逻辑不阻断启动
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 若缺少 Jeecg 同构数据字典项表,则启动时强制创建。
|
||
/// </summary>
|
||
private static void EnsureJeecgSysDictItemMirrorTable(SqlSugarScopeProvider dbProvider, DbConnectionConfig config)
|
||
{
|
||
try
|
||
{
|
||
var tableName = dbProvider.EntityMaintenance.GetEntityInfo(typeof(JeecgSysDictItem)).DbTableName;
|
||
if (dbProvider.DbMaintenance.IsAnyTable(tableName, false))
|
||
{
|
||
return;
|
||
}
|
||
|
||
Retry(() =>
|
||
{
|
||
dbProvider.CodeFirst.InitTables(typeof(JeecgSysDictItem));
|
||
}, maxRetry: 3, retryIntervalMs: 1000);
|
||
|
||
if (!dbProvider.DbMaintenance.IsAnyTable(tableName, false) && config.DbType == DbType.Sqlite)
|
||
{
|
||
const string createSql = @"
|
||
CREATE TABLE IF NOT EXISTS jeecg_sys_dict_item (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
dict_id TEXT NULL,
|
||
dict_name TEXT NULL,
|
||
dict_code TEXT NULL,
|
||
dict_type INTEGER NULL,
|
||
dict_description TEXT NULL,
|
||
item_text TEXT NULL,
|
||
item_value TEXT NULL,
|
||
item_description TEXT NULL,
|
||
sort_order INTEGER NULL,
|
||
status INTEGER NULL,
|
||
item_color TEXT NULL,
|
||
create_by TEXT NULL,
|
||
create_time TEXT NULL,
|
||
update_by TEXT NULL,
|
||
update_time TEXT NULL
|
||
);";
|
||
Retry(() => dbProvider.Ado.ExecuteCommand(createSql), maxRetry: 3, retryIntervalMs: 1000);
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 兜底逻辑不阻断启动
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化种子数据
|
||
/// </summary>
|
||
/// <param name="db">SqlSugarScope 实例</param>
|
||
/// <param name="config">数据库连接配置</param>
|
||
private static void InitSeedData(SqlSugarScope db, DbConnectionConfig config)
|
||
{
|
||
var dbProvider = db.GetConnectionScope(config.ConfigId);
|
||
_isHandlingSeedData = true;
|
||
|
||
// Log.Information($"初始化种子数据 {config.DbType} - {config.ConfigId}");
|
||
var seedDataTypes = GetSeedDataTypes(config);
|
||
|
||
int count = 0, sum = seedDataTypes.Count;
|
||
var tasks = seedDataTypes.Select(seedType => Task.Run(() =>
|
||
{
|
||
var entityType = seedType.GetInterfaces().First().GetGenericArguments().First();
|
||
if (!IsEntityForConfig(entityType, config)) return;
|
||
|
||
var seedData = GetSeedData(seedType);
|
||
if (seedData == null) return;
|
||
|
||
AdjustSeedDataIds(seedData, config);
|
||
InsertOrUpdateSeedData(dbProvider, seedType, entityType, seedData, config, ref count, sum);
|
||
}));
|
||
|
||
Task.WhenAll(tasks).GetAwaiter().GetResult();
|
||
_isHandlingSeedData = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取需要初始化的实体类型
|
||
/// </summary>
|
||
/// <param name="config">数据库连接配置</param>
|
||
/// <returns>实体类型列表</returns>
|
||
/// <summary>
|
||
/// 获取需要初始化的实体类型
|
||
/// </summary>
|
||
/// <param name="config">数据库连接配置</param>
|
||
/// <returns>实体类型列表</returns>
|
||
private static List<Type> GetEntityTypesForInit(DbConnectionConfig config)
|
||
{
|
||
// 获取当前应用程序域中的所有程序集
|
||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||
|
||
// 收集所有符合条件的类型
|
||
var allTypes = new List<Type>();
|
||
foreach (var assembly in assemblies)
|
||
{
|
||
try
|
||
{
|
||
// 跳过动态程序集(通常不包含用户定义的实体类)
|
||
if (assembly.IsDynamic) continue;
|
||
|
||
// 获取程序集中定义的所有类型
|
||
var types = assembly.GetTypes();
|
||
allTypes.AddRange(types);
|
||
}
|
||
catch (ReflectionTypeLoadException ex)
|
||
{
|
||
// 处理部分类型加载失败的情况
|
||
var loadedTypes = ex.Types.Where(t => t != null);
|
||
allTypes.AddRange(loadedTypes!);
|
||
}
|
||
catch
|
||
{
|
||
// 忽略无法加载的程序集
|
||
}
|
||
}
|
||
|
||
// 过滤类型
|
||
return allTypes
|
||
.Where(u => u != null)
|
||
.Where(u => u.IsClass && !u.IsAbstract && !u.IsInterface)
|
||
.Where(u => u.IsDefined(typeof(SugarTable), false))
|
||
.Where(u => !u.IsDefined(typeof(IgnoreTableAttribute), false))
|
||
.WhereIF(config.TableSettings.EnableIncreTable, u => u.IsDefined(typeof(IncreTableAttribute), false))
|
||
.Where(u => IsEntityForConfig(u, config))
|
||
.ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取种子数据类型
|
||
/// </summary>
|
||
/// <param name="config">数据库连接配置</param>
|
||
/// <returns>种子数据类型列表</returns>
|
||
private static List<Type> GetSeedDataTypes(DbConnectionConfig config)
|
||
{
|
||
// 获取所有程序集及其类型
|
||
var allTypes = AppDomain.CurrentDomain.GetAssemblies()
|
||
.Where(a => !a.IsDynamic) // 跳过动态程序集
|
||
.SelectMany(assembly =>
|
||
{
|
||
try
|
||
{
|
||
return assembly.GetTypes();
|
||
}
|
||
catch (ReflectionTypeLoadException ex)
|
||
{
|
||
// 处理部分类型加载失败的情况
|
||
return ex.Types?.Where(t => t != null) ?? Enumerable.Empty<Type>();
|
||
}
|
||
catch
|
||
{
|
||
// 忽略无法加载的程序集
|
||
return Enumerable.Empty<Type>();
|
||
}
|
||
})
|
||
.Where(t => t != null)
|
||
.ToList();
|
||
|
||
// 过滤种子数据类型
|
||
var seedTypes = allTypes
|
||
.Where(u => u!.IsClass && !u.IsAbstract && !u.IsInterface)
|
||
.Where(u => u!.HasImplementedRawGeneric(typeof(ISqlSugarEntitySeedData<>)))
|
||
.WhereIF(config.SeedSettings.EnableIncreSeed,
|
||
u => u!.IsDefined(typeof(IncreSeedAttribute), false))
|
||
.OrderBy(u =>
|
||
{
|
||
var seedAttrs = u!.GetCustomAttributes(typeof(SeedDataAttribute), false);
|
||
return seedAttrs.Length > 0 ? ((SeedDataAttribute)seedAttrs[0]).Order : 0;
|
||
})
|
||
.ToList();
|
||
|
||
return seedTypes!;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断实体是否属于当前配置
|
||
/// </summary>
|
||
/// <param name="entityType">实体类型</param>
|
||
/// <param name="config">数据库连接配置</param>
|
||
/// <returns>是否属于当前配置</returns>
|
||
private static bool IsEntityForConfig(Type entityType, DbConnectionConfig config)
|
||
{
|
||
switch (config.ConfigId.ToString())
|
||
{
|
||
case SqlSugarConst.MainConfigId:
|
||
return entityType.GetCustomAttributes<SysTableAttribute>().Any() ||
|
||
(!entityType.GetCustomAttributes<LogTableAttribute>().Any() &&
|
||
!entityType.GetCustomAttributes<TenantAttribute>().Any());
|
||
|
||
case SqlSugarConst.LogConfigId:
|
||
return entityType.GetCustomAttributes<LogTableAttribute>().Any();
|
||
|
||
default:
|
||
{
|
||
var tenantAttribute = entityType.GetCustomAttribute<TenantAttribute>();
|
||
return tenantAttribute != null && tenantAttribute.configId.ToString() == config.ConfigId.ToString();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化表结构
|
||
/// </summary>
|
||
/// <param name="dbProvider">SqlSugarScopeProvider 实例</param>
|
||
/// <param name="entityTypes">实体类型列表</param>
|
||
/// <param name="config">数据库连接配置</param>
|
||
private static void InitializeTables(SqlSugarScopeProvider dbProvider, List<Type> entityTypes, DbConnectionConfig config)
|
||
{
|
||
int count = 0, sum = entityTypes.Count;
|
||
var tasks = entityTypes.Select(entityType => Task.Run(() =>
|
||
{
|
||
Console.WriteLine($"初始化表结构 {entityType.FullName,-64} ({config.ConfigId} - {Interlocked.Increment(ref count):D003}/{sum:D003})");
|
||
UpdateNullableColumns(dbProvider, entityType);
|
||
InitializeTable(dbProvider, entityType);
|
||
}));
|
||
|
||
Task.WhenAll(tasks).GetAwaiter().GetResult();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新表中不存在于实体的字段为可空
|
||
/// </summary>
|
||
/// <param name="dbProvider">SqlSugarScopeProvider 实例</param>
|
||
/// <param name="entityType">实体类型</param>
|
||
private static void UpdateNullableColumns(SqlSugarScopeProvider dbProvider, Type entityType)
|
||
{
|
||
var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(entityType);
|
||
var dbColumns = dbProvider.DbMaintenance.GetColumnInfosByTableName(entityInfo.DbTableName) ?? new List<DbColumnInfo>();
|
||
|
||
foreach (var dbColumn in dbColumns.Where(c => !c.IsPrimarykey && entityInfo.Columns.All(u => u.DbColumnName != c.DbColumnName)))
|
||
{
|
||
dbColumn.IsNullable = true;
|
||
Retry(() =>
|
||
{
|
||
dbProvider.DbMaintenance.UpdateColumn(entityInfo.DbTableName, dbColumn);
|
||
}, maxRetry: 3, retryIntervalMs: 1000);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 等待数据库就绪
|
||
/// </summary>
|
||
/// <param name="dbProvider"></param>
|
||
private static void WaitForDatabaseReady(SqlSugarScopeProvider db)
|
||
{
|
||
do
|
||
{
|
||
try
|
||
{
|
||
if (db.Ado.Connection.State != ConnectionState.Open)
|
||
db.Ado.Connection.Open();
|
||
|
||
// 如果连接成功,直接返回
|
||
//logger.Information("数据库连接成功。");
|
||
return;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// logger.Warning($"数据库尚未就绪,等待中... 错误:{ex.Message}");
|
||
Thread.Sleep(1000);
|
||
}
|
||
} while (true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 简单的重试机制
|
||
/// </summary>
|
||
/// <param name="action"></param>
|
||
/// <param name="maxRetry"></param>
|
||
/// <param name="retryIntervalMs"></param>
|
||
private static void Retry(Action action, int maxRetry, int retryIntervalMs)
|
||
{
|
||
int attempt = 0;
|
||
while (true)
|
||
{
|
||
try
|
||
{
|
||
action();
|
||
return;
|
||
}
|
||
catch (SqliteException ex) when (ex.SqliteErrorCode == 5) // SQLITE_BUSY
|
||
{
|
||
if (++attempt >= maxRetry)
|
||
{
|
||
// logger.Error($"简单的重试机制:{ex.Message}"); throw;
|
||
}
|
||
//logger.Information($"数据库忙,正在重试... (尝试 {attempt}/{maxRetry})");
|
||
Thread.Sleep(retryIntervalMs);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取种子数据
|
||
/// </summary>
|
||
/// <param name="seedType">种子数据类型</param>
|
||
/// <returns>种子数据列表</returns>
|
||
private static IEnumerable<object> GetSeedData(Type seedType)
|
||
{
|
||
var instance = Activator.CreateInstance(seedType);
|
||
var hasDataMethod = seedType.GetMethod("HasData");
|
||
return ((IEnumerable)hasDataMethod?.Invoke(instance, null)!)?.Cast<object>()!;
|
||
}
|
||
/// <summary>
|
||
/// 插入或更新种子数据
|
||
/// </summary>
|
||
/// <param name="dbProvider">SqlSugarScopeProvider 实例</param>
|
||
/// <param name="seedType">种子数据类型</param>
|
||
/// <param name="entityType">实体类型</param>
|
||
/// <param name="seedData">种子数据列表</param>
|
||
/// <param name="config">数据库连接配置</param>
|
||
/// <param name="count">当前处理的数量</param>
|
||
/// <param name="sum">总数量</param>
|
||
private static void InsertOrUpdateSeedData(SqlSugarScopeProvider dbProvider, Type seedType, Type entityType, IEnumerable<object> seedData, DbConnectionConfig config, ref int count, int sum)
|
||
{
|
||
var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(entityType);
|
||
var dataList = seedData.ToList();
|
||
|
||
if (entityType.GetCustomAttribute<SplitTableAttribute>(true) != null)
|
||
{
|
||
var initMethod = seedType.GetMethod("Init");
|
||
initMethod?.Invoke(Activator.CreateInstance(seedType), new object[] { dbProvider });
|
||
}
|
||
else
|
||
{
|
||
int updateCount = 0, insertCount = 0;
|
||
if (entityInfo.Columns.Any(u => u.IsPrimarykey))
|
||
{
|
||
var storage = dbProvider.StorageableByObject(dataList).ToStorage();
|
||
if (seedType.GetCustomAttribute<IgnoreUpdateSeedAttribute>() == null)
|
||
{
|
||
updateCount = storage.AsUpdateable
|
||
.IgnoreColumns(entityInfo.Columns
|
||
.Where(u => u.PropertyInfo.GetCustomAttribute<IgnoreUpdateSeedColumnAttribute>() != null)
|
||
.Select(u => u.PropertyName).ToArray())
|
||
.ExecuteCommand();
|
||
}
|
||
insertCount = storage.AsInsertable.ExecuteCommand();
|
||
}
|
||
else
|
||
{
|
||
if (!dbProvider.Queryable(entityInfo.DbTableName, entityInfo.DbTableName).Any())
|
||
{
|
||
insertCount = dataList.Count;
|
||
dbProvider.InsertableByObject(dataList).ExecuteCommand();
|
||
}
|
||
}
|
||
Console.WriteLine($"添加数据 {entityInfo.DbTableName,-32} ({config.ConfigId} - {Interlocked.Increment(ref count):D003}/{sum:D003},数据量:{dataList.Count:D003},插入 {insertCount:D003} 条记录,修改 {updateCount:D003} 条记录)");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 调整种子数据的 ID
|
||
/// </summary>
|
||
/// <param name="seedData">种子数据列表</param>
|
||
/// <param name="config">数据库连接配置</param>
|
||
private static void AdjustSeedDataIds(IEnumerable<object> seedData, DbConnectionConfig config)
|
||
{
|
||
var seedId = config.ConfigId.ToLong();
|
||
foreach (var data in seedData)
|
||
{
|
||
var idProperty = data.GetType().GetProperty(nameof(EntityBaseId.Id));
|
||
if (idProperty == null || idProperty.PropertyType != typeof(Int64)) continue;
|
||
|
||
var idValue = idProperty.GetValue(data);
|
||
if (idValue == null || idValue.ToString() == "0" || string.IsNullOrWhiteSpace(idValue.ToString()))
|
||
{
|
||
idProperty.SetValue(data, ++seedId);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化表
|
||
/// </summary>
|
||
/// <param name="dbProvider">SqlSugarScopeProvider 实例</param>
|
||
/// <param name="entityType">实体类型</param>
|
||
private static void InitializeTable(SqlSugarScopeProvider dbProvider, Type entityType)
|
||
{
|
||
Retry(() =>
|
||
{
|
||
if (entityType.GetCustomAttribute<SplitTableAttribute>() == null)
|
||
{
|
||
dbProvider.CodeFirst.InitTables(entityType);
|
||
}
|
||
else
|
||
{
|
||
dbProvider.CodeFirst.SplitTables().InitTables(entityType);
|
||
}
|
||
}, maxRetry: 3, retryIntervalMs: 1000);
|
||
}
|
||
}
|
||
}
|