SSE前后端使用说明书.md 32 KB

购物车实时通信使用说明书(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层

@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层

接口定义:

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 推送消息示例

在购物车操作后推送更新:

// 添加商品到购物车后
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):

    # 此条 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_timeoutproxy_send_timeout 也可适当调大,避免代理层先断连。

3. 多实例部署(alien-dining 多节点)

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

4. 前端线上地址

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

5. 小结

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

三、前端实现说明

3.1 基础使用(原生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实现示例

<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实现示例

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 微信小程序实现示例

// 微信小程序不支持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对象

{
  "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)

"连接成功"

4.3 心跳消息(heartbeat)

"ping"

五、错误处理

5.1 连接错误

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 重连机制

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 数据解析错误

eventSource.addEventListener('cart_update', function(event) {
  try {
    const cart = JSON.parse(event.data);
    updateCartUI(cart);
  } catch (error) {
    console.error('解析购物车数据失败:', error);
    // 可以显示错误提示或使用默认值
    showError('购物车数据解析失败,请刷新页面');
  }
});

六、最佳实践

6.1 连接管理

  1. 页面加载时建立连接

    mounted() {
     this.initSSE();
    }
    
  2. 页面卸载时关闭连接

    beforeDestroy() {
     this.closeSSE();
    }
    
  3. 路由切换时关闭旧连接

    watch: {
     '$route'(to, from) {
       if (to.params.tableId !== from.params.tableId) {
         this.closeSSE();
         this.initSSE();
       }
     }
    }
    

6.2 性能优化

  1. 避免频繁更新UI

    let updateTimer = null;
    eventSource.addEventListener('cart_update', function(event) {
     // 防抖处理,避免频繁更新
     clearTimeout(updateTimer);
     updateTimer = setTimeout(() => {
       const cart = JSON.parse(event.data);
       updateCartUI(cart);
     }, 100);
    });
    
  2. 使用虚拟滚动(如果商品很多)

    // 使用vue-virtual-scroll-list等库
    

6.3 用户体验

  1. 显示连接状态

    <div v-if="connected" class="status connected">已连接</div>
    <div v-else class="status disconnected">连接中...</div>
    
  2. 显示更新提示

    eventSource.addEventListener('cart_update', function(event) {
     const cart = JSON.parse(event.data);
     updateCartUI(cart);
     showToast('购物车已更新'); // 显示提示
    });
    
  3. 处理网络异常

    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秒发送一次,用于保持连接
  • 如果不需要处理心跳,可以忽略该事件

解决方案:

// 不监听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实现

// 建立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实现示例

<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 客户端发送消息格式

添加商品

{
  "type": "add_item",
  "data": {
    "cuisineId": 1001,
    "quantity": 2,
    "remark": "不要花生"
  }
}

更新数量

{
  "type": "update_quantity",
  "data": {
    "cuisineId": 1001,
    "quantity": 3
  }
}

删除商品

{
  "type": "remove_item",
  "data": {
    "cuisineId": 1001
  }
}

清空购物车

{
  "type": "clear_cart"
}

4.2 服务器推送消息格式

连接成功

{
  "type": "connected",
  "message": "连接成功",
  "data": null,
  "timestamp": 1704067200000
}

购物车更新

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

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