2023最新谷粒商城笔记之订单服务篇(全文总共13万字,超详细)

news2024/11/26 4:26:19

订单服务

页面环境搭建

配置动静环境

在服务器的mydata/nginx/html/static 路径下创建一个 order 文件夹,在order路径下分别创建以下几个文件夹,用来存放对应的静态资源

  • detail 文件夹下存放 等待付款的静态资源,
    并将等待付款文件夹下的页面复制到 gulimall-order服务中并命名为 detail.html

    href="		==>		href="/static/order/detail/
    src="		==>		src="/static/order/detail/
    
  • list 文件夹下存放 订单页的静态资源,并将订单页文件夹下的页面复制到 gulimall-order服务中并命名为 list.html

    href="		==>		href="/static/order/list/
    src="		==>		src="/static/order/list/
    
  • confirm 文件夹下存放 结算页的静态资源,并将结算页文件夹下的页面复制到 gulimall-order服务中并命名为 confirm.html

    src="		==>			src="/static/order/confirm/
    href="		==>			href="/static/order/confirm/
    
  • pay 文件夹下存放 收银页的静态资源,并将收银页文件夹下的页面复制到 gulimall-order服务中并命名为 pay.html

    href="		==>		href="/static/order/pay/
    src="		==>		src="/static/order/pay/
    

网关路由配置

  1. 修改文件,添加新的域名 vim /etc/hosts
# Gulimall Host Start
127.0.0.1 gulimall.com
127.0.0.1 search.gulimall.com
127.0.0.1 item.gulimall.com
127.0.0.1 auth.gulimall.com
127.0.0.1 cart.gulimall.com
127.0.0.1 order.gulimall.com
# Gulimall Host End
  1. 配置网关路由 gulimall-gateway
- id: gulimall_order_route
  uri: lb://gulimall-order
  predicates:
    - Host=order.gulimall.cn

配置加入注册中心Nacos

将订单服务注册到注册中心去:

  1. 导入Nacos依赖

  2. 主启动类加上 @EnableDiscoveryClient 注解

    @EnableDiscoveryClient
    @EnableRabbit
    @SpringBootApplication
    public class GulimallOrderApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(GulimallOrderApplication.class, args);
        }
    }
    
  3. 配置注册中心信息

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: gulimall-order

页面渲染

  1. 导入 thymeleaf的依赖并在开发期间禁用缓存

    
    
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  1. 配置清除缓存
spring:
  thymeleaf:
    cache: false

整合SpringSession

第一步、 导入依赖

<!--属性配置的提示工具-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<!-- 整合SpringSession完成Session共享问题-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<!--引入Redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

第二步、 修改配置

spring:
  #SpringSession的存储类型
  session:
    store-type: redis
  #reidis地址
  redis:
    host: 124.222.223.222
# 配置线程池
gulimall:
  thread:
    core-size: 20
    max-size: 200
    keep-alive-time: 10

主启动类上添加SpingSession自动启动的注解

@EnableRedisHttpSession
@EnableDiscoveryClient
@EnableRabbit
@SpringBootApplication
public class GulimallOrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallOrderApplication.class, args);
    }

}

第三步、 导入SpringSession、线程池配置类

  1. 添加SpringSession的配置,添加“com.atguigu.gulimall.order.config.GulimallSessionConfig”类,代码如下

    @Configuration
    public class GulimallSessionConfig {
        @Bean
        public CookieSerializer cookieSerializer() {
            DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
            cookieSerializer.setDomainName("gulimall.cn");
            cookieSerializer.setCookieName("GULISESSION");
    
            return cookieSerializer;
        }
        @Bean
        public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
            return new GenericJackson2JsonRedisSerializer();
        }
    }
    
  2. 添加线程池的配置,添加“com.atguigu.gulimall.order.config.MyThreadConfig”类,代码如下

    @Configuration
    public class MyThreadConfig {
        @Bean
        public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) {
            return new ThreadPoolExecutor(pool.getCoreSize(),
                    pool.getMaxSize(),
                    pool.getKeepAliveTime(),
                    TimeUnit.SECONDS,
                    new LinkedBlockingDeque<>(100000),
                    Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.AbortPolicy());
        }
    }
    
  3. 线程池配置需要的属性

    添加“com.atguigu.gulimall.order.config.ThreadPoolConfigProperties”类,代码如下

    @ConfigurationProperties(prefix = "gulimall.thread")
    @Component
    @Data
    public class ThreadPoolConfigProperties {
        private Integer coreSize;
        private Integer maxSize;
        private Integer keepAliveTime;
    }
    

第四步、 页面调整

  1. 修改商城首页、商品页我的订单地链接地址

在这里插入图片描述
在这里插入图片描述

  1. 获取用户信息

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

分析订单业务

订单构成

在这里插入图片描述

订单状态

  1. 代付款
  2. 已付款/待发货
  3. 待收货/已发货
  4. 已完成
  5. 已取消
  6. 售后中

订单流程

订单生成 -> 支付订单 -> 卖家发货 -> 确认收货 -> 交易成功

在这里插入图片描述

订单登录拦截

需求:去结算、查看订单必须是登录用户之后才能进行的,这里编写一个拦截器。

  • 用户登录则放行请求

  • 用户未登录则跳转到登录页面进行登录

修改前端页面

修改cartList.html 页面的**“去结算”**的链接地址

在这里插入图片描述

修改gulimall-auth-server的login.html页面接收提醒信息,将未登录消息进行提醒回显

在这里插入图片描述

编写Controller层

Gulimall-order服务中com.atguigu.gulimall.order.web 路径下

package com.atguigu.gulimall.order.web;
@Controller
public class OrderWebController {
    @GetMapping("/toTrade")
    public String toTrade(){
        return "confirm";
    }
}

编写拦截器

Gulimall-order服务中com.atguigu.gulimall.order.interceptoe 路径下

package com.atguigu.gulimall.order.interceptoe;

@Component
public class LoginUserInterceptor implements HandlerInterceptor {
    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
    /**
     * 用户登录拦截器
     * @param request
     * @param response
     * @param handler
     * @return 
     *      用户登录:放行
     *      用户未登录:跳转到登录页面
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute!=null){
            loginUser.set(attribute);
            return true;
        } else {
            // 没登录就去登录
            request.getSession().setAttribute("msg", "请先进行登录");
            response.sendRedirect("http://auth.gulimall.cn/login.html");
            return false;
        }
    }
}

添加拦截器的配置

Gulimall-order服务中com.atguigu.gulimall.order.config 路径下,需要配置以下配置类拦截器才会生效:

package com.atguigu.gulimall.order.config;

import com.atguigu.gulimall.order.interceptoe.LoginUserInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
 * Data time:2022/4/11 22:21
 * StudentID:2019112118
 * Author:hgw
 * Description:
 */
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
    @Autowired
    LoginUserInterceptor interceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor).addPathPatterns("/**");
    }
}

订单确认页

订单确认页的模型抽取

在这里插入图片描述

分析订单确认页得到订单确认页需要用到的数据,此数据需要传递给后端,因此我们需要将其封装为一个VO

  • 因为存在网路延迟等问题,若一直点下单会下许多。所以我们需要防重令牌

gulimall-order 服务中 com.atguigu.gulimall.order.vo 路径下 VO类:

/**
 * Data time:2022/4/12 09:31
 * StudentID:2019112118
 * Author:hgw
 * Description: 订单确认页需要用的数据
 */
public class OrderConfirmVo {
    /**
     * 收货地址,ums_member_receive_address 表
     */
    @Setter@Getter
    List<MemberAddressVo> addressVos;
    /**
     * 所有选中的购物车项
     */
    @Setter@Getter
    List<OrderItemVo> items;
    // 发票记录。。。
    /**
     * 优惠券信息
     */
    @Setter@Getter
    Integer integration;
    /**
     * 是否有库存
     */
    @Setter@Getter
    Map<Long,Boolean> stocks;
    /**
     * 防重令牌
     */
    @Setter@Getter
    String OrderToken;
    /**
     * @return  订单总额
     * 所有选中商品项的价格 * 其数量
     */
    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 pryPrice;
    public BigDecimal getPryPrice() {
        return getTotal();
    }
    public Integer getCount(){
        Integer i =0;
        if (items!=null){
            for (OrderItemVo item : items) {
               i+=item.getCount();
            }
        }
        return i;
    }
}

收货地址,ums_member_receive_address 表

package com.atguigu.gulimall.order.vo;

@Data
public class OrderConfirmVo {
    /**
     * 收货地址,ums_member_receive_address 表
     */
    List<MemberAddressVo> addressVos;
    /**
     * 所有选中的购物车项
     */
    List<OrderItemVo> items;
    // 发票记录。。。
    /**
     * 优惠券信息
     */
    Integer integration;
    /**
     * 订单总额
     */
    BigDecimal total;
    /**
     * 应付价格
     */
    BigDecimal pryPrice;
}

商品项信息

package com.atguigu.gulimall.order.vo;
@Data
public class OrderItemVo {
    /**
     * 商品Id
     */
    private Long skuId;
    /**
     * 商品标题
     */
    private String title;
    /**
     * 商品图片
     */
    private String image;
    /**
     * 商品套餐信
     */
    private List<String> skuAttr;
    /**
     * 商品价格
     */
    private BigDecimal price;
    /**
     * 数量
     */
    private Integer count;
    /**
     * 小计价格
     */
    private BigDecimal totalPrice;

}

