1. 程式人生 > >JAVA WEB實現微信掃碼支付(模式二)

JAVA WEB實現微信掃碼支付(模式二)

一.準備微信掃碼支付要用到的相關引數,這裡將其全部寫入一個配置類,程式碼如下:

public class ZbWxPayConfig {
    public static String APP_ID = "***********************";
    public static String APP_SECRET = "***********************";
    public static String MCH_ID = "***********************";    //商戶號
    public static String MCH_KEY = "***********************";    //商戶平臺設定的金鑰
    public static String UFDODER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";//統一下單介面
    public static String ORDER_QUERY_URL = "https://api.mch.weixin.qq.com/pay/orderquery";//統一查詢訂單介面 
    public static String NOTIFY_URL = "***********************";//支付成功微信回撥地址
}

二.相關工具類

public class WxPayUtil {
    
    /**
     * @author     : YY
     * @date       : 2018-7-3-下午04:20:37
     * @param      : @return
     * @Description: 生成隨機字串
     */
    public static String getNonceStr(){
        String allChar = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        int maxPos = allChar.length();
        String nonceStr = "";
        for ( int i = 0; i < 16; i++) {
            nonceStr += allChar.charAt((int) Math.floor(Math.random() * maxPos));
        }
        return nonceStr;    
    }
    
    /**
     * @author     : YY
     * @date       : 2018-7-3-下午04:26:04
     * @param      : @return
     * @Description: 獲取當前時間字串
     */
    public static String getCurrTime() {
        Date now = new Date();
        SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        String s = outFormat.format(now);
        return s;
    }
    
    /**
     * @author     : YY
     * @date       : 2018-7-3-下午04:30:48
     * @param      : @return
     * @Description: 獲取10分鐘後的時間字串
     */
    public static String getAddCueeTime(){
        long currentTime = System.currentTimeMillis() ;
        currentTime +=10*60*1000;
        Date date=new Date(currentTime);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        String s = dateFormat.format(date);
        return s;
    }
    
    /**
     * @author     : YY
     * @date       : 2018-7-3-下午04:26:58
     * @param      : @param length
     * @param      : @return
     * @Description: 生成指定長度隨機數
     */
    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));
    }
    
    /**
     * @author     : YY
     * @date       : 2018-7-3-下午04:43:23
     * @param      : @param params
     * @param      : @param mchKey
     * @param      : @return
     * @Description: 生成微信支付簽名
     */
    @SuppressWarnings("unchecked")
    public static String createSign(SortedMap<String, String> params, String mchKey) {
        StringBuffer sb = new StringBuffer();
        Set set = params.entrySet();
        Iterator it = set.iterator();
        while(it.hasNext()){
            Entry entry = (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=" + mchKey);
        String sign = ZbMD5Util.EncryptionStr(sb.toString()).toUpperCase();
        return sign;
    }
    
    /**
     * @author     : YY
     * @date       : 2018-7-4-下午03:21:07
     * @param      : @param packageParams
     * @param      : @param mchKey
     * @param      : @return
     * @Description: 驗證簽名  是否簽名正確,規則是:按引數名稱a-z排序,遇到空值的引數不參加簽名。
     */
    @SuppressWarnings("unchecked")
    public static boolean isTenpaySign(SortedMap<String, String> packageParams,String mchKey) {
        if(packageParams != null){
            StringBuffer sb = new StringBuffer();
            Set es = packageParams.entrySet();
            Iterator it = es.iterator();
            while (it.hasNext()) {
                Entry entry = (Map.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=" + mchKey);
            // 算出摘要
            String mysign = MD5Util.EncryptionStr(sb.toString()).toLowerCase();
            String tenpaySign = ((String) packageParams.get("sign")).toLowerCase();
            return tenpaySign.equals(mysign);
        }else{
            return false;
        }
    }
    
    
    @SuppressWarnings("unchecked")
    public static SortedMap<String, String> xmlConvertToMap(String rxml) {
        
        // 解析xml成map
        Map<String, String> resultMap = new HashMap<String, String>();
        try {
            Document dom = DocumentHelper.parseText(rxml);
            Element root = dom.getRootElement();
            List<Element> elementList = root.elements();
            for (Element e : elementList) {
                resultMap.put(e.getName(), e.getText());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 過濾空 設定 SortedMap
        SortedMap<String, String> packageParams = new TreeMap<String, String>();
        Iterator it = resultMap.keySet().iterator();
        while (it.hasNext()) {
            String parameter = (String) it.next();
            String parameterValue = resultMap.get(parameter);

            String v = "";
            if (null != parameterValue) {
                v = parameterValue.trim();
            }
            packageParams.put(parameter, v);
        }
        return packageParams;
    }
    
    
    /**
     * @author     : YY
     * @date       : 2018-7-3-下午04:48:23
     * @param      : @param arr
     * @param      : @return
     * @Description: 將map資料轉化為xml格式字串
     */
    public static String MapToXml(Map<String, String> arr) {
        String xml = "<xml>";  
        Iterator<Entry<String, String>> iter = arr.entrySet().iterator();  
        while (iter.hasNext()) {  
            Entry<String, String> entry = iter.next();  
            String key = entry.getKey();  
            String val = entry.getValue();  
            if (IsNumeric(val)) {  
                xml += "<" + key + ">" + val + "</" + key + ">";  
            } else  
                xml += "<" + key + "><![CDATA[" + val + "]]></" + key + ">";  
        }  
        xml += "</xml>";  
        return xml;  
    }
    
    private static boolean IsNumeric(String str) {  
        if (str.matches("\\d *")) {  
            return true;  
        } else {  
            return false;  
        }  
    }

}




public class MD5Util {
    /**
     * @author      : YY
     * @time        : 2018-7-3-下午04:44:24
     * @package     : package com.tcmce.zibo.wxpay.utils;
     * @description : MD5加密字串
     */
    public static String EncryptionStr(String str){

        StringBuffer sb = new StringBuffer();
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(str.getBytes());
            byte[] bytes = md.digest();
            for (byte aByte : bytes) {
                String s=Integer.toHexString(0xff & aByte);
                if(s.length()==1){
                    sb.append("0"+s);
                }else{
                    sb.append(s);
                }
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return sb.toString();
    }

}





public class HttpUtil {
    
    /**
     * @author     : YY
     * @date       : 2018-7-6-上午11:25:01
     * @param      : @param urlStr
     * @param      : @param data
     * @param      : @return
     * @param      : @throws DocumentException
     * @Description: HTTP請求微信介面,返回map格式資料
     */
    @SuppressWarnings("unchecked")
    public static Map<String, String> postWx(String requestUrl, String outputStr) throws DocumentException{
        try {
            URL url = new URL(requestUrl);  
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();  
            conn.setDoOutput(true);  
            conn.setDoInput(true);  
            conn.setUseCaches(false);  
            // 設定請求方式(GET/POST)  
            //conn.setRequestMethod(requestMethod);  
            conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");  
            // 當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;
            StringBuffer buffer = new StringBuffer();  
            while ((str = bufferedReader.readLine()) != null) {  
                buffer.append(str);  
            }  
            // 釋放資源  
            bufferedReader.close();  
            inputStreamReader.close();  
            inputStream.close();  
            inputStream = null;  
            conn.disconnect();  
            if(buffer != null && buffer.toString().length() != 0){
                Map<String, String> preMap = new HashMap<String, String>();
                Document dom = DocumentHelper.parseText(buffer.toString());
                Element root = dom.getRootElement();
                List<Element> elementList = root.elements();
                for (Element e : elementList) {
                    preMap.put(e.getName(), e.getText());
                }
                return preMap;
            }
        } catch (Exception e) {
            logger.error("HTTP請求:["+requestUrl+"]異常: " + e.getMessage());
        }
        return null;
    }
    
}

三.編寫呼叫微信下單介面(使用AJAX請求該地址,返回JSON格式資料,返回支付二維碼地址,用於下一步生成二維碼圖片)

public class UnifiedOrderServlet extends HttpServlet{
    
    private static Logger logger = Logger.getLogger(UnifiedOrderServlet.class);
    
    private static final long serialVersionUID = 1L;

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

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        
        request.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
        Map<String, String> msgMap = new HashMap<String, String>();
        try {
            String prodcutId = request.getParameter("prodcutId").trim();//商品ID
            String orderType = request.getParameter("orderType").trim();//訂單型別
                //生成商戶訂單號
                String outTradeNo = WxPayUtil.getCurrTime() + WxPayUtil.buildRandom(4);
                //獲取商品價格
                String totalFee = "999";
                //回撥引數     長度有限制
                Map<String, String> attachMap = new HashMap<String, String>();
                attachMap.put("memberId", decryMemberId);
                attachMap.put("prodcutId", prodcutId);
                JSONObject attachJson = JSONObject.fromObject(attachMap);
                //完善微信下單所需引數
                SortedMap<String, String> orderParams =  new TreeMap<String, String>();
                orderParams.put("appid", WxPayConfig.APP_ID);
                orderParams.put("mch_id", WxPayConfig.MCH_ID);
                orderParams.put("nonce_str", WxPayUtil.getNonceStr());
                orderParams.put("body", "商品描述");//商品描述
                orderParams.put("attach", attachJson.toString());
                orderParams.put("out_trade_no", outTradeNo);
                orderParams.put("total_fee", new BigDecimal(totalFee).multiply(new BigDecimal(100)).intValue() + "");//訂單總金額,單位為分
                orderParams.put("spbill_create_ip", request.getRemoteAddr());
                orderParams.put("time_start", WxPayUtil.getCurrTime());////交易起始時間
                orderParams.put("time_expire", WxPayUtil.getAddCueeTime());//交易結束時間
                orderParams.put("notify_url", WxPayConfig.NOTIFY_URL);//非同步接收微信支付結果通知的回撥地址,通知url必須為外網可訪問的url,不能攜帶引數。
                orderParams.put("trade_type", "NATIVE");//交易型別 JSAPI 公眾號支付    NATIVE 掃碼支付    APP APP支付
                orderParams.put("device_info", "WEB");//終端裝置號(門店號或收銀裝置ID),預設請傳"WEB"
                orderParams.put("product_id", prodcutId);//商品ID
                
                String sign = WxPayUtil.createSign(orderParams, WxPayConfig.MCH_KEY);//將所有引數進行簽名
                orderParams.put("sign", sign);//簽名
                String xmlContent = WxPayUtil.MapToXml(orderParams);
                //呼叫微信下單介面
                Map<String, String> resultMap = HttpUtil.postWx(WxPayConfig.UFDODER_URL, xmlContent);
                logger.info("下單結果:" + resultMap.toString());
                //下單成功
                if("SUCCESS".equals(resultMap.get("result_code"))){
                    //二維碼連結
                    orderParams.put("code_url", resultMap.get("code_url"));
                    //預支付ID
                    orderParams.put("prepay_id", resultMap.get("prepay_id"));
                    orderParams.put("total_fee", totalFee.toString());
                    orderParams.put("payType", payType);
                    orderParams.put("memberId", decryMemberId);
                    orderParams.put("trade_state", "NOTPAY");
                    orderParams.put("trade_state_desc", "未支付");
                    orderParams.put("wx_pc", "PC");
                    orderParams.put("pay_source", "ZB");
                    //商戶系統記錄訂單資訊
                    此處程式碼省略
                    //以下引數為了頁面顯示所用,
                    msgMap.put("body", body);
                    msgMap.put("outTradeNo", outTradeNo);
                    msgMap.put("totalFee", totalFee.toString());
                    msgMap.put("codeUrl", resultMap.get("code_url"));//二維碼支付地址
                    msgMap.put("success", "true");
                }else{
                    msgMap.put("success", "false");
                    msgMap.put("message", "下單失敗,您可以嘗試稍後再進行下單!");
                } 
        } catch (Exception e) {
            msgMap.put("success", "false");
            msgMap.put("message", "系統異常,您可以稍後再進行嘗試下單!");
            logger.error("呼叫微信下單介面異常:" + e.getMessage());
        }
        Gson gson = new Gson();
        String json = gson.toJson(msgMap);
        PrintWriter out = response.getWriter();
        out.write(json);
        out.close();
    }

}

三.利用上一步獲得的二維碼路徑生成二維碼,用於頁面展示:

public class CreateCodeServlet extends HttpServlet{
    
    //二維碼預設尺寸
    private static int defaultWidthAndHeight = 250;

    private static final long serialVersionUID = 1L;

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

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        
        request.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
        
        try {
            String codeUrl = request.getParameter("code_url").trim();
            //以下開始生成二維碼
            Map<EncodeHintType, Object> hints = new HashMap<EncodeHintType, Object>();
            //指定糾錯等級
            hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
            //指定編碼格式
            hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
            //hints.put(EncodeHintType.MARGIN, 1);
            try {
                BitMatrix bitMatrix = new MultiFormatWriter().encode(codeUrl, BarcodeFormat.QR_CODE, defaultWidthAndHeight, defaultWidthAndHeight,hints);
                OutputStream outputStream = response.getOutputStream();
                MatrixToImageWriter.writeToStream(bitMatrix, "png", outputStream);//輸出二維碼
                outputStream.flush();
                outputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }    

}

四.實現支付成功後微信後臺回撥的函式,第一個配置檔案裡最後一個引數就是指向這裡,必須可以外網訪問的地址:

public class WxPayNotifyServlet extends HttpServlet{
    
    private static Logger logger = Logger.getLogger(WxPayNotifyServlet.class);
    private static final long serialVersionUID = 1L;

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

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        
        request.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
        String resXml = "";
        try {
            InputStream inputStream = request.getInputStream();
            StringBuffer sb = new StringBuffer();
            String s;
            BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
            while((s = in.readLine()) != null){
                sb.append(s);
            }
            in.close();
            inputStream.close();
            logger.error("支付結果:" + sb.toString());
            if(sb != null && sb.toString().length() != 0){


                SortedMap<String, String> resultMap = ZbWxPayUtil.xmlConvertToMap(sb.toString());
                //驗證簽名
                if(ZbWxPayUtil.isTenpaySign(resultMap, ZbWxPayConfig.MCH_KEY)){
                    if("SUCCESS".equals(resultMap.get("return_code")) && "SUCCESS".equals(resultMap.get("result_code"))){
                        //呼叫支付成功後執行業務邏輯程式碼
                        Integer rows = this.executePayNotify(resultMap);
                        if(rows != 0){
                            resXml = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
                        }else{
                            logger.error("----業務結果執行失敗  返回FALL 通知微信繼續重試----");
                            resXml = "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[商戶系統出錯]]></return_msg></xml> ";
                        }
                    }else{
                        logger.error("----支付失敗----" + resultMap.get("return_msg"));
                        resXml = "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[支付失敗]]></return_msg></xml>";
                    }
                }else{
                    logger.error("----簽名驗證失敗----");
                    resXml = "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[簽名驗證失敗]]></return_msg></xml> ";
                }
            }else{
                logger.error("----未接收到微信通知資訊----");
                resXml = "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[未接收到回撥資訊]]></return_msg></xml> ";
            }
        } catch (Exception e) {
            logger.error("----商戶系統異常----");
            resXml = "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[商戶系統異常]]></return_msg></xml> ";
            e.printStackTrace();
        }
        BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());  
        out.write(resXml.getBytes());  
        out.flush();  
        out.close(); 
    }
    
    /**
     * @author     : YY
     * @date       : 2018-7-6-下午12:59:58
     * @param      : @param resultMap
     * @param      : @return
     * @param      : @throws Exception
     * @Description: 微信掃碼支付成功後執行業務程式碼
     */
    private Integer executePayNotify(Map<String, String> resultMap) throws Exception{
        
        Double cashFee = Double.parseDouble(resultMap.get("cash_fee")) / 100,//現金支付金額
        totalFee = Double.parseDouble(resultMap.get("total_fee")) / 100;//總金額
        String bankType = resultMap.get("bank_type"),//付款銀行
        feeType = resultMap.get("fee_type"),//貨幣種類
        openid = resultMap.get("openid"),//使用者標識
        timeEnd = resultMap.get("time_end"),//支付完成時間
        transactionId = resultMap.get("transaction_id"),//微信支付訂單號
        out_trade_no = resultMap.get("out_trade_no"),//商戶訂單號
        attach = resultMap.get("attach");//商家資料包  傳入的是JSON格式字串
        //解析訂單攜帶引數
        JSONObject attachJsonObj = JSONObject.fromObject(attach);
        Map<String, String> argsMap = new HashMap<String, String>();
        argsMap.put("bankType", bankType);
        argsMap.put("cashFee", cashFee.toString());
        argsMap.put("feeType", feeType);
        argsMap.put("openid", openid);
        argsMap.put("timeEnd", timeEnd);
        argsMap.put("memberId", memberId);
        argsMap.put("transactionId", transactionId);
        argsMap.put("out_trade_no", out_trade_no);
        argsMap.put("trade_state", "SUCCESS");
        argsMap.put("trade_state_desc", "支付成功");

        此處開始執行商戶系統邏輯業務程式碼,自主實現


        return 0;
    }
}

五.支付功能到此結束,此處新增一個支付狀態查詢功能,支付成功後微信後臺不會主動通知支付頁面,需要自主新增查詢按鈕或定時查詢功能:

public class QueryOrderStatusServlet extends HttpServlet{

    private static final long serialVersionUID = 1L;

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

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        
        request.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
        Map<String, String> result = new HashMap<String, String>();
        
        try {
            String outTradeNo = request.getParameter("outTradeNo");
            if(StringUtils.isNotBlank(outTradeNo)){
                String nonceStr = WxPayUtil.getNonceStr();
                SortedMap<String, String> orderParams =  new TreeMap<String, String>();
                orderParams.put("appid", WxPayConfig.APP_ID);
                orderParams.put("mch_id", WxPayConfig.MCH_ID);
                orderParams.put("out_trade_no", outTradeNo);
                orderParams.put("nonce_str", nonceStr);
                //引數簽名
                String sign = WxPayUtil.createSign(orderParams, WxPayConfig.MCH_KEY);
                orderParams.put("sign", sign);
                String xmlContent = WxPayUtil.MapToXml(orderParams);
                Map<String, String> resultMap = HttpUtil.postWx(WxPayConfig.ORDER_QUERY_URL, xmlContent);
                
                if("SUCCESS".equals(resultMap.get("return_code"))){
                    if("SUCCESS".equals(resultMap.get("result_code"))){
                        String tradeState = resultMap.get("trade_state");
                        if("SUCCESS".equals(tradeState)){
                            result.put("code", "success");
                            result.put("msg", "支付成功 [ 即將跳轉至學習頁面......請稍後!]。");
                        }else{
                            result.put("code", "fail");
                            result.put("msg", resultMap.get("trade_state_desc") + "  [ 請您在微信上確認支付成功後,再執行該操作。]");
                        }
                    }else{
                        result.put("code", "fail");
                        result.put("msg", resultMap.get("err_code_des"));
                    }
                }else{
                    result.put("code", "fail");
                    result.put("msg", "網路異常,您可以稍後再試!");
                }
            }else{
                result.put("code", "fail");
                result.put("msg", "訂單號有誤!");
            }
            
        } catch (Exception e) {
            result.put("code", "fail");
            result.put("msg", "系統異常,您可以稍後再試!");
            e.printStackTrace();
        }
        
        Gson gson = new Gson();
        String json = gson.toJson(result);
        PrintWriter out = response.getWriter();
        out.write(json);
        out.close();
    }
    

}