java初探(1)之秒杀项目总结
阅读原文时间:2023年07月14日阅读:3

在开始总结之前,先记录一个刚看到的博客,编程规约。该博客记录了一些java开发上的规范,可以在编码的时候引入这些规范。

无论流行框架一直怎么改变,web开发中的三层架构一直属于理论的基础存在。

表现层 -> 业务层 -> 持久层

箭头所指的方向就是层之间调用的方向,在SSM框架中,利用springmvc来实现表现层,利用spring来实现业务层,用mybatis来实现持久层。

简单来说,一个web网站的开发,首先明确需求以后,要先设计与需求有关的各种数据表,针对秒杀案例,用户登录网站,查看秒杀商品,完成下单,因此,最基础的需要三个表:用户表、商品表、订单表

事实上,我们虽然做的秒杀功能,但不可能这个web只有一个秒杀的项目,而是一个商城,因此,为了便于维护我们的数据表,需要在抽象出以下两个表:秒杀商品表、秒杀订单表


用户表:

包括用户id、昵称、密码、密码混淆盐值、用户头像、注册日期、最近登录日期、登录次数。

(用户表可以尽可能详细的将用户的所有特征加入,如果系统庞大,也可以抽象出一些子表,但这里没必要,但如果在一些实际的网站,可以秒杀的用户和主用户表肯定是分开的,否则主用户表的字段会越来越多,难以维护)

CREATE TABLE `miaosha_user` (
`id` BIGINT(20) NOT NULL COMMENT '用户ID,手机号码',
`nickname` VARCHAR(255) NOT NULL,
`password` VARCHAR(32) DEFAULT NULL COMMENT 'MD5(MD5(pass明文+固定salt) + salt)',
`salt` VARCHAR(10) DEFAULT NULL,
`head` VARCHAR(128) DEFAULT NULL COMMENT '头像,云存储的ID',
`register_date` DATETIME DEFAULT NULL COMMENT '注册时间',
`last_login_date` DATETIME DEFAULT NULL COMMENT '上蔟登录时间',
`login_count` INT(11) DEFAULT '' COMMENT '登录次数',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;



商品表:

包括商品ID、商品名称、商品标题、商品图片、商品的详细介绍、商品单价、商品库存

(商品表应着力于描述商品的具体特征,而不是添加秒杀的特性,理由也是为了维护系统的可用性)

CREATE TABLE `goods` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`goods_name` VARCHAR(16) DEFAULT NULL COMMENT '商品名称',
`goods_title` VARCHAR(64) DEFAULT NULL COMMENT '商品标题',
`goods_img` VARCHAR(64) DEFAULT NULL COMMENT '商品的图片',
`goods_detail` LONGTEXT COMMENT '商品的详情介绍',
`goods_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '商品单价',
`goods_stock` INT(11) DEFAULT '' COMMENT '商品库存,-1表示没有限制',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;



秒杀商品表 :

包括秒杀商品id、商品id、秒杀价、库存数、秒杀开始时间、秒杀结束时间

(抽象出来的秒杀商品表显然是商品表的子表,它可以拥有商品表的全部字段,但它有自己的价格,有自己的库存,增加了秒杀的时间限制,如果在商品表中增加字段,这无疑商品表会是个巨大无比的表)

CREATE TABLE `miaosha_goods` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀的商品表',
`goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品Id',
`miaosha_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '秒杀价',
`stock_count` INT(11) DEFAULT NULL COMMENT '库存数量',
`start_date` DATETIME DEFAULT NULL COMMENT '秒杀开始时间',
`end_date` DATETIME DEFAULT NULL COMMENT '秒杀结束时间',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;



订单表

包括订单id、用户id、商品id、收货地址id、冗余过来的商品名称、订单上商品的数量、商品单价、订单的渠道、订单的状态、订单的创建时间、订单的支付时间

(这里有些字段是不需要的比如商品名称、商品单价。通过用户id和商品id就可以找到这些信息)

CREATE TABLE `order_info` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`user_id` BIGINT(20) DEFAULT NULL COMMENT '用户ID',
`goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品ID',
`delivery_addr_id` BIGINT(20) DEFAULT NULL COMMENT '收获地址ID',
`goods_name` VARCHAR(16) DEFAULT NULL COMMENT '冗余过来的商品名称',
`goods_count` INT(11) DEFAULT '' COMMENT '商品数量',
`goods_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '商品单价',
`order_channel` TINYINT(4) DEFAULT '' COMMENT '1pc,2android,3ios',
`status` TINYINT(4) DEFAULT '' COMMENT '订单状态,0新建未支付,1已支付,2已发货,3已收货,4已退款,5已完成',
`create_date` DATETIME DEFAULT NULL COMMENT '订单的创建时间',
`pay_date` DATETIME DEFAULT NULL COMMENT '支付时间',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1565 DEFAULT CHARSET=utf8mb4;



秒杀订单表

包括秒杀订单表id、用户id、商品id、订单id

(根据这些id,可以得到具体的秒杀订单详情,其实这里可以有一个秒杀商品的id,根据该id来获取秒杀商品的价格)

CREATE TABLE `miaosha_order` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`user_id` BIGINT(20) DEFAULT NULL COMMENT '用户ID',
`order_id` BIGINT(20) DEFAULT NULL COMMENT '订单ID',
`goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品ID',
PRIMARY KEY (`id`),
UNIQUE KEY `u_uid_gid` (`user_id`,`goods_id`) USING BTREE
) ENGINE=INNODB AUTO_INCREMENT=1551 DEFAULT CHARSET=utf8mb4;


以上就是数据库的设计。同时,有了该张数据库,我们可以更加的理清楚业务的逻辑:

用户登录页面:输入用户id和密码,传到服务器,通过查询用户表,来判断是否登录成功,成功跳转到商品的列表页面;

商品列表页面(这次项目不展示该页面):通过查询数据库,将所有商品展示在页面上。并提供一个秒杀商品列表页面的入口;

秒杀商品列表(此次项目当登录成功后直接跳转的页面):通过查询秒杀商品数据库,将所有秒杀的商品展示在页面上,并在每一个商品后面添加一个【详情】链接或按钮,点击直接跳转到秒杀商品的详情页。

秒杀商品详情页:将秒杀商品的信息展示出来,包括秒杀价、秒杀库存。秒杀的时间等,并提供一个立即秒杀的按钮,点击后执行秒杀逻辑,跳转到秒杀成功页面。

秒杀成功页面:显示秒杀成功后的订单详情,通过查询数据库,将订单的详情查出来显示。

对页面进行梳理之后,就可以创建出这四张页面,关于页面,为了前后端分离,建议使用纯html,但事实上,不可能做到完全的前后端分离,因此,用户登录页面和秒杀商品列表页面可以使用thymeleaf框架提供的标签模板, 而秒杀详情页和秒杀成功页面将采用纯html的方式辅助使用ajax请求的方式来完成数据的传递。


用户登录页面

(登录页面由三部分组成,一部分是引入了thymeleaf模板,可以依照此规则,引入标签,获取参数,然后显示。一部分是纯的html标签和css样式,对布局样式进行规定,使页面更加美观。另一部分就是完成数据传递或者页面动态展示的js代码,更多的是ajax请求,以及数据处理。)




登录

<!-- jquery -->  
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>  
<!-- bootstrap -->  
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}" />  
<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>  
<!-- jquery-validator -->  
<script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}"></script>  
<script type="text/javascript" th:src="@{/jquery-validation/localization/messages\_zh.min.js}"></script>  
<!-- layer -->  
<script type="text/javascript" th:src="@{/layer/layer.js}"></script>  
<!-- md5.js -->  
<script type="text/javascript" th:src="@{/js/md5.min.js}"></script>  
<!-- common.js -->  
<script type="text/javascript" th:src="@{/js/common.js}"></script>


用户登录



页面分析:

该页面通过引入bootstrap模板,来规定页面的样式。

该页面通过引入thymeleaf模板,来对数据进行动态的展示。

该页面通过引入jQuery以及jQuery-validate模板,来使用各种js函数以及对输入数据进行基础验证。

note:

就内容而言,该页面只提供了一个form表单,然后提供了id和密码的输入框。通过对标签属性的设置,规定数据的验证规则。

对于js代码,主要是一个ajax请求。对于传送的数据,基于安全原则,不能在网络中传输明文密码,因此,需要将传递的密码值加密。

ajax请求规定了接收到数据响应后的操作。



秒杀商品列表页面:

(页面由于只有展示的业务,因此,只需要根据thymeleaf模板的标签,拿到返回值并在页面上做显示。出口只提供一个详情的页面跳转)



商品列表

秒杀商品列表
商品名称商品图片商品原价秒杀价库存数量详情
详情


note:

需要使用thymeleaf提供的命名空间,将页面显示出来。

详情页面由于使用静态页面,就不需要请求服务器然后跳转页面的方式了。而是直接跳转到秒杀商品的详情页面。



秒杀商品详情页面



商品详情

秒杀商品详情
您还没有登录,请登陆后再操作
没有收货地址的提示。。。
商品名称
商品图片
秒杀开始时间
商品原价
秒杀价
库存数量



页面分析:

该页面比较复杂,主要包括,静态页面部分、秒杀商品详情数据的返回(ajax请求,由入口函数调用得到)、点击秒杀按钮触发的秒杀逻辑。

静态页面:显示用户登录信息、商品名称、图片、秒杀开始时间。价格、库存等基础信息的标签,并提供标签id,方便利用jQuery进行获取。当静态页面加载之后,就会被客户端(浏览器)缓存,以后请求如果页面不变,就不会想服务器请求调用静态资源。

数据返回:数据通过入口函数,调用一个ajax请求来返回数据。返回的数据包括所有需要显示的数据。,当调用成功后,通过js代码来控制数据如何显示,包括秒杀的倒计时。

秒杀事件触发:这里通过ajax请求,将逻辑交给服务器执行。(源代码通过几次ajax请求,申请随机地址,然后执行秒杀逻辑,得到秒杀结果,应该还有改进的空间)


秒杀成功页面



订单详情

秒杀订单详情
商品名称
商品图片
订单价格
下单时间
订单状态
收货人 玉皇大帝
收货地址 天宫一号



页面分析:

秒杀成功的页面比较简单,和上一页面类似,主要包括,静态页面部分、秒杀商品订单详情数据的返回(ajax请求,由入口函数调用得到)、点击支付按钮触发的支付逻辑。

由于没有做支付的业务逻辑,因此,此页面只有一个ajax请求来回去展示数据。


通过以上页面和数据库的创建和分析,其实已经大致摸清了整个秒杀项目的时序图:

登录页面 -> (ajax传数据) ->表现层(Controller)->返回数据 -> (跳转到商品列表的处理的类或者显示错误信息)

商品列表的处理类 -> 封装需要的数据 ->返回页面 -> 展示页面

(上述过程是一个时序,不需要用户进行输入或者点击)

点击详情 -> 跳转到商品详情页面(注意是页面,不是服务器控制类)-> 页面入口函数 -> ajax请求(获取页面需要展示的数据) ->返回数据,显示数据。

(上述过程是一个时序,不需要用户进行输入或者点击)

点击秒杀 -> 跳转获取秒杀路径 ->返回数据 ->跳转秒杀实际逻辑 ->返回数据 ->执行轮询请求判断是否秒杀成功 -> 秒杀成功跳转订单详情页。

梳理了以上时序图,可以在搭建完整的代码。


构建项目环境,使用springboot以及mybatis构建,maven自动导入项目坐标。



maven导入坐标的pom文件

org.springframework.boot spring-boot-starter-parent 1.5.8.RELEASE

miaosha_2
http://maven.apache.org

UTF-8

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

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

<dependency>  
    <groupId>org.mybatis.spring.boot</groupId>  
    <artifactId>mybatis-spring-boot-starter</artifactId>  
    <version>1.3.1</version>  
</dependency>

<dependency>  
    <groupId>mysql</groupId>  
    <artifactId>mysql-connector-java</artifactId>  
</dependency>

<dependency>  
    <groupId>com.alibaba</groupId>  
    <artifactId>druid</artifactId>  
    <version>1.0.5</version>  
</dependency>

<dependency>  
    <groupId>redis.clients</groupId>  
    <artifactId>jedis</artifactId>  
</dependency>

<dependency>  
    <groupId>com.alibaba</groupId>  
    <artifactId>fastjson</artifactId>  
    <version>1.2.38</version>  
</dependency>

<dependency>  
    <groupId>commons-codec</groupId>  
    <artifactId>commons-codec</artifactId>  
</dependency>  
<dependency>  
    <groupId>org.apache.commons</groupId>  
    <artifactId>commons-lang3</artifactId>  
    <version>3.6</version>  
</dependency>

<dependency>  
    <groupId>commons-codec</groupId>  
    <artifactId>commons-codec</artifactId>  
</dependency>  
<dependency>  
    <groupId>org.apache.commons</groupId>  
    <artifactId>commons-lang3</artifactId>  
    <version>3.6</version>  
</dependency>

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

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

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

notes:

如果加载坐标太慢,可以建立私服,或者建立本地仓库。



创建配置文件application.properties

notes:可以暂时不用配置任何参数,用到什么框架配置什么内容。



创建包以及资源文件夹,并将静态资源引入

具体项目目录就如上图所示。

controller包表示表现层代码、service包表示业务层代码、dao包表示持久层代码、entity包表示实体类代码(domain与数据库传递的实体、bo与业务层传递的实体、vo与表现层传递的实体)、util包表示一些配置或者其他的工具类。

APP类是项目启动类

package com.miaosha;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class APP {

public static void main(String\[\] args) {

    SpringApplication.run(APP.class);  
}  

}



实现登录功能

所用技术:redis缓存、参数验证(自定义注解)、全局异常处理、返回数据抽象。

业务逻辑分析:

第一步,页面发起ajax请求,将数据传给服务器。服务器首先将数据进行参数验证,若参数不符合规则,直接返回错误信息。

第二步,若参数验证通过,将从缓存中取到用户数据,若缓存中没有数据,则从数据库中查询,然后存到缓存中。

第三步,将取到的用户数据与页面传递的数据比较,若错误,则返回错误信息,若正确,则返回正确信息。

以上是对业务的简单分析,但涉及到分布式,可能不同服务器之间没有共通的session,因此如果传到其他页面,user就会失效。因此需要考虑如何能获取到user的值。

同时,根据分析,可以知道,需要参数验证,如果每次在controller类中进行参数验证,势必会使系统很冗余,因此,采用注解的方式对参数进行验证。

还有就是返回值,返回值抽象出来管理,不同的控制器返回不同类型的值,有可能返回一个基本类型,有可能返回实体类,也有可能返回错误信息。

对结果集的封装

结果集是指返回结果,起码包含两部分,一部分是提示信息,一部分是真正的返回值,因此他是一个vo的实体类

该类还需要进一步抽象出一个专门存储错误消息的类,因为我们在返回过程中会有很多的消需要提醒。

分析:结果集需要哪些内容,一般来说,一个是数字提示的代码,一个是文字提示的消息,一个是真正需要返回的vo,result其实也是vo,是封装了的vo。

需要提供两个方法,业务正确时的success方法,业务出现错误时的error方法,如果业务正确,则代码和消息是固定的,但需要传递不同类型的数据;如果业务错误,则代码和消息不固定,但不需要传递数据。

因此抽象一个泛型的Result类来返回类,将代码和消息封装到CodeMsg类中,提供一系列静态实例。

package com.miaosha.entity.vo.result;

public class CodeMsg {

private Integer code; //返回代码  
private String msg; //返回信息

private CodeMsg( ) {  
}

private CodeMsg( int code,String msg ) {  
    this.code = code;  
    this.msg = msg;  
}

//通用的错误码  
public static CodeMsg SUCCESS = new CodeMsg(0, "success");  
public static CodeMsg SERVER\_ERROR = new CodeMsg(500100, "服务端异常");  
public static CodeMsg BIND\_ERROR = new CodeMsg(500101, "参数校验异常:%s");  
public static CodeMsg REQUEST\_ILLEGAL = new CodeMsg(500102, "请求非法");  
public static CodeMsg ACCESS\_LIMIT\_REACHED= new CodeMsg(500104, "访问太频繁!");  
//登录模块 5002XX  
public static CodeMsg SESSION\_ERROR = new CodeMsg(500210, "Session不存在或者已经失效");  
public static CodeMsg PASSWORD\_EMPTY = new CodeMsg(500211, "登录密码不能为空");  
public static CodeMsg MOBILE\_EMPTY = new CodeMsg(500212, "手机号不能为空");  
public static CodeMsg MOBILE\_ERROR = new CodeMsg(500213, "手机号格式错误");  
public static CodeMsg MOBILE\_NOT\_EXIST = new CodeMsg(500214, "手机号不存在");  
public static CodeMsg PASSWORD\_ERROR = new CodeMsg(500215, "密码错误");

//商品模块 5003XX

//订单模块 5004XX  
public static CodeMsg ORDER\_NOT\_EXIST = new CodeMsg(500400, "订单不存在");

//秒杀模块 5005XX  
public static CodeMsg MIAO\_SHA\_OVER = new CodeMsg(500500, "商品已经秒杀完毕");  
public static CodeMsg REPEATE\_MIAOSHA = new CodeMsg(500501, "不能重复秒杀");  
public static CodeMsg MIAOSHA\_FAIL = new CodeMsg(500502, "秒杀失败");

public Integer getCode() {  
    return code;  
}

public String getMsg() {  
    return msg;  
}  

}

CodeMsg

package com.miaosha.entity.vo.result;

public class Result {

private Integer code; //返回代码  
private String msg; //返回信息  
private T date;//返回实体

private Result(CodeMsg codeMsg){

    if(codeMsg!=null) {  
        this.code = codeMsg.getCode();  
        this.msg = codeMsg.getMsg();  
        this.date=null;  
    }

}

private Result(T date){  
    this.code=0;  
    this.msg="success";  
    this.date=date;  
}

public static <T> Result<T> success(T date){  
    return new Result<T> (date);  
}

public static <T> Result<T> error(CodeMsg codeMsg){

    return new Result<T>(codeMsg);  
}

public Integer getCode() {  
    return code;  
}

public void setCode(Integer code) {  
    this.code = code;  
}

public String getMsg() {  
    return msg;  
}

public void setMsg(String msg) {  
    msg = msg;  
}

public T getDate() {  
    return date;  
}

public void setDate(T date) {  
    this.date = date;  
}  

}

Result

这种封装能极大的提供代码的可用性,扩展性。

对登录信息实体类的封装

对于登录信息传递过来的值,显然用不到用户表中的那么多字段,只需要得到两个字段,一个用户id,一个用户密码。而且该类需要和表现层交互,因此也是在vo包创建

package com.miaosha.entity.vo;

public class LoginVO {

private Long mobile;

private String password;

public Long getMobile() {  
    return mobile;  
}

public void setMobile(Long mobile) {  
    this.mobile = mobile;  
}

public String getPassword() {  
    return password;  
}

public void setPassword(String password) {  
    this.password = password;  
}  

}

LoginVO

该类的属性名需要和页面上手机号码和密码的name属性一致(这样做是为了让springboot能自动识别bean)

实现验证功能

当页面将数据传到Controller类后,需要对登录数据的格式进行验证(是否为空,长度是否正确,格式是否正确。。。)

使用JSR-303标准的验证形式,在jdk1.8中支持这样的验证。

步骤1:在LoginVO(即待验证的属性上加上需要验证的注解项,@NotNull。。。)

package com.miaosha.entity.vo;

import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.NotNull;

public class LoginVO {

@NotNull  
private Long mobile;

@NotNull  
@Length(min = 32)  
private String password;

public Long getMobile() {  
    return mobile;  
}

public void setMobile(Long mobile) {  
    this.mobile = mobile;  
}

public String getPassword() {  
    return password;  
}

public void setPassword(String password) {  
    this.password = password;  
}  

}

LoginVO

步骤2:在LoginController类的doLogin方法的参数LoginVo前加上@Valid注解。

package com.miaosha.controller;

import com.miaosha.entity.vo.LoginVO;
import com.miaosha.entity.vo.result.Result;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.validation.Valid;

@Controller
@RequestMapping("/login")
public class LoginController {

//去登录页面  
@RequestMapping("/to\_login")  
public String toLogin(){

    return "login";

}

//执行登录  
@RequestMapping("/do\_login")  
@ResponseBody  
public Result<Boolean> doLogin(@Valid LoginVO loginVO){

    return Result.success(true);

}

}

LoginController

验证原理:让注解的逻辑类实现ConstraintValidator接口的方法,确定是否验证通过,验证通过返回true,没有通过返回false,当没有通过后,会抛出一个BindException类型的异常,异常信息就是注解中默认的信息。

自定义验证注解

根据对验证原理的分析,可以通过本身的业务需求自定义一个验证的注解——@IsMobile

步骤1:定义一个IsMobile注解,并继承Constraint注解,来指定一个继承了ConstraintValidator接口的逻辑类。

package com.miaosha.util.valid;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class })
public @interface IsMobile {

boolean required() default true;

String message() default "手机号码格式错误";

Class<?>\[\] groups() default { };

Class<? extends Payload>\[\] payload() default { };

}

IsMobile

步骤2:实现该注解的逻辑类

在初始方法中,将请求初始化为默认的请求。

在验证逻辑判断方法中,如果验证成功,返回true,若验证失败则返回false。

package com.miaosha.util.valid;

import com.miaosha.util.valid.IsMobile;
import org.apache.commons.lang3.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class IsMobileValidator implements ConstraintValidator {

private boolean required = false;

@Override  
public void initialize(IsMobile isMobile) {  
    required = isMobile.required();  
}

@Override  
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {  
    if(required) {  
        return ValidatorUtil.isMobile(s);  
    }else {  
        if(StringUtils.isEmpty(s)) {  
            return true;  
        }else {  
            return ValidatorUtil.isMobile(s);  
        }  
    }  
}  

}

IsMobileValidator

package com.miaosha.util.valid;

import org.apache.commons.lang3.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ValidatorUtil {

private static final Pattern mobile\_pattern = Pattern.compile("1\\\\d{10}");

public static boolean isMobile(String src) {  
    if(StringUtils.isEmpty(src)) {  
        return false;  
    }  
    Matcher m = mobile\_pattern.matcher(src);  
    return m.matches();  
}  

}

ValidatorUtil

查看页面返回结果:

{"timestamp":1582541833111,"status":400,"error":"Bad Request","exception":"org.springframework.validation.BindException","errors":[{"codes":["IsMobile.loginVO.mobile","IsMobile.mobile","IsMobile.java.lang.String","IsMobile"],"arguments":[{"codes":["loginVO.mobile","mobile"],"arguments":null,"defaultMessage":"mobile","code":"mobile"},true],"defaultMessage":"手机号码格式错误","objectName":"loginVO","field":"mobile","rejectedValue":"23588038176","bindingFailure":false,"code":"IsMobile"}],"message":"Validation failed for object='loginVO'. Error count: 1","path":"/login/do_login"}

确实可以看到返回了我们需要的默认错误消息。只不过我们不需要这样的返回规则,那么需要一个统一处理异常的类,来对异常做统一的返回类型处理。

异常统一处理

springboot提供了可以统一处理异常的机制,但在此之前,我们需要自定义一个全局异常类,该类用于返回各种我们人为抛出的业务逻辑异常。比如用户不存在,密码错误等信息。

步骤1:创建全局异常类,提供自定义异常信息的方法

package com.miaosha.util.exception;

import com.miaosha.entity.vo.result.CodeMsg;

public class GlobalException extends RuntimeException {

private CodeMsg cm;

public GlobalException(CodeMsg cm){  
    super(cm.toString());  
    this.cm=cm;  
}

public CodeMsg getCm() {  
    return cm;  
}  

}

GlobalException

步骤2:对异常进行统一管理,具体就是将各种异常规则化为之前定义的Result返回。根据的是springboot提供的ControllerAdvice注解和ExceptionHandler注解

package com.miaosha.util.exception;

import com.miaosha.entity.vo.result.CodeMsg;
import com.miaosha.entity.vo.result.Result;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

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

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

@ExceptionHandler(value=Exception.class)  
public Result<String> exceptionHandler(HttpServletRequest request, Exception e){  
    e.printStackTrace();  
    if(e instanceof GlobalException) {  
        GlobalException ex = (GlobalException)e;  
        return Result.error(ex.getCm());  
    }else if(e instanceof BindException) {  
        BindException ex = (BindException)e;  
        List<ObjectError> errors = ex.getAllErrors();  
        ObjectError error = errors.get(0);  
        String msg = error.getDefaultMessage();  
        return Result.error(CodeMsg.BIND\_ERROR.fillArgs(msg));  
    }else {  
        return Result.error(CodeMsg.SERVER\_ERROR);  
    }  
}

}

GlobalExceptionHandler

这里的ControllerAdvice注解会在启动的时候加加载初始化bean,将GlobalExceptionHandler类扫描进包,然后通过ExceptionHandler注解对异常统一管理

参考博客

返回结果如下:

{"code":500101,"msg":"参数校验异常:手机号码格式错误","date":null}

redis缓存技术

jedis类似于jdbc是一个redis的操作api,如果要用redis技术,需要对jedis进行一定的配置,并最后能封装一个service方法来调用jedis。

步骤1:对jedis进行配置,先在application.properties配置文件中引入配置信息,然后通过ConfigurationProperties注解,对配置信息进行解析,得到所有的配置参数,包括redis的主机号和端口等

#redis
redis.host=10.110.3.62
redis.port=6379
redis.timeout=10
redis.password=123456
redis.poolMaxTotal=1000
redis.poolMaxIdle=500
redis.poolMaxWait=500

步骤二:解析配置文件,得到将这些配置信息转换为属性,方便操作。

package com.miaosha.util.redisconfig;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "redis")
public class RedisConfig {

private String host;  
private int port;  
private int timeout;//秒  
private String password;  
private int poolMaxTotal;  
private int poolMaxIdle;  
private int poolMaxWait;//秒

public String getHost() {  
    return host;  
}

public void setHost(String host) {  
    this.host = host;  
}

public int getPort() {  
    return port;  
}

public void setPort(int port) {  
    this.port = port;  
}

public int getTimeout() {  
    return timeout;  
}

public void setTimeout(int timeout) {  
    this.timeout = timeout;  
}

public String getPassword() {  
    return password;  
}

public void setPassword(String password) {  
    this.password = password;  
}

public int getPoolMaxTotal() {  
    return poolMaxTotal;  
}

public void setPoolMaxTotal(int poolMaxTotal) {  
    this.poolMaxTotal = poolMaxTotal;  
}

public int getPoolMaxIdle() {  
    return poolMaxIdle;  
}

public void setPoolMaxIdle(int poolMaxIdle) {  
    this.poolMaxIdle = poolMaxIdle;  
}

public int getPoolMaxWait() {  
    return poolMaxWait;  
}

public void setPoolMaxWait(int poolMaxWait) {  
    this.poolMaxWait = poolMaxWait;  
}  

}

RedisConfig

步骤三:封装一个可以获取redis池对象的方法,这是一个工厂类,类似于工厂模式,得到了JedisPook对象

package com.miaosha.util.redisconfig;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@Component
public class RedisPoolFactory {

@Autowired  
private RedisConfig redisConfig;

@Bean  
public JedisPool JedisPoolFactory(){  
    JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();  
    jedisPoolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait());  
    jedisPoolConfig.setMaxTotal(redisConfig.getPoolMaxTotal());  
    jedisPoolConfig.setMaxIdle(redisConfig.getPoolMaxIdle());

    JedisPool jedisPool=new JedisPool(jedisPoolConfig,redisConfig.getHost(),redisConfig.getPort(),redisConfig.getTimeout() \*1000,null,0);  
    return jedisPool;  
}

}

RedisPoolFactory

步骤四:根据这样的池对象封装一些操作的方法供缓存使用,之后就可以像类似于redisTemplate对象一样了。因此,将其命名为MyResidTemplate类

package com.miaosha.util.redisconfig;

import java.util.ArrayList;
import java.util.List;

import com.miaosha.util.redisconfig.key.KeyPrefix;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.alibaba.fastjson.JSON;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;

@Service
public class MyRedisTemplate {

@Autowired  
private JedisPool jedisPool;

/\*\*  
 \* 获取当个对象  
 \* \*/  
public <T> T get(KeyPrefix prefix, String key, Class<T> clazz) {  
    Jedis jedis = null;  
    try {  
        jedis =  jedisPool.getResource();  
        //生成真正的key  
        String realKey  = prefix.getPrefix() + key;  
        String  str = jedis.get(realKey);  
        T t =  stringToBean(str, clazz);  
        return t;  
    }finally {  
        returnToPool(jedis);  
    }  
}

/\*\*  
 \* 设置对象  
 \* \*/  
public <T> boolean set(KeyPrefix prefix, String key,  T value) {  
    Jedis jedis = null;  
    try {  
        jedis =  jedisPool.getResource();  
        String str = beanToString(value);  
        if(str == null || str.length() <= 0) {  
            return false;  
        }  
        //生成真正的key  
        String realKey  = prefix.getPrefix() + key;  
        int seconds =  prefix.expireSeconds();  
        if(seconds <= 0) {  
            jedis.set(realKey, str);  
        }else {  
            jedis.setex(realKey, seconds, str);  
        }  
        return true;  
    }finally {  
        returnToPool(jedis);  
    }  
}

/\*\*  
 \* 判断key是否存在  
 \* \*/  
public <T> boolean exists(KeyPrefix prefix, String key) {  
    Jedis jedis = null;  
    try {  
        jedis =  jedisPool.getResource();  
        //生成真正的key  
        String realKey  = prefix.getPrefix() + key;  
        return  jedis.exists(realKey);  
    }finally {  
        returnToPool(jedis);  
    }  
}

/\*\*  
 \* 删除  
 \* \*/  
public boolean delete(KeyPrefix prefix, String key) {  
    Jedis jedis = null;  
    try {  
        jedis =  jedisPool.getResource();  
        //生成真正的key  
        String realKey  = prefix.getPrefix() + key;  
        long ret =  jedis.del(realKey);  
        return ret > 0;  
    }finally {  
        returnToPool(jedis);  
    }  
}

/\*\*  
 \* 增加值  
 \* \*/  
public <T> Long incr(KeyPrefix prefix, String key) {  
    Jedis jedis = null;  
    try {  
        jedis =  jedisPool.getResource();  
        //生成真正的key  
        String realKey  = prefix.getPrefix() + key;  
        return  jedis.incr(realKey);  
    }finally {  
        returnToPool(jedis);  
    }  
}

/\*\*  
 \* 减少值  
 \* \*/  
public <T> Long decr(KeyPrefix prefix, String key) {  
    Jedis jedis = null;  
    try {  
        jedis =  jedisPool.getResource();  
        //生成真正的key  
        String realKey  = prefix.getPrefix() + key;  
        return  jedis.decr(realKey);  
    }finally {  
        returnToPool(jedis);  
    }  
}

public boolean delete(KeyPrefix prefix) {  
    if(prefix == null) {  
        return false;  
    }  
    List<String> keys = scanKeys(prefix.getPrefix());  
    if(keys==null || keys.size() <= 0) {  
        return true;  
    }  
    Jedis jedis = null;  
    try {  
        jedis = jedisPool.getResource();  
        jedis.del(keys.toArray(new String\[0\]));  
        return true;  
    } catch (final Exception e) {  
        e.printStackTrace();  
        return false;  
    } finally {  
        if(jedis != null) {  
            jedis.close();  
        }  
    }  
}

public List<String> scanKeys(String key) {  
    Jedis jedis = null;  
    try {  
        jedis = jedisPool.getResource();  
        List<String> keys = new ArrayList<String>();  
        String cursor = "0";  
        ScanParams sp = new ScanParams();  
        sp.match("\*"+key+"\*");  
        sp.count(100);  
        do{  
            ScanResult<String> ret = jedis.scan(cursor, sp);  
            List<String> result = ret.getResult();  
            if(result!=null && result.size() > 0){  
                keys.addAll(result);  
            }  
            //再处理cursor  
            cursor = ret.getStringCursor();  
        }while(!cursor.equals("0"));  
        return keys;  
    } finally {  
        if (jedis != null) {  
            jedis.close();  
        }  
    }  
}

public static <T> String beanToString(T value) {  
    if(value == null) {  
        return null;  
    }  
    Class<?> clazz = value.getClass();  
    if(clazz == int.class || clazz == Integer.class) {  
        return ""+value;  
    }else if(clazz == String.class) {  
        return (String)value;  
    }else if(clazz == long.class || clazz == Long.class) {  
        return ""+value;  
    }else {  
        return JSON.toJSONString(value);  
    }  
}

@SuppressWarnings("unchecked")  
public static <T> T stringToBean(String str, Class<T> clazz) {  
    if(str == null || str.length() <= 0 || clazz == null) {  
        return null;  
    }  
    if(clazz == int.class || clazz == Integer.class) {  
        return (T)Integer.valueOf(str);  
    }else if(clazz == String.class) {  
        return (T)str;  
    }else if(clazz == long.class || clazz == Long.class) {  
        return  (T)Long.valueOf(str);  
    }else {  
        return JSON.toJavaObject(JSON.parseObject(str), clazz);  
    }  
}

private void returnToPool(Jedis jedis) {  
    if(jedis != null) {  
        jedis.close();  
    }  
}  

}

MyRedisTemplate

该类中实现了jedis操作缓存的若干方法,包括向缓存中存入对象(泛型)、取出对象、判断key值是否存在、删除对象、自增、自减、获取所有key。

但在此之前,需要抽象一个有关key的类,redis缓存技术是基于key-value格式去缓存的,而对于一个项目而言,key值有很多个,因此,需要通过接口 -> 抽象类 ->实体类的方式,对key值进行扩展。一个key值包括,一个前缀和一个真正的key值名称,同时还需要包括这个key值在redis中的存活时间。

接口 -> 抽象类 -> 实现类

步骤1:抽象一个接口,提供两个方法,一个是过期时间,一个是key的前缀

package com.miaosha.util.redisconfig.key;

public interface KeyPrefix {

int expireSeconds();  
String getPrefix();  

}

KeyPrefix

步骤2:继承接口的抽象类

package com.miaosha.util.redisconfig.key;

public class BasePrefix implements KeyPrefix {

private int expireSeconds;

private String prefix;

public BasePrefix(String prefix) {//0代表永不过期  
    this(0, prefix);  
}

public BasePrefix( int expireSeconds, String prefix) {  
    this.expireSeconds = expireSeconds;  
    this.prefix = prefix;  
}

public int expireSeconds() {//默认0代表永不过期  
    return expireSeconds;  
}

public String getPrefix() {  
    String className = getClass().getSimpleName();  
    return className+":" + prefix;  
}  

}

BasePrefix

步骤3:实现抽象类

//该类在具体用到的时候进行抽象。

登录逻辑

登录逻辑分析:

步骤1:得到页面传过来的mobile、password。并根据mobile值从缓存中去找user是否存在。

步骤2:若缓存中有user,则执行下一步,比较密码是否一致。若缓存中没有user,则需要从数据库中找,如果找到,则执行下一步,比较密码是否一致。若也不存在,则抛出全局异常。

步骤3:验证密码,密码在页面传过来时已经经过md5加密,而且数据库中所存的密码是执行了两个md5的结果,因此,对密码应该进行一次md5加密才能进行比较,若密码相同,则说明信息正确,若不相同,抛出异常。

步骤4:若密码相同,说明登录成功,则需要向页面传回一个cookie值,将cookie值存在页面上,保证跳转到其他服务器中,session值已经过期的情况下,也可以在其他页面拿到现在得到的user值,因此,该cookie值应当对应于缓存中的一个key值,即需要在缓存中存入一个key值为cookie值的user。

步骤5:返回成功信息。

密码工具类:

package com.miaosha.util;

import org.apache.commons.codec.digest.DigestUtils;

public class MD5Util {

public static String md5(String src) {  
    return DigestUtils.md5Hex(src);  
}

private static final String salt = "1a2b3c4d";

public static String inputPassToFormPass(String inputPass) {  
    String str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);  
    System.out.println(str);  
    return md5(str);  
}

public static String formPassToDBPass(String formPass, String salt) {  
    String str = ""+salt.charAt(0)+salt.charAt(2) + formPass +salt.charAt(5) + salt.charAt(4);  
    return md5(str);  
}

public static String inputPassToDbPass(String inputPass, String saltDB) {  
    String formPass = inputPassToFormPass(inputPass);  
    String dbPass = formPassToDBPass(formPass, saltDB);  
    return dbPass;  
}  

}

MD5Util

随机COOKIE工具类:

package com.miaosha.util;

import java.util.UUID;

public class UUIDUtil {

public static String uuid() {  
    return UUID.randomUUID().toString().replace("-", "");  
}  

}

UUIDUtil

登录的控制类

package com.miaosha.controller;

import com.miaosha.entity.vo.LoginVO;
import com.miaosha.entity.vo.result.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.validation.Valid;

@Controller
@RequestMapping("/login")
public class LoginController {

private static Logger log = LoggerFactory.getLogger(LoginController.class);

@Autowired  
MiaoshaUserService userService;  
//去登录页面  
@RequestMapping("/to\_login")  
public String toLogin(){

    return "login";

}

//执行登录  
@RequestMapping("/do\_login")  
@ResponseBody  
public Result<String> doLogin(@Valid LoginVO loginVO){

    log.info(loginVO.toString());  
    //登录  
    String token = userService.login(response, loginVo);  
    return Result.success(token);

}

}

LoginController

属性加入MiaoshaUserService类来提供登录验证的具体方法,这里登录验证的逻辑无言多说,但对于加入cookie的方法,其作用是,在登录后,可以在客户端存一份cookie值,当跳转到其他页面后,有可能服务器的seeison值因为分布式系统而取不到用户,因此可以通过客户端存的cookie值去缓存中取。

用户登录的业务类

package com.miaosha.service;

import com.miaosha.entity.domain.MiaoshaUser;
import com.miaosha.entity.vo.LoginVO;
import com.miaosha.entity.vo.result.CodeMsg;
import com.miaosha.util.MD5Util;
import com.miaosha.util.UUIDUtil;
import com.miaosha.util.exception.GlobalException;
import com.miaosha.util.redisconfig.MyRedisTemplate;
import com.miaosha.util.redisconfig.key.MiaoshaUserKey;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;

@Service
public class MiaoshaUserService {

public static final String COOKI\_NAME\_TOKEN = "token";

@Autowired  
MiaoshaUserDao miaoshaUserDao;

@Autowired  
MyRedisTemplate redisService;

public MiaoshaUser getById(long id) {  
    //取缓存  
    MiaoshaUser user = redisService.get(MiaoshaUserKey.getById, ""+id, MiaoshaUser.class);  
    if(user != null) {  
        return user;  
    }  
    //取数据库  
    user = miaoshaUserDao.getById(id);  
    if(user != null) {  
        redisService.set(MiaoshaUserKey.getById, ""+id, user);  
    }  
    return user;  
}  
// http://blog.csdn.net/tTU1EvLDeLFq5btqiK/article/details/78693323  
public boolean updatePassword(String token, long id, String formPass) {  
    //取user  
    MiaoshaUser user = getById(id);  
    if(user == null) {  
        throw new GlobalException(CodeMsg.MOBILE\_NOT\_EXIST);  
    }  
    //更新数据库  
    MiaoshaUser toBeUpdate = new MiaoshaUser();  
    toBeUpdate.setId(id);  
    toBeUpdate.setPassword(MD5Util.formPassToDBPass(formPass, user.getSalt()));  
    miaoshaUserDao.update(toBeUpdate);  
    //处理缓存  
    redisService.delete(MiaoshaUserKey.getById, ""+id);  
    user.setPassword(toBeUpdate.getPassword());  
    redisService.set(MiaoshaUserKey.token, token, user);  
    return true;  
}

public MiaoshaUser getByToken(HttpServletResponse response, String token) {  
    if(StringUtils.isEmpty(token)) {  
        return null;  
    }  
    MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);  
    //延长有效期  
    if(user != null) {  
        addCookie(response, token, user);  
    }  
    return user;  
}

public String login(HttpServletResponse response, LoginVO loginVo) {  
    if(loginVo == null) {  
        throw new GlobalException(CodeMsg.SERVER\_ERROR);  
    }  
    String mobile = loginVo.getMobile();  
    String formPass = loginVo.getPassword();  
    //判断手机号是否存在  
    MiaoshaUser user = getById(Long.parseLong(mobile));  
    if(user == null) {  
        throw new GlobalException(CodeMsg.MOBILE\_NOT\_EXIST);  
    }  
    //验证密码  
    String dbPass = user.getPassword();  
    String saltDB = user.getSalt();  
    String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);  
    if(!calcPass.equals(dbPass)) {  
        throw new GlobalException(CodeMsg.PASSWORD\_ERROR);  
    }  
    //生成cookie  
    String token     = UUIDUtil.uuid();  
    addCookie(response, token, user);  
    return token;  
}

private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {  
    redisService.set(MiaoshaUserKey.token, token, user);  
    Cookie cookie = new Cookie(COOKI\_NAME\_TOKEN, token);  
    cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());  
    cookie.setPath("/");  
    response.addCookie(cookie);  
}  

}

MiaoshaUserService

用户的查询数据库的类

package com.miaosha.service;

import com.miaosha.entity.domain.MiaoshaUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

@Mapper
public interface MiaoshaUserDao {

    @Select("select \* from miaosha\_user where id = #{id}")  
    public MiaoshaUser getById(@Param("id")long id);

    @Update("update miaosha\_user set password = #{password} where id = #{id}")  
    public void update(MiaoshaUser toBeUpdate);  

}

MiaoshaUserDao

以上就是登录功能的实现,可以说我们在前期做了很多的工作,使用了很多的技术,其实就是为了在后面编程的过程中很好继续下去,这种代码的可用性很高。



秒杀功能的实现

所用的技术:页面静态化、缓存技术、RabbitMQ技术、路径隐藏、内存标记。。。

业务逻辑的分析:

第一步:登录成功后,请求服务器去返回商品列表页面,将使用thymeleaf技术将页面和数据进行交互,展示商品页面。

第二步:点击详情页面,跳转到商品的详情页面,此次跳转不经过服务器,实现页面的静态化。跳转之后,在入口函数调用ajax请求,请求返回页面展示需要的数据。

第三步:请求展示的数据有验证码的数据,但不需要返回,是通过读取内存得到的,对验证码图片进行显示。

第四步:输入验证码,并点击秒杀按钮,获取秒杀的请求服务器路径,返回路径后,请求服务器的执行秒杀功能的请求。

第五步:判断是否库存足够,并判断是否已经下单,然后将用户和商品id放入rabbitMQ队列中,等到队列出队。

第六步:队列出队执行秒杀的逻辑,减库存。下订单。

第七步:在浏览器中的返回成功逻辑中不停的请求服务器,判断是否下订单成功,如果成功,则跳转到订单详情页面。

第八步:订单详情页面的入口函数请求服务器,返回展示页面的数据。

各类实体类

首先domian包里的类要与数据库的相关联,回顾前面的数据库表,商品表、秒杀商品表、订单表、秒杀订单表。那么对应的domain的实体类应该有Goods、MiaoshaGoods、OrderInfo、MiaoshaOrder。

这一类的实体类很好创建,只要其属性和数据库字段一一对应,然后添加相应的getset方法。

package com.miaosha.entity.domain;

public class Goods {

private Long id;  
private String goodsName;  
private String goodsTitle;  
private String goodsImg;  
private String goodsDetail;  
private Double goodsPrice;  
private Integer goodsStock;

public Long getId() {  
    return id;  
}

public void setId(Long id) {  
    this.id = id;  
}

public String getGoodsName() {  
    return goodsName;  
}

public void setGoodsName(String goodsName) {  
    this.goodsName = goodsName;  
}

public String getGoodsTitle() {  
    return goodsTitle;  
}

public void setGoodsTitle(String goodsTitle) {  
    this.goodsTitle = goodsTitle;  
}

public String getGoodsImg() {  
    return goodsImg;  
}

public void setGoodsImg(String goodsImg) {  
    this.goodsImg = goodsImg;  
}

public String getGoodsDetail() {  
    return goodsDetail;  
}

public void setGoodsDetail(String goodsDetail) {  
    this.goodsDetail = goodsDetail;  
}

public Double getGoodsPrice() {  
    return goodsPrice;  
}

public void setGoodsPrice(Double goodsPrice) {  
    this.goodsPrice = goodsPrice;  
}

public Integer getGoodsStock() {  
    return goodsStock;  
}

public void setGoodsStock(Integer goodsStock) {  
    this.goodsStock = goodsStock;  
}  

}

Goods

package com.miaosha.entity.domain;

import java.util.Date;

public class MiaoshaGoods {

private Long id;  
private Long goodsId;  
private Integer stockCount;  
private Date startDate;  
private Date endDate;

public Long getId() {  
    return id;  
}

public void setId(Long id) {  
    this.id = id;  
}

public Long getGoodsId() {  
    return goodsId;  
}

public void setGoodsId(Long goodsId) {  
    this.goodsId = goodsId;  
}

public Integer getStockCount() {  
    return stockCount;  
}

public void setStockCount(Integer stockCount) {  
    this.stockCount = stockCount;  
}

public Date getStartDate() {  
    return startDate;  
}

public void setStartDate(Date startDate) {  
    this.startDate = startDate;  
}

public Date getEndDate() {  
    return endDate;  
}

public void setEndDate(Date endDate) {  
    this.endDate = endDate;  
}  

}

MiaoshaGoods

package com.miaosha.entity.domain;

import java.util.Date;

