8年来,五哥一直在美团滴滴猿辅导等公司一直从事虚拟商品售卖业务,从云计算虚拟机,线上课程到会员卡等业务,虚拟商品的模型设计有很多共性问题,今天把我的思考分享给大家
目前互联网用户已突破10亿人,用户增长已经见顶,因此如何提升用户留存率和使用频次成为企业运营的重中之重。
在这种背景下,各大互联网公司开始尝试付费会员和付费优惠券等全新的营销工具。而付费会员和付费优惠券属于典型的虚拟商品。
什么是虚拟商品?
虚拟商品是相对于实物商品而言的一种概念。典型的虚拟商品包括各类会员卡、付费优惠券、在线课程、电子书籍以及各种类别的资格凭证(如团购凭证、门票和电影票等),这些都是虚拟类的用户权益。
目前几乎每家互联网公司都纷纷推出了付费会员服务。像腾讯视频、优酷、爱奇艺等视频平台,还有淘宝的88VIP会员、饿了么会员、美团会员,以及拼多多的省钱月卡、美团的神券券包和神枪手券包等等。此外,一些线上教育公司如猿辅导、好未来等也在售卖线上课程。这些营销工具都属于虚拟商品售卖
除此之外,还包括团购资格、门票、电影票、游戏皮肤道具等任何虚拟的用户权益,都可以在线上售卖,不需要实体发货,统称为虚拟商品售卖。
因为我只负责过付费会员卡、付费券包和付费K12课程这一类虚拟商品售卖,对于其他类型的虚拟商品如电影票、游戏道具和门票,我并不很了解,所以接下来将不再谈论后一类虚拟商品。
虚拟商品售卖的购买流程
为了了解商品结构的设计,必须首先了解虚拟商品购买过程中的关键节点。
从以上流程图可以得知商品结构应该包括如下几类信息
功能分类 | 所属关键节点 |
---|---|
商品基础属性 | 包含商品上下架状态,商品所属业务线,商品类型等 |
商品投放属性 | 用户进入App后,虚拟商品推荐环节依赖虚拟商品的投放策略信息 |
商品销售属性 | 包括原价,当前价等销售信息 |
商品展示属性 | 商品介绍,商品名称,缩略图等 |
商品履约属性 | 主要包括发放给用户的权益信息 |
商品结算属性 | 订单售卖一定涉及财务结算,商品要包含财务信息 |
商品限购属性 | 限制用户购买数量的属性 |
商品库存属性 | 商品总库存,日库存等库存属性(会有单独库存表,这只是库存元数据) |
商品购买流程需要的元数据应来源于商品模型
用户进入App后,给用户推荐虚拟商品,例如推荐线上课程,商品推荐环节需要依赖商品投放信息
用户选中商品预览商品时需要商品头图介绍等展示信息,需要商品价格等销售信息。
用户提交订单后需要扣减商品库存,增加用户已购数量。商品上需要有库存类型,限购类型。
用户支付完成后,需要向用户履约,需要商品中包含权益配置信息。
此外虚拟商品售卖还包括财务结算,所以商品中还需配置财务结算数据。
以上售卖环节中任何和商品相关的配置数据都需要存储在商品结构中。要想设计商品模型,必须对购买过程的任一环节了然于胸。
重点要指出的是,售卖虚拟商品并不是最终目的,最终目的是让用户更好地核销虚拟商品。例如,用户可以通过核销会员优惠券购买实物商品;用户来购买线上课程后可以在线上听课。(相比实物电商,虚拟商品的核销环节发生在线上,而非线下)
商品购买和商品核销两个环节密切相关且至关重要,因此明确两者的边界对于虚拟商品结构的设计至关重要。
下面我将用两个场景来举例说明虚拟商品购买和商品核销这两个关键环节的关联性。在进行模型设计时,我们需要特别关注两个模型之间的边界划分。
商品购买和权益核销是两个环节
付费券包的商品结构与券核销
用户购买虚拟商品后,需要核销附带的用户权益。举例来说,当用户购买付费会员后,会得到会员优惠券,这些优惠券可以用来购买实物商品。与商品模型相比,优惠券本身是一个独立的数据模型,而优惠券的发放则是属于营销领域的问题。因此,在设计虚拟商品模型时要和 "如何设计优惠券的数据模型"这一类问题加以区分。
需要注意:
应避免将商品核销所需的数据全部放在商品结构上,同时更要避免把本属于商品模型的属性放到核销模型里!
会员卡履约配置需要预留一些重要的优惠券发放属性。不同公司的发券接口通常需要指定相似的重要发放属性,下面我举两个例子来说明。
-
指定券模版,券模版包含了券的关键配置属性。另外需指定发放渠道、发放场景、发放个数、发放时间等。
-
指定券渠道模版,渠道模版不光指定了券的关键配置属性,也指定了发放渠道、发放场景、发放个数等配置信息。
在配置虚拟商品配置券的发放属性时,关键属性包括券模版id和券渠道模版id等核心属性即可。在设计虚拟商品模型时不需要需关心优惠券的可用商品类型、金额、使用条件等参数。(如果需要特殊展示券属性,可以在商品结构中预留JSON扩展字段)。
线上课程的商品结构与核销
除了付费券包外,线上课程售卖也属于虚拟商品销售。
虚拟商品在用户导购、提单交易、购买列表和售后退款等环节中起着重要的角色。虚拟商品模型需要与线上课程ID进行关联,具体的关联关系可以根据不同的业务场景来确定,既可以是一对一的关系,也可以是一对多的关系,需要具备一定的灵活性。用户购买商品后,需要在履约环节给予课程的上课凭证。这需要核销领域提供相应的接口,根据用户ID和课程ID来创建可以上课的凭证。
用户购买课程后,可以在课程列表中参与线上课程,这是虚拟商品附带权益的核销环节。在线上教育中,上课是一个关键环节,需要在课程模型中存储大量字段,例如当前科目、年级、初中高中、学季(春夏秋冬)、授课老师、课程难度等。
这些与课程核销环节相关的属性是否应该存储在商品模型中呢?不应该
线上教育这类复杂的虚拟商品售卖场景,【属性优先放在商品上还是放在课程上?】这类问题,引起了很多争议。我举个例子来说明。
课程配套教材信息放到商品模型还是课程模型上?
为提高教学效果,线上课程都会附带实体书教材。用户购买后需要线下发货,所以虚拟商品履约时就需要创建快递运单,配送教材给用户(一般调用第三方快递公司接口,委托快递发货)。
课程配套教材信息放到商品模型还是课程模型上?在回答这个问题前,需要先调研实际场景
履约环节需要教材信息
履约环节需要为用户创建上课凭证,并创建配送单来发送教材。如果课程绑定了教材信息,那么交易履约环节就需要解析课程模型以获取配套教材数据。这种方式在课程领域中是一种侵入性做法,会导致两个领域之间产生严重耦合。
此外,这种做法还限制了购买环节的灵活性!
教材和课程应该分离开
例如,有些家庭有两个孩子,希望他们可以共用一本教材。然而,现在的情况是,教材和课程绑定在一起,用户没有选择的余地。
举个例子,一些课程会在多个学期中使用同一本教材。教材和课程绑定在一起,用户购买多个学期的课程时,就会买到多本相同的教材。这给平台带来了退款和客诉的处理负担。
还有一些其他的场景,比如用户丢失了教材,希望重新购买一本;用户在其他地方借用了一本教材,不希望再额外购买教材等场景。
如果这些问题处理不当,就会引发大量的客诉,并且用户购买的意愿也会降低。
以上的问题和场景都指出了一个问题:教材和课程这两个模型应该分开来。教材本质上是一种商品,是一种实物商品,不需要与课程绑定在一起。
为了支持用户的灵活购买,我们在运营配置课程和商品的过程中,不仅需要配置课程,还需要配置教材的数据。
为了满足用户的需求,我们可以在运营环节创建三个商品,一个商品包含教材,一个商品不包含教材,还需要单独创建一个教材商品。在商品的详细页面中,课程商品与教材商品相关联,引导用户可以单独购买教材。
虚拟商品的SPU
一个课程可选不同的"规格"对应不同的商品,这属于SPU的概念吗?
SKU是最小库存售卖单元,SPU则包含关键售卖属性相同,规格和价格不同的SKU集合。典型区分为:银色内存128G的iphone 14为SKU,iphone 14作为SPU。
虚拟商品售卖的关键售卖属性相同则可认定为是一类SPU,例如同一个课程对应的商品,带教材,不带教材,只有教材等都属于同一个SPU。
同一个SPU的商品可以聚合展示,例如下图,选中任意一个商品,都会展示SPU下所有商品,供用户选择。
商品的组合售卖
课程商品的捆绑售卖
将多个课程合并销售成同一个商品,这是常见的营销手法之一。例如,我们可以将数学和英语课程打包成一个商品,并在商品上进行营销降价活动,从而有效提高客单价,这样的策略通过高频商品促进了低频商品的销售
在商品履约配置方面,我们应该考虑支持多个课程和多个教材的配置。
此外,还存在一种较为复杂的系统实践,即用户同时购买两个商品,使得降价活动需满足特定的组合条件。只有在同一个订单中购买了这两种特定的商品,才能享受降价活动的优惠。然而,这种实现方式非常复杂,大大增加了营销系统的复杂性!因此,不建议采用这种方式!
多权益的会员卡
会员卡不单单只包含一种用户权益,常见有会员优惠券,会员日,会员价,其他推广优惠券(例如美团外卖会员推广骑行卡、汽车券、火车券等)
所以会员卡等类虚拟商品天然要支持 商品上配置多种权益。
虚拟商品的履约配置
虚拟商品的履约配置是由多种权益组合而成,而权益则是对营销资源的一种抽象。只有准确地收集各种营销资源的共性,才能抽象出权益的模型。
首先,权益的基础属性应该包括C端展示名称、B端展示名称、有效期配置(可以选择固定天数、自然月、自然周、自然年等),默认头图、描述和创建人等基本信息。
此外,权益还可能包括其他类型的属性。
RightKey
权益Key用来作为权益的唯一ID,避免使用主键ID。由于虚拟权益并非实物商品,权益数量极其有限,所以权益Key是研发同学分配的。
权益物料类型 rightMaterialType
除会员专享优惠券,其他还有很多种物料类型,例如具备抽奖资格的权益。例如可享受会员价的资格等。除此外权益最终的物料类型多种多样!
权益物料类型包含 优惠券,领取资格,抽奖资格,会员价资格,课程,教材等。
权益的履约方 rightPerformSource
权益是对营销资源的抽象。在履约权益时,需要与具体的营销资源平台进行相应的履约操作。例如,要使用优惠券,就需要到优惠券平台进行履约;参与抽奖或领取资格,则需要到相应的平台完成相应的履约操作(也可以自行建立平台);而课程类需要到课程平台履约,教材则需要通过教材平台进行履约。
需要注意的是,并非所有权益类型的履约方都必须在其他平台进行履约,具体情况需根据实际场景自行决定。例如,抽奖或领取资格类的履约往往可以通过自建平台实现。在线教育领域中,用户与课程的购买关系,以及教材的配送,都是关键模块,因此应由交易团队自行建立履约机制。而优惠券物料则比较特殊,一般由独立的营销平台负责。
因此,履约方类型指的是提供履约接口的相应方。在系统代码实践中,可以为履约方指定相应的策略类。
权益发放时点类型 rightGrantTimeType
某些权益是立即发放,某些权益则需要在某个时机发放。例如每个月的1号,或者购买 1天后。
是否需要配送
对于需要配送的教材类实物商品,要有字段标记。
关键发放属性 rightGrantConfig
权益的关键发放属性是指在发放权益时必须要指定的属性。
举例来说,优惠券就必须要指定券模版ID,而课程权益则必须要指定课程ID等等。除了ID以外,不同权益的发放属性可能会有很大的差别,因此最好使用JSON扩展字段来提高可扩展性。
另外,通用的发放属性还包括发放数量、有效期起止时间等。
使用条件 rightUseCondition
例如某些权益包含使用条件,例如优惠券的使用条件等。这一类数据使用扩展字段承载即可
展示信息 rightDisplayConfig
在展示商品的权益时,我们需要从中获取特殊的展示信息。这些信息可以通过使用扩展字段来承载。
履约配置的其他数据
在履约配置中,除了包含多种权益之外,还需要考虑其他因素。一个重要的因素是有效期。虽然权益本身可能有各自的有效期,但是如果多个权益的有效期不同,那么我们应该根据哪个权益来确定商品的有效期呢?因此,在商品的履约配置中,我们需要明确设定有效期。
权益是否应该有库存?
一般情况下,权益不应该有库存。例如用户支付成功会员后,系统告诉用户:"会员优惠券的库存售罄了",这很不合理。用户支付成功课程后,告诉用户"库存售罄",也不合理。
所以库存一般都是配置在商品上,库存校验和扣减发生在用户提单环节,在用户支付前校验和扣减库存。避免支付成功但库存不足的情况发生!
虚拟商品库存
尽管SKU被认为是最小库存单位,但是SPU也可以配置库存。例如,在线课程会限制学生人数(例如2000人,过多的学生会影响上课效果)。同一SPU下的SKU商品需要共享相同的库存。
这就要求商品库存设计必须非常灵活。既要支持SPU维度,也要支持SKU维度。
在班课商品上,只扣减SPU的库存,而将SKU的库存设置为不限制。教材商品需要独立配置库存,不能共享班课商品的SPU库存。
因为需要区分SPU库存和SKU库存,所以需要将这部分库存类型的元数据储存在商品结构中。商品的库存元数据模型如下所示。
InventoryType 包含:无限制库存,SPU库存、SKU库存,并支持扩展其他类型。
targetId是指共享的目标Id,如果为SPU类型,则为对应的SPU_ID;如果为SKU类型,则为商品模型自己的主键ID即可。当需要扣减库存时,从商品库存配置中获取InventoryType和targetId,就能确定要扣减库存的对象。
付费券包有时段库存
一般付费券包除总库存的需求外,也会有时段库存的需求。例如某个券包每天售卖1万单。这要求库存系统支持分时库存的能力,商品库存元数据上也要有所区分。
periodType:全周期库存、自然日库存、自然周库存、自然月库存、自然年库存。
扣减库存时,获取到periodType,就可以知道扣减日库存还是全周期库存......
商品和库存的关联关系
商品的库存配置元数据需要支持多个库存配置。以付费券包为例,它既要配置总周期库存,也要配置日库存。而课程商品则要扣减SPU库存和SKU库存。
为了满足这些需求,商品库存配置的能力必须支持多个库存配置。最终,库存的元数据配置如下:
虚拟商品限额
虚拟商品限额和库存类似,在提单阶段需要校验限购额度是否足够,同时新增用户的购买数量。发起增加限额的动作前,需要查询商品限额配置信息,拿到商品的限额类型。
限额从有效期看,可包括全周期限额、单日限额、单周限额、一年限额等。时间单位的类型也有24H和自然日之分,365天和自然年之分。
除时间维度外,限额的用户维度也有区分,除限制UserId维度外,还可以限制设备uuid、手机号等其他维度。
以上元数据配置都应该放到商品模型中。
关于限购能力的建设,可以参考本篇文章 # 用户限额系统的7个问题,你能回答吗?
虚拟商品推荐和商品投放模型
虚拟商品推荐是指给不同的用户推荐不同的虚拟商品,比如不同用户可以看到不同的会员卡和课程列表。
不同用户的定义并不是任意两个用户,而是在某些投放维度上两个用户不相同。例如在地级市这个投放维度上,河北省廊坊市和唐山市的会员卡权益和价格是不同的。另外,用户画像也是一种投放维度,比如大学生和城市白领所看到的会员卡也是不同的。
在K12线上教育领域,课程的投放会根据地区、年级、教材版本、学季、科目进行区分。比如在五年级语文春季这个课程中,不同地区和不同教材版本都需要有专门的投放策略。
这些投放维度的组合关联上商品ID就是投放策略,而投放策略和虚拟商品的业务场景、运营策略是密切相关的。因此,没有一种适用于所有情况的通用投放策略。
在系统设计上,投放策略和商品模型是两个独立的模型,不应将投放策略的属性放在商品模型中。
举个例子:我们配置了一个课程商品,其关键属性包括五年级、语文、春季、人教版。当用户进入App后,如何根据用户的年级,在不同的科目、地区和学季中推荐相应的课程商品呢?
把投放维度全部放到商品模型上,对商品进行检索吗?不。为了保持投放策略的灵活性,我们应该从商品模型中抽离出一个独立的投放模型。该投放模型包括科目、地区、学季、年级、教材版本等维度,每个维度都可以有多个可选项。
举个例子,今年在河北地区使用人教版,而明年改为苏教版。此时只需要调整投放策略,将河北地区且使用人教版的投放策略改为苏教版即可。无需调整课程商品的配置,只需调整投放策略。这样做最大程度地保证了投放的灵活性,并实现了商品模型与投放业务逻辑的解耦。
使用投放策略还可以配置实验策略,在同一个投放策略下关联多个商品,并为每个商品配置不同的实验权重,可以利用A/B实验来验证不同商品的售卖效果。
虚拟商品投放策略很多吗?
考虑到线上课程、会员卡、付费券包等业务场景一般都是由平台自己运营管理,而不是商家运营管理,因此虚拟商品的数量非常有限。在线上发布的商品数量更是非常少。
与之相应的投放策略也非常有限。
虚拟商品投放策略与规则引擎
线上教育领域的课程商品投放维度较多但投放策略数量较少,天然适合使用规则引擎实现商品推荐。全量的投放策略保存在本地,有单独的监听器监听投放策略新增、删除、上下线。
每来一个推荐请求,走单独的规则引擎匹配对应的投放策略。
在业务简单时,可以考虑自己实现规则引擎,例如For循环判断投放策略和用户推荐请求是否匹配。
虚拟商品投放策略和 Redis
如果在推荐虚拟商品时存在一个高区分度的投放维度,可以将该投放维度作为主要投放维度,并在Redis中进行维护主投放维度和投放策略之间的关联关系。
例如,对于会员卡而言,可以按照人群或地区的维度进行区分。可以将人群或地区作为主投放维度存储在Redis中。这样,在进行推荐请求时,首先查询Redis,获取一批投放策略列表,然后再根据其他维度进行匹配推荐。
虚拟商品投放的整体流程
全量的投放策略在服务启动时,预热加载到应用服务本地。在运行时,涉及到新增、删除、上下线状态的投放策略binlog变更都需及时处理,更新本地缓存中的投放策略。
如下图中所示
当商品推荐请求到来时,系统可以根据实际情况选择三种方式中的一种来匹配投放策略,包括规则引擎、For循环匹配和Redis主维度匹配。
在这三种方式中,我更倾向于使用For循环匹配投放策略,因为这是一个纯CPU操作,不涉及网络IO和磁盘IO,所以速度会非常快。而且,它还可以轻松实现机器的横向扩展。大家可以根据投放策略的数量级和投放维度来评估选择合适的方案。
成功匹配投放策略后,我们可以从投放策略中获取商品ID列表。由于商品较少的原因,我们可以考虑将商品放到本地缓存中,这样在商品推荐时可以从本地缓存中快速获取商品详情,提高速度。
虚拟商品与本地缓存
由于虚拟商品模型数量较少、很少变动、访问量级较高的特点,非常适合把商品放到本地缓存,提高访问性能。
例如使用Guava实现 本地缓存的代码
scss
public LoadingCache<String, Product> createProductCache() {
return CacheBuilder.newBuilder()
.initialCapacity(10000) // 初始容量
.maximumSize(100000L) // 设定最大容量
.expireAfterWrite(60L, TimeUnit.SECONDS) // 写入过期时间
.concurrencyLevel(8) // 最大并发写操作线程数
.refreshAfterWrite(1L, TimeUnit.MINUTES) // 自动刷新数据时间
.recordStats() // 开启缓存执行情况统计
.build(new CacheLoader<String, Product>() {
@Override
public Product load(String key) throws Exception {
return ProductDao.getProduct(key);
}
});
}
使用Guava 创建了一个初始容量为1W,最大容量为10W的缓存容器,60秒过期,失效后可自动从数据库中加载策略到缓存中。
查询缓存可以使用 get
和 getAllPresent
两个方法,输入商品ID,查询商品数据。
再谈虚拟商品模型和投放策略模型
通过对投放策略的讨论,我们至少应该明白,在C端流程中,不应该把商品投放策略信息放到商品模型中。相反地,我们应该将投放策略抽象成一个独立的实体。
举个例子,对于会员卡商品模型,我们不应该在其中包含投放的地域数据和人群数据。
再举个例子,对于课程商品模型,投放的地域信息也不应该被包含其中。但是像科目和年级这样的信息是课程商品重要的详细信息字段,它们可以放在商品模型中。因为这些字段不仅用于商品推荐和投放,还用于详情页展示、数据检索等场景。
当我们设计商品模型时,一定要有一种认识:要识别出投放的维度和投放策略数据,将相关字段转移到投放策略中,而不是全部堆积在商品模型中。
虚拟商品的属性会有很多吗?
我们常用的拼多多、京东等实物电商平台上,商品种类多达数以千万、亿级,不同商品之间差异巨大,因此会存在大量属性。而实物商品模型的一个重要难点就是设计一个高扩展性、高性能的属性系统。
不同的SPU和不同的品类可能对应不同的属性库。为了解决这个问题,可以建立一个独立的属性管理后台,方便平台运营和商家进行属性的管理。
虚拟商品的种类远没有实物商品那么多,自然不需要海量的属性。虚拟商品中销售属性、履约属性、展示属性等使用扩展字段JSON格式存储即可。无需过度抽象,过度设计。
BC端的数据异构
通过以上分析,我们可以得到四个数据模型:商品模型、核销模型、投放策略模型和权益模型。在C端的商品投放、履约和权益核销等环节都需要使用这四个模型。在运营配置商品阶段,将会生成这四个数据模型。
在电商交易场景中,配置端数据模型与C端数据模型往往存在极大的差异,因为它们面临的使用场景不同。配置端数据模型旨在更方便、更高效、更强大地配置运营策略和商品数据,同时还需要兼顾审核流程、数据校验等非功能性要求。而C端交易流程则面向用户端流量,对稳定性和性能要求更高。
当BC端共用数据模型时,会相互制约。例如,如果希望将运营后台页面进行改版,配置流程的顺序和使用方式可能会发生变化,这就需要相应地改变数据模型。但如果和C端共用模型,这种改动就会影响到C端的稳定性。
因此,一般来说,B端和C端的数据模型是独立的。
当运营配置投放策略、权益、商品配置时,需要B端在审核完成后生成B端数据。此时需要有一个数据异构模块,按照C端定义的标准和协议生成C端的数据模型。
例如,运营在一个页面上配置了10个城市的投放策略,数据异构模块就需要将B端的一条投放配置异构到C端,可能对应着10条投放策略。
为了方便查询和检索,以上四个模型之间会存在大量冗余的字段,数据异构模块也需要保证这些冗余字段的一致性。解决方法就是以B端数据模型为准,当B端模型审核完成后,就将数据异构到C端模型上。
数据异构一般是基于MySQL binlog变更作为事件触发源。
总结
在设计虚拟商品模型之前,我们需要了解商品的购买流程。只有了解购买的每个环节,才能确定商品模型中需要包含哪些元数据。
此外,虚拟商品模型需要与核销模型区分开来,核销相关的属性应该放在专门的核销领域模型中。
与实物商品模型不同,虚拟商品模型一般属于平台自营类,因此配置工作通常由平台运营负责。相比实物商品,虚拟商品的总数、属性和类目都要少得多。
在进行系统设计时,设计者应该从具体的业务场景出发,分析当前商品的库存和限购业务场景,同时分析虚拟商品应该包含的权益种类,以设计出合理的商品模型。