# WebSocket 与通知系统完整文档 ## 目录 - [概述](#概述) - [功能特性](#功能特性) - [技术架构](#技术架构) - [后端实现](#后端实现) - [前端实现](#前端实现) - [API接口](#api接口) - [配置说明](#配置说明) - [使用示例](#使用示例) - [连接问题修复](#连接问题修复) - [故障排查与修复](#故障排查与修复) - [性能优化](#性能优化) - [安全考虑](#安全考虑) - [最佳实践](#最佳实践) - [更新日志](#更新日志) --- ## 概述 本项目基于 Laravel-S 和 Swoole 实现了完整的 WebSocket 与通知系统,支持实时双向通信、消息推送、通知管理等功能。系统采用 WebSocket 实现实时推送,同时将通知持久化到数据库,确保离线用户也能收到通知。 ### 核心特性 - ✅ 实时双向通信(WebSocket) - ✅ 用户连接管理(单例模式,避免重复连接) - ✅ 点对点消息发送 - ✅ 群发消息/广播 - ✅ 频道订阅/取消订阅 - ✅ 心跳机制与自动重连 - ✅ 在线状态管理 - ✅ 系统通知推送 - ✅ 数据更新推送 - ✅ 通知持久化存储 - ✅ 已读/未读状态管理 - ✅ 批量操作支持 - ✅ **连接时自动授权(无需单独发送 auth 事件)** --- ## 功能特性 ### WebSocket 功能 | 功能 | 说明 | 状态 | |------|------|------| | 自动连接管理 | 登录后自动建立连接,退出时自动关闭 | ✅ | | 单例连接模式 | 全局只有一个 WebSocket 实例,避免重复连接 | ✅ | | 连接时授权 | 通过 URL 参数传递 token,握手时即完成认证 | ✅ | | 断线重连 | 连接断开自动重连(最多5次) | ✅ | | 心跳机制 | 客户端每30秒发送心跳保持连接 | ✅ | | 用户认证 | JWT token 验证,验证用户 ID 匹配和过期时间 | ✅ | | 点对点消息 | 发送消息给指定用户 | ✅ | | 广播消息 | 向所有在线用户发送消息 | ✅ | | 频道订阅 | 支持频道订阅和取消订阅 | ✅ | | 在线状态 | 实时获取用户在线状态 | ✅ | ### 通知功能 | 功能 | 说明 | 状态 | |------|------|------| | 通知发送 | 支持单个/批量/广播发送 | ✅ | | 通知类型 | info/success/warning/error/task/system | ✅ | | 通知分类 | system/task/message/reminder/announcement | ✅ | | 实时推送 | 在线用户通过 WebSocket 实时推送 | ✅ | | 持久化存储 | 所有通知保存到数据库 | ✅ | | 已读管理 | 支持标记已读(单个/批量/全部) | ✅ | | 通知删除 | 支持删除(单个/批量/清空) | ✅ | | 重试机制 | 发送失败自动重试(最多3次) | ✅ | | 统计分析 | 提供通知统计数据 | ✅ | --- ## 技术架构 ### 后端架构 ``` ┌─────────────────────────────────────────┐ │ Laravel + Laravel-S │ ├─────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────┐ │ │ │ WebSocketHandler │ │ │ │ (WebSocket 事件处理) │ │ │ │ - onOpen: 连接时授权 │ │ │ │ - onMessage: 消息路由 │ │ │ │ - onClose: 清理连接 │ │ │ └──────────┬───────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────┐ │ │ │ WebSocketService │ │ │ │ (WebSocket 管理) │ │ │ └──────────┬───────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────┐ │ │ │ NotificationService │ │ │ │ (通知服务) │ │ │ └──────────┬───────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────┐ │ │ │ Notification Model │ │ │ │ (通知模型) │ │ │ └──────────┬───────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────┐ │ │ │ Swoole Server │ │ │ │ (WebSocket 服务器) │ │ │ └──────────┬───────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────┐ │ │ │ MySQL Database │ │ │ │ (数据持久化) │ │ │ └──────────────────────────────┘ │ │ │ └─────────────────────────────────────────┘ ``` ### 前端架构 ``` ┌─────────────────────────────────────────┐ │ Vue 3 + Vite │ ├─────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────┐ │ │ │ App.vue (统一连接点) │ │ │ │ - 初始化 WebSocket │ │ │ │ - 监听用户状态 │ │ │ └──────────┬───────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────┐ │ │ │ useWebSocket Hook │ │ │ │ (单例管理 + 消息路由) │ │ │ └──────────┬───────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────┐ │ │ │ WebSocket Client │ │ │ │ (单例 + 重连 + 心跳) │ │ │ └──────────┬───────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────┐ │ │ │ Notification Store │ │ │ │ (Pinia 状态管理) │ │ │ └──────────┬───────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────┐ │ │ │ Userbar/Notification Page │ │ │ │ (展示 + 交互) │ │ │ └──────────────────────────────┘ │ │ │ └─────────────────────────────────────────┘ ``` --- ## 后端实现 ### 1. 数据模型 #### system_notifications 表 | 字段 | 类型 | 说明 | |------|------|------| | id | bigint | 主键ID | | user_id | bigint | 接收通知的用户ID | | title | varchar(255) | 通知标题 | | content | text | 通知内容 | | type | varchar(50) | 通知类型 | | category | varchar(50) | 通知分类 | | data | json | 附加数据 | | action_type | varchar(50) | 操作类型 | | action_data | text | 操作数据 | | is_read | boolean | 是否已读 | | read_at | timestamp | 阅读时间 | | sent_via_websocket | boolean | 是否已通过WebSocket发送 | | sent_at | timestamp | 发送时间 | | retry_count | int | 重试次数 | | created_at | timestamp | 创建时间 | | updated_at | timestamp | 更新时间 | | deleted_at | timestamp | 删除时间(软删除) | ### 2. 核心服务类 #### WebSocketHandler (`app/Services/WebSocket/WebSocketHandler.php`) WebSocket 处理器,实现 Swoole 的 `WebSocketHandlerInterface` 接口。 **主要方法:** - `onOpen()`: 处理连接建立事件,**连接时通过 URL 参数授权** - `onMessage()`: 处理消息接收事件 - `onClose()`: 处理连接关闭事件 **支持的消息类型:** - `ping/pong`: 心跳检测 - `heartbeat`: 心跳确认 - `chat`: 私聊消息 - `broadcast`: 广播消息 - `subscribe/unsubscribe`: 频道订阅/取消订阅 **连接时授权流程:** ```php public function onOpen(Server $server, Request $request): void { // 从 URL 查询参数获取认证信息 $userId = (int)($request->get['user_id'] ?? 0); $token = $request->get['token'] ?? ''; // 用户认证 if (!$userId || !$token) { $server->push($request->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '认证失败:缺少 user_id 或 token', 'code' => 401 ] ])); $server->disconnect($request->fd); return; } // 验证 JWT token try { $payload = JWTAuth::setToken($token)->getPayload(); // 验证 token 中的用户 ID 是否匹配 $tokenUserId = $payload['sub'] ?? null; if ($tokenUserId != $userId) { Log::warning('WebSocket 认证失败:用户 ID 不匹配', [ 'fd' => $request->fd, 'token_user_id' => $tokenUserId, 'query_user_id' => $userId ]); $server->push($request->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '认证失败:用户 ID 不匹配', 'code' => 401 ] ])); $server->disconnect($request->fd); return; } // 验证 token 是否过期 if (isset($payload['exp']) && $payload['exp'] < time()) { Log::warning('WebSocket 认证失败:token 已过期', [ 'fd' => $request->fd, 'user_id' => $userId, 'exp' => $payload['exp'], 'current_time' => time() ]); $server->push($request->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '认证失败:token 已过期', 'code' => 401 ] ])); $server->disconnect($request->fd); return; } Log::info('WebSocket 认证成功', [ 'fd' => $request->fd, 'user_id' => $userId ]); } catch (\Exception $e) { Log::warning('WebSocket 认证失败:无效的 token', [ 'fd' => $request->fd, 'user_id' => $userId, 'error' => $e->getMessage() ]); $server->push($request->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '认证失败:无效的 token', 'code' => 401 ] ])); $server->disconnect($request->fd); return; } // 认证成功,存储连接映射 $wsTable->set('uid:' . $userId, [ 'value' => $request->fd, 'expiry' => time() + 3600 ]); $wsTable->set('fd:' . $request->fd, [ 'value' => $userId, 'expiry' => time() + 3600 ]); // 发送欢迎消息 $server->push($request->fd, json_encode([ 'type' => 'connected', 'data' => [ 'message' => '欢迎连接到 LaravelS WebSocket', 'user_id' => $userId, 'fd' => $request->fd, 'timestamp' => time() ] ])); } ``` #### WebSocketService (`app/Services/WebSocket/WebSocketService.php`) WebSocket 服务类,提供便捷的 WebSocket 操作方法。 **主要方法:** ```php // 发送消息给指定用户 sendToUser(int $userId, array $data): bool // 发送消息给多个用户 sendToUsers(array $userIds, array $data): array // 广播消息给所有用户 broadcast(array $data, ?int $excludeUserId = null): int // 发送消息到频道 sendToChannel(string $channel, array $data): int // 获取在线用户数 getOnlineUserCount(): int // 检查用户是否在线 isUserOnline(int $userId): bool // 获取在线用户ID列表 getOnlineUserIds(): array // 断开用户连接 disconnectUser(int $userId): bool // 发送系统通知 sendSystemNotification(string $title, string $content, string $type): int // 发送通知给指定用户 sendNotificationToUsers(array $userIds, string $title, string $content, string $type): int // 推送数据更新 pushDataUpdate(array $userIds, string $resourceType, string $action, array $data): array // 推送数据更新到频道 pushDataUpdateToChannel(string $channel, string $resourceType, string $action, array $data): int ``` #### NotificationService (`app/Services/System/NotificationService.php`) 通知服务类,负责通知的创建、发送和管理。 **主要方法:** ```php // 发送通知给单个用户 sendToUser(int $userId, string $title, string $content, string $type, string $category, ?array $extraData = null): array // 发送通知给多个用户 sendToUsers(array $userIds, string $title, string $content, string $type, string $category, ?array $extraData = null): array // 广播通知给所有用户 broadcast(string $title, string $content, string $type, string $category, ?array $extraData = null): array // 发送任务通知 sendTaskNotification(int $userId, string $title, string $content, array $taskData): array // 发送消息通知 sendNewMessageNotification(int $userId, string $title, string $content, array $messageData): array // 发送提醒通知 sendReminderNotification(int $userId, string $title, string $content, array $reminderData): array // 标记通知为已读 markAsRead(int $notificationId): bool // 批量标记为已读 markMultipleAsRead(array $notificationIds): int // 标记所有通知为已读 markAllAsRead(int $userId): int // 删除通知 deleteNotification(int $notificationId): bool // 批量删除通知 deleteMultipleNotifications(array $notificationIds): int // 清空已读通知 clearReadNotifications(int $userId): int // 重试未发送的通知 retryUnsentNotifications(int $limit = 100): int // 获取通知统计 getStatistics(int $userId): array ``` ### 3. 控制器 #### NotificationController (`app/Http/Controllers/System/Admin/Notification.php`) 后台管理通知控制器,提供完整的 CRUD 操作。 **主要方法:** - `index()`: 获取通知列表 - `show()`: 获取通知详情 - `read()`: 标记通知为已读 - `batchRead()`: 批量标记为已读 - `readAll()`: 标记所有通知为已读 - `destroy()`: 删除通知 - `batchDelete()`: 批量删除通知 - `clearRead()`: 清空已读通知 - `unread()`: 获取未读通知列表 - `unreadCount()`: 获取未读通知数量 - `statistics()`: 获取通知统计 - `send()`: 发送通知(管理员) - `retryUnsent()`: 重试未发送的通知(管理员) #### WebSocketController (`app/Http/Controllers/System/WebSocket.php`) WebSocket API 控制器,提供 HTTP 接口用于管理 WebSocket 连接。 **主要方法:** - `getOnlineCount()`: 获取在线用户数 - `getOnlineUsers()`: 获取在线用户列表 - `checkOnline()`: 检查用户在线状态 - `sendToUser()`: 发送消息给指定用户 - `sendToUsers()`: 发送消息给多个用户 - `broadcast()`: 广播消息 - `sendToChannel()`: 发送消息到频道 - `sendNotification()`: 发送系统通知 - `sendNotificationToUsers()`: 发送通知给指定用户 - `pushDataUpdate()`: 推送数据更新 - `pushDataUpdateChannel()`: 推送数据更新到频道 - `disconnectUser()`: 断开用户连接 ### 4. 定时任务 #### RetryUnsentNotifications (`app/Console/Commands/RetryUnsentNotifications.php`) 自动重试发送失败的通知。 **使用方法:** ```bash # 重试最多100条未发送的通知 php artisan notifications:retry-unsent # 重试最多50条未发送的通知 php artisan notifications:retry-unsent --limit=50 ``` **定时任务配置:** ```php // config/crontab.php // 每5分钟重试一次未发送的通知 */5 * * * * php /path/to/artisan notifications:retry-unsent --limit=50 ``` --- ## 前端实现 ### 1. WebSocket 客户端 #### WebSocketClient (`resources/admin/src/utils/websocket.js`) WebSocket 客户端封装类,提供自动连接、重连、消息处理等功能。 **主要功能:** - 单例模式,确保全局只有一个实例 - 自动连接和重连 - 心跳机制 - 消息类型路由 - 事件监听 - 连接状态管理 - **连接时通过 URL 参数授权** **连接 URL 构建:** ```javascript export function createWebSocket(userId, token, options = {}) { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const host = options.wsUrl || window.location.host // ✅ 认证信息通过 URL 参数传递 const url = `${protocol}//${host}/ws?user_id=${userId}&token=${token}` return new WebSocketClient(url, options) } ``` **单例管理:** ```javascript let wsClient = null let currentUserId = null let currentToken = null let pendingConnection = null export function getWebSocket(userId, token, options = {}) { // 检查是否已有相同用户的连接 if (wsClient) { if (currentUserId === userId && currentToken === token) { const state = wsClient.getConnectionState() // 如果已连接或正在连接,返回现有实例 if (state === 'OPEN' || state === 'CONNECTING') { return wsClient } // 如果连接已关闭,清理 if (state === 'CLOSED' || state === 'CLOSING') { wsClient = null } } // 不同用户/Token,断开旧连接 if (currentUserId !== userId || currentToken !== token) { if (wsClient) { wsClient.disconnect() } wsClient = null } } // 创建新连接 currentUserId = userId currentToken = token wsClient = createWebSocket(userId, token, options) return wsClient } ``` **使用示例:** ```javascript import { getWebSocket, closeWebSocket } from '@/utils/websocket' import { useUserStore } from '@/stores/modules/user' const userStore = useUserStore() // 连接 WebSocket(连接时自动授权) const ws = getWebSocket(userStore.userInfo.id, userStore.token, { onOpen: (event) => { console.log('WebSocket 已连接(已通过 URL 参数完成认证)') }, onMessage: (message) => { console.log('收到消息:', message) }, onError: (error) => { console.error('WebSocket 错误:', error) }, onClose: (event) => { console.log('WebSocket 已关闭') } }) // 连接 ws.connect() // 发送消息 ws.send('heartbeat', { timestamp: Date.now() }) // 监听特定消息类型 ws.on('notification', (data) => { message.success(data.title, data.message) }) // 断开连接 ws.disconnect() ``` ### 2. useWebSocket Hook #### useWebSocket (`resources/admin/src/composables/useWebSocket.js`) WebSocket 组合式函数,统一管理 WebSocket 连接和消息处理。 **主要功能:** - 统一初始化 WebSocket(只在 App.vue 中调用) - 防止重复初始化 - 自动注册消息处理器 - 监听用户状态变化 - 连接时自动授权(无需发送 auth 事件) **关键实现:** ```javascript let wsInstance = null let isInitialized = false export function useWebSocket() { const userStore = useUserStore() const initWebSocket = () => { // 防止重复初始化 if (isInitialized && wsInstance && wsInstance.isConnected) { console.log('WebSocket 已连接,跳过重复初始化') return } isInitialized = true // 获取 WebSocket 实例(单例) wsInstance = getWebSocket(userStore.userInfo.id, userStore.token, { onOpen: handleOpen, onMessage: handleMessage, onError: handleError, onClose: handleClose }) // 注册消息处理器 registerMessageHandlers() // 连接 wsInstance.connect() } // 连接打开(已通过 URL 参数完成认证) function handleOpen(event) { console.log('WebSocket 连接已建立(已通过 URL 参数完成认证)', event) } return { ws: wsInstance, initWebSocket, closeWebSocket, reconnect, isConnected, send } } ``` ### 3. 通知 Store #### Notification Store (`resources/admin/src/stores/modules/notification.js`) 通知状态管理,基于 Pinia 实现。 **主要方法:** ```javascript // 获取未读数量 await notificationStore.fetchUnreadCount() // 获取未读通知列表 await notificationStore.fetchUnreadNotifications({ page: 1, page_size: 10 }) // 获取通知列表 await notificationStore.fetchNotifications({ page: 1, page_size: 20, type: 'notification', category: 'system' }) // 标记为已读 await notificationStore.markAsRead(notificationId) // 批量标记为已读 await notificationStore.markMultipleAsRead([1, 2, 3]) // 标记所有为已读 await notificationStore.markAllAsRead() // 删除通知 await notificationStore.deleteNotification(notificationId) // 批量删除 await notificationStore.deleteMultipleNotifications([1, 2, 3]) // 清空已读通知 await notificationStore.clearReadNotifications() // 发送通知(管理员) await notificationStore.sendNotification({ recipient_id: userId, title: '通知标题', content: '通知内容', type: 'info', category: 'system' }) // 重试未发送的通知(管理员) await notificationStore.retryUnsentNotifications(100) // 获取统计信息 await notificationStore.fetchStatistics() ``` ### 4. 用户栏通知组件 #### Userbar Component (`resources/admin/src/layouts/components/userbar.vue`) 顶部用户栏中的通知组件,提供: - 通知铃铛图标 - 未读数量徽章 - 通知下拉列表 - 快捷操作(全部已读、清空) - 通知分类筛选 - 点击标记已读 **功能特性:** - 未读消息数量实时更新 - 支持按类型筛选(全部/通知/任务/警告) - 悬浮显示删除按钮 - 分页浏览 - 本地缓存(localStorage) ### 5. 通知列表页面 #### Notification List Page (`resources/admin/src/pages/system/notifications/index.vue`) 通知管理页面,提供完整的通知管理功能。 **功能特性:** - 通知列表展示 - 搜索和筛选 - 批量操作 - 通知详情查看 - 已读/未读状态切换 - 删除和清空 --- ## API接口 ### WebSocket 接口 #### 1. 获取在线用户数 ``` GET /admin/websocket/online-count ``` **响应:** ```json { "code": 200, "message": "success", "data": { "online_count": 10 } } ``` #### 2. 获取在线用户列表 ``` GET /admin/websocket/online-users ``` **响应:** ```json { "code": 200, "message": "success", "data": { "user_ids": [1, 2, 3, 4, 5], "count": 5 } } ``` #### 3. 检查用户在线状态 ``` POST /admin/websocket/check-online ``` **请求参数:** ```json { "user_id": 1 } ``` **响应:** ```json { "code": 200, "message": "success", "data": { "user_id": 1, "is_online": true } } ``` #### 4. 发送消息给指定用户 ``` POST /admin/websocket/send-to-user ``` **请求参数:** ```json { "user_id": 1, "type": "notification", "data": { "title": "新消息", "message": "您有一条新消息" } } ``` #### 5. 发送消息给多个用户 ``` POST /admin/websocket/send-to-users ``` **请求参数:** ```json { "user_ids": [1, 2, 3], "type": "notification", "data": { "title": "系统通知", "message": "系统将在今晚进行维护" } } ``` #### 6. 广播消息 ``` POST /admin/websocket/broadcast ``` **请求参数:** ```json { "type": "notification", "data": { "title": "公告", "message": "欢迎使用新版本" }, "exclude_user_id": 1 // 可选:排除某个用户 } ``` #### 7. 发送消息到频道 ``` POST /admin/websocket/send-to-channel ``` **请求参数:** ```json { "channel": "orders", "type": "data_update", "data": { "order_id": 123, "status": "paid" } } ``` #### 8. 发送系统通知 ``` POST /admin/websocket/send-notification ``` **请求参数:** ```json { "title": "系统维护", "message": "系统将于今晚 23:00-24:00 进行维护", "type": "warning", "extra_data": { "start_time": "23:00", "end_time": "24:00" } } ``` #### 9. 发送通知给指定用户 ``` POST /admin/websocket/send-notification-to-users ``` **请求参数:** ```json { "user_ids": [1, 2, 3], "title": "订单更新", "message": "您的订单已发货", "type": "success" } ``` #### 10. 推送数据更新 ``` POST /admin/websocket/push-data-update ``` **请求参数:** ```json { "user_ids": [1, 2, 3], "resource_type": "order", "action": "update", "data": { "id": 123, "status": "shipped" } } ``` #### 11. 推送数据更新到频道 ``` POST /admin/websocket/push-data-update-channel ``` **请求参数:** ```json { "channel": "orders", "resource_type": "order", "action": "create", "data": { "id": 124, "customer": "张三", "amount": 100.00 } } ``` #### 12. 断开用户连接 ``` POST /admin/websocket/disconnect-user ``` **请求参数:** ```json { "user_id": 1 } ``` ### 通知接口 #### 1. 获取通知列表 ``` GET /admin/system/notifications ``` **请求参数:** ```json { "user_id": 1, // 用户ID(可选,默认为当前用户) "keyword": "通知", // 关键字搜索 "is_read": false, // 阅读状态(true/false) "type": "info", // 通知类型 "category": "system", // 通知分类 "start_date": "2024-01-01", // 开始日期 "end_date": "2024-12-31", // 结束日期 "page": 1, // 页码 "page_size": 20 // 每页数量 } ``` **响应:** ```json { "code": 200, "message": "success", "data": { "list": [ { "id": 1, "user_id": 1, "title": "系统通知", "content": "这是一个测试通知", "type": "info", "category": "system", "data": {}, "action_type": null, "action_data": null, "is_read": false, "read_at": null, "sent_via_websocket": true, "sent_at": "2024-02-18 10:00:00", "retry_count": 0, "created_at": "2024-02-18 10:00:00" } ], "total": 100, "page": 1, "page_size": 20 } } ``` #### 2. 获取未读通知 ``` GET /admin/system/notifications/unread?limit=10 ``` #### 3. 获取未读通知数量 ``` GET /admin/system/notifications/unread-count ``` **响应:** ```json { "code": 200, "message": "success", "data": { "count": 5 } } ``` #### 4. 获取通知详情 ``` GET /admin/system/notifications/{id} ``` #### 5. 标记通知为已读 ``` POST /admin/system/notifications/{id}/read ``` #### 6. 批量标记为已读 ``` POST /admin/system/notifications/batch-read ``` **请求参数:** ```json { "ids": [1, 2, 3, 4, 5] } ``` #### 7. 标记所有通知为已读 ``` POST /admin/system/notifications/read-all ``` #### 8. 删除通知 ``` DELETE /admin/system/notifications/{id} ``` #### 9. 批量删除通知 ``` POST /admin/system/notifications/batch-delete ``` **请求参数:** ```json { "ids": [1, 2, 3] } ``` #### 10. 清空已读通知 ``` POST /admin/system/notifications/clear-read ``` #### 11. 获取通知统计 ``` GET /admin/system/notifications/statistics ``` **响应:** ```json { "code": 200, "message": "success", "data": { "total": 100, "unread": 5, "read": 95, "by_type": { "info": 50, "success": 20, "warning": 15, "error": 10, "task": 5 }, "by_category": { "system": 60, "task": 20, "message": 10, "reminder": 8, "announcement": 2 } } } ``` #### 12. 发送通知(管理员功能) ``` POST /admin/system/notifications/send ``` **请求参数:** ```json { "user_ids": [1, 2, 3], // 用户ID数组,为空则发送给所有用户 "title": "系统维护通知", "content": "系统将于今晚22:00进行维护,预计维护时间2小时。", "type": "warning", "category": "announcement", "data": { "maintenance_start": "2024-02-18 22:00:00", "maintenance_end": "2024-02-19 00:00:00" }, "action_type": "link", "action_data": { "url": "/system/maintenance" } } ``` #### 13. 重试发送未发送的通知(管理员功能) ``` POST /admin/system/notifications/retry-unsent?limit=100 ``` --- ## 配置说明 ### 后端配置 #### Laravel-S 配置 (`config/laravels.php`) ```php 'websocket' => [ 'enable' => env('LARAVELS_WEBSOCKET', true), 'handler' => \App\Services\WebSocket\WebSocketHandler::class, ], 'swoole' => [ 'enable_coroutine' => true, 'worker_num' => 4, 'max_request' => 5000, 'max_request_grace' => 500, 'dispatch_mode' => 2, // 重要:使用抢占模式确保连接状态一致性 ], 'swoole_tables' => [ 'wsTable' => [ 'size' => 102400, 'column' => [ ['name' => 'value', 'type' => \Swoole\Table::TYPE_STRING, 'size' => 1024], ['name' => 'expiry', 'type' => \Swoole\Table::TYPE_INT, 'size' => 4], ], ], ], ``` #### 环境变量 在 `.env` 文件中添加: ```env LARAVELS_WEBSOCKET=true ``` **重要配置说明:** **dispatch_mode 模式详解** | 模式 | 值 | 说明 | 适用场景 | |------|-----|------|----------| | 轮询模式 | 1 | 按顺序依次分配请求到 Worker | 需要平均分配负载 | | 抢占模式 | 2 | 固定 Worker 处理特定连接 | **推荐:保持连接状态一致性** | | 抢占模式 | 3 | 随机分配请求到 Worker | ❌ 会导致状态不一致 | **dispatch_mode = 3 会导致的问题:** - 请求被随机分配到不同的 Worker 进程 - 同一用户的连接和消息发送可能被分配到不同 Worker - wsTable 中的用户连接数据和消息发送操作无法正确匹配 - 导致消息发送失败、通知无法接收 **dispatch_mode = 2 的优势:** - 确保同一用户的请求始终由同一个 Worker 处理 - 连接状态保持一致 - 消息发送可靠 ### 前端配置 #### WebSocket 配置 在 `resources/admin/src/utils/websocket.js` 中: ```javascript const WS_URL = 'ws://localhost:5200' // WebSocket 服务器地址 const RECONNECT_INTERVAL = 5000 // 重连间隔(毫秒) const MAX_RECONNECT_ATTEMPTS = 5 // 最大重连次数 const HEARTBEAT_INTERVAL = 30000 // 心跳间隔(毫秒) ``` #### 通知配置 在 `resources/admin/src/stores/modules/notification.js` 中: ```javascript const pageSize = 20 // 每页数量 const maxLocalNotifications = 100 // 本地最大存储数量 ``` ### Nginx 配置示例 ```nginx server { listen 80; server_name yourdomain.com; root /path/to/your/project/public; location / { try_files $uri $uri/ /index.php?$query_string; } # WebSocket 代理配置 location /ws { proxy_pass http://127.0.0.1:5200; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_read_timeout 86400; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } } ``` ### 定时任务配置 建议配置以下定时任务: ```php // config/crontab.php // 每5分钟重试一次未发送的通知 */5 * * * * php /path/to/artisan notifications:retry-unsent --limit=50 // 每天凌晨清理30天前的已读通知 0 0 * * * php /path/to/artisan notifications:cleanup ``` --- ## 使用示例 ### 后端使用示例 #### 1. 发送通知给单个用户 ```php use App\Services\System\NotificationService; use App\Models\System\Notification; class YourService { protected $notificationService; public function __construct(NotificationService $notificationService) { $this->notificationService = $notificationService; } public function someMethod() { // 发送通知给单个用户 $result = $this->notificationService->sendToUser( $userId = 1, $title = '欢迎加入', $content = '欢迎加入我们的系统!', $type = Notification::TYPE_SUCCESS, $category = Notification::CATEGORY_MESSAGE, $extraData = ['welcome' => true] ); } } ``` #### 2. 发送通知给多个用户 ```php // 发送通知给多个用户 $result = $this->notificationService->sendToUsers( $userIds = [1, 2, 3], $title = '系统维护通知', $content = '系统将于今晚进行维护', $type = Notification::TYPE_WARNING, $category = Notification::CATEGORY_ANNOUNCEMENT ); ``` #### 3. 广播通知 ```php // 广播通知(所有用户) $result = $this->notificationService->broadcast( $title = '新功能上线', $content = '我们推出了新的功能,快来体验吧!', $type = Notification::TYPE_INFO, $category = Notification::CATEGORY_ANNOUNCEMENT ); ``` #### 4. 发送任务通知 ```php // 发送任务通知 $result = $this->notificationService->sendTaskNotification( $userId = 1, $title = '任务提醒', $content = '您有一个任务即将到期', $taskData = [ 'task_id' => 123, 'task_name' => '完成报告', 'due_date' => '2024-02-20' ] ); ``` #### 5. 使用 WebSocket Service ```php use App\Services\WebSocket\WebSocketService; $wsService = app(WebSocketService::class); // 发送给单个用户 $wsService->sendToUser($userId, [ 'type' => 'notification', 'data' => [ 'title' => '系统通知', 'content' => '这是一条通知', 'type' => 'info', 'timestamp' => time() ] ]); // 发送给多个用户 $userIds = [1, 2, 3]; $sentTo = $wsService->sendToUsers($userIds, $data); // 广播给所有用户 $count = $wsService->broadcast($data); // 广播给所有用户(排除指定用户) $count = $wsService->broadcast($data, $excludeUserId); // 发送到频道 $count = $wsService->sendToChannel('orders', [ 'type' => 'data_update', 'data' => [ 'order_id' => 123, 'status' => 'paid' ] ]); // 推送数据更新 $sentTo = $wsService->pushDataUpdate( $userIds, 'dictionary', 'update', ['id' => 1, 'name' => 'test'] ); // 检查用户是否在线 $isOnline = $wsService->isUserOnline($userId); // 获取在线用户数量 $count = $wsService->getOnlineUserCount(); // 获取在线用户ID列表 $userIds = $wsService->getOnlineUserIds(); // 断开用户连接 $wsService->disconnectUser($userId); ``` ### 前端使用示例 #### 1. 基本连接(App.vue 中) ```vue ``` #### 2. 监听特定消息类型 ```javascript // 在 useWebSocket 中注册消息处理器 ws.on('notification', (data) => { message.success(data.title, data.message) }) ws.on('data_update', (data) => { console.log('数据更新:', data.resource_type, data.action) // 刷新数据 loadData() }) ws.on('chat', (data) => { console.log('收到聊天消息:', data) }) ``` #### 3. 发送消息 ```javascript import { useWebSocket } from '@/composables/useWebSocket' const { send } = useWebSocket() // 发送心跳 send('heartbeat', { timestamp: Date.now() }) // 发送私聊消息 send('chat', { to_user_id: 2, content: '你好,这是一条私聊消息' }) // 订阅频道 send('subscribe', { channel: 'orders' }) // 取消订阅 send('unsubscribe', { channel: 'orders' }) // 发送广播消息 send('broadcast', { message: '这是一条广播消息' }) ``` #### 4. 使用通知 Store ```javascript import { useNotificationStore } from '@/stores/modules/notification' const notificationStore = useNotificationStore() // 获取未读数量 await notificationStore.fetchUnreadCount() // 获取未读通知列表 const unreadList = await notificationStore.fetchUnreadNotifications({ page: 1, page_size: 10 }) // 获取通知列表 const list = await notificationStore.fetchNotifications({ page: 1, page_size: 20, type: 'notification', category: 'system' }) // 标记为已读 await notificationStore.markAsRead(notificationId) // 批量标记为已读 await notificationStore.markMultipleAsRead([1, 2, 3]) // 标记所有为已读 await notificationStore.markAllAsRead() // 删除通知 await notificationStore.deleteNotification(notificationId) // 批量删除 await notificationStore.deleteMultipleNotifications([1, 2, 3]) // 清空已读通知 await notificationStore.clearReadNotifications() // 获取统计信息 const stats = await notificationStore.fetchStatistics() console.log('统计信息:', stats) ``` #### 5. 在 Vue 组件中使用 ```vue 发送消息 连接状态: {{ connectionStatus }} ``` --- ## 连接问题修复 ### 问题描述 在开发过程中发现 WebSocket 连接存在重复创建的问题,导致: 1. 每个组件页面都创建新的 WebSocket 连接 2. 用户登录后可能同时存在多个 WebSocket 连接 3. 消息接收混乱,状态不一致 ### 根本原因 经过代码分析,发现以下**三个地方**都创建了 WebSocket 连接: 1. **App.vue(主要连接点)**:正确,应该在此处统一初始化 2. **userbar.vue(重复连接)**:错误,不应该重复初始化 3. **notifications/index.vue(重复连接)**:错误,不应该重复初始化 ### 解决方案 #### 原则 1. **单一连接点**:只在 `App.vue` 中统一初始化和关闭 WebSocket 连接 2. **全局状态管理**:通过 `useWebSocket` composable 管理单例 WebSocket 实例 3. **事件驱动**:组件通过监听 store 中的事件来处理 WebSocket 消息 4. **连接时授权**:通过 URL 参数传递认证信息,无需单独发送 auth 事件 #### 修复内容 ##### 1. websocket.js(单例模式) 使用单例模式确保全局只有一个 WebSocket 实例: ```javascript let wsClient = null let currentUserId = null let currentToken = null let pendingConnection = null export function getWebSocket(userId, token, options = {}) { // 检查是否已有相同用户的连接 if (wsClient) { if (currentUserId === userId && currentToken === token) { const state = wsClient.getConnectionState() // 如果已连接或正在连接,返回现有实例 if (state === 'OPEN' || state === 'CONNECTING') { return wsClient } // 如果连接已关闭,清理 if (state === 'CLOSED' || state === 'CLOSING') { wsClient = null } } // 不同用户/Token,断开旧连接 if (currentUserId !== userId || currentToken !== token) { if (wsClient) { wsClient.disconnect() } wsClient = null } } // 创建新连接 currentUserId = userId currentToken = token wsClient = createWebSocket(userId, token, options) return wsClient } ``` ##### 2. useWebSocket.js(统一初始化) 添加防止重复初始化的机制: ```javascript let wsInstance = null let isInitialized = false export function useWebSocket() { const initWebSocket = () => { // 防止重复初始化 if (isInitialized && wsInstance && wsInstance.isConnected) { console.log('WebSocket 已连接,跳过重复初始化') return } isInitialized = true wsInstance = getWebSocket(userStore.userInfo.id, userStore.token, { onOpen: handleOpen, onMessage: handleMessage, onError: handleError, onClose: handleClose }) // 注册消息处理器 registerMessageHandlers() wsInstance.connect() } return { ws: wsInstance, initWebSocket, closeWebSocket, reconnect, isConnected, send } } ``` ##### 3. 移除重复连接 **userbar.vue:** ```vue ``` **notifications/index.vue:** ```vue ``` ### WebSocket 授权机制优化 #### 优化前 ```javascript // 旧方式:前端在 handleOpen 中发送 auth 事件 function handleOpen(event) { console.log('WebSocket 连接已建立', event) // ❌ 需要单独发送 auth 事件 if (ws.value) { ws.value.send('auth', { token: userStore.token, user_id: userStore.userInfo.id }) } } ``` #### 优化后 ```javascript // 新方式:连接时通过 URL 参数认证 // URL: ws://host/ws?user_id=1&token=xxxxx function handleOpen(event) { console.log('WebSocket 连接已建立(已通过 URL 参数完成认证)', event) // ✅ 不需要发送 auth 事件 } ``` #### 优势 ✅ **性能优化**:减少 1 次消息往返,连接建立后立即可用 ✅ **安全性增强**:握手阶段即进行认证,未认证的连接直接拒绝 ✅ **代码简化**:前端无需处理 auth 事件,后端逻辑更清晰 #### 认证流程 ``` 前端发起连接 │ ├─ ws://host/ws?user_id=1&token=xxxxx │ ▼ 后端 onOpen │ ├─ 1. 提取 URL 参数(user_id, token) │ ├─ 2. 验证参数完整性 │ └─ 缺失 → 返回错误,断开连接 │ ├─ 3. 验证 JWT token │ ├─ 3.1 解析 token │ ├─ 3.2 验证用户 ID 匹配 │ │ └─ 不匹配 → 返回错误,断开连接 │ └─ 3.3 验证 token 是否过期 │ └─ 已过期 → 返回错误,断开连接 │ ├─ 4. 认证成功 │ │ │ ├─ 存储用户到 fd 映射 │ ├─ 存储 fd 到用户映射 │ └─ 发送 connected 消息 │ ▼ 连接建立完成,可以正常通信 ``` ### 架构说明 #### WebSocket 连接流程 ``` ┌─────────────────────────────────────────────────────────────┐ │ App.vue │ │ ┌───────────────────────────────────────────────────┐ │ │ │ useWebSocket() 初始化 │ │ │ │ - 获取用户信息 │ │ │ │ - 创建 WebSocket 连接(单例) │ │ │ │ - 连接时通过 URL 参数授权 │ │ │ │ - 注册消息处理器 │ │ │ └──────────────────┬────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────┐ │ │ │ WebSocket Client (单例) │ │ │ │ - 管理连接状态 │ │ │ │ - 处理消息路由 │ │ │ │ - 心跳检测和重连 │ │ │ └──────────────────┬────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────┐ │ │ │ Notification Store │ │ │ │ - 接收 WebSocket 消息 │ │ │ │ - 更新通知状态 │ │ │ │ - 触发 Vue 响应式更新 │ │ │ └──────────────────┬────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────┐ │ │ │ 组件 (userbar, notifications等) │ │ │ │ - 监听 store 状态变化 │ │ │ │ - 响应消息更新 │ │ │ └───────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` #### 消息处理流程 ``` 后端推送消息 │ ▼ WebSocketClient.onMessage() │ ▼ 消息类型路由 (switch/case) │ ├─► notification │ │ │ ▼ │ notificationStore.handleWebSocketMessage() │ │ │ ├─► 更新未读数量 │ ├─► 添加到通知列表 │ └─► 触发通知提示 │ ├─► data_update │ │ │ ▼ │ 自定义处理函数 │ └─► heartbeat │ ▼ 响应心跳确认 ``` ### 最佳实践 #### 1. WebSocket 连接管理 ✅ **推荐做法:** - 只在应用根组件(App.vue)中初始化 WebSocket - 使用单例模式确保全局只有一个实例 - 通过状态管理(Pinia Store)分发消息 ❌ **避免做法:** - 在多个组件中创建 WebSocket 连接 - 直接在子组件中调用 `initWebSocket()` - 绕过 Store 直接处理 WebSocket 消息 #### 2. 组件中使用 WebSocket ✅ **推荐做法:** ```javascript // 只需要监听 store 中的状态变化 import { useNotificationStore } from '@/stores/modules/notification' const notificationStore = useNotificationStore() // 监听未读数量变化 watch(() => notificationStore.unreadCount, (newCount) => { console.log('未读数量更新:', newCount) }) // 监听通知列表变化 watch(() => notificationStore.notifications, (list) => { console.log('通知列表更新:', list) }) ``` ❌ **避免做法:** ```javascript // 不要在组件中直接使用 WebSocket import { useWebSocket } from '@/composables/useWebSocket' const { initWebSocket } = useWebSocket() initWebSocket() // ❌ 重复连接! ``` #### 3. 消息处理器注册 ✅ **推荐做法:** ```javascript // 在 useWebSocket 中统一注册处理器 const initWebSocket = () => { wsInstance.on('notification', (data) => { notificationStore.handleWebSocketMessage({ type: 'notification', data }) }) wsInstance.on('data_update', (data) => { console.log('收到数据更新:', data) }) } ``` ❌ **避免做法:** ```javascript // 不要在每个组件中单独注册 ws.onMessage((msg) => { if (msg.type === 'notification') { // 处理通知 } }) ``` --- ## 故障排查与修复 ### 常见问题与解决方案 #### 问题 1:WebSocket 连接失败 **可能原因:** - WebSocket 服务未启动 - 端口被占用 - 防火墙阻止连接 - URL 配置错误 **排查步骤:** ```bash # 1. 检查 Laravel-S 是否运行 php bin/laravels status # 2. 启动 Laravel-S php bin/laravels start # 3. 检查端口是否被占用 netstat -ano | findstr :5200 # 4. 检查防火墙设置 # Windows: 控制面板 -> 系统和安全 -> Windows 防火墙 -> 允许应用通过防火墙 # Linux: sudo ufw allow 5200 ``` #### 问题 2:登录后 WebSocket 未连接 **可能原因:** - 用户信息未加载完成 - token 无效 **解决方法:** ```javascript // 检查控制台日志 // 确认 userStore.isUserInfoComplete() 返回 true // 查看 getWebSocket 调用参数 // 手动测试连接 import { useUserStore } from '@/stores/modules/user' const userStore = useUserStore() console.log('User Info:', userStore.userInfo) console.log('Token:', userStore.token) console.log('Is Complete:', userStore.isUserInfoComplete()) ``` #### 问题 3:消息未收到 **可能原因:** - 消息处理器未注册 - 消息类型不匹配 - 网络问题 - dispatch_mode 配置错误 **排查步骤:** ```bash # 1. 检查 dispatch_mode 配置 cat config/laravels.php | grep dispatch_mode # 应该是 dispatch_mode = 2 # 2. 完全重启 Laravel-S php bin/laravels stop && php bin/laravels start # 3. 检查消息处理器 # 查看 useWebSocket.js 中的消息处理器注册 # 4. 查看网络面板 WebSocket 帧 # 浏览器开发者工具 -> Network -> WS # 5. 查看日志 tail -f storage/logs/swoole.log tail -f storage/logs/laravel.log ``` #### 问题 4:消息发送失败 **可能原因:** - wsTable 访问方式错误 - 用户不在线 - dispatch_mode 配置错误 **错误信息:** ``` TypeError: Access to undefined property Swoole\WebSocket\Server::$wsTable ``` **正确修复方法:** 确保通过 `app('swoole')->wsTable` 而不是 `$server->wsTable` 访问。 ```php // ❌ 错误:$server 对象没有 wsTable 属性 $server->wsTable->set('uid:' . $userId, [...]); $fdInfo = $server->wsTable->get('fd:' . $fd); // ✅ 正确:通过服务容器获取 wsTable $wsTable = app('swoole')->wsTable; $wsTable->set('uid:' . $userId, [...]); $fdInfo = $wsTable->get('fd:' . $fd); ``` #### 问题 5:dispatch_mode 配置不生效 **检查方法:** ```bash # 1. 确认配置文件 cat config/laravels.php | grep dispatch_mode # 2. 完全重启 Laravel-S php bin/laravels stop && php bin/laravels start # 3. 检查运行时配置 php bin/laravels config ``` **正确配置:** ```php 'swoole' => [ 'dispatch_mode' => 2, // ✅ 正确:抢占模式 'worker_num' => 4, ], ``` #### 问题 6:通知未收到 **检查项:** - 用户是否在线 - WebSocket 连接是否正常 - 数据库中是否有通知记录 - `sent_via_websocket` 字段是否为 true **排查步骤:** ```bash # 1. 检查用户在线状态 php bin/laravels # 在控制台中执行: $wsService = app(App\Services\WebSocket\WebSocketService::class); $wsService->isUserOnline(1); # 2. 检查通知记录 php artisan tinker >>> $notifications = App\Models\System\Notification::where('user_id', 1)->get(); >>> $notifications->each(fn($n) => echo "ID: {$n->id}, Sent via WS: {$n->sent_via_websocket}\n"); # 3. 检查日志 tail -f storage/logs/swoole.log | grep "notification" ``` #### 问题 7:通知重复发送 **检查项:** - 是否有多个任务在重试 - `retry_count` 是否超过限制 - 是否有重复的创建逻辑 **解决方法:** ```php // 检查重试逻辑 // 通知最多重试3次,超过后不再重试 // 检查是否有重复的发送逻辑 // 确保不会多次调用 sendNotification ``` #### 问题 8:WebSocket 认证失败 **可能原因:** - Token 无效或已过期 - 用户 ID 不匹配 - 缺少认证参数 **排查步骤:** 检查前端控制台日志: ```javascript // 查看连接 URL 是否正确 console.log('WebSocket URL:', ws.url) // 查看收到的错误消息 // 检查错误类型和错误信息 ``` 检查后端日志: ```bash # 查看认证失败日志 tail -f storage/logs/laravel.log | grep "WebSocket 认证失败" ``` **解决方法:** - 确保 token 有效且未过期 - 确保 URL 参数中的 user_id 与 token 中的 sub 匹配 - 重新登录获取新的 token ### Laravel-S wsTable 使用规范 #### 正确的 wsTable 访问方式 根据 Laravel-S 文档和源码,正确访问 wsTable 的方式有两种: **方式 1:在 WebSocketHandler 中通过构造函数获取(推荐)** ```php class WebSocketHandler implements WebSocketHandlerInterface { protected $wsTable; public function __construct() { $this->wsTable = app('swoole')->wsTable; } public function onOpen(Server $server, Request $request): void { // 直接使用 $this->wsTable $this->wsTable->set('uid:' . $userId, [...]); } } ``` **方式 2:在普通 Service 中通过服务容器获取** ```php class WebSocketService { public function sendToUser(int $userId, array $data): bool { $server = $this->getServer(); // 每次都通过服务容器获取最新的 wsTable $wsTable = app('swoole')->wsTable; $fdInfo = $wsTable->get('uid:' . $userId); // ... } } ``` #### 错误的访问方式(禁止使用) ```php // ❌ 错误:$server 对象没有 wsTable 属性 $server->wsTable->set('uid:' . $userId, [...]); $fdInfo = $server->wsTable->get('fd:' . $fd); ``` **原因:** - `$server` 是 Swoole\WebSocket\Server 对象 - 该对象没有 `wsTable` 属性 - wsTable 是通过 Laravel-S 扩展动态添加到 Swoole Server 的 - Laravel-S 通过服务容器管理 wsTable,应该通过 `app('swoole')->wsTable` 访问 ### Swoole Table 数据结构 wsTable 用于存储 WebSocket 连接映射关系: ```php 'ws' => [ 'size' => 102400, // 表最大行数 'columns' => [ 'value' => ['type' => \Swoole\Table::TYPE_STRING, 'size' => 256], // 值 'expiry' => ['type' => \Swoole\Table::TYPE_INT, 'size' => 4], // 过期时间 ] ] ``` **存储的键值对:** - `uid:{userId}` → `['value' => {fd}, 'expiry' => timestamp]` - 用户 ID 到文件描述符的映射 - `fd:{fd}` → `['value' => {userId}, 'expiry' => timestamp]` - 文件描述符到用户 ID 的映射 - `channel:{channel}:fd:{fd}` → `['value' => 1, 'expiry' => timestamp]` - 频道订阅关系 ### 多 Worker 进程注意事项 当使用多个 Worker 进程时(worker_num > 1): 1. **进程隔离**:每个 Worker 有独立的内存空间 2. **状态同步**:使用 Swoole Table 实现跨进程数据共享 3. **连接一致性**:同一用户的连接必须由同一 Worker 处理 4. **消息路由**:dispatch_mode = 2 确保连接和消息在同一 Worker --- ## 性能优化 ### 后端优化 #### 1. 数据库优化 ```php // 为常用查询字段添加索引 Schema::table('system_notifications', function (Blueprint $table) { $table->index('user_id'); $table->index('is_read'); $table->index('type'); $table->index('category'); $table->index(['user_id', 'is_read']); $table->index(['user_id', 'created_at']); }); ``` #### 2. 批量操作 ```php // 使用批量插入减少查询次数 $notifications = []; foreach ($userIds as $userId) { $notifications[] = [ 'user_id' => $userId, 'title' => $title, 'content' => $content, 'type' => $type, 'category' => $category, 'created_at' => now(), 'updated_at' => now(), ]; } Notification::insert($notifications); ``` #### 3. 连接池管理 ```php // 定期清理过期连接 public function cleanExpiredConnections(): void { $server = $this->getServer(); $wsTable = app('swoole')->wsTable; $currentTime = time(); foreach ($wsTable as $key => $row) { if (strpos($key, 'uid:') === 0) { $fd = $row['value']; if (!$server->isEstablished($fd)) { // 清理无效的连接 $wsTable->del($key); $wsTable->del('fd:' . $fd); } } } } ``` #### 4. 消息队列 对于大量消息发送场景,建议使用队列异步处理: ```php use Illuminate\Support\Facades\Queue; // 异步发送通知 dispatch(function () use ($userIds, $message) { $webSocketService = new WebSocketService(); $webSocketService->sendNotificationToUsers($userIds, $title, $message); })->onQueue('websocket'); ``` #### 5. 缓存优化 ```php // 使用 Redis 缓存未读数量 use Illuminate\Support\Facades\Cache; public function getUnreadCount(int $userId): int { $cacheKey = "unread_count:{$userId}"; return Cache::remember($cacheKey, 300, function() use ($userId) { return Notification::where('user_id', $userId) ->where('is_read', false) ->count(); }); } // 通知标记为已读时清除缓存 public function markAsRead(int $notificationId): bool { $notification = Notification::find($notificationId); $notification->markAsRead(); // 清除缓存 Cache::forget("unread_count:{$notification->user_id}"); return true; } ``` ### 前端优化 #### 1. 消息限制 ```javascript // 限制本地存储数量 const maxLocalNotifications = 100 // 添加消息时检查数量 function addMessage(message) { if (messages.value.length >= maxLocalNotifications) { messages.value.pop() // 删除最旧的消息 } messages.value.unshift(message) } ``` #### 2. 分页加载 ```javascript // 消息列表使用分页,避免一次性加载过多数据 const currentPage = ref(1) const pageSize = ref(20) async function loadNotifications() { const response = await api.notifications.get({ page: currentPage.value, page_size: pageSize.value }) // ... } ``` #### 3. 虚拟滚动 对于大量消息列表,使用虚拟滚动提升性能: ```vue ``` #### 4. 防抖和节流 ```javascript // 搜索输入防抖 import { debounce } from 'lodash-es' const handleSearch = debounce((keyword) => { fetchNotifications({ keyword }) }, 300) // 滚动加载节流 import { throttle } from 'lodash-es' const handleScroll = throttle(() => { if (isNearBottom()) { loadMore() } }, 500) ``` --- ## 安全考虑 ### 后端安全 #### 1. 连接认证 ```php public function onOpen(Server $server, Request $request): void { $userId = $request->get['user_id'] ?? null; $token = $request->get['token'] ?? null; // ✅ 验证 token if (!$token || !$this->validateToken($userId, $token)) { $server->push($request->fd, json_encode([ 'type' => 'error', 'data' => ['message' => 'Authentication failed'] ])); $server->disconnect($request->fd); return; } // 认证成功,存储连接 $this->wsTable->set('uid:' . $userId, [ 'value' => $request->fd, 'expiry' => time() + 3600 ]); } ``` #### 2. 消息验证 ```php public function onMessage(Server $server, Frame $frame): void { $message = json_decode($frame->data, true); // ✅ 验证消息格式 if (!$message || !isset($message['type'])) { $server->push($frame->fd, json_encode([ 'type' => 'error', 'data' => ['message' => 'Invalid message format'] ])); return; } // 处理消息 // ... } ``` #### 3. 速率限制 ```php // 防止消息滥用 private $messageRateLimits = []; public function checkRateLimit(int $fd): bool { $key = 'fd:' . $fd; $now = time(); if (!isset($this->messageRateLimits[$key])) { $this->messageRateLimits[$key] = []; } // 清理旧记录 $this->messageRateLimits[$key] = array_filter( $this->messageRateLimits[$key], fn($time) => $time > $now - 60 ); // 检查频率(每分钟最多 60 条) if (count($this->messageRateLimits[$key]) >= 60) { return false; } $this->messageRateLimits[$key][] = $now; return true; } ``` #### 4. 权限控制 ```php // 确保用户只能查看和操作自己的通知 public function index(Request $request) { $userId = $request->user()->id; $notifications = Notification::where('user_id', $userId) ->where(function($query) use ($request) { // 应用搜索和筛选条件 if ($request->has('keyword')) { $query->where('title', 'like', '%' . $request->keyword . '%'); } // ... }) ->paginate($request->page_size ?? 20); return response()->json($notifications); } ``` #### 5. 数据安全 - 敏感信息不要放在通知内容中 - 使用参数验证和过滤 - SQL 注入防护(使用 Eloquent ORM) - XSS 防护(使用 `htmlspecialchars` 或类似函数) ### 前端安全 #### 1. Token 管理 ```javascript // 不要在前端硬编码 token // 从 store 中动态获取 import { useUserStore } from '@/stores/modules/user' const userStore = useUserStore() const ws = getWebSocket(userStore.userInfo.id, userStore.token) ``` #### 2. 消息过滤 ```javascript // 处理接收到的消息时,进行过滤和验证 function handleMessage(message) { // 验证消息格式 if (!message || !message.type || !message.data) { console.warn('Invalid message format:', message) return } // 过滤敏感内容 if (containsSensitiveContent(message.data)) { console.warn('Message contains sensitive content') return } // 处理消息 // ... } ``` #### 3. 连接限制 ```javascript // 限制每个用户的连接数量 const MAX_CONNECTIONS_PER_USER = 3 function canConnect(userId) { const existingConnections = getConnectionsByUser(userId) return existingConnections.length < MAX_CONNECTIONS_PER_USER } ``` --- ## 最佳实践 ### 1. 通知类型选择 - **info**: 一般信息,如欢迎消息、功能更新 - **success**: 成功操作,如创建成功、导入成功 - **warning**: 警告信息,如即将过期、维护通知 - **error**: 错误信息,如执行失败、验证错误 - **task**: 任务相关,如任务提醒、执行结果 - **system**: 系统级,如系统维护、重要公告 ### 2. 通知分类选择 - **system**: 系统管理、配置变更 - **task**: 定时任务、后台任务 - **message**: 用户消息、聊天消息 - **reminder**: 日程提醒、待办事项 - **announcement**: 公告、通知 ### 3. 避免通知轰炸 - 合理设置通知频率 - 对相似通知进行合并 - 提供通知偏好设置 - 允许用户关闭特定类型的通知 ### 4. 异步处理 ```php // 大量通知建议使用队列异步处理 use App\Jobs\SendNotificationJob; // 发送大量通知 dispatch(new SendNotificationJob( $userIds, $title, $content, $type, $category ))->onQueue('notifications'); ``` ### 5. 错误处理 ```javascript // 前端错误处理 async function markAsRead(notificationId) { try { await api.notifications.read(notificationId) // 更新本地状态 } catch (error) { console.error('标记已读失败:', error) // 显示错误提示 message.error('标记已读失败,请重试') } } ``` ### 6. 日志记录 ```php // 后端日志记录 use Illuminate\Support\Facades\Log; public function sendToUser(int $userId, array $data): bool { Log::info('Sending notification to user', [ 'user_id' => $userId, 'type' => $data['type'] ]); // ... 发送逻辑 Log::info('Notification sent successfully', [ 'user_id' => $userId, 'notification_id' => $notification->id ]); return true; } ``` ### 7. 测试 ```php // 单元测试示例 public function test_send_notification() { $result = $this->service->sendNotification([ 'user_id' => 1, 'title' => 'Test', 'content' => 'Test content' ]); $this->assertTrue($result['success']); $this->assertDatabaseHas('system_notifications', [ 'title' => 'Test' ]); } ``` --- ## 更新日志 ### 2024-02-18 - ✅ 初始版本发布 - ✅ 实现基础 WebSocket 功能 - ✅ 实现通知系统功能 - ✅ 实现消息推送功能 - ✅ 实现频道订阅功能 - ✅ 实现前端客户端封装 - ✅ 实现管理 API 接口 - ✅ 修复 dispatch_mode 配置问题 - ✅ 修复 wsTable 访问问题 - ✅ 添加定时任务重试功能 - ✅ 修复 WebSocket 重复连接问题 - ✅ 实现单例模式管理连接 - ✅ 优化授权机制(连接时自动认证) - ✅ 合并文档,提供完整的使用指南 --- ## 参考资料 - [Laravel-S 文档](https://github.com/hhxsv5/laravel-s) - [Swoole 文档](https://www.swoole.com/) - [WebSocket API](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket) - [JWT-Auth 文档](https://github.com/tymondesigns/jwt-auth) - [Laravel Modules 文档](https://nwidart.com/laravel-modules/) --- **文档版本:** v3.0 **更新日期:** 2024-02-18 **维护者:** Development Team