ThinkPHP5.0+mpvue開發小程序私聊功能

一、實踐效果圖

 二、環境準備

項目架構採用前後端分離模式進行開發,前端使用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;
    }


    
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章