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