高并发秒杀API(五)

前言

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

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

一、使用Bootstrap开发页面结构

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

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

<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html>
   <head>
      <title>Bootstrap 模板</title>
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <!-- 引入 Bootstrap -->
      <link href="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
 
      <!-- HTML5 Shim 和 Respond.js 用于让 IE8 支持 HTML5元素和媒体查询 -->
      <!-- 注意: 如果通过 file://  引入 Respond.js 文件,则该文件无法起效果 -->
      <!--[if lt IE 9]>
         <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
         <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
      <![endif]-->
   </head>
   <body>
      <h1>Hello, world!</h1>
 
      <!-- jQuery (Bootstrap 的 JavaScript 插件需要引入 jQuery) -->
      <script src="https://code.jquery.com/jquery.js"></script>
      <!-- 包括所有已编译的插件 -->
      <script src="js/bootstrap.min.js"></script>
   </body>
</html>

1、list.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<!-- 引入jstl -->
<%@ include file="common/tag.jsp" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
   <head>
      <title>秒杀列表页</title>
      <%@ include file="common/head.jsp" %>
   </head>
   <body>
   </body>
   
<!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
<script src="http://cdn.static.runoob.com/libs/jquery/2.1.1/jquery.min.js"></script>
     
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</html>

在最上面的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标签中的内容所有页面基本都一样

<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 引入 Bootstrap -->
<link href="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
 
<!-- HTML5 Shim 和 Respond.js 用于让 IE8 支持 HTML5元素和媒体查询 -->
<!-- 注意: 如果通过 file://  引入 Respond.js 文件,则该文件无法起效果 -->
<!--[if lt IE 9]>
    <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
    <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->

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

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

list.jsp

panel-defaulttext-center都是使用Bootstrap提供的样式

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

<thead>
    <tr>
        <th>名称</th>
        <th>库存</th>
        <th>开始时间</th>
        <th>结束时间</th>
        <th>创建时间</th>
        <th>详情页</th>
    </tr>
</thead>
<tbody>
    <c:forEach var="sk" items="${list}">
     <tr>
        <td>${sk.name}</td>
        <td>${sk.number}</td>
        <td>
            <fmt:formatDate value="${sk.startTime}" pattern="yyyy-MM-dd HH:mm:ss"/>             
        </td>
        <td>
            <fmt:formatDate value="${sk.endTime}" pattern="yyyy-MM-dd HH:mm:ss"/>               
        </td>
        <td>
            <fmt:formatDate value="${sk.createTime}" pattern="yyyy-MM-dd HH:mm:ss"/>                
        </td>
        <td>
            <a class="btn btn-info" href="/seckill/${sk.seckillId}/detail" target="_blank">link</a>             
        </td>
     </tr>
    </c:forEach>
</tbody>

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

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

@RequestMapping(value = "/list", method = RequestMethod.GET)
public String list(Model model){
        
    //获取列表页
    List<Seckill> 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

detail.jsp

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

<h1>${seckill.name }</h1>

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

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

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

detail.jsp中的登录弹出层

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

<div id="killPhoneModal" class="modal fade">

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

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

  • modal-header:显示一些文本
  • modal-body:用户输入登录信息
  • modal-footer:登录按钮
<div class="modal-header">
    <h3 class="modal-title text-center">
        <span class="glyphicon glyphicon-phone"></span>秒杀电话:
    </h3>               
</div>

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

<div class="modal-body">
    <div class="row">
        <div class="col-xs-8 col-xs-offset-2">
            <input type="text" name="killPhone" id="killPhoneKey" 
                            placeholder="填写手机号^o^" class="form-control">
        </div>
    </div>
</div>

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

<div class="modal-footer">
    <!-- 验证信息 -->
    <span id="killPhoneMessage" class="glyphicon"></span>
    <button type="button" id="killPhoneBtn" class="btn btn-success">
        <span class="glyphicon glyphicon-phone"></span>
    </button>
</div>

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

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

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

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

<!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
<script src="http://cdn.static.runoob.com/libs/jquery/2.1.1/jquery.min.js"></script>
     
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/js/bootstrap.min.js"></script>

<!-- 使用CDN获取公共js  -->
<!-- jQuery cookie操作插件 -->
<script src="http://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.js"></script>
<!-- jQuery countDown倒计时插件 -->
<script src="http://cdn.bootcss.com/jquery.countdown/2.2.0/jquery.countdown.min.js"></script>

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

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

2、交互逻辑

1、交互流程

前端页面交互流程

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

详情页流程
  • 获取标准系统时间,因为用户可能处在不同的时区,用户终端的时间也不可能完全一致,所以要统一地采用一个标准时间,也就是服务器时间

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

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

2、页面展示

列表页
登录弹出层
可以秒杀
秒杀结束
秒杀未开始

3、交互逻辑编程

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

创建一个seckill.js


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('<label class="label label-danger">手机号错误!</label>').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中也提前做好了一个登录弹出层,并进行了埋点

登录弹出层

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('<label class="label label-danger">手机号错误!</label>').show(300);
        }
    });
}

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

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

<div class="modal-footer">
    <!-- 验证信息 -->
    <span id="killPhoneMessage" class="glyphicon"></span>
    <button type="button" id="killPhoneBtn" class="btn btn-success">
        <span class="glyphicon glyphicon-phone"></span>
    </button>
</div>

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

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

<div class="modal-body">
    <div class="row">
        <div class="col-xs-8 col-xs-offset-2">
            <input type="text" name="killPhone" id="killPhoneKey" 
                placeholder="填写手机号^o^" class="form-control">
        </div>
    </div>
</div>

在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('<label class="label label-danger">手机号错误!</label>').show(300);
}

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

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

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

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

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

<span id="killPhoneMessage" class="glyphicon"></span>

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

$('#killPhoneMessage').hide().html('<label class="label label-danger">手机号错误!</label>').show(300);

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

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

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

整个前端的流程基本完成


前端页面交互流程

接着是详情页的流程


详情页流程

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

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

<!-- 开始编写交互逻辑 -->
<script src="/resources/script/seckill.js" type="text/javascript"></script>

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

<script type="text/javascript">
    $(function(){
        //使用EL表达式传入参数
        seckill.detail.init({
            seckillId : "${seckill.seckillId}",
            startTime : "${seckill.startTime.time}",
            endTime : "${seckill.endTime.time}"
        });
        
    });
</script>

接着在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<Long> time(){
    Date now = new Date();
    return new SeckillResult<Long>(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<Long>类型的对象

public class SeckillResult<T> {
    
    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,用于显示时间判断的结果

<div class="panel-body">
    <h2 class="text-danger">
        <!-- 显示time图标 -->
        <span class="glyphicon glyphicon-time"></span> 
        <!-- 显示面板 -->
        <span class="glyphicon" id="seckill-box"></span>
    </h2>
</div>

提前设置了埋点,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('<button class="btn btn-primary btn-lg" id="killBtn">开始秒杀</button>');
    $.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('<span class="label label-success">' + stateInfo + '</span>');
                        }
                    });
                });
                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('<button class="btn btn-primary btn-lg" id="killBtn">开始秒杀</button>');

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

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

@RequestMapping(
        value = "/{seckillId}/exposer", 
        method = RequestMethod.POST,
        produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<Exposer> exposer(@PathVariable("seckillId") Long seckillId){
        
    SeckillResult<Exposer> result;
    try {
        Exposer exposer = seckillService.exportSeckillUrl(seckillId);
        result = new SeckillResult<Exposer>(true, exposer);
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
        result = new SeckillResult<Exposer>(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<SeckillExecution> 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('<span class="label label-success">' + stateInfo + '</span>');
        }
    });
});

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

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

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

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

if(phone == null){
    return new SeckillResult<SeckillExecution>(false, "未注册");
}
//SeckillResult<SeckillExecution> result;
try {
    SeckillExecution execution = seckillService.executeSeckill(seckillId, phone, md5);
    return new SeckillResult<SeckillExecution>(true, execution);
} catch (RepeatKillException e) {
    SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.REPEAT_KILL);
    return new SeckillResult<SeckillExecution>(true, execution);
} catch (SeckillCloseException e) {
    SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.END);
    return new SeckillResult<SeckillExecution>(true, execution);
} catch (Exception e) {
    logger.error(e.getMessage(), e);
    SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);
    return new SeckillResult<SeckillExecution>(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('<span class="label label-success">' + stateInfo + '</span>');
    }
});

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

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

node.show();

至此,前端页面完成了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容