一、實踐效果圖
二、環境準備
項目架構採用前後端分離模式進行開發,前端使用mpvue,後端使用ThinkPHP開發接口爲前端提供業務功能服務。
我在ThinkPHP5.0.22版本中集成了GatewayWorker框架。我選擇的集成方式是自己去下載軟件包進行解壓,也可以選擇composer命令集成。首先下載GatewayWorker與GatewayClient,然後在項目根目錄下的vendor目錄下進行解壓:
GatewayWorker官方文檔傳送門:http://doc2.workerman.net/326107,感興趣的大佬可以看看。一開始不是很瞭解GatewayWorker框架,看官方文檔:與ThinkPHP等框架結合那一篇的時候就比較分不清楚GatewayWorker與GatewayClient的關係。之後通過一番摸索,大概理解了一番:
接下來,我們先把環境運行起來。首先,修改GatewayWorker解壓目錄下Applications/YourApp/start_gateway.php文件,將text協議改成Websocket:
之後在GatewayWorker解壓目錄下找到:start_for_win.bat文件,雙擊運行:
需要特別注意兩個地址,作用我們稍後在代碼中會看到:
三、功能開發
對於GatewayWorker框架,主要需要編輯到的文件是:GatewayWorker/Applications/YourApp/Events.php文件
對該文件,在本項目中主要需要關注一個函數:onConnect($client_id),當前端建立websocket與 GatewayWorker相連接成功時,該函數被調用,參數$client_id是 GatewayWorker分配給該客戶端的client_id。下面onConnect($client_id)中使用了Gateway API:sendToClient(client_id,message),向指定client_id發送消息。下面代碼的作用是將分配到的client_id返回給當前客戶端。
我們可以在mpvue中建立websocket連接,看看效果。首先mpvue聊天界面對應的vue文件:chat.vue代碼如下:
<template>
<div class="wrapper" :style="{MinHeight: windowHeight+'px', width: windowWidth+'px'}">
<scroll-view scroll-y class="chat_content">
<ul>
<li class="tidings_base" :class="[item.isSelf?'myself':'other']" v-for="(item, index) in list" :key="index">
<div class="user_img">
<img src="/static/images/chat-user.png" />
</div>
<div class="text">
<span>{{item.text}}</span>
</div>
</li>
</ul>
</scroll-view>
<div class="send_box">
<textarea v-model="say" fixed="true" contenteditable="true" auto-height="true"></textarea>
<span class="send_btn" @click="sendSocketMessage">發送</span>
</div>
</div>
</template>
<script>
export default {
data () {
return {
windowHeight: 0,
windowWidth: 0,
say: '',
list: [],
socketOpen: false,
clientId: '',
otherId: ''
}
},
methods: {
startChat () {
// 啓動wwebSocket
wx.connectSocket({
url: 'ws://www.zwl.com:8282'
})
// 監聽鏈接成功
wx.onSocketOpen(res => {
console.log('調用了onSocketOpen')
this.socketOpen = true
console.log('鏈接成功')
})
// 監聽接受到服務器的消息
wx.onSocketMessage(res => {
console.log('監聽接收到服務器的消息:')
console.log(res)
// 進行json解析
let data = JSON.parse(res.data)
if (data.type === 'init') {
this.clientId = data.client_id
// 綁定用戶
}
})
// 監聽鏈接關閉事件
wx.onSocketClose(res => {
this.socketOpen = false
console.log('調用了onSocketClose')
})
},
// 發送聊天消息
sendSocketMessage () {
console.log('調用了發送消息方法')
if (this.socketOpen) {
wx.sendSocketMessage({
data: {
'message': this.say,
'toUid': this.otherId
}
})
}
},
// 獲取屏幕寬高
initPageStyle () {
let that = this
wx.getSystemInfo({
success (res) {
that.windowHeight = res.windowHeight
that.windowWidth = res.windowWidth
}
})
},
// 初始化數據
async initData () {
let goodsId = this.$root.$mp.query.id
// 根據商品id獲取商家ID並賦值給this.otherId
this.otherId = await this.$http.get({
url: '/goods/owner/' + goodsId
}).then(res => {
return res['storeId']
}).catch(() => {
return ''
})
}
},
// 退出頁面前關閉websocket連接,清空頁面數據
onUnload () {
if (this.socketOpen) {
wx.closeSocket()
}
this.list = []
this.say = ''
this.socketOpen = false
this.clientId = ''
this.otherId = ''
},
onShow () {
this.initPageStyle()
this.initData()
this.startChat()
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.wrapper
box-sizing: border-box
padding: 20rpx
padding-bottom: 100rpx
background: #EDEDED
.send_box
width: 100%
padding: 20rpx
border-top: 1rpx solid #f4f4f4
borderbox-shadow: 0 0 5px silver
background: #F6F6F6
display: flex
justify-content: space-between
align-items: center
box-sizing: border-box
position: fixed
bottom: 0
left: 0
& textarea
flex: 1
background: white
padding: 10rpx
max-height: 58px!important
.send_btn
background: #85A5CC
border-radius: 8rpx
color: white
margin-left: 20rpx
padding: 10rpx 20rpx
.chat_content
min-height: 100%
.tidings_base
display: flex
padding: 10rpx 0
align-items: center
.user_img
width: 84rpx
height: 84rpx
background: white
border-radius: 8rpx
& img
width: 100%
height: 100%
.text
flex: 1
padding: 20rpx
border-radius: 8rpx
margin: 20rpx 0 20rpx 20rpx
position: relative
.other
.text
margin-right: 20rpx
background: white
color: black
& span::before
content: ''
right: 100%
top: 25%
border: 16rpx solid #ffffff00
border-right: 16rpx solid white
position: absolute
.myself
justify-content: flex-end
.text
order: -1
margin-right: 20rpx
background: #85A5CC
color: black
& span::after
content: ''
left: 100%
top: 25%
border: 16rpx solid #ffffff00
border-left: 16rpx solid #85A5CC
position: absolute
</style>
該vue的data中定義的與聊天功能相關的變量意義如下:
- say:存放用戶輸入的聊天信息
- socketOpen:默認是false,用於保存websocket連接的狀態,true表示建立websocket成功。false表示連接關閉。
- clientId:存放當前用戶從GatewayWorker分配到的client_id
- otherId:存放消息接受方的用戶ID(對方可能處於還未初始化,未從GatewayWorker分配到client_id的狀態,所以通過用戶ID指定接受方比較好)。
- list:初始值是空數組,該數組用於存放當前用戶發出去的消息與接受到的消息。數組內每個對象都是一個對象,每個對象的屬性如下:
- isSelf:false | true,用於區分該條消息是發送的還是接收到的,根據該值可以動態添加class值,改變樣式
- text: 消息內容
主要是注意wx.connectSocket中url地址。當連接建立成功時,前端自動觸發wx.onSocketOpen函數,該函數代表連接建立成功。當客戶端與GatewayWorker成功建立連接時,GatewayWorker的Events.php文件中的onConnect函數自動被調用,並將返回我們自己規定好的消息結構體:
GatewayWorker返回消息時,前端的wx.onSocketMessage()將被自動調用。換句話說就是我們可以在前端的wx.onSocketMessage()中接收GatewayWorker返回的消息。在該函數中,我們可以對返回的數據進行判斷,判斷type值是否爲init,我們用init來代表這條消息是當前用戶初次接入GatewayWorker。
對於GatewayWorker框架來說,每個用戶與它建立websocket連接的時候,都會被分配到一個client_id。而GatewayWorker就有提供函數用於向指定client_id用戶發送消息。但是,在實際應用場景中,我們需要考慮:接收方用戶可能不在線、接收方用戶可能從未與GatewayWorker建立websocket連接、離線消息需要進行存儲,等待用戶查看。接下來,我們以實際聊天功能進行分析。
在該聊天功能中,用戶每次進入聊天界面,就會開始與GatewayWorker建立websocket連接,在用戶退出聊天界面的時候,進行websocket連接關閉操作。所以用戶每次進入聊天界面,用戶被分配到的client_id都是不一樣的,並且發送方發送消息時,接收方並不一定處於在線狀態,所以接收方的client_id是未知的。因此我們只能通過用戶id來向指定用戶發送消息。可以使用Gateway::bindUid(client_id,uid)來實現client_id與用戶ID的綁定,使用Gateway::sendToUid($uid,$message)實現向指定用戶ID發送消息。
那麼我們在哪裏使用Gateway::bindUid等函數?
我選擇在前端拿到分配的client_id之後,發送到後端Controller層進行client_id與用戶id的綁定處理。這個時候還記得我們在Events.php的onConnect()中定義返回的消息結構體嗎?
[
'type' => 'init',
'client_id' => $client_id,
'message' => ''
]
這裏type='init'就可以作爲當前用戶是否剛進入聊天界面的判斷。
接下來我們來看看後臺綁定邏輯是怎麼樣的。首先,爲了能夠在controller層中使用Gateway::bindUid等函數,需要集成GatewayClient,才能夠在Controller層中使用Gateway API。之前我們已經把GatewayClient集成進來了。在控制器Chat.php中使用的時候記得引入GatewayClient/Gateway.php文件:
use GatewayClient\Gateway;
require_once VENDOR.'GatewayClient/Gateway.php';
之後在控制器Chat.php中編寫函數bindUid(),用於綁定client_id與用戶id,同時也可讀取未讀狀態的消息。
private $uid;
/**
* 綁定用戶id
* @url /chat/init
* @http post
*/
public function bindUid () {
$dataArr = input('post.');
$client_id = $dataArr['client_id'];
/**
* 註釋開始
* 此處可選擇由前端發送user_id用戶ID過來,也可以採用token獲取當前用戶id
*/
// 根據Token獲取uid
$this->uid = Token::getCurrentUid();
// 判斷當前用戶是否存在
UserService::isUserExist($this->uid);
/**
* 註釋結束
* 此處可選擇由前端發送user_id用戶ID過來,也可以採用token獲取當前用戶id
*/
// 綁定
Gateway::bindUid($client_id,$this->uid);
/**
* 註釋開始
* 獲取當前用戶未讀狀態消息,該部分可自己設計
*/
$chat = ChatModel::getChatAndChange($this->uid,1,10);
$result = array_map(function ($item) {
$temp = [
'msg_id' => $item['msg_id'],
'content' => $item['content'],
'is_self' => false,
'other' => $item['receiver_uid'],
'create_time' => $item['create_time']
];
if($item['uid'] == $this->uid) {
$temp['is_self'] = true;
}
return $temp;
},$chat);
/**
* 註釋結束
* 獲取當前用戶未讀狀態消息,該部分可自己設計
*/
/**
* 返回註釋
* 如果不打算獲取未讀消息,可以直接如下注釋返回
*/
//return json(new SuccessMessage(['msg' => '綁定用戶成功', 'data' => '']),201);
return json(new SuccessMessage(['msg' => '綁定用戶成功', 'data' => $result]),201);
}
對於消息記錄以及離線消息的處理,網上提供了幾種方案:在數據庫裏設計張表來存放消息、採用文件形式存放消息。這裏我選擇自己設計表來存放消息,我個人設計存放消息的數據庫表時,主要分了兩張,一張是隻有:發送方、接收方、最新通信時間、最新通信內容等字段。這張主要是用於顯示聊天列表的。還有一張是有:消息ID、發送方、接收方、消息狀態(已讀、未讀)、發送時間、消息內容。這張主要用於顯示聊天界面中的聊天信息。具體如何讀取可自行設計。
後端綁定後,我們在前端打印一下:
到這裏,我們就可以正式開始發送消息啦。關於發送消息有兩種方式,一種是通過GatewayWorker框架的onMessage()方法來發送消息。一種是通過controller層提供接口實現發送消息(這種主要利用GatewayClient實現)。這兩種方式,我都會在下面展示用法。
首先先看第一種,使用GatewayWorker框架的onMessage()方法。前端方面主要監聽聊天界面的發送按鈕:
在GatewayWorker框架中,修改onMessage函數,編輯GatewayWorker/Applications/YourApp/Events.php文件:
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<[email protected]>
* @copyright walkor<[email protected]>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
/**
* 用於檢測業務代碼死循環或者長時間阻塞等問題
* 如果發現業務卡死,可以將下面declare打開(去掉//註釋),並執行php start.php reload
* 然後觀察一段時間workerman.log看是否有process_timeout異常
*/
//declare(ticks=1);
use \GatewayWorker\Lib\Gateway;
use app\api\service\Token as Token;
/**
* 主邏輯
* 主要是處理 onConnect onMessage onClose 三個方法
* onConnect 和 onClose 如果不需要可以不用實現並刪除
*/
class Events
{
/**
* 當客戶端連接時觸發
* 如果業務不需此回調可以刪除onConnect
*
* @param int $client_id 連接id
*/
public static function onConnect($client_id)
{
// 返回數據給當前用戶
Gateway::sendToClient($client_id, json_encode([
'type' => 'init',
'client_id' => $client_id,
'message' => ''
]));
}
/**
* 當客戶端發來消息時觸發
* @param int $client_id 連接id
* @param mixed $message 具體消息
*/
public static function onMessage($client_id, $message)
{
// 對數據進行json解碼
$data = json_decode($message, JSON_UNESCAPED_UNICODE);
// 這裏採用直接向指定用戶ID發送數據
Gateway::sendToUid($message['toUid'],json_encode([
'type' => 'tidings',
'client_id' => $client_id,
'message' => $data['message']
]));
// 爲了方便測試,同時將數據也發給自己,方便前端在控制檯打印
Gateway::sendToClient($client_id,json_encode([
'type' => 'test',
'client_id' => $client_id,
'message' => $data['message']
]));
}
}
注意,每次修改Events.php文件,都需要重新啓動GatewayWorker服務。在前端看看效果:
注意JSON.stringify()會對中文進行unicode編碼,解決方式:https://developers.weixin.qq.com/community/develop/doc/0008ea2e650cb86cb987789cb51800。就是對唄編碼成unicode的中文,用String()括住。代碼如下:
對消息內容中文處理完畢後就可以push到list數組中,在template中進行for循環渲染。
這種方式的缺點是,如果用戶不在線時,我需要將消息存至數據庫時,我無法在Events.php文件中使用數據庫模型(model層的數據模型)。這就使得我無法在onMessage函數中處理接收方用戶不在線的情況。這迫使我選擇了第二種方式。
將GatewayWorker/Applications/YourApp/Events.php的onMessage函數置空,記得重複GatewayWorker服務:
/**
* 當客戶端發來消息時觸發
* @param int $client_id 連接id
* @param mixed $message 具體消息
*/
public static function onMessage($client_id, $message)
{
}
在控制器Chat.php中編寫函數sendToStore()用於實現向指定用戶ID發送數據。代碼如下:
/**
* 當前用戶向某一商品所有者發起聊天
* @url /chat/send_to_store
* @HTTP POST
* @id 商品ID
*/
public function sendToStore(){
/**
* 註釋開始
* 此處可選擇由前端發送user_id用戶ID過來,也可以採用token獲取當前用戶id
*/
// 根據Token獲取uid
$uid = Token::getCurrentUid();
// 判斷當前用戶是否存在
UserService::isUserExist($uid);
/**
* 註釋結束
* 此處可選擇由前端發送user_id用戶ID過來,也可以採用token獲取當前用戶id
*/
// 獲取信息
$dataArr = input('post.');
$client_id = $dataArr['client_id']; // 發送方client_id
$receiver = $dataArr['receiver_uid']; // 接收方用戶ID
$message = $dataArr['message']; // 發送消息內容
Gateway::$registerAddress = 'www.zwl.com:1238';
// 對方不在線則將消息存儲起來,在線返回1,不在線返回0
$isOnline = Gateway::isUidOnline($receiver);
if(!$isOnline){
// 插入等待讀取狀態的消息
$chat = ChatModel::saveChat($uid,$receiver,$message,0);
} else {
// 插入已經被讀取的消息
$chat = ChatModel::saveChat($uid,$receiver,$message,1);
}
//發送消息結構體
$send = json_encode([
'type' => 'tidings',
'client_id' => $client_id,
'message' => $message
],JSON_UNESCAPED_UNICODE);
// 向指定用戶發送
Gateway::sendToUid($receiver,$send);
// 用於測試,同時給自己發送一份
Gateway::sendToClient($client_id,$send);
return json(new SuccessMessage(['msg' => '發送消息成功','data' => $receiver]),201);
}
重點關注:
前端對應代碼如下:
至此,前端已經可以接收與發送消息了,就差對接收到的消息進行處理與展示部分沒有繼續寫出來。後面會貼全部代碼。現在回看GatewayWorker官方文檔的幾句話。同篇博客算是我對GatewayWorker的一點小理解。
前端全部源代碼(包括瞭如何展示部分的代碼):
<template>
<div class="wrapper" :style="{MinHeight: windowHeight+'px', width: windowWidth+'px'}">
<scroll-view scroll-y class="chat_content">
<ul>
<li class="tidings_base" :class="[item.isSelf?'myself':'other']" v-for="(item, index) in list" :key="index">
<div class="user_img">
<img src="/static/images/chat-user.png" />
</div>
<div class="text">
<span>{{item.text}}</span>
</div>
</li>
</ul>
</scroll-view>
<div class="send_box">
<textarea v-model="say" fixed="true" contenteditable="true" auto-height="true"></textarea>
<span class="send_btn" @click="sendSocketMessage">發送</span>
</div>
</div>
</template>
<script>
export default {
data () {
return {
windowHeight: 0,
windowWidth: 0,
say: '',
goodsId: 0,
list: [],
socketOpen: false,
clientId: '',
otherId: '',
isBind: false,
storeName: '',
buyerName: ''
}
},
methods: {
startChat () {
// 啓動wwebSocket
wx.connectSocket({
url: 'ws://www.zwl.com:8282'
})
// 監聽鏈接成功
wx.onSocketOpen(res => {
console.log('調用了onSocketOpen')
this.socketOpen = true
// this.list.push('鏈接成功')
console.log('準備發送消息')
// console.log(this.list)
})
// 監聽接受到服務器的消息
wx.onSocketMessage(res => {
console.log('onSocketMessage')
console.log('返回消息:')
console.log(res)
let data = JSON.parse(res.data)
if (data.type === 'init') {
this.clientId = data.client_id
// 綁定用戶
this.$http.post({
url: '/chat/init',
data: {'client_id': this.clientId}
}).then(info => {
this.isBind = true
info.data.forEach(item => {
let obj = {
'isSelf': item.is_self,
'text': item.content,
'create_time': item.create_time
}
this.list.unshift(obj)
})
console.log('綁定成功')
console.log(info)
})
} else if (data.type === 'tidings') {
let item = {
'isSelf': false,
'text': data.message,
'create_time': ''
}
console.log('接收到的')
console.log(data)
this.list.push(item)
this.fromClientId = data.client_id
}
})
// 監聽鏈接關閉事件
wx.onSocketClose(res => {
this.socketOpen = false
console.log('調用了onSocketClose')
})
},
// 發送聊天消息
sendSocketMessage () {
console.log('調用了發送消息方法')
console.log(this.otherId)
let item = {
'isSelf': true,
'text': this.say,
'create_time': ''
}
this.list.push(item)
this.$http.post({
url: '/chat/send_to_store',
data: {'message': this.say, 'client_id': this.clientId, 'receiver_uid': this.otherId}
}).then(res => {
console.log(this.list)
console.log('post請求')
console.log(res)
})
},
// 使頁面滾動到容器最底部
pageScrollToBottom () {
wx.createSelectorQuery().select('.chat_content').boundingClientRect(rect => {
wx.pageScrollTo({
scrollTop: rect.bottom
})
}).exec()
},
initPageStyle () {
let that = this
wx.getSystemInfo({
success (res) {
that.windowHeight = res.windowHeight
that.windowWidth = res.windowWidth
}
})
},
// 動態設置導航標題
async initNavigationBarTitle () {
let goodsId = this.$root.$mp.query.id
this.goodsId = goodsId
this.storeName = await this.$http.get({
url: '/goods/owner/' + goodsId
}).then(res => {
this.otherId = res['storeId']
this.buyerName = res['buyerName']
return res['storeName']
}).catch(() => {
return ''
})
wx.setNavigationBarTitle({
title: this.storeName
})
},
getPageParams () {
console.log('initDara')
let goodsId = this.$root.$mp.query.id
let info = this.$root.$mp.query.info
if (goodsId) {
this.goodsId = goodsId
this.initNavigationBarTitle()
}
if (info) {
info = JSON.parse(info)
console.log('初始阿虎')
console.log(info)
this.otherId = info['other']
wx.setNavigationBarTitle({
title: info['name']
})
}
console.log('this.otherId')
}
},
onUnload () {
if (this.socketOpen) {
wx.closeSocket()
}
this.list = []
this.say = ''
this.socketOpen = false
this.clientId = ''
this.otherId = ''
this.isBind = false
this.storeName = ''
this.buyerName = ''
},
onShow () {
this.initPageStyle()
this.getPageParams()
this.startChat()
this.pageScrollToBottom()
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.wrapper
box-sizing: border-box
padding: 20rpx
padding-bottom: 100rpx
background: #EDEDED
.send_box
width: 100%
padding: 20rpx
border-top: 1rpx solid #f4f4f4
borderbox-shadow: 0 0 5px silver
background: #F6F6F6
display: flex
justify-content: space-between
align-items: center
box-sizing: border-box
position: fixed
bottom: 0
left: 0
& textarea
flex: 1
background: white
padding: 10rpx
max-height: 58px!important
.send_btn
background: #85A5CC
border-radius: 8rpx
color: white
margin-left: 20rpx
padding: 10rpx 20rpx
.chat_content
min-height: 100%
.tidings_base
display: flex
padding: 10rpx 0
align-items: center
.user_img
width: 84rpx
height: 84rpx
background: white
border-radius: 8rpx
& img
width: 100%
height: 100%
.text
flex: 1
padding: 20rpx
border-radius: 8rpx
margin: 20rpx 0 20rpx 20rpx
position: relative
.other
.text
margin-right: 20rpx
background: white
color: black
& span::before
content: ''
right: 100%
top: 25%
border: 16rpx solid #ffffff00
border-right: 16rpx solid white
position: absolute
.myself
justify-content: flex-end
.text
order: -1
margin-right: 20rpx
background: #85A5CC
color: black
& span::after
content: ''
left: 100%
top: 25%
border: 16rpx solid #ffffff00
border-left: 16rpx solid #85A5CC
position: absolute
</style>
後端代碼(我自己的後臺中用了token認證):
<?php
namespace app\api\controller\v1;
use app\api\model\Chat as ChatModel;
use app\api\service\Token as Token;
use app\api\service\User as UserService;
use app\api\validate\ChatInitValidate;
use app\api\validate\ChatMessage;
use app\lib\exception\SuccessMessage;
use think\Controller;
use GatewayClient\Gateway;
require_once VENDOR.'GatewayClient/Gateway.php';
class Chat extends Controller
{
private $uid;
/**
* 綁定用戶id
* @url /chat/init
* @http post
*/
public function bindUid () {
$validate = new ChatInitValidate();
$validate->goCheck();
$dataArr = $validate->getDataByRule(input('post.'));
$client_id = $dataArr['client_id'];
// 根據Token獲取uid
$this->uid = Token::getCurrentUid();
// 判斷當前用戶是否存在
UserService::isUserExist($this->uid);
// 綁定
Gateway::bindUid($client_id,$this->uid);
// 獲取未讀狀態消息
$chat = ChatModel::getChatAndChange($this->uid,1,10);
$result = array_map(function ($item) {
$temp = [
'msg_id' => $item['msg_id'],
'content' => $item['content'],
'is_self' => false,
'other' => $item['receiver_uid'],
'create_time' => $item['create_time']
];
if($item['uid'] == $this->uid) {
$temp['is_self'] = true;
}
return $temp;
},$chat);
return json(new SuccessMessage(['msg' => '綁定用戶成功', 'data' => $result]),201);
}
/**
* 當前用戶向某一商品所有者發起聊天
* @url /chat/send_to_store
* @HTTP POST
* @id 商品ID
*/
public function sendToStore(){
$validate = new ChatMessage();
$validate->goCheck();
// 根據Token獲取uid
$uid = Token::getCurrentUid();
// 判斷當前用戶是否存在
UserService::isUserExist($uid);
// 獲取信息
$dataArr = $validate->getDataByRule(input('post.'));
$client_id = $dataArr['client_id'];
$receiver = $dataArr['receiver_uid'];
$message = $dataArr['message'];
Gateway::$registerAddress = 'www.zwl.com:1238';
// 對方不在線則將消息存儲起來
$isOnline = Gateway::isUidOnline($receiver);
if(!$isOnline){
// 插入等待接收狀態的消息
$chat = ChatModel::saveChat($uid,$receiver,$message,0);
} else {
// 插入已經被接收的消息
$chat = ChatModel::saveChat($uid,$receiver,$message,1);
}
//發送消息
$send = json_encode([
'type' => 'tidings',
'client_id' => $client_id,
'message' => $message
],JSON_UNESCAPED_UNICODE);
Gateway::sendToUid($receiver,$send);
return json(new SuccessMessage(['msg' => '發送消息成功','data' => $receiver]),201);
}
}
<?php
namespace app\lib\exception;
class SuccessMessage
{
public $code = 201;
public $msg = '操作成功';
public $errorCode = 0;
public $data = '';
//傳入可選參數
public function __construct($params = [])
{
//如果傳入參數不是數組,返回默認值
if(!is_array($params)){
return ;
}
if(array_key_exists('msg',$params)){
$this->msg = $params['msg'];
}
if(array_key_exists('data',$params)){
$this->data = $params['data'];
}
}
}
<?php
namespace app\api\model;
use app\api\model\BaseModel;
use app\api\model\ChatList as ChatListModel;
use Exception;
use think\Db;
class Chat extends BaseModel
{
protected $hidden = ['delete_time'];
// 關閉update_time字段自動寫入
protected $updateTime = false;
protected static $uid;
/**
* 存儲聊天記錄
*/
public static function saveChat($uid,$receiver,$content,$status){
Db::startTrans();
try {
$chatListModel = new ChatListModel();
$isExist = $chatListModel::where(['uid' => $uid, 'receiver_uid' => $receiver])->find();
if(!$isExist){
$chatListModel->save(['uid' => $uid, 'receiver_uid' => $receiver]);
}else {
$chatListModel->isUpdate()->save(['uid' => $uid, 'receiver_uid' => $receiver]);
}
$chat = self::create(['uid' => $uid, 'receiver_uid' => $receiver, 'content' => $content, 'status' => $status]);
} catch (\Exception $e) {
Db::rollback();
throw new Exception($e);
}
return $chat;
}
/**
* 獲取未讀消息並設置成已讀取
*/
public static function getChatAndChange($id,$pages,$pageNum){
self::$uid = $id;
$chat = self::order('create_time desc')->where(['receiver_uid' => $id])->whereOr(['uid' => $id])->page($pages,$pageNum)->select()->toArray();
$result = array_map(function ($item) {
if($item['receiver_uid'] == self::$uid){
return ['msg_id' => $item['msg_id'], 'status' => 1];
}else{
return [];
}
},$chat);
$chatModel = new Chat();
$chatModel->isUpdate(true)->saveAll($result);
return $chat;
}
}