document
API test

确认页

POST

Description or Example

# Bug修复 ## 异步任务的相关知识 > 总所周知, 异步任务需要遵循等待原则, 即需要等待所有的任务执行完毕才能返回, 但是, 这里有一个易错点, 即阶段传递, `suppleAsync()`, 这个通常是连接两个阶段的载体, 因此, 在最后等待的时候, 需要等待这两个阶段, 否则第二个阶段可能会没有完成 # 相关知识 ## 只要选中的购物项? > 因为购物车里面有多个购物项, 只有选中的才能进订单, 因此, 在获取购物项的时候需要对选中状态进行过滤, 仅选中选中项 ## 为什么获取最新价格? > 因为加入购物车的时间和下单的时间不一样, 加入购物车时, 数据被持久化在Redis, 价格是那个时间的, 当前时间的价格和那个时间的有所不同, 因此, 我们需要查询每件商品的最新价格 ### 老师获取价格有什么致命的缺陷?以及修改方案? > 老师的获取价格复用了之前的API, 即利用增强FOR, 遍历每一个订单项, 利用订单项的SkuId去查询最新的价格 > 这里最大的缺点就是, 每次远程微服务只能获取一个订单项的最新价格信息, 如果有N个订单项就要远程调用N次, 效率极低, 不建议使用 > 因此, 我们可以采取一次查询的方式, 即, 我们可以将全部SkuId收集起来, 一次性全部查完, 最后映射回去即可 ## 为什么用Feign进行远程调用, 获取不了订单信息? 或者Feign的内部异常? ### 理论 ![image.png](https://cos.easydoc.net/13568421/files/lm5hd5f9.png) > 因为Feing远程调用的时候, 本质也是一次请求, 因此, Feign在发送请求之前也会构造一次请求, 通过源码和DEBUG可知, 其构造请求默认是由一个个请求拦截器`RequestInterceptor`组成的, 因此默认情况下没有请求拦截器, 因此, 最后的请求头是空的 > 请求头为空 -> 没有Cookie信息 -> 没有会话信息 -> 没有登录信息 -> 获取不到在线购物车操作 -> 导致可能的Feign内部异常或查询不到记录 ### 源码图 [源码图](https://www.processon.com/embed/64f661b9b011bd58abce4a57) ### 解决 ```java @Configuration public class FeignConfig { @Bean public RequestInterceptor requestInterceptor() { return requestTemplate -> { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request; if (requestAttributes != null) { // 当这个值等于空的时候, 只可能出现在并发的场景, 如果为空, 默认认为目标微服务不需要的登陆信息 request = requestAttributes.getRequest(); String cookie = request.getHeader("Cookie"); requestTemplate.header("Cookie", cookie); } }; } } ``` ### 为什么修改过后还是会抛内部异常? SQL导致的错误 ### RequestContextHolder原理 ![image.png](https://cos.easydoc.net/13568421/files/lm5huymh.png) > 这个类具有获取请求上下文的能力, 其根本就是底层是由ThreadLocal这个类支撑起来的, 在线程的ThreadLocalMap中存储了请求的信息, 因此可以共享请求上下文 > 注意的是, 不同线程下, 这些请求信息就无法共享了 ## MyBatis复习 ### 如何映射一个符合目的的MAP > 没有特别好的方法, 只能将实体类查出来, 然后通过stream()流映射成一个MAP # 改进 ## 异步编排对代码的改进 > 因为这里有多个微服务的远程调用, 如果采取传统的前后顺序执行, 吞吐量和延迟都非常差, 因此可以采用异步编排 ### 异步编排下无法获取数据的问题 > 由之前的知识点可知, RequestContextHolder的低等逻辑是通过ThreadLocal来实现当前线程内Request共享的, 但是, 不同的线程, 有不同的ThreadLocalMap, 因此, 当我们使用异步编排的时候, 会使用其他线程, 其他线程没有保存request信息, 因此没有COokie信息 > 没有Req -> 没有Cookie -> 最终封装请求Header为空 -> 获取离线操作hash -> 最终无法获取 > 因此, 我们需要在当前线程获取Req, 然后再在异步编排的代码内部赋值即可 ## 关于Stream流的阐述 > stream流中的map可以对元素进行映射, 可以先确定的是, stream流最后收集的集合是一个新的集合, 但是, 是否是新的集合并没有什么关系, 重要的是集合中强引用指向的数据, 这里map我是基于原来的对象修改的, 所以, 这个对象并不会有新的, 就是在原来的基础上改, 而且改动的东西不同, 因此不可能有线程安全问题 # 核心源码 ```java @GetMapping("/toTrade") public ModelAndView toTrade(ModelAndView mv) { // 获取支付确认页数据 ConfirmVO confirmVO = orderConfirmService.getConfirmVOData(); mv.setViewName("confirm"); mv.addObject(confirmVO); return mv; } ``` ```java @Override public ConfirmVO getConfirmVOData() { // 0. 通过ThreadLocal获取用户相关信息 MemberVO memberVO = UserLoginInterceptor.USER_STATE.get(); Long memberId = memberVO.getId(); // 1. 创建目标对象 ConfirmVO confirmVO = new ConfirmVO(); RequestAttributes mainThreadReq = RequestContextHolder.getRequestAttributes(); // 补充: 异步创建防重令牌 CompletableFuture.runAsync(() -> { String orderToken = UUID.randomUUID().toString().replace("-", ""); redisTemplate.opsForValue().set(OrderConstant.ORDER_TOKEN_PREFIX + memberId, orderToken); confirmVO.setOrderToken(orderToken); }, threadPoolExecutor); // 任务1 CompletableFuture<Void> getAddressTask = CompletableFuture.runAsync(() -> { // 2. 查询用户地址相关信息 List<MemberAddressVO> addresses = memberService.getMemberAllAddressesWithOrder(memberId); confirmVO.setAddress(addresses); }, threadPoolExecutor); // 任务2 CompletableFuture<List<OrderItemVO>> getOrderItemsTask = CompletableFuture.supplyAsync(() -> { RequestContextHolder.setRequestAttributes(mainThreadReq); // 3. 查询用户的选中的订单项 List<OrderItemVO> orderItemVOS = cartService.getMemberAllOrderItemsWithOrder(); confirmVO.setItems(orderItemVOS); return orderItemVOS; }, threadPoolExecutor); // 任务2 -> 3 CompletableFuture<Void> getNewestPriceTask = getOrderItemsTask.thenAcceptAsync(orderItems -> { // 4. 更新订单项的价格 // 4.1 获取SkuId集合 List<Long> skuIds = orderItems.stream().map(OrderItemVO::getSkuId).collect(Collectors.toList()); // 4.2 获取价格映射 Map<Long, BigDecimal> currentPrice = productService.getCurrentPrice(skuIds); // 4.3 映射最新价格 orderItems = orderItems.stream().map(orderItemVO // 如果没有价格, 默认价格为0 -> orderItemVO.setPrice(currentPrice.getOrDefault(orderItemVO.getSkuId(), new BigDecimal(0)))) .collect(Collectors.toList()); }, threadPoolExecutor); CompletableFuture<Void> getWeightTask = getOrderItemsTask.thenAcceptAsync(orderItems -> { // 补充: 获取重量 List<Long> skuIds = orderItems.stream().map(OrderItemVO::getSkuId).collect(Collectors.toList()); Map<Long, Long> spuIdBySkuId = productService.getSpuIdBySkuId(skuIds); // SKU 和 SPU的映射关系 List<Long> spuIds = new ArrayList<>(spuIdBySkuId.values()); // 窄化类型转换成List, 方便操作 Map<Long, BigDecimal> weightBySpuId = productService.getWeightBySpuId(spuIds); // SPU 和 weight的映射关系 Map<Long, BigDecimal> skuAndWeight = new HashMap<>(); for (Map.Entry<Long, Long> skuSpu : spuIdBySkuId.entrySet()) { Long skuId = skuSpu.getKey(); Long spuId = skuSpu.getValue(); BigDecimal weight = weightBySpuId.getOrDefault(spuId, new BigDecimal(0)); skuAndWeight.putIfAbsent(skuId, weight); } // 构建好了Sku 与 重量的映射关系 orderItems = orderItems.stream().map(orderItem -> orderItem.setWeight(skuAndWeight.get(orderItem.getSkuId()))).collect(Collectors.toList()); }, threadPoolExecutor); CompletableFuture<Void> getStockTask = getOrderItemsTask.thenAcceptAsync(orderItems -> { // 补充: 查询这些订单项是否有库存 List<Long> skuIds = orderItems.stream().map(OrderItemVO::getSkuId).collect(Collectors.toList()); R info = wareService.getStockStatusBySkuIds(skuIds); List<SkuStockTo> stocks = info.getData(new TypeReference<List<SkuStockTo>>() { }); Map<Long, Boolean> stockMap = stocks.stream().collect(Collectors.toMap(SkuStockTo::getSkuId, SkuStockTo::getHasStock)); confirmVO.setStocks(stockMap); }, threadPoolExecutor); // 任务0 // 5. 获取对应的优惠信息(京豆) Integer integration = memberVO.getIntegration(); confirmVO.setIntegration(integration == null ? 0 : integration); // 等待所有任务的完成 CompletableFuture.allOf(getOrderItemsTask, getAddressTask, getNewestPriceTask, getStockTask, getWeightTask).join(); return confirmVO; } ``` ```java @FeignClient("bitmall-member") public interface MemberService { /** * 获取用户收货地址相关信息 * @param memberId * @return */ @RequestMapping("/member/memberreceiveaddress/{memberId}") List<MemberAddressVO> getMemberAllAddressesWithOrder(@PathVariable Long memberId); } ``` ```java @RequestMapping("/{memberId}") public List<MemberReceiveAddressEntity> getMemberAllAddressesWithOrder(@PathVariable Long memberId) { return memberReceiveAddressService.getMemberAllAddressesWithOrder(memberId); } ``` ```java @Override public List<MemberReceiveAddressEntity> getMemberAllAddressesWithOrder(Long memberId) { LambdaQueryWrapper<MemberReceiveAddressEntity> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(MemberReceiveAddressEntity::getMemberId, memberId); return this.list(queryWrapper); } ``` ```java @FeignClient("bitmall-cart") public interface CartService { @RequestMapping("/getMember/order") @ResponseBody List<OrderItemVO> getMemberAllOrderItemsWithOrder(); } ``` ```java @RequestMapping("/getMember/order") @ResponseBody public List<CartItemVO> getMemberAllOrderItemsWithOrder() { List<CartItemVO> allCartItems = cartService.getAllCartItems(); // 过滤出选中的购物项*(只要选中的购物项) // 有可能有些二货直接调用, 而且没有扽牢固, 会造成空指针异常, 因此加以判断 return allCartItems != null ? allCartItems.stream().filter(CartItemVO::getCheck).collect(Collectors.toList()) : null; } ``` ```java /** * 获取所有的购物项, 该方法可以自行决定在线或离线的购物车操作 * 即自动从在线购物车中取数据(因为订单只能在登录下完成) */ public List<CartItemVO> getAllCartItems() { return getAllCartItems(getHashHandle()); } ``` ```java @FeignClient("bitmall-product") public interface ProductService { @RequestMapping("/product/skuinfo/skus/currentPrice") Map<Long, BigDecimal> getCurrentPrice(@RequestParam List<Long> skuIds); @RequestMapping("/product/skuinfo/getSpuId") Map<Long, Long> getSpuIdBySkuId(@RequestParam List<Long> skuIds); @RequestMapping("/product/spuinfo/weight") Map<Long, BigDecimal> getWeightBySpuId(@RequestParam List<Long> spuIds); } ``` ```java @RequestMapping("/skus/currentPrice") public Map<Long, BigDecimal> getCurrentPrice(@RequestParam List<Long> skuIds) { return skuInfoService.getCurrentPrice(skuIds); } ``` ```xml <select id="getCurrentPrice" resultMap="skuInfoMap"> SELECT sku_id, price FROM pms_sku_info WHERE sku_id IN <foreach collection="skuIds" item="skuId" open="(" close=")" separator=","> #{skuId} </foreach> </select> ``` ```java @RequestMapping("/getSpuId") public Map<Long, Long> getSpuIdBySkuId(@RequestParam List<Long> skuIds) { return skuInfoService.getSpuIdBySkuId(skuIds); } ``` ```java @Override public Map<Long, Long> getSpuIdBySkuId(List<Long> skuIds) { LambdaQueryWrapper<SkuInfoEntity> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.select(SkuInfoEntity::getSkuId, SkuInfoEntity::getSpuId).in(SkuInfoEntity::getSkuId, skuIds); List<SkuInfoEntity> skuInfoEntities = this.list(queryWrapper); if (skuInfoEntities != null && !skuInfoEntities.isEmpty()) { return skuInfoEntities.stream().collect(Collectors.toMap( SkuInfoEntity::getSkuId, SkuInfoEntity::getSpuId )); } return null; } ``` ```java @RequestMapping("/weight") public Map<Long, BigDecimal> getWeightBySpuId(@RequestParam List<Long> spuIds) { return spuInfoService.getWeightBySpuIds(spuIds); } ``` ```java @Override public Map<Long, BigDecimal> getWeightBySpuIds(List<Long> spuIds) { LambdaQueryWrapper<SpuInfoEntity> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.select(SpuInfoEntity::getId, SpuInfoEntity::getWeight).in(SpuInfoEntity::getId, spuIds); List<SpuInfoEntity> spuInfoEntities = this.list(queryWrapper); if (spuInfoEntities != null && !spuInfoEntities.isEmpty()) { return spuInfoEntities.stream().collect(Collectors.toMap( SpuInfoEntity::getId, SpuInfoEntity::getWeight )); } return null; } ``` # 确认页面流程的补充 ![订单确认页流程.png](https://cos.easydoc.net/13568421/files/lm8sase0.png)