ThinkPHP5.0+mpvue开发小程序私聊功能

一、实践效果图

ThinkPHP5.0+mpvue开发小程序私聊功能插图

 二、环境准备

项目架构采用前后端分离模式进行开发,前端使用mpvue,后端使用ThinkPHP开发接口为前端提供业务功能服务。

我在ThinkPHP5.0.22版本中集成了GatewayWorker框架。我选择的集成方式是自己去下载软件包进行解压,也可以选择composer命令集成。首先下载GatewayWorker与GatewayClient,然后在项目根目录下的vendor目录下进行解压:

ThinkPHP5.0+mpvue开发小程序私聊功能插图(1)

GatewayWorker官方文档传送门:http://doc2.workerman.net/326107,感兴趣的大佬可以看看。一开始不是很了解GatewayWorker框架,看官方文档:与ThinkPHP等框架结合那一篇的时候就比较分不清楚GatewayWorker与GatewayClient的关系。之后通过一番摸索,大概理解了一番:

ThinkPHP5.0+mpvue开发小程序私聊功能插图(2)

接下来,我们先把环境运行起来。首先,修改GatewayWorker解压目录下Applications/YourApp/start_gateway.php文件,将text协议改成Websocket:

ThinkPHP5.0+mpvue开发小程序私聊功能插图(3)

ThinkPHP5.0+mpvue开发小程序私聊功能插图(4)

之后在GatewayWorker解压目录下找到:start_for_win.bat文件,双击运行:

ThinkPHP5.0+mpvue开发小程序私聊功能插图(5)

需要特别注意两个地址,作用我们稍后在代码中会看到:

ThinkPHP5.0+mpvue开发小程序私聊功能插图(6)

三、功能开发

对于GatewayWorker框架,主要需要编辑到的文件是:GatewayWorker/Applications/YourApp/Events.php文件

ThinkPHP5.0+mpvue开发小程序私聊功能插图(7)

对该文件,在本项目中主要需要关注一个函数:onConnect($client_id),当前端建立websocket与 GatewayWorker相连接成功时,该函数被调用,参数$client_id是 GatewayWorker分配给该客户端的client_id。下面onConnect($client_id)中使用了Gateway API:sendToClient(client_id,message),向指定client_id发送消息。下面代码的作用是将分配到的client_id返回给当前客户端。

ThinkPHP5.0+mpvue开发小程序私聊功能插图(8)

 我们可以在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函数自动被调用,并将返回我们自己规定好的消息结构体:

ThinkPHP5.0+mpvue开发小程序私聊功能插图(9)

ThinkPHP5.0+mpvue开发小程序私聊功能插图(10)

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发送消息。

ThinkPHP5.0+mpvue开发小程序私聊功能插图(11)

那么我们在哪里使用Gateway::bindUid等函数?

我选择在前端拿到分配的client_id之后,发送到后端Controller层进行client_id与用户id的绑定处理。这个时候还记得我们在Events.php的onConnect()中定义返回的消息结构体吗?

[

            'type' => 'init',

            'client_id' => $client_id,

            'message' => ''

 ]

这里type='init'就可以作为当前用户是否刚进入聊天界面的判断。

ThinkPHP5.0+mpvue开发小程序私聊功能插图(12)

接下来我们来看看后台绑定逻辑是怎么样的。首先,为了能够在controller层中使用Gateway::bindUid等函数,需要集成GatewayClient,才能够在Controller层中使用Gateway API。之前我们已经把GatewayClient集成进来了。在控制器Chat.php中使用的时候记得引入GatewayClient/Gateway.php文件:

use GatewayClient\Gateway;

require_once VENDOR.'GatewayClient/Gateway.php';

ThinkPHP5.0+mpvue开发小程序私聊功能插图(13)

ThinkPHP5.0+mpvue开发小程序私聊功能插图(14)

之后在控制器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、发送方、接收方、消息状态(已读、未读)、发送时间、消息内容。这张主要用于显示聊天界面中的聊天信息。具体如何读取可自行设计。

后端绑定后,我们在前端打印一下:

ThinkPHP5.0+mpvue开发小程序私聊功能插图(15)

到这里,我们就可以正式开始发送消息啦。关于发送消息有两种方式,一种是通过GatewayWorker框架的onMessage()方法来发送消息。一种是通过controller层提供接口实现发送消息(这种主要利用GatewayClient实现)。这两种方式,我都会在下面展示用法。

首先先看第一种,使用GatewayWorker框架的onMessage()方法。前端方面主要监听聊天界面的发送按钮:

ThinkPHP5.0+mpvue开发小程序私聊功能插图(16)

在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服务。在前端看看效果:

ThinkPHP5.0+mpvue开发小程序私聊功能插图(17)

注意JSON.stringify()会对中文进行unicode编码,解决方式:https://developers.weixin.qq.com/community/develop/doc/0008ea2e650cb86cb987789cb51800。就是对呗编码成unicode的中文,用String()括住。代码如下:

ThinkPHP5.0+mpvue开发小程序私聊功能插图(18)

ThinkPHP5.0+mpvue开发小程序私聊功能插图(19)

对消息内容中文处理完毕后就可以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);
    }

重点关注:

ThinkPHP5.0+mpvue开发小程序私聊功能插图(20) 

前端对应代码如下:

ThinkPHP5.0+mpvue开发小程序私聊功能插图(21)

ThinkPHP5.0+mpvue开发小程序私聊功能插图(22)

至此,前端已经可以接收与发送消息了,就差对接收到的消息进行处理与展示部分没有继续写出来。后面会贴全部代码。现在回看GatewayWorker官方文档的几句话。同篇博客算是我对GatewayWorker的一点小理解。

ThinkPHP5.0+mpvue开发小程序私聊功能插图(23)

ThinkPHP5.0+mpvue开发小程序私聊功能插图(24) 前端全部源代码(包括了如何展示部分的代码):

<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;
    }


    
}

 

没有账号? 忘记密码?

社交账号快速登录