← 返回生产场景

📋 订单系统

下单流程 / 状态机 / 超时取消 / 幂等 / 订单履约

🔄问题1:订单状态机设计核心
订单从创建到完成经过多个状态,不能随便跳转(比如已退款不能再发货)。用状态机控制合法的状态流转。
状态code允许的下一步触发条件
待支付PENDING已支付 / 已取消用户下单
已支付PAID已发货 / 已退款支付回调成功
已发货SHIPPED已签收仓库发货
已签收DELIVERED已完成 / 退款中物流签收
已完成COMPLETED退款中(售后期内)确认收货/自动确认
已取消CANCELLED终态用户取消/超时取消
已退款REFUNDED终态退款完成
// 状态机:用枚举定义合法流转 public enum OrderStatus { PENDING, PAID, SHIPPED, DELIVERED, COMPLETED, CANCELLED, REFUNDED; // 定义每个状态允许的下一步 public Set<OrderStatus> nextStatuses() { return switch (this) { case PENDING -> Set.of(PAID, CANCELLED); case PAID -> Set.of(SHIPPED, REFUNDED); case SHIPPED -> Set.of(DELIVERED); case DELIVERED -> Set.of(COMPLETED); default -> Set.of(); // 终态不能流转 }; } } // 状态变更时校验 public void changeStatus(Long orderId, OrderStatus target) { Order order = orderMapper.selectById(orderId); if (!order.getStatus().nextStatuses().contains(target)) { throw new BizException("非法状态变更: " + order.getStatus() + " -> " + target); } // 用条件更新防并发(乐观锁) int n = orderMapper.updateStatus(orderId, order.getStatus(), target); // UPDATE orders SET status=#{target} WHERE id=#{id} AND status=#{oldStatus} if (n == 0) throw new BizException("订单状态已变更,请刷新重试"); }
关键:① 用枚举+方法定义合法流转,非法变更直接拒绝;② DB 层用 WHERE status=#{oldStatus} 条件更新防并发(乐观锁);③ 前端隐藏不该显示的操作按钮(但后端必须校验,前端可以绕过)。
问题2:订单超时自动取消高频
用户下单后30分钟不支付,自动取消订单、释放库存。怎么实现?
// 方案1:延时消息(推荐,RabbitMQ死信队列/RocketMQ延时消息) // 下单时发一条延时30分钟的消息,到期消费检查是否已支付 public void createOrder(Order order) { orderMapper.insert(order); // 发延时消息,30分钟后检查 rabbitTemplate.convertAndSend("order.delay.queue", order.getId(), msg -> { msg.getMessageProperties().setExpiration("1800000"); // 30分钟 return msg; }); } // 消费:检查是否已支付,没支付就取消 @RabbitListener(queues = "order.cancel.queue") public void handleTimeout(Long orderId) { Order order = orderMapper.selectById(orderId); if (order.getStatus() == OrderStatus.PENDING) { cancelOrder(orderId); // 取消+释放库存 } // 已支付就忽略 } // 方案2:Redis过期通知(轻量,但不可靠) // 方案3:定时任务扫描(简单,但有延迟+扫描压力大)
方案对比:延时消息最可靠(到期精准触发);定时任务最简单(每分钟扫一次,但最多延迟1分钟);Redis过期通知不可靠(过期事件可能丢)。生产推荐延时消息。
🔁问题3:防重复下单(幂等)必做
用户快速点两次"提交订单"按钮,不能创建两个订单。
// 方案:前端生成唯一 requestId,后端用唯一索引拦截 @PostMapping("/orders") public Result<OrderVO> create(@Valid @RequestBody CreateOrderDTO dto) { // dto.getRequestId() 由前端生成(UUID) try { return Result.ok(orderService.create(dto)); } catch (DuplicateKeyException e) { // requestId 唯一索引冲突 = 已经创建过了 return Result.ok(orderService.getByRequestId(dto.getRequestId())); } } // 数据表加唯一索引 // CREATE UNIQUE INDEX uk_request_id ON orders(request_id);
📦问题4:分布式事务(下单=扣库存+加积分+发券)
下单涉及多个服务,要保证要么全成功要么全回滚。
// 生产推荐:本地消息表(最终一致) @Transactional public void createOrder(CreateOrderDTO dto) { // 1. 创建订单 + 扣库存(同一个库,本地事务保证原子) orderMapper.insert(order); stockMapper.deduct(dto.getProductId()); // 2. 写本地消息表(和订单同一个事务) msgMapper.insert(new Msg("ADD_POINT", orderId)); // 订单和消息要么一起成功要么一起失败 } // 3. 定时任务扫消息表,发MQ给积分服务(消费端幂等)
核心:把跨服务事务拆成"本地事务+消息",用本地消息表保证可靠。详见 分布式事务专题
订单系统核心:① 状态机控制流转 ② 超时自动取消(延时消息)③ 幂等防重复 ④ 分布式事务(本地消息表)
生产场景 · 订单系统 · 全栈资料库