概述
购物车模块的难点有以下几点
1、购物车促销的显示和价格计算
2、结算页促销的显示和价格计算
3、计算和显示逻辑复杂,还要时时判断活动的有效性
4、两个地方的购物车显示和计算,有一样的逻辑的地方,也有差异的
5、促销规则的多种多样
思路
接下来,我们来详细看一下Javashop电商系统中购物车的架构思路:
一、将存储分为两部分:
sku原始数据
用户选择的促销活动
每次购物车的显示,都根据这些数据进行一次重新渲染和计算
二、将促销规则的算法和计算分开
抽像出规则对象,由每个活动根据原始数据去生成这些规则
然后统一将这些规则进行计算形成要显示的效果和价格
三、把不可避免的耗性能的操作,放在加入购物车中完成,而不是在列表循环中完成
领域模型
CartVO
属性 | 说明 | 备注 |
---|---|---|
skuList | 规格列表 | 对应CartSkuVO对象 |
sellerId | 卖家id | |
price | 价格对象 | 对应PriceDetailVO对象 |
ruleList | 促销规则列表 | 对应PromotionRule对象 |
couponList | 优惠卷列表 | 对应CouponVO对象 |
giftList | 赠品列表 | 对应FullDiscountGiftDO |
giftCouponList | 赠送优惠卷列表 | 对应CouponVO对象 |
promotionNotice | 促销提示 | 目前只有满优惠提示 |
CartSkuVO
属性 | 说明 | 备注 |
---|---|---|
name | 商品名称 | |
skuId | skuid | |
specList | 规则列表 | 对应SpecValueVo对象 |
singleList | 单品活动列表 | 对应CartPromotionVo对象, 显示在列表中供用户选择 |
groupList | 组合活动列表 | 对应CartPromotionVo对象 |
invalid | 是否失效 | |
errorMessage | 失效原因 | |
originalPrice | 商品原价 | 用于计算优惠的基础价格 |
purchasePrice | 成交价 | |
num | 数量 | |
subtotal | 小计 | |
promotionTags | 促销标签 | 显示当前sku应用了何种优惠 |
PriceDetailVO
属性 | 说明 | 备注 |
---|---|---|
originalPrice | 原价 | |
goodsPrice | 成交价 | |
freightPrice | 运费 | |
totalPrice | 合计 | |
discountPrice | 总优惠价格 | |
cashBack | 返现金额 | 所有单品活动产生的优惠 |
fullMinus | 满减金额 | |
couponPrice | 优惠卷抵扣金额 | 不计在返现中 |
isFreeFreight | 是否免运费 | |
exchangePoint | 用了多少积分 | 用于兑换此商品 |
PromotionRule
属性 | 说明 | 备注 |
---|---|---|
originalPrice | 原价 | |
goodsPrice | 成交价 | |
freightPrice | 运费 | |
totalPrice | 合计 | |
discountPrice | 总优惠价格 | |
cashBack | 返现金额 | 所有单品活动产生的优惠 |
fullMinus | 满减金额 | |
couponPrice | 优惠卷抵扣金额 | 不计在返现中 |
isFreeFreight | 是否免运费 | |
exchangePoint | 用了多少积分 | 用于兑换此商品 |
CouponVO
属性 | 说明 | 备注 |
---|---|---|
memberCouponId | 会员优惠卷id | 会员领取后的唯一id,取消时或使用时 要用此id |
couponId | 此优惠卷的id | 会员领取后,此值不变,不能做为使用时调用 |
sellerId | 卖家id | |
amount | 面值 | |
endTime | 有效期 | 到秒的时间戳 |
useTerm | 使用条件 | 如:“满100元可用” |
selected | 是否选中 | 当用户选择此优惠卷时,会标记为1,未选中时为0 |
enable | 是否可用 | 当不可用时(不满足条件或已过期)为0,可用为1 |
数据存储
SelectedPromotionVo
属性 | 说明 | 备注 |
---|---|---|
singlePromotionMap | 用户选择的单品活动 | |
couponMap | 用户选择的优惠卷 |
1、singlePromotionMap
类型:Map
key是店铺id ,对应此店铺对应的促销活动
2、couponMap
类型:Map
key是店铺id ,对应此店铺使用的优惠卷
项 | 前缀 | 连接 | 存储对象 |
---|---|---|---|
购物车原始数据 | CART_ORIGIN_DATA_PREFIX | buyer.uid | List |
购物车促销 | CART_PROMOTION_PREFIX | buyer.uid | SelectedPromotionVo |
购物车的添加
1、调用原始数据业务类(CartOriginDataManager)的添加方法
根据sku读出商品数据,并形成CartSkuOriginVo
2、填充促销信息
读取此商品的促销活动,填充到上述的Vo中
此时如果传递了要使用的活动id(需要使用活动的,见下面)
3、写入缓存
形成list并写入redis
4、使用活动
如果传递了活动id,则调用CartPromotionManager 使用此活动
5、写入缓存
在使用活动时,会将组合好的 singlePromotionMap 写入redis
购物车显示
通过“建造者”模式来完成购物车的促销信息渲染、价格计算的。
其中要建造的“产品”是CartView,包含一个List和一个price 对象(即列表和总价)
建造过程是一条流水线:
1、首先由SkuRenderer(Sku构建器)构建出全新的一个CartList
这个CartList是由缓存中OriginSku的skulist做为物料生成出来的
2、接下来由促销规则渲染器(PromotionRuleRenderer)构建出促销规则(Promotion)
此时的物料是用户选择的Promotiont生成出来的,具体的制造过程参见《促销规则的构建》
3、流水线中下一个制造环节是生产Price
此时的物料是上一步生产的Rule,按照一定的规则算法对价格进行计算:
具体的制造过程参见《价格的计算过程》
4、流水线是由CartBuilder来总体控制的,最终由他来组装成品:CartView
调用时序如下:
促销规则的构建
根据需求,促销规则主要有以下几种:
组合促销:满减
单品促销:第二件半价、单品立减、团购,秒杀等
优惠卷
其中组合促销是应用在整个购物车中的,
单品促销是应用在Sku上的,
优惠卷只有在结算页才能使用和计算,而且不计算在返现金额中。
综上所述,我们分别针对如上的种类,定义了:
SkuPromotionRuleBuilder(Sku促销规则构建器)
CartPromotionRuleBuilder(Cart促销规则构建器)
CartCouponRuleBuilder(优惠卷促销规则构建器)
用关系:
先调用CartPromotionManager 获取已经选中的促销
再分别调用各种构建器构建出Rule,
从流水线的控制上,优惠卷的构建是要被跳过的(因为购物车是不处理优惠卷的)
将Rule分别放在Cart和Sku中的Rule中
SKU规则构建器
根据目前的单品促销类型,实现了5个具体的构建器:
SeckillPluginNew 秒杀
GroupBuyGoodsPluginNew 团购
MinusPluginNew 单品立减
HalfPricePluginNew 第二件半件
ExchangePluginNew 积分兑换
具体调用哪个构建器完build rule ,则由实现者的
getPromotionType(): PromotionTypeEnum
方法来决定
Cart规则构建器
这是应用在购物车上的规则构建器,目前只有一个满减的实现
优惠卷规则构建器
目前只有一个默认实现
结算页购物车的显示和价格计算
根据需求,在结算页要计算运费和优惠卷,因此在流水线上要控制其制造流程:
在促销规则的构建过程中加入了优惠卷的构建
在计算价格之前加入了运费的计算
在最后加入了优惠卷的渲染CartVo中的CouponLIst
购物车构建器的总体类图
那么最终购物车构建器总体类图如下:
促销规则和价格计算
促销规则
从上面的架构可以看出,促销规则的定义非常重要,可以参见《PromotionRule》,即:
在这里我们定义了:
reducedTotalPrice是总体减的金额
reducedPrice:是单品减的金额
useCoupon:是要使用的优惠卷
invalid: 定义了是否失效了,比如加入购物车时活动还有效,但过了一会正好失效了。
invalidReason:
不光定义了失效的原因,还有一些特殊情况:比如加入购物车是商品活动售空数是5,买了5个,过了一会别人下单成功了,售后数是3个了,此时在这里要提示用户,但不失效,用户可以勾选改为3个继续下单
价格计算
价格计算统一面向规则,而不管规则的构建过程,从而实现了算法和计算的分离。
这是在CartPriceCalculator中来完成的,实现过程就比较简单了: