Files
laravel_swoole/docs/README_WEBSOCKET_NOTIFICATION.md
2026-02-18 19:41:03 +08:00

2208 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# WebSocket 与通知系统完整文档
## 目录
- [概述](#概述)
- [功能特性](#功能特性)
- [技术架构](#技术架构)
- [后端实现](#后端实现)
- [前端实现](#前端实现)
- [API接口](#api接口)
- [配置说明](#配置说明)
- [使用示例](#使用示例)
- [故障排查与修复](#故障排查与修复)
- [性能优化](#性能优化)
- [安全考虑](#安全考虑)
- [最佳实践](#最佳实践)
- [更新日志](#更新日志)
---
## 概述
本项目基于 Laravel-S 和 Swoole 实现了完整的 WebSocket 与通知系统,支持实时双向通信、消息推送、通知管理等功能。系统采用 WebSocket 实现实时推送,同时将通知持久化到数据库,确保离线用户也能收到通知。
### 核心特性
- ✅ 实时双向通信WebSocket
- ✅ 用户连接管理
- ✅ 点对点消息发送
- ✅ 群发消息/广播
- ✅ 频道订阅/取消订阅
- ✅ 心跳机制与自动重连
- ✅ 在线状态管理
- ✅ 系统通知推送
- ✅ 数据更新推送
- ✅ 通知持久化存储
- ✅ 已读/未读状态管理
- ✅ 批量操作支持
---
## 功能特性
### WebSocket 功能
| 功能 | 说明 | 状态 |
|------|------|------|
| 自动连接管理 | 登录后自动建立连接,退出时自动关闭 | ✅ |
| 断线重连 | 连接断开自动重连最多5次 | ✅ |
| 心跳机制 | 客户端每30秒发送心跳保持连接 | ✅ |
| 用户认证 | 通过 token 验证用户身份 | ✅ |
| 点对点消息 | 发送消息给指定用户 | ✅ |
| 广播消息 | 向所有在线用户发送消息 | ✅ |
| 频道订阅 | 支持频道订阅和取消订阅 | ✅ |
| 在线状态 | 实时获取用户在线状态 | ✅ |
### 通知功能
| 功能 | 说明 | 状态 |
|------|------|------|
| 通知发送 | 支持单个/批量/广播发送 | ✅ |
| 通知类型 | info/success/warning/error/task/system | ✅ |
| 通知分类 | system/task/message/reminder/announcement | ✅ |
| 实时推送 | 在线用户通过 WebSocket 实时推送 | ✅ |
| 持久化存储 | 所有通知保存到数据库 | ✅ |
| 已读管理 | 支持标记已读(单个/批量/全部) | ✅ |
| 通知删除 | 支持删除(单个/批量/清空) | ✅ |
| 重试机制 | 发送失败自动重试最多3次 | ✅ |
| 统计分析 | 提供通知统计数据 | ✅ |
---
## 技术架构
### 后端架构
```
┌─────────────────────────────────────────┐
│ Laravel + Laravel-S │
├─────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────┐ │
│ │ WebSocketHandler │ │
│ │ (WebSocket 事件处理) │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ WebSocketService │ │
│ │ (WebSocket 管理) │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ NotificationService │ │
│ │ (通知服务) │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ Notification Model │ │
│ │ (通知模型) │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ Swoole Server │ │
│ │ (WebSocket 服务器) │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ MySQL Database │ │
│ │ (数据持久化) │ │
│ └──────────────────────────────┘ │
│ │
└─────────────────────────────────────────┘
```
### 前端架构
```
┌─────────────────────────────────────────┐
│ Vue 3 + Vite │
├─────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────┐ │
│ │ Userbar Component │ │
│ │ (通知入口 + 徽章显示) │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ Notification Store │ │
│ │ (Pinia 状态管理) │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ System API │ │
│ │ (接口封装) │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ useWebSocket Hook │ │
│ │ (WebSocket 管理) │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ WebSocket Client │ │
│ └──────────────────────────────┘ │
│ │
└─────────────────────────────────────────┘
```
---
## 后端实现
### 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()`: 处理连接建立事件
- `onMessage()`: 处理消息接收事件
- `onClose()`: 处理连接关闭事件
**支持的消息类型:**
- `ping/pong`: 心跳检测
- `heartbeat`: 心跳确认
- `chat`: 私聊消息
- `broadcast`: 广播消息
- `subscribe/unsubscribe`: 频道订阅/取消订阅
#### 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 客户端封装类,提供自动连接、重连、消息处理等功能。
**主要功能:**
- 自动连接和重连
- 心跳机制
- 消息类型路由
- 事件监听
- 连接状态管理
**使用示例:**
```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 已连接')
},
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. 通知 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()
```
### 3. 通知 API
#### System API (`resources/admin/src/api/system.js`)
通知相关的 API 接口封装。
```javascript
// API 方法
export default {
// 通知列表
notifications: {
get: async function(params) {
return await request.get('admin/system/notifications', { params })
},
// 未读列表
unread: {
get: async function(params) {
return await request.get('admin/system/notifications/unread', { params })
}
},
// 未读数量
unreadCount: {
get: async function() {
return await request.get('admin/system/notifications/unread-count')
}
},
// 详情
show: async function(id) {
return await request.get(`admin/system/notifications/${id}`)
},
// 标记已读
read: async function(id) {
return await request.post(`admin/system/notifications/${id}/read`)
},
// 批量标记已读
batchRead: async function(data) {
return await request.post('admin/system/notifications/batch-read', data)
},
// 全部标记已读
readAll: async function() {
return await request.post('admin/system/notifications/read-all')
},
// 删除
delete: async function(id) {
return await request.delete(`admin/system/notifications/${id}`)
},
// 批量删除
batchDelete: async function(data) {
return await request.post('admin/system/notifications/batch-delete', data)
},
// 清空已读
clearRead: async function() {
return await request.post('admin/system/notifications/clear-read')
},
// 统计
statistics: {
get: async function() {
return await request.get('admin/system/notifications/statistics')
}
},
// 发送通知
send: async function(data) {
return await request.post('admin/system/notifications/send', data)
},
// 重试未发送
retryUnsent: async function(params) {
return await request.post('admin/system/notifications/retry-unsent', null, { params })
}
}
}
```
### 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. 基本连接
```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 已连接')
},
onMessage: (message) => {
console.log('收到消息:', message)
},
onError: (error) => {
console.error('WebSocket 错误:', error)
},
onClose: (event) => {
console.log('WebSocket 已关闭')
}
})
// 连接
ws.connect()
```
#### 2. 监听特定消息类型
```javascript
// 监听通知消息
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
// 发送心跳
ws.send('heartbeat', { timestamp: Date.now() })
// 发送私聊消息
ws.send('chat', {
to_user_id: 2,
content: '你好,这是一条私聊消息'
})
// 订阅频道
ws.send('subscribe', { channel: 'orders' })
// 取消订阅
ws.send('unsubscribe', { channel: 'orders' })
// 发送广播消息
ws.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
<template>
<div>
<a-button @click="connectWebSocket">连接 WebSocket</a-button>
<a-button @click="disconnectWebSocket">断开连接</a-button>
<a-button @click="sendMessage">发送消息</a-button>
<div>连接状态: {{ connectionStatus }}</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { getWebSocket } from '@/utils/websocket'
import { useUserStore } from '@/stores/modules/user'
const userStore = useUserStore()
const ws = ref(null)
const connectionStatus = ref('未连接')
const connectWebSocket = () => {
ws.value = getWebSocket(userStore.userInfo.id, userStore.token, {
onOpen: () => {
connectionStatus.value = '已连接'
},
onMessage: (message) => {
handleMessage(message)
},
onClose: () => {
connectionStatus.value = '已断开'
}
})
ws.value.connect()
}
const disconnectWebSocket = () => {
if (ws.value) {
ws.value.disconnect()
connectionStatus.value = '已断开'
}
}
const sendMessage = () => {
if (ws.value && ws.value.isConnected) {
ws.value.send('chat', {
to_user_id: 2,
content: '测试消息'
})
}
}
const handleMessage = (message) => {
switch (message.type) {
case 'notification':
message.success(message.data.title, message.data.message)
break
case 'data_update':
// 处理数据更新
break
case 'chat':
// 处理聊天消息
break
}
}
onMounted(() => {
connectWebSocket()
})
onUnmounted(() => {
disconnectWebSocket()
})
</script>
```
---
## 故障排查与修复
### 常见问题与解决方案
#### 问题 1WebSocket 连接失败
**可能原因:**
- 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);
```
#### 问题 5dispatch_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
```
### 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], // 值
]
]
```
**存储的键值对:**
- `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
<template>
<a-virtual-list
:data-sources="messages"
:data-key="'id'"
:keeps="30"
:item-size="60"
>
<template #default="{ data }">
<NotificationItem :notification="data" />
</template>
</a-virtual-list>
</template>
```
#### 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 访问问题
- ✅ 添加定时任务重试功能
---
## 参考资料
- [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/)
---
**文档版本:** v2.0
**更新日期:** 2024-02-18
**维护者:** Development Team