diff --git a/NPin.sln b/NPin.sln index aa3046d..433a4d8 100644 --- a/NPin.sln +++ b/NPin.sln @@ -44,6 +44,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPin.Application.Contracts" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPin.SqlSugarCore", "src\NPin.SqlSugarCore\NPin.SqlSugarCore.csproj", "{DC8E2E59-589F-4521-95E2-9CE7E1DD5541}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "audit-logging", "audit-logging", "{29EA07EB-E1D2-4BCA-9EA4-D69E28F14978}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "upms", "upms", "{CA0606BE-4146-4390-86CD-AD92FC161A9E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPin.Framework.AuditLogging.Domain.Shared", "module\NPin.Framework.AuditLogging.Domain.Shared\NPin.Framework.AuditLogging.Domain.Shared.csproj", "{67D8E078-69EC-4D2C-8B12-BDB66FB45B58}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPin.Framework.AuditLogging.Domain", "module\NPin.Framework.AuditLogging.Domain\NPin.Framework.AuditLogging.Domain.csproj", "{CF3EECF5-EE94-47F4-978B-2F648B02F99F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPin.Framework.AuditLogging.SqlSugarCore", "module\NPin.Framework.AuditLogging.SqlSugarCore\NPin.Framework.AuditLogging.SqlSugarCore.csproj", "{E50D68D7-8A0C-451D-9344-EE022054E31E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -110,6 +120,18 @@ Global {DC8E2E59-589F-4521-95E2-9CE7E1DD5541}.Debug|Any CPU.Build.0 = Debug|Any CPU {DC8E2E59-589F-4521-95E2-9CE7E1DD5541}.Release|Any CPU.ActiveCfg = Release|Any CPU {DC8E2E59-589F-4521-95E2-9CE7E1DD5541}.Release|Any CPU.Build.0 = Release|Any CPU + {67D8E078-69EC-4D2C-8B12-BDB66FB45B58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67D8E078-69EC-4D2C-8B12-BDB66FB45B58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67D8E078-69EC-4D2C-8B12-BDB66FB45B58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67D8E078-69EC-4D2C-8B12-BDB66FB45B58}.Release|Any CPU.Build.0 = Release|Any CPU + {CF3EECF5-EE94-47F4-978B-2F648B02F99F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF3EECF5-EE94-47F4-978B-2F648B02F99F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF3EECF5-EE94-47F4-978B-2F648B02F99F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF3EECF5-EE94-47F4-978B-2F648B02F99F}.Release|Any CPU.Build.0 = Release|Any CPU + {E50D68D7-8A0C-451D-9344-EE022054E31E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E50D68D7-8A0C-451D-9344-EE022054E31E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E50D68D7-8A0C-451D-9344-EE022054E31E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E50D68D7-8A0C-451D-9344-EE022054E31E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {088B4948-AE5B-45B3-A9FA-B853671CFA05} = {F2A0A89E-A2F9-48CF-AD38-0318B5ACD11C} @@ -127,5 +149,10 @@ Global {34CEB35B-97EF-41DB-B10B-DC83B2453E9D} = {86F61EBB-4ACC-459C-AB3C-C8D486C3017D} {C4D324BB-01B6-436F-9AA6-136C7D5438F6} = {86F61EBB-4ACC-459C-AB3C-C8D486C3017D} {DC8E2E59-589F-4521-95E2-9CE7E1DD5541} = {86F61EBB-4ACC-459C-AB3C-C8D486C3017D} + {29EA07EB-E1D2-4BCA-9EA4-D69E28F14978} = {EEAD0AD4-0F90-46D9-A775-D88AE07E2869} + {CA0606BE-4146-4390-86CD-AD92FC161A9E} = {EEAD0AD4-0F90-46D9-A775-D88AE07E2869} + {67D8E078-69EC-4D2C-8B12-BDB66FB45B58} = {29EA07EB-E1D2-4BCA-9EA4-D69E28F14978} + {CF3EECF5-EE94-47F4-978B-2F648B02F99F} = {29EA07EB-E1D2-4BCA-9EA4-D69E28F14978} + {E50D68D7-8A0C-451D-9344-EE022054E31E} = {29EA07EB-E1D2-4BCA-9EA4-D69E28F14978} EndGlobalSection EndGlobal diff --git a/framework/NPin.Framework.Ddd.Application/NPin.Framework.Ddd.Application.csproj b/framework/NPin.Framework.Ddd.Application/NPin.Framework.Ddd.Application.csproj index 064894c..8dd8ecc 100644 --- a/framework/NPin.Framework.Ddd.Application/NPin.Framework.Ddd.Application.csproj +++ b/framework/NPin.Framework.Ddd.Application/NPin.Framework.Ddd.Application.csproj @@ -9,7 +9,7 @@ - + diff --git a/module/NPin.Framework.AuditLogging.Domain.Shared/Consts/AuditLogActionConsts.cs b/module/NPin.Framework.AuditLogging.Domain.Shared/Consts/AuditLogActionConsts.cs new file mode 100644 index 0000000..c4febac --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain.Shared/Consts/AuditLogActionConsts.cs @@ -0,0 +1,19 @@ +namespace NPin.Framework.AuditLogging.Domain.Shared.Consts; + +public class AuditLogActionConsts +{ + /// + /// Default value: 256 + /// + public static int MaxServiceNameLength { get; set; } = 256; + + /// + /// Default value: 128 + /// + public static int MaxMethodNameLength { get; set; } = 128; + + /// + /// Default value: 2000 + /// + public static int MaxParametersLength { get; set; } = 2000; +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.Domain.Shared/Consts/AuditLogConsts.cs b/module/NPin.Framework.AuditLogging.Domain.Shared/Consts/AuditLogConsts.cs new file mode 100644 index 0000000..96684fa --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain.Shared/Consts/AuditLogConsts.cs @@ -0,0 +1,59 @@ +namespace NPin.Framework.AuditLogging.Domain.Shared.Consts; + +public class AuditLogConsts +{ + /// + /// Default value: 96 + /// + public static int MaxApplicationNameLength { get; set; } = 96; + + /// + /// Default value: 64 + /// + public static int MaxClientIpAddressLength { get; set; } = 64; + + /// + /// Default value: 128 + /// + public static int MaxClientNameLength { get; set; } = 128; + + /// + /// Default value: 64 + /// + public static int MaxClientIdLength { get; set; } = 64; + + /// + /// Default value: 64 + /// + public static int MaxCorrelationIdLength { get; set; } = 64; + + /// + /// Default value: 512 + /// + public static int MaxBrowserInfoLength { get; set; } = 512; + + /// + /// Default value: 256 + /// + public static int MaxCommentsLength { get; set; } = 256; + + /// + /// Default value: 256 + /// + public static int MaxUrlLength { get; set; } = 256; + + /// + /// Default value: 16 + /// + public static int MaxHttpMethodLength { get; set; } = 16; + + /// + /// Default value: 256 + /// + public static int MaxUserNameLength { get; set; } = 256; + + /// + /// Default value: 64 + /// + public static int MaxTenantNameLength { get; set; } = 64; +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.Domain.Shared/Consts/EntityChangeConsts.cs b/module/NPin.Framework.AuditLogging.Domain.Shared/Consts/EntityChangeConsts.cs new file mode 100644 index 0000000..b4ea5e8 --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain.Shared/Consts/EntityChangeConsts.cs @@ -0,0 +1,14 @@ +namespace NPin.Framework.AuditLogging.Domain.Shared.Consts; + +public class EntityChangeConsts +{ + /// + /// Default value: 128 + /// + public static int MaxEntityTypeFullNameLength { get; set; } = 128; + + /// + /// Default value: 128 + /// + public static int MaxEntityIdLength { get; set; } = 128; +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.Domain.Shared/Consts/EntityPropertyChangeConsts.cs b/module/NPin.Framework.AuditLogging.Domain.Shared/Consts/EntityPropertyChangeConsts.cs new file mode 100644 index 0000000..6ea2c56 --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain.Shared/Consts/EntityPropertyChangeConsts.cs @@ -0,0 +1,24 @@ +namespace NPin.Framework.AuditLogging.Domain.Shared.Consts; + +public class EntityPropertyChangeConsts +{ + /// + /// Default value: 512 + /// + public static int MaxNewValueLength { get; set; } = 512; + + /// + /// Default value: 512 + /// + public static int MaxOriginalValueLength { get; set; } = 512; + + /// + /// Default value: 128 + /// + public static int MaxPropertyNameLength { get; set; } = 128; + + /// + /// Default value: 64 + /// + public static int MaxPropertyTypeFullNameLength { get; set; } = 64; +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.Domain.Shared/NPin.Framework.AuditLogging.Domain.Shared.csproj b/module/NPin.Framework.AuditLogging.Domain.Shared/NPin.Framework.AuditLogging.Domain.Shared.csproj new file mode 100644 index 0000000..8d0d14d --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain.Shared/NPin.Framework.AuditLogging.Domain.Shared.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/module/NPin.Framework.AuditLogging.Domain.Shared/NPinFrameworkAuditLoggingDomainSharedModule.cs b/module/NPin.Framework.AuditLogging.Domain.Shared/NPinFrameworkAuditLoggingDomainSharedModule.cs new file mode 100644 index 0000000..03ed959 --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain.Shared/NPinFrameworkAuditLoggingDomainSharedModule.cs @@ -0,0 +1,10 @@ +using Volo.Abp.Domain; +using Volo.Abp.Modularity; + +namespace NPin.Framework.AuditLogging.Domain.Shared; + +[DependsOn(typeof(AbpDddDomainSharedModule))] +public class NPinFrameworkAuditLoggingDomainSharedModule: AbpModule +{ + +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.Domain/AuditLogInfoToAuditLogConverter.cs b/module/NPin.Framework.AuditLogging.Domain/AuditLogInfoToAuditLogConverter.cs new file mode 100644 index 0000000..d1cf80e --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain/AuditLogInfoToAuditLogConverter.cs @@ -0,0 +1,88 @@ +using NPin.Framework.AuditLogging.Domain.Entities; +using Volo.Abp.AspNetCore.ExceptionHandling; +using Volo.Abp.Auditing; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; +using Volo.Abp.Json; + +namespace NPin.Framework.AuditLogging.Domain; + +public class AuditLogInfoToAuditLogConverter : IAuditLogInfoToAuditLogConverter, ITransientDependency +{ + protected IGuidGenerator GuidGenerator { get; } + protected IExceptionToErrorInfoConverter ExceptionToErrorInfoConverter { get; } + protected IJsonSerializer JsonSerializer { get; } + protected AbpExceptionHandlingOptions ExceptionHandlingOptions { get; } + + public AuditLogInfoToAuditLogConverter(IGuidGenerator guidGenerator, + IExceptionToErrorInfoConverter exceptionToErrorInfoConverter, IJsonSerializer jsonSerializer, + AbpExceptionHandlingOptions exceptionHandlingOptions) + { + GuidGenerator = guidGenerator; + ExceptionToErrorInfoConverter = exceptionToErrorInfoConverter; + JsonSerializer = jsonSerializer; + ExceptionHandlingOptions = exceptionHandlingOptions; + } + + public virtual Task ConvertAsync(AuditLogInfo auditLogInfo) + { + var auditLogId = GuidGenerator.Create(); + + var extraProperties = new ExtraPropertyDictionary(); + foreach (var pair in auditLogInfo.ExtraProperties) + { + extraProperties.Add(pair.Key, pair.Value); + } + + var entityChanges = auditLogInfo.EntityChanges? + .Select(info => new EntityChangeEntity(GuidGenerator, auditLogId, info, auditLogInfo.TenantId)) + .ToList() ?? []; + var actions = auditLogInfo.Actions? + .Select(info => new AuditLogActionEntity(GuidGenerator.Create(), auditLogId, info, auditLogInfo.TenantId)) + .ToList() ?? []; + var remoteServiceErrorInfos = auditLogInfo.Exceptions? + .Select(ex => ExceptionToErrorInfoConverter.Convert(ex, options => + { + options.SendExceptionsDetailsToClients = ExceptionHandlingOptions.SendExceptionsDetailsToClients; + options.SendStackTraceToClients = ExceptionHandlingOptions.SendStackTraceToClients; + })) ?? []; + + var exceptions = remoteServiceErrorInfos.Any() + ? JsonSerializer.Serialize(remoteServiceErrorInfos, indented: true) + : null; + + var comments = auditLogInfo.Comments? + .JoinAsString(Environment.NewLine); + + var auditLog = new AuditLogAggregateRoot( + auditLogId, + auditLogInfo.ApplicationName, + auditLogInfo.UserId, + auditLogInfo.UserName, + auditLogInfo.TenantName, + auditLogInfo.ImpersonatorUserId, + auditLogInfo.ImpersonatorUserName, + auditLogInfo.ImpersonatorTenantId, + auditLogInfo.ImpersonatorTenantName, + auditLogInfo.ExecutionTime, + auditLogInfo.ExecutionDuration, + auditLogInfo.ClientIpAddress, + auditLogInfo.ClientName, + auditLogInfo.ClientId, + auditLogInfo.CorrelationId, + auditLogInfo.BrowserInfo, + auditLogInfo.HttpMethod, + auditLogInfo.Url, + exceptions, + comments, + auditLogInfo.HttpStatusCode, + auditLogInfo.TenantId, + entityChanges, + actions, + extraProperties + ); + + return Task.FromResult(auditLog); + } +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.Domain/AuditingStore.cs b/module/NPin.Framework.AuditLogging.Domain/AuditingStore.cs new file mode 100644 index 0000000..68b0e50 --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain/AuditingStore.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using NPin.Framework.AuditLogging.Domain.Repositories; +using Volo.Abp.Auditing; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; + +namespace NPin.Framework.AuditLogging.Domain; + +public class AuditingStore : IAuditingStore, ITransientDependency +{ + public ILogger Logger { get; set; } + protected IAuditLogRepository AuditLogRepository { get; } + protected IUnitOfWorkManager UnitOfWorkManager { get; } + protected AbpAuditingOptions Options { get; } + protected IAuditLogInfoToAuditLogConverter Converter { get; } + + public AuditingStore(ILogger logger, IAuditLogRepository auditLogRepository, + IUnitOfWorkManager unitOfWorkManager, AbpAuditingOptions options, IAuditLogInfoToAuditLogConverter converter) + { + Logger = logger; + AuditLogRepository = auditLogRepository; + UnitOfWorkManager = unitOfWorkManager; + Options = options; + Converter = converter; + } + + public virtual async Task SaveAsync(AuditLogInfo auditInfo) + { + if (!Options.HideErrors) + { + await SaveLogAsync(auditInfo); + return; + } + + try + { + await SaveLogAsync(auditInfo); + } + catch (Exception ex) + { + Logger.LogWarning($"无法保存审计日志: {Environment.NewLine}{auditInfo}"); + Logger.LogException(ex, LogLevel.Error); + } + } + + protected virtual async Task SaveLogAsync(AuditLogInfo auditInfo) + { + var timeConverter = new IsoDateTimeConverter + { + DateTimeFormat = "yyyy-MM-dd HH:mm:ss" + }; + Logger.LogDebug($"NPin-请求日志:{JsonConvert.SerializeObject(auditInfo, Formatting.Indented, timeConverter)}"); + + using var uow = UnitOfWorkManager.Begin(true); + await AuditLogRepository.InsertAsync(await Converter.ConvertAsync(auditInfo)); + await uow.CompleteAsync(); + } +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.Domain/Entities/AuditLogActionEntity.cs b/module/NPin.Framework.AuditLogging.Domain/Entities/AuditLogActionEntity.cs new file mode 100644 index 0000000..33e6d01 --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain/Entities/AuditLogActionEntity.cs @@ -0,0 +1,51 @@ +using NPin.Framework.AuditLogging.Domain.Shared.Consts; +using SqlSugar; +using Volo.Abp.Auditing; +using Volo.Abp.Domain.Entities; +using Volo.Abp.MultiTenancy; + +namespace NPin.Framework.AuditLogging.Domain.Entities; + +[DisableAuditing] +[SugarTable("NPinAuditLogAction", "审计日志操作表")] +[SugarIndex($"index_{nameof(AuditLogId)}", nameof(AuditLogId), OrderByType.Asc)] +[SugarIndex($"index_{nameof(TenantId)}_{nameof(ExecutionTime)}", nameof(TenantId), OrderByType.Asc, nameof(ServiceName), + OrderByType.Asc, nameof(MethodName), OrderByType.Asc, nameof(ExecutionTime), OrderByType.Asc)] +public class AuditLogActionEntity : Entity, IMultiTenant +{ + public virtual Guid? TenantId { get; protected set; } + + public virtual Guid AuditLogId { get; protected set; } + + public virtual string? ServiceName { get; protected set; } + + public virtual string? MethodName { get; protected set; } + + public virtual string? Parameters { get; protected set; } + + public virtual DateTime? ExecutionTime { get; protected set; } + + public virtual int? ExecutionDuration { get; protected set; } + + [SugarColumn(ColumnName = "Id", IsPrimaryKey = true)] + public override Guid Id { get; protected set; } + + public AuditLogActionEntity() + { + } + + public AuditLogActionEntity(Guid id, Guid auditLogId, AuditLogActionInfo actionInfo, Guid? tenantId = null) + { + Id = id; + TenantId = tenantId; + AuditLogId = auditLogId; + ExecutionTime = actionInfo.ExecutionTime; + ExecutionDuration = actionInfo.ExecutionDuration; + + ServiceName = actionInfo.ServiceName.TruncateFromBeginning(AuditLogActionConsts.MaxServiceNameLength); + MethodName = actionInfo.MethodName.TruncateFromBeginning(AuditLogActionConsts.MaxMethodNameLength); + Parameters = actionInfo.Parameters.Length > AuditLogActionConsts.MaxParametersLength + ? "" + : actionInfo.Parameters; + } +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.Domain/Entities/AuditLogAggregateRoot.cs b/module/NPin.Framework.AuditLogging.Domain/Entities/AuditLogAggregateRoot.cs new file mode 100644 index 0000000..2a9213c --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain/Entities/AuditLogAggregateRoot.cs @@ -0,0 +1,111 @@ +using NPin.Framework.AuditLogging.Domain.Shared.Consts; +using SqlSugar; +using Volo.Abp.Auditing; +using Volo.Abp.Data; +using Volo.Abp.Domain.Entities; +using Volo.Abp.MultiTenancy; + +namespace NPin.Framework.AuditLogging.Domain.Entities; + +[DisableAuditing] +[SugarTable("NPinAuditLog", "审计日志")] +[SugarIndex($"index_{nameof(ExecutionTime)}", nameof(TenantId), OrderByType.Asc, nameof(ExecutionTime), + OrderByType.Asc)] +[SugarIndex($"index_{nameof(ExecutionTime)}_{nameof(UserId)}", nameof(TenantId), OrderByType.Asc, nameof(UserId), + OrderByType.Asc, nameof(ExecutionTime), OrderByType.Asc)] +public class AuditLogAggregateRoot : AggregateRoot, IMultiTenant +{ + [SugarColumn(ColumnName = "Id", IsPrimaryKey = true)] + public override Guid Id { get; protected set; } + + public virtual string? ApplicationName { get; set; } + + + public virtual Guid? UserId { get; protected set; } + + public virtual string? UserName { get; protected set; } + + public virtual string? TenantName { get; protected set; } + + public virtual Guid? ImpersonatorUserId { get; protected set; } + + public virtual string? ImpersonatorUserName { get; protected set; } + + public virtual Guid? ImpersonatorTenantId { get; protected set; } + + public virtual string? ImpersonatorTenantName { get; protected set; } + + public virtual DateTime? ExecutionTime { get; protected set; } + + public virtual int? ExecutionDuration { get; protected set; } + + public virtual string? ClientIpAddress { get; protected set; } + + public virtual string? ClientName { get; protected set; } + + public virtual string? ClientId { get; set; } + + public virtual string? CorrelationId { get; set; } + + public virtual string? BrowserInfo { get; protected set; } + + public virtual string? HttpMethod { get; protected set; } + + public virtual string? Url { get; protected set; } + + public virtual string? Exceptions { get; protected set; } + + public virtual string? Comments { get; protected set; } + + public virtual int? HttpStatusCode { get; set; } + + public virtual Guid? TenantId { get; protected set; } + + // 导航属性 + [Navigate(NavigateType.OneToMany, nameof(EntityChangeEntity.AuditLogId))] + public virtual List EntityChanges { get; protected set; } + + // 导航属性 + [Navigate(NavigateType.OneToMany, nameof(AuditLogActionEntity.AuditLogId))] + public virtual List Actions { get; protected set; } + + [SugarColumn(IsIgnore = true)] public override ExtraPropertyDictionary ExtraProperties { get; protected set; } + + public AuditLogAggregateRoot() + { + } + + public AuditLogAggregateRoot(Guid id, string? applicationName, Guid? userId, string? userName, string? tenantName, + Guid? impersonatorUserId, string? impersonatorUserName, Guid? impersonatorTenantId, + string? impersonatorTenantName, DateTime? executionTime, int? executionDuration, string? clientIpAddress, + string? clientName, string? clientId, string? correlationId, string? browserInfo, string? httpMethod, + string? url, string? exceptions, string? comments, int? httpStatusCode, Guid? tenantId, + List entityChanges, List actions, + ExtraPropertyDictionary extraProperties) : base(id) + { + ApplicationName = applicationName.Truncate(AuditLogConsts.MaxApplicationNameLength); + UserId = userId; + UserName = userName.Truncate(AuditLogConsts.MaxUserNameLength); + TenantName = tenantName.Truncate(AuditLogConsts.MaxTenantNameLength); + ImpersonatorUserId = impersonatorUserId; + ImpersonatorUserName = impersonatorUserName.Truncate(AuditLogConsts.MaxUserNameLength); + ImpersonatorTenantId = impersonatorTenantId; + ImpersonatorTenantName = impersonatorTenantName.Truncate(AuditLogConsts.MaxTenantNameLength); + ExecutionTime = executionTime; + ExecutionDuration = executionDuration; + ClientIpAddress = clientIpAddress.Truncate(AuditLogConsts.MaxClientIpAddressLength); + ClientName = clientName.Truncate(AuditLogConsts.MaxClientNameLength); + ClientId = clientId.Truncate(AuditLogConsts.MaxClientIdLength); + CorrelationId = correlationId.Truncate(AuditLogConsts.MaxCorrelationIdLength); + BrowserInfo = browserInfo.Truncate(AuditLogConsts.MaxBrowserInfoLength); + HttpMethod = httpMethod.Truncate(AuditLogConsts.MaxHttpMethodLength); + Url = url.Truncate(AuditLogConsts.MaxUrlLength); + Exceptions = exceptions; + Comments = comments.Truncate(AuditLogConsts.MaxCommentsLength); + HttpStatusCode = httpStatusCode; + TenantId = tenantId; + EntityChanges = entityChanges; + Actions = actions; + ExtraProperties = extraProperties; + } +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.Domain/Entities/EntityChangeEntity.cs b/module/NPin.Framework.AuditLogging.Domain/Entities/EntityChangeEntity.cs new file mode 100644 index 0000000..fadddb6 --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain/Entities/EntityChangeEntity.cs @@ -0,0 +1,63 @@ +using NPin.Framework.AuditLogging.Domain.Shared.Consts; +using SqlSugar; +using Volo.Abp.Auditing; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; + +namespace NPin.Framework.AuditLogging.Domain.Entities; + +[SugarTable("NPinEntityChange")] +[SugarIndex($"index_{nameof(AuditLogId)}", nameof(AuditLogId), OrderByType.Asc)] +[SugarIndex($"index_{nameof(TenantId)}_{nameof(EntityId)}", nameof(TenantId), OrderByType.Asc, + nameof(EntityTypeFullName), OrderByType.Asc, nameof(EntityId), OrderByType.Asc)] +public class EntityChangeEntity : Entity, IMultiTenant +{ + [SugarColumn(ColumnName = "Id", IsPrimaryKey = true)] + public override Guid Id { get; protected set; } + + public virtual Guid AuditLogId { get; protected set; } + + public virtual Guid? TenantId { get; protected set; } + + public virtual DateTime? ChangeTime { get; protected set; } + + public virtual EntityChangeType? ChangeType { get; protected set; } + + public virtual Guid? EntityTenantId { get; protected set; } + + public virtual string? EntityId { get; protected set; } + + public virtual string? EntityTypeFullName { get; protected set; } + + // 关联 + [Navigate(NavigateType.OneToMany, nameof(EntityPropertyChangeEntity.EntityChangeId))] + public virtual List PropertyChanges { get; protected set; } + + public EntityChangeEntity() + { + } + + public EntityChangeEntity( + IGuidGenerator guidGenerator, + Guid auditLogId, + EntityChangeInfo entityChangeInfo, + Guid? tenantId = null) + { + Id = guidGenerator.Create(); + AuditLogId = auditLogId; + TenantId = tenantId; + ChangeTime = entityChangeInfo.ChangeTime; + ChangeType = entityChangeInfo.ChangeType; + EntityTenantId = entityChangeInfo.EntityTenantId; + EntityId = entityChangeInfo.EntityId.Truncate(EntityChangeConsts.MaxEntityTypeFullNameLength); + EntityTypeFullName = + entityChangeInfo.EntityTypeFullName.TruncateFromBeginning(EntityChangeConsts.MaxEntityTypeFullNameLength); + + PropertyChanges = entityChangeInfo + .PropertyChanges? + .Select(p => new EntityPropertyChangeEntity(guidGenerator, Id, p, tenantId)) + .ToList() + ?? new List(); + } +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.Domain/Entities/EntityPropertyChangeEntity.cs b/module/NPin.Framework.AuditLogging.Domain/Entities/EntityPropertyChangeEntity.cs new file mode 100644 index 0000000..ab2057a --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain/Entities/EntityPropertyChangeEntity.cs @@ -0,0 +1,50 @@ +using NPin.Framework.AuditLogging.Domain.Shared.Consts; +using SqlSugar; +using Volo.Abp.Auditing; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; + +namespace NPin.Framework.AuditLogging.Domain.Entities; + +[SugarTable("NPinEntityPropertyChange")] +[SugarIndex($"index_{nameof(EntityChangeId)}", nameof(EntityChangeId), OrderByType.Asc)] +public class EntityPropertyChangeEntity : Entity, IMultiTenant +{ + [SugarColumn(ColumnName = "Id", IsPrimaryKey = true)] + public override Guid Id { get; protected set; } + + public virtual Guid? TenantId { get; protected set; } + public virtual Guid? EntityChangeId { get; protected set; } + + public virtual string? NewValue { get; protected set; } + + public virtual string? OriginalValue { get; protected set; } + + public virtual string? PropertyName { get; protected set; } + + public virtual string? PropertyTypeFullName { get; protected set; } + + public EntityPropertyChangeEntity() + { + } + + + public EntityPropertyChangeEntity( + IGuidGenerator guidGenerator, + Guid entityChangeId, + EntityPropertyChangeInfo entityChangeInfo, + Guid? tenantId = null) + { + Id = guidGenerator.Create(); + TenantId = tenantId; + EntityChangeId = entityChangeId; + NewValue = entityChangeInfo.NewValue.Truncate(EntityPropertyChangeConsts.MaxNewValueLength); + OriginalValue = entityChangeInfo.OriginalValue.Truncate(EntityPropertyChangeConsts.MaxOriginalValueLength); + PropertyName = + entityChangeInfo.PropertyName.TruncateFromBeginning(EntityPropertyChangeConsts.MaxPropertyNameLength); + PropertyTypeFullName = + entityChangeInfo.PropertyTypeFullName.TruncateFromBeginning(EntityPropertyChangeConsts + .MaxPropertyTypeFullNameLength); + } +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.Domain/Events/EntityChangeWithUsername.cs b/module/NPin.Framework.AuditLogging.Domain/Events/EntityChangeWithUsername.cs new file mode 100644 index 0000000..0dec11c --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain/Events/EntityChangeWithUsername.cs @@ -0,0 +1,10 @@ +using NPin.Framework.AuditLogging.Domain.Entities; + +namespace NPin.Framework.AuditLogging.Domain.Events; + +public class EntityChangeWithUsername +{ + public EntityChangeEntity EntityChange { get; set; } + + public string Username { get; set; } +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.Domain/IAuditLogInfoToAuditLogConverter.cs b/module/NPin.Framework.AuditLogging.Domain/IAuditLogInfoToAuditLogConverter.cs new file mode 100644 index 0000000..0915544 --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain/IAuditLogInfoToAuditLogConverter.cs @@ -0,0 +1,12 @@ +using NPin.Framework.AuditLogging.Domain.Entities; +using Volo.Abp.Auditing; + +namespace NPin.Framework.AuditLogging.Domain; + +/// +/// 审计日志转换器接口 +/// +public interface IAuditLogInfoToAuditLogConverter +{ + Task ConvertAsync(AuditLogInfo auditLogInfo); +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.Domain/NPin.Framework.AuditLogging.Domain.csproj b/module/NPin.Framework.AuditLogging.Domain/NPin.Framework.AuditLogging.Domain.csproj new file mode 100644 index 0000000..73259ba --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain/NPin.Framework.AuditLogging.Domain.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/module/NPin.Framework.AuditLogging.Domain/NPinFrameworkAuditLoggingDomainModule.cs b/module/NPin.Framework.AuditLogging.Domain/NPinFrameworkAuditLoggingDomainModule.cs new file mode 100644 index 0000000..c030d3a --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain/NPinFrameworkAuditLoggingDomainModule.cs @@ -0,0 +1,15 @@ +using NPin.Framework.AuditLogging.Domain.Shared; +using Volo.Abp.Auditing; +using Volo.Abp.Domain; +using Volo.Abp.Modularity; + +namespace NPin.Framework.AuditLogging.Domain; + +[DependsOn( + typeof(NPinFrameworkAuditLoggingDomainSharedModule), + typeof(AbpDddDomainModule), + typeof(AbpAuditingModule) +)] +public class NPinFrameworkAuditLoggingDomainModule : AbpModule +{ +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.Domain/Repositories/IAuditLogRepository.cs b/module/NPin.Framework.AuditLogging.Domain/Repositories/IAuditLogRepository.cs new file mode 100644 index 0000000..d733c84 --- /dev/null +++ b/module/NPin.Framework.AuditLogging.Domain/Repositories/IAuditLogRepository.cs @@ -0,0 +1,131 @@ +using System.Net; +using NPin.Framework.AuditLogging.Domain.Entities; +using NPin.Framework.AuditLogging.Domain.Events; +using NPin.Framework.SqlSugarCore.Abstractions; +using Volo.Abp.Auditing; + +namespace NPin.Framework.AuditLogging.Domain.Repositories; + +public interface IAuditLogRepository : ISqlSugarRepository +{ + /// + /// 获取每日平均执行时长 + /// + /// + /// + /// + /// + Task> GetAverageExecutionDurationPerDayAsync(DateTime startDate, DateTime endDate, + CancellationToken cancellationToken = default); + + /// + /// 获取数量 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task GetCountAsync(DateTime? startTime = null, DateTime? endTime = null, string httpMethod = null, + string url = null, Guid? userId = null, string userName = null, string applicationName = null, + string clientIpAddress = null, string correlationId = null, int? maxExecutionDuration = null, + int? minExecutionDuration = null, bool? hasException = null, HttpStatusCode? httpStatusCode = null, + CancellationToken cancellationToken = default); + + /// + /// 获取实体改变日志 + /// + /// + /// + /// + Task GetEntityChange(Guid entityChangeId, CancellationToken cancellationToken = default); + + /// + /// 获取实体改变日志数量 + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task GetEntityChangeCountAsync(Guid? auditLogId = null, DateTime? startTime = null, DateTime? endTime = null, + EntityChangeType? changeType = null, string entityId = null, string entityTypeFullName = null, + CancellationToken cancellationToken = default); + + /// + /// 获取实体改变日志列表 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task> GetEntityChangeListAsync(string sorting = null, int maxResultCount = 50, + int skipCount = 0, Guid? auditLogId = null, DateTime? startTime = null, DateTime? endTime = null, + EntityChangeType? changeType = null, string entityId = null, string entityTypeFullName = null, + bool includeDetails = false, CancellationToken cancellationToken = default); + + /// + /// 获取用户名改变日志列表 + /// + /// + /// + /// + /// + Task> GetEntityChangesWithUsernameAsync(string entityId, string entityTypeFullName, + CancellationToken cancellationToken = default); + + /// + /// 获取用户名改变日志 + /// + /// + /// + Task GetEntityChangeWithUsernameAsync(Guid entityChangeId); + + /// + /// 获取审计日志列表 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task> GetListAsync(string sorting = null, int maxResultCount = 50, int skipCount = 0, + DateTime? startTime = null, DateTime? endTime = null, string httpMethod = null, string url = null, + Guid? userId = null, string userName = null, string applicationName = null, string clientIpAddress = null, + string correlationId = null, int? maxExecutionDuration = null, int? minExecutionDuration = null, + bool? hasException = null, HttpStatusCode? httpStatusCode = null, bool includeDetails = false); +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.SqlSugarCore/NPin.Framework.AuditLogging.SqlSugarCore.csproj b/module/NPin.Framework.AuditLogging.SqlSugarCore/NPin.Framework.AuditLogging.SqlSugarCore.csproj new file mode 100644 index 0000000..8f04e3e --- /dev/null +++ b/module/NPin.Framework.AuditLogging.SqlSugarCore/NPin.Framework.AuditLogging.SqlSugarCore.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/module/NPin.Framework.AuditLogging.SqlSugarCore/NPinFrameworkAuditLoggingSqlSugarCoreModule.cs b/module/NPin.Framework.AuditLogging.SqlSugarCore/NPinFrameworkAuditLoggingSqlSugarCoreModule.cs new file mode 100644 index 0000000..a4011d0 --- /dev/null +++ b/module/NPin.Framework.AuditLogging.SqlSugarCore/NPinFrameworkAuditLoggingSqlSugarCoreModule.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using NPin.Framework.AuditLogging.Domain; +using NPin.Framework.AuditLogging.Domain.Repositories; +using NPin.Framework.AuditLogging.SqlSugarCore.Repositories; +using NPin.Framework.SqlSugarCore; +using Volo.Abp.Modularity; + +namespace NPin.Framework.AuditLogging.SqlSugarCore; + +[DependsOn( + typeof(NPinFrameworkAuditLoggingDomainModule), + typeof(NPinFrameworkSqlSugarCoreModule) +)] +public class NPinFrameworkAuditLoggingSqlSugarCoreModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddTransient(); + } +} \ No newline at end of file diff --git a/module/NPin.Framework.AuditLogging.SqlSugarCore/Repositories/AuditLogRepository.cs b/module/NPin.Framework.AuditLogging.SqlSugarCore/Repositories/AuditLogRepository.cs new file mode 100644 index 0000000..71cb50a --- /dev/null +++ b/module/NPin.Framework.AuditLogging.SqlSugarCore/Repositories/AuditLogRepository.cs @@ -0,0 +1,238 @@ +using System.Net; +using NPin.Framework.AuditLogging.Domain.Entities; +using NPin.Framework.AuditLogging.Domain.Events; +using NPin.Framework.AuditLogging.Domain.Repositories; +using NPin.Framework.SqlSugarCore.Abstractions; +using NPin.Framework.SqlSugarCore.Repositories; +using SqlSugar; +using Volo.Abp.Auditing; +using Volo.Abp.Domain.Entities; + +namespace NPin.Framework.AuditLogging.SqlSugarCore.Repositories; + +public class AuditLogRepository : SqlSugarRepository, IAuditLogRepository +{ + public AuditLogRepository(ISugarDbContextProvider sugarDbContextProvider) : base( + sugarDbContextProvider) + { + } + + /// + /// 重写插入逻辑以支持关联关系(导航) + /// + /// + /// + public override async Task InsertAsync(AuditLogAggregateRoot insertObj) + { + return await Db.InsertNav(insertObj) + .Include(z1 => z1.Actions) + // .Include(z1=>z1.EntityChanges) + // .ThenInclude(z2=>z2.PropertyChanges) + .ExecuteCommandAsync(); + } + + + public async Task> GetAverageExecutionDurationPerDayAsync(DateTime startDate, + DateTime endDate, + CancellationToken cancellationToken = default) + { + // 分组排序查询 + var result = await DbQueryable + .Where(a => a.ExecutionTime < endDate.AddDays(1) && a.ExecutionTime > startDate) + .OrderBy(t => t.ExecutionTime) + .GroupBy(g => new { g.ExecutionTime!.Value.Date }) + .Select(s => new + { + Day = SqlFunc.AggregateMin(s.ExecutionTime), + avgExecutionTime = SqlFunc.AggregateAvg(s.ExecutionDuration) + }) + .ToListAsync(cancellationToken); + + // 散列为Dict + return result.ToDictionary(e => e.Day!.Value.ClearTime(), e => (double)e.avgExecutionTime!); + } + + public async Task> GetListAsync(string sorting = null, int maxResultCount = 50, + int skipCount = 0, DateTime? startTime = null, + DateTime? endTime = null, string httpMethod = null, string url = null, Guid? userId = null, + string userName = null, + string applicationName = null, string clientIpAddress = null, string correlationId = null, + int? maxExecutionDuration = null, int? minExecutionDuration = null, bool? hasException = null, + HttpStatusCode? httpStatusCode = null, bool includeDetails = false) + { + var query = await GetListQueryAsync( + startTime, + endTime, + httpMethod, + url, + userId, + userName, + applicationName, + clientIpAddress, + correlationId, + maxExecutionDuration, + minExecutionDuration, + hasException, + httpStatusCode, + includeDetails + ); + var auditLogs = await query + .OrderBy(sorting.IsNullOrWhiteSpace() ? $"{nameof(AuditLogAggregateRoot.ExecutionTime)} DESC" : sorting) + .ToPageListAsync(skipCount, maxResultCount); + return auditLogs; + } + + public async Task GetCountAsync(DateTime? startTime = null, DateTime? endTime = null, + string httpMethod = null, + string url = null, + Guid? userId = null, string userName = null, string applicationName = null, string clientIpAddress = null, + string correlationId = null, int? maxExecutionDuration = null, int? minExecutionDuration = null, + bool? hasException = null, HttpStatusCode? httpStatusCode = null, CancellationToken cancellationToken = default) + { + var query = await GetListQueryAsync( + startTime, + endTime, + httpMethod, + url, + userId, + userName, + applicationName, + clientIpAddress, + correlationId, + maxExecutionDuration, + minExecutionDuration, + hasException, + httpStatusCode + ); + + var totalCount = await query.CountAsync(cancellationToken); + + return totalCount; + } + + protected virtual async Task> GetListQueryAsync( + DateTime? startTime = null, + DateTime? endTime = null, + string httpMethod = null, + string url = null, + Guid? userId = null, + string userName = null, + string applicationName = null, + string clientIpAddress = null, + string correlationId = null, + int? maxExecutionDuration = null, + int? minExecutionDuration = null, + bool? hasException = null, + HttpStatusCode? httpStatusCode = null, + bool includeDetails = false) + { + var nHttpStatusCode = (int?)httpStatusCode; + return DbQueryable + .WhereIF(startTime.HasValue, auditLog => auditLog.ExecutionTime >= startTime) + .WhereIF(endTime.HasValue, auditLog => auditLog.ExecutionTime <= endTime) + .WhereIF(hasException.HasValue && hasException.Value, + auditLog => auditLog.Exceptions != null && auditLog.Exceptions != "") + .WhereIF(hasException.HasValue && !hasException.Value, + auditLog => auditLog.Exceptions == null || auditLog.Exceptions == "") + .WhereIF(httpMethod != null, auditLog => auditLog.HttpMethod == httpMethod) + .WhereIF(url != null, auditLog => auditLog.Url != null && auditLog.Url.Contains(url)) + .WhereIF(userId != null, auditLog => auditLog.UserId == userId) + .WhereIF(userName != null, auditLog => auditLog.UserName == userName) + .WhereIF(applicationName != null, auditLog => auditLog.ApplicationName == applicationName) + .WhereIF(clientIpAddress != null, + auditLog => auditLog.ClientIpAddress != null && auditLog.ClientIpAddress == clientIpAddress) + .WhereIF(correlationId != null, auditLog => auditLog.CorrelationId == correlationId) + .WhereIF(httpStatusCode != null && httpStatusCode > 0, + auditLog => auditLog.HttpStatusCode == nHttpStatusCode) + .WhereIF(maxExecutionDuration != null && maxExecutionDuration.Value > 0, + auditLog => auditLog.ExecutionDuration <= maxExecutionDuration) + .WhereIF(minExecutionDuration != null && minExecutionDuration.Value > 0, + auditLog => auditLog.ExecutionDuration >= minExecutionDuration); + } + + public async Task GetEntityChange(Guid entityChangeId, + CancellationToken cancellationToken = default) + { + var entityChange = await (await GetDbContextAsync()).Queryable() + .Where(x => x.Id == entityChangeId) + .OrderBy(x => x.Id) + .FirstAsync(cancellationToken); + if (entityChange == null) + { + throw new EntityNotFoundException(typeof(EntityChangeEntity)); + } + + return entityChange; + } + + public async Task GetEntityChangeCountAsync(Guid? auditLogId = null, DateTime? startTime = null, + DateTime? endTime = null, + EntityChangeType? changeType = null, string entityId = null, string entityTypeFullName = null, + CancellationToken cancellationToken = default) + { + var query = await GetEntityChangeListQueryAsync(auditLogId, startTime, endTime, changeType, entityId, + entityTypeFullName); + var totalCount = await query.CountAsync(cancellationToken); + return totalCount; + } + + public async Task> GetEntityChangeListAsync(string sorting = null, int maxResultCount = 50, + int skipCount = 0, + Guid? auditLogId = null, DateTime? startTime = null, DateTime? endTime = null, + EntityChangeType? changeType = null, + string entityId = null, string entityTypeFullName = null, bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var query = await GetEntityChangeListQueryAsync(auditLogId, startTime, endTime, changeType, entityId, + entityTypeFullName, includeDetails); + return await query + .OrderBy(sorting.IsNullOrWhiteSpace() ? $"{nameof(EntityChangeEntity.ChangeTime)} DESC" : sorting) + .ToPageListAsync(skipCount, maxResultCount, cancellationToken); + } + + protected virtual async Task> GetEntityChangeListQueryAsync( + Guid? auditLogId = null, + DateTime? startTime = null, + DateTime? endTime = null, + EntityChangeType? changeType = null, + string entityId = null, + string entityTypeFullName = null, + bool includeDetails = false) + { + return (await GetDbContextAsync()) + .Queryable() + .WhereIF(auditLogId.HasValue, e => e.AuditLogId == auditLogId) + .WhereIF(startTime.HasValue, e => e.ChangeTime >= startTime) + .WhereIF(endTime.HasValue, e => e.ChangeTime <= endTime) + .WhereIF(changeType.HasValue, e => e.ChangeType == changeType) + .WhereIF(!string.IsNullOrWhiteSpace(entityId), e => e.EntityId == entityId) + .WhereIF(!string.IsNullOrWhiteSpace(entityTypeFullName), + e => e.EntityTypeFullName.Contains(entityTypeFullName)); + } + + public async Task> GetEntityChangesWithUsernameAsync(string entityId, + string entityTypeFullName, + CancellationToken cancellationToken = default) + { + var query = (await GetDbContextAsync()).Queryable() + .Where(x => x.EntityId == entityId && x.EntityTypeFullName == entityTypeFullName); + + var result = await query.LeftJoin((x, audit) => x.AuditLogId == audit.Id) + .Select((x, audit) => new EntityChangeWithUsername { EntityChange = x, Username = audit.UserName! }) + .OrderByDescending(x => x.EntityChange.ChangeTime) + .ToListAsync(cancellationToken); + return result; + } + + public async Task GetEntityChangeWithUsernameAsync(Guid entityChangeId) + { + var auditLog = await DbQueryable + .Where(x => x.EntityChanges.Any(y => y.Id == entityChangeId)) + .FirstAsync(); + return new EntityChangeWithUsername + { + EntityChange = auditLog.EntityChanges.First(x => x.Id == entityChangeId), + Username = auditLog.UserName! + }; + } +} \ No newline at end of file diff --git a/src/NPin.Application/NPinApplicationModule.cs b/src/NPin.Application/NPinApplicationModule.cs index c409886..6a2b8f3 100644 --- a/src/NPin.Application/NPinApplicationModule.cs +++ b/src/NPin.Application/NPinApplicationModule.cs @@ -1,9 +1,16 @@ using NPin.Application.Contracts; +using NPin.Domain; +using NPin.Framework.Ddd.Application; using NPin.Framework.Ddd.Application.Contracts; namespace NPin.Application; - +[DependsOn( + typeof(NPinApplicationContractsModule), + typeof(NPinDomainModule), + // TODO rbac bbs tenant codegen + typeof(NPinFrameworkDddApplicationModule) + )] public class NPinApplicationModule: AbpModule { } \ No newline at end of file diff --git a/src/NPin.Domain.Shared/NPin.Domain.Shared.csproj b/src/NPin.Domain.Shared/NPin.Domain.Shared.csproj index b40bfea..02e4d8c 100644 --- a/src/NPin.Domain.Shared/NPin.Domain.Shared.csproj +++ b/src/NPin.Domain.Shared/NPin.Domain.Shared.csproj @@ -9,6 +9,7 @@ + diff --git a/src/NPin.Domain.Shared/NPinDomainSharedModule.cs b/src/NPin.Domain.Shared/NPinDomainSharedModule.cs index 78f04bf..053b4b4 100644 --- a/src/NPin.Domain.Shared/NPinDomainSharedModule.cs +++ b/src/NPin.Domain.Shared/NPinDomainSharedModule.cs @@ -1,11 +1,11 @@ namespace NPin.Domain.Shared; [DependsOn( - typeof(NPinDomainSharedModule)) -// TODO RBAC + // TODO RBAC // TODO BBS // TODO AuditLogging -] + // TODO AbpDddDomainSharedModule +)] public class NPinDomainSharedModule : AbpModule { } \ No newline at end of file diff --git a/src/NPin.Domain/NPinDomainModule.cs b/src/NPin.Domain/NPinDomainModule.cs new file mode 100644 index 0000000..e2b1cba --- /dev/null +++ b/src/NPin.Domain/NPinDomainModule.cs @@ -0,0 +1,20 @@ +using NPin.Domain.Shared; +using NPin.Framework.Mapster; +using Volo.Abp.Caching; +using Volo.Abp.Domain; + +namespace NPin.Domain; + +[DependsOn( + typeof(NPinDomainSharedModule), + // TODO Tenant + // TODO Rbac + // TODO Bbs + // TODO Audit + typeof(NPinFrameworkMapsterModule), + typeof(AbpDddDomainModule), + typeof(AbpCachingModule) +)] +public class NPinDomainModule : AbpModule +{ +} \ No newline at end of file diff --git a/src/NPin.SqlSugarCore/NPin.SqlSugarCore.csproj b/src/NPin.SqlSugarCore/NPin.SqlSugarCore.csproj index 0090a15..1595520 100644 --- a/src/NPin.SqlSugarCore/NPin.SqlSugarCore.csproj +++ b/src/NPin.SqlSugarCore/NPin.SqlSugarCore.csproj @@ -9,6 +9,7 @@ + diff --git a/src/NPin.SqlSugarCore/NPinDbContext.cs b/src/NPin.SqlSugarCore/NPinDbContext.cs new file mode 100644 index 0000000..0ba18b9 --- /dev/null +++ b/src/NPin.SqlSugarCore/NPinDbContext.cs @@ -0,0 +1,6 @@ +namespace NPin.SqlSugarCore; + +public class NPinDbContext +{ + +} \ No newline at end of file diff --git a/src/NPin.SqlSugarCore/NPinSqlSugarCoreModule.cs b/src/NPin.SqlSugarCore/NPinSqlSugarCoreModule.cs new file mode 100644 index 0000000..208a45b --- /dev/null +++ b/src/NPin.SqlSugarCore/NPinSqlSugarCoreModule.cs @@ -0,0 +1,24 @@ +using NPin.Domain; +using NPin.Framework.AuditLogging.SqlSugarCore; +using NPin.Framework.Mapster; +using NPin.Framework.SqlSugarCore; + +namespace NPin.SqlSugarCore; + +[DependsOn( + typeof(NPinDomainModule), + // TODO rbac bbs codegen + typeof(NPinFrameworkAuditLoggingSqlSugarCoreModule), + // TODO tenant + typeof(NPinFrameworkMapsterModule), + typeof(NPinFrameworkSqlSugarCoreModule) +)] +public class NPinSqlSugarCoreModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + // context.Services.AddNPinDbContext(); + // 默认不开放,可根据项目需要是否直接对外开放db + // context.Services.AddTransient(x => x.GetRequiredService().SqlSugarClient); + } +} \ No newline at end of file diff --git a/src/NPin.Web/NPin.Web.csproj b/src/NPin.Web/NPin.Web.csproj index fd844b8..f7d04a2 100644 --- a/src/NPin.Web/NPin.Web.csproj +++ b/src/NPin.Web/NPin.Web.csproj @@ -21,6 +21,8 @@ + + diff --git a/src/NPin.Web/NPinWebModule.cs b/src/NPin.Web/NPinWebModule.cs index b1664d0..7d95467 100644 --- a/src/NPin.Web/NPinWebModule.cs +++ b/src/NPin.Web/NPinWebModule.cs @@ -1,10 +1,181 @@ -using Volo.Abp.Modularity; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Cors; +using Microsoft.OpenApi.Models; +using Newtonsoft.Json.Converters; +using NPin.Application; +using NPin.Framework.AspNetCore; +using NPin.Framework.AspNetCore.Authentication.OAuth; +using NPin.Framework.AspNetCore.Authentication.OAuth.Gitee; +using NPin.Framework.AspNetCore.Authentication.OAuth.QQ; +using NPin.Framework.AspNetCore.Microsoft.AspNetCore.Builder; +using NPin.Framework.AspNetCore.Microsoft.Extensions.DependencyInjection; +using NPin.SqlSugarCore; +using Volo.Abp.AspNetCore.Authentication.JwtBearer; +using Volo.Abp.AspNetCore.MultiTenancy; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.AntiForgery; +using Volo.Abp.Auditing; +using Volo.Abp.Autofac; +using Volo.Abp.Caching; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Swashbuckle; namespace NPin; -// [DependsOn( -// typeof(NPin))] -public class NPinWebModule: AbpModule +[DependsOn( + typeof(NPinSqlSugarCoreModule), + typeof(NPinApplicationModule), + // Abp modules + typeof(AbpAspNetCoreMultiTenancyModule), + typeof(AbpAspNetCoreMvcModule), + typeof(AbpAutofacModule), + typeof(AbpSwashbuckleModule), + typeof(AbpAuditingModule), + typeof(AbpAspNetCoreAuthenticationJwtBearerModule), + // Framework modules + typeof(NPinFrameworkAspNetCoreModule), + typeof(NPinFrameworkAspNetCoreAuthenticationOAuthModule) +)] +public class NPinWebModule : AbpModule { - + private const string DefaultCorsPolicyName = "Default"; + + public override Task ConfigureServicesAsync(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + var services = context.Services; + + // 请求日志 + Configure(opt => + { + // 默认关闭,开启后有大量的审计日志 + opt.IsEnabled = false; + // 审计日志过滤器 + opt.AlwaysLogSelectors.Add(_ => Task.FromResult(true)); + }); + + // 动态API + Configure(opt => + { + opt.ConventionalControllers.Create(typeof(NPinApplicationModule).Assembly, + opts => opts.RemoteServiceName = "default"); + // TODO 添加其它模块的动态API + // Rbac bbs tenant code-gen + }); + + // Api格式配置 + services.AddControllers().AddNewtonsoftJson(opt => + { + // 时间格式 + opt.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; + // 枚举 <-> string 转换 + opt.SerializerSettings.Converters.Add(new StringEnumConverter()); + }); + + // 设置分布式缓存不要过期,滑动时间20分钟 + Configure(opt => + { + opt.GlobalCacheEntryOptions.SlidingExpiration = null; + // 缓存key前缀 + opt.KeyPrefix = "NPin:"; + }); + + // 关闭CSRF防伪验证 + Configure(opt => { opt.AutoValidate = false; }); + + // 设置 swagger + context.Services.AddNPinSwaggerGen(opt => + { + opt.SwaggerDoc("default", new OpenApiInfo + { + Title = "NPin.Framework", + Version = "v1", + Description = "NPin.Framework API文档" + }); + }); + + // 配置跨域 + context.Services.AddCors(opt => + { + opt.AddPolicy(DefaultCorsPolicyName, builder => + { + builder.WithOrigins( + configuration["App:CorsOrigins"]!.Split(";", StringSplitOptions.RemoveEmptyEntries) + .Select(o => o.RemovePostFix("/")) + .ToArray() + ) + .WithAbpExposedHeaders() + .SetIsOriginAllowedToAllowWildcardSubdomains() + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); + }); + + // 配置 多租户 + Configure(opt => + { + // 只需要 Header 方式的多租户 + // Cookie 方式在 jwt上有坑 + opt.TenantResolvers.Clear(); + opt.TenantResolvers.Add(new HeaderTenantResolveContributor()); + }); + + // 配置 JWT 鉴权 + // var jwtOptions = configuration.GetSection(nameof(JwtOptions)) + + context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(opt => { }) + .AddJwtBearer(opt => { }) + .AddQQ(opt => { configuration.GetSection("OAuth:QQ").Bind(opt); }) + .AddGitee(opt => { configuration.GetSection("OAuth:Gitee").Bind(opt); }); + + // 授权 + context.Services.AddAuthorization(); + + return Task.CompletedTask; + } + + public override Task OnApplicationInitializationAsync(ApplicationInitializationContext context) + { + var app = context.GetApplicationBuilder(); + + app.UseRouting(); + + // Cors + app.UseCors(DefaultCorsPolicyName); + + // TODO 无感Token refresh + + // 多租户 + app.UseMultiTenancy(); + + // Swagger + app.UseNPinSwagger(); + + // 中间件 + app.UseNPinApiMiddleware(); + + // 静态资源 + app.UseStaticFiles("/api/app/wwwroot"); + app.UseDefaultFiles(); + app.UseDirectoryBrowser("/api/app/wwwroot"); + + // 工作单元 + app.UseUnitOfWork(); + + // 授权 + app.UseAuthorization(); + + // 审计 + app.UseAuditing(); + + // 日志 + app.UseAbpSerilogEnrichers(); + + // 终端节点 + app.UseConfiguredEndpoints(); + + return Task.CompletedTask; + } } \ No newline at end of file