订单确认页数据获取

gulimall-order 订单确认页数据获取接口编写

  1. Controller 层方法编写
    Gulimall-product 服务中 com.atguigu.gulimall.order.web 路径下 OrderWebController类
@Controller
public class OrderWebController {
    @Autowired
    OrderService orderService;

    @GetMapping("/toTrade")
    public String toTrade(Model model){
        OrderConfirmVo confirmVo = orderService.confirmOrder();
        model.addAttribute("OrderConfirmData",confirmVo);
        return "confirm";
    }
}
  1. Service层实现类方法编写

    • 1、远程查询所有的地址列表
    • 2、远程查询购物车所有选中的购物项
    • 3、查询用户积分
    • 4、其他数据自动计算
    • 5、防重令牌

    Gulimall-product 服务中 com.atguigu.gulimall.order.service.impl 路径下 OrderServiceImpl

@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {
    @Autowired
    MemberFeignService memberFeignService;
    @Autowired
    CartFeignService cartFeignService;

    @Override
    public OrderConfirmVo confirmOrder() {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        // 1、远程查询所有的地址列表
        List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
        confirmVo.setAddressVos(address);
        // 2、远程查询购物车所有选中的购物项
        List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
        confirmVo.setItems(items);
        // 3、查询用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.setIntegration(integration);
        // 4、其他数据自动计算
        // 5、防重令牌
        return confirmVo;
    }
}

编写Gulimall-common获取会员所有收货地址接口

  1. 编写Controller层接口方法
    Gulimall-member 服务中com.atguigu.gulimall.member.controller路径下 MemberReceiveAddressController 类

    package com.atguigu.gulimall.member.controller;
    
    @RestController
    @RequestMapping("member/memberreceiveaddress")
    public class MemberReceiveAddressController {
        @Autowired
        private MemberReceiveAddressService memberReceiveAddressService;
    
        @GetMapping("/{memberId}/address")
        public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId) {
            return memberReceiveAddressService.getAddress(memberId);
        }
    
  2. Service层实现类 编写 获取会有收货地址列表 方法
    Gulimall-member 服务中com.atguigu.gulimall.member.service.impl路径下 MemberReceiveAddressServiceImpl 实现类

    @Override
    public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
      return this.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));
    }
    
  3. Gulimall-order服务中编写远程调用 gulimall-member服务 feign接口
    Gulimall-order服务中 com.atguigu.gulimall.order.feign 路径下 MemberFeignService 接口

    package com.atguigu.gulimall.order.feign;
    
    @FeignClient("gulimall-member")
    public interface MemberFeignService {
    
        /**
         * 返回会员所有的收货地址列表
         * @param memberId 会员ID
         * @return
         */
        @GetMapping("/member/memberreceiveaddress/{memberId}/address")
        List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
    
    }
    

编写GuliMall-cart 购物车服务中用户选择的所有购物项

  1. 首先通过用户ID在Redis中查询到购物车中的所有的购物项
  2. 通过 filter 过滤 用户购物车中被选择的购物项
  3. 查询数据库中当前购物项的价格,不能使用之前加入购物车的价格
    • 编写远程 gulimall-product 服务中的 查询sku价格接口

第一步、编写Controller层接口

编写 gulimall-cart 服务中 package com.atguigu.cart.controller; 路径下的 CartController 类:

package com.atguigu.cart.controller;

@Controller
public class CartController {

    @Autowired
    CartService cartService;

    @GetMapping("/currentUserCartItems")
    @ResponseBody
    public List<CartItem> getCurrentUserCartItems(){
        return cartService.getUserCartItems();
    }
  	//....
}

第二步、Service层实现类 获取用户选择的所有购物项方法编写

编写 gulimall-cart 服务中 com.atguigu.cart.service.impl 路径中 CartServiceImpl 类

@Autowired
ProductFeignService productFeignService;

/**
* 获取用户选择的所有购物项
* @return
*/
@Override
public List<CartItem> getUserCartItems() {
  UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
  if (userInfoTo.getUserId() == null) {
    return null;
  } else {
    String cartKey = CART_PREFIX + userInfoTo.getUserId();
    // 获取所有用户选择的购物项
    List<CartItem> collect = getCartItems(cartKey).stream()
      .filter(item -> item.getCheck())
      .map(item->{
        // TODO 1、更新为最新价格
        R price = productFeignService.getPrice(item.getSkuId());
        String data = (String) price.get("data");
        item.setPrice(new BigDecimal(data));
        return item;
      })
      .collect(Collectors.toList());
    return collect;
  }
}

第三步、编写Gulimall-product 服务中获取指定商品的价格接口

Gulimall-product 服务中 com.atguigu.gulimall.product.app 路径下的 SkuInfoController

package com.atguigu.gulimall.product.app;

@RestController
@RequestMapping("product/skuinfo")
public class SkuInfoController {
    @Autowired
    private SkuInfoService skuInfoService;

    /**
     * 获取指定商品的价格
     * @param skuId
     * @return
     */
    @GetMapping("/{skuId}/price")
    public R getPrice(@PathVariable("skuId") Long skuId){
        SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);
        return R.ok().setData(skuInfoEntity.getPrice().toString());
    }

Gulimall-cart 服务中的 com.atguigu.cart.feign 路径下的远程调用接口 ProductFeignService

package com.atguigu.cart.feign;
@FeignClient("gulimall-product")
public interface ProductFeignService {
    //.....
    @GetMapping("/product/skuinfo/{skuId}/price")
    R getPrice(@PathVariable("skuId") Long skuId);
}

第四步、Gulimall-order服务中编写远程调用 gulimall-cart服务 feign接口

Gulimall-order服务中com.atguigu.gulimall.order.feign 路径下的 CartFeignService接口

package com.atguigu.gulimall.order.feign;

@FeignClient("gulimall-cart")
public interface CartFeignService {

    @GetMapping("/currentUserCartItems")
    List<OrderItemVo> getCurrentUserCartItems();
}

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

  • 问题 :Feign远程调用的时候会丢失请求头(因为feign远程调用是新创建了一个request):导致请求在远程调用之后请求头没有携带cookie信息,那么就无法找到那个携带session信息的cookie了,也就无法从redis中获取用户登录信息了(因为每个用户用浏览器登录之后都会存储一个包含他登录信息的cookie,key可以是任意的因为服务器知道它是存储session用的,value是唯一标识这个用户的一个值,服务器识别到这个存储session的cookie之后就会根据它的value去redis中查是否有这个用户的登录信息)。
  • 解决:加上feign它的远程调用的请求拦截器。(RequestInterceptor),在这个拦截器中重写apply方法,在这个方法中把老请求的cookie复制到新request的请求头中
    • 因为feign在远程调用之前会执行所有的RequestInterceptor拦截器(feign的拦截器)

在这里插入图片描述
在这里插入图片描述

在 gulimall-order 服务中 com.atguigu.gulimall.order.config 路径下编写Feign配置类:GulimallFeignConfig类并编写请求拦截器

package com.atguigu.gulimall.order.config;

@Configuration
public class GulimallFeignConfig {
    /**
     * feign在远程调用之前会执行所有的RequestInterceptor拦截器
     * @return
     */
    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor(){
            @Override
            public void apply(RequestTemplate requestTemplate) {
                // 1、使用 RequestContextHolder 拿到请求数据,RequestContextHolder底层使用过线程共享数据 ThreadLocal<RequestAttributes>
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if (attributes!=null){
                    HttpServletRequest request = attributes.getRequest();
                    // 2、同步请求头数据,Cookie
                    String cookie = request.getHeader("Cookie");
                    // 给新请求同步了老请求的cookie
                    requestTemplate.header("Cookie",cookie);
                }
            }
        };
    }
}

Feign异步调用丢失请求头问题

此时:查询购物项、库存和收货地址都要调用远程服务,串行执行远程调用会浪费大量时间,因此我们进行异步编排优化

  • 问题
    因为我们给feign的拦截器所拦截的请求加上了请求头,但这个获取请求头的过程是在一个线程执行过程中进行的,但是由于 RequestContextHolder底层使用的是线程共享数据 ThreadLocal<RequestAttributes>,我们知道线程共享数据的域是当前线程下,线程之间是不共享的。所以在开启异步时获取不到老请求的信息即拦截器中会发生空指针的错误,即老request对象获取不到,是null,自然也就无法共享cookie了。但是我们现在进行的是异步操作,使用到了线程池,这就导致请求头不在一个线程中,最终导致请求头信息丢失。

  • 解决
    RequestContextHolder线程域中放主线程的请求域。即先在主线程中获取到请求头相关信息(RequestContextHolder.getRequestAttributes();),然后在异步调用的过程中将情求头加到正在执行异步操作的线程中( RequestContextHolder.setRequestAttributes(requestAttributes);),这样请求头就不会丢失。

    虽然都是同一个 RequestContextHolder类在调用方法,但是 RequestContextHolder在不同线程中的意义是不同的,它仅代表当前线程。

在这里插入图片描述

修改 gulimall-order 服务中 com.atguigu.gulimall.order.service.impl 目录下的 OrderServiceImpl 类

