🆕 #4044 【微信支付】新增商家转账用户授权免确认相关接口
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
package com.github.binarywang.wxpay.service.impl;
|
||||
|
||||
import com.github.binarywang.wxpay.bean.transfer.ReservationTransferBatchGetResult;
|
||||
import com.github.binarywang.wxpay.bean.transfer.ReservationTransferBatchRequest;
|
||||
import com.github.binarywang.wxpay.bean.transfer.ReservationTransferBatchResult;
|
||||
import com.github.binarywang.wxpay.bean.transfer.TransferBillsRequest;
|
||||
import com.github.binarywang.wxpay.bean.transfer.UserAuthorizationStatusResult;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import com.github.binarywang.wxpay.service.WxPayService;
|
||||
import com.google.gson.Gson;
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 用户授权免确认相关接口 API 路径兼容性测试
|
||||
* <p>
|
||||
* 通过动态代理拦截 WxPayService 调用,验证各接口的请求路径和参数是否符合官方文档要求,
|
||||
* 无需真实微信 API 凭据即可运行。
|
||||
* </p>
|
||||
*
|
||||
* @author GitHub Copilot
|
||||
*/
|
||||
@Test
|
||||
public class TransferAuthorizationApiCompatibilityTest {
|
||||
|
||||
private static final String BASE_URL = "https://api.mch.weixin.qq.com";
|
||||
|
||||
/**
|
||||
* 验证查询用户授权状态接口使用正确的 API 路径和查询参数。
|
||||
*/
|
||||
public void shouldGetUserAuthorizationStatusWithCorrectPath() throws WxPayException {
|
||||
RequestCaptureHandler handler = new RequestCaptureHandler();
|
||||
WxPayService wxPayService = handler.createWxPayService();
|
||||
TransferServiceImpl transferService = new TransferServiceImpl(wxPayService);
|
||||
|
||||
transferService.getUserAuthorizationStatus("oX_7Jzr9gSZz4X_Xc9-_7HGf8XzI", "1005");
|
||||
|
||||
Assert.assertEquals(handler.lastGetUrl,
|
||||
BASE_URL + "/v3/fund-app/mch-transfer/authorization/openid/oX_7Jzr9gSZz4X_Xc9-_7HGf8XzI?transfer_scene_id=1005");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证查询用户授权状态接口返回结果可正确反序列化。
|
||||
*/
|
||||
public void shouldDeserializeUserAuthorizationStatusResult() {
|
||||
Gson gson = new Gson();
|
||||
String json = "{\"appid\":\"wxf636efh5xxxxx\",\"mch_id\":\"1900000109\","
|
||||
+ "\"openid\":\"oX_7Jzr9gSZz4X_Xc9-_7HGf8XzI\","
|
||||
+ "\"authorization_state\":\"AUTHORIZED\","
|
||||
+ "\"authorize_time\":\"2024-01-01T10:00:00+08:00\","
|
||||
+ "\"deauthorize_time\":null}";
|
||||
UserAuthorizationStatusResult result = gson.fromJson(json, UserAuthorizationStatusResult.class);
|
||||
|
||||
Assert.assertEquals(result.getAppid(), "wxf636efh5xxxxx");
|
||||
Assert.assertEquals(result.getMchId(), "1900000109");
|
||||
Assert.assertEquals(result.getOpenid(), "oX_7Jzr9gSZz4X_Xc9-_7HGf8XzI");
|
||||
Assert.assertEquals(result.getAuthorizationState(), "AUTHORIZED");
|
||||
Assert.assertEquals(result.getAuthorizeTime(), "2024-01-01T10:00:00+08:00");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证批量预约商家转账接口使用正确的 API 路径。
|
||||
*/
|
||||
public void shouldReservationTransferBatchUseCorrectPath() throws WxPayException {
|
||||
RequestCaptureHandler handler = new RequestCaptureHandler();
|
||||
WxPayService wxPayService = handler.createWxPayService();
|
||||
TransferServiceImpl transferService = new TransferServiceImpl(wxPayService);
|
||||
|
||||
ReservationTransferBatchRequest request = ReservationTransferBatchRequest.newBuilder()
|
||||
.appid("wxf636efh5xxxxx")
|
||||
.outBatchNo("BATCH20240101001")
|
||||
.transferSceneId("1005")
|
||||
.batchRemark("测试批量预约转账")
|
||||
.totalAmount(1000)
|
||||
.totalNum(1)
|
||||
.transferDetailList(Collections.singletonList(
|
||||
ReservationTransferBatchRequest.TransferDetail.newBuilder()
|
||||
.outDetailNo("detail001")
|
||||
.transferAmount(1000)
|
||||
.transferRemark("测试转账")
|
||||
.openid("oX_7Jzr9gSZz4X_Xc9-_7HGf8XzI")
|
||||
// 不设置 userName,避免触发证书加密流程
|
||||
.build()
|
||||
))
|
||||
.notifyUrl("https://example.com/notify")
|
||||
.build();
|
||||
|
||||
transferService.reservationTransferBatch(request);
|
||||
|
||||
Assert.assertEquals(handler.lastPostUrl,
|
||||
BASE_URL + "/v3/fund-app/mch-transfer/reservation/transfer-batches");
|
||||
Assert.assertTrue(handler.lastPostBody.contains("\"out_batch_no\""));
|
||||
Assert.assertTrue(handler.lastPostBody.contains("BATCH20240101001"));
|
||||
Assert.assertTrue(handler.lastPostBody.contains("\"transfer_detail_list\""));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证批量预约商家转账响应结果可正确反序列化。
|
||||
*/
|
||||
public void shouldDeserializeReservationTransferBatchResult() {
|
||||
Gson gson = new Gson();
|
||||
String json = "{\"out_batch_no\":\"BATCH20240101001\","
|
||||
+ "\"reservation_batch_no\":\"1030000071100999991182020050700019480001\","
|
||||
+ "\"create_time\":\"2024-01-01T10:00:00+08:00\","
|
||||
+ "\"batch_state\":\"ACCEPTED\"}";
|
||||
ReservationTransferBatchResult result = gson.fromJson(json, ReservationTransferBatchResult.class);
|
||||
|
||||
Assert.assertEquals(result.getOutBatchNo(), "BATCH20240101001");
|
||||
Assert.assertEquals(result.getReservationBatchNo(), "1030000071100999991182020050700019480001");
|
||||
Assert.assertEquals(result.getBatchState(), "ACCEPTED");
|
||||
Assert.assertEquals(result.getCreateTime(), "2024-01-01T10:00:00+08:00");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证商户预约批次单号查询接口使用正确的 API 路径和查询参数。
|
||||
*/
|
||||
public void shouldGetReservationBatchByOutBatchNoWithCorrectPath() throws WxPayException {
|
||||
RequestCaptureHandler handler = new RequestCaptureHandler();
|
||||
WxPayService wxPayService = handler.createWxPayService();
|
||||
TransferServiceImpl transferService = new TransferServiceImpl(wxPayService);
|
||||
|
||||
transferService.getReservationTransferBatchByOutBatchNo(
|
||||
"BATCH20240101001", true, 0, 20, "SUCCESS");
|
||||
|
||||
Assert.assertTrue(handler.lastGetUrl.contains(
|
||||
"/v3/fund-app/mch-transfer/reservation/transfer-batches/out-batch-no/BATCH20240101001"));
|
||||
Assert.assertTrue(handler.lastGetUrl.contains("need_query_detail=true"));
|
||||
Assert.assertTrue(handler.lastGetUrl.contains("offset=0"));
|
||||
Assert.assertTrue(handler.lastGetUrl.contains("limit=20"));
|
||||
Assert.assertTrue(handler.lastGetUrl.contains("detail_state=SUCCESS"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证不携带可选参数时商户预约批次单号查询接口仍能正确构造 URL。
|
||||
*/
|
||||
public void shouldGetReservationBatchByOutBatchNoWithoutOptionalParams() throws WxPayException {
|
||||
RequestCaptureHandler handler = new RequestCaptureHandler();
|
||||
WxPayService wxPayService = handler.createWxPayService();
|
||||
TransferServiceImpl transferService = new TransferServiceImpl(wxPayService);
|
||||
|
||||
transferService.getReservationTransferBatchByOutBatchNo(
|
||||
"BATCH20240101001", false, null, null, null);
|
||||
|
||||
Assert.assertTrue(handler.lastGetUrl.contains(
|
||||
"/v3/fund-app/mch-transfer/reservation/transfer-batches/out-batch-no/BATCH20240101001"));
|
||||
Assert.assertTrue(handler.lastGetUrl.contains("need_query_detail=false"));
|
||||
Assert.assertFalse(handler.lastGetUrl.contains("offset="));
|
||||
Assert.assertFalse(handler.lastGetUrl.contains("limit="));
|
||||
Assert.assertFalse(handler.lastGetUrl.contains("detail_state="));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证微信预约批次单号查询接口使用正确的 API 路径和查询参数。
|
||||
*/
|
||||
public void shouldGetReservationBatchByReservationBatchNoWithCorrectPath() throws WxPayException {
|
||||
RequestCaptureHandler handler = new RequestCaptureHandler();
|
||||
WxPayService wxPayService = handler.createWxPayService();
|
||||
TransferServiceImpl transferService = new TransferServiceImpl(wxPayService);
|
||||
|
||||
transferService.getReservationTransferBatchByReservationBatchNo(
|
||||
"1030000071100999991182020050700019480001", true, 0, 20, "PROCESSING");
|
||||
|
||||
Assert.assertTrue(handler.lastGetUrl.contains(
|
||||
"/v3/fund-app/mch-transfer/reservation/transfer-batches/reservation-batch-no/"
|
||||
+ "1030000071100999991182020050700019480001"));
|
||||
Assert.assertTrue(handler.lastGetUrl.contains("need_query_detail=true"));
|
||||
Assert.assertTrue(handler.lastGetUrl.contains("offset=0"));
|
||||
Assert.assertTrue(handler.lastGetUrl.contains("limit=20"));
|
||||
Assert.assertTrue(handler.lastGetUrl.contains("detail_state=PROCESSING"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证批次查询结果可正确反序列化(包括明细列表)。
|
||||
*/
|
||||
public void shouldDeserializeReservationBatchGetResult() {
|
||||
Gson gson = new Gson();
|
||||
String json = "{\"mch_id\":\"1900000109\","
|
||||
+ "\"out_batch_no\":\"BATCH20240101001\","
|
||||
+ "\"reservation_batch_no\":\"1030000071100999991182020050700019480001\","
|
||||
+ "\"appid\":\"wxf636efh5xxxxx\","
|
||||
+ "\"batch_state\":\"FINISHED\","
|
||||
+ "\"total_amount\":1000,"
|
||||
+ "\"total_num\":1,"
|
||||
+ "\"success_amount\":1000,"
|
||||
+ "\"success_num\":1,"
|
||||
+ "\"fail_amount\":0,"
|
||||
+ "\"fail_num\":0,"
|
||||
+ "\"transfer_detail_list\":["
|
||||
+ "{\"out_detail_no\":\"detail001\","
|
||||
+ "\"transfer_bill_no\":\"bill001\","
|
||||
+ "\"detail_state\":\"SUCCESS\"}"
|
||||
+ "]}";
|
||||
ReservationTransferBatchGetResult result = gson.fromJson(json, ReservationTransferBatchGetResult.class);
|
||||
|
||||
Assert.assertEquals(result.getMchId(), "1900000109");
|
||||
Assert.assertEquals(result.getBatchState(), "FINISHED");
|
||||
Assert.assertEquals(result.getTotalAmount(), Integer.valueOf(1000));
|
||||
Assert.assertEquals(result.getSuccessNum(), Integer.valueOf(1));
|
||||
Assert.assertNotNull(result.getTransferDetailList());
|
||||
Assert.assertEquals(result.getTransferDetailList().size(), 1);
|
||||
Assert.assertEquals(result.getTransferDetailList().get(0).getDetailState(), "SUCCESS");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证关闭预约商家转账批次接口使用正确的 API 路径。
|
||||
*/
|
||||
public void shouldCloseReservationTransferBatchWithCorrectPath() throws WxPayException {
|
||||
RequestCaptureHandler handler = new RequestCaptureHandler();
|
||||
WxPayService wxPayService = handler.createWxPayService();
|
||||
TransferServiceImpl transferService = new TransferServiceImpl(wxPayService);
|
||||
|
||||
transferService.closeReservationTransferBatch("BATCH20240101001");
|
||||
|
||||
Assert.assertEquals(handler.lastPostUrl,
|
||||
BASE_URL + "/v3/fund-app/mch-transfer/reservation/transfer-batches/out-batch-no/BATCH20240101001/close");
|
||||
Assert.assertEquals(handler.lastPostBody, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 TransferBillsRequest 中授权相关字段可正确序列化为 JSON(含 @SerializedName 映射)。
|
||||
*/
|
||||
public void shouldSerializeTransferBillsRequestAuthorizationFields() {
|
||||
Gson gson = new Gson();
|
||||
TransferBillsRequest request = TransferBillsRequest.newBuilder()
|
||||
.appid("wxf636efh5xxxxx")
|
||||
.outBillNo("OUT_BILL_001")
|
||||
.transferSceneId("1005")
|
||||
.openid("oX_7Jzr9gSZz4X_Xc9-_7HGf8XzI")
|
||||
.transferAmount(100)
|
||||
.transferRemark("测试转账")
|
||||
.receiptAuthorizationMode("NO_CONFIRM_RECEIPT_AUTHORIZATION")
|
||||
.authorizationInfo(TransferBillsRequest.AuthorizationInfo.newBuilder()
|
||||
.userDisplayName("张三")
|
||||
.outAuthorizationNo("OUT_AUTH_001")
|
||||
.authorizationNotifyUrl("https://example.com/auth/notify")
|
||||
.build())
|
||||
.build();
|
||||
|
||||
String json = gson.toJson(request);
|
||||
|
||||
Assert.assertTrue(json.contains("\"receipt_authorization_mode\""),
|
||||
"JSON 应包含 receipt_authorization_mode 字段");
|
||||
Assert.assertTrue(json.contains("NO_CONFIRM_RECEIPT_AUTHORIZATION"),
|
||||
"JSON 应包含 receipt_authorization_mode 的值");
|
||||
Assert.assertTrue(json.contains("\"authorization_info\""),
|
||||
"JSON 应包含 authorization_info 嵌套字段");
|
||||
Assert.assertTrue(json.contains("\"user_display_name\""),
|
||||
"JSON 应包含 user_display_name 字段");
|
||||
Assert.assertTrue(json.contains("张三"),
|
||||
"JSON 应包含 user_display_name 的值");
|
||||
Assert.assertTrue(json.contains("\"out_authorization_no\""),
|
||||
"JSON 应包含 out_authorization_no 字段");
|
||||
Assert.assertTrue(json.contains("OUT_AUTH_001"),
|
||||
"JSON 应包含 out_authorization_no 的值");
|
||||
Assert.assertTrue(json.contains("\"authorization_notify_url\""),
|
||||
"JSON 应包含 authorization_notify_url 字段");
|
||||
Assert.assertTrue(json.contains("https://example.com/auth/notify"),
|
||||
"JSON 应包含 authorization_notify_url 的值");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证通过 authorization_id 和 out_authorization_no 顶层字段发起免确认转账时序列化正确。
|
||||
*/
|
||||
public void shouldSerializeTransferBillsRequestWithTopLevelAuthFields() {
|
||||
Gson gson = new Gson();
|
||||
TransferBillsRequest request = TransferBillsRequest.newBuilder()
|
||||
.appid("wxf636efh5xxxxx")
|
||||
.outBillNo("OUT_BILL_002")
|
||||
.transferSceneId("1005")
|
||||
.openid("oX_7Jzr9gSZz4X_Xc9-_7HGf8XzI")
|
||||
.transferAmount(200)
|
||||
.transferRemark("免确认转账")
|
||||
.receiptAuthorizationMode("NO_CONFIRM_RECEIPT_AUTHORIZATION")
|
||||
.authorizationId("AUTH_ID_001")
|
||||
.outAuthorizationNo("OUT_AUTH_002")
|
||||
.build();
|
||||
|
||||
String json = gson.toJson(request);
|
||||
|
||||
Assert.assertTrue(json.contains("\"authorization_id\""),
|
||||
"JSON 应包含 authorization_id 字段");
|
||||
Assert.assertTrue(json.contains("AUTH_ID_001"),
|
||||
"JSON 应包含 authorization_id 的值");
|
||||
Assert.assertTrue(json.contains("\"out_authorization_no\""),
|
||||
"JSON 应包含 out_authorization_no 字段");
|
||||
Assert.assertTrue(json.contains("OUT_AUTH_002"),
|
||||
"JSON 应包含 out_authorization_no 的值");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过动态代理拦截 WxPayService 请求并记录 URL/请求体,便于断言接口路径和参数。
|
||||
*/
|
||||
private static class RequestCaptureHandler implements InvocationHandler {
|
||||
private String lastPostUrl;
|
||||
private String lastPostBody;
|
||||
private String lastGetUrl;
|
||||
|
||||
private WxPayService createWxPayService() {
|
||||
return (WxPayService) Proxy.newProxyInstance(
|
||||
WxPayService.class.getClassLoader(),
|
||||
new Class<?>[]{WxPayService.class},
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) {
|
||||
if ("getPayBaseUrl".equals(method.getName())) {
|
||||
return BASE_URL;
|
||||
}
|
||||
if ("postV3".equals(method.getName())) {
|
||||
this.lastPostUrl = (String) args[0];
|
||||
this.lastPostBody = (String) args[1];
|
||||
return "{}";
|
||||
}
|
||||
if ("postV3WithWechatpaySerial".equals(method.getName())) {
|
||||
this.lastPostUrl = (String) args[0];
|
||||
this.lastPostBody = (String) args[1];
|
||||
return "{}";
|
||||
}
|
||||
if ("getV3".equals(method.getName())) {
|
||||
this.lastGetUrl = (String) args[0];
|
||||
return "{}";
|
||||
}
|
||||
if ("toString".equals(method.getName())) {
|
||||
return "MockWxPayService";
|
||||
}
|
||||
Class<?> returnType = method.getReturnType();
|
||||
if (boolean.class.equals(returnType)) {
|
||||
return false;
|
||||
}
|
||||
if (int.class.equals(returnType)) {
|
||||
return 0;
|
||||
}
|
||||
if (long.class.equals(returnType)) {
|
||||
return 0L;
|
||||
}
|
||||
if (double.class.equals(returnType)) {
|
||||
return 0D;
|
||||
}
|
||||
if (float.class.equals(returnType)) {
|
||||
return 0F;
|
||||
}
|
||||
if (short.class.equals(returnType)) {
|
||||
return (short) 0;
|
||||
}
|
||||
if (byte.class.equals(returnType)) {
|
||||
return (byte) 0;
|
||||
}
|
||||
if (char.class.equals(returnType)) {
|
||||
return (char) 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.github.binarywang.wxpay.service.impl;
|
||||
|
||||
import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesRequest;
|
||||
import com.github.binarywang.wxpay.bean.transfer.ReservationTransferBatchRequest;
|
||||
import com.github.binarywang.wxpay.bean.transfer.TransferBatchesRequest;
|
||||
import com.github.binarywang.wxpay.bean.transfer.TransferBillsRequest;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
@@ -12,6 +13,7 @@ import org.testng.annotations.Guice;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -102,4 +104,53 @@ public class TransferServiceImplTest {
|
||||
public void testGetBillsByTransferBillNo() throws WxPayException {
|
||||
log.info("微信单号查询转账单:{}", this.payService.getTransferService().getBillsByTransferBillNo("123456"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUserAuthorizationStatus() throws WxPayException {
|
||||
log.info("查询用户授权状态:{}",
|
||||
this.payService.getTransferService().getUserAuthorizationStatus("oX_7Jzr9gSZz4X_Xc9-_7HGf8XzI", "1005"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReservationTransferBatch() throws WxPayException {
|
||||
ReservationTransferBatchRequest request = ReservationTransferBatchRequest.newBuilder()
|
||||
.appid("wxf636efh5xxxxx")
|
||||
.outBatchNo("BATCH20240101001")
|
||||
.transferSceneId("1005")
|
||||
.batchRemark("测试批量预约转账")
|
||||
.totalAmount(1000)
|
||||
.totalNum(1)
|
||||
.transferDetailList(Collections.singletonList(
|
||||
ReservationTransferBatchRequest.TransferDetail.newBuilder()
|
||||
.outDetailNo("detail001")
|
||||
.transferAmount(1000)
|
||||
.transferRemark("测试转账备注")
|
||||
.openid("oX_7Jzr9gSZz4X_Xc9-_7HGf8XzI")
|
||||
.build()
|
||||
))
|
||||
.notifyUrl("https://example.com/notify")
|
||||
.build();
|
||||
log.info("批量预约商家转账:{}", this.payService.getTransferService().reservationTransferBatch(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetReservationTransferBatchByOutBatchNo() throws WxPayException {
|
||||
log.info("商户预约批次单号查询批次单:{}",
|
||||
this.payService.getTransferService()
|
||||
.getReservationTransferBatchByOutBatchNo("BATCH20240101001", true, 0, 20, "SUCCESS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetReservationTransferBatchByReservationBatchNo() throws WxPayException {
|
||||
log.info("微信预约批次单号查询批次单:{}",
|
||||
this.payService.getTransferService()
|
||||
.getReservationTransferBatchByReservationBatchNo(
|
||||
"1030000071100999991182020050700019480001", false, null, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCloseReservationTransferBatch() throws WxPayException {
|
||||
this.payService.getTransferService().closeReservationTransferBatch("BATCH20240101001");
|
||||
log.info("关闭预约商家转账批次成功");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user