1
0
mirror of synced 2026-05-20 09:16:25 +08:00

🎨 #3994 【微信支付】对齐新版「商家转账-电子回单」接口规范:将直连商户/服务商的电子回单申请与查询接口从旧版 v3/transfer/bill-receipt 切换到新版 v3/fund-app/mch-transfer/elecsign/out-bill-no/

This commit is contained in:
Copilot
2026-05-11 13:30:23 +08:00
committed by GitHub
parent b9bf178048
commit 403483402a
9 changed files with 185 additions and 46 deletions

View File

@@ -29,16 +29,16 @@ public class BillReceiptResult implements Serializable {
/**
* <pre>
* 字段名:商家批次单号
* 变量名out_batch_no
* 字段名:商户转账单号
* 变量名out_bill_no
* 是否必填:是
* 类型string[5,32]
* 描述:
* 商户系统内部的商家批次单号,在商户系统内部唯一。需要电子回单的批次单号
* 商户系统内部的商户转账单号,在商户系统内部唯一。兼容旧字段out_batch_no
* 示例值plfk2020042013
* </pre>
*/
@SerializedName(value = "out_batch_no")
@SerializedName(value = "out_bill_no", alternate = {"out_batch_no"})
private String outBatchNo;
/**
@@ -58,17 +58,18 @@ public class BillReceiptResult implements Serializable {
/**
* <pre>
* 字段名:电子回单状态
* 变量名signature_status
* 变量名state
* 是否必填:否
* 类型string[1,10]
* 描述:
* 枚举值:
* ACCEPTED:已受理,电子签章已受理成功
* FINISHED:已完成。电子签章已处理完成
* 兼容旧字段signature_status
* 示例值ACCEPTED
* </pre>
*/
@SerializedName(value = "signature_status")
@SerializedName(value = "state", alternate = {"signature_status"})
private String signatureStatus;
/**

View File

@@ -9,7 +9,7 @@ import java.io.Serializable;
/**
* 转账电子回单申请受理API
* <pre>
* 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/transfer/chapter4_1.shtml
* 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012716452
* </pre>
*
* @author xiaoqiang
@@ -21,15 +21,15 @@ public class ReceiptBillRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:商家批次单号
* 变量名out_batch_no
* 字段名:商户转账单号
* 变量名out_bill_no
* 是否必填:是
* 类型string[5, 32]
* 描述:
* body商户系统内部的商家批次单号,在商户系统内部唯一。需要电子回单的批次单号
* body商户系统内部的商户转账单号,在商户系统内部唯一。兼容旧字段out_batch_no
* 示例值plfk2020042013
* </pre>
*/
@SerializedName(value = "out_batch_no")
@SerializedName(value = "out_bill_no", alternate = {"out_batch_no"})
private String outBatchNo;
}

View File

@@ -24,15 +24,15 @@ public class ElectronicBillApplyRequest implements Serializable {
private static final long serialVersionUID = -2121536206019844928L;
/**
* <pre>
* 字段名:商家批次单号
* 变量名out_batch_no
* 字段名:商户转账单号
* 变量名out_bill_no
* 是否必填:是
* 类型string[5,32]
* 描述:
* body商户系统内部的商家批次单号,在商户系统内部唯一。需要电子回单的批次单号
* body商户系统内部的商户转账单号,在商户系统内部唯一。兼容旧字段out_batch_no
* 示例值plfk2020042013
* </pre>
*/
@SerializedName("out_batch_no")
@SerializedName(value = "out_bill_no", alternate = {"out_batch_no"})
private String outBatchNo;
}

View File

@@ -24,16 +24,16 @@ public class ElectronicBillResult implements Serializable {
private static final long serialVersionUID = 7528245102572829190L;
/**
* <pre>
* 字段名:商家批次单号
* 变量名out_batch_no
* 字段名:商户转账单号
* 变量名out_bill_no
* 是否必填:是
* 类型string[5,32]
* 描述:
* body商户系统内部的商家批次单号,在商户系统内部唯一。需要电子回单的批次单号
* body商户系统内部的商户转账单号,在商户系统内部唯一。兼容旧字段out_batch_no
* 示例值plfk2020042013
* </pre>
*/
@SerializedName("out_batch_no")
@SerializedName(value = "out_bill_no", alternate = {"out_batch_no"})
private String outBatchNo;
/**
@@ -53,17 +53,18 @@ public class ElectronicBillResult implements Serializable {
/**
* <pre>
* 字段名:电子回单状态
* 变量名signature_status
* 变量名state
* 是否必填:否
* 类型string[1,10]
* 描述:
* 枚举值:
* ACCEPTED:已受理,电子签章已受理成功
* FINISHED:已完成。电子签章已处理完成
* 兼容旧字段signature_status
* 示例值ACCEPTED
* </pre>
*/
@SerializedName("signature_status")
@SerializedName(value = "state", alternate = {"signature_status"})
private String signatureStatus;
/**

View File

@@ -91,8 +91,8 @@ public interface MerchantTransferService {
* 转账电子回单申请受理API
* <p>
* 适用对象:直连商户
* 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_7.shtml
* 请求URLhttps://api.mch.weixin.qq.com/v3/transfer/bill-receipt
* 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012716452
* 请求URLhttps://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/elecsign/out-bill-no
* 请求方式POST
* 接口限频: 单个商户 20QPS如果超过频率限制会报错FREQUENCY_LIMITED请降低频率请求。
*
@@ -106,15 +106,15 @@ public interface MerchantTransferService {
* 查询转账电子回单API
* <p>
* 适用对象:直连商户
* 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_8.shtml
* 请求URLhttps://api.mch.weixin.qq.com/v3/transfer/bill-receipt/{out_batch_no}
* 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012716436
* 请求URLhttps://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/elecsign/out-bill-no/{out_bill_no}
* 请求方式GET
*
* @param outBatchNo the out batch no
* @param outBillNo 商户转账单号
* @return electronic bill result
* @throws WxPayException the wx pay exception
*/
ElectronicBillResult queryElectronicBill(String outBatchNo) throws WxPayException;
ElectronicBillResult queryElectronicBill(String outBillNo) throws WxPayException;
/**
* 转账明细电子回单受理API

View File

@@ -99,11 +99,11 @@ public interface PartnerTransferService {
* 转账电子回单申请受理API
* 接口说明
* 适用对象:直连商户 服务商
* 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/transfer/chapter4_1.shtml
* 请求URLhttps://api.mch.weixin.qq.com/v3/transfer/bill-receipt
* 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012716452
* 请求URLhttps://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/elecsign/out-bill-no
* 请求方式POST
*
* @param request 商家批次单号
* @param request 商户转账单号
* @return 返回数据 fund balance result
* @throws WxPayException the wx pay exception
*/
@@ -114,15 +114,15 @@ public interface PartnerTransferService {
* 查询转账电子回单API
* 接口说明
* 适用对象:直连商户 服务商
* 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/transfer/chapter4_2.shtml
* 请求URLhttps://api.mch.weixin.qq.com/v3/transfer/bill-receipt/{out_batch_no}
* 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012716436
* 请求URLhttps://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/elecsign/out-bill-no/{out_bill_no}
* 请求方式GET
*
* @param outBatchNo 商家批次单号
* @param outBillNo 商户转账单号
* @return 返回数据 fund balance result
* @throws WxPayException the wx pay exception
*/
BillReceiptResult queryBillReceipt(String outBatchNo) throws WxPayException;
BillReceiptResult queryBillReceipt(String outBillNo) throws WxPayException;
/**
* 转账明细电子回单受理API

View File

@@ -92,14 +92,15 @@ public class MerchantTransferServiceImpl implements MerchantTransferService {
@Override
public ElectronicBillResult applyElectronicBill(ElectronicBillApplyRequest request) throws WxPayException {
String url = String.format("%s/v3/transfer/bill-receipt", this.wxPayService.getPayBaseUrl());
String url = String.format("%s/v3/fund-app/mch-transfer/elecsign/out-bill-no", this.wxPayService.getPayBaseUrl());
String response = wxPayService.postV3(url, GSON.toJson(request));
return GSON.fromJson(response, ElectronicBillResult.class);
}
@Override
public ElectronicBillResult queryElectronicBill(String outBatchNo) throws WxPayException {
String url = String.format("%s/v3/transfer/bill-receipt/%s", this.wxPayService.getPayBaseUrl(), outBatchNo);
public ElectronicBillResult queryElectronicBill(String outBillNo) throws WxPayException {
String url = String.format("%s/v3/fund-app/mch-transfer/elecsign/out-bill-no/%s",
this.wxPayService.getPayBaseUrl(), outBillNo);
String response = wxPayService.getV3(url);
return GSON.fromJson(response, ElectronicBillResult.class);
}

View File

@@ -186,17 +186,17 @@ public class PartnerTransferServiceImpl implements PartnerTransferService {
* 转账电子回单申请受理API
* 接口说明
* 适用对象:直连商户 服务商
* 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/transfer/chapter4_1.shtml
* 请求URLhttps://api.mch.weixin.qq.com/v3/transfer/bill-receipt
* 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012716452
* 请求URLhttps://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/elecsign/out-bill-no
* 请求方式POST
*
* @param request 商家批次单号
* @param request 商户转账单号
* @return 返回数据 fund balance result
* @throws WxPayException the wx pay exception
*/
@Override
public BillReceiptResult receiptBill(ReceiptBillRequest request) throws WxPayException {
String url = String.format("%s/v3/transfer/bill-receipt", this.payService.getPayBaseUrl());
String url = String.format("%s/v3/fund-app/mch-transfer/elecsign/out-bill-no", this.payService.getPayBaseUrl());
String response = this.payService.postV3(url, GSON.toJson(request));
return GSON.fromJson(response, BillReceiptResult.class);
}
@@ -206,17 +206,18 @@ public class PartnerTransferServiceImpl implements PartnerTransferService {
* 查询转账电子回单API
* 接口说明
* 适用对象:直连商户 服务商
* 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/transfer/chapter4_2.shtml
* 请求URLhttps://api.mch.weixin.qq.com/v3/transfer/bill-receipt/{out_batch_no}
* 文档详见: https://pay.weixin.qq.com/doc/v3/merchant/4012716436
* 请求URLhttps://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/elecsign/out-bill-no/{out_bill_no}
* 请求方式GET
*
* @param outBatchNo 商家批次单号
* @param outBillNo 商户转账单号
* @return 返回数据 fund balance result
* @throws WxPayException the wx pay exception
*/
@Override
public BillReceiptResult queryBillReceipt(String outBatchNo) throws WxPayException {
String url = String.format("%s/v3/transfer/bill-receipt/%s", this.payService.getPayBaseUrl(), outBatchNo);
public BillReceiptResult queryBillReceipt(String outBillNo) throws WxPayException {
String url = String.format("%s/v3/fund-app/mch-transfer/elecsign/out-bill-no/%s",
this.payService.getPayBaseUrl(), outBillNo);
String response = this.payService.getV3(url);
return GSON.fromJson(response, BillReceiptResult.class);
}

View File

@@ -0,0 +1,135 @@
package com.github.binarywang.wxpay.service.impl;
import com.github.binarywang.wxpay.bean.marketing.transfer.BillReceiptResult;
import com.github.binarywang.wxpay.bean.marketing.transfer.ReceiptBillRequest;
import com.github.binarywang.wxpay.bean.merchanttransfer.ElectronicBillApplyRequest;
import com.github.binarywang.wxpay.bean.merchanttransfer.ElectronicBillResult;
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;
@Test
public class TransferReceiptApiCompatibilityTest {
private static final String BASE_URL = "https://api.mch.weixin.qq.com";
/**
* 验证直连商户电子回单接口已切换到新版fund-app路径。
*/
public void shouldUseNewMerchantTransferElecsignApiPath() throws WxPayException {
RequestCaptureHandler handler = new RequestCaptureHandler();
WxPayService wxPayService = handler.createWxPayService();
MerchantTransferServiceImpl merchantTransferService = new MerchantTransferServiceImpl(wxPayService);
merchantTransferService.applyElectronicBill(new ElectronicBillApplyRequest().setOutBatchNo("plfk2020042013"));
Assert.assertEquals(handler.lastPostUrl, BASE_URL + "/v3/fund-app/mch-transfer/elecsign/out-bill-no");
Assert.assertTrue(handler.lastPostBody.contains("\"out_bill_no\""));
Assert.assertFalse(handler.lastPostBody.contains("\"out_batch_no\""));
merchantTransferService.queryElectronicBill("plfk2020042013");
Assert.assertEquals(handler.lastGetUrl, BASE_URL + "/v3/fund-app/mch-transfer/elecsign/out-bill-no/plfk2020042013");
}
/**
* 验证服务商电子回单接口已切换到新版fund-app路径。
*/
public void shouldUseNewPartnerTransferElecsignApiPath() throws WxPayException {
RequestCaptureHandler handler = new RequestCaptureHandler();
WxPayService wxPayService = handler.createWxPayService();
PartnerTransferServiceImpl partnerTransferService = new PartnerTransferServiceImpl(wxPayService);
ReceiptBillRequest request = new ReceiptBillRequest();
request.setOutBatchNo("plfk2020042013");
partnerTransferService.receiptBill(request);
Assert.assertEquals(handler.lastPostUrl, BASE_URL + "/v3/fund-app/mch-transfer/elecsign/out-bill-no");
Assert.assertTrue(handler.lastPostBody.contains("\"out_bill_no\""));
Assert.assertFalse(handler.lastPostBody.contains("\"out_batch_no\""));
partnerTransferService.queryBillReceipt("plfk2020042013");
Assert.assertEquals(handler.lastGetUrl, BASE_URL + "/v3/fund-app/mch-transfer/elecsign/out-bill-no/plfk2020042013");
}
/**
* 验证新版字段名能够正确反序列化到现有结果对象。
*/
public void shouldDeserializeNewResponseFieldNames() {
Gson gson = new Gson();
BillReceiptResult billReceiptResult =
gson.fromJson("{\"out_bill_no\":\"plfk2020042013\",\"state\":\"FINISHED\"}", BillReceiptResult.class);
Assert.assertEquals(billReceiptResult.getOutBatchNo(), "plfk2020042013");
Assert.assertEquals(billReceiptResult.getSignatureStatus(), "FINISHED");
ElectronicBillResult electronicBillResult =
gson.fromJson("{\"out_bill_no\":\"plfk2020042013\",\"state\":\"FINISHED\"}", ElectronicBillResult.class);
Assert.assertEquals(electronicBillResult.getOutBatchNo(), "plfk2020042013");
Assert.assertEquals(electronicBillResult.getSignatureStatus(), "FINISHED");
}
/**
* 通过动态代理拦截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 ("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;
}
}
}