持续学习&持续更新中…
守破离
【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【23】【订单服务】
订单中心
电商系统涉及到 3 流,分别是信息流,资金流,物流,而订单系统作为中枢将三者有机的集合起来。
订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通。
订单信息
用户信息
用户信息包括用户账号、用户等级、用户的收货地址、收货人、收货人电话等组成
用户账户需要绑定手机号码,但是用户绑定的手机号码不一定是收货信息上的电话。
用户可以添加多个收货信息,用户等级信息可以用来和促销系统进行匹配,获取商品折扣,同时用户等级还可以获取积分的奖励等
订单基础信息
订单基础信息是订单流转的核心,其包括订单类型、父/子订单、订单编号、订单状态、订单流转的时间等。
- 订单类型包括实体商品订单和虚拟订单商品等,这个根据商城商品和服务类型进行区分。
- 同时订单都需要做父子订单处理,之前在初创公司一直只有一个订单,没有做父子订 单处理后期需要进行拆单的时候就比较麻烦,尤其是多商户商场,和不同仓库商品的时候, 父子订单就是为后期做拆单准备的。
- 订单编号不多说了,需要强调的一点是父子订单都需要有订单编号,需要完善的时候 可以对订单编号的每个字段进行统一定义和诠释。
- 订单状态记录订单每次流转过程,后面会对订单状态进行单独的说明。
- 订单流转时间需要记录下单时间,支付时间,发货时间,结束时间/关闭时间等等
商品信息
商品信息从商品库中获取商品的 SKU 信息、图片、名称、属性规格、商品单价、商户信息等,从用户下单行为记录的用户下单数量,商品合计价格等。
优惠信息
优惠信息记录用户参与的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优惠券信息,优惠券满足条件的优惠券需要默认展示出来,具体方式已在之前的优惠券 篇章做过详细介绍,另外还虚拟币抵扣信息等进行记录。
为什么把优惠信息单独拿出来而不放在支付信息里面呢?
因为优惠信息只是记录用户使用的条目,而支付信息需要加入数据进行计算,所以做为区分。
支付信息
- 支付流水单号,这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支 付流水号,财务通过订单号和流水单号与支付通道进行对账使用。
- 支付方式用户使用的支付方式,比如微信支付、支付宝支付、钱包支付、快捷支付等。 支付方式有时候可能有两个——余额支付+第三方支付。
- 商品总金额,每个商品加总后的金额;运费,物流产生的费用;优惠总金额,包括促 销活动的优惠金额,优惠券优惠金额,虚拟积分或者虚拟币抵扣的金额,会员折扣的金额等 之和;实付金额,用户实际需要付款的金额。
- 用户实付金额=商品总金额+运费-优惠总金额
物流信息
物流信息包括配送方式,物流公司,物流单号,物流状态
物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点。
订单状态
-
待付款
用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超 时后将自动取消订单,订单变更关闭状态。 -
已付款/待发货
用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到 WMS 系统,仓库进行调拨,配货,分拣,出库等操作。 -
待收货/已发货
仓储将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时知悉物 品物流状态 -
已完成
用户确认收货后,订单交易完成。后续支付侧进行结算,如果订单存在问题进入售后状态 5. 售后中
用户在付款后申请退款,或商家发货后用户申请退换货。
售后也同样存在各种状态,当发起售后申请后生成售后订单,售后订单状态为待审核,等待 商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后 订单 状态更新为待退款状态,退款到用户原账户后订单状态更新为售后成功。 -
已取消
付款之前取消订单。包括超时未付款或用户商户取消订单都会产生这种订单状态。
订单流程
订单流程是指从订单产生到完成整个流转的过程,从而行程了一套标准流程规则。
不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与 O2O 订单等,所以需要根据不同的类型进行构建订单流程。
不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程
正向流程就是一个正常的网购步骤:订单生成–>支付订单–>卖家发货–>确认收货–>交易成功。 而每个步骤的背后,订单是如何在多系统之间交互流转的,可概括如下图:
订单创建与支付
- 订单创建前需要预览订单,选择收货信息等
- 订单创建需要锁定库存,库存有才可创建,否则不能创建
- 订单创建后超时未支付需要解锁库存
- 支付成功后,需要进行拆单,根据商品打包方式,所在仓库,物流等进行拆单
- 支付的每笔流水都需要记录,以待查账
- 订单创建,支付成功等状态都需要给 MQ 发送消息,方便其他系统感知订阅
逆向流程
- 修改订单,用户没有提交订单,可以对订单一些信息进行修改,比如配送信息, 优惠信息,及其他一些订单可修改范围的内容,此时只需对数据进行变更即可。
- 订单取消,用户主动取消订单和用户超时未支付,两种情况下订单都会取消订 单,而超时情况是系统自动关闭订单,所以在订单支付的响应机制上面要做支付的限时处理,尤其是在前面说的下单减库存的情形下面,可以保证快速的释放库存。 另外需要需要处理的是促销优惠中使用的优惠券,权益等视平台规则,进行相应补回给用户。
- 退款,在待发货订单状态下取消订单时,分为缺货退款和用户申请退款。如果是 全部退款则订单更新为关闭状态,若只是做部分退款则订单仍需进行进行,同时生 成一条退款的售后订单,走退款流程。退款金额需原路返回用户的账户。
- 发货后的退款,发生在仓储货物配送,在配送过程中商品遗失,用户拒收,用户 收货后对商品不满意,这样情况下用户发起退款的售后诉求后,需要商户进行退款 的审核,双方达成一致后,系统更新退款状态,对订单进行退款操作,金额原路返 回用户的账户,同时关闭原订单数据。仅退款情况下暂不考虑仓库系统变化。如果 发生双方协调不一致情况下,可以申请平台客服介入。在退款订单商户不处理的情 况下,系统需要做限期判断,比如 5 天商户不处理,退款单自动变更同意退款。
订单确认页
Feign远程调用丢失请求头问题
用户访问订单确认页面会来到OrderWebController的toTrade方法,在这之前,我们通过LoginUserInterceptor对用户请求进行拦截,判断用户是否登录,如果用户登陆了会把登录的用户信息放到ThreadLocal中,方便之后的service等使用:
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//登录了就放行
MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null) {
loginUser.set(attribute);
return true;
}
//没登录就去登录
request.getSession().setAttribute("msg", "请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/**");
}
}
通过 LoginUserInterceptor 拦截器后,来到OrderWebController的toTrade方法,我们会通过orderService.confirmOrder();
去获取用户的确认订单信息,我们还得通过Feign的远程调用去获取一些信息,代码如下:
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
final MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
//1、远程查询所有的收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address);
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
CompletableFuture.allOf(getAddressFuture, cartFuture).get();
return confirmVo;
}
但是我们会发现cartFeignService.getCurrentUserCartItems()
这句代码会返回空结果。这就是因为出现了Feign远程调用丢失请求头问题:
Feign在远程调用之前会构造新请求,在构造请求过程中,会调用很多类型为RequestInterceptor的拦截器
那么我们就可以自定义拦截器,让在Feign构造新请求的时候,通过拦截器让它带上之前请求的请求头信息,就可以解决此问题:
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
return template -> {
// 通过RequestContextHolder拿到刚进来的这个请求
// 通过RequestContextHolder获取到的RequestAttributes是Spring自动设置的
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest(); //老请求
//同步请求头数据,Cookie
String cookie = request.getHeader("Cookie");
template.header("Cookie", cookie); //给新请求同步老请求的header头信息,比如Cookie信息
}
};
}
}
Feign异步情况丢失上下文问题
我们发现通过orderService.confirmOrder();
去获取用户的确认订单信息,会调用两个Feign的远程请求,这种情况下,为了提高该接口的响应速度,执行效率,提升性能等,我们应该使用异步编排的方式,让两个Feign远程请求同时执行,加快速度。但如果直接开启异步任务又会有新的问题出现:
我们之前会通过RequestContextHolder拿到刚进来的请求 :ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
,然后将其设置给每个Feign创建的新请求,这样通过Feign发出的远程调用请求就可以带上用户通过浏览器发送过来的请求头数据,解决了Feign远程调用丢失请求头这个问题
但是RequestContextHolder 内部是通过 ThreadLocal 共享数据的
以前同步调用这两个Feign的远程请求是这样工作的:
发送Feign请求,Feign在构建新请求时会先来到 RequestInterceptor 拦截器,我们在拦截器中会获取并使用 RequestAttributes,由于是同步调用也就是说大家都是同一个线程(Tomcat进来使用同一条线程执行我们的Controller/Service等),使用RequestContextHolder.getRequestAttributes()
获取数据时当然可以获取到。
然而直接开启异步任务发送Feign请求,Feign来到 RequestInterceptor 拦截器获取 RequestAttributes 时,由于是不同的线程,当然获取不到之前线程的RequestAttributes对象,也就无法使用了。
所以,开启异步调用Feign时,为了可以获取到之前的请求信息,我们可以这样写:
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
final MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
// List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
// confirmVo.setAddress(address);
//
//feign在远程调用之前要构造请求,调用很多的拦截器
//RequestInterceptor interceptor : requestInterceptors
// List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
// confirmVo.setItems(items);
System.out.println("主线程...." + Thread.currentThread().getId());
//获取之前的请求
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据
RequestContextHolder