xiangning 52383d83f3 style: 目录统一 PascalCase 驼峰
- build-flow.host → BuildFlow.Host(子目录也驼峰:Configuration/Endpoints/Hubs/Services)
- build-flow.shared → BuildFlow.Shared
- build-flow.slnx → BuildFlow.slnx
- 同步所有 slnx/ProjectReference/run-dev.sh/架构测试 路径
- 删 macOS 大小写残留重复目录(旧 BuildFlow.Host/BuildFlow.Shared 空壳)
- build 0/0
2026-07-02 17:52:39 +08:00

108 lines
4.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using BuildFlow.Shared.Domain.Common;
namespace BuildFlow.Shared.Infrastructure;
/// <summary>
/// 审计拦截器,在 SaveChanges 时自动填充实体的审计字段(创建人/时间、修改人/时间、IP、软删除)。
/// 通过标记接口(IAuditable/ISoftDelete/IHasOperatorIP)判断实体需要哪些审计行为。
/// 注意:未实现 ISoftDelete 的实体在 Deleted 分支不触发,走真实物理删除。
/// </summary>
public class AuditInterceptor : SaveChangesInterceptor
{
private readonly ICurrentUserContext _currentUserContext;
public AuditInterceptor(ICurrentUserContext currentUserContext)
{
_currentUserContext = currentUserContext;
}
public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
ApplyAuditFields(eventData.Context);
return base.SavingChanges(eventData, result);
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
ApplyAuditFields(eventData.Context);
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
/// <summary>
/// 遍历 ChangeTracker 中所有实体,根据状态和标记接口自动填充审计字段。
/// </summary>
private void ApplyAuditFields(DbContext? context)
{
if (context is null) return;
var userId = _currentUserContext.UserId;
var ipAddress = _currentUserContext.IPAddress;
var utcNow = DateTime.UtcNow;
foreach (var entry in context.ChangeTracker.Entries())
{
switch (entry.State)
{
case EntityState.Added:
if (entry.Entity is IAuditable auditableAdded)
{
auditableAdded.CreatedBy = userId;
auditableAdded.CreatedAt = utcNow;
// 新增时也填充 Updated 字段,便于"最后变更"查询
auditableAdded.UpdatedBy = userId;
auditableAdded.UpdatedAt = utcNow;
}
if (entry.Entity is IHasOperatorIP hasIpAdded)
{
hasIpAdded.OperatorIP = ipAddress;
}
break;
case EntityState.Modified:
if (entry.Entity is IAuditable auditableModified)
{
auditableModified.UpdatedBy = userId;
auditableModified.UpdatedAt = utcNow;
// 防止业务层误改创建信息,强制标记为未修改
entry.Property(nameof(IAuditable.CreatedBy)).IsModified = false;
entry.Property(nameof(IAuditable.CreatedAt)).IsModified = false;
}
if (entry.Entity is IHasOperatorIP hasIpModified)
{
hasIpModified.OperatorIP = ipAddress;
}
break;
case EntityState.Deleted:
// 软删除:将硬删除操作拦截,转为修改 IsDeleted = true
// 未实现 ISoftDelete 的实体(如 RefreshToken)仍走硬删除
if (entry.Entity is ISoftDelete softDeletable)
{
entry.State = EntityState.Modified;
softDeletable.IsDeleted = true;
if (entry.Entity is IAuditable auditableDeleted)
{
auditableDeleted.UpdatedBy = userId;
auditableDeleted.UpdatedAt = utcNow;
entry.Property(nameof(IAuditable.CreatedBy)).IsModified = false;
entry.Property(nameof(IAuditable.CreatedAt)).IsModified = false;
}
if (entry.Entity is IHasOperatorIP hasIpDeleted)
{
hasIpDeleted.OperatorIP = ipAddress;
}
}
break;
}
}
}
}