一、背景

官网商城在双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. python基础之os模块操作

    # os模块 目录相关内置库import os# . 当前目录 .. 返回上一级目录# 1. os.path.abspath() --获取当前文件的绝对路径(不包含os模块.py) pwd# path ...

  2. Requests方法 -- 参数化

    import requests#禁用安全请求警告from requests.packages.urllib3.exceptions import InsecureRequestWarningreque ...

  3. MySQL检查与性能优化示例脚本

    最近在玩python,为了熟悉一下python,写了个mysql的检查与性能优化建议的脚本. 虽然,真的只能算是一个半成残次品.也拿出来现眼一下. 不过对于初学者来说,还是有一定的参考价值的.比如说如 ...

  4. RHEL7配置端口转发和地址伪装

    说明:这里是Linux服务综合搭建文章的一部分,本文可以作为Linux上使用firewalld做端口转发和地址伪装以及外网访问内网的参考. 注意:这里所有的标题都是根据主要的文章(Linux基础服务搭 ...

  5. SAML 2.0简介(1)

    1.什么是SAML: SAML是Web浏览器用来通过安全令牌启用单点登录(SSO)的标准协议 2.优点: 跨多个应用程序管理用户身份和授权. 3.单点登录(SSO)是什么: 它使用户仅使用一组凭据(用 ...

  6. Matplotlib不能显示中文和正负号的问题

    参考链接:https://www.jianshu.com/p/240ea3ae0dc9 在使用matplotlib画饼状图时,遇到了如下问题 UserWarning: findfont: Font f ...

  7. Intouch 关于报表数据的一种思路

    在熟悉Intouch项目有一段时间了,也做有相关的三个项目,关于Intouch的一些报表数据的采集,也有了自己一定的看法(主要还是因为自己是野路子)今天就把我常用的一种制作思路,提供给大家.(仅供参考 ...

  8. js 跨域请求失败

    注:错误返回:Failed to load http://xxxxxxxxxxx: No 'Access-Control-Allow-Origin' header is present on the ...

  9. K-Fold 交叉验证

    转载--原文地址 www.likecs.com 1.K-Fold 交叉验证概念 在机器学习建模过程中,通行的做法通常是将数据分为训练集和测试集.测试集是与训练独立的数据,完全不参与训练,用于最终模型的 ...

  10. JUC学习笔记(四)

    JUC学习笔记(一)https://www.cnblogs.com/lm66/p/15118407.html JUC学习笔记(二)https://www.cnblogs.com/lm66/p/1511 ...