3

基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录 -...

 1 year ago
source link: https://www.cnblogs.com/wuhuacong/p/16371025.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在我们对数据进行重要修改调整的时候,往往需要跟踪记录好用户操作日志。一般来说,如对重要表记录的插入、修改、删除都需要记录下来,由于用户操作日志会带来一定的额外消耗,因此我们通过配置的方式来决定记录那些业务数据的重要调整。本篇随笔介绍如何在基于SqlSugar的开发框架中,实现对用户操作日志记录的配置设置,以及根据配置信息自动实现用户操作日志记录。

1、用户操作日志记录的配置处理

前面提到,由于用户操作日志会带来一定的额外消耗,因此我们通过配置的方式来决定记录那些业务数据的重要调整。

首先我们在系统中定义一个用户操作日志记录表和一个操作日志配置信息表,系统根据配置进行记录重要的修改调整信息。

8867-20220613154021862-2102032789.png

 列表展示信息如下所示

 

8867-20220613154144943-2010029546.png

有了这些信息记录,我们可以在操作基类函数中,通过判断SqlSugar实体类信息中的是否插入、更新、删除的重要设置,可以决定记录它们那些操作日志信息。

下面列表记录了对一些表的增加、修改、删除、以及一些重要的系统操作日志信息,如“密码重置”、“密码修改”、“用户过期设置”等操作日志。

8867-20220613154752046-1395208845.png

 

2、在基类中实现用户操作日志记录处理

上面界面展示了如何通过配置,自动记录用户对某业务的相关重要操作记录的界面。系统之所以能够进行相关的信息记录,是在基类函数中定义了相关的逻辑,根据配置逻辑,把插入对象的详细信息、修改对象的变化比记录、删除对象的详细信息进行写入,以及对一些重要的处理,如重置密码等,进行自定义的信息记录的。

下面我们来看看如何在基类中处理这些操作。

例如,我们在删除记录的时候,有时候接收的是实体类的ID,有时候接收的是实体类,那么对于这些条件,我们相应的进行日志处理,如下代码所示。

        /// <summary>
        /// 删除指定ID的对象
        /// </summary>
        /// <param name="id">记录ID</param>
        /// <returns></returns>
        public virtual async Task<bool> DeleteAsync(TEntity input)
        {
            await OnOperationLog(input, OperationLogTypeEnum.删除);
            return await EntityDb.DeleteAsync(input);
        }

        /// <summary>
        /// 删除指定ID的对象
        /// </summary>
        /// <param name="id">记录ID</param>
        /// <returns></returns>
        public virtual async Task<bool> DeleteAsync(TKey id)
        {
            await OnOperationLog(id, OperationLogTypeEnum.删除);
            return await EntityDb.DeleteByIdAsync(id);
        }

其中我们根据日志的操作,定义一个枚举的对象,如下所示。

    /// <summary>
    /// 操作日志的枚举类型
    /// </summary>
    public enum OperationLogTypeEnum
    {
        增加,
        删除,
        修改
    }

对于删除记录的Id,我们需要把它转换为对应的实体类,然后进行记录的。

        /// <summary>
        /// 统一处理实体类的日志记录
        /// </summary>
        /// <param name="id">实体对象Id</param>
        /// <param name="logType">记录类型</param>
        /// <returns></returns>
        protected override async Task OnOperationLog(TKey id, OperationLogTypeEnum logType)
        {
            var enableLog = await CheckOperationLogEnable(logType);
            if (enableLog)
            {
                var input = await this.EntityDb.GetByIdAsync(id);
                if (input != null)
                {
                    string note = JsonConvert.SerializeObject(input, Formatting.Indented);
                    await AddOperationLog(logType.ToString(), note);
                }
            }
            await Task.CompletedTask;//结束处理
        }

其中 CheckOperationLogEnable 就是用来判断是否存在指定操作类型的配置信息的,如果存在,那么就记录操作日志。

