From 11165fcfb9cac799d2bc0c5c7f82bf5c1f0b6f8b Mon Sep 17 00:00:00 2001 From: mzl Date: Fri, 22 Aug 2025 18:16:13 +0800 Subject: [PATCH] Merge commit 'ed96e0f8589042fb8e9d6cfee713c5150612bb34' --- .../MessageNotificationsController.php | 52 ++++ .../MessageUserStatusController.php | 95 ++++++ .../logic/MessageNotificationsLogic.php | 29 ++ app/common/logic/MessageUserStatusLogic.php | 17 ++ app/common/model/MessageNotifications.php | 98 +++++++ app/common/model/MessageUserStatus.php | 66 +++++ .../service/MessageNotificationsService.php | 276 ++++++++++++++++++ .../service/MessageUserStatusService.php | 136 +++++++++ app/common/utils/WsEvents.php | 244 ++++++++++++++++ app/common/utils/WsResponse.php | 47 +++ .../validate/MessageNotificationsValidate.php | 29 ++ .../validate/MessageUserStatusValidate.php | 29 ++ app/controller/IndexController.php | 50 ++-- .../controller/CustomerController.php | 48 ++- app/crmAdmin/service/CustomerService.php | 16 +- app/functions.php | 24 ++ app/queue/redis/Notice.php | 48 +++ composer.json | 3 +- composer.lock | 89 +++++- config/plugin/webman/gateway-worker/app.php | 4 + .../plugin/webman/gateway-worker/process.php | 39 +++ config/route.php | 20 ++ plugin/webman/gateway/Events.php | 38 +++ 23 files changed, 1465 insertions(+), 32 deletions(-) create mode 100644 app/common/controller/MessageNotificationsController.php create mode 100644 app/common/controller/MessageUserStatusController.php create mode 100644 app/common/logic/MessageNotificationsLogic.php create mode 100644 app/common/logic/MessageUserStatusLogic.php create mode 100644 app/common/model/MessageNotifications.php create mode 100644 app/common/model/MessageUserStatus.php create mode 100644 app/common/service/MessageNotificationsService.php create mode 100644 app/common/service/MessageUserStatusService.php create mode 100644 app/common/utils/WsEvents.php create mode 100644 app/common/utils/WsResponse.php create mode 100644 app/common/validate/MessageNotificationsValidate.php create mode 100644 app/common/validate/MessageUserStatusValidate.php create mode 100644 app/queue/redis/Notice.php create mode 100644 config/plugin/webman/gateway-worker/app.php create mode 100644 config/plugin/webman/gateway-worker/process.php create mode 100644 plugin/webman/gateway/Events.php diff --git a/app/common/controller/MessageNotificationsController.php b/app/common/controller/MessageNotificationsController.php new file mode 100644 index 0000000..19341fc --- /dev/null +++ b/app/common/controller/MessageNotificationsController.php @@ -0,0 +1,52 @@ +service = new MessageNotificationsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + return $data; + } + + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + return $data; + } + + + public function push(Request $request) + { + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'store'); + $res = $this->service->servicePush($request, $data); + return $this->success($res, '推送成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/common/controller/MessageUserStatusController.php b/app/common/controller/MessageUserStatusController.php new file mode 100644 index 0000000..9a896a1 --- /dev/null +++ b/app/common/controller/MessageUserStatusController.php @@ -0,0 +1,95 @@ +service = new MessageUserStatusService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['read_at']) && $data['read_at'] !== null) { + $data['read_at'] = date('Y-m-d H:i:s', $data['read_at']); + } else { + $data['read_at'] = ' - '; + } + + if (isset($data['message'])) + $data['message']['created_at'] = date('Y-m-d H:i:s', $data['message']['created_at']); + return $data; + } + + protected function showFormat($data) + { + if (isset($data['read_at']) && $data['read_at'] !== null) { + $data['read_at'] = date('Y-m-d H:i:s', $data['read_at']); + } else { + $data['read_at'] = ' - '; + } + + if (isset($data['message'])) + $data['message']['created_at'] = date('Y-m-d H:i:s', $data['message']['created_at']); + return $data; + } + + protected function unReadFormat($data) + { + foreach ($data as &$item) { + if (isset($item['read_at']) && $item['read_at'] !== null) { + $item['read_at'] = date('Y-m-d H:i:s', $item['read_at']); + } else { + $item['read_at'] = ' - '; + } + + if (isset($item['message'])) { + $item['message']['created_at'] = date('Y-m-d H:i:s', $item['message']['created_at']); + } + } + return $data; + } + + public function read(Request $request) + { + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'read'); + $this->service->serviceRead($request, $data); + return $this->success([], '操作成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function listUnRead(Request $request) + { + try { + $raw = $this->service->serviceListUnRead($request); + $data = $this->transformData($raw, method_exists($this, 'unReadFormat') + ? [$this, 'unReadFormat'] + : null); + $data = [ + 'list' => $data, + 'counts' => count($data) + ]; + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/common/logic/MessageNotificationsLogic.php b/app/common/logic/MessageNotificationsLogic.php new file mode 100644 index 0000000..594bb96 --- /dev/null +++ b/app/common/logic/MessageNotificationsLogic.php @@ -0,0 +1,29 @@ +logicShow($config); + if ($msg) { + // 删除关联的用户状态记录 + $msg->userStatuses()->delete(); + // 删除消息本身 + $msg->delete(); + } + return true; + } +} \ No newline at end of file diff --git a/app/common/logic/MessageUserStatusLogic.php b/app/common/logic/MessageUserStatusLogic.php new file mode 100644 index 0000000..ea4b63e --- /dev/null +++ b/app/common/logic/MessageUserStatusLogic.php @@ -0,0 +1,17 @@ + '系统消息', + self::SCENE1 => '系统通知' + ]; + const SCENE0 = 0; + const SCENE1 = 1; + + // 推送方式 + const PUSH_METHOD = [ + self::PUSH_METHOD0 => '系统推送', + self::PUSH_METHOD1 => '手动推送' + ]; + const PUSH_METHOD0 = 0; + const PUSH_METHOD1 = 1; + + + /** + * 关联用户消息状态 (一对多) + */ + public function userStatuses() + { + return $this->hasMany(MessageUserStatus::class, 'msg_id', 'id'); + } + + /** + * 获取已读用户状态 + */ + public function readUsers() + { + return $this->hasMany(MessageUserStatus::class, 'msg_id', 'id') + ->where('status', 1); + } + + /** + * 获取未读用户状态 + */ + public function unreadUsers() + { + return $this->hasMany(MessageUserStatus::class, 'msg_id', 'id') + ->where('status', 0); + } + + public function sender() + { + return $this->belongsTo(User::class, 'sender_id', 'id'); + } +} \ No newline at end of file diff --git a/app/common/model/MessageUserStatus.php b/app/common/model/MessageUserStatus.php new file mode 100644 index 0000000..0e73ef5 --- /dev/null +++ b/app/common/model/MessageUserStatus.php @@ -0,0 +1,66 @@ +belongsTo(MessageNotifications::class, 'msg_id', 'id'); + } + + /** + * 关联用户 (假设你有 User 模型) + */ + public function user() + { + return $this->belongsTo(User::class, 'uid', 'id'); + } +} \ No newline at end of file diff --git a/app/common/service/MessageNotificationsService.php b/app/common/service/MessageNotificationsService.php new file mode 100644 index 0000000..806bc1d --- /dev/null +++ b/app/common/service/MessageNotificationsService.php @@ -0,0 +1,276 @@ +validator; + } + + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + + public function serviceDestroy(Request $request) + { + $config = $this->buildQueryConfig($request, 'destroy'); + $deleted = (new MessageNotificationsLogic())->logicDeleteBy($config); + return $this->postProcess('Destroy', $deleted); + } + + /** + * 添加前置处理 + */ + public function servicePush(Request $request, $data) + { + $pushData = [ + 'sender_id' => $request->user->uid, + 'sender_type' => 'admin', + 'title' => $data['title'], + 'content' => $data['content'], + 'scene' => 1, + 'recipient' => $data['recipient'], + 'push_method' => 1 + ]; + $msgId = (new MessageNotificationsService())->pushNotice($pushData); + return ['msg_id' => $msgId]; + } + + public function afterServiceList($data) + { + $data->each(function (&$item) { + $temp = explode(',', $item->recipient); + $item->recipient_list = User::whereIn('id', $temp)->pluck('name')->toArray(); + $item->scene_name = MessageNotifications::SCENE[$item->scene]; + $item->push_method_name = MessageNotifications::PUSH_METHOD[$item->push_method]; + }); + unset($item); + return $data; + } + + public function afterServiceShow($data) + { + $temp = explode(',', $data->recipient); + $data->recipient_list = User::whereIn('id', $temp)->pluck('name')->toArray(); + $data->scene_name = MessageNotifications::SCENE[$data->scene]; + $data->push_method_name = MessageNotifications::PUSH_METHOD[$data->push_method]; + return $data; + } + + + + // ====== ====== ====== 通知操作的处理 ====== ====== ====== + /** + * 推送消息 + */ + public function pushNotice(array $data = [], string $scene = "Default"): int + { + // 拼装数据(可能需要处理 消息标题、内容等等数据,可自行处理) + $pushData = $this->assemblyProcess($scene, $data); + // 写入消息 + $msgId = $this->insertMessage($pushData); + // 推送 + $this->pushWs($msgId, $pushData); + return $msgId; + } + + /** + * 执行推送 + */ + public function pushWs(int $msgId, array $pushData) + { + foreach ($pushData['recipient'] as $userId) { + // 查询消息数据 + $config['model'] = MessageUserStatus::class; + $config['order']['id'] = 'desc'; + $config['with']['message'] = ['sender_id', 'sender_type', 'msg_no', 'title', 'content', 'scene', 'created_at']; + $config['where']['uid'] = $userId; + $config['where']['msg_id'] = $msgId; + $config['where']['status'] = 0; + $wsData = (new MessageUserStatusService())->logicShow($config)->toArray(); + + // 组装数据 + if (isset($wsData['message']) && $wsData['message']) { + $wsData['message']['sender_name'] = User::where('id', $wsData['message']['sender_id'])->value('name') ?: '系统'; + $wsData['message']['scene_name'] = MessageNotifications::SCENE[$wsData['message']['scene']]; + $wsData['message']['created_at'] = date('Y-m-d H:i:s', $wsData['message']['created_at']); + } + + // 获取未读消息数 + $counts = MessageUserStatus::where('uid', $userId)->where('status', 0)->count('id'); + $data = [ + 'notice_data' => $wsData, + 'unread_counts' => $counts + ]; + + // 执行推送 + (new \app\common\utils\WsEvents())->pushWsNotice($data, $userId); + } + return true; + } + + /** + * 插入消息 + * @param array $data + * @return int + */ + public function insertMessage(array $data): int + { + // 写消息 + $msgId = $this->createMessage([ + 'sender_id' => $data['sender_id'], + 'sender_type' => $data['sender_type'], + 'title' => $data['title'], + 'content' => $data['content'], + 'scene' => $data['scene'], + 'recipient' => implode(',', $data['recipient']), + 'push_method' => $data['push_method'], + ]); + // 写用户状态 + $this->addUserStatus($msgId, $data['recipient']); + return $msgId; + } + + /** + * 数据拼装方法调用 + */ + protected function assemblyProcess(string $op, mixed $data): mixed + { + $hook = "assembly{$op}"; + return method_exists($this, $hook) + ? $this->{$hook}($data) + : $data; + } + + /** + * 默认数据拼装 + */ + public function assemblyDefault(array $data) + { + return [ + 'sender_id' => isset($data['sender_id']) && $data['sender_id'] ? $data['sender_id'] : 0, + 'sender_type' => isset($data['sender_type']) && $data['sender_type'] ? $data['sender_type'] : 'system', + 'title' => isset($data['title']) && $data['title'] ? $data['title'] : '系统通知', + 'content' => isset($data['content']) && $data['content'] ? $data['content'] : '系统通知-默认', + 'scene' => isset($data['scene']) && $data['scene'] ? $data['scene'] : 1, + 'recipient' => isset($data['recipient']) && count($data['recipient']) > 0 ? $data['recipient'] : $this->getAllUserIds(), + 'push_method' => isset($data['push_method']) && $data['push_method'] ? $data['push_method'] : 0, + ]; + } + + // *** *** 可以扩展数据组拼装方法 *** *** + + // ====== ====== ====== 通知操作的基础方法 ====== ====== ====== + + /** + * 创建一条消息 + */ + public function createMessage(array $data): int + { + $msgId = Db::table('message_notifications')->insertGetId([ + 'msg_no' => generateMsgId(), + 'sender_id' => $data['sender_id'], + 'sender_type' => $data['sender_type'], + 'title' => $data['title'], + 'content' => $data['content'], + 'scene' => $data['scene'], + 'recipient' => $data['recipient'], + 'push_method' => $data['push_method'], + 'created_at' => time(), + ]); + + return $msgId; + } + + /** + * 批量插入用户消息状态 + */ + public function addUserStatus(int $msgId, array $uids) + { + $rows = []; + foreach ($uids as $uid) { + $rows[] = [ + 'msg_id' => $msgId, + 'uid' => $uid, + 'status' => 0, + 'read_at' => null + ]; + } + if ($rows) { + Db::table('message_user_status')->insert($rows); + } + } + + /** + * 标记消息已读 + */ + public function markAsRead(array|string $msgIds, int $uid) + { + $query = Db::table('message_user_status')->where('uid', $uid)->where('status', 0); + if (is_array($msgIds)) { + $query->whereIn('msg_id', $msgIds); + } + return $query->update([ + 'status' => 1, + 'read_at' => time() + ]); + } + + /** + * 获取所有用户ID + */ + public function getAllUserIds(): array + { + return User::where('status', 1)->pluck('id')->toArray(); + } +} \ No newline at end of file diff --git a/app/common/service/MessageUserStatusService.php b/app/common/service/MessageUserStatusService.php new file mode 100644 index 0000000..2162f86 --- /dev/null +++ b/app/common/service/MessageUserStatusService.php @@ -0,0 +1,136 @@ +validator; + } + + + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'unread': + $config['order']['id'] = 'desc'; + $config['with']['message'] = ['sender_id', 'sender_type', 'msg_no', 'title', 'content', 'scene', 'created_at']; + $config['where']['uid'] = $request->user->uid; + $config['where']['status'] = 0; + break; + case 'index': + $config['order']['id'] = 'desc'; + $config['with']['message'] = ['sender_id', 'sender_type', 'msg_no', 'title', 'content', 'scene', 'created_at']; + $config['where']['uid'] = $request->user->uid; + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['with']['message'] = ['sender_id', 'sender_type', 'msg_no', 'title', 'content', 'scene', 'created_at']; + $config['where']['uid'] = $request->user->uid; + break; + case 'store': + break; + case 'update': + case 'read': + $config['where']['id'] = $p['id']; + $config['where']['msg_id'] = $p['msg_id']; + $config['where']['uid'] = $request->user->uid; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + $config['where']['uid'] = $request->user->uid; + break; + } + + return $config; + } + + public function serviceListUnRead(Request $request) + { + $config = $this->buildQueryConfig($request, 'unread'); + $result = $this->logicListAll($config); + return $this->postProcess('ListUnRead', $result); + } + + public function serviceRead(Request $request, array $data = []) + { + return $this->noticeRead($request->post('msg_ids'), $request->user->uid); + } + + public function noticeRead(array|string $msgIds, int $uid): bool + { + $model = MessageUserStatus::where('uid', $uid)->where('status', 0); + if (is_array($msgIds)) { + $model->whereIn('msg_id', $msgIds); + } + $udata['read_at'] = time(); + $udata['status'] = 1; + return $model->update($udata); + } + + + protected function afterServiceList($res) + { + $res->each(function (&$item) { + if (isset($item->message)) { + $item->message->sender_name = User::where('id', $item->message->sender_id)->value('name') ?: '系统'; + $item->message->scene_name = MessageNotifications::SCENE[$item->message->scene]; + } + }); + unset($item); + return $res; + } + + protected function afterServiceShow($res) + { + if (isset($res->message)) { + $res->message->sender_name = User::where('id', $res->message->sender_id)->value('name') ?: '系统'; + $res->message->scene_name = MessageNotifications::SCENE[$res->message->scene]; + } + // 标记已读 + if ($res->status == 0) { + $this->noticeRead([$res->msg_id], $res->uid); + } + return $res; + } + + protected function afterServiceListUnRead($res) + { + $res->each(function (&$item) { + if (isset($item->message)) { + $item->message->sender_name = User::where('id', $item->message->sender_id)->value('name') ?: '系统'; + $item->message->scene_name = MessageNotifications::SCENE[$item->message->scene]; + } + }); + unset($item); + return $res; + } +} \ No newline at end of file diff --git a/app/common/utils/WsEvents.php b/app/common/utils/WsEvents.php new file mode 100644 index 0000000..2a65f3e --- /dev/null +++ b/app/common/utils/WsEvents.php @@ -0,0 +1,244 @@ +registerAddress; + } + + /** + * 监听消息接收 + */ + public function message($client_id, $message) + { + $this->clientId = $client_id; + + $payload = json_decode($message, true); + $event = $payload['event']; + $this->eventData = $payload['data'] ?? []; + $traceId = $payload['trace_id'] ?? null; + + $method = $this->eventToMethod($event); + + if (method_exists($this, $method)) { + try { + $this->$method($traceId); + } catch (\Throwable $th) { + Gateway::sendToClient($client_id, WsResponse::error( + $event, + 500, + $th->getMessage(), + $traceId + )); + } + } else { + Gateway::sendToClient($client_id, WsResponse::error( + $event, + 404, + "Event handler not found: $method", + $traceId + )); + } + } + + + // ================= 系统事件 ================= + /** + * 认证 + */ + public function auth($client_id, $payload) + { + try { + // 检查来源 + if (!isset($payload['server']['HTTP_ORIGIN'])) { + flg('[x] [TOKEN认证] 未定义HTTP_ORIGIN', false); + Gateway::sendToClient($client_id, WsResponse::error( + 'system.auth', + 1001, + '[x] [TOKEN认证] 未定义HTTP_ORIGIN' + )); + Gateway::closeClient($client_id); + return; + } + + // 连接是否满足条件 + // if ($payload['server']['HTTP_ORIGIN'] != 'https://wstool.js.org') { + // flg('[x] [TOKEN认证] HTTP_ORIGIN不满足条件', false); + // Gateway::sendToClient($client_id, WsResponse::error( + // 'system.auth', + // 1001, + // '[x] [TOKEN认证] HTTP_ORIGIN不满足条件' + // )); + // Gateway::closeClient($client_id); + // return; + // } + + // 签名检查 + if (!isset($payload['get']['token'])) { + flg('[x] [TOKEN认证] 未携带TOKEN参数', false); + Gateway::sendToClient($client_id, WsResponse::error( + 'system.auth', + 1001, + '[x] [TOKEN认证] 未携带TOKEN参数' + )); + Gateway::closeClient($client_id); + return; + } + + // 解析TOKEN + $user = (new Jwt())->verifyToken($payload['get']['token']); + // 绑定用户ID + Gateway::bindUid($client_id, $user->uid); + + // 响应 + Gateway::sendToClient($client_id, WsResponse::success( + 'system.auth', + ['uid' => $user->uid] + )); + } catch (\Throwable $th) { + flg('[x] [TOKEN认证] TOKEN认证失败:'.$th->getMessage(), false); + Gateway::sendToClient($client_id, WsResponse::error( + 'system.auth', + 1001, + '[x] [TOKEN认证] TOKEN认证失败:'.$th->getMessage() + )); + Gateway::closeClient($client_id); + return; + } + } + + /** + * 系统心跳 + * + * @param string|null $traceId + */ + public function systemPing(?string $traceId) + { + Gateway::sendToClient($this->clientId, WsResponse::success( + 'system.pong', + ['time' => time()], + $traceId + )); + } + + + // ================= 消息通知事件 ================= + /** + * 管理端主动推送通知 + */ + public function notifyPush(?string $traceId) + { + $title = $this->eventData['title'] ?? null; + $content = $this->eventData['content'] ?? null; + $toUids = $this->eventData['to_uids'] ?? []; + + if (!$title || !$content) { + Gateway::sendToClient($this->clientId, WsResponse::error( + 'notify.push.result', + 400, + '标题和内容不能为空', + $traceId + )); + return; + } + + // 写消息 + $msgService = new MessageNotificationsService(); + $msgId = $msgService->createMessage([ + 'sender_id' => $this->eventData['sender_id'] ?? 0, + 'sender_type' => $this->eventData['sender_type'] ?? 'system', + 'title' => $title, + 'content' => $content, + 'scene' => $this->eventData['scene'] ?? 0, + 'recipient' => $this->eventData['to_uids'] ? implode(',', $this->eventData['to_uids']) : '', + 'push_method' => isset($this->eventData['push_method']) ? $this->eventData['push_method'] : 0 + ]); + + // 写用户状态 + $uids = !empty($toUids) ? $toUids : $msgService->getAllUserIds(); + $msgService->addUserStatus($msgId, $uids); + + $payload = WsResponse::success('notify.receive', [ + 'msg_id' => $msgId, + 'title' => $title, + 'content' => $content, + 'created_at' => time() + ], $traceId); + + if (!empty($toUids)) { + // 推送给指定用户 + foreach ($toUids as $uid) { + Gateway::sendToUid($uid, $payload); + } + } else { + // 推送给全体在线用户 + Gateway::sendToAll($payload); + } + + // 回复发送者确认 + Gateway::sendToClient($this->clientId, WsResponse::success( + 'notify.push.result', + ['msg_id' => $msgId], + $traceId, + '消息发送成功' + )); + } + + /** + * 客户端标记通知已读 + */ + public function notifyRead(?string $traceId) + { + $msgIds = $this->eventData['msg_ids']; + if (!is_array($msgIds) && $msgIds !== 'all') { + Gateway::sendToClient($this->clientId, WsResponse::error( + 'notify.read.result', + 400, + '消息ID不能为空', + $traceId + )); + return; + } + + // 更新消息为已读 + $msgService = new MessageNotificationsService(); + $msgService->markAsRead($msgIds, Gateway::getUidByClientId($this->clientId)); + + Gateway::sendToClient($this->clientId, WsResponse::success( + 'notify.read.result', + ['msg_ids' => $msgIds], + $traceId + )); + } + + /** + * 推送消息通知 + */ + public function pushWsNotice(array $wsData, int $userId) + { + Gateway::sendToUid($userId, WsResponse::success('notify.receive', $wsData)); + } + + // ================= 辅助方法 ================= + protected function eventToMethod(string $event): string + { + $parts = explode('.', $event); + $method = array_shift($parts); + foreach ($parts as $part) { + $method .= ucfirst($part); + } + return $method; + } +} \ No newline at end of file diff --git a/app/common/utils/WsResponse.php b/app/common/utils/WsResponse.php new file mode 100644 index 0000000..6cf0b28 --- /dev/null +++ b/app/common/utils/WsResponse.php @@ -0,0 +1,47 @@ + $event, + 'code' => $code, + 'message' => $message, + 'data' => $data, + 'trace_id' => $traceId, // 如果没有传就是 null + ], JSON_UNESCAPED_UNICODE); + } + + /** + * 错误返回 + * + * @param string $event 事件名称 + * @param int $code 错误码 + * @param string $message 错误信息 + * @param string|null $traceId 请求追踪ID,可选,默认 null + * @param array $data 附加数据(可选) + * @return string JSON + */ + public static function error(string $event, int $code, string $message, ?string $traceId = null, array $data = []): string + { + return json_encode([ + 'event' => $event, + 'code' => $code, + 'message' => $message, + 'data' => $data, + 'trace_id' => $traceId, // 如果没有传就是 null + ], JSON_UNESCAPED_UNICODE); + } +} \ No newline at end of file diff --git a/app/common/validate/MessageNotificationsValidate.php b/app/common/validate/MessageNotificationsValidate.php new file mode 100644 index 0000000..c5dd6e1 --- /dev/null +++ b/app/common/validate/MessageNotificationsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/MessageUserStatusValidate.php b/app/common/validate/MessageUserStatusValidate.php new file mode 100644 index 0000000..647cebd --- /dev/null +++ b/app/common/validate/MessageUserStatusValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index e7ae182..8e15d2a 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -6,6 +6,7 @@ use app\common\model\InquiryLog; use app\common\model\User; use app\common\model\UserRoles; use app\common\model\UserSystems; +use app\common\service\MessageNotificationsService; use app\common\traits\DataPermission; use app\common\utils\email\MailSend; use app\common\utils\scaffold\Generate; @@ -77,25 +78,25 @@ class IndexController $options = [ 'controller' => [ 'module' => 'common', - 'name' => 'resources' + 'name' => 'message_notifications' ], 'model' => [ 'module' => 'common', - 'name' => 'resources' + 'name' => 'message_notifications' ], 'service' => [ 'module' => 'common', - 'name' => 'resources' + 'name' => 'message_notifications' ], 'logic' => [ 'module' => 'common', - 'name' => 'resources' + 'name' => 'message_notifications' ], 'validate' => [ 'module' => 'common', - 'name' => 'resources' + 'name' => 'message_notifications' ], - 'table' => 'resources' + 'table' => 'message_notifications' ]; $result = (new Generate($options))->doGenerate(); @@ -107,20 +108,35 @@ class IndexController { // $data = [ - // 'code' => 'GD20250703-1DC80', - // 'category' => '账号类问题', - // 'content' => '这是一条测试工单内容。' + // 'sender_id' => 0, + // 'sender_type' => 'system', + // 'title' => '测试消息 - 标题', + // 'content' => '测试消息 - 内容', + // 'scene' => 1, + // 'recipient' => [1], // ]; - // \app\common\utils\email\MailSend::send('mzl@hnxinyouhe.com', '【XYH】新的工单提醒', replaceTpl($data, 'work_order')); - // $options = [ - // 'corp_id' => 3, - // 'user_id' => 666 - // ]; - // \Webman\RedisQueue\Redis::send('perm-agt', $options); + $data['recipient'] = [1, 74]; + // // 同步推送 + // (new MessageNotificationsService())->pushNotice($data); - $res = Cache::delete("user_resources_74_fdesk"); - return 22; + + // 异步推送 + \Webman\RedisQueue\Redis::send('notice', $data); + + + return 'success'; + + // return json($data); + + // $phone = ''; + // $email = ''; + + // $userByPhone = User::where('corp_id', 2)->where('phone', $phone)->where('phone', '!=', '')->first(); + // $userByEmail = User::where('corp_id', 2)->where('email', $email)->where('email', '!=', '')->first(); + // $user = $userByPhone ?: $userByEmail; + + // return json($user); } } \ No newline at end of file diff --git a/app/crmAdmin/controller/CustomerController.php b/app/crmAdmin/controller/CustomerController.php index 900046d..c68bd18 100644 --- a/app/crmAdmin/controller/CustomerController.php +++ b/app/crmAdmin/controller/CustomerController.php @@ -4,6 +4,7 @@ namespace app\crmAdmin\controller; use app\common\model\Corp; use app\crm\model\Customer; +use app\crm\utils\Util; use app\crmAdmin\service\CustomerService; use support\Request; @@ -148,19 +149,18 @@ class CustomerController extends BaseController } $customer_id = explode(',', $param['customer_id']); - $customer = Customer::whereIn('id',$customer_id)->withTrashed()->first(); - if(empty($customer)){ + $query = Customer::whereIn('id',$customer_id)->withTrashed(); + if(!$query->exists()){ return $this->error( trans('fail')); } - if($param['scene'] == 'list'){ //管理端客户列表移除 - $customer->status = 4; - - }else{ //公海软删除 - $customer->status = 3; - + $scene = $param['scene'] ?? ''; + if ($scene === 'list') { //管理端客户列表移除 + $res = $query->update(['status' => 4, 'deleted_at' => time()]); + } elseif ($scene === 'force') { //强删除 + $res = $query->forceDelete(); + } else { //公海软删除 + $res = $query->update(['status' => 3, 'deleted_at' => time()]); } - $res = $customer->save(); - $customer->delete(); return $this->success($res); } catch (\Throwable $th) { @@ -168,6 +168,34 @@ class CustomerController extends BaseController } } + + /** + * 客户恢复 + * @param Request $request + * @return \support\Response + */ + public function restore(Request $request) + { + try { + $id = $request->input('id', ''); + if (Util::isEmpty($id)) { + return $this->error(trans('id_empty')); + } + $ids = explode(',', $id); + $query = Customer::withTrashed()->whereIn('id', $ids); + if (!$query->exists()) { + return $this->error(trans('fail')); + } + // 恢复软删除(清空 deleted_at) + $query->restore(); + // 恢复为在库状态,放入公海 + $res = Customer::whereIn('id', $ids)->update(['status' => 1, 'user_id' => 0]); + return $this->success($res, trans('success')); + } catch (\Throwable $th) { + return $this->error($th->getMessage()); + } + } + /** * 客户详情 * @param Request $request diff --git a/app/crmAdmin/service/CustomerService.php b/app/crmAdmin/service/CustomerService.php index c38162b..635b6cf 100644 --- a/app/crmAdmin/service/CustomerService.php +++ b/app/crmAdmin/service/CustomerService.php @@ -51,12 +51,14 @@ class CustomerService $follow_last = $params['follow_last'] ?? ''; $tagId = $params['tag_id'] ?? []; $user_id = $params['user_id'] ?? ''; - $seas = $params['seas'] ?? ''; + $seas = $params['seas'] ?? ''; //recyclea:管理端回收站,audit:待审核列表,seas:公海 $scene = $params['scene'] ?? ''; $where = []; if ($scene == 'audit') { $where[] = ['status', '=', 2]; + } elseif ($scene == 'recyclea') { + $where[] = ['status', '>', 2]; } else { $where[] = ['status', '=', 1]; } @@ -121,8 +123,8 @@ class CustomerService if ($recycle == 'recycle' || $scene == 'recycle' || $scene == 'audit') { $query->onlyTrashed(); } - if ($seas == 'seas' || $scene == 'seas') { - $query->withTrashed(); + if ($seas == 'seas' || $scene == 'recyclea') { + $query = $query->withTrashed(); } if (!$export) { @@ -153,6 +155,14 @@ class CustomerService } $item['audit'] = $audit; } + if ($scene == 'recyclea' && $item['status'] == 3) { //回收站删除来源 + $item['del_source'] = '公海'; + }elseif ($scene == 'recyclea' && $item['status'] == 4) { + $item['del_source'] = '管理端'; + }elseif ($scene == 'recyclea' && $item['status'] == 5) { + $source = (!empty($item['user_id'])) ? (':' . User::where('id',$item['user_id'])->value('name')) : ''; + $item['del_source'] = '业务员' . $source; + } } } unset($item); diff --git a/app/functions.php b/app/functions.php index 299bad4..d9bb411 100644 --- a/app/functions.php +++ b/app/functions.php @@ -283,4 +283,28 @@ if (!function_exists('normalizeRichText')) { // 替换所有空白实体为普通空格 return preg_replace('/\s+/u', ' ', $result); } +} + + +/** + * 生成消息编号(唯一业务标识) + * 格式:年月日时分秒 + 微秒 + 随机数 + * 例如:20250820175530987654321 + * + * @return string + */ +if (!function_exists('generateMsgId')) { + function generateMsgId(): string + { + // 时间部分(精确到微秒) + $time = microtime(true); + $micro = sprintf("%06d", ($time - floor($time)) * 1e6); + $date = date('YmdHis', (int)$time); + + // 随机部分(3位) + $rand = mt_rand(100, 999); + + // 拼接:日期时间 + 微秒 + 随机数 + return $date . $micro . $rand; + } } \ No newline at end of file diff --git a/app/queue/redis/Notice.php b/app/queue/redis/Notice.php new file mode 100644 index 0000000..3a310c8 --- /dev/null +++ b/app/queue/redis/Notice.php @@ -0,0 +1,48 @@ +pushNotice($data); + return true; + } catch (\Throwable $th) { + var_dump('4 :'.$th->getFile()."|".$th->getLine()."|".$th->getMessage()); + throw new BusinessException($th->getFile()."|".$th->getLine()."|".$th->getMessage()); + } + } + + // 消费失败回调 + /* + $package = [ + 'id' => 1357277951, // 消息ID + 'time' => 1709170510, // 消息时间 + 'delay' => 0, // 延迟时间 + 'attempts' => 2, // 消费次数 + 'queue' => 'send-mail', // 队列名 + 'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容 + 'max_attempts' => 5, // 最大重试次数 + 'error' => '错误信息' // 错误信息 + ] + */ + public function onConsumeFailure(\Throwable $e, $package) + { + echo "consume failure\n"; + echo $e->getMessage() . "\n"; + // 无需反序列化 + var_export($package); + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index 98e3e54..c474010 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,8 @@ "symfony/filesystem": "^7.3", "workerman/crontab": "^1.0", "phpoffice/phpspreadsheet": "^4.5", - "phpmailer/phpmailer": "^6.10" + "phpmailer/phpmailer": "^6.10", + "webman/gateway-worker": "^1.0" }, "suggest": { "ext-event": "For better performance. " diff --git a/composer.lock b/composer.lock index edf286f..7d41f97 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "505fb3d97c338ea53b787196fcc3daaa", + "content-hash": "7c70d8c8bda8a5098b8baed463db8839", "packages": [ { "name": "aliyuncs/oss-sdk-php", @@ -6634,6 +6634,39 @@ }, "time": "2025-04-04T13:23:25+00:00" }, + { + "name": "webman/gateway-worker", + "version": "v1.0.9", + "source": { + "type": "git", + "url": "https://github.com/webman-php/gateway-worker.git", + "reference": "6780c1c788c273a24699caee84feb14224234000" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/gateway-worker/zipball/6780c1c788c273a24699caee84feb14224234000", + "reference": "6780c1c788c273a24699caee84feb14224234000", + "shasum": "" + }, + "require": { + "workerman/gateway-worker": ">=3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webman\\GatewayWorker\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "issues": "https://github.com/webman-php/gateway-worker/issues", + "source": "https://github.com/webman-php/gateway-worker/tree/v1.0.9" + }, + "time": "2024-04-30T01:05:42+00:00" + }, { "name": "webman/redis", "version": "v2.1.3", @@ -6793,6 +6826,60 @@ }, "time": "2025-01-15T07:20:50+00:00" }, + { + "name": "workerman/gateway-worker", + "version": "v4.0.1", + "source": { + "type": "git", + "url": "https://github.com/walkor/GatewayWorker.git", + "reference": "25e4430982caa98f82e9187d092ce34b2fddfc90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/GatewayWorker/zipball/25e4430982caa98f82e9187d092ce34b2fddfc90", + "reference": "25e4430982caa98f82e9187d092ce34b2fddfc90", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "workerman/workerman": "^4.0.30 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "GatewayWorker\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "homepage": "http://www.workerman.net", + "keywords": [ + "communication", + "distributed" + ], + "support": { + "issues": "https://github.com/walkor/GatewayWorker/issues", + "source": "https://github.com/walkor/GatewayWorker/tree/v4.0.1" + }, + "funding": [ + { + "url": "https://opencollective.com/walkor", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/walkor", + "type": "patreon" + } + ], + "time": "2025-06-27T01:30:09+00:00" + }, { "name": "workerman/redis", "version": "v2.0.4", diff --git a/config/plugin/webman/gateway-worker/app.php b/config/plugin/webman/gateway-worker/app.php new file mode 100644 index 0000000..8f9c426 --- /dev/null +++ b/config/plugin/webman/gateway-worker/app.php @@ -0,0 +1,4 @@ + true, +]; \ No newline at end of file diff --git a/config/plugin/webman/gateway-worker/process.php b/config/plugin/webman/gateway-worker/process.php new file mode 100644 index 0000000..5c7efaa --- /dev/null +++ b/config/plugin/webman/gateway-worker/process.php @@ -0,0 +1,39 @@ + [ + 'handler' => Gateway::class, + 'listen' => 'websocket://0.0.0.0:5151', + 'count' => 2, + 'reloadable' => false, + 'constructor' => ['config' => [ + 'lanIp' => '127.0.0.1', + 'startPort' => 2300, + 'pingInterval' => 55, + 'pingNotResponseLimit' => 1, + 'pingData' => '', + 'registerAddress' => '127.0.0.1:1236', + 'onConnect' => function(){}, + ]] + ], + 'worker' => [ + 'handler' => BusinessWorker::class, + 'count' => cpu_count()*2, + 'constructor' => ['config' => [ + 'eventHandler' => plugin\webman\gateway\Events::class, + 'name' => 'ChatBusinessWorker', + 'registerAddress' => '127.0.0.1:1236', + ]] + ], + 'register' => [ + 'handler' => Register::class, + 'listen' => 'text://127.0.0.1:1236', + 'count' => 1, // Must be 1 + 'reloadable' => false, + 'constructor' => [] + ], +]; diff --git a/config/route.php b/config/route.php index 2abd88a..6393f2d 100644 --- a/config/route.php +++ b/config/route.php @@ -64,10 +64,29 @@ Route::group('/common', function () { Route::any('/cache/clear', [app\common\controller\PresetController::class, 'cacheClear']); Route::any('/menus', [app\common\controller\PresetController::class, 'menus']); }); + + + // 消息通知 + Route::group('/notice', function () { + Route::any('/list', [app\common\controller\MessageUserStatusController::class, 'index']); + Route::any('/show', [app\common\controller\MessageUserStatusController::class, 'show']); + Route::any('/read', [app\common\controller\MessageUserStatusController::class, 'read']); + Route::any('/destroy', [app\common\controller\MessageUserStatusController::class, 'destroy']); + + Route::any('/unread', [app\common\controller\MessageUserStatusController::class, 'listUnRead']); + }); }); // 后台管理 Route::group('/admin', function () { + // 消息通知 + Route::group('/notice', function () { + Route::any('/list', [app\common\controller\MessageNotificationsController::class, 'index']); + Route::any('/show', [app\common\controller\MessageNotificationsController::class, 'show']); + Route::any('/push', [app\common\controller\MessageNotificationsController::class, 'push']); + Route::any('/destroy', [app\common\controller\MessageNotificationsController::class, 'destroy']); + }); + // 角色 Route::group('/role', function () { Route::any('/list', [app\admin\controller\RoleController::class, 'index']); @@ -157,6 +176,7 @@ Route::group('/admin', function () { Route::any('/transfer', [app\crmAdmin\controller\CustomerController::class, 'transfer']); Route::any('/detail', [app\crmAdmin\controller\CustomerController::class, 'detail']); Route::any('/delete', [app\crmAdmin\controller\CustomerController::class, 'delete']); + Route::any('/restore', [app\crmAdmin\controller\CustomerController::class, 'restore']); //恢复客户 Route::any('/cede', [app\crm\controller\CustomerController::class, 'cede']); Route::any('/follow', [app\crmAdmin\controller\CustomerController::class, 'follow']); Route::any('/allocation', [app\crmAdmin\controller\CustomerController::class, 'allocation']); diff --git a/plugin/webman/gateway/Events.php b/plugin/webman/gateway/Events.php new file mode 100644 index 0000000..d63708e --- /dev/null +++ b/plugin/webman/gateway/Events.php @@ -0,0 +1,38 @@ +auth($client_id, $data); + } + + public static function onMessage($client_id, $message) + { + self::$wsEvents->message($client_id, $message); + } + + public static function onClose($client_id) + { + var_dump('关闭客户端ID:'.$client_id); + } + +} \ No newline at end of file