Merge remote-tracking branch 'upstream/main'

This commit is contained in:
master 2024-11-07 11:11:56 +08:00
commit 6438995d47
23 changed files with 507 additions and 49 deletions

View File

@ -40,12 +40,12 @@
<PackageReference Include="RabbitMQ.Client" Version="6.8.1" /> <PackageReference Include="RabbitMQ.Client" Version="6.8.1" />
<PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.2" /> <PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.2" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.8" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.8" />
<PackageReference Include="SKIT.FlurlHttpClient.Wechat.Api" Version="3.5.0" /> <PackageReference Include="SKIT.FlurlHttpClient.Wechat.Api" Version="3.6.0" />
<PackageReference Include="SKIT.FlurlHttpClient.Wechat.TenpayV3" Version="3.8.0" /> <PackageReference Include="SKIT.FlurlHttpClient.Wechat.TenpayV3" Version="3.9.0" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.170" /> <PackageReference Include="SqlSugarCore" Version="5.1.4.170" />
<PackageReference Include="SSH.NET" Version="2024.1.0" /> <PackageReference Include="SSH.NET" Version="2024.1.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.4.8" /> <PackageReference Include="System.Linq.Dynamic.Core" Version="1.4.8" />
<PackageReference Include="TencentCloudSDK.Sms" Version="3.0.1116" /> <PackageReference Include="TencentCloudSDK.Sms" Version="3.0.1118" />
<PackageReference Include="UAParser" Version="3.1.47" /> <PackageReference Include="UAParser" Version="3.1.47" />
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" /> <PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
</ItemGroup> </ItemGroup>

View File

@ -110,7 +110,7 @@ public enum ErrorCodeEnum
D1012, D1012,
/// <summary> /// <summary>
/// 所属机构不在自己的数据范围内 /// 没有权限操作该数据
/// </summary> /// </summary>
[ErrorCodeItemMetadata("没有权限操作该数据")] [ErrorCodeItemMetadata("没有权限操作该数据")]
D1013, D1013,

View File

@ -0,0 +1,62 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 事件类型-系统用户操作枚举
/// </summary>
[Description("事件类型-系统用户操作枚举")]
public enum SysUserEventTypeEnum
{
/// <summary>
/// 增加用户
/// </summary>
[Description("增加用户")]
Add = 111,
/// <summary>
/// 更新用户
/// </summary>
[Description("更新用户")]
Update = 222,
/// <summary>
/// 授权用户角色
/// </summary>
[Description("授权用户角色")]
UpdateRole = 333,
/// <summary>
/// 删除用户
/// </summary>
[Description("删除用户")]
Delete = 444,
/// <summary>
/// 设置用户状态
/// </summary>
[Description("设置用户状态")]
SetStatus = 555,
/// <summary>
/// 修改密码
/// </summary>
[Description("修改密码")]
ChangePwd = 666,
/// <summary>
/// 重置密码
/// </summary>
[Description("重置密码")]
ResetPwd = 777,
/// <summary>
/// 解除登录锁定
/// </summary>
[Description("解除登录锁定")]
UnlockLogin = 888
}

View File

@ -49,6 +49,7 @@ public class SysDatabaseService : IDynamicApiController, ITransient
var visualTableList = new List<VisualTable>(); var visualTableList = new List<VisualTable>();
var visualColumnList = new List<VisualColumn>(); var visualColumnList = new List<VisualColumn>();
var columnRelationList = new List<ColumnRelation>(); var columnRelationList = new List<ColumnRelation>();
var dbOptions = App.GetOptions<DbConnectionOptions>().ConnectionConfigs.First(u => u.ConfigId.ToString() == SqlSugarConst.MainConfigId);
// 遍历所有实体获取所有库表结构 // 遍历所有实体获取所有库表结构
var random = new Random(); var random = new Random();
@ -71,7 +72,7 @@ public class SysDatabaseService : IDynamicApiController, ITransient
var visualColumn = new VisualColumn var visualColumn = new VisualColumn
{ {
TableName = columnInfo.DbTableName, TableName = columnInfo.DbTableName,
ColumnName = columnInfo.DbColumnName, ColumnName = dbOptions.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(columnInfo.DbColumnName) : columnInfo.DbColumnName,
DataType = columnInfo.PropertyInfo.PropertyType.Name, DataType = columnInfo.PropertyInfo.PropertyType.Name,
DataLength = columnInfo.Length.ToString(), DataLength = columnInfo.Length.ToString(),
ColumnDescription = columnInfo.ColumnDescription, ColumnDescription = columnInfo.ColumnDescription,
@ -83,13 +84,14 @@ public class SysDatabaseService : IDynamicApiController, ITransient
{ {
var name1 = columnInfo.Navigat.GetName(); var name1 = columnInfo.Navigat.GetName();
var name2 = columnInfo.Navigat.GetName2(); var name2 = columnInfo.Navigat.GetName2();
var targetColumnName = string.IsNullOrEmpty(name2) ? "Id" : name2;
var relation = new ColumnRelation var relation = new ColumnRelation
{ {
SourceTableName = columnInfo.DbTableName, 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", Type = columnInfo.Navigat.GetNavigateType() == NavigateType.OneToOne ? "ONE_TO_ONE" : "ONE_TO_MANY",
TargetTableName = columnInfo.DbColumnName, TargetTableName = dbOptions.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(columnInfo.DbColumnName) : columnInfo.DbColumnName,
TargetColumnName = string.IsNullOrEmpty(name2) ? "Id" : name2 TargetColumnName = dbOptions.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(targetColumnName) : targetColumnName
}; };
columnRelationList.Add(relation); columnRelationList.Add(relation);
} }

View File

@ -30,6 +30,8 @@ public class SysDbBackupService : IDynamicApiController, ITransient
{ {
try try
{ {
if (!Directory.Exists(backupDir))
Directory.CreateDirectory(backupDir);
var fileList = Directory.GetFiles(backupDir); var fileList = Directory.GetFiles(backupDir);
var dbBackupList = new List<DbBackupOutput>(); var dbBackupList = new List<DbBackupOutput>();

View File

@ -6,13 +6,8 @@
namespace Admin.NET.Core.Service; namespace Admin.NET.Core.Service;
public class DictDataInput : BaseIdInput public class DictDataInput : BaseStatusInput
{ {
/// <summary>
/// 状态
/// </summary>
[Dict("StatusEnum")]
public StatusEnum Status { get; set; }
} }
public class PageDictDataInput : BasePageInput public class PageDictDataInput : BasePageInput

View File

@ -6,13 +6,8 @@
namespace Admin.NET.Core.Service; namespace Admin.NET.Core.Service;
public class DictTypeInput : BaseIdInput public class DictTypeInput : BaseStatusInput
{ {
/// <summary>
/// 状态
/// </summary>
[Dict("StatusEnum")]
public StatusEnum Status { get; set; }
} }
public class PageDictTypeInput : BasePageInput public class PageDictTypeInput : BasePageInput

View File

@ -72,6 +72,9 @@ public class SysNoticeService : IDynamicApiController, ITransient
[DisplayName("更新通知公告")] [DisplayName("更新通知公告")]
public async Task UpdateNotice(UpdateNoticeInput input) public async Task UpdateNotice(UpdateNoticeInput input)
{ {
if (input.CreateUserId != _userManager.UserId)
throw Oops.Oh(ErrorCodeEnum.D7003);
var notice = input.Adapt<SysNotice>(); var notice = input.Adapt<SysNotice>();
InitNoticeInfo(notice); InitNoticeInfo(notice);
await _sysNoticeRep.UpdateAsync(notice); await _sysNoticeRep.UpdateAsync(notice);
@ -87,6 +90,12 @@ public class SysNoticeService : IDynamicApiController, ITransient
[DisplayName("删除通知公告")] [DisplayName("删除通知公告")]
public async Task DeleteNotice(DeleteNoticeInput input) 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 _sysNoticeRep.DeleteAsync(u => u.Id == input.Id);
await _sysNoticeUserRep.DeleteAsync(u => u.NoticeId == input.Id); await _sysNoticeUserRep.DeleteAsync(u => u.NoticeId == input.Id);
@ -100,6 +109,9 @@ public class SysNoticeService : IDynamicApiController, ITransient
[DisplayName("发布通知公告")] [DisplayName("发布通知公告")]
public async Task Public(NoticeInput input) 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); await _sysNoticeRep.UpdateAsync(u => new SysNotice() { Status = NoticeStatusEnum.PUBLIC, PublicTime = DateTime.Now }, u => u.Id == input.Id);

View File

@ -0,0 +1,30 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 系统用户操作事件参数
/// </summary>
public class SysUserEventArgs : EventArgs
{
/// <summary>
/// 事件类型
/// </summary>
[Required]
public SysUserEventTypeEnum EventType { get; set; }
/// <summary>
/// 接口输入参数
/// </summary>
public object Input { get; set; }
public SysUserEventArgs(SysUserEventTypeEnum eventType, object input)
{
this.EventType = eventType;
this.Input = input;
}
}

View File

@ -9,13 +9,8 @@ namespace Admin.NET.Core.Service;
/// <summary> /// <summary>
/// 设置用户状态输入参数 /// 设置用户状态输入参数
/// </summary> /// </summary>
public class UserInput : BaseIdInput public class UserInput : BaseStatusInput
{ {
/// <summary>
/// 状态
/// </summary>
[Dict("StatusEnum")]
public StatusEnum Status { get; set; }
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,32 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core.Service;
/// <summary>
/// 系统用户事件处理类
/// </summary>
public class SysUserEventHandler : ISingleton
{
private event EventHandler Event;
/// <summary>
/// 订阅
/// </summary>
/// <param name="eventHandler"></param>
public void Subscribe(EventHandler eventHandler) => Event += eventHandler;
/// <summary>
/// 发布事件
/// </summary>
/// <param name="sender"></param>
/// <param name="eventType"></param>
/// <param name="input"></param>
public void OnEvent(object sender, SysUserEventTypeEnum eventType, object input)
{
Event?.Invoke(sender, new SysUserEventArgs(eventType, input));
}
}

View File

@ -22,6 +22,7 @@ public class SysUserService : IDynamicApiController, ITransient
private readonly SysCacheService _sysCacheService; private readonly SysCacheService _sysCacheService;
private readonly SysUserLdapService _sysUserLdapService; private readonly SysUserLdapService _sysUserLdapService;
private readonly SqlSugarRepository<SysUser> _sysUserRep; private readonly SqlSugarRepository<SysUser> _sysUserRep;
private readonly SysUserEventHandler _sysUserEventHandler;
public SysUserService(UserManager userManager, public SysUserService(UserManager userManager,
SysOrgService sysOrgService, SysOrgService sysOrgService,
@ -32,7 +33,8 @@ public class SysUserService : IDynamicApiController, ITransient
SysOnlineUserService sysOnlineUserService, SysOnlineUserService sysOnlineUserService,
SysCacheService sysCacheService, SysCacheService sysCacheService,
SysUserLdapService sysUserLdapService, SysUserLdapService sysUserLdapService,
SqlSugarRepository<SysUser> sysUserRep) SqlSugarRepository<SysUser> sysUserRep,
SysUserEventHandler sysUserEventHandler)
{ {
_userManager = userManager; _userManager = userManager;
_sysOrgService = sysOrgService; _sysOrgService = sysOrgService;
@ -44,6 +46,7 @@ public class SysUserService : IDynamicApiController, ITransient
_sysCacheService = sysCacheService; _sysCacheService = sysCacheService;
_sysUserLdapService = sysUserLdapService; _sysUserLdapService = sysUserLdapService;
_sysUserRep = sysUserRep; _sysUserRep = sysUserRep;
_sysUserEventHandler = sysUserEventHandler;
} }
/// <summary> /// <summary>
@ -113,6 +116,9 @@ public class SysUserService : IDynamicApiController, ITransient
if (!string.IsNullOrWhiteSpace(input.DomainAccount)) if (!string.IsNullOrWhiteSpace(input.DomainAccount))
await _sysUserLdapService.AddUserLdap(newUser.TenantId.Value, newUser.Id, newUser.Account, input.DomainAccount); await _sysUserLdapService.AddUserLdap(newUser.TenantId.Value, newUser.Id, newUser.Account, input.DomainAccount);
// 执行订阅事件
_sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.Add, input);
return newUser.Id; return newUser.Id;
} }
@ -144,6 +150,9 @@ public class SysUserService : IDynamicApiController, ITransient
await _sysOnlineUserService.ForceOffline(input.Id); await _sysOnlineUserService.ForceOffline(input.Id);
// 更新域账号 // 更新域账号
await _sysUserLdapService.AddUserLdap(user.TenantId.Value, user.Id, user.Account, input.DomainAccount); await _sysUserLdapService.AddUserLdap(user.TenantId.Value, user.Id, user.Account, input.DomainAccount);
// 执行订阅事件
_sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.Update, input);
} }
/// <summary> /// <summary>
@ -195,6 +204,9 @@ public class SysUserService : IDynamicApiController, ITransient
// 删除域账号 // 删除域账号
await _sysUserLdapService.DeleteUserLdapByUserId(input.Id); await _sysUserLdapService.DeleteUserLdapByUserId(input.Id);
// 执行订阅事件
_sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.Delete, input);
} }
/// <summary> /// <summary>
@ -241,7 +253,12 @@ public class SysUserService : IDynamicApiController, ITransient
await SetUserBalckList(user, input.Status); await SetUserBalckList(user, input.Status);
user.Status = 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;
} }
/// <summary> /// <summary>
@ -280,6 +297,9 @@ public class SysUserService : IDynamicApiController, ITransient
// throw Oops.Oh(ErrorCodeEnum.D1022); // throw Oops.Oh(ErrorCodeEnum.D1022);
await _sysUserRoleService.GrantUserRole(input); await _sysUserRoleService.GrantUserRole(input);
// 执行订阅事件
_sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.UpdateRole, input);
} }
/// <summary> /// <summary>
@ -322,7 +342,12 @@ public class SysUserService : IDynamicApiController, ITransient
} }
user.LastChangePasswordTime = DateTime.Now; 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;
} }
/// <summary> /// <summary>
@ -343,6 +368,9 @@ public class SysUserService : IDynamicApiController, ITransient
var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}"; var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}";
_sysCacheService.Remove(keyPasswordErrorTimes); _sysCacheService.Remove(keyPasswordErrorTimes);
// 执行订阅事件
_sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.ResetPwd, input);
return password; return password;
} }
@ -359,6 +387,9 @@ public class SysUserService : IDynamicApiController, ITransient
// 清空密码错误次数 // 清空密码错误次数
var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}"; var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}";
_sysCacheService.Remove(keyPasswordErrorTimes); _sysCacheService.Remove(keyPasswordErrorTimes);
// 执行订阅事件
_sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.UnlockLogin, input);
} }
/// <summary> /// <summary>

