2023年2月

在上篇总结随笔《
Winform开发框架之权限管理系统改进的经验总结(1)-TreeListLookupEdit控件的使用
》介绍了权限管理模块的用户管理部分,其中主要介绍了其中的用户所属公司、所属部门、直属经理(人员列表)的几级数据级联的展示,通过引入TreeListLookupEdit控件,能增强用户的体验效果。本篇继续介绍权限系统模块中的一些闪光点,介绍组织机构管理里面选择用户的界面设计和实现,用户选择在很多场合会用到,如组织机构的用户选择,角色里面的用户选择,或者流程里面的用户选择等用途。

1、选择用户界面效果展示

用户选择在很多地方需要用到,本篇以组织机构里面的用户选择为例,介绍用户选择的界面效果。我们知道,用户一般可以按组织机构进行分类,也可以按照角色进行分类,因此我们需要结合两者进行快速展示用户的层次关系,界面效果如下所示。

在上面的界面分为三个部分:左边主要是机构和角色的展示;右边则是通过列表控件进行展示,并可以进行勾选的操作;底部则是已选用户的列表展示(可以移除)。

2、左边机构树的递归展现

组织机构本身设计就是一个有层次关系的树,因此它可以通过递归函数进行展现,展示方式可以使用传统样式的TreeView控件或者DevExpress样式的TreeList控件,不过我倾向于使用TreeView,觉得这个线状的层次关系更美观一些,递归展示结构树的代码如下所示。

        private voidInitDeptTree()
{
this.treeDept.BeginUpdate();this.treeDept.Nodes.Clear();

TreeNode node
= newTreeNode();
node.Text
= "所有部门";

List
<OUNodeInfo> list = BLLFactory<OU>.Instance.GetTree();
AddDept(list, node);
this.treeDept.Nodes.Add(node);this.treeDept.ExpandAll();this.treeDept.EndUpdate();
}
private void AddDept(List<OUNodeInfo>list, TreeNode treeNode)
{
foreach (OUNodeInfo ouInfo inlist)
{
TreeNode deptNode
= newTreeNode();
deptNode.Text
=ouInfo.Name;
deptNode.Tag
=ouInfo.ID;
deptNode.ImageIndex
=Portal.gc.GetImageIndex(ouInfo.Category);
deptNode.SelectedImageIndex
=Portal.gc.GetImageIndex(ouInfo.Category);
treeNode.Nodes.Add(deptNode);

AddDept(ouInfo.Children, deptNode);
}
}

角色树不是一个递归的关系,因此只需要按列表展示即可,展示代码如下所示。

        private voidInitRoleTree()
{
this.treeRole.BeginUpdate();this.treeRole.Nodes.Clear();

TreeNode node
= newTreeNode();
node.Text
= "所有角色";

List
<RoleInfo> list = BLLFactory<Role>.Instance.GetAll();foreach (RoleInfo info inlist)
{
TreeNode roleNode
= newTreeNode();
roleNode.Text
=info.Name;
roleNode.Tag
=info.ID;
roleNode.ImageIndex
= 5;
roleNode.SelectedImageIndex
= 5;

node.Nodes.Add(roleNode);
}
this.treeRole.Nodes.Add(node);this.treeRole.ExpandAll();this.treeRole.EndUpdate();
}

角色列表大概效果如下所示。

3、右边可勾选列表的实现

右边其实可以通过一般的GridView进行展示,但为了更好的封装和使用,我使用我的Winform分页控件中的WinGridview对象进行展示,这样使用起来更简便。

    public partial classFrmSelectUser : BaseForm
{
publicFrmSelectUser()
{
InitializeComponent();
this.winGridView1.ShowCheckBox = true;this.winGridView1.ShowExportButton = false;this.winGridView1.ShowLineNumber = true;this.winGridView1.BestFitColumnWith = false;//是否设置为自动调整宽度,false为不设置 this.winGridView1.OnRefresh += newEventHandler(winGridView1_OnRefresh);this.winGridView1.gridView1.DataSourceChanged += newEventHandler(gridView1_DataSourceChanged);if (!this.DesignMode)
{
InitDeptTree();
InitRoleTree();
}
}

绑定数据是通过左边的树进行条件检索的,因此可以通过获取组织机构或者角色的节点数据进行查询,我们通过判断组织机构树节点或者角色树节点是否选中来判断即可,具体列表绑定的代码如下所示。

        private voidBindGridData()
{
List
<UserInfo> list = new List<UserInfo>();if (this.treeDept.SelectedNode != null && this.treeDept.SelectedNode.Tag != null)
{
int ouId = this.treeDept.SelectedNode.Tag.ToString().ToInt32();
list
= BLLFactory<User>.Instance.FindByDept(ouId);
}
else if (this.treeRole.SelectedNode != null && this.treeRole.SelectedNode.Tag != null)
{
int roleId = this.treeRole.SelectedNode.Tag.ToString().ToInt32();
list
= BLLFactory<User>.Instance.GetUsersByRole(roleId);
}
//entity this.winGridView1.DisplayColumns = "HandNo,Name,FullName,Title,MobilePhone,OfficePhone,Email,Gender,QQ,Note";this.winGridView1.ColumnNameAlias = BLLFactory<User>.Instance.GetColumnNameAlias();//字段列显示名称转义 this.winGridView1.DataSource = new WHC.Pager.WinControl.SortableBindingList<UserInfo>(list);
}

单用户勾选列表的复选框的时候,该行的数据会被选中,我们最后要获取用户的勾选记录(通过WinGridview控件的GetCheckedRows方法获取),然后获取对应的数据,添加到关联关系的数据库即可,具体代码如下所示。

        private void btnAddUser_Click(objectsender, EventArgs e)
{
List
<int> list = this.winGridView1.GetCheckedRows();foreach(int rowIndex inlist)
{
string ID = this.winGridView1.GridView1.GetRowCellDisplayText(rowIndex, "ID");string Name= this.winGridView1.GridView1.GetRowCellDisplayText(rowIndex, "Name");string FullName = this.winGridView1.GridView1.GetRowCellDisplayText(rowIndex, "FullName");string displayname = string.Format("{0}({1})", FullName, Name);if (!this.SelectUserDict.ContainsKey(ID))
{
this.SelectUserDict.Add(ID, displayname);
}
}

RefreshSelectItems();
}

4、用户选择结果的展示

在一些场景中,我们可能需要在多个组织机构和角色中选择不同的用户,为了更方便展示我们选中的记录,我设计了一个用户控件(一个删除按钮(Button)+标签控件(Lable))组合即可,如下所示。

由于我们选择的内容,无非就是选择它的人员名称即可,如果需要,单击删除按钮,让用户剔除不需要的人员,因此控件增加一个OnDeleteItem事件用来处理这个删除操作。

我们展示多个用户信息的时候,就是通过构造多个这样的控件,并动态增加到Panel里面即可,实现代码如下所示。

        /// <summary>
        ///刷新选择信息/// </summary>
        private voidRefreshSelectItems()
{
this.flowLayoutPanel1.Controls.Clear();foreach (string key inSelectUserDict.Keys)
{
string info =SelectUserDict[key];if (!string.IsNullOrEmpty(info))
{
UserNameControl control
= newUserNameControl();
control.BindData(key, info);
control.OnDeleteItem
+= newUserNameControl.DeleteEventHandler(control_OnDeleteItem);this.flowLayoutPanel1.Controls.Add(control);
}
}
this.lblItemCount.Text = string.Format("当前选择【{0}】项目", SelectUserDict.Keys.Count);
}

5、最终的组织机构管理界面效果

在开篇说了,用户选择在很多场合会用到,如组织机构的用户选择,角色里面的用户选择,或者流程里面的用户选择等用途。

下面是组织机构里面的主体界面。

在右上角的包含用户区域,单击添加按钮,就会出现前面说到的用户选择对话框,如下所示。

在一般的权限系统里面,可能经常会看到系统的黑名单或者白名单的拦截功能。在一般权限系统里面,常见的黑名单就是禁止用户在某些IP上登录系统,白名单就是允许用户只在某些IP上登录系统。本随笔主要介绍在我的权限系统里面,如何实现这个黑白名单的功能,以及介绍在其中应用到的IP对比操作,IP段判断等操作代码。

1、黑白名单的配置

要完成黑名单的拦截和白名单的放行,我们需要进行名单的配置操作,我们把相关的配置放到列表里面进行展示,可以添加多个黑名单或者白名单,如下界面所示。

可以单击新建按钮进行添加一条记录,或者在已有记录上双击黑白名单可以进行编辑,界面如下所示。

2、IP段的录入和对比

在上面的IP输入中,我们需要确认IP段的起始地址小于结束地址,否则我们对比的时候,就可能发生混乱,因此需要在用户输入的时候进行确认,也就是IP地址的大小对比关系。

输入内容的检查代码如下所示。

        /// <summary>
        ///实现控件输入检查的函数/// </summary>
        /// <returns></returns>
        public override boolCheckInput()
{
bool result = true;//默认是可以通过 #region MyRegion if (this.txtName.Text.Trim().Length == 0)
{
MessageDxUtil.ShowTips(
"请输入显示名称");this.txtName.Focus();
result
= false;
}
else if (this.txtAuthorizeType.Text.Length == 0)
{
MessageDxUtil.ShowTips(
"请选择授权类型");this.txtAuthorizeType.Focus();
result
= false;
}
else if (this.txtIPStart.Text.Length == 0)
{
MessageDxUtil.ShowTips(
"请输入IP起始地址");this.txtIPStart.Focus();
result
= false;
}
else if (this.txtIPEnd.Text.Length == 0)
{
MessageDxUtil.ShowTips(
"请输入IP结束地址");this.txtIPEnd.Focus();
result
= false;
}

IPAddress ip1
= IPAddress.Parse(this.txtIPStart.Text);
IPAddress ip2
= IPAddress.Parse(this.txtIPEnd.Text);if (ip1.Compare(ip2) == 1)
{
MessageDxUtil.ShowTips(
"请IP开始地址不能大于结束地址, 请修改");this.txtIPEnd.Focus();
result
= false;
}
#endregion returnresult;
}

上面代码的IP地址的Compare函数,其实是我的扩展方法,注释如下所示。

IP地址的对比操作,其实就是把IP文本转换整形数值及性能对比,Compare扩展方法的定义代码如下所示。

        /// <summary>
        ///IP地址转换为INT类型/// </summary>
        /// <param name="IP">IP地址</param>
        /// <returns></returns>
        public static int ToInteger(thisIPAddress IP)
{
int result = 0;byte[] bytes =IP.GetAddressBytes();
result
= (int)(bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]);returnresult;
}
/// <summary> ///比较两个IP的大小。如果相等返回0,如果IP1大于IP2返回1,如果IP1小于IP2返回-1。/// </summary> /// <param name="IP1">IP地址1</param> /// <param name="IP2">IP地址2</param> /// <returns>如果相等返回0,如果IP1大于IP2返回1,如果IP1小于IP2返回-1。</returns> public static int Compare(thisIPAddress IP1, IPAddress IP2)
{
int ip1 =IP1.ToInteger();int ip2 =IP2.ToInteger();return (((ip1 - ip2) >> 0x1F) | (int)((uint)(-(ip1 - ip2)) >> 0x1F));
}

