feat(payment): 微信直连渠道对接(退款/支付/关闭/同步) + 退款菜单

This commit is contained in:
DaxPay Dev
2026-07-03 22:06:43 +08:00
parent 05bbd58e92
commit cc3a9f87e1
27 changed files with 1139 additions and 3 deletions

View File

@@ -3,16 +3,16 @@
Source Server : 229本地服务
Source Server Type : PostgreSQL
Source Server Version : 160009 (160009)
Source Server Version : 160014 (160014)
Source Host : 192.168.1.229:5432
Source Catalog : daxpay-dev
Source Schema : public
Target Server Type : PostgreSQL
Target Server Version : 160009 (160009)
Target Server Version : 160014 (160014)
File Encoding : 65001
Date: 03/07/2026 12:01:10
Date: 03/07/2026 18:32:43
*/
@@ -154,6 +154,7 @@ INSERT INTO "public"."iam_perm_menu" VALUES (40105, 405, 'payment:config:product
INSERT INTO "public"."iam_perm_menu" VALUES (4040116, 401, 'payment:platform:provider_manage', 'admin', 'PayProviderManage', '支付渠道', 'Payment Provider', 'menu.payment.platform.provider.manage', 'lucide:wallet', 'f', 'f', '/payment/masterdata/provider/PayProviderList', '/payment/platform/pay-provider', NULL, 0.5, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-17 10:00:31.640482+00', '2026-06-27 13:01:24.318649+00');
INSERT INTO "public"."iam_perm_menu" VALUES (309, 308, 'system:notify:notice', 'admin', 'SystemNotify', '通知公告', 'Notification', 'menu.system.notify.notice', 'lucide:megaphone', 'f', 'f', '/system/notify/notice/NoticeList', '/system/notify/notice', NULL, 20, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, '2026-06-24 16:00:00+00', '2026-06-24 16:00:00+00', NULL);
INSERT INTO "public"."iam_perm_menu" VALUES (308, 3, 'system:notify', 'admin', 'SystemNotice', '通知中心', 'Notification Center', 'menu.system.notify', 'lucide:bell', 'f', 'f', NULL, '/system/notify', '/system/notify/notice', 20, 'f', 't', 'f', 1, 1, 0, 'f', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-01 07:16:30.927645+00', '2026-07-01 07:16:30.927645+00');
INSERT INTO "public"."iam_perm_menu" VALUES (603, 6, 'payment:refund', 'admin', 'RefundOrderList', '退款订单', 'Refund Orders', 'menu.trade.refundOrder', 'lucide:rotate-ccw', 'f', 'f', '/payment/order/RefundOrderList', '/trade/refund-order', NULL, 3, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-03 16:00:00+00', '2026-07-03 16:00:00+00');
-- ----------------------------
-- Primary Key structure for table iam_perm_menu

View File

@@ -0,0 +1,44 @@
package cn.daxpay.open.channel.wechat.client;
import cn.daxpay.open.channel.wechat.client.req.WechatCloseReq;
import cn.daxpay.open.channel.wechat.client.req.WechatPayReq;
import cn.daxpay.open.channel.wechat.client.req.WechatRefundReq;
import cn.daxpay.open.channel.wechat.client.req.WechatRefundSyncReq;
import cn.daxpay.open.channel.wechat.client.req.WechatSyncReq;
import cn.daxpay.open.channel.wechat.client.resp.WechatCloseResp;
import cn.daxpay.open.channel.wechat.client.resp.WechatPayResp;
import cn.daxpay.open.channel.wechat.client.resp.WechatRefundResp;
import cn.daxpay.open.channel.wechat.client.resp.WechatRefundSyncResp;
import cn.daxpay.open.channel.wechat.client.resp.WechatSyncResp;
import cn.daxpay.open.payment.common.result.DaxResult;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;
/// # 微信通道客户端
///
/// 声明式 HTTP 接口, 调用子应用 dax-pay-channel-one 的微信通道接口。
@HttpExchange(accept = MediaType.APPLICATION_JSON_VALUE, contentType = MediaType.APPLICATION_JSON_VALUE)
public interface WechatChannelClient {
/// 支付下单
@PostExchange("/channel/wechat/pay")
DaxResult<WechatPayResp> pay(@RequestBody WechatPayReq req);
/// 支付同步(查询微信订单状态)
@PostExchange("/channel/wechat/sync")
DaxResult<WechatSyncResp> sync(@RequestBody WechatSyncReq req);
/// 关闭微信订单
@PostExchange("/channel/wechat/close")
DaxResult<WechatCloseResp> close(@RequestBody WechatCloseReq req);
/// 退款
@PostExchange("/channel/wechat/refund")
DaxResult<WechatRefundResp> refund(@RequestBody WechatRefundReq req);
/// 退款同步(查询退款状态)
@PostExchange("/channel/wechat/refund-sync")
DaxResult<WechatRefundSyncResp> refundSync(@RequestBody WechatRefundSyncReq req);
}

View File

@@ -0,0 +1,32 @@
package cn.daxpay.open.channel.wechat.client.credential;
import lombok.Data;
/// # 微信 SDK 凭证
///
/// 与子应用 dax-pay-channel-one 的 `WechatSdkCredential` 镜像, 跨 HTTP 传输时字段对齐。
/// 主应用从进件实体(WechatDirectApp + WechatDirectKeyConfig)提取商户号 / 应用ID / 私钥 / 证书后组装,
/// 下发给子应用构建 WxJava [com.github.binarywang.wxpay.service.WxPayService]。
@Data
public class WechatSdkCredential {
/// 微信商户号
private String wxMchId;
/// 微信应用ID(公众号 / 小程序 / APP 的 appId)
private String wxAppId;
/// APIv3 密钥
private String apiKeyV3;
/// 商户私钥(PEM 格式 PKCS#8 字符串)
private String privateKey;
/// 商户证书内容(PEM 格式)
private String privateCert;
/// 商户证书序列号
private String certSerialNo;
/// 支付公钥(支付公钥新模式使用, 为空则走平台证书模式)
private String publicKey;
/// 支付公钥ID
private String publicKeyId;
/// APIv2 密钥(仅付款码 MICROPAY 走 V2 接口时需要)
private String apiV2Key;
/// 是否沙箱环境
private Boolean sandbox;
}

View File

@@ -0,0 +1,15 @@
package cn.daxpay.open.channel.wechat.client.enums;
/// # 微信支付内容类型
///
/// 与子应用 dax-pay-channel-one 的 `WechatPayBodyType` 镜像, 跨 HTTP 传输时按枚举名(name)序列化对齐。
public enum WechatPayBodyType {
/// 跳转链接(H5 场景)
LINK,
/// 二维码内容(NATIVE 扫码场景)
QR_CODE,
/// 调起参数 JSON(JSAPI / 小程序场景)
IDENTIFIER,
/// APP 调起参数 JSON(APP 场景)
APP_ORDER_STR;
}

View File

@@ -0,0 +1,19 @@
package cn.daxpay.open.channel.wechat.client.enums;
/// # 微信支付方式
///
/// 与子应用 dax-pay-channel-one 的 `WechatPayMethod` 镜像, 跨 HTTP 传输时按枚举名(name)序列化对齐。
public enum WechatPayMethod {
/// 扫码支付(NATIVE)
NATIVE,
/// 公众号支付(JSAPI)
JSAPI,
/// 小程序支付(JSAPI 接口)
MINI,
/// APP 支付
APP,
/// H5 支付
H5,
/// 付款码支付(V2 micropay)
MICROPAY;
}

View File

@@ -0,0 +1,17 @@
package cn.daxpay.open.channel.wechat.client.req;
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
import lombok.Data;
/// # 微信通道关闭请求
///
/// 与子应用 dax-pay-channel-one 的 `WechatCloseReq` 镜像, 字段对齐。
@Data
public class WechatCloseReq {
/// 商户订单号(主应用支付交易号)
private String outTradeNo;
/// 微信支付订单号(transaction_id, 可选)
private String transactionId;
/// 通道调用凭证
private WechatSdkCredential credential;
}

View File

@@ -0,0 +1,41 @@
package cn.daxpay.open.channel.wechat.client.req;
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
import cn.daxpay.open.channel.wechat.client.enums.WechatPayMethod;
import lombok.Data;
import java.time.OffsetDateTime;
/// # 微信通道支付请求
///
/// 与子应用 dax-pay-channel-one 的 `WechatPayReq` 镜像, 经声明式 HTTP 客户端
/// [cn.daxpay.open.channel.wechat.client.WechatChannelClient] 序列化传输, 字段对齐。
@Data
public class WechatPayReq {
/// 商户订单号(主应用支付交易号, 作为微信 out_trade_no)
private String outTradeNo;
/// 订单金额(单位: 分)
private Long amount;
/// 商品描述(对应微信 description)
private String description;
/// 支付方式
private WechatPayMethod method;
/// 订单过期时间
private OffsetDateTime expireTime;
/// 异步通知地址
private String notifyUrl;
/// 附加数据(对应微信 attach)
private String attach;
/// 用户标识(JSAPI / MINI 必填)
private String openId;
/// 付款码(MICROPAY 必填)
private String authCode;
/// 用户终端IP(H5 场景必填)
private String payerClientIp;
/// H5 场景 wap_url
private String wapUrl;
/// H5 场景 wap_name
private String wapName;
/// 通道调用凭证
private WechatSdkCredential credential;
}

View File

@@ -0,0 +1,27 @@
package cn.daxpay.open.channel.wechat.client.req;
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
import lombok.Data;
/// # 微信通道退款请求
///
/// 与子应用 dax-pay-channel-one 的 `WechatRefundReq` 镜像, 字段对齐。
@Data
public class WechatRefundReq {
/// 商户订单号(主应用支付交易号)
private String outTradeNo;
/// 微信支付订单号(transaction_id, 优先使用)
private String transactionId;
/// 退款单号(对应微信 out_refund_no)
private String outRefundNo;
/// 原订单总金额(单位: 分)
private Long totalAmount;
/// 退款金额(单位: 分)
private Long refundAmount;
/// 退款原因(对应微信 reason)
private String reason;
/// 退款异步通知地址(可选)
private String notifyUrl;
/// 通道调用凭证
private WechatSdkCredential credential;
}

View File

@@ -0,0 +1,15 @@
package cn.daxpay.open.channel.wechat.client.req;
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
import lombok.Data;
/// # 微信通道退款同步请求
///
/// 与子应用 dax-pay-channel-one 的 `WechatRefundSyncReq` 镜像, 字段对齐。
@Data
public class WechatRefundSyncReq {
/// 退款单号(对应微信 out_refund_no)
private String outRefundNo;
/// 通道调用凭证
private WechatSdkCredential credential;
}

View File

@@ -0,0 +1,17 @@
package cn.daxpay.open.channel.wechat.client.req;
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
import lombok.Data;
/// # 微信通道同步请求
///
/// 与子应用 dax-pay-channel-one 的 `WechatSyncReq` 镜像, 字段对齐。
@Data
public class WechatSyncReq {
/// 商户订单号(主应用支付交易号)
private String outTradeNo;
/// 微信支付订单号(transaction_id, 可选)
private String transactionId;
/// 通道调用凭证
private WechatSdkCredential credential;
}

View File

@@ -0,0 +1,14 @@
package cn.daxpay.open.channel.wechat.client.resp;
import lombok.Data;
/// # 微信通道关闭响应
///
/// 与子应用 dax-pay-channel-one 的 `WechatCloseResp` 镜像, 字段对齐。
@Data
public class WechatCloseResp {
/// 商户订单号(透传 WechatCloseReq.outTradeNo)
private String outTradeNo;
/// 微信支付订单号(transaction_id)
private String transactionId;
}

View File

@@ -0,0 +1,31 @@
package cn.daxpay.open.channel.wechat.client.resp;
import cn.daxpay.open.channel.wechat.client.enums.WechatPayBodyType;
import lombok.Data;
import java.time.OffsetDateTime;
/// # 微信通道支付响应
///
/// 与子应用 dax-pay-channel-one 的 `WechatPayResp` 镜像, 字段对齐。
@Data
public class WechatPayResp {
/// 商户订单号(透传 WechatPayReq.outTradeNo)
private String outTradeNo;
/// 微信支付订单号(transaction_id)
private String transactionId;
/// 支付内容(H5 跳转链接 / 二维码内容 / 调起参数 JSON)
private String payBody;
/// 支付内容类型
private WechatPayBodyType payBodyType;
/// 是否已终态完成(MICROPAY 付款码同步成功时为 true)
private Boolean complete;
/// 完成时间
private OffsetDateTime finishTime;
/// 订单总金额(单位: 分)
private Long totalAmount;
/// 用户支付金额(单位: 分)
private Long payerTotal;
/// 用户标识(openid)
private String openId;
}

View File

@@ -0,0 +1,30 @@
package cn.daxpay.open.channel.wechat.client.resp;
import lombok.Data;
import java.time.OffsetDateTime;
/// # 微信通道退款响应
///
/// 与子应用 dax-pay-channel-one 的 `WechatRefundResp` 镜像, 字段对齐。
@Data
public class WechatRefundResp {
/// 商户订单号
private String outTradeNo;
/// 微信支付订单号(transaction_id)
private String transactionId;
/// 退款单号(透传 WechatRefundReq.outRefundNo)
private String outRefundNo;
/// 微信退款单号(refund_id)
private String refundId;
/// 退款状态(SUCCESS / CLOSED / PROCESSING / ABNORMAL)
private String status;
/// 是否已终态完成(SUCCESS / CLOSED 为 true)
private Boolean complete;
/// 退款完成时间
private OffsetDateTime finishTime;
/// 退款金额(单位: 分)
private Long refundAmount;
/// 用户退款金额(单位: 分)
private Long payerRefund;
}

View File

@@ -0,0 +1,28 @@
package cn.daxpay.open.channel.wechat.client.resp;
import lombok.Data;
import java.time.OffsetDateTime;
/// # 微信通道退款同步响应
///
/// 与子应用 dax-pay-channel-one 的 `WechatRefundSyncResp` 镜像, 字段对齐。
@Data
public class WechatRefundSyncResp {
/// 退款状态(SUCCESS / CLOSED / PROCESSING / ABNORMAL)
private String status;
/// 微信退款单号(refund_id)
private String refundId;
/// 退款单号(out_refund_no)
private String outRefundNo;
/// 微信支付订单号(transaction_id)
private String transactionId;
/// 商户订单号(out_trade_no)
private String outTradeNo;
/// 退款完成时间(success_time)
private OffsetDateTime finishTime;
/// 退款金额(单位: 分)
private Long refundAmount;
/// 用户退款金额(单位: 分)
private Long payerRefund;
}

View File

@@ -0,0 +1,28 @@
package cn.daxpay.open.channel.wechat.client.resp;
import lombok.Data;
import java.time.OffsetDateTime;
/// # 微信通道同步响应
///
/// 与子应用 dax-pay-channel-one 的 `WechatSyncResp` 镜像, 字段对齐。
@Data
public class WechatSyncResp {
/// 交易状态(SUCCESS / REFUND / NOTPAY / CLOSED / REVOKED / USERPAYING / PAYERROR / ACCEPT)
private String tradeState;
/// 交易状态描述
private String tradeStateDesc;
/// 微信支付订单号(transaction_id)
private String transactionId;
/// 商户订单号(out_trade_no)
private String outTradeNo;
/// 支付完成时间(success_time)
private OffsetDateTime successTime;
/// 订单总金额(单位: 分)
private Long totalAmount;
/// 用户支付金额(单位: 分)
private Long payerTotal;
/// 用户标识(openid)
private String openId;
}

View File

@@ -0,0 +1,34 @@
package cn.daxpay.open.channel.wechat.config;
import cn.daxpay.open.channel.wechat.client.WechatChannelClient;
import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
///
/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装:
/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池),
/// 经 `mutate()` 派生出绑定 channel-one baseUrl 的实例。
///
/// baseUrl 从 [DaxpayChannelProperties] 统一读取。
@Configuration
public class WechatClientConfig {
@Bean
public WechatChannelClient wechatChannelClient(
RestClient restClient,
DaxpayChannelProperties channelProperties) {
// mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl
RestClient channelClient = restClient
.mutate()
.baseUrl(channelProperties.getOne().getBaseUrl())
.build();
return HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(channelClient))
.build()
.createClient(WechatChannelClient.class);
}
}

View File

@@ -0,0 +1,76 @@
package cn.daxpay.open.channel.wechat.service.direct;
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
import cn.daxpay.open.channel.wechat.dao.direct.WechatDirectAppManager;
import cn.daxpay.open.channel.wechat.entity.direct.WechatDirectApp;
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;
import org.springframework.stereotype.Service;
/// # 微信直连通道凭证组装器
///
/// 从进件对象([WechatDirectApp] + [WechatDirectKeyConfig])读取密钥/证书,
/// 组装为下发给子应用的通道调用凭证 [WechatSdkCredential]。
///
/// 应用解析优先级(委托 [WechatDirectAppCapabilityService.resolveApp]):
/// 能力关联(显式配置 > appType自动推导) > 通道商户首个应用。
/// 密钥配置按通道商户号维度查询(一个商户号共享一套密钥/证书)。
///
/// 供支付策略([cn.daxpay.open.channel.wechat.strategy.direct.WechatDirectPayStrategy])组装通道调用凭证。
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatDirectConfigAssembler {
private final WechatDirectAppManager wechatDirectAppManager;
private final WechatDirectKeyConfigService wechatDirectKeyConfigService;
private final WechatDirectAppCapabilityService wechatDirectAppCapabilityService;
private final PayProductConfigManager payProductConfigManager;
/// 组装直连商户的通道调用凭证(下发给子应用)
///
/// @param mchNo 商户号(兜底定位应用)
/// @param channelMchNo 通道商户号(微信商户号, 密钥查询与应用定位主键)
/// @param capability 支付能力编码(用于选择匹配的应用)
/// @return 微信 SDK 凭证, 字段对齐子应用 WechatSdkCredential
public WechatSdkCredential buildConfig(String mchNo, String channelMchNo, String capability) {
WechatDirectApp app = resolveApp(mchNo, channelMchNo, capability);
WechatDirectKeyConfig keyConfig = wechatDirectKeyConfigService.findByChannelMchNo(channelMchNo);
WechatSdkCredential credential = new WechatSdkCredential();
// 微信商户号 = 通道商户号
credential.setWxMchId(app.getChannelMchNo());
credential.setWxAppId(app.getWxAppId());
// 密钥与证书
credential.setApiKeyV3(keyConfig.getApiKeyV3());
credential.setPrivateKey(keyConfig.getPrivateKey());
credential.setPrivateCert(keyConfig.getPrivateCert());
credential.setCertSerialNo(keyConfig.getCertSerialNo());
// 支付公钥新模式(为空则子应用走平台证书模式)
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;
}
/// 解析支付使用的应用
///
/// 优先级(委托 [WechatDirectAppCapabilityService.resolveApp]):
/// 能力关联(显式配置 > appType自动推导) > 通道商户首个应用
private WechatDirectApp resolveApp(String mchNo, String channelMchNo, String capability) {
return wechatDirectAppCapabilityService.resolveApp(channelMchNo, capability)
// 兜底: 按通道商户号取首个应用
.or(() -> wechatDirectAppManager.findFirstByChannelMchNo(channelMchNo))
// 微信: 直连商户应用不存在
.orElseThrow(() -> new DataNotExistException("error.channel.wechat.mchAppNotFound"));
}
}

View File

@@ -0,0 +1,48 @@
package cn.daxpay.open.channel.wechat.service.payment.close;
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.enums.pay.pay.CloseTypeEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/// # 微信支付关闭业务服务
///
/// 通过 [WechatChannelClient] 调用子应用 dax-pay-channel-one 关闭微信订单。
/// 微信 V3 仅提供关单接口(closeOrderV3), 无撤销接口(useCancel 参数对微信无意义, 统一返回 [CloseTypeEnum.CLOSE]);
/// 关闭失败的网关状态兜底(订单已关闭/不存在)由子应用内部处理。
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatCloseService {
private final WechatChannelClient wechatChannelClient;
/// 执行微信订单关闭
///
/// @param trade 支付订单(tradeNo 作为 out_trade_no, outOrderNo 作为 transaction_id)
/// @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.close(req);
if (result.getCode() != 0) {
throw new IllegalStateException("微信通道关闭失败: " + result.getMsg());
}
// 微信仅支持关闭, 无撤销
return CloseTypeEnum.CLOSE;
}
}

View File

@@ -0,0 +1,110 @@
package cn.daxpay.open.channel.wechat.service.payment.pay;
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.enums.pay.channel.PayMethodEnum;
import cn.daxpay.open.platform.core.enums.unipay.PayBodyTypeEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/// # 微信支付执行业务服务
///
/// 通过 [WechatChannelClient] 调用子应用 dax-pay-channel-one 完成微信支付下单。
/// 请求构建、响应解析全部在本类中完成。
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatPayService {
private final WechatChannelClient wechatChannelClient;
/// 执行微信支付
///
/// @param order 支付订单
/// @param payParam 支付参数
/// @param credential 通道调用凭证(密钥/证书)
/// @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());
// 将平台支付方式(PayMethodEnum code)映射为微信通道支付方式
req.setMethod(mapMethod(payParam.getMethod()));
// 付款码(MICROPAY) / 买家标识(JSAPI/MINI) 通道专属参数透传
req.setAuthCode(payParam.getAuthCode());
req.setOpenId(payParam.getOpenId());
req.setAttach(payParam.getAttach());
req.setNotifyUrl(payParam.getNotifyUrl());
req.setExpireTime(payParam.getExpiredTime());
// H5 场景信息(场景参数)
if (req.getMethod() == WechatPayMethod.H5) {
req.setWapUrl(payParam.getReturnUrl());
req.setWapName(payParam.getTitle());
}
req.setCredential(credential);
// 调用子应用
DaxResult<WechatPayResp> result = wechatChannelClient.pay(req);
if (result.getCode() != 0) {
throw new IllegalStateException("微信通道支付失败: " + result.getMsg());
}
return toPayResult(result.getData());
}
/// 平台支付方式([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 UnsupportedOperationException(
"暂不支持的微信支付方式: " + 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,83 @@
package cn.daxpay.open.channel.wechat.service.payment.refund;
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 发起微信退款。
/// 请求构建、结果判定在本类中完成。
///
/// 退款状态 status 判定:
/// - SUCCESS / CLOSED → 终态(complete=true), 退款成功
/// - PROCESSING / ABNORMAL → 未终态(complete=false), 需退款同步查询确认最终状态
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatRefundService {
private final WechatChannelClient wechatChannelClient;
/// 微信退款状态
private static final String STATUS_SUCCESS = "SUCCESS";
private static final String STATUS_CLOSED = "CLOSED";
/// 执行微信退款
///
/// @param refundOrder 退款订单(refundNo 作为 out_refund_no, orderNo 作为 out_trade_no, outOrderNo 作为 transaction_id)
/// @param credential 通道调用凭证
/// @return 退款结果(含映射后的退款状态)
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.refund(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,94 @@
package cn.daxpay.open.channel.wechat.service.payment.refund;
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]。
///
/// 映射规则(参照商业版 WechatRefundSyncService):
/// - SUCCESS → SUCCESS(退款成功)
/// - CLOSED → CLOSE(退款关闭)
/// - PROCESSING → PROGRESS(退款中)
/// - ABNORMAL → FAIL(退款异常)
/// - 查询失败 → syncSuccess=false
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatRefundSyncService {
private final WechatChannelClient wechatChannelClient;
/// 微信退款状态
private static final String STATUS_SUCCESS = "SUCCESS";
private static final String STATUS_CLOSED = "CLOSED";
private static final String STATUS_PROCESSING = "PROCESSING";
private static final String STATUS_ABNORMAL = "ABNORMAL";
/// 执行微信退款同步查询
///
/// @param refundOrder 退款订单(refundNo 作为 out_refund_no)
/// @param credential 通道调用凭证
/// @return 同步结果(含映射后的退款状态)
public RefundResultBo sync(PayRefundOrder refundOrder, WechatSdkCredential credential) {
// 构建请求
WechatRefundSyncReq req = new WechatRefundSyncReq();
req.setOutRefundNo(refundOrder.getRefundNo());
req.setCredential(credential);
// 调用子应用
DaxResult<WechatRefundSyncResp> result = wechatChannelClient.refundSync(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,110 @@
package cn.daxpay.open.channel.wechat.service.payment.sync;
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]。
///
/// 映射规则(参照商业版 WechatPaySyncService):
/// - SUCCESS / REFUND → SUCCESS(REFUND 表示原订单已支付, 后续发起退款)
/// - NOTPAY / USERPAYING / ACCEPT → PROCESSING
/// - CLOSED → CLOSE
/// - REVOKED → CANCEL(付款码撤销)
/// - PAYERROR → FAIL
/// - 查询失败 → syncSuccess=false
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatSyncService {
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";
/// 执行微信支付同步
///
/// @param trade 支付订单(tradeNo 作为 out_trade_no, outOrderNo 作为 transaction_id)
/// @param credential 通道调用凭证
/// @return 同步结果(含映射后的资金状态)
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.sync(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

@@ -0,0 +1,50 @@
package cn.daxpay.open.channel.wechat.strategy.direct;
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
import cn.daxpay.open.channel.wechat.service.direct.WechatDirectConfigAssembler;
import cn.daxpay.open.channel.wechat.service.payment.close.WechatCloseService;
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_PAY)下的支付关闭策略。
/// 从上下文容器读取通道路由参数(channelMchNo / capability),
/// 组装通道凭证(委托 [WechatDirectConfigAssembler]), 关闭执行委托给 [WechatCloseService]。
///
/// 注意: 微信 V3 仅提供关单接口, 无撤销接口(useCancel 参数由 service 层忽略)。
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatDirectCloseStrategy extends AbsPayCloseStrategy {
private final WechatCloseService wechatCloseService;
private final WechatDirectConfigAssembler wechatDirectConfigAssembler;
@Override
public ProductEnum getProduct() {
return ProductEnum.WECHAT_PAY;
}
@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 = wechatDirectConfigAssembler.buildConfig(
trade.getMchNo(), channelMchNo, capability);
return wechatCloseService.close(trade, credential, useCancel);
}
}

View File

@@ -0,0 +1,49 @@
package cn.daxpay.open.channel.wechat.strategy.direct;
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
import cn.daxpay.open.channel.wechat.service.direct.WechatDirectConfigAssembler;
import cn.daxpay.open.channel.wechat.service.payment.pay.WechatPayService;
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_PAY)下发起支付的具体执行策略。
/// 配置组装在 [doBeforePay] 阶段完成(委托 [WechatDirectConfigAssembler]), 支付执行委托给 [WechatPayService]。
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatDirectPayStrategy extends AbsNormalPayStrategy {
private final WechatPayService wechatPayService;
private final WechatDirectConfigAssembler wechatDirectConfigAssembler;
@Override
public ProductEnum getProduct() {
return ProductEnum.WECHAT_PAY;
}
/// 支付前预处理: 从容器读取通道路由参数, 组装直连通道凭证写入上下文
/// 配置缺失在此阶段抛异常, fail-fast, 不会进入 doPay
@Override
public void doBeforePay(PayStrategyContext context) {
// 组装直连通道凭证(只依赖请求参数, 订单尚未创建)
NormalPayParam payParam = context.getPayParam();
WechatSdkCredential credential = wechatDirectConfigAssembler.buildConfig(
payParam.getMchNo(), payParam.getChannelMchNo(), payParam.getCapability());
context.setChannelConfig(credential);
}
@Override
public PayTradeResultBo doPay(PayStrategyContext context) {
// 直接使用 doBeforePay 预组装的通道凭证
WechatSdkCredential credential = context.getChannelConfig(WechatSdkCredential.class);
return wechatPayService.pay(context.getTrade(), context.getPayParam(), credential);
}
}

View File

@@ -0,0 +1,39 @@
package cn.daxpay.open.channel.wechat.strategy.direct;
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
import cn.daxpay.open.channel.wechat.service.direct.WechatDirectConfigAssembler;
import cn.daxpay.open.channel.wechat.service.payment.refund.WechatRefundService;
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_PAY)下的退款策略。
/// 从退款订单读取通道路由参数(channelMchNo / capability),
/// 组装通道凭证(委托 [WechatDirectConfigAssembler]), 退款执行委托给 [WechatRefundService]。
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatDirectRefundStrategy extends AbsRefundStrategy {
private final WechatRefundService wechatRefundService;
private final WechatDirectConfigAssembler wechatDirectConfigAssembler;
@Override
public ProductEnum getProduct() {
return ProductEnum.WECHAT_PAY;
}
@Override
public RefundResultBo doRefund(PayRefundOrder refundOrder) {
// 组装直连通道调用凭证
WechatSdkCredential credential = wechatDirectConfigAssembler.buildConfig(
refundOrder.getMchNo(), refundOrder.getChannelMchNo(), refundOrder.getCapability());
return wechatRefundService.refund(refundOrder, credential);
}
}

View File

@@ -0,0 +1,38 @@
package cn.daxpay.open.channel.wechat.strategy.direct;
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
import cn.daxpay.open.channel.wechat.service.direct.WechatDirectConfigAssembler;
import cn.daxpay.open.channel.wechat.service.payment.refund.WechatRefundSyncService;
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_PAY)下的退款同步策略。
/// 组装通道凭证(委托 [WechatDirectConfigAssembler]), 同步执行委托给 [WechatRefundSyncService]。
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatDirectSyncRefundStrategy extends AbsSyncRefundStrategy {
private final WechatRefundSyncService wechatRefundSyncService;
private final WechatDirectConfigAssembler wechatDirectConfigAssembler;
@Override
public ProductEnum getProduct() {
return ProductEnum.WECHAT_PAY;
}
@Override
public RefundResultBo doSync(PayRefundOrder refundOrder) {
// 组装直连通道调用凭证
WechatSdkCredential credential = wechatDirectConfigAssembler.buildConfig(
refundOrder.getMchNo(), refundOrder.getChannelMchNo(), refundOrder.getCapability());
return wechatRefundSyncService.sync(refundOrder, credential);
}
}

View File

@@ -0,0 +1,46 @@
package cn.daxpay.open.channel.wechat.strategy.direct;
import cn.daxpay.open.channel.wechat.client.credential.WechatSdkCredential;
import cn.daxpay.open.channel.wechat.service.direct.WechatDirectConfigAssembler;
import cn.daxpay.open.channel.wechat.service.payment.sync.WechatSyncService;
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_PAY)下的支付同步策略。
/// 从上下文容器读取通道路由参数(channelMchNo / capability),
/// 组装通道凭证(委托 [WechatDirectConfigAssembler]), 同步执行委托给 [WechatSyncService]。
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatDirectSyncStrategy extends AbsSyncPayOrderStrategy {
private final WechatSyncService wechatSyncService;
private final WechatDirectConfigAssembler wechatDirectConfigAssembler;
@Override
public ProductEnum getProduct() {
return ProductEnum.WECHAT_PAY;
}
@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 = wechatDirectConfigAssembler.buildConfig(
context.getTrade().getMchNo(), channelMchNo, capability);
return wechatSyncService.sync(context.getTrade(), credential);
}
}