前言
公司業(yè)務(wù)原因,之前申請(qǐng)的微信支付功能-企業(yè)付款到零錢功能被停用,與客服咨詢后得知該功能不再繼續(xù)維護(hù),需要使用-企業(yè)轉(zhuǎn)賬到零錢功能。
企業(yè)轉(zhuǎn)賬到零錢是微信支付V3版本的接口,直接沒(méi)有對(duì)接過(guò),遇到了挺多問(wèn)題,在此記錄一下。
流程代碼
提現(xiàn)方法:
public WithdrawalResponse withdrawal(String bankCard, String bankUserName, BigDecimal amount, String uuid, String orderNo) {
try {
int intValue = amount.multiply(new BigDecimal("100")).intValue();
log.info("amount:{}",amount);
WechatTransferBatchesParam param = new WechatTransferBatchesParam();
log.info("轉(zhuǎn)賬Id:{}", orderNo);
param.setAppid(wxPayApiManage.getAppId());
param.setOut_batch_no(orderNo);
param.setBatch_name("轉(zhuǎn)賬");
param.setBatch_remark("轉(zhuǎn)賬");
param.setTotal_amount(intValue);
param.setTotal_num(1);
// 批量轉(zhuǎn)賬,可以同時(shí)轉(zhuǎn)賬給多ren
List<WechatTransferBatchesParam.transferDetail> detailList = new ArrayList<>();
WechatTransferBatchesParam.transferDetail detail = new WechatTransferBatchesParam.transferDetail();
detail.setOut_detail_no(orderNo);
detail.setTransfer_amount(intValue);
detail.setTransfer_remark("轉(zhuǎn)賬詳情");
detail.setOpenid(uuid);
detailList.add(detail);
param.setTransfer_detail_list(detailList);
String jsonObject = WxPayTransferBatchesUtils.transferBatches(wxPayApiManage, param);
JSONObject object = JSONObject.parseObject(jsonObject);
log.info("提現(xiàn)結(jié)果:" + jsonObject);
String returnCode = (String) object.get("return_code");
String resultCode = (String) object.get("result_code");
String outBatchNo = (String) object.get("out_batch_no");
if (StrUtil.isNotEmpty(outBatchNo)) {
// 提現(xiàn)成功
return new WithdrawalResponse((String) object.get("out_batch_no"), DateUtils.parseDate((String) object.get("create_time"), "yyyy-MM-dd HH:mm:ss"));
} else {
log.error("微信提現(xiàn)錯(cuò)誤,", returnCode, resultCode);
}
} catch (Exception e) {
log.error("微信提現(xiàn)錯(cuò)誤,", e);
}
return null;
}
WxPayTransferBatchesUtils工具類
請(qǐng)求轉(zhuǎn)賬接口之前需要通過(guò)認(rèn)證接口拿到serial_no參數(shù),所以可以看到該類中有兩次http請(qǐng)求
package com.aiminerva.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.aiminerva.model.CertificatesBody;
import com.aiminerva.model.WechatTransferBatchesParam;
import com.alibaba.fastjson.JSONObject;
import com.familyhealth.util.result.ResultCode;
import com.familyhealth.vo.exception.CommonException;
import com.google.gson.Gson;
import com.ijpay.wxpay.WxPayApiConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
@Slf4j
public class WxPayTransferBatchesUtils {
/**
* 發(fā)起商家轉(zhuǎn)賬,支持批量轉(zhuǎn)賬
*
* @param wxPayConfig 微信配置信息
* @param param 轉(zhuǎn)賬請(qǐng)求參數(shù)
* @return 微信支付二維碼地址
*/
public static String transferBatches(WxPayApiConfig wxPayConfig, WechatTransferBatchesParam param) {
CloseableHttpClient httpclient = HttpClients.createDefault();
CloseableHttpResponse response = null;
HttpEntity entity = null;
try {
String v3Certificates = VechatPayV3Util.getTokenByV3Certificates("GET",
"/v3/certificates", "", wxPayConfig.getMchId(),
"填入商家證書(shū)號(hào)", wxPayConfig.getKeyPemPath());
HttpRequest post = new HttpRequest("https://api.mch.weixin.qq.com/v3/certificates");
// NOTE: 建議指定charset=utf-8。低于4.4.6版本的HttpCore,不能正確的設(shè)置字符集,可能導(dǎo)致簽名錯(cuò)誤
post.header("Content-Type", "application/json");
post.header("Accept", "application/json");
post.header("Authorization",
"WECHATPAY2-SHA256-RSA2048" + " "
+ v3Certificates);
HttpResponse execute = post.execute();
String body = execute.body();
JSONObject jsonObject = JSONObject.parseObject(body);
Object data = jsonObject.get("data");
List<CertificatesBody> certificatesBodies = JSONObject.parseArray(data.toString(), CertificatesBody.class);
if (CollUtil.isEmpty(certificatesBodies)) {
log.error(certificatesBodies.toString());
return null;
}
//商戶私鑰證書(shū)
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/transfer/batches");
// NOTE: 建議指定charset=utf-8。低于4.4.6版本的HttpCore,不能正確的設(shè)置字符集,可能導(dǎo)致簽名錯(cuò)誤
httpPost.addHeader("Content-Type", "application/json");
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Wechatpay-Serial", certificatesBodies.get(0).getSerial_no());
//-------------------------核心認(rèn)證 start-----------------------------------------------------------------
String strToken = VechatPayV3Util.getToken("POST",
"/v3/transfer/batches",
JSONObject.toJSONString(param), wxPayConfig.getMchId(), "填入商家證書(shū)號(hào)", wxPayConfig.getKeyPemPath());
log.error("微信轉(zhuǎn)賬token " + strToken);
// 添加認(rèn)證信息
httpPost.addHeader("Authorization",
"WECHATPAY2-SHA256-RSA2048" + " "
+ strToken);
//---------------------------核心認(rèn)證 end---------------------------------------------------------------
StringEntity stringEntity = new StringEntity(JSONObject.toJSONString(param), "UTF-8");
stringEntity.setContentType("application/json");
httpPost.setEntity(stringEntity);
//發(fā)起轉(zhuǎn)賬請(qǐng)求
response = httpclient.execute(httpPost);
HashMap<String, String> resultMap = resolverResponse(response);
log.info("resultMap:{}",resultMap);
entity = response.getEntity();
log.info("-----getHeaders.Request-ID:" + response.getHeaders("Request-ID"));
return EntityUtils.toString(entity);
} catch (Exception e) {
e.printStackTrace();
throw new CommonException(ResultCode.ERROY, "商家轉(zhuǎn)賬請(qǐng)求失敗");
}
}
/**
* 解析響應(yīng)數(shù)據(jù)
*
* @param response 發(fā)送請(qǐng)求成功后,返回的數(shù)據(jù)
* @return 微信返回的參數(shù)
*/
private static HashMap<String, String> resolverResponse(CloseableHttpResponse response) {
try {
// 1.獲取請(qǐng)求碼
int statusCode = response.getStatusLine().getStatusCode();
// 2.獲取返回值 String 格式
final String bodyAsString = EntityUtils.toString(response.getEntity());
Gson gson = new Gson();
if (statusCode == 200) {
// 3.如果請(qǐng)求成功則解析成Map對(duì)象返回
HashMap<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
return resultMap;
} else {
if (StringUtils.isNoneBlank(bodyAsString)) {
log.error("商戶轉(zhuǎn)賬請(qǐng)求失敗,提示信息:{}", bodyAsString);
// 4.請(qǐng)求碼顯示失敗,則嘗試獲取提示信息
HashMap<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
throw new CommonException(ResultCode.ERROY, resultMap.get("message"));
}
log.error("商戶轉(zhuǎn)賬請(qǐng)求失敗,未查詢到原因,提示信息:{}", response);
// 其他異常,微信也沒(méi)有返回?cái)?shù)據(jù),這就需要具體排查了
throw new IOException("request failed");
}
} catch (Exception e) {
e.printStackTrace();
throw new CommonException(ResultCode.ERROY, e.getMessage());
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
VechatPayV3Util工具類
package com.aiminerva.util;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Random;
import cn.hutool.core.date.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.springframework.util.StringUtils;
@Slf4j
public class VechatPayV3Util {
/**
*
* @param method 請(qǐng)求方法 post
* @param canonicalUrl 請(qǐng)求地址
* @param body 請(qǐng)求參數(shù)
* @param merchantId 這里用的商戶號(hào)
* @param certSerialNo 商戶證書(shū)序列號(hào)
* @param keyPath 商戶證書(shū)地址
* @return
* @throws Exception
*/
public static String getToken(
String method,
String canonicalUrl,
String body,
String merchantId,
String certSerialNo,
String keyPath) throws Exception {
String signStr = "";
//獲取32位隨機(jī)字符串
String nonceStr = getRandomString(32);
//當(dāng)前系統(tǒng)運(yùn)行時(shí)間
long timestamp = System.currentTimeMillis() / 1000;
if (StringUtils.isEmpty(body)) {
body = "";
}
//簽名操作
String message = buildMessage(method, canonicalUrl, timestamp, nonceStr, body);
//簽名操作
String signature = sign(message.getBytes("utf-8"), keyPath);
//組裝參數(shù)
return "mchid=\"" + merchantId + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + certSerialNo + "\","
+ "signature=\"" + signature + "\"";
}
/**
*
* @param method 請(qǐng)求方法 post
* @param canonicalUrl 請(qǐng)求地址
* @param body 請(qǐng)求參數(shù)
* @param merchantId 這里用的商戶號(hào)
* @param certSerialNo 商戶證書(shū)序列號(hào)
* @param keyPath 商戶證書(shū)地址
* @return
* @throws Exception
*/
public static String getTokenByV3Certificates(
String method,
String canonicalUrl,
String body,
String merchantId,
String certSerialNo,
String keyPath) throws Exception {
String signStr = "";
//獲取32位隨機(jī)字符串
String nonceStr = getRandomString(32);
//當(dāng)前系統(tǒng)運(yùn)行時(shí)間
long timestamp = System.currentTimeMillis() / 1000;
if (StringUtils.isEmpty(body)) {
body = "";
}
//簽名操作
String message = buildMessageByCertificates(method, canonicalUrl, timestamp, nonceStr, body);
//簽名操作
String signature = sign(message.getBytes("utf-8"), keyPath);
//組裝參數(shù)
return "mchid=\"" + merchantId + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + certSerialNo + "\","
+ "signature=\"" + signature + "\"";
}
/**
*
* @param method 請(qǐng)求方法 post
* @param canonicalUrl 請(qǐng)求地址
* @param body 請(qǐng)求參數(shù)
* @return
* @throws Exception
*/
public static String buildMessageByCertificates(
String method,
String canonicalUrl,
Long timestamp,
String nonceStr,
String body) throws Exception {
return method + "\n"
+ canonicalUrl + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ body + "\n";
}
public static String buildMessage(String method, String canonicalUrl, long timestamp, String nonceStr, String body) {
// String canonicalUrl = url.encodedPath();
// if (url.encodedQuery() != null) {
// canonicalUrl += "?" + url.encodedQuery();
// }
return method + "\n"
+ canonicalUrl + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ body + "\n";
}
public static String sign(byte[] message, String keyPath) throws Exception {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(getPrivateKey(keyPath));
sign.update(message);
return Base64.encodeBase64String(sign.sign());
}
/**
* 微信支付-前端喚起支付參數(shù)-獲取商戶私鑰
*
* @param filename 私鑰文件路徑 (required)
* @return 私鑰對(duì)象
*/
public static PrivateKey getPrivateKey(String filename) throws IOException {
// log.error("簽名 證書(shū)地址是 "+filename);
Path path = Paths.get(filename);
// byte[] bytes = Files.readAllBytes(path);
// String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
try {
// String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
// .replace("-----END PRIVATE KEY-----", "")
// .replaceAll("\\s+", "");
String privateKey = "放入私鑰(可在apiclient_key.pem文件中拿到【apiclient_key.p12可轉(zhuǎn)換為apiclient_key.pem】)";
//System.out.println("--------privateKey---------:"+privateKey);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(
new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("當(dāng)前Java環(huán)境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("無(wú)效的密鑰格式");
}
}
/**
* 獲取隨機(jī)位數(shù)的字符串
* @param length
* @return
*/
public static String getRandomString(int length) {
String base = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
}
參考連接
https://blog.csdn.net/weixin_43401380/article/details/128027468
https://blog.csdn.net/heima005/article/details/126740367
https://developers.weixin.qq.com/community/develop/doc/000a2887e28f00773cfe05b3651c00?source=indexmixflow
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/wechatpay5_1.shtml
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
https://developers.weixin.qq.com/community/pay/article/doc/00084630cf40e82afc2c843eb5b413