一、订单确认页功能流程图
1、进入登录确认页之前会,先进入登录拦截器
同样是使用ThreadLocal来存储用户的登录信息,从请求的request中获取登录信息
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
/**
* 在目标请求之前执行该方法
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
boolean match = new AntPathMatcher().match("/order/order/queryOrderBySn/**", requestURI);
if (match) {
return true;
}
// 获取登录的用户
MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null) {
// 已登录,将登录用户信息存放到threadLocal中,并放行
loginUser.set(attribute);
return true;
} else {
// 未登录,重定向到用户登录页
request.getSession().setAttribute("msg", "请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
2、查询订单确认页需要的数据
/**
* 查询订单确认页需要的数据
*
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
log.info("主线程:{}", Thread.currentThread().getId());
// 获取之前的请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 收货地址列表 异步任务一
CompletableFuture<Void> getAddressTask = CompletableFuture.runAsync(() -> {
log.info("getAddress线程:{}", Thread.currentThread().getId());
// 每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeginService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address);
}, executor);
// 远程查询购物车中所有选中的购物项 异步任务二
CompletableFuture<Void> getCartItemsTask = CompletableFuture.runAsync(() -> {
log.info("getUserCartItems线程:{}", Thread.currentThread().getId());
// 每一个线程都来共享之前的请求数据,在Feign的拦截器中就不会丢失老请求的数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> userCartItems = cartFeginService.getUserCartItems();
confirmVo.setItems(userCartItems);
}, executor).thenRunAsync(() -> {
// 查询商品的库存信息,判断该商品是否有库存量
List<OrderItemVo> items = confirmVo.getItems();
if (items != null && !items.isEmpty()) {
List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
// 调用库存服务
R hasStock = wareSkuFeginService.getSkuHasStock(collect);
if (hasStock != null) {
List<SkuHasStockVo> data = hasStock.getData(new TypeReference<List<SkuHasStockVo>>() {
});
if (CollectionUtils.isNotEmpty(data)) {
Map<Long, Boolean> hasStockMap = data.stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, SkuHasStockVo::getHasStock));
confirmVo.setStocks(hasStockMap);
}
}
}
}, executor);
// 用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
// 防重令牌
String token = UUID.randomUUID().toString().replace("-", "");
confirmVo.setOrderToken(token);
stringRedisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(), token, 30, TimeUnit.MINUTES);
// 其他数据自动计算
CompletableFuture.allOf(getAddressTask, getCartItemsTask).get();
return confirmVo;
}
2.1 获取收货地址列表信息
// 收货地址列表 异步任务一
CompletableFuture<Void> getAddressTask = CompletableFuture.runAsync(() -> {
log.info("getAddress线程:{}", Thread.currentThread().getId());
// 每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeginService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address);
}, executor);
通过openFeign远程调用获取收货地址信息
@FeignClient("gulimall-member")
public interface MemberFeginService {
/**
* 返回会员的所有收货地址列表
* @param memberId
* @return
*/
@GetMapping("/member/memberreceiveaddress/{memberId}/address")
List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
}
2.2 设置防重令牌
防重令牌的作用在用户下订单的时候为了防止用户重复提交的情况
// 防重令牌
String token = UUID.randomUUID().toString().replace("-", "");
confirmVo.setOrderToken(token);
二、用户下订单
/**
* 下单
* @param vo
* @return
* @Transactional:本地事务,在分布式系统下,只能控制自己的回滚,控制不了其他服务的回滚,这里只能使用分布式事务处理
* @GlobalTransactional:全局事务,使用seata解决分布式事务问题。由于seata处理的方式比较麻烦,后改为RabbitMQ的延迟队列的方式处理分布式事务的问题。
*/
// @GlobalTransactional
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
response.setCode(0);
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
orderSubmitLocal.set(vo);
/**
* 验证令牌的原因是保证接口的幂等性,即防止重复提交订单
* 1、验证令牌【令牌的获取、令牌的对比和删除三个操作必须保证原子性】
* 0:令牌失败
* 1:删除成功
*/
// 前端传过来的token
String orderToken = vo.getOrderToken();
// 使用redis的lua脚本来保证原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 原子验证令牌和删除令牌 【0 -》令牌验证失败,1-》删除成功】
Long result = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if(result == 0L){
// 令牌验证失败
response.setCode(1);
return response;
}else{
// 令牌验证成功,准备下单操作
// 1、创建订单,订单项等信息
OrderCreateTo order = createOrder();
// 2、验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if(Math.abs(payPrice.subtract(payAmount).doubleValue()) <= 0.01){
// 金额对比成功
// 3、保存订单
saveOrder(order);
// 4、库存锁定,只要有异常需要回滚订单数据
WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemEntity> orderItems = order.getOrderItems();
List<OrderItemVo> collect = orderItems.stream().map(item -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setTitle(item.getSkuName());
orderItemVo.setCount(item.getSkuQuantity());
return orderItemVo;
}).collect(Collectors.toList());
wareSkuLockVo.setLocks(collect);
//TODO 远程锁库存 ,库存成功了,但是网络原因超时了,订单回滚,库存不滚
R r = wareSkuFeginService.orderLockStock(wareSkuLockVo);
if(r.getCode() == 0){
response.setOrder(order.getOrder());
// TODO: 远程扣减积分 出现异常 如果使用传统的方式,那么订单回滚,锁定库存不回滚
// int i = 10/0;
// 将订单消息存入到延迟队列里面,一分钟后(真实情况下,是30分钟未支付,会自动取消订单),如果状态为待支付,则执行关单操作
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
return response;
}else{
// 商品库存不足
response.setCode(3);
return response;
}
}else{
// 金额对比失败
response.setCode(2);
return response;
}
}
}