基於socket.io的實時在線選座系統


基於socket.io的實時在線選座系統(demo)


前言

前段時間公司做一個關於劇院的項目,遇到了這樣一種情況。
在高並發多用戶同時選座的情況下,假設A用戶進入選座頁面,正在選擇座位,此時還沒有提交所選擇的座位。
這時B用戶進入選座頁面,迅速的選擇了座位,提交。
而這個時候,A終於選擇完畢,提交。 發現座位已經被買了。
當用戶越多這樣的情況越嚴重。
具體場景就是如此。

1、簡介

本項目是基於jquery.seat-charts在線選座插件。集合socket.io,實現的實時選座系統,可應用於劇院,影院,車票等!

Socket.IO是一個WebSocket庫,包括了客戶端的js和服務器端的nodejs,它主要是為了實現客戶端和服務端的全雙工通信。我們傳統的http請求(拋開長鏈接不談),只實現了一請求一回復的,沒有辦法做到服務器端向客戶端推送數據的情況。而Socket.IO則實現了這一點。

依賴的模塊

  • node.js
  • express
  • socket.io
  • jquery.seat-charts

2、安裝部署

2.1 部署 express服務器

express是一個小巧的Node.js的Web應用框架,在構建HTTP服務器時經常使用到,所以直接以Socket.IO和express為例子來講解。

在node.js環境下

npm install express 

express XXX                 *(XXX)是你的項目名字*

cd XXX                      *進入你的項目*

npm install                 *下載依賴*     
2.2 添加依賴模塊、修改默認的express框架

本項目沒有使用express默認的模板引擎jade,采用了ejs模板,對新手來說更友好,學習成本更低。

npm install -D socket.io ejs 

雖然簡單,但是如果使用在express框架上則需要修改以下幾個位置。

> views目錄下所有文件

改為ejs后綴。 並把內容改為標准html 5 模板。

app.js 文件

app.set('view engine', 'jade');  
修改為 ==> app.set('view engine', 'ejs');

bin > www 文件

因為socket.io需要監聽服務,所以我們需要把www文件中的server 拋出 
module.exports = server;  添加在www文件最后一行即可

新建 bin > socket.js 文件 (后續添加該處代碼)

放置socket.io核心代碼。實現模塊分離。
bin > www         放置服務配置
bin > socket.js   放置socket.io配置信息

package.json 修改入口配置

"scripts": {
    "start": "node ./bin/www"
}
修改為
"scripts": {
    "start": "node ./bin/socket.js"
}

3、服務端代碼

bin > socket.js

var server = require("./www");
var io = require("socket.io")(server);

io.chooseSeat = {};

io.on('connection', function(socket) {
	//用戶選擇的座位
	socket.chooseSeat = {};
	socket.isSold = false;

    socket.on('login', function(data) {
        io.emit("loginlock",io.chooseSeat);
    });

    //監聽用戶選擇座位
    socket.on('selected', function(data) {
        socket.chooseSeat[data.id] = data;
    	io.chooseSeat[data.id] = data;
        io.emit("locking",data);
    });
    socket.on('cancleselected', function(data) {
        delete socket.chooseSeat[data.id];
    	delete io.chooseSeat[data.id];
        io.emit("canclelocking",data)
    });

    socket.on('sold', function(data) {
        // 把售賣的座位信息返給其他用戶
        socket.isSold = true
        io.emit('seatsold', Object.keys(data));
    });

    //監聽用戶退出  釋放用戶選擇的未提交座位
    socket.on('disconnect', function() {
    	// 如果沒有購買,直接就退出了,才去釋放座位

        for(var t in socket.chooseSeat){
            delete io.chooseSeat[t];
        }

    	if (!socket.isSold) {
    		io.emit('userout', socket.chooseSeat);
    	}
    })
});

4、客戶端代碼

bin > socket.js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>基於socket.io的實時在線選座系統(影院版)</title>
    <meta name="keywords" content="jQuery在線選座,jQuery選座系統,WebSocket,socket.io,實時選座系統" />
    <meta name="description" content="本項目是基於jquery.seat-charts在線選座插件。集合socket.io,實現的實時選座系統,可應用於劇院,影院,車票等" />
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <link rel="stylesheet" type="text/css" href="css/reset.css" />
    <link rel="stylesheet" type="text/css" href="css/index.css" />
</head>
<body>
    <div class="container">
        <h2 class="title"><a href="#">jQuery在線選座(影院版)</a></h2>
        <div class="demo clearfix">
            <!---左邊座位列表-->
            <div id="seat_area">
                <div class="front">屏幕</div>
            </div>
            <!---右邊選座信息-->
            <div class="booking_area">
                <p>電影:<span>天將雄師</span></p>
                <p>時間:<span>03月20日 22:15</span></p>
                <p>座位:</p>
                <ul id="seats_chose"></ul>
                <p>票數:<span id="tickects_num">0</span></p>
                <p>總價:<b>¥<span id="total_price">0</span></b></p>
                <input type="button" class="btn" id="commitSeat" value="確定購買" />
                <div id="legend"></div>
            </div>
        </div>
    </div> 
    <script src="js/jquery-3.2.1.js"></script>
    <script src="/socket.io/socket.io.js"></script>
    <script src="js/jquery.seat-charts.js"></script>
    <script src="js/index.js"></script>
</body>
</html> 

pulic > js > index.js

