高并发秒杀API(五)

前言

本篇将完成前端页面的设计与开发,包括:

  • 使用Bootstrap开发页面结构
  • 交互逻辑编程

一、使用Bootstrap开发页面结构

在设计SeckillController中我们已经设置了jsp文件的路径,在/WEB-INF/新建一个jsp目录,在该目录下新建list.jsp和detail.jsp

使用Bootstrap的模板,这个模板基本上是固定的

<%@ page language="java" contentType="text/html; charset=UTF-8" %>


   
      Bootstrap 模板
      
      
      
 
      
      
      
   
   
      

Hello, world!

1、list.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" %>

<%@ include file="common/tag.jsp" %>


   
      秒杀列表页
      <%@ include file="common/head.jsp" %>
   
   
   
   


     



在最上面的jsp内置对象page中的contentType修改为UTF-8,这个模板已经引入了一些文件包含了 jquery.js、bootstrap.min.js 和 bootstrap.min.css 文件,用于让一个常规的 HTML 文件变为使用了Bootstrap的模板

最下面有两个script标签,通过CDN加载一些Bootstrap资源,** JavaScript有一个先后引入规则,jQuery作为Bootstrap的底层依赖,要先于Bootstrap声明 **,这两个script标签在上面介绍的网站上都有

这里有些通用的标签以及要引入的文件都单独提取出来,不用把这些相同的代码都写在每一个页面中

在jsp目录下新建一个common目录,专门存放通用的jsp文件

新建一个tag.jsp,用于引入jstl,如果以后还要引入别的标签,再添加

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>

新建一个head.jsp,head标签中的内容所有页面基本都一样




 



然后使用jsp的内置对象include,静态引入head.jsp,** 静态包含 是会把引入的文件合并过来 ,也就是head.jsp中的内容会放到外层list.jsp中作为一个Servlet输出,如果是 动态包含 的话,那么head.jsp会作为一个 独立的jsp,先转换为Servlet **,转换后的结果再和list.jsp合并

接着开始编写lsit.jsp的细节部分

高并发秒杀API(五)_第1张图片
list.jsp

panel-default、 text-center都是使用Bootstrap提供的样式

在panel-body中使用表格,通过jstl提供的方法来显示要展示的秒杀商品


    
        名称
        库存
        开始时间
        结束时间
        创建时间
        详情页
    


    
     
        ${sk.name}
        ${sk.number}
        
                         
        
        
                           
        
        
                            
        
        
            link             
        
     
    

首先使用jstl的c:forEach标签,用来迭代从SeckillController中的list方法传过来的"list",这个list是存放秒杀的商品,属性var代表当前项目的变量名,items表示进行循环的项目

一个tr标签是一行,每个td标签是一列,数据库有多少个秒杀商品这个表格就有多少行

@RequestMapping(value = "/list", method = RequestMethod.GET)
public String list(Model model){
        
    //获取列表页
    List list = seckillService.getSeckillList();
    model.addAttribute("list", list);
    return "list";
        
}

从SeckillController的list方法返回的是字符串,但是之前说过,Spring MVC会拼接成一个URL地址,返回的数据是个泛型,类型是Seckill

public class Seckill {
    
    private long seckillId;
    
    private String name;
    
    private int number;
    
    private Date startTime;
    
    private Date endTime;
    
    private Date createTime;
}

这是Seckill定义的属性,所以在list.jsp页面中通过sk.name来调用相关的参数

日期类型的输出默认是直接调用日期类型的toString,这不符合我们的规范,所以使用jstl的fmt:formatDate标签来格式化输出的时间

最后一列给一个超链接,用于链接这个秒杀商品的详情页,可以把这个超链接做成一个按钮,使用的也是Bootstrap的CSS

2、detail.jsp

高并发秒杀API(五)_第2张图片
detail.jsp

这是detail.jsp的一个大的框架,先是由两个div组成,一个用于显示日期或者文本的一个显示面板,在显示面板中做一个埋点,因为这个面板在之后的交互逻辑编码中,在不同时间显示的是不同的内容

${seckill.name }

这里可以直接这样写的原因是:

model.addAttribute("seckill", seckill);//SeckillController中的detail方法

另一个div就是登录弹出层,在进入详情页的时候,会通过Cookie判断用户时候登录,没有登录的用户的页面会显示这个登录弹出层,提示用户登录

高并发秒杀API(五)_第3张图片
detail.jsp中的登录弹出层

首先在最外围的div中进行埋点

因为这个登录弹出层不是每次用户到详情页都要出现,只有验证Cookie中没有用户登录信息才会出现,所以在这里埋点,如果Cookie中有用户的信息,在交互逻辑中我们会控制这个div不出现

登录弹出层实际是一个模态框,在页面显示的时候主要由三个部分:

  • modal-header:显示一些文本
  • modal-body:用户输入登录信息
  • modal-footer:登录按钮

在modal-header中有个span面板用于显示一些文本和图标


在modal-body中有一个输入框,这里需要在输入框中进行埋点,之后的交互逻辑要通过这个埋点来获取用户输入的信息


在modal-footer中由两部分组成:

  • span:显示错误信息
  • button:登录按钮

在button中也需要埋点,用于绑定点击事件

body标签中的内容完成了,下面也要通过CDN引入一些文件



     








jquery文件和bootstrap.min.js之前在list.jsp也引入了

对Cookie的操作使用jQuery Cookie插件,倒计时使用jQuery的countDown插件

2、交互逻辑

1、交互流程

高并发秒杀API(五)_第4张图片
前端页面交互流程

当用户点击某一个秒杀商品的按钮的时候,会进入到相应的详情页,这个详情页会判断用户是否登录过,如果登录过就展示详情页页面,如果没有登录过,就弹出登录弹出层,在用户正确填写登录信息后就可以进入详情页

高并发秒杀API(五)_第5张图片
详情页流程
  • 获取标准系统时间,因为用户可能处在不同的时区,用户终端的时间也不可能完全一致,所以要统一地采用一个标准时间,也就是服务器时间

  • 通过秒杀商品的开始时间和结束时间来做出不同的判断:

    • 系统时间大于结束时间:秒杀活动已结束,在detail.jsp的显示面板显示“秒杀结束”字样
    • 系统时间小于开始时间:秒杀活动未开始,在detail.jsp的显示面板显示倒计时,使用的是jQuery的countDown插件,倒计时完成后,会出现秒杀按钮,用户可以执行秒杀操作
    • 系统时间介于开始时间和结束时间之间:秒杀活动正在进行,直接出现秒杀按钮,用户可以执行秒杀操作

2、页面展示

高并发秒杀API(五)_第6张图片
列表页
高并发秒杀API(五)_第7张图片
登录弹出层
高并发秒杀API(五)_第8张图片
可以秒杀
高并发秒杀API(五)_第9张图片
秒杀结束
高并发秒杀API(五)_第10张图片
秒杀未开始

3、交互逻辑编程

在src/main/webapp目录下新建一个resources文件夹,再在其中新建一个script文件夹,用于存放脚本文件

创建一个seckill.js


高并发秒杀API(五)_第11张图片
seckill.js

这是最后完成的总览,接着一步步来,整个seckil这样写的原因是模拟高级语言分包的概念,使JavaScript模块化,这样当调用一个方法可以用seckill.detail.init(params)的形式

在详情页初始化中,首先要做的就是获取killPhone节点,这个killPhone节点不是程序中具体的标签,而是Cookie中的用于标识用户信息的数据,用户的信息都放在Cookie中名为killPhone的节点

//在cookie中查找手机号
var killPhone = $.cookie('killPhone');
//验证手机号
if(!seckill.validatePhone(killPhone)){
    var killPhoneModal = $('#killPhoneModal');
    killPhoneModal.modal({
        show : true,//显示登录弹出层
        backdrop : 'static',//禁止位置关闭
        keyboard : false//关闭键盘事件
    });
    $('#killPhoneBtn').click(function(){
        var inputPhone = $('#killPhoneKey').val();
        if(seckill.validatePhone(inputPhone)){
            $.cookie('killPhone', inputPhone, {expires:7, path:'/seckill'});//手机号写入cookie
            window.location.reload();//刷新页面
        }else{
            $('#killPhoneMessage').hide().html('').show(300);
        }
    });
}

从Cookie的killPhone中获取数据后,就要验证手机号,验证手机号的逻辑建议提取到更上层,因为可能多个地方都要用到

创建一个函数,名字为validatePhone,这个函数的位置在这一节最开始的图片上可以看到

//验证手机号
validatePhone : function(phone){
    if(phone && phone.length == 11 && !isNaN(phone)){
        return true;
    }else{
        return false;
    }
},

要验证手机号,所以传入一个手机号的参数,这里使用if语句简单的判断一下

首先要判断手机号是否为空,在js中直接传入参数,它会判断这个参数是否为空,空的话就是undefine,就认为是false

手机号长度必须为11位

isNaN是判断这个参数是否是非数字,如果是非数字的话就是true,所以这里要取反

接着就可以在init方法中调用validatePhone函数来验证手机号

if(!seckill.validatePhone(killPhone)){
    var killPhoneModal = $('#killPhoneModal');
    killPhoneModal.modal({
        show : true,//显示登录弹出层
        backdrop : 'static',//禁止位置关闭
        keyboard : false//关闭键盘事件
    });

如果手机号存在,就可以直接跳转到详情页了,所以这里处理手机号不存在的情况,因为这个if语句中东西比较多,所以分开来说,完整的代码在前面已经展示过了

手机号不存在,就需要用户进行绑定,之前在detail.jsp中也提前做好了一个登录弹出层,并进行了埋点

高并发秒杀API(五)_第12张图片
登录弹出层

id为killPhoneModal,在seckill.js中使用jQuery的选择器可以取到这个节点

var killPhoneModal = $('#killPhoneModal');

这个登录弹出层已经不是单纯的div了,因为使用了Bootstrap的modal,它本身有一个modal的方法,向这个方法传入json, 用于设置这个模态框的一些属性

之前在detail.jsp中这个modal的属性为fade,是隐藏的,既然要让用户绑定手机号,所以要把这个弹出层显示出来

killPhoneModal.modal({
    show : true,//显示登录弹出层
    backdrop : 'static',//禁止位置关闭
    keyboard : false//关闭键盘事件

我们希望在用户没有正确的填写手机号之前,是不能关掉这个弹出层,所以把backdrop关掉,因为用户点击其他区域可能把这个弹出层关掉;通过键盘的ESC也可能关闭弹出层,所以要禁止键盘事件

弹出层显示出来后,要给按钮做事件绑定

    $('#killPhoneBtn').click(function(){
        var inputPhone = $('#killPhoneKey').val();
        if(seckill.validatePhone(inputPhone)){
            $.cookie('killPhone', inputPhone, {expires:7, path:'/seckill'});//手机号写入cookie
            window.location.reload();//刷新页面
        }else{
            $('#killPhoneMessage').hide().html('').show(300);
        }
    });
}

按钮事件绑定完成后整个验证手机号的if语句才完成了

对按钮做绑定,首先就是要获取到按钮在详情页的节点


可以看到,按钮的节点为killPhoneBtn

当用户点击了按钮,我们认为用户已经填写了在登录弹出层的input


在input中,之前已经提前进行了埋点,id为killPhoneKey

在seckill.js中获取到这个节点,同时使用val()方法获取到用户输入的内容

var inputPhone = $('#killPhoneKey').val();

拿到用户输入的内容,还要再进行验证,再调用用于验证手机号的函数validatePhone

if(seckill.validatePhone(inputPhone)){
     $.cookie('killPhone', inputPhone, {expires:7, path:'/seckill'});//手机号写入cookie
     window.location.reload();//刷新页面
}else{
     $('#killPhoneMessage').hide().html('').show(300);
}

如果验证通过了,先将inputPhone的值也就是用户输入的手机号写入Cookie中

  • expires:Cookie的有效期,单位是“天”
  • path:给出有效路径,Cookie只在该路径下有效

为什么path不写全路径?
因为当一些URL没有用到这个Cookie的时候,如果把Cookie中的path设置为全路径,那么这个Cookie中的数据也会传递到后端,对后端处理会有一些影响,所以这只这个killPhone只在seckill模块下有效

然后就是刷新页面,会重新调用detail属性的init方法

如果验证没有通过,在detail.jsp中登录弹出层的modal-footer提前预留了一个span,用于显示错误信息


同样,在seckill.js中获取到这个span节点

$('#killPhoneMessage').hide().html('').show(300);

对html标签进行操作的时候,通常是先隐藏一下,避免用户看到中间过程,然后插入一些内容,显示的时候给一个时间,单位毫秒,这样看起来有动态的效果

插入的是label标签,使用Bootstrap的CSS,这里显示的文本没有经过处理,直接是写死了,实际的工作中这里应该是要配合前端的数据字典,根据不同的情况显示不同的文本

至此,详情页初始化部分完成,也就是开头的if语句

整个前端的流程基本完成


高并发秒杀API(五)_第13张图片
前端页面交互流程

接着是详情页的流程


高并发秒杀API(五)_第14张图片
详情页流程

首先就是要获取标准系统时间

所以在detail.jsp的最下面添加一些内容,首先是要引入seckill.js



然后使用EL表达式传入参数


接着在seckill.js中获取到这些参数

//已经登录
//计时交互逻辑
var startTime = parseInt(params['startTime']);
var endTime = parseInt(params['endTime']);
var seckillId = parseInt(params['seckillId']);
$.get(seckill.URL.now(), {}, function(result){
    if(result && result['success']){
        var nowTime = result['data'];
        //时间判断,计时交互
        seckill.countdown(seckillId, nowTime, startTime, endTime);
    }else{
        console.log('result: ' + result);
    }
});

** 这里从列表页传递过来的日期参数需要转型,否则之后会出现日期无效的情况 **

然后通过ajax请求来获取到系统当前时间

@RequestMapping(value = "/time/now", method = RequestMethod.GET)
@ResponseBody
public SeckillResult time(){
    Date now = new Date();
    return new SeckillResult(true, now.getTime());
}

在SeckillController中的time方法就是用来获取系统时间的,在@RequestMapping注解中显示系统当前时间的URL是“/time/now”,限制了请求方式为GET,所以在seckill.js中使用$.get()方法

简单说下$.get()方法

$.get(URL,data,function(data,status,xhr),dataType)

  • URL:必需,规定您需要请求的 URL
  • data:可选,规定连同请求发送到服务器的数据
  • function(data,status,xhr):可选,规定当请求成功时运行的函数
    • data:包含来自请求的结果数据
    • status:包含请求的状态("success"、"notmodified"、"error"、"timeout"、"parsererror")
    • xhr:包含 XMLHttpRequest 对象
  • dataType:可选,规定预期的服务器响应的数据类型,默认地,jQuery 会智能判断。
    可能的类型:
    • xml - 一个 XML 文档
    • html - HTML 作为纯文本
    • text - 纯文本字符串
    • script - 以 JavaScript 运行响应,并以纯文本返回
    • json - 以 JSON 运行响应,并以 JavaScript 对象返回
    • jsonp - 使用 JSONP 加载一个 JSON 块,将添加一个 "?callback=?" 到 URL 来规定回调
$.get(seckill.URL.now, {}, function(result){
    if(result && result['success']){
        var nowTime = result['data'];
        //时间判断,计时交互
        seckill.countdown(seckillId, nowTime, startTime, endTime);
    }else{
        console.log('result: ' + result);
    }
});

第一个参数是请求的URL,由于URL太多,为了后期维护、代码的整洁,所以要对URL进行统一的管理,在seckill中新建一个属性URL,用于封装秒杀相关ajax的URL

//封装秒杀相关ajax的URL 
URl : {
    now : function(){
        return '/seckill/time/now';
    }
},

在SeckillController中的time方法返回的是SeckillResult类型的对象

public class SeckillResult {
    
    private boolean success;
    
    private T data;
    
    private String error;
}

这是SeckillResult中定义的属性,其中success是判断是否成功请求,所以在$.get()方法的回调函数中要判断请求是否为空,如果不为空,则在控制台输出信息

if(result && result['success']){
   var nowTime = result['data'];
   //时间判断,计时交互
   seckill.countdown(seckillId, nowTime, startTime, endTime);
}else{
   console.log('result: ' + result);
}

如果请求成功,就可以获取到系统当前时间,再加上之前获取到的三个参数,就可以进行时间判断,判断系统当前时间在不在秒杀活动期内,如果不在是秒杀未开始还是秒杀已结束

在seckill中创建countdown函数,用于时间判断

countdown : function(seckillId, nowTime, startTime, endTime){
    var seckillBox = $('#seckill-box');
    //时间判断
    if(nowTime > endTime){
        //秒杀结束
        seckillBox.html('秒杀结束!');
    }else if(nowTime < startTime){
        //秒杀未开始,计时事件绑定
        var killTime = new Date(startTime + 1000);//设置基准时间
        seckillBox.countdown(killTime, function(event){
            //时间格式
            var format = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒');
            //时间完成后回调事件
        }).on('finish.countdown', function(){
            //调用执行秒杀的函数
            seckill.handleSeckill(seckillId, seckillBox);
        });
    }else{
        //调用执行秒杀的函数
        seckill.handleSeckill(seckillId, seckillBox);
    }
},

因为对于时间判断的不同结果,要在详情页中展示不同的内容,所以在detail.jsp中专门设置了一个span,用于显示时间判断的结果

提前设置了埋点,id为seckill-box,在seckill.js通过jQuery的加载器获取到这个span节点

然后进行时间判断

if(nowTime > endTime){
    //秒杀结束
    seckillBox.html('秒杀结束!');
}

系统当前时间大于秒杀的结束时间,说明秒杀结束,这里不用和后端做通信,可以直接通过时间的判断就再详情页显示“秒杀结束”的字样,因为时间到了,不管有没有库存,都无所谓了

if(nowTime < startTime){
    //秒杀未开始,计时事件绑定
    var killTime = new Date(startTime + 1000);//设置基准时间
    seckillBox.countdown(killTime, function(event){
        //时间格式
        var format = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒');
        //时间完成后回调事件
    }).on('finish.countdown', function(){
        //调用执行秒杀的函数
        seckill.handleSeckill(seckillId, seckillBox);
    });
}

系统当前时间小于秒杀开启时间,秒杀未开始,在详情页显示倒计时,既然是倒计时,就要给系统一个基准时间,其实也就是秒杀的开启时间,但是这里在秒杀开始时间的基础+1s,防止用户端的计时偏移

接着使用Bootstrap提供的countdown方法,实际上就是一个事件绑定方法

seckillBox.countdown(killTime, function(event){
    //时间格式
    var format = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒');
    seckillBox.html(format);
    //倒计时完成后回调事件
})

countdown事件绑定方法中也有一个回调函数,当日期在不断的变化的时候,这个回调函数会做相应的输出,对日期的格式做个调整

countdown插件只是负责倒计时,倒计时完成后就可以执行秒杀操作了,所以在countdown时间绑定后再接上一个事件操作

.on('finish.countdown', function(){
    //调用执行秒杀的函数
    seckill.handleSeckill(seckillId, seckillBox);
});

事件的名字是finish.countdown,再加上一个回调函数,用于倒计时完成后回调事件,在这个函数中要调用执行秒杀的函数

这里把执行秒杀的函数单独的提取出来,一是降低耦合,二是避免代码重复,因为在最初调用时间判断函数countdown的时候,可能秒杀正在进行,而上面的代码是秒杀未开始,倒计时完成后才可以执行秒杀,在多个地方需要执行秒杀的操作,所以要把执行秒杀的操作单独创建一个函数

handleSeckill : function(seckillId, node){
    //获取秒杀地址,控制显示逻辑,执行秒杀
    node.hide()
        .html('');
    $.post(seckill.URL.exposer(seckillId), {}, function(result){
        //在回调函数中执行交互流程
        if(result && result['success']){
            var exposer = result['data'];
            if(exposer['exposed']){
                //开启秒杀,获取秒杀地址
                var md5 = exposer['md5'];
                var killUrl = seckill.URL.execution(seckillId, md5);
                console.log('killUrl: ' + killUrl);
                //绑定一次点击事件
                $('#killBtn').one('click', function(){
                    //执行秒杀请求
                    //1.禁用按钮
                    $(this).addClass('disabled');
                        
                    //2.发送秒杀请求执行秒杀
                    $.post(killUrl, {}, function(result){
                        if(result && result['success']){
                            var killResult = result['data'];
                            var state = killResult['state'];
                            var stateInfo = killResult['stateInfo'];
                                
                            //3.显示秒杀结果
                            node.html('' + stateInfo + '');
                        }
                    });
                });
                node.show();
            }else{
                //未开启秒杀
                var now = exposer['now'];
                var start = exposer['start'];
                var end = exposer['end'];
                seckill.countdown(seckillId, now, start, end);
            }
        }else{
            console.log('result: ' + result);
        }
    });
}.

这个方法的参数有个node,用来获取节点的,因为之前在detail.jsp中有专门显示时间判断的结果的span,当可以进行秒杀的时候,这个span显示的就是一个按钮,所以这里也要获取这个span节点,来对这个span进行操作,加入一个button标签

node.hide()
    .html('');

插入按钮后先不要显示出来,因为后面还要对用户信息也就是手机号进行验证

执行秒杀操作之前,就要先取得秒杀的地址

@RequestMapping(
        value = "/{seckillId}/exposer", 
        method = RequestMethod.POST,
        produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult exposer(@PathVariable("seckillId") Long seckillId){
        
    SeckillResult result;
    try {
        Exposer exposer = seckillService.exportSeckillUrl(seckillId);
        result = new SeckillResult(true, exposer);
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
        result = new SeckillResult(false, e.getMessage());
    }
    return result;
}

在SeckillController的exposer方法就是用来暴露秒杀地址的,这个方法只接收POST请求,返回的是SeckillResult对象,类型是Exposer、

在seckill.js中使用$.post()方法,类似前面讲过的$.get()方法

$.post(seckill.URL.exposer(seckillId), {}, function(result){
    //在回调函数中执行交互流程
    if(result && result['success']){
        var exposer = result['data'];
    }else{
        console.log('result: ' + result);
    }
});

要传入请求的URL,也要放在seckill的URL属性中

exposer : function(seckillId){
    return '/seckill/' + seckillId + '/exposer';
}

这个URL需要传递秒杀商品的id,因为不同的秒杀商品需要相应的UEL

首先还是要判断ajax请求是否成功,如果没有请求成功,在控制台打印信息

如果请求成功,获取$.post()方法返回过来的数据,是Exposer类型的,封装在SeckillResult的data属性中

public class Exposer {
    
    //是否开启秒杀
    private boolean exposed;
    
    //加密措施
    private String md5;
    
    //id
    private long seckillId;
    
    //系统当前时间(毫秒)
    private long now;
    
    //秒杀开启时间
    private long start;
    
    //秒杀结束时间
    private long end;
}

获取到Exposer对象后,在Exposer类中有一个exposed属性,用来判断是否开启秒杀,如果开启秒杀,就要控制之前定义的按钮,先绑定点击事件,然后显示出来

如果不开启秒杀,就返回系统当前时间、秒杀开启时间、秒杀结束时间,再调用countdown函数

if(exposer['exposed']){

}else{
    //未开启秒杀
    var now = exposer['now'];
    var start = exposer['start'];
    var end = exposer['end'];
    seckill.countdown(seckillId, now, start, end);
}

既然都到这一步了,什么情况下还是秒杀未开始?

当不同的终端显示过长的时间的时候,可能出现一些偏差,用户显示已经开启秒杀,但是实际上服务器的时间还没到,虽然时间差很小,但是还是要重新计算计时逻辑,所以调用countdown函数

判断开启秒杀之后,先要获取秒杀地址

//开启秒杀,获取秒杀地址
var md5 = exposer['md5'];
var killUrl = seckill.URL.execution(seckillId, md5);
console.log('killUrl: ' + killUrl);

用于执行秒杀操作的URL需要经过MD5的加密,所以还要从后端获取到MD5,同样,ajax请求的URL都要封装在seckill.js的URL属性中

execution : function(seckillId, md5){
    return '/seckill/' + seckillId + '/' + md5 + '/execution';
}

这些URL之前在Controller层都已经定义好的

@RequestMapping(
        value = "/{seckillId}/{md5}/execution",
        method = RequestMethod.POST,
        produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult execute(@PathVariable("seckillId") Long seckillId, 
                                                   @PathVariable("md5") String md5,
                                                   @CookieValue(value = "killPhone", required = false) Long phone)

获取到了执行秒杀的URL,就可以控制按钮,绑定点击事件

//绑定一次点击事件
$('#killBtn').one('click', function(){
    //执行秒杀请求
    //1.禁用按钮
    $(this).addClass('disabled');
                        
    //2.发送秒杀请求执行秒杀
    $.post(killUrl, {}, function(result){
        if(result && result['success']){
            var killResult = result['data'];
            var state = killResult['state'];
            var stateInfo = killResult['stateInfo'];
                                
            //3.显示秒杀结果
            node.html('' + stateInfo + '');
        }
    });
});

但是只绑定一次点击事件,防止用户连续点击,比如用户不放心页面是否响应,所以可能会连续的点击按钮,如果不在这控制的话,这些点击最后都会发送到服务器端,会造成服务器端在同一时间接到大量相同的URL请求,对各方面都有影响

所以点击完之后就要禁用按钮,通过this指代当前对象,也就是相当于使用$('#killBtn')

之后就是发送秒杀请求,执行秒杀操作,在SeckillController的execute方法只接收POST请求,所以使用$.post()方法

然后通过SeckillResult中的success属性判断是否请求成功

if(phone == null){
    return new SeckillResult(false, "未注册");
}
//SeckillResult result;
try {
    SeckillExecution execution = seckillService.executeSeckill(seckillId, phone, md5);
    return new SeckillResult(true, execution);
} catch (RepeatKillException e) {
    SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.REPEAT_KILL);
    return new SeckillResult(true, execution);
} catch (SeckillCloseException e) {
    SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.END);
    return new SeckillResult(true, execution);
} catch (Exception e) {
    logger.error(e.getMessage(), e);
    SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);
    return new SeckillResult(true, execution);
}

这是SeckillController的execute方法,返回的都是SeckillExecution对象,这些对象存放在SeckillResult的data属性中

public class SeckillExecution {
    
    private long seckillId;
    
    //秒杀结果执行后的状态
    private int state;
    
    //状态信息
    private String stateInfo;

    //秒杀成功对象
    private SuccessKilled successKilled;
}

这是SeckillExecution类中定义的方法,在seckill.js中获取到这些属性

$.post(killUrl, {}, function(result){
    if(result && result['success']){
        var killResult = result['data'];
        var state = killResult['state'];
        var stateInfo = killResult['stateInfo'];
                                
        //3.显示秒杀结果
        node.html('' + stateInfo + '');
    }
});

获取到执行秒杀的结果后,还要在详情页中显示出来,所以控制节点,输出状态信息,因为在SeckillController的execute方法中已经定义了重复秒杀、秒杀结束等异常也算请求成功,只是不对数据库进行操作,但是结果信息要返回到详情页

最后就可以把按钮显示出来了

node.show();

至此,前端页面完成了

你可能感兴趣的:(高并发秒杀API(五))