Java高并发秒杀——web层及前端页面开发

该博客聚焦Java高并发秒杀场景下的web层及前端页面开发。介绍了Restful接口设计与实现、SpringMVC整合Spring的方法,阐述通过SpringMVC实现Restful接口的步骤。还讲解了用BootStrap开发前端页面结构,以及cookie登录、计时、秒杀的前后端交互的js实现。

Java高并发秒杀——web层及前端页面开发

web(Controller)层:连通前后端

前端页面开发:BootStrap+JavaScript(模块化)

Service层分析目录

  1、Restful接口的设计与实现
  2、SpringMVC整合Spring
  3、通过SpringMVC实现Restful接口
  4、BootStrap开发前端页面结构
  5、cookie登录、计时、秒杀的前后端交互

在这里插入图片描述
在这里插入图片描述

一、Restful接口的设计与实现

  1、Restful规范:GET->查询操作;POST->添加/修改操作;PUT->修改操作;DELETE->删除操作
  2、Restful中URL的设计:/模块/资源/{标识}/集合1/…

在这里插入图片描述

二、SpringMVC整合Spring

  1、SpringMVC运行流程

在这里插入图片描述

  2、@RequestMapping注解的好处
    (1)支持标准的URL
    (2)支持Ant风格的URL(即?和和**等字符)——->任意字符但不能没有这一层;**->任意字符且可以没有这一层
    (3)支持带{xxx}占位符的URL
  3、配置spring-web.xml(SpringMVC)
    (1)开启SpringMVC注解模式简化配置
      1>自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter
      2>提供一系列:数据绑定、数字和日期的format,@NumberFormat,@DataTimeFormat,重点:提供xml,json默认读写支持
			<mvc:annotation-driven/>
			<!--  该配置提供xml,json默认读写支持-->
    (2)静态资源默认servlet配置
      1>加入对静态资源的处理:js、gif、png
      2>允许使用"/"做整体映射
			<mvc:default-servlet-handler/>
		    <!-- 该配置允许使用"/"做整体映射-->
    (3)配置jsp,显示ViewResolver——视图解析器
			<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
			        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
			        <property name="prefix" value="/WEB-INF/jsp/"/>
			        <property name="suffix" value=".jsp"/>
		    </bean>
    (4)扫描web相关的bean,将这些bean注入到Spring容器中
			 <context:component-scan base-package="com.fehead.web"/>
  4、在web.xml中同时配置SpringMVC+Spring+MyBatis,直接配置一个dispatcherServlet并在该servlet中初始化spring的配置参数即可完成对ssm的配置
	<!-- 配置DispatcherServlet-->
	    <servlet>
	        <servlet-name>seckill_dispatcher</servlet-name>
	        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	        <!-- 配置SpringMVC时需要加载的配置文件——把spring,mybatis,springmvc放在一起加载即可
	            spring-dao.xml,spring-service.xml,spring-web.xml
	            MyBatis      ->   spring       ->   springmvc
	        -->
	        <init-param>
	            <param-name>contextConfigLocation</param-name>
	            <param-value>classpath:spring/spring-*.xml</param-value>
	        </init-param>
	    </servlet>
	    <servlet-mapping>
	        <servlet-name>seckill_dispatcher</servlet-name>
	        <!-- 默认匹配所有的请求-->
	        <url-pattern>/</url-pattern>
	    </servlet-mapping>

三、通过SpringMVC实现Restful接口

  1、创建与前端交互的DTO,所有ajax请求返回的类型,封装json结果——三个属性:成功与否(success为true或false);返回的json数据的类型(利用泛型定义data);错误信息(若返回失败而返回error信息)
	package com.fehead.dto;

	/**
	 * Created by xiaoaxiao on 2019/5/4
	 * Description:所有ajax请求返回类型,封装json结果
	 */
	public class SeckillResult<T> {
	
	    private boolean success;
	    private T data;
	    private String error;
	
	    public SeckillResult(boolean success, T data) {
	        this.success = success;
	        this.data = data;
	    }
	
	    public SeckillResult(boolean success, String error) {
	        this.success = success;
	        this.error = error;
	    }
	
	    public boolean isSuccess() {
	        return success;
	    }
	
	    public void setSuccess(boolean success) {
	        this.success = success;
	    }
	
	    public T getData() {
	        return data;
	    }
	
	    public void setData(T data) {
	        this.data = data;
	    }
	
	    public String getError() {
	        return error;
	    }
	
	    public void setError(String error) {
	        this.error = error;
	    }
	}

  2、SeckillController的创建
    (1)可以设置@RequestMapping的参数获取url地址(value = “/{seckillId}/exposer”)以及请求提交的方式(method = RequestMethod.POST)以及方法返回类型为json类型(produces = {“application/json;charset=UTF-8”})
    (2)通过@PathVariable(“xxx”)获取路径变量的值({标识符})
    (3)通过@RequestParam获取请求参数的值(key=value)。
     注意@PathVariable(标识符)和@RequestParam(参数)的区别
    (4)通过@ResponseBody与@RequestMapping的将相应ajax请求并返回给前端的DTO设置为json数据类型
    (5)通过@CookieValue读取服务器Cookie中的数据,需要将required设置为false,如果服务器中没有Cookie中value对应的值,SpringMVC不会报错,把Cookie中value的验证放在程序中处理而不是让SpringMVC直接报错
    (6)顺序的设置多个catch,若产生已设定好的异常(RepeatKillException ,SeckillCloseException )则返回该异常对应的信息即可,若是其他异常则统一返回SeckillException
    (7)SeckillController的逻辑代码
package com.fehead.web;

import com.fehead.bean.Seckill;
import com.fehead.dto.Exposer;
import com.fehead.dto.SeckillExecution;
import com.fehead.dto.SeckillResult;
import com.fehead.enums.SeckillStatEnum;
import com.fehead.exception.RepeatKillException;
import com.fehead.exception.SeckillCloseException;
import com.fehead.exception.SeckillException;
import com.fehead.service.SeckillService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

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

/**
 * Created by xiaoaxiao on 2019/5/4
 * Description:
 */
@Controller
@RequestMapping("/seckill")     //url:/模块/资源/{id}/细分
public class SeckillController {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private SeckillService seckillService;

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

        model.addAttribute("list",list);
        //list.jsp + model = ModelAndView
        return "list";  //  /WEB-INF/list.jsp
    }

    @RequestMapping(value = "/{seckillId}/detail",method = RequestMethod.GET)
    public String detail(@PathVariable("seckillId") Long seckillId, Model model){
        if(seckillId==null){
            return "redirect:/seckill/list";
        }
        Seckill seckill = seckillService.getSeckillById(seckillId);
        if(seckill==null){
            return "forward:/seckill/list";
        }
        model.addAttribute("seckill",seckill);
        return "detail";
    }

    //ajax,json
    @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;
    }

    @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){
        if (phone==null){
            return new SeckillResult<SeckillExecution>(false,"未注册");
        }
        SeckillResult<SeckillExecution> result;
        try {
//            SeckillExecution execution = seckillService.executeSeckill(seckillId,phone,md5);
            //通过存储过程+redis缓存调用
            SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId,phone,md5);
            return new SeckillResult<SeckillExecution>(true,execution);
            //即便是报错也需要返回true——可以将错误的信息返回至前端页面
        }catch (RepeatKillException e){
            SeckillExecution execution = new SeckillExecution(seckillId,SeckillStatEnum.REPEAT_KILL);
            return new SeckillResult<SeckillExecution>(true,execution);
        }catch (SeckillCloseException e){
            SeckillExecution execution = new SeckillExecution(seckillId,SeckillStatEnum.END);
            return new SeckillResult<SeckillExecution>(true,execution);
        }catch (Exception e){
            logger.error(e.getMessage(),e);
            SeckillExecution execution = new SeckillExecution(seckillId,SeckillStatEnum.INNER_ERROR);
            return new SeckillResult<SeckillExecution>(true,execution);
        }
    }

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

