微信JSAPI公共号支付及常见问题处理

微信官方文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_3

前期准备:

  1. 设置支付目录
  2. 设置授权域名

开发步骤:

调用微信统一下单接口https://api.mch.weixin.qq.com/pay/unifiedorder

获取微信支付四大参数
公众APPID,APPSECEPT ,微信商户平台商户ID, API密钥

统一下单参数说明:

参数名 参数说明
appid 公共号appId
mch_id 微信支付商户号id
device_info 自定义参数,可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB"
nonce_str 随机字符串,长度要求在32位以内。
sign 通过签名算法计算得出的签名值
sign_type 签名类型,默认为MD5,支持HMAC-SHA256和MD5
body 商品简单描述
out_trade_no 商户系统内部订单号
total_fee 订单总金额,单位为分
spbill_create_ip 支持IPV4和IPV6两种格式的IP地址。用户的客户端IP
notify_url 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
trade_type JSAPI -JSAPI支付、NATIVE -Native支付、APP -APP支付
product_id trade_type=NATIVE时,此参数必传。此参数为二维码中包含的商品ID,商户自行定义。
openid trade_type=JSAPI时(即JSAPI支付),此参数必传,此参数为微信用户在商户对应appid下的唯一标识

参数举例:

<xml>
   <appid>wx2421b1c4370ec43bappid>
   <attach>支付测试attach>
   <body>JSAPI支付测试body>
   <mch_id>10000100mch_id>
   <detail>detail>
   <nonce_str>1add1a30ac87aa2db72f57a2375d8fecnonce_str>
   <notify_url>http://wxpay.wxutil.com/pub_v2/pay/notify.v2.phpnotify_url>
   <openid>oUpF8uMuAJO_M2pxb1Q9zNjWeS6oopenid>
   <out_trade_no>1415659990out_trade_no>
   <spbill_create_ip>14.23.150.211spbill_create_ip>
   <total_fee>1total_fee>
   <trade_type>JSAPItrade_type>
   <sign>0CB01533B8C1EF103065174F50BCA001sign>
xml>

注:参数值用XML转义即可,CDATA标签用于说明数据不被XML解析器解析。

调用微信统一下单接口示例:

Controller:

    @ApiOperation(value = "创建JSAPI订单")
    @RequestMapping(value = "/order/create/jsapi", method = RequestMethod.GET)
    public JSONObject createJSAPI(
      @ApiParam(value = "openId", name = "openId") String openId,
      HttpServletRequest request) throws Exception {
      
        WechatUnifiedOrderRes wechatUnifiedOrderRes = wechatAppPayHandler.getJSAPIOrderRes(openId);
        logger.error("wechat createJSAPI wechatUnifiedOrderRes: {}", JSON.toJSONString(wechatUnifiedOrderRes));
        Map<String, String> res = new HashMap<>();
        res.put("payOrderId", payOrderId.toString());
        res.put("desc", payGoodsEnum.getGoodsName());
        String returnCode = wechatUnifiedOrderRes.getReturnCode();
        res.put("return_code", returnCode);
        res.put("return_msg", wechatUnifiedOrderRes.getReturnMsg().toString());
        if ("SUCCESS".equals(returnCode)) {
            res.put("nonce_str", wechatUnifiedOrderRes.getNonceStr());
            res.put("sign", wechatUnifiedOrderRes.getSign());
            String resultCode = wechatUnifiedOrderRes.getResultCode();
            res.put("result_code", resultCode);
            if ("SUCCESS".equals(resultCode)) {
                res.put("prepay_id", wechatUnifiedOrderRes.getPrepayId());
            } else {
                res.put("err_code", wechatUnifiedOrderRes.getErrCode());
                res.put("err_code_des", wechatUnifiedOrderRes.getErrCodeDes());
            }
        } else {
            logger.error("wechat createJSAPI wechatUnifiedOrderRes: {}", JSON.toJSONString(wechatUnifiedOrderRes));
            logger.error("wechat createJSAPI wechatUnifiedOrderRes.params: {}", JSON.toJSONString(wechatUnifiedOrderRes.getParams()));
            return Response.fail("创建JSAPI订单失败,调用微信返回结果失败");
        }
        return Response.succ(res);
    }

getJSAPIOrderRes:

    public WechatUnifiedOrderRes getJSAPIOrderRes(String openId) throws Exception {
        Map<String, Object> map = new TreeMap<>(String::compareTo);
        map.put("appid", wechatAppId);
        map.put("device_info", "WEB");
        map.put("trade_type", "JSAPI");
        map.put("body", mchName + "测试商品名称");
        map.put("mch_id", mchId);
        map.put("notify_url", notify_url);
        map.put("out_trade_no", "商户订单号");
        map.put("total_fee", "总金额");
        String spbillCreateIp = request.getRemoteAddr();
        map.put("spbill_create_ip", spbillCreateIp);
        String nonceStr = WXPayUtil.generateNonceStr();
        map.put("nonce_str", nonceStr);
				map.put("openid", openId);
        map.put("sign", WXPayUtil.generateSignature(map, "商户密钥"));
        String convertToXML = XMLUtils.convertToXML(map);
        logger.info("wechat jsapi unifiedorder convertToXML:{}", convertToXML);
        String result = httpClientInstance.post("https://api.mch.weixin.qq.com/pay/unifiedorder", convertToXML);
        logger.info("wechat jsapi unifiedorder content:{}", result);
        Map<String, Object> xmlResultMap = XMLUtils.XML2Map(result);
        logger.info("wechat jsapi unifiedorder xml:{}", xmlResultMap.toString());
        WechatUnifiedOrderRes res = new WechatUnifiedOrderRes(xmlResultMap);
        logger.info("wechat jsapi unifiedorder res:{}", JSON.toJSONString(res));
        return res;
    }

WXPayUtil:

package com.miniprogram.common.utils;

import com.miniprogram.common.pay.wechat.WechatUnifiedOrderReq;

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.StringWriter;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Map;
import java.util.Random;
import java.util.Set;

public class WXPayUtil {

    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private static final Random RANDOM = new SecureRandom();

    /**
     * 获取随机字符串 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);
    }

    /**
     * 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
     *
     * @param data 待签名数据
     * @param key  API密钥
     * @return 签名
     */
    public static String generateSignature(Map<String, Object> data, String key) 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(WechatUnifiedOrderReq.SIGN)) {
                continue;
            }
            if (data.get(k).toString().trim().length() > 0) {
                // 参数值为空,则不参与签名
                sb.append(k).append("=").append(data.get(k).toString().trim()).append("&");
            }
        }
        sb.append("key=").append(key);
        return MD5(sb.toString()).toUpperCase();
    }

    /**
     * 生成 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();
    }
}

XMLUtils:

package com.miniprogram.common.utils;

import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class XMLUtils {

    /**
     * @param map 考虑到微信中只有一层XML,因此不考虑MAP嵌套循环的情况
     * @return
     * @function 将输入的MAP转化为XML
     */
    public static String convertToXML(Map<String, Object> map) {
        Document document = DocumentHelper.createDocument();
        document.setXMLEncoding("UTF-8");
        Element root = document.addElement("xml");
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            root.addElement(entry.getKey()).addText(String.valueOf(entry.getValue()));
        }
        return document.asXML();
    }

    /**
     * @param xml 字符串类型的XML
     * @return
     * @function 将XML转化为Map
     */
    public static Map<String, Object> XML2Map(String xml) throws Exception {
        Document document = DocumentHelper.parseText(xml);
        return XML2Map(document);
    }

    /**
     * @param doc
     * @return
     * @function 将XML转化为Map
     */
    public static Map<String, Object> XML2Map(Document doc) {
        Map<String, Object> map = new HashMap<String, Object>();
        if (doc == null)
            return map;
        Element root = doc.getRootElement();
        for (Iterator iterator = root.elementIterator(); iterator.hasNext(); ) {
            Element e = (Element) iterator.next();
            //System.out.println(e.getName());
            List list = e.elements();
            if (list.size() > 0) {
                map.put(e.getName(), Dom2Map(e));
            } else
                map.put(e.getName(), e.getText());
        }
        return map;
    }

    /**
     * @param e
     * @return
     * @function 私有方法, 将某个Element转化为Map
     */
    @SuppressWarnings("unchecked")
    private static Map Dom2Map(Element e) {
        Map map = new HashMap();
        List list = e.elements();
        if (list.size() > 0) {
            for (int i = 0; i < list.size(); i++) {
                Element iter = (Element) list.get(i);
                List mapList = new ArrayList();

                if (iter.elements().size() > 0) {
                    Map m = Dom2Map(iter);
                    if (map.get(iter.getName()) != null) {
                        Object obj = map.get(iter.getName());
                        if (!obj.getClass().getName().equals("java.util.ArrayList")) {
                            mapList = new ArrayList();
                            mapList.add(obj);
                            mapList.add(m);
                        }
                        if (obj.getClass().getName().equals("java.util.ArrayList")) {
                            mapList = (List) obj;
                            mapList.add(m);
                        }
                        map.put(iter.getName(), mapList);
                    } else
                        map.put(iter.getName(), m);
                } else {
                    if (map.get(iter.getName()) != null) {
                        Object obj = map.get(iter.getName());
                        if (!obj.getClass().getName().equals("java.util.ArrayList")) {
                            mapList = new ArrayList();
                            mapList.add(obj);
                            mapList.add(iter.getText());
                        }
                        if (obj.getClass().getName().equals("java.util.ArrayList")) {
                            mapList = (List) obj;
                            mapList.add(iter.getText());
                        }
                        map.put(iter.getName(), mapList);
                    } else
                        map.put(iter.getName(), iter.getText());
                }
            }
        } else
            map.put(e.getName(), e.getText());
        return map;
    }
}

httpClientInstance.post:

public String post(String url, String entity) throws Exception {
        HttpPost httpPost = new HttpPost(url);
        httpPost.setHeader("Accept-Charset", "UTF-8");
        httpPost.addHeader("Content-Type", "application/json;");
        httpPost.setHeader("Accept", "application/json");
        httpPost.setEntity(new StringEntity(entity, "UTF-8"));
        return getResponseContent(url, httpPost);
    }

getResponseContent:

		@Autowired
    CloseableHttpClient httpClient;

private String getResponseContent(String url, HttpRequestBase request) throws Exception {
        HttpResponse response = null;
        try {
            response = httpClient.execute(request);
            return EntityUtils.toString(response.getEntity(), "UTF-8");
        } catch (Exception e) {
            throw new Exception("got an error from HTTP for url : " + URLDecoder.decode(url, "UTF-8"), e);
        } finally {
            if (response != null) {
                EntityUtils.consumeQuietly(response.getEntity());
            }
            request.releaseConnection();
        }
    }

常见错误处理:

1.调微信接口返回「签名错误」

​ 解决方案:所有传入需要加密的非空参数按照参数名ASCII码从小到大排序(字典序)

2.验签工具校验通过后,前端调微信支付仍然报「支付验证签名失败」:

​ 解决方案:微信接口返回的 签名(sign) 不能直接给h5,需要再次签名!!!

再次签名:paySign=MD5(appId=${appid}&nonceStr=${nonceStr}&package=prepay_id=${prepay_id}&signType=MD5&timeStamp=${timeStamp}&key=${key}).toString().toUpperCase();

你可能感兴趣的:(微信,java,后端)