SprintBoot整合微信支付API v3,wechatpay-apache-httpclient

微信支付API v3:https://wechatpay-api.gitbook.io/wechatpay-api-v3

API字典:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/index.shtml

根据下面的流程,完成所有配置、准备后,分分钟唤起支付。

支持App、Jsapi、H5、Native,4种支付方式。

一、配置

        要调通微信支付最新的v3支付,需配置提供4项内容:商户私钥、证书序列号、APIv3密钥、平台证书

        登录(https://pay.weixin.qq.com)后,在“账户中心”->“API安全”。完成“申请API证书”和“设置APIv3密钥”配置。“设置API密钥”可不设置,v3支付用不到。

        1、商户私钥

        在“申请API证书”,根据引导流程完成配置,你将下载获得(apiclient_cert.p12、apiclient_cert.pem、apiclient_key.pem)三个文件,其中apiclient_key.pem就是商户私钥,另外两个文件没用。

        2、证书序列号

        在完成“申请API证书”配置后,右侧点击“管理证书”,可以看到证书序列号,复制保存下来。

        3、设置APIv3密钥

        随机字符串,对于v3支付,该密钥很重要。

        SprintBoot整合微信支付API v3,wechatpay-apache-httpclient_第1张图片

        4、平台证书

                4.1、https://github.com/EliasZzz/CertificateDownloader/releases,这里下载CertificateDownloader-1.1.jar包;

                4.2、cmd执行命令:“java -jar CertificateDownloader-1.1.jar -f 商户私钥文件路径 -k 证书解密的密钥 -m 商户号 -o 证书保存路径 -s 商户证书序列号”。

                        商户私钥文件路径:apiclient_key.pem的本地存放路径;

                        证书解密的密钥:设置的APIv3密钥;

                        商户号:当前配置的商户号;

                        证书保存路径:生成平台证书后,存放的路径;

                        商户证书序列号:保存的证书序列号。

                4.3、执行完后,在证书保存路径下,有个类似wechatpay_3B******27691.pem的文件,里面的内容就是平台证书。

        5、绑定

                在“产品中心”->“开发配置”。完成相关域名路径配置。

                在“产品中心”->“AppID账号管理”。完成相关公众号、小程序绑定。(JSAPI支付需要)。

 二、引入jar包


    com.github.wechatpay-apiv3
    wechatpay-apache-httpclient
    0.2.2

三、创建实体

/**
 * 微信支付预下单-主体信息
 * @author shuaifengjie.com
 * v3
 */
@Data
public class WxpayTradeVo implements Serializable {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 8189139771687842957L;
	
	// 由微信生成的应用ID,全局唯一。请求基础下单接口时请注意APPID的应用属性,应为公众号的APPID
	// 示例值:wxd678efh567hg6787
	private String appid;
	
	// 直连商户的商户号,由微信支付生成并下发。
	// 示例值:1230000109
	private String mchid;
	
	// 商品描述
	// 示例值:Image形象店-深圳腾大-QQ公仔
	private String description;
	
	// 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
	// 示例值:1217752501201407033233368018
	private String out_trade_no;
	
	// 通知URL必须为直接可访问的URL,不允许携带查询串。
	// 示例值:https://www.weixin.qq.com/wxpay/pay.php
	private String notify_url;
	
	// 订单金额信息
	private WxTradeAmountVo amount;
	
	// 支付者信息 - JSAPI,必传
	private WxPayerVo payer;
	
	// 支付场景描述 - H5,必传
	private WxSceneInfoVo scene_info;

	public WxpayTradeVo() {
		super();
	}
	
	public WxpayTradeVo(String appid, String mchid, String description, String out_trade_no, String notify_url) {
		super();
		this.appid = appid;
		this.mchid = mchid;
		this.description = description;
		this.out_trade_no = out_trade_no;
		this.notify_url = notify_url;
	}

	@Data
	public static class WxTradeAmountVo {
		// 订单总金额,单位为分。
		// 示例值:100
		private long total;
		// CNY:人民币,境内商户号仅支持人民币。
		// 示例值:CNY
		private String currency = "CNY";
		
		public WxTradeAmountVo() {
			super();
		}
		public WxTradeAmountVo(long total) {
			super();
			this.total = total;
		}
	}
	
	@Data
	public static class WxPayerVo {
		// 用户在直连商户appid下的唯一标识。 下单前需获取到用户的Openid,Openid获取详见
		// 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
		private String openid;
		
		public WxPayerVo() {
			super();
		}
		public WxPayerVo(String openid) {
			super();
			this.openid = openid;
		}
	}

	@Data
	public static class WxSceneInfoVo {
		// 用户的客户端IP,支持IPv4和IPv6两种格式的IP地址。
		// 示例值:14.23.150.211
		private String payer_client_ip;
		// H5场景信息
		private WxH5InfoVo h5_info;

		public WxSceneInfoVo() {
			super();
		}
		public WxSceneInfoVo(String payer_client_ip) {
			super();
			this.payer_client_ip = payer_client_ip;
		}
	}

	@Data
	public static class WxH5InfoVo {
		// 场景类型
		// 示例值:iOS, Android, Wap
		private String type = "Wap";
	}
	
}
/**
 * 支付、退款数据回调通用
 * @author shuaifengjie.com
 */
@Data
public class CallbackBodyVo implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = -3279905156049535047L;


    /** 通知Id */
    private String id;
    /**
     * 通知创建时间
     */
    private String create_time;
    /**
     * 通知类型
     * 支付成功:TRANSACTION.SUCCESS
     * 退款成功:REFUND.SUCCESS
     */
    private String resource_type;
    /**
     * 通知数据类型
     */
    private String event_type;
    /**
     * 回调摘要
     */
    private String summary;
    /**
     * 通知数据
     */
    private Resource resource;

    /**
     * 通知数据
     */
    @Data
    public static class Resource {
        /**
         * 对开启结果数据进行加密的加密算法,目前只支持AEAD_AES_256_GCM。
         */
        private String algorithm;
        /**
         * Base64编码后的开启/停用结果数据密文。
         */
        private String ciphertext;
        /**
         * 附加数据。
         */
        private String associated_data;
        /**
         * 加密使用的随机串。
         */
        private String nonce;
        /**
         * 原始回调类型。
         */
        private String original_type;
    }
}
/**
 * 微信退款预下单-主体信息
 * @author shuaifengjie.com
 * v3
 */
@Data
public class WxpayRefundVo implements Serializable {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 8189139771687842957L;
	// 原支付交易对应的微信订单号
	// 示例值:1217752501201407033233368018
	private String transaction_id;
	// 若商户传入,会在下发给用户的退款消息中体现退款原因
	// 示例值:商品已售完
	private String reason;
	// 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。
	// 示例值:1217752501201407033233368018
	private String out_refund_no;
	// 异步接收微信支付退款结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效,优先回调当前传的这个地址。
	// 示例值:https://weixin.qq.com
	private String notify_url;
	// 订单金额信息
	private WxRefundAmountVo amount;

	public WxpayRefundVo() {
		super();
	}

	@Data
	public static class WxRefundAmountVo {
		// 退款金额,币种的最小单位,只能为整数,不能超过原订单支付金额。
		// 示例值:888
		private long refund;
		// 原支付交易的订单总金额,币种的最小单位,只能为整数。
		// 示例值:888
		private long total;
		// CNY:人民币,境内商户号仅支持人民币。
		// 示例值:CNY
		private String currency = "CNY";
		public WxRefundAmountVo() {
			super();
		}
		public WxRefundAmountVo(long refund, long total) {
			super();
			this.refund = refund;
			this.total = total;
		}
	}
}

四、生成预订单

        1、关键参数

private static String appid= "wxadf3******062eaa"; // appid(第“一”点,第5步,绑定的的公众号、小程序appid)

private static String mchid = "1xxxxxxx4"; // 商户号

private static String apiv3Key= "8af077******52de54c27"; // v3密钥

private static String mchSerialNo = "185xxxxxxxxxxxxxxxxx84D5"; // 商户私钥证书序列号

// 商户私钥(apiclient_key.pem文件,用文本编辑器打开复制进来就可以了)
private static String privateKey = "-----BEGIN PRIVATE KEY-----\n"
			+ "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCx6p0IY6gvOa/S\n"
			+ "..........\n"
			+ "RhjzfVRMPMHXkBeZ60CH/oNG8dCFA96LRA4ZtdaBAoGAVdRPW71M9DcNOnQcXDHT\n"
			+ "-----END PRIVATE KEY-----";

// 平台证书(类似wechatpay_3B4C48********2027691.pem的文件,文本编辑器打开复制)
private static String certificate = "-----BEGIN CERTIFICATE-----\n"
			+ "MIID3DCCAsSgAwIBAgIUO0xIb2Q8Fm7S2M1BP82GdfICdpEwDQYJKoZIhvcNAQEL\n"
			+ "..........\n"
			+ "oDIMMb+IMVvRat8MiWRuWEe6OjSZmeeNatCV0pcyers=\n"
            + "-----END CERTIFICATE-----";

        如果出现“应答的微信支付签名验证失败”,请检查平台证书(certificate)。该证书是第“一”点的第4步获取的证书。

        2、微信支付帮助类

PayScene:app/h5/pc/wx;

ClientException:自定义异常,删掉即可;

StringHelper:String转Map参数方法;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.X509Certificate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;

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.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.util.Base64Utils;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;

import lombok.extern.slf4j.Slf4j;

/**
 * 微信支付帮助类
 * @author shuaifengjie.com
 */
@Slf4j
public class WxpayUtils {
	
	/**
	 * 发起支付
	 * @param tradeVo
	 * @param scene
	 * @param privateKey
	 * @param certificate
	 * @param serialNo
	 * @return
	 */
	public static ObjectNode getWxpay(WxpayTradeVo tradeVo, String scene, String privateKey, String certificate, String serialNo) {
		// ...
		String responseBody = response(JSON.toJSONString(tradeVo), getUrl(scene), tradeVo.getMchid(), privateKey, certificate, serialNo);
		log.info("======>>> wxpay response body: ", responseBody);
		// parse
		@SuppressWarnings("unchecked")
		Map resultMap = StringHelper.strToMap(responseBody);
		// ...
		ObjectMapper objectMapper = new ObjectMapper();
		ObjectNode objectNode = objectMapper.createObjectNode();
		// ...
		if (PayScene.WEB.getScene().equals(scene)) {// pc - 扫码支付(Native)
			objectNode.put("code_url", resultMap.get("code_url"));
		} else if (PayScene.H5.getScene().equals(scene)) {// 手机网页(H5)
			objectNode.put("h5_url", resultMap.get("h5_url"));
		} else if (PayScene.WX_H5.getScene().equals(scene)) {// 微信公众号(H5)
			String prepayId = resultMap.get("prepay_id");
			objectNode.put("prepay_id", prepayId);
			// ...
			objectNode.put("appId", tradeVo.getAppid());
			String timestamp = getTimestamp();
			objectNode.put("timeStamp", timestamp);
			String nonceStr = CreateNoncestr();
			objectNode.put("nonceStr", nonceStr);
			String packageStr = "prepay_id=" + prepayId;
			objectNode.put("package", packageStr);
			// 签名
            String paySign = doRequestSign(privateKey, tradeVo.getAppid(), timestamp, nonceStr, packageStr);
			objectNode.put("paySign", paySign);
			objectNode.put("signType", "RSA");
		}
		return objectNode;
	}
	
	/**
	 * 发起退款
	 * @param tradeVo
	 * @param mchid
	 * @param privateKey
	 * @param certificate
	 * @param serialNo
	 * @return
	 */
	public static void refund(WxpayRefundVo tradeVo, String mchid, String privateKey, String certificate, String serialNo) {
		// ...
		String responseBody = response(JSON.toJSONString(tradeVo), REFUND_URL, mchid, privateKey, certificate, serialNo);
		log.info("======>>> wxrefund response body: ", responseBody);
	}
	
	/**
	 * 公共请求
	 * @param data
	 * @param url
	 * @param mchid
	 * @param privateKey
	 * @param certificate
	 * @param serialNo
	 * @return
	 */
	public static String response(String data, String url, String mchid, String privateKey, String certificate, String serialNo) {
		log.info("====>>> wxpay response, mchid: {}, serialNo: {}, url: {}, privateKey: {}, certificate: {}, data: {}", 
				mchid, serialNo, url, privateKey, certificate, data);
		// ...
		HttpPost httpPost = new HttpPost(url);
		StringEntity reqEntity = new StringEntity(data, ContentType.create("application/json", "utf-8"));
		// ...
		httpPost.setEntity(reqEntity);
		httpPost.addHeader("Accept", "application/json");
		// ...
		CloseableHttpResponse response = null;
		// 返回
		String body = null;
		try {
			// 商户私钥
			PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
			// ...
			response = getClient(mchid, serialNo, merchantPrivateKey, certificate).execute(httpPost);
			log.info("====>>> wxpay response, result: {}", JSON.toJSONString(response));
			// response = httpClient.execute(httpPost);
			// assertTrue(response.getStatusLine().getStatusCode() != 401);
			int statusCode = response.getStatusLine().getStatusCode();
			HttpEntity entity = response.getEntity();
			body = EntityUtils.toString(entity);
			log.info("====>>> wxpay response, success: {}, body: {}", statusCode, body);
			if (statusCode != 200) {
				@SuppressWarnings("unchecked")
				Map errorMsg = StringHelper.strToMap(body);
				throw new ClientException(errorMsg.get("message"), ClientExceptionEnum.SERVICE_FORBIDDEN.getCode());
			}
			// 执行释放
			EntityUtils.consume(entity);
		} catch (ClientException e) {
			log.error("======>>> wxpay response, client exception: {}", e);
			throw new ClientException(e.getMessage(), e.getCode());
		} catch (Exception e) {
			log.error("======>>> wxpay response, exception: {}", e);
			throw new ClientException("微信请求失败,请重试(1)", ClientExceptionEnum.SERVICE_FORBIDDEN.getCode());
		} finally {
			try {
				response.close();
			} catch (IOException e) {
				log.error("======>>> wxpay response, ioe exception: {}", e);
				throw new ClientException("微信请求失败,请重试(2)", ClientExceptionEnum.SERVICE_FORBIDDEN.getCode());
			}
		}
		return body;
	}

	/**
	 * 创建Client
	 * @param mchid
	 * @param serialNo
	 * @param privateKey
	 * @param certificate
	 * @return
	 * @throws UnsupportedEncodingException 
	 */
	public static CloseableHttpClient getClient(String mchid, String serialNo, PrivateKey privateKey, String certificate) throws UnsupportedEncodingException {

		// 使用自动更新的签名验证器,不需要传入证书
//		AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
//	        new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)), apiV3Key.getBytes("utf-8"));
		
		// builder
		WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create().withMerchant(mchid, serialNo, privateKey);
//				.withValidator(new WechatPay2Validator(verifier));

		List certs = new ArrayList<>();
		certs.add(PemUtil.loadCertificate(new ByteArrayInputStream(certificate.getBytes("utf-8"))));
		builder.withWechatpay(certs);
		return builder.build();
	}

	private static final String JSAPI_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
	
	private static final String H5_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/h5";

	private static final String NATIVE_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/native";

	private static final String REFUND_URL = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
	
	/**
	 * 
	 * @param scene
	 * @return
	 */
	private static String getUrl(String scene) {
		// ...
		if (PayScene.WEB.getScene().equals(scene)) {// pc - 扫码支付(Native)
			return NATIVE_URL;
		} else if (PayScene.H5.getScene().equals(scene)) {// 手机网页(H5)
			return H5_URL;
		} else if (PayScene.WX_H5.getScene().equals(scene)) {// 微信公众号(H5)
			return JSAPI_URL;
		} else {
			throw new ClientException("不支持的[" + scene + "]场景", ClientExceptionEnum.DATA_MATCH_FAIL.getCode());
		}
	}

	/**
	 * 字符串
	 * @return
	 */
	public static String CreateNoncestr() {
		String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
		String res = "";
		for (int i = 0; i < 16; i++) {
			Random rd = new Random();
			res += chars.charAt(rd.nextInt(chars.length() - 1));
		}
		return res;
	}
	
	/**
	 * 得到时间戳
	 * @return
	 */
	public static String getTimestamp() {
        long epochSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
        return String.valueOf(epochSecond);
//		return Long.toString(System.currentTimeMillis() / 1000);
	}

    /**
     * 加密算法提供方 - BouncyCastle
     */
    private static final String BC_PROVIDER = "BC";
    
    /**
     * 
     * @param privateKey
     * @param orderedComponents
     * @return
     */
    public static String doRequestSign(String privateKey, String... orderedComponents) {
    	try {
			// 商户私钥
			PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
            Signature signer = Signature.getInstance("SHA256withRSA", BC_PROVIDER);
            signer.initSign(merchantPrivateKey);
            final String signatureStr = createSign(true, orderedComponents);
            signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
            return Base64Utils.encodeToString(signer.sign());
		} catch (InvalidKeyException e) {
			log.error("InvalidKeyException: {}", e);
		} catch (SignatureException e) {
			log.error("SignatureException: {}", e);
		} catch (NoSuchProviderException e) {
			log.error("NoSuchProviderException: {}", e);
		} catch (NoSuchAlgorithmException e) {
			log.error("NoSuchAlgorithmException: {}", e);
		} catch (UnsupportedEncodingException e) {
			log.error("UnsupportedEncodingException: {}", e);
		}
		return null;
    }

    /**
     * 请求时设置签名   组件
     * @param components the components
     * @return string string
     */
    private static String createSign(boolean newLine, String... components) {
        String suffix = newLine ? "\n" : "";
        return Arrays.stream(components).collect(Collectors.joining("\n", "", suffix));
    }

    /**
     * 使用微信平台证书,对微信回调数据验签,做应答签名比较
     * @param certificate
     * @param wechatpaySignature
     * @param wechatpayTimestamp
     * @param wechatpayNonce
     * @param body
     * @return
     */
    public static boolean responseSignVerify(String certificate, String wechatpaySignature, String wechatpayTimestamp, String wechatpayNonce, String body) {
    	try {
            final String signatureStr = createSign(true, wechatpayTimestamp, wechatpayNonce, body);
            Signature signer = Signature.getInstance("SHA256withRSA");
            signer.initVerify(PemUtil.loadCertificate(new ByteArrayInputStream(certificate.getBytes("utf-8"))));
            signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
            // ...
            return signer.verify(Base64Utils.decodeFromString(wechatpaySignature));
		} catch (UnsupportedEncodingException e) {
			log.error("UnsupportedEncodingException: {}", e);
		} catch (SignatureException e) {
			log.error("SignatureException: {}", e);
		} catch (NoSuchAlgorithmException e) {
			log.error("NoSuchAlgorithmException: {}", e);
		} catch (InvalidKeyException e) {
			log.error("InvalidKeyException: {}", e);
		}
		return false;
    }
    
    /**
     * 微信V3密钥解密响应体
     * @param apiv3Key
     * @param bodyVo
     * @return
     */
    public static String decryptResponseBody(String apiv3Key, CallbackBodyVo bodyVo) {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
 
            SecretKeySpec key = new SecretKeySpec(apiv3Key.getBytes(StandardCharsets.UTF_8), "AES");
            GCMParameterSpec spec = new GCMParameterSpec(128, bodyVo.getResource().getNonce().getBytes(StandardCharsets.UTF_8));
 
            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(bodyVo.getResource().getAssociated_data().getBytes(StandardCharsets.UTF_8));
 
            byte[] bytes = cipher.doFinal(Base64Utils.decodeFromString(bodyVo.getResource().getCiphertext()));
            return new String(bytes, StandardCharsets.UTF_8);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
			log.error("NoSuchAlgorithmException: {}", e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
			log.error("InvalidKeyException: {}", e);
        } catch (IllegalBlockSizeException e) {
			log.error("IllegalBlockSizeException: {}", e);
		} catch (BadPaddingException e) {
			log.error("BadPaddingException: {}", e);
		}
		return null;
    }
	
	
}

        3、发起支付

// 主体
WxpayTradeVo tradeVo = new WxpayTradeVo();
tradeVo.setAppid("appid");
tradeVo.setMchid("mchid" );
tradeVo.setNotify_url("notifyUrl");
tradeVo.setOut_trade_no("outTradeNo");
tradeVo.setDescription("descr");
// amount
tradeVo.setAmount(new WxTradeAmountVo(1));
// 场景
tradeVo.setScene_info(new WxSceneInfoVo("115.28.90.28"));
// 发起支付的场景:app、h5、pc、wx
String scene = "wx";
// 微信公众号(H5),需要openid
if ("wx".equals(scene)) {
	tradeVo.setPayer(new WxPayerVo(openid));
}
// ...
ObjectNode objectNode = WxpayUtils.getWxpay(tradeVo, scene, privateKey, publicKey, paymentMchDto.getSerialNo());

 

你可能感兴趣的:(微信,java,微信支付,springboot,v3支付)