1. 程式人生 > >微信支付(2)

微信支付(2)

接著上文,做微信支付(h5)需要微信登入的環節。不多講

在微信登入的時候可以獲取到微信對於的微信的公眾號的唯一標識即openid,這裡我是將獲取的openid存放在使用者表的。需要將他加入引數列表進行生成預支付的訂單號。

話歸正題,微信支付需要做的準備工作。需要登入微信商戶平臺設定好祕鑰。

設定支付安全目錄,就是你要拉起支付的專案域名。

在商戶平臺獲取到支付所需要的證書(微信升級後需要自己配置)

用微信的工具生成證書並放在伺服器內。

獲取商戶號和祕鑰

現在準備工具工作做完。

專案實戰

1.匯入jar包 wx 1.0.0.jar

2.生成引數列表生成預支付的訂單id ,直接貼程式碼。

 @SuppressWarnings("unchecked")
    @RequestMapping(value = "gopay")
    @ResponseBody
    public synchronized String Gopay(HttpServletRequest request,Model model,HttpServletResponse resp) throws Exception {
        SetRespHander.setHander(resp);
        User currentUser = (User) request.getSession().getAttribute(Constants.FRONT_USER_SESSION);
        if(currentUser==null){
            return "{\"status\": 403}";
        }
        String id=request.getParameter("id");
     
        String openId=currentUser.getWecatopenid();
        double    je=price;
        if(activity.getHeadcount()-activity.getNowcount()<rs){//報名人數超出限制
            return "{\"status\": 401,\"message\": \"報名人數超出限制\"}";
        }
        System.out.println(je);
//        String totalPrices = request.getParameter("totalPrice");//商品價格(單位:元)
//        String url = request.getParameter("url");//支付成功回撥地址(預設字首http://域名/kkqshop/f/)
//        double totalPrice = Double.parseDouble(totalPrices);
        String path = request.getContextPath();
        String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"+"f/";//專案路徑
        log.info("basePath=" + basePath);
        /** 總金額(分為單位) */
        int total = (int) (je*100);
//        int total=1;
        System.out.println(total);
        SortedMap<Object, Object> parameters = new TreeMap<Object, Object>();
        /** 公眾號APPID */
        parameters.put("appid", APPID);
        /** 商戶號 */
        parameters.put("mch_id", MCH_ID);
        /** 隨機字串 */
        parameters.put("nonce_str", WXUtil.getNonceStr());
        /** 商品名稱 */
        parameters.put("body", "商品");
        
        //String oppId = user.getWxOpenId();
        
        /** 當前時間 yyyyMMddHHmmss */
        String currTime = TenpayUtil.getCurrTime();
        /** 8位日期 */
        String strTime = currTime.substring(8, currTime.length());
        /** 四位隨機數 */
        String strRandom = TenpayUtil.buildRandom(4) + "";
        /** 訂單號 */
        parameters.put("out_trade_no", strTime + strRandom);
        
        /** 訂單金額以分為單位,只能為整數 */
        parameters.put("total_fee", total);
        /** 客戶端本地ip */
        parameters.put("spbill_create_ip", request.getRemoteAddr());
        /** 支付回撥地址 */
        parameters.put("notify_url", basePath + NOTIFY_URL);
        /** 支付方式為JSAPI支付 */
        parameters.put("trade_type", "JSAPI");
        /** 使用者微信的openid,當trade_type為JSAPI的時候,該屬性欄位必須設定 */
        parameters.put("openid", openId);
        /** MD5進行簽名,必須為UTF-8編碼,注意上面幾個引數名稱的大小寫 */
     
        String sign = createSign("UTF-8", parameters);
        parameters.put("sign", sign);
        String result =WeixinJSBridgeController.getResult(parameters);
        try {
            /** 解析微信返回的資訊,以Map形式儲存便於取值 */
            Map<String, String> map = XMLUtil.doXMLParse(result);
            
            SortedMap<Object, Object> params = new TreeMap<Object, Object>();
            params.put("appId", APPID);
            params.put("timeStamp", WXUtil.getTimeStamp());
            params.put("nonceStr", WXUtil.getNonceStr());
            /** 
             * 獲取預支付單號prepay_id後,需要將它參與簽名。
             * 微信支付最新介面中,要求package的值的固定格式為prepay_id=... 
             */
            params.put("package", "prepay_id=" + map.get("prepay_id"));

            /** 微信支付新版本簽名演算法使用MD5,不是SHA1 */
            params.put("signType", "MD5");
            /**
             * 獲取預支付prepay_id之後,需要再次進行簽名,參與簽名的引數有:appId、timeStamp、nonceStr、package、signType.
             * 主意上面引數名稱的大小寫.
             * 該簽名用於前端js中WeixinJSBridge.invoke中的paySign的引數值
             */
            String paySign = createSign("UTF-8", params);
            params.put("paySign", paySign);
            
            /** 預支付單號,前端ajax回撥獲取。由於js中package為關鍵字,所以,這裡使用packageValue作為key。 */
            params.put("packageValue", "prepay_id=" + map.get("prepay_id"));
            
            /** 付款成功後,微信會同步請求我們自定義的成功通知頁面,通知使用者支付成功 */
           // params.put("sendUrl", basePath + "pay/paysuccess?totalPrice=" + totalPrice);
//            params.put("sendUrl", url);
            /** 獲取使用者的微信客戶端版本號,用於前端支付之前進行版本判斷,微信版本低於5.0無法使用微信支付 */
            String userAgent = request.getHeader("user-agent");
            char agent = userAgent.charAt(userAgent.indexOf("MicroMessenger") + 15);
            params.put("agent", new String(new char[] { agent }));
            params.put("results", "");
            params.put("commodityName", "商品");
            params.put("out_trade_no", strTime + strRandom);
            params.put("total_fee", total);
            return "{\"status\": 200,\"data\": "+json+"}";

        } catch (JDOMException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }

這裡要注意生成隨機字串WXUtil.getTimeStamp()   ios要求10位

 

生成預支付訂單的方法

  public static String getResult(SortedMap<Object, Object> parameters) throws Exception{
        KeyStore keyStore  = KeyStore.getInstance("PKCS12");
       // InputStream instream =  WeixinJSBridgeController.class.getResourceAsStream("D://apiclient_cert.p12");//指定證書檔案
        FileInputStream instream = new FileInputStream(new File("C:/apache-tomcat-7.0.91/webapps/1242390902_20181018_cert.p12"));
        try {
            keyStore.load(instream, MCH_ID.toCharArray());//密碼預設是商戶號
        } finally {
            instream.close();
        }
        SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, MCH_ID.toCharArray()).build();
        // 只允許TLSv1協議
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new String[] { "TLSv1" }, null, SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
        CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
        HttpPost httpost = new HttpPost(UNI_URL);
        httpost.addHeader("Connection", "keep-alive");
        httpost.addHeader("Accept", "*/*");
        httpost.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
        httpost.addHeader("Host", "api.mch.weixin.qq.com");
        httpost.addHeader("X-Requested-With", "XMLHttpRequest");
        httpost.addHeader("Cache-Control", "max-age=0");
        httpost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
        httpost.setEntity(new StringEntity(getRequestXml(parameters), "UTF-8"));
        CloseableHttpResponse response = httpclient.execute(httpost);
         HttpEntity entity = response.getEntity();
        // String jsonStr = EntityUtils .toString(response.getEntity(), "UTF-8");
         StringBuffer result = new StringBuffer();
         if (entity != null) {
             BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(entity.getContent()));
             String text;
             while ((text = bufferedReader.readLine()) != null) {
                 result.append(text);
             }
         }
         log.info("請求統一支付介面的返回結果:");
         log.info(result.toString());
         EntityUtils.consume(entity);
         return result.toString();
    }

 

