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" />
+
+
+
+
+
diff --git a/src/frontend/admin/src/views/sys/user/save.vue b/src/frontend/admin/src/views/sys/user/save.vue
index 969751d3..5f7726a0 100644
--- a/src/frontend/admin/src/views/sys/user/save.vue
+++ b/src/frontend/admin/src/views/sys/user/save.vue
@@ -70,6 +70,9 @@
+
+
+