很多开发者在设计初期的常见思路。将“取消”和“退款”分开,从功能上看似乎很清晰,但在实际的复杂业务场景中,这种设计可能会带来一些问题。
更主流和推荐的设计是提供一个统一的“申请取消订单”接口,由后端服务根据订单的当前状态,自动路由到不同的处理逻辑。
为什么统一接口是更好的选择?
- 前端逻辑简化: 对于用户而言,他的诉求只有一个:“我不想要这个订单了”。无论订单是否支付,他在前端点击的都是“取消订单”按钮。如果后端分为两个接口,前端就需要先查询订单状态,再根据状态决定调用哪个接口,这无疑增加了前端的复杂度和出错的可能性。
- 业务逻辑内聚: “取消订单”是一个完整的业务行为。这个行为的具体实现(是直接关单,还是触发退款)应该由订单系统自己来决定,而不是暴露给调用方。这符合面向对象设计中“封装变化”的原则。
- 避免状态不一致: 如果分为两个接口,可能会出现调用顺序错误的问题。例如,一个订单刚从未支付变为已支付,但前端缓存的状态还是未支付,此时调用了“取消未支付订单”的接口,就可能导致业务错误。统一接口可以确保在同一个事务中完成状态判断和后续操作,保证数据一致性。
💡 统一接口的设计思路
你可以设计一个名为 cancelOrder 的接口,接收 orderId 和 cancelReason 等参数。在后端的 Service 层,通过策略模式或简单的条件判断来实现不同的业务分支。
下面是一个简化的代码逻辑示例,展示了如何在一个接口中处理不同情况:
// Service层实现
public void cancelOrder(Long orderId, String cancelReason) {
// 1. 根据订单ID查询订单信息
Order order = orderMapper.selectById(orderId);
// 2. 校验订单是否可以取消(例如,已发货的订单可能不允许直接取消)
if (!order.canBeCancelled()) {
throw new BusinessException("当前订单状态不允许取消");
}
// 3. 根据订单的支付状态,执行不同的取消逻辑
if (order.getPayStatus() == OrderPayStatus.UNPAID) {
// --- 情况一:订单未支付 ---
// 1. 更新订单状态为“已取消”
order.setStatus(OrderStatus.CANCELLED);
order.setCancelReason(cancelReason);
order.setCancelTime(LocalDateTime.now());
orderMapper.update(order);
// 2. 可能还需要调用支付网关的“关单”接口,防止用户后续支付
} else if (order.getPayStatus() == OrderPayStatus.PAID) {
// --- 情况二:订单已支付 ---
// 1. 更新订单状态为“退款中”或“取消申请中”
order.setStatus(OrderStatus.REFUNDING);
order.setCancelReason(cancelReason);
orderMapper.update(order);
// 2. 创建一条退款记录,用于异步处理退款
RefundRecord refundRecord = new RefundRecord();
refundRecord.setOrderId(orderId);
refundRecord.setRefundAmount(order.getPayAmount());
// ...设置其他退款信息
refundRecordMapper.insert(refundRecord);
// 3. 触发异步退款流程(例如,发送消息到消息队列,或由定时任务处理)
refundService.asyncRefund(refundRecord.getId());
}
}
关键设计要点
- 统一入口:
POST /api/orders/{orderId}/cancel 这样的接口路径清晰地表达了“取消订单”这个意图。
- 状态驱动:后端服务是“聪明”的,它根据订单的
status 和 payStatus 等字段来决定下一步做什么。
- 异步退款:对于已支付的订单,调用第三方支付接口进行退款是一个耗时且可能失败的操作。不应在主事务中同步执行。正确的做法是:
- 在数据库事务中,将订单状态更新为“退款中”,并创建一条退款记录。
- 事务提交后,通过异步方式(如启动一个新线程、发送MQ消息)去调用支付网关的退款接口。
- 通过定时任务或支付网关的回调通知来查询退款结果,并更新订单和退款记录的状态。
POST /api/orders/{orderId}/cancel 这样的接口路径清晰地表达了“取消订单”这个意图。status 和 payStatus 等字段来决定下一步做什么。- 在数据库事务中,将订单状态更新为“退款中”,并创建一条退款记录。
- 事务提交后,通过异步方式(如启动一个新线程、发送MQ消息)去调用支付网关的退款接口。
- 通过定时任务或支付网关的回调通知来查询退款结果,并更新订单和退款记录的状态。
总结来说,采用统一的“取消订单”接口,将复杂性封装在后端,是更符合业务逻辑、更易于维护和扩展的设计方式。
文章版权声明
