From 4b1ddfa950d23ecb4a606f33390ef30140273f81 Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Wed, 15 Jul 2026 12:11:46 +0800 Subject: [PATCH] =?UTF-8?q?refactor(refund):=20=E9=80=80=E6=AC=BE=E5=BF=AB?= =?UTF-8?q?=E7=85=A7=E9=87=8D=E5=91=BD=E5=90=8D=EF=BC=8C=E6=BE=84=E6=B8=85?= =?UTF-8?q?=E9=9D=9E=E4=BA=8C=E6=AC=A1=E8=B7=AF=E7=94=B1=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 RefundRouteContext 重命名为 RefundOrderSnapshot,resolveRoute 改为 loadContainerSnapshot,强调退款继承原支付容器快照、不调用 PayRouteService 选路。 --- .../service/refund/PayRefundService.java | 47 +++++++++--------- .../service/refund/RefundOrderSnapshot.java | 49 +++++++++++++++++++ .../service/refund/RefundRouteContext.java | 45 ----------------- 3 files changed, 73 insertions(+), 68 deletions(-) create mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/RefundOrderSnapshot.java delete mode 100644 daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/RefundRouteContext.java diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/PayRefundService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/PayRefundService.java index 69d96df33..e2e4c8877 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/PayRefundService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/PayRefundService.java @@ -32,9 +32,10 @@ import java.util.Objects; /// # 退款服务 /// -/// 退款编排: 查找原支付交易 → 解析容器路由 → 预占可退余额 → 创建退款单 → 调通道 → 终态结算。 +/// 退款编排: 查找原支付交易 → 加载原支付容器快照 → 预占可退余额 → 创建退款单 → 调通道 → 终态结算。 /// 资金预占/成功/失败回滚委托 [PayRefundSettleService], 与同步/回调共用 trade 级锁。 /// 支持 [PayTradeTypeEnum#NORMAL] 与 [PayTradeTypeEnum#GATEWAY] 两种容器。 +/// 通道/产品继承自原支付容器快照,不调用 [PayRouteService] 二次选路。 @Slf4j @Service @RequiredArgsConstructor @@ -70,18 +71,18 @@ public class PayRefundService { PayTrade lockedTrade = payTradeManager.findById(trade.getId()).orElseThrow(); validateRefundable(lockedTrade, param.getAmount()); - // 按 tradeType 解析容器路由字段 - RefundRouteContext route = resolveRoute(lockedTrade); + // 按 tradeType 加载原支付容器快照(非二次路由) + RefundOrderSnapshot snapshot = loadContainerSnapshot(lockedTrade); // 预占可退余额 + 创建退款单(progress) payRefundSettleService.reserveBalanceUnderLock(lockedTrade, param.getAmount()); - PayRefundOrder refundOrder = buildRefundOrder(lockedTrade, route, param); + PayRefundOrder refundOrder = buildRefundOrder(lockedTrade, snapshot, param); refundOrder.setStatus(RefundOrderStatusEnum.PROGRESS.getCode()); payRefundOrderManager.save(refundOrder); - // 调用通道退款策略 + // 调用通道退款策略(product 继承自原支付单) AbsRefundStrategy strategy = PaymentStrategyFactory.createByProduct( - route.getProduct(), AbsRefundStrategy.class); + snapshot.getProduct(), AbsRefundStrategy.class); RefundResultBo result; try { strategy.doBeforeRefund(refundOrder); @@ -141,15 +142,15 @@ public class PayRefundService { throw new BizInfoException(DaxPayErrorCode.TRADE_STATUS_ERROR, "pay.error.notExists"); } - /// 按 tradeType 从容器读取退款路由字段 - private RefundRouteContext resolveRoute(PayTrade trade) { + /// 按 tradeType 从原支付容器加载退款建单快照(继承通道/产品, 不二次路由) + private RefundOrderSnapshot loadContainerSnapshot(PayTrade trade) { if (Objects.equals(trade.getTradeType(), PayTradeTypeEnum.GATEWAY.getCode())) { GatewayPayOrder order = gatewayPayOrderManager.findById(trade.getContainerId()) .orElseThrow(() -> new BizInfoException(DaxPayErrorCode.TRADE_STATUS_ERROR, "pay.error.notExists")); if (StrUtil.isBlank(order.getProduct())) { throw new BizInfoException(DaxPayErrorCode.TRADE_STATUS_ERROR, "pay.error.refund.statusNotAllow", trade.getStatus()); } - return new RefundRouteContext() + return new RefundOrderSnapshot() .setProduct(order.getProduct()) .setChannel(order.getChannel()) .setMethod(order.getMethod()) @@ -168,7 +169,7 @@ public class PayRefundService { if (StrUtil.isBlank(order.getProduct())) { throw new BizInfoException(DaxPayErrorCode.TRADE_STATUS_ERROR, "pay.error.refund.statusNotAllow", trade.getStatus()); } - return new RefundRouteContext() + return new RefundOrderSnapshot() .setProduct(order.getProduct()) .setChannel(order.getChannel()) .setMethod(order.getMethod()) @@ -193,8 +194,8 @@ public class PayRefundService { } } - /// 构建退款订单 - private PayRefundOrder buildRefundOrder(PayTrade trade, RefundRouteContext route, PayRefundParam param) { + /// 构建退款订单(通道/产品/通知字段来自原支付容器快照) + private PayRefundOrder buildRefundOrder(PayTrade trade, RefundOrderSnapshot snapshot, PayRefundParam param) { PayRefundOrder refundOrder = new PayRefundOrder(); // setMchNo 继承自父类, 单独调用避免链式返回父类型 refundOrder.setMchNo(trade.getMchNo()); @@ -207,19 +208,19 @@ public class PayRefundService { .setOrderAmount(trade.getAmount()) .setCurrency(trade.getCurrency()) .setReason(param.getReason()); - refundOrder.setChannel(route.getChannel()) - .setProduct(route.getProduct()) - .setMethod(route.getMethod()) - .setTitle(route.getTitle()) - .setBizOrderNo(route.getBizOrderNo()) - .setChannelMchNo(route.getChannelMchNo()) - .setCapability(route.getCapability()) - .setChannelAppId(route.getChannelAppId()) + refundOrder.setChannel(snapshot.getChannel()) + .setProduct(snapshot.getProduct()) + .setMethod(snapshot.getMethod()) + .setTitle(snapshot.getTitle()) + .setBizOrderNo(snapshot.getBizOrderNo()) + .setChannelMchNo(snapshot.getChannelMchNo()) + .setCapability(snapshot.getCapability()) + .setChannelAppId(snapshot.getChannelAppId()) // notifyUrl 语义: 商户出站通知地址, 通道回调 URL 由各通道 buildRefundNotifyUrl 生成 - .setNotifyUrl(route.getNotifyUrl()) - .setAttach(route.getAttach()); + .setNotifyUrl(snapshot.getNotifyUrl()) + .setAttach(snapshot.getAttach()); // 客户端IP: 优先取下单时留存的原订单IP, 为空则从当前HTTP请求兜底 - String refundClientIp = route.getClientIp(); + String refundClientIp = snapshot.getClientIp(); if (StrUtil.isBlank(refundClientIp)) { refundClientIp = WebServletUtil.getClientIp(); } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/RefundOrderSnapshot.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/RefundOrderSnapshot.java new file mode 100644 index 000000000..67541189d --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/RefundOrderSnapshot.java @@ -0,0 +1,49 @@ +package cn.daxpay.open.payment.trade.runtime.service.refund; + +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 退款建单快照 +/// +/// 从原支付业务容器([NormalPayOrder] / [GatewayPayOrder])拷贝的字段投影,用于: +/// 1. 填充 [PayRefundOrder](通道凭证与业务展示/通知字段) +/// 2. 按 [#product] 选择 [AbsRefundStrategy] +/// +/// **不是** [PayRouteService] 的选路结果;退款锚定原支付,禁止再走应用路由。 +@Data +@Accessors(chain = true) +public class RefundOrderSnapshot { + + /// 支付产品编码(策略选择,继承自原支付单) + private String product; + + /// 支付通道(继承自原支付单) + private String channel; + + /// 支付方式(继承自原支付单) + private String method; + + /// 通道商户号(继承自原支付单) + private String channelMchNo; + + /// 支付能力(继承自原支付单) + private String capability; + + /// 通道应用 AppId(原支付单快照) + private String channelAppId; + + /// 标题(继承自原支付单) + private String title; + + /// 商户业务单号(继承自原支付单) + private String bizOrderNo; + + /// 商户异步通知地址(出站, 非通道回调;继承自原支付单) + private String notifyUrl; + + /// 客户端 IP(继承自原支付单,可在建单时用当前请求兜底) + private String clientIp; + + /// 商户附加参数(继承自原支付单) + private String attach; +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/RefundRouteContext.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/RefundRouteContext.java deleted file mode 100644 index 839b945f5..000000000 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/refund/RefundRouteContext.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.daxpay.open.payment.trade.runtime.service.refund; - -import lombok.Data; -import lombok.experimental.Accessors; - -/// # 退款路由上下文 -/// -/// 从业务容器([NormalPayOrder] / [GatewayPayOrder])解析出的退款所需路由与业务字段。 -@Data -@Accessors(chain = true) -public class RefundRouteContext { - - /// 支付产品编码(策略选择) - private String product; - - /// 支付通道 - private String channel; - - /// 支付方式 - private String method; - - /// 通道商户号 - private String channelMchNo; - - /// 支付能力 - private String capability; - - /// 通道应用 AppId(原支付单快照) - private String channelAppId; - - /// 标题 - private String title; - - /// 商户业务单号 - private String bizOrderNo; - - /// 商户异步通知地址(出站, 非通道回调) - private String notifyUrl; - - /// 客户端 IP - private String clientIp; - - /// 商户附加参数 - private String attach; -}