chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
/**
|
||||
* GatewayWorker Events 增强版
|
||||
* 功能:登录认证 / 单群聊转发 / 心跳 / 离线消息 / 消息持久化 / ACK / 状态(输入/已读/撤回) / 关系消息
|
||||
* 依赖:GatewayWorker 3.0+
|
||||
*/
|
||||
namespace addon\wxchat\worker;
|
||||
|
||||
use addon\wxchat\worker\enum\MsgFrame;
|
||||
use GatewayWorker\Lib\Gateway;
|
||||
|
||||
/**
|
||||
* ChatEvent 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class ChatEvent
|
||||
{
|
||||
/**
|
||||
* 当 GatewayWorker 业务进程启动时触发(初始化 ThinkPHP 容器)
|
||||
*/
|
||||
public static function onWorkerStart($businessWorker)
|
||||
{
|
||||
self::initApp();
|
||||
}
|
||||
|
||||
/**
|
||||
* 当客户端连接时触发
|
||||
*/
|
||||
public static function onConnect($client_id)
|
||||
{
|
||||
echo "client connected: $client_id\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* 当客户端发来消息时触发
|
||||
*/
|
||||
public static function onMessage($client_id, $msgJson)
|
||||
{
|
||||
self::initApp();
|
||||
echo "Received message: [$client_id] $msgJson \n";
|
||||
|
||||
$message = Message::fromJson($msgJson);
|
||||
$header = $message->header;
|
||||
$payload = $message->payload;
|
||||
|
||||
// 顶层帧类型路由(MsgFrame)
|
||||
switch ($header->type) {
|
||||
case MsgFrame::PING->value:
|
||||
case MsgFrame::PONG->value:
|
||||
handle\Heartbeat::handle($client_id, $header, $payload);
|
||||
break;
|
||||
case MsgFrame::CHAT->value:
|
||||
handle\ChatHandle::handle($client_id, $header, $payload);
|
||||
break;
|
||||
case MsgFrame::SYSTEM->value:
|
||||
handle\SystemHandle::handle($client_id, $header, $payload);
|
||||
break;
|
||||
case MsgFrame::RELATION->value:
|
||||
handle\RelationHandle::handle($client_id, $header, $payload);
|
||||
break;
|
||||
case MsgFrame::STATUS->value:
|
||||
handle\StatusHandle::handle($client_id, $header, $payload);
|
||||
break;
|
||||
default:
|
||||
MsgReply::sendError($client_id, '不支持的消息协议: ' . ($header->type ?? ''));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当用户断开连接时触发:更新在线状态
|
||||
*/
|
||||
public static function onClose($client_id)
|
||||
{
|
||||
echo "onClose:> [$client_id] \n";
|
||||
$uid = Gateway::getUidByClientId($client_id);
|
||||
if ($uid) {
|
||||
try {
|
||||
self::initApp();
|
||||
\ywxapp\model\MemberProfile::where('uid', $uid)
|
||||
->update(['online_status' => 0, 'last_active_at' => time()]);
|
||||
} catch (\Throwable $e) {
|
||||
echo "onClose update failed: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 ThinkPHP 应用(供模型 / Db 门面使用)
|
||||
*/
|
||||
public static function initApp()
|
||||
{
|
||||
static $app = null;
|
||||
if ($app === null) {
|
||||
$app = new \think\App();
|
||||
$app->initialize();
|
||||
}
|
||||
return $app;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace addon\wxchat\worker;
|
||||
|
||||
/**
|
||||
* 消息实体类
|
||||
*/
|
||||
class Message implements \JsonSerializable
|
||||
{
|
||||
private array $fields = ['header', 'payload'];
|
||||
private Header $header; // 消息扩展信息
|
||||
private Payload $payload; // 消息扩展信息
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->header = new Header();
|
||||
$this->payload = new Payload();
|
||||
}
|
||||
|
||||
|
||||
public function __set($name, $value)
|
||||
{
|
||||
$this->$name = $value;
|
||||
}
|
||||
|
||||
|
||||
public function __get($name)
|
||||
{
|
||||
if (isset($this->$name)) {
|
||||
return $this->$name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$data = [];
|
||||
foreach ($this->fields as $key) {
|
||||
if (isset($this->$key) && $this->$key !== null) {
|
||||
$data[$key] = $this->$key;
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
// 反序列化:JSON → Message
|
||||
|
||||
public static function fromJson(string $json): self
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
if (! is_array($data)) {
|
||||
throw new \InvalidArgumentException('Invalid JSON structure');
|
||||
}
|
||||
return self::fromArray($data);
|
||||
}
|
||||
|
||||
// 反序列化:数组 → Message
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$message = new self();
|
||||
foreach ($message->fields as $key) {
|
||||
if (! isset($data[$key]) || empty($data[$key])) {
|
||||
continue;
|
||||
}
|
||||
if ($key == 'header') {
|
||||
$message->header = Header::fromArray($data['header']);
|
||||
} else if ($key == 'payload') {
|
||||
$message->payload = Payload::fromArray($data['payload']);
|
||||
} else {
|
||||
$message->$key = $data[$key];
|
||||
}
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息头信息
|
||||
*/
|
||||
class Header implements \JsonSerializable
|
||||
{
|
||||
protected array $fields = ['frame', 'id', 'type', 'target', 'seqno', 'timestamp', 'source', 'version', 'token', 'platform', 'device_id', 'network_type'];
|
||||
private string $id; // 消息唯一ID(雪花算法生成,必填)
|
||||
private string $type = ''; // 消息类型(必填)
|
||||
private string $target = 'system'; // 目标会话类型(single/group/room/system,选填)
|
||||
private int $seqno = 0; // 消息序列号(同一会话内递增,或全局唯一)
|
||||
private int $timestamp; // 消息生成时间(Unix时间戳,毫秒级)
|
||||
private string $source = 'server'; // 消息来源(user_id或system,选填)
|
||||
private string $version = '1.0.0'; // 协议版本(默认1.0.0,选填)
|
||||
private string $token; // 认证Token(选填,连接控制帧可选)
|
||||
private string $platform; // 平台类型
|
||||
private string $device_id; // 设备ID
|
||||
private string $network_type; // 网络类型
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = 'msg_' . substr(md5(uniqid((string) mt_rand(), true)), 0, 16);
|
||||
$this->timestamp = time() * 1000; // 毫秒级时间戳
|
||||
}
|
||||
|
||||
//__set()方法用来设置私有属性
|
||||
|
||||
public function __set($name, $value)
|
||||
{
|
||||
$this->$name = $value;
|
||||
}
|
||||
|
||||
//__get()方法用来获取私有属性
|
||||
|
||||
public function __get($name)
|
||||
{
|
||||
return $this->$name;
|
||||
}
|
||||
|
||||
// ✅ 严格实现接口方法
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$data = [];
|
||||
foreach ($this->fields as $key) {
|
||||
if (isset($this->$key) && $this->$key !== null) {
|
||||
$data[$key] = $this->$key;
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
// 仅输出非 null 值(符合“可选字段”语义)
|
||||
$data = [];
|
||||
foreach ($this->fields as $key) {
|
||||
if (isset($this->$key) && $value !== null) {
|
||||
$data[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
// 反序列化:JSON → Message
|
||||
|
||||
public static function fromJson(string $json): self
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
if (! is_array($data)) {
|
||||
throw new \InvalidArgumentException('Invalid JSON structure');
|
||||
}
|
||||
return self::fromArray($data);
|
||||
}
|
||||
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$header = new self();
|
||||
foreach ($header->fields as $key) {
|
||||
if (isset($data[$key]) && ! empty($data[$key])) {
|
||||
$header->$key = $data[$key];
|
||||
}
|
||||
}
|
||||
return $header;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息扩展信息
|
||||
*/
|
||||
class Payload implements \JsonSerializable
|
||||
{
|
||||
|
||||
private array $fields = ['uid', 'nickname', 'avatar', 'session_id', 'receiver', 'content', 'type', 'status', 'msg_id', 'at_user', 'ext'];
|
||||
private int $uid = 0; // 用户ID(发送者)
|
||||
private string $nickname; // 昵称(可选,用于显示)
|
||||
private string $avatar; // 头像URL(可选,用于显示)
|
||||
private string $session_id; // 会话ID(单聊为对方UID,群聊为group_id,系统消息可选)
|
||||
private int $receiver; // 接收用户UID(单聊必填,群聊可选)
|
||||
private mixed $content; // 消息内容(文本直接字符串,其他类型可为URL或结构化数据)
|
||||
private string $type; // 消息类型(text/image/voice/video/file/system/notification/typing/read/revoke)
|
||||
private string $status = 'delivered'; // 消息状态(sending/sent/delivered/read/failed)
|
||||
private string $msg_id; // 消息ID(可选,服务端生成或客户端指定)
|
||||
private array $at_user; // @了哪些人(可选,群聊中使用)
|
||||
private mixed $ext; // 扩展字段(可选,根据业务需求定义)
|
||||
|
||||
//__set()方法用来设置私有属性
|
||||
|
||||
public function __set($name, $value)
|
||||
{
|
||||
$this->$name = $value;
|
||||
}
|
||||
|
||||
//__get()方法用来获取私有属性
|
||||
|
||||
public function __get($name)
|
||||
{
|
||||
return $this->$name;
|
||||
}
|
||||
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$data = [];
|
||||
foreach ($this->fields as $key) {
|
||||
if (isset($this->$key) && $this->$key !== null) {
|
||||
$data[$key] = $this->$key;
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
// 仅输出非 null 值(符合“可选字段”语义)
|
||||
$data = [];
|
||||
foreach ($this->fields as $key) {
|
||||
$value = $this->$key ?? null;
|
||||
if (isset($this->$key) && $value !== null) {
|
||||
$data[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
public static function fromJson(string $json): self
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
if (! is_array($data)) {
|
||||
throw new \InvalidArgumentException('Invalid JSON structure');
|
||||
}
|
||||
return self::fromArray($data);
|
||||
}
|
||||
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$payload = new self();
|
||||
foreach ($payload->fields as $key) {
|
||||
if (isset($data[$key]) && ! empty($data[$key])) {
|
||||
$payload->$key = $data[$key];
|
||||
}
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
// 其他属性的 getter/setter 按相同模式补充...
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace addon\wxchat\worker;
|
||||
|
||||
use addon\wxchat\worker\Message;
|
||||
use GatewayWorker\Lib\Gateway;
|
||||
use addon\wxchat\worker\enum\MsgFrame;
|
||||
use addon\wxchat\worker\enum\MsgType;
|
||||
|
||||
/**
|
||||
* 统一的消息下发工具
|
||||
*/
|
||||
class MsgReply
|
||||
{
|
||||
/**
|
||||
* 发送错误消息给指定客户端
|
||||
*/
|
||||
public static function sendError($client_id, $payload)
|
||||
{
|
||||
$message = new Message();
|
||||
$message->header->frame = MsgFrame::ERROR->value;
|
||||
$message->header->type = MsgType::ERROR->value;
|
||||
$message->payload->content = $payload;
|
||||
Gateway::sendToClient($client_id, json_encode($message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送一条通用消息给指定客户端
|
||||
* @param string $frame 帧类型(MsgFrame 值)
|
||||
* @param string $type 消息类型(MsgType 值)
|
||||
*/
|
||||
public static function send($client_id, $frame, $type, $content, $extra = [])
|
||||
{
|
||||
$message = new Message();
|
||||
$message->header->frame = $frame;
|
||||
$message->header->type = $type;
|
||||
$message->payload->content = $content;
|
||||
foreach ($extra as $k => $v) {
|
||||
$message->payload->$k = $v;
|
||||
}
|
||||
Gateway::sendToClient($client_id, json_encode($message));
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送给某用户(若在线)
|
||||
*/
|
||||
public static function sendToUid($uid, $frame, $type, $content, $extra = [])
|
||||
{
|
||||
if (! Gateway::isUidOnline($uid)) {
|
||||
return false;
|
||||
}
|
||||
$message = new Message();
|
||||
$message->header->frame = $frame;
|
||||
$message->header->type = $type;
|
||||
$message->payload->content = $content;
|
||||
foreach ($extra as $k => $v) {
|
||||
$message->payload->$k = $v;
|
||||
}
|
||||
Gateway::sendToUid($uid, json_encode($message));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace addon\wxchat\worker\enum;
|
||||
|
||||
// ==================== 第一层:协议帧类型(传输层) ====================
|
||||
enum MsgFrame: string {
|
||||
|
||||
/**
|
||||
* 聊天内容 文本 图片 视频 语音 文件
|
||||
*/
|
||||
case PING = 'ping'; // 心跳帧(客户端定时发送,服务端回复PONG)
|
||||
case PONG = 'pong';
|
||||
/**
|
||||
* 聊天内容 文本 图片 视频 语音 文件
|
||||
*/
|
||||
case CHAT = 'chat';
|
||||
/**
|
||||
* 系统消息 心跳 认证 错误
|
||||
*/
|
||||
case SYSTEM = 'sys';
|
||||
/**
|
||||
* 关系消息 加好友 同意加好友 拒绝加好友 加群组 同意加入 拒绝加入
|
||||
*/
|
||||
case RELATION = 'rel';
|
||||
|
||||
/**
|
||||
* 状态消息 正在输入 送达 未读 已读 撤回
|
||||
*/
|
||||
case STATUS = 'state'; // 连接控制(心跳/认证/状态)
|
||||
|
||||
case ERROR = 'err'; // 协议错误(独立错误帧)
|
||||
}
|
||||
|
||||
// [
|
||||
// 'id' => 'msg_1234567890abcdef', // 唯一ID(UUID / 雪花ID,服务端可生成)
|
||||
// 'from' => 'user_001', // 消息发送者 UID
|
||||
// 'to' => 'user_002', // 接收者 UID(单聊) 或 to_gid: "group_001"(群聊)
|
||||
// 'totype' => 'member', // 接收目标类型:user(私聊) / group(群聊)
|
||||
// 'type' => 'text', // 消息类型:text / image / voice / video / file / system / notification / typing / read / revoke
|
||||
// 'action' => 'chat.send', // 【可选】业务动作,用于服务端路由(如 chat.send, system.notify, typing.start)
|
||||
// 'content' => 'Hello world!', // 消息正文内容(文本消息直接是字符串,其他类型可能是 URL 或结构化数据)
|
||||
// 'extra' => [ // 【可选】扩展字段,根据 type 不同而不同
|
||||
// 'image_url' => 'https://xxx.com/img.jpg',
|
||||
// 'image_width' => 800,
|
||||
// 'image_height' => 600,
|
||||
// 'voice_url' => 'https://xxx.com/voice.amr',
|
||||
// 'voice_duration' => 3,
|
||||
// 'file_name' => 'doc.pdf',
|
||||
// 'file_size' => 102400,
|
||||
// 'reply_to_msg_id' => 'msg_0987654321', // 引用回复的消息ID
|
||||
// 'mention_uids' => ['user_003', 'user_004'] // @了哪些人
|
||||
// ],
|
||||
// 'timestamp' => 1700000000123, // 消息发送时间(毫秒级 UNIX 时间戳,推荐)
|
||||
// 'status' => 'sent', // 消息状态:sending / sent / delivered / read / failed
|
||||
// 'encipher' => true, // 是否经过 AES 加密(可选字段,用于标识)
|
||||
// 'revoked' => false // 是否被撤回
|
||||
// ];
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace addon\wxchat\worker\enum;
|
||||
|
||||
// ==================== 第三层:具体消息子类型(MsgSubType) ====================
|
||||
enum MsgType: string {
|
||||
|
||||
// -------------------- CHAT 类消息 --------------------
|
||||
case TEXT = 'text';
|
||||
case IMAGE = 'image';
|
||||
case VOICE = 'voice';
|
||||
case VIDEO = 'video'; // 修正大小写
|
||||
case FILE = 'file';
|
||||
|
||||
// -------------------- RELATION 类消息 --------------------
|
||||
case FRIEND_REQUEST = 'friend_request'; // 好友申请
|
||||
case FRIEND_AGREE = 'friend_agree'; // 同意添加
|
||||
case FRIEND_REFUSE = 'friend_refuse'; // 拒绝添加
|
||||
case GROUP_INVITE = 'group_invite'; // 群邀请
|
||||
case GROUP_JOIN = 'group_join'; // 主动加群
|
||||
|
||||
// -------------------- SYSTEM 系统类 类专属 --------------------
|
||||
case HEARTBEAT = 'heartbeat';
|
||||
case AUTH = 'auth';
|
||||
case Notice = 'notice';
|
||||
case ERROR = 'error';
|
||||
|
||||
// -------------------- STATUS 状态 类专属 --------------------
|
||||
case TYPING = 'typing'; // 正在输入(实时状态,不存库)
|
||||
case DELIVERED = 'delivered'; // 送达回执(服务端→发送方)
|
||||
case RECEIVED = 'received'; // 接收回执(接收方→服务端)
|
||||
case READ = 'read';
|
||||
case REVOKED = 'revoked';
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace addon\wxchat\worker\enum;
|
||||
|
||||
// ==================== 聊天目标 ====================
|
||||
enum Target: string {
|
||||
case USER = 'single';
|
||||
case GROUP = 'group';
|
||||
case ROOM = 'room';
|
||||
case SYSTEM = 'system';
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare (strict_types = 1);
|
||||
namespace addon\wxchat\worker\handle;
|
||||
|
||||
use addon\wxchat\worker\enum\MsgType;
|
||||
|
||||
/**
|
||||
* ControlHandle 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class ControlHandle
|
||||
{
|
||||
|
||||
public static function handle($client_id, $header, $payload)
|
||||
{
|
||||
switch ($header->type) {
|
||||
case MsgType::HEARTBEAT->value:
|
||||
self::Heartbeat($client_id, $header, $payload);
|
||||
break;
|
||||
case MsgType::AUTH->value:
|
||||
self::Auth($client_id, $header, $payload);
|
||||
break;
|
||||
case MsgType::NOTICE->value:
|
||||
self::Notification($client_id, $header, $payload);
|
||||
break;
|
||||
default:
|
||||
ErrorHandle::sendErrorMsg($client_id, '不支持的消息协议! ');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 心跳响应
|
||||
*/
|
||||
public static function heartbeat($client_id, $packetId)
|
||||
{
|
||||
Error::sendErrorMsg($client_id, '不支持的消息类型! ');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace addon\wxchat\worker\handle;
|
||||
|
||||
use addon\wxchat\worker\MsgReply;
|
||||
|
||||
/**
|
||||
* 错误发送包装(兼容 ControlHandle / SystemHandle 的原有引用)
|
||||
*/
|
||||
class ErrorHandle
|
||||
{
|
||||
|
||||
public static function sendErrorMsg($client_id, $msg)
|
||||
{
|
||||
MsgReply::sendError($client_id, $msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
namespace addon\wxchat\worker\handle;
|
||||
|
||||
use addon\wxchat\worker\enum\MsgFrame;
|
||||
use addon\wxchat\worker\Message;
|
||||
use GatewayWorker\Lib\Gateway;
|
||||
|
||||
/**
|
||||
* Heartbeat 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Heartbeat
|
||||
{
|
||||
|
||||
public static function handle($client_id, $header, $payload)
|
||||
{
|
||||
if ($header->type === MsgFrame::PONG->value) {
|
||||
return;
|
||||
}
|
||||
$newMessage = new Message();
|
||||
$newMessage->header->type = MsgFrame::PONG->value;
|
||||
echo "Received PING newMessage: " . json_encode($newMessage) . ", responding with PONG\n";
|
||||
Gateway::sendToClient($client_id, json_encode($newMessage));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace addon\wxchat\wprker\handle;
|
||||
|
||||
use GatewayWorker\Lib\Gateway;
|
||||
|
||||
/**
|
||||
* MessageHandle 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class MessageHandle
|
||||
{
|
||||
|
||||
protected static $sensitiveWords = ['fuck', 'shit', '赌博', '色情'];
|
||||
/**
|
||||
* 敏感词过滤
|
||||
*/
|
||||
protected static function filterSensitiveWords($text)
|
||||
{
|
||||
foreach (self::$sensitiveWords as $word) {
|
||||
$text = str_replace($word, '**', $text);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理普通消息(文本/图片/文件)
|
||||
*/
|
||||
protected static function handle($client_id, $message)
|
||||
{
|
||||
$header = $message->header;
|
||||
$payload = $message->payload;
|
||||
|
||||
$fromUid = $header->from ?? '';
|
||||
$uid = Gateway::getUidByClientId($client_id);
|
||||
echo "Received message from UID: $fromUid, ClientID: $client_id , $uid\n";
|
||||
if (! $uid || $uid != $fromUid) {
|
||||
self::sendError($client_id, '未认证用户,无法发送消息');
|
||||
return;
|
||||
}
|
||||
if ($header->type == 'text' && ! empty($payload->content)) {
|
||||
$filteredContent = self::filterSensitiveWords($payload->content);
|
||||
if ($filteredContent !== $payload->content) {
|
||||
// file_put_contents('/tmp/sensitive_log.log', date('Y-m-d H:i:s') . " {$fromUid} 发送敏感词: {$data['payload']['content']}\n", FILE_APPEND);
|
||||
$message->payload->content = $filteredContent;
|
||||
}
|
||||
}
|
||||
// 消息持久化(重要!写入数据库)
|
||||
// self::saveMessageToDB($data);
|
||||
// 转发消息
|
||||
if ($header->subtype === 'single') {
|
||||
// 单聊:发送给目标用户
|
||||
if (Gateway::isUidOnline($header->to)) {
|
||||
|
||||
Gateway::sendToUid($header->to, json_encode($message));
|
||||
// 发送ACK给发送方(表示已送达)
|
||||
$ackMsg = new Message();
|
||||
$ackMsg->header->type = MessageType::System;
|
||||
$ackMsg->header->subtype = MessageType::Delivered;
|
||||
$ackMsg->payload->content = '不支持的消息类型! ';
|
||||
Gateway::sendToUid($header->from, json_encode($message));
|
||||
} else {
|
||||
// 对方不在线,存离线消息(写入Redis/DB)
|
||||
// self::saveOfflineMessage($toId, $data);
|
||||
|
||||
$newMessage = new Message();
|
||||
$newMessage->header->type = MessageType::Error;
|
||||
$newMessage->payload->content = "用户 {$toId} 不在线,消息已存为离线";
|
||||
Gateway::sendToClient($client_id, json_encode($message));
|
||||
}
|
||||
} elseif ($header->subtype === 'group') {
|
||||
// 群聊:发送给群内所有成员(排除自己)
|
||||
// TODO: 从数据库获取群成员列表
|
||||
// $members = self::getGroupMembers($toId);
|
||||
// foreach ($members as $memberUid) {
|
||||
// if ($memberUid != $fromUid && Gateway::hasUid($memberUid)) {
|
||||
// Gateway::sendToUid($memberUid, json_encode($data));
|
||||
// }
|
||||
// }
|
||||
Gateway::sendToGroup($header->to, json_encode($message));
|
||||
|
||||
$ackMsg = new Message();
|
||||
$ackMsg->header->type = MessageType::System;
|
||||
$ackMsg->header->subtype = MessageType::Delivered;
|
||||
$ackMsg->payload->content = '不支持的消息类型! ';
|
||||
Gateway::sendToUid($header->from, json_encode($message));
|
||||
} else {
|
||||
$errorMsg = new Message();
|
||||
$errorMsg->header->type = MessageType::Error;
|
||||
$errorMsg->payload->content = "未知会话类型";
|
||||
Gateway::sendToClient($client_id, json_encode($errorMsg));
|
||||
return;
|
||||
}
|
||||
// 回复发送方:消息已接收(前端更新状态用)
|
||||
$receivedMsg = new Message();
|
||||
$receivedMsg->header->type = MessageType::Error;
|
||||
Gateway::sendToClient($client_id, json_encode($receivedMsg));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace addon\wxchat\worker\handle;
|
||||
|
||||
use addon\wxchat\model\WxchatFriend;
|
||||
use addon\wxchat\model\WxchatGroup;
|
||||
use addon\wxchat\model\WxchatGroupMember;
|
||||
use addon\wxchat\worker\enum\MsgFrame;
|
||||
use addon\wxchat\worker\enum\MsgType;
|
||||
use addon\wxchat\worker\MsgReply;
|
||||
use GatewayWorker\Lib\Gateway;
|
||||
use ywxapp\model\MemberUser;
|
||||
|
||||
/**
|
||||
* 关系消息处理(加好友 / 同意 / 拒绝 / 加群 / 邀请)
|
||||
* 真实落库:wxapp_wxchat_friend / wxapp_wxchat_group_members / wxapp_wxchat_groups
|
||||
*
|
||||
* 协议约定(客户端上行):
|
||||
* {
|
||||
* "header": { "type":"rel", "timestamp":... },
|
||||
* "payload": { "type":"friend_request"|"friend_agree"|"friend_refuse"|"group_join"|"group_invite",
|
||||
* "receiver":<对方UID>, "session_id":<群ID>, "remark":"备注/招呼语" }
|
||||
* }
|
||||
*/
|
||||
class RelationHandle
|
||||
{
|
||||
|
||||
public static function handle($client_id, $header, $payload)
|
||||
{
|
||||
$uid = Gateway::getUidByClientId($client_id);
|
||||
if (empty($uid)) {
|
||||
MsgReply::sendError($client_id, '未认证,请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
// 关系子类型:优先 payload.type(与 ChatHandle 一致),兼容 header.type
|
||||
$subType = (string) ($payload->type ?? ($header->type ?? ''));
|
||||
$peer = (int) ($payload->receiver ?? 0);
|
||||
$groupId = (int) ($payload->session_id ?? ($payload->group_id ?? 0));
|
||||
$remark = trim((string) ($payload->remark ?? ($payload->content ?? '')));
|
||||
|
||||
switch ($subType) {
|
||||
case MsgType::FRIEND_REQUEST->value:
|
||||
self::friendRequest($client_id, $uid, $peer, $remark);
|
||||
break;
|
||||
case MsgType::FRIEND_AGREE->value:
|
||||
self::friendAgree($client_id, $uid, $peer);
|
||||
break;
|
||||
case MsgType::FRIEND_REFUSE->value:
|
||||
self::friendRefuse($client_id, $uid, $peer);
|
||||
break;
|
||||
case MsgType::GROUP_JOIN->value:
|
||||
self::groupJoin($client_id, $uid, $groupId);
|
||||
break;
|
||||
case MsgType::GROUP_INVITE->value:
|
||||
self::groupInvite($client_id, $uid, $groupId, $peer);
|
||||
break;
|
||||
default:
|
||||
MsgReply::sendError($client_id, '不支持的关系消息类型: ' . $subType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 好友申请:写入待处理好友记录(status=0),并实时通知对方
|
||||
*/
|
||||
private static function friendRequest($client_id, $uid, $peer, $remark)
|
||||
{
|
||||
if (! $peer) {
|
||||
MsgReply::sendError($client_id, '对方 UID 无效');
|
||||
return;
|
||||
}
|
||||
if ($peer == $uid) {
|
||||
MsgReply::sendError($client_id, '不能添加自己为好友');
|
||||
return;
|
||||
}
|
||||
// 已是好友(双向任一存在 status=1)
|
||||
if (self::isFriend($uid, $peer)) {
|
||||
MsgReply::sendError($client_id, '你们已是好友');
|
||||
return;
|
||||
}
|
||||
// 去重:已存在待处理请求则不重复写入
|
||||
$exist = WxchatFriend::where('uid', $uid)->where('fid', $peer)->where('status', 0)->find();
|
||||
if (! $exist) {
|
||||
$fr = new WxchatFriend();
|
||||
$fr->uid = $uid;
|
||||
$fr->fid = $peer;
|
||||
$fr->remark = $remark;
|
||||
$fr->status = 0; // 0-请求中
|
||||
$fr->create_at = time();
|
||||
$fr->save();
|
||||
}
|
||||
|
||||
$myName = self::nickname($uid);
|
||||
// 实时通知对方
|
||||
MsgReply::sendToUid($peer, MsgFrame::RELATION->value, MsgType::FRIEND_REQUEST->value, '收到好友申请', [
|
||||
'from' => $uid,
|
||||
'from_name' => $myName,
|
||||
'remark' => $remark,
|
||||
]);
|
||||
// 回执发送方
|
||||
MsgReply::send($client_id, MsgFrame::RELATION->value, MsgType::FRIEND_REQUEST->value, '已发送好友申请', [
|
||||
'to' => $peer,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同意申请:$uid 同意 $peer 发来的请求,建立双向好友
|
||||
*/
|
||||
private static function friendAgree($client_id, $uid, $peer)
|
||||
{
|
||||
if (! $peer) {
|
||||
MsgReply::sendError($client_id, '对方 UID 无效');
|
||||
return;
|
||||
}
|
||||
$req = WxchatFriend::where('uid', $peer)->where('fid', $uid)->where('status', 0)->find();
|
||||
if (! $req) {
|
||||
MsgReply::sendError($client_id, '无待处理的好友申请');
|
||||
return;
|
||||
}
|
||||
$req->status = 1; // 1-已通过
|
||||
$req->update_at = time();
|
||||
$req->save();
|
||||
|
||||
// 写入反向好友记录(保证双向查询一致)
|
||||
if (! self::isFriend($uid, $peer)) {
|
||||
$reverse = new WxchatFriend();
|
||||
$reverse->uid = $uid;
|
||||
$reverse->fid = $peer;
|
||||
$reverse->status = 1;
|
||||
$reverse->create_at = time();
|
||||
$reverse->save();
|
||||
}
|
||||
|
||||
// 通知申请方
|
||||
MsgReply::sendToUid($peer, MsgFrame::RELATION->value, MsgType::FRIEND_AGREE->value, '对方已通过你的好友申请', [
|
||||
'from' => $uid,
|
||||
'from_name' => self::nickname($uid),
|
||||
]);
|
||||
MsgReply::send($client_id, MsgFrame::RELATION->value, MsgType::FRIEND_AGREE->value, '已同意', [
|
||||
'to' => $peer,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝申请
|
||||
*/
|
||||
private static function friendRefuse($client_id, $uid, $peer)
|
||||
{
|
||||
if (! $peer) {
|
||||
MsgReply::sendError($client_id, '对方 UID 无效');
|
||||
return;
|
||||
}
|
||||
$req = WxchatFriend::where('uid', $peer)->where('fid', $uid)->where('status', 0)->find();
|
||||
if (! $req) {
|
||||
MsgReply::sendError($client_id, '无待处理的好友申请');
|
||||
return;
|
||||
}
|
||||
$req->status = 2; // 2-已拒绝
|
||||
$req->update_at = time();
|
||||
$req->save();
|
||||
|
||||
MsgReply::sendToUid($peer, MsgFrame::RELATION->value, MsgType::FRIEND_REFUSE->value, '对方拒绝了你的好友申请', [
|
||||
'from' => $uid,
|
||||
]);
|
||||
MsgReply::send($client_id, MsgFrame::RELATION->value, MsgType::FRIEND_REFUSE->value, '已拒绝', [
|
||||
'to' => $peer,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动加群:加入 group_members
|
||||
*/
|
||||
private static function groupJoin($client_id, $uid, $groupId)
|
||||
{
|
||||
if (! $groupId) {
|
||||
MsgReply::sendError($client_id, '群组 ID 无效');
|
||||
return;
|
||||
}
|
||||
$group = WxchatGroup::find($groupId);
|
||||
if (! $group) {
|
||||
MsgReply::sendError($client_id, '群组不存在');
|
||||
return;
|
||||
}
|
||||
if (WxchatGroupMember::where('gid', $groupId)->where('uid', $uid)->find()) {
|
||||
MsgReply::sendError($client_id, '你已在群内');
|
||||
return;
|
||||
}
|
||||
self::addGroupMember($groupId, $uid, 0);
|
||||
MsgReply::send($client_id, MsgFrame::RELATION->value, MsgType::GROUP_JOIN->value, '已加入群组', [
|
||||
'group_id' => $groupId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请加群:$uid 邀请 $peer 加入群组
|
||||
*/
|
||||
private static function groupInvite($client_id, $uid, $groupId, $peer)
|
||||
{
|
||||
if (! $groupId || ! $peer) {
|
||||
MsgReply::sendError($client_id, '群组或受邀人无效');
|
||||
return;
|
||||
}
|
||||
$group = WxchatGroup::find($groupId);
|
||||
if (! $group) {
|
||||
MsgReply::sendError($client_id, '群组不存在');
|
||||
return;
|
||||
}
|
||||
if (! WxchatGroupMember::where('gid', $groupId)->where('uid', $peer)->find()) {
|
||||
self::addGroupMember($groupId, $peer, 0);
|
||||
}
|
||||
MsgReply::sendToUid($peer, MsgFrame::RELATION->value, MsgType::GROUP_INVITE->value, '你被邀请加入群组', [
|
||||
'group_id' => $groupId,
|
||||
'from' => $uid,
|
||||
'from_name' => self::nickname($uid),
|
||||
]);
|
||||
MsgReply::send($client_id, MsgFrame::RELATION->value, MsgType::GROUP_INVITE->value, '邀请已发送', [
|
||||
'group_id' => $groupId,
|
||||
'to' => $peer,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入群成员并自增群人数
|
||||
*/
|
||||
private static function addGroupMember($groupId, $uid, $role)
|
||||
{
|
||||
$m = new WxchatGroupMember();
|
||||
$m->gid = $groupId;
|
||||
$m->uid = $uid;
|
||||
$m->role = $role;
|
||||
$m->join_time = date('Y-m-d H:i:s');
|
||||
$m->save();
|
||||
WxchatGroup::where('id', $groupId)->inc('member_count')->update();
|
||||
}
|
||||
|
||||
|
||||
private static function isFriend($uid, $fid): bool
|
||||
{
|
||||
return (bool) WxchatFriend::where('uid', $uid)
|
||||
->where('fid', $fid)
|
||||
->where('status', 1)
|
||||
->find();
|
||||
}
|
||||
|
||||
|
||||
private static function nickname($uid): string
|
||||
{
|
||||
try {
|
||||
$name = MemberUser::where('uid', $uid)->value('nickname');
|
||||
return $name ?: '';
|
||||
} catch (\Throwable $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace addon\wxchat\worker\handle;
|
||||
|
||||
use addon\wxchat\model\WxchatMessage;
|
||||
use addon\wxchat\model\WxchatUnreadCount;
|
||||
use addon\wxchat\worker\enum\MsgFrame;
|
||||
use addon\wxchat\worker\enum\MsgType;
|
||||
use addon\wxchat\worker\MsgReply;
|
||||
use GatewayWorker\Lib\Gateway;
|
||||
|
||||
/**
|
||||
* 状态消息处理:正在输入 / 送达 / 已读 / 撤回
|
||||
* typing 仅实时转发不落库;read 清零未读;revoked 标记撤回
|
||||
*/
|
||||
class StatusHandle
|
||||
{
|
||||
|
||||
public static function handle($client_id, $header, $payload)
|
||||
{
|
||||
$uid = Gateway::getUidByClientId($client_id);
|
||||
if (empty($uid)) {
|
||||
MsgReply::sendError($client_id, '未认证,请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($header->type) {
|
||||
case MsgType::TYPING->value:
|
||||
// 实时转发给对方,不落库
|
||||
$to = (int)($payload->receiver ?? 0);
|
||||
if ($to && Gateway::isUidOnline($to)) {
|
||||
MsgReply::sendToUid($to, MsgFrame::STATUS->value, MsgType::TYPING->value, 'typing', [
|
||||
'from' => $uid,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
||||
case MsgType::READ->value:
|
||||
// 标记某会话已读:清零自己对该会话的未读
|
||||
$peer = (int)($payload->receiver ?? 0);
|
||||
if ($peer) {
|
||||
WxchatUnreadCount::reset($uid, 0, $peer);
|
||||
}
|
||||
break;
|
||||
|
||||
case MsgType::REVOKED->value:
|
||||
// 撤回消息
|
||||
$msgId = (int)($payload->msg_id ?? 0);
|
||||
$msg = WxchatMessage::find($msgId);
|
||||
if ($msg && $msg->sender_id == $uid) {
|
||||
$msg->is_recalled = 1;
|
||||
$msg->recalled_at = date('Y-m-d H:i:s');
|
||||
$msg->save();
|
||||
// 通知对方撤回
|
||||
$to = (int)($payload->receiver ?? 0);
|
||||
if ($to && Gateway::isUidOnline($to)) {
|
||||
MsgReply::sendToUid($to, MsgFrame::STATUS->value, MsgType::REVOKED->value, 'revoked', [
|
||||
'msg_id' => $msgId,
|
||||
'from' => $uid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MsgType::DELIVERED->value:
|
||||
// 送达回执,无需处理
|
||||
break;
|
||||
|
||||
default:
|
||||
MsgReply::sendError($client_id, '不支持的状态消息类型');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace addon\wxchat\worker\handle;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* SystemHandle 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class SystemHandle
|
||||
{
|
||||
|
||||
public static function handle($client_id, $header, $payload)
|
||||
{
|
||||
switch ($header->type) {
|
||||
case MsgType::HEARTBEAT->value:
|
||||
self::Heartbeat($client_id, $header, $payload);
|
||||
break;
|
||||
case MsgType::AUTH->value:
|
||||
self::Auth($client_id, $header, $payload);
|
||||
break;
|
||||
case MsgType::NOTICE->value:
|
||||
self::Notification($client_id, $header, $payload);
|
||||
break;
|
||||
default:
|
||||
ErrorHandle::sendErrorMsg($client_id, '不支持的系统消息类型');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 心跳响应
|
||||
*/
|
||||
private static function Heartbeat($client_id, $header, $payload)
|
||||
{
|
||||
$newMessage = new Message();
|
||||
$newMessage->header->frame = MsgFrame::SYSTEM->value;
|
||||
$newMessage->header->type = MsgType::HEARTBEAT->value;
|
||||
$newMessage->header->to = Gateway::getUidByClientId($client_id);
|
||||
Gateway::sendToClient($client_id, json_encode($newMessage));
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录认证:校验 JWT,绑定 uid,更新在线状态
|
||||
*/
|
||||
private static function Auth($client_id, $header, $payload)
|
||||
{
|
||||
try {
|
||||
$app = \addon\wxchat\worker\ChatEvent::initApp();
|
||||
$token = \ywxapp\service\JwtService::instance()->parseAndValidate($payload->content ?? '');
|
||||
$claims = $token->claims()->all();
|
||||
$uid = (int)($claims['uid'] ?? 0);
|
||||
if (! $uid) {
|
||||
MsgReply::sendError($client_id, '认证失败:无效令牌');
|
||||
Gateway::closeClient($client_id);
|
||||
return;
|
||||
}
|
||||
|
||||
$userLib = \ywxapp\library\Auth::instance();
|
||||
$userLib->initUser($uid);
|
||||
if (! $userLib->isLogin) {
|
||||
MsgReply::sendError($client_id, '认证失败:用户不存在');
|
||||
Gateway::closeClient($client_id);
|
||||
return;
|
||||
}
|
||||
|
||||
Gateway::bindUid($client_id, $uid);
|
||||
|
||||
// 更新在线状态
|
||||
\ywxapp\model\MemberProfile::where('uid', $uid)
|
||||
->update(['online_status' => 1, 'last_active_at' => time()]);
|
||||
|
||||
$newMsg = new Message();
|
||||
$newMsg->header->frame = MsgFrame::SYSTEM->value;
|
||||
$newMsg->header->type = MsgType::AUTH->value;
|
||||
$newMsg->header->to = $uid;
|
||||
$newMsg->payload->content = ['uid' => $uid, 'status' => 'success'];
|
||||
Gateway::sendToClient($client_id, json_encode($newMsg));
|
||||
|
||||
// TODO: 拉取离线消息(从 DB history 由客户端主动拉取,此处可推送未读数)
|
||||
} catch (\Throwable $th) {
|
||||
echo "Auth failed: " . $th->getMessage() . "\n";
|
||||
MsgReply::sendError($client_id, '认证失败:' . $th->getMessage());
|
||||
Gateway::closeClient($client_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static function Notification($client_id, $header, $payload)
|
||||
{
|
||||
// 服务端下发通知(如系统公告),当前仅回显
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user