diff --git a/_config/sql/iam_perm_menu.sql b/_config/sql/iam_perm_menu.sql index e78fcbfbc..f93009fda 100644 --- a/_config/sql/iam_perm_menu.sql +++ b/_config/sql/iam_perm_menu.sql @@ -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 diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/WechatChannelClient.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/WechatChannelClient.java new file mode 100644 index 000000000..f8073f87c --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/WechatChannelClient.java @@ -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 pay(@RequestBody WechatPayReq req); + + /// 支付同步(查询微信订单状态) + @PostExchange("/channel/wechat/sync") + DaxResult sync(@RequestBody WechatSyncReq req); + + /// 关闭微信订单 + @PostExchange("/channel/wechat/close") + DaxResult close(@RequestBody WechatCloseReq req); + + /// 退款 + @PostExchange("/channel/wechat/refund") + DaxResult refund(@RequestBody WechatRefundReq req); + + /// 退款同步(查询退款状态) + @PostExchange("/channel/wechat/refund-sync") + DaxResult refundSync(@RequestBody WechatRefundSyncReq req); +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/credential/WechatSdkCredential.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/credential/WechatSdkCredential.java new file mode 100644 index 000000000..e6cce4666 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/credential/WechatSdkCredential.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/enums/WechatPayBodyType.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/enums/WechatPayBodyType.java new file mode 100644 index 000000000..aeb953452 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/enums/WechatPayBodyType.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/enums/WechatPayMethod.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/enums/WechatPayMethod.java new file mode 100644 index 000000000..52fec7bed --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/enums/WechatPayMethod.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatCloseReq.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatCloseReq.java new file mode 100644 index 000000000..84e5405d8 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatCloseReq.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatPayReq.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatPayReq.java new file mode 100644 index 000000000..6feeac8cf --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatPayReq.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatRefundReq.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatRefundReq.java new file mode 100644 index 000000000..fe1b1d7f0 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatRefundReq.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatRefundSyncReq.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatRefundSyncReq.java new file mode 100644 index 000000000..119f49a1c --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatRefundSyncReq.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatSyncReq.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatSyncReq.java new file mode 100644 index 000000000..feb76d33d --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/req/WechatSyncReq.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatCloseResp.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatCloseResp.java new file mode 100644 index 000000000..eb05da2a2 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatCloseResp.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatPayResp.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatPayResp.java new file mode 100644 index 000000000..773a5fc21 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatPayResp.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatRefundResp.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatRefundResp.java new file mode 100644 index 000000000..a12d2a142 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatRefundResp.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatRefundSyncResp.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatRefundSyncResp.java new file mode 100644 index 000000000..2415e4a0f --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatRefundSyncResp.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatSyncResp.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatSyncResp.java new file mode 100644 index 000000000..027236aa1 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/client/resp/WechatSyncResp.java @@ -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; +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/config/WechatClientConfig.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/config/WechatClientConfig.java new file mode 100644 index 000000000..5ed82c7f6 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/config/WechatClientConfig.java @@ -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); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/direct/WechatDirectConfigAssembler.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/direct/WechatDirectConfigAssembler.java new file mode 100644 index 000000000..ac7942a7f --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/direct/WechatDirectConfigAssembler.java @@ -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")); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/close/WechatCloseService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/close/WechatCloseService.java new file mode 100644 index 000000000..fd81d422f --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/close/WechatCloseService.java @@ -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 result = wechatChannelClient.close(req); + if (result.getCode() != 0) { + throw new IllegalStateException("微信通道关闭失败: " + result.getMsg()); + } + + // 微信仅支持关闭, 无撤销 + return CloseTypeEnum.CLOSE; + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/pay/WechatPayService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/pay/WechatPayService.java new file mode 100644 index 000000000..bdeb8b1f3 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/pay/WechatPayService.java @@ -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 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; + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/refund/WechatRefundService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/refund/WechatRefundService.java new file mode 100644 index 000000000..acbaf9094 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/refund/WechatRefundService.java @@ -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 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; + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/refund/WechatRefundSyncService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/refund/WechatRefundSyncService.java new file mode 100644 index 000000000..f76d41f3b --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/refund/WechatRefundSyncService.java @@ -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 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); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/sync/WechatSyncService.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/sync/WechatSyncService.java new file mode 100644 index 000000000..937f389b7 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/service/payment/sync/WechatSyncService.java @@ -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 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)); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectCloseStrategy.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectCloseStrategy.java new file mode 100644 index 000000000..068c58ff3 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectCloseStrategy.java @@ -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); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectPayStrategy.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectPayStrategy.java new file mode 100644 index 000000000..7cf208cf3 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectPayStrategy.java @@ -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); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectRefundStrategy.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectRefundStrategy.java new file mode 100644 index 000000000..be764b9b0 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectRefundStrategy.java @@ -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); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectSyncRefundStrategy.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectSyncRefundStrategy.java new file mode 100644 index 000000000..e559ca7f5 --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectSyncRefundStrategy.java @@ -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); + } +} diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectSyncStrategy.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectSyncStrategy.java new file mode 100644 index 000000000..8be45f48d --- /dev/null +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/strategy/direct/WechatDirectSyncStrategy.java @@ -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); + } +}