单品活动-领域建设思考
单品活动-领域建设
1. 焦点
梳理电商流程中,单品活动领域相关的内容,以当前公司的「单品活动」领域为基础,概括出如下几点关键内容:
- 边界
- 功能
- 模型
- 架构
说明:
- 这里不涉及商业、公司机密等相关内容。
2. 边界
2.1 什么是活动
这里只做简单说明,活动涉及的领域比较复杂,会有单独的一篇文章进行梳理。
电商中的活动,最直接感受,是反应出来的商品价格,商品在日常售卖的价格是 5元,当在某些指定场景 或 时间下,售卖价格是3元。 这里5元就是商品原价,3元就是商品的活动价格。
活动可以是面向B端商家的(曝光,流量),可以是面向C端用户的(降价,补贴),而活动本身的形式又是多样的。这些这里都不做详细梳理,后面会单独写文章。
2.2 什么是单品活动
单品,即单个商品。单品活动,即只作用在单个商品上的活动。
举例1:商品A,日常售卖价格是5元,由于店铺周年庆,推出半价购买的活动,即在店铺周年庆当天,顾客将以2.5元的价格购得。 店铺周年庆当天商品A以半价销售,就是单品活动。
举例2: 商品A,日常售卖价格是5元,由于店铺周年庆,推出1元钱够的活动,仅限10件,先到先得。即在店铺周年庆当天,前十名购买商品A的顾客将以1元的价格购得。 店铺周年庆当天商品A以1元销售10件,也单品活动。
举例3: 商品A,日常销售价格是5元,由于店铺周年庆,推出店铺内消费满10元,可以换购一件商品A,虽然A是0元购买的,但这种活动,就不能说是单品活动,是属于店铺门槛活动,即满足一定门槛之后,可以享受的活动。
举例4: 商品A,日常销售价格是5元,由于店铺周年庆,推出店铺内商品买够4件,商品总价打6折,虽然A可以用3元购得,但也不是单品活动,属于多拼活动-N件M折。
后面所有提到的 「活动」,若无特指,都是「单品活动」
2.3 边界
只作用在单个商品上的活动,即不管是单独购买这个商品,还是购买多件商品中包括这件商品,都可以享受一个固定的活动价格,而不会因为购买了其他商品,导致以一个特殊的价格购买这个商品。
单品活动的边界:只作用单个商品,不会与其他商品联动,活动本身附着于该商品上的活动。
3 功能
3.1 表现形式和价格形式
单品活动的活动形式主要分为:限时活动,限量活动 单品活动的价格形式主要分为:一口价,折扣
限时活动:活动时间有限,在一定时间范围内商品以某个价格(比原价便宜)进行售卖,时间结束后恢复商品本身的售卖价格。
限量活动:活动数量有限,一定数量的商品,以某个价格(比原价便宜)进行售卖,活动数量售罄后,恢复商品本身的售卖价格。
一口价:一口价不是活动形式,是活动价格计算的一种手段,比如商品原价5元,活动一口价3元,即当商品涨价到6元的时候,活动价仍然是3元。
折扣:折扣也是活动价格的计算手段,商品原价5元,折扣6折,此时活动价是3元,当商品涨价到10元,此时活动价是6元,即活动价随着商品价格涨跌而涨跌。
关键字段:
- startTime
- endTime
- discountType
- activityValue
3.2 活动库存相关
活动库存,就是商品可以用这个活动价格,售卖多少件。
举例:商品A一共10件库存,原价5元,参加了周年庆活动,以3元的价格,卖5件(店铺可能只能容忍亏损5件),这个5件,就是说明「店铺周年庆」活动库存为 5件。
活动库存有两种形式:
- 锁定库存形式
- 共享库存形式
锁定库存举例:商品A一共10件库存,由于商品报名了「店铺周年庆」活动,要以3元的价格卖5件,那么:A商品只能以5元(原价)卖5件,以3元(活动价)卖5件。5件原价的卖光了,就不能用5元买A商品了(因为只能用3元买参加活动的5件)。
共享库存举例:商品A一共10件库存,由于商品报名了「店铺周年庆」活动,要以3元的价格卖5件,那么:A商品只能以5元(原价)卖5件,以3元(活动价)卖5件。5件原价的卖光了,还可以继续以原价购买,直到商品卖光。
区别:锁定库存的形式,会定死以活动卖多少件,以原价卖多少件;共享库存的形式,会将活动的库存和商品库存共享,是以活动最多购买多少件的意思。
活动库存维度有两种:
- 商品维度
- Sku维度
商品维度举例:商品A一共两个Sku(Sku1 售价10元和Sku2 售价20元),Sku1 5个库存,Sku2 5个库存,由于商品报名了「店铺周年庆」活动, Sku1活动价5元,sku2活动价8元,活动库存一共5个,即无论是买sku1 还是买sku2,如果买够5件之后,活动就会被卖光。
Sku维度举例:商品A一共两个Sku(Sku1 售价10元和Sku2 售价20元),Sku1 5个库存,Sku2 5个库存,由于商品报名了「店铺周年庆」活动, Sku1活动价5元,活动库存3件,sku2活动价8元,活动库存2件。即只能以5元活动价买sku1最多买3件,以8元活动价买sku2最多买2件。 当 sku1 和 sku2 的活动库存都卖光的时候,活动售罄。
优缺点:sku维度的活动库存,控制粒度更细,但更复杂。要考虑商品详情,当sku1的活动售罄,sku2的活动没有售罄,sku1是否可以售卖? 商品维度的活动库存,不需要考虑这一点(因为商品所有sku都共享同一个活动库存,并不存在单一sku活动售罄的情况) 。商品维度的活动库存可以避免热点sku活动被抢光,但冷门sku活动没有人买,导致活动效果(销量)不理想的情况,可以增加活动的GMV。
关键字段:
- quantity
- soldQuantity
- reserveQuantity
- abandonQuantity
3.3 多活动叠加
商品不会只参与一个活动,由于业务的发展,一个商品可能同时参加多个活动,所以,多活动叠加之后,怎么进行最优活动的计算(即活动选价逻辑),是单品活动必须要考虑的事情。
两种策略:
- 价格优先
- 业务优先
价格优先指的是:当商品存在多个活动的时候,哪个活动的价格低,优先选择这个活动,它被用户看见,被用户下单享受该活动价。
思考的点1:如果一个商品多个sku同时参加了两个活动,那怎么定义两个活动谁的价格低就是需要思考的问题,通常可以采用比较两个活动中所参与的sku里面,用价格最低的那个进行比较。比如A活动中Sku1卖4元,Sku2卖20元;B活动中Sku1卖5元,Sku2卖15元。由于A活动最低价是4元,B活动最低价是5元,则A活动整体优先选择,被用户使用。
思考的点2:A活动中Sku1卖4元,Sku2卖20元;B活动中Sku1卖5元,Sku2卖15元。 为什么不能Sku1使用活动A的价格4元,Sku2使用活动B的价格5元? 答案是:可以,但对商详不友好。因为商详是Goods维度展示的,同一个goods下,假设sku1是展示A活动价格,sku2是展示B活动价格,那如果商详展示活动氛围,应该展示哪个互动的氛围?(活动氛围有助于增强用户活动心智)
虽然感觉上,sku1是展示A活动价格,sku2是展示B活动价格(每个sku都是当前参与的所有活动中最低的活动价格),但对用户感知活动,不一定有正向的影响。
业务优先指的是:当商品存在不同业务报名活动的时候,有的业务话语权比较高,当存在高业务优先级的活动,会优先出高业务优先级的活动(该逻辑跟公司具体业务有关,算是一种扩展能力)。
总体上,单品活动应该支持多活动叠加,可以在多个活动中选择最优活动供用户使用,活动应该是以商品维度进行比对和选择,最终决定整个商品出一个活动供用户使用,而不是在sku维度进行比对和选择。
3.5 条件筛选
可能需要针对不同的渠道,不同的人群,进行不同活动价展示/使用。
-
需求: A活动只能被渠道1的人使用,B活动只能被渠道2的人群α使用,C活动可以被所有人使用。
-
选价规则:
- 如果存在渠道人群价,优先出渠道人群价,如果存在多个,按多活动并存的规则出
- 如果存在渠道价,再出渠道价,如果存在多个,按多活动并存的价格出
- 出其他活动,如果存在多个,按多活动并存的价格出
根据选价规则,模型上要体现出两个属性,一个是 条件类型(如 渠道价,渠道人群价,人群价)等,第二个是 条件参数(如 渠道值在某个范围内才可以)。
根据选价规则,可以看出整体活动过滤规则顺序为:活动通用有效性过滤(如时间,库存) -> 活动特殊条件过滤 -> 多活动并存出价规则
注意1: 过滤特殊条件过滤中有两种方式,一种是排他性的过滤:比如命中A活动后,如果A活动不再符合后面的过滤条件,则用户享受不到该活动,也享受不到其他活动(因为其他活动已经在 拍他性的条件过滤中筛选去掉了);另一种是不排他性的过滤:比如命中A活动后,如果A活动不再符合后面的过滤条件(即发现用户没有享受任何活动),可以再使用其他活动再走后面的逻辑。
注意2: 排他性的过滤好处是条件过滤和多活动并存出价代码只走一次。坏处是如果过滤选出了一个活动,当该活动在后面的条件中不满足的时候,就不能再选其他活动了。
注意3: 将多活动并存出价的逻辑尽可能的简单,不要放置过滤操作,只做比较操作,即如果存在多个活动价的时候,一定会选出一个最终的活动价(这样就可以避免排他性过滤带来的问题)。所以说:边界清晰的重要性。
问题:所需要的渠道和人群都从哪里来?渠道一般是上游带下来的,人群可以是上游带下来,也可以在条件参数里面冗余人群的查询地址,需要的时候去该地址进行查询。
关键字段:
- conditionType
- conditionParams
3.6 展示控制
某些页面(商详 / 落地页)需要展示某一个点才能购买的活动,在展示和售卖之间的时间段,称为:预热期(展示不可购买期)。
注意:可能是某些页面需要展示某个活动的预热期,也可能是所有页面都展示活动的预热期,而理论上来说,在该页面,只能存在一个需要展示预热的活动(如果存在多个,将会经过活动选择引擎选出之后,出现奇怪的问题),全页面展示预热 和 特定页面展示预热 也应该互斥。 注意:预热也会有开始前预热和结束后预热,都是为了营造氛围。在展示期的活动应该不可被以活动购买(可能允许原价购买)。
关键字段:
- preheatPageFrom
- preheatStartTime
- preheatEndTime
3.7 购买控制
购买控制比较笼统,不同的业态有不同的玩法,在电商交易链路中下单这一环节,在用户点击支付拉起收银台之前,下单流程中会走一次活动校验,活动校验中可以做一些业态相关的校验规则:
- 库存
- 活动时间
- 是否是当前最优活动价
- 是否命中活动限购
这里单独说一下活动限购,即用户在一段时间范围内只能以某个活动购买几次的问题。这个周期可能很长(如整个活动生命周期),也可能是一天。活动限购属于活动信息,但判断用户购买这个动作是否命中限购,可以独立成单独的服务,这个服务可以异步冗余活动上的限购信息,也可以根据上游传入活动限购信息(设计不同而已),然后出是否命中限购的结果给上游,这样限购相关的聚合起来,内部做逻辑会比较方便。
限购演化的复杂一点,可以包括:
- Sku限购
- Sku限购组
- Goods限购
- Goods限购组
- 单品活动限购
- 单品活动限购组
补充:Sku限购组即把多个Sku应用一个限购策略,比如在一天以内,Sku1或Sku2或Sku3,只能购买一件,无论购买了哪件,都不能购买其他剩余两件Sku,其他Goods限购组和活动限购组同理。
3.8 业务标记
活动信息会有一些特定的标记,这些标记可能会影响选价引擎逻辑,可能什么都不影响,只用来带给下游。这些标记可能是boolean,可能是enum。
关键字段:
- options
- optionsVersion
3.9 活动状态机
单品活动本身有着自己的数据状态,状态可以分为主状态和辅状态,主状态主要用作数据筛选使用,辅状态主要用作信息展示使用,每个主状态都会有自己的默认辅状态。
比如:
- 预生效(主状态:code 100)
- 预生效默认(辅状态:code 101)
- 生效中(主状态:code 200)
- 生效中未到开始时间(辅状态:code 201)
- 生效中已到开始时间(辅状态:code 202)
- 生效中已售罄待补货(辅状态:code 203)
- 已废弃(主状态:code 300)
- 已废弃被商家删除(辅状态:code 301)
- 已废弃被运营删除(辅状态:code 302)
- 已废弃被XX系统删除(辅状态:code 303)
- 已结束(主状态:code 400)
- 已结束到结束时间结束(辅状态:code 401)
- 已结束售罄结束(辅状态:code 402)
- 屏蔽中(主状态:code 500)
- 屏蔽中被XX系统屏蔽(辅状态:code 501)
状态机流转:
说明1: 预生效到生效为什么分两步?模仿两阶段提交,预生效到生效本身出错的概率会比较低,创建活动校验逻辑复杂,出错的概率较高。如果流程A和创建活动操作绑定,即只有先创建活动成功后,才能操作流程A,但流程A成功,才能启用活动。 这种就需要先预生效,再启用。(比如报名活动,和上资源位)
关键字段:
- status
- subStatus
4 模型
4.1 ER图
一个活动会创建一个 goods_activity_info
和 多个 sku_activity_price
.
4.2 goods_activity_info 结构
4.3 sku_activity_price 结构
5 附加能力
一般基础服务(比如商品,活动等),大都具备的三个基础能力是:
- 修改流水
- 操作记录
- 消息通知
流水 和 记录 这里被分成了两类东西,作用也不一样,实现也不同。
5.1 流水
类似WAL的效果,跟真正的数据是一个库的,需要做事物,先写数据流水,再做数据更新,如果其中任何一个异常,需要回滚另一个。流水的作用是可以用来做幂等,对操作幂等。每次操作都要有流水id(操作id),写入流水代表本次操作成功,下次用同样的流水id进行操作将不被执行。可执行回滚动作,不过一般用不上回滚,或者回滚会以新的操作来体现。可以根据流水id进行整体链路的数据核对。
注意1: 流水并不是所有信息都要记录,只关注重要的,需要核对的信息(比如活动价格, 活动状态)。
注意2: 流水的数据结构的并不一定要跟DB的数据结构一样,比如可以记录(活动创建:仅是个动作)。
注意3: 流水可以做动作上的幂等,但不能用来做乐观锁等,如果要做乐观锁,还是要使用乐观锁相关方案。
5.2 操作记录 & 消息通知
记录每次操作的结果,比如操作前什么样子,操作后什么样子,谁,什么时候,在哪操作的。记录不需要做事务,不需要阻塞流程。消息通知数据结构和传递的信息跟记录差不多。
问题1: 记录和消息存储的是DTO还是BO,这个可以考虑下。DTO屏蔽了部分业务数据,反而BO暴露了更多业务数据,都不是很好的方案。亦或是可以独立建设消息/记录的DTO,跟正常业务的DTO隔离。
问题2: 流水记录可以依赖消息,消息做通知使用,记录可以反过查询,流水记录的存储不需要局限于MySql,可以考虑NoSQL。
6 系统架构
6.1 整体链路交互
单品活动分别面向B端和C端两类。
- B端:给商家/运营使用,进行单品活动的创建。
- C端:
- 给商品列表,商品详情,下单合同使用,查询当前商品的最优价格。
- 给下单校验使用,校验用户使用的活动信息是否合法(比如活动是否结束,库存是否售罄,活动是否可购买)。
单品活动主要依赖商品服务,有时候还会依赖人群标签等服务(比如某些人可以使用某些活动,通过活动筛选条件实现这种能力)。
6.2 内部选价逻辑
注意1: 在选最优价格之前,将能过滤去掉的都过滤去掉。
6.3 系统物理架构
链路介绍:
- 管理员后台/研发/运营 查询活动的创建&更新记录。
- 商品活动消费活动价变更消息,MQ可以做失败重试。
- 将商品活动变更消息加工(也可不加工)写入到ES中,提供搜索查询能力。
- 商品活动变更广播变更现场,由于只是广播,可以考虑做异步广播(即失败不阻塞主变更流程)。
- 「商品活动Sync Cache For Single」 调用 「商品活动 For single」清理缓存,前提流量不会因为缓存瞬间清除导致穿透且打挂DB。
- 「商品活动Cmp Cache For Single」 调用「商品活动 For single」查询缓存信息,并且比对8链路中查出来的数据,如果发现不一致,则调用「商品活动 For single」清理缓存。
- 商品活动DB变更广播DTS,「商品活动Cmp Cache For Single」接收到DTS直接清理缓存。
- 「商品活动Cmp Cache For Single」直接连商品活动的Slave库扫秒全库,并且调用「商品活动 For single」查询Redis中的数据,并且进行比对。
- Redis缓存未命中后,查询DB数据,并且将DB数据设置到Redis中(可以做防穿透措施)。
- 读Redis中的数据,这一层也可做本地缓存(针对特别热点数据)。
- 商品详情/下单等链路进行查询(QPS较高,对数据延迟不敏感,单商品维度查询)。
- 「商品活动Sync Cache For List」发现DB和HBase/Redis中的数据不一致,则查询「商品活动Base」的最新商品维度的数据。
- 商家/运营端/B端等 小流量且对数据延迟敏感的操作(创建,更新,更新之后的数据获取等)。
- 「商品活动Sync Cache For List」查询商品维度的数据(直接是DB数据)。
- 商品活动Base服务写/读 商品活动数据(直接走DB)。
- 「商品活动Sync Cache For List」将从「商品活动Base」中查询到的数据直接写入「商品活动For List」的Redis和HBase中。
- 「商品活动Cmp Cache For List」将从「商品活动Base」中查询到的最新DB数据写入「商品活动For List」的Redis和HBase中。
- 商品活动DB信息变更,发送DTS通知「商品活动Sync Cache For List」。
- 「商品活动Cmp Cache For List」扫描全库,和PHbase和Codis的数据进行比对。
- 搜/广/推 等超大流量,对数据延迟不敏感,且批量查询,对性能RT有要求的请求。
- 商品活动ForList 先走本地缓存查询,本地缓存Miss的话走Redis。
- 有些请求对数据延迟要求稍高一些,不走本地缓存,直接走Redis。因为本地缓存的更新是被动的。
- Redis数据Miss的时候,穿透到Hbase中。为什么要用HBase?因为HBase容纳数据量特别多,且支持批量查询。