Spring Task与WebSocket实战:构建高效外卖订单状态管理系统
1. 为什么外卖系统需要订单状态管理每次点外卖的时候你有没有想过后台是怎么处理你的订单的为什么超时未支付的订单会自动取消为什么商家能秒接你的单这背后其实是一套精密的订单状态管理系统在运作。想象一下这样的场景中午12点你下单了一份黄焖鸡米饭但忙着开会忘了付款。15分钟后系统自动取消了订单——这就是Spring Task定时任务在起作用。而当你完成支付后商家那边立刻叮咚一声弹出新订单提醒——这则是WebSocket实时通信的功劳。传统的外卖系统如果只用HTTP协议商家页面需要不断刷新才能看到新订单就像你不停地按F5刷新网页一样低效。而现代系统采用WebSocket技术后就像给商家装了个门铃有客人来了自动提醒既省电又高效。2. Spring Task处理超时订单实战2.1 定时任务配置Spring Task是Spring框架自带的定时任务工具用起来特别简单。先来看个最基本的例子Slf4j Component public class OrderTask { Scheduled(cron 0 * * * * ?) // 每分钟执行一次 public void checkTimeoutOrders(){ log.info(开始检查超时订单: {}, LocalDateTime.now()); // 业务逻辑... } }这里的cron表达式可能看起来像天书其实很好理解。以0 * * * * ?为例第一位0表示第0秒开始第二位*表示每分钟都执行后几位同理分别代表小时、日、月、星期2.2 处理支付超时订单具体到外卖系统的实现我们需要查询超过15分钟未支付的订单Scheduled(cron 0 * * * * ?) public void processTimeoutOrder(){ LocalDateTime deadline LocalDateTime.now().minusMinutes(15); ListOrders unpaidOrders orderMapper.getByStatusAndOrderTimeLT( Orders.PENDING_PAYMENT, deadline); unpaidOrders.forEach(order - { order.setStatus(Orders.CANCELLED); order.setCancelReason(支付超时自动取消); orderMapper.update(order); }); }这里有几个关键点需要注意查询条件要同时满足状态为待支付且下单时间早于15分钟前批量更新比逐条更新效率更高记得记录取消原因方便后续统计分析2.3 处理滞留的派送中订单有些骑手可能会忘记点击送达我们可以每天凌晨检查Scheduled(cron 0 0 1 * * ?) // 每天凌晨1点执行 public void processDeliveryOrder(){ LocalDateTime deadline LocalDateTime.now().minusHours(1); ListOrders deliveringOrders orderMapper.getByStatusAndOrderTimeLT( Orders.DELIVERY_IN_PROGRESS, deadline); deliveringOrders.forEach(order - { order.setStatus(Orders.COMPLETED); orderMapper.update(order); }); }建议把这个任务放在凌晨执行因为凌晨订单量少系统压力小给骑手留出足够的操作时间避免高峰时段占用系统资源3. WebSocket实现实时通知3.1 WebSocket基础配置首先在Spring Boot项目中添加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency然后配置WebSocket端点Configuration public class WebSocketConfig { Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }3.2 WebSocket服务端实现核心的服务端代码是这样的Component ServerEndpoint(/ws/{sid}) public class WebSocketServer { private static MapString, Session sessions new ConcurrentHashMap(); OnOpen public void onOpen(Session session, PathParam(sid) String sid) { sessions.put(sid, session); } public void sendToAll(String message) { sessions.values().forEach(session - { try { session.getBasicRemote().sendText(message); } catch (IOException e) { log.error(消息发送失败, e); } }); } }这里使用了ServerEndpoint注解定义WebSocket端点sid可以用来区分不同的客户端比如商家ID。3.3 前端连接WebSocket前端连接也很简单const socket new WebSocket(ws://${location.host}/ws/${shopId}); socket.onmessage (event) { const data JSON.parse(event.data); if(data.type 1) { showNewOrderAlert(data); // 显示新订单提醒 } else if(data.type 2) { showReminderAlert(data); // 显示催单提醒 } };4. 订单状态全流程实战4.1 来单提醒实现当用户完成支付后系统需要通知商家public void paySuccess(String orderId) { // 更新订单状态 order.setStatus(Orders.TO_BE_CONFIRMED); orderMapper.update(order); // 发送WebSocket通知 MapString, Object message new HashMap(); message.put(type, 1); // 1表示新订单 message.put(orderId, orderId); message.put(content, 新订单 order.getNumber()); webSocketServer.sendToAll(JSON.toJSONString(message)); }商家端收到消息后可以播放提示音弹出通知窗口自动刷新订单列表4.2 客户催单功能用户等待太久时可以催单GetMapping(/reminder/{orderId}) public Result reminder(PathVariable Long orderId) { Orders order orderMapper.getById(orderId); if(order null) { throw new BusinessException(订单不存在); } MapString, Object message new HashMap(); message.put(type, 2); // 2表示催单 message.put(orderId, orderId); message.put(content, 客户催单 order.getNumber()); webSocketServer.sendToAll(JSON.toJSONString(message)); return Result.success(); }催单消息应该包含订单基本信息催单时间当前订单状态预计送达时间5. 性能优化与注意事项5.1 定时任务优化技巧在实际项目中处理大量订单时需要注意分页查询避免一次性加载全部超时订单PageHelper.startPage(pageNum, pageSize); ListOrders orders orderMapper.getTimeoutOrders();批量更新使用MyBatis的批量操作orderMapper.batchUpdate(orders);分布式锁防止集群环境下重复执行if(redisLock.tryLock(order:timeout:task, 10, TimeUnit.MINUTES)){ try { // 执行任务 } finally { redisLock.unlock(); } }5.2 WebSocket常见问题连接断开问题需要实现重连机制function connect() { const socket new WebSocket(url); socket.onclose () setTimeout(connect, 5000); }心跳检测保持连接活跃Scheduled(fixedRate 30000) public void sendHeartbeat() { webSocketServer.sendToAll(heartbeat); }消息堆积当客户端离线时要考虑消息存储public void sendMessage(String clientId, String message) { Session session sessions.get(clientId); if(session ! null session.isOpen()) { session.sendMessage(message); } else { redisTemplate.opsForList().leftPush(ws:offline:clientId, message); } }6. 测试与调试技巧6.1 定时任务测试测试定时任务时不要真的等15分钟临时修改cron表达式为更频繁比如每10秒一次Scheduled(cron 0/10 * * * * ?) // 测试用手动修改数据库中的订单时间使其看起来已经超时UPDATE orders SET order_time DATE_SUB(NOW(), INTERVAL 16 MINUTE) WHERE id 123;检查日志输出和数据库状态变更6.2 WebSocket调试Chrome开发者工具可以查看WebSocket消息打开开发者工具(F12)切换到Network标签过滤WS类型的请求查看Messages面板中的消息往来对于移动端可以使用代理工具如Charles抓包分析。7. 扩展思考这套方案不仅适用于外卖系统还可以应用到电商平台的订单超时取消在线课程的预约提醒医院挂号系统的超时支付处理共享单车的使用超时提醒不同场景下可以调整超时时间和处理逻辑。比如电商平台通常给30分钟支付时间而秒杀商品可能只给5分钟。