java初探(1)之秒殺項目總結


在開始總結之前,先記錄一個剛看到的博客,編程規約。該博客記錄了一些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()+"&timestamp="+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

 

 

 到此,整個秒殺項目完成。

 

 

 

 

 

 

 

 

 

 

 

 

 

 
 
#


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM