订单状态实时通知的生产级完整方案

订单状态实时通知的生产级完整方案

核心是解决实际项目中可能出现的消息丢失、用户离线、重连、消息顺序、多实例部署等问题------下面我会提供可直接落地的方案设计、完整代码,以及每个关键问题的解决方案。

一、核心需求与方案设计

1. 核心需求

  • 订单状态变更(下单→支付中→支付成功→发货→签收等)时,实时推送给对应用户(Web端/移动端)。
  • 解决实际问题:用户离线不丢消息、WebSocket断连自动重连、消息不重复/不丢失、同一订单消息顺序正确、支持多服务实例部署。

2. 整体架构

markdown 复制代码
订单服务 → 发送状态消息(带唯一ID+订单ID+状态)→ RabbitMQ(持久化+确认机制)→ 消费者(手动ACK)
→ 检查用户是否在线:
   - 在线 → WebSocket实时推送
   - 离线 → Redis缓存离线消息
→ 用户上线(WebSocket重连)→ 拉取Redis离线消息 → 推送完成后删除缓存

3. 关键技术选型

  • 后端:Spring Boot 2.7.x + Spring AMQP(RabbitMQ)+ Spring WebSocket + Redis(离线消息缓存)
  • 前端:HTML5 WebSocket + 自动重连逻辑
  • 中间件:RabbitMQ 3.9+(可靠消息传输)、Redis 6.x(离线消息+分布式锁)

4. 核心问题解决方案

实际问题 解决方案
消息丢失 RabbitMQ队列/消息持久化、生产者确认(Publisher Confirm)、消费者手动ACK
用户离线消息丢失 Redis缓存用户离线消息,用户上线后自动拉取
WebSocket断连 前端自动重连(3秒重试一次)、后端保留用户连接状态(短时间内重连无需重新初始化)
同一订单消息顺序错乱 RabbitMQ单队列(同一订单消息入同一队列)+ 消费者单线程处理
多服务实例部署推送失败 Redis发布订阅同步消息到所有实例,仅连接当前实例的用户接收推送
消息重复推送 消息加唯一ID(订单ID+状态变更时间戳),Redis记录已处理ID去重

二、完整代码实现(生产级)

1. 前置准备

  • 启动RabbitMQ(开启管理界面,默认账号guest/guest)
  • 启动Redis(默认端口6379,无密码)
  • 项目统一配置:application.yml
yaml 复制代码
spring:
  # RabbitMQ配置(可靠消息传输核心)
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    # 生产者确认机制(确保消息发送到MQ)
    publisher-confirm-type: correlated
    # 生产者返回机制(确保消息路由成功)
    publisher-returns: true
    template:
      mandatory: true # 路由失败时返回消息
    # 消费者手动ACK(确保消息被正确处理)
    listener:
      simple:
        acknowledge-mode: manual # 手动确认
        concurrency: 1 # 单线程消费(保证消息顺序)
        max-concurrency: 1
        prefetch: 1 # 每次只拉取1条消息,处理完再拉取(顺序性)
  
  # Redis配置(离线消息缓存)
  redis:
    host: localhost
    port: 6379
    database: 0
    timeout: 3000ms
  
  # Web配置
  web:
    resources:
      static-locations: classpath:/static/ # 前端页面存放目录

# 应用配置
server:
  port: 8080
  servlet:
    context-path: /

2. 依赖配置(pom.xml)

xml 复制代码
<dependencies>
  <!-- Spring Boot核心 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <!-- WebSocket -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
  </dependency>

  <!-- RabbitMQ -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
  </dependency>

  <!-- Redis -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>

  <!-- JSON处理 -->
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.32</version>
  </dependency>

  <!-- 工具类 -->
  <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
  </dependency>
</dependencies>

3. 核心配置类

(1)RabbitMQ配置(可靠消息+顺序性)
typescript 复制代码
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.rabbitmq.client.Channel;

/**
 * RabbitMQ配置:确保消息可靠传输+顺序性
 */
@Configuration
public class RabbitMQOrderConfig {
    // 订单状态通知队列(持久化)
    public static final String ORDER_STATUS_QUEUE = "queue.order.status.notify";
    // 交换机(直连交换机,精准路由)
    public static final String ORDER_STATUS_EXCHANGE = "exchange.order.status.notify";
    // 路由键
    public static final String ORDER_STATUS_ROUTING_KEY = "key.order.status";