public class OrderInfo {

private Long id;  
private Long userId;  
private Long goodsId;  
private Long  deliveryAddrId;  
private String goodsName;  
private Integer goodsCount;  
private Double goodsPrice;  
private Integer orderChannel;  
private Integer status;  
private Date createDate;  
private Date payDate;

public Long getId() {  
    return id;  
}

public void setId(Long id) {  
    this.id = id;  
}

public Long getUserId() {  
    return userId;  
}

public void setUserId(Long userId) {  
    this.userId = userId;  
}

public Long getGoodsId() {  
    return goodsId;  
}

public void setGoodsId(Long goodsId) {  
    this.goodsId = goodsId;  
}

public Long getDeliveryAddrId() {  
    return deliveryAddrId;  
}

public void setDeliveryAddrId(Long deliveryAddrId) {  
    this.deliveryAddrId = deliveryAddrId;  
}

public String getGoodsName() {  
    return goodsName;  
}

public void setGoodsName(String goodsName) {  
    this.goodsName = goodsName;  
}

public Integer getGoodsCount() {  
    return goodsCount;  
}

public void setGoodsCount(Integer goodsCount) {  
    this.goodsCount = goodsCount;  
}

public Double getGoodsPrice() {  
    return goodsPrice;  
}

public void setGoodsPrice(Double goodsPrice) {  
    this.goodsPrice = goodsPrice;  
}

public Integer getOrderChannel() {  
    return orderChannel;  
}

public void setOrderChannel(Integer orderChannel) {  
    this.orderChannel = orderChannel;  
}

public Integer getStatus() {  
    return status;  
}

public void setStatus(Integer status) {  
    this.status = status;  
}

public Date getCreateDate() {  
    return createDate;  
}

public void setCreateDate(Date createDate) {  
    this.createDate = createDate;  
}

public Date getPayDate() {  
    return payDate;  
}

public void setPayDate(Date payDate) {  
    this.payDate = payDate;  
}  

}

OrderInfo

package com.miaosha.entity.domain;

public class MiaoshaOrder {

private Long id;  
private Long userId;  
private Long  orderId;  
private Long goodsId;

public Long getId() {  
    return id;  
}

public void setId(Long id) {  
    this.id = id;  
}

public Long getUserId() {  
    return userId;  
}

public void setUserId(Long userId) {  
    this.userId = userId;  
}

public Long getOrderId() {  
    return orderId;  
}

public void setOrderId(Long orderId) {  
    this.orderId = orderId;  
}

public Long getGoodsId() {  
    return goodsId;  
}

public void setGoodsId(Long goodsId) {  
    this.goodsId = goodsId;  
}  

}

MiaoshaOrder

在vo包下,分析业务逻辑,可以知道在商品列表页面,需要一个展示所有商品的属性,因此可以抽象出一个vo类:GoodsVO;在商品详情页面,可以抽象出GoodsDetailVo类,在订单详情页面也可以抽象出一个实体类:OrderDetailVo类。总结来说,一个页面对应一个vo实体类。

package com.miaosha.entity.vo;

import com.miaosha.entity.domain.Goods;

import java.util.Date;

public class GoodsVo extends Goods {

private Double miaoshaPrice;  
private Integer stockCount;  
private Date startDate;  
private Date endDate;

public Double getMiaoshaPrice() {  
    return miaoshaPrice;  
}

public void setMiaoshaPrice(Double miaoshaPrice) {  
    this.miaoshaPrice = miaoshaPrice;  
}

public Integer getStockCount() {  
    return stockCount;  
}

public void setStockCount(Integer stockCount) {  
    this.stockCount = stockCount;  
}

public Date getStartDate() {  
    return startDate;  
}

public void setStartDate(Date startDate) {  
    this.startDate = startDate;  
}

public Date getEndDate() {  
    return endDate;  
}

public void setEndDate(Date endDate) {  
    this.endDate = endDate;  
}  

}

GoodsVo

package com.miaosha.entity.vo;

import com.miaosha.entity.domain.MiaoshaUser;

public class GoodsDetailVo {

private int miaoshaStatus = 0;  
private int remainSeconds = 0;  
private GoodsVo goods ;  
private MiaoshaUser user;

public int getMiaoshaStatus() {  
    return miaoshaStatus;  
}

public void setMiaoshaStatus(int miaoshaStatus) {  
    this.miaoshaStatus = miaoshaStatus;  
}

public int getRemainSeconds() {  
    return remainSeconds;  
}

public void setRemainSeconds(int remainSeconds) {  
    this.remainSeconds = remainSeconds;  
}

public GoodsVo getGoods() {  
    return goods;  
}

public void setGoods(GoodsVo goods) {  
    this.goods = goods;  
}

public MiaoshaUser getUser() {  
    return user;  
}

public void setUser(MiaoshaUser user) {  
    this.user = user;  
}  

}

GoodsDetailVo

package com.miaosha.entity.vo;

import com.miaosha.entity.domain.OrderInfo;

public class OrderDetailVo {

private GoodsVo goods;  
private OrderInfo order;

public GoodsVo getGoods() {  
    return goods;  
}

public void setGoods(GoodsVo goods) {  
    this.goods = goods;  
}

public OrderInfo getOrder() {  
    return order;  
}

public void setOrder(OrderInfo order) {  
    this.order = order;  
}  

}

OrderDetailVo

以上就是我们所有与业务直接相关的实体类

这里我们就将数据库的字段和实体类之间做了初步的映射,利用mybaties框架,可以很轻松的将其一一对应。这在mybaties的配置文件中已经有了说明

# mybatis
mybatis.type-aliases-package=com.miaosha.entity
mybatis.configuration.map-underscore-to-camel-case=true #开启驼峰验证,对应关系
mybatis.configuration.default-fetch-size=100
mybatis.configuration.default-statement-timeout=3000
#mybatis.mapperLocations = classpath:com/imooc/miaosha/dao/*.xml

持久层操作接口

为了实现IOC原则,尽量使用注入原则,dao类可以使用接口加注解来实现,之后就只使用注入的方式创建对象,减低耦合性。

dao包中的持久层类应该与domain包下的实体类相对应,并兼顾业务逻辑。

分为两大类,一类是商品相关的,一类是订单相关的。

在商品列表页面,需要返回所有的商品列表,因此,需要将所有的GoodsVO都查询出来,包装在list里面       如 List listGoodsVo();

在业务判断的过程中也需要获取GoodsVO,经常传过来的有user和goosId。 如GoodsVo getGoodsVoByGoodsId(@Param("goodsId")long goodsId);

在执行秒杀操作的时候,需要更新秒杀商品表的库存    如int reduceStock(MiaoshaGoods g);

商品的dao如下:

package com.miaosha.dao;

import com.miaosha.entity.domain.MiaoshaGoods;
import com.miaosha.entity.vo.GoodsVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import java.util.List;

@Mapper
public interface GoodsDao {

@Select("select g.\*,mg.stock\_count, mg.start\_date, mg.end\_date,mg.miaosha\_price from miaosha\_goods mg left join goods g on mg.goods\_id = g.id")  
 List<GoodsVo> listGoodsVo();

@Select("select g.\*,mg.stock\_count, mg.start\_date, mg.end\_date,mg.miaosha\_price from miaosha\_goods mg left join goods g on mg.goods\_id = g.id where g.id = #{goodsId}")  
 GoodsVo getGoodsVoByGoodsId(@Param("goodsId")long goodsId);

@Update("update miaosha\_goods set stock\_count = stock\_count - 1 where goods\_id = #{goodsId} and stock\_count > 0")  
 int reduceStock(MiaoshaGoods g);

@Update("update miaosha\_goods set stock\_count = #{stockCount} where goods\_id = #{goodsId}")  
 int resetStock(MiaoshaGoods g);

}

GoodsDao

package com.miaosha.dao;

import com.miaosha.entity.domain.MiaoshaOrder;
import com.miaosha.entity.domain.OrderInfo;
import org.apache.ibatis.annotations.*;

@Mapper
public interface OrderDao {

@Select("select \* from miaosha\_order where user\_id=#{userId} and goods\_id=#{goodsId}")  
public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(@Param("userId")long userId, @Param("goodsId")long goodsId);

@Insert("insert into order\_info(user\_id, goods\_id, goods\_name, goods\_count, goods\_price, order\_channel, status, create\_date)values("  
        + "#{userId}, #{goodsId}, #{goodsName}, #{goodsCount}, #{goodsPrice}, #{orderChannel},#{status},#{createDate} )")  
@SelectKey(keyColumn="id", keyProperty="id", resultType=long.class, before=false, statement="select last\_insert\_id()")  
public long insert(OrderInfo orderInfo);

@Insert("insert into miaosha\_order (user\_id, goods\_id, order\_id)values(#{userId}, #{goodsId}, #{orderId})")  
public int insertMiaoshaOrder(MiaoshaOrder miaoshaOrder);

@Select("select \* from order\_info where id = #{orderId}")  
public OrderInfo getOrderById(@Param("orderId")long orderId);

@Delete("delete from order\_info")  
public void deleteOrders();

@Delete("delete from miaosha\_order")  
public void deleteMiaoshaOrders();  

}

OrderDao

商品列表页面

登录成功以后,跳转到服务器的goods/tolist请求,在此页面,获得商品的列表,然后在页面显示,这里使用了页面缓存的技术,将整个页面缓存在redis中,除了第一次请求,以后每次请求都将从缓存中直接取到html页面。

但在此之前,我们需要提供一个配置类,继承一个WebMvcConfigurerAdapter类,此类是一个在启动后就配置的类,常用方法如下:

/** 解决跨域问题 **/
public void addCorsMappings(CorsRegistry registry) ;

/** 添加拦截器 **/
void addInterceptors(InterceptorRegistry registry);

/** 这里配置视图解析器 **/
void configureViewResolvers(ViewResolverRegistry registry);

/** 配置内容裁决的一些选项 **/
void configureContentNegotiation(ContentNegotiationConfigurer configurer);

/** 视图跳转控制器 **/
void addViewControllers(ViewControllerRegistry registry);

/** 静态资源处理 **/
void addResourceHandlers(ResourceHandlerRegistry registry);

/** 默认静态资源处理器 **/
void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer);

这里是配置一个和参数有关的方法addArgumentResolvers,这是一个参数解析器,将页面传过来的参数自己去解析成想要的类型,其实在spring中很多参数是自动解析的,不需要我们配置,但有时候需要我们自己去解析这些参数。

这里的业务场景是,我们如果不传reques,就无法取到user对象,而且,如果我们的服务器时分布式的,就无法传递session值,因此我们在登录的时候,就在客户端添加了一个参数随机的cookie,并将该cookie值作为可以,进行redis缓存。

因此,我们可以利用参数解析器,将user值通过cookie来取到。

首先创建一个WebConfig类,然后继承WebMvcConfigurerAdapter前置配置类,然后实现其参数解析器的方法,并将自己定义的解析逻辑添加进去

package com.miaosha.util.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.util.List;

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{

@Autowired  
UserArgumentResolver userArgumentResolver;

@Override  
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {  
    argumentResolvers.add(userArgumentResolver);  
}

}

WebConfig

实现解析逻辑的类先继承参数解析的接口,然后添加支持MiaoshaUser类作为解析参数的方法,然后添加具体的解析逻辑,其实逻辑很简单,就是通过request取到cookie值,然后通过MiaoshaUserService的方法来从缓存中得到user值。

package com.miaosha.util.config;

import com.miaosha.entity.domain.MiaoshaUser;
import com.miaosha.service.MiaoshaUserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

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

@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver{

private MiaoshaUserService userService;

//添加支持的参数类型  
@Override  
public boolean supportsParameter(MethodParameter methodParameter) {  
    Class<?> clazz=methodParameter.getParameterType();  
    return clazz== MiaoshaUser.class;  
}

@Override  
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {

    HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);  
    HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);

    String paramToken = request.getParameter(MiaoshaUserService.COOKI\_NAME\_TOKEN);  
    String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI\_NAME\_TOKEN);  
    if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {  
        return null;  
    }  
    String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;  
    return userService.getByToken(response, token);  
}

private String getCookieValue(HttpServletRequest request, String cookiName) {  
    Cookie\[\]  cookies = request.getCookies();  
    if(cookies == null || cookies.length <= 0){  
        return null;  
    }  
    for(Cookie cookie : cookies) {  
        if(cookie.getName().equals(cookiName)) {  
            return cookie.getValue();  
        }  
    }  
    return null;  
}  

}