生成簽名的方法(使用MD5的加密方式)

 

 

講引數轉換成xmL形式

/**
     * 將請求引數轉換為xml格式的string
     * 
     * 作者: zhoubang 日期:2015年6月10日 上午9:25:51
     * 
     * @param parameters
     * @return
     */
    public static String getRequestXml(SortedMap<Object, Object> parameters) {
        StringBuffer sb = new StringBuffer();
        sb.append("<xml>");
        Set<Entry<Object, Object>> es = parameters.entrySet();
        Iterator<Entry<Object, Object>> it = es.iterator();
        while (it.hasNext()) {
            Map.Entry<Object, Object> entry = (Map.Entry<Object, Object>) it.next();
            String k = (String) entry.getKey();
            String v = 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();
    }

 

伺服器解析xml檔案

   /**
     * 傳送xml格式資料到微信伺服器 告知微信伺服器回撥資訊已經收到。
     * 
     * 作者: zhoubang 日期:2015年6月10日 上午9:27:33
     * 
     * @param return_code
     * @param return_msg
     * @return
     */
    public static String setXML(String return_code, String return_msg) {
        return "<xml><return_code><![CDATA[" + return_code
                + "]]></return_code><return_msg><![CDATA[" + return_msg
                + "]]></return_msg></xml>";
    }

另外微信支付成功會回撥,會返回商戶自己生成的訂單號和微信生成的訂單號(這個訂單號可用於微信退款等操作)

這裡的支付回撥方法由支付生成預支付訂單的引數決定

 /***
     * 付款成功回撥處理
     * 
     * 作者: zhoubang 日期:2015年6月10日 上午9:25:29
     * 
     * @param request
     * @param response
     * @throws IOException
     * @throws JDOMException
     */
    @SuppressWarnings("unchecked")
    @RequestMapping(value = "pay")
    public @ResponseBody void notify_success(HttpServletRequest request,
            HttpServletResponse response) throws IOException, JDOMException {
//        SetReqHander.setHander(response);
        log.info("微信支付成功呼叫回撥URL");
        InputStream inStream = request.getInputStream();
        ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = inStream.read(buffer)) != -1) {
            outSteam.write(buffer, 0, len);
        }
        log.info("~~~~~~~~~~~~~~~~付款成功~~~~~~~~~");
        outSteam.close();
        inStream.close();
        /** 支付成功後,微信回撥返回的資訊 */
        String result = new String(outSteam.toByteArray(), "utf-8");
        log.info("微信返回的訂單支付資訊:" + result);
        Map<Object, Object> map = XMLUtil.doXMLParse(result);

        // 用於驗籤
        SortedMap<Object, Object> parameters = new TreeMap<Object, Object>();
        String  transaction_id=(String) map.get("transaction_id");
        String  out_trade_no=(String) map.get("out_trade_no");
        for (Object keyValue : map.keySet()) {
            /** 輸出返回的訂單支付資訊 */
            log.info(keyValue + "=" + map.get(keyValue));
            if (!"sign".equals(keyValue)) {
                parameters.put(keyValue, map.get(keyValue));
            }
        }
        if (map.get("result_code").toString().equalsIgnoreCase("SUCCESS")) {
            // 先進行校驗,是否是微信伺服器返回的資訊
            String checkSign = createSign("UTF-8", parameters);
            log.info("對伺服器返回的結果進行簽名:" + checkSign);
            log.info("伺服器返回的結果簽名:" + map.get("sign"));
            if (checkSign.equals(map.get("sign"))) {// 如果簽名和伺服器返回的簽名一致,說明資料沒有被篡改過
                log.info("簽名校驗成功,資訊合法,未被篡改過");
                //告訴微信伺服器,我收到資訊了,不要再呼叫回撥方法了
                /**如果不返回SUCCESS的資訊給微信伺服器,則微信伺服器會在一定時間內,多次呼叫該回調方法,如果最終還未收到回饋,微信預設該訂單支付失敗*/
                /** 微信預設會呼叫8次該回調地址 */
                /*
                 * 【需要注意】:
                 *      後期很多朋友都反應說,微信會一直請求這個回撥地址,也都是使用的是 response.getWriter().write(setXML("SUCCESS", ""));
                 *      百思不得其解,最終發現處理辦法。其實不能直接使用response.getWriter()返回結果,這樣微信是接收不到的。
                 *      只能使用OutputStream流的方式返回結果給微信。
                 *      切記!!!!
                 * */
                OutputStream outputStream = null;
                try {
                    outputStream = response.getOutputStream();
                    outputStream.flush();
                    outputStream.write(setXML("SUCCESS", "").getBytes());
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    try {
                        outputStream.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                
                //response.getWriter().write(setXML("SUCCESS", ""));
                
                log.info("-------------" + setXML("SUCCESS", ""));
            }
        }
    }

至此微信的h5支付伺服器端已經結束了。

 

在貼出微信支付用到的工具類

public class WXUtil {
    
    /**
     * 獲取32位隨機字串
     * 
     * 作者: zhoubang 
     * 日期:2015年6月26日 下午3:51:44
     * @return
     */
    public static String getNonceStr() {
        Random random = new Random();
        return MD5Util.MD5Encode(String.valueOf(random.nextInt(10000)), "UTF-8");
    }
    
    public static String getNonceStr(int length) {
        String base = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
}

    /**
     * 時間戳
     * 
     * 作者: zhoubang 
     * 日期:2015年6月26日 下午3:52:08
     * @return
     */
    public static String getTimeStamp() {
        return String.valueOf(System.currentTimeMillis() / 1000);
    }
    
}
 

 

public class XMLUtil {
    /**
     * 解析xml,返回第一級元素鍵值對。如果第一級元素有子節點,則此節點的值是子節點的xml資料。
     * @param strxml
     * @return
     * @throws JDOMException
     * @throws IOException
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static Map doXMLParse(String strxml) throws JDOMException, 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 = builder.build(in);
        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 = XMLUtil.getChildrenText(children);
            }
            
            m.put(k, v);
        }
        
        //關閉流
        in.close();
        
        return m;
    }
    
    /**
     * 獲取子結點的xml
     * @param children
     * @return String
     */
    @SuppressWarnings("rawtypes")
    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(XMLUtil.getChildrenText(list));
                }
                sb.append(value);
                sb.append("</" + name + ">");
            }
        }
        
        return sb.toString();
    }
}
 

 