$(function(){
    var price = 100; //電影票價
    var initData = {
        socket: io.connect('http://192.168.1.96:3000'),
        mapData:[ //座位結構圖 a 代表座位; 下划線 "_" 代表過道
            'cccccccccc',
            'cccccccccc',
            '__________',
            'cccccccc__',
            'cccccccccc',
            'cccccccccc',
            'cccccccccc',
            'cccccccccc',
            'cccccccccc',
            'cc__cc__cc'
        ],
        iconStatus:[    // 座位狀態
            ['c', 'available', '可選座'],
            ['c', 'selected', '已選中'],
            ['c', 'locking', '已鎖定'],
            ['c', 'unavailable', '已售出'],
        ],
        selectedSeat:{}  
    }
    var interaction = {
        initMap:function(){
            var _this = this;
            var $cart = $('#seats_chose'), //座位區
                $tickects_num = $('#tickects_num'), //票數
                $total_price = $('#total_price'); //票價總額
            var sc = $('#seat_area').seatCharts({
                map:initData.mapData,
                naming: { //設置行列等信息
                    top: false, //不顯示頂部橫坐標(行) 
                    getLabel: function(character, row, column) { //返回座位信息 
                        return column;
                    }
                },
                legend: { //定義圖例
                    node: $('#legend'),
                    items: initData.iconStatus
                },
                click: function() {
                    if (this.status() == 'available') { //若為可選座狀態,添加座位
                        $('<li>' + (this.settings.row + 1) + '排' + this.settings.label + '座</li>')
                            .attr('id', 'cart-item-' + this.settings.id)
                            .data('seatId', this.settings.id)
                            .appendTo($cart);
                        $tickects_num.text(sc.find('selected').length + 1); //統計選票數量
                        $total_price.text(_this.getTotalPrice(sc) + price); //計算票價總金額

                        // 向服務器發送消息,座位被我選中
                        _this.emit("selected",{
                            firetype:'selected',
                            firetime:new Date().toLocaleString(),
                            character:this.settings.character,
                            column:this.settings.column,
                            data:this.settings.data,
                            id:this.settings.id,
                            label:this.settings.label,
                            row:this.settings.row
                        })
                        initData.selectedSeat[this.settings.id] = this.settings;
                        return 'selected';
                    } else if (this.status() == 'selected') { //若為選中狀態
                        $tickects_num.text(sc.find('selected').length - 1); //更新票數量
                        $total_price.text(_this.getTotalPrice(sc) - price); //更新票價總金額
                        $('#cart-item-' + this.settings.id).remove(); //刪除已預訂座位

                        // 向服務器發送消息,座位被我取消
                        _this.emit("cancleselected",{
                            firetype:'cancleselected',
                            firetime:new Date().toLocaleString(),
                            character:this.settings.character,
                            column:this.settings.column,
                            data:this.settings.data,
                            id:this.settings.id,
                            label:this.settings.label,
                            row:this.settings.row
                        })
                        delete initData.selectedSeat[this.settings.id];
                        return 'available';
                    } else if (this.status() == 'unavailable') { //若為已售出狀態
                        return 'unavailable';
                    } else {
                        return this.style();
                    }
                }
            });
            //設置已售出的座位
            sc.get(['1_3', '1_4', '4_4', '4_5', '4_6', '4_7', '4_8']).status('unavailable'); 
            interaction.commitSeat();
        },
        getTotalPrice:function(sc){//計算票價總額
            var total = 0;
            sc.find('selected').each(function() {
                total += price;
            });
            return total;
        },
        emit:function(type,msg){
           initData.socket.emit(type,msg); 
        },
        socketEvent:function(){
            this.emit("login","用戶進入選座頁面");        
            initData.socket.on("loginlock",function(loginlock){
                for(var t in loginlock){
                    var isMine = interaction.isMineFire(t,"selected");
                    if (!isMine) {
                       $('#'+t).addClass("locking");  
                    }
                }    
            })
            initData.socket.on("locking",function(data){
                var isMine = interaction.isMineFire(data.id,"selected");
                if (!isMine) {
                    $('#'+data.id).addClass("locking")
                }
            })
            initData.socket.on("canclelocking",function(data){
                $('#'+data.id).removeClass("locking"); 
            })
            initData.socket.on("userout",function(outuser){
                // outuser 為退出用戶所選擇的座位。
                for(var t in outuser){
                    $('#'+t).removeClass("locking"); 
                }
            })
            initData.socket.on("seatsold",function(soldseat){
                // soldseat 為用戶已經購買的座位。  客戶端更新座位狀態
                $.each(soldseat,function(index,item){
                    $('#'+item).addClass('unavailable');
                })
            })        
        },
        isMineFire:function(id,type){
            return  $('#'+id).attr('class').indexOf(type) > 0;
        },
        commitSeat:function(){
            $("#commitSeat").click(function(){
                if (JSON.stringify(initData.selectedSeat) === "{}") {
                    alert("請至少選擇一個座位再提交!")
                    return false;
                }
                //$.post("http://XXXXXXXX",座位數據,function(){
                // 延遲2秒模擬生成訂單的ajax請求,請求成功跳轉訂單頁。
                setTimeout(function() {
                    interaction.emit("sold",initData.selectedSeat);
                    location.href = "/order";
                }, 2000);
                //})
            })
        }
    }
    interaction.initMap();
    interaction.socketEvent();  
})

5、查看效果

打開瀏覽器,輸入localhost:3000 
多打開幾個瀏覽器,可查看實時響應效果

6、注意事項

此處需要修改為你自己的端口,否則會出現監聽不到的情況。

注意事項


作者 HoChine
2017 年 09月 03日
項目演示: http://hochine.cn/demo/realTimeChooseSeat
GitHub地址: https://github.com/HoChine/RealTime-chooseSeat


免責聲明!

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



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