diff --git a/assets/res/Fields.ln b/assets/res/Fields.ln index e86f980b..95b42809 100644 --- a/assets/res/Fields.ln +++ b/assets/res/Fields.ln @@ -78,6 +78,7 @@ 日期范围 是否启用 显示仪表板 +最后登录时间 未处理异常 未婚 未读 diff --git a/build/code.quality.props b/build/code.quality.props index a6b66f29..9c1ce338 100644 --- a/build/code.quality.props +++ b/build/code.quality.props @@ -23,7 +23,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/package.json b/package.json index fa5de7e3..809ee2ca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "version": "1.5.0", "devDependencies": { - "cz-git": "^1.9.3", + "cz-git": "^1.9.4", "commitizen": "^4.3.0", "prettier": "^3.3.3", "standard-version": "^9.5.0" @@ -11,4 +11,4 @@ "path": "node_modules/cz-git" } } -} +} \ No newline at end of file diff --git a/scripts/switcher.furion.json b/scripts/switcher.furion.json index 8aaa8b08..8ea70fdc 100644 --- a/scripts/switcher.furion.json +++ b/scripts/switcher.furion.json @@ -9,7 +9,7 @@ "packages": [ { "packageName": "Furion.Pure.NS", - "version": "4.9.4.6-ns4" + "version": "4.9.5.2-ns1" } ] } diff --git a/src/backend/NetAdmin.AdmServer.Host/Startup.cs b/src/backend/NetAdmin.AdmServer.Host/Startup.cs index 67c16c86..8de3924b 100644 --- a/src/backend/NetAdmin.AdmServer.Host/Startup.cs +++ b/src/backend/NetAdmin.AdmServer.Host/Startup.cs @@ -3,11 +3,9 @@ using NetAdmin.AdmServer.Host.Extensions; using NetAdmin.Host.Extensions; using NetAdmin.Host.Middlewares; using NetAdmin.SysComponent.Host.Extensions; +using NetAdmin.SysComponent.Host.Middlewares; using Spectre.Console.Cli; using ValidationResult = Spectre.Console.ValidationResult; -#if !DEBUG -using Prometheus; -#endif NetAdmin.Host.Startup.Entry(args); @@ -36,7 +34,7 @@ namespace NetAdmin.AdmServer.Host .UseOpenApiSkin() // 使用OpenApiSkin中间件(仅在调试模式下),提供Swagger UI皮肤 #else .UseVueAdmin() // 托管管理后台,仅在非调试模式下 - .UseHttpMetrics() // 使用HttpMetrics中间件,启用HTTP性能监控 + .UsePrometheus() // 使用Prometheus中间件,启用HTTP性能监控 #endif .UseInject(string.Empty) // 使用Inject中间件,Furion脚手架的依赖注入支持 .UseUnifyResultStatusCodes() // 使用UnifyResultStatusCodes中间件,用于统一处理结果状态码 @@ -45,6 +43,8 @@ namespace NetAdmin.AdmServer.Host .UseAuthentication() // 使用Authentication中间件,启用身份验证 .UseAuthorization() // 使用Authorization中间件,启用授权 .UseMiddleware() // 使用RemoveNullNodeMiddleware中间件,删除JSON中的空节点 + .UseWebSockets() // 使用WebSockets中间件,启用WebSocket支持 + .UseMiddleware() // 使用VersionUpdaterMiddleware中间件,用于检查版本 .UseEndpoints(); // 配置端点以处理请求 _ = lifeTime.ApplicationStopping.Register(SafetyShopHostMiddleware.OnStopping); } diff --git a/src/backend/NetAdmin.AdmServer.Tests/NetAdmin.AdmServer.Tests.csproj b/src/backend/NetAdmin.AdmServer.Tests/NetAdmin.AdmServer.Tests.csproj index edf2ba66..0bba584e 100644 --- a/src/backend/NetAdmin.AdmServer.Tests/NetAdmin.AdmServer.Tests.csproj +++ b/src/backend/NetAdmin.AdmServer.Tests/NetAdmin.AdmServer.Tests.csproj @@ -4,6 +4,6 @@ - + \ No newline at end of file diff --git a/src/backend/NetAdmin.Application/Services/RedLockerService.cs b/src/backend/NetAdmin.Application/Services/RedLockerService.cs deleted file mode 100644 index 9f5a83c3..00000000 --- a/src/backend/NetAdmin.Application/Services/RedLockerService.cs +++ /dev/null @@ -1,50 +0,0 @@ -using NetAdmin.Application.Repositories; -using RedLockNet; - -namespace NetAdmin.Application.Services; - -/// -/// RedLocker Service Base -/// -public abstract class RedLockerService( - BasicRepository rpo - , RedLocker redLocker) : RepositoryService(rpo) - where TEntity : EntityBase // - where TPrimary : IEquatable -{ - /// - /// 获取锁 - /// - protected Task GetLockerAsync(string lockName) - { - return GetLockerAsync(lockName, TimeSpan.FromSeconds(Numbers.SECS_RED_LOCK_WAIT) - , TimeSpan.FromSeconds(Numbers.SECS_RED_LOCK_RETRY_INTERVAL)); - } - - /// - /// 获取锁 - /// - /// NetAdminGetLockerException - protected async Task 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(); - } - - /// - /// 获取锁 - /// - /// - /// 不重试,失败直接抛出异常 - /// - protected Task GetLockerOnceAsync(string lockName) - { - return GetLockerAsync(lockName, default, default); - } -} \ No newline at end of file diff --git a/src/backend/NetAdmin.Application/Services/RedisService.cs b/src/backend/NetAdmin.Application/Services/RedisService.cs new file mode 100644 index 00000000..16236bda --- /dev/null +++ b/src/backend/NetAdmin.Application/Services/RedisService.cs @@ -0,0 +1,47 @@ +using NetAdmin.Application.Repositories; +using StackExchange.Redis; + +namespace NetAdmin.Application.Services; + +/// +/// Redis Service Base +/// +/// +/// Initializes a new instance of the class. +/// Redis Service Base +/// +public abstract class RedisService(BasicRepository rpo) + : RepositoryService(rpo) + where TEntity : EntityBase // + where TPrimary : IEquatable +{ + /// + /// Redis Database + /// + protected IDatabase RedisDatabase { get; } // + = App.GetService() + .GetDatabase(App.GetOptions() + .Instances.First(x => x.Name == Chars.FLG_REDIS_INSTANCE_DATA_CACHE) + .Database); + + /// + /// 获取锁 + /// + protected Task 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)); + } + + /// + /// 获取锁(仅获取一次) + /// + protected Task GetLockerOnceAsync(string lockerName) + { + return RedisLocker.GetLockerAsync(RedisDatabase, lockerName + , TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_EXPIRY), 1 + , TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_RETRY_DELAY)); + } +} \ No newline at end of file diff --git a/src/backend/NetAdmin.Application/Services/RepositoryService.cs b/src/backend/NetAdmin.Application/Services/RepositoryService.cs index 356dec99..006fb3f9 100644 --- a/src/backend/NetAdmin.Application/Services/RepositoryService.cs +++ b/src/backend/NetAdmin.Application/Services/RepositoryService.cs @@ -35,7 +35,7 @@ public abstract class RepositoryService(BasicReposit /// protected async Task ExportAsync( // Func, ISelectGrouping> selector, QueryReq query, string fileName - , Expression, object>> listExp) + , Expression, object>> listExp = null) where TQuery : DataAbstraction, new() { var list = await selector(query).Take(Numbers.MAX_LIMIT_EXPORT).ToListAsync(listExp).ConfigureAwait(false); @@ -95,6 +95,7 @@ public abstract class RepositoryService(BasicReposit /// 包含的属性 /// 排除的属性 /// 查询表达式 + /// 查询sql /// 是否忽略版本锁 /// 更新后的实体列表 protected Task> UpdateReturnListAsync( // @@ -102,11 +103,15 @@ public abstract class RepositoryService(BasicReposit , IEnumerable includeFields // , string[] excludeFields = null // , Expression> whereExp = null // + , string whereSql = null // , bool ignoreVersion = false) { // 默认匹配主键 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 diff --git a/src/backend/NetAdmin.Domain/Attributes/DangerFieldAttribute.cs b/src/backend/NetAdmin.Domain/Attributes/DangerFieldAttribute.cs new file mode 100644 index 00000000..63deb7c0 --- /dev/null +++ b/src/backend/NetAdmin.Domain/Attributes/DangerFieldAttribute.cs @@ -0,0 +1,7 @@ +namespace NetAdmin.Domain.Attributes; + +/// +/// 危险字段标记 +/// +[AttributeUsage(AttributeTargets.Property)] +public sealed class DangerFieldAttribute : Attribute; \ No newline at end of file diff --git a/src/backend/NetAdmin.Domain/DbMaps/Sys/Sys_User.cs b/src/backend/NetAdmin.Domain/DbMaps/Sys/Sys_User.cs index afb60a4d..b161d56b 100644 --- a/src/backend/NetAdmin.Domain/DbMaps/Sys/Sys_User.cs +++ b/src/backend/NetAdmin.Domain/DbMaps/Sys/Sys_User.cs @@ -51,6 +51,14 @@ public record Sys_User : VersionEntity, IFieldSummary, IFieldEnabled, IRegister [JsonIgnore] public virtual bool Enabled { get; init; } + /// + /// 最后登录时间 + /// + [Column] + [CsvIgnore] + [JsonIgnore] + public virtual DateTime? LastLoginTime { get; init; } + /// /// 手机号码 /// @@ -64,6 +72,7 @@ public record Sys_User : VersionEntity, IFieldSummary, IFieldEnabled, IRegister /// [Column] [CsvIgnore] + [DangerField] [JsonIgnore] public Guid Password { get; init; } diff --git a/src/backend/NetAdmin.Domain/Dto/Dependency/QueryReq.cs b/src/backend/NetAdmin.Domain/Dto/Dependency/QueryReq.cs index cc06091e..3b8ff65d 100644 --- a/src/backend/NetAdmin.Domain/Dto/Dependency/QueryReq.cs +++ b/src/backend/NetAdmin.Domain/Dto/Dependency/QueryReq.cs @@ -36,4 +36,37 @@ public record QueryReq : DataAbstraction /// 排序字段 /// public string Prop { get; init; } + + /// + /// 所需字段 + /// + public string[] RequiredFields { get; set; } + + /// + /// 列表表达式 + /// + public Expression> GetToListExp() + { + if (RequiredFields.NullOrEmpty()) { + return null; + } + + var expParameter = Expression.Parameter(typeof(TEntity), "a"); + var bindings = new List(); + + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (var field in RequiredFields) { + var prop = typeof(TEntity).GetProperty(field); + if (prop == null || prop.GetCustomAttribute() != 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>(expBody, expParameter); + } } \ No newline at end of file diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/Api/ExportApiRsp.cs b/src/backend/NetAdmin.Domain/Dto/Sys/Api/ExportApiRsp.cs index 0c975774..23f45161 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/Api/ExportApiRsp.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/Api/ExportApiRsp.cs @@ -3,7 +3,7 @@ namespace NetAdmin.Domain.Dto.Sys.Api; /// /// 响应:导出接口 /// -public record ExportApiRsp : QueryApiRsp +public sealed record ExportApiRsp : QueryApiRsp { /// [CsvIgnore] diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/Config/ExportConfigRsp.cs b/src/backend/NetAdmin.Domain/Dto/Sys/Config/ExportConfigRsp.cs index ef260b0b..1ac80f88 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/Config/ExportConfigRsp.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/Config/ExportConfigRsp.cs @@ -6,7 +6,7 @@ namespace NetAdmin.Domain.Dto.Sys.Config; /// /// 响应:导出配置 /// -public record ExportConfigRsp : QueryConfigRsp, IRegister +public sealed record ExportConfigRsp : QueryConfigRsp, IRegister { /// [CsvIndex(6)] diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/Dept/ExportDeptRsp.cs b/src/backend/NetAdmin.Domain/Dto/Sys/Dept/ExportDeptRsp.cs index dfb0fc33..3a2424c2 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/Dept/ExportDeptRsp.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/Dept/ExportDeptRsp.cs @@ -3,7 +3,7 @@ namespace NetAdmin.Domain.Dto.Sys.Dept; /// /// 响应:导出部门 /// -public record ExportDeptRsp : QueryDeptRsp +public sealed record ExportDeptRsp : QueryDeptRsp { /// [CsvIgnore] diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/Dic/Content/ExportDicContentRsp.cs b/src/backend/NetAdmin.Domain/Dto/Sys/Dic/Content/ExportDicContentRsp.cs index f8e9639a..b78689a2 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/Dic/Content/ExportDicContentRsp.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/Dic/Content/ExportDicContentRsp.cs @@ -3,7 +3,7 @@ namespace NetAdmin.Domain.Dto.Sys.Dic.Content; /// /// 响应:导出字典内容 /// -public record ExportDicContentRsp : QueryDicContentRsp +public sealed record ExportDicContentRsp : QueryDicContentRsp { /// [CsvIndex(2)] diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/Job/ExportJobRsp.cs b/src/backend/NetAdmin.Domain/Dto/Sys/Job/ExportJobRsp.cs index 544cfcf6..58a87fab 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/Job/ExportJobRsp.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/Job/ExportJobRsp.cs @@ -7,7 +7,7 @@ namespace NetAdmin.Domain.Dto.Sys.Job; /// /// 响应:导出计划作业 /// -public record ExportJobRsp : QueryJobRsp +public sealed record ExportJobRsp : QueryJobRsp { /// [CsvIndex(5)] diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/JobRecord/CreateJobRecordReq.cs b/src/backend/NetAdmin.Domain/Dto/Sys/JobRecord/CreateJobRecordReq.cs index df6c3b8f..a7ae2dd1 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/JobRecord/CreateJobRecordReq.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/JobRecord/CreateJobRecordReq.cs @@ -3,4 +3,4 @@ namespace NetAdmin.Domain.Dto.Sys.JobRecord; /// /// 请求:创建计划作业执行记录 /// -public record CreateJobRecordReq : Sys_JobRecord; \ No newline at end of file +public sealed record CreateJobRecordReq : Sys_JobRecord; \ No newline at end of file diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/JobRecord/ExportJobRecordRsp.cs b/src/backend/NetAdmin.Domain/Dto/Sys/JobRecord/ExportJobRecordRsp.cs index 66531d2f..3c2963d1 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/JobRecord/ExportJobRecordRsp.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/JobRecord/ExportJobRecordRsp.cs @@ -5,7 +5,7 @@ namespace NetAdmin.Domain.Dto.Sys.JobRecord; /// /// 响应:导出计划作业执行记录 /// -public record ExportJobRecordRsp : QueryJobRecordRsp, IRegister +public sealed record ExportJobRecordRsp : QueryJobRecordRsp, IRegister { /// [CsvIndex(1)] diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/LoginLog/CreateLoginLogReq.cs b/src/backend/NetAdmin.Domain/Dto/Sys/LoginLog/CreateLoginLogReq.cs index fee1abca..f5c4ee96 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/LoginLog/CreateLoginLogReq.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/LoginLog/CreateLoginLogReq.cs @@ -7,7 +7,7 @@ namespace NetAdmin.Domain.Dto.Sys.LoginLog; /// /// 请求:创建登录日志 /// -public record CreateLoginLogReq : Sys_LoginLog, IRegister +public sealed record CreateLoginLogReq : Sys_LoginLog, IRegister { /// public void Register(TypeAdapterConfig config) diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/RequestLog/ExportRequestLogRsp.cs b/src/backend/NetAdmin.Domain/Dto/Sys/RequestLog/ExportRequestLogRsp.cs index e4324262..8efc6671 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/RequestLog/ExportRequestLogRsp.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/RequestLog/ExportRequestLogRsp.cs @@ -8,7 +8,7 @@ namespace NetAdmin.Domain.Dto.Sys.RequestLog; /// /// 响应:导出请求日志 /// -public record ExportRequestLogRsp : QueryRequestLogRsp +public sealed record ExportRequestLogRsp : QueryRequestLogRsp { /// /// 接口路径 diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/RequestLogDetail/CreateRequestLogDetailReq.cs b/src/backend/NetAdmin.Domain/Dto/Sys/RequestLogDetail/CreateRequestLogDetailReq.cs index dfe5c113..8d48caad 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/RequestLogDetail/CreateRequestLogDetailReq.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/RequestLogDetail/CreateRequestLogDetailReq.cs @@ -3,4 +3,4 @@ namespace NetAdmin.Domain.Dto.Sys.RequestLogDetail; /// /// 请求:创建请求日志明细 /// -public record CreateRequestLogDetailReq : Sys_RequestLogDetail; \ No newline at end of file +public sealed record CreateRequestLogDetailReq : Sys_RequestLogDetail; \ No newline at end of file diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsg/ExportSiteMsgRsp.cs b/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsg/ExportSiteMsgRsp.cs index 036691c9..88d0b74e 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsg/ExportSiteMsgRsp.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsg/ExportSiteMsgRsp.cs @@ -8,7 +8,7 @@ namespace NetAdmin.Domain.Dto.Sys.SiteMsg; /// /// 响应:导出站内信 /// -public record ExportSiteMsgRsp : QuerySiteMsgRsp +public sealed record ExportSiteMsgRsp : QuerySiteMsgRsp { /// [CsvIndex(5)] diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsgDept/CreateSiteMsgDeptReq.cs b/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsgDept/CreateSiteMsgDeptReq.cs index 3cd27ff2..f4f85a14 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsgDept/CreateSiteMsgDeptReq.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsgDept/CreateSiteMsgDeptReq.cs @@ -3,4 +3,4 @@ namespace NetAdmin.Domain.Dto.Sys.SiteMsgDept; /// /// 请求:创建站内信-部门映射 /// -public record CreateSiteMsgDeptReq : Sys_SiteMsgDept; \ No newline at end of file +public sealed record CreateSiteMsgDeptReq : Sys_SiteMsgDept; \ No newline at end of file diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsgRole/CreateSiteMsgRoleReq.cs b/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsgRole/CreateSiteMsgRoleReq.cs index 04e74c36..29c7dfe6 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsgRole/CreateSiteMsgRoleReq.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsgRole/CreateSiteMsgRoleReq.cs @@ -3,4 +3,4 @@ namespace NetAdmin.Domain.Dto.Sys.SiteMsgRole; /// /// 请求:创建站内信-角色映射 /// -public record CreateSiteMsgRoleReq : Sys_SiteMsgRole; \ No newline at end of file +public sealed record CreateSiteMsgRoleReq : Sys_SiteMsgRole; \ No newline at end of file diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsgUser/CreateSiteMsgUserReq.cs b/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsgUser/CreateSiteMsgUserReq.cs index 1568d1be..57557bce 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsgUser/CreateSiteMsgUserReq.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/SiteMsgUser/CreateSiteMsgUserReq.cs @@ -3,4 +3,4 @@ namespace NetAdmin.Domain.Dto.Sys.SiteMsgUser; /// /// 请求:创建站内信-用户映射 /// -public record CreateSiteMsgUserReq : Sys_SiteMsgUser; \ No newline at end of file +public sealed record CreateSiteMsgUserReq : Sys_SiteMsgUser; \ No newline at end of file diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/User/ExportUserRsp.cs b/src/backend/NetAdmin.Domain/Dto/Sys/User/ExportUserRsp.cs index 31ae043b..96c62c26 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/User/ExportUserRsp.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/User/ExportUserRsp.cs @@ -6,7 +6,7 @@ namespace NetAdmin.Domain.Dto.Sys.User; /// /// 响应:导出用户 /// -public record ExportUserRsp : QueryUserRsp +public sealed record ExportUserRsp : QueryUserRsp { /// [CsvIndex(7)] @@ -43,6 +43,12 @@ public record ExportUserRsp : QueryUserRsp [CsvName(nameof(Ln.唯一编码))] public override long Id { get; init; } + /// + [CsvIndex(8)] + [CsvIgnore(false)] + [CsvName(nameof(Ln.最后登录时间))] + public override DateTime? LastLoginTime { get; init; } + /// [CsvIndex(2)] [CsvIgnore(false)] diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/User/QueryUserRsp.cs b/src/backend/NetAdmin.Domain/Dto/Sys/User/QueryUserRsp.cs index d27e2d9a..6e6d58a3 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/User/QueryUserRsp.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/User/QueryUserRsp.cs @@ -31,6 +31,10 @@ public record QueryUserRsp : Sys_User [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public override long Id { get; init; } + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public override DateTime? LastLoginTime { get; init; } + /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public override string Mobile { get; init; } diff --git a/src/backend/NetAdmin.Domain/Dto/Sys/UserProfile/SetSessionUserAppConfigReq.cs b/src/backend/NetAdmin.Domain/Dto/Sys/UserProfile/SetSessionUserAppConfigReq.cs index 31786a04..99308fa9 100644 --- a/src/backend/NetAdmin.Domain/Dto/Sys/UserProfile/SetSessionUserAppConfigReq.cs +++ b/src/backend/NetAdmin.Domain/Dto/Sys/UserProfile/SetSessionUserAppConfigReq.cs @@ -3,7 +3,7 @@ namespace NetAdmin.Domain.Dto.Sys.UserProfile; /// /// 请求:设置当前用户应用配置 /// -public record SetSessionUserAppConfigReq : Sys_UserProfile +public sealed record SetSessionUserAppConfigReq : Sys_UserProfile { /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/src/backend/NetAdmin.Domain/NetAdmin.Domain.csproj b/src/backend/NetAdmin.Domain/NetAdmin.Domain.csproj index 89ba658b..d0d678da 100644 --- a/src/backend/NetAdmin.Domain/NetAdmin.Domain.csproj +++ b/src/backend/NetAdmin.Domain/NetAdmin.Domain.csproj @@ -13,7 +13,6 @@ - \ No newline at end of file diff --git a/src/backend/NetAdmin.Host/BackgroundRunning/WorkBase.cs b/src/backend/NetAdmin.Host/BackgroundRunning/WorkBase.cs index 2e544f65..9ab18258 100644 --- a/src/backend/NetAdmin.Host/BackgroundRunning/WorkBase.cs +++ b/src/backend/NetAdmin.Host/BackgroundRunning/WorkBase.cs @@ -1,4 +1,4 @@ -using RedLockNet; +using StackExchange.Redis; namespace NetAdmin.Host.BackgroundRunning; @@ -7,8 +7,6 @@ namespace NetAdmin.Host.BackgroundRunning; /// public abstract class WorkBase { - private readonly RedLocker _redLocker; - /// /// Initializes a new instance of the class. /// @@ -17,7 +15,6 @@ public abstract class WorkBase ServiceProvider = App.GetService().CreateScope().ServiceProvider; UowManager = ServiceProvider.GetService(); Logger = ServiceProvider.GetService>(); - _redLocker = ServiceProvider.GetService(); } /// @@ -53,10 +50,8 @@ public abstract class WorkBase { if (singleInstance) { // 加锁 - await using var redLock = await GetLockerAsync(GetType().FullName).ConfigureAwait(false); - if (!redLock.IsAcquired) { - throw new NetAdminGetLockerException(); - } + var lockName = GetType().FullName; + await using var redisLocker = await GetLockerAsync(lockName).ConfigureAwait(false); await WorkflowAsync(cancelToken).ConfigureAwait(false); return; @@ -68,10 +63,15 @@ public abstract class WorkBase /// /// 获取锁 /// - private Task GetLockerAsync(string lockId) + private Task GetLockerAsync(string lockId) { - return _redLocker.RedLockFactory.CreateLockAsync(lockId, TimeSpan.FromSeconds(Numbers.SECS_RED_LOCK_EXPIRY) - , TimeSpan.FromSeconds(Numbers.SECS_RED_LOCK_WAIT) - , TimeSpan.FromSeconds(Numbers.SECS_RED_LOCK_RETRY_INTERVAL)); + var db = ServiceProvider.GetService() + .GetDatabase(ServiceProvider.GetService>() + .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)); } } \ No newline at end of file diff --git a/src/backend/NetAdmin.Host/Extensions/IApplicationBuilderExtensions.cs b/src/backend/NetAdmin.Host/Extensions/IApplicationBuilderExtensions.cs index 133a8a46..d1c9d7bb 100644 --- a/src/backend/NetAdmin.Host/Extensions/IApplicationBuilderExtensions.cs +++ b/src/backend/NetAdmin.Host/Extensions/IApplicationBuilderExtensions.cs @@ -3,6 +3,7 @@ using IGeekFan.AspNetCore.Knife4jUI; #else using Prometheus; +using Prometheus.HttpMetrics; #endif namespace NetAdmin.Host.Extensions; @@ -40,5 +41,22 @@ public static class IApplicationBuilderExtensions } }); } + #else + /// + /// 使用 Prometheus + /// + 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 } \ No newline at end of file diff --git a/src/backend/NetAdmin.Host/Utils/RequestLogger.cs b/src/backend/NetAdmin.Host/Utils/RequestLogger.cs index 5c13cfd8..a5f17fb9 100644 --- a/src/backend/NetAdmin.Host/Utils/RequestLogger.cs +++ b/src/backend/NetAdmin.Host/Utils/RequestLogger.cs @@ -28,6 +28,7 @@ public sealed class RequestLogger(ILogger logger, IEventPublisher , x => context.Request.ContentType?.Contains(x, StringComparison.OrdinalIgnoreCase) ?? false) ? await context.ReadBodyContentAsync().ConfigureAwait(false) : string.Empty; + var apiId = context.Request.Path.Value!.TrimStart('/'); var auditData = new CreateRequestLogReq // { Detail = new CreateRequestLogDetailReq // @@ -47,7 +48,7 @@ public sealed class RequestLogger(ILogger logger, IEventPublisher } , Duration = (int)duration , HttpMethod = Enum.Parse(context.Request.Method, true) - , ApiPathCrc32 = context.Request.Path.Value!.TrimStart('/').Crc32() + , ApiPathCrc32 = apiId.Crc32() , HttpStatusCode = context.Response.StatusCode , CreatedClientIp = context.GetRealIpAddress()?.MapToIPv4().ToString().IpV4ToInt32() , OwnerId = associatedUser?.UserId @@ -56,11 +57,14 @@ public sealed class RequestLogger(ILogger logger, IEventPublisher , TraceId = context.GetTraceId() }; - // 打印日志 - logger.Info(auditData); + // ReSharper disable once InvertIf + 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; } diff --git a/src/backend/NetAdmin.Infrastructure/Constant/Numbers.cs b/src/backend/NetAdmin.Infrastructure/Constant/Numbers.cs index 33435377..9b67b067 100644 --- a/src/backend/NetAdmin.Infrastructure/Constant/Numbers.cs +++ b/src/backend/NetAdmin.Infrastructure/Constant/Numbers.cs @@ -16,20 +16,20 @@ public static class Numbers public const int HTTP_STATUS_BIZ_FAIL = 900; // Http状态码-业务异常 public const long ID_DIC_CATALOG_GEO_AREA = 379794295185413; // 唯一编号:字典目录-行政区划字典 - public const int MAX_LIMIT_BULK_REQ = 100; // 最大限制:批量请求数 - 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_SQL = 4096; // 最大限制:打印长度(SQL 语句) - public const int MAX_LIMIT_QUERY = 1000; // 最大限制:非分页查询条数 - public const int MAX_LIMIT_QUERY_PAGE_NO = 10000; // 最大限制:分页查询页码 - public const int MAX_LIMIT_QUERY_PAGE_SIZE = 100; // 最大限制:分页查询页容量 + public const int MAX_LIMIT_BULK_REQ = 100; // 最大限制:批量请求数 + 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_SQL = 4096; // 最大限制:打印长度(SQL 语句) + public const int MAX_LIMIT_QUERY = 1000; // 最大限制:非分页查询条数 + public const int MAX_LIMIT_QUERY_PAGE_NO = 10000; // 最大限制:分页查询页码 + 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_DEFAULT = 60; // 秒:缓存时间-默认 - public const int SECS_CACHE_DIC_CATALOG_CODE = 300; // 秒:缓存时间-字典配置-目录代码 - public const int SECS_RED_LOCK_EXPIRY = 30; // 秒:RedLock-锁过期时间,假如持有锁的进程挂掉,最多在此时间内锁将被释放(如持有锁的进程正常,此值不会生效) - public const int SECS_RED_LOCK_RETRY_INTERVAL = 1; // 秒:RedLock-锁等待时间内,多久尝试获取一次 - public const int SECS_RED_LOCK_WAIT = 10; // 秒:RedLock-锁等待时间,相同的 resource 如果当前的锁被其他线程占用,最多等待时间 - public const int SECS_TIMEOUT_HTTP_CLIENT = 15; // 秒:超时时间-默认HTTP客户端 - public const int SECS_TIMEOUT_JOB = 600; // 秒:超时时间-作业 + public const int SECS_CACHE_CHART = 300; // 秒:缓存时间-仪表 + public const int SECS_CACHE_DEFAULT = 60; // 秒:缓存时间-默认 + public const int SECS_CACHE_DIC_CATALOG_CODE = 300; // 秒:缓存时间-字典配置-目录代码 + public const int SECS_REDIS_LOCK_EXPIRY = 60; // 秒:Redis锁过期时间 + public const int SECS_REDIS_LOCK_RETRY_DELAY = 1; // 秒:Redis锁重试间隔 + public const int SECS_TIMEOUT_HTTP_CLIENT = 15; // 秒:超时时间-默认HTTP客户端 + public const int SECS_TIMEOUT_JOB = 600; // 秒:超时时间-作业 } \ No newline at end of file diff --git a/src/backend/NetAdmin.Infrastructure/Exceptions/NetAdminGetLockerException.cs b/src/backend/NetAdmin.Infrastructure/Exceptions/NetAdminGetLockerException.cs index 7a53bf6c..fb8871b8 100644 --- a/src/backend/NetAdmin.Infrastructure/Exceptions/NetAdminGetLockerException.cs +++ b/src/backend/NetAdmin.Infrastructure/Exceptions/NetAdminGetLockerException.cs @@ -7,5 +7,5 @@ namespace NetAdmin.Infrastructure.Exceptions; /// 并发执行时锁竞争失败 /// #pragma warning disable RCS1194 -public sealed class NetAdminGetLockerException() : NetAdminInvalidOperationException(null) { } +public sealed class NetAdminGetLockerException(string message = null) : NetAdminInvalidOperationException(message) { } #pragma warning restore RCS1194 \ No newline at end of file diff --git a/src/backend/NetAdmin.Infrastructure/GlobalStatic.cs b/src/backend/NetAdmin.Infrastructure/GlobalStatic.cs index 360c80eb..23e435f9 100644 --- a/src/backend/NetAdmin.Infrastructure/GlobalStatic.cs +++ b/src/backend/NetAdmin.Infrastructure/GlobalStatic.cs @@ -24,6 +24,11 @@ public static class GlobalStatic #endif ; + /// + /// 日志记录器忽略的API编号 + /// + public static string[] LoggerIgnoreApiIds => []; + /// /// 系统内部密钥 /// diff --git a/src/backend/NetAdmin.Infrastructure/NetAdmin.Infrastructure.csproj b/src/backend/NetAdmin.Infrastructure/NetAdmin.Infrastructure.csproj index 69faa155..7f1a5486 100644 --- a/src/backend/NetAdmin.Infrastructure/NetAdmin.Infrastructure.csproj +++ b/src/backend/NetAdmin.Infrastructure/NetAdmin.Infrastructure.csproj @@ -8,13 +8,13 @@ - - - + + + + - diff --git a/src/backend/NetAdmin.Infrastructure/Utils/RedLocker.cs b/src/backend/NetAdmin.Infrastructure/Utils/RedLocker.cs deleted file mode 100644 index 4ee2fe70..00000000 --- a/src/backend/NetAdmin.Infrastructure/Utils/RedLocker.cs +++ /dev/null @@ -1,98 +0,0 @@ -using RedLockNet.SERedis; -using RedLockNet.SERedis.Configuration; -using StackExchange.Redis; - -namespace NetAdmin.Infrastructure.Utils; - -/// -/// Redis 分布锁 -/// -#pragma warning disable DesignedForInheritance -public class RedLocker : IDisposable, ISingleton -#pragma warning restore DesignedForInheritance -{ - // Track whether Dispose has been called. - private bool _disposed; - - /// - /// Initializes a new instance of the class. - /// - public RedLocker(IOptions redisOptions) - { - RedLockFactory = RedLockFactory.Create( // - new List // - { - ConnectionMultiplexer.Connect( // - redisOptions.Value.Instances.First(x => x.Name == Chars.FLG_REDIS_INSTANCE_DATA_CACHE).ConnStr) - }); - } - - /// - /// Finalizes an instance of the 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. - /// - ~RedLocker() - { - // Do not re-create Dispose clean-up code here. - // Calling Dispose(disposing: false) is optimal in terms of - // readability and maintainability. - Dispose(false); - } - - /// - /// RedLockFactory - /// - public RedLockFactory RedLockFactory { get; } - - /// - /// Implement IDisposable. - /// Do not make this method virtual. - /// A derived class should not be able to override this method. - /// - 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); - } - - /// - /// 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. - /// - 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; - } -} \ No newline at end of file diff --git a/src/backend/NetAdmin.Infrastructure/Utils/RedisLocker.cs b/src/backend/NetAdmin.Infrastructure/Utils/RedisLocker.cs new file mode 100644 index 00000000..523e13b6 --- /dev/null +++ b/src/backend/NetAdmin.Infrastructure/Utils/RedisLocker.cs @@ -0,0 +1,70 @@ +using StackExchange.Redis; + +namespace NetAdmin.Infrastructure.Utils; + +/// +/// Redis 分布锁 +/// +/// +/// Initializes a new instance of the class. +/// +#pragma warning disable DesignedForInheritance +public sealed class RedisLocker : IAsyncDisposable +#pragma warning restore DesignedForInheritance +{ + private readonly IDatabase _redisDatabase; + + private readonly string _redisKey; + + /// + /// Initializes a new instance of the class. + /// Redis 分布锁 + /// + private RedisLocker(IDatabase redisDatabase, string redisKey) + { + _redisDatabase = redisDatabase; + _redisKey = redisKey; + } + + /// + /// 获取锁 + /// + /// NetAdminGetLockerException + public static async Task 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().Error(ex.Message); + } + + if (setOk) { + return new RedisLocker(redisDatabase, lockerName); + } + + await Task.Delay(retryDelay).ConfigureAwait(false); + } + + throw new NetAdminGetLockerException(lockerName); + } + + /// + public async ValueTask DisposeAsync() + { + try { + _ = await _redisDatabase.KeyDeleteAsync(_redisKey, CommandFlags.DemandMaster | CommandFlags.FireAndForget) + .ConfigureAwait(false); + } + catch (Exception ex) { + LogHelper.Get().Error(ex.Message); + } + } +} \ No newline at end of file diff --git a/src/backend/NetAdmin.SysComponent.Application/Services/Sys/ApiService.cs b/src/backend/NetAdmin.SysComponent.Application/Services/Sys/ApiService.cs index 49078079..7a905dc7 100644 --- a/src/backend/NetAdmin.SysComponent.Application/Services/Sys/ApiService.cs +++ b/src/backend/NetAdmin.SysComponent.Application/Services/Sys/ApiService.cs @@ -8,11 +8,10 @@ namespace NetAdmin.SysComponent.Application.Services.Sys; /// public sealed class ApiService( - BasicRepository rpo // - , XmlCommentReader xmlCommentReader // - , IActionDescriptorCollectionProvider actionDescriptorCollectionProvider - , RedLocker redLocker) // - : RedLockerService(rpo, redLocker), IApiService + BasicRepository rpo // + , XmlCommentReader xmlCommentReader // + , IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) // + : RedisService(rpo), IApiService { /// public Task BulkDeleteAsync(BulkReq req) diff --git a/src/backend/NetAdmin.SysComponent.Application/Services/Sys/UserService.cs b/src/backend/NetAdmin.SysComponent.Application/Services/Sys/UserService.cs index c73a3bd3..b8c785b4 100644 --- a/src/backend/NetAdmin.SysComponent.Application/Services/Sys/UserService.cs +++ b/src/backend/NetAdmin.SysComponent.Application/Services/Sys/UserService.cs @@ -18,19 +18,23 @@ public sealed class UserService( , IEventPublisher eventPublisher) // : RepositoryService(rpo), IUserService { - private readonly Expression> _selectUserFields = a => new Sys_User { - Id = a.Id - , Avatar = a.Avatar - , Email = a.Email - , Mobile = a.Mobile - , Enabled = a.Enabled - , UserName = a.UserName - , Summary = a.Summary - , Version = a.Version - , CreatedTime = a.CreatedTime - , Dept = new Sys_Dept { Id = a.Dept.Id, Name = a.Dept.Name } - , Roles = a.Roles - }; + private readonly Expression> _listUserExp = a => new Sys_User { + Id = a.Id + , Avatar = a.Avatar + , Email = a.Email + , Mobile = a.Mobile + , Enabled = a.Enabled + , UserName = a.UserName + , Summary = a.Summary + , Version = a.Version + , CreatedTime = a.CreatedTime + , LastLoginTime = a.LastLoginTime + , Dept = new Sys_Dept { + Id = a.Dept.Id + , Name = a.Dept.Name + } + , Roles = a.Roles + }; /// public async Task BulkDeleteAsync(BulkReq req) @@ -232,14 +236,15 @@ public sealed class UserService( public async Task> PagedQueryAsync(PagedQueryReq req) { req.ThrowIfInvalid(); - var list = await (await QueryInternalAsync(req).ConfigureAwait(false)).Page(req.Page, req.PageSize) - #if DBTYPE_SQLSERVER - .WithLock(SqlServerLock.NoLock | - SqlServerLock.NoWait) - #endif - .Count(out var total) - .ToListAsync(_selectUserFields) - .ConfigureAwait(false); + var listUserExp = req.GetToListExp() ?? _listUserExp; + var select = await QueryInternalAsync(req, listUserExp == _listUserExp).ConfigureAwait(false); + var list = await select.Page(req.Page, req.PageSize) + #if DBTYPE_SQLSERVER + .WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait) + #endif + .Count(out var total) + .ToListAsync(listUserExp) + .ConfigureAwait(false); return new PagedQueryRsp(req.Page, req.PageSize, total, list.Adapt>()); } @@ -248,7 +253,7 @@ public sealed class UserService( { req.ThrowIfInvalid(); var list = await (await QueryInternalAsync(req).ConfigureAwait(false)).Take(req.Count) - .ToListAsync(_selectUserFields) + .ToListAsync(_listUserExp) .ConfigureAwait(false); return list.Adapt>(); } @@ -437,22 +442,6 @@ public sealed class UserService( return dbUser.Adapt(); } - private static LoginRsp LoginInternal(Sys_User dbUser) - { - if (!dbUser.Enabled) { - throw new NetAdminInvalidOperationException(Ln.请联系管理员激活账号); - } - - var tokenPayload - = new Dictionary { { nameof(ContextUserToken), dbUser.Adapt() } }; - - var accessToken = JWTEncryption.Encrypt(tokenPayload); - return new LoginRsp { - AccessToken = accessToken - , RefreshToken = JWTEncryption.GenerateRefreshToken(accessToken) - }; - } - private async Task> CreateEditCheckAsync(CreateEditUserReq req) { // 检查角色是否存在 @@ -475,21 +464,44 @@ public sealed class UserService( return dept.Count != 1 ? throw new NetAdminInvalidOperationException(Ln.部门不存在) : roles; } - private ISelect QueryInternal(QueryReq req, IEnumerable deptIds) + private LoginRsp LoginInternal(Sys_User dbUser) { - var ret = Rpo.Select.Include(a => a.Dept) - .IncludeMany(a => a.Roles.Select(b => new Sys_Role { Id = b.Id, Name = b.Name })) - .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)); + if (!dbUser.Enabled) { + throw new NetAdminInvalidOperationException(Ln.请联系管理员激活账号); + } + + _ = UpdateAsync(dbUser with { LastLoginTime = DateTime.Now }, [nameof(Sys_User.LastLoginTime)] +, ignoreVersion: true); + + var tokenPayload + = new Dictionary { { nameof(ContextUserToken), dbUser.Adapt() } }; + + var accessToken = JWTEncryption.Encrypt(tokenPayload); + return new LoginRsp { + AccessToken = accessToken + , RefreshToken = JWTEncryption.GenerateRefreshToken(accessToken) + }; + } + + private ISelect QueryInternal(QueryReq req, IEnumerable 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 switch (req.Order) { @@ -518,7 +530,7 @@ public sealed class UserService( return QueryInternal(req, deptIds); } - private async Task> QueryInternalAsync(QueryReq req) + private async Task> QueryInternalAsync(QueryReq req, bool includeRoles = true) { IEnumerable deptIds = null; if (req.Filter?.DeptId > 0) { @@ -529,6 +541,6 @@ public sealed class UserService( .ConfigureAwait(false); } - return QueryInternal(req, deptIds); + return QueryInternal(req, deptIds, includeRoles); } } \ No newline at end of file diff --git a/src/backend/NetAdmin.SysComponent.Host/Middlewares/VersionCheckerMiddleware.cs b/src/backend/NetAdmin.SysComponent.Host/Middlewares/VersionCheckerMiddleware.cs new file mode 100644 index 00000000..3b753b68 --- /dev/null +++ b/src/backend/NetAdmin.SysComponent.Host/Middlewares/VersionCheckerMiddleware.cs @@ -0,0 +1,49 @@ +using System.Net.WebSockets; +using NetAdmin.SysComponent.Cache.Sys.Dependency; + +namespace NetAdmin.SysComponent.Host.Middlewares; + +/// +/// 版本更新检查中间件 +/// +public sealed class VersionCheckerMiddleware(RequestDelegate next) +{ + /// + /// 主函数 + /// + 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(buffer), CancellationToken.None) + .ConfigureAwait(false); + if (receiveResult.MessageType != WebSocketMessageType.Text) { + continue; + } + + var ver = await App.GetService().GetVersionAsync().ConfigureAwait(false); + await webSocket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(ver)), WebSocketMessageType.Text + , true, CancellationToken.None) + .ConfigureAwait(false); + } + + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None) + .ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/backend/NetAdmin.Tests/NetAdmin.Tests.csproj b/src/backend/NetAdmin.Tests/NetAdmin.Tests.csproj index 2e0a96f1..cadded48 100644 --- a/src/backend/NetAdmin.Tests/NetAdmin.Tests.csproj +++ b/src/backend/NetAdmin.Tests/NetAdmin.Tests.csproj @@ -5,7 +5,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/frontend/admin/index.html b/src/frontend/admin/index.html index 6c752ea6..6396e5a3 100644 --- a/src/frontend/admin/index.html +++ b/src/frontend/admin/index.html @@ -83,7 +83,7 @@ } .dark .app-loading__title { - color: #d0d0d0; + color: #c0c0c0; } @keyframes loader { diff --git a/src/frontend/admin/package.json b/src/frontend/admin/package.json index 4e0b56e2..eee79303 100644 --- a/src/frontend/admin/package.json +++ b/src/frontend/admin/package.json @@ -12,13 +12,13 @@ "@element-plus/icons-vue": "^2.3.1", "ace-builds": "^1.35.4", "aieditor": "^1.0.13", - "axios": "^1.7.2", + "axios": "^1.7.3", "clipboard": "^2.0.11", - "core-js": "^3.37.1", + "core-js": "^3.38.0", "cropperjs": "^1.6.2", "crypto-js": "^4.2.0", "echarts": "^5.5.1", - "element-plus": "^2.7.8", + "element-plus": "^2.8.0", "json-bigint": "^1.0.0", "json5-to-table": "^0.1.8", "markdown-it": "^14.1.0", @@ -28,21 +28,21 @@ "qrcodejs2": "^0.0.2", "sortablejs": "^1.15.2", "vkbeautify": "^0.99.3", - "vue": "^3.4.34", + "vue": "^3.4.37", "vue-i18n": "^9.13.1", - "vue-router": "^4.4.0", + "vue-router": "^4.4.3", "vue3-ace-editor": "^2.2.4", "vue3-json-viewer": "^2.2.2", "vuedraggable": "^4.0.3", "vuex": "^4.1.0" }, "devDependencies": { - "@vitejs/plugin-vue": "^5.1.1", + "@vitejs/plugin-vue": "^5.1.2", "prettier": "^3.3.3", "prettier-plugin-organize-attributes": "^1.0.0", "sass": "^1.77.8", - "terser": "^5.31.3", - "vite": "^5.3.5" + "terser": "^5.31.5", + "vite": "^5.4.0" }, "browserslist": [ "> 1%", diff --git a/src/frontend/admin/src/components/naSearch/index.vue b/src/frontend/admin/src/components/naSearch/index.vue index a75fc0ae..72444429 100644 --- a/src/frontend/admin/src/components/naSearch/index.vue +++ b/src/frontend/admin/src/components/naSearch/index.vue @@ -21,6 +21,21 @@ :placeholder="item.placeholder" :style="item.style" clearable /> + + + x.type === 'select-input')?.field[1][0].key if (this.dateType === 'datetimerange') { this.dateShortCuts.unshift( { @@ -390,6 +407,14 @@ export default { }, }, 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() { return vkbeautify }, diff --git a/src/frontend/admin/src/components/naUserSelect/index.vue b/src/frontend/admin/src/components/naUserSelect/index.vue index 714a0034..05782757 100644 --- a/src/frontend/admin/src/components/naUserSelect/index.vue +++ b/src/frontend/admin/src/components/naUserSelect/index.vue @@ -32,7 +32,9 @@ export default { data() { return { user: {}, - form: {}, + form: { + requiredFields: ['Id', 'UserName', 'Mobile'], + }, } }, watch: { diff --git a/src/frontend/admin/src/components/naVersionUpdater/index.vue b/src/frontend/admin/src/components/naVersionUpdater/index.vue index 4cc4dc46..5addaa07 100644 --- a/src/frontend/admin/src/components/naVersionUpdater/index.vue +++ b/src/frontend/admin/src/components/naVersionUpdater/index.vue @@ -2,20 +2,26 @@ diff --git a/src/frontend/admin/src/views/sys/log/operation/index.vue b/src/frontend/admin/src/views/sys/log/operation/index.vue index 2c243e4b..b63bc93e 100644 --- a/src/frontend/admin/src/views/sys/log/operation/index.vue +++ b/src/frontend/admin/src/views/sys/log/operation/index.vue @@ -44,10 +44,18 @@ style: 'width:20rem', }, { - type: 'input', - field: ['root', 'keywords'], - placeholder: $t('日志编号 / 用户编号 / 客户端IP'), + type: 'select-input', + field: [ + 'dy', + [ + { label: '日志编号', key: 'id' }, + { label: '用户编号', key: 'ownerId' }, + { label: '客户端IP', key: 'createdClientIp' }, + ], + ], + placeholder: '匹配内容', style: 'width:25rem', + selectStyle: 'width:8rem', }, ]" :vue="this" @@ -178,6 +186,9 @@ export default { if (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() { return { @@ -194,10 +205,7 @@ export default { { field: 'createdTime', operator: 'dateRange', - value: [ - 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'), - ], + value: [this.$TOOL.dateFormat(new Date(), 'yyyy-MM-dd'), this.$TOOL.dateFormat(new Date(), 'yyyy-MM-dd')], }, ], }, @@ -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) { this.query.dynamicFilter.filters.push({ 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') { this.query.dynamicFilter.filters.push( form.dy.operationResult @@ -334,9 +372,18 @@ export default { 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.$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'), + this.$TOOL.dateFormat(new Date(), 'yyyy-MM-dd 00:00:00'), + this.$TOOL.dateFormat(new Date(), 'yyyy-MM-dd 00:00:00'), ] this.$refs.search.keeps.push({ field: 'createdTime', @@ -344,7 +391,7 @@ export default { type: 'dy', }) }, - props: ['keywords', 'ownerId'], + props: ['keywords', 'ownerId', 'excludeApiPathCrc32'], watch: {}, } diff --git a/src/frontend/admin/src/views/sys/user/index.vue b/src/frontend/admin/src/views/sys/user/index.vue index e9f55775..35dcff0b 100644 --- a/src/frontend/admin/src/views/sys/user/index.vue +++ b/src/frontend/admin/src/views/sys/user/index.vue @@ -102,6 +102,11 @@ field="name" prop="dept" width="200" /> + + + + + +