1
0
mirror of synced 2026-02-05 00:57:50 +08:00

🆕 #3862 【微信支付】 增加V3「微工卡」的批量转账接口实现

This commit is contained in:
Copilot
2026-01-20 13:07:50 +08:00
committed by GitHub
parent 373d9fa5f1
commit e02f6d234c
5 changed files with 542 additions and 0 deletions

View File

@@ -0,0 +1,239 @@
package com.github.binarywang.wxpay.bean.marketing.payroll;
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;
/**
* <pre>
* 微工卡批量转账API请求参数
* 文档地址https://pay.weixin.qq.com/wiki/doc/apiv3_partner/Offline/apis/chapter4_1_8.shtml
*
* 适用对象:服务商
* 请求URLhttps://api.mch.weixin.qq.com/v3/payroll-card/transfer-batches
* 请求方式POST
* </pre>
*
* @author binarywang
* created on 2025/01/19
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayrollTransferBatchesRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名应用ID
* 变量名appid
* 是否必填:二选一
* 类型string[1, 32]
* 描述:
* 服务商在微信申请公众号/小程序或移动应用成功后分配的账号ID
* 示例值wxa1111111
* </pre>
*/
@SerializedName(value = "appid")
private String appid;
/**
* <pre>
* 字段名子商户应用ID
* 变量名sub_appid
* 是否必填:二选一
* 类型string[1, 32]
* 描述:
* 特约商户在微信申请公众号/小程序或移动应用成功后分配的账号ID
* 示例值wxa1111111
* </pre>
*/
@SerializedName(value = "sub_appid")
private String subAppid;
/**
* <pre>
* 字段名:子商户号
* 变量名sub_mchid
* 是否必填:是
* 类型string[1, 32]
* 描述:
* 微信服务商下特约商户的商户号,由微信支付生成并下发
* 示例值1111111
* </pre>
*/
@SerializedName(value = "sub_mchid")
private String subMchid;
/**
* <pre>
* 字段名:商家批次单号
* 变量名out_batch_no
* 是否必填:是
* 类型string[1, 32]
* 描述:
* 商户系统内部的商家批次单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
* 示例值plfk2020042013
* </pre>
*/
@SerializedName(value = "out_batch_no")
private String outBatchNo;
/**
* <pre>
* 字段名:批次名称
* 变量名batch_name
* 是否必填:是
* 类型string[1, 32]
* 描述:
* 该笔批量转账的名称
* 示例值2019年1月深圳分部报销单
* </pre>
*/
@SerializedName(value = "batch_name")
private String batchName;
/**
* <pre>
* 字段名:批次备注
* 变量名batch_remark
* 是否必填:是
* 类型string[1, 32]
* 描述:
* 转账说明UTF8编码最多允许32个字符
* 示例值2019年1月深圳分部报销单
* </pre>
*/
@SerializedName(value = "batch_remark")
private String batchRemark;
/**
* <pre>
* 字段名:转账总金额
* 变量名total_amount
* 是否必填:是
* 类型int64
* 描述:
* 转账金额单位为"分"。转账总金额必须与批次内所有明细转账金额之和保持一致,否则无法发起转账操作
* 示例值4000000
* </pre>
*/
@SerializedName(value = "total_amount")
private Long totalAmount;
/**
* <pre>
* 字段名:转账总笔数
* 变量名total_num
* 是否必填:是
* 类型int
* 描述:
* 一个转账批次单最多发起一千笔转账。转账总笔数必须与批次内所有明细之和保持一致,否则无法发起转账操作
* 示例值200
* </pre>
*/
@SerializedName(value = "total_num")
private Integer totalNum;
/**
* <pre>
* 字段名:转账明细列表
* 变量名transfer_detail_list
* 是否必填:是
* 类型array
* 描述:
* 发起批量转账的明细列表,最多一千笔
* </pre>
*/
@SerializedName(value = "transfer_detail_list")
private List<TransferDetail> transferDetailList;
/**
* 转账明细
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class TransferDetail implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:商家明细单号
* 变量名out_detail_no
* 是否必填:是
* 类型string[1, 32]
* 描述:
* 商户系统内部区分转账批次单下不同转账明细单的唯一标识
* 示例值x23zy545Bd5436
* </pre>
*/
@SerializedName(value = "out_detail_no")
private String outDetailNo;
/**
* <pre>
* 字段名:转账金额
* 变量名transfer_amount
* 是否必填:是
* 类型int64
* 描述:
* 转账金额单位为"分"
* 示例值200000
* </pre>
*/
@SerializedName(value = "transfer_amount")
private Long transferAmount;
/**
* <pre>
* 字段名:转账备注
* 变量名transfer_remark
* 是否必填:是
* 类型string[1, 32]
* 描述:
* 单条转账备注微信用户会收到该备注UTF8编码最多允许32个字符
* 示例值2020年4月报销
* </pre>
*/
@SerializedName(value = "transfer_remark")
private String transferRemark;
/**
* <pre>
* 字段名:用户标识
* 变量名openid
* 是否必填:是
* 类型string[1, 64]
* 描述:
* 用户在商户对应appid下的唯一标识
* 示例值o-MYE42l80oelYMDE34nYD456Xoy
* </pre>
*/
@SerializedName(value = "openid")
private String openid;
/**
* <pre>
* 字段名:收款用户姓名
* 变量名user_name
* 是否必填:否
* 类型string[1, 1024]
* 描述:
* 收款用户真实姓名。该字段需进行加密处理,加密方法详见敏感信息加密说明
* 示例值757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45
* </pre>
*/
@SpecEncrypt
@SerializedName(value = "user_name")
private String userName;
}
}

