|
|
@@ -0,0 +1,1270 @@
|
|
|
+# 购物车实时通信使用说明书(SSE + WebSocket)
|
|
|
+
|
|
|
+## 📌 重要说明
|
|
|
+
|
|
|
+**SSE(Server-Sent Events)是单向通信**,只能从服务器推送到客户端,**不能从客户端发送消息到服务器**。
|
|
|
+
|
|
|
+如果您需要前端向服务器发送购物车变化,有两种方案:
|
|
|
+
|
|
|
+1. **方案一:SSE + HTTP API(推荐)**
|
|
|
+ - 前端通过HTTP API更新购物车
|
|
|
+ - 后端通过SSE推送更新给所有连接的客户端
|
|
|
+ - 简单、稳定、兼容性好
|
|
|
+
|
|
|
+2. **方案二:WebSocket(双向通信)**
|
|
|
+ - 前端可以直接通过WebSocket发送购物车变化
|
|
|
+ - 后端通过WebSocket推送更新给所有连接的客户端
|
|
|
+ - 支持双向实时通信
|
|
|
+
|
|
|
+**本系统同时支持两种方案,您可以根据需求选择使用。**
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+# 一、SSE(Server-Sent Events)使用说明
|
|
|
+
|
|
|
+## 一、SSE简介
|
|
|
+
|
|
|
+### 1.1 什么是SSE
|
|
|
+
|
|
|
+SSE(Server-Sent Events)是一种服务器推送技术,允许服务器主动向客户端推送数据。与WebSocket相比,SSE更简单、更轻量,特别适合单向数据推送场景。
|
|
|
+
|
|
|
+### 1.2 适用场景
|
|
|
+
|
|
|
+- ✅ 实时购物车更新(多人点餐场景)
|
|
|
+- ✅ 实时通知推送
|
|
|
+- ✅ 实时数据监控
|
|
|
+- ✅ 服务器主动推送消息
|
|
|
+
|
|
|
+### 1.3 技术特点
|
|
|
+
|
|
|
+- **单向通信**:服务器 → 客户端
|
|
|
+- **基于HTTP**:使用标准HTTP协议,无需额外协议
|
|
|
+- **自动重连**:浏览器自动处理连接断开和重连
|
|
|
+- **简单易用**:API简单,易于集成
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 二、后端实现说明
|
|
|
+
|
|
|
+### 2.1 接口地址
|
|
|
+
|
|
|
+```
|
|
|
+GET /api/store/order/sse/{tableId}
|
|
|
+Content-Type: text/event-stream
|
|
|
+```
|
|
|
+
|
|
|
+**参数说明:**
|
|
|
+- `tableId`:桌号ID(路径参数,必填)
|
|
|
+
|
|
|
+**响应类型:** `text/event-stream`
|
|
|
+
|
|
|
+### 2.2 后端实现代码
|
|
|
+
|
|
|
+#### 2.2.1 Controller层
|
|
|
+
|
|
|
+```java
|
|
|
+@ApiOperation(value = "建立SSE连接", notes = "建立SSE连接,用于接收购物车更新消息")
|
|
|
+@GetMapping(value = "/sse/{tableId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
|
|
+public SseEmitter createSseConnection(
|
|
|
+ @ApiParam(value = "桌号ID", required = true) @PathVariable Integer tableId
|
|
|
+) {
|
|
|
+ log.info("建立SSE连接, tableId={}", tableId);
|
|
|
+ return sseService.createConnection(tableId);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.2.2 Service层
|
|
|
+
|
|
|
+**接口定义:**
|
|
|
+```java
|
|
|
+public interface SseService {
|
|
|
+ /**
|
|
|
+ * 创建SSE连接
|
|
|
+ * @param tableId 桌号ID
|
|
|
+ * @return SSE连接对象
|
|
|
+ */
|
|
|
+ SseEmitter createConnection(Integer tableId);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 推送购物车更新消息
|
|
|
+ * @param tableId 桌号ID
|
|
|
+ * @param message 消息内容
|
|
|
+ */
|
|
|
+ void pushCartUpdate(Integer tableId, Object message);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 关闭SSE连接
|
|
|
+ * @param tableId 桌号ID
|
|
|
+ */
|
|
|
+ void closeConnection(Integer tableId);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.2.3 推送消息示例
|
|
|
+
|
|
|
+在购物车操作后推送更新:
|
|
|
+
|
|
|
+```java
|
|
|
+// 添加商品到购物车后
|
|
|
+CartDTO cart = cartService.addItem(dto);
|
|
|
+sseService.pushCartUpdate(dto.getTableId(), cart);
|
|
|
+
|
|
|
+// 更新购物车商品数量后
|
|
|
+CartDTO cart = cartService.updateQuantity(tableId, cuisineId, quantity);
|
|
|
+sseService.pushCartUpdate(tableId, cart);
|
|
|
+
|
|
|
+// 删除购物车商品后
|
|
|
+CartDTO cart = cartService.removeItem(tableId, cuisineId);
|
|
|
+sseService.pushCartUpdate(tableId, cart);
|
|
|
+
|
|
|
+// 清空购物车后
|
|
|
+cartService.clearCart(tableId);
|
|
|
+sseService.pushCartUpdate(tableId, new CartDTO());
|
|
|
+```
|
|
|
+
|
|
|
+### 2.3 消息事件类型
|
|
|
+
|
|
|
+后端会发送以下类型的事件:
|
|
|
+
|
|
|
+| 事件名称 | 说明 | 数据格式 |
|
|
|
+|---------|------|---------|
|
|
|
+| `connected` | 连接成功 | 字符串:"连接成功" |
|
|
|
+| `cart_update` | 购物车更新 | JSON格式的CartDTO对象 |
|
|
|
+| `heartbeat` | 心跳消息 | 字符串:"ping" |
|
|
|
+
|
|
|
+### 2.4 连接特性
|
|
|
+
|
|
|
+- **超时时间**:30分钟
|
|
|
+- **心跳间隔**:每30秒发送一次心跳
|
|
|
+- **多连接支持**:一个桌号可以同时有多个SSE连接(支持多人点餐)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 三、前端实现说明
|
|
|
+
|
|
|
+### 3.1 基础使用(原生JavaScript)
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 建立SSE连接
|
|
|
+const tableId = 123; // 桌号ID
|
|
|
+const eventSource = new EventSource(`/api/store/order/sse/${tableId}`);
|
|
|
+
|
|
|
+// 监听连接成功事件
|
|
|
+eventSource.addEventListener('connected', function(event) {
|
|
|
+ console.log('SSE连接成功:', event.data);
|
|
|
+ // event.data = "连接成功"
|
|
|
+});
|
|
|
+
|
|
|
+// 监听购物车更新事件
|
|
|
+eventSource.addEventListener('cart_update', function(event) {
|
|
|
+ console.log('购物车更新:', event.data);
|
|
|
+ // event.data 是JSON字符串,需要解析
|
|
|
+ const cart = JSON.parse(event.data);
|
|
|
+ updateCartUI(cart); // 更新购物车UI
|
|
|
+});
|
|
|
+
|
|
|
+// 监听心跳事件
|
|
|
+eventSource.addEventListener('heartbeat', function(event) {
|
|
|
+ console.log('收到心跳:', event.data);
|
|
|
+ // event.data = "ping"
|
|
|
+});
|
|
|
+
|
|
|
+// 监听错误事件
|
|
|
+eventSource.onerror = function(event) {
|
|
|
+ console.error('SSE连接错误:', event);
|
|
|
+ // 连接断开或出错时会触发
|
|
|
+ // 浏览器会自动尝试重连
|
|
|
+};
|
|
|
+
|
|
|
+// 关闭连接
|
|
|
+// eventSource.close();
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 Vue.js实现示例
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <div>
|
|
|
+ <div v-if="!connected">正在连接...</div>
|
|
|
+ <div v-else>已连接</div>
|
|
|
+ <!-- 购物车UI -->
|
|
|
+ <div v-for="item in cart.items" :key="item.cuisineId">
|
|
|
+ {{ item.cuisineName }} x {{ item.quantity }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ eventSource: null,
|
|
|
+ connected: false,
|
|
|
+ cart: {
|
|
|
+ items: [],
|
|
|
+ totalAmount: 0,
|
|
|
+ totalQuantity: 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.initSSE();
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ this.closeSSE();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // 初始化SSE连接
|
|
|
+ initSSE() {
|
|
|
+ const tableId = this.$route.params.tableId || 123;
|
|
|
+ const url = `/api/store/order/sse/${tableId}`;
|
|
|
+
|
|
|
+ this.eventSource = new EventSource(url);
|
|
|
+
|
|
|
+ // 连接成功
|
|
|
+ this.eventSource.addEventListener('connected', (event) => {
|
|
|
+ console.log('SSE连接成功');
|
|
|
+ this.connected = true;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 购物车更新
|
|
|
+ this.eventSource.addEventListener('cart_update', (event) => {
|
|
|
+ try {
|
|
|
+ const cart = JSON.parse(event.data);
|
|
|
+ this.cart = cart;
|
|
|
+ this.$message.success('购物车已更新');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('解析购物车数据失败:', error);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 心跳
|
|
|
+ this.eventSource.addEventListener('heartbeat', (event) => {
|
|
|
+ console.log('收到心跳');
|
|
|
+ });
|
|
|
+
|
|
|
+ // 错误处理
|
|
|
+ this.eventSource.onerror = (error) => {
|
|
|
+ console.error('SSE连接错误:', error);
|
|
|
+ this.connected = false;
|
|
|
+ // 可以在这里实现重连逻辑
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ // 关闭SSE连接
|
|
|
+ closeSSE() {
|
|
|
+ if (this.eventSource) {
|
|
|
+ this.eventSource.close();
|
|
|
+ this.eventSource = null;
|
|
|
+ this.connected = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+```
|
|
|
+
|
|
|
+### 3.3 React实现示例
|
|
|
+
|
|
|
+```jsx
|
|
|
+import React, { useEffect, useState, useRef } from 'react';
|
|
|
+
|
|
|
+function CartComponent({ tableId }) {
|
|
|
+ const [connected, setConnected] = useState(false);
|
|
|
+ const [cart, setCart] = useState({ items: [], totalAmount: 0, totalQuantity: 0 });
|
|
|
+ const eventSourceRef = useRef(null);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ // 建立SSE连接
|
|
|
+ const url = `/api/store/order/sse/${tableId}`;
|
|
|
+ const eventSource = new EventSource(url);
|
|
|
+ eventSourceRef.current = eventSource;
|
|
|
+
|
|
|
+ // 连接成功
|
|
|
+ eventSource.addEventListener('connected', (event) => {
|
|
|
+ console.log('SSE连接成功');
|
|
|
+ setConnected(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 购物车更新
|
|
|
+ eventSource.addEventListener('cart_update', (event) => {
|
|
|
+ try {
|
|
|
+ const cartData = JSON.parse(event.data);
|
|
|
+ setCart(cartData);
|
|
|
+ console.log('购物车已更新');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('解析购物车数据失败:', error);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 心跳
|
|
|
+ eventSource.addEventListener('heartbeat', (event) => {
|
|
|
+ console.log('收到心跳');
|
|
|
+ });
|
|
|
+
|
|
|
+ // 错误处理
|
|
|
+ eventSource.onerror = (error) => {
|
|
|
+ console.error('SSE连接错误:', error);
|
|
|
+ setConnected(false);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 清理函数
|
|
|
+ return () => {
|
|
|
+ if (eventSourceRef.current) {
|
|
|
+ eventSourceRef.current.close();
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }, [tableId]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div>
|
|
|
+ <div>{connected ? '已连接' : '正在连接...'}</div>
|
|
|
+ <div>
|
|
|
+ {cart.items.map(item => (
|
|
|
+ <div key={item.cuisineId}>
|
|
|
+ {item.cuisineName} x {item.quantity}
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ <div>总金额: {cart.totalAmount}</div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export default CartComponent;
|
|
|
+```
|
|
|
+
|
|
|
+### 3.4 微信小程序实现示例
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 微信小程序不支持EventSource,需要使用wx.request或第三方库
|
|
|
+// 方案1:使用wx.request轮询(不推荐,但可用)
|
|
|
+// 方案2:使用WebSocket(推荐)
|
|
|
+// 方案3:使用第三方SSE库
|
|
|
+
|
|
|
+// 这里提供一个简单的轮询方案作为备选
|
|
|
+let pollingTimer = null;
|
|
|
+
|
|
|
+function startSSEPolling(tableId, onMessage) {
|
|
|
+ const poll = () => {
|
|
|
+ wx.request({
|
|
|
+ url: `https://your-api.com/api/store/order/cart/${tableId}`,
|
|
|
+ method: 'GET',
|
|
|
+ success: (res) => {
|
|
|
+ if (res.data && res.data.code === 200) {
|
|
|
+ onMessage(res.data.data); // 购物车数据
|
|
|
+ }
|
|
|
+ },
|
|
|
+ complete: () => {
|
|
|
+ // 每2秒轮询一次
|
|
|
+ pollingTimer = setTimeout(poll, 2000);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ poll();
|
|
|
+}
|
|
|
+
|
|
|
+function stopSSEPolling() {
|
|
|
+ if (pollingTimer) {
|
|
|
+ clearTimeout(pollingTimer);
|
|
|
+ pollingTimer = null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 使用
|
|
|
+Page({
|
|
|
+ onLoad(options) {
|
|
|
+ const tableId = options.tableId;
|
|
|
+ startSSEPolling(tableId, (cart) => {
|
|
|
+ this.setData({ cart });
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ onUnload() {
|
|
|
+ stopSSEPolling();
|
|
|
+ }
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 四、消息数据格式
|
|
|
+
|
|
|
+### 4.1 购物车更新消息(cart_update)
|
|
|
+
|
|
|
+**数据格式:** JSON字符串,解析后为CartDTO对象
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "tableId": 123,
|
|
|
+ "tableNumber": "A01",
|
|
|
+ "storeId": 1,
|
|
|
+ "items": [
|
|
|
+ {
|
|
|
+ "cuisineId": 1001,
|
|
|
+ "cuisineName": "宫保鸡丁",
|
|
|
+ "cuisineType": 1,
|
|
|
+ "cuisineImage": "https://example.com/image.jpg",
|
|
|
+ "unitPrice": 38.00,
|
|
|
+ "quantity": 2,
|
|
|
+ "subtotalAmount": 76.00,
|
|
|
+ "addUserId": 100,
|
|
|
+ "addUserPhone": "13800138000",
|
|
|
+ "remark": "不要花生"
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "totalAmount": 76.00,
|
|
|
+ "totalQuantity": 2
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**字段说明:**
|
|
|
+
|
|
|
+| 字段 | 类型 | 说明 |
|
|
|
+|-----|------|------|
|
|
|
+| tableId | Integer | 桌号ID |
|
|
|
+| tableNumber | String | 桌号 |
|
|
|
+| storeId | Integer | 门店ID |
|
|
|
+| items | Array | 购物车商品列表 |
|
|
|
+| totalAmount | BigDecimal | 总金额 |
|
|
|
+| totalQuantity | Integer | 商品总数量 |
|
|
|
+
|
|
|
+**CartItemDTO字段说明:**
|
|
|
+
|
|
|
+| 字段 | 类型 | 说明 |
|
|
|
+|-----|------|------|
|
|
|
+| cuisineId | Integer | 菜品ID |
|
|
|
+| cuisineName | String | 菜品名称 |
|
|
|
+| cuisineType | Integer | 菜品类型(1:单品, 2:套餐) |
|
|
|
+| cuisineImage | String | 菜品图片 |
|
|
|
+| unitPrice | BigDecimal | 单价 |
|
|
|
+| quantity | Integer | 数量 |
|
|
|
+| subtotalAmount | BigDecimal | 小计金额 |
|
|
|
+| addUserId | Integer | 添加该菜品的用户ID |
|
|
|
+| addUserPhone | String | 添加该菜品的用户手机号 |
|
|
|
+| remark | String | 备注 |
|
|
|
+
|
|
|
+### 4.2 连接成功消息(connected)
|
|
|
+
|
|
|
+```json
|
|
|
+"连接成功"
|
|
|
+```
|
|
|
+
|
|
|
+### 4.3 心跳消息(heartbeat)
|
|
|
+
|
|
|
+```json
|
|
|
+"ping"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 五、错误处理
|
|
|
+
|
|
|
+### 5.1 连接错误
|
|
|
+
|
|
|
+```javascript
|
|
|
+eventSource.onerror = function(event) {
|
|
|
+ console.error('SSE连接错误:', event);
|
|
|
+
|
|
|
+ // 检查连接状态
|
|
|
+ if (eventSource.readyState === EventSource.CLOSED) {
|
|
|
+ console.log('连接已关闭');
|
|
|
+ // 可以在这里实现重连逻辑
|
|
|
+ reconnect();
|
|
|
+ } else if (eventSource.readyState === EventSource.CONNECTING) {
|
|
|
+ console.log('正在重连...');
|
|
|
+ }
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 重连机制
|
|
|
+
|
|
|
+```javascript
|
|
|
+let reconnectAttempts = 0;
|
|
|
+const maxReconnectAttempts = 5;
|
|
|
+let reconnectTimer = null;
|
|
|
+
|
|
|
+function reconnect() {
|
|
|
+ if (reconnectAttempts >= maxReconnectAttempts) {
|
|
|
+ console.error('达到最大重连次数,停止重连');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ reconnectAttempts++;
|
|
|
+ console.log(`尝试重连 (${reconnectAttempts}/${maxReconnectAttempts})`);
|
|
|
+
|
|
|
+ reconnectTimer = setTimeout(() => {
|
|
|
+ initSSE(); // 重新初始化SSE连接
|
|
|
+ }, 3000 * reconnectAttempts); // 指数退避
|
|
|
+}
|
|
|
+
|
|
|
+function initSSE() {
|
|
|
+ const eventSource = new EventSource(url);
|
|
|
+
|
|
|
+ eventSource.addEventListener('connected', () => {
|
|
|
+ reconnectAttempts = 0; // 重置重连次数
|
|
|
+ clearTimeout(reconnectTimer);
|
|
|
+ });
|
|
|
+
|
|
|
+ eventSource.onerror = () => {
|
|
|
+ reconnect();
|
|
|
+ };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5.3 数据解析错误
|
|
|
+
|
|
|
+```javascript
|
|
|
+eventSource.addEventListener('cart_update', function(event) {
|
|
|
+ try {
|
|
|
+ const cart = JSON.parse(event.data);
|
|
|
+ updateCartUI(cart);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('解析购物车数据失败:', error);
|
|
|
+ // 可以显示错误提示或使用默认值
|
|
|
+ showError('购物车数据解析失败,请刷新页面');
|
|
|
+ }
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 六、最佳实践
|
|
|
+
|
|
|
+### 6.1 连接管理
|
|
|
+
|
|
|
+1. **页面加载时建立连接**
|
|
|
+ ```javascript
|
|
|
+ mounted() {
|
|
|
+ this.initSSE();
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+2. **页面卸载时关闭连接**
|
|
|
+ ```javascript
|
|
|
+ beforeDestroy() {
|
|
|
+ this.closeSSE();
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+3. **路由切换时关闭旧连接**
|
|
|
+ ```javascript
|
|
|
+ watch: {
|
|
|
+ '$route'(to, from) {
|
|
|
+ if (to.params.tableId !== from.params.tableId) {
|
|
|
+ this.closeSSE();
|
|
|
+ this.initSSE();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+### 6.2 性能优化
|
|
|
+
|
|
|
+1. **避免频繁更新UI**
|
|
|
+ ```javascript
|
|
|
+ let updateTimer = null;
|
|
|
+ eventSource.addEventListener('cart_update', function(event) {
|
|
|
+ // 防抖处理,避免频繁更新
|
|
|
+ clearTimeout(updateTimer);
|
|
|
+ updateTimer = setTimeout(() => {
|
|
|
+ const cart = JSON.parse(event.data);
|
|
|
+ updateCartUI(cart);
|
|
|
+ }, 100);
|
|
|
+ });
|
|
|
+ ```
|
|
|
+
|
|
|
+2. **使用虚拟滚动(如果商品很多)**
|
|
|
+ ```javascript
|
|
|
+ // 使用vue-virtual-scroll-list等库
|
|
|
+ ```
|
|
|
+
|
|
|
+### 6.3 用户体验
|
|
|
+
|
|
|
+1. **显示连接状态**
|
|
|
+ ```javascript
|
|
|
+ <div v-if="connected" class="status connected">已连接</div>
|
|
|
+ <div v-else class="status disconnected">连接中...</div>
|
|
|
+ ```
|
|
|
+
|
|
|
+2. **显示更新提示**
|
|
|
+ ```javascript
|
|
|
+ eventSource.addEventListener('cart_update', function(event) {
|
|
|
+ const cart = JSON.parse(event.data);
|
|
|
+ updateCartUI(cart);
|
|
|
+ showToast('购物车已更新'); // 显示提示
|
|
|
+ });
|
|
|
+ ```
|
|
|
+
|
|
|
+3. **处理网络异常**
|
|
|
+ ```javascript
|
|
|
+ eventSource.onerror = function(event) {
|
|
|
+ if (navigator.onLine === false) {
|
|
|
+ showError('网络连接已断开,请检查网络设置');
|
|
|
+ }
|
|
|
+ };
|
|
|
+ ```
|
|
|
+
|
|
|
+### 6.4 安全性
|
|
|
+
|
|
|
+1. **验证tableId权限**
|
|
|
+ - 后端需要验证用户是否有权限访问该桌号的SSE连接
|
|
|
+ - 可以在请求头中添加token进行验证
|
|
|
+
|
|
|
+2. **防止XSS攻击**
|
|
|
+ - 确保后端返回的数据是安全的
|
|
|
+ - 前端对接收到的数据进行验证和转义
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 七、常见问题
|
|
|
+
|
|
|
+### 7.1 连接立即断开
|
|
|
+
|
|
|
+**可能原因:**
|
|
|
+- 后端未正确设置`Content-Type: text/event-stream`
|
|
|
+- 后端连接超时设置过短
|
|
|
+- 网络代理或防火墙阻止了SSE连接
|
|
|
+
|
|
|
+**解决方案:**
|
|
|
+- 检查后端Controller的`produces`属性
|
|
|
+- 检查后端超时设置(当前为30分钟)
|
|
|
+- 检查网络配置
|
|
|
+
|
|
|
+### 7.2 收不到消息
|
|
|
+
|
|
|
+**可能原因:**
|
|
|
+- 事件名称不匹配
|
|
|
+- 数据格式错误
|
|
|
+- 连接已断开
|
|
|
+
|
|
|
+**解决方案:**
|
|
|
+- 检查事件名称是否正确(`cart_update`)
|
|
|
+- 检查数据是否为有效的JSON
|
|
|
+- 检查连接状态
|
|
|
+
|
|
|
+### 7.3 心跳消息过多
|
|
|
+
|
|
|
+**说明:**
|
|
|
+- 心跳消息每30秒发送一次,用于保持连接
|
|
|
+- 如果不需要处理心跳,可以忽略该事件
|
|
|
+
|
|
|
+**解决方案:**
|
|
|
+```javascript
|
|
|
+// 不监听heartbeat事件即可
|
|
|
+// eventSource.addEventListener('heartbeat', ...); // 注释掉
|
|
|
+```
|
|
|
+
|
|
|
+### 7.4 多个用户同时点餐
|
|
|
+
|
|
|
+**说明:**
|
|
|
+- 一个桌号可以同时有多个SSE连接
|
|
|
+- 每个用户的购物车操作都会推送给该桌号的所有连接
|
|
|
+
|
|
|
+**实现:**
|
|
|
+- 后端已支持多连接,无需额外配置
|
|
|
+- 前端每个用户建立自己的SSE连接即可
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 八、测试建议
|
|
|
+
|
|
|
+### 8.1 功能测试
|
|
|
+
|
|
|
+1. **连接测试**
|
|
|
+ - 测试正常连接
|
|
|
+ - 测试连接超时
|
|
|
+ - 测试连接断开重连
|
|
|
+
|
|
|
+2. **消息推送测试**
|
|
|
+ - 测试购物车添加商品
|
|
|
+ - 测试购物车更新数量
|
|
|
+ - 测试购物车删除商品
|
|
|
+ - 测试清空购物车
|
|
|
+
|
|
|
+3. **多用户测试**
|
|
|
+ - 测试多个用户同时连接
|
|
|
+ - 测试一个用户操作,其他用户是否收到推送
|
|
|
+
|
|
|
+### 8.2 性能测试
|
|
|
+
|
|
|
+1. **连接数测试**
|
|
|
+ - 测试单个桌号最多支持多少个连接
|
|
|
+ - 测试系统最多支持多少个连接
|
|
|
+
|
|
|
+2. **消息推送测试**
|
|
|
+ - 测试高频推送场景
|
|
|
+ - 测试大量数据推送
|
|
|
+
|
|
|
+### 8.3 兼容性测试
|
|
|
+
|
|
|
+1. **浏览器兼容性**
|
|
|
+ - Chrome/Edge(支持)
|
|
|
+ - Firefox(支持)
|
|
|
+ - Safari(支持)
|
|
|
+ - IE(不支持,需要使用polyfill)
|
|
|
+
|
|
|
+2. **移动端测试**
|
|
|
+ - iOS Safari
|
|
|
+ - Android Chrome
|
|
|
+ - 微信内置浏览器
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 九、API参考
|
|
|
+
|
|
|
+### 9.1 建立SSE连接
|
|
|
+
|
|
|
+```
|
|
|
+GET /api/store/order/sse/{tableId}
|
|
|
+```
|
|
|
+
|
|
|
+**请求参数:**
|
|
|
+- `tableId` (路径参数): 桌号ID
|
|
|
+
|
|
|
+**响应:**
|
|
|
+- Content-Type: `text/event-stream`
|
|
|
+- 事件流
|
|
|
+
|
|
|
+### 9.2 相关购物车API
|
|
|
+
|
|
|
+```
|
|
|
+POST /api/store/order/cart/add # 添加商品到购物车
|
|
|
+PUT /api/store/order/cart/update # 更新购物车商品数量
|
|
|
+DELETE /api/store/order/cart/remove # 删除购物车商品
|
|
|
+DELETE /api/store/order/cart/clear # 清空购物车
|
|
|
+GET /api/store/order/cart/{tableId} # 获取购物车
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十、总结
|
|
|
+
|
|
|
+SSE是一种简单高效的服务器推送技术,特别适合购物车实时更新场景。通过本文档,您可以:
|
|
|
+
|
|
|
+1. ✅ 了解SSE的基本概念和适用场景
|
|
|
+2. ✅ 掌握后端SSE接口的实现方式
|
|
|
+3. ✅ 掌握前端SSE连接的使用方法
|
|
|
+4. ✅ 了解消息格式和事件类型
|
|
|
+5. ✅ 掌握错误处理和最佳实践
|
|
|
+
|
|
|
+如有问题,请参考代码实现或联系开发团队。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+# 二、WebSocket(双向通信)使用说明
|
|
|
+
|
|
|
+## 1. WebSocket简介
|
|
|
+
|
|
|
+### 1.1 什么是WebSocket
|
|
|
+
|
|
|
+WebSocket是一种全双工通信协议,允许客户端和服务器之间进行双向实时通信。与SSE相比,WebSocket支持客户端主动向服务器发送消息。
|
|
|
+
|
|
|
+### 1.2 适用场景
|
|
|
+
|
|
|
+- ✅ 需要双向实时通信的场景
|
|
|
+- ✅ 前端需要主动向服务器发送消息
|
|
|
+- ✅ 实时购物车更新(前端可以直接发送变化)
|
|
|
+- ✅ 实时聊天、协作等场景
|
|
|
+
|
|
|
+### 1.3 技术特点
|
|
|
+
|
|
|
+- **双向通信**:客户端 ↔ 服务器
|
|
|
+- **实时性**:低延迟,实时推送
|
|
|
+- **持久连接**:建立连接后保持连接
|
|
|
+- **支持二进制和文本消息**
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 后端实现说明
|
|
|
+
|
|
|
+### 2.1 WebSocket端点地址
|
|
|
+
|
|
|
+```
|
|
|
+ws://your-domain/ws/cart/{tableId}
|
|
|
+或
|
|
|
+wss://your-domain/ws/cart/{tableId} (HTTPS环境)
|
|
|
+```
|
|
|
+
|
|
|
+**参数说明:**
|
|
|
+- `tableId`:桌号ID(路径参数,必填)
|
|
|
+
|
|
|
+### 2.2 后端实现代码
|
|
|
+
|
|
|
+后端已实现 `CartWebSocketProcess` 类,位于:
|
|
|
+```
|
|
|
+shop.alien.store.config.CartWebSocketProcess
|
|
|
+```
|
|
|
+
|
|
|
+**主要功能:**
|
|
|
+- 建立WebSocket连接
|
|
|
+- 接收客户端消息(添加商品、更新数量、删除商品、清空购物车)
|
|
|
+- 推送购物车更新给所有连接的客户端
|
|
|
+- 自动处理连接断开和错误
|
|
|
+
|
|
|
+### 2.3 消息类型
|
|
|
+
|
|
|
+#### 2.3.1 客户端发送的消息类型
|
|
|
+
|
|
|
+| 消息类型 | 说明 | 数据格式 |
|
|
|
+|---------|------|---------|
|
|
|
+| `heartbeat` | 心跳消息 | `{"type": "heartbeat"}` |
|
|
|
+| `add_item` | 添加商品 | 见下方示例 |
|
|
|
+| `update_quantity` | 更新数量 | 见下方示例 |
|
|
|
+| `remove_item` | 删除商品 | 见下方示例 |
|
|
|
+| `clear_cart` | 清空购物车 | `{"type": "clear_cart"}` |
|
|
|
+
|
|
|
+#### 2.3.2 服务器推送的消息类型
|
|
|
+
|
|
|
+| 消息类型 | 说明 | 数据格式 |
|
|
|
+|---------|------|---------|
|
|
|
+| `connected` | 连接成功 | 见下方示例 |
|
|
|
+| `cart_update` | 购物车更新 | 见下方示例 |
|
|
|
+| `add_item_success` | 添加商品成功 | 见下方示例 |
|
|
|
+| `update_quantity_success` | 更新数量成功 | 见下方示例 |
|
|
|
+| `remove_item_success` | 删除商品成功 | 见下方示例 |
|
|
|
+| `clear_cart_success` | 清空购物车成功 | 见下方示例 |
|
|
|
+| `error` | 错误消息 | 见下方示例 |
|
|
|
+| `heartbeat` | 心跳回复 | `{"type": "heartbeat", "message": "pong"}` |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 前端实现说明
|
|
|
+
|
|
|
+### 3.1 原生JavaScript实现
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 建立WebSocket连接
|
|
|
+const tableId = 123; // 桌号ID
|
|
|
+const ws = new WebSocket(`ws://your-domain/ws/cart/${tableId}`);
|
|
|
+
|
|
|
+// 连接成功
|
|
|
+ws.onopen = function(event) {
|
|
|
+ console.log('WebSocket连接成功');
|
|
|
+};
|
|
|
+
|
|
|
+// 接收消息
|
|
|
+ws.onmessage = function(event) {
|
|
|
+ const message = JSON.parse(event.data);
|
|
|
+ console.log('收到消息:', message);
|
|
|
+
|
|
|
+ switch(message.type) {
|
|
|
+ case 'connected':
|
|
|
+ console.log('连接成功:', message.message);
|
|
|
+ break;
|
|
|
+ case 'cart_update':
|
|
|
+ // 更新购物车UI
|
|
|
+ updateCartUI(message.data);
|
|
|
+ break;
|
|
|
+ case 'add_item_success':
|
|
|
+ console.log('添加商品成功');
|
|
|
+ updateCartUI(message.data);
|
|
|
+ break;
|
|
|
+ case 'update_quantity_success':
|
|
|
+ console.log('更新数量成功');
|
|
|
+ updateCartUI(message.data);
|
|
|
+ break;
|
|
|
+ case 'remove_item_success':
|
|
|
+ console.log('删除商品成功');
|
|
|
+ updateCartUI(message.data);
|
|
|
+ break;
|
|
|
+ case 'clear_cart_success':
|
|
|
+ console.log('清空购物车成功');
|
|
|
+ updateCartUI(message.data);
|
|
|
+ break;
|
|
|
+ case 'error':
|
|
|
+ console.error('错误:', message.message);
|
|
|
+ showError(message.message);
|
|
|
+ break;
|
|
|
+ case 'heartbeat':
|
|
|
+ console.log('收到心跳');
|
|
|
+ break;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 连接错误
|
|
|
+ws.onerror = function(error) {
|
|
|
+ console.error('WebSocket错误:', error);
|
|
|
+};
|
|
|
+
|
|
|
+// 连接关闭
|
|
|
+ws.onclose = function(event) {
|
|
|
+ console.log('WebSocket连接关闭');
|
|
|
+ // 可以实现重连逻辑
|
|
|
+ reconnect();
|
|
|
+};
|
|
|
+
|
|
|
+// 发送消息:添加商品
|
|
|
+function addItem(cuisineId, quantity, remark) {
|
|
|
+ const message = {
|
|
|
+ type: 'add_item',
|
|
|
+ data: {
|
|
|
+ cuisineId: cuisineId,
|
|
|
+ quantity: quantity,
|
|
|
+ remark: remark || null
|
|
|
+ }
|
|
|
+ };
|
|
|
+ ws.send(JSON.stringify(message));
|
|
|
+}
|
|
|
+
|
|
|
+// 发送消息:更新数量
|
|
|
+function updateQuantity(cuisineId, quantity) {
|
|
|
+ const message = {
|
|
|
+ type: 'update_quantity',
|
|
|
+ data: {
|
|
|
+ cuisineId: cuisineId,
|
|
|
+ quantity: quantity
|
|
|
+ }
|
|
|
+ };
|
|
|
+ ws.send(JSON.stringify(message));
|
|
|
+}
|
|
|
+
|
|
|
+// 发送消息:删除商品
|
|
|
+function removeItem(cuisineId) {
|
|
|
+ const message = {
|
|
|
+ type: 'remove_item',
|
|
|
+ data: {
|
|
|
+ cuisineId: cuisineId
|
|
|
+ }
|
|
|
+ };
|
|
|
+ ws.send(JSON.stringify(message));
|
|
|
+}
|
|
|
+
|
|
|
+// 发送消息:清空购物车
|
|
|
+function clearCart() {
|
|
|
+ const message = {
|
|
|
+ type: 'clear_cart'
|
|
|
+ };
|
|
|
+ ws.send(JSON.stringify(message));
|
|
|
+}
|
|
|
+
|
|
|
+// 发送心跳
|
|
|
+function sendHeartbeat() {
|
|
|
+ const message = {
|
|
|
+ type: 'heartbeat'
|
|
|
+ };
|
|
|
+ ws.send(JSON.stringify(message));
|
|
|
+}
|
|
|
+
|
|
|
+// 每30秒发送一次心跳
|
|
|
+setInterval(sendHeartbeat, 30000);
|
|
|
+
|
|
|
+// 关闭连接
|
|
|
+// ws.close();
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 Vue.js实现示例
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <div>
|
|
|
+ <div v-if="!connected">正在连接...</div>
|
|
|
+ <div v-else>已连接</div>
|
|
|
+
|
|
|
+ <!-- 购物车UI -->
|
|
|
+ <div v-for="item in cart.items" :key="item.cuisineId">
|
|
|
+ {{ item.cuisineName }} x {{ item.quantity }}
|
|
|
+ <button @click="updateQuantity(item.cuisineId, item.quantity + 1)">+</button>
|
|
|
+ <button @click="updateQuantity(item.cuisineId, item.quantity - 1)">-</button>
|
|
|
+ <button @click="removeItem(item.cuisineId)">删除</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button @click="clearCart">清空购物车</button>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ ws: null,
|
|
|
+ connected: false,
|
|
|
+ cart: {
|
|
|
+ items: [],
|
|
|
+ totalAmount: 0,
|
|
|
+ totalQuantity: 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.initWebSocket();
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ this.closeWebSocket();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // 初始化WebSocket连接
|
|
|
+ initWebSocket() {
|
|
|
+ const tableId = this.$route.params.tableId || 123;
|
|
|
+ const wsUrl = `ws://your-domain/ws/cart/${tableId}`;
|
|
|
+
|
|
|
+ this.ws = new WebSocket(wsUrl);
|
|
|
+
|
|
|
+ // 连接成功
|
|
|
+ this.ws.onopen = () => {
|
|
|
+ console.log('WebSocket连接成功');
|
|
|
+ this.connected = true;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 接收消息
|
|
|
+ this.ws.onmessage = (event) => {
|
|
|
+ const message = JSON.parse(event.data);
|
|
|
+ this.handleMessage(message);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 连接错误
|
|
|
+ this.ws.onerror = (error) => {
|
|
|
+ console.error('WebSocket错误:', error);
|
|
|
+ this.connected = false;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 连接关闭
|
|
|
+ this.ws.onclose = () => {
|
|
|
+ console.log('WebSocket连接关闭');
|
|
|
+ this.connected = false;
|
|
|
+ // 可以在这里实现重连逻辑
|
|
|
+ this.reconnect();
|
|
|
+ };
|
|
|
+
|
|
|
+ // 定时发送心跳
|
|
|
+ setInterval(() => {
|
|
|
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
|
+ this.sendMessage({ type: 'heartbeat' });
|
|
|
+ }
|
|
|
+ }, 30000);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 处理消息
|
|
|
+ handleMessage(message) {
|
|
|
+ switch(message.type) {
|
|
|
+ case 'connected':
|
|
|
+ this.$message.success('连接成功');
|
|
|
+ break;
|
|
|
+ case 'cart_update':
|
|
|
+ case 'add_item_success':
|
|
|
+ case 'update_quantity_success':
|
|
|
+ case 'remove_item_success':
|
|
|
+ case 'clear_cart_success':
|
|
|
+ this.cart = message.data || this.cart;
|
|
|
+ break;
|
|
|
+ case 'error':
|
|
|
+ this.$message.error(message.message);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 发送消息
|
|
|
+ sendMessage(message) {
|
|
|
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
|
+ this.ws.send(JSON.stringify(message));
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 添加商品
|
|
|
+ addItem(cuisineId, quantity, remark) {
|
|
|
+ this.sendMessage({
|
|
|
+ type: 'add_item',
|
|
|
+ data: {
|
|
|
+ cuisineId: cuisineId,
|
|
|
+ quantity: quantity,
|
|
|
+ remark: remark
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 更新数量
|
|
|
+ updateQuantity(cuisineId, quantity) {
|
|
|
+ if (quantity <= 0) {
|
|
|
+ this.removeItem(cuisineId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.sendMessage({
|
|
|
+ type: 'update_quantity',
|
|
|
+ data: {
|
|
|
+ cuisineId: cuisineId,
|
|
|
+ quantity: quantity
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 删除商品
|
|
|
+ removeItem(cuisineId) {
|
|
|
+ this.sendMessage({
|
|
|
+ type: 'remove_item',
|
|
|
+ data: {
|
|
|
+ cuisineId: cuisineId
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 清空购物车
|
|
|
+ clearCart() {
|
|
|
+ this.$confirm('确定要清空购物车吗?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(() => {
|
|
|
+ this.sendMessage({
|
|
|
+ type: 'clear_cart'
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 关闭连接
|
|
|
+ closeWebSocket() {
|
|
|
+ if (this.ws) {
|
|
|
+ this.ws.close();
|
|
|
+ this.ws = null;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 重连
|
|
|
+ reconnect() {
|
|
|
+ setTimeout(() => {
|
|
|
+ this.initWebSocket();
|
|
|
+ }, 3000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 消息格式说明
|
|
|
+
|
|
|
+### 4.1 客户端发送消息格式
|
|
|
+
|
|
|
+#### 添加商品
|
|
|
+```json
|
|
|
+{
|
|
|
+ "type": "add_item",
|
|
|
+ "data": {
|
|
|
+ "cuisineId": 1001,
|
|
|
+ "quantity": 2,
|
|
|
+ "remark": "不要花生"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 更新数量
|
|
|
+```json
|
|
|
+{
|
|
|
+ "type": "update_quantity",
|
|
|
+ "data": {
|
|
|
+ "cuisineId": 1001,
|
|
|
+ "quantity": 3
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 删除商品
|
|
|
+```json
|
|
|
+{
|
|
|
+ "type": "remove_item",
|
|
|
+ "data": {
|
|
|
+ "cuisineId": 1001
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 清空购物车
|
|
|
+```json
|
|
|
+{
|
|
|
+ "type": "clear_cart"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.2 服务器推送消息格式
|
|
|
+
|
|
|
+#### 连接成功
|
|
|
+```json
|
|
|
+{
|
|
|
+ "type": "connected",
|
|
|
+ "message": "连接成功",
|
|
|
+ "data": null,
|
|
|
+ "timestamp": 1704067200000
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 购物车更新
|
|
|
+```json
|
|
|
+{
|
|
|
+ "type": "cart_update",
|
|
|
+ "message": "购物车更新",
|
|
|
+ "data": {
|
|
|
+ "tableId": 123,
|
|
|
+ "tableNumber": "A01",
|
|
|
+ "storeId": 1,
|
|
|
+ "items": [
|
|
|
+ {
|
|
|
+ "cuisineId": 1001,
|
|
|
+ "cuisineName": "宫保鸡丁",
|
|
|
+ "cuisineType": 1,
|
|
|
+ "cuisineImage": "https://example.com/image.jpg",
|
|
|
+ "unitPrice": 38.00,
|
|
|
+ "quantity": 2,
|
|
|
+ "subtotalAmount": 76.00,
|
|
|
+ "addUserId": 100,
|
|
|
+ "addUserPhone": "13800138000",
|
|
|
+ "remark": "不要花生"
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "totalAmount": 76.00,
|
|
|
+ "totalQuantity": 2
|
|
|
+ },
|
|
|
+ "timestamp": 1704067200000
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. SSE vs WebSocket 选择建议
|
|
|
+
|
|
|
+### 5.1 使用SSE的场景
|
|
|
+
|
|
|
+- ✅ 只需要服务器向客户端推送数据
|
|
|
+- ✅ 前端通过HTTP API更新数据
|
|
|
+- ✅ 需要更好的浏览器兼容性
|
|
|
+- ✅ 需要更简单的实现
|
|
|
+
|
|
|
+### 5.2 使用WebSocket的场景
|
|
|
+
|
|
|
+- ✅ 需要双向实时通信
|
|
|
+- ✅ 前端需要主动向服务器发送消息
|
|
|
+- ✅ 需要更低的延迟
|
|
|
+- ✅ 需要支持二进制数据
|
|
|
+
|
|
|
+### 5.3 本系统支持
|
|
|
+
|
|
|
+**本系统同时支持SSE和WebSocket两种方式:**
|
|
|
+
|
|
|
+- **HTTP API + SSE**:前端通过HTTP API更新购物车,后端通过SSE推送更新
|
|
|
+- **WebSocket**:前端通过WebSocket发送购物车变化,后端通过WebSocket推送更新
|
|
|
+
|
|
|
+**两种方式可以同时使用,互不冲突。**
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 注意事项
|
|
|
+
|
|
|
+1. **用户认证**:WebSocket连接建立时,用户信息从JWT Token中获取(如果使用HTTP API)
|
|
|
+2. **连接管理**:页面卸载时记得关闭WebSocket连接
|
|
|
+3. **心跳机制**:建议每30秒发送一次心跳,保持连接活跃
|
|
|
+4. **错误处理**:实现完善的错误处理和重连机制
|
|
|
+5. **多用户场景**:一个桌号可以同时有多个WebSocket连接,所有连接都会收到购物车更新
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 总结
|
|
|
+
|
|
|
+WebSocket提供了双向实时通信能力,特别适合需要前端主动发送消息的场景。通过本文档,您可以:
|
|
|
+
|
|
|
+1. ✅ 了解WebSocket的基本概念和适用场景
|
|
|
+2. ✅ 掌握后端WebSocket接口的实现方式
|
|
|
+3. ✅ 掌握前端WebSocket连接的使用方法
|
|
|
+4. ✅ 了解消息格式和事件类型
|
|
|
+5. ✅ 掌握错误处理和重连机制
|
|
|
+
|
|
|
+如有问题,请参考代码实现或联系开发团队。
|