四、BootStrap开发前端页面结构

  1、从BootStrap官网上知道模板,创建几乎所有的页面都会用到的(会共用的)common包下的jsp
    head.jsp:引入BootStrap相关资源
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<!-- 引入 Bootstrap -->
	<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
	
	<!-- HTML5 Shiv 和 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]-->
    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" %>
  2、根据前端+BootStrap的相关知识创建list.jsp(列表页)+detail.jsp(详情页)
    注意事项:(1)使用<%@ include file=“xxx.jsp” %>的方式实现静态包含
         (2)将对js文件的引用放在body标签后,且需要注意:jQuery文件务必在最新的 Bootstrap 核心 JavaScript 文件之前引入
    list.jsp
	<%@ page contentType="text/html;charset=UTF-8" language="java" %>
	<%@ include file="common/tag.jsp" %>
	<!DOCTYPE html>
	<html>
	<head>
	    <title>秒杀列表页</title>
	    <!-- 静态包含,将引入jsp放入该jsp,当做一个servlet使用,若是动态包含就会当做两个servlet使用-->
	    <%@ include file="common/head.jsp" %>
	</head>
	<body>
	<!-- 页面显示部分-->
	<div class="container">
	    <div class="panel panel-default">
	        <div class="panel-heading text-center">
	            <h2>秒杀列表</h2>
	        </div>
	        <div class="panel-body">
	            <table class="table table-hover">
	                <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>
	            </table>
	        </div>
	    </div>
	</div>
	</body>
	<!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
	<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
	<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
	<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
	</html>

    最初的detail.jsp
	<%@ page contentType="text/html;charset=UTF-8" language="java" %>
	<%@ include file="common/tag.jsp" %>
	<!DOCTYPE html>
	<html>
	<head>
	    <title>秒杀详情页</title>
	    <!-- 静态包含,将引入jsp放入该jsp,当做一个servlet使用,若是动态包含就会当做两个servlet使用-->
	    <%@ include file="common/head.jsp" %>
	</head>
	<body>
	    <div class="container">
	        <div class="panel panel-default text-center">
	            <div class="pannel-heading">
	                <h1>${seckill.name}</h1>
	            </div>
	            <div class="panel-body">
	            		xxx...
	            </div>
	        </div>
	    </div>
	</body>
	<!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
	<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
	<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
	<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
	</html>

五、cookie登录、计时、秒杀的前后端交互——js实现

  1、cookie登录:根据BootStrap创建登录弹出层,输入电话,引入jQuery cookie操作插件
<!-- 登录弹出层,输入电话-->
<div id="killPhoneModal" class="modal fade">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h3 class="modal-title text-center">
                    <span class="glyphicon glyphicon-phone"></span>秒杀电话:
                </h3>
            </div>
            <div class="modal-body">
                <div class="row">
                    <div class="col-xs-8 col-xs-offset-2">
                        <input type="text" name="killPhone" id="killPhoneKey"
                            placeholder="填手机号^0^" class="form-control"/>
                    </div>
                </div>
            </div>

            <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>
                    Submit
                </button>
            </div>
        </div>
    </div>
