增加技术端与客户端聊天
This commit is contained in:
238
server/app/api/controller/ChatController.php
Normal file
238
server/app/api/controller/ChatController.php
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\api\controller;
|
||||||
|
|
||||||
|
use app\common\model\chat\ChatConversation;
|
||||||
|
use app\common\model\chat\ChatMessage;
|
||||||
|
use app\common\model\coach\Coach;
|
||||||
|
use app\common\model\user\User;
|
||||||
|
use think\facade\Db;
|
||||||
|
|
||||||
|
class ChatController extends BaseApiController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 获取技师信息
|
||||||
|
*/
|
||||||
|
public function info()
|
||||||
|
{
|
||||||
|
// 获取技师ID
|
||||||
|
$techId = $this->request->param('id/d', 0);
|
||||||
|
|
||||||
|
if (!$techId) {
|
||||||
|
return json([
|
||||||
|
'code' => 400,
|
||||||
|
'msg' => '技师ID不能为空'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 查询技师信息
|
||||||
|
$tech = Coach::find($techId);
|
||||||
|
|
||||||
|
if (!$tech) {
|
||||||
|
return json([
|
||||||
|
'code' => 404,
|
||||||
|
'msg' => '技师不存在'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化返回数据
|
||||||
|
$result = [
|
||||||
|
'id' => $tech['id'],
|
||||||
|
'name' => $tech['name'],
|
||||||
|
'avatar' => $tech['work_photo'],
|
||||||
|
'service_count' => $tech['service_count'],
|
||||||
|
'rating' => $tech['rating'],
|
||||||
|
'skills' => $tech['skills'],
|
||||||
|
'description' => $tech['description'],
|
||||||
|
'status' => $tech['status'],
|
||||||
|
'create_time' => $tech['create_time']
|
||||||
|
];
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => '成功',
|
||||||
|
'data' => $result
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return json([
|
||||||
|
'code' => 500,
|
||||||
|
'msg' => '获取技师信息失败: ' . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取会话ID
|
||||||
|
public function conversation_id()
|
||||||
|
{
|
||||||
|
$techId = $this->request->param('tech_id/d', 0);
|
||||||
|
$userId = $this->userId;
|
||||||
|
|
||||||
|
if (!$techId||!$userId) {
|
||||||
|
return json(['code' => 400, 'msg' => '用户ID和技师ID不能为空']);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 查询会话
|
||||||
|
$conversation = Db::name('chat_conversation')
|
||||||
|
->where('user_id', $userId)
|
||||||
|
->where('tech_id', $techId)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if ($conversation) {
|
||||||
|
return json(['code' => 200, 'data' => ['conversation_id' => $conversation['id']]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新会话
|
||||||
|
$conversationId = Db::name('chat_conversation')
|
||||||
|
->insertGetId([
|
||||||
|
'user_id' => $userId,
|
||||||
|
'tech_id' => $techId,
|
||||||
|
'unread_count' => 0,
|
||||||
|
'update_time' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return json(['code' => 200, 'data' => ['conversation_id' => $conversationId]]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return json(['code' => 500, 'msg' => '获取会话ID失败: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取聊天历史记录
|
||||||
|
*/
|
||||||
|
public function history()
|
||||||
|
{
|
||||||
|
$params = $this->request->param();
|
||||||
|
$conversationId = $params['conversation_id'] ?? 0;
|
||||||
|
$page = $params['page'] ?? 1;
|
||||||
|
$pageSize = $params['page_size'] ?? 20;
|
||||||
|
|
||||||
|
$where = [
|
||||||
|
'conversation_id' => $conversationId
|
||||||
|
];
|
||||||
|
|
||||||
|
$query = ChatMessage::where($where)
|
||||||
|
->order('id', 'asc');
|
||||||
|
|
||||||
|
$list = $query->page($page, $pageSize)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
$total = $query->count();
|
||||||
|
|
||||||
|
foreach ($list as &$item) {
|
||||||
|
if ($item['sender_type']==1){//发送者类型是用户的话 则获取用户信息
|
||||||
|
$item['user'] = User::find($item['sender_id']);
|
||||||
|
}else{
|
||||||
|
$item['user'] = Coach::find($item['sender_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'data' => [
|
||||||
|
'list' => $list,
|
||||||
|
'total' => $total,
|
||||||
|
'has_more' => $total > $page * $pageSize
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
*/
|
||||||
|
public function send()
|
||||||
|
{
|
||||||
|
$params = $this->request->param();
|
||||||
|
|
||||||
|
$techId = $params['tech_id'] ?? 0;
|
||||||
|
$orderId = $params['order_id'] ?? 0;
|
||||||
|
$content = $params['content'] ?? '';
|
||||||
|
|
||||||
|
if (empty($content)) {
|
||||||
|
return json(['code' => 400, 'msg' => '消息内容不能为空']);
|
||||||
|
}
|
||||||
|
// 生成会话ID
|
||||||
|
$conversation = ChatConversation::where(['tech_id'=>$techId,'user_id'=>$this->userId])->find();
|
||||||
|
if (empty($conversation)) {
|
||||||
|
$cccc = new ChatConversation;
|
||||||
|
$conversationId = $cccc->insertGetId(['tech_id'=>$techId,'user_id'=>$this->userId]);
|
||||||
|
}else{
|
||||||
|
$conversationId = $conversation['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建消息
|
||||||
|
$message = new ChatMessage();
|
||||||
|
$message->conversation_id = $conversationId;
|
||||||
|
$message->sender_id = $this->userId;
|
||||||
|
$message->sender_type = ChatMessage::SENDER_USER; // 用户
|
||||||
|
$message->receiver_id = $techId;
|
||||||
|
$message->receiver_type = ChatMessage::SENDER_TECH; // 技师
|
||||||
|
$message->content = $content;
|
||||||
|
$message->message_type = ChatMessage::TYPE_TEXT; // 文本
|
||||||
|
$message->read_status = ChatMessage::UNREAD; // 未读
|
||||||
|
$message->order_id = $orderId;
|
||||||
|
$message->save();
|
||||||
|
|
||||||
|
// TODO: 这里应该通过WebSocket推送给技师
|
||||||
|
|
||||||
|
return json(['code' => 200, 'msg' => '发送成功', 'data' => $message]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记消息为已读
|
||||||
|
*/
|
||||||
|
public function markAsRead()
|
||||||
|
{
|
||||||
|
$params = $this->request->param();
|
||||||
|
|
||||||
|
$conversationId = $params['conversation_id'] ?? '';
|
||||||
|
$userId = $this->request->uid;
|
||||||
|
|
||||||
|
if (empty($conversationId)) {
|
||||||
|
return json(['code' => 400, 'msg' => '会话ID不能为空']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记消息为已读
|
||||||
|
Db::name('chat_message')
|
||||||
|
->where('conversation_id', $conversationId)
|
||||||
|
->where('receiver_id', $userId)
|
||||||
|
->where('read_status', ChatMessage::UNREAD)
|
||||||
|
->update(['read_status' => ChatMessage::READ]);
|
||||||
|
|
||||||
|
return json(['code' => 200, 'msg' => '已标记为已读']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取未读消息数量
|
||||||
|
*/
|
||||||
|
public function unreadCount()
|
||||||
|
{
|
||||||
|
$unreadCount = ChatMessage::where([
|
||||||
|
'conversation_id' => $this->request->uid,
|
||||||
|
'receiver_type' => ChatMessage::SENDER_USER, // 用户
|
||||||
|
'read_status' => ChatMessage::UNREAD
|
||||||
|
])->count();
|
||||||
|
|
||||||
|
return json(['code' => 200, 'data' => ['count' => $unreadCount]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最后一条消息
|
||||||
|
*/
|
||||||
|
public function lastMessage()
|
||||||
|
{
|
||||||
|
|
||||||
|
// 生成会话ID
|
||||||
|
$conversationId = $this->request->param('conversation_id/d', 0);
|
||||||
|
|
||||||
|
$message = ChatMessage::where([
|
||||||
|
'conversation_id' => $conversationId,
|
||||||
|
])
|
||||||
|
->order('id', 'desc')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
return json(['code' => 200, 'data' => $message]);
|
||||||
|
}
|
||||||
|
}
|
||||||
312
server/app/coachapi/controller/ChatController.php
Normal file
312
server/app/coachapi/controller/ChatController.php
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\coachapi\controller;
|
||||||
|
|
||||||
|
use app\common\model\chat\ChatConversation;
|
||||||
|
use app\common\model\chat\ChatMessage;
|
||||||
|
use app\common\model\coach\Coach;
|
||||||
|
use app\common\model\user\User;
|
||||||
|
use think\facade\Db;
|
||||||
|
use think\Request;
|
||||||
|
|
||||||
|
class ChatController extends BaseCoachController
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话信息
|
||||||
|
*/
|
||||||
|
public function conversation_info()
|
||||||
|
{
|
||||||
|
$conversationId = $this->request->param('conversation_id/d', 0);
|
||||||
|
|
||||||
|
if (!$conversationId) {
|
||||||
|
return json(['code' => 400, 'msg' => '会话ID不能为空']);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 查询会话信息
|
||||||
|
$conversation = ChatConversation::find($conversationId);
|
||||||
|
|
||||||
|
if (!$conversation) {
|
||||||
|
return json(['code' => 404, 'msg' => '会话不存在']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户信息
|
||||||
|
$userInfo = User::field('id, nickname, avatar')->find($conversation['user_id']);
|
||||||
|
|
||||||
|
// 查询技师信息
|
||||||
|
$techInfo = Coach::field('id, name as nickname, work_photo as avatar')->find($conversation['tech_id']);
|
||||||
|
|
||||||
|
// 查询最后一条消息
|
||||||
|
$lastMessage = [];
|
||||||
|
if ($conversation['last_msg_id']) {
|
||||||
|
$lastMessage = ChatMessage::field('content, create_time')->find($conversation['last_msg_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'data' => [
|
||||||
|
'id' => $conversation['id'],
|
||||||
|
'user_info' => $userInfo ?: null,
|
||||||
|
'tech_info' => $techInfo ?: null,
|
||||||
|
'last_message' => $lastMessage ?: null,
|
||||||
|
'unread_count' => $conversation['unread_count'],
|
||||||
|
'update_time' => $conversation['update_time']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return json(['code' => 500, 'msg' => '获取会话信息失败: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取聊天历史
|
||||||
|
*/
|
||||||
|
public function history()
|
||||||
|
{
|
||||||
|
$conversationId = $this->request->param('conversation_id/d', 0);
|
||||||
|
$page = $this->request->param('page/d', 1);
|
||||||
|
$pageSize = $this->request->param('page_size/d', 20);
|
||||||
|
|
||||||
|
if (!$conversationId) {
|
||||||
|
return json(['code' => 400, 'msg' => '会话ID不能为空']);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 计算偏移量
|
||||||
|
$offset = ($page - 1) * $pageSize;
|
||||||
|
|
||||||
|
// 查询消息列表
|
||||||
|
$messages = Db::name('chat_message')
|
||||||
|
->where('conversation_id', $conversationId)
|
||||||
|
->order('id', 'asc')
|
||||||
|
->limit($offset, 999999)
|
||||||
|
->select()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
// 查询总数
|
||||||
|
$total = Db::name('chat_message')
|
||||||
|
->where('conversation_id', $conversationId)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'data' => [
|
||||||
|
'list' => $messages,
|
||||||
|
'total' => $total,
|
||||||
|
'has_more' => ($offset + count($messages)) < $total
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return json(['code' => 500, 'msg' => '获取聊天历史失败: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记消息为已读
|
||||||
|
*/
|
||||||
|
public function mark_as_read()
|
||||||
|
{
|
||||||
|
$conversationId = $this->request->param('conversation_id/d', 0);
|
||||||
|
$userId = $this->request->param('user_id/d', 0);
|
||||||
|
|
||||||
|
if (!$conversationId || !$userId) {
|
||||||
|
return json(['code' => 400, 'msg' => '参数不完整']);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 开始事务
|
||||||
|
Db::startTrans();
|
||||||
|
|
||||||
|
// 1. 标记消息为已读
|
||||||
|
Db::name('chat_message')
|
||||||
|
->where('conversation_id', $conversationId)
|
||||||
|
->where('receiver_id', $userId)
|
||||||
|
->where('read_status', 0)
|
||||||
|
->update([
|
||||||
|
'read_status' => 1,
|
||||||
|
'update_time' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. 重置会话未读数
|
||||||
|
Db::name('chat_conversation')
|
||||||
|
->where('id', $conversationId)
|
||||||
|
->update([
|
||||||
|
'unread_count' => 0,
|
||||||
|
'update_time' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
Db::commit();
|
||||||
|
|
||||||
|
return json(['code' => 200, 'msg' => '标记成功']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// 回滚事务
|
||||||
|
Db::rollback();
|
||||||
|
return json(['code' => 500, 'msg' => '标记失败: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话列表
|
||||||
|
* @return \think\Response
|
||||||
|
*/
|
||||||
|
public function conversations(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 获取当前技师ID (从token或session中获取)
|
||||||
|
$techId = $this->getCurrentTechId();
|
||||||
|
|
||||||
|
// 获取请求参数
|
||||||
|
$page = $request->param('page/d', 1);
|
||||||
|
$pageSize = $request->param('page_size/d', 10);
|
||||||
|
|
||||||
|
// 查询会话列表
|
||||||
|
$conversations = ChatConversation::where('tech_id', $techId)
|
||||||
|
->with(['user', 'lastMessage'])
|
||||||
|
->order('update_time', 'desc')
|
||||||
|
->page($page, $pageSize)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
// 格式化返回数据
|
||||||
|
$data = [];
|
||||||
|
foreach ($conversations as $convo) {
|
||||||
|
$data[] = [
|
||||||
|
'id' => $convo->id,
|
||||||
|
'user_id' => $convo->user_id,
|
||||||
|
'user_name' => $convo->user->nickname ?? '',
|
||||||
|
'user_avatar' => $convo->user->avatar ?? '',
|
||||||
|
'last_message' => $convo->lastMessage->content ?? '',
|
||||||
|
'last_message_time' => $convo->lastMessage->create_time ?? '',
|
||||||
|
'unread_count' => $convo->unread_count,
|
||||||
|
'update_time' => $convo->update_time
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => '成功',
|
||||||
|
'data' => $data
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return json([
|
||||||
|
'code' => 500,
|
||||||
|
'msg' => '获取会话列表失败: ' . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前技师ID
|
||||||
|
* @return int
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function getCurrentTechId()
|
||||||
|
{
|
||||||
|
|
||||||
|
return $this->coachInfo['coach_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
*/
|
||||||
|
public function send()
|
||||||
|
{
|
||||||
|
$params = $this->request->param();
|
||||||
|
|
||||||
|
$techId = $params['tech_id'] ?? 0;
|
||||||
|
$orderId = $params['order_id'] ?? 0;
|
||||||
|
$content = $params['content'] ?? '';
|
||||||
|
|
||||||
|
if (empty($content)) {
|
||||||
|
return json(['code' => 400, 'msg' => '消息内容不能为空']);
|
||||||
|
}
|
||||||
|
// 生成会话ID
|
||||||
|
$conversation = ChatConversation::where(['tech_id'=>$techId,'user_id'=>$this->userId])->find();
|
||||||
|
if (empty($conversation)) {
|
||||||
|
$cccc = new ChatConversation;
|
||||||
|
$conversationId = $cccc->insertGetId(['tech_id'=>$techId,'user_id'=>$this->userId]);
|
||||||
|
}else{
|
||||||
|
$conversationId = $conversation['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建消息
|
||||||
|
$message = new ChatMessage();
|
||||||
|
$message->conversation_id = $conversationId;
|
||||||
|
$message->sender_id = $this->userId;
|
||||||
|
$message->sender_type = ChatMessage::SENDER_USER; // 用户
|
||||||
|
$message->receiver_id = $techId;
|
||||||
|
$message->receiver_type = ChatMessage::SENDER_TECH; // 技师
|
||||||
|
$message->content = $content;
|
||||||
|
$message->message_type = ChatMessage::TYPE_TEXT; // 文本
|
||||||
|
$message->read_status = ChatMessage::UNREAD; // 未读
|
||||||
|
$message->order_id = $orderId;
|
||||||
|
$message->save();
|
||||||
|
|
||||||
|
// TODO: 这里应该通过WebSocket推送给技师
|
||||||
|
|
||||||
|
return json(['code' => 200, 'msg' => '发送成功', 'data' => $message]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记消息为已读
|
||||||
|
*/
|
||||||
|
public function markAsRead()
|
||||||
|
{
|
||||||
|
$params = $this->request->param();
|
||||||
|
|
||||||
|
$conversationId = $params['conversation_id'] ?? '';
|
||||||
|
$userId = $this->request->uid;
|
||||||
|
|
||||||
|
if (empty($conversationId)) {
|
||||||
|
return json(['code' => 400, 'msg' => '会话ID不能为空']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记消息为已读
|
||||||
|
Db::name('chat_message')
|
||||||
|
->where('conversation_id', $conversationId)
|
||||||
|
->where('receiver_id', $userId)
|
||||||
|
->where('read_status', ChatMessage::UNREAD)
|
||||||
|
->update(['read_status' => ChatMessage::READ]);
|
||||||
|
|
||||||
|
return json(['code' => 200, 'msg' => '已标记为已读']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取未读消息数量
|
||||||
|
*/
|
||||||
|
public function unreadCount()
|
||||||
|
{
|
||||||
|
$unreadCount = ChatMessage::where([
|
||||||
|
'conversation_id' => $this->request->uid,
|
||||||
|
'receiver_type' => ChatMessage::SENDER_USER, // 用户
|
||||||
|
'read_status' => ChatMessage::UNREAD
|
||||||
|
])->count();
|
||||||
|
|
||||||
|
return json(['code' => 200, 'data' => ['count' => $unreadCount]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最后一条消息
|
||||||
|
*/
|
||||||
|
public function lastMessage()
|
||||||
|
{
|
||||||
|
$params = $this->request->param();
|
||||||
|
|
||||||
|
$techId = $params['tech_id'] ?? 0;
|
||||||
|
$orderId = $params['order_id'] ?? 0;
|
||||||
|
|
||||||
|
// 生成会话ID
|
||||||
|
$conversationId = min($this->request->uid, $techId) . '_' . max($this->request->uid, $techId);
|
||||||
|
|
||||||
|
$message = ChatMessage::where([
|
||||||
|
'conversation_id' => $conversationId,
|
||||||
|
'order_id' => $orderId
|
||||||
|
])
|
||||||
|
->order('id', 'desc')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
return json(['code' => 200, 'data' => $message]);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
server/app/common/model/chat/ChatConversation.php
Normal file
31
server/app/common/model/chat/ChatConversation.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\model\chat;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
class ChatConversation extends Model
|
||||||
|
{
|
||||||
|
|
||||||
|
// 定义时间戳字段
|
||||||
|
protected $autoWriteTimestamp = 'datetime';
|
||||||
|
protected $updateTime = 'update_time';
|
||||||
|
|
||||||
|
// 关联用户
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('app\common\model\user\User', 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关联技师
|
||||||
|
public function tech()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('app\common\model\tech\Tech', 'tech_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关联最后一条消息
|
||||||
|
public function lastMessage()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('app\common\model\chat\ChatMessage', 'last_msg_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
89
server/app/common/model/chat/ChatMessage.php
Normal file
89
server/app/common/model/chat/ChatMessage.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\common\model\chat;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
class ChatMessage extends Model
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
// 定义时间戳字段
|
||||||
|
protected $autoWriteTimestamp = 'datetime';
|
||||||
|
protected $createTime = 'create_time';
|
||||||
|
|
||||||
|
// 消息类型
|
||||||
|
const TYPE_TEXT = 1; // 文本
|
||||||
|
const TYPE_IMAGE = 2; // 图片
|
||||||
|
const TYPE_VOICE = 3; // 语音
|
||||||
|
|
||||||
|
// 发送者类型
|
||||||
|
const SENDER_USER = 1; // 用户
|
||||||
|
const SENDER_TECH = 2; // 技师
|
||||||
|
|
||||||
|
// 读取状态
|
||||||
|
const UNREAD = 0; // 未读
|
||||||
|
const READ = 1; // 已读
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息类型文本
|
||||||
|
* @param $value
|
||||||
|
* @param $data
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getMessageTypeTextAttr($value, $data)
|
||||||
|
{
|
||||||
|
$status = [
|
||||||
|
self::TYPE_TEXT => '文本',
|
||||||
|
self::TYPE_IMAGE => '图片',
|
||||||
|
self::TYPE_VOICE => '语音',
|
||||||
|
];
|
||||||
|
return $status[$data['message_type']] ?? '未知';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取发送者类型文本
|
||||||
|
* @param $value
|
||||||
|
* @param $data
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getSenderTypeTextAttr($value, $data)
|
||||||
|
{
|
||||||
|
$status = [
|
||||||
|
self::SENDER_USER => '用户',
|
||||||
|
self::SENDER_TECH => '技师',
|
||||||
|
];
|
||||||
|
return $status[$data['sender_type']] ?? '未知';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取读取状态文本
|
||||||
|
* @param $value
|
||||||
|
* @param $data
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getReadStatusTextAttr($value, $data)
|
||||||
|
{
|
||||||
|
$status = [
|
||||||
|
self::UNREAD => '未读',
|
||||||
|
self::READ => '已读',
|
||||||
|
];
|
||||||
|
return $status[$data['read_status']] ?? '未知';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联会话
|
||||||
|
*/
|
||||||
|
public function conversation()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('app\common\model\chat\ChatConversation', 'conversation_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联订单
|
||||||
|
*/
|
||||||
|
public function order()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('app\common\model\order\Order', 'order_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
387
server/app/socket/ChatServer.php
Normal file
387
server/app/socket/ChatServer.php
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
<?php
|
||||||
|
// /app/socket/ChatServer.php
|
||||||
|
use Swoole\WebSocket\Server;
|
||||||
|
use Swoole\Table;
|
||||||
|
|
||||||
|
// 设置错误报告
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
// 设置自定义错误处理
|
||||||
|
set_error_handler(function($errno, $errstr, $errfile, $errline) {
|
||||||
|
error_log("PHP错误: [{$errno}] {$errstr} in {$errfile} on line {$errline}");
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置异常处理
|
||||||
|
set_exception_handler(function($e) {
|
||||||
|
error_log("未捕获异常: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine());
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建 WebSocket 服务器
|
||||||
|
$server = new Server("0.0.0.0", 9501);
|
||||||
|
|
||||||
|
// 初始化数据库连接
|
||||||
|
$db = createDbConnection();
|
||||||
|
|
||||||
|
// 创建连接表
|
||||||
|
$table = new Table(1024);
|
||||||
|
$table->column('fd', Table::TYPE_INT);
|
||||||
|
$table->column('uid', Table::TYPE_INT);
|
||||||
|
$table->column('type', Table::TYPE_INT); // 1-用户 2-技师
|
||||||
|
$table->create();
|
||||||
|
|
||||||
|
$server->table = $table;
|
||||||
|
$server->db = $db; // 将数据库连接附加到服务器对象
|
||||||
|
|
||||||
|
// 连接处理
|
||||||
|
$server->on('open', function (Server $server, $request) {
|
||||||
|
$params = getQueryParams($request->server['query_string']);
|
||||||
|
|
||||||
|
// 获取用户类型
|
||||||
|
$userType = $params['type'] ?? 0;
|
||||||
|
|
||||||
|
// 用户验证逻辑
|
||||||
|
if (!$user = verifyToken($server->db, $params['token'] ?? '', $userType)) {
|
||||||
|
$server->close($request->fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保类型参数存在
|
||||||
|
if (!in_array($userType, [1, 2])) {
|
||||||
|
error_log("无效的用户类型: {$userType}");
|
||||||
|
$server->close($request->fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储连接信息
|
||||||
|
$info = [
|
||||||
|
'fd' => $request->fd,
|
||||||
|
'uid' => $user['id'],
|
||||||
|
'type' => $userType
|
||||||
|
];
|
||||||
|
|
||||||
|
$server->table->set($request->fd, $info);
|
||||||
|
|
||||||
|
echo "客户端 {$request->fd} 已连接 (UID: {$user['id']}, 类型: {$userType})\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
// 消息处理
|
||||||
|
$server->on('message', function (Server $server, $frame) {
|
||||||
|
$data = json_decode($frame->data, true);
|
||||||
|
|
||||||
|
// 获取发送者信息
|
||||||
|
$senderInfo = $server->table->get($frame->fd);
|
||||||
|
if (!$senderInfo) {
|
||||||
|
error_log("无法获取发送者信息: FD={$frame->fd}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($data['action']) {
|
||||||
|
case 'send':
|
||||||
|
// 确保所有必要字段都存在
|
||||||
|
if (!isset($data['conversation_id'], $data['content'])) {
|
||||||
|
error_log("消息缺少必要字段: " . json_encode($data));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据会话ID获取会话信息
|
||||||
|
$conversation = getConversationById($server->db, $data['conversation_id']);
|
||||||
|
if (!$conversation) {
|
||||||
|
error_log("会话不存在: conversation_id={$data['conversation_id']}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定接收者信息
|
||||||
|
if ($senderInfo['type'] == 1) { // 发送者是用户
|
||||||
|
$receiverId = $conversation['tech_id'];
|
||||||
|
$receiverType = 2; // 技师
|
||||||
|
} else { // 发送者是技师
|
||||||
|
$receiverId = $conversation['user_id'];
|
||||||
|
$receiverType = 1; // 用户
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 存储到数据库
|
||||||
|
$messageData = [
|
||||||
|
'sender_id' => $senderInfo['uid'],
|
||||||
|
'sender_type' => $senderInfo['type'],
|
||||||
|
'receiver_id' => $receiverId,
|
||||||
|
'receiver_type' => $receiverType,
|
||||||
|
'content' => $data['content'],
|
||||||
|
'order_id' => $data['order_id'] ?? 0,
|
||||||
|
'conversation_id' => $data['conversation_id']
|
||||||
|
];
|
||||||
|
|
||||||
|
$messageId = saveMessage($server->db, $messageData);
|
||||||
|
|
||||||
|
// 2. 构建消息体
|
||||||
|
$message = [
|
||||||
|
'action' => 'new',
|
||||||
|
'data' => [
|
||||||
|
'id' => $messageId,
|
||||||
|
'content' => $data['content'],
|
||||||
|
'sender_type' => $senderInfo['type'],
|
||||||
|
'create_time' => date('Y-m-d H:i:s'),
|
||||||
|
'conversation_id' => $data['conversation_id'],
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// 3. 找到接收者发送消息
|
||||||
|
sendToUser($server, $receiverId, $receiverType, $message);
|
||||||
|
|
||||||
|
// 4. 也发给自己(保证消息同步)
|
||||||
|
$server->push($frame->fd, json_encode($message));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'read':
|
||||||
|
// 确保所有必要字段都存在
|
||||||
|
if (!isset($data['conversation_id'], $data['user_id'])) {
|
||||||
|
error_log("标记已读缺少必要字段: " . json_encode($data));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记消息为已读
|
||||||
|
markMessagesAsRead($server->db, $data['conversation_id'], $data['user_id']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 关闭连接
|
||||||
|
$server->on('close', function ($server, $fd) {
|
||||||
|
$server->table->del($fd);
|
||||||
|
echo "客户端 {$fd} 已断开连接\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析查询字符串
|
||||||
|
*/
|
||||||
|
function getQueryParams($queryString) {
|
||||||
|
$params = [];
|
||||||
|
parse_str($queryString, $params);
|
||||||
|
|
||||||
|
// 过滤特殊字符
|
||||||
|
foreach ($params as $key => $value) {
|
||||||
|
$params[$key] = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建数据库连接
|
||||||
|
*/
|
||||||
|
function createDbConnection() {
|
||||||
|
$host = '127.0.0.1';
|
||||||
|
$dbname = 'anmo';
|
||||||
|
$username = 'anmo';
|
||||||
|
$password = 'fmyrGXBYijbmSMbi';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $username, $password);
|
||||||
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||||
|
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
|
||||||
|
return $db;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("数据库连接失败: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 token
|
||||||
|
*/
|
||||||
|
function verifyToken(PDO $db, $token, $userType) {
|
||||||
|
try {
|
||||||
|
if (empty($token)) {
|
||||||
|
throw new Exception("Token不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据用户类型选择不同的验证逻辑
|
||||||
|
switch ($userType) {
|
||||||
|
case 1: // 普通用户
|
||||||
|
$stmt = $db->prepare("SELECT * FROM ls_user_session WHERE token = :token");
|
||||||
|
$stmt->bindParam(':token', $token);
|
||||||
|
$stmt->execute();
|
||||||
|
$session = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$session) {
|
||||||
|
error_log("用户会话不存在: token={$token}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查会话是否过期
|
||||||
|
if ($session['expire_time'] < time()) {
|
||||||
|
error_log("用户会话已过期: token={$token}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $db->prepare("SELECT * FROM ls_user WHERE id = :user_id");
|
||||||
|
$stmt->bindParam(':user_id', $session['user_id']);
|
||||||
|
$stmt->execute();
|
||||||
|
return $stmt->fetch();
|
||||||
|
|
||||||
|
case 2: // 技师
|
||||||
|
$stmt = $db->prepare("SELECT * FROM ls_coach_user_session WHERE token = :token");
|
||||||
|
$stmt->bindParam(':token', $token);
|
||||||
|
$stmt->execute();
|
||||||
|
$session = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$session) {
|
||||||
|
error_log("技师会话不存在: token={$token}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查会话是否过期
|
||||||
|
if ($session['expire_time'] < time()) {
|
||||||
|
error_log("技师会话已过期: token={$token}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT c.*
|
||||||
|
FROM ls_coach c
|
||||||
|
WHERE c.coach_user_id = :coach_user_id
|
||||||
|
");
|
||||||
|
$stmt->bindParam(':coach_user_id', $session['coach_user_id']);
|
||||||
|
$stmt->execute();
|
||||||
|
return $stmt->fetch();
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Exception("无效的用户类型: {$userType}");
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Token验证错误: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID获取会话信息
|
||||||
|
*/
|
||||||
|
function getConversationById(PDO $db, $conversationId) {
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT *
|
||||||
|
FROM ls_chat_conversation
|
||||||
|
WHERE id = :conversation_id
|
||||||
|
");
|
||||||
|
$stmt->bindParam(':conversation_id', $conversationId);
|
||||||
|
$stmt->execute();
|
||||||
|
$conversation = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$conversation) {
|
||||||
|
error_log("会话不存在: conversation_id={$conversationId}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $conversation;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("获取会话信息失败: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存消息到数据库
|
||||||
|
*/
|
||||||
|
function saveMessage(PDO $db, $data) {
|
||||||
|
try {
|
||||||
|
// 确保所有必要字段都存在
|
||||||
|
if (!isset($data['sender_id'], $data['sender_type'], $data['receiver_id'], $data['receiver_type'], $data['conversation_id'])) {
|
||||||
|
throw new Exception("缺少必要的消息字段");
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO ls_chat_message
|
||||||
|
(conversation_id, sender_id, sender_type, receiver_id, receiver_type, content, message_type, read_status, order_id)
|
||||||
|
VALUES (:conversation_id, :sender_id, :sender_type, :receiver_id, :receiver_type, :content, :message_type, :read_status, :order_id)
|
||||||
|
");
|
||||||
|
|
||||||
|
$messageType = $data['message_type'] ?? 1; // 默认为文本消息
|
||||||
|
$readStatus = 0; // 默认为未读
|
||||||
|
$orderId = $data['order_id'] ?? 0;
|
||||||
|
|
||||||
|
$stmt->bindParam(':conversation_id', $data['conversation_id']);
|
||||||
|
$stmt->bindParam(':sender_id', $data['sender_id']);
|
||||||
|
$stmt->bindParam(':sender_type', $data['sender_type']);
|
||||||
|
$stmt->bindParam(':receiver_id', $data['receiver_id']);
|
||||||
|
$stmt->bindParam(':receiver_type', $data['receiver_type']);
|
||||||
|
$stmt->bindParam(':content', $data['content']);
|
||||||
|
$stmt->bindParam(':message_type', $messageType);
|
||||||
|
$stmt->bindParam(':read_status', $readStatus);
|
||||||
|
$stmt->bindParam(':order_id', $orderId);
|
||||||
|
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return $db->lastInsertId();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("保存消息失败: " . $e->getMessage());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记消息为已读
|
||||||
|
*/
|
||||||
|
function markMessagesAsRead(PDO $db, $conversationId, $userId) {
|
||||||
|
try {
|
||||||
|
// 确保参数有效
|
||||||
|
if (empty($conversationId) || empty($userId)) {
|
||||||
|
throw new Exception("无效的会话ID或用户ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始事务
|
||||||
|
$db->beginTransaction();
|
||||||
|
|
||||||
|
// 1. 标记消息为已读
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
UPDATE ls_chat_message
|
||||||
|
SET read_status = 1
|
||||||
|
WHERE conversation_id = :conversation_id
|
||||||
|
AND receiver_id = :receiver_id
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->bindParam(':conversation_id', $conversationId);
|
||||||
|
$stmt->bindParam(':receiver_id', $userId);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
// 2. 重置会话未读数
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
UPDATE ls_chat_conversation
|
||||||
|
SET unread_count = 0
|
||||||
|
WHERE id = :conversation_id
|
||||||
|
");
|
||||||
|
$stmt->bindParam(':conversation_id', $conversationId);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
$db->commit();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// 回滚事务
|
||||||
|
$db->rollBack();
|
||||||
|
error_log("标记消息已读失败: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向用户发送消息
|
||||||
|
*/
|
||||||
|
function sendToUser($server, $uid, $type, $message) {
|
||||||
|
foreach ($server->table as $row) {
|
||||||
|
if ($row['uid'] == $uid && $row['type'] == $type) {
|
||||||
|
try {
|
||||||
|
$server->push($row['fd'], json_encode($message));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("发送消息失败: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
echo "启动 WebSocket 服务器在 ws://0.0.0.0:9501\n";
|
||||||
|
$server->start();
|
||||||
Reference in New Issue
Block a user