在构建外卖系统时,订单状态的智能流转和实时消息推送是提升用户体验的关键。我经历过多个外卖系统的开发,发现Spring Task和WebSocket的组合能完美解决这个问题。Spring Task就像个精准的闹钟,而WebSocket则是条永不挂断的电话线。
Spring Task特别适合处理以下场景:
而WebSocket的厉害之处在于:
这两个技术搭配使用时有个小技巧:Spring Task处理后台状态变更后,立即通过WebSocket广播通知所有相关方。就像餐厅后厨做好一道菜,服务员马上就知道该上菜了。
在实际项目中,我发现很多用户下单后会忘记支付。我们是这样实现的:
java复制@Scheduled(cron = "0 * * * * ?")
public void processTimeoutOrder(){
LocalDateTime deadline = LocalDateTime.now().minusMinutes(15);
List<Orders> unpaidOrders = orderMapper.getUnpaidOrders(deadline);
unpaidOrders.forEach(order -> {
order.setStatus(Orders.CANCELLED);
order.setCancelReason("支付超时,自动取消");
orderMapper.update(order);
log.info("已取消超时订单:{}", order.getId());
});
}
这里有几个优化点:
有些骑手送完餐会忘记点击完成,我们每天凌晨1点处理:
java复制@Scheduled(cron = "0 0 1 * * ?")
public void autoCompleteOrders(){
LocalDateTime yesterday = LocalDateTime.now().minusDays(1);
List<Orders> deliveringOrders = orderMapper.getDeliveringOrders(yesterday);
deliveringOrders.forEach(order -> {
order.setStatus(Orders.COMPLETED);
orderMapper.update(order);
// 同时更新骑手配送统计
deliveryService.updateDeliveryStats(order.getDeliverymanId());
});
}
踩过的坑:曾经有商家反馈凌晨自动完成的订单无法评价,后来我们改为只处理超过24小时的订单,并增加了异常状态过滤。
WebSocket服务端是消息中转站,这是经过多个项目验证的稳定版本:
java复制@ServerEndpoint("/ws/{shopId}")
public class WebSocketServer {
private static ConcurrentHashMap<String, Session> sessions = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("shopId") String shopId) {
sessions.put(shopId, session);
log.info("商家{}连接建立", shopId);
}
public static void sendMessage(String shopId, String message) {
Session session = sessions.get(shopId);
if(session != null && session.isOpen()) {
session.getAsyncRemote().sendText(message);
}
}
}
特别注意:在实际部署时发现需要添加心跳检测机制,否则Nginx会在60秒后断开空闲连接。
前端连接需要处理各种异常情况:
javascript复制let socket = null;
const maxRetries = 5;
let retryCount = 0;
function connect() {
socket = new WebSocket(`wss://${location.host}/ws/${shopId}`);
socket.onclose = () => {
if(retryCount++ < maxRetries) {
setTimeout(connect, 3000);
}
};
socket.onerror = () => socket.close();
}
// 页面加载时建立连接
connect();
实际项目中我们还增加了:
当用户下单并支付成功后:
java复制public void handleNewOrder(Long orderId) {
Order order = orderService.confirmPayment(orderId);
String message = buildOrderMessage(order);
// 获取该商家所有在线设备
List<String> devices = deviceService.getOnlineDevices(order.getShopId());
devices.forEach(deviceId -> {
WebSocketServer.sendMessage(deviceId, message);
});
// 同时发送APP推送
pushService.sendPushNotification(order.getShopId(), "您有新订单");
}
客户催单功能要特别注意频率控制:
java复制public void remindOrder(Long orderId) {
// 检查是否已催单过
if(reminderCache.getIfPresent(orderId) != null) {
throw new BusinessException("请勿频繁催单");
}
Order order = orderService.getById(orderId);
String message = buildReminderMessage(order);
WebSocketServer.sendMessage(order.getShopId(), message);
reminderCache.put(orderId, true, 5, TimeUnit.MINUTES);
// 记录催单日志
reminderLogService.logReminder(orderId);
}
我们使用Guava Cache实现5分钟内只能催单一次的限制,既保护商家不被骚扰,又能让紧急需求得到响应。
在高并发场景下,我们发现连接数过多会导致服务器压力大。解决方案是:
java复制@Scheduled(fixedRate = 30000)
public void checkConnections() {
sessions.forEach((shopId, session) -> {
if(!session.isOpen()) {
sessions.remove(shopId);
} else {
// 发送心跳包
session.getAsyncRemote().sendText("ping");
}
});
}
针对消息丢失问题,我们实现了三级保障机制:
消息格式示例:
json复制{
"msgId": "123e4567-e89b-12d3-a456-426614174000",
"type": 1,
"orderId": 10086,
"content": "新订单:红烧牛肉面×2",
"timestamp": 1625097600000,
"retryCount": 0
}
在线上环境部署时,我们遇到了几个典型问题:
nginx复制location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
SSL证书配置:WebSocket的wss协议需要正确配置证书链
负载均衡策略:需要保持会话粘滞,确保同一商家的请求总是路由到同一服务器
监控指标:我们添加了以下监控项
这套系统上线后,商家的平均接单时间从3分钟缩短到40秒,客户催单率下降了65%。特别是在午晚高峰时段,系统处理能力提升了3倍以上。