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