我们是根据实体类的全名进行判断,如果存在指定的操作设置,就返回True,如下所示。(刚好基类中可以判断泛型约束TEntity的全名)

        /// <summary>
        /// 判断指定的类型(增加、删除、修改)是否配置启用
        /// </summary>
        /// <param name="logType">指定的类型(增加、删除、修改)</param>
        /// <returns></returns>
        protected async Task<bool> CheckOperationLogEnable(OperationLogTypeEnum logType)
        {
            var result = false;

            string tableName = typeof(TEntity).FullName;//表名称或者实体类全名
            var settingInfo = await this._logService.GetOperationLogSetting(tableName);
            if (settingInfo != null)
            {
                if (logType == OperationLogTypeEnum.修改)
                {
                    result = settingInfo.UpdateLog > 0;
                }
                else if (logType == OperationLogTypeEnum.增加)
                {
                    result = settingInfo.InsertLog > 0;
                }
                else if (logType == OperationLogTypeEnum.删除)
                {
                    result = settingInfo.DeleteLog > 0;
                }
            }
            return result;
        }

对于插入记录,我们也可以同时进行判断并处理日志信息。

        /// <summary>
        /// 创建对象
        /// </summary>
        /// <param name="input">实体对象</param>
        /// <returns></returns>
        public virtual async Task<bool> InsertAsync(TEntity input)
        {
            SetIdForGuids(input);//如果Id为空,设置有序的GUID值

            await OnOperationLog(input, OperationLogTypeEnum.增加);//判断并记录日志
            return await EntityDb.InsertAsync(input);
        }

对于更新原有记录,它也只需要接收更新前的对象,然后进行判断处理即可。

        /// <summary>
        /// 更新对象
        /// </summary>
        /// <param name="input">实体对象</param>
        /// <returns></returns>
        public virtual async Task<bool> UpdateAsync(TEntity input)
        {
            SetIdForGuids(input);//如果Id为空,设置有序的GUID值

            await OnOperationLog(input, OperationLogTypeEnum.修改);//判断并记录日志
            return await EntityDb.UpdateAsync(input);
        }

比较两者,我们需要提供一个操作日志方法重载用于记录信息即可。

由于修改的信息,我们需要对比两个不同记录之间的差异信息,这样我们才能友好的判断那些信息变化了。也就是更新前后两个实体对象之间的属性差异信息,需要获取出来。

        /// <summary>
        /// 统一处理实体类的日志记录
        /// </summary>
        /// <param name="input">实体对象</param>
        /// <param name="logType">记录类型</param>
        /// <returns></returns>
        protected override async Task OnOperationLog(TEntity input, OperationLogTypeEnum logType)
        {
            var enableLog = await CheckOperationLogEnable(logType);
            if (enableLog && input != null)
            {
                if (logType == OperationLogTypeEnum.修改)
                {
                    var oldInput = await this.EntityDb.GetByIdAsync(input.Id);
                    //对于更新记录,需要判断更新前后两个对象的差异信息
                    var changeNote = oldInput.GetChangedNote(input); //计算差异的部分
                    if (!string.IsNullOrEmpty(changeNote))
                    {
                        await AddOperationLog(logType.ToString(), changeNote);
                    }
                }
                else
                {
                    //对于插入、删除的操作,只需要记录对象的信息
                    var note = JsonConvert.SerializeObject(input, Formatting.Indented);
                    await AddOperationLog(logType.ToString(), note);
                }
            }
            await Task.CompletedTask;//结束处理
        }