@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
    OrderConfirmVo confirmVo = new OrderConfirmVo();
    MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
    // 获取主线程的域,主线程中获取请求头信息
    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    // 1、远程查询所有的地址列表
    CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
        RequestContextHolder.setRequestAttributes(requestAttributes);
        // 将主线程的域放在该线程的域中
        List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
        confirmVo.setAddressVos(address);
    }, executor);

    // 2、远程查询购物车所有选中的购物项
    CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
        // 将老请求的域放在该线程的域中
        RequestContextHolder.setRequestAttributes(requestAttributes);
        List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
        confirmVo.setItems(items);
    }, executor);
    // feign在远程调用请求之前要构造
    // 3、查询用户积分
    Integer integration = memberRespVo.getIntegration();
    confirmVo.setIntegration(integration);
    // 4、其他数据自动计算
    // TODO 5、防重令牌
    CompletableFuture.allOf(getAddressFuture,cartFuture).get();
    return confirmVo;
}

订单确认页渲染

修改 gulimall-order 服务中,src/main/resources/templates/路径下的 confirm.html

<!--主体部分-->
<p class="p1">填写并核对订单信息</p>
<div class="section">
   <!--收货人信息-->
   <div class="top-2">
      <span>收货人信息</span>
      <span>新增收货地址</span>
   </div>
   <!--地址-->
   <div class="top-3" th:each="addr:${orderConfirmData.addressVos}">
      <p>[[${addr.name}]]</p><span>[[${addr.name}]]  [[${addr.province}]]  [[${addr.city}]] [[${addr.detailAddress}]] [[${addr.phone}]]</span>
   </div>
   <p class="p2">更多地址︾</p>
   <div class="hh1"/></div>
<div class="xia">
   <div class="qian">
      <p class="qian_y">
         <span>[[${orderConfirmData.count}]]</span>
         <span>件商品,总商品金额:</span>
         <span class="rmb">¥[[${#numbers.formatDecimal(orderConfirmData.total,1,2)}]]</span>
      </p>
      <p class="qian_y">
         <span>返现:</span>
         <span class="rmb">  -¥0.00</span>
      </p>
      <p class="qian_y">
         <span>运费: </span>
         <span class="rmb"> &nbsp ¥0.00</span>
      </p>
      <p class="qian_y">
         <span>服务费: </span>
         <span class="rmb"> &nbsp ¥0.00</span>
      </p>
      <p class="qian_y">
         <span>退换无忧: </span>
         <span class="rmb"> &nbsp ¥0.00</span>
      </p>
   </div>
   <div class="yfze">
      <p class="yfze_a"><span class="z">应付总额:</span><span class="hq">¥[[${#numbers.formatDecimal(orderConfirmData.pryPrice,1,2)}]]</span></p>
      <p class="yfze_b">寄送至:  IT-中心研发二部 收货人:</p>
   </div>
   <button class="tijiao">提交订单</button>
</div>

订单确认页库存查询

需求:查询订单项有货还是无货

在远程查询购物车所有选中的购物项之后进行批量查询库存

在这里插入图片描述

在订单确认页数据获取 Service层实现类 OrderServiceImpl 方法中进行批量查询库存

1、修改Gulimall-order 服务中 com.atguigu.gulimall.order.service.impl 路径下的 OrderServiceImpl 类

在这里插入图片描述

    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();

        // 获取主线程的请求域
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 1、远程查询所有的地址列表
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 将主线程的请求域放在该线程请求域中
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddressVos(address);
        }, executor);

        // 2、远程查询购物车所有选中的购物项
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            // 将主线程的请求域放在该线程请求域中
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor).thenRunAsync(()->{
            // 批量查询商品项库存
            List<OrderItemVo> items = confirmVo.getItems();
            List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
            R hasStock = wareFeignService.getSkusHasStock(collect);
            List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
            });
            if (data != null) {
                Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(map);
            }
        }, executor);
        // feign在远程调用请求之前要构造
        // 3、查询用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.setIntegration(integration);
        // 4、其他数据自动计算
        // TODO 5、防重令牌
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        return confirmVo;
    }

2、在gulimall-order 服务中创建商品是否有库存的VO类

在 Gulimall-order 服务中 package com.atguigu.gulimall.order.vo 路径下创建 SkuStockVo 类

package com.atguigu.gulimall.order.vo;
@Data
public class SkuStockVo {
    private Long skuId;
    private Boolean hasStock;
}

3、gulimall-ware 库存服务中提供 查询库存的接口

  1. gulimall-ware 服务中 com.atguigu.gulimall.ware.controller 路径下的 WareSkuController 类,之前编写过。
package com.atguigu.gulimall.ware.controller;

@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
    @Autowired
    private WareSkuService wareSkuService;


    // 查询sku是否有库存
    @PostMapping("/hasstock")
    public R getSkusHasStock(@RequestBody List<Long> skuIds){
        // sku_id,stock
        List<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds);
        return R.ok().setData(vos);
    }
  //....
}
  1. gulimall-order 服务中编写远程调用 gulimall-ware 库存服务中 查询库存 feign接口
    gulimall-order 服务下 com.atguigu.gulimall.order.feign 路径下:WareFeignService
package com.atguigu.gulimall.order.feign;

@FeignClient("gulimall-ware")
public interface WareFeignService {
    @PostMapping("/ware/waresku/hasstock")
    R getSkusHasStock(@RequestBody List<Long> skuIds);
}

4、页面效果

