微信支付(JSAPI)java-微信小程序版本

935 阅读7分钟
没啥好说的,直接淦!
一、前期准备
1. 微信支付商户号
2. 商户平台至API安全的API秘钥
3. 微信小程序appid(其它平台支付对应相应id即可)
二、本次对接的是微信支付JSAPI模式,后端java,前端微信小程序
三、接口官方文档地址pay.weixin.qq.com/wiki/doc/ap…
官方文档说明大致如下:
1. 小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API
2. 商户server调用支付统一下单,api参见公共api【统一下单API
3. 商户server调用再次签名,api参见公共api【再次签名
4. 商户server接收支付通知,api参见公共api【支付结果通知API
5. 商户server查询支付结果,如未收到支付通知的情况,商户后台系统可调用【查询订单API】 (查单实现可参考:支付回调和查单实现指引
ps:对于1、4、5都是基于基础以及调用支付成功之后的业务逻辑,较简单理解;可直接百度。这里只记录调用支付统一下单
四、统一下单
说明:调用统一下单接口主要是为了获取前端调用支付所需要的参数(prepay_id)信息
文档简述如下:
1. 应用场景:商户在小程序中先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易后调起支付。
2. 接口链接:api.mch.weixin.qq.com/pay/unified…
3. 是否需要证书:否
4. 请求参数:可以去文档细看pay.weixin.qq.com/wiki/doc/ap…
ps:此处参数都比较好得到,在官方给的SDK文件的方法中可以直接或者间接得到,下面会丢出SDK下载地址
注意:其中有个sign(签名)参数,需要根据特定算法拿到,同学们可以看着文档的示例理解获取sign值的原理,下面我也会贴出源码;文档地址pay.weixin.qq.com/wiki/doc/ap…
精简分析:
1. 填写所有参数信息
2. 通过算法得到sign参数(也是所有参数其中之一)
3. 所有参数以xml数据格式请求至api.mch.weixin.qq.com/pay/unified…
4. 请求回调中得到prepay_id参数信息
至此第一轮验证完成!
紧接第一轮验证得到prepay_id参数信息继续第二轮参数验证,目的为了得到小程序调起支付所需要的参数paySign;
5.第二次验证需要的参数不多(包括:appId小程序ID、timeStamp时间戳、nonceStr随机串、package数据包、signType签名方式);注意的是这里的timeStamp时间戳、nonceStr随机串要和第一轮验证相应的参数保持一致,不然会导致前端支付报错签名验证失败
ps:至此经过第一、第二请求验证后,已经全部得到前端支付所需要的参数信息,回调给前端既可。
五、代码部分
1. 下载微信官方提供的SDK(demo)pay.weixin.qq.com/wiki/doc/ap…
3.参数转换方法工具类(下载的官方SDK有提供)
package com.example.wxpaySDK;

import com.github.wxpay.sdk.WXPayConstants.SignType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.*;


public class WXPayUtil {

    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private static final Random RANDOM = new SecureRandom();

    /**
     * XML格式字符串转换为Map
     *
     * @param strXML XML字符串
     * @return XML数据转换后的Map
     * @throws Exception
     */
    public static Map<String, String> xmlToMap(String strXML) throws Exception {
        try {
            Map<String, String> data = new HashMap<String, String>();
            DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder();
            InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
            org.w3c.dom.Document doc = documentBuilder.parse(stream);
            doc.getDocumentElement().normalize();
            NodeList nodeList = doc.getDocumentElement().getChildNodes();
            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
                Node node = nodeList.item(idx);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    org.w3c.dom.Element element = (org.w3c.dom.Element) node;
                    data.put(element.getNodeName(), element.getTextContent());
                }
            }
            try {
                stream.close();
            } catch (Exception ex) {
                // do nothing
            }
            return data;
        } catch (Exception ex) {
            WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
            throw ex;
        }

    }

    /**
     * 将Map转换为XML格式的字符串
     *
     * @param data Map类型数据
     * @return XML格式的字符串
     * @throws Exception
     */
    public static String mapToXml(Map<String, String> data) throws Exception {
        org.w3c.dom.Document document = WXPayXmlUtil.newDocument();
        org.w3c.dom.Element root = document.createElement("xml");
        document.appendChild(root);
        for (String key: data.keySet()) {
            String value = data.get(key);
            if (value == null) {
                value = "";
            }
            value = value.trim();
            org.w3c.dom.Element filed = document.createElement(key);
            filed.appendChild(document.createTextNode(value));
            root.appendChild(filed);
        }
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        DOMSource source = new DOMSource(document);
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StringWriter writer = new StringWriter();
        StreamResult result = new StreamResult(writer);
        transformer.transform(source, result);
        String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
        try {
            writer.close();
        }
        catch (Exception ex) {
        }
        return output;
    }


    /**
     * 生成带有 sign 的 XML 格式字符串
     *
     * @param data Map类型数据
     * @param key API密钥
     * @param md5 
     * @return 含有sign字段的XML
     */
    public static String generateSignedXml(final Map<String, String> data, String key, com.example.wxpaySDK.WXPayConstants.SignType md5) throws Exception {
        return generateSignedXml(data, key, SignType.MD5);
    }

    /**
     * 生成带有 sign 的 XML 格式字符串
     *
     * @param data Map类型数据
     * @param key API密钥
     * @param signType 签名类型
     * @return 含有sign字段的XML
     */
    public static String generateSignedXml(final Map<String, String> data, String key, SignType signType) throws Exception {
        String sign = generateSignature(data, key, signType);
        data.put(WXPayConstants.FIELD_SIGN, sign);
        return mapToXml(data);
    }


    /**
     * 判断签名是否正确
     *
     * @param xmlStr XML格式数据
     * @param key API密钥
     * @return 签名是否正确
     * @throws Exception
     */
    public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
        Map<String, String> data = xmlToMap(xmlStr);
        if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
            return false;
        }
        String sign = data.get(WXPayConstants.FIELD_SIGN);
        return generateSignature(data, key).equals(sign);
    }

    /**
     * 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。
     *
     * @param data Map类型数据
     * @param key API密钥
     * @return 签名是否正确
     * @throws Exception
     */
    public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
        return isSignatureValid(data, key, SignType.MD5);
    }

    /**
     * 判断签名是否正确,必须包含sign字段,否则返回false。
     *
     * @param data Map类型数据
     * @param key API密钥
     * @param signType 签名方式
     * @return 签名是否正确
     * @throws Exception
     */
    public static boolean isSignatureValid(Map<String, String> data, String key, SignType signType) throws Exception {
        if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
            return false;
        }
        String sign = data.get(WXPayConstants.FIELD_SIGN);
        return generateSignature(data, key, signType).equals(sign);
    }

    /**
     * 生成签名
     *
     * @param data 待签名数据
     * @param key API密钥
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key) throws Exception {
        return generateSignature(data, key, SignType.MD5);
    }

    /**
     * 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
     *
     * @param data 待签名数据
     * @param key API密钥
     * @param signType 签名方式
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key, SignType signType) throws Exception {
        Set<String> keySet = data.keySet();
        String[] keyArray = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArray);
        StringBuilder sb = new StringBuilder();
        for (String k : keyArray) {
            if (k.equals(WXPayConstants.FIELD_SIGN)) {
                continue;
            }
            if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
                sb.append(k).append("=").append(data.get(k).trim()).append("&");
        }
        sb.append("key=").append(key);
        if (SignType.MD5.equals(signType)) {
        	System.err.println("参数MD5加密完成");
            return MD5(sb.toString()).toUpperCase();
        }
        else if (SignType.HMACSHA256.equals(signType)) {
            return HMACSHA256(sb.toString(), key);
        }
        else {
            throw new Exception(String.format("Invalid sign_type: %s", signType));
        }
    }


    /**
     * 获取随机字符串 Nonce Str
     *
     * @return String 随机字符串
     */
    public static String generateNonceStr() {
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
        }
        return new String(nonceChars);
    }


    /**
     * 生成 MD5
     *
     * @param data 待处理数据
     * @return MD5结果
     */
    public static String MD5(String data) throws Exception {
        java.security.MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] array = md.digest(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 生成 HMACSHA256
     * @param data 待处理数据
     * @param key 密钥
     * @return 加密结果
     * @throws Exception
     */
    public static String HMACSHA256(String data, String key) throws Exception {
        Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
        sha256_HMAC.init(secret_key);
        byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 日志
     * @return
     */
    public static Logger getLogger() {
        Logger logger = LoggerFactory.getLogger("wxpay java sdk");
        return logger;
    }

    /**
     * 获取当前时间戳,单位秒
     * @return
     */
    public static long getCurrentTimestamp() {
        return System.currentTimeMillis()/1000;
    }

    /**
     * 获取当前时间戳,单位毫秒
     * @return
     */
    public static long getCurrentTimestampMs() {
        return System.currentTimeMillis();
    }

}

3.接口地址类
@PostMapping("pay")
	public static Map<Object, Object> pay(String code, HttpServletRequest request) throws Exception {// 获取统一下单
		Map<Object, Object> miniMap = new HashMap<Object, Object>();

		WXPayUtil xmlUtil = null;//此类包含所有参数转换的方法

		System.err.println("输出用户的code:" + code);
		// 授权(必填)
		String grant_type = "authorization_code";
		// 请求微信服务器,使用code获取openid和session_key
		String param = "appid=" + APPID + "&secret=" + AppSecret + "&js_code=" + code + "&grant_type=" + grant_type;
		// 发送请求
		String sr = HttpRequestUtil.sendGet("https://api.weixin.qq.com/sns/jscode2session", param);
		// 解析相应内容(转换成json对象)
		JSONObject json = JSONObject.parseObject(sr);
		String useropenid = (String) json.get("openid");// 用户的唯一标识(openid)
		String sessionKey = (String) json.get("session_key");// 用户的sessionkey
		String notify_url = "https://www.xxx.com";// 通知地址;这里notify_url是 支付完成后微信发给该链接信息,可以判断会员是否支付成功,改变订单状态等。
		String trade_type = "JSAPI";// 交易的类型
		String nonceStr = xmlUtil.generateNonceStr();// 小程序调取微信支付需要的随机字符串

		Map<String, String> data = new HashMap<String, String>();
		data.put("appid", "xxxx55648xxxx");// appID
		data.put("mch_id", "xxxx56545");// 商户号
		data.put("nonce_str", nonceStr);// 随机字符串
		data.put("openid", useropenid);// 用户标识
		data.put("body", "商品结算支付");// 商品描述
		data.put("out_trade_no", RandomStringGenerator.getRandomStringByLength(18));// 商户订单号
		data.put("fee_type", "CNY");// 标价币种,默认人民币:CNY
		data.put("total_fee", "1");// 标价金额,按分算
		data.put("spbill_create_ip", request.getRemoteAddr());// 终端IP
		data.put("notify_url", notify_url);// 回调地址
		data.put("trade_type", trade_type); // 交易类型
		data.put("sign_type", "MD5");

		// 统一下单生成签名
		String newsign = xmlUtil.generateSignature(data, "填写你的商户API秘钥");
		System.err.println("输出我的签名:" + newsign);
		data.put("sign", newsign);

		System.err.println(xmlUtil.mapToXml(data));

		// 统一下单 发送请求https://api.mch.weixin.qq.com/pay/unifiedorder,获取结果
		String result = HttpRequestUtil.sendPost("https://api.mch.weixin.qq.com/pay/unifiedorder",
				xmlUtil.mapToXml(data));
		System.err.println("支付统一下单回调结果:" + xmlUtil.xmlToMap(result));

		Date d = new Date();
		long timeStamp = d.getTime() / 1000; // getTime()得到的是微秒, 需要换算成秒
		String create_time = String.valueOf(timeStamp);// 这边要将返回的时间戳转化成字符串,不然小程序端调用wx.requestPayment方法会报签名错误

		Map<String, String> successMap = xmlUtil.xmlToMap(result);
		System.err.println("-----------开始支付二次验证------------");
		System.err.println(successMap.get("prepay_id"));

		String return_code = successMap.get("return_code");// 返回状态码
		String result_code = successMap.get("result_code");// 结果状态码
		String prepay_id = successMap.get("prepay_id");

		// 获取统一下单回调结果;判断并二次签名
		if ("SUCCESS".equals(return_code) && return_code.equals(result_code)) {
			HashMap<String, String> map = new HashMap<>();
			map.put("appId", "填写你的小程序appid");// 公众appid
			map.put("timeStamp", create_time);// 这边要将返回的时间戳转化成字符串,不然小程序端调用wx.requestPayment方法会报签名错误
			map.put("nonceStr", nonceStr);
			map.put("package", "prepay_id=" + prepay_id);
			map.put("signType", "MD5");
			System.err.println("二次验证签名参数 : " + map);// 需要生成二次签名 所用的参数
			// 再次签名sign,这个签名用于小程序端调用wx.requesetPayment方法
			String sign = WXPayUtil.generateSignature(map, "填写你的商户号秘钥");// 商户号key
			map.put("paySign", sign); // 生成签名 重要
			System.err.println("而成验证生成的签名paySign : " + sign);
			miniMap.put("paySign", sign);// 小程序调取微信支付需要的签名参数
		}

		miniMap.put("timeStamp", create_time);// 小程序调取微信支付需要的时间戳参数
		miniMap.put("nonceStr", nonceStr);// 小程序调取微信支付需要的时间戳参数
		miniMap.put("package", xmlUtil.xmlToMap(result));// 小程序调取微信支付需要的数据包

		return miniMap;
	}
ok,至此已完成后端微信支付的调用,别的平台调用方法大同小异,相应的修改既可。
六、前端调用支付
1.微信小程序调用
uni.request({
	url: "http://192.168.1.4:8080/pay",
	method: "POST",
	data: {
            code: loginsuccessRes.code, 
	},
	header: {'content-type': 'application/x-www-form-urlencoded'}, //post提交方式这里json需改成这个x-www-form-urlencoded,否则后台接收不到数据				
	success: (res) => {
		// console.log("res.data",res.data);
                console.log("res.data",res.data);
		wx.requestPayment({
			"appId":res.data.package.appid,
			"timeStamp": res.data.timeStamp,//时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间
			"nonceStr": res.data.nonceStr,//随机字符串,长度为32个字符以下。
			"package": "prepay_id="+res.data.package.prepay_id,//统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=*
			"signType": "MD5",//签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致
			"paySign": res.data.paySign,//签名,具体签名方案参见
                            "success": (fail)=> {
				console.log("调用支付成功02")
                            },
                            "fail": (fail)=> {
				console.log("调用支付失败02",fail)
                            },
                            "complete": (fail)=> {}
		})
            }
	});
ok!完成。