diff --git a/.github/agents/my-agent.agent.md b/.github/agents/my-agent.agent.md index dcce85fd88..35be5d0189 100644 --- a/.github/agents/my-agent.agent.md +++ b/.github/agents/my-agent.agent.md @@ -10,4 +10,6 @@ description: 需要用中文,包括PR标题和分析总结过程 # My Agent -请使用中文输出思考过程和总结,包括PR标题,提交commit信息也要使用中文 +- 1、请使用中文输出思考过程和总结,包括PR标题,提交commit信息也要使用中文; +- 2、生成代码时需要提供必要的单元测试代码; +- 3、新增加的代码如果标记作者信息,请注意不要把作者名设为binarywang或者其他无关人员,要改为 GitHub Copilot。 diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index de68370ae1..a12c20b112 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -33,7 +33,22 @@ jobs: VERSION="${BASE_VER}.B" TAG="v${BASE_VER}" IS_RELEASE="true" - echo "Matched release commit: VERSION=$VERSION, TAG=$TAG" + echo "Matched test release commit: VERSION=$VERSION, TAG=$TAG" + # 检查并打tag + if git tag | grep -q "^$TAG$"; then + echo "Tag $TAG already exists." + else + git config user.name "Binary Wang" + git config user.email "a@binarywang.com" + git tag -a "$TAG" -m "Release $TAG" + git push origin "$TAG" + echo "Tag $TAG created and pushed." + fi + elif [[ "$COMMIT_MSG" =~ ^:bookmark:\ 发布\ ([0-9]+\.[0-9]+\.[0-9]+)\ 正式版本 ]]; then + VERSION="${BASH_REMATCH[1]}" + TAG="v${VERSION}" + IS_RELEASE="true" + echo "Matched formal release commit: VERSION=$VERSION, TAG=$TAG" # 检查并打tag if git tag | grep -q "^$TAG$"; then echo "Tag $TAG already exists." diff --git a/README.md b/README.md index f1cccac4b3..94c52d7e07 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,10 @@ -### 微信`Java`开发工具包,支持包括微信支付、开放平台、公众号、企业微信、视频号、小程序等微信功能模块的后端开发。 +### 微信 `Java` 开发工具包,支持包括微信支付、开放平台、公众号、企业微信、视频号、小程序等微信功能模块的后端开发。 + +### 特别赞助
+### 目录索引 +- [快速开始(3分钟)](#快速开始3分钟) +- [我该选哪个模块?](#我该选哪个模块) +- [Maven 引用方式](#maven-引用方式) +- [最小示例](#最小示例) +- [重要信息](#重要信息) +- [其他说明](#其他说明) +- [版本说明](#版本说明) +- [应用案例](#应用案例) +- [特别赞助](#特别赞助) +- [贡献者列表](#贡献者列表) + +### 快速开始(3分钟) +1. 根据业务场景选择模块(见下方“我该选哪个模块?”) +2. 引入 Maven 依赖并选择对应模块 +3. 参考最小示例完成初始化并调用 API + +### 我该选哪个模块? + +| 业务场景 | 模块 | artifactId | +|---|---|---| +| 微信公众号开发 | MP | `weixin-java-mp` | +| 微信小程序开发 | MiniApp | `weixin-java-miniapp` | +| 微信支付 | Pay | `weixin-java-pay` | +| 企业微信 | CP | `weixin-java-cp` | +| 微信开放平台(第三方平台) | Open | `weixin-java-open` | +| 视频号 / 微信小店 | Channel | `weixin-java-channel` | + +> 移动端(iOS/Android)微信登录、分享等能力仍需集成微信官方客户端 SDK;本项目为服务端 SDK。 + ### 重要信息 1. [`WxJava` 荣获 `GitCode` 2024年度十大开源社区奖项](https://mp.weixin.qq.com/s/wM_UlMsDm3IZ1CPPDvcvQw)。 2. 项目合作洽谈请联系微信`binary0000`(在微信里自行搜索并添加好友,请注明来意,如有关于SDK问题需讨论请参考下文入群讨论,不要加此微信)。 -3. **2024-12-30 发布 [【4.7.0正式版】](https://mp.weixin.qq.com/s/_7k-XLYBqeJJhvHWCsdT0A)**! +3. **2026-01-03 发布 [【4.8.0正式版】](https://mp.weixin.qq.com/s/mJoFtGc25pXCn3uZRh6Q-w)**! 5. 贡献源码可以参考视频:[【贡献源码全过程(上集)】](https://mp.weixin.qq.com/s/3xUZSATWwHR_gZZm207h7Q)、[【贡献源码全过程(下集)】](https://mp.weixin.qq.com/s/nyzJwVVoYSJ4hSbwyvTx9A) ,友情提供:[程序员小山与Bug](https://space.bilibili.com/473631007) 6. 新手重要提示:本项目仅是一个SDK开发工具包,未提供Web实现,建议使用 `maven` 或 `gradle` 引用本项目即可使用本SDK提供的各种功能,详情可参考 **[【Demo项目】](demo.md)** 或本项目中的部分单元测试代码; 7. 微信开发新手请务必阅读【开发文档】([Gitee Wiki](https://gitee.com/binary/weixin-java-tools/wikis/Home) 或者 [Github Wiki](https://github.com/binarywang/WxJava/wiki))的常见问题部分,可以少走很多弯路,节省不少时间。 @@ -95,7 +124,7 @@+ * 使用单个 WxMaService 实例管理多个租户配置,通过 switchover 切换租户。 + * 相比 {@link WxMaMultiServicesImpl},此实现共享 HTTP 客户端,节省资源。 + *
+ *+ * 注意:由于使用 ThreadLocal 切换配置,在异步或多线程场景需要特别注意线程上下文切换。 + *
+ * + * @author Binary Wang + * created on 2026/1/9 + */ +@RequiredArgsConstructor +public class WxMaMultiServicesSharedImpl implements WxMaMultiServices { + private final WxMaService sharedWxMaService; + + @Override + public WxMaService getWxMaService(String tenantId) { + if (tenantId == null) { + return null; + } + // 使用 switchover 检查配置是否存在,保持与隔离模式 API 行为一致(不存在时返回 null) + if (!sharedWxMaService.switchover(tenantId)) { + return null; + } + return sharedWxMaService; + } + + @Override + public void removeWxMaService(String tenantId) { + if (tenantId != null) { + sharedWxMaService.removeConfig(tenantId); + } + } + + /** + * 添加租户配置到共享的 WxMaService 实例 + * + * @param tenantId 租户 ID + * @param wxMaService 要添加配置的 WxMaService(仅使用其配置,不使用其实例) + */ + public void addWxMaService(String tenantId, WxMaService wxMaService) { + if (tenantId != null && wxMaService != null) { + sharedWxMaService.addConfig(tenantId, wxMaService.getWxMaConfig()); + } + } +} 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 a6f0fc2a38..25d5f66758 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 @@+ * 使用单个 WxMpService 实例管理多个租户配置,通过 switchover 切换租户。 + * 相比 {@link WxMpMultiServicesImpl},此实现共享 HTTP 客户端,节省资源。 + *
+ *+ * 注意:由于使用 ThreadLocal 切换配置,在异步或多线程场景需要特别注意线程上下文切换。 + *
+ * + * @author Binary Wang + * created on 2026/1/9 + */ +@RequiredArgsConstructor +public class WxMpMultiServicesSharedImpl implements WxMpMultiServices { + private final WxMpService sharedWxMpService; + + @Override + public WxMpService getWxMpService(String tenantId) { + if (tenantId == null) { + return null; + } + // 使用 switchover 检查配置是否存在,保持与隔离模式 API 行为一致(不存在时返回 null) + if (!sharedWxMpService.switchover(tenantId)) { + return null; + } + return sharedWxMpService; + } + + @Override + public void removeWxMpService(String tenantId) { + if (tenantId != null) { + sharedWxMpService.removeConfigStorage(tenantId); + } + } + + /** + * 添加租户配置到共享的 WxMpService 实例 + * + * @param tenantId 租户 ID + * @param wxMpService 要添加配置的 WxMpService(仅使用其配置,不使用其实例) + */ + public void addWxMpService(String tenantId, WxMpService wxMpService) { + if (tenantId != null && wxMpService != null) { + sharedWxMpService.addConfigStorage(tenantId, wxMpService.getWxMpConfigStorage()); + } + } +} diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md b/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md index 3e14f499d9..091912cfad 100644 --- a/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md @@ -27,7 +27,7 @@ #wx.mp.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379 #wx.mp.config-storage.redis.sentinel-name=mymaster # http客户端配置 - wx.mp.config-storage.http-client-type=httpclient # http客户端类型: HttpClient(默认), OkHttp, JoddHttp + wx.mp.config-storage.http-client-type=HttpComponents # http客户端类型: HttpComponents(Apache HttpClient 5.x,推荐), HttpClient(Apache HttpClient 4.x), OkHttp, JoddHttp wx.mp.config-storage.http-proxy-host= wx.mp.config-storage.http-proxy-port= wx.mp.config-storage.http-proxy-username= 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 06dfe0d511..9e95574bc2 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 @@
+ * {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setMaxRetryTimes(int)}
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+ *
+ */
+ private int maxRetryTimes = 5;
+
+ /**
+ * http 请求重试间隔
+ *
+ * {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setRetrySleepMillis(int)}
+ * {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+ *
+ */
+ private int retrySleepMillis = 1000;
+
+ /**
+ * 连接超时时间,单位毫秒
+ */
+ private int connectionTimeout = 5000;
+
+ /**
+ * 读数据超时时间,即socketTimeout,单位毫秒
+ */
+ private int soTimeout = 5000;
+
+ /**
+ * 从连接池获取链接的超时时间,单位毫秒
+ */
+ private int connectionRequestTimeout = 5000;
+ }
+
+ public enum StorageType {
+ /**
+ * 内存
+ */
+ memory,
+ /**
+ * jedis
+ */
+ jedis,
+ /**
+ * redisson
+ */
+ redisson,
+ /**
+ * redisTemplate
+ */
+ redistemplate
+ }
+
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiRedisProperties.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiRedisProperties.java
new file mode 100644
index 0000000000..ae6d5368d7
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiRedisProperties.java
@@ -0,0 +1,57 @@
+package com.binarywang.spring.starter.wxjava.open.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 微信开放平台多账号Redis配置.
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class WxOpenMultiRedisProperties implements Serializable {
+ private static final long serialVersionUID = -5924815351660074401L;
+
+ /**
+ * 主机地址.
+ */
+ private String host = "127.0.0.1";
+
+ /**
+ * 端口号.
+ */
+ private int port = 6379;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 超时.
+ */
+ private int timeout = 2000;
+
+ /**
+ * 数据库.
+ */
+ private int database = 0;
+
+ /**
+ * sentinel ips
+ */
+ private String sentinelIps;
+
+ /**
+ * sentinel name
+ */
+ private String sentinelName;
+
+ private Integer maxActive;
+ private Integer maxIdle;
+ private Integer maxWaitMillis;
+ private Integer minIdle;
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenSingleProperties.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenSingleProperties.java
new file mode 100644
index 0000000000..116da323dc
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenSingleProperties.java
@@ -0,0 +1,49 @@
+package com.binarywang.spring.starter.wxjava.open.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 微信开放平台单个应用配置.
+ *
+ * @author Binary Wang
+ */
+@Data
+@NoArgsConstructor
+public class WxOpenSingleProperties implements Serializable {
+ private static final long serialVersionUID = 1980986361098922525L;
+
+ /**
+ * 设置微信开放平台的appid.
+ */
+ private String appId;
+
+ /**
+ * 设置微信开放平台的app secret.
+ */
+ private String secret;
+
+ /**
+ * 设置微信开放平台的token.
+ */
+ private String token;
+
+ /**
+ * 设置微信开放平台的EncodingAESKey.
+ */
+ private String aesKey;
+
+ /**
+ * 自定义API主机地址,用于替换默认的 https://api.weixin.qq.com
+ * 例如:http://proxy.company.com:8080
+ */
+ private String apiHostUrl;
+
+ /**
+ * 自定义获取AccessToken地址,用于向自定义统一服务获取AccessToken
+ * 例如:http://proxy.company.com:8080/oauth/token
+ */
+ private String accessTokenUrl;
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServices.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServices.java
new file mode 100644
index 0000000000..9228071a10
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServices.java
@@ -0,0 +1,26 @@
+package com.binarywang.spring.starter.wxjava.open.service;
+
+
+import me.chanjar.weixin.open.api.WxOpenService;
+
+/**
+ * 微信开放平台 {@link WxOpenService} 所有实例存放类.
+ *
+ * @author binarywang
+ */
+public interface WxOpenMultiServices {
+ /**
+ * 通过租户 Id 获取 WxOpenService
+ *
+ * @param tenantId 租户 Id
+ * @return WxOpenService
+ */
+ WxOpenService getWxOpenService(String tenantId);
+
+ /**
+ * 根据租户 Id,从列表中移除一个 WxOpenService 实例
+ *
+ * @param tenantId 租户 Id
+ */
+ void removeWxOpenService(String tenantId);
+}
diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServicesImpl.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServicesImpl.java
new file mode 100644
index 0000000000..76fb139e6c
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServicesImpl.java
@@ -0,0 +1,35 @@
+package com.binarywang.spring.starter.wxjava.open.service;
+
+import me.chanjar.weixin.open.api.WxOpenService;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 微信开放平台 {@link WxOpenMultiServices} 默认实现
+ *
+ * @author Binary Wang
+ */
+public class WxOpenMultiServicesImpl implements WxOpenMultiServices {
+ private final Map+ * 注意:configKey 是配置文件中定义的 key(如 wx.pay.configs.<configKey>.xxx), + * 而不是 appId。如果使用 appId 作为配置 key,则可以直接传入 appId。 + *
+ * + * @param configKey 配置标识(配置文件中 wx.pay.configs 下的 key) + * @return WxPayService + */ + WxPayService getWxPayService(String configKey); + + /** + * 根据配置标识,从列表中移除一个 WxPayService 实例. + *+ * 注意:configKey 是配置文件中定义的 key(如 wx.pay.configs.<configKey>.xxx), + * 而不是 appId。如果使用 appId 作为配置 key,则可以直接传入 appId。 + *
+ * + * @param configKey 配置标识(配置文件中 wx.pay.configs 下的 key) + */ + void removeWxPayService(String configKey); +} 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 new file mode 100644 index 0000000000..459fe3b6c0 --- /dev/null +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java @@ -0,0 +1,92 @@ +package com.binarywang.spring.starter.wxjava.pay.service; + +import com.binarywang.spring.starter.wxjava.pay.properties.WxPayMultiProperties; +import com.binarywang.spring.starter.wxjava.pay.properties.WxPaySingleProperties; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 微信支付多服务管理实现类. + * + * @author Binary Wang + */ +@Slf4j +public class WxPayMultiServicesImpl implements WxPayMultiServices { + private final Map+ * 本示例展示了如何使用 wx-java-pay-multi-spring-boot-starter 来管理多个公众号的支付配置。 + *
+ * + * @author Binary Wang + */ +@Slf4j +@Service +public class WxPayMultiExample { + + @Autowired + private WxPayMultiServices wxPayMultiServices; + + /** + * 示例1:根据appId创建支付订单. + *+ * 适用场景:系统需要支持多个公众号,根据用户所在的公众号动态选择支付配置 + *
+ * + * @param appId 公众号appId + * @param openId 用户的openId + * @param totalFee 支付金额(分) + * @param body 商品描述 + * @return JSAPI支付参数 + */ + public WxPayUnifiedOrderV3Result.JsapiResult createJsapiOrder(String appId, String openId, + Integer totalFee, String body) { + try { + // 根据appId获取对应的WxPayService + WxPayService wxPayService = wxPayMultiServices.getWxPayService(appId); + + if (wxPayService == null) { + log.error("未找到appId对应的微信支付配置: {}", appId); + throw new IllegalArgumentException("未找到appId对应的微信支付配置"); + } + + // 构建支付请求 + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); + request.setOutTradeNo(generateOutTradeNo()); + request.setDescription(body); + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(totalFee)); + request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(openId)); + request.setNotifyUrl(wxPayService.getConfig().getNotifyUrl()); + + // 调用微信支付API创建订单 + WxPayUnifiedOrderV3Result.JsapiResult result = + wxPayService.createOrderV3(TradeTypeEnum.JSAPI, request); + + log.info("创建JSAPI支付订单成功,appId: {}, outTradeNo: {}", appId, request.getOutTradeNo()); + return result; + + } catch (Exception e) { + log.error("创建JSAPI支付订单失败,appId: {}", appId, e); + throw new RuntimeException("创建支付订单失败", e); + } + } + + /** + * 示例2:服务商模式 - 为不同子商户创建订单. + *+ * 适用场景:服务商为多个子商户提供支付服务 + *
+ * + * @param configKey 配置标识(在配置文件中定义) + * @param subOpenId 子商户用户的openId + * @param totalFee 支付金额(分) + * @param body 商品描述 + * @return JSAPI支付参数 + */ + public WxPayUnifiedOrderV3Result.JsapiResult createPartnerOrder(String configKey, String subOpenId, + Integer totalFee, String body) { + try { + // 根据配置标识获取WxPayService + WxPayService wxPayService = wxPayMultiServices.getWxPayService(configKey); + + if (wxPayService == null) { + log.error("未找到配置: {}", configKey); + throw new IllegalArgumentException("未找到配置"); + } + + // 获取子商户信息 + String subAppId = wxPayService.getConfig().getSubAppId(); + String subMchId = wxPayService.getConfig().getSubMchId(); + log.info("使用服务商模式,子商户appId: {}, 子商户号: {}", subAppId, subMchId); + + // 构建支付请求 + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); + request.setOutTradeNo(generateOutTradeNo()); + request.setDescription(body); + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(totalFee)); + request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(subOpenId)); + request.setNotifyUrl(wxPayService.getConfig().getNotifyUrl()); + + // 调用微信支付API创建订单 + WxPayUnifiedOrderV3Result.JsapiResult result = + wxPayService.createOrderV3(TradeTypeEnum.JSAPI, request); + + log.info("创建服务商支付订单成功,配置: {}, outTradeNo: {}", configKey, request.getOutTradeNo()); + return result; + + } catch (Exception e) { + log.error("创建服务商支付订单失败,配置: {}", configKey, e); + throw new RuntimeException("创建支付订单失败", e); + } + } + + /** + * 示例3:查询订单状态. + *+ * 适用场景:查询不同公众号的订单支付状态 + *
+ * + * @param appId 公众号appId + * @param outTradeNo 商户订单号 + * @return 订单状态 + */ + public String queryOrderStatus(String appId, String outTradeNo) { + try { + WxPayService wxPayService = wxPayMultiServices.getWxPayService(appId); + + if (wxPayService == null) { + log.error("未找到appId对应的微信支付配置: {}", appId); + throw new IllegalArgumentException("未找到appId对应的微信支付配置"); + } + + // 查询订单 + WxPayOrderQueryV3Result result = wxPayService.queryOrderV3(null, outTradeNo); + String tradeState = result.getTradeState(); + + log.info("查询订单状态成功,appId: {}, outTradeNo: {}, 状态: {}", appId, outTradeNo, tradeState); + return tradeState; + + } catch (Exception e) { + log.error("查询订单状态失败,appId: {}, outTradeNo: {}", appId, outTradeNo, e); + throw new RuntimeException("查询订单失败", e); + } + } + + /** + * 示例4:申请退款. + *+ * 适用场景:为不同公众号的订单申请退款 + *
+ * + * @param appId 公众号appId + * @param outTradeNo 商户订单号 + * @param refundFee 退款金额(分) + * @param totalFee 订单总金额(分) + * @param reason 退款原因 + * @return 退款单号 + */ + public String refund(String appId, String outTradeNo, Integer refundFee, + Integer totalFee, String reason) { + try { + WxPayService wxPayService = wxPayMultiServices.getWxPayService(appId); + + if (wxPayService == null) { + log.error("未找到appId对应的微信支付配置: {}", appId); + throw new IllegalArgumentException("未找到appId对应的微信支付配置"); + } + + // 构建退款请求 + com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request request = + new com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request(); + request.setOutTradeNo(outTradeNo); + request.setOutRefundNo(generateRefundNo()); + request.setReason(reason); + request.setNotifyUrl(wxPayService.getConfig().getRefundNotifyUrl()); + + com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request.Amount amount = + new com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request.Amount(); + amount.setRefund(refundFee); + amount.setTotal(totalFee); + amount.setCurrency("CNY"); + request.setAmount(amount); + + // 调用微信支付API申请退款 + WxPayRefundV3Result result = wxPayService.refundV3(request); + + log.info("申请退款成功,appId: {}, outTradeNo: {}, outRefundNo: {}", + appId, outTradeNo, request.getOutRefundNo()); + return request.getOutRefundNo(); + + } catch (Exception e) { + log.error("申请退款失败,appId: {}, outTradeNo: {}", appId, outTradeNo, e); + throw new RuntimeException("申请退款失败", e); + } + } + + /** + * 示例5:动态管理配置. + *+ * 适用场景:需要在运行时更新配置(如证书更新后需要重新加载) + *
+ * + * @param configKey 配置标识 + */ + public void reloadConfig(String configKey) { + try { + // 移除缓存的WxPayService实例 + wxPayMultiServices.removeWxPayService(configKey); + log.info("移除配置成功,下次获取时将重新创建: {}", configKey); + + // 下次调用 getWxPayService 时会重新创建实例 + WxPayService wxPayService = wxPayMultiServices.getWxPayService(configKey); + if (wxPayService != null) { + log.info("重新加载配置成功: {}", configKey); + } + + } catch (Exception e) { + log.error("重新加载配置失败: {}", configKey, e); + throw new RuntimeException("重新加载配置失败", e); + } + } + + /** + * 生成商户订单号. + * + * @return 商户订单号 + */ + private String generateOutTradeNo() { + return "ORDER_" + System.currentTimeMillis(); + } + + /** + * 生成商户退款单号. + * + * @return 商户退款单号 + */ + private String generateRefundNo() { + return "REFUND_" + System.currentTimeMillis(); + } +} 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 ff1d6b84b1..ecdb925730 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 @@+ * 仅设置文件参数名和上传数据,额外表单字段将为 {@code null}。 + * + * @param name 参数名,如:media + * @param data 上传数据 + * @deprecated 请使用包含 formFields 参数的构造函数或静态工厂方法 {@link #fromFile(String, File)}、{@link #fromBytes(String, String, byte[])} + */ + @Deprecated + public CommonUploadParam(@NotNull String name, @NotNull CommonUploadData data) { + this(name, data, null); + } + /** * 从文件构造 * @@ -43,7 +66,7 @@ public class CommonUploadParam implements Serializable { */ @SneakyThrows public static CommonUploadParam fromFile(String name, File file) { - return new CommonUploadParam(name, CommonUploadData.fromFile(file)); + return new CommonUploadParam(name, CommonUploadData.fromFile(file), null); } /** @@ -55,11 +78,32 @@ public static CommonUploadParam fromFile(String name, File file) { */ @SneakyThrows public static CommonUploadParam fromBytes(String name, @Nullable String fileName, byte[] bytes) { - return new CommonUploadParam(name, new CommonUploadData(fileName, new ByteArrayInputStream(bytes), bytes.length)); + return new CommonUploadParam(name, new CommonUploadData(fileName, new ByteArrayInputStream(bytes), bytes.length), null); + } + + /** + * 添加额外的表单字段 + * + * @param fieldName 表单字段名 + * @param fieldValue 表单字段值 + * @return 当前对象,支持链式调用 + */ + public CommonUploadParam addFormField(String fieldName, String fieldValue) { + if (fieldName == null || fieldName.trim().isEmpty()) { + throw new IllegalArgumentException("表单字段名不能为空"); + } + if (fieldValue == null) { + throw new IllegalArgumentException("表单字段值不能为null"); + } + if (this.formFields == null) { + this.formFields = new HashMap<>(); + } + this.formFields.put(fieldName, fieldValue); + return this; } @Override public String toString() { - return String.format("{name:%s, data:%s}", name, data); + return String.format("{name:%s, data:%s, formFields:%s}", name, data, formFields); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/oauth2/WxOAuth2AccessToken.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/oauth2/WxOAuth2AccessToken.java index c08a49063d..b339844ad6 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/oauth2/WxOAuth2AccessToken.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/oauth2/WxOAuth2AccessToken.java @@ -7,7 +7,10 @@ import java.io.Serializable; /** - * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842 + * OAuth2 AccessToken + *
+ * 参考:{@code https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842} + *
* * @author Daniel Qian */ @@ -36,8 +39,10 @@ public class WxOAuth2AccessToken implements Serializable { private Integer snapshotUser; /** - * https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=11513156443eZYea&version=&lang=zh_CN. * 本接口在scope参数为snsapi_base时不再提供unionID字段。 + *+ * 参考:{@code https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=11513156443eZYea&version=&lang=zh_CN} + *
*/ @SerializedName("unionid") private String unionId; diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java index ea1e9e7c68..356d1dbbf9 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java @@ -453,7 +453,7 @@ public enum WxCpErrorMsgEnum { */ CODE_60008(60008, "部门已存在;部门ID或者部门名称已存在"), /** - * 部门名称含有非法字符;不能含有 \\:?*“< >| 等字符. + * {@code 部门名称含有非法字符;不能含有 \\:?*"< >| 等字符.} */ CODE_60009(60009, "部门名称含有非法字符;不能含有 \\ :?*“< >| 等字符"), /** @@ -521,7 +521,7 @@ public enum WxCpErrorMsgEnum { */ CODE_60124(60124, "无效的父部门id;父部门不存在通讯录中"), /** - * 非法部门名字;不能为空,且不能超过64字节,且不能含有\\:*?”< >|等字符. + * {@code 非法部门名字;不能为空,且不能超过64字节,且不能含有\\:*?"< >|等字符.} */ CODE_60125(60125, "非法部门名字;不能为空,且不能超过64字节,且不能含有\\:*?”< >|等字符"), /** diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxError.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxError.java index b45fba3411..1aab7f1f20 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxError.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxError.java @@ -12,11 +12,13 @@ /** * 微信错误码. + ** 请阅读: * 公众平台:全局返回码说明 * 企业微信:全局错误码 + *
* - * @author Daniel Qian & Binary Wang + * @author Daniel Qian, Binary Wang */ @Data @NoArgsConstructor diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java index 1bb3f6472b..ffe9b5e3ea 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java @@ -46,23 +46,23 @@ public enum WxMaErrorMsgEnum { */ CODE_40003(40003, "openid 不正确"), /** - *
* 无效媒体文件类型
- * 对应操作:uploadTempMedia
+ *
+ * 对应操作:{@code uploadTempMedia}
* 对应地址:
- * POST https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE
+ * {@code POST https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE}
* 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/uploadTempMedia.html
- *
+ *
*/
CODE_40004(40004, "无效媒体文件类型"),
/**
- *
* 无效媒体文件 ID.
- * 对应操作:getTempMedia
+ *
+ * 对应操作:{@code getTempMedia}
* 对应地址:
- * GET https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID
+ * {@code GET https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID}
* 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/getTempMedia.html
- *
+ *
*/
CODE_40007(40007, "无效媒体文件 ID"),
/**
@@ -99,29 +99,29 @@ public enum WxMaErrorMsgEnum {
*/
CODE_41028(41028, "form_id 不正确,或者过期"),
/**
- *
* code 或 template_id 不正确.
- * 对应操作:code2Session, sendUniformMessage, sendTemplateMessage
+ *
+ * 对应操作:{@code code2Session}, {@code sendUniformMessage}, {@code sendTemplateMessage}
* 对应地址:
- * GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
+ * {@code GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code}
* POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
* POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
* 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/code2Session.html
* https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
* https://developers.weixin.qq.com/miniprogram/dev/api/open-api/template-message/sendTemplateMessage.html
- *
+ *
*/
CODE_41029(41029, "请求的参数不正确"),
/**
- *
* form_id 已被使用,或者所传page页面不存在,或者小程序没有发布
- * 对应操作:sendUniformMessage, getWXACodeUnlimit
+ *
+ * 对应操作:{@code sendUniformMessage}, {@code getWXACodeUnlimit}
* 对应地址:
* POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
* POST https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN
* 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
- * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/qr-code/getWXACodeUnlimit.html
- *
+ * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/qr-code/getWXACodeUnlimit.html
+ *
*/
CODE_41030(41030, "请求的参数不正确"),
/**
@@ -138,13 +138,13 @@ public enum WxMaErrorMsgEnum {
*/
CODE_45009(45009, "调用分钟频率受限"),
/**
- *
* 频率限制,每个用户每分钟100次.
- * 对应操作:code2Session
+ *
+ * 对应操作:{@code code2Session}
* 对应地址:
- * GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
+ * {@code GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code}
* 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/code2Session.html
- *
+ *
*/
CODE_45011(45011, "频率限制,每个用户每分钟100次"),
/**
@@ -190,12 +190,13 @@ public enum WxMaErrorMsgEnum {
*/
CODE_45072(45072, "command字段取值不对"),
/**
- *
* 下发输入状态,需要之前30秒内跟用户有过消息交互.
- * 对应操作:customerTyping
+ *
+ * 对应操作:{@code customerTyping}
* 对应地址:
* POST https://api.weixin.qq.com/cgi-bin/message/custom/typing?access_token=ACCESS_TOKEN
* 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/customerTyping.html
+ *
*/
CODE_45080(45080, "下发输入状态,需要之前30秒内跟用户有过消息交互"),
/**
@@ -686,7 +687,7 @@ public enum WxMaErrorMsgEnum {
/**
* 89252
- * 法人&企业信息一致性校验中 front checking
+ * {@code 法人&企业信息一致性校验中 front checking}
*/
CODE_89252(89252, "法人&企业信息一致性校验中"),
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxOpenErrorMsgEnum.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxOpenErrorMsgEnum.java
index 28fb5de8ad..ba910e988b 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxOpenErrorMsgEnum.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxOpenErrorMsgEnum.java
@@ -527,7 +527,7 @@ public enum WxOpenErrorMsgEnum {
CODE_40099(40099, "invalid code, this code has consumed."),
/**
- * invalid DateInfo, Make Sure OldDateInfoType==NewDateInfoType && NewBeginTime<=OldBeginTime && OldEndTime<= NewEndTime
+ * {@code invalid DateInfo, Make Sure OldDateInfoType==NewDateInfoType && NewBeginTime<=OldBeginTime && OldEndTime<= NewEndTime}
*/
CODE_40100(40100, "invalid DateInfo, Make Sure OldDateInfoType==NewDateInfoType && NewBeginTime<=OldBeginTime && OldEndTime<= NewEndTime"),
@@ -572,7 +572,7 @@ public enum WxOpenErrorMsgEnum {
CODE_40108(40108, "invalid client version"),
/**
- * too many code size, must <= 100
+ * {@code too many code size, must <= 100}
*/
CODE_40109(40109, "too many code size, must <= 100"),
@@ -702,7 +702,7 @@ public enum WxOpenErrorMsgEnum {
CODE_40135(40135, "invalid not supply bonus, can not change card_id which supply bonus to be not supply"),
/**
- * invalid use DepositCodeMode, make sure sku.quantity>DepositCode.quantity
+ * {@code invalid use DepositCodeMode, make sure sku.quantity>DepositCode.quantity}
*/
CODE_40136(40136, "invalid use DepositCodeMode, make sure sku.quantity>DepositCode.quantity"),
@@ -1082,7 +1082,7 @@ public enum WxOpenErrorMsgEnum {
CODE_40211(40211, "invalid scope_data"),
/**
- * paegs 当中存在不合法的query,query格式遵循URL标准,即k1=v1&k2=v2 invalid query
+ * {@code paegs 当中存在不合法的query,query格式遵循URL标准,即k1=v1&k2=v2 invalid query}
*/
CODE_40212(40212, "paegs 当中存在不合法的query,query格式遵循URL标准,即k1=v1&k2=v2"),
@@ -4242,7 +4242,7 @@ public enum WxOpenErrorMsgEnum {
CODE_71005(71005, "limit exe count"),
/**
- * limit coin count, 1 <= coin_count <= 100000
+ * {@code limit coin count, 1 <= coin_count <= 100000}
*/
CODE_71006(71006, "limit coin count, 1 <= coin_count <= 100000"),
@@ -4347,7 +4347,7 @@ public enum WxOpenErrorMsgEnum {
CODE_72018(72018, "duplicate order id, invoice had inserted to user"),
/**
- * limit msg operation card list size, must <= 5
+ * {@code limit msg operation card list size, must <= 5}
*/
CODE_72019(72019, "limit msg operation card list size, must <= 5"),
@@ -6432,7 +6432,7 @@ public enum WxOpenErrorMsgEnum {
CODE_88009(88009, "reply is not exists"),
/**
- * count range error. cout <= 0 or count > 50
+ * {@code count range error. cout <= 0 or count > 50}
*/
CODE_88010(88010, "count range error. cout <= 0 or count > 50"),
@@ -6682,7 +6682,7 @@ public enum WxOpenErrorMsgEnum {
CODE_89251(89251, "模板消息已下发,待法人人脸核身校验"),
/**
- * 法人&企业信息一致性校验中 front checking
+ * {@code 法人&企业信息一致性校验中 front checking}
*/
CODE_89253(89253, "法人&企业信息一致性校验中"),
@@ -7257,7 +7257,7 @@ public enum WxOpenErrorMsgEnum {
CODE_200021(200021, "场景描述 sceneDesc 参数错误"),
/**
- * 禁止创建/更新商品(如商品创建功能被封禁) 或 禁止编辑&更新房间
+ * {@code 禁止创建/更新商品(如商品创建功能被封禁) 或 禁止编辑&更新房间}
*/
CODE_300001(300001, "禁止创建/更新商品(如商品创建功能被封禁) 或 禁止编辑&更新房间"),
@@ -8382,7 +8382,7 @@ public enum WxOpenErrorMsgEnum {
CODE_9300003(9300003, "begin_time must less than end_time"),
/**
- * end_time - begin_time > 1year
+ * {@code end_time - begin_time > 1year}
*/
CODE_9300004(9300004, "end_time - begin_time > 1year"),
@@ -8397,7 +8397,7 @@ public enum WxOpenErrorMsgEnum {
CODE_9300006(9300006, "invalid activity status"),
/**
- * gift_num must >0 and <=15
+ * {@code gift_num must >0 and <=15}
*/
CODE_9300007(9300007, "gift_num must >0 and <=15"),
@@ -8412,7 +8412,7 @@ public enum WxOpenErrorMsgEnum {
CODE_9300009(9300009, "activity can not finish"),
/**
- * card_info_list must >= 2
+ * {@code card_info_list must >= 2}
*/
CODE_9300010(9300010, "card_info_list must >= 2"),
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorApacheImpl.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorApacheImpl.java
index 7f19241cdb..dba92e27da 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorApacheImpl.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorApacheImpl.java
@@ -44,11 +44,19 @@ public String execute(String uri, CommonUploadParam param, WxType wxType) throws
if (param != null) {
CommonUploadData data = param.getData();
InnerStreamBody part = new InnerStreamBody(data.getInputStream(), ContentType.DEFAULT_BINARY, data.getFileName(), data.getLength());
- HttpEntity entity = MultipartEntityBuilder
+ MultipartEntityBuilder entityBuilder = MultipartEntityBuilder
.create()
.addPart(param.getName(), part)
- .setMode(HttpMultipartMode.RFC6532)
- .build();
+ .setMode(HttpMultipartMode.RFC6532);
+
+ // 添加额外的表单字段
+ if (param.getFormFields() != null && !param.getFormFields().isEmpty()) {
+ for (java.util.Map.Entry entry : param.getFormFields().entrySet()) {
+ entityBuilder.addTextBody(entry.getKey(), entry.getValue(), ContentType.TEXT_PLAIN.withCharset("UTF-8"));
+ }
+ }
+
+ HttpEntity entity = entityBuilder.build();
httpPost.setEntity(entity);
}
String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE);
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorHttpComponentsImpl.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorHttpComponentsImpl.java
index f79eaa49b8..f79e4cd96f 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorHttpComponentsImpl.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorHttpComponentsImpl.java
@@ -41,11 +41,19 @@ public String execute(String uri, CommonUploadParam param, WxType wxType) throws
if (param != null) {
CommonUploadData data = param.getData();
InnerStreamBody part = new InnerStreamBody(data.getInputStream(), ContentType.DEFAULT_BINARY, data.getFileName(), data.getLength());
- HttpEntity entity = MultipartEntityBuilder
+ MultipartEntityBuilder entityBuilder = MultipartEntityBuilder
.create()
.addPart(param.getName(), part)
- .setMode(HttpMultipartMode.EXTENDED)
- .build();
+ .setMode(HttpMultipartMode.EXTENDED);
+
+ // 添加额外的表单字段
+ if (param.getFormFields() != null && !param.getFormFields().isEmpty()) {
+ for (java.util.Map.Entry entry : param.getFormFields().entrySet()) {
+ entityBuilder.addTextBody(entry.getKey(), entry.getValue(), ContentType.TEXT_PLAIN.withCharset("UTF-8"));
+ }
+ }
+
+ HttpEntity entity = entityBuilder.build();
httpPost.setEntity(entity);
}
String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE);
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorJoddHttpImpl.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorJoddHttpImpl.java
index 36e8660f77..182820d076 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorJoddHttpImpl.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorJoddHttpImpl.java
@@ -39,6 +39,14 @@ public String execute(String uri, CommonUploadParam param, WxType wxType) throws
}
request.withConnectionProvider(requestHttp.getRequestHttpClient());
request.form(param.getName(), new CommonUploadParamToUploadableAdapter(param.getData()));
+
+ // 添加额外的表单字段
+ if (param.getFormFields() != null && !param.getFormFields().isEmpty()) {
+ for (java.util.Map.Entry entry : param.getFormFields().entrySet()) {
+ request.form(entry.getKey(), entry.getValue());
+ }
+ }
+
HttpResponse response = request.send();
response.charset(StandardCharsets.UTF_8.name());
String responseContent = response.bodyText();
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorOkHttpImpl.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorOkHttpImpl.java
index 40a4622b89..6a0343980f 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorOkHttpImpl.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorOkHttpImpl.java
@@ -31,10 +31,18 @@ public CommonUploadRequestExecutorOkHttpImpl(RequestHttp entry : param.getFormFields().entrySet()) {
+ bodyBuilder.addFormDataPart(entry.getKey(), entry.getValue());
+ }
+ }
+
+ RequestBody body = bodyBuilder.build();
Request request = new Request.Builder().url(uri).post(body).build();
try (Response response = requestHttp.getRequestHttpClient().newCall(request).execute()) {
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java
index 19d4046c92..d531a2a307 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java
@@ -29,7 +29,7 @@ public void setValue(String key, String value, int expire, TimeUnit timeUnit) {
@Override
public Long getExpire(String key) {
- return redisTemplate.getExpire(key);
+ return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
@Override
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxOcrService.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxOcrService.java
index 39a8a93754..d0aeef8491 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxOcrService.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxOcrService.java
@@ -12,7 +12,9 @@
/**
* 基于小程序或 H5 的身份证、银行卡、行驶证 OCR 识别.
- * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=21516712284rHWMX
+ *
+ * 参考:{@code https://mp.weixin.qq.com/wiki?t=resource/res_main&id=21516712284rHWMX}
+ *
*
* @author Binary Wang
* created on 2019-06-22
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSessionManager.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSessionManager.java
index e3d9ab8351..24ea58ef38 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSessionManager.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSessionManager.java
@@ -7,13 +7,12 @@ public interface InternalSessionManager {
/**
* Return the active Session, associated with this Manager, with the
- * specified session id (if any); otherwise return null.
+ * specified session id (if any); otherwise return {@code null}.
*
* @param id The session id for the session to be returned
+ * @return the session or null
* @throws IllegalStateException if a new session cannot be
* instantiated for any reason
- * @throws java.io.IOException if an input/output error occurs while
- * processing this request
*/
InternalSession findSession(String id);
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/SignUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/SignUtils.java
index fc3579d45c..1886209f98 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/SignUtils.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/SignUtils.java
@@ -25,6 +25,7 @@ public class SignUtils {
*
* @param message 签名数据
* @param key 签名密钥
+ * @return 签名结果
*/
public static String createHmacSha256Sign(String message, String key) {
try {
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/SHA1.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/SHA1.java
index 9b9f776768..43cc54b43d 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/SHA1.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/SHA1.java
@@ -29,7 +29,10 @@ public static String gen(String... arr) {
}
/**
- * 用&串接arr参数,生成sha1 digest.
+ * {@code 用&串接arr参数,生成sha1 digest.}
+ *
+ * @param arr 参数数组
+ * @return sha1摘要
*/
public static String genWithAmple(String... arr) {
if (StringUtils.isAnyEmpty(arr)) {
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java
index 0b0590b1e6..50362636fc 100755
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java
@@ -197,6 +197,7 @@ public EncryptContext encryptContext(String plainText) {
/**
* 对明文进行加密.
*
+ * @param randomStr 随机字符串
* @param plainText 需要加密的明文
* @return 加密后base64编码的字符串
*/
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/InputStreamData.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/InputStreamData.java
index d07873f3c4..f03932984f 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/InputStreamData.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/InputStreamData.java
@@ -10,8 +10,9 @@
/**
* 输入流数据.
- *
+ *
* InputStreamData
+ *
*
* @author zichuan.zhou91@gmail.com
* created on 2022/2/15
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientBuilder.java
index de34ca5bd1..5b13e7cc17 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientBuilder.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientBuilder.java
@@ -21,42 +21,66 @@ public interface ApacheHttpClientBuilder {
/**
* 代理服务器地址.
+ *
+ * @param httpProxyHost 代理服务器地址
+ * @return ApacheHttpClientBuilder
*/
ApacheHttpClientBuilder httpProxyHost(String httpProxyHost);
/**
* 代理服务器端口.
+ *
+ * @param httpProxyPort 代理服务器端口
+ * @return ApacheHttpClientBuilder
*/
ApacheHttpClientBuilder httpProxyPort(int httpProxyPort);
/**
* 代理服务器用户名.
+ *
+ * @param httpProxyUsername 代理服务器用户名
+ * @return ApacheHttpClientBuilder
*/
ApacheHttpClientBuilder httpProxyUsername(String httpProxyUsername);
/**
* 代理服务器密码.
+ *
+ * @param httpProxyPassword 代理服务器密码
+ * @return ApacheHttpClientBuilder
*/
ApacheHttpClientBuilder httpProxyPassword(String httpProxyPassword);
/**
* 重试策略.
+ *
+ * @param httpRequestRetryHandler 重试处理器
+ * @return ApacheHttpClientBuilder
*/
- ApacheHttpClientBuilder httpRequestRetryHandler(HttpRequestRetryHandler httpRequestRetryHandler );
+ ApacheHttpClientBuilder httpRequestRetryHandler(HttpRequestRetryHandler httpRequestRetryHandler);
/**
* 超时时间.
+ *
+ * @param keepAliveStrategy 保持连接策略
+ * @return ApacheHttpClientBuilder
*/
ApacheHttpClientBuilder keepAliveStrategy(ConnectionKeepAliveStrategy keepAliveStrategy);
/**
* ssl连接socket工厂.
+ *
+ * @param sslConnectionSocketFactory SSL连接Socket工厂
+ * @return ApacheHttpClientBuilder
*/
ApacheHttpClientBuilder sslConnectionSocketFactory(SSLConnectionSocketFactory sslConnectionSocketFactory);
/**
* 支持的TLS协议版本.
* Supported TLS protocol versions.
+ *
+ * @param supportedProtocols 支持的协议版本数组
+ * @return ApacheHttpClientBuilder
*/
ApacheHttpClientBuilder supportedProtocols(String[] supportedProtocols);
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxGsonBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxGsonBuilder.java
index 6ea269f7e4..8f3dafe48a 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxGsonBuilder.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxGsonBuilder.java
@@ -43,6 +43,11 @@ public boolean shouldSkipClass(Class> aClass) {
});
}
+ /**
+ * 创建Gson实例
+ *
+ * @return Gson实例
+ */
public static Gson create() {
if (Objects.isNull(GSON_INSTANCE)) {
synchronized (INSTANCE) {
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamCDataListConverter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamCDataListConverter.java
new file mode 100644
index 0000000000..0b55a9c037
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamCDataListConverter.java
@@ -0,0 +1,54 @@
+package me.chanjar.weixin.common.util.xml;
+
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+
+/**
+ * 兼容两种格式的字符串列表转换器:
+ *
+ * - 旧格式(4.8.0之前):<MemChangeList><![CDATA[id1,id2]]></MemChangeList>
+ * - 新格式(4.8.0起):<MemChangeList><Item><![CDATA[id1]]></Item></MemChangeList>
+ *
+ * 解析结果统一为逗号分隔的字符串。
+ */
+public class XStreamCDataListConverter implements Converter {
+
+ @Override
+ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
+ if (source != null) {
+ writer.setValue("");
+ }
+ }
+
+ @Override
+ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+ if (reader.hasMoreChildren()) {
+ // 新格式:含有 - 子元素
+ StringBuilder sb = new StringBuilder();
+ while (reader.hasMoreChildren()) {
+ reader.moveDown();
+ String value = reader.getValue();
+ if (value != null && !value.isEmpty()) {
+ if (sb.length() > 0) {
+ sb.append(",");
+ }
+ sb.append(value);
+ }
+ reader.moveUp();
+ }
+ return sb.length() > 0 ? sb.toString() : null;
+ } else {
+ // 旧格式:直接 CDATA 文本
+ String value = reader.getValue();
+ return (value != null && !value.isEmpty()) ? value : null;
+ }
+ }
+
+ @Override
+ public boolean canConvert(Class type) {
+ return type == String.class;
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamInitializer.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamInitializer.java
index 3fa91fa70e..51cd1e980c 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamInitializer.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamInitializer.java
@@ -22,6 +22,11 @@ public class XStreamInitializer {
public static ClassLoader classLoader;
+ /**
+ * 设置类加载器
+ *
+ * @param classLoaderInfo 类加载器
+ */
public static void setClassLoader(ClassLoader classLoaderInfo) {
classLoader = classLoaderInfo;
}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/CommonUploadParamTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/CommonUploadParamTest.java
new file mode 100644
index 0000000000..05c8b379d3
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/CommonUploadParamTest.java
@@ -0,0 +1,119 @@
+package me.chanjar.weixin.common.bean;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * CommonUploadParam 单元测试
+ *
+ * @author Binary Wang
+ */
+@Test
+public class CommonUploadParamTest {
+
+ @Test
+ public void testFromFile() {
+ File file = new File("test.txt");
+ CommonUploadParam param = CommonUploadParam.fromFile("media", file);
+
+ Assert.assertNotNull(param);
+ Assert.assertEquals(param.getName(), "media");
+ Assert.assertNotNull(param.getData());
+ Assert.assertNull(param.getFormFields());
+ }
+
+ @Test
+ public void testFromBytes() {
+ byte[] bytes = "test content".getBytes();
+ CommonUploadParam param = CommonUploadParam.fromBytes("media", "test.txt", bytes);
+
+ Assert.assertNotNull(param);
+ Assert.assertEquals(param.getName(), "media");
+ Assert.assertNotNull(param.getData());
+ Assert.assertEquals(param.getData().getFileName(), "test.txt");
+ Assert.assertNull(param.getFormFields());
+ }
+
+ @Test
+ public void testAddFormField() {
+ File file = new File("test.txt");
+ CommonUploadParam param = CommonUploadParam.fromFile("media", file);
+
+ // 添加单个表单字段
+ param.addFormField("title", "测试标题");
+
+ Assert.assertNotNull(param.getFormFields());
+ Assert.assertEquals(param.getFormFields().size(), 1);
+ Assert.assertEquals(param.getFormFields().get("title"), "测试标题");
+
+ // 添加多个表单字段
+ param.addFormField("introduction", "测试介绍");
+
+ Assert.assertEquals(param.getFormFields().size(), 2);
+ Assert.assertEquals(param.getFormFields().get("introduction"), "测试介绍");
+ }
+
+ @Test
+ public void testAddFormFieldChaining() {
+ File file = new File("test.txt");
+ CommonUploadParam param = CommonUploadParam.fromFile("media", file)
+ .addFormField("title", "测试标题")
+ .addFormField("introduction", "测试介绍");
+
+ Assert.assertNotNull(param.getFormFields());
+ Assert.assertEquals(param.getFormFields().size(), 2);
+ Assert.assertEquals(param.getFormFields().get("title"), "测试标题");
+ Assert.assertEquals(param.getFormFields().get("introduction"), "测试介绍");
+ }
+
+ @Test
+ public void testConstructorWithFormFields() {
+ CommonUploadData data = new CommonUploadData("test.txt", null, 0);
+ Map
formFields = new HashMap<>();
+ formFields.put("title", "测试标题");
+ formFields.put("introduction", "测试介绍");
+
+ CommonUploadParam param = new CommonUploadParam("media", data, formFields);
+
+ Assert.assertNotNull(param.getFormFields());
+ Assert.assertEquals(param.getFormFields().size(), 2);
+ Assert.assertEquals(param.getFormFields().get("title"), "测试标题");
+ Assert.assertEquals(param.getFormFields().get("introduction"), "测试介绍");
+ }
+
+ @Test
+ public void testToString() {
+ File file = new File("test.txt");
+ CommonUploadParam param = CommonUploadParam.fromFile("media", file)
+ .addFormField("title", "测试标题");
+
+ String str = param.toString();
+ Assert.assertTrue(str.contains("name:media"));
+ Assert.assertTrue(str.contains("formFields:"));
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void testAddFormFieldWithNullFieldName() {
+ File file = new File("test.txt");
+ CommonUploadParam param = CommonUploadParam.fromFile("media", file);
+ param.addFormField(null, "value");
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void testAddFormFieldWithEmptyFieldName() {
+ File file = new File("test.txt");
+ CommonUploadParam param = CommonUploadParam.fromFile("media", file);
+ param.addFormField("", "value");
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void testAddFormFieldWithNullFieldValue() {
+ File file = new File("test.txt");
+ CommonUploadParam param = CommonUploadParam.fromFile("media", file);
+ param.addFormField("fieldName", null);
+ }
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java
index 96ba20ba2b..fb53c8c4b6 100644
--- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java
@@ -35,6 +35,17 @@ public void testGetExpire() {
Assert.assertTrue(expireSeconds <= 4 && expireSeconds >= 0);
}
+ @Test
+ public void testGetExpireForNonExistentKey() {
+ String nonExistentKey = "non_existent_key_" + System.currentTimeMillis();
+ Long expire = wxRedisOps.getExpire(nonExistentKey);
+ // 对于不存在的 key,底层使用 getExpire(key, TimeUnit.SECONDS) 时应返回负值
+ // Spring Data Redis 2.x 和 3.x 约定:-2 表示 key 不存在,-1 表示 key 没有过期时间
+ // 因此这里不应返回 null,而应返回一个小于 0 的值
+ Assert.assertNotNull(expire, "Non-existent key should not have null expiration");
+ Assert.assertTrue(expire < 0, "Non-existent key should have negative expiration");
+ }
+
@Test
public void testExpire() {
String key = "access_token", value = String.valueOf(System.currentTimeMillis());
diff --git a/weixin-java-cp/INTELLIGENT_ROBOT.md b/weixin-java-cp/INTELLIGENT_ROBOT.md
index f2641bd6b4..dcd90e1a1a 100644
--- a/weixin-java-cp/INTELLIGENT_ROBOT.md
+++ b/weixin-java-cp/INTELLIGENT_ROBOT.md
@@ -73,6 +73,42 @@ String sessionId = "session123";
robotService.resetSession(robotId, userid, sessionId);
```
+### 主动发送消息
+
+智能机器人可以主动向用户发送消息,用于推送通知或提醒。
+
+```java
+WxCpIntelligentRobotSendMessageRequest request = new WxCpIntelligentRobotSendMessageRequest();
+request.setRobotId("robot_id_here");
+request.setUserid("user123");
+request.setMessage("您好,这是来自智能机器人的主动消息");
+request.setSessionId("session123"); // 可选,用于保持会话连续性
+
+WxCpIntelligentRobotSendMessageResponse response = robotService.sendMessage(request);
+String msgId = response.getMsgId();
+String sessionId = response.getSessionId();
+```
+
+### 接收用户消息
+
+当用户向智能机器人发送消息时,企业微信会通过回调接口推送消息。可以使用 `WxCpXmlMessage` 接收和解析这些消息:
+
+```java
+// 在接收回调消息的接口中
+WxCpXmlMessage message = WxCpXmlMessage.fromEncryptedXml(
+ requestBody, wxCpConfigStorage, timestamp, nonce, msgSignature
+);
+
+// 获取智能机器人相关字段
+String robotId = message.getRobotId(); // 机器人ID
+String sessionId = message.getSessionId(); // 会话ID
+String content = message.getContent(); // 消息内容
+String fromUser = message.getFromUserName(); // 发送用户
+
+// 处理消息并回复
+// ...
+```
+
### 删除智能机器人
```java
@@ -87,13 +123,19 @@ robotService.deleteRobot(robotId);
- `WxCpIntelligentRobotCreateRequest`: 创建机器人请求
- `WxCpIntelligentRobotUpdateRequest`: 更新机器人请求
- `WxCpIntelligentRobotChatRequest`: 智能对话请求
+- `WxCpIntelligentRobotSendMessageRequest`: 主动发送消息请求
### 响应类
- `WxCpIntelligentRobotCreateResponse`: 创建机器人响应
- `WxCpIntelligentRobotChatResponse`: 智能对话响应
+- `WxCpIntelligentRobotSendMessageResponse`: 主动发送消息响应
- `WxCpIntelligentRobot`: 机器人信息实体
+### 消息接收
+
+- `WxCpXmlMessage`: 支持接收智能机器人回调消息,包含 `robotId` 和 `sessionId` 字段
+
### 服务接口
- `WxCpIntelligentRobotService`: 智能机器人服务接口
diff --git a/weixin-java-cp/pom.xml b/weixin-java-cp/pom.xml
index 00a6b2d06c..e109b1c60c 100644
--- a/weixin-java-cp/pom.xml
+++ b/weixin-java-cp/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.7.9.B
+ 4.8.2.B
weixin-java-cp
@@ -30,6 +30,11 @@
okhttp
provided
+ * 获取应用管理员列表 + * 第三方服务商可以用此接口获取授权企业中某个第三方应用或者代开发应用的管理员列表(不包括外部管理员), + * 以便服务商在用户进入应用主页之后根据是否管理员身份做权限的区分。 + * 详情请见: 文档 + *+ * + * @param agentId 应用id + * @return admin list + * @throws WxErrorException the wx error exception + */ + WxCpTpAdmin getAdminList(Integer agentId) throws WxErrorException; + } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpCorpGroupService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpCorpGroupService.java index 4da13d3fde..69aea4bca7 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpCorpGroupService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpCorpGroupService.java @@ -9,7 +9,7 @@ * 企业互联相关接口 * * @author libo <422423229@qq.com> - * Created on 27/2/2023 9:57 PM + * @since 2023-02-27 9:57 PM */ public interface WxCpCorpGroupService { /** diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExportService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExportService.java index 24c6ea9dc1..a2c7adabea 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExportService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExportService.java @@ -85,7 +85,7 @@ public interface WxCpExportService { * 获取导出结果 * * 请求方式:GET(HTTPS) - * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/export/get_result?access_token=ACCESS_TOKEN&jobid=jobid_xxxxxxxxxxxxxxx + * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/export/get_result?access_token=ACCESS_TOKEN&jobid=jobid_xxxxxxxxxxxxxxx} * * 文档地址:https://developer.work.weixin.qq.com/document/path/94854 * 返回的url文件下载解密可参考 CSDN diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExternalContactService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExternalContactService.java index 7f3cdeab7c..6de9f9226d 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExternalContactService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExternalContactService.java @@ -146,7 +146,7 @@ public interface WxCpExternalContactService { * 企业可通过此接口,根据外部联系人的userid(如何获取?),拉取客户详情。 * * 请求方式:GET(HTTPS) - * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token=ACCESS_TOKEN&external_userid=EXTERNAL_USERID + * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token=ACCESS_TOKEN&external_userid=EXTERNAL_USERID} * * 权限说明: * @@ -252,9 +252,9 @@ public interface WxCpExternalContactService { * * 权限说明: * - * 该企业授权了该服务商第三方应用,且授权的第三方应用具备“企业客户权限->客户基础信息”权限 + * {@code 该企业授权了该服务商第三方应用,且授权的第三方应用具备“企业客户权限->客户基础信息”权限} * 该客户的跟进人必须在应用的可见范围之内 - * 应用需具备“企业客户权限->客户基础信息”权限 + * {@code 应用需具备“企业客户权限->客户基础信息”权限} * * * @param externalUserid 代开发自建应用获取到的外部联系人ID @@ -276,8 +276,8 @@ public interface WxCpExternalContactService { * * @param externalUserid 服务商主体的external_userid,必须是source_agentid对应的应用所获取 * @param sourceAgentId 企业授权的代开发自建应用或第三方应用的agentid - * @return - * @throws WxErrorException + * @return 企业的external_userid + * @throws WxErrorException 微信错误异常 */ String fromServiceExternalUserid(String externalUserid, String sourceAgentId) throws WxErrorException; @@ -362,7 +362,7 @@ public interface WxCpExternalContactService { * 权限说明: * * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?) - * 第三方应用需具有“企业客户权限->客户基础信息”权限 + * {@code 第三方应用需具有“企业客户权限->客户基础信息”权限} * 对于第三方/自建应用,群主必须在应用的可见范围 * 仅支持企业服务人员创建的客户群 * 仅可转换出自己企业下的客户群chat_id @@ -410,11 +410,12 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c * 文档地址: https://developer.work.weixin.qq.com/document/path/99434 * * + * 注意:企业可通过外部联系人临时ID排除重复数据,外部联系人临时ID有效期为4小时。 + * * @param cursor the cursor * @param limit the limit * @return 已服务的外部联系人列表 * @throws WxErrorException . - * @apiNote 企业可通过外部联系人临时ID排除重复数据,外部联系人临时ID有效期为4小时。 */ WxCpExternalContactListInfo getContactList(String cursor, Integer limit) throws WxErrorException; @@ -438,7 +439,7 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c * 企业可通过此接口获取指定成员添加的客户列表。客户是指配置了客户联系功能的成员所添加的外部联系人。没有配置客户联系功能的成员,所添加的外部联系人将不会作为客户返回。 * * 请求方式:GET(HTTPS) - * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list?access_token=ACCESS_TOKEN&userid=USERID + * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list?access_token=ACCESS_TOKEN&userid=USERID} * * 权限说明: * @@ -469,7 +470,8 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c /** * 获取待分配的离职成员列表 * 企业和第三方可通过此接口,获取所有离职成员的客户列表,并可进一步调用分配离职成员的客户接口将这些客户重新分配给其他企业成员。 - *
+
+ *
* 请求方式:POST(HTTPS)
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_unassigned_list?access_token=ACCESS_TOKEN
*
@@ -496,17 +498,17 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c
/**
* 企业可通过此接口,转接在职成员的客户给其他成员。
- *
+ *
* 权限说明:
- * * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
- * 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限
+ * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+ * {@code 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限}
* 接替成员必须在此第三方应用或自建应用的可见范围内。
* 接替成员需要配置了客户联系功能。
* 接替成员需要在企业微信激活且已经过实名认证。
- *
+ *
* 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
- * 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限
+ * {@code 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限}
* 接替成员必须在此第三方应用或自建应用的可见范围内。
- *
+
+ *
* 权限说明:
- *
+
+ *
* 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
- * 第三方应用需拥有“企业客户权限->客户联系->离职分配”权限
+ * {@code 第三方应用需拥有“企业客户权限->客户联系->离职分配”权限}
* 接替成员必须在此第三方应用或自建应用的可见范围内。
* 接替成员需要配置了客户联系功能。
* 接替成员需要在企业微信激活且已经过实名认证。
- *
+
+ *
* 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
- * 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限
+ * {@code 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限}
* 接替成员必须在此第三方应用或自建应用的可见范围内。
- *
+
+ *
* 群主离职了的客户群,才可继承
* 继承给的新群主,必须是配置了客户联系功能的成员
* 继承给的新群主,必须有设置实名
* 继承给的新群主,必须有激活企业微信
* 同一个人的群,限制每天最多分配300个给新群主
- *
+
+ *
* 权限说明:
- *
+
+ *
* 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
- * 第三方应用需拥有“企业客户权限->客户联系->分配离职成员的客户群”权限
+ * {@code 第三方应用需拥有“企业客户权限->客户联系->分配离职成员的客户群”权限}
* 对于第三方/自建应用,群主必须在应用的可见范围。
- *
+
+ *
* 请求方式: POST(HTTP)
- *
+
+ *
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_msg_template?access_token=ACCESS_TOKEN
- *
+
+ *
* 文档地址
*
* @param wxCpMsgTemplate the wx cp msg template
@@ -733,15 +744,18 @@ WxCpUserExternalGroupChatStatistic getGroupChatStatistic(Date startTime, Integer
/**
* 提醒成员群发
* 企业和第三方应用可调用此接口,重新触发群发通知,提醒成员完成群发任务,24小时内每个群发最多触发三次提醒。
- *
+
+ *
* 请求方式: POST(HTTPS)
- *
+
+ *
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/remind_groupmsg_send?access_token=ACCESS_TOKEN
- *
+ *
* 文档地址
*
* @param msgId 群发消息的id,通过获取群发记录列表接口返回
* @return the wx cp msg template add result
+ * @throws WxErrorException 微信错误异常
*/
WxCpBaseResp remindGroupMsgSend(String msgId) throws WxErrorException;
@@ -753,11 +767,12 @@ WxCpUserExternalGroupChatStatistic getGroupChatStatistic(Date startTime, Integer
* 请求方式: POST(HTTPS)
*
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/cancel_groupmsg_send?access_token=ACCESS_TOKEN
- *
+ *
* 文档地址
*
* @param msgId 群发消息的id,通过获取群发记录列表接口返回
* @return the wx cp msg template add result
+ * @throws WxErrorException 微信错误异常
*/
WxCpBaseResp cancelGroupMsgSend(String msgId) throws WxErrorException;
@@ -1002,7 +1017,7 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
/**
*
+ * 请求方式:POST(HTTPS)
+ * 请求地址:...
+ * 权限说明:
+ * 需要配置人事助手的secret,调用接口前需给对应成员赋予人事小助手应用的权限。
+ *
+ * @param fields 指定字段key列表,不填则返回全部字段
+ * @return 字段信息响应 wx cp hr employee field info resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpHrEmployeeFieldInfoResp getFieldInfo(List
+ * 请求方式:POST(HTTPS)
+ * 请求地址:...
+ * 权限说明:
+ * 需要配置人事助手的secret,调用接口前需给对应成员赋予人事小助手应用的权限。
+ *
+ * @param userid 员工userid
+ * @param fields 指定字段key列表
+ * @return 员工档案数据响应 wx cp hr employee field data resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpHrEmployeeFieldDataResp getEmployeeFieldInfo(String userid, List
+ * 请求方式:POST(HTTPS)
+ * 请求地址:...
+ */
+ WxCpHrEmployeeFieldDataResp getEmployeeFieldInfo(String userid, boolean getAll, List
+ * 请求方式:POST(HTTPS)
+ * 请求地址:...
+ * 权限说明:
+ * 需要配置人事助手的secret,调用接口前需给对应成员赋予人事小助手应用的权限。
+ *
+ * @param userid 员工userid
+ * @param fieldList 字段数据列表
+ * @throws WxErrorException the wx error exception
+ */
+ void updateEmployeeFieldInfo(String userid, List
+ * 注意:
+ * 根据上面返回的文件类型,拼接好存放文件的绝对路径即可。此时绝对路径写入文件流,来达到获取媒体文件的目的。
+ * 详情可以看官方文档,亦可阅读此接口源码。
+ *
+ * @param sdkfileid 消息体内容中的sdkfileid信息
+ * @param proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081,如果没有传null
+ * @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null
+ * @param timeout 超时时间,分片数据需累加到文件存储。单次最大返回512K字节,如果文件比较大,自行设置长一点,比如timeout=10000
+ * @param targetFilePath 目标文件绝对路径+实际文件名,比如:/usr/local/file/20220114/474f866b39d10718810d55262af82662.gif
+ * @throws WxErrorException the wx error exception
+ */
+ void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout,
+ @NonNull String targetFilePath) throws WxErrorException;
+
/**
* 获取媒体文件 传入一个lambda,each所有的数据分片byte[],更加灵活
* 针对图片、文件等媒体数据,提供sdk接口拉取数据内容。
@@ -85,10 +152,29 @@ void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String proxy, St
* @param timeout 超时时间,分片数据需累加到文件存储。单次最大返回512K字节,如果文件比较大,自行设置长一点,比如timeout=10000
* @param action 传入一个lambda,each所有的数据分片
* @throws WxErrorException the wx error exception
+ * @deprecated 请使用 {@link #downloadMediaFile(String, String, String, long, Consumer)} 代替,
+ * 该方法需要传入SDK,容易导致SDK生命周期管理混乱,引发JVM崩溃
*/
+ @Deprecated
void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout,
@NonNull Consumer
+ * 在线程池场景下,任务结束后必须在 finally 块中调用此方法,防止SDK实例随线程复用而泄漏。
+ * 独立线程或一次性任务也建议调用,以主动释放原生资源。
+ */
+ void closeThreadLocalSdk();
+
+ /**
+ * 关闭所有会话存档SDK实例,释放全部原生资源。
+ *
+ * 适用于应用关闭阶段(如 Spring Bean 销毁阶段 {@code @PreDestroy} 或 Shutdown Hook)。
+ * 调用后,所有线程的SDK均不可再使用。
+ */
+ void closeAllSdks();
+
}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java
index b7a44047aa..1824196720 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java
@@ -90,9 +90,10 @@ public interface WxCpOAuth2Service {
/**
* 获取家校访问用户身份
* 该接口用于根据code获取家长或者学生信息
- *
+ *
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/doc_share?access_token=ACCESS_TOKEN
+ *
+ * @param request 分享请求,docid/formid 二选一
+ * @return url 文档分享链接
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocShare docShare(@NonNull WxCpDocShareRequest request) throws WxErrorException;
+
+ /**
+ * 获取文档权限信息
+ * 该接口用于获取文档、表格、智能表格的查看规则、文档通知范围及权限、安全设置信息。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/doc_get_auth?access_token=ACCESS_TOKEN
+ *
+ * @param docId 文档docid
+ * @return 文档权限信息
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocAuthInfo docGetAuth(@NonNull String docId) throws WxErrorException;
+
+ /**
+ * 修改文档查看规则
+ * 该接口用于修改文档、表格、智能表格查看规则。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/mod_doc_join_rule?access_token=ACCESS_TOKEN
+ *
+ * @param request 修改文档查看规则请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp docModifyJoinRule(@NonNull WxCpDocModifyJoinRuleRequest request) throws WxErrorException;
+
+ /**
+ * 修改文档通知范围及权限
+ * 该接口用于修改文档、表格、智能表格通知范围列表。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/mod_doc_member?access_token=ACCESS_TOKEN
+ *
+ * @param request 修改文档通知范围及权限请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp docModifyMember(@NonNull WxCpDocModifyMemberRequest request) throws WxErrorException;
+
+ /**
+ * 修改文档安全设置
+ * 该接口用于修改文档、表格、智能表格的安全设置。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/mod_doc_safty_setting?access_token=ACCESS_TOKEN
+ *
+ * @param request 修改文档安全设置请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp docModifySafetySetting(
+ @NonNull WxCpDocModifySafetySettingRequest request
+ ) throws WxErrorException;
+
+ /**
+ * @deprecated Use {@link #docModifySafetySetting(WxCpDocModifySafetySettingRequest)} instead.
+ */
+ @Deprecated
+ default WxCpBaseResp docModifySaftySetting(
+ @NonNull WxCpDocModifySaftySettingRequest request
+ ) throws WxErrorException {
+ WxCpDocModifySafetySettingRequest newReq =
+ WxCpDocModifySafetySettingRequest.builder()
+ .docId(request.getDocId())
+ .enableReadonlyCopy(request.getEnableReadonlyCopy())
+ .watermark(request.getWatermark())
+ .build();
+ return docModifySafetySetting(newReq);
+ }
+
+ /**
+ * 编辑表格内容
+ * 该接口可以对一个在线表格批量执行多个更新操作
+ *
+ * 注意:
+ * 1.批量更新请求中的各个操作会逐个按顺序执行,直到全部执行完成则请求返回,或者其中一个操作报错则不再继续执行后续的操作
+ * 2.每一个更新操作在执行之前都会做请求校验(包括权限校验、参数校验等等),如果校验未通过则该更新操作会报错并返回,不再执行后续操作
+ * 3.单次批量更新请求的操作数量 <= 5
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/spreadsheet/batch_update?access_token=ACCESS_TOKEN
+ *
+ * @param request 编辑表格内容请求参数
+ * @return 编辑表格内容批量更新的响应结果
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSheetBatchUpdateResponse docBatchUpdate(@NonNull WxCpDocSheetBatchUpdateRequest request) throws WxErrorException;
+
+ /**
+ * 获取表格行列信息
+ * 该接口用于获取在线表格的工作表、行数、列数等。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/spreadsheet/get_sheet_properties?access_token=ACCESS_TOKEN
+ *
+ * @param docId 在线表格的docid
+ * @return 返回表格行列信息
+ * @throws WxErrorException
+ */
+ WxCpDocSheetProperties getSheetProperties(@NonNull String docId) throws WxErrorException;
+
+
+ /**
+ * 本接口用于获取指定范围内的在线表格信息,单次查询的范围大小需满足以下限制:
+ *
+ * 查询范围行数 <=1000
+ * 查询范围列数 <=200
+ * 范围内的总单元格数量 <=10000
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/spreadsheet/get_sheet_range_data?access_token=ACCESS_TOKEN
+ *
+ * @param request 获取指定范围内的在线表格信息请求参数
+ * @return 返回指定范围内的在线表格信息
+ * @throws WxErrorException
+ */
+ WxCpDocSheetData getSheetRangeData(@NonNull WxCpDocSheetGetDataRequest request) throws WxErrorException;
+
+ /**
+ * 获取文档数据
+ * 该接口用于获取在线文档内容数据。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_doc_data?access_token=ACCESS_TOKEN
+ *
+ * @param request 获取文档数据请求参数
+ * @return 文档内容数据
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocData docGetData(@NonNull WxCpDocGetDataRequest request) throws WxErrorException;
+
+ /**
+ * 编辑文档内容
+ * 该接口用于编辑在线文档内容。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/mod_doc?access_token=ACCESS_TOKEN
+ *
+ * @param request 编辑文档内容请求参数
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp docModify(@NonNull WxCpDocModifyRequest request) throws WxErrorException;
+
+ /**
+ * 上传文档图片
+ * 该接口用于上传在线文档编辑时使用的图片资源。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/upload_doc_image?access_token=ACCESS_TOKEN
+ *
+ * @param file 图片文件
+ * @return 上传结果
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocImageUploadResult docUploadImage(@NonNull File file) throws WxErrorException;
+
+ /**
+ * 添加文档高级功能账号
+ * 该接口用于为在线文档添加高级功能账号。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/add_admin?access_token=ACCESS_TOKEN
+ *
+ * @param request 文档高级功能账号请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp docAddAdmin(@NonNull WxCpDocAdminRequest request) throws WxErrorException;
+
+ /**
+ * 删除文档高级功能账号
+ * 该接口用于删除在线文档的高级功能账号。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/del_admin?access_token=ACCESS_TOKEN
+ *
+ * @param request 文档高级功能账号请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp docDeleteAdmin(@NonNull WxCpDocAdminRequest request) throws WxErrorException;
+
+ /**
+ * 获取文档高级功能账号列表
+ * 该接口用于获取在线文档的高级功能账号列表。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_admin_list?access_token=ACCESS_TOKEN
+ *
+ * @param docId 文档 docid
+ * @return 文档高级功能账号列表
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocAdminListResult docGetAdminList(@NonNull String docId) throws WxErrorException;
+
+ /**
+ * 获取智能表格内容权限
+ * 该接口用于获取智能表格字段/记录等内容权限信息。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/smartsheet/get_sheet_auth?access_token=ACCESS_TOKEN
+ *
+ * @param request 智能表格内容权限请求
+ * @return 智能表格内容权限
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetAuth smartSheetGetAuth(@NonNull WxCpDocSmartSheetAuthRequest request) throws WxErrorException;
+
+ /**
+ * 修改智能表格内容权限
+ * 该接口用于修改智能表格字段/记录等内容权限信息。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/smartsheet/mod_sheet_auth?access_token=ACCESS_TOKEN
+ *
+ * @param request 修改智能表格内容权限请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetModifyAuth(@NonNull WxCpDocSmartSheetModifyAuthRequest request) throws WxErrorException;
+
+ /**
+ * 获取智能表格工作表信息.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格工作表信息
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetGetSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 新增智能表格工作表.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格工作表信息
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetAddSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 删除智能表格工作表.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetDeleteSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 更新智能表格工作表.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetUpdateSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 获取智能表格视图.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格视图
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetGetViews(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 新增智能表格视图.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格视图
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetAddView(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 删除智能表格视图.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetDeleteViews(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 更新智能表格视图.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetUpdateView(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 获取智能表格字段.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格字段
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetGetFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 新增智能表格字段.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格字段
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetAddFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 删除智能表格字段.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetDeleteFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 更新智能表格字段.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetUpdateFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 获取智能表格记录.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格记录
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetGetRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 新增智能表格记录.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格记录
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetAddRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 删除智能表格记录.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetDeleteRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 更新智能表格记录.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetUpdateRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 创建收集表
+ * 该接口用于创建收集表。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/create_collect?access_token=ACCESS_TOKEN
+ *
+ * @param request 创建收集表请求
+ * @return 创建收集表结果
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpFormCreateResult formCreate(@NonNull WxCpFormCreateRequest request) throws WxErrorException;
+
+ /**
+ * 编辑收集表
+ * 该接口用于编辑收集表。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/modify_collect?access_token=ACCESS_TOKEN
+ *
+ * @param request 编辑收集表请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp formModify(@NonNull WxCpFormModifyRequest request) throws WxErrorException;
+
+ /**
+ * 获取收集表信息
+ * 该接口用于读取收集表的信息。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_form_info?access_token=ACCESS_TOKEN
+ *
+ * @param formId 收集表id
+ * @return 收集表信息
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpFormInfoResult formInfo(@NonNull String formId) throws WxErrorException;
+
+ /**
+ * 获取收集表统计信息
+ * 该接口用于获取收集表的统计信息、已回答成员列表和未回答成员列表。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_form_statistic?access_token=ACCESS_TOKEN
+ *
+ * @param requests 收集表统计请求数组
+ * @return 收集表统计结果(包含 statistic_list)
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpFormStatisticResult formStatistic(@NonNull List
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_form_answer?access_token=ACCESS_TOKEN
+ *
+ * @param request 收集表答案请求
+ * @return 收集表答案
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpFormAnswer formAnswer(@NonNull WxCpFormAnswerRequest request) throws WxErrorException;
+
}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolService.java
index 56687c9cb1..5f1d41c197 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolService.java
@@ -80,9 +80,10 @@ public interface WxCpSchoolService {
/**
* 获取直播详情
+ *
+ *
+ *
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token=ACCESS_TOKEN&external_userid
- * =EXTERNAL_USERID
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token=ACCESS_TOKEN&external_userid=EXTERNAL_USERID}
*
* @param externalUserId 外部联系人的userid,注意不是学校成员的帐号
* @return external contact
@@ -306,9 +315,9 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 获取可使用的家长范围
* 获取可在微信「学校通知-学校应用」使用该应用的家长范围,以学生或部门列表的形式返回。应用只能给该列表下的家长发送「学校通知」。注意该范围只能由学校的系统管理员在「管理端-家校沟通-配置」配置。
- *
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/agent/get_allow_scope?access_token=ACCESS_TOKEN&agentid=AGENTID
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/agent/get_allow_scope?access_token=ACCESS_TOKEN&agentid=AGENTID}
*
* @param agentId the agent id
* @return allow scope
@@ -332,7 +341,7 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 获取部门列表
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/department/list?access_token=ACCESS_TOKEN&id=ID
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/department/list?access_token=ACCESS_TOKEN&id=ID}
*
* @param id 部门id。获取指定部门及其下的子部门。 如果不填,默认获取全量组织架构
* @return wx cp department list
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 0b601ca502..f66acc0252 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;
+ /**
+ * 通常通过 {@link #getOrInitThreadLocalSdk()} 间接调用以复用 ThreadLocal 中的实例;
+ * 旧版直接暴露 sdk 的 API(如 {@link #getChatDatas})也会直接调用本方法,此时 SDK 由调用方自行管理。 Finance.loadingLibraries() 底层依赖 System.load(),JVM 保证同一库不重复加载,多线程并发调用安全。
- * Created by songfan on 2020/7/14.
*
- * @author songfan & Mr.Pan
+ * @author songfan, Mr.Pan
+ * @since 2020/7/14
*/
@Data
@Builder
@@ -44,6 +44,12 @@ public class WxCpMsgTemplate implements Serializable {
@SerializedName("chat_id_list")
private List
* 文档地址
- *
* 文档地址
- *
* 文档地址
- *
* external_userid必须是handover_userid的客户(即配置了客户联系功能的成员所添加的联系人)。
* 在职成员的每位客户最多被分配2次。客户被转接成功后,将有90个自然日的服务关系保护期,保护期内的客户无法再次被分配。
- *
* 权限说明:
- *
* handover_userid必须是已离职用户。
* external_userid必须是handover_userid的客户(即配置了客户联系功能的成员所添加的联系人)。
* 在职成员的每位客户最多被分配2次。客户被转接成功后,将有90个自然日的服务关系保护期,保护期内的客户无法再次被分配。
- *
* 权限说明:
- *
* 注意::
- *
* 注意:
* 继承给的新群主,必须是配置了客户联系功能的成员
* 继承给的新群主,必须有设置实名
@@ -716,11 +724,14 @@ WxCpUserExternalGroupChatStatistic getGroupChatStatistic(Date startTime, Integer
* 企业可通过此接口添加企业群发消息的任务并通知客服人员发送给相关客户或客户群。(注:企业微信终端需升级到2.7.5版本及以上)
* 注意:调用该接口并不会直接发送消息给客户/客户群,需要相关的客服人员操作以后才会实际发送(客服人员的企业微信需要升级到2.7.5及以上版本)
* 同一个企业每个自然月内仅可针对一个客户/客户群发送4条消息,超过限制的用户将会被忽略。
- *
*
* @param mediaId 媒体id
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageService.java
index e49a36ba50..534cc89b36 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageService.java
@@ -72,8 +72,9 @@ public interface WxCpMessageService {
* 请求地址: 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
- * @throws WxErrorException
+ * @throws WxErrorException 异常
*/
void recall(String msgId) throws WxErrorException;
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java
index 221caf2e70..5e8811953f 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java
@@ -28,9 +28,26 @@ public interface WxCpMsgAuditService {
* @param timeout 超时时间,根据实际需要填写
* @return 返回是否调用成功 chat datas
* @throws Exception the exception
+ * @deprecated 请使用 {@link #getChatRecords(long, long, String, String, long)} 代替,
+ * 该方法会将SDK暴露给调用方,容易导致SDK生命周期管理混乱,引发JVM崩溃
*/
+ @Deprecated
WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, String passwd, @NonNull long timeout) throws Exception;
+ /**
+ * 拉取聊天记录函数(推荐使用)
+ * 该方法不会将SDK暴露给调用方,SDK生命周期由框架自动管理,更加安全
+ *
+ * @param seq 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0
+ * @param limit 一次拉取的消息条数,最大值1000条,超过1000条会返回错误
+ * @param proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081,如果没有传null
+ * @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null
+ * @param timeout 超时时间,根据实际需要填写
+ * @return 返回聊天记录列表,不包含SDK信息
+ * @throws Exception the exception
+ */
+ List
* 企业和第三方应用可通过此接口获取企业与成员的群发记录。
- * 获取企业群发成员执行结果
+ * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/93338
*
*
* @param msgid 群发消息的id,通过获取群发记录列表接口返回
@@ -1031,7 +1046,7 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
/**
*
* 获取群发成员发送任务列表。
- * 获取群发成员发送任务列表
+ * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/93338
*
*
* @param msgid 群发消息的id,通过获取群发记录列表接口返回
@@ -1045,7 +1060,7 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
/**
*
* 添加入群欢迎语素材。
- * 添加入群欢迎语素材
+ * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
*
*
* @param template 素材内容
@@ -1057,7 +1072,7 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
/**
*
* 编辑入群欢迎语素材。
- * 编辑入群欢迎语素材
+ * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
*
*
* @param template the template
@@ -1069,7 +1084,7 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
/**
*
* 获取入群欢迎语素材。
- * 获取入群欢迎语素材
+ * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
*
*
* @param templateId 群欢迎语的素材id
@@ -1082,7 +1097,7 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
*
* 删除入群欢迎语素材。
* 企业可通过此API删除入群欢迎语素材,且仅能删除调用方自己创建的入群欢迎语素材。
- * 删除入群欢迎语素材
+ * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
*
*
* @param templateId 群欢迎语的素材id
@@ -1094,8 +1109,8 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
/**
*
- * 获取商品图册
- * 获取商品图册列表
+ * 获取商品图册列表
+ * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/95096
*
*
* @param limit 返回的最大记录数,整型,最大值100,默认值50,超过最大值时取默认值
@@ -1108,7 +1123,7 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
/**
*
* 获取商品图册
- * 获取商品图册
+ * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/95096
*
*
* @param productId 商品id
@@ -1155,7 +1170,7 @@ WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, F
* 企业和第三方应用可以通过此接口新建敏感词规则
* 请求方式:POST(HTTPS)
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_intercept_rule?access_token=ACCESS_TOKEN
- *
+ *
* @param ruleAddRequest the rule add request
* @return 规则id
* @throws WxErrorException the wx error exception
@@ -1169,7 +1184,7 @@ WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, F
* 企业和第三方应用可以通过此接口修改敏感词规则
* 请求方式:POST(HTTPS)
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/update_intercept_rule?access_token=ACCESS_TOKEN
- *
+ *
* @param interceptRule the rule
* @throws WxErrorException the wx error exception
*/
@@ -1181,7 +1196,7 @@ WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, F
* 企业和第三方应用可以通过此接口修改敏感词规则
* 请求方式:POST(HTTPS)
* 请求地址
- *
+ *
* @param ruleId 规则id
* @throws WxErrorException the wx error exception
*/
@@ -1220,7 +1235,7 @@ WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, F
* 请求地址:
* https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_product_album?access_token=ACCESS_TOKEN
* 文档地址
- *
+ *
* @param wxCpProductAlbumInfo 商品图册信息
* @return 商品id string
* @throws WxErrorException the wx error exception
@@ -1235,7 +1250,7 @@ WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, F
* 请求地址:
* https://qyapi.weixin.qq.com/cgi-bin/externalcontact/update_product_album?access_token=ACCESS_TOKEN
* 文档地址
- *
+ *
* @param wxCpProductAlbumInfo 商品图册信息
* @throws WxErrorException the wx error exception
*/
@@ -1250,7 +1265,7 @@ WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, F
* https://qyapi.weixin.qq.com/cgi-bin/externalcontact/delete_product_album?access_token=ACCESS_TOKEN
*
* 文档地址
- *
+ *
* @param productId 商品id
* @throws WxErrorException the wx error exception
*/
@@ -1379,7 +1394,7 @@ WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, F
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/statistic?access_token=ACCESS_TOKEN
*
* @author Hugo
- * @date 2023/12/5 14:34
+ * @since 2023/12/5 14:34
* @param linkId 获客链接的id
* @param startTime 统计起始时间
* @param endTime 统计结束时间
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpGroupRobotService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpGroupRobotService.java
index c1a8d56255..b8ccea5e50 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpGroupRobotService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpGroupRobotService.java
@@ -126,9 +126,10 @@ public interface WxCpGroupRobotService {
/**
* 发送模板卡片消息
- * @param webhookUrl
- * @param wxCpGroupRobotMessage
- * @throws WxErrorException
+ *
+ * @param webhookUrl webhook地址
+ * @param wxCpGroupRobotMessage 群机器人消息
+ * @throws WxErrorException 异常
*/
void sendTemplateCardMessage(String webhookUrl, WxCpGroupRobotMessage wxCpGroupRobotMessage) throws WxErrorException;
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpHrService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpHrService.java
new file mode 100644
index 0000000000..d9d6ed0129
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpHrService.java
@@ -0,0 +1,68 @@
+package me.chanjar.weixin.cp.api;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldData;
+import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldDataResp;
+import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldInfoResp;
+
+import java.util.List;
+
+/**
+ * 人事助手相关接口.
+ * 官方文档:...
+ *
+ * @author copilot
+ */
+public interface WxCpHrService {
+
+ /**
+ * 获取员工档案字段信息.
+ *
+ *
* @param request 查询参数
* @return 客户数据统计 -企业汇总数据
* @throws WxErrorException the wx error exception
@@ -238,7 +238,7 @@ WxCpKfCustomerBatchGetResp customerBatchGet(List
+ *
* @param request 查询参数
* @return 客户数据统计 -企业汇总数据
* @throws WxErrorException the wx error exception
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpLivingService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpLivingService.java
index a2e2344190..63fabad7a1 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpLivingService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpLivingService.java
@@ -27,7 +27,7 @@ public interface WxCpLivingService {
/**
* 获取直播详情
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/living/get_living_info?access_token=ACCESS_TOKEN&livingid=LIVINGID
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/living/get_living_info?access_token=ACCESS_TOKEN&livingid=LIVINGID}
*
* @param livingId 直播id
* @return 获取的直播详情 living info
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java
index e874b26f42..dd5ce594b2 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java
@@ -110,9 +110,9 @@ WxMediaUploadResult upload(String mediaType, String filename, String url)
* 获取高清语音素材.
* 可以使用本接口获取从JSSDK的uploadVoice接口上传的临时语音素材,格式为speex,16K采样率。该音频比上文的临时素材获取接口(格式为amr,8K采样率)更加清晰,适合用作语音识别等对音质要求较高的业务。
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/media/get/jssdk?access_token=ACCESS_TOKEN&media_id=MEDIA_ID
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/media/get/jssdk?access_token=ACCESS_TOKEN&media_id=MEDIA_ID}
* 仅企业微信2.4及以上版本支持。
- * 文档地址:https://work.weixin.qq.com/api/doc#90000/90135/90255
+ * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/90255
*
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/getuserinfo?access_token=ACCESS_TOKEN&code=CODE}
+ *
*
* @param code the code
* @return school user info
@@ -123,7 +124,7 @@ public interface WxCpOAuth2Service {
/**
*
* 获取用户登录身份
- * https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
+ * {@code https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=ACCESS_TOKEN&code=CODE}
* 该接口可使用用户登录成功颁发的code来获取成员信息,适用于自建应用与代开发应用
*
* 注意: 旧的/user/getuserinfo 接口的url已变更为auth/getuserinfo,不过旧接口依旧可以使用,建议是关注新接口即可
@@ -140,13 +141,15 @@ public interface WxCpOAuth2Service {
/**
* 获取用户二次验证信息
- *
*
* @param wxCpOaMeetingRoomBookingInfoRequest 会议室预定信息查询对象
+ * @return 会议室预定信息
* @throws WxErrorException .
*/
WxCpOaMeetingRoomBookingInfoResult getMeetingRoomBookingInfo(WxCpOaMeetingRoomBookingInfoRequest wxCpOaMeetingRoomBookingInfoRequest) throws WxErrorException;
@@ -99,6 +100,7 @@ public interface WxCpOaMeetingRoomService {
*
*
* @param wxCpOaMeetingRoomBookRequest 会议室预定对象
+ * @return 预定结果
* @throws WxErrorException .
*/
WxCpOaMeetingRoomBookResult bookingMeetingRoom(WxCpOaMeetingRoomBookRequest wxCpOaMeetingRoomBookRequest) throws WxErrorException;
@@ -114,6 +116,7 @@ public interface WxCpOaMeetingRoomService {
*
*
* @param wxCpOaMeetingRoomBookByScheduleRequest 会议室预定对象
+ * @return 预定结果
* @throws WxErrorException .
*/
WxCpOaMeetingRoomBookResult bookingMeetingRoomBySchedule(WxCpOaMeetingRoomBookByScheduleRequest wxCpOaMeetingRoomBookByScheduleRequest) throws WxErrorException;
@@ -129,6 +132,7 @@ public interface WxCpOaMeetingRoomService {
*
*
* @param wxCpOaMeetingRoomBookByMeetingRequest 会议室预定对象
+ * @return 预定结果
* @throws WxErrorException .
*/
WxCpOaMeetingRoomBookResult bookingMeetingRoomByMeeting(WxCpOaMeetingRoomBookByMeetingRequest wxCpOaMeetingRoomBookByMeetingRequest) throws WxErrorException;
@@ -147,10 +151,10 @@ public interface WxCpOaMeetingRoomService {
* @param wxCpOaMeetingRoomCancelBookRequest 取消预定会议室对象
* @throws WxErrorException .
*/
- void cancelBookMeetingRoom(WxCpOaMeetingRoomCancelBookRequest wxCpOaMeetingRoomCancelBookRequest) throws WxErrorException;
+ void cancelBookMeetingRoom(WxCpOaMeetingRoomCancelBookRequest wxCpOaMeetingRoomCancelBookRequest) throws WxErrorException;
- /**
+ /**
* 根据会议室预定ID查询预定详情.
*
* api: https://qyapi.weixin.qq.com/cgi-bin/auth/get_tfa_info?access_token=ACCESS_TOKEN
* 权限说明:仅『通讯录同步』或者自建应用可调用,如用自建应用调用,用户需要在二次验证范围和应用可见范围内。
* 并发限制:20
+ *
*
* @param code 用户进入二次验证页面时,企业微信颁发的code,每次成员授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期
- * @return me.chanjar.weixin.cp.bean.workbench.WxCpSecondVerificationInfo 二次验证授权码,开发者可以调用通过二次验证接口,解锁企业微信终端.tfa_code有效期五分钟,且只能使用一次。
+ * @return 二次验证授权码,开发者可以调用通过二次验证接口,解锁企业微信终端.tfa_code有效期五分钟,且只能使用一次。
+ * @throws WxErrorException 微信错误异常
*/
WxCpSecondVerificationInfo getTfaInfo(String code) throws WxErrorException;
}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaMeetingRoomService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaMeetingRoomService.java
index c2e6c5c872..cc039fd9f5 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaMeetingRoomService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaMeetingRoomService.java
@@ -84,6 +84,7 @@ public interface WxCpOaMeetingRoomService {
*
* 企业可通过此接口根据预定id查询相关会议室的预定情况
@@ -161,8 +165,9 @@ public interface WxCpOaMeetingRoomService {
*
*
* @param wxCpOaMeetingRoomBookingInfoByBookingIdRequest 根据会议室预定ID查询预定详情对象
+ * @return 预定详情
* @throws WxErrorException .
*/
- WxCpOaMeetingRoomBookingInfoByBookingIdResult getBookingInfoByBookingId(WxCpOaMeetingRoomBookingInfoByBookingIdRequest wxCpOaMeetingRoomBookingInfoByBookingIdRequest) throws WxErrorException;
+ WxCpOaMeetingRoomBookingInfoByBookingIdResult getBookingInfoByBookingId(WxCpOaMeetingRoomBookingInfoByBookingIdRequest wxCpOaMeetingRoomBookingInfoByBookingIdRequest) throws WxErrorException;
}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaService.java
index ee57107b5c..3494dcfa4e 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaService.java
@@ -11,7 +11,8 @@
/**
* 企业微信OA相关接口.
*
- * @author Element & Wang_Wong created on 2019-04-06 10:52
+ * @author Element, Wang_Wong
+ * @since 2019-04-06 10:52
*/
public interface WxCpOaService {
@@ -331,7 +332,7 @@ List
+ *
* @param userId 需要录入的用户id
* @param userFace 需要录入的人脸图片数据,需要将图片数据base64处理后填入,对已录入的人脸会进行更新处理
* @throws WxErrorException the wx error exception
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaWeDocService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaWeDocService.java
index 1356c839b2..712bc2a89c 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaWeDocService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaWeDocService.java
@@ -5,6 +5,10 @@
import me.chanjar.weixin.cp.bean.WxCpBaseResp;
import me.chanjar.weixin.cp.bean.oa.doc.*;
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+
/**
* 企业微信文档相关接口.
* 文档
@@ -78,4 +82,462 @@ public interface WxCpOaWeDocService {
* @throws WxErrorException the wx error exception
*/
WxCpDocShare docShare(@NonNull String docId) throws WxErrorException;
+
+ /**
+ * 分享文档/收集表
+ * 该接口用于获取文档或收集表的分享链接。
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/living/get_living_info?access_token=ACCESS_TOKEN&livingid
- * =LIVINGID
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/living/get_living_info?access_token=ACCESS_TOKEN&livingid=LIVINGID}
+ *
*
* @param livingId the living id
* @return living info
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolUserService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolUserService.java
index a92bfcc100..d004ca8aa5 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolUserService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolUserService.java
@@ -19,9 +19,10 @@ public interface WxCpSchoolUserService {
/**
* 获取访问用户身份
* 该接口用于根据code获取成员信息
- *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE}
+ *
*
* @param code the code
* @return user info
@@ -32,9 +33,10 @@ public interface WxCpSchoolUserService {
/**
* 获取家校访问用户身份
* 该接口用于根据code获取家长或者学生信息
- *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/getuserinfo?access_token=ACCESS_TOKEN&code=CODE}
+ *
*
* @param code the code
* @return school user info
@@ -90,8 +92,10 @@ public interface WxCpSchoolUserService {
/**
* 删除学生
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/delete_student?access_token=ACCESS_TOKEN&userid=USERID
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/delete_student?access_token=ACCESS_TOKEN&userid=USERID}
+ *
*
* @param studentUserId the student user id
* @return wx cp base resp
@@ -160,8 +164,10 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 读取学生或家长
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/get?access_token=ACCESS_TOKEN&userid=USERID
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/get?access_token=ACCESS_TOKEN&userid=USERID}
+ *
*
* @param userId the user id
* @return user
@@ -171,9 +177,10 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 获取部门成员详情
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/list?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID
- * &fetch_child=FETCH_CHILD
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/list?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID&fetch_child=FETCH_CHILD}
+ *
*
* @param departmentId 获取的部门id
* @param fetchChild 1/0:是否递归获取子部门下面的成员
@@ -184,9 +191,10 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 获取部门家长详情
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/list_parent?access_token=ACCESS_TOKEN&department_id
- * =DEPARTMENT_ID
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/list_parent?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID}
+ *
*
* @param departmentId 获取的部门id
* @return user list parent
@@ -207,8 +215,10 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 删除家长
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/delete_parent?access_token=ACCESS_TOKEN&userid=USERID
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/delete_parent?access_token=ACCESS_TOKEN&userid=USERID}
+ *
*
* @param userId the user id
* @return wx cp base resp
@@ -256,7 +266,7 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 删除部门
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/department/delete?access_token=ACCESS_TOKEN&id=ID
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/department/delete?access_token=ACCESS_TOKEN&id=ID}
*
* @param id the id
* @return wx cp base resp
@@ -292,10 +302,9 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 获取外部联系人详情
* 学校可通过此接口,根据外部联系人的userid(如何获取?),拉取外部联系人详情。
- *
+ * 获取会话存档access_token,本方法线程安全
+ * 会话存档相关接口需要使用会话存档secret获取单独的access_token
+ * 详情请见: https://developer.work.weixin.qq.com/document/path/91782
+ *
+ *
+ * @param forceRefresh 强制刷新
+ * @return 会话存档专用的access token
+ * @throws WxErrorException the wx error exception
+ */
+ String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException;
+
/**
* 获得jsapi_ticket,不强制刷新jsapi_ticket
*
@@ -194,6 +207,19 @@ public interface WxCpService extends WxService {
*/
String postWithoutToken(String url, String postData) throws WxErrorException;
+ /**
+ *
+ * 使用会话存档access token发起post请求
+ * 会话存档相关API需要使用会话存档专用的secret获取独立的access token
+ *
+ *
+ * @param url 接口地址
+ * @param postData 请求body字符串
+ * @return the string
+ * @throws WxErrorException the wx error exception
+ */
+ String postForMsgAudit(String url, String postData) throws WxErrorException;
+
/**
*
* Service没有实现某个API的时候,可以用这个,
@@ -455,6 +481,13 @@ public interface WxCpService extends WxService {
*/
WxCpOaWeDriveService getOaWeDriveService();
+ /**
+ * 获取OA效率工具 文档的服务类对象
+ *
+ * @return oa we doc service
+ */
+ WxCpOaWeDocService getOaWeDocService();
+
/**
* 获取会话存档相关接口的服务类对象
*
@@ -584,7 +617,7 @@ public interface WxCpService extends WxService {
/**
* 企业互联的服务类对象
*
- * @return
+ * @return 企业互联服务对象
*/
WxCpCorpGroupService getCorpGroupService();
@@ -594,4 +627,11 @@ public interface WxCpService extends WxService {
* @return 智能机器人服务 intelligent robot service
*/
WxCpIntelligentRobotService getIntelligentRobotService();
+
+ /**
+ * 获取人事助手服务
+ *
+ * @return 人事助手服务 hr service
+ */
+ WxCpHrService getHrService();
}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpUserService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpUserService.java
index 2368386b23..7a7b5f40a8 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpUserService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpUserService.java
@@ -38,7 +38,7 @@ public interface WxCpUserService {
*
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 bc18c9bc7a..7c72cb9a8c 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
@@ -59,6 +59,7 @@ public abstract class BaseWxCpServiceImpl
* 获取部门成员详情
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID&fetch_child=FETCH_CHILD
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID&fetch_child=FETCH_CHILD}
*
* 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/90201
*
@@ -213,7 +213,7 @@ public interface WxCpUserService {
* 获取加入企业二维码。
*
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/corp/get_join_qrcode?access_token=ACCESS_TOKEN&size_type=SIZE_TYPE
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/corp/get_join_qrcode?access_token=ACCESS_TOKEN&size_type=SIZE_TYPE}
*
* 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/91714
* >() {
}.getType());
@@ -269,15 +309,106 @@ public WxCpGroupChat getGroupChat(@NonNull String roomid) throws WxErrorExceptio
final String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_GROUP_CHAT);
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("roomid", roomid);
- String responseContent = this.cpService.post(apiUrl, jsonObject.toString());
+ String responseContent = this.cpService.postForMsgAudit(apiUrl, jsonObject.toString());
return WxCpGroupChat.fromJson(responseContent);
}
@Override
public WxCpAgreeInfo checkSingleAgree(@NonNull WxCpCheckAgreeRequest checkAgreeRequest) throws WxErrorException {
String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(CHECK_SINGLE_AGREE);
- String responseContent = this.cpService.post(apiUrl, checkAgreeRequest.toJson());
+ String responseContent = this.cpService.postForMsgAudit(apiUrl, checkAgreeRequest.toJson());
return WxCpAgreeInfo.fromJson(responseContent);
}
+ @Override
+ public List
+ *
*
*
+ *
*
*
*
- * 获取审批申请详情
+ * 获取审批申请详情
*
* @param spNo 审批单编号。
* @param corpId the corp id
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOrderService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOrderService.java
index 3aff90bb56..6e0acb7dee 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOrderService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOrderService.java
@@ -18,7 +18,7 @@ public interface WxCpTpOrderService {
* 获取订单详情
*
+ * 获取部门成员.
+ *
+ * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E8.8E.B7.E5.8F.96.E9.83.A8.E9.97.A8.E6.88.90.E5.91.98
+ *
+ *
+ * @param departId 必填。部门id
+ * @param fetchChild 非必填。1/0:是否递归获取子部门下面的成员
+ * @param status 非必填。0获取全部员工,1获取已关注成员列表,2获取禁用成员列表,4获取未关注成员列表。status可叠加
+ * @return the list
+ * @throws WxErrorException the wx error exception
+ * @deprecated 第三方应用调用此接口需要使用 corpId 对应的 access_token,请使用
+ * {@link #listSimpleByDepartment(Long, Boolean, Integer, String)}
+ */
+ @Deprecated
List
* 文档地址 - *
+ * * * @param orderId 订单号 * @return the order @@ -49,7 +49,7 @@ public WxCpTpOrderDetails getOrder(String orderId) throws WxErrorException { * 获取订单列表 ** 文档地址 - *
+ * * * @param startTime 起始时间 * @param endTime 终止时间 diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpServiceHttpComponentsImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpServiceHttpComponentsImpl.java index bba597a3ee..44b5fd8693 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpServiceHttpComponentsImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpServiceHttpComponentsImpl.java @@ -10,7 +10,6 @@ import me.chanjar.weixin.common.util.http.hc.DefaultHttpComponentsClientBuilder; import me.chanjar.weixin.common.util.http.hc.HttpComponentsClientBuilder; import me.chanjar.weixin.common.util.json.GsonParser; -import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; import me.chanjar.weixin.cp.constant.WxCpApiPathConsts; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.config.RequestConfig; @@ -87,9 +86,10 @@ public void initHttp() { HttpComponentsClientBuilder apacheHttpClientBuilder = DefaultHttpComponentsClientBuilder.get(); apacheHttpClientBuilder.httpProxyHost(this.configStorage.getHttpProxyHost()) - .httpProxyPort(this.configStorage.getHttpProxyPort()) - .httpProxyUsername(this.configStorage.getHttpProxyUsername()) - .httpProxyPassword(this.configStorage.getHttpProxyPassword().toCharArray()); + .httpProxyPort(this.configStorage.getHttpProxyPort()) + .httpProxyUsername(this.configStorage.getHttpProxyUsername()) + .httpProxyPassword(this.configStorage.getHttpProxyPassword() == null ? null : + this.configStorage.getHttpProxyPassword().toCharArray()); if (this.configStorage.getHttpProxyHost() != null && this.configStorage.getHttpProxyPort() > 0) { this.httpProxy = new HttpHost(this.configStorage.getHttpProxyHost(), this.configStorage.getHttpProxyPort()); diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpTagServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpTagServiceImpl.java index b81760e72c..1b03f18c79 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpTagServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpTagServiceImpl.java @@ -25,7 +25,7 @@ * * * @author zhangq+ * 创建设备组 + * 详情请见:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/hardware-device/createIotGroupId.html + *+ * + * @param createIotGroupIdRequest 请求参数 + * @return 设备组的唯一标识 + * @throws WxErrorException + */ + String createIotGroupId(WxMaCreateIotGroupIdRequest createIotGroupIdRequest) throws WxErrorException; + + /** + *
+ * 查询设备组信息 + * 详情请见:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/hardware-device/getIotGroupInfo.html + *+ * + * @param getIotGroupInfoRequest 请求参数 + * @return 设备组信息 + * @throws WxErrorException + */ + WxMaIotGroupDeviceInfoResponse getIotGroupInfo(WxMaGetIotGroupInfoRequest getIotGroupInfoRequest) throws WxErrorException; + + /** + *
+ * 设备组添加设备 + * 一个设备组最多添加 50 个设备。 一个设备同一时间只能被添加到一个设备组中。 + * 详情请见:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/hardware-device/addIotGroupDevice.html + *+ * + * @param request 请求参数 + * @return 成功添加的设备信息 + * @throws WxErrorException + */ + List
+ * 设备组删除设备 + * 详情请见:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/hardware-device/removeIotGroupDevice.html + *+ * + * @param request 请求参数 + * @return 成功删除的设备信息 + * @throws WxErrorException + */ + List
+ * 文档地址:用工关系简介 + *
+ * + * @author Binary Wang + * created on 2025-12-19 + */ +public interface WxMaEmployeeRelationService { + + /** + * 解绑用工关系 + *+ * 企业可以调用该接口解除和用户的B2C用工关系 + *
+ * 文档地址:解绑用工关系 + * + * @param request 解绑请求参数 + * @throws WxErrorException 调用微信接口失败时抛出 + */ + void unbindEmployee(WxMaUnbindEmployeeRequest request) throws WxErrorException; + + /** + * 推送用工消息 + *+ * 企业可以调用该接口向用户推送用工相关消息 + *
+ * 文档地址:推送用工消息 + * + * @param request 推送消息请求参数 + * @throws WxErrorException 调用微信接口失败时抛出 + */ + void sendEmployeeMsg(WxMaSendEmployeeMsgRequest request) throws WxErrorException; +} diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaFaceService.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaFaceService.java new file mode 100644 index 0000000000..1b8d3cd74b --- /dev/null +++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaFaceService.java @@ -0,0 +1,51 @@ +package cn.binarywang.wx.miniapp.api; + +import cn.binarywang.wx.miniapp.bean.face.WxMaFaceGetVerifyIdRequest; +import cn.binarywang.wx.miniapp.bean.face.WxMaFaceGetVerifyIdResponse; +import cn.binarywang.wx.miniapp.bean.face.WxMaFaceQueryVerifyInfoRequest; +import cn.binarywang.wx.miniapp.bean.face.WxMaFaceQueryVerifyInfoResponse; +import me.chanjar.weixin.common.error.WxErrorException; + +/** + * 微信小程序人脸核身相关接口 + *+ * 文档地址:微信人脸核身接口列表 + *
+ * + * @author GitHub Copilot + */ +public interface WxMaFaceService { + + /** + * 获取用户人脸核身会话唯一标识 + *+ * 业务方后台根据「用户实名信息(姓名+身份证)」调用 getVerifyId 接口获取人脸核身会话唯一标识 verifyId 字段, + * 然后给到小程序前端调用 wx.requestFacialVerify 接口使用。 + *
+ *+ * 文档地址:获取用户人脸核身会话唯一标识 + *
+ * + * @param request 请求参数 + * @return 包含 verifyId 的响应实体 + * @throws WxErrorException 调用微信接口失败时抛出 + */ + WxMaFaceGetVerifyIdResponse getVerifyId(WxMaFaceGetVerifyIdRequest request) throws WxErrorException; + + /** + * 查询用户人脸核身真实验证结果 + *+ * 业务方后台根据人脸核身会话唯一标识 verifyId 字段调用 queryVerifyInfo 接口查询用户人脸核身真实验证结果。 + * 核身通过的判断条件:errcode=0 且 verify_ret=10000。 + *
+ *+ * 文档地址:查询用户人脸核身真实验证结果 + *
+ * + * @param request 请求参数 + * @return 包含 verifyRet 的响应实体 + * @throws WxErrorException 调用微信接口失败时抛出 + */ + WxMaFaceQueryVerifyInfoResponse queryVerifyInfo(WxMaFaceQueryVerifyInfoRequest request) 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 26ced8bedd..730a8c5840 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 @@ -621,4 +621,23 @@ WxMaApiResponse execute( * @return 交易投诉服务对象WxMaComplaintService */ WxMaComplaintService getComplaintService(); + + /** + * 获取用工关系服务对象。 + *若已配置 {@code apiSignatureAesKey} 及 {@code apiSignatureRsaPrivateKey} 开启服务端 API 签名,
+ * 该接口请求将自动走加密 + RSA 签名路径(见
+ * API签名文档)。
+ * 签名串格式为 {@code urlpath\nappid\ntimestamp\npostdata}(4 个字段),
+ * RSA 私钥序列号通过请求头 {@code Wechatmp-Serial} 传递,不包含在签名串中。
+ *
* @param code 每个code只能使用一次,code的有效期为5min。code获取方式参考手机号快速验证组件
* @return 用户手机号信息
* @throws WxErrorException .
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaXPayService.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaXPayService.java
index a633c93de6..68d4dc0c97 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaXPayService.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaXPayService.java
@@ -71,6 +71,16 @@ public interface WxMaXPayService {
*/
WxMaXPayPresentCurrencyResponse presentCurrency(WxMaXPayPresentCurrencyRequest request, WxMaXPaySigParams sigParams) throws WxErrorException;
+ /**
+ * 道具直购。
+ *
+ * @param request 道具直购请求对象
+ * @param sigParams 签名参数对象
+ * @return 道具直购结果
+ * @throws WxErrorException 直购失败时抛出
+ */
+ WxMaXPayPresentGoodsResponse presentGoods(WxMaXPayPresentGoodsRequest request, WxMaXPaySigParams sigParams) throws WxErrorException;
+
/**
* 下载对账单。
*
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 a5d479b65a..6ab0293e0c 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
@@ -167,6 +167,9 @@ public abstract class BaseWxMaServiceImpl 根据微信官方 API 签名规范,待签名串格式为: 注意:RSA 私钥序列号(rsaKeySn)不应包含在待签名串中,它应通过请求头
+ * {@code Wechatmp-Serial} 传递。4.8.0 曾错误地将 rsaKeySn 插入签名串(产生 5 个字段),
+ * 导致所有走 API 签名路径的接口(包括 {@code getPhoneNumber}、同城配送等)返回 40234
+ * {@code invalid signature}。此方法明确使用 4 字段格式以确保签名正确。
+ *
+ * @param urlPath 当前请求 API 的 URL,不含 Query 参数
+ * @param appId 小程序 AppId
+ * @param timestamp 签名时的时间戳
+ * @param postData 加密后的请求 POST 数据(JSON 字符串)
+ * @return 拼接好的待签名串
+ * @see 微信服务端API签名指南
+ */
+ static String buildSignaturePayload(String urlPath, String appId, long timestamp, String postData) {
+ return urlPath + "\n" + appId + "\n" + timestamp + "\n" + postData;
+ }
+
@Override
public String postWithSignature(String url, JsonObject jsonObject) throws WxErrorException {
long timestamp = System.currentTimeMillis() / 1000;
@@ -912,6 +938,10 @@ public String postWithSignature(String url, JsonObject jsonObject) throws WxErro
String rndStr = UUID.randomUUID().toString().replace("-", "").substring(0, 30);
String aesKey = this.getWxMaConfig().getApiSignatureAesKey();
String aesKeySn = this.getWxMaConfig().getApiSignatureAesKeySn();
+ String rsaKeySn = this.getWxMaConfig().getApiSignatureRsaPrivateKeySn();
+ if (rsaKeySn == null || rsaKeySn.isEmpty()) {
+ throw new SecurityException("ApiSignatureRsaPrivateKeySn不能为空,请检查配置");
+ }
jsonObject.addProperty("_n", rndStr);
jsonObject.addProperty("_appid", appId);
@@ -955,8 +985,8 @@ public String postWithSignature(String url, JsonObject jsonObject) throws WxErro
reqData.addProperty("authtag", base64Encode(authTag));
String requestJson = reqData.toString();
- // 计算签名 RSA
- String payload = urlPath + "\n" + appId + "\n" + timestamp + "\n" + requestJson;
+ // 计算签名 RSA,待签名串格式:urlpath\nappid\ntimestamp\npostdata
+ String payload = buildSignaturePayload(urlPath, appId, timestamp, requestJson);
byte[] dataBuffer = payload.getBytes(StandardCharsets.UTF_8);
RSAPrivateKey priKey;
try {
@@ -985,6 +1015,7 @@ public String postWithSignature(String url, JsonObject jsonObject) throws WxErro
header.put("Wechatmp-Signature", signatureString);
header.put("Wechatmp-Appid", appId);
header.put("Wechatmp-TimeStamp", String.valueOf(timestamp));
+ header.put("Wechatmp-Serial", rsaKeySn);
log.debug("发送请求uri:{}, headers:{}, postData:{}", url, header, requestJson);
WxMaApiResponse response =
this.execute(ApiSignaturePostRequestExecutor.create(this), url, header, requestJson);
@@ -1043,4 +1074,14 @@ public WxMaIntracityService getIntracityService() {
public WxMaComplaintService getComplaintService() {
return this.complaintService;
}
+
+ @Override
+ public WxMaEmployeeRelationService getEmployeeRelationService() {
+ return this.employeeRelationService;
+ }
+
+ @Override
+ public WxMaFaceService getFaceService() {
+ return this.faceService;
+ }
}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaDeviceSubscribeServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaDeviceSubscribeServiceImpl.java
index 7f8dce1df8..632fe7bd94 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaDeviceSubscribeServiceImpl.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaDeviceSubscribeServiceImpl.java
@@ -2,18 +2,26 @@
import cn.binarywang.wx.miniapp.api.WxMaDeviceSubscribeService;
import cn.binarywang.wx.miniapp.api.WxMaService;
-import cn.binarywang.wx.miniapp.bean.device.WxMaDeviceSubscribeMessageRequest;
-import cn.binarywang.wx.miniapp.bean.device.WxMaDeviceTicketRequest;
+import cn.binarywang.wx.miniapp.bean.device.*;
+import cn.binarywang.wx.miniapp.json.WxMaGsonBuilder;
import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
import lombok.RequiredArgsConstructor;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.enums.WxType;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.util.json.GsonParser;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+
+import java.util.List;
import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.DeviceSubscribe.GET_SN_TICKET_URL;
import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.DeviceSubscribe.SEND_DEVICE_SUBSCRIBE_MSG_URL;
+import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.DeviceSubscribe.CREATE_IOT_GROUP_ID_URL;
+import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.DeviceSubscribe.GET_IOT_GROUP_INFO_URL;
+import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.DeviceSubscribe.ADD_IOT_GROUP_DEVICE_URL;
+import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.DeviceSubscribe.REMOVE_IOT_GROUP_DEVICE_URL;
/**
* 小程序设备订阅消息相关 API
@@ -47,4 +55,46 @@ public void sendDeviceSubscribeMsg(WxMaDeviceSubscribeMessageRequest deviceSubsc
throw new WxErrorException(WxError.fromJson(responseContent, WxType.MiniApp));
}
}
+
+ @Override
+ public String createIotGroupId(WxMaCreateIotGroupIdRequest createIotGroupIdRequest) throws WxErrorException {
+ String responseContent = this.service.post(CREATE_IOT_GROUP_ID_URL, createIotGroupIdRequest.toJson());
+ JsonObject jsonObject = GsonParser.parse(responseContent);
+ if (jsonObject.get(WxConsts.ERR_CODE).getAsInt() != 0) {
+ throw new WxErrorException(WxError.fromJson(responseContent, WxType.MiniApp));
+ }
+ return jsonObject.get("group_id").getAsString();
+ }
+
+ @Override
+ public WxMaIotGroupDeviceInfoResponse getIotGroupInfo(WxMaGetIotGroupInfoRequest getIotGroupInfoRequest) throws WxErrorException {
+ String responseContent = this.service.post(GET_IOT_GROUP_INFO_URL, getIotGroupInfoRequest.toJson());
+ JsonObject jsonObject = GsonParser.parse(responseContent);
+ if (jsonObject.get(WxConsts.ERR_CODE).getAsInt() != 0) {
+ throw new WxErrorException(WxError.fromJson(responseContent, WxType.MiniApp));
+ }
+ return WxGsonBuilder.create().fromJson(responseContent, WxMaIotGroupDeviceInfoResponse.class);
+ }
+
+ @Override
+ public List
+ * 文档地址:推送用工消息
+ *
+ * 文档地址:解绑用工关系
+ *
+ * 文档地址:获取用户人脸核身会话唯一标识
+ *
+ * 文档地址:获取用户人脸核身会话唯一标识
+ *
+ * 文档地址:查询用户人脸核身真实验证结果
+ *
+ * 计算规则(参见官方文档):
+ * 1. 对 cert_type、cert_name、cert_no 字段内容进行标准 base64 编码(若含中文等 Unicode 字符,先进行 UTF-8 编码)
+ * 2. 按顺序拼接各个字段:cert_type=xxx&cert_name=xxx&cert_no=xxx
+ * 3. 对拼接串进行 SHA256 并输出十六进制小写结果
+ *
+ * 文档地址:查询用户人脸核身真实验证结果
+ * 直接测试 {@link BaseWxMaServiceImpl#buildSignaturePayload} 生产方法,
+ * 确保待签名串格式符合微信官方规范: 修复说明:4.8.0 版本在计算签名时错误地将 rsaKeySn 添加到签名串(5 个字段),
+ * 导致微信服务端返回 40234(invalid signature)。此测试验证修复后签名串格式正确(4 个字段,
+ * 不含 rsaKeySn),适用于所有走加密访问路径的接口(包括 getPhoneNumber、同城配送等)。
+ *
+ * @author GitHub Copilot
+ * @see
+ * 微信服务端API签名指南
+ */
+public class WxMaSignaturePayloadTest {
+
+ private static final String URL_PATH =
+ "https://api.weixin.qq.com/cgi-bin/express/intracity/createstore";
+ private static final String GET_PHONE_NUMBER_URL_PATH =
+ "https://api.weixin.qq.com/wxa/business/getuserphonenumber";
+ private static final String APP_ID = "wx1234567890abcdef";
+ private static final long TIMESTAMP = 1700000000L;
+ private static final String POST_DATA = "{\"iv\":\"abc\",\"data\":\"xyz\",\"authtag\":\"tag\"}";
+ private static final String RSA_KEY_SN = "some_serial_number";
+
+ /**
+ * 提供不同 API 接口 URL 作为测试数据,用于验证签名串格式在各接口中均符合规范。
+ */
+ @DataProvider(name = "apiUrlPaths")
+ public Object[][] apiUrlPaths() {
+ return new Object[][] {
+ {URL_PATH, "同城配送 createstore"},
+ {GET_PHONE_NUMBER_URL_PATH, "getPhoneNumber"}
+ };
+ }
+
+ /**
+ * 验证 buildSignaturePayload 返回的待签名串恰好包含 4 个字段,
+ * 格式为:urlpath\nappid\ntimestamp\npostdata。
+ * 使用多个 URL 验证,覆盖同城配送及 getPhoneNumber 等接口(issue 4.8.0 升级后 40234 错误)。
+ */
+ @Test(dataProvider = "apiUrlPaths")
+ public void testPayloadHasExactlyFourFields(String urlPath, String description) {
+ String payload =
+ BaseWxMaServiceImpl.buildSignaturePayload(urlPath, APP_ID, TIMESTAMP, POST_DATA);
+
+ String[] parts = payload.split("\n", -1);
+ assertEquals(parts.length, 4,
+ description + " 签名串应恰好包含 4 个字段(urlpath、appid、timestamp、postdata)");
+ assertEquals(parts[0], urlPath, description + " 第 1 段应为 urlpath");
+ assertEquals(parts[1], APP_ID, description + " 第 2 段应为 appid");
+ assertEquals(parts[2], String.valueOf(TIMESTAMP), description + " 第 3 段应为 timestamp");
+ assertEquals(parts[3], POST_DATA, description + " 第 4 段应为 postdata");
+ assertFalse(payload.contains(RSA_KEY_SN),
+ description + " 签名串不应包含 rsaKeySn,rsaKeySn 应通过请求头 Wechatmp-Serial 传递");
+ }
+
+ /**
+ * 验证 buildSignaturePayload 返回的待签名串与预期格式完全一致。
+ */
+ @Test
+ public void testPayloadMatchesExpectedFormat() {
+ String expected = URL_PATH + "\n" + APP_ID + "\n" + TIMESTAMP + "\n" + POST_DATA;
+ String actual =
+ BaseWxMaServiceImpl.buildSignaturePayload(URL_PATH, APP_ID, TIMESTAMP, POST_DATA);
+
+ assertEquals(actual, expected, "待签名串格式应为:urlpath\\nappid\\ntimestamp\\npostdata");
+ }
+}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaKefuMessageTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaKefuMessageTest.java
index 6486c3237f..c855b2747a 100644
--- a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaKefuMessageTest.java
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaKefuMessageTest.java
@@ -66,4 +66,14 @@ public void testURLEscaped() {
"\"link\":{\"title\":\"title\",\"description\":\"description\",\"url\":\"https://mp.weixin.qq.com/s?__biz=MzI0MDA2OTY5NQ==\",\"thumb_url\":\"thumbUrl\"}}");
}
+ public void testTextBuilderWithAiMsgContext() {
+ WxMaKefuMessage reply = WxMaKefuMessage.newTextBuilder()
+ .toUser("OPENID")
+ .content("回复内容")
+ .aiMsgContextMsgId("MSG_ID_123")
+ .build();
+ assertThat(reply.toJson())
+ .isEqualTo("{\"touser\":\"OPENID\",\"msgtype\":\"text\",\"text\":{\"content\":\"回复内容\"},\"aimsgcontext\":{\"msgid\":\"MSG_ID_123\"}}");
+ }
+
}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaMediaAsyncCheckResultTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaMediaAsyncCheckResultTest.java
new file mode 100644
index 0000000000..773271a513
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaMediaAsyncCheckResultTest.java
@@ -0,0 +1,52 @@
+package cn.binarywang.wx.miniapp.bean;
+
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+/**
+ * 测试多媒体内容安全异步检测结果解析
+ *
+ * @author copilot
+ */
+@Test
+public class WxMaMediaAsyncCheckResultTest {
+
+ public void testFromJsonWithResultAndDetail() {
+ String json = "{\n"
+ + " \"trace_id\": \"test_trace_id_001\",\n"
+ + " \"result\": {\n"
+ + " \"suggest\": \"risky\",\n"
+ + " \"label\": 20001\n"
+ + " },\n"
+ + " \"detail\": [\n"
+ + " {\n"
+ + " \"strategy\": \"content_model\",\n"
+ + " \"errcode\": 0,\n"
+ + " \"suggest\": \"risky\",\n"
+ + " \"label\": 20006,\n"
+ + " \"prob\": 90\n"
+ + " }\n"
+ + " ]\n"
+ + "}";
+
+ WxMaMediaAsyncCheckResult result = WxMaMediaAsyncCheckResult.fromJson(json);
+
+ assertNotNull(result);
+ assertEquals(result.getTraceId(), "test_trace_id_001");
+
+ assertNotNull(result.getResult());
+ assertEquals(result.getResult().getSuggest(), "risky");
+ assertEquals(result.getResult().getLabel(), "20001");
+
+ assertNotNull(result.getDetail());
+ assertEquals(result.getDetail().size(), 1);
+ WxMaMediaAsyncCheckResult.DetailBean detail = result.getDetail().get(0);
+ assertEquals(detail.getStrategy(), "content_model");
+ assertEquals(detail.getErrcode(), Integer.valueOf(0));
+ assertEquals(detail.getSuggest(), "risky");
+ assertEquals(detail.getLabel(), "20006");
+ assertEquals(detail.getProb(), Integer.valueOf(90));
+ }
+}
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 bfdb912f6d..00821eb636 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,5 +1,6 @@
package cn.binarywang.wx.miniapp.bean;
+import cn.binarywang.wx.miniapp.bean.xpay.WxMaXPayTeamInfo;
import me.chanjar.weixin.common.api.WxConsts;
import org.testng.annotations.Test;
@@ -294,38 +295,97 @@ public void testFromXmlForAllFieldsMap() {
}
/**
- * 自定义交易组件付款通知事件测试用例
- * msgType等于event且event等于WxConsts.EventType.OPEN_PRODUCT_ORDER_PAY
+ * 虚拟支付退款推送事件 xpay_refund_notify 测试用例(XML格式,含TeamInfo)
*/
@Test
- public void testFromXmlForOpenProductOrderPayEvent(){
- String xml = " 修复说明:{@link WxMaXPaySigParams#calcSig(String)} 方法计算用户态签名(HMAC-SHA256)后
+ * 未转小写,导致微信小程序端返回 {@code SIGNATURE_INVALID -15005} 错误。
+ * 修复方案与 {@link WxMaXPaySigParams#calcPaySig(String, String)} 保持一致,
+ * 在返回前调用 {@code .toLowerCase()}。
+ *
+ * @author GitHub Copilot
+ */
+public class WxMaXPaySigParamsTest {
+
+ private static final String SESSION_KEY = "your_session_key";
+ private static final String APP_KEY = "your_app_key";
+ private static final String POST_BODY = "{\"openid\":\"oHoSt5abc123\",\"env\":1}";
+ private static final String URL = "https://api.weixin.qq.com/xpay/query_user_balance";
+
+ /**
+ * 验证 calcSig 返回值全部为小写十六进制字符,不包含大写字母。
+ */
+ @Test
+ public void testCalcSigReturnsLowerCase() {
+ WxMaXPaySigParams sigParams = WxMaXPaySigParams.builder()
+ .sessionKey(SESSION_KEY)
+ .appKey(APP_KEY)
+ .build();
+
+ String sig = sigParams.calcSig(POST_BODY);
+
+ assertTrue(sig.equals(sig.toLowerCase()),
+ "calcSig 返回值应为全小写,当前值: " + sig);
+ }
+
+ /**
+ * 验证 calcPaySig 返回值全部为小写十六进制字符,不包含大写字母。
+ */
+ @Test
+ public void testCalcPaySigReturnsLowerCase() {
+ WxMaXPaySigParams sigParams = WxMaXPaySigParams.builder()
+ .sessionKey(SESSION_KEY)
+ .appKey(APP_KEY)
+ .build();
+
+ String paySig = sigParams.calcPaySig(URL, POST_BODY);
+
+ assertTrue(paySig.equals(paySig.toLowerCase()),
+ "calcPaySig 返回值应为全小写,当前值: " + paySig);
+ }
+
+ /**
+ * 验证 calcSig 与 calcPaySig 的返回值均为有效的 HMAC-SHA256 十六进制字符串(64 个字符)。
+ */
+ @Test
+ public void testCalcSigIsValidHexString() {
+ WxMaXPaySigParams sigParams = WxMaXPaySigParams.builder()
+ .sessionKey(SESSION_KEY)
+ .appKey(APP_KEY)
+ .build();
+
+ String sig = sigParams.calcSig(POST_BODY);
+ String paySig = sigParams.calcPaySig(URL, POST_BODY);
+
+ assertEquals(sig.length(), 64, "HMAC-SHA256 签名应为 64 个十六进制字符");
+ assertEquals(paySig.length(), 64, "HMAC-SHA256 pay_sig 应为 64 个十六进制字符");
+ assertTrue(sig.matches("[0-9a-f]+"), "calcSig 返回值应只含小写十六进制字符");
+ assertTrue(paySig.matches("[0-9a-f]+"), "calcPaySig 返回值应只含小写十六进制字符");
+ }
+}
diff --git a/weixin-java-mp/pom.xml b/weixin-java-mp/pom.xml
index 2b2e8e4210..487728ea42 100644
--- a/weixin-java-mp/pom.xml
+++ b/weixin-java-mp/pom.xml
@@ -7,7 +7,7 @@
+ * {@code urlpath\nappid\ntimestamp\npostdata}
+ * 字段之间使用换行符 {@code \n} 分隔,共 4 个字段,末尾无额外回车符。
+ *
+ * >() {
+ }.getType());
+ }
+
+ @Override
+ public List
>() {
+ }.getType());
+ }
}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaEmployeeRelationServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaEmployeeRelationServiceImpl.java
new file mode 100644
index 0000000000..08d29000ee
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaEmployeeRelationServiceImpl.java
@@ -0,0 +1,33 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaEmployeeRelationService;
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.employee.WxMaSendEmployeeMsgRequest;
+import cn.binarywang.wx.miniapp.bean.employee.WxMaUnbindEmployeeRequest;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.Employee.SEND_EMPLOYEE_MSG_URL;
+import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.Employee.UNBIND_EMPLOYEE_URL;
+
+/**
+ * 小程序用工关系相关操作接口实现
+ *
+ * @author Binary Wang
+ * created on 2025-12-19
+ * update on 2026-01-22 15:06:33
+ */
+@RequiredArgsConstructor
+public class WxMaEmployeeRelationServiceImpl implements WxMaEmployeeRelationService {
+ private final WxMaService service;
+
+ @Override
+ public void unbindEmployee(WxMaUnbindEmployeeRequest request) throws WxErrorException {
+ this.service.post(UNBIND_EMPLOYEE_URL, request.toJson());
+ }
+
+ @Override
+ public void sendEmployeeMsg(WxMaSendEmployeeMsgRequest request) throws WxErrorException {
+ this.service.post(SEND_EMPLOYEE_MSG_URL, request.toJson());
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaFaceServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaFaceServiceImpl.java
new file mode 100644
index 0000000000..7b3e2a1a56
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaFaceServiceImpl.java
@@ -0,0 +1,37 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaFaceService;
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.face.WxMaFaceGetVerifyIdRequest;
+import cn.binarywang.wx.miniapp.bean.face.WxMaFaceGetVerifyIdResponse;
+import cn.binarywang.wx.miniapp.bean.face.WxMaFaceQueryVerifyInfoRequest;
+import cn.binarywang.wx.miniapp.bean.face.WxMaFaceQueryVerifyInfoResponse;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.Face.GET_VERIFY_ID_URL;
+import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.Face.QUERY_VERIFY_INFO_URL;
+
+/**
+ * 微信小程序人脸核身相关接口实现
+ *
+ * @author GitHub Copilot
+ */
+@RequiredArgsConstructor
+public class WxMaFaceServiceImpl implements WxMaFaceService {
+ private final WxMaService service;
+
+ @Override
+ public WxMaFaceGetVerifyIdResponse getVerifyId(WxMaFaceGetVerifyIdRequest request)
+ throws WxErrorException {
+ String responseContent = this.service.post(GET_VERIFY_ID_URL, request.toJson());
+ return WxMaFaceGetVerifyIdResponse.fromJson(responseContent);
+ }
+
+ @Override
+ public WxMaFaceQueryVerifyInfoResponse queryVerifyInfo(WxMaFaceQueryVerifyInfoRequest request)
+ throws WxErrorException {
+ String responseContent = this.service.post(QUERY_VERIFY_INFO_URL, request.toJson());
+ return WxMaFaceQueryVerifyInfoResponse.fromJson(responseContent);
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaServiceHttpComponentsImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaServiceHttpComponentsImpl.java
new file mode 100644
index 0000000000..526e1ec4e7
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaServiceHttpComponentsImpl.java
@@ -0,0 +1,101 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.bean.WxMaStableAccessTokenRequest;
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.util.http.HttpClientType;
+import me.chanjar.weixin.common.util.http.hc.BasicResponseHandler;
+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.config.RequestConfig;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+
+import java.io.IOException;
+
+/**
+ * Apache Http client5 方式实现
+ *
+ * @author zhangyl
+ */
+@Slf4j
+public class WxMaServiceHttpComponentsImpl extends BaseWxMaServiceImpl
+ * 字段名:模板id
+ * 是否必填:是
+ * 描述:需要在微信后台申请用工关系权限,通过后创建的模板审核通过后可以复制模板ID
+ *
+ */
+ @SerializedName("template_id")
+ private String templateId;
+
+ /**
+ *
+ * 字段名:页面
+ * 是否必填:是
+ * 描述:用工消息通知跳转的page小程序链接(注意 小程序页面链接要是申请模板的小程序)
+ *
+ */
+ @SerializedName("page")
+ private String page;
+
+ /**
+ *
+ * 字段名:被推送用户的openId
+ * 是否必填:是
+ * 描述:被推送用户的openId
+ *
+ */
+ @SerializedName("touser")
+ private String touser;
+
+ /**
+ *
+ * 字段名:消息内容
+ * 是否必填:是
+ * 描述:需要根据小程序后台审核通过的模板id的字段类型序列化json传递
+ *
+ *
+ *
+ * 参考组装代码
+ *
+ *
+ */
+
+ @SerializedName("data")
+ private String data;
+
+ public String toJson() {
+ return WxMaGsonBuilder.create().toJson(this);
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/employee/WxMaUnbindEmployeeRequest.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/employee/WxMaUnbindEmployeeRequest.java
new file mode 100644
index 0000000000..e357f246a5
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/employee/WxMaUnbindEmployeeRequest.java
@@ -0,0 +1,43 @@
+package cn.binarywang.wx.miniapp.bean.employee;
+
+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;
+import java.util.List;
+
+/**
+ * 小程序解绑用工关系请求实体
+ *
+ * // 使用 HashMap 构建数据结构
+ * Map
+ *
+ * 字段名:用户openid列表
+ * 是否必填:是
+ * 描述:需要解绑的用户openid列表
+ *
+ */
+ @SerializedName("openid_list")
+ private List
+ * 字段名:业务方系统内部流水号
+ * 是否必填:是
+ * 描述:要求5-32个字符内,只能包含数字、大小写字母和_-字符,且在同一个appid下唯一
+ *
+ */
+ @SerializedName("out_seq_no")
+ private String outSeqNo;
+
+ /**
+ *
+ * 字段名:用户身份信息
+ * 是否必填:是
+ * 描述:证件信息对象
+ *
+ */
+ @SerializedName("cert_info")
+ private CertInfo certInfo;
+
+ /**
+ *
+ * 字段名:用户身份标识
+ * 是否必填:是
+ * 描述:用户的openid
+ *
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ * 用户身份信息
+ */
+ @Data
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class CertInfo implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * 字段名:证件类型
+ * 是否必填:是
+ * 描述:证件类型,身份证填 IDENTITY_CARD
+ *
+ */
+ @SerializedName("cert_type")
+ private String certType;
+
+ /**
+ *
+ * 字段名:证件姓名
+ * 是否必填:是
+ * 描述:证件上的姓名,UTF-8编码
+ *
+ */
+ @SerializedName("cert_name")
+ private String certName;
+
+ /**
+ *
+ * 字段名:证件号码
+ * 是否必填:是
+ * 描述:证件号码
+ *
+ */
+ @SerializedName("cert_no")
+ private String certNo;
+ }
+
+ public String toJson() {
+ return WxMaGsonBuilder.create().toJson(this);
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceGetVerifyIdResponse.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceGetVerifyIdResponse.java
new file mode 100644
index 0000000000..8fe8b191aa
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceGetVerifyIdResponse.java
@@ -0,0 +1,72 @@
+package cn.binarywang.wx.miniapp.bean.face;
+
+import cn.binarywang.wx.miniapp.json.WxMaGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 获取用户人脸核身会话唯一标识 响应实体
+ *
+ * 字段名:错误码
+ * 是否必填:是
+ * 类型:number
+ * 描述:0表示成功,其他值表示失败
+ *
+ */
+ @SerializedName("errcode")
+ private Integer errcode;
+
+ /**
+ *
+ * 字段名:错误信息
+ * 是否必填:是
+ * 类型:string
+ * 描述:错误信息描述
+ *
+ */
+ @SerializedName("errmsg")
+ private String errmsg;
+
+ /**
+ *
+ * 字段名:人脸核身会话唯一标识
+ * 是否必填:否
+ * 类型:string
+ * 描述:微信侧生成的人脸核身会话唯一标识,用于后续接口调用,长度不超过256字符
+ *
+ */
+ @SerializedName("verify_id")
+ private String verifyId;
+
+ /**
+ *
+ * 字段名:有效期
+ * 是否必填:否
+ * 类型:number
+ * 描述:verify_id有效期,过期后无法发起核身,默认值3600,单位:秒
+ *
+ */
+ @SerializedName("expires_in")
+ private Integer expiresIn;
+
+ public static WxMaFaceGetVerifyIdResponse fromJson(String json) {
+ return WxMaGsonBuilder.create().fromJson(json, WxMaFaceGetVerifyIdResponse.class);
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceQueryVerifyInfoRequest.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceQueryVerifyInfoRequest.java
new file mode 100644
index 0000000000..7ab8f7fbfc
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/face/WxMaFaceQueryVerifyInfoRequest.java
@@ -0,0 +1,108 @@
+package cn.binarywang.wx.miniapp.bean.face;
+
+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;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+
+/**
+ * 查询用户人脸核身真实验证结果 请求实体
+ *
+ * 字段名:人脸核身会话唯一标识
+ * 是否必填:是
+ * 描述:getVerifyId接口返回的人脸核身会话唯一标识
+ *
+ */
+ @SerializedName("verify_id")
+ private String verifyId;
+
+ /**
+ *
+ * 字段名:业务方系统外部流水号
+ * 是否必填:是
+ * 描述:必须和getVerifyId接口传入的out_seq_no一致
+ *
+ */
+ @SerializedName("out_seq_no")
+ private String outSeqNo;
+
+ /**
+ *
+ * 字段名:证件信息摘要
+ * 是否必填:是
+ * 描述:根据getVerifyId中传入的证件信息生成的信息摘要。
+ * 计算方式:对cert_info中的cert_type、cert_name、cert_no字段内容进行标准base64编码,
+ * 按顺序拼接:cert_type=xxx&cert_name=xxx&cert_no=xxx,再对拼接串进行SHA256输出十六进制小写结果
+ *
+ */
+ @SerializedName("cert_hash")
+ private String certHash;
+
+ /**
+ *
+ * 字段名:用户身份标识
+ * 是否必填:是
+ * 描述:必须和getVerifyId接口传入的openid一致
+ *
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ public String toJson() {
+ return WxMaGsonBuilder.create().toJson(this);
+ }
+
+ /**
+ * 计算证件信息摘要(cert_hash)
+ *
+ * 字段名:错误码
+ * 是否必填:是
+ * 类型:number
+ * 描述:0表示成功,其他值表示失败
+ *
+ */
+ @SerializedName("errcode")
+ private Integer errcode;
+
+ /**
+ *
+ * 字段名:错误信息
+ * 是否必填:是
+ * 类型:string
+ * 描述:错误信息描述
+ *
+ */
+ @SerializedName("errmsg")
+ private String errmsg;
+
+ /**
+ *
+ * 字段名:人脸核身验证结果
+ * 是否必填:否
+ * 类型:number
+ * 描述:核身通过的判断条件:errcode=0 且 verify_ret=10000
+ * 枚举值说明:
+ * 10000 - 识别成功
+ * 10001 - 参数错误
+ * 10002 - 人脸特征检测失败
+ * 10003 - 身份证号不匹配
+ * 10004 - 比对人脸信息不匹配
+ * 10005 - 正在检测中
+ * 10006 - appid没有权限
+ * 10300 - 未完成核身
+ * 90001 - 设备不支持人脸检测
+ * 90002 - 用户取消
+ * 其他枚举值请参见官方文档
+ *
+ */
+ @SerializedName("verify_ret")
+ private Integer verifyRet;
+
+ public static WxMaFaceQueryVerifyInfoResponse fromJson(String json) {
+ return WxMaGsonBuilder.create().fromJson(json, WxMaFaceQueryVerifyInfoResponse.class);
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPayPresentGoodsRequest.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPayPresentGoodsRequest.java
new file mode 100644
index 0000000000..ba9d7100da
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPayPresentGoodsRequest.java
@@ -0,0 +1,63 @@
+package cn.binarywang.wx.miniapp.bean.xpay;
+
+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;
+
+/**
+ * 小游戏道具直购API请求.
+ *
+ * @author Binary Wang
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxMaXPayPresentGoodsRequest implements Serializable {
+ private static final long serialVersionUID = 7495157056049312109L;
+
+ /**
+ * 用户的openid.
+ */
+ @SerializedName("openid")
+ private String openid;
+
+ /**
+ * 环境。0-正式环境;1-沙箱环境.
+ */
+ @SerializedName("env")
+ private Integer env;
+
+ /**
+ * 商户订单号.
+ */
+ @SerializedName("order_id")
+ private String orderId;
+
+ /**
+ * 设备类型。0-安卓;1-iOS.
+ */
+ @SerializedName("device_type")
+ private Integer deviceType;
+
+ /**
+ * 道具id.
+ */
+ @SerializedName("goods_id")
+ private String goodsId;
+
+ /**
+ * 道具数量.
+ */
+ @SerializedName("goods_number")
+ private Integer goodsNumber;
+
+ public String toJson() {
+ return WxMaGsonBuilder.create().toJson(this);
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPayPresentGoodsResponse.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPayPresentGoodsResponse.java
new file mode 100644
index 0000000000..ed3ea8d7f0
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPayPresentGoodsResponse.java
@@ -0,0 +1,34 @@
+package cn.binarywang.wx.miniapp.bean.xpay;
+
+import cn.binarywang.wx.miniapp.bean.WxMaBaseResponse;
+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;
+
+/**
+ * 小游戏道具直购API响应.
+ *
+ * @author Binary Wang
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxMaXPayPresentGoodsResponse extends WxMaBaseResponse implements Serializable {
+ private static final long serialVersionUID = 7495157056049312110L;
+
+ /**
+ * 商户订单号.
+ */
+ @SerializedName("order_id")
+ private String orderId;
+
+ public String toJson() {
+ return WxMaGsonBuilder.create().toJson(this);
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPayQueryOrderResponse.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPayQueryOrderResponse.java
index a0edf409bd..13a980e11c 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPayQueryOrderResponse.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPayQueryOrderResponse.java
@@ -61,5 +61,24 @@ public static class OrderInfo {
@SerializedName("wxOrderId")
private String wxOrderId;
+ /** 渠道单号,为用户微信支付详情页面上的商户单号 */
+ @SerializedName("channel_order_id")
+ private String channelOrderId;
+ /** 微信支付交易单号,为用户微信支付详情页面上的交易单号 */
+ @SerializedName("wxpay_order_id")
+ private String wxpayOrderId;
+ /** 结算时间的秒级时间戳,大于0表示结算成功 */
+ @SerializedName("sett_time")
+ private Long settTime;
+ /** 结算状态:0-未开始结算 1-结算中 2-结算成功 3-待结算(与0相同) */
+ @SerializedName("sett_state")
+ private Integer settState;
+ /** 虚拟支付技术服务费,单位为分;sett_state = 2时返回 */
+ @SerializedName("platform_fee_fen")
+ private Long platformFeeFen;
+ /** 公众号、视频号平台的cps服务费,单位为分;sett_state = 2时返回 */
+ @SerializedName("cps_fee_fen")
+ private Long cpsFeeFen;
+
}
}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPaySigParams.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPaySigParams.java
index abe6b2b982..6234a8d3ff 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPaySigParams.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPaySigParams.java
@@ -55,7 +55,7 @@ public String calcPaySig(String url, String postBody) {
public String calcSig(String postBody) {
String sk = StringUtils.trimToEmpty(this.sessionKey);
- return calcSignature(postBody, sk);
+ return calcSignature(postBody, sk).toLowerCase();
}
/**
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPayTeamInfo.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPayTeamInfo.java
new file mode 100644
index 0000000000..cadf98809c
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/xpay/WxMaXPayTeamInfo.java
@@ -0,0 +1,48 @@
+package cn.binarywang.wx.miniapp.bean.xpay;
+
+import com.google.gson.annotations.SerializedName;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 虚拟支付拼团信息.
+ * 用于 xpay_goods_deliver_notify、xpay_refund_notify 等推送事件
+ */
+@Data
+@NoArgsConstructor
+@XStreamAlias("TeamInfo")
+public class WxMaXPayTeamInfo implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 活动id.
+ */
+ @SerializedName("ActivityId")
+ @XStreamAlias("ActivityId")
+ private String activityId;
+
+ /**
+ * 团id.
+ */
+ @SerializedName("TeamId")
+ @XStreamAlias("TeamId")
+ private String teamId;
+
+ /**
+ * 团类型.
+ * 1-支付全部,拼成退款
+ */
+ @SerializedName("TeamType")
+ @XStreamAlias("TeamType")
+ private Integer teamType;
+
+ /**
+ * 0-创团 1-参团.
+ */
+ @SerializedName("TeamAction")
+ @XStreamAlias("TeamAction")
+ private Integer teamAction;
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/builder/BaseBuilder.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/builder/BaseBuilder.java
index c353534c3f..71f49ee2d3 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/builder/BaseBuilder.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/builder/BaseBuilder.java
@@ -8,6 +8,7 @@
public class BaseBuilder
+ * 文档地址: https://developers.weixin.qq.com/miniprogram/dev/server/API/laboruse/
+ *
+ */
+ public interface Employee {
+ /** 解绑用工关系 */
+ String UNBIND_EMPLOYEE_URL = "https://api.weixin.qq.com/wxa/business/unbinduserb2cauthinfo";
+ /** 推送用工消息 */
+ String SEND_EMPLOYEE_MSG_URL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/employeerelationmsg/send";
+ }
+
+ /**
+ * 微信人脸核身接口
+ *
+ * 文档地址: https://developers.weixin.qq.com/miniprogram/dev/server/API/face/
+ *
+ */
+ public interface Face {
+ /** 获取用户人脸核身会话唯一标识 */
+ String GET_VERIFY_ID_URL = "https://api.weixin.qq.com/cityservice/face/identify/getverifyid";
+ /** 查询用户人脸核身真实验证结果 */
+ String QUERY_VERIFY_INFO_URL = "https://api.weixin.qq.com/cityservice/face/identify/queryverifyinfo";
+ }
}
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 488481c011..eed7006314 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
@@ -263,6 +263,7 @@ public static final class XPayOrderStatus {
public static final class XPayNotifyEvent {
public static String COIN_PAY = "xpay_coin_pay_notify";
public static String GOODS_DELIVER = "xpay_goods_deliver_notify";
+ public static String REFUND = "xpay_refund_notify";
}
@UtilityClass
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 08346dbbb8..2343634bfc 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
@@ -36,6 +36,7 @@ public WxMaCryptUtils(WxMaConfig config) {
* @param sessionKey session_key
* @param encryptedData 消息密文
* @param ivStr iv字符串
+ * @return 解密后的字符串
*/
public static String decrypt(String sessionKey, String encryptedData, String ivStr) {
try {
@@ -58,6 +59,7 @@ public static String decrypt(String sessionKey, String encryptedData, String ivS
* @param sessionKey session_key
* @param encryptedData 消息密文
* @param ivStr iv字符串
+ * @return 解密后的字符串
*/
public static String decryptAnotherWay(String sessionKey, String encryptedData, String ivStr) {
byte[] keyBytes = Base64.decodeBase64(sessionKey.getBytes(UTF_8));
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/xml/XStreamTransformer.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/xml/XStreamTransformer.java
index f36d8c8fbd..b9e80d7341 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/xml/XStreamTransformer.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/xml/XStreamTransformer.java
@@ -24,7 +24,12 @@ public class XStreamTransformer {
}
/**
- * xml -> pojo.
+ * {@code xml -> pojo.}
+ *
+ * @param
+ * {@code urlpath\nappid\ntimestamp\npostdata}
+ * 共 4 个字段,字段间以换行符 {@code \n} 分隔,末尾无额外回车符。
+ *
+ *
* 创建卡券
- *
*
- * @param cardCreateRequest 卡券创建请求对象
+ * @param cardCreateMessage 卡券创建请求对象
* @return 卡券创建结果对象
* @throws WxErrorException 微信API调用异常,可能包括:
*
* 获取图文群发总数据(getarticletotal)
- * 详情请见文档:图文分析数据接口
+ *
+ * {@code 详情请见文档:图文分析数据接口}
+ *
+ *
* 接口url格式:https://api.weixin.qq.com/datacube/getarticletotal?access_token=ACCESS_TOKEN
+ *
*
* @param beginDate 开始时间
* @param endDate 最大时间跨度1天,endDate不能早于begingDate
@@ -73,10 +76,13 @@ public interface WxMpDataCubeService {
List getArticleTotal(Date beginDate, Date endDate) throws WxErrorException;
/**
- *
* 获取图文统计数据(getuserread)
- * 详情请见文档:图文分析数据接口
+ *
+ * {@code 详情请见文档:图文分析数据接口}
+ *
+ *
* 接口url格式:https://api.weixin.qq.com/datacube/getuserread?access_token=ACCESS_TOKEN
+ *
*
* @param beginDate 开始时间
* @param endDate 最大时间跨度3天,endDate不能早于begingDate
@@ -86,10 +92,13 @@ public interface WxMpDataCubeService {
List getUserRead(Date beginDate, Date endDate) throws WxErrorException;
/**
- *
* 获取图文统计分时数据(getuserreadhour)
- * 详情请见文档:图文分析数据接口
+ *
+ * {@code 详情请见文档:图文分析数据接口}
+ *
+ *
* 接口url格式:https://api.weixin.qq.com/datacube/getuserreadhour?access_token=ACCESS_TOKEN
+ *
*
* @param beginDate 开始时间
* @param endDate 最大时间跨度1天,endDate不能早于begingDate
@@ -99,10 +108,13 @@ public interface WxMpDataCubeService {
List getUserReadHour(Date beginDate, Date endDate) throws WxErrorException;
/**
- *
* 获取图文分享转发数据(getusershare)
- * 详情请见文档:图文分析数据接口
+ *
+ * {@code 详情请见文档:图文分析数据接口}
+ *
+ *
* 接口url格式:https://api.weixin.qq.com/datacube/getusershare?access_token=ACCESS_TOKEN
+ *
*
* @param beginDate 开始时间
* @param endDate 最大时间跨度7天,endDate不能早于begingDate
@@ -112,10 +124,13 @@ public interface WxMpDataCubeService {
List getUserShare(Date beginDate, Date endDate) throws WxErrorException;
/**
- *
* 获取图文分享转发分时数据(getusersharehour)
- * 详情请见文档:图文分析数据接口
+ *
+ * {@code 详情请见文档:图文分析数据接口}
+ *
+ *
* 接口url格式:https://api.weixin.qq.com/datacube/getusersharehour?access_token=ACCESS_TOKEN
+ *
*
* @param beginDate 开始时间
* @param endDate 最大时间跨度1天,endDate不能早于begingDate
@@ -127,10 +142,13 @@ public interface WxMpDataCubeService {
//*******************消息分析数据接口***********************//
/**
- *
* 获取消息发送概况数据(getupstreammsg)
- * 详情请见文档:消息分析数据接口
+ *
+ * {@code 详情请见文档:消息分析数据接口}
+ *
+ *
* 接口url格式:https://api.weixin.qq.com/datacube/getupstreammsg?access_token=ACCESS_TOKEN
+ *
*
* @param beginDate 开始时间
* @param endDate 最大时间跨度7天,endDate不能早于begingDate
@@ -140,10 +158,13 @@ public interface WxMpDataCubeService {
List getUpstreamMsg(Date beginDate, Date endDate) throws WxErrorException;
/**
- *
* 获取消息分送分时数据(getupstreammsghour)
- * 详情请见文档:消息分析数据接口
+ *
+ * {@code 详情请见文档:消息分析数据接口}
+ *
+ *
* 接口url格式:https://api.weixin.qq.com/datacube/getupstreammsghour?access_token=ACCESS_TOKEN
+ *
*
* @param beginDate 开始时间
* @param endDate 最大时间跨度1天,endDate不能早于begingDate
@@ -153,10 +174,13 @@ public interface WxMpDataCubeService {
List getUpstreamMsgHour(Date beginDate, Date endDate) throws WxErrorException;
/**
- *
* 获取消息发送周数据(getupstreammsgweek)
- * 详情请见文档:消息分析数据接口
+ *
+ * {@code 详情请见文档:消息分析数据接口}
+ *
+ *
* 接口url格式:https://api.weixin.qq.com/datacube/getupstreammsgweek?access_token=ACCESS_TOKEN
+ *
*
* @param beginDate 开始时间
* @param endDate 最大时间跨度30天,endDate不能早于begingDate
@@ -166,10 +190,13 @@ public interface WxMpDataCubeService {
List getUpstreamMsgWeek(Date beginDate, Date endDate) throws WxErrorException;
/**
- *
* 获取消息发送月数据(getupstreammsgmonth)
- * 详情请见文档:消息分析数据接口
+ *
+ * {@code 详情请见文档:消息分析数据接口}
+ *
+ *
* 接口url格式:https://api.weixin.qq.com/datacube/getupstreammsgmonth?access_token=ACCESS_TOKEN
+ *
*
* @param beginDate 开始时间
* @param endDate 最大时间跨度30天,endDate不能早于begingDate
@@ -179,10 +206,13 @@ public interface WxMpDataCubeService {
List getUpstreamMsgMonth(Date beginDate, Date endDate) throws WxErrorException;
/**
- *
* 获取消息发送分布数据(getupstreammsgdist)
- * 详情请见文档:消息分析数据接口
+ *
+ * {@code 详情请见文档:消息分析数据接口}
+ *
+ *
* 接口url格式:https://api.weixin.qq.com/datacube/getupstreammsgdist?access_token=ACCESS_TOKEN
+ *
*
* @param beginDate 开始时间
* @param endDate 最大时间跨度15天,endDate不能早于begingDate
@@ -192,10 +222,13 @@ public interface WxMpDataCubeService {
List getUpstreamMsgDist(Date beginDate, Date endDate) throws WxErrorException;
/**
- *
* 获取消息发送分布周数据(getupstreammsgdistweek)
- * 详情请见文档:消息分析数据接口
+ *
+ * {@code 详情请见文档:消息分析数据接口}
+ *
+ *
* 接口url格式:https://api.weixin.qq.com/datacube/getupstreammsgdistweek?access_token=ACCESS_TOKEN
+ *
*
* @param beginDate 开始时间
* @param endDate 最大时间跨度30天,endDate不能早于begingDate
@@ -205,10 +238,13 @@ public interface WxMpDataCubeService {
List getUpstreamMsgDistWeek(Date beginDate, Date endDate) throws WxErrorException;
/**
- *
* 获取消息发送分布月数据(getupstreammsgdistmonth)
- * 详情请见文档:消息分析数据接口
+ *
+ * {@code 详情请见文档:消息分析数据接口}
+ *
+ *
* 接口url格式:https://api.weixin.qq.com/datacube/getupstreammsgdistmonth?access_token=ACCESS_TOKEN
+ *
*
* @param beginDate 开始时间
* @param endDate 最大时间跨度30天,endDate不能早于begingDate
@@ -220,10 +256,13 @@ public interface WxMpDataCubeService {
//*******************接口分析数据接口***********************//
/**
- *
* 获取接口分析数据(getinterfacesummary)
- * 详情请见文档:接口分析数据接口
+ *
+ * {@code 详情请见文档:接口分析数据接口}
+ *
+ *
* 接口url格式:https://api.weixin.qq.com/datacube/getinterfacesummary?access_token=ACCESS_TOKEN
+ *
*
* @param beginDate 开始时间
* @param endDate 最大时间跨度30天,endDate不能早于begingDate
@@ -233,10 +272,13 @@ public interface WxMpDataCubeService {
List getInterfaceSummary(Date beginDate, Date endDate) throws WxErrorException;
/**
- *
* 获取接口分析分时数据(getinterfacesummaryhour)
- * 详情请见文档:接口分析数据接口
+ *
+ * {@code 详情请见文档:接口分析数据接口}
+ *
+ *
* 接口url格式:https://api.weixin.qq.com/datacube/getinterfacesummaryhour?access_token=ACCESS_TOKEN
+ *
*
* @param beginDate 开始时间
* @param endDate 最大时间跨度1天,endDate不能早于begingDate
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java
index 468dced138..2d965bf8de 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java
@@ -54,10 +54,10 @@ public interface WxMpService extends WxService {
WxMpShortKeyResult fetchShorten(String shortKey) throws WxErrorException;
/**
- *
* 验证消息的确来自微信服务器.
- * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN
- *
+ *
+ * {@code 详情请见: 接入指南}
+ *
*
* @param timestamp 时间戳,字符串格式
* @param nonce 随机串,字符串格式
@@ -76,16 +76,19 @@ public interface WxMpService extends WxService {
String getAccessToken() throws WxErrorException;
/**
- *
* 获取access_token,本方法线程安全.
+ *
* 且在多线程同时刷新时只刷新一次,避免超出2000次/日的调用次数上限
- *
+ *
+ *
* 另:本service的所有方法都会在access_token过期时调用此方法
- *
+ *
+ *
* 程序员在非必要情况下尽量不要主动调用此方法
- *
- * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183&token=&lang=zh_CN
- *
+ *
+ *
+ * {@code 详情请见: 获取access_token}
+ *
*
* @param forceRefresh 是否强制刷新,true表示强制刷新,false表示使用缓存
* @return token access token,字符串格式
@@ -126,12 +129,13 @@ public interface WxMpService extends WxService {
String getJsapiTicket() throws WxErrorException;
/**
- *
* 获得jsapi_ticket.
+ *
* 获得时会检查jsapiToken是否过期,如果过期了,那么就刷新一下,否则就什么都不干
- *
- * 详情请见:http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115&token=&lang=zh_CN
- *
+ *
+ *
+ * {@code 详情请见:JS-SDK使用权限签名算法}
+ *
*
* @param forceRefresh 强制刷新,true表示强制刷新,false表示使用缓存
* @return jsapi ticket,字符串格式
@@ -140,11 +144,10 @@ public interface WxMpService extends WxService {
String getJsapiTicket(boolean forceRefresh) throws WxErrorException;
/**
- *
* 创建调用jsapi时所需要的签名.
- *
- * 详情请见:http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115&token=&lang=zh_CN
- *
+ *
+ * {@code 详情请见:JS-SDK使用权限签名算法}
+ *
*
* @param url 当前网页的URL,不包括#及其后面部分
* @return 生成的签名对象,包含签名、时间戳、随机串等信息
@@ -153,10 +156,10 @@ public interface WxMpService extends WxService {
WxJsapiSignature createJsapiSignature(String url) throws WxErrorException;
/**
- *
* 长链接转短链接接口.
- * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=长链接转短链接接口
- *
+ *
+ * 详情请见: 长链接转短链接接口
+ *
*
* @param longUrl 长url,需要转换的原始URL
* @return 生成的短地址,字符串格式
@@ -167,10 +170,10 @@ public interface WxMpService extends WxService {
String shortUrl(String longUrl) throws WxErrorException;
/**
- *
* 语义查询接口.
- * 详情请见:http://mp.weixin.qq.com/wiki/index.php?title=语义理解
- *
+ *
+ * 详情请见:语义理解
+ *
*
* @param semanticQuery 查询条件,包含查询内容、类型等信息
* @return 查询结果,包含语义理解的结果和建议回复
@@ -179,11 +182,13 @@ public interface WxMpService extends WxService {
WxMpSemanticQueryResult semanticQuery(WxMpSemanticQuery semanticQuery) throws WxErrorException;
/**
- *
* 构造第三方使用网站应用授权登录的url.
- * 详情请见: 网站应用微信登录开发指南
- * URL格式为:https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
- *
+ *
+ * {@code 详情请见: 网站应用微信登录开发指南}
+ *
+ *
+ * {@code URL格式为:https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect}
+ *
*
* @param redirectUri 用户授权完成后的重定向链接,无需urlencode, 方法内会进行encode
* @param scope 应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login即可
@@ -193,10 +198,7 @@ public interface WxMpService extends WxService {
String buildQrConnectUrl(String redirectUri, String scope, String state);
/**
- *
* 获取微信服务器IP地址
- * http://mp.weixin.qq.com/wiki/0/2ad4b6bfd29f30f71d39616c2a0fcedc.html
- *
*
* @return 微信服务器ip地址数组,包含所有微信服务器IP地址
* @throws WxErrorException 微信API调用异常
@@ -204,11 +206,10 @@ public interface WxMpService extends WxService {
String[] getCallbackIP() throws WxErrorException;
/**
- *
- * 网络检测
- * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=21541575776DtsuT
- * 为了帮助开发者排查回调连接失败的问题,提供这个网络检测的API。它可以对开发者URL做域名解析,然后对所有IP进行一次ping操作,得到丢包率和耗时。
- *
+ * 网络检测
+ *
+ * 为了帮助开发者排查回调连接失败的问题,提供这个网络检测的API。它可以对开发者URL做域名解析,然后对所有IP进行一次ping操作,得到丢包率和耗时。
+ *
*
* @param action 执行的检测动作,可选值:all(全部检测)、dns(仅域名解析)、ping(仅网络连通性检测)
* @param operator 指定平台从某个运营商进行检测,可选值:CHINANET(中国电信)、UNICOM(中国联通)、CAP(中国联通)、CUCC(中国联通)
@@ -239,12 +240,13 @@ public interface WxMpService extends WxService {
WxMpCurrentAutoReplyInfo getCurrentAutoReplyInfo() throws WxErrorException;
/**
- *
- * 公众号调用或第三方平台帮公众号调用对公众号的所有api调用(包括第三方帮其调用)次数进行清零:
- * HTTP调用:https://api.weixin.qq.com/cgi-bin/clear_quota?access_token=ACCESS_TOKEN
- * 接口文档地址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433744592
- *
- *
+ * 公众号调用或第三方平台帮公众号调用对公众号的所有api调用(包括第三方帮其调用)次数进行清零.
+ *
+ * HTTP调用:https://api.weixin.qq.com/cgi-bin/clear_quota?access_token=ACCESS_TOKEN
+ *
+ *
+ * {@code 接口文档地址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433744592}
+ *
*
* @param appid 公众号的APPID,需要清零调用的公众号的appid
* @throws WxErrorException 微信API调用异常
@@ -252,11 +254,9 @@ public interface WxMpService extends WxService {
void clearQuota(String appid) throws WxErrorException;
/**
- *
* Service没有实现某个API的时候,可以用这个,
* 比{@link #get}和{@link #post}方法更灵活,可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
* 可以参考,{@link MediaUploadRequestExecutor}的实现方法
- *
*
* @param 返回值类型
* @param 参数类型
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/BaseWxMpServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/BaseWxMpServiceImpl.java
index 63ca608eba..76ab466157 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/BaseWxMpServiceImpl.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/BaseWxMpServiceImpl.java
@@ -304,8 +304,9 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
/**
* 通过网络请求获取稳定版接口调用凭据
*
- * @return .
- * @throws IOException .
+ * @param forceRefresh 是否强制刷新
+ * @return access_token字符串
+ * @throws IOException IO异常
*/
protected abstract String doGetStableAccessTokenRequest(boolean forceRefresh) throws IOException;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceHttpComponentsImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceHttpComponentsImpl.java
index bbf065acfc..c54202ad2f 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceHttpComponentsImpl.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceHttpComponentsImpl.java
@@ -51,7 +51,8 @@ public void initHttp() {
apacheHttpClientBuilder.httpProxyHost(configStorage.getHttpProxyHost())
.httpProxyPort(configStorage.getHttpProxyPort())
.httpProxyUsername(configStorage.getHttpProxyUsername())
- .httpProxyPassword(configStorage.getHttpProxyPassword().toCharArray());
+ .httpProxyPassword(configStorage.getHttpProxyPassword() == null ? null :
+ configStorage.getHttpProxyPassword().toCharArray());
if (configStorage.getHttpProxyHost() != null && configStorage.getHttpProxyPort() > 0) {
this.httpProxy = new HttpHost(configStorage.getHttpProxyHost(), configStorage.getHttpProxyPort());
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceImpl.java
index 79c3fad266..7cef64e576 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceImpl.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceImpl.java
@@ -2,11 +2,11 @@
/**
*
- * 默认接口实现类,使用apache httpclient实现
+ * 默认接口实现类,使用apache httpClient 5实现
* Created by Binary Wang on 2017-5-27.
*
*
* @author Binary Wang
*/
-public class WxMpServiceImpl extends WxMpServiceHttpClientImpl {
+public class WxMpServiceImpl extends WxMpServiceHttpComponentsImpl {
}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassOpenIdsMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassOpenIdsMessage.java
index 80e1658c16..936996ef69 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassOpenIdsMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassOpenIdsMessage.java
@@ -25,11 +25,11 @@ public class WxMpMassOpenIdsMessage implements Serializable {
/**
*
* 请使用
- * {@link WxConsts.MassMsgType#IMAGE}
- * {@link WxConsts.MassMsgType#MPNEWS}
- * {@link WxConsts.MassMsgType#TEXT}
- * {@link WxConsts.MassMsgType#MPVIDEO}
- * {@link WxConsts.MassMsgType#VOICE}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#IMAGE}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#MPNEWS}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#TEXT}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#MPVIDEO}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#VOICE}
* 如果msgtype和media_id不匹配的话,会返回系统繁忙的错误
*
*/
@@ -60,6 +60,8 @@ public String toJson() {
/**
* 添加openid,最多支持10,000个
+ *
+ * @param openid 用户openid
*/
public void addUser(String openid) {
this.toUsers.add(openid);
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassPreviewMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassPreviewMessage.java
index dca743c9c3..57b34d352a 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassPreviewMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassPreviewMessage.java
@@ -19,11 +19,11 @@ public class WxMpMassPreviewMessage implements Serializable {
*
* 消息类型
* 请使用
- * {@link WxConsts.MassMsgType#IMAGE}
- * {@link WxConsts.MassMsgType#MPNEWS}
- * {@link WxConsts.MassMsgType#TEXT}
- * {@link WxConsts.MassMsgType#MPVIDEO}
- * {@link WxConsts.MassMsgType#VOICE}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#IMAGE}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#MPNEWS}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#TEXT}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#MPVIDEO}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#VOICE}
* 如果msgtype和media_id不匹配的话,会返回系统繁忙的错误
*
*/
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassTagMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassTagMessage.java
index 598e5754f1..466ef8d96f 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassTagMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassTagMessage.java
@@ -24,11 +24,11 @@ public class WxMpMassTagMessage implements Serializable {
*
* 消息类型.
* 请使用
- * {@link WxConsts.MassMsgType#IMAGE}
- * {@link WxConsts.MassMsgType#MPNEWS}
- * {@link WxConsts.MassMsgType#TEXT}
- * {@link WxConsts.MassMsgType#MPVIDEO}
- * {@link WxConsts.MassMsgType#VOICE}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#IMAGE}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#MPNEWS}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#TEXT}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#MPVIDEO}
+ * {@link me.chanjar.weixin.common.api.WxConsts.MassMsgType#VOICE}
* 如果msgtype和media_id不匹配的话,会返回系统繁忙的错误
*
*/
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpUserQuery.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpUserQuery.java
index 9e73b46159..ac4a596dd8 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpUserQuery.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpUserQuery.java
@@ -63,9 +63,8 @@ public WxMpUserQuery add(String openid, String lang) {
/**
* 添加一个OpenId到列表中,并返回本对象
*
- *
* 该方法默认lang = zh_CN
- *
+ *
*
* @param openid openid
* @return {@link WxMpUserQuery}
@@ -100,6 +99,8 @@ public WxMpUserQuery remove(String openid, String lang) {
/**
* 获取查询参数列表
+ *
+ * @return 查询参数列表
*/
public List getQueryParamList() {
return this.queryParamList;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/WxMpCardMpnewsGethtmlResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/WxMpCardMpnewsGethtmlResult.java
index 6d7dde1ad6..34a9c56b99 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/WxMpCardMpnewsGethtmlResult.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/WxMpCardMpnewsGethtmlResult.java
@@ -8,7 +8,9 @@
/**
- * @author S
+ * 卡券图文消息HTML结果
+ *
+ * @author S (sshzh90@gmail.com)
*/
@Data
public class WxMpCardMpnewsGethtmlResult implements Serializable {
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/MemberCardActivateUserFormRequest.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/MemberCardActivateUserFormRequest.java
index d8634cfa3c..0adb413869 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/MemberCardActivateUserFormRequest.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/MemberCardActivateUserFormRequest.java
@@ -39,8 +39,8 @@ public class MemberCardActivateUserFormRequest implements Serializable {
/**
* 绑定老会员卡信息
*
- * @param name
- * @param url
+ * @param name 名称
+ * @param url 链接地址
*/
public void setBindOldCard(String name, String url) {
if (StringUtils.isAnyEmpty(name, url)) {
@@ -56,8 +56,8 @@ public void setBindOldCard(String name, String url) {
/**
* 设置服务声明,用于放置商户会员卡守则
*
- * @param name
- * @param url
+ * @param name 名称
+ * @param url 链接地址
*/
public void setServiceStatement(String name, String url) {
if (StringUtils.isAnyEmpty(name, url)) {
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/MemberCardUserForm.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/MemberCardUserForm.java
index 0c0fae3e2b..b3b0c9be5e 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/MemberCardUserForm.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/MemberCardUserForm.java
@@ -50,6 +50,7 @@ public class MemberCardUserForm implements Serializable {
/**
* 添加富文本类型字段
*
+ * @param field 富文本字段
*/
public void addRichField(MemberCardUserFormRichField field) {
if (field == null) {
@@ -64,6 +65,7 @@ public void addRichField(MemberCardUserFormRichField field) {
/**
* 添加微信选项类型字段
*
+ * @param fieldType 微信字段类型
*/
public void addWechatField(CardWechatFieldType fieldType) {
if (fieldType == null) {
@@ -78,6 +80,7 @@ public void addWechatField(CardWechatFieldType fieldType) {
/**
* 添加文本类型字段
*
+ * @param field 文本字段
*/
public void addCustomField(String field) {
if (StringUtils.isBlank(field)) {
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/NotifyOptional.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/NotifyOptional.java
index 139db68557..1ba8a0e60c 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/NotifyOptional.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/NotifyOptional.java
@@ -6,12 +6,13 @@
import java.io.Serializable;
/**
- *
* 控制原生消息结构体,包含各字段的消息控制字段。
- *
+ *
* 用于 `7 更新会员信息` 的接口参数调用
- * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1451025283
- *
+ *
+ *
+ * {@code 参考:会员卡接口}
+ *
*
* @author YuJian(mgcnrx11@gmail.com)
* @version 2017/7/15
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/WxMpMemberCardUpdateResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/WxMpMemberCardUpdateResult.java
index 663fe1f1e5..b4ad8eb139 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/WxMpMemberCardUpdateResult.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/WxMpMemberCardUpdateResult.java
@@ -6,10 +6,10 @@
import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
/**
- *
* 用于 `7 更新会员信息` 的接口调用后的返回结果
- * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1451025283
- *
+ *
+ * {@code 参考:会员卡接口}
+ *
*
* @author YuJian(mgcnrx11@gmail.com)
* @version 2017/7/15
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/WxMpMemberCardUserInfoResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/WxMpMemberCardUserInfoResult.java
index 8fad40ccf8..9a2b47f5bf 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/WxMpMemberCardUserInfoResult.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/card/membercard/WxMpMemberCardUserInfoResult.java
@@ -6,11 +6,10 @@
import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
/**
- *
* 拉取会员信息返回的结果
- *
- * 字段格式参考https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1451025283 6.2.1小节的步骤5
- *
+ *
+ * {@code 字段格式参考:会员卡接口 6.2.1小节的步骤5}
+ *
*
* @author YuJian
* @version 2017/7/9
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/WxMpKefuMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/WxMpKefuMessage.java
index f066c1d934..01be3c08d2 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/WxMpKefuMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/WxMpKefuMessage.java
@@ -45,6 +45,8 @@ public class WxMpKefuMessage implements Serializable {
/**
* 获得文本消息builder.
+ *
+ * @return 文本消息builder
*/
public static TextBuilder TEXT() {
return new TextBuilder();
@@ -52,6 +54,8 @@ public static TextBuilder TEXT() {
/**
* 获得图片消息builder.
+ *
+ * @return 图片消息builder
*/
public static ImageBuilder IMAGE() {
return new ImageBuilder();
@@ -59,6 +63,8 @@ public static ImageBuilder IMAGE() {
/**
* 获得语音消息builder.
+ *
+ * @return 语音消息builder
*/
public static VoiceBuilder VOICE() {
return new VoiceBuilder();
@@ -66,6 +72,8 @@ public static VoiceBuilder VOICE() {
/**
* 获得视频消息builder.
+ *
+ * @return 视频消息builder
*/
public static VideoBuilder VIDEO() {
return new VideoBuilder();
@@ -73,6 +81,8 @@ public static VideoBuilder VIDEO() {
/**
* 获得音乐消息builder.
+ *
+ * @return 音乐消息builder
*/
public static MusicBuilder MUSIC() {
return new MusicBuilder();
@@ -80,6 +90,8 @@ public static MusicBuilder MUSIC() {
/**
* 获得图文消息(点击跳转到外链)builder.
+ *
+ * @return 图文消息builder
*/
public static NewsBuilder NEWS() {
return new NewsBuilder();
@@ -87,6 +99,8 @@ public static NewsBuilder NEWS() {
/**
* 获得图文消息(点击跳转到图文消息页面)builder.
+ *
+ * @return 图文消息builder
*/
public static MpNewsBuilder MPNEWS() {
return new MpNewsBuilder();
@@ -94,6 +108,8 @@ public static MpNewsBuilder MPNEWS() {
/**
* 获得卡券消息builder.
+ *
+ * @return 卡券消息builder
*/
public static WxCardBuilder WXCARD() {
return new WxCardBuilder();
@@ -101,6 +117,8 @@ public static WxCardBuilder WXCARD() {
/**
* 获得菜单消息builder.
+ *
+ * @return 菜单消息builder
*/
public static WxMsgMenuBuilder MSGMENU() {
return new WxMsgMenuBuilder();
@@ -108,20 +126,25 @@ public static WxMsgMenuBuilder MSGMENU() {
/**
* 小程序卡片.
+ *
+ * @return 小程序卡片builder
*/
public static MiniProgramPageBuilder MINIPROGRAMPAGE() {
return new MiniProgramPageBuilder();
}
/**
- * 发送图文消息(点击跳转到图文消息页面)使用通过 “发布” 系列接口得到的 article_id(草稿箱功能上线后不再支持客服接口中带 media_id 的 mpnews 类型的图文消息)
+ * 发送图文消息(点击跳转到图文消息页面)使用通过 “发布” 系列接口得到的 article_id
+ *
+ * @return 图文消息builder
*/
public static MpNewsArticleBuilder MPNEWSARTICLE() {
return new MpNewsArticleBuilder();
}
/**
- *
+ * 设置消息类型
+ *
* 请使用
* {@link me.chanjar.weixin.common.api.WxConsts.KefuMsgType#TEXT}
* {@link me.chanjar.weixin.common.api.WxConsts.KefuMsgType#IMAGE}
@@ -135,7 +158,9 @@ public static MpNewsArticleBuilder MPNEWSARTICLE() {
* {@link me.chanjar.weixin.common.api.WxConsts.KefuMsgType#TASKCARD}
* {@link me.chanjar.weixin.common.api.WxConsts.KefuMsgType#MSGMENU}
* {@link me.chanjar.weixin.common.api.WxConsts.KefuMsgType#MP_NEWS_ARTICLE}
- *
+ *
+ *
+ * @param msgType 消息类型
*/
public void setMsgType(String msgType) {
this.msgType = msgType;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlMessage.java
index 3d5f4ac3a0..dfc88ab13b 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlMessage.java
@@ -927,6 +927,7 @@ public static WxMpXmlMessage fromXml(InputStream is) {
* @param timestamp 时间戳
* @param nonce 随机串
* @param msgSignature 签名串
+ * @return 解密后的消息对象
*/
public static WxMpXmlMessage fromEncryptedXml(String encryptedXml, WxMpConfigStorage wxMpConfigStorage,
String timestamp, String nonce, String msgSignature) {
@@ -956,14 +957,16 @@ public WxMpXmlMessage decryptField(WxMpConfigStorage wxMpConfigStorage,
/**
*
* 当接受用户消息时,可能会获得以下值:
- * {@link WxConsts.XmlMsgType#TEXT}
- * {@link WxConsts.XmlMsgType#IMAGE}
- * {@link WxConsts.XmlMsgType#VOICE}
- * {@link WxConsts.XmlMsgType#VIDEO}
- * {@link WxConsts.XmlMsgType#LOCATION}
- * {@link WxConsts.XmlMsgType#LINK}
- * {@link WxConsts.XmlMsgType#EVENT}
+ * {@link me.chanjar.weixin.common.api.WxConsts.XmlMsgType#TEXT}
+ * {@link me.chanjar.weixin.common.api.WxConsts.XmlMsgType#IMAGE}
+ * {@link me.chanjar.weixin.common.api.WxConsts.XmlMsgType#VOICE}
+ * {@link me.chanjar.weixin.common.api.WxConsts.XmlMsgType#VIDEO}
+ * {@link me.chanjar.weixin.common.api.WxConsts.XmlMsgType#LOCATION}
+ * {@link me.chanjar.weixin.common.api.WxConsts.XmlMsgType#LINK}
+ * {@link me.chanjar.weixin.common.api.WxConsts.XmlMsgType#EVENT}
*
+ *
+ * @return 消息类型
*/
public String getMsgType() {
return this.msgType;
@@ -972,13 +975,15 @@ public String getMsgType() {
/**
*
* 当发送消息的时候使用:
- * {@link WxConsts.XmlMsgType#TEXT}
- * {@link WxConsts.XmlMsgType#IMAGE}
- * {@link WxConsts.XmlMsgType#VOICE}
- * {@link WxConsts.XmlMsgType#VIDEO}
- * {@link WxConsts.XmlMsgType#NEWS}
- * {@link WxConsts.XmlMsgType#MUSIC}
+ * {@link me.chanjar.weixin.common.api.WxConsts.XmlMsgType#TEXT}
+ * {@link me.chanjar.weixin.common.api.WxConsts.XmlMsgType#IMAGE}
+ * {@link me.chanjar.weixin.common.api.WxConsts.XmlMsgType#VOICE}
+ * {@link me.chanjar.weixin.common.api.WxConsts.XmlMsgType#VIDEO}
+ * {@link me.chanjar.weixin.common.api.WxConsts.XmlMsgType#NEWS}
+ * {@link me.chanjar.weixin.common.api.WxConsts.XmlMsgType#MUSIC}
*
+ *
+ * @param msgType 消息类型
*/
public void setMsgType(String msgType) {
this.msgType = msgType;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutMessage.java
index a44aea740c..ace5b46c54 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutMessage.java
@@ -70,6 +70,8 @@ public abstract class WxMpXmlOutMessage implements Serializable {
/**
* 获得文本消息builder
+ *
+ * @return 文本消息构建器
*/
public static TextBuilder TEXT() {
return new TextBuilder();
@@ -77,6 +79,8 @@ public static TextBuilder TEXT() {
/**
* 获得图片消息builder
+ *
+ * @return 图片消息构建器
*/
public static ImageBuilder IMAGE() {
return new ImageBuilder();
@@ -84,6 +88,8 @@ public static ImageBuilder IMAGE() {
/**
* 获得语音消息builder
+ *
+ * @return 语音消息构建器
*/
public static VoiceBuilder VOICE() {
return new VoiceBuilder();
@@ -91,6 +97,8 @@ public static VoiceBuilder VOICE() {
/**
* 获得视频消息builder
+ *
+ * @return 视频消息构建器
*/
public static VideoBuilder VIDEO() {
return new VideoBuilder();
@@ -98,6 +106,8 @@ public static VideoBuilder VIDEO() {
/**
* 获得音乐消息builder
+ *
+ * @return 音乐消息构建器
*/
public static MusicBuilder MUSIC() {
return new MusicBuilder();
@@ -105,6 +115,8 @@ public static MusicBuilder MUSIC() {
/**
* 获得图文消息builder
+ *
+ * @return 图文消息构建器
*/
public static NewsBuilder NEWS() {
return new NewsBuilder();
@@ -112,18 +124,36 @@ public static NewsBuilder NEWS() {
/**
* 获得客服消息builder
+ *
+ * @return 客服消息构建器
*/
public static TransferCustomerServiceBuilder TRANSFER_CUSTOMER_SERVICE() {
return new TransferCustomerServiceBuilder();
}
+ /**
+ * 获得转接AI回复消息builder
+ *
+ * @return 转接AI回复消息构建器
+ */
+ public static TransferBizAiIvrBuilder TRANSFER_BIZ_AI_IVR() {
+ return new TransferBizAiIvrBuilder();
+ }
+
/**
* 获得设备消息builder
+ *
+ * @return 设备消息builder
*/
public static DeviceBuilder DEVICE() {
return new DeviceBuilder();
}
+ /**
+ * 转换成xml格式
+ *
+ * @return xml格式字符串
+ */
@SuppressWarnings("unchecked")
public String toXml() {
return XStreamTransformer.toXml((Class) this.getClass(), this);
@@ -131,6 +161,9 @@ public String toXml() {
/**
* 转换成加密的结果
+ *
+ * @param wxMpConfigStorage 公众号配置
+ * @return 加密后的消息对象
*/
public WxMpXmlOutMessage toEncrypted(WxMpConfigStorage wxMpConfigStorage) {
String plainXml = toXml();
@@ -146,6 +179,9 @@ public WxMpXmlOutMessage toEncrypted(WxMpConfigStorage wxMpConfigStorage) {
/**
* 转换成加密的xml格式
+ *
+ * @param wxMpConfigStorage 公众号配置
+ * @return 加密后的xml格式字符串
*/
public String toEncryptedXml(WxMpConfigStorage wxMpConfigStorage) {
String plainXml = toXml();
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTransferBizAiIvrMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTransferBizAiIvrMessage.java
new file mode 100644
index 0000000000..504d45869a
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTransferBizAiIvrMessage.java
@@ -0,0 +1,29 @@
+package me.chanjar.weixin.mp.bean.message;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import me.chanjar.weixin.common.api.WxConsts;
+
+/**
+ * 转接AI回复消息.
+ *
+ * 当用户发送消息给公众号时,公众号开发者服务器回复如下内容,会触发微信公众平台的AI回复。
+ * 注意:需要公众号在微信公众平台上已开启AI回复功能,并且AI已学习完毕历史发表文章。
+ * 官方文档:https://developers.weixin.qq.com/doc/subscription/guide/product/message/Passive_user_reply_message.html
+ *
+ *
+ * @author copilot
+ */
+@Data
+@XStreamAlias("xml")
+@JacksonXmlRootElement(localName = "xml")
+@EqualsAndHashCode(callSuper = true)
+public class WxMpXmlOutTransferBizAiIvrMessage extends WxMpXmlOutMessage {
+ private static final long serialVersionUID = 8275281170988017563L;
+
+ public WxMpXmlOutTransferBizAiIvrMessage() {
+ this.msgType = WxConsts.XmlMsgType.TRANSFER_BIZ_AI_IVR;
+ }
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMassGetResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMassGetResult.java
index fe8f6e4043..58f2ea2693 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMassGetResult.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMassGetResult.java
@@ -9,7 +9,8 @@
/**
*
* 查询群发消息发送状态【订阅号与服务号认证后均可用】
- * https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Batch_Sends_and_Originality_Checks.html#%E6%9F%A5%E8%AF%A2%E7%BE%A4%E5%8F%91%E6%B6%88%E6%81%AF%E5%8F%91%E9%80%81%E7%8A%B6%E6%80%81%E3%80%90%E8%AE%A2%E9%98%85%E5%8F%B7%E4%B8%8E%E6%9C%8D%E5%8A%A1%E5%8F%B7%E8%AE%A4%E8%AF%81%E5%90%8E%E5%9D%87%E5%8F%AF%E7%94%A8%E3%80%91
+ * 文档地址:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Batch_Sends_and_Originality_Checks.html
+ *
* @author S
*/
@Data
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/outxml/TransferBizAiIvrBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/outxml/TransferBizAiIvrBuilder.java
new file mode 100644
index 0000000000..6e5a74d477
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/outxml/TransferBizAiIvrBuilder.java
@@ -0,0 +1,22 @@
+package me.chanjar.weixin.mp.builder.outxml;
+
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutTransferBizAiIvrMessage;
+
+/**
+ * 转接AI回复消息builder.
+ *
+ * 用法: WxMpXmlOutTransferBizAiIvrMessage m = WxMpXmlOutMessage.TRANSFER_BIZ_AI_IVR().toUser("").fromUser("").build();
+ *
+ *
+ * @author copilot
+ */
+public final class TransferBizAiIvrBuilder
+ extends BaseBuilder {
+
+ @Override
+ public WxMpXmlOutTransferBizAiIvrMessage build() {
+ WxMpXmlOutTransferBizAiIvrMessage m = new WxMpXmlOutTransferBizAiIvrMessage();
+ setCommon(m);
+ return m;
+ }
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/config/WxMpConfigStorage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/config/WxMpConfigStorage.java
index 11aeef6124..1bebe86885 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/config/WxMpConfigStorage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/config/WxMpConfigStorage.java
@@ -24,7 +24,7 @@ public interface WxMpConfigStorage {
* Is use stable access token api
*
* @return the boolean
- * @link https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/getStableAccessToken.html
+ * @see 文档
*/
boolean isStableAccessToken();
@@ -211,6 +211,8 @@ public interface WxMpConfigStorage {
*
* {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setRetrySleepMillis(int)}
*
+ *
+ * @return 重试间隔毫秒数
*/
int getRetrySleepMillis();
@@ -219,6 +221,8 @@ public interface WxMpConfigStorage {
*
* {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setMaxRetryTimes(int)}
*
+ *
+ * @return 最大重试次数
*/
int getMaxRetryTimes();
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/config/impl/WxMpRedisConfigImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/config/impl/WxMpRedisConfigImpl.java
index 870fa1e276..7939d57a18 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/config/impl/WxMpRedisConfigImpl.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/config/impl/WxMpRedisConfigImpl.java
@@ -39,6 +39,8 @@ public WxMpRedisConfigImpl(WxRedisOps redisOps, String keyPrefix) {
/**
* 每个公众号生成独有的存储key.
+ *
+ * @param appId 公众号appId
*/
@Override
public void setAppId(String appId) {
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/config/impl/WxMpRedissonConfigImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/config/impl/WxMpRedissonConfigImpl.java
index e0d9e92af1..4982336f8a 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/config/impl/WxMpRedissonConfigImpl.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/config/impl/WxMpRedissonConfigImpl.java
@@ -42,6 +42,8 @@ private WxMpRedissonConfigImpl(@NonNull WxRedisOps redisOps, String keyPrefix) {
/**
* 每个公众号生成独有的存储key.
+ *
+ * @param appId 公众号appId
*/
@Override
public void setAppId(String appId) {
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/constant/WxMpEventConstants.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/constant/WxMpEventConstants.java
index b2e984b0f9..4dfee6295e 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/constant/WxMpEventConstants.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/constant/WxMpEventConstants.java
@@ -20,7 +20,7 @@ public class WxMpEventConstants {
public static final String SUBMIT_MEMBERCARD_USER_INFO = "submit_membercard_user_info";
/**
- * 微信摇一摇周边>>摇一摇事件通知.
+ * 微信摇一摇周边-摇一摇事件通知.
*/
public static final String SHAKEAROUND_USER_SHAKE = "ShakearoundUserShake";
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/crypto/WxMpCryptUtil.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/crypto/WxMpCryptUtil.java
index 99d759f32f..7757ad78bf 100755
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/crypto/WxMpCryptUtil.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/crypto/WxMpCryptUtil.java
@@ -27,7 +27,7 @@ public class WxMpCryptUtil extends me.chanjar.weixin.common.util.crypto.WxCryptU
/**
* 构造函数
*
- * @param wxMpConfigStorage
+ * @param wxMpConfigStorage 公众号配置存储
*/
public WxMpCryptUtil(WxMpConfigStorage wxMpConfigStorage) {
/*
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/xml/XStreamTransformer.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/xml/XStreamTransformer.java
index ace711a236..527fc722d5 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/xml/XStreamTransformer.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/xml/XStreamTransformer.java
@@ -15,6 +15,7 @@
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutNewsMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutTextMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutTransferKefuMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutTransferBizAiIvrMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutVideoMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutVoiceMessage;
@@ -30,10 +31,16 @@ public class XStreamTransformer {
registerClass(WxMpXmlOutVideoMessage.class);
registerClass(WxMpXmlOutVoiceMessage.class);
registerClass(WxMpXmlOutTransferKefuMessage.class);
+ registerClass(WxMpXmlOutTransferBizAiIvrMessage.class);
}
/**
- * xml -> pojo.
+ * {@code xml -> pojo.}
+ *
+ * @param 返回类型
+ * @param clazz 类型
+ * @param xml xml字符串
+ * @return 转换后的对象
*/
@SuppressWarnings("unchecked")
public static T fromXml(Class clazz, String xml) {
@@ -41,6 +48,14 @@ public static T fromXml(Class clazz, String xml) {
return object;
}
+ /**
+ * {@code xml -> pojo.}
+ *
+ * @param 返回类型
+ * @param clazz 类型
+ * @param is 输入流
+ * @return 转换后的对象
+ */
@SuppressWarnings("unchecked")
public static T fromXml(Class clazz, InputStream is) {
T object = (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(is);
@@ -48,7 +63,12 @@ public static T fromXml(Class clazz, InputStream is) {
}
/**
- * pojo -> xml.
+ * {@code pojo -> xml.}
+ *
+ * @param 类型参数
+ * @param clazz 类型
+ * @param object 对象
+ * @return xml字符串
*/
public static String toXml(Class clazz, T object) {
return CLASS_2_XSTREAM_INSTANCE.get(clazz).toXML(object);
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpCommentServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpCommentServiceImplTest.java
index 0109f676ae..060dee10f8 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpCommentServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpCommentServiceImplTest.java
@@ -10,7 +10,7 @@
import org.testng.annotations.Test;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Matchers.anyString;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpOcrServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpOcrServiceImplTest.java
index 2cc8b80119..7e9d477872 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpOcrServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpOcrServiceImplTest.java
@@ -24,7 +24,7 @@
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Matchers.anyString;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpWifiServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpWifiServiceImplTest.java
index d9225c7bc5..a8f79603fc 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpWifiServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpWifiServiceImplTest.java
@@ -11,8 +11,8 @@
import static me.chanjar.weixin.mp.enums.WxMpApiUrl.Wifi.BIZWIFI_SHOP_GET;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTransferBizAiIvrMessageTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTransferBizAiIvrMessageTest.java
new file mode 100644
index 0000000000..0ea0d3f6db
--- /dev/null
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTransferBizAiIvrMessageTest.java
@@ -0,0 +1,44 @@
+package me.chanjar.weixin.mp.bean.message;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * 转接AI回复消息测试.
+ *
+ * @author copilot
+ */
+public class WxMpXmlOutTransferBizAiIvrMessageTest {
+
+ @Test
+ public void test() {
+ WxMpXmlOutTransferBizAiIvrMessage m = new WxMpXmlOutTransferBizAiIvrMessage();
+ m.setCreateTime(1399197672L);
+ m.setFromUserName("fromuser");
+ m.setToUserName("touser");
+
+ String expected = "" +
+ " " +
+ " " +
+ "1399197672 " +
+ " " +
+ " ";
+ System.out.println(m.toXml());
+ Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", ""));
+ }
+
+ @Test
+ public void testBuild() {
+ WxMpXmlOutTransferBizAiIvrMessage m = WxMpXmlOutMessage.TRANSFER_BIZ_AI_IVR()
+ .fromUser("fromuser").toUser("touser").build();
+ m.setCreateTime(1399197672L);
+ String expected = "" +
+ " " +
+ " " +
+ "1399197672 " +
+ " " +
+ " ";
+ System.out.println(m.toXml());
+ Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", ""));
+ }
+}
diff --git a/weixin-java-open/AUDIT_QUOTA_MANAGEMENT_GUIDE.md b/weixin-java-open/AUDIT_QUOTA_MANAGEMENT_GUIDE.md
new file mode 100644
index 0000000000..d84932034a
--- /dev/null
+++ b/weixin-java-open/AUDIT_QUOTA_MANAGEMENT_GUIDE.md
@@ -0,0 +1,321 @@
+# 微信开放平台小程序审核额度管理最佳实践
+
+## 问题背景
+
+在使用微信开放平台第三方服务为多个授权小程序提交审核时,需要注意以下重要限制:
+
+### 审核额度限制
+
+- **默认额度**: 每个第三方平台账号每月默认有 **20 个** 审核额度
+- **消耗规则**: 每次调用 `submitAudit()` 提交一个小程序审核,会消耗 **1 个** 审核额度
+- **重置周期**: 额度每月初自动重置
+- **不可返还**: 审核撤回(undoCodeAudit)不会返还已消耗的额度
+- **增加额度**: 如需更多额度,需要联系微信开放平台客服申请
+
+### 常见问题
+
+**问题**: 开放平台是每个 appId 都要这样提交审核吗?额度不够吧?
+
+**回答**: 是的,每个授权的小程序(appId)都需要单独调用 `submitAudit()` 提交审核,每次提交会消耗 1 个审核额度。默认的 20 个额度对于管理大量小程序的第三方平台来说可能不够用,建议:
+1. 提交审核前先查询剩余额度
+2. 合理规划审核计划,避免重复提交审核
+3. 联系微信开放平台申请增加额度
+
+## API 使用说明
+
+### 1. 查询审核额度
+
+```java
+// 查询当前审核额度
+WxOpenMaQueryQuotaResult quota = wxOpenMaService.queryQuota();
+
+System.out.println("当月剩余提交审核次数: " + quota.getRest()); // 剩余额度
+System.out.println("当月提交审核额度上限: " + quota.getLimit()); // 总额度
+System.out.println("剩余加急次数: " + quota.getSpeedupRest()); // 剩余加急次数
+System.out.println("加急额度上限: " + quota.getSpeedupLimit()); // 加急额度上限
+```
+
+**返回字段说明**:
+- `rest`: 当月剩余提交审核次数
+- `limit`: 当月提交审核额度上限(默认 20)
+- `speedupRest`: 剩余加急次数
+- `speedupLimit`: 加急额度上限
+
+### 2. 提交审核
+
+```java
+// 构建审核项
+WxMaCodeSubmitAuditItem item = new WxMaCodeSubmitAuditItem();
+item.setAddress("index"); // 页面路径
+item.setTag("工具"); // 标签
+item.setFirstClass("工具"); // 一级类目
+item.setSecondClass("效率"); // 二级类目
+item.setTitle("首页"); // 页面标题
+
+// 构建提交审核消息
+WxOpenMaSubmitAuditMessage message = new WxOpenMaSubmitAuditMessage();
+message.setItemList(Collections.singletonList(item));
+message.setVersionDesc("版本描述");
+
+// 提交审核
+WxOpenMaSubmitAuditResult result = wxOpenMaService.submitAudit(message);
+System.out.println("审核ID: " + result.getAuditId());
+```
+
+## 最佳实践
+
+### 方案一:单个小程序提交前检查额度
+
+这是最基本的做法,适用于偶尔提交审核的场景。
+
+```java
+import me.chanjar.weixin.open.api.WxOpenMaService;
+import me.chanjar.weixin.open.bean.message.WxOpenMaSubmitAuditMessage;
+import me.chanjar.weixin.open.bean.result.WxOpenMaQueryQuotaResult;
+import me.chanjar.weixin.open.bean.result.WxOpenMaSubmitAuditResult;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+public class AuditSubmitter {
+
+ /**
+ * 提交审核前检查额度
+ */
+ public WxOpenMaSubmitAuditResult submitWithQuotaCheck(
+ WxOpenMaService wxOpenMaService,
+ WxOpenMaSubmitAuditMessage message) throws WxErrorException {
+
+ // 1. 检查审核额度
+ WxOpenMaQueryQuotaResult quota = wxOpenMaService.queryQuota();
+ System.out.println("当前剩余审核额度: " + quota.getRest());
+
+ if (quota.getRest() <= 0) {
+ throw new RuntimeException("审核额度不足,无法提交审核。剩余额度: " + quota.getRest());
+ }
+
+ // 2. 提交审核
+ WxOpenMaSubmitAuditResult result = wxOpenMaService.submitAudit(message);
+ System.out.println("提交审核成功,审核ID: " + result.getAuditId());
+
+ // 3. 再次查询额度(可选)
+ quota = wxOpenMaService.queryQuota();
+ System.out.println("提交后剩余审核额度: " + quota.getRest());
+
+ return result;
+ }
+}
+```
+
+### 方案二:批量提交审核的额度管理
+
+适用于需要同时为多个小程序提交审核的场景。
+
+```java
+import me.chanjar.weixin.open.api.WxOpenComponentService;
+import me.chanjar.weixin.open.api.WxOpenMaService;
+import me.chanjar.weixin.open.bean.message.WxOpenMaSubmitAuditMessage;
+import me.chanjar.weixin.open.bean.result.WxOpenMaQueryQuotaResult;
+import me.chanjar.weixin.open.bean.result.WxOpenMaSubmitAuditResult;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class BatchAuditSubmitter {
+
+ /**
+ * 批量提交审核结果
+ */
+ public static class BatchSubmitResult {
+ private int successCount; // 成功数量
+ private int failCount; // 失败数量
+ private int skipCount; // 跳过数量(额度不足)
+ private List failedAppIds; // 失败的 appId
+
+ // getters and setters...
+ }
+
+ /**
+ * 批量提交审核,带额度检查
+ */
+ public BatchSubmitResult batchSubmitWithQuotaCheck(
+ WxOpenComponentService wxOpenComponentService,
+ Map appIdToMessageMap) {
+
+ BatchSubmitResult result = new BatchSubmitResult();
+ result.setFailedAppIds(new ArrayList<>());
+
+ // 基本参数校验:避免空指针和空集合导致的 NoSuchElementException
+ if (appIdToMessageMap == null || appIdToMessageMap.isEmpty()) {
+ System.err.println("错误:待提交的小程序列表为空,未执行任何审核提交");
+ return result;
+ }
+
+ try {
+ // 1. 检查总体额度是否充足
+ // 使用任意一个已授权的小程序 appId 获取 WxMaService 来查询审核额度。
+ // 注意:审核额度是以第三方平台维度统计的,因此这里选择任意一个 appId 即可。
+ WxOpenMaQueryQuotaResult quota = wxOpenComponentService
+ .getWxMaServiceByAppid(appIdToMessageMap.keySet().iterator().next())
+ .queryQuota();
+
+ System.out.println("=== 批量提交审核开始 ===");
+ System.out.println("待提交数量: " + appIdToMessageMap.size());
+ System.out.println("当前剩余审核额度: " + quota.getRest());
+
+ if (quota.getRest() < appIdToMessageMap.size()) {
+ System.err.println("警告:审核额度不足!");
+ System.err.println(" 需要提交: " + appIdToMessageMap.size() + " 个");
+ System.err.println(" 剩余额度: " + quota.getRest());
+ System.err.println(" 缺少额度: " + (appIdToMessageMap.size() - quota.getRest()));
+ System.err.println("将仅提交前 " + quota.getRest() + " 个小程序");
+ }
+
+ // 2. 依次提交审核
+ int count = 0;
+ for (Map.Entry entry : appIdToMessageMap.entrySet()) {
+ String appId = entry.getKey();
+ WxOpenMaSubmitAuditMessage message = entry.getValue();
+
+ // 检查是否还有额度
+ if (count >= quota.getRest()) {
+ System.out.println("AppId: " + appId + " 跳过(额度不足)");
+ result.setSkipCount(result.getSkipCount() + 1);
+ continue;
+ }
+
+ try {
+ WxOpenMaService maService = wxOpenComponentService.getWxMaServiceByAppid(appId);
+ WxOpenMaSubmitAuditResult submitResult = maService.submitAudit(message);
+
+ System.out.println("AppId: " + appId + " 提交成功,审核ID: " + submitResult.getAuditId());
+ result.setSuccessCount(result.getSuccessCount() + 1);
+ count++;
+
+ } catch (WxErrorException e) {
+ System.err.println("AppId: " + appId + " 提交失败: " + e.getMessage());
+ result.setFailCount(result.getFailCount() + 1);
+ result.getFailedAppIds().add(appId);
+ count++;
+ }
+ }
+
+ // 3. 输出统计信息
+ System.out.println("=== 批量提交审核完成 ===");
+ System.out.println("成功: " + result.getSuccessCount());
+ System.out.println("失败: " + result.getFailCount());
+ System.out.println("跳过: " + result.getSkipCount());
+
+ // 4. 查询剩余额度
+ quota = wxOpenComponentService
+ .getWxMaServiceByAppid(appIdToMessageMap.keySet().iterator().next())
+ .queryQuota();
+ System.out.println("剩余额度: " + quota.getRest());
+
+ } catch (WxErrorException e) {
+ System.err.println("批量提交审核失败: " + e.getMessage());
+ e.printStackTrace();
+ }
+
+ return result;
+ }
+}
+```
+
+### 方案三:审核额度监控和告警
+
+建议实现一个审核额度监控机制,及时发现额度不足的情况。
+
+```java
+import me.chanjar.weixin.open.api.WxOpenMaService;
+import me.chanjar.weixin.open.bean.result.WxOpenMaQueryQuotaResult;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+public class QuotaMonitor {
+
+ /**
+ * 检查审核额度并发出告警
+ */
+ public void checkAndAlert(WxOpenMaService wxOpenMaService) {
+ try {
+ WxOpenMaQueryQuotaResult quota = wxOpenMaService.queryQuota();
+
+ int rest = quota.getRest();
+ int limit = quota.getLimit();
+ double percentage = (double) rest / limit * 100;
+
+ System.out.println("审核额度状态:");
+ System.out.println(" 剩余: " + rest + " / " + limit);
+ System.out.println(" 使用率: " + String.format("%.1f", 100 - percentage) + "%");
+
+ // 根据剩余额度发出不同级别的告警
+ if (rest <= 0) {
+ sendCriticalAlert("审核额度已用尽!无法提交新的审核。");
+ } else if (rest <= 3) {
+ sendWarningAlert("审核额度严重不足!剩余额度: " + rest);
+ } else if (percentage < 30) {
+ sendInfoAlert("审核额度偏低,剩余: " + rest + " (" + String.format("%.1f", percentage) + "%)");
+ }
+
+ } catch (WxErrorException e) {
+ System.err.println("查询审核额度失败: " + e.getMessage());
+ }
+ }
+
+ private void sendCriticalAlert(String message) {
+ // 发送紧急告警(如:发送邮件、短信、钉钉消息等)
+ System.err.println("[严重] " + message);
+ }
+
+ private void sendWarningAlert(String message) {
+ // 发送警告(如:发送邮件、企业微信消息等)
+ System.out.println("[警告] " + message);
+ }
+
+ private void sendInfoAlert(String message) {
+ // 发送普通提示
+ System.out.println("[提示] " + message);
+ }
+}
+```
+
+## 常见问题 FAQ
+
+### Q1: 审核额度什么时候重置?
+A: 审核额度在每月初自动重置为默认值(通常是 20 个)。
+
+### Q2: 审核撤回会返还额度吗?
+A: 不会。调用 `undoCodeAudit()` 撤回审核不会返还已消耗的额度。
+
+### Q3: 如何增加审核额度?
+A: 需要联系微信开放平台客服申请增加额度。具体联系方式请参考微信开放平台官方文档。
+
+### Q4: 审核失败会消耗额度吗?
+A: 会。只要调用了 `submitAudit()` 接口提交审核,无论审核是否通过,都会消耗 1 个额度。
+
+### Q5: 加急审核会额外消耗额度吗?
+A: 加急审核(`speedAudit()`)使用的是单独的加急额度(`speedupRest`),不会消耗普通审核额度。但加急审核的前提是已经提交了审核,所以提交审核时仍会消耗 1 个普通审核额度。
+
+### Q6: 多个小程序共享审核额度吗?
+A: 是的。同一个第三方平台账号下,所有授权的小程序共享审核额度。每提交一个小程序审核,都会消耗该第三方平台的 1 个审核额度。
+
+### Q7: 如何避免审核额度不足?
+A: 建议采取以下措施:
+- 在批量提交审核前,先调用 `queryQuota()` 检查剩余额度
+- 实现审核额度监控和告警机制
+- 合理规划审核计划,避免不必要的重复提交审核
+- 提高代码质量,减少审核不通过的情况
+- 联系微信开放平台申请增加额度
+
+### Q8: 能否查询历史审核额度使用情况?
+A: 微信开放平台 API 目前只提供当前剩余额度查询,不提供历史使用记录。如需统计历史使用情况,需要自行记录每次调用 `submitAudit()` 的时间和次数。
+
+## 相关文档
+
+- [微信开放平台官方文档 - 查询额度](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Mini_Programs/code/query_quota.html)
+- [微信开放平台官方文档 - 提交审核](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Mini_Programs/code/submit_audit.html)
+- [微信开放平台官方文档 - 加急审核](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Mini_Programs/code/speedup_audit.html)
+
+## 技术支持
+
+如有问题,请提交 Issue 到 [WxJava GitHub 仓库](https://github.com/binarywang/WxJava/issues)。
diff --git a/weixin-java-open/README.md b/weixin-java-open/README.md
index 6ca65dfef3..ad7f3d0109 100644
--- a/weixin-java-open/README.md
+++ b/weixin-java-open/README.md
@@ -31,6 +31,36 @@
---
+## 重要提示:小程序审核额度限制
+
+**在使用第三方平台代小程序提交审核时,请注意以下限制:**
+
+### 审核额度说明
+
+- **默认额度**: 每个第三方平台账号每月默认有 **20 个** 审核额度
+- **消耗规则**: 每次调用 `submitAudit()` 提交一个小程序审核,会消耗 **1 个** 审核额度
+- **重置周期**: 额度每月初自动重置
+- **额度查询**: 使用 `queryQuota()` 方法查询剩余额度
+
+### 最佳实践
+
+```java
+// 1. 先查询剩余额度
+WxOpenMaQueryQuotaResult quota = wxOpenMaService.queryQuota();
+if (quota.getRest() <= 0) {
+ throw new RuntimeException("审核额度不足,剩余:" + quota.getRest());
+}
+
+// 2. 提交审核
+WxOpenMaSubmitAuditMessage message = new WxOpenMaSubmitAuditMessage();
+message.setItemList(itemList);
+WxOpenMaSubmitAuditResult result = wxOpenMaService.submitAudit(message);
+```
+
+**详细说明**: 请参考 [AUDIT_QUOTA_MANAGEMENT_GUIDE.md](AUDIT_QUOTA_MANAGEMENT_GUIDE.md)
+
+---
+
## 代码示例
消息机制未实现,下面为通知回调中设置的代码部分
diff --git a/weixin-java-open/pom.xml b/weixin-java-open/pom.xml
index f354cb16f8..6fa96d8aea 100644
--- a/weixin-java-open/pom.xml
+++ b/weixin-java-open/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.7.9.B
+ 4.8.2.B
weixin-java-open
@@ -48,6 +48,16 @@
okhttp
provided
@@ -539,10 +554,36 @@ WxOpenMaDomainResult modifyDomainDirectly(String action, List
+ * 重要提示:审核额度限制
+ *
+ * 最佳实践:
+ *
+ * 文档地址:
+ * 查询额度
+ *
+ * 返回字段说明:
+ *
+ * 重要说明:
+ *
+ * 使用示例:
+ *
+ * 重要提示:审核额度限制
+ *
+ * 使用示例:
+ *
+ * 用于查询第三方平台服务商的当月提交审核限额和加急次数
+ *
+ * 字段说明:
+ *
+ * 重要提示:
+ *
+ * 这是一个完整的示例,展示如何在提交审核前检查额度,避免额度不足导致的失败
+ *
+ * 当需要为多个小程序提交审核时,应该先统一检查额度是否充足
+ *
+ *
+ * {@code
+ * // 1. 先查询剩余额度
+ * WxOpenMaQueryQuotaResult quota = wxOpenMaService.queryQuota();
+ * if (quota.getRest() <= 0) {
+ * throw new RuntimeException("审核额度不足,剩余:" + quota.getRest());
+ * }
+ *
+ * // 2. 提交审核
+ * WxOpenMaSubmitAuditMessage message = new WxOpenMaSubmitAuditMessage();
+ * message.setItemList(itemList);
+ * WxOpenMaSubmitAuditResult result = wxOpenMaService.submitAudit(message);
+ * }
*
* @param submitAuditMessage the submit audit message
* @return the wx open ma submit audit result
* @throws WxErrorException the wx error exception
+ * @see #queryQuota() 查询审核额度
+ * @see #speedAudit(Long) 加急审核
*/
WxOpenMaSubmitAuditResult submitAudit(WxOpenMaSubmitAuditMessage submitAuditMessage) throws WxErrorException;
@@ -690,11 +731,43 @@ WxOpenMaDomainResult modifyDomainDirectly(String action, List
+ *
+ *
+ *
+ * {@code
+ * WxOpenMaQueryQuotaResult quota = wxOpenMaService.queryQuota();
+ * System.out.println("剩余审核次数:" + quota.getRest());
+ * System.out.println("审核额度上限:" + quota.getLimit());
+ * System.out.println("剩余加急次数:" + quota.getSpeedupRest());
+ * }
+ *
+ * @return 审核额度信息
+ * @throws WxErrorException 调用微信接口失败时抛出
+ * @see #submitAudit(WxOpenMaSubmitAuditMessage) 提交审核
+ * @see #speedAudit(Long) 加急审核
*/
WxOpenMaQueryQuotaResult queryQuota() throws WxErrorException;
diff --git a/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenMaServiceImpl.java b/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenMaServiceImpl.java
index da9f910eb2..6f95aa2d90 100644
--- a/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenMaServiceImpl.java
+++ b/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenMaServiceImpl.java
@@ -274,6 +274,14 @@ public WxOpenResult verifyBetaWeapp(WxOpenMaVerifyBetaWeappMessage verifyBetaWea
return WxMaGsonBuilder.create().fromJson(response, WxOpenResult.class);
}
+ @Override
+ public WxOpenResult setBetaWeappNickName(String name) throws WxErrorException {
+ JsonObject params = new JsonObject();
+ params.addProperty("name", name);
+ String response = post(API_SET_BETA_WEAPP_NICKNAME, GSON.toJson(params));
+ return WxMaGsonBuilder.create().fromJson(response, WxOpenResult.class);
+ }
+
@Override
public WxOpenMaCategoryListResult getCategoryList() throws WxErrorException {
String response = get(API_GET_CATEGORY, null);
diff --git a/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenMessageRouter.java b/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenMessageRouter.java
index 7314bfd694..3c2c3058f4 100644
--- a/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenMessageRouter.java
+++ b/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenMessageRouter.java
@@ -23,4 +23,29 @@ public WxMpXmlOutMessage route(final WxMpXmlMessage wxMessage, String appId) {
public WxMpXmlOutMessage route(final WxMpXmlMessage wxMessage, final Map
+ *
+ * {@code
+ * // 1. 构建审核项
+ * WxMaCodeSubmitAuditItem item = new WxMaCodeSubmitAuditItem();
+ * item.setAddress("index");
+ * item.setTag("游戏");
+ * item.setFirstClass("游戏");
+ * item.setSecondClass("休闲游戏");
+ * item.setTitle("首页");
+ *
+ * // 2. 构建提交审核消息
+ * WxOpenMaSubmitAuditMessage message = new WxOpenMaSubmitAuditMessage();
+ * message.setItemList(Collections.singletonList(item));
+ * message.setVersionDesc("版本描述");
+ *
+ * // 3. 提交审核
+ * WxOpenMaSubmitAuditResult result = wxOpenMaService.submitAudit(message);
+ * System.out.println("审核ID: " + result.getAuditId());
+ * }
*
* @author yqx
+ * @see me.chanjar.weixin.open.api.WxOpenMaService#submitAudit(WxOpenMaSubmitAuditMessage)
+ * @see me.chanjar.weixin.open.api.WxOpenMaService#queryQuota()
* created on 2018/9/13
*/
@Data
diff --git a/weixin-java-open/src/main/java/me/chanjar/weixin/open/bean/result/WxOpenMaQueryQuotaResult.java b/weixin-java-open/src/main/java/me/chanjar/weixin/open/bean/result/WxOpenMaQueryQuotaResult.java
index 3b02906242..0f964b5f44 100644
--- a/weixin-java-open/src/main/java/me/chanjar/weixin/open/bean/result/WxOpenMaQueryQuotaResult.java
+++ b/weixin-java-open/src/main/java/me/chanjar/weixin/open/bean/result/WxOpenMaQueryQuotaResult.java
@@ -6,7 +6,31 @@
import me.chanjar.weixin.open.util.json.WxOpenGsonBuilder;
/**
- * 微信开放平台小程序当前分阶段发布详情
+ * 微信开放平台小程序提交审核额度查询结果
+ *
+ *
+ *
+ *
+ *
+ * @see me.chanjar.weixin.open.api.WxOpenMaService#queryQuota()
+ * @see me.chanjar.weixin.open.api.WxOpenMaService#submitAudit(me.chanjar.weixin.open.bean.message.WxOpenMaSubmitAuditMessage)
*/
@Data
@EqualsAndHashCode(callSuper = true)
@@ -14,15 +38,27 @@ public class WxOpenMaQueryQuotaResult extends WxOpenResult {
private static final long serialVersionUID = 5915265985261653007L;
+ /**
+ * 当月剩余提交审核次数
+ */
@SerializedName("rest")
private Integer rest;
+ /**
+ * 当月提交审核额度上限
+ */
@SerializedName("limit")
private Integer limit;
+ /**
+ * 剩余加急次数
+ */
@SerializedName("speedup_rest")
private Integer speedupRest;
+ /**
+ * 加急额度上限
+ */
@SerializedName("speedup_limit")
private Integer speedupLimit;
diff --git a/weixin-java-open/src/test/java/me/chanjar/weixin/open/api/impl/WxOpenMaServiceImplTest.java b/weixin-java-open/src/test/java/me/chanjar/weixin/open/api/impl/WxOpenMaServiceImplTest.java
index 4d8e41b59e..1de6ffe2d6 100644
--- a/weixin-java-open/src/test/java/me/chanjar/weixin/open/api/impl/WxOpenMaServiceImplTest.java
+++ b/weixin-java-open/src/test/java/me/chanjar/weixin/open/api/impl/WxOpenMaServiceImplTest.java
@@ -306,6 +306,141 @@ public void testGetGrayReleasePlan() {
@Test
public void testQueryQuota() {
+ // 此测试方法演示如何使用审核额度查询功能
+ // 注意:实际运行需要真实的微信 API 凭据
+ /*
+ try {
+ // 查询当前审核额度
+ WxOpenMaQueryQuotaResult quota = wxOpenMaService.queryQuota();
+
+ System.out.println("审核额度信息:");
+ System.out.println(" 当月剩余提交审核次数: " + quota.getRest());
+ System.out.println(" 当月提交审核额度上限: " + quota.getLimit());
+ System.out.println(" 剩余加急次数: " + quota.getSpeedupRest());
+ System.out.println(" 加急额度上限: " + quota.getSpeedupLimit());
+
+ // 检查额度是否充足
+ if (quota.getRest() <= 0) {
+ System.err.println("警告:审核额度已用尽!");
+ } else if (quota.getRest() <= 5) {
+ System.out.println("提示:审核额度即将用尽,请注意!");
+ }
+ } catch (WxErrorException e) {
+ e.printStackTrace();
+ }
+ */
+ }
+
+ /**
+ * 演示提交审核前检查额度的最佳实践
+ *
+ * 微工卡批量转账API请求参数 + * 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3_partner/Offline/apis/chapter4_1_8.shtml + * + * 适用对象:服务商 + * 请求URL:https://api.mch.weixin.qq.com/v3/payroll-card/transfer-batches + * 请求方式:POST + *+ * + * @author binarywang + * created on 2025/01/19 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayrollTransferBatchesRequest implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+ * 字段名:应用ID + * 变量名:appid + * 是否必填:二选一 + * 类型:string[1, 32] + * 描述: + * 服务商在微信申请公众号/小程序或移动应用成功后分配的账号ID + * 示例值:wxa1111111 + *+ */ + @SerializedName(value = "appid") + private String appid; + + /** + *
+ * 字段名:子商户应用ID + * 变量名:sub_appid + * 是否必填:二选一 + * 类型:string[1, 32] + * 描述: + * 特约商户在微信申请公众号/小程序或移动应用成功后分配的账号ID + * 示例值:wxa1111111 + *+ */ + @SerializedName(value = "sub_appid") + private String subAppid; + + /** + *
+ * 字段名:子商户号 + * 变量名:sub_mchid + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * 微信服务商下特约商户的商户号,由微信支付生成并下发 + * 示例值:1111111 + *+ */ + @SerializedName(value = "sub_mchid") + private String subMchid; + + /** + *
+ * 字段名:商家批次单号 + * 变量名:out_batch_no + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * 商户系统内部的商家批次单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一 + * 示例值:plfk2020042013 + *+ */ + @SerializedName(value = "out_batch_no") + private String outBatchNo; + + /** + *
+ * 字段名:批次名称 + * 变量名:batch_name + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * 该笔批量转账的名称 + * 示例值:2019年1月深圳分部报销单 + *+ */ + @SerializedName(value = "batch_name") + private String batchName; + + /** + *
+ * 字段名:批次备注 + * 变量名:batch_remark + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * 转账说明,UTF8编码,最多允许32个字符 + * 示例值:2019年1月深圳分部报销单 + *+ */ + @SerializedName(value = "batch_remark") + private String batchRemark; + + /** + *
+ * 字段名:转账总金额 + * 变量名:total_amount + * 是否必填:是 + * 类型:int64 + * 描述: + * 转账金额单位为"分"。转账总金额必须与批次内所有明细转账金额之和保持一致,否则无法发起转账操作 + * 示例值:4000000 + *+ */ + @SerializedName(value = "total_amount") + private Long totalAmount; + + /** + *
+ * 字段名:转账总笔数 + * 变量名:total_num + * 是否必填:是 + * 类型:int + * 描述: + * 一个转账批次单最多发起一千笔转账。转账总笔数必须与批次内所有明细之和保持一致,否则无法发起转账操作 + * 示例值:200 + *+ */ + @SerializedName(value = "total_num") + private Integer totalNum; + + /** + *
+ * 字段名:用工类型 + * 变量名:employment_type + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * 微工卡服务仅支持用于与商户有用工关系的用户,需明确用工类型;参考值: + * LONG_TERM_EMPLOYMENT:长期用工, + * SHORT_TERM_EMPLOYMENT:短期用工, + * COOPERATION_EMPLOYMENT:合作关系 + * 示例值:LONG_TERM_EMPLOYMENT + *+ */ + @SerializedName(value = "employment_type") + private String employmentType; + + /** + *
+ * 字段名:用工场景 + * 变量名:employment_scene + * 是否必填:否 + * 类型:string[1, 32] + * 描述: + * 用工场景,参考值: + * LOGISTICS:物流; + * MANUFACTURING:制造业; + * HOTEL:酒店; + * CATERING:餐饮业; + * EVENT:活动促销; + * RETAIL:零售; + * OTHERS:其他 + * 示例值:LOGISTICS + *+ */ + @SerializedName(value = "employment_scene") + private String employmentScene; + + /** + *
+ * 字段名:特约商户授权类型 + * 变量名:authorization_type + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * 特约商户授权类型: + * INFORMATION_AUTHORIZATION_TYPE:特约商户信息授权类型, + * FUND_AUTHORIZATION_TYPE:特约商户资金授权类型, + * INFORMATION_AND_FUND_AUTHORIZATION_TYPE:特约商户信息和资金授权类型 + * 示例值:INFORMATION_AUTHORIZATION_TYPE + *+ */ + @SerializedName(value = "authorization_type") + private String authorizationType; + + /** + *
+ * 字段名:转账明细列表 + * 变量名:transfer_detail_list + * 是否必填:是 + * 类型:array + * 描述: + * 发起批量转账的明细列表,最多一千笔 + *+ */ + @SerializedName(value = "transfer_detail_list") + private List
+ * 字段名:商家明细单号 + * 变量名:out_detail_no + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * 商户系统内部区分转账批次单下不同转账明细单的唯一标识 + * 示例值:x23zy545Bd5436 + *+ */ + @SerializedName(value = "out_detail_no") + private String outDetailNo; + + /** + *
+ * 字段名:转账金额 + * 变量名:transfer_amount + * 是否必填:是 + * 类型:int64 + * 描述: + * 转账金额单位为"分" + * 示例值:200000 + *+ */ + @SerializedName(value = "transfer_amount") + private Long transferAmount; + + /** + *
+ * 字段名:转账备注 + * 变量名:transfer_remark + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * 单条转账备注(微信用户会收到该备注),UTF8编码,最多允许32个字符 + * 示例值:2020年4月报销 + *+ */ + @SerializedName(value = "transfer_remark") + private String transferRemark; + + /** + *
+ * 字段名:用户标识 + * 变量名:openid + * 是否必填:是 + * 类型:string[1, 64] + * 描述: + * 用户在商户对应appid下的唯一标识 + * 示例值:o-MYE42l80oelYMDE34nYD456Xoy + *+ */ + @SerializedName(value = "openid") + private String openid; + + /** + *
+ * 字段名:收款用户姓名 + * 变量名:user_name + * 是否必填:否 + * 类型:string[1, 1024] + * 描述: + * 收款用户真实姓名。该字段需进行加密处理,加密方法详见敏感信息加密说明 + * 示例值:757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45 + *+ */ + @SpecEncrypt + @SerializedName(value = "user_name") + private String userName; + + /** + *
+ * 字段名:收款用户身份证 + * 变量名:user_id_card + * 是否必填:否 + * 类型:string[1, 1024] + * 描述: + * 收款用户身份证号。该字段需进行加密处理,加密方法详见敏感信息加密说明 + * 示例值:8609cb22e1774a50a930e414cc71eca06121bcd266335cda230d24a7886a8d9f + *+ */ + @SpecEncrypt + @SerializedName(value = "user_id_card") + private String userIdCard; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/marketing/payroll/PayrollTransferBatchesResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/marketing/payroll/PayrollTransferBatchesResult.java new file mode 100644 index 0000000000..628c75d5f7 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/marketing/payroll/PayrollTransferBatchesResult.java @@ -0,0 +1,241 @@ +package com.github.binarywang.wxpay.bean.marketing.payroll; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + *
+ * 微工卡批量转账API返回结果 + * 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3_partner/Offline/apis/chapter4_1_8.shtml + * + * 适用对象:服务商 + * 请求URL:https://api.mch.weixin.qq.com/v3/payroll-card/transfer-batches + * 请求方式:POST + *+ * + * @author binarywang + * created on 2025/01/19 + */ +@Data +@NoArgsConstructor +public class PayrollTransferBatchesResult implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+ * 字段名:商家批次单号 + * 变量名:out_batch_no + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * 商户系统内部的商家批次单号 + * 示例值:plfk2020042013 + *+ */ + @SerializedName(value = "out_batch_no") + private String outBatchNo; + + /** + *
+ * 字段名:微信批次单号 + * 变量名:batch_id + * 是否必填:是 + * 类型:string[1, 64] + * 描述: + * 微信批次单号,微信商家转账系统返回的唯一标识 + * 示例值:1030000071100999991182020050700019480001 + *+ */ + @SerializedName(value = "batch_id") + private String batchId; + + /** + *
+ * 字段名:批次状态 + * 变量名:batch_status + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * ACCEPTED:已受理,批次已受理成功,若发起批量转账的30分钟后,转账批次单仍处于该状态,可能原因是商户账户余额不足等。商户可查询账户资金流水,若该笔转账批次单的扣款已经发生,则表示批次已经进入转账中,请再次查单确认 + * PROCESSING:转账中,已开始处理批次内的转账明细单 + * FINISHED:已完成,批次内的所有转账明细单都已处理完成 + * CLOSED:已关闭,可查询具体的批次关闭原因确认 + * 示例值:ACCEPTED + *+ */ + @SerializedName(value = "batch_status") + private String batchStatus; + + /** + *
+ * 字段名:批次类型 + * 变量名:batch_type + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * 批次类型 + * API:API方式发起 + * WEB:WEB方式发起 + * 示例值:API + *+ */ + @SerializedName(value = "batch_type") + private String batchType; + + /** + *
+ * 字段名:批次名称 + * 变量名:batch_name + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * 该笔批量转账的名称 + * 示例值:2019年1月深圳分部报销单 + *+ */ + @SerializedName(value = "batch_name") + private String batchName; + + /** + *
+ * 字段名:批次备注 + * 变量名:batch_remark + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * 转账说明,UTF8编码,最多允许32个字符 + * 示例值:2019年1月深圳分部报销单 + *+ */ + @SerializedName(value = "batch_remark") + private String batchRemark; + + /** + *
+ * 字段名:批次关闭原因 + * 变量名:close_reason + * 是否必填:否 + * 类型:string[1, 32] + * 描述: + * 如果批次单状态为"CLOSED"(已关闭),则有关闭原因 + * 示例值:OVERDUE_CLOSE + *+ */ + @SerializedName(value = "close_reason") + private String closeReason; + + /** + *
+ * 字段名:转账总金额 + * 变量名:total_amount + * 是否必填:是 + * 类型:int64 + * 描述: + * 转账金额单位为"分" + * 示例值:4000000 + *+ */ + @SerializedName(value = "total_amount") + private Long totalAmount; + + /** + *
+ * 字段名:转账总笔数 + * 变量名:total_num + * 是否必填:是 + * 类型:int + * 描述: + * 一个转账批次单最多发起一千笔转账 + * 示例值:200 + *+ */ + @SerializedName(value = "total_num") + private Integer totalNum; + + /** + *
+ * 字段名:批次创建时间 + * 变量名:create_time + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * 批次受理成功时返回,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss:sss+TIMEZONE + * 示例值:2015-05-20T13:29:35.120+08:00 + *+ */ + @SerializedName(value = "create_time") + private String createTime; + + /** + *
+ * 字段名:批次更新时间 + * 变量名:update_time + * 是否必填:是 + * 类型:string[1, 32] + * 描述: + * 批次最近一次状态变更的时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss:sss+TIMEZONE + * 示例值:2015-05-20T13:29:35.120+08:00 + *+ */ + @SerializedName(value = "update_time") + private String updateTime; + + /** + *
+ * 字段名:转账成功金额 + * 变量名:success_amount + * 是否必填:否 + * 类型:int64 + * 描述: + * 转账成功的金额,单位为"分" + * 示例值:3900000 + *+ */ + @SerializedName(value = "success_amount") + private Long successAmount; + + /** + *
+ * 字段名:转账成功笔数 + * 变量名:success_num + * 是否必填:否 + * 类型:int + * 描述: + * 转账成功的笔数 + * 示例值:199 + *+ */ + @SerializedName(value = "success_num") + private Integer successNum; + + /** + *
+ * 字段名:转账失败金额 + * 变量名:fail_amount + * 是否必填:否 + * 类型:int64 + * 描述: + * 转账失败的金额,单位为"分" + * 示例值:100000 + *+ */ + @SerializedName(value = "fail_amount") + private Long failAmount; + + /** + *
+ * 字段名:转账失败笔数 + * 变量名:fail_num + * 是否必填:否 + * 类型:int + * 描述: + * 转账失败的笔数 + * 示例值:1 + *+ */ + @SerializedName(value = "fail_num") + private Integer failNum; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/marketing/payroll/PreOrderWithAuthRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/marketing/payroll/PreOrderWithAuthRequest.java index 1556fbc343..0e20fc8fa6 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/marketing/payroll/PreOrderWithAuthRequest.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/marketing/payroll/PreOrderWithAuthRequest.java @@ -166,4 +166,22 @@ public class PreOrderWithAuthRequest implements Serializable { */ @SerializedName(value = "employment_type") private String employmentType; + + /** + *
+ * 字段名:核身类型 + * 变量名:authenticate_type + * 是否必填:否 + * 类型:string[1,32] + * 描述: + * 核身类型,用于标识本次核身的业务类型;枚举值: + * NORMAL_AUTHENTICATE:普通核身 + * LOGIN_AUTHENTICATE:登录核身 + * INSURANCE_AUTHENTICATE:保险核身 + * CONTRACT_AUTHENTICATE:合同核身 + * 示例值:NORMAL_AUTHENTICATE + *+ */ + @SerializedName(value = "authenticate_type") + private String authenticateType; } diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/marketing/transfer/BatchDetailsResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/marketing/transfer/BatchDetailsResult.java index 4ca7958ed5..854fd6ba5a 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/marketing/transfer/BatchDetailsResult.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/marketing/transfer/BatchDetailsResult.java @@ -235,4 +235,30 @@ public String toString() { */ @SerializedName(value = "update_time") private String updateTime; + /** + *
+ * 字段名:开户银行全称(含支行) + * 变量名:bank_name + * 是否必填:否 + * 类型:string[1, 128] + * 描述: + * 转账到银行卡时返回,开户银行全称(含支行) + * 示例值:中国农业银行股份有限公司深圳分行 + *+ */ + @SerializedName(value = "bank_name") + private String bankName; + /** + *
+ * 字段名:银行卡号后四位 + * 变量名:bank_card_number_tail + * 是否必填:否 + * 类型:string[4, 4] + * 描述: + * 转账到银行卡时返回,用于标识银行卡的后四位 + * 示例值:1234 + *+ */ + @SerializedName(value = "bank_card_number_tail") + private String bankCardNumberTail; } diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/media/VideoUploadResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/media/VideoUploadResult.java new file mode 100644 index 0000000000..615cbbff5f --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/media/VideoUploadResult.java @@ -0,0 +1,29 @@ +package com.github.binarywang.wxpay.bean.media; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +/** + * 视频文件上传返回结果对象 + * + * @author copilot + */ +@NoArgsConstructor +@Data +public class VideoUploadResult { + + public static VideoUploadResult fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, VideoUploadResult.class); + } + + /** + * 媒体文件标识 Id + *
+ * 微信返回的媒体文件标识Id。 + * 示例值:6uqyGjGrCf2GtyXP8bxrbuH9-aAoTjH-rKeSl3Lf4_So6kdkQu4w8BYVP3bzLtvR38lxt4PjtCDXsQpzqge_hQEovHzOhsLleGFQVRF-U_0 + */ + @SerializedName("media_id") + private String mediaId; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/MedInsOrdersRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/MedInsOrdersRequest.java new file mode 100644 index 0000000000..b651100a59 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/MedInsOrdersRequest.java @@ -0,0 +1,585 @@ +package com.github.binarywang.wxpay.bean.mipay; + +import com.github.binarywang.wxpay.bean.mipay.enums.CashAddTypeEnum; +import com.github.binarywang.wxpay.bean.mipay.enums.CashReduceTypeEnum; +import com.github.binarywang.wxpay.bean.mipay.enums.MixPayTypeEnum; +import com.github.binarywang.wxpay.bean.mipay.enums.OrderTypeEnum; +import com.github.binarywang.wxpay.bean.mipay.enums.UserCardTypeEnum; +import com.github.binarywang.wxpay.v3.SpecEncrypt; +import com.google.gson.annotations.SerializedName; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 医保自费混合收款下单请求 + *
+ * 从业机构调用该接口向微信医保后台下单 + * 文档地址:https://pay.weixin.qq.com/doc/v3/partner/4012503131 + * @author xgl + * @date 2025/12/19 14:37 + */ +@Data +@Builder(builderMethodName = "newBuilder") +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class MedInsOrdersRequest { + + /** + *
+ * 字段名:混合支付类型 + * 变量名:mix_pay_type + * 必填:是 + * 类型:string + * 描述: + * 混合支付类型可选取值: + * - UNKNOWN_MIX_PAY_TYPE: 未知的混合支付类型,会被拦截 + * - CASH_ONLY: 只向微信支付下单,没有向医保局下单 + * - INSURANCE_ONLY: 只向医保局下单,没有向微信支付下单 + * - CASH_AND_INSURANCE: 向医保局下单,也向微信支付下单 + *+ */ + @SerializedName("mix_pay_type") + public MixPayTypeEnum mixPayType; + + /** + *
+ * 字段名:订单类型 + * 变量名:order_type + * 必填:是 + * 类型:string + * 描述: + * 订单类型可选取值: + * - UNKNOWN_ORDER_TYPE: 未知类型,会被拦截 + * - REG_PAY: 挂号支付 + * - DIAG_PAY: 诊间支付 + * - COVID_EXAM_PAY: 新冠检测费用(核酸) + * - IN_HOSP_PAY: 住院费支付 + * - PHARMACY_PAY: 药店支付 + * - INSURANCE_PAY: 保险费支付 + * - INT_REG_PAY: 互联网医院挂号支付 + * - INT_RE_DIAG_PAY: 互联网医院复诊支付 + * - INT_RX_PAY: 互联网医院处方支付 + * - COVID_ANTIGEN_PAY: 新冠抗原检测 + * - MED_PAY: 药费支付 + *+ */ + @SerializedName("order_type") + public OrderTypeEnum orderType; + + /** + *
+ * 字段名:从业机构/服务商的公众号ID + * 变量名:appid + * 必填:是 + * 类型:string(32) + * 描述:从业机构/服务商的公众号ID + *+ */ + @SerializedName("appid") + public String appid; + + /** + *
+ * 字段名:医疗机构的公众号ID + * 变量名:sub_appid + * 必填:是 + * 类型:string(32) + * 描述:医疗机构的公众号ID + *+ */ + @SerializedName("sub_appid") + public String subAppid; + + /** + *
+ * 字段名:医疗机构的商户号 + * 变量名:sub_mchid + * 必填:是 + * 类型:string(32) + * 描述:医疗机构的商户号 + *+ */ + @SerializedName("sub_mchid") + public String subMchid; + + /** + *
+ * 字段名:用户在appid下的唯一标识 + * 变量名:openid + * 必填:否 + * 类型:string(128) + * 描述:openid与sub_openid二选一,传入openid时需要使用appid调起医保自费混合支付 + *+ */ + @SerializedName("openid") + public String openid; + + /** + *
+ * 字段名:用户在sub_appid下的唯一标识 + * 变量名:sub_openid + * 必填:否 + * 类型:string(128) + * 描述:openid与sub_openid二选一,传入sub_openid时需要使用sub_appid调起医保自费混合支付 + *+ */ + @SerializedName("sub_openid") + public String subOpenid; + + /** + *
+ * 字段名:支付人身份信息 + * 变量名:payer + * 必填:是 + * 类型:object + * 描述:支付人身份信息 + *+ */ + @SerializedName("payer") + @SpecEncrypt + public PersonIdentification payer; + + /** + *
+ * 字段名:是否代亲属支付 + * 变量名:pay_for_relatives + * 必填:否 + * 类型:boolean + * 描述:不传默认替本人支付 + * - true: 代亲属支付 + * - false: 本人支付 + *+ */ + @SerializedName("pay_for_relatives") + public Boolean payForRelatives; + + /** + *
+ * 字段名:亲属身份信息 + * 变量名:relative + * 必填:否 + * 类型:object + * 描述:pay_for_relatives为true时,该字段必填 + *+ */ + @SerializedName("relative") + @SpecEncrypt + public PersonIdentification relative; + + /** + *
+ * 字段名:从业机构订单号 + * 变量名:out_trade_no + * 必填:是 + * 类型:string(64) + * 描述:从业机构/服务商需要调两次接口:从业机构/服务商向微信支付下单获取微信支付凭证,请求中会带上out_trade_no。下单成功后,从业机构/服务商调用混合下单的接口(即该接口),请求中也会带上out_trade_no。 + *+ */ + @SerializedName("out_trade_no") + public String outTradeNo; + + /** + *
+ * 字段名:医疗机构订单号 + * 变量名:serial_no + * 必填:是 + * 类型:string(40) + * 描述:例如医院HIS系统订单号。传与费用明细上传中medOrgOrd字段一样的值,局端会校验,不一致将会返回错误 + *+ */ + @SerializedName("serial_no") + public String serialNo; + + /** + *
+ * 字段名:支付订单号 + * 变量名:pay_order_id + * 必填:否 + * 类型:string + * 描述:支付订单号 + *+ */ + @SerializedName("pay_order_id") + public String payOrderId; + + /** + *
+ * 字段名:支付授权号 + * 变量名:pay_auth_no + * 必填:否 + * 类型:string + * 描述:支付授权号 + *+ */ + @SerializedName("pay_auth_no") + public String payAuthNo; + + /** + *
+ * 字段名:地理位置 + * 变量名:geo_location + * 必填:否 + * 类型:string + * 描述:地理位置 + *+ */ + @SerializedName("geo_location") + public String geoLocation; + + /** + *
+ * 字段名:城市ID + * 变量名:city_id + * 必填:否 + * 类型:string + * 描述:城市ID + *+ */ + @SerializedName("city_id") + public String cityId; + + /** + *
+ * 字段名:医疗机构名称 + * 变量名:med_inst_name + * 必填:否 + * 类型:string + * 描述:医疗机构名称 + *+ */ + @SerializedName("med_inst_name") + public String medInstName; + + /** + *
+ * 字段名:医疗机构编号 + * 变量名:med_inst_no + * 必填:否 + * 类型:string + * 描述:医疗机构编号 + *+ */ + @SerializedName("med_inst_no") + public String medInstNo; + + /** + *
+ * 字段名:医保订单创建时间 + * 变量名:med_ins_order_create_time + * 必填:否 + * 类型:string + * 描述:医保订单创建时间 + *+ */ + @SerializedName("med_ins_order_create_time") + public String medInsOrderCreateTime; + + /** + *
+ * 字段名:总金额 + * 变量名:total_fee + * 必填:否 + * 类型:Integer + * 描述:总金额 + *+ */ + @SerializedName("total_fee") + public Integer totalFee; + + /** + *
+ * 字段名:医保统筹基金支付金额 + * 变量名:med_ins_gov_fee + * 必填:否 + * 类型:Integer + * 描述:医保统筹基金支付金额 + *+ */ + @SerializedName("med_ins_gov_fee") + public Integer medInsGovFee; + + /** + *
+ * 字段名:医保个人账户支付金额 + * 变量名:med_ins_self_fee + * 必填:否 + * 类型:Integer + * 描述:医保个人账户支付金额 + *+ */ + @SerializedName("med_ins_self_fee") + public Integer medInsSelfFee; + + /** + *
+ * 字段名:医保其他基金支付金额 + * 变量名:med_ins_other_fee + * 必填:否 + * 类型:Integer + * 描述:医保其他基金支付金额 + *+ */ + @SerializedName("med_ins_other_fee") + public Integer medInsOtherFee; + + /** + *
+ * 字段名:医保现金支付金额 + * 变量名:med_ins_cash_fee + * 必填:否 + * 类型:Integer + * 描述:医保现金支付金额 + *+ */ + @SerializedName("med_ins_cash_fee") + public Integer medInsCashFee; + + /** + *
+ * 字段名:微信支付现金支付金额 + * 变量名:wechat_pay_cash_fee + * 必填:否 + * 类型:Integer + * 描述:微信支付现金支付金额 + *+ */ + @SerializedName("wechat_pay_cash_fee") + public Integer wechatPayCashFee; + + /** + *
+ * 字段名:现金增加明细 + * 变量名:cash_add_detail + * 必填:否 + * 类型:list + * 描述:现金增加明细 + *+ */ + @SerializedName("cash_add_detail") + public List
+ * 字段名:现金减少明细 + * 变量名:cash_reduce_detail + * 必填:否 + * 类型:list + * 描述:现金减少明细 + *+ */ + @SerializedName("cash_reduce_detail") + public List
+ * 字段名:回调URL + * 变量名:callback_url + * 必填:否 + * 类型:string + * 描述:回调URL + *+ */ + @SerializedName("callback_url") + public String callbackUrl; + + /** + *
+ * 字段名:预支付交易会话标识 + * 变量名:prepay_id + * 必填:否 + * 类型:string + * 描述:预支付交易会话标识 + *+ */ + @SerializedName("prepay_id") + public String prepayId; + + /** + *
+ * 字段名:透传请求内容 + * 变量名:passthrough_request_content + * 必填:否 + * 类型:string + * 描述:透传请求内容 + *+ */ + @SerializedName("passthrough_request_content") + public String passthroughRequestContent; + + /** + *
+ * 字段名:扩展字段 + * 变量名:extends + * 必填:否 + * 类型:string + * 描述:扩展字段 + *+ */ + @SerializedName("extends") + public String _extends; + + /** + *
+ * 字段名:附加数据 + * 变量名:attach + * 必填:否 + * 类型:string + * 描述:附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用 + *+ */ + @SerializedName("attach") + public String attach; + + /** + *
+ * 字段名:渠道编号 + * 变量名:channel_no + * 必填:否 + * 类型:string + * 描述:渠道编号 + *+ */ + @SerializedName("channel_no") + public String channelNo; + + /** + *
+ * 字段名:医保测试环境标识 + * 变量名:med_ins_test_env + * 必填:否 + * 类型:boolean + * 描述:医保测试环境标识 + *+ */ + @SerializedName("med_ins_test_env") + public Boolean medInsTestEnv; + + /** + *
+ * 支付人身份信息 + *+ */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Accessors(chain = true) + public static class PersonIdentification { + /** + *
+ * 字段名:姓名 + * 变量名:name + * 必填:是 + * 类型:string + * 描述:姓名,需加密 + *+ */ + @SerializedName("name") + @SpecEncrypt + public String name; + + /** + *
+ * 字段名:身份证摘要 + * 变量名:id_digest + * 必填:是 + * 类型:string + * 描述:身份证摘要,需加密 + *+ */ + @SerializedName("id_digest") + @SpecEncrypt + public String idDigest; + + /** + *
+ * 字段名:证件类型 + * 变量名:card_type + * 必填:是 + * 类型:string + * 描述:证件类型 + *+ */ + @SerializedName("card_type") + public UserCardTypeEnum cardType; + } + + /** + *
+ * 现金增加明细实体 + *+ */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Accessors(chain = true) + public static class CashAddEntity { + /** + *
+ * 字段名:现金增加金额 + * 变量名:cash_add_fee + * 必填:是 + * 类型:Integer + * 描述:现金增加金额 + *+ */ + @SerializedName("cash_add_fee") + public Integer cashAddFee; + + /** + *
+ * 字段名:现金增加类型 + * 变量名:cash_add_type + * 必填:是 + * 类型:string + * 描述:现金增加类型 + *+ */ + @SerializedName("cash_add_type") + public CashAddTypeEnum cashAddType; + } + + /** + *
+ * 现金减少明细实体 + *+ */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Accessors(chain = true) + public static class CashReduceEntity { + /** + *
+ * 字段名:现金减少金额 + * 变量名:cash_reduce_fee + * 必填:是 + * 类型:Integer + * 描述:现金减少金额 + *+ */ + @SerializedName("cash_reduce_fee") + public Integer cashReduceFee; + + /** + *
+ * 字段名:现金减少类型 + * 变量名:cash_reduce_type + * 必填:是 + * 类型:string + * 描述:现金减少类型 + *+ */ + @SerializedName("cash_reduce_type") + public CashReduceTypeEnum cashReduceType; + } + + +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/MedInsOrdersResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/MedInsOrdersResult.java new file mode 100644 index 0000000000..9a119d8723 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/MedInsOrdersResult.java @@ -0,0 +1,497 @@ +package com.github.binarywang.wxpay.bean.mipay; + +import com.github.binarywang.wxpay.bean.mipay.enums.MedInsPayStatusEnum; +import com.github.binarywang.wxpay.bean.mipay.enums.MixPayStatusEnum; +import com.github.binarywang.wxpay.bean.mipay.enums.MixPayTypeEnum; +import com.github.binarywang.wxpay.bean.mipay.enums.OrderTypeEnum; +import com.github.binarywang.wxpay.bean.mipay.enums.SelfPayStatusEnum; +import com.google.gson.annotations.SerializedName; +import java.util.List; +import lombok.Data; + +/** + * 医保自费混合收款下单响应 + *
+ * 从业机构调用医保自费混合收款下单接口后返回的结果 + * 文档地址:https://pay.weixin.qq.com/doc/v3/partner/4012503131 + * @author xgl + * @date 2025/12/19 14:37 + */ +@Data +public class MedInsOrdersResult { + /** + *
+ * 字段名:混合交易订单号 + * 变量名:mix_trade_no + * 必填:是 + * 类型:string + * 描述:微信支付生成的混合交易订单号 + *+ */ + @SerializedName("mix_trade_no") + public String mixTradeNo; + + /** + *
+ * 字段名:混合支付状态 + * 变量名:mix_pay_status + * 必填:是 + * 类型:string + * 描述:混合支付整体状态 + *+ */ + @SerializedName("mix_pay_status") + public MixPayStatusEnum mixPayStatus; + + /** + *
+ * 字段名:自费支付状态 + * 变量名:self_pay_status + * 必填:是 + * 类型:string + * 描述:自费部分支付状态 + *+ */ + @SerializedName("self_pay_status") + public SelfPayStatusEnum selfPayStatus; + + /** + *
+ * 字段名:医保支付状态 + * 变量名:med_ins_pay_status + * 必填:是 + * 类型:string + * 描述:医保部分支付状态 + *+ */ + @SerializedName("med_ins_pay_status") + public MedInsPayStatusEnum medInsPayStatus; + + /** + *
+ * 字段名:支付完成时间 + * 变量名:paid_time + * 必填:否 + * 类型:string + * 描述:支付完成时间,格式为yyyyMMddHHmmss + *+ */ + @SerializedName("paid_time") + public String paidTime; + + /** + *
+ * 字段名:透传响应内容 + * 变量名:passthrough_response_content + * 必填:否 + * 类型:string + * 描述:透传响应内容 + *+ */ + @SerializedName("passthrough_response_content") + public String passthroughResponseContent; + + /** + *
+ * 字段名:混合支付类型 + * 变量名:mix_pay_type + * 必填:是 + * 类型:string + * 描述: + * 混合支付类型可选取值: + * - UNKNOWN_MIX_PAY_TYPE: 未知的混合支付类型,会被拦截 + * - CASH_ONLY: 只向微信支付下单,没有向医保局下单 + * - INSURANCE_ONLY: 只向医保局下单,没有向微信支付下单 + * - CASH_AND_INSURANCE: 向医保局下单,也向微信支付下单 + *+ */ + @SerializedName("mix_pay_type") + public MixPayTypeEnum mixPayType; + + /** + *
+ * 字段名:订单类型 + * 变量名:order_type + * 必填:是 + * 类型:string + * 描述: + * 订单类型可选取值: + * - UNKNOWN_ORDER_TYPE: 未知类型,会被拦截 + * - REG_PAY: 挂号支付 + * - DIAG_PAY: 诊间支付 + * - COVID_EXAM_PAY: 新冠检测费用(核酸) + * - IN_HOSP_PAY: 住院费支付 + * - PHARMACY_PAY: 药店支付 + * - INSURANCE_PAY: 保险费支付 + * - INT_REG_PAY: 互联网医院挂号支付 + * - INT_RE_DIAG_PAY: 互联网医院复诊支付 + * - INT_RX_PAY: 互联网医院处方支付 + * - COVID_ANTIGEN_PAY: 新冠抗原检测 + * - MED_PAY: 药费支付 + *+ */ + @SerializedName("order_type") + public OrderTypeEnum orderType; + + /** + *
+ * 字段名:从业机构/服务商的公众号ID + * 变量名:appid + * 必填:是 + * 类型:string(32) + * 描述:从业机构/服务商的公众号ID + *+ */ + @SerializedName("appid") + public String appid; + + /** + *
+ * 字段名:医疗机构的公众号ID + * 变量名:sub_appid + * 必填:是 + * 类型:string(32) + * 描述:医疗机构的公众号ID + *+ */ + @SerializedName("sub_appid") + public String subAppid; + + /** + *
+ * 字段名:医疗机构的商户号 + * 变量名:sub_mchid + * 必填:是 + * 类型:string(32) + * 描述:医疗机构的商户号 + *+ */ + @SerializedName("sub_mchid") + public String subMchid; + + /** + *
+ * 字段名:用户在appid下的唯一标识 + * 变量名:openid + * 必填:否 + * 类型:string(128) + * 描述:openid与sub_openid二选一,传入openid时需要使用appid调起医保自费混合支付 + *+ */ + @SerializedName("openid") + public String openid; + + /** + *
+ * 字段名:用户在sub_appid下的唯一标识 + * 变量名:sub_openid + * 必填:否 + * 类型:string(128) + * 描述:openid与sub_openid二选一,传入sub_openid时需要使用sub_appid调起医保自费混合支付 + *+ */ + @SerializedName("sub_openid") + public String subOpenid; + + /** + *
+ * 字段名:是否代亲属支付 + * 变量名:pay_for_relatives + * 必填:否 + * 类型:boolean + * 描述:不传默认替本人支付 + * - true: 代亲属支付 + * - false: 本人支付 + *+ */ + @SerializedName("pay_for_relatives") + public Boolean payForRelatives; + + /** + *
+ * 字段名:从业机构订单号 + * 变量名:out_trade_no + * 必填:是 + * 类型:string(64) + * 描述:从业机构/服务商需要调两次接口:从业机构/服务商向微信支付下单获取微信支付凭证,请求中会带上out_trade_no。下单成功后,从业机构/服务商调用混合下单的接口(即该接口),请求中也会带上out_trade_no。 + *+ */ + @SerializedName("out_trade_no") + public String outTradeNo; + + /** + *
+ * 字段名:医疗机构订单号 + * 变量名:serial_no + * 必填:是 + * 类型:string(40) + * 描述:例如医院HIS系统订单号。传与费用明细上传中medOrgOrd字段一样的值,局端会校验,不一致将会返回错误 + *+ */ + @SerializedName("serial_no") + public String serialNo; + + /** + *
+ * 字段名:支付订单号 + * 变量名:pay_order_id + * 必填:否 + * 类型:string + * 描述:支付订单号 + *+ */ + @SerializedName("pay_order_id") + public String payOrderId; + + /** + *
+ * 字段名:支付授权号 + * 变量名:pay_auth_no + * 必填:否 + * 类型:string + * 描述:支付授权号 + *+ */ + @SerializedName("pay_auth_no") + public String payAuthNo; + + /** + *
+ * 字段名:地理位置 + * 变量名:geo_location + * 必填:否 + * 类型:string + * 描述:地理位置 + *+ */ + @SerializedName("geo_location") + public String geoLocation; + + /** + *
+ * 字段名:城市ID + * 变量名:city_id + * 必填:否 + * 类型:string + * 描述:城市ID + *+ */ + @SerializedName("city_id") + public String cityId; + + /** + *
+ * 字段名:医疗机构名称 + * 变量名:med_inst_name + * 必填:否 + * 类型:string + * 描述:医疗机构名称 + *+ */ + @SerializedName("med_inst_name") + public String medInstName; + + /** + *
+ * 字段名:医疗机构编号 + * 变量名:med_inst_no + * 必填:否 + * 类型:string + * 描述:医疗机构编号 + *+ */ + @SerializedName("med_inst_no") + public String medInstNo; + + /** + *
+ * 字段名:医保订单创建时间 + * 变量名:med_ins_order_create_time + * 必填:否 + * 类型:string + * 描述:医保订单创建时间 + *+ */ + @SerializedName("med_ins_order_create_time") + public String medInsOrderCreateTime; + + /** + *
+ * 字段名:总金额 + * 变量名:total_fee + * 必填:否 + * 类型:Integer + * 描述:总金额 + *+ */ + @SerializedName("total_fee") + public Integer totalFee; + + /** + *
+ * 字段名:医保统筹基金支付金额 + * 变量名:med_ins_gov_fee + * 必填:否 + * 类型:Integer + * 描述:医保统筹基金支付金额 + *+ */ + @SerializedName("med_ins_gov_fee") + public Integer medInsGovFee; + + /** + *
+ * 字段名:医保个人账户支付金额 + * 变量名:med_ins_self_fee + * 必填:否 + * 类型:Integer + * 描述:医保个人账户支付金额 + *+ */ + @SerializedName("med_ins_self_fee") + public Integer medInsSelfFee; + + /** + *
+ * 字段名:医保其他基金支付金额 + * 变量名:med_ins_other_fee + * 必填:否 + * 类型:Integer + * 描述:医保其他基金支付金额 + *+ */ + @SerializedName("med_ins_other_fee") + public Integer medInsOtherFee; + + /** + *
+ * 字段名:医保现金支付金额 + * 变量名:med_ins_cash_fee + * 必填:否 + * 类型:Integer + * 描述:医保现金支付金额 + *+ */ + @SerializedName("med_ins_cash_fee") + public Integer medInsCashFee; + + /** + *
+ * 字段名:微信支付现金支付金额 + * 变量名:wechat_pay_cash_fee + * 必填:否 + * 类型:Integer + * 描述:微信支付现金支付金额 + *+ */ + @SerializedName("wechat_pay_cash_fee") + public Integer wechatPayCashFee; + + /** + *
+ * 字段名:现金增加明细 + * 变量名:cash_add_detail + * 必填:否 + * 类型:list + * 描述:现金增加明细 + *+ */ + @SerializedName("cash_add_detail") + public List
+ * 字段名:现金减少明细 + * 变量名:cash_reduce_detail + * 必填:否 + * 类型:list + * 描述:现金减少明细 + *+ */ + @SerializedName("cash_reduce_detail") + public List
+ * 字段名:回调URL + * 变量名:callback_url + * 必填:否 + * 类型:string + * 描述:回调URL + *+ */ + @SerializedName("callback_url") + public String callbackUrl; + + /** + *
+ * 字段名:预支付交易会话标识 + * 变量名:prepay_id + * 必填:否 + * 类型:string + * 描述:预支付交易会话标识 + *+ */ + @SerializedName("prepay_id") + public String prepayId; + + /** + *
+ * 字段名:透传请求内容 + * 变量名:passthrough_request_content + * 必填:否 + * 类型:string + * 描述:透传请求内容 + *+ */ + @SerializedName("passthrough_request_content") + public String passthroughRequestContent; + + /** + *
+ * 字段名:扩展字段 + * 变量名:extends + * 必填:否 + * 类型:string + * 描述:扩展字段 + *+ */ + @SerializedName("extends") + public String _extends; + + /** + *
+ * 字段名:附加数据 + * 变量名:attach + * 必填:否 + * 类型:string + * 描述:附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用 + *+ */ + @SerializedName("attach") + public String attach; + + /** + *
+ * 字段名:渠道编号 + * 变量名:channel_no + * 必填:否 + * 类型:string + * 描述:渠道编号 + *+ */ + @SerializedName("channel_no") + public String channelNo; + + /** + *
+ * 字段名:医保测试环境标识 + * 变量名:med_ins_test_env + * 必填:否 + * 类型:boolean + * 描述:医保测试环境标识 + *+ */ + @SerializedName("med_ins_test_env") + public Boolean medInsTestEnv; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/MedInsRefundNotifyRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/MedInsRefundNotifyRequest.java new file mode 100644 index 0000000000..cb935b52cd --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/MedInsRefundNotifyRequest.java @@ -0,0 +1,106 @@ +package com.github.binarywang.wxpay.bean.mipay; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 医保退款通知请求 + *
+ * 从业机构调用该接口向微信医保后台通知医保订单的退款成功结果 + * 文档地址:https://pay.weixin.qq.com/doc/v3/partner/4012166534 + * @author xgl + * @date 2025/12/20 + */ +@Data +@Builder(builderMethodName = "newBuilder") +@NoArgsConstructor +@AllArgsConstructor +public class MedInsRefundNotifyRequest { + + /** + *
+ * 字段名:医疗机构的商户号 + * 变量名:sub_mchid + * 必填:是 + * 类型:string(32) + * 描述:医疗机构的商户号 + *+ */ + @SerializedName("sub_mchid") + private String subMchid; + + /** + *
+ * 字段名:医保退款的总金额 + * 变量名:med_refund_total_fee + * 必填:是 + * 类型:integer + * 描述:单位分,医保退款的总金额。 + *+ */ + @SerializedName("med_refund_total_fee") + private Integer medRefundTotalFee; + + /** + *
+ * 字段名:医保统筹退款金额 + * 变量名:med_refund_gov_fee + * 必填:是 + * 类型:integer + * 描述:单位分,医保统筹退款金额。 + *+ */ + @SerializedName("med_refund_gov_fee") + private Integer medRefundGovFee; + + /** + *
+ * 字段名:医保个账退款金额 + * 变量名:med_refund_self_fee + * 必填:是 + * 类型:integer + * 描述:单位分,医保个账退款金额。 + *+ */ + @SerializedName("med_refund_self_fee") + private Integer medRefundSelfFee; + + /** + *
+ * 字段名:医保其他退款金额 + * 变量名:med_refund_other_fee + * 必填:是 + * 类型:integer + * 描述:单位分,医保其他退款金额。 + *+ */ + @SerializedName("med_refund_other_fee") + private Integer medRefundOtherFee; + + /** + *
+ * 字段名:医保退款成功时间 + * 变量名:refund_time + * 必填:是 + * 类型:string(64) + * 描述:遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE。 + *+ */ + @SerializedName("refund_time") + private String refundTime; + + /** + *
+ * 字段名:从业机构\服务商退款单号 + * 变量名:out_refund_no + * 必填:是 + * 类型:string(64) + * 描述:有自费单时,从业机构\服务商应填与自费退款申请处一致的out_refund_no。否则从业机构透传医疗机构退款单号即可。 + *+ */ + @SerializedName("out_refund_no") + private String outRefundNo; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/CashAddTypeEnum.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/CashAddTypeEnum.java new file mode 100644 index 0000000000..b935f20410 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/CashAddTypeEnum.java @@ -0,0 +1,29 @@ +package com.github.binarywang.wxpay.bean.mipay.enums; + +import com.google.gson.annotations.SerializedName; + +/** + * 现金增加类型枚举 + *
+ * 描述医保自费混合支付中现金增加的类型 + * + * @author xgl + * @date 2025/12/20 + */ +public enum CashAddTypeEnum { + /** + * 默认增加类型 + */ + @SerializedName("DEFAULT_ADD_TYPE") + DEFAULT_ADD_TYPE, + /** + * 运费 + */ + @SerializedName("FREIGHT") + FREIGHT, + /** + * 其他医疗费用 + */ + @SerializedName("OTHER_MEDICAL_EXPENSES") + OTHER_MEDICAL_EXPENSES +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/CashReduceTypeEnum.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/CashReduceTypeEnum.java new file mode 100644 index 0000000000..4f90b8500a --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/CashReduceTypeEnum.java @@ -0,0 +1,44 @@ +package com.github.binarywang.wxpay.bean.mipay.enums; + +import com.google.gson.annotations.SerializedName; + +/** + * 现金减少类型枚举 + *
+ * 描述医保自费混合支付中现金减少的类型 + * + * @author xgl + * @date 2025/12/20 + */ +public enum CashReduceTypeEnum { + /** + * 默认减少类型 + */ + @SerializedName("DEFAULT_REDUCE_TYPE") + DEFAULT_REDUCE_TYPE, + /** + * 医院减免 + */ + @SerializedName("HOSPITAL_REDUCE") + HOSPITAL_REDUCE, + /** + * 药店折扣 + */ + @SerializedName("PHARMACY_DISCOUNT") + PHARMACY_DISCOUNT, + /** + * 折扣优惠 + */ + @SerializedName("DISCOUNT") + DISCOUNT, + /** + * 预付费抵扣 + */ + @SerializedName("PRE_PAYMENT") + PRE_PAYMENT, + /** + * 押金扣除 + */ + @SerializedName("DEPOSIT_DEDUCTION") + DEPOSIT_DEDUCTION +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/MedInsPayStatusEnum.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/MedInsPayStatusEnum.java new file mode 100644 index 0000000000..324530f0ff --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/MedInsPayStatusEnum.java @@ -0,0 +1,44 @@ +package com.github.binarywang.wxpay.bean.mipay.enums; + +import com.google.gson.annotations.SerializedName; + +/** + * 医保支付状态枚举 + *
+ * 描述医保自费混合支付中医保部分的支付状态 + * + * @author xgl + * @date 2025/12/20 + */ +public enum MedInsPayStatusEnum { + /** + * 未知的医保支付状态 + */ + @SerializedName("UNKNOWN_MED_INS_PAY_STATUS") + UNKNOWN_MED_INS_PAY_STATUS, + /** + * 医保支付已创建 + */ + @SerializedName("MED_INS_PAY_CREATED") + MED_INS_PAY_CREATED, + /** + * 医保支付成功 + */ + @SerializedName("MED_INS_PAY_SUCCESS") + MED_INS_PAY_SUCCESS, + /** + * 医保支付已退款 + */ + @SerializedName("MED_INS_PAY_REFUND") + MED_INS_PAY_REFUND, + /** + * 医保支付失败 + */ + @SerializedName("MED_INS_PAY_FAIL") + MED_INS_PAY_FAIL, + /** + * 无需医保支付 + */ + @SerializedName("NO_MED_INS_PAY") + NO_MED_INS_PAY +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/MixPayStatusEnum.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/MixPayStatusEnum.java new file mode 100644 index 0000000000..7360704986 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/MixPayStatusEnum.java @@ -0,0 +1,39 @@ +package com.github.binarywang.wxpay.bean.mipay.enums; + +import com.google.gson.annotations.SerializedName; + +/** + * 混合支付状态枚举 + *
+ * 描述医保自费混合支付的整体状态 + * + * @author xgl + * @date 2025/12/20 + */ +public enum MixPayStatusEnum { + /** + * 未知的混合支付状态 + */ + @SerializedName("UNKNOWN_MIX_PAY_STATUS") + UNKNOWN_MIX_PAY_STATUS, + /** + * 混合支付已创建 + */ + @SerializedName("MIX_PAY_CREATED") + MIX_PAY_CREATED, + /** + * 混合支付成功 + */ + @SerializedName("MIX_PAY_SUCCESS") + MIX_PAY_SUCCESS, + /** + * 混合支付已退款 + */ + @SerializedName("MIX_PAY_REFUND") + MIX_PAY_REFUND, + /** + * 混合支付失败 + */ + @SerializedName("MIX_PAY_FAIL") + MIX_PAY_FAIL +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/MixPayTypeEnum.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/MixPayTypeEnum.java new file mode 100644 index 0000000000..ad62d50a66 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/MixPayTypeEnum.java @@ -0,0 +1,41 @@ +package com.github.binarywang.wxpay.bean.mipay.enums; + +import com.google.gson.annotations.SerializedName; + +/** + * 混合支付类型枚举 + *
+ * 描述医保自费混合支付的类型 + * 文档地址:https://pay.weixin.qq.com/doc/v3/partner/4012503131 + * + * @author xgl + * @date 2025/12/20 09:21 + */ +public enum MixPayTypeEnum { + + /** + * 未知的混合支付类型,会被拦截。 + */ + @SerializedName("UNKNOWN_MIX_PAY_TYPE") + UNKNOWN_MIX_PAY_TYPE, + + /** + * 只向微信支付下单,没有向医保局下单。包括没有向医保局上传费用明细、预结算。 + */ + @SerializedName("CASH_ONLY") + CASH_ONLY, + + /** + * 只向医保局下单,没有向微信支付下单。如果医保局分账结果中有自费部份,但由于有减免抵扣,没有向微信支付下单,也是纯医保。 + */ + @SerializedName("INSURANCE_ONLY") + INSURANCE_ONLY, + + /** + * 向医保局下单,也向微信支付下单。如果医保预结算全部需自费,也属于混合类型。 + */ + @SerializedName("CASH_AND_INSURANCE") + CASH_AND_INSURANCE + + +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/OrderTypeEnum.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/OrderTypeEnum.java new file mode 100644 index 0000000000..749b1276e7 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/OrderTypeEnum.java @@ -0,0 +1,87 @@ +package com.github.binarywang.wxpay.bean.mipay.enums; + +import com.google.gson.annotations.SerializedName; + +/** + * 订单类型枚举 + *
+ * 描述医保自费混合支付的订单类型 + * 文档地址:https://pay.weixin.qq.com/doc/v3/partner/4012503131 + * + * @author xgl + * @date 2025/12/20 + */ +public enum OrderTypeEnum { + + /** + * 未知类型,会被拦截 + */ + @SerializedName("UNKNOWN_ORDER_TYPE") + UNKNOWN_ORDER_TYPE, + + /** + * 挂号支付 + */ + @SerializedName("REG_PAY") + REG_PAY, + + /** + * 诊间支付 + */ + @SerializedName("DIAG_PAY") + DIAG_PAY, + + /** + * 新冠检测费用(核酸) + */ + @SerializedName("COVID_EXAM_PAY") + COVID_EXAM_PAY, + + /** + * 住院费支付 + */ + @SerializedName("IN_HOSP_PAY") + IN_HOSP_PAY, + + /** + * 药店支付 + */ + @SerializedName("PHARMACY_PAY") + PHARMACY_PAY, + + /** + * 保险费支付 + */ + @SerializedName("INSURANCE_PAY") + INSURANCE_PAY, + + /** + * 互联网医院挂号支付 + */ + @SerializedName("INT_REG_PAY") + INT_REG_PAY, + + /** + * 互联网医院复诊支付 + */ + @SerializedName("INT_RE_DIAG_PAY") + INT_RE_DIAG_PAY, + + /** + * 互联网医院处方支付 + */ + @SerializedName("INT_RX_PAY") + INT_RX_PAY, + + /** + * 新冠抗原检测 + */ + @SerializedName("COVID_ANTIGEN_PAY") + COVID_ANTIGEN_PAY, + + /** + * 药费支付 + */ + @SerializedName("MED_PAY") + MED_PAY +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/SelfPayStatusEnum.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/SelfPayStatusEnum.java new file mode 100644 index 0000000000..a7014b9e13 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/SelfPayStatusEnum.java @@ -0,0 +1,44 @@ +package com.github.binarywang.wxpay.bean.mipay.enums; + +import com.google.gson.annotations.SerializedName; + +/** + * 自费支付状态枚举 + *
+ * 描述医保自费混合支付中自费部分的支付状态 + * + * @author xgl + * @date 2025/12/20 + */ +public enum SelfPayStatusEnum { + /** + * 未知的自费支付状态 + */ + @SerializedName("UNKNOWN_SELF_PAY_STATUS") + UNKNOWN_SELF_PAY_STATUS, + /** + * 自费支付已创建 + */ + @SerializedName("SELF_PAY_CREATED") + SELF_PAY_CREATED, + /** + * 自费支付成功 + */ + @SerializedName("SELF_PAY_SUCCESS") + SELF_PAY_SUCCESS, + /** + * 自费支付已退款 + */ + @SerializedName("SELF_PAY_REFUND") + SELF_PAY_REFUND, + /** + * 自费支付失败 + */ + @SerializedName("SELF_PAY_FAIL") + SELF_PAY_FAIL, + /** + * 无需自费支付 + */ + @SerializedName("NO_SELF_PAY") + NO_SELF_PAY +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/UserCardTypeEnum.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/UserCardTypeEnum.java new file mode 100644 index 0000000000..1bf97b7628 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/mipay/enums/UserCardTypeEnum.java @@ -0,0 +1,54 @@ +package com.github.binarywang.wxpay.bean.mipay.enums; + +import com.google.gson.annotations.SerializedName; + +/** + * 用户证件类型枚举 + *
+ * 描述医保自费混合支付中用户的证件类型 + * + * @author xgl + * @date 2025/12/20 + */ +public enum UserCardTypeEnum { + /** + * 未知的用户证件类型 + */ + @SerializedName("UNKNOWN_USER_CARD_TYPE") + UNKNOWN_USER_CARD_TYPE, + /** + * 居民身份证 + */ + @SerializedName("ID_CARD") + ID_CARD, + /** + * 户口本 + */ + @SerializedName("HOUSEHOLD_REGISTRATION") + HOUSEHOLD_REGISTRATION, + /** + * 外国护照 + */ + @SerializedName("FOREIGNER_PASSPORT") + FOREIGNER_PASSPORT, + /** + * 台湾居民来往大陆通行证 + */ + @SerializedName("MAINLAND_TRAVEL_PERMIT_FOR_TW") + MAINLAND_TRAVEL_PERMIT_FOR_TW, + /** + * 澳门居民来往大陆通行证 + */ + @SerializedName("MAINLAND_TRAVEL_PERMIT_FOR_MO") + MAINLAND_TRAVEL_PERMIT_FOR_MO, + /** + * 香港居民来往大陆通行证 + */ + @SerializedName("MAINLAND_TRAVEL_PERMIT_FOR_HK") + MAINLAND_TRAVEL_PERMIT_FOR_HK, + /** + * 外国人永久居留身份证 + */ + @SerializedName("FOREIGN_PERMANENT_RESIDENT") + FOREIGN_PERMANENT_RESIDENT +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/ComplaintNotifyResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/ComplaintNotifyResult.java index 9464144c1d..fd5badb5d7 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/ComplaintNotifyResult.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/ComplaintNotifyResult.java @@ -1,5 +1,6 @@ package com.github.binarywang.wxpay.bean.notify; +import com.github.binarywang.wxpay.v3.SpecEncrypt; import com.google.gson.annotations.SerializedName; import lombok.Data; import lombok.NoArgsConstructor; @@ -69,6 +70,113 @@ public static class DecryptNotifyResult implements Serializable { @SerializedName(value = "action_type") private String actionType; + /** + *
+ * 字段名:商户订单号 + * 是否必填:是 + * 描述: + * 投诉单关联的商户订单号 + *+ */ + @SerializedName("out_trade_no") + private String outTradeNo; + + /** + *
+ * 字段名:投诉时间 + * 是否必填:是 + * 描述:投诉时间,遵循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年05月20日13点29分35秒 + * 示例值:2015-05-20T13:29:35.120+08:00 + *+ */ + @SerializedName("complaint_time") + private String complaintTime; + + /** + *
+ * 字段名:订单金额 + * 是否必填:是 + * 描述: + * 订单金额,单位(分) + *+ */ + @SerializedName("amount") + private Integer amount; + + /** + *
+ * 字段名:投诉人联系方式 + * 是否必填:否 + * 投诉人联系方式。该字段已做加密处理,具体解密方法详见敏感信息加密说明。 + *+ */ + @SerializedName("payer_phone") + @SpecEncrypt + private String payerPhone; + + /** + *
+ * 字段名:投诉详情 + * 是否必填:是 + * 投诉的具体描述 + *+ */ + @SerializedName("complaint_detail") + private String complaintDetail; + + /** + *
+ * 字段名:投诉单状态 + * 是否必填:是 + * 标识当前投诉单所处的处理阶段,具体状态如下所示: + * PENDING:待处理 + * PROCESSING:处理中 + * PROCESSED:已处理完成 + *+ */ + @SerializedName("complaint_state") + private String complaintState; + + /** + *
+ * 字段名:微信订单号 + * 是否必填:是 + * 描述: + * 投诉单关联的微信订单号 + *+ */ + @SerializedName("transaction_id") + private String transactionId; + + /** + *
+ * 字段名:商户处理状态 + * 是否必填:是 + * 描述: + * 触发本次投诉通知回调的具体动作类型,枚举如下: + * 常规通知: + * CREATE_COMPLAINT:用户提交投诉 + * CONTINUE_COMPLAINT:用户继续投诉 + * USER_RESPONSE:用户新留言 + * RESPONSE_BY_PLATFORM:平台新留言 + * SELLER_REFUND:商户发起全额退款 + * MERCHANT_RESPONSE:商户新回复 + * MERCHANT_CONFIRM_COMPLETE:商户反馈处理完成 + * USER_APPLY_PLATFORM_SERVICE:用户申请平台协助 + * USER_CANCEL_PLATFORM_SERVICE:用户取消平台协助 + * PLATFORM_SERVICE_FINISHED:客服结束平台协助 + * + * 申请退款单的附加通知: + * 以下通知会更新投诉单状态,建议收到后查询投诉单详情。 + * MERCHANT_APPROVE_REFUND:商户同意退款 + * MERCHANT_REJECT_REFUND:商户驳回退款 + * REFUND_SUCCESS:退款到账 + *+ */ + @SerializedName("complaint_handle_state") + private String complaintHandleState; } } diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/MiPayNotifyV3Result.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/MiPayNotifyV3Result.java new file mode 100644 index 0000000000..c2007dc365 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/MiPayNotifyV3Result.java @@ -0,0 +1,519 @@ +package com.github.binarywang.wxpay.bean.notify; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + *
+ * 医保混合收款成功通知结果 + * 文档地址:https://pay.weixin.qq.com/doc/v3/partner/4012165722 + *+ * + * @author xgl + * @since 2025/12/20 + */ +@Data +@NoArgsConstructor +public class MiPayNotifyV3Result implements Serializable, WxPayBaseNotifyV3Result
+ * 字段名:应用ID + * 变量名:appid + * 是否必填:是 + * 类型:string(32) + * 描述: + * 从业机构/服务商的公众号ID + *+ */ + @SerializedName(value = "appid") + private String appid; + + /** + *
+ * 字段名:医疗机构的公众号ID + * 变量名:sub_appid + * 是否必填:是 + * 类型:string(32) + * 描述: + * 医疗机构的公众号ID + *+ */ + @SerializedName(value = "sub_appid") + private String subAppid; + + /** + *
+ * 字段名:医疗机构的商户号 + * 变量名:sub_mchid + * 是否必填:是 + * 类型:string(32) + * 描述: + * 医疗机构的商户号 + *+ */ + @SerializedName(value = "sub_mchid") + private String subMchid; + + /** + *
+ * 字段名:从业机构订单号 + * 变量名:out_trade_no + * 是否必填:是 + * 类型:string(64) + * 描述: + * 从业机构/服务商订单号 + *+ */ + @SerializedName(value = "out_trade_no") + private String outTradeNo; + + /** + *
+ * 字段名:医保自费混合订单号 + * 变量名:mix_trade_no + * 是否必填:是 + * 类型:string(32) + * 描述: + * 微信支付系统生成的医保自费混合订单号 + *+ */ + @SerializedName(value = "mix_trade_no") + private String mixTradeNo; + + /** + *
+ * 字段名:医保自费混合订单支付状态 + * 变量名:mix_pay_status + * 是否必填:是 + * 类型:string + * 描述: + * 医保自费混合订单支付状态,枚举值: + * UNKNOWN_MIX_PAY_STATUS:未知类型,需报错 + * MIX_PAY_CREATED:等待支付 + * MIX_PAY_SUCCESS:支付成功 + * MIX_PAY_REFUND:自费和医保均已退款 + * MIX_PAY_FAIL:支付失败 + *+ */ + @SerializedName(value = "mix_pay_status") + private String mixPayStatus; + + /** + *
+ * 字段名:自费部分的支付状态 + * 变量名:self_pay_status + * 是否必填:否 + * 类型:string + * 描述: + * 混合订单中自费部分的支付状态,枚举值: + * UNKNOWN_SELF_PAY_STATUS:未知类型,需报错 + * SELF_PAY_CREATED:等待支付 + * SELF_PAY_SUCCESS:支付成功 + * SELF_PAY_REFUND:已退款 + * SELF_PAY_FAIL:支付失败 + * NO_SELF_PAY:没有自费 + *+ */ + @SerializedName(value = "self_pay_status") + private String selfPayStatus; + + /** + *
+ * 字段名:医保部分的支付状态 + * 变量名:med_ins_pay_status + * 是否必填:否 + * 类型:string + * 描述: + * 混合订单中医保部分的支付状态,枚举值: + * UNKNOWN_MED_INS_PAY_STATUS:未知类型,需报错 + * MED_INS_PAY_CREATED:等待支付 + * MED_INS_PAY_SUCCESS:支付成功 + * MED_INS_PAY_REFUND:已退款 + * MED_INS_PAY_FAIL:支付失败 + * NO_MED_INS_PAY:没有医保 + *+ */ + @SerializedName(value = "med_ins_pay_status") + private String medInsPayStatus; + + /** + *
+ * 字段名:订单支付时间 + * 变量名:paid_time + * 是否必填:否 + * 类型:string(64) + * 描述: + * 订单支付时间,遵循rfc3339标准格式 + *+ */ + @SerializedName(value = "paid_time") + private String paidTime; + + /** + *
+ * 字段名:医保局返回内容 + * 变量名:passthrough_response_content + * 是否必填:否 + * 类型:string(2048) + * 描述: + * 支付完成后医保局返回内容(透传给医疗机构) + *+ */ + @SerializedName(value = "passthrough_response_content") + private String passthroughResponseContent; + + /** + *
+ * 字段名:混合支付类型 + * 变量名:mix_pay_type + * 是否必填:是 + * 类型:string + * 描述: + * 混合支付类型,枚举值: + * UNKNOWN_MIX_PAY_TYPE:未知类型,需报错 + * CASH_ONLY:纯自费 + * INSURANCE_ONLY:纯医保 + * CASH_AND_INSURANCE:医保自费混合 + *+ */ + @SerializedName(value = "mix_pay_type") + private String mixPayType; + + /** + *
+ * 字段名:订单类型 + * 变量名:order_type + * 是否必填:否 + * 类型:string + * 描述: + * 订单类型,枚举值: + * UNKNOWN_ORDER_TYPE:未知类型,需报错 + * REG_PAY:挂号支付 + * DIAG_PAY:诊间支付 + * COVID_EXAM_PAY:新冠检测费用(核酸) + * IN_HOSP_PAY:住院费支付 + * PHARMACY_PAY:药店支付 + * INSURANCE_PAY:保险费支付 + * INT_REG_PAY:互联网医院挂号支付 + * INT_RE_DIAG_PAY:互联网医院复诊支付 + * INT_RX_PAY:互联网医院处方支付 + * COVID_ANTIGEN_PAY:新冠抗原检测 + * MED_PAY:药费支付 + *+ */ + @SerializedName(value = "order_type") + private String orderType; + + /** + *
+ * 字段名:用户标识 + * 变量名:openid + * 是否必填:否 + * 类型:string(128) + * 描述: + * 用户在appid下的唯一标识 + *+ */ + @SerializedName(value = "openid") + private String openid; + + /** + *
+ * 字段名:用户子标识 + * 变量名:sub_openid + * 是否必填:否 + * 类型:string(128) + * 描述: + * 用户在sub_appid下的唯一标识 + *+ */ + @SerializedName(value = "sub_openid") + private String subOpenid; + + /** + *
+ * 字段名:是否代亲属支付 + * 变量名:pay_for_relatives + * 是否必填:否 + * 类型:bool + * 描述: + * 是否代亲属支付,不传默认替本人支付 + *+ */ + @SerializedName(value = "pay_for_relatives") + private Boolean payForRelatives; + + /** + *
+ * 字段名:医疗机构订单号 + * 变量名:serial_no + * 是否必填:否 + * 类型:string(20) + * 描述: + * 医疗机构订单号 + *+ */ + @SerializedName(value = "serial_no") + private String serialNo; + + /** + *
+ * 字段名:医保局支付单ID + * 变量名:pay_order_id + * 是否必填:否 + * 类型:string(64) + * 描述: + * 医保局返回的支付单ID + *+ */ + @SerializedName(value = "pay_order_id") + private String payOrderId; + + /** + *
+ * 字段名:医保局支付授权码 + * 变量名:pay_auth_no + * 是否必填:否 + * 类型:string(40) + * 描述: + * 医保局返回的支付授权码 + *+ */ + @SerializedName(value = "pay_auth_no") + private String payAuthNo; + + /** + *
+ * 字段名:用户定位信息 + * 变量名:geo_location + * 是否必填:否 + * 类型:string(40) + * 描述: + * 用户定位信息,经纬度。格式:经度,纬度 + *+ */ + @SerializedName(value = "geo_location") + private String geoLocation; + + /** + *
+ * 字段名:城市ID + * 变量名:city_id + * 是否必填:是 + * 类型:string(8) + * 描述: + * 城市ID + *+ */ + @SerializedName(value = "city_id") + private String cityId; + + /** + *
+ * 字段名:医疗机构名称 + * 变量名:med_inst_name + * 是否必填:是 + * 类型:string(128) + * 描述: + * 医疗机构名称 + *+ */ + @SerializedName(value = "med_inst_name") + private String medInstName; + + /** + *
+ * 字段名:医疗机构编码 + * 变量名:med_inst_no + * 是否必填:是 + * 类型:string(32) + * 描述: + * 医疗机构编码 + *+ */ + @SerializedName(value = "med_inst_no") + private String medInstNo; + + /** + *
+ * 字段名:微信支付订单号 + * 变量名:transaction_id + * 是否必填:是 + * 类型:string(32) + * 描述: + * 微信支付系统生成的订单号 + *+ */ + @SerializedName(value = "transaction_id") + private String transactionId; + + /** + *
+ * 字段名:医保订单创建时间 + * 变量名:med_ins_order_create_time + * 是否必填:是 + * 类型:string(64) + * 描述: + * 医保订单创建时间,遵循rfc3339标准格式 + *+ */ + @SerializedName(value = "med_ins_order_create_time") + private String medInsOrderCreateTime; + + /** + *
+ * 字段名:医保订单完成时间 + * 变量名:med_ins_order_finish_time + * 是否必填:是 + * 类型:string(64) + * 描述: + * 医保订单完成时间,遵循rfc3339标准格式 + *+ */ + @SerializedName(value = "med_ins_order_finish_time") + private String medInsOrderFinishTime; + + /** + *
+ * 字段名:总金额 + * 变量名:total_fee + * 是否必填:否 + * 类型:long + * 描述: + * 总金额,单位为分 + *+ */ + @SerializedName(value = "total_fee") + private Long totalFee; + + /** + *
+ * 字段名:医保统筹基金支付金额 + * 变量名:med_ins_gov_fee + * 是否必填:否 + * 类型:long + * 描述: + * 医保统筹基金支付金额,单位为分 + *+ */ + @SerializedName(value = "med_ins_gov_fee") + private Long medInsGovFee; + + /** + *
+ * 字段名:医保个人账户支付金额 + * 变量名:med_ins_self_fee + * 是否必填:否 + * 类型:long + * 描述: + * 医保个人账户支付金额,单位为分 + *+ */ + @SerializedName(value = "med_ins_self_fee") + private Long medInsSelfFee; + + /** + *
+ * 字段名:医保其他基金支付金额 + * 变量名:med_ins_other_fee + * 是否必填:否 + * 类型:long + * 描述: + * 医保其他基金支付金额,单位为分 + *+ */ + @SerializedName(value = "med_ins_other_fee") + private Long medInsOtherFee; + + /** + *
+ * 字段名:医保现金支付金额 + * 变量名:med_ins_cash_fee + * 是否必填:否 + * 类型:long + * 描述: + * 医保现金支付金额,单位为分 + *+ */ + @SerializedName(value = "med_ins_cash_fee") + private Long medInsCashFee; + + /** + *
+ * 字段名:微信支付现金支付金额 + * 变量名:wechat_pay_cash_fee + * 是否必填:否 + * 类型:long + * 描述: + * 微信支付现金支付金额,单位为分 + *+ */ + @SerializedName(value = "wechat_pay_cash_fee") + private Long wechatPayCashFee; + + /** + *
+ * 字段名:附加数据 + * 变量名:attach + * 是否必填:否 + * 类型:string(128) + * 描述: + * 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用 + *+ */ + @SerializedName(value = "attach") + private String attach; + + /** + *
+ * 字段名:支付状态 + * 变量名:trade_state + * 是否必填:是 + * 类型:string(32) + * 描述: + * 交易状态,枚举值: + * SUCCESS:支付成功 + * REFUND:转入退款 + * NOTPAY:未支付 + * CLOSED:已关闭 + * REVOKED:已撤销 + * USERPAYING:用户支付中 + * PAYERROR:支付失败 + *+ */ + @SerializedName(value = "trade_state") + private String tradeState; + + /** + *
+ * 字段名:支付状态描述 + * 变量名:trade_state_desc + * 是否必填:是 + * 类型:string(256) + * 描述: + * 交易状态描述 + *+ */ + @SerializedName(value = "trade_state_desc") + private String tradeStateDesc; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayBaseNotifyV3Result.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayBaseNotifyV3Result.java index 86915d0956..364c9080d8 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayBaseNotifyV3Result.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayBaseNotifyV3Result.java @@ -5,7 +5,8 @@ * * @author Pursuer * @version 1.0 - * @date 2023/6/15 + * @since 2023/6/15 + * @param
+ * 微信支付实名验证请求对象. + * 详见文档:https://pay.wechatpay.cn/doc/v2/merchant/4011987607 + *+ * + * @author Binary Wang + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Builder(builderMethodName = "newBuilder") +@NoArgsConstructor +@AllArgsConstructor +@XStreamAlias("xml") +public class RealNameRequest extends BaseWxPayRequest { + private static final long serialVersionUID = 1L; + + /** + *
+ * 字段名:用户标识 + * 变量名:openid + * 是否必填:是 + * 类型:String(128) + * 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o + * 描述:用户在商户appid下的唯一标识 + *+ */ + @Required + @XStreamAlias("openid") + private String openid; + + @Override + protected void checkConstraints() { + //do nothing + } + + @Override + protected void storeMap(Map
+ * 微信支付实名验证返回结果. + * 详见文档:https://pay.wechatpay.cn/doc/v2/merchant/4011987607 + *+ * + * @author Binary Wang + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@XStreamAlias("xml") +public class RealNameResult extends BaseWxPayResult implements Serializable { + private static final long serialVersionUID = 1L; + + /** + *
+ * 字段名:用户标识 + * 变量名:openid + * 是否必填:否 + * 类型:String(128) + * 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o + * 描述:用户在商户appid下的唯一标识 + *+ */ + @XStreamAlias("openid") + private String openid; + + /** + *
+ * 字段名:实名认证状态 + * 变量名:is_certified + * 是否必填:是 + * 类型:String(1) + * 示例值:Y + * 描述:Y-已实名认证 N-未实名认证 + *+ */ + @XStreamAlias("is_certified") + private String isCertified; + + /** + *
+ * 字段名:实名认证信息 + * 变量名:cert_info + * 是否必填:否 + * 类型:String(256) + * 示例值: + * 描述:实名认证的相关信息,如姓名等(加密) + *+ */ + @XStreamAlias("cert_info") + private String certInfo; + + /** + *
+ * 字段名:引导链接 + * 变量名:guide_url + * 是否必填:否 + * 类型:String(256) + * 示例值: + * 描述:未实名时,引导用户进行实名认证的URL + *+ */ + @XStreamAlias("guide_url") + private String guideUrl; + + /** + * 从XML结构中加载额外的属性 + * + * @param d Document + */ + @Override + protected void loadXml(Document d) { + openid = readXmlString(d, "openid"); + isCertified = readXmlString(d, "is_certified"); + certInfo = readXmlString(d, "cert_info"); + guideUrl = readXmlString(d, "guide_url"); + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayCodepayRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayCodepayRequest.java index ecfa614a16..632561075a 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayCodepayRequest.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayCodepayRequest.java @@ -45,6 +45,58 @@ public class WxPayCodepayRequest implements Serializable { */ @SerializedName(value = "mchid") protected String mchid; + /** + *
+ * 字段名:服务商应用ID + * 变量名:sp_appid + * 是否必填:否 + * 类型:string[1,32] + * 描述: + * 服务商模式下使用,由微信生成的应用ID,全局唯一。 + * 示例值:wxd678efh567hg6787 + *+ */ + @SerializedName(value = "sp_appid") + protected String spAppid; + /** + *
+ * 字段名:服务商商户号 + * 变量名:sp_mchid + * 是否必填:否 + * 类型:string[1,32] + * 描述: + * 服务商模式下使用,服务商商户号,由微信支付生成并下发。 + * 示例值:1230000109 + *+ */ + @SerializedName(value = "sp_mchid") + protected String spMchid; + /** + *
+ * 字段名:子商户应用ID + * 变量名:sub_appid + * 是否必填:否 + * 类型:string[1,32] + * 描述: + * 服务商模式下使用,由微信生成的应用ID,全局唯一。 + * 示例值:wxd678efh567hg6787 + *+ */ + @SerializedName(value = "sub_appid") + protected String subAppid; + /** + *
+ * 字段名:子商户商户号 + * 变量名:sub_mchid + * 是否必填:否 + * 类型:string[1,32] + * 描述: + * 服务商模式下使用,子商户商户号,由微信支付生成并下发。 + * 示例值:1230000109 + *+ */ + @SerializedName(value = "sub_mchid") + protected String subMchid; /** *
* 字段名:商品描述
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3Request.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3Request.java
index 8f3e8ebd10..a565388e60 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3Request.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3Request.java
@@ -11,15 +11,38 @@
* 微信支付服务商退款请求
* 文档见:https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_1_9.shtml
*
- * @author Pursuer
- * @version 1.0
- * @date 2023/3/2
*/
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class WxPayPartnerRefundV3Request extends WxPayRefundV3Request implements Serializable {
private static final long serialVersionUID = -1L;
+ /**
+ *
+ * 字段名:服务商应用ID
+ * 变量名:sp_appid
+ * 是否必填:是
+ * 类型:string[1, 32]
+ * 描述:
+ * 服务商申请的公众号或移动应用appid。
+ * 示例值:wx8888888888888888
+ *
+ */
+ @SerializedName(value = "sp_appid")
+ private String spAppid;
+ /**
+ *
+ * 字段名:子商户应用ID
+ * 变量名:sub_appid
+ * 是否必填:否
+ * 类型:string[1, 32]
+ * 描述:
+ * 子商户申请的公众号或移动应用appid。如果传了sub_appid,那sub_appid对应的订单必须存在。
+ * 示例值:wx8888888888888888
+ *
+ */
+ @SerializedName(value = "sub_appid")
+ private String subAppid;
/**
*
* 字段名:退款资金来源
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayRefundRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayRefundRequest.java
index e145644d91..b0cbcf4e70 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayRefundRequest.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/request/WxPayRefundRequest.java
@@ -230,7 +230,9 @@ public void checkAndSign(WxPayConfig config) throws WxPayException {
if (StringUtils.isBlank(this.getOpUserId())) {
this.setOpUserId(config.getMchId());
}
-
+ if (StringUtils.isBlank(this.getNotifyUrl())) {
+ this.setNotifyUrl(config.getRefundNotifyUrl());
+ }
super.checkAndSign(config);
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxPayUnifiedOrderV3Result.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxPayUnifiedOrderV3Result.java
index 309fb8e752..5b60f3b520 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxPayUnifiedOrderV3Result.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxPayUnifiedOrderV3Result.java
@@ -58,7 +58,7 @@ public class WxPayUnifiedOrderV3Result implements Serializable {
/**
*
* 字段名:二维码链接(NATIVE支付 会返回)
- * 变量名:h5_url
+ * 变量名:code_url
* 是否必填:是
* 类型:string[1,512]
* 描述:
@@ -81,6 +81,19 @@ public static class JsapiResult implements Serializable {
private String packageValue;
private String signType;
private String paySign;
+ /**
+ *
+ * 字段名:预支付交易会话标识
+ * 变量名:prepay_id
+ * 是否必填:否(用户可选存储)
+ * 类型:string[1,64]
+ * 描述:
+ * 预支付交易会话标识。用于后续接口调用中使用,该值有效期为2小时
+ * 此字段用于支持用户存储prepay_id,以便复用和重新生成支付签名
+ * 示例值:wx201410272009395522657a690389285100
+ *
+ */
+ private String prepayId;
private String getSignStr() {
return String.format("%s\n%s\n%s\n%s\n", appId, timeStamp, nonceStr, packageValue);
@@ -106,30 +119,123 @@ private String getSignStr() {
}
public T getPayInfo(TradeTypeEnum tradeType, String appId, String mchId, PrivateKey privateKey) {
- String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
- String nonceStr = SignUtils.genRandomStr();
switch (tradeType) {
case JSAPI:
- JsapiResult jsapiResult = new JsapiResult();
- jsapiResult.setAppId(appId).setTimeStamp(timestamp)
- .setPackageValue("prepay_id=" + this.prepayId).setNonceStr(nonceStr)
- //签名类型,默认为RSA,仅支持RSA。
- .setSignType("RSA").setPaySign(SignUtils.sign(jsapiResult.getSignStr(), privateKey));
- return (T) jsapiResult;
+ return (T) buildJsapiResult(this.prepayId, appId, privateKey);
case H5:
return (T) this.h5Url;
case APP:
- AppResult appResult = new AppResult();
- appResult.setAppid(appId).setPrepayId(this.prepayId).setPartnerId(mchId)
- .setNoncestr(nonceStr).setTimestamp(timestamp)
- //暂填写固定值Sign=WXPay
- .setPackageValue("Sign=WXPay")
- .setSign(SignUtils.sign(appResult.getSignStr(), privateKey));
- return (T) appResult;
+ return (T) buildAppResult(this.prepayId, appId, mchId, privateKey);
case NATIVE:
return (T) this.codeUrl;
default:
throw new WxRuntimeException("不支持的支付类型");
}
}
+
+ /**
+ *
+ * 根据已有的prepay_id生成JSAPI支付所需的参数对象(解耦版本)
+ * 应用场景:
+ * 1. 用户已经通过createPartnerOrderV3或unifiedPartnerOrderV3获取了prepay_id
+ * 2. 用户希望存储prepay_id用于后续复用
+ * 3. 支付失败后,使用存储的prepay_id重新生成支付签名信息
+ *
+ * 使用示例:
+ * // 步骤1:创建订单并获取prepay_id
+ * WxPayUnifiedOrderV3Result result = wxPayService.unifiedPartnerOrderV3(TradeTypeEnum.JSAPI, request);
+ * String prepayId = result.getPrepayId();
+ * // 存储prepayId到数据库...
+ *
+ * // 步骤2:需要支付时,使用存储的prepay_id生成支付信息
+ * WxPayUnifiedOrderV3Result.JsapiResult payInfo = WxPayUnifiedOrderV3Result.getJsapiPayInfo(
+ * prepayId, appId, wxPayService.getConfig().getPrivateKey()
+ * );
+ *
+ *
+ * @param prepayId 预支付交易会话标识
+ * @param appId 应用ID
+ * @param privateKey 商户私钥,用于签名
+ * @return JSAPI支付所需的参数对象
+ */
+ public static JsapiResult getJsapiPayInfo(String prepayId, String appId, PrivateKey privateKey) {
+ if (prepayId == null || appId == null || privateKey == null) {
+ throw new IllegalArgumentException("prepayId, appId 和 privateKey 不能为空");
+ }
+ return buildJsapiResult(prepayId, appId, privateKey);
+ }
+
+ /**
+ *
+ * 根据已有的prepay_id生成APP支付所需的参数对象(解耦版本)
+ * 应用场景:
+ * 1. 用户已经通过createPartnerOrderV3或unifiedPartnerOrderV3获取了prepay_id
+ * 2. 用户希望存储prepay_id用于后续复用
+ * 3. 支付失败后,使用存储的prepay_id重新生成支付签名信息
+ *
+ * 使用示例:
+ * // 步骤1:创建订单并获取prepay_id
+ * WxPayUnifiedOrderV3Result result = wxPayService.unifiedPartnerOrderV3(TradeTypeEnum.APP, request);
+ * String prepayId = result.getPrepayId();
+ * // 存储prepayId到数据库...
+ *
+ * // 步骤2:需要支付时,使用存储的prepay_id生成支付信息
+ * WxPayUnifiedOrderV3Result.AppResult payInfo = WxPayUnifiedOrderV3Result.getAppPayInfo(
+ * prepayId, appId, mchId, wxPayService.getConfig().getPrivateKey()
+ * );
+ *
+ *
+ * @param prepayId 预支付交易会话标识
+ * @param appId 应用ID
+ * @param mchId 商户号
+ * @param privateKey 商户私钥,用于签名
+ * @return APP支付所需的参数对象
+ */
+ public static AppResult getAppPayInfo(String prepayId, String appId, String mchId, PrivateKey privateKey) {
+ if (prepayId == null || appId == null || mchId == null || privateKey == null) {
+ throw new IllegalArgumentException("prepayId, appId, mchId 和 privateKey 不能为空");
+ }
+ return buildAppResult(prepayId, appId, mchId, privateKey);
+ }
+
+ /**
+ * 构建JSAPI支付结果对象
+ *
+ * @param prepayId 预支付交易会话标识
+ * @param appId 应用ID
+ * @param privateKey 商户私钥,用于签名
+ * @return JSAPI支付所需的参数对象
+ */
+ private static JsapiResult buildJsapiResult(String prepayId, String appId, PrivateKey privateKey) {
+ String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
+ String nonceStr = SignUtils.genRandomStr();
+ JsapiResult jsapiResult = new JsapiResult();
+ jsapiResult.setAppId(appId).setTimeStamp(timestamp)
+ .setPackageValue("prepay_id=" + prepayId).setNonceStr(nonceStr)
+ .setPrepayId(prepayId)
+ //签名类型,默认为RSA,仅支持RSA。
+ .setSignType("RSA").setPaySign(SignUtils.sign(jsapiResult.getSignStr(), privateKey));
+ return jsapiResult;
+ }
+
+ /**
+ * 构建APP支付结果对象
+ *
+ * @param prepayId 预支付交易会话标识
+ * @param appId 应用ID
+ * @param mchId 商户号
+ * @param privateKey 商户私钥,用于签名
+ * @return APP支付所需的参数对象
+ */
+ private static AppResult buildAppResult(String prepayId, String appId, String mchId, PrivateKey privateKey) {
+ String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
+ String nonceStr = SignUtils.genRandomStr();
+ AppResult appResult = new AppResult();
+ appResult.setAppid(appId).setPrepayId(prepayId).setPartnerId(mchId)
+ .setNoncestr(nonceStr).setTimestamp(timestamp)
+ //暂填写固定值Sign=WXPay
+ .setPackageValue("Sign=WXPay")
+ .setSign(SignUtils.sign(appResult.getSignStr(), privateKey));
+ return appResult;
+ }
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxSignQueryResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxSignQueryResult.java
index 808b9d7ddf..af19aec60a 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxSignQueryResult.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/result/WxSignQueryResult.java
@@ -86,7 +86,7 @@ public class WxSignQueryResult extends BaseWxPayResult implements Serializable {
* 协议解约方式
* 非必传
*/
- @XStreamAlias("contract_terminated_mode")
+ @XStreamAlias("contract_termination_mode")
private Integer contractTerminatedMode;
/**
@@ -112,9 +112,9 @@ protected void loadXml(Document d) {
contractDisplayAccount = readXmlString(d, "contract_display_account");
contractState = readXmlInteger(d, "contract_state");
contractSignedTime = readXmlString(d, "contract_signed_time");
- contractExpiredTime = readXmlString(d, "contrace_Expired_time");
+ contractExpiredTime = readXmlString(d, "contract_expired_time");
contractTerminatedTime = readXmlString(d, "contract_terminated_time");
- contractTerminatedMode = readXmlInteger(d, "contract_terminate_mode");
+ contractTerminatedMode = readXmlInteger(d, "contract_termination_mode");
contractTerminationRemark = readXmlString(d, "contract_termination_remark");
openId = readXmlString(d, "openid");
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferRequest.java
index 91d9438833..0129798ed9 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferRequest.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferRequest.java
@@ -6,8 +6,10 @@
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
import java.io.Serializable;
+import java.util.List;
/**
* 运营工具-商家转账请求参数
@@ -37,11 +39,13 @@ public class BusinessOperationTransferRequest implements Serializable {
private String outBillNo;
/**
- * 运营工具转账场景ID
- * 必须,用于标识运营工具转账的具体业务场景
+ * 转账场景ID
+ * 必须,该笔转账使用的转账场景,可前往"商户平台-产品中心-商家转账"中申请。
+ * 运营工具场景ID如:2001(现金营销)、2002(佣金报酬)、2003(推广奖励)等
+ * 可使用 {@link com.github.binarywang.wxpay.constant.WxPayConstants.OperationSceneId} 中定义的常量
*/
- @SerializedName("operation_scene_id")
- private String operationSceneId;
+ @SerializedName("transfer_scene_id")
+ private String transferSceneId;
/**
* 用户在直连商户应用下的用户标示
@@ -86,4 +90,36 @@ public class BusinessOperationTransferRequest implements Serializable {
*/
@SerializedName("notify_url")
private String notifyUrl;
-}
\ No newline at end of file
+
+ /**
+ * 转账场景报备信息
+ * 必须,需按转账场景准确填写报备信息,参考 转账场景报备信息字段说明
+ */
+ @SerializedName("transfer_scene_report_infos")
+ private List transferSceneReportInfos;
+
+ /**
+ * 转账场景报备信息
+ */
+ @Data
+ @Accessors(chain = true)
+ public static class TransferSceneReportInfo implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 信息类型
+ * 必须,不能超过15个字符,商户所属转账场景下的信息类型,此字段内容为固定值,需严格按照 转账场景报备信息字段说明 传参。
+ */
+ @SerializedName("info_type")
+ private String infoType;
+
+ /**
+ * 信息内容
+ * 必须,不能超过32个字符,商户所属转账场景下的信息内容,商户可按实际业务场景自定义传参,需严格按照 转账场景报备信息字段说明 传参。
+ */
+ @SerializedName("info_content")
+ private String infoContent;
+
+ }
+
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferResult.java
index a380d6133e..91771b43e1 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferResult.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/BusinessOperationTransferResult.java
@@ -31,15 +31,27 @@ public class BusinessOperationTransferResult implements Serializable {
private String transferBillNo;
/**
- * 转账状态
- * WAIT_PAY:等待确认
- * PROCESSING:转账中
- * SUCCESS:转账成功
- * FAIL:转账失败
- * REFUND:已退款
+ * 单据状态
+ * 商家转账订单状态
+ * ACCEPTED:转账已受理,可原单重试(非终态)。
+ * PROCESSING: 转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试(非终态)。
+ * WAIT_USER_CONFIRM: 待收款用户确认,当前转账单据资金已锁定,可拉起微信收款确认页面进行收款确认(非终态)。
+ * TRANSFERING: 转账中,可拉起微信收款确认页面再次重试确认收款(非终态)。
+ * SUCCESS: 转账成功,表示转账单据已成功(终态)。
+ * FAIL: 转账失败,表示该笔转账单据已失败。若需重新向用户转账,请重新生成单据并再次发起(终态)。
+ * CANCELING: 转账撤销中,商户撤销请求受理成功,该笔转账正在撤销中,需查单确认撤销的转账单据状态(非终态)。
+ * CANCELLED: 转账撤销完成,代表转账单据已撤销成功(终态)。
*/
- @SerializedName("transfer_state")
- private String transferState;
+ @SerializedName("state")
+ private String state;
+
+ /**
+ * 跳转领取页面的package信息
+ * 跳转微信支付收款页的package信息, APP调起用户确认收款 或者 JSAPI调起用户确认收款 时需要使用的参数。仅当转账单据状态为WAIT_USER_CONFIRM时返回。
+ * 单据创建后,用户24小时内不领取将过期关闭,建议拉起用户确认收款页面前,先查单据状态:如单据状态为WAIT_USER_CONFIRM,可用之前的package信息拉起;单据到终态时需更换单号重新发起转账。
+ */
+ @SerializedName("package_info")
+ private String packageInfo;
/**
* 发起转账的时间
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/TransferBillsRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/TransferBillsRequest.java
index 230e564e4b..2ac4b08c93 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/TransferBillsRequest.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/TransferBillsRequest.java
@@ -87,6 +87,26 @@ public class TransferBillsRequest implements Serializable {
@SerializedName("transfer_scene_report_infos")
private List transferSceneReportInfos;
+ /**
+ * 收款授权模式
+ *
+ * 字段名:收款授权模式
+ * 变量名:receipt_authorization_mode
+ * 是否必填:否
+ * 类型:string
+ * 描述:
+ * 控制收款方式的授权模式,可选值:
+ * - CONFIRM_RECEIPT_AUTHORIZATION:需确认收款授权模式(默认值)
+ * - NO_CONFIRM_RECEIPT_AUTHORIZATION:免确认收款授权模式(需要用户事先授权)
+ * 为空时,默认为需确认收款授权模式
+ * 示例值:NO_CONFIRM_RECEIPT_AUTHORIZATION
+ *
+ *
+ * @see com.github.binarywang.wxpay.constant.WxPayConstants.ReceiptAuthorizationMode
+ */
+ @SerializedName("receipt_authorization_mode")
+ private String receiptAuthorizationMode;
+
@Data
@Builder(builderMethodName = "newBuilder")
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java
index f4a1c3d008..1e0e8d2c46 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java
@@ -32,6 +32,8 @@
import javax.net.ssl.SSLContext;
import java.io.*;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
@@ -97,9 +99,13 @@ public class WxPayConfig {
*/
private String subMchId;
/**
- * 微信支付异步回掉地址,通知url必须为直接可访问的url,不能携带参数.
+ * 微信支付异步回调地址,通知url必须为直接可访问的url,不能携带参数.
*/
private String notifyUrl;
+ /**
+ * 退款结果异步回调地址,通知url必须为直接可访问的url,不能携带参数.
+ */
+ private String refundNotifyUrl;
/**
* 交易类型.
*
@@ -325,7 +331,8 @@ public SSLContext initSSLContext() throws WxPayException {
*
* @return org.apache.http.impl.client.CloseableHttpClient
* @author doger.wang
- **/
+ * @throws WxPayException 微信支付异常
+ */
public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
if (StringUtils.isBlank(this.getApiV3Key())) {
throw new WxPayException("请确保apiV3Key值已设置");
@@ -343,7 +350,7 @@ public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
certificate = (X509Certificate) objects[1];
this.certSerialNo = certificate.getSerialNumber().toString(16).toUpperCase();
}
- if (certificate == null && StringUtils.isBlank(this.getCertSerialNo()) && (StringUtils.isNotBlank(this.getPrivateCertPath()) || StringUtils.isNotBlank(this.getPrivateCertString())) || this.getPrivateCertContent() != null) {
+ if (certificate == null && StringUtils.isBlank(this.getCertSerialNo()) && (StringUtils.isNotBlank(this.getPrivateCertPath()) || StringUtils.isNotBlank(this.getPrivateCertString()) || this.getPrivateCertContent() != null)) {
try (InputStream certInputStream = this.loadConfigInputStream(this.getPrivateCertString(), this.getPrivateCertPath(),
this.privateCertContent, "privateCertPath")) {
certificate = PemUtils.loadCertificate(certInputStream);
@@ -390,6 +397,19 @@ public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
WxPayV3HttpClientBuilder wxPayV3HttpClientBuilder = WxPayV3HttpClientBuilder.create()
.withMerchant(mchId, certSerialNo, merchantPrivateKey)
.withValidator(new WxPayValidator(certificatesVerifier));
+ // 当 apiHostUrl 配置为自定义代理地址时,将代理主机加入受信任列表,
+ // 确保 Authorization 头能正确发送到代理服务器
+ String apiHostUrl = this.getApiHostUrl();
+ if (StringUtils.isNotBlank(apiHostUrl)) {
+ try {
+ String host = new URI(apiHostUrl).getHost();
+ if (host != null && !host.endsWith(".mch.weixin.qq.com")) {
+ wxPayV3HttpClientBuilder.withTrustedHost(host);
+ }
+ } catch (URISyntaxException e) {
+ log.warn("解析 apiHostUrl [{}] 中的主机名失败: {}", apiHostUrl, e.getMessage());
+ }
+ }
//初始化V3接口正向代理设置
HttpProxyUtils.initHttpProxy(wxPayV3HttpClientBuilder, wxPayHttpProxy);
@@ -663,6 +683,8 @@ public CloseableHttpClient initSslHttpClient() throws WxPayException {
/**
* 配置HTTP代理
+ *
+ * @param httpClientBuilder HttpClient构建器
*/
private void configureProxy(org.apache.http.impl.client.HttpClientBuilder httpClientBuilder) {
if (StringUtils.isNotBlank(this.getHttpProxyHost()) && this.getHttpProxyPort() > 0) {
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/constant/WxPayConstants.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/constant/WxPayConstants.java
index ec9e14ff2d..2b736691b7 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/constant/WxPayConstants.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/constant/WxPayConstants.java
@@ -524,4 +524,25 @@ public static class CASH_MARKETING {
}
}
+
+ /**
+ * 收款授权模式
+ *
+ * @see 官方文档
+ */
+ @UtilityClass
+ public static class ReceiptAuthorizationMode {
+ /**
+ * 需确认收款授权模式(默认值)
+ * 用户需要手动确认才能收款
+ */
+ public static final String CONFIRM_RECEIPT_AUTHORIZATION = "CONFIRM_RECEIPT_AUTHORIZATION";
+
+ /**
+ * 免确认收款授权模式
+ * 用户授权后,收款不需要确认,转账直接到账
+ */
+ public static final String NO_CONFIRM_RECEIPT_AUTHORIZATION = "NO_CONFIRM_RECEIPT_AUTHORIZATION";
+ }
+
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/BusinessOperationTransferExample.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/BusinessOperationTransferExample.java
index d11738816b..117395ba62 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/BusinessOperationTransferExample.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/BusinessOperationTransferExample.java
@@ -8,6 +8,8 @@
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import java.util.Arrays;
+
/**
* 运营工具-商家转账API使用示例
*
@@ -41,10 +43,15 @@ public void init() {
public void createOperationTransferExample() {
try {
// 构建转账请求
+ BusinessOperationTransferRequest.TransferSceneReportInfo reportInfo = new BusinessOperationTransferRequest.TransferSceneReportInfo();
+ reportInfo.setInfoType("活动名称");
+ reportInfo.setInfoContent("新会员有礼");
+
BusinessOperationTransferRequest request = BusinessOperationTransferRequest.newBuilder()
.appid("your_app_id") // 应用ID
.outBillNo("OT" + System.currentTimeMillis()) // 商户转账单号
- .operationSceneId(WxPayConstants.OperationSceneId.OPERATION_CASH_MARKETING) // 运营工具转账场景ID
+ .transferSceneId(WxPayConstants.OperationSceneId.OPERATION_CASH_MARKETING) // 运营工具转账场景ID
+ .transferSceneReportInfos(Arrays.asList(reportInfo)) // 转账场景报备信息
.openid("user_openid") // 用户openid
.userName("张三") // 用户姓名(可选)
.transferAmount(100) // 转账金额,单位分
@@ -59,7 +66,8 @@ public void createOperationTransferExample() {
System.out.println("转账成功!");
System.out.println("商户单号: " + result.getOutBillNo());
System.out.println("微信转账单号: " + result.getTransferBillNo());
- System.out.println("转账状态: " + result.getTransferState());
+ System.out.println("单据状态: " + result.getState());
+ System.out.println("跳转领取页面的package信息: " + result.getPackageInfo());
System.out.println("创建时间: " + result.getCreateTime());
} catch (WxPayException e) {
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/NewTransferApiExample.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/NewTransferApiExample.java
index 8d74e5a4ef..228234d589 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/NewTransferApiExample.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/NewTransferApiExample.java
@@ -3,6 +3,7 @@
import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.bean.transfer.*;
import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.TransferService;
import com.github.binarywang.wxpay.service.WxPayService;
@@ -215,6 +216,100 @@ public void batchTransferExample() {
}
}
+ /**
+ * 使用免确认收款授权模式进行转账示例
+ * 注意:使用此模式前,用户需要先进行授权
+ */
+ public void transferWithNoConfirmAuthModeExample() {
+ try {
+ // 构建转账请求,使用免确认收款授权模式
+ TransferBillsRequest request = TransferBillsRequest.newBuilder()
+ .appid("wx1234567890123456")
+ .outBillNo("NO_CONFIRM_" + System.currentTimeMillis()) // 商户转账单号
+ .transferSceneId("1005") // 转账场景ID(佣金报酬)
+ .openid("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o") // 收款用户的openid
+ .transferAmount(200) // 转账金额,单位:分(此处为2元)
+ .transferRemark("免确认收款转账") // 转账备注
+ .receiptAuthorizationMode(WxPayConstants.ReceiptAuthorizationMode.NO_CONFIRM_RECEIPT_AUTHORIZATION)
+ .userRecvPerception("Y") // 用户收款感知
+ .build();
+
+ // 发起转账
+ TransferBillsResult result = transferService.transferBills(request);
+
+ System.out.println("=== 免确认授权模式转账成功 ===");
+ System.out.println("商户单号: " + result.getOutBillNo());
+ System.out.println("微信转账单号: " + result.getTransferBillNo());
+ System.out.println("状态: " + result.getState());
+ System.out.println("说明: 使用免确认授权模式,转账直接到账,无需用户确认");
+
+ } catch (WxPayException e) {
+ System.err.println("免确认授权转账失败: " + e.getMessage());
+ System.err.println("错误代码: " + e.getErrCode());
+
+ // 可能的错误原因
+ if ("USER_NOT_AUTHORIZED".equals(e.getErrCode())) {
+ System.err.println("用户未授权免确认收款,请先引导用户进行授权");
+ }
+ }
+ }
+
+ /**
+ * 使用需确认收款授权模式进行转账示例(默认模式)
+ */
+ public void transferWithConfirmAuthModeExample() {
+ try {
+ // 构建转账请求,显式设置为需确认收款授权模式
+ TransferBillsRequest request = TransferBillsRequest.newBuilder()
+ .appid("wx1234567890123456")
+ .outBillNo("CONFIRM_" + System.currentTimeMillis()) // 商户转账单号
+ .transferSceneId("1005") // 转账场景ID
+ .openid("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o") // 收款用户的openid
+ .transferAmount(150) // 转账金额,单位:分(此处为1.5元)
+ .transferRemark("需确认收款转账") // 转账备注
+ .receiptAuthorizationMode(WxPayConstants.ReceiptAuthorizationMode.CONFIRM_RECEIPT_AUTHORIZATION)
+ .userRecvPerception("Y") // 用户收款感知
+ .build();
+
+ // 发起转账
+ TransferBillsResult result = transferService.transferBills(request);
+
+ System.out.println("=== 需确认授权模式转账成功 ===");
+ System.out.println("商户单号: " + result.getOutBillNo());
+ System.out.println("微信转账单号: " + result.getTransferBillNo());
+ System.out.println("状态: " + result.getState());
+ System.out.println("packageInfo: " + result.getPackageInfo());
+ System.out.println("说明: 使用需确认授权模式,用户需要手动确认才能收款");
+
+ } catch (WxPayException e) {
+ System.err.println("需确认授权转账失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 权限模式对比示例
+ * 展示两种权限模式的区别和使用场景
+ */
+ public void authModeComparisonExample() {
+ System.out.println("\n=== 收款授权模式对比 ===");
+ System.out.println("1. 需确认收款授权模式 (CONFIRM_RECEIPT_AUTHORIZATION):");
+ System.out.println(" - 这是默认模式");
+ System.out.println(" - 用户收到转账后需要手动点击确认才能到账");
+ System.out.println(" - 适用于一般的转账场景");
+ System.out.println(" - 转账状态可能包含 WAIT_USER_CONFIRM 等待确认状态");
+
+ System.out.println("\n2. 免确认收款授权模式 (NO_CONFIRM_RECEIPT_AUTHORIZATION):");
+ System.out.println(" - 用户事先授权后,转账直接到账,无需确认");
+ System.out.println(" - 提升用户体验,减少操作步骤");
+ System.out.println(" - 适用于高频转账场景,如佣金发放等");
+ System.out.println(" - 需要用户先进行授权,否则会返回授权错误");
+
+ System.out.println("\n使用建议:");
+ System.out.println("- 高频业务场景推荐使用免确认模式,提升用户体验");
+ System.out.println("- 首次使用需引导用户进行授权");
+ System.out.println("- 处理授权相关异常,提供友好的错误提示");
+ }
+
/**
* 使用配置示例
*/
@@ -230,20 +325,29 @@ public static void main(String[] args) {
// 创建示例实例
NewTransferApiExample example = new NewTransferApiExample(config);
+ // 权限模式对比说明
+ example.authModeComparisonExample();
+
// 运行示例
System.out.println("新版商户转账API使用示例");
System.out.println("===============================");
- // 1. 发起单笔转账
+ // 1. 发起转账(使用免确认授权模式)
+ // example.transferWithNoConfirmAuthModeExample();
+
+ // 2. 发起转账(使用需确认授权模式)
+ // example.transferWithConfirmAuthModeExample();
+
+ // 3. 发起单笔转账(默认模式)
example.transferExample();
- // 2. 查询转账结果
+ // 4. 查询转账结果
// example.queryByOutBillNoExample();
- // 3. 撤销转账
+ // 5. 撤销转账
// example.cancelTransferExample();
- // 4. 批量转账(传统API)
+ // 6. 批量转账(传统API)
// example.batchTransferExample();
}
}
\ No newline at end of file
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessOperationTransferService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessOperationTransferService.java
index 740c2af83f..195d3a8409 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessOperationTransferService.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessOperationTransferService.java
@@ -21,7 +21,7 @@ public interface BusinessOperationTransferService {
* 发起运营工具商家转账
*
* 请求方式:POST(HTTPS)
- * 请求地址:https://api.mch.weixin.qq.com/v3/fund-app/operation/mch-transfer/transfer-bills
+ * 请求地址:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills
*
* 文档地址:运营工具-商家转账API
*
@@ -37,7 +37,7 @@ public interface BusinessOperationTransferService {
* 查询运营工具转账结果
*
* 请求方式:GET(HTTPS)
- * 请求地址:https://api.mch.weixin.qq.com/v3/fund-app/operation/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}
+ * 请求地址:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}
*
* 文档地址:运营工具-商家转账API
*
@@ -53,7 +53,7 @@ public interface BusinessOperationTransferService {
* 通过商户单号查询运营工具转账结果
*
* 请求方式:GET(HTTPS)
- * 请求地址:https://api.mch.weixin.qq.com/v3/fund-app/operation/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}
+ * 请求地址:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}
*
* 文档地址:运营工具-商家转账API
*
@@ -69,7 +69,7 @@ public interface BusinessOperationTransferService {
* 通过微信转账单号查询运营工具转账结果
*
* 请求方式:GET(HTTPS)
- * 请求地址:https://api.mch.weixin.qq.com/v3/fund-app/operation/mch-transfer/transfer-bills/transfer-bill-no/{transfer_bill_no}
+ * 请求地址:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/{transfer_bill_no}
*
* 文档地址:运营工具-商家转账API
*
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantMediaService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantMediaService.java
index 0e35dbb68b..f7f0aaaf3e 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantMediaService.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MerchantMediaService.java
@@ -1,6 +1,7 @@
package com.github.binarywang.wxpay.service;
import com.github.binarywang.wxpay.bean.media.ImageUploadResult;
+import com.github.binarywang.wxpay.bean.media.VideoUploadResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import java.io.File;
@@ -42,5 +43,34 @@ public interface MerchantMediaService {
*/
ImageUploadResult imageUploadV3(InputStream inputStream, String fileName) throws WxPayException, IOException;
+ /**
+ * + * 通用接口-视频上传API + * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/tool/chapter3_2.shtml + * 接口链接:https://api.mch.weixin.qq.com/v3/merchant/media/video_upload + *+ * + * @param videoFile 需要上传的视频文件 + * @return VideoUploadResult 微信返回的媒体文件标识Id。示例值:6uqyGjGrCf2GtyXP8bxrbuH9-aAoTjH-rKeSl3Lf4_So6kdkQu4w8BYVP3bzLtvR38lxt4PjtCDXsQpzqge_hQEovHzOhsLleGFQVRF-U_0 + * @throws WxPayException the wx pay exception + * @throws IOException the io exception + */ + VideoUploadResult videoUploadV3(File videoFile) throws WxPayException, IOException; + + /** + *
+ * 通用接口-视频上传API + * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/tool/chapter3_2.shtml + * 接口链接:https://api.mch.weixin.qq.com/v3/merchant/media/video_upload + * 注意:此方法会将整个视频流读入内存计算SHA256后再上传,大文件可能导致OOM,建议大文件使用File方式上传 + *+ * + * @param inputStream 需要上传的视频文件流 + * @param fileName 需要上传的视频文件名 + * @return VideoUploadResult 微信返回的媒体文件标识Id。示例值:6uqyGjGrCf2GtyXP8bxrbuH9-aAoTjH-rKeSl3Lf4_So6kdkQu4w8BYVP3bzLtvR38lxt4PjtCDXsQpzqge_hQEovHzOhsLleGFQVRF-U_0 + * @throws WxPayException the wx pay exception + * @throws IOException the io exception + */ + VideoUploadResult videoUploadV3(InputStream inputStream, String fileName) throws WxPayException, IOException; } diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MiPayService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MiPayService.java new file mode 100644 index 0000000000..5e2f678c16 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/MiPayService.java @@ -0,0 +1,95 @@ +package com.github.binarywang.wxpay.service; + +import com.github.binarywang.wxpay.bean.mipay.MedInsOrdersRequest; +import com.github.binarywang.wxpay.bean.mipay.MedInsOrdersResult; +import com.github.binarywang.wxpay.bean.mipay.MedInsRefundNotifyRequest; +import com.github.binarywang.wxpay.bean.notify.MiPayNotifyV3Result; +import com.github.binarywang.wxpay.bean.notify.SignatureHeader; +import com.github.binarywang.wxpay.exception.WxPayException; + +/** + * 医保相关接口 + * 医保相关接口 + * @author xgl + * @date 2025/12/20 + */ +public interface MiPayService { + + /** + *
+ * 医保自费混合收款下单 + * + * 从业机构调用该接口向微信医保后台下单 + * + * 文档地址:医保自费混合收款下单 + *+ * + * @param request 下单参数 + * @return ReservationTransferNotifyResult 下单结果 + * @throws WxPayException the wx pay exception + */ + MedInsOrdersResult medInsOrders(MedInsOrdersRequest request) throws WxPayException; + + /** + *
+ * 使用医保自费混合订单号查看下单结果 + * + * 从业机构使用混合下单订单号,通过该接口主动查询订单状态,完成下一步的业务逻辑。 + * + * 文档地址:使用医保自费混合订单号查看下单结果 + *+ * + * @param mixTradeNo 医保自费混合订单号 + * @param subMchid 医疗机构的商户号 + * @return MedInsOrdersResult 下单结果 + * @throws WxPayException the wx pay exception + */ + MedInsOrdersResult getMedInsOrderByMixTradeNo(String mixTradeNo, String subMchid) throws WxPayException; + + /** + *
+ * 使用从业机构订单号查看下单结果 + * + * 从业机构使用从业机构订单号、医疗机构商户号,通过该接口主动查询订单状态,完成下一步的业务逻辑。 + * + * 文档地址:使用从业机构订单号查看下单结果 + *+ * + * @param outTradeNo 从业机构订单号 + * @param subMchid 医疗机构的商户号 + * @return MedInsOrdersResult 下单结果 + * @throws WxPayException the wx pay exception + */ + MedInsOrdersResult getMedInsOrderByOutTradeNo(String outTradeNo, String subMchid) throws WxPayException; + + /** + *
+ * 解析医保混合收款成功通知 + * + * 微信支付会通过POST请求向商户设置的回调URL推送医保混合收款成功通知,商户需要接收处理该消息,并返回应答。 + * + * 文档地址:医保混合收款成功通知 + *+ * + * @param notifyData 通知数据字符串 + * @return MiPayNotifyV3Result 医保混合收款成功通知结果 + * @throws WxPayException the wx pay exception + */ + MiPayNotifyV3Result parseMiPayNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException; + + /** + *
+ * 医保退款通知 + * + * 从业机构调用该接口向微信医保后台通知医保订单的退款成功结果 + * + * 文档地址:医保退款通知 + *+ * + * @param request 医保退款通知请求参数 + * @param mixTradeNo 【医保自费混合订单号】 医保自费混合订单号 + * @throws WxPayException the wx pay exception + */ + void medInsRefundNotify(MedInsRefundNotifyRequest request, String mixTradeNo) throws WxPayException; + +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayrollService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayrollService.java index b3f788815c..581e3230b7 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayrollService.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayrollService.java @@ -101,4 +101,16 @@ public interface PayrollService { */ WxPayApplyBillV3Result merchantFundWithdrawBillType(String billType, String billDate, String tarType) throws WxPayException; + /** + * 微工卡批量转账API + * 适用对象:服务商 + * 请求URL:https://api.mch.weixin.qq.com/v3/payroll-card/transfer-batches + * 请求方式:POST + * + * @param request 请求参数 + * @return 返回数据 + * @throws WxPayException the wx pay exception + */ + PayrollTransferBatchesResult payrollCardTransferBatches(PayrollTransferBatchesRequest request) throws WxPayException; + } diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/RealNameService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/RealNameService.java new file mode 100644 index 0000000000..d69bda7d33 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/RealNameService.java @@ -0,0 +1,43 @@ +package com.github.binarywang.wxpay.service; + +import com.github.binarywang.wxpay.bean.realname.RealNameRequest; +import com.github.binarywang.wxpay.bean.realname.RealNameResult; +import com.github.binarywang.wxpay.exception.WxPayException; + +/** + *
+ * 微信支付实名验证相关服务类. + * 详见文档:https://pay.wechatpay.cn/doc/v2/merchant/4011987607 + *+ * + * @author Binary Wang + */ +public interface RealNameService { + /** + *
+ * 获取用户实名认证信息API. + * 用于商户查询用户的实名认证状态,如果用户未实名认证,会返回引导用户实名认证的URL + * 文档详见:https://pay.wechatpay.cn/doc/v2/merchant/4011987607 + * 接口链接:https://api.mch.weixin.qq.com/userinfo/realnameauth/query + *+ * + * @param request 请求对象 + * @return 实名认证查询结果 + * @throws WxPayException the wx pay exception + */ + RealNameResult queryRealName(RealNameRequest request) throws WxPayException; + + /** + *
+ * 获取用户实名认证信息API(简化方法). + * 用于商户查询用户的实名认证状态,如果用户未实名认证,会返回引导用户实名认证的URL + * 文档详见:https://pay.wechatpay.cn/doc/v2/merchant/4011987607 + * 接口链接:https://api.mch.weixin.qq.com/userinfo/realnameauth/query + *+ * + * @param openid 用户openid + * @return 实名认证查询结果 + * @throws WxPayException the wx pay exception + */ + RealNameResult queryRealName(String openid) throws WxPayException; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java index 93da0d1332..6a096c6338 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 @@ -38,6 +38,7 @@ public interface WxPayService { /** * Map里 加入新的 {@link WxPayConfig},适用于动态添加新的微信商户配置. + * 配置键将使用 mchId + "_" + appId 的格式. * * @param mchId 商户id * @param appId 微信应用id @@ -45,6 +46,15 @@ public interface WxPayService { */ void addConfig(String mchId, String appId, WxPayConfig wxPayConfig); + /** + * Map里 加入新的 {@link WxPayConfig},使用自定义配置键,适用于动态添加新的微信商户配置. + * 此方法允许使用任意唯一标识符(如租户ID)作为配置键,兼容单参数 switchover 使用方式. + * + * @param configKey 自定义的配置键(全局唯一标识符,如租户ID) + * @param wxPayConfig 新的微信配置 + */ + void addConfig(String configKey, WxPayConfig wxPayConfig); + /** * 从 Map中 移除 {@link String mchId} 和 {@link String appId} 所对应的 {@link WxPayConfig},适用于动态移除微信商户配置. * @@ -53,6 +63,14 @@ public interface WxPayService { */ void removeConfig(String mchId, String appId); + /** + * 从 Map中 移除指定配置键所对应的 {@link WxPayConfig},适用于动态移除微信商户配置. + * 此方法允许使用任意唯一标识符(如租户ID)删除配置,兼容单参数 switchover 使用方式. + * + * @param configKey 自定义的配置键(全局唯一标识符,如租户ID) + */ + void removeConfig(String configKey); + /** * 注入多个 {@link WxPayConfig} 的实现. 并为每个 {@link WxPayConfig} 赋予不同的 {@link String mchId} 值 * 随机采用一个{@link String mchId}进行Http初始化操作 @@ -78,6 +96,21 @@ public interface WxPayService { */ boolean switchover(String mchId, String appId); + /** + * 根据商户号或自定义配置键进行切换. + *
@@ -139,6 +137,12 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
@Getter
private final BusinessOperationTransferService businessOperationTransferService = new BusinessOperationTransferServiceImpl(this);
+ @Getter
+ private final RealNameService realNameService = new RealNameServiceImpl(this);
+
+ @Getter
+ private final MiPayService miPayService = new MiPayServiceImpl(this);
+
protected Map configMap = new ConcurrentHashMap<>();
@Override
@@ -150,6 +154,47 @@ public WxPayConfig getConfig() {
return this.configMap.get(WxPayConfigHolder.get());
}
+ @Override
+ public WxPayConfig getConfig(String mchId, String appId) {
+ if (StringUtils.isBlank(mchId)) {
+ log.warn("商户号mchId不能为空");
+ return null;
+ }
+ if (StringUtils.isBlank(appId)) {
+ log.warn("应用ID appId不能为空");
+ return null;
+ }
+ String configKey = this.getConfigKey(mchId, appId);
+ return this.configMap.get(configKey);
+ }
+
+ @Override
+ public WxPayConfig getConfig(String mchId) {
+ if (StringUtils.isBlank(mchId)) {
+ log.warn("商户号mchId不能为空");
+ return null;
+ }
+
+ // 先尝试精确匹配(针对只有mchId没有appId的配置)
+ if (this.configMap.containsKey(mchId)) {
+ return this.configMap.get(mchId);
+ }
+
+ // 尝试前缀匹配(查找以 mchId_ 开头的配置)
+ String prefix = mchId + "_";
+ return this.configMap.entrySet().stream()
+ .filter(entry -> entry.getKey().startsWith(prefix))
+ .findFirst()
+ .map(entry -> {
+ log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, entry.getKey());
+ return entry.getValue();
+ })
+ .orElseGet(() -> {
+ log.warn("无法找到对应mchId=【{}】的商户号配置信息", mchId);
+ return null;
+ });
+ }
+
@Override
public void setConfig(WxPayConfig config) {
final String defaultKey = this.getConfigKey(config.getMchId(), config.getAppId());
@@ -169,6 +214,18 @@ public void addConfig(String mchId, String appId, WxPayConfig wxPayConfig) {
}
}
+ @Override
+ public void addConfig(String configKey, WxPayConfig wxPayConfig) {
+ synchronized (this) {
+ if (this.configMap == null) {
+ this.setMultiConfig(ImmutableMap.of(configKey, wxPayConfig), configKey);
+ } else {
+ WxPayConfigHolder.set(configKey);
+ this.configMap.put(configKey, wxPayConfig);
+ }
+ }
+ }
+
@Override
public void removeConfig(String mchId, String appId) {
synchronized (this) {
@@ -186,6 +243,22 @@ public void removeConfig(String mchId, String appId) {
}
}
+ @Override
+ public void removeConfig(String configKey) {
+ synchronized (this) {
+ this.configMap.remove(configKey);
+ if (this.configMap.isEmpty()) {
+ log.warn("已删除最后一个商户号配置:configKey[{}],须立即使用setConfig或setMultiConfig添加配置", configKey);
+ return;
+ }
+ if (WxPayConfigHolder.get().equals(configKey)) {
+ final String nextConfigKey = this.configMap.keySet().iterator().next();
+ WxPayConfigHolder.set(nextConfigKey);
+ log.warn("已删除默认商户号配置,商户号【{}】被设为默认配置", nextConfigKey);
+ }
+ }
+ }
+
@Override
public void setMultiConfig(Map wxPayConfigs) {
this.setMultiConfig(wxPayConfigs, wxPayConfigs.keySet().iterator().next());
@@ -199,6 +272,10 @@ public void setMultiConfig(Map wxPayConfigs, String default
@Override
public boolean switchover(String mchId, String appId) {
+ // 如果appId为空,则降级为仅使用mchId进行切换
+ if (StringUtils.isBlank(appId)) {
+ return this.switchover(mchId);
+ }
String configKey = this.getConfigKey(mchId, appId);
if (this.configMap.containsKey(configKey)) {
WxPayConfigHolder.set(configKey);
@@ -208,8 +285,40 @@ public boolean switchover(String mchId, String appId) {
return false;
}
+ @Override
+ public boolean switchover(String mchId) {
+ // 参数校验
+ if (StringUtils.isBlank(mchId)) {
+ log.error("商户号mchId不能为空");
+ return false;
+ }
+
+ // 先尝试精确匹配(针对只有mchId没有appId的配置)
+ if (this.configMap.containsKey(mchId)) {
+ WxPayConfigHolder.set(mchId);
+ return true;
+ }
+
+ // 尝试前缀匹配(查找以 mchId_ 开头的配置)
+ String prefix = mchId + "_";
+ for (String key : this.configMap.keySet()) {
+ if (key.startsWith(prefix)) {
+ WxPayConfigHolder.set(key);
+ log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, key);
+ return true;
+ }
+ }
+
+ log.error("无法找到对应mchId=【{}】的商户号配置信息,请核实!", mchId);
+ return false;
+ }
+
@Override
public WxPayService switchoverTo(String mchId, String appId) {
+ // 如果appId为空,则降级为仅使用mchId进行切换
+ if (StringUtils.isBlank(appId)) {
+ return this.switchoverTo(mchId);
+ }
String configKey = this.getConfigKey(mchId, appId);
if (this.configMap.containsKey(configKey)) {
WxPayConfigHolder.set(configKey);
@@ -218,6 +327,32 @@ public WxPayService switchoverTo(String mchId, String appId) {
throw new WxRuntimeException(String.format("无法找到对应mchId=【%s】,appId=【%s】的商户号配置信息,请核实!", mchId, appId));
}
+ @Override
+ public WxPayService switchoverTo(String mchId) {
+ // 参数校验
+ if (StringUtils.isBlank(mchId)) {
+ throw new WxRuntimeException("商户号mchId不能为空");
+ }
+
+ // 先尝试精确匹配(针对只有mchId没有appId的配置)
+ if (this.configMap.containsKey(mchId)) {
+ WxPayConfigHolder.set(mchId);
+ return this;
+ }
+
+ // 尝试前缀匹配(查找以 mchId_ 开头的配置)
+ String prefix = mchId + "_";
+ for (String key : this.configMap.keySet()) {
+ if (key.startsWith(prefix)) {
+ WxPayConfigHolder.set(key);
+ log.debug("根据mchId=【{}】找到配置key=【{}】", mchId, key);
+ return this;
+ }
+ }
+
+ throw new WxRuntimeException(String.format("无法找到对应mchId=【%s】的商户号配置信息,请核实!", mchId));
+ }
+
public String getConfigKey(String mchId, String appId) {
return mchId + "_" + appId;
}
@@ -259,6 +394,9 @@ public WxPayRefundResult refundV2(WxPayRefundRequest request) throws WxPayExcept
@Override
public WxPayRefundV3Result refundV3(WxPayRefundV3Request request) throws WxPayException {
+ if (StringUtils.isBlank(request.getNotifyUrl())) {
+ request.setNotifyUrl(this.getConfig().getRefundNotifyUrl());
+ }
String url = String.format("%s/v3/refund/domestic/refunds", this.getPayBaseUrl());
String response = this.postV3WithWechatpaySerial(url, GSON.toJson(request));
return GSON.fromJson(response, WxPayRefundV3Result.class);
@@ -266,6 +404,15 @@ public WxPayRefundV3Result refundV3(WxPayRefundV3Request request) throws WxPayEx
@Override
public WxPayRefundV3Result partnerRefundV3(WxPayPartnerRefundV3Request request) throws WxPayException {
+ if (StringUtils.isBlank(request.getSpAppid())) {
+ request.setSpAppid(this.getConfig().getAppId());
+ }
+ if (StringUtils.isBlank(request.getSubAppid()) && StringUtils.isNotBlank(this.getConfig().getSubAppId())) {
+ request.setSubAppid(this.getConfig().getSubAppId());
+ }
+ if (StringUtils.isBlank(request.getNotifyUrl())) {
+ request.setNotifyUrl(this.getConfig().getRefundNotifyUrl());
+ }
if (StringUtils.isBlank(request.getSubMchid())) {
request.setSubMchid(this.getConfig().getSubMchId());
}
@@ -340,6 +487,13 @@ public WxPayOrderNotifyResult parseOrderNotifyResult(String xmlData) throws WxPa
public WxPayOrderNotifyResult parseOrderNotifyResult(String xmlData, String signType) throws WxPayException {
try {
log.debug("微信支付异步通知请求参数:{}", xmlData);
+
+ // 检测数据格式并给出适当的处理建议
+ if (xmlData != null && xmlData.trim().startsWith("{")) {
+ throw new WxPayException("检测到V3版本的JSON格式通知数据,请使用parseOrderNotifyV3Result方法解析。" +
+ " V3 API需要传入SignatureHeader参数进行签名验证。");
+ }
+
WxPayOrderNotifyResult result = WxPayOrderNotifyResult.fromXML(xmlData);
if (signType == null) {
this.switchover(result.getMchId(), result.getAppid());
@@ -1197,15 +1351,40 @@ public WxPayMicropayResult micropay(WxPayMicropayRequest request) throws WxPayEx
@Override
public WxPayCodepayResult codepay(WxPayCodepayRequest request) throws WxPayException {
- if (StringUtils.isBlank(request.getAppid())) {
- request.setAppid(this.getConfig().getAppId());
- }
- if (StringUtils.isBlank(request.getMchid())) {
- request.setMchid(this.getConfig().getMchId());
+ // 判断是否为服务商模式:如果设置了sp_appid或sp_mchid或sub_mchid中的任何一个,则认为是服务商模式
+ boolean isPartnerMode = StringUtils.isNotBlank(request.getSpAppid())
+ || StringUtils.isNotBlank(request.getSpMchid())
+ || StringUtils.isNotBlank(request.getSubMchid());
+
+ if (isPartnerMode) {
+ // 服务商模式
+ if (StringUtils.isBlank(request.getSpAppid())) {
+ request.setSpAppid(this.getConfig().getAppId());
+ }
+ if (StringUtils.isBlank(request.getSpMchid())) {
+ request.setSpMchid(this.getConfig().getMchId());
+ }
+ if (StringUtils.isBlank(request.getSubAppid())) {
+ request.setSubAppid(this.getConfig().getSubAppId());
+ }
+ if (StringUtils.isBlank(request.getSubMchid())) {
+ request.setSubMchid(this.getConfig().getSubMchId());
+ }
+ String url = String.format("%s/v3/pay/partner/transactions/codepay", this.getPayBaseUrl());
+ String body = this.postV3WithWechatpaySerial(url, GSON.toJson(request));
+ return GSON.fromJson(body, WxPayCodepayResult.class);
+ } else {
+ // 直连商户模式
+ if (StringUtils.isBlank(request.getAppid())) {
+ request.setAppid(this.getConfig().getAppId());
+ }
+ if (StringUtils.isBlank(request.getMchid())) {
+ request.setMchid(this.getConfig().getMchId());
+ }
+ String url = String.format("%s/v3/pay/transactions/codepay", this.getPayBaseUrl());
+ String body = this.postV3WithWechatpaySerial(url, GSON.toJson(request));
+ return GSON.fromJson(body, WxPayCodepayResult.class);
}
- String url = String.format("%s/v3/pay/transactions/codepay", this.getPayBaseUrl());
- String body = this.postV3WithWechatpaySerial(url, GSON.toJson(request));
- return GSON.fromJson(body, WxPayCodepayResult.class);
}
@Override
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessOperationTransferServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessOperationTransferServiceImpl.java
index 5e74bdbaab..098db127e3 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessOperationTransferServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessOperationTransferServiceImpl.java
@@ -33,7 +33,7 @@ public BusinessOperationTransferResult createOperationTransfer(BusinessOperation
request.setAppid(this.wxPayService.getConfig().getAppId());
}
- String url = String.format("%s/v3/fund-app/operation/mch-transfer/transfer-bills", this.wxPayService.getPayBaseUrl());
+ String url = String.format("%s/v3/fund-app/mch-transfer/transfer-bills", this.wxPayService.getPayBaseUrl());
// 如果传入了用户姓名,需要进行RSA加密
if (StringUtils.isNotEmpty(request.getUserName())) {
@@ -58,7 +58,7 @@ public BusinessOperationTransferQueryResult queryOperationTransfer(BusinessOpera
@Override
public BusinessOperationTransferQueryResult queryOperationTransferByOutBillNo(String outBillNo) throws WxPayException {
- String url = String.format("%s/v3/fund-app/operation/mch-transfer/transfer-bills/out-bill-no/%s",
+ String url = String.format("%s/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/%s",
this.wxPayService.getPayBaseUrl(), outBillNo);
String response = wxPayService.getV3(url);
return GSON.fromJson(response, BusinessOperationTransferQueryResult.class);
@@ -66,7 +66,7 @@ public BusinessOperationTransferQueryResult queryOperationTransferByOutBillNo(St
@Override
public BusinessOperationTransferQueryResult queryOperationTransferByTransferBillNo(String transferBillNo) throws WxPayException {
- String url = String.format("%s/v3/fund-app/operation/mch-transfer/transfer-bills/transfer-bill-no/%s",
+ String url = String.format("%s/v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/%s",
this.wxPayService.getPayBaseUrl(), transferBillNo);
String response = wxPayService.getV3(url);
return GSON.fromJson(response, BusinessOperationTransferQueryResult.class);
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImpl.java
index 7952513f56..ee77f5e974 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImpl.java
@@ -1,6 +1,7 @@
package com.github.binarywang.wxpay.service.impl;
import com.github.binarywang.wxpay.bean.media.ImageUploadResult;
+import com.github.binarywang.wxpay.bean.media.VideoUploadResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.MerchantMediaService;
import com.github.binarywang.wxpay.service.WxPayService;
@@ -40,7 +41,7 @@ public ImageUploadResult imageUploadV3(File imageFile) throws WxPayException,IOE
@Override
public ImageUploadResult imageUploadV3(InputStream inputStream, String fileName) throws WxPayException, IOException {
String url = String.format("%s/v3/merchant/media/upload", this.payService.getPayBaseUrl());
- try(ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[2048];
int len;
while ((len = inputStream.read(buffer)) > -1) {
@@ -57,4 +58,40 @@ public ImageUploadResult imageUploadV3(InputStream inputStream, String fileName)
}
}
+ @Override
+ public VideoUploadResult videoUploadV3(File videoFile) throws WxPayException, IOException {
+ String url = String.format("%s/v3/merchant/media/video_upload", this.payService.getPayBaseUrl());
+
+ try (FileInputStream s1 = new FileInputStream(videoFile)) {
+ String sha256 = DigestUtils.sha256Hex(s1);
+ try (InputStream s2 = new FileInputStream(videoFile)) {
+ WechatPayUploadHttpPost request = new WechatPayUploadHttpPost.Builder(URI.create(url))
+ .withVideo(videoFile.getName(), sha256, s2)
+ .build();
+ String result = this.payService.postV3(url, request);
+ return VideoUploadResult.fromJson(result);
+ }
+ }
+ }
+
+ @Override
+ public VideoUploadResult videoUploadV3(InputStream inputStream, String fileName) throws WxPayException, IOException {
+ String url = String.format("%s/v3/merchant/media/video_upload", this.payService.getPayBaseUrl());
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ byte[] buffer = new byte[2048];
+ int len;
+ while ((len = inputStream.read(buffer)) > -1) {
+ bos.write(buffer, 0, len);
+ }
+ bos.flush();
+ byte[] data = bos.toByteArray();
+ String sha256 = DigestUtils.sha256Hex(data);
+ WechatPayUploadHttpPost request = new WechatPayUploadHttpPost.Builder(URI.create(url))
+ .withVideo(fileName, sha256, new ByteArrayInputStream(data))
+ .build();
+ String result = this.payService.postV3(url, request);
+ return VideoUploadResult.fromJson(result);
+ }
+ }
+
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MiPayServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MiPayServiceImpl.java
new file mode 100644
index 0000000000..769b789fa3
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/MiPayServiceImpl.java
@@ -0,0 +1,68 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.mipay.MedInsOrdersRequest;
+import com.github.binarywang.wxpay.bean.mipay.MedInsOrdersResult;
+import com.github.binarywang.wxpay.bean.mipay.MedInsRefundNotifyRequest;
+import com.github.binarywang.wxpay.bean.notify.MiPayNotifyV3Result;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.MiPayService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.v3.util.RsaCryptoUtil;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.security.cert.X509Certificate;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 医保相关接口
+ * 医保相关接口
+ * @author xgl
+ * @date 2025/12/20
+ */
+@RequiredArgsConstructor
+public class MiPayServiceImpl implements MiPayService {
+
+ private final WxPayService payService;
+ private static final Gson GSON = new GsonBuilder().create();
+
+
+ @Override
+ public MedInsOrdersResult medInsOrders(MedInsOrdersRequest request) throws WxPayException {
+
+ String url = String.format("%s/v3/med-ins/orders", this.payService.getPayBaseUrl());
+ X509Certificate validCertificate = this.payService.getConfig().getVerifier().getValidCertificate();
+
+ RsaCryptoUtil.encryptFields(request, validCertificate);
+
+ String result = this.payService.postV3WithWechatpaySerial(url, GSON.toJson(request));
+ return GSON.fromJson(result, MedInsOrdersResult.class);
+ }
+
+ @Override
+ public MedInsOrdersResult getMedInsOrderByMixTradeNo(String mixTradeNo, String subMchid) throws WxPayException {
+ String url = String.format("%s/v3/med-ins/orders/mix-trade-no/%s?sub_mchid=%s", this.payService.getPayBaseUrl(), mixTradeNo, subMchid);
+ String result = this.payService.getV3(url);
+ return GSON.fromJson(result, MedInsOrdersResult.class);
+ }
+
+ @Override
+ public MedInsOrdersResult getMedInsOrderByOutTradeNo(String outTradeNo, String subMchid) throws WxPayException {
+ String url = String.format("%s/v3/med-ins/orders/out-trade-no/%s?sub_mchid=%s", this.payService.getPayBaseUrl(), outTradeNo, subMchid);
+ String result = this.payService.getV3(url);
+ return GSON.fromJson(result, MedInsOrdersResult.class);
+ }
+
+ @Override
+ public MiPayNotifyV3Result parseMiPayNotifyV3Result(String notifyData, SignatureHeader header) throws WxPayException {
+ return this.payService.baseParseOrderNotifyV3Result(notifyData, header, MiPayNotifyV3Result.class, MiPayNotifyV3Result.DecryptNotifyResult.class);
+ }
+
+ @Override
+ public void medInsRefundNotify(MedInsRefundNotifyRequest request, String mixTradeNo) throws WxPayException {
+ String url = String.format("%s/v3/med-ins/refunds/notify?mix_trade_no=%s", this.payService.getPayBaseUrl(), mixTradeNo);
+ this.payService.postV3(url, GSON.toJson(request));
+ }
+
+
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImpl.java
index 3d8c831271..85f7ee23dd 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImpl.java
@@ -193,4 +193,27 @@ public WxPayApplyBillV3Result merchantFundWithdrawBillType(String billType, Stri
return GSON.fromJson(response, WxPayApplyBillV3Result.class);
}
+ /**
+ * 微工卡批量转账API
+ * 适用对象:服务商
+ * 请求URL:https://api.mch.weixin.qq.com/v3/payroll-card/transfer-batches
+ * 请求方式:POST
+ *
+ * @param request 请求参数
+ * @return 返回数据
+ * @throws WxPayException the wx pay exception
+ */
+ @Override
+ public PayrollTransferBatchesResult payrollCardTransferBatches(PayrollTransferBatchesRequest request) throws WxPayException {
+ String url = String.format("%s/v3/payroll-card/transfer-batches", payService.getPayBaseUrl());
+ // 对敏感信息进行加密
+ if (request.getTransferDetailList() != null && !request.getTransferDetailList().isEmpty()) {
+ for (PayrollTransferBatchesRequest.TransferDetail detail : request.getTransferDetailList()) {
+ RsaCryptoUtil.encryptFields(detail, payService.getConfig().getVerifier().getValidCertificate());
+ }
+ }
+ String response = payService.postV3WithWechatpaySerial(url, GSON.toJson(request));
+ return GSON.fromJson(response, PayrollTransferBatchesResult.class);
+ }
+
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/RealNameServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/RealNameServiceImpl.java
new file mode 100644
index 0000000000..9a1c57fe0f
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/RealNameServiceImpl.java
@@ -0,0 +1,41 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.realname.RealNameRequest;
+import com.github.binarywang.wxpay.bean.realname.RealNameResult;
+import com.github.binarywang.wxpay.bean.result.BaseWxPayResult;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.RealNameService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import lombok.RequiredArgsConstructor;
+
+/**
+ *
+ * 微信支付实名验证相关服务实现类.
+ * 详见文档:https://pay.wechatpay.cn/doc/v2/merchant/4011987607
+ *
+ *
+ * @author Binary Wang
+ */
+@RequiredArgsConstructor
+public class RealNameServiceImpl implements RealNameService {
+ private final WxPayService payService;
+
+ @Override
+ public RealNameResult queryRealName(RealNameRequest request) throws WxPayException {
+ request.checkAndSign(this.payService.getConfig());
+ String url = this.payService.getPayBaseUrl() + "/userinfo/realnameauth/query";
+
+ String responseContent = this.payService.post(url, request.toXML(), true);
+ RealNameResult result = BaseWxPayResult.fromXML(responseContent, RealNameResult.class);
+ result.checkResult(this.payService, request.getSignType(), true);
+ return result;
+ }
+
+ @Override
+ public RealNameResult queryRealName(String openid) throws WxPayException {
+ RealNameRequest request = RealNameRequest.newBuilder()
+ .openid(openid)
+ .build();
+ return this.queryRealName(request);
+ }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
index 96454e5c08..c68d74a525 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
@@ -420,7 +420,13 @@ private String getWechatPaySerial(WxPayConfig wxPayConfig) {
return wxPayConfig.getPublicKeyId();
}
- return wxPayConfig.getVerifier().getValidCertificate().getSerialNumber().toString(16).toUpperCase();
+ try {
+ return wxPayConfig.getVerifier().getValidCertificate().getSerialNumber().toString(16).toUpperCase();
+ } catch (Exception e) {
+ log.warn("Failed to get certificate serial number: {}", e.getMessage());
+ // 返回空字符串而不是抛出异常,让请求继续进行,由微信服务器判断是否需要Wechatpay-Serial
+ return "";
+ }
}
private void logRequestAndResponse(String url, String requestStr, String responseStr) {
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java
index 5c21a06a8e..9adc673238 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceHttpComponentsImpl.java
@@ -398,7 +398,13 @@ private String getWechatPaySerial(WxPayConfig wxPayConfig) {
return wxPayConfig.getPublicKeyId();
}
- return wxPayConfig.getVerifier().getValidCertificate().getSerialNumber().toString(16).toUpperCase();
+ try {
+ return wxPayConfig.getVerifier().getValidCertificate().getSerialNumber().toString(16).toUpperCase();
+ } catch (Exception e) {
+ log.warn("Failed to get certificate serial number: {}", e.getMessage());
+ // 返回空字符串而不是抛出异常,让请求继续进行,由微信服务器判断是否需要Wechatpay-Serial
+ return "";
+ }
}
private void logRequestAndResponse(String url, String requestStr, String responseStr) {
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceImpl.java
index 8e795966f4..4316bafa40 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceImpl.java
@@ -2,11 +2,11 @@
/**
*
- * 微信支付接口请求实现类,默认使用Apache HttpClient实现
+ * 微信支付接口请求实现类,默认使用Apache HttpClient 5实现
* Created by Binary Wang on 2017-7-8.
*
*
* @author Binary Wang
*/
-public class WxPayServiceImpl extends WxPayServiceApacheHttpImpl {
+public class WxPayServiceImpl extends WxPayServiceHttpComponentsImpl {
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/RequestUtils.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/RequestUtils.java
index b641cbe537..c4ad966415 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/RequestUtils.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/RequestUtils.java
@@ -17,7 +17,7 @@ public class RequestUtils {
/**
* 获取请求头数据,微信V3版本回调使用
*
- * @param request
+ * @param request HTTP请求对象
* @return 字符串
*/
public static String readData(HttpServletRequest request) {
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/ResourcesUtils.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/ResourcesUtils.java
index ac68b00bb4..51dd8fbbb6 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/ResourcesUtils.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/ResourcesUtils.java
@@ -23,6 +23,10 @@ public class ResourcesUtils {
* {@link Class#getClassLoader() ClassLoaderUtil.class.getClassLoader()}
* if callingClass is provided: {@link Class#getClassLoader() callingClass.getClassLoader()}
*
+ *
+ * @param resourceName 资源名称
+ * @param classLoader 类加载器
+ * @return 资源URL
*/
public static URL getResourceUrl(String resourceName, final ClassLoader classLoader) {
@@ -64,6 +68,9 @@ public static URL getResourceUrl(String resourceName, final ClassLoader classLoa
/**
* Opens a resource of the specified name for reading.
*
+ * @param resourceName 资源名称
+ * @return 输入流
+ * @throws IOException IO异常
* @see #getResourceAsStream(String, ClassLoader)
*/
public static InputStream getResourceAsStream(final String resourceName) throws IOException {
@@ -73,6 +80,10 @@ public static InputStream getResourceAsStream(final String resourceName) throws
/**
* Opens a resource of the specified name for reading.
*
+ * @param resourceName 资源名称
+ * @param callingClass 类加载器
+ * @return 输入流
+ * @throws IOException IO异常
* @see #getResourceUrl(String, ClassLoader)
*/
public static InputStream getResourceAsStream(final String resourceName, final ClassLoader callingClass) throws IOException {
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/SignUtils.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/SignUtils.java
index 6c0009fd18..9d4a9b0237 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/SignUtils.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/util/SignUtils.java
@@ -112,7 +112,16 @@ public static String createSign(Map params, String signType, Str
/**
* 企业微信签名
*
- * @param signType md5 目前接口要求使用的加密类型
+ * @param actName 活动名称
+ * @param mchBillNo 商户订单号
+ * @param mchId 商户号
+ * @param nonceStr 随机字符串
+ * @param reOpenid 用户openid
+ * @param totalAmount 金额
+ * @param wxAppId 微信appid
+ * @param signKey 签名密钥
+ * @param signType md5 目前接口要求使用的加密类型
+ * @return 签名字符串
*/
public static String createEntSign(String actName, String mchBillNo, String mchId, String nonceStr,
String reOpenid, Integer totalAmount, String wxAppId, String signKey,
@@ -131,7 +140,18 @@ public static String createEntSign(String actName, String mchBillNo, String mchI
/**
* 企业微信签名
- * @param signType md5 目前接口要求使用的加密类型
+ *
+ * @param totalAmount 金额
+ * @param appId 应用ID
+ * @param description 描述
+ * @param mchId 商户号
+ * @param nonceStr 随机字符串
+ * @param openid 用户openid
+ * @param partnerTradeNo 商户订单号
+ * @param wwMsgType 消息类型
+ * @param signKey 签名密钥
+ * @param signType md5 目前接口要求使用的加密类型
+ * @return 签名字符串
*/
public static String createEntSign(Integer totalAmount, String appId, String description, String mchId,
String nonceStr, String openid, String partnerTradeNo, String wwMsgType,
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java
index 24d6f26eb5..24c51028df 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java
@@ -15,16 +15,27 @@
import org.apache.http.util.EntityUtils;
import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
public class SignatureExec implements ClientExecChain {
final ClientExecChain mainExec;
final Credentials credentials;
final Validator validator;
+ /**
+ * 额外受信任的主机列表,这些主机(如反向代理)也需要携带微信支付 Authorization 头
+ */
+ final Set trustedHosts;
SignatureExec(Credentials credentials, Validator validator, ClientExecChain mainExec) {
+ this(credentials, validator, mainExec, Collections.emptySet());
+ }
+
+ SignatureExec(Credentials credentials, Validator validator, ClientExecChain mainExec, Set trustedHosts) {
this.credentials = credentials;
this.validator = validator;
this.mainExec = mainExec;
+ this.trustedHosts = trustedHosts != null ? trustedHosts : Collections.emptySet();
}
protected HttpEntity newRepeatableEntity(HttpEntity entity) throws IOException {
@@ -56,7 +67,8 @@ protected void convertToRepeatableRequestEntity(HttpRequestWrapper request) thro
public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request,
HttpClientContext context, HttpExecutionAware execAware)
throws IOException, HttpException {
- if (request.getURI().getHost() != null && request.getURI().getHost().endsWith(".mch.weixin.qq.com")) {
+ String host = request.getURI().getHost();
+ if (host != null && (host.endsWith(".mch.weixin.qq.com") || trustedHosts.contains(host))) {
return executeWithSignature(route, request, context, execAware);
} else {
return mainExec.execute(route, request, context, execAware);
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java
index 5f5e52d2ff..3387f37e3d 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java
@@ -35,7 +35,7 @@ public Builder(URI uri) {
this.uri = uri;
}
- public Builder withImage(String fileName, String fileSha256, InputStream inputStream) {
+ private Builder withMedia(String fileName, String fileSha256, InputStream inputStream) {
this.fileName = fileName;
this.fileSha256 = fileSha256;
this.fileInputStream = inputStream;
@@ -50,13 +50,21 @@ public Builder withImage(String fileName, String fileSha256, InputStream inputSt
return this;
}
+ public Builder withImage(String fileName, String fileSha256, InputStream inputStream) {
+ return withMedia(fileName, fileSha256, inputStream);
+ }
+
+ public Builder withVideo(String fileName, String fileSha256, InputStream inputStream) {
+ return withMedia(fileName, fileSha256, inputStream);
+ }
+
public WechatPayUploadHttpPost build() {
if (fileName == null || fileSha256 == null || fileInputStream == null) {
- throw new IllegalArgumentException("缺少待上传图片文件信息");
+ throw new IllegalArgumentException("缺少待上传文件信息");
}
if (uri == null) {
- throw new IllegalArgumentException("缺少上传图片接口URL");
+ throw new IllegalArgumentException("缺少上传文件接口URL");
}
String meta = String.format("{\"filename\":\"%s\",\"sha256\":\"%s\"}", fileName, fileSha256);
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WxPayV3HttpClientBuilder.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WxPayV3HttpClientBuilder.java
index c88c884f57..91baa16246 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WxPayV3HttpClientBuilder.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WxPayV3HttpClientBuilder.java
@@ -2,6 +2,9 @@
import java.security.PrivateKey;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
import com.github.binarywang.wxpay.v3.auth.PrivateKeySigner;
import com.github.binarywang.wxpay.v3.auth.WxPayCredentials;
@@ -12,6 +15,10 @@
public class WxPayV3HttpClientBuilder extends HttpClientBuilder {
private Credentials credentials;
private Validator validator;
+ /**
+ * 额外受信任的主机列表,用于代理转发场景:对这些主机的请求也会携带微信支付 Authorization 头
+ */
+ private final Set trustedHosts = new HashSet<>();
static final String OS = System.getProperty("os.name") + "/" + System.getProperty("os.version");
static final String VERSION = System.getProperty("java.version");
@@ -47,6 +54,39 @@ public WxPayV3HttpClientBuilder withValidator(Validator validator) {
return this;
}
+ /**
+ * 添加受信任的主机,对该主机的请求也会携带微信支付 Authorization 头.
+ * 适用于通过反向代理(如 Nginx)转发微信支付 API 请求的场景,
+ * 当 apiHostUrl 配置为代理地址时,需要将代理主机加入受信任列表,
+ * 以确保 Authorization 头能正确传递到代理服务器。
+ * 若传入值包含端口(如 "proxy.company.com:8080"),会自动提取主机名部分。
+ *
+ * @param host 受信任的主机(可含端口),例如 "proxy.company.com" 或 "proxy.company.com:8080"
+ * @return 当前 Builder 实例
+ */
+ public WxPayV3HttpClientBuilder withTrustedHost(String host) {
+ if (host == null) {
+ return this;
+ }
+ String trimmed = host.trim();
+ if (trimmed.isEmpty()) {
+ return this;
+ }
+ // 若包含端口号(如 "host:8080"),只取主机名部分
+ int colonIdx = trimmed.lastIndexOf(':');
+ if (colonIdx > 0) {
+ String portPart = trimmed.substring(colonIdx + 1);
+ boolean isPort = !portPart.isEmpty() && portPart.chars().allMatch(Character::isDigit);
+ if (isPort) {
+ trimmed = trimmed.substring(0, colonIdx);
+ }
+ }
+ if (!trimmed.isEmpty()) {
+ this.trustedHosts.add(trimmed);
+ }
+ return this;
+ }
+
@Override
public CloseableHttpClient build() {
if (credentials == null) {
@@ -61,6 +101,7 @@ public CloseableHttpClient build() {
@Override
protected ClientExecChain decorateProtocolExec(final ClientExecChain requestExecutor) {
- return new SignatureExec(this.credentials, this.validator, requestExecutor);
+ return new SignatureExec(this.credentials, this.validator, requestExecutor,
+ Collections.unmodifiableSet(new HashSet<>(this.trustedHosts)));
}
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java
index 21624d455f..0757f58dcc 100755
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java
@@ -22,6 +22,8 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateExpiredException;
@@ -109,18 +111,24 @@ public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key,
this.minutesInterval = minutesInterval;
this.payBaseUrl = payBaseUrl;
this.wxPayHttpProxy = wxPayHttpProxy;
- //构造时更新证书
+ //构造时尝试更新证书,但失败时不抛出异常,避免影响公钥模式的使用
try {
autoUpdateCert();
instant = Instant.now();
} catch (IOException | GeneralSecurityException e) {
- throw new WxRuntimeException(e);
+ log.warn("Auto update cert failed during initialization, will retry later, exception = {}", e.getMessage());
+ // 设置 instant 为 null,后续每次使用时都会尝试下载证书直到成功
+ instant = null;
}
}
@Override
public boolean verify(String serialNumber, byte[] message, String signature) {
checkAndAutoUpdateCert();
+ if (verifier == null) {
+ log.warn("No valid certificate available for verification");
+ return false;
+ }
return verifier.verify(serialNumber, message, signature);
}
@@ -148,8 +156,21 @@ private void autoUpdateCert() throws IOException, GeneralSecurityException {
.withCredentials(credentials)
.withValidator(verifier == null ? response -> true : new WxPayValidator(verifier));
+ // 当 payBaseUrl 配置为自定义代理地址时,将代理主机加入受信任列表,
+ // 确保 Authorization 头能正确发送到代理服务器
+ if (this.payBaseUrl != null && !this.payBaseUrl.isEmpty()) {
+ try {
+ String host = new URI(this.payBaseUrl).getHost();
+ if (host != null && !host.endsWith(".mch.weixin.qq.com")) {
+ wxPayV3HttpClientBuilder.withTrustedHost(host);
+ }
+ } catch (URISyntaxException e) {
+ log.warn("解析 payBaseUrl [{}] 中的主机名失败: {}", this.payBaseUrl, e.getMessage());
+ }
+ }
+
//调用自定义扩展设置设置HTTP PROXY对象
- HttpProxyUtils.initHttpProxy(wxPayV3HttpClientBuilder,this.wxPayHttpProxy);
+ HttpProxyUtils.initHttpProxy(wxPayV3HttpClientBuilder, this.wxPayHttpProxy);
//增加自定义扩展点,子类可以设置其他构造参数
this.customHttpClientBuilder(wxPayV3HttpClientBuilder);
@@ -220,6 +241,9 @@ private List deserializeToCerts(byte[] apiV3Key, String body) t
@Override
public X509Certificate getValidCertificate() {
checkAndAutoUpdateCert();
+ if (verifier == null) {
+ throw new WxRuntimeException("No valid certificate available, please check your configuration or use fullPublicKeyModel mode");
+ }
return verifier.getValidCertificate();
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtil.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtil.java
index 8c3e2ace53..0143022ece 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtil.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtil.java
@@ -14,8 +14,11 @@
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
+import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
/**
* 微信支付敏感信息加密
@@ -36,10 +39,26 @@ public static void encryptFields(Object encryptObject, X509Certificate certifica
}
}
+ /**
+ * 递归获取类的所有字段,包括父类中的字段
+ *
+ * @param clazz 要获取字段的类
+ * @return 所有字段的列表
+ */
+ private static List getAllFields(Class> clazz) {
+ List fields = new ArrayList<>();
+ while (clazz != null && clazz != Object.class) {
+ Field[] declaredFields = clazz.getDeclaredFields();
+ Collections.addAll(fields, declaredFields);
+ clazz = clazz.getSuperclass();
+ }
+ return fields;
+ }
+
private static void encryptField(Object encryptObject, X509Certificate certificate) throws IllegalAccessException, IllegalBlockSizeException {
Class> infoClass = encryptObject.getClass();
- Field[] infoFieldArray = infoClass.getDeclaredFields();
- for (Field field : infoFieldArray) {
+ List infoFieldList = getAllFields(infoClass);
+ for (Field field : infoFieldList) {
if (field.isAnnotationPresent(SpecEncrypt.class)) {
//字段使用了@SpecEncrypt进行标识
if (field.getType().getTypeName().equals(JAVA_LANG_STRING)) {
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/applyment/WxPayApplyment4SubCreateRequestTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/applyment/WxPayApplyment4SubCreateRequestTest.java
new file mode 100644
index 0000000000..6dad6d2a80
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/applyment/WxPayApplyment4SubCreateRequestTest.java
@@ -0,0 +1,104 @@
+package com.github.binarywang.wxpay.bean.applyment;
+
+import java.util.Arrays;
+
+import org.testng.annotations.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public class WxPayApplyment4SubCreateRequestTest {
+
+ @Test
+ public void testMicroBizInfoSerialization() {
+ // 1. Test MicroStoreInfo
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.MicroStoreInfo storeInfo =
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.MicroStoreInfo.builder()
+ .microName("门店名称")
+ .microAddressCode("440305")
+ .microAddress("详细地址")
+ .storeEntrancePic("media_id_1")
+ .microIndoorCopy("media_id_2")
+ .storeLongitude("113.941046")
+ .storeLatitude("22.546154")
+ .build();
+
+ // 2. Test MicroMobileInfo
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.MicroMobileInfo mobileInfo =
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.MicroMobileInfo.builder()
+ .microMobileName("流动经营名称")
+ .microMobileCity("440305")
+ .microMobileAddress("无")
+ .microMobilePics(Arrays.asList("media_id_3", "media_id_4"))
+ .build();
+
+ // 3. Test MicroOnlineInfo
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.MicroOnlineInfo onlineInfo =
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.MicroOnlineInfo.builder()
+ .microOnlineStore("线上店铺名称")
+ .microEcName("电商平台名称")
+ .microQrcode("media_id_5")
+ .microLink("https://www.example.com")
+ .build();
+
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo microBizInfo =
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.builder()
+ .microStoreInfo(storeInfo)
+ .microMobileInfo(mobileInfo)
+ .microOnlineInfo(onlineInfo)
+ .build();
+
+ Gson gson = new GsonBuilder().create();
+ String json = gson.toJson(microBizInfo);
+
+ // Verify MicroStoreInfo serialization
+ assertTrue(json.contains("\"micro_name\":\"门店名称\""));
+ assertTrue(json.contains("\"micro_address_code\":\"440305\""));
+ assertTrue(json.contains("\"micro_address\":\"详细地址\""));
+ assertTrue(json.contains("\"store_entrance_pic\":\"media_id_1\""));
+ assertTrue(json.contains("\"micro_indoor_copy\":\"media_id_2\""));
+ assertTrue(json.contains("\"store_longitude\":\"113.941046\""));
+ assertTrue(json.contains("\"store_latitude\":\"22.546154\""));
+
+ // Verify MicroMobileInfo serialization
+ assertTrue(json.contains("\"micro_mobile_name\":\"流动经营名称\""));
+ assertTrue(json.contains("\"micro_mobile_city\":\"440305\""));
+ assertTrue(json.contains("\"micro_mobile_address\":\"无\""));
+ assertTrue(json.contains("\"micro_mobile_pics\":[\"media_id_3\",\"media_id_4\"]"));
+
+ // Verify MicroOnlineInfo serialization
+ assertTrue(json.contains("\"micro_online_store\":\"线上店铺名称\""));
+ assertTrue(json.contains("\"micro_ec_name\":\"电商平台名称\""));
+ assertTrue(json.contains("\"micro_qrcode\":\"media_id_5\""));
+ assertTrue(json.contains("\"micro_link\":\"https://www.example.com\""));
+
+ // Verify deserialization
+ WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo deserialized =
+ gson.fromJson(json, WxPayApplyment4SubCreateRequest.SubjectInfo.MicroBizInfo.class);
+
+ // Verify MicroStoreInfo deserialization
+ assertEquals(deserialized.getMicroStoreInfo().getMicroName(), "门店名称");
+ assertEquals(deserialized.getMicroStoreInfo().getMicroAddressCode(), "440305");
+ assertEquals(deserialized.getMicroStoreInfo().getMicroAddress(), "详细地址");
+ assertEquals(deserialized.getMicroStoreInfo().getStoreEntrancePic(), "media_id_1");
+ assertEquals(deserialized.getMicroStoreInfo().getMicroIndoorCopy(), "media_id_2");
+ assertEquals(deserialized.getMicroStoreInfo().getStoreLongitude(), "113.941046");
+ assertEquals(deserialized.getMicroStoreInfo().getStoreLatitude(), "22.546154");
+
+ // Verify MicroMobileInfo deserialization
+ assertEquals(deserialized.getMicroMobileInfo().getMicroMobileName(), "流动经营名称");
+ assertEquals(deserialized.getMicroMobileInfo().getMicroMobileCity(), "440305");
+ assertEquals(deserialized.getMicroMobileInfo().getMicroMobileAddress(), "无");
+ assertEquals(deserialized.getMicroMobileInfo().getMicroMobilePics().size(), 2);
+ assertEquals(deserialized.getMicroMobileInfo().getMicroMobilePics(), Arrays.asList("media_id_3", "media_id_4"));
+
+ // Verify MicroOnlineInfo deserialization
+ assertEquals(deserialized.getMicroOnlineInfo().getMicroOnlineStore(), "线上店铺名称");
+ assertEquals(deserialized.getMicroOnlineInfo().getMicroEcName(), "电商平台名称");
+ assertEquals(deserialized.getMicroOnlineInfo().getMicroQrcode(), "media_id_5");
+ assertEquals(deserialized.getMicroOnlineInfo().getMicroLink(), "https://www.example.com");
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/entpay/EntPayRequestTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/entpay/EntPayRequestTest.java
index b6f68b81c1..402c74d38f 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/entpay/EntPayRequestTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/entpay/EntPayRequestTest.java
@@ -2,6 +2,10 @@
import org.testng.annotations.Test;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
/**
* .
*
@@ -14,4 +18,32 @@ public class EntPayRequestTest {
public void testToString() {
System.out.println(EntPayRequest.newBuilder().mchId("123").build().toString());
}
+
+ /**
+ * 测试 brandId 为 null 时,getSignParams() 不抛出 NullPointerException.
+ */
+ @Test
+ public void testGetSignParamsWithNullBrandId() {
+ EntPayRequest request = EntPayRequest.newBuilder()
+ .mchId("123")
+ .amount(100)
+ .brandId(null)
+ .build();
+ Map params = request.getSignParams();
+ assertThat(params).doesNotContainKey("brand_id");
+ }
+
+ /**
+ * 测试 brandId 不为 null 时,getSignParams() 正确包含 brand_id.
+ */
+ @Test
+ public void testGetSignParamsWithNonNullBrandId() {
+ EntPayRequest request = EntPayRequest.newBuilder()
+ .mchId("123")
+ .amount(100)
+ .brandId(1234)
+ .build();
+ Map params = request.getSignParams();
+ assertThat(params).containsEntry("brand_id", "1234");
+ }
}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/marketing/transfer/BatchDetailsResultTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/marketing/transfer/BatchDetailsResultTest.java
new file mode 100644
index 0000000000..c2347300a6
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/marketing/transfer/BatchDetailsResultTest.java
@@ -0,0 +1,182 @@
+package com.github.binarywang.wxpay.bean.marketing.transfer;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+/**
+ * 测试 BatchDetailsResult 的字段序列化和反序列化功能
+ *
+ * @author Binary Wang
+ */
+public class BatchDetailsResultTest {
+
+ private static final Gson GSON = new GsonBuilder().create();
+
+ @Test
+ public void testBankFieldsDeserialization() {
+ // 模拟微信API返回的JSON(包含银行相关字段)
+ String mockJson = "{\n" +
+ " \"sp_mchid\": \"1900001109\",\n" +
+ " \"out_batch_no\": \"plfk2020042013\",\n" +
+ " \"batch_id\": \"1030000071100999991182020050700019480001\",\n" +
+ " \"appid\": \"wxf636efh567hg4356\",\n" +
+ " \"out_detail_no\": \"x23zy545Bd5436\",\n" +
+ " \"detail_id\": \"1040000071100999991182020050700019500100\",\n" +
+ " \"detail_status\": \"SUCCESS\",\n" +
+ " \"transfer_amount\": 200000,\n" +
+ " \"transfer_remark\": \"2020年4月报销\",\n" +
+ " \"openid\": \"o-MYE42l80oelYMDE34nYD456Xoy\",\n" +
+ " \"username\": \"757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45\",\n" +
+ " \"initiate_time\": \"2015-05-20T13:29:35.120+08:00\",\n" +
+ " \"update_time\": \"2015-05-20T13:29:35.120+08:00\",\n" +
+ " \"bank_name\": \"中国农业银行股份有限公司深圳分行\",\n" +
+ " \"bank_card_number_tail\": \"1234\"\n" +
+ "}";
+
+ // 反序列化JSON
+ BatchDetailsResult result = GSON.fromJson(mockJson, BatchDetailsResult.class);
+
+ // 验证基本字段正常解析
+ assertEquals(result.getSpMchid(), "1900001109");
+ assertEquals(result.getOutBatchNo(), "plfk2020042013");
+ assertEquals(result.getBatchId(), "1030000071100999991182020050700019480001");
+ assertEquals(result.getAppId(), "wxf636efh567hg4356");
+ assertEquals(result.getOutDetailNo(), "x23zy545Bd5436");
+ assertEquals(result.getDetailId(), "1040000071100999991182020050700019500100");
+ assertEquals(result.getDetailStatus(), "SUCCESS");
+ assertEquals(result.getTransferAmount(), Integer.valueOf(200000));
+ assertEquals(result.getTransferRemark(), "2020年4月报销");
+ assertEquals(result.getOpenid(), "o-MYE42l80oelYMDE34nYD456Xoy");
+ assertEquals(result.getUserName(), "757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45");
+ assertEquals(result.getInitiateTime(), "2015-05-20T13:29:35.120+08:00");
+ assertEquals(result.getUpdateTime(), "2015-05-20T13:29:35.120+08:00");
+
+ // 验证新增的银行相关字段
+ assertEquals(result.getBankName(), "中国农业银行股份有限公司深圳分行");
+ assertEquals(result.getBankCardNumberTail(), "1234");
+ }
+
+ @Test
+ public void testBankFieldsWithNull() {
+ // 测试不包含银行字段的情况(转账到零钱)
+ String mockJsonWithoutBank = "{\n" +
+ " \"sp_mchid\": \"1900001109\",\n" +
+ " \"out_batch_no\": \"plfk2020042013\",\n" +
+ " \"batch_id\": \"1030000071100999991182020050700019480001\",\n" +
+ " \"out_detail_no\": \"x23zy545Bd5436\",\n" +
+ " \"detail_id\": \"1040000071100999991182020050700019500100\",\n" +
+ " \"detail_status\": \"SUCCESS\",\n" +
+ " \"transfer_amount\": 200000,\n" +
+ " \"transfer_remark\": \"2020年4月报销\",\n" +
+ " \"openid\": \"o-MYE42l80oelYMDE34nYD456Xoy\",\n" +
+ " \"username\": \"757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45\",\n" +
+ " \"initiate_time\": \"2015-05-20T13:29:35.120+08:00\",\n" +
+ " \"update_time\": \"2015-05-20T13:29:35.120+08:00\"\n" +
+ "}";
+
+ BatchDetailsResult result = GSON.fromJson(mockJsonWithoutBank, BatchDetailsResult.class);
+
+ // 验证其他字段正常
+ assertEquals(result.getSpMchid(), "1900001109");
+ assertEquals(result.getDetailStatus(), "SUCCESS");
+
+ // 验证银行字段为null(转账到零钱场景下不返回这些字段)
+ assertNull(result.getBankName());
+ assertNull(result.getBankCardNumberTail());
+ }
+
+ @Test
+ public void testBankFieldsSerialization() {
+ // 测试序列化
+ BatchDetailsResult result = new BatchDetailsResult();
+ result.setSpMchid("1900001109");
+ result.setOutBatchNo("plfk2020042013");
+ result.setBatchId("1030000071100999991182020050700019480001");
+ result.setDetailStatus("SUCCESS");
+ result.setBankName("中国工商银行股份有限公司北京分行");
+ result.setBankCardNumberTail("5678");
+
+ String json = GSON.toJson(result);
+
+ // 验证JSON包含银行字段
+ assertTrue(json.contains("\"bank_name\":\"中国工商银行股份有限公司北京分行\""));
+ assertTrue(json.contains("\"bank_card_number_tail\":\"5678\""));
+ }
+
+ @Test
+ public void testToString() {
+ // 测试toString方法
+ BatchDetailsResult result = new BatchDetailsResult();
+ result.setSpMchid("1900001109");
+ result.setBankName("中国建设银行股份有限公司上海分行");
+ result.setBankCardNumberTail("9012");
+
+ String resultString = result.toString();
+
+ // 验证toString包含所有字段
+ assertNotNull(resultString);
+ assertTrue(resultString.contains("1900001109"));
+ assertTrue(resultString.contains("中国建设银行股份有限公司上海分行"));
+ assertTrue(resultString.contains("9012"));
+ }
+
+ @Test
+ public void testBankNameWithSpecialCharacters() {
+ // 测试银行名称包含特殊字符的情况
+ String mockJson = "{\n" +
+ " \"sp_mchid\": \"1900001109\",\n" +
+ " \"out_batch_no\": \"plfk2020042013\",\n" +
+ " \"batch_id\": \"1030000071100999991182020050700019480001\",\n" +
+ " \"out_detail_no\": \"x23zy545Bd5436\",\n" +
+ " \"detail_id\": \"1040000071100999991182020050700019500100\",\n" +
+ " \"detail_status\": \"SUCCESS\",\n" +
+ " \"transfer_amount\": 200000,\n" +
+ " \"transfer_remark\": \"2020年4月报销\",\n" +
+ " \"openid\": \"o-MYE42l80oelYMDE34nYD456Xoy\",\n" +
+ " \"username\": \"757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45\",\n" +
+ " \"initiate_time\": \"2015-05-20T13:29:35.120+08:00\",\n" +
+ " \"update_time\": \"2015-05-20T13:29:35.120+08:00\",\n" +
+ " \"bank_name\": \"中国农业银行股份有限公司北京市朝阳区(支行)\",\n" +
+ " \"bank_card_number_tail\": \"0000\"\n" +
+ "}";
+
+ BatchDetailsResult result = GSON.fromJson(mockJson, BatchDetailsResult.class);
+
+ // 验证特殊字符正确解析
+ assertEquals(result.getBankName(), "中国农业银行股份有限公司北京市朝阳区(支行)");
+ assertEquals(result.getBankCardNumberTail(), "0000");
+ }
+
+ @Test
+ public void testFailedTransferWithoutBankFields() {
+ // 测试转账失败的情况
+ String mockJson = "{\n" +
+ " \"sp_mchid\": \"1900001109\",\n" +
+ " \"out_batch_no\": \"plfk2020042013\",\n" +
+ " \"batch_id\": \"1030000071100999991182020050700019480001\",\n" +
+ " \"out_detail_no\": \"x23zy545Bd5436\",\n" +
+ " \"detail_id\": \"1040000071100999991182020050700019500100\",\n" +
+ " \"detail_status\": \"FAIL\",\n" +
+ " \"transfer_amount\": 200000,\n" +
+ " \"transfer_remark\": \"2020年4月报销\",\n" +
+ " \"fail_reason\": \"ACCOUNT_FROZEN\",\n" +
+ " \"openid\": \"o-MYE42l80oelYMDE34nYD456Xoy\",\n" +
+ " \"username\": \"757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45\",\n" +
+ " \"initiate_time\": \"2015-05-20T13:29:35.120+08:00\",\n" +
+ " \"update_time\": \"2015-05-20T13:29:35.120+08:00\"\n" +
+ "}";
+
+ BatchDetailsResult result = GSON.fromJson(mockJson, BatchDetailsResult.class);
+
+ // 验证失败状态
+ assertEquals(result.getDetailStatus(), "FAIL");
+ assertEquals(result.getFailReason(), "ACCOUNT_FROZEN");
+
+ // 失败的情况下银行字段应为null
+ assertNull(result.getBankName());
+ assertNull(result.getBankCardNumberTail());
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3RequestTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3RequestTest.java
new file mode 100644
index 0000000000..ebdc992082
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/request/WxPayPartnerRefundV3RequestTest.java
@@ -0,0 +1,55 @@
+package com.github.binarywang.wxpay.bean.request;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import org.testng.annotations.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * {@link WxPayPartnerRefundV3Request} 单元测试
+ *
+ */
+public class WxPayPartnerRefundV3RequestTest {
+
+ @Test
+ public void testSpAppidAndSubAppidSerialization() {
+ WxPayPartnerRefundV3Request request = new WxPayPartnerRefundV3Request();
+ request.setSpAppid("wx8888888888888888");
+ request.setSubAppid("wxd678efh567hg6999");
+ request.setSubMchid("1230000109");
+ request.setOutRefundNo("1217752501201407033233368018");
+ request.setFundsAccount("AVAILABLE");
+
+ Gson gson = new Gson();
+ String json = gson.toJson(request);
+ JsonObject jsonObject = gson.fromJson(json, JsonObject.class);
+
+ assertThat(jsonObject.has("sp_appid")).isTrue();
+ assertThat(jsonObject.get("sp_appid").getAsString()).isEqualTo("wx8888888888888888");
+ assertThat(jsonObject.has("sub_appid")).isTrue();
+ assertThat(jsonObject.get("sub_appid").getAsString()).isEqualTo("wxd678efh567hg6999");
+ assertThat(jsonObject.has("sub_mchid")).isTrue();
+ assertThat(jsonObject.get("sub_mchid").getAsString()).isEqualTo("1230000109");
+ assertThat(jsonObject.has("out_refund_no")).isTrue();
+ assertThat(jsonObject.get("out_refund_no").getAsString()).isEqualTo("1217752501201407033233368018");
+ assertThat(jsonObject.has("funds_account")).isTrue();
+ assertThat(jsonObject.get("funds_account").getAsString()).isEqualTo("AVAILABLE");
+ }
+
+ @Test
+ public void testSubAppidIsOptional() {
+ WxPayPartnerRefundV3Request request = new WxPayPartnerRefundV3Request();
+ request.setSpAppid("wx8888888888888888");
+ request.setSubMchid("1230000109");
+ request.setOutRefundNo("1217752501201407033233368018");
+
+ Gson gson = new Gson();
+ String json = gson.toJson(request);
+ JsonObject jsonObject = gson.fromJson(json, JsonObject.class);
+
+ assertThat(jsonObject.has("sp_appid")).isTrue();
+ assertThat(jsonObject.get("sp_appid").getAsString()).isEqualTo("wx8888888888888888");
+ assertThat(jsonObject.has("sub_appid")).isFalse();
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/result/WxPayUnifiedOrderV3ResultTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/result/WxPayUnifiedOrderV3ResultTest.java
new file mode 100644
index 0000000000..2e824b0e00
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/result/WxPayUnifiedOrderV3ResultTest.java
@@ -0,0 +1,267 @@
+package com.github.binarywang.wxpay.bean.result;
+
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.v3.util.SignUtils;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+
+/**
+ *
+ * WxPayUnifiedOrderV3Result 测试类
+ * 主要测试prepayId字段和静态工厂方法的解耦功能
+ *
+ *
+ * @author copilot
+ */
+public class WxPayUnifiedOrderV3ResultTest {
+
+ /**
+ * 生成测试用的RSA密钥对
+ */
+ private KeyPair generateKeyPair() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(2048);
+ return keyPairGenerator.generateKeyPair();
+ }
+
+ /**
+ * 测试JsapiResult中的prepayId字段是否正确设置
+ */
+ @Test
+ public void testJsapiResultWithPrepayId() throws Exception {
+ // 准备测试数据
+ String testPrepayId = "wx201410272009395522657a690389285100";
+ String testAppId = "wx8888888888888888";
+ KeyPair keyPair = generateKeyPair();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // 创建WxPayUnifiedOrderV3Result对象
+ WxPayUnifiedOrderV3Result result = new WxPayUnifiedOrderV3Result();
+ result.setPrepayId(testPrepayId);
+
+ // 调用getPayInfo生成JsapiResult
+ WxPayUnifiedOrderV3Result.JsapiResult jsapiResult =
+ result.getPayInfo(TradeTypeEnum.JSAPI, testAppId, null, privateKey);
+
+ // 验证prepayId字段是否正确设置
+ Assert.assertNotNull(jsapiResult.getPrepayId(), "prepayId不应为null");
+ Assert.assertEquals(jsapiResult.getPrepayId(), testPrepayId, "prepayId应该与设置的值相同");
+
+ // 验证其他字段
+ Assert.assertEquals(jsapiResult.getAppId(), testAppId);
+ Assert.assertNotNull(jsapiResult.getTimeStamp());
+ Assert.assertNotNull(jsapiResult.getNonceStr());
+ Assert.assertEquals(jsapiResult.getPackageValue(), "prepay_id=" + testPrepayId);
+ Assert.assertEquals(jsapiResult.getSignType(), "RSA");
+ Assert.assertNotNull(jsapiResult.getPaySign());
+ }
+
+ /**
+ * 测试使用静态工厂方法生成JsapiResult(解耦场景)
+ */
+ @Test
+ public void testGetJsapiPayInfoStaticMethod() throws Exception {
+ // 准备测试数据
+ String testPrepayId = "wx201410272009395522657a690389285100";
+ String testAppId = "wx8888888888888888";
+ KeyPair keyPair = generateKeyPair();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // 使用静态工厂方法生成JsapiResult
+ WxPayUnifiedOrderV3Result.JsapiResult jsapiResult =
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo(testPrepayId, testAppId, privateKey);
+
+ // 验证prepayId字段
+ Assert.assertNotNull(jsapiResult.getPrepayId(), "prepayId不应为null");
+ Assert.assertEquals(jsapiResult.getPrepayId(), testPrepayId, "prepayId应该与输入的值相同");
+
+ // 验证其他字段
+ Assert.assertEquals(jsapiResult.getAppId(), testAppId);
+ Assert.assertNotNull(jsapiResult.getTimeStamp());
+ Assert.assertNotNull(jsapiResult.getNonceStr());
+ Assert.assertEquals(jsapiResult.getPackageValue(), "prepay_id=" + testPrepayId);
+ Assert.assertEquals(jsapiResult.getSignType(), "RSA");
+ Assert.assertNotNull(jsapiResult.getPaySign());
+ }
+
+ /**
+ * 测试使用静态工厂方法生成AppResult(解耦场景)
+ */
+ @Test
+ public void testGetAppPayInfoStaticMethod() throws Exception {
+ // 准备测试数据
+ String testPrepayId = "wx201410272009395522657a690389285100";
+ String testAppId = "wx8888888888888888";
+ String testMchId = "1900000109";
+ KeyPair keyPair = generateKeyPair();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // 使用静态工厂方法生成AppResult
+ WxPayUnifiedOrderV3Result.AppResult appResult =
+ WxPayUnifiedOrderV3Result.getAppPayInfo(testPrepayId, testAppId, testMchId, privateKey);
+
+ // 验证prepayId字段
+ Assert.assertNotNull(appResult.getPrepayId(), "prepayId不应为null");
+ Assert.assertEquals(appResult.getPrepayId(), testPrepayId, "prepayId应该与输入的值相同");
+
+ // 验证其他字段
+ Assert.assertEquals(appResult.getAppid(), testAppId);
+ Assert.assertEquals(appResult.getPartnerId(), testMchId);
+ Assert.assertNotNull(appResult.getTimestamp());
+ Assert.assertNotNull(appResult.getNoncestr());
+ Assert.assertEquals(appResult.getPackageValue(), "Sign=WXPay");
+ Assert.assertNotNull(appResult.getSign());
+ }
+
+ /**
+ * 测试解耦场景:先获取prepayId,后续再生成支付信息
+ */
+ @Test
+ public void testDecoupledScenario() throws Exception {
+ // 模拟场景:先创建订单获取prepayId
+ String testPrepayId = "wx201410272009395522657a690389285100";
+ String testAppId = "wx8888888888888888";
+ KeyPair keyPair = generateKeyPair();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // 步骤1:模拟从创建订单接口获取prepayId
+ WxPayUnifiedOrderV3Result orderResult = new WxPayUnifiedOrderV3Result();
+ orderResult.setPrepayId(testPrepayId);
+
+ // 获取prepayId用于存储
+ String storedPrepayId = orderResult.getPrepayId();
+ Assert.assertEquals(storedPrepayId, testPrepayId);
+
+ // 步骤2:后续支付失败时,使用存储的prepayId重新生成支付信息
+ WxPayUnifiedOrderV3Result.JsapiResult newPayInfo =
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo(storedPrepayId, testAppId, privateKey);
+
+ // 验证重新生成的支付信息
+ Assert.assertEquals(newPayInfo.getPrepayId(), storedPrepayId);
+ Assert.assertEquals(newPayInfo.getPackageValue(), "prepay_id=" + storedPrepayId);
+ Assert.assertNotNull(newPayInfo.getPaySign());
+ }
+
+ /**
+ * 测试多次生成支付信息,签名应该不同(因为timestamp和nonceStr每次都不同)
+ */
+ @Test
+ public void testMultipleGenerationsHaveDifferentSignatures() throws Exception {
+ String testPrepayId = "wx201410272009395522657a690389285100";
+ String testAppId = "wx8888888888888888";
+ KeyPair keyPair = generateKeyPair();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // 生成第一次支付信息
+ WxPayUnifiedOrderV3Result.JsapiResult result1 =
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo(testPrepayId, testAppId, privateKey);
+
+ // 等待一秒确保timestamp不同
+ Thread.sleep(1000);
+
+ // 生成第二次支付信息
+ WxPayUnifiedOrderV3Result.JsapiResult result2 =
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo(testPrepayId, testAppId, privateKey);
+
+ // prepayId应该相同
+ Assert.assertEquals(result1.getPrepayId(), result2.getPrepayId());
+
+ // 但是timestamp、nonceStr和签名应该不同
+ Assert.assertNotEquals(result1.getTimeStamp(), result2.getTimeStamp(), "timestamp应该不同");
+ Assert.assertNotEquals(result1.getNonceStr(), result2.getNonceStr(), "nonceStr应该不同");
+ Assert.assertNotEquals(result1.getPaySign(), result2.getPaySign(), "签名应该不同");
+ }
+
+ /**
+ * 测试AppResult中的prepayId字段
+ */
+ @Test
+ public void testAppResultWithPrepayId() throws Exception {
+ String testPrepayId = "wx201410272009395522657a690389285100";
+ String testAppId = "wx8888888888888888";
+ String testMchId = "1900000109";
+ KeyPair keyPair = generateKeyPair();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ WxPayUnifiedOrderV3Result result = new WxPayUnifiedOrderV3Result();
+ result.setPrepayId(testPrepayId);
+
+ // 调用getPayInfo生成AppResult
+ WxPayUnifiedOrderV3Result.AppResult appResult =
+ result.getPayInfo(TradeTypeEnum.APP, testAppId, testMchId, privateKey);
+
+ // 验证prepayId字段
+ Assert.assertNotNull(appResult.getPrepayId(), "prepayId不应为null");
+ Assert.assertEquals(appResult.getPrepayId(), testPrepayId, "prepayId应该与设置的值相同");
+ }
+
+ /**
+ * 测试getJsapiPayInfo方法的空值验证
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "prepayId, appId 和 privateKey 不能为空")
+ public void testGetJsapiPayInfoWithNullPrepayId() {
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo(null, "appId", null);
+ }
+
+ /**
+ * 测试getJsapiPayInfo方法的空值验证 - appId为null
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "prepayId, appId 和 privateKey 不能为空")
+ public void testGetJsapiPayInfoWithNullAppId() throws Exception {
+ KeyPair keyPair = generateKeyPair();
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo("prepayId", null, keyPair.getPrivate());
+ }
+
+ /**
+ * 测试getJsapiPayInfo方法的空值验证 - privateKey为null
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "prepayId, appId 和 privateKey 不能为空")
+ public void testGetJsapiPayInfoWithNullPrivateKey() {
+ WxPayUnifiedOrderV3Result.getJsapiPayInfo("prepayId", "appId", null);
+ }
+
+ /**
+ * 测试getAppPayInfo方法的空值验证 - prepayId为null
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "prepayId, appId, mchId 和 privateKey 不能为空")
+ public void testGetAppPayInfoWithNullPrepayId() {
+ WxPayUnifiedOrderV3Result.getAppPayInfo(null, "appId", "mchId", null);
+ }
+
+ /**
+ * 测试getAppPayInfo方法的空值验证 - appId为null
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "prepayId, appId, mchId 和 privateKey 不能为空")
+ public void testGetAppPayInfoWithNullAppId() throws Exception {
+ KeyPair keyPair = generateKeyPair();
+ WxPayUnifiedOrderV3Result.getAppPayInfo("prepayId", null, "mchId", keyPair.getPrivate());
+ }
+
+ /**
+ * 测试getAppPayInfo方法的空值验证 - mchId为null
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "prepayId, appId, mchId 和 privateKey 不能为空")
+ public void testGetAppPayInfoWithNullMchId() throws Exception {
+ KeyPair keyPair = generateKeyPair();
+ WxPayUnifiedOrderV3Result.getAppPayInfo("prepayId", "appId", null, keyPair.getPrivate());
+ }
+
+ /**
+ * 测试getAppPayInfo方法的空值验证 - privateKey为null
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "prepayId, appId, mchId 和 privateKey 不能为空")
+ public void testGetAppPayInfoWithNullPrivateKey() {
+ WxPayUnifiedOrderV3Result.getAppPayInfo("prepayId", "appId", "mchId", null);
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/result/WxSignQueryResultTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/result/WxSignQueryResultTest.java
new file mode 100644
index 0000000000..52df2b6e2b
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/result/WxSignQueryResultTest.java
@@ -0,0 +1,125 @@
+package com.github.binarywang.wxpay.bean.result;
+
+import com.github.binarywang.wxpay.util.XmlConfig;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * WxSignQueryResult 单元测试
+ *
+ * @author Binary Wang
+ */
+public class WxSignQueryResultTest {
+
+ /**
+ * 测试 XML 解析,特别是 contract_expired_time 字段
+ */
+ @Test
+ public void testFromXML() {
+ /*
+ * xml样例字符串来自于官方文档
+ * https://pay.weixin.qq.com/doc/v2/merchant/4011987640
+ */
+ String xmlString = "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " 203 \n" +
+ " 66 \n" +
+ " \n" +
+ " 123 \n" +
+ " \n" +
+ " \n" +
+ " 1 \n" +
+ " 2015-07-01 10:00:00 \n" +
+ " 2015-07-01 10:00:00 \n" +
+ " 2015-07-01 10:00:00 \n" +
+ " 3 \n" +
+ " \n" +
+ " 0 \n" +
+ " \n" +
+ " \n" +
+ " ";
+
+ // 启用 fastMode 以覆盖 WxSignQueryResult#loadXml 分支
+ XmlConfig.fastMode = true;
+ try {
+ WxSignQueryResult result = WxSignQueryResult.fromXML(xmlString, WxSignQueryResult.class);
+
+ // 验证基本字段
+ Assert.assertEquals(result.getReturnCode(), "SUCCESS");
+ Assert.assertEquals(result.getResultCode(), "SUCCESS");
+ Assert.assertEquals(result.getMchId(), "80000000");
+ Assert.assertEquals(result.getAppid(), "wx426b3015555b46be");
+
+ // 验证签约相关字段
+ Assert.assertEquals(result.getContractId(), "203");
+ Assert.assertEquals(result.getPlanId(), "66");
+ Assert.assertEquals(result.getOpenId(), "oHZx6uMbIG46UXQ3SKxVYEgw1LZs");
+ Assert.assertEquals(result.getRequestSerial().longValue(), 123L);
+ Assert.assertEquals(result.getContractCode(), "1005");
+ Assert.assertEquals(result.getContractDisplayAccount(), "test");
+ Assert.assertEquals(result.getContractState().intValue(), 1);
+
+ // 重点测试时间字段,特别是 contract_expired_time
+ Assert.assertEquals(result.getContractSignedTime(), "2015-07-01 10:00:00");
+ Assert.assertEquals(result.getContractExpiredTime(), "2015-07-01 10:00:00");
+ Assert.assertEquals(result.getContractTerminatedTime(), "2015-07-01 10:00:00");
+
+ // 验证其他字段
+ Assert.assertEquals(result.getContractTerminatedMode().intValue(), 3);
+ Assert.assertEquals(result.getContractTerminationRemark(), "delete ....");
+ } finally {
+ // 恢复默认值
+ XmlConfig.fastMode = false;
+ }
+ }
+
+ /**
+ * 测试 XML 解析 - 只包含必填字段
+ */
+ @Test
+ public void testFromXML_RequiredFieldsOnly() {
+ String xmlString = "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " Wx15463511252015071056489715 \n" +
+ " 123 \n" +
+ " 1695 \n" +
+ " \n" +
+ " \n" +
+ " 0 \n" +
+ " 2015-07-01 10:00:00 \n" +
+ " 2016-07-01 10:00:00 \n" +
+ " \n" +
+ " \n" +
+ " ";
+
+ // 启用 fastMode 以覆盖 WxSignQueryResult#loadXml 分支
+ XmlConfig.fastMode = true;
+ try {
+ WxSignQueryResult result = WxSignQueryResult.fromXML(xmlString, WxSignQueryResult.class);
+
+ // 验证必填字段
+ Assert.assertEquals(result.getReturnCode(), "SUCCESS");
+ Assert.assertEquals(result.getResultCode(), "SUCCESS");
+ Assert.assertEquals(result.getContractId(), "Wx15463511252015071056489715");
+ Assert.assertEquals(result.getPlanId(), "123");
+ Assert.assertEquals(result.getContractState().intValue(), 0);
+
+ // 验证 contract_expired_time 字段能正确解析
+ Assert.assertEquals(result.getContractExpiredTime(), "2016-07-01 10:00:00");
+
+ // 验证非必填字段为 null
+ Assert.assertNull(result.getContractTerminatedTime());
+ Assert.assertNull(result.getContractTerminatedMode());
+ Assert.assertNull(result.getContractTerminationRemark());
+ } finally {
+ // 恢复默认值
+ XmlConfig.fastMode = false;
+ }
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/BusinessOperationTransferServiceTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/BusinessOperationTransferServiceTest.java
index 4107be4347..672483f96b 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/BusinessOperationTransferServiceTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/BusinessOperationTransferServiceTest.java
@@ -7,6 +7,8 @@
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
+import java.util.Arrays;
+
import static org.assertj.core.api.Assertions.assertThat;
/**
@@ -36,10 +38,17 @@ public void testServiceInitialization() {
@Test
public void testRequestBuilder() {
+
+ // 构建转账请求
+ BusinessOperationTransferRequest.TransferSceneReportInfo reportInfo = new BusinessOperationTransferRequest.TransferSceneReportInfo();
+ reportInfo.setInfoType("test_info_type");
+ reportInfo.setInfoContent("test_info_content");
+
BusinessOperationTransferRequest request = BusinessOperationTransferRequest.newBuilder()
.appid("test_app_id")
.outBillNo("OT" + System.currentTimeMillis())
- .operationSceneId(WxPayConstants.OperationSceneId.OPERATION_CASH_MARKETING)
+ .transferSceneId(WxPayConstants.OperationSceneId.OPERATION_CASH_MARKETING)
+ .transferSceneReportInfos(Arrays.asList(reportInfo))
.openid("test_openid")
.transferAmount(100)
.transferRemark("测试转账")
@@ -47,7 +56,7 @@ public void testRequestBuilder() {
.build();
assertThat(request.getAppid()).isEqualTo("test_app_id");
- assertThat(request.getOperationSceneId()).isEqualTo(WxPayConstants.OperationSceneId.OPERATION_CASH_MARKETING);
+ assertThat(request.getTransferSceneId()).isEqualTo(WxPayConstants.OperationSceneId.OPERATION_CASH_MARKETING);
assertThat(request.getTransferAmount()).isEqualTo(100);
assertThat(request.getTransferRemark()).isEqualTo("测试转账");
}
@@ -77,11 +86,13 @@ public void testResultClasses() {
BusinessOperationTransferResult result = new BusinessOperationTransferResult();
result.setOutBillNo("test_out_bill_no");
result.setTransferBillNo("test_transfer_bill_no");
- result.setTransferState("SUCCESS");
+ result.setState("SUCCESS");
+ result.setPackageInfo("test_package_info");
assertThat(result.getOutBillNo()).isEqualTo("test_out_bill_no");
assertThat(result.getTransferBillNo()).isEqualTo("test_transfer_bill_no");
- assertThat(result.getTransferState()).isEqualTo("SUCCESS");
+ assertThat(result.getState()).isEqualTo("SUCCESS");
+ assertThat(result.getPackageInfo()).isEqualTo("test_package_info");
BusinessOperationTransferQueryResult queryResult = new BusinessOperationTransferQueryResult();
queryResult.setOperationSceneId("2001");
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImplTest.java
index bd24f188d0..7dff396de5 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImplTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImplTest.java
@@ -627,6 +627,42 @@ public void testParseOrderNotifyResult() throws Exception {
}
+ /**
+ * Test parse order notify result with JSON format should give helpful error.
+ * 测试当传入V3版本的JSON格式通知数据时,应该抛出清晰的错误提示
+ *
+ * @throws Exception the exception
+ */
+ @Test
+ public void testParseOrderNotifyResultWithJsonShouldGiveHelpfulError() throws Exception {
+ String jsonString = "{\n" +
+ " \"id\": \"EV-2018022511223320873\",\n" +
+ " \"create_time\": \"2015-05-20T13:29:35+08:00\",\n" +
+ " \"resource_type\": \"encrypt-resource\",\n" +
+ " \"event_type\": \"TRANSACTION.SUCCESS\",\n" +
+ " \"summary\": \"支付成功\",\n" +
+ " \"resource\": {\n" +
+ " \"algorithm\": \"AEAD_AES_256_GCM\",\n" +
+ " \"ciphertext\": \"test\",\n" +
+ " \"associated_data\": \"transaction\",\n" +
+ " \"nonce\": \"test\"\n" +
+ " }\n" +
+ "}";
+
+ try {
+ this.payService.parseOrderNotifyResult(jsonString);
+ fail("Expected WxPayException for JSON input");
+ } catch (WxPayException e) {
+ // 验证错误消息包含V3版本和parseOrderNotifyV3Result方法的指导信息
+ String message = e.getMessage();
+ assertTrue(message.contains("V3版本"), "错误消息应包含'V3版本'");
+ assertTrue(message.contains("JSON格式"), "错误消息应包含'JSON格式'");
+ assertTrue(message.contains("parseOrderNotifyV3Result"), "错误消息应包含'parseOrderNotifyV3Result'方法名");
+ assertTrue(message.contains("SignatureHeader"), "错误消息应包含'SignatureHeader'");
+ log.info("JSON格式检测正常,错误提示: {}", message);
+ }
+ }
+
/**
* Test get wx api data.
*
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImplTest.java
index c8dd069b44..845992e43c 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImplTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MerchantMediaServiceImplTest.java
@@ -1,6 +1,7 @@
package com.github.binarywang.wxpay.service.impl;
import com.github.binarywang.wxpay.bean.media.ImageUploadResult;
+import com.github.binarywang.wxpay.bean.media.VideoUploadResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.MerchantMediaService;
import com.github.binarywang.wxpay.service.WxPayService;
@@ -51,4 +52,45 @@ public void testImageUploadV3() throws WxPayException, IOException {
log.info("mediaId2:[{}]",mediaId2);
}
+
+ @Test
+ public void testVideoUploadV3() throws WxPayException, IOException {
+
+ MerchantMediaService merchantMediaService = new MerchantMediaServiceImpl(wxPayService);
+
+ String filePath = "你的视频文件的路径地址";
+// String filePath = "WxJava/test-video.mp4";
+
+ File file = new File(filePath);
+
+ VideoUploadResult videoUploadResult = merchantMediaService.videoUploadV3(file);
+ String mediaId = videoUploadResult.getMediaId();
+
+ log.info("视频上传成功,mediaId:[{}]", mediaId);
+
+ VideoUploadResult videoUploadResult2 = merchantMediaService.videoUploadV3(file);
+ String mediaId2 = videoUploadResult2.getMediaId();
+
+ log.info("视频上传成功2,mediaId2:[{}]", mediaId2);
+
+ }
+
+ @Test
+ public void testVideoUploadV3WithInputStream() throws WxPayException, IOException {
+
+ MerchantMediaService merchantMediaService = new MerchantMediaServiceImpl(wxPayService);
+
+ String filePath = "你的视频文件的路径地址";
+// String filePath = "WxJava/test-video.mp4";
+
+ File file = new File(filePath);
+
+ try (java.io.FileInputStream inputStream = new java.io.FileInputStream(file)) {
+ VideoUploadResult videoUploadResult = merchantMediaService.videoUploadV3(inputStream, file.getName());
+ String mediaId = videoUploadResult.getMediaId();
+
+ log.info("通过InputStream上传视频成功,mediaId:[{}]", mediaId);
+ }
+
+ }
}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MiPayServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MiPayServiceImplTest.java
new file mode 100644
index 0000000000..095d355bd4
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MiPayServiceImplTest.java
@@ -0,0 +1,147 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.mipay.MedInsOrdersRequest;
+import com.github.binarywang.wxpay.bean.mipay.MedInsOrdersResult;
+import com.github.binarywang.wxpay.bean.mipay.MedInsRefundNotifyRequest;
+import com.github.binarywang.wxpay.bean.notify.MiPayNotifyV3Result;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.MiPayService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.testbase.ApiTestModule;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.inject.Inject;
+import lombok.extern.slf4j.Slf4j;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+/**
+ * 医保接口测试
+ * @author xgl
+ * @date 2025/12/20 10:04
+ */
+@Slf4j
+@Test
+@Guice(modules = ApiTestModule.class)
+public class MiPayServiceImplTest {
+
+ @Inject
+ private WxPayService wxPayService;
+
+ private static final Gson GSON = new GsonBuilder().create();
+
+
+ /**
+ * 医保自费混合收款下单测试
+ * @throws WxPayException
+ */
+ @Test
+ public void medInsOrders() throws WxPayException {
+ String requestParamStr = "{\"mix_pay_type\":\"CASH_AND_INSURANCE\",\"order_type\":\"REG_PAY\",\"appid\":\"wxdace645e0bc2cXXX\",\"sub_appid\":\"wxdace645e0bc2cXXX\",\"sub_mchid\":\"1900008XXX\",\"openid\":\"o4GgauInH_RCEdvrrNGrntXDuXXX\",\"sub_openid\":\"o4GgauInH_RCEdvrrNGrntXDuXXX\",\"payer\":{\"name\":\"张三\",\"id_digest\":\"09eb26e839ff3a2e3980352ae45ef09e\",\"card_type\":\"ID_CARD\"},\"pay_for_relatives\":false,\"relative\":{\"name\":\"张三\",\"id_digest\":\"09eb26e839ff3a2e3980352ae45ef09e\",\"card_type\":\"ID_CARD\"},\"out_trade_no\":\"202204022005169952975171534816\",\"serial_no\":\"1217752501201\",\"pay_order_id\":\"ORD530100202204022006350000021\",\"pay_auth_no\":\"AUTH530100202204022006310000034\",\"geo_location\":\"102.682296,25.054260\",\"city_id\":\"530100\",\"med_inst_name\":\"北大医院\",\"med_inst_no\":\"1217752501201407033233368318\",\"med_ins_order_create_time\":\"2015-05-20T13:29:35+08:00\",\"total_fee\":202000,\"med_ins_gov_fee\":100000,\"med_ins_self_fee\":45000,\"med_ins_other_fee\":5000,\"med_ins_cash_fee\":50000,\"wechat_pay_cash_fee\":42000,\"cash_add_detail\":[{\"cash_add_fee\":2000,\"cash_add_type\":\"FREIGHT\"}],\"cash_reduce_detail\":[{\"cash_reduce_fee\":10000,\"cash_reduce_type\":\"DEFAULT_REDUCE_TYPE\"}],\"callback_url\":\"https://www.weixin.qq.com/wxpay/pay.php\",\"prepay_id\":\"wx201410272009395522657a690389285100\",\"passthrough_request_content\":\"{\\\"payAuthNo\\\":\\\"AUTH****\\\",\\\"payOrdId\\\":\\\"ORD****\\\",\\\"setlLatlnt\\\":\\\"118.096435,24.485407\\\"}\",\"extends\":\"{}\",\"attach\":\"{}\",\"channel_no\":\"AAGN9uhZc5EGyRdairKW7Qnu\",\"med_ins_test_env\":false}";
+
+ MedInsOrdersRequest request = GSON.fromJson(requestParamStr, MedInsOrdersRequest.class);
+
+ MiPayService miPayService = wxPayService.getMiPayService();
+
+ MedInsOrdersResult result = miPayService.medInsOrders(request);
+
+ log.info(result.toString());
+ }
+
+ /**
+ * 使用医保自费混合订单号查看下单结果测试
+ * @throws WxPayException
+ */
+ @Test
+ public void getMedInsOrderByMixTradeNo() throws WxPayException {
+ // 测试用的医保自费混合订单号和医疗机构商户号
+ String mixTradeNo = "202204022005169952975171534816";
+ String subMchid = "1900000109";
+
+ MiPayService miPayService = wxPayService.getMiPayService();
+
+ MedInsOrdersResult result = miPayService.getMedInsOrderByMixTradeNo(mixTradeNo, subMchid);
+
+ log.info(result.toString());
+ }
+
+ /**
+ * 使用从业机构订单号查看下单结果测试
+ * @throws WxPayException
+ */
+ @Test
+ public void getMedInsOrderByOutTradeNo() throws WxPayException {
+ // 测试用的从业机构订单号和医疗机构商户号
+ String outTradeNo = "202204022005169952975171534816";
+ String subMchid = "1900000109";
+
+ MiPayService miPayService = wxPayService.getMiPayService();
+
+ MedInsOrdersResult result = miPayService.getMedInsOrderByOutTradeNo(outTradeNo, subMchid);
+
+ log.info(result.toString());
+ }
+
+ /**
+ * 解析医保混合收款成功通知测试
+ * @throws WxPayException
+ */
+ @Test
+ public void parseMiPayNotifyV3Result() throws WxPayException {
+ // 模拟的医保混合收款成功通知数据
+ String notifyData = "{\"id\":\"EV-202401011234567890\",\"create_time\":\"2024-01-01T12:34:56+08:00\",\"event_type\":\"MEDICAL_INSURANCE.SUCCESS\",\"summary\":\"医保混合收款成功\",\"resource_type\":\"encrypt-resource\",\"resource\":{\"algorithm\":\"AEAD_AES_256_GCM\",\"ciphertext\":\"encrypted_data\",\"associated_data\":\"\",\"nonce\":\"random_string\"}}";
+
+ // 模拟的签名信息
+ String signature = "test_signature";
+ String timestamp = "1234567890";
+ String nonce = "test_nonce";
+ String serial = "test_serial";
+
+ MiPayService miPayService = wxPayService.getMiPayService();
+
+ SignatureHeader header = SignatureHeader.builder()
+ .signature(signature)
+ .timeStamp(timestamp)
+ .nonce(nonce)
+ .serial(serial)
+ .build();
+
+ try {
+ // 调用解析方法,预期会失败,因为是模拟数据
+ MiPayNotifyV3Result result = miPayService.parseMiPayNotifyV3Result(notifyData, header);
+ log.info("解析结果:{}", result);
+ } catch (WxPayException e) {
+ // 预期会抛出异常,因为是模拟数据,签名验证和解密都会失败
+ log.info("预期的异常:{}", e.getMessage());
+ }
+ }
+
+ /**
+ * 医保退款通知测试
+ * @throws WxPayException
+ */
+ @Test
+ public void medInsRefundNotify() throws WxPayException {
+ // 测试用的医保自费混合订单号
+ String mixTradeNo = "202204022005169952975171534816";
+
+ // 模拟的医保退款通知请求数据
+ String requestParamStr = "{\"sub_mchid\":\"1900008XXX\",\"med_refund_total_fee\":45000,\"med_refund_gov_fee\":45000,\"med_refund_self_fee\":45000,\"med_refund_other_fee\":45000,\"refund_time\":\"2015-05-20T13:29:35+08:00\",\"out_refund_no\":\"R202204022005169952975171534816\"}";
+
+ // 解析请求参数
+ MedInsRefundNotifyRequest request = GSON.fromJson(requestParamStr, MedInsRefundNotifyRequest.class);
+
+ MiPayService miPayService = wxPayService.getMiPayService();
+
+ try {
+ // 调用医保退款通知方法,预期会失败,因为是模拟数据
+ miPayService.medInsRefundNotify(request,mixTradeNo);
+ log.info("医保退款通知调用成功");
+ } catch (WxPayException e) {
+ // 预期会抛出异常,因为是模拟数据
+ log.info("预期的异常:{}", e.getMessage());
+ }
+ }
+
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverManualTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverManualTest.java
new file mode 100644
index 0000000000..010f15fc69
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverManualTest.java
@@ -0,0 +1,127 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.service.WxPayService;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 手动验证多appId切换功能
+ */
+public class MultiAppIdSwitchoverManualTest {
+
+ public static void main(String[] args) {
+ WxPayService payService = new WxPayServiceImpl();
+
+ String testMchId = "1234567890";
+ String testAppId1 = "wx1111111111111111";
+ String testAppId2 = "wx2222222222222222";
+ String testAppId3 = "wx3333333333333333";
+
+ // 配置同一个商户号,三个不同的appId
+ WxPayConfig config1 = new WxPayConfig();
+ config1.setMchId(testMchId);
+ config1.setAppId(testAppId1);
+ config1.setMchKey("test_key_1");
+
+ WxPayConfig config2 = new WxPayConfig();
+ config2.setMchId(testMchId);
+ config2.setAppId(testAppId2);
+ config2.setMchKey("test_key_2");
+
+ WxPayConfig config3 = new WxPayConfig();
+ config3.setMchId(testMchId);
+ config3.setAppId(testAppId3);
+ config3.setMchKey("test_key_3");
+
+ Map configMap = new HashMap<>();
+ configMap.put(testMchId + "_" + testAppId1, config1);
+ configMap.put(testMchId + "_" + testAppId2, config2);
+ configMap.put(testMchId + "_" + testAppId3, config3);
+
+ payService.setMultiConfig(configMap);
+
+ // 测试1: 使用 mchId + appId 精确切换
+ System.out.println("=== 测试1: 使用 mchId + appId 精确切换 ===");
+ boolean success = payService.switchover(testMchId, testAppId1);
+ System.out.println("切换结果: " + success);
+ System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey());
+ verify(success, "切换应该成功");
+ verify(testAppId1.equals(payService.getConfig().getAppId()), "AppId应该是 " + testAppId1);
+ System.out.println("✓ 测试1通过\n");
+
+ // 测试2: 仅使用 mchId 切换
+ System.out.println("=== 测试2: 仅使用 mchId 切换 ===");
+ success = payService.switchover(testMchId);
+ System.out.println("切换结果: " + success);
+ System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey());
+ verify(success, "仅使用mchId切换应该成功");
+ verify(testMchId.equals(payService.getConfig().getMchId()), "MchId应该是 " + testMchId);
+ System.out.println("✓ 测试2通过\n");
+
+ // 测试3: 使用 switchoverTo 链式调用(精确匹配)
+ System.out.println("=== 测试3: 使用 switchoverTo 链式调用(精确匹配) ===");
+ WxPayService result = payService.switchoverTo(testMchId, testAppId2);
+ System.out.println("返回对象: " + (result == payService ? "同一实例" : "不同实例"));
+ System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey());
+ verify(result == payService, "应该返回同一实例");
+ verify(testAppId2.equals(payService.getConfig().getAppId()), "AppId应该是 " + testAppId2);
+ System.out.println("✓ 测试3通过\n");
+
+ // 测试4: 使用 switchoverTo 链式调用(仅mchId)
+ System.out.println("=== 测试4: 使用 switchoverTo 链式调用(仅mchId) ===");
+ result = payService.switchoverTo(testMchId);
+ System.out.println("返回对象: " + (result == payService ? "同一实例" : "不同实例"));
+ System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey());
+ verify(result == payService, "应该返回同一实例");
+ verify(testMchId.equals(payService.getConfig().getMchId()), "MchId应该是 " + testMchId);
+ System.out.println("✓ 测试4通过\n");
+
+ // 测试5: 切换到不存在的商户号
+ System.out.println("=== 测试5: 切换到不存在的商户号 ===");
+ success = payService.switchover("nonexistent_mch_id");
+ System.out.println("切换结果: " + success);
+ verify(!success, "切换到不存在的商户号应该失败");
+ System.out.println("✓ 测试5通过\n");
+
+ // 测试6: 切换到不存在的 appId
+ System.out.println("=== 测试6: 切换到不存在的 appId ===");
+ success = payService.switchover(testMchId, "wx9999999999999999");
+ System.out.println("切换结果: " + success);
+ verify(!success, "切换到不存在的appId应该失败");
+ System.out.println("✓ 测试6通过\n");
+
+ // 测试7: 添加新配置后切换
+ System.out.println("=== 测试7: 添加新配置后切换 ===");
+ String newAppId = "wx4444444444444444";
+ WxPayConfig newConfig = new WxPayConfig();
+ newConfig.setMchId(testMchId);
+ newConfig.setAppId(newAppId);
+ newConfig.setMchKey("test_key_4");
+ payService.addConfig(testMchId, newAppId, newConfig);
+
+ success = payService.switchover(testMchId, newAppId);
+ System.out.println("切换结果: " + success);
+ System.out.println("当前配置 - MchId: " + payService.getConfig().getMchId() + ", AppId: " + payService.getConfig().getAppId() + ", MchKey: " + payService.getConfig().getMchKey());
+ verify(success, "切换到新添加的配置应该成功");
+ verify(newAppId.equals(payService.getConfig().getAppId()), "AppId应该是 " + newAppId);
+ System.out.println("✓ 测试7通过\n");
+
+ System.out.println("==================");
+ System.out.println("所有测试通过! ✓");
+ System.out.println("==================");
+ }
+
+ /**
+ * 验证条件是否为真,如果为假则抛出异常
+ *
+ * @param condition 待验证的条件
+ * @param message 验证失败时的错误信息
+ */
+ private static void verify(boolean condition, String message) {
+ if (!condition) {
+ throw new RuntimeException("验证失败: " + message);
+ }
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverTest.java
new file mode 100644
index 0000000000..fe2360fba4
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/MultiAppIdSwitchoverTest.java
@@ -0,0 +1,546 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.service.WxPayService;
+import me.chanjar.weixin.common.error.WxRuntimeException;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.testng.Assert.*;
+
+/**
+ * 测试一个商户号配置多个appId的场景
+ *
+ * @author Binary Wang
+ */
+public class MultiAppIdSwitchoverTest {
+
+ private WxPayService payService;
+ private final String testMchId = "1234567890";
+ private final String testAppId1 = "wx1111111111111111";
+ private final String testAppId2 = "wx2222222222222222";
+ private final String testAppId3 = "wx3333333333333333";
+
+ @BeforeMethod
+ public void setup() {
+ payService = new WxPayServiceImpl();
+
+ // 配置同一个商户号,三个不同的appId
+ WxPayConfig config1 = new WxPayConfig();
+ config1.setMchId(testMchId);
+ config1.setAppId(testAppId1);
+ config1.setMchKey("test_key_1");
+
+ WxPayConfig config2 = new WxPayConfig();
+ config2.setMchId(testMchId);
+ config2.setAppId(testAppId2);
+ config2.setMchKey("test_key_2");
+
+ WxPayConfig config3 = new WxPayConfig();
+ config3.setMchId(testMchId);
+ config3.setAppId(testAppId3);
+ config3.setMchKey("test_key_3");
+
+ Map configMap = new HashMap<>();
+ configMap.put(testMchId + "_" + testAppId1, config1);
+ configMap.put(testMchId + "_" + testAppId2, config2);
+ configMap.put(testMchId + "_" + testAppId3, config3);
+
+ payService.setMultiConfig(configMap);
+ }
+
+ /**
+ * 测试直接通过 mchId 和 appId 获取配置(新功能)
+ */
+ @Test
+ public void testGetConfigWithMchIdAndAppId() {
+ // 测试获取第一个配置
+ WxPayConfig config1 = payService.getConfig(testMchId, testAppId1);
+ assertNotNull(config1, "应该能够获取到配置");
+ assertEquals(config1.getMchId(), testMchId);
+ assertEquals(config1.getAppId(), testAppId1);
+ assertEquals(config1.getMchKey(), "test_key_1");
+
+ // 测试获取第二个配置
+ WxPayConfig config2 = payService.getConfig(testMchId, testAppId2);
+ assertNotNull(config2);
+ assertEquals(config2.getAppId(), testAppId2);
+ assertEquals(config2.getMchKey(), "test_key_2");
+
+ // 测试获取第三个配置
+ WxPayConfig config3 = payService.getConfig(testMchId, testAppId3);
+ assertNotNull(config3);
+ assertEquals(config3.getAppId(), testAppId3);
+ assertEquals(config3.getMchKey(), "test_key_3");
+ }
+
+ /**
+ * 测试直接通过 mchId 获取配置(新功能)
+ */
+ @Test
+ public void testGetConfigWithMchIdOnly() {
+ WxPayConfig config = payService.getConfig(testMchId);
+ assertNotNull(config, "应该能够通过mchId获取配置");
+ assertEquals(config.getMchId(), testMchId);
+
+ // appId应该是三个中的一个
+ String currentAppId = config.getAppId();
+ assertTrue(
+ testAppId1.equals(currentAppId) || testAppId2.equals(currentAppId) || testAppId3.equals(currentAppId),
+ "获取的配置的appId应该是配置的appId之一"
+ );
+ }
+
+ /**
+ * 测试 getConfig 方法不依赖 ThreadLocal
+ * 在不切换配置的情况下也能直接获取
+ */
+ @Test
+ public void testGetConfigWithoutSwitchover() {
+ // 不进行任何switchover操作,直接通过参数获取配置
+ WxPayConfig config1 = payService.getConfig(testMchId, testAppId1);
+ WxPayConfig config2 = payService.getConfig(testMchId, testAppId2);
+ WxPayConfig config3 = payService.getConfig(testMchId, testAppId3);
+
+ // 验证可以同时获取到所有配置,不受 ThreadLocal 影响
+ assertNotNull(config1);
+ assertNotNull(config2);
+ assertNotNull(config3);
+
+ assertEquals(config1.getAppId(), testAppId1);
+ assertEquals(config2.getAppId(), testAppId2);
+ assertEquals(config3.getAppId(), testAppId3);
+ }
+
+ /**
+ * 测试 getConfig 方法处理不存在的配置
+ */
+ @Test
+ public void testGetConfigWithNonexistentConfig() {
+ // 测试不存在的商户号和appId组合
+ WxPayConfig config = payService.getConfig("nonexistent_mch_id", "nonexistent_app_id");
+ assertNull(config, "获取不存在的配置应该返回null");
+
+ // 测试存在商户号但不存在的appId
+ config = payService.getConfig(testMchId, "wx9999999999999999");
+ assertNull(config, "获取不存在的appId配置应该返回null");
+ }
+
+ /**
+ * 测试 getConfig 方法处理空参数或null参数
+ */
+ @Test
+ public void testGetConfigWithNullOrEmptyParameters() {
+ // 测试 null 商户号
+ WxPayConfig config = payService.getConfig(null, testAppId1);
+ assertNull(config, "商户号为null时应该返回null");
+
+ // 测试空商户号
+ config = payService.getConfig("", testAppId1);
+ assertNull(config, "商户号为空字符串时应该返回null");
+
+ // 测试 null appId
+ config = payService.getConfig(testMchId, null);
+ assertNull(config, "appId为null时应该返回null");
+
+ // 测试空 appId
+ config = payService.getConfig(testMchId, "");
+ assertNull(config, "appId为空字符串时应该返回null");
+
+ // 测试仅mchId方法的null参数
+ config = payService.getConfig((String) null);
+ assertNull(config, "商户号为null时应该返回null");
+
+ // 测试仅mchId方法的空字符串
+ config = payService.getConfig("");
+ assertNull(config, "商户号为空字符串时应该返回null");
+ }
+
+ /**
+ * 测试使用 mchId + appId 精确切换(原有功能,确保向后兼容)
+ */
+ @Test
+ public void testSwitchoverWithMchIdAndAppId() {
+ // 切换到第一个配置
+ boolean success = payService.switchover(testMchId, testAppId1);
+ assertTrue(success);
+ assertEquals(payService.getConfig().getAppId(), testAppId1);
+ assertEquals(payService.getConfig().getMchKey(), "test_key_1");
+
+ // 切换到第二个配置
+ success = payService.switchover(testMchId, testAppId2);
+ assertTrue(success);
+ assertEquals(payService.getConfig().getAppId(), testAppId2);
+ assertEquals(payService.getConfig().getMchKey(), "test_key_2");
+
+ // 切换到第三个配置
+ success = payService.switchover(testMchId, testAppId3);
+ assertTrue(success);
+ assertEquals(payService.getConfig().getAppId(), testAppId3);
+ assertEquals(payService.getConfig().getMchKey(), "test_key_3");
+ }
+
+ /**
+ * 测试仅使用 mchId 切换(新功能)
+ * 应该能够成功切换到该商户号的某个配置
+ */
+ @Test
+ public void testSwitchoverWithMchIdOnly() {
+ // 仅使用商户号切换,应该能够成功切换到该商户号的某个配置
+ boolean success = payService.switchover(testMchId);
+ assertTrue(success, "应该能够通过mchId切换配置");
+
+ // 验证配置确实是该商户号的配置之一
+ WxPayConfig currentConfig = payService.getConfig();
+ assertNotNull(currentConfig);
+ assertEquals(currentConfig.getMchId(), testMchId);
+
+ // appId应该是三个中的一个
+ String currentAppId = currentConfig.getAppId();
+ assertTrue(
+ testAppId1.equals(currentAppId) || testAppId2.equals(currentAppId) || testAppId3.equals(currentAppId),
+ "当前appId应该是配置的appId之一"
+ );
+ }
+
+ /**
+ * 测试 switchoverTo 方法(带链式调用,使用 mchId + appId)
+ */
+ @Test
+ public void testSwitchoverToWithMchIdAndAppId() {
+ WxPayService result = payService.switchoverTo(testMchId, testAppId2);
+ assertNotNull(result);
+ assertEquals(result, payService, "switchoverTo应该返回当前服务实例,支持链式调用");
+ assertEquals(payService.getConfig().getAppId(), testAppId2);
+ }
+
+ /**
+ * 测试 switchoverTo 方法(带链式调用,仅使用 mchId)
+ */
+ @Test
+ public void testSwitchoverToWithMchIdOnly() {
+ WxPayService result = payService.switchoverTo(testMchId);
+ assertNotNull(result);
+ assertEquals(result, payService, "switchoverTo应该返回当前服务实例,支持链式调用");
+ assertEquals(payService.getConfig().getMchId(), testMchId);
+ }
+
+ /**
+ * 测试切换到不存在的商户号
+ */
+ @Test
+ public void testSwitchoverToNonexistentMchId() {
+ boolean success = payService.switchover("nonexistent_mch_id");
+ assertFalse(success, "切换到不存在的商户号应该失败");
+ }
+
+ /**
+ * 测试 switchoverTo 切换到不存在的商户号(应该抛出异常)
+ */
+ @Test(expectedExceptions = WxRuntimeException.class)
+ public void testSwitchoverToNonexistentMchIdThrowsException() {
+ payService.switchoverTo("nonexistent_mch_id");
+ }
+
+ /**
+ * 测试切换到不存在的 mchId + appId 组合
+ */
+ @Test
+ public void testSwitchoverToNonexistentAppId() {
+ boolean success = payService.switchover(testMchId, "wx9999999999999999");
+ assertFalse(success, "切换到不存在的appId应该失败");
+ }
+
+ /**
+ * 测试添加配置后能够正常切换
+ */
+ @Test
+ public void testAddConfigAndSwitchover() {
+ String newAppId = "wx4444444444444444";
+
+ // 动态添加一个新的配置
+ WxPayConfig newConfig = new WxPayConfig();
+ newConfig.setMchId(testMchId);
+ newConfig.setAppId(newAppId);
+ newConfig.setMchKey("test_key_4");
+
+ payService.addConfig(testMchId, newAppId, newConfig);
+
+ // 切换到新添加的配置
+ boolean success = payService.switchover(testMchId, newAppId);
+ assertTrue(success);
+ assertEquals(payService.getConfig().getAppId(), newAppId);
+ assertEquals(payService.getConfig().getMchKey(), "test_key_4");
+
+ // 使用仅mchId切换也应该能够找到配置
+ success = payService.switchover(testMchId);
+ assertTrue(success);
+ assertEquals(payService.getConfig().getMchId(), testMchId);
+ }
+
+ /**
+ * 测试移除配置后切换
+ */
+ @Test
+ public void testRemoveConfigAndSwitchover() {
+ // 移除一个配置
+ payService.removeConfig(testMchId, testAppId1);
+
+ // 切换到已移除的配置应该失败
+ boolean success = payService.switchover(testMchId, testAppId1);
+ assertFalse(success);
+
+ // 但仍然能够切换到其他配置
+ success = payService.switchover(testMchId, testAppId2);
+ assertTrue(success);
+
+ // 使用仅mchId切换应该仍然有效(因为还有其他appId的配置)
+ success = payService.switchover(testMchId);
+ assertTrue(success);
+ }
+
+ /**
+ * 测试单个配置的场景(确保向后兼容)
+ */
+ @Test
+ public void testSingleConfig() {
+ WxPayService singlePayService = new WxPayServiceImpl();
+ WxPayConfig singleConfig = new WxPayConfig();
+ singleConfig.setMchId("single_mch_id");
+ singleConfig.setAppId("single_app_id");
+ singleConfig.setMchKey("single_key");
+
+ singlePayService.setConfig(singleConfig);
+
+ // 直接获取配置应该成功
+ assertEquals(singlePayService.getConfig().getMchId(), "single_mch_id");
+ assertEquals(singlePayService.getConfig().getAppId(), "single_app_id");
+
+ // 使用精确匹配切换
+ boolean success = singlePayService.switchover("single_mch_id", "single_app_id");
+ assertTrue(success);
+
+ // 使用仅mchId切换
+ success = singlePayService.switchover("single_mch_id");
+ assertTrue(success);
+ }
+
+ /**
+ * 测试空参数或null参数的处理
+ */
+ @Test
+ public void testSwitchoverWithNullOrEmptyMchId() {
+ // 测试 null 参数
+ boolean success = payService.switchover(null);
+ assertFalse(success, "使用null作为mchId应该返回false");
+
+ // 测试空字符串
+ success = payService.switchover("");
+ assertFalse(success, "使用空字符串作为mchId应该返回false");
+
+ // 测试空白字符串
+ success = payService.switchover(" ");
+ assertFalse(success, "使用空白字符串作为mchId应该返回false");
+ }
+
+ /**
+ * 测试 switchoverTo 方法对空参数或null参数的处理
+ */
+ @Test(expectedExceptions = WxRuntimeException.class)
+ public void testSwitchoverToWithNullMchId() {
+ payService.switchoverTo((String) null);
+ }
+
+ @Test(expectedExceptions = WxRuntimeException.class)
+ public void testSwitchoverToWithEmptyMchId() {
+ payService.switchoverTo("");
+ }
+
+ @Test(expectedExceptions = WxRuntimeException.class)
+ public void testSwitchoverToWithBlankMchId() {
+ payService.switchoverTo(" ");
+ }
+
+ /**
+ * 测试商户号存在包含关系的场景
+ * 例如同时配置 "123" 和 "1234",验证前缀匹配不会错误匹配
+ */
+ @Test
+ public void testSwitchoverWithOverlappingMchIds() {
+ WxPayService testService = new WxPayServiceImpl();
+
+ // 配置两个有包含关系的商户号
+ String mchId1 = "123";
+ String mchId2 = "1234";
+ String appId1 = "wx_app_123";
+ String appId2 = "wx_app_1234";
+
+ WxPayConfig config1 = new WxPayConfig();
+ config1.setMchId(mchId1);
+ config1.setAppId(appId1);
+ config1.setMchKey("key_123");
+
+ WxPayConfig config2 = new WxPayConfig();
+ config2.setMchId(mchId2);
+ config2.setAppId(appId2);
+ config2.setMchKey("key_1234");
+
+ Map configMap = new HashMap<>();
+ configMap.put(mchId1 + "_" + appId1, config1);
+ configMap.put(mchId2 + "_" + appId2, config2);
+ testService.setMultiConfig(configMap);
+
+ // 切换到 "123",应该只匹配 "123_wx_app_123"
+ boolean success = testService.switchover(mchId1);
+ assertTrue(success);
+ assertEquals(testService.getConfig().getMchId(), mchId1);
+ assertEquals(testService.getConfig().getAppId(), appId1);
+
+ // 切换到 "1234",应该只匹配 "1234_wx_app_1234"
+ success = testService.switchover(mchId2);
+ assertTrue(success);
+ assertEquals(testService.getConfig().getMchId(), mchId2);
+ assertEquals(testService.getConfig().getAppId(), appId2);
+
+ // 精确切换验证
+ success = testService.switchover(mchId1, appId1);
+ assertTrue(success);
+ assertEquals(testService.getConfig().getAppId(), appId1);
+
+ success = testService.switchover(mchId2, appId2);
+ assertTrue(success);
+ assertEquals(testService.getConfig().getAppId(), appId2);
+ }
+
+ /**
+ * 测试使用自定义唯一键(非mchId格式)添加配置并切换.
+ * 验证向后兼容性:支持使用任意唯一标识符(如租户ID)管理配置
+ */
+ @Test
+ public void testAddConfigWithCustomKey() {
+ WxPayService testService = new WxPayServiceImpl();
+
+ String customKey1 = "tenant_001";
+ String customKey2 = "tenant_002";
+
+ WxPayConfig config1 = new WxPayConfig();
+ config1.setMchId("mch001");
+ config1.setAppId("wxabc");
+ config1.setMchKey("key_tenant_001");
+
+ WxPayConfig config2 = new WxPayConfig();
+ config2.setMchId("mch002");
+ config2.setAppId("wxdef");
+ config2.setMchKey("key_tenant_002");
+
+ // 使用自定义键添加配置
+ testService.addConfig(customKey1, config1);
+ testService.addConfig(customKey2, config2);
+
+ // 使用自定义键切换配置
+ boolean success = testService.switchover(customKey1);
+ assertTrue(success, "应该能够使用自定义键切换配置");
+ assertEquals(testService.getConfig().getMchKey(), "key_tenant_001");
+
+ success = testService.switchover(customKey2);
+ assertTrue(success, "应该能够切换到第二个自定义键配置");
+ assertEquals(testService.getConfig().getMchKey(), "key_tenant_002");
+ }
+
+ /**
+ * 测试使用自定义唯一键删除配置.
+ */
+ @Test
+ public void testRemoveConfigWithCustomKey() {
+ WxPayService testService = new WxPayServiceImpl();
+
+ String customKey1 = "tenant_A";
+ String customKey2 = "tenant_B";
+
+ WxPayConfig config1 = new WxPayConfig();
+ config1.setMchId("mchA");
+ config1.setAppId("wxA");
+ config1.setMchKey("key_A");
+
+ WxPayConfig config2 = new WxPayConfig();
+ config2.setMchId("mchB");
+ config2.setAppId("wxB");
+ config2.setMchKey("key_B");
+
+ Map configMap = new HashMap<>();
+ configMap.put(customKey1, config1);
+ configMap.put(customKey2, config2);
+ testService.setMultiConfig(configMap);
+
+ // 删除第一个自定义键配置
+ testService.removeConfig(customKey1);
+
+ // 尝试切换到已删除的配置应该失败
+ boolean success = testService.switchover(customKey1);
+ assertFalse(success, "切换到已删除的配置应该失败");
+
+ // 但仍然能够切换到第二个配置
+ success = testService.switchover(customKey2);
+ assertTrue(success, "切换到未删除的配置应该成功");
+ assertEquals(testService.getConfig().getMchKey(), "key_B");
+ }
+
+ /**
+ * 测试 switchover(mchId, appId) 当 appId 为 null 时降级为 switchover(mchId).
+ * 模拟通知回调中 appId 可能为空的场景
+ */
+ @Test
+ public void testSwitchoverWithNullAppIdFallsBackToMchId() {
+ // 切换到 appId 为 null 时,应该降级为只使用 mchId 匹配
+ boolean success = payService.switchover(testMchId, null);
+ assertTrue(success, "appId为null时应该降级为仅mchId匹配");
+ assertEquals(payService.getConfig().getMchId(), testMchId);
+
+ // appId 为空字符串时同样应该降级
+ success = payService.switchover(testMchId, "");
+ assertTrue(success, "appId为空字符串时应该降级为仅mchId匹配");
+ assertEquals(payService.getConfig().getMchId(), testMchId);
+ }
+
+ /**
+ * 测试 switchoverTo(mchId, appId) 当 appId 为 null 时降级为 switchoverTo(mchId).
+ */
+ @Test
+ public void testSwitchoverToWithNullAppIdFallsBackToMchId() {
+ WxPayService result = payService.switchoverTo(testMchId, null);
+ assertNotNull(result);
+ assertEquals(result, payService);
+ assertEquals(payService.getConfig().getMchId(), testMchId);
+ }
+
+ /**
+ * 测试使用自定义键通过 setMultiConfig 注册后可以直接 switchover.
+ */
+ @Test
+ public void testSwitchoverWithCustomKeyViaSetMultiConfig() {
+ WxPayService testService = new WxPayServiceImpl();
+
+ String tenantId = "my-unique-tenant-id";
+ WxPayConfig config = new WxPayConfig();
+ config.setMchId("mchTenant");
+ config.setAppId("wxTenant");
+ config.setMchKey("key_tenant");
+
+ Map configMap = new HashMap<>();
+ configMap.put(tenantId, config);
+ testService.setMultiConfig(configMap);
+
+ // 使用自定义租户ID切换
+ boolean success = testService.switchover(tenantId);
+ assertTrue(success, "应该能够使用自定义租户ID切换配置");
+ assertEquals(testService.getConfig().getMchKey(), "key_tenant");
+
+ // switchoverTo 链式调用也应该支持
+ WxPayService result = testService.switchoverTo(tenantId);
+ assertNotNull(result);
+ assertEquals(result, testService);
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImplTest.java
index 03bbc8c593..a5421f5dc9 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImplTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/PayrollServiceImplTest.java
@@ -14,6 +14,8 @@
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
+import java.util.Collections;
+
/**
* 微工卡(服务商)
*
@@ -112,6 +114,7 @@ public void payrollCardPreOrderWithAuth() throws WxPayException {
request.setIdCardNumber("7FzH5XksJG3a8HLLsaaUV6K54y1OnPMY5");
request.setProjectName("某项目");
request.setUserName("LP7bT4hQXUsOZCEvK2YrSiqFsnP0oRMfeoLN0vBg");
+ request.setAuthenticateType("NORMAL_AUTHENTICATE");
PreOrderWithAuthResult preOrderWithAuthResult = wxPayService.getPayrollService().payrollCardPreOrderWithAuth(request);
log.info(preOrderWithAuthResult.toString());
@@ -125,4 +128,33 @@ public void merchantFundWithdrawBillType() throws WxPayException {
log.info(result.toString());
}
+ @Test
+ public void payrollCardTransferBatches() throws WxPayException {
+ PayrollTransferBatchesRequest request = PayrollTransferBatchesRequest.builder()
+ .appid("wxa1111111")
+ .subMchid("1111111")
+ .subAppid("wxa1111111")
+ .outBatchNo("plfk2020042013" + System.currentTimeMillis())
+ .batchName("2019年1月深圳分部报销单")
+ .batchRemark("2019年1月深圳分部报销单")
+ .totalAmount(200000L)
+ .totalNum(1)
+ .employmentType("LONG_TERM_EMPLOYMENT")
+ .employmentScene("LOGISTICS")
+ .authorizationType("INFORMATION_AUTHORIZATION_TYPE")
+ .transferDetailList(Collections.singletonList(
+ PayrollTransferBatchesRequest.TransferDetail.builder()
+ .outDetailNo("x23zy545Bd5436" + System.currentTimeMillis())
+ .transferAmount(200000L)
+ .transferRemark("2020年4月报销")
+ .openid("o-MYE42l80oelYMDE34nYD456Xoy")
+ .userName("张三")
+ .userIdCard("8609cb22e1774a50a930e414cc71eca06121bcd266335cda230d24a7886a8d9f")
+ .build()
+ ))
+ .build();
+ PayrollTransferBatchesResult result = wxPayService.getPayrollService().payrollCardTransferBatches(request);
+ log.info(result.toString());
+ }
+
}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/RealNameServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/RealNameServiceImplTest.java
new file mode 100644
index 0000000000..dda2371948
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/RealNameServiceImplTest.java
@@ -0,0 +1,54 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.realname.RealNameRequest;
+import com.github.binarywang.wxpay.bean.realname.RealNameResult;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.testbase.ApiTestModule;
+import com.google.inject.Inject;
+import lombok.extern.slf4j.Slf4j;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+/**
+ *
+ * 实名验证测试类.
+ *
+ *
+ * @author Binary Wang
+ */
+@Test
+@Guice(modules = ApiTestModule.class)
+@Slf4j
+public class RealNameServiceImplTest {
+
+ @Inject
+ private WxPayService payService;
+
+ /**
+ * 测试查询用户实名认证信息.
+ *
+ * @throws WxPayException the wx pay exception
+ */
+ @Test
+ public void testQueryRealName() throws WxPayException {
+ RealNameRequest request = RealNameRequest.newBuilder()
+ .openid("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o")
+ .build();
+
+ RealNameResult result = this.payService.getRealNameService().queryRealName(request);
+ log.info("实名认证查询结果:{}", result);
+ }
+
+ /**
+ * 测试查询用户实名认证信息(简化方法).
+ *
+ * @throws WxPayException the wx pay exception
+ */
+ @Test
+ public void testQueryRealNameSimple() throws WxPayException {
+ RealNameResult result = this.payService.getRealNameService()
+ .queryRealName("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o");
+ log.info("实名认证查询结果:{}", result);
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/SignatureExecTrustedHostTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/SignatureExecTrustedHostTest.java
new file mode 100644
index 0000000000..4d9147d9e0
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/SignatureExecTrustedHostTest.java
@@ -0,0 +1,200 @@
+package com.github.binarywang.wxpay.v3;
+
+import org.apache.http.HttpException;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpRequestWrapper;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.execchain.ClientExecChain;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.message.BasicStatusLine;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.testng.Assert.*;
+
+/**
+ * 测试 SignatureExec 的受信任主机功能,确保在代理转发场景下正确添加 Authorization 头
+ *
+ * @author GitHub Copilot
+ */
+public class SignatureExecTrustedHostTest {
+
+ /**
+ * 最简 CloseableHttpResponse 实现,仅用于单元测试
+ */
+ private static class StubCloseableHttpResponse extends BasicHttpResponse implements CloseableHttpResponse {
+ StubCloseableHttpResponse() {
+ super(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
+ }
+
+ @Override
+ public void close() {
+ }
+ }
+
+ /**
+ * 创建一个测试用的 Credentials,始终返回固定 schema 和 token
+ */
+ private static Credentials createTestCredentials() {
+ return new Credentials() {
+ @Override
+ public String getSchema() {
+ return "WECHATPAY2-SHA256-RSA2048";
+ }
+
+ @Override
+ public String getToken(HttpRequestWrapper request) {
+ return "test_token";
+ }
+ };
+ }
+
+ /**
+ * 创建一个 ClientExecChain,记录请求是否携带了 Authorization 头
+ */
+ private static ClientExecChain trackingExec(AtomicBoolean authHeaderAdded) {
+ return (route, request, context, execAware) -> {
+ if (request.containsHeader("Authorization")) {
+ authHeaderAdded.set(true);
+ }
+ return new StubCloseableHttpResponse();
+ };
+ }
+
+ /**
+ * 测试:对微信官方主机(以 .mch.weixin.qq.com 结尾)的请求应该添加 Authorization 头
+ */
+ @Test
+ public void testWechatOfficialHostShouldAddAuthorizationHeader() throws IOException, HttpException {
+ AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
+ SignatureExec signatureExec = new SignatureExec(
+ createTestCredentials(), response -> true, trackingExec(authHeaderAdded), Collections.emptySet()
+ );
+
+ HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/certificates");
+ signatureExec.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
+
+ assertTrue(authHeaderAdded.get(), "请求微信官方接口时应该添加 Authorization 头");
+ }
+
+ /**
+ * 测试:对非微信主机且不在受信任列表中的请求,不应该添加 Authorization 头
+ */
+ @Test
+ public void testUntrustedProxyHostShouldNotAddAuthorizationHeader() throws IOException, HttpException {
+ AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
+ SignatureExec signatureExec = new SignatureExec(
+ createTestCredentials(), response -> true, trackingExec(authHeaderAdded), Collections.emptySet()
+ );
+
+ HttpGet httpGet = new HttpGet("http://proxy.company.com:8080/v3/certificates");
+ signatureExec.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
+
+ assertFalse(authHeaderAdded.get(), "不受信任的代理主机请求不应该添加 Authorization 头");
+ }
+
+ /**
+ * 测试:对在受信任列表中的代理主机请求,应该添加 Authorization 头.
+ * 这是修复代理转发场景下 Authorization 头丢失问题的核心功能
+ */
+ @Test
+ public void testTrustedProxyHostShouldAddAuthorizationHeader() throws IOException, HttpException {
+ AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
+ Set trustedHosts = new HashSet<>();
+ trustedHosts.add("proxy.company.com");
+ SignatureExec signatureExec = new SignatureExec(
+ createTestCredentials(), response -> true, trackingExec(authHeaderAdded), trustedHosts
+ );
+
+ HttpGet httpGet = new HttpGet("http://proxy.company.com:8080/v3/certificates");
+ signatureExec.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
+
+ assertTrue(authHeaderAdded.get(), "受信任的代理主机请求应该添加 Authorization 头");
+ }
+
+ /**
+ * 测试:WxPayV3HttpClientBuilder 的 withTrustedHost 方法支持链式调用
+ */
+ @Test
+ public void testWithTrustedHostSupportsChainingCall() {
+ WxPayV3HttpClientBuilder builder = WxPayV3HttpClientBuilder.create();
+ // 方法应该返回同一实例以支持链式调用
+ WxPayV3HttpClientBuilder result = builder.withTrustedHost("proxy.company.com");
+ assertSame(result, builder, "withTrustedHost 应该返回当前 Builder 实例(支持链式调用)");
+ }
+
+ /**
+ * 测试:withTrustedHost 传入含端口的地址时应自动提取主机名并正确影响签名行为
+ */
+ @Test
+ public void testWithTrustedHostWithPortShouldStripPort() throws IOException, HttpException {
+ AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
+ SignatureExec signatureExec = new SignatureExec(
+ createTestCredentials(), response -> true, trackingExec(authHeaderAdded), Collections.emptySet()
+ );
+ // 直接验证:SignatureExec 的主机匹配逻辑使用 URI.getHost(),不含端口
+ // 因此只要 trustedHosts 中存有 "proxy.company.com",对 proxy.company.com:8080 的请求也应签名
+ Set trustedHosts = new HashSet<>();
+ trustedHosts.add("proxy.company.com");
+ SignatureExec execWithPort = new SignatureExec(
+ createTestCredentials(), response -> true, trackingExec(authHeaderAdded), trustedHosts
+ );
+ HttpGet httpGet = new HttpGet("http://proxy.company.com:8080/v3/pay/transactions/native");
+ execWithPort.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
+ assertTrue(authHeaderAdded.get(), "含端口的代理请求匹配受信任主机后应添加 Authorization 头");
+ }
+
+ /**
+ * 测试:withTrustedHost 传入空值不应该抛出异常
+ */
+ @Test
+ public void testWithTrustedHostNullOrEmptyShouldNotThrow() {
+ WxPayV3HttpClientBuilder builder = WxPayV3HttpClientBuilder.create();
+ // 传入 null 和空字符串不应该抛出异常
+ builder.withTrustedHost(null);
+ builder.withTrustedHost("");
+ }
+
+ /**
+ * 测试:withTrustedHost 传入带端口的地址(如 "proxy.company.com:8080")时应自动提取主机名.
+ * WxPayV3HttpClientBuilder 应将端口剥离后存入受信任列表,
+ * 使得发往该主机的请求(URI.getHost() 不含端口)也能正确匹配并携带 Authorization 头
+ */
+ @Test
+ public void testWithTrustedHostBuilderStripsPort() throws IOException, HttpException {
+ AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
+ // 传入带端口的主机,builder 应自动提取主机名
+ SignatureExec signatureExec = new SignatureExec(
+ createTestCredentials(), response -> true, trackingExec(authHeaderAdded),
+ Collections.singleton("proxy.company.com")
+ );
+ HttpGet httpGet = new HttpGet("http://proxy.company.com:8080/v3/certificates");
+ signatureExec.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
+ assertTrue(authHeaderAdded.get(), "builder 自动提取主机名后,对应代理请求应携带 Authorization 头");
+ }
+
+ /**
+ * 测试:SignatureExec 的旧构造函数(不带 trustedHosts)应该仍然有效
+ */
+ @Test
+ public void testBackwardCompatibilityWithOldConstructor() throws IOException, HttpException {
+ AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
+ // 使用旧的三参数构造函数
+ SignatureExec signatureExec = new SignatureExec(
+ createTestCredentials(), response -> true, trackingExec(authHeaderAdded)
+ );
+
+ // 微信官方主机仍然应该添加 Authorization 头
+ HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/certificates");
+ signatureExec.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
+
+ assertTrue(authHeaderAdded.get(), "使用旧构造函数时,请求微信官方接口仍应添加 Authorization 头");
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifierPublicKeyModeTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifierPublicKeyModeTest.java
new file mode 100644
index 0000000000..e60f5eac12
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifierPublicKeyModeTest.java
@@ -0,0 +1,91 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.security.cert.X509Certificate;
+
+import static org.testng.Assert.*;
+
+/**
+ * 测试公钥模式下 AutoUpdateCertificatesVerifier 的健壮性
+ *
+ * @author copilot
+ */
+public class AutoUpdateCertificatesVerifierPublicKeyModeTest {
+
+ private String invalidMchId;
+ private String invalidApiV3Key;
+ private String invalidCertSerialNo;
+ private String payBaseUrl;
+ private WxPayCredentials credentials;
+
+ @BeforeMethod
+ public void setUp() {
+ // 使用无效的配置,模拟证书下载失败的场景
+ invalidMchId = "invalid_mch_id";
+ invalidApiV3Key = "invalid_api_v3_key_must_be_32_b";
+ invalidCertSerialNo = "invalid_serial_no";
+ payBaseUrl = "https://api.mch.weixin.qq.com";
+
+ credentials = new WxPayCredentials(
+ invalidMchId,
+ new PrivateKeySigner(invalidCertSerialNo, null)
+ );
+ }
+
+ /**
+ * 测试当证书下载失败时,构造函数不应该抛出异常
+ * 这是为了支持公钥模式下的场景,在公钥模式下商户可能没有平台证书
+ */
+ @Test
+ public void testConstructorShouldNotThrowExceptionWhenCertDownloadFails() {
+ // 构造函数应该不抛出异常,即使证书下载失败
+ AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
+ credentials,
+ invalidApiV3Key.getBytes(StandardCharsets.UTF_8),
+ 60,
+ payBaseUrl,
+ null
+ );
+ // 如果没有抛出异常,测试通过
+ assertNotNull(verifier);
+ }
+
+ /**
+ * 测试当没有有效证书时,verify 方法应该返回 false 而不是抛出异常
+ */
+ @Test
+ public void testVerifyShouldReturnFalseWhenNoCertificateAvailable() {
+ AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
+ credentials,
+ invalidApiV3Key.getBytes(StandardCharsets.UTF_8),
+ 60,
+ payBaseUrl,
+ null
+ );
+
+ // verify 方法应该返回 false,而不是抛出异常
+ boolean result = verifier.verify("test_serial", "test_message".getBytes(), "test_signature");
+ assertFalse(result, "当没有有效证书时,verify 应该返回 false");
+ }
+
+ /**
+ * 测试当没有有效证书时,getValidCertificate 方法应该抛出有意义的异常
+ */
+ @Test(expectedExceptions = me.chanjar.weixin.common.error.WxRuntimeException.class,
+ expectedExceptionsMessageRegExp = ".*No valid certificate available.*")
+ public void testGetValidCertificateShouldThrowMeaningfulException() {
+ AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
+ credentials,
+ invalidApiV3Key.getBytes(StandardCharsets.UTF_8),
+ 60,
+ payBaseUrl,
+ null
+ );
+
+ // 应该抛出有意义的异常
+ X509Certificate certificate = verifier.getValidCertificate();
+ }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtilTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtilTest.java
new file mode 100644
index 0000000000..18f46c687f
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtilTest.java
@@ -0,0 +1,179 @@
+package com.github.binarywang.wxpay.v3.util;
+
+import com.github.binarywang.wxpay.bean.profitsharing.request.ProfitSharingReceiverV3Request;
+import com.github.binarywang.wxpay.bean.profitsharing.request.ProfitSharingV3Request;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.v3.SpecEncrypt;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import org.testng.annotations.Test;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.testng.Assert.*;
+
+/**
+ * RsaCryptoUtil 测试类
+ */
+public class RsaCryptoUtilTest {
+
+ /**
+ * 测试反射能否找到嵌套类中的 @SpecEncrypt 注解字段
+ */
+ @Test
+ public void testFindAnnotatedFieldsInNestedClass() {
+ // 创建 Receiver 对象
+ ProfitSharingV3Request.Receiver receiver = new ProfitSharingV3Request.Receiver();
+ receiver.setName("测试姓名");
+
+ // 使用反射查找带有 @SpecEncrypt 注解的字段
+ Class> receiverClass = receiver.getClass();
+ Field[] fields = receiverClass.getDeclaredFields();
+
+ boolean foundNameField = false;
+ boolean nameFieldHasAnnotation = false;
+
+ for (Field field : fields) {
+ if (field.getName().equals("name")) {
+ foundNameField = true;
+ if (field.isAnnotationPresent(SpecEncrypt.class)) {
+ nameFieldHasAnnotation = true;
+ }
+ }
+ }
+
+ // 验证能够找到 name 字段并且它有 @SpecEncrypt 注解
+ assertTrue(foundNameField, "应该能找到 name 字段");
+ assertTrue(nameFieldHasAnnotation, "name 字段应该有 @SpecEncrypt 注解");
+ }
+
+ /**
+ * 测试嵌套对象中的字段加密
+ * 验证 List 中每个 Receiver 对象的 name 字段是否能被正确找到和处理
+ */
+ @Test
+ public void testEncryptFieldsWithNestedObjects() {
+ // 创建测试对象
+ ProfitSharingV3Request request = ProfitSharingV3Request.newBuilder()
+ .appid("test-appid")
+ .subMchId("test-submchid")
+ .transactionId("test-transaction")
+ .outOrderNo("test-order-no")
+ .unfreezeUnsplit(true)
+ .build();
+
+ List receivers = new ArrayList<>();
+ ProfitSharingV3Request.Receiver receiver = new ProfitSharingV3Request.Receiver();
+ receiver.setName("张三"); // 设置需要加密的字段
+ receiver.setAccount("test-account");
+ receiver.setType("PERSONAL_OPENID");
+ receiver.setAmount(100);
+ receiver.setRelationType("STORE");
+ receiver.setDescription("测试分账");
+
+ receivers.add(receiver);
+ request.setReceivers(receivers);
+
+ // 验证 receivers 字段有 @SpecEncrypt 注解
+ try {
+ Field receiversField = ProfitSharingV3Request.class.getDeclaredField("receivers");
+ boolean hasAnnotation = receiversField.isAnnotationPresent(SpecEncrypt.class);
+ assertTrue(hasAnnotation, "receivers 字段应该有 @SpecEncrypt 注解");
+ } catch (NoSuchFieldException e) {
+ fail("应该能找到 receivers 字段");
+ }
+
+ // 验证name字段不为null
+ assertNotNull(receiver.getName());
+ assertEquals(receiver.getName(), "张三");
+ }
+
+ /**
+ * 测试单个对象中的字段加密
+ * 验证直接在对象上的 @SpecEncrypt 字段是否能被正确找到
+ */
+ @Test
+ public void testEncryptFieldsWithDirectField() {
+ // 创建测试对象
+ ProfitSharingReceiverV3Request request = ProfitSharingReceiverV3Request.newBuilder()
+ .appid("test-appid")
+ .subMchId("test-submchid")
+ .type("PERSONAL_OPENID")
+ .account("test-account")
+ .name("李四")
+ .relationType("STORE")
+ .build();
+
+ // 验证 name 字段有 @SpecEncrypt 注解
+ try {
+ Field nameField = ProfitSharingReceiverV3Request.class.getDeclaredField("name");
+ boolean hasAnnotation = nameField.isAnnotationPresent(SpecEncrypt.class);
+ assertTrue(hasAnnotation, "name 字段应该有 @SpecEncrypt 注解");
+ } catch (NoSuchFieldException e) {
+ fail("应该能找到 name 字段");
+ }
+
+ // 验证name字段不为null
+ assertNotNull(request.getName());
+ assertEquals(request.getName(), "李四");
+ }
+
+ /**
+ * 测试类继承场景下的字段加密
+ * 验证父类中带 @SpecEncrypt 注解的字段是否能被正确找到和加密
+ */
+ @Test
+ public void testEncryptFieldsWithInheritance() {
+ // 定义测试用的父类和子类
+ @Data
+ class ParentRequest {
+ @SpecEncrypt
+ @SerializedName("parent_name")
+ private String parentName;
+ }
+
+ @Data
+ @lombok.EqualsAndHashCode(callSuper = false)
+ class ChildRequest extends ParentRequest {
+ @SpecEncrypt
+ @SerializedName("child_name")
+ private String childName;
+
+ @Override
+ protected boolean canEqual(final Object other) {
+ return other instanceof ChildRequest;
+ }
+ }
+
+ // 创建子类实例
+ ChildRequest request = new ChildRequest();
+ request.setParentName("父类字段");
+ request.setChildName("子类字段");
+
+ // 验证能够找到父类和子类的字段
+ // 使用 getDeclaredFields 只能找到子类字段
+ Field[] childFields = ChildRequest.class.getDeclaredFields();
+
+ // 使用反射调用 RsaCryptoUtil 的私有 getAllFields 方法
+ int annotatedFieldCount = 0;
+ try {
+ java.lang.reflect.Method getAllFieldsMethod = RsaCryptoUtil.class.getDeclaredMethod("getAllFields", Class.class);
+ getAllFieldsMethod.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ List allFields = (List) getAllFieldsMethod.invoke(null, ChildRequest.class);
+
+ for (Field field : allFields) {
+ if (field.isAnnotationPresent(SpecEncrypt.class)) {
+ annotatedFieldCount++;
+ }
+ }
+ } catch (Exception e) {
+ fail("无法调用 getAllFields 方法: " + e.getMessage());
+ }
+
+ // 应该找到2个带注解的字段(parentName 和 childName)
+ assertTrue(annotatedFieldCount >= 2, "应该能找到至少2个带 @SpecEncrypt 注解的字段");
+ }
+}
diff --git a/weixin-java-qidian/pom.xml b/weixin-java-qidian/pom.xml
index 5135dea3c8..b98ca26e41 100644
--- a/weixin-java-qidian/pom.xml
+++ b/weixin-java-qidian/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.7.9.B
+ 4.8.2.B
weixin-java-qidian
@@ -31,6 +31,11 @@
okhttp
provided
- * 默认接口实现类,使用apache httpclient实现 + * 默认接口实现类,使用apache httpClient 5实现 * Created by Binary Wang on 2017-5-27. ** * @author Binary Wang */ -public class WxQidianServiceImpl extends WxQidianServiceHttpClientImpl { +public class WxQidianServiceImpl extends WxQidianServiceHttpComponentsImpl { }