OrderWebController提交订单

  1. /**
  2. * 下单功能,提交订单。 需要去创建订单、验令牌、验价格、锁库存
  3. * @param vo 订单提交的数据
  4. * @param model
  5. * @param redirectAttributes
  6. * @return 下单成功来到支付选择页,下单失败回到订单确认页重新确认订单信息
  7. */
  8. @PostMapping("/submitOrder")
  9. public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes redirectAttributes) {
  10. try {
  11. SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
  12. if (responseVo.getCode() == 0) {
  13. model.addAttribute("submitOrderResp", responseVo); // 返回页面数据
  14. // 下单成功来到支付选择页
  15. return "pay";
  16. } else {
  17. String msg = "下单失败";
  18. switch (responseVo.getCode()) {
  19. case 1:
  20. msg += "订单信息过期,请刷新重新提交";
  21. break;
  22. case 2:
  23. msg += "订单商品价格发生变化,请确认后再次提交";
  24. break;
  25. case 3:
  26. msg += "商品库存不足";
  27. }
  28. redirectAttributes.addFlashAttribute("msg", msg); // 返回页面数据
  29. // 下单失败回到订单确认页重新确认订单信息
  30. return "redirect:http://order.gulimall.com/toTrade";
  31. }
  32. }catch (Exception e){
  33. if(e instanceof NoStockException){
  34. String msg = ((NoStockException) e).getMessage();
  35. redirectAttributes.addFlashAttribute("msg",msg);
  36. }
  37. return "redirect:http://order.gulimall.com/toTrade";
  38. }
  39. }

OrderServiceImpl

  1. @Service("orderService")
  2. public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {
  3. // 将页面传递过来的数据在同一个线程内进行共享
  4. private ThreadLocal<OrderSubmitVo> submitVoThreadLocal = new ThreadLocal<>();
  5. @Autowired
  6. OrderItemService orderItemService;
  7. @Autowired
  8. CartFeginService cartFeginService;
  9. @Autowired
  10. MemberFeginService memberFeginService;
  11. @Autowired
  12. ProductFeignService productFeignService;
  13. @Autowired
  14. WmsFeignService wmsFeignService;
  15. @Autowired
  16. StringRedisTemplate redisTemplate;
  17. @Autowired
  18. RabbitTemplate rabbitTemplate;
  19. @Autowired
  20. ThreadPoolExecutor executor;
  21. @Autowired
  22. PaymentInfoService paymentInfoService;
  23. /**
  24. * 给订单确认页返回需要用的数据
  25. * @return
  26. */
  27. @Override
  28. public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
  29. // 登录拦截器,获取当前登录用户信息
  30. MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
  31. OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
  32. // 获取主线程中的requestAttributes
  33. RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
  34. // 异步处理
  35. CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
  36. // 每个子线程共享之前的请求数据
  37. RequestContextHolder.setRequestAttributes(requestAttributes);
  38. // 1、远程查询所有的收获地址列表
  39. List<MemberAddressVo> address = memberFeginService.getAddress(memberRespVo.getId());
  40. orderConfirmVo.setAddress(address);
  41. }, executor);
  42. // 异步处理
  43. CompletableFuture<Void> getCartItemsFuture = CompletableFuture.runAsync(() -> {
  44. // 每个子线程共享之前的请求数据
  45. RequestContextHolder.setRequestAttributes(requestAttributes);
  46. // 2、远程查询购物车所有选中的购物项,要注意获取最新的商品价格,而不是先前存到redis中的商品价格
  47. // 利用fegin的RequestInterceptor拦截器功能,远程调用其他服务时,其他服务也能感知当前登录的用户
  48. List<OrderItemVo> cartItems = cartFeginService.getCurrentUserCartItems();
  49. orderConfirmVo.setItems(cartItems);
  50. }, executor).thenRunAsync(() -> {
  51. // 获取购物项中的商品skuId
  52. List<OrderItemVo> items = orderConfirmVo.getItems();
  53. List<Long> collect = items.stream().map(OrderItemVo::getSkuId).collect(Collectors.toList());
  54. // 远程调用库存服务,获取商品的库存信息
  55. R skuHasStock = wmsFeignService.getSkuHasStock(collect);
  56. List<SkuStockVo> stockVos = skuHasStock.getData(new TypeReference<List<SkuStockVo>>() { });
  57. if(stockVos != null){
  58. Map<Long, Boolean> map = stockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
  59. // 设置购物车中商品的库存信息
  60. orderConfirmVo.setStocks(map);
  61. }
  62. },executor);
  63. // 3、查询用户积分信息
  64. Integer integration = memberRespVo.getIntegration();
  65. orderConfirmVo.setIntegration(integration);
  66. // 4、其他数据,如订单总价,商品总价自动计算
  67. // TODO 5、防重令牌
  68. String token = UUID.randomUUID().toString().replace("-", "");
  69. // 放重令牌保存至服务器
  70. redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(), token, 30 , TimeUnit.MINUTES);
  71. // 放重令牌发送给页面
  72. orderConfirmVo.setToken(token);
  73. CompletableFuture.allOf(getAddressFuture,getCartItemsFuture).get();
  74. return orderConfirmVo;
  75. }
  76. /**
  77. * 下单操作。创建订单、验令牌、验价格、锁库存
  78. * @param vo
  79. * @return
  80. */
  81. @Transactional
  82. @Override
  83. public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
  84. submitVoThreadLocal.set(vo);
  85. SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
  86. MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
  87. responseVo.setCode(0);
  88. // 1、验证令牌。令牌的对比和删除必须保证原子性
  89. String orderToken = vo.getOrderToken();
  90. // 如果调用get方法与传入的val相同,调用del方法,不相同返回0(0失败,1成功)
  91. String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
  92. // 使用Lua脚本,原子验证令牌和删除令牌
  93. Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
  94. Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
  95. // String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId());
  96. if (result == 0) {
  97. // 令牌验证失败
  98. responseVo.setCode(1);
  99. return responseVo;
  100. } else {
  101. // 令牌验证成功
  102. // 2、创建订单,订单项等信息
  103. OrderCreateTo order = createOrder();
  104. // 3、验价:根据订单计算的价格与界面提交的价格进行对比(可能因后台修改商品价格出现不一致的情况)
  105. BigDecimal payAmount = order.getOrder().getPayAmount();
  106. // 界面提交的价格
  107. BigDecimal payPrice = vo.getPayPrice();
  108. // 金额对比:只要差额在0.01就说明对比成功
  109. if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
  110. // 4、保存订单数据至数据库
  111. saveOrder(order);
  112. // 5、锁定库存、只要有异常就回滚订单数据
  113. // 需要订单号,所有订单项(skuId、skuName、数量)
  114. WareSkuLockVo lockVo = new WareSkuLockVo();
  115. lockVo.setOrderSn(order.getOrder().getOrderSn());
  116. // List<OrderItemEntity>转化为List<OrderItemVo>
  117. List<OrderItemVo> locks = order.getItems().stream().map(item -> {
  118. OrderItemVo itemVo = new OrderItemVo();
  119. itemVo.setSkuId(item.getSkuId());
  120. itemVo.setCount(item.getSkuQuantity());
  121. itemVo.setTitle(item.getSkuName());
  122. return itemVo;
  123. }).collect(Collectors.toList());
  124. lockVo.setLocks(locks);
  125. // 5.1 远程锁库存
  126. R r = wmsFeignService.orderLockStock(lockVo);
  127. if (r.getCode() == 0) {
  128. // 锁定库存成功(rebbitMQ消息通知)
  129. responseVo.setOrder(order.getOrder());
  130. // 6、远程扣减积分
  131. // int i = 10 / 0; // 模拟异常,此时订单回滚,库存不回滚,需要借助RabbitMQ使库存回滚
  132. // 7、订单创建成功,将每个订单信息发送给MQ
  133. // 通过路由键order.create.order发送给延迟队列order.create.order。
  134. // 30分钟后延迟队列将消息通过交换机order-event-exchange、路由键order.release.order
  135. // 发送给order.release.order.queue、order.release.coupon.queue等队列
  136. // 此时需要查询订单的最新状态,判断当前订单是正常关单还是异常关单,若是异常关单,需要回滚相关数据
  137. rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder());
  138. return responseVo;
  139. } else {
  140. // 锁定库存失败
  141. String msg = (String) r.get("msg");
  142. throw new NoStockException(1L);
  143. // responseVo.setCode(3);
  144. // return responseVo;
  145. }
  146. } else {
  147. // 金额对比失败
  148. responseVo.setCode(2);
  149. return responseVo;
  150. }
  151. }
  152. }
  153. // 保存订单数据
  154. private void saveOrder(OrderCreateTo order) {
  155. // 保存订单数据:oms_order
  156. OrderEntity orderEntity = order.getOrder();
  157. orderEntity.setModifyTime(new Date());
  158. this.save(orderEntity);
  159. // 批量保存订单项数据:oms_order_item
  160. List<OrderItemEntity> orderItems = order.getItems();
  161. orderItemService.saveBatch(orderItems);
  162. }
  163. // 创建订单
  164. private OrderCreateTo createOrder() {
  165. OrderCreateTo orderCreateTo = new OrderCreateTo();
  166. // 1、构建订单
  167. String orderSn = IdWorker.getTimeId();
  168. OrderEntity orderEntity = buildOrder(orderSn);
  169. // 2、获取所有的订单项
  170. List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
  171. // 3、验价
  172. computePrice(orderEntity, itemEntities);
  173. orderCreateTo.setItems(itemEntities);
  174. orderCreateTo.setOrder(orderEntity);
  175. return orderCreateTo;
  176. }
  177. /**
  178. * 验价:根据所有订单项的价格计算出订单的价格
  179. * @param orderEntity 订单内的价格
  180. * @param itemEntities 所有订单项价格
  181. */
  182. private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
  183. // 订单总额,计算各种优惠的总额,计算积分和成长值总额
  184. BigDecimal total = new BigDecimal("0.0");
  185. BigDecimal couponAmount = new BigDecimal("0.0");
  186. BigDecimal promotionAmount = new BigDecimal("0.0");
  187. BigDecimal integrationAmount = new BigDecimal("0.0");
  188. Integer integration = 0;
  189. Integer growth = 0;
  190. // 遍历所有订单项的价格。计算出订单的相关金额
  191. for (OrderItemEntity entity : itemEntities) {
  192. couponAmount = couponAmount.add(entity.getCouponAmount());
  193. promotionAmount = promotionAmount.add(entity.getPromotionAmount());
  194. integrationAmount = integrationAmount.add(entity.getIntegrationAmount());
  195. growth += entity.getGiftGrowth();
  196. integration += entity.getGiftIntegration();
  197. total = total.add(entity.getRealAmount());
  198. }
  199. // 订单的总额
  200. orderEntity.setTotalAmount(total);
  201. // 应付总额 = 订单的总额 + 运费
  202. orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
  203. // 各种优惠的总额
  204. orderEntity.setPromotionAmount(promotionAmount);
  205. orderEntity.setCouponAmount(couponAmount);
  206. orderEntity.setIntegrationAmount(integrationAmount);
  207. // 积分和成长值总额
  208. orderEntity.setGrowth(growth);
  209. orderEntity.setIntegration(integration);
  210. }
  211. // 构建订单
  212. private OrderEntity buildOrder(String orderSn) {
  213. MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
  214. OrderEntity orderEntity = new OrderEntity();
  215. orderEntity.setOrderSn(orderSn);
  216. orderEntity.setMemberId(memberRespVo.getId()); // 设置会员id
  217. // 获取收货地址信息
  218. // 想要获取ThreadLocal内的数据,必须先往里面set数据
  219. OrderSubmitVo orderSubmitVo = submitVoThreadLocal.get(); // 页面传递过来的数据
  220. R fare = wmsFeignService.getFare(orderSubmitVo.getAddrId());
  221. FareVo fareData = fare.getData(new TypeReference<FareVo>(){});
  222. // 设置运费信息
  223. orderEntity.setFreightAmount(fareData.getFare());
  224. // 设置收货地址信息、收货地址详情信息、收货人名字、收货人手机号、收货人邮编、省市区信息
  225. orderEntity.setReceiverCity(fareData.getAddress().getCity());
  226. orderEntity.setReceiverDetailAddress(fareData.getAddress().getDetailAddress());
  227. orderEntity.setReceiverName(fareData.getAddress().getName());
  228. orderEntity.setBillReceiverPhone(fareData.getAddress().getPhone());
  229. orderEntity.setReceiverPostCode(fareData.getAddress().getPostCode());
  230. orderEntity.setReceiverProvince(fareData.getAddress().getProvince());
  231. orderEntity.setReceiverRegion(fareData.getAddress().getRegion());
  232. // 设置订单状态
  233. orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
  234. orderEntity.setAutoConfirmDay(7);
  235. orderEntity.setDeleteStatus(0); // 删除状态,0未删除
  236. return orderEntity;
  237. }
  238. // 构建所有订单项信息
  239. private List<OrderItemEntity> buildOrderItems(String orderSn) {
  240. // 最后确定每个购物项的价格
  241. List<OrderItemVo> cartItems = cartFeginService.getCurrentUserCartItems();
  242. if(cartItems != null && cartItems.size() > 0){
  243. List<OrderItemEntity> orderItemEntities = cartItems.stream().map(cartItem -> {
  244. // 通过购物车的商品信息返回订单项的详细信息
  245. OrderItemEntity orderItemEntity = buildOrderItem(cartItem);
  246. orderItemEntity.setOrderSn(orderSn);
  247. return orderItemEntity;
  248. }).collect(Collectors.toList());
  249. return orderItemEntities;
  250. }
  251. return null;
  252. }
  253. // 构建指定的某一个订单项
  254. private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
  255. OrderItemEntity itemEntity = new OrderItemEntity();
  256. // 1、订单信息:订单号
  257. // 2、商品的spu信息
  258. Long skuId = cartItem.getSkuId();
  259. // 远程调用商品服务、根据skuId查询spu信息
  260. R r = productFeignService.getSpuInfoBySkuId(skuId);
  261. SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>() { });
  262. itemEntity.setSpuId(data.getId());
  263. itemEntity.setSpuBrand(data.getBrandId().toString());
  264. itemEntity.setSpuName(data.getSpuName());
  265. itemEntity.setCategoryId(data.getCatalogId());
  266. // 3、商品的sku信息
  267. itemEntity.setSkuId(skuId);
  268. itemEntity.setSkuName(cartItem.getTitle());
  269. itemEntity.setSkuPic(cartItem.getImage());
  270. itemEntity.setSkuPrice(cartItem.getPrice());
  271. // 将集合按照指定的分隔符转换为字符串数组
  272. String skuAttr = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";");
  273. itemEntity.setSkuAttrsVals(skuAttr);
  274. itemEntity.setSkuQuantity(cartItem.getCount());
  275. // 4、优惠信息(不做)
  276. itemEntity.setPromotionAmount(new BigDecimal("0"));
  277. itemEntity.setCouponAmount(new BigDecimal("0"));
  278. itemEntity.setIntegrationAmount(new BigDecimal("0"));
  279. // 5、积分信息。成长积分和赠送积分
  280. itemEntity.setGiftGrowth(cartItem.getPrice().intValue() * cartItem.getCount());
  281. itemEntity.setGiftIntegration(cartItem.getPrice().intValue() * cartItem.getCount());
  282. // 6.订单项价格信息。总额 - 优惠金额
  283. // 当前订单项的实际金额
  284. BigDecimal orgin = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
  285. // 当前订单项优惠后的金额
  286. BigDecimal subtract = orgin.subtract(itemEntity.getPromotionAmount()).subtract(itemEntity.getCouponAmount()).subtract(itemEntity.getIntegrationAmount());
  287. itemEntity.setRealAmount(subtract);
  288. return itemEntity;
  289. }
  290. /**
  291. * 关闭订单(30分钟未支付)
  292. * @param entity
  293. */
  294. @Override
  295. public void closeOrder(OrderEntity entity) {
  296. // 查询订单的最新状态(30分钟内容订单状态可能有变化,所以要取最新状态)
  297. OrderEntity orderEntity = this.getById(entity.getId());
  298. if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) { // 订单状态为待付款
  299. // 关闭订单,将订单状态修改为已取消
  300. OrderEntity update = new OrderEntity();
  301. update.setId(entity.getId());
  302. update.setStatus(OrderStatusEnum.CANCLED.getCode());
  303. this.updateById(update);
  304. OrderTo orderTo = new OrderTo();
  305. BeanUtils.copyProperties(orderEntity, orderTo);
  306. try {
  307. // 每一条消息进行日志记录(数据库保存每一条消息的详细信息)
  308. // 定期扫描数据库将失败的消息(待付款的订单数据)再发送到MQ
  309. rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
  310. } catch (Exception e) {
  311. // 将没法送成功的消息进行重试发送
  312. }
  313. }
  314. }
  315. /**
  316. * 根据订单号获取订单实体类
  317. * @param orderSn
  318. * @return
  319. */
  320. @Override
  321. public OrderEntity getOrderByOrderSn(String orderSn) {
  322. OrderEntity entity = this.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
  323. return entity;
  324. }
  325. }

WareSkuController锁定库存

  1. /**
  2. * 为当前订单锁定库存
  3. */
  4. @PostMapping("/lock/order")
  5. public R orderLockStock(@RequestBody WareSkuLockVo vo) {
  6. try{
  7. Boolean stock = wareSkuService.orderLockStock(vo);
  8. return R.ok();
  9. }catch (NoStockException e){
  10. return R.error(BizCodeEnume.NO_STOCK_EXCEPTION.getCode(),BizCodeEnume.NO_STOCK_EXCEPTION.getMsg());
  11. }
  12. }

WareSkuServiceImpl锁定库存

  1. @RabbitListener(queues = "stock.release.stock.queue")
  2. @Service("wareSkuService")
  3. public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {
  4. @Autowired
  5. private WareSkuDao wareSkuDao;
  6. @Autowired
  7. private ProductFeignService productFeignService;
  8. @Autowired
  9. WareOrderTaskService orderService;
  10. @Autowired
  11. WareOrderTaskDetailService orderDetailService;
  12. @Autowired
  13. WareOrderTaskService wareOrderTaskService;
  14. @Autowired
  15. OrderFeignService orderFeignService;
  16. @Autowired
  17. RabbitTemplate rabbitTemplate;
  18. /** 为某个订单锁定库存
  19. * @Transactional(rollbackFor = NoStockException.class):执行要回滚NoStockException异常。
  20. * 可以不用加。因为默认只要是运行时异常都会回滚
  21. *
  22. * 库存解锁的场景:
  23. * 1、下订单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁库存
  24. * 2、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚
  25. * 之前锁定的库存就要自动解锁
  26. */
  27. @Transactional(rollbackFor = NoStockException.class)
  28. @Override
  29. public Boolean orderLockStock(WareSkuLockVo vo) {
  30. // 保存库存工作单详情,方便追溯。
  31. WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
  32. taskEntity.setOrderSn(vo.getOrderSn()); //为哪个订单号锁的库存
  33. orderService.save(taskEntity);
  34. // 1、获取每个商品在每个仓库的库存详情:SkuWareHasStock
  35. List<OrderItemVo> locks = vo.getLocks();
  36. List<SkuWareHasStock> collect = locks.stream().map(item -> {
  37. SkuWareHasStock stock = new SkuWareHasStock();
  38. Long skuId = item.getSkuId();
  39. stock.setSkuId(skuId);
  40. stock.setNum(item.getCount());
  41. // 查询当前商品在哪些仓库有库存
  42. List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
  43. stock.setWareId(wareIds);
  44. return stock;
  45. }).collect(Collectors.toList());
  46. // 2、锁定库存
  47. for (SkuWareHasStock hasStock : collect) {
  48. Boolean skuStocked = false;
  49. Long skuId = hasStock.getSkuId();
  50. List<Long> wareIds = hasStock.getWareId();
  51. Integer num = hasStock.getNum();
  52. // 2.1 若仓库为空。即没有任何仓库有这个商品的库存,直接抛异常
  53. if (wareIds == null || wareIds.size() == 0) {
  54. throw new NoStockException(skuId);
  55. }
  56. // 2.2 遍历仓库,扣库存(每个仓库依次扣库存)
  57. // 如果每个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
  58. // 锁定失败,前面保存的工作单信息就回滚了。发送出去的消息,即使要解锁记录,由于去数据库查不到id,所以就不用解锁
  59. for (Long wareId : wareIds) {
  60. // 2.3 锁定库存。stock_locked加num
  61. // 返回1表示成功(库存表1行受影响),返回0表示失败(0行受影响)
  62. Long count = wareSkuDao.lockSkuStock(skuId, wareId, num);
  63. if (count == 1) {
  64. // 库存锁定成功
  65. skuStocked = true;
  66. // 将数据保存到数据库wms_ware_order_task_detail表
  67. WareOrderTaskDetailEntity detailEntity = new WareOrderTaskDetailEntity(null, skuId, "", hasStock.getNum(), taskEntity.getId(), wareId, 1);
  68. orderDetailService.save(detailEntity);
  69. StockLockedTo stockLockedTo = new StockLockedTo();
  70. stockLockedTo.setId(taskEntity.getId());
  71. StockDetailTo detailTo = new StockDetailTo();
  72. BeanUtils.copyProperties(detailEntity, detailTo);
  73. // 防止回滚之后找不到数据,所以保存完整库存单
  74. stockLockedTo.setDetail(detailTo);
  75. rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", stockLockedTo);
  76. break;
  77. } else {
  78. // 当前仓库库存不足,尝试锁下一个仓库
  79. }
  80. }
  81. // 当前商品所有仓库都无货(没锁住库存)
  82. if (skuStocked == false) {
  83. throw new NoStockException(skuId);
  84. }
  85. }
  86. // 所有商品的库存都是锁定成功才会返回true
  87. return true;
  88. }
  89. //内部类。商品在每个仓库拥有的库存数据
  90. @Data
  91. class SkuWareHasStock {
  92. private Long skuId; // 当前商品的ID
  93. private Integer num; //
  94. private List<Long> wareId; // 有库存的仓库ID
  95. }
  96. /**
  97. * 1、库存自动解锁
  98. * 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚,之前锁定的库存就要自动解锁
  99. * 订单失败:锁库存失败
  100. *
  101. * 只要解锁库存的消息失败,一定要告诉服务解锁失败,需要手动ACK,回复消息
  102. *
  103. * 解锁库存
  104. * 查询数据库关于这个订单的锁定库存信息wms_ware_order_task、wms_ware_order_task_detail
  105. * 若有数据,即表示库存锁定成功。此时根据订单情况判断是否需要解锁库存
  106. * 1、订单不存在,表示订单数据自身已回滚,此时必须解锁库存
  107. * 2、订单存在,根据订单状态确认是否需要解锁库存
  108. * 1)订单状态为已取消,此时需要解锁库存
  109. * 2)订单状态未取消,此时不能解锁库存
  110. * 若没有数据,表示库存锁定失败,库存已回滚,这种情况就无需解锁库存
  111. */
  112. @Override
  113. public void unlockStock(StockLockedTo stockLockedTo){
  114. System.out.println("收到解锁库存的消息......");
  115. StockDetailTo detail = stockLockedTo.getDetail();
  116. Long detailId = detail.getId();
  117. // 解锁库存
  118. // 1.查询关于这个订单的锁定库存信息
  119. WareOrderTaskDetailEntity orderTaskDetailEntity = orderDetailService.getById(detailId);
  120. if (orderTaskDetailEntity != null) {
  121. // 有锁定库存信息,即库存锁定成功,根据订单情况解锁
  122. Long id = stockLockedTo.getId(); //库存工作单wms_ware_order_task表的Id
  123. WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(id);
  124. String orderSn = taskEntity.getOrderSn();
  125. // 远程调用订单服务,根据订单号获取订单实体
  126. R r = orderFeignService.getOrderStatus(orderSn);
  127. if (r.getCode() == 0) {
  128. OrderVo data = r.getData(new TypeReference<OrderVo>() { });
  129. if (data == null || data.getStatus() == 4) {
  130. // 订单不存在(订单数据已经回滚) 或者 有订单但订单状态是已取消,才可以解锁库存
  131. // 只有状态是1(已锁定),才能解锁
  132. if (orderTaskDetailEntity.getLockStatus() == 1) {
  133. unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
  134. // 手动确认RabbitMQ中order.release.order.queue队列的的消息(即消费这条消息)
  135. // channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
  136. }
  137. }
  138. } else {
  139. // 其它状态(包含订单成功)不解锁
  140. // 拒绝消息以后重放到队列里面,让其他服务继续消费解锁(防止因自身原因误删RabbitMQ中的消息)
  141. // channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
  142. throw new RuntimeException("远程服务失败....."); // 需要重新解锁。监听器中已实现
  143. }
  144. } else {
  145. // 若不存在锁定库存信息,即库存锁定失败,库存回滚,这种情况无需解锁
  146. // 手动确认RabbitMQ中order.release.order.queue队列的的消息(即消费这条消息)
  147. // channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
  148. }
  149. }
  150. /**
  151. * 防止因为订单服务故障,导致订单状态未改变,从而无法解锁库存
  152. */
  153. @Transactional
  154. @Override
  155. public void unlockStock(OrderTo orderTo) {
  156. String orderSn = orderTo.getOrderSn();
  157. //查询最新库存状态
  158. WareOrderTaskEntity task = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
  159. Long id = task.getId();
  160. List<WareOrderTaskDetailEntity> list = orderDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
  161. .eq("task_id", id)
  162. .eq("lock_status", 1));
  163. for (WareOrderTaskDetailEntity entity : list) {
  164. unLockStock(entity.getSkuId(), entity.getWareId(), entity.getSkuNum(), entity.getId());
  165. }
  166. }
  167. public void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
  168. // 库存解锁,将锁定库存数stock_locked减去num。即恢复原库存
  169. wareSkuDao.unlockStock(skuId, wareId, num);
  170. // 更新库存工作单状态
  171. WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity();
  172. entity.setId(taskDetailId);
  173. entity.setLockStatus(2); // 2-已解锁
  174. orderDetailService.updateById(entity);
  175. }
  176. }

一、释放订单

1、流程

图片.png

2、订单服务的MQ配置类MyMQConfig

  1. @Configuration
  2. public class MyMQConfig {
  3. // @RabbitListener(queues = "order.release.order.queue")
  4. // public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
  5. //
  6. // System.out.println("收到过期的订单信息,准备关闭订单:" + orderEntity);
  7. // channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
  8. // }
  9. @Bean
  10. public MessageConverter messageConverter() {
  11. return new Jackson2JsonMessageConverter();
  12. }
  13. /**
  14. * 注解@Bean注解:可以在容器中自动创建Binding、Queue、Exchange(RabbitMQ没有的情况)
  15. * RabbitMQ只要存在,@Bean声明的属性发生变化,重启服务后也不会覆盖先前的数据,只有删除队列才行
  16. * 所有的消息默认会先抵达延迟队列order.delay.queue,延迟一段时间后,才会抵达order.release.order.queue队列。然后才被消费掉
  17. * @return
  18. */
  19. // 创建延迟队列(过了存活时间就会变成死信,然后将信息抛给orderReleaseOrderQueue队列,RabbitMQ监听orderReleaseOrderQueue队列,处理数据)
  20. @Bean
  21. public Queue orderDelayQueue() {
  22. Map<String, Object> arguments = new HashMap<>();
  23. // 指定死信路由
  24. arguments.put("x-dead-letter-exchange", "order-event-exchange");
  25. // 指定死信路由键
  26. arguments.put("x-dead-letter-routing-key", "order.release.order");
  27. // 消息过期时间(毫秒)
  28. arguments.put("x-message-ttl", 60000);
  29. Queue orderDelayQueue = new Queue("order.delay.queue", true, false, false, arguments);
  30. return orderDelayQueue;
  31. }
  32. // 创建普通队列
  33. @Bean
  34. public Queue orderReleaseOrderQueue() {
  35. return new Queue("order.release.order.queue", true, false, false);
  36. }
  37. // 创建topic类型交换机
  38. @Bean
  39. public Exchange orderEventExchange() {
  40. return new TopicExchange("order-event-exchange", true, false);
  41. }
  42. // 创建交换机与队列order.delay.queue的绑定关系。Binding(目的地,目的地类型,交换机,路由键,参数)
  43. @Bean
  44. public Binding orderCreateBingding() {
  45. return new Binding("order.delay.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.create.order", null);
  46. }
  47. // 创建交换机与队列order.release.order.queue的绑定关系
  48. @Bean
  49. public Binding orderReleaseBingding() {
  50. return new Binding("order.release.order.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.release.order", null);
  51. }
  52. @Bean
  53. public Binding orderReleaseOtherBingding() {
  54. return new Binding("stock.release.stock.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.release.other.#", null);
  55. }
  56. }

3、订单服务的监听器OrderCloseListener

  1. // 监听order.release.order.queue队列中的消息(30分钟后(订单30分钟未支付场景)才会到达这个队列)
  2. @Component
  3. @RabbitListener(queues = "order.release.order.queue")
  4. public class OrderCloseListener {
  5. @Autowired
  6. OrderService orderService;
  7. @RabbitHandler
  8. public void listener(OrderEntity entity, Channel channel, Message msg) throws IOException {
  9. try {
  10. System.out.println("收到过期的订单信息,准备关闭订单:" + entity.getOrderSn());
  11. orderService.closeOrder(entity);
  12. channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
  13. } catch (Exception e) {
  14. System.out.println("订单关闭异常,库存解锁异常:" + e.getMessage());
  15. // 拒绝消息,让其重新回到消息队列
  16. channel.basicReject(msg.getMessageProperties().getDeliveryTag(), true);
  17. }
  18. }
  19. }

4、订单创建成功后发送消息

通过路由键order.create.order发送给延迟队列order.create.order。30分钟后延迟队列将消息通过交换机order-event-exchange、路由键order.release.order发送给order.release.order.queue、order.release.coupon.queue等队列图片.png

5、关闭订单时消费消息

查询订单的最新状态,判断当前订单是正常关单还是异常关单,若是异常关单,需要回滚相关数据图片.png

二、库存自动解锁

1、流程

图片.png

2、库存服务的MQ配置类MyRabbitConfig

  1. @Configuration
  2. public class MyRabbitConfig {
  3. // 使用JSON序列化机制,进行消息转换
  4. @Bean
  5. public MessageConverter messageConverter() {
  6. return new Jackson2JsonMessageConverter();
  7. }
  8. // 创建topic类型交换机。路由键采用模糊匹配。TopicExchange(交换机名称,是否持久化,是否自动删除)
  9. @Bean
  10. public Exchange stockEventExchange() {
  11. return new TopicExchange("stock-event-exchange", true, false);
  12. }
  13. // 创建普通队列。Queue(队列名称,是否持久化,是否排他(只允许单个连接它,若支持多个连接,则谁抢到消息算谁的),是否自动删除)
  14. @Bean
  15. public Queue stockReleaseStockQueue() {
  16. return new Queue("stock.release.stock.queue", true, false, false);
  17. }
  18. // 创建延迟队列。库存锁定成功后,消息先发给延迟队列,等待消息过期后,再发给普通队列
  19. @Bean
  20. public Queue stockDelayQueue() {
  21. Map<String, Object> arguments = new HashMap<>();
  22. // 设置死信路由,表示消息过期后交给哪个交换机
  23. arguments.put("x-dead-letter-exchange", "stock-event-exchange");
  24. // 设置死信路由键,表示消息过期后交给哪个路由键
  25. arguments.put("x-dead-letter-routing-key", "stock.release");
  26. // 设置消息过期时间
  27. arguments.put("x-message-ttl", 60000);
  28. return new Queue("stock.delay.queue", true, false, false, arguments);
  29. }
  30. // 创建交换机与普通队列的绑定关系
  31. @Bean
  32. public Binding stockReleaseBinding() {
  33. return new Binding("stock.release.stock.queue", Binding.DestinationType.QUEUE, "stock-event-exchange", "stock.release.#", null);
  34. }
  35. // 创建交换机与延迟队列的绑定关系
  36. @Bean
  37. public Binding stockLockedBinding() {
  38. return new Binding("stock.delay.queue", Binding.DestinationType.QUEUE, "stock-event-exchange", "stock.locked", null);
  39. }
  40. // 第一次监听消息时,idea会连接RabbitMQ,此时才会创建RabbitMQ中没有的队列、交换机和绑定关系
  41. // 如果没有监听消息操作,RabbitMQ中就不会创建队列、交换机和绑定关系
  42. // 如果需要修改rabbitMQ中已存在的队列交换机,需要先删除,然后再次创建
  43. // @RabbitListener(queues = "stock.release.stock.queue") // 需要注释掉,否则此时有两个在监听stock.release.stock.queue队列,导致消息消费异常
  44. // public void listener(WareInfoEntity entity, Channel channel, Message msg) throws IOException, IOException {
  45. // System.out.println("收到过期的订单信息:准备关闭订单" + entity.getId());
  46. // channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
  47. // }
  48. }

3、库存服务的监听器StockReleaseListener

  1. @Service
  2. @RabbitListener(queues = "stock.release.stock.queue")
  3. public class StockReleaseListener {
  4. @Autowired
  5. WareSkuService wareSkuService;
  6. /**
  7. * 监听解锁库存功能
  8. */
  9. @RabbitHandler
  10. public void handleStockLockedRelease(StockLockedTo stockLockedTo, Message message, Channel channel) throws IOException {
  11. System.out.println("收到接收解锁库存的信息......");
  12. try {
  13. wareSkuService.unlockStock(stockLockedTo);
  14. // 手动确认RabbitMQ中order.release.order.queue队列的的消息(即消费这条消息)
  15. channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
  16. }catch (Exception e){
  17. System.out.println("rabbitMQ错误:"+e.getMessage());
  18. // 只要有任何异常,回退消息。拒绝消息以后重放到队列里面,让其他服务继续消费解锁(防止因自身原因误删RabbitMQ中的消息)
  19. channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
  20. }
  21. }
  22. }

4、锁定库存后发送消息图片.png

5、解锁库存时消费消息

  1. /**
  2. * 1、库存自动解锁
  3. * 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚,之前锁定的库存就要自动解锁
  4. * 订单失败:锁库存失败
  5. *
  6. * 只要解锁库存的消息失败,一定要告诉服务解锁失败,需要手动ACK,回复消息
  7. *
  8. * 解锁库存
  9. * 查询数据库关于这个订单的锁定库存信息wms_ware_order_task、wms_ware_order_task_detail
  10. * 若有数据,即表示库存锁定成功。此时根据订单情况判断是否需要解锁库存
  11. * 1、订单不存在,表示订单数据自身已回滚,此时必须解锁库存
  12. * 2、订单存在,根据订单状态确认是否需要解锁库存
  13. * 1)订单状态为已取消,此时需要解锁库存
  14. * 2)订单状态未取消,此时不能解锁库存
  15. * 若没有数据,表示库存锁定失败,库存已回滚,这种情况就无需解锁库存
  16. */
  17. @Override
  18. public void unlockStock(StockLockedTo stockLockedTo){
  19. System.out.println("收到解锁库存的消息......");
  20. StockDetailTo detail = stockLockedTo.getDetail();
  21. Long detailId = detail.getId();
  22. // 解锁库存
  23. // 1.查询关于这个订单的锁定库存信息
  24. WareOrderTaskDetailEntity orderTaskDetailEntity = orderDetailService.getById(detailId);
  25. if (orderTaskDetailEntity != null) {
  26. // 有锁定库存信息,即库存锁定成功,根据订单情况解锁
  27. Long id = stockLockedTo.getId(); //库存工作单wms_ware_order_task表的Id
  28. WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(id);
  29. String orderSn = taskEntity.getOrderSn();
  30. // 远程调用订单服务,根据订单号获取订单实体
  31. R r = orderFeignService.getOrderStatus(orderSn);
  32. if (r.getCode() == 0) {
  33. OrderVo data = r.getData(new TypeReference<OrderVo>() { });
  34. if (data == null || data.getStatus() == 4) {
  35. // 订单不存在(订单数据已经回滚) 或者 有订单但订单状态是已取消,才可以解锁库存
  36. // 只有状态是1(已锁定),才能解锁
  37. if (orderTaskDetailEntity.getLockStatus() == 1) {
  38. unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
  39. // 手动确认RabbitMQ中order.release.order.queue队列的的消息(即消费这条消息)
  40. // channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
  41. }
  42. }
  43. } else {
  44. // 其它状态(包含订单成功)不解锁
  45. // 拒绝消息以后重放到队列里面,让其他服务继续消费解锁(防止因自身原因误删RabbitMQ中的消息)
  46. // channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
  47. throw new RuntimeException("远程服务失败....."); // 需要重新解锁。监听器中已实现
  48. }
  49. } else {
  50. // 若不存在锁定库存信息,即库存锁定失败,库存回滚,这种情况无需解锁
  51. // 手动确认RabbitMQ中order.release.order.queue队列的的消息(即消费这条消息)
  52. // channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
  53. }
  54. }

