如何一步一步用DDD設計一個電商網站(十一)—— 最后的准備


 

本系列所有文章

如何一步一步用DDD設計一個電商網站(一)—— 先理解核心概念

如何一步一步用DDD設計一個電商網站(二)—— 項目架構

如何一步一步用DDD設計一個電商網站(三)—— 初涉核心域

如何一步一步用DDD設計一個電商網站(四)—— 把商品賣給用戶

如何一步一步用DDD設計一個電商網站(五)—— 停下腳步,重新出發

如何一步一步用DDD設計一個電商網站(六)—— 給購物車加點料,集成售價上下文

如何一步一步用DDD設計一個電商網站(七)—— 實現售價上下文

如何一步一步用DDD設計一個電商網站(八)—— 會員價的集成

如何一步一步用DDD設計一個電商網站(九)—— 小心陷入值對象持久化的坑

如何一步一步用DDD設計一個電商網站(十)—— 一個完整的購物車

如何一步一步用DDD設計一個電商網站(十一)—— 最后的准備

如何一步一步用DDD設計一個電商網站(十二)—— 提交並生成訂單

如何一步一步用DDD設計一個電商網站(十三)—— 領域事件擴展

 

 

閱讀目錄

 

一、前言

  最近實在太忙,上周停更了一周。按流程一步一步走到現在,到達了整個下單流程的最后一公里——結算頁的處理。從整個流程來看,這里需要用戶填寫的信息是最多的,那么在后端的設計中如何考慮到業務邊界的划分,和相互之間的交互復雜度,又是我們需要考慮的地方。總體來說本篇講述的內容在前幾篇都有涉及,所以這次一次性處理的業務比較多,已經比較熟練的看官可以跳過本篇。

 

二、准備

  主流的電商設計中結算頁包含以下5個概念:選擇收貨地址、選擇支付方式、選擇快遞、使用優惠券、使用余額和積分。筆者認為,根據我們在本系列的第一篇博文中的上下文映射圖,這背后涉及到了多個上下文的協作:

  1.用戶上下文:包含選擇收貨地址

  2.支付上下文:包含選擇支付方式、使用余額和積分

  3.售價上下文:使用優惠券。

  其中第“1”點我的理解是在整個大系統中,收貨地址並不是僅在購買的時候會用到,而是用戶可以直接管理的(一般主流電商都可以在《用戶中心》菜單內操作個人的收貨地址信息),在購物車中進行管理其實並不是一個必須經過的流程,大部分場景下只是在現有地址中做一個選擇,所以收貨地址更接近於用戶域而不是購買域,在購物車的管理可以理解為一個快捷方式而已。

  第“2”點,我的理解是,把支付操作相關的概念放到一起,可以做的很靈活,可以和運營打法搭配起來。如:支付方式和使用積分的聯動、像天貓那樣的紅包等促進用戶購買欲望的招式。

  第“3”點,我的理解是,優惠券也是會影響到整個商品的售價的,所以它應該屬於售價上下文,配合其它的促銷方式做出更多的打法。

  剩下的快遞我認為是本地購買上下文內的概念,因為它只服務於購買的流程之中。

 

