需求和定义
设计一个灵活的促销架构,可以容纳当前可预见到的所有促销活动类型。活动的配置方式要足够自由,允许运营管理员自由配置活动的参与人群、活动包含的商品范围、特别是不同活动的叠加关系。
配置时,一个商品可以同时在同一时间参加多个活动。而在结算时互斥的多个活动中只能选择其中一个参加,这个选择可以是系统自动的,也可以是用户自己选择的。不互斥的活动可以则同时参加。
这里的促销活动是指用户购买商品时会改变订单的实付价格或者订单商品数量的促销类型,例如限时降价、满100减10、买1赠1、会员打折等。不在下单过程中起作用的活动不包括在这个范围里,例如新用户注册送券、下单后赠券、下单后返现等。
逻辑架构
这个架构设计最关键的问题是要解决不同促销活动叠加使用的问题,而且能否叠加是可以由运营在配置的时候来指定的,而不是在代码里固定不变。例如运营设置了一个限时价活动为原价的2折,已经突破了成本价,这时候运营可能不希望用户再使用其它的促销活动;而另一个限时价活动只是原价的9折,这时候运营可能就允许用户叠加使用其它活动。
活动叠加存在先后关系,后面的促销结果基于前面的促销结果进行计算。
根据这个以上需求,设计一个分层的促销活动架构,处在同一层的促销表示互斥的、不能同时参加的活动类型,不同一层的活动则可以叠加使用。一个常见的分层方式是这样的:
层级 | 促销方式 | 举例 |
---|---|---|
1级 | 直接修改价格 | 直降价、秒杀、拼团、预订 |
2级 | 范围促销 | 指定商品买1送1;指定品牌买满100减10;全场商品满100减10; |
3级 | 加价换购 | 买满100后加10元换购xx; |
4级 | 优惠券 | 满100减10券;满3件8折券; |
5级 | 会员折上折 | 在实付金额上直接打95折; |
6级 | 包邮 | 单品包邮;满99包邮;大促全场包邮; |
7级 | 运费券 | |
8级 | 积分抵扣 | 100积分抵1元 |
分层的方式和层次之间的优先级主要依据以下2点:
1.逻辑上是否允许一个商品能否同时参加两个活动,例如用户购买一个商品时,显然不能同时参加秒杀活动又同时参加拼团活动。
2.从运营角度是否允许一个商品同时参加两个活动,例如有些平台会把范围促销分成类目促销和全场促销两种,并且允许用户同时参加这两种活动,这时候就要把范围促销拆分成两层。
层次之间能否叠加由前面的活动来指定,后面的活动不能指定能否叠加前面层级的促销,否则将需要不断的逆向回滚。即,在配置1级活动的时候可以指定能否叠加后面的2到8级的促销,而在配置7级活动时只能指定能否叠加8级促销。
促销计算过程
计算过程参考管道设计模式(pipeline),把每一个促销层级定义为一个主策略。同一层内有多种促销活动的,把每一种活动定义为一个子策略,在主策略里主要实现如果选择子策略的逻辑。
另外,不同位置和不同渠道可以使用的促销类型可能会有所不同,例如购物车一般不需要计算运费这一级以后促销、微信渠道不能使用会员折上折等。这种情况只要把主策略组合成不同的组合,计算时按需求指定要使用的策略组合就可以了。
也就是说,从上到下依次有三个层次的策略:策略组合、主策略、子策略。这三种策略的输入参数和输出结果完全一样。因此定义一个促销计算结果对象,作为所有策略的输入参数和输出结果。
计算时,把用户购物车里的商品组装成促销计算结果结构,依次通过全部策略,每个策略都会修改促销计算结果里的价格,以及在商品信息上附加一个促销计算结果。下面这个例子,用户购买了goodsId=1的商品2件,商品原价是10元,这时候还没有经过促销计算,实付价等于原价。注意promotion的isUserSelect字段,表示用户指定要参加id=10的这个秒杀活动。
{
"goods":[{
"goodsId":1,
"buyCount":2,
"originalPrice":10,
"payPrice":10,
"promotions":[{
"type":"flashSale",
"id":10,
"isUserSelect":true
}]
}],
"originalPrice":20,
"payPrice":20,
"promotions":[]
}
经过全部促销策略计算之后得到类似这样的一个结构。这个商品一共应用了两个促销活动,实付价变成5元,其中参加秒杀活动减了4元,优惠券减了1元。
levelStacking字段表示这个促销可以跟哪些层级的促销叠加下,例如新客秒杀这个活动的levelStacking=[4,5],表示这个活动可以叠加4级和5级促销,但是不能同时参加其它层级的活动。
priceAdjustment字段表示这个商品在这个促销活动上减了多少钱。
{
"goods":[{
"goodsId":1,
"goodsName":"示例商品",
"buyCount":2,
"originalPrice":10,
"payPrice":5,
"promotions":[{
"type":"flashSale",
"id":10,
"name":"新客秒杀",
"description":"仅限新客参加",
"isUserSelect":true,
"levelStacking":[4,5],
"priceAdjustment":4,
"extraInfo":[]
},{
"type":"discountCoupon",
"id":11,
"name":"新客无门槛减1券",
"description":"仅限新客参加",
"isUserSelect":false,
"levelStacking":[5],
"priceAdjustment":1,
"extraInfo":[]
}]
],
"originalPrice":20,
"payPrice":10,
"promotions":[{
"type":"flashSale",
"id":10,
"name":"新客秒杀",
"priceAdjustment":8,
"joinedGoodsId":[1],
"extraInfo":[]
},{
"type":"discountCoupon",
"id":11,
"levelStacking":[5],
"priceAdjustment":2,
"joinedGoodsId":[1],
"extraInfo":[]
}]
}
计算结果还要获取到商品和活动的其它详情信息,把这个结果返回给购物车和订单后,由调用方根据各自的需求解释成所需的结构。
由于策略的输入输出都是一样的,所以理论上所有策略是可以任意组合的。也就是说可以随意往架构里增加新的促销方式或减少促销方式。例如想是增加一个商品兑换券的促销方式,只需要在3级和4级促销之间增加一个主策略,再在这个主策略里增加兑换券的子策略就可以了。
数据库设计
首先把优惠券跟其它活动分开来,因为优惠券是属于用户资产类型,用户要先拥有了券才能参加活动。然后会员折上折和会员积分是由用户属性决定的,不需要在促销里另外保存。
除此之外的其它促销都满足“在某个时间内购买了某个商品,就可以优惠多少元或者获得赠品”这样一个结构,这些活动都可以放到一个表里保存。
把促销活动的公有信息放到主表里,各种不同活动的特有信息放到各自的主表里,子表的id就使用主表的id。如果使用Doctrine作为ORM可以很方便地实现这种架构。
优惠券的数据表也使用相同的结构,在主表里保存券的通用配置,在子表里保存满减券、满折券、兑换券等不同券的信息。
其它问题
活动的一般格式是买满多少元/多少件,可以减少多少元/打多少折/获得什么赠品,可以把这些抽象成活动条件和活动结果两类策略,在各种不同的促销里都可以调用。
不同活动叠加时,后面的活动可以按照原价来计算优惠,也可以按照实付价来计算,也可以从某一层级开始用实付价。
活动的可参与人群可以按用户标签以及标签的交并补运算进行配置。参与活动的商品范围一般按品牌、品类、商品id进行配置,或者这些属性的交并补运算。这两个配置的集合运算的实现方式将在后面的文章详细说明。
以上只是促销活动的大致架构,在实际开发中由于各种促销之间有很大的差异,这个架构还要做很多兼容处理。