Appearance
小程序商品活动金额计算
核心流程
小程序进入预下单页面之前,涉及的所有金额的计算均有前端进行计算。
进入预下单页面之后的金额由后端的preAddOrder
接口计算。接口返回与上一期pos 支付的内容是一致的。 这块不是本次核心讨论点。
为什么要如此设计
少调用几次接口,减轻后端压力
如果放在后端计算,那么每次购物车变动,都会调用一次接口,造成的请求数量将是成指数级别的上升,根本顶不住。
能够让页面操作更加流程
如果每次购物车变动都需要等待几百毫秒才能计算出价格,整个操作流程是非常卡顿和有割裂感的。放前端计算能够让用户在操作的时候更加丝滑。
app 和 小程序下单流程是类似的,app 为什么不采用这种模式
app 体量还是相对比较小,全国就几千家门店,接口完全顶得住。
维护成本变高,一旦逻辑变更,需要两端的开发同学一起同步更进维护,在体量小的情况下,恶心的事情一个人做就好了。
都是自己人,下单的流程金额出现的慢一点没事。
商品活动类型
商品活动主要有是三种:
后续 CodeReview 主要也针对这块代码进行 CodeReview
限时折扣
STRAIGHT_AT
没有任何门槛,好比于一个特价 6 元汉堡 🍔,直接购买就是 6 元。
🌰:
只要购物车有这个 🍔,价格均以 6 元进行计算
满减折扣
FULL
满足一定数量,才支持折扣,如:够买 N 件 其中 M 件享受优惠 🍔
注意:
这里的满减折扣指的不是金额的满减,而是商品数量级别的满减
🌰:
🍟 满 3 件,其中 1 件五折
- 购买 2 件,均原价
- 购买 3 件,1 件五折,2 件原价
- 购买 10 件,1 件五折,9 件原价
每购买 N 件,其中 M 件享折扣
EVERY_FULL
这个场景就相对复杂一点。
🌰:
🍔 每满 3 件,其中 2 件享五折。
- 购买 2 件,则均原价
- 购买 3 件,2 件五折,1 件原价
- 购物 5 件,2 件五折,1 件原价
- 购买 7 件,4 件五折,3 件原价
核心参数
这里我们以小程序视角来看:
商品所有的数据均来源于小程序端的store/product/mini/query
这个接口,活动信息存放在 activitys 这个数组中。
虽然是数组,但是这块好像是只会返回一条数据,因为一个商品只会享受一个活动。
其中最为关键的数据是activityId
和extJson
字段。
activityId
这个字段用于判断这个商品是否所属于哪个活动。
不同的商品可以共享同一个活动
🌰:
🍔和🍟都共同参与满二,其中一件半价的活动。所以这时时候只要这么情况就可以触发这个活动:
- 两个🍔
- 两个🍟
- 一个🍔和一个🍟
extJson
这个字段,其返回的是一个JSON化的字符串,需要我们手动JSON.parse
处理一下。
q:为啥要这样设计?是为了数据量小一点加快请求速度吗?
这个字段是一个还是相对大的数据:
ts
{"activityCanBuyTotal":2147483647,"addPriceFlag":false,"channel":"1,3,2","cornerMark":null,"cornerMarkBackgroundColor":null,"cornerMarkFlag":false,"cornerMarkFontColor":null,"discountConditionType":"STRAIGHT_AT","discountConditionValue":1,"joinActivityLimit":null,"joinDayLimit":5,"originalPrice":null,"originalPriceFlag":true,"price":33.80,"priceSort":"","priceType":"SPECIAL_PRICE","productLimit":"SINGLE","productQuantityLimit":5,"showActivityPrice":33.80,"todayCanBuyTotal":5,"userType":"CARD"}
{"activityCanBuyTotal":2147483647,"addPriceFlag":false,"channel":"1,3,2","cornerMark":null,"cornerMarkBackgroundColor":null,"cornerMarkFlag":false,"cornerMarkFontColor":null,"discountConditionType":"STRAIGHT_AT","discountConditionValue":1,"joinActivityLimit":null,"joinDayLimit":5,"originalPrice":null,"originalPriceFlag":true,"price":33.80,"priceSort":"","priceType":"SPECIAL_PRICE","productLimit":"SINGLE","productQuantityLimit":5,"showActivityPrice":33.80,"todayCanBuyTotal":5,"userType":"CARD"}
解析后得到这么一串数据:
ts
{
"activityCanBuyTotal":2147483647, // 活动的商品总数量
"addPriceFlag":false, // 加料是否算进优惠
"channel":"1,3,2",
"cornerMark":null,
"cornerMarkBackgroundColor":null,
"cornerMarkFlag":false,
"cornerMarkFontColor":null,
"discountConditionType":"STRAIGHT_AT", // 折扣类型
"discountConditionValue":1,
"joinActivityLimit":null,
"joinDayLimit":5,
"originalPrice":null,
"originalPriceFlag":true,
"price":33.8,
"priceSort":"ASC", // 商品排序的顺序
"priceType":"SPECIAL_PRICE",
"productLimit":"SINGLE", // 活动是否支持跨商品
"productQuantityLimit":5, // 商品可享受的数量
"showActivityPrice":33.8,
"todayCanBuyTotal":5, // 用户当天最多可以买的数量
"userType":"CARD"
}
{
"activityCanBuyTotal":2147483647, // 活动的商品总数量
"addPriceFlag":false, // 加料是否算进优惠
"channel":"1,3,2",
"cornerMark":null,
"cornerMarkBackgroundColor":null,
"cornerMarkFlag":false,
"cornerMarkFontColor":null,
"discountConditionType":"STRAIGHT_AT", // 折扣类型
"discountConditionValue":1,
"joinActivityLimit":null,
"joinDayLimit":5,
"originalPrice":null,
"originalPriceFlag":true,
"price":33.8,
"priceSort":"ASC", // 商品排序的顺序
"priceType":"SPECIAL_PRICE",
"productLimit":"SINGLE", // 活动是否支持跨商品
"productQuantityLimit":5, // 商品可享受的数量
"showActivityPrice":33.8,
"todayCanBuyTotal":5, // 用户当天最多可以买的数量
"userType":"CARD"
}
这里最为关键的是这么六个字段:
discountConditionType
商品活动类型:限时折扣、N减折扣
上面详细介绍过,这里不过过多介绍
productLimit
商品限制:"SINGLE"||"MULTI"
商品活动是否支持跨商品
当🍔和🍟都支持5折活动。且满3减,其中2件参与互动
SINGLE
只有至少3件🍔,或者3件🍟才能满足活动,如果1个🍔两个🍟是不能享受活动的。
MULTI
🍔或者🍟,只要二者加起来有3件就可以参与活动。如1个🍔和2个🍟。
priceSort
价格排序:"ASC"||"DESC"
当🍔(10元)和🍟(7元)都享受这个活动时,这时候到底用哪个商品参与活动呢?就是根据这个字段进行决定的
ASC
从高到底参与活动。则此时🍔参与活动
DESC
从低到高参与活动。则此时🍟参与活动
addPriceFlag
加料是否算进优惠
这个是给其他品牌打工的字段~ 我们涉及的加料的好像只有一两家门店。
🌰:
一杯普通奶茶(10元)可以加小料,如:+珍珠(2元)、+椰果(3元)、+芋圆(3元)。
奶茶设置了特价五折的活动。
- 当加料算进优惠
- 奶茶 = 10/2 = 5
- 奶茶+珍珠 = (10+2)/2 = 6
- 奶茶+珍珠+椰果 = (10+2+3)/2 = 7.5
- 当加料不算进优惠
- 奶茶 = 10/2 = 5
- 奶茶+珍珠 = (10/2)+2 = 7
- 奶茶+珍珠+椰果 = (10/2)+2+3 = 10
- 当加料算进优惠
productQuantityLimit
商品可参与活动的限制
🍔有5折限时折扣活动。
如这个限制是5,则就算是限时折扣活动,就算加购了10个🍔,最多也只有5个🍔能够参与活动。
todayCanBuyTotal
对于用户角度,今天还可以享受的活动次数。
与 productQuantityLimit 解析类似
activityCanBuyTotal
对这个活动总共可以卖的🍔数量。
以上的字段单独来看有的就有一些复杂了,但是他们如果组合起来,那个它的总体活动就机制复杂了。
核心计算逻辑如下:
维护一个 ActivityMap,key: cartItem.id value: 可参与活动的数量
ts
export let ActivityMap: { [key in string]: number } = {}
export function clearActivityMap() {
ActivityMap = {}
}
export let ActivityMap: { [key in string]: number } = {}
export function clearActivityMap() {
ActivityMap = {}
}
获取购物车中相同类型商品的数量
getSameTypeCartItems
获取每个项可参与活动的数量
这一块的处理前端是比后端更复杂的,如+珍珠和奶茶和+椰果的奶茶,在前端的购物车中是两项,而对于后端来说是同一个商品。
计算金额
小试牛刀:
了解了字段之后,我们来小看一个🌰,一起来计算下,看看一个相对复杂的场景应该怎么算。
🌰:
🍔(10元)支持(+加肉饼5元)和🍟(8元)支持(+10根3元)都参与每满3件其中2件5折的活动,且最多一个只能5个商品享受,此时购物车加购了4个🍔,4个🍟。
后台相关
活动相关的配置在:营销 => 提升客单 => 限时折扣||购N件享折扣
其中相关配置均在此处进行设置: