微信支付:商家转账到零钱的开发

主要所需:1、微信商户平台的证书apiclient_cert.pem 2、微信商户平台证书的密钥apiclient_key.pem 3、微信商户平台的证书的序列号

一、转账所需字段

public class WxTransferAccounts {
	private String appid;// 小程序ID	
	private String out_batch_no;// 商家批次订单号   由数字、大小写字母组成[1,32]
	private String batch_name;// 商家批次名称   示例值:2019年1月深圳分部报销单[1,32]	
	private String batch_remark;//批次备注  , [1,32]
	private int total_num;// 转账总笔数   这个总笔数要等于明细笔数的汇总
	private Integer total_amount;// 转账总金额   ,单位为分 这个总金额要等于明细金额的汇总
	
	private List<TransferAccountsArray> transfer_detail_list;//收款方明细

	public String getAppid() {
		return appid;
	}

	public void setAppid(String appid) {
		this.appid = appid;
	}

	public String getOut_batch_no() {
		return out_batch_no;
	}

	public void setOut_batch_no(String out_batch_no) {
		this.out_batch_no = out_batch_no;
	}

	public String getBatch_name() {
		return batch_name;
	}

	public void setBatch_name(String batch_name) {
		this.batch_name = batch_name;
	}

	public String getBatch_remark() {
		return batch_remark;
	}

	public void setBatch_remark(String batch_remark) {
		this.batch_remark = batch_remark;
	}

	public int getTotal_num() {
		return total_num;
	}

	public void setTotal_num(int total_num) {
		this.total_num = total_num;
	}

	public Integer getTotal_amount() {
		return total_amount;
	}

	public void setTotal_amount(Integer total_amount) {
		this.total_amount = total_amount;
	}

	public List<TransferAccountsArray> getTransfer_detail_list() {
		return transfer_detail_list;
	}

	public void setTransfer_detail_list(List<TransferAccountsArray> transfer_detail_list)         
    {
		this.transfer_detail_list = transfer_detail_list;
	}
}



public class TransferAccountsArray {
	private String out_detail_no;// 商家明细单号[1,32]
	private String openid;//用户openid
	private String user_name;// 用户真实姓名,要与微信号绑定的身份实名,超过2000元时必填  需进行加密处理;如低于2000元的转账,则可以不需要此字段
	private String transfer_remark;//转账备注   [1,32]
	private int transfer_amount;// 转账金额
	public String getOut_detail_no() {
		return out_detail_no;
	}
	public void setOut_detail_no(String out_detail_no) {
		this.out_detail_no = out_detail_no;
	}
	public String getOpenid() {
		return openid;
	}
	public void setOpenid(String openid) {
		this.openid = openid;
	}
	public String getUser_name() {
		return user_name;
	}
	public void setUser_name(String user_name) {
		this.user_name = user_name;
	}
	public String getTransfer_remark() {
		return transfer_remark;
	}
	public void setTransfer_remark(String transfer_remark) {
		this.transfer_remark = transfer_remark;
	}
	public int getTransfer_amount() {
		return transfer_amount;
	}
	public void setTransfer_amount(int transfer_amount) {
		this.transfer_amount = transfer_amount;
	}
}

二、转账接口调用前准备

            WxTransferAccounts paramWxTransferAccounts=new WxTransferAccounts();
            paramWxTransferAccounts.setAppid("个人的小程序appid,要与商户绑定");
            paramWxTransferAccounts.setBatch_name("2022.09.29测试新版转账");
            paramWxTransferAccounts.setBatch_remark("2022.09.29测试新版转账");
            paramWxTransferAccounts.setOut_batch_no("商户订单号 32位  自己生成");
            paramWxTransferAccounts.setTotal_amount(100);
            paramWxTransferAccounts.setTotal_num(1);
            
            TransferAccountsArray paramTransferAccountsArray=new TransferAccountsArray();
            paramTransferAccountsArray.setOpenid("收款人的opendid");
            paramTransferAccountsArray.setOut_detail_no("明细订单号 32位 自己生成");
            paramTransferAccountsArray.setTransfer_amount(100);
            paramTransferAccountsArray.setTransfer_remark("2022.09.29测试新版转账");

             paramTransferAccountsArray.setUser_name(rsaEncryptOAEP("真实姓名", certificate));//如果转账低于2000,无需这个字段,否则需要进行隐私信息进行加密处理,加密代码在后面

            List<TransferAccountsArray> listAccounts=new ArrayList<>();
            listAccounts.add(paramTransferAccountsArray);
            paramWxTransferAccounts.setTransfer_detail_list(listAccounts);

三、隐私信息安全加密

//要先获取微信支付平台的公钥证书,通过api获取;而后可以考虑放redis
String mch_id="你自己的商户号";
String privatekeypath="商户平台证书密钥的路径";
String nonce_str=StrUtil.getRandomStringByLength(32);//随机32位字符串
String body="";
long timestamp = System.currentTimeMillis() / 1000;
String orgSignText = "GET\n"
                    + "/v3/certificates\n"
                    + timestamp + "\n"
                    + nonce_str + "\n"
                    + body + "\n";
String signStr=VechatPayV3Util.sign(orgSignText.getBytes("utf-8"), privatekeypath);//获得签名

String wechatPayserialNo="微信商户平台证书的序列号";

String auth = "WECHATPAY2-SHA256-RSA2048 "
                    + "mchid=\""+mch_id+"\",nonce_str=\""
                    + nonce_str + "\",timestamp=\"" + timestamp
                    + "\",serial_no=\"" + wechatPayserialNo + "\",signature=\"" + signStr + "\"";

//获取微信支付平台公钥证书
            String platform_publickey = HttpUtil.sendGetRequest("https://api.mch.weixin.qq.com/v3/certificates", auth,null);
//获取的微信支付平台公钥证书是一个json字符串,自行转成json对象

//获得的公钥证书是加密的,需要用apiV3的密钥进行解密
String publickey=decryptResponseBody(tempWxpublicKeyData);//tempWxpublicKeyData 这个对象就是取回来的公钥字符串转换的
ByteArrayInputStream inputStream = new ByteArrayInputStream(publickey.getBytes(StandardCharsets.UTF_8));

X509Certificate certificate2=getCertificate(inputStream);
rsaEncryptOAEP("用户的真实姓名", certificate2)//加密隐私信息  这里我用来加密转账所需的姓名




/**
     * 解密响应体.   得到微信平台证书公钥,解密后的字符串即为公钥字符串
     *
     * @param apiV3Key       API V3 KEY  API v3密钥 商户平台设置的32位字符串
     * @param associatedData  response.body.data[i].encrypt_certificate.associated_data
     * @param nonce          response.body.data[i].encrypt_certificate.nonce
     * @param ciphertext     response.body.data[i].encrypt_certificate.ciphertext
     * @return the string
     * @throws GeneralSecurityException the general security exception
     */
    public static String decryptResponseBody(WxpublicKeyData tempWxpublicKeyData) {
    	
    	//tempWxpublicKeyData 这个对象就是取回来的公钥字符串转换的,有时回取回多条公钥,取时间最新的
    	 String apiV3Key="微信商户平台apiV3的密钥,记得去微信商户平台设置";
    	 String associatedData=tempWxpublicKeyData.getEncrypt_certificate().getAssociated_data();
    	 String nonce=tempWxpublicKeyData.getEncrypt_certificate().getNonce();
         String ciphertext=tempWxpublicKeyData.getEncrypt_certificate().getCiphertext();
    
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

            SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");

            GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));

            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
       
            byte[] bytes;
                try {
                    bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext));
                } catch (GeneralSecurityException e) {
                    throw new IllegalArgumentException(e);
                }    
            return new String(bytes, StandardCharsets.UTF_8);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }



/**
     * 获取证书
     *
     * @param inputStream 证书文件
     * @return {@link X509Certificate} 获取证书
     */
    public static X509Certificate getCertificate(InputStream inputStream) {
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X509");
            X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
            cert.checkValidity();
            return cert;
        } catch (CertificateExpiredException e) {
            throw new RuntimeException("证书已过期", e);
        } catch (CertificateNotYetValidException e) {
            throw new RuntimeException("证书尚未生效", e);
        } catch (CertificateException e) {
            throw new RuntimeException("无效的证书", e);
        }
    }



/**
     * 公钥加密   加密隐私信息数据
     *
     * @param data        待加密数据
     * @param certificate 平台公钥证书
     * @return 加密后的数据
     * @throws Exception 异常信息
     */
    public static String rsaEncryptOAEP(String data, X509Certificate certificate) throws Exception {
        try {
            Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
            cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
            byte[] dataByte = data.getBytes(StandardCharsets.UTF_8);
            byte[] cipherData = cipher.doFinal(dataByte);
           // String s = new String(cipherData);
            
            return java.util.Base64.getEncoder().encodeToString(cipherData);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
        } catch (InvalidKeyException e) {
            throw new IllegalArgumentException("无效的证书", e);
        } catch (IllegalBlockSizeException | BadPaddingException e) {
            throw new IllegalBlockSizeException("加密原串的长度不能超过214字节");
        }
    }

四、微信商家转账到零钱接口调用

 //发起转账操作 

//certificate2.getSerialNumber().toString(16).toUpperCase() :微信支付平台证书的序列号
//wechatPayserialNo :微信商户平台证书的序列号
//mch_id:商户号
//privatekeypath:微信商户平台密钥地址
//注:我这边传递了两个证书序列号,实际post时只需要一个,做测试发现当有传递加密隐私信息时,序列号用微信支付平台证书的序列号;没有传递加密隐私信息时,则用微信商户平台证书的序列号即可
  String transferurl="https://api.mch.weixin.qq.com/v3/transfer/batches";
  String resStr = HttpUtil.postTransBatRequest(
    				transferurl,
    				JSONObject.toJSONString(paramWxTransferAccounts),
    				certificate2.getSerialNumber().toString(16).toUpperCase(),
    				wechatPayserialNo,
    				mch_id,
    				privatekeypath);




HttpUtil的公用类

import java.io.IOException;
import java.util.HashMap;

import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.ParseException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
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.DefaultHttpClient;
import org.apache.http.impl.client.HttpClients;

import org.apache.http.util.EntityUtils;

import ch.qos.logback.classic.Logger;


/**
 * 微信支付专用类 请求操作方法 2022.09.29  新版商家转账到零钱
 *
 * @author Administrator
 */
public class HttpUtil {
	/**
	 * 发起批量转账API 批量转账到零钱
	 *
	 * @param requestUrl
	 * @param requestJson 组合参数
	 * @param wechatPayserialNo 商户证书序列号
	 * @param mchID4M  商户号 
	 * @param privatekeypath  商户私钥证书路径
	 * @return
	 */
	public static String postTransBatRequest(
			String requestUrl,
			String requestJson,
			String platform_wechatPayserialNo,
			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");
			//httpPost.addHeader("Wechatpay-Serial", wechatPayserialNo);
			httpPost.addHeader("Wechatpay-Serial", platform_wechatPayserialNo);//用了隐私信息加密时,上传的微信支付平台公钥的序列号
			//-------------------------核心认证 start-----------------------------------------------------------------
		
			String strToken = VechatPayV3Util.getToken("POST",
					"/v3/transfer/batches",
					requestJson,mchID4M,wechatPayserialNo, privatekeypath);
			
			System.out.println("微信转账token "+strToken);
			// 添加认证信息
			httpPost.addHeader("Authorization",
					"WECHATPAY2-SHA256-RSA2048" + " "
							+ strToken);
			//---------------------------核心认证 end---------------------------------------------------------------
			httpPost.setEntity(new StringEntity(requestJson, "UTF-8"));
			//发起转账请求
			response = httpclient.execute(httpPost);
			entity = response.getEntity();//获取返回的数据
			return EntityUtils.toString(entity);
		} catch (Exception e) {
			System.out.println(e.getMessage());
			e.printStackTrace();
		} finally {
			// 关闭流
		}
		return null;
	}
	
	
	/**
	 * 发送HTTP_GET请求
	 * 
	 * @see 该方法会自动关闭连接,释放资源
	 * @param reqURL
	 *            请求地址(含参数)
	 * @param decodeCharset
	 *            解码字符集,解析响应数据时用之,其为null时默认采用UTF-8解码
	 * @return 远程主机响应正文
	 */
	public static String sendGetRequest(String reqURL,String auth,String decodeCharset) {
		long responseLength = 0; // 响应长度
		String responseContent = null; // 响应内容
		HttpClient httpClient = new DefaultHttpClient(); // 创建默认的httpClient实例
		
		HttpGet httpGet = new HttpGet(reqURL); // 创建org.apache.http.client.methods.HttpGet
		httpGet.addHeader("Authorization", auth);
		httpGet.addHeader("Accept", "application/json");
		httpGet.addHeader("User-Agent", "https://zh.wikipedia.org/wiki/User_agent");
		try {
			HttpResponse response = httpClient.execute(httpGet); // 执行GET请求
			HttpEntity entity = response.getEntity(); // 获取响应实体
			if (null != entity) {
				responseLength = entity.getContentLength();
				responseContent = EntityUtils.toString(entity, decodeCharset == null ? "UTF-8" : decodeCharset);
				EntityUtils.consume(entity); // Consume response content
			}
		} catch (ClientProtocolException e) {
			System.out.println("该异常通常是协议错误导致,比如构造HttpGet对象时传入的协议不对(将'http'写成'htp')或者服务器端返回的内容不符合HTTP协议要求等,堆栈信息如下");
		} catch (ParseException e) {
			System.out.println(e.getMessage());
		} catch (IOException e) {
			System.out.println("该异常通常是网络原因引起的,如HTTP服务器未启动等,堆栈信息如下");
		} finally {
			httpClient.getConnectionManager().shutdown(); // 关闭连接,释放资源
		}
		return responseContent;
	}


}

VechatPayV3Util公用类

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.List;
import java.util.Random;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.springframework.util.Base64Utils;
import org.springframework.util.StringUtils;

import cn.eeyycc.mina.framework.wxpay.model.WxpublicKeyData;

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 {

	      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();
	}
	
}

转账成功示例:

 

获取微信支付平台证书时,解密可能会报错密钥

 java.security.InvalidKeyException: Illegal key size

解决方案:(异常: java.security.InvalidKeyException: Illegal key size - 萌新啊萌新是我 - 博客园

至于报错IP未设置,api支付未开启,余额不足啥的,都在微信商户平台进行设置即可

开发参考:微信支付 发起商家转账API 2022年v3 transfer batches_早起的年轻人的博客-CSDN博客_微信转账api

 java开发 微信商家转账到零钱,发起商家转账API,微信支付_海贝里的灰尘的博客-CSDN博客_java实现微信转账

Java中的微信支付(2):API V3 微信平台证书的获取与刷新 - 走看看

微信支付V3获取平台证书并解密平台证书详细流程_低调使人进步的博客-CSDN博客_微信支付平台证书

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值