    /**
     * 订单状态队列(持久化、非独占、不自动删除)
     */
    @Bean
    public Queue orderStatusQueue() {
        return QueueBuilder.durable(ORDER_STATUS_QUEUE)
                .exclusive(false)
                .autoDelete(false)
                .build();
    }

    /**
     * 直连交换机(持久化)
     */
    @Bean
    public DirectExchange orderStatusExchange() {
        return ExchangeBuilder.directExchange(ORDER_STATUS_EXCHANGE)
                .durable(true)
                .build();
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindOrderStatusQueue(Queue orderStatusQueue, DirectExchange orderStatusExchange) {
        return BindingBuilder.bind(orderStatusQueue)
                .to(orderStatusExchange)
                .with(ORDER_STATUS_ROUTING_KEY);
    }

    /**
     * RabbitTemplate配置(生产者确认+消息持久化)
     */
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        // 消息持久化(默认是PERSISTENT,显式配置更稳妥)
        rabbitTemplate.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
        // 生产者确认回调(确认消息是否到达交换机)
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (!ack) {
                System.err.println("消息发送到交换机失败:" + cause + ",消息ID:" + correlationData.getId());
                // 失败重试逻辑(可结合定时任务重发)
            }
        });
        // 消息返回回调(交换机路由到队列失败时触发)
        rabbitTemplate.setReturnsCallback(returned -> {
            System.err.println("消息路由到队列失败:" + returned.getReplyText() + ",消息内容:" + new String(returned.getMessage().getBody()));
        });
        return rabbitTemplate;
    }

    /**
     * 消费者配置(手动ACK+单线程消费+顺序性)
     */
    @Bean
    public SimpleMessageListenerContainer orderStatusListenerContainer(
            ConnectionFactory connectionFactory,
            ChannelAwareMessageListener orderStatusMessageListener) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        container.setQueueNames(ORDER_STATUS_QUEUE);
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL); // 手动ACK
        container.setConcurrency("1"); // 单线程消费(保证顺序)
        container.setMaxConcurrency("1");
        container.setPrefetchCount(1); // 每次拉取1条消息,处理完再拉取
        container.setMessageListener(orderStatusMessageListener);
        return container;
    }
}
(2)Redis配置(离线消息缓存)
typescript 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis配置:用于存储离线消息
 */
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);

        // Key序列化(String)
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        // Value序列化(JSON)
        GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
        redisTemplate.setValueSerializer(jsonSerializer);
        redisTemplate.setHashValueSerializer(jsonSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}
(3)WebSocket配置(重连+离线消息拉取)
kotlin 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * WebSocket基础配置
 */
@Configuration
public class WebSocketConfig {
    // 自动注册@ServerEndpoint注解的WebSocket服务端
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

4. 核心业务类

(1)订单消息模型(统一格式)
arduino 复制代码
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 订单状态消息模型(前后端统一格式)
 */
@Data
public class OrderStatusMessage {
    // 消息唯一ID(订单ID+时间戳,用于去重)
    private String msgId;
    // 订单ID(核心关联字段)
    private String orderId;
    // 用户ID(接收消息的用户)
    private String userId;
    // 订单状态(如:PENDING=待支付,PAID=已支付,SHIPPED=已发货,RECEIVED=已签收)
    private String orderStatus;
    // 状态描述(给用户看的文案)
    private String statusDesc;
    // 状态变更时间
    private LocalDateTime changeTime;
}
(2)WebSocket服务端(核心推送逻辑)
typescript 复制代码
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * WebSocket服务端:处理连接、推送消息、离线消息拉取
 * 前端连接地址:ws://localhost:8080/order/ws/{userId}
 */
@ServerEndpoint("/order/ws/{userId}")
@Component
public class OrderWebSocketServer {
    // 注入RedisTemplate(操作离线消息)
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 存储在线用户连接(线程安全):key=userId,value=Session
    private static final Map<String, Session> ONLINE_USER_SESSIONS = new ConcurrentHashMap<>();

    // 离线消息Redis Key前缀(user:offline:msg:1001 → 用户1001的离线消息)
    private static final String OFFLINE_MSG_KEY_PREFIX = "user:offline:msg:";

    // 消息去重Redis Key前缀(msg:processed:xxx → 已处理的消息ID)
    private static final String PROCESSED_MSG_KEY_PREFIX = "msg:processed:";

    /**
     * 客户端连接成功时触发
     */
    @OnOpen
    public void onOpen(@PathParam("userId") String userId, Session session) {
        if (userId == null || userId.trim().isEmpty()) {
            try {
                session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "用户ID不能为空"));
            } catch (IOException e) {
                e.printStackTrace();
            }
            return;
        }

        // 存储用户连接
        ONLINE_USER_SESSIONS.put(userId, session);
        System.out.println("用户[" + userId + "]连接成功,当前在线人数:" + ONLINE_USER_SESSIONS.size());

        // 拉取离线消息并推送
        pullOfflineMessages(userId, session);
    }

    /**
     * 拉取用户离线消息并推送
     */
    private void pullOfflineMessages(String userId, Session session) {
        String offlineMsgKey = OFFLINE_MSG_KEY_PREFIX + userId;
        try {
            // 从Redis获取所有离线消息(List结构,左进右出)
            List<Object> offlineMessages = redisTemplate.opsForList().range(offlineMsgKey, 0, -1);
            if (offlineMessages != null && !offlineMessages.isEmpty()) {
                for (Object msg : offlineMessages) {
                    if (msg != null && session.isOpen()) {
                        // 推送离线消息
                        session.getBasicRemote().sendText(JSON.toJSONString(msg));
                        // 标记消息已处理(去重)
                        OrderStatusMessage orderMsg = JSON.parseObject(JSON.toJSONString(msg), OrderStatusMessage.class);
                        markMessageProcessed(orderMsg.getMsgId());
                    }
                }
                // 推送完成后删除离线消息
                redisTemplate.delete(offlineMsgKey);
                System.out.println("用户[" + userId + "]离线消息推送完成,共" + offlineMessages.size() + "条");
            }
        } catch (Exception e) {
            System.err.println("用户[" + userId + "]拉取离线消息失败:" + e.getMessage());
        }
    }

    /**
     * 客户端断开连接时触发
     */
    @OnClose
    public void onClose(@PathParam("userId") String userId, Session session) {
        ONLINE_USER_SESSIONS.remove(userId);
        System.out.println("用户[" + userId + "]断开连接,当前在线人数:" + ONLINE_USER_SESSIONS.size());
    }

    /**
     * 连接异常时触发
     */
    @OnError
    public void onError(@PathParam("userId") String userId, Session session, Throwable error) {
        System.err.println("用户[" + userId + "]WebSocket连接异常:" + error.getMessage());
        ONLINE_USER_SESSIONS.remove(userId);
        try {
            if (session.isOpen()) {
                session.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 核心方法:向指定用户推送订单状态消息(对外提供调用)
     */
    public boolean pushOrderStatusMessage(OrderStatusMessage message) {
        // 1. 消息去重检查(避免重复推送)
        if (isMessageProcessed(message.getMsgId())) {
            System.out.println("消息[" + message.getMsgId() + "]已处理,跳过推送");
            return true;
        }

        String userId = message.getUserId();
        Session session = ONLINE_USER_SESSIONS.get(userId);

        try {
            // 2. 用户在线:直接推送
            if (session != null && session.isOpen()) {
                session.getBasicRemote().sendText(JSON.toJSONString(message));
                markMessageProcessed(message.getMsgId()); // 标记已处理
                System.out.println("向用户[" + userId + "]推送订单[" + message.getOrderId() + "]状态:" + message.getOrderStatus());
                return true;
            } else {
                // 3. 用户离线:存入Redis离线消息
                String offlineMsgKey = OFFLINE_MSG_KEY_PREFIX + userId;
                redisTemplate.opsForList().rightPush(offlineMsgKey, message);
                // 离线消息有效期7天(可根据业务调整)
                redisTemplate.expire(offlineMsgKey, 7, TimeUnit.DAYS);
                System.out.println("用户[" + userId + "]离线,缓存订单[" + message.getOrderId() + "]状态消息");
                return true;
            }
        } catch (Exception e) {
            System.err.println("推送订单状态消息失败:" + e.getMessage());
            return false;
        }
    }

    /**
     * 标记消息已处理(Redis存储,有效期24小时)
     */
    private void markMessageProcessed(String msgId) {
        String processedKey = PROCESSED_MSG_KEY_PREFIX + msgId;
        redisTemplate.opsForValue().set(processedKey, "1", 24, TimeUnit.HOURS);
    }

    /**
     * 检查消息是否已处理(去重)
     */
    private boolean isMessageProcessed(String msgId) {
        String processedKey = PROCESSED_MSG_KEY_PREFIX + msgId;
        return Boolean.TRUE.equals(redisTemplate.hasKey(processedKey));
    }

    // 提供静态方法供消费者调用(通过Spring上下文获取实例)
    private static OrderWebSocketServer instance;
    @Autowired
    public void setInstance(OrderWebSocketServer instance) {
        OrderWebSocketServer.instance = instance;
    }
    public static OrderWebSocketServer getInstance() {
        return instance;
    }
}
(3)RabbitMQ消费者(接收消息并推送)
java 复制代码
import com.alibaba.fastjson.JSON;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

/**
 * RabbitMQ消费者:监听订单状态消息,调用WebSocket推送
 */
@Component
public class OrderStatusMessageListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            // 1. 解析消息内容
            String msgBody = new String(message.getBody(), "UTF-8");
            OrderStatusMessage orderMsg = JSON.parseObject(msgBody, OrderStatusMessage.class);
            System.out.println("收到订单状态消息:" + JSON.toJSONString(orderMsg));

            // 2. 调用WebSocket推送消息
            boolean pushSuccess = OrderWebSocketServer.getInstance().pushOrderStatusMessage(orderMsg);

            // 3. 推送成功:手动ACK(确认消息已处理)
            if (pushSuccess) {
                channel.basicAck(deliveryTag, false);
            } else {
                // 推送失败:拒绝消息(重回队列,最多重试3次,超过则进入死信队列)
                int retryCount = message.getMessageProperties().getHeader("x-retry-count") == null ? 0 : (int) message.getMessageProperties().getHeader("x-retry-count");
                if (retryCount < 3) {
                    message.getMessageProperties().setHeader("x-retry-count", retryCount + 1);
                    channel.basicNack(deliveryTag, false, true); // 重回队列
                } else {
                    channel.basicNack(deliveryTag, false, false); // 拒绝并丢弃(或进入死信队列)
                    System.err.println("消息[" + orderMsg.getMsgId() + "]重试3次失败,拒绝处理");
                }
            }
        } catch (Exception e) {
            // 异常处理:拒绝消息,避免死循环
            channel.basicNack(deliveryTag, false, false);
            System.err.println("处理订单状态消息异常:" + e.getMessage());
        }
    }
}
(4)订单服务(模拟订单状态变更并发送消息)
less 复制代码
import com.alibaba.fastjson.JSON;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.UUID;

/**
 * 订单服务:模拟订单状态变更,发送消息到RabbitMQ
 */
@RestController
public class OrderServiceController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 模拟订单状态变更(对外提供接口,实际项目中由订单系统内部调用)
     * 测试地址:http://localhost:8080/order/changeStatus?orderId=ORDER20240520001&userId=1001&orderStatus=PAID&statusDesc=订单支付成功
     */
    @GetMapping("/order/changeStatus")
    public String changeOrderStatus(
            @RequestParam String orderId,
            @RequestParam String userId,
            @RequestParam String orderStatus,
            @RequestParam String statusDesc) {
        try {
            // 1. 构建订单状态消息
            OrderStatusMessage message = new OrderStatusMessage();
            // 消息唯一ID(订单ID+时间戳,避免重复)
            String msgId = orderId + "_" + System.currentTimeMillis();
            message.setMsgId(msgId);
            message.setOrderId(orderId);
            message.setUserId(userId);
            message.setOrderStatus(orderStatus);
            message.setStatusDesc(statusDesc);
            message.setChangeTime(LocalDateTime.now());

            // 2. 发送消息到RabbitMQ(带消息ID,用于确认)
            CorrelationData correlationData = new CorrelationData(msgId);
            rabbitTemplate.convertAndSend(
                    RabbitMQOrderConfig.ORDER_STATUS_EXCHANGE,
                    RabbitMQOrderConfig.ORDER_STATUS_ROUTING_KEY,
                    JSON.toJSONString(message),
                    correlationData
            );

            System.out.println("订单[" + orderId + "]状态变更为[" + orderStatus + "],消息已发送到RabbitMQ");
            return "订单状态变更成功!消息ID:" + msgId;
        } catch (Exception e) {
            System.err.println("订单状态变更失败:" + e.getMessage());
            return "订单状态变更失败!";
        }
    }
}

