CGB2005-京淘20(权限控制,ThreadLocal简化request传参,京淘订单模块 表关系说明 对象引用传参 时间戳设置主键 serialize传参 多表入库/查询,超时订单的处理 )

注意事项

1.实现京淘项目权限控制
Springmvc拦截器说明
SpringMVC程序调用流程
WebMvcConfigurer这个配置类继承的接口相当于web.xml配置文件
在拦截其中获取user对象传到request对象中到控制层,在取出来简化代码
ThreadLocal(本地线程变量):进一步简化request传参不能随便定义reques对象,说会用到ThreadLocal
2.京淘订单模块实现
订单表 订单商品表 订单物流表
多表:一对一 一对多插入 查询操作
根据物理模型图标识的字段说明表关系
@TableField(exist=false) //入库操作忽略该字段 因为为逻辑属性

设置主键为 时间戳 或者uuid
SpringMVC中参数格式:简单参数 对象参数 对象类型引用(为了解决参数同名)
3张表同时入库
Ajax中 表单序列化传参 serialize

用户id+时间戳拼接的字符串所以用String类型接收
超时订单的处理 任务调度的3种方式 Timer Java线程池 quartz

1.实现京淘项目权限控制

1.1 需求分析

说明:如果用户没有进行登录操作时,访问购物车/订单等敏感操作时将不允许访问,应该重定向到系统的登录页面,登录成功之后才能进行访问,例如京东商城.
在这里插入图片描述

知识点:
1.AOP: 对原有的方法进行扩展。(一般做service层的业务扩展)
2.拦截器: 控制了程序的执行轨迹,满足条件时才会执行任务,控制的是request对象/response对象 。(一般拦截用户请求)
3.数据传输: request对象 /ThreadLocal(本地线程变量)
总结:如图所示,Aop处于后端服务器业务里面进行拦截,而SpringMvc拦截器是在用户发送的请求时就进行拦截。这个地方明显需要拦截用户请求,所以这里用到是的拦截器。

在这里插入图片描述

1.2 关于拦截器说明

1.2.1 SpringMVC程序调用流程

1.用户发送请求 --> 前端控制器
2.前端控制器(若配置了拦截此路径)会拦截此请求,然后调用处理器映射器
3.处理器映射器确定要调用哪个处理器,并可能执行处理器链(过滤器、拦截器),最后将处理器地址返回给前端控制器
4.前端控制器拿着地址调用处理器适配器,由其去调用具体的处理器(业务处理方法实现)
5.处理器会返回页面以及数据(Model And View)给处理器适配器
6.处理器适配器将其再返回给前端控制器
7.前端控制器调用视图解析器,将MV解析成JSP或者HTML,返回给前端控制器
8.最后前端控制器将解析完的页面返回给用户
在这里插入图片描述

1.2.2 拦截器工作原理

流程:用户发送请求首先被pre进行拦截,之后处理完业务代码被post进行拦截,最后视图渲染后被after拦截返回给客户端。(注意一旦after执行完成,请求就不会在受到服务器的控制,直接返回给客户端)

pre拦截: 要么拦截返回要么通过(可以改变用户的拦截轨迹)
post:一般用于一些记录和收尾操作 比如:返回值,报的异常等(不能改变用户的拦截轨迹)
after : 用于释放资源 随用随消 比如还线程,还连接(不能改变用户的拦截轨迹)

在这里插入图片描述

1.3 拦截器业务实现

1.3.1 定义拦截器UserInterceptor(web)

说明:
1.拦截器拦截的是web项目的购物车和订单敏感操作,所以应该放在web端.
2.定义一个类实现HandlerInterceptor接口,这个接口提供了拦截的方法。

如何查看继承的接口方法???
双击选中继承的接口-----ctrl鼠标点击进入------选中接口名+ctrl+0查看接口的所有方法。
在这里插入图片描述
分析HandlerInterceptor接口源码 default属性:
jdk1.8新特性,接口中可以有普通方法,用static或者默认的default修饰 。子类可以不实现,想用的话直接实现即可。这个地方是spring版本升级为 5版本时更新的源码中添加的。

在这里插入图片描述

package com.jt.interceptor;

import com.jt.pojo.User;
import com.jt.util.CookieUtil;
import com.jt.util.ObjectMapperUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import redis.clients.jedis.JedisCluster;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/** 如何使用这个拦截器对象呢?
 * 1.交给spring容器管理此对象
 * 2.需要在web.xml文件进行配置,因为现在不用这个文件了,在之前写的伪静态配置类中 继承的接口相当于web.xml文件,
 *    所以在MvcConfigurer这个配置类进行配置。
 */
@Component
public class UserInterceptor implements HandlerInterceptor {
    
    //需要实现拦截器接口

    @Autowired
    private JedisCluster jedisCluster; //注入集群对象

    //Spring版本升级 spring 4 必须实现所有的方法  spring 5 只需要重写指定的方法即可.

    /**
     * 需求:   拦截/cart开头的所有的请求进行拦截,并且校验用户是否登录.....
     * 拦截器选择: preHandler
     * 如何判断用户是否登录:  1.检查cookie信息   2.检查Redis中是否有记录.
     *          true : 请求应该放行
     *          false: 请求应该拦截 则配合重定向的语法实现页面跳转到登录页面 使得程序流转起来

     */

    @Override //直接在页面输入继承的方法名preHandle,会提示方法,直接enter方法会自动重写在此页面
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        //1.判断用户是否登录  检查cookie是否有值
        String ticket = CookieUtil.getCookieValue("JT_TICKET",request); //通过之前封装的工具Api获取cookie的值value(即ticket凭证)
        //2.校验ticket是否有值
        if(!StringUtils.isEmpty(ticket)){
    
     //为空说明没有登录需要跳转校验   不为空说明有凭证需要进一步校验
            //3.判断redis中是否有值.
            if(jedisCluster.exists(ticket)){
    
      //根据key判断,因为map集合key是唯一的
                /**
                 *  4. 如何动态获取userId,因为之前单点登录把用户的信息对象转化为json存入到了redis中,
                 *  所以可以根据k(ticket)获取v(json类型数据对象),在把json转为为对象通过get方法获取属性。
                 *  但是购物车的crud4个方法都要用到用户id,每个方法都要写获取用户id的方法太麻烦(即使用工具Api也需要创建类)。pre拦截器处于发送请求
                 *  和处理业务控制层之间的位置,所以可以在拦截的时候获取到这个用户对象,之后想办法把它传到控制层,这样控制层直接可以通过对象调用userid属性了。
                 */

                String userJSON = jedisCluster.get(ticket);//动态获取json信息
                User user = ObjectMapperUtil.toObject(userJSON,User.class);//把json还原为user对象
                //因为这个pre拦截器到控制层是通过请求进行连接的,所以可以把对象传到request请求对象中携带到控制层,控制层在通过HttpServletRequest request获取
                request.setAttribute("JT_USER",user);//存入到request对象的map集合中  k-v(字符串   Object任意类型)

                return true;  //如果有则放行  true代表放行
            }
        }
        //主机名相同相对路径 主机名不同绝对路径
       response.sendRedirect("/user/login.html");//表示重定向 重定向用的是response对象发送url(重定向到哪的网址 用法查看第二阶段)
        return false;//表示拦截
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
    
        //用完之后销毁数据
        request.removeAttribute("JT_USER");
    }


}



在这里插入图片描述

1.3.2 编译配置类MvcConfigurer(web)

说明:使得拦截器生效。
在这里插入图片描述

package com.jt.config;

import com.jt.interceptor.UserInterceptor;
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.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration                        //web.xml配置文件
public class MvcConfigurer implements WebMvcConfigurer{
    
    
	
	//开启匹配后缀型配置
	@Override
	public void configurePathMatch(PathMatchConfigurer configurer) {
    
    
		//开启后缀类型的匹配.  xxxx.html
		configurer.setUseSuffixPatternMatch(true);
	}



	@Autowired
	private UserInterceptor userInterceptor;//注入拦截器对象

	//添加拦截器功能
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
    
    
          //     把拦截器加进来                    拦截什么样的请求和功能,1个*代表拦截1级目录,2个代表多级目录。
		registry.addInterceptor(userInterceptor).addPathPatterns("/cart/**","/order/**");
	}
}

1.3.3 CartController动态获取UserId(web)

说明:
1.之前在购物车操作中为了降低难度,所以用户id写死了,现在改为动态获取。
2.在拦截器对象中用pre拦截器实现,获取后存入request对象,这样代码更加简洁。原因写在代码注释中。
3.用完User对象后要进行销毁。在拦截器对象中用after拦截器中实现。

步骤:
获取用户对象
在这里插入图片描述
crud4个方法都要修改为动态获取。
在这里插入图片描述

package com.jt.controller;

import com.alibaba.dubbo.config.annotation.Reference;
import com.jt.pojo.Cart;
import com.jt.pojo.User;
import com.jt.service.DubboCartService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

@Controller
@RequestMapping("/cart")
public class CartController {
    
    

    @Reference(check = false,timeout = 3000)//消费者启动时不会效验是否有提供者。
    private DubboCartService cartService;



    /**
     * 删除购物车数据 使用对象接收resful参数
     * url地址:http://www.jt.com/cart/delete/562379.html
     * 参数:   562379   获取itemId
     * 返回值:  重定向到购物车的展现页面
     */
    @RequestMapping("/delete/{itemId}")
    public String deleteCart(Cart cart, HttpServletRequest request){
    
    
        User user=(User) request.getAttribute("JT_USER");//返回值为object所以需要强转
        Long userId =user.getId();//user对象用完后应该删除掉(用的时候传 不用用该删除),所以可以再after拦截器中进行删除
        cart.setUserId(userId);
        cartService.deleteCart(cart);
        return "redirect:/cart/show.html";
    }

//普通的方式接收参数:
    /**
     * 购物车删除操作
     * url地址: http://www.jt.com/cart/delete/562379.html
     * 参数:    获取itemId
     * 返回值:  重定向到购物车的展现页面
     */
   /* @RequestMapping("/delete/{itemId}")
    public String deleteCarts(@PathVariable Long itemId){

        Long userId = 7L;
        cartService.deleteCart(userId,itemId);
        return "redirect:/cart/show.html";
    }*/







    /**
     * 业务需求: 完成购物车入库操作
     * url地址: http://www.jt.com/cart/add/562379.html
     * 参数:    form表单提交的数据/itemId   对象接收
     * 返回值:  重定向到购物车列表页面
     */
    @RequestMapping("/add/{itemId}")
    public String addCart(Cart cart, HttpServletRequest request){
    
    //因为k和属性itemId保持一致 可以直接用对象来接收
        User user=(User) request.getAttribute("JT_USER");//返回值为object所以需要强转
        Long userId =user.getId();//user对象用完后应该删除掉(用的时候传 不用用该删除),所以可以再after拦截器中进行删除
        cart.setUserId(userId);
        cartService.addCart(cart);
        //因为没有用到ajax所以重定向用 redirect :/cart/show  又因为页面跳转是伪静态的方式所以路径需要拼接.html
        return "redirect:/cart/show.html";
    }



    /**
     * 业务说明: 完成购物车数量的更新操作
     * url地址: http://www.jt.com/cart/update/num/562379/9
     * 参数: itemId/num
     * 返回值:  void
     */
    @RequestMapping("/update/num/{itemId}/{num}")
    @ResponseBody  //ajax结束的标识符.(虽然没有返回值但是请求时ajax发送的)
    public void updateCartNum(Cart cart, HttpServletRequest request){
    
       //如果{name}即key 与po属性的名称一致,则可以自动的赋值.  controller规则

        User user=(User) request.getAttribute("JT_USER");//返回值为object所以需要强转
        Long userId =user.getId();//user对象用完后应该删除掉(用的时候传 不用用该删除),所以可以再after拦截器中进行删除
        cart.setUserId(userId);
        cartService.updateCartNum(cart);
    }

   /* //常规获取参数:
    public void updateCartNum(@PathVariable Long itemId, @PathVariable Integer num){

        Long userId = 7L;
        Cart cart =new Cart();
        cartService.updateCartNum(userId,itemId,num);
    }
*/



    /**
     业务需求: 根据userId查询购物车数据
     * 1.购物车列表数据展现
     * url地址: http://www.jt.com/cart/show.html
     * 参数:    动态获取userId  暂时没有
     * 返回值:  页面逻辑名称  cart.jsp
     * 页面取值: ${cartList}
     * 应该将数据添加到域对象中 Request域  model工具API操作request对象
     *
     */
    @RequestMapping("/show") //伪静态拦截的是后缀跟这个路径没有关系
    public String show(Model model, HttpServletRequest request){
    
    
        //1.暂时将userId写死    7L
        // long userId=7在int范围内可以转(自动转换),现在是包装类型(基本类型和包装类型之间不能自动类型转换,必须是同为包装或者同为基本类型),转换通过自动装箱功能(要求类型相同比如:都是long类型,所以加L转换为long类型实现自动装箱)
        User user=(User) request.getAttribute("JT_USER");//返回值为object所以需要强转
        Long userId =user.getId();//user对象用完后应该删除掉(用的时候传 不用用该删除),所以可以再after拦截器中进行删除
        //由页面取值看出查询的结果为list集合类型
        List<Cart> cartList = cartService.findCartListByUserId(userId);
        model.addAttribute("cartList",cartList);
        return "cart";


    }
}

销毁对象
在这里插入图片描述

1.3.4 访问测试

说明:在没有登录状态下不允许操作购物车和订单敏感数据,需要先进行登录。
在这里插入图片描述
在这里插入图片描述

1.4 ThreadLocal

1.4.1 request对象传值的缺点

使用request对象缺点:不能再程序的任意地方用到request对象。
解释:在controller中用只要通过@Controller注解进行标示,那么可以在controller的任意一个方法中拿到request对象(HttpServletRequest request,配置类是因为实现了接口重写了方法),但是程序执行到业务层不能直接在方法中通过定义参数HttpServletRequest request拿到对象,只能在请求的调用过程中把控制层的参数当做参数传过来 对象.方法名(request参数名),这样虽然能用但是不方便。所以就有了另一种API ,ThreadLocal
在这里插入图片描述

1.4.2 ThreadLocal(本地线程变量)介绍

1.名称: 本地线程变量
2.作用: 可以在同一个线程内,实现数据的共享.
说明:把请求当做线程来调用,一个请求一个线程。
在这里插入图片描述

1.4.3 入门案例

/** 案例要求:定义方法a  方法b,在方法a中定义一个参数 int a = 100,
  * 需要在方法b中对方法a中的参数int a=100进行乘以100计算。
  */
  
/** 常规写法:
  *  在a方法中调用方法b,把参数传到方法b中进行计算。
  * 但是现在要求方法b中不允许定义参数,a方法也就没办法调用b方法传参,
  * 那么方法b如何对参数:int b  = 100*a;进行计算呢????
  * 本地线程变量。
  */    
	 public void a(){
    
    
        int a = 100;
        b(a);
    }

    public void b(int a){
    
    //不允许定义参数的形式,用线程的方式
        int b  = 100*a;
    }


/** 本地线程变量实现:
  *  通过定义一个第三方公共的对象(本地线程对象),把a方法中的值存入,在b方法中
  * 取出来。因为a方法调用b方法是同一个线程所以可以实现调用。
  * 因为每次请求都会创建新的对象,所以不会出现线程安全问题。
  */    

 	private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    public void a(){
    
    
        int a = 100;
        threadLocal.set(a);
        b();
    }

    public void b(){
    
    //不允许定义参数的形式,用线程的方式
        int a = threadLocal.get();
        int b  = 100*a;
    }

1.4.4 创建UserThreadLocal工具API

在这里插入图片描述

package com.jt.thread;

import com.jt.pojo.User;

public class UserThreadLocal {
    
    
    //static不会影响线程 Thread创建时跟随线程
    //private static ThreadLocal<Map<k,v>> thread = new ThreadLocal<>();如果想要存入多个对象可以先把数据存入Map集合在存入这个对象,泛型为Map集合k v都要指定泛型
    private static ThreadLocal<User> thread = new ThreadLocal<>();

    public static void set(User user){
    
      //赋值
        thread.set(user);
    }

    public static User get(){
    
               //取值

        return thread.get();
    }

    public static void remove(){
    
            //移除
        thread.remove();
    }


}

1.4.5 重构User拦截器对象传值

在这里插入图片描述

1.4.6 重构CartController动态获取UserId

c r u d4个方法修改
在这里插入图片描述

1.4.7 重构User拦截器对象销毁

在这里插入图片描述

1.4.8 启动测试

购物车的crud仍能正常操作。
在这里插入图片描述

1.4.9 练习题

问: 在JT-WEB拦截器中使用ThreadLocal进行数据传参,
问题1:JT-WEB的Controller中能否获取数据? 可以
问题2:jt-cart的Service层中能否获取数据 不可以
因为:jt-web服务器与jt-cart的服务器的通讯是通过Dubbo框架的RPC实现的. RPC相当于开启了一个新的线程,所以无法通讯.

1.4.10 关于ThreadLocal总结

1.在同一个线程内可以使用ThreadLocal
2.“一般条件下” 在同一个tomcat内的线程为同一个线程.

2.京淘订单模块实现

2.1 订单模块的表设计说明

用到的表:订单表 订单商品表 订单物流表
订单和订单商品表:一对多 pk fk标识同一字段
订单和订单物流表:一对一 pk 标识不同字段
由下图可以看出 order id和订单物流id相同,又和订单商品有对应关系。

解释:pk主键 fk外键 ak也是外键,只是写法不同
主键:唯一且不为空 并且有标识作用
外键:保存另一张表的主键在自己表作为一列,如果表示外键数据库会自动维护这层关系。
定单表: pk标识 order_id表示为订单表主键id,用户id为外键约束()
订单物流表: pk fk都标识order_id说明 这个id既是订单表的主键又是外键,所以为1对一关系。因为之前第二阶段 表一对一 可以在任意一方保存另一张表的主键,既然是一对一那么主键和外键id应该是一摸一样的,何必多写一列呢,直接把外键充当主键来用,不用在写自己的主键了。所以表示同一字段一定为一对一关系。
订单商品表:同样pk fk标识的是不同的字段,不能省略为一行。所以为一对多关系

总结:实在不行一个一个的查看表的数据是否一 一对应,如果一 一对应肯定为一对多,如果一个表的主键对应的外键有多个相同的,一定为一对多。如果保存在第三张表关系 一定为多对多。

在这里插入图片描述

2.2 项目创建jt-order

2.2.1 创建项目

在这里插入图片描述

2.2.2 添加继承/依赖/插件(order)

在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <artifactId>jt-order</artifactId>

    <parent>
        <artifactId>jt</artifactId>
        <groupId>com.jt</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <!--添加依赖项-->
    <dependencies>
        <dependency>
            <groupId>com.jt</groupId>
            <artifactId>jt-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <!--添加插件-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

2.2.3 添加主启动类(order)

在这里插入图片描述

2.2.4 添加配置文件(order)

在这里插入图片描述

server:
  port: 8095
  servlet:
    context-path: /
spring:
  datasource:
    #引入druid数据源
    #type: com.alibaba.druid.pool.DruidDataSource
    #driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/jtdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
    username: root
    password: root

#关于Dubbo配置
dubbo:
  scan:
    basePackages: com.jt    #指定dubbo的包路径 为了扫描此项目下的dubbo注解(@service),可以指定大点的范围com,jt
  application:              #应用名称
    name: provider-order     #一个接口对应一个服务名称(一个接口可以有多个实现,但是如果实现同一个接口则提供的服务也应该是同一个。 eg:老王 老李都卖菜则实现同一个接口,老孙卖肉则和老王老李实现不同的借口)
  registry: #注册中心 2181连接的是从机 backup(备用) 用户获取数据从机中获取 主机只负责监控整个集群 实现数据同步,所以这个地方要连接从机而不是主机
    address: zookeeper://192.168.126.129:2181?backup=192.168.126.129:2182,192.168.126.129:2183
  protocol:  #指定协议 name:dubbo固定写法
    name: dubbo  #使用dubbo协议(tcp-ip)  web-controller直接调用sso-Service
    port: 20883  #每一个服务都有自己特定的端口 不能重复.


  mvc:
    view:
      prefix: /WEB-INF/views/
      suffix: .jsp
#mybatis-plush配置
mybatis-plus:
  type-aliases-package: com.jt.pojo
  mapper-locations: classpath:/mybatis/mappers/*.xml
  configuration:
    map-underscore-to-camel-case: true

logging:
  level: 
    com.jt.mapper: debug

2.2.5 添加pojo(common)

说明:将课前资料的pojo复制到common中

在这里插入图片描述
order:

package com.jt.pojo;


import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;

import java.util.Date;
import java.util.List;

@TableName("tb_order")
@Data
@Accessors(chain=true)
public class Order extends BasePojo{
    
    
	@TableField(exist=false)	//入库操作忽略该字段  封装订单物流信息  一对一
	private OrderShipping orderShipping;
								
	@TableField(exist=false)	//入库操作忽略该字段  封装订单商品信息  一对多 
	private List<OrderItem> orderItems;
	
	@TableId
    private String orderId;
    private String payment;
    private Integer paymentType;
    private String postFee;
    private Integer status;
    private Date paymentTime;
    private Date consignTime;
    private Date endTime;
    private Date closeTime;
    private String shippingName;
    private String shippingCode;
    private Long userId;
    private String buyerMessage;
    private String buyerNick;
    private Integer buyerRate;

}

OrderItem :

package com.jt.pojo;


import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
@TableName("tb_order_item")
@Data
@Accessors(chain=true)
public class OrderItem extends BasePojo{
    
    
	
	@TableId
    private String itemId;

    private String orderId;

    private Integer num;

    private String title;

    private Long price;

    private Long totalFee;

    private String picPath;
}

OrderShipping :

package com.jt.pojo;


import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
@TableName("tb_order_shipping")
@Data
@Accessors(chain=true)
public class OrderShipping extends BasePojo{
    
    
	
	@TableId
    private String orderId;

    private String receiverName;

    private String receiverPhone;

    private String receiverMobile;

    private String receiverState;

    private String receiverCity;

    private String receiverDistrict;

    private String receiverAddress;

    private String receiverZip;
    
}

2.2.6 添加DubboOrderService接口(common)

在这里插入图片描述

2.2.7 添加DubboOrderServiceImpl实现类(order)

在这里插入图片描述

package com.jt.service;

import com.alibaba.dubbo.config.annotation.Service;
import com.jt.mapper.OrderItemMapper;
import com.jt.mapper.OrderMapper;
import com.jt.mapper.OrderShippingMapper;
import org.springframework.beans.factory.annotation.Autowired;

@Service
public class DubboOrderServiceImpl implements  DubboOrderService{
    
    
    @Autowired
    private OrderMapper orderMapper; //订单
    @Autowired
    private OrderItemMapper orderItemMapper;//订单商品
    @Autowired
    private OrderShippingMapper orderShippingMapper;//订单物流
}


2.2.8 添加DubboOrderMapper接口(order)

说明:因为有3个表
在这里插入图片描述
OrderMapper:

package com.jt.mapper;


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.pojo.Order;

public interface OrderMapper extends BaseMapper<Order>{
    
    
	
}

OrderItemMapper:

package com.jt.mapper;


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.pojo.OrderItem;

public interface OrderItemMapper extends BaseMapper<OrderItem>{
    
    
	
}

OrderShippingMapper:

package com.jt.mapper;



import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.pojo.OrderShipping;

public interface OrderShippingMapper extends BaseMapper<OrderShipping>{
    
    
  
}

2.2.9 访问测试

在这里插入图片描述

2.3 订单确认页面跳转

2.3.1 页面说明

说明:当用户点击去结算时,应该跳转到订单确认页面 order-cart.jsp,并展现用户的购物车以及订单自身相关信息.之后提交订单即可.
在这里插入图片描述

2.3.2 页面url分析

伪静态方式

在这里插入图片描述

2.3.3 页面js分析

说明:复制url中固定路径ctrl+h,可以看到请求在购物车页面发送请求。但是他要跳转到的页面没办法看。这个快捷键作用是定位到谁发送的请求,现在要知道跳转那个页面 只能是熟悉这个项目的人告诉你。
在这里插入图片描述
要跳转到order-cart.jsp页面
在这里插入图片描述

2.2.4 编辑OrderController(web)

说明: 因为订单确认页面只缺少购物车信息,所以只用到了DubboCartService购物车的service层对象,而查询购物车的方法已经写好了,所以只需要通过本地线程变量获取用户名查询购物车信息 携带到页面即可。
在这里插入图片描述

package com.jt.controller;

import com.alibaba.dubbo.config.annotation.Reference;
import com.jt.pojo.Cart;
import com.jt.service.DubboCartService;
import com.jt.service.DubboOrderService;
import com.jt.thread.UserThreadLocal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@Controller
@RequestMapping("/order")
public class OrderController {
    
    

    @Reference(check = false,timeout = 3000)
    private DubboOrderService dubboOrderService;//注入订单的对象
    @Reference(check = false,timeout = 3000)
    private DubboCartService cartService;//查询购物车信息注入购物车的service层

    /**
     * 订单确认页面跳转:并把购物车信息以及订单自身页面信息显示到订单确认页面,这个页面只缺少购物车信息,所以只用到了DubboCartService对象
     * 1.url地址:http://www.jt.com/order/create.html
     * 2.参数说明:  暂时没有
     * 3.返回值:  order-cart.jsp页面
     * 4.页面的取值:  ${carts}
     */
    @RequestMapping("/create")
    public String findCartByUserId(Model model){
    
     //request域
        //1.根据useId查询购物车信息
        Long userId = UserThreadLocal.get().getId();//通过本地线程标量 封装的Api获取用户名
        List<Cart> cartList = cartService.findCartListByUserId(userId);//直接调用之前订单模块业务层写好的方法即可
        model.addAttribute("carts",cartList);//key与页面保持一致
        return "order-cart";
    }
}

2.2.5 页面效果展现

在这里插入图片描述

2.3 订单表说明

2.3.1 订单表

在这里插入图片描述

2.3.2 订单表注意事项

说明:
1.订单的ID号不是主键自增.
2.订单状态信息由 1-6 注意状态说明.

问题: 为什么这里数据库的主键没有设置主键自增(MP形式在注解中设置),而是通过在程序中进行设定???
:为了在高并发情况下降低数据库的访问压力。数据库主键自增的实现流程是: 在一条数据插入到数据库时,需要查询上一条数据的id 在进行加1,这样每次都要查询一次id 在进行计算,在高并发情况下操作数据库的时间提供,速度降低(一般数据库很少出问题 比较稳定)。

解决:可以由程序自己设定主键,只要保证它是唯一不为空的即可。比如在此订单模块采用:“登录用户id+当前时间戳” 或者UUID(2^128)
解释:通过用户登录的id拼接当前时间戳(时间戳单位是毫秒 用户的id又能保证为同一用户,因为同一个用户不可能在同一毫秒内提交多个订单,所以可以保证id值唯一) 或者采用UUID的方式生成随机数(UUID的取值范围为2^128次方,几乎不可能重复,所以可以保证id唯一)。

时间戳(毫秒):时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。

在这里插入图片描述

在这里插入图片描述

2.4 SpringMVC中参数格式说明

2.4.1 简单参数传值问题

说明:页面传参中 name属性必须与mvc中的数据保持一致.
1.页面信息

<input  type="text" name="name" value="二郎神"/>
<input  type="text" name="age" value="3500"/>

2.服务端接收

public String  saveUser(String name,Integer age){
    
    ....}

2.4.2 利用对象的方式接收参数

1.页面信息

<input  type="text" name="name" value="二郎神"/>
<input  type="text" name="age" value="3500"/>

2.服务端接收(pojo对象要有对应的set get方法)
问题:为什么这个方法里面可以用User对象类型来接收,也没有new User() 那么这个对象哪来的???
:创建对象的方式,可以用new /可以用反射。具体执行流程是,当程序加载到控制层的有@controller注解标示的方法时,会通过反射实例化这个参数对象,在通过对象调用set()方法进行赋值操作,所以说方法的参数用对象接收时需要有对应的set方法。

public String  saveUser(User user){
    
    ....}


public class User{
    
    
	private Integer name;
	private String age;
}

2.4.3 mvc为对象的引用赋值(第三种)

需求:如果页面的name属性传的值都为二郎神
问题:如何解决重名提交的问题???
方案: 可以将对象进行封装,之后采用对象引用赋值的方式实现该功能.
注意事项: 属性的名称必须一致,否则赋值失败.

执行流程:首先整个页面的参数都用一个User对象来接收,当执行到dog.name时发现User对象中有这个dog对象的引用属性,会根据.age属性调用这个Dog类里面对应的属性实现接收参数。

1.页面信息

<input  type="text" name="name" value="二郎神"/>
<input  type="text" name="age" value="3500"/>
<input  type="text" name="dog.name" value="啸天犬"/>
<input  type="text" name="dog.age" value="5000"/>

2.服务端接收

public class User {
    
     
	private String name;
	private Integer age;
	private Dog dog;
}

public class Dog{
    
    
	private String name;
	private Integer age;
}
public String  saveUser(User user){
    
    ....}

2.4.4 订单对象定义(订单表)

mp规则:pojo里面的属性和数据库表里面的字段一 一对应,而这2个是由业务逻辑写的属性和数据库的字段没有关系。可以使用这个注解@TableField(exist=false) 表示该属性与数据库表中的字段没有关系,操作数据库表时不会使用该字段。
在这里插入图片描述

2.5 订单入库

2.5.1 业务说明

说明:当用户点击提交订单时要求实现三张表同时入库业务功能,并且保证orderId的值是一致的.
在这里插入图片描述

2.5.2 页面url、参数分析

1.页面URL请求
在这里插入图片描述

2.页面参数
说明:可以看到部分参数传递方式为:对象类型的引用赋值。因为订单表与订单商品表是一对多的关系,所以查询的结果为List集合对象,所以在User对象中保存订单商品表的引用对象为List< OrderItem> orderItems;类型,取值就变为通过下标去到具体的对象,再根据对象赋值属性。订单表与订单物流表是一对一关系 数据库查询的结果一次为一条,在user对象中存的引用是OrderShipping orderShipping;单个的对象接收。

在这里插入图片描述

2.5.3 页面JS分析

说明:当订单入库成功后时需要 把id返回给页面,之后 再次发送请求跳转到提交成功页面。
在这里插入图片描述

jQuery.ajax( {
    
    
			type : "POST",
			dataType : "json",  //发送请求服务器回调函数返回值的类型
			url : "/order/submit",
			//key=value&key2=value2&....客户端向服务器提交的参数太多 一个一个写太麻烦
			data : $("#orderForm").serialize(), //表单序列化,通过程序的方式,把所有的结构进行动态拼接,然后把参数传过来。简化参数多个
			// data: {"key":"value","key2":"value2".....}
			// data:  id=1&name="xxx"&age=18......
			cache : false, //不使用缓存
			success : function(result) {
    
    
				if(result.status == 200){
    
    
				//提交成功跳转到正确页面
					location.href = "/order/success.html?id="+result.data;
				}else{
    
    
				//提交失败提示用户
					$("#submit_message").html("订单提交失败,请稍后重试...").show();
				}
			},
			error : function(error) {
    
    
				$("#submit_message").html("亲爱的用户请不要频繁点击, 请稍后重试...").show();
			}
		});

2.5.4 编辑OrderController(web)

在这里插入图片描述

  package com.jt.controller;

import com.alibaba.dubbo.config.annotation.Reference;
import com.jt.pojo.Cart;
import com.jt.pojo.Order;
import com.jt.service.DubboCartService;
import com.jt.service.DubboOrderService;
import com.jt.thread.UserThreadLocal;
import com.jt.vo.SysResult;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;

@Controller
@RequestMapping("/order")
public class OrderController {
    
    

    @Reference(check = false,timeout = 3000)
    private DubboOrderService dubboOrderService;//注入订单的对象
    @Reference(check = false,timeout = 3000)
    private DubboCartService cartService;//查询购物车信息注入购物车的service层

    /**
     * 订单提交
     * url: http://www.jt.com/order/submit
     * 参数: 整个form表单
     * 返回值: SysResult对象   携带返回值orderId
     * 业务说明:
     *   当订单入库之后,需要返回orderId.让用户查询跳转到订单入库成功页面.
     */
    @RequestMapping("/submit")
    @ResponseBody
    public SysResult saveOrder(Order order){
    
    
        //获取用户id 先根据用户id找到具体的订单  在进行入库
        Long userId = UserThreadLocal.get().getId();
        order.setUserId(userId);
        //返回订单id
        String orderId = dubboOrderService.saveOrder(order);
        //判断查询的 orderId是否有值
        if(StringUtils.isEmpty(orderId))
            return SysResult.fail();
        else
            return SysResult.success(orderId);

    }
}

2.5.5 编辑DubboOrderService(order)

在这里插入图片描述

package com.jt.service;

import com.alibaba.dubbo.config.annotation.Service;
import com.jt.mapper.OrderItemMapper;
import com.jt.mapper.OrderMapper;
import com.jt.mapper.OrderShippingMapper;
import com.jt.pojo.Order;
import com.jt.pojo.OrderItem;
import com.jt.pojo.OrderShipping;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

@Service
public class DubboOrderServiceImpl implements  DubboOrderService{
    
    
    @Autowired
    private OrderMapper orderMapper; //订单
    @Autowired
    private OrderItemMapper orderItemMapper;//订单商品
    @Autowired
    private OrderShippingMapper orderShippingMapper;//订单物流

    /**
     * Order{order订单本身/order物流信息/order商品信息}
     * 难点:  操作3张表完成入库操作
     * 主键信息: orderId 一对一 2张表的主键相同  一对多 在多的一方表中保存另一张表的主键 并设置为外键会自动维护关系
     *  3张表的 orderId相同,订单表 订单物流表的主键都为orderId  而订单商品表主键为itemId
     * @param order
     * @return
     */
    @Override
    public String saveOrder(Order order) {
    
    
        //1.拼接OrderId = 用户id+时间戳  注意他俩是拼接为字符串的方式,所以""不要放在最后面(变为先加在拼接了)
        String orderId =
                "" + order.getUserId() + System.currentTimeMillis();
        //2.完成订单入库        一般商品的订单未入库为 1 未付款状态
        order.setOrderId(orderId).setStatus(1);
        orderMapper.insert(order);

        //3.完成订单物流入库
        OrderShipping orderShipping = order.getOrderShipping();//因为为了区分参数重名问题 订单物流对象存入到了order对象中
        orderShipping.setOrderId(orderId);//一对一订单物流表的主键和订单表的主键一致
        orderShippingMapper.insert(orderShipping);

        //4.完成订单商品入库 一个订单中可以用多件商品信息
        List<OrderItem> orderItems = order.getOrderItems();
        //批量入库 通过动态SQL拼接  sql: insert into xxx(xxx,xx,xx)values (xx,xx,xx),(xx,xx,xx)....
        for (OrderItem orderItem : orderItems){
    
    
            orderItem.setOrderId(orderId);
            orderItemMapper.insert(orderItem);
        }
        System.out.println("订单入库成功!!!!");//打桩
        //返回订单编号
        return orderId;
    }
}


2.5.6 测试效果

说明:因为订单入库成功后需要返回订单id给前端页面,在页面js中需要再次发送请求进行跳转到订单提交成功页面,并根据订单id进行订单信息回显。这个地方只是做了入库,入库成功后页面发送请求跳转新页面的后台没写 所以404报错。
在这里插入图片描述

根据打桩提示,表示入库成功。
在这里插入图片描述

在这里插入图片描述

2.6 订单成功跳转

2.6.1 业务分析

业务说明: 根据orderId号,检索订单的数据信息.要求利用Order对象将所有的数据一起返回。

解释,点击提交订单发送的是2个请求:订单入库和订单成功页面回显,之前点击没显示是因为入库还没有成功 不可能发送入库成功时跳转 新页面的请求。
在这里插入图片描述
在这里插入图片描述

2.6.2 页面js分析

在需要跳转的页面为success.jsp
在这里插入图片描述

在这里插入图片描述

2.6.3 编辑OrderController(web)

在这里插入图片描述

package com.jt.controller;

import com.alibaba.dubbo.config.annotation.Reference;
import com.jt.pojo.Cart;
import com.jt.pojo.Order;
import com.jt.service.DubboCartService;
import com.jt.service.DubboOrderService;
import com.jt.thread.UserThreadLocal;
import com.jt.vo.SysResult;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;

@Controller
@RequestMapping("/order")
public class OrderController {
    
    

    @Reference(check = false,timeout = 3000)
    private DubboOrderService dubboOrderService;//注入订单的对象
    @Reference(check = false,timeout = 3000)
    private DubboCartService cartService;//查询购物车信息注入购物车的service层

  
    /**
     * 实现订单信息的查询  订单表 订单物流  订单商品
     * url: http://www.jt.com/order/success.html?id=191600674802663
     * 参数: id 订单的编号
     * 返回值: success.html
     * 页面取值: ${order.orderId}
     */
    @RequestMapping("/success")
    //因为这个id是 订单orderId 用户id+时间戳拼接的字符串所以用String类型接收
    public String findOrderById(String id,Model model){
    
    

        Order order = dubboOrderService.findOrderById(id);
        model.addAttribute("order",order );
        return "success";
    }
}

2.6.4 编辑DubboOrderService(order)

在这里插入图片描述

package com.jt.service;

import com.alibaba.dubbo.config.annotation.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.jt.mapper.OrderItemMapper;
import com.jt.mapper.OrderMapper;
import com.jt.mapper.OrderShippingMapper;
import com.jt.pojo.Order;
import com.jt.pojo.OrderItem;
import com.jt.pojo.OrderShipping;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

@Service
public class DubboOrderServiceImpl implements  DubboOrderService{
    
    
    @Autowired
    private OrderMapper orderMapper; //订单
    @Autowired
    private OrderItemMapper orderItemMapper;//订单商品
    @Autowired
    private OrderShippingMapper orderShippingMapper;//订单物流

   
    @Override
    public Order findOrderById(String id) {
    
    
        //1.查询订单信息
        Order order  = orderMapper.selectById(id);//保存到Order pojo对象
        //2.查询订单物流信息
        OrderShipping orderShipping = orderShippingMapper.selectById(id); //保存到OrderShipping pojo对象
        //3.查询订单商品  因为订单商品表的orderId不是主键 所以不能用selectById(id方法 只能用普通方式查询
        QueryWrapper<OrderItem> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_id",id);
        List<OrderItem> lists =orderItemMapper.selectList(queryWrapper); //保存到OrderItem pojo对象
        /**
         * 因为 3个查询的结果分别保存在 订单表 订单物流表 订单商品表
         * 现在需要通过对象引用防止页面传参同名问题 所以需要set方法把订单物流表 订单商品表保存到订单表的属性中
         */
        return order.setOrderItems(lists).setOrderShipping(orderShipping);
    }
}


注意事务控制
在这里插入图片描述

2.6.5 页面效果展现

说明:点击提交订单 跳转到订单提交成功页面并回显订单中的数据。
在这里插入图片描述

2.7 超时订单的处理

2.7.1 业务说明

说明:如果订单提交之后如果30分钟没有完成付款,则将订单状态(该属性在order订单表)改为6,表示订单交易失败。
(private Integer status;//状态:1、未付款 2、已付款 3、未发货 4、已发货 5、交易成功 6、交易失败)
问题:如何实现每个订单30分钟超时呀???

思路1: 利用数据库的计时的函数每当order入库之后,可以添加一个函数30分钟之后修改状态。
该方法不友好,100万的订单刚刚入库, 100万个监听的事件。

思路2: 利用消息队列的方式实现 ,redis开启线程向redis中存储数据之后设定超时时间,当key一旦失效则修改数据库状态。
Redis主要做缓存使用,不合适。

思路3:
开启单独的一个线程(异步),每隔1分钟查询一次数据库,修改超时的订单处理即可.

任务调度实现方式:
1)借助Java中的Timer对象去实现(缺点数据量访问大时每个都会创建线程,内存可能会崩掉,用下面2种)
2)借助Java线程池中的任务调度对象(ScheduledExecutorService )去实现
3)借助第三方框架去实现(quartz)

2.7.2 Quartz介绍

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的程序。Jobs可以做成标准的Java组件或 EJBs。Quartz的最新版本为Quartz 2.3.2。
在这里插入图片描述
组件说明:
1. Job 是用户自定义的任务.
2. JobDetail 负责封装任务的工具API.如果任务需要被执行,则必须经过jobDetail封装.
3. 调度器: 负责时间监控,当任务的执行时间一到则交给触发器处理
4. 触发器: 当接收到调度器的命令则开启新的线程执行job…

2.7.3 导入jar包

在这里插入图片描述

 <!--添加Quartz的支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>

2.7.4 编辑配置类

在这里插入图片描述

package com.jt.config;

import com.jt.quartz.OrderQuartz;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OrderQuartzConfig {
    
    

    /**
     * 思想说明:
     * 	如果需要执行定时任务需要考虑的问题
     * 	1.任务多久执行一次.  1分钟执行一次(线程一分钟启动一次检查是否超时)
     * 	2.定时任务应该执行什么
     *
     */
    //定义任务详情
    @Bean
    public JobDetail orderjobDetail() {
    
    
        //指定job的名称和持久化保存任务
        return JobBuilder
                .newJob(OrderQuartz.class)		//1.定义执行的任务
                .withIdentity("orderQuartz")	//2.任务指定名称
                .storeDurably()
                .build();
    }
    //定义触发器
    @Bean
    public Trigger orderTrigger() {
    
    
		/*SimpleScheduleBuilder builder = SimpleScheduleBuilder.simpleSchedule()
				.withIntervalInMinutes(1)	//定义时间周期
				.repeatForever();*/
        CronScheduleBuilder scheduleBuilder
                = CronScheduleBuilder.cronSchedule("0 0/1 * * * ?"); //时 分 秒
        return TriggerBuilder
                .newTrigger()
                .forJob(orderjobDetail())	//执行的任务
                .withIdentity("orderQuartz")	//任务的名称
                .withSchedule(scheduleBuilder).build();
    }
}


2.7.5 编辑定时任务

在这里插入图片描述

package com.jt.quartz;

import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.jt.mapper.OrderMapper;
import com.jt.pojo.Order;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.Calendar;
import java.util.Date;


//准备订单定时任务
@Component
public class OrderQuartz extends QuartzJobBean{
    
    

    @Autowired
    private OrderMapper orderMapper;


    /**
     * 如果用户30分钟之内没有完成支付,则将订单的状态status由1改为6.
     * 条件判断的依据:  now()-创建时间 > 30分钟   <==>  created < now()-30
     *
     * sql: update tb_order set status=6,updated=#{updated} where status=1 and created< #{timeOut}
     * @param context
     * @throws JobExecutionException
     */
    @Override
    @Transactional
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
    
    

        //1.利用java工具API完成计算  (用法查看java Api)
        Calendar calendar = Calendar.getInstance();  //获取当前的时间
        calendar.add(Calendar.MINUTE,-1);// 设置多长时间超时 比如1分钟 单位可以自己指定为分钟
        Date timeOut = calendar.getTime();	//获取超时时间(created < now()-1)

        Order order = new Order();
        order.setStatus(6);
        /**
         * order.setUpdated(new Date());因为mp时间自动填充 所以不需要设置更新时间
         * UPDATE tb_order SET updated=?, status=? WHERE (status = ? AND created < ?)
         * 如果订单状态为1 且订单创建时间created < now()-30则证明超时执行sql把状态修改为6 且更新订单时间
         * 订单创建时间哪来的???? 条件构造器是指sql执行的条件  传入对象是修改的值。
         * created是订单入库写sql时已经存入到数据库表里面的时间,所以直接进行比较即可
         *
         */
        UpdateWrapper<Order> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq("status", "1").lt("created",timeOut);
        orderMapper.update(order, updateWrapper);
        System.out.println("定时任务执行");
    }
}

2.7.6 测试效果

以一分钟超时为例:
订单入库成功后 先执行一次查询跳转到订单提示成功页面,之后一分钟检查一次是否超时 第一次还没有超时,sql不执行更新返回值行数为 0, 第二次超过1分钟再次进行检查发现超时执行sql更新返回值为影响的行数 1

在这里插入图片描述
可以看到订单表的状态修改为1
在这里插入图片描述

3 项目结构图

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/aa35434/article/details/108716844