Java后台实现小程序微信支付

本人第一篇博客,之前笔记一直放在有道云,想写博客很久了,一直没下定决心,也没静下心好好写,今天突然跟朋友谈 到平时工作根本没时间学习、整理、总结balabala,工作中遇到的问题很多会经常遇到,不整理总结第二次碰到又要半天,就这么扯吧扯吧,扯完之后,不知道哪来的决心,就下手了,哈哈,废话不多说,进入正题啦,有不对的或者更好的建议,希望各位看官指点指点小白~

1、 前述

首先还是要来看下微信官方的api文档,官网的开发步骤以及流程,稍后我会把自己代码的步骤和流程简述,这里就不一一贴图出来,链接献上:微信支付官方文档

2、步骤如下:

组装统一下单参数
按照参数名ASCII码从小到大排序(字典序)
第一次签名->拼	接XML
发起统一下单请求
解析统一下单返回的结果(xml形式)
(支付成功)将解析得到的预付单信息拼接,再次签名
返回前端签名字符串

3、后台代码:

public Map<String, Object> weixinPrepay(FirstSignDto data, HttpServletRequest request) {
     	//CommonSystemConfig为配置类
        CommonSystemConfig commonSystemConfig = SysConfigUtils.get(CommonSystemConfig.class);
        try {
            //准备下单所需要的参数
            String noStr = IdGenerator.getRandomString(32);//生成随机的字符串
            String notifyUrl = commonSystemConfig.getNotifyUrl(); //回调地址,注意这个地址不要被拦截
            String appId = commonSystemConfig.getAppId();//小程序appid
            String tradeType = commonSystemConfig.getTradeType();//类型 JSAPI
            String merchantNo = commonSystemConfig.getMerchantNo();//商户号
            String key = commonSystemConfig.getKey();//支付密钥,在商户那里可以获取

            String orderNo = data.getOrderNo();//业务订单号
            Double tradeMoney = data.getTradeMoney();//交易金额
            String openid = data.getOpenid();//openid

            logger.info("---- 订单 {} 预支付 ----", orderNo);
	        //验证业务订单号是否合法
            Order order =orderService.selectOne(new EntityWrapper<Order>().eq("order_no", orderNo));

            if (ParamUtil.isNullOrEmptyOrZero(order)) {
                LeaseException.throwSystemException(LeaseExceEnums.ORDER_EXCEPTION);
            }
            //校验该订单的支付金额跟前端传过来的金额是否一致
            if (order.getTradeMoney() == tradeMoney) {
                LeaseException.throwSystemException(LeaseExceEnums.ORDER_EXCEPTION);
            }
            //获取终端的ip
            String ipAddr = WxUtil.getIpAddr(request);
            //页面传来的交易金额单位是 元,转换成 分
            String fee = String.valueOf((int) (tradeMoney * 100));

            //微信支付必须大于0.01,所以这里个人做了对应业务处理,根据各自需求来处理
            if (tradeMoney == 0.00) {
         	    .....................
            }

            String bodyStr = new String("WashinFUN".getBytes("UTF-8"));

            //组装参数,生成下单所需要的参数map进行第一次签名
            Map<String, String> packageParams = new HashMap<>();
            packageParams.put("appid", appId);
            packageParams.put("mch_id", merchantNo);
            packageParams.put("nonce_str", noStr);
            packageParams.put("notify_url", notifyUrl);
            packageParams.put("out_trade_no", orderNo);
            packageParams.put("spbill_create_ip", ipAddr);
            packageParams.put("total_fee", fee);
            packageParams.put("trade_type", tradeType);
            packageParams.put("openid", openid);
            packageParams.put("body", bodyStr);
            //所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串,用来签名
            String preStr = PayUtils.createLinkString(packageParams);
            
            logger.info("微信支付preStr:{}", preStr);

            //第一次签名
            String sign = PayUtils.sign(preStr, key, "utf-8").toUpperCase();

            logger.info("微信支付签名sign:{}", sign);
            //拼接xml
            String xml = "<xml>" + "<appid>" + appId + "</appid>"
                    + "<body>" + bodyStr + "</body>"
                    + "<mch_id>" + merchantNo + "</mch_id>"
                    + "<nonce_str>" + noStr + "</nonce_str>"
                    + "<notify_url>" + notifyUrl + "</notify_url>"
                    + "<openid>" + openid + "</openid>"
                    + "<out_trade_no>" + orderNo + "</out_trade_no>"
                    + "<spbill_create_ip>" + ipAddr + "</spbill_create_ip>"
                    + "<total_fee>" + fee + "</total_fee>"
                    + "<trade_type>" + tradeType + "</trade_type>"
                    + "<sign>" + sign + "</sign>"
                    + "</xml>";

            logger.info("统一下单接口 XML数据:{}", xml);

            String result = PayUtils.httpRequest(PAY_URL, "POST", xml);

            logger.info("统一下单结果:{}", result);
	        //解析xml
            Map map = PayUtils.doXMLParse(result);
            String return_code = (String) map.get("return_code");//返回状态码

            Map<String, Object> response = new HashMap<String, Object>();//返回给小程序端需要的参数map
            if (return_code.equals("SUCCESS")) {
                String prepay_id = (String) map.get("prepay_id");//返回的预付单信息
                response.put("nonceStr", noStr);
                response.put("package", "prepay_id=" + prepay_id);
                
                Long timeStamp = System.currentTimeMillis() / 1000;
                response.put("timeStamp", timeStamp + "");//这边要将返回的时间戳转化成字符串,不然小程序端调用wx.requestPayment方法会报签名错误

                //拼接签名需要的参数
                String stringSignTemp = "appId=" + appId + "&nonceStr=" + noStr + "&package=prepay_id=" + prepay_id + "&signType=MD5&timeStamp=" + timeStamp;
                //第二次签名,这个签名的结果用于小程序调用接口(wx.requesetPayment)
                String paySign = PayUtils.sign(stringSignTemp, key, "utf-8").toUpperCase();

                Order order = orderService.selectOne(new EntityWrapper<Order>().eq("order_no", orderNo));
                if (!ParamUtil.isNullOrEmptyOrZero(order)) {
                    Date tradeTime = new Date();
                    order.setTradeTime(tradeTime);
                    order.setTradeScene(commonSystemConfig.getTradeType());
                    orderService.updateById(order);
                    logger.info("支付成功,设置订单表 {} 的交易时间 {} 交易场景 {} :", orderNo, tradeTime, tradeType);
                }
                response.put("paySign", paySign);
            }
            response.put("appid", appId);
            response.put("bypassPayStatus", 0);//这是业务需要所返回的字段
            return response;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

PayUtils

public class PayUtils {
    /**
     * 签名字符串
     *
     * @param
     * @param key 密钥
     * @param
     * @return 签名结果
     */
    public static String sign(String text, String key, String input_charset) {
        text = text + "&key=" + key;
        return DigestUtils.md5Hex(getContentBytes(text, input_charset));
    }

    public static String signature(Map<String, String> map, String key) {
        Set<String> keySet = map.keySet();
        String[] str = new String[map.size()];
        StringBuilder tmp = new StringBuilder();
        // 进行字典排序
        str = keySet.toArray(str);
        Arrays.sort(str);
        for (int i = 0; i < str.length; i++) {
            String t = str[i] + "=" + map.get(str[i]) + "&";
            tmp.append(t);
        }
        if (null != key) {
            tmp.append("key=" + key);
        }
        return DigestUtils.md5Hex(tmp.toString()).toUpperCase();
    }

    /**
     * 签名字符串
     *
     * @param
     * @param sign          签名结果
     * @param
     * @param input_charset 编码格式
     * @return 签名结果
     */
    public static boolean verify(String text, String sign, String key, String input_charset) {
        text = text + key;
        String mysign = DigestUtils.md5Hex(getContentBytes(text, input_charset));
        if (mysign.equals(sign)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * @param content
     * @param charset
     * @return
     * @throws SignatureException
     * @throws UnsupportedEncodingException
     */
    public static byte[] getContentBytes(String content, String charset) {
        if (charset == null || "".equals(charset)) {
            return content.getBytes();
        }
        try {
            return content.getBytes(charset);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("MD5签名过程中出现错误,指定的编码集不对,您目前指定的编码集是:" + charset);
        }
    }

    private static boolean isValidChar(char ch) {
        if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'))
            return true;
        if ((ch >= 0x4e00 && ch <= 0x7fff) || (ch >= 0x8000 && ch <= 0x952f))
            return true;// 简体中文汉字编码
        return false;
    }

    /**
     * 除去数组中的空值和签名参数
     *
     * @param sArray 签名参数组
     * @return 去掉空值与签名参数后的新签名参数组
     */
    public static Map<String, String> paraFilter(Map<String, String> sArray) {
        Map<String, String> result = new HashMap<String, String>();
        if (sArray == null || sArray.size() <= 0) {
            return result;
        }
        for (String key : sArray.keySet()) {
            String value = sArray.get(key);
            if (value == null || value.equals("") || key.equalsIgnoreCase("sign")
                    || key.equalsIgnoreCase("sign_type")) {
                continue;
            }
            result.put(key, value);
        }
        return result;
    }

    /**
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     *
     * @param params 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     */
    public static String createLinkString(Map<String, String> params) {
        List<String> keys = new ArrayList<String>(params.keySet());
        Collections.sort(keys);
        String prestr = "";
        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            String value = params.get(key);
            if (i == keys.size() - 1) {// 拼接时,不包括最后一个&字符
                prestr = prestr + key + "=" + value;
            } else {
                prestr = prestr + key + "=" + value + "&";
            }
        }
        return prestr;
    }

    /**
     * @param
     * @param
     * @param
     */
    public static String httpRequest(String requestUrl, String requestMethod, String outputStr) {
        // 创建SSLContext
        StringBuffer buffer = null;
        try {
            URL url = new URL(requestUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod(requestMethod);
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.connect();
            //往服务器端写内容
            if (null != outputStr) {
                OutputStream os = conn.getOutputStream();
                os.write(outputStr.getBytes("utf-8"));
                os.close();
            }
            // 读取服务器端返回的内容
            InputStream is = conn.getInputStream();
            InputStreamReader isr = new InputStreamReader(is, "utf-8");
            BufferedReader br = new BufferedReader(isr);
            buffer = new StringBuffer();
            String line = null;
            while ((line = br.readLine()) != null) {
                buffer.append(line);
            }
            br.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return buffer.toString();
    }

    public static String urlEncodeUTF8(String source) {
        String result = source;
        try {
            result = java.net.URLEncoder.encode(source, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
     *
     * @param strxml
     * @return
     * @throws JDOMException
     * @throws IOException
     */
    public static Map doXMLParse(String strxml) throws Exception {
        if (null == strxml || "".equals(strxml)) {
            return null;
        }
        /*=============  !!!!注意,修复了微信官方反馈的漏洞,更新于2018-10-16  ===========*/
        try {
            Map<String, String> data = new HashMap<String, String>();
            // TODO 在这里更换
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
            documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
            documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            documentBuilderFactory.setXIncludeAware(false);
            documentBuilderFactory.setExpandEntityReferences(false);

            InputStream stream = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
            org.w3c.dom.Document doc = documentBuilderFactory.newDocumentBuilder().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) {
            throw ex;
        }
    }

    /**
     * 获取子结点的xml
     *
     * @param children
     * @return String
     */
    public static String getChildrenText(List children) {
        StringBuffer sb = new StringBuffer();
        if (!children.isEmpty()) {
            Iterator it = children.iterator();
            while (it.hasNext()) {
                Element e = (Element) it.next();
                String name = e.getName();
                String value = e.getTextNormalize();
                List list = e.getChildren();
                sb.append("<" + name + ">");
                if (!list.isEmpty()) {
                    sb.append(getChildrenText(list));
                }
                sb.append(value);
                sb.append("</" + name + ">");
            }
        }
        return sb.toString();
    }
    public static InputStream String2Inputstream(String str) {
        return new ByteArrayInputStream(str.getBytes());
    }
}

4、总结下支付中踩过的坑

1、签名时一定要按照接口定义的规则签名,字必须的字段名称,交易金额的单位,签名的时候,可以使用微信的工具 微信接口调试工具 来验证,代码里的签名要跟工具生成的签名一致
2、支付密钥key是否有效正确,第一次拿错了,博主在这里卡了比较久,支付key错误导致统一下单报错:<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名错误]]></return_msg></xml>
3、按照文中步骤,微信支付亲测可用,文中如有错误或者不合理,欢迎留言,微信退款会在后续献上

猜你喜欢

转载自blog.csdn.net/qq_37189624/article/details/84991919