订单服务处理逻辑:
创建订单完成,发送消息到死信队列,设置消息过期时间,时间到消息通过绑定的exchange交换机和routing key 发送到指定队列,我们监听这个队列,对订单进行关闭处理。订单支付成功时,无需处理消息,关闭订单时会对订单状态进行判断,对于支付成功的订单不会进行关闭。死信队列中的消息也会得到处理。订单关闭的同时要发送消息给库存服务进行库存解锁。
库存锁定完成,发送消息到死信队列,设置过期时间,时间到消息通过绑定的exchange交换机和routing key 发送到指定队列,我们监听这个队列,对库存进行解锁。订单支付成功时,直接修改库存锁定状态及扣减库存,因为解锁库存时会对库存锁定状态进行判断,只有是锁定状态的才会解锁。
1.确认订单
前端购物车列表点击 ‘去结算’,到订单确认页。
后端接收:调用订单确认服务,将订单确认数据存到model,返回给前端进行显示。
@GetMapping("/toTrade")
public String toTrade(Model model) throws ExecutionException, InterruptedException {
OrderConfirmVo orderConfirmVo = orderService.confirmOrder();
model.addAttribute("confirmOrderData",orderConfirmVo);
return "confirm";
}
confirmOrder()方法:
//订单确认页,返回给前端需要的数据
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
//构建OrderConfirmVo 会员收货地址列表,购物项,库存,订单总额,优惠信息,防重复提交令牌
OrderConfirmVo confirmVo = new OrderConfirmVo();
//获取当前用户登录的信息 从threadlocal获取,threadlocal在拦截器中创建,从session中获取用户信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
//获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//1、远程查询所有的收货地址列表 memberService 第一个异步线程
CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
confirmVo.setMemberAddressVos(address);
}, threadPoolExecutor);
//2、远程查询购物车所有选中的购物项 第二个异步线程
CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
confirmVo.setItems(currentCartItems);
}, threadPoolExecutor).thenRunAsync(() -> {
List<OrderItemVo> items = confirmVo.getItems();
//获取全部商品的id
List<Long> skuIds = items.stream()
.map((itemVo -> itemVo.getSkuId()))
.collect(Collectors.toList());
//远程查询商品库存信息
R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});
if (skuStockVos != null && skuStockVos.size() > 0) {
//将skuStockVos集合转换为map
Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(skuHasStockMap);
}
},threadPoolExecutor);
//3、查询用户积分
Integer integration = memberResponseVo.getIntegration();
confirmVo.setIntegration(integration);
//4、价格数据自动计算
// 5、防重令牌(防止表单重复提交)
//为用户设置一个token,三十分钟过期时间(存在redis)
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(OrderConstent.USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId().toString(), token,30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
CompletableFuture.allOf(cartInfoFuture,addressFuture).get();
return confirmVo;
}
然后到订单确认页
点击提交订单,到订单生成页
传给后端收货地址信息,价格以及防重复提交的token信息
后端接收
/**
* 下单功能
* @param vo
* @return
*/
@PostMapping(value = "/submitOrder")
public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes attributes) {
try {
SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
//下单成功来到支付选择页
//下单失败回到订单确认页重新确定订单信息
if (responseVo.getCode() == 0) {
//成功
model.addAttribute("submitOrderResp",responseVo);
return "pay";
} else {
String msg = "下单失败";
switch (responseVo.getCode()) {
case 1: msg += "令牌订单信息过期,请刷新再次提交"; break;
case 2: msg += "订单商品价格发生变化,请确认后再次提交"; break;
case 3: msg += "库存锁定失败,商品库存不足"; break;
}
attributes.addFlashAttribute("msg",msg);
return "redirect:http://order.gulimall.com/toTrade";
}
} catch (Exception e) {
if (e instanceof NoStockException) {
String message = ((NoStockException)e).getMessage();
attributes.addFlashAttribute("msg",message);
}
return "redirect:http://order.gulimall.com/toTrade";
}
}
submitOrder()方法
1.验证令牌 lua脚本
2.创建订单 生成订单,及订单项的信息
3.前端传过来的价格和创建订单时计算的价格比较,差距小于0.01
4.保存订单,将订单和订单项数据存到数据库
5.库存锁定,锁定成功后,发送mq消息,带过期时间,过期后自动检测是否需要解锁。
6.将购物车中数据删除
7.订单生成成功,发送mq消息,带过期时间,过期后自动检测是否需要删除该订单。
//本地事务,只能控制本系统的回滚,控制不了远程服务的回滚,需要用分布式事务
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//线程共享
confirmVoThreadLocal.set(vo);
//生成订单的返回类,包含订单实体类和错误状态码
SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
//去创建、下订单、验令牌、验价格、锁定库存...
//获取当前用户登录的信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
//先设置状态0,为正常,有异常是改变code
responseVo.setCode(0);
//1、验证令牌是否合法【令牌的对比和删除必须保证原子性】
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
//通过lua脚本原子验证令牌和删除令牌
Long result = (Long) redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstent.USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()), orderToken);
if (result == 0L) {
//令牌验证失败
responseVo.setCode(1);
return responseVo;
} else {
//令牌验证成功
//1、创建订单、订单项等信息
OrderCreateTo order = createOrder();
//2、验证价格
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//TODO 3、保存订单
saveOrder(order);
//4、库存锁定,只要有异常,回滚订单数据
//订单号、所有订单项信息(skuId,skuNum,skuName)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
//获取出要锁定的商品数据信息
List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
orderItemVo.setTitle(item.getSkuName());
return orderItemVo;
}).collect(Collectors.toList());
lockVo.setLocks(orderItemVos);
//TODO 调用远程锁定库存的方法
//出现的问题:1.扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚,库存事务不回滚(解决方案:seata)
//2.锁定库存之后,出现了异常,下单操作可以回滚,但是库存却锁定了。远程服务的调用不能回滚。
//为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率,
//使用可靠消息+最终一致性,库存服务滋滋回滚,可以发消息给库存服务
//库存服务本身可以设置自动解锁
R r = wmsFeignService.orderLockStock(lockVo);
if (r.getCode() == 0) {
//锁定成功
responseVo.setOrder(order.getOrder());
//int i = 10/0;
//TODO 订单创建成功,发送消息给MQ
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
删除购物车里的数据
//redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());
return responseVo;
} else {
//锁定失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
// responseVo.setCode(3);
// return responseVo;
}
} else {
responseVo.setCode(2);
return responseVo;
}
}
}
订单消息监听,创建订单成功后,会发送mq消息,带过期时间,到过期时间后,消息会进入死信队列,我们监听此队列,来处理未付款的订单。
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
@Autowired
private OrderService orderService;
@RabbitHandler
public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息,准备关闭订单" + orderEntity.getOrderSn());
try {
orderService.closeOrder(orderEntity);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
关闭订单方法
@Override
public void closeOrder(OrderEntity orderEntity) {
//关闭订单之前先查询一下数据库,判断此订单状态是否已支付
OrderEntity orderInfo = this.getOne(new QueryWrapper<OrderEntity>().
eq("order_sn",orderEntity.getOrderSn()));
if (orderInfo.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())) {
//代付款状态进行关单
OrderEntity orderUpdate = new OrderEntity();
orderUpdate.setId(orderInfo.getId());
orderUpdate.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(orderUpdate);
// 发送消息给MQ
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderInfo, orderTo);
try {
//TODO 确保每个消息发送成功,给每个消息做好日志记录,(给数据库保存每一个详细信息)保存每个消息的详细信息
//这里发送消息是给库存服务的,订单取消了,库存要解锁
rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
} catch (Exception e) {
//TODO 定期扫描数据库,重新发送失败的消息
}
}
}
库存服务的消息监听:
两种方式触发解锁库存:
1.创建订单时,锁定完库存后,服务异常,导致订单创建失败回滚,但是库存仍然锁定了,办法是库存服务有定时解锁库存机制,就是锁定库存后,发送一个带过期时间的消息,过期后消息到死信队列,我们监听此队列。
2.创建订单成功,但是用户未付款,到时间后,订单会发送mq消息来取消订单,取消后会发mq消息来解锁库存,我们监听此消息
/**
* 1、库存自动解锁
* 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
*
* 2、订单失败
* 库存锁定失败
*
* 只要解锁库存的消息失败,一定要告诉服务解锁失败
*/
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
log.info("******收到解锁库存的信息******");
try {
//当前消息是否被第二次及以后(重新)派发过来了
// Boolean redelivered = message.getMessageProperties().getRedelivered();
//解锁库存
wareSkuService.unlockStock(to);
// 手动删除消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
// 解锁失败 将消息重新放回队列,让别人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
//订单自动取消了,需要解锁库存
@RabbitHandler
public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
log.info("******收到订单关闭,准备解锁库存的信息******");
try {
wareSkuService.unlockStock(orderTo);
// 手动删除消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
// 解锁失败 将消息重新放回队列,让别人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
解锁库存的方法:
@Override
public void unlockStock(StockLockedTo to) {
//库存工作单的id
StockDetailTo detail = to.getDetailTo();
Long detailId = detail.getId();
/**
* 解锁
* 1、查询数据库关于这个订单锁定库存信息
* 有:证明库存锁定成功了
* 解锁:订单状况
* 1、没有这个订单,必须解锁库存
* 2、有这个订单,不一定解锁库存
* 订单状态:已取消:解锁库存
* 已支付:不能解锁库存
*/
WareOrderTaskDetailEntity taskDetailInfo = wareOrderTaskDetailService.getById(detailId);
if (taskDetailInfo != null) {
//查出wms_ware_order_task工作单的信息
Long id = to.getId();
WareOrderTaskEntity orderTaskInfo = wareOrderTaskService.getById(id);
//获取订单号查询订单状态
String orderSn = orderTaskInfo.getOrderSn();
//远程查询订单信息,注意订单设置了登录限制,需要放行这个请求
R orderData = orderFeignService.getOrderStatus(orderSn);
if (orderData.getCode() == 0) {
//订单数据返回成功
OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() {});
//判断订单状态是否已取消或者支付或者订单不存在
if (orderInfo == null || orderInfo.getStatus() == 4) {
//订单已被取消,才能解锁库存
if (taskDetailInfo.getLockStatus() == 1) {
//当前库存工作单详情状态1,已锁定,但是未解锁才可以解锁
unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
}
}
} else {
//消息拒绝以后重新放在队列里面,让别人继续消费解锁
//远程调用服务失败
throw new RuntimeException("远程调用服务失败");
}
} else {
//无需解锁
}
}
/**
* 防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理
* 导致卡顿的订单,永远都不能解锁库存
* @param orderTo
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void unlockStock(OrderTo orderTo) {
String orderSn = orderTo.getOrderSn();
//查一下最新的库存解锁状态,防止重复解锁库存
WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
//按照工作单的id找到所有 没有解锁的库存,进行解锁
Long id = orderTaskEntity.getId();
List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
.eq("task_id", id).eq("lock_status", 1));
for (WareOrderTaskDetailEntity taskDetailEntity : list) {
unLockStock(taskDetailEntity.getSkuId(),
taskDetailEntity.getWareId(),
taskDetailEntity.getSkuNum(),
taskDetailEntity.getId());
}
}
/**
* 解锁库存的方法
* @param skuId
* @param wareId
* @param num
* @param taskDetailId
*/
public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {
//库存解锁
wareSkuDao.unLockStock(skuId,wareId,num);
//更新工作单的状态
WareOrderTaskDetailEntity taskDetailEntity = WareOrderTaskDetailEntity.builder().build();
//WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
taskDetailEntity.setId(taskDetailId);
//变为已解锁
taskDetailEntity.setLockStatus(2);
wareOrderTaskDetailService.updateById(taskDetailEntity);
}
订单页点击支付宝,跳转到支付宝支付页
后端接收:
/**
* 用户下单:支付宝支付
* 1、让支付页让浏览器展示
* 2、支付成功以后,跳转到用户的订单列表页
* @param orderSn
* @return
* @throws AlipayApiException
*/
@ResponseBody
@GetMapping(value = "/aliPayOrder",produces = "text/html")
public String aliPayOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
PayVo payVo = orderService.getOrderPay(orderSn);
String pay = alipayTemplate.pay(payVo);
System.out.println(pay);
return pay;
}
指定返回的为html页面
直接到支付的页面。
先调用orderservice获取此次订单信息。包括订单编号,订单名称,总价格。
然后调用支付宝的pay方法,传入订单信息,完成支付。
支付宝支付
先在支付宝平台申请一个网页应用,每个应用有一个沙箱应用。可以测试支付流程。
然后生成秘钥。
然后下载SDK demo https://opendocs.alipay.com/open/270/106291/
根据demo自定义工具类,
配置用户私钥和支付宝公钥,支付完成的异步通知和同步通知,支付宝网关等等。
支付完成后,返回到订单列表。