前言
新开发的小程序要调起微信支付,关于微信支付API V3对接(Java)的资料不是很多,研究了很久文档和SDK里的代码,也踩了很多坑,特此记录。
本文包括微信商家转账到零钱
依赖:
<!-- json转换 -->
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>maven-surefire-common</artifactId>
<version>2.22.2</version>
</dependency>
<dependency>
<groupId>org.codehaus.jettison</groupId>
<artifactId>jettison</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
支付的Service详细流程根据自己的需求编写
/**
* 微信支付Service
*
* @author suihao
*/
@Service
@Transactional
public class WechatWithdrawServiceImpl{
public WeChatMoneyVO weixinTransferBat(WeChatMoneyDTO weChatMoneyDTO) throws Exception {
if (weChatMoneyDTO == null) {
throw new Exception("参数异常");
}
if (weChatMoneyDTO.getOpenId() == null || weChatMoneyDTO.getOpenId() == "") {
throw new Exception("OpenId参数为空异常");
}
WechatUserInfo wechatUserInfo = wechatUserInfoMapper.selectOpenId(weChatMoneyDTO.getOpenId());
if (wechatUserInfo == null) {
throw new Exception("OpenId参数异常");
}
if (weChatMoneyDTO.getTotalAmount()>100){
throw new Exception("超出提现额度");
}
Integer totalAmount = weChatMoneyDTO.getTotalAmount();
int total_amount = totalAmount * 100;
Map<String, Object> postMap = new HashMap<String, Object>();
//商家批次单号 长度 1~32
String outNo = IdUtil.getSnowflake(0, 0).nextIdStr();
//申请商户号的appid或商户号绑定的appid(企业号corpid即为此appid)
postMap.put("appid", appId);
postMap.put("out_batch_no", outNo);
//该笔批量转账的名称 存储卡号
postMap.put("batch_name", weChatMoneyDTO.getUserAccount());
//转账说明,UTF8编码,最多允许32个字符
postMap.put("batch_remark", "测试提现");
//转账金额单位为“分”。 总金额
postMap.put("total_amount", total_amount);
//转账总笔数
postMap.put("total_num", 1);
List<Map> list = new ArrayList<>();
Map<String, Object> subMap = new HashMap<>(4);
//商家明细单号
subMap.put("out_detail_no", outNo);
//转账金额
subMap.put("transfer_amount", total_amount);
//转账备注
subMap.put("transfer_remark", weChatMoneyDTO.getTransferRemark());
//用户在直连商户应用下的用户标示(支付用户的openid)
subMap.put("openid", weChatMoneyDTO.getOpenId());
// subMap.put("user_name", RsaCryptoUtil.encryptOAEP(userName, x509Certificate));
list.add(subMap);
postMap.put("transfer_detail_list", list);
//发起转账操作
String resStr = HttpUtil.postTransBatRequest(
WeChatEnum.PAY_URL.getStatus(),
GSON.toJson(postMap),
wechatPayserialNo, //商户证书编号
mchid,//商户号
privatekeypath); //商户证书路径(本机测试时放你本机路径)
JSONObject resStrJsonObject = JSONObject.parseObject(resStr);
String batch_id = JsonUtils.getString(resStrJsonObject, "batch_id");
if (batch_id.isEmpty()) {
throw new Exception("支付失败");
}
String create_time = JsonUtils.getString(resStrJsonObject, "create_time");
if (create_time.isEmpty()) {
throw new Exception("支付失败");
}
String out_batch_no = JsonUtils.getString(resStrJsonObject, "out_batch_no");
if (out_batch_no.isEmpty()) {
throw new Exception("支付失败");
}
return weChatMoneyVO;
}
}
微信支付专用工具类 请求操作方法
package com.hssmart.utils;
import com.sun.xml.internal.bind.v2.runtime.output.Encoded;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.httpclient.util.EncodingUtil;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
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;
/**
* 微信支付专用类 请求操作方法
*
* @author suihao
*/
@Slf4j
public class HttpUtil {
/**
* 发起批量转账API 批量转账到零钱
*
* @param requestUrl
* @param requestJson 组合参数
* @param wechatPayserialNo 商户证书序列号
* @param mchID4M 商户号
* @param privatekeypath 商户私钥证书路径
* @return
*/
public static String postTransBatRequest(
String requestUrl,
String requestJson,
String wechatPayserialNo,
String mchID4M,
String privatekeypath) {
CloseableHttpClient httpclient = HttpClients.createDefault();
CloseableHttpResponse response = null;
HttpEntity entity = null;
try {
//商户私钥证书
HttpPost httpPost = new HttpPost(requestUrl);
// NOTE: 建议指定charset=utf-8。低于4.4.6版本的HttpCore,不能正确的设置字符集,可能导致签名错误
httpPost.addHeader("Content-Type", "application/json");
httpPost.addHeader("Accept", "application/json");
//"55E551E614BAA5A3EA38AE03849A76D8C7DA735A");
httpPost.addHeader("Wechatpay-Serial", wechatPayserialNo);
//-------------------------核心认证 start-----------------------------------------------------------------
String strToken = VechatPayV3Util.getToken("POST",
"/v3/transfer/batches",
requestJson,mchID4M,wechatPayserialNo, privatekeypath);
log.error("微信转账token "+strToken);
// 添加认证信息
httpPost.addHeader("Authorization",
"WECHATPAY2-SHA256-RSA2048" + " "
+ strToken);
//---------------------------核心认证 end---------------------------------------------------------------
httpPost.setEntity(new StringEntity(requestJson, "UTF-8"));
//发起转账请求
response = httpclient.execute(httpPost);
entity = response.getEntity();//获取返回的数据
log.info("-----getHeaders.Request-ID:"+response.getHeaders("Request-ID"));
return EntityUtils.toString(entity,"UTF-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭流
}
return null;
}
}
微信支付生成签名和token工具类
package com.hssmart.utils;
import java.io.IOException;
import java.nio.file.Files;
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 lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.springframework.util.StringUtils;
@Slf4j
public class VechatPayV3Util {
/**
*
* @param method 请求方法 post
* @param canonicalUrl 请求地址
* @param body 请求参数
* @param merchantId 这里用的商户号
* @param certSerialNo 商户证书序列号
* @param keyPath 商户证书地址
* @return
* @throws Exception
*/
public static String getToken(
String method,
String canonicalUrl,
String body,
String merchantId,
String certSerialNo,
String keyPath) throws Exception {
String signStr = "";
//获取32位随机字符串
String nonceStr = getRandomString(32);
//当前系统运行时间
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);
//组装参数
signStr = "mchid=\"" + merchantId + "\",timestamp=\"" + timestamp+ "\",nonce_str=\"" + nonceStr
+ "\",serial_no=\"" + certSerialNo + "\",signature=\"" + signature + "\"";
return signStr;
}
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());
}
/**
* 微信支付-前端唤起支付参数-获取商户私钥
*
* @param filename 私钥文件路径 (required)
* @return 私钥对象
*/
public static PrivateKey getPrivateKey(String filename) throws IOException {
log.error("签名 证书地址是 "+filename);
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+", "");
//System.out.println("--------privateKey---------:"+privateKey);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(
new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的密钥格式");
}
}
/**
* 获取随机位数的字符串
* @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();
}
}