一、背景

官网商城在双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全球商城时光机 - 大型促销活动保障利器的更多相关文章

  1. vivo全球商城-营销价格监控方案的探索

    一.背景 现在日常官网商城的运营中有一定概率出现以下两个问题: 1)优惠信息未对齐 官网商城促销优惠的类型越来越多,能影响最终用户实付价的优惠就有抢购.满减.优惠券.代金券等.实际业务操作中存在不同促 ...

  2. vivo 全球商城:电商平台通用取货码设计

    vivo官网商城开发团队 - Zhou Longjian 一.背景 随着O2O线上线下业务的不断扩展,电商平台也在逐步完善交易侧相关的产品功能.在最近的需求版本中,业务方为进一步提升用户的使用体验,规 ...

  3. vivo 全球商城:优惠券系统架构设计与实践

    一.业务背景 优惠券是电商常见的营销手段,具有灵活的特点,既可以作为促销活动的载体,也是重要的引流入口.优惠券系统是vivo商城营销模块中一个重要组成部分,早在15年vivo商城还是单体应用时,优惠券 ...

  4. vivo 全球商城:商品系统架构设计与实践

    一.前言 随着用户量级的快速增长,vivo官方商城v1.0的单体架构逐渐暴露出弊端:模块愈发臃肿.开发效率低下.性能出现瓶颈.系统维护困难. 从2017年开始启动的v2.0架构升级,基于业务模块进行垂 ...

  5. vivo全球商城全球化演进之路——多语言解决方案

    一.背景 随着经济全球化的深入,许多中国品牌纷纷开始在海外市场开疆扩土.实现全球化意味着你的产品或者应用需要能够在全球各地的语言环境使用,我们在进行海外业务的推进时,需要面对的最大挑战就是多语言问题. ...

  6. 从电商平台促销活动看电商app开发趋势

    据亿合科技小编了解到:尽管各大电商平台都进入了品质和品牌时代,但对于消费者来说,低价依然是一个有吸引力的因素.尼尔森<网络购物者趋势研究>报告显示,2016年价格敏感型购物者的比例从15% ...

  7. CJOJ 2482 【POI2000】促销活动

    CJOJ 2482 [POI2000]促销活动(STL优先队列,大根堆,小根堆) Description 促销活动遵守以下规则: 一个消费者 -- 想参加促销活动的消费者,在账单下记下他自己所付的费用 ...

  8. 【CJOJ2482】【POI2000】促销活动

    题面 Description 促销活动遵守以下规则: 一个消费者 -- 想参加促销活动的消费者,在账单下记下他自己所付的费用,他个人的详细情况,然后将账单放入一个特殊的投票箱. 当每天促销活动结束时, ...

  9. 面试作业之浅析京东促销活动核心模型 - DDD

    前言 京东作为中国最大的自营式B2C电商平台,提供一站式综合性购物,服务亿万家庭,涵盖3C.家电.消费品.服饰.家居家装.生鲜和新通路(B2B),满足了消费者的多元化需求.每天都会发布相关的促销活动, ...

随机推荐

  1. linux U盘安装系统工具usb-creator-gtk

    linux平台下U盘安装linux系统的工具.它是ubuntu自带的工具,将建时将覆盖U盘中的所有内容.

  2. Linux服务系统申请SSL证书方法

    inux主要面向专业性较强的技术人员,如果是WEB站点通常采取PHP语言为主选,可选的服务器环境中有Apache.Nginx.Tomcat这几类为主的框架环境,有的图方便会用一些可视化一键式的控制面板 ...

  3. Redis介绍及使用(八)

    一.什么是Redis 1.Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库.(非关系型缓存数据库) 2.五种数据类型: 3.支持持久化. 参看链接:https:// ...

  4. 开发工具IDE从入门到爱不释手(五)更多实用操作

    更多实用技巧 Tabs分屏和独立 日志链接及浏览器 设置浏览器 本地修改历史 三秒钟不操作,自动保存一个版本 右键--Local History--Show History 查看方法调用情况 按住方法 ...

  5. Android系统编程入门系列之界面Activity响应多元的属性动画

    在响应丝滑动画一篇文章中,分别介绍了作用于普通视图.绘制视图的绘制对象.和界面这三种对象的动画效果,但是都有一些使用的局限性.比如这些动画都只是以屏幕上绘制更新的方式绘制动画,并没有真实改变作用对象的 ...

  6. 监控实战之Prometheus

    author:JevonWei 版权声明:原创作品 目录 一 背景 二 部署Prometheus Server Prometheus 主配置文件 targets 节点配置文件 rules 告警规则 运 ...

  7. intouch 10.1出现暂停读取PLC数据问题及其解决

    问题描述 雨水泵站经过3年运行,突发dasmbtcp驱动与施耐德M580系列PLC时断时连问题 原因分析 在经过现场软件重装修复,授权重装,网络通讯状态监视(一直ping PLC IP地址方式)重装d ...

  8. 【开源】这可能是封装微信 API 最全的 .NET SDK 了

    ## 缘起 今年公司某个项目需要全面接入微信支付 V3 版 API.起初觉得,2014 年微信支付就已上线了 V3 版 API,这都 2021 年了,就算官方不给力,怎么着社区也该有几个造好的 .NE ...

  9. linux中的dhcp

    目录 一.DHCP服务 二.DHCP的租约过程 三.使用DHCP动态配置主机地址 四.安装DHCP服务器 一.DHCP服务 ① DHCP (Dynamic HostConfiguration Prot ...

  10. C++ //继承中构造和析构顺序

    1 #include <iostream> 2 #include <string> 3 using namespace std; 4 5 class Base 6 { 7 pu ...