diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/PreTransferWithAuthorizationRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/PreTransferWithAuthorizationRequest.java new file mode 100644 index 0000000000..e23b42c6ac --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/PreTransferWithAuthorizationRequest.java @@ -0,0 +1,154 @@ +package com.github.binarywang.wxpay.bean.transfer; + +import com.github.binarywang.wxpay.v3.SpecEncrypt; +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + * 发起转账并完成免确认收款授权请求参数. + * + *
该接口和普通 {@link TransferBillsRequest} 一样会创建商家转账单,但额外携带 + * {@code authorization_info},用于在用户确认收款时同时引导用户完成免确认收款授权。
+ * + * @see 发起转账并完成免确认收款授权 + */ +@Data +@Builder(builderMethodName = "newBuilder") +@NoArgsConstructor +@AllArgsConstructor +public class PreTransferWithAuthorizationRequest implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 商户 AppID. + */ + @SerializedName("appid") + private String appid; + + /** + * 商户系统内部的商家单号. + */ + @SerializedName("out_bill_no") + private String outBillNo; + + /** + * 转账场景 ID. + */ + @SerializedName("transfer_scene_id") + private String transferSceneId; + + /** + * 收款用户 OpenID. + */ + @SerializedName("openid") + private String openid; + + /** + * 收款用户姓名. + * + *该字段为敏感信息,提交前需要使用微信支付公钥或平台证书公钥加密。
+ */ + @SpecEncrypt + @SerializedName("user_name") + private String userName; + + /** + * 转账金额,单位为分. + */ + @SerializedName("transfer_amount") + private Integer transferAmount; + + /** + * 转账备注,用户确认收款时可见. + */ + @SerializedName("transfer_remark") + private String transferRemark; + + /** + * 转账结果通知地址. + */ + @SerializedName("notify_url") + private String notifyUrl; + + /** + * 用户收款感知. + */ + @SerializedName("user_recv_perception") + private String userRecvPerception; + + /** + * 转账场景报备信息. + */ + @SerializedName("transfer_scene_report_infos") + private List该接口用于给已经完成免确认收款授权的用户发起转账。请求中不再传 openid, + * 而是通过微信免确认收款授权单号或商户侧授权单号定位已授权用户。
+ * + * @see 用户授权后转账 + */ +@Data +@Builder(builderMethodName = "newBuilder") +@NoArgsConstructor +@AllArgsConstructor +public class TransferBillsAfterAuthorizationRequest implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 商户 AppID. + */ + @SerializedName("appid") + private String appid; + + /** + * 商户系统内部的商家转账单号. + */ + @SerializedName("out_bill_no") + private String outBillNo; + + /** + * 收款用户姓名. + * + *该字段为敏感信息,提交前需要使用微信支付公钥或平台证书公钥加密。
+ */ + @SpecEncrypt + @SerializedName("user_name") + private String userName; + + /** + * 转账金额,单位为分. + */ + @SerializedName("transfer_amount") + private Integer transferAmount; + + /** + * 转账备注. + */ + @SerializedName("transfer_remark") + private String transferRemark; + + /** + * 转账结果通知地址. + */ + @SerializedName("notify_url") + private String notifyUrl; + + /** + * 用户收款感知. + */ + @SerializedName("user_recv_perception") + private String userRecvPerception; + + /** + * 转账场景 ID. + */ + @SerializedName("transfer_scene_id") + private String transferSceneId; + + /** + * 转账场景报备信息. + */ + @SerializedName("transfer_scene_report_infos") + private List微信支付会把授权确认或授权关闭结果发送到商户在发起授权时传入的 + * {@code authorization_notify_url},商户可通过该通知保存 {@code authorization_id} + * 并用于后续用户授权后转账。
+ * + * @see 免确认收款授权结果通知 + */ +@Data +public class UserAuthorizationNotifyResult implements Serializable, + WxPayBaseNotifyV3ResultTAKING_EFFECT:授权生效中;CLOSED:授权已关闭。
+ */ + @SerializedName("state") + private String state; + + /** + * 用户确认授权的时间. + */ + @SerializedName("authorize_time") + private String authorizeTime; + + /** + * 授权关闭原因,授权状态为 CLOSED 时返回. + * + *CLOSE_VIA_MCH_API:商户通过 API 主动关闭;USER_CLOSE:用户主动关闭。
+ */ + @SerializedName("close_reason") + private String closeReason; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/UserConfirmAuthorizationRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/UserConfirmAuthorizationRequest.java new file mode 100644 index 0000000000..c0554b791d --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/UserConfirmAuthorizationRequest.java @@ -0,0 +1,102 @@ +package com.github.binarywang.wxpay.bean.transfer; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 发起免确认收款授权请求参数. + * + *该接口只创建免确认收款授权申请,不创建转账单。成功后返回的 {@code package_info} + * 需要交给业务侧用于 JSAPI/APP 调起用户授权页面。
+ * + * @see 发起免确认收款授权 + */ +@Data +@Builder(builderMethodName = "newBuilder") +@NoArgsConstructor +@AllArgsConstructor +public class UserConfirmAuthorizationRequest implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 商户侧授权单号. + */ + @SerializedName("out_authorization_no") + private String outAuthorizationNo; + + /** + * 商户 AppID. + */ + @SerializedName("appid") + private String appid; + + /** + * 收款用户 OpenID. + */ + @SerializedName("openid") + private String openid; + + /** + * 转账场景 ID. + */ + @SerializedName("transfer_scene_id") + private String transferSceneId; + + /** + * 用户展示名称,用于在授权详情中区分用户在商户侧的账号. + */ + @SerializedName("user_display_name") + private String userDisplayName; + + /** + * 用户收款感知. + */ + @SerializedName("user_recv_perception") + private String userRecvPerception; + + /** + * 授权结果通知地址. + */ + @SerializedName("authorization_notify_url") + private String authorizationNotifyUrl; + + /** + * 用户端场景信息. + */ + @SerializedName("scene_info") + private SceneInfo sceneInfo; + + /** + * 用户端场景信息. + */ + @Data + @Builder(builderMethodName = "newBuilder") + @NoArgsConstructor + @AllArgsConstructor + public static class SceneInfo implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 用户终端 IP,支持 IPv4 和 IPv6. + */ + @SerializedName("client_ip") + private String clientIp; + + /** + * 用户设备 ID. + */ + @SerializedName("device_id") + private String deviceId; + + /** + * 用户设备类型,如 IOS、ANDROID、HARMONY、OTHER. + */ + @SerializedName("device_type") + private String deviceType; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/UserConfirmAuthorizationResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/UserConfirmAuthorizationResult.java new file mode 100644 index 0000000000..eda2a75ac9 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/UserConfirmAuthorizationResult.java @@ -0,0 +1,121 @@ +package com.github.binarywang.wxpay.bean.transfer; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 免确认收款授权响应结果. + * + *发起授权、查询授权和解除授权接口返回的都是同一类授权实体,各接口返回字段会略有差异。
+ * + * @see 发起免确认收款授权 + * @see 商户单号查询授权结果 + * @see 解除免确认收款授权 + */ +@Data +@NoArgsConstructor +public class UserConfirmAuthorizationResult implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 商户侧授权单号. + */ + @SerializedName("out_authorization_no") + private String outAuthorizationNo; + + /** + * 商户 AppID. + */ + @SerializedName("appid") + private String appid; + + /** + * 用户 OpenID. + */ + @SerializedName("openid") + private String openid; + + /** + * 用户展示名称. + */ + @SerializedName("user_display_name") + private String userDisplayName; + + /** + * 微信免确认收款授权单号. + */ + @SerializedName("authorization_id") + private String authorizationId; + + /** + * 授权状态. + * + *WAIT_USER_CONFIRM:待用户确认;TAKING_EFFECT:授权生效中;CLOSED:授权已关闭。
+ */ + @SerializedName("state") + private String state; + + /** + * 用户确认授权的时间. + */ + @SerializedName("authorize_time") + private String authorizeTime; + + /** + * 授权关闭信息. + */ + @SerializedName("close_info") + private CloseInfo closeInfo; + + /** + * 转账场景 ID. + */ + @SerializedName("transfer_scene_id") + private String transferSceneId; + + /** + * 用户收款感知. + */ + @SerializedName("user_recv_perception") + private String userRecvPerception; + + /** + * 单据创建时间. + */ + @SerializedName("create_time") + private String createTime; + + /** + * 跳转授权页面的 package 信息. + */ + @SerializedName("package_info") + private String packageInfo; + + /** + * 授权关闭信息. + */ + @Data + @NoArgsConstructor + public static class CloseInfo implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 授权关闭时间. + */ + @SerializedName("close_time") + private String closeTime; + + /** + * 授权关闭原因. + * + *CLOSE_VIA_MCH_API:商户通过 API 主动关闭;USER_CLOSE:用户主动关闭; + * USER_OVERDUE_UNCONFIRMED:用户超时未确认;TRANSFER_RISK:转账风险; + * USER_ACCOUNT_ABNORMAL:用户账号异常。
+ */ + @SerializedName("close_reason") + private String closeReason; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/TransferService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/TransferService.java index e48e327505..5eb11de81e 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/TransferService.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/TransferService.java @@ -128,6 +128,45 @@ public interface TransferService { */ TransferBillsResult transferBills(TransferBillsRequest request) throws WxPayException; + /** + *
+ * 发起转账并完成免确认收款授权API
+ *
+ * 该接口与 {@link #transferBills(TransferBillsRequest)} 都会创建商家转账单,
+ * 区别是本接口可额外携带免确认收款授权信息,在用户确认收款流程中同步引导用户完成授权。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址:请求地址
+ *
+ * 文档地址:发起转账并完成免确认收款授权
+ *
+ *
+ * @param request 发起转账并完成免确认收款授权请求参数
+ * @return PreTransferWithAuthorizationResult 发起结果
+ * @throws WxPayException .
+ */
+ PreTransferWithAuthorizationResult transferBillsWithAuthorization(PreTransferWithAuthorizationRequest request) throws WxPayException;
+
+ /**
+ *
+ * 用户授权后转账API
+ *
+ * 该接口与 {@link #transferBills(TransferBillsRequest)} 都会创建商家转账单,
+ * 区别是本接口用于用户已经完成免确认收款授权后的直接转账场景。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址:请求地址
+ *
+ * 文档地址:用户授权后转账
+ *
+ *
+ * @param request 用户授权后转账请求参数
+ * @return TransferBillsAfterAuthorizationResult 转账结果
+ * @throws WxPayException .
+ */
+ TransferBillsAfterAuthorizationResult transferBillsAfterAuthorization(TransferBillsAfterAuthorizationRequest request)
+ throws WxPayException;
+
/**
*
*
@@ -192,6 +231,83 @@ public interface TransferService {
// ===================== 用户授权免确认模式相关接口 =====================
+ /**
+ *
+ * 发起免确认收款授权API
+ *
+ * 该接口只创建免确认收款授权申请,不创建转账单。接口返回的 package_info
+ * 需要用于 JSAPI/APP 调起用户授权页面。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址:请求地址
+ *
+ * 文档地址:发起免确认收款授权
+ *
+ *
+ * @param request 发起免确认收款授权请求参数
+ * @return UserConfirmAuthorizationResult 授权申请结果
+ * @throws WxPayException .
+ */
+ UserConfirmAuthorizationResult userConfirmAuthorization(UserConfirmAuthorizationRequest request) throws WxPayException;
+
+ /**
+ *
+ * 商户单号查询免确认收款授权结果API
+ *
+ * 商户可通过发起授权时传入的 out_authorization_no 查询用户是否已经完成免确认收款授权。
+ * 当返回 state 为 TAKING_EFFECT 时,表示授权生效中,可用于用户授权后转账。
+ *
+ * 请求方式:GET(HTTPS)
+ * 请求地址:请求地址
+ *
+ * 文档地址:商户单号查询授权结果
+ *
+ *
+ * @param outAuthorizationNo 商户侧授权单号
+ * @param isDisplayAuthorization 是否返回用于调起授权页面的 package_info
+ * @return UserConfirmAuthorizationResult 授权结果
+ * @throws WxPayException .
+ */
+ UserConfirmAuthorizationResult getUserConfirmAuthorizationByOutAuthorizationNo(String outAuthorizationNo,
+ Boolean isDisplayAuthorization)
+ throws WxPayException;
+
+ /**
+ *
+ * 解除免确认收款授权API
+ *
+ * 商户可通过发起授权时传入的 out_authorization_no 主动关闭用户的免确认收款授权。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址:请求地址
+ *
+ * 文档地址:解除免确认收款授权
+ *
+ *
+ * @param outAuthorizationNo 商户侧授权单号
+ * @return UserConfirmAuthorizationResult 解除授权结果
+ * @throws WxPayException .
+ */
+ UserConfirmAuthorizationResult closeUserConfirmAuthorization(String outAuthorizationNo) throws WxPayException;
+
+ /**
+ *
+ * 解析免确认收款授权结果通知
+ *
+ * 微信支付会把用户确认授权或关闭授权的结果通知到商户在发起授权时传入的 authorization_notify_url。
+ * 通知报文中的 resource 为 AES-256-GCM 加密内容,本方法会完成签名校验(传入 header 时)和资源解密。
+ *
+ * 文档地址:免确认收款授权结果通知
+ *
+ *
+ * @param notifyData 通知数据
+ * @param header 通知头部数据,不传则表示不校验头
+ * @return UserAuthorizationNotifyResult 授权通知结果
+ * @throws WxPayException .
+ */
+ UserAuthorizationNotifyResult parseUserAuthorizationNotifyResult(String notifyData, SignatureHeader header)
+ throws WxPayException;
+
/**
*
* 商户查询用户授权信息接口
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/TransferServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/TransferServiceImpl.java
index fe05ab89ad..88cec3e691 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/TransferServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/TransferServiceImpl.java
@@ -9,6 +9,7 @@
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
import java.security.cert.X509Certificate;
import java.util.List;
@@ -95,6 +96,59 @@ public TransferBillsResult transferBills(TransferBillsRequest request) throws Wx
return GSON.fromJson(result, TransferBillsResult.class);
}
+ @Override
+ public PreTransferWithAuthorizationResult transferBillsWithAuthorization(PreTransferWithAuthorizationRequest request) throws WxPayException {
+ this.checkTransferBillsWithAuthorizationRequest(request);
+ String url = String.format("%s/v3/fund-app/mch-transfer/transfer-bills/pre-transfer-with-authorization",
+ this.payService.getPayBaseUrl());
+ if (request.getUserName() != null && !request.getUserName().isEmpty()) {
+ X509Certificate validCertificate = this.payService.getConfig().getVerifier().getValidCertificate();
+ RsaCryptoUtil.encryptFields(request, validCertificate);
+ }
+ String result = this.payService.postV3WithWechatpaySerial(url, GSON.toJson(request));
+ return GSON.fromJson(result, PreTransferWithAuthorizationResult.class);
+ }
+
+ @Override
+ public TransferBillsAfterAuthorizationResult transferBillsAfterAuthorization(
+ TransferBillsAfterAuthorizationRequest request) throws WxPayException {
+ this.checkTransferBillsAfterAuthorizationRequest(request);
+ String url = String.format("%s/v3/fund-app/mch-transfer/transfer-bills/transfer",
+ this.payService.getPayBaseUrl());
+ if (request.getUserName() != null && !request.getUserName().isEmpty()) {
+ X509Certificate validCertificate = this.payService.getConfig().getVerifier().getValidCertificate();
+ RsaCryptoUtil.encryptFields(request, validCertificate);
+ }
+ String result = this.payService.postV3WithWechatpaySerial(url, GSON.toJson(request));
+ return GSON.fromJson(result, TransferBillsAfterAuthorizationResult.class);
+ }
+
+ private void checkTransferBillsWithAuthorizationRequest(PreTransferWithAuthorizationRequest request) throws WxPayException {
+ if (request == null) {
+ throw new WxPayException("发起转账并完成免确认收款授权请求参数不能为空");
+ }
+
+ PreTransferWithAuthorizationRequest.AuthorizationInfo authorizationInfo = request.getAuthorizationInfo();
+ if (authorizationInfo == null) {
+ throw new WxPayException("免确认收款授权信息authorizationInfo不能为空");
+ }
+
+ if (StringUtils.isAnyBlank(authorizationInfo.getUserDisplayName(),
+ authorizationInfo.getOutAuthorizationNo(), authorizationInfo.getAuthorizationNotifyUrl())) {
+ throw new WxPayException("免确认收款授权信息中的userDisplayName、outAuthorizationNo、authorizationNotifyUrl不能为空");
+ }
+ }
+
+ private void checkTransferBillsAfterAuthorizationRequest(TransferBillsAfterAuthorizationRequest request) throws WxPayException {
+ if (request == null) {
+ throw new WxPayException("用户授权后转账请求参数不能为空");
+ }
+
+ if (StringUtils.isBlank(request.getAuthorizationId()) && StringUtils.isBlank(request.getOutAuthorizationNo())) {
+ throw new WxPayException("用户授权后转账authorizationId和outAuthorizationNo不能同时为空");
+ }
+ }
+
@Override
public TransferBillsCancelResult transformBillsCancel(String outBillNo) throws WxPayException {
String url = String.format("%s/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/%s/cancel",
@@ -127,6 +181,51 @@ public TransferBillsNotifyResult parseTransferBillsNotifyResult(String notifyDat
// ===================== 用户授权免确认模式相关接口实现 =====================
+ @Override
+ public UserConfirmAuthorizationResult userConfirmAuthorization(UserConfirmAuthorizationRequest request) throws WxPayException {
+ String url = String.format("%s/v3/fund-app/mch-transfer/user-confirm-authorization", this.payService.getPayBaseUrl());
+ String result = this.payService.postV3(url, GSON.toJson(request));
+ return GSON.fromJson(result, UserConfirmAuthorizationResult.class);
+ }
+
+ @Override
+ public UserConfirmAuthorizationResult getUserConfirmAuthorizationByOutAuthorizationNo(String outAuthorizationNo,
+ Boolean isDisplayAuthorization)
+ throws WxPayException {
+ if (StringUtils.isBlank(outAuthorizationNo)) {
+ throw new WxPayException("商户侧授权单号outAuthorizationNo不能为空");
+ }
+
+ StringBuilder url = new StringBuilder();
+ url.append(this.payService.getPayBaseUrl())
+ .append("/v3/fund-app/mch-transfer/user-confirm-authorization/out-authorization-no/")
+ .append(outAuthorizationNo);
+ if (isDisplayAuthorization != null) {
+ url.append("?is_display_authorization=").append(isDisplayAuthorization);
+ }
+ String result = this.payService.getV3(url.toString());
+ return GSON.fromJson(result, UserConfirmAuthorizationResult.class);
+ }
+
+ @Override
+ public UserConfirmAuthorizationResult closeUserConfirmAuthorization(String outAuthorizationNo) throws WxPayException {
+ if (StringUtils.isBlank(outAuthorizationNo)) {
+ throw new WxPayException("商户侧授权单号outAuthorizationNo不能为空");
+ }
+
+ String url = String.format("%s/v3/fund-app/mch-transfer/user-confirm-authorization/out-authorization-no/%s/close",
+ this.payService.getPayBaseUrl(), outAuthorizationNo);
+ String result = this.payService.postV3(url, "");
+ return GSON.fromJson(result, UserConfirmAuthorizationResult.class);
+ }
+
+ @Override
+ public UserAuthorizationNotifyResult parseUserAuthorizationNotifyResult(String notifyData, SignatureHeader header)
+ throws WxPayException {
+ return this.payService.baseParseOrderNotifyV3Result(notifyData, header, UserAuthorizationNotifyResult.class,
+ UserAuthorizationNotifyResult.DecryptNotifyResult.class);
+ }
+
@Override
public UserAuthorizationStatusResult getUserAuthorizationStatus(String openid, String transferSceneId) throws WxPayException {
String url = String.format("%s/v3/fund-app/mch-transfer/authorization/openid/%s?transfer_scene_id=%s",
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/TransferUserAuthorizationApiCompatibilityTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/TransferUserAuthorizationApiCompatibilityTest.java
new file mode 100644
index 0000000000..ed4ccc15a7
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/TransferUserAuthorizationApiCompatibilityTest.java
@@ -0,0 +1,390 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.transfer.PreTransferWithAuthorizationRequest;
+import com.github.binarywang.wxpay.bean.transfer.PreTransferWithAuthorizationResult;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsAfterAuthorizationRequest;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsAfterAuthorizationResult;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsRequest;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
+import com.github.binarywang.wxpay.bean.transfer.UserAuthorizationNotifyResult;
+import com.github.binarywang.wxpay.bean.transfer.UserConfirmAuthorizationRequest;
+import com.github.binarywang.wxpay.bean.transfer.UserConfirmAuthorizationResult;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+@Test
+public class TransferUserAuthorizationApiCompatibilityTest {
+
+ private static final String BASE_URL = "https://api.mch.weixin.qq.com";
+
+ /**
+ * 验证新增接口不会改变原有 transferBills 发起转账接口路径.
+ */
+ public void shouldKeepTransferBillsApiPathUnchanged() throws Exception {
+ RequestCaptureHandler handler = new RequestCaptureHandler();
+ TransferServiceImpl transferService = new TransferServiceImpl(handler.createWxPayService());
+
+ TransferBillsResult result = transferService.transferBills(TransferBillsRequest.newBuilder()
+ .appid("wxf636efh567hg4356")
+ .outBillNo("plfk2020042013")
+ .transferSceneId("1000")
+ .openid("o-MYE42l80oelYMDE34nYD456Xoy")
+ .transferAmount(400000)
+ .transferRemark("新会员开通有礼")
+ .build());
+
+ Assert.assertEquals(handler.lastPostWithSerialUrl, BASE_URL + "/v3/fund-app/mch-transfer/transfer-bills");
+ Assert.assertFalse(handler.lastPostWithSerialBody.contains("authorization_info"));
+ Assert.assertEquals(result.getPackageInfo(), "transfer-package");
+ }
+
+ /**
+ * 验证“发起转账并完成免确认收款授权”使用独立接口路径和授权参数.
+ */
+ public void shouldUseTransferBillsWithAuthorizationPathAndPayload() throws Exception {
+ RequestCaptureHandler handler = new RequestCaptureHandler();
+ TransferServiceImpl transferService = new TransferServiceImpl(handler.createWxPayService());
+
+ PreTransferWithAuthorizationResult result = transferService.transferBillsWithAuthorization(
+ PreTransferWithAuthorizationRequest.newBuilder()
+ .appid("wxf636efh567hg4356")
+ .outBillNo("plfk2020042013")
+ .transferSceneId("1000")
+ .openid("o-MYE42l80oelYMDE34nYD456Xoy")
+ .transferAmount(400000)
+ .transferRemark("新会员开通有礼")
+ .authorizationInfo(PreTransferWithAuthorizationRequest.AuthorizationInfo.newBuilder()
+ .userDisplayName("wx_123456")
+ .outAuthorizationNo("auth2020042013")
+ .authorizationNotifyUrl("https://www.weixin.qq.com/wxpay/auth.php")
+ .build())
+ .build());
+
+ Assert.assertEquals(handler.lastPostWithSerialUrl,
+ BASE_URL + "/v3/fund-app/mch-transfer/transfer-bills/pre-transfer-with-authorization");
+ Assert.assertTrue(handler.lastPostWithSerialBody.contains("\"authorization_info\""));
+ Assert.assertTrue(handler.lastPostWithSerialBody.contains("\"out_authorization_no\":\"auth2020042013\""));
+ Assert.assertEquals(result.getUserDisplayName(), "wx_123456");
+ Assert.assertEquals(result.getOutAuthorizationNo(), "auth2020042013");
+ Assert.assertEquals(result.getPackageInfo(), "pre-transfer-package");
+ }
+
+ /**
+ * 验证转账并授权接口会在本地拦截缺失的免确认收款授权信息.
+ */
+ public void shouldRejectMissingAuthorizationInfo() throws Exception {
+ RequestCaptureHandler handler = new RequestCaptureHandler();
+ TransferServiceImpl transferService = new TransferServiceImpl(handler.createWxPayService());
+
+ try {
+ transferService.transferBillsWithAuthorization(PreTransferWithAuthorizationRequest.newBuilder()
+ .appid("wxf636efh567hg4356")
+ .outBillNo("plfk2020042013")
+ .build());
+ Assert.fail("缺少authorizationInfo时应抛出WxPayException");
+ } catch (WxPayException e) {
+ Assert.assertTrue(e.getMessage().contains("authorizationInfo"));
+ }
+ Assert.assertNull(handler.lastPostWithSerialUrl);
+ }
+
+ /**
+ * 验证免确认收款授权信息内部字段不能为空.
+ */
+ public void shouldRejectBlankAuthorizationInfoFields() throws Exception {
+ RequestCaptureHandler handler = new RequestCaptureHandler();
+ TransferServiceImpl transferService = new TransferServiceImpl(handler.createWxPayService());
+
+ try {
+ transferService.transferBillsWithAuthorization(PreTransferWithAuthorizationRequest.newBuilder()
+ .appid("wxf636efh567hg4356")
+ .outBillNo("plfk2020042013")
+ .authorizationInfo(PreTransferWithAuthorizationRequest.AuthorizationInfo.newBuilder()
+ .userDisplayName("wx_123456")
+ .outAuthorizationNo("")
+ .authorizationNotifyUrl("https://www.weixin.qq.com/wxpay/auth.php")
+ .build())
+ .build());
+ Assert.fail("authorizationInfo内部字段为空时应抛出WxPayException");
+ } catch (WxPayException e) {
+ Assert.assertTrue(e.getMessage().contains("outAuthorizationNo"));
+ }
+ Assert.assertNull(handler.lastPostWithSerialUrl);
+ }
+
+ /**
+ * 验证“用户授权后转账”使用独立接口路径和授权单号参数.
+ */
+ public void shouldUseTransferBillsAfterAuthorizationPathAndPayload() throws Exception {
+ RequestCaptureHandler handler = new RequestCaptureHandler();
+ TransferServiceImpl transferService = new TransferServiceImpl(handler.createWxPayService());
+
+ TransferBillsAfterAuthorizationResult result = transferService.transferBillsAfterAuthorization(
+ TransferBillsAfterAuthorizationRequest.newBuilder()
+ .appid("wxf636efh567hg4356")
+ .outBillNo("plfk2020042014")
+ .transferSceneId("1000")
+ .transferAmount(400000)
+ .transferRemark("新会员开通有礼")
+ .authorizationId("201202504101000123456789012")
+ .outAuthorizationNo("auth2020042013")
+ .build());
+
+ Assert.assertEquals(handler.lastPostWithSerialUrl,
+ BASE_URL + "/v3/fund-app/mch-transfer/transfer-bills/transfer");
+ Assert.assertTrue(handler.lastPostWithSerialBody.contains("\"authorization_id\":\"201202504101000123456789012\""));
+ Assert.assertTrue(handler.lastPostWithSerialBody.contains("\"out_authorization_no\":\"auth2020042013\""));
+ Assert.assertFalse(handler.lastPostWithSerialBody.contains("\"openid\""));
+ Assert.assertEquals(result.getState(), "SUCCESS");
+ Assert.assertEquals(result.getTransferAmount(), Integer.valueOf(400000));
+ Assert.assertEquals(result.getOpenid(), "o-MYE42l80oelYMDE34nYD456Xoy");
+ }
+
+ /**
+ * 验证用户授权后转账至少需要一个授权单号标识.
+ */
+ public void shouldRejectMissingAuthorizationIdentifiers() throws Exception {
+ RequestCaptureHandler handler = new RequestCaptureHandler();
+ TransferServiceImpl transferService = new TransferServiceImpl(handler.createWxPayService());
+
+ try {
+ transferService.transferBillsAfterAuthorization(TransferBillsAfterAuthorizationRequest.newBuilder()
+ .appid("wxf636efh567hg4356")
+ .outBillNo("plfk2020042014")
+ .build());
+ Assert.fail("缺少授权单号标识时应抛出WxPayException");
+ } catch (WxPayException e) {
+ Assert.assertTrue(e.getMessage().contains("authorizationId"));
+ }
+ Assert.assertNull(handler.lastPostWithSerialUrl);
+ }
+
+ /**
+ * 验证“发起免确认收款授权”只创建授权申请,并返回用于调起授权页的 package_info.
+ */
+ public void shouldUseUserConfirmAuthorizationPathAndPayload() throws Exception {
+ RequestCaptureHandler handler = new RequestCaptureHandler();
+ TransferServiceImpl transferService = new TransferServiceImpl(handler.createWxPayService());
+
+ UserConfirmAuthorizationResult result = transferService.userConfirmAuthorization(
+ UserConfirmAuthorizationRequest.newBuilder()
+ .outAuthorizationNo("auth2020042013")
+ .appid("wxf636efh567hg4356")
+ .openid("o-MYE42l80oelYMDE34nYD456Xoy")
+ .transferSceneId("1000")
+ .userDisplayName("wx_123456")
+ .userRecvPerception("现金奖励")
+ .authorizationNotifyUrl("https://www.weixin.qq.com/wxpay/auth.php")
+ .sceneInfo(UserConfirmAuthorizationRequest.SceneInfo.newBuilder()
+ .clientIp("113.84.136.9")
+ .deviceId("8d67f169fe104008cd20b72573a0c8c9")
+ .deviceType("IOS")
+ .build())
+ .build());
+
+ Assert.assertEquals(handler.lastPostUrl, BASE_URL + "/v3/fund-app/mch-transfer/user-confirm-authorization");
+ Assert.assertTrue(handler.lastPostBody.contains("\"scene_info\""));
+ Assert.assertTrue(handler.lastPostBody.contains("\"device_type\":\"IOS\""));
+ Assert.assertEquals(result.getState(), "WAIT_USER_CONFIRM");
+ Assert.assertEquals(result.getPackageInfo(), "authorization-package");
+ }
+
+ /**
+ * 验证商户侧授权单号查询免确认收款授权结果接口路径和响应字段.
+ */
+ public void shouldGetUserConfirmAuthorizationByOutAuthorizationNo() throws Exception {
+ RequestCaptureHandler handler = new RequestCaptureHandler();
+ TransferServiceImpl transferService = new TransferServiceImpl(handler.createWxPayService());
+
+ UserConfirmAuthorizationResult result = transferService.getUserConfirmAuthorizationByOutAuthorizationNo(
+ "auth2020042013", true);
+
+ Assert.assertEquals(handler.lastGetUrl,
+ BASE_URL + "/v3/fund-app/mch-transfer/user-confirm-authorization/out-authorization-no/auth2020042013"
+ + "?is_display_authorization=true");
+ Assert.assertEquals(result.getState(), "TAKING_EFFECT");
+ Assert.assertEquals(result.getAuthorizationId(), "201202504101000123456789012");
+ Assert.assertEquals(result.getTransferSceneId(), "1000");
+ Assert.assertEquals(result.getPackageInfo(), "authorization-package");
+ }
+
+ /**
+ * 验证解除免确认收款授权接口路径和关闭信息解析.
+ */
+ public void shouldCloseUserConfirmAuthorization() throws Exception {
+ RequestCaptureHandler handler = new RequestCaptureHandler();
+ TransferServiceImpl transferService = new TransferServiceImpl(handler.createWxPayService());
+
+ UserConfirmAuthorizationResult result = transferService.closeUserConfirmAuthorization("auth2020042013");
+
+ Assert.assertEquals(handler.lastPostUrl,
+ BASE_URL + "/v3/fund-app/mch-transfer/user-confirm-authorization/out-authorization-no/auth2020042013/close");
+ Assert.assertEquals(handler.lastPostBody, "");
+ Assert.assertEquals(result.getState(), "CLOSED");
+ Assert.assertNotNull(result.getCloseInfo());
+ Assert.assertEquals(result.getCloseInfo().getCloseReason(), "CLOSE_VIA_MCH_API");
+ }
+
+ /**
+ * 验证查询和解除授权接口会拦截空的商户侧授权单号.
+ */
+ public void shouldRejectBlankOutAuthorizationNoForAuthorizationQueryAndClose() throws Exception {
+ RequestCaptureHandler handler = new RequestCaptureHandler();
+ TransferServiceImpl transferService = new TransferServiceImpl(handler.createWxPayService());
+
+ try {
+ transferService.getUserConfirmAuthorizationByOutAuthorizationNo(" ", true);
+ Assert.fail("查询授权结果缺少outAuthorizationNo时应抛出WxPayException");
+ } catch (WxPayException e) {
+ Assert.assertTrue(e.getMessage().contains("outAuthorizationNo"));
+ }
+ try {
+ transferService.closeUserConfirmAuthorization("");
+ Assert.fail("解除授权缺少outAuthorizationNo时应抛出WxPayException");
+ } catch (WxPayException e) {
+ Assert.assertTrue(e.getMessage().contains("outAuthorizationNo"));
+ }
+ Assert.assertNull(handler.lastGetUrl);
+ Assert.assertNull(handler.lastPostUrl);
+ }
+
+ /**
+ * 验证免确认收款授权结果通知会复用微信支付V3通知解析能力,并绑定正确的结果类型.
+ */
+ public void shouldParseUserAuthorizationNotifyResult() throws Exception {
+ RequestCaptureHandler handler = new RequestCaptureHandler();
+ TransferServiceImpl transferService = new TransferServiceImpl(handler.createWxPayService());
+
+ UserAuthorizationNotifyResult result = transferService.parseUserAuthorizationNotifyResult("{\"id\":\"notify-id\"}", null);
+
+ Assert.assertEquals(handler.lastNotifyResultType, UserAuthorizationNotifyResult.class);
+ Assert.assertEquals(handler.lastNotifyDataType, UserAuthorizationNotifyResult.DecryptNotifyResult.class);
+ Assert.assertEquals(result.getResult().getOutAuthorizationNo(), "auth2020042013");
+ Assert.assertEquals(result.getResult().getAuthorizationId(), "201202504101000123456789012");
+ Assert.assertEquals(result.getResult().getState(), "TAKING_EFFECT");
+ Assert.assertEquals(result.getResult().getAuthorizeTime(), "2015-05-20T13:29:35.120+08:00");
+ }
+
+ /**
+ * 通过动态代理拦截 WxPayService 请求,便于断言接口路径、请求体和响应解析.
+ */
+ private static class RequestCaptureHandler implements InvocationHandler {
+ private String lastPostUrl;
+ private String lastPostBody;
+ private String lastPostWithSerialUrl;
+ private String lastPostWithSerialBody;
+ private String lastGetUrl;
+ private Class> lastNotifyResultType;
+ private Class> lastNotifyDataType;
+
+ private WxPayService createWxPayService() {
+ return (WxPayService) Proxy.newProxyInstance(
+ WxPayService.class.getClassLoader(),
+ new Class>[]{WxPayService.class},
+ this
+ );
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) {
+ if ("getPayBaseUrl".equals(method.getName())) {
+ return BASE_URL;
+ }
+ if ("getV3".equals(method.getName())) {
+ this.lastGetUrl = (String) args[0];
+ return "{\"out_authorization_no\":\"auth2020042013\",\"appid\":\"wxf636efh567hg4356\","
+ + "\"openid\":\"o-MYE42l80oelYMDE34nYD456Xoy\",\"user_display_name\":\"wx_123456\","
+ + "\"authorization_id\":\"201202504101000123456789012\",\"state\":\"TAKING_EFFECT\","
+ + "\"authorize_time\":\"2015-05-20T13:29:35.120+08:00\",\"transfer_scene_id\":\"1000\","
+ + "\"user_recv_perception\":\"现金奖励\",\"create_time\":\"2015-05-20T13:29:35.120+08:00\","
+ + "\"package_info\":\"authorization-package\"}";
+ }
+ if ("postV3".equals(method.getName())) {
+ this.lastPostUrl = (String) args[0];
+ this.lastPostBody = (String) args[1];
+ if (this.lastPostUrl.endsWith("/close")) {
+ return "{\"out_authorization_no\":\"auth2020042013\",\"appid\":\"wxf636efh567hg4356\","
+ + "\"openid\":\"o-MYE42l80oelYMDE34nYD456Xoy\",\"user_display_name\":\"wx_123456\","
+ + "\"authorization_id\":\"201202504101000123456789012\",\"state\":\"CLOSED\","
+ + "\"authorize_time\":\"2015-05-20T13:29:35.120+08:00\","
+ + "\"close_info\":{\"close_time\":\"2015-05-20T13:29:35.120+08:00\","
+ + "\"close_reason\":\"CLOSE_VIA_MCH_API\"}}";
+ }
+ return "{\"out_authorization_no\":\"auth2020042013\",\"state\":\"WAIT_USER_CONFIRM\","
+ + "\"create_time\":\"2015-05-20T13:29:35.120+08:00\",\"package_info\":\"authorization-package\"}";
+ }
+ if ("postV3WithWechatpaySerial".equals(method.getName())) {
+ this.lastPostWithSerialUrl = (String) args[0];
+ this.lastPostWithSerialBody = (String) args[1];
+ if (this.lastPostWithSerialUrl.endsWith("/pre-transfer-with-authorization")) {
+ return "{\"out_bill_no\":\"plfk2020042013\",\"transfer_bill_no\":\"1330000071100999991182020050700019480001\","
+ + "\"create_time\":\"2015-05-20T13:29:35.120+08:00\",\"state\":\"WAIT_USER_CONFIRM\","
+ + "\"package_info\":\"pre-transfer-package\",\"user_display_name\":\"wx_123456\","
+ + "\"out_authorization_no\":\"auth2020042013\"}";
+ }
+ if (this.lastPostWithSerialUrl.endsWith("/transfer-bills/transfer")) {
+ return "{\"mch_id\":\"1900001109\",\"out_bill_no\":\"plfk2020042014\","
+ + "\"transfer_bill_no\":\"1330000071100999991182020050700019480002\","
+ + "\"appid\":\"wxf636efh567hg4356\",\"state\":\"SUCCESS\",\"transfer_amount\":400000,"
+ + "\"transfer_remark\":\"新会员开通有礼\",\"openid\":\"o-MYE42l80oelYMDE34nYD456Xoy\","
+ + "\"user_name\":\"张三\",\"create_time\":\"2015-05-20T13:29:35.120+08:00\","
+ + "\"update_time\":\"2015-05-20T13:29:35.120+08:00\"}";
+ }
+ return "{\"out_bill_no\":\"plfk2020042013\",\"transfer_bill_no\":\"1330000071100999991182020050700019480001\","
+ + "\"create_time\":\"2015-05-20T13:29:35.120+08:00\",\"state\":\"WAIT_USER_CONFIRM\","
+ + "\"package_info\":\"transfer-package\"}";
+ }
+ if ("baseParseOrderNotifyV3Result".equals(method.getName())) {
+ this.lastNotifyResultType = (Class>) args[2];
+ this.lastNotifyDataType = (Class>) args[3];
+ UserAuthorizationNotifyResult notifyResult = new UserAuthorizationNotifyResult();
+ UserAuthorizationNotifyResult.DecryptNotifyResult decryptResult =
+ new UserAuthorizationNotifyResult.DecryptNotifyResult();
+ decryptResult.setOutAuthorizationNo("auth2020042013");
+ decryptResult.setAppid("wxf636efh567hg4356");
+ decryptResult.setOpenid("o-MYE42l80oelYMDE34nYD456Xoy");
+ decryptResult.setUserDisplayName("wx_123456");
+ decryptResult.setAuthorizationId("201202504101000123456789012");
+ decryptResult.setState("TAKING_EFFECT");
+ decryptResult.setAuthorizeTime("2015-05-20T13:29:35.120+08:00");
+ notifyResult.setResult(decryptResult);
+ return notifyResult;
+ }
+ if ("toString".equals(method.getName())) {
+ return "MockWxPayService";
+ }
+ Class> returnType = method.getReturnType();
+ if (boolean.class.equals(returnType)) {
+ return false;
+ }
+ if (int.class.equals(returnType)) {
+ return 0;
+ }
+ if (long.class.equals(returnType)) {
+ return 0L;
+ }
+ if (double.class.equals(returnType)) {
+ return 0D;
+ }
+ if (float.class.equals(returnType)) {
+ return 0F;
+ }
+ if (short.class.equals(returnType)) {
+ return (short) 0;
+ }
+ if (byte.class.equals(returnType)) {
+ return (byte) 0;
+ }
+ if (char.class.equals(returnType)) {
+ return (char) 0;
+ }
+ return null;
+ }
+ }
+}