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

一、实践效果图

ThinkPHP5.0+mpvue开发小程序私聊功能_第1张图片

 二、环境准备

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

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

ThinkPHP5.0+mpvue开发小程序私聊功能_第2张图片

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

ThinkPHP5.0+mpvue开发小程序私聊功能_第3张图片

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

ThinkPHP5.0+mpvue开发小程序私聊功能_第4张图片

ThinkPHP5.0+mpvue开发小程序私聊功能_第5张图片

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

ThinkPHP5.0+mpvue开发小程序私聊功能_第6张图片

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

ThinkPHP5.0+mpvue开发小程序私聊功能_第7张图片

三、功能开发

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

ThinkPHP5.0+mpvue开发小程序私聊功能_第8张图片

对该文件,在本项目中主要需要关注一个函数: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开发小程序私聊功能_第9张图片

 我们可以在mpvue中建立websocket连接,看看效果。首先mpvue聊天界面对应的vue文件:chat.vue代码如下:






该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开发小程序私聊功能_第10张图片

ThinkPHP5.0+mpvue开发小程序私聊功能_第11张图片

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开发小程序私聊功能_第12张图片

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

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

[

            'type' => 'init',

            'client_id' => $client_id,

            'message' => ''

 ]

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

ThinkPHP5.0+mpvue开发小程序私聊功能_第13张图片

接下来我们来看看后台绑定逻辑是怎么样的。首先,为了能够在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开发小程序私聊功能_第14张图片

ThinkPHP5.0+mpvue开发小程序私聊功能_第15张图片

之后在控制器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开发小程序私聊功能_第16张图片

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

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

ThinkPHP5.0+mpvue开发小程序私聊功能_第17张图片

在GatewayWorker框架中,修改onMessage函数,编辑GatewayWorker/Applications/YourApp/Events.php文件:


 * @copyright walkor
 * @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开发小程序私聊功能_第18张图片

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

ThinkPHP5.0+mpvue开发小程序私聊功能_第19张图片

ThinkPHP5.0+mpvue开发小程序私聊功能_第20张图片

对消息内容中文处理完毕后就可以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开发小程序私聊功能_第21张图片 

前端对应代码如下:

ThinkPHP5.0+mpvue开发小程序私聊功能_第22张图片

ThinkPHP5.0+mpvue开发小程序私聊功能_第23张图片

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

ThinkPHP5.0+mpvue开发小程序私聊功能_第24张图片

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






后端代码(我自己的后台中用了token认证):

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


    
}
msg = $params['msg'];
        }
        if(array_key_exists('data',$params)){
            $this->data = $params['data'];
        }
    }

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


    
}

 

你可能感兴趣的:(web,wx,ThinkPHP,mpvue,tp5.0,小程序私聊功能)