Java 微信支付

1,699 阅读4分钟

微信支付开发文档 (V2版)

详细请求参数与返回参数请参考: 微信支付接口文档

/**
 * <p>
 * 便于使用,将所有的工具方法都集中在此,包含:
 * 1. 执行 HTTP POST 请求,返回执行结果的 String
 * 2. 创建签名(为下单数据创建)
 * 3. 创建签名(为 APP 创建)
 * 4. 检验签名
 * 5. 读取 HTTP Request 内容
 * 6. 读取 HTTP Response 内容
 * 7. 将 Map 转化为 Xml
 * 8. 将 Xml 转化为 Map
 * 9. 生成 32 位随机字符串
 * 10. MD5 签名
 */
public class WxUtils {

    //APPID  微信开放平台 appid
    public static final String APPID = "xxx";

    //MCH_ID 微信商户平台  商户 mch_id
    public static final String MCH_ID = "xxx";

    //NOTIFY_URL  回调通知地址(接收微信支付异步通知回调地址,通知url必须为直接可访问的url,不能携带参数。)
    public static final String NOTIFY_URL = "xxx";
   
    //API_KEY 微信商户平台密钥
    public static String API_KEY = "xxx";

    // 下单 API 地址
    public static final String PLACE_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";


    /**
     * 1.请求方式
     * @param requestUrl
     * @param outputStr
     * @return
     */
    public static String httpsRequest(String requestUrl,String outputStr) {
        try {
            URL url = new URL(requestUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();

            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            // 设置请求方式(GET/POST)
            conn.setRequestMethod("POST");
            //conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
            conn.setRequestProperty("content-type", "text/xml;charset=utf-8");
            // 当outputStr不为null时向输出流写数据
            if (null != outputStr) {
                OutputStream outputStream = conn.getOutputStream();
                // 注意编码格式
                outputStream.write(outputStr.getBytes("UTF-8"));
                outputStream.close();
            }
            // 从输入流读取返回内容
            InputStream inputStream = conn.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String str = null;
            StringBuilder buffer = new StringBuilder();
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
            }
            // 释放资源
            bufferedReader.close();
            inputStreamReader.close();
            inputStream.close();
            conn.disconnect();
            return buffer.toString();
        } catch (ConnectException ce) {
            System.out.println("连接超时:{}"+ ce);
        } catch (Exception e) {
            System.out.println("https请求异常:{}"+ e);
        }
        return null;
    }

    /**  2
     * 第一次签名
     *
     * @param parameters 数据为服务器生成,下单时必须的字段排序签名
     * @param key
     * @return
     */
    public static String createSign(SortedMap<String, Object> parameters, String key) {
        StringBuffer sb = new StringBuffer();
        Set es = parameters.entrySet();//所有参与传参的参数按照accsii排序(升序)
        Iterator it = es.iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry) it.next();
            String k = (String) entry.getKey();
            Object v = entry.getValue();
            if (null != v && !"".equals(v)
                    && !"sign".equals(k) && !"key".equals(k)) {
                sb.append(k + "=" + v + "&");
            }
        }
        sb.append("key=" + key);
        return encodeMD5(sb.toString());
    }

    /** 3
     * 第二次签名
     *
     * @param result 数据为微信返回给服务器的数据(XML 的 String),再次签名后传回给客户端(APP)使用
     * @param key    密钥
     * @return
     * @throws IOException
     */
    public static Map createSign2(String result, String key) throws IOException {
        SortedMap<String, Object> map = new TreeMap<>(transferXmlToMap(result));
        String returnCode = (String)map.get("return_code");
        if(StringUtils.equalsIgnoreCase("success",returnCode)){
            String resultCode = (String)map.get("result_code");
            Map app = new HashMap();
            app.put("appid", map.get("appid"));//应用ID
            app.put("partnerid", map.get("mch_id"));//商户号
            app.put("prepayid", map.get("prepay_id"));//预支付交易会话ID
            app.put("package", "Sign=WXPay");// 固定字段,保留,不可修改
            app.put("noncestr", map.get("nonce_str"));//随机字符串
            app.put("timestamp", new Date().getTime() / 1000);  //时间戳  时间为秒,JDK 生成的是毫秒,故除以 1000
            app.put("sign", createSign(new TreeMap<>(app), key));//签名
            if(StringUtils.equalsIgnoreCase("success",resultCode)){
                app.put("tradestate",map.get("trade_state"));
            }
            System.out.println(app+"-------------");
            return app;
        }
        return null;
    }

    /** 4
     * 验证签名是否正确
     *
     * @return boolean
     * @throws Exception
     */
    public static boolean checkSign(SortedMap<String, Object> parameters, String key) throws Exception {
        String signWx = parameters.get("sign").toString();
        if (signWx == null) return false;
        parameters.remove("sign"); // 需要去掉原 map 中包含的 sign 字段再进行签名
        String signMe = createSign(parameters, key);
        return signWx.equals(signMe);
    }

    /**  5
     * 读取 request body 内容作为字符串
     *
     * @param request
     * @return
     * @throws IOException
     */
    public static String readRequest(HttpServletRequest request) throws IOException {
        InputStream inputStream;
        StringBuffer sb = new StringBuffer();
        inputStream = request.getInputStream();
        String str;
        BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
        while ((str = in.readLine()) != null) {
            sb.append(str);
        }
        in.close();
        inputStream.close();
        return sb.toString();
    }

    /**  6
     * 读取 response body 内容为字符串
     */
    public static String readResponse(HttpResponse response) throws IOException {
        BufferedReader in = new BufferedReader(
                new InputStreamReader(response.getEntity().getContent()));
        String result = new String();
        String line;
        while ((line = in.readLine()) != null) {
            result += line;
        }
        return result;
    }

    /**  7
     * 将 Map 转化为 XML
     *
     * @param map
     * @return
     */
    public static String transferMapToXml(SortedMap<String, Object> map) {
        StringBuffer sb = new StringBuffer();
        sb.append("<xml>");
        for (String key : map.keySet()) {
            sb.append("<").append(key).append(">")
                    .append(map.get(key))
                    .append("</").append(key).append(">");
        }
        return sb.append("</xml>").toString();
    }


    /**  8
     * 将 XML 转化为 map
     *
     * @param strxml
     * @return
     * @throws JDOMException
     * @throws IOException
     */
    public static Map transferXmlToMap(String strxml) throws IOException {
        strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
        if (null == strxml || "".equals(strxml)) {
            return null;
        }
        Map m = new HashMap();
        InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
        SAXBuilder builder = new SAXBuilder();
        Document doc = null;
        try {
            doc = builder.build(in);
        } catch (JDOMException e) {
            throw new IOException(e.getMessage()); // 统一转化为 IO 异常输出
        }
        // 解析 DOM
        Element root = doc.getRootElement();
        List list = root.getChildren();
        Iterator it = list.iterator();
        while (it.hasNext()) {
            Element e = (Element) it.next();
            String k = e.getName();
            String v = "";
            List children = e.getChildren();
            if (children.isEmpty()) {
                v = e.getTextNormalize();
            } else {
                v = getChildrenText(children);
            }
            m.put(k, v);
        }
        //关闭流
        in.close();
        return m;
    }

    /**  9
     * 辅助 transferXmlToMap 方法递归提取子节点数据
      * @param children
     * @return
     */
    private static String getChildrenText(List<Element> children) {
        StringBuffer sb = new StringBuffer();
        if (!children.isEmpty()) {
            Iterator<Element> it = children.iterator();
            while (it.hasNext()) {
                Element e = (Element) it.next();
                String name = e.getName();
                String value = e.getTextNormalize();
                List<Element> list = e.getChildren();
                sb.append("<" + name + ">");
                if (!list.isEmpty()) {
                    sb.append(getChildrenText(list));
                }
                sb.append(value);
                sb.append("</" + name + ">");
            }
        }
        return sb.toString();
    }


    /**  10
     * 生成 32 位随机字符串,包含:数字、字母大小写
     *
     * @return
     */
    public static String gen32RandomString() {
        char[] dict = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
                'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
                'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
        StringBuffer sb = new StringBuffer();
        Random random = new Random();
        for (int i = 0; i < 32; i++) {
            sb.append(String.valueOf(dict[(int) (Math.random() * 62)]));
        }
        return sb.toString().toUpperCase();
    }


    /**  11
     * MD5 签名
     *
     * @param str
     * @return 签名后的字符串信息
     */
    public static String encodeMD5(String str) {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            byte[] inputByteArray = (str).getBytes();
            messageDigest.update(inputByteArray);
            byte[] resultByteArray = messageDigest.digest();
            return byteArrayToHex(resultByteArray);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    /**  12
     * 辅助 encodeMD5 方法实现
     * @param byteArray
     * @return
     */
    private static String byteArrayToHex(byte[] byteArray) {
        char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        char[] resultCharArray = new char[byteArray.length * 2];
        int index = 0;
        for (byte b : byteArray) {
            resultCharArray[index++] = hexDigits[b >>> 4 & 0xf];
            resultCharArray[index++] = hexDigits[b & 0xf];
        }
        // 字符数组组合成字符串返回
        return new String(resultCharArray);
    }
}
package com.xp.service.order.impl;

@Service
public class WXPayServiceImpl implements WXPayService {

    @Override
    public Map pay(List<OrderInfo> list, Integer userId, List<UserCoupon>couponList,String ipAddress){
        try {
            SortedMap<String, Object> parameters = new TreeMap<>();
            parameters.put("appid", WxUtils.APPID);//应用ID
            parameters.put("body", "微信支付");//商品描述
            parameters.put("mch_id", WxUtils.MCH_ID);//商户号
            parameters.put("nonce_str", WxUtils.gen32RandomString());//随机字符串 不长于32位
            parameters.put("notify_url", WxUtils.NOTIFY_URL);//通知地址
            //parameters.put("device_info", "WEB"); //设备号  默认"WEB"
            parameters.put("out_trade_no", "唯一订单号");//商户订单号
            parameters.put("spbill_create_ip", ipAddress);//终端IP
            parameters.put("total_fee", "0.01"); // 总金额
            parameters.put("trade_type", "APP");//交易类型
            parameters.put("sign", WxUtils.createSign(parameters,WxUtils. API_KEY)); //签名 sign 必须在最后
            String requestXML = WxUtils.transferMapToXml(parameters);
            String result = WxUtils.httpsRequest(WxUtils.PLACE_URL, requestXML); // 执行 HTTP 请求,获取接收的字
            return WxUtils.createSign2(result, WxUtils.API_KEY);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public String notify(HttpServletRequest request, HttpServletResponse response){
        try {
            // 预先设定返回的 response 类型为 xml
            response.setHeader("Content-type", "application/xml");
            // 读取参数,解析Xml为map
            Map<String, String> map = WxUtils.transferXmlToMap(WxUtils.readRequest(request));
            // 转换为有序 map,判断签名是否正确
            boolean isSignSuccess = WxUtils.checkSign(new TreeMap<String, Object>(map), WxUtils.API_KEY);
            if (isSignSuccess) {
                // 签名校验成功,说明是微信服务器发出的数据
                //String orderId = map.get("out_trade_no");
                //if (tradeService.hasProcessed(orderId)) // 判断该订单是否已经被接收处理过
                    //return success();
                // 可在此持久化微信传回的该 map 数据
                if (map.get("return_code").equals("SUCCESS")) {
                    if (map.get("result_code").equals("SUCCESS")) {
                        /**
                        *回调成功之后处理业务逻辑(比如,更新订单状态,增加积分,通知仓库发货等等。)
                        */

                        return "SUCCESS";
                    } else {
                        return "FAIL";
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "FAIL";
    }
}

现在微信支付已经出现了 3.0 的文档版本,实现起来更加简单方便。微信支付开发文档 3.0 全新发布

但是,以上 v2 版的依然可用,已经亲测试过,完全 OK。