mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-13 07:45:41 +08:00
fix(callback): 支付宝/富友回调按 tradeType 分发容器, ISV 沙箱改读通用主表
- AlipayCallbackService.resolveCredentialByTradeNo 原硬编码查 NormalPayOrder, 导致 tradeType=gateway 时跨表查不到容器(报"无法解析通道凭证"); 改走 PayTradeContainerFields 统一按 tradeType 分发到 normal/gateway 容器 - PayTradeContainerFields 新增 CredentialFields(product/channelMchNo/capability) 与 resolveCredentialFields(),复用现有 isGateway 分发范式 - FuyouPayCallbackService 同源 bug:硬编码查 NormalPayOrderManager 反查, tradeType=gateway 时同样失败;简化为直接 findByRelationOrderNo, PayTrade.relationOrderNo 跨容器统一 - AlipayIsvConfigAssembler / DougongIsvConfigAssembler 沙箱标识原实时读 payProductConfig.activeEnv,与 AGENTS.md 约定及 PayRouteService 兜底校验 (读 channelMerchant.isSandbox 固化快照)不一致;改注入 ChannelMerchantManager 按 channelMchNo 读通用主表 mch_channel_merchant.sandbox,单一事实源, 与 Direct 通道(Yeepay/Ums/Adapay)完全对齐
This commit is contained in:
@@ -8,12 +8,11 @@ import cn.daxpay.open.channel.alipay.service.direct.AlipayDirectConfigAssembler;
|
||||
import cn.daxpay.open.channel.alipay.service.isv.AlipayIsvConfigAssembler;
|
||||
import cn.daxpay.open.payment.trade.runtime.bo.CallbackData;
|
||||
import cn.daxpay.open.payment.trade.runtime.bo.RefundCallbackData;
|
||||
import cn.daxpay.open.payment.trade.order.dao.NormalPayOrderManager;
|
||||
import cn.daxpay.open.payment.trade.order.dao.RefundOrderManager;
|
||||
import cn.daxpay.open.payment.trade.order.dao.PayTradeManager;
|
||||
import cn.daxpay.open.payment.trade.order.entity.NormalPayOrder;
|
||||
import cn.daxpay.open.payment.trade.order.entity.RefundOrder;
|
||||
import cn.daxpay.open.payment.trade.order.entity.PayTrade;
|
||||
import cn.daxpay.open.payment.trade.runtime.service.PayTradeContainerFields;
|
||||
import cn.daxpay.open.payment.trade.runtime.service.callback.PayCallbackService;
|
||||
import cn.daxpay.open.payment.trade.runtime.service.callback.RefundCallbackService;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
@@ -50,7 +49,7 @@ public class AlipayCallbackService {
|
||||
private final AlipayDirectConfigAssembler alipayDirectConfigAssembler;
|
||||
private final AlipayIsvConfigAssembler alipayIsvConfigAssembler;
|
||||
private final PayTradeManager payTradeManager;
|
||||
private final NormalPayOrderManager normalPayOrderManager;
|
||||
private final PayTradeContainerFields payTradeContainerFields;
|
||||
private final RefundOrderManager payRefundOrderManager;
|
||||
private final PayCallbackService payCallbackService;
|
||||
private final RefundCallbackService refundCallbackService;
|
||||
@@ -166,18 +165,23 @@ public class AlipayCallbackService {
|
||||
if (trade == null) {
|
||||
return null;
|
||||
}
|
||||
// 从容器获取 product/channelMchNo/capability
|
||||
NormalPayOrder order = normalPayOrderManager.findById(trade.getContainerId()).orElse(null);
|
||||
if (order == null) {
|
||||
// 按 tradeType 分发到对应容器(normal/gateway)读取通道路由字段
|
||||
// 与 PayCallbackService.resolveProduct / PayTradeContainerFields.resolve 同范式,
|
||||
// 避免硬编码 normalPayOrder 导致 tradeType=gateway 时跨表查不到容器
|
||||
PayTradeContainerFields.CredentialFields fields = payTradeContainerFields.resolveCredentialFields(trade);
|
||||
if (fields == null) {
|
||||
// 容器记录不存在(含跨容器误查), 附 tradeType 便于排查
|
||||
log.warn("支付宝回调: 容器记录不存在, tradeNo={}, tradeType={}, containerId={}",
|
||||
tradeNo, trade.getTradeType(), trade.getContainerId());
|
||||
return null;
|
||||
}
|
||||
// 按支付产品分发: 服务商 / 直连
|
||||
if (ProductEnum.ALIPAY_ISV.getCode().equals(order.getProduct())) {
|
||||
if (ProductEnum.ALIPAY_ISV.getCode().equals(fields.product())) {
|
||||
return alipayIsvConfigAssembler.buildConfig(trade.getMchNo());
|
||||
}
|
||||
String channelMchNo = StrUtil.blankToDefault(order.getChannelMchNo(), pathChannelMchNo);
|
||||
String channelMchNo = StrUtil.blankToDefault(fields.channelMchNo(), pathChannelMchNo);
|
||||
return alipayDirectConfigAssembler.buildConfig(
|
||||
trade.getMchNo(), channelMchNo, order.getCapability());
|
||||
trade.getMchNo(), channelMchNo, fields.capability());
|
||||
}
|
||||
|
||||
/// 提取 request 全部表单参数为 Map<String,String>(多值取首项)
|
||||
|
||||
@@ -6,9 +6,8 @@ import cn.daxpay.open.channel.alipay.dao.isv.AlipayIsvChannelMerchantManager;
|
||||
import cn.daxpay.open.channel.alipay.entity.isv.AlipayIsvApp;
|
||||
import cn.daxpay.open.channel.alipay.entity.isv.AlipayIsvAppKeyConfig;
|
||||
import cn.daxpay.open.channel.alipay.entity.isv.AlipayIsvChannelMerchant;
|
||||
import cn.daxpay.open.payment.masterdata.dao.product.PayProductConfigManager;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.config.PayEnvEnum;
|
||||
import cn.daxpay.open.payment.merchant.dao.channel.ChannelMerchantManager;
|
||||
import cn.daxpay.open.payment.merchant.entity.channel.ChannelMerchant;
|
||||
import cn.daxpay.open.platform.core.exception.DataNotExistException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -22,6 +21,10 @@ import org.springframework.stereotype.Service;
|
||||
/// 服务商代调用模式: appId / 私钥 / 证书取自服务商应用, 应用授权令牌(appAuthToken)取自子商户授权绑定,
|
||||
/// 子应用据此以服务商身份代子商户发起支付宝请求。
|
||||
///
|
||||
/// 沙箱标识读通用通道商户主表 [ChannelMerchant#isSandbox] (创建时按当时产品 activeEnv 固化, 不随产品切换改变),
|
||||
/// 据此选择对应环境的网关地址, 与 Direct 通道(Yeepay/Ums/Adapay)及
|
||||
/// [cn.daxpay.open.payment.route.service.runtime.PayRouteService#validateChannelMchEnvMatch] 单一事实源对齐。
|
||||
///
|
||||
/// 供服务商支付/同步/关闭策略组装通道调用凭证。
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -31,7 +34,7 @@ public class AlipayIsvConfigAssembler {
|
||||
private final AlipayIsvChannelMerchantManager alipayIsvChannelMerchantManager;
|
||||
private final AlipayIsvAppManager alipayIsvAppManager;
|
||||
private final AlipayIsvAppKeyConfigService alipayIsvAppKeyConfigService;
|
||||
private final PayProductConfigManager payProductConfigManager;
|
||||
private final ChannelMerchantManager channelMerchantManager;
|
||||
|
||||
/// 组装服务商模式的通道调用凭证(下发给子应用)
|
||||
///
|
||||
@@ -57,9 +60,10 @@ public class AlipayIsvConfigAssembler {
|
||||
credential.setAlipayRootCert(keyConfig.getAlipayRootCert());
|
||||
// 应用授权令牌(服务商代子商户调用接口的凭据)
|
||||
credential.setAppAuthToken(isvMerchant.getAppAuthToken());
|
||||
// 读取服务商支付产品当前生效环境, 判断是否沙箱
|
||||
boolean sandbox = payProductConfigManager.findByProduct(ProductEnum.ALIPAY_ISV.getCode())
|
||||
.map(c -> PayEnvEnum.SANDBOX.getCode().equals(c.getActiveEnv()))
|
||||
// 沙箱标识读通用通道商户主表的固化快照(创建时按当时产品 activeEnv 写入, 不随产品切换改变)
|
||||
// 单一事实源, 与 Direct 通道 + PayRouteService.validateChannelMchEnvMatch 完全一致
|
||||
boolean sandbox = channelMerchantManager.findByChannelMchNo(isvMerchant.getChannelMchNo())
|
||||
.map(ChannelMerchant::isSandbox)
|
||||
.orElse(false);
|
||||
credential.setSandbox(sandbox);
|
||||
return credential;
|
||||
|
||||
@@ -4,11 +4,10 @@ import cn.daxpay.open.channel.dougong.client.credential.DougongSdkCredential;
|
||||
import cn.daxpay.open.channel.dougong.dao.isv.DougongIsvChannelMerchantManager;
|
||||
import cn.daxpay.open.channel.dougong.entity.isv.DougongIsvChannelMerchant;
|
||||
import cn.daxpay.open.channel.dougong.entity.isv.DougongIsvKeyConfig;
|
||||
import cn.daxpay.open.payment.masterdata.dao.product.PayProductConfigManager;
|
||||
import cn.daxpay.open.payment.masterdata.entity.product.PayProductConfig;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.config.PayEnvEnum;
|
||||
import cn.daxpay.open.platform.core.exception.DataNotExistException;
|
||||
import cn.daxpay.open.payment.merchant.dao.channel.ChannelMerchantManager;
|
||||
import cn.daxpay.open.payment.merchant.entity.channel.ChannelMerchant;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -21,6 +20,10 @@ import org.springframework.stereotype.Service;
|
||||
/// 字段映射:
|
||||
/// - sysId/productId/privateKey/dgPublicKey ← [DougongIsvKeyConfig](服务商级, 全局唯一)
|
||||
/// - merchantNo/appId ← [DougongIsvChannelMerchant](商户级)
|
||||
/// - sandbox ← [ChannelMerchant#isSandbox] (通用通道商户主表, 创建时按当时产品 activeEnv 固化, 不随产品切换改变)
|
||||
///
|
||||
/// 沙箱来源与 Direct 通道(Yeepay/Ums/Adapay)及
|
||||
/// [cn.daxpay.open.payment.route.service.runtime.PayRouteService#validateChannelMchEnvMatch] 单一事实源对齐。
|
||||
///
|
||||
/// 供支付策略([cn.daxpay.open.channel.dougong.strategy.*])组装通道调用凭证。
|
||||
@Slf4j
|
||||
@@ -30,7 +33,7 @@ public class DougongIsvConfigAssembler {
|
||||
|
||||
private final DougongIsvChannelMerchantManager dougongIsvChannelMerchantManager;
|
||||
private final DougongIsvKeyConfigService dougongIsvKeyConfigService;
|
||||
private final PayProductConfigManager payProductConfigManager;
|
||||
private final ChannelMerchantManager channelMerchantManager;
|
||||
|
||||
/// 组装斗拱通道调用凭证(下发给子应用)
|
||||
///
|
||||
@@ -52,10 +55,10 @@ public class DougongIsvConfigAssembler {
|
||||
credential.setProductId(keyConfig.getProductId());
|
||||
credential.setPrivateKey(keyConfig.getPrivateKey());
|
||||
credential.setDgPublicKey(keyConfig.getDgPublicKey());
|
||||
// 沙箱状态读取支付产品配置的生效环境
|
||||
boolean sandbox = payProductConfigManager.findByProduct(ProductEnum.DOUGONG_PAY.getCode())
|
||||
.map(PayProductConfig::getActiveEnv)
|
||||
.map(PayEnvEnum.SANDBOX.getCode()::equals)
|
||||
// 沙箱标识读通用通道商户主表的固化快照(创建时按当时产品 activeEnv 写入, 不随产品切换改变)
|
||||
// 单一事实源, 与 Direct 通道 + PayRouteService.validateChannelMchEnvMatch 完全一致
|
||||
boolean sandbox = channelMerchantManager.findByChannelMchNo(channelMchNo)
|
||||
.map(ChannelMerchant::isSandbox)
|
||||
.orElse(false);
|
||||
credential.setSandbox(sandbox);
|
||||
// 子商户身份
|
||||
|
||||
@@ -7,9 +7,7 @@ import cn.daxpay.open.channel.fuyou.client.resp.FuyouCallbackParseResp;
|
||||
import cn.daxpay.open.channel.fuyou.dao.isv.FuyouIsvKeyConfigManager;
|
||||
import cn.daxpay.open.channel.fuyou.entity.isv.FuyouIsvKeyConfig;
|
||||
import cn.daxpay.open.payment.trade.runtime.bo.CallbackData;
|
||||
import cn.daxpay.open.payment.trade.order.dao.NormalPayOrderManager;
|
||||
import cn.daxpay.open.payment.trade.order.dao.PayTradeManager;
|
||||
import cn.daxpay.open.payment.trade.order.entity.NormalPayOrder;
|
||||
import cn.daxpay.open.payment.trade.order.entity.PayTrade;
|
||||
import cn.daxpay.open.payment.trade.runtime.service.callback.PayCallbackService;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
@@ -42,7 +40,6 @@ public class FuyouPayCallbackService {
|
||||
private final FuyouChannelClient fuyouChannelClient;
|
||||
private final FuyouIsvKeyConfigManager fuyouIsvKeyConfigManager;
|
||||
private final PayTradeManager payTradeManager;
|
||||
private final NormalPayOrderManager normalPayOrderManager;
|
||||
private final PayCallbackService payCallbackService;
|
||||
|
||||
/// 支付回调处理
|
||||
@@ -67,16 +64,11 @@ public class FuyouPayCallbackService {
|
||||
return RESP_FAIL;
|
||||
}
|
||||
|
||||
// 3. 凭关联订单号(mchnt_order_no)反查容器, 再查关联交易
|
||||
// 3. 凭关联订单号(mchnt_order_no)直接反查 PayTrade
|
||||
// relationOrderNo 是 PayTrade 上的字段(回调/关退同步反查权威), 跨 tradeType(normal/gateway)统一,
|
||||
// 避免硬编码查 NormalPayOrder 导致 tradeType=gateway 时跨表查不到容器(与支付宝回调同源 bug)
|
||||
String relationOrderNo = resp.getOutTradeNo();
|
||||
NormalPayOrder normalOrder = normalPayOrderManager.lambdaQuery()
|
||||
.eq(NormalPayOrder::getRelationOrderNo, relationOrderNo)
|
||||
.oneOpt()
|
||||
.orElse(null);
|
||||
PayTrade trade = null;
|
||||
if (normalOrder != null) {
|
||||
trade = payTradeManager.findByContainerId(normalOrder.getId()).orElse(null);
|
||||
}
|
||||
PayTrade trade = payTradeManager.findByRelationOrderNo(relationOrderNo).orElse(null);
|
||||
if (trade == null) {
|
||||
log.error("富友支付回调: 未找到关联订单 relationOrderNo={}", relationOrderNo);
|
||||
return RESP_FAIL;
|
||||
|
||||
@@ -23,6 +23,12 @@ public class PayTradeContainerFields {
|
||||
/// 容器侧通道常用回执字段
|
||||
public record Fields(String relationOrderNo, String tradeProduct) {}
|
||||
|
||||
/// 容器侧通道路由凭证字段(支付/退款回调组装通道凭证用)
|
||||
///
|
||||
/// [NormalPayOrder] / [GatewayPayOrder] 同名字段集合, 按 tradeType 分发到对应容器读取,
|
||||
/// 供回调服务组装通道调用凭证(直连/服务商)。
|
||||
public record CredentialFields(String product, String channelMchNo, String capability) {}
|
||||
|
||||
/// 按 tradeType 查一次容器,同时返回关联订单号与支付厂商/产品回执
|
||||
public Fields resolve(PayTrade trade) {
|
||||
if (isGateway(trade)) {
|
||||
@@ -35,6 +41,23 @@ public class PayTradeContainerFields {
|
||||
.orElse(new Fields(null, null));
|
||||
}
|
||||
|
||||
/// 按 tradeType 查一次容器, 返回通道路由凭证字段(product/channelMchNo/capability)
|
||||
///
|
||||
/// 容器记录不存在(含跨容器误查)时返回 null, 调用方据此报"无法解析通道凭证"。
|
||||
/// 当前仅支持 NORMAL/GATEWAY 容器, 其他 tradeType 按普通容器兜底(与 [resolve] 对齐)。
|
||||
public CredentialFields resolveCredentialFields(PayTrade trade) {
|
||||
if (isGateway(trade)) {
|
||||
return gatewayPayOrderManager.findById(trade.getContainerId())
|
||||
.map(order -> new CredentialFields(
|
||||
order.getProduct(), order.getChannelMchNo(), order.getCapability()))
|
||||
.orElse(null);
|
||||
}
|
||||
return normalPayOrderManager.findById(trade.getContainerId())
|
||||
.map(order -> new CredentialFields(
|
||||
order.getProduct(), order.getChannelMchNo(), order.getCapability()))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private boolean isGateway(PayTrade trade) {
|
||||
return Objects.equals(trade.getTradeType(), PayTradeTypeEnum.GATEWAY.getCode());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user