From d27a9221c3be19877ab64fba9dbf0229680223bb Mon Sep 17 00:00:00 2001
From: tarzanwang
+ * 在该原因下,若商户除了relate_limitations所罗列的被管控能力,还有其他被管控的能力时会返回(如有多项以英文逗号分隔) + *
+ */ + @SerializedName("other_relate_limitations") + private String otherRelateLimitations; + + /** + * 商户被该原因管控的解脱路径 + *+ * 在该原因下,若存在解脱路径时会返回 + *
+ * 若解脱路径recover_way为“填写尽调信息”、“补充受益所有人信息”,需通过提交尽调来解脱,此处会返回“尽调单号”;若解脱路径recover_way + * 为“提交相关信息申诉”,需通过提交资料来解脱,此处会返回“商户管理记录单号”;若解脱路径recover_way为“联系有权机关咨询”,此处会返回有权机关信息 + *
+ */ + @SerializedName("recover_way_param") + private String recoverWayParam; + + /** + * 商户被该原因管控的解脱帮助链接 + *+ * 在该原因下,若存在解脱帮助说明时会返回 + *
+ */ + @SerializedName("recover_help_url") + private String recoverHelpUrl; + + /** + * 处置方式 + *+ * 管控处置方式类型,默认是立即管控 + *
+ * 若商户被管控时会返回,延迟管控但是未到管控时间时不会返回 + *
+ */ + @SerializedName("limitation_date") + private String limitationDate; + + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/PartnerSubscribeNotifyResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/PartnerSubscribeNotifyResult.java new file mode 100644 index 0000000000..52917ddf17 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/PartnerSubscribeNotifyResult.java @@ -0,0 +1,66 @@ +package com.github.binarywang.wxpay.bean.notify; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 合作伙伴订阅通知 产品介绍 + *+ * 该类是订阅通知的通用结构,每个字段代表的含义和订阅类型有关。请依据文档自行判断使用。 + *
+ * + * @author zhangyl + */ +@Data +@NoArgsConstructor +public class PartnerSubscribeNotifyResult implements Serializable, + WxPayBaseNotifyV3Result+ * 产品介绍 + *
+ * + * @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/WxPayService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java index 6a096c6338..a460d5f248 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 @@ -400,6 +400,13 @@ default WxPayService switchoverTo(String mchIdOrConfigKey) { */ void setEntPayService(EntPayService entPayService); + /** + * 获取商户被管控能力及原因查询接口 + * + * @return MerchantLimitationService + */ + MerchantLimitationService getMerchantLimitationService(); + /** *
* 查询订单.
@@ -1164,6 +1171,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
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 36987f637d..6868cb644f 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
@@ -143,6 +143,9 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
@Getter
private final MiPayService miPayService = new MiPayServiceImpl(this);
+ @Getter
+ private final MerchantLimitationService merchantLimitationService = new MerchantLimitationServiceImpl(this);
+
protected Map configMap = new ConcurrentHashMap<>();
@Override
@@ -631,6 +634,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 {
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);
+ }
+}
From b545874f1a47b7cbeb42b2e8211addf4379646f7 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Mon, 11 May 2026 20:26:44 +0800
Subject: [PATCH 08/38] =?UTF-8?q?:new:=20#3952=E3=80=90=E5=B0=8F=E7=A8=8B?=
=?UTF-8?q?=E5=BA=8F=E3=80=91=E6=96=B0=E5=A2=9E=E6=9C=8D=E5=8A=A1=E5=8D=A1?=
=?UTF-8?q?=E7=89=87=E6=B6=88=E6=81=AF=EF=BC=88=E8=AE=A2=E9=98=85=E6=B6=88?=
=?UTF-8?q?=E6=81=AF=202.0=EF=BC=89=E6=9C=8D=E5=8A=A1=E7=AB=AF=E8=83=BD?=
=?UTF-8?q?=E5=8A=9B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../wx/miniapp/api/WxMaSubscribeService.java | 44 +++++++++
.../api/impl/WxMaSubscribeServiceImpl.java | 32 +++++++
.../bean/WxMaGetUserNotifyRequest.java | 66 +++++++++++++
.../miniapp/bean/WxMaGetUserNotifyResult.java | 60 ++++++++++++
.../bean/WxMaServiceNotifyExtRequest.java | 82 ++++++++++++++++
.../bean/WxMaServiceNotifyRequest.java | 93 +++++++++++++++++++
.../miniapp/constant/WxMaApiUrlConstants.java | 9 ++
.../impl/WxMaSubscribeServiceImplTest.java | 65 +++++++++++++
8 files changed, 451 insertions(+)
create mode 100644 weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaGetUserNotifyRequest.java
create mode 100644 weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaGetUserNotifyResult.java
create mode 100644 weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaServiceNotifyExtRequest.java
create mode 100644 weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaServiceNotifyRequest.java
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaSubscribeService.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaSubscribeService.java
index e6b1ed16a2..1dbb9f64c9 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaSubscribeService.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaSubscribeService.java
@@ -1,5 +1,9 @@
package cn.binarywang.wx.miniapp.api;
+import cn.binarywang.wx.miniapp.bean.WxMaGetUserNotifyRequest;
+import cn.binarywang.wx.miniapp.bean.WxMaGetUserNotifyResult;
+import cn.binarywang.wx.miniapp.bean.WxMaServiceNotifyExtRequest;
+import cn.binarywang.wx.miniapp.bean.WxMaServiceNotifyRequest;
import cn.binarywang.wx.miniapp.bean.WxMaSubscribeMessage;
import me.chanjar.weixin.common.bean.subscribemsg.CategoryData;
import me.chanjar.weixin.common.bean.subscribemsg.PubTemplateKeyword;
@@ -113,4 +117,44 @@ public interface WxMaSubscribeService {
*/
void sendSubscribeMsg(WxMaSubscribeMessage subscribeMessage) throws WxErrorException;
+ /**
+ *
+ * 激活与更新服务卡片
+ *
+ * 详情请见: 激活与更新服务卡片
+ * 接口url格式: POST https://api.weixin.qq.com/wxa/setusernotify?access_token=ACCESS_TOKEN
+ *
+ *
+ * @param request 请求参数
+ * @throws WxErrorException .
+ */
+ void setUserNotify(WxMaServiceNotifyRequest request) throws WxErrorException;
+
+ /**
+ *
+ * 更新服务卡片扩展信息
+ *
+ * 详情请见: 更新服务卡片扩展信息
+ * 接口url格式: POST https://api.weixin.qq.com/wxa/setusernotifyext?access_token=ACCESS_TOKEN
+ *
+ *
+ * @param request 请求参数
+ * @throws WxErrorException .
+ */
+ void setUserNotifyExt(WxMaServiceNotifyExtRequest request) throws WxErrorException;
+
+ /**
+ *
+ * 查询服务卡片状态
+ *
+ * 详情请见: 查询服务卡片状态
+ * 接口url格式: POST https://api.weixin.qq.com/wxa/getusernotify?access_token=ACCESS_TOKEN
+ *
+ *
+ * @param request 请求参数
+ * @return 服务卡片状态
+ * @throws WxErrorException .
+ */
+ WxMaGetUserNotifyResult getUserNotify(WxMaGetUserNotifyRequest request) throws WxErrorException;
+
}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaSubscribeServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaSubscribeServiceImpl.java
index a7db154a68..edf4d5ba10 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaSubscribeServiceImpl.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaSubscribeServiceImpl.java
@@ -2,6 +2,10 @@
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.WxMaSubscribeService;
+import cn.binarywang.wx.miniapp.bean.WxMaGetUserNotifyRequest;
+import cn.binarywang.wx.miniapp.bean.WxMaGetUserNotifyResult;
+import cn.binarywang.wx.miniapp.bean.WxMaServiceNotifyExtRequest;
+import cn.binarywang.wx.miniapp.bean.WxMaServiceNotifyRequest;
import cn.binarywang.wx.miniapp.bean.WxMaSubscribeMessage;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.bean.subscribemsg.CategoryData;
@@ -89,4 +93,32 @@ public void sendSubscribeMsg(WxMaSubscribeMessage subscribeMessage) throws WxErr
throw new WxErrorException(WxError.fromJson(responseContent, WxType.MiniApp));
}
}
+
+ @Override
+ public void setUserNotify(WxMaServiceNotifyRequest request) throws WxErrorException {
+ String responseContent = this.service.post(SERVICE_NOTIFY_SET_URL, request.toJson());
+ JsonObject jsonObject = GsonParser.parse(responseContent);
+ if (jsonObject.get(WxConsts.ERR_CODE).getAsInt() != 0) {
+ throw new WxErrorException(WxError.fromJson(responseContent, WxType.MiniApp));
+ }
+ }
+
+ @Override
+ public void setUserNotifyExt(WxMaServiceNotifyExtRequest request) throws WxErrorException {
+ String responseContent = this.service.post(SERVICE_NOTIFY_SET_EXT_URL, request.toJson());
+ JsonObject jsonObject = GsonParser.parse(responseContent);
+ if (jsonObject.get(WxConsts.ERR_CODE).getAsInt() != 0) {
+ throw new WxErrorException(WxError.fromJson(responseContent, WxType.MiniApp));
+ }
+ }
+
+ @Override
+ public WxMaGetUserNotifyResult getUserNotify(WxMaGetUserNotifyRequest request) throws WxErrorException {
+ String responseContent = this.service.post(SERVICE_NOTIFY_GET_URL, request.toJson());
+ JsonObject jsonObject = GsonParser.parse(responseContent);
+ if (jsonObject.get(WxConsts.ERR_CODE).getAsInt() != 0) {
+ throw new WxErrorException(WxError.fromJson(responseContent, WxType.MiniApp));
+ }
+ return WxMaGsonBuilder.create().fromJson(responseContent, WxMaGetUserNotifyResult.class);
+ }
}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaGetUserNotifyRequest.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaGetUserNotifyRequest.java
new file mode 100644
index 0000000000..abc7518e02
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaGetUserNotifyRequest.java
@@ -0,0 +1,66 @@
+package cn.binarywang.wx.miniapp.bean;
+
+import cn.binarywang.wx.miniapp.json.WxMaGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 查询服务卡片状态请求.
+ *
+ * 接口文档:
+ *
+ * 查询服务卡片状态
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxMaGetUserNotifyRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户身份标识符.
+ *
+ * 参数:openid
+ * 是否必填:是
+ *
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ * 动态更新令牌.
+ *
+ * 参数:notify_code
+ * 是否必填:是
+ *
+ */
+ @SerializedName("notify_code")
+ private String notifyCode;
+
+ /**
+ * 卡片ID.
+ *
+ * 参数:notify_type
+ * 是否必填:是
+ *
+ */
+ @SerializedName("notify_type")
+ private Integer notifyType;
+
+ /**
+ * 转为 JSON 字符串.
+ *
+ * @return JSON 字符串
+ */
+ public String toJson() {
+ return WxMaGsonBuilder.create().toJson(this);
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaGetUserNotifyResult.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaGetUserNotifyResult.java
new file mode 100644
index 0000000000..0090eb19b4
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaGetUserNotifyResult.java
@@ -0,0 +1,60 @@
+package cn.binarywang.wx.miniapp.bean;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/**
+ * 查询服务卡片状态响应.
+ *
+ * 接口文档:
+ *
+ * 查询服务卡片状态
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class WxMaGetUserNotifyResult extends WxMaBaseResponse {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 卡片状态信息.
+ */
+ @SerializedName("notify_info")
+ private NotifyInfo notifyInfo;
+
+ /**
+ * 卡片状态详情.
+ */
+ @Data
+ public static class NotifyInfo implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 卡片ID.
+ */
+ @SerializedName("notify_type")
+ private Integer notifyType;
+
+ /**
+ * 上次有效推送的卡片状态与状态相关字段,没推送过为空字符串.
+ */
+ @SerializedName("content_json")
+ private String contentJson;
+
+ /**
+ * code 状态:0 正常;1 有风险;2 异常;10 用户拒收本次code.
+ */
+ @SerializedName("code_state")
+ private Integer codeState;
+
+ /**
+ * code 过期时间,秒级时间戳.
+ */
+ @SerializedName("code_expire_time")
+ private Long codeExpireTime;
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaServiceNotifyExtRequest.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaServiceNotifyExtRequest.java
new file mode 100644
index 0000000000..56315ce95e
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaServiceNotifyExtRequest.java
@@ -0,0 +1,82 @@
+package cn.binarywang.wx.miniapp.bean;
+
+import cn.binarywang.wx.miniapp.json.WxMaGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 更新服务卡片扩展信息请求.
+ *
+ *
接口文档:
+ *
+ * 更新服务卡片扩展信息
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxMaServiceNotifyExtRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户身份标识符.
+ *
+ * 参数:openid
+ * 是否必填:是
+ * 描述:用户身份标识符。
+ * 当使用微信支付订单号作为 code 时,需要与实际支付用户一致;
+ * 当通过前端获取 code 时,需要与点击 button 的用户一致。
+ *
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ * 卡片ID.
+ *
+ * 参数:notify_type
+ * 是否必填:是
+ * 描述:卡片ID。
+ *
+ */
+ @SerializedName("notify_type")
+ private Integer notifyType;
+
+ /**
+ * 动态更新令牌.
+ *
+ * 参数:notify_code
+ * 是否必填:是
+ * 描述:动态更新令牌。
+ *
+ */
+ @SerializedName("notify_code")
+ private String notifyCode;
+
+ /**
+ * 扩展信息.
+ *
+ * 参数:ext_json
+ * 是否必填:是
+ * 描述:扩展信息,不同卡片的定义不同。
+ *
+ */
+ @SerializedName("ext_json")
+ private String extJson;
+
+ /**
+ * 转为 JSON 字符串.
+ *
+ * @return JSON 字符串
+ */
+ public String toJson() {
+ return WxMaGsonBuilder.create().toJson(this);
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaServiceNotifyRequest.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaServiceNotifyRequest.java
new file mode 100644
index 0000000000..e15e0782f9
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaServiceNotifyRequest.java
@@ -0,0 +1,93 @@
+package cn.binarywang.wx.miniapp.bean;
+
+import cn.binarywang.wx.miniapp.json.WxMaGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 激活与更新服务卡片请求.
+ *
+ * 接口文档:
+ *
+ * 激活与更新服务卡片
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxMaServiceNotifyRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户身份标识符.
+ *
+ * 参数:openid
+ * 是否必填:是
+ * 描述:用户身份标识符。
+ * 当使用微信支付订单号作为 code 时,需要与实际支付用户一致;
+ * 当通过前端获取 code 时,需要与点击 button 的用户一致。
+ *
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ * 卡片ID.
+ *
+ * 参数:notify_type
+ * 是否必填:是
+ * 描述:卡片ID。
+ *
+ */
+ @SerializedName("notify_type")
+ private Integer notifyType;
+
+ /**
+ * 动态更新令牌.
+ *
+ * 参数:notify_code
+ * 是否必填:是
+ * 描述:动态更新令牌。
+ *
+ */
+ @SerializedName("notify_code")
+ private String notifyCode;
+
+ /**
+ * 卡片状态与状态相关字段.
+ *
+ * 参数:content_json
+ * 是否必填:是
+ * 描述:卡片状态与状态相关字段,不同卡片的定义不同。
+ *
+ */
+ @SerializedName("content_json")
+ private String contentJson;
+
+ /**
+ * 微信支付订单号验证字段(可选).
+ *
+ * 参数:check_json
+ * 是否必填:否
+ * 描述:微信支付订单号验证字段。当将微信支付订单号作为 notify_code 时,在激活时需要传入。
+ *
+ */
+ @SerializedName("check_json")
+ private String checkJson;
+
+ /**
+ * 转为 JSON 字符串.
+ *
+ * @return JSON 字符串
+ */
+ public String toJson() {
+ return WxMaGsonBuilder.create().toJson(this);
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaApiUrlConstants.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaApiUrlConstants.java
index 86fa58ac6c..815d47c623 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaApiUrlConstants.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaApiUrlConstants.java
@@ -358,6 +358,15 @@ public interface Subscribe {
/** 发送订阅消息 */
String SUBSCRIBE_MSG_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send";
+
+ /** 激活与更新服务卡片 */
+ String SERVICE_NOTIFY_SET_URL = "https://api.weixin.qq.com/wxa/setusernotify";
+
+ /** 更新服务卡片扩展信息 */
+ String SERVICE_NOTIFY_SET_EXT_URL = "https://api.weixin.qq.com/wxa/setusernotifyext";
+
+ /** 查询服务卡片状态 */
+ String SERVICE_NOTIFY_GET_URL = "https://api.weixin.qq.com/wxa/getusernotify";
}
public interface User {
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaSubscribeServiceImplTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaSubscribeServiceImplTest.java
index 10993e5651..c910d121d1 100644
--- a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaSubscribeServiceImplTest.java
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaSubscribeServiceImplTest.java
@@ -1,6 +1,11 @@
package cn.binarywang.wx.miniapp.api.impl;
import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.api.WxMaSubscribeService;
+import cn.binarywang.wx.miniapp.bean.WxMaGetUserNotifyRequest;
+import cn.binarywang.wx.miniapp.bean.WxMaGetUserNotifyResult;
+import cn.binarywang.wx.miniapp.bean.WxMaServiceNotifyExtRequest;
+import cn.binarywang.wx.miniapp.bean.WxMaServiceNotifyRequest;
import cn.binarywang.wx.miniapp.bean.WxMaSubscribeMessage;
import me.chanjar.weixin.common.bean.subscribemsg.CategoryData;
import me.chanjar.weixin.common.bean.subscribemsg.PubTemplateKeyword;
@@ -10,12 +15,16 @@
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import me.chanjar.weixin.common.error.WxErrorException;
+import org.testng.Assert;
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
/**
* 测试类.
@@ -71,4 +80,60 @@ public void testSendSubscribeMsg() throws WxErrorException {
// TODO 待完善补充
this.wxService.getSubscribeService().sendSubscribeMsg(WxMaSubscribeMessage.builder().build());
}
+
+ @Test
+ public void testSetUserNotify() throws WxErrorException {
+ WxMaService service = mock(WxMaService.class);
+ when(service.post(anyString(), anyString())).thenReturn("{\"errcode\":0,\"errmsg\":\"ok\"}");
+
+ WxMaSubscribeService subscribeService = new WxMaSubscribeServiceImpl(service);
+ WxMaServiceNotifyRequest request = WxMaServiceNotifyRequest.builder()
+ .openid("test_openid")
+ .notifyType(1)
+ .notifyCode("test_notify_code")
+ .contentJson("{}")
+ .build();
+ subscribeService.setUserNotify(request);
+ }
+
+ @Test
+ public void testSetUserNotifyExt() throws WxErrorException {
+ WxMaService service = mock(WxMaService.class);
+ when(service.post(anyString(), anyString())).thenReturn("{\"errcode\":0,\"errmsg\":\"ok\"}");
+
+ WxMaSubscribeService subscribeService = new WxMaSubscribeServiceImpl(service);
+ WxMaServiceNotifyExtRequest request = WxMaServiceNotifyExtRequest.builder()
+ .openid("test_openid")
+ .notifyType(1)
+ .notifyCode("test_notify_code")
+ .extJson("{}")
+ .build();
+ subscribeService.setUserNotifyExt(request);
+ }
+
+ @Test
+ public void testGetUserNotify() throws WxErrorException {
+ WxMaService service = mock(WxMaService.class);
+ when(service.post(anyString(), anyString())).thenReturn(
+ "{\"errcode\":0,\"errmsg\":\"ok\","
+ + "\"notify_info\":{"
+ + "\"notify_type\":1,"
+ + "\"content_json\":\"{\\\"status\\\":1}\","
+ + "\"code_state\":0,"
+ + "\"code_expire_time\":1700000000"
+ + "}}");
+
+ WxMaSubscribeService subscribeService = new WxMaSubscribeServiceImpl(service);
+ WxMaGetUserNotifyRequest request = WxMaGetUserNotifyRequest.builder()
+ .openid("test_openid")
+ .notifyCode("test_notify_code")
+ .notifyType(1)
+ .build();
+ WxMaGetUserNotifyResult result = subscribeService.getUserNotify(request);
+ Assert.assertNotNull(result);
+ Assert.assertNotNull(result.getNotifyInfo());
+ Assert.assertEquals(result.getNotifyInfo().getNotifyType().intValue(), 1);
+ Assert.assertEquals(result.getNotifyInfo().getCodeState().intValue(), 0);
+ Assert.assertEquals(result.getNotifyInfo().getCodeExpireTime().longValue(), 1700000000L);
+ }
}
From bcb3110bd7adb76bc4f2c25ba9890b9473ccb5d6 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Mon, 11 May 2026 20:29:31 +0800
Subject: [PATCH 09/38] =?UTF-8?q?:new:=20#3969=20=E3=80=90=E5=B0=8F?=
=?UTF-8?q?=E7=A8=8B=E5=BA=8F=E3=80=91=E5=AE=9E=E7=8E=B0=E5=8A=A0=E5=AF=86?=
=?UTF-8?q?=E7=BD=91=E7=BB=9C=E9=80=9A=E9=81=93=E6=9C=8D=E5=8A=A1=E7=AB=AF?=
=?UTF-8?q?=E6=94=AF=E6=8C=81=EF=BC=8C=E5=B9=B6=E4=BF=AE=E5=A4=8D=20HMAC?=
=?UTF-8?q?=20=E7=AD=BE=E5=90=8D=E4=B8=8E=E9=94=99=E8=AF=AF=E5=A4=84?=
=?UTF-8?q?=E7=90=86=20Bug?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../api/impl/WxMaInternetServiceImpl.java | 5 +-
.../internet/WxMaInternetUserKeyInfo.java | 2 +-
.../wx/miniapp/util/crypt/WxMaCryptUtils.java | 99 +++++++++++++++++
.../util/crypt/WxMaCryptUtilsTest.java | 100 ++++++++++++++++++
4 files changed, 203 insertions(+), 3 deletions(-)
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaInternetServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaInternetServiceImpl.java
index 7da44ddaba..91d11795f3 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaInternetServiceImpl.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaInternetServiceImpl.java
@@ -9,6 +9,7 @@
import me.chanjar.weixin.common.enums.WxType;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
+import org.apache.commons.codec.binary.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@@ -26,7 +27,7 @@ public class WxMaInternetServiceImpl implements WxMaInternetService {
private String sha256(String data, String sessionKey) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
- SecretKeySpec secret_key = new SecretKeySpec(sessionKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+ SecretKeySpec secret_key = new SecretKeySpec(Base64.decodeBase64(sessionKey), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
@@ -57,7 +58,7 @@ public WxMaInternetResponse getUserEncryptKey(String openid, String sessionKey)
private WxMaInternetResponse getWxMaInternetResponse(String url) throws WxErrorException {
String responseContent = this.wxMaService.post(url, "");
WxMaInternetResponse response = WxMaGsonBuilder.create().fromJson(responseContent, WxMaInternetResponse.class);
- if (response.getErrcode() == -1) {
+ if (response.getErrcode() != null && response.getErrcode() != 0) {
throw new WxErrorException(WxError.fromJson(responseContent, WxType.MiniApp));
}
return response;
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/internet/WxMaInternetUserKeyInfo.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/internet/WxMaInternetUserKeyInfo.java
index 01bcfbce0b..305d8687e0 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/internet/WxMaInternetUserKeyInfo.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/internet/WxMaInternetUserKeyInfo.java
@@ -44,7 +44,7 @@ public class WxMaInternetUserKeyInfo implements Serializable {
private Long expireIn;
/**
- * 加密iv
+ * 加密iv(Hex 编码,通常为 32 位十六进制字符,解码后为 16 字节,用于 AES-128-CBC)
*/
private String iv;
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java
index 2343634bfc..252297fdcd 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java
@@ -84,4 +84,103 @@ public static String decryptAnotherWay(String sessionKey, String encryptedData,
}
}
+ /**
+ * 使用用户加密 key 对数据进行 AES-128-CBC 解密(用于小程序加密网络通道).
+ *
+ *
+ * 参考文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/user-encryptkey.html
+ * encryptKey 来自 getUserEncryptKey 接口返回的 encrypt_key 字段(Base64 编码,解码后须为 16 字节)
+ * hexIv 来自 getUserEncryptKey 接口返回的 iv 字段(Hex 编码,须为 32 位十六进制字符,解码后为 16 字节)
+ *
+ *
+ * @param encryptKey 用户加密 key(Base64 编码,解码后须为 16 字节)
+ * @param hexIv 加密 iv(Hex 编码,须为 32 位十六进制字符)
+ * @param encryptedData 加密数据(Base64 编码)
+ * @return 解密后的字符串
+ * @throws IllegalArgumentException 如果 encryptKey 解码后不为 16 字节,或 hexIv 格式非法/解码后不为 16 字节
+ */
+ public static String decryptWithEncryptKey(String encryptKey, String hexIv, String encryptedData) {
+ byte[] keyBytes = Base64.decodeBase64(encryptKey);
+ if (keyBytes.length != 16) {
+ throw new IllegalArgumentException(
+ "encryptKey 解码后必须为 16 字节(AES-128),实际为 " + keyBytes.length + " 字节");
+ }
+ byte[] ivBytes = hexToBytes(hexIv);
+ if (ivBytes.length != 16) {
+ throw new IllegalArgumentException(
+ "hexIv 解码后必须为 16 字节(AES-128-CBC),实际为 " + ivBytes.length + " 字节(需 32 位 Hex 字符串)");
+ }
+ byte[] dataBytes = Base64.decodeBase64(encryptedData);
+ try {
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.DECRYPT_MODE,
+ new SecretKeySpec(keyBytes, "AES"),
+ new IvParameterSpec(ivBytes));
+ return new String(cipher.doFinal(dataBytes), UTF_8);
+ } catch (Exception e) {
+ throw new WxRuntimeException("AES解密失败!", e);
+ }
+ }
+
+ /**
+ * 使用用户加密 key 对数据进行 AES-128-CBC 加密(用于小程序加密网络通道).
+ *
+ *
+ * 参考文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/user-encryptkey.html
+ * encryptKey 来自 getUserEncryptKey 接口返回的 encrypt_key 字段(Base64 编码,解码后须为 16 字节)
+ * hexIv 来自 getUserEncryptKey 接口返回的 iv 字段(Hex 编码,须为 32 位十六进制字符,解码后为 16 字节)
+ *
+ *
+ * @param encryptKey 用户加密 key(Base64 编码,解码后须为 16 字节)
+ * @param hexIv 加密 iv(Hex 编码,须为 32 位十六进制字符)
+ * @param data 待加密的明文字符串
+ * @return 加密后的数据(Base64 编码)
+ * @throws IllegalArgumentException 如果 encryptKey 解码后不为 16 字节,或 hexIv 格式非法/解码后不为 16 字节
+ */
+ public static String encryptWithEncryptKey(String encryptKey, String hexIv, String data) {
+ byte[] keyBytes = Base64.decodeBase64(encryptKey);
+ if (keyBytes.length != 16) {
+ throw new IllegalArgumentException(
+ "encryptKey 解码后必须为 16 字节(AES-128),实际为 " + keyBytes.length + " 字节");
+ }
+ byte[] ivBytes = hexToBytes(hexIv);
+ if (ivBytes.length != 16) {
+ throw new IllegalArgumentException(
+ "hexIv 解码后必须为 16 字节(AES-128-CBC),实际为 " + ivBytes.length + " 字节(需 32 位 Hex 字符串)");
+ }
+ try {
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE,
+ new SecretKeySpec(keyBytes, "AES"),
+ new IvParameterSpec(ivBytes));
+ return Base64.encodeBase64String(cipher.doFinal(data.getBytes(UTF_8)));
+ } catch (Exception e) {
+ throw new WxRuntimeException("AES加密失败!", e);
+ }
+ }
+
+ /**
+ * 将 Hex 字符串转换为字节数组.
+ *
+ * @param hex Hex 字符串(长度必须为偶数,只包含 0-9 和 a-f/A-F 字符)
+ * @return 字节数组
+ * @throws IllegalArgumentException 如果输入不是合法的 Hex 字符串
+ */
+ private static byte[] hexToBytes(String hex) {
+ if (hex == null || hex.length() % 2 != 0) {
+ throw new IllegalArgumentException("无效的十六进制字符串格式:长度必须为偶数");
+ }
+ int len = hex.length();
+ byte[] data = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ int high = Character.digit(hex.charAt(i), 16);
+ int low = Character.digit(hex.charAt(i + 1), 16);
+ if (high == -1 || low == -1) {
+ throw new IllegalArgumentException("无效的十六进制字符串格式:包含非法字符 '" + hex.charAt(high == -1 ? i : i + 1) + "'");
+ }
+ data[i / 2] = (byte) ((high << 4) + low);
+ }
+ return data;
+ }
+
}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtilsTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtilsTest.java
index 76b4e96743..742fa7d440 100644
--- a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtilsTest.java
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtilsTest.java
@@ -4,6 +4,7 @@
import org.testng.annotations.*;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
*
@@ -14,6 +15,11 @@
* @author Binary Wang
*/
public class WxMaCryptUtilsTest {
+ // 模拟来自 getUserEncryptKey 接口返回的 encrypt_key(Base64,解码后 16 字节)
+ // 和 iv(Hex,32 位十六进制字符,解码后 16 字节,AES-128-CBC 要求)
+ private static final String ENCRYPT_KEY = "VI6BpyrK9XH4i4AIGe86tg==";
+ private static final String HEX_IV = "6003f73ec441c3866003f73ec441c386";
+
@Test
public void testDecrypt() {
String sessionKey = "7MG7jbTToVVRWRXVA885rg==";
@@ -32,4 +38,98 @@ public void testDecryptAnotherWay() {
assertThat(WxMaCryptUtils.decrypt(sessionKey, encryptedData, ivStr))
.isEqualTo(WxMaCryptUtils.decryptAnotherWay(sessionKey, encryptedData, ivStr));
}
+
+ /**
+ * 测试使用用户加密 key(来自小程序加密网络通道)进行加密和解密的对称性.
+ * encrypt_key 为 Base64 编码的 16 字节 AES-128 密钥,iv 为 Hex 编码的 16 字节初始向量。
+ */
+ @Test
+ public void testEncryptAndDecryptWithEncryptKey() {
+ String plainText = "{\"userId\":\"12345\",\"amount\":100}";
+
+ String encrypted = WxMaCryptUtils.encryptWithEncryptKey(ENCRYPT_KEY, HEX_IV, plainText);
+ assertThat(encrypted).isNotNull().isNotEmpty();
+
+ String decrypted = WxMaCryptUtils.decryptWithEncryptKey(ENCRYPT_KEY, HEX_IV, encrypted);
+ assertThat(decrypted).isEqualTo(plainText);
+ }
+
+ /**
+ * 测试加密网络通道的加解密对称性(不同明文).
+ */
+ @Test
+ public void testEncryptDecryptSymmetryWithEncryptKey() {
+ String plainText = "hello miniprogram";
+
+ String encrypted = WxMaCryptUtils.encryptWithEncryptKey(ENCRYPT_KEY, HEX_IV, plainText);
+ String decrypted = WxMaCryptUtils.decryptWithEncryptKey(ENCRYPT_KEY, HEX_IV, encrypted);
+ assertThat(decrypted).isEqualTo(plainText);
+ }
+
+ /**
+ * 测试 hexIv 为奇数长度时,应抛出 IllegalArgumentException.
+ */
+ @Test
+ public void testEncryptWithEncryptKeyInvalidHexIvOddLength() {
+ assertThatThrownBy(() -> WxMaCryptUtils.encryptWithEncryptKey(ENCRYPT_KEY, "abc", "data"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("长度必须为偶数");
+ }
+
+ /**
+ * 测试 hexIv 包含非十六进制字符时,应抛出 IllegalArgumentException.
+ */
+ @Test
+ public void testEncryptWithEncryptKeyInvalidHexIvNonHexChar() {
+ // 32 位但含非法字符 'z'
+ assertThatThrownBy(() -> WxMaCryptUtils.encryptWithEncryptKey(
+ ENCRYPT_KEY, "6003f73ec441c3866003f73ec441z386", "data"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("非法字符");
+ }
+
+ /**
+ * 测试 hexIv 解码后不足 16 字节(如仅 16 位 hex = 8 字节)时,应抛出 IllegalArgumentException.
+ */
+ @Test
+ public void testEncryptWithEncryptKeyShortHexIv() {
+ // 16 位 hex = 8 字节,不满足 AES-CBC 要求的 16 字节
+ assertThatThrownBy(() -> WxMaCryptUtils.encryptWithEncryptKey(
+ ENCRYPT_KEY, "6003f73ec441c386", "data"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("hexIv 解码后必须为 16 字节");
+ }
+
+ /**
+ * 测试 encryptKey 解码后不足 16 字节时,应抛出 IllegalArgumentException.
+ */
+ @Test
+ public void testEncryptWithEncryptKeyShortKey() {
+ // Base64 编码的 8 字节 key(不符合 AES-128 要求)
+ String shortKey = java.util.Base64.getEncoder().encodeToString(new byte[8]);
+ assertThatThrownBy(() -> WxMaCryptUtils.encryptWithEncryptKey(shortKey, HEX_IV, "data"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("encryptKey 解码后必须为 16 字节");
+ }
+
+ /**
+ * 测试 decryptWithEncryptKey 使用非法 hexIv 时,应抛出 IllegalArgumentException.
+ */
+ @Test
+ public void testDecryptWithEncryptKeyInvalidHexIv() {
+ assertThatThrownBy(() -> WxMaCryptUtils.decryptWithEncryptKey(ENCRYPT_KEY, "abc", "data"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("长度必须为偶数");
+ }
+
+ /**
+ * 测试 decryptWithEncryptKey encryptKey 长度不合法时,应抛出 IllegalArgumentException.
+ */
+ @Test
+ public void testDecryptWithEncryptKeyShortKey() {
+ String shortKey = java.util.Base64.getEncoder().encodeToString(new byte[8]);
+ assertThatThrownBy(() -> WxMaCryptUtils.decryptWithEncryptKey(shortKey, HEX_IV, "data"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("encryptKey 解码后必须为 16 字节");
+ }
}
From 24703be5831a4404acb7f2d97ab3cea64f6accb4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=B0=B4=E4=BE=9D=E5=AF=92?=
Date: Mon, 11 May 2026 20:32:42 +0800
Subject: [PATCH 10/38] =?UTF-8?q?:art:=20=20#3968=E3=80=90=E5=BE=AE?=
=?UTF-8?q?=E4=BF=A1=E6=94=AF=E4=BB=98=E3=80=91=E4=BF=AE=E5=A4=8D=E5=BE=AE?=
=?UTF-8?q?=E4=BF=A1=E6=94=AF=E4=BB=98api-host-url=E9=85=8D=E7=BD=AE?=
=?UTF-8?q?=E5=8F=8D=E5=90=91=E4=BB=A3=E7=90=86=E8=B7=AF=E5=BE=84=E5=89=8D?=
=?UTF-8?q?=E7=BC=80=E6=97=B6=E4=BC=9A=E5=AF=BC=E8=87=B4v3=E7=AD=BE?=
=?UTF-8?q?=E5=90=8D=E5=BC=82=E5=B8=B8=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../wx-java-pay-solon-plugin/README.md | 2 +
.../pay/config/WxPayAutoConfiguration.java | 1 +
.../pay/properties/WxPayProperties.java | 6 +++
.../README.md | 1 +
.../pay/properties/WxPaySingleProperties.java | 6 +++
.../pay/service/WxPayMultiServicesImpl.java | 1 +
.../wxjava/pay/WxPayMultiServicesTest.java | 7 ++-
.../wx-java-pay-spring-boot-starter/README.md | 2 +
.../pay/config/WxPayAutoConfiguration.java | 1 +
.../pay/properties/WxPayProperties.java | 6 +++
.../wxpay/config/VerifierBuilder.java | 15 ++++++-
.../binarywang/wxpay/config/WxPayConfig.java | 44 +++++++++++++++++--
.../service/impl/BaseWxPayServiceImpl.java | 4 +-
.../wxpay/v3/WxPayV3HttpClientBuilder.java | 24 +++++++++-
.../wxpay/v3/auth/WxPayCredentials.java | 42 +++++++++++++++++-
.../wxpay/config/WxPayConfigTest.java | 11 +++++
16 files changed, 164 insertions(+), 9 deletions(-)
diff --git a/solon-plugins/wx-java-pay-solon-plugin/README.md b/solon-plugins/wx-java-pay-solon-plugin/README.md
index b0e212593b..8ff3416293 100644
--- a/solon-plugins/wx-java-pay-solon-plugin/README.md
+++ b/solon-plugins/wx-java-pay-solon-plugin/README.md
@@ -23,6 +23,8 @@ wx:
pay:
appId: xxxxxxxxxxx
mchId: 15xxxxxxxxx #商户id
+ apiHostUrl: http://10.0.0.1:3128 # 可选:代理主机
+ apiHostUrlPath: /api-weixin # 可选:代理入口前缀
apiV3Key: Dc1DBwSc094jACxxxxxxxxxxxxxxx #V3密钥
certSerialNo: 62C6CEAA360BCxxxxxxxxxxxxxxx
privateKeyPath: classpath:cert/apiclient_key.pem #apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径
diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/config/WxPayAutoConfiguration.java b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/config/WxPayAutoConfiguration.java
index 4043c19e13..c311a099a2 100644
--- a/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/config/WxPayAutoConfiguration.java
+++ b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/config/WxPayAutoConfiguration.java
@@ -59,6 +59,7 @@ public WxPayService wxPayService() {
payConfig.setPublicKeyId(StringUtils.trimToNull(this.properties.getPublicKeyId()));
payConfig.setPublicKeyPath(StringUtils.trimToNull(this.properties.getPublicKeyPath()));
payConfig.setApiHostUrl(StringUtils.trimToNull(this.properties.getApiHostUrl()));
+ payConfig.setApiHostUrlPath(StringUtils.trimToNull(this.properties.getApiHostUrlPath()));
payConfig.setStrictlyNeedWechatPaySerial(this.properties.isStrictlyNeedWechatPaySerial());
payConfig.setFullPublicKeyModel(this.properties.isFullPublicKeyModel());
diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/properties/WxPayProperties.java b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/properties/WxPayProperties.java
index df202a5b84..fe024f59f1 100644
--- a/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/properties/WxPayProperties.java
+++ b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/properties/WxPayProperties.java
@@ -113,6 +113,12 @@ public class WxPayProperties {
*/
private String apiHostUrl;
+ /**
+ * 自定义API主机路径前缀(用于代理入口前缀)
+ * 例如:/api-weixin
+ */
+ private String apiHostUrlPath;
+
/**
* 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加
*/
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/README.md
index 1af617aab8..1ae4ac6299 100644
--- a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/README.md
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/README.md
@@ -255,6 +255,7 @@ public class PayService {
| payScorePermissionNotifyUrl | 支付分授权回调地址 | 无 |
| useSandboxEnv | 是否使用沙箱环境 | false |
| apiHostUrl | 自定义API主机地址 | https://api.mch.weixin.qq.com |
+| apiHostUrlPath | 自定义API主机路径前缀(代理入口前缀) | 空 |
| strictlyNeedWechatPaySerial | 是否所有V3请求都添加序列号头 | true |
| fullPublicKeyModel | 是否完全使用公钥模式 | true |
| publicKeyId | 公钥ID | 无 |
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPaySingleProperties.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPaySingleProperties.java
index 93cdc9dd8f..ef936fc234 100644
--- a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPaySingleProperties.java
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPaySingleProperties.java
@@ -112,6 +112,12 @@ public class WxPaySingleProperties implements Serializable {
*/
private String apiHostUrl;
+ /**
+ * 自定义API主机路径前缀(用于代理入口前缀).
+ * 例如:/api-weixin
+ */
+ private String apiHostUrlPath;
+
/**
* 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加.
*/
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java
index 68f36ccb46..7cbcceabb4 100644
--- a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java
@@ -83,6 +83,7 @@ private WxPayService buildWxPayService(WxPaySingleProperties properties) {
payConfig.setPublicKeyId(StringUtils.trimToNull(properties.getPublicKeyId()));
payConfig.setPublicKeyPath(StringUtils.trimToNull(properties.getPublicKeyPath()));
payConfig.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl()));
+ payConfig.setApiHostUrlPath(StringUtils.trimToNull(properties.getApiHostUrlPath()));
payConfig.setStrictlyNeedWechatPaySerial(properties.isStrictlyNeedWechatPaySerial());
payConfig.setFullPublicKeyModel(properties.isFullPublicKeyModel());
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java
index a64822efd8..d60335ebed 100644
--- a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java
@@ -26,6 +26,8 @@
"wx.pay.configs.app1.notify-url=https://example.com/pay/notify",
"wx.pay.configs.app2.app-id=wx2222222222222222",
"wx.pay.configs.app2.mch-id=2222222222",
+ "wx.pay.configs.app2.api-host-url=http://10.0.0.1:3128",
+ "wx.pay.configs.app2.api-host-url-path=/api-weixin",
"wx.pay.configs.app2.apiv3-key=22222222222222222222222222222222",
"wx.pay.configs.app2.cert-serial-no=2222222222222222",
"wx.pay.configs.app2.private-key-path=classpath:cert/apiclient_key.pem",
@@ -57,7 +59,9 @@ public void testConfiguration() {
assertNotNull(app2Config, "app2 configuration should exist");
assertEquals("wx2222222222222222", app2Config.getAppId());
assertEquals("2222222222", app2Config.getMchId());
- assertEquals("22222222222222222222222222222222", app2Config.getApiV3Key());
+ assertEquals("http://10.0.0.1:3128", app2Config.getApiHostUrl());
+ assertEquals("/api-weixin", app2Config.getApiHostUrlPath());
+ assertEquals("22222222222222222222222222222222", app2Config.getApiv3Key());
}
@Test
@@ -71,6 +75,7 @@ public void testGetWxPayService() {
assertNotNull(app2Service, "Should get WxPayService for app2");
assertEquals("wx2222222222222222", app2Service.getConfig().getAppId());
assertEquals("2222222222", app2Service.getConfig().getMchId());
+ assertEquals("/api-weixin", app2Service.getConfig().getApiHostUrlPath());
// 测试相同key返回相同实例
WxPayService app1ServiceAgain = wxPayMultiServices.getWxPayService("app1");
diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md b/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md
index d87a38fb9c..bed890d5e8 100644
--- a/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md
+++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md
@@ -23,6 +23,8 @@ wx:
pay:
appId: xxxxxxxxxxx
mchId: 15xxxxxxxxx #商户id
+ apiHostUrl: http://10.0.0.1:3128 # 可选:代理主机
+ apiHostUrlPath: /api-weixin # 可选:代理入口前缀
apiV3Key: Dc1DBwSc094jACxxxxxxxxxxxxxxx #V3密钥
certSerialNo: 62C6CEAA360BCxxxxxxxxxxxxxxx
privateKeyPath: classpath:cert/apiclient_key.pem #apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径
diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java
index 0256aec990..7e748ba1a3 100644
--- a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java
+++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java
@@ -63,6 +63,7 @@ public WxPayService wxPayService() {
payConfig.setPublicKeyId(StringUtils.trimToNull(this.properties.getPublicKeyId()));
payConfig.setPublicKeyPath(StringUtils.trimToNull(this.properties.getPublicKeyPath()));
payConfig.setApiHostUrl(StringUtils.trimToNull(this.properties.getApiHostUrl()));
+ payConfig.setApiHostUrlPath(StringUtils.trimToNull(this.properties.getApiHostUrlPath()));
payConfig.setStrictlyNeedWechatPaySerial(this.properties.isStrictlyNeedWechatPaySerial());
payConfig.setFullPublicKeyModel(this.properties.isFullPublicKeyModel());
diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java
index a7308bd6ea..49045c4ee0 100644
--- a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java
+++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java
@@ -111,6 +111,12 @@ public class WxPayProperties {
*/
private String apiHostUrl;
+ /**
+ * 自定义API主机路径前缀(用于代理入口前缀)
+ * 例如:/api-weixin
+ */
+ private String apiHostUrlPath;
+
/**
* 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加
*/
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 bb9d6f7a12..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
@@ -64,6 +64,12 @@ public class WxPayConfig {
*/
private String apiHostUrl = DEFAULT_PAY_BASE_URL;
+ /**
+ * 微信支付接口请求地址路径前缀(用于网关代理前缀).
+ * 例如:/api-weixin
+ */
+ private String apiHostUrlPath;
+
/**
* http请求连接超时时间.
*/
@@ -285,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 路径前缀,不配置时为空字符串
+ */
+ 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;
+ }
- return this.apiHostUrl;
+ /**
+ * 返回用于请求层拼接的基础地址:host + pathPrefix.
+ *
+ * @return 拼接后的基础地址
+ */
+ public String getApiHostWithPathPrefix() {
+ return this.getApiHostUrl() + this.getApiHostUrlPath();
}
@SneakyThrows
@@ -391,10 +428,11 @@ public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
} 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 配置为自定义代理地址时,将代理主机加入受信任列表,
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 6868cb644f..2574e969d7 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
@@ -366,9 +366,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
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 91baa16246..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
@@ -15,6 +15,10 @@
public class WxPayV3HttpClientBuilder extends HttpClientBuilder {
private Credentials credentials;
private Validator validator;
+ /**
+ * 签名前从请求 URI Path 中移除的前缀(用于带路径前缀的代理场景)
+ */
+ private String signUriStripPrefix;
/**
* 额外受信任的主机列表,用于代理转发场景:对这些主机的请求也会携带微信支付 Authorization 头
*/
@@ -40,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;
}
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/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");
From b79206dc8c43493c65a302f171d17a81366953b8 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Mon, 11 May 2026 20:33:43 +0800
Subject: [PATCH 11/38] =?UTF-8?q?:memo:=20=E6=98=8E=E7=A1=AE=E8=AF=B4?=
=?UTF-8?q?=E6=98=8E=20wx-java-cp-multi-spring-boot-starter=20=E4=B8=AD=20?=
=?UTF-8?q?corp-secret=20=E7=9A=84=E9=85=8D=E7=BD=AE=E6=96=B9=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../wx-java-cp-multi-solon-plugin/README.md | 85 ++++++++++--------
.../services/AbstractWxCpConfiguration.java | 12 ++-
.../properties/WxCpSingleProperties.java | 26 +++++-
.../README.md | 89 +++++++++++--------
.../services/AbstractWxCpConfiguration.java | 12 ++-
.../cp/properties/WxCpSingleProperties.java | 26 +++++-
6 files changed, 168 insertions(+), 82 deletions(-)
diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/README.md b/solon-plugins/wx-java-cp-multi-solon-plugin/README.md
index 97bcf0723f..8eb467f98f 100644
--- a/solon-plugins/wx-java-cp-multi-solon-plugin/README.md
+++ b/solon-plugins/wx-java-cp-multi-solon-plugin/README.md
@@ -6,6 +6,25 @@
- 未实现 WxCpTpService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。
- 未实现 WxCpCgService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。
+## 关于 corp-secret 的说明
+
+企业微信中不同功能模块对应不同的 `corp-secret`,每种 Secret 只对对应模块的接口具有调用权限:
+
+| Secret 类型 | 获取位置 | 可调用的接口 | 是否需要 agent-id |
+|---|---|---|---|
+| 自建应用 Secret | 应用管理 → 自建应用 → 选择应用 → 查看 Secret | 该应用有权限的接口 | **必填** |
+| 通讯录同步 Secret | 管理工具 → 通讯录同步 → 查看 Secret | 部门/成员增删改查等通讯录接口 | **不填** |
+| 客户联系 Secret | 客户联系 → API → Secret | 客户联系相关接口 | 不填 |
+
+> **常见问题**:
+> - 使用自建应用 Secret + agent-id 可以获取部门列表,但**无法更新部门**(因为写接口需要通讯录同步权限)
+> - 使用通讯录同步 Secret 可以同步部门,但**调用某些需要 agent-id 的应用接口会报错**
+
+如需同时使用多种权限范围,可在 `wx.cp.corps` 下配置多个条目,每个条目使用对应权限的 Secret,通过不同的 `tenantId` 区分后使用。
+
+> **注意**:
+> 当前插件实现会校验同一 `corp-id` 下的 `agent-id` **必须唯一**,并且 **只能有一个条目不填写 `agent-id`**。
+> 如果在同一 `corp-id` 下同时配置多个未填写 `agent-id` 的条目,会因 token/ticket 缓存 key 冲突而在启动时直接抛异常。
## 快速开始
1. 引入依赖
@@ -18,25 +37,21 @@
```
2. 添加配置(app.properties)
```properties
- # 应用 1 配置
- wx.cp.corps.tenantId1.corp-id = @corp-id
- wx.cp.corps.tenantId1.corp-secret = @corp-secret
+ # 自建应用 1 配置(使用自建应用 Secret,需填写 agent-id)
+ wx.cp.corps.app1.corp-id = @corp-id
+ wx.cp.corps.app1.corp-secret = @自建应用的Secret(在"应用管理-自建应用"中查看)
+ wx.cp.corps.app1.agent-id = @自建应用的AgentId
## 选填
- wx.cp.corps.tenantId1.agent-id = @agent-id
- wx.cp.corps.tenantId1.token = @token
- wx.cp.corps.tenantId1.aes-key = @aes-key
- wx.cp.corps.tenantId1.msg-audit-priKey = @msg-audit-priKey
- wx.cp.corps.tenantId1.msg-audit-lib-path = @msg-audit-lib-path
-
- # 应用 2 配置
- wx.cp.corps.tenantId2.corp-id = @corp-id
- wx.cp.corps.tenantId2.corp-secret = @corp-secret
- ## 选填
- wx.cp.corps.tenantId2.agent-id = @agent-id
- wx.cp.corps.tenantId2.token = @token
- wx.cp.corps.tenantId2.aes-key = @aes-key
- wx.cp.corps.tenantId2.msg-audit-priKey = @msg-audit-priKey
- wx.cp.corps.tenantId2.msg-audit-lib-path = @msg-audit-lib-path
+ wx.cp.corps.app1.token = @token
+ wx.cp.corps.app1.aes-key = @aes-key
+ wx.cp.corps.app1.msg-audit-priKey = @msg-audit-priKey
+ wx.cp.corps.app1.msg-audit-lib-path = @msg-audit-lib-path
+
+ # 通讯录同步配置(使用通讯录同步 Secret,不需要填写 agent-id)
+ # 此配置用于部门、成员的增删改查等通讯录管理操作
+ wx.cp.corps.contact.corp-id = @corp-id
+ wx.cp.corps.contact.corp-secret = @通讯录同步的Secret(在"管理工具-通讯录同步"中查看)
+ ## agent-id 不填,通讯录同步不需要 agentId
# 公共配置
## ConfigStorage 配置(选填)
@@ -59,8 +74,10 @@
```java
import com.binarywang.solon.wxjava.cp_multi.service.WxCpMultiServices;
+import me.chanjar.weixin.cp.api.WxCpDepartmentService;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.api.WxCpUserService;
+import me.chanjar.weixin.cp.bean.WxCpDepart;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Inject;
@@ -70,27 +87,21 @@ public class DemoService {
private WxCpMultiServices wxCpMultiServices;
public void test() {
- // 应用 1 的 WxCpService
- WxCpService wxCpService1 = wxCpMultiServices.getWxCpService("tenantId1");
- WxCpUserService userService1 = wxCpService1.getUserService();
- userService1.getUserId("xxx");
- // todo ...
-
- // 应用 2 的 WxCpService
- WxCpService wxCpService2 = wxCpMultiServices.getWxCpService("tenantId2");
- WxCpUserService userService2 = wxCpService2.getUserService();
- userService2.getUserId("xxx");
+ // 使用自建应用的 WxCpService(对应 corp-secret 为自建应用 Secret)
+ WxCpService appService = wxCpMultiServices.getWxCpService("app1");
+ WxCpUserService userService = appService.getUserService();
+ userService.getUserId("xxx");
// todo ...
- // 应用 3 的 WxCpService
- WxCpService wxCpService3 = wxCpMultiServices.getWxCpService("tenantId3");
- // 判断是否为空
- if (wxCpService3 == null) {
- // todo wxCpService3 为空,请先配置 tenantId3 企业微信应用参数
- return;
- }
- WxCpUserService userService3 = wxCpService3.getUserService();
- userService3.getUserId("xxx");
+ // 使用通讯录同步的 WxCpService(对应 corp-secret 为通讯录同步 Secret)
+ // 通讯录同步 Secret 具有部门/成员增删改查等权限
+ WxCpService contactService = wxCpMultiServices.getWxCpService("contact");
+ WxCpDepartmentService departmentService = contactService.getDepartmentService();
+ // 更新部门示例(WxCpDepart 包含 id、name、parentId 等字段)
+ WxCpDepart depart = new WxCpDepart();
+ depart.setId(100L);
+ depart.setName("新部门名称");
+ departmentService.update(depart);
// todo ...
}
}
diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/AbstractWxCpConfiguration.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/AbstractWxCpConfiguration.java
index ada4ac504c..25b4ab3747 100644
--- a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/AbstractWxCpConfiguration.java
+++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/AbstractWxCpConfiguration.java
@@ -15,6 +15,7 @@
import java.util.Collection;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -37,6 +38,13 @@ protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiPrope
/**
* 校验同一个企业下,agentId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。
*
+ * 同一企业(corpId 相同)下可配置多个条目以使用不同的权限 Secret,例如:
+ *
+ * - 自建应用条目:填写应用对应的 corpSecret 和 agentId
+ * - 通讯录同步条目:填写通讯录同步 Secret,agentId 可不填(null)
+ *
+ * 但同一 corpId 下不允许出现重复的 agentId(包括多个 null)。
+ *
* 查看 {@link me.chanjar.weixin.cp.config.impl.AbstractWxCpInRedisConfigImpl#setAgentId(Integer)}
*/
Collection corpList = corps.values();
@@ -49,8 +57,8 @@ protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiPrope
String corpId = entry.getKey();
// 校验每个企业下,agentId 是否唯一
boolean multi = entry.getValue().stream()
- // 通讯录没有 agentId,如果不判断是否为空,这里会报 NPE 异常
- .collect(Collectors.groupingBy(c -> c.getAgentId() == null ? 0 : c.getAgentId(), Collectors.counting()))
+ // 通讯录没有 agentId,使用字符串转换避免 null 与 agentId=0 冲突
+ .collect(Collectors.groupingBy(c -> Objects.toString(c.getAgentId(), "null"), Collectors.counting()))
.entrySet().stream().anyMatch(e -> e.getValue() > 1);
if (multi) {
throw new RuntimeException("请确保企业微信配置唯一性[" + corpId + "]");
diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpSingleProperties.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpSingleProperties.java
index e761a09062..6f7f633c3f 100644
--- a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpSingleProperties.java
+++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpSingleProperties.java
@@ -8,6 +8,16 @@
/**
* 企业微信企业相关配置属性
*
+ * 企业微信中不同的 corpSecret 对应不同的权限范围,常见的有:
+ *
+ * - 自建应用 Secret:在"应用管理 - 自建应用"中查看,只能调用该应用有权限的接口
+ * - 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,用于管理部门和成员(增删改查)
+ * - 客户联系 Secret:在"客户联系"中查看,用于客户联系相关接口
+ *
+ * 如需同时使用多种权限范围(例如:既要操作通讯录,又要调用自建应用接口),
+ * 可在 {@code wx.cp.corps} 下配置多个条目,每个条目使用对应权限的 {@code corpSecret},
+ * 其中通讯录同步的条目无需填写 {@code agentId}。
+ *
* @author yl
* created on 2023/10/16
*/
@@ -20,7 +30,16 @@ public class WxCpSingleProperties implements Serializable {
*/
private String corpId;
/**
- * 微信企业号 corpSecret
+ * 微信企业号 corpSecret(权限密钥)
+ *
+ * 企业微信针对不同的功能模块提供了不同的 Secret,每种 Secret 只对对应模块的接口有调用权限:
+ *
+ * - 自建应用 Secret:在"应用管理 - 自建应用"中找到对应应用,查看其 Secret,
+ * 使用时需同时配置对应的 {@code agentId}
+ * - 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,
+ * 使用此 Secret 可管理部门、成员,无需配置 {@code agentId}
+ * - 其他 Secret(客户联系等):根据需要在企业微信后台查看对应 Secret
+ *
*/
private String corpSecret;
/**
@@ -28,7 +47,10 @@ public class WxCpSingleProperties implements Serializable {
*/
private String token;
/**
- * 微信企业号应用 ID
+ * 微信企业号应用 ID(AgentId)
+ *
+ * 使用自建应用 Secret 时,需要填写对应应用的 AgentId。
+ * 使用通讯录同步 Secret 时,无需填写此字段。
*/
private Integer agentId;
/**
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/README.md
index e3ea7bf0f8..0f0b74695e 100644
--- a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/README.md
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/README.md
@@ -6,6 +6,29 @@
- 未实现 WxCpTpService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。
- 未实现 WxCpCgService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。
+## 关于 corp-secret 的说明
+
+企业微信中不同功能模块对应不同的 `corp-secret`,每种 Secret 只对对应模块的接口具有调用权限:
+
+| Secret 类型 | 获取位置 | 可调用的接口 | 是否需要 agent-id |
+|---|---|---|---|
+| 自建应用 Secret | 应用管理 → 自建应用 → 选择应用 → 查看 Secret | 该应用有权限的接口 | **必填** |
+| 通讯录同步 Secret | 管理工具 → 通讯录同步 → 查看 Secret | 部门/成员增删改查等通讯录接口 | **不填** |
+| 客户联系 Secret | 客户联系 → API → Secret | 客户联系相关接口 | 不填 |
+
+> **常见问题**:
+> - 使用自建应用 Secret + agent-id 可以获取部门列表,但**无法更新部门**(因为写接口需要通讯录同步权限)
+> - 使用通讯录同步 Secret 可以同步部门,但**调用某些需要 agent-id 的应用接口会报错**
+
+如需同时使用多种权限范围,可在 `wx.cp.corps` 下配置多个条目,每个条目使用对应权限的 Secret,通过不同的 `tenantId` 区分后使用。
+
+> **配置限制说明**:
+> - 当前 starter 实现会校验:同一 `corp-id` 下,`agent-id` **必须唯一**
+> - 同一 `corp-id` 下,**只能有一个条目不填 `agent-id`**
+> - 否则会因为 token/ticket 缓存 key 冲突而在启动时直接抛异常
+>
+> 因此,像"通讯录同步 Secret""客户联系 Secret"这类通常不填写 `agent-id` 的配置,**不能**在同一个 `corp-id` 下同时配置多个 `agent-id` 均为空的条目;如确有多个条目,请确保其中最多只有一个未填写 `agent-id`。
+
## 快速开始
1. 引入依赖
@@ -18,25 +41,21 @@
```
2. 添加配置(application.properties)
```properties
- # 应用 1 配置
- wx.cp.corps.tenantId1.corp-id = @corp-id
- wx.cp.corps.tenantId1.corp-secret = @corp-secret
- ## 选填
- wx.cp.corps.tenantId1.agent-id = @agent-id
- wx.cp.corps.tenantId1.token = @token
- wx.cp.corps.tenantId1.aes-key = @aes-key
- wx.cp.corps.tenantId1.msg-audit-priKey = @msg-audit-priKey
- wx.cp.corps.tenantId1.msg-audit-lib-path = @msg-audit-lib-path
-
- # 应用 2 配置
- wx.cp.corps.tenantId2.corp-id = @corp-id
- wx.cp.corps.tenantId2.corp-secret = @corp-secret
+ # 自建应用 1 配置(使用自建应用 Secret,需填写 agent-id)
+ wx.cp.corps.app1.corp-id = @corp-id
+ wx.cp.corps.app1.corp-secret = @自建应用的Secret(在"应用管理-自建应用"中查看)
+ wx.cp.corps.app1.agent-id = @自建应用的AgentId
## 选填
- wx.cp.corps.tenantId2.agent-id = @agent-id
- wx.cp.corps.tenantId2.token = @token
- wx.cp.corps.tenantId2.aes-key = @aes-key
- wx.cp.corps.tenantId2.msg-audit-priKey = @msg-audit-priKey
- wx.cp.corps.tenantId2.msg-audit-lib-path = @msg-audit-lib-path
+ wx.cp.corps.app1.token = @token
+ wx.cp.corps.app1.aes-key = @aes-key
+ wx.cp.corps.app1.msg-audit-priKey = @msg-audit-priKey
+ wx.cp.corps.app1.msg-audit-lib-path = @msg-audit-lib-path
+
+ # 通讯录同步配置(使用通讯录同步 Secret,不需要填写 agent-id)
+ # 此配置用于部门、成员的增删改查等通讯录管理操作
+ wx.cp.corps.contact.corp-id = @corp-id
+ wx.cp.corps.contact.corp-secret = @通讯录同步的Secret(在"管理工具-通讯录同步"中查看)
+ ## agent-id 不填,通讯录同步不需要 agentId
# 公共配置
## ConfigStorage 配置(选填)
@@ -59,8 +78,10 @@
```java
import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServices;
+import me.chanjar.weixin.cp.api.WxCpDepartmentService;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.api.WxCpUserService;
+import me.chanjar.weixin.cp.bean.WxCpDepart;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -70,27 +91,21 @@ public class DemoService {
private WxCpMultiServices wxCpMultiServices;
public void test() {
- // 应用 1 的 WxCpService
- WxCpService wxCpService1 = wxCpMultiServices.getWxCpService("tenantId1");
- WxCpUserService userService1 = wxCpService1.getUserService();
- userService1.getUserId("xxx");
- // todo ...
-
- // 应用 2 的 WxCpService
- WxCpService wxCpService2 = wxCpMultiServices.getWxCpService("tenantId2");
- WxCpUserService userService2 = wxCpService2.getUserService();
- userService2.getUserId("xxx");
+ // 使用自建应用的 WxCpService(对应 corp-secret 为自建应用 Secret)
+ WxCpService appService = wxCpMultiServices.getWxCpService("app1");
+ WxCpUserService userService = appService.getUserService();
+ userService.getUserId("xxx");
// todo ...
- // 应用 3 的 WxCpService
- WxCpService wxCpService3 = wxCpMultiServices.getWxCpService("tenantId3");
- // 判断是否为空
- if (wxCpService3 == null) {
- // todo wxCpService3 为空,请先配置 tenantId3 企业微信应用参数
- return;
- }
- WxCpUserService userService3 = wxCpService3.getUserService();
- userService3.getUserId("xxx");
+ // 使用通讯录同步的 WxCpService(对应 corp-secret 为通讯录同步 Secret)
+ // 通讯录同步 Secret 具有部门/成员增删改查等权限
+ WxCpService contactService = wxCpMultiServices.getWxCpService("contact");
+ WxCpDepartmentService departmentService = contactService.getDepartmentService();
+ // 更新部门示例(WxCpDepart 包含 id、name、parentId 等字段)
+ WxCpDepart depart = new WxCpDepart();
+ depart.setId(100L);
+ depart.setName("新部门名称");
+ departmentService.update(depart);
// todo ...
}
}
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpConfiguration.java
index 9b959222e0..a10bdf9bed 100644
--- a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpConfiguration.java
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpConfiguration.java
@@ -18,6 +18,7 @@
import java.util.Collection;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -40,6 +41,13 @@ protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiPrope
/**
* 校验同一个企业下,agentId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。
*
+ * 同一企业(corpId 相同)下可配置多个条目以使用不同的权限 Secret,例如:
+ *
+ * - 自建应用条目:填写应用对应的 corpSecret 和 agentId
+ * - 通讯录同步条目:填写通讯录同步 Secret,agentId 可不填(null)
+ *
+ * 但同一 corpId 下不允许出现重复的 agentId(包括多个 null)。
+ *
* 查看 {@link me.chanjar.weixin.cp.config.impl.AbstractWxCpInRedisConfigImpl#setAgentId(Integer)}
*/
Collection corpList = corps.values();
@@ -52,8 +60,8 @@ protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiPrope
String corpId = entry.getKey();
// 校验每个企业下,agentId 是否唯一
boolean multi = entry.getValue().stream()
- // 通讯录没有 agentId,如果不判断是否为空,这里会报 NPE 异常
- .collect(Collectors.groupingBy(c -> c.getAgentId() == null ? 0 : c.getAgentId(), Collectors.counting()))
+ // 通讯录没有 agentId,使用字符串转换避免 null 与 agentId=0 冲突
+ .collect(Collectors.groupingBy(c -> Objects.toString(c.getAgentId(), "null"), Collectors.counting()))
.entrySet().stream().anyMatch(e -> e.getValue() > 1);
if (multi) {
throw new RuntimeException("请确保企业微信配置唯一性[" + corpId + "]");
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpSingleProperties.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpSingleProperties.java
index 8ad7149fe6..fcfa654a15 100644
--- a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpSingleProperties.java
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpSingleProperties.java
@@ -8,6 +8,16 @@
/**
* 企业微信企业相关配置属性
*
+ * 企业微信中不同的 corpSecret 对应不同的权限范围,常见的有:
+ *
+ * - 自建应用 Secret:在"应用管理 - 自建应用"中查看,只能调用该应用有权限的接口
+ * - 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,用于管理部门和成员(增删改查)
+ * - 客户联系 Secret:在"客户联系"中查看,用于客户联系相关接口
+ *
+ * 如需同时使用多种权限范围(例如:既要操作通讯录,又要调用自建应用接口),
+ * 可在 {@code wx.cp.corps} 下配置多个条目,每个条目使用对应权限的 {@code corpSecret},
+ * 其中通讯录同步的条目无需填写 {@code agentId}。
+ *
* @author yl
* created on 2023/10/16
*/
@@ -20,7 +30,16 @@ public class WxCpSingleProperties implements Serializable {
*/
private String corpId;
/**
- * 微信企业号 corpSecret
+ * 微信企业号 corpSecret(权限密钥)
+ *
+ * 企业微信针对不同的功能模块提供了不同的 Secret,每种 Secret 只对对应模块的接口有调用权限:
+ *
+ * - 自建应用 Secret:在"应用管理 - 自建应用"中找到对应应用,查看其 Secret,
+ * 使用时需同时配置对应的 {@code agentId}
+ * - 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,
+ * 使用此 Secret 可管理部门、成员,无需配置 {@code agentId}
+ * - 其他 Secret(客户联系等):根据需要在企业微信后台查看对应 Secret
+ *
*/
private String corpSecret;
/**
@@ -28,7 +47,10 @@ public class WxCpSingleProperties implements Serializable {
*/
private String token;
/**
- * 微信企业号应用 ID
+ * 微信企业号应用 ID(AgentId)
+ *
+ * 使用自建应用 Secret 时,需要填写对应应用的 AgentId。
+ * 使用通讯录同步 Secret 时,无需填写此字段。
*/
private Integer agentId;
/**
From 025430f4e657ba6d80f6a26d97a59aab39cae55c Mon Sep 17 00:00:00 2001
From: Binary Wang
Date: Mon, 11 May 2026 21:10:40 +0800
Subject: [PATCH 12/38] =?UTF-8?q?:bookmark:=20=E5=8F=91=E5=B8=83=204.8.3.B?=
=?UTF-8?q?=20=E6=B5=8B=E8=AF=95=E7=89=88=E6=9C=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 2 +-
solon-plugins/pom.xml | 2 +-
solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-channel-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-cp-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-miniapp-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-mp-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-open-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-pay-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-qidian-solon-plugin/pom.xml | 2 +-
spring-boot-starters/pom.xml | 2 +-
.../wx-java-channel-multi-spring-boot-starter/pom.xml | 2 +-
.../wx-java-channel-spring-boot-starter/pom.xml | 2 +-
.../wx-java-cp-multi-spring-boot-starter/pom.xml | 2 +-
spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml | 2 +-
.../wx-java-cp-tp-multi-spring-boot-starter/pom.xml | 2 +-
.../wx-java-miniapp-multi-spring-boot-starter/pom.xml | 2 +-
.../wx-java-miniapp-spring-boot-starter/pom.xml | 2 +-
.../wx-java-mp-multi-spring-boot-starter/pom.xml | 2 +-
spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml | 2 +-
.../wx-java-open-multi-spring-boot-starter/pom.xml | 2 +-
spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml | 2 +-
.../wx-java-pay-multi-spring-boot-starter/pom.xml | 2 +-
.../spring/starter/wxjava/pay/WxPayMultiServicesTest.java | 2 +-
spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml | 2 +-
spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml | 2 +-
weixin-graal/pom.xml | 2 +-
weixin-java-channel/pom.xml | 2 +-
weixin-java-common/pom.xml | 2 +-
weixin-java-cp/pom.xml | 2 +-
weixin-java-miniapp/pom.xml | 2 +-
weixin-java-mp/pom.xml | 2 +-
weixin-java-open/pom.xml | 2 +-
weixin-java-pay/pom.xml | 2 +-
weixin-java-qidian/pom.xml | 2 +-
wx-java-bom/pom.xml | 2 +-
39 files changed, 39 insertions(+), 39 deletions(-)
diff --git a/pom.xml b/pom.xml
index 3f4fc56610..905e582443 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
com.github.binarywang
wx-java
- 4.8.2.B
+ 4.8.3.B
pom
WxJava - Weixin/Wechat Java SDK
微信开发Java SDK
diff --git a/solon-plugins/pom.xml b/solon-plugins/pom.xml
index 9a375a60cc..87401a2c97 100644
--- a/solon-plugins/pom.xml
+++ b/solon-plugins/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.2.B
+ 4.8.3.B
pom
wx-java-solon-plugins
diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml
index 908e3957ee..d99f9a67c1 100644
--- a/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/solon-plugins/wx-java-channel-solon-plugin/pom.xml b/solon-plugins/wx-java-channel-solon-plugin/pom.xml
index 1e3f457cfe..a26072f8c4 100644
--- a/solon-plugins/wx-java-channel-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-channel-solon-plugin/pom.xml
@@ -3,7 +3,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml
index c0d1dcc180..9ccd05578b 100644
--- a/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml
@@ -4,7 +4,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/solon-plugins/wx-java-cp-solon-plugin/pom.xml b/solon-plugins/wx-java-cp-solon-plugin/pom.xml
index b8d2c43351..367d2a338c 100644
--- a/solon-plugins/wx-java-cp-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-cp-solon-plugin/pom.xml
@@ -4,7 +4,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml
index 6ca319ad7f..9ea8b7caff 100644
--- a/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml b/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml
index 28f80f00b1..0651e3b9b5 100644
--- a/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml
@@ -4,7 +4,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml
index f78f9b5d59..4dc7eae667 100644
--- a/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/solon-plugins/wx-java-mp-solon-plugin/pom.xml b/solon-plugins/wx-java-mp-solon-plugin/pom.xml
index 6ca5283c18..e0c79f79bf 100644
--- a/solon-plugins/wx-java-mp-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-mp-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/solon-plugins/wx-java-open-solon-plugin/pom.xml b/solon-plugins/wx-java-open-solon-plugin/pom.xml
index dcd856dc26..4cd4b1ac56 100644
--- a/solon-plugins/wx-java-open-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-open-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/solon-plugins/wx-java-pay-solon-plugin/pom.xml b/solon-plugins/wx-java-pay-solon-plugin/pom.xml
index 26e0b7faca..607c138fd3 100644
--- a/solon-plugins/wx-java-pay-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-pay-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/pom.xml b/solon-plugins/wx-java-qidian-solon-plugin/pom.xml
index cb0caaa1e4..f83c8a8066 100644
--- a/solon-plugins/wx-java-qidian-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-qidian-solon-plugin/pom.xml
@@ -3,7 +3,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/spring-boot-starters/pom.xml b/spring-boot-starters/pom.xml
index ff2ce88236..07a1226e6f 100644
--- a/spring-boot-starters/pom.xml
+++ b/spring-boot-starters/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.2.B
+ 4.8.3.B
pom
wx-java-spring-boot-starters
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml
index de7a389532..c3c3441c9b 100644
--- a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml
index 9f22f79503..f74d3bfaae 100644
--- a/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml
@@ -3,7 +3,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml
index 514a67b3ec..0cb592a7fc 100644
--- a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml
@@ -4,7 +4,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml
index df23601c73..881064d493 100644
--- a/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml
@@ -4,7 +4,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml
index fa0b98aabf..b3bd632cad 100644
--- a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml
@@ -4,7 +4,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml
index 05f595ac26..744ba094a1 100644
--- a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml
index 25d5f66758..1088b711e7 100644
--- a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml
@@ -4,7 +4,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml
index 88b11099a3..de88f187a7 100644
--- a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml
index 9e95574bc2..672cf2e35c 100644
--- a/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml
index c5cf07e799..dea66a5a35 100644
--- a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml
index 72c856f27c..22dbd864df 100644
--- a/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml
index 1964bcbbfe..c416b5ba40 100644
--- a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java
index d60335ebed..87132fdcf3 100644
--- a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java
@@ -61,7 +61,7 @@ public void testConfiguration() {
assertEquals("2222222222", app2Config.getMchId());
assertEquals("http://10.0.0.1:3128", app2Config.getApiHostUrl());
assertEquals("/api-weixin", app2Config.getApiHostUrlPath());
- assertEquals("22222222222222222222222222222222", app2Config.getApiv3Key());
+ assertEquals("22222222222222222222222222222222", app2Config.getApiV3Key());
}
@Test
diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml
index ecdb925730..3c1313bc22 100644
--- a/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml
index 7e314df780..d9b845adb1 100644
--- a/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml
@@ -3,7 +3,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/weixin-graal/pom.xml b/weixin-graal/pom.xml
index a55cc19226..9c23e95add 100644
--- a/weixin-graal/pom.xml
+++ b/weixin-graal/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.2.B
+ 4.8.3.B
weixin-graal
diff --git a/weixin-java-channel/pom.xml b/weixin-java-channel/pom.xml
index 68edb075ce..0d332daa20 100644
--- a/weixin-java-channel/pom.xml
+++ b/weixin-java-channel/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.2.B
+ 4.8.3.B
weixin-java-channel
diff --git a/weixin-java-common/pom.xml b/weixin-java-common/pom.xml
index 98b7ddda62..ad124f8052 100644
--- a/weixin-java-common/pom.xml
+++ b/weixin-java-common/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.2.B
+ 4.8.3.B
weixin-java-common
diff --git a/weixin-java-cp/pom.xml b/weixin-java-cp/pom.xml
index a75b7b034e..d9d8694352 100644
--- a/weixin-java-cp/pom.xml
+++ b/weixin-java-cp/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.8.2.B
+ 4.8.3.B
weixin-java-cp
diff --git a/weixin-java-miniapp/pom.xml b/weixin-java-miniapp/pom.xml
index 2a1b5e4f1a..ca426c4e8b 100644
--- a/weixin-java-miniapp/pom.xml
+++ b/weixin-java-miniapp/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.8.2.B
+ 4.8.3.B
weixin-java-miniapp
diff --git a/weixin-java-mp/pom.xml b/weixin-java-mp/pom.xml
index 487728ea42..b21ac9bb26 100644
--- a/weixin-java-mp/pom.xml
+++ b/weixin-java-mp/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.8.2.B
+ 4.8.3.B
weixin-java-mp
diff --git a/weixin-java-open/pom.xml b/weixin-java-open/pom.xml
index 6fa96d8aea..f7701d1809 100644
--- a/weixin-java-open/pom.xml
+++ b/weixin-java-open/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.8.2.B
+ 4.8.3.B
weixin-java-open
diff --git a/weixin-java-pay/pom.xml b/weixin-java-pay/pom.xml
index c9037cdf7f..80893a55ab 100644
--- a/weixin-java-pay/pom.xml
+++ b/weixin-java-pay/pom.xml
@@ -5,7 +5,7 @@
com.github.binarywang
wx-java
- 4.8.2.B
+ 4.8.3.B
4.0.0
diff --git a/weixin-java-qidian/pom.xml b/weixin-java-qidian/pom.xml
index b98ca26e41..b7aa187817 100644
--- a/weixin-java-qidian/pom.xml
+++ b/weixin-java-qidian/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.8.2.B
+ 4.8.3.B
weixin-java-qidian
diff --git a/wx-java-bom/pom.xml b/wx-java-bom/pom.xml
index 793d4e09ea..e7f3920dc2 100644
--- a/wx-java-bom/pom.xml
+++ b/wx-java-bom/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.2.B
+ 4.8.3.B
wx-java-bom
From 79307f762837585533a501f8117f2edd5e711d5f Mon Sep 17 00:00:00 2001
From: buaazyl
Date: Wed, 13 May 2026 10:11:41 +0800
Subject: [PATCH 13/38] =?UTF-8?q?:art:=20#3998=20=E3=80=90=E5=BE=AE?=
=?UTF-8?q?=E4=BF=A1=E6=94=AF=E4=BB=98=E3=80=91=E4=BF=AE=E6=AD=A3=E5=90=88?=
=?UTF-8?q?=E4=BD=9C=E4=BC=99=E4=BC=B4=E8=AE=A2=E9=98=85=E9=80=9A=E7=9F=A5?=
=?UTF-8?q?=E8=A7=A3=E5=AF=86=E5=90=8E=E6=B6=88=E6=81=AF=E4=BD=93=E6=95=B0?=
=?UTF-8?q?=E6=8D=AE=E7=BB=93=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../notify/PartnerSubscribeNotifyResult.java | 83 ++++++++++++++-----
1 file changed, 61 insertions(+), 22 deletions(-)
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/PartnerSubscribeNotifyResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/PartnerSubscribeNotifyResult.java
index 52917ddf17..1e0bf27ead 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/PartnerSubscribeNotifyResult.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/PartnerSubscribeNotifyResult.java
@@ -32,35 +32,74 @@ public class PartnerSubscribeNotifyResult implements Serializable,
@NoArgsConstructor
public static class DecryptNotifyResult implements Serializable {
private static final long serialVersionUID = 1L;
+
/**
- * 商户号
- */
- @SerializedName("merchant_code")
- private String merchantCode;
- /**
- * 商户全称
- */
- @SerializedName("merchant_company_name")
- private String merchantCompanyName;
- /**
- * 业务发生时间
+ * 消息内容
*/
- @SerializedName("business_time")
- private String businessTime;
+ @SerializedName("message_content")
+ private MessageContent messageContent;
/**
- * 业务单据
+ * 主题名称
*/
- @SerializedName("business_code")
- private String businessCode;
+ @SerializedName("topic_name")
+ private TopicName topicName;
+
/**
- * 业务状态
+ * 消息内容
*/
- @SerializedName("business_state")
- private String businessState;
+ @Data
+ @NoArgsConstructor
+ public static class MessageContent implements Serializable {
+ private static final long serialVersionUID = 1L;
+ /**
+ * 商户号
+ */
+ @SerializedName("merchant_code")
+ private String merchantCode;
+ /**
+ * 商户全称
+ */
+ @SerializedName("merchant_company_name")
+ private String merchantCompanyName;
+ /**
+ * 业务发生时间
+ */
+ @SerializedName("business_time")
+ private String businessTime;
+ /**
+ * 业务单据
+ */
+ @SerializedName("business_code")
+ private String businessCode;
+ /**
+ * 业务状态
+ */
+ @SerializedName("business_state")
+ private String businessState;
+ /**
+ * 备注
+ */
+ @SerializedName("remark")
+ private String remark;
+ }
+
/**
- * 备注
+ * 主题名称
*/
- @SerializedName("remark")
- private String remark;
+ @Data
+ @NoArgsConstructor
+ public static class TopicName implements Serializable {
+ private static final long serialVersionUID = 1L;
+ /**
+ * 主题英文名
+ */
+ @SerializedName("topic_english_name")
+ private String topicEnglishName;
+ /**
+ * 主题中文名
+ */
+ @SerializedName("topic_chinese_name")
+ private String topicChineseName;
+ }
}
}
From b882c673d999a68de54a1416bb1b3c171e31e43a Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Wed, 20 May 2026 10:22:12 +0800
Subject: [PATCH 14/38] =?UTF-8?q?:art:=20#4013=20=E3=80=90=E5=B0=8F?=
=?UTF-8?q?=E7=A8=8B=E5=BA=8F=E3=80=91=E4=BF=AE=E5=A4=8D=E5=BF=AB=E9=80=92?=
=?UTF-8?q?=E4=B8=8B=E5=8D=95=E6=8E=A5=E5=8F=A3=E8=AF=B7=E6=B1=82=E9=87=8C?=
=?UTF-8?q?=E7=9A=84=E4=BF=9D=E4=BB=B7=E5=AD=97=E6=AE=B5=E8=A2=AB=20`final?=
=?UTF-8?q?`=20=E5=9B=BA=E5=AE=9A=E5=AF=BC=E8=87=B4=E6=97=A0=E6=B3=95?=
=?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../request/WxMaExpressOrderInsured.java | 5 +-
.../request/WxMaExpressOrderInsuredTest.java | 56 +++++++++++++++++++
2 files changed, 59 insertions(+), 2 deletions(-)
create mode 100644 weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/express/request/WxMaExpressOrderInsuredTest.java
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/express/request/WxMaExpressOrderInsured.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/express/request/WxMaExpressOrderInsured.java
index d7ccda9aec..074b65382e 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/express/request/WxMaExpressOrderInsured.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/express/request/WxMaExpressOrderInsured.java
@@ -30,7 +30,8 @@ public class WxMaExpressOrderInsured implements Serializable {
*
*/
@SerializedName("use_insured")
- private final Integer useInsured = WxMaConstants.OrderAddInsured.INSURED_PROGRAM;
+ @Builder.Default
+ private Integer useInsured = WxMaConstants.OrderAddInsured.INSURED_PROGRAM;
/**
* 保价金额
@@ -41,6 +42,6 @@ public class WxMaExpressOrderInsured implements Serializable {
*/
@SerializedName("insured_value")
@Builder.Default
- private final Integer insuredValue = WxMaConstants.OrderAddInsured.DEFAULT_INSURED_VALUE;
+ private Integer insuredValue = WxMaConstants.OrderAddInsured.DEFAULT_INSURED_VALUE;
}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/express/request/WxMaExpressOrderInsuredTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/express/request/WxMaExpressOrderInsuredTest.java
new file mode 100644
index 0000000000..f308398147
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/express/request/WxMaExpressOrderInsuredTest.java
@@ -0,0 +1,56 @@
+package cn.binarywang.wx.miniapp.bean.express.request;
+
+import cn.binarywang.wx.miniapp.constant.WxMaConstants;
+import cn.binarywang.wx.miniapp.json.WxMaGsonBuilder;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public class WxMaExpressOrderInsuredTest {
+
+ @Test
+ public void testDefaultValueWithNoArgsConstructor() {
+ WxMaExpressOrderInsured insured = new WxMaExpressOrderInsured();
+
+ assertEquals(insured.getUseInsured(), Integer.valueOf(WxMaConstants.OrderAddInsured.INSURED_PROGRAM));
+ assertEquals(insured.getInsuredValue(), Integer.valueOf(WxMaConstants.OrderAddInsured.DEFAULT_INSURED_VALUE));
+ }
+
+ @Test
+ public void testCanModifyInsuredConfigBySetter() {
+ WxMaExpressOrderInsured insured = new WxMaExpressOrderInsured();
+ insured.setUseInsured(WxMaConstants.OrderAddInsured.USE_INSURED);
+ insured.setInsuredValue(10000);
+
+ assertEquals(insured.getUseInsured(), Integer.valueOf(WxMaConstants.OrderAddInsured.USE_INSURED));
+ assertEquals(insured.getInsuredValue(), Integer.valueOf(10000));
+ }
+
+ @Test
+ public void testBuilderSupportsCustomInsuredConfig() {
+ WxMaExpressOrderInsured insured = WxMaExpressOrderInsured.builder()
+ .useInsured(WxMaConstants.OrderAddInsured.USE_INSURED)
+ .insuredValue(5000)
+ .build();
+
+ assertEquals(insured.getUseInsured(), Integer.valueOf(WxMaConstants.OrderAddInsured.USE_INSURED));
+ assertEquals(insured.getInsuredValue(), Integer.valueOf(5000));
+
+ String json = WxMaGsonBuilder.create().toJson(insured);
+ assertTrue(json.contains("\"use_insured\":1"));
+ assertTrue(json.contains("\"insured_value\":5000"));
+ }
+
+ @Test
+ public void testBuilderDefaultsWhenNoFieldSet() {
+ WxMaExpressOrderInsured insured = WxMaExpressOrderInsured.builder().build();
+
+ assertEquals(insured.getUseInsured(), Integer.valueOf(WxMaConstants.OrderAddInsured.INSURED_PROGRAM));
+ assertEquals(insured.getInsuredValue(), Integer.valueOf(WxMaConstants.OrderAddInsured.DEFAULT_INSURED_VALUE));
+
+ String json = WxMaGsonBuilder.create().toJson(insured);
+ assertTrue(json.contains("\"use_insured\":0"));
+ assertTrue(json.contains("\"insured_value\":0"));
+ }
+}
From 2dc2da061ed76820864541f47bc977be0cedd9e4 Mon Sep 17 00:00:00 2001
From: Yixuan Xu <109468061+mzl2233@users.noreply.github.com>
Date: Wed, 20 May 2026 10:23:31 +0800
Subject: [PATCH 15/38] =?UTF-8?q?:art:=20#4001=20=E3=80=90=E5=BE=AE?=
=?UTF-8?q?=E4=BF=A1=E6=94=AF=E4=BB=98=E3=80=91=E4=B8=BA=E6=99=AE=E9=80=9A?=
=?UTF-8?q?=E5=95=86=E6=88=B7=E7=89=88=E9=80=80=E6=AC=BE=E8=AF=B7=E6=B1=82?=
=?UTF-8?q?=E5=AF=B9=E8=B1=A1=E7=B1=BB=E8=A1=A5=E9=BD=90=E4=B8=8E=E6=9C=8D?=
=?UTF-8?q?=E5=8A=A1=E5=95=86=E7=89=88=E4=B8=80=E8=87=B4=E7=9A=84=E2=80=9C?=
=?UTF-8?q?=E9=80=80=E6=AC=BE=E8=B5=84=E9=87=91=E6=9D=A5=E6=BA=90/?=
=?UTF-8?q?=E5=87=BA=E8=B5=84=E8=B4=A6=E6=88=B7=E6=98=8E=E7=BB=86=E2=80=9D?=
=?UTF-8?q?=E5=8F=82=E6=95=B0=EF=BC=8C=E4=BB=A5=E6=94=AF=E6=8C=81=E5=9C=A8?=
=?UTF-8?q?=20V3=20=E9=80=80=E6=AC=BE=E7=94=B3=E8=AF=B7=E6=97=B6=E6=8C=87?=
=?UTF-8?q?=E5=AE=9A=E9=80=80=E6=AC=BE=E6=9D=A5=E6=BA=90=E8=B5=84=E9=87=91?=
=?UTF-8?q?=E8=B4=A6=E6=88=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../bean/request/WxPayRefundV3Request.java | 65 +++++++++++++++++++
.../request/WxPayRefundV3RequestTest.java | 56 ++++++++++++++++
2 files changed, 121 insertions(+)
create mode 100644 weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/request/WxPayRefundV3RequestTest.java
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/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);
+ }
+}
From 93017a5ae0cc629a42c59be43c0d11429855e4c0 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Wed, 20 May 2026 21:38:13 +0800
Subject: [PATCH 16/38] =?UTF-8?q?:art:=20#4011=20=E3=80=90=E4=BC=81?=
=?UTF-8?q?=E4=B8=9A=E5=BE=AE=E4=BF=A1=E3=80=91=E5=A2=9E=E5=8A=A0=E6=99=BA?=
=?UTF-8?q?=E8=83=BD=E6=9C=BA=E5=99=A8=E4=BA=BA=20API=20=E6=A8=A1=E5=BC=8F?=
=?UTF-8?q?=20JSON=20=E5=9B=9E=E8=B0=83=E6=B6=88=E6=81=AF=E7=B1=BB?=
=?UTF-8?q?=E5=92=8C=E7=9B=B8=E5=BA=94=E7=9A=84=E8=A7=A3=E6=9E=90=E8=B0=83?=
=?UTF-8?q?=E7=94=A8=E6=96=B9=E6=B3=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
weixin-java-cp/INTELLIGENT_ROBOT.md | 12 +-
.../cp/api/WxCpIntelligentRobotService.java | 10 +-
.../impl/WxCpIntelligentRobotServiceImpl.java | 7 +-
.../WxCpIntelligentRobotMessage.java | 196 ++++++++++++++++++
.../WxCpIntelligentRobotServiceImplTest.java | 13 +-
.../WxCpIntelligentRobotMessageTest.java | 76 +++++++
6 files changed, 310 insertions(+), 4 deletions(-)
create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/intelligentrobot/WxCpIntelligentRobotMessage.java
create mode 100644 weixin-java-cp/src/test/java/me/chanjar/weixin/cp/bean/intelligentrobot/WxCpIntelligentRobotMessageTest.java
diff --git a/weixin-java-cp/INTELLIGENT_ROBOT.md b/weixin-java-cp/INTELLIGENT_ROBOT.md
index dcd90e1a1a..18dd0c677f 100644
--- a/weixin-java-cp/INTELLIGENT_ROBOT.md
+++ b/weixin-java-cp/INTELLIGENT_ROBOT.md
@@ -109,6 +109,16 @@ String fromUser = message.getFromUserName(); // 发送用户
// ...
```
+对于智能机器人 API 模式的 JSON 回调消息,可使用 `WxCpIntelligentRobotMessage` 解析:
+
+```java
+WxCpIntelligentRobotMessage callbackMessage =
+ robotService.parseCallbackMessage(jsonBody);
+String botId = callbackMessage.getAiBotId();
+String userId = callbackMessage.getFrom().getUserid();
+String msgType = callbackMessage.getMsgType();
+```
+
### 删除智能机器人
```java
@@ -146,4 +156,4 @@ robotService.deleteRobot(robotId);
1. 需要确保企业微信应用具有智能机器人相关权限
2. 智能机器人功能可能需要特定的企业微信版本支持
3. 会话ID可以用于保持对话的连续性,提升用户体验
-4. 机器人状态: 0表示停用,1表示启用
\ No newline at end of file
+4. 机器人状态: 0表示停用,1表示启用
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpIntelligentRobotService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpIntelligentRobotService.java
index bc5f3f1915..58f4373ceb 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpIntelligentRobotService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpIntelligentRobotService.java
@@ -74,4 +74,12 @@ public interface WxCpIntelligentRobotService {
*/
WxCpIntelligentRobotSendMessageResponse sendMessage(WxCpIntelligentRobotSendMessageRequest request) throws WxErrorException;
-}
\ No newline at end of file
+ /**
+ * 解析智能机器人 API 模式回调消息.
+ *
+ * @param callbackMessageJson 回调消息JSON
+ * @return 解析后的回调消息对象
+ */
+ WxCpIntelligentRobotMessage parseCallbackMessage(String callbackMessageJson);
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImpl.java
index 8a12fa4ff4..aba1ee85c4 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImpl.java
@@ -67,4 +67,9 @@ public WxCpIntelligentRobotSendMessageResponse sendMessage(WxCpIntelligentRobotS
return WxCpIntelligentRobotSendMessageResponse.fromJson(responseText);
}
-}
\ No newline at end of file
+ @Override
+ public WxCpIntelligentRobotMessage parseCallbackMessage(String callbackMessageJson) {
+ return WxCpIntelligentRobotMessage.fromJson(callbackMessageJson);
+ }
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/intelligentrobot/WxCpIntelligentRobotMessage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/intelligentrobot/WxCpIntelligentRobotMessage.java
new file mode 100644
index 0000000000..d485b59d69
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/intelligentrobot/WxCpIntelligentRobotMessage.java
@@ -0,0 +1,196 @@
+package me.chanjar.weixin.cp.bean.intelligentrobot;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 企业微信智能机器人回调消息.
+ *
+ * 官方文档: https://developer.work.weixin.qq.com/document/path/100719
+ */
+@Data
+public class WxCpIntelligentRobotMessage implements Serializable {
+ private static final long serialVersionUID = -1L;
+
+ /**
+ * 本次回调的唯一性标志.
+ */
+ @SerializedName("msgid")
+ private String msgId;
+
+ /**
+ * 智能机器人id.
+ */
+ @SerializedName("aibotid")
+ private String aiBotId;
+
+ /**
+ * 会话id,仅群聊类型时返回.
+ */
+ @SerializedName("chatid")
+ private String chatId;
+
+ /**
+ * 会话类型,single/group.
+ */
+ @SerializedName("chattype")
+ private String chatType;
+
+ /**
+ * 消息发送者.
+ */
+ @SerializedName("from")
+ private From from;
+
+ /**
+ * 支持主动回复消息的临时url.
+ */
+ @SerializedName("response_url")
+ private String responseUrl;
+
+ /**
+ * 消息类型.
+ */
+ @SerializedName("msgtype")
+ private String msgType;
+
+ @SerializedName("text")
+ private Text text;
+
+ @SerializedName("image")
+ private Image image;
+
+ @SerializedName("mixed")
+ private Mixed mixed;
+
+ @SerializedName("voice")
+ private Voice voice;
+
+ @SerializedName("file")
+ private FileInfo file;
+
+ @SerializedName("video")
+ private Video video;
+
+ @SerializedName("quote")
+ private Quote quote;
+
+ @SerializedName("stream")
+ private Stream stream;
+
+ public static WxCpIntelligentRobotMessage fromJson(String json) {
+ return WxCpGsonBuilder.create().fromJson(json, WxCpIntelligentRobotMessage.class);
+ }
+
+ public String toJson() {
+ return WxCpGsonBuilder.create().toJson(this);
+ }
+
+ @Data
+ public static class From implements Serializable {
+ private static final long serialVersionUID = -1L;
+
+ @SerializedName("userid")
+ private String userid;
+ }
+
+ @Data
+ public static class Text implements Serializable {
+ private static final long serialVersionUID = -1L;
+
+ @SerializedName("content")
+ private String content;
+ }
+
+ @Data
+ public static class Image implements Serializable {
+ private static final long serialVersionUID = -1L;
+
+ @SerializedName("url")
+ private String url;
+ }
+
+ @Data
+ public static class Voice implements Serializable {
+ private static final long serialVersionUID = -1L;
+
+ @SerializedName("content")
+ private String content;
+ }
+
+ @Data
+ public static class FileInfo implements Serializable {
+ private static final long serialVersionUID = -1L;
+
+ @SerializedName("url")
+ private String url;
+ }
+
+ @Data
+ public static class Video implements Serializable {
+ private static final long serialVersionUID = -1L;
+
+ @SerializedName("url")
+ private String url;
+ }
+
+ @Data
+ public static class Stream implements Serializable {
+ private static final long serialVersionUID = -1L;
+
+ @SerializedName("id")
+ private String id;
+ }
+
+ @Data
+ public static class Mixed implements Serializable {
+ private static final long serialVersionUID = -1L;
+
+ @SerializedName("msg_item")
+ private List msgItem;
+ }
+
+ @Data
+ public static class MixedItem implements Serializable {
+ private static final long serialVersionUID = -1L;
+
+ @SerializedName("msgtype")
+ private String msgType;
+
+ @SerializedName("text")
+ private Text text;
+
+ @SerializedName("image")
+ private Image image;
+ }
+
+ @Data
+ public static class Quote implements Serializable {
+ private static final long serialVersionUID = -1L;
+
+ @SerializedName("msgtype")
+ private String msgType;
+
+ @SerializedName("text")
+ private Text text;
+
+ @SerializedName("image")
+ private Image image;
+
+ @SerializedName("mixed")
+ private Mixed mixed;
+
+ @SerializedName("voice")
+ private Voice voice;
+
+ @SerializedName("file")
+ private FileInfo file;
+
+ @SerializedName("video")
+ private Video video;
+ }
+}
diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImplTest.java
index 85104ee73a..843bf6aac0 100644
--- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImplTest.java
+++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImplTest.java
@@ -119,4 +119,15 @@ public void testSendMessageResponse() {
assert response.getSessionId().equals("session123");
assert response.getErrcode() == 0;
}
-}
\ No newline at end of file
+
+ @Test
+ public void testParseCallbackMessage() {
+ String callbackJson = "{\"msgid\":\"msg_1\",\"aibotid\":\"bot_1\",\"chattype\":\"single\","
+ + "\"from\":{\"userid\":\"user_1\"},\"msgtype\":\"text\",\"text\":{\"content\":\"hello\"}}";
+ WxCpIntelligentRobotMessage message = this.wxCpService.getIntelligentRobotService().parseCallbackMessage(callbackJson);
+ assert message.getMsgId().equals("msg_1");
+ assert message.getAiBotId().equals("bot_1");
+ assert message.getFrom().getUserid().equals("user_1");
+ assert message.getText().getContent().equals("hello");
+ }
+}
diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/bean/intelligentrobot/WxCpIntelligentRobotMessageTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/bean/intelligentrobot/WxCpIntelligentRobotMessageTest.java
new file mode 100644
index 0000000000..f8f8791d0d
--- /dev/null
+++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/bean/intelligentrobot/WxCpIntelligentRobotMessageTest.java
@@ -0,0 +1,76 @@
+package me.chanjar.weixin.cp.bean.intelligentrobot;
+
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+/**
+ * 智能机器人回调消息测试.
+ */
+public class WxCpIntelligentRobotMessageTest {
+
+ @Test
+ public void testFromJsonWithTextMessage() {
+ String json = "{"
+ + "\"msgid\":\"msg_1\","
+ + "\"aibotid\":\"bot_1\","
+ + "\"chatid\":\"chat_1\","
+ + "\"chattype\":\"group\","
+ + "\"from\":{\"userid\":\"zhangsan\"},"
+ + "\"response_url\":\"https://example.com/reply\","
+ + "\"msgtype\":\"text\","
+ + "\"text\":{\"content\":\"@robot hello\"}"
+ + "}";
+
+ WxCpIntelligentRobotMessage message = WxCpIntelligentRobotMessage.fromJson(json);
+ assertEquals(message.getMsgId(), "msg_1");
+ assertEquals(message.getAiBotId(), "bot_1");
+ assertEquals(message.getChatId(), "chat_1");
+ assertEquals(message.getChatType(), "group");
+ assertNotNull(message.getFrom());
+ assertEquals(message.getFrom().getUserid(), "zhangsan");
+ assertEquals(message.getResponseUrl(), "https://example.com/reply");
+ assertEquals(message.getMsgType(), "text");
+ assertNotNull(message.getText());
+ assertEquals(message.getText().getContent(), "@robot hello");
+ assertNull(message.getMixed());
+ assertNull(message.getStream());
+ }
+
+ @Test
+ public void testFromJsonWithMixedAndQuote() {
+ String json = "{"
+ + "\"msgid\":\"msg_2\","
+ + "\"aibotid\":\"bot_2\","
+ + "\"chattype\":\"single\","
+ + "\"from\":{\"userid\":\"lisi\"},"
+ + "\"msgtype\":\"mixed\","
+ + "\"mixed\":{\"msg_item\":["
+ + "{\"msgtype\":\"text\",\"text\":{\"content\":\"hello\"}},"
+ + "{\"msgtype\":\"image\",\"image\":{\"url\":\"https://example.com/1.png\"}}"
+ + "]},"
+ + "\"quote\":{\"msgtype\":\"text\",\"text\":{\"content\":\"quoted\"}}"
+ + "}";
+
+ WxCpIntelligentRobotMessage message = WxCpIntelligentRobotMessage.fromJson(json);
+ assertEquals(message.getMsgType(), "mixed");
+ assertNotNull(message.getMixed());
+ assertNotNull(message.getMixed().getMsgItem());
+ assertEquals(message.getMixed().getMsgItem().size(), 2);
+ assertEquals(message.getMixed().getMsgItem().get(0).getMsgType(), "text");
+ assertEquals(message.getMixed().getMsgItem().get(0).getText().getContent(), "hello");
+ assertEquals(message.getMixed().getMsgItem().get(1).getMsgType(), "image");
+ assertEquals(message.getMixed().getMsgItem().get(1).getImage().getUrl(), "https://example.com/1.png");
+ assertNotNull(message.getQuote());
+ assertEquals(message.getQuote().getMsgType(), "text");
+ assertEquals(message.getQuote().getText().getContent(), "quoted");
+
+ String serialized = message.toJson();
+ WxCpIntelligentRobotMessage deserialized = WxCpIntelligentRobotMessage.fromJson(serialized);
+ assertEquals(deserialized.getAiBotId(), "bot_2");
+ assertEquals(deserialized.getFrom().getUserid(), "lisi");
+ assertEquals(deserialized.getMixed().getMsgItem().size(), 2);
+ }
+}
From 0a5704772fb1e63e3b828cc63bb8e2cb1078043b Mon Sep 17 00:00:00 2001
From: buaazyl
Date: Fri, 22 May 2026 09:10:13 +0800
Subject: [PATCH 17/38] =?UTF-8?q?:art:=20#4014=20=E3=80=90=E5=BE=AE?=
=?UTF-8?q?=E4=BF=A1=E6=94=AF=E4=BB=98=E3=80=91=E7=BB=9F=E4=B8=80=E6=94=B6?=
=?UTF-8?q?=E4=BB=98=E9=80=9A=E4=B8=8B=E5=8D=95=E5=85=B3=E5=8D=95=E6=8E=A5?=
=?UTF-8?q?=E5=8F=A3=E5=B9=B6=E6=9B=B4=E6=96=B0=E6=94=B6=E4=BB=98=E9=80=9A?=
=?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=96=87=E6=A1=A3=E5=9C=B0=E5=9D=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../CombineTransactionsNotifyResult.java | 29 -
.../ecommerce/CombineTransactionsRequest.java | 459 -------------
.../ecommerce/CombineTransactionsResult.java | 353 ----------
.../PartnerTransactionsCloseRequest.java | 62 --
.../PartnerTransactionsNotifyResult.java | 27 -
.../PartnerTransactionsQueryRequest.java | 69 --
.../ecommerce/PartnerTransactionsRequest.java | 646 ------------------
.../ecommerce/PartnerTransactionsResult.java | 600 ----------------
.../wxpay/bean/ecommerce/SignatureHeader.java | 41 --
.../bean/ecommerce/TransactionsResult.java | 126 ----
.../bean/ecommerce/enums/TradeTypeEnum.java | 37 -
.../WxPayPartnerUnifiedOrderV3Request.java | 14 +
.../bean/result/enums/TradeTypeEnum.java | 8 +-
.../wxpay/service/BusinessCircleService.java | 2 +-
.../wxpay/service/EcommerceService.java | 153 +++--
.../wxpay/service/MarketingFavorService.java | 2 +-
.../wxpay/service/PartnerPayScoreService.java | 2 +-
.../PartnerPayScoreSignPlanService.java | 2 +-
.../wxpay/service/PayScoreService.java | 2 +-
.../wxpay/service/WxPayService.java | 13 +-
.../service/impl/BaseWxPayServiceImpl.java | 47 +-
.../impl/BusinessCircleServiceImpl.java | 18 +-
.../service/impl/EcommerceServiceImpl.java | 129 +---
.../impl/MarketingFavorServiceImpl.java | 17 +-
.../impl/PartnerPayScoreServiceImpl.java | 20 +-
.../PartnerPayScoreSignPlanServiceImpl.java | 20 +-
.../service/impl/PayScoreServiceImpl.java | 20 +-
.../impl/BusinessCircleServiceImplTest.java | 10 +-
.../impl/EcommerceServiceImplTest.java | 28 +-
29 files changed, 209 insertions(+), 2747 deletions(-)
delete mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/CombineTransactionsNotifyResult.java
delete mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/CombineTransactionsRequest.java
delete mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/CombineTransactionsResult.java
delete mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsCloseRequest.java
delete mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsNotifyResult.java
delete mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsQueryRequest.java
delete mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsRequest.java
delete mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsResult.java
delete mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SignatureHeader.java
delete mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/TransactionsResult.java
delete mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/enums/TradeTypeEnum.java
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/CombineTransactionsNotifyResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/CombineTransactionsNotifyResult.java
deleted file mode 100644
index dcfae88247..0000000000
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/CombineTransactionsNotifyResult.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.github.binarywang.wxpay.bean.ecommerce;
-
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.io.Serializable;
-
-/**
- * 合单支付 通知结果
- *
- * 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/combine/chapter3_7.shtml
- *
- */
-@Data
-@NoArgsConstructor
-public class CombineTransactionsNotifyResult implements Serializable {
-
- private static final long serialVersionUID = -4710926828683593250L;
- /**
- * 源数据
- */
- private NotifyResponse rawData;
-
- /**
- * 解密后的数据
- */
- private CombineTransactionsResult result;
-
-}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/CombineTransactionsRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/CombineTransactionsRequest.java
deleted file mode 100644
index 3f285285ae..0000000000
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/CombineTransactionsRequest.java
+++ /dev/null
@@ -1,459 +0,0 @@
-package com.github.binarywang.wxpay.bean.ecommerce;
-
-import com.google.gson.annotations.SerializedName;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.io.Serializable;
-import java.util.List;
-
-/**
- * 合单支付API
- *
- * 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/e-combine.shtml
- *
- */
-@Data
-@NoArgsConstructor
-public class CombineTransactionsRequest implements Serializable {
- private static final long serialVersionUID = -1242741645939606441L;
- /**
- *
- * 字段名:合单商户appid
- * 变量名:combine_appid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 合单发起方的appid。
- * 示例值:wxd678efh567hg6787
- *
- */
- @SerializedName(value = "combine_appid")
- private String combineAppid;
-
- /**
- *
- * 字段名:合单商户号
- * 变量名:combine_mchid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 合单发起方商户号。
- * 示例值:1900000109
- *
- */
- @SerializedName(value = "combine_mchid")
- private String combineMchid;
-
- /**
- *
- * 字段名:合单商户订单号
- * 变量名:combine_out_trade_no
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 合单支付总订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。
- * 示例值:P20150806125346
- *
- */
- @SerializedName(value = "combine_out_trade_no")
- private String combineOutTradeNo;
-
- /**
- *
- * 字段名:+场景信息
- * 变量名:scene_info
- * 是否必填:否
- * 类型:object
- * 描述:支付场景信息描述
- *
- */
- @SerializedName(value = "scene_info")
- private SceneInfo sceneInfo;
-
- /**
- *
- * 字段名:+子单信息
- * 变量名:sub_orders
- * 是否必填:是
- * 类型:array
- * 描述:
- * 最多支持子单条数:50
- *
- *
- */
- @SerializedName(value = "sub_orders")
- private List subOrders;
-
- /**
- *
- * 字段名:+支付者
- * 变量名:combine_payer_info
- * 是否必填:否(JSAPI必填)
- * 类型:object
- * 描述:支付者信息
- *
- */
- @SerializedName(value = "combine_payer_info")
- private CombinePayerInfo combinePayerInfo;
-
- /**
- *
- * 字段名:交易起始时间
- * 变量名:time_start
- * 是否必填:否
- * 类型:string(14)
- * 描述:
- * 订单生成时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。
- * 示例值:2019-12-31T15:59:60+08:00
- *
- */
- @SerializedName(value = "time_start")
- private String timeStart;
-
- /**
- *
- * 字段名:交易结束时间
- * 变量名:time_expire
- * 是否必填:否
- * 类型:string(14)
- * 描述:
- * 订单失效时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。
- * 示例值:2019-12-31T15:59:60+08:00
- *
- */
- @SerializedName(value = "time_expire")
- private String timeExpire;
-
- /**
- *
- * 字段名:通知地址
- * 变量名:notify_url
- * 是否必填:是
- * 类型:string(256)
- * 描述:
- * 接收微信支付异步通知回调地址,通知url必须为直接可访问的URL,不能携带参数。
- * 格式: URL
- * 示例值:https://yourapp.com/notify
- *
- */
- @SerializedName(value = "notify_url")
- private String notifyUrl;
-
-
- @Data
- @NoArgsConstructor
- public static class SceneInfo implements Serializable {
- /**
- *
- * 字段名:商户端设备号
- * 变量名:device_id
- * 是否必填:否
- * 类型:string(16)
- * 描述:
- * 终端设备号(门店号或收银设备ID)。
- * 特殊规则:长度最小7个字节
- * 示例值:POS1:1
- *
- */
- @SerializedName(value = "device_id")
- private String deviceId;
-
- /**
- *
- * 字段名:用户终端IP
- * 变量名:payer_client_ip
- * 是否必填:是
- * 类型:string(45)
- * 描述:
- * 用户端实际ip
- * 格式: ip(ipv4+ipv6)
- * 示例值:14.17.22.32
- *
- */
- @SerializedName(value = "payer_client_ip")
- private String payerClientIp;
-
- /**
- *
- * 字段名:H5场景信息
- * 变量名:h5_info
- * 是否必填:否(H5支付必填)
- * 类型:object
- * 描述:
- * H5场景信息
- *
- */
- @SerializedName(value = "h5_info")
- private H5Info h5Info;
- }
-
- @Data
- @NoArgsConstructor
- public static class SubOrders implements Serializable {
- /**
- *
- * 字段名:子单商户号
- * 变量名:mchid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 子单发起方商户号,必须与发起方appid有绑定关系。
- * 示例值:1900000109
- * 此处一般填写服务商商户号
- *
- */
- @SerializedName(value = "mchid")
- private String mchid;
-
- /**
- *
- * 字段名:附加信息
- * 变量名:attach
- * 是否必填:是
- * 类型:string(128)
- * 描述:
- * 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。
- * 示例值:深圳分店
- *
- */
- @SerializedName(value = "attach")
- private String attach;
-
- /**
- *
- * 字段名:+订单金额
- * 变量名:amount
- * 是否必填:是
- * 类型:object
- * 描述:
- *
- */
- @SerializedName(value = "amount")
- private Amount amount;
-
- /**
- *
- * 字段名:子单商户订单号
- * 变量名:out_trade_no
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。
- * 特殊规则:最小字符长度为6
- * 示例值:20150806125346
- *
- */
- @SerializedName(value = "out_trade_no")
- private String outTradeNo;
-
- /**
- *
- * 字段名:二级商户号
- * 变量名:sub_mchid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 二级商户商户号,由微信支付生成并下发。
- * 注意:仅适用于电商平台 服务商
- * 示例值:1900000109
- *
- */
- @SerializedName(value = "sub_mchid")
- private String subMchid;
-
- /**
- *
- * 字段名:商品描述
- * 变量名:description
- * 是否必填:是
- * 类型:string(128)
- * 描述:
- * 商品简单描述。需传入应用市场上的APP名字-实际商品名称,例如:天天爱消除-游戏充值。
- * 示例值:腾讯充值中心-QQ会员充值
- *
- */
- @SerializedName(value = "description")
- private String description;
-
- /**
- *
- * 字段名:+结算信息
- * 变量名:settle_info
- * 是否必填:否
- * 类型:Object
- * 描述:结算信息
- *
- */
- @SerializedName(value = "settle_info")
- private SettleInfo settleInfo;
-
- }
-
- @Data
- @NoArgsConstructor
- public static class CombinePayerInfo implements Serializable {
- /**
- *
- * 字段名:用户标识
- * 变量名:openid
- * 是否必填:是
- * 类型:string(128)
- * 描述:
- * 使用合单appid获取的对应用户openid。是用户在商户appid下的唯一标识。
- * 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
- *
- */
- @SerializedName(value = "openid")
- private String openid;
-
- }
-
- @Data
- @NoArgsConstructor
- public static class Amount implements Serializable {
- /**
- *
- * 字段名:标价金额
- * 变量名:total_amount
- * 是否必填:是
- * 类型:int64
- * 描述:
- * 子单金额,单位为分。
- * 示例值:100
- *
- */
- @SerializedName(value = "total_amount")
- private Integer totalAmount;
-
- /**
- *
- * 字段名:标价币种
- * 变量名:currency
- * 是否必填:是
- * 类型:string(8)
- * 描述:
- * 符合ISO 4217标准的三位字母代码,人民币:CNY。
- * 示例值:CNY
- *
- */
- @SerializedName(value = "currency")
- private String currency;
-
- }
-
- @Data
- @NoArgsConstructor
- public static class SettleInfo implements Serializable {
- /**
- *
- * 字段名:是否指定分账
- * 变量名:profit_sharing
- * 是否必填:否
- * 类型:bool
- * 描述:
- * 是否分账,与外层profit_sharing同时存在时,以本字段为准。
- * true:是
- * false:否
- * 示例值:true
- *
- */
- @SerializedName(value = "profit_sharing")
- private Boolean profitSharing;
-
- /**
- *
- * 字段名:补差金额
- * 变量名:subsidy_amount
- * 是否必填:否
- * 类型:int64
- * 描述:
- * SettleInfo.profit_sharing为true时,该金额才生效。
- * 示例值:10
- *
- */
- @SerializedName(value = "subsidy_amount")
- private Integer subsidyAmount;
-
- }
-
- @Data
- @NoArgsConstructor
- public static class H5Info implements Serializable {
-
- /**
- *
- * 字段名:场景类型
- * 变量名:type
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 场景类型,枚举值:
- * iOS:IOS移动应用;
- * Android:安卓移动应用;
- * Wap:WAP网站应用;
- * 示例值:iOS
- *
- */
- @SerializedName(value = "type")
- private String type;
-
- /**
- *
- * 字段名:应用名称
- * 变量名:app_name
- * 是否必填:否
- * 类型:string(64)
- * 描述:
- * 应用名称
- * 示例值:王者荣耀
- *
- */
- @SerializedName(value = "app_name")
- private String appName;
-
- /**
- *
- * 字段名:网站URL
- * 变量名:app_url
- * 是否必填:否
- * 类型:string(128)
- * 描述:
- * 网站URL
- * 示例值:https://pay.qq.com
- *
- */
- @SerializedName(value = "app_url")
- private String appUrl;
-
- /**
- *
- * 字段名:iOS平台BundleID
- * 变量名:bundle_id
- * 是否必填:否
- * 类型:string(128)
- * 描述:
- * iOS平台BundleID
- * 示例值:com.tencent.wzryiOS
- *
- */
- @SerializedName(value = "bundle_id")
- private String bundleId;
-
- /**
- *
- * 字段名:Android平台PackageName
- * 变量名:package_name
- * 是否必填:否
- * 类型:string(128)
- * 描述:
- * Android平台PackageName
- * 示例值:com.tencent.tmgp.sgame
- *
- */
- @SerializedName(value = "package_name")
- private String packageName;
-
- }
-
-}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/CombineTransactionsResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/CombineTransactionsResult.java
deleted file mode 100644
index 1b929ed96f..0000000000
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/CombineTransactionsResult.java
+++ /dev/null
@@ -1,353 +0,0 @@
-package com.github.binarywang.wxpay.bean.ecommerce;
-
-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/wiki/doc/apiv3/wxpay/pay/combine/chapter3_3.shtml
- *
- */
-@Data
-@NoArgsConstructor
-public class CombineTransactionsResult implements Serializable {
-
- /**
- *
- * 字段名:合单商户appid
- * 变量名:combine_appid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 合单发起方的appid。(即电商平台appid)
- * 示例值:wxd678efh567hg6787
- *
- */
- @SerializedName(value = "combine_appid")
- private String combineAppid;
-
- /**
- *
- * 字段名:合单商户号
- * 变量名:combine_mchid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 合单发起方商户号。(即电商平台mchid)
- * 示例值:1900000109
- *
- */
- @SerializedName(value = "combine_mchid")
- private String combineMchid;
-
- /**
- *
- * 字段名:合单商户订单号
- * 变量名:combine_out_trade_no
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 合单支付总订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。
- * 示例值:P20150806125346
- *
- */
- @SerializedName(value = "combine_out_trade_no")
- private String combineOutTradeNo;
-
- /**
- *
- * 字段名:+场景信息
- * 变量名:scene_info
- * 是否必填:否
- * 类型:object
- * 描述:支付场景信息描述
- *
- */
- @SerializedName(value = "scene_info")
- private SceneInfo sceneInfo;
-
- /**
- *
- * 字段名:+子单信息
- * 变量名:sub_orders
- * 是否必填:是
- * 类型:array
- * 描述:
- * 最多支持子单条数:50
- *
- *
- */
- @SerializedName(value = "sub_orders")
- private List subOrders;
-
- /**
- *
- * 字段名:+支付者
- * 变量名:combine_payer_info
- * 是否必填:否
- * 类型:object
- * 描述:示例值:见请求示例
- *
- */
- @SerializedName(value = "combine_payer_info")
- private CombinePayerInfo combinePayerInfo;
-
- @Data
- @NoArgsConstructor
- public static class SubOrders implements Serializable {
- /**
- *
- * 字段名:子单商户号
- * 变量名:mchid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 子单发起方商户号,必须与发起方Appid有绑定关系。(即电商平台mchid)
- * 示例值:1900000109
- *
- */
- @SerializedName(value = "mchid")
- private String mchid;
-
- /**
- *
- * 字段名:交易类型
- * 变量名:trade_type
- * 是否必填:是
- * 类型:string (16)
- * 描述:
- * 枚举值:
- * NATIVE:扫码支付
- * JSAPI:公众号支付
- * APP:APP支付
- * MWEB:H5支付
- * 示例值: JSAPI
- *
- */
- @SerializedName(value = "trade_type")
- private String tradeType;
-
- /**
- *
- * 字段名:交易状态
- * 变量名:trade_state
- * 是否必填:是
- * 类型:string (32)
- * 描述:
- * 枚举值:
- * SUCCESS:支付成功
- * REFUND:转入退款
- * NOTPAY:未支付
- * CLOSED:已关闭
- * USERPAYING:用户支付中
- * PAYERROR:支付失败(其他原因,如银行返回失败)
- * 示例值: SUCCESS
- *
- */
- @SerializedName(value = "trade_state")
- private String tradeState;
-
- /**
- *
- * 字段名:付款银行
- * 变量名:bank_type
- * 是否必填:否
- * 类型:string(16)
- * 描述:
- * 银行类型,采用字符串类型的银行标识。
- * 示例值:CMC
- *
- */
- @SerializedName(value = "bank_type")
- private String bankType;
-
- /**
- *
- * 字段名:附加信息
- * 变量名:attach
- * 是否必填:是
- * 类型:string(128)
- * 描述:
- * 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。
- * 示例值:深圳分店
- *
- */
- @SerializedName(value = "attach")
- private String attach;
-
- /**
- *
- * 字段名:支付完成时间
- * 变量名:success_time
- * 是否必填:是
- * 类型:string(16)
- * 描述:
- * 订单支付时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss:sss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss:sss表示时分秒毫秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35.120+08:00表示,北京时间2015年5月20日 13点29分35秒。
- * 示例值:2015-05-20T13:29:35.120+08:00
- *
- */
- @SerializedName(value = "success_time")
- private String successTime;
-
- /**
- *
- * 字段名:微信订单号
- * 变量名:transaction_id
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 微信支付订单号。
- * 示例值: 1009660380201506130728806387
- *
- */
- @SerializedName(value = "transaction_id")
- private String transactionId;
-
- /**
- *
- * 字段名:子单商户订单号
- * 变量名:out_trade_no
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。
- * 特殊规则:最小字符长度为6
- * 示例值:20150806125346
- *
- */
- @SerializedName(value = "out_trade_no")
- private String outTradeNo;
-
- /**
- *
- * 字段名:二级商户号
- * 变量名:sub_mchid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 二级商户商户号,由微信支付生成并下发。
- * 注意:仅适用于电商平台 服务商
- * 示例值:1900000109
- *
- */
- @SerializedName(value = "sub_mchid")
- private String subMchid;
-
- /**
- *
- * 字段名:+订单金额
- * 变量名:amount
- * 是否必填:是
- * 类型:object
- * 描述:订单金额信息
- *
- */
- @SerializedName(value = "amount")
- private Amount amount;
-
- }
-
- @Data
- @NoArgsConstructor
- public static class SceneInfo implements Serializable {
- /**
- *
- * 字段名:商户端设备号
- * 变量名:device_id
- * 是否必填:否
- * 类型:string(16)
- * 描述:
- * 终端设备号(门店号或收银设备ID)。
- * 特殊规则:长度最小7个字节
- * 示例值:POS1:1
- *
- */
- @SerializedName(value = "device_id")
- private String deviceId;
-
- }
-
- @Data
- @NoArgsConstructor
- public static class CombinePayerInfo implements Serializable {
- /**
- *
- * 字段名:用户标识
- * 变量名:openid
- * 是否必填:是
- * 类型:string(128)
- * 描述:
- * 使用合单appid获取的对应用户openid。是用户在商户appid下的唯一标识。
- * 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
- *
- */
- @SerializedName(value = "openid")
- private String openid;
-
- }
-
- @Data
- @NoArgsConstructor
- public static class Amount implements Serializable {
- /**
- *
- * 字段名:标价金额
- * 变量名:total_amount
- * 是否必填:是
- * 类型:int64
- * 描述:
- * 子单金额,单位为分。
- * 示例值:100
- *
- */
- @SerializedName(value = "total_amount")
- private Integer totalAmount;
-
- /**
- *
- * 字段名:标价币种
- * 变量名:currency
- * 是否必填:是
- * 类型:string(8)
- * 描述:
- * 符合ISO 4217标准的三位字母代码,人民币:CNY。
- * 示例值:CNY
- *
- */
- @SerializedName(value = "currency")
- private String currency;
-
- /**
- *
- * 字段名:现金支付金额
- * 变量名:payer_amount
- * 是否必填:是
- * 类型:int64
- * 描述:
- * 订单现金支付金额。
- * 示例值:10
- *
- */
- @SerializedName(value = "payer_amount")
- private Integer payerAmount;
-
- /**
- *
- * 字段名:现金支付币种
- * 变量名:payer_currency
- * 是否必填:是
- * 类型:string(8)
- * 描述:
- * 货币类型,符合ISO 4217标准的三位字母代码,默认人民币:CNY。
- * 示例值: CNY
- *
- */
- @SerializedName(value = "payer_currency")
- private String payerCurrency;
- }
-}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsCloseRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsCloseRequest.java
deleted file mode 100644
index c09c1aede6..0000000000
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsCloseRequest.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.github.binarywang.wxpay.bean.ecommerce;
-
-import com.google.gson.annotations.SerializedName;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.io.Serializable;
-
-/**
- * 关闭普通订单请求
- *
- * @author f00lish
- * created on 2020/12/09
- */
-@Data
-@NoArgsConstructor
-public class PartnerTransactionsCloseRequest implements Serializable {
-
- private static final long serialVersionUID = -7602636370950088329L;
-
- /**
- *
- * 字段名:服务商户号
- * 变量名:sp_mchid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 服务商户号,由微信支付生成并下发
- * 示例值:1230000109
- *
- */
- @SerializedName(value = "sp_mchid")
- private String spMchid;
-
- /**
- *
- * 字段名:二级商户号
- * 变量名:sub_mchid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 二级商户的商户号,有微信支付生成并下发。
- * 示例值:1900000109
- *
- */
- @SerializedName(value = "sub_mchid")
- private String subMchid;
-
- /**
- *
- * 字段名:商户订单号
- * 变量名:out_trade_no
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一,详见【商户订单号】。
- * 特殊规则:最小字符长度为6
- * 示例值:1217752501201407033233368018
- *
- */
- private transient String outTradeNo;
-}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsNotifyResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsNotifyResult.java
deleted file mode 100644
index 03d9535fa8..0000000000
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsNotifyResult.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.github.binarywang.wxpay.bean.ecommerce;
-
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.io.Serializable;
-
-/**
- * 普通支付 通知结果
- *
- * 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/e_transactions/chapter3_11.shtml
- *
- */
-@Data
-@NoArgsConstructor
-public class PartnerTransactionsNotifyResult implements Serializable {
- private static final long serialVersionUID = -6602962275015706689L;
- /**
- * 源数据
- */
- private NotifyResponse rawData;
-
- /**
- * 解密后的数据
- */
- private PartnerTransactionsResult result;
-}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsQueryRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsQueryRequest.java
deleted file mode 100644
index 2b90e432bb..0000000000
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsQueryRequest.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.github.binarywang.wxpay.bean.ecommerce;
-
-import com.google.gson.annotations.SerializedName;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.io.Serializable;
-
-@Data
-@NoArgsConstructor
-public class PartnerTransactionsQueryRequest implements Serializable {
-
-
- /**
- *
- * 字段名:服务商户号
- * 变量名:sp_mchid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 服务商户号,由微信支付生成并下发
- * 示例值:1230000109
- *
- */
- @SerializedName(value = "sp_mchid")
- private String spMchid;
-
- /**
- *
- * 字段名:二级商户号
- * 变量名:sub_mchid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 二级商户的商户号,有微信支付生成并下发。
- * 示例值:1900000109
- *
- */
- @SerializedName(value = "sub_mchid")
- private String subMchid;
-
- /**
- *
- * 字段名:微信支付订单号
- * 变量名:transaction_id
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 微信支付系统生成的订单号
- * 示例值:1217752501201407033233368018
- *
- */
- @SerializedName(value = "transaction_id")
- private String transactionId;
- /**
- *
- * 字段名:商户订单号
- * 变量名:out_trade_no
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一,详见【商户订单号】。
- * 特殊规则:最小字符长度为6
- * 示例值:1217752501201407033233368018
- *
- */
- @SerializedName(value = "out_trade_no")
- private String outTradeNo;
-}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsRequest.java
deleted file mode 100644
index efe0978247..0000000000
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsRequest.java
+++ /dev/null
@@ -1,646 +0,0 @@
-package com.github.binarywang.wxpay.bean.ecommerce;
-
-import com.google.gson.annotations.SerializedName;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.io.Serializable;
-import java.math.BigDecimal;
-import java.util.List;
-
-/**
- * 普通支付(电商收付通)API
- *
- * 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/e_transactions.shtml
- *
- *
- * @author cloudX
- */
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class PartnerTransactionsRequest implements Serializable {
- private static final long serialVersionUID = -1550405819444680465L;
-
- /**
- *
- * 字段名:服务商公众号ID
- * 变量名:sp_appid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 服务商申请的公众号或移动应用appid
- * 示例值:wx8888888888888888
- *
- */
- @SerializedName(value = "sp_appid")
- private String spAppid;
- /**
- *
- * 字段名:服务商户号
- * 变量名:sp_mchid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 服务商户号,由微信支付生成并下发
- * 示例值:1230000109
- *
- */
- @SerializedName(value = "sp_mchid")
- private String spMchid;
- /**
- *
- * 字段名:子商户公众号ID
- * 变量名:sub_appid
- * 是否必填:否
- * 类型:string(32)
- * 描述:
- * 子商户申请的公众号或移动应用appid。
- * 示例值:wxd678efh567hg6999
- *
- */
- @SerializedName(value = "sub_appid")
- private String subAppid;
- /**
- *
- * 字段名:二级商户号
- * 变量名:sub_mchid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 二级商户的商户号,有微信支付生成并下发。
- * 示例值:1900000109
- *
- */
- @SerializedName(value = "sub_mchid")
- private String subMchid;
- /**
- *
- * 字段名:商品描述
- * 变量名:description
- * 是否必填:是
- * 类型:string(127)
- * 描述:
- * 商品描述
- * 示例值:Image形象店-深圳腾大-QQ公仔
- *
- */
- @SerializedName(value = "description")
- private String description;
- /**
- *
- * 字段名:商户订单号
- * 变量名:out_trade_no
- * 是否必填:是
- * 类型:string(127)
- * 描述:
- * 商户系统内部订单号, 只能是数字、大小写字母_-*且在同一个商户号下唯一,详见【商户订单号】
- * 特殊规则:最小字符长度为6
- * 示例值:1217752501201407033233368018
- *
- */
- @SerializedName(value = "out_trade_no")
- private String outTradeNo;
- /**
- *
- * 字段名:交易结束时间
- * 变量名:time_expire
- * 是否必填:否
- * 类型:string(14)
- * 描述:
- * 订单失效时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。
- * 示例值:2019-12-31T15:59:60+08:00
- *
- */
- @SerializedName(value = "time_expire")
- private String timeExpire;
- /**
- *
- * 字段名:附加数据
- * 变量名:attach
- * 是否必填:否
- * 类型:string(128)
- * 描述:
- * 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。
- * 示例值:自定义数据
- *
- */
- @SerializedName(value = "attach")
- private String attach;
- /**
- *
- * 字段名:通知地址
- * 变量名:notify_url
- * 是否必填:是
- * 类型:string(127)
- * 描述:
- * 通知URL必须为直接可访问的URL,不允许携带查询串。
- * 示例值:https://www.weixin.qq.com/wxpay/pay.php
- *
- */
- @SerializedName(value = "notify_url")
- private String notifyUrl;
- /**
- *
- * 字段名:订单优惠标记
- * 变量名:goods_tag
- * 是否必填:否
- * 类型:string(32)
- * 描述:
- * 订单优惠标记
- * 示例值:WXG
- *
- */
- @SerializedName(value = "goods_tag")
- private String goodsTag;
- /**
- *
- * 字段名:电子发票入口开放标识
- * 变量名:support_fapiao
- * 是否必填:否
- * 类型:boolean
- * 描述:传入true时,支付成功消息和支付详情页将出现开票入口。需要在微信支付商户平台或微信公众平台开通电子发票功能,传此字段才可生效。
- *
- */
- @SerializedName(value = "support_fapiao")
- private Boolean supportFapiao;
- /**
- *
- * 字段名:+结算信息
- * 变量名:settle_info
- * 是否必填:否
- * 类型:Object
- * 描述:结算信息
- *
- */
- @SerializedName(value = "settle_info")
- private SettleInfo settleInfo;
- /**
- *
- * 字段名:订单金额
- * 变量名:amount
- * 是否必填:是
- * 类型:object
- * 描述:
- * 订单金额信息
- *
- */
- @SerializedName(value = "amount")
- private Amount amount;
- /**
- *
- * 字段名:优惠功能
- * 变量名:detail
- * 是否必填:否
- * 类型:object
- * 描述:
- * 优惠功能
- *
- */
- @SerializedName(value = "detail")
- private Discount detail;
- /**
- *
- * 字段名:支付者
- * 变量名:payer
- * 是否必填:是(仅JSAPI支付必传)
- * 类型:object
- * 描述:
- * 支付者信息
- *
- */
- @SerializedName(value = "payer")
- private Payer payer;
- /**
- *
- * 字段名:场景信息
- * 变量名:scene_info
- * 是否必填:是(仅H5支付必传)
- * 类型:object
- * 描述:
- * 支付场景描述
- *
- */
- @SerializedName(value = "scene_info")
- private SceneInfo sceneInfo;
-
- @Data
- @NoArgsConstructor
- public static class Discount implements Serializable {
- private static final long serialVersionUID = 1090134053810201492L;
-
- /**
- *
- * 字段名:订单原价
- * 变量名:cost_price
- * 是否必填:否
- * 类型:int64
- * 描述:
- * 1、商户侧一张小票订单可能被分多次支付,订单原价用于记录整张小票的交易金额。
- * 2、当订单原价与支付金额不相等,则不享受优惠。
- * 3、该字段主要用于防止同一张小票分多次支付,以享受多次优惠的情况,正常支付订单不必上传此参数。
- * 示例值:608800
- *
- */
- @SerializedName(value = "cost_price")
- private Integer costPrice;
- /**
- *
- * 字段名:商品小票ID
- * 变量名:invoice_id
- * 是否必填:否
- * 类型:string(32)
- * 描述:
- * 商品小票ID
- * 示例值:微信123
- *
- */
- @SerializedName(value = "invoice_id")
- private String invoiceId;
- /**
- *
- * 字段名:单品列表
- * 变量名:goods_detail
- * 是否必填:否
- * 类型:array
- * 描述:
- * 单品列表信息
- * 条目个数限制:【1,undefined】
- *
- */
- @SerializedName(value = "goods_detail")
- private List goodsDetails;
- }
-
- @Data
- @NoArgsConstructor
- @AllArgsConstructor
- public static class Amount implements Serializable {
- private static final long serialVersionUID = -4967636398225864273L;
-
- /**
- *
- * 字段名:总金额
- * 变量名:total
- * 是否必填:是
- * 类型:int64
- * 描述:
- * 订单总金额,单位为分。
- * 示例值:100
- *
- */
- @SerializedName(value = "total")
- private Integer total;
- /**
- *
- * 字段名:币类型
- * 变量名:currency
- * 是否必填:否
- * 类型:string(16)
- * 描述:
- * CNY:人民币,境内商户号仅支持人民币。
- * 示例值:CNY
- *
- */
- @SerializedName(value = "currency")
- private String currency;
- }
-
- @Data
- @NoArgsConstructor
- @AllArgsConstructor
- public static class Payer implements Serializable {
- private static final long serialVersionUID = -3946401119476159971L;
-
- /**
- *
- * 字段名:用户服务标识
- * 变量名:sp_openid
- * 是否必填:是
- * 类型:string(128)
- * 描述:
- * 用户在服务商appid下的唯一标识。
- * 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
- *
- */
- @SerializedName(value = "sp_openid")
- private String spOpenid;
- /**
- *
- * 字段名:用户子标识
- * 变量名:sub_openid
- * 是否必填:否
- * 类型:string(128)
- * 描述:
- * 用户在子商户appid下的唯一标识。
- * 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
- *
- */
- @SerializedName(value = "sub_openid")
- private String subOpenid;
- }
-
- @Data
- @NoArgsConstructor
- public static class SettleInfo implements Serializable {
- private static final long serialVersionUID = 4438958789491671746L;
-
- /**
- *
- * 字段名:是否指定分账
- * 变量名:profit_sharing
- * 是否必填:否
- * 类型:bool
- * 描述:
- * 是否分账,与外层profit_sharing同时存在时,以本字段为准。
- * true:是
- * false:否
- * 示例值:true
- *
- */
- @SerializedName(value = "profit_sharing")
- private Boolean profitSharing;
- /**
- *
- * 字段名:补差金额
- * 变量名:subsidy_amount
- * 是否必填:否
- * 类型:int64
- * 描述:
- * SettleInfo.profit_sharing为true时,该金额才生效。
- * 注意:单笔订单最高补差金额为5000元
- * 示例值:10
- *
- */
- @SerializedName(value = "subsidy_amount")
- private BigDecimal subsidyAmount;
- }
-
- @Data
- @NoArgsConstructor
- public static class GoodsDetail implements Serializable {
- private static final long serialVersionUID = -2574001236925022932L;
-
- /**
- *
- * 字段名:商户侧商品编码
- * 变量名:merchant_goods_id
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 由半角的大小写字母、数字、中划线、下划线中的一种或几种组成。
- * 示例值:商品编码
- *
- */
- @SerializedName(value = "merchant_goods_id")
- private String merchantGoodsId;
- /**
- *
- * 字段名:微信侧商品编码
- * 变量名:wechatpay_goods_id
- * 是否必填:否
- * 类型:string(32)
- * 描述:
- * 微信支付定义的统一商品编号(没有可不传)
- * 示例值:1001
- *
- */
- @SerializedName(value = "wechatpay_goods_id")
- private String wechatpayGoodsId;
- /**
- *
- * 字段名:商品名称
- * 变量名:goods_name
- * 是否必填:否
- * 类型:string(256)
- * 描述:
- * 商品的实际名称
- * 示例值:iPhoneX 256G
- *
- */
- @SerializedName(value = "goods_name")
- private String goodsName;
- /**
- *
- * 字段名:商品数量
- * 变量名:quantity
- * 是否必填:是
- * 类型:int64
- * 描述:
- * 用户购买的数量
- * 示例值:1
- *
- */
- @SerializedName(value = "quantity")
- private Integer quantity;
- /**
- *
- * 字段名:商品单价
- * 变量名:unit_price
- * 是否必填:是
- * 类型:int64
- * 描述:
- * 商品单价,单位为分
- * 示例值:828800
- *
- */
- @SerializedName(value = "unit_price")
- private Integer unitPrice;
- }
-
- @Data
- @NoArgsConstructor
- public static class SceneInfo implements Serializable {
- private static final long serialVersionUID = 4678263124015070957L;
-
- /**
- *
- * 字段名:商户端设备号
- * 变量名:device_id
- * 是否必填:否
- * 类型:string(16)
- * 描述:
- * 终端设备号(门店号或收银设备ID)。
- * 特殊规则:长度最小7个字节
- * 示例值:POS1:1
- *
- */
- @SerializedName(value = "device_id")
- private String deviceId;
- /**
- *
- * 字段名:用户终端IP
- * 变量名:payer_client_ip
- * 是否必填:是
- * 类型:string(45)
- * 描述:
- * 用户端实际ip
- * 格式: ip(ipv4+ipv6)
- * 示例值:14.17.22.32
- *
- */
- @SerializedName(value = "payer_client_ip")
- private String payerClientIp;
- /**
- *
- * 字段名:H5场景信息
- * 变量名:h5_info
- * 是否必填:否(H5支付必填)
- * 类型:object
- * 描述:
- * H5场景信息
- *
- */
- @SerializedName(value = "h5_info")
- private H5Info h5Info;
- /**
- *
- * 字段名:商户门店信息
- * 变量名:store_info
- * 是否必填:否(H5支付必填)
- * 类型:object
- * 描述:
- * 商户门店信息
- *
- */
- @SerializedName(value = "store_info")
- private StoreInfo storeInfo;
- }
-
- @Data
- @NoArgsConstructor
- public static class H5Info implements Serializable {
- private static final long serialVersionUID = -6865738707329486532L;
-
- /**
- *
- * 字段名:场景类型
- * 变量名:type
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 场景类型,枚举值:
- * iOS:IOS移动应用;
- * Android:安卓移动应用;
- * Wap:WAP网站应用;
- * 示例值:iOS
- *
- */
- @SerializedName(value = "type")
- private String type;
- /**
- *
- * 字段名:应用名称
- * 变量名:app_name
- * 是否必填:否
- * 类型:string(64)
- * 描述:
- * 应用名称
- * 示例值:王者荣耀
- *
- */
- @SerializedName(value = "app_name")
- private String appName;
- /**
- *
- * 字段名:网站URL
- * 变量名:app_url
- * 是否必填:否
- * 类型:string(128)
- * 描述:
- * 网站URL
- * 示例值:https://pay.qq.com
- *
- */
- @SerializedName(value = "app_url")
- private String appUrl;
- /**
- *
- * 字段名:iOS平台BundleID
- * 变量名:bundle_id
- * 是否必填:否
- * 类型:string(128)
- * 描述:
- * iOS平台BundleID
- * 示例值:com.tencent.wzryiOS
- *
- */
- @SerializedName(value = "bundle_id")
- private String bundleId;
- /**
- *
- * 字段名:Android平台PackageName
- * 变量名:package_name
- * 是否必填:否
- * 类型:string(128)
- * 描述:
- * Android平台PackageName
- * 示例值:com.tencent.tmgp.sgame
- *
- */
- @SerializedName(value = "package_name")
- private String packageName;
- }
-
- @Data
- @NoArgsConstructor
- public static class StoreInfo implements Serializable {
- private static final long serialVersionUID = -8002411737407580701L;
-
- /**
- *
- * 字段名:门店编号
- * 变量名:id
- * 是否必填:否
- * 类型:string(32)
- * 描述:
- * 商户侧门店编号
- * 示例值:0001
- *
- */
- @SerializedName(value = "id")
- private String id;
- /**
- *
- * 字段名:门店名称
- * 变量名:name
- * 是否必填:是
- * 类型:string(256)
- * 描述:
- * 商户侧门店名称
- * 示例值:腾讯大厦分店
- *
- */
- @SerializedName(value = "name")
- private String name;
- /**
- *
- * 字段名:地区编码
- * 变量名:area_code
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 地区编码,详细请见省市区编号对照表(https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/applyments/chapter4_1.shtml)。
- * 示例值:440305
- *
- */
- @SerializedName(value = "area_code")
- private String areaCode;
- /**
- *
- * 字段名:详细地址
- * 变量名:address
- * 是否必填:是
- * 类型:string(512)
- * 描述:
- * 详细的商户门店地址
- * 示例值:广东省深圳市南山区科技中一道10000号
- *
- */
- @SerializedName(value = "address")
- private String address;
- }
-}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsResult.java
deleted file mode 100644
index 2c9086e7f4..0000000000
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/PartnerTransactionsResult.java
+++ /dev/null
@@ -1,600 +0,0 @@
-package com.github.binarywang.wxpay.bean.ecommerce;
-
-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/wiki/doc/apiv3/wxpay/ecommerce/e_transactions/chapter3_5.shtml
- *
- * @author cloudX
- */
-@Data
-@NoArgsConstructor
-public class PartnerTransactionsResult implements Serializable {
- private static final long serialVersionUID = 2371448241965534820L;
-
- /**
- *
- * 字段名:服务商公众号ID
- * 变量名:sp_appid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 服务商申请的公众号或移动应用appid。
- * 示例值:wx8888888888888888
- *
- */
- @SerializedName(value = "sp_appid")
- private String spAppid;
-
- /**
- *
- * 字段名:服务商户号
- * 变量名:sp_mchid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 服务商户号,由微信支付生成并下发
- * 示例值:1230000109
- *
- */
- @SerializedName(value = "sp_mchid")
- private String spMchid;
-
- /**
- *
- * 字段名:二级商户公众号ID
- * 变量名:sub_appid
- * 是否必填:否
- * 类型:string(32)
- * 描述:
- * 二级商户申请的公众号或移动应用appid。
- * 示例值:wxd678efh567hg6999
- *
- */
- @SerializedName(value = "sub_appid")
- private String subAppid;
-
- /**
- *
- * 字段名:二级商户号
- * 变量名:sub_mchid
- * 是否必填:是
- * 类型:string(32)
- * 描述:
- * 二级商户的商户号,有微信支付生成并下发。
- * 示例值:1900000109
- *
- */
- @SerializedName(value = "sub_mchid")
- private String subMchid;
-
- /**
- *
- * 字段名:+商户订单号
- * 变量名:out_trade_no
- * 是否必填:是
- * 类型:string(32)
- * 描述:商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一,详见【商户订单号】。
- * 特殊规则:最小字符长度为6
- * 示例值:1217752501201407033233368018
- *
- */
- @SerializedName(value = "out_trade_no")
- private String outTradeNo;
-
- /**
- *
- * 字段名:微信支付订单号
- * 变量名:transaction_id
- * 是否必填:否
- * 类型:string(32)
- * 描述:微信支付系统生成的订单号。
- * 示例值:1217752501201407033233368018
- *
- */
- @SerializedName(value = "transaction_id")
- private String transactionId;
-
- /**
- *
- * 字段名:交易类型
- * 变量名:trade_type
- * 是否必填:否
- * 类型:string(16)
- * 描述:交易类型,枚举值:
- * JSAPI:公众号支付
- * NATIVE:扫码支付
- * APP:APP支付
- * MICROPAY:付款码支付
- * MWEB:H5支付
- * FACEPAY:刷脸支付
- *
- * 示例值: MICROPAY
- *
- */
- @SerializedName(value = "trade_type")
- private String tradeType;
-
- /**
- *
- * 字段名:交易状态
- * 变量名:trade_state
- * 是否必填:是
- * 类型:string(32)
- * 描述:交易状态,枚举值:
- * SUCCESS:支付成功
- * REFUND:转入退款
- * NOTPAY:未支付
- * CLOSED:已关闭
- * REVOKED:已撤销(付款码支付)
- * USERPAYING:用户支付中(付款码支付)
- * PAYERROR:支付失败(其他原因,如银行返回失败)
- *
- * 示例值:SUCCESS
- *
- */
- @SerializedName(value = "trade_state")
- private String tradeState;
-
- /**
- *
- * 字段名:交易状态描述
- * 变量名:trade_state_desc
- * 是否必填:是
- * 类型:string(256)
- * 描述:交易状态描述
- * 示例值:支付失败,请重新下单支付
- *
- */
- @SerializedName(value = "trade_state_desc")
- private String tradeStateDesc;
-
- /**
- *
- * 字段名:付款银行
- * 变量名:bank_type
- * 是否必填:否
- * 类型:string(16)
- * 描述:银行类型,采用字符串类型的银行标识。
- * 示例值:CMC
- *
- */
- @SerializedName(value = "bank_type")
- private String bankType;
-
- /**
- *
- * 字段名:附加数据
- * 变量名:attach
- * 是否必填:否
- * 类型:string(128)
- * 描述:附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
- * 示例值:自定义数据
- *
- */
- @SerializedName(value = "attach")
- private String attach;
-
- /**
- *
- * 字段名:支付完成时间
- * 变量名:success_time
- * 是否必填:否
- * 类型:string(64)
- * 描述:支付完成时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。
- * 示例值:2018-06-08T10:34:56+08:00
- *
- */
- @SerializedName(value = "success_time")
- private String successTime;
-
- /**
- *
- * 字段名:支付者信息
- * 变量名:payer
- * 是否必填:是
- * 类型:object
- * 描述:基础支付支付者信息
- *
- */
- private CombinePayerInfo payer;
-
- /**
- *
- * 字段名:支付者
- * 变量名:combine_payer_info
- * 是否必填:否
- * 类型:object
- * 描述:合单支付支付者信息,示例值:见请求示例
- *
- */
- @SerializedName(value = "combine_payer_info")
- private CombinePayerInfo combinePayerInfo;
-
- /**
- *
- * 字段名:订单金额
- * 变量名:amount
- * 是否必填:是
- * 类型:object
- * 描述:订单金额信息
- *
- */
- @SerializedName(value = "amount")
- private Amount amount;
-
- /**
- *
- * 字段名:场景信息
- * 变量名:scene_info
- * 是否必填:否
- * 类型:object
- * 描述:支付场景信息描述
- *
- */
- @SerializedName(value = "scene_info")
- private SceneInfo sceneInfo;
-
- /**
- *
- * 字段名:优惠功能
- * 变量名:promotion_detail
- * 是否必填:否
- * 类型:array
- * 描述:优惠功能,享受优惠时返回该字段。
- *
- */
- @SerializedName(value = "promotion_detail")
- private List promotionDetails;
-
- @Data
- @NoArgsConstructor
- public static class SceneInfo implements Serializable {
- /**
- *
- * 字段名:商户端设备号
- * 变量名:device_id
- * 是否必填:否
- * 类型:string(16)
- * 描述:
- * 终端设备号(门店号或收银设备ID)。
- * 特殊规则:长度最小7个字节
- * 示例值:POS1:1
- *
- */
- @SerializedName(value = "device_id")
- private String deviceId;
-
- }
-
- @Data
- @NoArgsConstructor
- public static class CombinePayerInfo implements Serializable {
- /**
- *
- * 字段名:用户标识
- * 变量名:sp_openid
- * 是否必填:是
- * 类型:string(128)
- * 描述:
- * 用户在服务商appid下的唯一标识。
- * 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
- *
- */
- @SerializedName(value = "sp_openid")
- private String spOpenid;
-
-
- /**
- *
- * 字段名:二级商户用户标识
- * 变量名:sub_openid
- * 是否必填:否
- * 类型:string(128)
- * 描述:
- * 用户在二级商户appid下的唯一标识。
- * 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
- *
- */
- @SerializedName(value = "sub_openid")
- private String subOpenid;
-
- }
-
- @Data
- @NoArgsConstructor
- public static class Amount implements Serializable {
- /**
- *
- * 字段名:总金额
- * 变量名:total
- * 是否必填:否
- * 类型:int
- * 描述:
- * 订单总金额,单位为分
- * 示例值:100
- *
- */
- @SerializedName(value = "total")
- private Integer total;
-
-
- /**
- *
- * 字段名:用户支付金额
- * 变量名:payer_total
- * 是否必填:否
- * 类型:int
- * 描述:
- * 用户支付金额,单位为分。
- * 示例值:100
- *
- */
- @SerializedName(value = "payer_total")
- private Integer payerTotal;
-
-
- /**
- *
- * 字段名:货币类型
- * 变量名:currency
- * 是否必填:否
- * 类型:string(16)
- * 描述:
- * CNY:人民币,境内商户号仅支持人民币。
- * 示例值:CNY
- *
- */
- @SerializedName(value = "currency")
- private String currency;
-
-
- /**
- *
- * 字段名:用户支付币种
- * 变量名:payer_currency
- * 是否必填:否
- * 类型:string(8)
- * 描述:
- * 用户支付币种
- * 示例值: CNY
- *
- */
- @SerializedName(value = "payer_currency")
- private String payerCurrency;
- }
-
- @Data
- @NoArgsConstructor
- public static class PromotionDetail implements Serializable {
-
- /**
- *
- * 字段名:券ID
- * 变量名:coupon_id
- * 是否必填:是
- * 类型:string(32)
- * 描述: 券ID
- * 示例值:109519
- *
- */
- @SerializedName(value = "coupon_id")
- private String couponId;
-
- /**
- *
- * 字段名:优惠名称
- * 变量名:name
- * 是否必填:否
- * 类型:string(64)
- * 描述: 优惠名称
- * 示例值:单品惠-6
- *
- */
- @SerializedName(value = "name")
- private String name;
- /**
- *
- * 字段名:优惠范围
- * 变量名:scope
- * 是否必填:否
- * 类型:string(32)
- * 描述: 优惠名称
- * 示例值:
- * GLOBAL:全场代金券
- * SINGLE:单品优惠
- * 示例值:GLOBAL
- *
- */
- @SerializedName(value = "scope")
- private String scope;
-
- /**
- *
- * 字段名:优惠类型
- * 变量名:type
- * 是否必填:否
- * 类型:string(32)
- * 描述:
- * CASH:充值
- * NOCASH:预充值
- * 示例值:CASH
- *
- */
- @SerializedName(value = "type")
- private String type;
-
- /**
- *
- * 字段名:优惠券面额
- * 变量名:amount
- * 是否必填:是
- * 类型:int
- * 描述: 优惠券面额
- * 示例值:100
- *
- */
- @SerializedName(value = "amount")
- private Integer amount;
-
- /**
- *
- * 字段名:活动ID
- * 变量名:stock_id
- * 是否必填:否
- * 类型:string(32)
- * 描述:活动ID
- * 示例值:931386
- *
- */
- @SerializedName(value = "stock_id")
- private String stockId;
-
- /**
- *
- * 字段名:微信出资
- * 变量名:wechatpay_contribute
- * 是否必填:否
- * 类型:int
- * 描述:微信出资,单位为分
- * 示例值:0
- *
- */
- @SerializedName(value = "wechatpay_contribute")
- private Integer wechatpayContribute;
-
- /**
- *
- * 字段名:商户出资
- * 变量名:merchant_contribute
- * 是否必填:否
- * 类型:int
- * 描述:商户出资,单位为分
- * 示例值:0
- *
- */
- @SerializedName(value = "merchant_contribute")
- private Integer merchantContribute;
-
- /**
- *
- * 字段名:其他出资
- * 变量名:other_contribute
- * 是否必填:否
- * 类型:int
- * 描述:其他出资,单位为分
- * 示例值:0
- *
- */
- @SerializedName(value = "other_contribute")
- private Integer otherContribute;
-
- /**
- *
- * 字段名:优惠币种
- * 变量名:currency
- * 是否必填:否
- * 类型:String(16)
- * 描述:
- * CNY:人民币,境内商户号仅支持人民币。
- * 示例值:CNY
- *
- */
- @SerializedName(value = "currency")
- private String currency;
-
- /**
- *
- * 字段名:单品列表
- * 变量名:goods_detail
- * 是否必填:否
- * 类型:array
- * 描述:单品列表信息
- *
- */
- @SerializedName(value = "goods_detail")
- private List goodsDetails;
-
-
- }
-
- @Data
- @NoArgsConstructor
- public static class GoodsDetail implements Serializable {
-
- /**
- *
- * 字段名:商品编码
- * 变量名:goods_id
- * 是否必填:是
- * 类型:string(32)
- * 描述:商品编码
- * 示例值:M1006
- *
- */
- @SerializedName(value = "goods_id")
- private String goodsId;
-
- /**
- *
- * 字段名:商品数量
- * 变量名:quantity
- * 是否必填:是
- * 类型:int64
- * 描述:
- * 用户购买的数量
- * 示例值:1
- *
- */
- @SerializedName(value = "quantity")
- private Integer quantity;
-
- /**
- *
- * 字段名:商品单价
- * 变量名:unit_price
- * 是否必填:是
- * 类型:int64
- * 描述:
- * 商品单价,单位为分
- * 示例值:100
- *
- */
- @SerializedName(value = "unit_price")
- private Integer unitPrice;
-
- /**
- *
- * 字段名:商品优惠金额
- * 变量名:discount_amount
- * 是否必填:是
- * 类型:int
- * 描述:商品优惠金额
- * 示例值:0
- *
- */
- @SerializedName(value = "discount_amount")
- private Integer discountAmount;
-
- /**
- *
- * 字段名:商品备注
- * 变量名:goods_remark
- * 是否必填:否
- * 类型:string(128)
- * 描述:商品备注信息
- * 示例值:商品备注信息
- *
- */
- @SerializedName(value = "goods_remark")
- private String goodsRemark;
- }
-
-}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SignatureHeader.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SignatureHeader.java
deleted file mode 100644
index 498a788c07..0000000000
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SignatureHeader.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.github.binarywang.wxpay.bean.ecommerce;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.io.Serializable;
-
-/**
- * 微信通知接口头部信息,需要做签名验证
- * 文档地址: https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/qian-ming-yan-zheng
- *
- * @author cloudX
- */
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class SignatureHeader implements Serializable {
- private static final long serialVersionUID = -6958015499416059949L;
- /**
- * 时间戳
- */
- private String timeStamp;
-
- /**
- * 随机串
- */
- private String nonce;
-
- /**
- * 已签名字符串
- */
- private String signed;
-
- /**
- * 证书序列号
- */
- private String serialNo;
-}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/TransactionsResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/TransactionsResult.java
deleted file mode 100644
index 8e11d859b6..0000000000
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/TransactionsResult.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package com.github.binarywang.wxpay.bean.ecommerce;
-
-import com.github.binarywang.wxpay.bean.ecommerce.enums.TradeTypeEnum;
-import com.github.binarywang.wxpay.v3.util.SignUtils;
-import com.google.gson.annotations.SerializedName;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import lombok.experimental.Accessors;
-
-import java.io.Serializable;
-import java.security.PrivateKey;
-
-/**
- * 合单支付 JSAPI支付结果响应
- */
-@Data
-@NoArgsConstructor
-public class TransactionsResult implements Serializable {
- private static final long serialVersionUID = 1760592667519950149L;
- /**
- *
- * 字段名:预支付交易会话标识 (APP支付、JSAPI支付 会返回)
- * 变量名:prepay_id
- * 是否必填:是
- * 类型:string(64)
- * 描述:
- * 数字和字母。微信生成的预支付会话标识,用于后续接口调用使用。
- * 示例值:wx201410272009395522657a690389285100
- *
- */
- @SerializedName("prepay_id")
- private String prepayId;
-
- /**
- *
- * 字段名:支付跳转链接 (H5支付 会返回)
- * 变量名:h5_url
- * 是否必填:是
- * 类型:string(512)
- * 描述:
- * 支付跳转链接
- * 示例值:https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx2016121516420242444321ca0631331346&package=1405458241
- *
- */
- @SerializedName("h5_url")
- private String h5Url;
-
- /**
- *
- * 字段名:二维码链接 (NATIVE支付 会返回)
- * 变量名:h5_url
- * 是否必填:是
- * 类型:string(512)
- * 描述:
- * 二维码链接
- * 示例值:weixin://pay.weixin.qq.com/bizpayurl/up?pr=NwY5Mz9&groupid=00
- *
- */
- @SerializedName("code_url")
- private String codeUrl;
-
- @Data
- @Accessors(chain = true)
- 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;
-
- private String getSignStr() {
- return String.format("%s\n%s\n%s\n%s\n", appId, timeStamp, nonceStr, packageValue);
- }
- }
-
- @Data
- @Accessors(chain = true)
- 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;
- private String sign;
-
- private String getSignStr() {
- return String.format("%s\n%s\n%s\n%s\n", appid, timestamp, noncestr, prepayid);
- }
- }
-
- 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;
- case MWEB:
- 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;
- case NATIVE:
- return (T) this.codeUrl;
- }
- return null;
- }
-}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/enums/TradeTypeEnum.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/enums/TradeTypeEnum.java
deleted file mode 100644
index e8bd5ccba4..0000000000
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/enums/TradeTypeEnum.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.github.binarywang.wxpay.bean.ecommerce.enums;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-/**
- * 支付方式
- */
-@Getter
-@AllArgsConstructor
-public enum TradeTypeEnum {
- /**
- * APP
- */
- APP("/v3/combine-transactions/app", "/v3/pay/partner/transactions/app"),
- /**
- * JSAPI
- */
- JSAPI("/v3/combine-transactions/jsapi", "/v3/pay/partner/transactions/jsapi"),
- /**
- * NATIVE
- */
- NATIVE("/v3/combine-transactions/native", "/v3/pay/partner/transactions/native"),
- /**
- * MWEB
- */
- MWEB("/v3/combine-transactions/h5", "/v3/pay/partner/transactions/h5");
-
- /**
- * 合单url
- */
- private final String combineUrl;
- /**
- * 单独下单url
- */
- private final String partnerUrl;
-}
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/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/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/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/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/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/WxPayService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java
index a460d5f248..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;
@@ -1069,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
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 2574e969d7..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
@@ -1,21 +1,17 @@
package com.github.binarywang.wxpay.service.impl;
-import static com.github.binarywang.wxpay.constant.WxPayConstants.QUERY_COMMENT_DATE_FORMAT;
-import static com.github.binarywang.wxpay.constant.WxPayConstants.TarType;
-import com.github.binarywang.wxpay.bean.coupon.*;
-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.service.*;
-import java.util.*;
-import com.github.binarywang.wxpay.bean.result.enums.GlobalTradeTypeEnum;
-import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.utils.qrcode.QrcodeUtils;
import com.github.binarywang.wxpay.bean.WxPayApiData;
+import com.github.binarywang.wxpay.bean.coupon.*;
+import com.github.binarywang.wxpay.bean.notify.*;
import com.github.binarywang.wxpay.bean.order.WxPayAppOrderResult;
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
import com.github.binarywang.wxpay.bean.order.WxPayMwebOrderResult;
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.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;
@@ -23,6 +19,7 @@
import com.github.binarywang.wxpay.constant.WxPayConstants.TradeType;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.exception.WxSignTestException;
+import com.github.binarywang.wxpay.service.*;
import com.github.binarywang.wxpay.util.SignUtils;
import com.github.binarywang.wxpay.util.XmlConfig;
import com.github.binarywang.wxpay.util.ZipUtils;
@@ -32,6 +29,14 @@
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.error.WxRuntimeException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.reflect.ConstructorUtils;
+import org.apache.http.entity.ContentType;
+
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -40,15 +45,12 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
+import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.ZipException;
-import lombok.Getter;
-import lombok.Setter;
-import lombok.extern.slf4j.Slf4j;
-import me.chanjar.weixin.common.error.WxRuntimeException;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.reflect.ConstructorUtils;
-import org.apache.http.entity.ContentType;
+
+import static com.github.binarywang.wxpay.constant.WxPayConstants.QUERY_COMMENT_DATE_FORMAT;
+import static com.github.binarywang.wxpay.constant.WxPayConstants.TarType;
/**
*
@@ -526,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("微信支付签名探测流量");
@@ -959,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);
}
@@ -976,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);
}
@@ -1360,8 +1363,8 @@ public WxPayMicropayResult micropay(WxPayMicropayRequest request) throws WxPayEx
@Override
public WxPayCodepayResult codepay(WxPayCodepayRequest request) throws WxPayException {
// 判断是否为服务商模式:如果设置了sp_appid或sp_mchid或sub_mchid中的任何一个,则认为是服务商模式
- boolean isPartnerMode = StringUtils.isNotBlank(request.getSpAppid())
- || StringUtils.isNotBlank(request.getSpMchid())
+ boolean isPartnerMode = StringUtils.isNotBlank(request.getSpAppid())
+ || StringUtils.isNotBlank(request.getSpMchid())
|| StringUtils.isNotBlank(request.getSubMchid());
if (isPartnerMode) {
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/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/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/PayScoreServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayScoreServiceImpl.java
index ee92c6611a..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;
@@ -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/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);
}
From 388478e4c732461b22c1a158f9f13998a2fd0c9c Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Sat, 23 May 2026 22:55:43 +0800
Subject: [PATCH 18/38] =?UTF-8?q?:art:=20#4016=20=E7=A7=BB=E9=99=A4=20depe?=
=?UTF-8?q?ndencyManagement=20=E4=B8=AD=20joda-time=20=E7=9A=84=20test=20s?=
=?UTF-8?q?cope=EF=BC=8C=E9=98=B2=E6=AD=A2=E8=A6=86=E7=9B=96=E4=B8=8B?=
=?UTF-8?q?=E6=B8=B8=E9=A1=B9=E7=9B=AE=E4=BE=9D=E8=B5=96=E8=8C=83=E5=9B=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/pom.xml b/pom.xml
index 905e582443..d13d4bffcd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -221,13 +221,14 @@
import
-
+
joda-time
joda-time
2.10.6
- test
+
+
ch.qos.logback
logback-classic
From 3f517d10e88f47d517461e4ea22fd97b255f590d Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 21:20:41 +0800
Subject: [PATCH 19/38] =?UTF-8?q?:art:=20#3974=20=E3=80=90=E5=B0=8F?=
=?UTF-8?q?=E7=A8=8B=E5=BA=8F=E3=80=91=E6=96=B0=E5=A2=9E=E8=99=9A=E6=8B=9F?=
=?UTF-8?q?=E6=94=AF=E4=BB=98iOS=E9=80=80=E6=AC=BE=E6=9F=A5=E8=AF=A2?=
=?UTF-8?q?=E9=80=9A=E7=9F=A5=E4=BA=8B=E4=BB=B6=E7=9A=84=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../chanjar/weixin/common/api/WxConsts.java | 5 ++
.../wx/miniapp/bean/WxMaMessage.java | 74 +++++++++++++++++++
.../wx/miniapp/constant/WxMaConstants.java | 5 ++
.../wx/miniapp/bean/WxMaMessageTest.java | 67 +++++++++++++++++
4 files changed, 151 insertions(+)
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java
index 4924682e5e..5129410999 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java
@@ -499,6 +499,11 @@ public static class EventType {
* 订单完成发货时、订单结算时
*/
public static final String TRADE_MANAGE_ORDER_SETTLEMENT = "trade_manage_order_settlement";
+ /**
+ * 虚拟支付 iOS 退款查询通知
+ * 文档:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment/ios.html
+ */
+ public static final String XPAY_SUBSCRIBE_IOS_REFUND_QUERY_NOTIFY = "xpay_subscribe_ios_refund_query_notify";
}
/**
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaMessage.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaMessage.java
index 6421910015..83073850da 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaMessage.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaMessage.java
@@ -464,6 +464,80 @@ public class WxMaMessage implements Serializable {
@XStreamConverter(value = XStreamCDataConverter.class)
private String requestId;
+ // xpay_subscribe_ios_refund_query_notify iOS退款查询通知字段
+
+ /**
+ * 问询时间,Unix时间戳.
+ * xpay_subscribe_ios_refund_query_notify
+ */
+ @SerializedName("refund_time")
+ @XStreamAlias("refund_time")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String refundTime;
+
+ /**
+ * 该笔退款的订单时间(退款订单对应的交易时间),Unix时间戳.
+ * xpay_subscribe_ios_refund_query_notify
+ */
+ @SerializedName("order_time")
+ @XStreamAlias("order_time")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String orderTime;
+
+ /**
+ * Apple 支付票据号.
+ * xpay_subscribe_ios_refund_query_notify
+ */
+ @SerializedName("channel_bill")
+ @XStreamAlias("channel_bill")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String channelBill;
+
+ /**
+ * 应用的 Apple bundleid.
+ * xpay_subscribe_ios_refund_query_notify
+ */
+ @SerializedName("bundleid")
+ @XStreamAlias("bundleid")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String bundleid;
+
+ /**
+ * 道具 id.
+ * xpay_subscribe_ios_refund_query_notify
+ */
+ @SerializedName("product_id")
+ @XStreamAlias("product_id")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String xpayProductId;
+
+ /**
+ * 道具/代币数量.
+ * xpay_subscribe_ios_refund_query_notify
+ */
+ @SerializedName("p_count")
+ @XStreamAlias("p_count")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String pCount;
+
+ /**
+ * 用户请求退款的原因.
+ * xpay_subscribe_ios_refund_query_notify
+ */
+ @SerializedName("refund_request_reason")
+ @XStreamAlias("refund_request_reason")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String refundRequestReason;
+
+ /**
+ * 发货状态,0:未发货 1:已发货 2:发货中.
+ * xpay_subscribe_ios_refund_query_notify
+ */
+ @SerializedName("provide_status")
+ @XStreamAlias("provide_status")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String provideStatus;
+
/**
* 不要直接使用这个字段,
* 这个字段只是为了适配 SubscribeMsgPopupEvent SubscribeMsgChangeEvent SubscribeMsgSentEvent
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaConstants.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaConstants.java
index 0fcf3b10f0..df83630be7 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaConstants.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaConstants.java
@@ -265,6 +265,11 @@ public static final class XPayNotifyEvent {
public static final String GOODS_DELIVER = "xpay_goods_deliver_notify";
public static final String REFUND = "xpay_refund_notify";
public static final String COMPLAINT = "xpay_complaint_notify";
+ /**
+ * 虚拟支付 iOS 退款查询通知.
+ * 文档:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment/ios.html
+ */
+ public static final String IOS_REFUND_QUERY = "xpay_subscribe_ios_refund_query_notify";
}
@UtilityClass
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaMessageTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaMessageTest.java
index 3f7e154260..a25589d640 100644
--- a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaMessageTest.java
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaMessageTest.java
@@ -1,6 +1,7 @@
package cn.binarywang.wx.miniapp.bean;
import cn.binarywang.wx.miniapp.bean.xpay.WxMaXPayTeamInfo;
+import cn.binarywang.wx.miniapp.constant.WxMaConstants;
import me.chanjar.weixin.common.api.WxConsts;
import org.testng.annotations.Test;
@@ -457,4 +458,70 @@ private void checkXPayComplaintNotifyMessage(WxMaMessage msg) {
assertEquals(msg.getRetryTimes(), new Integer(0));
assertEquals(msg.getRequestId(), "req_005");
}
+
+ /**
+ * 虚拟支付 iOS 退款查询通知事件 xpay_subscribe_ios_refund_query_notify 测试用例(XML格式)
+ */
+ @Test
+ public void testXPaySubscribeIosRefundQueryNotifyFromXml() {
+ String xml = "\n" +
+ " \n" +
+ " \n" +
+ " 1700001000 \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " ";
+
+ WxMaMessage msg = WxMaMessage.fromXml(xml);
+ checkXPaySubscribeIosRefundQueryNotifyMessage(msg);
+ }
+
+ /**
+ * 虚拟支付 iOS 退款查询通知事件 xpay_subscribe_ios_refund_query_notify 测试用例(JSON格式)
+ */
+ @Test
+ public void testXPaySubscribeIosRefundQueryNotifyFromJson() {
+ String json = "{\n" +
+ " \"ToUserName\": \"gh_abcdefg\",\n" +
+ " \"FromUserName\": \"oABCDEFG\",\n" +
+ " \"CreateTime\": 1700001000,\n" +
+ " \"MsgType\": \"event\",\n" +
+ " \"Event\": \"xpay_subscribe_ios_refund_query_notify\",\n" +
+ " \"refund_time\": \"1700000900\",\n" +
+ " \"order_time\": \"1699990000\",\n" +
+ " \"channel_bill\": \"apple_bill_001\",\n" +
+ " \"bundleid\": \"com.example.app\",\n" +
+ " \"product_id\": \"product_xyz\",\n" +
+ " \"p_count\": \"1\",\n" +
+ " \"refund_request_reason\": \"不喜欢\",\n" +
+ " \"provide_status\": \"1\"\n" +
+ "}";
+
+ WxMaMessage msg = WxMaMessage.fromJson(json);
+ checkXPaySubscribeIosRefundQueryNotifyMessage(msg);
+ }
+
+ private void checkXPaySubscribeIosRefundQueryNotifyMessage(WxMaMessage msg) {
+ assertEquals(msg.getToUser(), "gh_abcdefg");
+ assertEquals(msg.getFromUser(), "oABCDEFG");
+ assertEquals(msg.getCreateTime(), new Integer(1700001000));
+ assertEquals(msg.getMsgType(), WxConsts.XmlMsgType.EVENT);
+ assertEquals(msg.getEvent(), WxMaConstants.XPayNotifyEvent.IOS_REFUND_QUERY);
+ assertEquals(msg.getRefundTime(), "1700000900");
+ assertEquals(msg.getOrderTime(), "1699990000");
+ assertEquals(msg.getChannelBill(), "apple_bill_001");
+ assertEquals(msg.getBundleid(), "com.example.app");
+ assertEquals(msg.getXpayProductId(), "product_xyz");
+ assertEquals(msg.getPCount(), "1");
+ assertEquals(msg.getRefundRequestReason(), "不喜欢");
+ assertEquals(msg.getProvideStatus(), "1");
+ }
}
From da27a75950ca99986c8359a235c5dff839aece1d Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Wed, 27 May 2026 20:50:32 +0800
Subject: [PATCH 20/38] =?UTF-8?q?:art:=20#4018=20=E3=80=90=E5=BE=AE?=
=?UTF-8?q?=E4=BF=A1=E6=94=AF=E4=BB=98=E3=80=91=E4=B8=BA=E7=9B=B4=E8=BF=9E?=
=?UTF-8?q?=E5=95=86=E6=88=B7=E8=A1=A5=E9=BD=90=E2=80=9C=E7=94=A8=E6=88=B7?=
=?UTF-8?q?=E6=8E=88=E6=9D=83=E5=85=8D=E7=A1=AE=E8=AE=A4=E6=A8=A1=E5=BC=8F?=
=?UTF-8?q?=E2=80=9D=E8=BD=AC=E8=B4=A6=E6=8E=A5=E5=8F=A3=E8=83=BD=E5=8A=9B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../service/MerchantTransferService.java | 88 +++++++++++++++++++
.../impl/MerchantTransferServiceImpl.java | 42 +++++++++
.../impl/MerchantTransferServiceImplTest.java | 50 +++++++++++
3 files changed, 180 insertions(+)
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 3b6c19cc9c..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;
/**
@@ -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/impl/MerchantTransferServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantTransferServiceImpl.java
index df4b36fdb9..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;
@@ -125,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/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");
+ }
+
}
From 20ce58b93f2b69a674bb4c9ef85bf24a90f6e464 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Wed, 27 May 2026 21:08:47 +0800
Subject: [PATCH 21/38] =?UTF-8?q?:art:=20#4021=20=E3=80=90=E8=A7=86?=
=?UTF-8?q?=E9=A2=91=E5=8F=B7=E3=80=91=E4=BF=AE=E5=A4=8D=E7=94=B5=E5=95=86?=
=?UTF-8?q?=E9=83=A8=E5=88=86=E6=8E=A5=E5=8F=A3URL=E8=B7=AF=E5=BE=84?=
=?UTF-8?q?=E5=89=8D=E7=BC=80=E6=9C=AA=E5=90=8C=E6=AD=A5=E5=AE=98=E6=96=B9?=
=?UTF-8?q?=E8=BF=81=E7=A7=BB=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../constant/WxChannelApiUrlConstants.java | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java
index 4859b723fb..6c2076d85b 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java
@@ -27,9 +27,9 @@ public interface Basics {
/** 获取店铺基本信息 */
String GET_SHOP_INFO = "https://api.weixin.qq.com/channels/ec/basics/info/get";
/** 上传图片 */
- String IMG_UPLOAD_URL = "https://api.weixin.qq.com/channels/ec/basics/img/upload";
+ String IMG_UPLOAD_URL = "https://api.weixin.qq.com/shop/ec/basics/img/upload";
/** 上传资质图片 */
- String UPLOAD_QUALIFICATION_FILE = "https://api.weixin.qq.com/channels/ec/basics/qualification/upload";
+ String UPLOAD_QUALIFICATION_FILE = "https://api.weixin.qq.com/shop/ec/basics/qualification/upload";
/** 下载图片 */
String GET_IMG_URL = "https://api.weixin.qq.com/channels/ec/basics/media/get";
/** 获取地址编码 */
@@ -40,9 +40,9 @@ public interface Basics {
public interface Category {
/** 获取所有的类目 */
- String LIST_ALL_CATEGORY_URL = "https://api.weixin.qq.com/channels/ec/category/all";
+ String LIST_ALL_CATEGORY_URL = "https://api.weixin.qq.com/shop/ec/category/all";
/** 获取类目详情 */
- String GET_CATEGORY_DETAIL_URL = "https://api.weixin.qq.com/channels/ec/category/detail";
+ String GET_CATEGORY_DETAIL_URL = "https://api.weixin.qq.com/shop/ec/category/detail";
/** 获取可用的子类目详情 */
String AVAILABLE_CATEGORY_URL = "https://api.weixin.qq.com/channels/ec/category/availablesoncategories/get";
/** 上传类目资质 */
@@ -50,7 +50,7 @@ public interface Category {
/** 获取类目审核结果 */
String GET_CATEGORY_AUDIT_URL = "https://api.weixin.qq.com/channels/ec/category/audit/get";
/** 取消类目提审 */
- String CANCEL_CATEGORY_AUDIT_URL = "https://api.weixin.qq.com/channels/ec/category/audit/cancel";
+ String CANCEL_CATEGORY_AUDIT_URL = "https://api.weixin.qq.com/shop/ec/category/audit/cancel";
/** 获取账号申请通过的类目和资质信息 */
String LIST_PASS_CATEGORY_URL = "https://api.weixin.qq.com/channels/ec/category/list/get";
/** 获取店铺的类目权限列表 */
@@ -103,13 +103,13 @@ public interface HomePage {
public interface Brand {
/** 获取品牌库列表 */
- String ALL_BRAND_URL = "https://api.weixin.qq.com/channels/ec/brand/all";
+ String ALL_BRAND_URL = "https://api.weixin.qq.com/shop/ec/brand/all";
/** 新增品牌资质 */
- String ADD_BRAND_URL = "https://api.weixin.qq.com/channels/ec/brand/add";
+ String ADD_BRAND_URL = "https://api.weixin.qq.com/shop/ec/brand/add";
/** 更新品牌资质 */
String UPDATE_BRAND_URL = "https://api.weixin.qq.com/channels/ec/brand/update";
/** 撤回品牌资质审核 */
- String CANCEL_BRAND_AUDIT_URL = "https://api.weixin.qq.com/channels/ec/brand/audit/cancel";
+ String CANCEL_BRAND_AUDIT_URL = "https://api.weixin.qq.com/shop/ec/brand/audit/cancel";
/** 删除品牌资质 */
String DELETE_BRAND_URL = "https://api.weixin.qq.com/channels/ec/brand/delete";
/** 获取品牌资质申请详情 */
From abc9a971b5d6df9fa0d47777234d52a86b9f2110 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Mon, 1 Jun 2026 21:20:49 +0800
Subject: [PATCH 22/38] =?UTF-8?q?:new:=20#4042=20=E5=A2=9E=E5=8A=A0?=
=?UTF-8?q?=E6=99=BA=E8=83=BD=E5=AF=B9=E8=AF=9D=20weixin-java-aispeech=20?=
=?UTF-8?q?=E6=A8=A1=E5=9D=97=EF=BC=8C=E6=89=BF=E6=8E=A5=E6=99=BA=E8=83=BD?=
=?UTF-8?q?=E5=AE=A2=E6=9C=8D=E5=92=8C=E7=9F=A5=E8=AF=86=E5=8A=A9=E7=90=86?=
=?UTF-8?q?=E7=9A=84=E6=8E=A5=E5=8F=A3=E8=83=BD=E5=8A=9B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 1 +
weixin-java-aispeech/pom.xml | 83 ++++++
.../aispeech/api/WxAispeechDialogService.java | 23 ++
.../api/WxAispeechKnowledgeService.java | 51 ++++
.../aispeech/api/WxAispeechService.java | 13 +
.../api/impl/WxAispeechDialogServiceImpl.java | 129 +++++++++
.../impl/WxAispeechKnowledgeServiceImpl.java | 184 +++++++++++++
.../impl/WxAispeechServiceHttpClientImpl.java | 4 +
.../WxAispeechServiceHttpComponentsImpl.java | 4 +
.../api/impl/WxAispeechServiceImpl.java | 250 ++++++++++++++++++
.../bean/dialog/AispeechApiResponse.java | 29 ++
.../aispeech/bean/dialog/AsyncTaskResult.java | 33 +++
.../aispeech/bean/dialog/BotIntent.java | 13 +
.../bean/dialog/DialogQueryRequest.java | 19 ++
.../aispeech/bean/dialog/DialogResult.java | 47 ++++
.../aispeech/bean/dialog/PublishProgress.java | 12 +
.../bean/knowledge/KnowledgeInfo.java | 31 +++
.../bean/knowledge/KnowledgeListResult.java | 19 ++
.../KnowledgeManualCreateRequest.java | 11 +
.../bean/knowledge/KnowledgeMoveProgress.java | 16 ++
.../bean/knowledge/KnowledgeMoveRequest.java | 16 ++
.../bean/knowledge/KnowledgeTagRequest.java | 12 +
.../knowledge/KnowledgeUpdateRequest.java | 12 +
.../knowledge/KnowledgeUrlCreateRequest.java | 10 +
.../config/WxAispeechConfigStorage.java | 31 +++
.../impl/WxAispeechDefaultConfigImpl.java | 88 ++++++
.../aispeech/util/WxAispeechSignUtil.java | 74 ++++++
.../WxAispeechKnowledgeServiceImplTest.java | 134 ++++++++++
.../aispeech/util/WxAispeechSignUtilTest.java | 31 +++
.../src/test/resources/testng.xml | 8 +
wx-java-bom/pom.xml | 5 +
31 files changed, 1393 insertions(+)
create mode 100644 weixin-java-aispeech/pom.xml
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/PublishProgress.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeInfo.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeListResult.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeManualCreateRequest.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeMoveProgress.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeMoveRequest.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeTagRequest.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeUpdateRequest.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/knowledge/KnowledgeUrlCreateRequest.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/config/WxAispeechConfigStorage.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/config/impl/WxAispeechDefaultConfigImpl.java
create mode 100644 weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/util/WxAispeechSignUtil.java
create mode 100644 weixin-java-aispeech/src/test/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImplTest.java
create mode 100644 weixin-java-aispeech/src/test/java/me/chanjar/weixin/aispeech/util/WxAispeechSignUtilTest.java
create mode 100644 weixin-java-aispeech/src/test/resources/testng.xml
diff --git a/pom.xml b/pom.xml
index d13d4bffcd..09d30e185f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -124,6 +124,7 @@
weixin-java-miniapp
weixin-java-open
weixin-java-qidian
+ weixin-java-aispeech
weixin-java-channel
spring-boot-starters
solon-plugins
diff --git a/weixin-java-aispeech/pom.xml b/weixin-java-aispeech/pom.xml
new file mode 100644
index 0000000000..2ca8aa84d8
--- /dev/null
+++ b/weixin-java-aispeech/pom.xml
@@ -0,0 +1,83 @@
+
+
+ 4.0.0
+
+ com.github.binarywang
+ wx-java
+ 4.8.3.B
+
+
+ weixin-java-aispeech
+ WxJava - Aispeech Java SDK
+ 微信智能对话 Java SDK
+
+
+
+ com.github.binarywang
+ weixin-java-common
+ ${project.version}
+
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+
+
+
+ org.testng
+ testng
+ test
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ src/test/resources/testng.xml
+
+
+
+
+
+
+
+
+ native-image
+
+ false
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.5.1
+
+
+ com.github.binarywang.wx.graal.GraalProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor,lombok.launch.AnnotationProcessorHider$ClaimingProcessor
+
+
+
+ com.github.binarywang
+ weixin-graal
+ ${project.version}
+
+
+
+
+
+
+
+
+
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java
new file mode 100644
index 0000000000..51d46562cb
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java
@@ -0,0 +1,23 @@
+package me.chanjar.weixin.aispeech.api;
+
+import java.util.List;
+import me.chanjar.weixin.aispeech.bean.dialog.AsyncTaskResult;
+import me.chanjar.weixin.aispeech.bean.dialog.BotIntent;
+import me.chanjar.weixin.aispeech.bean.dialog.DialogQueryRequest;
+import me.chanjar.weixin.aispeech.bean.dialog.DialogResult;
+import me.chanjar.weixin.aispeech.bean.dialog.PublishProgress;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+public interface WxAispeechDialogService {
+ String getAccessToken(String appid, String account) throws WxErrorException;
+
+ String importBotJson(int mode, List data) throws WxErrorException;
+
+ String publishBot() throws WxErrorException;
+
+ PublishProgress getPublishProgress(String env) throws WxErrorException;
+
+ AsyncTaskResult queryAsyncTask(String taskId) throws WxErrorException;
+
+ DialogResult query(DialogQueryRequest request) throws WxErrorException;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java
new file mode 100644
index 0000000000..fa27d48235
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java
@@ -0,0 +1,51 @@
+package me.chanjar.weixin.aispeech.api;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeInfo;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeManualCreateRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveProgress;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeTagRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUpdateRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUrlCreateRequest;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+public interface WxAispeechKnowledgeService {
+ KnowledgeInfo createKnowledgeByFile(String knowledgeBaseId, File file, String title, String description, String metadata)
+ throws WxErrorException;
+
+ KnowledgeInfo createKnowledgeByUrl(String knowledgeBaseId, KnowledgeUrlCreateRequest request) throws WxErrorException;
+
+ KnowledgeInfo createKnowledgeByManual(String knowledgeBaseId, KnowledgeManualCreateRequest request) throws WxErrorException;
+
+ List listKnowledge(String knowledgeBaseId, Integer page, Integer pageSize) throws WxErrorException;
+
+ List listKnowledgeByIds(List knowledgeIds) throws WxErrorException;
+
+ KnowledgeInfo getKnowledge(String knowledgeId) throws WxErrorException;
+
+ KnowledgeInfo updateKnowledge(String knowledgeId, KnowledgeUpdateRequest request) throws WxErrorException;
+
+ KnowledgeInfo updateManualKnowledge(String knowledgeId, KnowledgeManualCreateRequest request) throws WxErrorException;
+
+ boolean deleteKnowledge(String knowledgeId) throws WxErrorException;
+
+ boolean updateKnowledgeTags(List knowledgeIds, Long tagId) throws WxErrorException;
+
+ List searchKnowledge(String keyword, String knowledgeBaseId, Integer page, Integer pageSize)
+ throws WxErrorException;
+
+ String moveKnowledge(KnowledgeMoveRequest request) throws WxErrorException;
+
+ KnowledgeMoveProgress getMoveProgress(String taskId) throws WxErrorException;
+
+ boolean createKnowledgeBaseTag(String knowledgeBaseId, KnowledgeTagRequest request) throws WxErrorException;
+
+ boolean updateKnowledgeBaseTag(String knowledgeBaseId, String tagId, KnowledgeTagRequest request) throws WxErrorException;
+
+ String postRaw(String path, Object requestBody) throws WxErrorException;
+
+ String getRaw(String path, Map queryParams) throws WxErrorException;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java
new file mode 100644
index 0000000000..08ccf837e4
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java
@@ -0,0 +1,13 @@
+package me.chanjar.weixin.aispeech.api;
+
+import me.chanjar.weixin.aispeech.config.WxAispeechConfigStorage;
+
+public interface WxAispeechService {
+ WxAispeechDialogService getDialogService();
+
+ WxAispeechKnowledgeService getKnowledgeService();
+
+ WxAispeechConfigStorage getConfigStorage();
+
+ void setConfigStorage(WxAispeechConfigStorage configStorage);
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java
new file mode 100644
index 0000000000..9bd53b454e
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java
@@ -0,0 +1,129 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import me.chanjar.weixin.aispeech.api.WxAispeechDialogService;
+import me.chanjar.weixin.aispeech.bean.dialog.AispeechApiResponse;
+import me.chanjar.weixin.aispeech.bean.dialog.AsyncTaskResult;
+import me.chanjar.weixin.aispeech.bean.dialog.BotIntent;
+import me.chanjar.weixin.aispeech.bean.dialog.DialogQueryRequest;
+import me.chanjar.weixin.aispeech.bean.dialog.DialogResult;
+import me.chanjar.weixin.aispeech.bean.dialog.PublishProgress;
+import me.chanjar.weixin.aispeech.util.WxAispeechSignUtil;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+import org.apache.commons.lang3.StringUtils;
+
+public class WxAispeechDialogServiceImpl implements WxAispeechDialogService {
+ private final WxAispeechServiceImpl service;
+
+ public WxAispeechDialogServiceImpl(WxAispeechServiceImpl service) {
+ this.service = service;
+ }
+
+ @Override
+ public String getAccessToken(String appid, String account) throws WxErrorException {
+ Map request = new HashMap<>();
+ if (StringUtils.isNotBlank(account)) {
+ request.put("account", account);
+ }
+
+ String response = service.executeDialogPost("/v2/token", request, false, appid);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ String token = result.getData().get("access_token").getAsString();
+ service.getConfigStorage().setOpenAiToken(token);
+ return token;
+ }
+
+ @Override
+ public String importBotJson(int mode, List data) throws WxErrorException {
+ Map request = new HashMap<>();
+ request.put("mode", mode);
+ request.put("data", data);
+
+ String response = service.executeDialogPost("/v2/bot/import/json", request, true, null);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ return result.getData().get("task_id").getAsString();
+ }
+
+ @Override
+ public String publishBot() throws WxErrorException {
+ String response = service.executeDialogPost("/v2/bot/publish", "{}", true, null);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ return result.getRequestId();
+ }
+
+ @Override
+ public PublishProgress getPublishProgress(String env) throws WxErrorException {
+ Map request = new HashMap<>();
+ request.put("env", env);
+
+ String response = service.executeDialogPost("/v2/bot/effective_progress", request, true, null);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ return result.getData();
+ }
+
+ @Override
+ public AsyncTaskResult queryAsyncTask(String taskId) throws WxErrorException {
+ Map request = new HashMap<>();
+ request.put("task_id", taskId);
+
+ String response = service.executeDialogPost("/v2/async/fetch", request, true, null);
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type);
+ ensureSuccess(result);
+ return result.getData();
+ }
+
+ @Override
+ public DialogResult query(DialogQueryRequest request) throws WxErrorException {
+ String json = WxGsonBuilder.create().toJson(request);
+ String encrypted = WxAispeechSignUtil.encryptAesCbcToBase64(json, service.getConfigStorage().getAesKey());
+ String response = service.executeDialogPost("/v2/bot/query", encrypted, true, null);
+
+ String responseJson = response;
+ if (!looksLikeJson(response)) {
+ responseJson = WxAispeechSignUtil.decryptAesCbcFromBase64(response, service.getConfigStorage().getAesKey());
+ }
+
+ Type type = new TypeToken>() { } .getType();
+ AispeechApiResponse result = WxGsonBuilder.create().fromJson(responseJson, type);
+ ensureSuccess(result);
+
+ DialogResult dialogResult = result.getData();
+ if (dialogResult != null && looksLikeJson(dialogResult.getAnswer())) {
+ dialogResult.setRawAnswer(WxGsonBuilder.create().fromJson(dialogResult.getAnswer(), JsonElement.class));
+ }
+ return dialogResult;
+ }
+
+ private boolean looksLikeJson(String value) {
+ return StringUtils.isNotBlank(value) && (value.startsWith("{") || value.startsWith("["));
+ }
+
+ private void ensureSuccess(AispeechApiResponse> response) throws WxErrorException {
+ if (response == null) {
+ throw new WxErrorException("响应为空");
+ }
+ if (response.getCode() == null || response.getCode() != 0) {
+ throw new WxErrorException(WxError.builder()
+ .errorCode(response.getCode() == null ? -1 : response.getCode())
+ .errorMsg(response.getMsg())
+ .build());
+ }
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java
new file mode 100644
index 0000000000..708f12890d
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java
@@ -0,0 +1,184 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+import java.io.File;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+import me.chanjar.weixin.aispeech.api.WxAispeechKnowledgeService;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeInfo;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeListResult;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeManualCreateRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveProgress;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeTagRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUpdateRequest;
+import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUrlCreateRequest;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+import org.apache.commons.lang3.StringUtils;
+
+public class WxAispeechKnowledgeServiceImpl implements WxAispeechKnowledgeService {
+ private final WxAispeechServiceImpl service;
+
+ public WxAispeechKnowledgeServiceImpl(WxAispeechServiceImpl service) {
+ this.service = service;
+ }
+
+ @Override
+ public KnowledgeInfo createKnowledgeByFile(String knowledgeBaseId, File file, String title, String description, String metadata)
+ throws WxErrorException {
+ String response = service.executeKnowledgeMultipartPost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/file",
+ file, title, description, metadata);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public KnowledgeInfo createKnowledgeByUrl(String knowledgeBaseId, KnowledgeUrlCreateRequest request)
+ throws WxErrorException {
+ String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/url", request);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public KnowledgeInfo createKnowledgeByManual(String knowledgeBaseId, KnowledgeManualCreateRequest request)
+ throws WxErrorException {
+ String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/manual", request);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public List listKnowledge(String knowledgeBaseId, Integer page, Integer pageSize)
+ throws WxErrorException {
+ Map query = new HashMap<>();
+ query.put("page", page == null ? null : String.valueOf(page));
+ query.put("page_size", pageSize == null ? null : String.valueOf(pageSize));
+ String response = service.executeKnowledgeGet("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge", query);
+ KnowledgeListResult result = WxGsonBuilder.create().fromJson(response, KnowledgeListResult.class);
+ return result == null ? null : result.getData();
+ }
+
+ @Override
+ public List listKnowledgeByIds(List knowledgeIds) throws WxErrorException {
+ if (knowledgeIds == null || knowledgeIds.isEmpty()) {
+ return null;
+ }
+ StringJoiner joiner = new StringJoiner(",");
+ for (String knowledgeId : knowledgeIds) {
+ if (StringUtils.isNotBlank(knowledgeId)) {
+ joiner.add(knowledgeId);
+ }
+ }
+ if (joiner.length() == 0) {
+ return null;
+ }
+
+ Map query = new HashMap<>();
+ query.put("ids", joiner.toString());
+ String response = service.executeKnowledgeGet("/api/v1/knowledge/batch", query);
+ return parseKnowledgeInfoList(response);
+ }
+
+ @Override
+ public KnowledgeInfo getKnowledge(String knowledgeId) throws WxErrorException {
+ String response = service.executeKnowledgeGet("/api/v1/knowledge/" + knowledgeId, null);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public KnowledgeInfo updateKnowledge(String knowledgeId, KnowledgeUpdateRequest request) throws WxErrorException {
+ String response = service.executeKnowledgePut("/api/v1/knowledge/" + knowledgeId, request);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public KnowledgeInfo updateManualKnowledge(String knowledgeId, KnowledgeManualCreateRequest request) throws WxErrorException {
+ String response = service.executeKnowledgePut("/api/v1/knowledge/manual/" + knowledgeId, request);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class);
+ }
+
+ @Override
+ public boolean deleteKnowledge(String knowledgeId) throws WxErrorException {
+ service.executeKnowledgeDelete("/api/v1/knowledge/" + knowledgeId);
+ return true;
+ }
+
+ @Override
+ public boolean updateKnowledgeTags(List knowledgeIds, Long tagId) throws WxErrorException {
+ if (knowledgeIds == null || knowledgeIds.isEmpty() || tagId == null) {
+ return false;
+ }
+
+ Map request = new HashMap<>();
+ request.put("knowledge_ids", knowledgeIds);
+ request.put("tag_id", tagId);
+ String response = service.executeKnowledgePut("/api/v1/knowledge/tags", request);
+ return StringUtils.isNotBlank(response);
+ }
+
+ @Override
+ public List searchKnowledge(String keyword, String knowledgeBaseId, Integer page, Integer pageSize)
+ throws WxErrorException {
+ Map query = new HashMap<>();
+ query.put("keyword", keyword);
+ query.put("knowledge_base_id", knowledgeBaseId);
+ query.put("page", page == null ? null : String.valueOf(page));
+ query.put("page_size", pageSize == null ? null : String.valueOf(pageSize));
+ String response = service.executeKnowledgeGet("/api/v1/knowledge/search", query);
+ return parseKnowledgeInfoList(response);
+ }
+
+ @Override
+ public String moveKnowledge(KnowledgeMoveRequest request) throws WxErrorException {
+ return service.executeKnowledgePost("/api/v1/knowledge/move", request);
+ }
+
+ @Override
+ public KnowledgeMoveProgress getMoveProgress(String taskId) throws WxErrorException {
+ String response = service.executeKnowledgeGet("/api/v1/knowledge/move/progress/" + taskId, null);
+ return WxGsonBuilder.create().fromJson(response, KnowledgeMoveProgress.class);
+ }
+
+ @Override
+ public boolean createKnowledgeBaseTag(String knowledgeBaseId, KnowledgeTagRequest request) throws WxErrorException {
+ String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/tags", request);
+ return StringUtils.isNotBlank(response);
+ }
+
+ @Override
+ public boolean updateKnowledgeBaseTag(String knowledgeBaseId, String tagId, KnowledgeTagRequest request)
+ throws WxErrorException {
+ String response = service.executeKnowledgePut("/api/v1/knowledge-bases/" + knowledgeBaseId + "/tags/" + tagId, request);
+ return StringUtils.isNotBlank(response);
+ }
+
+ @Override
+ public String postRaw(String path, Object requestBody) throws WxErrorException {
+ return service.executeKnowledgePost(path, requestBody);
+ }
+
+ @Override
+ public String getRaw(String path, Map queryParams) throws WxErrorException {
+ return service.executeKnowledgeGet(path, queryParams);
+ }
+
+ private List parseKnowledgeInfoList(String response) {
+ if (StringUtils.isBlank(response)) {
+ return null;
+ }
+
+ JsonElement element = WxGsonBuilder.create().fromJson(response, JsonElement.class);
+ Type listType = new TypeToken>() { } .getType();
+ if (element != null && element.isJsonObject()) {
+ JsonObject object = element.getAsJsonObject();
+ if (object.has("data")) {
+ return WxGsonBuilder.create().fromJson(object.get("data"), listType);
+ }
+ }
+ return WxGsonBuilder.create().fromJson(element, listType);
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java
new file mode 100644
index 0000000000..e37d60e352
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java
@@ -0,0 +1,4 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+public class WxAispeechServiceHttpClientImpl extends WxAispeechServiceHttpComponentsImpl {
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java
new file mode 100644
index 0000000000..ac91d98938
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java
@@ -0,0 +1,4 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+public class WxAispeechServiceHttpComponentsImpl extends WxAispeechServiceImpl {
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java
new file mode 100644
index 0000000000..37a657cef2
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java
@@ -0,0 +1,250 @@
+package me.chanjar.weixin.aispeech.api.impl;
+
+import com.google.gson.Gson;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.UUID;
+import me.chanjar.weixin.aispeech.api.WxAispeechDialogService;
+import me.chanjar.weixin.aispeech.api.WxAispeechKnowledgeService;
+import me.chanjar.weixin.aispeech.api.WxAispeechService;
+import me.chanjar.weixin.aispeech.config.WxAispeechConfigStorage;
+import me.chanjar.weixin.aispeech.util.WxAispeechSignUtil;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.hc.DefaultHttpComponentsClientBuilder;
+import me.chanjar.weixin.common.util.http.hc.HttpComponentsClientBuilder;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.classic.methods.HttpPut;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.net.URIBuilder;
+import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
+
+public class WxAispeechServiceImpl implements WxAispeechService {
+ private static final Gson GSON = new Gson();
+
+ private final WxAispeechDialogService dialogService = new WxAispeechDialogServiceImpl(this);
+ private final WxAispeechKnowledgeService knowledgeService = new WxAispeechKnowledgeServiceImpl(this);
+
+ private WxAispeechConfigStorage configStorage;
+ private CloseableHttpClient httpClient;
+ private HttpHost proxy;
+
+ @Override
+ public WxAispeechDialogService getDialogService() {
+ return dialogService;
+ }
+
+ @Override
+ public WxAispeechKnowledgeService getKnowledgeService() {
+ return knowledgeService;
+ }
+
+ @Override
+ public WxAispeechConfigStorage getConfigStorage() {
+ return configStorage;
+ }
+
+ @Override
+ public void setConfigStorage(WxAispeechConfigStorage configStorage) {
+ this.configStorage = configStorage;
+ this.initHttp();
+ }
+
+ protected void initHttp() {
+ HttpComponentsClientBuilder builder = configStorage.getHttpComponentsClientBuilder();
+ if (builder == null) {
+ builder = DefaultHttpComponentsClientBuilder.get();
+ }
+
+ builder.httpProxyHost(configStorage.getHttpProxyHost())
+ .httpProxyPort(configStorage.getHttpProxyPort())
+ .httpProxyUsername(configStorage.getHttpProxyUsername())
+ .httpProxyPassword(configStorage.getHttpProxyPassword() == null ? null :
+ configStorage.getHttpProxyPassword().toCharArray());
+
+ if (configStorage.getHttpProxyHost() != null && configStorage.getHttpProxyPort() > 0) {
+ this.proxy = new HttpHost(configStorage.getHttpProxyHost(), configStorage.getHttpProxyPort());
+ } else {
+ this.proxy = null;
+ }
+
+ this.httpClient = builder.build();
+ }
+
+ protected String executeDialogPost(String path, Object requestBody, boolean withOpenToken, String appid)
+ throws WxErrorException {
+ String body = toBody(requestBody);
+ String requestId = UUID.randomUUID().toString();
+ long timestamp = System.currentTimeMillis() / 1000;
+ String nonce = randomNonce();
+ String sign = WxAispeechSignUtil.calcDialogSign(configStorage.getToken(), timestamp, nonce, body);
+ String resolvedAppid = StringUtils.defaultIfBlank(appid, configStorage.getAppid());
+
+ HttpPost request = new HttpPost(configStorage.getDialogApiBaseUrl() + path);
+ request.setHeader("request_id", requestId);
+ request.setHeader("timestamp", String.valueOf(timestamp));
+ request.setHeader("nonce", nonce);
+ request.setHeader("sign", sign);
+ request.setHeader("Content-Type", ContentType.APPLICATION_JSON.getMimeType());
+ if (withOpenToken) {
+ if (StringUtils.isBlank(configStorage.getOpenAiToken())) {
+ throw new WxErrorException("X-OPENAI-TOKEN不能为空,请先调用getAccessToken或手动设置");
+ }
+ request.setHeader("X-OPENAI-TOKEN", configStorage.getOpenAiToken());
+ } else {
+ if (StringUtils.isBlank(resolvedAppid)) {
+ throw new WxErrorException("X-APPID不能为空");
+ }
+ request.setHeader("X-APPID", resolvedAppid);
+ }
+ request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));
+ return executeRequest(request);
+ }
+
+ protected String executeKnowledgeGet(String path, Map queryParams) throws WxErrorException {
+ try {
+ URIBuilder builder = new URIBuilder(configStorage.getKnowledgeApiBaseUrl() + path);
+ if (queryParams != null) {
+ for (Map.Entry entry : queryParams.entrySet()) {
+ if (entry.getValue() != null) {
+ builder.addParameter(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+ HttpGet request = new HttpGet(builder.build());
+ enrichKnowledgeHeaders(request, "");
+ return executeRequest(request);
+ } catch (Exception e) {
+ throw toWxErrorException(e);
+ }
+ }
+
+ protected String executeKnowledgePost(String path, Object requestBody) throws WxErrorException {
+ String body = toBody(requestBody);
+ HttpPost request = new HttpPost(configStorage.getKnowledgeApiBaseUrl() + path);
+ request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));
+ enrichKnowledgeHeaders(request, body);
+ return executeRequest(request);
+ }
+
+ protected String executeKnowledgePut(String path, Object requestBody) throws WxErrorException {
+ String body = toBody(requestBody);
+ HttpPut request = new HttpPut(configStorage.getKnowledgeApiBaseUrl() + path);
+ request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));
+ enrichKnowledgeHeaders(request, body);
+ return executeRequest(request);
+ }
+
+ protected String executeKnowledgeMultipartPost(String path, File file, String title, String description, String metadata)
+ throws WxErrorException {
+ HttpPost request = new HttpPost(configStorage.getKnowledgeApiBaseUrl() + path);
+ MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+ builder.addBinaryBody("file", file, ContentType.DEFAULT_BINARY, file.getName());
+ if (StringUtils.isNotBlank(title)) {
+ builder.addTextBody("title", title, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8));
+ }
+ if (StringUtils.isNotBlank(description)) {
+ builder.addTextBody("description", description, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8));
+ }
+ if (StringUtils.isNotBlank(metadata)) {
+ builder.addTextBody("metadata", metadata, ContentType.APPLICATION_JSON);
+ }
+ HttpEntity entity = builder.build();
+ request.setEntity(entity);
+ if (entity.getContentType() != null) {
+ request.setHeader("Content-Type", entity.getContentType());
+ }
+ enrichKnowledgeHeaders(request, "");
+ return executeRequest(request);
+ }
+
+ protected String executeKnowledgeDelete(String path) throws WxErrorException {
+ HttpUriRequestBase request = new HttpUriRequestBase("DELETE", URI.create(configStorage.getKnowledgeApiBaseUrl() + path));
+ enrichKnowledgeHeaders(request, "");
+ return executeRequest(request);
+ }
+
+ private void enrichKnowledgeHeaders(HttpUriRequestBase request, String body) throws WxErrorException {
+ if (StringUtils.isBlank(configStorage.getAppid())) {
+ throw new WxErrorException("知识助理请求需要配置appid");
+ }
+ if (StringUtils.isBlank(configStorage.getSecretKey())) {
+ throw new WxErrorException("知识助理请求需要配置secretKey");
+ }
+
+ String requestId = UUID.randomUUID().toString();
+ long timestamp = System.currentTimeMillis() / 1000;
+ String nonce = randomNonce();
+ String signature = WxAispeechSignUtil.calcKnowledgeSignature(configStorage.getSecretKey(), timestamp, nonce,
+ requestId, body);
+
+ request.setHeader("X-APPID", configStorage.getAppid());
+ request.setHeader("X-Request-ID", requestId);
+ request.setHeader("X-Timestamp", String.valueOf(timestamp));
+ request.setHeader("X-Nonce", nonce);
+ request.setHeader("X-Signature", signature);
+ if (!request.containsHeader("Content-Type")) {
+ request.setHeader("Content-Type", ContentType.APPLICATION_JSON.getMimeType());
+ }
+ }
+
+ private String executeRequest(HttpUriRequestBase request) throws WxErrorException {
+ if (this.proxy != null) {
+ RequestConfig requestConfig = RequestConfig.custom().setProxy(this.proxy).build();
+ request.setConfig(requestConfig);
+ }
+
+ try (CloseableHttpResponse response = httpClient.execute(request)) {
+ int statusCode = response.getCode();
+ HttpEntity entity = response.getEntity();
+ String body = entity == null ? "" : EntityUtils.toString(entity, StandardCharsets.UTF_8);
+ if (statusCode >= 200 && statusCode < 300) {
+ return body;
+ }
+
+ throw new WxErrorException(WxError.builder().errorCode(statusCode).errorMsg(body).build());
+ } catch (IOException | ParseException e) {
+ throw toWxErrorException(e);
+ }
+ }
+
+ protected T fromJson(String json, Class clazz) {
+ return GSON.fromJson(json, clazz);
+ }
+
+ private String toBody(Object requestBody) {
+ if (requestBody == null) {
+ return "{}";
+ }
+ if (requestBody instanceof String) {
+ return (String) requestBody;
+ }
+ return GSON.toJson(requestBody);
+ }
+
+ private WxErrorException toWxErrorException(Exception e) {
+ if (e instanceof WxErrorException) {
+ return (WxErrorException) e;
+ }
+ return new WxErrorException(e);
+ }
+
+ private String randomNonce() {
+ return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java
new file mode 100644
index 0000000000..24595b8b46
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java
@@ -0,0 +1,29 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+@Data
+public class AispeechApiResponse {
+ private Integer code;
+ private String msg;
+ @SerializedName("request_id")
+ private String requestId;
+ private T data;
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public String getMsg() {
+ return msg;
+ }
+
+ public String getRequestId() {
+ return requestId;
+ }
+
+ public T getData() {
+ return data;
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java
new file mode 100644
index 0000000000..a806fb368a
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java
@@ -0,0 +1,33 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import com.google.gson.JsonElement;
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class AsyncTaskResult {
+ private Integer state;
+ private String msg;
+ private Integer progress;
+ private Long start;
+ private Long end;
+ private String url;
+ private Integer totalCount;
+ private Integer successCount;
+ private Integer failCount;
+ private JsonElement successSkillInfo;
+ private List successSkillInfoList;
+
+ @Data
+ public static class SkillInfo {
+ private Long id;
+ private String name;
+ private List intents;
+ }
+
+ @Data
+ public static class IntentInfo {
+ private Long id;
+ private String name;
+ }
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java
new file mode 100644
index 0000000000..3927461fc8
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java
@@ -0,0 +1,13 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class BotIntent {
+ private String skill;
+ private String intent;
+ private Boolean disable;
+ private List questions;
+ private List answers;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java
new file mode 100644
index 0000000000..dd748957ff
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java
@@ -0,0 +1,19 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import com.google.gson.annotations.SerializedName;
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class DialogQueryRequest {
+ private String query;
+ private String env;
+ @SerializedName("first_priority_skills")
+ private List firstPrioritySkills;
+ @SerializedName("second_priority_skills")
+ private List secondPrioritySkills;
+ @SerializedName("user_name")
+ private String userName;
+ private String avatar;
+ private String userid;
+}
diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java
new file mode 100644
index 0000000000..575628dc10
--- /dev/null
+++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java
@@ -0,0 +1,47 @@
+package me.chanjar.weixin.aispeech.bean.dialog;
+
+import com.google.gson.JsonElement;
+import com.google.gson.annotations.SerializedName;
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class DialogResult {
+ private String answer;
+ @SerializedName("answer_type")
+ private String answerType;
+ @SerializedName("skill_name")
+ private String skillName;
+ @SerializedName("intent_name")
+ private String intentName;
+ @SerializedName("msg_id")
+ private String msgId;
+ private List
+
+ com.github.binarywang
+ weixin-java-aispeech
+ ${project.version}
+
From 9de8759f0bb5762f7f66f198309f6d2c8e91eeb7 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Mon, 1 Jun 2026 21:57:09 +0800
Subject: [PATCH 23/38] =?UTF-8?q?:new:=20#4040=20=E3=80=90=E5=B0=8F?=
=?UTF-8?q?=E7=A8=8B=E5=BA=8F=E3=80=91=E6=96=B0=E5=A2=9E=20URL=20Link=20?=
=?UTF-8?q?=E4=BA=8C=E7=BB=B4=E7=A0=81=E5=BF=AB=E9=80=9F=E8=B7=B3=E8=BD=AC?=
=?UTF-8?q?=E8=A7=84=E5=88=99=E7=AE=A1=E7=90=86=E6=9C=8D=E5=8A=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../wx/miniapp/api/WxMaQrcodeJumpService.java | 56 +++++++++++
.../wx/miniapp/api/WxMaService.java | 9 ++
.../miniapp/api/impl/BaseWxMaServiceImpl.java | 6 ++
.../api/impl/WxMaQrcodeJumpServiceImpl.java | 76 ++++++++++++++
.../bean/qrcode/WxMaQrcodeJumpRule.java | 58 +++++++++++
.../WxMaQrcodeJumpRuleListResponse.java | 28 ++++++
.../bean/qrcode/WxMaQrcodeJumpWxaItem.java | 33 +++++++
.../miniapp/constant/WxMaApiUrlConstants.java | 10 ++
.../impl/WxMaQrcodeJumpServiceImplTest.java | 99 +++++++++++++++++++
9 files changed, 375 insertions(+)
create mode 100644 weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaQrcodeJumpService.java
create mode 100644 weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaQrcodeJumpServiceImpl.java
create mode 100644 weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/qrcode/WxMaQrcodeJumpRule.java
create mode 100644 weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/qrcode/WxMaQrcodeJumpRuleListResponse.java
create mode 100644 weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/qrcode/WxMaQrcodeJumpWxaItem.java
create mode 100644 weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaQrcodeJumpServiceImplTest.java
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaQrcodeJumpService.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaQrcodeJumpService.java
new file mode 100644
index 0000000000..81bd0f671c
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaQrcodeJumpService.java
@@ -0,0 +1,56 @@
+package cn.binarywang.wx.miniapp.api;
+
+import cn.binarywang.wx.miniapp.bean.qrcode.WxMaQrcodeJumpRule;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+import java.util.List;
+
+/**
+ * 小程序 URL Link 二维码快速跳转规则管理服务。
+ */
+public interface WxMaQrcodeJumpService {
+
+ /**
+ * 添加二维码快速跳转规则。
+ *
+ * 文档地址:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-link/qr-code-quickly-jump.html
+ *
+ * @param rule 规则
+ * @return 结果(errmsg/errcode)
+ */
+ String addRule(WxMaQrcodeJumpRule rule) throws WxErrorException;
+
+ /**
+ * 获取二维码快速跳转规则。
+ *
+ * 文档地址:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-link/get-qr-code-jump-rule.html
+ *
+ * @param isDefault 是否查询默认规则
+ * @param prefix 路径前缀(最长 32 个字符)
+ * @return 二维码规则列表
+ */
+ List getRules(Boolean isDefault, String prefix) throws WxErrorException;
+
+ /**
+ * 分页获取二维码快速跳转规则列表。
+ *
+ * 文档地址:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-link/get-qr-code-jump-rule-list.html
+ *
+ * @param getType 1:查询前缀匹配的规则;2:查询默认规则
+ * @param pageNum 页码,从 1 开始
+ * @param pageSize 每页条数,最多 20
+ * @return 二维码规则列表
+ */
+ List getRuleList(Integer getType, Integer pageNum, Integer pageSize) throws WxErrorException;
+
+ /**
+ * 删除二维码快速跳转规则。
+ *
+ * 文档地址:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-link/delete-qr-code-jump-rule.html
+ *
+ * @param prefix 路径前缀
+ * @return 结果(errmsg/errcode)
+ */
+ String deleteRule(String prefix) throws WxErrorException;
+}
+
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java
index 730a8c5840..4e001c6409 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java
@@ -472,6 +472,15 @@ WxMaApiResponse execute(
*/
WxMaLinkService getLinkService();
+ /**
+ * 获取 URL Link 二维码快速跳转规则管理服务对象。
+ *
+ * 文档:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-link/qr-code-quickly-jump.html
+ *
+ * @return 二维码快速跳转规则管理服务对象WxMaQrcodeJumpService
+ */
+ WxMaQrcodeJumpService getQrcodeJumpService();
+
/**
* 获取电子发票报销方服务接口服务对象。
*
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/BaseWxMaServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/BaseWxMaServiceImpl.java
index 6ab0293e0c..9d6c2c0fa6 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/BaseWxMaServiceImpl.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/BaseWxMaServiceImpl.java
@@ -140,6 +140,7 @@ public abstract class BaseWxMaServiceImpl implements WxMaService, RequestH
new WxMaShopAfterSaleServiceImpl(this);
private final WxMaShopDeliveryService shopDeliveryService = new WxMaShopDeliveryServiceImpl(this);
private final WxMaLinkService linkService = new WxMaLinkServiceImpl(this);
+ private final WxMaQrcodeJumpService qrcodeJumpService = new WxMaQrcodeJumpServiceImpl(this);
private final WxMaReimburseInvoiceService reimburseInvoiceService =
new WxMaReimburseInvoiceServiceImpl(this);
private final WxMaDeviceSubscribeService deviceSubscribeService =
@@ -788,6 +789,11 @@ public WxMaLinkService getLinkService() {
return this.linkService;
}
+ @Override
+ public WxMaQrcodeJumpService getQrcodeJumpService() {
+ return this.qrcodeJumpService;
+ }
+
@Override
public WxMaReimburseInvoiceService getReimburseInvoiceService() {
return this.reimburseInvoiceService;
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaQrcodeJumpServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaQrcodeJumpServiceImpl.java
new file mode 100644
index 0000000000..36d4f65545
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaQrcodeJumpServiceImpl.java
@@ -0,0 +1,76 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaQrcodeJumpService;
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.qrcode.WxMaQrcodeJumpRule;
+import cn.binarywang.wx.miniapp.bean.qrcode.WxMaQrcodeJumpRuleListResponse;
+import com.google.gson.JsonObject;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.QrcodeJump.*;
+import static me.chanjar.weixin.common.util.json.WxGsonBuilder.create;
+
+/**
+ * {@link WxMaQrcodeJumpService} 实现。
+ */
+@RequiredArgsConstructor
+public class WxMaQrcodeJumpServiceImpl implements WxMaQrcodeJumpService {
+ private final WxMaService wxMaService;
+
+ @Override
+ public String addRule(WxMaQrcodeJumpRule rule) throws WxErrorException {
+ return this.wxMaService.post(QRCODE_JUMP_ADD, create().toJson(rule));
+ }
+
+ @Override
+ public List getRules(Boolean isDefault, String prefix) throws WxErrorException {
+ final JsonObject request = new JsonObject();
+ if (isDefault != null) {
+ request.addProperty("is_default", isDefault);
+ }
+ if (prefix != null) {
+ request.addProperty("prefix", prefix);
+ }
+
+ String response = this.wxMaService.post(QRCODE_JUMP_GET, request.toString());
+ WxMaQrcodeJumpRuleListResponse result = create().fromJson(response, WxMaQrcodeJumpRuleListResponse.class);
+ if (result == null || result.getRuleList() == null || result.getRuleList().isEmpty()) {
+ return Collections.emptyList();
+ }
+ return result.getRuleList();
+ }
+
+ @Override
+ public List getRuleList(Integer getType, Integer pageNum, Integer pageSize) throws WxErrorException {
+ final JsonObject request = new JsonObject();
+ if (getType != null) {
+ request.addProperty("get_type", getType);
+ }
+ if (pageNum != null) {
+ request.addProperty("page_num", pageNum);
+ }
+ if (pageSize != null) {
+ request.addProperty("page_size", pageSize);
+ }
+
+ String response = this.wxMaService.post(QRCODE_JUMP_GET_LIST, request.toString());
+ WxMaQrcodeJumpRuleListResponse result = create().fromJson(response, WxMaQrcodeJumpRuleListResponse.class);
+ if (result == null || result.getRuleList() == null || result.getRuleList().isEmpty()) {
+ return Collections.emptyList();
+ }
+ return result.getRuleList();
+ }
+
+ @Override
+ public String deleteRule(String prefix) throws WxErrorException {
+ final Map request = new HashMap<>(1);
+ request.put("prefix", prefix);
+ return this.wxMaService.post(QRCODE_JUMP_DELETE, create().toJson(request));
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/qrcode/WxMaQrcodeJumpRule.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/qrcode/WxMaQrcodeJumpRule.java
new file mode 100644
index 0000000000..1bfdf18513
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/qrcode/WxMaQrcodeJumpRule.java
@@ -0,0 +1,58 @@
+package cn.binarywang.wx.miniapp.bean.qrcode;
+
+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;
+
+/**
+ * URL Link 二维码快速跳转规则。
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxMaQrcodeJumpRule implements Serializable {
+ private static final long serialVersionUID = -3450269467817402123L;
+
+ /**
+ * 跳转链接规则前缀,最多 32 个字符。
+ */
+ @SerializedName("prefix")
+ private String prefix;
+
+ /**
+ * 是否支持子路径匹配。
+ */
+ @SerializedName("permit_sub_rule")
+ private Boolean permitSubRule;
+
+ /**
+ * 跳转版本,1:正式版;2:测试版;3:体验版。
+ */
+ @SerializedName("open_version")
+ private Integer openVersion;
+
+ /**
+ * 正式版跳转页面。
+ */
+ @SerializedName("path")
+ private String path;
+
+ /**
+ * 测试版/体验版可跳转小程序信息。
+ */
+ @SerializedName("debug_wxa_info")
+ private List debugWxaInfo;
+
+ /**
+ * 二维码规则是否失效。
+ */
+ @SerializedName("is_expire")
+ private Boolean isExpire;
+}
+
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/qrcode/WxMaQrcodeJumpRuleListResponse.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/qrcode/WxMaQrcodeJumpRuleListResponse.java
new file mode 100644
index 0000000000..f279eff5dc
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/qrcode/WxMaQrcodeJumpRuleListResponse.java
@@ -0,0 +1,28 @@
+package cn.binarywang.wx.miniapp.bean.qrcode;
+
+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;
+
+/**
+ * URL Link 二维码快速跳转规则列表返回值。
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxMaQrcodeJumpRuleListResponse implements Serializable {
+ private static final long serialVersionUID = 6706970228943946110L;
+
+ /**
+ * 规则列表。
+ */
+ @SerializedName("rule_list")
+ private List ruleList;
+}
+
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/qrcode/WxMaQrcodeJumpWxaItem.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/qrcode/WxMaQrcodeJumpWxaItem.java
new file mode 100644
index 0000000000..d9a94d47db
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/qrcode/WxMaQrcodeJumpWxaItem.java
@@ -0,0 +1,33 @@
+package cn.binarywang.wx.miniapp.bean.qrcode;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * URL Link 跳转规则中的小程序信息。
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxMaQrcodeJumpWxaItem implements Serializable {
+ private static final long serialVersionUID = -675341413130655505L;
+
+ /**
+ * 小程序 appid。
+ */
+ @SerializedName("appid")
+ private String appId;
+
+ /**
+ * 跳转页面路径。
+ */
+ @SerializedName("path")
+ private String path;
+}
+
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaApiUrlConstants.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaApiUrlConstants.java
index 815d47c623..b9c237a115 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaApiUrlConstants.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaApiUrlConstants.java
@@ -301,6 +301,16 @@ public interface Link {
String QUERY_URLLINK_URL = "https://api.weixin.qq.com/wxa/query_urllink";
}
+ /**
+ * URL Link 二维码快速跳转规则管理.
+ */
+ public interface QrcodeJump {
+ String QRCODE_JUMP_ADD = "https://api.weixin.qq.com/wxaapi/wxaqrcodefast/addcategoryrule";
+ String QRCODE_JUMP_GET = "https://api.weixin.qq.com/wxaapi/wxaqrcodefast/getcategory";
+ String QRCODE_JUMP_GET_LIST = "https://api.weixin.qq.com/wxaapi/wxaqrcodefast/getcategorybypage";
+ String QRCODE_JUMP_DELETE = "https://api.weixin.qq.com/wxaapi/wxaqrcodefast/deletecategoryrule";
+ }
+
public interface ShortLink {
String GENERATE_SHORT_LINK_URL = "https://api.weixin.qq.com/wxa/genwxashortlink";
}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaQrcodeJumpServiceImplTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaQrcodeJumpServiceImplTest.java
new file mode 100644
index 0000000000..e6f438c8cc
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaQrcodeJumpServiceImplTest.java
@@ -0,0 +1,99 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaQrcodeJumpService;
+import cn.binarywang.wx.miniapp.bean.qrcode.WxMaQrcodeJumpRule;
+import me.chanjar.weixin.common.error.WxErrorException;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.util.List;
+
+import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.QrcodeJump.*;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+/**
+ * {@link WxMaQrcodeJumpServiceImpl} 单元测试。
+ */
+public class WxMaQrcodeJumpServiceImplTest {
+
+ private cn.binarywang.wx.miniapp.api.WxMaService wxMaService;
+ private WxMaQrcodeJumpService qrcodeJumpService;
+
+ @BeforeMethod
+ public void setUp() {
+ this.wxMaService = mock(cn.binarywang.wx.miniapp.api.WxMaService.class);
+ this.qrcodeJumpService = new WxMaQrcodeJumpServiceImpl(this.wxMaService);
+ }
+
+ @Test
+ public void testAddRule() throws WxErrorException {
+ when(this.wxMaService.post(anyString(), anyString())).thenReturn("{\"errcode\":0,\"errmsg\":\"ok\"}");
+
+ WxMaQrcodeJumpRule rule = WxMaQrcodeJumpRule.builder()
+ .prefix("/pages/index")
+ .permitSubRule(true)
+ .openVersion(1)
+ .path("pages/index")
+ .build();
+
+ String result = this.qrcodeJumpService.addRule(rule);
+ assertTrue(result.contains("\"errcode\":0"));
+ verify(this.wxMaService).post(eq(QRCODE_JUMP_ADD), anyString());
+ }
+
+ @Test
+ public void testGetRules() throws WxErrorException {
+ when(this.wxMaService.post(anyString(), anyString()))
+ .thenReturn("{\"rule_list\":[{\"prefix\":\"/pages/index\",\"path\":\"pages/index\"}]}");
+
+ List rules = this.qrcodeJumpService.getRules(false, "/pages");
+
+ assertNotNull(rules);
+ assertEquals(rules.size(), 1);
+ assertEquals(rules.get(0).getPrefix(), "/pages/index");
+ assertEquals(rules.get(0).getPath(), "pages/index");
+ verify(this.wxMaService).post(eq(QRCODE_JUMP_GET), eq("{\"is_default\":false,\"prefix\":\"/pages\"}"));
+ }
+
+ @Test
+ public void testGetRuleList() throws WxErrorException {
+ when(this.wxMaService.post(anyString(), anyString()))
+ .thenReturn("{\"rule_list\":[{\"prefix\":\"/pages/index\",\"path\":\"pages/index\"}]}");
+
+ List rules = this.qrcodeJumpService.getRuleList(1, 1, 20);
+
+ assertNotNull(rules);
+ assertEquals(rules.size(), 1);
+ assertEquals(rules.get(0).getPrefix(), "/pages/index");
+ verify(this.wxMaService).post(eq(QRCODE_JUMP_GET_LIST), eq("{\"get_type\":1,\"page_num\":1,\"page_size\":20}"));
+ }
+
+ @Test
+ public void testGetRuleListWhenNoRules() throws WxErrorException {
+ when(this.wxMaService.post(anyString(), anyString())).thenReturn("{\"errcode\":0,\"errmsg\":\"ok\"}");
+
+ List rules = this.qrcodeJumpService.getRuleList(null, null, null);
+
+ assertNotNull(rules);
+ assertTrue(rules.isEmpty());
+ verify(this.wxMaService).post(eq(QRCODE_JUMP_GET_LIST), eq("{}"));
+ }
+
+ @Test
+ public void testDeleteRule() throws WxErrorException {
+ when(this.wxMaService.post(anyString(), anyString())).thenReturn("{\"errcode\":0,\"errmsg\":\"ok\"}");
+
+ String result = this.qrcodeJumpService.deleteRule("/pages/index");
+
+ assertTrue(result.contains("\"errcode\":0"));
+ verify(this.wxMaService).post(eq(QRCODE_JUMP_DELETE), eq("{\"prefix\":\"/pages/index\"}"));
+ }
+}
+
From d427f0cf1f7f6b14f75e26cbfcb26353ad366eae Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Mon, 1 Jun 2026 22:12:41 +0800
Subject: [PATCH 24/38] =?UTF-8?q?:art:=20#4025=20=E3=80=90=E5=BE=AE?=
=?UTF-8?q?=E4=BF=A1=E6=94=AF=E4=BB=98=E3=80=91=E4=BF=AE=E5=A4=8D=E9=80=80?=
=?UTF-8?q?=E6=AC=BE=E8=AF=B7=E6=B1=82=20funds=5Faccount=20=E9=87=8D?=
=?UTF-8?q?=E5=A4=8D=E6=98=A0=E5=B0=84=E5=86=B2=E7=AA=81=EF=BC=8C=E7=A1=AE?=
=?UTF-8?q?=E4=BF=9D=20V3=20=E9=80=80=E6=AC=BE=E5=8F=82=E6=95=B0=E5=8F=AF?=
=?UTF-8?q?=E6=AD=A3=E7=A1=AE=E5=BA=8F=E5=88=97=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../request/WxPayPartnerRefundV3Request.java | 18 ------------------
1 file changed, 18 deletions(-)
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 a565388e60..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
@@ -43,22 +43,4 @@ public class WxPayPartnerRefundV3Request extends WxPayRefundV3Request implements
*/
@SerializedName(value = "sub_appid")
private String subAppid;
- /**
- *
- * 字段名:退款资金来源
- * 变量名:funds_account
- * 是否必填:否
- * 类型:string[1, 32]
- * 描述:
- * 若传递此参数则使用对应的资金账户退款,否则默认使用未结算资金退款(仅对老资金流商户适用)
- * 示例值:
- * UNSETTLED : 未结算资金
- * AVAILABLE : 可用余额
- * UNAVAILABLE : 不可用余额
- * OPERATION : 运营户
- * BASIC : 基本账户(含可用余额和不可用余额)
- *
- */
- @SerializedName(value = "funds_account")
- private String fundsAccount;
}
From 4284b0bc9706cced7dac41246edc39c84c7cedd0 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Mon, 1 Jun 2026 22:15:03 +0800
Subject: [PATCH 25/38] =?UTF-8?q?:art:=20#4023=20=E3=80=90=E8=A7=86?=
=?UTF-8?q?=E9=A2=91=E5=8F=B7=E3=80=91=E4=BF=AE=E5=A4=8D=20WxAssistantServ?=
=?UTF-8?q?iceImpl=20=E6=89=80=E6=9C=89=E6=96=B9=E6=B3=95=E8=AF=B7?=
=?UTF-8?q?=E6=B1=82=E5=8F=82=E6=95=B0=E6=9C=AA=E4=BC=A0=E9=80=92=E7=9A=84?=
=?UTF-8?q?=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../weixin/channel/api/impl/WxAssistantServiceImpl.java | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxAssistantServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxAssistantServiceImpl.java
index 55be5abcca..58d6d45269 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxAssistantServiceImpl.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxAssistantServiceImpl.java
@@ -31,25 +31,25 @@ public class WxAssistantServiceImpl implements WxAssistantService {
private final BaseWxChannelServiceImpl, ?> shopService;
@Override
public WxChannelBaseResponse addWindowProduct(AddWindowProductRequest req) throws WxErrorException {
- String resJson = shopService.post(ADD_WINDOW_PRODUCT_URL, "{}");
+ String resJson = shopService.post(ADD_WINDOW_PRODUCT_URL, req);
return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
}
@Override
public GetWindowProductResponse getWindowProduct(WindowProductRequest req) throws WxErrorException {
- String resJson = shopService.post(GET_WINDOW_PRODUCT_URL, "{}");
+ String resJson = shopService.post(GET_WINDOW_PRODUCT_URL, req);
return ResponseUtils.decode(resJson, GetWindowProductResponse.class);
}
@Override
public GetWindowProductListResponse getWindowProductList(GetWindowProductListRequest req) throws WxErrorException {
- String resJson = shopService.post(LIST_WINDOW_PRODUCT_URL, "{}");
+ String resJson = shopService.post(LIST_WINDOW_PRODUCT_URL, req);
return ResponseUtils.decode(resJson, GetWindowProductListResponse.class);
}
@Override
public WxChannelBaseResponse offWindowProduct(WindowProductRequest req) throws WxErrorException {
- String resJson = shopService.post(OFF_WINDOW_PRODUCT_URL, "{}");
+ String resJson = shopService.post(OFF_WINDOW_PRODUCT_URL, req);
return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
}
}
From 86b02922d04c9020a5ca2edc244f24fa9e913bc6 Mon Sep 17 00:00:00 2001
From: Binary Wang
Date: Wed, 3 Jun 2026 17:55:47 +0800
Subject: [PATCH 26/38] Create AGENTS.md
---
AGENTS.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 AGENTS.md
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000000..d7a5b96b34
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,5 @@
+## Review guidelines
+
+- 重点检查空指针、并发、资源释放、兼容性问题。
+- 不要只做代码风格建议,优先指出真实 bug 和回归风险。
+- 中文回复 review 结论。
From f8d0f5521b2150ba6e6a9bd8bffeda13d10d211a Mon Sep 17 00:00:00 2001
From: buaazyl
Date: Wed, 3 Jun 2026 18:00:51 +0800
Subject: [PATCH 27/38] =?UTF-8?q?:new:=20#4046=20=E3=80=90=E5=BE=AE?=
=?UTF-8?q?=E4=BF=A1=E6=94=AF=E4=BB=98=E3=80=91=E5=A2=9E=E5=8A=A0=E5=95=86?=
=?UTF-8?q?=E6=88=B7=E5=B9=B3=E5=8F=B0=E5=A4=84=E7=BD=AE=E8=AE=B0=E5=BD=95?=
=?UTF-8?q?=E5=9B=9E=E8=B0=83=E9=80=9A=E7=9F=A5=E5=AF=B9=E8=B1=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../notify/MerchantViolationNotifyResult.java | 74 +++++++++++++++++++
1 file changed, 74 insertions(+)
create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/MerchantViolationNotifyResult.java
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/MerchantViolationNotifyResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/MerchantViolationNotifyResult.java
new file mode 100644
index 0000000000..6d153a4e38
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/MerchantViolationNotifyResult.java
@@ -0,0 +1,74 @@
+package com.github.binarywang.wxpay.bean.notify;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 商户平台处置记录回调通知 产品介绍
+ *
+ * @author zhangyl
+ */
+@Data
+@NoArgsConstructor
+public class MerchantViolationNotifyResult implements Serializable,
+ WxPayBaseNotifyV3Result {
+ private static final long serialVersionUID = 1L;
+ /**
+ * 源数据
+ */
+ private OriginNotifyResponse rawData;
+ /**
+ * 解密后的数据
+ */
+ private DecryptNotifyResult result;
+
+ @Data
+ @NoArgsConstructor
+ public static class DecryptNotifyResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 处置记录对应的商户号
+ */
+ @SerializedName("sub_mchid")
+ private String subMchId;
+ /**
+ * 子商户公司名称
+ */
+ @SerializedName("company_name")
+ private String companyName;
+ /**
+ * 微信支付对违约商户处理通知的唯一标识,可用于去重
+ */
+ @SerializedName("record_id")
+ private String recordId;
+ /**
+ * 微信支付对违约商户的具体处罚方案,可根据具体的处罚方案指引商户登录商户平台/商家助手小程序进行申诉/相关操作,使用时请留意该值为处罚方法的文本内容,并非枚举值。
+ */
+ @SerializedName("punish_plan")
+ private String punishPlan;
+ /**
+ * 微信支付对违约商户的处置时间
+ */
+ @SerializedName("punish_time")
+ private String punishTime;
+ /**
+ * 微信支付对违约商户处罚方案的详细描述信息,补充处罚方案的相关影响。
+ */
+ @SerializedName("punish_description")
+ private String punishDescription;
+ /**
+ * 微信支付对违约商户定义的风险类型
+ */
+ @SerializedName("risk_type")
+ private String riskType;
+ /**
+ * 微信支付对违约商户定义的风险类型枚举值对应的中文描述
+ */
+ @SerializedName("risk_description")
+ private String riskDescription;
+ }
+}
From 25a332823a62134872d164dfecf2a7a01691b185 Mon Sep 17 00:00:00 2001
From: moil-xm <64048303+moil-xm@users.noreply.github.com>
Date: Fri, 5 Jun 2026 18:21:43 +0800
Subject: [PATCH 28/38] =?UTF-8?q?:art:=20=E4=BF=AE=E5=A4=8D=20Solon=20?=
=?UTF-8?q?=E7=89=88=20wx-java-mp=20=E5=9C=A8=E6=9C=AA=E6=98=BE=E5=BC=8F?=
=?UTF-8?q?=E9=85=8D=E7=BD=AE=20Redis=20host=20=E6=97=B6=E6=97=A0=E6=B3=95?=
=?UTF-8?q?=E5=9B=9E=E9=80=80=E5=88=B0=E7=94=A8=E6=88=B7=E8=87=AA=E5=AE=9A?=
=?UTF-8?q?=E4=B9=89=20RedissonClient=20=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../storage/WxMpInRedissonConfigStorageConfiguration.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java
index c1f5ebf0f3..7121f2d700 100644
--- a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java
+++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java
@@ -36,9 +36,9 @@ public WxMpConfigStorage wxMaConfig() {
}
private WxMpRedissonConfigImpl getWxMpInRedissonConfigStorage() {
- RedisProperties redisProperties = properties.getConfigStorage().getRedis();
+ String configuredHost = applicationContext.cfg().get(WxMpProperties.PREFIX + ".config-storage.redis.host");
RedissonClient redissonClient;
- if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
+ if (StringUtils.isNotEmpty(configuredHost)) {
redissonClient = applicationContext.getBean("wxMpRedissonClient");
} else {
redissonClient = applicationContext.getBean(RedissonClient.class);
From f2f5957777a4d630168d55e38f582e044f6dc74b Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Fri, 5 Jun 2026 18:24:10 +0800
Subject: [PATCH 29/38] =?UTF-8?q?:art:=20#4047=20=20=E3=80=90=E4=BC=81?=
=?UTF-8?q?=E4=B8=9A=E5=BE=AE=E4=BF=A1=E3=80=91=E9=80=9A=E8=AE=AF=E5=BD=95?=
=?UTF-8?q?=E5=90=8C=E6=AD=A5=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3=E8=A1=A5?=
=?UTF-8?q?=E9=BD=90=E2=80=9C=E7=8B=AC=E7=AB=8B=20secret=20+=20=E7=8B=AC?=
=?UTF-8?q?=E7=AB=8B=20access=20token=E2=80=9D=E7=9A=84=E8=B0=83=E7=94=A8?=
=?UTF-8?q?=E9=80=9A=E9=81=93?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../me/chanjar/weixin/cp/api/WxCpService.java | 39 +++
.../cp/api/impl/BaseWxCpServiceImpl.java | 23 ++
.../impl/WxCpServiceApacheHttpClientImpl.java | 45 +++
.../impl/WxCpServiceHttpComponentsImpl.java | 45 +++
.../weixin/cp/api/impl/WxCpServiceImpl.java | 43 +++
.../cp/api/impl/WxCpServiceJoddHttpImpl.java | 39 +++
.../cp/api/impl/WxCpServiceOkHttpImpl.java | 64 ++++-
.../weixin/cp/config/WxCpConfigStorage.java | 41 +++
.../cp/config/impl/WxCpDefaultConfigImpl.java | 53 ++++
.../cp/config/impl/WxCpRedisConfigImpl.java | 30 ++
.../cp/api/impl/BaseWxCpServiceImplTest.java | 5 +
.../WxCpServiceGetContactAccessTokenTest.java | 264 ++++++++++++++++++
...WxCpServiceGetMsgAuditAccessTokenTest.java | 10 +
13 files changed, 697 insertions(+), 4 deletions(-)
create mode 100644 weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpServiceGetContactAccessTokenTest.java
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java
index f66acc0252..269a69a475 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java
@@ -57,6 +57,19 @@ public interface WxCpService extends WxService {
*/
String getAccessToken(boolean forceRefresh) throws WxErrorException;
+ /**
+ *
+ * 获取通讯录同步access_token,本方法线程安全
+ * 通讯录同步相关接口仅支持通过"通讯录同步secret"调用,需要使用独立的access_token
+ * 详情请见: https://developer.work.weixin.qq.com/document/path/91579
+ *
+ *
+ * @param forceRefresh 强制刷新
+ * @return 通讯录同步专用的access token
+ * @throws WxErrorException the wx error exception
+ */
+ String getContactAccessToken(boolean forceRefresh) throws WxErrorException;
+
/**
*
* 获取会话存档access_token,本方法线程安全
@@ -220,6 +233,32 @@ public interface WxCpService extends WxService {
*/
String postForMsgAudit(String url, String postData) throws WxErrorException;
+ /**
+ *
+ * 使用通讯录同步access token发起get请求
+ * 通讯录同步相关API需要使用通讯录同步专用的secret获取独立的access token
+ *
+ *
+ * @param url 接口地址
+ * @param queryParam 请求参数
+ * @return the string
+ * @throws WxErrorException the wx error exception
+ */
+ String getForContact(String url, String queryParam) throws WxErrorException;
+
+ /**
+ *
+ * 使用通讯录同步access token发起post请求
+ * 通讯录同步相关API需要使用通讯录同步专用的secret获取独立的access token
+ *
+ *
+ * @param url 接口地址
+ * @param postData 请求body字符串
+ * @return the string
+ * @throws WxErrorException the wx error exception
+ */
+ String postForContact(String url, String postData) throws WxErrorException;
+
/**
*
* Service没有实现某个API的时候,可以用这个,
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
index 7c72cb9a8c..a3ec703ca4 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
@@ -313,6 +313,29 @@ public String postForMsgAudit(String url, String postData) throws WxErrorExcepti
return this.executeNormal(SimplePostRequestExecutor.create(this), urlWithToken, postData);
}
+ @Override
+ public String getForContact(String url, String queryParam) throws WxErrorException {
+ // 获取通讯录同步专用的access token
+ String contactAccessToken = getContactAccessToken(false);
+ // 拼接access_token参数
+ String urlWithToken = url + (url.contains("?") ? "&" : "?") + "access_token=" + contactAccessToken;
+ if (queryParam != null && !queryParam.isEmpty()) {
+ urlWithToken = urlWithToken + "&" + queryParam;
+ }
+ // 使用executeNormal方法,不自动添加token
+ return this.executeNormal(SimpleGetRequestExecutor.create(this), urlWithToken, null);
+ }
+
+ @Override
+ public String postForContact(String url, String postData) throws WxErrorException {
+ // 获取通讯录同步专用的access token
+ String contactAccessToken = getContactAccessToken(false);
+ // 拼接access_token参数
+ String urlWithToken = url + (url.contains("?") ? "&" : "?") + "access_token=" + contactAccessToken;
+ // 使用executeNormal方法,不自动添加token
+ return this.executeNormal(SimplePostRequestExecutor.create(this), urlWithToken, postData);
+ }
+
/**
* 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求.
*/
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java
index ef78116e12..8285e59df2 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java
@@ -75,6 +75,51 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
return this.configStorage.getAccessToken();
}
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+
+ Lock lock = this.configStorage.getContactAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+ // 使用通讯录同步secret获取access_token
+ String contactSecret = this.configStorage.getContactSecret();
+ if (contactSecret == null || contactSecret.trim().isEmpty()) {
+ throw new WxErrorException("通讯录同步secret未配置");
+ }
+ String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+ this.configStorage.getCorpId(), contactSecret);
+
+ try {
+ HttpGet httpGet = new HttpGet(url);
+ if (this.httpProxy != null) {
+ RequestConfig config = RequestConfig.custom()
+ .setProxy(this.httpProxy).build();
+ httpGet.setConfig(config);
+ }
+ String resultContent = getRequestHttpClient().execute(httpGet, ApacheBasicResponseHandler.INSTANCE);
+ WxError error = WxError.fromJson(resultContent, WxType.CP);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+
+ WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+ this.configStorage.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+ } catch (IOException e) {
+ throw new WxRuntimeException(e);
+ }
+ } finally {
+ lock.unlock();
+ }
+ return this.configStorage.getContactAccessToken();
+ }
+
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java
index 3ca041e7ec..129823df5a 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java
@@ -76,6 +76,51 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
return this.configStorage.getAccessToken();
}
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+
+ Lock lock = this.configStorage.getContactAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+ // 使用通讯录同步secret获取access_token
+ String contactSecret = this.configStorage.getContactSecret();
+ if (contactSecret == null || contactSecret.trim().isEmpty()) {
+ throw new WxErrorException("通讯录同步secret未配置");
+ }
+ String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+ this.configStorage.getCorpId(), contactSecret);
+
+ try {
+ HttpGet httpGet = new HttpGet(url);
+ if (this.httpProxy != null) {
+ RequestConfig config = RequestConfig.custom()
+ .setProxy(this.httpProxy).build();
+ httpGet.setConfig(config);
+ }
+ String resultContent = getRequestHttpClient().execute(httpGet, BasicResponseHandler.INSTANCE);
+ WxError error = WxError.fromJson(resultContent, WxType.CP);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+
+ WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+ this.configStorage.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+ } catch (IOException e) {
+ throw new WxRuntimeException(e);
+ }
+ } finally {
+ lock.unlock();
+ }
+ return this.configStorage.getContactAccessToken();
+ }
+
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java
index 7b651cbc08..69cc074be9 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java
@@ -70,6 +70,49 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
return configStorage.getAccessToken();
}
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ final WxCpConfigStorage configStorage = getWxCpConfigStorage();
+ if (!configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return configStorage.getContactAccessToken();
+ }
+ Lock lock = configStorage.getContactAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return configStorage.getContactAccessToken();
+ }
+ // 使用通讯录同步secret获取access_token
+ String contactSecret = configStorage.getContactSecret();
+ if (contactSecret == null || contactSecret.trim().isEmpty()) {
+ throw new WxErrorException("通讯录同步secret未配置");
+ }
+ String url = String.format(configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+ this.configStorage.getCorpId(), contactSecret);
+ try {
+ HttpGet httpGet = new HttpGet(url);
+ if (getRequestHttpProxy() != null) {
+ RequestConfig config = RequestConfig.custom().setProxy(getRequestHttpProxy()).build();
+ httpGet.setConfig(config);
+ }
+ String resultContent = getRequestHttpClient().execute(httpGet, ApacheBasicResponseHandler.INSTANCE);
+ WxError error = WxError.fromJson(resultContent, WxType.CP);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+
+ WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+ configStorage.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+ } catch (IOException e) {
+ throw new WxRuntimeException(e);
+ }
+ } finally {
+ lock.unlock();
+ }
+ return configStorage.getContactAccessToken();
+ }
+
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
final WxCpConfigStorage configStorage = getWxCpConfigStorage();
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java
index eba9315649..ef6d86c665 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java
@@ -65,6 +65,45 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
return this.configStorage.getAccessToken();
}
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+
+ Lock lock = this.configStorage.getContactAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+ // 使用通讯录同步secret获取access_token
+ String contactSecret = this.configStorage.getContactSecret();
+ if (contactSecret == null || contactSecret.trim().isEmpty()) {
+ throw new WxErrorException("通讯录同步secret未配置");
+ }
+ HttpRequest request = HttpRequest.get(String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+ this.configStorage.getCorpId(), contactSecret));
+ if (this.httpProxy != null) {
+ httpClient.useProxy(this.httpProxy);
+ }
+ request.withConnectionProvider(httpClient);
+ HttpResponse response = request.send();
+
+ String resultContent = response.bodyText();
+ WxError error = WxError.fromJson(resultContent, WxType.CP);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+ WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+ this.configStorage.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+ } finally {
+ lock.unlock();
+ }
+ return this.configStorage.getContactAccessToken();
+ }
+
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java
index ce77b37805..76847f1f93 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java
@@ -56,12 +56,15 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
this.configStorage.getCorpSecret()))
.get()
.build();
- String resultContent = null;
- try {
- Response response = client.newCall(request).execute();
+ String resultContent;
+ try (Response response = client.newCall(request).execute()) {
+ if (response.body() == null) {
+ throw new WxErrorException("请求access token失败:响应内容为空");
+ }
resultContent = response.body().string();
} catch (IOException e) {
log.error(e.getMessage(), e);
+ throw new WxErrorException(e);
}
WxError error = WxError.fromJson(resultContent, WxType.CP);
@@ -75,6 +78,55 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
return this.configStorage.getAccessToken();
}
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+
+ Lock lock = this.configStorage.getContactAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+ // 使用通讯录同步secret获取access_token
+ String contactSecret = this.configStorage.getContactSecret();
+ if (contactSecret == null || contactSecret.trim().isEmpty()) {
+ throw new WxErrorException("通讯录同步secret未配置");
+ }
+ //得到httpClient
+ OkHttpClient client = getRequestHttpClient();
+ //请求的request
+ Request request = new Request.Builder()
+ .url(String.format(this.configStorage.getApiUrl(GET_TOKEN), this.configStorage.getCorpId(),
+ contactSecret))
+ .get()
+ .build();
+ String resultContent;
+ try (Response response = client.newCall(request).execute()) {
+ if (response.body() == null) {
+ throw new WxErrorException("请求通讯录同步access token失败:响应内容为空");
+ }
+ resultContent = response.body().string();
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ throw new WxErrorException(e);
+ }
+ WxError error = WxError.fromJson(resultContent, WxType.CP);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+ WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+ this.configStorage.updateContactAccessToken(accessToken.getAccessToken(),
+ accessToken.getExpiresIn());
+ } finally {
+ lock.unlock();
+ }
+ return this.configStorage.getContactAccessToken();
+ }
+
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
@@ -101,11 +153,15 @@ public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorExcepti
msgAuditSecret))
.get()
.build();
- String resultContent = null;
+ String resultContent;
try (Response response = client.newCall(request).execute()) {
+ if (response.body() == null) {
+ throw new WxErrorException("请求会话存档access token失败:响应内容为空");
+ }
resultContent = response.body().string();
} catch (IOException e) {
log.error(e.getMessage(), e);
+ throw new WxErrorException(e);
}
WxError error = WxError.fromJson(resultContent, WxType.CP);
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java
index fe6acf12d3..4159e186f9 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java
@@ -258,6 +258,47 @@ public interface WxCpConfigStorage {
*/
String getWebhookKey();
+ /**
+ * 获取通讯录同步的secret
+ *
+ * @return contact secret
+ */
+ String getContactSecret();
+
+ /**
+ * 获取通讯录同步的access token
+ *
+ * @return contact access token
+ */
+ String getContactAccessToken();
+
+ /**
+ * 获取通讯录同步access token的锁
+ *
+ * @return contact access token lock
+ */
+ Lock getContactAccessTokenLock();
+
+ /**
+ * 检查通讯录同步access token是否已过期
+ *
+ * @return true: 已过期, false: 未过期
+ */
+ boolean isContactAccessTokenExpired();
+
+ /**
+ * 强制将通讯录同步access token过期掉
+ */
+ void expireContactAccessToken();
+
+ /**
+ * 更新通讯录同步access token
+ *
+ * @param accessToken 通讯录同步access token
+ * @param expiresInSeconds 过期时间(秒)
+ */
+ void updateContactAccessToken(String accessToken, int expiresInSeconds);
+
/**
* 获取会话存档的secret
*
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java
index c7b300ba48..8395ca28a5 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java
@@ -44,6 +44,16 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
private volatile String token;
private volatile String aesKey;
private volatile long expiresTime;
+ /**
+ * 通讯录同步secret及其access token
+ */
+ private volatile String contactSecret;
+ private volatile String contactAccessToken;
+ private volatile long contactAccessTokenExpiresTime;
+ /**
+ * 通讯录同步access token锁
+ */
+ protected transient Lock contactAccessTokenLock = new ReentrantLock();
/**
* 会话存档私钥以及sdk路径
*/
@@ -465,6 +475,49 @@ public WxCpDefaultConfigImpl setWebhookKey(String webhookKey) {
return this;
}
+ @Override
+ public String getContactSecret() {
+ return this.contactSecret;
+ }
+
+ /**
+ * 设置通讯录同步secret.
+ *
+ * @param contactSecret 通讯录同步secret
+ * @return this
+ */
+ public WxCpDefaultConfigImpl setContactSecret(String contactSecret) {
+ this.contactSecret = contactSecret;
+ return this;
+ }
+
+ @Override
+ public String getContactAccessToken() {
+ return this.contactAccessToken;
+ }
+
+ @Override
+ public Lock getContactAccessTokenLock() {
+ return this.contactAccessTokenLock;
+ }
+
+ @Override
+ public boolean isContactAccessTokenExpired() {
+ return System.currentTimeMillis() > this.contactAccessTokenExpiresTime;
+ }
+
+ @Override
+ public void expireContactAccessToken() {
+ this.contactAccessTokenExpiresTime = 0;
+ }
+
+ @Override
+ public synchronized void updateContactAccessToken(String accessToken, int expiresInSeconds) {
+ this.contactAccessToken = accessToken;
+ // 预留200秒的时间
+ this.contactAccessTokenExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
+ }
+
@Override
public String getMsgAuditSecret() {
return this.msgAuditSecret;
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java
index 2ba71fffb6..01c61673a5 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java
@@ -492,6 +492,36 @@ public String getMsgAuditSecret() {
return null;
}
+ @Override
+ public String getContactSecret() {
+ return null;
+ }
+
+ @Override
+ public String getContactAccessToken() {
+ return null;
+ }
+
+ @Override
+ public Lock getContactAccessTokenLock() {
+ return this.msgAuditAccessTokenLock;
+ }
+
+ @Override
+ public boolean isContactAccessTokenExpired() {
+ return true;
+ }
+
+ @Override
+ public void expireContactAccessToken() {
+ // 不支持
+ }
+
+ @Override
+ public void updateContactAccessToken(String accessToken, int expiresInSeconds) {
+ // 不支持
+ }
+
@Override
public String getMsgAuditAccessToken() {
return null;
diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImplTest.java
index 87d2094e58..bdc85afcfc 100644
--- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImplTest.java
+++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImplTest.java
@@ -106,6 +106,11 @@ public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorExcepti
return "mock_msg_audit_access_token";
}
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ return "mock_contact_access_token";
+ }
+
@Override
public void initHttp() {
diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpServiceGetContactAccessTokenTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpServiceGetContactAccessTokenTest.java
new file mode 100644
index 0000000000..5fb79aaaee
--- /dev/null
+++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpServiceGetContactAccessTokenTest.java
@@ -0,0 +1,264 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.HttpClientType;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.util.concurrent.locks.Lock;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * 测试 getContactAccessToken 方法在各个实现类中的正确性
+ *
+ * @author Binary Wang
+ */
+@Test
+public class WxCpServiceGetContactAccessTokenTest {
+
+ private WxCpDefaultConfigImpl config;
+
+ @BeforeMethod
+ public void setUp() {
+ config = new WxCpDefaultConfigImpl();
+ config.setCorpId("testCorpId");
+ config.setCorpSecret("testCorpSecret");
+ config.setContactSecret("testContactSecret");
+ }
+
+ /**
+ * 测试通讯录同步access token的缓存机制
+ * 验证当token未过期时,直接从配置中返回缓存的token
+ */
+ @Test
+ public void testGetContactAccessToken_Cache() throws WxErrorException {
+ // 预先设置一个有效的token
+ config.updateContactAccessToken("cached_token", 7200);
+
+ BaseWxCpServiceImpl service = createTestService(config);
+
+ // 不强制刷新时应该返回缓存的token
+ String token = service.getContactAccessToken(false);
+ assertThat(token).isEqualTo("cached_token");
+ }
+
+ /**
+ * 测试强制刷新通讯录同步access token
+ * 验证forceRefresh=true时会重新获取token
+ */
+ @Test
+ public void testGetContactAccessToken_ForceRefresh() throws WxErrorException {
+ // 预先设置一个有效的token
+ config.updateContactAccessToken("old_token", 7200);
+
+ BaseWxCpServiceImpl service = createTestServiceWithMockToken(config, "new_token");
+
+ // 强制刷新应该获取新token
+ String token = service.getContactAccessToken(true);
+ assertThat(token).isEqualTo("new_token");
+ }
+
+ /**
+ * 测试token过期时自动刷新
+ * 验证当token已过期时,会自动重新获取
+ */
+ @Test
+ public void testGetContactAccessToken_Expired() throws WxErrorException {
+ // 设置一个已过期的token(过期时间为负数,确保立即过期)
+ config.updateContactAccessToken("expired_token", -1);
+
+ BaseWxCpServiceImpl service = createTestServiceWithMockToken(config, "refreshed_token");
+
+ // 过期的token应该被自动刷新
+ String token = service.getContactAccessToken(false);
+ assertThat(token).isEqualTo("refreshed_token");
+ }
+
+ /**
+ * 测试获取锁机制
+ * 验证配置中的锁可以正常获取和使用
+ */
+ @Test
+ public void testGetContactAccessToken_Lock() {
+ // 验证配置提供的锁不为null
+ assertThat(config.getContactAccessTokenLock()).isNotNull();
+
+ // 验证锁可以正常使用
+ config.getContactAccessTokenLock().lock();
+ try {
+ assertThat(config.getContactAccessToken()).isNull();
+ } finally {
+ config.getContactAccessTokenLock().unlock();
+ }
+ }
+
+ /**
+ * 检查token是否需要刷新的公共逻辑
+ */
+ private boolean shouldRefreshToken(WxCpConfigStorage storage, boolean forceRefresh) {
+ return storage.isContactAccessTokenExpired() || forceRefresh;
+ }
+
+ /**
+ * 验证通讯录同步secret是否已配置的公共逻辑
+ */
+ private void validateContactSecret(String contactSecret) throws WxErrorException {
+ if (contactSecret == null || contactSecret.trim().isEmpty()) {
+ throw new WxErrorException("通讯录同步secret未配置");
+ }
+ }
+
+ /**
+ * 创建一个用于测试的BaseWxCpServiceImpl实现,
+ * 用于测试缓存和过期逻辑
+ */
+ private BaseWxCpServiceImpl createTestService(WxCpConfigStorage config) {
+ return new BaseWxCpServiceImpl() {
+ @Override
+ public Object getRequestHttpClient() {
+ return null;
+ }
+
+ @Override
+ public Object getRequestHttpProxy() {
+ return null;
+ }
+
+ @Override
+ public HttpClientType getRequestType() {
+ return null;
+ }
+
+ @Override
+ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
+ return "test_access_token";
+ }
+
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ // 检查是否需要刷新
+ if (!shouldRefreshToken(getWxCpConfigStorage(), forceRefresh)) {
+ return getWxCpConfigStorage().getContactAccessToken();
+ }
+
+ // 使用通讯录同步secret获取access_token
+ String contactSecret = getWxCpConfigStorage().getContactSecret();
+ validateContactSecret(contactSecret);
+
+ // 返回缓存的token(用于测试缓存机制)
+ return getWxCpConfigStorage().getContactAccessToken();
+ }
+
+ @Override
+ public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
+ return "test_msg_audit_token";
+ }
+
+ @Override
+ public void initHttp() {
+ }
+
+ @Override
+ public WxCpConfigStorage getWxCpConfigStorage() {
+ return config;
+ }
+ };
+ }
+
+ /**
+ * 创建一个用于测试的BaseWxCpServiceImpl实现,
+ * 模拟返回指定的token(用于测试刷新逻辑)
+ */
+ private BaseWxCpServiceImpl createTestServiceWithMockToken(WxCpConfigStorage config, String mockToken) {
+ return new BaseWxCpServiceImpl() {
+ @Override
+ public Object getRequestHttpClient() {
+ return null;
+ }
+
+ @Override
+ public Object getRequestHttpProxy() {
+ return null;
+ }
+
+ @Override
+ public HttpClientType getRequestType() {
+ return null;
+ }
+
+ @Override
+ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
+ return "test_access_token";
+ }
+
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ // 使用锁机制
+ Lock lock = getWxCpConfigStorage().getContactAccessTokenLock();
+ lock.lock();
+ try {
+ // 检查是否需要刷新
+ if (!shouldRefreshToken(getWxCpConfigStorage(), forceRefresh)) {
+ return getWxCpConfigStorage().getContactAccessToken();
+ }
+
+ // 使用通讯录同步secret获取access_token
+ String contactSecret = getWxCpConfigStorage().getContactSecret();
+ validateContactSecret(contactSecret);
+
+ // 模拟获取新token并更新配置
+ getWxCpConfigStorage().updateContactAccessToken(mockToken, 7200);
+ return mockToken;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ @Override
+ public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
+ return "test_msg_audit_token";
+ }
+
+ @Override
+ public void initHttp() {
+ }
+
+ @Override
+ public WxCpConfigStorage getWxCpConfigStorage() {
+ return config;
+ }
+ };
+ }
+
+ /**
+ * 测试当 ContactSecret 未配置时应该抛出异常
+ */
+ @Test
+ public void testGetContactAccessToken_WithoutSecret() {
+ config.setContactSecret(null);
+ BaseWxCpServiceImpl service = createTestService(config);
+
+ // 验证当 secret 为 null 时抛出异常
+ assertThatThrownBy(() -> service.getContactAccessToken(true))
+ .isInstanceOf(WxErrorException.class)
+ .hasMessageContaining("通讯录同步secret未配置");
+ }
+
+ /**
+ * 测试当 ContactSecret 为空字符串时应该抛出异常
+ */
+ @Test
+ public void testGetContactAccessToken_WithEmptySecret() {
+ config.setContactSecret(" ");
+ BaseWxCpServiceImpl service = createTestService(config);
+
+ // 验证当 secret 为空字符串时抛出异常
+ assertThatThrownBy(() -> service.getContactAccessToken(true))
+ .isInstanceOf(WxErrorException.class)
+ .hasMessageContaining("通讯录同步secret未配置");
+ }
+}
diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpServiceGetMsgAuditAccessTokenTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpServiceGetMsgAuditAccessTokenTest.java
index da74c1d13a..edea88e01e 100644
--- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpServiceGetMsgAuditAccessTokenTest.java
+++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpServiceGetMsgAuditAccessTokenTest.java
@@ -153,6 +153,11 @@ public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorExcepti
return getWxCpConfigStorage().getMsgAuditAccessToken();
}
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ return "mock_contact_access_token";
+ }
+
@Override
public void initHttp() {
}
@@ -213,6 +218,11 @@ public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorExcepti
}
}
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ return "mock_contact_access_token";
+ }
+
@Override
public void initHttp() {
}
From 990eb71703adbc3c713297361c9cae93a3fd4d47 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Fri, 5 Jun 2026 18:26:17 +0800
Subject: [PATCH 30/38] =?UTF-8?q?:new:=20#3931=E3=80=90=E4=BC=81=E4=B8=9A?=
=?UTF-8?q?=E5=BE=AE=E4=BF=A1=E3=80=91=E7=AC=AC=E4=B8=89=E6=96=B9=E5=BA=94?=
=?UTF-8?q?=E7=94=A8=E5=A2=9E=E5=8A=A0=E6=B6=88=E6=81=AF=E5=8F=91=E9=80=81?=
=?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../cp/tp/service/WxCpTpMessageService.java | 86 +++++++++++++
.../weixin/cp/tp/service/WxCpTpService.java | 18 +++
.../service/impl/BaseWxCpTpServiceImpl.java | 11 ++
.../impl/WxCpTpMessageServiceImpl.java | 66 ++++++++++
.../impl/WxCpTpMessageServiceImplTest.java | 118 ++++++++++++++++++
weixin-java-cp/src/test/resources/testng.xml | 1 +
6 files changed, 300 insertions(+)
create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpMessageService.java
create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImpl.java
create mode 100644 weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImplTest.java
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpMessageService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpMessageService.java
new file mode 100644
index 0000000000..edd1a96c85
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpMessageService.java
@@ -0,0 +1,86 @@
+package me.chanjar.weixin.cp.tp.service;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.message.*;
+
+/**
+ * 企业微信第三方应用消息推送接口.
+ *
+ * 第三方应用使用授权企业的 access_token 代表授权企业发送应用消息。
+ *
+ * @author GitHub Copilot
+ */
+public interface WxCpTpMessageService {
+
+ /**
+ *
+ * 发送应用消息(代授权企业发送).
+ * 详情请见: https://work.weixin.qq.com/api/doc/90000/90135/90236
+ *
+ *
+ * @param message 要发送的消息对象
+ * @param corpId 授权企业的 corpId
+ * @return 消息发送结果
+ * @throws WxErrorException 微信错误异常
+ */
+ WxCpMessageSendResult send(WxCpMessage message, String corpId) throws WxErrorException;
+
+ /**
+ *
+ * 查询应用消息发送统计.
+ * 请求方式:POST(HTTPS)
+ * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/message/get_statistics?access_token=ACCESS_TOKEN
+ * 详情请见: https://work.weixin.qq.com/api/doc/90000/90135/92369
+ *
+ *
+ * @param timeType 查询哪天的数据,0:当天;1:昨天。默认为0。
+ * @param corpId 授权企业的 corpId
+ * @return 统计结果
+ * @throws WxErrorException 微信错误异常
+ */
+ WxCpMessageSendStatistics getStatistics(int timeType, String corpId) throws WxErrorException;
+
+ /**
+ *
+ * 互联企业发送应用消息.
+ * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/linkedcorp/message/send?access_token=ACCESS_TOKEN
+ * 文章地址:https://work.weixin.qq.com/api/doc/90000/90135/90250
+ *
+ *
+ * @param message 要发送的消息对象
+ * @param corpId 授权企业的 corpId
+ * @return 消息发送结果
+ * @throws WxErrorException 微信错误异常
+ */
+ WxCpLinkedCorpMessageSendResult sendLinkedCorpMessage(WxCpLinkedCorpMessage message, String corpId) throws WxErrorException;
+
+ /**
+ *
+ * 发送「学校通知」.
+ * https://developer.work.weixin.qq.com/document/path/92321
+ * 学校可以通过此接口来给家长发送不同类型的学校通知。
+ * 请求方式:POST(HTTPS)
+ * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/message/send?access_token=ACCESS_TOKEN
+ *
+ *
+ * @param message 要发送的消息对象
+ * @param corpId 授权企业的 corpId
+ * @return 消息发送结果
+ * @throws WxErrorException 微信错误异常
+ */
+ WxCpSchoolContactMessageSendResult sendSchoolContactMessage(WxCpSchoolContactMessage message, String corpId) throws WxErrorException;
+
+ /**
+ *
+ * 撤回应用消息.
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/message/recall?access_token=ACCESS_TOKEN
+ * 文档地址: https://developer.work.weixin.qq.com/document/path/94867
+ *
+ *
+ * @param msgId 消息id
+ * @param corpId 授权企业的 corpId
+ * @throws WxErrorException 微信错误异常
+ */
+ void recall(String msgId, String corpId) throws WxErrorException;
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java
index 92966c1d03..5189c5d821 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java
@@ -530,6 +530,24 @@ public interface WxCpTpService {
*/
WxCpTpLicenseService getWxCpTpLicenseService();
+ /**
+ * get message service
+ *
+ * @return WxCpTpMessageService wx cp tp message service
+ */
+ default WxCpTpMessageService getWxCpTpMessageService() {
+ throw new UnsupportedOperationException("WxCpTpMessageService is not supported");
+ }
+
+ /**
+ * set message service
+ *
+ * @param wxCpTpMessageService the message service
+ */
+ default void setWxCpTpMessageService(WxCpTpMessageService wxCpTpMessageService) {
+ throw new UnsupportedOperationException("WxCpTpMessageService is not supported");
+ }
+
WxCpTpXmlMessage fromEncryptedXml(String encryptedXml,
String timestamp, String nonce, String msgSignature);
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java
index 25c1470eb2..d8ed7c0e47 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java
@@ -61,6 +61,7 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ
private WxCpTpIdConvertService wxCpTpIdConvertService = new WxCpTpIdConvertServiceImpl(this);
private WxCpTpOAuth2Service wxCpTpOAuth2Service = new WxCpTpOAuth2ServiceImpl(this);
private WxCpTpCustomizedService wxCpTpCustomizedService = new WxCpTpCustomizedServiceImpl(this);
+ private WxCpTpMessageService wxCpTpMessageService = new WxCpTpMessageServiceImpl(this);
/**
* 全局的是否正在刷新access token的锁.
*/
@@ -665,6 +666,16 @@ public void setWxCpTpLicenseService(WxCpTpLicenseService wxCpTpLicenseService) {
this.wxCpTpLicenseService = wxCpTpLicenseService;
}
+ @Override
+ public WxCpTpMessageService getWxCpTpMessageService() {
+ return wxCpTpMessageService;
+ }
+
+ @Override
+ public void setWxCpTpMessageService(WxCpTpMessageService wxCpTpMessageService) {
+ this.wxCpTpMessageService = wxCpTpMessageService;
+ }
+
@Override
public void setWxCpTpUserService(WxCpTpUserService wxCpTpUserService) {
this.wxCpTpUserService = wxCpTpUserService;
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImpl.java
new file mode 100644
index 0000000000..ef80d01ac1
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImpl.java
@@ -0,0 +1,66 @@
+package me.chanjar.weixin.cp.tp.service.impl;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gson.JsonObject;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.message.*;
+import me.chanjar.weixin.cp.tp.service.WxCpTpMessageService;
+import me.chanjar.weixin.cp.tp.service.WxCpTpService;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Message.*;
+
+/**
+ * 企业微信第三方应用消息推送接口实现类.
+ *
+ * 代授权企业发送应用消息,所有方法均需传入授权企业的 corpId。
+ *
+ * @author GitHub Copilot
+ */
+@RequiredArgsConstructor
+public class WxCpTpMessageServiceImpl implements WxCpTpMessageService {
+
+ private final WxCpTpService mainService;
+
+ @Override
+ public WxCpMessageSendResult send(WxCpMessage message, String corpId) throws WxErrorException {
+ String url = mainService.getWxCpTpConfigStorage().getApiUrl(MESSAGE_SEND)
+ + "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId);
+ return WxCpMessageSendResult.fromJson(this.mainService.post(url, message.toJson(), true));
+ }
+
+ @Override
+ public WxCpMessageSendStatistics getStatistics(int timeType, String corpId) throws WxErrorException {
+ String url = mainService.getWxCpTpConfigStorage().getApiUrl(GET_STATISTICS)
+ + "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId);
+ return WxCpMessageSendStatistics.fromJson(
+ this.mainService.post(url, WxCpGsonBuilder.create().toJson(ImmutableMap.of("time_type", timeType)), true));
+ }
+
+ @Override
+ public WxCpLinkedCorpMessageSendResult sendLinkedCorpMessage(WxCpLinkedCorpMessage message, String corpId)
+ throws WxErrorException {
+ String url = mainService.getWxCpTpConfigStorage().getApiUrl(LINKEDCORP_MESSAGE_SEND)
+ + "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId);
+ return WxCpLinkedCorpMessageSendResult.fromJson(this.mainService.post(url, message.toJson(), true));
+ }
+
+ @Override
+ public WxCpSchoolContactMessageSendResult sendSchoolContactMessage(WxCpSchoolContactMessage message, String corpId)
+ throws WxErrorException {
+ String url = mainService.getWxCpTpConfigStorage().getApiUrl(EXTERNAL_CONTACT_MESSAGE_SEND)
+ + "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId);
+ return WxCpSchoolContactMessageSendResult.fromJson(this.mainService.post(url, message.toJson(), true));
+ }
+
+ @Override
+ public void recall(String msgId, String corpId) throws WxErrorException {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("msgid", msgId);
+ String url = mainService.getWxCpTpConfigStorage().getApiUrl(MESSAGE_RECALL)
+ + "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId);
+ this.mainService.post(url, jsonObject.toString(), true);
+ }
+
+}
diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImplTest.java
new file mode 100644
index 0000000000..ff0a143b71
--- /dev/null
+++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImplTest.java
@@ -0,0 +1,118 @@
+package me.chanjar.weixin.cp.tp.service.impl;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.message.WxCpMessage;
+import me.chanjar.weixin.cp.bean.message.WxCpMessageSendResult;
+import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
+import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
+import me.chanjar.weixin.cp.tp.service.WxCpTpMessageService;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Message.MESSAGE_RECALL;
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Message.MESSAGE_SEND;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertNotNull;
+
+/**
+ * 企业微信第三方应用消息推送服务测试.
+ *
+ * @author GitHub Copilot
+ */
+public class WxCpTpMessageServiceImplTest {
+
+ @Mock
+ private WxCpTpServiceApacheHttpClientImpl wxCpTpService;
+
+ @Mock
+ private WxCpTpConfigStorage configStorage;
+
+ private WxCpTpMessageService wxCpTpMessageService;
+
+ private AutoCloseable mockitoAnnotations;
+
+ /**
+ * Sets up.
+ */
+ @BeforeClass
+ public void setUp() {
+ mockitoAnnotations = MockitoAnnotations.openMocks(this);
+ when(wxCpTpService.getWxCpTpConfigStorage()).thenReturn(configStorage);
+ WxCpTpDefaultConfigImpl defaultConfig = new WxCpTpDefaultConfigImpl();
+ when(configStorage.getApiUrl(anyString()))
+ .thenAnswer(invocation -> defaultConfig.getApiUrl(invocation.getArgument(0)));
+ wxCpTpMessageService = new WxCpTpMessageServiceImpl(wxCpTpService);
+ }
+
+ /**
+ * Tear down.
+ *
+ * @throws Exception the exception
+ */
+ @AfterClass
+ public void tearDown() throws Exception {
+ mockitoAnnotations.close();
+ }
+
+ /**
+ * 测试 send 方法:验证使用了 corpId 对应的 access_token,并以 withoutSuiteAccessToken=true 发起请求.
+ *
+ * @throws WxErrorException 微信错误异常
+ */
+ @Test
+ public void testSendMessage() throws WxErrorException {
+ String corpId = "test_corp_id";
+ String accessToken = "test_access_token";
+ String mockResponse = "{\"errcode\":0,\"errmsg\":\"ok\",\"msgid\":\"msg_001\"}";
+
+ when(configStorage.getAccessToken(corpId)).thenReturn(accessToken);
+ String expectedUrl = new WxCpTpDefaultConfigImpl().getApiUrl(MESSAGE_SEND)
+ + "?access_token=" + accessToken;
+ when(wxCpTpService.post(eq(expectedUrl), anyString(), eq(true))).thenReturn(mockResponse);
+
+ WxCpMessage message = WxCpMessage.TEXT().toUser("zhangsan").content("hello").agentId(1).build();
+ WxCpMessageSendResult result = wxCpTpMessageService.send(message, corpId);
+ assertNotNull(result);
+
+ // 验证调用时传入了 withoutSuiteAccessToken=true,确保不会附加 suite_access_token
+ verify(wxCpTpService).post(eq(expectedUrl), anyString(), eq(true));
+ }
+
+ /**
+ * 测试 recall 方法:验证使用了 corpId 对应的 access_token,并以 withoutSuiteAccessToken=true 发起请求.
+ *
+ * @throws WxErrorException 微信错误异常
+ */
+ @Test
+ public void testRecallMessage() throws WxErrorException {
+ String corpId = "test_corp_id";
+ String accessToken = "test_access_token";
+ String msgId = "test_msg_id";
+
+ when(configStorage.getAccessToken(corpId)).thenReturn(accessToken);
+ String expectedUrl = new WxCpTpDefaultConfigImpl().getApiUrl(MESSAGE_RECALL)
+ + "?access_token=" + accessToken;
+ when(wxCpTpService.post(eq(expectedUrl), contains(msgId), eq(true))).thenReturn("{\"errcode\":0,\"errmsg\":\"ok\"}");
+
+ wxCpTpMessageService.recall(msgId, corpId);
+
+ // 验证调用时传入了 withoutSuiteAccessToken=true,确保不会附加 suite_access_token
+ verify(wxCpTpService).post(eq(expectedUrl), contains(msgId), eq(true));
+ }
+
+ /**
+ * 测试 getWxCpTpMessageService 方法:验证 BaseWxCpTpServiceImpl 中正确初始化了消息服务.
+ */
+ @Test
+ public void testGetWxCpTpMessageServiceFromBase() {
+ WxCpTpServiceApacheHttpClientImpl tpService = new WxCpTpServiceApacheHttpClientImpl();
+ assertNotNull(tpService.getWxCpTpMessageService());
+ }
+}
diff --git a/weixin-java-cp/src/test/resources/testng.xml b/weixin-java-cp/src/test/resources/testng.xml
index 48589e709a..6508fc220d 100644
--- a/weixin-java-cp/src/test/resources/testng.xml
+++ b/weixin-java-cp/src/test/resources/testng.xml
@@ -9,6 +9,7 @@
+
From 126750b298e5e9deea68fde99ff3a3fd0e054e46 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Fri, 5 Jun 2026 18:29:15 +0800
Subject: [PATCH 31/38] =?UTF-8?q?:art:=20#4022=20=E3=80=90=E8=A7=86?=
=?UTF-8?q?=E9=A2=91=E5=8F=B7=E3=80=91=E5=B0=8F=E5=BA=97=E2=80=9C=E6=8B=92?=
=?UTF-8?q?=E7=BB=9D=E5=94=AE=E5=90=8E=E2=80=9D=E6=8E=A5=E5=8F=A3=E6=B7=BB?=
=?UTF-8?q?=E5=8A=A0=E6=8B=92=E7=BB=9D=E5=87=AD=E8=AF=81=E5=8F=82=E6=95=B0?=
=?UTF-8?q?=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../channel/api/WxChannelAfterSaleService.java | 16 ++++++++++++++++
.../api/impl/WxChannelAfterSaleServiceImpl.java | 8 +++++++-
.../channel/bean/after/AfterSaleRejectParam.java | 16 ++++++++++++++++
.../impl/WxChannelAfterSaleServiceImplTest.java | 12 ++++++++++++
4 files changed, 51 insertions(+), 1 deletion(-)
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelAfterSaleService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelAfterSaleService.java
index 85c945d428..b8d1156b66 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelAfterSaleService.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelAfterSaleService.java
@@ -77,6 +77,22 @@ AfterSaleListResponse listIds(Long beginCreateTime, Long endCreateTime, String n
*/
WxChannelBaseResponse reject(String afterSaleOrderId, String rejectReason, Integer rejectReasonType) throws WxErrorException;
+ /**
+ * 拒绝售后(支持拒绝凭证)
+ * 文档地址 https://developers.weixin.qq.com/doc/channels/API/aftersale/rejectapply.html
+ *
+ * @param afterSaleOrderId 售后单号
+ * @param rejectReason 拒绝原因
+ * @param rejectReasonType 拒绝原因枚举值
+ * @param rejectCertificates 拒绝凭证图片列表,可使用图片上传接口获取media_id
+ * @see #getRejectReason()
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse reject(String afterSaleOrderId, String rejectReason, Integer rejectReasonType,
+ List rejectCertificates) throws WxErrorException;
+
/**
* 上传退款凭证
*
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImpl.java
index 92f865444b..c7cdf9167a 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImpl.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImpl.java
@@ -61,7 +61,13 @@ public WxChannelBaseResponse accept(String afterSaleOrderId, String addressId, I
@Override
public WxChannelBaseResponse reject(String afterSaleOrderId, String rejectReason, Integer rejectReasonType) throws WxErrorException {
- AfterSaleRejectParam param = new AfterSaleRejectParam(afterSaleOrderId, rejectReason, rejectReasonType);
+ return reject(afterSaleOrderId, rejectReason, rejectReasonType, null);
+ }
+
+ @Override
+ public WxChannelBaseResponse reject(String afterSaleOrderId, String rejectReason, Integer rejectReasonType,
+ List rejectCertificates) throws WxErrorException {
+ AfterSaleRejectParam param = new AfterSaleRejectParam(afterSaleOrderId, rejectReason, rejectReasonType, rejectCertificates);
String resJson = shopService.post(AFTER_SALE_REJECT_URL, param);
return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/after/AfterSaleRejectParam.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/after/AfterSaleRejectParam.java
index cbde459fea..6b19a8058c 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/after/AfterSaleRejectParam.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/after/AfterSaleRejectParam.java
@@ -5,6 +5,8 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
+import java.util.List;
+
/**
* 售后单拒绝信息
*
@@ -27,6 +29,12 @@ public class AfterSaleRejectParam extends AfterSaleIdParam {
@JsonProperty("reject_reason_type")
private Integer rejectReasonType;
+ /**
+ * 拒绝凭证图片列表,可使用图片上传接口获取media_id
+ */
+ @JsonProperty("reject_certificates")
+ private List rejectCertificates;
+
public AfterSaleRejectParam() {
}
@@ -40,4 +48,12 @@ public AfterSaleRejectParam(String afterSaleOrderId, String rejectReason, Intege
this.rejectReason = rejectReason;
this.rejectReasonType = rejectReasonType;
}
+
+ public AfterSaleRejectParam(String afterSaleOrderId, String rejectReason, Integer rejectReasonType,
+ List rejectCertificates) {
+ super(afterSaleOrderId);
+ this.rejectReason = rejectReason;
+ this.rejectReasonType = rejectReasonType;
+ this.rejectCertificates = rejectCertificates;
+ }
}
diff --git a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImplTest.java b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImplTest.java
index 81122f7a03..529d455d46 100644
--- a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImplTest.java
+++ b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImplTest.java
@@ -70,6 +70,18 @@ public void testReject() throws WxErrorException {
assertTrue(response.isSuccess());
}
+ @Test
+ public void testRejectWithCertificates() throws WxErrorException {
+ WxChannelAfterSaleService afterSaleService = channelService.getAfterSaleService();
+ String afterSaleOrderId = "";
+ String rejectReason = null;
+ List rejectCertificates = new ArrayList<>(4);
+ rejectCertificates.add("THE_FILE_ID_1");
+ WxChannelBaseResponse response = afterSaleService.reject(afterSaleOrderId, rejectReason, 1, rejectCertificates);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
@Test
public void testUploadRefundEvidence() throws WxErrorException {
WxChannelAfterSaleService afterSaleService = channelService.getAfterSaleService();
From cae7d4fbf221144cfaa7c55810ec6e2a5af3cf9f Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Jun 2026 20:27:21 +0800
Subject: [PATCH 32/38] =?UTF-8?q?:art:=20#4024=20=E3=80=90=E5=BE=AE?=
=?UTF-8?q?=E4=BF=A1=E6=94=AF=E4=BB=98=E3=80=91=E8=A1=A5=E9=BD=90=20V3=20?=
=?UTF-8?q?=E9=80=80=E6=AC=BE=E9=80=9A=E7=9F=A5=E5=9B=9E=E8=B0=83=E4=B8=AD?=
=?UTF-8?q?=20amount=20=E5=AF=B9=E8=B1=A1=E7=9A=84=E5=AE=98=E6=96=B9?=
=?UTF-8?q?=E5=AD=97=E6=AE=B5=E5=B9=B6=E5=85=BC=E5=AE=B9=E6=97=A7=20refund?=
=?UTF-8?q?=20=E6=98=A0=E5=B0=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../notify/WxPayRefundNotifyV3Result.java | 116 +++++++++++++++++-
.../notify/WxPayRefundNotifyV3ResultTest.java | 61 +++++++++
2 files changed, 174 insertions(+), 3 deletions(-)
create mode 100644 weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyV3ResultTest.java
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyV3Result.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyV3Result.java
index c3473ee465..f9a68de684 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyV3Result.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyV3Result.java
@@ -5,6 +5,7 @@
import lombok.NoArgsConstructor;
import java.io.Serializable;
+import java.util.List;
/**
* 退款结果通知.
@@ -175,7 +176,7 @@ public static class Amount implements Serializable {
/**
*
* 字段名:退款金额
- * 变量名:refund
+ * 变量名:refund_fee
* 是否必填:是
* 类型:int
* 描述:
@@ -183,8 +184,21 @@ public static class Amount implements Serializable {
* 示例值:999
*
*/
- @SerializedName(value = "refund")
- private Integer refund;
+ @SerializedName(value = "refund_fee", alternate = {"refund"})
+ private Integer refundFee;
+ /**
+ *
+ * 字段名:退款结算金额
+ * 变量名:settlement_refund
+ * 是否必填:否
+ * 类型:int
+ * 描述:
+ * 退款结算金额,单位为分。
+ * 示例值:999
+ *
+ */
+ @SerializedName(value = "settlement_refund")
+ private Integer settlementRefund;
/**
*
* 字段名:用户支付金额
@@ -211,5 +225,101 @@ public static class Amount implements Serializable {
*/
@SerializedName(value = "payer_refund")
private Integer payerRefund;
+ /**
+ *
+ * 字段名:币种
+ * 变量名:currency
+ * 是否必填:否
+ * 类型:string[1,16]
+ * 描述:
+ * 符合ISO 4217标准的三位字母代码,目前只支持人民币:CNY。
+ * 示例值:CNY
+ *
+ */
+ @SerializedName(value = "currency")
+ private String currency;
+ /**
+ *
+ * 字段名:结算金额
+ * 变量名:settlement_total
+ * 是否必填:否
+ * 类型:int
+ * 描述:
+ * 结算金额,单位为分。
+ * 示例值:999
+ *
+ */
+ @SerializedName(value = "settlement_total")
+ private Integer settlementTotal;
+ /**
+ *
+ * 字段名:优惠券退款金额
+ * 变量名:discount_refund
+ * 是否必填:否
+ * 类型:int
+ * 描述:
+ * 优惠券退款金额,单位为分。
+ * 示例值:0
+ *
+ */
+ @SerializedName(value = "discount_refund")
+ private Integer discountRefund;
+ /**
+ *
+ * 字段名:退款账户来源
+ * 变量名:from
+ * 是否必填:否
+ * 类型:array
+ * 描述:
+ * 退款出资的账户类型及金额信息
+ *
+ */
+ @SerializedName(value = "from")
+ private List from;
+
+ @Deprecated
+ public Integer getRefund() {
+ return this.refundFee;
+ }
+
+ @Deprecated
+ public void setRefund(Integer refund) {
+ this.refundFee = refund;
+ }
+ }
+
+ @Data
+ @NoArgsConstructor
+ public static class FromItem 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;
}
}
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);
+ }
+}
From 45d529c062e527fd8373d6c0efe6cba158f35b93 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Jun 2026 20:29:18 +0800
Subject: [PATCH 33/38] =?UTF-8?q?:art:=20#3932=20=E4=BF=AE=E5=A4=8D?=
=?UTF-8?q?=E9=95=BF=E6=97=B6=E9=97=B4=E8=BF=90=E8=A1=8C=E6=97=B6=20Redis?=
=?UTF-8?q?=20=E5=91=BD=E4=BB=A4=E4=B8=AD=E6=96=AD=E5=AF=BC=E8=87=B4=20acc?=
=?UTF-8?q?essToken=20=E5=88=B7=E6=96=B0=E5=A4=B1=E8=B4=A5=E7=9A=84?=
=?UTF-8?q?=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../RedisTemplateSimpleDistributedLock.java | 6 +-
...ateSimpleDistributedLockInterruptTest.java | 98 +++++++++++
.../impl/AbstractWxCpInRedisConfigImpl.java | 55 +++++-
.../AbstractWxCpInRedisConfigImplTest.java | 156 ++++++++++++++++++
4 files changed, 308 insertions(+), 7 deletions(-)
create mode 100644 weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockInterruptTest.java
create mode 100644 weixin-java-cp/src/test/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImplTest.java
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java
index 3f5ce4d692..364b04b574 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java
@@ -42,13 +42,17 @@ public RedisTemplateSimpleDistributedLock( StringRedisTemplate redisTemplate, S
@Override
public void lock() {
+ boolean interrupted = false;
while (!tryLock()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
- // Ignore
+ interrupted = true;
}
}
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
}
@Override
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockInterruptTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockInterruptTest.java
new file mode 100644
index 0000000000..2b8ebccb10
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockInterruptTest.java
@@ -0,0 +1,98 @@
+package me.chanjar.weixin.common.util.locks;
+
+import org.mockito.Mockito;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * 测试 RedisTemplateSimpleDistributedLock 的线程中断处理行为
+ *
+ * @author GitHub Copilot
+ */
+public class RedisTemplateSimpleDistributedLockInterruptTest {
+
+ private StringRedisTemplate mockRedisTemplate;
+ private ValueOperations mockValueOps;
+ private RedisTemplateSimpleDistributedLock lock;
+
+ @BeforeMethod
+ @SuppressWarnings("unchecked")
+ public void setUp() {
+ mockRedisTemplate = Mockito.mock(StringRedisTemplate.class);
+ mockValueOps = Mockito.mock(ValueOperations.class);
+ Mockito.when(mockRedisTemplate.opsForValue()).thenReturn(mockValueOps);
+ lock = new RedisTemplateSimpleDistributedLock(mockRedisTemplate, "test_interrupt_lock", 60000);
+ }
+
+ /**
+ * 测试 lock() 在 Thread.sleep 被中断时应恢复线程中断标志
+ *
+ * 修复前:InterruptedException 被忽略(// Ignore),线程中断标志丢失
+ * 修复后:调用 Thread.currentThread().interrupt() 恢复中断标志
+ *
+ */
+ @Test(description = "lock() 方法在中断时应恢复线程中断标志")
+ public void testLockRestoresInterruptedFlagAfterSleepInterruption() throws InterruptedException {
+ AtomicBoolean interruptedFlagAfterLock = new AtomicBoolean(false);
+
+ // 第一次 setIfAbsent 返回 false(模拟锁被占用),第二次返回 true(模拟锁释放)
+ Mockito.when(mockValueOps.setIfAbsent(Mockito.anyString(), Mockito.anyString(),
+ Mockito.anyLong(), Mockito.any(TimeUnit.class)))
+ .thenReturn(false)
+ .thenReturn(true);
+ // get() 返回不同的值,确保不走可重入路径
+ Mockito.when(mockValueOps.get(Mockito.anyString())).thenReturn("other-value");
+
+ Thread testThread = new Thread(() -> {
+ // 设置中断标志
+ Thread.currentThread().interrupt();
+ // 调用 lock(),第一次 tryLock 失败,sleep 会因中断标志立即抛出 InterruptedException
+ lock.lock();
+ interruptedFlagAfterLock.set(Thread.currentThread().isInterrupted());
+ });
+
+ testThread.start();
+ testThread.join(5000);
+
+ // 线程应该已经完成(不会永远阻塞)
+ Assert.assertFalse(testThread.isAlive(), "线程应该已完成");
+ // 关键验证:中断标志应被恢复(而非被忽略丢失)
+ Assert.assertTrue(interruptedFlagAfterLock.get(), "lock()执行后线程中断标志应被恢复");
+ }
+
+ /**
+ * 测试 tryLock() 在 Redis 正常响应时的基本行为
+ */
+ @Test(description = "tryLock() 成功获取锁时应返回 true")
+ public void testTryLockSuccessfully() {
+ Mockito.when(mockValueOps.setIfAbsent(Mockito.anyString(), Mockito.anyString(),
+ Mockito.anyLong(), Mockito.any(TimeUnit.class)))
+ .thenReturn(true);
+
+ boolean result = lock.tryLock();
+
+ Assert.assertTrue(result, "应成功获取锁");
+ Assert.assertNotNull(lock.getLockSecretValue(), "锁值不应为null");
+ }
+
+ /**
+ * 测试 tryLock() 在锁已被其他线程持有时应返回 false
+ */
+ @Test(description = "锁被占用时 tryLock() 应返回 false")
+ public void testTryLockWhenLockHeld() {
+ Mockito.when(mockValueOps.setIfAbsent(Mockito.anyString(), Mockito.anyString(),
+ Mockito.anyLong(), Mockito.any(TimeUnit.class)))
+ .thenReturn(false);
+ Mockito.when(mockValueOps.get(Mockito.anyString())).thenReturn("other-lock-value");
+
+ boolean result = lock.tryLock();
+
+ Assert.assertFalse(result, "锁被占用时应返回false");
+ }
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImpl.java
index a078e8cf9e..448d2b62dd 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImpl.java
@@ -1,10 +1,12 @@
package me.chanjar.weixin.cp.config.impl;
import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.redis.WxRedisOps;
import org.apache.commons.lang3.StringUtils;
+import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
@@ -12,6 +14,7 @@
* @author yl
* created on 2023/04/23
*/
+@Slf4j
public abstract class AbstractWxCpInRedisConfigImpl extends WxCpDefaultConfigImpl {
private static final long serialVersionUID = 7157341535439380615L;
/**
@@ -120,8 +123,34 @@ public String getAccessToken() {
@Override
public boolean isAccessTokenExpired() {
- Long expire = redisOps.getExpire(this.accessTokenKey);
- return expire == null || expire < 2;
+ try {
+ Long expire = redisOps.getExpire(this.accessTokenKey);
+ return expire == null || expire < 2;
+ } catch (Exception e) {
+ log.warn("获取access_token过期时间时发生异常,将视为已过期以触发刷新,异常信息: {}", e.getMessage());
+ // 仅在当前线程已中断且异常为中断相关时,才清除中断标志,避免吞掉上层的中断语义
+ if (Thread.currentThread().isInterrupted() && isInterruptionRelated(e)) {
+ Thread.interrupted();
+ }
+ return true;
+ }
+ }
+
+ /**
+ * 判断异常及其原因链是否为中断相关异常。
+ *
+ * @param throwable 异常
+ * @return 如果异常链中包含 {@link InterruptedException} 或 {@link CancellationException},返回 true;否则返回 false
+ */
+ private boolean isInterruptionRelated(Throwable throwable) {
+ Throwable current = throwable;
+ while (current != null) {
+ if (current instanceof InterruptedException || current instanceof CancellationException) {
+ return true;
+ }
+ current = current.getCause();
+ }
+ return false;
}
@Override
@@ -146,8 +175,13 @@ public String getJsapiTicket() {
@Override
public boolean isJsapiTicketExpired() {
- Long expire = redisOps.getExpire(this.jsapiTicketKey);
- return expire == null || expire < 2;
+ try {
+ Long expire = redisOps.getExpire(this.jsapiTicketKey);
+ return expire == null || expire < 2;
+ } catch (Exception e) {
+ log.warn("获取jsapi_ticket过期时间时发生异常,将视为已过期,异常信息: {}", e.getMessage());
+ return true;
+ }
}
@Override
@@ -177,8 +211,17 @@ public String getAgentJsapiTicket() {
@Override
public boolean isAgentJsapiTicketExpired() {
- Long expire = redisOps.getExpire(this.agentJsapiTicketKey);
- return expire == null || expire < 2;
+ try {
+ Long expire = redisOps.getExpire(this.agentJsapiTicketKey);
+ return expire == null || expire < 2;
+ } catch (Exception e) {
+ log.warn("获取agent_jsapi_ticket过期时间时发生异常,将视为已过期,异常信息: {}", e.getMessage());
+ // 仅在当前线程已中断且异常为中断相关时,才清除中断标志,避免吞掉上层的中断语义
+ if (Thread.currentThread().isInterrupted() && isInterruptionRelated(e)) {
+ Thread.interrupted();
+ }
+ return true;
+ }
}
@Override
diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImplTest.java
new file mode 100644
index 0000000000..201286943e
--- /dev/null
+++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/config/impl/AbstractWxCpInRedisConfigImplTest.java
@@ -0,0 +1,156 @@
+package me.chanjar.weixin.cp.config.impl;
+
+import me.chanjar.weixin.common.redis.WxRedisOps;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * 测试 AbstractWxCpInRedisConfigImpl 对 Redis 异常的容错处理
+ *
+ * @author GitHub Copilot
+ */
+public class AbstractWxCpInRedisConfigImplTest {
+
+ private WxRedisOps mockRedisOps;
+ private AbstractWxCpInRedisConfigImpl config;
+
+ @BeforeMethod
+ public void setUp() {
+ mockRedisOps = Mockito.mock(WxRedisOps.class);
+ Mockito.when(mockRedisOps.getLock(Mockito.anyString()))
+ .thenReturn(new ReentrantLock());
+
+ config = new AbstractWxCpInRedisConfigImpl(mockRedisOps, "test") {
+ // 使用匿名类提供具体实现用于测试
+ };
+ config.setCorpId("testCorpId");
+ config.setAgentId(1);
+ }
+
+ /**
+ * 测试当 Redis getExpire 抛出异常时,isAccessTokenExpired() 应返回 true(视为已过期),且不影响线程中断标志
+ */
+ @Test
+ public void testIsAccessTokenExpiredWhenRedisThrowsException() {
+ Mockito.when(mockRedisOps.getExpire(Mockito.anyString()))
+ .thenThrow(new RuntimeException("Redis command interrupted"));
+
+ boolean expired = config.isAccessTokenExpired();
+
+ Assert.assertTrue(expired, "Redis异常时应将token视为已过期");
+ // 非中断相关异常不应影响线程中断标志
+ Assert.assertFalse(Thread.currentThread().isInterrupted(), "非中断异常时线程中断标志不应被改变");
+ }
+
+ /**
+ * 测试当线程中断状态已设置时,Redis 调用抛出中断相关异常,isAccessTokenExpired() 应处理并清除中断标志
+ */
+ @Test
+ public void testIsAccessTokenExpiredClearsInterruptedFlag() {
+ // 使用包含 InterruptedException cause 的异常,模拟 Lettuce 的 RedisCommandInterruptedException 行为
+ Mockito.when(mockRedisOps.getExpire(Mockito.anyString()))
+ .thenThrow(new RuntimeException("wrapped", new InterruptedException("command interrupted")));
+
+ Thread.currentThread().interrupt();
+ try {
+ boolean expired = config.isAccessTokenExpired();
+
+ Assert.assertTrue(expired, "Redis中断异常时应将token视为已过期");
+ // 中断标志应该被清除,允许后续操作正常进行
+ Assert.assertFalse(Thread.currentThread().isInterrupted(), "中断相关异常处理后线程中断标志应被清除");
+ } finally {
+ // 兜底清除当前线程的中断标志,避免影响后续测试用例
+ Thread.interrupted();
+ }
+ }
+
+ /**
+ * 测试正常情况下 isAccessTokenExpired() 的行为
+ */
+ @Test
+ public void testIsAccessTokenExpiredWhenTokenValid() {
+ // 返回60秒后过期(未过期)
+ Mockito.when(mockRedisOps.getExpire(Mockito.anyString())).thenReturn(60L);
+
+ boolean expired = config.isAccessTokenExpired();
+
+ Assert.assertFalse(expired, "token未过期时应返回false");
+ }
+
+ /**
+ * 测试 isAccessTokenExpired() 当 expire 为 null 时视为已过期
+ */
+ @Test
+ public void testIsAccessTokenExpiredWhenExpireIsNull() {
+ Mockito.when(mockRedisOps.getExpire(Mockito.anyString())).thenReturn(null);
+
+ boolean expired = config.isAccessTokenExpired();
+
+ Assert.assertTrue(expired, "expire为null时应视为已过期");
+ }
+
+ /**
+ * 测试当 Redis getExpire 抛出异常时,isJsapiTicketExpired() 应返回 true(视为已过期),且不影响线程中断标志
+ */
+ @Test
+ public void testIsJsapiTicketExpiredWhenRedisThrowsException() {
+ Mockito.when(mockRedisOps.getExpire(Mockito.anyString()))
+ .thenThrow(new RuntimeException("Redis command interrupted"));
+
+ boolean expired = config.isJsapiTicketExpired();
+
+ Assert.assertTrue(expired, "Redis异常时应将jsapi_ticket视为已过期");
+ Assert.assertFalse(Thread.currentThread().isInterrupted(), "非中断异常时线程中断标志不应被改变");
+ }
+
+ /**
+ * 测试当 Redis getExpire 抛出异常时,isAgentJsapiTicketExpired() 应返回 true(视为已过期),且不影响线程中断标志
+ */
+ @Test
+ public void testIsAgentJsapiTicketExpiredWhenRedisThrowsException() {
+ Mockito.when(mockRedisOps.getExpire(Mockito.anyString()))
+ .thenThrow(new RuntimeException("Redis command interrupted"));
+
+ boolean expired = config.isAgentJsapiTicketExpired();
+
+ Assert.assertTrue(expired, "Redis异常时应将agent_jsapi_ticket视为已过期");
+ Assert.assertFalse(Thread.currentThread().isInterrupted(), "非中断异常时线程中断标志不应被改变");
+ }
+
+ /**
+ * 测试当线程中断状态已设置时,Redis 调用抛出中断相关异常,isAgentJsapiTicketExpired() 应处理并清除中断标志
+ */
+ @Test
+ public void testIsAgentJsapiTicketExpiredClearsInterruptedFlag() {
+ Mockito.when(mockRedisOps.getExpire(Mockito.anyString()))
+ .thenThrow(new RuntimeException("wrapped", new InterruptedException("command interrupted")));
+
+ Thread.currentThread().interrupt();
+ try {
+ boolean expired = config.isAgentJsapiTicketExpired();
+
+ Assert.assertTrue(expired, "Redis中断异常时应将agent_jsapi_ticket视为已过期");
+ Assert.assertFalse(Thread.currentThread().isInterrupted(), "中断相关异常处理后线程中断标志应被清除");
+ } finally {
+ Thread.interrupted();
+ }
+ }
+
+ /**
+ * 测试提供自定义 Lock 实现时 getAccessTokenLock() 返回正确的锁
+ */
+ @Test
+ public void testGetAccessTokenLockReturnsMockedLock() {
+ Lock mockLock = Mockito.mock(Lock.class);
+ Mockito.when(mockRedisOps.getLock(Mockito.anyString())).thenReturn(mockLock);
+
+ Lock lock = config.getAccessTokenLock();
+
+ Assert.assertNotNull(lock, "获取到的锁不应为null");
+ }
+}
From fab98621c0901e31deee89d878f32ba3775ca2d5 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Jun 2026 20:31:06 +0800
Subject: [PATCH 34/38] =?UTF-8?q?:art:=20=20#3934=E3=80=90=E4=BC=81?=
=?UTF-8?q?=E4=B8=9A=E5=BE=AE=E4=BF=A1=E3=80=91=E4=BF=AE=E5=A4=8D=E4=BC=9A?=
=?UTF-8?q?=E8=AF=9D=E5=AD=98=E6=A1=A3=20SDK=20=E6=AF=8F=E6=AC=A1=20API=20?=
=?UTF-8?q?=E8=B0=83=E7=94=A8=E5=90=8E=E8=A2=AB=E9=94=80=E6=AF=81=E5=B9=B6?=
=?UTF-8?q?=E9=87=8D=E6=96=B0=E5=88=9D=E5=A7=8B=E5=8C=96=E7=9A=84=E9=97=AE?=
=?UTF-8?q?=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md | 19 +-
...SG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md | 2 +-
.../weixin/cp/config/WxCpConfigStorage.java | 8 +-
.../cp/config/impl/WxCpDefaultConfigImpl.java | 12 +-
.../cp/config/impl/WxCpRedisConfigImpl.java | 12 +-
.../WxCpDefaultConfigImplMsgAuditSdkTest.java | 230 ++++++++++++++++++
weixin-java-cp/src/test/resources/testng.xml | 1 +
7 files changed, 260 insertions(+), 24 deletions(-)
create mode 100644 weixin-java-cp/src/test/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImplMsgAuditSdkTest.java
diff --git a/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md b/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md
index b64e4612b9..83d9c08158 100644
--- a/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md
+++ b/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md
@@ -196,19 +196,22 @@ msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, data -> {
1. **获取SDK时**:引用计数 +1
2. **使用完成后**:引用计数 -1
-3. **计数归零时**:SDK被自动释放
+3. **计数归零且SDK已过期时**:SDK被销毁并清理缓存
+4. **计数归零但SDK未过期时**:保留缓存,供后续调用直接复用
+
+> **注意**:引用计数归零并不等同于立即销毁SDK。只有在 SDK 已超过有效期的情况下,框架才会调用 `Finance.DestroySdk()` 释放资源。这一机制避免了每次 API 调用后的频繁初始化/销毁循环。
```java
// 框架内部实现(简化版)
public void downloadMediaFile(String sdkFileId, ...) {
- long sdk = initSdk(); // 获取或初始化SDK
+ long sdk = initSdk(); // 获取或初始化SDK(有效期内直接复用缓存)
configStorage.incrementMsgAuditSdkRefCount(sdk); // 引用计数 +1
-
+
try {
// 执行实际操作
getMediaFile(sdk, sdkFileId, ...);
} finally {
- // 确保引用计数一定会减少
+ // 确保引用计数一定会减少;仅在归零且过期时销毁
configStorage.decrementMsgAuditSdkRefCount(sdk); // 引用计数 -1
}
}
@@ -216,13 +219,13 @@ public void downloadMediaFile(String sdkFileId, ...) {
### SDK缓存机制
-SDK初始化后会缓存7200秒(企业微信官方文档规定),避免频繁初始化:
+SDK初始化后会缓存7200秒,避免频繁初始化:
- **首次调用**:初始化新的SDK
-- **7200秒内**:复用缓存的SDK
-- **超过7200秒**:重新初始化SDK
+- **7200秒内**:复用缓存的SDK(即使引用计数曾归零也不重新初始化)
+- **超过7200秒**:下次 `acquireMsgAuditSdk()` 返回0,触发重新初始化,旧SDK在重新初始化时被销毁
-新API的引用计数机制与缓存机制完美配合,确保SDK不会被提前销毁。
+新API的引用计数机制与缓存机制完美配合,确保SDK不会被提前销毁,也不会永久残留。
## 迁移指南
diff --git a/docs/CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md b/docs/CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md
index 072ceefd0c..1f073905f4 100644
--- a/docs/CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md
+++ b/docs/CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md
@@ -5,7 +5,7 @@
当前实现(4.8.x)通过"共享SDK + 引用计数 + 7200秒过期"来管理会话存档SDK生命周期。
该方案存在以下核心问题:
-1. **频繁初始化/销毁**:每次调用 `releaseSdk()` 后引用计数归零即销毁SDK。对于"拉取→解密→下载媒体"这类典型串行调用链,每步操作都会触发重新初始化。
+1. ~~**频繁初始化/销毁**:每次调用 `releaseSdk()` 后引用计数归零即销毁SDK。对于"拉取→解密→下载媒体"这类典型串行调用链,每步操作都会触发重新初始化。~~ ✅ 已在 4.8.3.B+ 修复:引用计数归零时,仅在 SDK 已过期的情况下才销毁,有效期内继续复用缓存。
2. **7200秒过期规则无依据**:官方文档FAQ明确说"不需要每次new/init sdk,可以在多次拉取中复用同一个sdk",无任何7200秒过期说明。
3. **线程安全问题**:企微技术人员建议"一个线程一个SDK实例",当前设计多线程共享同一SDK实例,存在并发安全隐患。
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java
index 4159e186f9..7f66f05094 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java
@@ -392,10 +392,11 @@ public interface WxCpConfigStorage {
/**
* 减少会话存档SDK的引用计数
- * 当引用计数降为0时,自动销毁SDK以释放资源
+ * 当引用计数降为0且SDK已过期时,才自动销毁SDK以释放资源
+ * 如果SDK尚未过期,保留SDK缓存以供后续调用复用
*
* @param sdk sdk id
- * @return 减少后的引用计数,如果返回0表示SDK已被销毁,如果SDK不匹配返回-1
+ * @return 减少后的引用计数;SDK不匹配或引用计数已为0时返回-1
* @deprecated 引用计数机制已废弃,由 ThreadLocal 模式替代。
*/
@Deprecated
@@ -424,7 +425,8 @@ public interface WxCpConfigStorage {
/**
* 减少SDK引用计数并在必要时释放(原子操作)
- * 此方法确保引用计数递减和SDK检查在同一个同步块内完成
+ * 当引用计数降为0且SDK已过期时,才销毁SDK以释放资源
+ * 如果SDK尚未过期,保留SDK缓存以供后续调用复用,避免频繁初始化和销毁
*
* @param sdk sdk id
* @deprecated 引用计数机制已废弃,由 ThreadLocal 模式替代。
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java
index 8395ca28a5..6435370150 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java
@@ -609,9 +609,9 @@ public synchronized int incrementMsgAuditSdkRefCount(long sdk) {
public synchronized int decrementMsgAuditSdkRefCount(long sdk) {
if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) {
int newCount = --this.msgAuditSdkRefCount;
- // 当引用计数降为0时,自动销毁SDK以释放资源
- // 再次检查SDK是否仍然是当前缓存的SDK(防止并发重新初始化)
- if (newCount == 0 && this.msgAuditSdk == sdk) {
+ // 当引用计数降为0且SDK已过期时,才销毁SDK以释放资源
+ // 如果SDK尚未过期,保留SDK缓存以供后续调用复用,避免频繁初始化和销毁
+ if (newCount == 0 && this.msgAuditSdk == sdk && isMsgAuditSdkExpired()) {
Finance.DestroySdk(sdk);
this.msgAuditSdk = 0;
this.msgAuditSdkExpiresTime = 0;
@@ -646,9 +646,9 @@ public synchronized long acquireMsgAuditSdk() {
public synchronized void releaseMsgAuditSdk(long sdk) {
if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) {
int newCount = --this.msgAuditSdkRefCount;
- // 当引用计数降为0时,自动销毁SDK以释放资源
- // 再次检查SDK是否仍然是当前缓存的SDK(防止并发重新初始化)
- if (newCount == 0 && this.msgAuditSdk == sdk) {
+ // 当引用计数降为0且SDK已过期时,才销毁SDK以释放资源
+ // 如果SDK尚未过期,保留SDK缓存以供后续调用复用,避免频繁初始化和销毁
+ if (newCount == 0 && this.msgAuditSdk == sdk && isMsgAuditSdkExpired()) {
Finance.DestroySdk(sdk);
this.msgAuditSdk = 0;
this.msgAuditSdkExpiresTime = 0;
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java
index 01c61673a5..85d136e01d 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java
@@ -589,9 +589,9 @@ public synchronized int incrementMsgAuditSdkRefCount(long sdk) {
public synchronized int decrementMsgAuditSdkRefCount(long sdk) {
if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) {
int newCount = --this.msgAuditSdkRefCount;
- // 当引用计数降为0时,自动销毁SDK以释放资源
- // 再次检查SDK是否仍然是当前缓存的SDK(防止并发重新初始化)
- if (newCount == 0 && this.msgAuditSdk == sdk) {
+ // 当引用计数降为0且SDK已过期时,才销毁SDK以释放资源
+ // 如果SDK尚未过期,保留SDK缓存以供后续调用复用,避免频繁初始化和销毁
+ if (newCount == 0 && this.msgAuditSdk == sdk && isMsgAuditSdkExpired()) {
Finance.DestroySdk(sdk);
this.msgAuditSdk = 0;
this.msgAuditSdkExpiresTime = 0;
@@ -623,9 +623,9 @@ public synchronized long acquireMsgAuditSdk() {
public synchronized void releaseMsgAuditSdk(long sdk) {
if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) {
int newCount = --this.msgAuditSdkRefCount;
- // 当引用计数降为0时,自动销毁SDK以释放资源
- // 再次检查SDK是否仍然是当前缓存的SDK(防止并发重新初始化)
- if (newCount == 0 && this.msgAuditSdk == sdk) {
+ // 当引用计数降为0且SDK已过期时,才销毁SDK以释放资源
+ // 如果SDK尚未过期,保留SDK缓存以供后续调用复用,避免频繁初始化和销毁
+ if (newCount == 0 && this.msgAuditSdk == sdk && isMsgAuditSdkExpired()) {
Finance.DestroySdk(sdk);
this.msgAuditSdk = 0;
this.msgAuditSdkExpiresTime = 0;
diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImplMsgAuditSdkTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImplMsgAuditSdkTest.java
new file mode 100644
index 0000000000..f006ce0a70
--- /dev/null
+++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImplMsgAuditSdkTest.java
@@ -0,0 +1,230 @@
+package me.chanjar.weixin.cp.config.impl;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.lang.reflect.Field;
+
+/**
+ * 测试 WxCpDefaultConfigImpl 中会话存档 SDK 引用计数的正确性
+ * 验证修复:SDK 在引用计数降为 0 但尚未过期时,不应被销毁
+ *
+ * @author GitHub Copilot
+ */
+public class WxCpDefaultConfigImplMsgAuditSdkTest {
+
+ /**
+ * 用于测试的未过期时间偏移量(毫秒),模拟 SDK 有效状态
+ */
+ private static final long VALID_EXPIRATION_TIME_OFFSET = 7_000_000L;
+
+ private WxCpDefaultConfigImpl config;
+
+ @BeforeMethod
+ public void setUp() {
+ config = new WxCpDefaultConfigImpl();
+ }
+
+ /**
+ * 通过反射设置内部字段
+ */
+ private void setField(String fieldName, Object value) throws Exception {
+ Field field = WxCpDefaultConfigImpl.class.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(config, value);
+ }
+
+ /**
+ * 通过反射获取内部字段值
+ */
+ private Object getField(String fieldName) throws Exception {
+ Field field = WxCpDefaultConfigImpl.class.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return field.get(config);
+ }
+
+ /**
+ * 验证 acquireMsgAuditSdk 在 SDK 有效时能正确返回 SDK 并增加引用计数
+ */
+ @Test
+ public void testAcquireMsgAuditSdkWhenSdkValid() throws Exception {
+ long fakeSdk = 12345L;
+ // 设置一个有效的(未过期的)SDK
+ setField("msgAuditSdk", fakeSdk);
+ setField("msgAuditSdkExpiresTime", System.currentTimeMillis() + VALID_EXPIRATION_TIME_OFFSET);
+ setField("msgAuditSdkRefCount", 0);
+
+ long acquired = config.acquireMsgAuditSdk();
+
+ Assert.assertEquals(acquired, fakeSdk, "应返回已缓存的有效 SDK");
+ int refCount = (int) getField("msgAuditSdkRefCount");
+ Assert.assertEquals(refCount, 1, "引用计数应增加到 1");
+ }
+
+ /**
+ * 验证 acquireMsgAuditSdk 在 SDK 已过期时返回 0
+ */
+ @Test
+ public void testAcquireMsgAuditSdkWhenSdkExpired() throws Exception {
+ long fakeSdk = 12345L;
+ // 设置已过期的 SDK
+ setField("msgAuditSdk", fakeSdk);
+ setField("msgAuditSdkExpiresTime", System.currentTimeMillis() - 1000L);
+ setField("msgAuditSdkRefCount", 0);
+
+ long acquired = config.acquireMsgAuditSdk();
+
+ Assert.assertEquals(acquired, 0L, "SDK 已过期,应返回 0");
+ int refCount = (int) getField("msgAuditSdkRefCount");
+ Assert.assertEquals(refCount, 0, "引用计数不应改变");
+ }
+
+ /**
+ * 核心测试:验证当引用计数降为 0 但 SDK 尚未过期时,SDK 不会被销毁
+ * 这是修复 issue 的关键验证:避免每次 API 调用后频繁销毁和重新初始化 SDK
+ */
+ @Test
+ public void testReleaseMsgAuditSdkShouldNotDestroyWhenNotExpired() throws Exception {
+ long fakeSdk = 12345L;
+ // 设置一个有效的(未过期的)SDK,引用计数为 1
+ setField("msgAuditSdk", fakeSdk);
+ setField("msgAuditSdkExpiresTime", System.currentTimeMillis() + VALID_EXPIRATION_TIME_OFFSET);
+ setField("msgAuditSdkRefCount", 1);
+
+ // 释放引用,引用计数应降为 0,但 SDK 尚未过期,不应被销毁
+ config.releaseMsgAuditSdk(fakeSdk);
+
+ long sdkAfterRelease = (long) getField("msgAuditSdk");
+ int refCountAfterRelease = (int) getField("msgAuditSdkRefCount");
+
+ Assert.assertEquals(sdkAfterRelease, fakeSdk, "SDK 尚未过期,引用计数归零后不应被销毁,应继续缓存");
+ Assert.assertEquals(refCountAfterRelease, 0, "引用计数应为 0");
+ }
+
+ /**
+ * 验证:SDK 在未过期、引用计数为 0 时,下次调用 acquireMsgAuditSdk 应直接复用,无需重新初始化
+ * 这是修复后的核心行为:避免频繁初始化
+ */
+ @Test
+ public void testSdkReuseAfterReleaseWhenNotExpired() throws Exception {
+ long fakeSdk = 99999L;
+ // 模拟:SDK 有效,引用计数为 1(正在被使用)
+ setField("msgAuditSdk", fakeSdk);
+ setField("msgAuditSdkExpiresTime", System.currentTimeMillis() + VALID_EXPIRATION_TIME_OFFSET);
+ setField("msgAuditSdkRefCount", 1);
+
+ // 模拟方法调用结束,释放引用
+ config.releaseMsgAuditSdk(fakeSdk);
+
+ // 模拟下一次方法调用,应该直接复用缓存的 SDK
+ long reacquired = config.acquireMsgAuditSdk();
+
+ Assert.assertEquals(reacquired, fakeSdk, "SDK 应被复用,而不是返回 0(需要重新初始化)");
+ int refCount = (int) getField("msgAuditSdkRefCount");
+ Assert.assertEquals(refCount, 1, "复用后引用计数应为 1");
+ }
+
+ /**
+ * 验证:多次 acquire/release 的引用计数正确性(串行验证)
+ */
+ @Test
+ public void testMultipleAcquireAndReleaseSequential() throws Exception {
+ long fakeSdk = 77777L;
+ setField("msgAuditSdk", fakeSdk);
+ setField("msgAuditSdkExpiresTime", System.currentTimeMillis() + VALID_EXPIRATION_TIME_OFFSET);
+ setField("msgAuditSdkRefCount", 0);
+
+ // 三次 acquire,引用计数依次递增
+ long sdk1 = config.acquireMsgAuditSdk();
+ long sdk2 = config.acquireMsgAuditSdk();
+ long sdk3 = config.acquireMsgAuditSdk();
+
+ Assert.assertEquals(sdk1, fakeSdk);
+ Assert.assertEquals(sdk2, fakeSdk);
+ Assert.assertEquals(sdk3, fakeSdk);
+ Assert.assertEquals((int) getField("msgAuditSdkRefCount"), 3, "应有 3 个引用");
+
+ // 逐一释放,SDK 未过期,不应被销毁
+ config.releaseMsgAuditSdk(fakeSdk);
+ Assert.assertEquals((int) getField("msgAuditSdkRefCount"), 2, "释放一个后应有 2 个引用");
+ Assert.assertEquals((long) getField("msgAuditSdk"), fakeSdk, "SDK 仍有引用,不应被销毁");
+
+ config.releaseMsgAuditSdk(fakeSdk);
+ Assert.assertEquals((int) getField("msgAuditSdkRefCount"), 1, "释放两个后应有 1 个引用");
+
+ config.releaseMsgAuditSdk(fakeSdk);
+ Assert.assertEquals((int) getField("msgAuditSdkRefCount"), 0, "全部释放后引用计数应为 0");
+ // SDK 未过期,不应被销毁
+ Assert.assertEquals((long) getField("msgAuditSdk"), fakeSdk, "SDK 未过期,全部引用释放后不应被销毁");
+ }
+
+ /**
+ * 验证 incrementMsgAuditSdkRefCount 在 SDK 匹配时正确增加引用计数
+ */
+ @Test
+ public void testIncrementRefCount() throws Exception {
+ long fakeSdk = 11111L;
+ setField("msgAuditSdk", fakeSdk);
+ setField("msgAuditSdkRefCount", 2);
+
+ int result = config.incrementMsgAuditSdkRefCount(fakeSdk);
+
+ Assert.assertEquals(result, 3, "引用计数应增加到 3");
+ }
+
+ /**
+ * 验证 incrementMsgAuditSdkRefCount 在 SDK 不匹配时返回 -1
+ */
+ @Test
+ public void testIncrementRefCountWithWrongSdk() throws Exception {
+ setField("msgAuditSdk", 11111L);
+ setField("msgAuditSdkRefCount", 2);
+
+ int result = config.incrementMsgAuditSdkRefCount(99999L);
+
+ Assert.assertEquals(result, -1, "SDK 不匹配时应返回 -1");
+ }
+
+ /**
+ * 验证 getMsgAuditSdkRefCount 的正确性
+ */
+ @Test
+ public void testGetMsgAuditSdkRefCount() throws Exception {
+ long fakeSdk = 55555L;
+ setField("msgAuditSdk", fakeSdk);
+ setField("msgAuditSdkRefCount", 5);
+
+ int count = config.getMsgAuditSdkRefCount(fakeSdk);
+ Assert.assertEquals(count, 5, "应返回正确的引用计数");
+
+ int wrongCount = config.getMsgAuditSdkRefCount(99L);
+ Assert.assertEquals(wrongCount, -1, "SDK 不匹配时应返回 -1");
+ }
+
+ /**
+ * 验证:引用计数归零且 SDK 已过期时,releaseMsgAuditSdk 应尝试销毁 SDK
+ * 由于 Finance.DestroySdk 是原生方法,测试环境中不加载原生库时会抛出 UnsatisfiedLinkError,
+ * 但引用计数已在 Finance 调用前递减(可验证代码路径已进入销毁分支)。
+ * 当原生库可用时,应进一步断言 msgAuditSdk 和 msgAuditSdkExpiresTime 均被清零。
+ */
+ @Test
+ public void testReleaseMsgAuditSdkShouldDestroyWhenExpired() throws Exception {
+ long fakeSdk = 22222L;
+ // 设置已过期的 SDK,引用计数为 1
+ setField("msgAuditSdk", fakeSdk);
+ setField("msgAuditSdkExpiresTime", System.currentTimeMillis() - 1000L); // 已过期
+ setField("msgAuditSdkRefCount", 1);
+
+ try {
+ config.releaseMsgAuditSdk(fakeSdk);
+ // 原生库可用:断言字段已清零
+ Assert.assertEquals((long) getField("msgAuditSdk"), 0L, "过期且引用归零后 msgAuditSdk 应被清零");
+ Assert.assertEquals((long) getField("msgAuditSdkExpiresTime"), 0L, "过期时间应被清零");
+ } catch (UnsatisfiedLinkError e) {
+ // 测试环境未加载原生库:Finance.DestroySdk 被调用但抛出 UnsatisfiedLinkError
+ // 这证明代码路径正确进入了"过期时销毁 SDK"的分支,与"未过期时跳过销毁"的分支形成对比
+ Assert.assertEquals((int) getField("msgAuditSdkRefCount"), 0, "引用计数应已递减到 0(Finance 调用前完成)");
+ }
+ }
+}
diff --git a/weixin-java-cp/src/test/resources/testng.xml b/weixin-java-cp/src/test/resources/testng.xml
index 6508fc220d..f63d3f30f5 100644
--- a/weixin-java-cp/src/test/resources/testng.xml
+++ b/weixin-java-cp/src/test/resources/testng.xml
@@ -8,6 +8,7 @@
+
From 1037dfd89cb2b85f9c29c3410a26c3b005796bb1 Mon Sep 17 00:00:00 2001
From: buaazyl
Date: Mon, 8 Jun 2026 17:58:38 +0800
Subject: [PATCH 35/38] =?UTF-8?q?:art:=20=20#4049=20=E3=80=90=E5=B0=8F?=
=?UTF-8?q?=E7=A8=8B=E5=BA=8F=E3=80=91=E4=BC=98=E5=8C=96=E8=AE=A2=E9=98=85?=
=?UTF-8?q?=E6=B6=88=E6=81=AF=E5=AD=97=E7=AC=A6=E4=B8=B2=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../wx/miniapp/bean/WxMaSubscribeMessage.java | 128 +++++++++++++++++-
1 file changed, 127 insertions(+), 1 deletion(-)
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaSubscribeMessage.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaSubscribeMessage.java
index 984e9573db..e372f27039 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaSubscribeMessage.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaSubscribeMessage.java
@@ -4,10 +4,12 @@
import cn.binarywang.wx.miniapp.json.WxMaGsonBuilder;
import lombok.*;
import lombok.experimental.Accessors;
+import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
+import java.util.regex.Pattern;
/**
* 订阅消息
@@ -24,6 +26,19 @@
public class WxMaSubscribeMessage implements Serializable {
private static final long serialVersionUID = 6846729898251286686L;
+ /**
+ * 预编译正则,避免每次调用 resetValue 时重复编译
+ */
+ private static final Pattern NUMBER_PATTERN = Pattern.compile("[^0-9.\\-]");
+ private static final Pattern NUMBER_VALID_PATTERN = Pattern.compile("-?\\d+\\.?\\d*|-?\\.\\d+");
+ private static final Pattern LETTER_PATTERN = Pattern.compile("[^a-zA-Z]");
+ private static final Pattern SYMBOL_PATTERN = Pattern.compile("[a-zA-Z0-9\\u4e00-\\u9fa5]");
+ private static final Pattern PHONE_PATTERN = Pattern.compile("[^0-9+\\-]");
+ private static final Pattern NAME_PATTERN = Pattern.compile("[^\\u4e00-\\u9fa5a-zA-Z \\u00b7.\\u3001\\uff0c\\u3002\\-]");
+ private static final Pattern CHARACTER_STRING_PATTERN = Pattern.compile("[\\u4e00-\\u9fa5]");
+ private static final Pattern CHINESE_PATTERN = Pattern.compile("[\\u4e00-\\u9fa5]");
+ private static final Pattern PHRASE_PATTERN = Pattern.compile("[^\\u4e00-\\u9fa5]");
+
/**
* 接收者(用户)的 openid.
*
@@ -75,15 +90,126 @@ public class WxMaSubscribeMessage implements Serializable {
private String lang = WxMaConstants.MiniProgramLang.ZH_CN;
public WxMaSubscribeMessage addData(MsgData datum) {
+ if (datum == null) {
+ return this;
+ }
if (this.data == null) {
this.data = new ArrayList<>();
}
- this.data.add(datum);
+ this.data.add(resetValue(datum));
return this;
}
+ /**
+ * 处理订阅消息字符串长度及格式问题
+ *
+ * @link 发送订阅消息
+ */
+ private MsgData resetValue(MsgData datum) {
+ String name = datum.getName();
+ String value = datum.getValue();
+
+ if (StringUtils.isBlank(value)) {
+ // 空值会发送失败,改为-
+ datum.setValue("-");
+ return datum;
+ }
+
+ if (StringUtils.startsWith(name, "thing") && value.length() > 20) {
+ // thing.DATA: 20个以内字符,可汉字、数字、字母或符号组合
+ value = StringUtils.substring(value, 0, 17) + "...";
+ } else if (StringUtils.startsWith(name, "number")) {
+ // number.DATA: 32位以内数字,只能数字,可带小数
+ value = NUMBER_PATTERN.matcher(value).replaceAll("");
+ if (!NUMBER_VALID_PATTERN.matcher(value).matches()) {
+ value = "0";
+ }
+ if (value.length() > 32) {
+ value = StringUtils.substring(value, 0, 32);
+ }
+ } else if (StringUtils.startsWith(name, "letter")) {
+ // letter.DATA: 32位以内字母,只能字母
+ value = LETTER_PATTERN.matcher(value).replaceAll("");
+ if (value.isEmpty()) {
+ value = "A";
+ }
+ if (value.length() > 32) {
+ value = StringUtils.substring(value, 0, 32);
+ }
+ } else if (StringUtils.startsWith(name, "symbol")) {
+ // symbol.DATA: 5位以内符号,只能符号(除中文、英文、数字外的常见符号)
+ value = SYMBOL_PATTERN.matcher(value).replaceAll("");
+ if (value.isEmpty()) {
+ value = "-";
+ }
+ if (value.length() > 5) {
+ value = StringUtils.substring(value, 0, 5);
+ }
+ } else if (StringUtils.startsWith(name, "character_string")) {
+ // character_string.DATA: 32位以内,可数字、字母或符号组合(不含中文)
+ value = CHARACTER_STRING_PATTERN.matcher(value).replaceAll("");
+ if (value.isEmpty()) {
+ value = "0";
+ }
+ if (value.length() > 32) {
+ value = StringUtils.substring(value, 0, 32);
+ }
+ } else if (StringUtils.startsWith(name, "phone_number")) {
+ // phone_number.DATA: 17位以内,数字、符号
+ value = PHONE_PATTERN.matcher(value).replaceAll("");
+ // 只允许一个前导+号,且必须在开头
+ if (value.startsWith("+")) {
+ value = "+" + value.substring(1).replace("+", "");
+ } else {
+ value = value.replace("+", "");
+ }
+ if (value.isEmpty()) {
+ value = "0";
+ }
+ if (value.length() > 17) {
+ value = StringUtils.substring(value, 0, 17);
+ }
+ } else if (StringUtils.startsWith(name, "car_number")) {
+ // car_number.DATA: 8位以内,第一位与最后一位可为汉字,其余为字母或数字
+ if (value.length() > 8) {
+ value = StringUtils.substring(value, 0, 8);
+ }
+ } else if (StringUtils.startsWith(name, "name")) {
+ // name.DATA: 10个以内纯汉字或20个以内纯字母或符号,中文和字母混合按中文名算10个字内
+ // 过滤非法字符,不保留数字(name 类型不允许数字)
+ value = NAME_PATTERN.matcher(value).replaceAll("");
+ if (value.isEmpty()) {
+ value = "-";
+ }
+ boolean containsChinese = CHINESE_PATTERN.matcher(value).find();
+ if (containsChinese) {
+ // 含中文,按中文名算,10个字内
+ if (value.length() > 10) {
+ value = StringUtils.substring(value, 0, 7) + "...";
+ }
+ } else {
+ // 纯字母或符号,20个以内
+ if (value.length() > 20) {
+ value = StringUtils.substring(value, 0, 17) + "...";
+ }
+ }
+ } else if (StringUtils.startsWith(name, "phrase")) {
+ // phrase.DATA: 5个以内纯汉字
+ value = PHRASE_PATTERN.matcher(value).replaceAll("");
+ if (value.isEmpty()) {
+ value = "好";
+ }
+ if (value.length() > 5) {
+ value = StringUtils.substring(value, 0, 5);
+ }
+ }
+
+ datum.setValue(value);
+ return datum;
+ }
+
public String toJson() {
return WxMaGsonBuilder.create().toJson(this);
}
From a49d6e1461752c06b752d2afd8aeeb7e6e78cefe Mon Sep 17 00:00:00 2001
From: Binary Wang
Date: Mon, 8 Jun 2026 23:14:35 +0800
Subject: [PATCH 36/38] =?UTF-8?q?:bookmark:=20=E5=8F=91=E5=B8=83=204.8.4.B?=
=?UTF-8?q?=20=E6=B5=8B=E8=AF=95=E7=89=88=E6=9C=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 2 +-
solon-plugins/pom.xml | 2 +-
solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-channel-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-cp-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-miniapp-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-mp-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-open-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-pay-solon-plugin/pom.xml | 2 +-
solon-plugins/wx-java-qidian-solon-plugin/pom.xml | 2 +-
spring-boot-starters/pom.xml | 2 +-
.../wx-java-channel-multi-spring-boot-starter/pom.xml | 2 +-
.../wx-java-channel-spring-boot-starter/pom.xml | 2 +-
.../wx-java-cp-multi-spring-boot-starter/pom.xml | 2 +-
spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml | 2 +-
.../wx-java-cp-tp-multi-spring-boot-starter/pom.xml | 2 +-
.../wx-java-miniapp-multi-spring-boot-starter/pom.xml | 2 +-
.../wx-java-miniapp-spring-boot-starter/pom.xml | 2 +-
.../wx-java-mp-multi-spring-boot-starter/pom.xml | 2 +-
spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml | 2 +-
.../wx-java-open-multi-spring-boot-starter/pom.xml | 2 +-
spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml | 2 +-
.../wx-java-pay-multi-spring-boot-starter/pom.xml | 2 +-
spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml | 2 +-
spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml | 2 +-
weixin-graal/pom.xml | 2 +-
weixin-java-aispeech/pom.xml | 2 +-
weixin-java-channel/pom.xml | 2 +-
weixin-java-common/pom.xml | 2 +-
weixin-java-cp/pom.xml | 2 +-
weixin-java-miniapp/pom.xml | 2 +-
weixin-java-mp/pom.xml | 2 +-
weixin-java-open/pom.xml | 2 +-
weixin-java-pay/pom.xml | 2 +-
weixin-java-qidian/pom.xml | 2 +-
wx-java-bom/pom.xml | 2 +-
39 files changed, 39 insertions(+), 39 deletions(-)
diff --git a/pom.xml b/pom.xml
index 09d30e185f..ea7cf980e0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
com.github.binarywang
wx-java
- 4.8.3.B
+ 4.8.4.B
pom
WxJava - Weixin/Wechat Java SDK
微信开发Java SDK
diff --git a/solon-plugins/pom.xml b/solon-plugins/pom.xml
index 87401a2c97..620193dd4f 100644
--- a/solon-plugins/pom.xml
+++ b/solon-plugins/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.3.B
+ 4.8.4.B
pom
wx-java-solon-plugins
diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml
index d99f9a67c1..682f1531ed 100644
--- a/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/solon-plugins/wx-java-channel-solon-plugin/pom.xml b/solon-plugins/wx-java-channel-solon-plugin/pom.xml
index a26072f8c4..1e002c1e72 100644
--- a/solon-plugins/wx-java-channel-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-channel-solon-plugin/pom.xml
@@ -3,7 +3,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml
index 9ccd05578b..48b3fae27e 100644
--- a/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml
@@ -4,7 +4,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/solon-plugins/wx-java-cp-solon-plugin/pom.xml b/solon-plugins/wx-java-cp-solon-plugin/pom.xml
index 367d2a338c..4682d570cd 100644
--- a/solon-plugins/wx-java-cp-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-cp-solon-plugin/pom.xml
@@ -4,7 +4,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml
index 9ea8b7caff..7cfdbcbaf2 100644
--- a/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml b/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml
index 0651e3b9b5..a540df9492 100644
--- a/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml
@@ -4,7 +4,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml
index 4dc7eae667..359fffe823 100644
--- a/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/solon-plugins/wx-java-mp-solon-plugin/pom.xml b/solon-plugins/wx-java-mp-solon-plugin/pom.xml
index e0c79f79bf..8678e82a10 100644
--- a/solon-plugins/wx-java-mp-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-mp-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/solon-plugins/wx-java-open-solon-plugin/pom.xml b/solon-plugins/wx-java-open-solon-plugin/pom.xml
index 4cd4b1ac56..b4d9bc904c 100644
--- a/solon-plugins/wx-java-open-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-open-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/solon-plugins/wx-java-pay-solon-plugin/pom.xml b/solon-plugins/wx-java-pay-solon-plugin/pom.xml
index 607c138fd3..e158b9f1a6 100644
--- a/solon-plugins/wx-java-pay-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-pay-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/pom.xml b/solon-plugins/wx-java-qidian-solon-plugin/pom.xml
index f83c8a8066..b03fa0e22f 100644
--- a/solon-plugins/wx-java-qidian-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-qidian-solon-plugin/pom.xml
@@ -3,7 +3,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/spring-boot-starters/pom.xml b/spring-boot-starters/pom.xml
index 07a1226e6f..6d3c7c7e00 100644
--- a/spring-boot-starters/pom.xml
+++ b/spring-boot-starters/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.3.B
+ 4.8.4.B
pom
wx-java-spring-boot-starters
diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml
index c3c3441c9b..d3cf55aac1 100644
--- a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml
index f74d3bfaae..d36164d58a 100644
--- a/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml
@@ -3,7 +3,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml
index 0cb592a7fc..e4c3b1ba8d 100644
--- a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml
@@ -4,7 +4,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml
index 881064d493..f2b6d12f8e 100644
--- a/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml
@@ -4,7 +4,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml
index b3bd632cad..fc3b490242 100644
--- a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml
@@ -4,7 +4,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml
index 744ba094a1..ebb6deba33 100644
--- a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml
index 1088b711e7..9ebb1ee540 100644
--- a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml
@@ -4,7 +4,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml
index de88f187a7..37bc9faedb 100644
--- a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml
index 672cf2e35c..6e3d6da48c 100644
--- a/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml
index dea66a5a35..e1a4509c6f 100644
--- a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml
index 22dbd864df..e0d5779c6b 100644
--- a/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml
index c416b5ba40..a2320eca2a 100644
--- a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml
index 3c1313bc22..67a26da606 100644
--- a/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml
index d9b845adb1..b07dc4276e 100644
--- a/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml
@@ -3,7 +3,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/weixin-graal/pom.xml b/weixin-graal/pom.xml
index 9c23e95add..eb12b9a728 100644
--- a/weixin-graal/pom.xml
+++ b/weixin-graal/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.3.B
+ 4.8.4.B
weixin-graal
diff --git a/weixin-java-aispeech/pom.xml b/weixin-java-aispeech/pom.xml
index 2ca8aa84d8..40c6557065 100644
--- a/weixin-java-aispeech/pom.xml
+++ b/weixin-java-aispeech/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.3.B
+ 4.8.4.B
weixin-java-aispeech
diff --git a/weixin-java-channel/pom.xml b/weixin-java-channel/pom.xml
index 0d332daa20..f8eb264ed6 100644
--- a/weixin-java-channel/pom.xml
+++ b/weixin-java-channel/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.3.B
+ 4.8.4.B
weixin-java-channel
diff --git a/weixin-java-common/pom.xml b/weixin-java-common/pom.xml
index ad124f8052..b93a52b1d1 100644
--- a/weixin-java-common/pom.xml
+++ b/weixin-java-common/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.3.B
+ 4.8.4.B
weixin-java-common
diff --git a/weixin-java-cp/pom.xml b/weixin-java-cp/pom.xml
index d9d8694352..e789f7a73d 100644
--- a/weixin-java-cp/pom.xml
+++ b/weixin-java-cp/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.8.3.B
+ 4.8.4.B
weixin-java-cp
diff --git a/weixin-java-miniapp/pom.xml b/weixin-java-miniapp/pom.xml
index ca426c4e8b..5a8bc55162 100644
--- a/weixin-java-miniapp/pom.xml
+++ b/weixin-java-miniapp/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.8.3.B
+ 4.8.4.B
weixin-java-miniapp
diff --git a/weixin-java-mp/pom.xml b/weixin-java-mp/pom.xml
index b21ac9bb26..1c3b4239b5 100644
--- a/weixin-java-mp/pom.xml
+++ b/weixin-java-mp/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.8.3.B
+ 4.8.4.B
weixin-java-mp
diff --git a/weixin-java-open/pom.xml b/weixin-java-open/pom.xml
index f7701d1809..3d60efa951 100644
--- a/weixin-java-open/pom.xml
+++ b/weixin-java-open/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.8.3.B
+ 4.8.4.B
weixin-java-open
diff --git a/weixin-java-pay/pom.xml b/weixin-java-pay/pom.xml
index 80893a55ab..f661e62c75 100644
--- a/weixin-java-pay/pom.xml
+++ b/weixin-java-pay/pom.xml
@@ -5,7 +5,7 @@
com.github.binarywang
wx-java
- 4.8.3.B
+ 4.8.4.B
4.0.0
diff --git a/weixin-java-qidian/pom.xml b/weixin-java-qidian/pom.xml
index b7aa187817..567efd7adb 100644
--- a/weixin-java-qidian/pom.xml
+++ b/weixin-java-qidian/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.8.3.B
+ 4.8.4.B
weixin-java-qidian
diff --git a/wx-java-bom/pom.xml b/wx-java-bom/pom.xml
index 359d552499..4cb9489362 100644
--- a/wx-java-bom/pom.xml
+++ b/wx-java-bom/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.3.B
+ 4.8.4.B
wx-java-bom
From 85f728d81dbeb7901d679acb166a11f1ceb896ba Mon Sep 17 00:00:00 2001
From: Binary Wang
Date: Tue, 9 Jun 2026 15:32:42 +0800
Subject: [PATCH 37/38] :memo: Remove a sponsorship from README
Removed ccflow sponsorship section from README.
---
README.md | 7 -------
1 file changed, 7 deletions(-)
diff --git a/README.md b/README.md
index ad3e59ace7..026529a1c8 100644
--- a/README.md
+++ b/README.md
@@ -23,13 +23,6 @@
### 特别赞助