View File

@ -368,7 +368,7 @@ public class SysWechatPayService : IDynamicApiController, ITransient
{ {
TransactionId = input.OutTradeNumber, TransactionId = input.OutTradeNumber,
OutTradeNumber = request.OutTradeNumber, OutTradeNumber = request.OutTradeNumber,
OutRefundNo = request.OutTradeNumber, //每笔付款只退一次,所以这里直接用付款单号 OutRefundNo = request.OutTradeNumber, // 每笔付款只退一次,所以这里直接用付款单号
Reason = request.Reason, Reason = request.Reason,
Refund = input.Refund, Refund = input.Refund,
Total = input.Total, Total = input.Total,
@ -422,7 +422,7 @@ public class SysWechatPayService : IDynamicApiController, ITransient
{ {
MerchantId = _wechatPayOptions.MerchantId, MerchantId = _wechatPayOptions.MerchantId,
TransactionId = transactionId, TransactionId = transactionId,
WechatpayCertificateSerialNumber = _wechatPayOptions.MerchantCertificateSerialNumber WechatpaySerialNumber = _wechatPayOptions.MerchantCertificateSerialNumber
}; };
var response = await _wechatTenpayClient.ExecuteGetPayTransactionByIdAsync(request); var response = await _wechatTenpayClient.ExecuteGetPayTransactionByIdAsync(request);
if (response.TradeState == "SUCCESS" || response.TradeState == "CLOSED") if (response.TradeState == "SUCCESS" || response.TradeState == "CLOSED")
@ -463,7 +463,7 @@ public class SysWechatPayService : IDynamicApiController, ITransient
{ {
MerchantId = _wechatPayOptions.MerchantId, MerchantId = _wechatPayOptions.MerchantId,
OutTradeNumber = outTradeNumber, OutTradeNumber = outTradeNumber,
WechatpayCertificateSerialNumber = _wechatPayOptions.MerchantCertificateSerialNumber WechatpaySerialNumber = _wechatPayOptions.MerchantCertificateSerialNumber,
}; };
var response = await _wechatTenpayClient.ExecuteGetPayTransactionByOutTradeNumberAsync(request); var response = await _wechatTenpayClient.ExecuteGetPayTransactionByOutTradeNumberAsync(request);
if (response.TradeState == "SUCCESS" || response.TradeState == "CLOSED") if (response.TradeState == "SUCCESS" || response.TradeState == "CLOSED")

View File

@ -0,0 +1,27 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 数据导入输入参数
/// </summary>
public class BaseImportInput
{
/// <summary>
/// 记录Id
/// </summary>
[ImporterHeader(IsIgnore = true)]
[ExporterHeader(IsIgnore = true)]
public virtual long Id { get; set; }
/// <summary>
/// 错误信息
/// </summary>
[ImporterHeader(IsIgnore = true)]
[ExporterHeader("错误信息", ColumnIndex = 9999, IsBold = true, IsAutoFit = true)]
public virtual string Error { get; set; }
}

View File

@ -0,0 +1,19 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 设置状态输入参数
/// </summary>
public class BaseStatusInput : BaseIdInput
{
/// <summary>
/// 状态
/// </summary>
[Dict(nameof(StatusEnum))]
public StatusEnum Status { get; set; }
}

View File

@ -248,6 +248,34 @@ public static class CommonUtil
return res.Data; return res.Data;
} }
/// <summary>
/// 导入数据Excel
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="file"></param>
/// <returns></returns>
public static async Task<List<T>> ImportExcelDataAsync<T>([Required] IFormFile file) where T : class, new()
{
var sysFileService = App.GetRequiredService<SysFileService>();
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<T>(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<Dm_ApplyDemo> ls = CommonUtil.ParseList<Dm_ApplyDemoInport, Dm_ApplyDemo>(importResult.Data); // 例List<Dm_ApplyDemo> ls = CommonUtil.ParseList<Dm_ApplyDemoInport, Dm_ApplyDemo>(importResult.Data);
/// <summary> /// <summary>
/// 对象转换 含字典转换 /// 对象转换 含字典转换

View File

@ -0,0 +1,120 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using OfficeOpenXml;
namespace Admin.NET.Core;
public class ExcelHelper
{
/// <summary>
/// 数据导入
/// </summary>
/// <param name="file"></param>
/// <param name="action"></param>
/// <returns></returns>
public static IActionResult ImportData<IN, T>(IFormFile file, Action<List<IN>, Action<StorageableResult<T>, List<IN>, List<T>>> action) where IN : BaseImportInput, new() where T : EntityBaseId, new()
{
try
{
var result = CommonUtil.ImportExcelDataAsync<IN>(file).Result ?? throw Oops.Oh("有效数据为空");
var tasks = new List<Task>();
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<object>
{
Code = 500,
Message = ex.Message,
Result = null,
Type = "error",
Extras = UnifyContext.Take(),
Time = DateTime.Now
}.ToJson());
}
}
/// <summary>
/// 导出Xlsx数据
/// </summary>
/// <param name="list"></param>
/// <param name="fileName"></param>
/// <returns></returns>
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}");
}
/// <summary>
/// 根据类型导出Xlsx模板
/// </summary>
/// <param name="list"></param>
/// <param name="filename"></param>
/// <param name="addListValidationFun"></param>
/// <returns></returns>
public static IActionResult ExportTemplate<T>(List<T> list, string filename = "导入模板", Func<ExcelWorksheet, PropertyInfo, IEnumerable<string>> 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<ExporterHeaderAttribute>();
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<string> 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}");
}
}

