Files
qhmes/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarSetup.cs

1102 lines
46 KiB
C#
Raw Normal View History

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.Const;
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);
}
2026-05-18 17:31:18 +08:00
// SQLite 相对路径会解析到 LocalApplicationData\Data见 ResolveSqliteConnectionToAbsolutePath
if (config.DbType == DbType.Sqlite && !string.IsNullOrWhiteSpace(config.ConnectionString))
{
2026-05-18 17:31:18 +08:00
// 安装包随带的 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);
// 兼容旧库:补齐桌面端登录设置相关 sys_config 项
EnsureDesktopLoginConfigSeed(dbProvider, config);
//// 初始化视图
//if (config.DbSettings.EnableInitView) InitView(dbProvider);
// 初始化种子数据
if (config.SeedSettings.EnableInitSeed) InitSeedData(db, config);
// 关闭全量种子时首启可能无菜单数据;补一份基准菜单,避免打包版本左侧空白
EnsureBaselineSysMenuSeed(db, config);
// 旧库升级:按种子补全缺失菜单及租户/角色授权(仅插入缺失项)
EnsureIncrementalDesktopMenuSeed(db, config);
}
/// <summary>
2026-05-18 17:31:18 +08:00
/// 首次启动时:若用户数据目录中尚无该 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;
}
2026-05-18 17:31:18 +08:00
// 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 已有数据时,按 SysMenuSeedData 补全缺失菜单,并同步租户菜单、管理员角色菜单授权。
/// </summary>
private static void EnsureIncrementalDesktopMenuSeed(SqlSugarScope db, DbConnectionConfig config)
{
try
{
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;
}
if (dbProvider.Queryable<SysMenu>().ClearFilter().Count() == 0)
{
return;
}
var seedMenus = new SysMenuSeedData().HasData().ToList();
var existingMenuIds = dbProvider.Queryable<SysMenu>().ClearFilter()
.Select(m => m.Id).ToList().ToHashSet();
var missingMenus = seedMenus.Where(m => !existingMenuIds.Contains(m.Id)).ToList();
if (missingMenus.Count == 0)
{
return;
}
foreach (var menu in missingMenus)
{
if (menu.CreateTime == default)
{
menu.CreateTime = DateTime.Parse("2022-02-10 00:00:00");
}
menu.Status = StatusEnum.Enable;
}
dbProvider.Insertable(missingMenus).ExecuteCommand();
const long defaultTenantId = 1300000000001;
var tenantSeed = new SysTenantMenuSeedData().HasData()
.Where(t => t.TenantId == defaultTenantId)
.ToList();
var existingTenantMenuIds = dbProvider.Queryable<SysTenantMenu>()
.Where(t => t.TenantId == defaultTenantId)
.Select(t => t.MenuId)
.ToList()
.ToHashSet();
var tenantMenusToInsert = tenantSeed
.Where(t => missingMenus.Any(m => m.Id == t.MenuId) && !existingTenantMenuIds.Contains(t.MenuId))
.Select(t => new SysTenantMenu { Id = t.MenuId, TenantId = t.TenantId, MenuId = t.MenuId })
.ToList();
if (tenantMenusToInsert.Count > 0)
{
dbProvider.Insertable(tenantMenusToInsert).ExecuteCommand();
}
var adminRole = dbProvider.Queryable<SysRole>().OrderBy(r => r.Id).First();
if (adminRole == null)
{
return;
}
var existingRoleMenuIds = dbProvider.Queryable<SysRoleMenu>()
.Where(r => r.RoleId == adminRole.Id)
.Select(r => r.MenuId)
.ToList()
.ToHashSet();
var roleMenusToInsert = missingMenus
.Where(m => !existingRoleMenuIds.Contains(m.Id))
.Select(m => new SysRoleMenu
{
Id = m.Id + (adminRole.Id % 1300000000000),
RoleId = adminRole.Id,
MenuId = m.Id,
})
.ToList();
if (roleMenusToInsert.Count > 0)
{
dbProvider.Insertable(roleMenusToInsert).ExecuteCommand();
}
}
catch
{
// 启动阶段不因增量菜单失败而阻断
}
}
/// <summary>
/// 兼容旧库:补齐桌面端「登录设置」所需的 sys_config 配置项(升级前库可能缺少这些 code
/// </summary>
private static void EnsureDesktopLoginConfigSeed(SqlSugarScopeProvider dbProvider, DbConnectionConfig config)
{
try
{
var tableName = dbProvider.EntityMaintenance.GetEntityInfo(typeof(SysConfig)).DbTableName;
if (!dbProvider.DbMaintenance.IsAnyTable(tableName, false))
{
return;
}
var loginConfigCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
ConfigConst.SysTokenExpire,
ConfigConst.SysRefreshTokenExpire,
ConfigConst.SysTokenIdleExtendMinutes,
ConfigConst.SysTokenCheckIntervalMinutes,
ConfigConst.SysTokenNeverExpire,
};
var seedRows = new SysConfigSeedData().HasData()
.Where(c => !string.IsNullOrWhiteSpace(c.Code) && loginConfigCodes.Contains(c.Code))
.ToList();
if (seedRows.Count == 0)
{
return;
}
var existingCodes = dbProvider.Queryable<SysConfig>()
.Where(c => loginConfigCodes.Contains(c.Code))
.Select(c => c.Code)
.ToList()
.Where(c => !string.IsNullOrWhiteSpace(c))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var missing = seedRows
.Where(s => !existingCodes.Contains(s.Code!))
.ToList();
if (missing.Count == 0)
{
return;
}
dbProvider.Insertable(missing).ExecuteCommand();
}
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);
}
}
}