前言
我们在做企业管理系统时,有多多少少都有对数据的完整性有所要求,比如要求系统不能物理删除记录,要求添加每一条数据时都要有系统记录、或者更新某条数据都需要跟踪到变化的内容、或者删除数据时需要记录谁删除了,何时删除了,以便误删后可以通过系统的XXX功能来恢复误删的数据。
我将这种功能称为操作日志
为什么要做操作日志?
其实上文也描述了一些,其主要目的就是跟踪到每一个用户在系统的操作行为,如对数据进行查询、新增、编辑或删除甚至是登录等行为。更进一步的理解可以说是对用户使用系统情况的跟踪,对数据的跟踪防止数据意外删除、更改时有所记录,有所依据,以便对数据的还原,从某种程序上可以保护数据的完整性。
系统设计
场景
我们现在有一张表叫Employee:
ID | int |
Name | nvarchar(50) |
Gender | nvarchar(2) |
DateCreated | datetime |
CreateUser | nvarchar(50) |
在aspx页面中可能会有EmployeeEdit.aspx(用来添加或更新Employee信息等操作),EmployeeList.aspx(用来查询或进行删除Employee信息等操作)
好了,现在我们要对Empoyee表操作的信息做一个系统日志,那怎么办?
也许你可以建立多一个表跟Employee表一模一样的,叫做EmployeeLog:
ID | int |
Name | nvarchar(50) |
Gender | nvarchar(2) |
DateCreated | datetime |
CreateUser | nvarchar(50) |
LogCreated | datetime |
OperationType | int |
其中加多了一些附属的信息如LogCreated(日志添加日期)和OperationType(查询、新增、删除、更新)
此时这种情况可能大家在做用户登录日志的时候是一件很常见的事件。
但……问题来了,假如我需要对表EmployeeIncome(员工的收入情况)做日志那怎么办?
好建立多一张表叫EmployeeIncomeLog来记录员工收入情况的操作日志。
假如又需要对表FixedAsset(固定资产)进行日志记录那又怎么办?
好了,大家可能意识到我们这样做不但会造成表数量的增倍,而且大大的增加了工作量和开发时间,对数据库表不易管理等情况。
因此我们需要一个能够通过简单的配置和编写就可以完成以上功能的日志管理
数据库设计
包括三个表,
LogSetting(日志设置)——用来存储配置业务表名、业务名称、主键等
LogSettingDetail(日志设置明细)——用来存储配置业务表需要记录的详细内容,如Employee表中,我们可能需要记录字段Name、Gender等信息。
LogOperation(操作日志)——用来记录用户对各种业务操作的内容情况。
下篇将讨论用代码如何实现日志管理的功能,下面先来几张图:
日志列表:
查看日志内容:
通过思考大概清楚系统操作日志的设计,以下是其UML图:
通过上图,我们可以了解知道该UML主要由三个表组成,其中一个主表LogSetting和两个从表分别是LogOperation和LogSettingDetail。
那么怎么样才能通过这样的设计来现实我们的日志功能呢?
其实一开始我就觉得通过.net的反射功能可以很简单、很方便的实现这个功能,所以我就顺着一个思路来实现她;通过反射动态的获取Model实体的属性,然后再根据LogSettingDetail配置来匹配所要记录的字段信息。
先来主要的代码吧,发现将思想用文字表达出来还是较困难的,代码比较直接:
代码的实现
using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; using BLL.Sys; using System.Collections.Generic; using System.Collections.Specialized; using System.Text; using System.Reflection; /// <summary> /// LogManager 的摘要说明 /// </summary> public class LogManager<T> where T : new() { #region Constructor /// <summary> /// 日志管理构造函数 /// </summary> public LogManager() { tableName = typeof(T).Name; Model.Sys.LogSetting model = GetLogSetting(tableName); if (model != null) { businessName = model.BusinessName; logID = model.LogID; primaryKey = model.PrimaryKey; urlTemplate = model.UrlTemplate; deleteScriptTemplate = model.DeleteScriptTemplate; updateScriptTemplate = model.UpdateScriptTemplate; } else { throw new ArgumentNullException("日志设置为空!"); } } /// <summary> /// /// 日志管理构造函数 /// </summary> /// </summary> /// <param name="tableName">表名</param> /// <param name="businessName">业务名称</param> public LogManager(string tableName, string businessName) { this.tableName = tableName; this.businessName = businessName; Model.Sys.LogSetting model = GetLogSetting(tableName, businessName); if (model != null) { logID = model.LogID; primaryKey = model.PrimaryKey; urlTemplate = model.UrlTemplate; deleteScriptTemplate = model.DeleteScriptTemplate; updateScriptTemplate = model.UpdateScriptTemplate; } else { throw new ArgumentNullException("日志设置为空!"); } } #endregion #region Properties private int logID; private string tableName; private string businessName; private string primaryKey; private string urlTemplate; private string deleteScriptTemplate; private string updateScriptTemplate; /// <summary> /// 日志设置实体列表 /// </summary> public List<Model.Sys.LogSetting> LogSettingList { get { System.Web.Caching.Cache cache = HttpRuntime.Cache; List<Model.Sys.LogSetting> list = cache["LogSettingList"] as List<Model.Sys.LogSetting>; if (list != null && list.Count > 0) { return list; } else { LogSetting bll = new LogSetting(); list = bll.GetModelList(string.Empty); cache["LogSettingList"] = list; return list; } } set { System.Web.Caching.Cache cache = HttpRuntime.Cache; cache["LogSettingList"] = null; } } /// <summary> /// 日志设置明细 /// </summary> public List<Model.Sys.LogSettingDetail> LogSettingDetail { get { System.Web.Caching.Cache cache = HttpRuntime.Cache; List<Model.Sys.LogSettingDetail> list = cache["LogSettingDetail"] as List<Model.Sys.LogSettingDetail>; if (list != null && list.Count > 0) { return list; } else { LogSettingDetail bll = new LogSettingDetail(); list = bll.GetModelList(string.Empty); cache["LogSettingDetail"] = list; return list; } } set { System.Web.Caching.Cache cache = HttpRuntime.Cache; cache["LogSettingDetail"] = null; } } #endregion #region Method /// <summary> /// 通过logId获取日志设置明细 /// </summary> /// <param name="logId">日志设置编号</param> /// <returns></returns> private List<Model.Sys.LogSettingDetail> GetLogSettingDetails(int logId) { if (logId == 0) throw new ArgumentNullException("LogID为空"); List<Model.Sys.LogSettingDetail> list = new List<Model.Sys.LogSettingDetail>(); foreach (Model.Sys.LogSettingDetail var in LogSettingDetail) { if (var.LogID == logId) list.Add(var); } return list; } /// <summary> /// 通过tableName和businessName来获取日志设置对象 /// </summary> /// <param name="tableName"></param> /// <param name="businessName"></param> /// <returns></returns> private Model.Sys.LogSetting GetLogSetting(string tableName, string businessName) { foreach (Model.Sys.LogSetting var in LogSettingList) { if (var.TableName.Equals(tableName, StringComparison.InvariantCultureIgnoreCase) && var.BusinessName.Equals(businessName, StringComparison.InvariantCultureIgnoreCase)) return var; } return null; } private Model.Sys.LogSetting GetLogSetting(string tableName) { foreach (Model.Sys.LogSetting var in LogSettingList) { if (var.TableName.Equals(tableName, StringComparison.InvariantCultureIgnoreCase)) return var; } return null; } /// <summary> /// 比较两个实体,然后返回实体中每个属性值不同的内容 /// </summary> /// <param name="oldObj"></param> /// <param name="newObj"></param> /// <returns></returns> public string Compare(T oldObj, T newObj) { Type objTye = typeof(T); StringBuilder sbResult = new StringBuilder(); string tableHeader = "<table class=\"GridView\" cellspacing=\"0\" rules=\"all\" border=\"1\" id=\"gv\" style=\"border-collapse:collapse;\">"; tableHeader += "<tr><th scope=\"col\">序号</th><th scope=\"col\">字段</th><th scope=\"col\">名称</th><th scope=\"col\">旧值</th><th scope=\"col\">新值</th></tr>"; string tableRow = "<tr class='{0}'><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td><td>{5}</td></tr>"; List<Model.Sys.LogSettingDetail> list = GetLogSettingDetails(logID); int i = 1; foreach (Model.Sys.LogSettingDetail var in list) { PropertyInfo property = objTye.GetProperty(var.ColumnName); if (property != null && !property.IsSpecialName) { object o = property.GetValue(oldObj, null); object n = property.GetValue(newObj, null); if (!IsEqual(property.PropertyType, o, n)) { sbResult.AppendFormat(tableRow, i % 2 == 0 ? "odd" : "even", i, var.ColumnName, var.ColumnText, o, n); i++; } } } sbResult.Append("</table>"); #region Add Log Record if (i > 1) { Model.Sys.LogOperation operModel = new Model.Sys.LogOperation(); operModel.LogID = logID; operModel.OperationType = (int)OperationType.Update; operModel.Content = tableHeader + sbResult.ToString(); operModel.CreateTime = DateTime.Now; if (HttpContext.Current != null) operModel.CreateUser = HttpContext.Current.User.Identity.Name; if (!string.IsNullOrEmpty(primaryKey)) { PropertyInfo p = objTye.GetProperty(primaryKey); object o = p.GetValue(newObj, null); if (o != null) { operModel.PrimaryKeyValue = o.ToString(); if (urlTemplate.Contains("{0}")) operModel.Url = string.Format(urlTemplate, o.ToString()); } } LogOperation operBll = new LogOperation(); operBll.Add(operModel); } #endregion return tableHeader + sbResult.ToString(); } /// <summary> /// 删除实体操作,这里并不是真的删除该实体,而是将删除的操作记录在日志中 /// </summary> /// <param name="obj"></param> /// <returns></returns> public string Delete(T obj) { Type objTye = typeof(T); StringBuilder sbResult = new StringBuilder(); string tableHeader = "<table class=\"GridView\" cellspacing=\"0\" rules=\"all\" border=\"1\" id=\"gv\" style=\"border-collapse:collapse;\">"; tableHeader += "<tr><th scope=\"col\">序号</th><th scope=\"col\">字段</th><th scope=\"col\">名称</th><th scope=\"col\">值</th></tr>"; string tableRow = "<tr class='{0}'><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td></tr>"; List<Model.Sys.LogSettingDetail> list = GetLogSettingDetails(logID); int i = 1; foreach (Model.Sys.LogSettingDetail var in list) { PropertyInfo property = objTye.GetProperty(var.ColumnName); if (property != null && !property.IsSpecialName) { object o = property.GetValue(obj, null); sbResult.AppendFormat(tableRow, i % 2 == 0 ? "odd" : "even", i, var.ColumnName, var.ColumnText, o); i++; } } sbResult.Append("</table>"); #region Add Log Record Model.Sys.LogOperation operModel = new Model.Sys.LogOperation(); operModel.LogID = logID; operModel.OperationType = (int)OperationType.Delete; operModel.Content = tableHeader + sbResult.ToString(); operModel.CreateTime = DateTime.Now; if (!string.IsNullOrEmpty(primaryKey)) { PropertyInfo p = objTye.GetProperty(primaryKey); object o = p.GetValue(obj, null); if (o != null) { operModel.PrimaryKeyValue = o.ToString(); if (urlTemplate.Contains("{0}")) operModel.Url = string.Format(urlTemplate, o.ToString()); } } if (HttpContext.Current != null) operModel.CreateUser = HttpContext.Current.User.Identity.Name; LogOperation operBll = new LogOperation(); operBll.Add(operModel); #endregion return string.Empty; } /// <summary> /// 添加实体,将添加的操作记录在日志中 /// </summary> /// <param name="obj"></param> /// <returns></returns> public string Add(T obj) { Type objTye = typeof(T); StringBuilder sbResult = new StringBuilder(); string tableHeader = "<table class=\"GridView\" cellspacing=\"0\" rules=\"all\" border=\"1\" id=\"gv\" style=\"border-collapse:collapse;\">"; tableHeader += "<tr><th scope=\"col\">序号</th><th scope=\"col\">字段</th><th scope=\"col\">名称</th><th scope=\"col\">值</th></tr>"; string tableRow = "<tr class='{0}'><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td></tr>"; List<Model.Sys.LogSettingDetail> list = GetLogSettingDetails(logID); int i = 1; foreach (Model.Sys.LogSettingDetail var in list) { PropertyInfo property = objTye.GetProperty(var.ColumnName); if (property != null && !property.IsSpecialName) { object o = property.GetValue(obj, null); sbResult.AppendFormat(tableRow, i % 2 == 0 ? "odd" : "even", i, var.ColumnName, var.ColumnText, o); i++; } } sbResult.Append("</table>"); #region Add Log Record Model.Sys.LogOperation operModel = new Model.Sys.LogOperation(); operModel.LogID = logID; operModel.OperationType = (int)OperationType.Add; operModel.Content = tableHeader + sbResult.ToString(); operModel.CreateTime = DateTime.Now; if (!string.IsNullOrEmpty(primaryKey)) { PropertyInfo p = objTye.GetProperty(primaryKey); object o = p.GetValue(obj, null); if (o != null) { operModel.PrimaryKeyValue = o.ToString(); if (urlTemplate.Contains("{0}")) operModel.Url = string.Format(urlTemplate, o.ToString()); } } if (HttpContext.Current != null) operModel.CreateUser = HttpContext.Current.User.Identity.Name; LogOperation operBll = new LogOperation(); operBll.Add(operModel); #endregion return string.Empty; } /// <summary> /// 复制一个对象 /// </summary> /// <param name="obj"></param> /// <returns></returns> public T Clone(T obj) { Type objTye = typeof(T); T model = new T(); PropertyInfo[] properties = objTye.GetProperties(); foreach (PropertyInfo property in properties) { if(!property.IsSpecialName) { object o = property.GetValue(obj, null); property.SetValue(model, o, null); } } return model; } private bool IsEqual(Type dataType, object oldObj, object newObj) { if (oldObj == null && newObj == null) return true; if (dataType == typeof(int)) { return (int)oldObj == (int)newObj; } else if (dataType == typeof(decimal)) { return (decimal)oldObj == (decimal)newObj; } else if (dataType == typeof(double)) { return (double)oldObj == (double)newObj; } else if (dataType == typeof(Guid)) { return (Guid)oldObj == (Guid)newObj; } else if (dataType == typeof(DateTime)) { return (DateTime)oldObj == (DateTime)newObj; } else return oldObj.Equals(newObj); } #region Script Excute //public int DeleteBusRecode(string primaryKeyValue) //{ // if (string.IsNullOrEmpty(tableName)) // throw new ArgumentException("tableName为空"); // if(string.IsNullOrEmpty(primaryKey)) // throw new ArgumentException("primaryKey为空"); // if (string.IsNullOrEmpty(deleteScriptTemplate)) // throw new ArgumentException("deleteScriptTemplate为空"); // string strSql = string.Format(deleteScriptTemplate, primaryKeyValue); // Database db = DatabaseFactory.CreateDatabase(); // return 0; //} #endregion #endregion } public enum OperationType { Select = 0, Add = 1, Update = 2, Delete = 3 }
使用的场景
Model文件:
public class EmployeeModel { public int ID{get;set;} public string Name{get;set;} … }
下面介绍如何将系统操作日志集成到你的业务系统中
添加操作:
EmployeeBll bll = new EmployeeBll();
EmployeeModel model = new EmployeeModel();
/* model 实体经过漫长的 赋值 后… */
bll.Add(model); //添加实体
//添加系统操作记录
//日志 LogManager<EmployeeModel> log = new LogManager<EmployeeModel>();
log.Add(model);
更新操作:
EmployeeBll bll = new EmployeeBll();
EmployeeModel model = bll.GetModel(employeeID);
LogManager<EmployeeModel> log = new LogManager<EmployeeModel>();
EmployeeModel modelOld = log.Clone(model); //克隆EmployeeModel实体对象,这个主要是在系统操作日志记录时使用的
/* model 实体又经过漫长的 赋值 后… */
bll.Update(model); //更新实体
//将更新的内容写入系统操作日志中
log.Compare(modelOld, model); //原来的实体和赋值后的实体对比,并将更新的内容写入系统操作日志中
删除操作:
在GridView的RowDeleting事件中获取要删除的实体
EmployeeBll bll = new EmployeeBll();
EmployeeModel model = bll.GetModel(employeeID);
bll.Delete(employeeID);
LogManager<EmployeeModel> log = new LogManager<EmployeeModel>(); log.Delete(model); //实体的内容记录到日志中
总结:
大家可以看到代码还是比较粗糙的,有不少的重复的代码,下一节将会讨论如何进行系统操作日志管理。
另外如何大家有什么意见或想法请分享提出。
本节用到的知识点:
1、泛型
2、反射
3、缓存
优点:
1、使用和集成方便,代码量小;
2、大大提高工作效率,避免表爆炸;
缺点:
1、代码有待优化;
2、可扩展性较差