[[${orderConfirmData.stocks[item.skuId]?"有货":"无货"}]]
<div class="mi">
   <p>[[${item.title}]]<span style="color: red;">[[${#numbers.formatDecimal(item.price,1,2)}]]</span> <span> x[[${item.count}]]</span> <span>[[${orderConfirmData.stocks[item.skuId]?"有货":"无货"}]]</span></p>
   <p><span>0.095kg</span></p>
   <p class="tui-1"><img src="/static/order/confirm/img/i_07.png" />支持7天无理由退货</p>
</div>

模拟运费效果

需求:选择收货地址,计算物流费

在这里插入图片描述

选择收货地址页面效果

在这里插入图片描述在这里插入图片描述在这里插入图片描述

function highlight(){
   $(".addr-item p").css({"border": "2px solid gray"});
   $(".addr-item p[def='1']").css({"border": "2px solid red"});
}
$(".addr-item p").click(function () {
   $(".addr-item p").attr("def","0");
   $(this).attr("def","1");
   highlight();
   // 获取当前地址id
   var addrId = $(this).attr("addrId");
   // 发送ajax获取运费信息
   getFare(addrId);
});
function getFare(addrId) {
   $.get("http://gulimall.cn/api/ware/wareinfo/fare?addrId="+addrId,function (resp) {
      console.log(resp);
      $("#fareEle").text(resp.data.fare);
      var total = [[${orderConfirmData.total}]]
      // 设置运费信息
      $("#payPriceEle").text(total*1 + resp.data.fare*1);
      // 设置收货人信息
      $("#reciveAddressEle").text(resp.data.address.province+" " + resp.data.address.region+ "" + resp.data.address.detailAddress);
      $("#reveiverEle").text(resp.data.address.name);
   })
}

编写后端接口

gulimall-ware仓储服务编写根据用户地址,返回详细地址并计算物流费,修改gulimall-ware服务中 com.atguigu.gulimall.ware.controller路径下 WareInfoController 类

package com.atguigu.gulimall.ware.controller;

@RestController
@RequestMapping("ware/wareinfo")
public class WareInfoController {
    @Autowired
    private WareInfoService wareInfoService;

    @GetMapping("/fare")
    public R getFare(@RequestParam("addrId") Long addrId){
        FareVo fare = wareInfoService.getFare(addrId);
        return R.ok().setData(fare);
    }
  //...
}

gulimall-ware 服务中 com.atguigu.gulimall.ware.service.impl路径下 WareInfoServiceImpl 类

@Override
public FareVo getFare(Long addrId) {
  FareVo fareVo = new FareVo();
  R r = memberFeignService.addrInfo(addrId);
  MemberAddressVo data = r.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {
  });
  if (data!=null) {
    // 简单处理:截取手机号最后一位作为邮费
    String phone = data.getPhone();
    String substring = phone.substring(phone.length() - 1, phone.length());
    BigDecimal bigDecimal = new BigDecimal(substring);
    fareVo.setAddressVo(data);
    fareVo.setFare(bigDecimal);
    return fareVo;
  }
  return null;
}

gulimall-ware服务中 com.atguigu.gulimall.ware.feign路径下 MemberFeignService远程查询地址详细信息feign接口

package com.atguigu.gulimall.ware.feign;

@FeignClient("gulimall-member")
public interface MemberFeignService {
    /**
     * 根据地址id查询地址的详细信息
     * @param id
     * @return
     */
    @RequestMapping("/member/memberreceiveaddress/info/{id}")
    R addrInfo(@PathVariable("id") Long id);
}

gulimall-ware 服务中 com.atguigu.gulimall.ware.vo路径下的 Vo

@Data
public class FareVo {
    private MemberAddressVo addressVo;
    private BigDecimal fare;
}

接口幂等性

什么是幂等性

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用;比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条,这就没有保证接口的幂等性。我们项目中处理订单的提交时为了避免订单的重复提交,用专业的术语就称之为接口幂等性,通俗点讲就是用户提交一次和用户提交一百次的结果是一样的,数据库中只会有一条订单记录。

简单来说就是无论多少次重复的操作,结果都是一样的。就像数字 1 的无论多少次幂,结果都是 1 。

在我们的项目中如下情况可能需要幂等性处理:

  1. 用户多次点击按钮

  2. 用户页面回退再次提交

  3. 微服务互相调用,由于网络问题,导致请求失败。

  4. feign调用其他业务触发重试机制情况

什么情况下需要幂等

以 SQL 为例,有些操作是天然幂等的。

SELECT * FROM table WHER id=?,无论执行多少次都不会改变状态,是天然的幂等

UPDATE tab1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,也是幂等操作

delete from user where userid=1,多次操作,结果一样,具备幂等性

insert into user(userid,name) values(1,'a')userid 为唯一主键,即重复操作上面的业务,只 会插入一条用户数据,具备幂等性

UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的

insert into user(userid,name) values(1,'a') 如果 userid 不是主键,可以重复,那上面业务多次操 作,数据都会新增多条,不具备幂等性

幂等的解决方案

(1) Token机制

① 服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的, 就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。

② 然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。

③ 服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token, 继续执行业务。

④ 如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行。

危险性:

① 先删除 token 还是后删除 token

  • 先删除可能导致,业务确实没有执行,重试还带上之前 token,由于防重设计导致,请求还是不能执行。
  • 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除 token,别人继续重试,导致业务被执行两遍。
  • 我们最好设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求。

② Token 获取、比较和删除必须是原子性

redis.get(token)token.equalsredis.del(token)如果这几个操作不是原子,可能导致高并发下,都 get 到同样的数据,判断都成功,继续业务并发执行。

所以可以在 redis 使用 lua 脚本完成这个操作:

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
1
(2) 各种锁机制

1、数据库的悲观锁

select * from xxx where id = 1 for update;
1

悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。 另外要注意的是,id 字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会 非常麻烦。

2、数据库的乐观锁

这种方法适合在更新的场景中:

update t_goods set count = count -1 , version = version + 1 where good_id=2 and versio
1

根据 version 版本,也就是在操作库存前先获取当前商品的 version 版本号,然后操作的时候 带上此 version 号。我们梳理下,我们第一次操作库存时,得到version 为 1,调用库存服务 version 变成了 2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传如的 version 还是 1,再执行上面的 sql 语句时,就不会执行;因为 version 已经变 为 2 了,where 条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。 乐观锁主要使用于处理读多写少的问题。

(3) 业务层分布式锁

如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断 这个数据是否被处理过。

(4) 各种唯一约束

① 数据库唯一约束

插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。 我们在数据库层面防止重复

这个机制是利用了数据库的主键唯一约束的特性,解决了在 insert 场景时幂等问题。但主键 的要求不是自增的主键,这样就需要业务生成全局唯一的主键。

如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要 不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。

② redis set 防重

很多数据需要处理,只能被处理一次,比如我们可以计算数据的 MD5 将其放入 redis 的 set, 每次处理数据,先看这个 MD5 是否已经存在,存在就不处理。

Redis set的防重场景:每个数据的MD5加密后的值唯一,网盘就可以根据上传的数据进行MD5加密,将加密后的数据存储至Redis的set里,下次你上传同样的东西时先会去set进行判断是否存在,存在就不处理。

(5) 防重表

使用订单号 orderNo 做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且 他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。

之前说的 redis 防重也算。

防重表的应用场景:当我们去解库存的时候,先去防重表里插入一条数据,当请求再次过来的时候,先去防重表里插入数据,只有当插入成功才能进行下一步操作。

(6) 全局请求唯一 id

调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。

可以使用 nginx 设置每一个请求的唯一 id。Nginx为每一个请求设置唯一id可以用作链路追踪,看这个请求请求了那些服务

proxy_set_header X-Request-Id $request_id

提交订单

订单服务的执行流程如下图所示

image-20230112210222941

添加防重令牌

这里我们先把之前订单提交没有实现的防重令牌实现了,就是向redis缓存中间件中放入一个token,key为一个UUID+订单号,value就是一个令牌(相当于验证码,用来保证订单唯一的),之后再提交订单就会用这个订单的订单号组合令牌前缀去redis中取令牌,只有成功取出令牌才可以处理订单,取不出来表示这个订单不能处理,则直接返回。

gulimall-order服务 com.atguigu.gulimall.order.service.impl路径下的 OrderServiceImpl

package com.atguigu.gulimall.order.service.impl;

@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {
    @Autowired
    MemberFeignService memberFeignService;
    @Autowired
    CartFeignService cartFeignService;
    @Autowired
    ThreadPoolExecutor executor;
    @Autowired
    WareFeignService wareFeignService;
    @Autowired
    StringRedisTemplate redisTemplate;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<OrderEntity> page = this.page(
                new Query<OrderEntity>().getPage(params),
                new QueryWrapper<OrderEntity>()
        );
        return new PageUtils(page);
    }
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        // 获取主线程的请求域
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 1、远程查询所有的地址列表
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 将主线程的请求域放在该线程请求域中
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddressVos(address);
        }, executor);
        // 2、远程查询购物车所有选中的购物项
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            // 将主线程的请求域放在该线程请求域中
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor).thenRunAsync(()->{
            // 批量查询商品项库存
            List<OrderItemVo> items = confirmVo.getItems();
            List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
            R hasStock = wareFeignService.getSkusHasStock(collect);
            List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
            });
            if (data != null) {
                Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(map);
            }
        }, executor);
        // feign在远程调用请求之前要构造
        // 3、查询用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.setIntegration(integration);
        // 4、其他数据自动计算
        // TODO 5、防重令牌
        String token = UUID.randomUUID().toString().replace("-", "");
        //生成一个令牌,先存到redis中然后返回给前端页面
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberRespVo.getId(),token,30, TimeUnit.MINUTES);
        //把令牌设置进确认页Vo,前端页面可以拿到这个令牌,并且发送提交订单请求时会带上这个令牌进行验证
        confirmVo.setOrderToken(token);
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        return confirmVo;
    }
}

发送提交订单(下单)请求

Controller层编写下单功能接口

gulimall-order 服务 com.atguigu.gulimall.order.web 路径下的 OrderWebController 类,代码如下

/**
 * 下单功能
 * @param vo
 * @return
 */
@PostMapping("/submitOrder")
public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes redirectAttributes){
    // 1、创建订单、验令牌、验价格、验库存
    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+="商品库存不足";break;
            }
            redirectAttributes.addAttribute("msg",msg);
            return "redirect:http://order.gulimall.cn/toTrade";
        }
    } catch (Exception e){
        if (e instanceof NoStockException) {
            String message = e.getMessage();
            redirectAttributes.addFlashAttribute("msg", message);
        }
        return "redirect:http://order.gulimall.cn/toTrade";
    }
}

封装订单提交的VO

  1. 分析订单页面的提交数据,添加“com.atguigu.gulimall.order.vo.OrderSubmitVo”类,代码如下:
@Data
@ToString
public class OrderSubmitVo {
    /**
     * 收货地址Id
     */
    private Long addrId;
    /**
     * 支付方式
     */
    private Integer payType;
    // 无需提交需要购买的商品,去购物车再获取一遍
    // 优惠发票
    /**
     * 防重令牌
     */
    private String orderToken;
    /**
     * 应付价格,验价
     */
    private BigDecimal payPrice;
    /**
     * 订单备注
     */
    private String note;
    /**
     * 用户相关信息,直接去Session取出登录的用户
     */
}

说明:京东的结算页中的商品信息是实时获取的,结算的时候会去购物车中再去获取一遍,因此,提交页面的数据Vo没必要提交商品信息

  1. 前端页面 confirm.html 提供数据
<form action="http://order.gulimall.cn/submitOrder" method="post">
   <input id="addrIdInput" type="hidden" name="addrId">
   <input id="payPriceInput" type="hidden" name="payPrice">
   <input type="hidden" name="orderToken" th:value="${orderConfirmData.orderToken}">
   <button class="tijiao" type="submit">提交订单</button>
</form>
function getFare(addrId) {
   // 给表单回填的地址
   $("#addrIdInput").val(addrId);
   $.get("http://gulimall.cn/api/ware/wareinfo/fare?addrId="+addrId,function (resp) {
      console.log(resp);
      $("#fareEle").text(resp.data.fare);
      var total = [[${orderConfirmData.total}]]
      // 设置运费信息
      var pryPrice = total*1 + resp.data.fare*1;
      $("#payPriceEle").text(pryPrice);
      $("#payPriceInput").val(pryPrice);
      // 设置收货人信息
      $("#reciveAddressEle").text(resp.data.address.province+" " + resp.data.address.region+ "" + resp.data.address.detailAddress);
      $("#reveiverEle").text(resp.data.address.name);
   })
}

原子验令牌

  • 问题:存在网路延时,同时提交订单时,从Redis中拿到的令牌相同,导致重复提交
  • 原因:因为我们为了实现订单提交的幂等性,所以需要采用一些手段来保证其幂等性,在我们的项目中我们就用了令牌,这个令牌key是前缀+用户id(存疑),value是一个随机值(=验证码),订单提交时需要验证这个令牌,只有令牌通过才能提交,且提交后删除令牌
  • 解决:令牌的对比和删除必须保证原子性,这也是验证令牌的核心

封装提交订单数据

package com.atguigu.gulimall.order.vo;

@Data
public class SubmitOrderResponseVo {
    private OrderEntity order;
    private Integer code;   //0:代表令牌校验失败,1:代表令牌成功删除即成功   
}

修改 SubmitOrderResponseVo 类编写验证令牌操作

/**
 * 下单操作:验令牌、创建订单、验价格、验库存
 * @param vo
 * @return
 */
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
    SubmitOrderResponseVo response = new SubmitOrderResponseVo();
    // 从拦截器中拿到当前的用户
    MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
    // 1、验证令牌【令牌的对比和删除必须保证原子性】,通过使用脚本来完成(0:令牌校验失败; 1: 删除成功)
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    String orderToken = vo.getOrderToken();
    // 原子验证令牌和删除令牌
    Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(),
            OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(), orderToken);
    if (result == 0L) {
        // 令牌验证失败
        response.setCode(1);
        return response;
    } else {
        // 令牌验证成功
        return response;
    }
}

execute(arg1,arg2,arg3)参数解释:

  • arg1:用DefaultRedisScript的构造器封装脚本和返回值类型
  • arg2:数组,用于存放Redis中token的key
  • arg3:用于比较的token即浏览器存储的token

创建订单、订单项等信息

gulimall-order服务中 com.atguigu.gulimall.order.service.impl 路径下的 OrderServiceImpl 类

/**
 * 创建订单、订单项等信息
 * @return
 */
private OrderCreateTo createOrder(){
    OrderCreateTo createTo = new OrderCreateTo();
    // 1、生成一个订单号
    String orderSn = IdWorker.getTimeId();
    // 2、构建一个订单
    OrderEntity orderEntity = buildOrder(orderSn);
    // 3、获取到所有的订单项
    List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
    // 4、计算价格、积分等相关信息
    computePrice(orderEntity,itemEntities);

    createTo.setOrder(orderEntity);
    createTo.setOrderItems(itemEntities);
    return createTo;
}

创建订单

gulimall-order服务中 com.atguigu.gulimall.order.service.impl 路径下的 OrderServiceImpl 类

/**
 * 构建订单
 * @param orderSn
 * @return
 */
private OrderEntity buildOrder(String orderSn) {
    MemberRespVo respVp = LoginUserInterceptor.loginUser.get();
    OrderEntity entity = new OrderEntity();
    entity.setOrderSn(orderSn);
    entity.setMemberId(respVp.getId());

    OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
    // 1、获取运费 和 收货信息
    R fare = wareFeignService.getFare(orderSubmitVo.getAddrId());
    FareVo fareResp = fare.getData(new TypeReference<FareVo>() {});
    // 2、设置运费
    entity.setFreightAmount(fareResp.getFare());
    // 3、设置收货人信息
    entity.setReceiverPostCode(fareResp.getAddress().getPostCode());
    entity.setReceiverProvince(fareResp.getAddress().getProvince());
    entity.setReceiverRegion(fareResp.getAddress().getRegion());
    entity.setReceiverCity(fareResp.getAddress().getCity());
    entity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
    entity.setReceiverName(fareResp.getAddress().getName());
    entity.setReceiverPhone(fareResp.getAddress().getPhone());
    // 4、设置订单的相关状态信息
    entity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
    // 5、默认取消信息
    entity.setAutoConfirmDay(7);
    return entity;
}

创建远程调用 gulimall-ware 服务 计算运费和详细地址方法的接口

gulimall-order服务中 com.atguigu.gulimall.order.feign 路径下的 WareFeignService 类

package com.atguigu.gulimall.order.feign;

@FeignClient("gulimall-ware")
public interface WareFeignService {
    @PostMapping("/ware/waresku/hasstock")
    R getSkusHasStock(@RequestBody List<Long> skuIds);
    /**
     * 计算运费和详细地址
     * @param addrId
     * @return
     */
    @GetMapping("/ware/wareinfo/fare")
    R getFare(@RequestParam("addrId") Long addrId);
}

创建计算运费和详细地址方法信息封装VO

gulimall-order服务中 com.atguigu.gulimall.order.vo 路径下的 FareVo 类

package com.atguigu.gulimall.order.vo;

@Data
public class FareVo {
    private MemberAddressVo address;
    private BigDecimal fare;
}

构造订单项数据

构建订单项数据

gulimall-order服务中 com.atguigu.gulimall.order.service.impl 路径下的 OrderServiceImpl 类

/**
 * 构建所有订单项数据
 * @return
 */
private  List<OrderItemEntity> buildOrderItems(String orderSn) {
    // 最后确定每个购物项的价格
    List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
    if (currentUserCartItems != null && currentUserCartItems.size()>0){
        List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> {
            OrderItemEntity itemEntity = buildOrderItem(cartItem);
            itemEntity.setOrderSn(orderSn);
            return itemEntity;
        }).collect(Collectors.toList());
        return itemEntities;
    }
    return null;
}
/**
 * 构建某一个订单项
 * @param cartItem
 * @return
 */
private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
    OrderItemEntity itemEntity = new OrderItemEntity();
    // 1、订单信息:订单号 v
    // 2、商品的spu信息
    Long skuId = cartItem.getSkuId();
    R r = productFeignService.getSpuInfoBySkuId(skuId);
    SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>() {
    });
    itemEntity.setSpuId(data.getId());
    itemEntity.setSpuBrand(data.getBrandId().toString());
    itemEntity.setSpuName(data.getSpuName());
    itemEntity.setCategoryId(data.getCatalogId());
    // 3、商品的sku信息  v
    itemEntity.setSkuId(cartItem.getSkuId());
    itemEntity.setSkuName(cartItem.getTitle());
    itemEntity.setSkuPic(cartItem.getImage());
    itemEntity.setSkuPrice(cartItem.getPrice());
    itemEntity.setSkuQuantity(cartItem.getCount());
    itemEntity.setSkuAttrsVals(StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(),";"));
    // 4、优惠信息【不做】
    // 5、积分信息
    itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
    itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
    // 6、订单项的价格信息
    itemEntity.setPromotionAmount(new BigDecimal("0"));
    itemEntity.setCouponAmount(new BigDecimal("0"));
    itemEntity.setIntegrationAmount(new BigDecimal("0"));
    // 当前订单项的实际金额 总额-各种优惠
    BigDecimal orign = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
    BigDecimal subtract = orign.subtract(itemEntity.getCouponAmount()).
            subtract(itemEntity.getCouponAmount()).
            subtract(itemEntity.getIntegrationAmount());
    itemEntity.setRealAmount(subtract);
    return itemEntity;
}

gulimall-product服务中编写通过skuId查询spu信息接口

  1. gulimall-product服务 com.atguigu.gulimall.product.app 路径下 SpuInfoController 类,代码如下:
package com.atguigu.gulimall.product.app;

@RestController
@RequestMapping("product/spuinfo")
public class SpuInfoController {
    @Autowired
    private SpuInfoService spuInfoService;

    /**
     * 查询指定sku的spu信息
     * @param skuId
     * @return
     */
    @GetMapping("/skuId/{id}")
    public R getSpuInfoBySkuId(@PathVariable("id") Long skuId) {
        SpuInfoEntity entity = spuInfoService.getSpuInfoBySkuId(skuId);
        return R.ok().setData(entity);
    }

gulimall-product服务 com.atguigu.gulimall.product.service.impl 路径下 SpuInfoServiceImpl 类,代码如下:

package com.atguigu.gulimall.product.service.impl;

@Override
public SpuInfoEntity getSpuInfoBySkuId(Long skuId) {
    SkuInfoEntity byId = skuInfoService.getById(skuId);
    Long spuId = byId.getSpuId();
    SpuInfoEntity spuInfoEntity = getById(spuId);
    return spuInfoEntity;
}

gulimall-order服务 com.atguigu.gulimall.order.feign 路径下 ProductFeignService 类,代码如下:

package com.atguigu.gulimall.order.feign;
@FeignClient("gulimall-product")
public interface ProductFeignService {

    @GetMapping("/product/spuinfo/skuId/{id}")
    R getSpuInfoBySkuId(@PathVariable("id") Long skuId);
}

gulimall-order服务 com.atguigu.gulimall.order.vo 路径下 SpuInfoVo 类,用来接收查询过来的Spu信息;代码如下:

package com.atguigu.gulimall.order.vo;

@Data
public class SpuInfoVo {
    /**
     * 商品id
     */
    @TableId
    private Long id;
    /**
     * 商品名称
     */
    private String spuName;
    /**
     * 商品描述
     */
    private String spuDescription;
    /**
     * 所属分类id
     */
    private Long catalogId;
    /**
     * 品牌id
     */
    private Long brandId;
    /**
     *
     */
    private BigDecimal weight;
    /**
     * 上架状态[0 - 新建,1 - 上架,2-下架]
     */
    private Integer publishStatus;
    /**
     *
     */
    private Date createTime;
    /**
     *
     */
    private Date updateTime;
}

计算价格

/**
 * 计算价格
 * @param orderEntity
 * @param itemEntities
 */
private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
    BigDecimal total = new BigDecimal("0.0");
    BigDecimal coupon = new BigDecimal("0.0");
    BigDecimal integration = new BigDecimal("0.0");
    BigDecimal promotion = new BigDecimal("0.0");
    BigDecimal gift = new BigDecimal("0.0");
    BigDecimal growth = new BigDecimal("0.0");
    // 1、订单的总额,叠加每一个订单项的总额信息
    for (OrderItemEntity entity : itemEntities) {
        total = total.add(entity.getRealAmount());
        coupon = coupon.add(entity.getCouponAmount());
        integration = integration.add(entity.getIntegrationAmount());
        promotion = promotion.add(entity.getPromotionAmount());
        gift = gift.add(new BigDecimal(entity.getGiftIntegration()));
        growth = growth.add(new BigDecimal(entity.getGiftGrowth()));
    }
    // 订单总额
    orderEntity.setTotalAmount(total);
    // 应付总额
    orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
    orderEntity.setCouponAmount(coupon);
    orderEntity.setIntegrationAmount(integration);
    orderEntity.setPromotionAmount(promotion);
    // 设置积分等信息
    orderEntity.setIntegration(gift.intValue());
    orderEntity.setGrowth(growth.intValue());
    orderEntity.setDeleteStatus(0);//0 未删除
}

保存订单数据、锁定库存

在这里插入图片描述

保存订单数据

1、编写 保存订单数据并锁定库存逻辑实现代码

/**
 * 下单操作:验令牌、创建订单、验价格、验库存
 * @param vo
 * @return
 */
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
    // 在当前线程共享 OrderSubmitVo
    confirmVoThreadLocal.set(vo);
    SubmitOrderResponseVo response = new SubmitOrderResponseVo();
    // 从拦截器中拿到当前的用户
    MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
    response.setCode(0);
    // 1、验证令牌【令牌的对比和删除必须保证原子性】,通过使用脚本来完成(0:令牌校验失败; 1: 删除成功)
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    String orderToken = vo.getOrderToken();
    // 原子验证令牌和删除令牌
    Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
            Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
    if (result == 0L) {
        // 令牌验证失败
        response.setCode(1);
        return response;
    } else {
        // 令牌验证成功
        // 2、创建订单、订单项等信息
        OrderCreateTo order = createOrder();
        // 3、验价
        BigDecimal payAmount = order.getOrder().getPayAmount();
        BigDecimal payPrice = vo.getPayPrice();
        if (Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
            // 金额对比成功
            // 4、保存订单;
            saveOrder(order);
            // 5、库存锁定,只要有异常回滚订单数据
            // 订单号,所有订单项(skuId,skuName,num)
            WareSkuLockVo lockVo = new WareSkuLockVo();
            lockVo.setOrderSn(order.getOrder().getOrderSn());
            List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
                OrderItemVo itemVo = new OrderItemVo();
                itemVo.setSkuId(item.getSkuId());
                itemVo.setCount(item.getSkuQuantity());
                itemVo.setTitle(item.getSkuName());
                return itemVo;
            }).collect(Collectors.toList());
            lockVo.setLocks(locks);
            // TODO 远程锁库存
            R r = wareFeignService.orderLockStock(lockVo);
            if (r.getCode() == 0) {
                // 锁成功了
                response.setOrder(order.getOrder());
                return response;
            }else {
                // 锁定失败
                throw new NoStockException((String) r.get("msg"));
            }

        } else {
            // 金额对比失败
            response.setCode(2);
            return response;
        }
    }
}

2、编写超时异常类

gulimall-common服务中com.atguigu.common.exception路径下的 NoStockException 接口:

package com.atguigu.common.exception;

public class NoStockException extends RuntimeException{
    private Long skuId;

    public NoStockException(Long skuId){
        super("商品id:"+skuId+";没有足够的库存了!");
    }
    public NoStockException(String message) {
        super(message);
    }
    @Override
    public String getMessage() {
        return super.getMessage();
    }
    public Long getSkuId() {
        return skuId;
    }
    public void setSkuId(Long skuId) {
        this.skuId = skuId;
    }
}

锁定库存

1、gulimall-order服务中编写远程调用 gulimall-ware (仓储服务) 锁定库存方法的接口

gulimall-order服务中com.atguigu.gulimall.order.feign路径下的 WareFeignService 接口:

package com.atguigu.gulimall.order.feign;

@FeignClient("gulimall-ware")
public interface WareFeignService {
		//....
    /**
     * 锁定指定订单的库存
     * @param vo
     * @return
     */
    @PostMapping("/ware/waresku/lock/order")
    R orderLockStock(@RequestBody WareSkuLockVo vo);
}

2、gulimall-ware (仓储服务)中编写锁定库存的接口

gulimall-ware服务中com.atguigu.gulimall.ware.controller路径下的 WareSkuController 类:

package com.atguigu.gulimall.ware.controller;

@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
    @Autowired
    private WareSkuService wareSkuService;
    /**
     * 锁定订单项库存
     * @param vo
     * @return
     */
    @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());
        }
    }
  //....
}

gulimall-ware服务中com.atguigu.gulimall.ware.service.impl路径下的 WareSkuServiceImpl 类:

package com.atguigu.gulimall.ware.service.impl;

@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {
    @Autowired
    WareSkuDao wareSkuDao;
    @Autowired
    ProductFeignService productFeignService;
		//......
    /**
     * 锁定指定订单的库存
     * @param vo
     * @return
     * (rollbackFor = NoStockException.class)
     * 默认只要是运行时异常都会回滚,锁以上面那个注解的属性也可以不加
     */
    @Transactional
    @Override
    public Boolean orderLockStock(WareSkuLockVo vo) {
        // 1、每个商品在哪个库存里有库存
        List<OrderItemVo> locks = vo.getLocks();
        List<SkuWareHashStock> collect = locks.stream().map(item -> {
            SkuWareHashStock stock = new SkuWareHashStock();
            Long skuId = item.getSkuId();
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());
            // 查询这个商品在哪里有库存
            List<Long> wareIds = wareSkuDao.listWareIdHashSkuStock(skuId);
            stock.setWareId(wareIds);
            return stock;
        }).collect(Collectors.toList());
        // 2、锁定库存
        for (SkuWareHashStock hashStock : collect) {
            Boolean skuStocked = false;
            Long skuId = hashStock.getSkuId();
            List<Long> wareIds = hashStock.getWareId();
            if (wareIds == null || wareIds.size()==0){
                // 没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }
            for (Long wareId : wareIds) {
                // 成功就返回1,否则就返回0
                Long count = wareSkuDao.lockSkuStock(skuId,wareId,hashStock.getNum());
                if (count == 1){
                    skuStocked = true;
                    break;
                } else {
                    // 当前仓库锁失败,重试下一个仓库
                }
            }
            if (skuStocked == false){
                // 当前商品所有仓库都没有锁住,其他商品也不需要锁了,直接返回没有库存了
                throw new NoStockException(skuId);
            }
        }
        // 3、运行到这,全部都是锁定成功的
        return true;
    }
    @Data
    class SkuWareHashStock{
        private Long skuId;     // skuid
        private Integer num;    // 锁定件数
        private List<Long> wareId;  // 锁定仓库id
    }
}

查询这个商品在哪里有库存

gulimall-ware服务中com.atguigu.gulimall.ware.dao路径下的 WareSkuDao 类:

package com.atguigu.gulimall.ware.dao;

@Mapper
public interface WareSkuDao extends BaseMapper<WareSkuEntity> {

    /**
     * 通过skuId查询在哪个仓库有库存
     * @param skuId
     * @return  仓库的编号
     */
    List<Long> listWareIdHashSkuStock(@Param("skuId") Long skuId);
    /**
     * 锁库存
     * @param skuId
     * @param wareId
     * @param num
     * @return
     */
    Long lockSkuStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num);

}

gulimall-ware服务中gulimall-ware/src/main/resources/mapper/ware路径下的 WareSkuDao.xml:

<update id="addStock">
    UPDATE `wms_ware_sku` SET stock=stock+#{skuNum} WHERE sku_id=#{skuId} AND ware_id=#{wareId}
</update>
<select id="listWareIdHashSkuStock" resultType="java.lang.Long">
    SELECT ware_id FROM wms_ware_sku WHERE sku_id=#{skuId} and stock-stock_locked>0;
</select>

编写异常返回类

gulimall-ware服务中com.atguigu.gulimall.ware.exception路径下的 NoStockException:

package com.atguigu.gulimall.ware.exception;

public class NoStockException extends RuntimeException{
    private Long skuId;
    public NoStockException(Long skuId){
        super("商品id:"+skuId+";没有足够的库存了!");
    }
    public Long getSkuId() {
        return skuId;
    }
    public void setSkuId(Long skuId) {
        this.skuId = skuId;
    }
}

3、在 错误码和错误信息定义类 BizCodeEnume枚举类中新增 库存 错误码和信息

gulimall-common服务中com.atguigu.common.exception路径下的 BizCodeEnume:

21: 库存的错误状态码前缀

package com.atguigu.common.exception;

public enum BizCodeEnume {
    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败"),
    SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,稍后再试"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
    USER_EXIST_EXCEPTION(15001,"用户名已存在"),
    PHONE_EXIST_EXCEPTION(15002,"手机号已被注册"),
    NO_STOCK_EXCEPTION(21000,"商品库存不足"),
    LOGINACCT_PASSWORD_INVAILD_EXCEPTION(15003,"账号或密码错误");

    private int code;
    private String msg;
    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }
    public int getCode() {
        return code;
    }
    public String getMsg() {
        return msg;
    }
}

前端页面的修改

  1. 订单提交成功,跳转到支付页面 pay.html
<div class="Jdbox_BuySuc">
  <dl>
    <dt><img src="/static/order/pay/img/saoyisao.png" alt=""></dt>
    <dd>
      <span>订单提交成功,请尽快付款!订单号:[[${submitOrderResp.order.orderSn}]]</span>
      <span>应付金额<font>[[${#numbers.formatDecimal(submitOrderResp.order.payAmount,1,2)}]]</font></span>
    </dd>
    <dd>
      <span>推荐使用</span>
      <span>扫码支付请您在<font>24小时</font>内完成支付,否则订单会被自动取消(库存紧订单请参见详情页时限)</span>
      <span>订单详细</span>
    </dd>
  </dl>
</div>
  1. 订单提交失败,重定项到confirm.html 并回显 失败原因
<p class="p1">填写并核对订单信息 <span style="color: red" th:value="${msg!=null}" th:text="${msg}"></span></p>

在这里插入图片描述
在这里插入图片描述

主体代码

1、Controller层接口编写

gulimall-order服务中com.atguigu.gulimall.order.web路径下的 OrderWebController:

package com.atguigu.gulimall.order.web;

@Controller
public class OrderWebController {
    @Autowired
    OrderService orderService;

    @GetMapping("/toTrade")
    public String toTrade(Model model) throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = orderService.confirmOrder();
        model.addAttribute("orderConfirmData",confirmVo);
        return "confirm";
    }
    /**
     * 下单功能
     * @param vo
     * @return
     */
    @PostMapping("/submitOrder")
    public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes redirectAttributes){
        // 1、创建订单、验令牌、验价格、验库存
        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+="商品库存不足";break;
                }
                redirectAttributes.addAttribute("msg",msg);
                return "redirect:http://order.gulimall.cn/toTrade";
            }
        } catch (Exception e){
            if (e instanceof NoStockException) {
                String message = e.getMessage();
                redirectAttributes.addFlashAttribute("msg", message);
            }
            return "redirect:http://order.gulimall.cn/toTrade";
        }

    }
}

2、Service层代码

gulimall-order服务中com.atguigu.gulimall.order.service.impl路径下的 OrderServiceImpl:

@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {
    private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();
    @Autowired
    MemberFeignService memberFeignService;
    @Autowired
    CartFeignService cartFeignService;
    @Autowired
    ThreadPoolExecutor executor;
    @Autowired
    WareFeignService wareFeignService;
    @Autowired
    StringRedisTemplate redisTemplate;
    @Autowired
    ProductFeignService productFeignService;
    @Autowired
    OrderItemService orderItemService;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<OrderEntity> page = this.page(
                new Query<OrderEntity>().getPage(params),
                new QueryWrapper<OrderEntity>()
        );

        return new PageUtils(page);
    }
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        // 获取主线程的请求域
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 1、远程查询所有的地址列表
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 将主线程的请求域放在该线程请求域中
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddressVos(address);
        }, executor);
        // 2、远程查询购物车所有选中的购物项
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            // 将主线程的请求域放在该线程请求域中
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor).thenRunAsync(()->{
            // 批量查询商品项库存
            List<OrderItemVo> items = confirmVo.getItems();
            List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
            R hasStock = wareFeignService.getSkusHasStock(collect);
            List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
            });
            if (data != null) {
                Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(map);
            }
        }, executor);
        // feign在远程调用请求之前要构造
        // 3、查询用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.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);
        confirmVo.setOrderToken(token);
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        return confirmVo;
    }
    /**
     * 下单操作:验令牌、创建订单、验价格、验库存
     * @param vo
     * @return
     */
    @Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
        // 在当前线程共享 OrderSubmitVo
        confirmVoThreadLocal.set(vo);
        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        // 从拦截器中拿到当前的用户
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        response.setCode(0);
        // 1、验证令牌【令牌的对比和删除必须保证原子性】,通过使用脚本来完成(0:令牌校验失败; 1: 删除成功)
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        String orderToken = vo.getOrderToken();
        // 原子验证令牌和删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
        if (result == 0L) {
            // 令牌验证失败
            response.setCode(1);
            return response;
        } else {
            // 令牌验证成功
            // 2、创建订单、订单项等信息
            OrderCreateTo order = createOrder();
            // 3、验价
            BigDecimal payAmount = order.getOrder().getPayAmount();
            BigDecimal payPrice = vo.getPayPrice();
            if (Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
                // 金额对比成功
                // 4、保存订单;
                saveOrder(order);
                // 5、库存锁定,只要有异常回滚订单数据
                // 订单号,所有订单项(skuId,skuName,num)
                WareSkuLockVo lockVo = new WareSkuLockVo();
                lockVo.setOrderSn(order.getOrder().getOrderSn());
                List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
                    OrderItemVo itemVo = new OrderItemVo();
                    itemVo.setSkuId(item.getSkuId());
                    itemVo.setCount(item.getSkuQuantity());
                    itemVo.setTitle(item.getSkuName());
                    return itemVo;
                }).collect(Collectors.toList());
                lockVo.setLocks(locks);
                // TODO 远程锁库存
                R r = wareFeignService.orderLockStock(lockVo);
                if (r.getCode() == 0) {
                    // 锁成功了
                    response.setOrder(order.getOrder());
                    return response;
                }else {
                    // 锁定失败
                    throw new NoStockException((String) r.get("msg"));
                }
            } else {
                // 金额对比失败
                response.setCode(2);
                return response;
            }
        }
    }
    /**
     * 保存订单、订单项数据
     * @param order
     */
    private void saveOrder(OrderCreateTo order) {
        OrderEntity orderEntity = order.getOrder();
        orderEntity.setModifyTime(new Date());
        this.save(orderEntity);
        List<OrderItemEntity> orderItems = order.getOrderItems();
        orderItemService.saveBatch(orderItems);
    }
    /**
     * 创建订单、订单项等信息
     * @return
     */
    private OrderCreateTo createOrder(){
        OrderCreateTo createTo = new OrderCreateTo();
        // 1、生成一个订单号
        String orderSn = IdWorker.getTimeId();
        // 2、构建一个订单
        OrderEntity orderEntity = buildOrder(orderSn);
        // 3、获取到所有的订单项
        List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
        // 4、计算价格、积分等相关信息
        computePrice(orderEntity,itemEntities);
        createTo.setOrder(orderEntity);
        createTo.setOrderItems(itemEntities);
        return createTo;
    }
    /**
     * 构建订单
     * @param orderSn
     * @return
     */
    private OrderEntity buildOrder(String orderSn) {
        MemberRespVo respVp = LoginUserInterceptor.loginUser.get();
        OrderEntity entity = new OrderEntity();
        entity.setOrderSn(orderSn);
        entity.setMemberId(respVp.getId());
        OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
        // 1、获取运费 和 收货信息
        R fare = wareFeignService.getFare(orderSubmitVo.getAddrId());
        FareVo fareResp = fare.getData(new TypeReference<FareVo>() {
        });
        // 2、设置运费
        entity.setFreightAmount(fareResp.getFare());
        // 3、设置收货人信息
        entity.setReceiverPostCode(fareResp.getAddress().getPostCode());
        entity.setReceiverProvince(fareResp.getAddress().getProvince());
        entity.setReceiverRegion(fareResp.getAddress().getRegion());
        entity.setReceiverCity(fareResp.getAddress().getCity());
        entity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
        entity.setReceiverName(fareResp.getAddress().getName());
        entity.setReceiverPhone(fareResp.getAddress().getPhone());
        // 4、设置订单的相关状态信息
        entity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        // 5、默认取消信息
        entity.setAutoConfirmDay(7);
        return entity;
    }
    /**
     * 构建所有订单项数据
     * @return
     */
    private  List<OrderItemEntity> buildOrderItems(String orderSn) {
        // 最后确定每个购物项的价格
        List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
        if (currentUserCartItems != null && currentUserCartItems.size()>0){
            List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> {
                OrderItemEntity itemEntity = buildOrderItem(cartItem);
                itemEntity.setOrderSn(orderSn);
                return itemEntity;
            }).collect(Collectors.toList());
            return itemEntities;
        }
        return null;
    }
    /**
     * 构建某一个订单项
     * @param cartItem
     * @return
     */
    private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
        OrderItemEntity itemEntity = new OrderItemEntity();
        // 1、订单信息:订单号 v
        // 2、商品的spu信息
        Long skuId = cartItem.getSkuId();
        R r = productFeignService.getSpuInfoBySkuId(skuId);
        SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>() {
        });
        itemEntity.setSpuId(data.getId());
        itemEntity.setSpuBrand(data.getBrandId().toString());
        itemEntity.setSpuName(data.getSpuName());
        itemEntity.setCategoryId(data.getCatalogId());
        // 3、商品的sku信息  v
        itemEntity.setSkuId(cartItem.getSkuId());
        itemEntity.setSkuName(cartItem.getTitle());
        itemEntity.setSkuPic(cartItem.getImage());
        itemEntity.setSkuPrice(cartItem.getPrice());
        itemEntity.setSkuQuantity(cartItem.getCount());
        itemEntity.setSkuAttrsVals(StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(),";"));
        // 4、优惠信息【不做】
        // 5、积分信息
        itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
        itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
        // 6、订单项的价格信息
        itemEntity.setPromotionAmount(new BigDecimal("0"));
        itemEntity.setCouponAmount(new BigDecimal("0"));
        itemEntity.setIntegrationAmount(new BigDecimal("0"));
        // 当前订单项的实际金额 总额-各种优惠
        BigDecimal orign = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
        BigDecimal subtract = orign.subtract(itemEntity.getCouponAmount()).
                subtract(itemEntity.getCouponAmount()).
                subtract(itemEntity.getIntegrationAmount());
        itemEntity.setRealAmount(subtract);
        return itemEntity;
    }
    /**
     * 计算价格
     * @param orderEntity
     * @param itemEntities
     */
    private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
        BigDecimal total = new BigDecimal("0.0");
        BigDecimal coupon = new BigDecimal("0.0");
        BigDecimal integration = new BigDecimal("0.0");
        BigDecimal promotion = new BigDecimal("0.0");
        BigDecimal gift = new BigDecimal("0.0");
        BigDecimal growth = new BigDecimal("0.0");
        // 1、订单的总额,叠加每一个订单项的总额信息
        for (OrderItemEntity entity : itemEntities) {
            total = total.add(entity.getRealAmount());
            coupon = coupon.add(entity.getCouponAmount());
            integration = integration.add(entity.getIntegrationAmount());
            promotion = promotion.add(entity.getPromotionAmount());
            gift = gift.add(new BigDecimal(entity.getGiftIntegration()));
            growth = growth.add(new BigDecimal(entity.getGiftGrowth()));
        }
        // 订单总额
        orderEntity.setTotalAmount(total);
        // 应付总额
        orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
        orderEntity.setCouponAmount(coupon);
        orderEntity.setIntegrationAmount(integration);
        orderEntity.setPromotionAmount(promotion);
        // 设置积分等信息
        orderEntity.setIntegration(gift.intValue());
        orderEntity.setGrowth(growth.intValue());
        orderEntity.setDeleteStatus(0);//0 未删除
    }
}