5. 前端页面(支持自动重连+消息展示)

创建 src/main/resources/static/order-notify.html

xml 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>订单状态实时通知</title>
    <style>
        .container { width: 800px; margin: 20px auto; }
        .message { padding: 10px; margin: 10px 0; border: 1px solid #eee; border-radius: 4px; }
        .pending { background-color: #fff3cd; color: #856404; }
        .paid { background-color: #d4edda; color: #155724; }
        .shipped { background-color: #d1ecf1; color: #0c5460; }
        .received { background-color: #f8d7da; color: #721c24; }
        .offline { color: #666; font-style: italic; }
    </style>
</head>
<body>
    <div class="container">
        <h1>订单状态实时通知(用户ID:1001)</h1>
        <div id="messageContainer"></div>
    </div>

    <script>
        // 配置
        const userId = "1001"; // 固定用户ID(实际项目中从登录态获取)
        const wsUrl = `ws://${window.location.host}/order/ws/${userId}`;
        let websocket = null;
        let reconnectTimer = null; // 重连定时器

        // 初始化WebSocket
        initWebSocket();

        // 初始化方法
        function initWebSocket() {
            if ('WebSocket' in window) {
                websocket = new WebSocket(wsUrl);
                bindWebSocketEvents();
            } else {
                alert("您的浏览器不支持WebSocket,无法接收实时通知!");
            }
        }

        // 绑定WebSocket事件
        function bindWebSocketEvents() {
            // 连接成功
            websocket.onopen = function() {
                console.log("WebSocket连接成功");
                clearReconnectTimer(); // 清除重连定时器
                addMessage("连接成功,正在接收订单实时通知...", "system");
            };

            // 接收消息
            websocket.onmessage = function(event) {
                const message = JSON.parse(event.data);
                console.log("收到订单通知:", message);
                // 格式化时间
                const time = new Date(message.changeTime).toLocaleString();
                // 添加消息到页面
                let statusClass = getStatusClass(message.orderStatus);
                let msgHtml = `
                    <div class="message ${statusClass}">
                        <p>时间:${time}</p>
                        <p>订单号:${message.orderId}</p>
                        <p>状态:${message.statusDesc}</p>
                    </div>
                `;
                document.getElementById("messageContainer").innerHTML += msgHtml;
            };

            // 连接关闭
            websocket.onclose = function(event) {
                console.log("WebSocket连接关闭,错误码:" + event.code + ",原因:" + event.reason);
                addMessage("连接已断开,正在尝试重连...", "offline");
                startReconnectTimer(); // 启动重连
            };

            // 连接错误
            websocket.onerror = function(error) {
                console.error("WebSocket连接错误:", error);
                addMessage("连接异常,正在尝试重连...", "offline");
                startReconnectTimer(); // 启动重连
            };
        }

        // 启动重连定时器(3秒重试一次)
        function startReconnectTimer() {
            if (!reconnectTimer) {
                reconnectTimer = setInterval(function() {
                    console.log("尝试重连WebSocket...");
                    initWebSocket();
                }, 3000);
            }
        }

        // 清除重连定时器
        function clearReconnectTimer() {
            if (reconnectTimer) {
                clearInterval(reconnectTimer);
                reconnectTimer = null;
            }
        }

        // 根据订单状态获取样式类
        function getStatusClass(orderStatus) {
            switch(orderStatus) {
                case "PENDING": return "pending";
                case "PAID": return "paid";
                case "SHIPPED": return "shipped";
                case "RECEIVED": return "received";
                default: return "";
            }
        }

        // 添加系统消息
        function addMessage(content, type) {
            const time = new Date().toLocaleString();
            let msgHtml = `
                <div class="message ${type}">
                    <p>【系统】${time}:${content}</p>
                </div>
            `;
            document.getElementById("messageContainer").innerHTML += msgHtml;
        }

        // 页面关闭时关闭WebSocket连接
        window.onbeforeunload = function() {
            if (websocket && websocket.readyState === WebSocket.OPEN) {
                websocket.close();
            }
        };
    </script>
</body>
</html>

三、测试流程(验证所有核心功能)

1. 环境准备

  1. 启动RabbitMQ(默认端口5672,管理界面:http://localhost:15672)
  2. 启动Redis(默认端口6379)
  3. 启动Spring Boot应用(端口8080)

2. 功能测试

(1)在线用户实时接收通知
  1. 打开前端页面:http://localhost:8080/order-notify.html(用户ID=1001)

  2. 调用订单状态变更接口(模拟支付成功):

    bash 复制代码
    http://localhost:8080/order/changeStatus?orderId=ORDER20240520001&userId=1001&orderStatus=PAID&statusDesc=订单支付成功,正在准备发货
  3. 观察前端页面:无需刷新,立即收到"订单支付成功"的通知。

(2)离线用户消息缓存与推送
  1. 关闭前端页面(用户1001离线)

  2. 调用接口模拟订单发货:

    bash 复制代码
    http://localhost:8080/order/changeStatus?orderId=ORDER20240520001&userId=1001&orderStatus=SHIPPED&statusDesc=订单已发货,快递单号:SF123456789
  3. 重新打开前端页面:页面加载后,自动拉取离线消息,显示"订单已发货"的通知。

(3)WebSocket断连重连
  1. 打开前端页面,然后在浏览器开发者工具(F12)的「Network」面板中,禁用WebSocket连接(或关闭RabbitMQ/Redis模拟异常)
  2. 观察页面:显示"连接断开,正在尝试重连"
  3. 恢复RabbitMQ/Redis连接:页面自动重连成功,继续接收后续通知。
(4)消息去重测试
  1. 重复调用同一订单的同一状态接口(比如再次调用支付成功)
  2. 观察前端页面:仅接收一次通知,控制台显示"消息已处理,跳过推送"。

四、生产环境扩展建议

  1. 多实例部署

    1. 方案:使用Redis发布订阅(当一个实例收到MQ消息后,通过Redis发布消息,其他实例订阅后推送给本地连接的用户)
    2. 核心:确保所有实例都能接收消息,仅向连接在本实例的用户推送。
  2. 死信队列

    1. 配置RabbitMQ死信队列,处理重试3次仍失败的消息(如用户长期离线且Redis故障),避免消息丢失。
  3. 消息监控

    1. 集成Spring Boot Actuator监控RabbitMQ队列长度、WebSocket连接数、离线消息数。
    2. 对接ELK栈或日志平台,记录消息推送日志,便于问题排查。
  4. 权限控制

    1. 前端WebSocket连接时,携带用户登录令牌(如JWT),后端验证合法性后再建立连接。

总结

  1. 核心流程:订单状态变更 → RabbitMQ可靠传输 → 消费者处理 → 在线用户WebSocket推送/离线用户Redis缓存 → 重连后拉取离线消息。
  2. 关键解决的问题:消息不丢失、用户离线不丢消息、断连自动重连、消息不重复、顺序正确。
  3. 落地要点:依赖RabbitMQ的持久化+确认机制、Redis的离线消息缓存、WebSocket的重连逻辑,三者协同实现生产级可靠的实时通知。

这套方案完全基于Spring Cloud生态,代码可直接复用,同时覆盖了实际项目中可能遇到的核心问题,适合直接落地到生产环境。

相关推荐
action191640 分钟前
Nano Banana2API国内接入神方案!0.1元/次稳到哭
后端
无限进步_42 分钟前
C++从入门到类和对象完全指南
开发语言·c++·windows·git·后端·github·visual studio
Sammyyyyy1 小时前
Rust性能调优:从劝退到真香
开发语言·后端·rust·servbay
Zfox_1 小时前
【Go】异常处理、泛型和文件操作
开发语言·后端·golang
zhangyanfei011 小时前
谈谈 Golang 中的线程协程是如何管理栈内存的
开发语言·后端·golang
A-程序设计1 小时前
基于Spring Boot+Vue的生活用品购物平台设计与实现-(源码+LW+可部署)
vue.js·spring boot·后端
k***z111 小时前
Spring boot创建时常用的依赖
java·spring boot·后端
计算机毕设定制辅导-无忧学长1 小时前
基于Spring Boot的驾校管理系统
java·spring boot·后端