# 购物车实时通信使用说明书（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连接（支持多人点餐）

---

## 线上环境部署说明（SSE 必读）

项目发到线上时，SSE 长连接需要以下配置，否则容易出现连接被提前断开、收不到推送等问题。

### 1. 网关（Spring Cloud Gateway）

- **问题**：网关对后端响应有默认或全局 `response-timeout`，会主动断开长时间无“完成”的响应，导致 SSE 长连接被踢掉。
- **做法**：对 SSE 路径**单独配一条路由**，并关闭该路由的响应超时（`response-timeout: -1`），且该路由需放在点餐通用路由**之前**，保证先匹配 SSE。
- **配置示例**（加入 Nacos 中 `alien-gateway` 的网关路由配置，如 `alien-gateway.yml`）：

```yaml
# 此条 SSE 路由必须放在 aliendining 通用路由前面
- id: aliendining-sse
  uri: http://${route_or_local_ip}:30014   # 与现有 aliendining 保持一致
  predicates:
    - Path=/aliendining/store/order/sse/**
  filters:
    - StripPrefix=1
  metadata:
    response-timeout: -1   # -1 表示不超时，避免 SSE 被网关提前断开
```

- 若使用 **lb 负载均衡**（如 `lb://alien-dining`），同样给上述 SSE 路由加上 `metadata.response-timeout: -1` 即可。

### 2. Nginx / 反向代理（若存在）

若网关前还有 Nginx（或其它反向代理），需避免对 SSE 做缓冲并拉长超时：

- **关闭对 event-stream 的缓冲**：  
  `proxy_buffering off;`（或对 `location ~ /aliendining/store/order/sse` 单独关闭）
- **拉长读超时**：  
  `proxy_read_timeout` 建议 ≥ 30 分钟（如 `1800s`），或略大于业务侧 SSE 超时（当前为 30 分钟）。
- **可选**：  
  `proxy_connect_timeout`、`proxy_send_timeout` 也可适当调大，避免代理层先断连。

### 3. 多实例部署（alien-dining 多节点）

- 当前 SSE 连接是**按实例内存**维护的（每个实例一份 `ConcurrentHashMap`）。同一桌号若被负载均衡到不同实例，只有“写操作发生的那台实例”上的 SSE 连接会收到推送，其它实例上的同桌连接收不到。
- **建议**：
  - **单实例**：无需改代码，按上面 1、2 配置即可。
  - **多实例**：要么对该 SSE 路径做**会话保持**（同一桌号固定到同一实例），要么后续改造为基于 **Redis 等中间件** 的跨实例广播（需改 `SseServiceImpl` 与 Nacos/配置）。

### 4. 前端线上地址

- 线上建立 SSE 时，请使用**经过网关的完整路径**，例如：  
  `https://你的域名/aliendining/store/order/sse/{tableId}`  
  具体以你们网关的 `Path` 与 `StripPrefix` 为准，保证最终能路由到 `alien-dining` 的 `/store/order/sse/{tableId}`。
- 若网关或 Nacos 中给点餐服务配了统一前缀（如 `/api`），则 SSE 地址中也要带上该前缀。

### 5. 小结

| 环境         | 必做项 |
|--------------|--------|
| 网关         | 为 SSE 路径单独路由并设置 `response-timeout: -1`，且路由顺序优先 |
| Nginx/反向代理 | `proxy_buffering off`，`proxy_read_timeout` ≥ 30 分钟 |
| 多实例       | 会话保持或改为 Redis 等跨实例推送 |
| 前端         | 使用经网关的完整 SSE URL |

---

## 三、前端实现说明

### 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.dining.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. ✅ 掌握错误处理和重连机制

如有问题，请参考代码实现或联系开发团队。
