• 2022谷粒商城学习笔记(二十四)订单服务


    前言

    本系列博客基于B站谷粒商城,只作为本人学习总结使用。这里我会比较注重业务逻辑的编写和相关配置的流程。有问题可以评论或者联系我互相交流。原视频地址谷粒商城雷丰阳版。本人git仓库地址Draknessssw的谷粒商城


    Springsession整合

    pom依赖

    		<!--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>
    
            <!--整合springsession,实现session共享-->
            <dependency>
                <groupId>org.springframework.session</groupId>
                <artifactId>spring-session-data-redis</artifactId>
            </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    相关配置

    spring.redis.host=192.168.75.129
    spring.redis.port=6379
    
    spring.session.store-type=redis
    
    • 1
    • 2
    • 3
    • 4

    session名称配置

    package com.xxxx.gulimall.order.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.session.web.http.CookieSerializer;
    import org.springframework.session.web.http.DefaultCookieSerializer;
    
    
    
    @Configuration
    public class GulimallSessionConfig {
    
        @Bean
        public CookieSerializer cookieSerializer() {
    
            DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
    
            //放大作用域
            cookieSerializer.setDomainName("gulimall.com");
            cookieSerializer.setCookieName("GULISESSION");
    
            return cookieSerializer;
        }
    
    
        @Bean
        public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
            return new GenericJackson2JsonRedisSerializer();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    主启动类加上注解
    在这里插入图片描述


    获取订单结算页面所需数据

    在这里插入图片描述

    @Autowired
        private OrderService orderService;
    
        /**
         * 去结算确认页
         * @param model
         * @param request
         * @return
         * @throws ExecutionException
         * @throws InterruptedException
         */
        @GetMapping(value = "/toTrade")
        public String toTrade(Model model, HttpServletRequest request) throws ExecutionException, InterruptedException {
    
            OrderConfirmVo confirmVo = orderService.confirmOrder();
    
            model.addAttribute("confirmOrderData",confirmVo);
            //展示订单确认的数据
    
            return "confirm";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    webMVC配置一个拦截器,当session中有登录用户信息再放行请求

    在这里插入图片描述

    package com.xxxx.gulimall.order.config;
    
    import com.xxxx.gulimall.order.interceptor.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;
    
    
    
    @Configuration
    public class OrderWebConfig implements WebMvcConfigurer {
    
        @Autowired
        private LoginUserInterceptor loginUserInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    拦截器

    在这里插入图片描述

    在前置拦截器方法中,获取请求uri。匹配一下是否有订单状态和去支付的请求。然后获取请求sessoin中用户的信息后往ThreadLocal中设置用户信息,否则给客户端发送一个跳转登录页的弹出框。

    package com.xxxx.gulimall.order.interceptor;
    
    import com.xxxx.common.vo.MemberResponseVo;
    import org.springframework.stereotype.Component;
    import org.springframework.util.AntPathMatcher;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.PrintWriter;
    
    import static com.xxxx.common.constant.AuthServerConstant.LOGIN_USER;
    
    
    
    @Component
    public class LoginUserInterceptor implements HandlerInterceptor {
    
        public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            String uri = request.getRequestURI();
            AntPathMatcher antPathMatcher = new AntPathMatcher();
            boolean match = antPathMatcher.match("/order/order/status/**", uri);
            boolean match1 = antPathMatcher.match("/payed/notify", uri);
            if (match || match1) {
                return true;
            }
    
            //获取登录的用户信息
            MemberResponseVo attribute = (MemberResponseVo) request.getSession().getAttribute(LOGIN_USER);
    
            if (attribute != null) {
                //把登录后用户的信息放在ThreadLocal里面进行保存
                loginUser.set(attribute);
    
                return true;
            } else {
                //未登录,返回登录页面
                response.setContentType("text/html;charset=UTF-8");
                PrintWriter out = response.getWriter();
                out.println("");
                // session.setAttribute("msg", "请先进行登录");
                // response.sendRedirect("http://auth.gulimall.com/login.html");
                return false;
            }
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    订单页Vo

    package com.xxxx.gulimall.order.vo;
    
    import lombok.Getter;
    import lombok.Setter;
    
    import java.math.BigDecimal;
    import java.util.List;
    import java.util.Map;
    
    
    
    public class OrderConfirmVo {
    
        @Getter @Setter
        /** 会员收获地址列表 **/
        List<MemberAddressVo> memberAddressVos;
    
        @Getter @Setter
        /** 所有选中的购物项 **/
        List<OrderItemVo> items;
    
        /** 发票记录 **/
        @Getter @Setter
        /** 优惠券(会员积分) **/
        private Integer integration;
    
        /** 防止重复提交的令牌 **/
        @Getter @Setter
        private String orderToken;
    
        @Getter @Setter
        Map<Long,Boolean> stocks;
    
        public Integer getCount() {
            Integer count = 0;
            if (items != null && items.size() > 0) {
                for (OrderItemVo item : items) {
                    count += item.getCount();
                }
            }
            return count;
        }
    
    
        /** 订单总额 **/
        //BigDecimal total;
        //计算订单总额
        public BigDecimal getTotal() {
            BigDecimal totalNum = BigDecimal.ZERO;
            if (items != null && items.size() > 0) {
                for (OrderItemVo item : items) {
                    //计算当前商品的总价格
                    BigDecimal itemPrice = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
                    //再计算全部商品的总价格
                    totalNum = totalNum.add(itemPrice);
                }
            }
            return totalNum;
        }
    
    
        /** 应付价格 **/
        //BigDecimal payPrice;
        public BigDecimal getPayPrice() {
            return getTotal();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    会员信息Vo

    package com.xxxx.gulimall.order.vo;
    
    import lombok.Data;
    
    
    
    @Data
    public class MemberAddressVo {
    
        private Long id;
        /**
         * member_id
         */
        private Long memberId;
        /**
         * 收货人姓名
         */
        private String name;
        /**
         * 电话
         */
        private String phone;
        /**
         * 邮政编码
         */
        private String postCode;
        /**
         * 省份/直辖市
         */
        private String province;
        /**
         * 城市
         */
        private String city;
        /**
         * 区
         */
        private String region;
        /**
         * 详细地址(街道)
         */
        private String detailAddress;
        /**
         * 省市区代码
         */
        private String areacode;
        /**
         * 是否默认
         */
        private Integer defaultStatus;
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    购物项Vo

    package com.xxxx.gulimall.order.vo;
    
    import lombok.Data;
    
    import java.math.BigDecimal;
    import java.util.List;
    
    
    
    @Data
    public class OrderItemVo {
    
        private Long skuId;
    
        private Boolean check;
    
        private String title;
    
        private String image;
    
        /**
         * 商品套餐属性
         */
        private List<String> skuAttrValues;
    
        private BigDecimal price;
    
        private Integer count;
    
        private BigDecimal totalPrice;
    
        /** 商品重量 **/
        private BigDecimal weight = new BigDecimal("0.085");
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    接着是实现类

    获取ThreadLocal中当前用户登录信息,当前线程的请求头

    		//构建OrderConfirmVo
            OrderConfirmVo confirmVo = new OrderConfirmVo();
    
            //获取当前用户登录的信息
            MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
    
            //TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里订单服务调用远程接口存在请求头丢失的问题,原因是调用远程接口并不会自动装配cookie信息
    在这里插入图片描述
    配置拦截器对远程请求进行同步装配cookie信息
    在这里插入图片描述

    package com.xxxx.gulimall.order.config;
    
    import feign.RequestInterceptor;
    import feign.RequestTemplate;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * feign拦截器
     */
    
    @Configuration
    public class GuliFeignConfig {
    
        @Bean("requestInterceptor")
        public RequestInterceptor requestInterceptor() {
    
            RequestInterceptor requestInterceptor = new RequestInterceptor() {
                @Override
                public void apply(RequestTemplate template) {
                    //1、使用RequestContextHolder拿到刚进来的请求数据
                    ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    
                    if (requestAttributes != null) {
                        //老请求
                        HttpServletRequest request = requestAttributes.getRequest();
    
                        if (request != null) {
                            //2、同步请求头的数据(主要是cookie)
                            //把老请求的cookie值放到新请求上来,进行一个同步
                            String cookie = request.getHeader("Cookie");
                            template.header("Cookie", cookie);
                        }
                    }
                }
            };
    
            return requestInterceptor;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    接着在每一个线程共享请求数据

    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    
    • 1

    第一个异步任务,远程查询当前用户的收货地址

    		CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
    
                //每一个线程都来共享之前的请求数据
                RequestContextHolder.setRequestAttributes(requestAttributes);
    
                //1、远程查询所有的收货地址列表
                List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
                confirmVo.setMemberAddressVos(address);
            }, threadPoolExecutor);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    远程接口

    package com.xxxx.gulimall.order.feign;
    
    import com.xxxx.gulimall.order.vo.MemberAddressVo;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    import java.util.List;
    
    
    
    @FeignClient("gulimall-member")
    public interface MemberFeignService {
    
        /**
         * 查询当前用户的全部收货地址
         * @param memberId
         * @return
         */
        @GetMapping(value = "/member/memberreceiveaddress/{memberId}/address")
        List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    ****

    	@Override
        public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
    
            List<MemberReceiveAddressEntity> addressList = this.baseMapper.selectList
                    (new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));
    
            return addressList;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    第二个异步任务,查询购物车中的购物项,根据这些购物项再出查询相应的库存

    //开启第二个异步任务
            CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {
    
                //每一个线程都来共享之前的请求数据
                RequestContextHolder.setRequestAttributes(requestAttributes);
    
                //2、远程查询购物车所有选中的购物项
                List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
                confirmVo.setItems(currentCartItems);
                //feign在远程调用之前要构造请求,调用很多的拦截器
            }, threadPoolExecutor).thenRunAsync(() -> {
                List<OrderItemVo> items = confirmVo.getItems();
                //获取全部商品的id
                List<Long> skuIds = items.stream()
                        .map((itemVo -> itemVo.getSkuId()))
                        .collect(Collectors.toList());
    
                //远程查询商品库存信息
                R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
                List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});
    
                if (skuStockVos != null && skuStockVos.size() > 0) {
                    //将skuStockVos集合转换为map
                    Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                    confirmVo.setStocks(skuHasStockMap);
                }
            },threadPoolExecutor);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    查询购物项的远程接口

    package com.xxxx.gulimall.order.feign;
    
    import com.xxxx.gulimall.order.vo.OrderItemVo;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    
    import java.util.List;
    
    
    
    @FeignClient("gulimall-cart")
    public interface CartFeignService {
    
        /**
         * 查询当前用户购物车选中的商品项
         * @return
         */
        @GetMapping(value = "/currentUserCartItems")
        List<OrderItemVo> getCurrentCartItems();
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述

    	@Override
        public List<CartItemVo> getUserCartItems() {
    
            List<CartItemVo> cartItemVoList = new ArrayList<>();
            //获取当前用户登录的信息
            UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
            //如果用户未登录直接返回null
            if (userInfoTo.getUserId() == null) {
                return null;
            } else {
                //获取购物车项
                String cartKey = CART_PREFIX + userInfoTo.getUserId();
                //获取所有的
                List<CartItemVo> cartItems = getCartItems(cartKey);
                if (cartItems == null) {
                    throw new CartExceptionHandler();
                }
                //筛选出选中的
                cartItemVoList = cartItems.stream()
                        .filter(items -> items.getCheck())
                        .map(item -> {
                            //更新为最新的价格(查询数据库)
                            BigDecimal price = productFeignService.getPrice(item.getSkuId());
                            item.setPrice(price);
                            return item;
                        })
                        .collect(Collectors.toList());
            }
    
            return cartItemVoList;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    
        /**
         * 获取购物车里面的数据
         * @param cartKey
         * @return
         */
        private List<CartItemVo> getCartItems(String cartKey) {
            //获取购物车里面的所有商品
            BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
            List<Object> values = operations.values();
            if (values != null && values.size() > 0) {
                List<CartItemVo> cartItemVoStream = values.stream().map((obj) -> {
                    String str = (String) obj;
                    CartItemVo cartItem = JSON.parseObject(str, CartItemVo.class);
                    return cartItem;
                }).collect(Collectors.toList());
                return cartItemVoStream;
            }
            return null;
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    商品的价格可能会有较大可能随时更新,所以得获取最新的。远程调用商品服务来查询

    远程接口

    @FeignClient("gulimall-product")
    public interface ProductFeignService {
        /**
         * 根据skuId查询当前商品的最新价格
         * @param skuId
         * @return
         */
        @GetMapping(value = "/product/skuinfo/{skuId}/price")
        BigDecimal getPrice(@PathVariable("skuId") Long skuId);
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

        @Autowired
        private SkuInfoService skuInfoService;
    
        /**
         * 根据skuId查询当前商品的价格
         * @param skuId
         * @return
         */
        @GetMapping(value = "/{skuId}/price")
        public BigDecimal getPrice(@PathVariable("skuId") Long skuId) {
    
            //获取当前商品的信息
            SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
    
            //获取商品的价格
            BigDecimal price = skuInfo.getPrice();
    
            return price;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    查询商品库存的远程接口

    @FeignClient("gulimall-ware")
    public interface WmsFeignService {
    
        /**
         * 查询sku是否有库存
         * @return
         */
        @PostMapping(value = "/ware/waresku/hasStock")
        R getSkuHasStock(@RequestBody List<Long> skuIds);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    接着设置积分和防重复令牌

    		//3、查询用户积分
            Integer integration = memberResponseVo.getIntegration();
            confirmVo.setIntegration(integration);
    
            //4、价格数据自动计算
    
            //TODO 5、防重令牌(防止表单重复提交)
            //为用户设置一个token,三十分钟过期时间(存在redis)
            String token = UUID.randomUUID().toString().replace("-", "");
            redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
            confirmVo.setOrderToken(token);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    最终效果如下

    
        /**
         * 订单确认页返回需要用的数据
         * @return
         */
        @Override
        public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
    
            //构建OrderConfirmVo
            OrderConfirmVo confirmVo = new OrderConfirmVo();
    
            //获取当前用户登录的信息
            MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
    
            //TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    
            //开启第一个异步任务
            CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
    
                //每一个线程都来共享之前的请求数据
                RequestContextHolder.setRequestAttributes(requestAttributes);
    
                //1、远程查询所有的收货地址列表
                List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
                confirmVo.setMemberAddressVos(address);
            }, threadPoolExecutor);
    
            //开启第二个异步任务
            CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {
    
                //每一个线程都来共享之前的请求数据
                RequestContextHolder.setRequestAttributes(requestAttributes);
    
                //2、远程查询购物车所有选中的购物项
                List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
                confirmVo.setItems(currentCartItems);
                //feign在远程调用之前要构造请求,调用很多的拦截器
            }, threadPoolExecutor).thenRunAsync(() -> {
                List<OrderItemVo> items = confirmVo.getItems();
                //获取全部商品的id
                List<Long> skuIds = items.stream()
                        .map((itemVo -> itemVo.getSkuId()))
                        .collect(Collectors.toList());
    
                //远程查询商品库存信息
                R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
                List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});
    
                if (skuStockVos != null && skuStockVos.size() > 0) {
                    //将skuStockVos集合转换为map
                    Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                    confirmVo.setStocks(skuHasStockMap);
                }
            },threadPoolExecutor);
    
            //3、查询用户积分
            Integer integration = memberResponseVo.getIntegration();
            confirmVo.setIntegration(integration);
    
            //4、价格数据自动计算
    
            //TODO 5、防重令牌(防止表单重复提交)
            //为用户设置一个token,三十分钟过期时间(存在redis)
            String token = UUID.randomUUID().toString().replace("-", "");
            redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
            confirmVo.setOrderToken(token);
    
    
            CompletableFuture.allOf(addressFuture,cartInfoFuture).get();
    
            return confirmVo;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    幂等性问题

    在这里插入图片描述

    幂等解决方案

    在这里插入图片描述

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


    订单提交

    在这里插入图片描述
    下单成功去往支付页面,下单失败则重定向到订单页面。

    
        /**
         * 下单功能
         * @param vo
         * @return
         */
        @PostMapping(value = "/submitOrder")
        public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes attributes) {
    
            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;
                    }
                    attributes.addFlashAttribute("msg",msg);
                    return "redirect:http://order.gulimall.com/toTrade";
                }
            } catch (Exception e) {
                if (e instanceof NoStockException) {
                    String message = ((NoStockException)e).getMessage();
                    attributes.addFlashAttribute("msg",message);
                }
                return "redirect:http://order.gulimall.com/toTrade";
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    订单提交的Vo

    package com.xxxx.gulimall.order.vo;
    
    import lombok.Data;
    
    import java.math.BigDecimal;
    
    
    
    @Data
    public class OrderSubmitVo {
    
        /** 收获地址的id **/
        private Long addrId;
    
        /** 支付方式 **/
        private Integer payType;
        //无需提交要购买的商品,去购物车再获取一遍
        //优惠、发票
    
        /** 防重令牌 **/
        private String orderToken;
    
        /** 应付价格 **/
        private BigDecimal payPrice;
    
        /** 订单备注 **/
        private String remarks;
    
        //用户相关的信息,直接去session中取出即可
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    提交订单的响应Vo

    package com.xxxx.gulimall.order.vo;
    
    import com.xxxx.gulimall.order.entity.OrderEntity;
    import lombok.Data;
    
    
    @Data
    public class SubmitOrderResponseVo {
    
        private OrderEntity order;
    
        /** 错误状态码 **/
        private Integer code;
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    实现类

    首先暂且将请求提交的vo存到threadLocal里,从用户登录的拦截器设置的用户登录信息那里获取用户信息。

    		confirmVoThreadLocal.set(vo);
    
            SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
            //去创建、下订单、验令牌、验价格、锁定库存...
    
            //获取当前用户登录的信息
            MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
            responseVo.setCode(0);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    验证令牌防止重复提交
    lua脚本获取Redis中token和vo中的token比对,一致就删除Redis中的token,其他情况返回0
    而Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId())则是要验证的token中的key

    		//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();
    
            //通过lure脚本原子验证令牌和删除令牌
            Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                    Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
                    orderToken);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果token验证失败,则设置表单返回vo的状态码为1即可(Controller中状态码为1才设置显示表单数据),订单信息不用设置,直接返回。否则就去设置返回Vo的订单详情再返回。

    		if (result == 0L) {
                //令牌验证失败
                responseVo.setCode(1);
                return responseVo;
            } else {
                //令牌验证成功
                //1、创建订单、订单项等信息
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    令牌验证成功,创建订单

     //令牌验证成功
                //1、创建订单、订单项等信息
                OrderCreateTo order = createOrder();
    
    • 1
    • 2
    • 3

    创建订单方法有三个业务方法

    private OrderCreateTo createOrder() {
    
            OrderCreateTo createTo = new OrderCreateTo();
    
            //1、生成订单号
            String orderSn = IdWorker.getTimeId();
            //创建订单号
            OrderEntity orderEntity = builderOrder(orderSn);
    
            //2、获取到所有的订单项
            List<OrderItemEntity> orderItemEntities = builderOrderItems(orderSn);
    
            //3、验价(计算价格、积分等信息)
            computePrice(orderEntity,orderItemEntities);
    
            createTo.setOrder(orderEntity);
            createTo.setOrderItems(orderItemEntities);
    
            return createTo;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    1、构建订单数据的方法

    
        /**
         * 构建订单数据
         * @param orderSn
         * @return
         */
        private OrderEntity builderOrder(String orderSn) {
    
            //获取当前用户登录信息
            MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
    
            OrderEntity orderEntity = new OrderEntity();
            orderEntity.setMemberId(memberResponseVo.getId());
            orderEntity.setOrderSn(orderSn);
            orderEntity.setMemberUsername(memberResponseVo.getUsername());
    
            OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
    
            //远程获取收货地址和运费信息
            R fareAddressVo = wmsFeignService.getFare(orderSubmitVo.getAddrId());
            FareVo fareResp = fareAddressVo.getData("data", new TypeReference<FareVo>() {});
    
            //获取到运费信息
            BigDecimal fare = fareResp.getFare();
            orderEntity.setFreightAmount(fare);
    
            //获取到收货地址信息
            MemberAddressVo address = fareResp.getAddress();
            //设置收货人信息
            orderEntity.setReceiverName(address.getName());
            orderEntity.setReceiverPhone(address.getPhone());
            orderEntity.setReceiverPostCode(address.getPostCode());
            orderEntity.setReceiverProvince(address.getProvince());
            orderEntity.setReceiverCity(address.getCity());
            orderEntity.setReceiverRegion(address.getRegion());
            orderEntity.setReceiverDetailAddress(address.getDetailAddress());
    
            //设置订单相关的状态信息
            orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
            orderEntity.setAutoConfirmDay(7);
            orderEntity.setConfirmStatus(0);
            return orderEntity;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    其中,运费信息和地址信息需要远程调用库存服务来获取

     		//远程获取收货地址和运费信息
            R fareAddressVo = wmsFeignService.getFare(orderSubmitVo.getAddrId());
            FareVo fareResp = fareAddressVo.getData("data", new TypeReference<FareVo>() {});
    
    • 1
    • 2
    • 3

    运费和收货地址信息Vo

    package com.xxxx.gulimall.order.vo;
    
    import lombok.Data;
    
    import java.math.BigDecimal;
    
    
    
    @Data
    public class FareVo {
    
        private MemberAddressVo address;
    
        private BigDecimal fare;
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    远程接口

    @FeignClient("gulimall-ware")
    public interface WmsFeignService {
    
        /**
         * 查询运费和收货地址信息
         * @param addrId
         * @return
         */
        @GetMapping(value = "/ware/wareinfo/fare")
        R getFare(@RequestParam("addrId") Long addrId);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    
        /**
         * 获取运费信息
         * @return
         */
        @GetMapping(value = "/fare")
        public R getFare(@RequestParam("addrId") Long addrId) {
    
            FareVo fare = wareInfoService.getFare(addrId);
    
            return R.ok().setData(fare);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    实现类

    
        /**
         * 计算运费
         * @param addrId
         * @return
         */
        @Override
        public FareVo getFare(Long addrId) {
    
            FareVo fareVo = new FareVo();
    
            //收获地址的详细信息
            R addrInfo = memberFeignService.info(addrId);
    
            MemberAddressVo memberAddressVo = addrInfo.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {});
    
            if (memberAddressVo != null) {
                String phone = memberAddressVo.getPhone();
                //截取用户手机号码最后一位作为我们的运费计算
                //1558022051
                String fare = phone.substring(phone.length() - 10, phone.length()-8);
                BigDecimal bigDecimal = new BigDecimal(fare);
    
                fareVo.setFare(bigDecimal);
                fareVo.setAddress(memberAddressVo);
    
                return fareVo;
            }
            return null;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    这里又套娃去远程调用会员服务查询地址信息

    远程接口

    @FeignClient("gulimall-member")
    public interface MemberFeignService {
    
        /**
         * 根据id获取用户地址信息
         * @param id
         * @return
         */
        @RequestMapping("/member/memberreceiveaddress/info/{id}")
        R info(@PathVariable("id") Long id);
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    
        /**
         * 信息
         */
        @RequestMapping("/info/{id}")
        public R info(@PathVariable("id") Long id){
    		MemberReceiveAddressEntity memberReceiveAddress = memberReceiveAddressService.getById(id);
    
            return R.ok().put("memberReceiveAddress", memberReceiveAddress);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2、构建所有订单项数据

    
        /**
         * 构建所有订单项数据
         * @return
         */
        public List<OrderItemEntity> builderOrderItems(String orderSn) {
    
            List<OrderItemEntity> orderItemEntityList = new ArrayList<>();
    
            //最后确定每个购物项的价格
            List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
            if (currentCartItems != null && currentCartItems.size() > 0) {
                orderItemEntityList = currentCartItems.stream().map((items) -> {
                    //构建订单项数据
                    OrderItemEntity orderItemEntity = builderOrderItem(items);
                    orderItemEntity.setOrderSn(orderSn);
    
                    return orderItemEntity;
                }).collect(Collectors.toList());
            }
    
            return orderItemEntityList;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    商品项Vo

    package com.xxxx.gulimall.order.vo;
    
    import lombok.Data;
    
    import java.math.BigDecimal;
    import java.util.List;
    
    
    
    @Data
    public class OrderItemVo {
    
        private Long skuId;
    
        private Boolean check;
    
        private String title;
    
        private String image;
    
        /**
         * 商品套餐属性
         */
        private List<String> skuAttrValues;
    
        private BigDecimal price;
    
        private Integer count;
    
        private BigDecimal totalPrice;
    
        /** 商品重量 **/
        private BigDecimal weight = new BigDecimal("0.085");
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    获取当前购物车中的所有购物项远程接口

    @FeignClient("gulimall-cart")
    public interface CartFeignService {
    
        /**
         * 查询当前用户购物车选中的商品项
         * @return
         */
        @GetMapping(value = "/currentUserCartItems")
        List<OrderItemVo> getCurrentCartItems();
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
        @Autowired
        private CartService cartService;
    
    
        /**
         * 获取当前用户的购物车商品项
         * @return
         */
        @GetMapping(value = "/currentUserCartItems")
        @ResponseBody
        public List<CartItemVo> getCurrentCartItems() {
    
            List<CartItemVo> cartItemVoList = cartService.getUserCartItems();
    
            return cartItemVoList;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    
        /**
         * 获取当前用户的购物车商品项
         * @return
         */
        @GetMapping(value = "/currentUserCartItems")
        @ResponseBody
        public List<CartItemVo> getCurrentCartItems() {
    
            List<CartItemVo> cartItemVoList = cartService.getUserCartItems();
    
            return cartItemVoList;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    实现类

    	@Override
        public List<CartItemVo> getUserCartItems() {
    
            List<CartItemVo> cartItemVoList = new ArrayList<>();
            //获取当前用户登录的信息
            UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
            //如果用户未登录直接返回null
            if (userInfoTo.getUserId() == null) {
                return null;
            } else {
                //获取购物车项
                String cartKey = CART_PREFIX + userInfoTo.getUserId();
                //获取所有的
                List<CartItemVo> cartItems = getCartItems(cartKey);
                if (cartItems == null) {
                    throw new CartExceptionHandler();
                }
                //筛选出选中的
                cartItemVoList = cartItems.stream()
                        .filter(items -> items.getCheck())
                        .map(item -> {
                            //更新为最新的价格(查询数据库)
                            BigDecimal price = productFeignService.getPrice(item.getSkuId());
                            item.setPrice(price);
                            return item;
                        })
                        .collect(Collectors.toList());
            }
    
            return cartItemVoList;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    其中,获取购物车里的数据

    
        /**
         * 获取购物车里面的数据
         * @param cartKey
         * @return
         */
        private List<CartItemVo> getCartItems(String cartKey) {
            //获取购物车里面的所有商品
            BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
            List<Object> values = operations.values();
            if (values != null && values.size() > 0) {
                List<CartItemVo> cartItemVoStream = values.stream().map((obj) -> {
                    String str = (String) obj;
                    CartItemVo cartItem = JSON.parseObject(str, CartItemVo.class);
                    return cartItem;
                }).collect(Collectors.toList());
                return cartItemVoStream;
            }
            return null;
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    查询每一个购物项的价格
    远程接口

    
        /**
         * 根据skuId查询当前商品的最新价格
         * @param skuId
         * @return
         */
        @GetMapping(value = "/product/skuinfo/{skuId}/price")
        BigDecimal getPrice(@PathVariable("skuId") Long skuId);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    
        /**
         * 根据skuId查询当前商品的价格
         * @param skuId
         * @return
         */
        @GetMapping(value = "/{skuId}/price")
        public BigDecimal getPrice(@PathVariable("skuId") Long skuId) {
    
            //获取当前商品的信息
            SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
    
            //获取商品的价格
            BigDecimal price = skuInfo.getPrice();
    
            return price;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    接着回到构建所有订单项数据的方法,将获取的所有订单项封装重构

    
        /**
         * 构建某一个订单项的数据
         * @param items
         * @return
         */
        private OrderItemEntity builderOrderItem(OrderItemVo items) {
    
            OrderItemEntity orderItemEntity = new OrderItemEntity();
    
            //1、商品的spu信息
            Long skuId = items.getSkuId();
            //获取spu的信息
            R spuInfo = productFeignService.getSpuInfoBySkuId(skuId);
            SpuInfoVo spuInfoData = spuInfo.getData("data", new TypeReference<SpuInfoVo>() {
            });
            orderItemEntity.setSpuId(spuInfoData.getId());
            orderItemEntity.setSpuName(spuInfoData.getSpuName());
            orderItemEntity.setSpuBrand(spuInfoData.getBrandName());
            orderItemEntity.setCategoryId(spuInfoData.getCatalogId());
    
            //2、商品的sku信息
            orderItemEntity.setSkuId(skuId);
            orderItemEntity.setSkuName(items.getTitle());
            orderItemEntity.setSkuPic(items.getImage());
            orderItemEntity.setSkuPrice(items.getPrice());
            orderItemEntity.setSkuQuantity(items.getCount());
    
            //使用StringUtils.collectionToDelimitedString将list集合转换为String
            String skuAttrValues = StringUtils.collectionToDelimitedString(items.getSkuAttrValues(), ";");
            orderItemEntity.setSkuAttrsVals(skuAttrValues);
    
            //3、商品的优惠信息
    
            //4、商品的积分信息
            orderItemEntity.setGiftGrowth(items.getPrice().multiply(new BigDecimal(items.getCount())).intValue());
            orderItemEntity.setGiftIntegration(items.getPrice().multiply(new BigDecimal(items.getCount())).intValue());
    
            //5、订单项的价格信息
            orderItemEntity.setPromotionAmount(BigDecimal.ZERO);
            orderItemEntity.setCouponAmount(BigDecimal.ZERO);
            orderItemEntity.setIntegrationAmount(BigDecimal.ZERO);
    
            //当前订单项的实际金额.总额 - 各种优惠价格
            //原来的价格
            BigDecimal origin = orderItemEntity.getSkuPrice().multiply(new BigDecimal(orderItemEntity.getSkuQuantity().toString()));
            //原价减去优惠价得到最终的价格
            BigDecimal subtract = origin.subtract(orderItemEntity.getCouponAmount())
                    .subtract(orderItemEntity.getPromotionAmount())
                    .subtract(orderItemEntity.getIntegrationAmount());
            orderItemEntity.setRealAmount(subtract);
    
            return orderItemEntity;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    其中,获取商品集合的远程接口

    @FeignClient("gulimall-product")
    public interface ProductFeignService {
    
        /**
         * 根据skuId查询spu的信息
         * @param skuId
         * @return
         */
        @GetMapping(value = "/product/spuinfo/skuId/{skuId}")
        public R getSpuInfoBySkuId(@PathVariable("skuId") Long skuId);
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    
        /**
         * 根据skuId查询spu的信息
         * @param skuId
         * @return
         */
        @GetMapping(value = "/skuId/{skuId}")
        public R getSpuInfoBySkuId(@PathVariable("skuId") Long skuId) {
    
            SpuInfoEntity spuInfoEntity = spuInfoService.getSpuInfoBySkuId(skuId);
    
            return R.ok().setData(spuInfoEntity);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    实现类

    
        /**
         * 根据skuId查询spu的信息
         * @param skuId
         * @return
         */
        @Override
        public SpuInfoEntity getSpuInfoBySkuId(Long skuId) {
    
            //先查询sku表里的数据
            SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);
    
            //获得spuId
            Long spuId = skuInfoEntity.getSpuId();
    
            //再通过spuId查询spuInfo信息表里的数据
            SpuInfoEntity spuInfoEntity = this.baseMapper.selectById(spuId);
    
            //查询品牌表的数据获取品牌名
            BrandEntity brandEntity = brandService.getById(spuInfoEntity.getBrandId());
            spuInfoEntity.setBrandName(brandEntity.getName());
    
            return spuInfoEntity;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    3、计算价格和积分信息

    
        /**
         * 计算价格的方法
         * @param orderEntity
         * @param orderItemEntities
         */
        private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> orderItemEntities) {
    
            //总价
            BigDecimal total = new BigDecimal("0.0");
            //优惠价
            BigDecimal coupon = new BigDecimal("0.0");
            BigDecimal intergration = new BigDecimal("0.0");
            BigDecimal promotion = new BigDecimal("0.0");
    
            //积分、成长值
            Integer integrationTotal = 0;
            Integer growthTotal = 0;
    
            //订单总额,叠加每一个订单项的总额信息
            for (OrderItemEntity orderItem : orderItemEntities) {
                //优惠价格信息
                coupon = coupon.add(orderItem.getCouponAmount());
                promotion = promotion.add(orderItem.getPromotionAmount());
                intergration = intergration.add(orderItem.getIntegrationAmount());
    
                //总价
                total = total.add(orderItem.getRealAmount());
    
                //积分信息和成长值信息
                integrationTotal += orderItem.getGiftIntegration();
                growthTotal += orderItem.getGiftGrowth();
    
            }
            //1、订单价格相关的
            orderEntity.setTotalAmount(total);
            //设置应付总额(总额+运费)
            orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
            orderEntity.setCouponAmount(coupon);
            orderEntity.setPromotionAmount(promotion);
            orderEntity.setIntegrationAmount(intergration);
    
            //设置积分成长值信息
            orderEntity.setIntegration(integrationTotal);
            orderEntity.setGrowth(growthTotal);
    
            //设置删除状态(0-未删除,1-已删除)
            orderEntity.setDeleteStatus(0);
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    接着回到提交订单的方法
    验证提交订单的价格和最新价格一致再保存订单,否则设置订单响应错误状态码

    			//2、验证价格
                BigDecimal payAmount = order.getOrder().getPayAmount();
                BigDecimal payPrice = vo.getPayPrice();
    
                if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
                    //金额对比
                    //TODO 3、保存订单
                    saveOrder(order);
                 else {
                    responseVo.setCode(2);
                    return responseVo;
                }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    
        /**
         * 保存订单所有数据
         * @param orderCreateTo
         */
        private void saveOrder(OrderCreateTo orderCreateTo) {
    
            //获取订单信息
            OrderEntity order = orderCreateTo.getOrder();
            order.setModifyTime(new Date());
            order.setCreateTime(new Date());
            //保存订单
            this.baseMapper.insert(order);
    
            //获取订单项信息
            List<OrderItemEntity> orderItems = orderCreateTo.getOrderItems();
            //批量保存订单项数据
            orderItemService.saveBatch(orderItems);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    接着是锁定库存

    锁定库存的Vo

    package com.xxxx.gulimall.order.vo;
    
    import lombok.Data;
    
    import java.util.List;
    
    
    
    @Data
    public class WareSkuLockVo {
    
        private String orderSn;
    
        /** 需要锁住的所有库存信息 **/
        private List<OrderItemVo> locks;
    
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    重新封装要锁定的商品信息,调用远程服务锁定库存

    				//4、库存锁定,只要有异常,回滚订单数据
                    //订单号、所有订单项信息(skuId,skuNum,skuName)
                    WareSkuLockVo lockVo = new WareSkuLockVo();
                    lockVo.setOrderSn(order.getOrder().getOrderSn());
    
                    //获取出要锁定的商品数据信息
                    List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
                        OrderItemVo orderItemVo = new OrderItemVo();
                        orderItemVo.setSkuId(item.getSkuId());
                        orderItemVo.setCount(item.getSkuQuantity());
                        orderItemVo.setTitle(item.getSkuName());
                        return orderItemVo;
                    }).collect(Collectors.toList());
                    lockVo.setLocks(orderItemVos);
    
                    //TODO 调用远程锁定库存的方法
                    //出现的问题:扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚,库存事务不回滚(解决方案:seata)
                    //为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率,可以发消息给库存服务
                    R r = wmsFeignService.orderLockStock(lockVo);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    远程接口

    @FeignClient("gulimall-ware")
    public interface WmsFeignService {
        /**
         * 锁定库存
         * @param vo
         * @return
         */
        @PostMapping(value = "/ware/waresku/lock/order")
        R orderLockStock(@RequestBody WareSkuLockVo vo);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    尝试去锁定库存,锁定失败就返回一个错误信息

    
        /**
         * 锁定库存
         * @param vo
         *
         * 库存解锁的场景
         *      1)、下订单成功,订单过期没有支付被系统自动取消或者被用户手动取消,都要解锁库存
         *      2)、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
         *      3)、
         *
         * @return
         */
        @PostMapping(value = "/lock/order")
        public R orderLockStock(@RequestBody WareSkuLockVo vo) {
    
            try {
                boolean lockStock = wareSkuService.orderLockStock(vo);
                return R.ok().setData(lockStock);
            } catch (NoStockException e) {
                return R.error(NO_STOCK_EXCEPTION.getCode(),NO_STOCK_EXCEPTION.getMessage());
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    实现类

    首先保存库存单信息

    
            /**
             * 保存库存工作单详情信息
             * 追溯
             */
            WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity();
            wareOrderTaskEntity.setOrderSn(vo.getOrderSn());
            wareOrderTaskEntity.setCreateTime(new Date());
            wareOrderTaskService.save(wareOrderTaskEntity);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    查找商品库存信息,找到每个商品在哪个仓库都有库存

     //1、按照下单的收货地址,找到一个就近仓库,锁定库存
            //2、找到每个商品在哪个仓库都有库存
            List<OrderItemVo> locks = vo.getLocks();
    
            List<SkuWareHasStock> collect = locks.stream().map((item) -> {
                SkuWareHasStock stock = new SkuWareHasStock();
                Long skuId = item.getSkuId();
                stock.setSkuId(skuId);
                stock.setNum(item.getCount());
                //查询这个商品在哪个仓库有库存
                List<Long> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId);
                stock.setWareId(wareIdList);
    
                return stock;
            }).collect(Collectors.toList());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    锁定库存时,没有任何仓库有这个商品的库存就抛出一个自定义没有库存的异常

    		//2、锁定库存
            for (SkuWareHasStock hasStock : collect) {
                boolean skuStocked = false;
                Long skuId = hasStock.getSkuId();
                List<Long> wareIds = hasStock.getWareId();
    
                if (org.springframework.util.StringUtils.isEmpty(wareIds)) {
                    //没有任何仓库有这个商品的库存
                    throw new NoStockException(skuId);
                }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    package com.xxxx.common.exception;
    
    import lombok.Getter;
    import lombok.Setter;
    
    
    
    public class NoStockException extends RuntimeException {
    
        @Getter @Setter
        private Long skuId;
    
        public NoStockException(Long skuId) {
            super("商品id:"+ skuId + "库存不足!");
        }
    
        public NoStockException(String msg) {
            super(msg);
        }
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    而锁定库存操作成功,也就是锁定库存的影响行数为1。则保存锁定库存信息和锁定库存详细信息到库存工作单详情当中
    接着告诉MQ库存锁定成功

    			//1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
                //2、锁定失败。前面保存的工作单信息都回滚了。发送出去的消息,即使要解锁库存,由于在数据库查不到指定的id,所有就不用解锁
                for (Long wareId : wareIds) {
                    //锁定成功就返回1,失败就返回0
                    Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
                    if (count == 1) {
                        skuStocked = true;
                        WareOrderTaskDetailEntity taskDetailEntity = WareOrderTaskDetailEntity.builder()
                                .skuId(skuId)
                                .skuName("")
                                .skuNum(hasStock.getNum())
                                .taskId(wareOrderTaskEntity.getId())
                                .wareId(wareId)
                                .lockStatus(1)
                                .build();
                        wareOrderTaskDetailService.save(taskDetailEntity);
                        //TODO 告诉MQ库存锁定成功
                        StockLockedTo lockedTo = new StockLockedTo();
                        lockedTo.setId(wareOrderTaskEntity.getId());
                        StockDetailTo detailTo = new StockDetailTo();
                        BeanUtils.copyProperties(taskDetailEntity,detailTo);
                        lockedTo.setDetailTo(detailTo);
                        rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
                        break;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    sql

    	<update id="lockSkuStock">
            UPDATE wms_ware_sku
            SET stock_locked = stock_locked + #{num}
            WHERE
                sku_id = #{skuId}
              AND ware_id = #{wareId}
              AND stock - stock_locked > 0
        </update>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里为什么要使用MQ?

    假使因为网络问题导致库存在提交订单时扣除成功,但是订单取消……

    所以,这里使用延时队列来实现订单库存信息的锁定

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

    在这里插入图片描述

    结合死信路由

    在这里插入图片描述

    但是结合一个服务一个交换机的设计原则,延时队列过期,应当返回给交换机,交换机再转给死信队列即可。
    在这里插入图片描述
    创建上述队列和路由组件

    在这里插入图片描述
    死信队列
    需要特殊设置死信路由key和交换机和ttl

        /* 容器中的Queue、Exchange、Binding 会自动创建(在RabbitMQ)不存在的情况下 */
    
        /**
         * 死信队列
         *
         * @return
         */
        @Bean
        public Queue orderDelayQueue() {
            /*
                Queue(String name,  队列名字
                boolean durable,  是否持久化
                boolean exclusive,  是否排他
                boolean autoDelete, 是否自动删除
                Map arguments) 属性
             */
            HashMap<String, Object> arguments = new HashMap<>();
            arguments.put("x-dead-letter-exchange", "order-event-exchange");
            arguments.put("x-dead-letter-routing-key", "order.release.order");
            arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
            Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
    
            return queue;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    普通队列

    
        /**
         * 普通队列处理订单
         *
         * @return
         */
        @Bean
        public Queue orderReleaseQueue() {
    
            Queue queue = new Queue("order.release.order.queue", true, false, false);
    
            return queue;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    创建路由

    
        /**
         * TopicExchange
         *
         * @return
         */
        @Bean
        public Exchange orderEventExchange() {
            /*
             *   String name,
             *   boolean durable,
             *   boolean autoDelete,
             *   Map arguments
             * */
            return new TopicExchange("order-event-exchange", true, false);
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    三段绑定关系

    @Bean
        public Binding orderCreateBinding() {
            /*
             * String destination, 目的地(队列名或者交换机名字)
             * DestinationType destinationType, 目的地类型(Queue、Exhcange)
             * String exchange,
             * String routingKey,
             * Map arguments
             * */
            return new Binding("order.delay.queue",
                    Binding.DestinationType.QUEUE,
                    "order-event-exchange",
                    "order.create.order",
                    null);
        }
    
        @Bean
        public Binding orderReleaseBinding() {
    
            return new Binding("order.release.order.queue",
                    Binding.DestinationType.QUEUE,
                    "order-event-exchange",
                    "order.release.order",
                    null);
        }
    
        /**
         * 订单释放直接和库存释放进行绑定
         * @return
         */
        @Bean
        public Binding orderReleaseOtherBinding() {
    
            return new Binding("stock.release.stock.queue",
                    Binding.DestinationType.QUEUE,
                    "order-event-exchange",
                    "order.release.other.#",
                    null);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    最后实现

    package com.xxxx.gulimall.order.config;
    
    import org.springframework.amqp.core.Binding;
    import org.springframework.amqp.core.Exchange;
    import org.springframework.amqp.core.Queue;
    import org.springframework.amqp.core.TopicExchange;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.HashMap;
    
    
    
    
    @Configuration
    public class MyRabbitMQConfig {
    
        /* 容器中的Queue、Exchange、Binding 会自动创建(在RabbitMQ)不存在的情况下 */
    
        /**
         * 死信队列
         *
         * @return
         */
        @Bean
        public Queue orderDelayQueue() {
            /*
                Queue(String name,  队列名字
                boolean durable,  是否持久化
                boolean exclusive,  是否排他
                boolean autoDelete, 是否自动删除
                Map arguments) 属性
             */
            HashMap<String, Object> arguments = new HashMap<>();
            arguments.put("x-dead-letter-exchange", "order-event-exchange");
            arguments.put("x-dead-letter-routing-key", "order.release.order");
            arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
            Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
    
            return queue;
        }
    
        /**
         * 普通队列处理订单
         *
         * @return
         */
        @Bean
        public Queue orderReleaseQueue() {
    
            Queue queue = new Queue("order.release.order.queue", true, false, false);
    
            return queue;
        }
    
        /**
         * TopicExchange
         *
         * @return
         */
        @Bean
        public Exchange orderEventExchange() {
            /*
             *   String name,
             *   boolean durable,
             *   boolean autoDelete,
             *   Map arguments
             * */
            return new TopicExchange("order-event-exchange", true, false);
    
        }
    
    
        @Bean
        public Binding orderCreateBinding() {
            /*
             * String destination, 目的地(队列名或者交换机名字)
             * DestinationType destinationType, 目的地类型(Queue、Exhcange)
             * String exchange,
             * String routingKey,
             * Map arguments
             * */
            return new Binding("order.delay.queue",
                    Binding.DestinationType.QUEUE,
                    "order-event-exchange",
                    "order.create.order",
                    null);
        }
    
        @Bean
        public Binding orderReleaseBinding() {
    
            return new Binding("order.release.order.queue",
                    Binding.DestinationType.QUEUE,
                    "order-event-exchange",
                    "order.release.order",
                    null);
        }
    
        /**
         * 订单释放直接和库存释放进行绑定
         * @return
         */
        @Bean
        public Binding orderReleaseOtherBinding() {
    
            return new Binding("stock.release.stock.queue",
                    Binding.DestinationType.QUEUE,
                    "order-event-exchange",
                    "order.release.other.#",
                    null);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114

    最终效果

    
        /**
         * 为某个订单锁定库存
         * @param vo
         * @return
         */
        @Transactional
        @Override
        public boolean orderLockStock(WareSkuLockVo vo) {
    
            /**
             * 保存库存工作单详情信息
             * 追溯
             */
            WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity();
            wareOrderTaskEntity.setOrderSn(vo.getOrderSn());
            wareOrderTaskEntity.setCreateTime(new Date());
            wareOrderTaskService.save(wareOrderTaskEntity);
    
    
            //1、按照下单的收货地址,找到一个就近仓库,锁定库存
            //2、找到每个商品在哪个仓库都有库存
            List<OrderItemVo> locks = vo.getLocks();
    
            List<SkuWareHasStock> collect = locks.stream().map((item) -> {
                SkuWareHasStock stock = new SkuWareHasStock();
                Long skuId = item.getSkuId();
                stock.setSkuId(skuId);
                stock.setNum(item.getCount());
                //查询这个商品在哪个仓库有库存
                List<Long> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId);
                stock.setWareId(wareIdList);
    
                return stock;
            }).collect(Collectors.toList());
    
            //2、锁定库存
            for (SkuWareHasStock hasStock : collect) {
                boolean skuStocked = false;
                Long skuId = hasStock.getSkuId();
                List<Long> wareIds = hasStock.getWareId();
    
                if (org.springframework.util.StringUtils.isEmpty(wareIds)) {
                    //没有任何仓库有这个商品的库存
                    throw new NoStockException(skuId);
                }
    
                //1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
                //2、锁定失败。前面保存的工作单信息都回滚了。发送出去的消息,即使要解锁库存,由于在数据库查不到指定的id,所有就不用解锁
                for (Long wareId : wareIds) {
                    //锁定成功就返回1,失败就返回0
                    Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
                    if (count == 1) {
                        skuStocked = true;
                        WareOrderTaskDetailEntity taskDetailEntity = WareOrderTaskDetailEntity.builder()
                                .skuId(skuId)
                                .skuName("")
                                .skuNum(hasStock.getNum())
                                .taskId(wareOrderTaskEntity.getId())
                                .wareId(wareId)
                                .lockStatus(1)
                                .build();
                        wareOrderTaskDetailService.save(taskDetailEntity);
    
                        //TODO 告诉MQ库存锁定成功
                        StockLockedTo lockedTo = new StockLockedTo();
                        lockedTo.setId(wareOrderTaskEntity.getId());
                        StockDetailTo detailTo = new StockDetailTo();
                        BeanUtils.copyProperties(taskDetailEntity,detailTo);
                        lockedTo.setDetailTo(detailTo);
                        rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
                        break;
                    } else {
                        //当前仓库锁失败,重试下一个仓库
                    }
                }
    
                if (skuStocked == false) {
                    //当前商品所有仓库都没有锁住
                    throw new NoStockException(skuId);
                }
            }
    
            //3、肯定全部都是锁定成功的
            return true;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86

    解锁库存

    依赖

    		<!--amqp高级消息队列协议,rabbitmq实现-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-amqp</artifactId>
            </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    主启动类添加注解

    在这里插入图片描述
    配置

    spring:
      rabbitmq:
        host: 192.168.75.129
        port: 5672
        # 虚拟主机
        virtual-host: /
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    首先新建mq组件
    在这里插入图片描述

    package com.xxxx.gulimall.ware.config;
    
    import org.springframework.amqp.core.Binding;
    import org.springframework.amqp.core.Exchange;
    import org.springframework.amqp.core.Queue;
    import org.springframework.amqp.core.TopicExchange;
    import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
    import org.springframework.amqp.support.converter.MessageConverter;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.HashMap;
    
    
    
    @Configuration
    public class MyRabbitMQConfig {
    
        /**
         * 使用JSON序列化机制,进行消息转换
         * @return
         */
        @Bean
        public MessageConverter messageConverter() {
            return new Jackson2JsonMessageConverter();
        }
    
        // @RabbitListener(queues = "stock.release.stock.queue")
        // public void handle(Message message) {
        //
        // }
    
        /**
         * 库存服务默认的交换机
         * @return
         */
        @Bean
        public Exchange stockEventExchange() {
            //String name, boolean durable, boolean autoDelete, Map arguments
            TopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false);
            return topicExchange;
        }
    
        /**
         * 普通队列
         * @return
         */
        @Bean
        public Queue stockReleaseStockQueue() {
            //String name, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
            Queue queue = new Queue("stock.release.stock.queue", true, false, false);
            return queue;
        }
    
    
        /**
         * 延迟队列
         * @return
         */
        @Bean
        public Queue stockDelay() {
    
            HashMap<String, Object> arguments = new HashMap<>();
            arguments.put("x-dead-letter-exchange", "stock-event-exchange");
            arguments.put("x-dead-letter-routing-key", "stock.release");
            // 消息过期时间 2分钟
            arguments.put("x-message-ttl", 120000);
    
            Queue queue = new Queue("stock.delay.queue", true, false, false,arguments);
            return queue;
        }
    
    
        /**
         * 交换机与普通队列绑定
         * @return
         */
        @Bean
        public Binding stockLocked() {
            //String destination, DestinationType destinationType, String exchange, String routingKey,
            // 			Map arguments
            Binding binding = new Binding("stock.release.stock.queue",
                    Binding.DestinationType.QUEUE,
                    "stock-event-exchange",
                    "stock.release.#",
                    null);
    
            return binding;
        }
    
    
        /**
         * 交换机与延迟队列绑定
         * @return
         */
        @Bean
        public Binding stockLockedBinding() {
            return new Binding("stock.delay.queue",
                    Binding.DestinationType.QUEUE,
                    "stock-event-exchange",
                    "stock.locked",
                    null);
        }
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107

    解锁库存的两种情况

    写个监听器监听锁定库存时存入锁定库存信息的队列
    **加粗样式**

    第一种是业务失败,订单提交了,但是后续业务处理失败,解锁库存。
    第二种是订单取消了,解锁库存

    package com.xxxx.gulimall.ware.listener;
    
    import com.rabbitmq.client.Channel;
    import com.xxxx.common.to.OrderTo;
    import com.xxxx.common.to.mq.StockLockedTo;
    import com.xxxx.gulimall.ware.service.WareSkuService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.rabbit.annotation.RabbitHandler;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.io.IOException;
    
    
    
    @Slf4j
    @RabbitListener(queues = "stock.release.stock.queue")
    @Service
    public class StockReleaseListener {
    
        @Autowired
        private WareSkuService wareSkuService;
    
        /**
         * 1、库存自动解锁
         *  下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
         *
         *  2、订单失败
         *      库存锁定失败
         *
         *   只要解锁库存的消息失败,一定要告诉服务解锁失败
         */
        @RabbitHandler
        public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
            log.info("******收到解锁库存的信息******");
            try {
    
                //当前消息是否被第二次及以后(重新)派发过来了
                // Boolean redelivered = message.getMessageProperties().getRedelivered();
    
                //解锁库存
                wareSkuService.unlockStock(to);
                // 手动删除消息
                channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            } catch (Exception e) {
                // 解锁失败 将消息重新放回队列,让别人消费
                channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
            }
        }
    
        @RabbitHandler
        public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
    
            log.info("******收到订单关闭,准备解锁库存的信息******");
    
            try {
                wareSkuService.unlockStock(orderTo);
                // 手动删除消息
                channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            } catch (Exception e) {
                // 解锁失败 将消息重新放回队列,让别人消费
                channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
            }
        }
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    1、当订单业务提交后回滚:

    需要远程查询订单状态再调用解锁方法

    远程接口

    package com.xxxx.gulimall.ware.feign;
    
    import com.xxxx.common.utils.R;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    
    
    @FeignClient("gulimall-order")
    public interface OrderFeignService {
    
        @GetMapping(value = "/order/order/status/{orderSn}")
        R getOrderStatus(@PathVariable("orderSn") String orderSn);
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    
        /**
         * 根据订单编号查询订单状态
         * @param orderSn
         * @return
         */
        @GetMapping(value = "/status/{orderSn}")
        public R getOrderStatus(@PathVariable("orderSn") String orderSn) {
            OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);
            return R.ok().setData(orderEntity);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
        /**
         * 根据订单编号查询订单状态
         * @param orderSn
         * @return
         */
        @GetMapping(value = "/status/{orderSn}")
        public R getOrderStatus(@PathVariable("orderSn") String orderSn) {
            OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);
            return R.ok().setData(orderEntity);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    
        /**
         * 按照订单号获取订单信息
         * @param orderSn
         * @return
         */
        @Override
        public OrderEntity getOrderByOrderSn(String orderSn) {
    
            OrderEntity orderEntity = this.baseMapper.selectOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
    
            return orderEntity;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    最终效果

    	@Override
        public void unlockStock(StockLockedTo to) {
            //库存工作单的id
            StockDetailTo detail = to.getDetailTo();
            Long detailId = detail.getId();
    
            /**
             * 解锁
             * 1、查询数据库关于这个订单锁定库存信息
             *   有:证明库存锁定成功了
             *      解锁:订单状况
             *          1、没有这个订单,必须解锁库存
             *          2、有这个订单,不一定解锁库存
             *              订单状态:已取消:解锁库存
             *                      已支付:不能解锁库存
             */
            WareOrderTaskDetailEntity taskDetailInfo = wareOrderTaskDetailService.getById(detailId);
            if (taskDetailInfo != null) {
                //查出wms_ware_order_task工作单的信息
                Long id = to.getId();
                WareOrderTaskEntity orderTaskInfo = wareOrderTaskService.getById(id);
                //获取订单号查询订单状态
                String orderSn = orderTaskInfo.getOrderSn();
                //远程查询订单信息
                R orderData = orderFeignService.getOrderStatus(orderSn);
                if (orderData.getCode() == 0) {
                    //订单数据返回成功
                    OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() {});
    
                    //判断订单状态是否已取消或者支付或者订单不存在
                    if (orderInfo == null || orderInfo.getStatus() == 4) {
                        //订单已被取消,才能解锁库存
                        if (taskDetailInfo.getLockStatus() == 1) {
                            //当前库存工作单详情状态1,已锁定,但是未解锁才可以解锁
                            unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
                        }
                    }
                } else {
                    //消息拒绝以后重新放在队列里面,让别人继续消费解锁
                    //远程调用服务失败
                    throw new RuntimeException("远程调用服务失败");
                }
            } else {
                //无需解锁
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    2、订单取消解锁库存

    实现类

    
        /**
         * 防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理
         * 导致卡顿的订单,永远都不能解锁库存
         * @param orderTo
         */
        @Transactional(rollbackFor = Exception.class)
        @Override
        public void unlockStock(OrderTo orderTo) {
    
            String orderSn = orderTo.getOrderSn();
            //查一下最新的库存解锁状态,防止重复解锁库存
            WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
    
            //按照工作单的id找到所有 没有解锁的库存,进行解锁
            Long id = orderTaskEntity.getId();
            List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
                    .eq("task_id", id).eq("lock_status", 1));
    
            for (WareOrderTaskDetailEntity taskDetailEntity : list) {
                unLockStock(taskDetailEntity.getSkuId(),
                        taskDetailEntity.getWareId(),
                        taskDetailEntity.getSkuNum(),
                        taskDetailEntity.getId());
            }
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    最后是这两个解锁库存方式都调用的解锁方法

    
        /**
         * 解锁库存的方法
         * @param skuId
         * @param wareId
         * @param num
         * @param taskDetailId
         */
        public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {
    
            //库存解锁
            wareSkuDao.unLockStock(skuId,wareId,num);
    
            //更新工作单的状态
            WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
            taskDetailEntity.setId(taskDetailId);
            //变为已解锁
            taskDetailEntity.setLockStatus(2);
            wareOrderTaskDetailService.updateById(taskDetailEntity);
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    sql

    	<update id="unLockStock">
            UPDATE wms_ware_sku
            SET stock_locked = stock_locked - #{num}
            WHERE
                sku_id = ${skuId}
              AND ware_id = #{wareId}
        </update>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    关闭订单

    回到提交订单的业务
    当提交订单时库存锁定成功,给死信队列发消息,开始执行关闭订单的业务,删除购物车数据

    				if (r.getCode() == 0) {
                        //锁定成功
                        responseVo.setOrder(order.getOrder());
                        // int i = 10/0;
    
                        //TODO 订单创建成功,发送消息给MQ
                        rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
    
                        //删除购物车里的数据
                        redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());
                        return responseVo;
                    } else {
                        //锁定失败
                        String msg = (String) r.get("msg");
                        throw new NoStockException(msg);
                        // responseVo.setCode(3);
                        // return responseVo;
                    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    监听普通队列的消息。当普通队列中监听到消息,说明死信队列订单过期,返还给普通队列来处理关闭订单的消息。
    在这里插入图片描述

    package com.xxxx.gulimall.order.listener;
    
    import com.rabbitmq.client.Channel;
    import com.xxxx.gulimall.order.entity.OrderEntity;
    import com.xxxx.gulimall.order.service.OrderService;
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.rabbit.annotation.RabbitHandler;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.io.IOException;
    
    /**
     * @Description: 定时关闭订单
     *
     **/
    
    @RabbitListener(queues = "order.release.order.queue")
    @Service
    public class OrderCloseListener {
    
        @Autowired
        private OrderService orderService;
    
        @RabbitHandler
        public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
            System.out.println("收到过期的订单信息,准备关闭订单" + orderEntity.getOrderSn());
            try {
                orderService.closeOrder(orderEntity);
                channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            } catch (Exception e) {
                channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
            }
    
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    关闭订单
    订单关闭时发送消息给库存服务的普通队列,让库存服务解锁库存。

    
        /**
         * 关闭订单
         * @param orderEntity
         */
        @Override
        public void closeOrder(OrderEntity orderEntity) {
    
            //关闭订单之前先查询一下数据库,判断此订单状态是否已支付
            OrderEntity orderInfo = this.getOne(new QueryWrapper<OrderEntity>().
                    eq("order_sn",orderEntity.getOrderSn()));
    
            if (orderInfo.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())) {
                //代付款状态进行关单
                OrderEntity orderUpdate = new OrderEntity();
                orderUpdate.setId(orderInfo.getId());
                orderUpdate.setStatus(OrderStatusEnum.CANCLED.getCode());
                this.updateById(orderUpdate);
    
                // 发送消息给MQ
                OrderTo orderTo = new OrderTo();
                BeanUtils.copyProperties(orderInfo, orderTo);
    
                try {
                    //TODO 确保每个消息发送成功,给每个消息做好日志记录,(给数据库保存每一个详细信息)保存每个消息的详细信息
                    rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
                } catch (Exception e) {
                    //TODO 定期扫描数据库,重新发送失败的消息
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

  • 相关阅读:
    《 Python List 列表全实例详解系列(六)》__查找元素
    ERP系统是如何运作的?erp管理系统操作流程
    理解树状数组这一篇文章就够啦
    C++ 数据结构探索:构建高效程序的基础
    FTP和nfs 网络共享存储
    mysql转达梦的python脚本
    设计模式学习(二十一):命令模式
    [UEFI]EFI_DEVICE_PATH_PROTOCOL 结构体初始化的一个例子
    最小生成树
    MybatisPlus搭建项目
  • 原文地址:https://blog.csdn.net/qq_44737138/article/details/126807126