一、环境搭建

1、静态页面

图片.png

二、订单确认页

1、OrderWebController

  1. @Controller
  2. public class OrderWebController {
  3. @Autowired
  4. OrderService orderService;
  5. // 点击购物车的去结算按钮,跳转到订单确认页面
  6. @GetMapping("/toTrade")
  7. public String toTrade(Model model){
  8. OrderConfirmVo confirmVo = orderService.confirmOrder();
  9. // 展示订单确认的数据
  10. model.addAttribute("orderConfirmData",confirmVo);
  11. return "confirm";
  12. }
  13. }

2、OrderConfirmVo

  1. package com.atguigu.gulimall.order.vo;
  2. public class OrderConfirmVo {
  3. // 会员的收货地址列表
  4. @Setter @Getter
  5. List<MemberAddressVo> address;
  6. // 所有选中的购物项
  7. @Setter @Getter
  8. List<OrderItemVo> items;
  9. // 发票记录...
  10. // 优惠券信息...
  11. // 会员的积分信息
  12. @Setter @Getter
  13. Integer integration;
  14. // 商品的库存信息,键为skuId,值为是否有库存
  15. @Setter @Getter
  16. Map<Long,Boolean> stocks;
  17. //唯一令牌,多次提交订单防重(如网络问题用户多次提交订单)
  18. @Getter @Setter
  19. String token;
  20. public Integer getCount() {
  21. Integer i = 0;
  22. if(items!=null){
  23. for (OrderItemVo item : items){
  24. i+=item.getCount();
  25. }
  26. }
  27. return i;
  28. }
  29. // 订单总额
  30. // BigDecimal total;
  31. public BigDecimal getTotal() {
  32. BigDecimal sum = new BigDecimal("0");
  33. if(items!=null){
  34. for (OrderItemVo item : items){
  35. // 当前价格乘以数量
  36. BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
  37. sum = sum.add(multiply);
  38. }
  39. }
  40. return sum;
  41. }
  42. // 应付总额
  43. // BigDecimal payPrice;
  44. public BigDecimal getPayPrice() {
  45. BigDecimal sum = new BigDecimal("0");
  46. if(items!=null){
  47. for (OrderItemVo item : items){
  48. // 当前价格乘以数量
  49. BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
  50. sum = sum.add(multiply);
  51. }
  52. }
  53. return sum;
  54. }
  55. }

3、OrderConstant

  1. package com.atguigu.gulimall.order.constant;
  2. public class OrderConstant {
  3. // 用户订单令牌前置
  4. public static final String USER_ORDER_TOKEN_PREFIX = "order:token";
  5. }

4、OrderServiceImpl

  1. /**
  2. * 给订单确认页返回需要用的数据
  3. * @return
  4. */
  5. @Override
  6. public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
  7. // 登录拦截器,获取当前登录用户信息
  8. MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
  9. OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
  10. // 获取主线程中的requestAttributes
  11. RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
  12. // 异步处理
  13. CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
  14. // 每个子线程共享之前的请求数据
  15. RequestContextHolder.setRequestAttributes(requestAttributes);
  16. // 1、远程查询所有的收获地址列表
  17. List<MemberAddressVo> address = memberFeginService.getAddress(memberRespVo.getId());
  18. orderConfirmVo.setAddress(address);
  19. }, executor);
  20. // 异步处理
  21. CompletableFuture<Void> getCartItemsFuture = CompletableFuture.runAsync(() -> {
  22. // 每个子线程共享之前的请求数据
  23. RequestContextHolder.setRequestAttributes(requestAttributes);
  24. // 2、远程查询购物车所有选中的购物项,要注意获取最新的商品价格,而不是先前存到redis中的商品价格
  25. // 利用fegin的RequestInterceptor拦截器功能,远程调用其他服务时,其他服务也能感知当前登录的用户
  26. List<OrderItemVo> cartItems = cartFeginService.getCurrentUserCartItems();
  27. orderConfirmVo.setItems(cartItems);
  28. }, executor).thenRunAsync(() -> {
  29. // 获取购物项中的商品skuId
  30. List<OrderItemVo> items = orderConfirmVo.getItems();
  31. List<Long> collect = items.stream().map(OrderItemVo::getSkuId).collect(Collectors.toList());
  32. // 远程调用库存服务,获取商品的库存信息
  33. R skuHasStock = wmsFeignService.getSkuHasStock(collect);
  34. List<SkuStockVo> stockVos = skuHasStock.getData(new TypeReference<List<SkuStockVo>>() { });
  35. if(stockVos != null){
  36. Map<Long, Boolean> map = stockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
  37. // 设置购物车中商品的库存信息
  38. orderConfirmVo.setStocks(map);
  39. }
  40. },executor);
  41. // 3、查询用户积分信息
  42. Integer integration = memberRespVo.getIntegration();
  43. orderConfirmVo.setIntegration(integration);
  44. // 4、其他数据,如订单总价,商品总价自动计算
  45. // TODO 5、防重令牌
  46. String token = UUID.randomUUID().toString().replace("-", "");
  47. // 防重令牌保存至服务器
  48. redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(), token, 30 , TimeUnit.MINUTES);
  49. // 防重令牌发送给页面
  50. orderConfirmVo.setToken(token);
  51. CompletableFuture.allOf(getAddressFuture,getCartItemsFuture).get();
  52. return orderConfirmVo;
  53. }

5、Feign远程调用丢失请求头问题

1)feign远程调用的请求头中没有含有JSESSIONID的cookie,所以也就不能得到服务端的session数据,cart认为没登录,获取不了用户信息
2)但是在feign的调用过程中,会使用容器中的RequestInterceptor对RequestTemplate进行处理,因此我们可以通过向容器中导入定制的RequestInterceptor为请求加上cookie。
3)RequestContextHolder为SpingMVC中共享request数据的上下文,底层由ThreadLocal实现。经过RequestInterceptor处理后的请求如下,已经加上了请求头的Cookie信息
图片.png

  1. package com.atguigu.gulimall.order.config;
  2. /**
  3. * feign在远程调用之前都会先经过每个拦截器的apply(RequestTemplate template)方法,RequestTemplate相当于真正要发出去的请求
  4. * feign在远程调用之前要构造请求,调用很多拦截器:RequestInterceptor interceptor :requestInterceptors
  5. *
  6. * 浏览器发controller请求,service要远程调用feign,feign要创建一个新的对象来发请求,在创建对象的时候会调用拦截器
  7. * 拦截器、controller、service都在同一个线程。
  8. * 拦截器若想获取到原生的请求数据,原始办法是controller中可以传递HttpServletRequest到拦截器
  9. *
  10. * RequestContextHolder:上下文环境保持器,利用ThreadLocal帮我们从请求一开始就把当前的请求数据放到ThreadLocal,随用随取
  11. * RequestContextHolder.getRequestAttributes():可以获取到当前请求的所有属性
  12. */
  13. @Configuration
  14. public class GuliFeignConfig {
  15. // feign远程调用的请求拦截器
  16. @Bean("requestInterceptor") // requestInterceptor拦截器名称。默认为方法名
  17. public RequestInterceptor requestInterceptor() {
  18. return new RequestInterceptor() {
  19. @Override // RequestTemplate新请求
  20. public void apply(RequestTemplate template) {
  21. // 1、RequestContextHolder拿到刚进来的request请求(@GetMapping("/toTrade"):里面带有cookie)
  22. // 原理是通过threadLocal获取
  23. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  24. if(attributes != null){
  25. // 获取当前请求对象(老请求)。相当于controller中可以传递HttpServletRequest
  26. HttpServletRequest request = attributes.getRequest();
  27. if (request != null) {
  28. // 2、同步请求头信息:Cookie
  29. String cookie = request.getHeader("Cookie");
  30. // 给新请求(远程调用请求)同步老请求的cookie
  31. template.header("Cookie", cookie);
  32. // System.out.println("feign远程之前先执行RequestInterceptor.apply方法");
  33. }
  34. }
  35. }
  36. };
  37. }
  38. }

6、Feign异步情况丢失上下文问题

6.1 原因

1)查询购物项、库存和收货地址都要调用远程服务,串行会浪费大量时间,因此我们使用CompletableFuture进行异步编排
2)由于RequestContextHolder使用ThreadLocal共享数据,所以在开启异步时获取不到老请求的信息,自然也就无法共享cookie了。在这种情况下,我们需要在开启异步的时候将老请求的RequestContextHolder的数据设置进去
image.png
image.png
图片.png
图片.png

6.2 解决方案

image.png

7、订单确认页流程

image.png

8、创建防重令牌

image.png

三、提交订单

1、下单流程

图片.png图片.png

2、OrderSubmitVo

  1. package com.atguigu.gulimall.order.vo;
  2. // 封装订单提交的数据
  3. @Data
  4. public class OrderSubmitVo {
  5. // 收货地址的ID
  6. private Long addrId;
  7. // 支付方式
  8. private Integer payType;
  9. // 防重令牌
  10. private String orderToken;
  11. // 无需提交订单确认页需要购买的商品,直接去购物车再获取一遍
  12. // 应付总额,验价(商品价格与购物车价格是否一致)
  13. private BigDecimal payPrice;
  14. // 备注信息
  15. private String note;
  16. // 用户相关信息从session中取出
  17. }

3、SubmitOrderResponseVo

  1. package com.atguigu.gulimall.order.vo;
  2. // 下单操作后的返回信息
  3. @Data
  4. public class SubmitOrderResponseVo {
  5. // 订单的实体类
  6. private OrderEntity order;
  7. // 下单失败的错误状态码,0表示成功
  8. private Integer code;
  9. }

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

4.1 下单失败

图片.png

5、OrderServiceImpl

5.1 验证令牌

image.png

5.2 构造订单数据

1、OrderCreateTo

  1. package com.atguigu.gulimall.order.to;
  2. @Data
  3. public class OrderCreateTo {
  4. // 订单信息
  5. private OrderEntity order;
  6. // 每个订单项
  7. private List<OrderItemEntity> items;
  8. // 计算的应付订单总额
  9. private BigDecimal payPrice;
  10. // 运费
  11. private BigDecimal fare;
  12. }

2、创建订单createOrder

  1. // 创建订单
  2. private OrderCreateTo createOrder() {
  3. OrderCreateTo orderCreateTo = new OrderCreateTo();
  4. // 1、构建订单
  5. String orderSn = IdWorker.getTimeId();
  6. OrderEntity orderEntity = buildOrder(orderSn);
  7. // 2、获取所有的订单项
  8. List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
  9. // 3、验价
  10. computePrice(orderEntity, itemEntities);
  11. orderCreateTo.setItems(itemEntities);
  12. orderCreateTo.setOrder(orderEntity);
  13. return orderCreateTo;
  14. }

5.3、锁定库存

1、锁库存逻辑

图片.png

2、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. }

四、本地事务和分布式事务

五、RabbitMQ延时队列