feat(wechat): 服务商(ISV)支付链路 + Product策略归类 + 移除sandbox

This commit is contained in:
DaxPay Dev
2026-07-05 12:58:25 +08:00
parent b07bb91892
commit dbc80f3498
17 changed files with 774 additions and 23 deletions

View File

@@ -41,4 +41,26 @@ public interface WechatChannelClient {
/// 退款同步(查询退款状态)
@PostExchange("/channel/wechat/refund-sync")
DaxResult<WechatRefundSyncResp> refundSync(@RequestBody WechatRefundSyncReq req);
// ===== 服务商模式(isv), 对应子应用 /channel/wechat/isv/* 端点 =====
/// 服务商支付下单
@PostExchange("/channel/wechat/isv/pay")
DaxResult<WechatPayResp> isvPay(@RequestBody WechatPayReq req);
/// 服务商支付同步(查询微信订单状态)
@PostExchange("/channel/wechat/isv/sync")
DaxResult<WechatSyncResp> isvSync(@RequestBody WechatSyncReq req);
/// 服务商关闭微信订单
@PostExchange("/channel/wechat/isv/close")
DaxResult<WechatCloseResp> isvClose(@RequestBody WechatCloseReq req);
/// 服务商退款
@PostExchange("/channel/wechat/isv/refund")
DaxResult<WechatRefundResp> isvRefund(@RequestBody WechatRefundReq req);
/// 服务商退款同步(查询退款状态)
@PostExchange("/channel/wechat/isv/refund-sync")
DaxResult<WechatRefundSyncResp> isvRefundSync(@RequestBody WechatRefundSyncReq req);
}

View File

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

View File

@@ -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<WechatIsvChanne
.eq(WechatIsvChannelMerchant::getSubMchId, subMchId)
.exists();
}
/// 根据通道商户号查询
public Optional<WechatIsvChannelMerchant> findByChannelMchNo(String channelMchNo) {
return lambdaQuery()
.eq(WechatIsvChannelMerchant::getChannelMchNo, channelMchNo)
.oneOpt();
}
}

View File

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

View File

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

View File

@@ -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<WechatCloseResp> result = wechatChannelClient.isvClose(req);
if (result.getCode() != 0) {
throw new BizInfoException(DaxPayErrorCode.OPERATION_FAIL, "error.channel.wechat.closeFailed", result.getMsg());
}
// 微信仅支持关闭, 无撤销
return CloseTypeEnum.CLOSE;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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