領域驅動設計實戰—基於DDDLite的權限管理OpenAuth.net

        在園子裏面,搜索一下“權限管理”至少能得到上千條的有效記錄。記得剛開始工作的時候,寫個通用的權限系統一直是自己的一個夢想。中間因爲工作忙(其實就是懶!)等原因,被無限期擱置了。最近想想,自己寫東西時,很多都是偏理論方面的,常常找不到合適的例子來論證自己的觀點。於是用業餘時間來寫點東西。

園子中的權限管理系統有以下幾種:

  1. 寫的好的,界面NB的,但不開源,畢竟人家辛辛苦苦的勞動成果;
  2. 寫的好的,也公開源碼,但不公開數據庫設計和一些流程設計,你得看着源碼去猜字段去猜流程;
  3. 不定期講源碼和放截圖的,丫的就是不放出項目的,這種同1,就是沒事換個馬甲來水點廣告;
  4. 入門級的,開放源碼的,但那源碼實在是不想多看兩眼;

什麼也不說了,開幹!文字太多了,來個動態圖緩一緩:

screen

需求

        首先,做個東西必須要把需求搞清楚。園子裏面的權限管理需求分析的比較合理的,應該是蕭秦的我的權限系統設計實現MVC4 + WebAPI + EasyUI + Knockout(一) ,具體總結如下:

1、權限資源
    a.菜單權限  經理和業務員登陸系統擁有的功能菜單是不一樣的
    b.按鈕權限  經理能夠審批,而業務員不可以
    c.數據權限  A業務員看不到B業務員的單據
    d.字段權限  某些人查詢客戶信息時看不到客戶的手機號或其它字段

2、用戶,應用系統的具體操作者,我這裏設計用戶是不能直接分配權限的,必須要分配一個角色,角色中再分配權限,如果某個用戶權限比較特殊,可以爲他專門建一個角色來應用解決,因爲如果用戶也可以分配權限系統就會複雜很多。【我採用的還是可以直接給用戶分配菜單/按鈕,畢竟我們的人員就喜歡搞些特殊待遇】

3、角色,爲了對許多擁有相似權限的用戶進行分類管理,定義了角色的概念,以上所有的權限資源都可以分配給角色,角色和用戶N:N的關係。

4、機構,樹形的公司部門結構,國內公司用的比較多,它實際上就是一個用戶組,機構和用戶設計成N:N的關係,也就是說有時候一個用戶可以從屬於兩個部門,這種情況在我們客戶需求中的確都出現過。

設計

        本來想用DDD(也就是把CQRS/AES等一堆的東西全用上,如果你想學習完整的DDD框架,可以參考我的另一個項目BestQ&A --開源中國推薦項目/集CQRS AES等DDD高級特性於一體的問答系統)實現這個項目,思考再三還是被自己否定了。畢竟自己也在學習真正的領域驅動設計,思想上不是很成熟。再者,我相信對於普通的經典DDD架構(好高大上的說,悄悄地告訴你其實就是分層分的格調不一樣!),我是有絕對的信心可以把控的。

與其他權限管理相同的地方

        使用了萬惡的EF+MVC結構,當然,我沒惡俗到用EasyUI,爲了體現個性,選擇了酷炫的基於bootstrap的B-JUI前端(炫不炫,你說了算)。相同的東西總是無趣,你可以無視,請把注意力放在下面。

與其他權限管理不同的地方

1、項目採用經典DDD架構(用沃恩.弗農大神的話,其實這是DDD-Lite)思想進行開發,簡潔而不簡單,實用至上,並且所寫每一行代碼都經過深思熟慮,採用Autofac對項目進行解耦,符合S.O.L.I.D規則!來秀一下內在美:

using OpenAuth.Domain;
using OpenAuth.Domain.Interface;
using System;
using System.Collections.Generic;
using System.Linq;

namespace OpenAuth.App
{
    public class OrgManagerApp
    {
        private IOrgRepository _repository;

        public OrgManagerApp(IOrgRepository repository)
        {
            _repository = repository;
        }

        public IList<Org> GetAll()
        {
            return _repository.LoadOrgs().ToList();
        }

        /// <summary>
        /// 部門的直接子部門
        /// <para>TODO:可以根據用戶的喜好決定選擇LoadAllChildren或LoadDirectChildren</para>
        /// </summary>
        public IList<Org> LoadDirectChildren(int orgId)
        {
            return _repository.Find(u => u.ParentId == orgId).ToList();
        }

        /// <summary>
        /// 得到部門的所有子部門
        /// <para>如果orgId爲0,表示取得所有部門</para>
        /// </summary>
        public IList<Org> LoadAllChildren(int orgId)
        {
            string cascadeId = "0.";
            if (orgId != 0)
            {
                var org = _repository.FindSingle(u => u.Id == orgId);
                if (org == null)
                    throw new Exception("未能找到指定對象信息");
                cascadeId = org.CascadeId;
            }

            return _repository.Find(u => u.CascadeId.Contains(cascadeId) && u.Id != orgId).ToList();
        }

        /// <summary>
        /// 添加部門
        /// </summary>
        public int AddOrUpdate(Org org)
        {
            if (org.Id == 0)
            {
                ChangeModuleCascade(org);
                _repository.Add(org);
            }
            else
            {
                _repository.Update(org);
            }

            return org.Id;
        }

        /// <summary>
        /// 刪除指定ID的部門及其所有子部門
        /// </summary>
        public void DelOrg(int id)
        {
            var delOrg = _repository.FindSingle(u => u.Id == id);
            if (delOrg == null) return;

            _repository.Delete(u => u.CascadeId.Contains(delOrg.CascadeId));
        }

        #region 私有方法

        //修改對象的級聯ID,生成類似XXX.XXX.X.XX
        private void ChangeModuleCascade(Org org)
        {
            string cascadeId;
            int currentCascadeId = 1;  //當前結點的級聯節點最後一位
            var sameLevels = _repository.Find(o => o.ParentId == org.ParentId && o.Id != org.Id);
            foreach (var obj in sameLevels)
            {
                int objCascadeId = int.Parse(obj.CascadeId.Split('.').Last());
                if (currentCascadeId <= objCascadeId) currentCascadeId = objCascadeId + 1;
            }

            if (org.ParentId != 0)
            {
                var parentOrg = _repository.FindSingle(o => o.Id == org.ParentId);
                if (parentOrg != null)
                {
                    cascadeId = parentOrg.CascadeId + "." + currentCascadeId;
                    org.ParentName = parentOrg.Name;
                }
                else
                {
                    throw new Exception("未能找到該組織的父節點信息");
                }
            }
            else
            {
                cascadeId = "0." + currentCascadeId;
                org.ParentName = "根節點";
            }

            org.CascadeId = cascadeId;
        }

        #endregion 私有方法
    }
}

 

2、教科書級的分層思想,哪怕苛刻的你閱讀的是大神級經典大作(如:《企業應用架構模式》《重構與模式》《ASP.NET設計模式》等),你也可以參考本項目。不信?有圖爲證,Resharper自動生成的項目引用關係,毫無PS痕跡!

architect

記得以前弦子哥寫過一篇園子裏面搭構架對比的文章(.Net項目分層與文件夾結構大全(最佳架子獎,吐槽獎,陰溝翻船獎揭曉)),本想也建他10幾個項目,想一想還是算了,折騰讀的人也折騰我自己。畢竟我的項目還沒有分佈式的需求,就算有,也得遵循分佈式設計的最高準則------能不用分佈式就不要用!

所以精簡到6個項目,個個都是精華!

所有項目都依賴於領域層,而領域層不關心任何數據庫實現或界面UI實現;

通過依賴注入真正實現了上層與數據庫分離,雖然數據庫訪問採用了EF的方式,但WEB層對此毫不知情!

3、經過N次優化的數據庫結構設計。本來數據庫核心表中有很多多對多的關係(用戶與機構/用戶與角色/角色與模塊等等),如下:

pdm1

代碼寫到一半的時候,覺得何苦呢,爲什麼以前設計權限的人都喜歡這麼設計?去你的,看我的:

pdm2

瞬間少了很多,代碼風格也可以統一起來,多美好的事情啊。你會問:所有多對多關係放在一張表,性能怎麼辦?什麼?性能?沒有千萬級數據,別和我提性能。如果你的系統幾十萬數據時都會很卡,還是去惡補一下數據庫基礎吧!

界面

人要臉樹要皮,沒圖沒真相!

login

add

 

user

Module

 

源碼及說明

 

項目地址:https://git.oschina.net/yubaolee/OpenAuth.Net

 

源碼中包含所有的程序代碼,數據庫PowerDesigner設計圖,CodeSmith生成模板,數據庫初始腳本。請下載源碼後,先用Nuget還原引用的第三方包,再修改一下web.config裏面的連接字符串。

當前代碼已經實現核心功能如下:

  • 模塊/用戶/部門/角色的分級管理;
  • 爲用戶分配角色或直接爲用戶分配模塊;
  • 根據模塊URL地址與MVC的Controller適配授權;
  • 頁面菜單按鈕分配;
  • 內部已經集成log4net,只需要簡單的 LogHelper.Log("日誌內容") 即可;

最近開發功能展望:

  • 菜單授權處理;
  • 數據權限處理;
  • 用戶分級授權功能;

 

短短一文怎麼能表達完一個項目的功能與思想!在後續的博客中,我會結合自己對企業開發的一些看法,對DDD的一些看法,對重構的一些看法,等等等等,來講述自己的程序人生。

 

本博客其他精彩內容:http://www.cnblogs.com/yubaolee/p/Catalogue.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章