mirror of
				https://github.com/nsnail/NetAdmin.git
				synced 2025-11-01 03:35:28 +08:00 
			
		
		
		
	feat: ✨ 移除RedLocker,更改为自实现 (#169)
用户表增加最后登录时间字段 列表查询多字段模糊查询改为单字段精确查询 WebSocket版本更新检查 前端自定义字段筛选 暗黑模式样式调整 [skip ci] Co-authored-by: tk <fiyne1a@dingtalk.com>
This commit is contained in:
		| @@ -78,6 +78,7 @@ | |||||||
| 日期范围 | 日期范围 | ||||||
| 是否启用 | 是否启用 | ||||||
| 显示仪表板 | 显示仪表板 | ||||||
|  | 最后登录时间 | ||||||
| 未处理异常 | 未处理异常 | ||||||
| 未婚 | 未婚 | ||||||
| 未读 | 未读 | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ | |||||||
|             <PrivateAssets>all</PrivateAssets> |             <PrivateAssets>all</PrivateAssets> | ||||||
|             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|         </PackageReference> |         </PackageReference> | ||||||
|         <PackageReference Include="SonarAnalyzer.CSharp" Version="9.30.0.95878"> |         <PackageReference Include="SonarAnalyzer.CSharp" Version="9.31.0.96804"> | ||||||
|             <PrivateAssets>all</PrivateAssets> |             <PrivateAssets>all</PrivateAssets> | ||||||
|             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|         </PackageReference> |         </PackageReference> | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|   "version": "1.5.0", |   "version": "1.5.0", | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "cz-git": "^1.9.3", |     "cz-git": "^1.9.4", | ||||||
|     "commitizen": "^4.3.0", |     "commitizen": "^4.3.0", | ||||||
|     "prettier": "^3.3.3", |     "prettier": "^3.3.3", | ||||||
|     "standard-version": "^9.5.0" |     "standard-version": "^9.5.0" | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ | |||||||
|       "packages": [ |       "packages": [ | ||||||
|         { |         { | ||||||
|           "packageName": "Furion.Pure.NS", |           "packageName": "Furion.Pure.NS", | ||||||
|           "version": "4.9.4.6-ns4" |           "version": "4.9.5.2-ns1" | ||||||
|         } |         } | ||||||
|       ] |       ] | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -3,11 +3,9 @@ using NetAdmin.AdmServer.Host.Extensions; | |||||||
| using NetAdmin.Host.Extensions; | using NetAdmin.Host.Extensions; | ||||||
| using NetAdmin.Host.Middlewares; | using NetAdmin.Host.Middlewares; | ||||||
| using NetAdmin.SysComponent.Host.Extensions; | using NetAdmin.SysComponent.Host.Extensions; | ||||||
|  | using NetAdmin.SysComponent.Host.Middlewares; | ||||||
| using Spectre.Console.Cli; | using Spectre.Console.Cli; | ||||||
| using ValidationResult = Spectre.Console.ValidationResult; | using ValidationResult = Spectre.Console.ValidationResult; | ||||||
| #if !DEBUG |  | ||||||
| using Prometheus; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| NetAdmin.Host.Startup.Entry<Startup>(args); | NetAdmin.Host.Startup.Entry<Startup>(args); | ||||||
|  |  | ||||||
| @@ -36,7 +34,7 @@ namespace NetAdmin.AdmServer.Host | |||||||
|                 .UseOpenApiSkin() // 使用OpenApiSkin中间件(仅在调试模式下),提供Swagger UI皮肤 |                 .UseOpenApiSkin() // 使用OpenApiSkin中间件(仅在调试模式下),提供Swagger UI皮肤 | ||||||
|                 #else |                 #else | ||||||
|                 .UseVueAdmin()    // 托管管理后台,仅在非调试模式下 |                 .UseVueAdmin()    // 托管管理后台,仅在非调试模式下 | ||||||
|                 .UseHttpMetrics() // 使用HttpMetrics中间件,启用HTTP性能监控 |                 .UsePrometheus()  // 使用Prometheus中间件,启用HTTP性能监控 | ||||||
|                 #endif |                 #endif | ||||||
|                 .UseInject(string.Empty)                   // 使用Inject中间件,Furion脚手架的依赖注入支持 |                 .UseInject(string.Empty)                   // 使用Inject中间件,Furion脚手架的依赖注入支持 | ||||||
|                 .UseUnifyResultStatusCodes()               // 使用UnifyResultStatusCodes中间件,用于统一处理结果状态码 |                 .UseUnifyResultStatusCodes()               // 使用UnifyResultStatusCodes中间件,用于统一处理结果状态码 | ||||||
| @@ -45,6 +43,8 @@ namespace NetAdmin.AdmServer.Host | |||||||
|                 .UseAuthentication()                       // 使用Authentication中间件,启用身份验证 |                 .UseAuthentication()                       // 使用Authentication中间件,启用身份验证 | ||||||
|                 .UseAuthorization()                        // 使用Authorization中间件,启用授权 |                 .UseAuthorization()                        // 使用Authorization中间件,启用授权 | ||||||
|                 .UseMiddleware<RemoveNullNodeMiddleware>() // 使用RemoveNullNodeMiddleware中间件,删除JSON中的空节点 |                 .UseMiddleware<RemoveNullNodeMiddleware>() // 使用RemoveNullNodeMiddleware中间件,删除JSON中的空节点 | ||||||
|  |                 .UseWebSockets()                           // 使用WebSockets中间件,启用WebSocket支持 | ||||||
|  |                 .UseMiddleware<VersionCheckerMiddleware>() // 使用VersionUpdaterMiddleware中间件,用于检查版本 | ||||||
|                 .UseEndpoints();                           // 配置端点以处理请求 |                 .UseEndpoints();                           // 配置端点以处理请求 | ||||||
|             _ = lifeTime.ApplicationStopping.Register(SafetyShopHostMiddleware.OnStopping); |             _ = lifeTime.ApplicationStopping.Register(SafetyShopHostMiddleware.OnStopping); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -4,6 +4,6 @@ | |||||||
|         <ProjectReference Include="../NetAdmin.AdmServer.Host/NetAdmin.AdmServer.Host.csproj"/> |         <ProjectReference Include="../NetAdmin.AdmServer.Host/NetAdmin.AdmServer.Host.csproj"/> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0-release-24352-06"/> |         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0-release-24373-02"/> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
| </Project> | </Project> | ||||||
| @@ -1,50 +0,0 @@ | |||||||
| using NetAdmin.Application.Repositories; |  | ||||||
| using RedLockNet; |  | ||||||
|  |  | ||||||
| namespace NetAdmin.Application.Services; |  | ||||||
|  |  | ||||||
| /// <summary> |  | ||||||
| ///     RedLocker Service Base |  | ||||||
| /// </summary> |  | ||||||
| public abstract class RedLockerService<TEntity, TPrimary, TLogger>( |  | ||||||
|     BasicRepository<TEntity, TPrimary> rpo |  | ||||||
|   , RedLocker                          redLocker) : RepositoryService<TEntity, TPrimary, TLogger>(rpo) |  | ||||||
|     where TEntity : EntityBase<TPrimary> // |  | ||||||
|     where TPrimary : IEquatable<TPrimary> |  | ||||||
| { |  | ||||||
|     /// <summary> |  | ||||||
|     ///     获取锁 |  | ||||||
|     /// </summary> |  | ||||||
|     protected Task<IRedLock> GetLockerAsync(string lockName) |  | ||||||
|     { |  | ||||||
|         return GetLockerAsync(lockName, TimeSpan.FromSeconds(Numbers.SECS_RED_LOCK_WAIT) |  | ||||||
|                   ,                     TimeSpan.FromSeconds(Numbers.SECS_RED_LOCK_RETRY_INTERVAL)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /// <summary> |  | ||||||
|     ///     获取锁 |  | ||||||
|     /// </summary> |  | ||||||
|     /// <exception cref="NetAdminGetLockerException">NetAdminGetLockerException</exception> |  | ||||||
|     protected async Task<IRedLock> GetLockerAsync(string lockName, TimeSpan waitTime, TimeSpan retryInterval) |  | ||||||
|     { |  | ||||||
|         // 加锁 |  | ||||||
|         var lockTask = waitTime == default || retryInterval == default |  | ||||||
|             ? redLocker.RedLockFactory.CreateLockAsync(lockName, TimeSpan.FromSeconds(Numbers.SECS_RED_LOCK_EXPIRY)) |  | ||||||
|             : redLocker.RedLockFactory.CreateLockAsync( // |  | ||||||
|                 lockName, TimeSpan.FromSeconds(Numbers.SECS_RED_LOCK_EXPIRY), waitTime, retryInterval); |  | ||||||
|         var redLock = await lockTask.ConfigureAwait(false); |  | ||||||
|  |  | ||||||
|         return redLock.IsAcquired ? redLock : throw new NetAdminGetLockerException(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /// <summary> |  | ||||||
|     ///     获取锁 |  | ||||||
|     /// </summary> |  | ||||||
|     /// <remarks> |  | ||||||
|     ///     不重试,失败直接抛出异常 |  | ||||||
|     /// </remarks> |  | ||||||
|     protected Task<IRedLock> GetLockerOnceAsync(string lockName) |  | ||||||
|     { |  | ||||||
|         return GetLockerAsync(lockName, default, default); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										47
									
								
								src/backend/NetAdmin.Application/Services/RedisService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/backend/NetAdmin.Application/Services/RedisService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | using NetAdmin.Application.Repositories; | ||||||
|  | using StackExchange.Redis; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Application.Services; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     Redis Service Base | ||||||
|  | /// </summary> | ||||||
|  | /// <remarks> | ||||||
|  | ///     Initializes a new instance of the <see cref="RedisService{TEntity, TPrimary, TLogger}" /> class. | ||||||
|  | ///     Redis Service Base | ||||||
|  | /// </remarks> | ||||||
|  | public abstract class RedisService<TEntity, TPrimary, TLogger>(BasicRepository<TEntity, TPrimary> rpo) | ||||||
|  |     : RepositoryService<TEntity, TPrimary, TLogger>(rpo) | ||||||
|  |     where TEntity : EntityBase<TPrimary> // | ||||||
|  |     where TPrimary : IEquatable<TPrimary> | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Redis Database | ||||||
|  |     /// </summary> | ||||||
|  |     protected IDatabase RedisDatabase { get; } // | ||||||
|  |         = App.GetService<IConnectionMultiplexer>() | ||||||
|  |              .GetDatabase(App.GetOptions<RedisOptions>() | ||||||
|  |                              .Instances.First(x => x.Name == Chars.FLG_REDIS_INSTANCE_DATA_CACHE) | ||||||
|  |                              .Database); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     获取锁 | ||||||
|  |     /// </summary> | ||||||
|  |     protected Task<RedisLocker> GetLockerAsync(string lockerName) | ||||||
|  |     { | ||||||
|  |         return RedisLocker.GetLockerAsync(RedisDatabase, lockerName | ||||||
|  |                                         , TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_EXPIRY) | ||||||
|  |                                         , Numbers.MAX_LIMIT_RETRY_CNT_REDIS_LOCK | ||||||
|  |                                         , TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_RETRY_DELAY)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     获取锁(仅获取一次) | ||||||
|  |     /// </summary> | ||||||
|  |     protected Task<RedisLocker> GetLockerOnceAsync(string lockerName) | ||||||
|  |     { | ||||||
|  |         return RedisLocker.GetLockerAsync(RedisDatabase, lockerName | ||||||
|  |                                         , TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_EXPIRY), 1 | ||||||
|  |                                         , TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_RETRY_DELAY)); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -35,7 +35,7 @@ public abstract class RepositoryService<TEntity, TPrimary, TLogger>(BasicReposit | |||||||
|     /// </summary> |     /// </summary> | ||||||
|     protected async Task<IActionResult> ExportAsync<TQuery, TExport>( // |     protected async Task<IActionResult> ExportAsync<TQuery, TExport>( // | ||||||
|         Func<QueryReq<TQuery>, ISelectGrouping<TEntity, TEntity>> selector, QueryReq<TQuery> query, string fileName |         Func<QueryReq<TQuery>, ISelectGrouping<TEntity, TEntity>> selector, QueryReq<TQuery> query, string fileName | ||||||
|       , Expression<Func<ISelectGroupingAggregate<TEntity, TEntity>, object>> listExp) |       , Expression<Func<ISelectGroupingAggregate<TEntity, TEntity>, object>> listExp = null) | ||||||
|         where TQuery : DataAbstraction, new() |         where TQuery : DataAbstraction, new() | ||||||
|     { |     { | ||||||
|         var list = await selector(query).Take(Numbers.MAX_LIMIT_EXPORT).ToListAsync(listExp).ConfigureAwait(false); |         var list = await selector(query).Take(Numbers.MAX_LIMIT_EXPORT).ToListAsync(listExp).ConfigureAwait(false); | ||||||
| @@ -95,6 +95,7 @@ public abstract class RepositoryService<TEntity, TPrimary, TLogger>(BasicReposit | |||||||
|     /// <param name="includeFields">包含的属性</param> |     /// <param name="includeFields">包含的属性</param> | ||||||
|     /// <param name="excludeFields">排除的属性</param> |     /// <param name="excludeFields">排除的属性</param> | ||||||
|     /// <param name="whereExp">查询表达式</param> |     /// <param name="whereExp">查询表达式</param> | ||||||
|  |     /// <param name="whereSql">查询sql</param> | ||||||
|     /// <param name="ignoreVersion">是否忽略版本锁</param> |     /// <param name="ignoreVersion">是否忽略版本锁</param> | ||||||
|     /// <returns>更新后的实体列表</returns> |     /// <returns>更新后的实体列表</returns> | ||||||
|     protected Task<List<TEntity>> UpdateReturnListAsync(     // |     protected Task<List<TEntity>> UpdateReturnListAsync(     // | ||||||
| @@ -102,11 +103,15 @@ public abstract class RepositoryService<TEntity, TPrimary, TLogger>(BasicReposit | |||||||
|       , IEnumerable<string>             includeFields        // |       , IEnumerable<string>             includeFields        // | ||||||
|       , string[]                        excludeFields = null // |       , string[]                        excludeFields = null // | ||||||
|       , Expression<Func<TEntity, bool>> whereExp = null // |       , Expression<Func<TEntity, bool>> whereExp = null // | ||||||
|  |       , string                          whereSql = null // | ||||||
|       , bool                            ignoreVersion = false) |       , bool                            ignoreVersion = false) | ||||||
|     { |     { | ||||||
|         // 默认匹配主键 |         // 默认匹配主键 | ||||||
|         whereExp ??= a => a.Id.Equals(newValue.Id); |         whereExp ??= a => a.Id.Equals(newValue.Id); | ||||||
|         return BuildUpdate(newValue, includeFields, excludeFields, ignoreVersion).Where(whereExp).ExecuteUpdatedAsync(); |         return BuildUpdate(newValue, includeFields, excludeFields, ignoreVersion) | ||||||
|  |                .Where(whereExp) | ||||||
|  |                .Where(whereSql) | ||||||
|  |                .ExecuteUpdatedAsync(); | ||||||
|     } |     } | ||||||
|     #endif |     #endif | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | namespace NetAdmin.Domain.Attributes; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     危险字段标记 | ||||||
|  | /// </summary> | ||||||
|  | [AttributeUsage(AttributeTargets.Property)] | ||||||
|  | public sealed class DangerFieldAttribute : Attribute; | ||||||
| @@ -51,6 +51,14 @@ public record Sys_User : VersionEntity, IFieldSummary, IFieldEnabled, IRegister | |||||||
|     [JsonIgnore] |     [JsonIgnore] | ||||||
|     public virtual bool Enabled { get; init; } |     public virtual bool Enabled { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     最后登录时间 | ||||||
|  |     /// </summary> | ||||||
|  |     [Column] | ||||||
|  |     [CsvIgnore] | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public virtual DateTime? LastLoginTime { get; init; } | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     ///     手机号码 |     ///     手机号码 | ||||||
|     /// </summary> |     /// </summary> | ||||||
| @@ -64,6 +72,7 @@ public record Sys_User : VersionEntity, IFieldSummary, IFieldEnabled, IRegister | |||||||
|     /// </summary> |     /// </summary> | ||||||
|     [Column] |     [Column] | ||||||
|     [CsvIgnore] |     [CsvIgnore] | ||||||
|  |     [DangerField] | ||||||
|     [JsonIgnore] |     [JsonIgnore] | ||||||
|     public Guid Password { get; init; } |     public Guid Password { get; init; } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -36,4 +36,37 @@ public record QueryReq<T> : DataAbstraction | |||||||
|     ///     排序字段 |     ///     排序字段 | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     public string Prop { get; init; } |     public string Prop { get; init; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     所需字段 | ||||||
|  |     /// </summary> | ||||||
|  |     public string[] RequiredFields { get; set; } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     列表表达式 | ||||||
|  |     /// </summary> | ||||||
|  |     public Expression<Func<TEntity, TEntity>> GetToListExp<TEntity>() | ||||||
|  |     { | ||||||
|  |         if (RequiredFields.NullOrEmpty()) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var expParameter = Expression.Parameter(typeof(TEntity), "a"); | ||||||
|  |         var bindings     = new List<MemberBinding>(); | ||||||
|  |  | ||||||
|  |         // ReSharper disable once LoopCanBeConvertedToQuery | ||||||
|  |         foreach (var field in RequiredFields) { | ||||||
|  |             var prop = typeof(TEntity).GetProperty(field); | ||||||
|  |             if (prop == null || prop.GetCustomAttribute<DangerFieldAttribute>() != null) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var propExp = Expression.Property(expParameter, prop); | ||||||
|  |             var binding = Expression.Bind(prop, propExp); | ||||||
|  |             bindings.Add(binding); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var expBody = Expression.MemberInit(Expression.New(typeof(TEntity)), bindings); | ||||||
|  |         return Expression.Lambda<Func<TEntity, TEntity>>(expBody, expParameter); | ||||||
|  |     } | ||||||
| } | } | ||||||
| @@ -3,7 +3,7 @@ namespace NetAdmin.Domain.Dto.Sys.Api; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     响应:导出接口 | ///     响应:导出接口 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record ExportApiRsp : QueryApiRsp | public sealed record ExportApiRsp : QueryApiRsp | ||||||
| { | { | ||||||
|     /// <inheritdoc /> |     /// <inheritdoc /> | ||||||
|     [CsvIgnore] |     [CsvIgnore] | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ namespace NetAdmin.Domain.Dto.Sys.Config; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     响应:导出配置 | ///     响应:导出配置 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record ExportConfigRsp : QueryConfigRsp, IRegister | public sealed record ExportConfigRsp : QueryConfigRsp, IRegister | ||||||
| { | { | ||||||
|     /// <inheritdoc /> |     /// <inheritdoc /> | ||||||
|     [CsvIndex(6)] |     [CsvIndex(6)] | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ namespace NetAdmin.Domain.Dto.Sys.Dept; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     响应:导出部门 | ///     响应:导出部门 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record ExportDeptRsp : QueryDeptRsp | public sealed record ExportDeptRsp : QueryDeptRsp | ||||||
| { | { | ||||||
|     /// <inheritdoc /> |     /// <inheritdoc /> | ||||||
|     [CsvIgnore] |     [CsvIgnore] | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ namespace NetAdmin.Domain.Dto.Sys.Dic.Content; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     响应:导出字典内容 | ///     响应:导出字典内容 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record ExportDicContentRsp : QueryDicContentRsp | public sealed record ExportDicContentRsp : QueryDicContentRsp | ||||||
| { | { | ||||||
|     /// <inheritdoc /> |     /// <inheritdoc /> | ||||||
|     [CsvIndex(2)] |     [CsvIndex(2)] | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ namespace NetAdmin.Domain.Dto.Sys.Job; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     响应:导出计划作业 | ///     响应:导出计划作业 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record ExportJobRsp : QueryJobRsp | public sealed record ExportJobRsp : QueryJobRsp | ||||||
| { | { | ||||||
|     /// <inheritdoc /> |     /// <inheritdoc /> | ||||||
|     [CsvIndex(5)] |     [CsvIndex(5)] | ||||||
|   | |||||||
| @@ -3,4 +3,4 @@ namespace NetAdmin.Domain.Dto.Sys.JobRecord; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     请求:创建计划作业执行记录 | ///     请求:创建计划作业执行记录 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record CreateJobRecordReq : Sys_JobRecord; | public sealed record CreateJobRecordReq : Sys_JobRecord; | ||||||
| @@ -5,7 +5,7 @@ namespace NetAdmin.Domain.Dto.Sys.JobRecord; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     响应:导出计划作业执行记录 | ///     响应:导出计划作业执行记录 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record ExportJobRecordRsp : QueryJobRecordRsp, IRegister | public sealed record ExportJobRecordRsp : QueryJobRecordRsp, IRegister | ||||||
| { | { | ||||||
|     /// <inheritdoc /> |     /// <inheritdoc /> | ||||||
|     [CsvIndex(1)] |     [CsvIndex(1)] | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ namespace NetAdmin.Domain.Dto.Sys.LoginLog; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     请求:创建登录日志 | ///     请求:创建登录日志 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record CreateLoginLogReq : Sys_LoginLog, IRegister | public sealed record CreateLoginLogReq : Sys_LoginLog, IRegister | ||||||
| { | { | ||||||
|     /// <inheritdoc /> |     /// <inheritdoc /> | ||||||
|     public void Register(TypeAdapterConfig config) |     public void Register(TypeAdapterConfig config) | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ namespace NetAdmin.Domain.Dto.Sys.RequestLog; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     响应:导出请求日志 | ///     响应:导出请求日志 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record ExportRequestLogRsp : QueryRequestLogRsp | public sealed record ExportRequestLogRsp : QueryRequestLogRsp | ||||||
| { | { | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     ///     接口路径 |     ///     接口路径 | ||||||
|   | |||||||
| @@ -3,4 +3,4 @@ namespace NetAdmin.Domain.Dto.Sys.RequestLogDetail; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     请求:创建请求日志明细 | ///     请求:创建请求日志明细 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record CreateRequestLogDetailReq : Sys_RequestLogDetail; | public sealed record CreateRequestLogDetailReq : Sys_RequestLogDetail; | ||||||
| @@ -8,7 +8,7 @@ namespace NetAdmin.Domain.Dto.Sys.SiteMsg; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     响应:导出站内信 | ///     响应:导出站内信 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record ExportSiteMsgRsp : QuerySiteMsgRsp | public sealed record ExportSiteMsgRsp : QuerySiteMsgRsp | ||||||
| { | { | ||||||
|     /// <inheritdoc /> |     /// <inheritdoc /> | ||||||
|     [CsvIndex(5)] |     [CsvIndex(5)] | ||||||
|   | |||||||
| @@ -3,4 +3,4 @@ namespace NetAdmin.Domain.Dto.Sys.SiteMsgDept; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     请求:创建站内信-部门映射 | ///     请求:创建站内信-部门映射 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record CreateSiteMsgDeptReq : Sys_SiteMsgDept; | public sealed record CreateSiteMsgDeptReq : Sys_SiteMsgDept; | ||||||
| @@ -3,4 +3,4 @@ namespace NetAdmin.Domain.Dto.Sys.SiteMsgRole; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     请求:创建站内信-角色映射 | ///     请求:创建站内信-角色映射 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record CreateSiteMsgRoleReq : Sys_SiteMsgRole; | public sealed record CreateSiteMsgRoleReq : Sys_SiteMsgRole; | ||||||
| @@ -3,4 +3,4 @@ namespace NetAdmin.Domain.Dto.Sys.SiteMsgUser; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     请求:创建站内信-用户映射 | ///     请求:创建站内信-用户映射 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record CreateSiteMsgUserReq : Sys_SiteMsgUser; | public sealed record CreateSiteMsgUserReq : Sys_SiteMsgUser; | ||||||
| @@ -6,7 +6,7 @@ namespace NetAdmin.Domain.Dto.Sys.User; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     响应:导出用户 | ///     响应:导出用户 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record ExportUserRsp : QueryUserRsp | public sealed record ExportUserRsp : QueryUserRsp | ||||||
| { | { | ||||||
|     /// <inheritdoc /> |     /// <inheritdoc /> | ||||||
|     [CsvIndex(7)] |     [CsvIndex(7)] | ||||||
| @@ -43,6 +43,12 @@ public record ExportUserRsp : QueryUserRsp | |||||||
|     [CsvName(nameof(Ln.唯一编码))] |     [CsvName(nameof(Ln.唯一编码))] | ||||||
|     public override long Id { get; init; } |     public override long Id { get; init; } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     [CsvIndex(8)] | ||||||
|  |     [CsvIgnore(false)] | ||||||
|  |     [CsvName(nameof(Ln.最后登录时间))] | ||||||
|  |     public override DateTime? LastLoginTime { get; init; } | ||||||
|  |  | ||||||
|     /// <inheritdoc /> |     /// <inheritdoc /> | ||||||
|     [CsvIndex(2)] |     [CsvIndex(2)] | ||||||
|     [CsvIgnore(false)] |     [CsvIgnore(false)] | ||||||
|   | |||||||
| @@ -31,6 +31,10 @@ public record QueryUserRsp : Sys_User | |||||||
|     [JsonIgnore(Condition = JsonIgnoreCondition.Never)] |     [JsonIgnore(Condition = JsonIgnoreCondition.Never)] | ||||||
|     public override long Id { get; init; } |     public override long Id { get; init; } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc cref="Sys_User.LastLoginTime" /> | ||||||
|  |     [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||||||
|  |     public override DateTime? LastLoginTime { get; init; } | ||||||
|  |  | ||||||
|     /// <inheritdoc cref="Sys_User.Mobile" /> |     /// <inheritdoc cref="Sys_User.Mobile" /> | ||||||
|     [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |     [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||||||
|     public override string Mobile { get; init; } |     public override string Mobile { get; init; } | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ namespace NetAdmin.Domain.Dto.Sys.UserProfile; | |||||||
| /// <summary> | /// <summary> | ||||||
| ///     请求:设置当前用户应用配置 | ///     请求:设置当前用户应用配置 | ||||||
| /// </summary> | /// </summary> | ||||||
| public record SetSessionUserAppConfigReq : Sys_UserProfile | public sealed record SetSessionUserAppConfigReq : Sys_UserProfile | ||||||
| { | { | ||||||
|     /// <inheritdoc cref="Sys_UserProfile.AppConfig" /> |     /// <inheritdoc cref="Sys_UserProfile.AppConfig" /> | ||||||
|     [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] |     [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||||||
|   | |||||||
| @@ -13,7 +13,6 @@ | |||||||
|         <PackageReference Include="CronExpressionDescriptor" Version="2.36.0"/> |         <PackageReference Include="CronExpressionDescriptor" Version="2.36.0"/> | ||||||
|         <PackageReference Include="Cronos" Version="0.8.4"/> |         <PackageReference Include="Cronos" Version="0.8.4"/> | ||||||
|         <PackageReference Include="CsvHelper.NS" Version="33.0.2-ns2"/> |         <PackageReference Include="CsvHelper.NS" Version="33.0.2-ns2"/> | ||||||
|         <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.0-preview.6.24328.4"/> |  | ||||||
|         <PackageReference Include="Yitter.IdGenerator" Version="1.0.14"/> |         <PackageReference Include="Yitter.IdGenerator" Version="1.0.14"/> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
| </Project> | </Project> | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| using RedLockNet; | using StackExchange.Redis; | ||||||
|  |  | ||||||
| namespace NetAdmin.Host.BackgroundRunning; | namespace NetAdmin.Host.BackgroundRunning; | ||||||
|  |  | ||||||
| @@ -7,8 +7,6 @@ namespace NetAdmin.Host.BackgroundRunning; | |||||||
| /// </summary> | /// </summary> | ||||||
| public abstract class WorkBase<TLogger> | public abstract class WorkBase<TLogger> | ||||||
| { | { | ||||||
|     private readonly RedLocker _redLocker; |  | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     ///     Initializes a new instance of the <see cref="WorkBase{TLogger}" /> class. |     ///     Initializes a new instance of the <see cref="WorkBase{TLogger}" /> class. | ||||||
|     /// </summary> |     /// </summary> | ||||||
| @@ -17,7 +15,6 @@ public abstract class WorkBase<TLogger> | |||||||
|         ServiceProvider = App.GetService<IServiceScopeFactory>().CreateScope().ServiceProvider; |         ServiceProvider = App.GetService<IServiceScopeFactory>().CreateScope().ServiceProvider; | ||||||
|         UowManager      = ServiceProvider.GetService<UnitOfWorkManager>(); |         UowManager      = ServiceProvider.GetService<UnitOfWorkManager>(); | ||||||
|         Logger          = ServiceProvider.GetService<ILogger<TLogger>>(); |         Logger          = ServiceProvider.GetService<ILogger<TLogger>>(); | ||||||
|         _redLocker      = ServiceProvider.GetService<RedLocker>(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
| @@ -53,10 +50,8 @@ public abstract class WorkBase<TLogger> | |||||||
|     { |     { | ||||||
|         if (singleInstance) { |         if (singleInstance) { | ||||||
|             // 加锁 |             // 加锁 | ||||||
|             await using var redLock = await GetLockerAsync(GetType().FullName).ConfigureAwait(false); |             var             lockName    = GetType().FullName; | ||||||
|             if (!redLock.IsAcquired) { |             await using var redisLocker = await GetLockerAsync(lockName).ConfigureAwait(false); | ||||||
|                 throw new NetAdminGetLockerException(); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             await WorkflowAsync(cancelToken).ConfigureAwait(false); |             await WorkflowAsync(cancelToken).ConfigureAwait(false); | ||||||
|             return; |             return; | ||||||
| @@ -68,10 +63,15 @@ public abstract class WorkBase<TLogger> | |||||||
|     /// <summary> |     /// <summary> | ||||||
|     ///     获取锁 |     ///     获取锁 | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     private Task<IRedLock> GetLockerAsync(string lockId) |     private Task<RedisLocker> GetLockerAsync(string lockId) | ||||||
|     { |     { | ||||||
|         return _redLocker.RedLockFactory.CreateLockAsync(lockId, TimeSpan.FromSeconds(Numbers.SECS_RED_LOCK_EXPIRY) |         var db = ServiceProvider.GetService<IConnectionMultiplexer>() | ||||||
|                                                        , TimeSpan.FromSeconds(Numbers.SECS_RED_LOCK_WAIT) |                                 .GetDatabase(ServiceProvider.GetService<IOptions<RedisOptions>>() | ||||||
|                                                        , TimeSpan.FromSeconds(Numbers.SECS_RED_LOCK_RETRY_INTERVAL)); |                                                             .Value.Instances | ||||||
|  |                                                             .First(x => x.Name == Chars.FLG_REDIS_INSTANCE_DATA_CACHE) | ||||||
|  |                                                             .Database); | ||||||
|  |         return RedisLocker.GetLockerAsync(db, lockId, TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_EXPIRY) | ||||||
|  |                                         , Numbers.MAX_LIMIT_RETRY_CNT_REDIS_LOCK | ||||||
|  |                                         , TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_RETRY_DELAY)); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -3,6 +3,7 @@ using IGeekFan.AspNetCore.Knife4jUI; | |||||||
|  |  | ||||||
| #else | #else | ||||||
| using Prometheus; | using Prometheus; | ||||||
|  | using Prometheus.HttpMetrics; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| namespace NetAdmin.Host.Extensions; | namespace NetAdmin.Host.Extensions; | ||||||
| @@ -40,5 +41,22 @@ public static class IApplicationBuilderExtensions | |||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |     #else | ||||||
|  |     /// <summary> | ||||||
|  |     ///     使用 Prometheus | ||||||
|  |     /// </summary> | ||||||
|  |     public static IApplicationBuilder UsePrometheus(this IApplicationBuilder me) | ||||||
|  |     { | ||||||
|  |         return me.UseHttpMetrics(opt => { | ||||||
|  |             opt.RequestDuration.Histogram = Metrics.CreateHistogram( // | ||||||
|  |                 "http_request_duration_seconds" | ||||||
|  |               , "The duration of HTTP requests processed by an ASP.NET Core application." | ||||||
|  |               , HttpRequestLabelNames.All | ||||||
|  |               , new HistogramConfiguration { | ||||||
|  |                                                Buckets = Histogram.PowersOfTenDividedBuckets( | ||||||
|  |                                                    -2, 2, 4) | ||||||
|  |                                            }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|     #endif |     #endif | ||||||
| } | } | ||||||
| @@ -28,6 +28,7 @@ public sealed class RequestLogger(ILogger<RequestLogger> logger, IEventPublisher | |||||||
|           , x => context.Request.ContentType?.Contains(x, StringComparison.OrdinalIgnoreCase) ?? false) |           , x => context.Request.ContentType?.Contains(x, StringComparison.OrdinalIgnoreCase) ?? false) | ||||||
|             ? await context.ReadBodyContentAsync().ConfigureAwait(false) |             ? await context.ReadBodyContentAsync().ConfigureAwait(false) | ||||||
|             : string.Empty; |             : string.Empty; | ||||||
|  |         var apiId = context.Request.Path.Value!.TrimStart('/'); | ||||||
|         var auditData = new CreateRequestLogReq // |         var auditData = new CreateRequestLogReq // | ||||||
|                         { |                         { | ||||||
|                             Detail = new CreateRequestLogDetailReq // |                             Detail = new CreateRequestLogDetailReq // | ||||||
| @@ -47,7 +48,7 @@ public sealed class RequestLogger(ILogger<RequestLogger> logger, IEventPublisher | |||||||
|                                      } |                                      } | ||||||
|                           , Duration        = (int)duration |                           , Duration        = (int)duration | ||||||
|                           , HttpMethod      = Enum.Parse<HttpMethods>(context.Request.Method, true) |                           , HttpMethod      = Enum.Parse<HttpMethods>(context.Request.Method, true) | ||||||
|                           , ApiPathCrc32    = context.Request.Path.Value!.TrimStart('/').Crc32() |                           , ApiPathCrc32    = apiId.Crc32() | ||||||
|                           , HttpStatusCode  = context.Response.StatusCode |                           , HttpStatusCode  = context.Response.StatusCode | ||||||
|                           , CreatedClientIp = context.GetRealIpAddress()?.MapToIPv4().ToString().IpV4ToInt32() |                           , CreatedClientIp = context.GetRealIpAddress()?.MapToIPv4().ToString().IpV4ToInt32() | ||||||
|                           , OwnerId         = associatedUser?.UserId |                           , OwnerId         = associatedUser?.UserId | ||||||
| @@ -56,11 +57,14 @@ public sealed class RequestLogger(ILogger<RequestLogger> logger, IEventPublisher | |||||||
|                           , TraceId         = context.GetTraceId() |                           , TraceId         = context.GetTraceId() | ||||||
|                         }; |                         }; | ||||||
|  |  | ||||||
|         // 打印日志 |         // ReSharper disable once InvertIf | ||||||
|         logger.Info(auditData); |         if (!GlobalStatic.LoggerIgnoreApiIds.Contains(apiId)) { | ||||||
|  |             // 打印日志 | ||||||
|  |             logger.Info(auditData); | ||||||
|  |  | ||||||
|         // 发布请求日志事件 |             // 发布请求日志事件 | ||||||
|         await eventPublisher.PublishAsync(new RequestLogEvent(auditData)).ConfigureAwait(false); |             await eventPublisher.PublishAsync(new RequestLogEvent(auditData)).ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         return auditData; |         return auditData; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -16,20 +16,20 @@ public static class Numbers | |||||||
|     public const int  HTTP_STATUS_BIZ_FAIL    = 900;             // Http状态码-业务异常 |     public const int  HTTP_STATUS_BIZ_FAIL    = 900;             // Http状态码-业务异常 | ||||||
|     public const long ID_DIC_CATALOG_GEO_AREA = 379794295185413; // 唯一编号:字典目录-行政区划字典 |     public const long ID_DIC_CATALOG_GEO_AREA = 379794295185413; // 唯一编号:字典目录-行政区划字典 | ||||||
|  |  | ||||||
|     public const int MAX_LIMIT_BULK_REQ          = 100;   // 最大限制:批量请求数 |     public const int MAX_LIMIT_BULK_REQ             = 100;   // 最大限制:批量请求数 | ||||||
|     public const int MAX_LIMIT_EXPORT            = 10000; // 最大限制:导出为CSV文件的条数 |     public const int MAX_LIMIT_EXPORT               = 10000; // 最大限制:导出为CSV文件的条数 | ||||||
|     public const int MAX_LIMIT_PRINT_LEN_CONTENT = 4096;  // 最大限制:打印长度(HTTP 内容) |     public const int MAX_LIMIT_PRINT_LEN_CONTENT    = 4096;  // 最大限制:打印长度(HTTP 内容) | ||||||
|     public const int MAX_LIMIT_PRINT_LEN_SQL     = 4096;  // 最大限制:打印长度(SQL 语句) |     public const int MAX_LIMIT_PRINT_LEN_SQL        = 4096;  // 最大限制:打印长度(SQL 语句) | ||||||
|     public const int MAX_LIMIT_QUERY             = 1000;  // 最大限制:非分页查询条数 |     public const int MAX_LIMIT_QUERY                = 1000;  // 最大限制:非分页查询条数 | ||||||
|     public const int MAX_LIMIT_QUERY_PAGE_NO     = 10000; // 最大限制:分页查询页码 |     public const int MAX_LIMIT_QUERY_PAGE_NO        = 10000; // 最大限制:分页查询页码 | ||||||
|     public const int MAX_LIMIT_QUERY_PAGE_SIZE   = 100;   // 最大限制:分页查询页容量 |     public const int MAX_LIMIT_QUERY_PAGE_SIZE      = 100;   // 最大限制:分页查询页容量 | ||||||
|  |     public const int MAX_LIMIT_RETRY_CNT_REDIS_LOCK = 10;    // 最大限制:Redis锁重试次数 | ||||||
|  |  | ||||||
|     public const int SECS_CACHE_CHART             = 300; // 秒:缓存时间-仪表 |     public const int SECS_CACHE_CHART            = 300; // 秒:缓存时间-仪表 | ||||||
|     public const int SECS_CACHE_DEFAULT           = 60;  // 秒:缓存时间-默认 |     public const int SECS_CACHE_DEFAULT          = 60;  // 秒:缓存时间-默认 | ||||||
|     public const int SECS_CACHE_DIC_CATALOG_CODE  = 300; // 秒:缓存时间-字典配置-目录代码 |     public const int SECS_CACHE_DIC_CATALOG_CODE = 300; // 秒:缓存时间-字典配置-目录代码 | ||||||
|     public const int SECS_RED_LOCK_EXPIRY         = 30;  // 秒:RedLock-锁过期时间,假如持有锁的进程挂掉,最多在此时间内锁将被释放(如持有锁的进程正常,此值不会生效) |     public const int SECS_REDIS_LOCK_EXPIRY      = 60;  // 秒:Redis锁过期时间 | ||||||
|     public const int SECS_RED_LOCK_RETRY_INTERVAL = 1;   // 秒:RedLock-锁等待时间内,多久尝试获取一次 |     public const int SECS_REDIS_LOCK_RETRY_DELAY = 1;   // 秒:Redis锁重试间隔 | ||||||
|     public const int SECS_RED_LOCK_WAIT           = 10;  // 秒:RedLock-锁等待时间,相同的 resource 如果当前的锁被其他线程占用,最多等待时间 |     public const int SECS_TIMEOUT_HTTP_CLIENT    = 15;  // 秒:超时时间-默认HTTP客户端 | ||||||
|     public const int SECS_TIMEOUT_HTTP_CLIENT     = 15;  // 秒:超时时间-默认HTTP客户端 |     public const int SECS_TIMEOUT_JOB            = 600; // 秒:超时时间-作业 | ||||||
|     public const int SECS_TIMEOUT_JOB             = 600; // 秒:超时时间-作业 |  | ||||||
| } | } | ||||||
| @@ -7,5 +7,5 @@ namespace NetAdmin.Infrastructure.Exceptions; | |||||||
| ///     并发执行时锁竞争失败 | ///     并发执行时锁竞争失败 | ||||||
| /// </remarks> | /// </remarks> | ||||||
| #pragma warning disable RCS1194 | #pragma warning disable RCS1194 | ||||||
| public sealed class NetAdminGetLockerException() : NetAdminInvalidOperationException(null) { } | public sealed class NetAdminGetLockerException(string message = null) : NetAdminInvalidOperationException(message) { } | ||||||
| #pragma warning restore RCS1194 | #pragma warning restore RCS1194 | ||||||
| @@ -24,6 +24,11 @@ public static class GlobalStatic | |||||||
|     #endif |     #endif | ||||||
|     ; |     ; | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     日志记录器忽略的API编号 | ||||||
|  |     /// </summary> | ||||||
|  |     public static string[] LoggerIgnoreApiIds => []; | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     ///     系统内部密钥 |     ///     系统内部密钥 | ||||||
|     /// </summary> |     /// </summary> | ||||||
|   | |||||||
| @@ -8,13 +8,13 @@ | |||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|         <PackageReference Include="FreeSql.DbContext.NS" Version="3.2.833-preview20260627-ns1"/> |         <PackageReference Include="FreeSql.DbContext.NS" Version="3.2.833-preview20260627-ns1"/> | ||||||
|         <PackageReference Include="FreeSql.Provider.Sqlite.NS" Version="3.2.833-preview20260627-ns1"/> |         <PackageReference Include="FreeSql.Provider.Sqlite.NS" Version="3.2.833-preview20260627-ns1"/> | ||||||
|         <PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.4.6"/> |         <PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.5.2"/> | ||||||
|         <PackageReference Include="Furion.Extras.ObjectMapper.Mapster.NS" Version="4.9.4.6-ns4"/> |         <PackageReference Include="Furion.Extras.ObjectMapper.Mapster.NS" Version="4.9.5.2-ns1"/> | ||||||
|         <PackageReference Include="Furion.Pure.NS" Version="4.9.4.6-ns4"/> |         <PackageReference Include="Furion.Pure.NS" Version="4.9.5.2-ns1"/> | ||||||
|         <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.10.0"/> |         <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.10.0"/> | ||||||
|  |         <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.0-preview.6.24328.4"/> | ||||||
|         <PackageReference Include="Minio" Version="6.0.3"/> |         <PackageReference Include="Minio" Version="6.0.3"/> | ||||||
|         <PackageReference Include="NSExt" Version="2.2.0"/> |         <PackageReference Include="NSExt" Version="2.2.0"/> | ||||||
|         <PackageReference Include="RedLock.net" Version="2.3.2"/> |  | ||||||
|         <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4"/> |         <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4"/> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|   | |||||||
| @@ -1,98 +0,0 @@ | |||||||
| using RedLockNet.SERedis; |  | ||||||
| using RedLockNet.SERedis.Configuration; |  | ||||||
| using StackExchange.Redis; |  | ||||||
|  |  | ||||||
| namespace NetAdmin.Infrastructure.Utils; |  | ||||||
|  |  | ||||||
| /// <summary> |  | ||||||
| ///     Redis 分布锁 |  | ||||||
| /// </summary> |  | ||||||
| #pragma warning disable DesignedForInheritance |  | ||||||
| public class RedLocker : IDisposable, ISingleton |  | ||||||
| #pragma warning restore DesignedForInheritance |  | ||||||
| { |  | ||||||
|     // Track whether Dispose has been called. |  | ||||||
|     private bool _disposed; |  | ||||||
|  |  | ||||||
|     /// <summary> |  | ||||||
|     ///     Initializes a new instance of the <see cref="RedLocker" /> class. |  | ||||||
|     /// </summary> |  | ||||||
|     public RedLocker(IOptions<RedisOptions> redisOptions) |  | ||||||
|     { |  | ||||||
|         RedLockFactory = RedLockFactory.Create( // |  | ||||||
|             new List<RedLockMultiplexer>        // |  | ||||||
|             { |  | ||||||
|                 ConnectionMultiplexer.Connect( // |  | ||||||
|                     redisOptions.Value.Instances.First(x => x.Name == Chars.FLG_REDIS_INSTANCE_DATA_CACHE).ConnStr) |  | ||||||
|             }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /// <summary> |  | ||||||
|     ///     Finalizes an instance of the <see cref="RedLocker" /> class. |  | ||||||
|     ///     Use C# finalizer syntax for finalization code. |  | ||||||
|     ///     This finalizer will run only if the Dispose method |  | ||||||
|     ///     does not get called. |  | ||||||
|     ///     It gives your base class the opportunity to finalize. |  | ||||||
|     ///     Do not provide finalizer in types derived from this class. |  | ||||||
|     /// </summary> |  | ||||||
|     ~RedLocker() |  | ||||||
|     { |  | ||||||
|         // Do not re-create Dispose clean-up code here. |  | ||||||
|         // Calling Dispose(disposing: false) is optimal in terms of |  | ||||||
|         // readability and maintainability. |  | ||||||
|         Dispose(false); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /// <summary> |  | ||||||
|     ///     RedLockFactory |  | ||||||
|     /// </summary> |  | ||||||
|     public RedLockFactory RedLockFactory { get; } |  | ||||||
|  |  | ||||||
|     /// <summary> |  | ||||||
|     ///     Implement IDisposable. |  | ||||||
|     ///     Do not make this method virtual. |  | ||||||
|     ///     A derived class should not be able to override this method. |  | ||||||
|     /// </summary> |  | ||||||
|     public void Dispose() |  | ||||||
|     { |  | ||||||
|         Dispose(true); |  | ||||||
|  |  | ||||||
|         // This object will be cleaned up by the Dispose method. |  | ||||||
|         // Therefore, you should call GC.SuppressFinalize to |  | ||||||
|         // take this object off the finalization queue |  | ||||||
|         // and prevent finalization code for this object |  | ||||||
|         // from executing a second time. |  | ||||||
|         GC.SuppressFinalize(this); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /// <summary> |  | ||||||
|     ///     Dispose(bool disposing) executes in two distinct scenarios. |  | ||||||
|     ///     If disposing equals true, the method has been called directly |  | ||||||
|     ///     or indirectly by a user's code. Managed and unmanaged resources |  | ||||||
|     ///     can be disposed. |  | ||||||
|     ///     If disposing equals false, the method has been called by the |  | ||||||
|     ///     runtime from inside the finalizer and you should not reference |  | ||||||
|     ///     other objects. Only unmanaged resources can be disposed. |  | ||||||
|     /// </summary> |  | ||||||
|     protected virtual void Dispose(bool disposing) |  | ||||||
|     { |  | ||||||
|         // Check to see if Dispose has already been called. |  | ||||||
|         if (_disposed) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // If disposing equals true, dispose all managed |  | ||||||
|         // and unmanaged resources. |  | ||||||
|         if (disposing) { |  | ||||||
|             RedLockFactory.Dispose(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Call the appropriate methods to clean up |  | ||||||
|         // unmanaged resources here. |  | ||||||
|         // If disposing is false, |  | ||||||
|         // only the following code is executed. |  | ||||||
|  |  | ||||||
|         // Note disposing has been done. |  | ||||||
|         _disposed = true; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										70
									
								
								src/backend/NetAdmin.Infrastructure/Utils/RedisLocker.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/backend/NetAdmin.Infrastructure/Utils/RedisLocker.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | using StackExchange.Redis; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.Infrastructure.Utils; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     Redis 分布锁 | ||||||
|  | /// </summary> | ||||||
|  | /// <remarks> | ||||||
|  | ///     Initializes a new instance of the <see cref="RedisLocker" /> class. | ||||||
|  | /// </remarks> | ||||||
|  | #pragma warning disable DesignedForInheritance | ||||||
|  | public sealed class RedisLocker : IAsyncDisposable | ||||||
|  | #pragma warning restore DesignedForInheritance | ||||||
|  | { | ||||||
|  |     private readonly IDatabase _redisDatabase; | ||||||
|  |  | ||||||
|  |     private readonly string _redisKey; | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Initializes a new instance of the <see cref="RedisLocker" /> class. | ||||||
|  |     ///     Redis 分布锁 | ||||||
|  |     /// </summary> | ||||||
|  |     private RedisLocker(IDatabase redisDatabase, string redisKey) | ||||||
|  |     { | ||||||
|  |         _redisDatabase = redisDatabase; | ||||||
|  |         _redisKey      = redisKey; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     ///     获取锁 | ||||||
|  |     /// </summary> | ||||||
|  |     /// <exception cref="NetAdminGetLockerException">NetAdminGetLockerException</exception> | ||||||
|  |     public static async Task<RedisLocker> GetLockerAsync(IDatabase redisDatabase, string lockerName | ||||||
|  |                                                        , TimeSpan  lockerExpire,  int retryCount, TimeSpan retryDelay) | ||||||
|  |     { | ||||||
|  |         lockerName = $"{nameof(RedisLocker)}.{lockerName}"; | ||||||
|  |         var setOk = false; | ||||||
|  |         for (var i = 0; i != retryCount; ++i) { | ||||||
|  |             try { | ||||||
|  |                 setOk = await redisDatabase | ||||||
|  |                               .StringSetAsync(lockerName, RedisValue.EmptyString, lockerExpire, When.NotExists | ||||||
|  |                                 ,                         CommandFlags.DemandMaster) | ||||||
|  |                               .ConfigureAwait(false); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) { | ||||||
|  |                 LogHelper.Get<RedisLocker>().Error(ex.Message); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (setOk) { | ||||||
|  |                 return new RedisLocker(redisDatabase, lockerName); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             await Task.Delay(retryDelay).ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         throw new NetAdminGetLockerException(lockerName); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public async ValueTask DisposeAsync() | ||||||
|  |     { | ||||||
|  |         try { | ||||||
|  |             _ = await _redisDatabase.KeyDeleteAsync(_redisKey, CommandFlags.DemandMaster | CommandFlags.FireAndForget) | ||||||
|  |                                     .ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  |         catch (Exception ex) { | ||||||
|  |             LogHelper.Get<RedisLocker>().Error(ex.Message); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -8,11 +8,10 @@ namespace NetAdmin.SysComponent.Application.Services.Sys; | |||||||
|  |  | ||||||
| /// <inheritdoc cref="IApiService" /> | /// <inheritdoc cref="IApiService" /> | ||||||
| public sealed class ApiService( | public sealed class ApiService( | ||||||
|     BasicRepository<Sys_Api, string>    rpo              // |     BasicRepository<Sys_Api, string>    rpo                                 // | ||||||
|   , XmlCommentReader                    xmlCommentReader // |   , XmlCommentReader                    xmlCommentReader                    // | ||||||
|   , IActionDescriptorCollectionProvider actionDescriptorCollectionProvider |   , IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) // | ||||||
|   , RedLocker                           redLocker) // |     : RedisService<Sys_Api, string, IApiService>(rpo), IApiService | ||||||
|     : RedLockerService<Sys_Api, string, IApiService>(rpo, redLocker), IApiService |  | ||||||
| { | { | ||||||
|     /// <inheritdoc /> |     /// <inheritdoc /> | ||||||
|     public Task<int> BulkDeleteAsync(BulkReq<DelReq> req) |     public Task<int> BulkDeleteAsync(BulkReq<DelReq> req) | ||||||
|   | |||||||
| @@ -18,19 +18,23 @@ public sealed class UserService( | |||||||
|   , IEventPublisher                 eventPublisher)    // |   , IEventPublisher                 eventPublisher)    // | ||||||
|     : RepositoryService<Sys_User, long, IUserService>(rpo), IUserService |     : RepositoryService<Sys_User, long, IUserService>(rpo), IUserService | ||||||
| { | { | ||||||
|     private readonly Expression<Func<Sys_User, Sys_User>> _selectUserFields = a => new Sys_User { |     private readonly Expression<Func<Sys_User, Sys_User>> _listUserExp = a => new Sys_User { | ||||||
|         Id          = a.Id |                                                                                   Id            = a.Id | ||||||
|       , Avatar      = a.Avatar |                                                                                 , Avatar        = a.Avatar | ||||||
|       , Email       = a.Email |                                                                                 , Email         = a.Email | ||||||
|       , Mobile      = a.Mobile |                                                                                 , Mobile        = a.Mobile | ||||||
|       , Enabled     = a.Enabled |                                                                                 , Enabled       = a.Enabled | ||||||
|       , UserName    = a.UserName |                                                                                 , UserName      = a.UserName | ||||||
|       , Summary     = a.Summary |                                                                                 , Summary       = a.Summary | ||||||
|       , Version     = a.Version |                                                                                 , Version       = a.Version | ||||||
|       , CreatedTime = a.CreatedTime |                                                                                 , CreatedTime   = a.CreatedTime | ||||||
|       , Dept        = new Sys_Dept { Id = a.Dept.Id, Name = a.Dept.Name } |                                                                                 , LastLoginTime = a.LastLoginTime | ||||||
|       , Roles       = a.Roles |                                                                                 , Dept = new Sys_Dept { | ||||||
|     }; |                                                                                       Id   = a.Dept.Id | ||||||
|  |                                                                                     , Name = a.Dept.Name | ||||||
|  |                                                                                   } | ||||||
|  |                                                                                 , Roles = a.Roles | ||||||
|  |                                                                               }; | ||||||
|  |  | ||||||
|     /// <inheritdoc /> |     /// <inheritdoc /> | ||||||
|     public async Task<int> BulkDeleteAsync(BulkReq<DelReq> req) |     public async Task<int> BulkDeleteAsync(BulkReq<DelReq> req) | ||||||
| @@ -232,14 +236,15 @@ public sealed class UserService( | |||||||
|     public async Task<PagedQueryRsp<QueryUserRsp>> PagedQueryAsync(PagedQueryReq<QueryUserReq> req) |     public async Task<PagedQueryRsp<QueryUserRsp>> PagedQueryAsync(PagedQueryReq<QueryUserReq> req) | ||||||
|     { |     { | ||||||
|         req.ThrowIfInvalid(); |         req.ThrowIfInvalid(); | ||||||
|         var list = await (await QueryInternalAsync(req).ConfigureAwait(false)).Page(req.Page, req.PageSize) |         var listUserExp = req.GetToListExp<Sys_User>() ?? _listUserExp; | ||||||
|                                                                               #if DBTYPE_SQLSERVER |         var select      = await QueryInternalAsync(req, listUserExp == _listUserExp).ConfigureAwait(false); | ||||||
|                                                                               .WithLock(SqlServerLock.NoLock | |         var list = await select.Page(req.Page, req.PageSize) | ||||||
|                                                                                   SqlServerLock.NoWait) |                                #if DBTYPE_SQLSERVER | ||||||
|                                                                               #endif |                                .WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait) | ||||||
|                                                                               .Count(out var total) |                                #endif | ||||||
|                                                                               .ToListAsync(_selectUserFields) |                                .Count(out var total) | ||||||
|                                                                               .ConfigureAwait(false); |                                .ToListAsync(listUserExp) | ||||||
|  |                                .ConfigureAwait(false); | ||||||
|         return new PagedQueryRsp<QueryUserRsp>(req.Page, req.PageSize, total, list.Adapt<IEnumerable<QueryUserRsp>>()); |         return new PagedQueryRsp<QueryUserRsp>(req.Page, req.PageSize, total, list.Adapt<IEnumerable<QueryUserRsp>>()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -248,7 +253,7 @@ public sealed class UserService( | |||||||
|     { |     { | ||||||
|         req.ThrowIfInvalid(); |         req.ThrowIfInvalid(); | ||||||
|         var list = await (await QueryInternalAsync(req).ConfigureAwait(false)).Take(req.Count) |         var list = await (await QueryInternalAsync(req).ConfigureAwait(false)).Take(req.Count) | ||||||
|                                                                               .ToListAsync(_selectUserFields) |                                                                               .ToListAsync(_listUserExp) | ||||||
|                                                                               .ConfigureAwait(false); |                                                                               .ConfigureAwait(false); | ||||||
|         return list.Adapt<IEnumerable<QueryUserRsp>>(); |         return list.Adapt<IEnumerable<QueryUserRsp>>(); | ||||||
|     } |     } | ||||||
| @@ -437,22 +442,6 @@ public sealed class UserService( | |||||||
|         return dbUser.Adapt<UserInfoRsp>(); |         return dbUser.Adapt<UserInfoRsp>(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static LoginRsp LoginInternal(Sys_User dbUser) |  | ||||||
|     { |  | ||||||
|         if (!dbUser.Enabled) { |  | ||||||
|             throw new NetAdminInvalidOperationException(Ln.请联系管理员激活账号); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var tokenPayload |  | ||||||
|             = new Dictionary<string, object> { { nameof(ContextUserToken), dbUser.Adapt<ContextUserToken>() } }; |  | ||||||
|  |  | ||||||
|         var accessToken = JWTEncryption.Encrypt(tokenPayload); |  | ||||||
|         return new LoginRsp { |  | ||||||
|                                 AccessToken  = accessToken |  | ||||||
|                               , RefreshToken = JWTEncryption.GenerateRefreshToken(accessToken) |  | ||||||
|                             }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async Task<Dictionary<long, string>> CreateEditCheckAsync(CreateEditUserReq req) |     private async Task<Dictionary<long, string>> CreateEditCheckAsync(CreateEditUserReq req) | ||||||
|     { |     { | ||||||
|         // 检查角色是否存在 |         // 检查角色是否存在 | ||||||
| @@ -475,21 +464,44 @@ public sealed class UserService( | |||||||
|         return dept.Count != 1 ? throw new NetAdminInvalidOperationException(Ln.部门不存在) : roles; |         return dept.Count != 1 ? throw new NetAdminInvalidOperationException(Ln.部门不存在) : roles; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private ISelect<Sys_User> QueryInternal(QueryReq<QueryUserReq> req, IEnumerable<long> deptIds) |     private LoginRsp LoginInternal(Sys_User dbUser) | ||||||
|     { |     { | ||||||
|         var ret = Rpo.Select.Include(a => a.Dept) |         if (!dbUser.Enabled) { | ||||||
|                      .IncludeMany(a => a.Roles.Select(b => new Sys_Role { Id = b.Id, Name = b.Name })) |             throw new NetAdminInvalidOperationException(Ln.请联系管理员激活账号); | ||||||
|                      .WhereDynamicFilter(req.DynamicFilter) |         } | ||||||
|                      .WhereIf(deptIds != null, a => deptIds.Contains(a.DeptId)) |  | ||||||
|                      .WhereIf( // |         _ = UpdateAsync(dbUser with { LastLoginTime = DateTime.Now }, [nameof(Sys_User.LastLoginTime)] | ||||||
|                          req.Filter?.Id > 0, a => a.Id == req.Filter.Id) | ,                                                                     ignoreVersion: true); | ||||||
|                      .WhereIf( // |  | ||||||
|                          req.Filter?.RoleId > 0, a => a.Roles.Any(b => b.Id == req.Filter.RoleId)) |         var tokenPayload | ||||||
|                      .WhereIf( // |             = new Dictionary<string, object> { { nameof(ContextUserToken), dbUser.Adapt<ContextUserToken>() } }; | ||||||
|                          req.Keywords?.Length > 0 |  | ||||||
|                        , a => a.Id     == req.Keywords.Int64Try(0) || a.UserName == req.Keywords || |         var accessToken = JWTEncryption.Encrypt(tokenPayload); | ||||||
|                               a.Mobile == req.Keywords             || |         return new LoginRsp { | ||||||
|                               a.Email  == req.Keywords             || a.Summary.Contains(req.Keywords)); |                                 AccessToken  = accessToken | ||||||
|  |                               , RefreshToken = JWTEncryption.GenerateRefreshToken(accessToken) | ||||||
|  |                             }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private ISelect<Sys_User> QueryInternal(QueryReq<QueryUserReq> req, IEnumerable<long> deptIds | ||||||
|  |                                           , bool                   includeRoles = true) | ||||||
|  |     { | ||||||
|  |         var ret = Rpo.Select.Include(a => a.Dept); | ||||||
|  |         if (includeRoles) { | ||||||
|  |             ret = ret.IncludeMany(a => a.Roles.Select(b => new Sys_Role { Id = b.Id, Name = b.Name })); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ret = ret.WhereDynamicFilter(req.DynamicFilter) | ||||||
|  |                  .WhereIf(deptIds != null, a => deptIds.Contains(a.DeptId)) | ||||||
|  |                  .WhereIf( // | ||||||
|  |                      req.Filter?.Id > 0, a => a.Id == req.Filter.Id) | ||||||
|  |                  .WhereIf( // | ||||||
|  |                      req.Filter?.RoleId > 0, a => a.Roles.Any(b => b.Id == req.Filter.RoleId)) | ||||||
|  |                  .WhereIf( // | ||||||
|  |                      req.Keywords?.Length > 0 | ||||||
|  |                    , a => a.Id     == req.Keywords.Int64Try(0) || a.UserName == req.Keywords || | ||||||
|  |                           a.Mobile == req.Keywords             || | ||||||
|  |                           a.Email  == req.Keywords             || a.Summary.Contains(req.Keywords)); | ||||||
|  |  | ||||||
|         // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault |         // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault | ||||||
|         switch (req.Order) { |         switch (req.Order) { | ||||||
| @@ -518,7 +530,7 @@ public sealed class UserService( | |||||||
|         return QueryInternal(req, deptIds); |         return QueryInternal(req, deptIds); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private async Task<ISelect<Sys_User>> QueryInternalAsync(QueryReq<QueryUserReq> req) |     private async Task<ISelect<Sys_User>> QueryInternalAsync(QueryReq<QueryUserReq> req, bool includeRoles = true) | ||||||
|     { |     { | ||||||
|         IEnumerable<long> deptIds = null; |         IEnumerable<long> deptIds = null; | ||||||
|         if (req.Filter?.DeptId > 0) { |         if (req.Filter?.DeptId > 0) { | ||||||
| @@ -529,6 +541,6 @@ public sealed class UserService( | |||||||
|                                .ConfigureAwait(false); |                                .ConfigureAwait(false); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return QueryInternal(req, deptIds); |         return QueryInternal(req, deptIds, includeRoles); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -0,0 +1,49 @@ | |||||||
|  | using System.Net.WebSockets; | ||||||
|  | using NetAdmin.SysComponent.Cache.Sys.Dependency; | ||||||
|  |  | ||||||
|  | namespace NetAdmin.SysComponent.Host.Middlewares; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | ///     版本更新检查中间件 | ||||||
|  | /// </summary> | ||||||
|  | public sealed class VersionCheckerMiddleware(RequestDelegate next) | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     主函数 | ||||||
|  |     /// </summary> | ||||||
|  |     public async Task InvokeAsync(HttpContext context) | ||||||
|  |     { | ||||||
|  |         if (context.Request.Path == "/ws/version") { | ||||||
|  |             if (context.WebSockets.IsWebSocketRequest) { | ||||||
|  |                 var webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); | ||||||
|  |                 await ConnectionAsync(webSocket).ConfigureAwait(false); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 context.Response.StatusCode = 400; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             await next(context).ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static async Task ConnectionAsync(WebSocket webSocket) | ||||||
|  |     { | ||||||
|  |         var buffer = new byte[1024]; | ||||||
|  |         while (webSocket.State == WebSocketState.Open) { | ||||||
|  |             var receiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None) | ||||||
|  |                                                .ConfigureAwait(false); | ||||||
|  |             if (receiveResult.MessageType != WebSocketMessageType.Text) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var ver = await App.GetService<IToolsCache>().GetVersionAsync().ConfigureAwait(false); | ||||||
|  |             await webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(ver)), WebSocketMessageType.Text | ||||||
|  |                                     , true, CancellationToken.None) | ||||||
|  |                            .ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None) | ||||||
|  |                        .ConfigureAwait(false); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -5,7 +5,7 @@ | |||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|         <PackageReference Include="xunit" Version="2.9.0"/> |         <PackageReference Include="xunit" Version="2.9.0"/> | ||||||
|         <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0-preview.6.24328.4"/> |         <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0-preview.6.24328.4"/> | ||||||
|         <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"> |         <PackageReference Include="xunit.runner.visualstudio" Version="3.0.0-pre.24"> | ||||||
|             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|             <PrivateAssets>all</PrivateAssets> |             <PrivateAssets>all</PrivateAssets> | ||||||
|         </PackageReference> |         </PackageReference> | ||||||
|   | |||||||
| @@ -83,7 +83,7 @@ | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             .dark .app-loading__title { |             .dark .app-loading__title { | ||||||
|                 color: #d0d0d0; |                 color: #c0c0c0; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @keyframes loader { |             @keyframes loader { | ||||||
|   | |||||||
| @@ -12,13 +12,13 @@ | |||||||
|         "@element-plus/icons-vue": "^2.3.1", |         "@element-plus/icons-vue": "^2.3.1", | ||||||
|         "ace-builds": "^1.35.4", |         "ace-builds": "^1.35.4", | ||||||
|         "aieditor": "^1.0.13", |         "aieditor": "^1.0.13", | ||||||
|         "axios": "^1.7.2", |         "axios": "^1.7.3", | ||||||
|         "clipboard": "^2.0.11", |         "clipboard": "^2.0.11", | ||||||
|         "core-js": "^3.37.1", |         "core-js": "^3.38.0", | ||||||
|         "cropperjs": "^1.6.2", |         "cropperjs": "^1.6.2", | ||||||
|         "crypto-js": "^4.2.0", |         "crypto-js": "^4.2.0", | ||||||
|         "echarts": "^5.5.1", |         "echarts": "^5.5.1", | ||||||
|         "element-plus": "^2.7.8", |         "element-plus": "^2.8.0", | ||||||
|         "json-bigint": "^1.0.0", |         "json-bigint": "^1.0.0", | ||||||
|         "json5-to-table": "^0.1.8", |         "json5-to-table": "^0.1.8", | ||||||
|         "markdown-it": "^14.1.0", |         "markdown-it": "^14.1.0", | ||||||
| @@ -28,21 +28,21 @@ | |||||||
|         "qrcodejs2": "^0.0.2", |         "qrcodejs2": "^0.0.2", | ||||||
|         "sortablejs": "^1.15.2", |         "sortablejs": "^1.15.2", | ||||||
|         "vkbeautify": "^0.99.3", |         "vkbeautify": "^0.99.3", | ||||||
|         "vue": "^3.4.34", |         "vue": "^3.4.37", | ||||||
|         "vue-i18n": "^9.13.1", |         "vue-i18n": "^9.13.1", | ||||||
|         "vue-router": "^4.4.0", |         "vue-router": "^4.4.3", | ||||||
|         "vue3-ace-editor": "^2.2.4", |         "vue3-ace-editor": "^2.2.4", | ||||||
|         "vue3-json-viewer": "^2.2.2", |         "vue3-json-viewer": "^2.2.2", | ||||||
|         "vuedraggable": "^4.0.3", |         "vuedraggable": "^4.0.3", | ||||||
|         "vuex": "^4.1.0" |         "vuex": "^4.1.0" | ||||||
|     }, |     }, | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|         "@vitejs/plugin-vue": "^5.1.1", |         "@vitejs/plugin-vue": "^5.1.2", | ||||||
|         "prettier": "^3.3.3", |         "prettier": "^3.3.3", | ||||||
|         "prettier-plugin-organize-attributes": "^1.0.0", |         "prettier-plugin-organize-attributes": "^1.0.0", | ||||||
|         "sass": "^1.77.8", |         "sass": "^1.77.8", | ||||||
|         "terser": "^5.31.3", |         "terser": "^5.31.5", | ||||||
|         "vite": "^5.3.5" |         "vite": "^5.4.0" | ||||||
|     }, |     }, | ||||||
|     "browserslist": [ |     "browserslist": [ | ||||||
|         "> 1%", |         "> 1%", | ||||||
|   | |||||||
| @@ -21,6 +21,21 @@ | |||||||
|                 :placeholder="item.placeholder" |                 :placeholder="item.placeholder" | ||||||
|                 :style="item.style" |                 :style="item.style" | ||||||
|                 clearable /> |                 clearable /> | ||||||
|  |             <el-input | ||||||
|  |                 v-if="item.type === 'select-input' && (!item.condition || item.condition())" | ||||||
|  |                 v-model="form[item.field[0]][selectInputKey]" | ||||||
|  |                 v-role="item.role || '*/*/*'" | ||||||
|  |                 :class="item.class" | ||||||
|  |                 :placeholder="item.placeholder" | ||||||
|  |                 :style="item.style" | ||||||
|  |                 @change="trimSpaces(item.field[0])" | ||||||
|  |                 clearable> | ||||||
|  |                 <template #prepend> | ||||||
|  |                     <el-select v-model="selectInputKey" :placeholder="$t('查询字段')" :style="item.selectStyle" @change="selectInputChange(item)"> | ||||||
|  |                         <el-option v-for="(field, j) in item.field[1]" :label="field.label" :value="field.key" /> | ||||||
|  |                     </el-select> | ||||||
|  |                 </template> | ||||||
|  |             </el-input> | ||||||
|             <sc-select |             <sc-select | ||||||
|                 v-else-if="item.type === 'remote-select' && (!item.condition || item.condition())" |                 v-else-if="item.type === 'remote-select' && (!item.condition || item.condition())" | ||||||
|                 v-model="form[item.field[0]][item.field[1]]" |                 v-model="form[item.field[0]][item.field[1]]" | ||||||
| @@ -93,6 +108,7 @@ export default { | |||||||
|     }, |     }, | ||||||
|     data() { |     data() { | ||||||
|         return { |         return { | ||||||
|  |             selectInputKey: null, | ||||||
|             dateShortCuts: [ |             dateShortCuts: [ | ||||||
|                 { |                 { | ||||||
|                     text: this.$t('今日'), |                     text: this.$t('今日'), | ||||||
| @@ -342,6 +358,7 @@ export default { | |||||||
|     }, |     }, | ||||||
|     mounted() {}, |     mounted() {}, | ||||||
|     async created() { |     async created() { | ||||||
|  |         this.selectInputKey = this.controls.find((x) => x.type === 'select-input')?.field[1][0].key | ||||||
|         if (this.dateType === 'datetimerange') { |         if (this.dateType === 'datetimerange') { | ||||||
|             this.dateShortCuts.unshift( |             this.dateShortCuts.unshift( | ||||||
|                 { |                 { | ||||||
| @@ -390,6 +407,14 @@ export default { | |||||||
|         }, |         }, | ||||||
|     }, |     }, | ||||||
|     methods: { |     methods: { | ||||||
|  |         trimSpaces(key) { | ||||||
|  |             this.form[key][this.selectInputKey] = this.form[key][this.selectInputKey].replace(/^\s*(.*?)\s*$/g, '$1') | ||||||
|  |         }, | ||||||
|  |         selectInputChange(item) { | ||||||
|  |             for (const field of item.field[1]) { | ||||||
|  |                 delete this.form[item.field[0]][field.key] | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         vkbeautify() { |         vkbeautify() { | ||||||
|             return vkbeautify |             return vkbeautify | ||||||
|         }, |         }, | ||||||
|   | |||||||
| @@ -32,7 +32,9 @@ export default { | |||||||
|     data() { |     data() { | ||||||
|         return { |         return { | ||||||
|             user: {}, |             user: {}, | ||||||
|             form: {}, |             form: { | ||||||
|  |                 requiredFields: ['Id', 'UserName', 'Mobile'], | ||||||
|  |             }, | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     watch: { |     watch: { | ||||||
|   | |||||||
| @@ -2,20 +2,26 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { h } from 'vue' | import { h } from 'vue' | ||||||
|  | import config from '@/config' | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|     data() { |     data() { | ||||||
|         return {} |         return {} | ||||||
|     }, |     }, | ||||||
|     async created() { |     async created() { | ||||||
|         setInterval(async () => { |         const ws = new WebSocket(`ws://${config.API_URL.replace('http://', '')}/ws/version`) | ||||||
|             // 检查版本 |         ws.onopen = () => { | ||||||
|             const res = await this.$API.sys_tools.getVersion.post({}) |             ws.send('1') | ||||||
|  |         } | ||||||
|  |         ws.onmessage = async (res) => { | ||||||
|             if (res.data !== this.$TOOL.data.get('APP_VERSION')) { |             if (res.data !== this.$TOOL.data.get('APP_VERSION')) { | ||||||
|                 this.$TOOL.data.set('APP_VERSION', res.data) |                 await this.$TOOL.data.set('APP_VERSION', res.data) | ||||||
|                 this.showTip(res.data.slice(0, res.data.indexOf('+'))) |                 this.showTip(res.data.slice(0, res.data.indexOf('+'))) | ||||||
|  |             } else { | ||||||
|  |                 await new Promise((x) => setTimeout(x, 10000)) | ||||||
|  |                 ws.send('1') | ||||||
|             } |             } | ||||||
|         }, 10000) |         } | ||||||
|     }, |     }, | ||||||
|     methods: { |     methods: { | ||||||
|         /** |         /** | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ | |||||||
|             <el-card v-for="job in jobs" :class="`user-bar-jobs-item ${job.lastStatusCode === 'oK' ? '' : 'alert'}`" :key="job.id" shadow="hover"> |             <el-card v-for="job in jobs" :class="`user-bar-jobs-item ${job.lastStatusCode === 'oK' ? '' : 'alert'}`" :key="job.id" shadow="hover"> | ||||||
|                 <div class="user-bar-jobs-item-body"> |                 <div class="user-bar-jobs-item-body"> | ||||||
|                     <div class="jobIcon"> |                     <div class="jobIcon"> | ||||||
|                         {{ job.lastStatusCode }} |                         {{ job.lastStatusCode.toUpperCase() }} | ||||||
|                     </div> |                     </div> | ||||||
|                     <div class="jobMain"> |                     <div class="jobMain"> | ||||||
|                         <div class="title"> |                         <div class="title"> | ||||||
|   | |||||||
| @@ -495,4 +495,6 @@ export default { | |||||||
|     一行一个: 'One line per item', |     一行一个: 'One line per item', | ||||||
|     请输入字段名: 'Please enter field name', |     请输入字段名: 'Please enter field name', | ||||||
|     请输入操作符: 'Please enter operator', |     请输入操作符: 'Please enter operator', | ||||||
|  |     查询字段: 'Query field', | ||||||
|  |     最后登录: 'Last login', | ||||||
| } | } | ||||||
| @@ -492,4 +492,6 @@ export default { | |||||||
|     一行一个: '一行一个', |     一行一个: '一行一个', | ||||||
|     请输入字段名: '请输入字段名', |     请输入字段名: '请输入字段名', | ||||||
|     请输入操作符: '请输入操作符', |     请输入操作符: '请输入操作符', | ||||||
|  |     查询字段: '查询字段', | ||||||
|  |     最后登录: '最后登录', | ||||||
| } | } | ||||||
| @@ -2,7 +2,10 @@ | |||||||
|  |  | ||||||
| html.dark { | html.dark { | ||||||
|     //变量 |     //变量 | ||||||
|     --el-text-color-primary: #d0d0d0; |     --el-text-color-primary: #c0c0c0; | ||||||
|  |     --el-text-color-regular: #c0c0c0; | ||||||
|  |     --el-text-color-secondary: #666666; | ||||||
|  |     --el-mask-color: rgba(29, 30, 31, 0.9); | ||||||
|     --el-color-primary-dark-2: var(--el-color-primary-light-2) !important; |     --el-color-primary-dark-2: var(--el-color-primary-light-2) !important; | ||||||
|     --el-color-primary-light-9: var(--el-color-primary-dark-8) !important; |     --el-color-primary-light-9: var(--el-color-primary-dark-8) !important; | ||||||
|     --el-color-primary-light-8: var(--el-color-primary-dark-7) !important; |     --el-color-primary-light-8: var(--el-color-primary-dark-7) !important; | ||||||
| @@ -73,7 +76,7 @@ html.dark { | |||||||
|     .el-header, |     .el-header, | ||||||
|     .el-main.nopadding, |     .el-main.nopadding, | ||||||
|     .el-footer { |     .el-footer { | ||||||
|         background: var(--el-bg-color-overlay); |         background: var(--el-bg-color); | ||||||
|         border-color: var(--el-border-color-light); |         border-color: var(--el-border-color-light); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -93,4 +96,16 @@ html.dark { | |||||||
|     .el-table th.is-sortable:hover { |     .el-table th.is-sortable:hover { | ||||||
|         background: #111; |         background: #111; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     .jv-container .jv-code.open { | ||||||
|  |         background: var(--el-bg-color) !important; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .ace_editor { | ||||||
|  |         background: var(--el-bg-color-overlay); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .ace_gutter { | ||||||
|  |         background: var(--el-bg-color-overlay); | ||||||
|  |     } | ||||||
| } | } | ||||||
| @@ -257,7 +257,9 @@ export default { | |||||||
|         async copyJob(row) { |         async copyJob(row) { | ||||||
|             let loading = this.$loading() |             let loading = this.$loading() | ||||||
|             try { |             try { | ||||||
|                 const res = await this.$API.sys_job.create.post(Object.assign({}, row, { id: 0, jobName: row.jobName + '-copy' })) |                 const res = await this.$API.sys_job.create.post( | ||||||
|  |                     Object.assign({}, row, { id: 0, jobName: row.jobName + '-copy', enabled: false, nextTimeId: null, status: 'idle' }), | ||||||
|  |                 ) | ||||||
|                 if (res.data) { |                 if (res.data) { | ||||||
|                     this.$message.success(this.$t('操作成功')) |                     this.$message.success(this.$t('操作成功')) | ||||||
|                 } else { |                 } else { | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ | |||||||
|                             v-model:value="form.requestHeader" |                             v-model:value="form.requestHeader" | ||||||
|                             :theme="this.$TOOL.data.get('APP_SET_DARK') || this.$CONFIG.APP_SET_DARK ? 'github_dark' : 'github'" |                             :theme="this.$TOOL.data.get('APP_SET_DARK') || this.$CONFIG.APP_SET_DARK ? 'github_dark' : 'github'" | ||||||
|                             lang="json" |                             lang="json" | ||||||
|                             style="height: 5rem; width: 100%" /> |                             style="height: 10rem; width: 100%" /> | ||||||
|                         <el-button @click="form.requestHeader = jsonFormat(form.requestHeader)" type="text">{{ $t('JSON格式化') }}</el-button> |                         <el-button @click="form.requestHeader = jsonFormat(form.requestHeader)" type="text">{{ $t('JSON格式化') }}</el-button> | ||||||
|                     </el-form-item> |                     </el-form-item> | ||||||
|                     <el-form-item :label="$t('请求体')" prop="requestBody"> |                     <el-form-item :label="$t('请求体')" prop="requestBody"> | ||||||
| @@ -52,7 +52,7 @@ | |||||||
|                             v-model:value="form.requestBody" |                             v-model:value="form.requestBody" | ||||||
|                             :theme="this.$TOOL.data.get('APP_SET_DARK') || this.$CONFIG.APP_SET_DARK ? 'github_dark' : 'github'" |                             :theme="this.$TOOL.data.get('APP_SET_DARK') || this.$CONFIG.APP_SET_DARK ? 'github_dark' : 'github'" | ||||||
|                             lang="json" |                             lang="json" | ||||||
|                             style="height: 10rem; width: 100%" /> |                             style="height: 15rem; width: 100%" /> | ||||||
|                         <el-button @click="form.requestBody = jsonFormat(form.requestBody)" type="text">{{ $t('JSON格式化') }}</el-button> |                         <el-button @click="form.requestBody = jsonFormat(form.requestBody)" type="text">{{ $t('JSON格式化') }}</el-button> | ||||||
|                     </el-form-item> |                     </el-form-item> | ||||||
|                     <el-form-item :label="$t('请求的网络地址')" prop="requestUrl"> |                     <el-form-item :label="$t('请求的网络地址')" prop="requestUrl"> | ||||||
| @@ -107,7 +107,7 @@ | |||||||
|                 </el-form> |                 </el-form> | ||||||
|             </el-tab-pane> |             </el-tab-pane> | ||||||
|             <el-tab-pane v-if="mode === 'view'" :label="$t('执行记录')" name="record"> |             <el-tab-pane v-if="mode === 'view'" :label="$t('执行记录')" name="record"> | ||||||
|                 <record v-if="tabId === 'record'" :keywords="form.id" /> |                 <record v-if="tabId === 'record'" :job-id="form.id" /> | ||||||
|             </el-tab-pane> |             </el-tab-pane> | ||||||
|             <el-tab-pane v-if="mode === 'view'" :label="$t('原始数据')"> |             <el-tab-pane v-if="mode === 'view'" :label="$t('原始数据')"> | ||||||
|                 <json-viewer |                 <json-viewer | ||||||
|   | |||||||
| @@ -28,13 +28,21 @@ | |||||||
|                             style: 'width:20rem', |                             style: 'width:20rem', | ||||||
|                         }, |                         }, | ||||||
|                         { |                         { | ||||||
|                             type: 'input', |                             type: 'select-input', | ||||||
|                             field: ['root', 'keywords'], |                             field: [ | ||||||
|                             placeholder: $t('作业编号 / 作业名称 / 执行编号'), |                                 'dy', | ||||||
|                             style: 'width:20rem', |                                 [ | ||||||
|  |                                     { label: '唯一编码', key: 'id' }, | ||||||
|  |                                     { label: '作业编号', key: 'jobId' }, | ||||||
|  |                                 ], | ||||||
|  |                             ], | ||||||
|  |                             placeholder: '匹配内容', | ||||||
|  |                             style: 'width:25rem', | ||||||
|  |                             selectStyle: 'width:8rem', | ||||||
|                         }, |                         }, | ||||||
|                     ]" |                     ]" | ||||||
|                     :vue="this" |                     :vue="this" | ||||||
|  |                     @reset="onReset" | ||||||
|                     @search="onSearch" |                     @search="onSearch" | ||||||
|                     dateFormat="YYYY-MM-DD HH:mm:ss" |                     dateFormat="YYYY-MM-DD HH:mm:ss" | ||||||
|                     dateType="datetimerange" |                     dateType="datetimerange" | ||||||
| @@ -73,7 +81,7 @@ | |||||||
|                                 :data="row" |                                 :data="row" | ||||||
|                                 :options=" |                                 :options=" | ||||||
|                                     Object.entries(this.$GLOBAL.enums.httpMethods).map((x) => { |                                     Object.entries(this.$GLOBAL.enums.httpMethods).map((x) => { | ||||||
|                                         return { value: x[0], text: `${x[1][1]}`, type: x[1][2] } |                                         return { value: x[0], text: `${x[1][1].toString().toUpperCase()}`, type: x[1][2] } | ||||||
|                                     }) |                                     }) | ||||||
|                                 " |                                 " | ||||||
|                                 prop="httpMethod" /> |                                 prop="httpMethod" /> | ||||||
| @@ -96,8 +104,8 @@ | |||||||
|                     align="right" |                     align="right" | ||||||
|                     prop="duration" |                     prop="duration" | ||||||
|                     sortable="custom" |                     sortable="custom" | ||||||
|                     width="150" /> |                     width="100" /> | ||||||
|                 <el-table-column :label="$t('作业信息')" prop="jobId" show-overflow-tooltip sortable="custom" width="500"> |                 <el-table-column :label="$t('作业信息')" min-width="150" prop="jobId" show-overflow-tooltip sortable="custom"> | ||||||
|                     <template #default="{ row }"> |                     <template #default="{ row }"> | ||||||
|                         <p> |                         <p> | ||||||
|                             <el-link @click="jobClick(row.job)"> |                             <el-link @click="jobClick(row.job)"> | ||||||
| @@ -109,7 +117,7 @@ | |||||||
|                         </p> |                         </p> | ||||||
|                     </template> |                     </template> | ||||||
|                 </el-table-column> |                 </el-table-column> | ||||||
|                 <el-table-column :label="$t('响应体')" prop="responseBody" show-overflow-tooltip sortable="custom" /> |                 <el-table-column :label="$t('响应体')" min-width="300" prop="responseBody" show-overflow-tooltip sortable="custom" /> | ||||||
|                 <na-col-operation :buttons="[naColOperation.buttons[0]]" :vue="this" width="100" /> |                 <na-col-operation :buttons="[naColOperation.buttons[0]]" :vue="this" width="100" /> | ||||||
|             </sc-table> |             </sc-table> | ||||||
|         </el-main> |         </el-main> | ||||||
| @@ -165,6 +173,13 @@ export default { | |||||||
|                 }), |                 }), | ||||||
|             }) |             }) | ||||||
|         } |         } | ||||||
|  |         if (this.jobId) { | ||||||
|  |             this.query.dynamicFilter.filters.push({ | ||||||
|  |                 field: 'jobId', | ||||||
|  |                 operator: 'eq', | ||||||
|  |                 value: this.jobId, | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|     }, |     }, | ||||||
|     data() { |     data() { | ||||||
|         return { |         return { | ||||||
| @@ -181,7 +196,6 @@ export default { | |||||||
|                     ], |                     ], | ||||||
|                 }, |                 }, | ||||||
|                 filter: {}, |                 filter: {}, | ||||||
|                 keywords: this.keywords, |  | ||||||
|             }, |             }, | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
| @@ -190,6 +204,11 @@ export default { | |||||||
|         jobClick(job) { |         jobClick(job) { | ||||||
|             this.dialog.job = { mode: 'view', row: { id: job.id } } |             this.dialog.job = { mode: 'view', row: { id: job.id } } | ||||||
|         }, |         }, | ||||||
|  |         onReset() { | ||||||
|  |             if (this.jobId) { | ||||||
|  |                 this.$refs.search.selectInputKey = 'jobId' | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         //搜索 |         //搜索 | ||||||
|         onSearch(form) { |         onSearch(form) { | ||||||
|             if (Array.isArray(form.dy.createdTime)) { |             if (Array.isArray(form.dy.createdTime)) { | ||||||
| @@ -223,6 +242,22 @@ export default { | |||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             if (typeof form.dy.jobId === 'string' && form.dy.jobId.trim() !== '') { | ||||||
|  |                 this.query.dynamicFilter.filters.push({ | ||||||
|  |                     field: 'jobId', | ||||||
|  |                     operator: 'eq', | ||||||
|  |                     value: form.dy.jobId, | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (typeof form.dy.jobId === 'number' && form.dy.jobId !== 0) { | ||||||
|  |                 this.query.dynamicFilter.filters.push({ | ||||||
|  |                     field: 'jobId', | ||||||
|  |                     operator: 'eq', | ||||||
|  |                     value: form.dy.jobId, | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |  | ||||||
|             if (typeof form.dy.httpMethod === 'string' && form.dy.httpMethod.trim() !== '') { |             if (typeof form.dy.httpMethod === 'string' && form.dy.httpMethod.trim() !== '') { | ||||||
|                 this.query.dynamicFilter.filters.push({ |                 this.query.dynamicFilter.filters.push({ | ||||||
|                     field: 'httpMethod', |                     field: 'httpMethod', | ||||||
| @@ -234,12 +269,13 @@ export default { | |||||||
|         }, |         }, | ||||||
|     }, |     }, | ||||||
|     mounted() { |     mounted() { | ||||||
|         if (this.keywords) { |         if (this.jobId) { | ||||||
|             this.$refs.search.form.root.keywords = this.keywords |             this.$refs.search.selectInputKey = 'jobId' | ||||||
|  |             this.$refs.search.form.dy.jobId = this.jobId | ||||||
|             this.$refs.search.keeps.push({ |             this.$refs.search.keeps.push({ | ||||||
|                 field: 'keywords', |                 field: 'jobId', | ||||||
|                 value: this.keywords, |                 value: this.jobId, | ||||||
|                 type: 'root', |                 type: 'dy', | ||||||
|             }) |             }) | ||||||
|         } |         } | ||||||
|         if (this.statusCodes) { |         if (this.statusCodes) { | ||||||
| @@ -260,7 +296,7 @@ export default { | |||||||
|             type: 'dy', |             type: 'dy', | ||||||
|         }) |         }) | ||||||
|     }, |     }, | ||||||
|     props: ['keywords', 'statusCodes'], |     props: ['statusCodes', 'jobId'], | ||||||
|     watch: {}, |     watch: {}, | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -44,10 +44,18 @@ | |||||||
|                             style: 'width:20rem', |                             style: 'width:20rem', | ||||||
|                         }, |                         }, | ||||||
|                         { |                         { | ||||||
|                             type: 'input', |                             type: 'select-input', | ||||||
|                             field: ['root', 'keywords'], |                             field: [ | ||||||
|                             placeholder: $t('日志编号 / 用户编号 / 客户端IP'), |                                 'dy', | ||||||
|  |                                 [ | ||||||
|  |                                     { label: '日志编号', key: 'id' }, | ||||||
|  |                                     { label: '用户编号', key: 'ownerId' }, | ||||||
|  |                                     { label: '客户端IP', key: 'createdClientIp' }, | ||||||
|  |                                 ], | ||||||
|  |                             ], | ||||||
|  |                             placeholder: '匹配内容', | ||||||
|                             style: 'width:25rem', |                             style: 'width:25rem', | ||||||
|  |                             selectStyle: 'width:8rem', | ||||||
|                         }, |                         }, | ||||||
|                     ]" |                     ]" | ||||||
|                     :vue="this" |                     :vue="this" | ||||||
| @@ -178,6 +186,9 @@ export default { | |||||||
|         if (this.ownerId) { |         if (this.ownerId) { | ||||||
|             this.query.dynamicFilter.filters.push({ field: 'ownerId', operator: 'eq', value: this.ownerId }) |             this.query.dynamicFilter.filters.push({ field: 'ownerId', operator: 'eq', value: this.ownerId }) | ||||||
|         } |         } | ||||||
|  |         if (this.excludeApiPathCrc32) { | ||||||
|  |             this.query.dynamicFilter.filters.push({ field: 'apiPathCrc32', operator: 'notEqual', value: this.excludeApiPathCrc32 }) | ||||||
|  |         } | ||||||
|     }, |     }, | ||||||
|     data() { |     data() { | ||||||
|         return { |         return { | ||||||
| @@ -194,10 +205,7 @@ export default { | |||||||
|                         { |                         { | ||||||
|                             field: 'createdTime', |                             field: 'createdTime', | ||||||
|                             operator: 'dateRange', |                             operator: 'dateRange', | ||||||
|                             value: [ |                             value: [this.$TOOL.dateFormat(new Date(), 'yyyy-MM-dd'), this.$TOOL.dateFormat(new Date(), 'yyyy-MM-dd')], | ||||||
|                                 this.$TOOL.dateFormat(new Date(new Date() - 3600 * 1000), 'yyyy-MM-dd hh:mm:ss'), |  | ||||||
|                                 this.$TOOL.dateFormat(new Date(), 'yyyy-MM-dd hh:mm:ss'), |  | ||||||
|                             ], |  | ||||||
|                         }, |                         }, | ||||||
|                     ], |                     ], | ||||||
|                 }, |                 }, | ||||||
| @@ -268,6 +276,13 @@ export default { | |||||||
|                     }), |                     }), | ||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
|  |             if (typeof form.dy['excludeApiPathCrc32'] === 'number' && form.dy['excludeApiPathCrc32'] !== 0) { | ||||||
|  |                 this.query.dynamicFilter.filters.push({ | ||||||
|  |                     field: 'apiPathCrc32', | ||||||
|  |                     operator: 'notEqual', | ||||||
|  |                     value: form.dy['excludeApiPathCrc32'], | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|             if (typeof form.dy['apiPathCrc32'] === 'number' && form.dy['apiPathCrc32'] !== 0) { |             if (typeof form.dy['apiPathCrc32'] === 'number' && form.dy['apiPathCrc32'] !== 0) { | ||||||
|                 this.query.dynamicFilter.filters.push({ |                 this.query.dynamicFilter.filters.push({ | ||||||
|                     field: 'apiPathCrc32', |                     field: 'apiPathCrc32', | ||||||
| @@ -284,6 +299,29 @@ export default { | |||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             if (typeof form.dy.ownerId === 'string' && form.dy.ownerId.trim() !== '') { | ||||||
|  |                 this.query.dynamicFilter.filters.push({ | ||||||
|  |                     field: 'ownerId', | ||||||
|  |                     operator: 'eq', | ||||||
|  |                     value: form.dy.ownerId, | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |             if (typeof form.dy.id === 'string' && form.dy.id.trim() !== '') { | ||||||
|  |                 this.query.dynamicFilter.filters.push({ | ||||||
|  |                     field: 'id', | ||||||
|  |                     operator: 'eq', | ||||||
|  |                     value: form.dy.id, | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (typeof form.dy.createdClientIp === 'string' && form.dy.createdClientIp.trim() !== '') { | ||||||
|  |                 this.query.dynamicFilter.filters.push({ | ||||||
|  |                     field: 'createdClientIp', | ||||||
|  |                     operator: 'eq', | ||||||
|  |                     value: form.dy.createdClientIp, | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |  | ||||||
|             if (typeof form.dy.operationResult === 'boolean') { |             if (typeof form.dy.operationResult === 'boolean') { | ||||||
|                 this.query.dynamicFilter.filters.push( |                 this.query.dynamicFilter.filters.push( | ||||||
|                     form.dy.operationResult |                     form.dy.operationResult | ||||||
| @@ -334,9 +372,18 @@ export default { | |||||||
|             this.$refs.search.form.dy.ownerId = this.ownerId |             this.$refs.search.form.dy.ownerId = this.ownerId | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (this.excludeApiPathCrc32) { | ||||||
|  |             this.$refs.search.keeps.push({ | ||||||
|  |                 field: 'excludeApiPathCrc32', | ||||||
|  |                 value: this.excludeApiPathCrc32, | ||||||
|  |                 type: 'dy', | ||||||
|  |             }) | ||||||
|  |             this.$refs.search.form.dy.excludeApiPathCrc32 = this.excludeApiPathCrc32 | ||||||
|  |         } | ||||||
|  |  | ||||||
|         this.$refs.search.form.dy.createdTime = [ |         this.$refs.search.form.dy.createdTime = [ | ||||||
|             this.$TOOL.dateFormat(new Date(new Date() - 3600 * 1000), 'yyyy-MM-dd hh:mm:ss'), |             this.$TOOL.dateFormat(new Date(), 'yyyy-MM-dd 00:00:00'), | ||||||
|             this.$TOOL.dateFormat(new Date(), 'yyyy-MM-dd hh:mm:ss'), |             this.$TOOL.dateFormat(new Date(), 'yyyy-MM-dd 00:00:00'), | ||||||
|         ] |         ] | ||||||
|         this.$refs.search.keeps.push({ |         this.$refs.search.keeps.push({ | ||||||
|             field: 'createdTime', |             field: 'createdTime', | ||||||
| @@ -344,7 +391,7 @@ export default { | |||||||
|             type: 'dy', |             type: 'dy', | ||||||
|         }) |         }) | ||||||
|     }, |     }, | ||||||
|     props: ['keywords', 'ownerId'], |     props: ['keywords', 'ownerId', 'excludeApiPathCrc32'], | ||||||
|     watch: {}, |     watch: {}, | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -102,6 +102,11 @@ | |||||||
|                     field="name" |                     field="name" | ||||||
|                     prop="dept" |                     prop="dept" | ||||||
|                     width="200" /> |                     width="200" /> | ||||||
|  |                 <el-table-column :label="$t('最后登录')" align="right" prop="lastLoginTime" sortable="custom" width="120"> | ||||||
|  |                     <template #default="{ row }"> | ||||||
|  |                         <span v-time.tip="row.lastLoginTime" :title="row.lastLoginTime"></span> | ||||||
|  |                     </template> | ||||||
|  |                 </el-table-column> | ||||||
|                 <el-table-column :label="$t('启用')" align="center" prop="enabled" sortable="custom" width="100"> |                 <el-table-column :label="$t('启用')" align="center" prop="enabled" sortable="custom" width="100"> | ||||||
|                     <template #default="{ row }"> |                     <template #default="{ row }"> | ||||||
|                         <el-switch v-model="row.enabled" @change="changeSwitch($event, row)"></el-switch> |                         <el-switch v-model="row.enabled" @change="changeSwitch($event, row)"></el-switch> | ||||||
|   | |||||||
| @@ -70,6 +70,9 @@ | |||||||
|                             <el-switch v-model="form.enabled"></el-switch> |                             <el-switch v-model="form.enabled"></el-switch> | ||||||
|                         </el-form-item> |                         </el-form-item> | ||||||
|                     </template> |                     </template> | ||||||
|  |                     <el-form-item v-if="mode === 'view'" :label="$t('最后登录')" prop="summary"> | ||||||
|  |                         <el-input v-model="form.lastLoginTime" clearable></el-input> | ||||||
|  |                     </el-form-item> | ||||||
|                     <el-form-item :label="$t('备注')" prop="summary"> |                     <el-form-item :label="$t('备注')" prop="summary"> | ||||||
|                         <el-input v-model="form.summary" clearable type="textarea"></el-input> |                         <el-input v-model="form.summary" clearable type="textarea"></el-input> | ||||||
|                     </el-form-item> |                     </el-form-item> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 GitHub
						GitHub