public class TenpayUtil {

    /**
     * 把物件轉換成字串
     * 
     * @param obj
     * @return String 轉換成字串,若物件為null,則返回空字串.
     */
    public static String toString(Object obj) {
        if (obj == null)
            return "";

        return obj.toString();
    }

    /**
     * 把物件轉換為int數值.
     * 
     * @param obj
     *            包含數字的物件.
     * @return int 轉換後的數值,對不能轉換的物件返回0。
     */
    public static int toInt(Object obj) {
        int a = 0;
        try {
            if (obj != null)
                a = Integer.parseInt(obj.toString());
        } catch (Exception e) {

        }
        return a;
    }

    /**
     * 獲取當前時間 yyyyMMddHHmmss
     * 
     * @return String
     */
    public static String getCurrTime() {
        Date now = new Date();
        SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        String s = outFormat.format(now);
        return s;
    }

    /**
     * 獲取當前日期 yyyyMMdd
     * 
     * @param date
     * @return String
     */
    public static String formatDate(Date date) {
        SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd");
        String strDate = formatter.format(date);
        return strDate;
    }

    /**
     * 取出一個指定長度大小的隨機正整數.
     * 
     * @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));
    }

    /**
     * 獲取編碼字符集
     * 
     * @param request
     * @param response
     * @return String
     */
    public static String getCharacterEncoding(HttpServletRequest request,
            HttpServletResponse response) {

        if (null == request || null == response) {
            return "gbk";
        }

        String enc = request.getCharacterEncoding();
        if (null == enc || "".equals(enc)) {
            enc = response.getCharacterEncoding();
        }

        if (null == enc || "".equals(enc)) {
            enc = "gbk";
        }

        return enc;
    }

    /**
     * 獲取unix時間,從1970-01-01 00:00:00開始的秒數
     * 
     * @param date
     * @return long
     */
    public static long getUnixTime(Date date) {
        if (null == date) {
            return 0;
        }

        return date.getTime() / 1000;
    }

    /**
     * 時間轉換成字串
     * 
     * @param date
     *            時間
     * @param formatType
     *            格式化型別
     * @return String
     */
    public static String date2String(Date date, String formatType) {
        SimpleDateFormat sdf = new SimpleDateFormat(formatType);
        return sdf.format(date);
    }

}