// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 // // 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 // // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! using Admin.NET.Core.CodeGen; using System.IO.Compression; namespace Admin.NET.Core; /// /// 系统代码生成器服务 🧩 /// [ApiDescriptionSettings(Order = 270, Description = "代码生成器")] public class SysCodeGenService : IDynamicApiController, ITransient { private readonly CodeGenStrategyFactory _codeGenStrategyFactory; private readonly SysCodeGenColumnService _codeGenColumnService; private readonly DbConnectionOptions _dbConnectionOptions; private readonly SysCacheService _sysCacheService; private readonly SysMenuService _sysMenuService; private readonly CodeGenOptions _codeGenOptions; private readonly ISqlSugarClient _db; public SysCodeGenService( IOptions dbConnectionOptions, CodeGenStrategyFactory codeGenStrategyFactory, SysCodeGenColumnService codeGenColumnService, IOptions codeGenOptions, SysCacheService sysCacheService, SysMenuService sysMenuService, ISqlSugarClient db) { _db = db; _sysMenuService = sysMenuService; _sysCacheService = sysCacheService; _codeGenOptions = codeGenOptions.Value; _codeGenColumnService = codeGenColumnService; _codeGenStrategyFactory = codeGenStrategyFactory; _dbConnectionOptions = dbConnectionOptions.Value; } /// /// 获取代码生成分页列表 🔖 /// /// /// [DisplayName("获取代码生成分页列表")] public async Task> Page(PageCodeGenInput input) { return await _db.Queryable().Includes(u => u.TableList) .WhereIF(!string.IsNullOrWhiteSpace(input.TableName), u => u.TableList.Any(a => a.TableName.Contains(input.TableName.Trim()) || a.EntityName.Contains(input.TableName.Trim()))) .WhereIF(!string.IsNullOrWhiteSpace(input.BusName), u => u.BusName.Contains(input.BusName.Trim())) .OrderBy(u => u.Id, OrderByType.Desc) .ToPagedListAsync(input.Page, input.PageSize); } /// /// 增加代码生成 🔖 /// /// /// [DisplayName("增加代码生成")] [ApiDescriptionSettings(Name = "Add"), HttpPost] public async Task AddCodeGen(AddCodeGenInput input) { try { // 验证参数 ValidateTemplateParameters(input.Adapt()); await _db.Ado.BeginTranAsync(); // 保存代码生成记录 var codegen = input.Adapt(); await _db.Insertable(codegen).ExecuteCommandAsync(); // 保存关联表 codegen.TableList.ForEach(e => e.CodeGenId = codegen.Id); await _db.Insertable(codegen.TableList).ExecuteCommandAsync(); // 处理并保存表字段配置 codegen.TableList.ForEach(e => { int orderNo = 1; e.ColumnList = e.ColumnList.DistinctBy(u => u.PropertyName).ToList(); // 去重 e.ColumnList.ForEach(a => { a.CodeGenTableId = e.Id; // 关联表id a.OrderNo = orderNo++; // 排序 }); }); await _db.Storageable(codegen.TableList.SelectMany(e => e.ColumnList).ToList()).ExecuteCommandAsync(); await _db.Ado.CommitTranAsync(); } catch (Exception) { await _db.Ado.RollbackTranAsync(); throw; } } /// /// 更新代码生成 🔖 /// /// /// [DisplayName("更新代码生成")] [ApiDescriptionSettings(Name = "Update"), HttpPost] public async Task UpdateCodeGen(UpdateCodeGenInput input) { try { // 验证参数 ValidateTemplateParameters(input); await _db.Ado.BeginTranAsync(); // 保存代码生成记录 var codegen = input.Adapt(); await _db.Updateable(codegen).ExecuteCommandAsync(); // 保存关联表 codegen.TableList.ForEach(e => e.CodeGenId = codegen.Id); await _db.Storageable(codegen.TableList).ExecuteCommandAsync(); // 更新配置表 var tableIds = codegen.TableList.Select(w => w.Id).ToList(); await _db.Deleteable().Where(u => tableIds.Contains(u.CodeGenTableId)).ExecuteCommandAsync(); // 处理并保存表字段配置 codegen.TableList.ForEach(e => { int orderNo = 1; e.ColumnList = e.ColumnList.DistinctBy(u => u.PropertyName).ToList(); // 去重 e.ColumnList.ForEach(a => { a.CodeGenTableId = e.Id; // 关联表id a.OrderNo = orderNo++; // 排序 }); }); await _db.Storageable(codegen.TableList.SelectMany(e => e.ColumnList).ToList()).ExecuteCommandAsync(); await _db.Ado.CommitTranAsync(); } catch (Exception) { await _db.Ado.RollbackTranAsync(); throw; } } /// /// 删除代码生成 🔖 /// /// /// [DisplayName("删除代码生成")] [ApiDescriptionSettings(Name = "Delete"), HttpPost] public async Task DeleteCodeGen(BaseIdInput input) { try { await _db.Ado.BeginTranAsync(); var entity = await GetDetail(input.Id); if (entity == null) return; await _db.Deleteable(entity).ExecuteCommandAsync(); var tableList = entity.TableList; await _db.Deleteable(tableList).ExecuteCommandAsync(); // 删除表字段配置 var tableIds = tableList.Select(u => u.Id).ToList(); await _db.Deleteable().Where(u => tableIds.Contains(u.CodeGenTableId)) .ExecuteCommandAsync(); await _db.Ado.CommitTranAsync(); } catch (Exception) { await _db.Ado.RollbackTranAsync(); throw; } } /// /// 同步代码生成 🔖 /// /// /// [DisplayName("同步代码生成")] [ApiDescriptionSettings(Name = "Sync"), HttpPost] public async Task SyncCodeGen(BaseIdInput input) { try { await _db.Ado.BeginTranAsync(); var entity = await GetDetail(input.Id); if (entity == null) return; var addColumns = new List(); var updateColumns = new List(); var deleteColumns = new List(); foreach (var tableConfig in entity.TableList) { // 获取默认字段配置 var columns = await GetDefaultColumnConfigList(new DefaultColumnConfigInput { ConfigId = tableConfig.ConfigId, TableName = tableConfig.TableName }); // 计算新增字段 var addList = columns.Where(u => tableConfig.ColumnList.All(a => a.PropertyName != u.PropertyName)) .Select(u => { u.CodeGenTableId = tableConfig.Id; return u; }).ToList(); // 计算更新字段 var updateList = columns.Join(tableConfig.ColumnList, c => c.PropertyName, t => t.PropertyName, (c, t) => new { Column = c, OldColumn = t }) .Where(x => x.Column.NetType != x.OldColumn.NetType || x.Column.DataType != x.OldColumn.DataType || x.Column.IsPrimarykey != x.OldColumn.IsPrimarykey || x.Column.IsRequired != x.OldColumn.IsRequired || x.Column.ColumnLength != x.OldColumn.ColumnLength) .Select(x => { x.Column.Id = x.OldColumn.Id; x.Column.CodeGenTableId = x.OldColumn.CodeGenTableId; return x.Column; }).ToList(); // 计算删除字段 var deleteList = tableConfig.ColumnList.Where(u => columns.All(c => c.PropertyName != u.PropertyName)).ToList(); // 追加到集合 addColumns.AddRange(addList); updateColumns.AddRange(updateList); deleteColumns.AddRange(deleteList); } // 保存新增、更新、删除字段 if (addColumns.Count > 0) await _db.Insertable(addColumns).ExecuteCommandAsync(); if (updateColumns.Count > 0) await _db.Updateable(updateColumns).ExecuteCommandAsync(); if (deleteColumns.Count > 0) await _db.Deleteable(deleteColumns).ExecuteCommandAsync(); await _db.Ado.CommitTranAsync(); } catch (Exception) { await _db.Ado.RollbackTranAsync(); throw; } } /// /// 获取代码生成详情 🔖 /// /// /// [DisplayName("获取代码生成详情")] public async Task GetDetail([FromQuery] BaseIdInput input) { return await GetDetail(input.Id); } /// /// 获取代码生成详情 🔖 /// /// /// [NonAction] private async Task GetDetail(long id) { return await _db.Queryable().Includes(u => u.TableList, w => w.ColumnList).FirstAsync(u => u.Id == id); } /// /// 获取代码生成表详情 🔖 /// /// /// [DisplayName("获取代码生成表详情")] public async Task GetTableDetail([FromQuery] BaseIdInput input) { return await _db.Queryable().Includes(u => u.ColumnList).FirstAsync(u => u.Id == input.Id); } /// /// 获取默认表字段配置列表 🔖 /// /// /// [DisplayName("获取默认表字段配置列表")] [ApiDescriptionSettings, HttpPost] public async Task> GetDefaultColumnConfigList(DefaultColumnConfigInput input) { var list = await GetColumnListByTableName(input.TableName, input.ConfigId); return _codeGenColumnService.GetDefaultColumnConfigList(list, 0); } /// /// 获取数据库集合 🔖 /// /// [DisplayName("获取数据库集合")] public async Task> GetDatabaseList() { var dbConfigs = _dbConnectionOptions.ConnectionConfigs; return await Task.FromResult(dbConfigs.Adapt>()); } /// /// 获取数据库表(实体)集合 🔖 /// /// [DisplayName("获取数据库表(实体)集合")] public async Task> GetTableList(string configId = SqlSugarConst.MainConfigId) { var provider = _db.AsTenant().GetConnectionScope(configId); var dbTableInfos = provider.DbMaintenance.GetTableInfoList(false); var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => configId.Equals(u.ConfigId)); IEnumerable entityInfos = await _codeGenColumnService.GetEntityInfos(); // 获取所有实体定义 entityInfos = entityInfos.OrderBy(u => u.EntityName.StartsWith("Sys") ? 1 : 0).ThenBy(u => u.EntityName); var tableOutputList = new List(); foreach (var item in entityInfos) { var tbConfigId = item.Type.GetCustomAttribute()?.configId as string ?? SqlSugarConst.MainConfigId; if (item.Type.IsDefined(typeof(LogTableAttribute))) tbConfigId = SqlSugarConst.LogConfigId; if (tbConfigId != configId) continue; var dbTableName = item.DbTableName; int bracketIndex = dbTableName.IndexOf('{'); if (bracketIndex != -1) dbTableName = dbTableName.Substring(0, bracketIndex); var table = dbTableInfos.FirstOrDefault(u => u.Name.ToLower().Equals((config != null && config.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(dbTableName) : dbTableName).ToLower())); if (table == null) continue; // 计算字段个数 var columnCount = 0; try { var columns = await GetColumnListByTableName(table.Name, configId); columnCount = columns?.Count ?? 0; } catch (Exception) { // 如果获取字段失败,设为0,不影响主流程 columnCount = 0; } // 获取程序集名称 var assemblyName = item.Type.Assembly.ManifestModule.Name ?? ""; tableOutputList.Add(new TableOutput { ConfigId = configId, EntityName = item.EntityName, TableName = table.Name, TableComment = item.TableDescription, ColumnCount = columnCount, AssemblyName = assemblyName }); } return tableOutputList; } /// /// 根据表名获取列集合 🔖 /// /// [DisplayName("根据表名获取列集合")] public async Task> GetColumnListByTableName([Required] string tableName, string configId = SqlSugarConst.MainConfigId) { // 切库---多库代码生成用 var provider = _db.AsTenant().GetConnectionScope(configId); var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == configId); // 获取实体类型属性 tableName = CodeGenHelper.GetRealName(tableName, config); var entityType = provider.DbMaintenance.GetTableInfoList(false).FirstOrDefault(u => u.Name == tableName); if (entityType == null) return null; //var entityBasePropertyNames = _codeGenOptions.EntityBaseColumn[nameof(EntityTenantBaseData)]; var entityList = await _codeGenColumnService.GetEntityInfos(); var entityInfo = entityList.FirstOrDefault(u => CodeGenHelper.GetRealName(u.DbTableName, config).EqualIgnoreCase(tableName)); if (entityInfo == null) throw new Exception($"未找到实体类型:{tableName}"); var entityProps = entityInfo.Type.GetProperties(); var sugarProperties = entityProps.Where(u => u.GetCustomAttribute()?.IsIgnore == false).Select(u => new { PropertyInfo = u, PropertyName = u.Name, NetType = u.PropertyType, EntityInfo = entityInfo, ColumnComment = u.GetCustomAttribute()?.ColumnDescription, ColumnName = CodeGenHelper.GetRealName(u.GetCustomAttribute()?.ColumnName ?? u.Name, config), ForeignProperty = entityProps.FirstOrDefault(w => w.GetCustomAttribute()?.GetName() == u.Name) }).ToList(); // 按原始类型的顺序获取所有实体类型属性(不包含导航属性,会返回null) var columnList = provider.DbMaintenance.GetColumnInfosByTableName(entityType.Name).Select(u => new ColumnOutput { IsNullable = u.IsNullable, IsPrimarykey = u.IsPrimarykey, DataType = u.DataType.ToString(), ColumnComment = u.ColumnDescription, ColumnName = CodeGenHelper.GetRealName(u.DbColumnName, config), NetType = CodeGenHelper.ConvertDataType(u, provider.CurrentConnectionConfig.DbType) }).ToList(); foreach (var property in sugarProperties) { var column = columnList.FirstOrDefault(u => u.ColumnName == property.ColumnName); if (column == null) continue; column.ColumnComment ??= property.ColumnComment; column.PropertyName = property.PropertyName; column.PropertyType = property.NetType; column.PropertyInfo = property.PropertyInfo; // 设置外键配置 if (property.ForeignProperty != null) { column.IsForeignKey = true; column.ForeignProperty = property.ForeignProperty; column.ForeignEntityType = property.ForeignProperty.PropertyType.GetGenericArguments().FirstOrDefault() ?? property.ForeignProperty.PropertyType; } } return columnList.Where(u => !string.IsNullOrWhiteSpace(u.PropertyName)).ToList(); } /// /// 获取程序保存位置 🔖 /// /// [DisplayName("获取程序保存位置")] public List GetApplicationNamespaces() { return _codeGenOptions.BackendApplicationNamespaces; } /// /// 获取快捷设置外键配置 🔖 /// /// [DisplayName("获取快捷设置外键配置")] public async Task GetQuickConfigMap() { return new { sysUser = await GetEffectTreeConfig(u => u.Id, u => u.RealName, u => u.RealName), sysOrg = await GetEffectTreeConfig(u => u.Id, u => u.Name, u => u.Name, u => u.Pid) }; } /// /// 获取联表配置信息 /// /// /// /// /// /// /// private async Task GetEffectTreeConfig(Expression> linkExp, Expression> dispExp, Expression> searchExp, Expression> parentExp = null) { var entity = _db.EntityMaintenance.GetEntityInfoNoCache(typeof(T)); var configId = entity.Type.GetCustomAttribute()?.configId?.ToString() ?? SqlSugarConst.MainConfigId; var provider = _db.AsTenant().GetConnectionScope(configId); var linkProperty = GetPropertyInfo(linkExp); var disProperty = GetPropertyInfo(dispExp); var searchProperty = GetPropertyInfo(searchExp); var parentProperty = GetPropertyInfo(parentExp); // 获取字段对应的 .NET 类型 var columnNetTypeMap = (await GetColumnListByTableName(entity.Type.Name, configId)) ?.ToDictionary(u => u.PropertyName, u => u.NetType) ?? new(); return new() { ConfigId = configId, EntityName = entity.EntityName, TableName = entity.DbTableName, TableComment = entity.TableDescription, TreeTitle = entity.TableDescription, LinkPropertyName = linkProperty.Name, DisplayPropertyNames = disProperty.Name, SearchPropertyName = searchProperty.Name, ParentPropertyName = parentProperty?.Name, LinkPropertyType = columnNetTypeMap.GetValueOrDefault(linkProperty.Name), SearchPropertyType = columnNetTypeMap.GetValueOrDefault(searchProperty.Name), ParentPropertyType = columnNetTypeMap.GetValueOrDefault(parentProperty?.Name ?? ""), }; } /// /// 获取表达式属性信息 /// /// /// /// private PropertyInfo GetPropertyInfo(Expression> expression) { if (expression == null) return null; if (expression.Body is UnaryExpression { Operand: MemberExpression member }) return (PropertyInfo)member.Member; if (expression.Body is MemberExpression memberExpression) return (PropertyInfo)memberExpression.Member; throw Oops.Oh("表达式必须是一个属性访问: " + expression); } /// /// 执行代码生成 🔖 /// /// [DisplayName("执行代码生成")] public async Task Generate(BaseIdInput input) { var codeGen = await GetDetail(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); var codeGenStrategy = _codeGenStrategyFactory.GetStrategy(codeGen.Scene); var templateList = await codeGenStrategy.GenerateCode(codeGen); // 删除旧目录 string outputPath = Path.Combine(App.WebHostEnvironment.WebRootPath, "codeGen", codeGen.ModuleName); if (Directory.Exists(outputPath)) Directory.Delete(outputPath, true); // 输出文件 foreach (var template in templateList) { var dirPath = new DirectoryInfo(template.OutPath).Parent!.FullName; if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath); await File.WriteAllTextAsync(template.OutPath, template.Context); } // 生成菜单 if (codeGen.GenerateMenu) { var menuList = await GetMenusByCodeGen(codeGen); await AddMenu(menuList, codeGen.MenuPid); } // 非下载模式则返回空 if (!codeGen.GenerateMethod.ToString().StartsWith("DownloadZip")) return null; // 删除同名文件 var downloadPath = outputPath + ".zip"; if (File.Exists(downloadPath)) File.Delete(downloadPath); // 创建压缩文件并下载 ZipFile.CreateFromDirectory(outputPath, downloadPath); return new { url = $"{App.HttpContext.Request.Scheme}://{App.HttpContext.Request.Host.Value}/codeGen/{codeGen.ModuleName}.zip" }; } /// /// 获取代码生成预览 🔖 /// /// [DisplayName("获取代码生成预览")] public async Task> Preview(BaseIdInput input) { var codeGen = await GetDetail(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); var codeGenStrategy = _codeGenStrategyFactory.GetStrategy(codeGen.Scene); return (await codeGenStrategy.GenerateCode(codeGen)).ToDictionary(u => u.Name, u => u.Context); } /// /// 增加菜单 /// /// /// /// private async Task AddMenu(List menus, long pid) { var first = menus.First(); var menu = await _db.Queryable().FirstAsync(u => u.Type == first.Type && u.Name == first.Name && u.Pid == first.Pid); if (menu != null) await _sysMenuService.DeleteMenu(new() { Id = menu.Id }); await _db.Insertable(menus).ExecuteCommandAsync(); // 删除角色菜单按钮缓存 _sysCacheService.RemoveByPrefixKey(CacheConst.KeyUserApi); } /// /// 获取菜单列表 /// /// /// private async Task> GetMenusByCodeGen(SysCodeGen codeGen) { string pPath; // 若 pid=0 为顶级则创建菜单目录 SysMenu menuType0 = null; long tempPid = codeGen.MenuPid; var menuList = new List(); var classNameLower = codeGen.ModuleName.ToLower(); var classNameFirstLower = codeGen.ModuleName.ToFirstLetterLowerCase(); if (codeGen.MenuPid == 0) { // 目录 menuType0 = new SysMenu { Id = YitIdHelper.NextId(), Pid = 0, Title = codeGen.BusName + "管理", Type = MenuTypeEnum.Dir, Icon = "ele-Menu", Path = $"/{classNameLower}Manage", Name = classNameFirstLower + "Manage", Component = "Layout", OrderNo = 100, CreateTime = DateTime.Now }; codeGen.MenuPid = menuType0.Id; pPath = menuType0.Path; } else { var pMenu = await _db.Queryable().FirstAsync(u => u.Id == codeGen.MenuPid) ?? throw Oops.Oh(ErrorCodeEnum.D1505); pPath = pMenu.Path; } // 菜单 var menuType = new SysMenu { Id = YitIdHelper.NextId(), Pid = codeGen.MenuPid, Title = codeGen.BusName + "管理", Name = classNameFirstLower, Type = MenuTypeEnum.Menu, Icon = codeGen.MenuIcon, Path = pPath + "/" + classNameLower, Component = $"/{codeGen.PagePath}/{classNameFirstLower}/index" }; var menuPid = menuType.Id; int menuOrder = 100; // 按钮-page var menuTypePage = new SysMenu { Id = YitIdHelper.NextId(), Pid = menuPid, Title = "查询", Type = MenuTypeEnum.Btn, Permission = classNameFirstLower + "/page", OrderNo = menuOrder, CreateTime = DateTime.Now }; menuOrder += 10; menuList.Add(menuTypePage); // 按钮-detail var menuTypeDetail = new SysMenu { Id = YitIdHelper.NextId(), Pid = menuPid, Title = "详情", Type = MenuTypeEnum.Btn, Permission = classNameFirstLower + "/detail", OrderNo = menuOrder, CreateTime = DateTime.Now }; menuOrder += 10; menuList.Add(menuTypeDetail); // 按钮-add var menuTypeAdd = new SysMenu { Id = YitIdHelper.NextId(), Pid = menuPid, Title = "增加", Type = MenuTypeEnum.Btn, Permission = classNameFirstLower + "/add", OrderNo = menuOrder, CreateTime = DateTime.Now }; menuOrder += 10; menuList.Add(menuTypeAdd); // 按钮-delete var menuTypeDelete = new SysMenu { Id = YitIdHelper.NextId(), Pid = menuPid, Title = "删除", Type = MenuTypeEnum.Btn, Permission = classNameFirstLower + "/delete", OrderNo = menuOrder, CreateTime = DateTime.Now }; menuOrder += 10; menuList.Add(menuTypeDelete); // 按钮-update var menuTypeUpdate = new SysMenu { Id = YitIdHelper.NextId(), Pid = menuPid, Title = "编辑", Type = MenuTypeEnum.Btn, Permission = classNameFirstLower + "/update", OrderNo = menuOrder, CreateTime = DateTime.Now }; menuList.Add(menuTypeUpdate); menuList.Insert(0, menuType); if (tempPid == 0) menuList.Insert(0, menuType0); // 顶级目录需要添加目录本身 return menuList; } /// /// 验证模板参数 /// /// [NonAction] public void ValidateTemplateParameters(UpdateCodeGenInput input) { switch (input.Scene) { case CodeGenSceneEnum.SingleTable: // 单表操作 break; case CodeGenSceneEnum.TreeSingleTable: // 树单表操作 if (input.TreeConfig == null) throw Oops.Oh(ErrorCodeEnum.D1402); input.TreeConfig.Adapt().Validate(); input.Validate(); break; } // 字段组件配置参数校验 foreach (var column in input.TableList.SelectMany(x => x.ColumnList)) { if (column.IsCommon) return; if (column.EffectType is CodeGenEffectTypeEnum.DictSelector or CodeGenEffectTypeEnum.EnumSelector or CodeGenEffectTypeEnum.ConstSelector or CodeGenEffectTypeEnum.ApiTreeSelector or CodeGenEffectTypeEnum.ForeignKey or CodeGenEffectTypeEnum.Upload or CodeGenEffectTypeEnum.DatePicker && column.Config == null) throw Oops.Oh(ErrorCodeEnum.D1403); switch (column.EffectType) { case CodeGenEffectTypeEnum.DictSelector or CodeGenEffectTypeEnum.EnumSelector or CodeGenEffectTypeEnum.ConstSelector: JSON.Deserialize(column.Config)?.Validate(); break; case CodeGenEffectTypeEnum.ApiTreeSelector: JSON.Deserialize(column.Config)?.Validate(); break; case CodeGenEffectTypeEnum.ForeignKey: JSON.Deserialize(column.Config)?.Validate(); break; case CodeGenEffectTypeEnum.Upload: JSON.Deserialize(column.Config)?.Validate(); break; case CodeGenEffectTypeEnum.DatePicker: JSON.Deserialize(column.Config)?.Validate(); break; default: column.Config = null; break; } } } }