diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/WechatChannelClient.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/WechatChannelClient.java index f8073f87c..2bc796a61 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/WechatChannelClient.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/WechatChannelClient.java @@ -41,4 +41,26 @@ public interface WechatChannelClient { /// 退款同步(查询退款状态) @PostExchange("/channel/wechat/refund-sync") DaxResult refundSync(@RequestBody WechatRefundSyncReq req); + + // ===== 服务商模式(isv), 对应子应用 /channel/wechat/isv/* 端点 ===== + + /// 服务商支付下单 + @PostExchange("/channel/wechat/isv/pay") + DaxResult isvPay(@RequestBody WechatPayReq req); + + /// 服务商支付同步(查询微信订单状态) + @PostExchange("/channel/wechat/isv/sync") + DaxResult isvSync(@RequestBody WechatSyncReq req); + + /// 服务商关闭微信订单 + @PostExchange("/channel/wechat/isv/close") + DaxResult isvClose(@RequestBody WechatCloseReq req); + + /// 服务商退款 + @PostExchange("/channel/wechat/isv/refund") + DaxResult isvRefund(@RequestBody WechatRefundReq req); + + /// 服务商退款同步(查询退款状态) + @PostExchange("/channel/wechat/isv/refund-sync") + DaxResult isvRefundSync(@RequestBody WechatRefundSyncReq req); } diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/credential/WechatSdkCredential.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/credential/WechatSdkCredential.java index 268b76cfb..11dfb63e7 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/credential/WechatSdkCredential.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/credential/WechatSdkCredential.java @@ -9,10 +9,14 @@ import lombok.Data; /// 下发给子应用构建 WxJava [com.github.binarywang.wxpay.service.WxPayService]。 @Data public class WechatSdkCredential { - /// 微信商户号 + /// 微信商户号(服务商模式下为服务商商户号 sp_mchid) private String wxMchId; - /// 微信应用ID(公众号 / 小程序 / APP 的 appId) + /// 微信应用ID(公众号 / 小程序 / APP 的 appId; 服务商模式下为服务商 AppId sp_appid) private String wxAppId; + /// 微信特约商户号(服务商模式 sub_mchid; 直连模式留空) + private String subMchId; + /// 微信子应用 AppId(服务商模式 sub_appid; 直连模式留空) + private String subAppId; /// API V3 密钥 private String apiKeyV3; /// 商户私钥(PEM 格式 PKCS#8 字符串) @@ -25,6 +29,4 @@ public class WechatSdkCredential { private String publicKey; /// 支付公钥ID private String publicKeyId; - /// 是否沙箱环境 - private Boolean sandbox; } diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/dao/isv/WechatIsvChannelMerchantManager.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/dao/isv/WechatIsvChannelMerchantManager.java index 7d8dccf66..a724f93ef 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/dao/isv/WechatIsvChannelMerchantManager.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/dao/isv/WechatIsvChannelMerchantManager.java @@ -4,6 +4,8 @@ import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvChannelMerchant; import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager; import org.springframework.stereotype.Repository; +import java.util.Optional; + /// # 微信服务商通道商户绑定 /// @Repository @@ -16,4 +18,11 @@ public class WechatIsvChannelMerchantManager extends BaseManager findByChannelMchNo(String channelMchNo) { + return lambdaQuery() + .eq(WechatIsvChannelMerchant::getChannelMchNo, channelMchNo) + .oneOpt(); + } } diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/direct/WechatDirectConfigAssembler.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/direct/WechatDirectConfigAssembler.java index 2fa691013..a95431f32 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/direct/WechatDirectConfigAssembler.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/direct/WechatDirectConfigAssembler.java @@ -6,9 +6,6 @@ import cn.daxpay.open.channel.wechat.dao.direct.WechatDirectChannelMerchantManag import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectApp; import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectChannelMerchant; import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectKeyConfig; -import cn.daxpay.open.payment.masterdata.constants.product.dao.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.platform.core.exception.DataNotExistException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -33,7 +30,6 @@ public class WechatDirectConfigAssembler { private final WechatDirectChannelMerchantManager wechatDirectChannelMerchantManager; private final WechatDirectKeyConfigService wechatDirectKeyConfigService; private final WechatDirectAppCapabilityService wechatDirectAppCapabilityService; - private final PayProductConfigManager payProductConfigManager; /// 组装直连商户的通道调用凭证(下发给子应用) /// @@ -61,11 +57,6 @@ public class WechatDirectConfigAssembler { // 支付公钥新模式(为空则子应用走平台证书模式) credential.setPublicKey(keyConfig.getPublicKey()); credential.setPublicKeyId(keyConfig.getPublicKeyId()); - // 读取支付产品当前生效环境, 判断是否沙箱 - boolean sandbox = payProductConfigManager.findByProduct(ProductEnum.WECHAT_PAY.getCode()) - .map(c -> PayEnvEnum.SANDBOX.getCode().equals(c.getActiveEnv())) - .orElse(false); - credential.setSandbox(sandbox); return credential; } diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvConfigAssembler.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvConfigAssembler.java new file mode 100644 index 000000000..0c35b0da1 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/isv/WechatIsvConfigAssembler.java @@ -0,0 +1,80 @@ +package cn.daxpay.open.channel.wechat.service.isv; + +import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential; +import cn.daxpay.open.channel.wechat.dao.isv.WechatIsvChannelMerchantManager; +import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvApp; +import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvChannelMerchant; +import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvKeyConfig; +import cn.daxpay.open.channel.wechat.entity.isv.WechatIsvMchApp; +import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum; +import cn.daxpay.open.platform.core.exception.DataNotExistException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +/// # 微信服务商通道凭证组装器 +/// +/// 从服务商密钥配置([WechatIsvKeyConfig]) + 服务商应用([WechatIsvApp]) + +/// 特约商户绑定([WechatIsvChannelMerchant]) + 子商户应用([WechatIsvMchApp]) 组装通道调用凭证, +/// 下发给子应用构建 WxJava 服务商模式 [com.github.binarywang.wxpay.service.WxPayService]。 +/// +/// 字段映射(对齐微信支付 V3 服务商接口): +/// - sp_mchid ← [WechatIsvKeyConfig.wxMchId] (服务商商户号, 全局唯一) +/// - sp_appid ← [WechatIsvApp.wxAppId] (服务商应用AppId, 按能力解析) +/// - sub_mchid ← [WechatIsvChannelMerchant.subMchId] (特约商户号) +/// - sub_appid ← [WechatIsvMchApp.wxAppId] (子商户应用AppId, 可选; 未配置留空) +/// +/// 服务商应用解析(委托 [WechatIsvAppCapabilityService.resolveApp]): 显式配置 > appType自动推导 > 首个兜底。 +/// 子商户应用解析(委托 [WechatIsvMchAppCapabilityService.resolveApp]): 同优先级, 未命中返回 empty(sub_appid 留空)。 +/// +/// 供支付策略([cn.daxpay.open.channel.wechat.strategy.isv.*])组装通道调用凭证。 +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatIsvConfigAssembler { + + private final WechatIsvChannelMerchantManager wechatIsvChannelMerchantManager; + private final WechatIsvKeyConfigService wechatIsvKeyConfigService; + private final WechatIsvAppCapabilityService wechatIsvAppCapabilityService; + private final WechatIsvMchAppCapabilityService wechatIsvMchAppCapabilityService; + + /// 组装服务商模式的通道调用凭证(下发给子应用) + /// + /// @param mchNo 商户号(保留参数对齐直连签名, 服务商密钥全局唯一不依赖此字段) + /// @param channelMchNo 通道商户号(特约商户绑定主键) + /// @param capability 支付能力编码(用于解析应用) + /// @return 微信 SDK 凭证(服务商模式, subMchId/subAppId 已填充) + public WechatSdkCredential buildConfig(String mchNo, String channelMchNo, String capability) { + // 服务商密钥(全局唯一, 含 sp_mchid 与证书) + WechatIsvKeyConfig keyConfig = wechatIsvKeyConfigService.findByProduct(ProductEnum.WECHAT_ISV.getCode()); + // 特约商户绑定(取 sub_mchid) + WechatIsvChannelMerchant channelMerchant = wechatIsvChannelMerchantManager.findByChannelMchNo(channelMchNo) + // 微信: 通道商户配置不存在 + .orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist")); + // 服务商应用(取 sp_appid, 必须存在) + WechatIsvApp isvApp = wechatIsvAppCapabilityService.resolveApp(capability) + // 微信: 服务商应用不存在 + .orElseThrow(() -> new DataNotExistException("error.channel.wechat.appNotFound")); + // 子商户应用(取 sub_appid, 可选; 未配置时留空, SDK 仅用 sp_appid + sub_mchid 走服务商模式) + Optional mchApp = wechatIsvMchAppCapabilityService.resolveApp(channelMchNo, capability); + + WechatSdkCredential credential = new WechatSdkCredential(); + // 服务商身份(sp_mchid / sp_appid) + credential.setWxMchId(keyConfig.getWxMchId()); + credential.setWxAppId(isvApp.getWxAppId()); + // 特约商户身份(sub_mchid / sub_appid) + credential.setSubMchId(channelMerchant.getSubMchId()); + credential.setSubAppId(mchApp.map(WechatIsvMchApp::getWxAppId).orElse(null)); + // 服务商密钥与证书 + credential.setApiKeyV3(keyConfig.getApiKeyV3()); + credential.setPrivateKey(keyConfig.getPrivateKey()); + credential.setPrivateCert(keyConfig.getPrivateCert()); + credential.setCertSerialNo(keyConfig.getCertSerialNo()); + // 支付公钥新模式(为空则子应用走平台证书模式) + credential.setPublicKey(keyConfig.getPublicKey()); + credential.setPublicKeyId(keyConfig.getPublicKeyId()); + return credential; + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvCloseService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvCloseService.java new file mode 100644 index 000000000..f75b0329f --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvCloseService.java @@ -0,0 +1,49 @@ +package cn.daxpay.open.channel.wechat.service.payment.isv; + +import cn.daxpay.open.channel.wechat.client.WechatChannelClient; +import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential; +import cn.daxpay.open.channel.wechat.client.req.WechatCloseReq; +import cn.daxpay.open.channel.wechat.client.resp.WechatCloseResp; +import cn.daxpay.open.payment.common.result.DaxResult; +import cn.daxpay.open.payment.core.trade.entity.PayTrade; +import cn.daxpay.open.platform.core.code.DaxPayErrorCode; +import cn.daxpay.open.platform.core.enums.pay.pay.CloseTypeEnum; +import cn.daxpay.open.platform.core.exception.BizInfoException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 微信服务商支付关闭业务服务 +/// +/// 通过 [WechatChannelClient] 调用子应用 dax-pay-channel-one 服务商端点关闭微信订单。 +/// 微信 V3 仅提供关单接口(closePartnerOrderV3), 无撤销接口(useCancel 参数忽略, 统一返回 [CloseTypeEnum.CLOSE])。 +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatIsvCloseService { + + private final WechatChannelClient wechatChannelClient; + + /// 执行微信服务商订单关闭 + /// + /// @param trade 支付订单(tradeNo 作为 out_trade_no) + /// @param credential 通道调用凭证(服务商模式) + /// @param useCancel 是否使用撤销方式(微信无撤销接口, 忽略) + /// @return 关闭方式(微信固定返回 [CloseTypeEnum.CLOSE]) + public CloseTypeEnum close(PayTrade trade, WechatSdkCredential credential, boolean useCancel) { + // 构建请求(与直连一致) + WechatCloseReq req = new WechatCloseReq(); + req.setOutTradeNo(trade.getTradeNo()); + req.setTransactionId(trade.getOutOrderNo()); + req.setCredential(credential); + + // 调用子应用服务商端点 + DaxResult result = wechatChannelClient.isvClose(req); + if (result.getCode() != 0) { + throw new BizInfoException(DaxPayErrorCode.OPERATION_FAIL, "error.channel.wechat.closeFailed", result.getMsg()); + } + + // 微信仅支持关闭, 无撤销 + return CloseTypeEnum.CLOSE; + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvPayService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvPayService.java new file mode 100644 index 000000000..a4d867543 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvPayService.java @@ -0,0 +1,130 @@ +package cn.daxpay.open.channel.wechat.service.payment.isv; + +import cn.daxpay.open.channel.wechat.client.WechatChannelClient; +import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential; +import cn.daxpay.open.channel.wechat.client.enums.WechatPayBodyType; +import cn.daxpay.open.channel.wechat.client.enums.WechatPayMethod; +import cn.daxpay.open.channel.wechat.client.req.WechatPayReq; +import cn.daxpay.open.channel.wechat.client.resp.WechatPayResp; +import cn.daxpay.open.payment.common.result.DaxResult; +import cn.daxpay.open.payment.core.trade.bo.PayTradeResultBo; +import cn.daxpay.open.payment.core.trade.entity.PayTrade; +import cn.daxpay.open.payment.unipay.param.trade.pay.NormalPayParam; +import cn.daxpay.open.platform.core.code.CommonErrorCode; +import cn.daxpay.open.platform.core.code.DaxPayErrorCode; +import cn.daxpay.open.platform.core.enums.pay.channel.PayMethodEnum; +import cn.daxpay.open.platform.core.enums.unipay.PayBodyTypeEnum; +import cn.daxpay.open.platform.core.exception.BizInfoException; +import cn.daxpay.open.platform.system.service.config.PlatformUrlConfigService; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 微信服务商支付执行业务服务 +/// +/// 通过 [WechatChannelClient] 调用子应用 dax-pay-channel-one 完成微信服务商支付下单。 +/// 与直连模式([cn.daxpay.open.channel.wechat.service.payment.pay.WechatPayService])的核心差异: +/// - 调用服务商端点 `/channel/wechat/isv/pay`(子应用内部走 `/v3/partner/transactions/*`) +/// - 异步通知回调路径用 `/wechat/isv/pay`(服务商用服务商 apiV3Key 解密) +/// +/// 请求构建、响应解析与直连模式一致(子应用返回结构相同)。 +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatIsvPayService { + + private final WechatChannelClient wechatChannelClient; + private final PlatformUrlConfigService platformUrlConfigService; + + /// 执行微信服务商支付 + /// + /// @param order 支付订单(tradeNo 作为 out_trade_no) + /// @param payParam 支付参数 + /// @param credential 通道调用凭证(服务商模式, 含 sp_mchid/sub_mchid/sp_appid/sub_appid) + /// @return 支付结果 + public PayTradeResultBo pay(PayTrade order, NormalPayParam payParam, WechatSdkCredential credential) { + // 构建请求(与直连一致) + WechatPayReq req = new WechatPayReq(); + // 使用支付交易号作为商户订单号透传给微信, 回调时凭此反查 PayTrade + req.setOutTradeNo(order.getTradeNo()); + req.setAmount(payParam.getAmount()); + req.setDescription(payParam.getTitle()); + req.setMethod(mapMethod(payParam.getMethod())); + req.setAuthCode(payParam.getAuthCode()); + req.setOpenId(payParam.getOpenId()); + req.setAttach(payParam.getAttach()); + req.setNotifyUrl(this.buildNotifyUrl(order)); + req.setExpireTime(payParam.getExpiredTime()); + if (req.getMethod() == WechatPayMethod.H5) { + req.setWapUrl(payParam.getReturnUrl()); + req.setWapName(payParam.getTitle()); + } + req.setCredential(credential); + + // 调用子应用服务商端点 + DaxResult result = wechatChannelClient.isvPay(req); + if (result.getCode() != 0) { + throw new BizInfoException(DaxPayErrorCode.TRADE_FAIL, "error.channel.wechat.payFailed", result.getMsg()); + } + + return toPayResult(result.getData()); + } + + /// 生成微信服务商支付异步通知地址(微信→平台) + /// + /// 服务商回调路径: `{backendBaseUrl}/unipay/callback/{mchNo}/{appId}/wechat/isv/pay` + /// (与直连 `/wechat/pay` 分离, 服务商回调用服务商 apiV3Key 解密) + private String buildNotifyUrl(PayTrade order) { + String base = platformUrlConfigService.getUrlConfig().getBackendBaseUrl(); + if (StrUtil.isBlank(base)) { + // backendBaseUrl 未配置时抛清晰异常 + throw new BizInfoException(DaxPayErrorCode.CONFIG_ERROR, "error.common.backendBaseUrlNotConfigured"); + } + return StrUtil.format("{}/unipay/callback/{}/{}/wechat/isv/pay", + base, order.getMchNo(), order.getAppId()); + } + + /// 平台支付方式([PayMethodEnum] code) -> 微信通道支付方式 + private static WechatPayMethod mapMethod(String methodCode) { + PayMethodEnum m = PayMethodEnum.findByCode(methodCode); + return switch (m) { + case WECHAT_QR -> WechatPayMethod.NATIVE; + case WECHAT_JSAPI -> WechatPayMethod.JSAPI; + case WECHAT_MINI -> WechatPayMethod.MINI; + case WECHAT_APP -> WechatPayMethod.APP; + case WECHAT_H5 -> WechatPayMethod.H5; + case WECHAT_BARCODE -> WechatPayMethod.MICROPAY; + default -> throw new BizInfoException(CommonErrorCode.UN_SUPPORTED_OPERATE, + "error.channel.wechat.unsupportedPayMethod", methodCode); + }; + } + + /// 解析子应用响应为支付结果 BO + private PayTradeResultBo toPayResult(WechatPayResp resp) { + PayTradeResultBo bo = new PayTradeResultBo() + .setOutOrderNo(resp.getTransactionId()) + .setComplete(Boolean.TRUE.equals(resp.getComplete())) + .setPayBody(resp.getPayBody()); + + // 支付内容类型映射(子应用 WechatPayBodyType -> 平台 PayBodyTypeEnum) + WechatPayBodyType bodyType = resp.getPayBodyType(); + if (bodyType != null) { + switch (bodyType) { + case LINK -> bo.setPayBodyType(PayBodyTypeEnum.LINK); + case QR_CODE -> bo.setPayBodyType(PayBodyTypeEnum.QR_CODE); + case IDENTIFIER, APP_ORDER_STR -> bo.setPayBodyType(PayBodyTypeEnum.IDENTIFIER); + } + } + + // 完成时间(MICROPAY 同步成功时返回) + bo.setFinishTime(resp.getFinishTime()); + // 金额(MICROPAY 同步成功时返回) + bo.setTotalAmount(resp.getTotalAmount()); + bo.setRealAmount(resp.getPayerTotal()); + bo.setBuyerPayAmount(resp.getPayerTotal()); + // 用户标识 + bo.setBuyerId(resp.getOpenId()); + return bo; + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvRefundService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvRefundService.java new file mode 100644 index 000000000..6ccc562ab --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvRefundService.java @@ -0,0 +1,75 @@ +package cn.daxpay.open.channel.wechat.service.payment.isv; + +import cn.daxpay.open.channel.wechat.client.WechatChannelClient; +import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential; +import cn.daxpay.open.channel.wechat.client.req.WechatRefundReq; +import cn.daxpay.open.channel.wechat.client.resp.WechatRefundResp; +import cn.daxpay.open.payment.common.enums.RefundOrderStatusEnum; +import cn.daxpay.open.payment.common.result.DaxResult; +import cn.daxpay.open.payment.core.trade.bo.RefundResultBo; +import cn.daxpay.open.payment.core.trade.entity.PayRefundOrder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 微信服务商退款业务服务 +/// +/// 通过 [WechatChannelClient] 调用子应用 dax-pay-channel-one 服务商端点发起微信退款。 +/// 退款状态判定与直连模式一致。 +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatIsvRefundService { + + private final WechatChannelClient wechatChannelClient; + + /// 微信退款状态 + private static final String STATUS_SUCCESS = "SUCCESS"; + private static final String STATUS_CLOSED = "CLOSED"; + + /// 执行微信服务商退款 + public RefundResultBo refund(PayRefundOrder refundOrder, WechatSdkCredential credential) { + // 构建请求(与直连一致) + WechatRefundReq req = new WechatRefundReq(); + req.setOutTradeNo(refundOrder.getOrderNo()); + req.setTransactionId(refundOrder.getOutOrderNo()); + req.setOutRefundNo(refundOrder.getRefundNo()); + // 微信退款需原订单总额(orderAmount)与退款金额(amount) + req.setTotalAmount(refundOrder.getOrderAmount()); + req.setRefundAmount(refundOrder.getAmount()); + req.setReason(refundOrder.getReason()); + req.setNotifyUrl(refundOrder.getNotifyUrl()); + req.setCredential(credential); + + // 调用子应用服务商端点 + DaxResult result = wechatChannelClient.isvRefund(req); + if (result.getCode() != 0) { + log.error("微信服务商通道退款失败: refundNo={}, msg={}", refundOrder.getRefundNo(), result.getMsg()); + RefundResultBo bo = new RefundResultBo(); + bo.setComplete(false) + .setStatus(RefundOrderStatusEnum.FAIL) + .setSyncSuccess(false) + .setSyncErrorMsg(result.getMsg()); + return bo; + } + + return toRefundResult(result.getData()); + } + + /// 解析子应用响应 + private RefundResultBo toRefundResult(WechatRefundResp resp) { + RefundResultBo bo = new RefundResultBo(); + bo.setOutRefundNo(resp.getOutRefundNo()); + bo.setFinishTime(resp.getFinishTime()); + bo.setRefundAmount(resp.getRefundAmount()); + // SUCCESS / CLOSED → 退款终态成功; 否则退款中, 需同步查询确认 + if (STATUS_SUCCESS.equals(resp.getStatus()) || STATUS_CLOSED.equals(resp.getStatus())) { + bo.setComplete(true) + .setStatus(RefundOrderStatusEnum.SUCCESS); + } else { + bo.setComplete(false) + .setStatus(RefundOrderStatusEnum.PROGRESS); + } + return bo; + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvRefundSyncService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvRefundSyncService.java new file mode 100644 index 000000000..823f47bbe --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvRefundSyncService.java @@ -0,0 +1,82 @@ +package cn.daxpay.open.channel.wechat.service.payment.isv; + +import cn.daxpay.open.channel.wechat.client.WechatChannelClient; +import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential; +import cn.daxpay.open.channel.wechat.client.req.WechatRefundSyncReq; +import cn.daxpay.open.channel.wechat.client.resp.WechatRefundSyncResp; +import cn.daxpay.open.payment.common.enums.RefundOrderStatusEnum; +import cn.daxpay.open.payment.common.result.DaxResult; +import cn.daxpay.open.payment.core.trade.bo.RefundResultBo; +import cn.daxpay.open.payment.core.trade.entity.PayRefundOrder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 微信服务商退款同步业务服务 +/// +/// 通过 [WechatChannelClient] 调用子应用 dax-pay-channel-one 服务商端点查询微信退款状态, +/// 将微信退款 status 映射为平台 [RefundOrderStatusEnum]。映射规则与直连模式一致。 +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatIsvRefundSyncService { + + private final WechatChannelClient wechatChannelClient; + + /// 微信退款状态 + private static final String STATUS_SUCCESS = "SUCCESS"; + private static final String STATUS_CLOSED = "CLOSED"; + private static final String STATUS_ABNORMAL = "ABNORMAL"; + + /// 执行微信服务商退款同步查询 + public RefundResultBo sync(PayRefundOrder refundOrder, WechatSdkCredential credential) { + // 构建请求(与直连一致) + var req = new WechatRefundSyncReq(); + req.setOutRefundNo(refundOrder.getRefundNo()); + req.setCredential(credential); + + // 调用子应用服务商端点 + DaxResult result = wechatChannelClient.isvRefundSync(req); + if (result.getCode() != 0) { + log.error("微信服务商通道退款同步失败: refundNo={}, msg={}", refundOrder.getRefundNo(), result.getMsg()); + RefundResultBo bo = new RefundResultBo(); + bo.setSyncSuccess(false) + .setSyncErrorMsg(result.getMsg()); + return bo; + } + + return toSyncResult(result.getData()); + } + + /// 解析子应用响应, 映射 status → [RefundOrderStatusEnum] + private RefundResultBo toSyncResult(WechatRefundSyncResp resp) { + RefundResultBo bo = new RefundResultBo(); + bo.setOutRefundNo(resp.getOutRefundNo()); + String status = resp.getStatus(); + + // 退款成功 + if (STATUS_SUCCESS.equals(status)) { + return bo.setComplete(true) + .setStatus(RefundOrderStatusEnum.SUCCESS) + .setFinishTime(resp.getFinishTime()) + .setRefundAmount(resp.getRefundAmount()); + } + + // 退款关闭 + if (STATUS_CLOSED.equals(status)) { + return bo.setComplete(true) + .setStatus(RefundOrderStatusEnum.CLOSE) + .setFinishTime(resp.getFinishTime()); + } + + // 退款异常 + if (STATUS_ABNORMAL.equals(status)) { + return bo.setComplete(true) + .setStatus(RefundOrderStatusEnum.FAIL); + } + + // 退款中(PROCESSING 或未知) + return bo.setComplete(false) + .setStatus(RefundOrderStatusEnum.PROGRESS); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvSyncService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvSyncService.java new file mode 100644 index 000000000..ad4c486a4 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/isv/WechatIsvSyncService.java @@ -0,0 +1,98 @@ +package cn.daxpay.open.channel.wechat.service.payment.isv; + +import cn.daxpay.open.channel.wechat.client.WechatChannelClient; +import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential; +import cn.daxpay.open.channel.wechat.client.req.WechatSyncReq; +import cn.daxpay.open.channel.wechat.client.resp.WechatSyncResp; +import cn.daxpay.open.payment.common.enums.PayFundStatusEnum; +import cn.daxpay.open.payment.common.result.DaxResult; +import cn.daxpay.open.payment.core.trade.bo.PaySyncResultBo; +import cn.daxpay.open.payment.core.trade.entity.PayTrade; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 微信服务商支付同步业务服务 +/// +/// 通过 [WechatChannelClient] 调用子应用 dax-pay-channel-one 服务商端点查询微信订单状态, +/// 将微信 trade_state 映射为平台 [PayFundStatusEnum]。映射规则与直连模式一致。 +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatIsvSyncService { + + private final WechatChannelClient wechatChannelClient; + + /// 微信交易状态 + private static final String STATE_SUCCESS = "SUCCESS"; + private static final String STATE_REFUND = "REFUND"; + private static final String STATE_NOTPAY = "NOTPAY"; + private static final String STATE_USERPAYING = "USERPAYING"; + private static final String STATE_CLOSED = "CLOSED"; + private static final String STATE_REVOKED = "REVOKED"; + private static final String STATE_PAYERROR = "PAYERROR"; + private static final String STATE_ACCEPT = "ACCEPT"; + + /// 执行微信服务商支付同步 + public PaySyncResultBo sync(PayTrade trade, WechatSdkCredential credential) { + // 构建请求(与直连一致) + WechatSyncReq req = new WechatSyncReq(); + req.setOutTradeNo(trade.getTradeNo()); + req.setTransactionId(trade.getOutOrderNo()); + req.setCredential(credential); + + // 调用子应用服务商端点 + DaxResult result = wechatChannelClient.isvSync(req); + if (result.getCode() != 0) { + log.error("微信服务商通道同步失败: outTradeNo={}, msg={}", trade.getTradeNo(), result.getMsg()); + PaySyncResultBo bo = new PaySyncResultBo(); + bo.setSyncSuccess(false) + .setSyncErrorMsg(result.getMsg()); + return bo; + } + + return toSyncResult(result.getData()); + } + + /// 解析子应用响应, 映射 trade_state → [PayFundStatusEnum] + private PaySyncResultBo toSyncResult(WechatSyncResp resp) { + PaySyncResultBo bo = new PaySyncResultBo(); + bo.setOutOrderNo(resp.getTransactionId()); + + String tradeState = resp.getTradeState(); + + // 支付成功(REFUND 表示原订单已支付, 后续发起退款, 资金状态视为 SUCCESS) + if (STATE_SUCCESS.equals(tradeState) || STATE_REFUND.equals(tradeState)) { + return bo.setPayStatus(PayFundStatusEnum.SUCCESS) + .setFinishTime(resp.getSuccessTime()) + .setRealAmount(resp.getPayerTotal()) + .setAmount(resp.getTotalAmount()) + .setBuyerId(resp.getOpenId()); + } + + // 待支付 / 用户支付中 / 已接收(等待扣款) + if (STATE_NOTPAY.equals(tradeState) || STATE_USERPAYING.equals(tradeState) || STATE_ACCEPT.equals(tradeState)) { + return bo.setPayStatus(PayFundStatusEnum.PROCESSING); + } + + // 已关闭 + if (STATE_CLOSED.equals(tradeState)) { + return bo.setPayStatus(PayFundStatusEnum.CLOSE); + } + + // 已撤销(付款码支付撤销) + if (STATE_REVOKED.equals(tradeState)) { + return bo.setPayStatus(PayFundStatusEnum.CANCEL); + } + + // 支付失败 + if (STATE_PAYERROR.equals(tradeState)) { + return bo.setPayStatus(PayFundStatusEnum.FAIL); + } + + // 未知状态 + return bo.setSyncSuccess(false) + .setSyncErrorMsg(StrUtil.blankToDefault(resp.getTradeStateDesc(), "微信服务商同步查询未知状态: " + tradeState)); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/WechatDirectProductStrategy.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectProductStrategy.java similarity index 94% rename from daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/WechatDirectProductStrategy.java rename to daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectProductStrategy.java index c1a93d7ea..d0acc5b88 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/WechatDirectProductStrategy.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectProductStrategy.java @@ -1,4 +1,4 @@ -package cn.daxpay.open.channel.wechat.strategy; +package cn.daxpay.open.channel.wechat.strategy.direct; import cn.daxpay.open.platform.core.enums.pay.channel.ChannelApiCallMode; import cn.daxpay.open.platform.core.enums.pay.channel.ChannelPayIdType; @@ -36,11 +36,6 @@ public class WechatDirectProductStrategy extends AbsProductStrategy { return ProductEnum.WECHAT_PAY; } - @Override - public boolean isSandbox() { - return false; - } - @Override public ChannelApiCallMode getApiCallMode() { return ChannelApiCallMode.MCH; diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvCloseStrategy.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvCloseStrategy.java new file mode 100644 index 000000000..317973451 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvCloseStrategy.java @@ -0,0 +1,50 @@ +package cn.daxpay.open.channel.wechat.strategy.isv; + +import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential; +import cn.daxpay.open.channel.wechat.service.isv.WechatIsvConfigAssembler; +import cn.daxpay.open.channel.wechat.service.payment.isv.WechatIsvCloseService; +import cn.daxpay.open.payment.core.strategy.pay.AbsPayCloseStrategy; +import cn.daxpay.open.payment.core.strategy.pay.PayStrategyContext; +import cn.daxpay.open.payment.core.trade.entity.NormalPayOrder; +import cn.daxpay.open.payment.core.trade.entity.PayTrade; +import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum; +import cn.daxpay.open.platform.core.enums.pay.pay.CloseTypeEnum; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 微信服务商支付关闭策略 +/// +/// 微信服务商模式(ProductEnum.WECHAT_ISV)下的支付关闭策略。 +/// 从上下文容器读取通道路由参数(channelMchNo / capability), +/// 组装服务商通道凭证(委托 [WechatIsvConfigAssembler]), 关闭执行委托给 [WechatCloseService]。 +/// +/// 注意: 微信 V3 仅提供关单接口, 无撤销接口(useCancel 参数由 service 层忽略)。 +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatIsvCloseStrategy extends AbsPayCloseStrategy { + + private final WechatIsvCloseService wechatIsvCloseService; + private final WechatIsvConfigAssembler wechatIsvConfigAssembler; + + @Override + public ProductEnum getProduct() { + return ProductEnum.WECHAT_ISV; + } + + @Override + public CloseTypeEnum doClose(PayStrategyContext context, boolean useCancel) { + // 从上下文容器读取通道路由参数, 用于凭证解析 + NormalPayOrder normalOrder = context.getContainer(); + String channelMchNo = normalOrder != null ? normalOrder.getChannelMchNo() : null; + String capability = normalOrder != null ? normalOrder.getCapability() : null; + PayTrade trade = context.getTrade(); + + // 组装服务商通道调用凭证 + WechatSdkCredential credential = wechatIsvConfigAssembler.buildConfig( + trade.getMchNo(), channelMchNo, capability); + + return wechatIsvCloseService.close(trade, credential, useCancel); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvPayStrategy.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvPayStrategy.java new file mode 100644 index 000000000..7084f8148 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvPayStrategy.java @@ -0,0 +1,48 @@ +package cn.daxpay.open.channel.wechat.strategy.isv; + +import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential; +import cn.daxpay.open.channel.wechat.service.isv.WechatIsvConfigAssembler; +import cn.daxpay.open.channel.wechat.service.payment.isv.WechatIsvPayService; +import cn.daxpay.open.payment.core.strategy.pay.AbsNormalPayStrategy; +import cn.daxpay.open.payment.core.strategy.pay.PayStrategyContext; +import cn.daxpay.open.payment.core.trade.bo.PayTradeResultBo; +import cn.daxpay.open.payment.unipay.param.trade.pay.NormalPayParam; +import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 微信服务商支付策略 +/// +/// 微信服务商模式(ProductEnum.WECHAT_ISV)下发起支付的具体执行策略。 +/// 配置组装在 [doBeforePay] 阶段完成(委托 [WechatIsvConfigAssembler]), 支付执行委托给 [WechatPayService]。 +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatIsvPayStrategy extends AbsNormalPayStrategy { + + private final WechatIsvPayService wechatIsvPayService; + private final WechatIsvConfigAssembler wechatIsvConfigAssembler; + + @Override + public ProductEnum getProduct() { + return ProductEnum.WECHAT_ISV; + } + + /// 支付前预处理: 从请求参数读取通道路由参数, 组装服务商通道凭证写入上下文 + /// 配置缺失在此阶段抛异常, fail-fast, 不会进入 doPay + @Override + public void doBeforePay(PayStrategyContext context) { + NormalPayParam payParam = context.getPayParam(); + WechatSdkCredential credential = wechatIsvConfigAssembler.buildConfig( + payParam.getMchNo(), payParam.getChannelMchNo(), payParam.getCapability()); + context.setChannelConfig(credential); + } + + @Override + public PayTradeResultBo doPay(PayStrategyContext context) { + // 直接使用 doBeforePay 预组装的服务商通道凭证 + WechatSdkCredential credential = context.getChannelConfig(WechatSdkCredential.class); + return wechatIsvPayService.pay(context.getTrade(), context.getPayParam(), credential); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/WechatIsvProductStrategy.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvProductStrategy.java similarity index 94% rename from daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/WechatIsvProductStrategy.java rename to daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvProductStrategy.java index 0757ed564..6d3dd1f8e 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/WechatIsvProductStrategy.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvProductStrategy.java @@ -1,4 +1,4 @@ -package cn.daxpay.open.channel.wechat.strategy; +package cn.daxpay.open.channel.wechat.strategy.isv; import cn.daxpay.open.platform.core.enums.pay.channel.ChannelApiCallMode; import cn.daxpay.open.platform.core.enums.pay.channel.ChannelPayIdType; @@ -39,9 +39,6 @@ public class WechatIsvProductStrategy extends AbsProductStrategy { @Override public boolean isIsv() { return true; } - @Override - public boolean isSandbox() { return false; } - @Override public ChannelApiCallMode getApiCallMode() { return ChannelApiCallMode.MIX; } diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvRefundStrategy.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvRefundStrategy.java new file mode 100644 index 000000000..12a9fef86 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvRefundStrategy.java @@ -0,0 +1,39 @@ +package cn.daxpay.open.channel.wechat.strategy.isv; + +import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential; +import cn.daxpay.open.channel.wechat.service.isv.WechatIsvConfigAssembler; +import cn.daxpay.open.channel.wechat.service.payment.isv.WechatIsvRefundService; +import cn.daxpay.open.payment.core.strategy.refund.AbsRefundStrategy; +import cn.daxpay.open.payment.core.trade.bo.RefundResultBo; +import cn.daxpay.open.payment.core.trade.entity.PayRefundOrder; +import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 微信服务商退款策略 +/// +/// 微信服务商模式(ProductEnum.WECHAT_ISV)下的退款策略。 +/// 从退款订单读取通道路由参数(channelMchNo / capability), +/// 组装服务商通道凭证(委托 [WechatIsvConfigAssembler]), 退款执行委托给 [WechatRefundService]。 +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatIsvRefundStrategy extends AbsRefundStrategy { + + private final WechatIsvRefundService wechatIsvRefundService; + private final WechatIsvConfigAssembler wechatIsvConfigAssembler; + + @Override + public ProductEnum getProduct() { + return ProductEnum.WECHAT_ISV; + } + + @Override + public RefundResultBo doRefund(PayRefundOrder refundOrder) { + // 组装服务商通道调用凭证 + WechatSdkCredential credential = wechatIsvConfigAssembler.buildConfig( + refundOrder.getMchNo(), refundOrder.getChannelMchNo(), refundOrder.getCapability()); + return wechatIsvRefundService.refund(refundOrder, credential); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvSyncRefundStrategy.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvSyncRefundStrategy.java new file mode 100644 index 000000000..0d3c52e34 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvSyncRefundStrategy.java @@ -0,0 +1,38 @@ +package cn.daxpay.open.channel.wechat.strategy.isv; + +import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential; +import cn.daxpay.open.channel.wechat.service.isv.WechatIsvConfigAssembler; +import cn.daxpay.open.channel.wechat.service.payment.isv.WechatIsvRefundSyncService; +import cn.daxpay.open.payment.core.strategy.refund.AbsSyncRefundStrategy; +import cn.daxpay.open.payment.core.trade.bo.RefundResultBo; +import cn.daxpay.open.payment.core.trade.entity.PayRefundOrder; +import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 微信服务商退款同步策略 +/// +/// 微信服务商模式(ProductEnum.WECHAT_ISV)下的退款同步策略。 +/// 组装服务商通道凭证(委托 [WechatIsvConfigAssembler]), 同步执行委托给 [WechatRefundSyncService]。 +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatIsvSyncRefundStrategy extends AbsSyncRefundStrategy { + + private final WechatIsvRefundSyncService wechatIsvRefundSyncService; + private final WechatIsvConfigAssembler wechatIsvConfigAssembler; + + @Override + public ProductEnum getProduct() { + return ProductEnum.WECHAT_ISV; + } + + @Override + public RefundResultBo doSync(PayRefundOrder refundOrder) { + // 组装服务商通道调用凭证 + WechatSdkCredential credential = wechatIsvConfigAssembler.buildConfig( + refundOrder.getMchNo(), refundOrder.getChannelMchNo(), refundOrder.getCapability()); + return wechatIsvRefundSyncService.sync(refundOrder, credential); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvSyncStrategy.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvSyncStrategy.java new file mode 100644 index 000000000..06b7efb1a --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/isv/WechatIsvSyncStrategy.java @@ -0,0 +1,46 @@ +package cn.daxpay.open.channel.wechat.strategy.isv; + +import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential; +import cn.daxpay.open.channel.wechat.service.isv.WechatIsvConfigAssembler; +import cn.daxpay.open.channel.wechat.service.payment.isv.WechatIsvSyncService; +import cn.daxpay.open.payment.core.strategy.pay.PayStrategyContext; +import cn.daxpay.open.payment.core.strategy.sync.AbsSyncPayOrderStrategy; +import cn.daxpay.open.payment.core.trade.bo.PaySyncResultBo; +import cn.daxpay.open.payment.core.trade.entity.NormalPayOrder; +import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 微信服务商支付同步策略 +/// +/// 微信服务商模式(ProductEnum.WECHAT_ISV)下的支付同步策略。 +/// 从上下文容器读取通道路由参数(channelMchNo / capability), +/// 组装服务商通道凭证(委托 [WechatIsvConfigAssembler]), 同步执行委托给 [WechatSyncService]。 +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatIsvSyncStrategy extends AbsSyncPayOrderStrategy { + + private final WechatIsvSyncService wechatIsvSyncService; + private final WechatIsvConfigAssembler wechatIsvConfigAssembler; + + @Override + public ProductEnum getProduct() { + return ProductEnum.WECHAT_ISV; + } + + @Override + public PaySyncResultBo doSync(PayStrategyContext context) { + // 从上下文容器读取通道路由参数, 用于凭证解析 + NormalPayOrder normalOrder = context.getContainer(); + String channelMchNo = normalOrder != null ? normalOrder.getChannelMchNo() : null; + String capability = normalOrder != null ? normalOrder.getCapability() : null; + + // 组装服务商通道调用凭证 + WechatSdkCredential credential = wechatIsvConfigAssembler.buildConfig( + context.getTrade().getMchNo(), channelMchNo, capability); + + return wechatIsvSyncService.sync(context.getTrade(), credential); + } +}