mirror of
https://github.com/nsnail/NetAdmin.git
synced 2025-06-20 18:58:16 +08:00
feat: ✨ 登录日志独立存储 (#161)
请求日志自动分表 [skip ci] Co-authored-by: tk <fiyne1a@dingtalk.com>
This commit is contained in:
@ -1,4 +1,3 @@
|
||||
using NetAdmin.Domain.Dto.Dependency;
|
||||
using NetAdmin.Domain.Dto.Sys.Cache;
|
||||
|
||||
namespace NetAdmin.SysComponent.Application.Modules.Sys;
|
||||
@ -16,5 +15,10 @@ public interface ICacheModule
|
||||
/// <summary>
|
||||
/// 获取所有缓存项
|
||||
/// </summary>
|
||||
Task<PagedQueryRsp<GetAllEntriesRsp>> GetAllEntriesAsync(PagedQueryReq<GetAllEntriesReq> req);
|
||||
Task<IEnumerable<GetEntryRsp>> GetAllEntriesAsync(GetAllEntriesReq req);
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存项
|
||||
/// </summary>
|
||||
Task<GetEntryRsp> GetEntryAsync(GetEntriesReq req);
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
using NetAdmin.Application.Modules;
|
||||
using NetAdmin.Domain.Dto.Dependency;
|
||||
using NetAdmin.Domain.Dto.Sys.LoginLog;
|
||||
|
||||
namespace NetAdmin.SysComponent.Application.Modules.Sys;
|
||||
|
||||
/// <summary>
|
||||
/// 登录日志模块
|
||||
/// </summary>
|
||||
public interface ILoginLogModule : ICrudModule<CreateLoginLogReq, QueryLoginLogRsp // 创建类型
|
||||
, QueryLoginLogReq, QueryLoginLogRsp // 查询类型
|
||||
, DelReq // 删除类型
|
||||
>;
|
@ -8,10 +8,11 @@ namespace NetAdmin.SysComponent.Application.Services.Sys;
|
||||
|
||||
/// <inheritdoc cref="IApiService" />
|
||||
public sealed class ApiService(
|
||||
BasicRepository<Sys_Api, string> rpo //
|
||||
, XmlCommentReader xmlCommentReader //
|
||||
, IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) //
|
||||
: RepositoryService<Sys_Api, string, IApiService>(rpo), IApiService
|
||||
BasicRepository<Sys_Api, string> rpo //
|
||||
, XmlCommentReader xmlCommentReader //
|
||||
, IActionDescriptorCollectionProvider actionDescriptorCollectionProvider
|
||||
, RedLocker redLocker) //
|
||||
: RedLockerService<Sys_Api, string, IApiService>(rpo, redLocker), IApiService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
|
||||
@ -125,6 +126,7 @@ public sealed class ApiService(
|
||||
/// <inheritdoc />
|
||||
public async Task SyncAsync()
|
||||
{
|
||||
await using var locker = await GetLockerOnceAsync(nameof(SyncAsync)).ConfigureAwait(false);
|
||||
_ = await Rpo.DeleteAsync(_ => true).ConfigureAwait(false);
|
||||
|
||||
var list = ReflectionList(false);
|
||||
|
@ -1,5 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using NetAdmin.Application.Services;
|
||||
using NetAdmin.Domain.Dto.Dependency;
|
||||
using NetAdmin.Domain.Dto.Sys.Cache;
|
||||
using NetAdmin.SysComponent.Application.Services.Sys.Dependency;
|
||||
using StackExchange.Redis;
|
||||
@ -31,23 +31,58 @@ public sealed class CacheService(IConnectionMultiplexer connectionMultiplexer) /
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PagedQueryRsp<GetAllEntriesRsp>> GetAllEntriesAsync(PagedQueryReq<GetAllEntriesReq> req)
|
||||
public async Task<IEnumerable<GetEntryRsp>> GetAllEntriesAsync(GetAllEntriesReq req)
|
||||
{
|
||||
req.ThrowIfInvalid();
|
||||
#pragma warning disable VSTHRD103
|
||||
var server = connectionMultiplexer.GetServers()[0];
|
||||
|
||||
var database = connectionMultiplexer.GetDatabase(_redisInstance.Database);
|
||||
var redisResults = (RedisResult[])await database
|
||||
.ExecuteAsync("scan", (req.Page - 1) * req.PageSize, "count"
|
||||
, req.PageSize)
|
||||
.ConfigureAwait(false);
|
||||
var keys = server.Keys(_redisInstance.Database, $"*{req.Keywords}*", Numbers.MAX_LIMIT_BULK_REQ)
|
||||
.Take(Numbers.MAX_LIMIT_BULK_REQ)
|
||||
.ToList();
|
||||
#pragma warning restore VSTHRD103
|
||||
|
||||
var list = ((string[])redisResults![1])!.Where(x => database.KeyType(x) == RedisType.Hash)
|
||||
.Select(x => database.HashGetAll(x)
|
||||
.Append(new HashEntry("key", x))
|
||||
.ToArray()
|
||||
.ToStringDictionary())
|
||||
.ToList()
|
||||
.ConvertAll(x => x.Adapt<GetAllEntriesRsp>());
|
||||
var dic = new ConcurrentDictionary<string, (DateTime?, RedisType)>();
|
||||
|
||||
return new PagedQueryRsp<GetAllEntriesRsp>(req.Page, req.PageSize, 10000, list);
|
||||
await Parallel
|
||||
.ForEachAsync(
|
||||
keys
|
||||
, async (key, _) =>
|
||||
dic.TryAdd(
|
||||
key
|
||||
, (DateTime.Now + await database.KeyTimeToLiveAsync(key).ConfigureAwait(false)
|
||||
, await database.KeyTypeAsync(key).ConfigureAwait(false))))
|
||||
.ConfigureAwait(false);
|
||||
return dic.Select(x => new GetEntryRsp { Key = x.Key, ExpireTime = x.Value.Item1, Type = x.Value.Item2 });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<GetEntryRsp> GetEntryAsync(GetEntriesReq req)
|
||||
{
|
||||
var database = connectionMultiplexer.GetDatabase(_redisInstance.Database);
|
||||
|
||||
var ret = new GetEntryRsp {
|
||||
Type = await database.KeyTypeAsync(req.Key).ConfigureAwait(false)
|
||||
, Key = req.Key
|
||||
, ExpireTime = DateTime.Now +
|
||||
await database.KeyTimeToLiveAsync(req.Key).ConfigureAwait(false)
|
||||
};
|
||||
|
||||
#pragma warning disable IDE0072
|
||||
ret.Data = ret.Type switch
|
||||
#pragma warning restore IDE0072
|
||||
{
|
||||
RedisType.String => await database.StringGetAsync(req.Key).ConfigureAwait(false)
|
||||
, RedisType.List => string.Join(", ", await database.ListRangeAsync(req.Key).ConfigureAwait(false))
|
||||
, RedisType.Set => string.Join(", ", await database.SetMembersAsync(req.Key).ConfigureAwait(false))
|
||||
, RedisType.SortedSet =>
|
||||
string.Join(", ", await database.SortedSetRangeByRankAsync(req.Key).ConfigureAwait(false))
|
||||
, RedisType.Hash => string.Join(
|
||||
", ", await database.HashGetAllAsync(req.Key).ConfigureAwait(false))
|
||||
, _ => "Unsupported key type"
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using NetAdmin.Application.Services;
|
||||
using NetAdmin.SysComponent.Application.Modules.Sys;
|
||||
|
||||
namespace NetAdmin.SysComponent.Application.Services.Sys.Dependency;
|
||||
|
||||
/// <summary>
|
||||
/// 登录日志服务
|
||||
/// </summary>
|
||||
public interface ILoginLogService : IService, ILoginLogModule;
|
@ -0,0 +1,150 @@
|
||||
using NetAdmin.Application.Repositories;
|
||||
using NetAdmin.Application.Services;
|
||||
using NetAdmin.Domain.Dto.Dependency;
|
||||
using NetAdmin.Domain.Dto.Sys.LoginLog;
|
||||
using NetAdmin.SysComponent.Application.Services.Sys.Dependency;
|
||||
|
||||
namespace NetAdmin.SysComponent.Application.Services.Sys;
|
||||
|
||||
/// <inheritdoc cref="ILoginLogService" />
|
||||
public sealed class LoginLogService(BasicRepository<Sys_LoginLog, long> rpo) //
|
||||
: RepositoryService<Sys_LoginLog, long, ILoginLogService>(rpo), ILoginLogService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
|
||||
{
|
||||
req.ThrowIfInvalid();
|
||||
var ret = 0;
|
||||
|
||||
// ReSharper disable once LoopCanBeConvertedToQuery
|
||||
foreach (var item in req.Items) {
|
||||
ret += await DeleteAsync(item).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<long> CountAsync(QueryReq<QueryLoginLogReq> req)
|
||||
{
|
||||
req.ThrowIfInvalid();
|
||||
return QueryInternal(req)
|
||||
#if DBTYPE_SQLSERVER
|
||||
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
|
||||
#endif
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<QueryLoginLogRsp> CreateAsync(CreateLoginLogReq req)
|
||||
{
|
||||
req.ThrowIfInvalid();
|
||||
var ret = await Rpo.InsertAsync(req).ConfigureAwait(false);
|
||||
return ret.Adapt<QueryLoginLogRsp>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> DeleteAsync(DelReq req)
|
||||
{
|
||||
req.ThrowIfInvalid();
|
||||
return Rpo.DeleteAsync(a => a.Id == req.Id);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> ExistAsync(QueryReq<QueryLoginLogReq> req)
|
||||
{
|
||||
req.ThrowIfInvalid();
|
||||
return QueryInternal(req)
|
||||
#if DBTYPE_SQLSERVER
|
||||
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
|
||||
#endif
|
||||
.AnyAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IActionResult> ExportAsync(QueryReq<QueryLoginLogReq> req)
|
||||
{
|
||||
req.ThrowIfInvalid();
|
||||
return ExportAsync<QueryLoginLogReq, ExportLoginLogRsp>(QueryInternal, req, Ln.登录日志导出);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<QueryLoginLogRsp> GetAsync(QueryLoginLogReq req)
|
||||
{
|
||||
req.ThrowIfInvalid();
|
||||
var ret = await QueryInternal(new QueryReq<QueryLoginLogReq> { Filter = req })
|
||||
.ToOneAsync()
|
||||
.ConfigureAwait(false);
|
||||
return ret.Adapt<QueryLoginLogRsp>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PagedQueryRsp<QueryLoginLogRsp>> PagedQueryAsync(PagedQueryReq<QueryLoginLogReq> req)
|
||||
{
|
||||
req.ThrowIfInvalid();
|
||||
var list = await QueryInternal(req)
|
||||
.Include(a => a.Owner)
|
||||
.Page(req.Page, req.PageSize)
|
||||
#if DBTYPE_SQLSERVER
|
||||
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
|
||||
#endif
|
||||
.Count(out var total)
|
||||
.ToListAsync(a => new {
|
||||
a.CreatedClientIp
|
||||
, a.CreatedTime
|
||||
, a.CreatedUserAgent
|
||||
, a.Duration
|
||||
, a.ErrorCode
|
||||
, a.HttpStatusCode
|
||||
, a.Id
|
||||
, a.LoginUserName
|
||||
, Owner = new { a.Owner.Id, a.Owner.UserName }
|
||||
, a.RequestUrl
|
||||
, a.ServerIp
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new PagedQueryRsp<QueryLoginLogRsp>(req.Page, req.PageSize, total, list.Adapt<List<QueryLoginLogRsp>>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<QueryLoginLogRsp>> QueryAsync(QueryReq<QueryLoginLogReq> req)
|
||||
{
|
||||
req.ThrowIfInvalid();
|
||||
var ret = await QueryInternal(req)
|
||||
#if DBTYPE_SQLSERVER
|
||||
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
|
||||
#endif
|
||||
.Take(req.Count)
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
return ret.Adapt<IEnumerable<QueryLoginLogRsp>>();
|
||||
}
|
||||
|
||||
private ISelect<Sys_LoginLog> QueryInternal(QueryReq<QueryLoginLogReq> req)
|
||||
{
|
||||
var ret = Rpo.Select.WhereDynamicFilter(req.DynamicFilter).WhereDynamic(req.Filter);
|
||||
|
||||
if (req.Keywords?.Length > 0) {
|
||||
ret = req.Keywords.IsIpV4()
|
||||
? ret.Where(a => a.CreatedClientIp == req.Keywords.IpV4ToInt32())
|
||||
: ret.Where(a => a.Id == req.Keywords.Int64Try(0) || a.OwnerId == req.Keywords.Int64Try(0) ||
|
||||
a.LoginUserName == req.Keywords);
|
||||
}
|
||||
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (req.Order) {
|
||||
case Orders.None:
|
||||
return ret;
|
||||
case Orders.Random:
|
||||
return ret.OrderByRandom();
|
||||
}
|
||||
|
||||
ret = ret.OrderByPropertyNameIf(req.Prop?.Length > 0, req.Prop, req.Order == Orders.Ascending);
|
||||
if (!req.Prop?.Equals(nameof(req.Filter.Id), StringComparison.OrdinalIgnoreCase) ?? true) {
|
||||
ret = ret.OrderByDescending(a => a.Id);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
@ -2,19 +2,16 @@ using NetAdmin.Application.Repositories;
|
||||
using NetAdmin.Application.Services;
|
||||
using NetAdmin.Domain.Dto.Dependency;
|
||||
using NetAdmin.Domain.Dto.Sys;
|
||||
using NetAdmin.Domain.Dto.Sys.LoginLog;
|
||||
using NetAdmin.Domain.Dto.Sys.RequestLog;
|
||||
using NetAdmin.SysComponent.Application.Services.Sys.Dependency;
|
||||
|
||||
namespace NetAdmin.SysComponent.Application.Services.Sys;
|
||||
|
||||
/// <inheritdoc cref="IRequestLogService" />
|
||||
public sealed class RequestLogService(
|
||||
BasicRepository<Sys_RequestLog, long> rpo
|
||||
, RequestLogDetailService requestLogDetailService) //
|
||||
public sealed class RequestLogService(BasicRepository<Sys_RequestLog, long> rpo, LoginLogService loginLogService) //
|
||||
: RepositoryService<Sys_RequestLog, long, IRequestLogService>(rpo), IRequestLogService
|
||||
{
|
||||
private static readonly Regex _regex = new(Chars.RGXL_IP_V4);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
|
||||
{
|
||||
@ -45,7 +42,12 @@ public sealed class RequestLogService(
|
||||
{
|
||||
req.ThrowIfInvalid();
|
||||
var ret = await Rpo.InsertAsync(req).ConfigureAwait(false);
|
||||
_ = await requestLogDetailService.CreateAsync(req.Detail).ConfigureAwait(false);
|
||||
|
||||
// 插入登录日志
|
||||
if (req.ApiPathCrc32 == Chars.FLG_PATH_API_SYS_USER_LOGIN_BY_PWD.Crc32()) {
|
||||
_ = await loginLogService.CreateAsync(req.Adapt<CreateLoginLogReq>()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return ret.Adapt<QueryRequestLogRsp>();
|
||||
}
|
||||
|
||||
@ -88,7 +90,16 @@ public sealed class RequestLogService(
|
||||
public async Task<QueryRequestLogRsp> GetAsync(QueryRequestLogReq req)
|
||||
{
|
||||
req.ThrowIfInvalid();
|
||||
var ret = await QueryInternal(new QueryReq<QueryRequestLogReq> { Filter = req })
|
||||
var df = new DynamicFilterInfo {
|
||||
Field = nameof(QueryRequestLogReq.CreatedTime)
|
||||
, Operator = DynamicFilterOperators.DateRange
|
||||
, Value = new[] {
|
||||
req.CreatedTime.AddHours(-1).yyyy_MM_dd_HH_mm_ss()
|
||||
, req.CreatedTime.AddHours(1).yyyy_MM_dd_HH_mm_ss()
|
||||
}.Json()
|
||||
.Object<JsonElement>()
|
||||
};
|
||||
var ret = await QueryInternal(new QueryReq<QueryRequestLogReq> { Filter = req, DynamicFilter = df })
|
||||
.Include(a => a.Detail)
|
||||
.ToOneAsync()
|
||||
.ConfigureAwait(false);
|
||||
@ -155,43 +166,27 @@ public sealed class RequestLogService(
|
||||
public async Task<PagedQueryRsp<QueryRequestLogRsp>> PagedQueryAsync(PagedQueryReq<QueryRequestLogReq> req)
|
||||
{
|
||||
req.ThrowIfInvalid();
|
||||
var select = QueryInternal(req)
|
||||
.Page(req.Page, req.PageSize)
|
||||
#if DBTYPE_SQLSERVER
|
||||
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
|
||||
#endif
|
||||
.Count(out var total);
|
||||
var select = QueryInternal(req with { Order = Orders.None }, false);
|
||||
var selectTemp = select.WithTempQuery(a => new { temp = a });
|
||||
|
||||
object ret
|
||||
= req.DynamicFilter?.Filters?.Exists(
|
||||
x => nameof(QueryRequestLogReq.ApiPathCrc32).Equals(x.Field, StringComparison.OrdinalIgnoreCase) &&
|
||||
x.Value.ToString().Int32() == Chars.FLG_PATH_API_SYS_USER_LOGIN_BY_PWD.Crc32()) ?? false
|
||||
? await select.Include(a => a.Detail)
|
||||
.ToListAsync(a => new {
|
||||
Api = new { a.Api.Summary, a.Api.Id }
|
||||
, Owner = new { a.Owner.Id, a.Owner.UserName }
|
||||
, a.CreatedClientIp
|
||||
, a.CreatedTime
|
||||
, a.Duration
|
||||
, a.HttpMethod
|
||||
, a.HttpStatusCode
|
||||
, a.Id
|
||||
, a.ApiPathCrc32
|
||||
, Detail = new { a.Detail.RequestBody, a.Detail.CreatedUserAgent }
|
||||
})
|
||||
.ConfigureAwait(false)
|
||||
: await select.ToListAsync(a => new {
|
||||
Api = new { a.Api.Summary, a.Api.Id }
|
||||
, Owner = new { a.Owner.Id, a.Owner.UserName }
|
||||
, a.CreatedClientIp
|
||||
, a.CreatedTime
|
||||
, a.Duration
|
||||
, a.HttpMethod
|
||||
, a.HttpStatusCode
|
||||
, a.Id
|
||||
, a.ApiPathCrc32
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
if (req.Order == Orders.Random) {
|
||||
selectTemp = selectTemp.OrderByRandom();
|
||||
}
|
||||
else {
|
||||
selectTemp = selectTemp.OrderBy( //
|
||||
req.Prop?.Length > 0, $"{req.Prop} {(req.Order == Orders.Ascending ? "ASC" : "DESC")}");
|
||||
if (!req.Prop?.Equals(nameof(req.Filter.CreatedTime), StringComparison.OrdinalIgnoreCase) ?? true) {
|
||||
selectTemp = selectTemp.OrderByDescending(a => a.temp.CreatedTime);
|
||||
}
|
||||
}
|
||||
|
||||
var ret = await selectTemp.Page(req.Page, req.PageSize)
|
||||
#if DBTYPE_SQLSERVER
|
||||
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
|
||||
#endif
|
||||
.Count(out var total)
|
||||
.ToListAsync(a => a.temp)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new PagedQueryRsp<QueryRequestLogRsp>(req.Page, req.PageSize, total
|
||||
, ret.Adapt<IEnumerable<QueryRequestLogRsp>>());
|
||||
@ -229,10 +224,9 @@ public sealed class RequestLogService(
|
||||
}
|
||||
|
||||
if (req.Keywords?.Length > 0) {
|
||||
ret = _regex.IsMatch(req.Keywords)
|
||||
ret = req.Keywords.IsIpV4()
|
||||
? ret.Where(a => a.CreatedClientIp == req.Keywords.IpV4ToInt32())
|
||||
: ret.Where(a => a.Id == req.Keywords.Int64Try(0) || a.OwnerId == req.Keywords.Int64Try(0) ||
|
||||
a.Owner.UserName == req.Keywords);
|
||||
: ret.Where(a => a.Id == req.Keywords.Int64Try(0) || a.OwnerId == req.Keywords.Int64Try(0));
|
||||
}
|
||||
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
|
@ -18,14 +18,15 @@ public sealed class ToolsService : ServiceBase<IToolsService>, IToolsService
|
||||
/// <inheritdoc />
|
||||
public Task<IEnumerable<GetModulesRsp>> GetModulesAsync()
|
||||
{
|
||||
return Task.FromResult<IEnumerable<GetModulesRsp>>( //
|
||||
AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(x => !x.FullName?.Contains('#') ?? false)
|
||||
.Select(x => {
|
||||
var asm = x.GetName();
|
||||
return new GetModulesRsp { Name = asm.Name, Version = asm.Version?.ToString() };
|
||||
})
|
||||
.OrderBy(x => x.Name));
|
||||
return Task.FromResult<IEnumerable<GetModulesRsp>>(AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Select(x => {
|
||||
var asm = x.GetName();
|
||||
return new GetModulesRsp {
|
||||
Name = asm.Name
|
||||
, Version = asm.Version?.ToString()
|
||||
};
|
||||
})
|
||||
.OrderBy(x => x.Name));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
Reference in New Issue
Block a user