map) {
+ map.put("body", body);
+ map.put("out_trade_no", outTradeNo);
+ map.put("total_fee", totalFee.toString());
+ map.put("spbill_create_ip", spbillCreateIp);
+ map.put("notify_url", notifyUrl);
+ map.put("trade_type", tradeType);
+ if (openid != null) {
+ map.put("openid", openid);
+ }
+ if (detail != null) {
+ map.put("detail", detail);
+ }
+ if (attach != null) {
+ map.put("attach", attach);
+ }
+ if (feeType != null) {
+ map.put("fee_type", feeType);
+ }
+ if (timeStart != null) {
+ map.put("time_start", timeStart);
+ }
+ if (timeExpire != null) {
+ map.put("time_expire", timeExpire);
+ }
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayCodepayRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayCodepayRequest.java
index ecfa614a16..632561075a 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayCodepayRequest.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayCodepayRequest.java
@@ -45,6 +45,58 @@ public class WxPayCodepayRequest implements Serializable {
*/
@SerializedName(value = "mchid")
protected String mchid;
+ /**
+ *
+ * 字段名:服务商应用ID
+ * 变量名:sp_appid
+ * 是否必填:否
+ * 类型:string[1,32]
+ * 描述:
+ * 服务商模式下使用,由微信生成的应用ID,全局唯一。
+ * 示例值:wxd678efh567hg6787
+ *
+ */
+ @SerializedName(value = "sp_appid")
+ protected String spAppid;
+ /**
+ *
+ * 字段名:服务商商户号
+ * 变量名:sp_mchid
+ * 是否必填:否
+ * 类型:string[1,32]
+ * 描述:
+ * 服务商模式下使用,服务商商户号,由微信支付生成并下发。
+ * 示例值:1230000109
+ *
+ */
+ @SerializedName(value = "sp_mchid")
+ protected String spMchid;
+ /**
+ *
+ * 字段名:子商户应用ID
+ * 变量名:sub_appid
+ * 是否必填:否
+ * 类型:string[1,32]
+ * 描述:
+ * 服务商模式下使用,由微信生成的应用ID,全局唯一。
+ * 示例值:wxd678efh567hg6787
+ *
+ */
+ @SerializedName(value = "sub_appid")
+ protected String subAppid;
+ /**
+ *
+ * 字段名:子商户商户号
+ * 变量名:sub_mchid
+ * 是否必填:否
+ * 类型:string[1,32]
+ * 描述:
+ * 服务商模式下使用,子商户商户号,由微信支付生成并下发。
+ * 示例值:1230000109
+ *
+ */
+ @SerializedName(value = "sub_mchid")
+ protected String subMchid;
/**
*
* 字段名:商品描述
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3Request.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3Request.java
index 8f3e8ebd10..59e2968936 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3Request.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3Request.java
@@ -11,9 +11,6 @@
* 微信支付服务商退款请求
* 文档见:https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_1_9.shtml
*
- * @author Pursuer
- * @version 1.0
- * @date 2023/3/2
*/
@Data
@NoArgsConstructor
@@ -22,20 +19,28 @@ public class WxPayPartnerRefundV3Request extends WxPayRefundV3Request implements
private static final long serialVersionUID = -1L;
/**
*
- * 字段名:退款资金来源
- * 变量名:funds_account
+ * 字段名:服务商应用ID
+ * 变量名:sp_appid
+ * 是否必填:是
+ * 类型:string[1, 32]
+ * 描述:
+ * 服务商申请的公众号或移动应用appid。
+ * 示例值:wx8888888888888888
+ *
+ */
+ @SerializedName(value = "sp_appid")
+ private String spAppid;
+ /**
+ *
+ * 字段名:子商户应用ID
+ * 变量名:sub_appid
* 是否必填:否
* 类型:string[1, 32]
* 描述:
- * 若传递此参数则使用对应的资金账户退款,否则默认使用未结算资金退款(仅对老资金流商户适用)
- * 示例值:
- * UNSETTLED : 未结算资金
- * AVAILABLE : 可用余额
- * UNAVAILABLE : 不可用余额
- * OPERATION : 运营户
- * BASIC : 基本账户(含可用余额和不可用余额)
+ * 子商户申请的公众号或移动应用appid。如果传了sub_appid,那sub_appid对应的订单必须存在。
+ * 示例值:wx8888888888888888
*
*/
- @SerializedName(value = "funds_account")
- private String fundsAccount;
+ @SerializedName(value = "sub_appid")
+ private String subAppid;
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerUnifiedOrderV3Request.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerUnifiedOrderV3Request.java
index b121170c31..a548f30deb 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerUnifiedOrderV3Request.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerUnifiedOrderV3Request.java
@@ -606,5 +606,19 @@ public static class SettleInfo implements Serializable {
*/
@SerializedName(value = "profit_sharing")
private Boolean profitSharing;
+ /**
+ *
+ * 字段名:补差金额
+ * 变量名:subsidy_amount
+ * 是否必填:否
+ * 类型:int64
+ * 描述:
+ * SettleInfo.profit_sharing为true时,该金额才生效。
+ * 注意:单笔订单最高补差金额为5000元
+ * 示例值:10
+ *
+ */
+ @SerializedName(value = "subsidy_amount")
+ private Integer subsidyAmount;
}
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayRefundRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayRefundRequest.java
index e145644d91..b0cbcf4e70 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayRefundRequest.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayRefundRequest.java
@@ -230,7 +230,9 @@ public void checkAndSign(WxPayConfig config) throws WxPayException {
if (StringUtils.isBlank(this.getOpUserId())) {
this.setOpUserId(config.getMchId());
}
-
+ if (StringUtils.isBlank(this.getNotifyUrl())) {
+ this.setNotifyUrl(config.getRefundNotifyUrl());
+ }
super.checkAndSign(config);
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayRefundV3Request.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayRefundV3Request.java
index e9f1f3b140..e1bba3d266 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayRefundV3Request.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayRefundV3Request.java
@@ -84,6 +84,24 @@ public class WxPayRefundV3Request implements Serializable {
*/
@SerializedName(value = "notify_url")
private String notifyUrl;
+ /**
+ *
+ * 字段名:退款资金来源
+ * 变量名:funds_account
+ * 是否必填:否
+ * 类型:string[1, 32]
+ * 描述:
+ * 若传递此参数则使用对应的资金账户退款,否则默认使用未结算资金退款(仅对老资金流商户适用)
+ * 示例值:
+ * UNSETTLED : 未结算资金
+ * AVAILABLE : 可用余额
+ * UNAVAILABLE : 不可用余额
+ * OPERATION : 运营户
+ * BASIC : 基本账户(含可用余额和不可用余额)
+ *
+ */
+ @SerializedName(value = "funds_account")
+ private String fundsAccount;
/**
*
* 字段名:订单金额
@@ -152,6 +170,53 @@ public static class Amount implements Serializable {
*/
@SerializedName(value = "currency")
private String currency;
+ /**
+ *
+ * 字段名:退款出资账户及金额
+ * 变量名:from
+ * 是否必填:否
+ * 类型:array
+ * 描述:
+ * 退款出资的账户类型及金额信息
+ *
+ */
+ @SerializedName(value = "from")
+ private List from;
+ }
+
+ @Data
+ @NoArgsConstructor
+ public static class From implements Serializable {
+ private static final long serialVersionUID = 1L;
+ /**
+ *
+ * 字段名:出资账户类型
+ * 变量名:account
+ * 是否必填:是
+ * 类型:string[1, 32]
+ * 描述:
+ * 下面枚举值多选一。
+ * 枚举值:
+ * AVAILABLE : 可用余额
+ * UNAVAILABLE : 不可用余额
+ * 示例值:AVAILABLE
+ *
+ */
+ @SerializedName(value = "account")
+ private String account;
+ /**
+ *
+ * 字段名:出资金额
+ * 变量名:amount
+ * 是否必填:是
+ * 类型:int
+ * 描述:
+ * 对应账户出资金额
+ * 示例值:444
+ *
+ */
+ @SerializedName(value = "amount")
+ private Integer amount;
}
@Data
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/CombineTransactionsResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/CombineTransactionsResult.java
index 34512a4d05..2ff718b81a 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/CombineTransactionsResult.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/CombineTransactionsResult.java
@@ -73,6 +73,10 @@ public static class JsapiResult implements Serializable {
private String appId;
private String timeStamp;
private String nonceStr;
+ /**
+ * 由于package为java保留关键字,因此改为packageValue,序列化时会自动转换为package字段名
+ */
+ @SerializedName("package")
private String packageValue;
private String signType;
private String paySign;
@@ -89,6 +93,10 @@ public static class AppResult implements Serializable {
private String appid;
private String partnerid;
private String prepayid;
+ /**
+ * 由于package为java保留关键字,因此改为packageValue,序列化时会自动转换为package字段名
+ */
+ @SerializedName("package")
private String packageValue;
private String noncestr;
private String timestamp;
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositConsumeResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositConsumeResult.java
new file mode 100644
index 0000000000..dfb1bd3e69
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositConsumeResult.java
@@ -0,0 +1,103 @@
+package com.github.binarywang.wxpay.bean.result;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.w3c.dom.Document;
+
+import java.io.Serializable;
+
+/**
+ *
+ * 押金消费结果
+ * 详见:https://pay.weixin.qq.com/wiki/doc/api/deposit_sl.php?chapter=27_7&index=4
+ *
+ *
+ * @author Binary Wang
+ * created on 2024-09-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@XStreamAlias("xml")
+public class WxDepositConsumeResult extends BaseWxPayResult implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 微信订单号
+ * transaction_id
+ * 是
+ * String(32)
+ * 1217752501201407033233368018
+ * 微信支付押金订单号
+ *
+ */
+ @XStreamAlias("transaction_id")
+ private String transactionId;
+
+ /**
+ *
+ * 商户消费单号
+ * out_trade_no
+ * 是
+ * String(32)
+ * 20150806125346
+ * 商户系统内部的消费单号
+ *
+ */
+ @XStreamAlias("out_trade_no")
+ private String outTradeNo;
+
+ /**
+ *
+ * 消费金额
+ * consume_fee
+ * 是
+ * Int
+ * 88
+ * 本次消费的金额,单位为分
+ *
+ */
+ @XStreamAlias("consume_fee")
+ private Integer consumeFee;
+
+ /**
+ *
+ * 剩余押金
+ * remain_fee
+ * 是
+ * Int
+ * 11
+ * 剩余押金金额,单位为分
+ *
+ */
+ @XStreamAlias("remain_fee")
+ private Integer remainFee;
+
+ /**
+ *
+ * 消费时间
+ * time_end
+ * 是
+ * String(14)
+ * 20141030133525
+ * 消费完成时间,格式为yyyyMMddHHmmss
+ *
+ */
+ @XStreamAlias("time_end")
+ private String timeEnd;
+
+ @Override
+ protected void loadXml(Document d) {
+ transactionId = readXmlString(d, "transaction_id");
+ outTradeNo = readXmlString(d, "out_trade_no");
+ consumeFee = readXmlInteger(d, "consume_fee");
+ remainFee = readXmlInteger(d, "remain_fee");
+ timeEnd = readXmlString(d, "time_end");
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositOrderQueryResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositOrderQueryResult.java
new file mode 100644
index 0000000000..66a5acb658
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositOrderQueryResult.java
@@ -0,0 +1,152 @@
+package com.github.binarywang.wxpay.bean.result;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.w3c.dom.Document;
+
+import java.io.Serializable;
+
+/**
+ *
+ * 查询押金订单结果
+ * 详见:https://pay.weixin.qq.com/wiki/doc/api/deposit_sl.php?chapter=27_7&index=3
+ *
+ *
+ * @author Binary Wang
+ * created on 2024-09-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@XStreamAlias("xml")
+public class WxDepositOrderQueryResult extends BaseWxPayResult implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 微信订单号
+ * transaction_id
+ * 是
+ * String(32)
+ * 1217752501201407033233368018
+ * 微信支付订单号
+ *
+ */
+ @XStreamAlias("transaction_id")
+ private String transactionId;
+
+ /**
+ *
+ * 商户订单号
+ * out_trade_no
+ * 是
+ * String(32)
+ * 20150806125346
+ * 商户系统内部订单号
+ *
+ */
+ @XStreamAlias("out_trade_no")
+ private String outTradeNo;
+
+ /**
+ *
+ * 交易状态
+ * trade_state
+ * 是
+ * String(32)
+ * SUCCESS
+ * 交易状态:
+ * SUCCESS—支付成功
+ * REFUND—转入退款
+ * NOTPAY—未支付
+ * CLOSED—已关闭
+ * REVOKED—已撤销(付款码支付)
+ * USERPAYING—用户支付中(付款码支付)
+ * PAYERROR—支付失败(其他原因,如银行返回失败)
+ *
+ */
+ @XStreamAlias("trade_state")
+ private String tradeState;
+
+ /**
+ *
+ * 交易状态描述
+ * trade_state_desc
+ * 是
+ * String(256)
+ * 支付成功
+ * 对当前查询订单状态的描述和下一步操作的指引
+ *
+ */
+ @XStreamAlias("trade_state_desc")
+ private String tradeStateDesc;
+
+ /**
+ *
+ * 押金金额
+ * total_fee
+ * 否
+ * Int
+ * 99
+ * 订单总金额,单位为分
+ *
+ */
+ @XStreamAlias("total_fee")
+ private Integer totalFee;
+
+ /**
+ *
+ * 现金支付金额
+ * cash_fee
+ * 否
+ * Int
+ * 99
+ * 现金支付金额订单现金支付金额
+ *
+ */
+ @XStreamAlias("cash_fee")
+ private Integer cashFee;
+
+ /**
+ *
+ * 支付完成时间
+ * time_end
+ * 否
+ * String(14)
+ * 20141030133525
+ * 订单支付时间,格式为yyyyMMddHHmmss
+ *
+ */
+ @XStreamAlias("time_end")
+ private String timeEnd;
+
+ /**
+ *
+ * 剩余押金
+ * remain_fee
+ * 否
+ * Int
+ * 88
+ * 剩余押金金额,单位为分
+ *
+ */
+ @XStreamAlias("remain_fee")
+ private Integer remainFee;
+
+ @Override
+ protected void loadXml(Document d) {
+ transactionId = readXmlString(d, "transaction_id");
+ outTradeNo = readXmlString(d, "out_trade_no");
+ tradeState = readXmlString(d, "trade_state");
+ tradeStateDesc = readXmlString(d, "trade_state_desc");
+ totalFee = readXmlInteger(d, "total_fee");
+ cashFee = readXmlInteger(d, "cash_fee");
+ timeEnd = readXmlString(d, "time_end");
+ remainFee = readXmlInteger(d, "remain_fee");
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositRefundResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositRefundResult.java
new file mode 100644
index 0000000000..7c25b534a5
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositRefundResult.java
@@ -0,0 +1,103 @@
+package com.github.binarywang.wxpay.bean.result;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.w3c.dom.Document;
+
+import java.io.Serializable;
+
+/**
+ *
+ * 押金退款结果
+ * 详见:https://pay.weixin.qq.com/wiki/doc/api/deposit_sl.php?chapter=27_7&index=6
+ *
+ *
+ * @author Binary Wang
+ * created on 2024-09-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@XStreamAlias("xml")
+public class WxDepositRefundResult extends BaseWxPayResult implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 微信订单号
+ * transaction_id
+ * 是
+ * String(32)
+ * 1217752501201407033233368018
+ * 微信支付押金订单号
+ *
+ */
+ @XStreamAlias("transaction_id")
+ private String transactionId;
+
+ /**
+ *
+ * 商户退款单号
+ * out_refund_no
+ * 是
+ * String(32)
+ * 1217752501201407033233368018
+ * 商户系统内部的退款单号
+ *
+ */
+ @XStreamAlias("out_refund_no")
+ private String outRefundNo;
+
+ /**
+ *
+ * 微信退款单号
+ * refund_id
+ * 是
+ * String(32)
+ * 1217752501201407033233368018
+ * 微信退款单号
+ *
+ */
+ @XStreamAlias("refund_id")
+ private String refundId;
+
+ /**
+ *
+ * 退款金额
+ * refund_fee
+ * 是
+ * Int
+ * 100
+ * 退款总金额,单位为分,可以做部分退款
+ *
+ */
+ @XStreamAlias("refund_fee")
+ private Integer refundFee;
+
+ /**
+ *
+ * 现金退款金额
+ * cash_refund_fee
+ * 否
+ * Int
+ * 100
+ * 现金退款金额,单位为分,只能为整数
+ *
+ */
+ @XStreamAlias("cash_refund_fee")
+ private Integer cashRefundFee;
+
+ @Override
+ protected void loadXml(Document d) {
+ transactionId = readXmlString(d, "transaction_id");
+ outRefundNo = readXmlString(d, "out_refund_no");
+ refundId = readXmlString(d, "refund_id");
+ refundFee = readXmlInteger(d, "refund_fee");
+ cashRefundFee = readXmlInteger(d, "cash_refund_fee");
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositUnfreezeResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositUnfreezeResult.java
new file mode 100644
index 0000000000..98da8c878b
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositUnfreezeResult.java
@@ -0,0 +1,103 @@
+package com.github.binarywang.wxpay.bean.result;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.w3c.dom.Document;
+
+import java.io.Serializable;
+
+/**
+ *
+ * 押金撤销结果
+ * 详见:https://pay.weixin.qq.com/wiki/doc/api/deposit_sl.php?chapter=27_7&index=5
+ *
+ *
+ * @author Binary Wang
+ * created on 2024-09-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@XStreamAlias("xml")
+public class WxDepositUnfreezeResult extends BaseWxPayResult implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 微信订单号
+ * transaction_id
+ * 是
+ * String(32)
+ * 1217752501201407033233368018
+ * 微信支付押金订单号
+ *
+ */
+ @XStreamAlias("transaction_id")
+ private String transactionId;
+
+ /**
+ *
+ * 商户撤销单号
+ * out_trade_no
+ * 是
+ * String(32)
+ * 20150806125346
+ * 商户系统内部的撤销单号
+ *
+ */
+ @XStreamAlias("out_trade_no")
+ private String outTradeNo;
+
+ /**
+ *
+ * 撤销金额
+ * unfreeze_fee
+ * 是
+ * Int
+ * 99
+ * 撤销的押金金额,单位为分
+ *
+ */
+ @XStreamAlias("unfreeze_fee")
+ private Integer unfreezeFee;
+
+ /**
+ *
+ * 剩余押金
+ * remain_fee
+ * 是
+ * Int
+ * 0
+ * 剩余押金金额,单位为分
+ *
+ */
+ @XStreamAlias("remain_fee")
+ private Integer remainFee;
+
+ /**
+ *
+ * 撤销时间
+ * time_end
+ * 是
+ * String(14)
+ * 20141030133525
+ * 撤销完成时间,格式为yyyyMMddHHmmss
+ *
+ */
+ @XStreamAlias("time_end")
+ private String timeEnd;
+
+ @Override
+ protected void loadXml(Document d) {
+ transactionId = readXmlString(d, "transaction_id");
+ outTradeNo = readXmlString(d, "out_trade_no");
+ unfreezeFee = readXmlInteger(d, "unfreeze_fee");
+ remainFee = readXmlInteger(d, "remain_fee");
+ timeEnd = readXmlString(d, "time_end");
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositUnifiedOrderResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositUnifiedOrderResult.java
new file mode 100644
index 0000000000..120aeb111a
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxDepositUnifiedOrderResult.java
@@ -0,0 +1,89 @@
+package com.github.binarywang.wxpay.bean.result;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.w3c.dom.Document;
+
+import java.io.Serializable;
+
+/**
+ *
+ * 押金下单结果
+ * 详见:https://pay.weixin.qq.com/wiki/doc/api/deposit_sl.php?chapter=27_7&index=2
+ *
+ *
+ * @author Binary Wang
+ * created on 2024-09-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@XStreamAlias("xml")
+public class WxDepositUnifiedOrderResult extends BaseWxPayResult implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 交易类型
+ * trade_type
+ * 是
+ * String(16)
+ * JSAPI
+ * 交易类型,取值为:JSAPI,NATIVE,APP等
+ *
+ */
+ @XStreamAlias("trade_type")
+ private String tradeType;
+
+ /**
+ *
+ * 预支付交易会话标识
+ * prepay_id
+ * 是
+ * String(64)
+ * wx201410272009395522657a690389285100
+ * 微信生成的预支付会话标识,用于后续接口调用中使用,该值有效期为2小时
+ *
+ */
+ @XStreamAlias("prepay_id")
+ private String prepayId;
+
+ /**
+ *
+ * 二维码链接
+ * code_url
+ * 否
+ * String(64)
+ * URl:weixin://wxpay/s/An4baqw
+ * trade_type 为 NATIVE 时有返回,可将该参数值生成二维码展示出来进行扫码支付
+ *
+ */
+ @XStreamAlias("code_url")
+ private String codeUrl;
+
+ /**
+ *
+ * 微信订单号
+ * transaction_id
+ * 是
+ * String(32)
+ * 1217752501201407033233368018
+ * 微信支付分配的交易会话标识
+ *
+ */
+ @XStreamAlias("transaction_id")
+ private String transactionId;
+
+ @Override
+ protected void loadXml(Document d) {
+ tradeType = readXmlString(d, "trade_type");
+ prepayId = readXmlString(d, "prepay_id");
+ codeUrl = readXmlString(d, "code_url");
+ transactionId = readXmlString(d, "transaction_id");
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxPayUnifiedOrderV3Result.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxPayUnifiedOrderV3Result.java
index 309fb8e752..e832f4c02b 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxPayUnifiedOrderV3Result.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxPayUnifiedOrderV3Result.java
@@ -58,7 +58,7 @@ public class WxPayUnifiedOrderV3Result implements Serializable {
/**
*
* 字段名:二维码链接(NATIVE支付 会返回)
- * 变量名:h5_url
+ * 变量名:code_url
* 是否必填:是
* 类型:string[1,512]
* 描述:
@@ -78,9 +78,26 @@ public static class JsapiResult implements Serializable {
private String appId;
private String timeStamp;
private String nonceStr;
+ /**
+ * 由于package为java保留关键字,因此改为packageValue,序列化时会自动转换为package字段名
+ */
+ @SerializedName("package")
private String packageValue;
private String signType;
private String paySign;
+ /**
+ *
+ * 字段名:预支付交易会话标识
+ * 变量名:prepay_id
+ * 是否必填:否(用户可选存储)
+ * 类型:string[1,64]
+ * 描述:
+ * 预支付交易会话标识。用于后续接口调用中使用,该值有效期为2小时
+ * 此字段用于支持用户存储prepay_id,以便复用和重新生成支付签名
+ * 示例值:wx201410272009395522657a690389285100
+ *
+ */
+ private String prepayId;
private String getSignStr() {
return String.format("%s\n%s\n%s\n%s\n", appId, timeStamp, nonceStr, packageValue);
@@ -93,8 +110,14 @@ public static class AppResult implements Serializable {
private static final long serialVersionUID = 5465773025172875110L;
private String appid;
+ @SerializedName("partnerid")
private String partnerId;
+ @SerializedName("prepayid")
private String prepayId;
+ /**
+ * 由于package为java保留关键字,因此改为packageValue,序列化时会自动转换为package字段名
+ */
+ @SerializedName("package")
private String packageValue;
private String noncestr;
private String timestamp;
@@ -106,30 +129,123 @@ private String getSignStr() {
}
public T getPayInfo(TradeTypeEnum tradeType, String appId, String mchId, PrivateKey privateKey) {
- String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
- String nonceStr = SignUtils.genRandomStr();
switch (tradeType) {
case JSAPI:
- JsapiResult jsapiResult = new JsapiResult();
- jsapiResult.setAppId(appId).setTimeStamp(timestamp)
- .setPackageValue("prepay_id=" + this.prepayId).setNonceStr(nonceStr)
- //签名类型,默认为RSA,仅支持RSA。
- .setSignType("RSA").setPaySign(SignUtils.sign(jsapiResult.getSignStr(), privateKey));
- return (T) jsapiResult;
+ return (T) buildJsapiResult(this.prepayId, appId, privateKey);
case H5:
return (T) this.h5Url;
case APP:
- AppResult appResult = new AppResult();
- appResult.setAppid(appId).setPrepayId(this.prepayId).setPartnerId(mchId)
- .setNoncestr(nonceStr).setTimestamp(timestamp)
- //暂填写固定值Sign=WXPay
- .setPackageValue("Sign=WXPay")
- .setSign(SignUtils.sign(appResult.getSignStr(), privateKey));
- return (T) appResult;
+ return (T) buildAppResult(this.prepayId, appId, mchId, privateKey);
case NATIVE:
return (T) this.codeUrl;
default:
throw new WxRuntimeException("不支持的支付类型");
}
}
+
+ /**
+ *
+ * 根据已有的prepay_id生成JSAPI支付所需的参数对象(解耦版本)
+ * 应用场景:
+ * 1. 用户已经通过createPartnerOrderV3或unifiedPartnerOrderV3获取了prepay_id
+ * 2. 用户希望存储prepay_id用于后续复用
+ * 3. 支付失败后,使用存储的prepay_id重新生成支付签名信息
+ *
+ * 使用示例:
+ * // 步骤1:创建订单并获取prepay_id
+ * WxPayUnifiedOrderV3Result result = wxPayService.unifiedPartnerOrderV3(TradeTypeEnum.JSAPI, request);
+ * String prepayId = result.getPrepayId();
+ * // 存储prepayId到数据库...
+ *
+ * // 步骤2:需要支付时,使用存储的prepay_id生成支付信息
+ * WxPayUnifiedOrderV3Result.JsapiResult payInfo = WxPayUnifiedOrderV3Result.getJsapiPayInfo(
+ * prepayId, appId, wxPayService.getConfig().getPrivateKey()
+ * );
+ *
+ *
+ * @param prepayId 预支付交易会话标识
+ * @param appId 应用ID
+ * @param privateKey 商户私钥,用于签名
+ * @return JSAPI支付所需的参数对象
+ */
+ public static JsapiResult getJsapiPayInfo(String prepayId, String appId, PrivateKey privateKey) {
+ if (prepayId == null || appId == null || privateKey == null) {
+ throw new IllegalArgumentException("prepayId, appId 和 privateKey 不能为空");
+ }
+ return buildJsapiResult(prepayId, appId, privateKey);
+ }
+
+ /**
+ *
+ * 根据已有的prepay_id生成APP支付所需的参数对象(解耦版本)
+ * 应用场景:
+ * 1. 用户已经通过createPartnerOrderV3或unifiedPartnerOrderV3获取了prepay_id
+ * 2. 用户希望存储prepay_id用于后续复用
+ * 3. 支付失败后,使用存储的prepay_id重新生成支付签名信息
+ *
+ * 使用示例:
+ * // 步骤1:创建订单并获取prepay_id
+ * WxPayUnifiedOrderV3Result result = wxPayService.unifiedPartnerOrderV3(TradeTypeEnum.APP, request);
+ * String prepayId = result.getPrepayId();
+ * // 存储prepayId到数据库...
+ *
+ * // 步骤2:需要支付时,使用存储的prepay_id生成支付信息
+ * WxPayUnifiedOrderV3Result.AppResult payInfo = WxPayUnifiedOrderV3Result.getAppPayInfo(
+ * prepayId, appId, mchId, wxPayService.getConfig().getPrivateKey()
+ * );
+ *
+ *
+ * @param prepayId 预支付交易会话标识
+ * @param appId 应用ID
+ * @param mchId 商户号
+ * @param privateKey 商户私钥,用于签名
+ * @return APP支付所需的参数对象
+ */
+ public static AppResult getAppPayInfo(String prepayId, String appId, String mchId, PrivateKey privateKey) {
+ if (prepayId == null || appId == null || mchId == null || privateKey == null) {
+ throw new IllegalArgumentException("prepayId, appId, mchId 和 privateKey 不能为空");
+ }
+ return buildAppResult(prepayId, appId, mchId, privateKey);
+ }
+
+ /**
+ * 构建JSAPI支付结果对象
+ *
+ * @param prepayId 预支付交易会话标识
+ * @param appId 应用ID
+ * @param privateKey 商户私钥,用于签名
+ * @return JSAPI支付所需的参数对象
+ */
+ private static JsapiResult buildJsapiResult(String prepayId, String appId, PrivateKey privateKey) {
+ String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
+ String nonceStr = SignUtils.genRandomStr();
+ JsapiResult jsapiResult = new JsapiResult();
+ jsapiResult.setAppId(appId).setTimeStamp(timestamp)
+ .setPackageValue("prepay_id=" + prepayId).setNonceStr(nonceStr)
+ .setPrepayId(prepayId)
+ //签名类型,默认为RSA,仅支持RSA。
+ .setSignType("RSA").setPaySign(SignUtils.sign(jsapiResult.getSignStr(), privateKey));
+ return jsapiResult;
+ }
+
+ /**
+ * 构建APP支付结果对象
+ *
+ * @param prepayId 预支付交易会话标识
+ * @param appId 应用ID
+ * @param mchId 商户号
+ * @param privateKey 商户私钥,用于签名
+ * @return APP支付所需的参数对象
+ */
+ private static AppResult buildAppResult(String prepayId, String appId, String mchId, PrivateKey privateKey) {
+ String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
+ String nonceStr = SignUtils.genRandomStr();
+ AppResult appResult = new AppResult();
+ appResult.setAppid(appId).setPrepayId(prepayId).setPartnerId(mchId)
+ .setNoncestr(nonceStr).setTimestamp(timestamp)
+ //暂填写固定值Sign=WXPay
+ .setPackageValue("Sign=WXPay")
+ .setSign(SignUtils.sign(appResult.getSignStr(), privateKey));
+ return appResult;
+ }
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxSignQueryResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxSignQueryResult.java
index 808b9d7ddf..ca435f5b0c 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxSignQueryResult.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxSignQueryResult.java
@@ -86,7 +86,7 @@ public class WxSignQueryResult extends BaseWxPayResult implements Serializable {
* 协议解约方式
* 非必传
*/
- @XStreamAlias("contract_terminated_mode")
+ @XStreamAlias("contract_termination_mode")
private Integer contractTerminatedMode;
/**
@@ -102,6 +102,36 @@ public class WxSignQueryResult extends BaseWxPayResult implements Serializable {
@XStreamAlias("openid")
private String openId;
+ /**
+ * 变更类型, ADD:签约,DELETE:解约
+ * 签约/解约回调通知时返回
+ */
+ @XStreamAlias("change_type")
+ private String changeType;
+
+ /**
+ * 操作时间
+ * 签约/解约回调通知时返回
+ */
+ @XStreamAlias("operate_time")
+ private String operateTime;
+
+ /**
+ * 为保持向后兼容保留的构造函数(不含 changeType、operateTime 字段)。
+ *
+ * @deprecated 请使用包含所有字段的全参构造函数。
+ */
+ @Deprecated
+ public WxSignQueryResult(String contractId, String planId, Long requestSerial,
+ String contractCode, String contractDisplayAccount,
+ Integer contractState, String contractSignedTime,
+ String contractExpiredTime, String contractTerminatedTime,
+ Integer contractTerminatedMode, String contractTerminationRemark,
+ String openId) {
+ this(contractId, planId, requestSerial, contractCode, contractDisplayAccount,
+ contractState, contractSignedTime, contractExpiredTime, contractTerminatedTime,
+ contractTerminatedMode, contractTerminationRemark, openId, null, null);
+ }
@Override
protected void loadXml(Document d) {
@@ -112,11 +142,13 @@ protected void loadXml(Document d) {
contractDisplayAccount = readXmlString(d, "contract_display_account");
contractState = readXmlInteger(d, "contract_state");
contractSignedTime = readXmlString(d, "contract_signed_time");
- contractExpiredTime = readXmlString(d, "contrace_Expired_time");
+ contractExpiredTime = readXmlString(d, "contract_expired_time");
contractTerminatedTime = readXmlString(d, "contract_terminated_time");
- contractTerminatedMode = readXmlInteger(d, "contract_terminate_mode");
+ contractTerminatedMode = readXmlInteger(d, "contract_termination_mode");
contractTerminationRemark = readXmlString(d, "contract_termination_remark");
openId = readXmlString(d, "openid");
+ changeType = readXmlString(d, "change_type");
+ operateTime = readXmlString(d, "operate_time");
}
@Override
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/enums/TradeTypeEnum.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/enums/TradeTypeEnum.java
index 80edf2d99b..460da8f509 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/enums/TradeTypeEnum.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/enums/TradeTypeEnum.java
@@ -29,9 +29,9 @@ public enum TradeTypeEnum {
H5("/v3/pay/transactions/h5", "/v3/combine-transactions/h5", "/v3/pay/partner/transactions/h5");
/**
- * 单独下单url
+ * 直连商户支付url
*/
- private final String partnerUrl;
+ private final String merchantUrl;
/**
* 合并下单url
@@ -39,7 +39,7 @@ public enum TradeTypeEnum {
private final String combineUrl;
/**
- * 服务商下单
+ * 服务商支付url
*/
- private final String basePartnerUrl;
+ private final String partnerUrl;
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/BillingPlan.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/BillingPlan.java
new file mode 100644
index 0000000000..b664f4cb7b
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/BillingPlan.java
@@ -0,0 +1,110 @@
+package com.github.binarywang.wxpay.bean.subscriptionbilling;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 扣费计划信息
+ *
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ *
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class BillingPlan implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 字段名:计划类型
+ * 变量名:plan_type
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 扣费计划类型
+ * MONTHLY:按月扣费
+ * WEEKLY:按周扣费
+ * DAILY:按日扣费
+ * YEARLY:按年扣费
+ * 示例值:MONTHLY
+ *
+ */
+ @SerializedName("plan_type")
+ private String planType;
+
+ /**
+ *
+ * 字段名:扣费周期
+ * 变量名:period
+ * 是否必填:是
+ * 类型:int
+ * 描述:
+ * 扣费周期,配合plan_type使用
+ * 例如:plan_type为MONTHLY,period为1,表示每1个月扣费一次
+ * 示例值:1
+ *
+ */
+ @SerializedName("period")
+ private Integer period;
+
+ /**
+ *
+ * 字段名:总扣费次数
+ * 变量名:total_count
+ * 是否必填:否
+ * 类型:int
+ * 描述:
+ * 总扣费次数,不填表示无限次扣费
+ * 示例值:12
+ *
+ */
+ @SerializedName("total_count")
+ private Integer totalCount;
+
+ /**
+ *
+ * 字段名:已扣费次数
+ * 变量名:executed_count
+ * 是否必填:否
+ * 类型:int
+ * 描述:
+ * 已扣费次数,查询时返回
+ * 示例值:2
+ *
+ */
+ @SerializedName("executed_count")
+ private Integer executedCount;
+
+ /**
+ *
+ * 字段名:计划开始时间
+ * 变量名:start_time
+ * 是否必填:否
+ * 类型:string(32)
+ * 描述:
+ * 计划开始时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
+ * 示例值:2018-06-08T10:34:56+08:00
+ *
+ */
+ @SerializedName("start_time")
+ private String startTime;
+
+ /**
+ *
+ * 字段名:计划结束时间
+ * 变量名:end_time
+ * 是否必填:否
+ * 类型:string(32)
+ * 描述:
+ * 计划结束时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
+ * 示例值:2019-06-08T10:34:56+08:00
+ *
+ */
+ @SerializedName("end_time")
+ private String endTime;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionAmount.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionAmount.java
new file mode 100644
index 0000000000..c778a8ecb6
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionAmount.java
@@ -0,0 +1,49 @@
+package com.github.binarywang.wxpay.bean.subscriptionbilling;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 预约扣费金额信息
+ *
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ *
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class SubscriptionAmount implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 字段名:总金额
+ * 变量名:total
+ * 是否必填:是
+ * 类型:int
+ * 描述:
+ * 订单总金额,单位为分
+ * 示例值:100
+ *
+ */
+ @SerializedName("total")
+ private Integer total;
+
+ /**
+ *
+ * 字段名:货币类型
+ * 变量名:currency
+ * 是否必填:否
+ * 类型:string(16)
+ * 描述:
+ * CNY:人民币,境内商户号仅支持人民币
+ * 示例值:CNY
+ *
+ */
+ @SerializedName("currency")
+ private String currency;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionCancelRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionCancelRequest.java
new file mode 100644
index 0000000000..233d756f03
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionCancelRequest.java
@@ -0,0 +1,49 @@
+package com.github.binarywang.wxpay.bean.subscriptionbilling;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 取消预约扣费请求参数
+ *
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ *
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class SubscriptionCancelRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 字段名:预约扣费ID
+ * 变量名:subscription_id
+ * 是否必填:是
+ * 类型:string(64)
+ * 描述:
+ * 微信支付预约扣费ID
+ * 示例值:1217752501201407033233368018
+ *
+ */
+ @SerializedName("subscription_id")
+ private String subscriptionId;
+
+ /**
+ *
+ * 字段名:取消原因
+ * 变量名:cancel_reason
+ * 是否必填:否
+ * 类型:string(256)
+ * 描述:
+ * 取消原因描述
+ * 示例值:用户主动取消
+ *
+ */
+ @SerializedName("cancel_reason")
+ private String cancelReason;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionCancelResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionCancelResult.java
new file mode 100644
index 0000000000..74ca22f130
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionCancelResult.java
@@ -0,0 +1,77 @@
+package com.github.binarywang.wxpay.bean.subscriptionbilling;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 取消预约扣费响应结果
+ *
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ *
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class SubscriptionCancelResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 字段名:预约扣费ID
+ * 变量名:subscription_id
+ * 是否必填:是
+ * 类型:string(64)
+ * 描述:
+ * 微信支付预约扣费ID
+ * 示例值:1217752501201407033233368018
+ *
+ */
+ @SerializedName("subscription_id")
+ private String subscriptionId;
+
+ /**
+ *
+ * 字段名:预约状态
+ * 变量名:status
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 预约状态,取消后应为CANCELLED
+ * 示例值:CANCELLED
+ *
+ */
+ @SerializedName("status")
+ private String status;
+
+ /**
+ *
+ * 字段名:取消时间
+ * 变量名:cancel_time
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 取消时间,遵循rfc3339标准格式
+ * 示例值:2018-06-08T10:34:56+08:00
+ *
+ */
+ @SerializedName("cancel_time")
+ private String cancelTime;
+
+ /**
+ *
+ * 字段名:取消原因
+ * 变量名:cancel_reason
+ * 是否必填:否
+ * 类型:string(256)
+ * 描述:
+ * 取消原因描述
+ * 示例值:用户主动取消
+ *
+ */
+ @SerializedName("cancel_reason")
+ private String cancelReason;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionInstantBillingRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionInstantBillingRequest.java
new file mode 100644
index 0000000000..2b5a3dec37
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionInstantBillingRequest.java
@@ -0,0 +1,104 @@
+package com.github.binarywang.wxpay.bean.subscriptionbilling;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 立即扣费请求参数
+ *
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ *
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class SubscriptionInstantBillingRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 字段名:商户订单号
+ * 变量名:out_trade_no
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
+ * 示例值:1217752501201407033233368018
+ *
+ */
+ @SerializedName("out_trade_no")
+ private String outTradeNo;
+
+ /**
+ *
+ * 字段名:用户标识
+ * 变量名:openid
+ * 是否必填:是
+ * 类型:string(128)
+ * 描述:
+ * 用户在直连商户appid下的唯一标识
+ * 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
+ *
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ *
+ * 字段名:订单描述
+ * 变量名:description
+ * 是否必填:是
+ * 类型:string(127)
+ * 描述:
+ * 订单描述
+ * 示例值:腾讯充值中心-QQ会员充值
+ *
+ */
+ @SerializedName("description")
+ private String description;
+
+ /**
+ *
+ * 字段名:扣费金额
+ * 变量名:amount
+ * 是否必填:是
+ * 类型:object
+ * 描述:
+ * 扣费金额信息
+ *
+ */
+ @SerializedName("amount")
+ private SubscriptionAmount amount;
+
+ /**
+ *
+ * 字段名:通知地址
+ * 变量名:notify_url
+ * 是否必填:否
+ * 类型:string(256)
+ * 描述:
+ * 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数
+ * 示例值:https://www.weixin.qq.com/wxpay/pay.php
+ *
+ */
+ @SerializedName("notify_url")
+ private String notifyUrl;
+
+ /**
+ *
+ * 字段名:附加数据
+ * 变量名:attach
+ * 是否必填:否
+ * 类型:string(128)
+ * 描述:
+ * 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
+ * 示例值:自定义数据
+ *
+ */
+ @SerializedName("attach")
+ private String attach;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionInstantBillingResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionInstantBillingResult.java
new file mode 100644
index 0000000000..ac34307cd4
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionInstantBillingResult.java
@@ -0,0 +1,111 @@
+package com.github.binarywang.wxpay.bean.subscriptionbilling;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 立即扣费响应结果
+ *
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ *
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class SubscriptionInstantBillingResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 字段名:微信支付订单号
+ * 变量名:transaction_id
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 微信支付系统生成的订单号
+ * 示例值:1217752501201407033233368018
+ *
+ */
+ @SerializedName("transaction_id")
+ private String transactionId;
+
+ /**
+ *
+ * 字段名:商户订单号
+ * 变量名:out_trade_no
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 商户系统内部订单号
+ * 示例值:1217752501201407033233368018
+ *
+ */
+ @SerializedName("out_trade_no")
+ private String outTradeNo;
+
+ /**
+ *
+ * 字段名:交易状态
+ * 变量名:trade_state
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 交易状态
+ * SUCCESS:支付成功
+ * REFUND:转入退款
+ * NOTPAY:未支付
+ * CLOSED:已关闭
+ * REVOKED:已撤销(刷卡支付)
+ * USERPAYING:用户支付中
+ * PAYERROR:支付失败
+ * 示例值:SUCCESS
+ *
+ */
+ @SerializedName("trade_state")
+ private String tradeState;
+
+ /**
+ *
+ * 字段名:交易状态描述
+ * 变量名:trade_state_desc
+ * 是否必填:是
+ * 类型:string(256)
+ * 描述:
+ * 交易状态描述
+ * 示例值:支付成功
+ *
+ */
+ @SerializedName("trade_state_desc")
+ private String tradeStateDesc;
+
+ /**
+ *
+ * 字段名:支付完成时间
+ * 变量名:success_time
+ * 是否必填:否
+ * 类型:string(32)
+ * 描述:
+ * 支付完成时间,遵循rfc3339标准格式
+ * 示例值:2018-06-08T10:34:56+08:00
+ *
+ */
+ @SerializedName("success_time")
+ private String successTime;
+
+ /**
+ *
+ * 字段名:扣费金额
+ * 变量名:amount
+ * 是否必填:是
+ * 类型:object
+ * 描述:
+ * 扣费金额信息
+ *
+ */
+ @SerializedName("amount")
+ private SubscriptionAmount amount;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionQueryResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionQueryResult.java
new file mode 100644
index 0000000000..17e2c7dc19
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionQueryResult.java
@@ -0,0 +1,177 @@
+package com.github.binarywang.wxpay.bean.subscriptionbilling;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 预约扣费查询结果
+ *
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ *
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class SubscriptionQueryResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 字段名:预约扣费ID
+ * 变量名:subscription_id
+ * 是否必填:是
+ * 类型:string(64)
+ * 描述:
+ * 微信支付预约扣费ID
+ * 示例值:1217752501201407033233368018
+ *
+ */
+ @SerializedName("subscription_id")
+ private String subscriptionId;
+
+ /**
+ *
+ * 字段名:商户订单号
+ * 变量名:out_trade_no
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 商户系统内部订单号
+ * 示例值:1217752501201407033233368018
+ *
+ */
+ @SerializedName("out_trade_no")
+ private String outTradeNo;
+
+ /**
+ *
+ * 字段名:用户标识
+ * 变量名:openid
+ * 是否必填:是
+ * 类型:string(128)
+ * 描述:
+ * 用户在直连商户appid下的唯一标识
+ * 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
+ *
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ *
+ * 字段名:订单描述
+ * 变量名:description
+ * 是否必填:是
+ * 类型:string(127)
+ * 描述:
+ * 订单描述
+ * 示例值:腾讯充值中心-QQ会员充值
+ *
+ */
+ @SerializedName("description")
+ private String description;
+
+ /**
+ *
+ * 字段名:预约状态
+ * 变量名:status
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 预约状态
+ * SCHEDULED:已预约
+ * CANCELLED:已取消
+ * EXECUTED:已执行
+ * FAILED:执行失败
+ * 示例值:SCHEDULED
+ *
+ */
+ @SerializedName("status")
+ private String status;
+
+ /**
+ *
+ * 字段名:预约扣费时间
+ * 变量名:schedule_time
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 预约扣费的时间,遵循rfc3339标准格式
+ * 示例值:2018-06-08T10:34:56+08:00
+ *
+ */
+ @SerializedName("schedule_time")
+ private String scheduleTime;
+
+ /**
+ *
+ * 字段名:创建时间
+ * 变量名:create_time
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 预约创建时间,遵循rfc3339标准格式
+ * 示例值:2018-06-08T10:34:56+08:00
+ *
+ */
+ @SerializedName("create_time")
+ private String createTime;
+
+ /**
+ *
+ * 字段名:更新时间
+ * 变量名:update_time
+ * 是否必填:否
+ * 类型:string(32)
+ * 描述:
+ * 预约更新时间,遵循rfc3339标准格式
+ * 示例值:2018-06-08T10:34:56+08:00
+ *
+ */
+ @SerializedName("update_time")
+ private String updateTime;
+
+ /**
+ *
+ * 字段名:预约扣费金额
+ * 变量名:amount
+ * 是否必填:是
+ * 类型:object
+ * 描述:
+ * 预约扣费金额信息
+ *
+ */
+ @SerializedName("amount")
+ private SubscriptionAmount amount;
+
+ /**
+ *
+ * 字段名:扣费计划
+ * 变量名:billing_plan
+ * 是否必填:否
+ * 类型:object
+ * 描述:
+ * 扣费计划信息
+ *
+ */
+ @SerializedName("billing_plan")
+ private BillingPlan billingPlan;
+
+ /**
+ *
+ * 字段名:附加数据
+ * 变量名:attach
+ * 是否必填:否
+ * 类型:string(128)
+ * 描述:
+ * 附加数据
+ * 示例值:自定义数据
+ *
+ */
+ @SerializedName("attach")
+ private String attach;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionScheduleRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionScheduleRequest.java
new file mode 100644
index 0000000000..51cf5aebd1
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionScheduleRequest.java
@@ -0,0 +1,131 @@
+package com.github.binarywang.wxpay.bean.subscriptionbilling;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 预约扣费请求参数
+ *
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ *
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class SubscriptionScheduleRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 字段名:商户订单号
+ * 变量名:out_trade_no
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
+ * 示例值:1217752501201407033233368018
+ *
+ */
+ @SerializedName("out_trade_no")
+ private String outTradeNo;
+
+ /**
+ *
+ * 字段名:用户标识
+ * 变量名:openid
+ * 是否必填:是
+ * 类型:string(128)
+ * 描述:
+ * 用户在直连商户appid下的唯一标识
+ * 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
+ *
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ *
+ * 字段名:订单描述
+ * 变量名:description
+ * 是否必填:是
+ * 类型:string(127)
+ * 描述:
+ * 订单描述
+ * 示例值:腾讯充值中心-QQ会员充值
+ *
+ */
+ @SerializedName("description")
+ private String description;
+
+ /**
+ *
+ * 字段名:预约扣费金额
+ * 变量名:amount
+ * 是否必填:是
+ * 类型:object
+ * 描述:
+ * 预约扣费金额信息
+ *
+ */
+ @SerializedName("amount")
+ private SubscriptionAmount amount;
+
+ /**
+ *
+ * 字段名:预约扣费时间
+ * 变量名:schedule_time
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 预约扣费的时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
+ * 示例值:2018-06-08T10:34:56+08:00
+ *
+ */
+ @SerializedName("schedule_time")
+ private String scheduleTime;
+
+ /**
+ *
+ * 字段名:扣费计划
+ * 变量名:billing_plan
+ * 是否必填:否
+ * 类型:object
+ * 描述:
+ * 扣费计划信息,用于连续包月等场景
+ *
+ */
+ @SerializedName("billing_plan")
+ private BillingPlan billingPlan;
+
+ /**
+ *
+ * 字段名:通知地址
+ * 变量名:notify_url
+ * 是否必填:否
+ * 类型:string(256)
+ * 描述:
+ * 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数
+ * 示例值:https://www.weixin.qq.com/wxpay/pay.php
+ *
+ */
+ @SerializedName("notify_url")
+ private String notifyUrl;
+
+ /**
+ *
+ * 字段名:附加数据
+ * 变量名:attach
+ * 是否必填:否
+ * 类型:string(128)
+ * 描述:
+ * 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
+ * 示例值:自定义数据
+ *
+ */
+ @SerializedName("attach")
+ private String attach;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionScheduleResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionScheduleResult.java
new file mode 100644
index 0000000000..fc0f9f6615
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionScheduleResult.java
@@ -0,0 +1,121 @@
+package com.github.binarywang.wxpay.bean.subscriptionbilling;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 预约扣费响应结果
+ *
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ *
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class SubscriptionScheduleResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 字段名:预约扣费ID
+ * 变量名:subscription_id
+ * 是否必填:是
+ * 类型:string(64)
+ * 描述:
+ * 微信支付预约扣费ID
+ * 示例值:1217752501201407033233368018
+ *
+ */
+ @SerializedName("subscription_id")
+ private String subscriptionId;
+
+ /**
+ *
+ * 字段名:商户订单号
+ * 变量名:out_trade_no
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 商户系统内部订单号
+ * 示例值:1217752501201407033233368018
+ *
+ */
+ @SerializedName("out_trade_no")
+ private String outTradeNo;
+
+ /**
+ *
+ * 字段名:预约状态
+ * 变量名:status
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 预约状态
+ * SCHEDULED:已预约
+ * CANCELLED:已取消
+ * EXECUTED:已执行
+ * FAILED:执行失败
+ * 示例值:SCHEDULED
+ *
+ */
+ @SerializedName("status")
+ private String status;
+
+ /**
+ *
+ * 字段名:预约扣费时间
+ * 变量名:schedule_time
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 预约扣费的时间,遵循rfc3339标准格式
+ * 示例值:2018-06-08T10:34:56+08:00
+ *
+ */
+ @SerializedName("schedule_time")
+ private String scheduleTime;
+
+ /**
+ *
+ * 字段名:创建时间
+ * 变量名:create_time
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 预约创建时间,遵循rfc3339标准格式
+ * 示例值:2018-06-08T10:34:56+08:00
+ *
+ */
+ @SerializedName("create_time")
+ private String createTime;
+
+ /**
+ *
+ * 字段名:预约扣费金额
+ * 变量名:amount
+ * 是否必填:是
+ * 类型:object
+ * 描述:
+ * 预约扣费金额信息
+ *
+ */
+ @SerializedName("amount")
+ private SubscriptionAmount amount;
+
+ /**
+ *
+ * 字段名:扣费计划
+ * 变量名:billing_plan
+ * 是否必填:否
+ * 类型:object
+ * 描述:
+ * 扣费计划信息
+ *
+ */
+ @SerializedName("billing_plan")
+ private BillingPlan billingPlan;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionTransactionQueryRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionTransactionQueryRequest.java
new file mode 100644
index 0000000000..17b3681cea
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionTransactionQueryRequest.java
@@ -0,0 +1,91 @@
+package com.github.binarywang.wxpay.bean.subscriptionbilling;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 查询扣费记录请求参数
+ *
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ *
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class SubscriptionTransactionQueryRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 字段名:用户标识
+ * 变量名:openid
+ * 是否必填:否
+ * 类型:string(128)
+ * 描述:
+ * 用户在直连商户appid下的唯一标识
+ * 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
+ *
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ *
+ * 字段名:开始时间
+ * 变量名:begin_time
+ * 是否必填:否
+ * 类型:string(32)
+ * 描述:
+ * 查询开始时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
+ * 示例值:2018-06-08T10:34:56+08:00
+ *
+ */
+ @SerializedName("begin_time")
+ private String beginTime;
+
+ /**
+ *
+ * 字段名:结束时间
+ * 变量名:end_time
+ * 是否必填:否
+ * 类型:string(32)
+ * 描述:
+ * 查询结束时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE
+ * 示例值:2018-06-08T10:34:56+08:00
+ *
+ */
+ @SerializedName("end_time")
+ private String endTime;
+
+ /**
+ *
+ * 字段名:分页大小
+ * 变量名:limit
+ * 是否必填:否
+ * 类型:int
+ * 描述:
+ * 分页大小,不超过50
+ * 示例值:20
+ *
+ */
+ @SerializedName("limit")
+ private Integer limit;
+
+ /**
+ *
+ * 字段名:分页偏移量
+ * 变量名:offset
+ * 是否必填:否
+ * 类型:int
+ * 描述:
+ * 分页偏移量
+ * 示例值:0
+ *
+ */
+ @SerializedName("offset")
+ private Integer offset;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionTransactionQueryResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionTransactionQueryResult.java
new file mode 100644
index 0000000000..75fff11a22
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/subscriptionbilling/SubscriptionTransactionQueryResult.java
@@ -0,0 +1,190 @@
+package com.github.binarywang.wxpay.bean.subscriptionbilling;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 查询扣费记录响应结果
+ *
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ *
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class SubscriptionTransactionQueryResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 字段名:总数量
+ * 变量名:total_count
+ * 是否必填:是
+ * 类型:int
+ * 描述:
+ * 符合条件的记录总数量
+ * 示例值:100
+ *
+ */
+ @SerializedName("total_count")
+ private Integer totalCount;
+
+ /**
+ *
+ * 字段名:扣费记录列表
+ * 变量名:data
+ * 是否必填:是
+ * 类型:array
+ * 描述:
+ * 扣费记录列表
+ *
+ */
+ @SerializedName("data")
+ private List data;
+
+ /**
+ * 扣费记录
+ */
+ @Data
+ @NoArgsConstructor
+ public static class SubscriptionTransaction implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 字段名:微信支付订单号
+ * 变量名:transaction_id
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 微信支付系统生成的订单号
+ * 示例值:1217752501201407033233368018
+ *
+ */
+ @SerializedName("transaction_id")
+ private String transactionId;
+
+ /**
+ *
+ * 字段名:商户订单号
+ * 变量名:out_trade_no
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 商户系统内部订单号
+ * 示例值:1217752501201407033233368018
+ *
+ */
+ @SerializedName("out_trade_no")
+ private String outTradeNo;
+
+ /**
+ *
+ * 字段名:预约扣费ID
+ * 变量名:subscription_id
+ * 是否必填:否
+ * 类型:string(64)
+ * 描述:
+ * 微信支付预约扣费ID,预约扣费产生的交易才有此字段
+ * 示例值:1217752501201407033233368018
+ *
+ */
+ @SerializedName("subscription_id")
+ private String subscriptionId;
+
+ /**
+ *
+ * 字段名:交易状态
+ * 变量名:trade_state
+ * 是否必填:是
+ * 类型:string(32)
+ * 描述:
+ * 交易状态
+ * SUCCESS:支付成功
+ * REFUND:转入退款
+ * NOTPAY:未支付
+ * CLOSED:已关闭
+ * REVOKED:已撤销(刷卡支付)
+ * USERPAYING:用户支付中
+ * PAYERROR:支付失败
+ * 示例值:SUCCESS
+ *
+ */
+ @SerializedName("trade_state")
+ private String tradeState;
+
+ /**
+ *
+ * 字段名:支付完成时间
+ * 变量名:success_time
+ * 是否必填:否
+ * 类型:string(32)
+ * 描述:
+ * 支付完成时间,遵循rfc3339标准格式
+ * 示例值:2018-06-08T10:34:56+08:00
+ *
+ */
+ @SerializedName("success_time")
+ private String successTime;
+
+ /**
+ *
+ * 字段名:扣费金额
+ * 变量名:amount
+ * 是否必填:是
+ * 类型:object
+ * 描述:
+ * 扣费金额信息
+ *
+ */
+ @SerializedName("amount")
+ private SubscriptionAmount amount;
+
+ /**
+ *
+ * 字段名:用户标识
+ * 变量名:openid
+ * 是否必填:是
+ * 类型:string(128)
+ * 描述:
+ * 用户在直连商户appid下的唯一标识
+ * 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
+ *
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ *
+ * 字段名:订单描述
+ * 变量名:description
+ * 是否必填:是
+ * 类型:string(127)
+ * 描述:
+ * 订单描述
+ * 示例值:腾讯充值中心-QQ会员充值
+ *
+ */
+ @SerializedName("description")
+ private String description;
+
+ /**
+ *
+ * 字段名:附加数据
+ * 变量名:attach
+ * 是否必填:否
+ * 类型:string(128)
+ * 描述:
+ * 附加数据
+ * 示例值:自定义数据
+ *
+ */
+ @SerializedName("attach")
+ private String attach;
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferQueryRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferQueryRequest.java
new file mode 100644
index 0000000000..f1323655a1
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferQueryRequest.java
@@ -0,0 +1,44 @@
+package com.github.binarywang.wxpay.bean.transfer;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 运营工具-商家转账查询请求参数
+ *
+ * @author WxJava Team
+ * @see 运营工具-商家转账API
+ */
+@Data
+@Builder(builderMethodName = "newBuilder")
+@NoArgsConstructor
+@AllArgsConstructor
+public class BusinessOperationTransferQueryRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 商户系统内部的商家单号
+ * 与transfer_bill_no二选一
+ */
+ @SerializedName("out_bill_no")
+ private String outBillNo;
+
+ /**
+ * 微信转账单号
+ * 与out_bill_no二选一
+ */
+ @SerializedName("transfer_bill_no")
+ private String transferBillNo;
+
+ /**
+ * 直连商户的appid
+ * 可选
+ */
+ @SerializedName("appid")
+ private String appid;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferQueryResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferQueryResult.java
new file mode 100644
index 0000000000..0cfd8f8570
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferQueryResult.java
@@ -0,0 +1,101 @@
+package com.github.binarywang.wxpay.bean.transfer;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 运营工具-商家转账查询结果
+ *
+ * @author WxJava Team
+ * @see 运营工具-商家转账API
+ */
+@Data
+@NoArgsConstructor
+public class BusinessOperationTransferQueryResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 直连商户的appid
+ */
+ @SerializedName("appid")
+ private String appid;
+
+ /**
+ * 商户系统内部的商家单号
+ */
+ @SerializedName("out_bill_no")
+ private String outBillNo;
+
+ /**
+ * 微信转账单号
+ */
+ @SerializedName("transfer_bill_no")
+ private String transferBillNo;
+
+ /**
+ * 运营工具转账场景ID
+ */
+ @SerializedName("operation_scene_id")
+ private String operationSceneId;
+
+ /**
+ * 用户在直连商户应用下的用户标示
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ * 收款用户姓名
+ * 已脱敏
+ */
+ @SerializedName("user_name")
+ private String userName;
+
+ /**
+ * 转账金额
+ * 单位为"分"
+ */
+ @SerializedName("transfer_amount")
+ private Integer transferAmount;
+
+ /**
+ * 转账备注
+ */
+ @SerializedName("transfer_remark")
+ private String transferRemark;
+
+ /**
+ * 转账状态
+ * WAIT_PAY:等待确认
+ * PROCESSING:转账中
+ * SUCCESS:转账成功
+ * FAIL:转账失败
+ * REFUND:已退款
+ */
+ @SerializedName("transfer_state")
+ private String transferState;
+
+ /**
+ * 发起转账的时间
+ * 遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE
+ */
+ @SerializedName("create_time")
+ private String createTime;
+
+ /**
+ * 转账更新时间
+ * 遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE
+ */
+ @SerializedName("update_time")
+ private String updateTime;
+
+ /**
+ * 失败原因
+ * 当transfer_state为FAIL时返回
+ */
+ @SerializedName("fail_reason")
+ private String failReason;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferRequest.java
new file mode 100644
index 0000000000..0129798ed9
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferRequest.java
@@ -0,0 +1,125 @@
+package com.github.binarywang.wxpay.bean.transfer;
+
+import com.github.binarywang.wxpay.v3.SpecEncrypt;
+import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 运营工具-商家转账请求参数
+ *
+ * @author WxJava Team
+ * @see 运营工具-商家转账API
+ */
+@Data
+@Builder(builderMethodName = "newBuilder")
+@NoArgsConstructor
+@AllArgsConstructor
+public class BusinessOperationTransferRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 直连商户的appid
+ * 必须
+ */
+ @SerializedName("appid")
+ private String appid;
+
+ /**
+ * 商户系统内部的商家单号
+ * 必须,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
+ */
+ @SerializedName("out_bill_no")
+ private String outBillNo;
+
+ /**
+ * 转账场景ID
+ * 必须,该笔转账使用的转账场景,可前往"商户平台-产品中心-商家转账"中申请。
+ * 运营工具场景ID如:2001(现金营销)、2002(佣金报酬)、2003(推广奖励)等
+ * 可使用 {@link com.github.binarywang.wxpay.constant.WxPayConstants.OperationSceneId} 中定义的常量
+ */
+ @SerializedName("transfer_scene_id")
+ private String transferSceneId;
+
+ /**
+ * 用户在直连商户应用下的用户标示
+ * 必须
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ * 收款用户姓名
+ * 可选,传入则校验收款用户姓名
+ * 使用RSA加密,使用OAEP填充方式
+ */
+ @SpecEncrypt
+ @SerializedName("user_name")
+ private String userName;
+
+ /**
+ * 转账金额
+ * 必须,单位为"分"
+ */
+ @SerializedName("transfer_amount")
+ private Integer transferAmount;
+
+ /**
+ * 转账备注
+ * 必须,会在转账成功消息和转账详情页向用户展示
+ */
+ @SerializedName("transfer_remark")
+ private String transferRemark;
+
+ /**
+ * 用户收款感知
+ * 可选,用于在转账成功消息中向用户展示特定内容
+ */
+ @SerializedName("user_recv_perception")
+ private String userRecvPerception;
+
+ /**
+ * 异步接收微信支付转账结果通知的回调地址
+ * 可选,通知URL必须为外网可以正常访问的地址,不能携带查询参数
+ */
+ @SerializedName("notify_url")
+ private String notifyUrl;
+
+ /**
+ * 转账场景报备信息
+ * 必须,需按转账场景准确填写报备信息,参考 转账场景报备信息字段说明
+ */
+ @SerializedName("transfer_scene_report_infos")
+ private List transferSceneReportInfos;
+
+ /**
+ * 转账场景报备信息
+ */
+ @Data
+ @Accessors(chain = true)
+ public static class TransferSceneReportInfo implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 信息类型
+ * 必须,不能超过15个字符,商户所属转账场景下的信息类型,此字段内容为固定值,需严格按照 转账场景报备信息字段说明 传参。
+ */
+ @SerializedName("info_type")
+ private String infoType;
+
+ /**
+ * 信息内容
+ * 必须,不能超过32个字符,商户所属转账场景下的信息内容,商户可按实际业务场景自定义传参,需严格按照 转账场景报备信息字段说明 传参。
+ */
+ @SerializedName("info_content")
+ private String infoContent;
+
+ }
+
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferResult.java
new file mode 100644
index 0000000000..91771b43e1
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferResult.java
@@ -0,0 +1,76 @@
+package com.github.binarywang.wxpay.bean.transfer;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 运营工具-商家转账结果
+ *
+ * @author WxJava Team
+ * @see 运营工具-商家转账API
+ */
+@Data
+@NoArgsConstructor
+public class BusinessOperationTransferResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 商户系统内部的商家单号
+ */
+ @SerializedName("out_bill_no")
+ private String outBillNo;
+
+ /**
+ * 微信转账单号
+ * 微信商家转账系统返回的唯一标识
+ */
+ @SerializedName("transfer_bill_no")
+ private String transferBillNo;
+
+ /**
+ * 单据状态
+ * 商家转账订单状态
+ * ACCEPTED:转账已受理,可原单重试(非终态)。
+ * PROCESSING: 转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试(非终态)。
+ * WAIT_USER_CONFIRM: 待收款用户确认,当前转账单据资金已锁定,可拉起微信收款确认页面进行收款确认(非终态)。
+ * TRANSFERING: 转账中,可拉起微信收款确认页面再次重试确认收款(非终态)。
+ * SUCCESS: 转账成功,表示转账单据已成功(终态)。
+ * FAIL: 转账失败,表示该笔转账单据已失败。若需重新向用户转账,请重新生成单据并再次发起(终态)。
+ * CANCELING: 转账撤销中,商户撤销请求受理成功,该笔转账正在撤销中,需查单确认撤销的转账单据状态(非终态)。
+ * CANCELLED: 转账撤销完成,代表转账单据已撤销成功(终态)。
+ */
+ @SerializedName("state")
+ private String state;
+
+ /**
+ * 跳转领取页面的package信息
+ * 跳转微信支付收款页的package信息, APP调起用户确认收款 或者 JSAPI调起用户确认收款 时需要使用的参数。仅当转账单据状态为WAIT_USER_CONFIRM时返回。
+ * 单据创建后,用户24小时内不领取将过期关闭,建议拉起用户确认收款页面前,先查单据状态:如单据状态为WAIT_USER_CONFIRM,可用之前的package信息拉起;单据到终态时需更换单号重新发起转账。
+ */
+ @SerializedName("package_info")
+ private String packageInfo;
+
+ /**
+ * 发起转账的时间
+ * 遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE
+ */
+ @SerializedName("create_time")
+ private String createTime;
+
+ /**
+ * 转账更新时间
+ * 遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE
+ */
+ @SerializedName("update_time")
+ private String updateTime;
+
+ /**
+ * 失败原因
+ * 当transfer_state为FAIL时返回
+ */
+ @SerializedName("fail_reason")
+ private String failReason;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/ReservationTransferBatchGetResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/ReservationTransferBatchGetResult.java
new file mode 100644
index 0000000000..d32db8c7c2
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/ReservationTransferBatchGetResult.java
@@ -0,0 +1,170 @@
+package com.github.binarywang.wxpay.bean.transfer;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ *
+ * 预约转账批次单号查询接口响应结果
+ * 通过预约批次单号查询批量预约商家转账批次单基本信息。
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4015901167
+ *
+ *
+ * @author wanggang
+ * created on 2025/11/28
+ */
+@Data
+@NoArgsConstructor
+public class ReservationTransferBatchGetResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 【商户号】 微信支付分配的商户号
+ */
+ @SerializedName("mch_id")
+ private String mchId;
+
+ /**
+ * 【商户预约批次单号】 商户系统内部的商家预约批次单号
+ */
+ @SerializedName("out_batch_no")
+ private String outBatchNo;
+
+ /**
+ * 【微信预约批次单号】 微信预约批次单号,微信商家转账系统返回的唯一标识
+ */
+ @SerializedName("reservation_batch_no")
+ private String reservationBatchNo;
+
+ /**
+ * 【商户AppID】 商户在微信申请公众号或移动应用成功后分配的账号ID
+ */
+ @SerializedName("appid")
+ private String appid;
+
+ /**
+ * 【批次备注】 批次备注
+ */
+ @SerializedName("batch_remark")
+ private String batchRemark;
+
+ /**
+ * 【转账场景ID】 商户在商户平台-产品中心-商家转账中申请的转账场景ID
+ */
+ @SerializedName("transfer_scene_id")
+ private String transferSceneId;
+
+ /**
+ * 【批次状态】
+ * ACCEPTED: 批次已受理
+ * PROCESSING: 批次处理中
+ * FINISHED: 批次处理完成
+ * CLOSED: 批次已关闭
+ */
+ @SerializedName("batch_state")
+ private String batchState;
+
+ /**
+ * 【转账总金额】 转账金额单位为"分"
+ */
+ @SerializedName("total_amount")
+ private Integer totalAmount;
+
+ /**
+ * 【转账总笔数】 转账总笔数
+ */
+ @SerializedName("total_num")
+ private Integer totalNum;
+
+ /**
+ * 【转账成功金额】 转账成功金额单位为"分"
+ */
+ @SerializedName("success_amount")
+ private Integer successAmount;
+
+ /**
+ * 【转账成功笔数】 转账成功笔数
+ */
+ @SerializedName("success_num")
+ private Integer successNum;
+
+ /**
+ * 【转账失败金额】 转账失败金额单位为"分"
+ */
+ @SerializedName("fail_amount")
+ private Integer failAmount;
+
+ /**
+ * 【转账失败笔数】 转账失败笔数
+ */
+ @SerializedName("fail_num")
+ private Integer failNum;
+
+ /**
+ * 【批次创建时间】 批次受理成功时返回
+ * 遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE
+ */
+ @SerializedName("create_time")
+ private String createTime;
+
+ /**
+ * 【批次更新时间】 批次最后更新时间
+ * 遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE
+ */
+ @SerializedName("update_time")
+ private String updateTime;
+
+ /**
+ * 【批次关闭原因】 批次关闭原因
+ * MERCHANT_REVOCATION: 商户主动撤销
+ * OVERDUE_CLOSE: 系统超时关闭
+ */
+ @SerializedName("close_reason")
+ private String closeReason;
+
+ /**
+ * 【是否需要查询明细】
+ */
+ @SerializedName("need_query_detail")
+ private Boolean needQueryDetail;
+
+ /**
+ * 【转账明细列表】
+ */
+ @SerializedName("transfer_detail_list")
+ private List transferDetailList;
+
+ /**
+ * 转账明细
+ */
+ @Data
+ @NoArgsConstructor
+ public static class TransferDetail implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 【商户明细单号】 商户系统内部区分转账批次单下不同转账明细单的唯一标识
+ */
+ @SerializedName("out_detail_no")
+ private String outDetailNo;
+
+ /**
+ * 【微信转账单号】 微信转账单号,微信商家转账系统返回的唯一标识
+ */
+ @SerializedName("transfer_bill_no")
+ private String transferBillNo;
+
+ /**
+ * 【明细状态】
+ * PROCESSING: 转账处理中
+ * SUCCESS: 转账成功
+ * FAIL: 转账失败
+ */
+ @SerializedName("detail_state")
+ private String detailState;
+ }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/ReservationTransferBatchRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/ReservationTransferBatchRequest.java
new file mode 100644
index 0000000000..82e3d6f328
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/ReservationTransferBatchRequest.java
@@ -0,0 +1,148 @@
+package com.github.binarywang.wxpay.bean.transfer;
+
+import com.github.binarywang.wxpay.v3.SpecEncrypt;
+import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ *
+ * 批量预约商家转账请求参数
+ * 商户可以通过批量预约接口一次发起批量转账请求,最多可以同时向50个用户发起转账。
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4015901167
+ *
+ *
+ * @author wanggang
+ * created on 2025/11/28
+ */
+@Data
+@Builder(builderMethodName = "newBuilder")
+@NoArgsConstructor
+@AllArgsConstructor
+public class ReservationTransferBatchRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 【商户AppID】 商户在微信申请公众号或移动应用成功后分配的账号ID
+ */
+ @SerializedName("appid")
+ private String appid;
+
+ /**
+ * 【商户预约批次单号】 商户系统内部的商家预约批次单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
+ */
+ @SerializedName("out_batch_no")
+ private String outBatchNo;
+
+ /**
+ * 【转账场景ID】 商户在商户平台-产品中心-商家转账中申请的转账场景ID
+ */
+ @SerializedName("transfer_scene_id")
+ private String transferSceneId;
+
+ /**
+ * 【批次备注】 批次备注
+ */
+ @SerializedName("batch_remark")
+ private String batchRemark;
+
+ /**
+ * 【转账总金额】 转账金额单位为"分",转账总金额必须与批次内所有转账明细金额之和保持一致,否则无法发起转账操作
+ */
+ @SerializedName("total_amount")
+ private Integer totalAmount;
+
+ /**
+ * 【转账总笔数】 转账总笔数,需要与批次内所有转账明细笔数保持一致,否则无法发起转账操作
+ */
+ @SerializedName("total_num")
+ private Integer totalNum;
+
+ /**
+ * 【转账明细列表】 转账明细列表,最多50条
+ */
+ @SerializedName("transfer_detail_list")
+ private List transferDetailList;
+
+ /**
+ * 【异步回调地址】 异步接收微信支付结果通知的回调地址,通知url必须为公网可访问的url,必须为https,不能携带参数
+ */
+ @SerializedName("notify_url")
+ private String notifyUrl;
+
+ /**
+ * 转账明细
+ */
+ @Data
+ @Builder(builderMethodName = "newBuilder")
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class TransferDetail implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 【商户明细单号】 商户系统内部区分转账批次单下不同转账明细单的唯一标识,要求此参数只能由数字、大小写字母组成
+ */
+ @SerializedName("out_detail_no")
+ private String outDetailNo;
+
+ /**
+ * 【转账金额】 转账金额单位为"分"
+ */
+ @SerializedName("transfer_amount")
+ private Integer transferAmount;
+
+ /**
+ * 【转账备注】 单条转账备注(微信用户会收到该备注),UTF8编码,最多允许32个字符
+ */
+ @SerializedName("transfer_remark")
+ private String transferRemark;
+
+ /**
+ * 【收款用户OpenID】 商户AppID下,某用户的OpenID
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ * 【收款用户姓名】 收款方真实姓名。支持标准RSA算法和国密算法,公钥由微信侧提供
+ */
+ @SpecEncrypt
+ @SerializedName("user_name")
+ private String userName;
+
+ /**
+ * 【转账场景报备信息】
+ */
+ @SerializedName("transfer_scene_report_infos")
+ private List transferSceneReportInfos;
+ }
+
+ /**
+ * 转账场景报备信息
+ */
+ @Data
+ @Builder(builderMethodName = "newBuilder")
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class TransferSceneReportInfo implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 【信息类型】 信息类型编码
+ */
+ @SerializedName("info_type")
+ private String infoType;
+
+ /**
+ * 【信息内容】 信息内容
+ */
+ @SerializedName("info_content")
+ private String infoContent;
+ }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/ReservationTransferBatchResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/ReservationTransferBatchResult.java
new file mode 100644
index 0000000000..ef762cee5b
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/ReservationTransferBatchResult.java
@@ -0,0 +1,51 @@
+package com.github.binarywang.wxpay.bean.transfer;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ *
+ * 批量预约商家转账响应结果
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4015901167
+ *
+ *
+ * @author wanggang
+ * created on 2025/11/28
+ */
+@Data
+@NoArgsConstructor
+public class ReservationTransferBatchResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 【商户预约批次单号】 商户系统内部的商家预约批次单号
+ */
+ @SerializedName("out_batch_no")
+ private String outBatchNo;
+
+ /**
+ * 【微信预约批次单号】 微信预约批次单号,微信商家转账系统返回的唯一标识
+ */
+ @SerializedName("reservation_batch_no")
+ private String reservationBatchNo;
+
+ /**
+ * 【批次创建时间】 批次受理成功时返回
+ * 遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE
+ */
+ @SerializedName("create_time")
+ private String createTime;
+
+ /**
+ * 【批次状态】
+ * ACCEPTED: 批次已受理
+ * PROCESSING: 批次处理中
+ * FINISHED: 批次处理完成
+ * CLOSED: 批次已关闭
+ */
+ @SerializedName("batch_state")
+ private String batchState;
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/ReservationTransferNotifyResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/ReservationTransferNotifyResult.java
new file mode 100644
index 0000000000..438354e7bd
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/ReservationTransferNotifyResult.java
@@ -0,0 +1,173 @@
+package com.github.binarywang.wxpay.bean.transfer;
+
+import com.github.binarywang.wxpay.bean.notify.OriginNotifyResponse;
+import com.github.binarywang.wxpay.bean.notify.WxPayBaseNotifyV3Result;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ *
+ * 预约商家转账通知回调结果
+ * 预约批次单中的明细单在转账成功或转账失败时,微信会把相关结果信息发送给商户。
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4015901167
+ *
+ *
+ * @author wanggang
+ * created on 2025/11/28
+ */
+@Data
+public class ReservationTransferNotifyResult implements Serializable, WxPayBaseNotifyV3Result {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 源数据
+ */
+ private OriginNotifyResponse rawData;
+
+ /**
+ * 解密后的数据
+ */
+ private ReservationTransferNotifyResult.DecryptNotifyResult result;
+
+ @Data
+ @NoArgsConstructor
+ public static class DecryptNotifyResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 【商户号】 微信支付分配的商户号
+ */
+ @SerializedName("mch_id")
+ private String mchId;
+
+ /**
+ * 【商户预约批次单号】 商户系统内部的商家预约批次单号
+ */
+ @SerializedName("out_batch_no")
+ private String outBatchNo;
+
+ /**
+ * 【微信预约批次单号】 微信预约批次单号,微信商家转账系统返回的唯一标识
+ */
+ @SerializedName("reservation_batch_no")
+ private String reservationBatchNo;
+
+ /**
+ * 【批次状态】
+ * ACCEPTED: 批次已受理
+ * PROCESSING: 批次处理中
+ * FINISHED: 批次处理完成
+ * CLOSED: 批次已关闭
+ */
+ @SerializedName("batch_state")
+ private String batchState;
+
+ /**
+ * 【转账总金额】 转账金额单位为"分"
+ */
+ @SerializedName("total_amount")
+ private Integer totalAmount;
+
+ /**
+ * 【转账总笔数】 转账总笔数
+ */
+ @SerializedName("total_num")
+ private Integer totalNum;
+
+ /**
+ * 【转账成功金额】 转账成功金额单位为"分"
+ */
+ @SerializedName("success_amount")
+ private Integer successAmount;
+
+ /**
+ * 【转账成功笔数】 转账成功笔数
+ */
+ @SerializedName("success_num")
+ private Integer successNum;
+
+ /**
+ * 【转账失败金额】 转账失败金额单位为"分"
+ */
+ @SerializedName("fail_amount")
+ private Integer failAmount;
+
+ /**
+ * 【转账失败笔数】 转账失败笔数
+ */
+ @SerializedName("fail_num")
+ private Integer failNum;
+
+ /**
+ * 【批次创建时间】 批次受理成功时返回
+ * 遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE
+ */
+ @SerializedName("create_time")
+ private String createTime;
+
+ /**
+ * 【批次更新时间】 批次最后更新时间
+ * 遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE
+ */
+ @SerializedName("update_time")
+ private String updateTime;
+
+ /**
+ * 【转账明细列表】
+ */
+ @SerializedName("transfer_detail_list")
+ private List transferDetailList;
+ }
+
+ /**
+ * 转账明细通知
+ */
+ @Data
+ @NoArgsConstructor
+ public static class TransferDetailNotify implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 【商户明细单号】 商户系统内部区分转账批次单下不同转账明细单的唯一标识
+ */
+ @SerializedName("out_detail_no")
+ private String outDetailNo;
+
+ /**
+ * 【微信转账单号】 微信转账单号,微信商家转账系统返回的唯一标识
+ */
+ @SerializedName("transfer_bill_no")
+ private String transferBillNo;
+
+ /**
+ * 【明细状态】
+ * PROCESSING: 转账处理中
+ * SUCCESS: 转账成功
+ * FAIL: 转账失败
+ */
+ @SerializedName("detail_state")
+ private String detailState;
+
+ /**
+ * 【收款用户OpenID】 商户AppID下,某用户的OpenID
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ * 【转账金额】 转账金额单位为"分"
+ */
+ @SerializedName("transfer_amount")
+ private Integer transferAmount;
+
+ /**
+ * 【失败原因】 转账失败原因
+ */
+ @SerializedName("fail_reason")
+ private String failReason;
+ }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/TransferBillsRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/TransferBillsRequest.java
index 230e564e4b..2ac4b08c93 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/TransferBillsRequest.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/TransferBillsRequest.java
@@ -87,6 +87,26 @@ public class TransferBillsRequest implements Serializable {
@SerializedName("transfer_scene_report_infos")
private List transferSceneReportInfos;
+ /**
+ * 收款授权模式
+ *
+ * 字段名:收款授权模式
+ * 变量名:receipt_authorization_mode
+ * 是否必填:否
+ * 类型:string
+ * 描述:
+ * 控制收款方式的授权模式,可选值:
+ * - CONFIRM_RECEIPT_AUTHORIZATION:需确认收款授权模式(默认值)
+ * - NO_CONFIRM_RECEIPT_AUTHORIZATION:免确认收款授权模式(需要用户事先授权)
+ * 为空时,默认为需确认收款授权模式
+ * 示例值:NO_CONFIRM_RECEIPT_AUTHORIZATION
+ *
+ *
+ * @see com.github.binarywang.wxpay.constant.WxPayConstants.ReceiptAuthorizationMode
+ */
+ @SerializedName("receipt_authorization_mode")
+ private String receiptAuthorizationMode;
+
@Data
@Builder(builderMethodName = "newBuilder")
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/UserAuthorizationStatusResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/UserAuthorizationStatusResult.java
new file mode 100644
index 0000000000..e0bab82150
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/UserAuthorizationStatusResult.java
@@ -0,0 +1,63 @@
+package com.github.binarywang.wxpay.bean.transfer;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ *
+ * 商户查询用户授权信息接口响应结果
+ * 商户通过此接口可查询用户是否对商户的商家转账场景进行了授权。
+ * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4015901167
+ *
+ *
+ * @author wanggang
+ * created on 2025/11/28
+ */
+@Data
+@NoArgsConstructor
+public class UserAuthorizationStatusResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 【商户AppID】 商户在微信申请公众号或移动应用成功后分配的账号ID
+ */
+ @SerializedName("appid")
+ private String appid;
+
+ /**
+ * 【商户号】 微信支付分配的商户号
+ */
+ @SerializedName("mch_id")
+ private String mchId;
+
+ /**
+ * 【用户标识】 用户在直连商户应用下的用户标识
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ * 【授权状态】 用户授权状态
+ * UNAUTHORIZED: 未授权
+ * AUTHORIZED: 已授权
+ */
+ @SerializedName("authorization_state")
+ private String authorizationState;
+
+ /**
+ * 【授权时间】 用户授权时间,遵循rfc3339标准格式
+ * 格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE
+ */
+ @SerializedName("authorize_time")
+ private String authorizeTime;
+
+ /**
+ * 【取消授权时间】 用户取消授权时间,遵循rfc3339标准格式
+ * 格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE
+ */
+ @SerializedName("deauthorize_time")
+ private String deauthorizeTime;
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/VerifierBuilder.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/VerifierBuilder.java
index b0d9276a32..4c8aafb8ee 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/VerifierBuilder.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/VerifierBuilder.java
@@ -6,6 +6,8 @@
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.PublicKey;
@@ -118,8 +120,19 @@ private static AutoUpdateCertificatesVerifier getCertificatesVerifier(
String certSerialNo, String mchId, String apiV3Key, PrivateKey merchantPrivateKey,
WxPayHttpProxy wxPayHttpProxy, int certAutoUpdateTime, String payBaseUrl
) {
+ String signUriStripPrefix = null;
+ if (StringUtils.isNotBlank(payBaseUrl)) {
+ try {
+ String rawPath = new URI(payBaseUrl).getRawPath();
+ if (StringUtils.isNotBlank(rawPath) && !"/".equals(rawPath)) {
+ signUriStripPrefix = rawPath;
+ }
+ } catch (URISyntaxException ignored) {
+ // ignore
+ }
+ }
return new AutoUpdateCertificatesVerifier(
- new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)),
+ new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey), signUriStripPrefix),
apiV3Key.getBytes(StandardCharsets.UTF_8), certAutoUpdateTime,
payBaseUrl, wxPayHttpProxy);
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java
index 43da17f048..1db2e06306 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java
@@ -32,7 +32,10 @@
import javax.net.ssl.SSLContext;
import java.io.*;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.net.URL;
+import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
@@ -54,12 +57,19 @@ public class WxPayConfig {
private static final String DEFAULT_PAY_BASE_URL = "https://api.mch.weixin.qq.com";
private static final String PROBLEM_MSG = "证书文件【%s】有问题,请核实!";
private static final String NOT_FOUND_MSG = "证书文件【%s】不存在,请核实!";
+ private static final String CERT_NAME_P12 = "p12证书";
/**
* 微信支付接口请求地址域名部分.
*/
private String apiHostUrl = DEFAULT_PAY_BASE_URL;
+ /**
+ * 微信支付接口请求地址路径前缀(用于网关代理前缀).
+ * 例如:/api-weixin
+ */
+ private String apiHostUrlPath;
+
/**
* http请求连接超时时间.
*/
@@ -95,9 +105,13 @@ public class WxPayConfig {
*/
private String subMchId;
/**
- * 微信支付异步回掉地址,通知url必须为直接可访问的url,不能携带参数.
+ * 微信支付异步回调地址,通知url必须为直接可访问的url,不能携带参数.
*/
private String notifyUrl;
+ /**
+ * 退款结果异步回调地址,通知url必须为直接可访问的url,不能携带参数.
+ */
+ private String refundNotifyUrl;
/**
* 交易类型.
*
@@ -262,14 +276,14 @@ public class WxPayConfig {
private Verifier verifier;
/**
- * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认不添加
+ * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加
*/
- private boolean strictlyNeedWechatPaySerial = false;
+ private boolean strictlyNeedWechatPaySerial = true;
/**
- * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认不使用
+ * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认使用
*/
- private boolean fullPublicKeyModel = false;
+ private boolean fullPublicKeyModel = true;
/**
* 返回所设置的微信支付接口请求地址域名.
@@ -277,11 +291,42 @@ public class WxPayConfig {
* @return 微信支付接口请求地址域名
*/
public String getApiHostUrl() {
- if (StringUtils.isEmpty(this.apiHostUrl)) {
+ String hostUrl = StringUtils.trimToNull(this.apiHostUrl);
+ if (hostUrl == null) {
return DEFAULT_PAY_BASE_URL;
}
+ if (hostUrl.endsWith("/")) {
+ hostUrl = hostUrl.substring(0, hostUrl.length() - 1);
+ }
+ return hostUrl;
+ }
- return this.apiHostUrl;
+ /**
+ * 返回所设置的微信支付接口路径前缀.
+ *
+ * @return 路径前缀,不配置时为空字符串
+ */
+ public String getApiHostUrlPath() {
+ String pathPrefix = StringUtils.trimToNull(this.apiHostUrlPath);
+ if (pathPrefix == null || "/".equals(pathPrefix)) {
+ return "";
+ }
+ if (!pathPrefix.startsWith("/")) {
+ pathPrefix = "/" + pathPrefix;
+ }
+ if (pathPrefix.endsWith("/")) {
+ pathPrefix = pathPrefix.substring(0, pathPrefix.length() - 1);
+ }
+ return pathPrefix;
+ }
+
+ /**
+ * 返回用于请求层拼接的基础地址:host + pathPrefix.
+ *
+ * @return 拼接后的基础地址
+ */
+ public String getApiHostWithPathPrefix() {
+ return this.getApiHostUrl() + this.getApiHostUrlPath();
}
@SneakyThrows
@@ -305,7 +350,7 @@ public SSLContext initSSLContext() throws WxPayException {
}
try (InputStream inputStream = this.loadConfigInputStream(this.keyString, this.getKeyPath(),
- this.keyContent, "p12证书")) {
+ this.keyContent, CERT_NAME_P12)) {
KeyStore keystore = KeyStore.getInstance("PKCS12");
char[] partnerId2charArray = this.getMchId().toCharArray();
keystore.load(inputStream, partnerId2charArray);
@@ -323,7 +368,8 @@ public SSLContext initSSLContext() throws WxPayException {
*
* @return org.apache.http.impl.client.CloseableHttpClient
* @author doger.wang
- **/
+ * @throws WxPayException 微信支付异常
+ */
public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
if (StringUtils.isBlank(this.getApiV3Key())) {
throw new WxPayException("请确保apiV3Key值已设置");
@@ -341,7 +387,7 @@ public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
certificate = (X509Certificate) objects[1];
this.certSerialNo = certificate.getSerialNumber().toString(16).toUpperCase();
}
- if (certificate == null && StringUtils.isBlank(this.getCertSerialNo()) && StringUtils.isNotBlank(this.getPrivateCertPath())) {
+ if (certificate == null && StringUtils.isBlank(this.getCertSerialNo()) && (StringUtils.isNotBlank(this.getPrivateCertPath()) || StringUtils.isNotBlank(this.getPrivateCertString()) || this.getPrivateCertContent() != null)) {
try (InputStream certInputStream = this.loadConfigInputStream(this.getPrivateCertString(), this.getPrivateCertPath(),
this.privateCertContent, "privateCertPath")) {
certificate = PemUtils.loadCertificate(certInputStream);
@@ -349,7 +395,7 @@ public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
this.certSerialNo = certificate.getSerialNumber().toString(16).toUpperCase();
}
- if (this.getPublicKeyString() != null || this.getPublicKeyPath() != null || this.publicKeyContent != null) {
+ if (StringUtils.isNotBlank(this.getPublicKeyString()) || StringUtils.isNotBlank(this.getPublicKeyPath()) || this.publicKeyContent != null) {
if (StringUtils.isBlank(this.getPublicKeyId())) {
throw new WxPayException("请确保和publicKeyId配套使用");
}
@@ -375,16 +421,33 @@ public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
Verifier certificatesVerifier;
if (this.fullPublicKeyModel) {
// 使用完全公钥模式时,只加载公钥相关配置,避免下载平台证书使灰度切换无法达到100%覆盖
+ if (publicKey == null) {
+ throw new WxPayException("完全公钥模式下,请确保公钥配置(publicKeyPath/publicKeyString/publicKeyContent)及publicKeyId已设置");
+ }
certificatesVerifier = VerifierBuilder.buildPublicCertVerifier(this.publicKeyId, publicKey);
} else {
certificatesVerifier = VerifierBuilder.build(
this.getCertSerialNo(), this.getMchId(), this.getApiV3Key(), merchantPrivateKey, wxPayHttpProxy,
- this.getCertAutoUpdateTime(), this.getApiHostUrl(), this.getPublicKeyId(), publicKey);
+ this.getCertAutoUpdateTime(), this.getApiHostWithPathPrefix(), this.getPublicKeyId(), publicKey);
}
WxPayV3HttpClientBuilder wxPayV3HttpClientBuilder = WxPayV3HttpClientBuilder.create()
+ .withSignUriStripPrefix(this.getApiHostUrlPath())
.withMerchant(mchId, certSerialNo, merchantPrivateKey)
.withValidator(new WxPayValidator(certificatesVerifier));
+ // 当 apiHostUrl 配置为自定义代理地址时,将代理主机加入受信任列表,
+ // 确保 Authorization 头能正确发送到代理服务器
+ String apiHostUrl = this.getApiHostUrl();
+ if (StringUtils.isNotBlank(apiHostUrl)) {
+ try {
+ String host = new URI(apiHostUrl).getHost();
+ if (host != null && !host.endsWith(".mch.weixin.qq.com")) {
+ wxPayV3HttpClientBuilder.withTrustedHost(host);
+ }
+ } catch (URISyntaxException e) {
+ log.warn("解析 apiHostUrl [{}] 中的主机名失败: {}", apiHostUrl, e.getMessage());
+ }
+ }
//初始化V3接口正向代理设置
HttpProxyUtils.initHttpProxy(wxPayV3HttpClientBuilder, wxPayHttpProxy);
@@ -435,7 +498,36 @@ private InputStream loadConfigInputStream(String configString, String configPath
}
if (StringUtils.isNotEmpty(configString)) {
- configContent = Base64.getDecoder().decode(configString);
+ // 判断是否为PEM格式的字符串(包含-----BEGIN和-----END标记)
+ if (isPemFormat(configString)) {
+ // PEM格式直接转为字节流,让PemUtils处理
+ configContent = configString.getBytes(StandardCharsets.UTF_8);
+ } else {
+ // 尝试Base64解码
+ try {
+ byte[] decoded = Base64.getDecoder().decode(configString);
+ // 检查解码后的内容是否为PEM格式(即用户传入的是base64编码的完整PEM文件)
+ String decodedString = new String(decoded, StandardCharsets.UTF_8);
+ if (isPemFormat(decodedString)) {
+ // 解码后是PEM格式,使用解码后的内容
+ configContent = decoded;
+ } else {
+ // 解码后不是PEM格式,可能是:
+ // 1. p12证书的二进制内容 - 应该返回解码后的二进制数据
+ // 2. 私钥/公钥的纯base64内容(不含PEM头尾) - 应该返回原始字符串,让PemUtils处理
+ // 通过certName区分:p12证书使用解码后的数据,其他情况返回原始字符串
+ if (CERT_NAME_P12.equals(certName)) {
+ configContent = decoded;
+ } else {
+ // 对于私钥/公钥/证书,返回原始字符串字节,让PemUtils处理base64解码
+ configContent = configString.getBytes(StandardCharsets.UTF_8);
+ }
+ }
+ } catch (IllegalArgumentException e) {
+ // Base64解码失败,可能是格式不正确,抛出异常
+ throw new WxPayException(String.format("【%s】的Base64格式不正确", certName), e);
+ }
+ }
return new ByteArrayInputStream(configContent);
}
@@ -446,6 +538,16 @@ private InputStream loadConfigInputStream(String configString, String configPath
return this.loadConfigInputStream(configPath);
}
+ /**
+ * 判断字符串是否为PEM格式(包含-----BEGIN和-----END标记)
+ *
+ * @param content 要检查的字符串
+ * @return 是否为PEM格式
+ */
+ private boolean isPemFormat(String content) {
+ return content != null && content.contains("-----BEGIN") && content.contains("-----END");
+ }
+
/**
* 从配置路径 加载配置 信息(支持 classpath、本地路径、网络url)
@@ -515,7 +617,7 @@ private Object[] p12ToPem() {
// 分解p12证书文件
try (InputStream inputStream = this.loadConfigInputStream(this.keyString, this.getKeyPath(),
- this.keyContent, "p12证书")) {
+ this.keyContent, CERT_NAME_P12)) {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(inputStream, key.toCharArray());
@@ -619,6 +721,8 @@ public CloseableHttpClient initSslHttpClient() throws WxPayException {
/**
* 配置HTTP代理
+ *
+ * @param httpClientBuilder HttpClient构建器
*/
private void configureProxy(org.apache.http.impl.client.HttpClientBuilder httpClientBuilder) {
if (StringUtils.isNotBlank(this.getHttpProxyHost()) && this.getHttpProxyPort() > 0) {
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/constant/WxPayConstants.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/constant/WxPayConstants.java
index e8a6b6acb3..2b736691b7 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/constant/WxPayConstants.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/constant/WxPayConstants.java
@@ -401,6 +401,71 @@ public static class TransformBillState {
}
+
+ /**
+ * 用户授权状态
+ *
+ * @see 商户查询用户授权信息
+ */
+ @UtilityClass
+ public static class AuthorizationState {
+ /**
+ * 未授权
+ */
+ public static final String UNAUTHORIZED = "UNAUTHORIZED";
+
+ /**
+ * 已授权
+ */
+ public static final String AUTHORIZED = "AUTHORIZED";
+ }
+
+ /**
+ * 预约转账批次状态
+ *
+ * @see 批量预约商家转账
+ */
+ @UtilityClass
+ public static class ReservationBatchState {
+ /**
+ * 批次已受理
+ */
+ public static final String ACCEPTED = "ACCEPTED";
+
+ /**
+ * 批次处理中
+ */
+ public static final String PROCESSING = "PROCESSING";
+
+ /**
+ * 批次处理完成
+ */
+ public static final String FINISHED = "FINISHED";
+
+ /**
+ * 批次已关闭
+ */
+ public static final String CLOSED = "CLOSED";
+ }
+
+ /**
+ * 预约转账批次关闭原因
+ *
+ * @see 预约转账批次单号查询
+ */
+ @UtilityClass
+ public static class ReservationBatchCloseReason {
+ /**
+ * 商户主动撤销
+ */
+ public static final String MERCHANT_REVOCATION = "MERCHANT_REVOCATION";
+
+ /**
+ * 系统超时关闭
+ */
+ public static final String OVERDUE_CLOSE = "OVERDUE_CLOSE";
+ }
+
/**
* 【转账场景ID】 该笔转账使用的转账场景,可前往“商户平台-产品中心-商家转账”中申请。
*/
@@ -412,6 +477,29 @@ public static class TransformSceneId {
public static final String CASH_MARKETING = "1001";
}
+ /**
+ * 【运营工具转账场景ID】 运营工具专用转账场景,用于商户日常运营活动
+ *
+ * @see 运营工具-商家转账API
+ */
+ @UtilityClass
+ public static class OperationSceneId {
+ /**
+ * 运营工具现金营销
+ */
+ public static final String OPERATION_CASH_MARKETING = "2001";
+
+ /**
+ * 运营工具佣金报酬
+ */
+ public static final String OPERATION_COMMISSION = "2002";
+
+ /**
+ * 运营工具推广奖励
+ */
+ public static final String OPERATION_PROMOTION = "2003";
+ }
+
/**
* 用户收款感知
*
@@ -436,4 +524,25 @@ public static class CASH_MARKETING {
}
}
+
+ /**
+ * 收款授权模式
+ *
+ * @see 官方文档
+ */
+ @UtilityClass
+ public static class ReceiptAuthorizationMode {
+ /**
+ * 需确认收款授权模式(默认值)
+ * 用户需要手动确认才能收款
+ */
+ public static final String CONFIRM_RECEIPT_AUTHORIZATION = "CONFIRM_RECEIPT_AUTHORIZATION";
+
+ /**
+ * 免确认收款授权模式
+ * 用户授权后,收款不需要确认,转账直接到账
+ */
+ public static final String NO_CONFIRM_RECEIPT_AUTHORIZATION = "NO_CONFIRM_RECEIPT_AUTHORIZATION";
+ }
+
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/BusinessOperationTransferExample.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/BusinessOperationTransferExample.java
new file mode 100644
index 0000000000..117395ba62
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/BusinessOperationTransferExample.java
@@ -0,0 +1,143 @@
+package com.github.binarywang.wxpay.example;
+
+import com.github.binarywang.wxpay.bean.transfer.*;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.constant.WxPayConstants;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.BusinessOperationTransferService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+
+import java.util.Arrays;
+
+/**
+ * 运营工具-商家转账API使用示例
+ *
+ * 微信支付为商户提供的运营工具转账能力,用于商户的日常运营活动中进行转账操作
+ *
+ * @author WxJava Team
+ * @see 运营工具-商家转账API
+ */
+public class BusinessOperationTransferExample {
+
+ private WxPayService wxPayService;
+ private BusinessOperationTransferService businessOperationTransferService;
+
+ public void init() {
+ // 初始化配置
+ WxPayConfig config = new WxPayConfig();
+ config.setAppId("your_app_id");
+ config.setMchId("your_mch_id");
+ config.setMchKey("your_mch_key");
+ config.setKeyPath("path_to_your_cert.p12");
+
+ // 初始化服务
+ wxPayService = new WxPayServiceImpl();
+ wxPayService.setConfig(config);
+ businessOperationTransferService = wxPayService.getBusinessOperationTransferService();
+ }
+
+ /**
+ * 发起运营工具转账示例
+ */
+ public void createOperationTransferExample() {
+ try {
+ // 构建转账请求
+ BusinessOperationTransferRequest.TransferSceneReportInfo reportInfo = new BusinessOperationTransferRequest.TransferSceneReportInfo();
+ reportInfo.setInfoType("活动名称");
+ reportInfo.setInfoContent("新会员有礼");
+
+ BusinessOperationTransferRequest request = BusinessOperationTransferRequest.newBuilder()
+ .appid("your_app_id") // 应用ID
+ .outBillNo("OT" + System.currentTimeMillis()) // 商户转账单号
+ .transferSceneId(WxPayConstants.OperationSceneId.OPERATION_CASH_MARKETING) // 运营工具转账场景ID
+ .transferSceneReportInfos(Arrays.asList(reportInfo)) // 转账场景报备信息
+ .openid("user_openid") // 用户openid
+ .userName("张三") // 用户姓名(可选)
+ .transferAmount(100) // 转账金额,单位分
+ .transferRemark("运营活动奖励") // 转账备注
+ .userRecvPerception(WxPayConstants.UserRecvPerception.CASH_MARKETING.CASH) // 用户收款感知
+ .notifyUrl("https://your-domain.com/notify") // 回调通知地址
+ .build();
+
+ // 发起转账
+ BusinessOperationTransferResult result = businessOperationTransferService.createOperationTransfer(request);
+
+ System.out.println("转账成功!");
+ System.out.println("商户单号: " + result.getOutBillNo());
+ System.out.println("微信转账单号: " + result.getTransferBillNo());
+ System.out.println("单据状态: " + result.getState());
+ System.out.println("跳转领取页面的package信息: " + result.getPackageInfo());
+ System.out.println("创建时间: " + result.getCreateTime());
+
+ } catch (WxPayException e) {
+ System.err.println("转账失败: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 通过商户单号查询转账结果示例
+ */
+ public void queryByOutBillNoExample() {
+ try {
+ String outBillNo = "OT1640995200000"; // 商户转账单号
+
+ BusinessOperationTransferQueryResult result = businessOperationTransferService
+ .queryOperationTransferByOutBillNo(outBillNo);
+
+ System.out.println("查询成功!");
+ System.out.println("商户单号: " + result.getOutBillNo());
+ System.out.println("微信转账单号: " + result.getTransferBillNo());
+ System.out.println("转账状态: " + result.getTransferState());
+ System.out.println("转账金额: " + result.getTransferAmount() + "分");
+ System.out.println("创建时间: " + result.getCreateTime());
+ System.out.println("更新时间: " + result.getUpdateTime());
+
+ } catch (WxPayException e) {
+ System.err.println("查询失败: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 通过微信转账单号查询转账结果示例
+ */
+ public void queryByTransferBillNoExample() {
+ try {
+ String transferBillNo = "1040000071100999991182020050700019480001"; // 微信转账单号
+
+ BusinessOperationTransferQueryResult result = businessOperationTransferService
+ .queryOperationTransferByTransferBillNo(transferBillNo);
+
+ System.out.println("查询成功!");
+ System.out.println("商户单号: " + result.getOutBillNo());
+ System.out.println("微信转账单号: " + result.getTransferBillNo());
+ System.out.println("运营场景ID: " + result.getOperationSceneId());
+ System.out.println("转账状态: " + result.getTransferState());
+
+ } catch (WxPayException e) {
+ System.err.println("查询失败: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 使用配置示例
+ */
+ public static void main(String[] args) {
+ BusinessOperationTransferExample example = new BusinessOperationTransferExample();
+
+ // 初始化配置
+ example.init();
+
+ // 1. 发起运营工具转账
+ example.createOperationTransferExample();
+
+ // 2. 查询转账结果
+ // example.queryByOutBillNoExample();
+
+ // 3. 通过微信转账单号查询
+ // example.queryByTransferBillNoExample();
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/NewTransferApiExample.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/NewTransferApiExample.java
index 8d74e5a4ef..228234d589 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/NewTransferApiExample.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/NewTransferApiExample.java
@@ -3,6 +3,7 @@
import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.bean.transfer.*;
import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.TransferService;
import com.github.binarywang.wxpay.service.WxPayService;
@@ -215,6 +216,100 @@ public void batchTransferExample() {
}
}
+ /**
+ * 使用免确认收款授权模式进行转账示例
+ * 注意:使用此模式前,用户需要先进行授权
+ */
+ public void transferWithNoConfirmAuthModeExample() {
+ try {
+ // 构建转账请求,使用免确认收款授权模式
+ TransferBillsRequest request = TransferBillsRequest.newBuilder()
+ .appid("wx1234567890123456")
+ .outBillNo("NO_CONFIRM_" + System.currentTimeMillis()) // 商户转账单号
+ .transferSceneId("1005") // 转账场景ID(佣金报酬)
+ .openid("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o") // 收款用户的openid
+ .transferAmount(200) // 转账金额,单位:分(此处为2元)
+ .transferRemark("免确认收款转账") // 转账备注
+ .receiptAuthorizationMode(WxPayConstants.ReceiptAuthorizationMode.NO_CONFIRM_RECEIPT_AUTHORIZATION)
+ .userRecvPerception("Y") // 用户收款感知
+ .build();
+
+ // 发起转账
+ TransferBillsResult result = transferService.transferBills(request);
+
+ System.out.println("=== 免确认授权模式转账成功 ===");
+ System.out.println("商户单号: " + result.getOutBillNo());
+ System.out.println("微信转账单号: " + result.getTransferBillNo());
+ System.out.println("状态: " + result.getState());
+ System.out.println("说明: 使用免确认授权模式,转账直接到账,无需用户确认");
+
+ } catch (WxPayException e) {
+ System.err.println("免确认授权转账失败: " + e.getMessage());
+ System.err.println("错误代码: " + e.getErrCode());
+
+ // 可能的错误原因
+ if ("USER_NOT_AUTHORIZED".equals(e.getErrCode())) {
+ System.err.println("用户未授权免确认收款,请先引导用户进行授权");
+ }
+ }
+ }
+
+ /**
+ * 使用需确认收款授权模式进行转账示例(默认模式)
+ */
+ public void transferWithConfirmAuthModeExample() {
+ try {
+ // 构建转账请求,显式设置为需确认收款授权模式
+ TransferBillsRequest request = TransferBillsRequest.newBuilder()
+ .appid("wx1234567890123456")
+ .outBillNo("CONFIRM_" + System.currentTimeMillis()) // 商户转账单号
+ .transferSceneId("1005") // 转账场景ID
+ .openid("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o") // 收款用户的openid
+ .transferAmount(150) // 转账金额,单位:分(此处为1.5元)
+ .transferRemark("需确认收款转账") // 转账备注
+ .receiptAuthorizationMode(WxPayConstants.ReceiptAuthorizationMode.CONFIRM_RECEIPT_AUTHORIZATION)
+ .userRecvPerception("Y") // 用户收款感知
+ .build();
+
+ // 发起转账
+ TransferBillsResult result = transferService.transferBills(request);
+
+ System.out.println("=== 需确认授权模式转账成功 ===");
+ System.out.println("商户单号: " + result.getOutBillNo());
+ System.out.println("微信转账单号: " + result.getTransferBillNo());
+ System.out.println("状态: " + result.getState());
+ System.out.println("packageInfo: " + result.getPackageInfo());
+ System.out.println("说明: 使用需确认授权模式,用户需要手动确认才能收款");
+
+ } catch (WxPayException e) {
+ System.err.println("需确认授权转账失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 权限模式对比示例
+ * 展示两种权限模式的区别和使用场景
+ */
+ public void authModeComparisonExample() {
+ System.out.println("\n=== 收款授权模式对比 ===");
+ System.out.println("1. 需确认收款授权模式 (CONFIRM_RECEIPT_AUTHORIZATION):");
+ System.out.println(" - 这是默认模式");
+ System.out.println(" - 用户收到转账后需要手动点击确认才能到账");
+ System.out.println(" - 适用于一般的转账场景");
+ System.out.println(" - 转账状态可能包含 WAIT_USER_CONFIRM 等待确认状态");
+
+ System.out.println("\n2. 免确认收款授权模式 (NO_CONFIRM_RECEIPT_AUTHORIZATION):");
+ System.out.println(" - 用户事先授权后,转账直接到账,无需确认");
+ System.out.println(" - 提升用户体验,减少操作步骤");
+ System.out.println(" - 适用于高频转账场景,如佣金发放等");
+ System.out.println(" - 需要用户先进行授权,否则会返回授权错误");
+
+ System.out.println("\n使用建议:");
+ System.out.println("- 高频业务场景推荐使用免确认模式,提升用户体验");
+ System.out.println("- 首次使用需引导用户进行授权");
+ System.out.println("- 处理授权相关异常,提供友好的错误提示");
+ }
+
/**
* 使用配置示例
*/
@@ -230,20 +325,29 @@ public static void main(String[] args) {
// 创建示例实例
NewTransferApiExample example = new NewTransferApiExample(config);
+ // 权限模式对比说明
+ example.authModeComparisonExample();
+
// 运行示例
System.out.println("新版商户转账API使用示例");
System.out.println("===============================");
- // 1. 发起单笔转账
+ // 1. 发起转账(使用免确认授权模式)
+ // example.transferWithNoConfirmAuthModeExample();
+
+ // 2. 发起转账(使用需确认授权模式)
+ // example.transferWithConfirmAuthModeExample();
+
+ // 3. 发起单笔转账(默认模式)
example.transferExample();
- // 2. 查询转账结果
+ // 4. 查询转账结果
// example.queryByOutBillNoExample();
- // 3. 撤销转账
+ // 5. 撤销转账
// example.cancelTransferExample();
- // 4. 批量转账(传统API)
+ // 6. 批量转账(传统API)
// example.batchTransferExample();
}
}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessCircleService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessCircleService.java
index 21af39ae16..7fef47ed23 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessCircleService.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessCircleService.java
@@ -4,7 +4,7 @@
import com.github.binarywang.wxpay.bean.businesscircle.PaidResult;
import com.github.binarywang.wxpay.bean.businesscircle.PointsNotifyRequest;
import com.github.binarywang.wxpay.bean.businesscircle.RefundResult;
-import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.exception.WxPayException;
/**
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessOperationTransferService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessOperationTransferService.java
new file mode 100644
index 0000000000..195d3a8409
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessOperationTransferService.java
@@ -0,0 +1,82 @@
+package com.github.binarywang.wxpay.service;
+
+import com.github.binarywang.wxpay.bean.transfer.BusinessOperationTransferRequest;
+import com.github.binarywang.wxpay.bean.transfer.BusinessOperationTransferResult;
+import com.github.binarywang.wxpay.bean.transfer.BusinessOperationTransferQueryRequest;
+import com.github.binarywang.wxpay.bean.transfer.BusinessOperationTransferQueryResult;
+import com.github.binarywang.wxpay.exception.WxPayException;
+
+/**
+ * 运营工具-商家转账API
+ *
+ * 微信支付为商户提供的运营工具转账能力,用于商户的日常运营活动中进行转账操作
+ *
+ * @author WxJava Team
+ * @see 运营工具-商家转账API
+ */
+public interface BusinessOperationTransferService {
+
+ /**
+ *
+ * 发起运营工具商家转账
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills
+ *
+ * 文档地址:运营工具-商家转账API
+ *
+ *
+ * @param request 运营工具转账请求参数
+ * @return BusinessOperationTransferResult 转账结果
+ * @throws WxPayException 微信支付异常
+ */
+ BusinessOperationTransferResult createOperationTransfer(BusinessOperationTransferRequest request) throws WxPayException;
+
+ /**
+ *
+ * 查询运营工具转账结果
+ *
+ * 请求方式:GET(HTTPS)
+ * 请求地址:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}
+ *
+ * 文档地址:运营工具-商家转账API
+ *
+ *
+ * @param request 查询请求参数
+ * @return BusinessOperationTransferQueryResult 查询结果
+ * @throws WxPayException 微信支付异常
+ */
+ BusinessOperationTransferQueryResult queryOperationTransfer(BusinessOperationTransferQueryRequest request) throws WxPayException;
+
+ /**
+ *
+ * 通过商户单号查询运营工具转账结果
+ *
+ * 请求方式:GET(HTTPS)
+ * 请求地址:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}
+ *
+ * 文档地址:运营工具-商家转账API
+ *
+ *
+ * @param outBillNo 商户单号
+ * @return BusinessOperationTransferQueryResult 查询结果
+ * @throws WxPayException 微信支付异常
+ */
+ BusinessOperationTransferQueryResult queryOperationTransferByOutBillNo(String outBillNo) throws WxPayException;
+
+ /**
+ *
+ * 通过微信转账单号查询运营工具转账结果
+ *
+ * 请求方式:GET(HTTPS)
+ * 请求地址:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/{transfer_bill_no}
+ *
+ * 文档地址:运营工具-商家转账API
+ *
+ *
+ * @param transferBillNo 微信转账单号
+ * @return BusinessOperationTransferQueryResult 查询结果
+ * @throws WxPayException 微信支付异常
+ */
+ BusinessOperationTransferQueryResult queryOperationTransferByTransferBillNo(String transferBillNo) throws WxPayException;
+}
\ 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 b630ce1785..5ef94e531d 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
@@ -3,7 +3,15 @@
import com.github.binarywang.wxpay.bean.ecommerce.*;
import com.github.binarywang.wxpay.bean.ecommerce.enums.FundBillTypeEnum;
import com.github.binarywang.wxpay.bean.ecommerce.enums.SpAccountTypeEnum;
-import com.github.binarywang.wxpay.bean.ecommerce.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.bean.notify.CombineNotifyResult;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
+import com.github.binarywang.wxpay.bean.notify.WxPayPartnerNotifyV3Result;
+import com.github.binarywang.wxpay.bean.request.*;
+import com.github.binarywang.wxpay.bean.result.CombineQueryResult;
+import com.github.binarywang.wxpay.bean.result.CombineTransactionsResult;
+import com.github.binarywang.wxpay.bean.result.WxPayPartnerOrderQueryV3Result;
+import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.exception.WxPayException;
import java.io.File;
@@ -13,7 +21,7 @@
/**
*
* 电商收付通相关服务类.
- * 接口规则:https://wechatpay-api.gitbook.io/wechatpay-api-v3
+ * 产品介绍
*
*
* @author cloudX
@@ -24,7 +32,7 @@ public interface EcommerceService {
*
* 二级商户进件API
* 接口地址: https://api.mch.weixin.qq.com/v3/ecommerce/applyments/
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_1_8.shtml
+ * 接口文档
*
*
*
@@ -38,7 +46,7 @@ public interface EcommerceService {
*
* 查询申请状态API
* 请求URL: https://api.mch.weixin.qq.com/v3/ecommerce/applyments/{applyment_id}
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/applyments/chapter3_2.shtml
+ * 接口文档
*
*
* @param applymentId 申请单ID
@@ -51,7 +59,7 @@ public interface EcommerceService {
*
* 查询申请状态API
* 请求URL: https://api.mch.weixin.qq.com/v3/ecommerce/applyments/out-request-no/{out_request_no}
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/applyments/chapter3_2.shtml
+ * 接口文档
*
*
* @param outRequestNo 业务申请编号
@@ -64,21 +72,21 @@ public interface EcommerceService {
*
* 合单支付API(APP支付、JSAPI支付、H5支付、NATIVE支付).
* 请求URL:https://api.mch.weixin.qq.com/v3/combine-transactions/jsapi
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/e-combine.shtml
+ * 接口文档
*
*
* @param tradeType 支付方式
* @param request 请求对象
- * @return 微信合单支付返回 transactions result
+ * @return 微信合单支付返回 CombineTransactionsResult
* @throws WxPayException the wx pay exception
*/
- TransactionsResult combine(TradeTypeEnum tradeType, CombineTransactionsRequest request) throws WxPayException;
+ CombineTransactionsResult combine(TradeTypeEnum tradeType, CombineTransactionsRequest request) throws WxPayException;
/**
*
* 合单支付API(APP支付、JSAPI支付、H5支付、NATIVE支付).
* 请求URL:https://api.mch.weixin.qq.com/v3/combine-transactions/jsapi
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/e-combine.shtml
+ * 接口文档
*
*
* @param the type parameter
@@ -92,47 +100,59 @@ public interface EcommerceService {
/**
*
* 合单支付通知回调数据处理
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/e-combine.shtml
+ * 接口文档
*
*
* @param notifyData 通知数据
* @param header 通知头部数据,不传则表示不校验头
- * @return 解密后通知数据 combine transactions notify result
+ * @return 解密后通知数据 CombineNotifyResult
* @throws WxPayException the wx pay exception
*/
- CombineTransactionsNotifyResult parseCombineNotifyResult(String notifyData, SignatureHeader header) throws WxPayException;
+ CombineNotifyResult parseCombineNotifyResult(String notifyData, SignatureHeader header) throws WxPayException;
/**
*
* 合单查询订单API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/combine/chapter3_3.shtml
+ * 接口文档
*
*
- * @param outTradeNo 合单商户订单号
+ * @param combineOutTradeNo 合单商户订单号
* @return 支付订单信息
* @throws WxPayException the wx pay exception
*/
- CombineTransactionsResult queryCombineTransactions(String outTradeNo) throws WxPayException;
+ CombineQueryResult queryCombine(String combineOutTradeNo) throws WxPayException;
+
+ /**
+ *
+ * 合单关闭订单API
+ * 请求URL: https://api.mch.weixin.qq.com/v3/combine-transactions/out-trade-no/{combine_out_trade_no}/close
+ * 接口文档
+ *
+ *
+ * @param request 请求对象
+ * @throws WxPayException the wx pay exception
+ */
+ void closeCombine(CombineCloseRequest request) throws WxPayException;
/**
*
* 服务商模式普通支付API(APP支付、JSAPI支付、H5支付、NATIVE支付).
* 请求URL:https://api.mch.weixin.qq.com/v3/pay/partner/transactions/jsapi
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/transactions_sl.shtml
+ * 接口文档
*
*
* @param tradeType 支付方式
* @param request 请求对象
- * @return 调起支付需要的参数 transactions result
+ * @return 调起支付需要的参数 WxPayUnifiedOrderV3Result
* @throws WxPayException the wx pay exception
*/
- TransactionsResult partner(TradeTypeEnum tradeType, PartnerTransactionsRequest request) throws WxPayException;
+ WxPayUnifiedOrderV3Result unifiedPartnerOrder(TradeTypeEnum tradeType, WxPayPartnerUnifiedOrderV3Request request) throws WxPayException;
/**
*
* 服务商模式普通支付API(APP支付、JSAPI支付、H5支付、NATIVE支付).
* 请求URL:https://api.mch.weixin.qq.com/v3/pay/partner/transactions/jsapi
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/transactions_sl.shtml
+ * 接口文档
*
*
* @param the type parameter
@@ -141,49 +161,48 @@ public interface EcommerceService {
* @return 调起支付需要的参数 t
* @throws WxPayException the wx pay exception
*/
- T partnerTransactions(TradeTypeEnum tradeType, PartnerTransactionsRequest request) throws WxPayException;
+ T createPartnerOrder(TradeTypeEnum tradeType, WxPayPartnerUnifiedOrderV3Request request) throws WxPayException;
/**
*
* 普通支付通知回调数据处理
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/e_transactions.shtml
+ * 接口文档
*
*
* @param notifyData 通知数据
* @param header 通知头部数据,不传则表示不校验头
- * @return 解密后通知数据 partner transactions notify result
+ * @return 解密后通知数据 WxPayPartnerNotifyV3Result
* @throws WxPayException the wx pay exception
*/
- PartnerTransactionsNotifyResult parsePartnerNotifyResult(String notifyData, SignatureHeader header) throws WxPayException;
+ WxPayPartnerNotifyV3Result parsePartnerNotifyResult(String notifyData, SignatureHeader header) throws WxPayException;
/**
*
* 普通查询订单API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/e_transactions/chapter3_5.shtml
+ * 接口文档
*
*
* @param request 商户订单信息
* @return 支付订单信息
* @throws WxPayException the wx pay exception
*/
- PartnerTransactionsResult queryPartnerTransactions(PartnerTransactionsQueryRequest request) throws WxPayException;
+ WxPayPartnerOrderQueryV3Result queryPartnerOrder(WxPayPartnerOrderQueryV3Request request) throws WxPayException;
/**
*
* 关闭普通订单API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/e_transactions/chapter3_6.shtml
+ * 接口文档
*
*
- * @param request 关闭普通订单请求
+ * @param request 请求对象
* @throws WxPayException the wx pay exception
- * @return
*/
- String closePartnerTransactions(PartnerTransactionsCloseRequest request) throws WxPayException;
+ void closePartnerOrder(WxPayPartnerOrderCloseV3Request request) throws WxPayException;
/**
*
* 服务商账户实时余额
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/amount.shtml
+ * 接口文档
*
*
* @param accountType 服务商账户类型
@@ -195,7 +214,7 @@ public interface EcommerceService {
/**
*
* 服务商账户日终余额
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/amount.shtml
+ * 接口文档
*
*
* @param accountType 服务商账户类型
@@ -208,7 +227,7 @@ public interface EcommerceService {
/**
*
* 二级商户号账户实时余额
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/amount.shtml
+ * 接口文档
*
*
* @param subMchid 二级商户号
@@ -220,7 +239,7 @@ public interface EcommerceService {
/**
*
* 二级商户号账户实时余额
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/Offline/apis/chapter4_3_11.shtml
+ * 接口文档
*
*
* @param subMchid 二级商户号
@@ -233,7 +252,7 @@ public interface EcommerceService {
/**
*
* 二级商户号账户日终余额
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/amount.shtml
+ * 接口文档
*
*
* @param subMchid 二级商户号
@@ -246,7 +265,7 @@ public interface EcommerceService {
/**
*
* 请求分账API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/profitsharing/chapter3_1.shtml
+ * 接口文档
*
*
* @param request 分账请求
@@ -258,7 +277,7 @@ public interface EcommerceService {
/**
*
* 查询分账结果API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/profitsharing/chapter3_2.shtml
+ * 接口文档
*
*
* @param request 查询分账请求
@@ -270,7 +289,7 @@ public interface EcommerceService {
/**
*
* 查询订单剩余待分金额API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_4_9.shtml
+ * 接口文档
*
*
* @param request 查询订单剩余待分金额请求
@@ -282,7 +301,7 @@ public interface EcommerceService {
/**
*
* 添加分账接收方API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/profitsharing/chapter3_7.shtml
+ * 接口文档
*
*
* @param request 添加分账接收方
@@ -294,7 +313,7 @@ public interface EcommerceService {
/**
*
* 删除分账接收方API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/profitsharing/chapter3_8.shtml
+ * 接口文档
*
*
* @param request 删除分账接收方
@@ -306,7 +325,7 @@ public interface EcommerceService {
/**
*
* 请求分账回退API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/profitsharing/chapter3_3.shtml
+ * 接口文档
*
*
* @param request 分账回退请求
@@ -318,7 +337,7 @@ public interface EcommerceService {
/**
*
* 查询分账回退API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/profitsharing/chapter3_3.shtml
+ * 接口文档
*
*
* @param request 查询分账回退请求
@@ -330,7 +349,7 @@ public interface EcommerceService {
/**
*
* 完结分账API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/profitsharing/chapter3_5.shtml
+ * 接口文档
*
*
* @param request 完结分账请求
@@ -342,7 +361,7 @@ public interface EcommerceService {
/**
*
* 退款申请API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/refunds/chapter3_1.shtml
+ * 接口文档
*
*
* @param request 退款请求
@@ -354,7 +373,7 @@ public interface EcommerceService {
/**
*
* 查询退款API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/refunds/chapter3_2.shtml
+ * 接口文档
*
*
* @param subMchid 二级商户号
@@ -368,7 +387,7 @@ public interface EcommerceService {
/**
*
* 垫付退款回补API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_6_4.shtml
+ * 接口文档
*
*
* @param subMchid 二级商户号
@@ -382,7 +401,7 @@ public interface EcommerceService {
/**
*
* 查询垫付回补结果API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_6_5.shtml
+ * 接口文档
*
*
* @param subMchid 二级商户号
@@ -394,7 +413,7 @@ public interface EcommerceService {
/**
*
* 查询退款API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/refunds/chapter3_2.shtml
+ * 接口文档
*
*
* @param subMchid 二级商户号
@@ -407,7 +426,7 @@ public interface EcommerceService {
/**
*
* 退款通知回调数据处理
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/refunds/chapter3_3.shtml
+ * 接口文档
*
*
* @param notifyData 通知数据
@@ -420,7 +439,7 @@ public interface EcommerceService {
/**
*
* 提现状态变更通知回调数据处理
- * 文档地址: https://pay.weixin.qq.com/doc/v3/partner/4013049135
+ * 接口文档
*
*
* @param notifyData 通知数据
@@ -433,7 +452,7 @@ public interface EcommerceService {
/**
*
* 二级商户账户余额提现API
- * 文档地址: https://pay.weixin.qq.com/doc/v3/partner/4012476652
+ * 接口文档
*
*
* @param request 提现请求
@@ -445,7 +464,7 @@ public interface EcommerceService {
/**
*
* 电商平台提现API
- * 文档地址: https://pay.weixin.qq.com/doc/v3/partner/4012476670
+ * 接口文档
*
*
* @param request 提现请求
@@ -457,7 +476,7 @@ public interface EcommerceService {
/**
*
* 二级商户查询提现状态API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/fund/chapter3_3.shtml
+ * 接口文档
*
*
* @param subMchid 二级商户号
@@ -470,7 +489,7 @@ public interface EcommerceService {
/**
*
* 电商平台查询提现状态API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/fund/chapter3_6.shtml
+ * 接口文档
*
*
* @param outRequestNo 商户提现单号
@@ -482,7 +501,7 @@ public interface EcommerceService {
/**
*
* 平台查询预约提现状态(根据微信支付预约提现单号查询)
- * 文档地址: https://pay.weixin.qq.com/doc/v3/partner/4012476674
+ * 接口文档
*
*
* @param withdrawId 微信支付提现单号
@@ -494,7 +513,7 @@ public interface EcommerceService {
/**
*
* 二级商户按日终余额预约提现
- * 文档地址: https://pay.weixin.qq.com/doc/v3/partner/4013328143
+ * 接口文档
*
*
* @param request 提现请求
@@ -506,7 +525,7 @@ public interface EcommerceService {
/**
*
* 查询二级商户按日终余额预约提现状态
- * 文档地址: https://pay.weixin.qq.com/doc/v3/partner/4013328163
+ * 接口文档
*
*
* @param subMchid 二级商户号
@@ -519,7 +538,7 @@ public interface EcommerceService {
/**
*
* 修改结算账号API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/applyments/chapter3_4.shtml
+ * 接口文档
*
*
* @param subMchid 二级商户号。
@@ -531,7 +550,7 @@ public interface EcommerceService {
/**
*
* 查询结算账户API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/applyments/chapter3_5.shtml
+ * 接口文档
*
*
* @param subMchid 二级商户号。
@@ -543,7 +562,7 @@ public interface EcommerceService {
/**
*
* 请求账单API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/bill.shtml
+ * 接口文档
*
*
* @param request 请求信息。
@@ -555,7 +574,7 @@ public interface EcommerceService {
/**
*
* 申请资金账单API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/bill/chapter3_2.shtml
+ * 接口文档
*
*
* @param billType 账单类型。
@@ -568,7 +587,7 @@ public interface EcommerceService {
/**
*
* 下载账单API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/bill.shtml
+ * 接口文档
*
*
* @param url 微信返回的账单地址。
@@ -581,7 +600,7 @@ public interface EcommerceService {
/**
*
* 请求补差API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_5_1.shtml
+ * 接口文档
*
*
* @param subsidiesCreateRequest 请求补差。
@@ -593,7 +612,7 @@ public interface EcommerceService {
/**
*
* 请求补差回退API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_5_2.shtml
+ * 接口文档
*
*
* @param subsidiesReturnRequest 请求补差。
@@ -605,7 +624,7 @@ public interface EcommerceService {
/**
*
* 取消补差API
- * 文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_5_3.shtml
+ * 接口文档
*
*
* @param subsidiesCancelRequest 请求补差。
@@ -617,7 +636,7 @@ public interface EcommerceService {
/**
*
* 提交注销申请单
- * 文档地址: https://pay.weixin.qq.com/docs/partner/apis/ecommerce-cancel/cancel-applications/create-cancel-application.html
+ * 接口文档
*
*
* @param accountCancelApplicationsRequest 提交注销申请单
@@ -629,7 +648,7 @@ public interface EcommerceService {
/**
*
* 查询注销单状态
- * 文档地址: https://pay.weixin.qq.com/docs/partner/apis/ecommerce-cancel/cancel-applications/get-cancel-application.html
+ * 接口文档
*
*
* @param outApplyNo 注销申请单号
@@ -641,7 +660,7 @@ public interface EcommerceService {
/**
*
* 注销单资料图片上传
- * 文档地址: https://pay.weixin.qq.com/docs/partner/apis/ecommerce-cancel/media/upload-media.html
+ * 接口文档
*
*
* @param imageFile 图片
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MarketingFavorService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MarketingFavorService.java
index ac0ed5212f..47e7035510 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MarketingFavorService.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MarketingFavorService.java
@@ -1,6 +1,6 @@
package com.github.binarywang.wxpay.service;
-import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.bean.marketing.*;
import com.github.binarywang.wxpay.exception.WxPayException;
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantLimitationService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantLimitationService.java
new file mode 100644
index 0000000000..bc3753cce5
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantLimitationService.java
@@ -0,0 +1,28 @@
+package com.github.binarywang.wxpay.service;
+
+import com.github.binarywang.wxpay.bean.merchantlimitation.MerchantLimitationResult;
+import com.github.binarywang.wxpay.exception.WxPayException;
+
+/**
+ * 商户被管控能力及原因查询 接口
+ *
+ * 产品介绍
+ *
+ *
+ * @author zhangyl
+ */
+public interface MerchantLimitationService {
+
+ /**
+ * 查询子商户管控情况
+ *
+ * 接口文档
+ *
+ *
+ * @param subMchId 子商户号
+ * @return 子商户管控情况
+ * @throws WxPayException the wx pay exception
+ */
+ MerchantLimitationResult fetchLimitations(String subMchId) throws WxPayException;
+
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantMediaService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantMediaService.java
index 0e35dbb68b..f7f0aaaf3e 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantMediaService.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantMediaService.java
@@ -1,6 +1,7 @@
package com.github.binarywang.wxpay.service;
import com.github.binarywang.wxpay.bean.media.ImageUploadResult;
+import com.github.binarywang.wxpay.bean.media.VideoUploadResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import java.io.File;
@@ -42,5 +43,34 @@ public interface MerchantMediaService {
*/
ImageUploadResult imageUploadV3(InputStream inputStream, String fileName) throws WxPayException, IOException;
+ /**
+ *
+ * 通用接口-视频上传API
+ * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/tool/chapter3_2.shtml
+ * 接口链接:https://api.mch.weixin.qq.com/v3/merchant/media/video_upload
+ *
+ *
+ * @param videoFile 需要上传的视频文件
+ * @return VideoUploadResult 微信返回的媒体文件标识Id。示例值:6uqyGjGrCf2GtyXP8bxrbuH9-aAoTjH-rKeSl3Lf4_So6kdkQu4w8BYVP3bzLtvR38lxt4PjtCDXsQpzqge_hQEovHzOhsLleGFQVRF-U_0
+ * @throws WxPayException the wx pay exception
+ * @throws IOException the io exception
+ */
+ VideoUploadResult videoUploadV3(File videoFile) throws WxPayException, IOException;
+
+ /**
+ *
+ * 通用接口-视频上传API
+ * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/tool/chapter3_2.shtml
+ * 接口链接:https://api.mch.weixin.qq.com/v3/merchant/media/video_upload
+ * 注意:此方法会将整个视频流读入内存计算SHA256后再上传,大文件可能导致OOM,建议大文件使用File方式上传
+ *
+ *
+ * @param inputStream 需要上传的视频文件流
+ * @param fileName 需要上传的视频文件名
+ * @return VideoUploadResult 微信返回的媒体文件标识Id。示例值:6uqyGjGrCf2GtyXP8bxrbuH9-aAoTjH-rKeSl3Lf4_So6kdkQu4w8BYVP3bzLtvR38lxt4PjtCDXsQpzqge_hQEovHzOhsLleGFQVRF-U_0
+ * @throws WxPayException the wx pay exception
+ * @throws IOException the io exception
+ */
+ VideoUploadResult videoUploadV3(InputStream inputStream, String fileName) throws WxPayException, IOException;
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantTransferService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantTransferService.java
index 585a96e763..5919968618 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantTransferService.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantTransferService.java
@@ -1,6 +1,12 @@
package com.github.binarywang.wxpay.service;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.bean.merchanttransfer.*;
+import com.github.binarywang.wxpay.bean.transfer.ReservationTransferBatchGetResult;
+import com.github.binarywang.wxpay.bean.transfer.ReservationTransferBatchRequest;
+import com.github.binarywang.wxpay.bean.transfer.ReservationTransferBatchResult;
+import com.github.binarywang.wxpay.bean.transfer.ReservationTransferNotifyResult;
+import com.github.binarywang.wxpay.bean.transfer.UserAuthorizationStatusResult;
import com.github.binarywang.wxpay.exception.WxPayException;
/**
@@ -91,8 +97,8 @@ public interface MerchantTransferService {
* 转账电子回单申请受理API
*
* 适用对象:直连商户
- * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_7.shtml
- * 请求URL:https://api.mch.weixin.qq.com/v3/transfer/bill-receipt
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012716452
+ * 请求URL:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/elecsign/out-bill-no
* 请求方式:POST
* 接口限频: 单个商户 20QPS,如果超过频率限制,会报错FREQUENCY_LIMITED,请降低频率请求。
*
@@ -106,15 +112,15 @@ public interface MerchantTransferService {
* 查询转账电子回单API
*
* 适用对象:直连商户
- * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_8.shtml
- * 请求URL:https://api.mch.weixin.qq.com/v3/transfer/bill-receipt/{out_batch_no}
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012716436
+ * 请求URL:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/elecsign/out-bill-no/{out_bill_no}
* 请求方式:GET
*
- * @param outBatchNo the out batch no
+ * @param outBillNo 商户转账单号
* @return electronic bill result
* @throws WxPayException the wx pay exception
*/
- ElectronicBillResult queryElectronicBill(String outBatchNo) throws WxPayException;
+ ElectronicBillResult queryElectronicBill(String outBillNo) throws WxPayException;
/**
* 转账明细电子回单受理API
@@ -147,4 +153,86 @@ public interface MerchantTransferService {
* @throws WxPayException the wx pay exception
*/
DetailElectronicBillResult queryDetailElectronicBill(DetailElectronicBillRequest request) throws WxPayException;
+
+ /**
+ * 商户查询用户授权信息接口.
+ *
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4014399293
+ * 请求URL:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/authorization/openid/{openid}
+ *
+ * @param openid 用户在直连商户应用下的用户标识
+ * @param transferSceneId 转账场景ID
+ * @return 用户授权信息
+ * @throws WxPayException the wx pay exception
+ */
+ UserAuthorizationStatusResult getUserAuthorizationStatus(String openid, String transferSceneId) throws WxPayException;
+
+ /**
+ * 批量预约商家转账接口.
+ *
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4014399293
+ * 请求URL:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/reservation/transfer-batches
+ *
+ * @param request 批量预约商家转账请求参数
+ * @return 批量预约商家转账结果
+ * @throws WxPayException the wx pay exception
+ */
+ ReservationTransferBatchResult reservationTransferBatch(ReservationTransferBatchRequest request) throws WxPayException;
+
+ /**
+ * 商户预约批次单号查询批次单接口.
+ *
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4014399293
+ * 请求URL:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/reservation/transfer-batches/out-batch-no/{out_batch_no}
+ *
+ * @param outBatchNo 商户预约批次单号
+ * @param needQueryDetail 是否需要查询明细
+ * @param offset 分页偏移量
+ * @param limit 分页大小
+ * @param detailState 明细状态(PROCESSING/SUCCESS/FAIL)
+ * @return 批量预约商家转账批次查询结果
+ * @throws WxPayException the wx pay exception
+ */
+ ReservationTransferBatchGetResult getReservationTransferBatchByOutBatchNo(String outBatchNo, Boolean needQueryDetail,
+ Integer offset, Integer limit, String detailState) throws WxPayException;
+
+ /**
+ * 微信预约批次单号查询批次单接口.
+ *
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4014399293
+ * 请求URL:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/reservation/transfer-batches/reservation-batch-no/{reservation_batch_no}
+ *
+ * @param reservationBatchNo 微信预约批次单号
+ * @param needQueryDetail 是否需要查询明细
+ * @param offset 分页偏移量
+ * @param limit 分页大小
+ * @param detailState 明细状态(PROCESSING/SUCCESS/FAIL)
+ * @return 批量预约商家转账批次查询结果
+ * @throws WxPayException the wx pay exception
+ */
+ ReservationTransferBatchGetResult getReservationTransferBatchByReservationBatchNo(String reservationBatchNo, Boolean needQueryDetail,
+ Integer offset, Integer limit, String detailState) throws WxPayException;
+
+ /**
+ * 解析预约商家转账通知回调结果.
+ *
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4014399293
+ *
+ * @param notifyData 通知数据
+ * @param header 通知头部数据,不传则表示不校验头
+ * @return 预约商家转账通知结果
+ * @throws WxPayException the wx pay exception
+ */
+ ReservationTransferNotifyResult parseReservationTransferNotifyResult(String notifyData, SignatureHeader header) throws WxPayException;
+
+ /**
+ * 关闭预约商家转账批次接口.
+ *
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4014399293
+ * 请求URL:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/reservation/transfer-batches/out-batch-no/{out_batch_no}/close
+ *
+ * @param outBatchNo 商户预约批次单号
+ * @throws WxPayException the wx pay exception
+ */
+ void closeReservationTransferBatch(String outBatchNo) throws WxPayException;
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MiPayService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MiPayService.java
new file mode 100644
index 0000000000..5e2f678c16
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MiPayService.java
@@ -0,0 +1,95 @@
+package com.github.binarywang.wxpay.service;
+
+import com.github.binarywang.wxpay.bean.mipay.MedInsOrdersRequest;
+import com.github.binarywang.wxpay.bean.mipay.MedInsOrdersResult;
+import com.github.binarywang.wxpay.bean.mipay.MedInsRefundNotifyRequest;
+import com.github.binarywang.wxpay.bean.notify.MiPayNotifyV3Result;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
+import com.github.binarywang.wxpay.exception.WxPayException;
+
+/**
+ * 医保相关接口
+ * 医保相关接口
+ * @author xgl
+ * @date 2025/12/20
+ */
+public interface MiPayService {
+
+ /**
+ *
+ * 医保自费混合收款下单
+ *
+ * 从业机构调用该接口向微信医保后台下单
+ *
+ * 文档地址:医保自费混合收款下单
+ *
+ *
+ * @param request 下单参数
+ * @return ReservationTransferNotifyResult 下单结果
+ * @throws WxPayException the wx pay exception
+ */
+ MedInsOrdersResult medInsOrders(MedInsOrdersRequest request) throws WxPayException;
+
+ /**
+ *
+ * 使用医保自费混合订单号查看下单结果
+ *
+ * 从业机构使用混合下单订单号,通过该接口主动查询订单状态,完成下一步的业务逻辑。
+ *
+ * 文档地址:使用医保自费混合订单号查看下单结果
+ *
+ *
+ * @param mixTradeNo 医保自费混合订单号
+ * @param subMchid 医疗机构的商户号
+ * @return MedInsOrdersResult 下单结果
+ * @throws WxPayException the wx pay exception
+ */
+ MedInsOrdersResult getMedInsOrderByMixTradeNo(String mixTradeNo, String subMchid) throws WxPayException;
+
+ /**
+ *
+ * 使用从业机构订单号查看下单结果
+ *
+ * 从业机构使用从业机构订单号、医疗机构商户号,通过该接口主动查询订单状态,完成下一步的业务逻辑。
+ *
+ * 文档地址:使用从业机构订单号查看下单结果
+ *
+ *
+ * @param outTradeNo 从业机构订单号
+ * @param subMchid 医疗机构的商户号
+ * @return MedInsOrdersResult 下单结果
+ * @throws WxPayException the wx pay exception
+ */
+ MedInsOrdersResult getMedInsOrderByOutTradeNo(String outTradeNo, String subMchid) throws WxPayException;
+
+ /**
+ *
+ * 解析医保混合收款成功通知
+ *
+ * 微信支付会通过POST请求向商户设置的回调URL推送医保混合收款成功通知,商户需要接收处理该消息,并返回应答。
+ *
+ * 文档地址:医保混合收款成功通知
+ *
+ *
+ * @param notifyData 通知数据字符串
+ * @return MiPayNotifyV3Result 医保混合收款成功通知结果
+ * @throws WxPayException the wx pay exception
+ */
+ MiPayNotifyV3Result parseMiPayNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException;
+
+ /**
+ *
+ * 医保退款通知
+ *
+ * 从业机构调用该接口向微信医保后台通知医保订单的退款成功结果
+ *
+ * 文档地址:医保退款通知
+ *
+ *
+ * @param request 医保退款通知请求参数
+ * @param mixTradeNo 【医保自费混合订单号】 医保自费混合订单号
+ * @throws WxPayException the wx pay exception
+ */
+ void medInsRefundNotify(MedInsRefundNotifyRequest request, String mixTradeNo) throws WxPayException;
+
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerPayScoreService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerPayScoreService.java
index c5c4e06796..0bb9b82af1 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerPayScoreService.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerPayScoreService.java
@@ -1,6 +1,6 @@
package com.github.binarywang.wxpay.service;
-import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.bean.payscore.PayScoreNotifyData;
import com.github.binarywang.wxpay.bean.payscore.WxPartnerPayScoreRequest;
import com.github.binarywang.wxpay.bean.payscore.WxPartnerPayScoreResult;
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerPayScoreSignPlanService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerPayScoreSignPlanService.java
index 3e51ebd7f0..f72f004fb8 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerPayScoreSignPlanService.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerPayScoreSignPlanService.java
@@ -1,6 +1,6 @@
package com.github.binarywang.wxpay.service;
-import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.bean.payscore.PartnerUserSignPlanEntity;
import com.github.binarywang.wxpay.bean.payscore.WxPartnerPayScoreSignPlanRequest;
import com.github.binarywang.wxpay.bean.payscore.WxPartnerPayScoreSignPlanResult;
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerTransferService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerTransferService.java
index b7397605ac..cea20e86f0 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerTransferService.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PartnerTransferService.java
@@ -99,11 +99,11 @@ public interface PartnerTransferService {
* 转账电子回单申请受理API
* 接口说明
* 适用对象:直连商户 服务商
- * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/transfer/chapter4_1.shtml
- * 请求URL:https://api.mch.weixin.qq.com/v3/transfer/bill-receipt
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012716452
+ * 请求URL:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/elecsign/out-bill-no
* 请求方式:POST
*
- * @param request 商家批次单号
+ * @param request 商户转账单号
* @return 返回数据 fund balance result
* @throws WxPayException the wx pay exception
*/
@@ -114,15 +114,15 @@ public interface PartnerTransferService {
* 查询转账电子回单API
* 接口说明
* 适用对象:直连商户 服务商
- * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/transfer/chapter4_2.shtml
- * 请求URL:https://api.mch.weixin.qq.com/v3/transfer/bill-receipt/{out_batch_no}
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012716436
+ * 请求URL:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/elecsign/out-bill-no/{out_bill_no}
* 请求方式:GET
*
- * @param outBatchNo 商家批次单号
+ * @param outBillNo 商户转账单号
* @return 返回数据 fund balance result
* @throws WxPayException the wx pay exception
*/
- BillReceiptResult queryBillReceipt(String outBatchNo) throws WxPayException;
+ BillReceiptResult queryBillReceipt(String outBillNo) throws WxPayException;
/**
* 转账明细电子回单受理API
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayScoreService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayScoreService.java
index 5b4f692033..ee816f1ab3 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayScoreService.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayScoreService.java
@@ -1,6 +1,6 @@
package com.github.binarywang.wxpay.service;
-import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.bean.payscore.PayScoreNotifyData;
import com.github.binarywang.wxpay.bean.payscore.UserAuthorizationStatusNotifyResult;
import com.github.binarywang.wxpay.bean.payscore.WxPayScoreRequest;
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayrollService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayrollService.java
index b3f788815c..581e3230b7 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayrollService.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayrollService.java
@@ -101,4 +101,16 @@ public interface PayrollService {
*/
WxPayApplyBillV3Result merchantFundWithdrawBillType(String billType, String billDate, String tarType) throws WxPayException;
+ /**
+ * 微工卡批量转账API
+ * 适用对象:服务商
+ * 请求URL:https://api.mch.weixin.qq.com/v3/payroll-card/transfer-batches
+ * 请求方式:POST
+ *
+ * @param request 请求参数
+ * @return 返回数据
+ * @throws WxPayException the wx pay exception
+ */
+ PayrollTransferBatchesResult payrollCardTransferBatches(PayrollTransferBatchesRequest request) throws WxPayException;
+
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/RealNameService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/RealNameService.java
new file mode 100644
index 0000000000..d69bda7d33
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/RealNameService.java
@@ -0,0 +1,43 @@
+package com.github.binarywang.wxpay.service;
+
+import com.github.binarywang.wxpay.bean.realname.RealNameRequest;
+import com.github.binarywang.wxpay.bean.realname.RealNameResult;
+import com.github.binarywang.wxpay.exception.WxPayException;
+
+/**
+ *
+ * 微信支付实名验证相关服务类.
+ * 详见文档:https://pay.wechatpay.cn/doc/v2/merchant/4011987607
+ *
+ *
+ * @author Binary Wang
+ */
+public interface RealNameService {
+ /**
+ *
+ * 获取用户实名认证信息API.
+ * 用于商户查询用户的实名认证状态,如果用户未实名认证,会返回引导用户实名认证的URL
+ * 文档详见:https://pay.wechatpay.cn/doc/v2/merchant/4011987607
+ * 接口链接:https://api.mch.weixin.qq.com/userinfo/realnameauth/query
+ *
+ *
+ * @param request 请求对象
+ * @return 实名认证查询结果
+ * @throws WxPayException the wx pay exception
+ */
+ RealNameResult queryRealName(RealNameRequest request) throws WxPayException;
+
+ /**
+ *
+ * 获取用户实名认证信息API(简化方法).
+ * 用于商户查询用户的实名认证状态,如果用户未实名认证,会返回引导用户实名认证的URL
+ * 文档详见:https://pay.wechatpay.cn/doc/v2/merchant/4011987607
+ * 接口链接:https://api.mch.weixin.qq.com/userinfo/realnameauth/query
+ *
+ *
+ * @param openid 用户openid
+ * @return 实名认证查询结果
+ * @throws WxPayException the wx pay exception
+ */
+ RealNameResult queryRealName(String openid) throws WxPayException;
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/SubscriptionBillingService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/SubscriptionBillingService.java
new file mode 100644
index 0000000000..e662e4dd4f
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/SubscriptionBillingService.java
@@ -0,0 +1,105 @@
+package com.github.binarywang.wxpay.service;
+
+import com.github.binarywang.wxpay.bean.subscriptionbilling.*;
+import com.github.binarywang.wxpay.exception.WxPayException;
+
+/**
+ * 微信支付-预约扣费服务 (连续包月功能)
+ *
+ * 微信支付预约扣费功能,支持商户在用户授权的情况下,
+ * 按照约定的时间和金额,自动从用户的支付账户中扣取费用。
+ * 主要用于连续包月、订阅服务等场景。
+ *
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ *
+ *
+ * @author Binary Wang
+ * created on 2024-08-31
+ */
+public interface SubscriptionBillingService {
+
+ /**
+ * 预约扣费
+ *
+ * 商户可以通过该接口预约未来某个时间点的扣费。
+ * 适用于连续包月、订阅服务等场景。
+ *
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/schedule
+ * 请求方式: POST
+ * 是否需要证书: 是
+ *
+ *
+ * @param request 预约扣费请求参数
+ * @return 预约扣费结果
+ * @throws WxPayException 微信支付异常
+ */
+ SubscriptionScheduleResult scheduleSubscription(SubscriptionScheduleRequest request) throws WxPayException;
+
+ /**
+ * 查询预约扣费
+ *
+ * 商户可以通过该接口查询已预约的扣费信息。
+ *
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/schedule/{subscription_id}
+ * 请求方式: GET
+ *
+ *
+ * @param subscriptionId 预约扣费ID
+ * @return 预约扣费查询结果
+ * @throws WxPayException 微信支付异常
+ */
+ SubscriptionQueryResult querySubscription(String subscriptionId) throws WxPayException;
+
+ /**
+ * 取消预约扣费
+ *
+ * 商户可以通过该接口取消已预约的扣费。
+ *
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/schedule/{subscription_id}/cancel
+ * 请求方式: POST
+ * 是否需要证书: 是
+ *
+ *
+ * @param request 取消预约扣费请求参数
+ * @return 取消预约扣费结果
+ * @throws WxPayException 微信支付异常
+ */
+ SubscriptionCancelResult cancelSubscription(SubscriptionCancelRequest request) throws WxPayException;
+
+ /**
+ * 立即扣费
+ *
+ * 商户可以通过该接口立即执行扣费操作。
+ * 通常用于补扣失败的费用或者特殊情况下的即时扣费。
+ *
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/instant-billing
+ * 请求方式: POST
+ * 是否需要证书: 是
+ *
+ *
+ * @param request 立即扣费请求参数
+ * @return 立即扣费结果
+ * @throws WxPayException 微信支付异常
+ */
+ SubscriptionInstantBillingResult instantBilling(SubscriptionInstantBillingRequest request) throws WxPayException;
+
+ /**
+ * 查询扣费记录
+ *
+ * 商户可以通过该接口查询扣费记录。
+ *
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ * 请求URL: https://api.mch.weixin.qq.com/v3/subscription-billing/transactions
+ * 请求方式: GET
+ *
+ *
+ * @param request 查询扣费记录请求参数
+ * @return 扣费记录查询结果
+ * @throws WxPayException 微信支付异常
+ */
+ SubscriptionTransactionQueryResult queryTransactions(SubscriptionTransactionQueryRequest request) throws WxPayException;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/TransferService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/TransferService.java
index 01113c9506..e48e327505 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/TransferService.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/TransferService.java
@@ -189,4 +189,123 @@ public interface TransferService {
* @throws WxPayException the wx pay exception
*/
TransferBillsNotifyResult parseTransferBillsNotifyResult(String notifyData, SignatureHeader header) throws WxPayException;
+
+ // ===================== 用户授权免确认模式相关接口 =====================
+
+ /**
+ *
+ * 商户查询用户授权信息接口
+ *
+ * 商户通过此接口可查询用户是否对商户的商家转账场景进行了授权。
+ *
+ * 请求方式:GET(HTTPS)
+ * 请求地址:请求地址
+ *
+ * 文档地址:商户查询用户授权信息
+ *
+ *
+ * @param openid 用户在直连商户应用下的用户标识
+ * @param transferSceneId 转账场景ID
+ * @return UserAuthorizationStatusResult 用户授权信息
+ * @throws WxPayException .
+ */
+ UserAuthorizationStatusResult getUserAuthorizationStatus(String openid, String transferSceneId) throws WxPayException;
+
+ /**
+ *
+ * 批量预约商家转账接口
+ *
+ * 商户可以通过批量预约接口一次发起批量转账请求,最多可以同时向50个用户发起转账。
+ * 批量预约接口适用于用户已授权免确认的场景,在转账时无需用户确认即可完成转账。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址:请求地址
+ *
+ * 文档地址:批量预约商家转账
+ *
+ *
+ * @param request 批量预约商家转账请求参数
+ * @return ReservationTransferBatchResult 批量预约商家转账结果
+ * @throws WxPayException .
+ */
+ ReservationTransferBatchResult reservationTransferBatch(ReservationTransferBatchRequest request) throws WxPayException;
+
+ /**
+ *
+ * 商户预约批次单号查询批次单接口
+ *
+ * 通过商户预约批次单号查询批量预约商家转账批次单基本信息。
+ *
+ * 请求方式:GET(HTTPS)
+ * 请求地址:请求地址
+ *
+ * 文档地址:商户预约批次单号查询批次单
+ *
+ *
+ * @param outBatchNo 商户预约批次单号
+ * @param needQueryDetail 是否需要查询明细
+ * @param offset 分页偏移量
+ * @param limit 分页大小
+ * @param detailState 明细状态(PROCESSING/SUCCESS/FAIL)
+ * @return ReservationTransferBatchGetResult 批量预约商家转账批次查询结果
+ * @throws WxPayException .
+ */
+ ReservationTransferBatchGetResult getReservationTransferBatchByOutBatchNo(String outBatchNo, Boolean needQueryDetail,
+ Integer offset, Integer limit, String detailState) throws WxPayException;
+
+ /**
+ *
+ * 微信预约批次单号查询批次单接口
+ *
+ * 通过微信预约批次单号查询批量预约商家转账批次单基本信息。
+ *
+ * 请求方式:GET(HTTPS)
+ * 请求地址:请求地址
+ *
+ * 文档地址:微信预约批次单号查询批次单
+ *
+ *
+ * @param reservationBatchNo 微信预约批次单号
+ * @param needQueryDetail 是否需要查询明细
+ * @param offset 分页偏移量
+ * @param limit 分页大小
+ * @param detailState 明细状态(PROCESSING/SUCCESS/FAIL)
+ * @return ReservationTransferBatchGetResult 批量预约商家转账批次查询结果
+ * @throws WxPayException .
+ */
+ ReservationTransferBatchGetResult getReservationTransferBatchByReservationBatchNo(String reservationBatchNo, Boolean needQueryDetail,
+ Integer offset, Integer limit, String detailState) throws WxPayException;
+
+ /**
+ *
+ * 解析预约商家转账通知回调结果
+ *
+ * 预约批次单中的明细单在转账成功或转账失败时,微信会把相关结果信息发送给商户。
+ *
+ * 文档地址:预约商家转账通知
+ *
+ *
+ * @param notifyData 通知数据
+ * @param header 通知头部数据,不传则表示不校验头
+ * @return ReservationTransferNotifyResult 预约商家转账通知结果
+ * @throws WxPayException the wx pay exception
+ */
+ ReservationTransferNotifyResult parseReservationTransferNotifyResult(String notifyData, SignatureHeader header) throws WxPayException;
+
+ /**
+ *
+ * 关闭预约商家转账批次接口
+ *
+ * 商户可以通过此接口关闭预约商家转账批次单。关闭后,该批次内所有未成功的转账将被取消。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址:请求地址
+ *
+ * 文档地址:关闭预约商家转账批次
+ *
+ *
+ * @param outBatchNo 商户预约批次单号
+ * @throws WxPayException .
+ */
+ void closeReservationTransferBatch(String outBatchNo) throws WxPayException;
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxDepositService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxDepositService.java
new file mode 100644
index 0000000000..cb0bc3b062
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxDepositService.java
@@ -0,0 +1,90 @@
+package com.github.binarywang.wxpay.service;
+
+import com.github.binarywang.wxpay.bean.request.WxDepositConsumeRequest;
+import com.github.binarywang.wxpay.bean.request.WxDepositOrderQueryRequest;
+import com.github.binarywang.wxpay.bean.request.WxDepositRefundRequest;
+import com.github.binarywang.wxpay.bean.request.WxDepositUnfreezeRequest;
+import com.github.binarywang.wxpay.bean.request.WxDepositUnifiedOrderRequest;
+import com.github.binarywang.wxpay.bean.result.WxDepositConsumeResult;
+import com.github.binarywang.wxpay.bean.result.WxDepositOrderQueryResult;
+import com.github.binarywang.wxpay.bean.result.WxDepositRefundResult;
+import com.github.binarywang.wxpay.bean.result.WxDepositUnfreezeResult;
+import com.github.binarywang.wxpay.bean.result.WxDepositUnifiedOrderResult;
+import com.github.binarywang.wxpay.exception.WxPayException;
+
+/**
+ *
+ * 微信押金支付相关接口.
+ * https://pay.weixin.qq.com/wiki/doc/api/deposit_sl.php?chapter=27_7&index=1
+ *
+ *
+ * @author Binary Wang
+ * created on 2024-09-24
+ */
+public interface WxDepositService {
+
+ /**
+ *
+ * 押金下单
+ * 详见:https://pay.weixin.qq.com/wiki/doc/api/deposit_sl.php?chapter=27_7&index=2
+ * 用于商户发起押金支付,支持JSAPI、NATIVE、APP等支付方式
+ *
+ *
+ * @param request 押金下单请求对象
+ * @return wx deposit unified order result
+ * @throws WxPayException wx pay exception
+ */
+ WxDepositUnifiedOrderResult unifiedOrder(WxDepositUnifiedOrderRequest request) throws WxPayException;
+
+ /**
+ *
+ * 查询押金订单
+ * 详见:https://pay.weixin.qq.com/wiki/doc/api/deposit_sl.php?chapter=27_7&index=3
+ * 通过商户订单号或微信订单号查询押金订单状态
+ *
+ *
+ * @param request 查询押金订单请求对象
+ * @return wx deposit order query result
+ * @throws WxPayException wx pay exception
+ */
+ WxDepositOrderQueryResult queryOrder(WxDepositOrderQueryRequest request) throws WxPayException;
+
+ /**
+ *
+ * 押金消费
+ * 详见:https://pay.weixin.qq.com/wiki/doc/api/deposit_sl.php?chapter=27_7&index=4
+ * 用于对已支付的押金进行消费扣减
+ *
+ *
+ * @param request 押金消费请求对象
+ * @return wx deposit consume result
+ * @throws WxPayException wx pay exception
+ */
+ WxDepositConsumeResult consume(WxDepositConsumeRequest request) throws WxPayException;
+
+ /**
+ *
+ * 押金撤销
+ * 详见:https://pay.weixin.qq.com/wiki/doc/api/deposit_sl.php?chapter=27_7&index=5
+ * 用于对已支付的押金进行撤销退还
+ *
+ *
+ * @param request 押金撤销请求对象
+ * @return wx deposit unfreeze result
+ * @throws WxPayException wx pay exception
+ */
+ WxDepositUnfreezeResult unfreeze(WxDepositUnfreezeRequest request) throws WxPayException;
+
+ /**
+ *
+ * 押金退款
+ * 详见:https://pay.weixin.qq.com/wiki/doc/api/deposit_sl.php?chapter=27_7&index=6
+ * 用于对已消费的押金进行退款
+ *
+ *
+ * @param request 押金退款请求对象
+ * @return wx deposit refund result
+ * @throws WxPayException wx pay exception
+ */
+ WxDepositRefundResult refund(WxDepositRefundRequest request) throws WxPayException;
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java
index 4ee5226d3d..9cf5aba4a4 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java
@@ -5,12 +5,13 @@
import com.github.binarywang.wxpay.bean.notify.*;
import com.github.binarywang.wxpay.bean.request.*;
import com.github.binarywang.wxpay.bean.result.*;
-import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.bean.result.enums.GlobalTradeTypeEnum;
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsNotifyResult;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.exception.WxSignTestException;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
@@ -38,6 +39,7 @@ public interface WxPayService {
/**
* Map里 加入新的 {@link WxPayConfig},适用于动态添加新的微信商户配置.
+ * 配置键将使用 mchId + "_" + appId 的格式.
*
* @param mchId 商户id
* @param appId 微信应用id
@@ -45,6 +47,15 @@ public interface WxPayService {
*/
void addConfig(String mchId, String appId, WxPayConfig wxPayConfig);
+ /**
+ * Map里 加入新的 {@link WxPayConfig},使用自定义配置键,适用于动态添加新的微信商户配置.
+ * 此方法允许使用任意唯一标识符(如租户ID)作为配置键,兼容单参数 switchover 使用方式.
+ *
+ * @param configKey 自定义的配置键(全局唯一标识符,如租户ID)
+ * @param wxPayConfig 新的微信配置
+ */
+ void addConfig(String configKey, WxPayConfig wxPayConfig);
+
/**
* 从 Map中 移除 {@link String mchId} 和 {@link String appId} 所对应的 {@link WxPayConfig},适用于动态移除微信商户配置.
*
@@ -53,6 +64,14 @@ public interface WxPayService {
*/
void removeConfig(String mchId, String appId);
+ /**
+ * 从 Map中 移除指定配置键所对应的 {@link WxPayConfig},适用于动态移除微信商户配置.
+ * 此方法允许使用任意唯一标识符(如租户ID)删除配置,兼容单参数 switchover 使用方式.
+ *
+ * @param configKey 自定义的配置键(全局唯一标识符,如租户ID)
+ */
+ void removeConfig(String configKey);
+
/**
* 注入多个 {@link WxPayConfig} 的实现. 并为每个 {@link WxPayConfig} 赋予不同的 {@link String mchId} 值
* 随机采用一个{@link String mchId}进行Http初始化操作
@@ -78,6 +97,21 @@ public interface WxPayService {
*/
boolean switchover(String mchId, String appId);
+ /**
+ * 根据商户号或自定义配置键进行切换.
+ *
+ * - 当传入商户号(mchId)时,会先尝试精确匹配,若未找到则前缀匹配(mchId_*)。
+ * - 也可传入通过 {@link #addConfig(String, WxPayConfig)} 或 {@link #setMultiConfig(Map)} 注册的任意自定义配置键,此时直接精确匹配。
+ *
+ * 注意:当存在多个前缀匹配项时返回的配置是不可预测的,建议使用精确匹配方式.
+ *
+ * @param mchIdOrConfigKey 商户标识或自定义配置键
+ * @return 切换是否成功,如果找不到匹配的配置则返回false
+ */
+ default boolean switchover(String mchIdOrConfigKey) {
+ return false;
+ }
+
/**
* 进行相应的商户切换.
*
@@ -87,6 +121,22 @@ public interface WxPayService {
*/
WxPayService switchoverTo(String mchId, String appId);
+ /**
+ * 根据商户号或自定义配置键进行切换,支持链式调用.
+ *
+ * - 当传入商户号(mchId)时,会先尝试精确匹配,若未找到则前缀匹配(mchId_*)。
+ * - 也可传入通过 {@link #addConfig(String, WxPayConfig)} 或 {@link #setMultiConfig(Map)} 注册的任意自定义配置键,此时直接精确匹配。
+ *
+ * 注意:当存在多个前缀匹配项时返回的配置是不可预测的,建议使用精确匹配方式.
+ *
+ * @param mchIdOrConfigKey 商户标识或自定义配置键
+ * @return 切换成功,则返回当前对象,方便链式调用
+ * @throws me.chanjar.weixin.common.error.WxRuntimeException 如果找不到匹配的配置
+ */
+ default WxPayService switchoverTo(String mchIdOrConfigKey) {
+ throw new me.chanjar.weixin.common.error.WxRuntimeException("子类需要实现此方法");
+ }
+
/**
* 发送post请求,得到响应字节数组.
*
@@ -229,6 +279,13 @@ public interface WxPayService {
*/
WxEntrustPapService getWxEntrustPapService();
+ /**
+ * 获取微信押金支付服务类
+ *
+ * @return deposit service
+ */
+ WxDepositService getWxDepositService();
+
/**
* 获取批量转账到零钱服务类.
*
@@ -330,6 +387,13 @@ public interface WxPayService {
*/
BrandMerchantTransferService getBrandMerchantTransferService();
+ /**
+ * 获取微信支付预约扣费服务类 (连续包月功能)
+ *
+ * @return the subscription billing service
+ */
+ SubscriptionBillingService getSubscriptionBillingService();
+
/**
* 设置企业付款服务类,允许开发者自定义实现类.
*
@@ -337,6 +401,13 @@ public interface WxPayService {
*/
void setEntPayService(EntPayService entPayService);
+ /**
+ * 获取商户被管控能力及原因查询接口
+ *
+ * @return MerchantLimitationService
+ */
+ MerchantLimitationService getMerchantLimitationService();
+
/**
*
* 查询订单.
@@ -746,11 +817,33 @@ public interface WxPayService {
/**
* 获取配置.
+ * 在多商户配置场景下,会根据 WxPayConfigHolder 中的值获取对应的配置.
*
* @return the config
*/
WxPayConfig getConfig();
+ /**
+ * 根据商户号和 appId 直接获取配置.
+ * 此方法不依赖 ThreadLocal,可以在任何上下文中使用,适用于多商户管理场景.
+ *
+ * @param mchId 商户号
+ * @param appId 微信应用 id
+ * @return 对应的配置对象,如果不存在则返回 null
+ */
+ WxPayConfig getConfig(String mchId, String appId);
+
+ /**
+ * 根据商户号直接获取配置.
+ * 此方法不依赖 ThreadLocal,可以在任何上下文中使用.
+ * 适用于一个商户号对应多个 appId 的场景,会返回该商户号的任意一个配置.
+ * 注意:当存在多个匹配项时返回的配置是不可预测的,建议使用精确匹配方式.
+ *
+ * @param mchId 商户号
+ * @return 对应的配置对象,如果不存在则返回 null
+ */
+ WxPayConfig getConfig(String mchId);
+
/**
* 设置配置对象.
*
@@ -825,6 +918,32 @@ public interface WxPayService {
*/
WxPayRefundV3Result refundV3(WxPayRefundV3Request request) throws WxPayException;
+ /**
+ *
+ * 微信支付-服务商申请退款.
+ * 应用场景
+ * 当交易发生之后一年内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付金额退还给买家,微信支付将在收到退款请求并且验证成功之后,将支付款按原路退还至买家账号上。
+ *
+ * 注意:
+ * 1、交易时间超过一年的订单无法提交退款
+ * 2、微信支付退款支持单笔交易分多次退款(不超50次),多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
+ * 3、错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
+ * 4、每个支付订单的部分退款次数不能超过50次
+ * 5、如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
+ * 6、申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果
+ * 7、一个月之前的订单申请退款频率限制为:5000/min
+ *
+ * 详见 https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_1_9.shtml
+ * 接口地址
+ * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds
+ *
+ *
+ * @param request 请求对象
+ * @return 退款操作结果 wx pay refund result
+ * @throws WxPayException the wx pay exception
+ */
+ WxPayRefundV3Result partnerRefundV3(WxPayPartnerRefundV3Request request) throws WxPayException;
+
/**
*
* 微信支付-查询退款.
@@ -951,6 +1070,16 @@ WxPayRefundQueryResult refundQuery(String transactionId, String outTradeNo, Stri
*/
WxPayOrderNotifyResult parseOrderNotifyResult(String xmlData, String signType) throws WxPayException;
+ /**
+ * 校验通知签名
+ *
+ * @param header 通知头信息
+ * @param data 通知数据
+ * @return true:校验通过 false:校验不通过
+ * @throws WxSignTestException 微信支付签名探测流量异常
+ */
+ boolean verifyNotifySign(SignatureHeader header, String data) throws WxSignTestException;
+
/**
* 解析支付结果v3通知. 直连商户模式
* 详见https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_5.shtml
@@ -1053,6 +1182,16 @@ WxPayRefundQueryResult refundQuery(String transactionId, String outTradeNo, Stri
*/
WxPayPartnerRefundNotifyV3Result parsePartnerRefundNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException;
+ /**
+ * 解析合作伙伴订阅通知
+ *
+ * @param notifyData 通知数据
+ * @param header 通知头部数据
+ * @return 合作伙伴订阅通知
+ * @throws WxPayException the wx pay exception
+ */
+ PartnerSubscribeNotifyResult parsePartnerSubscribeNotify(String notifyData, SignatureHeader header) throws WxPayException;
+
/**
* 解析扫码支付回调通知
* 详见https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4
@@ -1646,6 +1785,13 @@ WxPayRefundQueryResult refundQuery(String transactionId, String outTradeNo, Stri
*/
TransferService getTransferService();
+ /**
+ * 获取运营工具-商家转账服务类
+ *
+ * @return the business operation transfer service
+ */
+ BusinessOperationTransferService getBusinessOperationTransferService();
+
/**
* 获取服务商支付分服务类
*
@@ -1659,4 +1805,19 @@ WxPayRefundQueryResult refundQuery(String transactionId, String outTradeNo, Stri
* @return the partner pay score sign plan service
*/
PartnerPayScoreSignPlanService getPartnerPayScoreSignPlanService();
+
+ /**
+ * 获取实名验证服务类
+ *
+ * @return the real name service
+ */
+ RealNameService getRealNameService();
+
+ /**
+ * 获取医保支付服务类
+ *
+ * @return the merchant transfer service
+ */
+ MiPayService getMiPayService();
+
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java
index 3884881b8d..943894146c 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java
@@ -10,8 +10,8 @@
import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
import com.github.binarywang.wxpay.bean.request.*;
import com.github.binarywang.wxpay.bean.result.*;
-import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.bean.result.enums.GlobalTradeTypeEnum;
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsNotifyResult;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.config.WxPayConfigHolder;
@@ -103,6 +103,9 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
@Getter
private final WxEntrustPapService wxEntrustPapService = new WxEntrustPapServiceImpl(this);
+ @Getter
+ private final WxDepositService wxDepositService = new WxDepositServiceImpl(this);
+
@Getter
private final PartnerTransferService partnerTransferService = new PartnerTransferServiceImpl(this);
@@ -130,6 +133,21 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
@Getter
private final BrandMerchantTransferService brandMerchantTransferService = new BrandMerchantTransferServiceImpl(this);
+ @Getter
+ private final SubscriptionBillingService subscriptionBillingService = new SubscriptionBillingServiceImpl(this);
+
+ @Getter
+ private final BusinessOperationTransferService businessOperationTransferService = new BusinessOperationTransferServiceImpl(this);
+
+ @Getter
+ private final RealNameService realNameService = new RealNameServiceImpl(this);
+
+ @Getter
+ private final MiPayService miPayService = new MiPayServiceImpl(this);
+
+ @Getter
+ private final MerchantLimitationService merchantLimitationService = new MerchantLimitationServiceImpl(this);
+
protected Map configMap = new ConcurrentHashMap<>();
@Override
@@ -141,6 +159,47 @@ public WxPayConfig getConfig() {
return this.configMap.get(WxPayConfigHolder.get());
}
+ @Override
+ public WxPayConfig getConfig(String mchId, String appId) {
+ if (StringUtils.isBlank(mchId)) {
+ log.warn("商户号mchId不能为空");
+ return null;
+ }
+ if (StringUtils.isBlank(appId)) {
+ log.warn("应用ID appId不能为空");
+ return null;
+ }
+ String configKey = this.getConfigKey(mchId, appId);
+ return this.configMap.get(configKey);
+ }
+
+ @Override
+ public WxPayConfig getConfig(String mchId) {
+ if (StringUtils.isBlank(mchId)) {
+ log.warn("商户号mchId不能为空");
+ return null;
+ }
+
+ // 先尝试精确匹配(针对只有mchId没有appId的配置)
+ if (this.configMap.containsKey(mchId)) {
+ return this.configMap.get(mchId);
+ }
+
+ // 尝试前缀匹配(查找以 mchId_ 开头的配置)
+ String prefix = mchId + "_";
+ return this.configMap.entrySet().stream()
+ .filter(entry -> entry.getKey().startsWith(prefix))
+ .findFirst()
+ .map(entry -> {
+ log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, entry.getKey());
+ return entry.getValue();
+ })
+ .orElseGet(() -> {
+ log.warn("无法找到对应mchId=【{}】的商户号配置信息", mchId);
+ return null;
+ });
+ }
+
@Override
public void setConfig(WxPayConfig config) {
final String defaultKey = this.getConfigKey(config.getMchId(), config.getAppId());
@@ -160,6 +219,18 @@ public void addConfig(String mchId, String appId, WxPayConfig wxPayConfig) {
}
}
+ @Override
+ public void addConfig(String configKey, WxPayConfig wxPayConfig) {
+ synchronized (this) {
+ if (this.configMap == null) {
+ this.setMultiConfig(ImmutableMap.of(configKey, wxPayConfig), configKey);
+ } else {
+ WxPayConfigHolder.set(configKey);
+ this.configMap.put(configKey, wxPayConfig);
+ }
+ }
+ }
+
@Override
public void removeConfig(String mchId, String appId) {
synchronized (this) {
@@ -177,6 +248,22 @@ public void removeConfig(String mchId, String appId) {
}
}
+ @Override
+ public void removeConfig(String configKey) {
+ synchronized (this) {
+ this.configMap.remove(configKey);
+ if (this.configMap.isEmpty()) {
+ log.warn("已删除最后一个商户号配置:configKey[{}],须立即使用setConfig或setMultiConfig添加配置", configKey);
+ return;
+ }
+ if (WxPayConfigHolder.get().equals(configKey)) {
+ final String nextConfigKey = this.configMap.keySet().iterator().next();
+ WxPayConfigHolder.set(nextConfigKey);
+ log.warn("已删除默认商户号配置,商户号【{}】被设为默认配置", nextConfigKey);
+ }
+ }
+ }
+
@Override
public void setMultiConfig(Map wxPayConfigs) {
this.setMultiConfig(wxPayConfigs, wxPayConfigs.keySet().iterator().next());
@@ -190,6 +277,10 @@ public void setMultiConfig(Map wxPayConfigs, String default
@Override
public boolean switchover(String mchId, String appId) {
+ // 如果appId为空,则降级为仅使用mchId进行切换
+ if (StringUtils.isBlank(appId)) {
+ return this.switchover(mchId);
+ }
String configKey = this.getConfigKey(mchId, appId);
if (this.configMap.containsKey(configKey)) {
WxPayConfigHolder.set(configKey);
@@ -199,8 +290,40 @@ public boolean switchover(String mchId, String appId) {
return false;
}
+ @Override
+ public boolean switchover(String mchId) {
+ // 参数校验
+ if (StringUtils.isBlank(mchId)) {
+ log.error("商户号mchId不能为空");
+ return false;
+ }
+
+ // 先尝试精确匹配(针对只有mchId没有appId的配置)
+ if (this.configMap.containsKey(mchId)) {
+ WxPayConfigHolder.set(mchId);
+ return true;
+ }
+
+ // 尝试前缀匹配(查找以 mchId_ 开头的配置)
+ String prefix = mchId + "_";
+ for (String key : this.configMap.keySet()) {
+ if (key.startsWith(prefix)) {
+ WxPayConfigHolder.set(key);
+ log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, key);
+ return true;
+ }
+ }
+
+ log.error("无法找到对应mchId=【{}】的商户号配置信息,请核实!", mchId);
+ return false;
+ }
+
@Override
public WxPayService switchoverTo(String mchId, String appId) {
+ // 如果appId为空,则降级为仅使用mchId进行切换
+ if (StringUtils.isBlank(appId)) {
+ return this.switchoverTo(mchId);
+ }
String configKey = this.getConfigKey(mchId, appId);
if (this.configMap.containsKey(configKey)) {
WxPayConfigHolder.set(configKey);
@@ -209,6 +332,32 @@ public WxPayService switchoverTo(String mchId, String appId) {
throw new WxRuntimeException(String.format("无法找到对应mchId=【%s】,appId=【%s】的商户号配置信息,请核实!", mchId, appId));
}
+ @Override
+ public WxPayService switchoverTo(String mchId) {
+ // 参数校验
+ if (StringUtils.isBlank(mchId)) {
+ throw new WxRuntimeException("商户号mchId不能为空");
+ }
+
+ // 先尝试精确匹配(针对只有mchId没有appId的配置)
+ if (this.configMap.containsKey(mchId)) {
+ WxPayConfigHolder.set(mchId);
+ return this;
+ }
+
+ // 尝试前缀匹配(查找以 mchId_ 开头的配置)
+ String prefix = mchId + "_";
+ for (String key : this.configMap.keySet()) {
+ if (key.startsWith(prefix)) {
+ WxPayConfigHolder.set(key);
+ log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, key);
+ return this;
+ }
+ }
+
+ throw new WxRuntimeException(String.format("无法找到对应mchId=【%s】的商户号配置信息,请核实!", mchId));
+ }
+
public String getConfigKey(String mchId, String appId) {
return mchId + "_" + appId;
}
@@ -219,9 +368,9 @@ public String getPayBaseUrl() {
if (StringUtils.isNotBlank(this.getConfig().getApiV3Key())) {
throw new WxRuntimeException("微信支付V3 目前不支持沙箱模式!");
}
- return this.getConfig().getApiHostUrl() + "/xdc/apiv2sandbox";
+ return this.getConfig().getApiHostWithPathPrefix() + "/xdc/apiv2sandbox";
}
- return this.getConfig().getApiHostUrl();
+ return this.getConfig().getApiHostWithPathPrefix();
}
@Override
@@ -250,6 +399,28 @@ public WxPayRefundResult refundV2(WxPayRefundRequest request) throws WxPayExcept
@Override
public WxPayRefundV3Result refundV3(WxPayRefundV3Request request) throws WxPayException {
+ if (StringUtils.isBlank(request.getNotifyUrl())) {
+ request.setNotifyUrl(this.getConfig().getRefundNotifyUrl());
+ }
+ String url = String.format("%s/v3/refund/domestic/refunds", this.getPayBaseUrl());
+ String response = this.postV3WithWechatpaySerial(url, GSON.toJson(request));
+ return GSON.fromJson(response, WxPayRefundV3Result.class);
+ }
+
+ @Override
+ public WxPayRefundV3Result partnerRefundV3(WxPayPartnerRefundV3Request request) throws WxPayException {
+ if (StringUtils.isBlank(request.getSpAppid())) {
+ request.setSpAppid(this.getConfig().getAppId());
+ }
+ if (StringUtils.isBlank(request.getSubAppid()) && StringUtils.isNotBlank(this.getConfig().getSubAppId())) {
+ request.setSubAppid(this.getConfig().getSubAppId());
+ }
+ if (StringUtils.isBlank(request.getNotifyUrl())) {
+ request.setNotifyUrl(this.getConfig().getRefundNotifyUrl());
+ }
+ if (StringUtils.isBlank(request.getSubMchid())) {
+ request.setSubMchid(this.getConfig().getSubMchId());
+ }
String url = String.format("%s/v3/refund/domestic/refunds", this.getPayBaseUrl());
String response = this.postV3WithWechatpaySerial(url, GSON.toJson(request));
return GSON.fromJson(response, WxPayRefundV3Result.class);
@@ -321,6 +492,13 @@ public WxPayOrderNotifyResult parseOrderNotifyResult(String xmlData) throws WxPa
public WxPayOrderNotifyResult parseOrderNotifyResult(String xmlData, String signType) throws WxPayException {
try {
log.debug("微信支付异步通知请求参数:{}", xmlData);
+
+ // 检测数据格式并给出适当的处理建议
+ if (xmlData != null && xmlData.trim().startsWith("{")) {
+ throw new WxPayException("检测到V3版本的JSON格式通知数据,请使用parseOrderNotifyV3Result方法解析。" +
+ " V3 API需要传入SignatureHeader参数进行签名验证。");
+ }
+
WxPayOrderNotifyResult result = WxPayOrderNotifyResult.fromXML(xmlData);
if (signType == null) {
this.switchover(result.getMchId(), result.getAppid());
@@ -350,7 +528,8 @@ public WxPayOrderNotifyResult parseOrderNotifyResult(String xmlData, String sign
* @param data 通知数据
* @return true:校验通过 false:校验不通过
*/
- private boolean verifyNotifySign(SignatureHeader header, String data) throws WxSignTestException {
+ @Override
+ public boolean verifyNotifySign(SignatureHeader header, String data) throws WxSignTestException {
String wxPaySign = header.getSignature();
if (wxPaySign.startsWith("WECHATPAY/SIGNTEST/")) {
throw new WxSignTestException("微信支付签名探测流量");
@@ -458,6 +637,11 @@ public WxPayPartnerRefundNotifyV3Result parsePartnerRefundNotifyV3Result(String
return this.baseParseOrderNotifyV3Result(notifyData, header, WxPayPartnerRefundNotifyV3Result.class, WxPayPartnerRefundNotifyV3Result.DecryptNotifyResult.class);
}
+ @Override
+ public PartnerSubscribeNotifyResult parsePartnerSubscribeNotify(String notifyData, SignatureHeader header) throws WxPayException {
+ return this.baseParseOrderNotifyV3Result(notifyData, header, PartnerSubscribeNotifyResult.class, PartnerSubscribeNotifyResult.DecryptNotifyResult.class);
+ }
+
@Override
public WxScanPayNotifyResult parseScanPayNotifyResult(String xmlData, @Deprecated String signType) throws WxPayException {
try {
@@ -778,7 +962,7 @@ public WxPayUnifiedOrderV3Result unifiedPartnerOrderV3(TradeTypeEnum tradeType,
request.setSubMchId(this.getConfig().getSubMchId());
}
- String url = this.getPayBaseUrl() + tradeType.getBasePartnerUrl();
+ String url = this.getPayBaseUrl() + tradeType.getPartnerUrl();
String response = this.postV3WithWechatpaySerial(url, GSON.toJson(request));
return GSON.fromJson(response, WxPayUnifiedOrderV3Result.class);
}
@@ -795,7 +979,7 @@ public WxPayUnifiedOrderV3Result unifiedOrderV3(TradeTypeEnum tradeType, WxPayUn
request.setNotifyUrl(this.getConfig().getNotifyUrl());
}
- String url = this.getPayBaseUrl() + tradeType.getPartnerUrl();
+ String url = this.getPayBaseUrl() + tradeType.getMerchantUrl();
String response = this.postV3WithWechatpaySerial(url, GSON.toJson(request));
return GSON.fromJson(response, WxPayUnifiedOrderV3Result.class);
}
@@ -1178,15 +1362,40 @@ public WxPayMicropayResult micropay(WxPayMicropayRequest request) throws WxPayEx
@Override
public WxPayCodepayResult codepay(WxPayCodepayRequest request) throws WxPayException {
- if (StringUtils.isBlank(request.getAppid())) {
- request.setAppid(this.getConfig().getAppId());
- }
- if (StringUtils.isBlank(request.getMchid())) {
- request.setMchid(this.getConfig().getMchId());
+ // 判断是否为服务商模式:如果设置了sp_appid或sp_mchid或sub_mchid中的任何一个,则认为是服务商模式
+ boolean isPartnerMode = StringUtils.isNotBlank(request.getSpAppid())
+ || StringUtils.isNotBlank(request.getSpMchid())
+ || StringUtils.isNotBlank(request.getSubMchid());
+
+ if (isPartnerMode) {
+ // 服务商模式
+ if (StringUtils.isBlank(request.getSpAppid())) {
+ request.setSpAppid(this.getConfig().getAppId());
+ }
+ if (StringUtils.isBlank(request.getSpMchid())) {
+ request.setSpMchid(this.getConfig().getMchId());
+ }
+ if (StringUtils.isBlank(request.getSubAppid())) {
+ request.setSubAppid(this.getConfig().getSubAppId());
+ }
+ if (StringUtils.isBlank(request.getSubMchid())) {
+ request.setSubMchid(this.getConfig().getSubMchId());
+ }
+ String url = String.format("%s/v3/pay/partner/transactions/codepay", this.getPayBaseUrl());
+ String body = this.postV3WithWechatpaySerial(url, GSON.toJson(request));
+ return GSON.fromJson(body, WxPayCodepayResult.class);
+ } else {
+ // 直连商户模式
+ if (StringUtils.isBlank(request.getAppid())) {
+ request.setAppid(this.getConfig().getAppId());
+ }
+ if (StringUtils.isBlank(request.getMchid())) {
+ request.setMchid(this.getConfig().getMchId());
+ }
+ String url = String.format("%s/v3/pay/transactions/codepay", this.getPayBaseUrl());
+ String body = this.postV3WithWechatpaySerial(url, GSON.toJson(request));
+ return GSON.fromJson(body, WxPayCodepayResult.class);
}
- String url = String.format("%s/v3/pay/transactions/codepay", this.getPayBaseUrl());
- String body = this.postV3WithWechatpaySerial(url, GSON.toJson(request));
- return GSON.fromJson(body, WxPayCodepayResult.class);
}
@Override
@@ -1415,4 +1624,9 @@ public BankService getBankService() {
public TransferService getTransferService() {
return transferService;
}
+
+ @Override
+ public BusinessOperationTransferService getBusinessOperationTransferService() {
+ return businessOperationTransferService;
+ }
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImpl.java
index 49c400538d..8ed8286c9a 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImpl.java
@@ -4,7 +4,7 @@
import com.github.binarywang.wxpay.bean.businesscircle.PaidResult;
import com.github.binarywang.wxpay.bean.businesscircle.PointsNotifyRequest;
import com.github.binarywang.wxpay.bean.businesscircle.RefundResult;
-import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.BusinessCircleService;
import com.github.binarywang.wxpay.service.WxPayService;
@@ -16,7 +16,6 @@
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
-import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.Objects;
@@ -38,22 +37,9 @@ public void notifyPoints(PointsNotifyRequest request) throws WxPayException {
this.payService.postV3WithWechatpaySerial(url, GSON.toJson(request));
}
- /**
- * 校验通知签名
- *
- * @param header 通知头信息
- * @param data 通知数据
- * @return true:校验通过 false:校验不通过
- */
- private boolean verifyNotifySign(SignatureHeader header, String data) {
- String beforeSign = String.format("%s\n%s\n%s\n", header.getTimeStamp(), header.getNonce(), data);
- return payService.getConfig().getVerifier().verify(header.getSerialNo(),
- beforeSign.getBytes(StandardCharsets.UTF_8), header.getSigned());
- }
-
@Override
public BusinessCircleNotifyData parseNotifyData(String data, SignatureHeader header) throws WxPayException {
- if (Objects.nonNull(header) && !this.verifyNotifySign(header, data)) {
+ if (Objects.nonNull(header) && !this.payService.verifyNotifySign(header, data)) {
throw new WxPayException("非法请求,头部信息验证失败");
}
return GSON.fromJson(data, BusinessCircleNotifyData.class);
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessOperationTransferServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessOperationTransferServiceImpl.java
new file mode 100644
index 0000000000..098db127e3
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessOperationTransferServiceImpl.java
@@ -0,0 +1,74 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.transfer.*;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.BusinessOperationTransferService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.v3.util.RsaCryptoUtil;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * 运营工具-商家转账API实现
+ *
+ * @author WxJava Team
+ * @see 运营工具-商家转账API
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class BusinessOperationTransferServiceImpl implements BusinessOperationTransferService {
+
+ private static final Gson GSON = new GsonBuilder().create();
+ private final WxPayService wxPayService;
+
+ @Override
+ public BusinessOperationTransferResult createOperationTransfer(BusinessOperationTransferRequest request) throws WxPayException {
+ // 设置默认appid
+ if (StringUtils.isEmpty(request.getAppid())) {
+ request.setAppid(this.wxPayService.getConfig().getAppId());
+ }
+
+ String url = String.format("%s/v3/fund-app/mch-transfer/transfer-bills", this.wxPayService.getPayBaseUrl());
+
+ // 如果传入了用户姓名,需要进行RSA加密
+ if (StringUtils.isNotEmpty(request.getUserName())) {
+ X509Certificate validCertificate = this.wxPayService.getConfig().getVerifier().getValidCertificate();
+ RsaCryptoUtil.encryptFields(request, validCertificate);
+ }
+
+ String response = wxPayService.postV3WithWechatpaySerial(url, GSON.toJson(request));
+ return GSON.fromJson(response, BusinessOperationTransferResult.class);
+ }
+
+ @Override
+ public BusinessOperationTransferQueryResult queryOperationTransfer(BusinessOperationTransferQueryRequest request) throws WxPayException {
+ if (StringUtils.isNotEmpty(request.getOutBillNo())) {
+ return queryOperationTransferByOutBillNo(request.getOutBillNo());
+ } else if (StringUtils.isNotEmpty(request.getTransferBillNo())) {
+ return queryOperationTransferByTransferBillNo(request.getTransferBillNo());
+ } else {
+ throw new WxPayException("商户单号(out_bill_no)和微信转账单号(transfer_bill_no)必须提供其中一个");
+ }
+ }
+
+ @Override
+ public BusinessOperationTransferQueryResult queryOperationTransferByOutBillNo(String outBillNo) throws WxPayException {
+ String url = String.format("%s/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/%s",
+ this.wxPayService.getPayBaseUrl(), outBillNo);
+ String response = wxPayService.getV3(url);
+ return GSON.fromJson(response, BusinessOperationTransferQueryResult.class);
+ }
+
+ @Override
+ public BusinessOperationTransferQueryResult queryOperationTransferByTransferBillNo(String transferBillNo) throws WxPayException {
+ String url = String.format("%s/v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/%s",
+ this.wxPayService.getPayBaseUrl(), transferBillNo);
+ String response = wxPayService.getV3(url);
+ return GSON.fromJson(response, BusinessOperationTransferQueryResult.class);
+ }
+}
\ No newline at end of file
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 171535c992..0f99d428fc 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
@@ -3,7 +3,15 @@
import com.github.binarywang.wxpay.bean.ecommerce.*;
import com.github.binarywang.wxpay.bean.ecommerce.enums.FundBillTypeEnum;
import com.github.binarywang.wxpay.bean.ecommerce.enums.SpAccountTypeEnum;
-import com.github.binarywang.wxpay.bean.ecommerce.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.bean.notify.CombineNotifyResult;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
+import com.github.binarywang.wxpay.bean.notify.WxPayPartnerNotifyV3Result;
+import com.github.binarywang.wxpay.bean.request.*;
+import com.github.binarywang.wxpay.bean.result.CombineQueryResult;
+import com.github.binarywang.wxpay.bean.result.CombineTransactionsResult;
+import com.github.binarywang.wxpay.bean.result.WxPayPartnerOrderQueryV3Result;
+import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.EcommerceService;
import com.github.binarywang.wxpay.service.WxPayService;
@@ -28,9 +36,7 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
-import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
-import java.text.DateFormat;
import java.util.*;
@RequiredArgsConstructor
@@ -38,10 +44,6 @@ public class EcommerceServiceImpl implements EcommerceService {
private static final Gson GSON = new GsonBuilder().create();
- // https://stackoverflow.com/questions/6873020/gson-date-format
- // gson default date format not match, so custom DateFormat
- // detail DateFormat: FULL,LONG,SHORT,MEDIUM
- private static final Gson GSON_CUSTOM = new GsonBuilder().setDateFormat(DateFormat.FULL, DateFormat.FULL).create();
private final WxPayService payService;
@Override
@@ -68,104 +70,53 @@ public ApplymentsStatusResult queryApplyStatusByOutRequestNo(String outRequestNo
}
@Override
- public TransactionsResult combine(TradeTypeEnum tradeType, CombineTransactionsRequest request) throws WxPayException {
- String url = this.payService.getPayBaseUrl() + tradeType.getCombineUrl();
- String response = this.payService.postV3(url, GSON.toJson(request));
- return GSON.fromJson(response, TransactionsResult.class);
+ public CombineTransactionsResult combine(TradeTypeEnum tradeType, CombineTransactionsRequest request) throws WxPayException {
+ return this.payService.combine(tradeType, request);
}
@Override
public T combineTransactions(TradeTypeEnum tradeType, CombineTransactionsRequest request) throws WxPayException {
- TransactionsResult result = this.combine(tradeType, request);
- return result.getPayInfo(tradeType, request.getCombineAppid(),
- request.getCombineMchid(), payService.getConfig().getPrivateKey());
+ return this.payService.combineTransactions(tradeType, request);
}
@Override
- public CombineTransactionsNotifyResult parseCombineNotifyResult(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);
- CombineTransactionsResult transactionsResult = GSON.fromJson(result, CombineTransactionsResult.class);
-
- CombineTransactionsNotifyResult notifyResult = new CombineTransactionsNotifyResult();
- notifyResult.setRawData(response);
- notifyResult.setResult(transactionsResult);
- return notifyResult;
- } catch (GeneralSecurityException | IOException e) {
- throw new WxPayException("解析报文异常!", e);
- }
+ public CombineNotifyResult parseCombineNotifyResult(String notifyData, SignatureHeader header) throws WxPayException {
+ return this.payService.parseCombineNotifyResult(notifyData, header);
}
@Override
- public CombineTransactionsResult queryCombineTransactions(String outTradeNo) throws WxPayException {
- String url = String.format("%s/v3/combine-transactions/out-trade-no/%s", this.payService.getPayBaseUrl(), outTradeNo);
- String response = this.payService.getV3(url);
- return GSON.fromJson(response, CombineTransactionsResult.class);
+ public CombineQueryResult queryCombine(String combineOutTradeNo) throws WxPayException {
+ return this.payService.queryCombine(combineOutTradeNo);
}
@Override
- public TransactionsResult partner(TradeTypeEnum tradeType, PartnerTransactionsRequest request) throws WxPayException {
- String url = this.payService.getPayBaseUrl() + tradeType.getPartnerUrl();
- String response = this.payService.postV3(url, GSON.toJson(request));
- return GSON.fromJson(response, TransactionsResult.class);
+ public void closeCombine(CombineCloseRequest request) throws WxPayException {
+ this.payService.closeCombine(request);
}
@Override
- public T partnerTransactions(TradeTypeEnum tradeType, PartnerTransactionsRequest request) throws WxPayException {
- TransactionsResult result = this.partner(tradeType, request);
- String appId = request.getSubAppid() != null ? request.getSubAppid() : request.getSpAppid();
- return result.getPayInfo(tradeType, appId,
- request.getSpMchid(), payService.getConfig().getPrivateKey());
+ public WxPayUnifiedOrderV3Result unifiedPartnerOrder(TradeTypeEnum tradeType, WxPayPartnerUnifiedOrderV3Request request) throws WxPayException {
+ return this.payService.unifiedPartnerOrderV3(tradeType, request);
}
@Override
- public PartnerTransactionsNotifyResult parsePartnerNotifyResult(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);
- PartnerTransactionsResult transactionsResult = GSON.fromJson(result, PartnerTransactionsResult.class);
+ public T createPartnerOrder(TradeTypeEnum tradeType, WxPayPartnerUnifiedOrderV3Request request) throws WxPayException {
+ return this.payService.createPartnerOrderV3(tradeType, request);
+ }
- PartnerTransactionsNotifyResult notifyResult = new PartnerTransactionsNotifyResult();
- notifyResult.setRawData(response);
- notifyResult.setResult(transactionsResult);
- return notifyResult;
- } catch (GeneralSecurityException | IOException e) {
- throw new WxPayException("解析报文异常!", e);
- }
+ @Override
+ public WxPayPartnerNotifyV3Result parsePartnerNotifyResult(String notifyData, SignatureHeader header) throws WxPayException {
+ return this.payService.parsePartnerOrderNotifyV3Result(notifyData, header);
}
@Override
- public PartnerTransactionsResult queryPartnerTransactions(PartnerTransactionsQueryRequest request) throws WxPayException {
- String url = String.format("%s/v3/pay/partner/transactions/out-trade-no/%s", this.payService.getPayBaseUrl(), request.getOutTradeNo());
- if (Objects.isNull(request.getOutTradeNo())) {
- url = String.format("%s/v3/pay/partner/transactions/id/%s", this.payService.getPayBaseUrl(), request.getTransactionId());
- }
- String query = String.format("?sp_mchid=%s&sub_mchid=%s", request.getSpMchid(), request.getSubMchid());
- String response = this.payService.getV3(url + query);
- return GSON.fromJson(response, PartnerTransactionsResult.class);
+ public WxPayPartnerOrderQueryV3Result queryPartnerOrder(WxPayPartnerOrderQueryV3Request request) throws WxPayException {
+ return this.payService.queryPartnerOrderV3(request);
}
@Override
- public String closePartnerTransactions(PartnerTransactionsCloseRequest request) throws WxPayException {
- String url = String.format("%s/v3/pay/partner/transactions/out-trade-no/%s/close", this.payService.getPayBaseUrl(), request.getOutTradeNo());
- return this.payService.postV3(url, GSON.toJson(request));
+ public void closePartnerOrder(WxPayPartnerOrderCloseV3Request request) throws WxPayException {
+ this.payService.closePartnerOrderV3(request);
}
@Override
@@ -318,7 +269,7 @@ public RefundQueryResult queryRefundByOutRefundNo(String subMchid, String outRef
@Override
public RefundNotifyResult parseRefundNotifyResult(String notifyData, SignatureHeader header) throws WxPayException {
- if (Objects.nonNull(header) && !this.verifyNotifySign(header, notifyData)) {
+ if (Objects.nonNull(header) && !payService.verifyNotifySign(header, notifyData)) {
throw new WxPayException("非法请求,头部信息验证失败");
}
NotifyResponse response = GSON.fromJson(notifyData, NotifyResponse.class);
@@ -339,7 +290,7 @@ public RefundNotifyResult parseRefundNotifyResult(String notifyData, SignatureHe
@Override
public WithdrawNotifyResult parseWithdrawNotifyResult(String notifyData, SignatureHeader header) throws WxPayException {
- if (Objects.nonNull(header) && !this.verifyNotifySign(header, notifyData)) {
+ if (Objects.nonNull(header) && !payService.verifyNotifySign(header, notifyData)) {
throw new WxPayException("非法请求,头部信息验证失败");
}
NotifyResponse response = GSON.fromJson(notifyData, NotifyResponse.class);
@@ -491,22 +442,6 @@ public AccountCancelApplicationsMediaResult uploadMediaAccountCancelApplication(
}
}
- /**
- * 校验通知签名
- *
- * @param header 通知头信息
- * @param data 通知数据
- * @return true:校验通过 false:校验不通过
- */
- private boolean verifyNotifySign(SignatureHeader header, String data) {
- String beforeSign = String.format("%s\n%s\n%s\n",
- header.getTimeStamp(),
- header.getNonce(),
- data);
- return payService.getConfig().getVerifier().verify(header.getSerialNo(),
- beforeSign.getBytes(StandardCharsets.UTF_8), header.getSigned());
- }
-
/**
* 对象拼接到url
*
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MarketingFavorServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MarketingFavorServiceImpl.java
index 0f84d5f126..6352eb8f40 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MarketingFavorServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MarketingFavorServiceImpl.java
@@ -1,6 +1,6 @@
package com.github.binarywang.wxpay.service.impl;
-import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.bean.marketing.*;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.MarketingFavorService;
@@ -175,22 +175,9 @@ public FavorStocksRestartResult restartFavorStocksV3(String stockId, FavorStocks
return GSON.fromJson(result, FavorStocksRestartResult.class);
}
- /**
- * 校验通知签名
- *
- * @param header 通知头信息
- * @param data 通知数据
- * @return true:校验通过 false:校验不通过
- */
- private boolean verifyNotifySign(SignatureHeader header, String data) {
- String beforeSign = String.format("%s\n%s\n%s\n", header.getTimeStamp(), header.getNonce(), data);
- return payService.getConfig().getVerifier().verify(header.getSerialNo(),
- beforeSign.getBytes(StandardCharsets.UTF_8), header.getSigned());
- }
-
@Override
public UseNotifyData parseNotifyData(String data, SignatureHeader header) throws WxPayException {
- if (Objects.nonNull(header) && !this.verifyNotifySign(header, data)) {
+ if (Objects.nonNull(header) && !payService.verifyNotifySign(header, data)) {
throw new WxPayException("非法请求,头部信息验证失败");
}
return GSON.fromJson(data, UseNotifyData.class);
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantLimitationServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantLimitationServiceImpl.java
new file mode 100644
index 0000000000..d946336e31
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantLimitationServiceImpl.java
@@ -0,0 +1,28 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.merchantlimitation.MerchantLimitationResult;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.MerchantLimitationService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 商户被管控能力及原因查询 接口实现
+ *
+ * @author zhangyl
+ */
+@RequiredArgsConstructor
+public class MerchantLimitationServiceImpl implements MerchantLimitationService {
+ private final WxPayService payService;
+ private static final Gson GSON = new GsonBuilder().create();
+
+ @Override
+ public MerchantLimitationResult fetchLimitations(String subMchId) throws WxPayException {
+ String url = String.format("%s/v3/mch-operation-manage/merchant-limitations/sub-mchid/%s",
+ this.payService.getPayBaseUrl(), subMchId);
+ String result = this.payService.getV3(url);
+ return GSON.fromJson(result, MerchantLimitationResult.class);
+ }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImpl.java
index 7952513f56..ee77f5e974 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImpl.java
@@ -1,6 +1,7 @@
package com.github.binarywang.wxpay.service.impl;
import com.github.binarywang.wxpay.bean.media.ImageUploadResult;
+import com.github.binarywang.wxpay.bean.media.VideoUploadResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.MerchantMediaService;
import com.github.binarywang.wxpay.service.WxPayService;
@@ -40,7 +41,7 @@ public ImageUploadResult imageUploadV3(File imageFile) throws WxPayException,IOE
@Override
public ImageUploadResult imageUploadV3(InputStream inputStream, String fileName) throws WxPayException, IOException {
String url = String.format("%s/v3/merchant/media/upload", this.payService.getPayBaseUrl());
- try(ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[2048];
int len;
while ((len = inputStream.read(buffer)) > -1) {
@@ -57,4 +58,40 @@ public ImageUploadResult imageUploadV3(InputStream inputStream, String fileName)
}
}
+ @Override
+ public VideoUploadResult videoUploadV3(File videoFile) throws WxPayException, IOException {
+ String url = String.format("%s/v3/merchant/media/video_upload", this.payService.getPayBaseUrl());
+
+ try (FileInputStream s1 = new FileInputStream(videoFile)) {
+ String sha256 = DigestUtils.sha256Hex(s1);
+ try (InputStream s2 = new FileInputStream(videoFile)) {
+ WechatPayUploadHttpPost request = new WechatPayUploadHttpPost.Builder(URI.create(url))
+ .withVideo(videoFile.getName(), sha256, s2)
+ .build();
+ String result = this.payService.postV3(url, request);
+ return VideoUploadResult.fromJson(result);
+ }
+ }
+ }
+
+ @Override
+ public VideoUploadResult videoUploadV3(InputStream inputStream, String fileName) throws WxPayException, IOException {
+ String url = String.format("%s/v3/merchant/media/video_upload", this.payService.getPayBaseUrl());
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ byte[] buffer = new byte[2048];
+ int len;
+ while ((len = inputStream.read(buffer)) > -1) {
+ bos.write(buffer, 0, len);
+ }
+ bos.flush();
+ byte[] data = bos.toByteArray();
+ String sha256 = DigestUtils.sha256Hex(data);
+ WechatPayUploadHttpPost request = new WechatPayUploadHttpPost.Builder(URI.create(url))
+ .withVideo(fileName, sha256, new ByteArrayInputStream(data))
+ .build();
+ String result = this.payService.postV3(url, request);
+ return VideoUploadResult.fromJson(result);
+ }
+ }
+
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantTransferServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantTransferServiceImpl.java
index 8974ca7e2b..2d6becb479 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantTransferServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantTransferServiceImpl.java
@@ -1,6 +1,12 @@
package com.github.binarywang.wxpay.service.impl;
import com.github.binarywang.wxpay.bean.merchanttransfer.*;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
+import com.github.binarywang.wxpay.bean.transfer.ReservationTransferBatchGetResult;
+import com.github.binarywang.wxpay.bean.transfer.ReservationTransferBatchRequest;
+import com.github.binarywang.wxpay.bean.transfer.ReservationTransferBatchResult;
+import com.github.binarywang.wxpay.bean.transfer.ReservationTransferNotifyResult;
+import com.github.binarywang.wxpay.bean.transfer.UserAuthorizationStatusResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.MerchantTransferService;
import com.github.binarywang.wxpay.service.WxPayService;
@@ -92,14 +98,15 @@ public DetailsQueryResult queryMerchantDetails(MerchantDetailsQueryRequest reque
@Override
public ElectronicBillResult applyElectronicBill(ElectronicBillApplyRequest request) throws WxPayException {
- String url = String.format("%s/v3/transfer/bill-receipt", this.wxPayService.getPayBaseUrl());
+ String url = String.format("%s/v3/fund-app/mch-transfer/elecsign/out-bill-no", this.wxPayService.getPayBaseUrl());
String response = wxPayService.postV3(url, GSON.toJson(request));
return GSON.fromJson(response, ElectronicBillResult.class);
}
@Override
- public ElectronicBillResult queryElectronicBill(String outBatchNo) throws WxPayException {
- String url = String.format("%s/v3/transfer/bill-receipt/%s", this.wxPayService.getPayBaseUrl(), outBatchNo);
+ public ElectronicBillResult queryElectronicBill(String outBillNo) throws WxPayException {
+ String url = String.format("%s/v3/fund-app/mch-transfer/elecsign/out-bill-no/%s",
+ this.wxPayService.getPayBaseUrl(), outBillNo);
String response = wxPayService.getV3(url);
return GSON.fromJson(response, ElectronicBillResult.class);
}
@@ -124,4 +131,40 @@ public DetailElectronicBillResult queryDetailElectronicBill(DetailElectronicBill
return GSON.fromJson(response, DetailElectronicBillResult.class);
}
+ @Override
+ public UserAuthorizationStatusResult getUserAuthorizationStatus(String openid, String transferSceneId) throws WxPayException {
+ return this.wxPayService.getTransferService().getUserAuthorizationStatus(openid, transferSceneId);
+ }
+
+ @Override
+ public ReservationTransferBatchResult reservationTransferBatch(ReservationTransferBatchRequest request) throws WxPayException {
+ return this.wxPayService.getTransferService().reservationTransferBatch(request);
+ }
+
+ @Override
+ public ReservationTransferBatchGetResult getReservationTransferBatchByOutBatchNo(String outBatchNo, Boolean needQueryDetail,
+ Integer offset, Integer limit, String detailState) throws WxPayException {
+ return this.wxPayService.getTransferService()
+ .getReservationTransferBatchByOutBatchNo(outBatchNo, needQueryDetail, offset, limit, detailState);
+ }
+
+ @Override
+ public ReservationTransferBatchGetResult getReservationTransferBatchByReservationBatchNo(String reservationBatchNo,
+ Boolean needQueryDetail,
+ Integer offset, Integer limit,
+ String detailState) throws WxPayException {
+ return this.wxPayService.getTransferService()
+ .getReservationTransferBatchByReservationBatchNo(reservationBatchNo, needQueryDetail, offset, limit, detailState);
+ }
+
+ @Override
+ public ReservationTransferNotifyResult parseReservationTransferNotifyResult(String notifyData, SignatureHeader header) throws WxPayException {
+ return this.wxPayService.getTransferService().parseReservationTransferNotifyResult(notifyData, header);
+ }
+
+ @Override
+ public void closeReservationTransferBatch(String outBatchNo) throws WxPayException {
+ this.wxPayService.getTransferService().closeReservationTransferBatch(outBatchNo);
+ }
+
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MiPayServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MiPayServiceImpl.java
new file mode 100644
index 0000000000..769b789fa3
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MiPayServiceImpl.java
@@ -0,0 +1,68 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.mipay.MedInsOrdersRequest;
+import com.github.binarywang.wxpay.bean.mipay.MedInsOrdersResult;
+import com.github.binarywang.wxpay.bean.mipay.MedInsRefundNotifyRequest;
+import com.github.binarywang.wxpay.bean.notify.MiPayNotifyV3Result;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.MiPayService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.v3.util.RsaCryptoUtil;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.security.cert.X509Certificate;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 医保相关接口
+ * 医保相关接口
+ * @author xgl
+ * @date 2025/12/20
+ */
+@RequiredArgsConstructor
+public class MiPayServiceImpl implements MiPayService {
+
+ private final WxPayService payService;
+ private static final Gson GSON = new GsonBuilder().create();
+
+
+ @Override
+ public MedInsOrdersResult medInsOrders(MedInsOrdersRequest request) throws WxPayException {
+
+ String url = String.format("%s/v3/med-ins/orders", this.payService.getPayBaseUrl());
+ X509Certificate validCertificate = this.payService.getConfig().getVerifier().getValidCertificate();
+
+ RsaCryptoUtil.encryptFields(request, validCertificate);
+
+ String result = this.payService.postV3WithWechatpaySerial(url, GSON.toJson(request));
+ return GSON.fromJson(result, MedInsOrdersResult.class);
+ }
+
+ @Override
+ public MedInsOrdersResult getMedInsOrderByMixTradeNo(String mixTradeNo, String subMchid) throws WxPayException {
+ String url = String.format("%s/v3/med-ins/orders/mix-trade-no/%s?sub_mchid=%s", this.payService.getPayBaseUrl(), mixTradeNo, subMchid);
+ String result = this.payService.getV3(url);
+ return GSON.fromJson(result, MedInsOrdersResult.class);
+ }
+
+ @Override
+ public MedInsOrdersResult getMedInsOrderByOutTradeNo(String outTradeNo, String subMchid) throws WxPayException {
+ String url = String.format("%s/v3/med-ins/orders/out-trade-no/%s?sub_mchid=%s", this.payService.getPayBaseUrl(), outTradeNo, subMchid);
+ String result = this.payService.getV3(url);
+ return GSON.fromJson(result, MedInsOrdersResult.class);
+ }
+
+ @Override
+ public MiPayNotifyV3Result parseMiPayNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException {
+ return this.payService.baseParseOrderNotifyV3Result(notifyData, header, MiPayNotifyV3Result.class, MiPayNotifyV3Result.DecryptNotifyResult.class);
+ }
+
+ @Override
+ public void medInsRefundNotify(MedInsRefundNotifyRequest request, String mixTradeNo) throws WxPayException {
+ String url = String.format("%s/v3/med-ins/refunds/notify?mix_trade_no=%s", this.payService.getPayBaseUrl(), mixTradeNo);
+ this.payService.postV3(url, GSON.toJson(request));
+ }
+
+
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerPayScoreServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerPayScoreServiceImpl.java
index 55c913e79c..b7ba4a6c03 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerPayScoreServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerPayScoreServiceImpl.java
@@ -1,6 +1,6 @@
package com.github.binarywang.wxpay.service.impl;
-import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.bean.payscore.*;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.exception.WxPayException;
@@ -316,7 +316,7 @@ public WxPartnerUserAuthorizationStatusNotifyResult parseUserAuthorizationStatus
@Override
public PayScoreNotifyData parseNotifyData(String data, SignatureHeader header) throws WxPayException {
- if (Objects.nonNull(header) && !this.verifyNotifySign(header, data)) {
+ if (Objects.nonNull(header) && !this.payService.verifyNotifySign(header, data)) {
throw new WxPayException("非法请求,头部信息验证失败");
}
return GSON.fromJson(data, PayScoreNotifyData.class);
@@ -335,20 +335,4 @@ public WxPartnerPayScoreResult decryptNotifyDataResource(PayScoreNotifyData data
throw new WxPayException("解析报文异常!", e);
}
}
-
- /**
- * 校验通知签名
- *
- * @param header 通知头信息
- * @param data 通知数据
- * @return true:校验通过 false:校验不通过
- */
- private boolean verifyNotifySign(SignatureHeader header, String data) {
- String beforeSign = String.format("%s\n%s\n%s\n", header.getTimeStamp(), header.getNonce(), data);
- return this.payService.getConfig().getVerifier().verify(
- header.getSerialNo(),
- beforeSign.getBytes(StandardCharsets.UTF_8),
- header.getSigned()
- );
- }
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerPayScoreSignPlanServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerPayScoreSignPlanServiceImpl.java
index e81454bb75..4553bf9797 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerPayScoreSignPlanServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerPayScoreSignPlanServiceImpl.java
@@ -1,6 +1,6 @@
package com.github.binarywang.wxpay.service.impl;
-import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.bean.payscore.*;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.PartnerPayScoreService;
@@ -260,7 +260,7 @@ public PartnerUserSignPlanEntity parseSignPlanNotifyResult(String notifyData, Si
* @return {@link PayScoreNotifyData}
**/
public PayScoreNotifyData parseNotifyData(String data, SignatureHeader header) throws WxPayException {
- if (Objects.nonNull(header) && !verifyNotifySign(header, data)) {
+ if (Objects.nonNull(header) && !payService.verifyNotifySign(header, data)) {
throw new WxPayException("非法请求,头部信息验证失败");
}
return GSON.fromJson(data, PayScoreNotifyData.class);
@@ -289,20 +289,4 @@ public PartnerUserSignPlanEntity decryptNotifyDataResource(PayScoreNotifyData da
}
}
- /**
- * 校验通知签名
- *
- * @param header 通知头信息
- * @param data 通知数据
- *
- * @return true:校验通过 false:校验不通过
- */
- private boolean verifyNotifySign(SignatureHeader header, String data) {
- String beforeSign = String.format("%s\n%s\n%s\n", header.getTimeStamp(), header.getNonce(), data);
- return this.payService.getConfig().getVerifier().verify(
- header.getSerialNo(),
- beforeSign.getBytes(StandardCharsets.UTF_8),
- header.getSigned()
- );
- }
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerTransferServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerTransferServiceImpl.java
index d5ee9dfebb..0fe6ac860d 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerTransferServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PartnerTransferServiceImpl.java
@@ -186,17 +186,17 @@ public BatchDetailsResult queryBatchDetailByMch(String outBatchNo, String outDet
* 转账电子回单申请受理API
* 接口说明
* 适用对象:直连商户 服务商
- * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/transfer/chapter4_1.shtml
- * 请求URL:https://api.mch.weixin.qq.com/v3/transfer/bill-receipt
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012716452
+ * 请求URL:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/elecsign/out-bill-no
* 请求方式:POST
*
- * @param request 商家批次单号
+ * @param request 商户转账单号
* @return 返回数据 fund balance result
* @throws WxPayException the wx pay exception
*/
@Override
public BillReceiptResult receiptBill(ReceiptBillRequest request) throws WxPayException {
- String url = String.format("%s/v3/transfer/bill-receipt", this.payService.getPayBaseUrl());
+ String url = String.format("%s/v3/fund-app/mch-transfer/elecsign/out-bill-no", this.payService.getPayBaseUrl());
String response = this.payService.postV3(url, GSON.toJson(request));
return GSON.fromJson(response, BillReceiptResult.class);
}
@@ -206,17 +206,18 @@ public BillReceiptResult receiptBill(ReceiptBillRequest request) throws WxPayExc
* 查询转账电子回单API
* 接口说明
* 适用对象:直连商户 服务商
- * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/transfer/chapter4_2.shtml
- * 请求URL:https://api.mch.weixin.qq.com/v3/transfer/bill-receipt/{out_batch_no}
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012716436
+ * 请求URL:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/elecsign/out-bill-no/{out_bill_no}
* 请求方式:GET
*
- * @param outBatchNo 商家批次单号
+ * @param outBillNo 商户转账单号
* @return 返回数据 fund balance result
* @throws WxPayException the wx pay exception
*/
@Override
- public BillReceiptResult queryBillReceipt(String outBatchNo) throws WxPayException {
- String url = String.format("%s/v3/transfer/bill-receipt/%s", this.payService.getPayBaseUrl(), outBatchNo);
+ public BillReceiptResult queryBillReceipt(String outBillNo) throws WxPayException {
+ String url = String.format("%s/v3/fund-app/mch-transfer/elecsign/out-bill-no/%s",
+ this.payService.getPayBaseUrl(), outBillNo);
String response = this.payService.getV3(url);
return GSON.fromJson(response, BillReceiptResult.class);
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayScoreServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayScoreServiceImpl.java
index 249cfa3f56..63c3a5220d 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayScoreServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayScoreServiceImpl.java
@@ -1,6 +1,6 @@
package com.github.binarywang.wxpay.service.impl;
-import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.bean.payscore.PayScoreNotifyData;
import com.github.binarywang.wxpay.bean.payscore.UserAuthorizationStatusNotifyResult;
import com.github.binarywang.wxpay.bean.payscore.WxPayScoreRequest;
@@ -230,7 +230,7 @@ public WxPayScoreResult modifyServiceOrder(WxPayScoreRequest request) throws WxP
if(Strings.isNullOrEmpty(request.getAppid())){
request.setAppid(config.getAppId());
}
- if(Strings.isNullOrEmpty(config.getServiceId())){
+ if(Strings.isNullOrEmpty(request.getServiceId())){
request.setServiceId(config.getServiceId());
}
request.setOutOrderNo(null);
@@ -301,7 +301,7 @@ public UserAuthorizationStatusNotifyResult parseUserAuthorizationStatusNotifyRes
@Override
public PayScoreNotifyData parseNotifyData(String data, SignatureHeader header) throws WxPayException {
- if (Objects.nonNull(header) && !this.verifyNotifySign(header, data)) {
+ if (Objects.nonNull(header) && !payService.verifyNotifySign(header, data)) {
throw new WxPayException("非法请求,头部信息验证失败");
}
return GSON.fromJson(data, PayScoreNotifyData.class);
@@ -321,20 +321,4 @@ public WxPayScoreResult decryptNotifyDataResource(PayScoreNotifyData data) throw
}
}
- /**
- * 校验通知签名
- *
- * @param header 通知头信息
- * @param data 通知数据
- * @return true:校验通过 false:校验不通过
- */
- private boolean verifyNotifySign(SignatureHeader header, String data) throws WxSignTestException {
- String wxPaySign = header.getSigned();
- if(wxPaySign.startsWith("WECHATPAY/SIGNTEST/")){
- throw new WxSignTestException("微信支付签名探测流量");
- }
- String beforeSign = String.format("%s\n%s\n%s\n", header.getTimeStamp(), header.getNonce(), data);
- return payService.getConfig().getVerifier().verify(header.getSerialNo(),
- beforeSign.getBytes(StandardCharsets.UTF_8), header.getSigned());
- }
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImpl.java
index 3d8c831271..85f7ee23dd 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImpl.java
@@ -193,4 +193,27 @@ public WxPayApplyBillV3Result merchantFundWithdrawBillType(String billType, Stri
return GSON.fromJson(response, WxPayApplyBillV3Result.class);
}
+ /**
+ * 微工卡批量转账API
+ * 适用对象:服务商
+ * 请求URL:https://api.mch.weixin.qq.com/v3/payroll-card/transfer-batches
+ * 请求方式:POST
+ *
+ * @param request 请求参数
+ * @return 返回数据
+ * @throws WxPayException the wx pay exception
+ */
+ @Override
+ public PayrollTransferBatchesResult payrollCardTransferBatches(PayrollTransferBatchesRequest request) throws WxPayException {
+ String url = String.format("%s/v3/payroll-card/transfer-batches", payService.getPayBaseUrl());
+ // 对敏感信息进行加密
+ if (request.getTransferDetailList() != null && !request.getTransferDetailList().isEmpty()) {
+ for (PayrollTransferBatchesRequest.TransferDetail detail : request.getTransferDetailList()) {
+ RsaCryptoUtil.encryptFields(detail, payService.getConfig().getVerifier().getValidCertificate());
+ }
+ }
+ String response = payService.postV3WithWechatpaySerial(url, GSON.toJson(request));
+ return GSON.fromJson(response, PayrollTransferBatchesResult.class);
+ }
+
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/RealNameServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/RealNameServiceImpl.java
new file mode 100644
index 0000000000..9a1c57fe0f
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/RealNameServiceImpl.java
@@ -0,0 +1,41 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.realname.RealNameRequest;
+import com.github.binarywang.wxpay.bean.realname.RealNameResult;
+import com.github.binarywang.wxpay.bean.result.BaseWxPayResult;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.RealNameService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import lombok.RequiredArgsConstructor;
+
+/**
+ *
+ * 微信支付实名验证相关服务实现类.
+ * 详见文档:https://pay.wechatpay.cn/doc/v2/merchant/4011987607
+ *
+ *
+ * @author Binary Wang
+ */
+@RequiredArgsConstructor
+public class RealNameServiceImpl implements RealNameService {
+ private final WxPayService payService;
+
+ @Override
+ public RealNameResult queryRealName(RealNameRequest request) throws WxPayException {
+ request.checkAndSign(this.payService.getConfig());
+ String url = this.payService.getPayBaseUrl() + "/userinfo/realnameauth/query";
+
+ String responseContent = this.payService.post(url, request.toXML(), true);
+ RealNameResult result = BaseWxPayResult.fromXML(responseContent, RealNameResult.class);
+ result.checkResult(this.payService, request.getSignType(), true);
+ return result;
+ }
+
+ @Override
+ public RealNameResult queryRealName(String openid) throws WxPayException {
+ RealNameRequest request = RealNameRequest.newBuilder()
+ .openid(openid)
+ .build();
+ return this.queryRealName(request);
+ }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/SubscriptionBillingServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/SubscriptionBillingServiceImpl.java
new file mode 100644
index 0000000000..45c1a9f0d2
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/SubscriptionBillingServiceImpl.java
@@ -0,0 +1,91 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.subscriptionbilling.*;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.SubscriptionBillingService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 微信支付-预约扣费服务实现 (连续包月功能)
+ *
+ * 微信支付预约扣费功能,支持商户在用户授权的情况下,
+ * 按照约定的时间和金额,自动从用户的支付账户中扣取费用。
+ * 主要用于连续包月、订阅服务等场景。
+ *
+ * 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012161105
+ *
+ *
+ * @author Binary Wang
+ * created on 2024-08-31
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class SubscriptionBillingServiceImpl implements SubscriptionBillingService {
+
+ private static final Gson GSON = new GsonBuilder().create();
+ private final WxPayService payService;
+
+ @Override
+ public SubscriptionScheduleResult scheduleSubscription(SubscriptionScheduleRequest request) throws WxPayException {
+ String url = String.format("%s/v3/subscription-billing/schedule", this.payService.getPayBaseUrl());
+ String response = this.payService.postV3(url, GSON.toJson(request));
+ return GSON.fromJson(response, SubscriptionScheduleResult.class);
+ }
+
+ @Override
+ public SubscriptionQueryResult querySubscription(String subscriptionId) throws WxPayException {
+ String url = String.format("%s/v3/subscription-billing/schedule/%s", this.payService.getPayBaseUrl(), subscriptionId);
+ String response = this.payService.getV3(url);
+ return GSON.fromJson(response, SubscriptionQueryResult.class);
+ }
+
+ @Override
+ public SubscriptionCancelResult cancelSubscription(SubscriptionCancelRequest request) throws WxPayException {
+ String url = String.format("%s/v3/subscription-billing/schedule/%s/cancel",
+ this.payService.getPayBaseUrl(), request.getSubscriptionId());
+ String response = this.payService.postV3(url, GSON.toJson(request));
+ return GSON.fromJson(response, SubscriptionCancelResult.class);
+ }
+
+ @Override
+ public SubscriptionInstantBillingResult instantBilling(SubscriptionInstantBillingRequest request) throws WxPayException {
+ String url = String.format("%s/v3/subscription-billing/instant-billing", this.payService.getPayBaseUrl());
+ String response = this.payService.postV3(url, GSON.toJson(request));
+ return GSON.fromJson(response, SubscriptionInstantBillingResult.class);
+ }
+
+ @Override
+ public SubscriptionTransactionQueryResult queryTransactions(SubscriptionTransactionQueryRequest request) throws WxPayException {
+ String url = String.format("%s/v3/subscription-billing/transactions", this.payService.getPayBaseUrl());
+
+ StringBuilder queryString = new StringBuilder();
+ if (request.getOpenid() != null) {
+ queryString.append("openid=").append(request.getOpenid()).append("&");
+ }
+ if (request.getBeginTime() != null) {
+ queryString.append("begin_time=").append(request.getBeginTime()).append("&");
+ }
+ if (request.getEndTime() != null) {
+ queryString.append("end_time=").append(request.getEndTime()).append("&");
+ }
+ if (request.getLimit() != null) {
+ queryString.append("limit=").append(request.getLimit()).append("&");
+ }
+ if (request.getOffset() != null) {
+ queryString.append("offset=").append(request.getOffset()).append("&");
+ }
+
+ if (queryString.length() > 0) {
+ // Remove trailing &
+ queryString.setLength(queryString.length() - 1);
+ url += "?" + queryString.toString();
+ }
+
+ String response = this.payService.getV3(url);
+ return GSON.fromJson(response, SubscriptionTransactionQueryResult.class);
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/TransferServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/TransferServiceImpl.java
index 038af32b87..fe05ab89ad 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/TransferServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/TransferServiceImpl.java
@@ -124,4 +124,85 @@ public TransferBillsGetResult getBillsByTransferBillNo(String transferBillNo) th
public TransferBillsNotifyResult parseTransferBillsNotifyResult(String notifyData, SignatureHeader header) throws WxPayException {
return this.payService.baseParseOrderNotifyV3Result(notifyData, header, TransferBillsNotifyResult.class, TransferBillsNotifyResult.DecryptNotifyResult.class);
}
+
+ // ===================== 用户授权免确认模式相关接口实现 =====================
+
+ @Override
+ public UserAuthorizationStatusResult getUserAuthorizationStatus(String openid, String transferSceneId) throws WxPayException {
+ String url = String.format("%s/v3/fund-app/mch-transfer/authorization/openid/%s?transfer_scene_id=%s",
+ this.payService.getPayBaseUrl(), openid, transferSceneId);
+ String result = this.payService.getV3(url);
+ return GSON.fromJson(result, UserAuthorizationStatusResult.class);
+ }
+
+ @Override
+ public ReservationTransferBatchResult reservationTransferBatch(ReservationTransferBatchRequest request) throws WxPayException {
+ String url = String.format("%s/v3/fund-app/mch-transfer/reservation/transfer-batches", this.payService.getPayBaseUrl());
+ List transferDetailList = request.getTransferDetailList();
+ if (transferDetailList != null && !transferDetailList.isEmpty()) {
+ X509Certificate validCertificate = this.payService.getConfig().getVerifier().getValidCertificate();
+ for (ReservationTransferBatchRequest.TransferDetail detail : transferDetailList) {
+ if (detail.getUserName() != null && !detail.getUserName().isEmpty()) {
+ RsaCryptoUtil.encryptFields(detail, validCertificate);
+ }
+ }
+ }
+ String result = this.payService.postV3WithWechatpaySerial(url, GSON.toJson(request));
+ return GSON.fromJson(result, ReservationTransferBatchResult.class);
+ }
+
+ @Override
+ public ReservationTransferBatchGetResult getReservationTransferBatchByOutBatchNo(String outBatchNo, Boolean needQueryDetail,
+ Integer offset, Integer limit, String detailState) throws WxPayException {
+ String url = buildReservationBatchQueryUrl("out-batch-no", outBatchNo, needQueryDetail, offset, limit, detailState);
+ String result = this.payService.getV3(url);
+ return GSON.fromJson(result, ReservationTransferBatchGetResult.class);
+ }
+
+ @Override
+ public ReservationTransferBatchGetResult getReservationTransferBatchByReservationBatchNo(String reservationBatchNo, Boolean needQueryDetail,
+ Integer offset, Integer limit, String detailState) throws WxPayException {
+ String url = buildReservationBatchQueryUrl("reservation-batch-no", reservationBatchNo, needQueryDetail, offset, limit, detailState);
+ String result = this.payService.getV3(url);
+ return GSON.fromJson(result, ReservationTransferBatchGetResult.class);
+ }
+
+ private String buildReservationBatchQueryUrl(String batchNoType, String batchNo, Boolean needQueryDetail,
+ Integer offset, Integer limit, String detailState) {
+ StringBuilder url = new StringBuilder();
+ url.append(this.payService.getPayBaseUrl())
+ .append("/v3/fund-app/mch-transfer/reservation/transfer-batches/")
+ .append(batchNoType).append("/").append(batchNo);
+
+ boolean hasParams = false;
+ if (needQueryDetail != null) {
+ url.append("?need_query_detail=").append(needQueryDetail);
+ hasParams = true;
+ }
+ if (offset != null) {
+ url.append(hasParams ? "&" : "?").append("offset=").append(offset);
+ hasParams = true;
+ }
+ if (limit != null) {
+ url.append(hasParams ? "&" : "?").append("limit=").append(limit);
+ hasParams = true;
+ }
+ if (detailState != null && !detailState.isEmpty()) {
+ url.append(hasParams ? "&" : "?").append("detail_state=").append(detailState);
+ }
+ return url.toString();
+ }
+
+ @Override
+ public ReservationTransferNotifyResult parseReservationTransferNotifyResult(String notifyData, SignatureHeader header) throws WxPayException {
+ return this.payService.baseParseOrderNotifyV3Result(notifyData, header, ReservationTransferNotifyResult.class,
+ ReservationTransferNotifyResult.DecryptNotifyResult.class);
+ }
+
+ @Override
+ public void closeReservationTransferBatch(String outBatchNo) throws WxPayException {
+ String url = String.format("%s/v3/fund-app/mch-transfer/reservation/transfer-batches/out-batch-no/%s/close",
+ this.payService.getPayBaseUrl(), outBatchNo);
+ this.payService.postV3(url, "");
+ }
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxDepositServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxDepositServiceImpl.java
new file mode 100644
index 0000000000..70bc5a5162
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxDepositServiceImpl.java
@@ -0,0 +1,84 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.request.*;
+import com.github.binarywang.wxpay.bean.result.*;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxDepositService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ *
+ * 微信押金支付服务实现
+ *
+ *
+ * @author Binary Wang
+ * created on 2024-09-24
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class WxDepositServiceImpl implements WxDepositService {
+
+ private final WxPayService payService;
+
+ @Override
+ public WxDepositUnifiedOrderResult unifiedOrder(WxDepositUnifiedOrderRequest request) throws WxPayException {
+ request.checkAndSign(payService.getConfig());
+
+ String url = payService.getPayBaseUrl() + "/pay/depositpay";
+ String responseContent = payService.post(url, request.toXML(), false);
+ WxDepositUnifiedOrderResult result = BaseWxPayResult.fromXML(responseContent, WxDepositUnifiedOrderResult.class);
+ result.checkResult(payService, request.getSignType(), true);
+
+ return result;
+ }
+
+ @Override
+ public WxDepositOrderQueryResult queryOrder(WxDepositOrderQueryRequest request) throws WxPayException {
+ request.checkAndSign(payService.getConfig());
+
+ String url = payService.getPayBaseUrl() + "/pay/depositorderquery";
+ String responseContent = payService.post(url, request.toXML(), false);
+ WxDepositOrderQueryResult result = BaseWxPayResult.fromXML(responseContent, WxDepositOrderQueryResult.class);
+ result.checkResult(payService, request.getSignType(), true);
+
+ return result;
+ }
+
+ @Override
+ public WxDepositConsumeResult consume(WxDepositConsumeRequest request) throws WxPayException {
+ request.checkAndSign(payService.getConfig());
+
+ String url = payService.getPayBaseUrl() + "/pay/depositconsume";
+ String responseContent = payService.post(url, request.toXML(), false);
+ WxDepositConsumeResult result = BaseWxPayResult.fromXML(responseContent, WxDepositConsumeResult.class);
+ result.checkResult(payService, request.getSignType(), true);
+
+ return result;
+ }
+
+ @Override
+ public WxDepositUnfreezeResult unfreeze(WxDepositUnfreezeRequest request) throws WxPayException {
+ request.checkAndSign(payService.getConfig());
+
+ String url = payService.getPayBaseUrl() + "/pay/depositreverse";
+ String responseContent = payService.post(url, request.toXML(), false);
+ WxDepositUnfreezeResult result = BaseWxPayResult.fromXML(responseContent, WxDepositUnfreezeResult.class);
+ result.checkResult(payService, request.getSignType(), true);
+
+ return result;
+ }
+
+ @Override
+ public WxDepositRefundResult refund(WxDepositRefundRequest request) throws WxPayException {
+ request.checkAndSign(payService.getConfig());
+
+ String url = payService.getPayBaseUrl() + "/pay/depositrefund";
+ String responseContent = payService.post(url, request.toXML(), true);
+ WxDepositRefundResult result = BaseWxPayResult.fromXML(responseContent, WxDepositRefundResult.class);
+ result.checkResult(payService, request.getSignType(), true);
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
index 96454e5c08..c68d74a525 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
@@ -420,7 +420,13 @@ private String getWechatPaySerial(WxPayConfig wxPayConfig) {
return wxPayConfig.getPublicKeyId();
}
- return wxPayConfig.getVerifier().getValidCertificate().getSerialNumber().toString(16).toUpperCase();
+ try {
+ return wxPayConfig.getVerifier().getValidCertificate().getSerialNumber().toString(16).toUpperCase();
+ } catch (Exception e) {
+ log.warn("Failed to get certificate serial number: {}", e.getMessage());
+ // 返回空字符串而不是抛出异常,让请求继续进行,由微信服务器判断是否需要Wechatpay-Serial
+ return "";
+ }
}
private void logRequestAndResponse(String url, String requestStr, String responseStr) {
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java
index 5c21a06a8e..9adc673238 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java
@@ -398,7 +398,13 @@ private String getWechatPaySerial(WxPayConfig wxPayConfig) {
return wxPayConfig.getPublicKeyId();
}
- return wxPayConfig.getVerifier().getValidCertificate().getSerialNumber().toString(16).toUpperCase();
+ try {
+ return wxPayConfig.getVerifier().getValidCertificate().getSerialNumber().toString(16).toUpperCase();
+ } catch (Exception e) {
+ log.warn("Failed to get certificate serial number: {}", e.getMessage());
+ // 返回空字符串而不是抛出异常,让请求继续进行,由微信服务器判断是否需要Wechatpay-Serial
+ return "";
+ }
}
private void logRequestAndResponse(String url, String requestStr, String responseStr) {
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceImpl.java
index 8e795966f4..4316bafa40 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceImpl.java
@@ -2,11 +2,11 @@
/**
*
- * 微信支付接口请求实现类,默认使用Apache HttpClient实现
+ * 微信支付接口请求实现类,默认使用Apache HttpClient 5实现
* Created by Binary Wang on 2017-7-8.
*
*
* @author Binary Wang
*/
-public class WxPayServiceImpl extends WxPayServiceApacheHttpImpl {
+public class WxPayServiceImpl extends WxPayServiceHttpComponentsImpl {
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/RequestUtils.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/RequestUtils.java
index b641cbe537..c4ad966415 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/RequestUtils.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/RequestUtils.java
@@ -17,7 +17,7 @@ public class RequestUtils {
/**
* 获取请求头数据,微信V3版本回调使用
*
- * @param request
+ * @param request HTTP请求对象
* @return 字符串
*/
public static String readData(HttpServletRequest request) {
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/ResourcesUtils.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/ResourcesUtils.java
index ac68b00bb4..51dd8fbbb6 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/ResourcesUtils.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/ResourcesUtils.java
@@ -23,6 +23,10 @@ public class ResourcesUtils {
* {@link Class#getClassLoader() ClassLoaderUtil.class.getClassLoader()}
* if callingClass is provided: {@link Class#getClassLoader() callingClass.getClassLoader()}
*
+ *
+ * @param resourceName 资源名称
+ * @param classLoader 类加载器
+ * @return 资源URL
*/
public static URL getResourceUrl(String resourceName, final ClassLoader classLoader) {
@@ -64,6 +68,9 @@ public static URL getResourceUrl(String resourceName, final ClassLoader classLoa
/**
* Opens a resource of the specified name for reading.
*
+ * @param resourceName 资源名称
+ * @return 输入流
+ * @throws IOException IO异常
* @see #getResourceAsStream(String, ClassLoader)
*/
public static InputStream getResourceAsStream(final String resourceName) throws IOException {
@@ -73,6 +80,10 @@ public static InputStream getResourceAsStream(final String resourceName) throws
/**
* Opens a resource of the specified name for reading.
*
+ * @param resourceName 资源名称
+ * @param callingClass 类加载器
+ * @return 输入流
+ * @throws IOException IO异常
* @see #getResourceUrl(String, ClassLoader)
*/
public static InputStream getResourceAsStream(final String resourceName, final ClassLoader callingClass) throws IOException {
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/SignUtils.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/SignUtils.java
index 6c0009fd18..9d4a9b0237 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/SignUtils.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/SignUtils.java
@@ -112,7 +112,16 @@ public static String createSign(Map params, String signType, Str
/**
* 企业微信签名
*
- * @param signType md5 目前接口要求使用的加密类型
+ * @param actName 活动名称
+ * @param mchBillNo 商户订单号
+ * @param mchId 商户号
+ * @param nonceStr 随机字符串
+ * @param reOpenid 用户openid
+ * @param totalAmount 金额
+ * @param wxAppId 微信appid
+ * @param signKey 签名密钥
+ * @param signType md5 目前接口要求使用的加密类型
+ * @return 签名字符串
*/
public static String createEntSign(String actName, String mchBillNo, String mchId, String nonceStr,
String reOpenid, Integer totalAmount, String wxAppId, String signKey,
@@ -131,7 +140,18 @@ public static String createEntSign(String actName, String mchBillNo, String mchI
/**
* 企业微信签名
- * @param signType md5 目前接口要求使用的加密类型
+ *
+ * @param totalAmount 金额
+ * @param appId 应用ID
+ * @param description 描述
+ * @param mchId 商户号
+ * @param nonceStr 随机字符串
+ * @param openid 用户openid
+ * @param partnerTradeNo 商户订单号
+ * @param wwMsgType 消息类型
+ * @param signKey 签名密钥
+ * @param signType md5 目前接口要求使用的加密类型
+ * @return 签名字符串
*/
public static String createEntSign(Integer totalAmount, String appId, String description, String mchId,
String nonceStr, String openid, String partnerTradeNo, String wwMsgType,
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java
index 24d6f26eb5..24c51028df 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java
@@ -15,16 +15,27 @@
import org.apache.http.util.EntityUtils;
import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
public class SignatureExec implements ClientExecChain {
final ClientExecChain mainExec;
final Credentials credentials;
final Validator validator;
+ /**
+ * 额外受信任的主机列表,这些主机(如反向代理)也需要携带微信支付 Authorization 头
+ */
+ final Set trustedHosts;
SignatureExec(Credentials credentials, Validator validator, ClientExecChain mainExec) {
+ this(credentials, validator, mainExec, Collections.emptySet());
+ }
+
+ SignatureExec(Credentials credentials, Validator validator, ClientExecChain mainExec, Set trustedHosts) {
this.credentials = credentials;
this.validator = validator;
this.mainExec = mainExec;
+ this.trustedHosts = trustedHosts != null ? trustedHosts : Collections.emptySet();
}
protected HttpEntity newRepeatableEntity(HttpEntity entity) throws IOException {
@@ -56,7 +67,8 @@ protected void convertToRepeatableRequestEntity(HttpRequestWrapper request) thro
public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request,
HttpClientContext context, HttpExecutionAware execAware)
throws IOException, HttpException {
- if (request.getURI().getHost() != null && request.getURI().getHost().endsWith(".mch.weixin.qq.com")) {
+ String host = request.getURI().getHost();
+ if (host != null && (host.endsWith(".mch.weixin.qq.com") || trustedHosts.contains(host))) {
return executeWithSignature(route, request, context, execAware);
} else {
return mainExec.execute(route, request, context, execAware);
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java
index 5f5e52d2ff..3387f37e3d 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java
@@ -35,7 +35,7 @@ public Builder(URI uri) {
this.uri = uri;
}
- public Builder withImage(String fileName, String fileSha256, InputStream inputStream) {
+ private Builder withMedia(String fileName, String fileSha256, InputStream inputStream) {
this.fileName = fileName;
this.fileSha256 = fileSha256;
this.fileInputStream = inputStream;
@@ -50,13 +50,21 @@ public Builder withImage(String fileName, String fileSha256, InputStream inputSt
return this;
}
+ public Builder withImage(String fileName, String fileSha256, InputStream inputStream) {
+ return withMedia(fileName, fileSha256, inputStream);
+ }
+
+ public Builder withVideo(String fileName, String fileSha256, InputStream inputStream) {
+ return withMedia(fileName, fileSha256, inputStream);
+ }
+
public WechatPayUploadHttpPost build() {
if (fileName == null || fileSha256 == null || fileInputStream == null) {
- throw new IllegalArgumentException("缺少待上传图片文件信息");
+ throw new IllegalArgumentException("缺少待上传文件信息");
}
if (uri == null) {
- throw new IllegalArgumentException("缺少上传图片接口URL");
+ throw new IllegalArgumentException("缺少上传文件接口URL");
}
String meta = String.format("{\"filename\":\"%s\",\"sha256\":\"%s\"}", fileName, fileSha256);
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WxPayV3HttpClientBuilder.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WxPayV3HttpClientBuilder.java
index c88c884f57..63a92b25ce 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WxPayV3HttpClientBuilder.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WxPayV3HttpClientBuilder.java
@@ -2,6 +2,9 @@
import java.security.PrivateKey;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
import com.github.binarywang.wxpay.v3.auth.PrivateKeySigner;
import com.github.binarywang.wxpay.v3.auth.WxPayCredentials;
@@ -12,6 +15,14 @@
public class WxPayV3HttpClientBuilder extends HttpClientBuilder {
private Credentials credentials;
private Validator validator;
+ /**
+ * 签名前从请求 URI Path 中移除的前缀(用于带路径前缀的代理场景)
+ */
+ private String signUriStripPrefix;
+ /**
+ * 额外受信任的主机列表,用于代理转发场景:对这些主机的请求也会携带微信支付 Authorization 头
+ */
+ private final Set trustedHosts = new HashSet<>();
static final String OS = System.getProperty("os.name") + "/" + System.getProperty("os.version");
static final String VERSION = System.getProperty("java.version");
@@ -33,12 +44,30 @@ public static WxPayV3HttpClientBuilder create() {
public WxPayV3HttpClientBuilder withMerchant(String merchantId, String serialNo, PrivateKey privateKey) {
this.credentials =
- new WxPayCredentials(merchantId, new PrivateKeySigner(serialNo, privateKey));
+ new WxPayCredentials(merchantId, new PrivateKeySigner(serialNo, privateKey), this.signUriStripPrefix);
return this;
}
public WxPayV3HttpClientBuilder withCredentials(Credentials credentials) {
this.credentials = credentials;
+ if (this.credentials instanceof WxPayCredentials) {
+ ((WxPayCredentials) this.credentials).setSignUriStripPrefix(this.signUriStripPrefix);
+ }
+ return this;
+ }
+
+ /**
+ * 配置签名前需要移除的 URI Path 前缀.
+ * 例如设置为 "/api-weixin" 时,签名串中的 Path 会从 "/api-weixin/v3/..." 调整为 "/v3/..."。
+ *
+ * @param signUriStripPrefix 需要移除的前缀
+ * @return 当前 Builder 实例
+ */
+ public WxPayV3HttpClientBuilder withSignUriStripPrefix(String signUriStripPrefix) {
+ this.signUriStripPrefix = signUriStripPrefix;
+ if (this.credentials instanceof WxPayCredentials) {
+ ((WxPayCredentials) this.credentials).setSignUriStripPrefix(signUriStripPrefix);
+ }
return this;
}
@@ -47,6 +76,39 @@ public WxPayV3HttpClientBuilder withValidator(Validator validator) {
return this;
}
+ /**
+ * 添加受信任的主机,对该主机的请求也会携带微信支付 Authorization 头.
+ * 适用于通过反向代理(如 Nginx)转发微信支付 API 请求的场景,
+ * 当 apiHostUrl 配置为代理地址时,需要将代理主机加入受信任列表,
+ * 以确保 Authorization 头能正确传递到代理服务器。
+ * 若传入值包含端口(如 "proxy.company.com:8080"),会自动提取主机名部分。
+ *
+ * @param host 受信任的主机(可含端口),例如 "proxy.company.com" 或 "proxy.company.com:8080"
+ * @return 当前 Builder 实例
+ */
+ public WxPayV3HttpClientBuilder withTrustedHost(String host) {
+ if (host == null) {
+ return this;
+ }
+ String trimmed = host.trim();
+ if (trimmed.isEmpty()) {
+ return this;
+ }
+ // 若包含端口号(如 "host:8080"),只取主机名部分
+ int colonIdx = trimmed.lastIndexOf(':');
+ if (colonIdx > 0) {
+ String portPart = trimmed.substring(colonIdx + 1);
+ boolean isPort = !portPart.isEmpty() && portPart.chars().allMatch(Character::isDigit);
+ if (isPort) {
+ trimmed = trimmed.substring(0, colonIdx);
+ }
+ }
+ if (!trimmed.isEmpty()) {
+ this.trustedHosts.add(trimmed);
+ }
+ return this;
+ }
+
@Override
public CloseableHttpClient build() {
if (credentials == null) {
@@ -61,6 +123,7 @@ public CloseableHttpClient build() {
@Override
protected ClientExecChain decorateProtocolExec(final ClientExecChain requestExecutor) {
- return new SignatureExec(this.credentials, this.validator, requestExecutor);
+ return new SignatureExec(this.credentials, this.validator, requestExecutor,
+ Collections.unmodifiableSet(new HashSet<>(this.trustedHosts)));
}
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java
index 21624d455f..0757f58dcc 100755
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java
@@ -22,6 +22,8 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateExpiredException;
@@ -109,18 +111,24 @@ public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key,
this.minutesInterval = minutesInterval;
this.payBaseUrl = payBaseUrl;
this.wxPayHttpProxy = wxPayHttpProxy;
- //构造时更新证书
+ //构造时尝试更新证书,但失败时不抛出异常,避免影响公钥模式的使用
try {
autoUpdateCert();
instant = Instant.now();
} catch (IOException | GeneralSecurityException e) {
- throw new WxRuntimeException(e);
+ log.warn("Auto update cert failed during initialization, will retry later, exception = {}", e.getMessage());
+ // 设置 instant 为 null,后续每次使用时都会尝试下载证书直到成功
+ instant = null;
}
}
@Override
public boolean verify(String serialNumber, byte[] message, String signature) {
checkAndAutoUpdateCert();
+ if (verifier == null) {
+ log.warn("No valid certificate available for verification");
+ return false;
+ }
return verifier.verify(serialNumber, message, signature);
}
@@ -148,8 +156,21 @@ private void autoUpdateCert() throws IOException, GeneralSecurityException {
.withCredentials(credentials)
.withValidator(verifier == null ? response -> true : new WxPayValidator(verifier));
+ // 当 payBaseUrl 配置为自定义代理地址时,将代理主机加入受信任列表,
+ // 确保 Authorization 头能正确发送到代理服务器
+ if (this.payBaseUrl != null && !this.payBaseUrl.isEmpty()) {
+ try {
+ String host = new URI(this.payBaseUrl).getHost();
+ if (host != null && !host.endsWith(".mch.weixin.qq.com")) {
+ wxPayV3HttpClientBuilder.withTrustedHost(host);
+ }
+ } catch (URISyntaxException e) {
+ log.warn("解析 payBaseUrl [{}] 中的主机名失败: {}", this.payBaseUrl, e.getMessage());
+ }
+ }
+
//调用自定义扩展设置设置HTTP PROXY对象
- HttpProxyUtils.initHttpProxy(wxPayV3HttpClientBuilder,this.wxPayHttpProxy);
+ HttpProxyUtils.initHttpProxy(wxPayV3HttpClientBuilder, this.wxPayHttpProxy);
//增加自定义扩展点,子类可以设置其他构造参数
this.customHttpClientBuilder(wxPayV3HttpClientBuilder);
@@ -220,6 +241,9 @@ private List deserializeToCerts(byte[] apiV3Key, String body) t
@Override
public X509Certificate getValidCertificate() {
checkAndAutoUpdateCert();
+ if (verifier == null) {
+ throw new WxRuntimeException("No valid certificate available, please check your configuration or use fullPublicKeyModel mode");
+ }
return verifier.getValidCertificate();
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PublicCertificateVerifier.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PublicCertificateVerifier.java
index 8c9c4f3569..62ad61ce19 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PublicCertificateVerifier.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PublicCertificateVerifier.java
@@ -24,9 +24,17 @@ public void setOtherVerifier(Verifier verifier) {
@Override
public boolean verify(String serialNumber, byte[] message, String signature) {
- if (!serialNumber.contains("PUB_KEY_ID") && this.certificateVerifier != null) {
- return this.certificateVerifier.verify(serialNumber, message, signature);
+ // 如果序列号不为空且不包含"PUB_KEY_ID"且有证书验证器,先尝试证书验证
+ if (serialNumber != null && !serialNumber.contains("PUB_KEY_ID") && this.certificateVerifier != null) {
+ try {
+ if (this.certificateVerifier.verify(serialNumber, message, signature)) {
+ return true;
+ }
+ } catch (Exception e) {
+ // 证书验证失败,继续尝试公钥验证
+ }
}
+ // 使用公钥验证(兜底方案,适用于公钥转账等场景)
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initVerify(publicKey);
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayCredentials.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayCredentials.java
index 80eea8f686..4b78a26f73 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayCredentials.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayCredentials.java
@@ -20,16 +20,42 @@ public class WxPayCredentials implements Credentials {
private static final SecureRandom RANDOM = new SecureRandom();
protected String merchantId;
protected Signer signer;
+ /**
+ * 签名前从 URI Path 中移除的前缀(用于带路径前缀的反向代理场景)
+ * 例如配置为 "/api-weixin" 时,"/api-weixin/v3/pay/..." 将参与签名为 "/v3/pay/..."
+ */
+ protected String signUriStripPrefix;
public WxPayCredentials(String merchantId, Signer signer) {
this.merchantId = merchantId;
this.signer = signer;
}
+ public WxPayCredentials(String merchantId, Signer signer, String signUriStripPrefix) {
+ this.merchantId = merchantId;
+ this.signer = signer;
+ this.setSignUriStripPrefix(signUriStripPrefix);
+ }
+
public String getMerchantId() {
return merchantId;
}
+ public void setSignUriStripPrefix(String signUriStripPrefix) {
+ if (signUriStripPrefix == null || signUriStripPrefix.trim().isEmpty()) {
+ this.signUriStripPrefix = null;
+ return;
+ }
+ String normalized = signUriStripPrefix.trim();
+ if (!normalized.startsWith("/")) {
+ normalized = "/" + normalized;
+ }
+ if (normalized.length() > 1 && normalized.endsWith("/")) {
+ normalized = normalized.substring(0, normalized.length() - 1);
+ }
+ this.signUriStripPrefix = normalized;
+ }
+
protected long generateTimestamp() {
return System.currentTimeMillis() / 1000;
}
@@ -70,7 +96,7 @@ public final String getToken(HttpRequestWrapper request) throws IOException {
protected final String buildMessage(String nonce, long timestamp, HttpRequestWrapper request)
throws IOException {
URI uri = request.getURI();
- String canonicalUrl = uri.getRawPath();
+ String canonicalUrl = stripPathPrefix(uri.getRawPath());
if (uri.getQuery() != null) {
canonicalUrl += "?" + uri.getRawQuery();
}
@@ -90,4 +116,18 @@ protected final String buildMessage(String nonce, long timestamp, HttpRequestWra
+ body + "\n";
}
+ private String stripPathPrefix(String rawPath) {
+ if (rawPath == null || rawPath.isEmpty() || signUriStripPrefix == null) {
+ return rawPath;
+ }
+ if (!rawPath.startsWith(signUriStripPrefix)) {
+ return rawPath;
+ }
+ String stripped = rawPath.substring(signUriStripPrefix.length());
+ if (stripped.isEmpty()) {
+ return "/";
+ }
+ return stripped.startsWith("/") ? stripped : "/" + stripped;
+ }
+
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtil.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtil.java
index 8c3e2ace53..0143022ece 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtil.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtil.java
@@ -14,8 +14,11 @@
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
+import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
/**
* 微信支付敏感信息加密
@@ -36,10 +39,26 @@ public static void encryptFields(Object encryptObject, X509Certificate certifica
}
}
+ /**
+ * 递归获取类的所有字段,包括父类中的字段
+ *
+ * @param clazz 要获取字段的类
+ * @return 所有字段的列表
+ */
+ private static List getAllFields(Class> clazz) {
+ List fields = new ArrayList<>();
+ while (clazz != null && clazz != Object.class) {
+ Field[] declaredFields = clazz.getDeclaredFields();
+ Collections.addAll(fields, declaredFields);
+ clazz = clazz.getSuperclass();
+ }
+ return fields;
+ }
+
private static void encryptField(Object encryptObject, X509Certificate certificate) throws IllegalAccessException, IllegalBlockSizeException {
Class> infoClass = encryptObject.getClass();
- Field[] infoFieldArray = infoClass.getDeclaredFields();
- for (Field field : infoFieldArray) {
+ List infoFieldList = getAllFields(infoClass);
+ for (Field field : infoFieldList) {
if (field.isAnnotationPresent(SpecEncrypt.class)) {
//字段使用了@SpecEncrypt进行标识
if (field.getType().getTypeName().equals(JAVA_LANG_STRING)) {
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/applyment/WxPayApplyment4SubCreateRequestTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/applyment/WxPayApplyment4SubCreateRequestTest.java
new file mode 100644
index 0000000000..6dad6d2a80
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/applyment/WxPayApplyment4SubCreateRequestTest.java
@@ -0,0 +1,104 @@
+package com.github.binarywang.wxpay.bean.applyment;
+
+import java.util.Arrays;
+
+import org.testng.annotations.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public class WxPayApplyment4SubCreateRequestTest {
+
+ @Test
+ public void testMicroBizInfoSerialization() {
+ // 1. Test MicroStoreInfo
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.MicroStoreInfo storeInfo =
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.MicroStoreInfo.builder()
+ .microName("门店名称")
+ .microAddressCode("440305")
+ .microAddress("详细地址")
+ .storeEntrancePic("media_id_1")
+ .microIndoorCopy("media_id_2")
+ .storeLongitude("113.941046")
+ .storeLatitude("22.546154")
+ .build();
+
+ // 2. Test MicroMobileInfo
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.MicroMobileInfo mobileInfo =
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.MicroMobileInfo.builder()
+ .microMobileName("流动经营名称")
+ .microMobileCity("440305")
+ .microMobileAddress("无")
+ .microMobilePics(Arrays.asList("media_id_3", "media_id_4"))
+ .build();
+
+ // 3. Test MicroOnlineInfo
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.MicroOnlineInfo onlineInfo =
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.MicroOnlineInfo.builder()
+ .microOnlineStore("线上店铺名称")
+ .microEcName("电商平台名称")
+ .microQrcode("media_id_5")
+ .microLink("https://www.example.com")
+ .build();
+
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo microBizInfo =
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.builder()
+ .microStoreInfo(storeInfo)
+ .microMobileInfo(mobileInfo)
+ .microOnlineInfo(onlineInfo)
+ .build();
+
+ Gson gson = new GsonBuilder().create();
+ String json = gson.toJson(microBizInfo);
+
+ // Verify MicroStoreInfo serialization
+ assertTrue(json.contains("\"micro_name\":\"门店名称\""));
+ assertTrue(json.contains("\"micro_address_code\":\"440305\""));
+ assertTrue(json.contains("\"micro_address\":\"详细地址\""));
+ assertTrue(json.contains("\"store_entrance_pic\":\"media_id_1\""));
+ assertTrue(json.contains("\"micro_indoor_copy\":\"media_id_2\""));
+ assertTrue(json.contains("\"store_longitude\":\"113.941046\""));
+ assertTrue(json.contains("\"store_latitude\":\"22.546154\""));
+
+ // Verify MicroMobileInfo serialization
+ assertTrue(json.contains("\"micro_mobile_name\":\"流动经营名称\""));
+ assertTrue(json.contains("\"micro_mobile_city\":\"440305\""));
+ assertTrue(json.contains("\"micro_mobile_address\":\"无\""));
+ assertTrue(json.contains("\"micro_mobile_pics\":[\"media_id_3\",\"media_id_4\"]"));
+
+ // Verify MicroOnlineInfo serialization
+ assertTrue(json.contains("\"micro_online_store\":\"线上店铺名称\""));
+ assertTrue(json.contains("\"micro_ec_name\":\"电商平台名称\""));
+ assertTrue(json.contains("\"micro_qrcode\":\"media_id_5\""));
+ assertTrue(json.contains("\"micro_link\":\"https://www.example.com\""));
+
+ // Verify deserialization
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo deserialized =
+ gson.fromJson(json, WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.class);
+
+ // Verify MicroStoreInfo deserialization
+ assertEquals(deserialized.getMicroStoreInfo().getMicroName(), "门店名称");
+ assertEquals(deserialized.getMicroStoreInfo().getMicroAddressCode(), "440305");
+ assertEquals(deserialized.getMicroStoreInfo().getMicroAddress(), "详细地址");
+ assertEquals(deserialized.getMicroStoreInfo().getStoreEntrancePic(), "media_id_1");
+ assertEquals(deserialized.getMicroStoreInfo().getMicroIndoorCopy(), "media_id_2");
+ assertEquals(deserialized.getMicroStoreInfo().getStoreLongitude(), "113.941046");
+ assertEquals(deserialized.getMicroStoreInfo().getStoreLatitude(), "22.546154");
+
+ // Verify MicroMobileInfo deserialization
+ assertEquals(deserialized.getMicroMobileInfo().getMicroMobileName(), "流动经营名称");
+ assertEquals(deserialized.getMicroMobileInfo().getMicroMobileCity(), "440305");
+ assertEquals(deserialized.getMicroMobileInfo().getMicroMobileAddress(), "无");
+ assertEquals(deserialized.getMicroMobileInfo().getMicroMobilePics().size(), 2);
+ assertEquals(deserialized.getMicroMobileInfo().getMicroMobilePics(), Arrays.asList("media_id_3", "media_id_4"));
+
+ // Verify MicroOnlineInfo deserialization
+ assertEquals(deserialized.getMicroOnlineInfo().getMicroOnlineStore(), "线上店铺名称");
+ assertEquals(deserialized.getMicroOnlineInfo().getMicroEcName(), "电商平台名称");
+ assertEquals(deserialized.getMicroOnlineInfo().getMicroQrcode(), "media_id_5");
+ assertEquals(deserialized.getMicroOnlineInfo().getMicroLink(), "https://www.example.com");
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/entpay/EntPayRequestTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/entpay/EntPayRequestTest.java
index b6f68b81c1..402c74d38f 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/entpay/EntPayRequestTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/entpay/EntPayRequestTest.java
@@ -2,6 +2,10 @@
import org.testng.annotations.Test;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
/**
* .
*
@@ -14,4 +18,32 @@ public class EntPayRequestTest {
public void testToString() {
System.out.println(EntPayRequest.newBuilder().mchId("123").build().toString());
}
+
+ /**
+ * 测试 brandId 为 null 时,getSignParams() 不抛出 NullPointerException.
+ */
+ @Test
+ public void testGetSignParamsWithNullBrandId() {
+ EntPayRequest request = EntPayRequest.newBuilder()
+ .mchId("123")
+ .amount(100)
+ .brandId(null)
+ .build();
+ Map params = request.getSignParams();
+ assertThat(params).doesNotContainKey("brand_id");
+ }
+
+ /**
+ * 测试 brandId 不为 null 时,getSignParams() 正确包含 brand_id.
+ */
+ @Test
+ public void testGetSignParamsWithNonNullBrandId() {
+ EntPayRequest request = EntPayRequest.newBuilder()
+ .mchId("123")
+ .amount(100)
+ .brandId(1234)
+ .build();
+ Map params = request.getSignParams();
+ assertThat(params).containsEntry("brand_id", "1234");
+ }
}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/marketing/transfer/BatchDetailsResultTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/marketing/transfer/BatchDetailsResultTest.java
new file mode 100644
index 0000000000..c2347300a6
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/marketing/transfer/BatchDetailsResultTest.java
@@ -0,0 +1,182 @@
+package com.github.binarywang.wxpay.bean.marketing.transfer;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+/**
+ * 测试 BatchDetailsResult 的字段序列化和反序列化功能
+ *
+ * @author Binary Wang
+ */
+public class BatchDetailsResultTest {
+
+ private static final Gson GSON = new GsonBuilder().create();
+
+ @Test
+ public void testBankFieldsDeserialization() {
+ // 模拟微信API返回的JSON(包含银行相关字段)
+ String mockJson = "{\n" +
+ " \"sp_mchid\": \"1900001109\",\n" +
+ " \"out_batch_no\": \"plfk2020042013\",\n" +
+ " \"batch_id\": \"1030000071100999991182020050700019480001\",\n" +
+ " \"appid\": \"wxf636efh567hg4356\",\n" +
+ " \"out_detail_no\": \"x23zy545Bd5436\",\n" +
+ " \"detail_id\": \"1040000071100999991182020050700019500100\",\n" +
+ " \"detail_status\": \"SUCCESS\",\n" +
+ " \"transfer_amount\": 200000,\n" +
+ " \"transfer_remark\": \"2020年4月报销\",\n" +
+ " \"openid\": \"o-MYE42l80oelYMDE34nYD456Xoy\",\n" +
+ " \"username\": \"757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45\",\n" +
+ " \"initiate_time\": \"2015-05-20T13:29:35.120+08:00\",\n" +
+ " \"update_time\": \"2015-05-20T13:29:35.120+08:00\",\n" +
+ " \"bank_name\": \"中国农业银行股份有限公司深圳分行\",\n" +
+ " \"bank_card_number_tail\": \"1234\"\n" +
+ "}";
+
+ // 反序列化JSON
+ BatchDetailsResult result = GSON.fromJson(mockJson, BatchDetailsResult.class);
+
+ // 验证基本字段正常解析
+ assertEquals(result.getSpMchid(), "1900001109");
+ assertEquals(result.getOutBatchNo(), "plfk2020042013");
+ assertEquals(result.getBatchId(), "1030000071100999991182020050700019480001");
+ assertEquals(result.getAppId(), "wxf636efh567hg4356");
+ assertEquals(result.getOutDetailNo(), "x23zy545Bd5436");
+ assertEquals(result.getDetailId(), "1040000071100999991182020050700019500100");
+ assertEquals(result.getDetailStatus(), "SUCCESS");
+ assertEquals(result.getTransferAmount(), Integer.valueOf(200000));
+ assertEquals(result.getTransferRemark(), "2020年4月报销");
+ assertEquals(result.getOpenid(), "o-MYE42l80oelYMDE34nYD456Xoy");
+ assertEquals(result.getUserName(), "757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45");
+ assertEquals(result.getInitiateTime(), "2015-05-20T13:29:35.120+08:00");
+ assertEquals(result.getUpdateTime(), "2015-05-20T13:29:35.120+08:00");
+
+ // 验证新增的银行相关字段
+ assertEquals(result.getBankName(), "中国农业银行股份有限公司深圳分行");
+ assertEquals(result.getBankCardNumberTail(), "1234");
+ }
+
+ @Test
+ public void testBankFieldsWithNull() {
+ // 测试不包含银行字段的情况(转账到零钱)
+ String mockJsonWithoutBank = "{\n" +
+ " \"sp_mchid\": \"1900001109\",\n" +
+ " \"out_batch_no\": \"plfk2020042013\",\n" +
+ " \"batch_id\": \"1030000071100999991182020050700019480001\",\n" +
+ " \"out_detail_no\": \"x23zy545Bd5436\",\n" +
+ " \"detail_id\": \"1040000071100999991182020050700019500100\",\n" +
+ " \"detail_status\": \"SUCCESS\",\n" +
+ " \"transfer_amount\": 200000,\n" +
+ " \"transfer_remark\": \"2020年4月报销\",\n" +
+ " \"openid\": \"o-MYE42l80oelYMDE34nYD456Xoy\",\n" +
+ " \"username\": \"757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45\",\n" +
+ " \"initiate_time\": \"2015-05-20T13:29:35.120+08:00\",\n" +
+ " \"update_time\": \"2015-05-20T13:29:35.120+08:00\"\n" +
+ "}";
+
+ BatchDetailsResult result = GSON.fromJson(mockJsonWithoutBank, BatchDetailsResult.class);
+
+ // 验证其他字段正常
+ assertEquals(result.getSpMchid(), "1900001109");
+ assertEquals(result.getDetailStatus(), "SUCCESS");
+
+ // 验证银行字段为null(转账到零钱场景下不返回这些字段)
+ assertNull(result.getBankName());
+ assertNull(result.getBankCardNumberTail());
+ }
+
+ @Test
+ public void testBankFieldsSerialization() {
+ // 测试序列化
+ BatchDetailsResult result = new BatchDetailsResult();
+ result.setSpMchid("1900001109");
+ result.setOutBatchNo("plfk2020042013");
+ result.setBatchId("1030000071100999991182020050700019480001");
+ result.setDetailStatus("SUCCESS");
+ result.setBankName("中国工商银行股份有限公司北京分行");
+ result.setBankCardNumberTail("5678");
+
+ String json = GSON.toJson(result);
+
+ // 验证JSON包含银行字段
+ assertTrue(json.contains("\"bank_name\":\"中国工商银行股份有限公司北京分行\""));
+ assertTrue(json.contains("\"bank_card_number_tail\":\"5678\""));
+ }
+
+ @Test
+ public void testToString() {
+ // 测试toString方法
+ BatchDetailsResult result = new BatchDetailsResult();
+ result.setSpMchid("1900001109");
+ result.setBankName("中国建设银行股份有限公司上海分行");
+ result.setBankCardNumberTail("9012");
+
+ String resultString = result.toString();
+
+ // 验证toString包含所有字段
+ assertNotNull(resultString);
+ assertTrue(resultString.contains("1900001109"));
+ assertTrue(resultString.contains("中国建设银行股份有限公司上海分行"));
+ assertTrue(resultString.contains("9012"));
+ }
+
+ @Test
+ public void testBankNameWithSpecialCharacters() {
+ // 测试银行名称包含特殊字符的情况
+ String mockJson = "{\n" +
+ " \"sp_mchid\": \"1900001109\",\n" +
+ " \"out_batch_no\": \"plfk2020042013\",\n" +
+ " \"batch_id\": \"1030000071100999991182020050700019480001\",\n" +
+ " \"out_detail_no\": \"x23zy545Bd5436\",\n" +
+ " \"detail_id\": \"1040000071100999991182020050700019500100\",\n" +
+ " \"detail_status\": \"SUCCESS\",\n" +
+ " \"transfer_amount\": 200000,\n" +
+ " \"transfer_remark\": \"2020年4月报销\",\n" +
+ " \"openid\": \"o-MYE42l80oelYMDE34nYD456Xoy\",\n" +
+ " \"username\": \"757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45\",\n" +
+ " \"initiate_time\": \"2015-05-20T13:29:35.120+08:00\",\n" +
+ " \"update_time\": \"2015-05-20T13:29:35.120+08:00\",\n" +
+ " \"bank_name\": \"中国农业银行股份有限公司北京市朝阳区(支行)\",\n" +
+ " \"bank_card_number_tail\": \"0000\"\n" +
+ "}";
+
+ BatchDetailsResult result = GSON.fromJson(mockJson, BatchDetailsResult.class);
+
+ // 验证特殊字符正确解析
+ assertEquals(result.getBankName(), "中国农业银行股份有限公司北京市朝阳区(支行)");
+ assertEquals(result.getBankCardNumberTail(), "0000");
+ }
+
+ @Test
+ public void testFailedTransferWithoutBankFields() {
+ // 测试转账失败的情况
+ String mockJson = "{\n" +
+ " \"sp_mchid\": \"1900001109\",\n" +
+ " \"out_batch_no\": \"plfk2020042013\",\n" +
+ " \"batch_id\": \"1030000071100999991182020050700019480001\",\n" +
+ " \"out_detail_no\": \"x23zy545Bd5436\",\n" +
+ " \"detail_id\": \"1040000071100999991182020050700019500100\",\n" +
+ " \"detail_status\": \"FAIL\",\n" +
+ " \"transfer_amount\": 200000,\n" +
+ " \"transfer_remark\": \"2020年4月报销\",\n" +
+ " \"fail_reason\": \"ACCOUNT_FROZEN\",\n" +
+ " \"openid\": \"o-MYE42l80oelYMDE34nYD456Xoy\",\n" +
+ " \"username\": \"757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45\",\n" +
+ " \"initiate_time\": \"2015-05-20T13:29:35.120+08:00\",\n" +
+ " \"update_time\": \"2015-05-20T13:29:35.120+08:00\"\n" +
+ "}";
+
+ BatchDetailsResult result = GSON.fromJson(mockJson, BatchDetailsResult.class);
+
+ // 验证失败状态
+ assertEquals(result.getDetailStatus(), "FAIL");
+ assertEquals(result.getFailReason(), "ACCOUNT_FROZEN");
+
+ // 失败的情况下银行字段应为null
+ assertNull(result.getBankName());
+ assertNull(result.getBankCardNumberTail());
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java
new file mode 100644
index 0000000000..be35523ec4
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayOrderNotifyUnknownFieldTest.java
@@ -0,0 +1,104 @@
+package com.github.binarywang.wxpay.bean.notify;
+
+import com.github.binarywang.wxpay.constant.WxPayConstants;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.util.*;
+
+/**
+ * 测试当微信支付回调 XML 包含未在 Java Bean 中定义的字段时,签名验证是否正常。
+ *
+ * 问题背景:当微信返回的 XML 包含某些未在 WxPayOrderNotifyResult 中定义的字段时,
+ * 这些字段会被微信服务器用于签名计算。如果 toMap() 方法丢失了这些字段,
+ * 则签名验证会失败,抛出 "参数格式校验错误!" 异常。
+ *
+ *
+ * 解决方案:修改 WxPayOrderNotifyResult.toMap() 方法,使用父类的 toMap() 方法
+ * 直接从原始 XML 解析所有字段,而不是使用 SignUtils.xmlBean2Map(this)。
+ *
+ *
+ * @see Issue #3750
+ */
+public class WxPayOrderNotifyUnknownFieldTest {
+
+ private static final String MCH_KEY = "testmchkey1234567890123456789012";
+ private static final List NO_SIGN_PARAMS = Arrays.asList("sign", "key", "xmlString", "xmlDoc", "couponList");
+
+ @Test
+ public void testSignatureWithUnknownField() throws Exception {
+ // 创建一个测试用的 XML,包含一个未知字段 (未在 WxPayOrderNotifyResult 中定义)
+ Map params = new LinkedHashMap<>();
+ params.put("appid", "wx58ff40508696691f");
+ params.put("bank_type", "ICBC_DEBIT");
+ params.put("cash_fee", "1");
+ params.put("fee_type", "CNY");
+ params.put("is_subscribe", "N");
+ params.put("mch_id", "1545462911");
+ params.put("nonce_str", "1761723102373");
+ params.put("openid", "o1gdd16CZCi6yYvkn6j9EB_1TObM");
+ params.put("out_trade_no", "20251029153140");
+ params.put("result_code", "SUCCESS");
+ params.put("return_code", "SUCCESS");
+ params.put("time_end", "20251029153852");
+ params.put("total_fee", "1");
+ params.put("trade_type", "JSAPI");
+ params.put("transaction_id", "4200002882220251029816273963B");
+ // 添加一个未知字段
+ params.put("unknown_field", "unknown_value");
+
+ // 计算正确的签名 (包含未知字段)
+ String correctSign = createSign(params, WxPayConstants.SignType.MD5, MCH_KEY);
+ params.put("sign", correctSign);
+
+ // 创建 XML
+ StringBuilder xmlBuilder = new StringBuilder("");
+ for (Map.Entry entry : params.entrySet()) {
+ xmlBuilder.append("<").append(entry.getKey()).append(">")
+ .append(entry.getValue())
+ .append("").append(entry.getKey()).append(">");
+ }
+ xmlBuilder.append("");
+ String xml = xmlBuilder.toString();
+
+ System.out.println("测试 XML (包含未知字段 unknown_field):");
+ System.out.println(xml);
+ System.out.println("正确的签名 (包含未知字段计算): " + correctSign);
+
+ // 解析 XML
+ WxPayOrderNotifyResult result = WxPayOrderNotifyResult.fromXML(xml);
+ Map beanMap = result.toMap();
+
+ System.out.println("\ntoMap() 结果:");
+ TreeMap sortedMap = new TreeMap<>(beanMap);
+ for (Map.Entry entry : sortedMap.entrySet()) {
+ System.out.println(" " + entry.getKey() + " = " + entry.getValue());
+ }
+
+ // 检查 unknown_field 是否存在
+ boolean hasUnknownField = beanMap.containsKey("unknown_field");
+ System.out.println("\ntoMap() 是否包含 unknown_field: " + hasUnknownField);
+
+ // 验证签名
+ String verifySign = createSign(beanMap, WxPayConstants.SignType.MD5, MCH_KEY);
+ System.out.println("原始签名: " + result.getSign());
+ System.out.println("计算签名: " + verifySign);
+
+ // 这个测试验证修复后 toMap() 能正确包含所有字段
+ Assert.assertTrue(hasUnknownField, "toMap() 应该包含 unknown_field");
+ Assert.assertEquals(verifySign, result.getSign(), "签名应该匹配");
+ }
+
+ private static String createSign(Map params, String signType, String signKey) {
+ StringBuilder toSign = new StringBuilder();
+ for (String key : new TreeMap<>(params).keySet()) {
+ String value = params.get(key);
+ if (value != null && !value.isEmpty() && !NO_SIGN_PARAMS.contains(key)) {
+ toSign.append(key).append("=").append(value).append("&");
+ }
+ }
+ toSign.append("key=").append(signKey);
+ return DigestUtils.md5Hex(toSign.toString()).toUpperCase();
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyV3ResultTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyV3ResultTest.java
new file mode 100644
index 0000000000..8b3487c51c
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyV3ResultTest.java
@@ -0,0 +1,61 @@
+package com.github.binarywang.wxpay.bean.notify;
+
+import com.google.gson.Gson;
+import org.testng.annotations.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class WxPayRefundNotifyV3ResultTest {
+
+ private static final Gson GSON = new Gson();
+
+ @Test
+ public void shouldDeserializeOfficialAmountFields() {
+ String json = "{"
+ + "\"mchid\":\"1900000100\","
+ + "\"out_trade_no\":\"1217752501201407033233368018\","
+ + "\"transaction_id\":\"4200000000000000000000000000\","
+ + "\"out_refund_no\":\"1217752501201407033233368019\","
+ + "\"refund_id\":\"50000000382019052709732678859\","
+ + "\"refund_status\":\"SUCCESS\","
+ + "\"success_time\":\"2020-12-01T12:00:00+08:00\","
+ + "\"user_received_account\":\"支付用户零钱\","
+ + "\"amount\":{"
+ + "\"refund_fee\":10,"
+ + "\"settlement_refund\":9,"
+ + "\"total\":100,"
+ + "\"currency\":\"CNY\","
+ + "\"payer_total\":90,"
+ + "\"payer_refund\":10,"
+ + "\"settlement_total\":90,"
+ + "\"discount_refund\":1,"
+ + "\"from\":[{\"account\":\"AVAILABLE\",\"amount\":10}]"
+ + "}"
+ + "}";
+
+ WxPayRefundNotifyV3Result.DecryptNotifyResult result =
+ GSON.fromJson(json, WxPayRefundNotifyV3Result.DecryptNotifyResult.class);
+
+ assertThat(result.getAmount().getRefundFee()).isEqualTo(10);
+ assertThat(result.getAmount().getRefund()).isEqualTo(10);
+ assertThat(result.getAmount().getSettlementRefund()).isEqualTo(9);
+ assertThat(result.getAmount().getTotal()).isEqualTo(100);
+ assertThat(result.getAmount().getCurrency()).isEqualTo("CNY");
+ assertThat(result.getAmount().getPayerTotal()).isEqualTo(90);
+ assertThat(result.getAmount().getPayerRefund()).isEqualTo(10);
+ assertThat(result.getAmount().getSettlementTotal()).isEqualTo(90);
+ assertThat(result.getAmount().getDiscountRefund()).isEqualTo(1);
+ assertThat(result.getAmount().getFrom()).hasSize(1);
+ assertThat(result.getAmount().getFrom().get(0).getAccount()).isEqualTo("AVAILABLE");
+ assertThat(result.getAmount().getFrom().get(0).getAmount()).isEqualTo(10);
+ }
+
+ @Test
+ public void shouldKeepBackwardCompatibilityForRefundAlias() {
+ WxPayRefundNotifyV3Result.Amount amount =
+ GSON.fromJson("{\"refund\":88}", WxPayRefundNotifyV3Result.Amount.class);
+
+ assertThat(amount.getRefundFee()).isEqualTo(88);
+ assertThat(amount.getRefund()).isEqualTo(88);
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3RequestTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3RequestTest.java
new file mode 100644
index 0000000000..ebdc992082
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3RequestTest.java
@@ -0,0 +1,55 @@
+package com.github.binarywang.wxpay.bean.request;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import org.testng.annotations.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * {@link WxPayPartnerRefundV3Request} 单元测试
+ *
+ */
+public class WxPayPartnerRefundV3RequestTest {
+
+ @Test
+ public void testSpAppidAndSubAppidSerialization() {
+ WxPayPartnerRefundV3Request request = new WxPayPartnerRefundV3Request();
+ request.setSpAppid("wx8888888888888888");
+ request.setSubAppid("wxd678efh567hg6999");
+ request.setSubMchid("1230000109");
+ request.setOutRefundNo("1217752501201407033233368018");
+ request.setFundsAccount("AVAILABLE");
+
+ Gson gson = new Gson();
+ String json = gson.toJson(request);
+ JsonObject jsonObject = gson.fromJson(json, JsonObject.class);
+
+ assertThat(jsonObject.has("sp_appid")).isTrue();
+ assertThat(jsonObject.get("sp_appid").getAsString()).isEqualTo("wx8888888888888888");
+ assertThat(jsonObject.has("sub_appid")).isTrue();
+ assertThat(jsonObject.get("sub_appid").getAsString()).isEqualTo("wxd678efh567hg6999");
+ assertThat(jsonObject.has("sub_mchid")).isTrue();
+ assertThat(jsonObject.get("sub_mchid").getAsString()).isEqualTo("1230000109");
+ assertThat(jsonObject.has("out_refund_no")).isTrue();
+ assertThat(jsonObject.get("out_refund_no").getAsString()).isEqualTo("1217752501201407033233368018");
+ assertThat(jsonObject.has("funds_account")).isTrue();
+ assertThat(jsonObject.get("funds_account").getAsString()).isEqualTo("AVAILABLE");
+ }
+
+ @Test
+ public void testSubAppidIsOptional() {
+ WxPayPartnerRefundV3Request request = new WxPayPartnerRefundV3Request();
+ request.setSpAppid("wx8888888888888888");
+ request.setSubMchid("1230000109");
+ request.setOutRefundNo("1217752501201407033233368018");
+
+ Gson gson = new Gson();
+ String json = gson.toJson(request);
+ JsonObject jsonObject = gson.fromJson(json, JsonObject.class);
+
+ assertThat(jsonObject.has("sp_appid")).isTrue();
+ assertThat(jsonObject.get("sp_appid").getAsString()).isEqualTo("wx8888888888888888");
+ assertThat(jsonObject.has("sub_appid")).isFalse();
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/request/WxPayRefundV3RequestTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/request/WxPayRefundV3RequestTest.java
new file mode 100644
index 0000000000..1d7a79f3d4
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/request/WxPayRefundV3RequestTest.java
@@ -0,0 +1,56 @@
+package com.github.binarywang.wxpay.bean.request;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import org.testng.annotations.Test;
+
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * {@link WxPayRefundV3Request} 单元测试
+ *
+ */
+public class WxPayRefundV3RequestTest {
+
+ @Test
+ public void testFundsAccountSerialization() {
+ WxPayRefundV3Request request = new WxPayRefundV3Request();
+ request.setOutRefundNo("1217752501201407033233368018");
+ request.setFundsAccount("AVAILABLE");
+
+ Gson gson = new Gson();
+ String json = gson.toJson(request);
+ JsonObject jsonObject = gson.fromJson(json, JsonObject.class);
+
+ assertThat(jsonObject.has("funds_account")).isTrue();
+ assertThat(jsonObject.get("funds_account").getAsString()).isEqualTo("AVAILABLE");
+ }
+
+ @Test
+ public void testAmountFromSerialization() {
+ WxPayRefundV3Request.From from = new WxPayRefundV3Request.From();
+ from.setAccount("AVAILABLE");
+ from.setAmount(444);
+
+ WxPayRefundV3Request.Amount amount = new WxPayRefundV3Request.Amount();
+ amount.setRefund(888);
+ amount.setTotal(888);
+ amount.setCurrency("CNY");
+ amount.setFrom(Collections.singletonList(from));
+
+ WxPayRefundV3Request request = new WxPayRefundV3Request();
+ request.setAmount(amount);
+
+ Gson gson = new Gson();
+ String json = gson.toJson(request);
+ JsonObject jsonObject = gson.fromJson(json, JsonObject.class);
+ JsonArray fromJson = jsonObject.getAsJsonObject("amount").getAsJsonArray("from");
+
+ assertThat(fromJson).hasSize(1);
+ assertThat(fromJson.get(0).getAsJsonObject().get("account").getAsString()).isEqualTo("AVAILABLE");
+ assertThat(fromJson.get(0).getAsJsonObject().get("amount").getAsInt()).isEqualTo(444);
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/result/WxPayUnifiedOrderV3ResultTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/result/WxPayUnifiedOrderV3ResultTest.java
new file mode 100644
index 0000000000..15775ed701
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/result/WxPayUnifiedOrderV3ResultTest.java
@@ -0,0 +1,322 @@
+package com.github.binarywang.wxpay.bean.result;
+
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.v3.util.SignUtils;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+
+/**
+ *
+ * WxPayUnifiedOrderV3Result 测试类
+ * 主要测试prepayId字段和静态工厂方法的解耦功能
+ *
+ *
+ * @author copilot
+ */
+public class WxPayUnifiedOrderV3ResultTest {
+
+ /**
+ * 生成测试用的RSA密钥对
+ */
+ private KeyPair generateKeyPair() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(2048);
+ return keyPairGenerator.generateKeyPair();
+ }
+
+ /**
+ * 测试JsapiResult中的prepayId字段是否正确设置
+ */
+ @Test
+ public void testJsapiResultWithPrepayId() throws Exception {
+ // 准备测试数据
+ String testPrepayId = "wx201410272009395522657a690389285100";
+ String testAppId = "wx8888888888888888";
+ KeyPair keyPair = generateKeyPair();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // 创建WxPayUnifiedOrderV3Result对象
+ WxPayUnifiedOrderV3Result result = new WxPayUnifiedOrderV3Result();
+ result.setPrepayId(testPrepayId);
+
+ // 调用getPayInfo生成JsapiResult
+ WxPayUnifiedOrderV3Result.JsapiResult jsapiResult =
+ result.getPayInfo(TradeTypeEnum.JSAPI, testAppId, null, privateKey);
+
+ // 验证prepayId字段是否正确设置
+ Assert.assertNotNull(jsapiResult.getPrepayId(), "prepayId不应为null");
+ Assert.assertEquals(jsapiResult.getPrepayId(), testPrepayId, "prepayId应该与设置的值相同");
+
+ // 验证其他字段
+ Assert.assertEquals(jsapiResult.getAppId(), testAppId);
+ Assert.assertNotNull(jsapiResult.getTimeStamp());
+ Assert.assertNotNull(jsapiResult.getNonceStr());
+ Assert.assertEquals(jsapiResult.getPackageValue(), "prepay_id=" + testPrepayId);
+ Assert.assertEquals(jsapiResult.getSignType(), "RSA");
+ Assert.assertNotNull(jsapiResult.getPaySign());
+ }
+
+ /**
+ * 测试使用静态工厂方法生成JsapiResult(解耦场景)
+ */
+ @Test
+ public void testGetJsapiPayInfoStaticMethod() throws Exception {
+ // 准备测试数据
+ String testPrepayId = "wx201410272009395522657a690389285100";
+ String testAppId = "wx8888888888888888";
+ KeyPair keyPair = generateKeyPair();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // 使用静态工厂方法生成JsapiResult
+ WxPayUnifiedOrderV3Result.JsapiResult jsapiResult =
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo(testPrepayId, testAppId, privateKey);
+
+ // 验证prepayId字段
+ Assert.assertNotNull(jsapiResult.getPrepayId(), "prepayId不应为null");
+ Assert.assertEquals(jsapiResult.getPrepayId(), testPrepayId, "prepayId应该与输入的值相同");
+
+ // 验证其他字段
+ Assert.assertEquals(jsapiResult.getAppId(), testAppId);
+ Assert.assertNotNull(jsapiResult.getTimeStamp());
+ Assert.assertNotNull(jsapiResult.getNonceStr());
+ Assert.assertEquals(jsapiResult.getPackageValue(), "prepay_id=" + testPrepayId);
+ Assert.assertEquals(jsapiResult.getSignType(), "RSA");
+ Assert.assertNotNull(jsapiResult.getPaySign());
+ }
+
+ /**
+ * 测试使用静态工厂方法生成AppResult(解耦场景)
+ */
+ @Test
+ public void testGetAppPayInfoStaticMethod() throws Exception {
+ // 准备测试数据
+ String testPrepayId = "wx201410272009395522657a690389285100";
+ String testAppId = "wx8888888888888888";
+ String testMchId = "1900000109";
+ KeyPair keyPair = generateKeyPair();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // 使用静态工厂方法生成AppResult
+ WxPayUnifiedOrderV3Result.AppResult appResult =
+ WxPayUnifiedOrderV3Result.getAppPayInfo(testPrepayId, testAppId, testMchId, privateKey);
+
+ // 验证prepayId字段
+ Assert.assertNotNull(appResult.getPrepayId(), "prepayId不应为null");
+ Assert.assertEquals(appResult.getPrepayId(), testPrepayId, "prepayId应该与输入的值相同");
+
+ // 验证其他字段
+ Assert.assertEquals(appResult.getAppid(), testAppId);
+ Assert.assertEquals(appResult.getPartnerId(), testMchId);
+ Assert.assertNotNull(appResult.getTimestamp());
+ Assert.assertNotNull(appResult.getNoncestr());
+ Assert.assertEquals(appResult.getPackageValue(), "Sign=WXPay");
+ Assert.assertNotNull(appResult.getSign());
+ }
+
+ /**
+ * 测试解耦场景:先获取prepayId,后续再生成支付信息
+ */
+ @Test
+ public void testDecoupledScenario() throws Exception {
+ // 模拟场景:先创建订单获取prepayId
+ String testPrepayId = "wx201410272009395522657a690389285100";
+ String testAppId = "wx8888888888888888";
+ KeyPair keyPair = generateKeyPair();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // 步骤1:模拟从创建订单接口获取prepayId
+ WxPayUnifiedOrderV3Result orderResult = new WxPayUnifiedOrderV3Result();
+ orderResult.setPrepayId(testPrepayId);
+
+ // 获取prepayId用于存储
+ String storedPrepayId = orderResult.getPrepayId();
+ Assert.assertEquals(storedPrepayId, testPrepayId);
+
+ // 步骤2:后续支付失败时,使用存储的prepayId重新生成支付信息
+ WxPayUnifiedOrderV3Result.JsapiResult newPayInfo =
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo(storedPrepayId, testAppId, privateKey);
+
+ // 验证重新生成的支付信息
+ Assert.assertEquals(newPayInfo.getPrepayId(), storedPrepayId);
+ Assert.assertEquals(newPayInfo.getPackageValue(), "prepay_id=" + storedPrepayId);
+ Assert.assertNotNull(newPayInfo.getPaySign());
+ }
+
+ /**
+ * 测试多次生成支付信息,签名应该不同(因为timestamp和nonceStr每次都不同)
+ */
+ @Test
+ public void testMultipleGenerationsHaveDifferentSignatures() throws Exception {
+ String testPrepayId = "wx201410272009395522657a690389285100";
+ String testAppId = "wx8888888888888888";
+ KeyPair keyPair = generateKeyPair();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // 生成第一次支付信息
+ WxPayUnifiedOrderV3Result.JsapiResult result1 =
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo(testPrepayId, testAppId, privateKey);
+
+ // 等待一秒确保timestamp不同
+ Thread.sleep(1000);
+
+ // 生成第二次支付信息
+ WxPayUnifiedOrderV3Result.JsapiResult result2 =
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo(testPrepayId, testAppId, privateKey);
+
+ // prepayId应该相同
+ Assert.assertEquals(result1.getPrepayId(), result2.getPrepayId());
+
+ // 但是timestamp、nonceStr和签名应该不同
+ Assert.assertNotEquals(result1.getTimeStamp(), result2.getTimeStamp(), "timestamp应该不同");
+ Assert.assertNotEquals(result1.getNonceStr(), result2.getNonceStr(), "nonceStr应该不同");
+ Assert.assertNotEquals(result1.getPaySign(), result2.getPaySign(), "签名应该不同");
+ }
+
+ /**
+ * 测试AppResult中的prepayId字段
+ */
+ @Test
+ public void testAppResultWithPrepayId() throws Exception {
+ String testPrepayId = "wx201410272009395522657a690389285100";
+ String testAppId = "wx8888888888888888";
+ String testMchId = "1900000109";
+ KeyPair keyPair = generateKeyPair();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ WxPayUnifiedOrderV3Result result = new WxPayUnifiedOrderV3Result();
+ result.setPrepayId(testPrepayId);
+
+ // 调用getPayInfo生成AppResult
+ WxPayUnifiedOrderV3Result.AppResult appResult =
+ result.getPayInfo(TradeTypeEnum.APP, testAppId, testMchId, privateKey);
+
+ // 验证prepayId字段
+ Assert.assertNotNull(appResult.getPrepayId(), "prepayId不应为null");
+ Assert.assertEquals(appResult.getPrepayId(), testPrepayId, "prepayId应该与设置的值相同");
+ }
+
+ /**
+ * 测试JsapiResult序列化为JSON时,packageValue字段名应为package(兼容微信官方API要求)
+ */
+ @Test
+ public void testJsapiResultJsonSerializationPackageFieldName() throws Exception {
+ String testPrepayId = "wx201410272009395522657a690389285100";
+ String testAppId = "wx8888888888888888";
+ KeyPair keyPair = generateKeyPair();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ WxPayUnifiedOrderV3Result.JsapiResult jsapiResult =
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo(testPrepayId, testAppId, privateKey);
+
+ // 验证Java字段名仍为packageValue
+ Assert.assertEquals(jsapiResult.getPackageValue(), "prepay_id=" + testPrepayId);
+
+ // 验证JSON序列化后字段名为package(微信官方要求)
+ Gson gson = new Gson();
+ JsonObject jsonObject = gson.toJsonTree(jsapiResult).getAsJsonObject();
+ Assert.assertTrue(jsonObject.has("package"), "JSON中应该包含package字段");
+ Assert.assertFalse(jsonObject.has("packageValue"), "JSON中不应该包含packageValue字段");
+ Assert.assertEquals(jsonObject.get("package").getAsString(), "prepay_id=" + testPrepayId);
+ }
+
+ /**
+ * 测试AppResult序列化为JSON时,packageValue字段名应为package(兼容微信官方API要求)
+ */
+ @Test
+ public void testAppResultJsonSerializationPackageFieldName() throws Exception {
+ String testPrepayId = "wx201410272009395522657a690389285100";
+ String testAppId = "wx8888888888888888";
+ String testMchId = "1900000109";
+ KeyPair keyPair = generateKeyPair();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ WxPayUnifiedOrderV3Result.AppResult appResult =
+ WxPayUnifiedOrderV3Result.getAppPayInfo(testPrepayId, testAppId, testMchId, privateKey);
+
+ // 验证Java字段名仍为packageValue
+ Assert.assertEquals(appResult.getPackageValue(), "Sign=WXPay");
+
+ // 验证JSON序列化后字段名为package(微信官方要求)
+ Gson gson = new Gson();
+ JsonObject jsonObject = gson.toJsonTree(appResult).getAsJsonObject();
+ Assert.assertTrue(jsonObject.has("package"), "JSON中应该包含package字段");
+ Assert.assertFalse(jsonObject.has("packageValue"), "JSON中不应该包含packageValue字段");
+ Assert.assertEquals(jsonObject.get("package").getAsString(), "Sign=WXPay");
+ // 验证JSON序列化后partnerid和prepayid字段名为全小写(微信官方要求)
+ Assert.assertTrue(jsonObject.has("partnerid"), "JSON中应该包含partnerid字段");
+ Assert.assertFalse(jsonObject.has("partnerId"), "JSON中不应该包含驼峰的partnerId字段");
+ Assert.assertEquals(jsonObject.get("partnerid").getAsString(), testMchId);
+ Assert.assertTrue(jsonObject.has("prepayid"), "JSON中应该包含prepayid字段");
+ Assert.assertFalse(jsonObject.has("prepayId"), "JSON中不应该包含驼峰的prepayId字段");
+ Assert.assertEquals(jsonObject.get("prepayid").getAsString(), testPrepayId);
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "prepayId, appId 和 privateKey 不能为空")
+ public void testGetJsapiPayInfoWithNullPrepayId() {
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo(null, "appId", null);
+ }
+
+ /**
+ * 测试getJsapiPayInfo方法的空值验证 - appId为null
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "prepayId, appId 和 privateKey 不能为空")
+ public void testGetJsapiPayInfoWithNullAppId() throws Exception {
+ KeyPair keyPair = generateKeyPair();
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo("prepayId", null, keyPair.getPrivate());
+ }
+
+ /**
+ * 测试getJsapiPayInfo方法的空值验证 - privateKey为null
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "prepayId, appId 和 privateKey 不能为空")
+ public void testGetJsapiPayInfoWithNullPrivateKey() {
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo("prepayId", "appId", null);
+ }
+
+ /**
+ * 测试getAppPayInfo方法的空值验证 - prepayId为null
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "prepayId, appId, mchId 和 privateKey 不能为空")
+ public void testGetAppPayInfoWithNullPrepayId() {
+ WxPayUnifiedOrderV3Result.getAppPayInfo(null, "appId", "mchId", null);
+ }
+
+ /**
+ * 测试getAppPayInfo方法的空值验证 - appId为null
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "prepayId, appId, mchId 和 privateKey 不能为空")
+ public void testGetAppPayInfoWithNullAppId() throws Exception {
+ KeyPair keyPair = generateKeyPair();
+ WxPayUnifiedOrderV3Result.getAppPayInfo("prepayId", null, "mchId", keyPair.getPrivate());
+ }
+
+ /**
+ * 测试getAppPayInfo方法的空值验证 - mchId为null
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "prepayId, appId, mchId 和 privateKey 不能为空")
+ public void testGetAppPayInfoWithNullMchId() throws Exception {
+ KeyPair keyPair = generateKeyPair();
+ WxPayUnifiedOrderV3Result.getAppPayInfo("prepayId", "appId", null, keyPair.getPrivate());
+ }
+
+ /**
+ * 测试getAppPayInfo方法的空值验证 - privateKey为null
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "prepayId, appId, mchId 和 privateKey 不能为空")
+ public void testGetAppPayInfoWithNullPrivateKey() {
+ WxPayUnifiedOrderV3Result.getAppPayInfo("prepayId", "appId", "mchId", null);
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/result/WxSignQueryResultTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/result/WxSignQueryResultTest.java
new file mode 100644
index 0000000000..12d62b148d
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/result/WxSignQueryResultTest.java
@@ -0,0 +1,190 @@
+package com.github.binarywang.wxpay.bean.result;
+
+import com.github.binarywang.wxpay.util.XmlConfig;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * WxSignQueryResult 单元测试
+ *
+ * @author Binary Wang
+ */
+public class WxSignQueryResultTest {
+
+ /**
+ * 测试 XML 解析,特别是 contract_expired_time 字段
+ */
+ @Test
+ public void testFromXML() {
+ /*
+ * xml样例字符串来自于官方文档
+ * https://pay.weixin.qq.com/doc/v2/merchant/4011987640
+ */
+ String xmlString = "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " 203\n" +
+ " 66\n" +
+ " \n" +
+ " 123\n" +
+ " \n" +
+ " \n" +
+ " 1\n" +
+ " 2015-07-01 10:00:00\n" +
+ " 2015-07-01 10:00:00\n" +
+ " 2015-07-01 10:00:00\n" +
+ " 3\n" +
+ " \n" +
+ " 0\n" +
+ " \n" +
+ " \n" +
+ "";
+
+ // 启用 fastMode 以覆盖 WxSignQueryResult#loadXml 分支
+ XmlConfig.fastMode = true;
+ try {
+ WxSignQueryResult result = WxSignQueryResult.fromXML(xmlString, WxSignQueryResult.class);
+
+ // 验证基本字段
+ Assert.assertEquals(result.getReturnCode(), "SUCCESS");
+ Assert.assertEquals(result.getResultCode(), "SUCCESS");
+ Assert.assertEquals(result.getMchId(), "80000000");
+ Assert.assertEquals(result.getAppid(), "wx426b3015555b46be");
+
+ // 验证签约相关字段
+ Assert.assertEquals(result.getContractId(), "203");
+ Assert.assertEquals(result.getPlanId(), "66");
+ Assert.assertEquals(result.getOpenId(), "oHZx6uMbIG46UXQ3SKxVYEgw1LZs");
+ Assert.assertEquals(result.getRequestSerial().longValue(), 123L);
+ Assert.assertEquals(result.getContractCode(), "1005");
+ Assert.assertEquals(result.getContractDisplayAccount(), "test");
+ Assert.assertEquals(result.getContractState().intValue(), 1);
+
+ // 重点测试时间字段,特别是 contract_expired_time
+ Assert.assertEquals(result.getContractSignedTime(), "2015-07-01 10:00:00");
+ Assert.assertEquals(result.getContractExpiredTime(), "2015-07-01 10:00:00");
+ Assert.assertEquals(result.getContractTerminatedTime(), "2015-07-01 10:00:00");
+
+ // 验证其他字段
+ Assert.assertEquals(result.getContractTerminatedMode().intValue(), 3);
+ Assert.assertEquals(result.getContractTerminationRemark(), "delete ....");
+ } finally {
+ // 恢复默认值
+ XmlConfig.fastMode = false;
+ }
+ }
+
+ /**
+ * 测试 XML 解析 - 只包含必填字段
+ */
+ @Test
+ public void testFromXML_RequiredFieldsOnly() {
+ String xmlString = "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " Wx15463511252015071056489715\n" +
+ " 123\n" +
+ " 1695\n" +
+ " \n" +
+ " \n" +
+ " 0\n" +
+ " 2015-07-01 10:00:00\n" +
+ " 2016-07-01 10:00:00\n" +
+ " \n" +
+ " \n" +
+ "";
+
+ // 启用 fastMode 以覆盖 WxSignQueryResult#loadXml 分支
+ XmlConfig.fastMode = true;
+ try {
+ WxSignQueryResult result = WxSignQueryResult.fromXML(xmlString, WxSignQueryResult.class);
+
+ // 验证必填字段
+ Assert.assertEquals(result.getReturnCode(), "SUCCESS");
+ Assert.assertEquals(result.getResultCode(), "SUCCESS");
+ Assert.assertEquals(result.getContractId(), "Wx15463511252015071056489715");
+ Assert.assertEquals(result.getPlanId(), "123");
+ Assert.assertEquals(result.getContractState().intValue(), 0);
+
+ // 验证 contract_expired_time 字段能正确解析
+ Assert.assertEquals(result.getContractExpiredTime(), "2016-07-01 10:00:00");
+
+ // 验证非必填字段为 null
+ Assert.assertNull(result.getContractTerminatedTime());
+ Assert.assertNull(result.getContractTerminatedMode());
+ Assert.assertNull(result.getContractTerminationRemark());
+ Assert.assertNull(result.getChangeType());
+ Assert.assertNull(result.getOperateTime());
+ } finally {
+ // 恢复默认值
+ XmlConfig.fastMode = false;
+ }
+ }
+
+ /**
+ * 测试签约回调通知 XML 解析 - change_type = ADD
+ */
+ @Test
+ public void testFromXML_SignCallback_Add() {
+ String xmlString = "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " 123\n" +
+ " \n" +
+ " 1695\n" +
+ " \n" +
+ " \n" +
+ " 2015-07-01 10:00:00\n" +
+ " 2016-07-01 10:00:00\n" +
+ " \n" +
+ "";
+
+ XmlConfig.fastMode = true;
+ try {
+ WxSignQueryResult result = WxSignQueryResult.fromXML(xmlString, WxSignQueryResult.class);
+
+ Assert.assertEquals(result.getChangeType(), "ADD");
+ Assert.assertEquals(result.getOperateTime(), "2015-07-01 10:00:00");
+ } finally {
+ XmlConfig.fastMode = false;
+ }
+ }
+
+ /**
+ * 测试解约回调通知 XML 解析 - change_type = DELETE
+ */
+ @Test
+ public void testFromXML_SignCallback_Delete() {
+ String xmlString = "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " 123\n" +
+ " \n" +
+ " 1695\n" +
+ " \n" +
+ " \n" +
+ " 2015-07-01 11:00:00\n" +
+ " 2\n" +
+ " \n" +
+ "";
+
+ XmlConfig.fastMode = true;
+ try {
+ WxSignQueryResult result = WxSignQueryResult.fromXML(xmlString, WxSignQueryResult.class);
+
+ Assert.assertEquals(result.getChangeType(), "DELETE");
+ Assert.assertEquals(result.getOperateTime(), "2015-07-01 11:00:00");
+ Assert.assertEquals(result.getContractTerminatedMode().intValue(), 2);
+ } finally {
+ XmlConfig.fastMode = false;
+ }
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/config/WxPayConfigPrivateKeyTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/config/WxPayConfigPrivateKeyTest.java
new file mode 100644
index 0000000000..927e0c4125
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/config/WxPayConfigPrivateKeyTest.java
@@ -0,0 +1,116 @@
+package com.github.binarywang.wxpay.config;
+
+import com.github.binarywang.wxpay.exception.WxPayException;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+/**
+ * Test cases for private key format handling in WxPayConfig
+ */
+public class WxPayConfigPrivateKeyTest {
+
+ @Test
+ public void testPrivateKeyStringFormat_PemFormat() {
+ WxPayConfig config = new WxPayConfig();
+
+ // Set minimal required configuration
+ config.setMchId("1234567890");
+ config.setApiV3Key("test-api-v3-key-32-characters-long");
+ config.setCertSerialNo("test-serial-number");
+
+ // Test with PEM format private key string that would previously fail
+ String pemKey = "-----BEGIN PRIVATE KEY-----\n" +
+ "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC2pK3buBufh8Vo\n" +
+ "X4sfYbZ5CcPeGMnVQTGmj0b6\n" +
+ "-----END PRIVATE KEY-----";
+
+ config.setPrivateKeyString(pemKey);
+
+ // This should not throw a "无效的密钥格式" exception immediately
+ // The actual key validation will happen during HTTP client initialization
+ // but at least the format parsing should not fail
+
+ try {
+ // Try to initialize API V3 HTTP client - this might fail for other reasons
+ // (like invalid key content) but should not fail due to format parsing
+ config.initApiV3HttpClient();
+ // If we get here without InvalidKeySpecException, the format detection worked
+ } catch (WxPayException e) {
+ // Check that it's not the specific "无效的密钥格式" error from PemUtils
+ if (e.getCause() != null &&
+ e.getCause().getMessage() != null &&
+ e.getCause().getMessage().contains("无效的密钥格式")) {
+ fail("Private key format detection failed - PEM format was not handled correctly: " + e.getMessage());
+ }
+ // Other exceptions are acceptable for this test since we're using a dummy key
+ } catch (Exception e) {
+ // Check for the specific InvalidKeySpecException that indicates format problems
+ if (e.getCause() != null &&
+ e.getCause().getMessage() != null &&
+ e.getCause().getMessage().contains("无效的密钥格式")) {
+ fail("Private key format detection failed - PEM format was not handled correctly: " + e.getMessage());
+ }
+ // Other exceptions are acceptable for this test since we're using a dummy key
+ }
+ }
+
+ @Test
+ public void testPrivateKeyStringFormat_EmptyString() {
+ WxPayConfig config = new WxPayConfig();
+
+ // Test with empty string - should not cause format errors
+ config.setPrivateKeyString("");
+
+ // This should handle empty strings gracefully
+ // No assertion needed, just ensuring no exceptions during object creation
+ assertNotNull(config);
+ }
+
+ @Test
+ public void testPrivateKeyStringFormat_NullString() {
+ WxPayConfig config = new WxPayConfig();
+
+ // Test with null string - should not cause format errors
+ config.setPrivateKeyString(null);
+
+ // This should handle null strings gracefully
+ assertNotNull(config);
+ }
+
+ @Test
+ public void testPrivateCertStringFormat_PemFormat() {
+ WxPayConfig config = new WxPayConfig();
+
+ // Set minimal required configuration
+ config.setMchId("1234567890");
+ config.setApiV3Key("test-api-v3-key-32-characters-long");
+
+ // Test with PEM format certificate string that would previously fail
+ String pemCert = "-----BEGIN CERTIFICATE-----\n" +
+ "MIICdTCCAd4CAQAwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQVUxEzARBgNV\n" +
+ "BAsKClRlc3QgQ2VydCBEYXRhMRswGQYDVQQDDBJUZXN0IENlcnRpZmljYXRlQ0Ew\n" +
+ "-----END CERTIFICATE-----";
+
+ config.setPrivateCertString(pemCert);
+
+ // This should not throw a format parsing exception immediately
+ // The actual certificate validation will happen during HTTP client initialization
+ // but at least the format parsing should not fail
+
+ try {
+ // Try to initialize API V3 HTTP client - this might fail for other reasons
+ // (like invalid cert content) but should not fail due to format parsing
+ config.initApiV3HttpClient();
+ // If we get here without Base64 decoding issues, the format detection worked
+ } catch (Exception e) {
+ // Check that it's not the specific Base64 decoding error
+ if (e.getCause() != null &&
+ e.getCause().getMessage() != null &&
+ e.getCause().getMessage().contains("Illegal base64 character")) {
+ fail("Certificate format detection failed - PEM format was not handled correctly: " + e.getMessage());
+ }
+ // Other exceptions are acceptable for this test since we're using a dummy cert
+ }
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/config/WxPayConfigTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/config/WxPayConfigTest.java
index 46bc23aac2..0b5d1b7329 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/config/WxPayConfigTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/config/WxPayConfigTest.java
@@ -2,6 +2,8 @@
import org.testng.annotations.Test;
+import static org.testng.Assert.assertEquals;
+
/**
*
* Created by BinaryWang on 2017/6/18.
@@ -38,6 +40,15 @@ public void testHashCode() {
payConfig.hashCode();
}
+ @Test
+ public void testApiHostUrlPath() {
+ payConfig.setApiHostUrl("http://10.0.0.1:3128/");
+ payConfig.setApiHostUrlPath("api-weixin/");
+ assertEquals(payConfig.getApiHostUrl(), "http://10.0.0.1:3128");
+ assertEquals(payConfig.getApiHostUrlPath(), "/api-weixin");
+ assertEquals(payConfig.getApiHostWithPathPrefix(), "http://10.0.0.1:3128/api-weixin");
+ }
+
@Test
public void testInitSSLContext_base64() throws Exception {
payConfig.setMchId("123");
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/BusinessOperationTransferServiceTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/BusinessOperationTransferServiceTest.java
new file mode 100644
index 0000000000..672483f96b
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/BusinessOperationTransferServiceTest.java
@@ -0,0 +1,104 @@
+package com.github.binarywang.wxpay.service;
+
+import com.github.binarywang.wxpay.bean.transfer.*;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.constant.WxPayConstants;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.util.Arrays;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * 运营工具-商家转账API测试
+ *
+ * @author WxJava Team
+ */
+public class BusinessOperationTransferServiceTest {
+
+ private WxPayService wxPayService;
+
+ @BeforeClass
+ public void setup() {
+ WxPayConfig config = new WxPayConfig();
+ config.setAppId("test_app_id");
+ config.setMchId("test_mch_id");
+
+ wxPayService = new WxPayServiceImpl();
+ wxPayService.setConfig(config);
+ }
+
+ @Test
+ public void testServiceInitialization() {
+ BusinessOperationTransferService service = this.wxPayService.getBusinessOperationTransferService();
+ assertThat(service).isNotNull();
+ }
+
+ @Test
+ public void testRequestBuilder() {
+
+ // 构建转账请求
+ BusinessOperationTransferRequest.TransferSceneReportInfo reportInfo = new BusinessOperationTransferRequest.TransferSceneReportInfo();
+ reportInfo.setInfoType("test_info_type");
+ reportInfo.setInfoContent("test_info_content");
+
+ BusinessOperationTransferRequest request = BusinessOperationTransferRequest.newBuilder()
+ .appid("test_app_id")
+ .outBillNo("OT" + System.currentTimeMillis())
+ .transferSceneId(WxPayConstants.OperationSceneId.OPERATION_CASH_MARKETING)
+ .transferSceneReportInfos(Arrays.asList(reportInfo))
+ .openid("test_openid")
+ .transferAmount(100)
+ .transferRemark("测试转账")
+ .userRecvPerception(WxPayConstants.UserRecvPerception.CASH_MARKETING.CASH)
+ .build();
+
+ assertThat(request.getAppid()).isEqualTo("test_app_id");
+ assertThat(request.getTransferSceneId()).isEqualTo(WxPayConstants.OperationSceneId.OPERATION_CASH_MARKETING);
+ assertThat(request.getTransferAmount()).isEqualTo(100);
+ assertThat(request.getTransferRemark()).isEqualTo("测试转账");
+ }
+
+ @Test
+ public void testQueryRequestBuilder() {
+ BusinessOperationTransferQueryRequest request = BusinessOperationTransferQueryRequest.newBuilder()
+ .outBillNo("OT123456789")
+ .appid("test_app_id")
+ .build();
+
+ assertThat(request.getOutBillNo()).isEqualTo("OT123456789");
+ assertThat(request.getAppid()).isEqualTo("test_app_id");
+ }
+
+ @Test
+ public void testConstants() {
+ // 测试运营工具转账场景ID常量
+ assertThat(WxPayConstants.OperationSceneId.OPERATION_CASH_MARKETING).isEqualTo("2001");
+ assertThat(WxPayConstants.OperationSceneId.OPERATION_COMMISSION).isEqualTo("2002");
+ assertThat(WxPayConstants.OperationSceneId.OPERATION_PROMOTION).isEqualTo("2003");
+ }
+
+ @Test
+ public void testResultClasses() {
+ // 测试结果类的基本功能
+ BusinessOperationTransferResult result = new BusinessOperationTransferResult();
+ result.setOutBillNo("test_out_bill_no");
+ result.setTransferBillNo("test_transfer_bill_no");
+ result.setState("SUCCESS");
+ result.setPackageInfo("test_package_info");
+
+ assertThat(result.getOutBillNo()).isEqualTo("test_out_bill_no");
+ assertThat(result.getTransferBillNo()).isEqualTo("test_transfer_bill_no");
+ assertThat(result.getState()).isEqualTo("SUCCESS");
+ assertThat(result.getPackageInfo()).isEqualTo("test_package_info");
+
+ BusinessOperationTransferQueryResult queryResult = new BusinessOperationTransferQueryResult();
+ queryResult.setOperationSceneId("2001");
+ queryResult.setTransferAmount(100);
+
+ assertThat(queryResult.getOperationSceneId()).isEqualTo("2001");
+ assertThat(queryResult.getTransferAmount()).isEqualTo(100);
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImplTest.java
index 955071e10f..7dff396de5 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImplTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImplTest.java
@@ -33,6 +33,7 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Calendar;
@@ -626,6 +627,42 @@ public void testParseOrderNotifyResult() throws Exception {
}
+ /**
+ * Test parse order notify result with JSON format should give helpful error.
+ * 测试当传入V3版本的JSON格式通知数据时,应该抛出清晰的错误提示
+ *
+ * @throws Exception the exception
+ */
+ @Test
+ public void testParseOrderNotifyResultWithJsonShouldGiveHelpfulError() throws Exception {
+ String jsonString = "{\n" +
+ " \"id\": \"EV-2018022511223320873\",\n" +
+ " \"create_time\": \"2015-05-20T13:29:35+08:00\",\n" +
+ " \"resource_type\": \"encrypt-resource\",\n" +
+ " \"event_type\": \"TRANSACTION.SUCCESS\",\n" +
+ " \"summary\": \"支付成功\",\n" +
+ " \"resource\": {\n" +
+ " \"algorithm\": \"AEAD_AES_256_GCM\",\n" +
+ " \"ciphertext\": \"test\",\n" +
+ " \"associated_data\": \"transaction\",\n" +
+ " \"nonce\": \"test\"\n" +
+ " }\n" +
+ "}";
+
+ try {
+ this.payService.parseOrderNotifyResult(jsonString);
+ fail("Expected WxPayException for JSON input");
+ } catch (WxPayException e) {
+ // 验证错误消息包含V3版本和parseOrderNotifyV3Result方法的指导信息
+ String message = e.getMessage();
+ assertTrue(message.contains("V3版本"), "错误消息应包含'V3版本'");
+ assertTrue(message.contains("JSON格式"), "错误消息应包含'JSON格式'");
+ assertTrue(message.contains("parseOrderNotifyV3Result"), "错误消息应包含'parseOrderNotifyV3Result'方法名");
+ assertTrue(message.contains("SignatureHeader"), "错误消息应包含'SignatureHeader'");
+ log.info("JSON格式检测正常,错误提示: {}", message);
+ }
+ }
+
/**
* Test get wx api data.
*
@@ -976,4 +1013,44 @@ public void testCreatePartnerOrderV3() throws WxPayException {
WxPayUnifiedOrderV3Result.JsapiResult result = payService.createPartnerOrderV3(TradeTypeEnum.JSAPI, request);
System.out.println(result);
}
+
+ @Test
+ public void test_certSerialNoExtractedFromPrivateCertContentOrPrivateCertString() throws Exception {
+ WxPayConfig wxPayConfig = new WxPayConfig();
+ //服务商的参数
+ wxPayConfig.setMchId("xxx");
+ wxPayConfig.setAppId("xxx");
+ wxPayConfig.setApiV3Key("xxx");
+ wxPayConfig.setPrivateKeyContent("xxx".getBytes(StandardCharsets.UTF_8));
+ wxPayConfig.setPrivateCertContent("xxx".getBytes(StandardCharsets.UTF_8)
+ );
+ wxPayConfig.setPublicKeyId("xxx");
+ wxPayConfig.setPublicKeyContent("xxx".getBytes(StandardCharsets.UTF_8));
+ //创建支付服务
+ WxPayService wxPayService = new WxPayServiceImpl();
+ wxPayService.setConfig(wxPayConfig);
+
+ String outTradeNo = RandomUtils.getRandomStr();
+ String notifyUrl = "https://api.qq.com/";
+ System.out.println("outTradeNo = " + outTradeNo);
+ WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
+ request.setOutTradeNo(outTradeNo);
+ request.setNotifyUrl(notifyUrl);
+ request.setDescription("test");
+
+ WxPayUnifiedOrderV3Request.Payer payer = new WxPayUnifiedOrderV3Request.Payer();
+ payer.setOpenid("xxx");
+ request.setPayer(payer);
+
+ //构建金额信息
+ WxPayUnifiedOrderV3Request.Amount amount = new WxPayUnifiedOrderV3Request.Amount();
+ //设置币种信息
+ amount.setCurrency(WxPayConstants.CurrencyType.CNY);
+ //设置金额
+ amount.setTotal(BaseWxPayRequest.yuan2Fen(BigDecimal.ONE));
+ request.setAmount(amount);
+
+ wxPayService.createOrderV3(TradeTypeEnum.JSAPI, request);
+ }
+
}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImplTest.java
index d07392f17e..02edae7d84 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImplTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImplTest.java
@@ -4,7 +4,7 @@
import com.github.binarywang.wxpay.bean.businesscircle.PaidResult;
import com.github.binarywang.wxpay.bean.businesscircle.PointsNotifyRequest;
import com.github.binarywang.wxpay.bean.businesscircle.RefundResult;
-import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.testbase.ApiTestModule;
@@ -52,10 +52,10 @@ public void testNotifyPointsV3() throws WxPayException {
@Test
public void testDecryptPaidNotifyDataResource() throws WxPayException {
SignatureHeader header = new SignatureHeader();
- header.setSerialNo("Wechatpay-Serial");
+ header.setSerial("Wechatpay-Serial");
header.setTimeStamp("Wechatpay-Timestamp");
header.setNonce("Wechatpay-Nonce");
- header.setSigned("Wechatpay-Signature");
+ header.setSignature("Wechatpay-Signature");
String data = "body";
BusinessCircleNotifyData notifyData = wxPayService.getBusinessCircleService().parseNotifyData(data, header);
PaidResult result = wxPayService.getBusinessCircleService().decryptPaidNotifyDataResource(notifyData);
@@ -66,10 +66,10 @@ public void testDecryptPaidNotifyDataResource() throws WxPayException {
@Test
public void testDecryptRefundNotifyDataResource() throws WxPayException {
SignatureHeader header = new SignatureHeader();
- header.setSerialNo("Wechatpay-Serial");
+ header.setSerial("Wechatpay-Serial");
header.setTimeStamp("Wechatpay-Timestamp");
header.setNonce("Wechatpay-Nonce");
- header.setSigned("Wechatpay-Signature");
+ header.setSignature("Wechatpay-Signature");
String data = "body";
BusinessCircleNotifyData notifyData = wxPayService.getBusinessCircleService().parseNotifyData(data, header);
RefundResult result = wxPayService.getBusinessCircleService().decryptRefundNotifyDataResource(notifyData);
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/EcommerceServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/EcommerceServiceImplTest.java
index e250b9ea1c..73aff7f6bb 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/EcommerceServiceImplTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/EcommerceServiceImplTest.java
@@ -1,13 +1,17 @@
package com.github.binarywang.wxpay.service.impl;
-import com.google.common.collect.Lists;
import com.github.binarywang.wxpay.bean.ecommerce.*;
import com.github.binarywang.wxpay.bean.ecommerce.enums.SpAccountTypeEnum;
-import com.github.binarywang.wxpay.bean.ecommerce.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
+import com.github.binarywang.wxpay.bean.request.CombineTransactionsRequest;
+import com.github.binarywang.wxpay.bean.request.WxPayPartnerOrderQueryV3Request;
+import com.github.binarywang.wxpay.bean.result.CombineTransactionsResult;
+import com.github.binarywang.wxpay.bean.result.WxPayPartnerOrderQueryV3Result;
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.testbase.ApiTestModule;
-import com.google.gson.GsonBuilder;
+import com.google.common.collect.Lists;
import com.google.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.util.RandomUtils;
@@ -42,16 +46,16 @@ public void testNotifySign() {
SignatureHeader header = new SignatureHeader();
header.setNonce(nonce);
- header.setSerialNo(serialNo);
+ header.setSerial(serialNo);
header.setTimeStamp(timeStamp);
- header.setSigned(signed);
+ header.setSignature(signed);
String beforeSign = String.format("%s\n%s\n%s\n",
header.getTimeStamp(),
header.getNonce(),
notifyData);
- boolean signResult = wxPayService.getConfig().getVerifier().verify(header.getSerialNo(),
- beforeSign.getBytes(StandardCharsets.UTF_8), header.getSigned());
+ boolean signResult = wxPayService.getConfig().getVerifier().verify(header.getSerial(),
+ beforeSign.getBytes(StandardCharsets.UTF_8), header.getSignature());
log.info("签名结果:{} \nheader:{} \ndata:{}", signResult, header, notifyData);
}
@@ -97,23 +101,23 @@ public void testCombinePay() throws WxPayException {
subOrder2.setAmount(requestAmount);
request.setSubOrders(Arrays.asList(subOrder1, subOrder2));
- TransactionsResult result = wxPayService.getEcommerceService().combine(TradeTypeEnum.JSAPI, request);
+ CombineTransactionsResult result = wxPayService.getEcommerceService().combine(TradeTypeEnum.JSAPI, request);
System.out.println("result = " + result);
}
@Test
public void testQueryPartnerTransactions() throws WxPayException {
- PartnerTransactionsQueryRequest request = new PartnerTransactionsQueryRequest();
+ WxPayPartnerOrderQueryV3Request request = new WxPayPartnerOrderQueryV3Request();
//服务商商户号
- request.setSpMchid("");
+ request.setSpMchId("");
//二级商户号
- request.setSubMchid("");
+ request.setSubMchId("");
//商户订单号
request.setOutTradeNo("");
//微信订单号
request.setTransactionId("");
- PartnerTransactionsResult result = wxPayService.getEcommerceService().queryPartnerTransactions(request);
+ WxPayPartnerOrderQueryV3Result result = wxPayService.getEcommerceService().queryPartnerOrder(request);
System.out.println("result = " + result);
}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImplTest.java
index c8dd069b44..845992e43c 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImplTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImplTest.java
@@ -1,6 +1,7 @@
package com.github.binarywang.wxpay.service.impl;
import com.github.binarywang.wxpay.bean.media.ImageUploadResult;
+import com.github.binarywang.wxpay.bean.media.VideoUploadResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.MerchantMediaService;
import com.github.binarywang.wxpay.service.WxPayService;
@@ -51,4 +52,45 @@ public void testImageUploadV3() throws WxPayException, IOException {
log.info("mediaId2:[{}]",mediaId2);
}
+
+ @Test
+ public void testVideoUploadV3() throws WxPayException, IOException {
+
+ MerchantMediaService merchantMediaService = new MerchantMediaServiceImpl(wxPayService);
+
+ String filePath = "你的视频文件的路径地址";
+// String filePath = "WxJava/test-video.mp4";
+
+ File file = new File(filePath);
+
+ VideoUploadResult videoUploadResult = merchantMediaService.videoUploadV3(file);
+ String mediaId = videoUploadResult.getMediaId();
+
+ log.info("视频上传成功,mediaId:[{}]", mediaId);
+
+ VideoUploadResult videoUploadResult2 = merchantMediaService.videoUploadV3(file);
+ String mediaId2 = videoUploadResult2.getMediaId();
+
+ log.info("视频上传成功2,mediaId2:[{}]", mediaId2);
+
+ }
+
+ @Test
+ public void testVideoUploadV3WithInputStream() throws WxPayException, IOException {
+
+ MerchantMediaService merchantMediaService = new MerchantMediaServiceImpl(wxPayService);
+
+ String filePath = "你的视频文件的路径地址";
+// String filePath = "WxJava/test-video.mp4";
+
+ File file = new File(filePath);
+
+ try (java.io.FileInputStream inputStream = new java.io.FileInputStream(file)) {
+ VideoUploadResult videoUploadResult = merchantMediaService.videoUploadV3(inputStream, file.getName());
+ String mediaId = videoUploadResult.getMediaId();
+
+ log.info("通过InputStream上传视频成功,mediaId:[{}]", mediaId);
+ }
+
+ }
}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MerchantTransferServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MerchantTransferServiceImplTest.java
index d578fcab93..838cd512aa 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MerchantTransferServiceImplTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MerchantTransferServiceImplTest.java
@@ -1,6 +1,7 @@
package com.github.binarywang.wxpay.service.impl;
import com.github.binarywang.wxpay.bean.merchanttransfer.*;
+import com.github.binarywang.wxpay.bean.transfer.ReservationTransferBatchRequest;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.testbase.ApiTestModule;
@@ -107,4 +108,53 @@ public void queryDetailElectronicBill() throws WxPayException {
log.info(result.toString());
}
+ @Test
+ public void getUserAuthorizationStatus() throws WxPayException {
+ log.info("查询用户授权信息:{}",
+ wxPayService.getMerchantTransferService().getUserAuthorizationStatus("or1b65DLMUir7F-_vLwKlutmm3qw", "1005"));
+ }
+
+ @Test
+ public void reservationTransferBatch() throws WxPayException {
+ String requestParamStr = "{\n"
+ + " \"appid\": \"wxf636efh5xxxxx\",\n"
+ + " \"out_batch_no\": \"RESERVATION_1655447999520\",\n"
+ + " \"batch_name\": \"预约测试批次\",\n"
+ + " \"batch_remark\": \"预约测试批次备注\",\n"
+ + " \"total_amount\": 100,\n"
+ + " \"total_num\": 1,\n"
+ + " \"transfer_scene_id\": \"1005\",\n"
+ + " \"transfer_detail_list\": [\n"
+ + " {\n"
+ + " \"out_detail_no\": \"RESERVATION_DETAIL_1655447989156\",\n"
+ + " \"transfer_amount\": 100,\n"
+ + " \"transfer_remark\": \"预约测试转账\",\n"
+ + " \"openid\": \"or1b65DLMUir7F-_vLwKlutmm3qw\"\n"
+ + " }\n"
+ + " ]\n"
+ + "}";
+ ReservationTransferBatchRequest request = GSON.fromJson(requestParamStr, ReservationTransferBatchRequest.class);
+ log.info("发起预约商家转账:{}",
+ wxPayService.getMerchantTransferService().reservationTransferBatch(request));
+ }
+
+ @Test
+ public void getReservationTransferBatchByOutBatchNo() throws WxPayException {
+ log.info("商户预约批次单号查询批次单:{}",
+ wxPayService.getMerchantTransferService().getReservationTransferBatchByOutBatchNo("RESERVATION_1655447999520",
+ true, 0, 20, "PROCESSING"));
+ }
+
+ @Test
+ public void getReservationTransferBatchByReservationBatchNo() throws WxPayException {
+ log.info("微信预约批次单号查询批次单:{}",
+ wxPayService.getMerchantTransferService().getReservationTransferBatchByReservationBatchNo("12345678901234567890123456789012",
+ true, 0, 20, "PROCESSING"));
+ }
+
+ @Test
+ public void closeReservationTransferBatch() throws WxPayException {
+ wxPayService.getMerchantTransferService().closeReservationTransferBatch("RESERVATION_1655447999520");
+ }
+
}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MiPayServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MiPayServiceImplTest.java
new file mode 100644
index 0000000000..095d355bd4
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MiPayServiceImplTest.java
@@ -0,0 +1,147 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.mipay.MedInsOrdersRequest;
+import com.github.binarywang.wxpay.bean.mipay.MedInsOrdersResult;
+import com.github.binarywang.wxpay.bean.mipay.MedInsRefundNotifyRequest;
+import com.github.binarywang.wxpay.bean.notify.MiPayNotifyV3Result;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.MiPayService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.testbase.ApiTestModule;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.inject.Inject;
+import lombok.extern.slf4j.Slf4j;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+/**
+ * 医保接口测试
+ * @author xgl
+ * @date 2025/12/20 10:04
+ */
+@Slf4j
+@Test
+@Guice(modules = ApiTestModule.class)
+public class MiPayServiceImplTest {
+
+ @Inject
+ private WxPayService wxPayService;
+
+ private static final Gson GSON = new GsonBuilder().create();
+
+
+ /**
+ * 医保自费混合收款下单测试
+ * @throws WxPayException
+ */
+ @Test
+ public void medInsOrders() throws WxPayException {
+ String requestParamStr = "{\"mix_pay_type\":\"CASH_AND_INSURANCE\",\"order_type\":\"REG_PAY\",\"appid\":\"wxdace645e0bc2cXXX\",\"sub_appid\":\"wxdace645e0bc2cXXX\",\"sub_mchid\":\"1900008XXX\",\"openid\":\"o4GgauInH_RCEdvrrNGrntXDuXXX\",\"sub_openid\":\"o4GgauInH_RCEdvrrNGrntXDuXXX\",\"payer\":{\"name\":\"张三\",\"id_digest\":\"09eb26e839ff3a2e3980352ae45ef09e\",\"card_type\":\"ID_CARD\"},\"pay_for_relatives\":false,\"relative\":{\"name\":\"张三\",\"id_digest\":\"09eb26e839ff3a2e3980352ae45ef09e\",\"card_type\":\"ID_CARD\"},\"out_trade_no\":\"202204022005169952975171534816\",\"serial_no\":\"1217752501201\",\"pay_order_id\":\"ORD530100202204022006350000021\",\"pay_auth_no\":\"AUTH530100202204022006310000034\",\"geo_location\":\"102.682296,25.054260\",\"city_id\":\"530100\",\"med_inst_name\":\"北大医院\",\"med_inst_no\":\"1217752501201407033233368318\",\"med_ins_order_create_time\":\"2015-05-20T13:29:35+08:00\",\"total_fee\":202000,\"med_ins_gov_fee\":100000,\"med_ins_self_fee\":45000,\"med_ins_other_fee\":5000,\"med_ins_cash_fee\":50000,\"wechat_pay_cash_fee\":42000,\"cash_add_detail\":[{\"cash_add_fee\":2000,\"cash_add_type\":\"FREIGHT\"}],\"cash_reduce_detail\":[{\"cash_reduce_fee\":10000,\"cash_reduce_type\":\"DEFAULT_REDUCE_TYPE\"}],\"callback_url\":\"https://www.weixin.qq.com/wxpay/pay.php\",\"prepay_id\":\"wx201410272009395522657a690389285100\",\"passthrough_request_content\":\"{\\\"payAuthNo\\\":\\\"AUTH****\\\",\\\"payOrdId\\\":\\\"ORD****\\\",\\\"setlLatlnt\\\":\\\"118.096435,24.485407\\\"}\",\"extends\":\"{}\",\"attach\":\"{}\",\"channel_no\":\"AAGN9uhZc5EGyRdairKW7Qnu\",\"med_ins_test_env\":false}";
+
+ MedInsOrdersRequest request = GSON.fromJson(requestParamStr, MedInsOrdersRequest.class);
+
+ MiPayService miPayService = wxPayService.getMiPayService();
+
+ MedInsOrdersResult result = miPayService.medInsOrders(request);
+
+ log.info(result.toString());
+ }
+
+ /**
+ * 使用医保自费混合订单号查看下单结果测试
+ * @throws WxPayException
+ */
+ @Test
+ public void getMedInsOrderByMixTradeNo() throws WxPayException {
+ // 测试用的医保自费混合订单号和医疗机构商户号
+ String mixTradeNo = "202204022005169952975171534816";
+ String subMchid = "1900000109";
+
+ MiPayService miPayService = wxPayService.getMiPayService();
+
+ MedInsOrdersResult result = miPayService.getMedInsOrderByMixTradeNo(mixTradeNo, subMchid);
+
+ log.info(result.toString());
+ }
+
+ /**
+ * 使用从业机构订单号查看下单结果测试
+ * @throws WxPayException
+ */
+ @Test
+ public void getMedInsOrderByOutTradeNo() throws WxPayException {
+ // 测试用的从业机构订单号和医疗机构商户号
+ String outTradeNo = "202204022005169952975171534816";
+ String subMchid = "1900000109";
+
+ MiPayService miPayService = wxPayService.getMiPayService();
+
+ MedInsOrdersResult result = miPayService.getMedInsOrderByOutTradeNo(outTradeNo, subMchid);
+
+ log.info(result.toString());
+ }
+
+ /**
+ * 解析医保混合收款成功通知测试
+ * @throws WxPayException
+ */
+ @Test
+ public void parseMiPayNotifyV3Result() throws WxPayException {
+ // 模拟的医保混合收款成功通知数据
+ String notifyData = "{\"id\":\"EV-202401011234567890\",\"create_time\":\"2024-01-01T12:34:56+08:00\",\"event_type\":\"MEDICAL_INSURANCE.SUCCESS\",\"summary\":\"医保混合收款成功\",\"resource_type\":\"encrypt-resource\",\"resource\":{\"algorithm\":\"AEAD_AES_256_GCM\",\"ciphertext\":\"encrypted_data\",\"associated_data\":\"\",\"nonce\":\"random_string\"}}";
+
+ // 模拟的签名信息
+ String signature = "test_signature";
+ String timestamp = "1234567890";
+ String nonce = "test_nonce";
+ String serial = "test_serial";
+
+ MiPayService miPayService = wxPayService.getMiPayService();
+
+ SignatureHeader header = SignatureHeader.builder()
+ .signature(signature)
+ .timeStamp(timestamp)
+ .nonce(nonce)
+ .serial(serial)
+ .build();
+
+ try {
+ // 调用解析方法,预期会失败,因为是模拟数据
+ MiPayNotifyV3Result result = miPayService.parseMiPayNotifyV3Result(notifyData, header);
+ log.info("解析结果:{}", result);
+ } catch (WxPayException e) {
+ // 预期会抛出异常,因为是模拟数据,签名验证和解密都会失败
+ log.info("预期的异常:{}", e.getMessage());
+ }
+ }
+
+ /**
+ * 医保退款通知测试
+ * @throws WxPayException
+ */
+ @Test
+ public void medInsRefundNotify() throws WxPayException {
+ // 测试用的医保自费混合订单号
+ String mixTradeNo = "202204022005169952975171534816";
+
+ // 模拟的医保退款通知请求数据
+ String requestParamStr = "{\"sub_mchid\":\"1900008XXX\",\"med_refund_total_fee\":45000,\"med_refund_gov_fee\":45000,\"med_refund_self_fee\":45000,\"med_refund_other_fee\":45000,\"refund_time\":\"2015-05-20T13:29:35+08:00\",\"out_refund_no\":\"R202204022005169952975171534816\"}";
+
+ // 解析请求参数
+ MedInsRefundNotifyRequest request = GSON.fromJson(requestParamStr, MedInsRefundNotifyRequest.class);
+
+ MiPayService miPayService = wxPayService.getMiPayService();
+
+ try {
+ // 调用医保退款通知方法,预期会失败,因为是模拟数据
+ miPayService.medInsRefundNotify(request,mixTradeNo);
+ log.info("医保退款通知调用成功");
+ } catch (WxPayException e) {
+ // 预期会抛出异常,因为是模拟数据
+ log.info("预期的异常:{}", e.getMessage());
+ }
+ }
+
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverManualTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverManualTest.java
new file mode 100644
index 0000000000..010f15fc69
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverManualTest.java
@@ -0,0 +1,127 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.service.WxPayService;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 手动验证多appId切换功能
+ */
+public class MultiAppIdSwitchoverManualTest {
+
+ public static void main(String[] args) {
+ WxPayService payService = new WxPayServiceImpl();
+
+ String testMchId = "1234567890";
+ String testAppId1 = "wx1111111111111111";
+ String testAppId2 = "wx2222222222222222";
+ String testAppId3 = "wx3333333333333333";
+
+ // 配置同一个商户号,三个不同的appId
+ WxPayConfig config1 = new WxPayConfig();
+ config1.setMchId(testMchId);
+ config1.setAppId(testAppId1);
+ config1.setMchKey("test_key_1");
+
+ WxPayConfig config2 = new WxPayConfig();
+ config2.setMchId(testMchId);
+ config2.setAppId(testAppId2);
+ config2.setMchKey("test_key_2");
+
+ WxPayConfig config3 = new WxPayConfig();
+ config3.setMchId(testMchId);
+ config3.setAppId(testAppId3);
+ config3.setMchKey("test_key_3");
+
+ Map configMap = new HashMap<>();
+ configMap.put(testMchId + "_" + testAppId1, config1);
+ configMap.put(testMchId + "_" + testAppId2, config2);
+ configMap.put(testMchId + "_" + testAppId3, config3);
+
+ payService.setMultiConfig(configMap);
+
+ // 测试1: 使用 mchId + appId 精确切换
+ System.out.println("=== 测试1: 使用 mchId + appId 精确切换 ===");
+ boolean success = payService.switchover(testMchId, testAppId1);
+ System.out.println("切换结果: " + success);
+ System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey());
+ verify(success, "切换应该成功");
+ verify(testAppId1.equals(payService.getConfig().getAppId()), "AppId应该是 " + testAppId1);
+ System.out.println("✓ 测试1通过\n");
+
+ // 测试2: 仅使用 mchId 切换
+ System.out.println("=== 测试2: 仅使用 mchId 切换 ===");
+ success = payService.switchover(testMchId);
+ System.out.println("切换结果: " + success);
+ System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey());
+ verify(success, "仅使用mchId切换应该成功");
+ verify(testMchId.equals(payService.getConfig().getMchId()), "MchId应该是 " + testMchId);
+ System.out.println("✓ 测试2通过\n");
+
+ // 测试3: 使用 switchoverTo 链式调用(精确匹配)
+ System.out.println("=== 测试3: 使用 switchoverTo 链式调用(精确匹配) ===");
+ WxPayService result = payService.switchoverTo(testMchId, testAppId2);
+ System.out.println("返回对象: " + (result == payService ? "同一实例" : "不同实例"));
+ System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey());
+ verify(result == payService, "应该返回同一实例");
+ verify(testAppId2.equals(payService.getConfig().getAppId()), "AppId应该是 " + testAppId2);
+ System.out.println("✓ 测试3通过\n");
+
+ // 测试4: 使用 switchoverTo 链式调用(仅mchId)
+ System.out.println("=== 测试4: 使用 switchoverTo 链式调用(仅mchId) ===");
+ result = payService.switchoverTo(testMchId);
+ System.out.println("返回对象: " + (result == payService ? "同一实例" : "不同实例"));
+ System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey());
+ verify(result == payService, "应该返回同一实例");
+ verify(testMchId.equals(payService.getConfig().getMchId()), "MchId应该是 " + testMchId);
+ System.out.println("✓ 测试4通过\n");
+
+ // 测试5: 切换到不存在的商户号
+ System.out.println("=== 测试5: 切换到不存在的商户号 ===");
+ success = payService.switchover("nonexistent_mch_id");
+ System.out.println("切换结果: " + success);
+ verify(!success, "切换到不存在的商户号应该失败");
+ System.out.println("✓ 测试5通过\n");
+
+ // 测试6: 切换到不存在的 appId
+ System.out.println("=== 测试6: 切换到不存在的 appId ===");
+ success = payService.switchover(testMchId, "wx9999999999999999");
+ System.out.println("切换结果: " + success);
+ verify(!success, "切换到不存在的appId应该失败");
+ System.out.println("✓ 测试6通过\n");
+
+ // 测试7: 添加新配置后切换
+ System.out.println("=== 测试7: 添加新配置后切换 ===");
+ String newAppId = "wx4444444444444444";
+ WxPayConfig newConfig = new WxPayConfig();
+ newConfig.setMchId(testMchId);
+ newConfig.setAppId(newAppId);
+ newConfig.setMchKey("test_key_4");
+ payService.addConfig(testMchId, newAppId, newConfig);
+
+ success = payService.switchover(testMchId, newAppId);
+ System.out.println("切换结果: " + success);
+ System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey());
+ verify(success, "切换到新添加的配置应该成功");
+ verify(newAppId.equals(payService.getConfig().getAppId()), "AppId应该是 " + newAppId);
+ System.out.println("✓ 测试7通过\n");
+
+ System.out.println("==================");
+ System.out.println("所有测试通过! ✓");
+ System.out.println("==================");
+ }
+
+ /**
+ * 验证条件是否为真,如果为假则抛出异常
+ *
+ * @param condition 待验证的条件
+ * @param message 验证失败时的错误信息
+ */
+ private static void verify(boolean condition, String message) {
+ if (!condition) {
+ throw new RuntimeException("验证失败: " + message);
+ }
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverTest.java
new file mode 100644
index 0000000000..fe2360fba4
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverTest.java
@@ -0,0 +1,546 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.service.WxPayService;
+import me.chanjar.weixin.common.error.WxRuntimeException;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.testng.Assert.*;
+
+/**
+ * 测试一个商户号配置多个appId的场景
+ *
+ * @author Binary Wang
+ */
+public class MultiAppIdSwitchoverTest {
+
+ private WxPayService payService;
+ private final String testMchId = "1234567890";
+ private final String testAppId1 = "wx1111111111111111";
+ private final String testAppId2 = "wx2222222222222222";
+ private final String testAppId3 = "wx3333333333333333";
+
+ @BeforeMethod
+ public void setup() {
+ payService = new WxPayServiceImpl();
+
+ // 配置同一个商户号,三个不同的appId
+ WxPayConfig config1 = new WxPayConfig();
+ config1.setMchId(testMchId);
+ config1.setAppId(testAppId1);
+ config1.setMchKey("test_key_1");
+
+ WxPayConfig config2 = new WxPayConfig();
+ config2.setMchId(testMchId);
+ config2.setAppId(testAppId2);
+ config2.setMchKey("test_key_2");
+
+ WxPayConfig config3 = new WxPayConfig();
+ config3.setMchId(testMchId);
+ config3.setAppId(testAppId3);
+ config3.setMchKey("test_key_3");
+
+ Map configMap = new HashMap<>();
+ configMap.put(testMchId + "_" + testAppId1, config1);
+ configMap.put(testMchId + "_" + testAppId2, config2);
+ configMap.put(testMchId + "_" + testAppId3, config3);
+
+ payService.setMultiConfig(configMap);
+ }
+
+ /**
+ * 测试直接通过 mchId 和 appId 获取配置(新功能)
+ */
+ @Test
+ public void testGetConfigWithMchIdAndAppId() {
+ // 测试获取第一个配置
+ WxPayConfig config1 = payService.getConfig(testMchId, testAppId1);
+ assertNotNull(config1, "应该能够获取到配置");
+ assertEquals(config1.getMchId(), testMchId);
+ assertEquals(config1.getAppId(), testAppId1);
+ assertEquals(config1.getMchKey(), "test_key_1");
+
+ // 测试获取第二个配置
+ WxPayConfig config2 = payService.getConfig(testMchId, testAppId2);
+ assertNotNull(config2);
+ assertEquals(config2.getAppId(), testAppId2);
+ assertEquals(config2.getMchKey(), "test_key_2");
+
+ // 测试获取第三个配置
+ WxPayConfig config3 = payService.getConfig(testMchId, testAppId3);
+ assertNotNull(config3);
+ assertEquals(config3.getAppId(), testAppId3);
+ assertEquals(config3.getMchKey(), "test_key_3");
+ }
+
+ /**
+ * 测试直接通过 mchId 获取配置(新功能)
+ */
+ @Test
+ public void testGetConfigWithMchIdOnly() {
+ WxPayConfig config = payService.getConfig(testMchId);
+ assertNotNull(config, "应该能够通过mchId获取配置");
+ assertEquals(config.getMchId(), testMchId);
+
+ // appId应该是三个中的一个
+ String currentAppId = config.getAppId();
+ assertTrue(
+ testAppId1.equals(currentAppId) || testAppId2.equals(currentAppId) || testAppId3.equals(currentAppId),
+ "获取的配置的appId应该是配置的appId之一"
+ );
+ }
+
+ /**
+ * 测试 getConfig 方法不依赖 ThreadLocal
+ * 在不切换配置的情况下也能直接获取
+ */
+ @Test
+ public void testGetConfigWithoutSwitchover() {
+ // 不进行任何switchover操作,直接通过参数获取配置
+ WxPayConfig config1 = payService.getConfig(testMchId, testAppId1);
+ WxPayConfig config2 = payService.getConfig(testMchId, testAppId2);
+ WxPayConfig config3 = payService.getConfig(testMchId, testAppId3);
+
+ // 验证可以同时获取到所有配置,不受 ThreadLocal 影响
+ assertNotNull(config1);
+ assertNotNull(config2);
+ assertNotNull(config3);
+
+ assertEquals(config1.getAppId(), testAppId1);
+ assertEquals(config2.getAppId(), testAppId2);
+ assertEquals(config3.getAppId(), testAppId3);
+ }
+
+ /**
+ * 测试 getConfig 方法处理不存在的配置
+ */
+ @Test
+ public void testGetConfigWithNonexistentConfig() {
+ // 测试不存在的商户号和appId组合
+ WxPayConfig config = payService.getConfig("nonexistent_mch_id", "nonexistent_app_id");
+ assertNull(config, "获取不存在的配置应该返回null");
+
+ // 测试存在商户号但不存在的appId
+ config = payService.getConfig(testMchId, "wx9999999999999999");
+ assertNull(config, "获取不存在的appId配置应该返回null");
+ }
+
+ /**
+ * 测试 getConfig 方法处理空参数或null参数
+ */
+ @Test
+ public void testGetConfigWithNullOrEmptyParameters() {
+ // 测试 null 商户号
+ WxPayConfig config = payService.getConfig(null, testAppId1);
+ assertNull(config, "商户号为null时应该返回null");
+
+ // 测试空商户号
+ config = payService.getConfig("", testAppId1);
+ assertNull(config, "商户号为空字符串时应该返回null");
+
+ // 测试 null appId
+ config = payService.getConfig(testMchId, null);
+ assertNull(config, "appId为null时应该返回null");
+
+ // 测试空 appId
+ config = payService.getConfig(testMchId, "");
+ assertNull(config, "appId为空字符串时应该返回null");
+
+ // 测试仅mchId方法的null参数
+ config = payService.getConfig((String) null);
+ assertNull(config, "商户号为null时应该返回null");
+
+ // 测试仅mchId方法的空字符串
+ config = payService.getConfig("");
+ assertNull(config, "商户号为空字符串时应该返回null");
+ }
+
+ /**
+ * 测试使用 mchId + appId 精确切换(原有功能,确保向后兼容)
+ */
+ @Test
+ public void testSwitchoverWithMchIdAndAppId() {
+ // 切换到第一个配置
+ boolean success = payService.switchover(testMchId, testAppId1);
+ assertTrue(success);
+ assertEquals(payService.getConfig().getAppId(), testAppId1);
+ assertEquals(payService.getConfig().getMchKey(), "test_key_1");
+
+ // 切换到第二个配置
+ success = payService.switchover(testMchId, testAppId2);
+ assertTrue(success);
+ assertEquals(payService.getConfig().getAppId(), testAppId2);
+ assertEquals(payService.getConfig().getMchKey(), "test_key_2");
+
+ // 切换到第三个配置
+ success = payService.switchover(testMchId, testAppId3);
+ assertTrue(success);
+ assertEquals(payService.getConfig().getAppId(), testAppId3);
+ assertEquals(payService.getConfig().getMchKey(), "test_key_3");
+ }
+
+ /**
+ * 测试仅使用 mchId 切换(新功能)
+ * 应该能够成功切换到该商户号的某个配置
+ */
+ @Test
+ public void testSwitchoverWithMchIdOnly() {
+ // 仅使用商户号切换,应该能够成功切换到该商户号的某个配置
+ boolean success = payService.switchover(testMchId);
+ assertTrue(success, "应该能够通过mchId切换配置");
+
+ // 验证配置确实是该商户号的配置之一
+ WxPayConfig currentConfig = payService.getConfig();
+ assertNotNull(currentConfig);
+ assertEquals(currentConfig.getMchId(), testMchId);
+
+ // appId应该是三个中的一个
+ String currentAppId = currentConfig.getAppId();
+ assertTrue(
+ testAppId1.equals(currentAppId) || testAppId2.equals(currentAppId) || testAppId3.equals(currentAppId),
+ "当前appId应该是配置的appId之一"
+ );
+ }
+
+ /**
+ * 测试 switchoverTo 方法(带链式调用,使用 mchId + appId)
+ */
+ @Test
+ public void testSwitchoverToWithMchIdAndAppId() {
+ WxPayService result = payService.switchoverTo(testMchId, testAppId2);
+ assertNotNull(result);
+ assertEquals(result, payService, "switchoverTo应该返回当前服务实例,支持链式调用");
+ assertEquals(payService.getConfig().getAppId(), testAppId2);
+ }
+
+ /**
+ * 测试 switchoverTo 方法(带链式调用,仅使用 mchId)
+ */
+ @Test
+ public void testSwitchoverToWithMchIdOnly() {
+ WxPayService result = payService.switchoverTo(testMchId);
+ assertNotNull(result);
+ assertEquals(result, payService, "switchoverTo应该返回当前服务实例,支持链式调用");
+ assertEquals(payService.getConfig().getMchId(), testMchId);
+ }
+
+ /**
+ * 测试切换到不存在的商户号
+ */
+ @Test
+ public void testSwitchoverToNonexistentMchId() {
+ boolean success = payService.switchover("nonexistent_mch_id");
+ assertFalse(success, "切换到不存在的商户号应该失败");
+ }
+
+ /**
+ * 测试 switchoverTo 切换到不存在的商户号(应该抛出异常)
+ */
+ @Test(expectedExceptions = WxRuntimeException.class)
+ public void testSwitchoverToNonexistentMchIdThrowsException() {
+ payService.switchoverTo("nonexistent_mch_id");
+ }
+
+ /**
+ * 测试切换到不存在的 mchId + appId 组合
+ */
+ @Test
+ public void testSwitchoverToNonexistentAppId() {
+ boolean success = payService.switchover(testMchId, "wx9999999999999999");
+ assertFalse(success, "切换到不存在的appId应该失败");
+ }
+
+ /**
+ * 测试添加配置后能够正常切换
+ */
+ @Test
+ public void testAddConfigAndSwitchover() {
+ String newAppId = "wx4444444444444444";
+
+ // 动态添加一个新的配置
+ WxPayConfig newConfig = new WxPayConfig();
+ newConfig.setMchId(testMchId);
+ newConfig.setAppId(newAppId);
+ newConfig.setMchKey("test_key_4");
+
+ payService.addConfig(testMchId, newAppId, newConfig);
+
+ // 切换到新添加的配置
+ boolean success = payService.switchover(testMchId, newAppId);
+ assertTrue(success);
+ assertEquals(payService.getConfig().getAppId(), newAppId);
+ assertEquals(payService.getConfig().getMchKey(), "test_key_4");
+
+ // 使用仅mchId切换也应该能够找到配置
+ success = payService.switchover(testMchId);
+ assertTrue(success);
+ assertEquals(payService.getConfig().getMchId(), testMchId);
+ }
+
+ /**
+ * 测试移除配置后切换
+ */
+ @Test
+ public void testRemoveConfigAndSwitchover() {
+ // 移除一个配置
+ payService.removeConfig(testMchId, testAppId1);
+
+ // 切换到已移除的配置应该失败
+ boolean success = payService.switchover(testMchId, testAppId1);
+ assertFalse(success);
+
+ // 但仍然能够切换到其他配置
+ success = payService.switchover(testMchId, testAppId2);
+ assertTrue(success);
+
+ // 使用仅mchId切换应该仍然有效(因为还有其他appId的配置)
+ success = payService.switchover(testMchId);
+ assertTrue(success);
+ }
+
+ /**
+ * 测试单个配置的场景(确保向后兼容)
+ */
+ @Test
+ public void testSingleConfig() {
+ WxPayService singlePayService = new WxPayServiceImpl();
+ WxPayConfig singleConfig = new WxPayConfig();
+ singleConfig.setMchId("single_mch_id");
+ singleConfig.setAppId("single_app_id");
+ singleConfig.setMchKey("single_key");
+
+ singlePayService.setConfig(singleConfig);
+
+ // 直接获取配置应该成功
+ assertEquals(singlePayService.getConfig().getMchId(), "single_mch_id");
+ assertEquals(singlePayService.getConfig().getAppId(), "single_app_id");
+
+ // 使用精确匹配切换
+ boolean success = singlePayService.switchover("single_mch_id", "single_app_id");
+ assertTrue(success);
+
+ // 使用仅mchId切换
+ success = singlePayService.switchover("single_mch_id");
+ assertTrue(success);
+ }
+
+ /**
+ * 测试空参数或null参数的处理
+ */
+ @Test
+ public void testSwitchoverWithNullOrEmptyMchId() {
+ // 测试 null 参数
+ boolean success = payService.switchover(null);
+ assertFalse(success, "使用null作为mchId应该返回false");
+
+ // 测试空字符串
+ success = payService.switchover("");
+ assertFalse(success, "使用空字符串作为mchId应该返回false");
+
+ // 测试空白字符串
+ success = payService.switchover(" ");
+ assertFalse(success, "使用空白字符串作为mchId应该返回false");
+ }
+
+ /**
+ * 测试 switchoverTo 方法对空参数或null参数的处理
+ */
+ @Test(expectedExceptions = WxRuntimeException.class)
+ public void testSwitchoverToWithNullMchId() {
+ payService.switchoverTo((String) null);
+ }
+
+ @Test(expectedExceptions = WxRuntimeException.class)
+ public void testSwitchoverToWithEmptyMchId() {
+ payService.switchoverTo("");
+ }
+
+ @Test(expectedExceptions = WxRuntimeException.class)
+ public void testSwitchoverToWithBlankMchId() {
+ payService.switchoverTo(" ");
+ }
+
+ /**
+ * 测试商户号存在包含关系的场景
+ * 例如同时配置 "123" 和 "1234",验证前缀匹配不会错误匹配
+ */
+ @Test
+ public void testSwitchoverWithOverlappingMchIds() {
+ WxPayService testService = new WxPayServiceImpl();
+
+ // 配置两个有包含关系的商户号
+ String mchId1 = "123";
+ String mchId2 = "1234";
+ String appId1 = "wx_app_123";
+ String appId2 = "wx_app_1234";
+
+ WxPayConfig config1 = new WxPayConfig();
+ config1.setMchId(mchId1);
+ config1.setAppId(appId1);
+ config1.setMchKey("key_123");
+
+ WxPayConfig config2 = new WxPayConfig();
+ config2.setMchId(mchId2);
+ config2.setAppId(appId2);
+ config2.setMchKey("key_1234");
+
+ Map configMap = new HashMap<>();
+ configMap.put(mchId1 + "_" + appId1, config1);
+ configMap.put(mchId2 + "_" + appId2, config2);
+ testService.setMultiConfig(configMap);
+
+ // 切换到 "123",应该只匹配 "123_wx_app_123"
+ boolean success = testService.switchover(mchId1);
+ assertTrue(success);
+ assertEquals(testService.getConfig().getMchId(), mchId1);
+ assertEquals(testService.getConfig().getAppId(), appId1);
+
+ // 切换到 "1234",应该只匹配 "1234_wx_app_1234"
+ success = testService.switchover(mchId2);
+ assertTrue(success);
+ assertEquals(testService.getConfig().getMchId(), mchId2);
+ assertEquals(testService.getConfig().getAppId(), appId2);
+
+ // 精确切换验证
+ success = testService.switchover(mchId1, appId1);
+ assertTrue(success);
+ assertEquals(testService.getConfig().getAppId(), appId1);
+
+ success = testService.switchover(mchId2, appId2);
+ assertTrue(success);
+ assertEquals(testService.getConfig().getAppId(), appId2);
+ }
+
+ /**
+ * 测试使用自定义唯一键(非mchId格式)添加配置并切换.
+ * 验证向后兼容性:支持使用任意唯一标识符(如租户ID)管理配置
+ */
+ @Test
+ public void testAddConfigWithCustomKey() {
+ WxPayService testService = new WxPayServiceImpl();
+
+ String customKey1 = "tenant_001";
+ String customKey2 = "tenant_002";
+
+ WxPayConfig config1 = new WxPayConfig();
+ config1.setMchId("mch001");
+ config1.setAppId("wxabc");
+ config1.setMchKey("key_tenant_001");
+
+ WxPayConfig config2 = new WxPayConfig();
+ config2.setMchId("mch002");
+ config2.setAppId("wxdef");
+ config2.setMchKey("key_tenant_002");
+
+ // 使用自定义键添加配置
+ testService.addConfig(customKey1, config1);
+ testService.addConfig(customKey2, config2);
+
+ // 使用自定义键切换配置
+ boolean success = testService.switchover(customKey1);
+ assertTrue(success, "应该能够使用自定义键切换配置");
+ assertEquals(testService.getConfig().getMchKey(), "key_tenant_001");
+
+ success = testService.switchover(customKey2);
+ assertTrue(success, "应该能够切换到第二个自定义键配置");
+ assertEquals(testService.getConfig().getMchKey(), "key_tenant_002");
+ }
+
+ /**
+ * 测试使用自定义唯一键删除配置.
+ */
+ @Test
+ public void testRemoveConfigWithCustomKey() {
+ WxPayService testService = new WxPayServiceImpl();
+
+ String customKey1 = "tenant_A";
+ String customKey2 = "tenant_B";
+
+ WxPayConfig config1 = new WxPayConfig();
+ config1.setMchId("mchA");
+ config1.setAppId("wxA");
+ config1.setMchKey("key_A");
+
+ WxPayConfig config2 = new WxPayConfig();
+ config2.setMchId("mchB");
+ config2.setAppId("wxB");
+ config2.setMchKey("key_B");
+
+ Map configMap = new HashMap<>();
+ configMap.put(customKey1, config1);
+ configMap.put(customKey2, config2);
+ testService.setMultiConfig(configMap);
+
+ // 删除第一个自定义键配置
+ testService.removeConfig(customKey1);
+
+ // 尝试切换到已删除的配置应该失败
+ boolean success = testService.switchover(customKey1);
+ assertFalse(success, "切换到已删除的配置应该失败");
+
+ // 但仍然能够切换到第二个配置
+ success = testService.switchover(customKey2);
+ assertTrue(success, "切换到未删除的配置应该成功");
+ assertEquals(testService.getConfig().getMchKey(), "key_B");
+ }
+
+ /**
+ * 测试 switchover(mchId, appId) 当 appId 为 null 时降级为 switchover(mchId).
+ * 模拟通知回调中 appId 可能为空的场景
+ */
+ @Test
+ public void testSwitchoverWithNullAppIdFallsBackToMchId() {
+ // 切换到 appId 为 null 时,应该降级为只使用 mchId 匹配
+ boolean success = payService.switchover(testMchId, null);
+ assertTrue(success, "appId为null时应该降级为仅mchId匹配");
+ assertEquals(payService.getConfig().getMchId(), testMchId);
+
+ // appId 为空字符串时同样应该降级
+ success = payService.switchover(testMchId, "");
+ assertTrue(success, "appId为空字符串时应该降级为仅mchId匹配");
+ assertEquals(payService.getConfig().getMchId(), testMchId);
+ }
+
+ /**
+ * 测试 switchoverTo(mchId, appId) 当 appId 为 null 时降级为 switchoverTo(mchId).
+ */
+ @Test
+ public void testSwitchoverToWithNullAppIdFallsBackToMchId() {
+ WxPayService result = payService.switchoverTo(testMchId, null);
+ assertNotNull(result);
+ assertEquals(result, payService);
+ assertEquals(payService.getConfig().getMchId(), testMchId);
+ }
+
+ /**
+ * 测试使用自定义键通过 setMultiConfig 注册后可以直接 switchover.
+ */
+ @Test
+ public void testSwitchoverWithCustomKeyViaSetMultiConfig() {
+ WxPayService testService = new WxPayServiceImpl();
+
+ String tenantId = "my-unique-tenant-id";
+ WxPayConfig config = new WxPayConfig();
+ config.setMchId("mchTenant");
+ config.setAppId("wxTenant");
+ config.setMchKey("key_tenant");
+
+ Map configMap = new HashMap<>();
+ configMap.put(tenantId, config);
+ testService.setMultiConfig(configMap);
+
+ // 使用自定义租户ID切换
+ boolean success = testService.switchover(tenantId);
+ assertTrue(success, "应该能够使用自定义租户ID切换配置");
+ assertEquals(testService.getConfig().getMchKey(), "key_tenant");
+
+ // switchoverTo 链式调用也应该支持
+ WxPayService result = testService.switchoverTo(tenantId);
+ assertNotNull(result);
+ assertEquals(result, testService);
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImplTest.java
index 03bbc8c593..a5421f5dc9 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImplTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImplTest.java
@@ -14,6 +14,8 @@
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
+import java.util.Collections;
+
/**
* 微工卡(服务商)
*
@@ -112,6 +114,7 @@ public void payrollCardPreOrderWithAuth() throws WxPayException {
request.setIdCardNumber("7FzH5XksJG3a8HLLsaaUV6K54y1OnPMY5");
request.setProjectName("某项目");
request.setUserName("LP7bT4hQXUsOZCEvK2YrSiqFsnP0oRMfeoLN0vBg");
+ request.setAuthenticateType("NORMAL_AUTHENTICATE");
PreOrderWithAuthResult preOrderWithAuthResult = wxPayService.getPayrollService().payrollCardPreOrderWithAuth(request);
log.info(preOrderWithAuthResult.toString());
@@ -125,4 +128,33 @@ public void merchantFundWithdrawBillType() throws WxPayException {
log.info(result.toString());
}
+ @Test
+ public void payrollCardTransferBatches() throws WxPayException {
+ PayrollTransferBatchesRequest request = PayrollTransferBatchesRequest.builder()
+ .appid("wxa1111111")
+ .subMchid("1111111")
+ .subAppid("wxa1111111")
+ .outBatchNo("plfk2020042013" + System.currentTimeMillis())
+ .batchName("2019年1月深圳分部报销单")
+ .batchRemark("2019年1月深圳分部报销单")
+ .totalAmount(200000L)
+ .totalNum(1)
+ .employmentType("LONG_TERM_EMPLOYMENT")
+ .employmentScene("LOGISTICS")
+ .authorizationType("INFORMATION_AUTHORIZATION_TYPE")
+ .transferDetailList(Collections.singletonList(
+ PayrollTransferBatchesRequest.TransferDetail.builder()
+ .outDetailNo("x23zy545Bd5436" + System.currentTimeMillis())
+ .transferAmount(200000L)
+ .transferRemark("2020年4月报销")
+ .openid("o-MYE42l80oelYMDE34nYD456Xoy")
+ .userName("张三")
+ .userIdCard("8609cb22e1774a50a930e414cc71eca06121bcd266335cda230d24a7886a8d9f")
+ .build()
+ ))
+ .build();
+ PayrollTransferBatchesResult result = wxPayService.getPayrollService().payrollCardTransferBatches(request);
+ log.info(result.toString());
+ }
+
}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/RealNameServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/RealNameServiceImplTest.java
new file mode 100644
index 0000000000..dda2371948
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/RealNameServiceImplTest.java
@@ -0,0 +1,54 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.realname.RealNameRequest;
+import com.github.binarywang.wxpay.bean.realname.RealNameResult;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.testbase.ApiTestModule;
+import com.google.inject.Inject;
+import lombok.extern.slf4j.Slf4j;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+/**
+ *
+ * 实名验证测试类.
+ *
+ *
+ * @author Binary Wang
+ */
+@Test
+@Guice(modules = ApiTestModule.class)
+@Slf4j
+public class RealNameServiceImplTest {
+
+ @Inject
+ private WxPayService payService;
+
+ /**
+ * 测试查询用户实名认证信息.
+ *
+ * @throws WxPayException the wx pay exception
+ */
+ @Test
+ public void testQueryRealName() throws WxPayException {
+ RealNameRequest request = RealNameRequest.newBuilder()
+ .openid("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o")
+ .build();
+
+ RealNameResult result = this.payService.getRealNameService().queryRealName(request);
+ log.info("实名认证查询结果:{}", result);
+ }
+
+ /**
+ * 测试查询用户实名认证信息(简化方法).
+ *
+ * @throws WxPayException the wx pay exception
+ */
+ @Test
+ public void testQueryRealNameSimple() throws WxPayException {
+ RealNameResult result = this.payService.getRealNameService()
+ .queryRealName("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o");
+ log.info("实名认证查询结果:{}", result);
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/SubscriptionBillingServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/SubscriptionBillingServiceImplTest.java
new file mode 100644
index 0000000000..21143a47d1
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/SubscriptionBillingServiceImplTest.java
@@ -0,0 +1,144 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.subscriptionbilling.*;
+import com.github.binarywang.wxpay.service.SubscriptionBillingService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.testbase.ApiTestModule;
+import com.google.inject.Inject;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+/**
+ * 微信支付预约扣费服务测试类
+ *
+ * 注意:由于预约扣费功能需要用户授权和实际的签约关系,
+ * 这些测试主要用于验证接口调用的正确性,而不是功能的完整性。
+ * 实际测试需要在具有有效签约关系的环境中进行。
+ *
+ *
+ * @author Binary Wang
+ */
+@Test(enabled = false) // 默认关闭,需要实际环境配置才能测试
+@Guice(modules = ApiTestModule.class)
+public class SubscriptionBillingServiceImplTest {
+
+ @Inject
+ private WxPayService wxPayService;
+
+ @Test
+ public void testScheduleSubscription() {
+ try {
+ SubscriptionBillingService service = this.wxPayService.getSubscriptionBillingService();
+
+ SubscriptionScheduleRequest request = new SubscriptionScheduleRequest();
+ request.setOutTradeNo("test_subscription_" + System.currentTimeMillis());
+ request.setOpenid("test_openid");
+ request.setDescription("测试预约扣费");
+ request.setScheduleTime("2024-09-01T10:00:00+08:00");
+
+ SubscriptionAmount amount = new SubscriptionAmount();
+ amount.setTotal(100); // 1元,单位分
+ amount.setCurrency("CNY");
+ request.setAmount(amount);
+
+ BillingPlan billingPlan = new BillingPlan();
+ billingPlan.setPlanType("MONTHLY");
+ billingPlan.setPeriod(1);
+ billingPlan.setTotalCount(12);
+ request.setBillingPlan(billingPlan);
+
+ SubscriptionScheduleResult result = service.scheduleSubscription(request);
+
+ System.out.println("预约扣费结果:" + result.toString());
+ assert result.getSubscriptionId() != null;
+ assert "SCHEDULED".equals(result.getStatus());
+
+ } catch (Exception e) {
+ // 预期会因为测试环境没有有效的签约关系而失败
+ System.out.println("预约扣费测试异常(预期):" + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testQuerySubscription() {
+ try {
+ SubscriptionBillingService service = this.wxPayService.getSubscriptionBillingService();
+ SubscriptionQueryResult result = service.querySubscription("test_subscription_id");
+
+ System.out.println("查询预约扣费结果:" + result.toString());
+
+ } catch (Exception e) {
+ // 预期会因为测试数据不存在而失败
+ System.out.println("查询预约扣费测试异常(预期):" + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testCancelSubscription() {
+ try {
+ SubscriptionBillingService service = this.wxPayService.getSubscriptionBillingService();
+
+ SubscriptionCancelRequest request = new SubscriptionCancelRequest();
+ request.setSubscriptionId("test_subscription_id");
+ request.setCancelReason("测试取消");
+
+ SubscriptionCancelResult result = service.cancelSubscription(request);
+
+ System.out.println("取消预约扣费结果:" + result.toString());
+ assert "CANCELLED".equals(result.getStatus());
+
+ } catch (Exception e) {
+ // 预期会因为测试数据不存在而失败
+ System.out.println("取消预约扣费测试异常(预期):" + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testInstantBilling() {
+ try {
+ SubscriptionBillingService service = this.wxPayService.getSubscriptionBillingService();
+
+ SubscriptionInstantBillingRequest request = new SubscriptionInstantBillingRequest();
+ request.setOutTradeNo("test_instant_" + System.currentTimeMillis());
+ request.setOpenid("test_openid");
+ request.setDescription("测试立即扣费");
+
+ SubscriptionAmount amount = new SubscriptionAmount();
+ amount.setTotal(100); // 1元,单位分
+ amount.setCurrency("CNY");
+ request.setAmount(amount);
+
+ SubscriptionInstantBillingResult result = service.instantBilling(request);
+
+ System.out.println("立即扣费结果:" + result.toString());
+ assert result.getTransactionId() != null;
+
+ } catch (Exception e) {
+ // 预期会因为测试环境没有有效的签约关系而失败
+ System.out.println("立即扣费测试异常(预期):" + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testQueryTransactions() {
+ try {
+ SubscriptionBillingService service = this.wxPayService.getSubscriptionBillingService();
+
+ SubscriptionTransactionQueryRequest request = new SubscriptionTransactionQueryRequest();
+ request.setOpenid("test_openid");
+ request.setBeginTime("2024-08-01T00:00:00+08:00");
+ request.setEndTime("2024-08-31T23:59:59+08:00");
+ request.setLimit(20);
+ request.setOffset(0);
+
+ SubscriptionTransactionQueryResult result = service.queryTransactions(request);
+
+ System.out.println("查询扣费记录结果:" + result.toString());
+ assert result.getTotalCount() != null;
+
+ } catch (Exception e) {
+ // 预期会因为测试环境数据问题而失败
+ System.out.println("查询扣费记录测试异常(预期):" + e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/TransferReceiptApiCompatibilityTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/TransferReceiptApiCompatibilityTest.java
new file mode 100644
index 0000000000..2fbb56fded
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/TransferReceiptApiCompatibilityTest.java
@@ -0,0 +1,135 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.marketing.transfer.BillReceiptResult;
+import com.github.binarywang.wxpay.bean.marketing.transfer.ReceiptBillRequest;
+import com.github.binarywang.wxpay.bean.merchanttransfer.ElectronicBillApplyRequest;
+import com.github.binarywang.wxpay.bean.merchanttransfer.ElectronicBillResult;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.google.gson.Gson;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+@Test
+public class TransferReceiptApiCompatibilityTest {
+
+ private static final String BASE_URL = "https://api.mch.weixin.qq.com";
+
+ /**
+ * 验证直连商户电子回单接口已切换到新版fund-app路径。
+ */
+ public void shouldUseNewMerchantTransferElecsignApiPath() throws WxPayException {
+ RequestCaptureHandler handler = new RequestCaptureHandler();
+ WxPayService wxPayService = handler.createWxPayService();
+ MerchantTransferServiceImpl merchantTransferService = new MerchantTransferServiceImpl(wxPayService);
+
+ merchantTransferService.applyElectronicBill(new ElectronicBillApplyRequest().setOutBatchNo("plfk2020042013"));
+ Assert.assertEquals(handler.lastPostUrl, BASE_URL + "/v3/fund-app/mch-transfer/elecsign/out-bill-no");
+ Assert.assertTrue(handler.lastPostBody.contains("\"out_bill_no\""));
+ Assert.assertFalse(handler.lastPostBody.contains("\"out_batch_no\""));
+
+ merchantTransferService.queryElectronicBill("plfk2020042013");
+ Assert.assertEquals(handler.lastGetUrl, BASE_URL + "/v3/fund-app/mch-transfer/elecsign/out-bill-no/plfk2020042013");
+ }
+
+ /**
+ * 验证服务商电子回单接口已切换到新版fund-app路径。
+ */
+ public void shouldUseNewPartnerTransferElecsignApiPath() throws WxPayException {
+ RequestCaptureHandler handler = new RequestCaptureHandler();
+ WxPayService wxPayService = handler.createWxPayService();
+ PartnerTransferServiceImpl partnerTransferService = new PartnerTransferServiceImpl(wxPayService);
+
+ ReceiptBillRequest request = new ReceiptBillRequest();
+ request.setOutBatchNo("plfk2020042013");
+ partnerTransferService.receiptBill(request);
+ Assert.assertEquals(handler.lastPostUrl, BASE_URL + "/v3/fund-app/mch-transfer/elecsign/out-bill-no");
+ Assert.assertTrue(handler.lastPostBody.contains("\"out_bill_no\""));
+ Assert.assertFalse(handler.lastPostBody.contains("\"out_batch_no\""));
+
+ partnerTransferService.queryBillReceipt("plfk2020042013");
+ Assert.assertEquals(handler.lastGetUrl, BASE_URL + "/v3/fund-app/mch-transfer/elecsign/out-bill-no/plfk2020042013");
+ }
+
+ /**
+ * 验证新版字段名能够正确反序列化到现有结果对象。
+ */
+ public void shouldDeserializeNewResponseFieldNames() {
+ Gson gson = new Gson();
+ BillReceiptResult billReceiptResult =
+ gson.fromJson("{\"out_bill_no\":\"plfk2020042013\",\"state\":\"FINISHED\"}", BillReceiptResult.class);
+ Assert.assertEquals(billReceiptResult.getOutBatchNo(), "plfk2020042013");
+ Assert.assertEquals(billReceiptResult.getSignatureStatus(), "FINISHED");
+
+ ElectronicBillResult electronicBillResult =
+ gson.fromJson("{\"out_bill_no\":\"plfk2020042013\",\"state\":\"FINISHED\"}", ElectronicBillResult.class);
+ Assert.assertEquals(electronicBillResult.getOutBatchNo(), "plfk2020042013");
+ Assert.assertEquals(electronicBillResult.getSignatureStatus(), "FINISHED");
+ }
+
+ /**
+ * 通过动态代理拦截WxPayService请求并记录URL/请求体,便于断言接口路径和参数。
+ */
+ private static class RequestCaptureHandler implements InvocationHandler {
+ private String lastPostUrl;
+ private String lastPostBody;
+ private String lastGetUrl;
+
+ private WxPayService createWxPayService() {
+ return (WxPayService) Proxy.newProxyInstance(
+ WxPayService.class.getClassLoader(),
+ new Class>[]{WxPayService.class},
+ this
+ );
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) {
+ if ("getPayBaseUrl".equals(method.getName())) {
+ return BASE_URL;
+ }
+ if ("postV3".equals(method.getName())) {
+ this.lastPostUrl = (String) args[0];
+ this.lastPostBody = (String) args[1];
+ return "{}";
+ }
+ if ("getV3".equals(method.getName())) {
+ this.lastGetUrl = (String) args[0];
+ return "{}";
+ }
+ if ("toString".equals(method.getName())) {
+ return "MockWxPayService";
+ }
+ Class> returnType = method.getReturnType();
+ if (boolean.class.equals(returnType)) {
+ return false;
+ }
+ if (int.class.equals(returnType)) {
+ return 0;
+ }
+ if (long.class.equals(returnType)) {
+ return 0L;
+ }
+ if (double.class.equals(returnType)) {
+ return 0D;
+ }
+ if (float.class.equals(returnType)) {
+ return 0F;
+ }
+ if (short.class.equals(returnType)) {
+ return (short) 0;
+ }
+ if (byte.class.equals(returnType)) {
+ return (byte) 0;
+ }
+ if (char.class.equals(returnType)) {
+ return (char) 0;
+ }
+ return null;
+ }
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/WxDepositServiceTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/WxDepositServiceTest.java
new file mode 100644
index 0000000000..1bbf347211
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/WxDepositServiceTest.java
@@ -0,0 +1,135 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.request.*;
+import com.github.binarywang.wxpay.bean.result.*;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.testbase.ApiTestModule;
+import com.google.inject.Inject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+/**
+ *
+ * 微信押金支付测试
+ *
+ *
+ * @author Binary Wang
+ * created on 2024-09-24
+ */
+@Test
+@Guice(modules = ApiTestModule.class)
+public class WxDepositServiceTest {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ @Inject
+ private WxPayService payService;
+
+ /**
+ * 测试押金下单
+ */
+ @Test
+ public void testUnifiedOrder() throws WxPayException {
+ WxDepositUnifiedOrderRequest request = WxDepositUnifiedOrderRequest.newBuilder()
+ .body("共享单车押金")
+ .outTradeNo("D" + System.currentTimeMillis())
+ .totalFee(99)
+ .spbillCreateIp("192.168.1.1")
+ .notifyUrl("https://example.com/wxpay/notify")
+ .tradeType("JSAPI")
+ .openid("test_openid_123")
+ .build();
+
+ try {
+ WxDepositUnifiedOrderResult result = this.payService.getWxDepositService().unifiedOrder(request);
+ logger.info("押金下单结果: {}", result);
+ } catch (WxPayException e) {
+ logger.error("押金下单失败", e);
+ // For demo purposes, just log the error - tests need proper WeChat credentials to run
+ }
+ }
+
+ /**
+ * 测试查询押金订单
+ */
+ @Test
+ public void testQueryOrder() throws WxPayException {
+ WxDepositOrderQueryRequest request = WxDepositOrderQueryRequest.newBuilder()
+ .outTradeNo("D1695559200000")
+ .build();
+
+ try {
+ WxDepositOrderQueryResult result = this.payService.getWxDepositService().queryOrder(request);
+ logger.info("押金订单查询结果: {}", result);
+ } catch (WxPayException e) {
+ logger.error("押金订单查询失败", e);
+ // For demo purposes, just log the error - tests need proper WeChat credentials to run
+ }
+ }
+
+ /**
+ * 测试押金消费
+ */
+ @Test
+ public void testConsume() throws WxPayException {
+ WxDepositConsumeRequest request = WxDepositConsumeRequest.newBuilder()
+ .transactionId("1217752501201407033233368018")
+ .outTradeNo("C" + System.currentTimeMillis())
+ .consumeFee(10)
+ .consumeDesc("单车使用费")
+ .build();
+
+ try {
+ WxDepositConsumeResult result = this.payService.getWxDepositService().consume(request);
+ logger.info("押金消费结果: {}", result);
+ } catch (WxPayException e) {
+ logger.error("押金消费失败", e);
+ // For demo purposes, just log the error - tests need proper WeChat credentials to run
+ }
+ }
+
+ /**
+ * 测试押金撤销
+ */
+ @Test
+ public void testUnfreeze() throws WxPayException {
+ WxDepositUnfreezeRequest request = WxDepositUnfreezeRequest.newBuilder()
+ .transactionId("1217752501201407033233368018")
+ .outTradeNo("U" + System.currentTimeMillis())
+ .unfreezeFee(99)
+ .unfreezeDesc("用户主动取消")
+ .build();
+
+ try {
+ WxDepositUnfreezeResult result = this.payService.getWxDepositService().unfreeze(request);
+ logger.info("押金撤销结果: {}", result);
+ } catch (WxPayException e) {
+ logger.error("押金撤销失败", e);
+ // For demo purposes, just log the error - tests need proper WeChat credentials to run
+ }
+ }
+
+ /**
+ * 测试押金退款
+ */
+ @Test
+ public void testRefund() throws WxPayException {
+ WxDepositRefundRequest request = WxDepositRefundRequest.newBuilder()
+ .transactionId("1217752501201407033233368018")
+ .outRefundNo("R" + System.currentTimeMillis())
+ .refundFee(50)
+ .refundDesc("部分退款")
+ .build();
+
+ try {
+ WxDepositRefundResult result = this.payService.getWxDepositService().refund(request);
+ logger.info("押金退款结果: {}", result);
+ } catch (WxPayException e) {
+ logger.error("押金退款失败", e);
+ // For demo purposes, just log the error - tests need proper WeChat credentials to run
+ }
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/SignatureExecTrustedHostTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/SignatureExecTrustedHostTest.java
new file mode 100644
index 0000000000..4d9147d9e0
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/SignatureExecTrustedHostTest.java
@@ -0,0 +1,200 @@
+package com.github.binarywang.wxpay.v3;
+
+import org.apache.http.HttpException;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpRequestWrapper;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.execchain.ClientExecChain;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.message.BasicStatusLine;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.testng.Assert.*;
+
+/**
+ * 测试 SignatureExec 的受信任主机功能,确保在代理转发场景下正确添加 Authorization 头
+ *
+ * @author GitHub Copilot
+ */
+public class SignatureExecTrustedHostTest {
+
+ /**
+ * 最简 CloseableHttpResponse 实现,仅用于单元测试
+ */
+ private static class StubCloseableHttpResponse extends BasicHttpResponse implements CloseableHttpResponse {
+ StubCloseableHttpResponse() {
+ super(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
+ }
+
+ @Override
+ public void close() {
+ }
+ }
+
+ /**
+ * 创建一个测试用的 Credentials,始终返回固定 schema 和 token
+ */
+ private static Credentials createTestCredentials() {
+ return new Credentials() {
+ @Override
+ public String getSchema() {
+ return "WECHATPAY2-SHA256-RSA2048";
+ }
+
+ @Override
+ public String getToken(HttpRequestWrapper request) {
+ return "test_token";
+ }
+ };
+ }
+
+ /**
+ * 创建一个 ClientExecChain,记录请求是否携带了 Authorization 头
+ */
+ private static ClientExecChain trackingExec(AtomicBoolean authHeaderAdded) {
+ return (route, request, context, execAware) -> {
+ if (request.containsHeader("Authorization")) {
+ authHeaderAdded.set(true);
+ }
+ return new StubCloseableHttpResponse();
+ };
+ }
+
+ /**
+ * 测试:对微信官方主机(以 .mch.weixin.qq.com 结尾)的请求应该添加 Authorization 头
+ */
+ @Test
+ public void testWechatOfficialHostShouldAddAuthorizationHeader() throws IOException, HttpException {
+ AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
+ SignatureExec signatureExec = new SignatureExec(
+ createTestCredentials(), response -> true, trackingExec(authHeaderAdded), Collections.emptySet()
+ );
+
+ HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/certificates");
+ signatureExec.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
+
+ assertTrue(authHeaderAdded.get(), "请求微信官方接口时应该添加 Authorization 头");
+ }
+
+ /**
+ * 测试:对非微信主机且不在受信任列表中的请求,不应该添加 Authorization 头
+ */
+ @Test
+ public void testUntrustedProxyHostShouldNotAddAuthorizationHeader() throws IOException, HttpException {
+ AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
+ SignatureExec signatureExec = new SignatureExec(
+ createTestCredentials(), response -> true, trackingExec(authHeaderAdded), Collections.emptySet()
+ );
+
+ HttpGet httpGet = new HttpGet("http://proxy.company.com:8080/v3/certificates");
+ signatureExec.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
+
+ assertFalse(authHeaderAdded.get(), "不受信任的代理主机请求不应该添加 Authorization 头");
+ }
+
+ /**
+ * 测试:对在受信任列表中的代理主机请求,应该添加 Authorization 头.
+ * 这是修复代理转发场景下 Authorization 头丢失问题的核心功能
+ */
+ @Test
+ public void testTrustedProxyHostShouldAddAuthorizationHeader() throws IOException, HttpException {
+ AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
+ Set trustedHosts = new HashSet<>();
+ trustedHosts.add("proxy.company.com");
+ SignatureExec signatureExec = new SignatureExec(
+ createTestCredentials(), response -> true, trackingExec(authHeaderAdded), trustedHosts
+ );
+
+ HttpGet httpGet = new HttpGet("http://proxy.company.com:8080/v3/certificates");
+ signatureExec.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
+
+ assertTrue(authHeaderAdded.get(), "受信任的代理主机请求应该添加 Authorization 头");
+ }
+
+ /**
+ * 测试:WxPayV3HttpClientBuilder 的 withTrustedHost 方法支持链式调用
+ */
+ @Test
+ public void testWithTrustedHostSupportsChainingCall() {
+ WxPayV3HttpClientBuilder builder = WxPayV3HttpClientBuilder.create();
+ // 方法应该返回同一实例以支持链式调用
+ WxPayV3HttpClientBuilder result = builder.withTrustedHost("proxy.company.com");
+ assertSame(result, builder, "withTrustedHost 应该返回当前 Builder 实例(支持链式调用)");
+ }
+
+ /**
+ * 测试:withTrustedHost 传入含端口的地址时应自动提取主机名并正确影响签名行为
+ */
+ @Test
+ public void testWithTrustedHostWithPortShouldStripPort() throws IOException, HttpException {
+ AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
+ SignatureExec signatureExec = new SignatureExec(
+ createTestCredentials(), response -> true, trackingExec(authHeaderAdded), Collections.emptySet()
+ );
+ // 直接验证:SignatureExec 的主机匹配逻辑使用 URI.getHost(),不含端口
+ // 因此只要 trustedHosts 中存有 "proxy.company.com",对 proxy.company.com:8080 的请求也应签名
+ Set trustedHosts = new HashSet<>();
+ trustedHosts.add("proxy.company.com");
+ SignatureExec execWithPort = new SignatureExec(
+ createTestCredentials(), response -> true, trackingExec(authHeaderAdded), trustedHosts
+ );
+ HttpGet httpGet = new HttpGet("http://proxy.company.com:8080/v3/pay/transactions/native");
+ execWithPort.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
+ assertTrue(authHeaderAdded.get(), "含端口的代理请求匹配受信任主机后应添加 Authorization 头");
+ }
+
+ /**
+ * 测试:withTrustedHost 传入空值不应该抛出异常
+ */
+ @Test
+ public void testWithTrustedHostNullOrEmptyShouldNotThrow() {
+ WxPayV3HttpClientBuilder builder = WxPayV3HttpClientBuilder.create();
+ // 传入 null 和空字符串不应该抛出异常
+ builder.withTrustedHost(null);
+ builder.withTrustedHost("");
+ }
+
+ /**
+ * 测试:withTrustedHost 传入带端口的地址(如 "proxy.company.com:8080")时应自动提取主机名.
+ * WxPayV3HttpClientBuilder 应将端口剥离后存入受信任列表,
+ * 使得发往该主机的请求(URI.getHost() 不含端口)也能正确匹配并携带 Authorization 头
+ */
+ @Test
+ public void testWithTrustedHostBuilderStripsPort() throws IOException, HttpException {
+ AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
+ // 传入带端口的主机,builder 应自动提取主机名
+ SignatureExec signatureExec = new SignatureExec(
+ createTestCredentials(), response -> true, trackingExec(authHeaderAdded),
+ Collections.singleton("proxy.company.com")
+ );
+ HttpGet httpGet = new HttpGet("http://proxy.company.com:8080/v3/certificates");
+ signatureExec.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
+ assertTrue(authHeaderAdded.get(), "builder 自动提取主机名后,对应代理请求应携带 Authorization 头");
+ }
+
+ /**
+ * 测试:SignatureExec 的旧构造函数(不带 trustedHosts)应该仍然有效
+ */
+ @Test
+ public void testBackwardCompatibilityWithOldConstructor() throws IOException, HttpException {
+ AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
+ // 使用旧的三参数构造函数
+ SignatureExec signatureExec = new SignatureExec(
+ createTestCredentials(), response -> true, trackingExec(authHeaderAdded)
+ );
+
+ // 微信官方主机仍然应该添加 Authorization 头
+ HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/certificates");
+ signatureExec.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
+
+ assertTrue(authHeaderAdded.get(), "使用旧构造函数时,请求微信官方接口仍应添加 Authorization 头");
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifierPublicKeyModeTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifierPublicKeyModeTest.java
new file mode 100644
index 0000000000..e60f5eac12
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifierPublicKeyModeTest.java
@@ -0,0 +1,91 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.security.cert.X509Certificate;
+
+import static org.testng.Assert.*;
+
+/**
+ * 测试公钥模式下 AutoUpdateCertificatesVerifier 的健壮性
+ *
+ * @author copilot
+ */
+public class AutoUpdateCertificatesVerifierPublicKeyModeTest {
+
+ private String invalidMchId;
+ private String invalidApiV3Key;
+ private String invalidCertSerialNo;
+ private String payBaseUrl;
+ private WxPayCredentials credentials;
+
+ @BeforeMethod
+ public void setUp() {
+ // 使用无效的配置,模拟证书下载失败的场景
+ invalidMchId = "invalid_mch_id";
+ invalidApiV3Key = "invalid_api_v3_key_must_be_32_b";
+ invalidCertSerialNo = "invalid_serial_no";
+ payBaseUrl = "https://api.mch.weixin.qq.com";
+
+ credentials = new WxPayCredentials(
+ invalidMchId,
+ new PrivateKeySigner(invalidCertSerialNo, null)
+ );
+ }
+
+ /**
+ * 测试当证书下载失败时,构造函数不应该抛出异常
+ * 这是为了支持公钥模式下的场景,在公钥模式下商户可能没有平台证书
+ */
+ @Test
+ public void testConstructorShouldNotThrowExceptionWhenCertDownloadFails() {
+ // 构造函数应该不抛出异常,即使证书下载失败
+ AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
+ credentials,
+ invalidApiV3Key.getBytes(StandardCharsets.UTF_8),
+ 60,
+ payBaseUrl,
+ null
+ );
+ // 如果没有抛出异常,测试通过
+ assertNotNull(verifier);
+ }
+
+ /**
+ * 测试当没有有效证书时,verify 方法应该返回 false 而不是抛出异常
+ */
+ @Test
+ public void testVerifyShouldReturnFalseWhenNoCertificateAvailable() {
+ AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
+ credentials,
+ invalidApiV3Key.getBytes(StandardCharsets.UTF_8),
+ 60,
+ payBaseUrl,
+ null
+ );
+
+ // verify 方法应该返回 false,而不是抛出异常
+ boolean result = verifier.verify("test_serial", "test_message".getBytes(), "test_signature");
+ assertFalse(result, "当没有有效证书时,verify 应该返回 false");
+ }
+
+ /**
+ * 测试当没有有效证书时,getValidCertificate 方法应该抛出有意义的异常
+ */
+ @Test(expectedExceptions = me.chanjar.weixin.common.error.WxRuntimeException.class,
+ expectedExceptionsMessageRegExp = ".*No valid certificate available.*")
+ public void testGetValidCertificateShouldThrowMeaningfulException() {
+ AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
+ credentials,
+ invalidApiV3Key.getBytes(StandardCharsets.UTF_8),
+ 60,
+ payBaseUrl,
+ null
+ );
+
+ // 应该抛出有意义的异常
+ X509Certificate certificate = verifier.getValidCertificate();
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtilTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtilTest.java
new file mode 100644
index 0000000000..18f46c687f
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtilTest.java
@@ -0,0 +1,179 @@
+package com.github.binarywang.wxpay.v3.util;
+
+import com.github.binarywang.wxpay.bean.profitsharing.request.ProfitSharingReceiverV3Request;
+import com.github.binarywang.wxpay.bean.profitsharing.request.ProfitSharingV3Request;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.v3.SpecEncrypt;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import org.testng.annotations.Test;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.testng.Assert.*;
+
+/**
+ * RsaCryptoUtil 测试类
+ */
+public class RsaCryptoUtilTest {
+
+ /**
+ * 测试反射能否找到嵌套类中的 @SpecEncrypt 注解字段
+ */
+ @Test
+ public void testFindAnnotatedFieldsInNestedClass() {
+ // 创建 Receiver 对象
+ ProfitSharingV3Request.Receiver receiver = new ProfitSharingV3Request.Receiver();
+ receiver.setName("测试姓名");
+
+ // 使用反射查找带有 @SpecEncrypt 注解的字段
+ Class> receiverClass = receiver.getClass();
+ Field[] fields = receiverClass.getDeclaredFields();
+
+ boolean foundNameField = false;
+ boolean nameFieldHasAnnotation = false;
+
+ for (Field field : fields) {
+ if (field.getName().equals("name")) {
+ foundNameField = true;
+ if (field.isAnnotationPresent(SpecEncrypt.class)) {
+ nameFieldHasAnnotation = true;
+ }
+ }
+ }
+
+ // 验证能够找到 name 字段并且它有 @SpecEncrypt 注解
+ assertTrue(foundNameField, "应该能找到 name 字段");
+ assertTrue(nameFieldHasAnnotation, "name 字段应该有 @SpecEncrypt 注解");
+ }
+
+ /**
+ * 测试嵌套对象中的字段加密
+ * 验证 List 中每个 Receiver 对象的 name 字段是否能被正确找到和处理
+ */
+ @Test
+ public void testEncryptFieldsWithNestedObjects() {
+ // 创建测试对象
+ ProfitSharingV3Request request = ProfitSharingV3Request.newBuilder()
+ .appid("test-appid")
+ .subMchId("test-submchid")
+ .transactionId("test-transaction")
+ .outOrderNo("test-order-no")
+ .unfreezeUnsplit(true)
+ .build();
+
+ List receivers = new ArrayList<>();
+ ProfitSharingV3Request.Receiver receiver = new ProfitSharingV3Request.Receiver();
+ receiver.setName("张三"); // 设置需要加密的字段
+ receiver.setAccount("test-account");
+ receiver.setType("PERSONAL_OPENID");
+ receiver.setAmount(100);
+ receiver.setRelationType("STORE");
+ receiver.setDescription("测试分账");
+
+ receivers.add(receiver);
+ request.setReceivers(receivers);
+
+ // 验证 receivers 字段有 @SpecEncrypt 注解
+ try {
+ Field receiversField = ProfitSharingV3Request.class.getDeclaredField("receivers");
+ boolean hasAnnotation = receiversField.isAnnotationPresent(SpecEncrypt.class);
+ assertTrue(hasAnnotation, "receivers 字段应该有 @SpecEncrypt 注解");
+ } catch (NoSuchFieldException e) {
+ fail("应该能找到 receivers 字段");
+ }
+
+ // 验证name字段不为null
+ assertNotNull(receiver.getName());
+ assertEquals(receiver.getName(), "张三");
+ }
+
+ /**
+ * 测试单个对象中的字段加密
+ * 验证直接在对象上的 @SpecEncrypt 字段是否能被正确找到
+ */
+ @Test
+ public void testEncryptFieldsWithDirectField() {
+ // 创建测试对象
+ ProfitSharingReceiverV3Request request = ProfitSharingReceiverV3Request.newBuilder()
+ .appid("test-appid")
+ .subMchId("test-submchid")
+ .type("PERSONAL_OPENID")
+ .account("test-account")
+ .name("李四")
+ .relationType("STORE")
+ .build();
+
+ // 验证 name 字段有 @SpecEncrypt 注解
+ try {
+ Field nameField = ProfitSharingReceiverV3Request.class.getDeclaredField("name");
+ boolean hasAnnotation = nameField.isAnnotationPresent(SpecEncrypt.class);
+ assertTrue(hasAnnotation, "name 字段应该有 @SpecEncrypt 注解");
+ } catch (NoSuchFieldException e) {
+ fail("应该能找到 name 字段");
+ }
+
+ // 验证name字段不为null
+ assertNotNull(request.getName());
+ assertEquals(request.getName(), "李四");
+ }
+
+ /**
+ * 测试类继承场景下的字段加密
+ * 验证父类中带 @SpecEncrypt 注解的字段是否能被正确找到和加密
+ */
+ @Test
+ public void testEncryptFieldsWithInheritance() {
+ // 定义测试用的父类和子类
+ @Data
+ class ParentRequest {
+ @SpecEncrypt
+ @SerializedName("parent_name")
+ private String parentName;
+ }
+
+ @Data
+ @lombok.EqualsAndHashCode(callSuper = false)
+ class ChildRequest extends ParentRequest {
+ @SpecEncrypt
+ @SerializedName("child_name")
+ private String childName;
+
+ @Override
+ protected boolean canEqual(final Object other) {
+ return other instanceof ChildRequest;
+ }
+ }
+
+ // 创建子类实例
+ ChildRequest request = new ChildRequest();
+ request.setParentName("父类字段");
+ request.setChildName("子类字段");
+
+ // 验证能够找到父类和子类的字段
+ // 使用 getDeclaredFields 只能找到子类字段
+ Field[] childFields = ChildRequest.class.getDeclaredFields();
+
+ // 使用反射调用 RsaCryptoUtil 的私有 getAllFields 方法
+ int annotatedFieldCount = 0;
+ try {
+ java.lang.reflect.Method getAllFieldsMethod = RsaCryptoUtil.class.getDeclaredMethod("getAllFields", Class.class);
+ getAllFieldsMethod.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ List allFields = (List) getAllFieldsMethod.invoke(null, ChildRequest.class);
+
+ for (Field field : allFields) {
+ if (field.isAnnotationPresent(SpecEncrypt.class)) {
+ annotatedFieldCount++;
+ }
+ }
+ } catch (Exception e) {
+ fail("无法调用 getAllFields 方法: " + e.getMessage());
+ }
+
+ // 应该找到2个带注解的字段(parentName 和 childName)
+ assertTrue(annotatedFieldCount >= 2, "应该能找到至少2个带 @SpecEncrypt 注解的字段");
+ }
+}
diff --git a/weixin-java-qidian/pom.xml b/weixin-java-qidian/pom.xml
index 62734353df..567efd7adb 100644
--- a/weixin-java-qidian/pom.xml
+++ b/weixin-java-qidian/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.7.8.B
+ 4.8.4.B
weixin-java-qidian
@@ -31,6 +31,11 @@
okhttp
provided