// +---------------------------------------------------------------------- declare (strict_types = 1); namespace addon\wxchat\worker\handle; use addon\wxchat\model\WxchatMessage; use addon\wxchat\model\WxchatUnreadCount; use addon\wxchat\service\SafetyService; use addon\wxchat\worker\enum\MsgFrame; use addon\wxchat\worker\enum\MsgType; use addon\wxchat\worker\Message; use addon\wxchat\worker\MsgReply; use GatewayWorker\Lib\Gateway; /** * 聊天消息处理(实时落库 + 转发 + ACK) * * 协议约定(客户端上行): * { * "header": { "type":"chat", "target":"single"|"group", "timestamp":... }, * "payload": { "uid":123, "receiver":456, "session_id":789, "type":"text"|"image"|"voice"|"video"|"file"|"gift", "content":"...", "ext":{} } * } */ class ChatHandle { // 内容类型字符串 -> 数据库 content_type(int) private const CONTENT_TYPE_MAP = [ 'text' => 0, 'image' => 1, 'voice' => 2, 'video' => 3, 'file' => 4, 'gift' => 5, ]; public static function handle($client_id, $header, $payload) { $uid = Gateway::getUidByClientId($client_id); if (empty($uid)) { MsgReply::sendError($client_id, '未认证,请先登录'); return; } $payloadUid = (int)($payload->uid ?? 0); if ($payloadUid && $payloadUid != $uid) { MsgReply::sendError($client_id, '身份不一致'); return; } $contentType = (string)($payload->type ?? 'text'); if (! isset(self::CONTENT_TYPE_MAP[$contentType])) { MsgReply::sendError($client_id, '不支持的消息类型: ' . $contentType); return; } // 礼物消息:仅做实时通知,落库由 HTTP Gift::send 负责 if ($contentType === 'gift') { self::relayGift($client_id, $uid, $payload); return; } $target = (string)($header->target ?? 'single'); $receiverType = ($target === 'group') ? 1 : 0; $receiverId = (int)($target === 'group' ? ($payload->session_id ?? 0) : ($payload->receiver ?? 0)); if (! $receiverId) { MsgReply::sendError($client_id, '接收者无效'); return; } $content = $payload->content ?? ''; // 文本类消息做敏感词过滤 if ($contentType === 'text') { $content = SafetyService::instance()->filter($content); } $msgId = self::saveMessage($uid, $receiverType, $receiverId, self::CONTENT_TYPE_MAP[$contentType], $content); // 单聊:对方未读 +1 if ($receiverType == 0) { WxchatUnreadCount::increment($receiverId, 0, $uid); } // 构造下发消息并转发 $out = self::buildOutbound($msgId, $uid, $contentType, $content, $payload); if ($receiverType == 0) { if (Gateway::isUidOnline($receiverId)) { Gateway::sendToUid($receiverId, json_encode($out)); } // 离线:消息已落库,接收方下次拉取 history 获取 } else { Gateway::sendToGroup((string)$receiverId, json_encode($out)); } // 发送方回执 MsgReply::send($client_id, MsgFrame::STATUS->value, MsgType::DELIVERED->value, 'delivered', [ 'msg_id' => $msgId, 'receiver' => $receiverId, 'session_id' => $receiverId, ]); } /** * 消息落库(可被 HTTP / WebSocket 共用,返回消息ID) */ public static function saveMessage($senderId, $receiverType, $receiverId, $contentType, $content) { $msg = new WxchatMessage(); $msg->sender_id = $senderId; $msg->receiver_type = $receiverType; $msg->receiver_id = $receiverId; $msg->content = $content; $msg->content_type = $contentType; $msg->save(); return $msg->id; } private static function buildOutbound($msgId, $senderId, $contentType, $content, $payload) { $out = new Message(); $out->header->frame = MsgFrame::CHAT->value; $out->header->type = MsgFrame::CHAT->value; $out->payload->msg_id = $msgId; $out->payload->uid = $senderId; $out->payload->type = $contentType; // 文本类为字符串,媒体类为URL $out->payload->content = $content; $out->payload->receiver = $payload->receiver ?? 0; $out->payload->session_id = $payload->session_id ?? 0; $out->payload->status = 'delivered'; if (! empty($payload->ext)) { $out->payload->ext = $payload->ext; } return $out; } private static function relayGift($client_id, $uid, $payload) { $receiver = (int)($payload->receiver ?? 0); if (! $receiver) { MsgReply::sendError($client_id, '礼物接收者无效'); return; } $out = new Message(); $out->header->frame = MsgFrame::CHAT->value; $out->header->type = 'gift'; $out->payload->uid = $uid; $out->payload->receiver = $receiver; $out->payload->content = $payload->content ?? ''; $out->payload->ext = $payload->ext ?? null; if (Gateway::isUidOnline($receiver)) { Gateway::sendToUid($receiver, json_encode($out)); } MsgReply::send($client_id, MsgFrame::STATUS->value, MsgType::DELIVERED->value, 'delivered', [ 'receiver' => $receiver, ]); } }