</div>
  2、在detail.jsp中边写与resources中js文件的交互逻辑:将页面上的参数通过EL表达式传入到对应js文件中进行处理
	<!-- 先引入自己写好的js文件-->
	<script src="/resources/script/seckill.js" type="text/javascript"></script>
	<!-- 在该jsp文件中使用jquery调用js文件中的方法-->
	<script type="text/javascript">
	    $(function () {
	        //使用EL表达式传入参数
	        seckill.detail.init({
	            seckillId:${seckill.seckillId},
	            startTime:${seckill.startTime.time},    //转换为毫秒
	            endTime:${seckill.endTime.time}
	        });
	    });
	</script>
  3、在resources中创建js文件处理页面中的参数以及与后端Controller层进行交互,JavaScript模块化,模块化的seckill分为三部分:封装秒杀相关的ajax的url+详情页的秒杀逻辑+秒杀逻辑需要用到的其他方法
    (1)处理cookie并验证手机号
    1>在js中创建一个验证手机号的函数,需要判断的是输入的手机号是否为11位数字
    2>先在服务器的cookie中查找手机号进行验证,如果cookie中没有已添加过得手机号或已添加过得手机号不符合11位数字(被篡改),则显示弹出框,如果已经有(添加过的且能通过验证的)手机号则该模块之后的内容都不需要做了。
    3>显示弹出框时需要 将backdrop设置为static:禁止位置关闭——点击别的地方无效;将keyboard设置为false:关闭键盘事件——按键盘中某些按键无效。
    4>获取到前端弹出框中输入的手机号后,将该手机号经过验证后写入到cookie中并刷新页面(重新再进行一个init——验证手机号判断,由于已经存在通过验证的手机号,所以刷新后直接跳过该模块,也不会弹出弹出框),若该手机没有通过验证则提示手机号错误直到输入的手机号为正确为止。
	//验证手机号
	    validatePhone: function (phone) {
	        if (phone && phone.length === 11 && !isNaN(phone)) {
	            return true;
	        } else {
	            return false;
	        }
	    },


	//手机验证和登录,计时交互
            //规划交互流程

            //在cookie中查找手机号,对应detail.jsp中input的name
            var killPhone = $.cookie('killPhone');

            //验证手机号
            if (!seckill.validatePhone(killPhone)) {
                //绑定phone
                //控制输出
                var killPhoneModal = $('#killPhoneModal');
                //显示弹出层
                killPhoneModal.modal({
                    show: true,//显示弹出框
                    backdrop: 'static',//禁止位置关闭——点击别的地方无效
                    keyboard: false//关闭键盘事件——按键盘某些键无效
                });
                $('#killPhoneBtn').click(function () {
                    var inputPhone = $('#killPhoneKey').val();
                    console.log('inputPhone=' + inputPhone);//TODO
                    if (seckill.validatePhone(inputPhone)) {
                        //电话写入cookie
                        $.cookie('killPhone', inputPhone, {expires: 7, path: '/seckill'});
                        //刷新页面
                        window.location.reload();
                    } else {
                        $('#killPhoneMessage').hide().html('<label class="label label-danger">手机号错误!</label>').show(300);
                    }
                });
            }
    (2)处理计时交互
    1>通过detail.jsp传入到js中init方法的参数拿到页面上的seckillId,startTime,endTime,通过ajax请求调用后端Controller的now方法拿到服务器的当前时间nowTime。将时间判断计时交互封装成一个函数(countdown)。
    2>countdown:首先对时间进行判断,若nowTime>endTime则表示秒杀已结束,直接在页面上显示秒杀已结束即可,若nowTime<startTime则表示秒杀还未开始,则需要在页面上显示秒杀倒计时,当倒计时结束后则变为(开始秒杀)按钮,若startTime<nowTime<endTime,则同样变为(开始秒杀)按钮,点击即可执行秒杀交互。
    3>秒杀未开始时的倒计时:将页面的当前时间设置为秒杀开始后的1s(防止各pc端时间不一致,则必须以服务器端为标准),使用event.strftime渲染seckillBox为格式化(‘秒杀倒计时: %D天 %H时 %M分 %S秒’)的时间,并在时间倒计时完成后开启回调事件(.on(‘finish.countdown’, function () ))。
	//已经登录
	            //计时交互
	            var seckillId = params['seckillId'];
	            var startTime = params['startTime'];
	            var endTime = params['endTime'];
	            $.get(seckill.URL.now(), {}, function (result) {
	                //success和data对应SeckillResult类中的属性
	                if (result && result['success']) {
	                    var nowTime = result['data'];
	                    //时间判断,计时交互
	                    seckill.countdown(seckillId, nowTime, startTime, endTime);
	                } else {
	                    console.log('result:' + result);
	                }
	            });
	
	 //倒计时(秒杀未开始)/秒杀结束/秒杀中
	    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秒');
	                seckillBox.html(format);
	                //时间完成后回调事件
	            }).on('finish.countdown', function () {
	                //在倒计时结束后开始秒杀
	                seckill.handleSeckillkill(seckillId, seckillBox);
	            });
	        } else {
	            //直接开始秒杀
	            seckill.handleSeckillkill(seckillId, seckillBox);
	        }
	    },
    (3)处理秒杀交互
    1>获取秒杀地址:通过ajax请求拿到Controller层返回的SeckillResult中的data中的md5,在js的URL对md5和其他操作请求进行拼接,得到最终的killURL。
    2>执行秒杀:对秒杀绑定一次点击事件(防止多次点击给服务器发送重复无用的请求)——$(’#killBtn’).one(‘click’, function ();当点击按钮后将按钮禁用(防止重复点击),通过ajax向后端Controller层发送请求,得到data中的state与stateInfo(Enum描述——成功/异常信息),将stateInfo显示在页面。
    3>若未开启秒杀:可能有一个原因是不同的pc端的时间之间有微小的差距,所以需要将服务器端的时间传入,再做一遍coutdown操作,重新计算计时逻辑。
handleSeckillkill: 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']) {
                    //开启秒杀
                    //获取秒杀的地址(包括md5)
                    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。显示秒杀结果(stateInfo)
                                node.html('<span class="label label-success">' + stateInfo + '</span>')
                            }
                        });
                    });
                    node.show();
                } else {
                    //未开启秒杀
                    //未开启秒杀可能有一个原因是:不同的pc端的时间之间有微笑的差距,所以将服务器端的时间传入
                    // 再做一遍countdown
                    var now = exposer['now'];
                    var start = exposer['start'];
                    var end = exposer['end'];
                    //重新计算计时逻辑
                    seckill.countdown(seckillId, now, start, end);
                }
            } else {
                console.log('result:' + result);
            }
        })
    },
  4、detail.jsp+seckill.js最终代码
    detail.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ include file="common/tag.jsp" %>
