在前面介绍了几篇关于我的权限系统改进的一些经验总结,本篇继续这一系列主体,介绍如何一行代码实现重要表的操作日志记录。我们知道,在很多业务系统里面,数据是很敏感的,特别对于一些增加、修改、删除等关键的操作,如果能在框架层面的支持基础上,以最少的代码实现重要表的日志记录,那么是一件非常值得庆贺的事情,也能够为我们客户的数据提供重要的日志跟踪,甚至是数据恢复的参考。
1、数据访问层的对象继承关系
首先,为了减少重复代码的编写,合理的继承关系是必要的,我们需要在数据访问层上建立合理的继承关系,如下是我的Winform开发框架的继承关系。每个数据访问对象(如ItemDetail数据访问对象)都继承一个抽象基类AbstractBaseDAL和一个IBaseDAL基类接口,同时它也有自己特殊的业务接口,如IItemDetail,关系如下所示。
有了上面的继承关系,我们就可以把常规的数据库重要操作(增删改)放到一个高一级的层次上去解决这个问题,而不需要在每个数据访问层的业务类来实现。
2、操作日志记录事件的定义和使用
为了更好实现数据操作日志的记录,我们以事件方式来触发操作日志的记录,事件的具体记录实现,可以交给外部来记录处理。如果事件被外部赋值了,那么就可以在底层触发这个事件记录,记录事件的定义代码在抽象基类进行定义,如下所示。
/// 定义一个记录操作日志的事件处理 /// </summary> /// <param name="userId">操作的用户ID</param> /// <param name="tableName">操作表名称</param> /// <param name="operationType">操作类型:增加、修改、删除</param> /// <param name="note">操作的详细记录信息</param> /// <returns></returns> public delegate bool OperationLogEventHandler(string userId, string tableName, string operationType, string note, DbTransaction trans = null); /// <summary> /// 数据访问层的超级基类,所有数据库的数据访问基类都继承自这个超级基类,包括Oracle、SqlServer、Sqlite、MySql、Access等 /// </summary> public abstract class AbstractBaseDAL<T> where T : BaseEntity, new() { #region 构造函数 protected string dbConfigName = ""; //数据库配置名称 protected string parameterPrefix = "@";//数据库参数化访问的占位符 protected string safeFieldFormat = "[{0}]";//防止和保留字、关键字同名的字段格式,如[value] protected string tableName;//需要初始化的对象表名 protected string primaryKey;//数据库的主键字段名 protected string sortField;//排序字段 protected bool isDescending = true;//是否为降序 protected string selectedFields = " * ";//选择的字段,默认为所有(*) public event OperationLogEventHandler OnOperationLog;//定义一个操作记录的事件处理
.....................
以上是抽象基类AbstractBaseDAL的部分代码,上面代码定义了一个操作记录的委托和事件对象来处理操作日志的记录,通过委托的定义,我们可以规定具体的事件接口定义,并在抽象基类的底层构造这些参数的数值,传递给外部的对象进行处理。
那么我们是如何在底层操作构造这些信息的呢?
其实就是在相应的重要操作接口函数上调用这个定义的事件。我们可以在抽象基类的插入、修改、删除等接口上调用事件进行处理即可,为了更好处理相关数据的构造逻辑,我们把调用OnOperationLog的事件封装到一个单独的函数里面进行处理,如下所示是底层更新操作的代码,通过增加一个OperationLogOfUpdate来实现数据日志的事件处理。
/// <summary> /// 更新对象属性到数据库中 /// </summary> /// <param name="obj">指定的对象</param> /// <param name="primaryKeyValue">主键的值</param> /// <param name="trans">事务对象</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c>。</returns> public virtual bool Update(T obj, object primaryKeyValue, DbTransaction trans = null) { ArgumentValidation.CheckForNullReference(obj, "传入的对象obj为空"); OperationLogOfUpdate(obj, primaryKeyValue, trans);//根据设置记录操作日志 Hashtable hash = GetHashByEntity(obj); return Update(primaryKeyValue, hash, trans); }
然后我们在具体的事件处理封装函数OnOperationLog的里面添加处理逻辑即可,一般事件的标准处理为如下代码。
/// <summary> /// 修改操作的日志记录 /// </summary> /// <param name="id">记录ID</param> /// <param name="obj">数据对象</param> /// <param name="trans">事务对象</param> protected virtual void OperationLogOfUpdate(T obj, object id, DbTransaction trans = null) { if (OnOperationLog != null) { ...............................//构造相关参数 OnOperationLog(userId, this.tableName, operationType, note, trans); } } }
我们知道,一般操作日志都会记录是谁进行操作的,然后把它写到日志里面,并把操作的内容可读化即可,那么在更新的时候,我们如何知道是谁操作的对象呢?因为我们没有传递具体的用户ID等标识的啊。
这个问题挺头痛,如果增加多一个参数,那么就得修改很多相关的调用逻辑,这个明显不太符合我们简约的风格,因此最好另寻其他方式来实现这个人员身份记录的问题。
我们知道,一般插入、更新操作,都是带一个操作对象的,这个操作对象是一个实体类,基类是BaseEntity,那么我们可以在它的身上定义多一个属性,这个属性不参数数据的保存,只是作为参数的传递和识别而已,实体类基类的代码如下所示。
/// <summary> /// 框架实体类的基类 /// </summary> [DataContract] public class BaseEntity { private string m_CurrentLoginUserId; /// <summary> /// 当前登录用户ID。该字段不保存到数据表中,只用于记录用户的操作日志。 /// </summary> [DataMember] public string CurrentLoginUserId { get { return m_CurrentLoginUserId; } set { m_CurrentLoginUserId = value; } } } }
有了这个信息,我们就可以在刚才的事件处理逻辑上进行获取用户的ID操作了。
string userId = obj.CurrentLoginUserId;
下一个问题是,如何把操作的信息可读化,我们知道,一般操作只是对部分字段进行修改,那么我们一般也不需要把所有的字段信息都弄出来显示,只需要显示那些修改的即可。
为了实现这个数据的差异化显示,我们需要在更新操作之前进行获取数据库的对象信息,然后和将要进行更新的对象进行对比,把差异的信息作为备注信息记录下来即可,具体逻辑如下所示。
/// <summary> /// 修改操作的日志记录 /// </summary> /// <param name="id">记录ID</param> /// <param name="obj">数据对象</param> /// <param name="trans">事务对象</param> protected virtual void OperationLogOfUpdate(T obj, object id, DbTransaction trans = null) { if (OnOperationLog != null) { string operationType = "修改"; string userId = obj.CurrentLoginUserId; Hashtable recordField = GetHashByEntity(obj); Dictionary<string, string> dictColumnNameAlias = GetColumnNameAlias(); T objInDb = FindByID(id, trans); if (objInDb != null) { Hashtable dbrecordField = GetHashByEntity(objInDb);//把数据库里的实体对象数据转换为哈希表 StringBuilder sb = new StringBuilder(); foreach (string field in recordField.Keys) { string newValue = recordField[field].ToString(); string oldValue = dbrecordField[field].ToString(); if (newValue != oldValue)//只记录变化的内容 { string columnAlias = ""; bool result = dictColumnNameAlias.TryGetValue(field, out columnAlias); if (result && !string.IsNullOrEmpty(columnAlias)) { columnAlias = string.Format("({0})", columnAlias);//字段中文名称前,增加一个括号显示,方便区分显示 } sb.AppendLine(string.Format("{0}{1}:", field, columnAlias)); sb.AppendLine(string.Format("\t {0} -> {1}", dbrecordField[field], recordField[field])); sb.AppendLine(); } } sb.AppendLine(); string note = sb.ToString(); OnOperationLog(userId, this.tableName, operationType, note, trans); } } }
上面是更新操作的日志记录处理,其他的插入、删除等操作类似这样的操作方式,再次不在赘述。
3、业务层对操作日志信息的处理
上面的代码只是实现了对底层操作的信息记录并传递给操作日志的记录事件,并没有知道上层是如何处理事件信息的记录的,这个问题留给上层去处理。
为了实现这个信息的记录,我们在权限系统里面增加一个单独的数据库表如T_ACL_OperationLog表用来专门记录这些信息的。
上层的处理逻辑是获取用户ID的登陆信息,包括用户名、用户IP地址、Mac地址信息等,这些信息一旦用户登陆到系统就会发生了,所以可以方便获取到,然后就是把这些信息作为一个数据库记录写入数据库表即可。
OperationLogInfo info = new OperationLogInfo(); info.TableName = tableName; info.OperationType = operationType; info.Note = note; info.CreateTime = DateTime.Now; if (!string.IsNullOrEmpty(userId)) { UserInfo userInfo = BLLFactory<User>.Instance.FindByID(userId, trans); if (userInfo != null) { info.User_ID = userId; info.LoginName = userInfo.Name; info.FullName = userInfo.FullName; info.Company_ID = userInfo.Company_ID; info.CompanyName = userInfo.CompanyName; info.MacAddress = userInfo.CurrentMacAddress; info.IPAddress = userInfo.CurrentLoginIP; } } return BLLFactory<OperationLog>.Instance.Insert(info, trans);
因为这些记录的操作都是一样的,为了更方便,我们把这些逻辑封装在一个静态的方法里面,然后所有需要记录操作日志的,在业务对象里面增加一行代码,就可以轻松实现日志记录了,具体代码如下所示。
/// <summary> /// 部门机构信息 /// </summary> public class OU : BaseBLL<OUInfo> { private IOU ouDal; /// <summary> /// 构造函数 /// </summary> public OU() : base() { base.Init(this.GetType().FullName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Name); baseDal.OnOperationLog += new OperationLogEventHandler(WHC.Security.BLL.OperationLog.OnOperationLog);//如果需要记录操作日志,则实现这个事件 this.ouDal = baseDal as IOU; }
这样数据访问类baseDal一旦事件初始化,那么就会在底层进行触发,然后交给事件的处理逻辑(上层操作)进行处理了。
为了更好控制用户的增加、修改、删除的相关事件,我们可以通过一个配置表进行登记处理,然后根据配置表的参数来决定记录那些信息,这些就是细化的问题了。
4、我的Winform开发框架的权限系统模块里面对于操作日志的支持
在我的Winform开发框架里面,权限系统是其中的一个基础部分,因此也根据上面的逻辑实现了对操作日志的参数配置和记录显示,方便对业务系统所有表的操作记录进行跟踪和处理。
通过一行代码就能实现业务表的日志记录,对我们开发新的业务模块,效率可以提高很多,同时也能给客户提供更好的数据支持服务。通过在权限系统模块里面配置参数和显示操作日志记录,能够给业务开发提供基础性的开发框架支持。
下面是我的Winform开发框架的权限系统模块的一些功能 截图,供参考学习。
双击打开记录的明细,可以看到操作记录的明细显示。
参数配置界面如下所示。
以上显示只是基于权限系统进行日志的记录,当然整个业务系统框架都可以提供上面的记录操作,因为它们所有的数据访问基类都是继承自同一个抽象对象基类的。把这个模块集成在权限系统里面,和登陆日志一样,是供基础性的记录和查阅的,和业务不太相关。
最后附上Winform开发框架的一个功能总结图形,Winform开发框架的主要功能概览如下图所示。