三、订单关闭后主动解锁库存

1、流程

图片.png

2、订单服务的MQ配置类MyMQConfig

创建订单交换机与库存释放队列stock.release.stock.queue的绑定关系图片.png

3、库存服务的MQ监听器StockReleaseListener

监听库存服务的stock.release.stock.queue队列图片.png

4、关闭订单后发送消息

OrderServiceImpl订单关闭后(30分钟未支付),将消息通过订单交换机order-event-exchange、路由键order.release.other发送给库存队列stock.release.stock.queue监听库存队列,收到消息后判断该订单的状态,若为已取消,则主动释放库存图片.png

5、解锁库存时消费消息

监听库存队列stock.release.stock.queue,收到消息后解锁库存,WareSkuServiceImpl判断该订单的状态,若为已取消,则主动释放库存图片.png

下单流程

1、验证令牌。令牌的对比和删除必须保证原子性
2、创建订单,订单项等信息
3、验价:根据订单计算的价格与界面提交的价格进行对比(可能因后台修改商品价格出现不一致的情况)
4、保存订单数据(OrderEntity和OrderItemEntity)至数据库
5、远程锁定库存、只要有异常就回滚订单数据
6、远程扣减积分
7、订单创建成功,将每个订单信息发送给MQ
Copy of 下单操作 - 图12

订单信息(OrderEntity)发送给MQ

1、通过路由键order.create.order发送给延迟队列order.create.order。
2、30分钟后延迟队列将消息通过交换机order-event-exchange、路由键order.release.order发送给order.release.order.queue、order.release.coupon.queue等队列
3、OrderCloseListener监听order.release.order.queue队列数据,调用关闭订单方法orderService.closeOrder(entity)
4、closeOrder首先查询订单的最新状态,若订单状态仍为待付款,主动关闭订单(即回滚数据),即将订单状态更新为已取消;同时将这条订单消息再次发送给MQ,30分钟后再次判断订单状态(正常情况此时应该为已取消,目的是防止关单程序异常导致关单失败)

解锁库存

库存锁定成功后,将锁定库存信息(StockLockedTo:含库存工作单Id和工作的详情表所有信息)发送给MQ。
1、通过路由键stock.locked发送给延迟队列stock.delay.queue
2、2分钟后延迟队列将消息通过交换机stock-event-exchange、路由键stock.release发送给stock.release.stock.queue队列
3、StockReleaseListener监听stock.release.stock.queue队列数据,调用解锁库存方法wareSkuService.unlockStock(stockLockedTo)
4、解锁库存
1)查询关于这个订单的锁定库存信息WareOrderTaskDetailEntity,若有锁定库存信息,即库存锁定成功,根据订单情况解锁
2)远程调用订单服务,根据订单号获取订单实体
3)只有订单不存在(订单数据已经回滚) 或者 有订单但订单状态是已取消,同时锁定库存详情的锁定状态为已锁定,才可以解锁库存。其他状态不需要解锁库存
4)库存解锁,将WareSkuEntity表的锁定库存数stock_locked减去num。即恢复原库存
5)更新库存工作单WareOrderTaskDetailEntity的锁定状态为已解锁

MQ消息记录表

图片.png图片.png图片.png