注意事项
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