diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/callback/AlipayCallbackService.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/callback/AlipayCallbackService.java index 5a0a5604f..7b8f9ef46 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/callback/AlipayCallbackService.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/callback/AlipayCallbackService.java @@ -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(多值取首项) diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/isv/AlipayIsvConfigAssembler.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/isv/AlipayIsvConfigAssembler.java index c3c311a2d..39d2ef069 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/isv/AlipayIsvConfigAssembler.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/service/isv/AlipayIsvConfigAssembler.java @@ -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; diff --git a/daxpay-channel/daxpay-channel-dougong/src/main/java/cn/daxpay/open/channel/dougong/service/isv/DougongIsvConfigAssembler.java b/daxpay-channel/daxpay-channel-dougong/src/main/java/cn/daxpay/open/channel/dougong/service/isv/DougongIsvConfigAssembler.java index e1a3e5f73..4ae36bd12 100644 --- a/daxpay-channel/daxpay-channel-dougong/src/main/java/cn/daxpay/open/channel/dougong/service/isv/DougongIsvConfigAssembler.java +++ b/daxpay-channel/daxpay-channel-dougong/src/main/java/cn/daxpay/open/channel/dougong/service/isv/DougongIsvConfigAssembler.java @@ -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); // 子商户身份 diff --git a/daxpay-channel/daxpay-channel-fuyou/src/main/java/cn/daxpay/open/channel/fuyou/service/callback/FuyouPayCallbackService.java b/daxpay-channel/daxpay-channel-fuyou/src/main/java/cn/daxpay/open/channel/fuyou/service/callback/FuyouPayCallbackService.java index d20243b60..64cbbe3bf 100644 --- a/daxpay-channel/daxpay-channel-fuyou/src/main/java/cn/daxpay/open/channel/fuyou/service/callback/FuyouPayCallbackService.java +++ b/daxpay-channel/daxpay-channel-fuyou/src/main/java/cn/daxpay/open/channel/fuyou/service/callback/FuyouPayCallbackService.java @@ -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; diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/PayTradeContainerFields.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/PayTradeContainerFields.java index 3a4809c70..bf3559d28 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/PayTradeContainerFields.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/runtime/service/PayTradeContainerFields.java @@ -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()); }