Java微信扫码支付

柔情痞子 提交于 2019-11-26 19:39:57

        前言:让我用Java写个微信扫码支付,身为小白,网上搜了好多文章,终于找到一个看得明白的,链接。表示人家讲的够详细了,现在自己要是实现一个,我觉得吧,可能入手比较乱。其实后来发现,代码都是按照那个流程图写的,写代码是用来实现功能的,当然要按照功能分析别人的代码,否则都不知道人家写来干什么的。别人的东西不一定都好,就要给他改改,改的好才是真好,,哈哈。下面看内容!!!

 

一、微信扫码支付内容

1.文档:

2.选择:

  • 代码工具与方式----用的eclipse的插件版:STS,用servlet简单实现。
  • 入手----实现微信扫码支付的功能,先从模式二的时序流程图入手。
  • 后台:微信商户平台----申请成为微信商户后能登陆的,后台查看订单情况。
  • 模式:模式二----模式二的时序流程图,模式二相比模式一更简单,因为模式二不需要自己生成二维码信息,只是接收微信返回的信息。
  • 交易类型:扫码支付----实现的是扫码支付,如果是公众号支付、app支付和刷卡支付,请绕行。
  • 穿透工具:花生壳----因为公司已经买了,我用就行。由于微信的回调地址要求必须是是外网的80端口,怎么办?我用的是付费的花生壳内网穿透版,可以使用国内免费的类似ngrok的natapp
  • debug调试:查看返回xml内容
  • 流程大概:从时序图分析,
  • ①调用统一下单API:就是给统一下单的参数赋值(必需的参数就行),然后向地址(微信提供的统一下单URL)传参数,然后接受微信返回参数。其中传递的参数格式都是xml格式。
  • ②微信返回的信息里有二维码对应的地址url(code_url)。用第三方库将code_url生成二维码。
  • ③调用支付结果API异步接收微信通知的支付结果,接收地址(统一下单里自定义的)接收微信传过来的参数,再返给微信参数,表示收到通知。

3.流程

(1)参数赋值

  • 根据统一下单API必须传的参数,进行参数赋值:
  • 商户号 mch_id :微信分配,企业号corpid即为此appId 
  • 随机字符串 nonce_str :微信分配,微信支付分配的商户号
  • 商品描述 body  :商户自定义,商品名称
  • 商户订单号 out_trade_no :商户自定义,唯一,详见商户订单号
  • 总金额 total_fee :商户自定义,整数,单位为分,测试值设为1,代表一分钱
  • 终端IP spbill_create_ip :获取支付端IP,测试值设为固定值,如127.0.0.1
  • 通知地址 notify_url :商户自定义,微信返回含有二维码信息的地址
  • 交易类型 trade_type :三选一,扫码支付是 NATIVE,详细说明见参数规定
  • 签名 sign :详见签名生成算法

(2)拼接参数

  • 将参数拼成xml格式字符串:

(3)参数提交

  • 传xml字符串格式的参数到统一下单API的URL地址:post方式提交

(4)解析返回参数

  • 微信返回xml字符串,详细参数见统一下单API,解析为map集合,其中解析xml会用到jdom库。

(5)接收支付结果

  • 接收xml字符串,解析成map集合,判断签名是否正确,执行成功或失败的相应业务,
  • 返回xml字符串,告诉微信已经接收到支付结果通知了。
  • 因为对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败。

(6)申请退款

4.资料

    (1)固定参数:申请成为微信商户成功,微信会邮件通知

  • APP_ID--微信分配的公众账号ID,如何获得
  • MCH_ID--微信支付分配的商户号
  • API_KEY--商户API密钥KEY,如何获得
  • NOTIFY_URL--商户自定义的接收微信异步通知支付结果的网址

    (2)用到的jar包:

    (3)穿透工具:

    (4)参考文章 :     

    (5)易错总结--签名错误

  • 产生原因:参数名多加空格???(我也不清楚)参数名拼写错误也有可能,待验证
  • 解决方法:微信官方接口调试生成的字符串,和调试过程中的签名字符串对比。
  • 解决步骤:
  • 微信公众平台支付接口调试工具,对应输入参数和值。
  • ②点击生成签名,出现
  •                 #1.生成字符串:
  •                 #2.连接商户key:假如叫签名字符串A
  •                 #3.md5编码并转成大写:
  •                 #4.最终的提交xml:
  • ③debug调试:在生成签名过程中打断点,输出拼成字符串,假如叫签名字符串B
  • ④微信官方调试工具生成的签名字符串A,和实际debug过程中的签名字符串B,对比,不同之处一目了然。

(6)后续总结和问题

二、实现步骤代码

先来张图,文件结构要清晰

1.生成二维码

  • servlet写的:参数赋值-->拼接参数-->参数提交-->参数返回并解析-->对应参数生成二维码
public class EnterServlet extends HttpServlet {
	public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
	
        //1,根据统一下单API,进行参数设定
        String appid = PayConfigUtil.APP_ID;    // appid   
        String mch_id = PayConfigUtil.MCH_ID;   // 商业号  
        String key = PayConfigUtil.API_KEY;     // key  
        String currTime = PayCommonUtil.getCurrTime();   
        String strRandom = PayCommonUtil.buildRandom(4) + "";  
        String nonce_str = currTime + strRandom;        // 随机字符串 
        String order_price = "1";       // 价格的单位是分(必须整数)   
        String body = "iphone7";        // 商品名称
        String out_trade_no = nonce_str + "520" ;       // 订单号
        String spbill_create_ip = PayCommonUtil.CREAT_IP;       // 获取发起电脑ip    
        String notify_url = PayConfigUtil.NOTIFY_URL;   // 回调接口
        String trade_type = "NATIVE";   // 交易类型是扫码支付
        //2,将参数放进Map集合
        SortedMap<String,String> packageParams = new TreeMap<String,String>();  
        packageParams.put("appid", appid);  
        packageParams.put("mch_id", mch_id);  
        packageParams.put("nonce_str", nonce_str);  
        packageParams.put("body", body);  
        packageParams.put("out_trade_no", out_trade_no);  
        packageParams.put("total_fee", order_price);  
        packageParams.put("spbill_create_ip", spbill_create_ip);  
        packageParams.put("notify_url", notify_url);  
        packageParams.put("trade_type", trade_type);
        //3,签名算法
        String sign = PayCommonUtil.createSign("UTF-8", packageParams,key);  
        packageParams.put("sign", sign);  

        //4,将Map集合转换为xml格式
        String requestXML = PayCommonUtil.getRequestXml(packageParams);  
        //5,用POST方式提交
        String resXml = HttpUtil.postData(PayConfigUtil.PAY_API, requestXML);  
        //6,解析xml为Map集合,得到code_url(二维码链接)
        Map<String, String> map = XMLUtil.doXMLParse(resXml);
        String urlCode = (String) map.get("code_url");
        //7,将code_url生成二维码
        Qrcode encoder = new Qrcode();
        encoder.createQRCoder(urlCode, response);
        }
        
        protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doGet(request, response);
	}
}

2.支付结果通知

  • 返回xml数据解析-->返回xml表示已接收到通知
​

public class NotifyServlet extends HttpServlet {

        public void doGet(HttpServletRequest request, HttpServletResponse response) 
                throws ServletException, IOException {
        //1,读取微信返回的xml格式信息流
        StringBuffer sb = new StringBuffer();  
        InputStream inputStream = request.getInputStream();  
        String s ;  
        BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));  
        while ((s = in.readLine()) != null){  
            sb.append(s);  
        }  
        in.close();  
        inputStream.close();  
        //2,解析xml为Map集合
        Map<String, String> m = XMLUtil.doXMLParse(sb.toString());
        //3,过滤空,用TreeMap集合存储信息
        SortedMap<String, String> packageParams = new TreeMap<String, String>();        
        Iterator<String> it = m.keySet().iterator();  
        while (it.hasNext()) {  
            String parameter = it.next();  
            String parameterValue = m.get(parameter);  
            String v = "";  
            if(null != parameterValue) {  
                v = parameterValue.trim();  
            }  
            packageParams.put(parameter, v);  
        }
        // 4,判断签名是否正确
        String key = PayConfigUtil.API_KEY; // key  
        if(PayCommonUtil.isTenpaySign("UTF-8", packageParams,key)) {  
           String resXml = "";   
        // String out_trade_no = packageParams.get("out_trade_no"); 
           if("SUCCESS".equals((String)packageParams.get("result_code"))){  
        //里是支付成功 ,执行自己的业务逻辑
              
        //通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.  
                resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"  
                        + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";  
            } else {  
        //提示支付失败,执行相应业务逻辑
              
                resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"  
                        + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";  
            }  
            BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());  
            out.write(resXml.getBytes());  
            out.flush();  
            out.close();  
        } else{  
        //通知签名验证失败
        }
        }

        public void doPost(HttpServletRequest request, HttpServletResponse response) 
                throws ServletException, IOException {
                doGet(request, response);
        }
}

​

3.工具类 

(1)固定参数

  • 微信分配的账户号 + 自定义的接收支付结果通知用的回调地址
public class PayConfigUtil {
        public static String APP_ID = "";//微信开放平台应用ID
        public static String MCH_ID = "";//商业号
        public static String API_KEY = "";//API key
        public static String PayConfigUtil.CREATE_IP = "127.0.0.1";//测试用的固定值,怎么获取公网IP暂时不会
        public static String NOTIFY_URL = "";//回调地址
}

(2)通用方法

  • 通用方法:签名是否正确 + 签名算法 + 拼成xml方法 + 生成随机数方法 + 获取当前时间方法
public class PayCommonUtil {

        /**
         * 是否签名正确,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
         * 
         * @return boolean
         */
        public static boolean isTenpaySign(String characterEncoding, SortedMap<String, String> packageParams,
                        String API_KEY) {
                StringBuffer sb = new StringBuffer();
                Set<Entry<String,String>> es = packageParams.entrySet();
                Iterator<Entry<String, String>> it = es.iterator();
                while (it.hasNext()) {
                        Entry<String, String> entry =  it.next();
                        String k = (String) entry.getKey();
                        String v = (String) entry.getValue();
                        if (!"sign".equals(k) && null != v && !"".equals(v)) {
                                sb.append(k + "=" + v + "&");
                        }
                }

                sb.append("key=" + API_KEY);

                // 算出摘要
                String mysign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toLowerCase();
                String tenpaySign = ((String) packageParams.get("sign")).toLowerCase();

                // System.out.println(tenpaySign + " " + mysign);
                return tenpaySign.equals(mysign);
        }

        /**
         * @author
         * @date 
         * @Description:sign签名
         * @param characterEncoding
         *            编码格式
         * @param parameters
         *            请求参数
         * @return
         */
        
        public static String createSign(String characterEncoding, SortedMap<String,String> packageParams, String API_KEY) {
                StringBuffer sb = new StringBuffer();
                Set<Entry<String,String>> es = packageParams.entrySet();
                Iterator<Entry<String, String>> it = es.iterator();
                while (it.hasNext()) {
                        Entry<String,String> entry = 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=" + API_KEY);
                String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
                return sign;
        }

        /**
         * @Description:将请求参数转换为xml格式的string
         */
        public static String getRequestXml(SortedMap<String,String> parameters) {
                StringBuffer sb = new StringBuffer();
                sb.append("<xml>");
                Set<Entry<String,String>> es = parameters.entrySet();
                Iterator<Entry<String, String>> it = es.iterator();
                while (it.hasNext()) {
                        Entry<String,String> entry = it.next();
                        String k = (String) entry.getKey();
                        String v = (String) entry.getValue();
                        if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) {
                                sb.append("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">");
                        } else {
                                sb.append("<" + k + ">" + v + "</" + k + ">");
                        }
                }
                sb.append("</xml>");
                return sb.toString();
        }

        /**
         * 取出一个指定长度大小的随机正整数.
         * 
         * @param length
         *            int 设定所取出随机数的长度。length小于11
         * @return int 返回生成的随机数。
         */
        public static int buildRandom(int length) {
                int num = 1;
                double random = Math.random();
                if (random < 0.1) {
                        random = random + 0.1;
                }
                for (int i = 0; i < length; i++) {
                        num = num * 10;
                }
                return (int) ((random * num));
        }

        /**
         * 获取当前时间 yyyyMMddHHmmss
         * 
         * @return String
         */
        public static String getCurrTime() {
                Date now = new Date();
                SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss");
                String s = outFormat.format(now);
                return s;
        }
}

(3)解析XML 

  • 将xml解析成map集合,根元素-->子元素-->遍历存到集合-->子元素是多层的要递归(按道理说)
public class XMLUtil {  
    
    /** 
     * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。 
     * @param strxml 
     * @return 
     * @throws JDOMException 
     * @throws IOException 
     */  
    @SuppressWarnings("unchecked")
    public static Map<String, Object> doXMLParse(String strxml) throws JDOMException, IOException {  
        strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");  
  
        if(null == strxml || "".equals(strxml)) {  
            return null;  
        }  
        Map<String, Object> m = new HashMap<String, Object>();  
          
        InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));  
        SAXBuilder builder = new SAXBuilder();  
        Document doc = builder.build(in);  
        Element root = doc.getRootElement();  
        List<Element> list = root.getChildren();  
        Iterator<Element> it = list.iterator();  
        while(it.hasNext()) {  
            Element e = (Element) it.next();  
            String k = e.getName();  
            String v = "";  
            List<Element> children = e.getChildren();  
            if(children.isEmpty()) {  
                v = e.getTextNormalize();  
            } else {  
                v = XMLUtil.getChildrenText(children);  
            }  
            m.put(k, v);  
        }  
          
        //关闭流  
        in.close();  
          
        return m;  
    }  
      
    /** 
     * 获取子结点的xml 
     * @param children 
     * @return String 
     */  
    @SuppressWarnings("unchecked")
    public 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(XMLUtil.getChildrenText(list));  
                }  
                sb.append(value);  
                sb.append("</" + name + ">");  
            }  
        }  
        return sb.toString();  
    }  
}

(4)POST提交

  • post方式提交,说get方式不安全,不涉及证书
public class HttpUtil {

        private final static int CONNECT_TIMEOUT = 5000; // in milliseconds
        private final static String DEFAULT_ENCODING = "UTF-8";

        public static String postData(String urlStr, String data) {
                return postData(urlStr, data, null);
        }

        public static String postData(String urlStr, String data, String contentType) {
                BufferedReader reader = null;
                try {
                        URL url = new URL(urlStr);
                        URLConnection conn = url.openConnection();
                        conn.setDoOutput(true);
                        conn.setConnectTimeout(CONNECT_TIMEOUT);
                        conn.setReadTimeout(CONNECT_TIMEOUT);
                        if (contentType != null)
                                conn.setRequestProperty("content-type", contentType);
                        OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), DEFAULT_ENCODING);
                        if (data == null)
                                data = "";
                        writer.write(data);
                        writer.flush();
                        writer.close();

                        reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), DEFAULT_ENCODING));
                        StringBuilder sb = new StringBuilder();
                        String line = null;
                        while ((line = reader.readLine()) != null) {
                                sb.append(line);
                                sb.append("\r\n");
                        }
                        return sb.toString();
                } catch (IOException e) {
                        // logger.error("Error connecting to " + urlStr + ": " +
                        // e.getMessage());
                } finally {
                        try {
                                if (reader != null)
                                        reader.close();
                        } catch (IOException e) {
                        }
                }
                return null;
        }
}

(5)MD5工具 

public class MD5Util {

        private static String byteArrayToHexString(byte b[]) {
                StringBuffer resultSb = new StringBuffer();
                for (int i = 0; i < b.length; i++)
                        resultSb.append(byteToHexString(b[i]));

                return resultSb.toString();
        }

        private static String byteToHexString(byte b) {
                int n = b;
                if (n < 0)
                        n += 256;
                int d1 = n / 16;
                int d2 = n % 16;
                return hexDigits[d1] + hexDigits[d2];
        }

        public static String MD5Encode(String origin, String charsetname) {
                String resultString = null;
                try {
                        resultString = new String(origin);
                        MessageDigest md = MessageDigest.getInstance("MD5");
                        if (charsetname == null || "".equals(charsetname))
                                resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
                        else
                                resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
                } catch (Exception exception) {
                }
                return resultString;
        }

        private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d",
                        "e", "f" };

}

 MD5Util

(6)第三方库生成二维码 

  • 谷歌生成二维码的zxing包示例使用好简单,直接返回BufferImage类型,二进制流图片
public class Qrcode {
        public void createQRCoder(String content, HttpServletResponse response) {
        try {
            int width = 400;//二维码宽度
            int height = 400;//二维码高度
            String qrcodeFormat = "png";//图片类型
            HashMap<EncodeHintType, String> hints = new HashMap<EncodeHintType, String>();
            hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
            //MultiFormatWriter 对象为生成二维码的核心类,后面的 MatrixToImageWriter 只是将二维码矩阵输出到图片上面。
            BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
            MatrixToImageWriter.writeToStream(bitMatrix, qrcodeFormat, response.getOutputStream());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }  
}

 

     

     

    易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
    该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!