而对于差异信息,我能定义一个扩展函数来处理他们的差异信息,如下所示。

    /// <summary>
    /// 对象属性的处理操作
    /// </summary>
    public static class ObjectExtensions
    {
        /// <summary>
        /// 对比两个属性的差异信息
        /// </summary>
        /// <typeparam name="T">对象类型</typeparam>
        /// <param name="val1">对象实例1</param>
        /// <param name="val2">对象实例2</param>
        /// <returns></returns>
        public static List<Variance> DetailedCompare<T>(this T val1, T val2)
        {
            var propertyInfo = val1.GetType().GetProperties();
            return propertyInfo.Select(f => new Variance
            {
                Property = f.Name,
                ValueA = (f.GetValue(val1, null)?.ToString()) ?? "", //确保不为null
                ValueB = (f.GetValue(val2, null)?.ToString()) ?? ""
            })
            .Where(v => !v.ValueA.Equals(v.ValueB)) //调用内置的Equals判断
            .ToList();
        }

        /// <summary>
        /// 把两个对象的差异信息转换为JSON格式
        /// </summary>
        /// <typeparam name="T">对象类型</typeparam>
        /// <param name="val1">对象实例1</param>
        /// <param name="val2">对象实例2</param>
        /// <returns></returns>
        public static string GetChangedNote<T>(this T oldVal, T newVal)
        {
            var specialList = new List<string> { "edittime", "createtime", "lastupdated" };
            var list = DetailedCompare<T>(oldVal, newVal);
            var newList = list.Select(s => new { Property = s.Property, OldValue = s.ValueA, NewValue = s.ValueB })
                             .Where(s=> !specialList.Contains(s.Property.ToLower())).ToList();//排除某些属性
            
            string note = null;
            if (newList?.Count > 0)
            {
                //增加一个ID属性记录显示
                var id = EntityHelper.GetEntityId(oldVal)?.ToString();
                newList.Add(new { Property = "Id", OldValue = id, NewValue = id });

                note = JsonConvert.SerializeObject(newList, Formatting.Indented);
            }
            return note;
        }

        public class Variance
        {
            public string Property { get; set; }
            public string ValueA { get; set; }
            public string ValueB { get; set; }
        }
    }

这样我们通过LINQ把两个对象的差异信息生成,就可以用来记录变更操作的信息了,最终可以获得类似下面界面提示的差异信息。

8867-20220613165044996-2029829457.png

 也就是获得类似字符串的差异信息。

[
  {
    "Property": "PID",
    "OldValue": "-1",
    "NewValue": "0"
  },
  {
    "Property": "OfficePhone",
    "OldValue": "",
    "NewValue": "18620292076"
  },
  {
    "Property": "WorkAddr",
    "OldValue": "广州市白云区同和路**小区**号",
    "NewValue": "广州市白云区同和路330号君立公寓B栋1803房"
  },
  {
    "Property": "Id",
    "OldValue": "1",
    "NewValue": "1"
  }
]

最后的属性Id,是我们强行加到变化列表中的,因为不记录Id的话,不清楚那个记录变更了。

这样我们就实现了增删改的重要操作的记录,并且由于是基类实现,我们只需要在系统中配置决定哪些业务类需要记录即可自动实现重要日志的记录。

另外,我们在类别中还发现了其他一些不同类别的重要操作日志,如重置密码、修改密码、用户过期设置等,这些操作我们提供接口给这些处理调用即可。

        /// <summary>
        /// 设置用户的过期与否
        /// </summary>
        /// <param name="userId">用户ID</param>
        /// <param name="expired">是否禁用,true为禁用,否则为启用</param>
        public async Task<bool> SetExpire(int userId, bool expired)
        {
            bool result = false;
            var info = await this.GetAsync(userId);
            if (info != null)
            {
                info.IsExpire = expired;
                result = await this.UpdateAsync(info);
                if (result)
                {
                    //记录用户修改密码日志
                    string note = string.Format("{0} {1}了用户【{2}】的账号", this.CurrentApiUser.FullName, expired ? "禁用" : "启用", info.Name);
                    await base.AddOperationLog("用户过期设置", note);
                }
            }
            return result;
        }

其中 AddOperationLog 就是我们调用基类插入指定类型和日志信息的记录的,通过自定义类型和自定义日志信息,可以让我们弹性化的处理一些重要日志记录。

系列文章:

基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用

基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理

基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发

基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理 

基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转

基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口 

基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传

基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录 


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK