业务场景:基本上做业务的话,也是逃不开对接各种支付接口的,比如数字人民币支付、农行免密支付、支付宝支付、微信支付等等。在着手开发时候,也是遇到不少阻力,微信官方提供的接口文档很散乱,如果之前没接触过,一上来就来搞微信支付,即使参考很多文档、材料,也是很令人抓狂的。在这篇文章中,主要记录的就是对接微信支付的流程,以及开发中遇到的问题。

一、了解支付流程

商户自行申请入驻微信支付,无服务商协助。(商户平台申请)成为直连商户

申请APPID

申请mchid

绑定APPID及mchid

配置API key

(这些是需要准备的参数)

二、开发流程

2.1 添加依赖

<dependency>
        <groupId>com.github.liyiorg</groupId>
        <artifactId>weixin-popular</artifactId>
        <version>2.8.5</version>
    </dependency>

2.2 建配置类:WxPayConfig

public class WxPayConfig {

    /**   APPID */
    public static final String APPID = "";

    /** 商户号 */
    public static final String MCH_ID = "";

    /** 密钥 */
    public static final String PRIVATE_KEY = "";

    /**AppSecret 微信开放平台 对应app的开发密码,app支付不需要 */
    public static final String APPSECRET = "";

    /** 用户订单支付结果异步通知url */
    public static final String NOTIFY_URL = "";

}

2.3 建工具类:WxPayConfig

import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
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.HttpClients;
import org.apache.http.util.EntityUtils;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
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.net.InetAddress;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.util.*;


public class WXPayUtil {

    /**
     * 生成微信支付sign
     * @return
     */
    public static String createSign(SortedMap<String, String> params, String key){
        StringBuilder sb = new StringBuilder();
        Set<Map.Entry<String, String>> es =  params.entrySet();
        Iterator<Map.Entry<String, String>> it =  es.iterator();
        //生成 stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
        while (it.hasNext()){
            Map.Entry<String, String> entry = (Map.Entry<String, String>)it.next();
            String k = (String)entry.getKey();
            String v = (String)entry.getValue();
            if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)){
                sb.append(k+"="+v+"&");
            }
        }
        sb.append("key=").append(key);
        String sign = MD5(sb.toString()).toUpperCase();
        return sign;
    }

    /**
     * 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>();
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = documentBuilderFactory.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) {

            }
            return data;
        } catch (Exception ex) {
            throw ex;
        }
    }

    /**
     * 将Map转换为XML格式的字符串
     * @param data Map类型数据
     * @return XML格式的字符串
     * @throws Exception
     */
    public static String mapToXml(Map<String, String> data) throws Exception {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
        org.w3c.dom.Document document = documentBuilder.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;
    }



    /**
     * 生成uuid,即用来标识一笔单,也用做 微信支付的nonce_str
     * @return
     */
    public static String generateUUID(){
        String uuid = UUID.randomUUID().toString().
                replaceAll("-","").substring(0,32);
        return uuid;
    }


    /**
     * MD
     * @param data
     * @return
     */
    public static String MD5(String data){
        try {

            MessageDigest md5 = MessageDigest.getInstance("MD5");
            byte [] array = md5.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();

        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取用户请求ip
     * @param request
     * @return
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress = request.getHeader("x-forwarded-for");
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
            if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
                //根据网卡取本机配置的IP
                InetAddress inet = null;
                try {
                    inet = InetAddress.getLocalHost();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
                ipAddress = inet.getHostAddress();
            }
        }
        //对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
        if (ipAddress != null && ipAddress.length() > 15) { //"***.***.***.***".length() = 15
            if (ipAddress.indexOf(",") > 0) {
                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
            }
        }
        return ipAddress;
    }


    /**
     * 封装post,返回请求的结果
     * @return
     */
    public static String doPost(String url, String data, int timeout){
        CloseableHttpClient httpClient =  HttpClients.createDefault();
        //超时设置
        RequestConfig requestConfig =  RequestConfig.custom().setConnectTimeout(timeout) //连接超时
                .setConnectionRequestTimeout(timeout)//请求超时
                .setSocketTimeout(timeout)
                .setRedirectsEnabled(true)  //允许自动重定向
                .build();
        HttpPost httpPost  = new HttpPost(url);
        httpPost.setConfig(requestConfig);
        httpPost.addHeader("Content-Type","application/json;charset=utf-8");
        if(data != null && data instanceof String){ //使用字符串传参
            StringEntity stringEntity = new StringEntity(data,"UTF-8");
            httpPost.setEntity(stringEntity);
        }
        try{
            CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
            HttpEntity httpEntity = httpResponse.getEntity();
            if(httpResponse.getStatusLine().getStatusCode() == 200){
                String result = EntityUtils.toString(httpEntity, "UTF-8");
                return result;
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try{
                httpClient.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return null;
    }
    
}

2.4 建控制层:WxController

/**
     * 微信app支付
     * @param
     * @return
     * @throws Exception
     */
    @PostMapping("/WXPayment")
    ResultMsg<Object> WXPayment(@RequestBody OrderVO orderVO) throws Exception {
        ResultMsg<Object> resultMsg = new ResultMsg<>();
        try {
            resultMsg = wxService.WXPayment(orderVO);
        } catch (Exception e) {
            e.printStackTrace();
            resultMsg.setSuccess(false);
            resultMsg.setResultMsg("系统异常,支付失败!");
            return resultMsg;
        }
        return resultMsg;
    }

    /**
     * 微信支付回调
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @PostMapping(value = "/wechatPay/callback")
    public String wechatPayCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {
        return wxService.wechatPayCallback(request, response);
    }

2.4 service实现类:WxServiceImpl

/***
     * 微信预支付接口
     * @param orderVO
     * @return
     */
 @Override
    public ResultMsg<Object> WXPayment(OrderVO orderVO) {
        ResultMsg<Object> resultMsg = new ResultMsg<Object>();

        if (StringUtils.isBlank(orderVO.getUserId()) ||
                StringUtils.isBlank(orderVO.getOrderId())||
                StringUtils.isBlank(orderVO.getType())||
                StringUtils.isBlank(orderVO.getTotalPrice())
        ) {
            resultMsg.setSuccess(false);
            resultMsg.setResultMsg("信息不完整!");
            return resultMsg;

        }

        Unifiedorder unifiedorder = new Unifiedorder();
        unifiedorder.setAppid(WxPayConfig.APPID);
        unifiedorder.setMch_id(WxPayConfig.MCH_ID);
        unifiedorder.setNonce_str(UUID.randomUUID().toString().replaceAll("-", ""));
        unifiedorder.setBody(orderVO.getOrderId());//订单id 拉起后,在微信页面上方显示的数据
        unifiedorder.setOut_trade_no(orderVO.getOrderId());//订单id
        String receivableAmount = orderVO.getTotalPrice();//自己业务的支付总价,但是微信支付接口以分为单位
        String money = new BigDecimal(receivableAmount).multiply(new BigDecimal("100")).stripTrailingZeros().toPlainString();
        String fee = money;
        unifiedorder.setTotal_fee(fee); // 订单总金额,单位为分;
        unifiedorder.setSpbill_create_ip("127.0.0.1");
        unifiedorder.setNotify_url(WxPayConfig.NOTIFY_URL);//回调接口地址,用来接收微信通知,以及处理我们自己的业务逻辑
        unifiedorder.setTrade_type("APP");
        unifiedorder.setAttach(orderVO.getType());//附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用,实际情况下只有支付完成状态才会返回该字段。示例值:自定义数据 说白了就是前端调微信拉起预支付接口的某些参数,需要我们在回调接口处理业务逻辑使用

        log.info("微信APP支付--(签名前):" + XMLConverUtil.convertToXML(unifiedorder));

        /** 获取签名 */
        UnifiedorderResult unifiedorderResult = PayMchAPI.payUnifiedorder(unifiedorder, WxPayConfig.PRIVATE_KEY);

        log.info("微信APP支付--支付统一下单接口请求状态(return_code):" + unifiedorderResult.getReturn_code());
        log.info("微信APP支付--支付统一下单接口请求状态(return_msg):" + unifiedorderResult.getReturn_msg());
        log.info("微信APP支付--支付统一下单接口请求状态(result_code):" + unifiedorderResult.getResult_code());
        log.info("微信APP支付--支付请求参数封装(签名后):" + XMLConverUtil.convertToXML(unifiedorder));
        log.info("微信APP支付--支付统一下单接口返回数据:" + JsonUtil.toJson(unifiedorderResult));
        // 下单结果验签;
        if (unifiedorderResult.getSign_status() != null && unifiedorderResult.getSign_status()) {
            log.info("微信APP支付验签成功");
            MchPayApp generateMchAppData = PayUtil.generateMchAppData(unifiedorderResult.getPrepay_id(), WxPayConfig.APPID, WxPayConfig.MCH_ID,
                    WxPayConfig.PRIVATE_KEY);
            Map<String, Object> map = new HashMap<>();
            map.put("payOrderId", orderVO.getOrderId());//订单id
            map.put("mchPayApp", generateMchAppData);
            log.info(" WXPayment  return map" + map);
            resultMsg.setData(map);
            resultMsg.setSuccess(true);

            return resultMsg;
        }
        return resultMsg;
    }



    /***
     * 微信回调接口
     * @return
     */
    @Override
    public String wechatPayCallback(HttpServletRequest request, HttpServletResponse response) {
        ResultMsg<Object> resultMsg = new ResultMsg<>();
        // 解析微信支付异步通知请求参数;
        String xml = null;
        try {
            xml = StreamUtils.copyToString(request.getInputStream(), Charset.forName("utf-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        Map<String, String> params = XMLConverUtil.convertToMap(xml);
        MchPayNotify payNotify = XMLConverUtil.convertToObject(MchPayNotify.class, xml);


        /** 打印日志信息 */
        log.info("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$---进入微信支付异步通知请求接口---$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$");
        log.info("微信支付用户订单支付结果异步通知请求参数(xml):" + params);
        log.info("微信支付用户订单支付结果异步通知请求参数(map):" + params);
        log.info("return_code:" + payNotify.getReturn_code());
        log.info("return_msg:" + params.get("return_msg"));
        String out_trade_no = payNotify.getOut_trade_no();  // 用户订单号;
        String money = String.valueOf(payNotify.getTotal_fee());//支付金额
        String total_amount = new BigDecimal(money).multiply(new BigDecimal("0.01")).stripTrailingZeros().toPlainString();//单位换算成元
        String trade_no = payNotify.getTransaction_id();//微信交易号
        //CampOrder campOrder = campDao.selectOrderDetail(orderId);
        /** 校验支付成功还是失败 */
        if ("SUCCESS".equals(payNotify.getReturn_code())) {
            /** 获取微信后台返回来的异步通知参数 */
            String tradeNo = payNotify.getTransaction_id();   // 微信交易号;
            String tradeStatus = payNotify.getResult_code();  // 微信支付状态;
            Integer totalFee = payNotify.getTotal_fee();      // 支付金额 (单位:分)
           String  subject = payNotify.getAttach(); // 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用,实际情况下只有支付完成状态才会返回该字段。示例值:自定义数据
            boolean flag = SignatureUtil.validateSign(params, WxPayConfig.PRIVATE_KEY);
            //返回结果return_code 和result_code都为SUCCESS的时候才算成功
            if (flag && "SUCCESS".equals(tradeStatus)) {
                /** 更新订单支付信息: */

                log.info("********************** 支付成功(微信异步通知) **********************");
                log.info("* 订单号: " + out_trade_no);
                log.info("* 微信交易号: " + trade_no);
                log.info("* 实付金额: " + total_amount);
                log.info("***************************************************************");

				/************         愣着干嘛,处理业务啊            ************/

                log.info("微信支付成功...");

                /** 封装通知信息 */
                MchBaseResult baseResult = new MchBaseResult();
                baseResult.setReturn_code("SUCCESS");
                baseResult.setReturn_msg("OK");
                xml = XMLConverUtil.convertToXML(baseResult);
            } else {
                MchBaseResult baseResult = new MchBaseResult();
                baseResult.setReturn_code("FAIL");
                baseResult.setReturn_msg("FAIL");
                xml = XMLConverUtil.convertToXML(baseResult);
                //TODO 支付失败;逻辑
            }
        } else {
            MchBaseResult baseResult = new MchBaseResult();
            baseResult.setReturn_code("FAIL");
            baseResult.setReturn_msg("FAIL");
            xml = XMLConverUtil.convertToXML(baseResult);
        }
        log.info("微信支付--APP支付方式支付用户订单异步通知返回数据:" + xml);
        return xml;
    }