// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 // // 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 // // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! using Npgsql; namespace Admin.NET.Core.Service; /// /// 系统数据库管理服务 🧩 /// [ApiDescriptionSettings(Order = 250, Description = "数据库管理")] public class SysDatabaseService : IDynamicApiController, ITransient { private readonly UserManager _userManager; private readonly ISqlSugarClient _db; private readonly IViewEngine _viewEngine; private readonly CodeGenOptions _codeGenOptions; private readonly CodeGenStrategyFactory _codeGenStrategyFactory; public SysDatabaseService(UserManager userManager, ISqlSugarClient db, IViewEngine viewEngine, IOptions codeGenOptions, CodeGenStrategyFactory codeGenStrategyFactory) { _userManager = userManager; _db = db; _viewEngine = viewEngine; _codeGenOptions = codeGenOptions.Value; _codeGenStrategyFactory = codeGenStrategyFactory; } /// /// 增加列 🔖 /// /// [ApiDescriptionSettings(Name = "AddColumn"), HttpPost] [DisplayName("增加列")] public void AddColumn(DbColumnInput input) { var column = new DbColumnInfo { ColumnDescription = input.ColumnDescription, DbColumnName = input.DbColumnName, IsIdentity = input.IsIdentity == 1, IsNullable = input.IsNullable == 1, IsPrimarykey = input.IsPrimarykey == 1, Length = input.Length, DecimalDigits = input.DecimalDigits, DataType = input.DataType }; var db = _db.AsTenant().GetConnectionScope(input.ConfigId); db.DbMaintenance.AddColumn(input.TableName, column); // 添加默认值 if (!string.IsNullOrWhiteSpace(input.DefaultValue)) db.DbMaintenance.AddDefaultValue(input.TableName, column.DbColumnName, input.DefaultValue); db.DbMaintenance.AddColumnRemark(input.DbColumnName, input.TableName, input.ColumnDescription); if (column.IsPrimarykey) db.DbMaintenance.AddPrimaryKey(input.TableName, input.DbColumnName); } /// /// 删除列 🔖 /// /// [ApiDescriptionSettings(Name = "DeleteColumn"), HttpPost] [DisplayName("删除列")] public void DeleteColumn(DeleteDbColumnInput input) { var db = _db.AsTenant().GetConnectionScope(input.ConfigId); db.DbMaintenance.DropColumn(input.TableName, input.DbColumnName); } /// /// 编辑列 🔖 /// /// [ApiDescriptionSettings(Name = "UpdateColumn"), HttpPost] [DisplayName("编辑列")] public void UpdateColumn(UpdateDbColumnInput input) { var db = _db.AsTenant().GetConnectionScope(input.ConfigId); db.DbMaintenance.RenameColumn(input.TableName, input.OldColumnName, input.ColumnName); if (!string.IsNullOrWhiteSpace(input.DefaultValue)) db.DbMaintenance.AddDefaultValue(input.TableName, input.ColumnName, input.DefaultValue); if (db.DbMaintenance.IsAnyColumnRemark(input.ColumnName, input.TableName)) db.DbMaintenance.DeleteColumnRemark(input.ColumnName, input.TableName); db.DbMaintenance.AddColumnRemark(input.ColumnName, input.TableName, string.IsNullOrWhiteSpace(input.Description) ? input.ColumnName : input.Description); } /// /// 移动列顺序 🔖 /// /// [DisplayName("移动列顺序")] public void MoveColumn(MoveDbColumnInput input) { var db = _db.AsTenant().GetConnectionScope(input.ConfigId); var dbMaintenance = db.DbMaintenance; var columns = dbMaintenance.GetColumnInfosByTableName(input.TableName, false); var targetColumn = columns.FirstOrDefault(u => u.DbColumnName.Equals(input.ColumnName, StringComparison.OrdinalIgnoreCase)) ?? throw new Exception($"列 {input.ColumnName} 在表 {input.TableName} 中不存在"); var dbType = db.CurrentConnectionConfig.DbType; switch (dbType) { case SqlSugar.DbType.MySql: MoveColumnInMySQL(db, input.TableName, input.ColumnName, input.AfterColumnName); break; default: throw new NotSupportedException($"暂不支持 {dbType} 数据库的列移动操作"); } } /// /// 获取列定义 /// /// /// /// /// /// /// private string GetColumnDefinitionInMySQL(ISqlSugarClient db, string tableName, string columnName, bool noDefault = false) { var columnDef = db.Ado.SqlQuery($"SHOW FULL COLUMNS FROM `{tableName}` WHERE Field = '{columnName}'").FirstOrDefault() ?? throw new Exception($"Column {columnName} not found"); var definition = new StringBuilder(); definition.Append($"`{columnName}` "); // 列名 definition.Append($"{columnDef.Type} "); // 数据类型 // 处理约束条件 definition.Append(columnDef.Null == "YES" ? "NULL " : "NOT NULL "); if (columnDef.Default != null && !noDefault) definition.Append($"DEFAULT '{columnDef.Default}' "); if (!string.IsNullOrEmpty(columnDef.Extra)) definition.Append($"{columnDef.Extra} "); if (!string.IsNullOrEmpty(columnDef.Comment)) definition.Append($"COMMENT '{columnDef.Comment.Replace("'", "''")}'"); return definition.ToString(); } /// /// MySQL 列移动实现 /// /// /// /// /// private void MoveColumnInMySQL(ISqlSugarClient db, string tableName, string columnName, string afterColumnName) { var definition = GetColumnDefinitionInMySQL(db, tableName, columnName); var sql = new StringBuilder(); sql.Append($"ALTER TABLE `{tableName}` MODIFY COLUMN {definition}"); if (string.IsNullOrEmpty(afterColumnName)) sql.Append(" FIRST"); else sql.Append($" AFTER `{afterColumnName}`"); db.Ado.ExecuteCommand(sql.ToString()); } /// /// 增加表 🔖 /// /// [ApiDescriptionSettings(Name = "AddTable"), HttpPost] [DisplayName("增加表")] public void AddTable(DbTableInput input) { if (input.DbColumnInfoList == null || input.DbColumnInfoList.Count == 0) throw Oops.Oh(ErrorCodeEnum.db1000); if (input.DbColumnInfoList.GroupBy(u => u.DbColumnName).Any(u => u.Count() > 1)) throw Oops.Oh(ErrorCodeEnum.db1002); var config = App.GetOptions().ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == input.ConfigId); var db = _db.AsTenant().GetConnectionScope(input.ConfigId); var typeBuilder = db.DynamicBuilder().CreateClass(input.TableName, new SugarTable() { TableName = input.TableName, TableDescription = input.Description }); input.DbColumnInfoList.ForEach(u => { var dbColumnName = config!.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(u.DbColumnName.Trim()) : u.DbColumnName.Trim(); // 虚拟类都默认string类型,具体以列数据类型为准 typeBuilder.CreateProperty(dbColumnName, typeof(string), new SugarColumn() { IsPrimaryKey = u.IsPrimarykey == 1, IsIdentity = u.IsIdentity == 1, ColumnDataType = u.DataType, Length = u.Length, IsNullable = u.IsNullable == 1, DecimalDigits = u.DecimalDigits, ColumnDescription = u.ColumnDescription, DefaultValue = u.DefaultValue, }); }); db.CodeFirst.InitTables(typeBuilder.BuilderType()); } /// /// 删除表 🔖 /// /// [ApiDescriptionSettings(Name = "DeleteTable"), HttpPost] [DisplayName("删除表")] public void DeleteTable(DeleteDbTableInput input) { var db = _db.AsTenant().GetConnectionScope(input.ConfigId); db.DbMaintenance.DropTable(input.TableName); } /// /// 编辑表 🔖 /// /// [ApiDescriptionSettings(Name = "UpdateTable"), HttpPost] [DisplayName("编辑表")] public void UpdateTable(UpdateDbTableInput input) { var db = _db.AsTenant().GetConnectionScope(input.ConfigId); db.DbMaintenance.RenameTable(input.OldTableName, input.TableName); try { if (db.DbMaintenance.IsAnyTableRemark(input.TableName)) db.DbMaintenance.DeleteTableRemark(input.TableName); if (!string.IsNullOrWhiteSpace(input.Description)) db.DbMaintenance.AddTableRemark(input.TableName, input.Description); } catch (NotSupportedException ex) { throw Oops.Oh(ex.ToString()); } } /// /// 获取库列表 🔖 /// /// [DisplayName("获取库列表")] public List GetList() { var dbOutputs = new List(); var configIds = App.GetOptions().ConnectionConfigs.Select(u => u.ConfigId.ToString()).ToList(); foreach (var config in configIds) { var db = _db.AsTenant().GetConnectionScope(config); dbOutputs.Add(new DbOutput { ConfigId = config, DbName = db.Ado.Connection.Database }); } return dbOutputs; } /// /// 获取表列表 🔖 /// /// ConfigId /// [DisplayName("获取表列表")] public List GetTableList(string configId = SqlSugarConst.MainConfigId) { var db = _db.AsTenant().GetConnectionScope(configId); return db.DbMaintenance.GetTableInfoList(false); } /// /// 获取字段列表 🔖 /// /// 表名 /// ConfigId /// [DisplayName("获取字段列表")] public List GetColumnList(string tableName, string configId = SqlSugarConst.MainConfigId) { if (string.IsNullOrWhiteSpace(tableName)) return []; var db = _db.AsTenant().GetConnectionScope(configId); return db.DbMaintenance.GetColumnInfosByTableName(tableName, false).Adapt>(); } /// /// 获取数据库数据类型列表 🔖 /// /// /// [DisplayName("获取数据库数据类型列表")] public List GetDbTypeList(string configId = SqlSugarConst.MainConfigId) { var db = _db.AsTenant().GetConnectionScope(configId); return db.DbMaintenance.GetDbTypes().OrderBy(u => u).ToList(); } /// /// 获取可视化库表结构 🔖 /// /// [DisplayName("获取可视化库表结构")] public VisualDbTable GetVisualDbTable() { var visualTableList = new List(); var visualColumnList = new List(); var columnRelationList = new List(); var dbOptions = App.GetOptions().ConnectionConfigs.First(u => u.ConfigId.ToString() == SqlSugarConst.MainConfigId); // 遍历所有实体获取所有库表结构 var random = new Random(); var entityTypes = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(SugarTable), false)).ToList(); foreach (var entityType in entityTypes) { var entityInfo = _db.EntityMaintenance.GetEntityInfoNoCache(entityType); var visualTable = new VisualTable { TableName = entityInfo.DbTableName, TableComents = entityInfo.TableDescription + entityInfo.DbTableName, X = random.Next(5000), Y = random.Next(5000) }; visualTableList.Add(visualTable); foreach (EntityColumnInfo columnInfo in entityInfo.Columns) { var visualColumn = new VisualColumn { TableName = columnInfo.DbTableName, ColumnName = dbOptions.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(columnInfo.DbColumnName) : columnInfo.DbColumnName, DataType = columnInfo.PropertyInfo.PropertyType.Name, DataLength = columnInfo.Length.ToString(), ColumnDescription = columnInfo.ColumnDescription, }; visualColumnList.Add(visualColumn); // 根据导航配置获取表之间关联关系 if (columnInfo.Navigat != null) { var name1 = columnInfo.Navigat.GetName(); var name2 = columnInfo.Navigat.GetName2(); var targetColumnName = string.IsNullOrEmpty(name2) ? "Id" : name2; var relation = new ColumnRelation { SourceTableName = columnInfo.DbTableName, SourceColumnName = dbOptions.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(name1) : name1, Type = columnInfo.Navigat.GetNavigateType() == NavigateType.OneToOne ? "ONE_TO_ONE" : "ONE_TO_MANY", TargetTableName = dbOptions.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(columnInfo.DbColumnName) : columnInfo.DbColumnName, TargetColumnName = dbOptions.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(targetColumnName) : targetColumnName }; columnRelationList.Add(relation); } } } return new VisualDbTable { VisualTableList = visualTableList, VisualColumnList = visualColumnList, ColumnRelationList = columnRelationList }; } /// /// 创建实体 🔖 /// /// [ApiDescriptionSettings(Name = "CreateEntity"), HttpPost] [DisplayName("创建实体")] public async Task CreateEntity(CreateEntityInput input) { var template = await GenerateEntity(input); await File.WriteAllTextAsync(template.OutPath, template.Context, Encoding.UTF8); } /// /// 创建实体文件内容 /// /// /// [ApiDescriptionSettings(Name = "GenerateEntity"), HttpPost] [DisplayName("创建实体文件内容")] public async Task GenerateEntity(CreateEntityInput input) { var strategy = _codeGenStrategyFactory.GetStrategy(CodeGenSceneEnum.TableEntity); return (await strategy.GenerateCode(input))?.FirstOrDefault(); } /// /// 创建种子数据 🔖 /// /// [ApiDescriptionSettings(Name = "CreateSeedData"), HttpPost] [DisplayName("创建种子数据")] public async Task CreateSeedData(CreateSeedDataInput input) { var strategy = _codeGenStrategyFactory.GetStrategy(CodeGenSceneEnum.TableSeedData); var result = (await strategy.GenerateCode(input))?.FirstOrDefault(); await File.WriteAllTextAsync(result!.OutPath, result!.Context, Encoding.UTF8); } /// /// 获取种子数据列表 🔖 /// /// /// [DisplayName("获取种子数据列表")] public List GetSeedDataList([FromQuery] string configId) { var seedDataTypes = SeedDataHelper.GetInitSeedDataTypeList(); var outputList = new List(); foreach (var seedDataType in seedDataTypes) { var instance = Activator.CreateInstance(seedDataType); var hasDataMethod = seedDataType.GetMethod("HasData"); var seedData = ((IEnumerable)hasDataMethod?.Invoke(instance, null))?.Cast().ToArray() ?? []; var entityType = seedDataType.GetInterfaces().First().GetGenericArguments().First(); var seedDataAtt = seedDataType.GetCustomAttribute(); outputList.Add(new DataInitItemOutput() { Name = seedDataType.Name, AssemblyName = seedDataType.Assembly.ManifestModule.Name, Order = seedDataAtt != null ? seedDataAtt.Order : 0, Count = seedData.Length, Description = entityType.GetCustomAttribute().TableDescription + "种子数据" }); } return outputList; } /// /// 初始化表结构 🔖 /// /// [DisplayName("初始化表结构")] public void InitTable(InitTableInput input) { if (!_userManager.SuperAdmin) throw Oops.Oh("只有超管才可以操作!"); var dbProvider = _db.AsTenant().GetConnectionScope(input.ConfigId); SqlSugarSetup.InitTable(dbProvider, false, input.EntityNames); } /// /// 初始化种子数据 🔖 /// /// [DisplayName("初始化种子数据")] public void InitSeedData(InitSeedDataInput input) { if (!_userManager.SuperAdmin) throw Oops.Oh("只有超管才可以操作!"); var dbProvider = _db.AsTenant().GetConnectionScope(input.ConfigId); SqlSugarSetup.InitSeedData(dbProvider, false, input.SeedNames); } /// /// 获取库表信息 /// /// private async Task> GetEntityInfos() { var entityInfos = new List(); var type = typeof(SugarTable); var types = new List(); if (_codeGenOptions.EntityAssemblyNames != null) { foreach (var asm in _codeGenOptions.EntityAssemblyNames.Select(Assembly.Load)) { types.AddRange(asm.GetExportedTypes().ToList()); } } Type[] cosType = types.Where(u => IsMyAttribute(Attribute.GetCustomAttributes(u, false))).ToArray(); foreach (var c in cosType) { var sugarAttribute = c.GetCustomAttributes(type, true)?.FirstOrDefault(); var des = c.GetCustomAttributes(typeof(DescriptionAttribute), true); var description = ""; if (des.Length > 0) { description = ((DescriptionAttribute)des[0]).Description; } entityInfos.Add(new EntityInfo() { EntityName = c.Name, DbTableName = sugarAttribute == null ? c.Name : ((SugarTable)sugarAttribute).TableName, TableDescription = description, Type = c }); } return await Task.FromResult(entityInfos); bool IsMyAttribute(Attribute[] o) { return o.Any(a => a.GetType() == type); } } /// /// 获取实体模板文件路径 /// /// private static string GetEntityTemplatePath() { var templatePath = Path.Combine(App.WebHostEnvironment.WebRootPath, "template"); return Path.Combine(templatePath, "Entity.cs.vm"); } /// /// 获取种子数据模板文件路径 /// /// private static string GetSeedDataTemplatePath() { var templatePath = Path.Combine(App.WebHostEnvironment.WebRootPath, "template"); return Path.Combine(templatePath, "SeedData.cs.vm"); } /// /// 设置生成实体文件路径 /// /// /// private static string GetEntityTargetPath(CreateEntityInput input) { var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, input.Position, "Entity"); if (!Directory.Exists(backendPath)) Directory.CreateDirectory(backendPath); return Path.Combine(backendPath, input.EntityName + ".cs"); } /// /// 设置生成种子数据文件路径 /// /// /// private static string GetSeedDataTargetPath(CreateSeedDataInput input) { var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, input.Position, "SeedData"); if (!Directory.Exists(backendPath)) Directory.CreateDirectory(backendPath); return Path.Combine(backendPath, input.SeedDataName + ".cs"); } /// /// 备份数据库(PostgreSQL)🔖 /// /// [HttpPost, NonUnify] [DisplayName("备份数据库(PostgreSQL)")] public async Task BackupDatabase() { if (_db.CurrentConnectionConfig.DbType != SqlSugar.DbType.PostgreSQL) throw Oops.Oh("只支持 PostgreSQL 数据库 😁"); var npgsqlConn = new NpgsqlConnectionStringBuilder(_db.CurrentConnectionConfig.ConnectionString); if (npgsqlConn == null || string.IsNullOrWhiteSpace(npgsqlConn.Host) || string.IsNullOrWhiteSpace(npgsqlConn.Username) || string.IsNullOrWhiteSpace(npgsqlConn.Password) || string.IsNullOrWhiteSpace(npgsqlConn.Database)) throw Oops.Oh("PostgreSQL 数据库配置错误"); // 确保备份目录存在 var backupDirectory = Path.Combine(Directory.GetCurrentDirectory(), "backups"); Directory.CreateDirectory(backupDirectory); // 构建备份文件名 string backupFileName = $"backup_{DateTime.Now:yyyyMMddHHmmss}.sql"; string backupFilePath = Path.Combine(backupDirectory, backupFileName); // 启动pg_dump进程进行备份 // 设置密码:export PGPASSWORD='xxxxxx' var bash = $"-U {npgsqlConn.Username} -h {npgsqlConn.Host} -p {npgsqlConn.Port} -E UTF8 -F c -b -v -f {backupFilePath} {npgsqlConn.Database}"; var startInfo = new ProcessStartInfo { FileName = "pg_dump", Arguments = bash, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, EnvironmentVariables = { ["PGPASSWORD"] = npgsqlConn.Password } }; //_logger.LogInformation("备份数据库:pg_dump " + bash); //try //{ using (var backupProcess = Process.Start(startInfo)) { await backupProcess.WaitForExitAsync(); //var output = await backupProcess.StandardOutput.ReadToEndAsync(); //var error = await backupProcess.StandardError.ReadToEndAsync(); // 检查备份是否成功 if (backupProcess.ExitCode != 0) { throw Oops.Oh($"备份失败:ExitCode({backupProcess.ExitCode})"); } } // _logger.LogInformation($"备份成功:{backupFilePath}"); //} //catch (Exception ex) //{ // _logger.LogError(ex, $"备份失败:"); // throw; //} // 若备份成功则提供下载链接 return new FileStreamResult(new FileStream(backupFilePath, FileMode.Open), "application/octet-stream") { FileDownloadName = backupFileName }; } /// /// 生成SQL语句 🔖 /// /// /// [DisplayName("生成SQL语句")] public async Task GenerateSQL(GenerateSQLInput input) { return await Task.FromResult(BuildSQL.Build(input)); } /// /// 执行SQL语句 🔖 /// /// /// [DisplayName("执行SQL语句")] public async Task ExecuteSQL(GenerateSQLInput input) { var sql = await GenerateSQL(input); if (Regex.IsMatch(sql, @"\b(delete|update)\b", RegexOptions.IgnoreCase)) throw Oops.Oh("非法SQL语句,已停止执行"); var dt = await _db.Ado.GetDataTableAsync(sql); return dt; } }