当前位置: 首页 > 后端技术 > Java

vivo全球商城时光机-大型促销活动保障利器

时间:2023-04-01 23:54:49 Java

1.背景官网商城在双11、双12等大促期间运营。学生会精心设计很多促销活动,让用户受益。时间长了,会涉及到很多的运营配置工作(比如指定活动的有效期,指定活动的起止状态,指定活动的参与商品等)。如果因为某些原因,有些配置没有按预期配置,等到大促的时候才发现配置不对,很有可能会丢掉很多订单,有可能也有不匹配的折扣,导致一些不应该享受的。用户享受的也是优惠,可能会给商城带来比较大的损失。所以,为了尽量减少上述情况发生的概率,我们想能不能提供一个能力,让运营类的学生可以参加重要的电子商务专业。在促销活动正式开始之前,提前检查是否所有预期的折扣都配置正确。2.思路是让运营同学检查促销折扣是否正常,同时希望不要有多余的工作。怎么做?考虑到电商业务的特殊性,配置的折扣会主要体现在折扣价格上,所以我们从这个角度考虑实施。在电商的核心环节上,主要有商家详情页、购物车、订单确认、订单提交等几个核心场景。在这些场景下,你只需要提前看到打折后的价格就可以判断价格了。促销折扣是否配置正确。现在的关键问题是如何“提前”看到它?在系列宣传文章的序言中,我们介绍了定价中心的建设。定价中心统一了所有折扣价格的计算能力,所以我们只需要让定价中心提供“预付”能力即可。通常情况下,定价中心对折扣价格的计算只会实时计算出产品当前时间可以享受的各种折扣,并将最终的折扣价格告知上游业务方,所以我们可以启用定价中心计算“未来某个时间点”的折扣价,而定价中心在计算优惠价时所依赖的关键信息是“当前时间”,所以我们只需要“伪造”所谓的“当前时间”进入未来的某个时间点,以实现我们所谓的时间旅行目的。还有一个极其重要的点需要注意,这也是这个穿越能力的大前提,那就是不能影响正常的网上交易,也就是不能让普通的普通用户看到未来的优惠价格”提前”。那么如何在不影响普通用户的情况下让操作体验更好呢?我们考虑采用白名单机制,这样所谓的遍历体验只能对登录过且用户ID在白名单中的用户进行。确定大体思路后,还有一些问题需要确认:商场购物流程是否只需要完整的时间旅行体验?如果想体验大促期间整个官网商城的所有氛围,可能涉及到很多变化,比如大促促销页面,专属聚合商品页面,简化版只关注整个购物订单流程。整个遍历过程需要真正创建一个订单吗?穿越后,用户的下单时间与订单确认时间相同,因此订单确认页面的所有折扣和最终价格真正做到所见即所得,无需实际下单即可获取所有促销信息订单。提交订单时,建议直接拉黑提醒用户“您目前正在穿越时空,请先回到现实再下单”。它实际上并不创建订单,不会有后续的写入资源的链式操作。同时,在这种情况下,会减少很多不必要的改动。用户在穿越过程中收到的特殊优惠券是否需要特殊标识?a)如果穿越过程中领取的优惠券有特殊标记,建议退出时光机后,优惠券实际有效期届满后不要使用,防止占用普通用户资源。同时,不建议在这种情况下添加优惠券的发放数量。b)穿越过程中领取的优惠券没有特殊标记,因此退出时光机后使用此优惠券与其他正常领取的优惠券没有区别。这种情况算是占用了普通用户的资源,所以也建议相应增加优惠券。发放的优惠券数量。A方案要求优惠券系统进行相关适配改动,但不存在污染或占用真实线上资源的情况;planb不需要做任何改动,但会占用极少量的真实资源。如果运营商认为问题不严重,建议使用b方案,从项目角度考虑成本最低。3.实现3.1核心流程图根据上述构思方案,得到如下商场穿越核心购物流程:3.2改造要点从以上流程图可以看出改造要点:维护白名单“当前时间”信息及获取3.2.1白名单信息维护为了方便后续出行用户共享时间信息,我们将此信息(openId:travelTime)存储在配置中心,并提供相应的管理控制台,方便出行用户和出行时间点设置。3.2.2获取“当前时间”整个上下游相关系统可能都需要获取“当前时间”,而获取“当前时间”需要具备获取配置的白名单信息和当前用户信息的能力。显然,为了尽可能减少每个业务系统的代码改动,在一个公共模块中获取“当前时间”是合适的。各个业务系统都依赖这个通用模块自动获取预期的“当前时间”。因此,集成时光机模块后整个业务系统的链接关系如下:3.2.3时光机模块从以上内容,我们可以得到时光机模块需要包含的主要能力(vivo-xxx-time-travel):a)遍历用户白名单信息b)获取“当前时间”c)读取并设置上下文openIda和b的实现比较简单,正常访问公司配置中心,获取“当前时间”即可“根据指定的openId时间”就可以了,比较麻烦的是获取用户“当前时间”的openId信息,以前各个业务系统之间的接口调用可能不需要用户openId信息,现在遍历用户指定白名单用户,所以入口链路检测到的用户openId信息必须向下传递到业务系统中的各个下游解决方案1:各业务系统之间的接口调用耦合openId信息需要对所有业务系统进行改造。显然,这个方案比较原始,对所有业务方都非常不友好,所以不推荐使用。方案二:由于我们后台业务系统之间是使用dubbo进行接口调用的,所以可以利用dubbo基于spi插件机制的自定义业务过滤器,在调用额外接口时透传openId作为附加信息。(如果使用其他接口调用方式,也建议采用类似的处理原则。)接下来我们看一下时间机器模块中的一些核心代码实现:(将当前业务系统作为调用时执行的filterconsumer)当前业务系统作为消费者时执行的过滤器/***当前业务系统作为消费者时执行的过滤器*/@Activate(group=Constants.CONSUMER)publicclassBizConsumerFilterimplementsFilter{@Overridepublic结果invoke(Invokerinvocation,Invocationinvocation)throwsRpcException{if(invocationinstanceofRpcInvocation){StringopenId=invocation.getAttachment("tc_xxx_travel_openId");if(openId==null&&TimeTravelUtil.getContextOpenId()!=null){//作为发起者中的消费者在调用之前,如果openId缺失,设置上下文的openId((RpcInvocation)invocation).setAttachment(openIdAttachmentKey,TimeTravelUtil.getContextOpenId());}}返回invoker.invoke(invocation);}}当前业务系统作为服务提供者过滤器执行;/***当前业务系统为服务提供者时执行的过滤器*/@Activate(group=Constants.PROVIDER)publicclassBizProviderFilterimplementsFilter{@OverridepublicResultinvoke(Invokerinvoker,Invocationinvocation)throwsRpcException{if(invocationinstanceofRpcInvocation){StringopenId=invocation.getAttachment("tc_xxx_travel_openId");}if(openId!=null){//作为下游服务提供者,获取上游系统设置的上下文openIdTimeTravelUtil.setContextOpenId(openId);}}try{returninvoker.invoke(invocation);}最后{TimeTravelUtil.removeContextOpenId();}}}通过时间获取工具类;/***通过时间获取工具类*/publicfinalclassTimeTravelUtil{privatestaticfinalThreadLocalcurrentUserTimeTravelInfoThreadLocal=newThreadLocal<>();privatestaticfinalThreadLocalcontextOpenId=newThreadLocal<>();publicstaticvoidsetContextOpenId(StringopenId){contextOpenId.set(openId);setUserTravelInfoIfExists(openId);}publicstaticStringgetContextOpenId(){返回contextOpenId.get();}publicstaticvoidremoveContextOpenId(){contextOpenId.remove();removeUserTimeTravelInfo();}/***设置当前context用户出行信息,如果存在*@paramopenId*/publicstaticvoidsetUserTravelInfoIfExists(StringopenId){//TimeTravellersConfig会连接配置中心携带所有白名单遍历用户信息配置,并转换每次遍历用户信息进入TimeTravelInfoTimeTravelInfouserTimeTravelInfo=TimeTravellersConfig.getUserTimeTravelInfo(openId);如果(userTimeTravelInfo.isInTravel()){currentUserTimeTravelInfoThreadLocal.set(userTimeTravelInfo);}}/***移除当前上下文用户遍历信息*/publicstaticvoidremoveUserTimeTravelInfo(){currentUserTimeTravelInfoThreadLocal.remove();}/***当前链接上下文是否在遍历中*@return*/publicstaticbooleanisInTimeTravel(){returncurrentUserTimeTravelInfoThreadLocal.get()!=null;}/***获取“当前”时间,单位:毫秒*如果当前正在旅行,则返回设置的旅行时间,否则返回实际系统时间*@return*/publicstaticlonggetNow(){TimeTravelInfotravelInfo=currentUserTimeTravelInfoThreadLocal.get();返回travelInfo!=null?travelInfo.getTravelTime():System.currentTimeMillis();}}用户旅行信息/***用户旅行信息*/publicclassTimeTravelInfo{/***当前是否在旅行*/privatebooleanisInTravel=false;/***当前旅行时间点,仅当isInTravel=true时有效*/privateLongtravelTime;publicbooleanisInTravel(){returnisInTravel;}publicvoidsetInTravel(booleaninTravel){isInTravel=inTravel;}publicLonggetTravelTime(){返回旅行时间;}publicvoidsetTravelTime(LongtravelTime){this.travelTime=travelTime;}}业务系统依赖了这个vivo-xxx-time-travel模块后,凡是需要获取当前时间的地方,把原来的System.currentTimeMillis()改成TimeTravelUtil.getNow()就可以了。3.4存在的问题在时间机器建容的过程中,遇到了一个比较重要的问题,就是上下文传递openId信息时,会出现跨线程传递丢失的问题。如果底层是直接实现异步调用的Java线程池,那么线程池相关的拦截就可以实现上下文复制和拷贝传递。我们内部的全链路系统已经通过相关的代理技术对线程上下文信息进行了处理。如果你使用Hystrix实现异步调用,可以看下作者专门介绍的另一篇文章《Hystrix中如何解决ThreadLocal信息丢失》。4、最后,本文介绍的时光机相关能力主要应用于官网商城,但不限于电商场景。时光机模块在设计的时候并没有和具体的业务耦合,所以也可以应用到其他的业务场景中。或者有一些参考。另外,本文的电商场景关注的是优惠价是否正常,基本涉及到阅读操作。如果有些场景需要在遍历之后进行完整的业务功能操作,比如实际下单,那么就会涉及到一些写操作,这时候,你就可以利用影子库的相关能力,通过操作完成一个完整的旅程。作者:vivo官网商城开发组-魏富平