UserArgumentResolver

这样,就自定义了一个参数解析器,每次请求都会得到user参数

接下来需要从缓存中去html,如果有html则直接返回,加上responseBody标签,可以直接在页面显示商品列表页面。如果没有,则需要将商品的列表从数据库中取到,然后利用spring的页面解析器将页面解析好。使用的是thymeleafViewResolver.getTemplateEngine().process()方法。

package com.miaosha.controller;

import com.miaosha.entity.domain.MiaoshaUser;
import com.miaosha.entity.vo.GoodsDetailVo;
import com.miaosha.entity.vo.GoodsVo;
import com.miaosha.entity.vo.result.Result;
import com.miaosha.service.GoodsService;
import com.miaosha.service.MiaoshaUserService;
import com.miaosha.util.redisconfig.MyRedisTemplate;
import com.miaosha.util.redisconfig.key.GoodsKey;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.thymeleaf.spring4.context.SpringWebContext;
import org.thymeleaf.spring4.view.ThymeleafViewResolver;

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

@Controller
@RequestMapping("/goods")
public class GoodsController {

@Autowired  
MiaoshaUserService userService;

@Autowired  
MyRedisTemplate redisService;

@Autowired  
GoodsService goodsService;

@Autowired  
ThymeleafViewResolver thymeleafViewResolver;

@Autowired  
ApplicationContext applicationContext;

@RequestMapping(value="/to\_list", produces="text/html")  
@ResponseBody  
public String list(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user) {  
    model.addAttribute("user", user);  
    //取缓存  
    String html = redisService.get(GoodsKey.getGoodsList, "", String.class);  
    if(!StringUtils.isEmpty(html)) {  
        return html;  
    }  
    List<GoodsVo> goodsList = goodsService.listGoodsVo();  
    model.addAttribute("goodsList", goodsList);  

// return "goods_list";
SpringWebContext ctx = new SpringWebContext(request,response,
request.getServletContext(),request.getLocale(), model.asMap(), applicationContext );
//手动渲染
html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
if(!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsList, "", html);
}
System.out.println("GoodsController:"+html);
return html;
}

@RequestMapping(value="/detail/{goodsId}")  
@ResponseBody  
public Result<GoodsDetailVo> detail(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user,  
                                    @PathVariable("goodsId")long goodsId) {  
    GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);  
    long startAt = goods.getStartDate().getTime();  
    long endAt = goods.getEndDate().getTime();  
    long now = System.currentTimeMillis();  
    int miaoshaStatus = 0;  
    int remainSeconds = 0;  
    if(now < startAt ) {//秒杀还没开始,倒计时  
        miaoshaStatus = 0;  
        remainSeconds = (int)((startAt - now )/1000);  
    }else  if(now > endAt){//秒杀已经结束  
        miaoshaStatus = 2;  
        remainSeconds = -1;  
    }else {//秒杀进行中  
        miaoshaStatus = 1;  
        remainSeconds = 0;  
    }  
    GoodsDetailVo vo = new GoodsDetailVo();  
    vo.setGoods(goods);  
    vo.setUser(user);  
    vo.setRemainSeconds(remainSeconds);  
    vo.setMiaoshaStatus(miaoshaStatus);  
    return Result.success(vo);  
}  

}

GoodsController

商品详情页面

这个页面因为有秒杀的实际操作,因此是最复杂的。将商品列表展示出来之后,每个商品后面有一个详情的链接,如果点击的话,就直接跳转到商品详情页。

直接跳转的页面需要是静态页面,但我们的页面时动态的,因此需要利用页面静态化的技术,当客户端将页面加载解析完之后,通过一个入口函数,自动ajax请求,将需要的详情数据返回,然后在页面上展示出来。

$(function(){
//countDown();
getDetail();
});

function getDetail(){
var goodsId = g_getQueryString("goodsId");
$.ajax({
url:"/goods/detail/"+goodsId,
type:"GET",
success:function(data){
if(data.code == 0){
render(data.data);
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.msg("客户端请求有误");
}
});
}
function render(detail){
var miaoshaStatus = detail.miaoshaStatus;
var remainSeconds = detail.remainSeconds;
var goods = detail.goods;
var user = detail.user;
if(user){
$("#userTip").hide();
}
$("#goodsName").text(goods.goodsName);
$("#goodsImg").attr("src", goods.goodsImg);
$("#startTime").text(new Date(goods.startDate).format("yyyy-MM-dd hh:mm:ss"));
$("#remainSeconds").val(remainSeconds);
$("#goodsId").val(goods.id);
$("#goodsPrice").text(goods.goodsPrice);
$("#miaoshaPrice").text(goods.miaoshaPrice);
$("#stockCount").text(goods.stockCount);
countDown();
}
function countDown(){
var remainSeconds = $("#remainSeconds").val();
var timeout;
if(remainSeconds > 0){//秒杀还没开始,倒计时
$("#buyButton").attr("disabled", true);
$("#miaoshaTip").html("秒杀倒计时:"+remainSeconds+"秒");
timeout = setTimeout(function(){
$("#countDown").text(remainSeconds - 1);
$("#remainSeconds").val(remainSeconds - 1);
countDown();
},1000);
}else if(remainSeconds == 0){//秒杀进行中
$("#buyButton").attr("disabled", false);
if(timeout){
clearTimeout(timeout);
}
$("#miaoshaTip").html("秒杀进行中");
$("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val());
$("#verifyCodeImg").show();
$("#verifyCode").show();
}else{//秒杀已经结束
$("#buyButton").attr("disabled", true);
$("#miaoshaTip").html("秒杀已经结束");
$("#verifyCodeImg").hide();
$("#verifyCode").hide();
}
}

入口函数加载数据

ajax请求的路径是获取详情,这里需要获取的数据都已经封装到GoodsDetailVo类中了,只需要将其全部得到,然后返回即可(这里注意的是,在获取这些数据的过程中如果使用缓存技术能很大程度的提高性能。降低并发)

@RequestMapping(value="/detail/{goodsId}")
@ResponseBody
public Result detail(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user,
@PathVariable("goodsId")long goodsId) {
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
long startAt = goods.getStartDate().getTime();
long endAt = goods.getEndDate().getTime();
long now = System.currentTimeMillis();
int miaoshaStatus = 0;
int remainSeconds = 0;
if(now < startAt ) {//秒杀还没开始,倒计时 miaoshaStatus = 0; remainSeconds = (int)((startAt - now )/1000); }else if(now > endAt){//秒杀已经结束
miaoshaStatus = 2;
remainSeconds = -1;
}else {//秒杀进行中
miaoshaStatus = 1;
remainSeconds = 0;
}
GoodsDetailVo vo = new GoodsDetailVo();
vo.setGoods(goods);
vo.setUser(user);
vo.setRemainSeconds(remainSeconds);
vo.setMiaoshaStatus(miaoshaStatus);
return Result.success(vo);
}

detail

秒杀逻辑

点击秒杀按钮,然后不是直接执行秒杀逻辑,而是需要去服务器请求一个路径,(为了在秒杀前隐藏接口,防止盗刷),然后将返回的接口进行拼装,然后请求这个随机的路径,执行真正的秒杀逻辑。

首先判断库存是否大于0,然后判断是否已经下单(不能重复秒杀),当这些都达到条件的时候,可以将user和商品id封装到一个类中,传到RabbitMQ队列中。然后从接收的队列中取。这样就会极大的缓减了网站的并发量,从出队中取到值后,再进行判断库存和订单,然后再执行减库存和下订单的操作。

当完成下订单的操作之后,秒杀逻辑其实已经做完了,但需要将数据返回到页面去跳转到订单的详情页面,这就需要在返回成功的逻辑上设置一个轮询函数,不停的请求服务器,看是否已经下了订单,当下了订单之后,就可以直接跳转到订单详情页面。

秒杀验证码,该验证码是直接向内存中拿到的图片,但需要我们服务器传回到内存中

$("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val());
$("#verifyCodeImg").show();

验证码生成逻辑:

页面发出对服务器的请求,请求一张图片,而服务器将图片生成后,写入到内存中,被浏览器取到。

package com.miaosha.controller;

import com.miaosha.entity.domain.MiaoshaUser;
import com.miaosha.entity.vo.result.CodeMsg;
import com.miaosha.entity.vo.result.Result;
import com.miaosha.service.MiaoshaService;
import com.miaosha.service.MiaoshaUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.OutputStream;

@Controller
@RequestMapping("/miaosha")
public class MiaoshaController {

@Autowired  
private MiaoshaService miaoshaService;

@RequestMapping(value="/verifyCode", method=RequestMethod.GET)  
@ResponseBody  
public Result<String> getMiaoshaVerifyCod(HttpServletResponse response, MiaoshaUser user,  
                                          @RequestParam("goodsId")long goodsId) {  
    if(user == null) {  
        return Result.error(CodeMsg.SESSION\_ERROR);  
    }  
    System.out.println("请求进来了");  
    try {  
        BufferedImage image  = miaoshaService.createVerifyCode(user, goodsId);  
        OutputStream out = response.getOutputStream();  
        ImageIO.write(image, "JPEG", out);  
        out.flush();  
        out.close();  
        return null;  
    }catch(Exception e) {  
        e.printStackTrace();  
        return Result.error(CodeMsg.MIAOSHA\_FAIL);  
    }  
}

// @RequestMapping(value="/path", method= RequestMethod.GET)
// @ResponseBody
// public Result getMiaoshaPath(HttpServletRequest request, MiaoshaUser user,
// @RequestParam("goodsId")long goodsId,
// @RequestParam(value="verifyCode", defaultValue="0")int verifyCode
// ) {
// if(user == null) {
// return Result.error(CodeMsg.SESSION_ERROR);
// }
// boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode);
// if(!check) {
// return Result.error(CodeMsg.REQUEST_ILLEGAL);
// }
// String path =miaoshaService.createMiaoshaPath(user, goodsId);
// return Result.success(path);
// }

}

MiaoshaController

package com.miaosha.service;

import com.miaosha.entity.domain.MiaoshaUser;
import com.miaosha.util.redisconfig.MyRedisTemplate;
import com.miaosha.util.redisconfig.key.MiaoshaKey;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

@Service
public class MiaoshaService {

@Autowired  
private MyRedisTemplate redisService;

public BufferedImage createVerifyCode(MiaoshaUser user, long goodsId) {  
    if(user == null || goodsId <=0) {  
        return null;  
    }  
    int width = 80;  
    int height = 32;  
    //create the image  
    BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE\_INT\_RGB);  
    Graphics g = image.getGraphics();  
    // set the background color  
    g.setColor(new Color(0xDCDCDC));  
    g.fillRect(0, 0, width, height);  
    // draw the border  
    g.setColor(Color.black);  
    g.drawRect(0, 0, width - 1, height - 1);  
    // create a random instance to generate the codes  
    Random rdm = new Random();  
    // make some confusion  
    for (int i = 0; i < 50; i++) {  
        int x = rdm.nextInt(width);  
        int y = rdm.nextInt(height);  
        g.drawOval(x, y, 0, 0);  
    }  
    // generate a random code  
    String verifyCode = generateVerifyCode(rdm);  
    g.setColor(new Color(0, 100, 0));  
    g.setFont(new Font("Candara", Font.BOLD, 24));  
    g.drawString(verifyCode, 8, 24);  
    g.dispose();  
    //把验证码存到redis中  
    int rnd = calc(verifyCode);  
    redisService.set(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId, rnd);  
    //输出图片  
    return image;  
}

private static char\[\] ops = new char\[\] {'+', '-', '\*'};  
/\*\*  
 \* + - \*  
 \* \*/  
private String generateVerifyCode(Random rdm) {  
    int num1 = rdm.nextInt(10);  
    int num2 = rdm.nextInt(10);  
    int num3 = rdm.nextInt(10);  
    char op1 = ops\[rdm.nextInt(3)\];  
    char op2 = ops\[rdm.nextInt(3)\];  
    String exp = ""+ num1 + op1 + num2 + op2 + num3;  
    return exp;  
}

private static int calc(String exp) {  
    try {  
        ScriptEngineManager manager = new ScriptEngineManager();  
        ScriptEngine engine = manager.getEngineByName("JavaScript");  
        return (Integer)engine.eval(exp);  
    }catch(Exception e) {  
        e.printStackTrace();  
        return 0;  
    }  
}  

}

MiaoshaService

当页面根据图片输入验证码之后,点击立刻秒杀按钮,不是立刻执行秒杀逻辑,而是去请求一个随机的秒杀路径。

MiaoshaController类中的方法,得到随机的秒杀路径

@RequestMapping(value="/path", method= RequestMethod.GET)
@ResponseBody
public Result getMiaoshaPath(HttpServletRequest request, MiaoshaUser user,
@RequestParam("goodsId")long goodsId,
@RequestParam(value="verifyCode", defaultValue="0")int verifyCode
) {
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode);
if(!check) {
return Result.error(CodeMsg.REQUEST_ILLEGAL);
}
String path =miaoshaService.createMiaoshaPath(user, goodsId);
return Result.success(path);
}

getMiaoshaPath

其中调用两个方法,MiaoshaService中

public boolean checkVerifyCode(MiaoshaUser user, long goodsId, int verifyCode) {
if(user == null || goodsId <=0) {
return false;
}
Integer codeOld = redisService.get(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId, Integer.class);
if(codeOld == null || codeOld - verifyCode != 0 ) {
return false;
}
redisService.delete(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId);
return true;
}

public String createMiaoshaPath(MiaoshaUser user, long goodsId) {  
    if(user == null || goodsId <=0) {  
        return null;  
    }  
    String str = MD5Util.md5(UUIDUtil.uuid()+"123456");  
    redisService.set(MiaoshaKey.getMiaoshaPath, ""+user.getId() + "\_"+ goodsId, str);  
    return str;  
}

checkVerifyCode and createMiaoshaPath

这样就请求到了随机的路径,并检查了验证码是否正确。

然后返回一个随机路径,页面收到之后,立刻又发起了ajax请求,请求执行秒杀逻辑,判断库存、订单,之后,将用户和商品id封装到一个实体类中,抛入RabbitMQ的入队中。

定义需要封装的BO类,MiaoshaMessage

package com.miaosha.entity.bo;

import com.miaosha.entity.domain.MiaoshaUser;

public class MiaoshaMessage {

private MiaoshaUser user;  
private long goodsId;

public MiaoshaUser getUser() {  
    return user;  
}

public void setUser(MiaoshaUser user) {  
    this.user = user;  
}

public long getGoodsId() {  
    return goodsId;  
}

public void setGoodsId(long goodsId) {  
    this.goodsId = goodsId;  
}  

}

MiaoshaMessage

然后将此类抛到消息队列中去

在此之前,需要做一些准备工作

步骤1:检查路径是否正确

public boolean checkPath(MiaoshaUser user, long goodsId, String path) {
if(user == null || path == null) {
return false;
}
String pathOld = redisService.get(MiaoshaKey.getMiaoshaPath, ""+user.getId() + "_"+ goodsId, String.class);
return path.equals(pathOld);
}

checkPath

步骤2:对内存进行标记,减少对redis的访问,将库存是否已经为0的消息存放在一个map中,在最开始去判断它,如果没有库存直接返回,有继续。这里需要MiaoshaController类中继承一个初始化接口,实现一个方法,该方法会在类加载的时候就开始执行,将库存先标记为有。

private HashMap localOverMap = new HashMap();
@Override
public void afterPropertiesSet() throws Exception {
List goodsList = goodsService.listGoodsVo();
if(goodsList == null) {
return;
}
for(GoodsVo goods : goodsList) {
redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
localOverMap.put(goods.getId(), false);
}
}

afterPropertiesSet

//内存标记,减少redis访问
boolean over = localOverMap.get(goodsId);
if(over) {
return Result.error(CodeMsg.MIAO_SHA_OVER);
}

内存标记

步骤3:预减库存,在redis中存储库存值,先减掉一个判断是否小于0,小于0,则标记内存,然后直接返回错误。

//预减库存
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//
if(stock < 0) {
localOverMap.put(goodsId, true);
return Result.error(CodeMsg.MIAO_SHA_OVER);
}

预减库存

步骤4:判断是否已经下单,若下单,则返回错误,不能重复下单,这里需要一个Order的service类,来返回从数据库中查到的结果。

package com.miaosha.service;

import com.miaosha.dao.OrderDao;
import com.miaosha.entity.domain.MiaoshaOrder;
import com.miaosha.entity.domain.OrderInfo;
import com.miaosha.util.redisconfig.MyRedisTemplate;
import com.miaosha.util.redisconfig.key.OrderKey;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

@Autowired  
private OrderDao orderDao;

@Autowired  
private MyRedisTemplate redisService;

public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {  
    //return orderDao.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);  
    return redisService.get(OrderKey.getMiaoshaOrderByUidGid, ""+userId+"\_"+goodsId, MiaoshaOrder.class);  
}

public OrderInfo getOrderById(long orderId) {  
    return orderDao.getOrderById(orderId);  
}

public void deleteOrders() {  
    orderDao.deleteOrders();  
    orderDao.deleteMiaoshaOrders();  
}  

}

