26.订单模块

环境搭建

1.整理前端页面以及静态资源环境

2.配置域名

 3.nginx配置 静态资源去 /usr/share/nginx/html里找,动态资源去gulimall,并且由于nginx会丢失host配置Host

[root@wuyimin conf.d]# cat gulimall.conf 
server {
    listen       80;
    server_name gulimall.com *.gulimall.com;


    location /static {
        root   /usr/share/nginx/html;
    }

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;
    location / {
        proxy_pass http://gulimall;
        proxy_set_header Host $host;
    }
}

这里主要配置upstream(上游), 配置代理服务器为网关模块

[root@wuyimin conf]# cat nginx.conf 

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;
    upstream gulimall{
     server 192.168.10.100:88;
	}
    include /etc/nginx/conf.d/*.conf;
}

4.网关配置

 5.themeleaf模板引擎依赖

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

关闭缓存

6.测试页面

@Controller
public class HelloController {
    @GetMapping("/{page}.html")
    public String listPage(@PathVariable("page")String page){
        return page;
    }
}

整合SpringSession

1.导入依赖

 <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

2.配置文件

 3.配置类

package com.wuyimin.gulimall.order.config;

/**
 * @ Author wuyimin
 * @ Date 2021/8/23-14:50
 * @ Description
 */
@Configuration
public class RedisSessionConfig {
    @Bean // redis的json序列化
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

    @Bean // cookie
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("GULISESSIONID"); // cookie的键
        serializer.setDomainName("gulimall.com"); // 扩大session作用域,也就是cookie的有效域
        return serializer;
    }
}

4.线程池配置

配置类

package com.wuyimin.gulimall.order.config;

/**
 * @ Author wuyimin
 * @ Date 2021/8/20-18:39
 * @ Description
 */

@Configuration
public class MyThreadConfig {
    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
        return new ThreadPoolExecutor(pool.getCoreSize(),pool.getMaxSize(),pool.getKeepAliveTime(), TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(100000), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
    }
}
package com.wuyimin.gulimall.order.config;


/**
 * @ Author wuyimin
 * @ Date 2021/8/20-18:42
 * @ Description
 */
@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
    private Integer coreSize;
    private Integer maxSize;
    private Integer keepAliveTime;
}

线程池配置文件

#配置线程池
gulimall:
  thread:
    core-size: 20
    max-size: 200
    keep-alive-time: 10

5.RabbitMQ配置--》见上一篇博客

6.配置redis

依赖

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

配置文件

spring:
  redis:
    host: 192.168.116.128

7.启用redisSession

package com.wuyimin.gulimall.order;

@EnableDiscoveryClient
@SpringBootApplication
@EnableRabbit//开启RabbitMQ
@EnableRedisHttpSession
public class GulimallOrderApplication {

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

}

订单的基本概念

电商系统设计信息流,资金流,物流,订单系统作为中枢将三者有机结合起来

订单应该包含的信息:用户信息,订单信息,商品信息,促销信息,支付信息,物流信息

订单的状态应该包含:

  • 待付款状态:(对库存进行锁定,配置支付超时时间,超时后自动取消订单)
  • 已付款状态/待发货:(联动仓库进行调拨,配货。。)
  • 待收货/已发货:(同步物流信息)
  • 已完成:(用户确认收货,订单交易完成)
  • 已取消:(用户在付款之前都可以取消订单)
  • 售后中:(用户在付款后申请退款,或者商家发货之后会用户申请退换货)
  • 售后也同时存在多种状态:发起售后后,订单状态为待审核,商家审核完毕后,跟新到待退货,等待用户寄回,商家收货后订单状态更新为待退款,退款到用户账户之后订单状态更新为售后成功

订单登录拦截

拦截器配置类--添加一个拦截器

package com.wuyimin.gulimall.order.config;

/**
 * @ Author wuyimin
 * @ Date 2021/8/26-10:53
 * @ Description
 */
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
    @Autowired
    LoginUserInterceptor loginUserInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");//拦截所有请求
    }
}

拦截器--拦截未登录的用户

package com.wuyimin.gulimall.order.interceptor;

/**
 * @ Author wuyimin
 * @ Date 2021/8/26-10:52
 * @ Description 拦截未登录用户
 */
@Component //放入容器中
public class LoginUserInterceptor implements HandlerInterceptor {
    public static ThreadLocal<MemberRespVo> loginUser=new ThreadLocal<>();//方便其他请求拿到
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();//获取session
        MemberRespVo attribute = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
        if(attribute!=null){
            //已经登录
            loginUser.set(attribute);
            return true;
        }else{
            //给前端用户的提示
            request.getSession().setAttribute("msg","请先进行登录");
            //未登录
            response.sendRedirect("http://auth.gulimall.com/login.html");//重定向到登录页
            return false;
        }
    }
}

前置知识

原文链接1

听说看完这篇就可以和面试官扯cookie,session和token了

Cookie:

Session:

1)浏览器端第一次发送请求到服务器端,服务器端创建一个Session,同时会创建一个特殊的Cookie(name为JSESSIONID的固定值,value为session对象的ID),然后将该Cookie发送至浏览器端
(2)浏览器端发送第N(N>1)次请求到服务器端,浏览器端访问服务器端时就会携带该name为JSESSIONID的Cookie对象
(3)服务器端根据name为JSESSIONID的Cookie的value(sessionId),去查询Session对象,从而区分不同用户。
name为JSESSIONID的Cookie不存在(关闭或更换浏览器),返回1中重新去创建Session与特殊的Cookie
name为JSESSIONID的Cookie存在,根据value中的SessionId去寻找session对象
value为SessionId不存在**(Session对象默认存活30分钟)**,返回1中重新去创建Session与特殊的Cookie
value为SessionId存在,返回session对象

Cookie和Session的区别:

(1)cookie数据存放在客户的浏览器上,session数据放在服务器上
(2)cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,如果主要考虑到安全应当使用session
(3)session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,如果主要考虑到减轻服务器性能方面,应当使用COOKIE
(4)单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能3K。
(5)所以:将登陆信息等重要信息存放为SESSION;其他信息如果需要保留,可以放在COOKIE中

重定向与转发的区别:原文链接2

地址栏
转发:不变,不会显示出转向的地址
重定向:会显示转向之后的地址
请求
重定向:至少提交了两次请求
数据
转发:对request对象的信息不会丢失,因此可以在多个页面交互过程中实现请求数据的共享
重定向:request信息将丢失
原理
转发(服务器行为):是在服务器内部控制权的转移,是由服务器区请求,客户端并不知道是怎样转移的,因此客户端浏览器的地址不会显示出转向的地址。
重定向(浏览器/客户端行为):是服务器告诉了客户端要转向哪个地址,客户端再自己去请求转向的地址,因此会显示转向后的地址,也可以理解浏览器至少进行了两次的访问请求。

订单确认页模型抽取

package com.wuyimin.gulimall.order.vo;

/**
 * @ Author wuyimin
 * @ Date 2021/8/26-11:47
 * @ Description 订单确认页需要的数据
 */

public class OrderConfirmVo { // 跳转到确认页时需要携带的数据模型。
    @Getter
    @Setter
    /** 会员收获地址列表 **/
    private List<MemberAddressVo> memberAddressVos;

    @Getter @Setter
    /** 所有选中的购物项 **/
    private 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();
    }
}
package com.wuyimin.gulimall.order.vo;
/**
 * @ Author wuyimin
 * @ Date 2021/8/26-11:48
 * @ Description 会员收货地址
 */
@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;
}
package com.wuyimin.gulimall.order.vo;
@Data
public class OrderItemVo {
    private Long skuId;
    private String title;
    private String image;
    private List<String> skuAttr;//套餐信息
    private BigDecimal price;
    private Integer count;
    private BigDecimal totalPrice;
}

controller

package com.wuyimin.gulimall.order.web;

/**
 * @ Author wuyimin
 * @ Date 2021/8/26-10:46
 * @ Description
 */
@Controller
public class OrderWebController {
    @Autowired
    OrderService orderService;
    //跳到订单确认列
    @GetMapping("/toTrade")
    public String toTrade(Model model){
        OrderConfirmVo confirmVo=orderService.confirmOrder();
        model.addAttribute("orderConfirmData",confirmVo);
        return "confirm";
    }
    
}

订单确认页数据获取

1.获取用户地址(远程调用)记得在order主类上开启远程调用注解

远程调用的member的controller, 这里偷懒懒得写进impl里了

package com.wuyimin.gulimall.member.controller;

@GetMapping("/{memberId}/address")
    public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long id){
        List<MemberReceiveAddressEntity> list = memberReceiveAddressService.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", id));
        return list;
    }

远程调用接口

package com.wuyimin.gulimall.order.feign;

/**
 * @ Author wuyimin
 * @ Date 2021/8/26-12:06
 * @ Description
 */
@FeignClient("gulimall-member")
public interface MemberFeignService {
    @GetMapping("/member/memberreceiveaddress/{memberId}/address")
    List<MemberAddressVo> getAddress(@PathVariable("memberId") Long id);
}

2.获取购物项(远程调用)

package com.wuyimin.gulimall.order.feign;

/**
 * @ Author wuyimin
 * @ Date 2021/8/26-13:03
 * @ Description
 */
@FeignClient("gulimall-cart")
public interface CartFeignService {
    @GetMapping("/currentUserCartItems")
    List<OrderItemVo> getCurrentUserCartItems();
}

 由于这里类上我们没有写restController注解,所以在方法上写ResponseBody来传递json

package com.wuyimin.gulimall.cart.controller; 
    @GetMapping("/currentUserCartItems")
    @ResponseBody
    public List<CartItem> getCurrentUserCartItems(){
        return cartService.getUserCartItems();
    }
package com.wuyimin.gulimall.cart.service.impl; 
@Override
    public List<CartItem> getUserCartItems() {
        //获取用户信息
        UserInfoVo userInfoVo = CartInterceptor.threadLocal.get();
        if(userInfoVo.getUserId()==null){
            return null;
        }else{
            String loginKey = CartConstant.CART_PREFIX + userInfoVo.getUserId();
            List<CartItem> cartItems = getCartItems(loginKey);
            //过滤掉没有勾选的信息
            List<CartItem> cartItemList = cartItems.stream().filter(i -> i.getCheck()).collect(Collectors.toList());
            //所有勾选的项目的id远程调用所需
            List<Long> skuIds = cartItemList.stream().map(CartItem::getSkuId).collect(Collectors.toList());
            //远程调用product服务,查询最新的商品信息
            HashMap<Long, BigDecimal> newPrices = productFeignService.getNewPrices(skuIds);
            for (CartItem cartItem : cartItemList) {
                if(newPrices.containsKey(cartItem.getSkuId())){
                    cartItem.setPrice(newPrices.get(cartItem.getSkuId()));
                }
            }
            return cartItems;
        }
    }

在获取购物项的时候我们需要获取最新的价格信息,这里cart模块又需要远程调用product模块

package com.wuyimin.gulimall.cart.feign;

/**
 * @ Author wuyimin
 * @ Date 2021/8/24-13:15
 * @ Description
 */
@FeignClient("gulimall-product")
public interface ProductFeignService {
    @RequestMapping("/product/skuinfo/info/{skuId}")
    R info(@PathVariable("skuId") Long skuId);
    @GetMapping("/product/skusaleattrvalue/stringlist/{skuId}")
    List<String> getSkuAttrValues(@PathVariable("skuId") Long skuId);
    @RequestMapping("/product/skuinfo/getNewPrices")
    R getNewPrices(@RequestBody Long[] ids);
}
package com.wuyimin.gulimall.product.app;

    
    //获得最新的价格
    @RequestMapping("/getNewPrices")
    public R getNewPrices(@RequestBody Long[] ids){
        List<SkuInfoEntity> skuInfoEntities = skuInfoService.list(new QueryWrapper<SkuInfoEntity>().in("sku_id", ids));
        HashMap<Long, BigDecimal> map = new HashMap<>();
        for (SkuInfoEntity skuInfoEntity : skuInfoEntities) {
            map.put(skuInfoEntity.getSkuId(),skuInfoEntity.getPrice());
        }
        return R.ok().put("data",map);
    }

总方法confirmOrder

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

@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {

    @Override
    public OrderConfirmVo confirmOrder() {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        //从拦截器里获得用户信息
        MemberRespVo loginUser = LoginUserInterceptor.loginUser.get();
        //远程查询用户地址信息
        List<MemberAddressVo> address = memberFeignService.getAddress(loginUser.getId());
        confirmVo.setMemberAddressVos(address);
        //远程查询用户的购物车
        List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
        confirmVo.setItems(currentUserCartItems);
        //查询用户积分
        Integer integration = loginUser.getIntegration();
        confirmVo.setIntegration(integration);
        //其他数据自动计算
        return confirmVo;
    }

}

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

feign远程调用的请求头中没有含有JSESSIONIDcookie,所以也就不能得到服务端的session数据,也就没有用户数据,cart认为没登录,获取不了用户信息

我们追踪远程调用的源码,可以在SynchronousMethodHandler.targetRequest()方法中看到他会遍历容器中的RequestInterceptor进行封装

Request targetRequest(RequestTemplate template) {
  for (RequestInterceptor interceptor : requestInterceptors) {
    interceptor.apply(template);
  }
  return target.apply(template);
}

根据追踪源码,我们可以知道我们可以通过给容器中注入RequestInterceptor,从而给远程调用转发时带上cookie

但是在feign的调用过程中,会使用容器中的RequestInterceptor对RequestTemplate进行处理,因此我们可以通过向容器中导入定制的RequestInterceptor为请求加上cookie。

package com.wuyimin.gulimall.order.config;


/**
 * @ Author wuyimin
 * @ Date 2021/8/26-16:55
 * @ Description
 */
@Configuration
public class OrderFeignConfig {
    //这个拦截器方法会在远程调用之前触发
    @Bean
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                //源码里这个方法就是从threadLocal里拿
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                HttpServletRequest request = requestAttributes.getRequest();//这里获取到的是老请求
                //同步请求头数据--同步cookie
                template.header("Cookie",request.getHeader("Cookie"));//Feign创建的新请求
            }
        };
    }
}

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

在我们增加了异步操作以后,请求头又丢失了

因为异步编排的原因,他会丢掉ThreadLocal中原来线程的数据,从而获取不到loginUser,这种情况下我们可以在方法内的局部变量中先保存原来线程的信息,在异步编排的新线程中拿着局部变量的值重新设置到新线程中即可。

由于RequestContextHolder使用ThreadLocal共享数据,所以在开启异步时获取不到老请求的信息,自然也就无法共享cookie了

 修改配置类代码,增加判断不为空的条件

package com.wuyimin.gulimall.order.config;

/**
 * @ Author wuyimin
 * @ Date 2021/8/26-16:55
 * @ Description
 */
@Configuration
public class OrderFeignConfig {
    //这个拦截器方法会在远程调用之前触发
    @Bean
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                //源码里这个方法就是从threadLocal里拿
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if(requestAttributes!=null){
                    HttpServletRequest request = requestAttributes.getRequest();//这里获取到的是老请求
                    if(request!=null){
                        //同步请求头数据--同步cookie
                        template.header("Cookie",request.getHeader("Cookie"));//Feign创建的新请求
                    }
                }
            }
        };
    }
}

增加主方法异步和设置threadLocal

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

@Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        //从拦截器里获得用户信息
        MemberRespVo loginUser = LoginUserInterceptor.loginUser.get();
        //主线程里先把threadLocal的值取出来,因为下面异步就取不到了
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        CompletableFuture<Void> futureAddress = CompletableFuture.runAsync(() -> {
            //远程查询用户地址信息
            //子线程里拿到父线程的threadLocal里储存的信息
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<MemberAddressVo> address = memberFeignService.getAddress(loginUser.getId());
            confirmVo.setMemberAddressVos(address);
        }, executor);
        CompletableFuture<Void> futureItems = CompletableFuture.runAsync(() -> {
            //远程查询用户的购物车
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(currentUserCartItems);
        }, executor);

        //查询用户积分
        Integer integration = loginUser.getIntegration();
        confirmVo.setIntegration(integration);
        //其他数据自动计算
        //等待异步任务完成
        CompletableFuture.allOf(futureAddress,futureItems).get();
        return confirmVo;
    }

Bug调试-FeignException$MethodNotAllowed

List传参问题

修改后的代码

package com.wuyimin.gulimall.product.app;    
//获得最新的价格
    @RequestMapping("/getNewPrices")
    public R getNewPrices(@RequestBody Long[] ids){
        List<SkuInfoEntity> skuInfoEntities = skuInfoService.list(new QueryWrapper<SkuInfoEntity>().in("sku_id", ids));
        HashMap<Long, BigDecimal> map = new HashMap<>();
        for (SkuInfoEntity skuInfoEntity : skuInfoEntities) {
            map.put(skuInfoEntity.getSkuId(),skuInfoEntity.getPrice());
        }
        return R.ok().put("data",map);
    }

1.SpringCloud中微服务之间的调用,传递参数时需要加相应的注解。用到的主要是三个注解@RequestBody,@RequestParam(),@PathVariable()

2.get和post请求中对于传递单个引用类型的参数,比如String,Integer....用@RequestParam(),括号中一定要有值(参数的别名)。调用方需要加注解,被调用方不需要加。当然加上也不会出错。被调用方的参数名和调用方的别名保持一致即可。

3.post请求中对于javaBean,map,list类型的参数的传递,用@RequestBody,调用方不需要加注解,被调用方加注解即可。

注:get请求中使用@RequestBody会出错,同时也不能传递javaBean,map,list类型的参数
当我们返回json数据的时候必须要加上注解responseBody和restController注解

 库存查询:

远程调用接口:

package com.wuyimin.gulimall.order.feign;

/**
 * @ Author wuyimin
 * @ Date 2021/8/26-20:11
 * @ Description
 */
@FeignClient("gulimall-ware")
public interface WareFeignService {
    @PostMapping("/ware/waresku/hasstock")
    R getSkuHasStock(@RequestBody List<Long> skuIds);//运用之前已经写过的方法,这个方法会返回有库存的商品列表
}

 修改confirmOrder方法

package com.wuyimin.gulimall.order.service.impl;
@Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberRespVo loginUser = LoginUserInterceptor.loginUser.get();
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        CompletableFuture<Void> futureAddress = CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<MemberAddressVo> address = memberFeignService.getAddress(loginUser.getId());
            confirmVo.setMemberAddressVos(address);
        }, executor);
        CompletableFuture<Void> futureItems = CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(currentUserCartItems);
        }, executor).thenRunAsync(()->{
            //获取所有的库存信息
            List<OrderItemVo> items = confirmVo.getItems();
            List<Long> collect=items.stream().map(i->i.getSkuId()).collect(Collectors.toList());
            R r = wareFeignService.getSkuHasStock(collect);
            List<SkuStockVo> data = r.getData(new TypeReference<List<SkuStockVo>>() {
            });
            if(data!=null){
                Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(map);
            }
        },executor);
        Integer integration = loginUser.getIntegration();
        confirmVo.setIntegration(integration);
        CompletableFuture.allOf(futureAddress,futureItems).get();
        return confirmVo;
    }

模拟运费

 package com.wuyimin.gulimall.ware.controller;

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

getFare方法

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

  //根据用户的收货地址计算运费
    @Override
    public FareVo getFare(Long addrId) {
        FareVo fareVo = new FareVo();
        R info = memberFeignService.info(addrId);
        MemberAddressVo addressData = info.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {
        });
        if(addressData!=null){
            //直接拿手机号的最后一位信息当运费信息得了
            String phone = addressData.getPhone();
            String fare = phone.substring(phone.length() - 1, phone.length());
            fareVo.setAddressVo(addressData);
            fareVo.setFare(new BigDecimal(fare));
            return fareVo;
        }
        return null;
    }

在查询运费的时候我们需要拿到用户的地址信息(远程调用会员服务)

package com.wuyimin.gulimall.ware.feign;

/**
 * @ Author wuyimin
 * @ Date 2021/8/27-9:58
 * @ Description
 */
@FeignClient("gulimall-member")
public interface MemberFeignService {
    @RequestMapping("/member/memberreceiveaddress/info/{id}")
    R info(@PathVariable("id") Long id);
}

抽取的vo,地址对应运费

package com.wuyimin.gulimall.ware.vo;
@Data
public class FareVo {
    private MemberAddressVo addressVo;
    private BigDecimal fare;
}

接口幂等性

用户对于同一操作发起的一次或者多次请求结果应该是一致的

数据库层面--添加数据库字段unique限制订单号成唯一约束

 业务层面

token机制-验证码,只有验证码核对通过才可以发送请求

令牌什么时候删除:

业务结束完以后删除:不能保证幂等性,如果两个请求很快进来了,那么都能创建订单

业务结束前删除令牌:前端带来一个token,如果相同就直接删除令牌,再调用业务逻辑,如果redis里的数据没来的及删,也不能导致幂等性,所以获取令牌,对比和删除操作必须是一个原子操作,使用lua脚本可以完成此操作

数据库的悲观锁和乐观锁:

悲观锁:select * from xxx where id=1 for update 这里的id必须是主键或者唯一索引,悲观锁使用的时候一般伴随事务,数据锁定的时间可能会很长,需要一句实际情况使用

乐观锁:update goods set count=count-1,version=version+1 where good_id=2 and version=1根据version版本,也就是操作库存前先获取商品的version号,操作的时候带有此version号,乐观锁适合放在更新场景中使用,处理读多写少的问题

防重表:使用订单号作为去重表的唯一索引,插入去重表后再进行业务操作,且保证去重表和业务表操作在同一个事务,同一个数据库中。

全局唯一请求id:调用接口的时候,生成唯一一个id,redis将数据保存到集合中(去重),存在即使处理过,可以通过nginx设置每个请求的唯一id

解决订单提交的幂等性

添加防重令牌

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

    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        //从拦截器里获得用户信息
        MemberRespVo loginUser = LoginUserInterceptor.loginUser.get();
        //主线程里先把threadLocal的值取出来,因为下面异步就取不到了
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        CompletableFuture<Void> futureAddress = CompletableFuture.runAsync(() -> {
            //远程查询用户地址信息
            //子线程里拿到父线程的threadLocal里储存的信息
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<MemberAddressVo> address = memberFeignService.getAddress(loginUser.getId());
            confirmVo.setMemberAddressVos(address);
        }, executor);
        CompletableFuture<Void> futureItems = CompletableFuture.runAsync(() -> {
            //远程查询用户的购物车
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(currentUserCartItems);
        }, executor).thenRunAsync(()->{
            //获取所有的库存信息
            List<OrderItemVo> items = confirmVo.getItems();
            List<Long> collect=items.stream().map(i->i.getSkuId()).collect(Collectors.toList());
            R r = wareFeignService.getSkuHasStock(collect);
            List<SkuStockVo> data = r.getData(new TypeReference<List<SkuStockVo>>() {
            });
            if(data!=null){
                Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(map);
            }
        },executor);

        //查询用户积分
        Integer integration = loginUser.getIntegration();
        confirmVo.setIntegration(integration);
        //其他数据自动计算
        //防重令牌
        String token= UUID.randomUUID().toString().replace("-","");
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+loginUser.getId(),token,30, TimeUnit.MINUTES);
        confirmVo.setOrderToken(token);
        //等待异步任务完成
        CompletableFuture.allOf(futureAddress,futureItems).get();
        return confirmVo;
    }

}

抽取提交订单的vo

package com.wuyimin.gulimall.order.vo;
/**
 * @ Author wuyimin
 * @ Date 2021/8/27-12:03
 * @ Description
 */
@Data
@ToString
public class OrderSubmitVo {
    /** 收获地址的id **/
    private Long addrId;

    /** 支付方式 **/
    private Integer payType;
    //无需提交要购买的商品,去购物车再获取一遍
    //优惠、发票

    /** 防重令牌 **/
    private String orderToken;

    /** 应付价格 **/
    private BigDecimal payPrice;

    /** 订单备注 **/
    private String remarks;

    //用户相关的信息,直接去session中取出即可
}

成功后转发至支付页面携带的数据Vo

package com.wuyimin.gulimall.order.vo;
/**
 * @ Author wuyimin
 * @ Date 2021/8/27-12:08
 * @ Description
 */
@Data
public class SubmitOrderResponseVo {

    // 该实体为order表的映射
    private OrderEntity order;

    /** 错误状态码 **/
    private Integer code;
}

订单创建后应该返回的vo

package com.wuyimin.gulimall.order.vo;

/**
 * @ Author wuyimin
 * @ Date 2021/8/27-15:09
 * @ Description 订单创建成功后需要返回的数据
 */
@Data
public class OrderCreateVo {

    private OrderEntity order;

    private List<OrderItemEntity> orderItems;

    /** 订单计算的应付价格 **/
    private BigDecimal payPrice;

    /** 运费 **/
    private BigDecimal fare;
}

下单功能

下单功能对应的controller

 package com.wuyimin.gulimall.order.web;
 @PostMapping("/submitOrder")
    public String submitOrder(OrderSubmitVo submitVo, Model model, RedirectAttributes redirectAttributes){
        try {
            // 去OrderServiceImpl服务里验证和下单
            SubmitOrderResponseVo responseVo = orderService.submitOrder(submitVo);
            // 下单失败回到订单重新确认订单信息
            if(responseVo.getCode() == 0){
                // 下单成功去支付响应
                model.addAttribute("submitOrderResp", responseVo);
                // 支付页
                return "pay";
            }else{
                String msg = "下单失败";
                switch (responseVo.getCode()){
                    case 1: msg += "订单信息过期,请刷新在提交";break;
                    case 2: msg += "订单商品价格发送变化,请确认后再次提交";break;
                    case 3: msg += "商品库存不足";break;
                }
                redirectAttributes.addFlashAttribute("msg", msg);
                // 重定向
                return "redirect:http://order.gulimall.com/toTrade";
            }
        } catch (Exception e) {
            if (e instanceof NoStockException){
                String message = e.getMessage();
                redirectAttributes.addFlashAttribute("msg", message);
            }
            return "redirect:http://order.gulimall.com/toTrade";
        }
    }

submitOrder主方法

该方法的结构如下

package com.wuyimin.gulimall.order.service.impl;
@Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo orderSubmitVo) {
        threadLocal.set(orderSubmitVo);
        SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
        //下单:去创建订单,检验令牌,检验价格,锁定库存
        //1.验证令牌
        String orderToken = orderSubmitVo.getOrderToken();//页面传递过来的值
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        String key = OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId();//redis里存的key
        //lua脚本保证原子性 返回1代表删除成功,0代表删除失败
        String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        Long res = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(key), orderToken);
        if(res==1){
            //2.验证成功--下单创建订单,检验令牌,检验价格,锁库存
            OrderCreateVo order = createOrder();
            BigDecimal payAmount = order.getOrder().getPayAmount();
            BigDecimal payPrice = orderSubmitVo.getPayPrice();
            if(Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
                //3.金额对比成功
                //保存信息
                saveOrder(order);
                //锁定库存,只要有异常就回滚订单数据
                WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
                wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
                List<OrderItemVo> locks=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());
                wareSkuLockVo.setLocks(locks);
                //远程锁库存操作
                R r = wareFeignService.orderLockStock(wareSkuLockVo);
                if(r.getCode()==0){
                    //锁定成功了
                    responseVo.setOrder(order.getOrder());
                    responseVo.setCode(0);
                    return responseVo;
                }else{
                    //锁定失败了
                    responseVo.setCode(3);
                    throw new NoStockException();
                }

            }else{
                responseVo.setCode(2);
                return responseVo;
            }
        }else{
            //验证失败
            return responseVo;
        }
    }

原子验证使用到的lua脚本

if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end

java中对其的使用,第一个参数指定返回类型和脚本,第二个参数指定Key[1]传入一个集合,第三个参数指定对比的值

String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        Long res = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(key), orderToken);

订单状态枚举类

package com.wuyimin.gulimall.order.enume;

public enum  OrderStatusEnum {
    CREATE_NEW(0,"待付款"),
    PAYED(1,"已付款"),
    SENDED(2,"已发货"),
    RECIEVED(3,"已完成"),
    CANCLED(4,"已取消"),
    SERVICING(5,"售后中"),
    SERVICED(6,"售后完成");
    private Integer code;
    private String msg;

    OrderStatusEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

接下来是对这个大方法里的一些方法的介绍

1.createOrder创建订单方法

    //创建订单
    private OrderCreateVo createOrder(){
        String timeId = IdWorker.getTimeId();
        OrderCreateVo orderCreateVo = new OrderCreateVo();
        //1.生成订单号
        OrderEntity orderEntity = buildOrder(timeId);
        orderCreateVo.setOrder(orderEntity);
        //2.获取到所有的订单项
        List<OrderItemEntity> orderItemEntities = buildOrderItems(timeId);
        orderCreateVo.setOrderItems(orderItemEntities);

        //3.计算价格相关
        computerPrice(orderEntity,orderItemEntities);
        return orderCreateVo;
    }

 buildOrder方法

private OrderEntity buildOrder(String timeId) {
        //创建订单号
        OrderEntity orderEntity=new OrderEntity();
        orderEntity.setOrderSn(timeId);
        //获取收货地址信息
        OrderSubmitVo orderSubmitVo = threadLocal.get();
        //远程调用
        R fare = wareFeignService.getFare(orderSubmitVo.getAddrId());
        FareVo fareVo = fare.getData(new TypeReference<FareVo>() {
        });
        orderEntity.setFreightAmount(fareVo.getFare());
        orderEntity.setReceiverCity(fareVo.getAddressVo().getCity());
        orderEntity.setReceiverDetailAddress(fareVo.getAddressVo().getDetailAddress());
        orderEntity.setReceiverName(fareVo.getAddressVo().getName());
        orderEntity.setBillReceiverPhone(fareVo.getAddressVo().getPhone());
        orderEntity.setReceiverProvince(fareVo.getAddressVo().getProvince());
        orderEntity.setMemberId(fareVo.getAddressVo().getMemberId());
        orderEntity.setMemberUsername(fareVo.getAddressVo().getName());
        return orderEntity;
    }

远程调用的getFare接口,这个接口是之前写的

package com.wuyimin.gulimall.ware.controller;
@GetMapping("/fare")
    public R getFare(@RequestParam("addrId")Long addrId){
        FareVo fare =wareInfoService.getFare(addrId);
        return R.ok().put("data",fare);
    }

buildOrderItems方法

//构建全部的订单项目
    private List<OrderItemEntity> buildOrderItems(String timeId) {
        List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
        if(currentUserCartItems!=null&&currentUserCartItems.size()>0){
            List<OrderItemEntity> collect = currentUserCartItems.stream().map(i -> {
                OrderItemEntity orderItemEntity = buildOrderItem(i);
                orderItemEntity.setOrderSn(timeId);//这里只放订单号
                return orderItemEntity;
            }).collect(Collectors.toList());
            return collect;
        }
        return null;
    }

getCurrentUserItems远程调用接口,这个远程接口也是以前写的

 package com.wuyimin.gulimall.cart.controller;
@GetMapping("/currentUserCartItems")
    @ResponseBody
    public List<CartItem> getCurrentUserCartItems(){
        return cartService.getUserCartItems();
    }

buildOrderItem方法

//构建特定的订单项
    private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
        OrderItemEntity itemEntity = new OrderItemEntity();
        // 1.订单信息: 订单号
        // 已经在items里设置了

        // 2.商品spu信息
        Long skuId = cartItem.getSkuId();
        // 远程获取spu的信息
        R r = productFeignService.getSpuInfoBySkuId(skuId);
        SpuInfoVo spuInfo = r.getData(new TypeReference<SpuInfoVo>() {
        });
        itemEntity.setSpuId(spuInfo.getId());
        itemEntity.setSpuBrand(spuInfo.getBrandId().toString());
        itemEntity.setSpuName(spuInfo.getSpuName());
        itemEntity.setCategoryId(spuInfo.getCatalogId());

        // 3.商品的sku信息
        itemEntity.setSkuId(cartItem.getSkuId());
        itemEntity.setSkuName(cartItem.getTitle());
        itemEntity.setSkuPic(cartItem.getImage());
        itemEntity.setSkuPrice(cartItem.getPrice());
        // 把一个集合按照指定的字符串进行分割得到一个字符串
        // 属性list生成一个string
        String skuAttr = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";");
        itemEntity.setSkuAttrsVals(skuAttr);
        itemEntity.setSkuQuantity(cartItem.getCount());
        // 4.积分信息 买的数量越多积分越多 成长值越多
        itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount())).intValue());
        itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount())).intValue());

        // 5.订单项的价格信息 优惠金额
        itemEntity.setPromotionAmount(new BigDecimal("0.0")); // 促销打折
        itemEntity.setCouponAmount(new BigDecimal("0.0")); // 优惠券
        itemEntity.setIntegrationAmount(new BigDecimal("0.0")); // 积分

        // 当前订单项的原价
        BigDecimal orign = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
        // 减去各种优惠的价格
        BigDecimal subtract =
                orign.subtract(itemEntity.getCouponAmount()) // 优惠券逻辑没有写,应该去coupon服务查用户的sku优惠券
                        .subtract(itemEntity.getPromotionAmount()) // 官方促销
                        .subtract(itemEntity.getIntegrationAmount()); // 京豆/积分
        itemEntity.setRealAmount(subtract);
        return itemEntity;
    }

getSpuInfoBySkuId远程调用方法

    @Autowired
    private SkuInfoService skuInfoService; 
@GetMapping("/skuId/{id}")
    public R getSpuInfoBySkuId(@PathVariable("id") Long skuId){
        SpuInfoEntity entity=new SpuInfoEntity();
        SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);
        Long spuId = skuInfoEntity.getSpuId();
        SpuInfoEntity res = spuInfoService.getById(spuId);
        return R.ok().setData(res);
    }

computePrice方法

private void computerPrice(OrderEntity orderEntity, List<OrderItemEntity> items) {

        // 叠加每一个订单项的金额
        BigDecimal coupon = new BigDecimal("0.0");
        BigDecimal integration = new BigDecimal("0.0");
        BigDecimal promotion = new BigDecimal("0.0");
        BigDecimal gift = new BigDecimal("0.0");
        BigDecimal growth = new BigDecimal("0.0");

        // 总价
        BigDecimal totalPrice = new BigDecimal("0.0");
        for (OrderItemEntity item : items) {  // 这段逻辑不是特别合理,最重要的是累积总价,别的可以跳过
            // 优惠券的金额
            coupon = coupon.add(item.getCouponAmount());
            // 积分优惠的金额
            integration = integration.add(item.getIntegrationAmount());
            // 打折的金额
            promotion = promotion.add(item.getPromotionAmount());
            BigDecimal realAmount = item.getRealAmount();
            totalPrice = totalPrice.add(realAmount);

            // 购物获取的积分、成长值
            gift.add(new BigDecimal(item.getGiftIntegration().toString()));
            growth.add(new BigDecimal(item.getGiftGrowth().toString()));
        }
        // 1.订单价格相关 总额、应付总额
        orderEntity.setTotalAmount(totalPrice);
        orderEntity.setPayAmount(totalPrice.add(orderEntity.getFreightAmount()));

        orderEntity.setPromotionAmount(promotion);
        orderEntity.setIntegrationAmount(integration);
        orderEntity.setCouponAmount(coupon);

        // 设置积分、成长值
        orderEntity.setIntegration(gift.intValue());
        orderEntity.setGrowth(growth.intValue());

        // 设置订单的删除状态
        orderEntity.setDeleteStatus(OrderStatusEnum.CREATE_NEW.getCode());
    }

2.saveOrder方法--保存订单信息

    //保存订单信息
    private void saveOrder(OrderCreateVo orderCreateTo) {
        OrderEntity order = orderCreateTo.getOrder();
        order.setCreateTime(new Date());
        order.setModifyTime(new Date());
        this.save(order);
        orderItemService.saveBatch(orderCreateTo.getOrderItems());
    }

3.锁定仓库远程方法orderLockStock

package com.wuyimin.gulimall.ware.controller;


@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
    @Autowired
    private WareSkuService wareSkuService;
    @PostMapping("/lock/order")
    public R orderLockStock(@RequestBody WareSkuLockVo vo){
        Boolean results= null;
        try {
            results = wareSkuService.orderLockStock(vo);
        } catch (NoStockException e) {
            return R.error(BizCodeEnum.NO_STOCK_EXCEPTION.getCode(),BizCodeEnum.NO_STOCK_EXCEPTION.getMsg());
        }
        return R.ok().setData(results);
    }
}

orderLockStock方法

package com.wuyimin.gulimall.ware.service.impl;
//为订单锁定库存
    @Override
    @Transactional(rollbackFor =NoStockException.class )//不写class也可以,默认是运行时异常都会回滚
    public Boolean orderLockStock(WareSkuLockVo vo) {
        //1.找到每个商品在那个仓库都有库存
        List<OrderItemVo> locks = vo.getLocks();
        List<SkuWareHasStock> skuWareHasStocks = locks.stream().map(item -> {
            SkuWareHasStock stock = new SkuWareHasStock();
            Long skuId = item.getSkuId();
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());
            //查询这个商品在哪有库存
            List<Long> list = wareSkuDao.listWareIdHasStock(skuId);
            stock.setWareId(list);
            return stock;
        }).collect(Collectors.toList());
        //2.锁定库存
        for (SkuWareHasStock skuWareHasStock : skuWareHasStocks) {
            Boolean skuStocked=false;//当前商品是否锁住
            Long skuId = skuWareHasStock.getSkuId();
            List<Long> wareIds = skuWareHasStock.getWareId();
            if(wareIds==null||wareIds.size()==0){
                //没有任何仓库有这个商品的库存,订单失败,全部回滚
                throw new NoStockException(skuId);
            }
            for (Long wareId : wareIds) {
               Long count= wareSkuDao.lockSkuStock(skuId,wareId,skuWareHasStock.getNum());
               if(count==1){
                  skuStocked=true;//锁定成功
                   break;
               }else{
                   //当前仓库锁定失败,尝试下一个仓库
               }
            }
            if(!skuStocked){
                //当前商品所有仓库都没有锁住
                throw new NoStockException(skuId);
            }
        }
        //能走到这就说明锁定成功了
        return true;

    }

listWareIdHasStock方法和lockSkuStock方法

    <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>=#{num}
    </update>
    <select id="listWareIdHasStock" resultType="java.lang.Long">
        select ware_id from wms_ware_sku where sku_id=#{skuId} and stock-stock_locked>0
    </select>

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值