document
API test

提交订单

POST

Description or Example

# Bug修复 ## RedirectAttribute与Alibaba的GenerFastJson序列化器不兼容的问题 ### 分析原因 > 简要来说, 因为SpringSession存储会话在Redis, 因此里面的对象可以用GenerFastJson这个序列化器进行序列化和反序列化, 但是, RedirectAttribute的底层是FlashMap, 默认情况下是不支持该类型的反序列化的, 详情解决方案在下图 ![关于SpringSession下FlashMap无法反序列化的问题.png](https://cos.easydoc.net/13568421/files/lmj6d2vn) # 知识 ## 为什么需要防重令牌 > 因为我们提交订单的时候可能网络突然间故障, 导致误认为订单没有提交而重复点击, 导致订单的重复提交, 因此在确认也的时候保存了一个防重令牌并被备份到Redis, 下订单的时候校验防重令牌, 完成幂等性 ### 防重令牌的比较为什么用LUA脚本 > 用LUA脚本的Redis指令具有原子性, 这里需要保证原子性, 倘若我们不能保证原子性, 那么在判断成功等待删除的时候, 有可能另一个请求同时进来也判断成功了, 最终可能会导致同一个订单被重复提交, 无法解决幂等性问题, 因此, 这里需要保证原子性 ## 为什么不直接收集订单项 > 如果想要提交订单, 固然要收集每一个订单项, 但是, 有这么一个场景, 即, 当我们的购物项的选中属性发生改变时, 提交订单的个数也应该发生改变, 为了实现该效果, 我们不能直接从确认页收订单, 因为有些订单可能会被取消, 或者新增一些购物项, 所以订单项需要重新获取 ## 下订单的时序图 ## 为什么要创建OrderBO对象 > 因为当我们下订单的时候, 我们需要往后端的数据库表中存储数据, 比如订单数据, 订单项数据, 还有比价等, 因此, 我们需要一个BO对象批量将这些数据收集起来 > 这里因为前端需要的数据只有订单实体, 其他的不需要, 因此BO和VO的数据会有不同, 并不是说前端要什么才查什么 ## 异步的要点 > 这里的异步, 有一个操作是获取订单项的, 而订单项的获取需要登陆状态, 异步情况下, 我们的请求拦截器在fegin调用的时候把以ThreadLocal为原理的线程上下文请求头赋值, 而异步情况下, 则则没有请求上下文, 自然没有Cookie信息, 自然没有登陆状态, 也自然获取不到数据 ## 为什么要比价 > 因为前端传过来的总价不一定是正确的, 该价格可能会被恶意篡改, 如果我们扣除了恶意篡改的价格非常的危险, 因此需要比价 ## 为什么锁定库存失败了仍有订单数据 > 因为锁定库存需要调用另一个微服务, 而它们两个微服务都是使用本地事务, 当库存微服务发生异常时, 锁定库存全部回滚, 但是该异常在controller被捕获了, 因此, 对于订单微服务不可知, 因此, 订单不回滚 > 我们可以判断返回的状态码, 如果状态码异常, 手动抛异常, 让订单微服务也回滚 ## 为什么需要库存工作单? > 因为我们采用的是**可靠消息+最终一致性**, 所以, 我们需要手动反向补偿数据, 因此, 为了反向补偿数据, 我们需要记录我们都做过什么, 怎么做的, 就可以根据这些来反向补偿数据, 否则无从反向补偿, 因此, 工作单就是用来记录工作状态的 ## 批量保存工作单详情为什么可靠? > 单个保存的场景下-如果ware微服务发生了异常, 如果单个保存工作单, 会因为异常的原因全部回滚 > 批量保存的场景下-如果ware微服务发生了异常, 根本执行不了批量保存, 和单个保存一致, 最终都没有对应的数据 > 没有异常的场景下, 两个的最终结果都一样, 都是保存了多个工作单详情 > 在宕机情况下, 两个的最终结果都一样, 都会因为没有提交事务全部不发生 ## 批量发送消息为什么可靠? > 在正常情况下, 单个发送和批量发送的结果都一样 > 在宕机情况下, 单个发送会发送无用消息, 无用消息不会被监听器处理, 而批量发送而不会发送消息, 最终的结果都是不处理 ## ~~为批量发送消息失败需要回滚?~~ > ~~如果批量发送消息失败, 说明有某个订单项的消息失败了, 这就很严重了, 这个订单项可能会因为没有成功发送这个消息而导致不具有回滚能力, 这就会产生锁定泄漏问题, 因此, 如果发送不了消息, 就不让他锁定库存, 整体失败~~ ## 为什么发送消息失败是被允许的? > 因为我们可以在发送消息的时候记录消息的日志, 将日志保存在数据库, 然后通过一个定时任务扫描数据库, 将失败的消息重新发送 > 无论是哪个失败都没有问题 ## 为什么需要发送消息 > 因为为了完成订单自动取消的机制, 发送消息, 通过延迟队列, 可以实现在指定时间外自动释放消息 # 核心代码 ## 逻辑简述 > 调用service来提交订单, 提交订单的时候可能会出现异常或错误, 因此, 如果出现异常或返回的状态码不对, 代表着提交订单失败, 重定向到当前页 ## 代码 ```java @PostMapping("/submitOrder") public String submitOrder(OrderSubmitVO orderSubmitVO, Model model, RedirectAttributes redirectAttributes) { try { OrderSubmitRespVO orderSubmitRespVO = orderSubmitService.submitOrder(orderSubmitVO); if (orderSubmitRespVO.getCode() == 0) { // 只有状态码为0, 下单成功 model.addAttribute(orderSubmitRespVO); return "pay"; } // 下单失败 HashMap<String, String> errors = new HashMap<>(); errors.put("reason", orderSubmitRespVO.getReason()); redirectAttributes.addFlashAttribute("errors", errors); return "redirect:http://order.bitmall.com/toTrade"; // 重定向到当前订单页 } catch (RuntimeException runtimeException) { HashMap<String, String> errors = new HashMap<>(); errors.put("reason", runtimeException.getMessage()); redirectAttributes.addFlashAttribute("errors", errors); return "redirect:http://order.bitmall.com/toTrade"; // 重定向到当前订单页 } } ``` ## 逻辑简述 ```java @Override // @GlobalTransactional @Transactional public OrderSubmitRespVO submitOrder(OrderSubmitVO orderSubmitVO) { //TODO: 异步执行图 // ===================权限校验===================== // 0. 创建结果 OrderSubmitRespVO orderSubmitRespVO = new OrderSubmitRespVO(); // 1. 获取会员信息 MemberVO memberVO = UserLoginInterceptor.USER_STATE.get(); Long memberId = memberVO.getId(); // 获取会员的ID String orderToken = orderSubmitVO.getOrderToken(); // 2. 判断是否使用过该防重令牌, 如果使用过, 即重复操作, 直接返回 Long resNum = redisTemplate.execute(new DefaultRedisScript<>("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", Long.class), Collections.singletonList(OrderConstant.ORDER_TOKEN_PREFIX + memberId), orderToken); if (resNum == null || resNum == 0L) { // 没有重复操作, 封装订单 return orderSubmitRespVO.setCode(OrderConstant.ErrorEnum.REPEATED_COMMIT.getCode()) .setReason(OrderConstant.ErrorEnum.REPEATED_COMMIT.getMsg()); // 因为重复操作而退出 } // 没有重复操作, 封装订单 // ===================封装数据===================== // 3.0 获取订单号 String orderSn = IdWorker.getTimeId(); // 3.1 创建订单的BO对象 OrderBO orderBO = new OrderBO(); // 创建阶段性结果 StageResult stageResult = new StageResult(); // 注意: RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); // 3.2 获取运费信息 // TASK 1 有异步 CompletableFuture<Void> getFareTask = CompletableFuture.runAsync(() -> { R info = wareService.getFare(orderSubmitVO.getAddrId()); stageResult.setFareVO(info.getData(new TypeReference<FareVO>(){})); }, threadPoolExecutor); // TASK 1 无远程 CompletableFuture<Void> getOrderEntityTask = CompletableFuture.runAsync(() -> { // 3.2 构建订单 stageResult.setOrderEntity(this.buildOrderEntityINOrderBO(orderSubmitVO, memberVO, orderSn)); }, threadPoolExecutor); // TASK 1 有远程 CompletableFuture<Void> getOrderItemEntities = CompletableFuture.runAsync(() -> { // 这里需要登录信息, 异步情况下, 登录信息会丢失 RequestContextHolder.setRequestAttributes(requestAttributes); // 3.3 构建订单项 stageResult.setOrderItemEntities(this.buildOrderItemsEntitiesINOrderBO(orderSn, memberVO)); }, threadPoolExecutor); // 等待任务完成 CompletableFuture.allOf(getFareTask, getOrderEntityTask, getOrderItemEntities).join(); // TASK 2 无远程 this.buildOrderEntityINOrderBO(stageResult.orderItemEntities, stageResult.orderEntity, stageResult.fareVO); orderBO.setOrderEntity(stageResult.orderEntity) .setOrderItemEntities(stageResult.orderItemEntities) .setFare(stageResult.fareVO.getFare()) .setPayPrice(orderSubmitVO.getPayPrice()); // ===================比价环节===================== BigDecimal cartPrice = orderSubmitVO.getPayPrice(); // 购物车里面的总价 BigDecimal currentPrice = orderBO.getOrderEntity().getPayAmount(); // 最新的总价 // 进行比价 if (currentPrice.subtract(cartPrice).abs().compareTo(new BigDecimal("0.01")) > 0) { return orderSubmitRespVO.setCode(OrderConstant.ErrorEnum.PRICE_NOT_RIGHT.getCode()) .setReason(OrderConstant.ErrorEnum.PRICE_NOT_RIGHT.getMsg()); } // ===================保存订单以及订单项===================== orderService.save(orderBO.getOrderEntity()); orderItemService.saveBatch(orderBO.getOrderItemEntities()); // 这种方案是为了适配seata的, 不建议使用 // for (OrderItemEntity orderItemEntity : orderBO.getOrderItemEntities()) { // orderItemService.save(orderItemEntity); // } // ===================库存锁定===================== List<WareLockTO.OrderItemInfo> orderItemInfoList = orderBO.getOrderItemEntities().stream().map(orderItem -> { WareLockTO.OrderItemInfo orderItemInfo = new WareLockTO.OrderItemInfo(); return orderItemInfo.setSkuId(orderItem.getSkuId()).setNum(orderItem.getSkuQuantity()); }).collect(Collectors.toList()); WareLockTO wareLockTO = new WareLockTO(); R info = wareService.lockSkuStock(wareLockTO.setOrderSn(orderSn).setOrderItemInfoList(orderItemInfoList)); if (info.getCode() != 0) { // 锁定库存失败 // return orderSubmitRespVO.setCode(OrderConstant.ErrorEnum.STOCK_NOT_ENOUGH.getCode()) // .setReason(OrderConstant.ErrorEnum.STOCK_NOT_ENOUGH.getMsg()); throw new RuntimeException(OrderConstant.ErrorEnum.STOCK_NOT_ENOUGH.getMsg()); } // ===================发送消息===================== // 发送消息失败其实也没什么关系, 保存在日志里面重新发送就行 // 但是 如果这里有问题, 有可能是超时 也可能是 保存日志失败, 因此, 这些因此都有可能导致消息丢失, 因此需要回滚 info = mqService.sendMessageWithOrder(orderSn); if (info.getCode() != 0) { // 锁定库存失败 throw new RuntimeException(OrderConstant.ErrorEnum.MESSAGE.getMsg()); } return orderSubmitRespVO.setCode(0).setOrderEntity(orderBO.getOrderEntity()); // 没有任何的问题 } ``` ```java /** * 构建一部分订单信息, 这部分订单信息并不会与订单项耦合 * @param orderSubmitVO * @param memberVO * @return */ private OrderEntity buildOrderEntityINOrderBO(OrderSubmitVO orderSubmitVO, MemberVO memberVO, String orderSn) { OrderEntity orderEntity = new OrderEntity(); orderEntity.setOrderSn(orderSn) .setCreateTime(new Date()) .setMemberUsername(memberVO.getUsername()) .setNote(orderSubmitVO.getNote()) .setPayType(orderSubmitVO.getPayType().intValue()) .setSourceType(1) .setStatus(OrderStatusEnum.CREATE_NEW.getCode()) .setAutoConfirmDay(15) .setConfirmStatus(0) .setDeleteStatus(0) .setUseIntegration(memberVO.getIntegration()); return orderEntity; } ``` ```java private List<OrderItemEntity> buildOrderItemsEntitiesINOrderBO(String orderSn, MemberVO memberVO) { // 重新获取订单项 List<OrderItemVO> orderItemVOS = cartService.getMemberAllOrderItemsWithOrder(); if (orderItemVOS != null && !orderItemVOS.isEmpty()) { List<Long> skuIds = orderItemVOS.stream().map(OrderItemVO::getSkuId).collect(Collectors.toList()); // 通过大多数的sku分别找到对应的SPU信息 CompletableFuture<Map<Long, OrderItemSpuInfoTO>> getAllSpuIdsTask = CompletableFuture.supplyAsync(() -> productService.getAllSpuInfoBySkuIds(skuIds) , threadPoolExecutor); CompletableFuture<Map<Long, BigDecimal>> getPriceTask = CompletableFuture.supplyAsync(() -> productService.getCurrentPrice(skuIds) , threadPoolExecutor); CompletableFuture<List<OrderItemEntity>> packageObjTask = getPriceTask.thenCombineAsync(getAllSpuIdsTask, (currentPrice, spuInfoBySkuIds) -> orderItemVOS.stream().map(orderVO -> { OrderItemEntity orderItemEntity = new OrderItemEntity(); OrderItemSpuInfoTO orderItemSpuInfoTO = spuInfoBySkuIds.get(orderVO.getSkuId()); BigDecimal defaultDiscount = new BigDecimal(0L); String skuAttr = StringUtils.collectionToDelimitedString(orderVO.getSkuAttr(), ";"); return orderItemEntity.setOrderSn(orderSn) .setSpuId(orderItemSpuInfoTO.getSpuId()) .setSpuName(orderItemSpuInfoTO.getSpuName()) .setSpuPic(orderItemSpuInfoTO.getSpuPic()) .setSpuBrand(orderItemSpuInfoTO.getBrandId().toString()) .setCategoryId(orderItemSpuInfoTO.getCatalogId()) .setSkuId(orderVO.getSkuId()) .setSkuName(orderVO.getTitle()) .setSkuPic(orderVO.getImage()) .setSkuPrice(currentPrice.get(orderItemEntity.getSkuId())) .setSkuQuantity(orderVO.getCount()) .setSkuAttrsVals(skuAttr) .setPromotionAmount(defaultDiscount) .setCouponAmount(defaultDiscount) .setIntegrationAmount(new BigDecimal(memberVO.getIntegration() != null ? memberVO.getIntegration() : 0).multiply(ORDER_PAY_RATE)) .setRealAmount( orderItemEntity.getSkuPrice().multiply(new BigDecimal(orderItemEntity.getSkuQuantity())) .subtract(defaultDiscount) .subtract(defaultDiscount) .subtract(orderItemEntity.getIntegrationAmount()) ) .setGiftGrowth(orderItemEntity.getSkuPrice().intValue()) .setGiftIntegration(orderItemEntity.getSkuPrice().intValue()); }).collect(Collectors.toList()) , threadPoolExecutor); return packageObjTask.join(); } return null; } ``` ```java /** * 构建需要与订单项耦合的订单 * @param orderItemEntities * @param orderEntity */ private void buildOrderEntityINOrderBO(List<OrderItemEntity> orderItemEntities, OrderEntity orderEntity, FareVO fareVO) { MemberAddressVO address = fareVO.getMemberAddressVO(); if (orderItemEntities != null && !orderItemEntities.isEmpty()) { BigDecimal totalAmount = new BigDecimal(0); BigDecimal promotionAmount = new BigDecimal(0); BigDecimal integrationAmount = new BigDecimal(0); BigDecimal couponAmount = new BigDecimal(0); BigDecimal discountAmount = new BigDecimal(0); BigDecimal integration = new BigDecimal(0); BigDecimal growth = new BigDecimal(0); for (OrderItemEntity orderItemEntity : orderItemEntities) { totalAmount = totalAmount.add(orderItemEntity.getRealAmount()); promotionAmount = promotionAmount.add(orderItemEntity.getPromotionAmount()); integrationAmount = integrationAmount.add(orderItemEntity.getIntegrationAmount()); couponAmount = couponAmount.add(orderItemEntity.getCouponAmount()); integration = integration.add(new BigDecimal(orderItemEntity.getGiftIntegration())); growth = growth.add(new BigDecimal(orderItemEntity.getGiftGrowth())); } orderEntity.setTotalAmount(totalAmount) .setPayAmount(totalAmount.add(fareVO.getFare())) .setPromotionAmount(promotionAmount) .setIntegrationAmount(integrationAmount) .setCouponAmount(couponAmount) .setDiscountAmount(discountAmount) .setIntegration(integration.intValue()) .setGrowth(growth.intValue()) .setMemberId(address.getMemberId()) .setReceiverCity(address.getCity()) .setReceiverRegion(address.getRegion()) .setReceiverProvince(address.getProvince()) .setReceiverDetailAddress(address.getDetailAddress()) .setReceiverPostCode(address.getPostCode()) .setReceiverName(address.getName()) .setReceiverPhone(address.getPhone()) .setFreightAmount(fareVO.getFare()); } } ```