一、实践效果图
二、环境准备
项目架构采用前后端分离模式进行开发,前端使用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<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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;
}
}