OrderService

步骤5:实现入队操作,先配置RabbitMQ,配置一个队列。

package com.miaosha.util.rabbitmq;

import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static org.springframework.amqp.core.Binding.DestinationType.QUEUE;

@Configuration
public class MQConfig {
public static final String MIAOSHA_QUEUE = "miaosha.queue";

@Bean  
public Queue queue() {  
    return new Queue(MIAOSHA\_QUEUE, true);  
}  

}

MQConfig

入队函数

package com.miaosha.util.rabbitmq;

import com.miaosha.entity.bo.MiaoshaMessage;
import com.miaosha.util.redisconfig.MyRedisTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MQSender {

private static Logger log = LoggerFactory.getLogger(MQSender.class);

@Autowired  
AmqpTemplate amqpTemplate ;

public void sendMiaoshaMessage(MiaoshaMessage mm) {  
    String msg = MyRedisTemplate.beanToString(mm);  
    log.info("send message:"+msg);  
    amqpTemplate.convertAndSend(MQConfig.MIAOSHA\_QUEUE, msg);  
}  

}

MQSender

将封装好的消息转换为字符串然后发出去,发到队列中

秒杀逻辑没有结束,但doMiaosha的请求已经处理完了。

@RequestMapping(value="/{path}/do_miaosha", method=RequestMethod.POST)
@ResponseBody
public Result miaosha(Model model, MiaoshaUser user,
@RequestParam("goodsId")long goodsId,
@PathVariable("path") String path) {
model.addAttribute("user", user);
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
//验证path
boolean check = miaoshaService.checkPath(user, goodsId, path);
if(!check){
return Result.error(CodeMsg.REQUEST_ILLEGAL);
}
//内存标记,减少redis访问
boolean over = localOverMap.get(goodsId);
if(over) {
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//预减库存
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//
if(stock < 0) {
localOverMap.put(goodsId, true);
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//判断是否已经秒杀到了
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if(order != null) {
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
//入队
MiaoshaMessage mm = new MiaoshaMessage();
mm.setUser(user);
mm.setGoodsId(goodsId);
sender.sendMiaoshaMessage(mm);
return Result.success(0);//排队中

}

miaosha

入队之后,需要出队,因此,需要编写出队的类方法来执行真正的秒杀逻辑,减库存,下单

先将入队时候转换的字符串,转成封装的消息类,然后将继续判断库存,是否下单,最后执行秒杀的真正逻辑

package com.miaosha.util.rabbitmq;

import com.miaosha.entity.bo.MiaoshaMessage;
import com.miaosha.entity.domain.MiaoshaOrder;
import com.miaosha.entity.domain.MiaoshaUser;
import com.miaosha.entity.vo.GoodsVo;
import com.miaosha.service.GoodsService;
import com.miaosha.service.MiaoshaService;
import com.miaosha.service.OrderService;
import com.miaosha.util.redisconfig.MyRedisTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MQReceiver {

private static Logger log = LoggerFactory.getLogger(MQReceiver.class);

@Autowired  
private GoodsService goodsService;

@Autowired  
private OrderService orderService;

@Autowired  
private MiaoshaService miaoshaService;

@RabbitListener(queues=MQConfig.MIAOSHA\_QUEUE)  
public void receive(String message) {  
    log.info("receive message:"+message);  
    MiaoshaMessage mm  = MyRedisTemplate.stringToBean(message, MiaoshaMessage.class);  
    MiaoshaUser user = mm.getUser();  
    long goodsId = mm.getGoodsId();

    GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);  
    int stock = goods.getStockCount();  
    if(stock <= 0) {  
        return;  
    }  
    //判断是否已经秒杀到了  
    MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);  
    if(order != null) {  
        return;  
    }  
    //减库存 下订单 写入秒杀订单  
    miaoshaService.miaosha(user, goods);  
}

}

MQReceiver

真正的秒杀逻辑

package com.miaosha.service;

import com.miaosha.entity.domain.MiaoshaUser;
import com.miaosha.entity.domain.OrderInfo;
import com.miaosha.entity.vo.GoodsVo;
import com.miaosha.util.MD5Util;
import com.miaosha.util.UUIDUtil;
import com.miaosha.util.redisconfig.MyRedisTemplate;
import com.miaosha.util.redisconfig.key.MiaoshaKey;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

@Service
public class MiaoshaService {

@Autowired  
private MyRedisTemplate redisService;

@Autowired  
private GoodsService goodsService;

@Autowired  
private OrderService orderService;

public BufferedImage createVerifyCode(MiaoshaUser user, long goodsId) {  
    if(user == null || goodsId <=0) {  
        return null;  
    }  
    int width = 80;  
    int height = 32;  
    //create the image  
    BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE\_INT\_RGB);  
    Graphics g = image.getGraphics();  
    // set the background color  
    g.setColor(new Color(0xDCDCDC));  
    g.fillRect(0, 0, width, height);  
    // draw the border  
    g.setColor(Color.black);  
    g.drawRect(0, 0, width - 1, height - 1);  
    // create a random instance to generate the codes  
    Random rdm = new Random();  
    // make some confusion  
    for (int i = 0; i < 50; i++) {  
        int x = rdm.nextInt(width);  
        int y = rdm.nextInt(height);  
        g.drawOval(x, y, 0, 0);  
    }  
    // generate a random code  
    String verifyCode = generateVerifyCode(rdm);  
    g.setColor(new Color(0, 100, 0));  
    g.setFont(new Font("Candara", Font.BOLD, 24));  
    g.drawString(verifyCode, 8, 24);  
    g.dispose();  
    //把验证码存到redis中  
    int rnd = calc(verifyCode);  
    redisService.set(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId, rnd);  
    //输出图片  
    return image;  
}

private static char\[\] ops = new char\[\] {'+', '-', '\*'};  
/\*\*  
 \* + - \*  
 \* \*/  
private String generateVerifyCode(Random rdm) {  
    int num1 = rdm.nextInt(10);  
    int num2 = rdm.nextInt(10);  
    int num3 = rdm.nextInt(10);  
    char op1 = ops\[rdm.nextInt(3)\];  
    char op2 = ops\[rdm.nextInt(3)\];  
    String exp = ""+ num1 + op1 + num2 + op2 + num3;  
    return exp;  
}

private static int calc(String exp) {  
    try {  
        ScriptEngineManager manager = new ScriptEngineManager();  
        ScriptEngine engine = manager.getEngineByName("JavaScript");  
        return (Integer)engine.eval(exp);  
    }catch(Exception e) {  
        e.printStackTrace();  
        return 0;  
    }  
}

public boolean checkVerifyCode(MiaoshaUser user, long goodsId, int verifyCode) {  
    if(user == null || goodsId <=0) {  
        return false;  
    }  
    Integer codeOld = redisService.get(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId, Integer.class);  
    if(codeOld == null || codeOld - verifyCode != 0 ) {  
        return false;  
    }  
    redisService.delete(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId);  
    return true;  
}

public String createMiaoshaPath(MiaoshaUser user, long goodsId) {  
    if(user == null || goodsId <=0) {  
        return null;  
    }  
    String str = MD5Util.md5(UUIDUtil.uuid()+"123456");  
    redisService.set(MiaoshaKey.getMiaoshaPath, ""+user.getId() + "\_"+ goodsId, str);  
    return str;  
}

public boolean checkPath(MiaoshaUser user, long goodsId, String path) {  
    if(user == null || path == null) {  
        return false;  
    }  
    String pathOld = redisService.get(MiaoshaKey.getMiaoshaPath, ""+user.getId() + "\_"+ goodsId, String.class);  
    return path.equals(pathOld);  
}

@Transactional  
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {  
    //减库存 下订单 写入秒杀订单  
    boolean success = goodsService.reduceStock(goods);  
    if(success) {  
        //order\_info maiosha\_order  
        return orderService.createOrder(user, goods);  
    }else {  
        setGoodsOver(goods.getId());  
        return null;  
    }  
}

private void setGoodsOver(Long goodsId) {  
    redisService.set(MiaoshaKey.isGoodsOver, ""+goodsId, true);  
}

private boolean getGoodsOver(long goodsId) {  
    return redisService.exists(MiaoshaKey.isGoodsOver, ""+goodsId);  
}  

}

MiaoshaService

然后去处理页面发出的轮询请求

然后向页面返回了一个0,表示正在排队中,需要等待,等待的逻辑是不停的调用一个请求结果的ajax。

getMiaoshaResult

@RequestMapping(value="/result", method=RequestMethod.GET)
@ResponseBody
public Result miaoshaResult(Model model,MiaoshaUser user,
@RequestParam("goodsId")long goodsId) {
model.addAttribute("user", user);
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
long result =miaoshaService.getMiaoshaResult(user.getId(), goodsId);
return Result.success(result);
}

miaoshaResult

public long getMiaoshaResult(Long userId, long goodsId) {
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
if(order != null) {//秒杀成功
return order.getOrderId();
}else {
boolean isOver = getGoodsOver(goodsId);
if(isOver) {
return -1;
}else {
return 0;
}
}
}

getMiaoshaResult

这三个函数就是查询订单数据库是否已经下单,若是下单就是秒杀成功了,跳转到订单详情页面,若没有则继续轮询。同时,从缓存中查询秒杀是否已经结束,若结束则返回已结束。

但没有添加防盗刷的功能,需要判断一个用户发起了几次请求,比如一个用户在五秒内只能发起5次请求,防止一些人使用机器去刷,因此,需要判断请求次数,如果请求超过了五次,就返回错误消息。

使用拦截器,自定义一个注解来方便的配置这些信息。

步骤1:定义一个注解,可以配置时间,次数,和是否必须登录

package com.miaosha.util.access;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Retention(RUNTIME)
@Target(METHOD)
public @interface AccessLimit {

int seconds();  
int maxCount();  
boolean needLogin() default true;  

}

AccessLimit

步骤2:实现注解的逻辑

该逻辑类继承一个拦截器的抽象类,然后得到上述的注解,并得到用户,这里的用户使用cookie来得到,然后用本地线程将用户类绑定,定义得到用户和保存用户的方法,这样是线程安全的,并将从cookie中得到的用户,和当地线程绑定,这样就可以直接获取了。(有了当地线程,可以不用之前写的参数解析器得到的用户了。)然后在redis中使用一个专属的key来存储时间限制,并当每次请求都自增,这样就可以实现注解的功能了。

本地线程绑定用户类

package com.miaosha.util.access;

import com.miaosha.entity.domain.MiaoshaUser;

public class UserContext {

private static ThreadLocal<MiaoshaUser> userHolder = new ThreadLocal<MiaoshaUser>();

public static void setUser(MiaoshaUser user) {  
    userHolder.set(user);  
}

public static MiaoshaUser getUser() {  
    return userHolder.get();  
}  

}

UserContext

package com.miaosha.util.access;

import com.alibaba.fastjson.JSON;
import com.miaosha.entity.domain.MiaoshaUser;
import com.miaosha.entity.vo.result.CodeMsg;
import com.miaosha.entity.vo.result.Result;
import com.miaosha.service.MiaoshaUserService;
import com.miaosha.util.redisconfig.MyRedisTemplate;
import com.miaosha.util.redisconfig.key.AccessKey;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;

public class AccessInterceptor extends HandlerInterceptorAdapter {

@Autowired  
MiaoshaUserService userService;

@Autowired  
MyRedisTemplate redisService;

@Override  
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)  
        throws Exception {  
    if(handler instanceof HandlerMethod) {  
        MiaoshaUser user = getUser(request, response);  
        UserContext.setUser(user);  
        HandlerMethod hm = (HandlerMethod)handler;  
        AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);  
        if(accessLimit == null) {  
            return true;  
        }  
        int seconds = accessLimit.seconds();  
        int maxCount = accessLimit.maxCount();  
        boolean needLogin = accessLimit.needLogin();  
        String key = request.getRequestURI();  
        if(needLogin) {  
            if(user == null) {  
                render(response, CodeMsg.SESSION\_ERROR);  
                return false;  
            }  
            key += "\_" + user.getId();  
        }else {  
            //do nothing  
        }  
        AccessKey ak = AccessKey.withExpire(seconds);  
        Integer count = redisService.get(ak, key, Integer.class);  
        if(count  == null) {  
            redisService.set(ak, key, 1);  
        }else if(count < maxCount) {  
            redisService.incr(ak, key);  
        }else {  
            render(response, CodeMsg.ACCESS\_LIMIT\_REACHED);  
            return false;  
        }  
    }  
    return true;  
}

private void render(HttpServletResponse response, CodeMsg cm)throws Exception {  
    response.setContentType("application/json;charset=UTF-8");  
    OutputStream out = response.getOutputStream();  
    String str  = JSON.toJSONString(Result.error(cm));  
    out.write(str.getBytes("UTF-8"));  
    out.flush();  
    out.close();  
}

private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) {  
    String paramToken = request.getParameter(MiaoshaUserService.COOKI\_NAME\_TOKEN);  
    String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI\_NAME\_TOKEN);  
    if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {  
        return null;  
    }  
    String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;  
    return userService.getByToken(response, token);  
}

private String getCookieValue(HttpServletRequest request, String cookiName) {  
    Cookie\[\]  cookies = request.getCookies();  
    if(cookies == null || cookies.length <= 0){  
        return null;  
    }  
    for(Cookie cookie : cookies) {  
        if(cookie.getName().equals(cookiName)) {  
            return cookie.getValue();  
        }  
    }  
    return null;  
}  

}

AccessInterceptor

然后配置拦截器

package com.miaosha.util.config;

import com.miaosha.util.access.AccessInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.util.List;

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{

@Autowired  
UserArgumentResolver userArgumentResolver;

@Autowired  
AccessInterceptor accessInterceptor;

@Override  
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {  
    argumentResolvers.add(userArgumentResolver);  
}

@Override  
public void addInterceptors(InterceptorRegistry registry) {  
    registry.addInterceptor(accessInterceptor);  
}

}

WebConfig

到此,所有的秒杀功能都已经结束。



订单详情页面的展示

当轮询函数请求到了,返回参数为1,说明已经秒杀成功,则直接跳转到订单的详情页面,然后通过入口函数,将需要展示的数据请求过来。

需要订单的业务类

package com.miaosha.service;

import com.miaosha.dao.OrderDao;
import com.miaosha.entity.domain.MiaoshaOrder;
import com.miaosha.entity.domain.MiaoshaUser;
import com.miaosha.entity.domain.OrderInfo;
import com.miaosha.entity.vo.GoodsVo;
import com.miaosha.util.redisconfig.MyRedisTemplate;
import com.miaosha.util.redisconfig.key.OrderKey;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;

@Service
public class OrderService {

@Autowired  
private OrderDao orderDao;

@Autowired  
private MyRedisTemplate redisService;

public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {  
    return orderDao.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);  

// return redisService.get(OrderKey.getMiaoshaOrderByUidGid, ""+userId+"_"+goodsId, MiaoshaOrder.class);
}

public OrderInfo getOrderById(long orderId) {  
    return orderDao.getOrderById(orderId);  
}

public void deleteOrders() {  
    orderDao.deleteOrders();  
    orderDao.deleteMiaoshaOrders();  
}

@Transactional  
public OrderInfo createOrder(MiaoshaUser user, GoodsVo goods) {  
    OrderInfo orderInfo = new OrderInfo();  
    orderInfo.setCreateDate(new Date());  
    orderInfo.setDeliveryAddrId(0L);  
    orderInfo.setGoodsCount(1);  
    orderInfo.setGoodsId(goods.getId());  
    orderInfo.setGoodsName(goods.getGoodsName());  
    orderInfo.setGoodsPrice(goods.getMiaoshaPrice());  
    orderInfo.setOrderChannel(1);  
    orderInfo.setStatus(0);  
    orderInfo.setUserId(user.getId());  
    orderDao.insert(orderInfo);  
    MiaoshaOrder miaoshaOrder = new MiaoshaOrder();  
    miaoshaOrder.setGoodsId(goods.getId());  
    miaoshaOrder.setOrderId(orderInfo.getId());  
    miaoshaOrder.setUserId(user.getId());  
    orderDao.insertMiaoshaOrder(miaoshaOrder);

    redisService.set(OrderKey.getMiaoshaOrderByUidGid, ""+user.getId()+"\_"+goods.getId(), miaoshaOrder);

    return orderInfo;  
}  

}

OrderService

然后有一个订单的控制器,设置页面的请求路径

package com.miaosha.controller;

import com.miaosha.entity.domain.MiaoshaUser;
import com.miaosha.entity.domain.OrderInfo;
import com.miaosha.entity.vo.GoodsVo;
import com.miaosha.entity.vo.OrderDetailVo;
import com.miaosha.entity.vo.result.CodeMsg;
import com.miaosha.entity.vo.result.Result;
import com.miaosha.service.GoodsService;
import com.miaosha.service.MiaoshaUserService;
import com.miaosha.service.OrderService;
import com.miaosha.util.redisconfig.MyRedisTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

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

@Autowired  
OrderService orderService;

@Autowired  
GoodsService goodsService;

@RequestMapping("/detail")  
@ResponseBody  
public Result<OrderDetailVo> info(Model model, MiaoshaUser user,  
                                  @RequestParam("orderId") long orderId) {  
    if(user == null) {  
        return Result.error(CodeMsg.SESSION\_ERROR);  
    }  
    OrderInfo order = orderService.getOrderById(orderId);  
    if(order == null) {  
        return Result.error(CodeMsg.ORDER\_NOT\_EXIST);  
    }  
    long goodsId = order.getGoodsId();  
    GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);  
    OrderDetailVo vo = new OrderDetailVo();  
    vo.setOrder(order);  
    vo.setGoods(goods);  
    return Result.success(vo);  
}  

}

OrderController

到此,整个秒杀项目完成。

#