refactor(refund): 退款快照重命名,澄清非二次路由语义

将 RefundRouteContext 重命名为 RefundOrderSnapshot,resolveRoute 改为 loadContainerSnapshot,强调退款继承原支付容器快照、不调用 PayRouteService 选路。
This commit is contained in:
DaxPay Dev
2026-07-15 12:11:46 +08:00
parent 6551777535
commit 4b1ddfa950
3 changed files with 73 additions and 68 deletions

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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;
}