617 lines
15 KiB
Markdown
617 lines
15 KiB
Markdown
# WebSocket 实施计划
|
||
|
||
## 🎯 项目信息
|
||
|
||
- **项目名称**:英星AI - 浙江科技大学招生咨询系统
|
||
- **技术栈**:uni-app + Vue 2
|
||
- **后端**:ASP.NET Core(推测)
|
||
- **方案选择**:WebSocket 原生方案 ✅
|
||
|
||
---
|
||
|
||
## 📋 实施步骤
|
||
|
||
### 阶段一:前端准备(已完成 ✅)
|
||
|
||
#### 1. WebSocket 管理器
|
||
- ✅ 文件:`utils/websocket-manager.js`
|
||
- ✅ 功能:自动重连、心跳检测、消息队列、事件监听
|
||
|
||
#### 2. WebSocket 配置
|
||
- ✅ 文件:`config/websocket.config.js`
|
||
- ✅ 功能:集中管理 WebSocket 配置
|
||
|
||
#### 3. 使用文档
|
||
- ✅ 文件:`docs/WebSocket使用示例.md`
|
||
- ✅ 文件:`docs/实时通讯方案对比.md`
|
||
- ✅ 文件:`docs/实时通讯快速选型.md`
|
||
|
||
---
|
||
|
||
### 阶段二:后端开发(待实施 ⏳)
|
||
|
||
#### 选项 A:ASP.NET Core WebSocket 服务(推荐)
|
||
|
||
**时间**:3-5 天
|
||
|
||
**工作量:**
|
||
1. 创建 WebSocket 中间件(1 天)
|
||
2. 实现消息路由(1 天)
|
||
3. 集成用户认证(1 天)
|
||
4. 消息持久化(1 天)
|
||
5. 测试调试(1 天)
|
||
|
||
**文件清单:**
|
||
- `WebSocketMiddleware.cs` - WebSocket 中间件
|
||
- `WebSocketConnectionManager.cs` - 连接管理器
|
||
- `WebSocketMessageHandler.cs` - 消息处理器
|
||
- `ChatController.cs` - HTTP API(获取历史消息)
|
||
- `MessageRepository.cs` - 消息存储
|
||
|
||
**后端代码**:见下文 `.NET Core WebSocket 服务端代码`
|
||
|
||
#### 选项 B:使用现有 SignalR(备选)
|
||
|
||
你的项目已经有 SignalR:
|
||
```javascript
|
||
// main.js
|
||
var connection = new HubConnectionBuilder()
|
||
.withUrl("http://sl.vrgon.com:8003/ChatHub")
|
||
.build();
|
||
```
|
||
|
||
**建议:**
|
||
- 如果 SignalR 服务正常,可以继续使用
|
||
- 如果需要切换到原生 WebSocket,按选项 A 实施
|
||
|
||
---
|
||
|
||
### 阶段三:前端集成(2-3 天)
|
||
|
||
#### 1. 在 App.vue 初始化 WebSocket
|
||
|
||
**文件**:`App.vue`
|
||
|
||
```javascript
|
||
import wsManager from '@/utils/websocket-manager.js'
|
||
import wsConfig from '@/config/websocket.config.js'
|
||
|
||
export default {
|
||
onLaunch() {
|
||
// 登录后连接 WebSocket
|
||
if (this.vuex_token) {
|
||
this.connectWebSocket()
|
||
}
|
||
},
|
||
|
||
methods: {
|
||
connectWebSocket() {
|
||
// 构建 WebSocket URL(带 token)
|
||
const wsUrl = `${wsConfig.url}?token=${this.vuex_token}`
|
||
|
||
// 连接
|
||
wsManager.connect(wsUrl, {
|
||
reconnectInterval: wsConfig.reconnect.interval,
|
||
maxReconnectAttempts: wsConfig.reconnect.maxAttempts,
|
||
heartbeatInterval: wsConfig.heartbeat.interval
|
||
})
|
||
|
||
// 监听事件
|
||
this.setupWebSocketListeners()
|
||
},
|
||
|
||
setupWebSocketListeners() {
|
||
// 连接成功
|
||
wsManager.on('open', () => {
|
||
console.log('[App] WebSocket 已连接')
|
||
})
|
||
|
||
// 收到消息
|
||
wsManager.on('message', (data) => {
|
||
this.handleWebSocketMessage(data)
|
||
})
|
||
|
||
// 连接关闭
|
||
wsManager.on('close', () => {
|
||
console.log('[App] WebSocket 已断开')
|
||
})
|
||
},
|
||
|
||
handleWebSocketMessage(data) {
|
||
// 根据消息类型处理
|
||
switch (data.type) {
|
||
case 'message':
|
||
// 新消息提醒
|
||
this.$store.commit('addMessage', data)
|
||
break
|
||
case 'system':
|
||
// 系统通知
|
||
uni.showToast({ title: data.content, icon: 'none' })
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 2. 修改聊天页面
|
||
|
||
**文件**:`pages/message/dialogBox/dialogBox.vue`
|
||
|
||
需要:
|
||
1. 导入 WebSocket 管理器
|
||
2. 监听实时消息
|
||
3. 发送消息通过 WebSocket
|
||
4. 加载历史消息通过 HTTP API
|
||
|
||
**代码示例**:见 `docs/WebSocket使用示例.md`
|
||
|
||
#### 3. 添加聊天相关 API
|
||
|
||
**文件**:`common/http.api.js`
|
||
|
||
```javascript
|
||
// 获取聊天历史记录
|
||
let getChatHistory = (params = {}) => vm.$u.get('api/Chat/GetHistory', params);
|
||
|
||
// 发送离线消息(WebSocket 断开时使用)
|
||
let sendOfflineMessage = (params = {}) => vm.$u.post('api/Chat/SendOffline', params);
|
||
|
||
// 标记消息已读
|
||
let markMessageRead = (params = {}) => vm.$u.post('api/Chat/MarkRead', params);
|
||
```
|
||
|
||
---
|
||
|
||
### 阶段四:测试验证(2-3 天)
|
||
|
||
#### 1. 功能测试
|
||
|
||
- [ ] 连接建立
|
||
- [ ] 发送文字消息
|
||
- [ ] 接收文字消息
|
||
- [ ] 心跳保持
|
||
- [ ] 自动重连
|
||
- [ ] 离线消息推送
|
||
- [ ] 消息已读回执
|
||
|
||
#### 2. 场景测试
|
||
|
||
- [ ] 单人聊天
|
||
- [ ] 多人并发
|
||
- [ ] 网络断开
|
||
- [ ] 网络恢复
|
||
- [ ] App 切换后台
|
||
- [ ] App 恢复前台
|
||
- [ ] 登录/登出
|
||
|
||
#### 3. 性能测试
|
||
|
||
- [ ] 消息延迟 < 200ms
|
||
- [ ] 重连时间 < 5s
|
||
- [ ] 内存占用正常
|
||
- [ ] CPU 占用正常
|
||
- [ ] 100+ 并发用户
|
||
|
||
#### 4. 兼容性测试
|
||
|
||
- [ ] H5(Chrome、Safari)
|
||
- [ ] 微信小程序
|
||
- [ ] Android App
|
||
- [ ] iOS App
|
||
|
||
---
|
||
|
||
## 🔧 后端实现
|
||
|
||
### .NET Core WebSocket 服务端代码
|
||
|
||
#### 1. WebSocket 中间件
|
||
|
||
**文件**:`WebSocketMiddleware.cs`
|
||
|
||
```csharp
|
||
using System;
|
||
using System.Net.WebSockets;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using Microsoft.AspNetCore.Http;
|
||
using Newtonsoft.Json;
|
||
|
||
public class WebSocketMiddleware
|
||
{
|
||
private readonly RequestDelegate _next;
|
||
private readonly WebSocketConnectionManager _connectionManager;
|
||
|
||
public WebSocketMiddleware(RequestDelegate next, WebSocketConnectionManager connectionManager)
|
||
{
|
||
_next = next;
|
||
_connectionManager = connectionManager;
|
||
}
|
||
|
||
public async Task InvokeAsync(HttpContext context)
|
||
{
|
||
// 检查是否是 WebSocket 请求
|
||
if (!context.WebSockets.IsWebSocketRequest)
|
||
{
|
||
await _next(context);
|
||
return;
|
||
}
|
||
|
||
// 验证 token
|
||
var token = context.Request.Query["token"].ToString();
|
||
var userId = ValidateToken(token);
|
||
|
||
if (string.IsNullOrEmpty(userId))
|
||
{
|
||
context.Response.StatusCode = 401;
|
||
await context.Response.WriteAsync("Unauthorized");
|
||
return;
|
||
}
|
||
|
||
// 接受 WebSocket 连接
|
||
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||
|
||
// 保存连接
|
||
_connectionManager.AddConnection(userId, webSocket);
|
||
|
||
Console.WriteLine($"[WebSocket] 用户 {userId} 已连接");
|
||
|
||
try
|
||
{
|
||
await HandleWebSocketAsync(userId, webSocket);
|
||
}
|
||
finally
|
||
{
|
||
// 移除连接
|
||
_connectionManager.RemoveConnection(userId);
|
||
Console.WriteLine($"[WebSocket] 用户 {userId} 已断开");
|
||
}
|
||
}
|
||
|
||
private async Task HandleWebSocketAsync(string userId, WebSocket webSocket)
|
||
{
|
||
var buffer = new byte[1024 * 4];
|
||
|
||
while (webSocket.State == WebSocketState.Open)
|
||
{
|
||
var result = await webSocket.ReceiveAsync(
|
||
new ArraySegment<byte>(buffer),
|
||
CancellationToken.None
|
||
);
|
||
|
||
if (result.MessageType == WebSocketMessageType.Close)
|
||
{
|
||
await webSocket.CloseAsync(
|
||
WebSocketCloseStatus.NormalClosure,
|
||
"关闭连接",
|
||
CancellationToken.None
|
||
);
|
||
break;
|
||
}
|
||
|
||
// 解析消息
|
||
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||
await HandleMessageAsync(userId, message);
|
||
}
|
||
}
|
||
|
||
private async Task HandleMessageAsync(string userId, string message)
|
||
{
|
||
try
|
||
{
|
||
var data = JsonConvert.DeserializeObject<WebSocketMessage>(message);
|
||
|
||
switch (data.Type)
|
||
{
|
||
case "message":
|
||
// 处理聊天消息
|
||
await HandleChatMessageAsync(userId, data);
|
||
break;
|
||
|
||
case "ping":
|
||
// 处理心跳
|
||
await SendToUserAsync(userId, new
|
||
{
|
||
type = "pong",
|
||
timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds()
|
||
});
|
||
break;
|
||
|
||
case "read":
|
||
// 处理已读回执
|
||
await HandleReadReceiptAsync(userId, data);
|
||
break;
|
||
|
||
default:
|
||
Console.WriteLine($"未知消息类型: {data.Type}");
|
||
break;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"消息处理失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private async Task HandleChatMessageAsync(string fromUserId, WebSocketMessage data)
|
||
{
|
||
var toUserId = data.ToUserId;
|
||
var content = data.Content;
|
||
|
||
// 保存消息到数据库
|
||
var messageId = await SaveMessageAsync(fromUserId, toUserId, content);
|
||
|
||
// 构建消息对象
|
||
var messageObj = new
|
||
{
|
||
type = "message",
|
||
messageId = messageId,
|
||
fromUserId = fromUserId,
|
||
toUserId = toUserId,
|
||
content = content,
|
||
timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds()
|
||
};
|
||
|
||
// 发送给接收者
|
||
await SendToUserAsync(toUserId, messageObj);
|
||
|
||
// 发送给发送者(确认消息已发送)
|
||
await SendToUserAsync(fromUserId, messageObj);
|
||
}
|
||
|
||
private async Task<string> SaveMessageAsync(string fromUserId, string toUserId, string content)
|
||
{
|
||
// TODO: 保存消息到数据库
|
||
// 返回消息 ID
|
||
return Guid.NewGuid().ToString();
|
||
}
|
||
|
||
private async Task HandleReadReceiptAsync(string userId, WebSocketMessage data)
|
||
{
|
||
// TODO: 更新消息已读状态
|
||
Console.WriteLine($"用户 {userId} 已读消息 {data.MessageId}");
|
||
}
|
||
|
||
private async Task SendToUserAsync(string userId, object message)
|
||
{
|
||
var webSocket = _connectionManager.GetConnection(userId);
|
||
if (webSocket != null && webSocket.State == WebSocketState.Open)
|
||
{
|
||
var json = JsonConvert.SerializeObject(message);
|
||
var bytes = Encoding.UTF8.GetBytes(json);
|
||
await webSocket.SendAsync(
|
||
new ArraySegment<byte>(bytes),
|
||
WebSocketMessageType.Text,
|
||
true,
|
||
CancellationToken.None
|
||
);
|
||
}
|
||
}
|
||
|
||
private string ValidateToken(string token)
|
||
{
|
||
// TODO: 验证 JWT token,返回用户 ID
|
||
// 这里简化处理,直接返回一个用户 ID
|
||
return "user123";
|
||
}
|
||
}
|
||
|
||
// 消息实体
|
||
public class WebSocketMessage
|
||
{
|
||
public string Type { get; set; }
|
||
public string FromUserId { get; set; }
|
||
public string ToUserId { get; set; }
|
||
public string Content { get; set; }
|
||
public string MessageId { get; set; }
|
||
public long Timestamp { get; set; }
|
||
}
|
||
```
|
||
|
||
#### 2. 连接管理器
|
||
|
||
**文件**:`WebSocketConnectionManager.cs`
|
||
|
||
```csharp
|
||
using System.Collections.Concurrent;
|
||
using System.Net.WebSockets;
|
||
|
||
public class WebSocketConnectionManager
|
||
{
|
||
// 存储所有连接(用户 ID -> WebSocket)
|
||
private readonly ConcurrentDictionary<string, WebSocket> _connections =
|
||
new ConcurrentDictionary<string, WebSocket>();
|
||
|
||
// 添加连接
|
||
public void AddConnection(string userId, WebSocket webSocket)
|
||
{
|
||
_connections.TryAdd(userId, webSocket);
|
||
}
|
||
|
||
// 移除连接
|
||
public void RemoveConnection(string userId)
|
||
{
|
||
_connections.TryRemove(userId, out _);
|
||
}
|
||
|
||
// 获取连接
|
||
public WebSocket GetConnection(string userId)
|
||
{
|
||
_connections.TryGetValue(userId, out var webSocket);
|
||
return webSocket;
|
||
}
|
||
|
||
// 获取所有在线用户
|
||
public IEnumerable<string> GetOnlineUsers()
|
||
{
|
||
return _connections.Keys;
|
||
}
|
||
|
||
// 获取在线用户数
|
||
public int GetOnlineCount()
|
||
{
|
||
return _connections.Count;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3. 注册中间件
|
||
|
||
**文件**:`Startup.cs`
|
||
|
||
```csharp
|
||
public class Startup
|
||
{
|
||
public void ConfigureServices(IServiceCollection services)
|
||
{
|
||
// 注册连接管理器为单例
|
||
services.AddSingleton<WebSocketConnectionManager>();
|
||
|
||
// 其他服务...
|
||
}
|
||
|
||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||
{
|
||
// 启用 WebSocket
|
||
app.UseWebSockets(new WebSocketOptions
|
||
{
|
||
KeepAliveInterval = TimeSpan.FromSeconds(30)
|
||
});
|
||
|
||
// 注册 WebSocket 中间件
|
||
app.Map("/ws/chat", builder =>
|
||
{
|
||
builder.UseMiddleware<WebSocketMiddleware>();
|
||
});
|
||
|
||
// 其他中间件...
|
||
app.UseRouting();
|
||
app.UseAuthentication();
|
||
app.UseAuthorization();
|
||
app.UseEndpoints(endpoints =>
|
||
{
|
||
endpoints.MapControllers();
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4. HTTP API(获取历史消息)
|
||
|
||
**文件**:`ChatController.cs`
|
||
|
||
```csharp
|
||
[ApiController]
|
||
[Route("api/[controller]")]
|
||
public class ChatController : ControllerBase
|
||
{
|
||
// 获取聊天历史
|
||
[HttpGet("GetHistory")]
|
||
public async Task<IActionResult> GetHistory(string userId, int page = 1, int pageSize = 50)
|
||
{
|
||
// TODO: 从数据库查询历史消息
|
||
var messages = new List<object>
|
||
{
|
||
new
|
||
{
|
||
messageId = "msg001",
|
||
fromUserId = "user123",
|
||
toUserId = userId,
|
||
content = "你好",
|
||
timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds()
|
||
}
|
||
};
|
||
|
||
return Ok(new
|
||
{
|
||
success = true,
|
||
data = messages,
|
||
total = messages.Count
|
||
});
|
||
}
|
||
|
||
// 发送离线消息
|
||
[HttpPost("SendOffline")]
|
||
public async Task<IActionResult> SendOffline([FromBody] SendMessageRequest request)
|
||
{
|
||
// TODO: 保存离线消息到数据库
|
||
return Ok(new { success = true });
|
||
}
|
||
|
||
// 标记已读
|
||
[HttpPost("MarkRead")]
|
||
public async Task<IActionResult> MarkRead([FromBody] MarkReadRequest request)
|
||
{
|
||
// TODO: 更新消息已读状态
|
||
return Ok(new { success = true });
|
||
}
|
||
}
|
||
|
||
public class SendMessageRequest
|
||
{
|
||
public string ToUserId { get; set; }
|
||
public string Content { get; set; }
|
||
}
|
||
|
||
public class MarkReadRequest
|
||
{
|
||
public string MessageId { get; set; }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 时间安排
|
||
|
||
| 阶段 | 工作内容 | 预计时间 | 负责人 |
|
||
|------|----------|----------|--------|
|
||
| **阶段一** | 前端准备 | ✅ 已完成 | 前端 |
|
||
| **阶段二** | 后端开发 | 3-5 天 | 后端 |
|
||
| **阶段三** | 前端集成 | 2-3 天 | 前端 |
|
||
| **阶段四** | 测试验证 | 2-3 天 | 全员 |
|
||
| **总计** | | **7-11 天** | |
|
||
|
||
---
|
||
|
||
## 🎯 下一步行动
|
||
|
||
### 立即可做(前端)
|
||
|
||
1. **在 App.vue 中初始化 WebSocket**
|
||
- 导入 `websocket-manager.js`
|
||
- 在 `onLaunch` 中调用 `wsManager.connect()`
|
||
- 设置事件监听
|
||
|
||
2. **测试 WebSocket 管理器**
|
||
- 使用在线 WebSocket 测试服务(如 `wss://echo.websocket.org`)
|
||
- 验证连接、发送、接收功能
|
||
|
||
### 需要后端配合
|
||
|
||
1. **确认后端技术栈**
|
||
- 是否是 ASP.NET Core?
|
||
- 后端开发人员是谁?
|
||
|
||
2. **部署 WebSocket 服务**
|
||
- 使用上面提供的 .NET Core 代码
|
||
- 或者告诉我你的后端技术,我提供对应代码
|
||
|
||
3. **配置服务器**
|
||
- 确保服务器支持 WebSocket
|
||
- 配置 Nginx 反向代理(如果需要)
|
||
|
||
---
|
||
|
||
## 📞 联系与支持
|
||
|
||
如果你需要:
|
||
1. ✅ 其他后端语言的 WebSocket 服务端代码(Node.js/Java/Go/Python)
|
||
2. ✅ 帮助修改 `dialogBox.vue` 集成 WebSocket
|
||
3. ✅ Nginx 配置 WebSocket 反向代理
|
||
4. ✅ 调试和测试支持
|
||
|
||
随时告诉我!🚀
|
||
|