Skip to content

小程序商品活动金额计算

核心流程

小程序进入预下单页面之前,涉及的所有金额的计算均有前端进行计算。

进入预下单页面之后的金额由后端的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 件原价

核心参数

这里我们以小程序视角来看:

image-20231225094555009

商品所有的数据均来源于小程序端的store/product/mini/query这个接口,活动信息存放在 activitys 这个数组中。

虽然是数组,但是这块好像是只会返回一条数据,因为一个商品只会享受一个活动。

其中最为关键的数据是activityIdextJson字段。

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 = {}
}
  1. 获取购物车中相同类型商品的数量 getSameTypeCartItems

  2. 获取每个项可参与活动的数量

    这一块的处理前端是比后端更复杂的,如+珍珠和奶茶和+椰果的奶茶,在前端的购物车中是两项,而对于后端来说是同一个商品。

    image-20231225152022224

  3. 计算金额

小试牛刀:

了解了字段之后,我们来小看一个🌰,一起来计算下,看看一个相对复杂的场景应该怎么算。

🌰:

🍔(10元)支持(+加肉饼5元)和🍟(8元)支持(+10根3元)都参与每满3件其中2件5折的活动,且最多一个只能5个商品享受,此时购物车加购了4个🍔,4个🍟。

后台相关

活动相关的配置在:营销 => 提升客单 => 限时折扣||购N件享折扣

image-20231225144928486

其中相关配置均在此处进行设置:

image-20231225150309742