[.NET領域驅動設計實戰系列]專題七:DDD實踐案例:引入事件驅動與中間件機制來實現后台管理功能


一、引言

 在當前的電子商務平台中,用戶下完訂單之后,然后店家會在后台看到客戶下的訂單,然后店家可以對客戶的訂單進行發貨操作。此時客戶會在自己的訂單狀態看到店家已經發貨。從上面的業務邏輯可以看出,當用戶下完訂單之后,店家或管理員可以對客戶訂單進行跟蹤和操作。上一專題我們已經實現創建訂單的功能,則接下來自然就是后台管理功能的實現了。所以在這一專題中將詳細介紹如何在網上書店案例中實現后台管理功能。

二、后台管理中的權限管理的實現

   后台管理中,首先需要實現的自然就是權限管理了,因為要進行商品管理等操作的話,則必須對不同的用戶指定的不同角色,然后為不同角色指定不同的權限。這樣才能確保普通用戶不能進行一些后台操作。

  然而角色和權限的賦予一般都是由系統管理員來操作。所以在最開始創建一個管理員用戶,之后就可以以管理員的賬號進行登錄來進行后台操作的管理,包括添加角色,為用戶分配角色、添加用戶等操作。

  這里就牽涉到一個權限管理的問題了。系統如何針對不同用戶的全新進行管理呢?

  其權限管理一個實現思路其實如下:

  • 不同角色可以看到不同的鏈接,只有指定權限的用戶才可以看到與其對應權限的操作。如只有管理員才可以添加用戶和為用戶賦予權限,而賣家只能對消費者訂單的處理和對自己商店添加商品等操作。

  從上面的描述可以發現,權限管理的實現主要包括兩部分:

  1. 為不同用戶指定不同的鏈接顯示。如管理員可以看到后台管理的所有鏈接:包括角色管理,商品管理,用戶管理、訂單管理,商品分類管理,而賣家只能看到訂單管理,商品管理和商品類別管理等。其實現就是為這些鏈接的生成指定不同的權限,只有達到權限用戶才進行生成該鏈接
  2. 既然要為不同用戶指定不同的權限,則首先要獲得用戶的權限,然后根據用戶的權限來動態生成對應的鏈接。

  有了上面的思路,下面就讓我們一起為網上書店案例加入權限管理的功能:

  首先,我在Layout.cshtml頁面加入指定權限的鏈接,具體的代碼如下所示:

<table width="996" border="0" cellspacing="0" cellpadding="0" align="center">
        <tr>
            <td height="607" valign="top">
                <table width="996" border="0" cellspacing="0" cellpadding="0">
                    <tr>
                        <td width="300" height="55" class="logo"></td>
                        <td width="480" class="menu">
                             <ul class="sf-menu">
                                <li>@Html.ActionLink("首頁", "Index", "Home")</li>
                                @if (User.Identity.IsAuthenticated)
                                {
                                <li>@Html.ActionLink("我的", "Manage", "Account")
                                    <ul>
                                        <li>@Html.ActionLink("訂單", "Orders", "Home")</li>
                                        <li>@Html.ActionLink("賬戶", "Manage", "Account")</li>
                                        <li>@Html.ActionLink("購物車", "ShoppingCart", "Home")</li>
                                    </ul>
                                </li>
                                }
                                @if (User.Identity.IsAuthenticated) { <li>@Html.ActionLinkWithPermission("管理", "Administration", "Admin", PermissionKeys.Administrators | PermissionKeys.Buyers | PermissionKeys.SalesReps) <ul> <li>@Html.ActionLinkWithPermission("銷售訂單管理", "Orders", "Admin", PermissionKeys.Administrators | PermissionKeys.SalesReps)</li> <li>@Html.ActionLinkWithPermission("商品分類管理", "Categories", "Admin", PermissionKeys.Administrators | PermissionKeys.Buyers)</li> <li>@Html.ActionLinkWithPermission("商品信息管理", "Products", "Admin", PermissionKeys.Administrators | PermissionKeys.Buyers)</li> <li>@Html.ActionLinkWithPermission("用戶賬戶管理", "UserAccounts", "Admin", PermissionKeys.Administrators)</li> <li>@Html.ActionLinkWithPermission("用戶角色管理", "Roles", "Admin", PermissionKeys.Administrators)</li> </ul> </li> } <li>@Html.ActionLink("關於", "About", "Home")
                                    <ul>
                                        <li>@Html.ActionLink("Online Store 項目", "About", "Home")</li>
                                        <li>@Html.ActionLink("聯系方式", "Contact", "Home")</li>
                                    </ul>
                                </li>
                            </ul>
                        </td>
                        <td width="216" class="menu">
                            @{Html.RenderAction("_LoginPartial", "Layout");}
                        </td>
                    </tr>
                </table>
                <table width="100%" border="0" cellspacing="0" cellpadding="0">
                    <tr>
                        <td width="100%" height="10px" />
                    </tr>
                </table>
                <table width="996" border="0" cellspacing="0" cellpadding="0">
                    <tr>
                        <td>
                            <img src="/images/header.jpg" alt="" width="996" height="400" border="0"></td>
                    </tr>
                </table>
                <table width="996" border="0" cellspacing="0" cellpadding="0">
                    <tr align="left" valign="top">
                        <td width="202" height="334">
                            @{Html.RenderAction("CategoriesPartial", "Layout");}
                        </td>
                        <td width="20">&nbsp;</td>
                        <td width="774">

                            <table width="774" border="0" cellspacing="0" cellpadding="0">
                                <tr>
                                    @(MvcSiteMap.Instance.Navigator())
                                </tr>
                                <tr>
                                    <td>
                                        @RenderBody()
                                    </td>
                                </tr>

                            </table>
                        </td>
                    </tr>
                </table>
                <table width="996" border="0" cellspacing="0" cellpadding="0">
                    <tr>
                        <td>
                            <img src="/images/footer.jpg" alt="" width="996" height="5"></td>
                    </tr>
                    <tr>
                        <td height="76">
                            <table width="996" border="0" cellspacing="0" cellpadding="0" align="center">
                                <tr>
                                    <td width="329" height="78" align="right"></td>
                                    <td width="14">&nbsp;</td>
                                    <td width="653"><span class="style7">@Html.ActionLink("主頁", "Index", "Home")&nbsp; | &nbsp;&nbsp;@Html.ActionLink("所有分類", "Category", "Home", null, null)&nbsp;&nbsp;|&nbsp;&nbsp; @Html.ActionLink("我的賬戶", "Account", "Account")&nbsp;&nbsp; |&nbsp;&nbsp; @Html.ActionLink("聯系我們", "Contact", "Home")&nbsp;&nbsp; |&nbsp;&nbsp;@Html.ActionLink("關於本站", "About", "Home")</span><br>
                                        版權所有 &copy; 2014-2015, Online Store, 保留所有權利。 </td>
                                </tr>
                            </table>
                        </td>
                    </tr>
                </table>

            </td>
        </tr>
    </table>

  上面紅色加粗部分就是設置不同角色的不同權限。其中ActionLinkWithPermission是一個擴展方法,其具體實現就是獲得登陸用戶的角色,然后把用戶的角色與當前的需要權限進行比較,如果相同,則通過HtmlHelper.GenerateLink方法來生成對應的鏈接。該方法的實現代碼如下所示:

 public static MvcHtmlString ActionLinkWithPermission(this HtmlHelper helper, string linkText, string action, string controller, PermissionKeys required)
        {
            if (helper == null ||
                helper.ViewContext == null ||
                helper.ViewContext.RequestContext == null ||
                helper.ViewContext.RequestContext.HttpContext == null ||
                helper.ViewContext.RequestContext.HttpContext.User == null ||
                helper.ViewContext.RequestContext.HttpContext.User.Identity == null)
                return MvcHtmlString.Empty;

            using (var proxy = new UserServiceClient())
            {
                var role = proxy.GetRoleByUserName(helper.ViewContext.RequestContext.HttpContext.User.Identity.Name);
                if (role == null)
                    return MvcHtmlString.Empty;
                var keyName = role.Name;
                var permissionKey = (PermissionKeys)Enum.Parse(typeof(PermissionKeys), keyName);

                // 通過用戶的角色和對應對應的權限進行與操作
                // 與結果等於用戶角色時,表示用戶角色與所需要的權限一樣,則創建對應權限的鏈接
                return (permissionKey & required) == permissionKey ? 
                    MvcHtmlString.Create(HtmlHelper.GenerateLink(helper.ViewContext.RequestContext, helper.RouteCollection, linkText, null, action, controller, null, null)) 
                    : MvcHtmlString.Empty;
            }
        }

  通過上面的代碼,我們就已經完成了權限管理的實現了。

