fix(order): 修复订单取消和退款流程中的并发安全问题

- 添加订单ID空值检查,防止空指针异常
- 使用条件更新替代直接更新,避免并发导致的状态污染
- 扩展退款状态检查范围,包含更多退款相关状态
- 添加未支付订单退款验证,防止脏状态产生
- 增加重复退款申请检查,避免状态冲突
- 区分用户取消和系统取消的原因标记
- 优化更新逻辑确保状态一致性
This commit is contained in:
2026-02-07 15:36:35 +08:00
parent 9b31b3ce57
commit d50e85fc52
2 changed files with 51 additions and 8 deletions

View File

@@ -279,7 +279,10 @@ public class ShopOrderController extends BaseController {
return fail("订单不存在"); return fail("订单不存在");
} }
// 退款相关操作单独走退款接口,便于做财务权限隔离 // 退款相关操作单独走退款接口,便于做财务权限隔离
if (Objects.equals(shopOrder.getOrderStatus(), 6)) { if (Objects.equals(shopOrder.getOrderStatus(), 4)
|| Objects.equals(shopOrder.getOrderStatus(), 5)
|| Objects.equals(shopOrder.getOrderStatus(), 6)
|| Objects.equals(shopOrder.getOrderStatus(), 7)) {
return fail("退款相关操作请使用退款接口: PUT /api/shop/shop-order/refund"); return fail("退款相关操作请使用退款接口: PUT /api/shop/shop-order/refund");
} }
ShopOrder shopOrderNow = shopOrderService.getById(shopOrder.getOrderId()); ShopOrder shopOrderNow = shopOrderService.getById(shopOrder.getOrderId());
@@ -394,6 +397,20 @@ public class ShopOrderController extends BaseController {
// 申请退款:只记录申请时间/原因/金额(如有) // 申请退款:只记录申请时间/原因/金额(如有)
if (Objects.equals(req.getOrderStatus(), 4)) { if (Objects.equals(req.getOrderStatus(), 4)) {
// 未支付订单不允许进入退款流程(否则会出现“取消订单/超时取消后变成退款申请中”的严重脏状态)
if (!Boolean.TRUE.equals(current.getPayStatus())) {
return fail("订单未支付,无法申请退款");
}
if (Objects.equals(current.getOrderStatus(), 2)) {
return fail("订单已取消,无法申请退款");
}
if (Objects.equals(current.getOrderStatus(), 4)
|| Objects.equals(current.getOrderStatus(), 5)
|| Objects.equals(current.getOrderStatus(), 6)
|| Objects.equals(current.getOrderStatus(), 7)) {
return fail("订单已在退款流程中,请勿重复申请");
}
ShopOrder patch = new ShopOrder(); ShopOrder patch = new ShopOrder();
patch.setOrderId(req.getOrderId()); patch.setOrderId(req.getOrderId());
patch.setOrderStatus(4); patch.setOrderStatus(4);
@@ -506,7 +523,10 @@ public class ShopOrderController extends BaseController {
if (batchParam != null && batchParam.getData() != null) { if (batchParam != null && batchParam.getData() != null) {
Integer status = batchParam.getData().getOrderStatus(); Integer status = batchParam.getData().getOrderStatus();
// 退款相关操作单独走退款接口,避免绕过财务权限 // 退款相关操作单独走退款接口,避免绕过财务权限
if (Objects.equals(status, 4) || Objects.equals(status, 6)) { if (Objects.equals(status, 4)
|| Objects.equals(status, 5)
|| Objects.equals(status, 6)
|| Objects.equals(status, 7)) {
return fail("退款相关操作请使用退款接口: PUT /api/shop/shop-order/refund"); return fail("退款相关操作请使用退款接口: PUT /api/shop/shop-order/refund");
} }
} }
@@ -571,6 +591,8 @@ public class ShopOrderController extends BaseController {
return fail("订单状态不允许取消"); return fail("订单状态不允许取消");
} }
// 标记为用户主动取消(定时任务/系统取消会走默认文案)
order.setCancelReason("用户取消");
boolean success = orderCancelService.cancelOrder(order); boolean success = orderCancelService.cancelOrder(order);
if (success) { if (success) {
return success("订单取消成功"); return success("订单取消成功");

View File

@@ -1,6 +1,8 @@
package com.gxwebsoft.shop.service.impl; package com.gxwebsoft.shop.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.gxwebsoft.common.core.annotation.IgnoreTenant; import com.gxwebsoft.common.core.annotation.IgnoreTenant;
import com.gxwebsoft.shop.entity.*; import com.gxwebsoft.shop.entity.*;
import com.gxwebsoft.shop.service.*; import com.gxwebsoft.shop.service.*;
@@ -44,6 +46,11 @@ public class OrderCancelServiceImpl implements OrderCancelService {
try { try {
log.info("开始取消订单,订单号:{}订单ID{}", order.getOrderNo(), order.getOrderId()); log.info("开始取消订单,订单号:{}订单ID{}", order.getOrderNo(), order.getOrderId());
if (order.getOrderId() == null) {
log.warn("订单ID为空无法取消");
return false;
}
// 1. 检查订单状态 // 1. 检查订单状态
if (order.getPayStatus() != null && order.getPayStatus()) { if (order.getPayStatus() != null && order.getPayStatus()) {
log.warn("订单已支付,无法取消,订单号:{}", order.getOrderNo()); log.warn("订单已支付,无法取消,订单号:{}", order.getOrderNo());
@@ -55,17 +62,31 @@ public class OrderCancelServiceImpl implements OrderCancelService {
return false; return false;
} }
// 2. 更新订单状态为已取消 // 2. 更新订单状态为已取消(只更新必要字段 + 加条件,避免并发/越权导致“取消后又被改成退款中”等脏状态)
order.setOrderStatus(2); // 2表示已取消 LocalDateTime now = LocalDateTime.now();
order.setCancelTime(LocalDateTime.now()); String reason = StrUtil.isNotBlank(order.getCancelReason())
order.setCancelReason("系统自动取消(超时未支付)"); ? order.getCancelReason()
: "系统自动取消(超时未支付)";
boolean updateSuccess = shopOrderService.updateById(order); boolean updateSuccess = shopOrderService.update(
new LambdaUpdateWrapper<ShopOrder>()
.eq(ShopOrder::getOrderId, order.getOrderId())
.eq(ShopOrder::getPayStatus, false)
.eq(ShopOrder::getOrderStatus, 0)
.set(ShopOrder::getOrderStatus, 2) // 2表示已取消
.set(ShopOrder::getCancelTime, now)
.set(ShopOrder::getCancelReason, reason)
);
if (!updateSuccess) { if (!updateSuccess) {
log.error("更新订单状态失败,订单号{}", order.getOrderNo()); log.error("更新订单状态失败(可能已被支付/退款/取消),订单号:{}订单ID{}", order.getOrderNo(), order.getOrderId());
return false; return false;
} }
// 让后续库存/优惠券逻辑使用最新状态(不再依赖 updateById 回写)
order.setOrderStatus(2);
order.setCancelTime(now);
order.setCancelReason(reason);
// 3. 回退库存 // 3. 回退库存
boolean stockRestored = restoreOrderStock(order); boolean stockRestored = restoreOrderStock(order);
if (!stockRestored) { if (!stockRestored) {