> 首先我们在这里严重地批评一些,在接口订单的接口中,直接传订单金额,而不是使用下单是已经计算好金额的人,这些接口岂不是使用0.01就能将全部的商品都买下来了? 我们回到订单设计这一个模块,首先我们在确认订单的时候就已经将价格计算完成了,那么我们肯定是想将计算结果给保留下来的,至于计算的过程,我们并不希望这个过程还要进行一遍的计算。 我们返回确认订单的接口,看到这样一行代码: ```java @ApiOperation(value = "结算,生成订单信息", notes = "传入下单所需要的参数进行下单") public ResponseEntity confirm(@Valid @RequestBody OrderParam orderParam) { // 缓存计算 confirmOrderManager.cacheCalculatedInfo(userId, shopCartOrderMerger); } ``` ```java public void cacheCalculatedInfo(String userId, ShopCartOrderMergerDto shopCartOrderMerger) { // 防止重复提交 RedisUtil.STRING_REDIS_TEMPLATE.opsForValue().set(OrderCacheNames.ORDER_CONFIRM_UUID_KEY + CacheNames.UNION + userId, String.valueOf(userId)); // 保存订单计算结果缓存,省得重新计算 并且 用户确认的订单金额与提交的一致 cacheManagerUtil.putCache(OrderCacheNames.ORDER_CONFIRM_KEY, String.valueOf(userId), shopCartOrderMerger); } ``` 这里每经过一次计算,就将整个订单通过`userId`进行了保存,而这个缓存的时间为30分钟,当用户使用 ```java @PostMapping("/submit") @ApiOperation(value = "提交订单,返回支付流水号", notes = "根据传入的参数判断是否为购物车提交订单,同时对购物车进行删除,用户开始进行支付") public ResponseEntity submitOrders(@Valid @RequestBody SubmitOrderParam submitOrderParam) { String userId = SecurityUtils.getUser().getUserId(); ServerResponseEntity orderCheckResult = submitOrderManager.checkSubmitInfo(submitOrderParam, userId); // 省略中间一大段。。。 orderService.removeConfirmOrderCache(userId + submitOrderParam.getUuid()); } ``` 其中`checkSubmitInfo`这个方法里面有一段代码如下 ```java ShopCartOrderMergerDto mergerOrder = cacheManagerUtil.getCache(OrderCacheNames.ORDER_CONFIRM_KEY, String.valueOf(userId)); // 看看订单有没有过期 if (Objects.isNull(mergerOrder)) { // 订单已过期,请重新下单 throw new YamiShopBindException("yami.order.expired"); } // 防止重复、同时提交 boolean cad = RedisUtil.cad(OrderCacheNames.ORDER_CONFIRM_UUID_KEY + OrderCacheNames.UNION + userId, userId); if (!cad) { OrderNumbersDto orderNumbersDto = new OrderNumbersDto(null); orderNumbersDto.setDuplicateError(1); return ServerResponseEntity.fail(ResponseEnum.REPEAT_ORDER); } // 看看订单的标记有没有过期 if (cacheManagerUtil.getCache(OrderCacheNames.ORDER_CONFIRM_KEY, userId) == null) { // 订单已过期,请重新下单 throw new YamiShopBindException("yami.order.expired"); } ``` 当无法获取缓存的时候告知用户订单过期,当订单进行提交完毕的时候,将之前的缓存给清除。 我们又回到提交订单中间这几行代码: ```java List orders = orderService.submit(mergerOrder); ``` 这行代码也就是提交订单的核心代码 ```java eventPublisher.publishEvent(new SubmitOrderEvent(mergerOrder, orderList)); ``` 其中这里依旧是使用时间的方式,将订单进行提交,看下这个`SubmitOrderEvent`的默认监听事件。 ```java @Component("defaultSubmitOrderListener") @AllArgsConstructor public class SubmitOrderListener { public void defaultSubmitOrderListener(SubmitOrderEvent event) { // ... } } ``` - 为每个店铺生成一个订单 ```java // 每个店铺生成一个订单 for (ShopCartOrderDto shopCartOrderDto : shopCartOrders) { } ``` 这里为每个店铺创建一个订单,是为了以后平台结算给商家时,每个商家的订单单独结算。用户确认收货时,也可以为每家店铺单独确认收货。 - 当用户提交订单的时候,购物车里面勾选的商品,理所当然的要清空掉 ```java // 删除购物车的商品信息 if (!basketIds.isEmpty()) { basketService.deleteShopCartItemsByBasketIds(userId, basketIds); } ``` - 我们的库存都是存在redis的,这里使用redis的lua脚本进行锁定库存,防止超卖: ```java @PostMapping("/submit") @Operation(summary = "提交订单,返回支付流水号" , description = "根据传入的参数判断是否为购物车提交订单,同时对购物车进行删除,用户开始进行支付,根据店铺进行拆单") public ServerResponseEntity submitOrders(@Valid @RequestBody SubmitOrderParam submitOrderParam){ // 锁定sku库存 submitOrderManager.tryLockStock(mergerOrder,userId); } ``` 具体可以看下代码中`OrderStock.lua`脚本,里面备注很详细了。在这一步可以看到我们锁库存的时候没有使用事务,而用lua脚本只能保证脚本的事务,不能保证提交订单整个事务,所以在这里我们有用到定时任务来保证后续创建订单失败时库存正确解锁,具体可以看`OrderTask#cancelOrder()`。 ``` @XxlJob("cancelOrder") public void cancelOrder() { logger.info("解锁创建失败订单的库存。。。"); orderService.unLockFailOrderStock(); ... } ``` 可以看到`orderService.unLockFailOrderStock()`里面的`skuStockLockService.unLockStock(skuStockList)`方法进行了执行lua脚本解锁了创建失败的订单库存 ``` public Map unLockStock(List skuStockLocks) { // 返回 订单id_仓库id_skuId_剩余库存,订单id_仓库id_skuId_剩余库存,... List list = new ArrayList<>(); for (SkuStockVO skuStockVO : skuStockLocks) { if (Objects.isNull(skuStockVO.getStockPointId())) { skuStockVO.setStockPointId(Constant.ZERO_LONG); } String[] strArray = StockUtil.loadQueryArray( skuStockVO.getOrderNumber(), skuStockVO.getStockPointId(), skuStockVO.getSkuId(), skuStockVO.getProdId(), 0, skuStockVO.getDefaultPointId() ); list.add(strArray); } String result = RedisLuaUtil.execute(stringRedisTemplate, ORDER_UNLOCK_STOCK_BYTES, list); if (StrUtil.isBlank(result)) { return Collections.emptyMap(); } List skuStockList = StockUtil.listOrderResult(result); return skuStockList.stream().collect( Collectors.toMap(skuStockVO -> skuStockVO.getOrderNumber() + Constant.UNDERLINE + skuStockVO.getSkuId(), Function.identity(), (v1, v2) -> v1)); } ``` 可以看到这里用了`ORDER_UNLOCK_STOCK_BYTES`lua脚本,具体可以看下脚本里面的详细备注,这里就不多赘述了。 看下`deliverySubmitOrderListener`的代码 这里有几段值得注意的地方: - 这里是`UserAddrOrder` 并不是`UserAddr`: ```java // 把订单地址保存到数据库 UserAddrOrder userAddrOrder = mapperFacade.map(mergerOrder.getUserAddr(), UserAddrOrder.class); if (userAddrOrder == null) { throw new YamiShopBindException("请填写收货地址"); } userAddrOrder.setUserId(userId); userAddrOrder.setCreateTime(now); userAddrOrderService.save(userAddrOrder); ``` 这里是将订单的收货地址进行了保存入库的操作,这里是绝对不能只保存用户的地址id在订单中的,要将地址入库,原因是如果用户在订单中设置了一个地址,如果用户在订单还没配送的时候,将自己的地址改了的话。如果仅采用关联的地址,就会出现问题。 最后我们回到`controller` ```java return ServerResponseEntity.success(new OrderNumbersDto(orderNumbers.toString())); ``` 这里面返回了多个订单项,这里就变成了并单支付咯,在多个店铺一起进行支付的时候需要进行并单支付的操作,一个店铺的时候,又要变成一个订单支付的操作,可是我们只希望有一个统一支付的接口进行调用,所以我们的支付接口要进行一点点的设计咯。