一、前言

最近实在太忙,上周停更了一周。按流程一步一步走到现在,到达了整个下单流程的最后一公里——结算页的处理。

从整个流程来看,这里需要用户填写的信息是最多的,那么在后端的设计中如何考虑到业务边界的划分,和相互之间的交互复杂度,又是我们需要考虑的地方。

总体来说本篇讲述的内容在前几篇都有涉及,所以这次一次性处理的业务比较多,已经比较熟练的看官可以跳过本篇。

二、准备

主流的电商设计中结算页包含以下 5 个概念:选择收货地址、选择支付方式、选择快递、使用优惠券、使用余额和积分。笔者认为,根据我们在本系列的第一篇博文中的上下文映射图,这背后涉及到了多个上下文的协作:

  1. 用户上下文:包含选择收货地址

  2. 支付上下文:包含选择支付方式、使用余额和积分

  3. 售价上下文:使用优惠券。

其中第 “1” 点我的理解是在整个大系统中,收货地址并不是仅在购买的时候会用到,而是用户可以直接管理的(一般主流电商都可以在《用户中心》菜单内操作个人的收货地址信息),在购物车中进行管理其实并不是一个必须经过的流程,大部分场景下只是在现有地址中做一个选择,所以收货地址更接近于用户域而不是购买域,在购物车的管理可以理解为一个快捷方式而已。

第 “2” 点,我的理解是,把支付操作相关的概念放到一起,可以做的很灵活,可以和运营打法搭配起来。如:支付方式和使用积分的联动、像天猫那样的红包等促进用户购买欲望的招式。

第 “3” 点,我的理解是,优惠券也是会影响到整个商品的售价的,所以它应该属于售价上下文,配合其它的促销方式做出更多的打法。

剩下的快递我认为是本地购买上下文内的概念,因为它只服务于购买的流程之中。

三、实现

根据服务能力来编写 ApplicationService,那么这里总共是提供了 3 种服务能力,所以定义了 3 个 ApplicationService 来提供这些功能:

1.IDeliveryService:其中包含选择收货地址和选择快递

2.IPaymentService:其中包含选择支付方式、使用余额和积分

3.ICouponService:包含选择礼券。

好了接下来就是其中涉及到的领域模型的设计,这里需要纠正一个之前的错误,在之前的设计中把余额直接放到了 User 这个值对象中,并且是从用户上下文获取的,现在看看当初的设计不是很妥当。

因为余额并不是用户与生俱来的东西,就好比我要认识一个人,并不一定要知道他有多少钱,但是必然需要知道姓名、年龄等。

所以余额与用户之间并不是一个强依赖关系。而且分属于 2 个不同的领域聚合、甚至是上下文。这里涉及的所有领域模型的 UML 图如下图 1 所示:

11 最后的准备 - 图1

图 1

其中的值对象都是从远程上下文获取的,所以这里在购买上下文里只是使用了其的一个副本。在购买上下文的 3 个 ApplicationService 如下:

  1. public interface IDeliveryService
  2. {
  3. List<ShippingAddressDTO> GetAllShippingAddresses(string userId);
  4. Result AddNewShippingAddress(string userId, DeliveryAddNewShippingAddressRequest request);
  5. Result EditShippingAddress(string userId, DeliveryEditShippingAddressRequest request);
  6. Result DeleteShippingAddress(string id);
  7. List<ExpressDTO> GetAllCanUseExpresses();
  8. }
  9. public interface IPaymentService
  10. {
  11. List<PaymentMethodDTO> GetAllCanUsePaymentMethods();
  12. WalletDTO GetUserWallet(string userId);
  13. }
  14. public interface ICouponService
  15. {
  16. List<CouponDTO> GetAllCoupons(string userId);
  17. }

这里接口定义思路是把界面上的操作记录全部由 UI 程序做本地缓存 / Cookie 等,减少服务端的处理压力,所以接口看上去比较简单,没有那些使用礼券,修改使用的收货地址这类的接口。

另外提一下,在当前的解决方案中的售价上下文中的处理中,增加了 2 个聚合来处理优惠券相关的业务。

  1. public class Coupon : AggregateRoot
  2. {
  3. public string Name { get; private set; }
  4. public decimal Value { get; private set; }
  5. public DateTime ExpiryDate { get; private set; }
  6. public List<string> ContainsProductIds { get; private set; }
  7. public Coupon(string name, decimal value, DateTime expiryDate, IEnumerable<string> containsProductIds)
  8. {
  9. if (string.IsNullOrWhiteSpace(name))
  10. throw new ArgumentNullException("name");
  11. if (value <= 0)
  12. throw new ArgumentException("value不能小于等于0", "value");
  13. if (expiryDate == default(DateTime))
  14. throw new ArgumentException("请传入正确的expiryDate", "expiryDate");
  15. if (containsProductIds == null)
  16. throw new ArgumentNullException("containsProductIds");
  17. this.Name = name;
  18. this.Value = value;
  19. this.ExpiryDate = expiryDate;
  20. this.ContainsProductIds = containsProductIds.ToList();
  21. }
  22. }
  23. public class CouponNo : AggregateRoot
  24. {
  25. public string CouponId { get; private set; }
  26. public DateTime UsedTime { get; private set; }
  27. public bool IsUsed
  28. {
  29. get { return UsedTime != default(DateTime) && UsedTime < DateTime.Now; }
  30. }
  31. public string UserId { get; private set; }
  32. public CouponNo(string couponId, DateTime usedTime, string userId)
  33. {
  34. if (string.IsNullOrWhiteSpace(couponId))
  35. throw new ArgumentNullException("couponId");
  36. if (string.IsNullOrWhiteSpace(userId))
  37. throw new ArgumentNullException("userId");
  38. this.CouponId = couponId;
  39. this.UsedTime = usedTime;
  40. this.UserId = userId;
  41. }
  42. public void BeUsed()
  43. {
  44. this.UsedTime = DateTime.Now;
  45. }
  46. }

其中 CouponNo 中的 CouponId 是保持了一个对 Coupon 聚合 ID 的引用,在需要的时候从 Repository 中取出 Coupon 的信息。部分代码如下:

  1. var couponNos = DomainRegistry.CouponNoRepository().GetNotUsedByUserId(cart.UserId);
  2. var buyProductIds = cart.CartItems.Select(ent => ent.ProductId);
  3. List<CouponDTO> couponDtos = new List<CouponDTO>();
  4. foreach (var couponNo in couponNos)
  5. {
  6. if (couponNo.IsUsed)
  7. continue;
  8. var coupon = DomainRegistry.CouponRepository().GetByIdentity(couponNo.CouponId);
  9. if (coupon.ContainsProductIds.Count == 0 || coupon.ContainsProductIds.Any(ent => buyProductIds.Any(e => e == ent)))
  10. {
  11. couponDtos.Add(new CouponDTO
  12. {
  13. CanUse = couponNo.IsUsed,
  14. ExpiryDate = coupon.ExpiryDate,
  15. ID = couponNo.ID,
  16. Name = coupon.Name,
  17. Value = coupon.Value
  18. });
  19. }
  20. }

四、结语

本篇比较简单不多述了,下面源码奉上,有兴趣的同学自行下载查看全部源码。

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


原创文章,转载请注明本文链接: https://zacharyfan.com/archives/876.html

关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描二维码~

11 最后的准备 - 图2

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

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

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