前言
大家好,我是肥宅阿久,很久没更新文章了,最近,公司的会员商品做了一次升级,所有的会员商品增加自定义商品优惠(按人群拥有不同优惠),因此总结一下设计方案以及实现过程,总的来说,实现并不复杂的;
值得注意的是,本文档中所有的截图以及订单/订阅数据都来自测试环境,数据没有任何意义,请关注设计方案本身。
先简单介绍一下我们VIP会员商品的设计逻辑
首先,VIP底层是产品,一个产品(例如单月)可以在赋予价格之后包装成多个商品,而商品需要在哪个渠道包下的哪个国家投放,投放时会修改价格以及一些营销信息,这就是最终售卖的上架商品。
这么设计最好的一点就是复用的逻辑,上架商品复用商品,而商品又复用产品。
产品属性
如上图,产品定义了类型(固定日期/周期扣款)以及发放的VIP天数。
商品属性
而商品是在产品的基础上赋予了价格/名称等信息。
商品优惠方案属性
这里最重要的就是这个优惠ID,这是和谷歌优惠方案匹配的标识。优惠周期是指的这个商品的优惠价格持续几期(针对续订商品)
上架商品/优惠方案
上架商品顾名思义就是,商品在哪个渠道包下的哪个国家进行售卖,这里除了商品本身的属性,还会分各渠道各国家修改商品价格以及一些营销信息和优惠方案,这里的优惠方案也是从商品里带过来的(在原来商品关联的优惠方案上增加了营销信息,优惠倒计时等属性)。
基于上面的产品逻辑表设计
原来的订单/商品表在之前的博文有过介绍:
传送门:
Google 支付订阅商品服务端设计方案
Google 支付订阅补坑之路
增加如下三张表设计
sql
CREATE TABLE `vip_goods_preferential` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`goods_id` bigint(20) NOT NULL COMMENT '商品ID',
`plan` tinyint(2) DEFAULT NULL COMMENT '优惠资格(计划) 0 新用户首购',
`plan_code` varchar(100) NOT NULL COMMENT '优惠计划ID',
`group_tags` varchar(500) NOT NULL COMMENT '群体标签,多个逗号隔开',
`description` text COMMENT '描述',
`price_type` tinyint(2) DEFAULT NULL COMMENT '优惠价格类型',
`settlement_cycle` int(11) DEFAULT NULL COMMENT '结算周期',
`sale_price_config` text NOT NULL COMMENT '商品各国家优惠价格配置',
`sort` int(11) DEFAULT NULL COMMENT '顺位',
`enable` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否启用 0 未启用,1启用',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `index_goods_id` (`goods_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='会员商品优惠方案';
sql
CREATE TABLE `market_vip_goods_preferential` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`goods_preferential_id` bigint(20) NOT NULL COMMENT '商品优惠ID',
`market_vip_goods_id` bigint(20) NOT NULL COMMENT '上架商品ID',
`goods_id` bigint(20) NOT NULL COMMENT '商品ID',
`local_price` int(11) DEFAULT NULL COMMENT '本地价格',
`local_unit` varchar(100) DEFAULT NULL COMMENT '本地币种',
`dollar_price` int(11) DEFAULT NULL COMMENT '美元价格',
`subscript` text COMMENT '角标多语言',
`description` text COMMENT '描述多语言',
`short_desc` text COMMENT '短描述多语言',
`popup_desc` text COMMENT '弹窗描述多语言',
`limited_time` bigint(20) DEFAULT NULL COMMENT '商品限时(时间戳)',
`sort` int(11) DEFAULT NULL COMMENT '顺位',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `index_goods_id` (`goods_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='会员上架商品优惠方案';
sql
CREATE TABLE `user_vip_goods_preferential_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`market_vip_goods_id` bigint(20) NOT NULL COMMENT '商品ID',
`market_vip_goods_preferential_id` bigint(20) NOT NULL COMMENT '商品优惠ID',
`business_order_sn` varchar(255) DEFAULT NULL COMMENT '关联业务订单号',
`settlement_cycle` int(11) DEFAULT NULL COMMENT '优惠周期',
`start_time` bigint(20) NOT NULL COMMENT '商品优惠计时开始',
`end_time` bigint(20) DEFAULT NULL COMMENT '商品优惠计时结束',
`used` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否使用 0 未使用,1使用',
`preferential_detail` text NOT NULL COMMENT '固化商品优惠详情',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `index_goods_user` (`market_vip_goods_id`,`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户优惠记录';
之前的 vip_order 表需要增加一个 offer_id(优惠ID),有优惠的话谷歌会携带优惠信息。
vip_renewal_order 表增加如下字段: offer_id(优惠ID)、discount_periods(折扣期数)、discount_period_price(折扣期价格)、remain_discount_periods(剩余可用的折扣期数)
简单介绍一下这些字段的作用,offer_id 固话续订使用的是哪个优惠,discount_periods(折扣期数)、discount_period_price(折扣期价格)也都是固化折扣优惠的信息,remain_discount_periods(剩余可用的折扣期数)用来判断优惠的续订价格还能用几期,每续订一期 remain_discount_periods 减一,直到remain_discount_periods为0。
那直接用 discount_periods(折扣期数) 不行吗?
主要是有这样的业务场景,谷歌优惠方案替换,下面再说。
谷歌如何给商品配置优惠方案
目前支持的谷歌后台配置是:
1、资格条件: 由开发者控制 & 阶段类型: 扣款周期/单次扣款;
2、资格条件: 新客户获取 & 阶段类型: 单次付款
在订阅商品里面可以添加优惠:
添加优惠阶段:
类型:周期性付款折扣/单次扣款折扣/免费
以上配置就可以完成谷歌商品的优惠方案了,我们选择的资格条件是 由开发者控制,所以这个时候用户满足哪些优惠方案完全是由我们服务端控制了,不再是谷歌控制的; 服务端会检查用户满足商品的哪个优惠,然后将优惠ID给客户端,安卓拿到我们给的优惠ID去谷歌那边配置的优惠方案中获取优惠价格。 我们的设计是每次只会返回给客户端一个优惠ID。
如果选择的资格条件是 新客户获取,那用户是否有优惠是由谷歌来判断的。
谷歌回调的变化
那用户匹配了优惠下单之后,我们怎么拿到对应的优惠ID呢? 我们拿着谷歌回调的数据去请求谷歌服务器会得到对应的优惠ID,如下图:
通过这个offerId以及商品code,我们可以拿到用户购买的商品以及优惠价格。
但是值得注意的事: 谷歌会携带优惠ID,每期都会带来,即使没有优惠了。比如:我在谷歌后台配置的优惠结算周期是3期,那续订第4期,我们请求谷歌服务器数据还是能得到这个offerId。
所以我们自己续订需要维护优惠折扣几期;
优惠变更问题
假如一个上架商品配置了多种优惠方案,正好整个用户可以使用多种优惠方案,那么当用户使用了一种优惠方案后,再次购买本商品会触发优惠方案的变动;
但是注意,只是优惠方案变动,订阅还是沿用老的订阅。
为了解释清楚,我把谷歌扣款的账单拉出来,如上图,最下面的三笔是一个折扣2期的优惠方案,三期之后,价格恢复,之后用户再次购买本商品,发生优惠方案变更,此时将收到一个账单为0的订阅(服务端也会收到一个新的订阅,本次订阅会员权益的时间并没有增加),然后将以新优惠方案继续订阅。
所以为此,我在 vip_renewal_order 增加 discount_periods(折扣期数)和 remain_discount_periods(剩余可用的折扣期数)两个字段。
原来是通过 续订期数 index 和 discount_periods(折扣期数)比较来判断是否有优惠的,即 index <= discount_periods 就还有优惠,但是发生优惠变更之后需要重置 discount_periods(折扣期数),而 续订期数(index)是不断增加的,这样就无法判断是有优惠了,因此单独维护一个 remain_discount_periods(剩余可用的折扣期数)字段,只要 remain_discount_periods > 0 就还有优惠。
当修改订阅方案时,会重置 vip_renewal_order 的折扣信息。
还有另外一个问题,谷歌的一次性扣款应该是不支持优惠方案的。
h5 优惠方案
我们H5订阅使用了渠道是 payermax 和 pingpoing。
payermax 不支持几期的优惠方案,他们只支持首期优惠,而且他们的订阅业务已经下架了,我们接入的比较早,还是能用的,但是比较局限吧。
pingpong 扣款的时间和扣款的费用完全是由商户决定的,那么通过 vip_renewal_order 的折扣价/可用折扣期数很容易就做到商品优惠的方案了。
好了,谷歌/h5的优惠方案就到此结束了,是不是很简单。