View File

@@ -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;
/**
* <pre>
* 微工卡批量转账API返回结果
* 文档地址https://pay.weixin.qq.com/wiki/doc/apiv3_partner/Offline/apis/chapter4_1_8.shtml
*
* 适用对象:服务商
* 请求URLhttps://api.mch.weixin.qq.com/v3/payroll-card/transfer-batches
* 请求方式POST
* </pre>
*
* @author binarywang
* created on 2025/01/19
*/
@Data
@NoArgsConstructor
public class PayrollTransferBatchesResult implements Serializable {
private static final long serialVersionUID = 1L;
/**
* <pre>
* 字段名:商家批次单号
* 变量名out_batch_no
* 是否必填:是
* 类型string[1, 32]
* 描述:
* 商户系统内部的商家批次单号
* 示例值plfk2020042013
* </pre>
*/
@SerializedName(value = "out_batch_no")
private String outBatchNo;
/**
* <pre>
* 字段名:微信批次单号
* 变量名batch_id
* 是否必填:是
* 类型string[1, 64]
* 描述:
* 微信批次单号,微信商家转账系统返回的唯一标识
* 示例值1030000071100999991182020050700019480001
* </pre>
*/
@SerializedName(value = "batch_id")
private String batchId;
/**
* <pre>
* 字段名:批次状态
* 变量名batch_status
* 是否必填:是
* 类型string[1, 32]
* 描述:
* ACCEPTED:已受理批次已受理成功若发起批量转账的30分钟后转账批次单仍处于该状态可能原因是商户账户余额不足等。商户可查询账户资金流水若该笔转账批次单的扣款已经发生则表示批次已经进入转账中请再次查单确认
* PROCESSING:转账中,已开始处理批次内的转账明细单
* FINISHED:已完成,批次内的所有转账明细单都已处理完成
* CLOSED:已关闭,可查询具体的批次关闭原因确认
* 示例值ACCEPTED
* </pre>
*/
@SerializedName(value = "batch_status")
private String batchStatus;
/**
* <pre>
* 字段名:批次类型
* 变量名batch_type
* 是否必填:是
* 类型string[1, 32]
* 描述:
* 批次类型
* API:API方式发起
* WEB:WEB方式发起
* 示例值API
* </pre>
*/
@SerializedName(value = "batch_type")
private String batchType;
/**
* <pre>
* 字段名:批次名称
* 变量名batch_name
* 是否必填:是
* 类型string[1, 32]
* 描述:
* 该笔批量转账的名称
* 示例值2019年1月深圳分部报销单
* </pre>
*/
@SerializedName(value = "batch_name")
private String batchName;
/**
* <pre>
* 字段名:批次备注
* 变量名batch_remark
* 是否必填:是
* 类型string[1, 32]
* 描述:
* 转账说明UTF8编码最多允许32个字符
* 示例值2019年1月深圳分部报销单
* </pre>
*/
@SerializedName(value = "batch_remark")
private String batchRemark;
/**
* <pre>
* 字段名:批次关闭原因
* 变量名close_reason
* 是否必填:否
* 类型string[1, 32]
* 描述:
* 如果批次单状态为"CLOSED"(已关闭),则有关闭原因
* 示例值OVERDUE_CLOSE
* </pre>
*/
@SerializedName(value = "close_reason")
private String closeReason;
/**
* <pre>
* 字段名:转账总金额
* 变量名total_amount
* 是否必填:是
* 类型int64
* 描述:
* 转账金额单位为"分"
* 示例值4000000
* </pre>
*/
@SerializedName(value = "total_amount")
private Long totalAmount;
/**
* <pre>
* 字段名:转账总笔数
* 变量名total_num
* 是否必填:是
* 类型int
* 描述:
* 一个转账批次单最多发起一千笔转账
* 示例值200
* </pre>
*/
@SerializedName(value = "total_num")
private Integer totalNum;
/**
* <pre>
* 字段名:批次创建时间
* 变量名create_time
* 是否必填:是
* 类型string[1, 32]
* 描述:
* 批次受理成功时返回遵循rfc3339标准格式格式为YYYY-MM-DDTHH:mm:ss:sss+TIMEZONE
* 示例值2015-05-20T13:29:35.120+08:00
* </pre>
*/
@SerializedName(value = "create_time")
private String createTime;
/**
* <pre>
* 字段名:批次更新时间
* 变量名update_time
* 是否必填:是
* 类型string[1, 32]
* 描述:
* 批次最近一次状态变更的时间遵循rfc3339标准格式格式为YYYY-MM-DDTHH:mm:ss:sss+TIMEZONE
* 示例值2015-05-20T13:29:35.120+08:00
* </pre>
*/
@SerializedName(value = "update_time")
private String updateTime;
/**
* <pre>
* 字段名:转账成功金额
* 变量名success_amount
* 是否必填:否
* 类型int64
* 描述:
* 转账成功的金额,单位为"分"
* 示例值3900000
* </pre>
*/
@SerializedName(value = "success_amount")
private Long successAmount;
/**
* <pre>
* 字段名:转账成功笔数
* 变量名success_num
* 是否必填:否
* 类型int
* 描述:
* 转账成功的笔数
* 示例值199
* </pre>
*/
@SerializedName(value = "success_num")
private Integer successNum;
/**
* <pre>
* 字段名:转账失败金额
* 变量名fail_amount
* 是否必填:否
* 类型int64
* 描述:
* 转账失败的金额,单位为"分"
* 示例值100000
* </pre>
*/
@SerializedName(value = "fail_amount")
private Long failAmount;
/**
* <pre>
* 字段名:转账失败笔数
* 变量名fail_num
* 是否必填:否
* 类型int
* 描述:
* 转账失败的笔数
* 示例值1
* </pre>
*/
@SerializedName(value = "fail_num")
private Integer failNum;
}

View File

@@ -101,4 +101,16 @@ public interface PayrollService {
*/
WxPayApplyBillV3Result merchantFundWithdrawBillType(String billType, String billDate, String tarType) throws WxPayException;
/**
* 微工卡批量转账API
* 适用对象:服务商
* 请求URLhttps://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;
}

View File

@@ -193,4 +193,27 @@ public class PayrollServiceImpl implements PayrollService {
return GSON.fromJson(response, WxPayApplyBillV3Result.class);
}
/**
* 微工卡批量转账API
* 适用对象:服务商
* 请求URLhttps://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);
}
}

View File

@@ -14,6 +14,8 @@ import lombok.extern.slf4j.Slf4j;
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
import java.util.Collections;
/**
* 微工卡(服务商)
*
@@ -125,4 +127,29 @@ public class PayrollServiceImplTest {
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)
.transferDetailList(Collections.singletonList(
PayrollTransferBatchesRequest.TransferDetail.builder()
.outDetailNo("x23zy545Bd5436" + System.currentTimeMillis())
.transferAmount(200000L)
.transferRemark("2020年4月报销")
.openid("o-MYE42l80oelYMDE34nYD456Xoy")
.userName("张三")
.build()
))
.build();
PayrollTransferBatchesResult result = wxPayService.getPayrollService().payrollCardTransferBatches(request);
log.info(result.toString());
}
}