View File

@ -0,0 +1,105 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// Excel文件ActionResult
/// </summary>
/// <typeparam name="T"></typeparam>
public class XlsxFileResult<T> : XlsxFileResultBase where T : class, new()
{
public string FileDownloadName { get; }
public ICollection<T> Data { get; }
/// <summary>
///
/// </summary>
/// <param name="data"></param>
/// <param name="fileDownloadName"></param>
public XlsxFileResult(ICollection<T> 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);
}
}
/// <summary>
///
/// </summary>
public class XlsxFileResult : XlsxFileResultBase
{
/// <summary>
///
/// </summary>
/// <param name="stream"></param>
/// <param name="fileDownloadName"></param>
public XlsxFileResult(Stream stream, string fileDownloadName = null)
{
Stream = stream;
FileDownloadName = fileDownloadName;
}
/// <summary>
///
/// </summary>
/// <param name="bytes"></param>
/// <param name="fileDownloadName"></param>
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);
}
}
/// <summary>
/// 基类
/// </summary>
public class XlsxFileResultBase : ActionResult
{
/// <summary>
/// 下载Excel文件
/// </summary>
/// <param name="context"></param>
/// <param name="stream"></param>
/// <param name="downloadFileName"></param>
/// <returns></returns>
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);
}
}

View File

