在开始总结之前,先记录一个刚看到的博客,编程规约。该博客记录了一些java开发上的规范,可以在编码的时候引入这些规范。
无论流行框架一直怎么改变,web开发中的三层架构一直属于理论的基础存在。
表现层 -> 业务层 -> 持久层
箭头所指的方向就是层之间调用的方向,在SSM框架中,利用springmvc来实现表现层,利用spring来实现业务层,用mybatis来实现持久层。
简单来说,一个web网站的开发,首先明确需求以后,要先设计与需求有关的各种数据表,针对秒杀案例,用户登录网站,查看秒杀商品,完成下单,因此,最基础的需要三个表:用户表、商品表、订单表。
事实上,我们虽然做的秒杀功能,但不可能这个web只有一个秒杀的项目,而是一个商城,因此,为了便于维护我们的数据表,需要在抽象出以下两个表:秒杀商品表、秒杀订单表。
用户表:
包括用户id、昵称、密码、密码混淆盐值、用户头像、注册日期、最近登录日期、登录次数。
(用户表可以尽可能详细的将用户的所有特征加入,如果系统庞大,也可以抽象出一些子表,但这里没必要,但如果在一些实际的网站,可以秒杀的用户和主用户表肯定是分开的,否则主用户表的字段会越来越多,难以维护)
CREATE TABLE `miaosha_user` ( `id` BIGINT(20) NOT NULL COMMENT '用户ID,手机号码', `nickname` VARCHAR(255) NOT NULL, `password` VARCHAR(32) DEFAULT NULL COMMENT 'MD5(MD5(pass明文+固定salt) + salt)', `salt` VARCHAR(10) DEFAULT NULL, `head` VARCHAR(128) DEFAULT NULL COMMENT '头像,云存储的ID', `register_date` DATETIME DEFAULT NULL COMMENT '注册时间', `last_login_date` DATETIME DEFAULT NULL COMMENT '上蔟登录时间', `login_count` INT(11) DEFAULT '0' COMMENT '登录次数', PRIMARY KEY (`id`) ) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;
商品表:
包括商品ID、商品名称、商品标题、商品图片、商品的详细介绍、商品单价、商品库存
(商品表应着力于描述商品的具体特征,而不是添加秒杀的特性,理由也是为了维护系统的可用性)
CREATE TABLE `goods` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID', `goods_name` VARCHAR(16) DEFAULT NULL COMMENT '商品名称', `goods_title` VARCHAR(64) DEFAULT NULL COMMENT '商品标题', `goods_img` VARCHAR(64) DEFAULT NULL COMMENT '商品的图片', `goods_detail` LONGTEXT COMMENT '商品的详情介绍', `goods_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '商品单价', `goods_stock` INT(11) DEFAULT '0' COMMENT '商品库存,-1表示没有限制', PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
秒杀商品表 :
包括秒杀商品id、商品id、秒杀价、库存数、秒杀开始时间、秒杀结束时间
(抽象出来的秒杀商品表显然是商品表的子表,它可以拥有商品表的全部字段,但它有自己的价格,有自己的库存,增加了秒杀的时间限制,如果在商品表中增加字段,这无疑商品表会是个巨大无比的表)
CREATE TABLE `miaosha_goods` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀的商品表', `goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品Id', `miaosha_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '秒杀价', `stock_count` INT(11) DEFAULT NULL COMMENT '库存数量', `start_date` DATETIME DEFAULT NULL COMMENT '秒杀开始时间', `end_date` DATETIME DEFAULT NULL COMMENT '秒杀结束时间', PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
订单表
包括订单id、用户id、商品id、收货地址id、冗余过来的商品名称、订单上商品的数量、商品单价、订单的渠道、订单的状态、订单的创建时间、订单的支付时间
(这里有些字段是不需要的比如商品名称、商品单价。通过用户id和商品id就可以找到这些信息)
CREATE TABLE `order_info` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `user_id` BIGINT(20) DEFAULT NULL COMMENT '用户ID', `goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品ID', `delivery_addr_id` BIGINT(20) DEFAULT NULL COMMENT '收获地址ID', `goods_name` VARCHAR(16) DEFAULT NULL COMMENT '冗余过来的商品名称', `goods_count` INT(11) DEFAULT '0' COMMENT '商品数量', `goods_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '商品单价', `order_channel` TINYINT(4) DEFAULT '0' COMMENT '1pc,2android,3ios', `status` TINYINT(4) DEFAULT '0' COMMENT '订单状态,0新建未支付,1已支付,2已发货,3已收货,4已退款,5已完成', `create_date` DATETIME DEFAULT NULL COMMENT '订单的创建时间', `pay_date` DATETIME DEFAULT NULL COMMENT '支付时间', PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=1565 DEFAULT CHARSET=utf8mb4;
秒杀订单表
包括秒杀订单表id、用户id、商品id、订单id
(根据这些id,可以得到具体的秒杀订单详情,其实这里可以有一个秒杀商品的id,根据该id来获取秒杀商品的价格)
CREATE TABLE `miaosha_order` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `user_id` BIGINT(20) DEFAULT NULL COMMENT '用户ID', `order_id` BIGINT(20) DEFAULT NULL COMMENT '订单ID', `goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品ID', PRIMARY KEY (`id`), UNIQUE KEY `u_uid_gid` (`user_id`,`goods_id`) USING BTREE ) ENGINE=INNODB AUTO_INCREMENT=1551 DEFAULT CHARSET=utf8mb4;
以上就是数据库的设计。同时,有了该张数据库,我们可以更加的理清楚业务的逻辑:
用户登录页面:输入用户id和密码,传到服务器,通过查询用户表,来判断是否登录成功,成功跳转到商品的列表页面;
商品列表页面(这次项目不展示该页面):通过查询数据库,将所有商品展示在页面上。并提供一个秒杀商品列表页面的入口;
秒杀商品列表(此次项目当登录成功后直接跳转的页面):通过查询秒杀商品数据库,将所有秒杀的商品展示在页面上,并在每一个商品后面添加一个【详情】链接或按钮,点击直接跳转到秒杀商品的详情页。
秒杀商品详情页:将秒杀商品的信息展示出来,包括秒杀价、秒杀库存。秒杀的时间等,并提供一个立即秒杀的按钮,点击后执行秒杀逻辑,跳转到秒杀成功页面。
秒杀成功页面:显示秒杀成功后的订单详情,通过查询数据库,将订单的详情查出来显示。
对页面进行梳理之后,就可以创建出这四张页面,关于页面,为了前后端分离,建议使用纯html,但事实上,不可能做到完全的前后端分离,因此,用户登录页面和秒杀商品列表页面可以使用thymeleaf框架提供的标签模板, 而秒杀详情页和秒杀成功页面将采用纯html的方式辅助使用ajax请求的方式来完成数据的传递。
用户登录页面
(登录页面由三部分组成,一部分是引入了thymeleaf模板,可以依照此规则,引入标签,获取参数,然后显示。一部分是纯的html标签和css样式,对布局样式进行规定,使页面更加美观。另一部分就是完成数据传递或者页面动态展示的js代码,更多的是ajax请求,以及数据处理。)
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>登录</title> <!-- jquery --> <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script> <!-- bootstrap --> <link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}" /> <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script> <!-- jquery-validator --> <script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}"></script> <script type="text/javascript" th:src="@{/jquery-validation/localization/messages_zh.min.js}"></script> <!-- layer --> <script type="text/javascript" th:src="@{/layer/layer.js}"></script> <!-- md5.js --> <script type="text/javascript" th:src="@{/js/md5.min.js}"></script> <!-- common.js --> <script type="text/javascript" th:src="@{/js/common.js}"></script> </head> <body> <form name="loginForm" id="loginForm" method="post" style="width:50%; margin:0 auto"> <h2 style="text-align:center; margin-bottom: 20px">用户登录</h2> <div class="form-group"> <div class="row"> <label class="form-label col-md-4">请输入手机号码</label> <div class="col-md-5"> <input id="mobile" name = "mobile" class="form-control" type="text" placeholder="手机号码" required="true" minlength="11" maxlength="11" /> </div> <div class="col-md-1"> </div> </div> </div> <div class="form-group"> <div class="row"> <label class="form-label col-md-4">请输入密码</label> <div class="col-md-5"> <input id="password" name="password" class="form-control" type="password" placeholder="密码" required="true" minlength="6" maxlength="16" /> </div> </div> </div> <div class="row"> <div class="col-md-5"> <button class="btn btn-primary btn-block" type="reset" onclick="reset()">重置</button> </div> <div class="col-md-5"> <button class="btn btn-primary btn-block" type="submit" onclick="login()">登录</button> </div> </div> </form> </body> <script> function login(){ $("#loginForm").validate({ submitHandler:function(form){ doLogin(); } }); } function doLogin(){ g_showLoading(); var inputPass = $("#password").val(); var salt = g_passsword_salt; var str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4); var password = md5(str); $.ajax({ url: "/login/do_login", type: "POST", data:{ mobile:$("#mobile").val(), password: password }, success:function(data){ layer.closeAll(); if(data.code == 0){ layer.msg("成功"); window.location.href="/goods/to_list"; }else{ layer.msg(data.msg); } }, error:function(){ layer.closeAll(); } }); } </script> </html>
页面分析:
该页面通过引入bootstrap模板,来规定页面的样式。
该页面通过引入thymeleaf模板,来对数据进行动态的展示。
该页面通过引入jQuery以及jQuery-validate模板,来使用各种js函数以及对输入数据进行基础验证。
note:
就内容而言,该页面只提供了一个form表单,然后提供了id和密码的输入框。通过对标签属性的设置,规定数据的验证规则。
对于js代码,主要是一个ajax请求。对于传送的数据,基于安全原则,不能在网络中传输明文密码,因此,需要将传递的密码值加密。
ajax请求规定了接收到数据响应后的操作。
秒杀商品列表页面:
(页面由于只有展示的业务,因此,只需要根据thymeleaf模板的标签,拿到返回值并在页面上做显示。出口只提供一个详情的页面跳转)
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>商品列表</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <!-- jquery --> <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script> <!-- bootstrap --> <link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}" /> <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script> <!-- jquery-validator --> <script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}"></script> <script type="text/javascript" th:src="@{/jquery-validation/localization/messages_zh.min.js}"></script> <!-- layer --> <script type="text/javascript" th:src="@{/layer/layer.js}"></script> <!-- md5.js --> <script type="text/javascript" th:src="@{/js/md5.min.js}"></script> <!-- common.js --> <script type="text/javascript" th:src="@{/js/common.js}"></script> </head> <body> <div class="panel panel-default"> <div class="panel-heading">秒杀商品列表</div> <table class="table" id="goodslist"> <tr><td>商品名称</td><td>商品图片</td><td>商品原价</td><td>秒杀价</td><td>库存数量</td><td>详情</td></tr> <tr th:each="goods,goodsStat : ${goodsList}"> <td th:text="${goods.goodsName}"></td> <td ><img th:src="@{${goods.goodsImg}}" width="100" height="100" /></td> <td th:text="${goods.goodsPrice}"></td> <td th:text="${goods.miaoshaPrice}"></td> <td th:text="${goods.stockCount}"></td> <td><a th:href="'/goods_detail.htm?goodsId='+${goods.id}">详情</a></td> </tr> </table> </div> </body> </html>
note:
需要使用thymeleaf提供的命名空间,将页面显示出来。
详情页面由于使用静态页面,就不需要请求服务器然后跳转页面的方式了。而是直接跳转到秒杀商品的详情页面。
秒杀商品详情页面
<!DOCTYPE HTML> <html > <head> <title>商品详情</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <!-- jquery --> <script type="text/javascript" src="/js/jquery.min.js"></script> <!-- bootstrap --> <link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" /> <script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script> <!-- jquery-validator --> <script type="text/javascript" src="/jquery-validation/jquery.validate.min.js"></script> <script type="text/javascript" src="/jquery-validation/localization/messages_zh.min.js"></script> <!-- layer --> <script type="text/javascript" src="/layer/layer.js"></script> <!-- md5.js --> <script type="text/javascript" src="/js/md5.min.js"></script> <!-- common.js --> <script type="text/javascript" src="/js/common.js"></script> </head> <body> <div class="panel panel-default"> <div class="panel-heading">秒杀商品详情</div> <div class="panel-body"> <span id="userTip"> 您还没有登录,请登陆后再操作<br/></span> <span>没有收货地址的提示。。。</span> </div> <table class="table" id="goodslist"> <tr> <td>商品名称</td> <td colspan="3" id="goodsName"></td> </tr> <tr> <td>商品图片</td> <td colspan="3"><img id="goodsImg" width="200" height="200" /></td> </tr> <tr> <td>秒杀开始时间</td> <td id="startTime"></td> <td > <input type="hidden" id="remainSeconds" /> <span id="miaoshaTip"></span> </td> <td> <!-- <form id="miaoshaForm" method="post" action="/miaosha/do_miaosha"> <button class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒杀</button> <input type="hidden" name="goodsId" id="goodsId" /> </form>--> <div class="row"> <div class="form-inline"> <img id="verifyCodeImg" width="80" height="32" style="display:none" onclick="refreshVerifyCode()"/> <input id="verifyCode" class="form-control" style="display:none"/> <button class="btn btn-primary" type="button" id="buyButton" onclick="getMiaoshaPath()">立即秒杀</button> </div> </div> <input type="hidden" name="goodsId" id="goodsId" /> </td> </tr> <tr> <td>商品原价</td> <td colspan="3" id="goodsPrice"></td> </tr> <tr> <td>秒杀价</td> <td colspan="3" id="miaoshaPrice"></td> </tr> <tr> <td>库存数量</td> <td colspan="3" id="stockCount"></td> </tr> </table> </div> </body> </html> <script> function getMiaoshaPath() { g_showLoading(); //ajax请求 $.ajax({ url:"/miaosha/path", type:"GET", data:{ goodsId:$("#goodsId").val(), verifyCode:$("#verifyCode").val() }, success:function(data){ if(data.code == 0){ var path = data.data; doMiaosha(path); }else{ layer.msg(data.msg); } }, error:function(){ layer.msg("客户端请求有误"); } }); } function getMiaoshaResult(goodsId) { g_showLoading();//显示处理等待 //发起ajax请求 $.ajax({ url:"/miaosha/result", type:"GET", data:{ goodsId:$("#goodsId").val() }, success:function(data){ if (data.code==0){ var result=data.data; if(result<0){ layer.msg("对不起,秒杀失败"); }else if(result==0){ //继续轮询 setTimeout(function () { getMiaoshaResult(goodsId) },50); } else { layer.confirm("恭喜你,秒杀成功!查看订单?", {btn:["确定","取消"]}, function(){ window.location.href="/order_detail.htm?orderId="+result; }, function(){ layer.closeAll(); }); } }else { layer.msg(data.msg); } }, error:function(){ layer.msg("客户端请求有误"); } }); } function doMiaosha(path) { $.ajax({ url:"/miaosha/"+path+"/do_miaosha", type:"POST", data:{ goodsId:$("#goodsId").val(), }, success:function(data){ if(data.code == 0){ // window.location.href="/order_detail.htm?orderId="+data.data.id; //code为0,说明秒杀请求已经入队,那么需要客户端发起对服务器的ajax请求,进行轮询。 getMiaoshaResult($("#goodsId").val());//这里将逻辑写成函数 }else{ layer.msg(data.msg); } }, error:function(){ layer.msg("客户端请求有误"); } }); } $(function () { getDetail(); }); function getDetail() { //获取goodsId var goodsId=g_getQueryString("goodsId"); //设置ajax请求,得到数据 $.ajax({ url:"/goods/to_detail/"+goodsId, type:"GET", success:function (data) { if(data.code==0){ //展示数据 render(data.data); }else{ //展示错误信息 layer.msg(data.msg); } }, error:function () { //未请求成功信息 layer.msg("客户端请求有误") } }); } function render(detail) { //取到vo传过来的四个属性 var miaoshaStatus = detail.miaoshaStatus; var remainSeconds = detail.remainSeconds; var goods = detail.goods; var user = detail.user; //逻辑判断 //如果用户存在,则隐藏需要登录的提醒 if(user){ $("#userTip").hide(); } //展示数据 $("#goodsName").text(goods.goodsName); $("#goodsImg").attr("src", goods.goodsImg); $("#startTime").text(new Date(goods.startDate).format("yyyy-MM-dd hh:mm:ss")); $("#remainSeconds").val(remainSeconds); $("#goodsId").val(goods.id); $("#goodsPrice").text(goods.goodsPrice); $("#miaoshaPrice").text(goods.miaoshaPrice); $("#stockCount").text(goods.stockCount); //引入倒计时 countDown(); } function countDown() { //获取剩余时间 var remainSeconds = $("#remainSeconds").val(); //定义超时变量 var timeout; if(remainSeconds>0){ //秒杀还没有开始 //隐藏秒杀的按钮,展示倒计时提醒 $("#buyButton").attr("disabled", true); $("#miaoshaTip").html("秒杀倒计时:"+remainSeconds+"秒"); //利用setTimeout进行时间控制 timeout=setTimeout(function () { //剩余秒数减一 $("#countDown").text(remainSeconds - 1); $("#remainSeconds").val(remainSeconds - 1); countDown();//递归执行。 },1000)//里面函数每执行一次,就延时一秒。 }else if(remainSeconds==0){ //秒杀正在进行 //显示秒杀按钮 $("#buyButton").attr("disabled", false); //清理设计的超时函数 if(timeout){ clearTimeout(timeout); } $("#miaoshaTip").html("秒杀进行中"); //显示图片验证码 //此图片需要请求服务器传回 $("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val()); $("#verifyCodeImg").show(); $("#verifyCode").show(); }else { //秒杀已经结束 $("#buyButton").attr("disabled", true); $("#miaoshaTip").html("秒杀已经结束"); //秒杀失败后隐藏 $("#verifyCodeImg").hide(); $("#verifyCode").hide(); } } function refreshVerifyCode(){ $("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val()+"×tamp="+new Date().getTime()); } </script>View Code
页面分析:
该页面比较复杂,主要包括,静态页面部分、秒杀商品详情数据的返回(ajax请求,由入口函数调用得到)、点击秒杀按钮触发的秒杀逻辑。
静态页面:显示用户登录信息、商品名称、图片、秒杀开始时间。价格、库存等基础信息的标签,并提供标签id,方便利用jQuery进行获取。当静态页面加载之后,就会被客户端(浏览器)缓存,以后请求如果页面不变,就不会想服务器请求调用静态资源。
数据返回:数据通过入口函数,调用一个ajax请求来返回数据。返回的数据包括所有需要显示的数据。,当调用成功后,通过js代码来控制数据如何显示,包括秒杀的倒计时。
秒杀事件触发:这里通过ajax请求,将逻辑交给服务器执行。(源代码通过几次ajax请求,申请随机地址,然后执行秒杀逻辑,得到秒杀结果,应该还有改进的空间)
秒杀成功页面
<!DOCTYPE HTML> <html> <head> <title>订单详情</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <!-- jquery --> <script type="text/javascript" src="/js/jquery.min.js"></script> <!-- bootstrap --> <link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" /> <script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script> <!-- jquery-validator --> <script type="text/javascript" src="/jquery-validation/jquery.validate.min.js"></script> <script type="text/javascript" src="/jquery-validation/localization/messages_zh.min.js"></script> <!-- layer --> <script type="text/javascript" src="/layer/layer.js"></script> <!-- md5.js --> <script type="text/javascript" src="/js/md5.min.js"></script> <!-- common.js --> <script type="text/javascript" src="/js/common.js"></script> </head> <body> <div class="panel panel-default"> <div class="panel-heading">秒杀订单详情</div> <table class="table" id="goodslist"> <tr> <td>商品名称</td> <td colspan="3" id="goodsName"></td> </tr> <tr> <td>商品图片</td> <td colspan="2"><img id="goodsImg" width="200" height="200" /></td> </tr> <tr> <td>订单价格</td> <td colspan="2" id="orderPrice"></td> </tr> <tr> <td>下单时间</td> <td id="createDate" colspan="2"></td> </tr> <tr> <td>订单状态</td> <td id="orderStatus"> </td> <td> <button class="btn btn-primary btn-block" type="submit" id="payButton">立即支付</button> </td> </tr> <tr> <td>收货人</td> <td colspan="2">玉皇大帝</td> </tr> <tr> <td>收货地址</td> <td colspan="2">天宫一号</td> </tr> </table> </div> </body> </html> <script> //入口函数 $(function () { getOrderDetail(); }) function getOrderDetail() { //一个ajax请求 var orderId = g_getQueryString("orderId"); $.ajax({ url:"/order/detail", type:"GET", data:{ orderId:orderId }, success:function(data){ if(data.code == 0){ //展示数据 render(data.data); }else{ layer.msg(data.msg); } }, error:function(){ layer.msg("客户端请求有误"); } }); } function render(detail) { //获取商品和订单信息 var goods = detail.goods; var order = detail.order; //对数据进行展示 $("#goodsName").text(goods.goodsName); $("#goodsImg").attr("src", goods.goodsImg); $("#orderPrice").text(order.goodsPrice); $("#createDate").text(new Date(order.createDate).format("yyyy-MM-dd hh:mm:ss")); //对订单的状态进行判断 var status = ""; if(order.status == 0){ status = "未支付" }else if(order.status == 1){ status = "待发货"; $("payButton").hide(); } $("#orderStatus").text(status); } </script>View Code
页面分析:
秒杀成功的页面比较简单,和上一页面类似,主要包括,静态页面部分、秒杀商品订单详情数据的返回(ajax请求,由入口函数调用得到)、点击支付按钮触发的支付逻辑。
由于没有做支付的业务逻辑,因此,此页面只有一个ajax请求来回去展示数据。
通过以上页面和数据库的创建和分析,其实已经大致摸清了整个秒杀项目的时序图:
登录页面 -> (ajax传数据) ->表现层(Controller)->返回数据 -> (跳转到商品列表的处理的类或者显示错误信息)
商品列表的处理类 -> 封装需要的数据 ->返回页面 -> 展示页面
(上述过程是一个时序,不需要用户进行输入或者点击)
点击详情 -> 跳转到商品详情页面(注意是页面,不是服务器控制类)-> 页面入口函数 -> ajax请求(获取页面需要展示的数据) ->返回数据,显示数据。
(上述过程是一个时序,不需要用户进行输入或者点击)
点击秒杀 -> 跳转获取秒杀路径 ->返回数据 ->跳转秒杀实际逻辑 ->返回数据 ->执行轮询请求判断是否秒杀成功 -> 秒杀成功跳转订单详情页。
梳理了以上时序图,可以在搭建完整的代码。
构建项目环境,使用springboot以及mybatis构建,maven自动导入项目坐标。
maven导入坐标的pom文件
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.8.RELEASE</version> </parent> <name>miaosha_2</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.5</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.38</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.6</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.6</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> </dependencies>View Code
notes:
如果加载坐标太慢,可以建立私服,或者建立本地仓库。
创建配置文件application.properties
notes:可以暂时不用配置任何参数,用到什么框架配置什么内容。
创建包以及资源文件夹,并将静态资源引入
具体项目目录就如上图所示。
controller包表示表现层代码、service包表示业务层代码、dao包表示持久层代码、entity包表示实体类代码(domain与数据库传递的实体、bo与业务层传递的实体、vo与表现层传递的实体)、util包表示一些配置或者其他的工具类。
APP类是项目启动类
package com.miaosha; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class APP { public static void main(String[] args) { SpringApplication.run(APP.class); } }
实现登录功能
所用技术:redis缓存、参数验证(自定义注解)、全局异常处理、返回数据抽象。
业务逻辑分析:
第一步,页面发起ajax请求,将数据传给服务器。服务器首先将数据进行参数验证,若参数不符合规则,直接返回错误信息。
第二步,若参数验证通过,将从缓存中取到用户数据,若缓存中没有数据,则从数据库中查询,然后存到缓存中。
第三步,将取到的用户数据与页面传递的数据比较,若错误,则返回错误信息,若正确,则返回正确信息。
以上是对业务的简单分析,但涉及到分布式,可能不同服务器之间没有共通的session,因此如果传到其他页面,user就会失效。因此需要考虑如何能获取到user的值。
同时,根据分析,可以知道,需要参数验证,如果每次在controller类中进行参数验证,势必会使系统很冗余,因此,采用注解的方式对参数进行验证。
还有就是返回值,返回值抽象出来管理,不同的控制器返回不同类型的值,有可能返回一个基本类型,有可能返回实体类,也有可能返回错误信息。
对结果集的封装
结果集是指返回结果,起码包含两部分,一部分是提示信息,一部分是真正的返回值,因此他是一个vo的实体类
该类还需要进一步抽象出一个专门存储错误消息的类,因为我们在返回过程中会有很多的消需要提醒。
分析:结果集需要哪些内容,一般来说,一个是数字提示的代码,一个是文字提示的消息,一个是真正需要返回的vo,result其实也是vo,是封装了的vo。
需要提供两个方法,业务正确时的success方法,业务出现错误时的error方法,如果业务正确,则代码和消息是固定的,但需要传递不同类型的数据;如果业务错误,则代码和消息不固定,但不需要传递数据。
因此抽象一个泛型的Result类来返回类,将代码和消息封装到CodeMsg类中,提供一系列静态实例。
package com.miaosha.entity.vo.result; public class CodeMsg { private Integer code; //返回代码 private String msg; //返回信息 private CodeMsg( ) { } private CodeMsg( int code,String msg ) { this.code = code; this.msg = msg; } //通用的错误码 public static CodeMsg SUCCESS = new CodeMsg(0, "success"); public static CodeMsg SERVER_ERROR = new CodeMsg(500100, "服务端异常"); public static CodeMsg BIND_ERROR = new CodeMsg(500101, "参数校验异常:%s"); public static CodeMsg REQUEST_ILLEGAL = new CodeMsg(500102, "请求非法"); public static CodeMsg ACCESS_LIMIT_REACHED= new CodeMsg(500104, "访问太频繁!"); //登录模块 5002XX public static CodeMsg SESSION_ERROR = new CodeMsg(500210, "Session不存在或者已经失效"); public static CodeMsg PASSWORD_EMPTY = new CodeMsg(500211, "登录密码不能为空"); public static CodeMsg MOBILE_EMPTY = new CodeMsg(500212, "手机号不能为空"); public static CodeMsg MOBILE_ERROR = new CodeMsg(500213, "手机号格式错误"); public static CodeMsg MOBILE_NOT_EXIST = new CodeMsg(500214, "手机号不存在"); public static CodeMsg PASSWORD_ERROR = new CodeMsg(500215, "密码错误"); //商品模块 5003XX //订单模块 5004XX public static CodeMsg ORDER_NOT_EXIST = new CodeMsg(500400, "订单不存在"); //秒杀模块 5005XX public static CodeMsg MIAO_SHA_OVER = new CodeMsg(500500, "商品已经秒杀完毕"); public static CodeMsg REPEATE_MIAOSHA = new CodeMsg(500501, "不能重复秒杀"); public static CodeMsg MIAOSHA_FAIL = new CodeMsg(500502, "秒杀失败"); public Integer getCode() { return code; } public String getMsg() { return msg; } }CodeMsg
package com.miaosha.entity.vo.result; public class Result <T> { private Integer code; //返回代码 private String msg; //返回信息 private T date;//返回实体 private Result(CodeMsg codeMsg){ if(codeMsg!=null) { this.code = codeMsg.getCode(); this.msg = codeMsg.getMsg(); this.date=null; } } private Result(T date){ this.code=0; this.msg="success"; this.date=date; } public static <T> Result<T> success(T date){ return new Result<T> (date); } public static <T> Result<T> error(CodeMsg codeMsg){ return new Result<T>(codeMsg); } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { msg = msg; } public T getDate() { return date; } public void setDate(T date) { this.date = date; } }Result
这种封装能极大的提供代码的可用性,扩展性。
对登录信息实体类的封装
对于登录信息传递过来的值,显然用不到用户表中的那么多字段,只需要得到两个字段,一个用户id,一个用户密码。而且该类需要和表现层交互,因此也是在vo包创建
package com.miaosha.entity.vo; public class LoginVO { private Long mobile; private String password; public Long getMobile() { return mobile; } public void setMobile(Long mobile) { this.mobile = mobile; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }LoginVO
该类的属性名需要和页面上手机号码和密码的name属性一致(这样做是为了让springboot能自动识别bean)
实现验证功能
当页面将数据传到Controller类后,需要对登录数据的格式进行验证(是否为空,长度是否正确,格式是否正确。。。)
使用JSR-303标准的验证形式,在jdk1.8中支持这样的验证。
步骤1:在LoginVO(即待验证的属性上加上需要验证的注解项,@NotNull。。。)
package com.miaosha.entity.vo; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotNull; public class LoginVO { @NotNull private Long mobile; @NotNull @Length(min = 32) private String password; public Long getMobile() { return mobile; } public void setMobile(Long mobile) { this.mobile = mobile; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }LoginVO
步骤2:在LoginController类的doLogin方法的参数LoginVo前加上@Valid注解。
package com.miaosha.controller; import com.miaosha.entity.vo.LoginVO; import com.miaosha.entity.vo.result.Result; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.validation.Valid; @Controller @RequestMapping("/login") public class LoginController { //去登录页面 @RequestMapping("/to_login") public String toLogin(){ return "login"; } //执行登录 @RequestMapping("/do_login") @ResponseBody public Result<Boolean> doLogin(@Valid LoginVO loginVO){ return Result.success(true); } }LoginController
验证原理:让注解的逻辑类实现ConstraintValidator接口的方法,确定是否验证通过,验证通过返回true,没有通过返回false,当没有通过后,会抛出一个BindException类型的异常,异常信息就是注解中默认的信息。
自定义验证注解
根据对验证原理的分析,可以通过本身的业务需求自定义一个验证的注解——@IsMobile
步骤1:定义一个IsMobile注解,并继承Constraint注解,来指定一个继承了ConstraintValidator接口的逻辑类。
package com.miaosha.util.valid; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {IsMobileValidator.class }) public @interface IsMobile { boolean required() default true; String message() default "手机号码格式错误"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; }IsMobile
步骤2:实现该注解的逻辑类
在初始方法中,将请求初始化为默认的请求。
在验证逻辑判断方法中,如果验证成功,返回true,若验证失败则返回false。
package com.miaosha.util.valid; import com.miaosha.util.valid.IsMobile; import org.apache.commons.lang3.StringUtils; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class IsMobileValidator implements ConstraintValidator<IsMobile, String> { private boolean required = false; @Override public void initialize(IsMobile isMobile) { required = isMobile.required(); } @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { if(required) { return ValidatorUtil.isMobile(s); }else { if(StringUtils.isEmpty(s)) { return true; }else { return ValidatorUtil.isMobile(s); } } } }IsMobileValidator
package com.miaosha.util.valid; import org.apache.commons.lang3.StringUtils; import java.util.regex.Matcher; import java.util.regex.Pattern; public class ValidatorUtil { private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}"); public static boolean isMobile(String src) { if(StringUtils.isEmpty(src)) { return false; } Matcher m = mobile_pattern.matcher(src); return m.matches(); } }ValidatorUtil
查看页面返回结果:
{"timestamp":1582541833111,"status":400,"error":"Bad Request","exception":"org.springframework.validation.BindException","errors":[{"codes":["IsMobile.loginVO.mobile","IsMobile.mobile","IsMobile.java.lang.String","IsMobile"],"arguments":[{"codes":["loginVO.mobile","mobile"],"arguments":null,"defaultMessage":"mobile","code":"mobile"},true],"defaultMessage":"手机号码格式错误","objectName":"loginVO","field":"mobile","rejectedValue":"23588038176","bindingFailure":false,"code":"IsMobile"}],"message":"Validation failed for object='loginVO'. Error count: 1","path":"/login/do_login"}
确实可以看到返回了我们需要的默认错误消息。只不过我们不需要这样的返回规则,那么需要一个统一处理异常的类,来对异常做统一的返回类型处理。
异常统一处理
springboot提供了可以统一处理异常的机制,但在此之前,我们需要自定义一个全局异常类,该类用于返回各种我们人为抛出的业务逻辑异常。比如用户不存在,密码错误等信息。
步骤1:创建全局异常类,提供自定义异常信息的方法
package com.miaosha.util.exception; import com.miaosha.entity.vo.result.CodeMsg; public class GlobalException extends RuntimeException { private CodeMsg cm; public GlobalException(CodeMsg cm){ super(cm.toString()); this.cm=cm; } public CodeMsg getCm() { return cm; } }GlobalException
步骤2:对异常进行统一管理,具体就是将各种异常规则化为之前定义的Result返回。根据的是springboot提供的ControllerAdvice注解和ExceptionHandler注解
package com.miaosha.util.exception; import com.miaosha.entity.vo.result.CodeMsg; import com.miaosha.entity.vo.result.Result; import org.springframework.validation.BindException; import org.springframework.validation.ObjectError; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import java.util.List; @ControllerAdvice @ResponseBody public class GlobalExceptionHandler { @ExceptionHandler(value=Exception.class) public Result<String> exceptionHandler(HttpServletRequest request, Exception e){ e.printStackTrace(); if(e instanceof GlobalException) { GlobalException ex = (GlobalException)e; return Result.error(ex.getCm()); }else if(e instanceof BindException) { BindException ex = (BindException)e; List<ObjectError> errors = ex.getAllErrors(); ObjectError error = errors.get(0); String msg = error.getDefaultMessage(); return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg)); }else { return Result.error(CodeMsg.SERVER_ERROR); } } }GlobalExceptionHandler
这里的ControllerAdvice注解会在启动的时候加加载初始化bean,将GlobalExceptionHandler类扫描进包,然后通过ExceptionHandler注解对异常统一管理
返回结果如下:
{"code":500101,"msg":"参数校验异常:手机号码格式错误","date":null}
redis缓存技术
jedis类似于jdbc是一个redis的操作api,如果要用redis技术,需要对jedis进行一定的配置,并最后能封装一个service方法来调用jedis。
步骤1:对jedis进行配置,先在application.properties配置文件中引入配置信息,然后通过ConfigurationProperties注解,对配置信息进行解析,得到所有的配置参数,包括redis的主机号和端口等
#redis redis.host=10.110.3.62 redis.port=6379 redis.timeout=10 redis.password=123456 redis.poolMaxTotal=1000 redis.poolMaxIdle=500 redis.poolMaxWait=500
步骤二:解析配置文件,得到将这些配置信息转换为属性,方便操作。
package com.miaosha.util.redisconfig; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "redis") public class RedisConfig { private String host; private int port; private int timeout;//秒 private String password; private int poolMaxTotal; private int poolMaxIdle; private int poolMaxWait;//秒 public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getPoolMaxTotal() { return poolMaxTotal; } public void setPoolMaxTotal(int poolMaxTotal) { this.poolMaxTotal = poolMaxTotal; } public int getPoolMaxIdle() { return poolMaxIdle; } public void setPoolMaxIdle(int poolMaxIdle) { this.poolMaxIdle = poolMaxIdle; } public int getPoolMaxWait() { return poolMaxWait; } public void setPoolMaxWait(int poolMaxWait) { this.poolMaxWait = poolMaxWait; } }RedisConfig
步骤三:封装一个可以获取redis池对象的方法,这是一个工厂类,类似于工厂模式,得到了JedisPook对象
package com.miaosha.util.redisconfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; @Component public class RedisPoolFactory { @Autowired private RedisConfig redisConfig; @Bean public JedisPool JedisPoolFactory(){ JedisPoolConfig jedisPoolConfig=new JedisPoolConfig(); jedisPoolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait()); jedisPoolConfig.setMaxTotal(redisConfig.getPoolMaxTotal()); jedisPoolConfig.setMaxIdle(redisConfig.getPoolMaxIdle()); JedisPool jedisPool=new JedisPool(jedisPoolConfig,redisConfig.getHost(),redisConfig.getPort(),redisConfig.getTimeout() *1000,null,0); return jedisPool; } }RedisPoolFactory
步骤四:根据这样的池对象封装一些操作的方法供缓存使用,之后就可以像类似于redisTemplate对象一样了。因此,将其命名为MyResidTemplate类
package com.miaosha.util.redisconfig; import java.util.ArrayList; import java.util.List; import com.miaosha.util.redisconfig.key.KeyPrefix; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.alibaba.fastjson.JSON; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.ScanParams; import redis.clients.jedis.ScanResult; @Service public class MyRedisTemplate { @Autowired private JedisPool jedisPool; /** * 获取当个对象 * */ public <T> T get(KeyPrefix prefix, String key, Class<T> clazz) { Jedis jedis = null; try { jedis = jedisPool.getResource(); //生成真正的key String realKey = prefix.getPrefix() + key; String str = jedis.get(realKey); T t = stringToBean(str, clazz); return t; }finally { returnToPool(jedis); } } /** * 设置对象 * */ public <T> boolean set(KeyPrefix prefix, String key, T value) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String str = beanToString(value); if(str == null || str.length() <= 0) { return false; } //生成真正的key String realKey = prefix.getPrefix() + key; int seconds = prefix.expireSeconds(); if(seconds <= 0) { jedis.set(realKey, str); }else { jedis.setex(realKey, seconds, str); } return true; }finally { returnToPool(jedis); } } /** * 判断key是否存在 * */ public <T> boolean exists(KeyPrefix prefix, String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); //生成真正的key String realKey = prefix.getPrefix() + key; return jedis.exists(realKey); }finally { returnToPool(jedis); } } /** * 删除 * */ public boolean delete(KeyPrefix prefix, String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); //生成真正的key String realKey = prefix.getPrefix() + key; long ret = jedis.del(realKey); return ret > 0; }finally { returnToPool(jedis); } } /** * 增加值 * */ public <T> Long incr(KeyPrefix prefix, String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); //生成真正的key String realKey = prefix.getPrefix() + key; return jedis.incr(realKey); }finally { returnToPool(jedis); } } /** * 减少值 * */ public <T> Long decr(KeyPrefix prefix, String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); //生成真正的key String realKey = prefix.getPrefix() + key; return jedis.decr(realKey); }finally { returnToPool(jedis); } } public boolean delete(KeyPrefix prefix) { if(prefix == null) { return false; } List<String> keys = scanKeys(prefix.getPrefix()); if(keys==null || keys.size() <= 0) { return true; } Jedis jedis = null; try { jedis = jedisPool.getResource(); jedis.del(keys.toArray(new String[0])); return true; } catch (final Exception e) { e.printStackTrace(); return false; } finally { if(jedis != null) { jedis.close(); } } } public List<String> scanKeys(String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); List<String> keys = new ArrayList<String>(); String cursor = "0"; ScanParams sp = new ScanParams(); sp.match("*"+key+"*"); sp.count(100); do{ ScanResult<String> ret = jedis.scan(cursor, sp); List<String> result = ret.getResult(); if(result!=null && result.size() > 0){ keys.addAll(result); } //再处理cursor cursor = ret.getStringCursor(); }while(!cursor.equals("0")); return keys; } finally { if (jedis != null) { jedis.close(); } } } public static <T> String beanToString(T value) { if(value == null) { return null; } Class<?> clazz = value.getClass(); if(clazz == int.class || clazz == Integer.class) { return ""+value; }else if(clazz == String.class) { return (String)value; }else if(clazz == long.class || clazz == Long.class) { return ""+value; }else { return JSON.toJSONString(value); } } @SuppressWarnings("unchecked") public static <T> T stringToBean(String str, Class<T> clazz) { if(str == null || str.length() <= 0 || clazz == null) { return null; } if(clazz == int.class || clazz == Integer.class) { return (T)Integer.valueOf(str); }else if(clazz == String.class) { return (T)str; }else if(clazz == long.class || clazz == Long.class) { return (T)Long.valueOf(str); }else { return JSON.toJavaObject(JSON.parseObject(str), clazz); } } private void returnToPool(Jedis jedis) { if(jedis != null) { jedis.close(); } } }MyRedisTemplate
该类中实现了jedis操作缓存的若干方法,包括向缓存中存入对象(泛型)、取出对象、判断key值是否存在、删除对象、自增、自减、获取所有key。
但在此之前,需要抽象一个有关key的类,redis缓存技术是基于key-value格式去缓存的,而对于一个项目而言,key值有很多个,因此,需要通过接口 -> 抽象类 ->实体类的方式,对key值进行扩展。一个key值包括,一个前缀和一个真正的key值名称,同时还需要包括这个key值在redis中的存活时间。
接口 -> 抽象类 -> 实现类
步骤1:抽象一个接口,提供两个方法,一个是过期时间,一个是key的前缀
package com.miaosha.util.redisconfig.key; public interface KeyPrefix { int expireSeconds(); String getPrefix(); }KeyPrefix
步骤2:继承接口的抽象类
package com.miaosha.util.redisconfig.key; public class BasePrefix implements KeyPrefix { private int expireSeconds; private String prefix; public BasePrefix(String prefix) {//0代表永不过期 this(0, prefix); } public BasePrefix( int expireSeconds, String prefix) { this.expireSeconds = expireSeconds; this.prefix = prefix; } public int expireSeconds() {//默认0代表永不过期 return expireSeconds; } public String getPrefix() { String className = getClass().getSimpleName(); return className+":" + prefix; } }BasePrefix
步骤3:实现抽象类
//该类在具体用到的时候进行抽象。
登录逻辑
登录逻辑分析:
步骤1:得到页面传过来的mobile、password。并根据mobile值从缓存中去找user是否存在。
步骤2:若缓存中有user,则执行下一步,比较密码是否一致。若缓存中没有user,则需要从数据库中找,如果找到,则执行下一步,比较密码是否一致。若也不存在,则抛出全局异常。
步骤3:验证密码,密码在页面传过来时已经经过md5加密,而且数据库中所存的密码是执行了两个md5的结果,因此,对密码应该进行一次md5加密才能进行比较,若密码相同,则说明信息正确,若不相同,抛出异常。
步骤4:若密码相同,说明登录成功,则需要向页面传回一个cookie值,将cookie值存在页面上,保证跳转到其他服务器中,session值已经过期的情况下,也可以在其他页面拿到现在得到的user值,因此,该cookie值应当对应于缓存中的一个key值,即需要在缓存中存入一个key值为cookie值的user。
步骤5:返回成功信息。
密码工具类:
package com.miaosha.util; import org.apache.commons.codec.digest.DigestUtils; public class MD5Util { public static String md5(String src) { return DigestUtils.md5Hex(src); } private static final String salt = "1a2b3c4d"; public static String inputPassToFormPass(String inputPass) { String str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4); System.out.println(str); return md5(str); } public static String formPassToDBPass(String formPass, String salt) { String str = ""+salt.charAt(0)+salt.charAt(2) + formPass +salt.charAt(5) + salt.charAt(4); return md5(str); } public static String inputPassToDbPass(String inputPass, String saltDB) { String formPass = inputPassToFormPass(inputPass); String dbPass = formPassToDBPass(formPass, saltDB); return dbPass; } }MD5Util
随机COOKIE工具类:
package com.miaosha.util; import java.util.UUID; public class UUIDUtil { public static String uuid() { return UUID.randomUUID().toString().replace("-", ""); } }UUIDUtil
登录的控制类
package com.miaosha.controller; import com.miaosha.entity.vo.LoginVO; import com.miaosha.entity.vo.result.Result; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.validation.Valid; @Controller @RequestMapping("/login") public class LoginController { private static Logger log = LoggerFactory.getLogger(LoginController.class); @Autowired MiaoshaUserService userService; //去登录页面 @RequestMapping("/to_login") public String toLogin(){ return "login"; } //执行登录 @RequestMapping("/do_login") @ResponseBody public Result<String> doLogin(@Valid LoginVO loginVO){ log.info(loginVO.toString()); //登录 String token = userService.login(response, loginVo); return Result.success(token); } }LoginController
属性加入MiaoshaUserService类来提供登录验证的具体方法,这里登录验证的逻辑无言多说,但对于加入cookie的方法,其作用是,在登录后,可以在客户端存一份cookie值,当跳转到其他页面后,有可能服务器的seeison值因为分布式系统而取不到用户,因此可以通过客户端存的cookie值去缓存中取。
用户登录的业务类
package com.miaosha.service; import com.miaosha.entity.domain.MiaoshaUser; import com.miaosha.entity.vo.LoginVO; import com.miaosha.entity.vo.result.CodeMsg; import com.miaosha.util.MD5Util; import com.miaosha.util.UUIDUtil; import com.miaosha.util.exception.GlobalException; import com.miaosha.util.redisconfig.MyRedisTemplate; import com.miaosha.util.redisconfig.key.MiaoshaUserKey; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; @Service public class MiaoshaUserService { public static final String COOKI_NAME_TOKEN = "token"; @Autowired MiaoshaUserDao miaoshaUserDao; @Autowired MyRedisTemplate redisService; public MiaoshaUser getById(long id) { //取缓存 MiaoshaUser user = redisService.get(MiaoshaUserKey.getById, ""+id, MiaoshaUser.class); if(user != null) { return user; } //取数据库 user = miaoshaUserDao.getById(id); if(user != null) { redisService.set(MiaoshaUserKey.getById, ""+id, user); } return user; } // http://blog.csdn.net/tTU1EvLDeLFq5btqiK/article/details/78693323 public boolean updatePassword(String token, long id, String formPass) { //取user MiaoshaUser user = getById(id); if(user == null) { throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST); } //更新数据库 MiaoshaUser toBeUpdate = new MiaoshaUser(); toBeUpdate.setId(id); toBeUpdate.setPassword(MD5Util.formPassToDBPass(formPass, user.getSalt())); miaoshaUserDao.update(toBeUpdate); //处理缓存 redisService.delete(MiaoshaUserKey.getById, ""+id); user.setPassword(toBeUpdate.getPassword()); redisService.set(MiaoshaUserKey.token, token, user); return true; } public MiaoshaUser getByToken(HttpServletResponse response, String token) { if(StringUtils.isEmpty(token)) { return null; } MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class); //延长有效期 if(user != null) { addCookie(response, token, user); } return user; } public String login(HttpServletResponse response, LoginVO loginVo) { if(loginVo == null) { throw new GlobalException(CodeMsg.SERVER_ERROR); } String mobile = loginVo.getMobile(); String formPass = loginVo.getPassword(); //判断手机号是否存在 MiaoshaUser user = getById(Long.parseLong(mobile)); if(user == null) { throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST); } //验证密码 String dbPass = user.getPassword(); String saltDB = user.getSalt(); String calcPass = MD5Util.formPassToDBPass(formPass, saltDB); if(!calcPass.equals(dbPass)) { throw new GlobalException(CodeMsg.PASSWORD_ERROR); } //生成cookie String token = UUIDUtil.uuid(); addCookie(response, token, user); return token; } private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) { redisService.set(MiaoshaUserKey.token, token, user); Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token); cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds()); cookie.setPath("/"); response.addCookie(cookie); } }MiaoshaUserService
用户的查询数据库的类
package com.miaosha.service; import com.miaosha.entity.domain.MiaoshaUser; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; @Mapper public interface MiaoshaUserDao { @Select("select * from miaosha_user where id = #{id}") public MiaoshaUser getById(@Param("id")long id); @Update("update miaosha_user set password = #{password} where id = #{id}") public void update(MiaoshaUser toBeUpdate); }MiaoshaUserDao
以上就是登录功能的实现,可以说我们在前期做了很多的工作,使用了很多的技术,其实就是为了在后面编程的过程中很好继续下去,这种代码的可用性很高。
秒杀功能的实现
所用的技术:页面静态化、缓存技术、RabbitMQ技术、路径隐藏、内存标记。。。
业务逻辑的分析:
第一步:登录成功后,请求服务器去返回商品列表页面,将使用thymeleaf技术将页面和数据进行交互,展示商品页面。
第二步:点击详情页面,跳转到商品的详情页面,此次跳转不经过服务器,实现页面的静态化。跳转之后,在入口函数调用ajax请求,请求返回页面展示需要的数据。
第三步:请求展示的数据有验证码的数据,但不需要返回,是通过读取内存得到的,对验证码图片进行显示。
第四步:输入验证码,并点击秒杀按钮,获取秒杀的请求服务器路径,返回路径后,请求服务器的执行秒杀功能的请求。
第五步:判断是否库存足够,并判断是否已经下单,然后将用户和商品id放入rabbitMQ队列中,等到队列出队。
第六步:队列出队执行秒杀的逻辑,减库存。下订单。
第七步:在浏览器中的返回成功逻辑中不停的请求服务器,判断是否下订单成功,如果成功,则跳转到订单详情页面。
第八步:订单详情页面的入口函数请求服务器,返回展示页面的数据。
各类实体类
首先domian包里的类要与数据库的相关联,回顾前面的数据库表,商品表、秒杀商品表、订单表、秒杀订单表。那么对应的domain的实体类应该有Goods、MiaoshaGoods、OrderInfo、MiaoshaOrder。
这一类的实体类很好创建,只要其属性和数据库字段一一对应,然后添加相应的getset方法。
package com.miaosha.entity.domain; public class Goods { private Long id; private String goodsName; private String goodsTitle; private String goodsImg; private String goodsDetail; private Double goodsPrice; private Integer goodsStock; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getGoodsName() { return goodsName; } public void setGoodsName(String goodsName) { this.goodsName = goodsName; } public String getGoodsTitle() { return goodsTitle; } public void setGoodsTitle(String goodsTitle) { this.goodsTitle = goodsTitle; } public String getGoodsImg() { return goodsImg; } public void setGoodsImg(String goodsImg) { this.goodsImg = goodsImg; } public String getGoodsDetail() { return goodsDetail; } public void setGoodsDetail(String goodsDetail) { this.goodsDetail = goodsDetail; } public Double getGoodsPrice() { return goodsPrice; } public void setGoodsPrice(Double goodsPrice) { this.goodsPrice = goodsPrice; } public Integer getGoodsStock() { return goodsStock; } public void setGoodsStock(Integer goodsStock) { this.goodsStock = goodsStock; } }Goods
package com.miaosha.entity.domain; import java.util.Date; public class MiaoshaGoods { private Long id; private Long goodsId; private Integer stockCount; private Date startDate; private Date endDate; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getGoodsId() { return goodsId; } public void setGoodsId(Long goodsId) { this.goodsId = goodsId; } public Integer getStockCount() { return stockCount; } public void setStockCount(Integer stockCount) { this.stockCount = stockCount; } public Date getStartDate() { return startDate; } public void setStartDate(Date startDate) { this.startDate = startDate; } public Date getEndDate() { return endDate; } public void setEndDate(Date endDate) { this.endDate = endDate; } }MiaoshaGoods
package com.miaosha.entity.domain; import java.util.Date; public class OrderInfo { private Long id; private Long userId; private Long goodsId; private Long deliveryAddrId; private String goodsName; private Integer goodsCount; private Double goodsPrice; private Integer orderChannel; private Integer status; private Date createDate; private Date payDate; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public Long getGoodsId() { return goodsId; } public void setGoodsId(Long goodsId) { this.goodsId = goodsId; } public Long getDeliveryAddrId() { return deliveryAddrId; } public void setDeliveryAddrId(Long deliveryAddrId) { this.deliveryAddrId = deliveryAddrId; } public String getGoodsName() { return goodsName; } public void setGoodsName(String goodsName) { this.goodsName = goodsName; } public Integer getGoodsCount() { return goodsCount; } public void setGoodsCount(Integer goodsCount) { this.goodsCount = goodsCount; } public Double getGoodsPrice() { return goodsPrice; } public void setGoodsPrice(Double goodsPrice) { this.goodsPrice = goodsPrice; } public Integer getOrderChannel() { return orderChannel; } public void setOrderChannel(Integer orderChannel) { this.orderChannel = orderChannel; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } public Date getPayDate() { return payDate; } public void setPayDate(Date payDate) { this.payDate = payDate; } }OrderInfo
package com.miaosha.entity.domain; public class MiaoshaOrder { private Long id; private Long userId; private Long orderId; private Long goodsId; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public Long getOrderId() { return orderId; } public void setOrderId(Long orderId) { this.orderId = orderId; } public Long getGoodsId() { return goodsId; } public void setGoodsId(Long goodsId) { this.goodsId = goodsId; } }MiaoshaOrder
在vo包下,分析业务逻辑,可以知道在商品列表页面,需要一个展示所有商品的属性,因此可以抽象出一个vo类:GoodsVO;在商品详情页面,可以抽象出GoodsDetailVo类,在订单详情页面也可以抽象出一个实体类:OrderDetailVo类。总结来说,一个页面对应一个vo实体类。
package com.miaosha.entity.vo; import com.miaosha.entity.domain.Goods; import java.util.Date; public class GoodsVo extends Goods { private Double miaoshaPrice; private Integer stockCount; private Date startDate; private Date endDate; public Double getMiaoshaPrice() { return miaoshaPrice; } public void setMiaoshaPrice(Double miaoshaPrice) { this.miaoshaPrice = miaoshaPrice; } public Integer getStockCount() { return stockCount; } public void setStockCount(Integer stockCount) { this.stockCount = stockCount; } public Date getStartDate() { return startDate; } public void setStartDate(Date startDate) { this.startDate = startDate; } public Date getEndDate() { return endDate; } public void setEndDate(Date endDate) { this.endDate = endDate; } }GoodsVo
package com.miaosha.entity.vo; import com.miaosha.entity.domain.MiaoshaUser; public class GoodsDetailVo { private int miaoshaStatus = 0; private int remainSeconds = 0; private GoodsVo goods ; private MiaoshaUser user; public int getMiaoshaStatus() { return miaoshaStatus; } public void setMiaoshaStatus(int miaoshaStatus) { this.miaoshaStatus = miaoshaStatus; } public int getRemainSeconds() { return remainSeconds; } public void setRemainSeconds(int remainSeconds) { this.remainSeconds = remainSeconds; } public GoodsVo getGoods() { return goods; } public void setGoods(GoodsVo goods) { this.goods = goods; } public MiaoshaUser getUser() { return user; } public void setUser(MiaoshaUser user) { this.user = user; } }GoodsDetailVo
package com.miaosha.entity.vo; import com.miaosha.entity.domain.OrderInfo; public class OrderDetailVo { private GoodsVo goods; private OrderInfo order; public GoodsVo getGoods() { return goods; } public void setGoods(GoodsVo goods) { this.goods = goods; } public OrderInfo getOrder() { return order; } public void setOrder(OrderInfo order) { this.order = order; } }OrderDetailVo
以上就是我们所有与业务直接相关的实体类
这里我们就将数据库的字段和实体类之间做了初步的映射,利用mybaties框架,可以很轻松的将其一一对应。这在mybaties的配置文件中已经有了说明
# mybatis mybatis.type-aliases-package=com.miaosha.entity mybatis.configuration.map-underscore-to-camel-case=true #开启驼峰验证,对应关系 mybatis.configuration.default-fetch-size=100 mybatis.configuration.default-statement-timeout=3000 #mybatis.mapperLocations = classpath:com/imooc/miaosha/dao/*.xml
持久层操作接口
为了实现IOC原则,尽量使用注入原则,dao类可以使用接口加注解来实现,之后就只使用注入的方式创建对象,减低耦合性。
dao包中的持久层类应该与domain包下的实体类相对应,并兼顾业务逻辑。
分为两大类,一类是商品相关的,一类是订单相关的。
在商品列表页面,需要返回所有的商品列表,因此,需要将所有的GoodsVO都查询出来,包装在list里面 如 List<GoodsVo> listGoodsVo();
在业务判断的过程中也需要获取GoodsVO,经常传过来的有user和goosId。 如GoodsVo getGoodsVoByGoodsId(@Param("goodsId")long goodsId);
在执行秒杀操作的时候,需要更新秒杀商品表的库存 如int reduceStock(MiaoshaGoods g);
商品的dao如下:
package com.miaosha.dao; import com.miaosha.entity.domain.MiaoshaGoods; import com.miaosha.entity.vo.GoodsVo; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import java.util.List; @Mapper public interface GoodsDao { @Select("select g.*,mg.stock_count, mg.start_date, mg.end_date,mg.miaosha_price from miaosha_goods mg left join goods g on mg.goods_id = g.id") List<GoodsVo> listGoodsVo(); @Select("select g.*,mg.stock_count, mg.start_date, mg.end_date,mg.miaosha_price from miaosha_goods mg left join goods g on mg.goods_id = g.id where g.id = #{goodsId}") GoodsVo getGoodsVoByGoodsId(@Param("goodsId")long goodsId); @Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0") int reduceStock(MiaoshaGoods g); @Update("update miaosha_goods set stock_count = #{stockCount} where goods_id = #{goodsId}") int resetStock(MiaoshaGoods g); }GoodsDao
package com.miaosha.dao; import com.miaosha.entity.domain.MiaoshaOrder; import com.miaosha.entity.domain.OrderInfo; import org.apache.ibatis.annotations.*; @Mapper public interface OrderDao { @Select("select * from miaosha_order where user_id=#{userId} and goods_id=#{goodsId}") public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(@Param("userId")long userId, @Param("goodsId")long goodsId); @Insert("insert into order_info(user_id, goods_id, goods_name, goods_count, goods_price, order_channel, status, create_date)values(" + "#{userId}, #{goodsId}, #{goodsName}, #{goodsCount}, #{goodsPrice}, #{orderChannel},#{status},#{createDate} )") @SelectKey(keyColumn="id", keyProperty="id", resultType=long.class, before=false, statement="select last_insert_id()") public long insert(OrderInfo orderInfo); @Insert("insert into miaosha_order (user_id, goods_id, order_id)values(#{userId}, #{goodsId}, #{orderId})") public int insertMiaoshaOrder(MiaoshaOrder miaoshaOrder); @Select("select * from order_info where id = #{orderId}") public OrderInfo getOrderById(@Param("orderId")long orderId); @Delete("delete from order_info") public void deleteOrders(); @Delete("delete from miaosha_order") public void deleteMiaoshaOrders(); }OrderDao
商品列表页面
登录成功以后,跳转到服务器的goods/tolist请求,在此页面,获得商品的列表,然后在页面显示,这里使用了页面缓存的技术,将整个页面缓存在redis中,除了第一次请求,以后每次请求都将从缓存中直接取到html页面。
但在此之前,我们需要提供一个配置类,继承一个WebMvcConfigurerAdapter类,此类是一个在启动后就配置的类,常用方法如下:
/** 解决跨域问题 **/ public void addCorsMappings(CorsRegistry registry) ; /** 添加拦截器 **/ void addInterceptors(InterceptorRegistry registry); /** 这里配置视图解析器 **/ void configureViewResolvers(ViewResolverRegistry registry); /** 配置内容裁决的一些选项 **/ void configureContentNegotiation(ContentNegotiationConfigurer configurer); /** 视图跳转控制器 **/ void addViewControllers(ViewControllerRegistry registry); /** 静态资源处理 **/ void addResourceHandlers(ResourceHandlerRegistry registry); /** 默认静态资源处理器 **/ void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer);
这里是配置一个和参数有关的方法addArgumentResolvers,这是一个参数解析器,将页面传过来的参数自己去解析成想要的类型,其实在spring中很多参数是自动解析的,不需要我们配置,但有时候需要我们自己去解析这些参数。
这里的业务场景是,我们如果不传reques,就无法取到user对象,而且,如果我们的服务器时分布式的,就无法传递session值,因此我们在登录的时候,就在客户端添加了一个参数随机的cookie,并将该cookie值作为可以,进行redis缓存。
因此,我们可以利用参数解析器,将user值通过cookie来取到。
首先创建一个WebConfig类,然后继承WebMvcConfigurerAdapter前置配置类,然后实现其参数解析器的方法,并将自己定义的解析逻辑添加进去
package com.miaosha.util.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import java.util.List; @Configuration public class WebConfig extends WebMvcConfigurerAdapter{ @Autowired UserArgumentResolver userArgumentResolver; @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(userArgumentResolver); } }WebConfig
实现解析逻辑的类先继承参数解析的接口,然后添加支持MiaoshaUser类作为解析参数的方法,然后添加具体的解析逻辑,其实逻辑很简单,就是通过request取到cookie值,然后通过MiaoshaUserService的方法来从缓存中得到user值。
package com.miaosha.util.config; import com.miaosha.entity.domain.MiaoshaUser; import com.miaosha.service.MiaoshaUserService; import org.apache.commons.lang3.StringUtils; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Service; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Service public class UserArgumentResolver implements HandlerMethodArgumentResolver{ private MiaoshaUserService userService; //添加支持的参数类型 @Override public boolean supportsParameter(MethodParameter methodParameter) { Class<?> clazz=methodParameter.getParameterType(); return clazz== MiaoshaUser.class; } @Override public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class); HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class); String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN); String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN); if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) { return null; } String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken; return userService.getByToken(response, token); } private String getCookieValue(HttpServletRequest request, String cookiName) { Cookie[] cookies = request.getCookies(); if(cookies == null || cookies.length <= 0){ return null; } for(Cookie cookie : cookies) { if(cookie.getName().equals(cookiName)) { return cookie.getValue(); } } return null; } }UserArgumentResolver
这样,就自定义了一个参数解析器,每次请求都会得到user参数
接下来需要从缓存中去html,如果有html则直接返回,加上responseBody标签,可以直接在页面显示商品列表页面。如果没有,则需要将商品的列表从数据库中取到,然后利用spring的页面解析器将页面解析好。使用的是thymeleafViewResolver.getTemplateEngine().process()方法。
package com.miaosha.controller; import com.miaosha.entity.domain.MiaoshaUser; import com.miaosha.entity.vo.GoodsDetailVo; import com.miaosha.entity.vo.GoodsVo; import com.miaosha.entity.vo.result.Result; import com.miaosha.service.GoodsService; import com.miaosha.service.MiaoshaUserService; import com.miaosha.util.redisconfig.MyRedisTemplate; import com.miaosha.util.redisconfig.key.GoodsKey; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.thymeleaf.spring4.context.SpringWebContext; import org.thymeleaf.spring4.view.ThymeleafViewResolver; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.List; @Controller @RequestMapping("/goods") public class GoodsController { @Autowired MiaoshaUserService userService; @Autowired MyRedisTemplate redisService; @Autowired GoodsService goodsService; @Autowired ThymeleafViewResolver thymeleafViewResolver; @Autowired ApplicationContext applicationContext; @RequestMapping(value="/to_list", produces="text/html") @ResponseBody public String list(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user) { model.addAttribute("user", user); //取缓存 String html = redisService.get(GoodsKey.getGoodsList, "", String.class); if(!StringUtils.isEmpty(html)) { return html; } List<GoodsVo> goodsList = goodsService.listGoodsVo(); model.addAttribute("goodsList", goodsList); // return "goods_list"; SpringWebContext ctx = new SpringWebContext(request,response, request.getServletContext(),request.getLocale(), model.asMap(), applicationContext ); //手动渲染 html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx); if(!StringUtils.isEmpty(html)) { redisService.set(GoodsKey.getGoodsList, "", html); } System.out.println("GoodsController:"+html); return html; } @RequestMapping(value="/detail/{goodsId}") @ResponseBody public Result<GoodsDetailVo> detail(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user, @PathVariable("goodsId")long goodsId) { GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId); long startAt = goods.getStartDate().getTime(); long endAt = goods.getEndDate().getTime(); long now = System.currentTimeMillis(); int miaoshaStatus = 0; int remainSeconds = 0; if(now < startAt ) {//秒杀还没开始,倒计时 miaoshaStatus = 0; remainSeconds = (int)((startAt - now )/1000); }else if(now > endAt){//秒杀已经结束 miaoshaStatus = 2; remainSeconds = -1; }else {//秒杀进行中 miaoshaStatus = 1; remainSeconds = 0; } GoodsDetailVo vo = new GoodsDetailVo(); vo.setGoods(goods); vo.setUser(user); vo.setRemainSeconds(remainSeconds); vo.setMiaoshaStatus(miaoshaStatus); return Result.success(vo); } }GoodsController
商品详情页面
这个页面因为有秒杀的实际操作,因此是最复杂的。将商品列表展示出来之后,每个商品后面有一个详情的链接,如果点击的话,就直接跳转到商品详情页。
直接跳转的页面需要是静态页面,但我们的页面时动态的,因此需要利用页面静态化的技术,当客户端将页面加载解析完之后,通过一个入口函数,自动ajax请求,将需要的详情数据返回,然后在页面上展示出来。
$(function(){ //countDown(); getDetail(); }); function getDetail(){ var goodsId = g_getQueryString("goodsId"); $.ajax({ url:"/goods/detail/"+goodsId, type:"GET", success:function(data){ if(data.code == 0){ render(data.data); }else{ layer.msg(data.msg); } }, error:function(){ layer.msg("客户端请求有误"); } }); } function render(detail){ var miaoshaStatus = detail.miaoshaStatus; var remainSeconds = detail.remainSeconds; var goods = detail.goods; var user = detail.user; if(user){ $("#userTip").hide(); } $("#goodsName").text(goods.goodsName); $("#goodsImg").attr("src", goods.goodsImg); $("#startTime").text(new Date(goods.startDate).format("yyyy-MM-dd hh:mm:ss")); $("#remainSeconds").val(remainSeconds); $("#goodsId").val(goods.id); $("#goodsPrice").text(goods.goodsPrice); $("#miaoshaPrice").text(goods.miaoshaPrice); $("#stockCount").text(goods.stockCount); countDown(); } function countDown(){ var remainSeconds = $("#remainSeconds").val(); var timeout; if(remainSeconds > 0){//秒杀还没开始,倒计时 $("#buyButton").attr("disabled", true); $("#miaoshaTip").html("秒杀倒计时:"+remainSeconds+"秒"); timeout = setTimeout(function(){ $("#countDown").text(remainSeconds - 1); $("#remainSeconds").val(remainSeconds - 1); countDown(); },1000); }else if(remainSeconds == 0){//秒杀进行中 $("#buyButton").attr("disabled", false); if(timeout){ clearTimeout(timeout); } $("#miaoshaTip").html("秒杀进行中"); $("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val()); $("#verifyCodeImg").show(); $("#verifyCode").show(); }else{//秒杀已经结束 $("#buyButton").attr("disabled", true); $("#miaoshaTip").html("秒杀已经结束"); $("#verifyCodeImg").hide(); $("#verifyCode").hide(); } }入口函数加载数据
ajax请求的路径是获取详情,这里需要获取的数据都已经封装到GoodsDetailVo类中了,只需要将其全部得到,然后返回即可(这里注意的是,在获取这些数据的过程中如果使用缓存技术能很大程度的提高性能。降低并发)
@RequestMapping(value="/detail/{goodsId}") @ResponseBody public Result<GoodsDetailVo> detail(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user, @PathVariable("goodsId")long goodsId) { GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId); long startAt = goods.getStartDate().getTime(); long endAt = goods.getEndDate().getTime(); long now = System.currentTimeMillis(); int miaoshaStatus = 0; int remainSeconds = 0; if(now < startAt ) {//秒杀还没开始,倒计时 miaoshaStatus = 0; remainSeconds = (int)((startAt - now )/1000); }else if(now > endAt){//秒杀已经结束 miaoshaStatus = 2; remainSeconds = -1; }else {//秒杀进行中 miaoshaStatus = 1; remainSeconds = 0; } GoodsDetailVo vo = new GoodsDetailVo(); vo.setGoods(goods); vo.setUser(user); vo.setRemainSeconds(remainSeconds); vo.setMiaoshaStatus(miaoshaStatus); return Result.success(vo); }detail
秒杀逻辑
点击秒杀按钮,然后不是直接执行秒杀逻辑,而是需要去服务器请求一个路径,(为了在秒杀前隐藏接口,防止盗刷),然后将返回的接口进行拼装,然后请求这个随机的路径,执行真正的秒杀逻辑。
首先判断库存是否大于0,然后判断是否已经下单(不能重复秒杀),当这些都达到条件的时候,可以将user和商品id封装到一个类中,传到RabbitMQ队列中。然后从接收的队列中取。这样就会极大的缓减了网站的并发量,从出队中取到值后,再进行判断库存和订单,然后再执行减库存和下订单的操作。
当完成下订单的操作之后,秒杀逻辑其实已经做完了,但需要将数据返回到页面去跳转到订单的详情页面,这就需要在返回成功的逻辑上设置一个轮询函数,不停的请求服务器,看是否已经下了订单,当下了订单之后,就可以直接跳转到订单详情页面。
秒杀验证码,该验证码是直接向内存中拿到的图片,但需要我们服务器传回到内存中
$("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val()); $("#verifyCodeImg").show();
验证码生成逻辑:
页面发出对服务器的请求,请求一张图片,而服务器将图片生成后,写入到内存中,被浏览器取到。
package com.miaosha.controller; import com.miaosha.entity.domain.MiaoshaUser; import com.miaosha.entity.vo.result.CodeMsg; import com.miaosha.entity.vo.result.Result; import com.miaosha.service.MiaoshaService; import com.miaosha.service.MiaoshaUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import javax.imageio.ImageIO; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; import java.io.OutputStream; @Controller @RequestMapping("/miaosha") public class MiaoshaController { @Autowired private MiaoshaService miaoshaService; @RequestMapping(value="/verifyCode", method=RequestMethod.GET) @ResponseBody public Result<String> getMiaoshaVerifyCod(HttpServletResponse response, MiaoshaUser user, @RequestParam("goodsId")long goodsId) { if(user == null) { return Result.error(CodeMsg.SESSION_ERROR); } System.out.println("请求进来了"); try { BufferedImage image = miaoshaService.createVerifyCode(user, goodsId); OutputStream out = response.getOutputStream(); ImageIO.write(image, "JPEG", out); out.flush(); out.close(); return null; }catch(Exception e) { e.printStackTrace(); return Result.error(CodeMsg.MIAOSHA_FAIL); } } // @RequestMapping(value="/path", method= RequestMethod.GET) // @ResponseBody // public Result<String> getMiaoshaPath(HttpServletRequest request, MiaoshaUser user, // @RequestParam("goodsId")long goodsId, // @RequestParam(value="verifyCode", defaultValue="0")int verifyCode // ) { // if(user == null) { // return Result.error(CodeMsg.SESSION_ERROR); // } // boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode); // if(!check) { // return Result.error(CodeMsg.REQUEST_ILLEGAL); // } // String path =miaoshaService.createMiaoshaPath(user, goodsId); // return Result.success(path); // } }MiaoshaController
package com.miaosha.service; import com.miaosha.entity.domain.MiaoshaUser; import com.miaosha.util.redisconfig.MyRedisTemplate; import com.miaosha.util.redisconfig.key.MiaoshaKey; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import java.awt.*; import java.awt.image.BufferedImage; import java.util.Random; @Service public class MiaoshaService { @Autowired private MyRedisTemplate redisService; public BufferedImage createVerifyCode(MiaoshaUser user, long goodsId) { if(user == null || goodsId <=0) { return null; } int width = 80; int height = 32; //create the image BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics g = image.getGraphics(); // set the background color g.setColor(new Color(0xDCDCDC)); g.fillRect(0, 0, width, height); // draw the border g.setColor(Color.black); g.drawRect(0, 0, width - 1, height - 1); // create a random instance to generate the codes Random rdm = new Random(); // make some confusion for (int i = 0; i < 50; i++) { int x = rdm.nextInt(width); int y = rdm.nextInt(height); g.drawOval(x, y, 0, 0); } // generate a random code String verifyCode = generateVerifyCode(rdm); g.setColor(new Color(0, 100, 0)); g.setFont(new Font("Candara", Font.BOLD, 24)); g.drawString(verifyCode, 8, 24); g.dispose(); //把验证码存到redis中 int rnd = calc(verifyCode); redisService.set(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId, rnd); //输出图片 return image; } private static char[] ops = new char[] {'+', '-', '*'}; /** * + - * * */ private String generateVerifyCode(Random rdm) { int num1 = rdm.nextInt(10); int num2 = rdm.nextInt(10); int num3 = rdm.nextInt(10); char op1 = ops[rdm.nextInt(3)]; char op2 = ops[rdm.nextInt(3)]; String exp = ""+ num1 + op1 + num2 + op2 + num3; return exp; } private static int calc(String exp) { try { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); return (Integer)engine.eval(exp); }catch(Exception e) { e.printStackTrace(); return 0; } } }MiaoshaService
当页面根据图片输入验证码之后,点击立刻秒杀按钮,不是立刻执行秒杀逻辑,而是去请求一个随机的秒杀路径。
MiaoshaController类中的方法,得到随机的秒杀路径
@RequestMapping(value="/path", method= RequestMethod.GET) @ResponseBody public Result<String> getMiaoshaPath(HttpServletRequest request, MiaoshaUser user, @RequestParam("goodsId")long goodsId, @RequestParam(value="verifyCode", defaultValue="0")int verifyCode ) { if(user == null) { return Result.error(CodeMsg.SESSION_ERROR); } boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode); if(!check) { return Result.error(CodeMsg.REQUEST_ILLEGAL); } String path =miaoshaService.createMiaoshaPath(user, goodsId); return Result.success(path); }getMiaoshaPath
其中调用两个方法,MiaoshaService中
public boolean checkVerifyCode(MiaoshaUser user, long goodsId, int verifyCode) { if(user == null || goodsId <=0) { return false; } Integer codeOld = redisService.get(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId, Integer.class); if(codeOld == null || codeOld - verifyCode != 0 ) { return false; } redisService.delete(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId); return true; } public String createMiaoshaPath(MiaoshaUser user, long goodsId) { if(user == null || goodsId <=0) { return null; } String str = MD5Util.md5(UUIDUtil.uuid()+"123456"); redisService.set(MiaoshaKey.getMiaoshaPath, ""+user.getId() + "_"+ goodsId, str); return str; }checkVerifyCode and createMiaoshaPath
这样就请求到了随机的路径,并检查了验证码是否正确。
然后返回一个随机路径,页面收到之后,立刻又发起了ajax请求,请求执行秒杀逻辑,判断库存、订单,之后,将用户和商品id封装到一个实体类中,抛入RabbitMQ的入队中。
定义需要封装的BO类,MiaoshaMessage
package com.miaosha.entity.bo; import com.miaosha.entity.domain.MiaoshaUser; public class MiaoshaMessage { private MiaoshaUser user; private long goodsId; public MiaoshaUser getUser() { return user; } public void setUser(MiaoshaUser user) { this.user = user; } public long getGoodsId() { return goodsId; } public void setGoodsId(long goodsId) { this.goodsId = goodsId; } }MiaoshaMessage
然后将此类抛到消息队列中去
在此之前,需要做一些准备工作
步骤1:检查路径是否正确
public boolean checkPath(MiaoshaUser user, long goodsId, String path) { if(user == null || path == null) { return false; } String pathOld = redisService.get(MiaoshaKey.getMiaoshaPath, ""+user.getId() + "_"+ goodsId, String.class); return path.equals(pathOld); }checkPath
步骤2:对内存进行标记,减少对redis的访问,将库存是否已经为0的消息存放在一个map中,在最开始去判断它,如果没有库存直接返回,有继续。这里需要MiaoshaController类中继承一个初始化接口,实现一个方法,该方法会在类加载的时候就开始执行,将库存先标记为有。
private HashMap<Long, Boolean> localOverMap = new HashMap<Long, Boolean>(); @Override public void afterPropertiesSet() throws Exception { List<GoodsVo> goodsList = goodsService.listGoodsVo(); if(goodsList == null) { return; } for(GoodsVo goods : goodsList) { redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount()); localOverMap.put(goods.getId(), false); } }afterPropertiesSet
//内存标记,减少redis访问 boolean over = localOverMap.get(goodsId); if(over) { return Result.error(CodeMsg.MIAO_SHA_OVER); }内存标记
步骤3:预减库存,在redis中存储库存值,先减掉一个判断是否小于0,小于0,则标记内存,然后直接返回错误。
//预减库存 long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10 if(stock < 0) { localOverMap.put(goodsId, true); return Result.error(CodeMsg.MIAO_SHA_OVER); }预减库存
步骤4:判断是否已经下单,若下单,则返回错误,不能重复下单,这里需要一个Order的service类,来返回从数据库中查到的结果。
package com.miaosha.service; import com.miaosha.dao.OrderDao; import com.miaosha.entity.domain.MiaoshaOrder; import com.miaosha.entity.domain.OrderInfo; import com.miaosha.util.redisconfig.MyRedisTemplate; import com.miaosha.util.redisconfig.key.OrderKey; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class OrderService { @Autowired private OrderDao orderDao; @Autowired private MyRedisTemplate redisService; public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) { //return orderDao.getMiaoshaOrderByUserIdGoodsId(userId, goodsId); return redisService.get(OrderKey.getMiaoshaOrderByUidGid, ""+userId+"_"+goodsId, MiaoshaOrder.class); } public OrderInfo getOrderById(long orderId) { return orderDao.getOrderById(orderId); } public void deleteOrders() { orderDao.deleteOrders(); orderDao.deleteMiaoshaOrders(); } }OrderService
步骤5:实现入队操作,先配置RabbitMQ,配置一个队列。
package com.miaosha.util.rabbitmq; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import static org.springframework.amqp.core.Binding.DestinationType.QUEUE; @Configuration public class MQConfig { public static final String MIAOSHA_QUEUE = "miaosha.queue"; @Bean public Queue queue() { return new Queue(MIAOSHA_QUEUE, true); } }MQConfig
入队函数
package com.miaosha.util.rabbitmq; import com.miaosha.entity.bo.MiaoshaMessage; import com.miaosha.util.redisconfig.MyRedisTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MQSender { private static Logger log = LoggerFactory.getLogger(MQSender.class); @Autowired AmqpTemplate amqpTemplate ; public void sendMiaoshaMessage(MiaoshaMessage mm) { String msg = MyRedisTemplate.beanToString(mm); log.info("send message:"+msg); amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg); } }MQSender
将封装好的消息转换为字符串然后发出去,发到队列中
秒杀逻辑没有结束,但doMiaosha的请求已经处理完了。
@RequestMapping(value="/{path}/do_miaosha", method=RequestMethod.POST) @ResponseBody public Result<Integer> miaosha(Model model, MiaoshaUser user, @RequestParam("goodsId")long goodsId, @PathVariable("path") String path) { model.addAttribute("user", user); if(user == null) { return Result.error(CodeMsg.SESSION_ERROR); } //验证path boolean check = miaoshaService.checkPath(user, goodsId, path); if(!check){ return Result.error(CodeMsg.REQUEST_ILLEGAL); } //内存标记,减少redis访问 boolean over = localOverMap.get(goodsId); if(over) { return Result.error(CodeMsg.MIAO_SHA_OVER); } //预减库存 long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10 if(stock < 0) { localOverMap.put(goodsId, true); return Result.error(CodeMsg.MIAO_SHA_OVER); } //判断是否已经秒杀到了 MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId); if(order != null) { return Result.error(CodeMsg.REPEATE_MIAOSHA); } //入队 MiaoshaMessage mm = new MiaoshaMessage(); mm.setUser(user); mm.setGoodsId(goodsId); sender.sendMiaoshaMessage(mm); return Result.success(0);//排队中 }miaosha
入队之后,需要出队,因此,需要编写出队的类方法来执行真正的秒杀逻辑,减库存,下单
先将入队时候转换的字符串,转成封装的消息类,然后将继续判断库存,是否下单,最后执行秒杀的真正逻辑
package com.miaosha.util.rabbitmq; import com.miaosha.entity.bo.MiaoshaMessage; import com.miaosha.entity.domain.MiaoshaOrder; import com.miaosha.entity.domain.MiaoshaUser; import com.miaosha.entity.vo.GoodsVo; import com.miaosha.service.GoodsService; import com.miaosha.service.MiaoshaService; import com.miaosha.service.OrderService; import com.miaosha.util.redisconfig.MyRedisTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MQReceiver { private static Logger log = LoggerFactory.getLogger(MQReceiver.class); @Autowired private GoodsService goodsService; @Autowired private OrderService orderService; @Autowired private MiaoshaService miaoshaService; @RabbitListener(queues=MQConfig.MIAOSHA_QUEUE) public void receive(String message) { log.info("receive message:"+message); MiaoshaMessage mm = MyRedisTemplate.stringToBean(message, MiaoshaMessage.class); MiaoshaUser user = mm.getUser(); long goodsId = mm.getGoodsId(); GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId); int stock = goods.getStockCount(); if(stock <= 0) { return; } //判断是否已经秒杀到了 MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId); if(order != null) { return; } //减库存 下订单 写入秒杀订单 miaoshaService.miaosha(user, goods); } }MQReceiver
真正的秒杀逻辑
package com.miaosha.service; import com.miaosha.entity.domain.MiaoshaUser; import com.miaosha.entity.domain.OrderInfo; import com.miaosha.entity.vo.GoodsVo; import com.miaosha.util.MD5Util; import com.miaosha.util.UUIDUtil; import com.miaosha.util.redisconfig.MyRedisTemplate; import com.miaosha.util.redisconfig.key.MiaoshaKey; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import java.awt.*; import java.awt.image.BufferedImage; import java.util.Random; @Service public class MiaoshaService { @Autowired private MyRedisTemplate redisService; @Autowired private GoodsService goodsService; @Autowired private OrderService orderService; public BufferedImage createVerifyCode(MiaoshaUser user, long goodsId) { if(user == null || goodsId <=0) { return null; } int width = 80; int height = 32; //create the image BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics g = image.getGraphics(); // set the background color g.setColor(new Color(0xDCDCDC)); g.fillRect(0, 0, width, height); // draw the border g.setColor(Color.black); g.drawRect(0, 0, width - 1, height - 1); // create a random instance to generate the codes Random rdm = new Random(); // make some confusion for (int i = 0; i < 50; i++) { int x = rdm.nextInt(width); int y = rdm.nextInt(height); g.drawOval(x, y, 0, 0); } // generate a random code String verifyCode = generateVerifyCode(rdm); g.setColor(new Color(0, 100, 0)); g.setFont(new Font("Candara", Font.BOLD, 24)); g.drawString(verifyCode, 8, 24); g.dispose(); //把验证码存到redis中 int rnd = calc(verifyCode); redisService.set(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId, rnd); //输出图片 return image; } private static char[] ops = new char[] {'+', '-', '*'}; /** * + - * * */ private String generateVerifyCode(Random rdm) { int num1 = rdm.nextInt(10); int num2 = rdm.nextInt(10); int num3 = rdm.nextInt(10); char op1 = ops[rdm.nextInt(3)]; char op2 = ops[rdm.nextInt(3)]; String exp = ""+ num1 + op1 + num2 + op2 + num3; return exp; } private static int calc(String exp) { try { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); return (Integer)engine.eval(exp); }catch(Exception e) { e.printStackTrace(); return 0; } } public boolean checkVerifyCode(MiaoshaUser user, long goodsId, int verifyCode) { if(user == null || goodsId <=0) { return false; } Integer codeOld = redisService.get(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId, Integer.class); if(codeOld == null || codeOld - verifyCode != 0 ) { return false; } redisService.delete(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId); return true; } public String createMiaoshaPath(MiaoshaUser user, long goodsId) { if(user == null || goodsId <=0) { return null; } String str = MD5Util.md5(UUIDUtil.uuid()+"123456"); redisService.set(MiaoshaKey.getMiaoshaPath, ""+user.getId() + "_"+ goodsId, str); return str; } public boolean checkPath(MiaoshaUser user, long goodsId, String path) { if(user == null || path == null) { return false; } String pathOld = redisService.get(MiaoshaKey.getMiaoshaPath, ""+user.getId() + "_"+ goodsId, String.class); return path.equals(pathOld); } @Transactional public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) { //减库存 下订单 写入秒杀订单 boolean success = goodsService.reduceStock(goods); if(success) { //order_info maiosha_order return orderService.createOrder(user, goods); }else { setGoodsOver(goods.getId()); return null; } } private void setGoodsOver(Long goodsId) { redisService.set(MiaoshaKey.isGoodsOver, ""+goodsId, true); } private boolean getGoodsOver(long goodsId) { return redisService.exists(MiaoshaKey.isGoodsOver, ""+goodsId); } }MiaoshaService
然后去处理页面发出的轮询请求
然后向页面返回了一个0,表示正在排队中,需要等待,等待的逻辑是不停的调用一个请求结果的ajax。
getMiaoshaResult@RequestMapping(value="/result", method=RequestMethod.GET) @ResponseBody public Result<Long> miaoshaResult(Model model,MiaoshaUser user, @RequestParam("goodsId")long goodsId) { model.addAttribute("user", user); if(user == null) { return Result.error(CodeMsg.SESSION_ERROR); } long result =miaoshaService.getMiaoshaResult(user.getId(), goodsId); return Result.success(result); }miaoshaResult
public long getMiaoshaResult(Long userId, long goodsId) { MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(userId, goodsId); if(order != null) {//秒杀成功 return order.getOrderId(); }else { boolean isOver = getGoodsOver(goodsId); if(isOver) { return -1; }else { return 0; } } }getMiaoshaResult
这三个函数就是查询订单数据库是否已经下单,若是下单就是秒杀成功了,跳转到订单详情页面,若没有则继续轮询。同时,从缓存中查询秒杀是否已经结束,若结束则返回已结束。
但没有添加防盗刷的功能,需要判断一个用户发起了几次请求,比如一个用户在五秒内只能发起5次请求,防止一些人使用机器去刷,因此,需要判断请求次数,如果请求超过了五次,就返回错误消息。
使用拦截器,自定义一个注解来方便的配置这些信息。
步骤1:定义一个注解,可以配置时间,次数,和是否必须登录
package com.miaosha.util.access; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) @Target(METHOD) public @interface AccessLimit { int seconds(); int maxCount(); boolean needLogin() default true; }AccessLimit
步骤2:实现注解的逻辑
该逻辑类继承一个拦截器的抽象类,然后得到上述的注解,并得到用户,这里的用户使用cookie来得到,然后用本地线程将用户类绑定,定义得到用户和保存用户的方法,这样是线程安全的,并将从cookie中得到的用户,和当地线程绑定,这样就可以直接获取了。(有了当地线程,可以不用之前写的参数解析器得到的用户了。)然后在redis中使用一个专属的key来存储时间限制,并当每次请求都自增,这样就可以实现注解的功能了。
本地线程绑定用户类
package com.miaosha.util.access; import com.miaosha.entity.domain.MiaoshaUser; public class UserContext { private static ThreadLocal<MiaoshaUser> userHolder = new ThreadLocal<MiaoshaUser>(); public static void setUser(MiaoshaUser user) { userHolder.set(user); } public static MiaoshaUser getUser() { return userHolder.get(); } }UserContext
package com.miaosha.util.access; import com.alibaba.fastjson.JSON; import com.miaosha.entity.domain.MiaoshaUser; import com.miaosha.entity.vo.result.CodeMsg; import com.miaosha.entity.vo.result.Result; import com.miaosha.service.MiaoshaUserService; import com.miaosha.util.redisconfig.MyRedisTemplate; import com.miaosha.util.redisconfig.key.AccessKey; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.OutputStream; public class AccessInterceptor extends HandlerInterceptorAdapter { @Autowired MiaoshaUserService userService; @Autowired MyRedisTemplate redisService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if(handler instanceof HandlerMethod) { MiaoshaUser user = getUser(request, response); UserContext.setUser(user); HandlerMethod hm = (HandlerMethod)handler; AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class); if(accessLimit == null) { return true; } int seconds = accessLimit.seconds(); int maxCount = accessLimit.maxCount(); boolean needLogin = accessLimit.needLogin(); String key = request.getRequestURI(); if(needLogin) { if(user == null) { render(response, CodeMsg.SESSION_ERROR); return false; } key += "_" + user.getId(); }else { //do nothing } AccessKey ak = AccessKey.withExpire(seconds); Integer count = redisService.get(ak, key, Integer.class); if(count == null) { redisService.set(ak, key, 1); }else if(count < maxCount) { redisService.incr(ak, key); }else { render(response, CodeMsg.ACCESS_LIMIT_REACHED); return false; } } return true; } private void render(HttpServletResponse response, CodeMsg cm)throws Exception { response.setContentType("application/json;charset=UTF-8"); OutputStream out = response.getOutputStream(); String str = JSON.toJSONString(Result.error(cm)); out.write(str.getBytes("UTF-8")); out.flush(); out.close(); } private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) { String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN); String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN); if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) { return null; } String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken; return userService.getByToken(response, token); } private String getCookieValue(HttpServletRequest request, String cookiName) { Cookie[] cookies = request.getCookies(); if(cookies == null || cookies.length <= 0){ return null; } for(Cookie cookie : cookies) { if(cookie.getName().equals(cookiName)) { return cookie.getValue(); } } return null; } }AccessInterceptor
然后配置拦截器
package com.miaosha.util.config; import com.miaosha.util.access.AccessInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import java.util.List; @Configuration public class WebConfig extends WebMvcConfigurerAdapter{ @Autowired UserArgumentResolver userArgumentResolver; @Autowired AccessInterceptor accessInterceptor; @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(userArgumentResolver); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(accessInterceptor); } }WebConfig
到此,所有的秒杀功能都已经结束。
订单详情页面的展示
当轮询函数请求到了,返回参数为1,说明已经秒杀成功,则直接跳转到订单的详情页面,然后通过入口函数,将需要展示的数据请求过来。
需要订单的业务类
package com.miaosha.service; import com.miaosha.dao.OrderDao; import com.miaosha.entity.domain.MiaoshaOrder; import com.miaosha.entity.domain.MiaoshaUser; import com.miaosha.entity.domain.OrderInfo; import com.miaosha.entity.vo.GoodsVo; import com.miaosha.util.redisconfig.MyRedisTemplate; import com.miaosha.util.redisconfig.key.OrderKey; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Date; @Service public class OrderService { @Autowired private OrderDao orderDao; @Autowired private MyRedisTemplate redisService; public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) { return orderDao.getMiaoshaOrderByUserIdGoodsId(userId, goodsId); // return redisService.get(OrderKey.getMiaoshaOrderByUidGid, ""+userId+"_"+goodsId, MiaoshaOrder.class); } public OrderInfo getOrderById(long orderId) { return orderDao.getOrderById(orderId); } public void deleteOrders() { orderDao.deleteOrders(); orderDao.deleteMiaoshaOrders(); } @Transactional public OrderInfo createOrder(MiaoshaUser user, GoodsVo goods) { OrderInfo orderInfo = new OrderInfo(); orderInfo.setCreateDate(new Date()); orderInfo.setDeliveryAddrId(0L); orderInfo.setGoodsCount(1); orderInfo.setGoodsId(goods.getId()); orderInfo.setGoodsName(goods.getGoodsName()); orderInfo.setGoodsPrice(goods.getMiaoshaPrice()); orderInfo.setOrderChannel(1); orderInfo.setStatus(0); orderInfo.setUserId(user.getId()); orderDao.insert(orderInfo); MiaoshaOrder miaoshaOrder = new MiaoshaOrder(); miaoshaOrder.setGoodsId(goods.getId()); miaoshaOrder.setOrderId(orderInfo.getId()); miaoshaOrder.setUserId(user.getId()); orderDao.insertMiaoshaOrder(miaoshaOrder); redisService.set(OrderKey.getMiaoshaOrderByUidGid, ""+user.getId()+"_"+goods.getId(), miaoshaOrder); return orderInfo; } }OrderService
然后有一个订单的控制器,设置页面的请求路径
package com.miaosha.controller; import com.miaosha.entity.domain.MiaoshaUser; import com.miaosha.entity.domain.OrderInfo; import com.miaosha.entity.vo.GoodsVo; import com.miaosha.entity.vo.OrderDetailVo; import com.miaosha.entity.vo.result.CodeMsg; import com.miaosha.entity.vo.result.Result; import com.miaosha.service.GoodsService; import com.miaosha.service.MiaoshaUserService; import com.miaosha.service.OrderService; import com.miaosha.util.redisconfig.MyRedisTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @Controller @RequestMapping("/order") public class OrderController { @Autowired OrderService orderService; @Autowired GoodsService goodsService; @RequestMapping("/detail") @ResponseBody public Result<OrderDetailVo> info(Model model, MiaoshaUser user, @RequestParam("orderId") long orderId) { if(user == null) { return Result.error(CodeMsg.SESSION_ERROR); } OrderInfo order = orderService.getOrderById(orderId); if(order == null) { return Result.error(CodeMsg.ORDER_NOT_EXIST); } long goodsId = order.getGoodsId(); GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId); OrderDetailVo vo = new OrderDetailVo(); vo.setOrder(order); vo.setGoods(goods); return Result.success(vo); } }OrderController
到此,整个秒杀项目完成。
#