1. 项目背景与核心需求
在餐饮外卖系统的实际运营中,订单状态流转的及时性和准确性直接影响用户体验和商家运营效率。传统做法是依赖用户主动刷新页面或服务员手动操作,这种方式存在明显的滞后性。以"苍穹外卖"这个日订单量超过5000单的中型平台为例,我们遇到了三个典型问题:
- 超时未支付订单堆积:用户下单后未支付,15分钟后系统未自动关闭订单,导致库存虚占
- 商家接单响应延迟:新订单到达时,商家端没有实时提醒,平均响应时间达8分钟
- 催单处理效率低下:用户发起催单后,前台服务员需要人工通知后厨,平均耗时3分钟
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术方案选型与架构设计
2.1 定时任务技术对比
我们对比了三种主流的定时任务实现方案:
| 方案 | 触发精度 | 分布式支持 | 管理界面 | 学习成本 | 适用场景 |
|---|---|---|---|---|---|
| Spring Scheduled | 秒级 | 需额外配置 | 无 | 低 | 单机简单任务 |
| Quartz | 毫秒级 | 支持 | 需开发 | 中 | 复杂调度需求 |
| XXL-JOB | 秒级 | 原生支持 | 完善 | 低 | 企业级分布式调度 |
最终选择XXL-JOB作为核心调度引擎,主要基于:
- 天生支持分布式部署,避免单点故障
- 提供可视化控制台,方便监控和干预
- 失败重试和报警机制完善
- 与Spring Boot集成仅需3步配置
2.2 状态流转设计
订单状态机采用状态模式实现,核心状态包括:
java复制public enum OrderStatus {
PENDING_PAYMENT, // 待支付
PAID, // 已支付
ACCEPTED, // 已接单
COOKING, // 制作中
DELIVERING, // 配送中
COMPLETED, // 已完成
CANCELLED // 已取消
}
状态转换规则通过状态模式封装,避免复杂的if-else判断:
java复制public interface OrderState {
void pay(Order order);
void accept(Order order);
void cancel(Order order);
// 其他状态方法...
}
// 具体状态类实现业务逻辑
public class PendingPaymentState implements OrderState {
@Override
public void pay(Order order) {
order.setState(new PaidState());
// 记录状态变更日志
}
// 其他方法实现...
}
2.3 实时通知方案
采用WebSocket+本地推送双保险机制:
- WebSocket建立长连接通道
- 本地使用Notification API作为降级方案
- 消息去重采用Redis SETNX实现
消息协议设计:
json复制{
"msgId": "uuid",
"type": "NEW_ORDER|REMINDER|URGE",
"content": {
"orderId": "123",
"createTime": "2023-07-20T14:30:00",
"timeout": 300
},
"retry": 3
}
3. 核心实现细节
3.1 订单超时处理
XXL-JOB配置示例:
properties复制# 任务配置
xxl.job.executor.appname=food-delivery
xxl.job.executor.port=9999
xxl.job.accessToken=
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
xxl.job.executor.logretentiondays=30
# 超时任务配置
xxl.job.jobGroup=2
xxl.job.jobDesc=订单超时关闭
xxl.job.author=tech_lead
xxl.job.scheduleType=CRON
xxl.job.scheduleConf=0 */1 * * * ?
xxl.job.glueType=BEAN
xxl.job.executorHandler=orderTimeoutHandler
业务逻辑实现要点:
- 使用JPA的@Modifying和@Query注解实现批量更新
- 采用乐观锁避免并发问题
- 记录操作日志用于对账
java复制@XxlJob("orderTimeoutHandler")
public void handleTimeoutOrders() {
LocalDateTime threshold = LocalDateTime.now().minusMinutes(15);
List<Order> timeoutOrders = orderRepo.findByStatusAndCreateTimeBefore(
OrderStatus.PENDING_PAYMENT, threshold);
timeoutOrders.forEach(order -> {
order.setStatus(OrderStatus.CANCELLED);
order.setCancelReason("超时未支付");
inventoryService.releaseStock(order.getItems());
});
orderRepo.saveAll(timeoutOrders);
}
3.2 实时提醒实现
WebSocket配置核心代码:
java复制@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-notify")
.setAllowedOrigins("*")
.withSockJS();
}
}
消息推送服务:
java复制@Service
@RequiredArgsConstructor
public class NotificationService {
private final SimpMessagingTemplate messagingTemplate;
private final RedisTemplate<String, String> redisTemplate;
public void notifyNewOrder(Order order) {
String storeId = order.getStoreId();
String key = "notify:new:" + storeId + ":" + order.getId();
if (redisTemplate.opsForValue().setIfAbsent(key, "1", 5, TimeUnit.MINUTES)) {
NotificationMsg msg = new NotificationMsg(
"NEW_ORDER",
Map.of("orderId", order.getId(), "amount", order.getTotalAmount())
);
messagingTemplate.convertAndSend(
"/topic/store/" + storeId,
msg
);
// 本地通知降级
if (!pushNativeNotification(storeId, msg)) {
log.warn("Native notification failed for store {}", storeId);
}
}
}
}
3.3 催单处理优化
催单流程改造前后对比:
| 环节 | 改造前 | 改造后 |
|---|---|---|
| 用户发起 | 需拨打商家电话 | APP内一键催单 |
| 通知方式 | 服务员口头传达 | 后厨打印机自动打印红色单据 |
| 响应时间 | 3-5分钟 | 即时 |
| 状态追踪 | 无记录 | 系统记录每次催单时间 |
核心代码实现:
java复制public class UrgeService {
@Transactional
public void handleUrge(String orderId) {
Order order = orderRepo.findById(orderId)
.orElseThrow(() -> new BizException("订单不存在"));
if (order.getUrgeCount() >= 3) {
throw new BizException("催单次数已达上限");
}
order.setUrgeCount(order.getUrgeCount() + 1);
order.setLastUrgeTime(LocalDateTime.now());
orderRepo.save(order);
// 触发后厨提醒
kitchenNotifyService.notifyUrge(
order.getStoreId(),
order.getId(),
order.getUrgeCount()
);
}
}
4. 性能优化与问题排查
4.1 定时任务执行监控
通过Prometheus+Grafana搭建监控看板,关键指标:
- 任务执行耗时分布
- 任务执行成功率
- 任务排队数量
- 资源占用情况
配置示例:
yaml复制management:
endpoints:
web:
exposure:
include: prometheus
metrics:
export:
prometheus:
enabled: true
tags:
application: ${spring.application.name}
4.2 常见问题解决方案
问题1:分布式环境下的任务重复执行
- 现象:同一个任务在多个实例上同时执行
- 解决方案:采用Redis分布式锁
java复制public void executeWithLock(String lockKey, Runnable task) {
String lockValue = UUID.randomUUID().toString();
try {
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);
if (locked != null && locked) {
task.run();
}
} finally {
// 确保释放自己的锁
if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
问题2:WebSocket连接不稳定
- 现象:移动端网络切换时连接中断
- 解决方案:实现自动重连机制
javascript复制let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
const reconnectDelay = 3000;
function connectWebSocket() {
const socket = new SockJS('/ws-notify');
const stompClient = Stomp.over(socket);
stompClient.connect({}, () => {
reconnectAttempts = 0;
// 订阅逻辑...
}, (error) => {
if (reconnectAttempts < maxReconnectAttempts) {
setTimeout(() => {
reconnectAttempts++;
connectWebSocket();
}, reconnectDelay);
}
});
}
5. 实际效果与业务指标
上线两周后的关键指标对比:
| 指标 | 改造前 | 改造后 | 提升幅度 |
|---|---|---|---|
| 超时订单占比 | 12.3% | 0.8% | 93.5%↓ |
| 商家平均接单时间 | 8分12秒 | 2分45秒 | 66.7%↓ |
| 催单投诉率 | 6.2% | 1.1% | 82.3%↓ |
| 库存周转率 | 3.2次/天 | 4.7次/天 | 46.9%↑ |
6. 扩展优化方向
- 智能调度进阶:结合历史数据预测备餐时间,动态调整状态流转阈值
java复制public class SmartScheduler {
public Duration predictPrepareTime(String storeId, List<MenuItem> items) {
// 基于店铺历史数据+当前负荷的预测算法
}
}
-
消息分级处理:根据订单金额、用户等级等维度设置不同的提醒优先级
-
语音播报集成:与智能音响设备对接,实现语音播报新订单和催单
-
压力测试方案:
bash复制# 使用wrk模拟并发催单
wrk -t12 -c400 -d60s --latency \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-s scripts/urge.lua http://localhost:8080/api/urge
在实际开发中,我们发现XXL-JOB的控制台API存在频率限制,需要调整源码中的限流配置。另外,iOS系统的通知权限获取需要特别处理,否则静默期会导致提醒延迟。这些细节问题往往在测试环境难以发现,建议在上线前进行全链路压测。