三、后台管理中商品管理的實現

   如果你是管理員的話,這樣你就可以進入后台頁面對商品、用戶、訂單等進行管理了。在上面我們已經完成了權限管理的實現。接下來,我們可以用一個管理員賬號登陸之后,你可以看到管理員對應的權限。這里我直接在數據庫中添加了一條管理員賬號,其賬號信息是admin,密碼也是admin。下面我就這個賬號后看到的界面如下圖所示:

  從上圖可以看出,后台管理包括銷售訂單管理、商品類別管理、商品信息管理等。這些都是一些類似的實現,都是一些增、刪、改功能的實現。這里就是商品信息管理為例來介紹下。點擊商品信息管理后,將可以看到所有商品列表,在該頁面可以進商品進行添加、修改和刪除等操作。其實現主要是通過應用服務來調用倉儲來實現商品的信息的持久化罷了,下面就具體介紹下商品添加功能的實現。因為商品的添加需要首先把上傳的圖片先添加到服務器上的Images文件夾下,然后通過控制器來調用ProductService的CreateProducts方法來把商品保存到數據庫中。

  首先是圖片上傳功能的實現,其實現代碼如下所示:

 [HandleError]
    public class AdminController : ControllerBase
    {
        #region Common Utility Actions

        // 保存圖片到服務器指定目錄下
        [NonAction]
        private void SaveFile(HttpPostedFileBase postedFile, string filePath, string saveName)
        {
            string phyPath = Request.MapPath("~" + filePath);
            if (!Directory.Exists(phyPath))
            {
                Directory.CreateDirectory(phyPath);
            }
            try
            {
                postedFile.SaveAs(phyPath + saveName);
            }
            catch (Exception e)
            {
                throw new ApplicationException(e.Message);

            }
        }

        // 圖片上傳功能的實現
        [HttpPost]
        public ActionResult Upload(HttpPostedFileBase fileData, string folder)
        {
            var result = string.Empty;
            if (fileData != null)
            {
                string ext = Path.GetExtension(fileData.FileName);
                result = Guid.NewGuid()+ ext;
                SaveFile(fileData, Url.Content("~/Images/Products/"), result);
            }
            return Content(result);
        }
}

  圖片上傳成功之后,接下來點擊保存按鈕則把商品進行持久化到數據庫中。其實現邏輯主要是調用商品倉儲的實現類來完成商品的添加。主要的實現代碼如下所示:

  [HandleError]
    public class AdminController : ControllerBase
    {
        [HttpPost]
        [Authorize]
        public ActionResult AddProduct(ProductDto product)
        {
            using (var proxy = new ProductServiceClient())
            {
                if (string.IsNullOrEmpty(product.ImageUrl))
                {
                    var fileName = Guid.NewGuid() + ".png";
                    System.IO.File.Copy(Server.MapPath("~/Images/Products/ProductImage.png"), Server.MapPath(string.Format("~/Images/Products/{0}", fileName)));
                    product.ImageUrl = fileName;
                }
                var addedProducts = proxy.CreateProducts(new List<ProductDto> { product }.ToArray());
                if (product.Category != null &&
                    product.Category.Id != Guid.Empty.ToString())
                    proxy.CategorizeProduct(new Guid(addedProducts[0].Id), new Guid(product.Category.Id));
                return RedirectToSuccess("添加商品信息成功!", "Products", "Admin");
            }
        }
    }

    // 商品服務的實現
    public class ProductServiceImp : ApplicationService, IProductService
    {
           public List<ProductDto> CreateProducts(List<ProductDto> productsDtos)
        {
            return PerformCreateObjects<List<ProductDto>, ProductDto, Product>(productsDtos, _productRepository);
        }
    }

  到此,我們已經完成了商品添加功能的實現,下面讓我們看看商品添加的具體效果如何。添加商品頁面:

  點擊保存更改按鈕后,則進行商品的添加,添加成功后界面效果:

四、后台管理中發貨操作和確認收貨的實現

   當消費者創建訂單之后,然后賣家或管理員可以通過訂單管理頁面來對訂單進行發貨處理操作。以通知購買者該商品已發貨了。在當前的電子商務網站中,除了更新訂單的狀態外,還會發郵件或短信通知購買者。為了保證這兩個操作同時完成,此時需要將這兩個放在同一個事務中進行提交。

  這里為了使系統有更好地可擴展性,采用了基於消息隊列和事件驅動的方式來完成發貨操作。在看具體實現代碼之前,我們先來分析下實現思路:

  • 賣家或管理員在訂單管理頁面,點擊發貨按鈕后,此時相當於訂單的狀態進行了更新,從已付款狀態到已發貨狀態。這里當然你可以采用傳統的方式來實現,即調用訂單倉儲來更新對應訂單的狀態。但是這樣的實現方式,郵件發送操作可能會嵌套在應用服務層了。這樣的設計顯然不適合擴展。所以這里采用基於事件驅動和消息隊列方式來改進這種方式。
    1. 首先,當商家點擊發貨操作,此時會產生一個發貨事件;
    2. 接着由注冊的領域事件處理程序進行對該領域事件處理,處理邏輯主要是更新訂單的狀態和更新時間;
    3. 然后再將該事件發布到EventBus,EventBus中保存了一個隊列來存放事件,發布操作的實現就是往該隊列插入一個待處理的事件;
    4. 最后在EventBus中的Commit方法中對隊列中的事件進行出隊列操作,通過事件聚合類來獲得對應事件處理器來對出隊列的事件進行處理。
  • 事件聚合器通過Unity注入(應用)事件的處理器。在EventAggregator類中定義_eventHandlers來保存所有(應用)事件的處理器,在EventAggregator的構造函數中通過調用其Register方法把對應的事件處理器添加到_eventHandlers字典中。然后在EventBus中的Commit方法中通過找到EventAggregator中的Handle方法來觸發事件處理器來處理對應事件,即發出郵件通知。這里事件聚合器起到映射的功能,映射應用事件到對應的事件處理器來處理。

  通過上面的分析可以發現,發貨操作和收貨操作都涉及2類事件,一類是領域事件,另一類處於應用事件,領域事件的處理由領域事件處理器來處理,而應用事件的處理不能定義在領域層,所以我們這里新建了一個應用事件處理層,叫OnlineStore.Events.Handlers,已經新建了一個對EventBus支持的層,叫OnlineStore.Events。經過上面的分析,實現發貨操作和收貨操作是不是有點清晰了呢?如果不是的話也沒關系,我們可以結合下面具體的實現代碼再來理解下上面分析的思路。因為收貨操作和發貨操作的實現非常類似,這里只貼出發貨操作實現的主要代碼進行演示。

  首先是AdminController中DispatchOrder操作的實現:

 

 public ActionResult DispatchOrder(string id)
        {
            using (var proxy = new OrderServiceClient())
            {
                proxy.Dispatch(new Guid(id));
                return RedirectToSuccess(string.Format("訂單 {0} 已成功發貨!", id.ToUpper()), "Orders", "Admin");
            }
        }

 

  接下來便是OrderService中Dispatch方法的實現:

public void Dispatch(Guid orderId)
        {
            using (var transactionScope = new TransactionScope())
            {
                var order = _orderRepository.GetByKey(orderId);
                order.Dispatch();
                _orderRepository.Update(order);
                RepositorytContext.Commit();
                _eventBus.Commit();
                transactionScope.Complete();
            }
        }

  下面是Order實體類中Dispatch方法的實現:

 /// <summary>
        /// 處理發貨。
        /// </summary>
        public void Dispatch()
        {
            // 處理領域事件
            DomainEvent.Handle<OrderDispatchedEvent>(new OrderDispatchedEvent(this) { DispatchedDate = DateTime.Now, OrderId = this.Id, UserEmailAddress = this.User.Email });
        }

  接下來便是領域事件中Handle方法的實現了,其實現邏輯就是獲得所有已注冊的領域事件處理器,然后分別事件處理器進行調用。具體的實現代碼如下所示:

 public static void Handle<TDomainEvent>(TDomainEvent domainEvent)
            where TDomainEvent : class, IDomainEvent
        {
            // 找到對應的事件處理器來對事件進行處理
            var handlers = ServiceLocator.Instance.ResolveAll<IDomainEventHandler<TDomainEvent>>();
            foreach (var handler in handlers)
            {
                if (handler.GetType().IsDefined(typeof(HandlesAsynchronouslyAttribute), false))
                    Task.Factory.StartNew(() => handler.Handle(domainEvent));
                else
                    handler.Handle(domainEvent);
            }
        }

  對應OrderDispatchedEventHandler類中Handle方法的實現如下所示:

 // 發貨事件處理器
    public class OrderDispatchedEventHandler : IDomainEventHandler<OrderDispatchedEvent>
    {
        private readonly IEventBus _bus;
       

        public OrderDispatchedEventHandler(IEventBus bus)
        {
            _bus = bus;
        }

        public void Handle(OrderDispatchedEvent @event)
        {
            // 獲得事件源對象
            var order = @event.Source as Order;
            // 更新事件源對象的屬性
            if (order == null) return;

            order.DispatchedDate = @event.DispatchedDate;
            order.Status = OrderStatus.Dispatched;

            // 這里把領域事件認為是一種消息,推送到EventBus中進行進一步處理。
            _bus.Publish<OrderDispatchedEvent>(@event);
        }
    }

  從上面代碼中可以發現,領域事件處理器中只是簡單更新訂單狀態的狀態為Dispatched和更新訂單發貨時間,之后就把該事件繼續發布到EventBus中進一步進行處理。EventBus類的具體實現代碼如下所示:

// 領域事件處理器只是對事件對象的狀態進行更新
    // 后續的事件處理操作交給EventBus進行處理
    // 本案例中EventBus主要處理的任務就是發送郵件通知,
    // 在EventBus一般處理應用事件,而領域事件處理器一般處理領域事件
    public class EventBus : DisposableObject, IEventBus
    {
            public EventBus(IEventAggregator aggregator)
        {
            this._aggregator = aggregator;

            // 獲得EventAggregator中的Handle方法
            _handleMethod = (from m in aggregator.GetType().GetMethods()
                             let parameters = m.GetParameters()
                             let methodName = m.Name
                             where methodName == "Handle" &&
                             parameters != null &&
                             parameters.Length == 1
                             select m).First();
        }
        
             public void Publish<TMessage>(TMessage message)
            where TMessage : class, IEvent
        {
            _messageQueue.Value.Enqueue(message);
            _committed.Value = false;
        }
        
        // 觸發應用事件處理器對事件進行處理
        public void Commit()
        {
            while (_messageQueue.Value.Count > 0)
            {
                var evnt = _messageQueue.Value.Dequeue();
                var evntType = evnt.GetType();
                var method = _handleMethod.MakeGenericMethod(evntType);
                // 調用應用事件處理器來對應用事件進行處理
                method.Invoke(_aggregator, new object[] { evnt });
            }
            _committed.Value = true;
        }
     }

  其EventAggregator類的實現如下所示:

public class EventAggregator : IEventAggregator
    {
        private readonly object _sync = new object();
        private readonly Dictionary<Type, List<object>> _eventHandlers = new Dictionary<Type, List<object>>();
        private readonly MethodInfo _registerEventHandlerMethod;

        public EventAggregator()
        {
            
            // 通過反射獲得EventAggregator的Register方法 
            _registerEventHandlerMethod = (from p in this.GetType().GetMethods()
                                          let methodName = p.Name
                                          let parameters = p.GetParameters()
                                          where methodName == "Register" &&
                                          parameters != null &&
                                          parameters.Length == 1 &&
                                          parameters[0].ParameterType.GetGenericTypeDefinition() == typeof(IEventHandler<>)
                                          select p).First();
        }

        public EventAggregator(object[] handlers)
            : this()
        {
            // 遍歷注冊的EventHandler來把配置文件中具體的EventHanler通過Register添加進_eventHandlers字典中
            foreach (var obj in handlers)
            {
                var type = obj.GetType();
                var implementedInterfaces = type.GetInterfaces();
                foreach (var implementedInterface in implementedInterfaces)
                {
                    if (implementedInterface.IsGenericType &&
                        implementedInterface.GetGenericTypeDefinition() == typeof(IEventHandler<>))
                    {
                        var eventType = implementedInterface.GetGenericArguments().First();
                        var method = _registerEventHandlerMethod.MakeGenericMethod(eventType);
                        // 調用Register方法將EventHandler添加進_eventHandlers字典中
                        method.Invoke(this, new object[] { obj });
                    }
                }
            }
        }

        public void Register<TEvent>(IEventHandler<TEvent> eventHandler)
            where TEvent : class, IEvent
        {
            lock (_sync)
            {
                var eventType = typeof(TEvent);
                if (_eventHandlers.ContainsKey(eventType))
                {
                    var handlers = _eventHandlers[eventType];
                    if (handlers != null)
                    {
                        handlers.Add(eventHandler);
                    }
                    else
                    {
                        handlers = new List<object> {eventHandler};
                    }
                }
                else
                    _eventHandlers.Add(eventType, new List<object> { eventHandler });
            }
        }

        public void Register<TEvent>(IEnumerable<IEventHandler<TEvent>> eventHandlers)
            where TEvent : class, IEvent
        {
            foreach (var eventHandler in eventHandlers)
                Register<TEvent>(eventHandler);
        }

        // 調用具體的EventHanler的Handle方法來對事件進行處理
         public void Handle<TEvent>(TEvent evnt)
            where TEvent : class, IEvent
        {
            if (evnt == null)
                throw new ArgumentNullException("evnt");
            var eventType = evnt.GetType();
            if (_eventHandlers.ContainsKey(eventType) &&
                _eventHandlers[eventType] != null &&
                _eventHandlers[eventType].Count > 0)
            {
                var handlers = _eventHandlers[eventType];
                foreach (var handler in handlers)
                {
                    var eventHandler = handler as IEventHandler<TEvent>;
                    if(eventHandler == null)
                        continue;

                    // 異步處理
                    if (eventHandler.GetType().IsDefined(typeof(HandlesAsynchronouslyAttribute), false))
                    {
                        Task.Factory.StartNew((o) => eventHandler.Handle((TEvent)o), evnt);
                    }
                    else
                    {
                        eventHandler.Handle(evnt);
                    }
                }
            }
        }
View Code

  至於確認收貨操作的實現也是類似,大家可以自行參考Github源碼進行實現。到此,我們商品發貨和確認收貨的功能就實現完成了。此時,我們解決方案已經調整為:

 

  經過本專題后,我們網上書店案例的業務功能都完成的差不多了,后面添加的一些功能都是附加功能,例如分布式緩存的支持、分布式消息隊列的支持以及面向切面編程的支持等功能。既然業務功能都完成的差不多了,下面讓我們具體看看發貨操作的實現效果吧。

  首先是銷售訂單管理首頁,在這里可以看到所有用戶的訂單狀態。具體效果如下圖示所示:

  點擊上圖的發貨按鈕后便可以完成商品發貨操作,此時創建該訂單的用戶郵箱中會收到一份發貨郵件通知,具體實現效果截圖如下所示:

  其確認收貨操作實現的效果與發貨操作的效果差不多,這里就不一一截圖了,大家可以自行到github上下載源碼進行運行查看。

五、總結

   到這里,該專題的介紹的內容就結束。本專題主要介紹后台管理中權限管理的實現、商品管理、類別管理、角色管理、用戶角色管理和訂單管理等功能。正如上面所說的,到此,本網上書店的DDD案例一些業務功能都實現的差不多了,接下來需要完善的功能主要是一些附加功能,這些功能主要是為了提高網站的可擴展性和可伸縮性。這些主要包括緩存的支持、分布式消息隊列的支持以及AOP的支持。在下一個專題將介紹分布式緩存和分布式消息隊列的支持,請大家繼續關注。

  本專題的所有源碼下載:https://github.com/lizhi5753186/OnlineStore_Second/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM