mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-10 14:56:06 +08:00
feat(payment): 微信直连渠道对接(退款/支付/关闭/同步) + 退款菜单
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user