从零开发高并发秒杀系统(三)web层

一、前端交互流程设计

由于不同用户使用的机器上的时间可能与服务器端时间不一致,因此我们需要从服务器端获取一致的标准时间

Restful接口设计

Restful是一种优雅的URI表达方式,用于表示资源的状态与状态转移,URI应该使用名词,用GET/POST/DELETE/PUT表示动作

Restful规范:POST和PUT用于修改操作的区别在于操作的幂等性上,POST修改操作是非幂等的,PUT是幂等的

GET          ->   查询操作

POST        ->   添加/修改操作

PUT           ->   修改操作

DELETE    ->   删除操作

URI设计:/模块/资源/标识/集合...

Restful示例:

GET           /seckill/list                              查询秒杀商品列表

POST        /seckill/{seckillId}/execution    执行秒杀(相当于添加一个秒杀操作)

DELETE    /seckill/{id}/delete                   删除指定的秒杀商品记录

springMVC运行流程

@RequestMapping注解映射技巧

(1)标准的URL

(2)Ant风格的URL(即?、* 和 ** 等字符,?表示匹配一个字符,* 表示匹配任意字符,** 表示匹配任意的URL路径)

(3)带{xxx}占位符的URL

例如:

/user/?/creation                  匹配    /user/a/creation、/user/b/creation  等

/user/*/creation                   匹配    /user/a/creation、/user/abc/creation  等

/user/**/creation                  匹配    /user/creation、/user/b/creation、/user/a/b/creation   等

/user/{userId}/creation        匹配    /user/b/creation、/user/123/creation   等

请求方法细节的处理

1、请求参数的绑定         使用@PathVariable注解

2、请求方式的限制         使用method=RequestMethod.GET等

3、请求转发和重定向     使用redirect和forward

4、数据模型赋值            使用model.addAttribute(key,value)

5、返回json数据            在方法上加上@ResponseBody注解,在@RequestMapping注解中加上produces={"application/json;charset=UTF-8"}

6、cookie的访问            在方法的参数前添加@CookieValue注解

二、使用springMVC

spring-web.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
  <!-- 配置SpringMVC -->
  <!-- 1.开启SpringMVC注解模式 -->
  <!-- 简化配置:
    (1)自动注册DefaultAnootationHandlerMapping,AnotationMethodHandlerAdapter
    (2)提供一些列:数据绑定,数字和日期的format @NumberFormat, @DateTimeFormat, xml,json默认读写支持
  -->
  <mvc:annotation-driven />

  <!-- 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 -->
  <context:component-scan base-package="com.jiangwangxiang.web" />
</beans>

web.xml

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
    http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    version="3.1" metadata-complete="true">
  <!-- 修改servlet版本为3.1 -->
  <!-- 配置DispatcherServlet -->
  <servlet>
    <servlet-name>seckill-dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 配置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>
</web-app>

封装json结果类SeckillResult

package com.jiangwangxiang.dto;

//封装json结果
public class SeckillResult<T> {

  private boolean success;

  private T data;

  private String error;

  public SeckillResult(boolean success, String error) {
    this.success = success;
    this.error = error;
  }

  public SeckillResult(boolean success, T data) {
    this.success = success;
    this.data = data;
  }

  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;
  }

  @Override
  public String toString() {
    return "SeckillResult [success=" + success + ", data=" + data + ", error=" + error + "]";
  }
}

SeckillController类

package com.jiangwangxiang.web;

import java.util.Date;
import java.util.List;
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.CookieValue;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.jiangwangxiang.dto.Exposer;
import com.jiangwangxiang.dto.SeckillExecution;
import com.jiangwangxiang.dto.SeckillResult;
import com.jiangwangxiang.entity.Seckill;
import com.jiangwangxiang.enums.SeckillStateEnum;
import com.jiangwangxiang.exception.RepeatKillException;
import com.jiangwangxiang.exception.SeckillCloseException;
import com.jiangwangxiang.service.SeckillService;

@Controller // @Service @Componet
@RequestMapping("/seckill") // url:/模块/资源/{id}/细分 /seckill/list
public class SeckillController {
  private 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/jsp/"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.getById(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) {
    // springmvc valid
    if (phone == null) {
      return new SeckillResult<SeckillExecution>(false, "未注册");
    }
    try {
      // 存储过程调用
      SeckillExecution execution = seckillService.executeSeckillProcedure(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);
    }
  }

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

三、基于bootstrap开发页面

bootstrap模板,基于该模板做修改

<%@ contentType="text/html;charset=utf-8" page language="java" %>
<!DOCTYPE html>
<html>
  <head>
    <title>Bootstrap Template</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- 引入Bootstrap -->
    <link href="http://apps.bdimg.com/libs/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet" media="screen">
    <!-- 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>
  </head>
  <body>
    <h1>Hello, world!</h1>

    <!-- jQuery文件作为bootstrap的底层依赖,务必在bootstrap.min.js之前引入 -->
    <script src="http://code.jquery.com/jquery.js"></script>
    <script src="http://apps.bdimg.com/libs/bootstrap/3.3.0/js/bootstrap.min.js"></script>
  </body>
</html>

-----------------------------------------

head.jsp

<%
  String path = request.getContextPath();
  String basePath = request.getScheme() + "://"
    + request.getServerName() + ":" + request.getServerPort() + path + "/";
  pageContext.setAttribute("basePath",basePath);
%>

<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->

<!-- 新 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet"
  href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">

<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="//cdn.bootcss.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="//cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->

tag.jsp

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

list.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
  pageEncoding="UTF-8"%>
<!-- 引入jstl -->
<%@include file="common/tag.jsp" %>
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <%@include file="common/head.jsp"%>
    <title>秒杀列表页</title>
  </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="yyy-MM-dd HH:mm:ss"/>
                  </td>
                  <td>
                    <fmt:formatDate value="${sk.endTime}" pattern="yyy-MM-dd HH:mm:ss"/>
                  </td>
                  <td>
                    <fmt:formatDate value="${sk.createTime}" pattern="yyy-MM-dd HH:mm:ss"/>
                  </td>
                  <td>
                    <a class="btn btn-info" href="${basePath}seckill/${sk.seckillId}/detail" target="_blank">link</a>
                  </td>
                </tr>
              </c:forEach>
            </tbody>
          </table>
        </div>
      </div>
    </div>

    <!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
    <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
    <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
    <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
  </body>
</html>

detail.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
  pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <%@include file="common/head.jsp"%>
    <title>秒杀详情页</title>
  </head>
  <body>
    <input type="hidden" id="basePath" value="${basePath}" />
    <div class="container">
      <div class="panel panel-default text-center">
        <div class="panel-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="seckillBox"></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="填手机号^O^" 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>

  <!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
  <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
  <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
  <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
  <!-- jQuery cookie操作插件 -->
  <script src="//cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
  <!-- jQery countDonw倒计时插件 -->
  <script src="//cdn.bootcss.com/jquery.countdown/2.1.0/jquery.countdown.min.js"></script>
  <!-- 开始编写交互逻辑 -->
  <script src="${basePath}resources/js/seckill.js" type="text/javascript"></script>
  <script type="text/javascript">
    $(function(){
      //使用EL表达式传入参数
      seckill.detail.init({
        seckillId : ${seckill.seckillId},
        startTime : ${seckill.startTime.time},//毫秒
        endTime : ${seckill.endTime.time}
      });
    });
  </script>
</body>
</html>

注意js文件引入的写法是<script src="" type="text/javascript"></script>,写成<script src="" type="text/javascript"/>有可能是不加载的

seckill.js

// 存放主要交换逻辑js代码
// javascript 模块化
var seckill = {
  // 封装秒杀相关ajax的url
  URL : {
    basePath : function() {
    return $('#basePath').val();
  },
  now : function() {
    return seckill.URL.basePath() + 'seckill/time/now';
  },
  exposer : function(seckillId) {
    return seckill.URL.basePath() + 'seckill/' + seckillId + '/exposer';
  },
  execution : function(seckillId, md5) {
    return seckill.URL.basePath() + 'seckill/' + seckillId + '/' + md5 + '/execution';
  }
},
// 处理秒杀逻辑
handleSeckill : function(seckillId, node) {
  // 获取秒杀地址,控制显示逻辑,执行秒杀
  node.hide().html('<button class="btn btn-primary btn-lg" id="killBtn">开始秒杀</button>');
  console.log('exposerUrl=' + seckill.URL.exposer(seckillId));//TODO
  $.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);//TODO
        $('#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);
    }
  });
},
// 验证手机号
validatePhone : function(phone) {
  if (phone && phone.length == 11 && !isNaN(phone)) {
    return true;
  } else {
    return false;
  }
},
// 倒计时
countdown : function(seckillId, nowTime, startTime, endTime) {
  // 时间判断
  var seckillBox = $('#seckillBox');
  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.handleSeckill(seckillId, seckillBox);
    });
  } else {
    // 秒杀开始
    seckill.handleSeckill(seckillId ,seckillBox);
  }
},
  // 详情页秒杀逻辑
  detail : {
    // 详情页初始化
    init : function(params) {
      // 用户手机验证和登录,计时交互
      // 规划我们的交互流程
      // 在cookie中查找手机号
      var killPhone = $.cookie('killPhone');
      var startTime = params['startTime'];
      var endTime = params['endTime'];
      var seckillId = params['seckillId'];
      // 验证手机号
      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 startTime = params['startTime'];
      var endTime = params['endTime'];
      var seckillId = 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['reult:'] + result);
        }
      });
    }
  }
}

posted @ 2018-03-07 22:03  将王相  阅读(143)  评论(0)    收藏  举报