SqlSugar 差异日志功能实现
前言
在企业级应用开发中,数据库操作的审计日志是非常重要的功能。本文介绍如何基于 SqlSugar 框架实现数据库操作的差异日志记录功能,能够记录新增、修改、删除操作的具体变更内容。
功能特点
- 自动记录:在执行数据库操作时自动触发,无需手动调用
- 差异对比:更新操作记录字段修改前后的值
- 批量收集:支持同一请求中多次数据库操作的差异合并
- 统一输出:请求结束后合并输出为一条日志
核心代码实现
1. SqlSugar 配置
csharp
// 配置 SqlSugar 的 Updateable 对象完成时的回调:启用差异日志事件
StaticConfig.CompleteUpdateableFunc = it =>
{
var method = it.GetType().GetMethod("EnableDiffLogEvent");
method.Invoke(it, new object[] { null });
};
// 配置 SqlSugar 的 Insertable 对象完成时的回调:启用差异日志事件
StaticConfig.CompleteInsertableFunc = it =>
{
var method = it.GetType().GetMethod("EnableDiffLogEvent");
method.Invoke(it, new object[] { null });
};
// 配置 SqlSugar 的 Deleteable 对象完成时的回调:启用差异日志事件
StaticConfig.CompleteDeleteableFunc = it =>
{
var method = it.GetType().GetMethod("EnableDiffLogEvent");
method.Invoke(it, new object[] { null });
};
2. 差异日志处理核心逻辑
csharp
// 记录差异
db.Aop.OnDiffLogEvent = it =>
{
// 操作前记录(包含字段描述、列名、值、表名、表描述)
var editBeforeData = it.BeforeData;
// 操作后记录
var editAfterData = it.AfterData;
// 获取操作类型(枚举值:update、insert、delete)
var diffType = it.DiffType;
// 更新操作:比较修改前后的差异
if (diffType == DiffType.update)
{
var data = getDiff(editBeforeData, editAfterData);
if (data != null && !string.IsNullOrEmpty(data.diffData))
{
DiffLogCollector.AddDiff(editAfterData[0]?.TableDescription, data.diffData);
}
}
// 插入操作:获取新增的字段和值
if (diffType == DiffType.insert)
{
var data = getInsertDiff(editAfterData);
if (data != null && !string.IsNullOrEmpty(data.diffData))
{
DiffLogCollector.AddDiff(editAfterData[0]?.TableDescription, data.diffData);
}
}
// 删除操作:获取被删除的字段和值
if (diffType == DiffType.delete)
{
var data = getDeleteDiff(editBeforeData);
if (data != null && !string.IsNullOrEmpty(data.diffData))
{
DiffLogCollector.AddDiff(editBeforeData[0]?.TableDescription, data.diffData);
}
}
};
3. 差异比较辅助类
csharp
public class diffLog
{
public string ID { get; set; }
public string diffData { get; set; }
}
/// <summary>
/// 忽略的字段
/// </summary>
public static readonly List<string> IgnoreColumns = new List<string>()
{
"id",
"created_by",
"created_time",
"updated_time",
"createtime",
"updated_by",
"IsDeleted",
};
/// <summary>
/// 比较两个数据对象的修改内容
/// </summary>
public static diffLog getDiff(List<DiffLogTableInfo> beforeData, List<DiffLogTableInfo> afterData)
{
string mianID = null;
// 获取主键值
if (beforeData != null)
{
var keyCoulumn = beforeData[0].Columns.FirstOrDefault(p => p.IsPrimaryKey == true);
if (keyCoulumn != null)
{
mianID = keyCoulumn.Value.ToString();
}
}
else if (afterData != null)
{
var keyCoulumn = afterData[0].Columns.FirstOrDefault(p => p.IsPrimaryKey == true);
if (keyCoulumn != null)
{
mianID = keyCoulumn.Value.ToString();
}
}
StringBuilder sb = new StringBuilder();
if (beforeData != null && afterData != null)
{
var befroeColumns = beforeData[0].Columns;
var afterCloums = afterData[0].Columns;
foreach (var item in befroeColumns)
{
if (IgnoreColumns.Contains(item.ColumnName))
continue;
var afterItem = afterCloums.FirstOrDefault(p => p.ColumnName == item.ColumnName && !p.Value.Equals(item.Value));
if (afterItem != null)
{
sb.Append($"[字段:{item.ColumnDescription},修改前:{item.Value},修改后:{afterItem.Value}]");
}
}
}
string diffData = sb.Length > 0 ? $"修改: {sb}" : "";
return new diffLog { ID = mianID, diffData = diffData };
}
/// <summary>
/// 获取插入操作的新增数据内容
/// </summary>
public static diffLog getInsertDiff(List<DiffLogTableInfo> afterData)
{
string mainID = null;
StringBuilder sb = new StringBuilder();
if (afterData != null && afterData.Count > 0)
{
var columns = afterData[0].Columns;
// 获取主键值
var keyColumn = columns.FirstOrDefault(p => p.IsPrimaryKey == true);
if (keyColumn != null)
{
mainID = keyColumn.Value?.ToString();
}
// 遍历所有字段,排除忽略列
foreach (var col in columns)
{
if (IgnoreColumns.Contains(col.ColumnName))
continue;
sb.Append($"[{col.ColumnDescription}:{col.Value}]");
}
}
string diffData = sb.Length > 0 ? $"新增: {sb}" : "";
return new diffLog { ID = mainID, diffData = diffData };
}
/// <summary>
/// 获取删除操作的数据内容
/// </summary>
public static diffLog getDeleteDiff(List<DiffLogTableInfo> beforeData)
{
string mainID = null;
StringBuilder sb = new StringBuilder();
if (beforeData != null && beforeData.Count > 0)
{
var columns = beforeData[0].Columns;
// 获取主键值
var keyColumn = columns.FirstOrDefault(p => p.IsPrimaryKey == true);
if (keyColumn != null)
{
mainID = keyColumn.Value?.ToString();
}
// 遍历所有字段,排除忽略列
foreach (var col in columns)
{
sb.Append($"[{col.ColumnDescription}:{col.Value}]");
}
}
string diffData = sb.Length > 0 ? $"删除: {sb}" : "";
return new diffLog { ID = mainID, diffData = diffData };
}
4. 差异日志收集器
csharp
/// <summary>
/// 差异日志收集器,用于在单个 HTTP 请求生命周期内收集多个数据库操作的差异信息,
/// 最终合并输出一条日志。基于 HttpContext.Items 存储,确保每个请求独立,不受异步线程切换影响。
/// </summary>
public static class DiffLogCollector
{
private static IHttpContextAccessor _httpContextAccessor;
/// <summary>
/// 配置 IHttpContextAccessor 实例,必须在应用程序启动时调用
/// </summary>
public static void Configure(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// 获取当前 HTTP 上下文
/// </summary>
private static HttpContext Current => _httpContextAccessor?.HttpContext;
/// <summary>
/// 添加一条差异记录到当前请求的收集器中
/// </summary>
/// <param name="tableName">操作的表名</param>
/// <param name="diff">差异内容字符串</param>
public static void AddDiff(string tableName, string diff)
{
if (string.IsNullOrEmpty(diff)) return;
var context = Current;
if (context == null) return;
var list = context.Items["DiffLogs"] as List<string>;
if (list == null)
{
list = new List<string>();
context.Items["DiffLogs"] = list;
}
list.Add($"[{tableName}]{diff}");
}
/// <summary>
/// 获取当前请求收集到的所有差异记录列表
/// </summary>
public static List<string> GetDiffs()
{
var context = Current;
if (context == null) return new List<string>();
return context.Items["DiffLogs"] as List<string> ?? new List<string>();
}
/// <summary>
/// 清除当前请求收集的差异记录
/// </summary>
public static void Clear()
{
var context = Current;
context?.Items.Remove("DiffLogs");
}
/// <summary>
/// 指示当前请求是否收集了任何差异记录
/// </summary>
public static bool HasDiffs => GetDiffs().Any();
}
5. 差异日志中间件
csharp
/// <summary>
/// 差异日志中间件,用于在请求结束后输出合并后的数据库操作差异日志
/// </summary>
public class DiffLogMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public DiffLogMiddleware(RequestDelegate next, ILogger<DiffLogMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// 清空上次请求残留数据
DiffLogCollector.Clear();
// 执行真正的请求处理
await _next(context);
// 请求结束后,如果有差异则统一输出一条日志
if (DiffLogCollector.HasDiffs)
{
var apiDescription = GetApiDescription(context);
var combinedDiff = DiffLogCollector.GetDiffs();
var logMessage = $"{apiDescription} {combinedDiff}";
ConsoleHelper.WriteLine($"接口描述:{apiDescription}{Environment.NewLine}");
ConsoleHelper.WriteLine($"差异数据:{Environment.NewLine}{combinedDiff.ToJson()}");
}
}
/// <summary>
/// 从当前 HTTP 上下文中获取接口的描述信息
/// </summary>
private string GetApiDescription(HttpContext context)
{
var endpoint = context.GetEndpoint();
if (endpoint != null)
{
var methodInfo = endpoint.Metadata
.OfType<ControllerActionDescriptor>()
.FirstOrDefault()?.MethodInfo;
if (methodInfo != null)
{
var descAttr = methodInfo.GetCustomAttribute<DescriptionAttribute>();
if (descAttr != null)
return descAttr.Description;
}
}
return "";
}
}
6. 中间件注册
csharp
// 获取 IHttpContextAccessor 实例并配置
var httpContextAccessor = app.Services.GetRequiredService<IHttpContextAccessor>();
DiffLogCollector.Configure(httpContextAccessor);
// 注册差异日志中间件
app.UseMiddleware<DiffLogMiddleware>();
日志输出示例
接口描述:批量分发物料接口
差异数据:
["[物料分配表]新增: [使用组织ID:2044354624294621184][物料库ID:2052592996469313536][物料主图:xxx.webp][物料编码:trer][物料名称:te]...",
"[物料分配图片表]新增: [物料分配ID:2052664953516724224][图片URL:xxx.webp]",
"[物料分配表]新增: [使用组织ID:2044354028762173440]...",
"[物料分配图片表]新增: [物料分配ID:2052664953554472960][图片URL:xxx.webp]"]
使用前提
- 实体类字段需添加 Description 特性:用于显示字段中文描述
- 注册 IHttpContextAccessor :在
Program.cs中添加services.AddHttpContextAccessor() - 特性引用 :使用
[Description("xxx")]特性标注控制器方法
csharp
[HttpPost("BatchDistribute")]
[Description("批量分发物料接口")]
public async Task<Result> BatchDistribute(...)
{
// 业务代码
}
总结
通过 SqlSugar 的 OnDiffLogEvent 事件和 HttpContext.Items,我们实现了一个轻量级的数据库审计日志功能。该方案:
- 无侵入:不需要在每个业务方法中手动记录日志
- 高性能:只在数据库操作时记录,批量合并输出
- 易使用:配合特性可以自动获取接口描述
- 可扩展:可以很方便地对接不同的日志存储系统
希望这个方案对你有所帮助!如有问题欢迎留言讨论。