如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文
本系列所有文章
如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念
如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户
如何一步一步用DDD设计一个电商网站(五)—— 停下脚步,重新出发
如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文
如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文
如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成
如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑
如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车
如何一步一步用DDD设计一个电商网站(十一)—— 最后的准备
如何一步一步用DDD设计一个电商网站(十二)—— 提交并生成订单
如何一步一步用DDD设计一个电商网站(十三)—— 领域事件扩展
阅读目录
一、前言
上一篇我们已经确立的购买上下文和销售上下文的交互方式,传送门在此:http://www.cnblogs.com/Zachary-Fan/p/DDD_6.html,本篇我们来实现售价上下文的具体细节。
二、明确业务细节
电商市场越来越成熟,竞争也越来越激烈,影响客户流量的关键因素之一就是价格,运营的主要打法之一也是价格,所以是商品价格是一个在电商中很重要的一环。正因为如此也让促销演变的越来越复杂,那么如何在编码上花点心思来尽可能的降低业务的复杂化带来的影响和提高可扩展性来拥抱变化就变得很重要了。先从最简单的开始,我浏览了某东的促销,先把影响价格相关的几个促销找出来,暂时得出以下几个结论(这里又要提一下,我们实际工作中应在开始编码之前要做的就是和领域专家讨论促销的细节):
1.满减:可以多个商品共同参与,汇总金额达到某个阈值之后减免XX金额。
2.多买优惠(方式1):可以多个商品共同参与,汇总购买数量达到一定数量得到X折的优惠。
3.多买优惠(方式2):可以多个商品共同参与,汇总购买数量达到一定数量减免最便宜的X件商品。
4.限时折扣:直接商品的购买金额被修改到指定值。
5.满减促销的金额满足点以优惠后价格为准,比如该商品既有限时折扣又有满减,则使用限时折扣的价格来计算金额满足点。
6.优惠券是在之上的规则计算之后得出的金额基础下计算金额满足点。
7.每一个商品的满减+多买优惠仅能参与一种。并且相同促销商品在购物车中商品展示的方式是在一组中。
三、建模
根据上面的业务描述先找到其中的几个领域对象,然后在做一些适当的抽象,得出下面的UML图(点击图片可查看大图):
【图1】
四、实现
建模完之后下面的事情就容易了,先梳理一下我们的业务处理顺序:
1.根据购买上下文传入的购物车信息获取产品的相关促销。
2.先处理单品促销。
3.最后处理多商品共同参与的促销。
梳理的过程中发现,为了能够实现满减和多买优惠促销仅能参与一个,所以需要再购买上下文和售价上下文之间传递购物项时增加一个参数选择的促销唯一标识(SelectedMultiProductsPromotionId)。
随后根据上面业务处理顺序,发现整个处理的链路比较长,那么这里我决定定义一个值对象来承载整个处理的过程。如下:
public class BoughtProduct
{
private readonly List<PromotionRule> _promotionRules = new List<PromotionRule>(); public string ProductId { get; private set; } public int Quantity { get; private set; } public decimal UnitPrice { get; private set; } public decimal ReducePrice { get; private set; } /// <summary>
/// 商品在单品优惠后的单价,如果没有优惠则为正常购买的单价
/// </summary>
public decimal DiscountedUnitPrice
{
get { return UnitPrice - ReducePrice; }
} public decimal TotalDiscountedPrice
{
get { return DiscountedUnitPrice * Quantity; }
} public ReadOnlyCollection<ISingleProductPromotion> InSingleProductPromotionRules
{
get { return _promotionRules.OfType<ISingleProductPromotion>().ToList().AsReadOnly(); }
} public IMultiProductsPromotion InMultiProductPromotionRule { get; private set; } public BoughtProduct(string productId, int quantity, decimal unitPrice, decimal reducePrice, IEnumerable<PromotionRule> promotionRules, string selectedMultiProdcutsPromotionId)
{
if (string.IsNullOrWhiteSpace(productId))
throw new ArgumentException("productId不能为null或者空字符串", "productId"); if (quantity <= )
throw new ArgumentException("quantity不能小于等于0", "quantity"); if (unitPrice < )
throw new ArgumentException("unitPrice不能小于0", "unitPrice"); if (reducePrice < )
throw new ArgumentException("reducePrice不能小于0", "reducePrice"); this.ProductId = productId;
this.Quantity = quantity;
this.UnitPrice = unitPrice;
this.ReducePrice = reducePrice; if (promotionRules != null)
{
this._promotionRules.AddRange(promotionRules);
var multiProductsPromotions = this._promotionRules.OfType<IMultiProductsPromotion>().ToList();
if (multiProductsPromotions.Count > )
{
var selectedMultiProductsPromotionRule = multiProductsPromotions.SingleOrDefault(ent => ((PromotionRule)ent).PromotoinId == selectedMultiProdcutsPromotionId); InMultiProductPromotionRule = selectedMultiProductsPromotionRule ?? multiProductsPromotions.First();
}
}
} public BoughtProduct ChangeReducePrice(decimal reducePrice)
{
if (reducePrice < )
throw new ArgumentException("result.ReducePrice不能小于0"); var selectedMultiProdcutsPromotionId = this.InMultiProductPromotionRule == null
? null
: ((PromotionRule) this.InMultiProductPromotionRule).PromotoinId;
return new BoughtProduct(this.ProductId, this.Quantity, this.UnitPrice, reducePrice, this._promotionRules, selectedMultiProdcutsPromotionId);
}
}
需要注意一下,值对象的不可变性,所以这里的ChangeReducePrice方法返回的是一个新的BoughtProduct对象。另外这次我们的例子比较简单,单品促销只有1种。理论上单品促销是支持叠加参与的,所以这里的单品促销设计了一个集合来存放。
下面的代码是处理单品促销的代码:
foreach (var promotionRule in singleProductPromotionRules)
{
var tempReducePrice = ((PromotionRuleLimitTimeDiscount)promotionRule).CalculateReducePrice(productId, unitPrice, DateTime.Now); //在创建的时候约束促销的重复性。此处逻辑上允许重复
if (unitPrice - reducePrice <= tempReducePrice)
{
reducePrice = unitPrice;
}
else
{
reducePrice += tempReducePrice;
}
}
这里也可以考虑把它重构成一个领域服务来合并同一个商品多个单品促销计算结果。
整个应用服务的代码如下:
public class CalculateSalePriceService : ICalculateSalePriceService
{
private static readonly MergeSingleProductPromotionForOneProductDomainService _mergeSingleProductPromotionForOneProductDomainService = new MergeSingleProductPromotionForOneProductDomainService(); public CalculatedCartDTO Calculate(CartRequest cart)
{
List<BoughtProduct> boughtProducts = new List<BoughtProduct>(); foreach (var cartItemRequest in cart.CartItems)
{
var promotionRules = DomainRegistry.PromotionRepository().GetListByContainsProductId(cartItemRequest.ProductId);
var boughtProduct = new BoughtProduct(cartItemRequest.ProductId, cartItemRequest.Quantity, cartItemRequest.UnitPrice, , promotionRules, cartItemRequest.SelectedMultiProductsPromotionId);
boughtProducts.Add(boughtProduct);
} #region 处理单品促销
foreach (var boughtProduct in boughtProducts.ToList())
{
var calculateResult = _mergeSingleProductPromotionForOneProductDomainService.Merge(boughtProduct.ProductId, boughtProduct.DiscountedUnitPrice, boughtProduct.InSingleProductPromotionRules); var newBoughtProduct = boughtProduct.ChangeReducePrice(calculateResult); boughtProducts.Remove(boughtProduct);
boughtProducts.Add(newBoughtProduct);
}
#endregion #region 处理多商品促销&构造DTO模型
List<CalculatedFullGroupDTO> fullGroupDtos = new List<CalculatedFullGroupDTO>();
foreach (var groupedPromotoinId in boughtProducts.Where(ent => ent.InMultiProductPromotionRule != null).GroupBy(ent => ((PromotionRule)ent.InMultiProductPromotionRule).PromotoinId))
{
var multiProdcutsReducePricePromotion = (IMultiProdcutsReducePricePromotion)groupedPromotoinId.First().InMultiProductPromotionRule; //暂时只有减金额的多商品促销
var products = groupedPromotoinId.ToList(); if (multiProdcutsReducePricePromotion == null)
continue; var reducePrice = multiProdcutsReducePricePromotion.CalculateReducePrice(products);
fullGroupDtos.Add(new CalculatedFullGroupDTO
{
CalculatedCartItems = products.Select(ent => ent.ToDTO()).ToArray(),
ReducePrice = reducePrice,
MultiProductsPromotionId = groupedPromotoinId.Key
});
}
#endregion return new CalculatedCartDTO
{
CalculatedCartItems = boughtProducts.Where(ent => fullGroupDtos.SelectMany(e => e.CalculatedCartItems).All(e => e.ProductId != ent.ProductId))
.Select(ent => ent.ToDTO()).ToArray(),
CalculatedFullGroups = fullGroupDtos.ToArray(),
CartId = cart.CartId
};
}
}
五、结语
这里的设计没有考虑促销规则的冲突问题,如果做的话把它放在创建促销规则的时候进行约束即可。
本文的源码地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo7。
作者:Zachary
出处:https://zacharyfan.com/archives/154.html
▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描右侧的二维码~。
定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。
如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。
如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。
如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文的更多相关文章
- 如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑
阅读目录 前言 场景1的思考 场景2的思考 避坑方式 实践 结语 一.前言 在上一篇中(如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成),有一行注释的代码: public interfa ...
- 如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成
阅读目录 前言 建模 实现 结语 一.前言 前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念.会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是 ...
- 如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车
阅读目录 前言 回顾 梳理 实现 结语 一.前言 之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能.本篇准备把剩下的购物车的基本概念一次处理完. 二.回顾 在动手之前我对之 ...
- 如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文
阅读目录 前言 如何在一个项目中实现多个上下文的业务 售价上下文与购买上下文的集成 结语 一.前言 前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西.比如促销.会员价等,在我们的 ...
- 如何一步一步用DDD设计一个电商网站(五)—— 停下脚步,重新出发
阅读目录 前言 单元测试 纠正错误,重新出发 结语 一.前言 实际编码已经写了2篇了,在这过程中非常感谢有听到观点不同的声音,借着这个契机,今天这篇就把大家提出的建议一个个的过一遍,重新整理,重新出发 ...
- 如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户
阅读目录 前言 怎么卖 领域服务的使用 回到现实 结语 一.前言 上篇中我们讲述了“把商品卖给用户”中的商品和用户的初步设计.现在把剩余的“卖”这个动作给做了.这里提醒一下,正常情况下,我们的每一步业 ...
- 如何一步一步用DDD设计一个电商网站(三)—— 初涉核心域
一.前言 结合我们本次系列的第一篇博文中提到的上下文映射图(传送门:如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念),得知我们这个电商网站的核心域就是销售子域.因为电子商务是以信息网络 ...
- 如何一步一步用DDD设计一个电商网站(十一)—— 最后的准备
阅读目录 前言 准备 实现 结语 一.前言 最近实在太忙,上周停更了一周.按流程一步一步走到现在,到达了整个下单流程的最后一公里——结算页的处理.从整个流程来看,这里需要用户填写的信息是最多的,那么 ...
- 如何一步一步用DDD设计一个电商网站(十二)—— 提交并生成订单
阅读目录 前言 解决数据一致性的方案 回到DDD 设计 实现 结语 一.前言 之前的十一篇把用户购买商品并提交订单整个流程上的中间环节都过了一遍.现在来到了这最后一个环节,提交订单.单从业务上看,这个 ...
随机推荐
- JS调用Android、Ios原生控件
在上一篇博客中已经和大家聊了,关于JS与Android.Ios原生控件之间相互通信的详细代码实现,今天我们一起聊一下JS调用Android.Ios通信的相同点和不同点,以便帮助我们在进行混合式开发时, ...
- FFmpeg学习6:视音频同步
在上一篇文章中,视频和音频是各自独立播放的,并不同步.本文主要描述了如何以音频的播放时长为基准,将视频同步到音频上以实现视音频的同步播放的.主要有以下几个方面的内容 视音频同步的简单介绍 DTS 和 ...
- SQL Server技术内幕笔记合集
SQL Server技术内幕笔记合集 发这一篇文章主要是方便大家找到我的笔记入口,方便大家o(∩_∩)o Microsoft SQL Server 6.5 技术内幕 笔记http://www.cnbl ...
- DDD CQRS架构和传统架构的优缺点比较
明天就是大年三十了,今天在家有空,想集中整理一下CQRS架构的特点以及相比传统架构的优缺点分析.先提前祝大家猴年新春快乐.万事如意.身体健康! 最近几年,在DDD的领域,我们经常会看到CQRS架构的概 ...
- Kooboo CMS技术文档之二:Kooboo CMS的安装步骤
在IIS上安装Kooboo CMS Kooboo CMS安装之后 安装的常见问题 1. 在IIS上安装Kooboo CMS Kooboo CMS部署到正式环境相当简单,安装过程是一个普通MVC站点在I ...
- 设置tomcat远程debug
查看端口占用情况命令: netstat -tunlp |grep 8000 tomcat 启动远程debug: startup.sh 中的最后一行 exec "$PRGDIR"/& ...
- PHP类和对象之重载
PHP中的重载指的是动态的创建属性与方法,是通过魔术方法来实现的.属性的重载通过__set,__get,__isset,__unset来分别实现对不存在属性的赋值.读取.判断属性是否设置.销毁属性. ...
- iOS 键盘添加完成按钮,delegate和block回调
这个是一个比较初级一点的文章,新人可以看看.当然实现这个需求的时候自己也有一点收获,记下来吧. 前两天产品要求在工程的所有数字键盘弹出时,上面带一个小帽子,上面安装一个“完成”按钮,这个完成按钮也没有 ...
- 微信小程序开发—快速掌握组件及API的方法
微信小程序框架为开发者提供了一系列的组件和API接口. 组件主要完成小程序的视图部分,例如文字.图片显示.API主要完成逻辑功能,例如网络请求.数据存储.音视频播放控制,以及微信开放的微信登录.微信支 ...
- mac下生成ssh keys 并上传github仓储
使用github仓储需要本机生成一个公钥key 添加到自己的git账户SSH keys中 mac 生成方法: 1. 打开终端 输入 ssh-keygen 然后系统提示输入文件保存位置等信息 ...