<!DOCTYPE html>
<html>
<head>
    <title>秒杀详情页</title>
    <!-- 静态包含,将引入jsp放入该jsp,当做一个servlet使用,若是动态包含就会当做两个servlet使用-->
    <%@ include file="common/head.jsp" %>
</head>
<body>
    <div class="container">
        <div class="panel panel-default text-center">
            <div class="pannel-heading">
                <h1>${seckill.name}</h1>
            </div>
            <div class="panel-body">
                <h2 class="text-danger">
                    <!-- 显示time图标-->
                    <span class="glyphicon glyphicon-time"></span>
                    <!-- 展示倒计时(未开始)/已结束/进行中-->
                    <span class="glyphicon" id="seckill-box"></span>
                </h2>
            </div>
        </div>
    </div>
<!-- 登录弹出层,输入电话-->
<div id="killPhoneModal" class="modal fade">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h3 class="modal-title text-center">
                    <span class="glyphicon glyphicon-phone"></span>秒杀电话:
                </h3>
            </div>
            <div class="modal-body">
                <div class="row">
                    <div class="col-xs-8 col-xs-offset-2">
                        <input type="text" name="killPhone" id="killPhoneKey"
                            placeholder="填手机号^0^" class="form-control"/>
                    </div>
                </div>
            </div>

            <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>
                    Submit
                </button>
            </div>
        </div>
    </div>
</div>
</body>
<!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<!-- jQuery cookie倒计时插件-->
<script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<!-- jQuery countDown倒计时插件-->
<!-- 倒计时插件一定要看准确,是jquery.countdown而不是jquery-countdown...-->
<script src="https://cdn.bootcss.com/jquery.countdown/2.0.2/jquery.countdown.min.js"></script>

