微信支付之商家转账功能(用于商家转账给微信用户),java可用详细版本,腾讯官方文档: https://pay.weixin.qq.com/doc/v3/merchant/4012711988
历史名称:企业付款到零钱 >> 商家转账到零钱 >> 商家转账,2025年1月15日后升级,调用方式也不一样了。本文介绍最新的调用方式(java代码),小白版本,大神勿喷…
一、准备工作,首先要把开发需要的材料都准备好,登录后台,找到API安全,把以下4项申请下来
-
商户API证书: 点击右上角<申请新证书>,按照腾讯指引下载新证书,证书下来是一个压缩包,解压出来有4个文件,需要用到的是apiclient_key.pem文件,4个可以放在代码static包里,或者放在服务器的文件夹里,只要java代码能访问到就可以。
-
商户APIv2秘钥: 自行设置32位秘钥,要记得,腾讯后台不会明文展示,如果忘记了只能重置。
-
APIv3秘钥: 自行设置32位秘钥,要记得,腾讯后台不会明文展示,如果忘记了只能重置。
-
平台证书: 官方文档: https://pay.weixin.qq.com/doc/v3/merchant/4012068814这个比较麻烦一点,需要用到APIv3秘钥、商户ID、商户平台证书路径(步骤1下载下来的apiclient_key.pem文件所在的路径),申请方式:
首先下载一个jar包,腾讯已经有放在github上了,如访问不了github,可以使用这个资源下载:https://download.youkuaiyun.com/download/weixin_42436080/90541583 下载后jar包放在本地文件夹,用cmd执行以下命令行:java -jar CertificateDownloader.jar -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
${apiV3key}: apiV3秘钥, 请使用自己的秘钥,如: OUiH6Z6kGxBHPclssO562OXkZnVYOWBX
${mchId}: 商户号,如16254032
${mchPrivateKeyFilePath}: 证书所在路径(步骤1下载的商户证书路径), 如: C:\apiclient_key.pem
${outputFilePath}: 申请后, 输出的证书路径, 如: F:\output
踩过的坑:申请的时候一直提示!Tag Matched,验签不通过,后来在后台重置了apiV3秘钥才行,可能是网络有延迟,如果确定命令、秘钥、路径都没问题,就在后台重置一下apiV3就行。
如果申请成功的话,F:\output文件夹会输出一个wechatpay_XXXXXXXXXXXXXXXXXXXXXXXXXXX.pem文件
二、材料都准备完成后,已经成功99%了,现在只需要复制拷贝就行。
- 新版的商家转账与之前的区别就是,原先是批量或者单个转账,直接到达用户微信零钱,新版需要调用接口生成账单,公众号、小程序或者H5再调用返回消息再进行用户收款。以下代码以小程序为例。
appId: 小程序的appID
secret: 小程序的秘钥
mchId: 商户号
apiV3Key: apiV3秘钥
mchSerialNo: 商户证书序列号,如下图
certUrl1: 商户证书地址,解压出来的文件,apiclient_key.pem所在的路径
certUrl2: 平台证书地址, 上一步骤申请的wechatpay_XXXXXXXXXXXXXXXXXXXXXXXXXXX.pem文件所在的路径
notifyUrl2: 回调地地址, 用户收款后的回调信息
2. 上代码
package com.ruoyi.common.wx;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* wxpay pay properties.
*
* @author Binary Wang
*/
@Data
@ConfigurationProperties(prefix = "wx.pay")
public class WxPayProperties {
// 小程序appId
private String appId;
//小程序秘钥
private String secret;
// 商户ID
private String mchId;
// 商户秘钥 apiV2
private String mchKey;
// apiV3秘钥
private String apiV3Key;
// 证书序列号
private String mchSerialNo;
// 证书地址: 商户证书
private String certUrl1;
//证书地址: 平台证书
private String certUrl2;
// 回调地址: native收款
private String notifyUrl1;
//回调地址: 商家转账
private String notifyUrl2;
}
Controller层代码
package com.ruoyi.web.controller.system;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.wx.WxPayProperties;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import io.swagger.annotations.Api;
import lombok.RequiredArgsConstructor;
import okhttp3.OkHttpClient;
import org.apache.commons.lang3.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.crypto.Cipher;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
import java.util.Random;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.core.http.DefaultHttpClientBuilder;
import com.wechat.pay.java.core.http.HttpClient;
import com.wechat.pay.java.core.http.HttpHeaders;
import com.wechat.pay.java.core.http.HttpMethod;
import com.wechat.pay.java.core.http.HttpRequest;
import com.wechat.pay.java.core.http.HttpResponse;
import com.wechat.pay.java.core.http.JsonRequestBody;
import com.wechat.pay.java.core.http.MediaType;
@Api(tags = "微信支付相关接口")
@RestController
@Validated
@RequiredArgsConstructor
@RequestMapping("/pay")
public class WechatPayController {
@Resource
private WxPayProperties wxPayProperties;
// 2025.01之后 商家转账功能
@GetMapping("/transfer/bat")
@ResponseBody
public String test1() {
long currentTimestamp = Instant.now().getEpochSecond();
Random random = new Random();
long randomIntBound = random.nextInt(10000);
String formattedNumber = String.format("%0" + 5 + "d", randomIntBound); // 不足5位, 补0
String orderNo = currentTimestamp + "8" + formattedNumber; // 订单编号
createTransferBills(orderNo, "oqX1E7F6aH8TiFlIcAnmXagOzBf1", "", 50L);
return "aa" ;
}
// step1 生成账单
public void createTransferBills(String orderNo, String openId, String userName, Long money) {
OkHttpClient okHttpClient = new OkHttpClient();
HttpClient httpClient = new DefaultHttpClientBuilder()
.config(rsaAutoCertificateConfig())
.okHttpClient(okHttpClient)
.build();
HttpHeaders headers = new HttpHeaders();
headers.addHeader("Accept", MediaType.APPLICATION_JSON.getValue());
headers.addHeader("Content-Type", MediaType.APPLICATION_JSON.getValue());
headers.addHeader("Wechatpay-Serial", wxPayProperties.getMchSerialNo()); // 商户证书序列号
HashMap<Object, Object> map = new HashMap<>();
map.put("appid", wxPayProperties.getAppId()); //小程序 appId
map.put("out_bill_no", orderNo); // 订单编号
map.put("transfer_scene_id", "1000"); // 转账场景ID
map.put("openid", openId); // openid
// 收款用户姓名(需要加密传入)
if (StringUtils.isNotBlank(userName)) {
map.put("user_name", rsaEncryptOAEP(userName));
}
map.put("transfer_amount", money); //金额(分)
map.put("transfer_remark", "货款结算: " + orderNo); // 转账备注
map.put("notify_url", wxPayProperties.getNotifyUrl2()); //回调地址: 商家转账回调地址
map.put("user_recv_perception", "现金奖励"); // 用户收款感知
// 转账场景报备信息
JSONArray jsonArray = new JSONArray();
jsonArray.add(new JSONObject().fluentPut("info_type", "活动名称").fluentPut("info_content", "货款回收"));
jsonArray.add(new JSONObject().fluentPut("info_type", "奖励说明").fluentPut("info_content", "货款回收结算"));
map.put("transfer_scene_report_infos", jsonArray);
JsonRequestBody build = new JsonRequestBody.Builder()
.body(JSONUtil.toJsonStr(map))
.build();
HttpRequest executeSendGetHttpRequest = new HttpRequest.Builder()
.httpMethod(HttpMethod.POST)
.url("https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills")
.headers(headers)
.body(build)
.build();
try {
HttpResponse<JSONObject> execute = httpClient.execute(executeSendGetHttpRequest, JSONObject.class);
JSONObject responseBody = execute.getServiceResponse();
// System.out.println(responseBody.toJSONString());
System.out.println(responseBody.getString("package_info")); // 需要用到的字段
} catch (ServiceException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
// API安全加密配置
private RSAAutoCertificateConfig rsaAutoCertificateConfig() {
return new RSAAutoCertificateConfig.Builder()
.merchantId(wxPayProperties.getMchId()) // 商户号
.privateKeyFromPath(wxPayProperties.getCertUrl1()) // 商户API证书私钥的存放路径
.merchantSerialNumber(wxPayProperties.getMchSerialNo()) // 商户API证书序列号
.apiV3Key(wxPayProperties.getApiV3Key()) // APIv3密钥
.build();
}
// 敏感信息加密
private String rsaEncryptOAEP(String message) {
X509Certificate cert = getX509Certificate();
try {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, cert.getPublicKey());
byte[] data = message.getBytes(StandardCharsets.UTF_8);
byte[] cipherdata = cipher.doFinal(data);
return Base64.getEncoder().encodeToString(cipherdata);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 获取 X509Certificate
private X509Certificate getX509Certificate() {
ClassLoader classLoader = this.getClass().getClassLoader();
try (InputStream in = classLoader.getResourceAsStream(wxPayProperties.getCertUrl2())) {
if (in == null) {
throw new IOException("Resource not found: " + wxPayProperties.getCertUrl2());
}
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(in);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
调用成功会返回json包,需要用到的是package_info里面的数据,并且状态应该为"state":“WAIT_USER_CONFIRM”,等待用户收款
小程序代码,可以放在提现按钮里,用于用户收款,把java返回的package_info复制到小程序的package里面来
if (!wx.canIUse('requestMerchantTransfer')) {
wx.showModal({
content: '你的微信版本过低,请更新至最新版本。',
showCancel: false,
});
return
}
wx.requestMerchantTransfer({
mchId: '10000000001', // 商户号
appId: 'waaaaaxxxxxxxxxxxe', // appId
package: "ABxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPMuG9v4Ssg=", // 商家转账付款单跳转收款页package信息
success: (res) => {
// res.err_msg将在页面展示成功后返回应用时返回ok,并不代表付款成功
console.log('success:', res);
},
fail: (res) => {
console.log('fail:', res);
},
})
用户点击提现后,会弹出以下提示框,确定收款即可完成转账.
这样就完成了一次商家转账,如有多次,就多次调用,并替换package数据即可。