-
-
Notifications
You must be signed in to change notification settings - Fork 9.1k
新增商家转账用户授权免确认相关接口 #4044
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
新增商家转账用户授权免确认相关接口 #4044
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -87,6 +87,24 @@ public class TransferBillsRequest implements Serializable { | |
| @SerializedName("transfer_scene_report_infos") | ||
| private List<TransferSceneReportInfo> transferSceneReportInfos; | ||
|
|
||
| /** | ||
| * 自动收款授权信息 | ||
| */ | ||
| @SerializedName("authorization_info") | ||
| private AuthorizationInfo authorizationInfo; | ||
|
|
||
| /** | ||
| * 微信免确认收款授权单号 | ||
| */ | ||
| @SerializedName("authorization_id") | ||
| private String authorizationId; | ||
|
Comment on lines
+99
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When callers set Useful? React with 👍 / 👎. |
||
|
|
||
| /** | ||
| * 商户侧授权单号 | ||
| */ | ||
| @SerializedName("out_authorization_no") | ||
| private String outAuthorizationNo; | ||
|
Comment on lines
+90
to
+106
|
||
|
|
||
| /** | ||
| * 收款授权模式 | ||
| * <pre> | ||
|
|
@@ -125,4 +143,28 @@ public static class TransferSceneReportInfo { | |
| @SerializedName("info_content") | ||
| private String infoContent; | ||
| } | ||
|
|
||
| @Data | ||
| @Builder(builderMethodName = "newBuilder") | ||
| @AllArgsConstructor | ||
| @NoArgsConstructor | ||
| public static class AuthorizationInfo { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Severity: medium 🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. |
||
| /** | ||
| * 用户展示名称 | ||
| */ | ||
| @SerializedName("user_display_name") | ||
| private String userDisplayName; | ||
|
|
||
| /** | ||
| * 商户侧授权单号 | ||
| */ | ||
| @SerializedName("out_authorization_no") | ||
| private String outAuthorizationNo; | ||
|
|
||
| /** | ||
| * 自动收款授权结果通知地址 | ||
| */ | ||
| @SerializedName("authorization_notify_url") | ||
| private String authorizationNotifyUrl; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,289 @@ | ||
| package com.github.binarywang.wxpay.service.impl; | ||
|
|
||
| import com.github.binarywang.wxpay.bean.transfer.ReservationTransferBatchRequest; | ||
| import com.github.binarywang.wxpay.bean.transfer.ReservationTransferBatchGetResult; | ||
| import com.github.binarywang.wxpay.bean.transfer.ReservationTransferBatchResult; | ||
| 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, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For querying an免确认收款 authorization result, the official API is keyed by Useful? React with 👍 / 👎. |
||
| 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Severity: high 🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In this test, Useful? React with 👍 / 👎. |
||
|
|
||
| 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, ""); | ||
| } | ||
|
|
||
| /** | ||
| * 通过动态代理拦截 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; | ||
| } | ||
|
Comment on lines
+313
to
+315
|
||
| 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; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When callers populate
authorizationInfo,TransferServiceImpl.transferBillsstill serializes this request to/v3/fund-app/mch-transfer/transfer-bills. WeChat documentsauthorization_infoonly forPOST /v3/fund-app/mch-transfer/transfer-bills/pre-transfer-with-authorization(https://pay.wechatpay.cn/doc/v3/merchant/4014399293); the regular transfer endpoint doesn't accept it, so this newly exposed field will produce an unsupported request instead of starting the transfer+authorization flow.Useful? React with 👍 / 👎.