三步搞定微信支付对接

772 阅读4分钟

一、统一下单:

要使用微信支付,需要先在后端调用统一下单接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易回话标识给前端后再按扫码/JSAPI/APP等不同场景生成交易串调起支付。
统一下单接口为:https://api.mch.weixin.qq.com/pay/unifiedorder
接口文档参见:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1

这个接口的一些必要参数如下:

字段名变量名描述
公众账号IDappid微信支付分配的公众账号ID(企业号corpid即为此appId)
商户号mch_id微信支付分配的商户号
随机字符串nonce_str随机字符串,长度要求在32位以内。
签名sign通过签名算法计算得出的签名值
商品描述body商品简单描述
商户订单号out_trade_no商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-*@ ,且在同一个商户号下唯一。
标价金额total_fee订单总金额,单位为分
终端IPspbill_create_ipAPP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。
通知地址notify_url异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
交易类型trade_type小程序取值如下:JSAPI
用户标识openidtrade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识

生成预支付交易单的代码如下:

public Map<String, String> miniPay(HttpRequestHeader header, OrderInfo order) {
	Map<String, String> body = new HashMap<>();
    body.put("appid", setting.getAppId());
    body.put("mch_id", setting.getMerchantId());
    body.put("nonce_str", UUID.randomUUID().toString().substring(0, 32));
    body.put("body", '商品订单');
    body.put("out_trade_no", order.getPayNo());
    body.put("total_fee", String.valueOf(order.getTransAmount().intValue()));
    body.put("spbill_create_ip", header.getIp());
    body.put("notify_url", setting.getPayNotifyUrl());
    body.put("trade_type", "JSAPI");
    body.put("openid", order.getOpenId());
	
	String sign = this.sign(setting, body);    
    String reqXml = this.buildXmlDoc(body, sign, Lists.newArrayList("body", "sign"));
	
	String respXml = httpCallableTemplate.postCall("https://api.mch.weixin.qq.com/pay/unifiedorder", reqXml, "utf8");
	
	Document respDoc = DocumentHelper.parseText(respXml);
    //请求状态码
    Element root = respDoc.getRootElement();
    String returnCode = root.element("return_code").getText();
    if (!"SUCCESS".equalsIgnoreCase(returnCode)) {
        //异常信息        
        throw new BusinessException("", String.format("微信小程序支付申请异常:[%s][%s]", returnCode, root.element("return_msg").getText()));
    }
	
	Map<String, String> payloadMap = new HashMap<>();
    payloadMap.put("appId", setting.getAppId());
    payloadMap.put("timeStamp", String.valueOf(new Date().getTime()).substring(0, 10));
    payloadMap.put("nonceStr", UUID.randomUUID().toString().substring(0, 32));
    payloadMap.put("package", "prepay_id=" + root.element("prepay_id").getText());
    payloadMap.put("signType", setting.getSignType());
	
	String paySign = this.sign(setting, payloadMap);
    payloadMap.put("paySign", paySign);
	
	return payloadMap;
}

payloadMap中的参数都是返回给前端用作调起微信支付用的,其中package参数将预支付交易单的prepay_id返回给前端。

上面的代码中涉及到工具类方法sign计算签名,buildXmlDoc根据字符串生成XML格式的字符串,httpCallableTemplate.postCall用于进行Post请求。其代码如下:

String sign(CallableConfigProperties.ApiConfig setting, Map<String, String> input) throws Exception {
    //ASCII排序
    SortedMap<String, String> sortedMap = new TreeMap<>(input);
    /**
     * 签名数据
     * 拼接key
     */
    String signed = HttpCallableTemplate.joinFormEntity(sortedMap, true);
    signed = signed + "&key=" + setting.getAppKey();

    System.out.println("签名字符:" + signed);
    //MD5签名
    try {
        MessageDigest md = MessageDigest.getInstance("MD5");
        return HexUtils.toHexString(md.digest(signed.getBytes())).toUpperCase();
    } catch (NoSuchAlgorithmException e) {
        throw new BusinessException("", "签名异常");
    }
}

public String buildXmlDoc(Map<String, String> body, String sign, List<String> CDATAFields) {
    Document reqDoc = DocumentHelper.createDocument();
    Element root = reqDoc.addElement("xml");
    Iterator<Map.Entry<String, String>> iterator = body.entrySet().iterator();
    while (iterator.hasNext()) {
        Map.Entry<String, String> entry = iterator.next();
        if (StringUtils.isEmpty(entry.getKey()) || StringUtils.isEmpty(entry.getValue())) {
            continue;
        }
        //XML  CDATA 格式
        if (!CollectionUtils.isEmpty(CDATAFields) && CDATAFields.contains(entry.getKey())) {
            root.addElement(entry.getKey()).addCDATA(String.valueOf(entry.getValue()));
            continue;
        }
        //默认格式
        root.addElement(entry.getKey()).setText(String.valueOf(entry.getValue()));
    }
    if (CDATAFields != null && CDATAFields.contains("sign")) {
        root.addElement("sign").addCDATA(sign);
    } else {
        root.addElement("sign").setText(sign);
    }
    reqDoc.setRootElement(root);
    return reqDoc.asXML();
}

public String postCall(String url, String body, String charset) {
    StringEntity formEntity = new StringEntity(body, charset);

    HttpPost httpPost = new HttpPost(url);
    httpPost.setHeader("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE);
    httpPost.setEntity(formEntity);

    CloseableHttpResponse response = null;
    try {
        response = httpClient.execute(httpPost);
        if (response.getStatusLine().getStatusCode() != HttpStatus.OK.value()) {
            throw new BusinessException("", response.getStatusLine().toString());
        }
        /**
         * 处理响应 emq返回的code为0 表示成功,映射为侧接口响应码为200
         */
        HttpEntity httpEntity = response.getEntity();
        return EntityUtils.toString(httpEntity, charset);
    } catch (Exception e) {
        logger.warn(e.getMessage(), e);
        throw new BusinessException("", "调用接口异常:" + e.getMessage());
    } finally {
        /**
         * 释放连接
         */
        try {
            if (response != null) {
                response.close();
            }
        } catch (Exception e) {
            logger.warn("释放连接失败:{}", e.getMessage());
        }
    }
}

二、发起微信支付:

发起微信支付需要调用wx.requestPayment,文档资料地址为https://developers.weixin.qq.com/miniprogram/dev/api/payment/wx.requestPayment.html

其参数如下:

字段名类型描述
timeStampstring时间戳,从 1970 年 1 月 1 日 00:00:00 至今的秒数,即当前的时间
nonceStrstring随机字符串,长度为32个字符以下
packagestring统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=***
signTypestring签名算法,应与后台下单时的值一致(其中MD5和HMAC-SHA256加密算法仅在 v2 版本接口适用,RSA加密算法仅在 v3 版本接口适用)
paySignstring签名
successfunction接口调用成功的回调函数
failfunction接口调用失败的回调函数
completefunction接口调用结束的回调函数(调用成功、失败都会执行)

示例代码如下:

//其中res为上一步预支付交易单生成接口返回的结果
wx.requestPayment({
  timeStamp: res.timeStamp,
  nonceStr: res.nonceStr,
  package: res.package,
  signType: res.signType,
  paySign: res.paySign,
  success (re) { 
  	// 支付成功后的回调函数
  },
  fail (re) {
  	// 支付失败或取消支付后的回调函数
  }
})

三、支付回调:

比如上面预支付交易单生成接口指定了回调URL地址(notify_url)为https://api.xxx.com/gateway/wxNotify/action/mini/payComplete
由于这个URL是要被微信支付调用的,因此他必须能被外网访问到。下面我们实现这个接口:

@ApiOperation("微信支付结果通知")
@PostMapping("wxNotify/action/mini/payComplete")
public ResponseEntity payComplete(@PathVariable("appId") String appId,
                                  @PathVariable("scene") String scene,
                                  HttpServletRequest request) {    
    return payService.complete(appId, request);
}

public void complete(String appId, HttpServletRequest request) {
	/**
     * 解析通知内容
     */
    String noticeBody = StreamUtils.copyToString(request.getInputStream(), "utf8");
    logger.info("微信支付回调:{}", noticeBody);
	    
    ByteArrayInputStream in = new ByteArrayInputStream(noticeBody.getBytes(StandardCharsets.UTF_8));
    Document notifyDoc = this.saxReader().read(in);
    Element root = notifyDoc.getRootElement();
    String returnCode = root.element("return_code").getText();
    if (!"SUCCESS".equalsIgnoreCase(returnCode)) {
        throw new BusinessException("", String.format("微信支付通知报文数据异常:[%s][%s]", returnCode, root.element("return_msg").getText()));
    }
	in.close();
	
	String merchantId = notifyDoc.getRootElement().element("mch_id").getText();
	String payNo = root.element("out_trade_no").getText();
	.......根据回调通知内容进行具体业务处理...........
}

支付结果通知返回的具体信息可以参见文档:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_7&index=8