这次做图书馆维护系统,首先要解决的问题就是角色权限动态分配,权限分配直接体现就是菜单的动态分配。在此和大家分享一下心得。
大多数系统,都有多种类型的用户,不同的用户权限不同,某一个功能,A类用户是可见的,但是B类用户没有必要或者不应该看见这个功能,这就要涉及到功能的动态分配。要解决这个问题,当然要从数据下手,在学姐的指导下,有了如下的UML设计图:
解释一下:
MemberType表是用户类型表。
SystemFunction表是系统所有功能表,记录了功能的名称和对应的页面URL,思想是一个功能即一个页面。
Tab表是菜单表,也就是顶级菜单,SystemFunction表中的功能将被归类到这个菜单中。
MemberFunction表是用户功能表,这个表负责连接MemberType表和Tab表,通过这个表可以得知何种用户有哪些菜单。
TabFunction是菜单功能表,负责连接Tab表和SystemFunction表,通过这个表可以得知何种菜单有哪些功能。
这种设计遵守了三范式设计原则,使用起来非常方便。假如我们要给某种类型的用户增加一种菜单(增加一种权限),只需要在MemberFunction表中建立一个连接即可:添加一条记录,字段值分别是该类型用户的id和对应菜单的ID。给某个菜单添加某个功能也是如此。这样一来,管理起来非常方便,只需要添加或删除MemberFunction表和TabFunction表中的记录,就可以达到灵活分配用户拥有的菜单、灵活分配菜单中的功能。
结合ASP.NET,我们还需要把这种数据库表示转换成界面表示。在D层,必须借助于下边两个存储过程:
GO /*-----------------------------用户身份(类型)对应顶级菜单表存储过程-------------------------------*/ /*选取某种用户顶级菜单*/ CREATE PROCEDURE proc_MemberFunction_SelectByTypeID @memberTypeID bigint AS BEGIN select t_MemberFunction.*,t_Tab.[name] from t_MemberFunction join t_Tab on t_Tab.id=t_MemberFunction.tabID where memberTypeID=@memberTypeID END
GO /*-----------------------------顶级菜单(选项卡)功能表存储过程-------------------------------*/ /*选取某种顶级菜单的下属功能*/ CREATE PROCEDURE proc_TabFunction_SelectTabFunction @tabID bigint AS BEGIN select t_TabFunction.*,t_SystemFunction.[name],t_SystemFunction.pageURL from t_TabFunction join t_SystemFunction on t_SystemFunction.id=t_TabFunction.systemFunctionID where tabID=@tabID END
有了这两个存储过程,就可以读出所有的菜单数据。接下来就要在界面上显示,一般情况下,界面上的菜单都是用ul和li标签,然后用javascript加以控制,在这一级菜单就可以满足我们的需求,类似下边这个结构:
<ul> <li class="menu"> <a href="#">个人管理</a> <ul> <li><a href='#'>查看信息</a></li> </ul> <ul> <li><a href='#'>修改密码</a></li> </ul> </li> </ul>
不难看出,个人管理的位置就是顶级菜单,查看信息、修改密码的位置是具体功能,很明显的一个嵌套结构(把上边的代码保存成html文件,打开看看就知道是啥样的结构了)。在界面上绑定数据,轻量级的repeater控件是非常不错的选择,具体怎么用就不赘述了。要用repeater控件显示出上边提到的结构,就必须进行repeater控件的嵌套。那么如何在ASP.NET中嵌套repeater控件呢?注:以下代码都是针对于本文的数据库,如果您想用,要改一改,起码要改改读取的字段。。。
aspx前台文件代码:
<ul> <!--读取顶级菜单--> <asp:Repeater ID="menuRepeater" runat="server" onitemdatabound="menuRepeater_ItemDataBound"> <ItemTemplate> <li class="menu"> <a href="#"><%# Eval("name") %></a> <ul> <!--读取二级菜单--> <asp:Repeater ID="functionRepeater" runat="server"> <ItemTemplate> <li><a href="#" onclick='javascript:changeSrc("<%# setSession(Eval("pageURL").ToString) % >");'><%# Eval("name") %></a></li> </ItemTemplate> </asp:Repeater> </ul> </li> </ItemTemplate> </asp:Repeater> </ul>
aspx.cs后台文件代码:
//外层repeater数据绑定 DataTable dt = new DataTable(); dt = menumanager.getMemberFunction(Convert.ToInt64(Request.QueryString["memberTypeID"].ToString())); menuRepeater.DataSource = dt; menuRepeater.DataBind(); //内层repeater数据绑定 protected void menuRepeater_ItemDataBound(object sender, System.Web.UI.WebControls.RepeaterItemEventArgs e) { DataTable dt = new DataTable(); Repeater functionRepeater = (Repeater)e.Item.FindControl("functionRepeater"); //找到内层的repeater控件 DataRowView rowv = (DataRowView)e.Item.DataItem; dt = menumanager.getTabFunction(Convert.ToInt64(rowv["tabID"])); //读取上一层repeater控件中保存的菜单id,并且根据该id去读取菜单下的功能 //绑定数据 functionRepeater.DataSource = dt; functionRepeater.DataBind(); }
repeater嵌套就是这么简单,需要注意的是,在外层repeater上注册的是onitemdatabound事件,也就是itemtemplate模版数据绑定事件,千万不要理解成是repeater的绑定事件。然后在用onitemdatabound注册的menuRepeater_ItemDataBound事件中,去绑定内层repeater控件的数据就可以了。
细心的读者可能会发现在aspx前台代码中调用了一个setSession函数,这个函数就是就是分配权限用的。函数内容:
public string setSession(string pageName) { Session["PagePermissions"] = Session["PagePermissions"].ToString() + "|" + pageName.Split(new char[1]{'.'})[0]; return pageName; }
这么简单的一个小函数,是如何做到分配权限的呢?地球人都知道,即使我们没有给X类型的用户显示某个功能页面,但这个页面是确确实实存在的,只是没有让X看到而已,假如X用户手动访问这个页面,如果显示出来了,不就乱了吗?通过这个函数我们可以获取所有的页面名称,把他们拼接成一个字符串,保存到session中,然后在每个页面的pageLoad事件中都检查这个session,看看这个session中有没有自己的名称,如果没有,就跳转到错误页面,如果有,就显示。这样一来,菜单分配和权限分配就一块搞定了,方便简洁!
至此,一个ASP.NET根据角色动态分配菜单+权限的例子就讲完了,但是做完这个工程之后我发现这样还不是很好,经过仔细的分析,这样的数据库设计可以用下边这张图表示:
可以看出,假如我们要增加一级菜单,就要额外增加两个表:一个菜单表一个连接表。这在实际应用中并不合理。用过wordpress的朋友都知道,它的菜单可以通过拖拉的方式进行排布,假如是上边这种结构,要来来回回的去删表、建表,这几乎是不敢想象的工作量。经过思考,无论是几级菜单,都放到一个表中(抽象成一个表),然后把所有的连接表也抽象成为一个表,有了如下结构:
其中表内的结构如下(表名:字段名1,字段名2…):
系统菜单(t_SystemMenu):id,level,menuName
菜单衔接(t_MenuLink):id,menuID,belongToID
数据库中读取菜单语句:
遍历分级(确定共有多少级):
SELECT * FROM t_SystemMenu WHERE level=@level
选出下属菜单(选出每一级下属的菜单或功能):
SELECT * FROM t_MenuLink WHERE belongToID=@id
这样设计在数据库读取方面没有问题,但是界面显示就比较困难了,因为我们无法确定repeater控件的数量,有兴趣的可以google搜“动态创建repeater控件”,由于这种技术与平台有很大关系,在此我只抛砖引玉,具体的就留给读者思考了。
PS:这种变态的设计很可能不符合数据库设计三范式,具体情况具体分析吧,有时候没必要迷信于什么范式!关系型数据库有时候还不好使呢!