<!-- 开始编写交互逻辑-->
<!-- 先引入自己写好的js文件-->
<script src="/resources/script/seckill.js" type="text/javascript"></script>
<!-- 在该jsp文件中使用jquery调用js文件中的方法-->
<script type="text/javascript">
    $(function () {
        //使用EL表达式传入参数
        seckill.detail.init({
            seckillId:${seckill.seckillId},
            startTime:${seckill.startTime.time},    //转换为毫秒
            endTime:${seckill.endTime.time}
        });
    });
</script>
</html>

    seckill.js
//存放主要交互逻辑js代码
//  javascript模式化
//  seckill.detail.xxx
var seckill = {
    //封装秒杀相关ajax的url
    URL: {
        //获取当前服务器时间的url
        now: function () {
            return '/seckill/time/now';
        },
        exposer: function (seckillId) {
            return '/seckill/' + seckillId + '/exposer';
        },
        execution: function (seckillId, md5) {
            return '/seckill/' + seckillId + '/' + md5 + '/execution';
        }
    },
    handleSeckillkill: 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']) {
                    //开启秒杀
                    //获取秒杀的地址(包括md5)
                    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。显示秒杀结果(stateInfo)
                                node.html('<span class="label label-success">' + stateInfo + '</span>')
                            }
                        });
                    });
                    node.show();
                } else {
                    //未开启秒杀
                    //未开启秒杀可能有一个原因是:不同的pc端的时间之间有微笑的差距,所以将服务器端的时间传入
                    // 再做一遍countdown
                    var now = exposer['now'];
                    var start = exposer['start'];
                    var end = exposer['end'];
                    //重新计算计时逻辑
                    seckill.countdown(seckillId, now, start, end);
                }
            } else {
                console.log('result:' + result);
            }
        })
    },
    //验证手机号
    validatePhone: function (phone) {
        if (phone && phone.length === 11 && !isNaN(phone)) {
            return true;
        } else {
            return false;
        }
    },
    //倒计时(秒杀未开始)/秒杀结束/秒杀中
    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秒');
                seckillBox.html(format);
                //时间完成后回调事件
            }).on('finish.countdown', function () {
                //在倒计时结束后开始秒杀
                seckill.handleSeckillkill(seckillId, seckillBox);
            });
        } else {
            //直接开始秒杀
            seckill.handleSeckillkill(seckillId, seckillBox);
        }
    },
    //详情页秒杀逻辑
    detail: {
        //详情页初始化
        init: function (params) {
            //手机验证和登录,计时交互
            //规划交互流程

            //在cookie中查找手机号,对应detail.jsp中input的name
            var killPhone = $.cookie('killPhone');

            //验证手机号
            if (!seckill.validatePhone(killPhone)) {
                //绑定phone
                //控制输出
                var killPhoneModal = $('#killPhoneModal');
                //显示弹出层
                killPhoneModal.modal({
                    show: true,//显示弹出框
                    backdrop: 'static',//禁止位置关闭——点击别的地方无效
                    keyboard: false//关闭键盘事件——按键盘某些键无效
                });
                $('#killPhoneBtn').click(function () {
                    var inputPhone = $('#killPhoneKey').val();
                    console.log('inputPhone=' + inputPhone);//TODO
                    if (seckill.validatePhone(inputPhone)) {
                        //电话写入cookie
                        $.cookie('killPhone', inputPhone, {expires: 7, path: '/seckill'});
                        //刷新页面
                        window.location.reload();
                    } else {
                        $('#killPhoneMessage').hide().html('<label class="label label-danger">手机号错误!</label>').show(300);
                    }
                });
            }

            //已经登录
            //计时交互
            var seckillId = params['seckillId'];
            var startTime = params['startTime'];
            var endTime = params['endTime'];
            $.get(seckill.URL.now(), {}, function (result) {
                //success和data对应SeckillResult类中的属性
                if (result && result['success']) {
                    var nowTime = result['data'];
                    //时间判断,计时交互
                    seckill.countdown(seckillId, nowTime, startTime, endTime);
                } else {
                    console.log('result:' + result);
                }
            });
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值