vivo全球商城时光机 - 大型促销活动保障利器
一、背景
官网商城在双11、双12等大促期间运营同学会精心设计许多给到用户福利的促销活动,当促销活动花样越来越多后就会涉及到很多的运营配置工作(如指定活动有效期,指定活动启停状态,指定活动参与商品等等)。
如果因为某些原因导致其中部分配置未按预期配置,等到大促那一刻才发现配置没有正确配置,这样大概率会流失不少订单,同样也可能会出现错配优惠导致一些本不该享受的优惠也被用户享受到,可能会给商城带来比较大的损失,因此为了尽量减小前面这些情况的发生的概率,我们就想能不能提供一种能力,让运营同学在重要的电商大促正式开始前,提前去校验所有期待的优惠是否配置正确。
二、构思
想让运营同学能去校验所配的大促优惠是否正常,同时又希望不会增加多余的额外工作,如何做到呢?
考虑到电商业务的特殊性,所配置的各种大促优惠最终主要都会体现在优惠后的价格上,因此我们考虑从这个角度去实现。
在电商的核心链路上,主要有商详页、购物车、确认订单、提交订单这几个核心场景,那么只需在这几个场景中实现提前看到优惠后的价格即可判断大促优惠是否配置正确。
那现在的关键问题是如何做到「提前」看到呢?在前序的促销系列文章我们介绍了计价中心的建设,计价中心统一收口了所有的优惠价的计算能力,因此我们只要让计价中心能提供「提前」的能力即可。
计价中心计算优惠价正常只会实时计算当前时间商品能够享受的各种优惠,并将最终优惠价告诉上游业务方,所以我们能让计价中心能够计算「未来某个时间点」的优惠价即可,而计价中心在计算优惠价时,依赖的一个关键信息是「当前时间」,因此我们只要将所谓的「当前时间」进行「篡改」变成未来的某个时间点,实现我们所谓的穿越的目的。
还有一个极为重要的点需要关注,也是这个穿越能力的大前提,就是不能影响线上正常交易,即不能让正常的普通用户也「提前」看到未来的优惠价。
因此如何做到既让运营体验又不影响正常用户呢?我们考虑采用白名单机制,只针对已登录且用户id在白名单中的用户才能进行所谓的穿越体验。
在确定大体思路后,还有一些问题需要确认:
对于穿越的完整体验是否只需要商城购物流程?
如果需体验大促期间整个官网商城的所有氛围,可能涉及改动的点较为多,比如大促宣传活动页面、专属聚合类商品页面,简化版的只关注整个购物下单流程。
整个穿越过程是否需要真的要真实创建订单?
由于穿越时光后,用户的下单时间和确认订单的时间是一致的,因此确认订单页的所有优惠及最终的价格是真正的所见即所得,无需真实下单即可获知所有优惠活动信息
所以在提交订单的时候建议直接阻断并提醒用户“您当前处于时空穿越,请回到现实中再下单哦”,并不作真正的创建订单,也就不会作后续许多写资源的连锁操作,同时这种情况下也会减少很多不必要的改动点。
对于穿越过程中领取的用户特殊券是否需要作特殊标识?
a)穿越过程中领取的券,如果作了特殊标识,那么退出时光机后,到了优惠券真实可用期后,应建议不作使用,防止占用普通用户资源,同时这种情况下也不建议增加优惠券已发券数量。
b)穿越过程中领取的券,不作特殊标识,那么退出时光机后使用该券与其他正常领取的券并无差别,这种情况算是占用了普通用户资源,那么相应的也建议增加优惠券已发券数量上。
a方案需要优惠券系统作相关的适配改动,但线上真实资源无任何污染或占用;b方案无需作任何改动,但会侵占极少量真实资源,如果运营方觉得问题不大建议采用b方案,从项目角度成本最小。
三、实现
3.1 核心流程图
根据前述的构思方案,得出如下商城穿越核心购物流程:
3.2 改造重点
从上述流程图中可以看出改造的重点:
白名单信息的维护
获取「当前时间」
3.2.1 白名单信息维护
为方便后续穿越用户时间信息共享,我们将此信息(openId: travelTime)存储在配置中心中,并提供相应的管理台方便设置穿越用户及穿越时间点。
3.2.2 获取「当前时间」
整个上下游关联系统可能都会需要获取「当前时间」,而获取「当前时间」需要能获取到配置的白名单信息以及当前用户信息。显然为了各个业务系统能尽可能减少代码变动,获取「当前时间」适合做到一个公共模块中,各个业务系统依赖这个公共模块自动具备能获取所期待的「当前时间」。
因此集成了时光机模块后的整个业务系统链路关系如下所示:
3.2.3 时光机模块
从前述内容,我们可以得出时光机模块(vivo-xxx-time-travel)中需要包含的主要能力:
a )穿越用户白名单信息
b )获取「当前时间」
c )读取、设置上下文openId
其中a、b的实现都比较简单,只需正常接入公司的配置中心,并根据指定openId获取「当前时间」即可,比较麻烦一点的是获取「当前时间」时的这个用户openId信息。
之前的各个业务系统间的接口调用可能是不需要用户openId信息的,但现在穿越用户是指定白名单用户的,所以必须要将入口链路检测到的用户openId信息一路向下传递到下游的各个业务系统中。
方案一:各个业务系统间接口调用耦合openId信息,需要各个业务系统全部都改造一遍,显然这个方案比较初级原始也对各业务方非常不友好,非常不建议采用。方案二:由于我们后端各个业务系统间都使用dubbo进行接口调用,因此我们可以利用dubbo基于spi插件机制的定制业务过滤器将openId当作附加接口调用时的附加信息进行透传。(如果是其他接口调用方式的,也建议采用类似原理的处理方式)
下面我们就看下时光机模块中一些核心的代码实现:(当前业务系统作为消费方时执行的过滤器)
当前业务系统作为消费方时执行的过滤器
/**
* 当前业务系统作为消费方时执行的过滤器
*/
@Activate(group = Constants.CONSUMER)
public class BizConsumerFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
if (invocation instanceof RpcInvocation) {
String openId = invocation.getAttachment("tc_xxx_travel_openId");
if (openId == null && TimeTravelUtil.getContextOpenId() != null) {
// 作为消费方在发起调用前,如果缺失openId,则设置上下文的openId
((RpcInvocation) invocation).setAttachment(openIdAttachmentKey, TimeTravelUtil.getContextOpenId());
}
}
return invoker.invoke(invocation);
}
}
当前业务系统作为服务提供方执行的过滤器;
/**
* 当前业务系统作为服务提供方时执行的过滤器
*/
@Activate(group = Constants.PROVIDER)
public class BizProviderFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
if (invocation instanceof RpcInvocation) {
String openId = invocation.getAttachment("tc_xxx_travel_openId");
if (openId != null) {// 作为下游服务提供方,获取上游系统设置的上下文openId
TimeTravelUtil.setContextOpenId(openId);
}
}
try {
return invoker.invoke(invocation);
} finally {
TimeTravelUtil.removeContextOpenId();
}
}
}
穿越时间获取工具类;
/**
* 穿越时间获取工具类
*/
public final class TimeTravelUtil {
private static final ThreadLocal<TimeTravelInfo> currentUserTimeTravelInfoThreadLocal = new ThreadLocal<>();
private static final ThreadLocal<String> contextOpenId = new ThreadLocal<>();
public static void setContextOpenId(String openId) {
contextOpenId.set(openId);
setUserTravelInfoIfExists(openId);
}
public static String getContextOpenId() {
return contextOpenId.get();
}
public static void removeContextOpenId() {
contextOpenId.remove();
removeUserTimeTravelInfo();
}
/**
* 设置当前上下文用户穿越信息,如果存在的话
* @param openId
*/
public static void setUserTravelInfoIfExists(String openId) {
// TimeTravellersConfig 会接入配置中心,承载所有白名单穿越用户信息配置,并将每一个穿越用户信息转换为TimeTravelInfo
TimeTravelInfo userTimeTravelInfo = TimeTravellersConfig.getUserTimeTravelInfo(openId);
if (userTimeTravelInfo.isInTravel()) {
currentUserTimeTravelInfoThreadLocal.set(userTimeTravelInfo);
}
}
/**
* 移除当前上下文用户穿越信息
*/
public static void removeUserTimeTravelInfo() {
currentUserTimeTravelInfoThreadLocal.remove();
}
/**
* 当前链路上下文是否处于穿越中
* @return
*/
public static boolean isInTimeTravel() {
return currentUserTimeTravelInfoThreadLocal.get() != null;
}
/**
* 获取「当前」时间,单位:毫秒。
* 若当前是穿越中,则返回设置的穿越时间,否则返回实际系统时间
* @return
*/
public static long getNow() {
TimeTravelInfo travelInfo = currentUserTimeTravelInfoThreadLocal.get();
return travelInfo != null ? travelInfo.getTravelTime() : System.currentTimeMillis();
}
}
用户穿越信息
/**
* 用户穿越信息
*/
public class TimeTravelInfo {
/**
* 当前是否处于穿越中
*/
private boolean isInTravel = false;
/**
* 当前穿越时间点,仅在isInTravel=true时有效
*/
private Long travelTime;
public boolean isInTravel() {
return isInTravel;
}
public void setInTravel(boolean inTravel) {
isInTravel = inTravel;
}
public Long getTravelTime() {
return travelTime;
}
public void setTravelTime(Long travelTime) {
this.travelTime = travelTime;
}
}
在业务系统依赖这个vivo-xxx-time-travel模块后,凡是需要获取当前时间的地方将原来的System.currentTimeMillis()改为TimeTravelUtil.getNow()即可。
3.4 问题
在时光机能力建设过程中碰到一个比较重要的问题,就是上下文传递openId信息时,会出现跨线程传递丢失问题。
如果底层是Java线程池直接实现异步调用,那通过对线程池相关拦截可以实现上下文复制拷贝传递,我们内部的全链路系统已经通过相关代理技术对线程上下文信息已作了相关处理。如果使用Hystrix实现异步调用,可以看下笔者另一篇专门介绍的文章《Hystrix中如何解决ThreadLocal信息丢失》 。
四、最后
本文介绍的时光机相关能力主要应用在官网商城,但并不局限于电商场景,时光机模块在设计的时候就没有与某个具体业务耦合,因此对于其他一些业务场景也可以适用或者有一些借鉴意义。
另外本文中电商场景中关注的是优惠价格是否正常,基本涉及到的是读操作,如果有些场景需要穿越后进行完整的业务功能操作,如进行实际下单,那么就会涉及到一些写操作,此时可以借助影子库的相关能力去完成完整的穿越操作之旅。
作者:vivo官网商城开发团队-Wei Fuping
vivo全球商城时光机 - 大型促销活动保障利器的更多相关文章
- vivo全球商城-营销价格监控方案的探索
一.背景 现在日常官网商城的运营中有一定概率出现以下两个问题: 1)优惠信息未对齐 官网商城促销优惠的类型越来越多,能影响最终用户实付价的优惠就有抢购.满减.优惠券.代金券等.实际业务操作中存在不同促 ...
- vivo 全球商城:电商平台通用取货码设计
vivo官网商城开发团队 - Zhou Longjian 一.背景 随着O2O线上线下业务的不断扩展,电商平台也在逐步完善交易侧相关的产品功能.在最近的需求版本中,业务方为进一步提升用户的使用体验,规 ...
- vivo 全球商城:优惠券系统架构设计与实践
一.业务背景 优惠券是电商常见的营销手段,具有灵活的特点,既可以作为促销活动的载体,也是重要的引流入口.优惠券系统是vivo商城营销模块中一个重要组成部分,早在15年vivo商城还是单体应用时,优惠券 ...
- vivo 全球商城:商品系统架构设计与实践
一.前言 随着用户量级的快速增长,vivo官方商城v1.0的单体架构逐渐暴露出弊端:模块愈发臃肿.开发效率低下.性能出现瓶颈.系统维护困难. 从2017年开始启动的v2.0架构升级,基于业务模块进行垂 ...
- vivo全球商城全球化演进之路——多语言解决方案
一.背景 随着经济全球化的深入,许多中国品牌纷纷开始在海外市场开疆扩土.实现全球化意味着你的产品或者应用需要能够在全球各地的语言环境使用,我们在进行海外业务的推进时,需要面对的最大挑战就是多语言问题. ...
- 从电商平台促销活动看电商app开发趋势
据亿合科技小编了解到:尽管各大电商平台都进入了品质和品牌时代,但对于消费者来说,低价依然是一个有吸引力的因素.尼尔森<网络购物者趋势研究>报告显示,2016年价格敏感型购物者的比例从15% ...
- CJOJ 2482 【POI2000】促销活动
CJOJ 2482 [POI2000]促销活动(STL优先队列,大根堆,小根堆) Description 促销活动遵守以下规则: 一个消费者 -- 想参加促销活动的消费者,在账单下记下他自己所付的费用 ...
- 【CJOJ2482】【POI2000】促销活动
题面 Description 促销活动遵守以下规则: 一个消费者 -- 想参加促销活动的消费者,在账单下记下他自己所付的费用,他个人的详细情况,然后将账单放入一个特殊的投票箱. 当每天促销活动结束时, ...
- 面试作业之浅析京东促销活动核心模型 - DDD
前言 京东作为中国最大的自营式B2C电商平台,提供一站式综合性购物,服务亿万家庭,涵盖3C.家电.消费品.服饰.家居家装.生鲜和新通路(B2B),满足了消费者的多元化需求.每天都会发布相关的促销活动, ...
随机推荐
- 简单配置nginx反向代理,实现跨域请求
简单配置nginx去做反向代理,实现跨域请求 简单介绍nginx的nginx.conf最核心的配置,去做反向代理,实现跨域请求. 更多详细配置,参考nginx官方文档 先介绍几个nginx命令 打开n ...
- Pytest单元测试框架之setup/teardown模块示例操作
"""模块级(setup_module/teardown_module)开始于模块始末,全局的函数级(setup_function/teardown_function)只 ...
- 对一个sql的分析
select * FROM LPEdorItem a, LCCont b, LPEdorApp c WHERE a.edoracceptno = c.edoracceptno and a.ContNo ...
- 【Web动画】科技感十足的暗黑字符雨动画
本文将使用纯 CSS,带大家一步一步实现一个这样的科幻字符跳动背景动画.类似于这样的字符雨动画: 或者是类似于这样的: 运用在一些类似科技主题的背景之上,非常的添彩. 文字的竖排 首先第一步,就是需要 ...
- 创建函数,传递一个数字n,返回斐波那契数列的第n的值。
斐波那契数列 第1项和第2项的值是1,从第3项开始,每项的值是前两项相加的和 1 1 2 3 5 8 13 21...... 法1: function fn(n) ...
- 如何在 NetCore 中定义我们自己的JSON配置文件的管理器。
一.介绍 微软已经对外提供了新的平台,我们叫它们是 Net Core 平台,这个平台和 Net Framework 平台有本质的区别,这个最本质的区别就是微软的C#代码可以跨平台了.当前我们主流的3大 ...
- 3G/4G串口服务器
Z3G/4G串口服务器 ZLAN8303-7是上海卓岚继ZLAN8100之后推出的3G/4G联网解决方案.支持7模的4G串口服务器.其产品支持Modbus功能.自定义注册包心跳包功能. ZLAN830 ...
- canal同步异常:当表结构变化时,同步失败
场景 canal 同步Mysql一段时间后突然失败,报如如下错误: 2021-08-06 16:16:51.732 [MultiStageCoprocessor-Parser-Twt_instance ...
- JSP的执行原理、JSP的内置对象、四大作用域解析、MVC模式理解>从零开始学JAVA系列
目录 JSP的执行原理.JSP的内置对象.四大作用域解析.MVC模式理解 JSP的执行原理 这里拿一个小例子来解析JSP是如何被访问到的 首先将该项目部署到tomcat,并且通过tomcat启动 通过 ...
- python语法入门
程序=数据+功能 我们学习编程语言的目的是为了控制计算机能够像人一样去做事 所以说,编程语言中出现的所有的语法都是为了控制计算机能够像人一样去做xxx事 一.注释: 1 ...