订单系统设计
1. 订单核心数据模型
1.1 订单主表
订单主表存储订单的核心信息,包含业务字段、状态字段、时间字段和物流字段。
java
@Entity
@Table(name = "order_main")
public class OrderMain {
// ========== 业务主键 ==========
@Id
@Column(name = "order_id", length = 32, nullable = false)
private String orderId; // 订单号,分布式ID生成
@Column(name = "user_id", length = 32, nullable = false)
private String userId; // 用户ID
@Column(name = "merchant_id", length = 32)
private String merchantId; // 商户ID(多商户场景)
@Column(name = "store_id", length = 32)
private String storeId; // 门店ID
// ========== 金额字段 ==========
@Column(name = "total_amount", nullable = false)
private Long totalAmount; // 订单总金额(单位:分)
@Column(name = "pay_amount")
private Long payAmount; // 实付金额(单位:分)
@Column(name = "freight_amount")
private Long freightAmount; // 运费金额(单位:分)
@Column(name = "discount_amount")
private Long discountAmount; // 优惠金额(单位:分)
// ========== 状态字段 ==========
@Enumerated(EnumType.STRING)
@Column(name = "order_status", length = 32, nullable = false)
private OrderStatus orderStatus; // 订单状态
@Enumerated(EnumType.STRING)
@Column(name = "pay_status", length = 16)
private PayStatus payStatus; // 支付状态
@Enumerated(EnumType.STRING)
@Column(name = "refund_status", length = 16)
private RefundStatus refundStatus; // 退款状态
@Enumerated(EnumType.STRING)
@Column(name = "delivery_status", length = 16)
private DeliveryStatus deliveryStatus; // 发货状态
// ========== 时间字段 ==========
@Column(name = "create_time")
private LocalDateTime createTime; // 下单时间
@Column(name = "pay_time")
private LocalDateTime payTime; // 支付时间
@Column(name = "delivery_time")
private LocalDateTime deliveryTime; // 发货时间
@Column(name = "confirm_time")
private LocalDateTime confirmTime; // 确认收货时间
@Column(name = "cancel_time")
private LocalDateTime cancelTime; // 取消时间
@Column(name = "close_time")
private LocalDateTime closeTime; // 订单关闭时间
// ========== 地址信息 ==========
@Column(name = "receiver_name", length = 64)
private String receiverName; // 收货人姓名
@Column(name = "receiver_phone", length = 20)
private String receiverPhone; // 收货人手机号
@Column(name = "receiver_province", length = 32)
private String receiverProvince; // 省
@Column(name = "receiver_city", length = 32)
private String receiverCity; // 市
@Column(name = "receiver_district", length = 32)
private String receiverDistrict; // 区
@Column(name = "receiver_address", length = 256)
private String receiverAddress; // 详细地址
@Column(name = "receiver_postcode", length = 6)
private String receiverPostcode; // 邮编
// ========== 支付信息 ==========
@Column(name = "pay_channel", length = 32)
private String payChannel; // 支付渠道(wechat/alipay)
@Column(name = "pay_order_id", length = 64)
private String payOrderId; // 支付平台订单号
@Column(name = "payment_no", length = 64)
private String paymentNo; // 商户支付流水号
// ========== 其他字段 ==========
@Column(name = "remark", length = 500)
private String remark; // 订单备注
@Column(name = "version")
private Integer version; // 乐观锁版本号
}1.2 订单明细表
订单明细表记录每笔订单中包含的商品信息。
java
@Entity
@Table(name = "order_item")
public class OrderItem {
@Id
@Column(name = "item_id", length = 32, nullable = false)
private String itemId; // 明细ID
@Column(name = "order_id", length = 32, nullable = false)
private String orderId; // 订单号
@Column(name = "sku_id", length = 32, nullable = false)
private String skuId; // SKU ID
@Column(name = "spu_id", length = 32)
private String spuId; // SPU ID
@Column(name = "sku_code", length = 64)
private String skuCode; // SKU 编码
@Column(name = "sku_name", length = 256, nullable = false)
private String skuName; // 商品名称
@Column(name = "sku_image", length = 512)
private String skuImage; // 商品图片URL
@Column(name = "price", nullable = false)
private Long price; // 单价(单位:分)
@Column(name = "quantity", nullable = false)
private Integer quantity; // 购买数量
@Column(name = "total_price", nullable = false)
private Long totalPrice; // 小计金额(单位:分)
@Column(name = "coupon_amount")
private Long couponAmount; // 优惠分摊金额
// ========== 售后相关 ==========
@Column(name = "refund_quantity")
private Integer refundQuantity; // 已退款数量
@Column(name = "refund_amount")
private Long refundAmount; // 已退款金额
@Column(name = "return_quantity")
private Integer returnQuantity; // 已退货数量
@Column(name = "after_sale_status")
private AfterSaleStatus afterSaleStatus; // 该明细的售后状态
}1.3 订单状态枚举
java
/**
* 订单主状态
*/
public enum OrderStatus {
PENDING_PAYMENT("待付款"),
PAID("已付款"),
DELIVERED("已发货"),
RECEIVED("已收货"),
COMPLETED("已完成"),
CANCELLED("已取消"),
AFTER_SALE("售后中"),
CLOSED("已关闭");
private final String desc;
OrderStatus(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
}
/**
* 支付状态
*/
public enum PayStatus {
UNPAID("未支付"),
PAYING("支付中"),
PAID("已支付"),
REFUNDING("退款中"),
REFUNDED("已退款"),
PARTIAL_REFUND("部分退款");
private final String desc;
// constructor, getter ...
}
/**
* 发货状态
*/
public enum DeliveryStatus {
NOT_DELIVERED("未发货"),
DELIVERING("发货中"),
DELIVERED("已发货"),
RECEIVED("已收货"),
PARTIAL_DELIVERY("部分发货");
private final String desc;
// constructor, getter ...
}1.4 订单号设计
订单号采用分布式ID方案,建议格式:业务标识 + 时间戳 + 序列号。
text
订单号格式: 2位业务码 + 14位时间戳 + 4位机器ID + 8位序列号 = 28位
示例: OD20250710142000000100000001
|--||---------------||------||--------|
业务 时间戳(yyMMddHHmmss) 机器ID 序列号java
@Component
public class OrderIdGenerator {
private static final String BIZ_CODE = "OD";
public String generate() {
// 使用雪花算法或发号器生成唯一ID
String timestamp = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyMMddHHmmss"));
String workerId = String.format("%04d", getWorkerId());
String seq = String.format("%08d", getSequence());
return BIZ_CODE + timestamp + workerId + seq;
}
private long getWorkerId() { /* 从配置中心获取机器ID */ }
private long getSequence() { /* 基于Redis自增或本地分段 */ }
}2. 订单状态机
2.1 状态流转图
┌─────────────┐
│ 待付款 │
└──────┬──────┘
│
┌───────────┼───────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 已取消 │ │ 已付款 │ │ 已关闭 │
└──────────┘ └─────┬────┘ └──────────┘
│
▼
┌──────────────┐
│ 已发货 │
└──────┬───────┘
│
▼
┌──────────────┐
│ 已收货 │
└──────┬───────┘
│
▼
┌──────────────┐
│ 已完成 │
└──────┬───────┘
│
┌──────┴───────┐
│ 售后中 │
└──────┬───────┘
│
┌──────┴───────┐
│ 已关闭 │
└──────────────┘2.2 Spring Statemachine 配置
java
@Configuration
@EnableStateMachine(name = "orderStateMachine")
public class OrderStateMachineConfig
extends StateMachineConfigurerAdapter<OrderStatus, OrderEvent> {
/**
* 状态定义
*/
@Override
public void configure(StateMachineStateConfigurer<OrderStatus, OrderEvent> states)
throws Exception {
states
.withStates()
.initial(OrderStatus.PENDING_PAYMENT)
.states(EnumSet.allOf(OrderStatus.class))
// 定义复合状态:售后中状态下包含多个子状态
.state(OrderStatus.AFTER_SALE)
// 结束状态
.end(OrderStatus.CLOSED)
.end(OrderStatus.CANCELLED);
}
/**
* 状态转移定义
*/
@Override
public void configure(StateMachineTransitionConfigurer<OrderStatus, OrderEvent> transitions)
throws Exception {
transitions
// 待付款 -> 已付款(支付成功)
.withExternal()
.source(OrderStatus.PENDING_PAYMENT)
.target(OrderStatus.PAID)
.event(OrderEvent.PAY_SUCCESS)
.guard(payGuard())
.action(payAction())
// 待付款 -> 已取消(用户取消或超时取消)
.and()
.withExternal()
.source(OrderStatus.PENDING_PAYMENT)
.target(OrderStatus.CANCELLED)
.event(OrderEvent.CANCEL)
.action(cancelAction())
// 待付款 -> 已关闭(超时未支付自动关闭)
.and()
.withExternal()
.source(OrderStatus.PENDING_PAYMENT)
.target(OrderStatus.CLOSED)
.event(OrderEvent.TIMEOUT_CLOSE)
.action(timeoutCloseAction())
// 已付款 -> 已发货
.and()
.withExternal()
.source(OrderStatus.PAID)
.target(OrderStatus.DELIVERED)
.event(OrderEvent.DELIVER)
.guard(deliverGuard())
.action(deliverAction())
// 已发货 -> 已收货(用户确认收货)
.and()
.withExternal()
.source(OrderStatus.DELIVERED)
.target(OrderStatus.RECEIVED)
.event(OrderEvent.CONFIRM_RECEIPT)
.action(confirmReceiptAction())
// 已收货 -> 已完成(自动确认完成)
.and()
.withExternal()
.source(OrderStatus.RECEIVED)
.target(OrderStatus.COMPLETED)
.event(OrderEvent.AUTO_COMPLETE)
// 已完成 -> 售后中
.and()
.withExternal()
.source(OrderStatus.COMPLETED)
.target(OrderStatus.AFTER_SALE)
.event(OrderEvent.AFTER_SALE_APPLY)
// 已付款 -> 售后中(未发货仅退款)
.and()
.withExternal()
.source(OrderStatus.PAID)
.target(OrderStatus.AFTER_SALE)
.event(OrderEvent.AFTER_SALE_APPLY)
// 售后中 -> 已完成(售后完成)
.and()
.withExternal()
.source(OrderStatus.AFTER_SALE)
.target(OrderStatus.COMPLETED)
.event(OrderEvent.AFTER_SALE_COMPLETE);
}
/**
* 状态机全局配置
*/
@Override
public void configure(StateMachineConfigurationConfigurer<OrderStatus, OrderEvent> config)
throws Exception {
config
.withConfiguration()
.machineId("orderStateMachine")
.listener(orderStateMachineListener());
}
}2.3 事件枚举
java
public enum OrderEvent {
PAY_SUCCESS, // 支付成功
CANCEL, // 取消订单
TIMEOUT_CLOSE, // 超时关闭
DELIVER, // 发货
CONFIRM_RECEIPT, // 确认收货
AUTO_COMPLETE, // 自动完成
AFTER_SALE_APPLY, // 申请售后
AFTER_SALE_COMPLETE, // 售后完成
REFUND_SUCCESS; // 退款成功
}2.4 Guard(守卫条件)
Guard 用于判断状态转移是否满足前置条件。
java
@Component
public class PayGuard implements Guard<OrderStatus, OrderEvent> {
@Override
public boolean evaluate(StateContext<OrderStatus, OrderEvent> context) {
OrderEvent event = context.getMessage().getPayload();
PayMessage payMessage = (PayMessage) context.getMessageHeader("payMessage");
// 校验:支付金额与订单金额一致
if (payMessage == null || payMessage.getPayAmount() <= 0) {
return false;
}
// 校验:支付金额 >= 订单应付金额
OrderMain order = (OrderMain) context.getExtendedState()
.getVariables().get("order");
return payMessage.getPayAmount().compareTo(order.getPayAmount()) >= 0;
}
}
@Component
public class DeliverGuard implements Guard<OrderStatus, OrderEvent> {
@Override
public boolean evaluate(StateContext<OrderStatus, OrderEvent> context) {
// 发货前校验:订单必须已付款
OrderMain order = (OrderMain) context.getExtendedState()
.getVariables().get("order");
return order.getPayStatus() == PayStatus.PAID;
}
}2.5 Action(执行动作)
java
@Component
public class PayAction implements Action<OrderStatus, OrderEvent> {
@Autowired
private OrderRepository orderRepository;
@Override
public void execute(StateContext<OrderStatus, OrderEvent> context) {
OrderMain order = (OrderMain) context.getExtendedState()
.getVariables().get("order");
PayMessage payMessage = (PayMessage) context.getMessageHeader("payMessage");
// 更新订单支付信息
order.setPayStatus(PayStatus.PAID);
order.setOrderStatus(OrderStatus.PAID);
order.setPayTime(LocalDateTime.now());
order.setPayChannel(payMessage.getPayChannel());
order.setPayOrderId(payMessage.getTransactionId());
order.setPaymentNo(payMessage.getPaymentNo());
orderRepository.save(order);
}
}
@Component
public class CancelAction implements Action<OrderStatus, OrderEvent> {
@Autowired
private OrderRepository orderRepository;
@Autowired
private InventoryService inventoryService;
@Override
public void execute(StateContext<OrderStatus, OrderEvent> context) {
OrderMain order = (OrderMain) context.getExtendedState()
.getVariables().get("order");
order.setOrderStatus(OrderStatus.CANCELLED);
order.setCancelTime(LocalDateTime.now());
orderRepository.save(order);
// 回滚库存(预扣库存释放)
inventoryService.rollbackStock(order.getOrderId());
}
}2.6 状态机持久化
状态机需要持久化,以便在应用重启后能恢复订单状态。
java
@Component
public class OrderStateMachineService {
@Autowired
private StateMachineFactory<OrderStatus, OrderEvent> stateMachineFactory;
@Autowired
private StateMachinePersister<OrderStatus, OrderEvent, String> persister;
/**
* 创建新订单的状态机
*/
public StateMachine<OrderStatus, OrderEvent> create(String orderId) {
StateMachine<OrderStatus, OrderEvent> sm = stateMachineFactory.getStateMachine(orderId);
sm.start();
// 持久化状态机
persister.persist(sm, orderId);
return sm;
}
/**
* 发送事件触发状态转移
*/
public boolean sendEvent(String orderId, OrderEvent event, Map<String, Object> headers) {
StateMachine<OrderStatus, OrderEvent> sm = stateMachineFactory.getStateMachine(orderId);
// 从数据库恢复状态机
persister.restore(sm, orderId);
Message<OrderEvent> message = MessageBuilder
.withPayload(event)
.copyHeaders(headers)
.build();
boolean accepted = sm.sendEvent(message);
if (accepted) {
// 持久化更新后的状态
persister.persist(sm, orderId);
}
return accepted;
}
}java
/**
* 基于 JPA 的状态机持久化实现
*/
@Component
public class JpaStateMachinePersister
implements StateMachinePersister<OrderStatus, OrderEvent, String> {
@Autowired
private OrderStateMachineRepository repository;
@Override
public void persist(StateMachine<OrderStatus, OrderEvent> stateMachine,
String orderId) {
OrderStateMachineEntity entity = repository.findById(orderId)
.orElse(new OrderStateMachineEntity());
entity.setOrderId(orderId);
entity.setState(stateMachine.getState().getId().name());
entity.setStateMachineObj(serializeStateMachine(stateMachine));
repository.save(entity);
}
@Override
public StateMachine<OrderStatus, OrderEvent> restore(
StateMachine<OrderStatus, OrderEvent> stateMachine, String orderId) {
OrderStateMachineEntity entity = repository.findById(orderId).orElse(null);
if (entity != null) {
deserializeStateMachine(stateMachine, entity.getStateMachineObj());
}
return stateMachine;
}
private String serializeStateMachine(StateMachine<OrderStatus, OrderEvent> sm) {
// 将状态机内存状态序列化为 JSON 存储
StateMachineContext<OrderStatus, OrderEvent> ctx =
StateMachineContextRepository.get(sm);
return JacksonUtils.toJson(ctx);
}
private void deserializeStateMachine(
StateMachine<OrderStatus, OrderEvent> sm, String data) {
// 从 JSON 反序列化恢复状态机上下文
StateMachineContext<OrderStatus, OrderEvent> ctx =
JacksonUtils.fromJson(data, StateMachineContext.class);
StateMachineContextRepository.set(sm, ctx);
}
}2.7 状态机事件驱动
使用 Spring 事件机制解耦状态转移和业务逻辑。
java
@Component
public class OrderStateMachineListener
implements StateMachineListener<OrderStatus, OrderEvent> {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Override
public void stateChanged(StateMachineEvent<OrderStatus, OrderEvent> event) {
// 发布领域事件,供其他服务消费
OrderStateChangedEvent domainEvent = new OrderStateChangedEvent(
this,
event.getSource().getId(),
event.getSource().getState().getId(),
event.getSource().getTarget().getId()
);
eventPublisher.publishEvent(domainEvent);
}
}java
/**
* 订单状态变更领域事件
*/
public class OrderStateChangedEvent extends ApplicationEvent {
private final String orderId;
private final OrderStatus sourceState;
private final OrderStatus targetState;
public OrderStateChangedEvent(
Object source, String orderId,
OrderStatus sourceState, OrderStatus targetState) {
super(source);
this.orderId = orderId;
this.sourceState = sourceState;
this.targetState = targetState;
}
// getters ...
}java
@Component
public class OrderStateChangeHandler {
@EventListener
@Async
public void handleOrderStateChanged(OrderStateChangedEvent event) {
// 发送通知(短信、邮件、推送)
// 记录操作日志
// 更新搜索引擎索引
// 触发数据同步
}
}3. 超时取消
3.1 延迟任务方案对比
| 方案 | 实现复杂度 | 精度 | 可靠性 | 适用场景 |
|---|---|---|---|---|
| 定时扫表 | 低 | 分钟级(取决于扫表周期) | 高(基于数据库持久化) | 对时间精度要求不高、数据量较小 |
| Redis 过期监听 | 低 | 秒级 | 低(Redis 不保证过期通知必达,丢失无补偿) | 辅助方案,不能作为唯一保障 |
| 延迟队列 | 中 | 毫秒级 | 高 | 对时间精度有要求、单机部署 |
| RocketMQ 延迟消息 | 中 | 秒级(固定等级) | 高(消息持久化 + 重试) | 分布式场景,推荐方案 |
3.2 RocketMQ 延迟消息实现订单超时取消
RocketMQ 支持固定等级的延迟消息,延迟等级定义:
text
1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m
1h 2h 6h 12h 24h 48h 72h 96h 128d 256d 512d 1024d30分钟超时取消对应的延迟等级为 16(30分钟)。
发送延迟消息
java
@Component
public class OrderTimeoutProducer {
@Autowired
private RocketMQTemplate rocketMqTemplate;
private static final String TOPIC = "ORDER_TIMEOUT";
private static final int DELAY_LEVEL_30M = 16; // 30分钟延迟等级
/**
* 下单成功后发送延迟消息,用于超时自动取消
*/
public void sendOrderTimeoutMessage(OrderMain order) {
OrderTimeoutMessage message = new OrderTimeoutMessage();
message.setOrderId(order.getOrderId());
message.setUserId(order.getUserId());
message.setCreateTime(order.getCreateTime());
rocketMqTemplate.syncSend(
TOPIC + ":timeout_tag",
message,
// 超时时间
3000,
// 延迟等级(30分钟后投递)
DELAY_LEVEL_30M
);
}
}消费延迟消息
java
@Component
@RocketMQMessageListener(
topic = "ORDER_TIMEOUT",
consumerGroup = "order_timeout_group",
selectorExpression = "timeout_tag"
)
public class OrderTimeoutConsumer implements RocketMQListener<OrderTimeoutMessage> {
@Autowired
private OrderStateMachineService stateMachineService;
@Autowired
private OrderRepository orderRepository;
@Override
public void onMessage(OrderTimeoutMessage message) {
String orderId = message.getOrderId();
// 幂等检查:已在消费记录表中存在则跳过
if (isConsumed(orderId)) {
return;
}
// 查询订单当前状态
OrderMain order = orderRepository.findById(orderId).orElse(null);
if (order == null) {
return;
}
// 只有待付款状态的订单才触发超时关闭
if (order.getOrderStatus() != OrderStatus.PENDING_PAYMENT) {
return;
}
// 发送超时关闭事件
Map<String, Object> headers = new HashMap<>();
headers.put("order", order);
headers.put("cancelReason", "TIMEOUT");
boolean success = stateMachineService.sendEvent(
orderId, OrderEvent.TIMEOUT_CLOSE, headers);
if (success) {
markConsumed(orderId);
} else {
// 处理失败,RocketMQ 会重试
throw new RuntimeException("订单超时取消失败: " + orderId);
}
}
private boolean isConsumed(String orderId) {
// 基于Redis或数据库的去重表
return false;
}
private void markConsumed(String orderId) {
// 记录消费流水
}
}3.3 定时扫表 + 分片方案
作为兜底方案,定时扫表可以弥补延迟消息可能丢失的问题。
java
@Component
public class OrderTimeoutScanTask {
@Autowired
private OrderRepository orderRepository;
@Autowired
private OrderStateMachineService stateMachineService;
// 每30秒执行一次
@Scheduled(cron = "*/30 * * * * ?")
public void scanTimeoutOrders() {
// 查询已超时的待付款订单
LocalDateTime deadline = LocalDateTime.now().minusMinutes(30);
List<OrderMain> timeoutOrders = orderRepository
.findByOrderStatusAndCreateTimeBefore(
OrderStatus.PENDING_PAYMENT, deadline);
for (OrderMain order : timeoutOrders) {
try {
Map<String, Object> headers = new HashMap<>();
headers.put("order", order);
headers.put("cancelReason", "TIMEOUT_SCAN");
stateMachineService.sendEvent(
order.getOrderId(), OrderEvent.TIMEOUT_CLOSE, headers);
} catch (Exception e) {
log.error("扫表超时取消失败: {}", order.getOrderId(), e);
}
}
}
}分片扫表方案(数据量大时)
java
@Component
public class ShardingTimeoutScanTask {
// 每个分片扫描 10 万条
private static final int PAGE_SIZE = 100000;
@Scheduled(cron = "*/30 * * * * ?")
public void scanBySharding() {
LocalDateTime deadline = LocalDateTime.now().minusMinutes(30);
long totalCount = orderRepository
.countByOrderStatusAndCreateTimeBefore(
OrderStatus.PENDING_PAYMENT, deadline);
int totalPages = (int) Math.ceil((double) totalCount / PAGE_SIZE);
// 使用线程池并行扫描各分片
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (int page = 0; page < totalPages; page++) {
int currentPage = page;
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
Pageable pageable = PageRequest.of(currentPage, PAGE_SIZE);
List<OrderMain> orders = orderRepository
.findByOrderStatusAndCreateTimeBefore(
OrderStatus.PENDING_PAYMENT, deadline, pageable);
processOrders(orders);
}, scanExecutor);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}
private void processOrders(List<OrderMain> orders) {
for (OrderMain order : orders) {
// 处理超时取消逻辑
}
}
}4. 库存扣减
4.1 库存扣减流程
下单 ──> 预扣库存 ──> 支付成功 ──> 确定扣减
│
└──> 超时/取消 ──> 回滚库存库存扣减采用两阶段设计:
| 阶段 | 操作 | 说明 |
|---|---|---|
| 下单预扣 | 冻结库存 | 用户下单时先行冻结库存,防止超卖 |
| 支付确认 | 扣减冻结库存 | 用户支付成功后,将冻结库存正式扣减 |
| 超时回滚 | 释放冻结库存 | 订单超时取消/用户取消时,释放冻结库存 |
4.2 Redis 原子扣减(防止超卖)
java
@Component
public class StockService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private StockRepository stockRepository;
private static final String STOCK_KEY_PREFIX = "stock:sku:";
private static final String FROZEN_KEY_PREFIX = "stock:frozen:";
/**
* 预扣库存(下单时调用)
*
* @return true=扣减成功,false=库存不足
*/
public boolean tryDeductStock(String skuId, Integer quantity) {
String stockKey = STOCK_KEY_PREFIX + skuId;
String frozenKey = FROZEN_KEY_PREFIX + skuId;
// 使用 Lua 脚本保证原子性
String luaScript =
"local stock = redis.call('GET', KEYS[1]) " +
"if not stock or tonumber(stock) < tonumber(ARGV[1]) then " +
" return 0 " +
"end " +
"redis.call('DECRBY', KEYS[1], ARGV[1]) " +
"redis.call('INCRBY', KEYS[2], ARGV[1]) " +
"return 1";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(
script, Arrays.asList(stockKey, frozenKey), quantity.toString());
return Long.valueOf(1).equals(result);
}
/**
* 确认扣减(支付成功时调用)
*/
public void confirmDeduct(String skuId, Integer quantity) {
String frozenKey = FROZEN_KEY_PREFIX + skuId;
// 从冻结库存中扣减(仅更新 Redis,数据库异步持久化)
redisTemplate.opsForValue().decrement(frozenKey, quantity);
// 发送消息异步同步数据库
sendStockChangeMessage(skuId, quantity, StockChangeType.CONFIRM);
}
/**
* 回滚库存(订单取消时调用)
*/
public void rollbackStock(String skuId, Integer quantity) {
String stockKey = STOCK_KEY_PREFIX + skuId;
String frozenKey = FROZEN_KEY_PREFIX + skuId;
String luaScript =
"redis.call('INCRBY', KEYS[1], ARGV[1]) " +
"redis.call('DECRBY', KEYS[2], ARGV[1]) " +
"return 1";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
redisTemplate.execute(
script, Arrays.asList(stockKey, frozenKey), quantity.toString());
sendStockChangeMessage(skuId, quantity, StockChangeType.ROLLBACK);
}
}4.3 数据库最终一致性
Redis 库存操作后,通过异步消息同步数据库,保证最终一致性。
java
@Component
@RocketMQMessageListener(
topic = "STOCK_CHANGE",
consumerGroup = "stock_confirm_group"
)
public class StockChangeConsumer implements RocketMQListener<StockChangeMessage> {
@Autowired
private StockRepository stockRepository;
@Override
public void onMessage(StockChangeMessage message) {
// 使用乐观锁更新数据库库存
int affectedRows;
if (message.getChangeType() == StockChangeType.CONFIRM) {
// UPDATE sku_stock SET frozen_stock = frozen_stock - ?,
// actual_stock = actual_stock - ?, version = version + 1
// WHERE sku_id = ? AND version = ?
affectedRows = stockRepository.confirmDeduct(
message.getSkuId(),
message.getQuantity(),
message.getVersion());
} else {
// 回滚:释放冻结库存
affectedRows = stockRepository.rollbackStock(
message.getSkuId(),
message.getQuantity(),
message.getVersion());
}
if (affectedRows == 0) {
// 乐观锁冲突,重试
throw new RuntimeException("库存更新冲突,重试: " + message.getSkuId());
}
}
}java
// 数据库乐观锁更新
@Modifying
@Query(value = "UPDATE sku_stock SET " +
"actual_stock = actual_stock - :quantity, " +
"frozen_stock = frozen_stock - :quantity, " +
"version = version + 1 " +
"WHERE sku_id = :skuId AND version = :version " +
"AND actual_stock >= :quantity AND frozen_stock >= :quantity",
nativeQuery = true)
int confirmDeduct(@Param("skuId") String skuId,
@Param("quantity") Integer quantity,
@Param("version") Integer version);4.4 库存流水表设计
每笔库存变更都需要记录流水,用于对账和问题排查。
java
@Entity
@Table(name = "stock_flow")
public class StockFlow {
@Id
@Column(name = "flow_id", length = 32, nullable = false)
private String flowId; // 流水号
@Column(name = "sku_id", length = 32, nullable = false)
private String skuId; // SKU ID
@Column(name = "order_id", length = 32)
private String orderId; // 关联订单号
@Column(name = "change_type", length = 16, nullable = false)
private String changeType; // 变更类型:DEDUCT/CONFIRM/ROLLBACK/RETURN
@Column(name = "change_quantity", nullable = false)
private Integer changeQuantity; // 变更数量(正=增加,负=减少)
@Column(name = "before_stock")
private Integer beforeStock; // 变更前可用库存
@Column(name = "after_stock")
private Integer afterStock; // 变更后可用库存
@Column(name = "before_frozen")
private Integer beforeFrozen; // 变更前冻结库存
@Column(name = "after_frozen")
private Integer afterFrozen; // 变更后冻结库存
@Column(name = "operator", length = 64)
private String operator; // 操作人(系统/用户ID)
@Column(name = "remark", length = 256)
private String remark; // 备注
@Column(name = "create_time")
private LocalDateTime createTime; // 创建时间
}sql
-- 库存流水表 DDL
CREATE TABLE `sku_stock` (
`sku_id` VARCHAR(32) NOT NULL COMMENT 'SKU ID',
`actual_stock` INT NOT NULL DEFAULT 0 COMMENT '实际库存',
`frozen_stock` INT NOT NULL DEFAULT 0 COMMENT '冻结库存',
`version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁',
`create_time` DATETIME NOT NULL,
`update_time` DATETIME NOT NULL,
PRIMARY KEY (`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SKU 库存表';
CREATE TABLE `stock_flow` (
`flow_id` VARCHAR(32) NOT NULL COMMENT '流水号',
`sku_id` VARCHAR(32) NOT NULL COMMENT 'SKU ID',
`order_id` VARCHAR(32) DEFAULT NULL COMMENT '订单号',
`change_type` VARCHAR(16) NOT NULL COMMENT '变更类型',
`change_quantity` INT NOT NULL COMMENT '变更数量',
`before_stock` INT DEFAULT NULL COMMENT '变更前可用库存',
`after_stock` INT DEFAULT NULL COMMENT '变更后可用库存',
`before_frozen` INT DEFAULT NULL COMMENT '变更前冻结库存',
`after_frozen` INT DEFAULT NULL COMMENT '变更后冻结库存',
`operator` VARCHAR(64) DEFAULT NULL COMMENT '操作人',
`remark` VARCHAR(256) DEFAULT NULL COMMENT '备注',
`create_time` DATETIME NOT NULL COMMENT '创建时间',
PRIMARY KEY (`flow_id`),
KEY `idx_sku_id` (`sku_id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水表';5. 售后系统
5.1 售后类型
java
public enum AfterSaleType {
REFUND_ONLY("仅退款"),
REFUND_WITH_RETURN("退货退款"),
EXCHANGE("换货");
private final String desc;
// constructor, getter ...
}| 售后类型 | 适用场景 | 是否需要退货 | 退款操作 |
|---|---|---|---|
| 仅退款 | 未发货取消、虚拟商品、缺货 | 否 | 直接退款 |
| 退货退款 | 已收货商品质量问题 | 是 | 退货入库后退款 |
| 换货 | 商品瑕疵、错发 | 是 | 不退款,换新发货 |
5.2 退款流程状态机
┌──────────┐
│ 待审核 │
└────┬─────┘
│
┌───────┼───────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────┐ ┌──────┐
│ 退款中 │ │已拒绝│ │已撤销 │
└───┬────┘ └────┘ └──────┘
│
▼
┌────────┐
│ 已完成 │
└────────┘java
@Configuration
@EnableStateMachine(name = "refundStateMachine")
public class RefundStateMachineConfig
extends StateMachineConfigurerAdapter<RefundStatus, RefundEvent> {
@Override
public void configure(StateMachineStateConfigurer<RefundStatus, RefundEvent> states)
throws Exception {
states
.withStates()
.initial(RefundStatus.PENDING_REVIEW) // 待审核
.state(RefundStatus.REFUNDING) // 退款中
.end(RefundStatus.COMPLETED) // 已完成
.end(RefundStatus.REJECTED) // 已拒绝
.end(RefundStatus.CANCELLED); // 已撤销
}
@Override
public void configure(StateMachineTransitionConfigurer<RefundStatus, RefundEvent> transitions)
throws Exception {
transitions
// 待审核 -> 退款中(审核通过)
.withExternal()
.source(RefundStatus.PENDING_REVIEW)
.target(RefundStatus.REFUNDING)
.event(RefundEvent.APPROVE)
.action(refundAction())
// 待审核 -> 已拒绝(审核不通过)
.and()
.withExternal()
.source(RefundStatus.PENDING_REVIEW)
.target(RefundStatus.REJECTED)
.event(RefundEvent.REJECT)
// 待审核 -> 已撤销(用户取消申请)
.and()
.withExternal()
.source(RefundStatus.PENDING_REVIEW)
.target(RefundStatus.CANCELLED)
.event(RefundEvent.CANCEL)
// 退款中 -> 已完成(退款成功)
.and()
.withExternal()
.source(RefundStatus.REFUNDING)
.target(RefundStatus.COMPLETED)
.event(RefundEvent.REFUND_SUCCESS)
.guard(refundSuccessGuard())
.action(refundSuccessAction());
}
}
public enum RefundStatus {
PENDING_REVIEW("待审核"),
REFUNDING("退款中"),
COMPLETED("已完成"),
REJECTED("已拒绝"),
CANCELLED("已撤销");
private final String desc;
// constructor, getter ...
}
public enum RefundEvent {
APPROVE, // 审核通过
REJECT, // 审核拒绝
CANCEL, // 用户撤销
REFUND_SUCCESS; // 退款成功
}5.3 售后申请与处理
java
@Service
public class AfterSaleService {
@Autowired
private RefundStateMachineService refundStateMachine;
@Autowired
private PaymentService paymentService;
@Autowired
private InventoryService inventoryService;
/**
* 提交售后申请
*/
public void applyAfterSale(AfterSaleApplyRequest request) {
// 1. 校验订单是否可售后
OrderMain order = orderRepository.findById(request.getOrderId()).orElse(null);
if (!canApplyAfterSale(order)) {
throw new BusinessException("该订单不可申请售后");
}
// 2. 创建售后申请记录
AfterSaleRecord record = new AfterSaleRecord();
record.setAfterSaleId(generateAfterSaleId());
record.setOrderId(request.getOrderId());
record.setUserId(request.getUserId());
record.setType(request.getType()); // 售后类型
record.setReason(request.getReason()); // 售后原因
record.setAmount(calcRefundAmount(order, request));
record.setStatus(AfterSaleStatus.PENDING_REVIEW);
record.setCreateTime(LocalDateTime.now());
afterSaleRepository.save(record);
// 3. 发送状态机事件
Map<String, Object> headers = new HashMap<>();
headers.put("afterSaleRecord", record);
refundStateMachine.sendEvent(
record.getAfterSaleId(), RefundEvent.APPLY, headers);
}
/**
* 审核通过
*/
@Transactional
public void approveAfterSale(String afterSaleId, String operator) {
AfterSaleRecord record = afterSaleRepository.findById(afterSaleId).orElse(null);
if (record == null) {
throw new BusinessException("售后记录不存在");
}
// 更新审核信息
record.setApprover(operator);
record.setApproveTime(LocalDateTime.now());
if (record.getType() == AfterSaleType.REFUND_ONLY) {
// 仅退款:直接进入退款中
Map<String, Object> headers = new HashMap<>();
headers.put("afterSaleRecord", record);
refundStateMachine.sendEvent(afterSaleId, RefundEvent.APPROVE, headers);
} else {
// 退货退款或换货:等待用户退货
record.setStatus(AfterSaleStatus.WAITING_RETURN);
afterSaleRepository.save(record);
}
}
/**
* 用户填写退货物流(退货退款 / 换货)
*/
public void submitReturnLogistics(ReturnLogisticsRequest request) {
AfterSaleRecord record = afterSaleRepository
.findById(request.getAfterSaleId()).orElse(null);
// 记录退货物流信息
ReturnLogistics logistics = new ReturnLogistics();
logistics.setAfterSaleId(request.getAfterSaleId());
logistics.setLogisticsCompany(request.getLogisticsCompany());
logistics.setLogisticsNo(request.getLogisticsNo());
logistics.setReturnTime(LocalDateTime.now());
returnLogisticsRepository.save(logistics);
// 更新售后状态为"待商家收货"
record.setStatus(AfterSaleStatus.WAITING_RECEIVE);
afterSaleRepository.save(record);
}
/**
* 商家确认收货(退货退款)
*/
@Transactional
public void confirmReturn(String afterSaleId, String operator) {
AfterSaleRecord record = afterSaleRepository.findById(afterSaleId).orElse(null);
// 逆向库存回库
inventoryService.returnStock(
record.getOrderId(), record.getSkuId(), record.getQuantity());
// 进入退款中
record.setStatus(AfterSaleStatus.REFUNDING);
afterSaleRepository.save(record);
Map<String, Object> headers = new HashMap<>();
headers.put("afterSaleRecord", record);
refundStateMachine.sendEvent(afterSaleId, RefundEvent.APPROVE, headers);
}
}5.4 退款执行
java
@Component
public class RefundAction implements Action<RefundStatus, RefundEvent> {
@Autowired
private PaymentService paymentService;
@Override
public void execute(StateContext<RefundStatus, RefundEvent> context) {
AfterSaleRecord record = (AfterSaleRecord) context.getMessageHeader("afterSaleRecord");
// 调用支付平台退款
RefundRequest refundRequest = new RefundRequest();
refundRequest.setOrderId(record.getOrderId());
refundRequest.setRefundAmount(record.getAmount());
refundRequest.setRefundNo(record.getAfterSaleId());
refundRequest.setRefundReason(record.getReason());
RefundResponse response = paymentService.refund(refundRequest);
if (response.isSuccess()) {
// 退款成功,自动触发 REFUND_SUCCESS 事件
} else {
// 退款失败,记录失败原因,后续人工处理
record.setFailReason(response.getFailReason());
afterSaleRepository.save(record);
throw new RuntimeException("退款失败: " + response.getFailReason());
}
}
}5.5 退货物流跟踪
java
@Entity
@Table(name = "return_logistics")
public class ReturnLogistics {
@Id
@Column(name = "id", length = 32)
private String id;
@Column(name = "after_sale_id", length = 32, nullable = false)
private String afterSaleId; // 售后单ID
@Column(name = "logistics_company", length = 32)
private String logisticsCompany; // 物流公司
@Column(name = "logistics_no", length = 64)
private String logisticsNo; // 物流单号
@Column(name = "return_time")
private LocalDateTime returnTime; // 退货时间
@Column(name = "receive_time")
private LocalDateTime receiveTime; // 收货时间
@Column(name = "status", length = 16)
private String status; // 物流状态
@Column(name = "remark", length = 256)
private String remark; // 备注
}5.6 逆向库存回库
java
@Component
public class InventoryService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 退货/换货入库,将库存回库
*/
public void returnStock(String orderId, String skuId, Integer quantity) {
// 1. 更新 Redis 库存
String stockKey = STOCK_KEY_PREFIX + skuId;
redisTemplate.opsForValue().increment(stockKey, quantity);
// 2. 记录库存流水
StockFlow flow = new StockFlow();
flow.setFlowId(generateFlowId());
flow.setSkuId(skuId);
flow.setOrderId(orderId);
flow.setChangeType("RETURN");
flow.setChangeQuantity(quantity); // 正数表示增加
flow.setOperator("SYSTEM_AFTER_SALE");
flow.setCreateTime(LocalDateTime.now());
stockFlowRepository.save(flow);
// 3. 异步同步数据库库存
sendStockChangeMessage(skuId, quantity, StockChangeType.RETURN);
}
}6. 订单幂等
6.1 幂等设计总则
| 操作 | 幂等键 | 实现方式 |
|---|---|---|
| 支付回调 | 支付平台交易号 + 商户订单号 | 唯一索引 + 状态机前置校验 |
| 取消订单 | 订单号 | 状态机状态校验 |
| 退款 | 退款流水号 | 唯一索引 + 状态机前置校验 |
6.2 支付回调幂等处理
支付回调可能因网络波动导致重复通知,必须做幂等处理。
java
@Service
public class PayCallbackService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PayCallbackRecordRepository callbackRecordRepository;
@Autowired
private OrderStateMachineService stateMachineService;
/**
* 处理支付回调(幂等)
*/
@Transactional
public PayCallbackResult handlePayCallback(PayCallbackRequest request) {
// Step 1: 使用支付平台的交易号 + 商户订单号作为幂等键
String idempotentKey = request.getTransactionId() + ":" + request.getOrderId();
// Step 2: 尝试插入幂等记录(数据库唯一索引保证)
try {
PayCallbackRecord record = new PayCallbackRecord();
record.setIdempotentKey(idempotentKey);
record.setOrderId(request.getOrderId());
record.setTransactionId(request.getTransactionId());
record.setCallbackData(JacksonUtils.toJson(request));
record.setStatus("PROCESSING");
record.setCreateTime(LocalDateTime.now());
callbackRecordRepository.insert(record);
} catch (DuplicateKeyException e) {
// 重复回调,查询处理结果
PayCallbackRecord existing = callbackRecordRepository
.findByIdempotentKey(idempotentKey);
return PayCallbackResult.alreadyProcessed(existing.getResult());
}
// Step 3: 查询订单
OrderMain order = orderRepository.findById(request.getOrderId()).orElse(null);
if (order == null) {
return PayCallbackResult.failed("订单不存在");
}
// Step 4: 状态机前置校验(只有待付款状态才能处理支付成功)
if (order.getOrderStatus() != OrderStatus.PENDING_PAYMENT) {
// 记录重复通知
callbackRecordRepository.updateStatus(idempotentKey, "DUPLICATE");
return PayCallbackResult.alreadyProcessed("订单已处理");
}
// Step 5: 发送支付成功事件
Map<String, Object> headers = new HashMap<>();
headers.put("order", order);
PayMessage payMessage = new PayMessage();
payMessage.setPayAmount(request.getPayAmount());
payMessage.setPayChannel(request.getPayChannel());
payMessage.setTransactionId(request.getTransactionId());
payMessage.setPaymentNo(request.getPaymentNo());
headers.put("payMessage", payMessage);
boolean accepted = stateMachineService.sendEvent(
request.getOrderId(), OrderEvent.PAY_SUCCESS, headers);
if (accepted) {
callbackRecordRepository.updateStatus(idempotentKey, "SUCCESS");
return PayCallbackResult.success();
} else {
callbackRecordRepository.updateStatus(idempotentKey, "FAILED");
return PayCallbackResult.failed("状态机拒绝");
}
}
}幂等记录表 DDL:
sql
CREATE TABLE `pay_callback_record` (
`id` BIGINT AUTO_INCREMENT COMMENT '自增ID',
`idempotent_key` VARCHAR(128) NOT NULL COMMENT '幂等键(交易号:订单号)',
`order_id` VARCHAR(32) NOT NULL COMMENT '订单号',
`transaction_id` VARCHAR(64) NOT NULL COMMENT '支付平台交易号',
`callback_data` TEXT COMMENT '回调原始数据',
`status` VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '处理状态',
`result` VARCHAR(256) DEFAULT NULL COMMENT '处理结果',
`create_time` DATETIME NOT NULL,
`update_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_idempotent_key` (`idempotent_key`),
KEY `idx_order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付回调幂等记录表';6.3 取消订单幂等
java
@Service
public class CancelOrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private OrderStateMachineService stateMachineService;
/**
* 取消订单(幂等)
*/
@Transactional
public CancelResult cancelOrder(String orderId, String userId, String reason) {
// Step 1: 查询订单
OrderMain order = orderRepository.findById(orderId).orElse(null);
if (order == null) {
return CancelResult.failed("订单不存在");
}
// Step 2: 状态前置校验
// 只有待付款状态的订单才可以取消(状态机的 Guard 也会校验,这里做第一层防御)
if (order.getOrderStatus() != OrderStatus.PENDING_PAYMENT) {
return CancelResult.alreadyCancelled(
"订单状态不允许取消: " + order.getOrderStatus().getDesc());
}
// Step 3: 发送取消事件(状态机内部保证幂等)
Map<String, Object> headers = new HashMap<>();
headers.put("order", order);
headers.put("cancelReason", reason);
headers.put("operator", userId);
boolean accepted = stateMachineService.sendEvent(
orderId, OrderEvent.CANCEL, headers);
if (accepted) {
return CancelResult.success();
} else {
// 状态机拒绝说明状态已变更,属于重复请求
return CancelResult.alreadyCancelled("订单已处理,无需重复取消");
}
}
}6.4 退款幂等
退款幂等通过唯一流水号 + 状态机前置条件双重保障。
java
@Service
public class RefundIdempotentService {
@Autowired
private RefundFlowRepository refundFlowRepository;
@Autowired
private RefundStateMachineService refundStateMachine;
/**
* 执行退款(幂等)
*/
@Transactional
public RefundResult executeRefund(RefundRequest request) {
String refundNo = request.getRefundNo();
// Step 1: 唯一流水号防重
RefundFlow existingFlow = refundFlowRepository.findByRefundNo(refundNo);
if (existingFlow != null) {
// 已存在的退款流水,直接返回已有结果
return RefundResult.alreadyProcessed(
existingFlow.getFlowId(), existingFlow.getStatus());
}
// Step 2: 创建退款流水记录
RefundFlow flow = new RefundFlow();
flow.setRefundNo(refundNo);
flow.setOrderId(request.getOrderId());
flow.setRefundAmount(request.getRefundAmount());
flow.setStatus("PROCESSING");
flow.setCreateTime(LocalDateTime.now());
refundFlowRepository.insert(flow);
// Step 3: 通过状态机执行退款
Map<String, Object> headers = new HashMap<>();
headers.put("refundFlow", flow);
headers.put("refundRequest", request);
boolean accepted = refundStateMachine.sendEvent(
refundNo, RefundEvent.APPROVE, headers);
if (accepted) {
flow.setStatus("SUCCESS");
refundFlowRepository.updateStatus(flow);
return RefundResult.success(flow.getFlowId());
} else {
flow.setStatus("REJECTED");
refundFlowRepository.updateStatus(flow);
return RefundResult.failed("状态机拒绝退款请求");
}
}
}退款流水表 DDL:
sql
CREATE TABLE `refund_flow` (
`refund_no` VARCHAR(64) NOT NULL COMMENT '退款流水号(唯一)',
`order_id` VARCHAR(32) NOT NULL COMMENT '订单号',
`after_sale_id` VARCHAR(32) DEFAULT NULL COMMENT '售后单号',
`refund_amount` BIGINT NOT NULL COMMENT '退款金额(分)',
`status` VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '状态',
`pay_refund_no` VARCHAR(64) DEFAULT NULL COMMENT '支付平台退款单号',
`fail_reason` VARCHAR(256) DEFAULT NULL COMMENT '失败原因',
`create_time` DATETIME NOT NULL,
`finish_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`refund_no`),
KEY `idx_order_id` (`order_id`),
KEY `idx_after_sale_id` (`after_sale_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='退款流水表';6.5 状态机前置校验在幂等中的作用
状态机天然具有幂等能力:当某个事件在某个状态下没有定义对应的转移时,状态机会直接拒绝该事件,从而防止重复处理。
text
示例:订单已从"待付款"转移到"已付款",支付回调重复到达
- 第一次:PENDING_PAYMENT --PAY_SUCCESS--> PAID (正常处理)
- 第二次:PAID --PAY_SUCCESS--> ? (状态机找不到对应转移,拒绝)
- 第三次:PAID --PAY_SUCCESS--> ? (同样拒绝,幂等)
示例:取消订单重复请求
- 第一次:PENDING_PAYMENT --CANCEL--> CANCELLED (正常处理)
- 第二次:CANCELLED --CANCEL--> ? (结束状态无对应转移,拒绝)7. 订单生命周期总流程
text
┌────────────────────────────────────────────────────────────────────┐
│ 订单完整生命周期 │
│ │
│ 下单 ──→ 预扣库存 ──→ 发送延迟消息 ──→ 等待支付 │
│ │ │
│ ┌─────────────────────────┤ │
│ │ │ │
│ ▼ ▼ │
│ 支付成功 30分钟未支付 │
│ │ │ │
│ ▼ ▼ │
│ 确认扣减库存 超时关闭订单 │
│ │ 回滚库存 │
│ │ │
│ ▼ │
│ 发货 ──→ 用户收货 ──→ 自动完成 │
│ │ │
│ ▼ │
│ 申请售后 │
│ │ │
│ ┌─────────┼─────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 仅退款 退货退款 换货 │
│ │ │
│ ┌─────────────┘ │
│ ▼ │
│ 逆向库存回库 │
│ │ │
│ ▼ │
│ 退款/换货完成 │
└────────────────────────────────────────────────────────────────────┘