3、IP段的判断

无论是黑名单还是白名单,我们都要实现对IP段的判断,也就是给定一个IP起始地址和结束地址,构成的IP段,我们要判断用户登陆的IP是否在这个区间里面。

bool ipAccess = BLLFactory<BlackIP>.Instance.ValidateIPAccess(ip, userInfo.ID);if(ipAccess)
{
........................
if (userPassword ==userInfo.Password)
{
......................
//记录用户登录日志 BLLFactory<LoginLog>.Instance.AddLoginLog(userInfo, systemType, ip, macAddr, "用户登录");
}
}
else{
BLLFactory
<LoginLog>.Instance.AddLoginLog(userInfo, systemType, ip, macAddr, "用户登录操作被黑白名单禁止登陆!");
}

在ValidateIPAccess函数里面,除了白名单优先于黑名单的逻辑外,主要的逻辑判断就是判断指定的IP是否落在IP段里面,这个逻辑可以通过下面的方法进行判断实现。

        ///检测指定的IP地址是否在两个IP段中/// </summary>
        /// <param name="ip">指定的IP地址</param>
        /// <param name="begip">起始ip</param>
        /// <param name="endip">结束ip</param>
        /// <returns></returns>
        public static bool IsInIp(string ip, string begip, stringendip)
{
int[] inip, begipint, endipint = new int[4];
inip
=GetIp(ip);
begipint
=GetIp(begip);
endipint
=GetIp(endip);for (int i = 0; i < 4; i++)
{
if (inip[i] < begipint[i] || inip[i] >endipint[i])
{
return false;
}
else if (inip[i] > begipint[i] || inip[i] <endipint[i])
{
return true;
}
}
return true;
}

