基於SqlSugar的開發框架循序漸進介紹(8)-- 在基類函數封裝實現用戶操作日誌記錄

在我們對數據進行重要修改調整的時候,往往需要跟蹤記錄好用戶操作日誌。一般來說,如對重要表記錄的插入、修改、刪除都需要記錄下來,由於用戶操作日誌會帶來一定的額外消耗,因此我們通過配置的方式來決定記錄那些業務數據的重要調整。本篇隨筆介紹如何在基於SqlSugar的開發框架中,實現對用戶操作日誌記錄的配置設置,以及根據配置資訊自動實現用戶操作日誌記錄。

1、用戶操作日誌記錄的配置處理

前面提到,由於用戶操作日誌會帶來一定的額外消耗,因此我們通過配置的方式來決定記錄那些業務數據的重要調整。

首先我們在系統中定義一個用戶操作日誌記錄表和一個操作日誌配置資訊表,系統根據配置進行記錄重要的修改調整資訊。

 列表展示資訊如下所示

 

有了這些資訊記錄,我們可以在操作基類函數中,通過判斷SqlSugar實體類資訊中的是否插入、更新、刪除的重要設置,可以決定記錄它們那些操作日誌資訊。

下面列表記錄了對一些表的增加、修改、刪除、以及一些重要的系統操作日誌資訊,如「密碼重置」、「密碼修改」、「用戶過期設置」等操作日誌。

 

 

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把兩個對象的差異資訊生成,就可以用來記錄變更操作的資訊了,最終可以獲得類似下面介面提示的差異資訊。

 也就是獲得類似字元串的差異資訊。

[
  {
    "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)-- 在基類函數封裝實現用戶操作日誌記錄 》

Tags: