82--JT项目20(订单模块实现/ThreadLocal本地线程变量/Quartz框架)

1.京淘权限设计

1.1 业务说明

当用户在不登录的条件下,不允许访问购物车/订单等受限的系统.并且重定向到用户的登录页面.
问题:
1.如何校验用户是否登录? 通过Cookie /Redis
2.如何拦截用户的请求呢? 拦截器设定.

1.2 拦截器实现用户权限校验

1.2.1 SpringMVC调用原理图

说明:通过图中的分析 handler处理器负责Controller之后的所有的业务处理.

在这里插入图片描述

1.2.2 mvc拦截器执行的示意图

image-20200824205722705

1.2.3编辑拦截器配置文件

package com.jt.config;

import com.jt.handler.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
public class MvcConfigurer implements WebMvcConfigurer{//web项目中的web.xml配置文件
	@Autowired
	private UserInterceptor userInterceptor;

	//开启匹配后缀型配置
	@Override
	public void configurePathMatch(PathMatchConfigurer configurer) {
		
		configurer.setUseSuffixPatternMatch(true);
	}

	//添加拦截器的配置
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		//拦截购物车和订单模块
		registry.addInterceptor(userInterceptor)
				.addPathPatterns("/cart/**","/order/**");
	}
}

1.2.4 定义拦截器

package com.jt.handler;

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.web.servlet.HandlerInterceptor;
import redis.clients.jedis.JedisCluster;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
//拦截器的类(业务)      拦截器的配置文件(拦截什么请求)
@Component
public class UserInterceptor implements HandlerInterceptor {

    private static final String TICKET = "JT_TICKET";
    private static final String JTUSER = "JT_USER";
    @Autowired
    private JedisCluster jedisCluster;

    /**
     *  实现pre的方法
     *  返回值说明:
     *          return  false 表示拦截 需要配合重定向一齐使用
     *          return  ture  表示放行
     *  需求1: 如果用户没有登录,则重定向到系统登录页面
     *  判断条件: 如何判断用户是否登录.  1.检查Cookie中是否有记录   2.Redis中是否有记录.
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        Cookie cookie = CookieUtil.getCookieByName(request, TICKET);
        if(cookie != null){ //不为null.则表示用户可能登录.
            String ticket = cookie.getValue(); //cookie中存储的是Redis的key ticket密钥
            if(jedisCluster.exists(ticket)){
                return true;
            }else{
                //Cookie中的记录与Redis中的记录不一致.应该删除Cookie中的数据.
                CookieUtil.deleteCookie(TICKET, "/", "jt.com",response);
            }
       }

        response.sendRedirect("/user/login.html");
        return false;   //表示拦截
    }
}


1.2.5 拦截器与AOP使用场景说明

1.检查是否用到Request/Response对象,如果需要使用则建议使用拦截器.
2.看具体业务功能. 具体业务具体分析.

1.3 动态获取当前用户ID

1.3.1 业务描述

当用户登录之后,点击购物车/订单模块时需要动态的获取userID.如何实现???
实现方式:
1).基于Request对象的方式实现数据传参
2).基于线程实现数据传参

1.3.2 利用Request实现数据传参

1.3.2.1 编辑拦截器

package com.jt.handler;

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.web.servlet.HandlerInterceptor;
import redis.clients.jedis.JedisCluster;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
//拦截器的类(业务)      拦截器的配置文件(拦截什么请求)
@Component
public class UserInterceptor implements HandlerInterceptor {

    private static final String TICKET = "JT_TICKET";
    private static final String JTUSER = "JT_USER";
    @Autowired
    private JedisCluster jedisCluster;

    /**
     *  实现pre的方法
     *  返回值说明:
     *          return  false 表示拦截 需要配合重定向一齐使用
     *          return  ture  表示放行
     *  需求1: 如果用户没有登录,则重定向到系统登录页面
     *  判断条件: 如何判断用户是否登录.  1.检查Cookie中是否有记录   2.Redis中是否有记录.
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        Cookie cookie = CookieUtil.getCookieByName(request, TICKET);
        if(cookie != null){ //不为null.则表示用户可能登录.
            String ticket = cookie.getValue(); //cookie中存储的是Redis的key ticket密钥
            if(jedisCluster.exists(ticket)){

                //如果redis中的数据存在,则说明用户已经登录.可以放行请求.
                //获取真实的用户信息
                String userJSON = jedisCluster.get(ticket);
                //将json转化为对象
                User user = ObjectMapperUtil.toObject(userJSON, User.class);
                request.setAttribute(JTUSER, user);
                return true;
            }else{
                //Cookie中的记录与Redis中的记录不一致.应该删除Cookie中的数据.
                CookieUtil.deleteCookie(TICKET, "/", "jt.com",response);
            }
       }

        response.sendRedirect("/user/login.html");
        return false;   //表示拦截
    }
}

1.3.2.2 编辑CartController

在这里插入图片描述

1.4 ThreadLocal介绍

1.4.1 业务说明

说明:如果利用Request对象的方式进行数据的传参,一般只能在Controller中进行动态的数据接收.
如果在业务执行过程中需要该数据,则通过参数的形式继续向下传递.导致接口方法中的参数个数较多.
虽然该写法没有任何的问题. 该操作是否可以优化???

在这里插入图片描述

1.4.2ThreadLocal说明

名字: 本地线程变量
作用: 在当前线程内,实现数据的共享.

在这里插入图片描述

1.4.3 ThreadLocal工具API

package com.jt.util;

import com.jt.pojo.User;

public class UserThreadLocal {

    //定义本地线程变量
    private static  ThreadLocal<User> threadLocal = new ThreadLocal<>();
    //定义数据新增的方法

    public   static  void set(User user){
        threadLocal.set(user);
    }
    //获取数据的方法

    public  static  User get(){
        return  threadLocal.get();
    }
    //移除方法 ,使用threadlocal时切记将数据移除,否则极端条件下,容易产生内存泄露
    public static  void remove(){
        threadLocal.remove();
    }

}

1.4.4 实现用户数据获取

1).将数据保存到ThreadLocal中,在拦截器中进行设置

在这里插入图片描述

2).动态获取数据

在这里插入图片描述

1.4.5 关于Dubbo中的ThreadLocal说明

问题: 利用Dubbo框架从Controller能否向Service利用ThreadLocal传递数据???
答案: 不可以
原因: ThreadLocal只适用与单个项目内使用.不适合多系统调用.只能在单体架构中使用

image-20200824211004589

2.京淘订单业务实现

2.1 订单项目创建

2.1.1构建项目

在这里插入图片描述

2.1.2 添加继承/依赖/插件

<?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">
    <parent>
        <artifactId>jt</artifactId>
        <groupId>com.jt</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>jt-order</artifactId>
    <!--添加依赖-->
    <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.1.3 编辑POJO对象

Order对象

package com.jt.pojo;


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

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;

@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;//订单id =用户登录id+当前时间戳
    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 java.util.Date;
import java.util.List;

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 orderId;

    private String itemId;

    private Integer num;

    private String title;

    private Long price;

    private Long totalFee;

    private String picPath;
}

OrderShipping对象

package com.jt.pojo;


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

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.1.4 编辑OrderService接口

在这里插入图片描述

2.1.5 订单提供者创建

在这里插入图片描述

2.2 订单确认页面跳转

2.2.1 业务需求

1.当在购物车中点击去结算操作时应该跳转到订单的确认页面. order-cart.jsp
2.应该展现用户的全部的购物车信息. ${carts}

在这里插入图片描述

2.2.2 编辑OrderController

package com.jt.controller;

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

import java.util.List;

//1.业务名称与Controller对应即可
@Controller
@RequestMapping("/order")
public class OrderController {

    //2.添加接口
    @Reference //使用dubbo的包
    private DubboCartService cartService;

    /**
     * 跳转订单确认页面
     * 1.url:http://www.jt.com/order/create.html
     * 2.参数: 没有参数 null
     * 3.返回值: order-cart
     * 4.页面取值  {carts}  需要查询购物车信息.,给页面返回
     */
    @RequestMapping("create")
    public String create(Model model){

        Long userId = UserThreadLocal.get().getId();
        List<Cart> cartList = cartService.findCartList(userId);
        model.addAttribute("carts",cartList);
        return "order-cart";
    }


}

2.2.3 页面效果展现

image-20200824211655494

2.3 关于SpringMVC中页面赋值说明

2.3.1 简单参数传递

1).html标签

<html>
	<input type="text" name="name" />
	<input type="text" name="age" />
</html>

2).Controller中的方法

public xxxx saveUser(String name,int age){
}

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

1).html标签

<html>
	<input type="text" name="name" />
	<input type="text" name="age" />
</html>

2).Controller中的方法