@ -57,7 +57,7 @@ namespace Admin.NET.Web.Core
var tenant = sysCacheService.Get<List<SysTenant>>(CacheConst.KeyTenant)?.FirstOrDefault(u => u.Id == long.Parse(tenantId)); var tenant = sysCacheService.Get<List<SysTenant>>(CacheConst.KeyTenant)?.FirstOrDefault(u => u.Id == long.Parse(tenantId));
if (tenant != null && tenant.ExpirationTime != null && DateTime.Now > tenant.ExpirationTime) if (tenant != null && tenant.ExpirationTime != null && DateTime.Now > tenant.ExpirationTime)
{ {
context.Fail(); context.Fail(new AuthorizationFailureReason(this, "租户已过期,请联系相关管理人员。"));
context.GetCurrentHttpContext().SignoutToSwagger(); context.GetCurrentHttpContext().SignoutToSwagger();
return; return;
} }

View File

@ -240,6 +240,9 @@ public class Startup : AppStartup
"image/svg+xml" "image/svg+xml"
}); });
}); });
// 注册虚拟文件系统服务
services.AddVirtualFileServer();
} }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

View File

@ -2,7 +2,7 @@
"name": "admin.net.pro", "name": "admin.net.pro",
"type": "module", "type": "module",
"version": "2.4.33", "version": "2.4.33",
"lastBuildTime": "2024.11.04", "lastBuildTime": "2024.11.06",
"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架", "description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
"author": "zuohuaijun", "author": "zuohuaijun",
"license": "MIT", "license": "MIT",
@ -20,7 +20,7 @@
"@microsoft/signalr": "^8.0.7", "@microsoft/signalr": "^8.0.7",
"@vue-office/docx": "^1.6.2", "@vue-office/docx": "^1.6.2",
"@vue-office/excel": "^1.7.11", "@vue-office/excel": "^1.7.11",
"@vue-office/pdf": "^2.0.7", "@vue-office/pdf": "^2.0.8",
"@vueuse/core": "^11.2.0", "@vueuse/core": "^11.2.0",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12", "@wangeditor/editor-for-vue": "^5.1.12",
@ -35,7 +35,7 @@
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "^2.8.7", "element-plus": "^2.8.7",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"ezuikit-js": "^8.1.1-alpha.2", "ezuikit-js": "^8.1.1-alpha.3",
"gcoord": "^1.0.6", "gcoord": "^1.0.6",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"js-table2excel": "^1.1.2", "js-table2excel": "^1.1.2",
@ -71,7 +71,7 @@
"vue-router": "^4.4.5", "vue-router": "^4.4.5",
"vue-signature-pad": "^3.0.2", "vue-signature-pad": "^3.0.2",
"vue3-tree-org": "^4.2.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": "^4.7.59",
"vxe-table-plugin-element": "^4.0.4", "vxe-table-plugin-element": "^4.0.4",
"vxe-table-plugin-export-xlsx": "^4.0.7", "vxe-table-plugin-export-xlsx": "^4.0.7",
@ -85,15 +85,15 @@
"@types/node": "^20.16.5", "@types/node": "^20.16.5",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^8.12.2", "@typescript-eslint/eslint-plugin": "^8.13.0",
"@typescript-eslint/parser": "^8.12.2", "@typescript-eslint/parser": "^8.13.0",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^5.1.4",
"@vitejs/plugin-vue-jsx": "^4.0.1", "@vitejs/plugin-vue-jsx": "^4.0.1",
"@vue/compiler-sfc": "^3.5.12", "@vue/compiler-sfc": "^3.5.12",
"code-inspector-plugin": "^0.17.7", "code-inspector-plugin": "^0.17.7",
"eslint": "^9.14.0", "eslint": "^9.14.0",
"eslint-plugin-vue": "^9.29.1", "eslint-plugin-vue": "^9.29.1",
"globals": "^15.11.0", "globals": "^15.12.0",
"less": "^4.2.0", "less": "^4.2.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.12.0",

View File

@ -85,14 +85,6 @@ export interface SysOAuthUser {
*/ */
isDelete?: boolean; isDelete?: boolean;
/**
*
*
* @type {string}
* @memberof SysOAuthUser
*/
email?: string | null;
/** /**
* Id * Id
* *
@ -155,6 +147,14 @@ export interface SysOAuthUser {
*/ */
avatar?: string | null; avatar?: string | null;
/**
*
*
* @type {string}
* @memberof SysOAuthUser
*/
email?: string | null;
/** /**
* *
* *

View File

@ -38,7 +38,7 @@
</template> </template>
<el-tag>{{ props.data.updateTime ?? '无' }}</el-tag> <el-tag>{{ props.data.updateTime ?? '无' }}</el-tag>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item> <el-descriptions-item v-if="'remark' in props.data">
<template #label> <template #label>
<el-text> <el-text>
<el-icon><ele-Tickets /></el-icon> <el-icon><ele-Tickets /></el-icon>