4、系统登录拦截效果

系统拦截IP登录后,会记录一条日志到登录日志里面,如下所示。

在前面介绍了几篇关于我的权限系统改进的一些经验总结,本篇继续这一系列主体,介绍如何一行代码实现重要表的操作日志记录。我们知道,在很多业务系统里面,数据是很敏感的,特别对于一些增加、修改、删除等关键的操作,如果能在框架层面的支持基础上,以最少的代码实现重要表的日志记录,那么是一件非常值得庆贺的事情,也能够为我们客户的数据提供重要的日志跟踪,甚至是数据恢复的参考。

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);returnUpdate(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 classBaseEntity
{
private stringm_CurrentLoginUserId;/// <summary> ///当前登录用户ID。该字段不保存到数据表中,只用于记录用户的操作日志。/// </summary> [DataMember]public stringCurrentLoginUserId
{
get { returnm_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= newStringBuilder();foreach (string field inrecordField.Keys)
{
string newValue =recordField[field].ToString();string oldValue =dbrecordField[field].ToString();if (newValue != oldValue)//只记录变化的内容 {string columnAlias = "";bool result = dictColumnNameAlias.TryGetValue(field, outcolumnAlias);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地址信息等,这些信息一旦用户登陆到系统就会发生了,所以可以方便获取到,然后就是把这些信息作为一个数据库记录写入数据库表即可。

OperationLogInfoinfo = newOperationLogInfo();
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>{privateIOU 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 asIOU;
}

这样数据访问类baseDal一旦事件初始化,那么就会在底层进行触发,然后交给事件的处理逻辑(上层操作)进行处理了。

为了更好控制用户的增加、修改、删除的相关事件,我们可以通过一个配置表进行登记处理,然后根据配置表的参数来决定记录那些信息,这些就是细化的问题了。

4、我的Winform开发框架的权限系统模块里面对于操作日志的支持

在我的Winform开发框架里面,权限系统是其中的一个基础部分,因此也根据上面的逻辑实现了对操作日志的参数配置和记录显示,方便对业务系统所有表的操作记录进行跟踪和处理。

通过一行代码就能实现业务表的日志记录,对我们开发新的业务模块,效率可以提高很多,同时也能给客户提供更好的数据支持服务。通过在权限系统模块里面配置参数和显示操作日志记录,能够给业务开发提供基础性的开发框架支持。

下面是我的Winform开发框架的权限系统模块的一些功能 截图,供参考学习。

双击打开记录的明细,可以看到操作记录的明细显示。

参数配置界面如下所示。

以上显示只是基于权限系统进行日志的记录,当然整个业务系统框架都可以提供上面的记录操作,因为它们所有的数据访问基类都是继承自同一个抽象对象基类的。把这个模块集成在权限系统里面,和登陆日志一样,是供基础性的记录和查阅的,和业务不太相关。

最后附上Winform开发框架的一个功能总结图形,Winform开发框架的主要功能概览如下图所示。

在实际的系统应用环境中,用户的分级管理一般也是比较常见的功能,小的业务系统可以不需要,但是一般涉及到集团、分子公司、或者是事业单位里面的各个处室或者某某局的人员管理,这些分级管理就显得比较必要,否则单靠管理员来处理账号的事情,是比较麻烦一点的。分级管理就是让不同层次、不同机构的人员实现一定的自治管理,如分公司的人员有专门的管理员,各地区的处室或者某某局实现内部人员的创建、调整、角色分配等工作。本篇随笔主要介绍在我的权限系统中如何实现人员的分级管理的,给大家提供相应的思路和样例参考。

1、用户分级管理的处理逻辑

我们可以把管理员用户分为两级,超级管理员和公司管理员,超级管理员可以管理系统所有用户和全局配置,公司管理员只能管理自己所在公司的人员信息及相关数据。这样我们就比较清晰具体的用户角色负责哪些信息,以便进行有效的过滤和控制了。

为了实现用户的角色控制,我们需要在用户登陆后,获取用户的信息和角色列表信息(一个用户有多个角色的),把它们放置到系统的全局变量里面。

        /// <summary>
        ///登录用户信息/// </summary>
        public UserInfo UserInfo { get; set; }/// <summary>
        ///用户具有的角色集合/// </summary>
        public List<RoleInfo> RoleList { get; set; }

在用户登陆成功后,我们就可以把用户信息和角色列表信息赋值给上面的变量,从而可以在本地实现更好的控制管理。通过UserIsAdmin的函数管理,可以只允许管理员级别(超级管理员和公司管理)的用户进行权限系统的登陆。

            try{string ip =NetworkUtil.GetLocalIP();string macAddr =HardwareInfoHelper.GetMacAddress();string loginName = this.txtUserName.Text.Trim();string identity = BLLFactory<User>.Instance.VerifyUser(loginName, this.txtPassword.Text, "Security", ip, macAddr);if (!string.IsNullOrEmpty(identity))
{
if (BLLFactory<User>.Instance.UserIsAdmin(loginName))
{
UserInfo info
= BLLFactory<WHC.Security.BLL.User>.Instance.GetUserByName(loginName);
Portal.gc.UserInfo
= info; //赋值给全局变量“管理用户” Portal.gc.RoleList = BLLFactory<WHC.Security.BLL.Role>.Instance.GetRolesByUser(info.ID);//用户的角色集合 bLogin= true;this.DialogResult =DialogResult.OK;
}
else{
MessageDxUtil.ShowWarning(
"该用户没有管理员权限");return;
}
}
else{
MessageDxUtil.ShowWarning(
"用户名或密码错误或被禁止登陆");return;
}
}
catch(Exception err)
{
MessageDxUtil.ShowError(err.Message);
}

然后需要判断用户的角色是否符合要求,创建一个判断函数即可。

        /// <summary>
        ///判断当前用户具有某个角色/// </summary>
        /// <param name="roleName">角色名称</param>
        /// <returns></returns>
        public bool UserInRole(stringroleName)
{
bool result = false;if (RoleList != null)
{
foreach (RoleInfo info inRoleList)
{
if(info.Name.Equals(roleName, StringComparison.OrdinalIgnoreCase))
{
result
= true;break;
}
}
}
returnresult;
}

我们知道,不同角色的用户管理的级别数据不同,很大程度上是他们负责的树节点层次不同,因此对于组织结构,我们可以根据用户不同的角色进行构造。

超级管理员,组织机构从顶级节点开始递归显示。

公司管理员,组织机构从所在公司节点开始递归显示。

具体的逻辑代码就是如下所示。

        /// <summary>
        ///根据当前用户身份,获取对应的顶级机构管理节点。///如果是超级管理员,返回集团节点;如果是公司管理员,返回其公司节点/// </summary>
        /// <returns></returns>
        publicOUInfo GetMyTopGroup()
{
OUInfo groupInfo
= null;if(UserInRole(RoleInfo.SuperAdminName))
{
groupInfo
= BLLFactory<OU>.Instance.GetTopGroup();//超级管理员取集团节点 }else{
groupInfo
= BLLFactory<OU>.Instance.FindByID(UserInfo.Company_ID);//公司管理员取公司节点 }returngroupInfo;
}

例如人员的组织机构分类,就可以通过下面的代码进行不同的限定

        /// <summary>
        ///初始化组织机构列表/// </summary>
        private voidInitDeptTreeview()
{
this.treeDept.BeginUpdate();this.treeDept.Nodes.Clear();

OUInfo groupInfo
=Portal.gc.GetMyTopGroup();if (groupInfo != null)
{
TreeNode topnode
= newTreeNode();
topnode.Text
=groupInfo.Name;
topnode.Name
=groupInfo.ID.ToString();
topnode.ImageIndex
=Portal.gc.GetImageIndex(groupInfo.Category);
topnode.SelectedImageIndex
=Portal.gc.GetImageIndex(groupInfo.Category);
topnode.Tag
= string.Format("Company_ID='{0}'", groupInfo.ID);this.treeDept.Nodes.Add(topnode);

List
<OUNodeInfo> list = BLLFactory<OU>.Instance.GetTreeByID(groupInfo.ID);
AddOUNode(list, topnode);
}
this.treeDept.ExpandAll();this.treeDept.EndUpdate();
}

2、用户分级管理的界面显示

用户分级,前面说到,分为了超级管理员和公司管理员,超级管理员用来管理整个系统的相关用户以及全局的设置;公司管理员用来管理分公司的人员信息。

我们使用超级管理员登陆权限管理系统,可以看到权限系统的全部功能,超级管理员的界面效果如下所示。

公司管理员登陆权限系统后,主界面效果如下所示。其中我们可以看到,对于功能、菜单的定义模块,公司管理员没有权限进行配置,而且用户仅仅能够管理属于自己公司的人员,如下所示 。

角色管理也是只能管理所属公司的角色信息,如下所示。

其他模块,也是只能根据自己公司范围进行查找,如操作日志,登陆日志等信息。

在开发Winform程序的时候,我们往往需要根据需要做一些自定义的控件模块,这样可以给系统模块重复利用,或者实现更好的效果等功能。但在使用的时候,我们又往往设计时刻发现一些莫名其妙的错误,那么我们该如何进行控件的设计时刻的开发调试呢,如何解决碰到设计时刻出现的错误呢?本文主要介绍我自己在这方面积累的一些经验和处理方法,期望对大家有帮助。

1、自定义控件的设计错误产生

例如我的通用附件模块里面,有一个自定义控件,需要提供给外部使用的,如下所示。

这里外部使用的模块,是工作流里面的一个模块,也是一个自定义控件,我想把它作为一个流程信息的展示控件。

因此就想把这个附件管理的自定义控件拖动到另外一个自定义控件流程信息展示控件里面,设计的流程信息管理的界面如下所示,这个时候,使用这个附近控件是没有问题的,正常拖动到另外一个控件里面。

编译整个项目,左边的VS工具箱会出现一个ApplyControl的自定义控件,
但是当我拖动该控件到新的窗体界面里面的时候,错误就出现了。

这个问题可能是因为调用了访问数据库的操作,但是更加详细的位置我们看不太清楚(数据给截断显示了)。

2、基于Visual Studio控件的调试

为了更好跟踪到错误的发生的地方,我们可以用VS自带的调试操作来进行跟踪。

首先我们在项目的【属性】-》【调试】里面设置启动操作为指定的VS,选择“启动外部程序”为对应版本的VS的IDE程序,如下所示。

启动调试后,打开对应的这个项目,然后再次模拟从工具箱里面拖动控件的效果,这样VS IDE就能定位到具体的位置了。

我们发现VS定位到一个绑定数据的数据库访问操作里面去,但是我开始一直不明白,这个BindData的操作,其实已经是通过指定了设计时刻不进行的了(
!this
.DesignMode
),不知道为什么还继续。

        public voidBindData()
{
ClearData();
if (!this.DesignMode)
{
List
<FileUploadInfo> fileList = new List<FileUploadInfo>();if (!string.IsNullOrEmpty(this.AttachmentGUID))
{
fileList
= BLLFactory<FileUpload>.Instance.GetByAttachGUID(this.AttachmentGUID, this.pager1.PagerInfo);
}
else{
fileList
= BLLFactory<FileUpload>.Instance.GetAllByUser(this.UserId, this.AttachmentDirectory, this.pager1.PagerInfo);
}

..........................

3、DesignMode的重载,问题解决

调试到这个DesignMode的时候,它的值竟然是false,那么肯定就会去从数据库获取了,而设计时候去找数据,这个时候就出错了。至于为什么会是DesignMode为false,开始有点搞不太清楚,不是说好设计时刻为True的吗?

通过搜索,发现有为仁兄总结的比较精辟,这里就借用一下。

“也就是说一个控件只有在它自己被拖拽到设计器的时候,其 DesignMode 才是真,如果它被包含在其他控件中被加入到设计器,那么那个控件才是在设计模式,而它不是!换句话说,DesignMode 并不能反映当前环境是否是运行时,它只能告诉你,这个控件当前是不是直接被设计器操作(嵌套的已经不算了) 。”

那解决方法应该如何呢,其实也很简单,就是重写下这个DesignMode的属性为我们期望的值即可,如下所示。

/// <summary>
///标题:获取一个值,用以指示 System.ComponentModel.Component 当前是否处于设计模式。///描述:DesignMode 在 Visual Studio 产品中存在 Bug ,使用下面的方式可以解决这个问题。/// </summary>
protected new boolDesignMode
{
get{bool returnFlag = false;#if DEBUG if (System.ComponentModel.LicenseManager.UsageMode ==System.ComponentModel.LicenseUsageMode.Designtime)
{
returnFlag
= true;
}
else if (System.Diagnostics.Process.GetCurrentProcess().ProcessName.ToUpper().Equals("DEVENV"))
{
returnFlag
= true;
}
#endif returnreturnFlag;
}
}

重新编译控件,然后测试拖动,操作正常,再无出错,搞定!