Преглед изворни кода

维护代码 添加websocket

lutong пре 2 месеци
родитељ
комит
22dc78a347

+ 1270 - 0
SSE前后端使用说明书.md

@@ -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. ✅ 掌握错误处理和重连机制
+
+如有问题,请参考代码实现或联系开发团队。

+ 387 - 0
alien-store/src/main/java/shop/alien/store/config/CartWebSocketProcess.java

@@ -0,0 +1,387 @@
+package shop.alien.store.config;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.store.dto.AddCartItemDTO;
+import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.store.service.CartService;
+
+import javax.websocket.*;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+import java.io.IOException;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 购物车WebSocket处理类
+ * 支持双向通信:前端可以发送购物车变化,后端可以推送购物车更新
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Component
+@ServerEndpoint(value = "/ws/cart/{tableId}", configurator = WebSocketConfig.WebSocketConfigurator.class)
+public class CartWebSocketProcess implements ApplicationContextAware {
+
+    private static ApplicationContext applicationContext;
+    private static CartService cartService;
+
+    // 存储每个桌号的WebSocket连接,一个桌号可以有多个连接(多个用户)
+    private static final ConcurrentHashMap<Integer, ConcurrentHashMap<String, CartWebSocketProcess>> connections = new ConcurrentHashMap<>();
+
+    /**
+     * 会话对象
+     */
+    private Session session;
+
+    /**
+     * 桌号ID
+     */
+    private Integer tableId;
+
+    /**
+     * 连接ID
+     */
+    private String connectionId;
+
+    @Override
+    public void setApplicationContext(ApplicationContext context) {
+        CartWebSocketProcess.applicationContext = context;
+        CartWebSocketProcess.cartService = context.getBean(CartService.class);
+    }
+
+    /**
+     * 客户端创建连接时触发
+     */
+    @OnOpen
+    public void onOpen(@PathParam("tableId") Integer tableId, Session session) {
+        try {
+            this.session = session;
+            this.tableId = tableId;
+            // 设置30分钟超时
+            this.session.setMaxIdleTimeout(30 * 60 * 1000);
+            
+            // 生成连接ID
+            this.connectionId = "conn_" + System.currentTimeMillis() + "_" + Thread.currentThread().getId();
+
+            // 存储连接
+            connections.computeIfAbsent(tableId, k -> new ConcurrentHashMap<>()).put(connectionId, this);
+
+            log.info("购物车WebSocket连接建立, tableId={}, connectionId={}", tableId, connectionId);
+
+            // 发送连接成功消息
+            sendMessage(createMessage("connected", "连接成功", null));
+
+            // 发送当前购物车数据
+            try {
+                CartDTO cart = cartService.getCart(tableId);
+                sendMessage(createMessage("cart_update", "购物车数据", cart));
+            } catch (Exception e) {
+                log.error("获取购物车数据失败, tableId={}", tableId, e);
+            }
+
+        } catch (Exception e) {
+            log.error("WebSocket连接建立失败, tableId={}", tableId, e);
+        }
+    }
+
+    /**
+     * 接收到客户端消息时触发
+     */
+    @OnMessage
+    public void onMessage(String message) {
+        try {
+            log.info("收到客户端消息, tableId={}, connectionId={}, message={}", tableId, connectionId, message);
+
+            // 解析消息
+            JSONObject jsonMessage = JSON.parseObject(message);
+            String type = jsonMessage.getString("type");
+
+            if (type == null) {
+                sendError("消息类型不能为空");
+                return;
+            }
+
+            // 处理不同类型的消息
+            switch (type) {
+                case "heartbeat":
+                    // 心跳消息,直接回复
+                    sendMessage(createMessage("heartbeat", "pong", null));
+                    break;
+
+                case "add_item":
+                    // 添加商品到购物车
+                    handleAddItem(jsonMessage);
+                    break;
+
+                case "update_quantity":
+                    // 更新商品数量
+                    handleUpdateQuantity(jsonMessage);
+                    break;
+
+                case "remove_item":
+                    // 删除商品
+                    handleRemoveItem(jsonMessage);
+                    break;
+
+                case "clear_cart":
+                    // 清空购物车
+                    handleClearCart();
+                    break;
+
+                default:
+                    sendError("未知的消息类型: " + type);
+                    break;
+            }
+
+        } catch (Exception e) {
+            log.error("处理客户端消息失败, tableId={}, connectionId={}", tableId, connectionId, e);
+            sendError("处理消息失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 客户端连接关闭时触发
+     */
+    @OnClose
+    public void onClose() {
+        try {
+            removeConnection();
+            log.info("购物车WebSocket连接关闭, tableId={}, connectionId={}", tableId, connectionId);
+        } catch (Exception e) {
+            log.error("关闭WebSocket连接失败, tableId={}, connectionId={}", tableId, connectionId, e);
+        }
+    }
+
+    /**
+     * 连接发生异常时触发
+     */
+    @OnError
+    public void onError(Throwable error) {
+        log.error("WebSocket连接错误, tableId={}, connectionId={}", tableId, connectionId, error);
+        removeConnection();
+    }
+
+    /**
+     * 处理添加商品
+     */
+    private void handleAddItem(JSONObject jsonMessage) {
+        try {
+            JSONObject data = jsonMessage.getJSONObject("data");
+            if (data == null) {
+                sendError("添加商品数据不能为空");
+                return;
+            }
+
+            AddCartItemDTO dto = new AddCartItemDTO();
+            dto.setTableId(tableId);
+            dto.setCuisineId(data.getInteger("cuisineId"));
+            dto.setQuantity(data.getInteger("quantity"));
+            dto.setRemark(data.getString("remark"));
+
+            // 调用服务添加商品
+            CartDTO cart = cartService.addItem(dto);
+
+            // 推送更新给该桌号的所有连接
+            pushCartUpdateToAll(tableId, cart);
+
+            // 回复成功消息
+            sendMessage(createMessage("add_item_success", "添加商品成功", cart));
+
+        } catch (Exception e) {
+            log.error("添加商品失败, tableId={}", tableId, e);
+            sendError("添加商品失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 处理更新商品数量
+     */
+    private void handleUpdateQuantity(JSONObject jsonMessage) {
+        try {
+            JSONObject data = jsonMessage.getJSONObject("data");
+            if (data == null) {
+                sendError("更新数量数据不能为空");
+                return;
+            }
+
+            Integer cuisineId = data.getInteger("cuisineId");
+            Integer quantity = data.getInteger("quantity");
+
+            if (cuisineId == null || quantity == null) {
+                sendError("菜品ID和数量不能为空");
+                return;
+            }
+
+            // 调用服务更新数量
+            CartDTO cart = cartService.updateItemQuantity(tableId, cuisineId, quantity);
+
+            // 推送更新给该桌号的所有连接
+            pushCartUpdateToAll(tableId, cart);
+
+            // 回复成功消息
+            sendMessage(createMessage("update_quantity_success", "更新数量成功", cart));
+
+        } catch (Exception e) {
+            log.error("更新商品数量失败, tableId={}", tableId, e);
+            sendError("更新数量失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 处理删除商品
+     */
+    private void handleRemoveItem(JSONObject jsonMessage) {
+        try {
+            JSONObject data = jsonMessage.getJSONObject("data");
+            if (data == null) {
+                sendError("删除商品数据不能为空");
+                return;
+            }
+
+            Integer cuisineId = data.getInteger("cuisineId");
+            if (cuisineId == null) {
+                sendError("菜品ID不能为空");
+                return;
+            }
+
+            // 调用服务删除商品
+            CartDTO cart = cartService.removeItem(tableId, cuisineId);
+
+            // 推送更新给该桌号的所有连接
+            pushCartUpdateToAll(tableId, cart);
+
+            // 回复成功消息
+            sendMessage(createMessage("remove_item_success", "删除商品成功", cart));
+
+        } catch (Exception e) {
+            log.error("删除商品失败, tableId={}", tableId, e);
+            sendError("删除商品失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 处理清空购物车
+     */
+    private void handleClearCart() {
+        try {
+            // 调用服务清空购物车
+            cartService.clearCart(tableId);
+
+            // 创建空的购物车对象
+            CartDTO cart = new CartDTO();
+            cart.setTableId(tableId);
+            cart.setItems(java.util.Collections.emptyList());
+            cart.setTotalAmount(java.math.BigDecimal.ZERO);
+            cart.setTotalQuantity(0);
+
+            // 推送更新给该桌号的所有连接
+            pushCartUpdateToAll(tableId, cart);
+
+            // 回复成功消息
+            sendMessage(createMessage("clear_cart_success", "清空购物车成功", cart));
+
+        } catch (Exception e) {
+            log.error("清空购物车失败, tableId={}", tableId, e);
+            sendError("清空购物车失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 推送购物车更新给该桌号的所有连接
+     */
+    private void pushCartUpdateToAll(Integer tableId, CartDTO cart) {
+        ConcurrentHashMap<String, CartWebSocketProcess> tableConnections = connections.get(tableId);
+        if (tableConnections != null && !tableConnections.isEmpty()) {
+            tableConnections.forEach((connId, ws) -> {
+                try {
+                    ws.sendMessage(createMessage("cart_update", "购物车更新", cart));
+                } catch (Exception e) {
+                    log.error("推送购物车更新失败, tableId={}, connectionId={}", tableId, connId, e);
+                }
+            });
+        }
+    }
+
+    /**
+     * 发送消息
+     */
+    public void sendMessage(String message) {
+        try {
+            if (session != null && session.isOpen()) {
+                session.getBasicRemote().sendText(message);
+            }
+        } catch (IOException e) {
+            log.error("发送WebSocket消息失败, tableId={}, connectionId={}", tableId, connectionId, e);
+            removeConnection();
+        }
+    }
+
+    /**
+     * 发送错误消息
+     */
+    private void sendError(String errorMessage) {
+        sendMessage(createMessage("error", errorMessage, null));
+    }
+
+    /**
+     * 创建消息JSON字符串
+     */
+    private String createMessage(String type, String message, Object data) {
+        JSONObject json = new JSONObject();
+        json.put("type", type);
+        json.put("message", message);
+        json.put("data", data);
+        json.put("timestamp", System.currentTimeMillis());
+        return json.toJSONString();
+    }
+
+    /**
+     * 移除连接
+     */
+    private void removeConnection() {
+        if (tableId != null && connectionId != null) {
+            ConcurrentHashMap<String, CartWebSocketProcess> tableConnections = connections.get(tableId);
+            if (tableConnections != null) {
+                tableConnections.remove(connectionId);
+                if (tableConnections.isEmpty()) {
+                    connections.remove(tableId);
+                }
+            }
+        }
+    }
+
+    /**
+     * 静态方法:推送购物车更新(供外部调用,如HTTP接口)
+     */
+    public static void pushCartUpdate(Integer tableId, CartDTO cart) {
+        ConcurrentHashMap<String, CartWebSocketProcess> tableConnections = connections.get(tableId);
+        if (tableConnections != null && !tableConnections.isEmpty()) {
+            String message = createStaticMessage("cart_update", "购物车更新", cart);
+            tableConnections.forEach((connectionId, ws) -> {
+                try {
+                    ws.sendMessage(message);
+                } catch (Exception e) {
+                    log.error("推送购物车更新失败, tableId={}, connectionId={}", tableId, connectionId, e);
+                }
+            });
+        }
+    }
+
+    /**
+     * 静态方法:创建消息
+     */
+    private static String createStaticMessage(String type, String message, Object data) {
+        JSONObject json = new JSONObject();
+        json.put("type", type);
+        json.put("message", message);
+        json.put("data", data);
+        json.put("timestamp", System.currentTimeMillis());
+        return json.toJSONString();
+    }
+}

+ 1 - 1
alien-store/src/main/java/shop/alien/store/controller/DiningController.java

@@ -19,7 +19,7 @@ import java.util.List;
  * @since 2025-01-XX
  */
 @Slf4j
-@Api(tags = {"点餐管理"})
+@Api(tags = {"小程序-点餐管理"})
 @CrossOrigin
 @RestController
 @RequestMapping("/store/dining")

+ 14 - 7
alien-store/src/main/java/shop/alien/store/controller/StoreOrderController.java

@@ -19,6 +19,7 @@ import shop.alien.mapper.StoreTableMapper;
 import shop.alien.mapper.StoreInfoMapper;
 import shop.alien.entity.store.StoreTable;
 import shop.alien.entity.store.StoreInfo;
+import shop.alien.store.config.CartWebSocketProcess;
 import shop.alien.store.service.CartService;
 import shop.alien.store.service.SseService;
 import shop.alien.store.service.StoreOrderService;
@@ -34,7 +35,7 @@ import java.util.List;
  * @since 2025-01-XX
  */
 @Slf4j
-@Api(tags = {"订单管理"})
+@Api(tags = {"小程序-订单管理"})
 @CrossOrigin
 @RestController
 @RequestMapping("/store/order")
@@ -61,13 +62,15 @@ public class StoreOrderController {
         }
     }
 
-    @ApiOperation(value = "添加商品到购物车", notes = "添加商品到购物车,并推送SSE消息")
+    @ApiOperation(value = "添加商品到购物车", notes = "添加商品到购物车,并推送SSE和WebSocket消息")
     @PostMapping("/cart/add")
     public R<CartDTO> addCartItem(@Valid @RequestBody AddCartItemDTO dto) {
         try {
             CartDTO cart = cartService.addItem(dto);
-            // 推送购物车更新消息
+            // 推送购物车更新消息(SSE)
             sseService.pushCartUpdate(dto.getTableId(), cart);
+            // 推送购物车更新消息(WebSocket)
+            CartWebSocketProcess.pushCartUpdate(dto.getTableId(), cart);
             return R.data(cart);
         } catch (Exception e) {
             log.error("添加商品到购物车失败: {}", e.getMessage(), e);
@@ -75,7 +78,7 @@ public class StoreOrderController {
         }
     }
 
-    @ApiOperation(value = "更新购物车商品数量", notes = "更新购物车中商品的数量,并推送SSE消息")
+    @ApiOperation(value = "更新购物车商品数量", notes = "更新购物车中商品的数量,并推送SSE和WebSocket消息")
     @PutMapping("/cart/update")
     public R<CartDTO> updateCartItem(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
@@ -83,8 +86,10 @@ public class StoreOrderController {
             @ApiParam(value = "数量", required = true) @RequestParam Integer quantity) {
         try {
             CartDTO cart = cartService.updateItemQuantity(tableId, cuisineId, quantity);
-            // 推送购物车更新消息
+            // 推送购物车更新消息(SSE)
             sseService.pushCartUpdate(tableId, cart);
+            // 推送购物车更新消息(WebSocket)
+            CartWebSocketProcess.pushCartUpdate(tableId, cart);
             return R.data(cart);
         } catch (Exception e) {
             log.error("更新购物车商品数量失败: {}", e.getMessage(), e);
@@ -92,15 +97,17 @@ public class StoreOrderController {
         }
     }
 
-    @ApiOperation(value = "删除购物车商品", notes = "从购物车中删除商品,并推送SSE消息")
+    @ApiOperation(value = "删除购物车商品", notes = "从购物车中删除商品,并推送SSE和WebSocket消息")
     @DeleteMapping("/cart/remove")
     public R<CartDTO> removeCartItem(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
             @ApiParam(value = "菜品ID", required = true) @RequestParam Integer cuisineId) {
         try {
             CartDTO cart = cartService.removeItem(tableId, cuisineId);
-            // 推送购物车更新消息
+            // 推送购物车更新消息(SSE)
             sseService.pushCartUpdate(tableId, cart);
+            // 推送购物车更新消息(WebSocket)
+            CartWebSocketProcess.pushCartUpdate(tableId, cart);
             return R.data(cart);
         } catch (Exception e) {
             log.error("删除购物车商品失败: {}", e.getMessage(), e);