如何一步一步用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)。坚持用心打磨每一篇高质量原创。欢迎扫描右侧的二维码~。

定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。

 

如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。

如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。

posted @ 2017-01-16 06:51  Zachary_Fan  阅读(4677)  评论(7编辑  收藏  举报