public xxxx saveUser(User user){ //名称需要和属性一致...
}

2.3.3 为对象的引用赋值

目的:该方法可以有效的解决重名参数提交的问题. 为对象的引用赋值.
1).html标签

<html>
	<input type="text" name="name" value="二郎神"/>
	<input type="text" name="age"  value="2000"/>
	<input type="text" name="dog.name" value="哮天犬"/>
	<input type="text" name="dog.age"  value="9000"/>
</html>

2).Controller中的方法

public xxxx saveUser(User user){ //名称需要和属性一致...
	
}
public class User{
	private Dog  dog; //为对象添加一个引用
	private String name; 
	private Integer age;
}

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

2.4 完成订单入库

2.4.1 页面表单说明

<form id="orderForm" class="hide">
    <input type="hidden" name="paymentType" value="1"/>
    <c:forEach items="${carts}" var="cart" varStatus="status">
        <c:set var="totalPrice"  value="${ totalPrice + (cart.itemPrice * cart.num)}"/>
        <input type="hidden" name="orderItems[${status.index}].itemId" value="${cart.itemId}"/>
        <input type="hidden" name="orderItems[${status.index}].num" value="${cart.num }"/>
        <input type="hidden" name="orderItems[${status.index}].price" value="${cart.itemPrice}"/>
        <input type="hidden" name="orderItems[${status.index}].totalFee" value="${cart.itemPrice * cart.num}"/>
        <input type="hidden" name="orderItems[${status.index}].title" value="${cart.itemTitle}"/>
        <input type="hidden" name="orderItems[${status.index}].picPath" value="${cart.itemImage}"/>
    </c:forEach>
    <input type="hidden" name="payment" value="<fmt:formatNumber groupingUsed="false" maxFractionDigits="2" minFractionDigits="2" value="${totalPrice/100 }"/>"/>
    <input type="hidden" name="orderShipping.receiverName" value="陈晨"/>
    <input type="hidden" name="orderShipping.receiverMobile" value="13800807944"/>
    <input type="hidden" name="orderShipping.receiverState" value="北京"/>
    <input type="hidden" name="orderShipping.receiverCity" value="北京"/>
    <input type="hidden" name="orderShipping.receiverDistrict" value="海淀区"/>
    <input type="hidden" name="orderShipping.receiverAddress" value="清华大学"/>
</form>

2.4.2 POJO说明

说明:利用Order对象封装了其他2张表的数据.,所以参数使用Order对象封装即可.

在这里插入图片描述

2.4.3 页面URL分析

1).post提交
在这里插入图片描述
2).参数提交
在这里插入图片描述
3).返回值结果
在这里插入图片描述

2.4.4 编辑OrderController

/**
    * 1.实现订单入库
    *   url:http://www. jt. com/order/submit
    *   参数:整个form表单利用order对象 接收
    *   返回值: SysResult对象 返回orderId
    *   业务:订单入库时应该入库3张表记录. order orderShipping orderItems
    *   orderId由登录用户id+当前时间戳手动拼接.
    *   并且要求三个对象的主键值相同.
    */
@RequestMapping("/submit")
@ResponseBody
public SysResult submit(Order order,HttpServletRequest request){
    //获取用户的id
    User user = (User) request.getAttribute("JT_USER");
    Long userId = user.getId();
    order.setUserId(userId);	//将userId进行赋值操作.
    //订单入库操作
    String orderId =dubboOrderService.saveOrder(order);
    if (StringUtils.isEmpty(orderId)){
        return  SysResult.fail();
    }
    return  SysResult.success(orderId);

}

2.4.5 编辑OrderService

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 org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class OrderServiceImpl implements  DubboOrderService{
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderItemMapper orderItemMapper;
    @Autowired
    private OrderShippingMapper orderShippingMapper;

    @Transactional //事务控制
    @Override
    public String saveOrder(Order order) {
        //orderId 由登录用户id和时间戳构成
        String orderId = ""+order.getUserId()+System.currentTimeMillis();
        //完成订单的入库操作
        order.setOrderId(orderId).setStatus(1);
        orderMapper.insert(order);

        //完成订单商品入库
        List<OrderItem> orderItems = order.getOrderItems();
//        for (OrderItem orderItem : orderItems) {
//            orderItem.setOrderId(orderId);
//            orderItemMapper.insert(orderItem);
//
//        }
       for (OrderItem orderItem : orderItems) {
            orderItem.setOrderId(orderId);
        }
        System.out.println(orderItems);
        orderItemMapper.insertOrderItem(orderItems);
        System.out.println("订单商品入库成功");

        //完成订单物流入库
        OrderShipping orderShipping = order.getOrderShipping();
        orderShipping.setOrderId(orderId);
        orderShippingMapper.insert(orderShipping);
        System.out.println("订单物流入库成功");
        return orderId;
    }
}

2.4.6 OrderItemMapper.xml文件编写

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.jt.mapper.OrderItemMapper" >
    <insert id="insertOrderItem">
            insert into tb_order_item values
            <foreach collection="list" item="orderItem" separator="," index="i">
                (
                    #{orderItem.itemId},
                    #{orderItem.orderId},
                    #{orderItem.num},
                    #{orderItem.title},
                    #{orderItem.price},
                    #{orderItem.totalFee},
                    #{orderItem.picPath},
                    now(),
                    now()
                )
            </foreach>
    </insert>
</mapper>

2.5 完成订单查询

2.5.1 业务分析

说明:根据orderId查询订单数据.,之后在success页面中展现数据
在这里插入图片描述

2.5.2 编辑OrderController

/**
     *  实现订单的查询 更具orderId
     *  url请求地址http://www.jt.com/order/success.html?id=71598258270221
     *  参数是 id
     *  返回值 success页面\
     *  页面参数 ${order.orderId}
     */
    @RequestMapping("/success")
    public String  findOrderById(String id,Model model){
        Order order = dubboOrderService.findOrderById(id);
        model.addAttribute("order",order);

        return "success";
    }

2.5.3 编辑OrderService

//查询订单
@Override
public Order findOrderById(String id) {
    //查询订单信息
    Order order = orderMapper.selectById(id);
    //查询订单商品
    QueryWrapper<OrderItem> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("item_id",id);
    List<OrderItem> orderItems = orderItemMapper.selectList(queryWrapper);
    //查询订单物流
    OrderShipping orderShipping = orderShippingMapper.selectById(id);
    //封装order
    order.setOrderItems(orderItems).setOrderShipping(orderShipping);
    return order;
}

2.6 订单超时实现状态修改

2.6.1业务说明

说明:当订单入库之后,如果30分钟用户没有完成付款操作,则将订单的状态信息由1未付款改为6交易关闭.
如何实现: 单独开启一个线程,每隔1分钟查询一次是否有超时订单.

2.6.2Quartz框架说明

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的程序。Jobs可以做成标准的Java组件或 EJBs。Quartz的最新版本为Quartz 2.3.2。

在这里插入图片描述
组件说明:
1.Job 用户自定义的任务.
2.JobDetail 将用户封装之后的结果.
3.调度器 负责任务的协调服务.
4.触发器 当接收调度器的指令后,开启线程执行任务.

2.6.3 引入jar包文件

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

2.6.4 关于配置类说明

package com.jt.conf;

import com.jt.quartz.OrderQuartz;
import org.quartz.CronScheduleBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration //标识为一个配置类
public class OrderQuartzConfig {
	
	//定义任务详情
	@Bean
	public JobDetail orderjobDetail() {
		//指定job的名称和持久化保存任务
		return JobBuilder
				.newJob(OrderQuartz.class)//添加自己的任务
				.withIdentity("orderQuartz")//定义任务名称
				.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.6.5 执行定时任务

package com.jt.quartz;

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

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 com.alibaba.dubbo.config.annotation.Reference;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;

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

	@Autowired
	private OrderMapper orderMapper;

	/**当用户订单提交30分钟后,如果还没有支付.则交易关闭
	 * 现在时间 - 订单创建时间 > 30分钟  则超时
	 * new date - 30 分钟 > 订单创建时间
	 */
	@Override
	@Transactional
	protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
		//设定30分钟超时
		Calendar calendar = Calendar.getInstance();
		calendar.add(Calendar.MINUTE, -30);
		Date date = calendar.getTime();
		Order order = new Order();
		order.setStatus(6);
		order.setUpdated(new Date());
		UpdateWrapper<Order> updateWrapper = new UpdateWrapper<>();
		updateWrapper.eq("status", "1").lt("created",date);
		orderMapper.update(order, updateWrapper);
		System.out.println("定时任务执行");
	}
}

posted on 2020-08-24 21:40  liqiangbk  阅读(285)  评论(0编辑  收藏  举报

导航