感谢耐心看到这里的同学,觉得文章对您有帮助的话希望同学们不要吝啬您手中的赞,动动您智慧的小手,您的认可就是我创作的动力!
之后还会勤更自己的学习笔记,感兴趣的朋友点点关注哦。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/385025.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DC-4 靶场学习

信息搜集&#xff1a; 首先获取靶场ip&#xff0c;和之前一样。 arp-scan -l nmap -sP 192.168.28.0/24然后访问。 发现需要登录。 漏洞分析: 直接用bp爆破&#xff0c;爆破出来密码为happy&#xff0c;登录。 发现执行了命令&#xff0c;抓包。 修改命令可以执行&#xff…

客户案例|FPGA研发管理解决方案:UniPro瀑布+敏捷 打造高效能组织

2023开年以来&#xff0c;新享科技项目管理软件UniPro收获一波客户侧的点赞好评。在过去一年中&#xff0c;UniPro不断与客户保持高频沟通&#xff0c;满足客户需求为出发点&#xff0c;以产品功能实现为落脚点&#xff0c;不断打磨产品。 以UniPro客户京微齐力为例&#xff0…

Vulnhub靶场----9、DC-9

文章目录一、环境搭建二、渗透流程三、思路总结一、环境搭建 DC-9下载地址&#xff1a;https://download.vulnhub.com/dc/DC-9.zip kali&#xff1a;192.168.144.148 DC-9&#xff1a;192.168.144.158 二、渗透流程 1、信息收集nmap -T5 -A -p- -sV -sT 192.168.144.158思路&am…

IDEA Android 网格布局(GridLayout)示例(计算器界面布局)

网格布局(GridLayout&#xff09; 示例程序效果&#xff08;实现类似vivo手机自带计算器UI&#xff09; 真机和模拟器运行效果&#xff1a; 简述&#xff1a; GridLayout(网格布局)和TableLayout&#xff08;表格布局&#xff09;有类似的地方&#xff0c;通俗来讲可以理解为…

搜广推 Product-based Neural Networks (PNN) - 改进特征交叉的方式

😄 PNN:2016年上海交通大学提出。 文章目录 1、PNN1.1、原理1.2、创新点:product层1.3、product层z部分的输出:l~z~ 的计算方式:1.4、product层z部分的输出:l~p~ 的计算方式:1.4.1、IPNN1.4.2、OPNN1.5、优点1.6、缺点Reference1、PNN PNN:Product-based Neural Netwo…

Spark 故障排除

1 故障排除一&#xff1a;控制reduce端缓冲大小以避免OOM 在Shuffle过程&#xff0c;reduce端task并不是等到map端task将其数据全部写入磁盘后再去拉取&#xff0c;而是map端写一点数据&#xff0c;reduce端task就会拉取一小部分数据&#xff0c;然后立即进行后面的聚合、算子…

colletions学习和链式调用,以及优雅的展示代码

1&#xff0c;python 中尽量减少缩进可以直接 if code ! 1: return {msg:2003} 继续写下面的逻辑 2&#xff0c;关于&#xff08;1&#xff09;&#xff0c;&#xff08;1&#xff0c;&#xff09;区别 &#xff08;1&#xff09;表示直接计算运行 &#xff08;1*2*345&a…

Leetcode.2359 找到离给定两个节点最近的节点

题目链接 Leetcode.2359 找到离给定两个节点最近的节点 Rating &#xff1a; 1715 题目描述 给你一个 n个节点的 有向图 &#xff0c;节点编号为 0到 n - 1&#xff0c;每个节点 至多 有一条出边。 有向图用大小为 n下标从 0开始的数组 edges表示&#xff0c;表示节点 i有一条…

数字档案室测评的些许感悟

我是甲方&#xff0c;明明我家是档案“室”&#xff0c;为什么申请的是数字档案“馆”&#xff1f; 笔者正对着手里的一份方案苦笑&#xff0c;甲方爸爸是某机关单位档案室&#xff0c;方案最后的附件赫然写着几个大字&#xff1a;“申请国家级数字档案馆……“。这样的事屡见…

SpringMVC再学习

基于原生的Servlet&#xff0c;通过了功能强大的前端控制器DispatcherServlet&#xff0c;对请求和相应进行统一处理 如今我们不再去web.xml中去主持servlet 而是直接创建一个配置类ServletContainersInitConfig去基础AbstractDispatcherServletInitializer createServletApp…

高性能 WPF 图表控件LightningChart.NET:支持从 Web 服务器获取数据 | 附最新版试用下载

LightningChart.NET 是一款高性能 WPF 和 Winforms 图表,可以实时可视化多达1万亿个数据点。可有效利用CPU和内存资源&#xff0c;实时监控数据流。同时&#xff0c;LightningChart使用突破性创新技术&#xff0c;以实时优化为前提&#xff0c;大大提升了实时渲染的效率和效果&…

Python的面向对象,详细讲解Python之用处等基本常识

目录 Python 面向对象 面向对象技术简介 创建类 实例 实例 self代表类的实例&#xff0c;而非类 实例 创建实例对象 访问属性 实例 Python内置类属性 实例 python对象销毁(垃圾回收) 实例 实例 类的继承 实例 方法重写 实例 基础重载方法 运算符重载 实例…

机器学习: 可视化反卷积操作

转置卷积操作的详细分解 1. 简介 转置卷积是用于生成图像的&#xff0c;尽管它们已经存在了一段时间&#xff0c;并且得到了很好的解释——我仍然很难理解它们究竟是如何完成工作的。我分享的文章[1]描述了一个简单的实验来说明这个过程。我还介绍了一些有助于提高网络性能的技…

yolov5的基本配置

yolov5的基本配置train.pydata.yaml数据集标签文件格式:总结train.py def parse_opt(knownFalse):parser argparse.ArgumentParser()parser.add_argument(--weights, typestr, defaultROOT / yolov5s.pt, helpinitial weights path)parser.add_argument(--cfg, typestr, defau…

【Java面试篇】Spring中@Transactional注解事务失效的常见场景

文章目录Transactional注解的失效场景☁️前言&#x1f340;前置知识&#x1f341;场景一&#xff1a;Transactional应用在非 public 修饰的方法上&#x1f341;场景二&#xff1a; propagation 属性设置错误&#x1f341;场景三&#xff1a;rollbackFor属性设置错误&#x1f3…

Apache druid未授权命令执行漏洞复现

简介 Apache Druid是一个实时分析型数据库&#xff0c;旨在对大型数据集进行快速的查询分析&#xff08;"OLAP"查询)。Druid最常被当做数据库来用以支持实时摄取、高性能查询和高稳定运行的应用场景&#xff0c;同时&#xff0c;Druid也通常被用来助力分析型应用的图…

【蓝桥杯嵌入式】拓展板之数码管显示

文章目录硬件电路连接方式函数实现文章福利硬件电路 通过上述原理图&#xff0c;可知拓展板上的数码管是一个共阴数码管&#xff0c;也就是说某段数码管接上高电平时&#xff0c;就会点亮。   上述原理图还给出一个提示&#xff0c;即&#xff1a;三个数码管分别与三个74HC59…

十八、Django-restframework之请求和响应(三)

1. 请求对象 REST框架引入了一个扩展了常规HttpRequest的请求对象&#xff0c; 并提供更灵活的请求解析。请求对象的核心功能是属性request.data&#xff0c;这与request.POST类似&#xff0c;但对于WebAPIs更有用。 request.POST # Only handles form data. Only works fo…

Barra模型因子的构建及应用系列五之NonLinear Size因子

一、摘要 在前期的Barra模型系列文章中&#xff0c;我们构建了Size因子、Beta因子、Momentum因子和Residual Volatility因子&#xff0c;并分别创建了对应的单因子策略&#xff0c;本节文章在该系列下进一步构建NonLinear Size因子。从回测结果看&#xff0c;自2022年以来&…

ConcurrentHashMap-Java八股面试(五)

系列文章目录 第一章 ArrayList-Java八股面试(一) 第二章 HashMap-Java八股面试(二) 第三章 单例模式-Java八股面试(三) 第四章 线程池和Volatile关键字-Java八股面试(四) 提示&#xff1a;动态每日更新算法题&#xff0c;想要学习的可以关注一下 文章目录系列文章目录一、…