diff --git a/Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj b/Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj index d8173c61..1e963046 100644 --- a/Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj +++ b/Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj @@ -40,12 +40,12 @@ - - + + - + diff --git a/Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs b/Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs index 28d8a6f8..e004ba39 100644 --- a/Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs +++ b/Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs @@ -110,7 +110,7 @@ public enum ErrorCodeEnum D1012, /// - /// 所属机构不在自己的数据范围内 + /// 没有权限操作该数据 /// [ErrorCodeItemMetadata("没有权限操作该数据")] D1013, diff --git a/Admin.NET/Admin.NET.Core/Enum/SysUserEventTypeEnum.cs b/Admin.NET/Admin.NET.Core/Enum/SysUserEventTypeEnum.cs new file mode 100644 index 00000000..04cd8e78 --- /dev/null +++ b/Admin.NET/Admin.NET.Core/Enum/SysUserEventTypeEnum.cs @@ -0,0 +1,62 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 事件类型-系统用户操作枚举 +/// +[Description("事件类型-系统用户操作枚举")] +public enum SysUserEventTypeEnum +{ + /// + /// 增加用户 + /// + [Description("增加用户")] + Add = 111, + + /// + /// 更新用户 + /// + [Description("更新用户")] + Update = 222, + + /// + /// 授权用户角色 + /// + [Description("授权用户角色")] + UpdateRole = 333, + + /// + /// 删除用户 + /// + [Description("删除用户")] + Delete = 444, + + /// + /// 设置用户状态 + /// + [Description("设置用户状态")] + SetStatus = 555, + + /// + /// 修改密码 + /// + [Description("修改密码")] + ChangePwd = 666, + + /// + /// 重置密码 + /// + [Description("重置密码")] + ResetPwd = 777, + + /// + /// 解除登录锁定 + /// + [Description("解除登录锁定")] + UnlockLogin = 888 +} \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Core/Service/DataBase/SysDatabaseService.cs b/Admin.NET/Admin.NET.Core/Service/DataBase/SysDatabaseService.cs index a9cf2a86..c283b841 100644 --- a/Admin.NET/Admin.NET.Core/Service/DataBase/SysDatabaseService.cs +++ b/Admin.NET/Admin.NET.Core/Service/DataBase/SysDatabaseService.cs @@ -49,6 +49,7 @@ public class SysDatabaseService : IDynamicApiController, ITransient 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(); @@ -71,7 +72,7 @@ public class SysDatabaseService : IDynamicApiController, ITransient var visualColumn = new VisualColumn { TableName = columnInfo.DbTableName, - ColumnName = columnInfo.DbColumnName, + ColumnName = dbOptions.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(columnInfo.DbColumnName) : columnInfo.DbColumnName, DataType = columnInfo.PropertyInfo.PropertyType.Name, DataLength = columnInfo.Length.ToString(), ColumnDescription = columnInfo.ColumnDescription, @@ -83,13 +84,14 @@ public class SysDatabaseService : IDynamicApiController, ITransient { 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 = name1, + SourceColumnName = dbOptions.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(name1) : name1, Type = columnInfo.Navigat.GetNavigateType() == NavigateType.OneToOne ? "ONE_TO_ONE" : "ONE_TO_MANY", - TargetTableName = columnInfo.DbColumnName, - TargetColumnName = string.IsNullOrEmpty(name2) ? "Id" : name2 + TargetTableName = dbOptions.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(columnInfo.DbColumnName) : columnInfo.DbColumnName, + TargetColumnName = dbOptions.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(targetColumnName) : targetColumnName }; columnRelationList.Add(relation); } diff --git a/Admin.NET/Admin.NET.Core/Service/DataBase/SysDbBackupService.cs b/Admin.NET/Admin.NET.Core/Service/DataBase/SysDbBackupService.cs index 028dc68b..587b321a 100644 --- a/Admin.NET/Admin.NET.Core/Service/DataBase/SysDbBackupService.cs +++ b/Admin.NET/Admin.NET.Core/Service/DataBase/SysDbBackupService.cs @@ -30,6 +30,8 @@ public class SysDbBackupService : IDynamicApiController, ITransient { try { + if (!Directory.Exists(backupDir)) + Directory.CreateDirectory(backupDir); var fileList = Directory.GetFiles(backupDir); var dbBackupList = new List(); diff --git a/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictDataInput.cs b/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictDataInput.cs index b9a1b183..fa757d05 100644 --- a/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictDataInput.cs +++ b/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictDataInput.cs @@ -6,13 +6,8 @@ namespace Admin.NET.Core.Service; -public class DictDataInput : BaseIdInput +public class DictDataInput : BaseStatusInput { - /// - /// 状态 - /// - [Dict("StatusEnum")] - public StatusEnum Status { get; set; } } public class PageDictDataInput : BasePageInput diff --git a/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictTypeInput.cs b/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictTypeInput.cs index fc6327e7..f311da17 100644 --- a/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictTypeInput.cs +++ b/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictTypeInput.cs @@ -6,13 +6,8 @@ namespace Admin.NET.Core.Service; -public class DictTypeInput : BaseIdInput +public class DictTypeInput : BaseStatusInput { - /// - /// 状态 - /// - [Dict("StatusEnum")] - public StatusEnum Status { get; set; } } public class PageDictTypeInput : BasePageInput diff --git a/Admin.NET/Admin.NET.Core/Service/Notice/SysNoticeService.cs b/Admin.NET/Admin.NET.Core/Service/Notice/SysNoticeService.cs index 139bff29..b4379d10 100644 --- a/Admin.NET/Admin.NET.Core/Service/Notice/SysNoticeService.cs +++ b/Admin.NET/Admin.NET.Core/Service/Notice/SysNoticeService.cs @@ -72,6 +72,9 @@ public class SysNoticeService : IDynamicApiController, ITransient [DisplayName("更新通知公告")] public async Task UpdateNotice(UpdateNoticeInput input) { + if (input.CreateUserId != _userManager.UserId) + throw Oops.Oh(ErrorCodeEnum.D7003); + var notice = input.Adapt(); InitNoticeInfo(notice); await _sysNoticeRep.UpdateAsync(notice); @@ -87,6 +90,12 @@ public class SysNoticeService : IDynamicApiController, ITransient [DisplayName("删除通知公告")] public async Task DeleteNotice(DeleteNoticeInput input) { + var sysNotice = await _sysNoticeRep.GetByIdAsync(input.Id); + if (sysNotice.CreateUserId != _userManager.UserId) + throw Oops.Oh(ErrorCodeEnum.D7003); + if (sysNotice.Status == NoticeStatusEnum.PUBLIC) + throw Oops.Oh(ErrorCodeEnum.D7001); + await _sysNoticeRep.DeleteAsync(u => u.Id == input.Id); await _sysNoticeUserRep.DeleteAsync(u => u.NoticeId == input.Id); @@ -100,6 +109,9 @@ public class SysNoticeService : IDynamicApiController, ITransient [DisplayName("发布通知公告")] public async Task Public(NoticeInput input) { + if (!(await _sysNoticeRep.IsAnyAsync(u => u.Id == input.Id && u.CreateUserId == _userManager.UserId))) + throw Oops.Oh(ErrorCodeEnum.D7003); + // 更新发布状态和时间 await _sysNoticeRep.UpdateAsync(u => new SysNotice() { Status = NoticeStatusEnum.PUBLIC, PublicTime = DateTime.Now }, u => u.Id == input.Id); diff --git a/Admin.NET/Admin.NET.Core/Service/User/Dto/SysUserEventArgs.cs b/Admin.NET/Admin.NET.Core/Service/User/Dto/SysUserEventArgs.cs new file mode 100644 index 00000000..35c4c0f5 --- /dev/null +++ b/Admin.NET/Admin.NET.Core/Service/User/Dto/SysUserEventArgs.cs @@ -0,0 +1,30 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户操作事件参数 +/// +public class SysUserEventArgs : EventArgs +{ + /// + /// 事件类型 + /// + [Required] + public SysUserEventTypeEnum EventType { get; set; } + + /// + /// 接口输入参数 + /// + public object Input { get; set; } + + public SysUserEventArgs(SysUserEventTypeEnum eventType, object input) + { + this.EventType = eventType; + this.Input = input; + } +} \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Core/Service/User/Dto/UserInput.cs b/Admin.NET/Admin.NET.Core/Service/User/Dto/UserInput.cs index 8d05eacf..834a013b 100644 --- a/Admin.NET/Admin.NET.Core/Service/User/Dto/UserInput.cs +++ b/Admin.NET/Admin.NET.Core/Service/User/Dto/UserInput.cs @@ -9,13 +9,8 @@ namespace Admin.NET.Core.Service; /// /// 设置用户状态输入参数 /// -public class UserInput : BaseIdInput +public class UserInput : BaseStatusInput { - /// - /// 状态 - /// - [Dict("StatusEnum")] - public StatusEnum Status { get; set; } } /// diff --git a/Admin.NET/Admin.NET.Core/Service/User/SysUserEventHandler.cs b/Admin.NET/Admin.NET.Core/Service/User/SysUserEventHandler.cs new file mode 100644 index 00000000..7544ad21 --- /dev/null +++ b/Admin.NET/Admin.NET.Core/Service/User/SysUserEventHandler.cs @@ -0,0 +1,32 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户事件处理类 +/// +public class SysUserEventHandler : ISingleton +{ + private event EventHandler Event; + + /// + /// 订阅 + /// + /// + public void Subscribe(EventHandler eventHandler) => Event += eventHandler; + + /// + /// 发布事件 + /// + /// + /// + /// + public void OnEvent(object sender, SysUserEventTypeEnum eventType, object input) + { + Event?.Invoke(sender, new SysUserEventArgs(eventType, input)); + } +} \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs b/Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs index dd25948e..90bd6fce 100644 --- a/Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs +++ b/Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs @@ -22,6 +22,7 @@ public class SysUserService : IDynamicApiController, ITransient private readonly SysCacheService _sysCacheService; private readonly SysUserLdapService _sysUserLdapService; private readonly SqlSugarRepository _sysUserRep; + private readonly SysUserEventHandler _sysUserEventHandler; public SysUserService(UserManager userManager, SysOrgService sysOrgService, @@ -32,7 +33,8 @@ public class SysUserService : IDynamicApiController, ITransient SysOnlineUserService sysOnlineUserService, SysCacheService sysCacheService, SysUserLdapService sysUserLdapService, - SqlSugarRepository sysUserRep) + SqlSugarRepository sysUserRep, + SysUserEventHandler sysUserEventHandler) { _userManager = userManager; _sysOrgService = sysOrgService; @@ -44,6 +46,7 @@ public class SysUserService : IDynamicApiController, ITransient _sysCacheService = sysCacheService; _sysUserLdapService = sysUserLdapService; _sysUserRep = sysUserRep; + _sysUserEventHandler = sysUserEventHandler; } /// @@ -113,6 +116,9 @@ public class SysUserService : IDynamicApiController, ITransient if (!string.IsNullOrWhiteSpace(input.DomainAccount)) await _sysUserLdapService.AddUserLdap(newUser.TenantId.Value, newUser.Id, newUser.Account, input.DomainAccount); + // 执行订阅事件 + _sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.Add, input); + return newUser.Id; } @@ -144,6 +150,9 @@ public class SysUserService : IDynamicApiController, ITransient await _sysOnlineUserService.ForceOffline(input.Id); // 更新域账号 await _sysUserLdapService.AddUserLdap(user.TenantId.Value, user.Id, user.Account, input.DomainAccount); + + // 执行订阅事件 + _sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.Update, input); } /// @@ -195,6 +204,9 @@ public class SysUserService : IDynamicApiController, ITransient // 删除域账号 await _sysUserLdapService.DeleteUserLdapByUserId(input.Id); + + // 执行订阅事件 + _sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.Delete, input); } /// @@ -241,7 +253,12 @@ public class SysUserService : IDynamicApiController, ITransient await SetUserBalckList(user, input.Status); user.Status = input.Status; - return await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new { u.Status }).ExecuteCommandAsync(); + var rows = await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new { u.Status }).ExecuteCommandAsync(); + + // 执行订阅事件 + if (rows > 0) _sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.SetStatus, input); + + return rows; } /// @@ -280,6 +297,9 @@ public class SysUserService : IDynamicApiController, ITransient // throw Oops.Oh(ErrorCodeEnum.D1022); await _sysUserRoleService.GrantUserRole(input); + + // 执行订阅事件 + _sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.UpdateRole, input); } /// @@ -322,7 +342,12 @@ public class SysUserService : IDynamicApiController, ITransient } user.LastChangePasswordTime = DateTime.Now; - return await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new { u.Password, u.LastChangePasswordTime }).ExecuteCommandAsync(); + var rows = await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new { u.Password, u.LastChangePasswordTime }).ExecuteCommandAsync(); + + // 执行订阅事件 + if (rows > 0) _sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.ChangePwd, input); + + return rows; } /// @@ -343,6 +368,9 @@ public class SysUserService : IDynamicApiController, ITransient var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}"; _sysCacheService.Remove(keyPasswordErrorTimes); + // 执行订阅事件 + _sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.ResetPwd, input); + return password; } @@ -359,6 +387,9 @@ public class SysUserService : IDynamicApiController, ITransient // 清空密码错误次数 var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}"; _sysCacheService.Remove(keyPasswordErrorTimes); + + // 执行订阅事件 + _sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.UnlockLogin, input); } /// diff --git a/Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatPayService.cs b/Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatPayService.cs index 7ccfd4ea..23bd09d9 100644 --- a/Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatPayService.cs +++ b/Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatPayService.cs @@ -368,7 +368,7 @@ public class SysWechatPayService : IDynamicApiController, ITransient { TransactionId = input.OutTradeNumber, OutTradeNumber = request.OutTradeNumber, - OutRefundNo = request.OutTradeNumber, //每笔付款只退一次,所以这里直接用付款单号 + OutRefundNo = request.OutTradeNumber, // 每笔付款只退一次,所以这里直接用付款单号 Reason = request.Reason, Refund = input.Refund, Total = input.Total, @@ -422,7 +422,7 @@ public class SysWechatPayService : IDynamicApiController, ITransient { MerchantId = _wechatPayOptions.MerchantId, TransactionId = transactionId, - WechatpayCertificateSerialNumber = _wechatPayOptions.MerchantCertificateSerialNumber + WechatpaySerialNumber = _wechatPayOptions.MerchantCertificateSerialNumber }; var response = await _wechatTenpayClient.ExecuteGetPayTransactionByIdAsync(request); if (response.TradeState == "SUCCESS" || response.TradeState == "CLOSED") @@ -463,7 +463,7 @@ public class SysWechatPayService : IDynamicApiController, ITransient { MerchantId = _wechatPayOptions.MerchantId, OutTradeNumber = outTradeNumber, - WechatpayCertificateSerialNumber = _wechatPayOptions.MerchantCertificateSerialNumber + WechatpaySerialNumber = _wechatPayOptions.MerchantCertificateSerialNumber, }; var response = await _wechatTenpayClient.ExecuteGetPayTransactionByOutTradeNumberAsync(request); if (response.TradeState == "SUCCESS" || response.TradeState == "CLOSED") diff --git a/Admin.NET/Admin.NET.Core/Utils/BaseImportInput.cs b/Admin.NET/Admin.NET.Core/Utils/BaseImportInput.cs new file mode 100644 index 00000000..dbd6ad00 --- /dev/null +++ b/Admin.NET/Admin.NET.Core/Utils/BaseImportInput.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 数据导入输入参数 +/// +public class BaseImportInput +{ + /// + /// 记录Id + /// + [ImporterHeader(IsIgnore = true)] + [ExporterHeader(IsIgnore = true)] + public virtual long Id { get; set; } + + /// + /// 错误信息 + /// + [ImporterHeader(IsIgnore = true)] + [ExporterHeader("错误信息", ColumnIndex = 9999, IsBold = true, IsAutoFit = true)] + public virtual string Error { get; set; } +} \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Core/Utils/BaseStatusInput.cs b/Admin.NET/Admin.NET.Core/Utils/BaseStatusInput.cs new file mode 100644 index 00000000..00ca9fcc --- /dev/null +++ b/Admin.NET/Admin.NET.Core/Utils/BaseStatusInput.cs @@ -0,0 +1,19 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 设置状态输入参数 +/// +public class BaseStatusInput : BaseIdInput +{ + /// + /// 状态 + /// + [Dict(nameof(StatusEnum))] + public StatusEnum Status { get; set; } +} \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Core/Utils/CommonUtil.cs b/Admin.NET/Admin.NET.Core/Utils/CommonUtil.cs index ab0910df..470c579f 100644 --- a/Admin.NET/Admin.NET.Core/Utils/CommonUtil.cs +++ b/Admin.NET/Admin.NET.Core/Utils/CommonUtil.cs @@ -248,6 +248,34 @@ public static class CommonUtil return res.Data; } + /// + /// 导入数据Excel + /// + /// + /// + /// + public static async Task> ImportExcelDataAsync([Required] IFormFile file) where T : class, new() + { + var sysFileService = App.GetRequiredService(); + var newFile = await sysFileService.UploadFile(new UploadFileInput { File = file }); + var filePath = Path.Combine(App.WebHostEnvironment.WebRootPath, newFile.FilePath!, newFile.Id + newFile.Suffix); + + IImporter importer = new ExcelImporter(); + var res = await importer.Import(filePath); + + // 删除文件 + _ = sysFileService.DeleteFile(new DeleteFileInput { Id = newFile.Id }); + + if (res == null) + throw Oops.Oh("导入数据为空"); + if (res.Exception != null) + throw Oops.Oh("导入异常:" + res.Exception); + if (res.TemplateErrors?.Count > 0) + throw Oops.Oh("模板异常:" + res.TemplateErrors.Select(x => $"[{x.RequireColumnName}]{x.Message}").Join("\n")); + + return res.Data.ToList(); + } + // 例:List ls = CommonUtil.ParseList(importResult.Data); /// /// 对象转换 含字典转换 diff --git a/Admin.NET/Admin.NET.Core/Utils/ExcelHelper.cs b/Admin.NET/Admin.NET.Core/Utils/ExcelHelper.cs new file mode 100644 index 00000000..ebe54ea3 --- /dev/null +++ b/Admin.NET/Admin.NET.Core/Utils/ExcelHelper.cs @@ -0,0 +1,120 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using OfficeOpenXml; + +namespace Admin.NET.Core; + +public class ExcelHelper +{ + /// + /// 数据导入 + /// + /// + /// + /// + public static IActionResult ImportData(IFormFile file, Action, Action, List, List>> action) where IN : BaseImportInput, new() where T : EntityBaseId, new() + { + try + { + var result = CommonUtil.ImportExcelDataAsync(file).Result ?? throw Oops.Oh("有效数据为空"); + + var tasks = new List(); + action.Invoke(result, (storageable, pageItems, rows) => + { + // 标记校验信息 + tasks.Add(Task.Run(() => + { + if (storageable.TotalList.Any()) + { + for (int i = 0; i < rows.Count; i++) pageItems[i].Id = rows[i].Id; + + for (int i = 0; i < storageable.TotalList.Count; i++) + pageItems[i].Error = storageable.TotalList[i].StorageMessage; + } + })); + }); + + // 等待所有标记验证信息任务完成 + Task.WhenAll(tasks).GetAwaiter().GetResult(); + + return ExportData(result); + } + catch (Exception ex) + { + App.HttpContext.Response.Headers["Content-Type"] = "application/json; charset=utf-8"; + throw Oops.Oh(new AdminResult + { + Code = 500, + Message = ex.Message, + Result = null, + Type = "error", + Extras = UnifyContext.Take(), + Time = DateTime.Now + }.ToJson()); + } + } + + /// + /// 导出Xlsx数据 + /// + /// + /// + /// + public static IActionResult ExportData(dynamic list, string fileName = "导入记录") + { + var exporter = new ExcelExporter(); + var fs = new MemoryStream(exporter.ExportAsByteArray(list).GetAwaiter().GetResult()); + return new XlsxFileResult(stream: fs, fileDownloadName: $"{fileName}-{DateTime.Now:yyyy-MM-dd_HHmmss}"); + } + + /// + /// 根据类型导出Xlsx模板 + /// + /// + /// + /// + /// + public static IActionResult ExportTemplate(List list, string filename = "导入模板", Func> addListValidationFun = null) + { + using var package = new ExcelPackage((ExportData(list, filename) as XlsxFileResult)!.Stream); + var worksheet = package.Workbook.Worksheets[0]; + + foreach (var prop in typeof(T).GetProperties()) + { + var propType = prop.PropertyType; + + var headerAttr = prop.GetCustomAttribute(); + var isNullableEnum = propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(Nullable<>) && Nullable.GetUnderlyingType(propType).IsEnum(); + if (isNullableEnum) propType = Nullable.GetUnderlyingType(propType); + if (headerAttr == null) continue; + + // 获取列序号 + var columnIndex = 0; + foreach (var item in worksheet.Cells[1, 1, 1, worksheet.Dimension.End.Column]) + if (++columnIndex > 0 && item.Text.Equals(headerAttr.DisplayName)) break; + if (columnIndex <= 0) continue; + + // 优先从代理函数中获取下列列表,若为空且字段为枚举型,则填充枚举项为下列列表,否则不设置下列列表 + var dataList = addListValidationFun?.Invoke(worksheet, prop)?.ToList(); + if (dataList == null && propType.IsEnum()) dataList = propType.EnumToList()?.Select(it => it.Describe).ToList(); + if (dataList != null) AddListValidation(columnIndex, dataList); + } + + void AddListValidation(int columnIndex, List dataList) + { + var validation = worksheet.DataValidations.AddListValidation(worksheet.Cells[2, columnIndex, 99999, columnIndex].Address); + dataList.ForEach(e => validation!.Formula.Values.Add(e)); + validation.ShowErrorMessage = true; + validation.ErrorTitle = "无效输入"; + validation.Error = "请从列表中选择一个有效的选项"; + } + + package.Save(); + package.Stream.Position = 0; + return new XlsxFileResult(stream: package.Stream, fileDownloadName: $"{filename}-{DateTime.Now:yyyy-MM-dd_HHmmss}"); + } +} \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Core/Utils/XlsxFileResult.cs b/Admin.NET/Admin.NET.Core/Utils/XlsxFileResult.cs new file mode 100644 index 00000000..1d08f467 --- /dev/null +++ b/Admin.NET/Admin.NET.Core/Utils/XlsxFileResult.cs @@ -0,0 +1,105 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// Excel文件ActionResult +/// +/// +public class XlsxFileResult : XlsxFileResultBase where T : class, new() +{ + public string FileDownloadName { get; } + public ICollection Data { get; } + + /// + /// + /// + /// + /// + public XlsxFileResult(ICollection data, string fileDownloadName = null) + { + FileDownloadName = fileDownloadName; + Data = data; + } + + public override async Task ExecuteResultAsync(ActionContext context) + { + var exporter = new ExcelExporter(); + var bytes = await exporter.ExportAsByteArray(Data); + var fs = new MemoryStream(bytes); + await DownloadExcelFileAsync(context, fs, FileDownloadName); + } +} + +/// +/// +/// +public class XlsxFileResult : XlsxFileResultBase +{ + /// + /// + /// + /// + /// + public XlsxFileResult(Stream stream, string fileDownloadName = null) + { + Stream = stream; + FileDownloadName = fileDownloadName; + } + + /// + /// + /// + /// + /// + + public XlsxFileResult(byte[] bytes, string fileDownloadName = null) + { + Stream = new MemoryStream(bytes); + FileDownloadName = fileDownloadName; + } + + public Stream Stream { get; protected set; } + public string FileDownloadName { get; protected set; } + + public override async Task ExecuteResultAsync(ActionContext context) + { + await DownloadExcelFileAsync(context, Stream, FileDownloadName); + } +} + +/// +/// 基类 +/// +public class XlsxFileResultBase : ActionResult +{ + /// + /// 下载Excel文件 + /// + /// + /// + /// + /// + protected virtual async Task DownloadExcelFileAsync(ActionContext context, Stream stream, string downloadFileName) + { + var response = context.HttpContext.Response; + response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + + if (downloadFileName == null) + { + downloadFileName = Guid.NewGuid().ToString("N") + ".xlsx"; + } + + if (string.IsNullOrEmpty(Path.GetExtension(downloadFileName))) + { + downloadFileName += ".xlsx"; + } + + context.HttpContext.Response.Headers.Append("Content-Disposition", new[] { "attachment; filename=" + HttpUtility.UrlEncode(downloadFileName) }); + await stream.CopyToAsync(context.HttpContext.Response.Body); + } +} \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs b/Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs index 5dc3825c..1129299f 100644 --- a/Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs +++ b/Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs @@ -57,7 +57,7 @@ namespace Admin.NET.Web.Core var tenant = sysCacheService.Get>(CacheConst.KeyTenant)?.FirstOrDefault(u => u.Id == long.Parse(tenantId)); if (tenant != null && tenant.ExpirationTime != null && DateTime.Now > tenant.ExpirationTime) { - context.Fail(); + context.Fail(new AuthorizationFailureReason(this, "租户已过期,请联系相关管理人员。")); context.GetCurrentHttpContext().SignoutToSwagger(); return; } diff --git a/Admin.NET/Admin.NET.Web.Core/Startup.cs b/Admin.NET/Admin.NET.Web.Core/Startup.cs index 911024fe..7850e2dc 100644 --- a/Admin.NET/Admin.NET.Web.Core/Startup.cs +++ b/Admin.NET/Admin.NET.Web.Core/Startup.cs @@ -240,6 +240,9 @@ public class Startup : AppStartup "image/svg+xml" }); }); + + // 注册虚拟文件系统服务 + services.AddVirtualFileServer(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) diff --git a/Web/package.json b/Web/package.json index 5005df5c..524d80d6 100644 --- a/Web/package.json +++ b/Web/package.json @@ -2,7 +2,7 @@ "name": "admin.net.pro", "type": "module", "version": "2.4.33", - "lastBuildTime": "2024.11.04", + "lastBuildTime": "2024.11.06", "description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架", "author": "zuohuaijun", "license": "MIT", @@ -20,7 +20,7 @@ "@microsoft/signalr": "^8.0.7", "@vue-office/docx": "^1.6.2", "@vue-office/excel": "^1.7.11", - "@vue-office/pdf": "^2.0.7", + "@vue-office/pdf": "^2.0.8", "@vueuse/core": "^11.2.0", "@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "^5.1.12", @@ -35,7 +35,7 @@ "echarts-wordcloud": "^2.1.0", "element-plus": "^2.8.7", "exceljs": "^4.4.0", - "ezuikit-js": "^8.1.1-alpha.2", + "ezuikit-js": "^8.1.1-alpha.3", "gcoord": "^1.0.6", "js-cookie": "^3.0.5", "js-table2excel": "^1.1.2", @@ -71,7 +71,7 @@ "vue-router": "^4.4.5", "vue-signature-pad": "^3.0.2", "vue3-tree-org": "^4.2.2", - "vxe-pc-ui": "^4.2.38", + "vxe-pc-ui": "^4.2.41", "vxe-table": "^4.7.59", "vxe-table-plugin-element": "^4.0.4", "vxe-table-plugin-export-xlsx": "^4.0.7", @@ -85,15 +85,15 @@ "@types/node": "^20.16.5", "@types/nprogress": "^0.2.3", "@types/sortablejs": "^1.15.8", - "@typescript-eslint/eslint-plugin": "^8.12.2", - "@typescript-eslint/parser": "^8.12.2", + "@typescript-eslint/eslint-plugin": "^8.13.0", + "@typescript-eslint/parser": "^8.13.0", "@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue-jsx": "^4.0.1", "@vue/compiler-sfc": "^3.5.12", "code-inspector-plugin": "^0.17.7", "eslint": "^9.14.0", "eslint-plugin-vue": "^9.29.1", - "globals": "^15.11.0", + "globals": "^15.12.0", "less": "^4.2.0", "prettier": "^3.3.3", "rollup-plugin-visualizer": "^5.12.0", diff --git a/Web/src/api-services/models/sys-oauth-user.ts b/Web/src/api-services/models/sys-oauth-user.ts index d45a42c5..5e8a0503 100644 --- a/Web/src/api-services/models/sys-oauth-user.ts +++ b/Web/src/api-services/models/sys-oauth-user.ts @@ -85,14 +85,6 @@ export interface SysOAuthUser { */ isDelete?: boolean; - /** - * 邮箱 - * - * @type {string} - * @memberof SysOAuthUser - */ - email?: string | null; - /** * 系统用户Id * @@ -155,6 +147,14 @@ export interface SysOAuthUser { */ avatar?: string | null; + /** + * 邮箱 + * + * @type {string} + * @memberof SysOAuthUser + */ + email?: string | null; + /** * 手机号码 * diff --git a/Web/src/components/table/modifyRecord.vue b/Web/src/components/table/modifyRecord.vue index 13f6e294..7f3d99ed 100644 --- a/Web/src/components/table/modifyRecord.vue +++ b/Web/src/components/table/modifyRecord.vue @@ -38,7 +38,7 @@ {{ props.data.updateTime ?? '无' }} - +