最終效果如下?(筆記本螢幕比較小?)
前言
練手專案,程式碼目前只完成了聊天部分,還有其他部分沒有寫完,有不完善的地方請多多指教。?
實現功能
- 會話列表
- 未讀
- 閱讀
- 聊天
- 文字訊息
- 圖片訊息
- emoji
待實現功能
- 傳送失敗
- 傳送失敗標記
- 重新傳送
- 撤回
- 聊天記錄定時儲存到資料庫
Redis資料
Key 型別 描述 值 laravel_database_(user_id) String 儲存client id,以使用者的id為key。傳送訊息時,根據id取出client id然後傳送訊息。 laravel_database_online_(user_id) String 儲存當前時間,五分鐘後過期,用於判斷使用者是否線上。可以在傳送心跳的時候更新這個值,close時清除該值。 laravel_database_logs_(會話id) List 以dialog_id為key,儲存聊天記錄。 laravel_database_logs_id_(會話id) Hash 記錄msg id在list中的索引(index)值 laravel_database_msg_group_(user_id) Hash 以使用者id為key,記錄該使用者的會話列表 - laravel_database_msg_group_(user_id)
{ "dialog_id": "2349643881_2535180514", # 兩個使用者的唯一會話id, 用於聊天記錄的查詢 "body": "你好 # 最後一條訊息內容,根據msg_type將展示不同的內容 "created_at": "2022-06-28 22:06:39", # 最後一條訊息的時間 "no_read": 0, # 未讀的數量 "msg_type": 1, # 訊息型別,1是文字訊息,2是圖片 "model": { # 聊天物件 "uuid": 2535180514, "name": "Sheldon Zboncak", "avatar": "avatar" } }
- laravel_database_logs_(會話id)
{ "class": "admin", "function": "say", "from_id": 2349643881, # 傳送者的id "from_model": "admin", # 傳送者的模型 "from": { # 傳送者的資訊 "uuid": 2349643881, "name": "Admin", "avatar": "avatar" }, "to_id": 2535180514, # 接收者的id "to_model": "user", # 接收者的模型 "to": { # 接收者的資訊 "uuid": 2535180514, "name": "Sheldon Zboncak", "avatar": "avatar" }, "body": "/uploads/images/201710/14/1/ZqM7iaP4CR.png", # 訊息內容, "msg_type": 2, # 訊息型別 "uuid": "43n2657oxv60000000", # 前端生成的訊息的唯一id "has_read": false, # 是否閱讀 "read_at": "", # 閱讀時間 "dialog_id": "2349643881_2535180514", # 會話id "created_at": "2022-06-28 22:06:39" # 建立時間 }
SwooleHandler
class SwooleHandler
{
public $chatCache;
public function __construct()
{
$this->chatCache = new ChatCache();
}
public function onMessage(Server $server, Frame $frame)
{
$content = $frame->data;
$content = json_decode($content);
$myFD = $frame->fd;
switch ($content->function) {
# 前端每十秒傳送一次心跳,接受到心跳後,更新線上時間
case 'ping': #{"class":"user","function":"ping","user":{"id":1376688201,"name":"Admin"}}
$uuid = $content->user->id;
$server->push($myFD, Message::ping());
$this->chatCache->setOnline($uuid);
break;
# 前端在onopen時傳送user資訊,後臺將user id與fa繫結,更新redis中的資料(未關閉舊裝置連線),並更新線上時間
case 'login': #{"class":"user","function":"login","user":{"id":1376688201,"name":"Admin"}}
$uuid = $content->user->id;
$this->chatCache->bindUuid($uuid, $myFD);
$server->bind($myFD, $uuid);
$server->push($myFD, Message::login());
$this->chatCache->setOnline($uuid);
break;
# 根據user_id獲取使用者是否線上
case 'is_online': #{"class":"user","function":"is_online","user":{"id":1376688201,"name":"Admin"}}
$uuid = $content->user->uuid;
$status = $this->chatCache->isOnline($uuid);
$server->push($myFD, Message::isOnline($status));
break;
# 當使用者處於對應的聊天頁面時,或正在與該使用者聊天時。前端將收到的訊息立即傳送回來,後端接收到後,根據msg的uuid更新list中的has_read和read_at兩個欄位
case 'read':#{"class":"user","function":"read","from_id":1420677271,"to_id":2455347791,"uuid":"81b7a11b-a3e7-463a-af0b-dc0bf64883c6"}
$dialogId = $content->dialog_id;
if ($content->uuid) {
# 根據會話id與msg的uuid獲取該條記錄的索引
$index = $this->chatCache->getLogId($dialogId, $content->uuid);
# 根據索引獲取聊天記錄
$log = $this->chatCache->getOneLogByIndex($dialogId, $index);
# 更新聊天記錄
$log = json_decode($log);
$log->has_read = true;
$log->read_at = now()->toDateTimeString();
$this->chatCache->setLogByIndex($dialogId, $index, $log);
# 更新會話列表
$group = $this->chatCache->getOneOfMsgGroup($content->to_id, $content->from_id);
$group = json_decode($group);
$group->no_read = 0;
$this->chatCache->setMsgGroup($content->to_id, $content->from_id, $group);
}
break;
# 點選會話列表時,閱讀所有的未讀訊息。前端會先根據no_read欄位的值判斷是否需要傳送readAll訊息
case 'readAll':#{"class":"user","function":"read","dialog_id":"254856121_1376688201","body":"aaaaaaaaaaaaaaaaa","created_at":"2022-06-17 15:59:15","no_read":0,"model":{"uuid":254856121,"name":"Admin"}}
$dialogId = $content->dialog_id;
$noRead = $content->no_read;
$uuid = $content->model->uuid;
# 判斷是否存在未讀訊息
if ($noRead) {
# 根據no_read數量獲取對應數量的記錄
$logs = $this->chatCache->getSomeLogsBy($dialogId, -1, $noRead);
foreach ($logs as $log) {
$log = json_decode($log);
if ($log->from_id == $content->model->uuid) {
$index = $this->chatCache->getLogId($dialogId, $log->uuid);
$log->has_read = true;
$log->read_at = now()->toDateTimeString();
$this->chatCache->setLogByIndex($dialogId, $index, $log);
}
}
$myUuid = str_replace('_', '', $dialogId);
$myUuid = str_replace($uuid, '', $myUuid);
$group = $this->chatCache->getOneOfMsgGroup($myUuid, $uuid);
$group = json_decode($group);
$group->no_read = 0;
$this->chatCache->setMsgGroup($myUuid, $uuid, $group);
}
break;
# 傳送訊息。根據收,發人的id生成唯一會話id,處理資料存入redis list中,新增或更新傳送者和接收者的會話列表
case 'say':#{"class":"user","function":"say","from":{"id":1376688201,"name":"Admin"},"from_id":1376688201,"from_model":"user","to":{"id":254856121,"name":"Admin"},"to_id":254856121,"to_model":"lawyer","body":"aaaaaaaaaaaaaaaaa","uuid":"b580320d-1b76-4df9-87a4-0c6e200e9910","msg_type":1,"created_at":""}
$dialogId = get_dialog_id($content->from_id, $content->to_id);
$uuid = $content->from_id;
$data = Message::say($content, $dialogId);
$num = $this->chatCache->setLogBy($dialogId, $data);
$this->chatCache->setLogId($dialogId, $content->uuid, $num - 1);
$group = [
'dialog_id' => $dialogId,
'body' => $content->body,
'created_at' => now()->toDateTimeString(),
'no_read' => 0,
'msg_type' => $content->msg_type,
'model' => [
'uuid' => $content->to_id,
'name' => $content->to->name,
'avatar' => $content->to->avatar,
],
];
$this->chatCache->setMsgGroup($uuid, $content->to_id, $group);
if ($this->chatCache->groupIsExists($content->to_id, $uuid)) {
$toGroup = $this->chatCache->getOneOfMsgGroup($content->to_id, $uuid);
$toGroup = json_decode($toGroup);
} else {
$toGroup = (object)[
'dialog_id' => $dialogId,
'body' => $content->body,
'created_at' => now()->toDateTimeString(),
'no_read' => 0,
'msg_type' => $content->msg_type,
'model' => [
'uuid' => $content->from_id,
'name' => $content->from->name,
'avatar' => $content->from->avatar,
],
];
}
$toGroup->body = $group['body'];
$toGroup->created_at = $group['created_at'];
$toGroup->no_read += 1;
$this->chatCache->setMsgGroup($content->to_id, $uuid, $toGroup);
$toFd = $this->chatCache->getFdBy($content->to_id);
if ($toFd && $server->isEstablished($toFd)) {
$server->push($toFd, json_encode($data));
}
$data['function'] = 'said';
$server->push($myFD, json_encode($data));
break;
case 'logs':#{"class":"user","function":"logs","dialog_id":"254856121_1376688201","page":"1","per_page":"10"}
$cacheData = $this->chatCache->getPaginateLogsBy($content->dialog_id, $content->page, $content->per_page);
$logs = [];
foreach ($cacheData['data'] as $datum) {
$logs[] = json_decode($datum);
}
$server->push($myFD, Message::logs($logs));
break;
}
}
}
ChatCache
class ChatCache
{
public $cache;
public function __construct()
{
$this->cache = Redis::connection();
}
public function bindUuid($uuid, $fd)
{
$this->cache->set($uuid, $fd);
}
public function getFdBy($uuid)
{
return $this->cache->get($uuid);
}
public function setOnline($uuid)
{
$key = 'online_' . $uuid;
$this->cache->setex($key, 5 * 60, now()->toDateTimeString());
}
public function setOffline($uuid)
{
$key = 'online_' . $uuid;
$this->cache->del($key);
}
public function isOnline($uuid)
{
$key = 'online_' . $uuid;
return $this->cache->exists($key);
}
public function setLogBy($dialogId, $data)
{
$key = 'logs_' . $dialogId;
return $this->cache->lpush($key, json_encode($data));
}
public function setLogByIndex($dialogId, $index, $data)
{
$key = 'logs_' . $dialogId;
return $this->cache->lset($key, $index, json_encode($data));
}
public function getPaginateLogsBy($dialogId, $page = 1, $perPage = 15)
{
$total = $this->getLogLen($dialogId);
$pageCount = ceil($total / $perPage);
$start = ($page - 1) * $perPage;
$end = $start + $perPage - 1;
$data = $this->getSomeLogsBy($dialogId, $start, $end);
return [
'data' => $data,
'meta' => [
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'last_page' => $pageCount,
],
];
}
public function getOneLogByIndex($dialogId, $index)
{
$key = 'logs_' . $dialogId;
return $this->cache->lindex($key, $index);
}
public function getSomeLogsBy($dialogId, $start, $stop)
{
$key = 'logs_' . $dialogId;
return $this->cache->lrange($key, $start, $stop);
}
public function getAllLogBy($dialogId)
{
$key = 'logs_' . $dialogId;
return $this->cache->lrange($key, 0, -1);
}
public function getLogLen($dialogId)
{
$key = 'logs_' . $dialogId;
return $this->cache->llen($key);
}
public function setLogId($dialogId, $msgId, $index)
{
$key = 'logs_id_' . $dialogId;
$this->cache->hset($key, $msgId, $index);
}
public function getLogId($dialogId, $msgId)
{
$key = 'logs_id_' . $dialogId;
return $this->cache->hget($key, $msgId);
}
public function setMsgGroup($uuid, $toUuid, $data)
{
$key = 'msg_group_' . $uuid;
$this->cache->hset($key, $toUuid, json_encode($data));
}
public function getMsgGroup($uuid)
{
$key = 'msg_group_' . $uuid;
return $this->cache->hgetall($key);
}
public function getOneOfMsgGroup($uuid, $toUuid)
{
$key = 'msg_group_' . $uuid;
return $this->cache->hget($key, $toUuid);
}
public function groupIsExists($uuid, $toUuid)
{
$key = 'msg_group_' . $uuid;
return $this->cache->hexists($key, $toUuid);
}
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結