這次做圖書館維護系統,首先要解決的問題就是角色許可權動態分配,許可權分配直接體現就是菜單的動態分配。在此和大家分享一下心得。
大多數系統,都有多種類型的使用者,不同的使用者權限不同,某一個功能,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 bigintASBEGINselect t_MemberFunction.*,t_Tab.[name] from t_MemberFunction join t_Tab on t_Tab.id=t_MemberFunction.tabIDwhere memberTypeID=@memberTypeIDEND
GO/*-----------------------------頂級菜單(選項卡)功能表格儲存體過程-------------------------------*//*選取某種頂級菜單的下屬功能*/CREATE PROCEDURE proc_TabFunction_SelectTabFunction@tabID bigintASBEGINselect t_TabFunction.*,t_SystemFunction.[name],t_SystemFunction.pageURL from t_TabFunctionjoin t_SystemFunction on t_SystemFunction.id=t_TabFunction.systemFunctionIDwheretabID=@tabIDEND
有了這兩個預存程序,就可以讀出所有的菜單資料。接下來就要在介面上顯示,一般情況下,介面上的菜單都是用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:這種變態的設計很可能不符合資料庫設計三範式,具體情況具體分析吧,有時候沒必要迷信於什麼範式!關係型資料庫有時候還不好使呢!