三、實現

  根據服務能力來編寫ApplicationService,那么這里總共是提供了3種服務能力,所以定義了3個ApplicationService來提供這些功能:

  1.IDeliveryService:其中包含選擇收貨地址和選擇快遞

  2.IPaymentService:其中包含選擇支付方式、使用余額和積分

  3.ICouponService:包含選擇禮券。

  好了接下來就是其中涉及到的領域模型的設計,這里需要糾正一個之前的錯誤,在之前的設計中把余額直接放到了User這個值對象中,並且是從用戶上下文獲取的,現在看看當初的設計不是很妥當。因為余額並不是用戶與生俱來的東西,就好比我要認識一個人,並不一定要知道他有多少錢,但是必然需要知道姓名、年齡等。所以余額與用戶之間並不是一個強依賴關系。而且分屬於2個不同的領域聚合、甚至是上下文。這里涉及的所有領域模型的UML圖如下圖1所示:

                          【圖1】

  其中的值對象都是從遠程上下文獲取的,所以這里在購買上下文里只是使用了其的一個副本。在購買上下文的3個ApplicationService如下:

    public interface IDeliveryService
    {
        List<ShippingAddressDTO> GetAllShippingAddresses(string userId);

        Result AddNewShippingAddress(string userId, DeliveryAddNewShippingAddressRequest request);

        Result EditShippingAddress(string userId, DeliveryEditShippingAddressRequest request);

        Result DeleteShippingAddress(string id);

        List<ExpressDTO> GetAllCanUseExpresses();
    }
    public interface IPaymentService
    {
        List<PaymentMethodDTO> GetAllCanUsePaymentMethods();

        WalletDTO GetUserWallet(string userId);
    }
    public interface ICouponService
    {
        List<CouponDTO> GetAllCoupons(string userId);
    }

  這里接口定義思路是把界面上的操作記錄全部由UI程序做本地緩存/Cookie等,減少服務端的處理壓力,所以接口看上去比較簡單,沒有那些使用禮券,修改使用的收貨地址這類的接口。

  另外提一下,在當前的解決方案中的售價上下文中的處理中,增加了2個聚合來處理優惠券相關的業務。

    public class Coupon : AggregateRoot
    {
        public string Name { get; private set; }

        public decimal Value { get; private set; }

        public DateTime ExpiryDate { get; private set; }

        public List<string> ContainsProductIds { get; private set; }

        public Coupon(string name, decimal value, DateTime expiryDate, IEnumerable<string> containsProductIds)
        {
            if (string.IsNullOrWhiteSpace(name))
                throw new ArgumentNullException("name");

            if (value <= 0)
                throw new ArgumentException("value不能小於等於0", "value");

            if (expiryDate == default(DateTime))
                throw new ArgumentException("請傳入正確的expiryDate", "expiryDate");

            if (containsProductIds == null)
                throw new ArgumentNullException("containsProductIds");

            this.Name = name;
            this.Value = value;
            this.ExpiryDate = expiryDate;
            this.ContainsProductIds = containsProductIds.ToList();
        }
    }
    public class CouponNo : AggregateRoot
    {
        public string CouponId { get; private set; }

        public DateTime UsedTime { get; private set; }

        public bool IsUsed
        {
            get { return UsedTime != default(DateTime) && UsedTime < DateTime.Now; }
        }

        public string UserId { get; private set; }

        public CouponNo(string couponId, DateTime usedTime, string userId)
        {
            if (string.IsNullOrWhiteSpace(couponId))
                throw new ArgumentNullException("couponId");

            if (string.IsNullOrWhiteSpace(userId))
                throw new ArgumentNullException("userId");

            this.CouponId = couponId;
            this.UsedTime = usedTime;
            this.UserId = userId;
        }

        public void BeUsed()
        {
            this.UsedTime = DateTime.Now;
        }
    }

  其中CouponNo中的CouponId是保持了一個對Coupon聚合ID的引用,在需要的時候從Repository中取出Coupon的信息。部分代碼如下:

            var couponNos = DomainRegistry.CouponNoRepository().GetNotUsedByUserId(cart.UserId);

            var buyProductIds = cart.CartItems.Select(ent => ent.ProductId);
            List<CouponDTO> couponDtos = new List<CouponDTO>();
            foreach (var couponNo in couponNos)
            {
                if (couponNo.IsUsed)
                    continue;

                var coupon = DomainRegistry.CouponRepository().GetByIdentity(couponNo.CouponId);

                if (coupon.ContainsProductIds.Count == 0 || coupon.ContainsProductIds.Any(ent => buyProductIds.Any(e => e == ent)))
                {
                    couponDtos.Add(new CouponDTO
                    {
                        CanUse = couponNo.IsUsed,
                        ExpiryDate = coupon.ExpiryDate,
                        ID = couponNo.ID,
                        Name = coupon.Name,
                        Value = coupon.Value
                    });
                }
            }

 

 

四、結語

  本篇比較簡單不多述了,下面源碼奉上,有興趣的同學自行下載查看全部源碼。

 

 

 

本文的源碼地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo11

 

作者:Zachary
出處:https://zacharyfan.com/archives/876.html

 

 

▶關於作者:張帆(Zachary,個人微信號:Zachary-ZF)。堅持用心打磨每一篇高質量原創。歡迎掃描右側的二維碼~。

定期發表原創內容:架構設計丨分布式系統丨產品丨運營丨一些思考。

 

如果你是初級程序員,想提升但不知道如何下手。又或者做程序員多年,陷入了一些瓶頸想拓寬一下視野。歡迎關注我的公眾號「跨界架構師」,回復「技術」,送你一份我長期收集和整理的思維導圖。

如果你是運營,面對不斷變化的市場束手無策。又或者想了解主流的運營策略,以豐富自己的“倉庫”。歡迎關注我的公眾號「跨界架構師」,回復「運營」,送你一份我長期收集和整理的思維導圖。


免責聲明!

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



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