From 1c5596c26396ad0981817c48f833c3218fa25acf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:12:48 +0000 Subject: [PATCH 1/2] Initial plan From 32779b1440710456d0f2d548dc44320af5fa6333 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:23:08 +0000 Subject: [PATCH 2/2] Update WeChat Pay withdrawal APIs to support notify_url parameter and callback Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../bean/ecommerce/SpWithdrawRequest.java | 17 +- .../bean/ecommerce/SubWithdrawRequest.java | 17 +- .../bean/ecommerce/WithdrawNotifyResult.java | 210 ++++++++++++++++++ .../wxpay/service/EcommerceService.java | 17 +- .../service/impl/EcommerceServiceImpl.java | 21 ++ 5 files changed, 278 insertions(+), 4 deletions(-) create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/WithdrawNotifyResult.java diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SpWithdrawRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SpWithdrawRequest.java index 0b836366d4..a3d70557f0 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SpWithdrawRequest.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SpWithdrawRequest.java @@ -9,7 +9,7 @@ /** * 电商平台提现 *
- * 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/fund/chapter3_5.shtml + * 文档地址:https://pay.weixin.qq.com/doc/v3/partner/4012476670 **/ @Data @@ -88,4 +88,19 @@ public class SpWithdrawRequest implements Serializable { @SerializedName(value = "account_type") private String accountType; + /** + *
+ * 字段名:回调通知地址 + * 变量名:notify_url + * 是否必填:否 + * 类型:string(256) + * 描述: + * 异步接收提现状态变更通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 + * 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效,优先回调当前传的地址。 + * 示例值:https://www.weixin.qq.com/wxpay/pay.php + *+ */ + @SerializedName(value = "notify_url") + private String notifyUrl; + } diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SubWithdrawRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SubWithdrawRequest.java index 3c74db24c9..541d779a30 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SubWithdrawRequest.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SubWithdrawRequest.java @@ -9,7 +9,7 @@ /** * 二级商户账户余额提现 *
- * 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/combine/chapter3_3.shtml + * 文档地址:https://pay.weixin.qq.com/doc/v3/partner/4012476652 **/ @Data @@ -86,4 +86,19 @@ public class SubWithdrawRequest implements Serializable { @SerializedName(value = "bank_memo") private String bankMemo; + /** + *
+ * 字段名:回调通知地址 + * 变量名:notify_url + * 是否必填:否 + * 类型:string(256) + * 描述: + * 异步接收提现状态变更通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 + * 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效,优先回调当前传的地址。 + * 示例值:https://www.weixin.qq.com/wxpay/pay.php + *+ */ + @SerializedName(value = "notify_url") + private String notifyUrl; + } diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/WithdrawNotifyResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/WithdrawNotifyResult.java new file mode 100644 index 0000000000..60dd51f193 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/WithdrawNotifyResult.java @@ -0,0 +1,210 @@ +package com.github.binarywang.wxpay.bean.ecommerce; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 提现状态变更通知结果 + *
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/partner/4013049135 + *+ * + * @author copilot + * created on 2024/12/24 + */ +@Data +@NoArgsConstructor +public class WithdrawNotifyResult implements Serializable { + + private static final long serialVersionUID = -7451351849088368701L; + + /** + * 源数据 + */ + private NotifyResponse rawData; + + /** + *
+ * 字段名:电商平台商户号 + * 变量名:sp_mchid + * 是否必填:是 + * 类型:string(32) + * 描述: + * 微信支付分配给电商平台的商户号 + * 示例值:1900000100 + *+ */ + @SerializedName(value = "sp_mchid") + private String spMchid; + + /** + *
+ * 字段名:二级商户号 + * 变量名:sub_mchid + * 是否必填:否 + * 类型:string(32) + * 描述: + * 微信支付分配给二级商户的商户号,仅二级商户提现时返回 + * 示例值:1900000109 + *+ */ + @SerializedName(value = "sub_mchid") + private String subMchid; + + /** + *
+ * 字段名:商户提现单号 + * 变量名:out_request_no + * 是否必填:是 + * 类型:string(32) + * 描述: + * 商户提现单号,由商户自定义生成 + * 示例值:20190611222222222200000000012122 + *+ */ + @SerializedName(value = "out_request_no") + private String outRequestNo; + + /** + *
+ * 字段名:微信支付提现单号 + * 变量名:withdraw_id + * 是否必填:是 + * 类型:string(32) + * 描述: + * 微信支付提现单号 + * 示例值:12321002198704230011101200 + *+ */ + @SerializedName(value = "withdraw_id") + private String withdrawId; + + /** + *
+ * 字段名:提现状态 + * 变量名:status + * 是否必填:是 + * 类型:string(16) + * 描述: + * 提现状态: + * CREATE_SUCCESS:受理成功 + * SUCCESS:提现成功 + * FAILED:提现失败 + * REFUND:提现退票 + * CLOSE:关单 + * 示例值:SUCCESS + *+ */ + @SerializedName(value = "status") + private String status; + + /** + *
+ * 字段名:提现金额 + * 变量名:amount + * 是否必填:是 + * 类型:int64 + * 描述: + * 提现金额,单位:分(人民币) + * 示例值:100 + *+ */ + @SerializedName(value = "amount") + private Integer amount; + + /** + *
+ * 字段名:提现发起时间 + * 变量名:create_time + * 是否必填:是 + * 类型:string(29) + * 描述: + * 提现发起时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss:sss+TIMEZONE, + * YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss:sss表示时分秒毫秒, + * TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。 + * 例如:2015-05-20T13:29:35.120+08:00表示,北京时间2015年5月20日13点29分35秒 + * 示例值:2018-06-08T10:34:56+08:00 + *+ */ + @SerializedName(value = "create_time") + private String createTime; + + /** + *
+ * 字段名:提现更新时间 + * 变量名:update_time + * 是否必填:是 + * 类型:string(29) + * 描述: + * 提现更新时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss:sss+TIMEZONE, + * YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss:sss表示时分秒毫秒, + * TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。 + * 例如:2015-05-20T13:29:35.120+08:00表示,北京时间2015年5月20日13点29分35秒 + * 示例值:2018-06-08T10:34:56+08:00 + *+ */ + @SerializedName(value = "update_time") + private String updateTime; + + /** + *
+ * 字段名:失败原因 + * 变量名:reason + * 是否必填:否 + * 类型:string(256) + * 描述: + * 提现失败原因,仅在提现失败、退票时有值 + * 示例值:账户余额不足 + *+ */ + @SerializedName(value = "reason") + private String reason; + + /** + *
+ * 字段名:备注 + * 变量名:remark + * 是否必填:否 + * 类型:string(256) + * 描述: + * 商户对提现单的备注,若提现申请时未传递,则无此字段 + * 示例值:交易提现 + *+ */ + @SerializedName(value = "remark") + private String remark; + + /** + *
+ * 字段名:银行附言 + * 变量名:bank_memo + * 是否必填:否 + * 类型:string(256) + * 描述: + * 展示在收款银行系统中的附言,若提现申请时未传递,则无此字段 + * 示例值:微信支付提现 + *+ */ + @SerializedName(value = "bank_memo") + private String bankMemo; + + /** + *
+ * 字段名:账户类型 + * 变量名:account_type + * 是否必填:否 + * 类型:string(16) + * 描述: + * 提现账户类型,仅电商平台提现时返回: + * BASIC:基本账户 + * OPERATION:运营账户 + * FEES:手续费账户 + * 示例值:BASIC + *+ */ + @SerializedName(value = "account_type") + private String accountType; +} \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/EcommerceService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/EcommerceService.java index 2dbb2906c3..00d82e85cc 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/EcommerceService.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/EcommerceService.java @@ -417,10 +417,23 @@ public interface EcommerceService { */ RefundNotifyResult parseRefundNotifyResult(String notifyData, SignatureHeader header) throws WxPayException; + /** + *
+ * 提现状态变更通知回调数据处理 + * 文档地址: https://pay.weixin.qq.com/doc/v3/partner/4013049135 + *+ * + * @param notifyData 通知数据 + * @param header 通知头部数据,不传则表示不校验头 + * @return 解密后通知数据 withdraw notify result + * @throws WxPayException the wx pay exception + */ + WithdrawNotifyResult parseWithdrawNotifyResult(String notifyData, SignatureHeader header) throws WxPayException; + /** *
* 二级商户账户余额提现API - * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/fund/chapter3_2.shtml + * 文档地址: https://pay.weixin.qq.com/doc/v3/partner/4012476652 ** * @param request 提现请求 @@ -432,7 +445,7 @@ public interface EcommerceService { /** *
* 电商平台提现API - * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/fund/chapter3_5.shtml + * 文档地址: https://pay.weixin.qq.com/doc/v3/partner/4012476670 ** * @param request 提现请求 diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/EcommerceServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/EcommerceServiceImpl.java index 479520d7f7..8be790db4e 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/EcommerceServiceImpl.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/EcommerceServiceImpl.java @@ -337,6 +337,27 @@ public RefundNotifyResult parseRefundNotifyResult(String notifyData, SignatureHe } } + @Override + public WithdrawNotifyResult parseWithdrawNotifyResult(String notifyData, SignatureHeader header) throws WxPayException { + if (Objects.nonNull(header) && !this.verifyNotifySign(header, notifyData)) { + throw new WxPayException("非法请求,头部信息验证失败"); + } + NotifyResponse response = GSON.fromJson(notifyData, NotifyResponse.class); + NotifyResponse.Resource resource = response.getResource(); + String cipherText = resource.getCiphertext(); + String associatedData = resource.getAssociatedData(); + String nonce = resource.getNonce(); + String apiV3Key = this.payService.getConfig().getApiV3Key(); + try { + String result = AesUtils.decryptToString(associatedData, nonce, cipherText, apiV3Key); + WithdrawNotifyResult notifyResult = GSON.fromJson(result, WithdrawNotifyResult.class); + notifyResult.setRawData(response); + return notifyResult; + } catch (GeneralSecurityException | IOException e) { + throw new WxPayException("解析报文异常!", e); + } + } + @Override public SubWithdrawResult subWithdraw(SubWithdrawRequest request) throws WxPayException { String url = String.format("%s/v3/ecommerce/fund/withdraw", this.payService.getPayBaseUrl());