# SqlSugar 差异日志功能实现

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]"]

使用前提

  1. 实体类字段需添加 Description 特性:用于显示字段中文描述
  2. 注册 IHttpContextAccessor :在 Program.cs 中添加 services.AddHttpContextAccessor()
  3. 特性引用 :使用 [Description("xxx")] 特性标注控制器方法
csharp 复制代码
[HttpPost("BatchDistribute")]
[Description("批量分发物料接口")]
public async Task<Result> BatchDistribute(...)
{
    // 业务代码
}

总结

通过 SqlSugar 的 OnDiffLogEvent 事件和 HttpContext.Items,我们实现了一个轻量级的数据库审计日志功能。该方案:

  • 无侵入:不需要在每个业务方法中手动记录日志
  • 高性能:只在数据库操作时记录,批量合并输出
  • 易使用:配合特性可以自动获取接口描述
  • 可扩展:可以很方便地对接不同的日志存储系统

希望这个方案对你有所帮助!如有问题欢迎留言讨论。

相关推荐
顾温2 小时前
协程结束——实测
开发语言·unity·c#
唐青枫3 小时前
C#.NET YARP 详解:用 ASP.NET Core 打造高性能反向代理网关
c#·.net
asdzx674 小时前
告别手工复制:用 C# 轻松合并多份 Word
c#·word
步步为营DotNet5 小时前
NET 11 中 C# 14 新特性在云原生微服务架构的深度实践
云原生·架构·c#
不会编程的懒洋洋6 小时前
WPF 性能优化+异步+渲染
开发语言·笔记·性能优化·c#·wpf·图形渲染·线程
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ18 小时前
通过java后端代码来实现给word内容补充格式文本内容控件,以及 设置控件的标记和标题
java·c#·word
Hesionberger1 天前
LeetCode79:单词搜索DFS回溯详解
java·开发语言·c++·python·算法·leetcode·c#
曹牧1 天前
C#:同一项目中维护多个版本的代码
开发语言·c#
工程师0071 天前
C# UI 跨线程刷新:Invoke/BeginInvoke 原理与封装
c#·invoke·begininvoke