第一次接触微信支付V3的批量转账功能时,我完全被那些专业术语搞懵了。什么直连商户、证书签名、批次单号,听着就头大。但实际用起来发现,这套API设计得还挺人性化,特别适合电商平台处理批量退款、佣金发放这类场景。
简单来说,这个接口允许你一次性给多个用户的微信零钱转账。比如你的平台有100个卖家需要结算佣金,传统做法是手动一个个操作,现在用这个API,只需要构造一个请求就能全部搞定。我去年给一家跨境电商做系统升级时,就用这个功能把原本需要财务人员加班到凌晨的结算工作,变成了后台自动执行的5分钟任务。
要使用这个功能,你需要先准备好几样东西:
这里有个新手容易踩的坑:证书文件通常有两个,一个是商户证书,一个是平台证书。批量转账只需要用到商户证书,别搞混了。我第一次集成时就因为用错证书,调试了半天才发现问题。
我习惯用Maven管理Java项目,首先要在pom.xml里添加必要的依赖:
xml复制<dependencies>
<!-- Hutool工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.2</version>
</dependency>
<!-- Apache HttpClient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<!-- Gson用于JSON处理 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.9</version>
</dependency>
</dependencies>
Hutool是个国产的Java工具库,里面的IdUtil可以方便地生成批次单号,比手动写随机字符串生成器省事多了。HttpClient则是发送HTTP请求的标准选择,微信支付API要求使用POST方法提交JSON数据。
证书管理是集成过程中最麻烦的部分。你需要:
下载的证书文件通常是.p12格式,但微信支付V3接口要求使用PEM格式的私钥。你需要用OpenSSL工具转换:
bash复制openssl pkcs12 -in apiclient_cert.p12 -nodes -out private_key.pem
转换完成后,把private_key.pem放在项目的resources目录下。注意千万别把这个文件提交到公开的代码仓库!我在第一次项目上线时就犯了这个错误,不得不重新生成证书。
批量转账的核心是构造正确的JSON请求体。下面是我封装的一个工具方法:
java复制public static String buildBatchTransferRequest(String appId, String mchId,
String batchName, String batchRemark, List<TransferDetail> details) {
Map<String, Object> requestMap = new HashMap<>();
// 生成批次号
String outBatchNo = "TF" + System.currentTimeMillis();
requestMap.put("appid", appId);
requestMap.put("out_batch_no", outBatchNo);
requestMap.put("batch_name", batchName);
requestMap.put("batch_remark", batchRemark);
// 计算总金额和笔数
int totalAmount = details.stream().mapToInt(TransferDetail::getAmount).sum();
requestMap.put("total_amount", totalAmount);
requestMap.put("total_num", details.size());
// 构造明细列表
List<Map<String, Object>> detailList = new ArrayList<>();
for (TransferDetail detail : details) {
Map<String, Object> detailMap = new HashMap<>();
detailMap.put("out_detail_no", "DT" + System.currentTimeMillis() + detail.getUserId());
detailMap.put("transfer_amount", detail.getAmount());
detailMap.put("transfer_remark", detail.getRemark());
detailMap.put("openid", detail.getOpenId());
detailList.add(detailMap);
}
requestMap.put("transfer_detail_list", detailList);
return new Gson().toJson(requestMap);
}
这个方法接收转账明细列表,自动计算总金额和笔数。其中TransferDetail是我定义的DTO类,包含每个收款人的openid、转账金额和备注。
微信支付V3使用基于RSA的签名认证,这是整个流程中最关键的安全环节。签名错误会导致请求直接被拒绝。我整理了一个签名工具类:
java复制public class WeChatPaySigner {
private static final String SIGN_ALGORITHM = "SHA256withRSA";
public static String generateSignature(String method, String url,
String body, String mchId, String serialNo, String privateKeyPath) throws Exception {
// 生成随机字符串和时间戳
String nonce = RandomStringUtils.randomAlphanumeric(32);
long timestamp = System.currentTimeMillis() / 1000;
// 构造签名串
String message = buildSignMessage(method, url, timestamp, nonce, body);
// 加载私钥
PrivateKey privateKey = loadPrivateKey(privateKeyPath);
// 生成签名
Signature signature = Signature.getInstance(SIGN_ALGORITHM);
signature.initSign(privateKey);
signature.update(message.getBytes(StandardCharsets.UTF_8));
String sign = Base64.encodeBase64String(signature.sign());
// 构造Authorization头
return String.format(
"mchid=\"%s\",timestamp=\"%d\",nonce_str=\"%s\",serial_no=\"%s\",signature=\"%s\"",
mchId, timestamp, nonce, serialNo, sign
);
}
private static String buildSignMessage(String method, String url,
long timestamp, String nonce, String body) {
return method + "\n" + url + "\n" + timestamp + "\n" + nonce + "\n" + (body == null ? "" : body) + "\n";
}
private static PrivateKey loadPrivateKey(String path) throws Exception {
String keyContent = new String(Files.readAllBytes(Paths.get(path)))
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
byte[] decoded = Base64.decodeBase64(keyContent);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded);
return KeyFactory.getInstance("RSA").generatePrivate(keySpec);
}
}
使用时需要注意,签名串的每个部分都必须严格按照顺序拼接,连换行符都不能少。我遇到过因为少了个换行符导致签名失败的情况,调试了很久才发现问题。
有了签名之后,就可以构造完整的HTTP请求了。我推荐使用Apache HttpClient:
java复制public class WeChatPayClient {
private static final String API_URL = "https://api.mch.weixin.qq.com/v3/transfer/batches";
public static String executeTransfer(String requestBody, String mchId,
String serialNo, String privateKeyPath) throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(API_URL);
// 设置请求头
httpPost.setHeader("Content-Type", "application/json");
httpPost.setHeader("Accept", "application/json");
httpPost.setHeader("Wechatpay-Serial", serialNo);
// 添加签名
String auth = "WECHATPAY2-SHA256-RSA2048 " +
WeChatPaySigner.generateSignature("POST", "/v3/transfer/batches",
requestBody, mchId, serialNo, privateKeyPath);
httpPost.setHeader("Authorization", auth);
// 设置请求体
httpPost.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
// 执行请求
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, StandardCharsets.UTF_8);
}
}
}
这个方法封装了请求发送的全过程,包括设置必要的请求头和签名信息。注意微信支付API要求Content-Type和Accept都设置为application/json。
成功的响应会返回JSON格式的数据:
json复制{
"out_batch_no": "TF123456789",
"batch_id": "1030000071100999991182020050700019480001",
"create_time": "2023-05-20T13:29:35+08:00"
}
我建议定义对应的Java类来解析:
java复制public class TransferBatchResult {
private String outBatchNo;
private String batchId;
private String createTime;
private String status;
// getters and setters
}
对于错误响应,微信支付会返回类似这样的JSON:
json复制{
"code": "INVALID_REQUEST",
"message": "商户账户余额不足",
"detail": {
"field": "/amount",
"value": "10000",
"issue": "余额不足"
}
}
处理错误时,除了检查code和message,还应该关注detail字段,它通常会告诉你具体哪个参数出了问题。我在处理大额转账时就遇到过余额不足的情况,后来发现是因为微信支付对单笔转账金额有限制。
微信支付对批量转账API有严格的频率限制:单个商户50QPS(每秒查询率)。如果超过这个限制,会收到FREQUENCY_LIMITED错误。
对于需要处理大量转账的场景,我建议:
这里有个简单的速率限制器实现:
java复制public class RateLimiter {
private final int maxPermits;
private final long interval;
private long lastTick;
private int storedPermits;
public RateLimiter(int maxPermits, long intervalMillis) {
this.maxPermits = maxPermits;
this.interval = intervalMillis * 1_000_000; // 转换为纳秒
this.lastTick = System.nanoTime();
this.storedPermits = maxPermits;
}
public synchronized void acquire() {
long now = System.nanoTime();
long elapsed = now - lastTick;
// 计算新增的许可数
int newPermits = (int) (elapsed / interval);
if (newPermits > 0) {
storedPermits = Math.min(storedPermits + newPermits, maxPermits);
lastTick = now;
}
while (storedPermits <= 0) {
try {
long waitTime = interval - elapsed;
TimeUnit.NANOSECONDS.sleep(waitTime);
now = System.nanoTime();
elapsed = now - lastTick;
newPermits = (int) (elapsed / interval);
storedPermits += newPermits;
lastTick = now;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
storedPermits--;
}
}
使用时,在发送请求前调用rateLimiter.acquire()即可。
批量转账最怕遇到中途失败的情况。我建议实现以下机制确保数据一致性:
这里有个简单的状态检查实现:
java复制public class TransferBatchService {
private static final long TIMEOUT = 30 * 60 * 1000; // 30分钟超时
@Scheduled(fixedDelay = 5 * 60 * 1000) // 每5分钟检查一次
public void checkTimeoutBatches() {
List<TransferBatch> pendingBatches = batchRepository
.findByStatusAndCreateTimeBefore(
BatchStatus.PROCESSING,
new Date(System.currentTimeMillis() - TIMEOUT));
for (TransferBatch batch : pendingBatches) {
try {
String result = queryBatchStatus(batch.getBatchId());
// 解析结果并更新状态
updateBatchStatus(batch, result);
} catch (Exception e) {
log.error("检查批次状态失败: {}", batch.getBatchId(), e);
}
}
}
private String queryBatchStatus(String batchId) {
// 调用微信支付查询接口
// ...
}
}
对于电商平台来说,还需要考虑与订单系统的对账。我通常会在转账成功后发送事件通知,触发订单状态的更新。