一、环境搭建
1、静态页面
二、订单确认页
1、OrderWebController
@Controllerpublic class OrderWebController {@AutowiredOrderService orderService;// 点击购物车的去结算按钮,跳转到订单确认页面@GetMapping("/toTrade")public String toTrade(Model model){OrderConfirmVo confirmVo = orderService.confirmOrder();// 展示订单确认的数据model.addAttribute("orderConfirmData",confirmVo);return "confirm";}}
2、OrderConfirmVo
package com.atguigu.gulimall.order.vo;public class OrderConfirmVo {// 会员的收货地址列表@Setter @GetterList<MemberAddressVo> address;// 所有选中的购物项@Setter @GetterList<OrderItemVo> items;// 发票记录...// 优惠券信息...// 会员的积分信息@Setter @GetterInteger integration;// 商品的库存信息,键为skuId,值为是否有库存@Setter @GetterMap<Long,Boolean> stocks;//唯一令牌,多次提交订单防重(如网络问题用户多次提交订单)@Getter @SetterString token;public Integer getCount() {Integer i = 0;if(items!=null){for (OrderItemVo item : items){i+=item.getCount();}}return i;}// 订单总额// BigDecimal total;public BigDecimal getTotal() {BigDecimal sum = new BigDecimal("0");if(items!=null){for (OrderItemVo item : items){// 当前价格乘以数量BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));sum = sum.add(multiply);}}return sum;}// 应付总额// BigDecimal payPrice;public BigDecimal getPayPrice() {BigDecimal sum = new BigDecimal("0");if(items!=null){for (OrderItemVo item : items){// 当前价格乘以数量BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));sum = sum.add(multiply);}}return sum;}}
3、OrderConstant
package com.atguigu.gulimall.order.constant;public class OrderConstant {// 用户订单令牌前置public static final String USER_ORDER_TOKEN_PREFIX = "order:token";}
4、OrderServiceImpl
/*** 给订单确认页返回需要用的数据* @return*/@Overridepublic OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {// 登录拦截器,获取当前登录用户信息MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();OrderConfirmVo orderConfirmVo = new OrderConfirmVo();// 获取主线程中的requestAttributesRequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();// 异步处理CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {// 每个子线程共享之前的请求数据RequestContextHolder.setRequestAttributes(requestAttributes);// 1、远程查询所有的收获地址列表List<MemberAddressVo> address = memberFeginService.getAddress(memberRespVo.getId());orderConfirmVo.setAddress(address);}, executor);// 异步处理CompletableFuture<Void> getCartItemsFuture = CompletableFuture.runAsync(() -> {// 每个子线程共享之前的请求数据RequestContextHolder.setRequestAttributes(requestAttributes);// 2、远程查询购物车所有选中的购物项,要注意获取最新的商品价格,而不是先前存到redis中的商品价格// 利用fegin的RequestInterceptor拦截器功能,远程调用其他服务时,其他服务也能感知当前登录的用户List<OrderItemVo> cartItems = cartFeginService.getCurrentUserCartItems();orderConfirmVo.setItems(cartItems);}, executor).thenRunAsync(() -> {// 获取购物项中的商品skuIdList<OrderItemVo> items = orderConfirmVo.getItems();List<Long> collect = items.stream().map(OrderItemVo::getSkuId).collect(Collectors.toList());// 远程调用库存服务,获取商品的库存信息R skuHasStock = wmsFeignService.getSkuHasStock(collect);List<SkuStockVo> stockVos = skuHasStock.getData(new TypeReference<List<SkuStockVo>>() { });if(stockVos != null){Map<Long, Boolean> map = stockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));// 设置购物车中商品的库存信息orderConfirmVo.setStocks(map);}},executor);// 3、查询用户积分信息Integer integration = memberRespVo.getIntegration();orderConfirmVo.setIntegration(integration);// 4、其他数据,如订单总价,商品总价自动计算// TODO 5、防重令牌String token = UUID.randomUUID().toString().replace("-", "");// 防重令牌保存至服务器redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(), token, 30 , TimeUnit.MINUTES);// 防重令牌发送给页面orderConfirmVo.setToken(token);CompletableFuture.allOf(getAddressFuture,getCartItemsFuture).get();return orderConfirmVo;}
5、Feign远程调用丢失请求头问题
1)feign远程调用的请求头中没有含有JSESSIONID的cookie,所以也就不能得到服务端的session数据,cart认为没登录,获取不了用户信息
2)但是在feign的调用过程中,会使用容器中的RequestInterceptor对RequestTemplate进行处理,因此我们可以通过向容器中导入定制的RequestInterceptor为请求加上cookie。
3)RequestContextHolder为SpingMVC中共享request数据的上下文,底层由ThreadLocal实现。经过RequestInterceptor处理后的请求如下,已经加上了请求头的Cookie信息
package com.atguigu.gulimall.order.config;/*** feign在远程调用之前都会先经过每个拦截器的apply(RequestTemplate template)方法,RequestTemplate相当于真正要发出去的请求* feign在远程调用之前要构造请求,调用很多拦截器:RequestInterceptor interceptor :requestInterceptors** 浏览器发controller请求,service要远程调用feign,feign要创建一个新的对象来发请求,在创建对象的时候会调用拦截器* 拦截器、controller、service都在同一个线程。* 拦截器若想获取到原生的请求数据,原始办法是controller中可以传递HttpServletRequest到拦截器** RequestContextHolder:上下文环境保持器,利用ThreadLocal帮我们从请求一开始就把当前的请求数据放到ThreadLocal,随用随取* RequestContextHolder.getRequestAttributes():可以获取到当前请求的所有属性*/@Configurationpublic class GuliFeignConfig {// feign远程调用的请求拦截器@Bean("requestInterceptor") // requestInterceptor拦截器名称。默认为方法名public RequestInterceptor requestInterceptor() {return new RequestInterceptor() {@Override // RequestTemplate新请求public void apply(RequestTemplate template) {// 1、RequestContextHolder拿到刚进来的request请求(@GetMapping("/toTrade"):里面带有cookie)// 原理是通过threadLocal获取ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if(attributes != null){// 获取当前请求对象(老请求)。相当于controller中可以传递HttpServletRequestHttpServletRequest request = attributes.getRequest();if (request != null) {// 2、同步请求头信息:CookieString cookie = request.getHeader("Cookie");// 给新请求(远程调用请求)同步老请求的cookietemplate.header("Cookie", cookie);// System.out.println("feign远程之前先执行RequestInterceptor.apply方法");}}}};}}
6、Feign异步情况丢失上下文问题
6.1 原因
1)查询购物项、库存和收货地址都要调用远程服务,串行会浪费大量时间,因此我们使用CompletableFuture进行异步编排
2)由于RequestContextHolder使用ThreadLocal共享数据,所以在开启异步时获取不到老请求的信息,自然也就无法共享cookie了。在这种情况下,我们需要在开启异步的时候将老请求的RequestContextHolder的数据设置进去


6.2 解决方案
7、订单确认页流程
8、创建防重令牌
三、提交订单
1、下单流程
2、OrderSubmitVo
package com.atguigu.gulimall.order.vo;// 封装订单提交的数据@Datapublic class OrderSubmitVo {// 收货地址的IDprivate Long addrId;// 支付方式private Integer payType;// 防重令牌private String orderToken;// 无需提交订单确认页需要购买的商品,直接去购物车再获取一遍// 应付总额,验价(商品价格与购物车价格是否一致)private BigDecimal payPrice;// 备注信息private String note;// 用户相关信息从session中取出}
3、SubmitOrderResponseVo
package com.atguigu.gulimall.order.vo;// 下单操作后的返回信息@Datapublic class SubmitOrderResponseVo {// 订单的实体类private OrderEntity order;// 下单失败的错误状态码,0表示成功private Integer code;}
4、OrderWebController
/*** 下单功能,提交订单。 需要去创建订单、验令牌、验价格、锁库存* @param vo 订单提交的数据* @param model* @param redirectAttributes* @return 下单成功来到支付选择页,下单失败回到订单确认页重新确认订单信息*/@PostMapping("/submitOrder")public String submitOrder(OrderSubmitVo vo, Model model,RedirectAttributes redirectAttributes) {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 += "商品库存不足";}redirectAttributes.addFlashAttribute("msg", msg); // 返回页面数据// 下单失败回到订单确认页重新确认订单信息return "redirect:http://order.gulimall.com/toTrade";}}catch (Exception e){if(e instanceof NoStockException){String msg = ((NoStockException) e).getMessage();redirectAttributes.addFlashAttribute("msg",msg);}return "redirect:http://order.gulimall.com/toTrade";}}
4.1 下单失败
5、OrderServiceImpl
5.1 验证令牌
5.2 构造订单数据
1、OrderCreateTo
package com.atguigu.gulimall.order.to;@Datapublic class OrderCreateTo {// 订单信息private OrderEntity order;// 每个订单项private List<OrderItemEntity> items;// 计算的应付订单总额private BigDecimal payPrice;// 运费private BigDecimal fare;}
2、创建订单createOrder
// 创建订单private OrderCreateTo createOrder() {OrderCreateTo orderCreateTo = new OrderCreateTo();// 1、构建订单String orderSn = IdWorker.getTimeId();OrderEntity orderEntity = buildOrder(orderSn);// 2、获取所有的订单项List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);// 3、验价computePrice(orderEntity, itemEntities);orderCreateTo.setItems(itemEntities);orderCreateTo.setOrder(orderEntity);return orderCreateTo;}
5.3、锁定库存
1、锁库存逻辑
2、WareSkuController
/*** 为当前订单锁定库存*/@PostMapping("/lock/order")public R orderLockStock(@RequestBody WareSkuLockVo vo) {try{Boolean stock = wareSkuService.orderLockStock(vo);return R.ok();}catch (NoStockException e){return R.error(BizCodeEnume.NO_STOCK_EXCEPTION.getCode(),BizCodeEnume.NO_STOCK_EXCEPTION.getMsg());}}

