feat(order): 添加退款单号幂等性支持并完善订单状态流转验证

- 在RefundRequest中新增refundNo字段作为退款单号幂等键
- 添加订单状态、支付状态、退款状态流转验证逻辑
- 完善updateRefundStatus方法支持退款单号和支付平台退款单号参数
- 优化createRefundRecord方法增加退款单号重复性检查和幂等处理
- 重构支付回调处理逻辑统一状态更新方式
- 增强订单状态流转的安全性和一致性校验
This commit is contained in:
2026-01-19 21:28:24 +08:00
parent 3084afc6a7
commit 679f2d3a79
3 changed files with 307 additions and 87 deletions

View File

@@ -10,6 +10,11 @@ import java.math.BigDecimal;
@Data
public class RefundRequest {
/**
* 退款单号(幂等键,必填)
*/
private String refundNo;
/**
* 订单ID
*/
@@ -49,4 +54,4 @@ public class RefundRequest {
* 退款渠道
*/
private String refundChannel = "ORIGINAL";
}
}

View File

@@ -102,9 +102,11 @@ public interface IOrderService {
*
* @param orderId 订单ID
* @param refundStatus 退款状态
* @param refundNo 退款单号(幂等键)
* @param paymentRefundId 支付平台退款单号
* @return 更新结果
*/
boolean updateRefundStatus(Long orderId, RefundStatus refundStatus);
boolean updateRefundStatus(Long orderId, RefundStatus refundStatus, String refundNo, String paymentRefundId);
/**
* 创建退款记录
@@ -142,4 +144,4 @@ public interface IOrderService {
* @return 回调处理结果
*/
PaymentCallbackResponse handlePaymentCallback(Long scenicId, HttpServletRequest request);
}
}

View File

@@ -47,6 +47,7 @@ import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.service.pc.ScenicService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -264,20 +265,31 @@ public class OrderServiceImpl implements IOrderService {
@Override
public boolean updateOrderStatus(Long orderId, String orderStatus, String paymentStatus) {
OrderV2 currentOrder = orderV2Mapper.selectById(orderId);
if (currentOrder == null) {
log.warn("订单不存在: orderId={}", orderId);
return false;
}
OrderStatus targetOrderStatus = OrderStatus.fromCode(orderStatus);
PaymentStatus targetPaymentStatus = PaymentStatus.fromCode(paymentStatus);
validateOrderStatusTransition(currentOrder, targetOrderStatus, targetPaymentStatus);
if (currentOrder.getOrderStatus() == targetOrderStatus
&& currentOrder.getPaymentStatus() == targetPaymentStatus) {
return true;
}
OrderV2 order = new OrderV2();
order.setId(orderId);
order.setOrderStatus(OrderStatus.fromCode(orderStatus));
order.setPaymentStatus(PaymentStatus.fromCode(paymentStatus));
order.setOrderStatus(targetOrderStatus);
order.setPaymentStatus(targetPaymentStatus);
order.setUpdateTime(new Date());
if ("PAID".equals(paymentStatus)) {
if (targetPaymentStatus == PaymentStatus.PAID && currentOrder.getPayTime() == null) {
order.setPayTime(new Date());
}
if ("COMPLETED".equals(orderStatus)) {
order.setCompleteTime(new Date());
}
return orderV2Mapper.updateById(order) > 0;
}
@@ -354,74 +366,101 @@ public class OrderServiceImpl implements IOrderService {
log.warn("订单不存在: orderId={}", orderId);
return false;
}
PaymentStatus targetPaymentStatus = PaymentStatus.fromCode(paymentStatus);
validatePaymentStatusTransition(currentOrder, targetPaymentStatus);
PaymentStatus oldPaymentStatus = currentOrder.getPaymentStatus();
PaymentStatus newPaymentStatus = PaymentStatus.fromCode(paymentStatus);
// 更新订单支付状态
if (oldPaymentStatus == targetPaymentStatus) {
return true;
}
OrderV2 order = new OrderV2();
order.setId(orderId);
order.setPaymentStatus(newPaymentStatus);
order.setPaymentStatus(targetPaymentStatus);
order.setOrderStatus(OrderStatus.PAID);
order.setUpdateTime(new Date());
if ("PAID".equals(paymentStatus)) {
if (currentOrder.getPayTime() == null) {
order.setPayTime(new Date());
// 支付完成后,订单状态也需要更新
order.setOrderStatus(OrderStatus.PAID);
}
boolean success = orderV2Mapper.updateById(order) > 0;
// 发布支付状态变更事件
if (success && !oldPaymentStatus.equals(newPaymentStatus)) {
if (success) {
PaymentStatusChangeEvent event = new PaymentStatusChangeEvent(
orderId, currentOrder.getOrderNo(),
oldPaymentStatus, newPaymentStatus,
oldPaymentStatus, targetPaymentStatus,
"支付状态更新", null
);
orderEventManager.publishPaymentStatusChangeEvent(event);
}
return success;
}
@Override
@Transactional
public boolean updateRefundStatus(Long orderId, RefundStatus refundStatus) {
// 先查询当前订单状态
public boolean updateRefundStatus(Long orderId, RefundStatus refundStatus, String refundNo, String paymentRefundId) {
OrderV2 currentOrder = orderV2Mapper.selectById(orderId);
if (currentOrder == null) {
log.warn("订单不存在: orderId={}", orderId);
return false;
}
RefundStatus oldRefundStatus = currentOrder.getRefundStatus();
// 更新订单退款状态
OrderV2 order = new OrderV2();
order.setId(orderId);
order.setRefundStatus(refundStatus);
order.setUpdateTime(new Date());
// 根据退款状态更新订单状态
if (RefundStatus.FULL_REFUND.equals(refundStatus)) {
order.setOrderStatus(OrderStatus.REFUNDED);
} else if (RefundStatus.REFUND_PROCESSING.equals(refundStatus)) {
order.setOrderStatus(OrderStatus.REFUNDING);
if (refundStatus == null) {
throw new BaseException("退款状态不能为空");
}
boolean success = orderV2Mapper.updateById(order) > 0;
// 发布退款状态变更事件
if (success && !oldRefundStatus.equals(refundStatus)) {
validateRefundStatusTransition(currentOrder, refundStatus);
OrderRefundV2 refundRecord = resolveRefundRecord(orderId, refundNo);
String resolvedRefundNo = refundRecord.getRefundNo();
if (StringUtils.isBlank(resolvedRefundNo)) {
throw new BaseException("退款单号不能为空");
}
RefundStatus oldRefundStatus = currentOrder.getRefundStatus();
boolean refundStatusChanged = oldRefundStatus != refundStatus;
boolean paymentRefundIdNeedUpdate = StringUtils.isNotBlank(paymentRefundId)
&& StringUtils.isBlank(refundRecord.getPaymentRefundId());
if (!refundStatusChanged && !paymentRefundIdNeedUpdate
&& isOrderRefundStateConsistent(currentOrder, refundStatus)) {
return true;
}
if (refundStatusChanged || paymentRefundIdNeedUpdate) {
OrderRefundV2 updateRefund = new OrderRefundV2();
updateRefund.setId(refundRecord.getId());
updateRefund.setRefundStatus(refundStatus);
if (paymentRefundIdNeedUpdate) {
updateRefund.setPaymentRefundId(paymentRefundId);
}
updateRefund.setUpdateTime(new Date());
orderRefundMapper.updateById(updateRefund);
}
boolean needUpdateOrder = refundStatusChanged || !isOrderRefundStateConsistent(currentOrder, refundStatus);
boolean success = true;
if (needUpdateOrder) {
OrderV2 order = new OrderV2();
order.setId(orderId);
order.setRefundStatus(refundStatus);
applyOrderRefundState(order, refundStatus);
order.setUpdateTime(new Date());
success = orderV2Mapper.updateById(order) > 0;
}
if (success && refundStatusChanged) {
RefundStatusChangeEvent event = new RefundStatusChangeEvent(
orderId, currentOrder.getOrderNo(), null, null,
oldRefundStatus, refundStatus, null,
orderId, currentOrder.getOrderNo(), refundRecord.getId(), resolvedRefundNo,
oldRefundStatus, refundStatus, refundRecord.getRefundAmount(),
"退款状态更新", null
);
orderEventManager.publishRefundStatusChangeEvent(event);
}
return success;
}
@@ -429,18 +468,51 @@ public class OrderServiceImpl implements IOrderService {
@Transactional
public Long createRefundRecord(RefundRequest request) {
Date now = new Date();
// 生成退款单号
String refundNo = generateRefundNo();
// 创建退款记录
if (request == null) {
throw new BaseException("退款请求不能为空");
}
if (request.getOrderId() == null) {
throw new BaseException("订单ID不能为空");
}
if (StringUtils.isBlank(request.getRefundNo())) {
throw new BaseException("退款单号不能为空");
}
if (StringUtils.isBlank(request.getRefundType())) {
throw new BaseException("退款类型不能为空");
}
OrderV2 order = orderV2Mapper.selectById(request.getOrderId());
if (order == null) {
throw new BaseException("订单不存在");
}
if (order.getOrderStatus() == OrderStatus.CANCELLED || order.getPaymentStatus() == PaymentStatus.UNPAID) {
throw new BaseException("未支付或已取消订单不允许退款");
}
if (order.getRefundStatus() == RefundStatus.FULL_REFUND) {
throw new BaseException("订单已完成退款");
}
String refundNo = request.getRefundNo();
OrderRefundV2 existingRefund = findRefundByNo(refundNo);
if (existingRefund != null) {
if (!request.getOrderId().equals(existingRefund.getOrderId())) {
throw new BaseException("退款单号已存在");
}
RefundStatus existingStatus = existingRefund.getRefundStatus() != null
? existingRefund.getRefundStatus()
: RefundStatus.REFUND_PROCESSING;
updateRefundStatus(request.getOrderId(), existingStatus, refundNo, existingRefund.getPaymentRefundId());
return existingRefund.getId();
}
OrderRefundV2 refund = new OrderRefundV2();
refund.setOrderId(request.getOrderId());
refund.setRefundNo(refundNo);
refund.setRefundType(RefundType.fromCode(request.getRefundType()));
refund.setRefundAmount(request.getRefundAmount());
refund.setRefundFee(request.getRefundFee());
refund.setRefundStatus(com.ycwl.basic.order.enums.RefundStatus.REFUND_PROCESSING);
refund.setRefundStatus(RefundStatus.REFUND_PROCESSING);
refund.setRefundReason(request.getRefundReason());
refund.setRefundDescription(request.getRefundDescription());
refund.setOperatorRemarks(request.getOperatorRemarks());
@@ -448,15 +520,14 @@ public class OrderServiceImpl implements IOrderService {
refund.setApplyTime(now);
refund.setCreateTime(now);
refund.setUpdateTime(now);
orderRefundMapper.insert(refund);
// 更新订单退款状态
updateRefundStatus(request.getOrderId(), com.ycwl.basic.order.enums.RefundStatus.REFUND_PROCESSING);
log.info("退款记录创建成功: refundId={}, refundNo={}, orderId={}, refundAmount={}",
updateRefundStatus(request.getOrderId(), RefundStatus.REFUND_PROCESSING, refundNo, null);
log.info("退款记录创建成功: refundId={}, refundNo={}, orderId={}, refundAmount={}",
refund.getId(), refundNo, request.getOrderId(), request.getRefundAmount());
return refund.getId();
}
@@ -467,6 +538,163 @@ public class OrderServiceImpl implements IOrderService {
}
// ====== 私有辅助方法 ======
private void validateOrderStatusTransition(OrderV2 currentOrder, OrderStatus targetOrderStatus,
PaymentStatus targetPaymentStatus) {
if (currentOrder == null || targetOrderStatus == null || targetPaymentStatus == null) {
throw new BaseException("订单状态参数不能为空");
}
OrderStatus currentOrderStatus = currentOrder.getOrderStatus();
PaymentStatus currentPaymentStatus = currentOrder.getPaymentStatus();
if (currentOrderStatus == null || currentPaymentStatus == null) {
throw new BaseException("订单状态异常");
}
if (currentOrderStatus == targetOrderStatus && currentPaymentStatus == targetPaymentStatus) {
return;
}
boolean allowed = false;
if (currentOrderStatus == OrderStatus.PENDING_PAYMENT) {
allowed = (targetOrderStatus == OrderStatus.PAID && targetPaymentStatus == PaymentStatus.PAID)
|| (targetOrderStatus == OrderStatus.CANCELLED && targetPaymentStatus == PaymentStatus.UNPAID);
} else if (currentOrderStatus == OrderStatus.PAID) {
allowed = (targetOrderStatus == OrderStatus.REFUNDING && targetPaymentStatus == PaymentStatus.PAID)
|| (targetOrderStatus == OrderStatus.REFUNDED && targetPaymentStatus == PaymentStatus.REFUNDED);
} else if (currentOrderStatus == OrderStatus.REFUNDING) {
allowed = targetOrderStatus == OrderStatus.REFUNDED && targetPaymentStatus == PaymentStatus.REFUNDED;
} else if (currentOrderStatus == OrderStatus.CANCELLED) {
allowed = targetOrderStatus == OrderStatus.CANCELLED && targetPaymentStatus == PaymentStatus.UNPAID;
} else if (currentOrderStatus == OrderStatus.REFUNDED) {
allowed = targetOrderStatus == OrderStatus.REFUNDED && targetPaymentStatus == PaymentStatus.REFUNDED;
}
if (!allowed) {
throw new BaseException("订单状态流转非法");
}
}
private void validatePaymentStatusTransition(OrderV2 currentOrder, PaymentStatus targetPaymentStatus) {
if (currentOrder == null || targetPaymentStatus == null) {
throw new BaseException("支付状态参数不能为空");
}
if (currentOrder.getOrderStatus() == null || currentOrder.getPaymentStatus() == null) {
throw new BaseException("订单状态异常");
}
if (targetPaymentStatus != PaymentStatus.PAID) {
throw new BaseException("支付状态仅允许更新为已支付");
}
if (currentOrder.getOrderStatus() == OrderStatus.CANCELLED) {
throw new BaseException("已取消订单不允许支付");
}
if (currentOrder.getPaymentStatus() == PaymentStatus.PAID) {
if (currentOrder.getOrderStatus() == OrderStatus.REFUNDED) {
throw new BaseException("订单状态不允许支付");
}
return;
}
if (currentOrder.getPaymentStatus() != PaymentStatus.UNPAID
|| currentOrder.getOrderStatus() != OrderStatus.PENDING_PAYMENT) {
throw new BaseException("订单状态不允许支付");
}
}
private void validateRefundStatusTransition(OrderV2 currentOrder, RefundStatus targetRefundStatus) {
if (currentOrder == null || targetRefundStatus == null) {
throw new BaseException("退款状态参数不能为空");
}
if (currentOrder.getOrderStatus() == null || currentOrder.getPaymentStatus() == null) {
throw new BaseException("订单状态异常");
}
if (currentOrder.getOrderStatus() == OrderStatus.CANCELLED) {
throw new BaseException("已取消订单不允许退款");
}
if (currentOrder.getPaymentStatus() == PaymentStatus.UNPAID) {
throw new BaseException("未支付订单不允许退款");
}
RefundStatus currentRefundStatus = currentOrder.getRefundStatus();
if (currentRefundStatus == null) {
throw new BaseException("订单退款状态异常");
}
if (currentRefundStatus == targetRefundStatus) {
return;
}
boolean allowed = false;
if (currentRefundStatus == RefundStatus.NO_REFUND) {
allowed = targetRefundStatus == RefundStatus.REFUND_PROCESSING
|| targetRefundStatus == RefundStatus.PARTIAL_REFUND
|| targetRefundStatus == RefundStatus.FULL_REFUND;
} else if (currentRefundStatus == RefundStatus.REFUND_PROCESSING) {
allowed = targetRefundStatus == RefundStatus.PARTIAL_REFUND
|| targetRefundStatus == RefundStatus.FULL_REFUND;
} else if (currentRefundStatus == RefundStatus.PARTIAL_REFUND) {
allowed = targetRefundStatus == RefundStatus.FULL_REFUND;
}
if (!allowed) {
throw new BaseException("退款状态流转非法");
}
}
private boolean isOrderRefundStateConsistent(OrderV2 order, RefundStatus refundStatus) {
if (order == null || refundStatus == null) {
return false;
}
if (refundStatus == RefundStatus.REFUND_PROCESSING || refundStatus == RefundStatus.PARTIAL_REFUND) {
return order.getOrderStatus() == OrderStatus.REFUNDING
&& order.getPaymentStatus() == PaymentStatus.PAID;
}
if (refundStatus == RefundStatus.FULL_REFUND) {
return order.getOrderStatus() == OrderStatus.REFUNDED
&& order.getPaymentStatus() == PaymentStatus.REFUNDED;
}
return order.getRefundStatus() == refundStatus;
}
private void applyOrderRefundState(OrderV2 order, RefundStatus refundStatus) {
if (order == null || refundStatus == null) {
return;
}
if (refundStatus == RefundStatus.REFUND_PROCESSING || refundStatus == RefundStatus.PARTIAL_REFUND) {
order.setOrderStatus(OrderStatus.REFUNDING);
order.setPaymentStatus(PaymentStatus.PAID);
} else if (refundStatus == RefundStatus.FULL_REFUND) {
order.setOrderStatus(OrderStatus.REFUNDED);
order.setPaymentStatus(PaymentStatus.REFUNDED);
}
}
private OrderRefundV2 resolveRefundRecord(Long orderId, String refundNo) {
if (StringUtils.isNotBlank(refundNo)) {
OrderRefundV2 refundRecord = findRefundByNo(refundNo);
if (refundRecord == null) {
throw new BaseException("退款单不存在");
}
if (!orderId.equals(refundRecord.getOrderId())) {
throw new BaseException("退款单不存在");
}
return refundRecord;
}
QueryWrapper<OrderRefundV2> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_id", orderId).orderByDesc("create_time");
List<OrderRefundV2> refunds = orderRefundMapper.selectList(queryWrapper);
if (refunds.isEmpty()) {
throw new BaseException("退款单不存在");
}
log.warn("退款单号缺失,使用最新退款记录: orderId={}", orderId);
return refunds.get(0);
}
private OrderRefundV2 findRefundByNo(String refundNo) {
if (StringUtils.isBlank(refundNo)) {
return null;
}
QueryWrapper<OrderRefundV2> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("refund_no", refundNo);
return orderRefundMapper.selectOne(queryWrapper);
}
/**
* 构建订单查询条件
@@ -624,8 +852,9 @@ public class OrderServiceImpl implements IOrderService {
}
// 3. 验证订单状态
if (order.getPaymentStatus() != PaymentStatus.UNPAID) {
throw new RuntimeException("订单状态不允许支付");
if (order.getOrderStatus() != OrderStatus.PENDING_PAYMENT
|| order.getPaymentStatus() != PaymentStatus.UNPAID) {
throw new BaseException("订单状态不允许支付");
}
// 4. 获取用户openId(从订单中获取)
@@ -713,31 +942,15 @@ public class OrderServiceImpl implements IOrderService {
if (callbackResponse.isPay()) {
// 支付成功
statusChangeType = "PAID";
updatePaymentStatus(order.getId(), PaymentStatus.PAID.name());
// 触发支付成功事件
orderEventManager.publishPaymentStatusChangeEvent(
new PaymentStatusChangeEvent(order.getId(), order.getOrderNo(),
PaymentStatus.UNPAID, PaymentStatus.PAID, "支付回调成功", null)
);
updatePaymentStatus(order.getId(), PaymentStatus.PAID.getCode());
} else if (callbackResponse.isCancel()) {
// 支付取消 - 这种情况下支付状态保持未支付,不需要特别处理
// 支付取消
statusChangeType = "CANCELLED";
log.info("支付被取消,支付状态保持未支付: orderId={}", order.getId());
updateOrderStatus(order.getId(), OrderStatus.CANCELLED.getCode(), PaymentStatus.UNPAID.getCode());
} else if (callbackResponse.isRefund()) {
// 退款
statusChangeType = "REFUNDED";
updatePaymentStatus(order.getId(), PaymentStatus.REFUNDED.name());
updateRefundStatus(order.getId(), RefundStatus.FULL_REFUND);
// 触发退款事件
orderEventManager.publishRefundStatusChangeEvent(
new RefundStatusChangeEvent(order.getId(), order.getOrderNo(), null, null,
RefundStatus.NO_REFUND, RefundStatus.FULL_REFUND,
order.getFinalAmount(), "支付回调退款", null)
);
updateRefundStatus(order.getId(), RefundStatus.FULL_REFUND, null, callbackResponse.getTransactionId());
}
log.info("支付回调处理成功: orderId={}, orderNo={}, statusChangeType={}",
@@ -958,4 +1171,4 @@ public class OrderServiceImpl implements IOrderService {
}
}
}
}
}