高并发秒杀API(五)

作者: MOVE1925 | 来源:发表于2017-01-23 03:20 被阅读735次

    前言

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

    • 使用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();
    

    至此,前端页面完成了

    相关文章

      网友评论

        本文标题:高并发秒杀API(五)

        本文链接:https://www.haomeiwen.com/subject/dsybbttx.html