1. 程式人生 > >Java 微信二維碼支付

Java 微信二維碼支付

本文程式碼並非原創,是根據網上一篇部落格修改而成,留作備忘。
微信支付有2種模式,第一種模式略微複雜,本文采用第二種模式;

 微信掃碼模式二
     業務流程說明:
     (1)商戶後臺系統根據使用者選購的商品生成訂單。
     (2)使用者確認支付後呼叫微信支付【統一下單API】生成預支付交易;
     (3)微信支付系統收到請求後生成預支付交易單,並返回交易會話的二維碼連結code_url。
     (4)商戶後臺系統根據返回的code_url生成二維碼。
     (5)使用者開啟微信“掃一掃”掃描二維碼,微信客戶端將掃碼內容傳送到微信支付系統。
     (6)微信支付系統收到客戶端請求,驗證連結有效性後發起使用者支付,要求使用者授權。
     (7)使用者在微信客戶端輸入密碼,確認支付後,微信客戶端提交授權。
     (8)微信支付系統根據使用者授權完成支付交易。
     (9)微信支付系統完成支付交易後給微信客戶端返回交易結果,並將交易結果通過簡訊、微信訊息提示使用者。微信客戶端展示支付交易結果頁面。
     (10)微信支付系統通過傳送非同步訊息通知商戶後臺系統支付結果。商戶後臺系統需回覆接收情況,通知微信後臺系統不再發送該單的支付通知。
     (11)未收到支付通知的情況,商戶後臺系統呼叫【查詢訂單API】。
     (12)商戶確認訂單已支付後給使用者發貨

     參閱:
        微信官方說明
        - https:
//pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5
- http://www.demodashi.com/demo/10268.html CSDN例項 - http://blog.csdn.net/wangqiuyun/article/details/51241064 - http://blog.csdn.net/bjlf_1989/article/details/51829557

根據訂單生成微信支付二維碼

本部分包含了微信支付模式二的1、2、3、4步驟,APPID、商戶號、key等引數需要到微信公眾號中查詢
WXPayController程式碼

    @Controller
    @RequestMapping(value = "${adminPath}/wxpay")
    public class WXPayController extends BaseController {

    @Autowired
    private OrderService orderService;
    @Autowired
    private ProductService productService;
    @Autowired
    private WXPayService wxPayService;

 /**
     *  生成微信支付二維碼圖片
     *
     * @param
orderId 訂單ID * @param request * @param response * @throws Exception */
@RequestMapping(value = "/wxqrcode.jpg") public void wxqrcode(String orderId, HttpServletRequest request, HttpServletResponse response, HttpSession session) throws Exception{ // 根據傳入的訂單號獲取到訂單的金額、訂單內容等引數,此處自行編碼實現 Order order = orderService.findById(orderId); if (null == order){ throw new NullPointerException("訂單不存在!"); } if (!Order.STATUS_CREATE.equals(order.getStatus())){ throw new NullPointerException("訂單已結算或狀態錯誤!"); } List<OrderDetail> orderDetails = order.getOrderDetails(); if (null == orderDetails || orderDetails.size() < 1){ throw new NullPointerException("訂單錯誤,訂單中沒有商品!"); } // 微信支付URL需要傳入的金額單位是分,此處將訂單金額轉換成'分'單位 BigDecimal fen = order.getTotalAmount().multiply(new BigDecimal(100)); fen = fen.setScale(0, BigDecimal.ROUND_HALF_UP); String order_price = fen.toString(); // 微信支付顯示標題 String body = "微信支付測試demo"; /*if (order.getTotalNumber() > 1){ StringBuilder sb = new StringBuilder(); sb = sb.append(orderDetails.get(0).getProduct().getName()) .append(" 等") .append(order.getTotalNumber()) .append("件商品"); body = sb.toString(); } else { body = orderDetails.get(0).getProduct().getName(); }*/ // 微信支交易訂單號,不能重複 String out_trade_no = "" + System.currentTimeMillis(); // 組裝引數 Map<String, Object> param = new HashMap<>(); param.put("order_price", order_price); param.put("body", body); param.put("out_trade_no", out_trade_no); param.put("attach", orderId); // 生成微信支付二維碼連結 Map<String, String> result = wxPayService.doUnifiedOrder(param, request); if ("FAIL".equals(result.get("return_code"))){ logger.error("生成二維碼錯誤: " + result.get("return_msg")); session.setAttribute("create_wx_qrcode_error_msg", result.get("return_msg")); } else { String urlCode = result.get("code_url"); // 生成微信二維碼,輸出到response流中 String icon = WXPayController.class.getClassLoader().getResource("coffee_icon.png").getPath(); BufferedImage bufferedImage = MatrixToImageWriterWithLogo.genBarcode(urlCode, 512, 512, icon); // 二維碼的內容,寬,高,二維碼中心的圖片地址 ImageIO.write(bufferedImage, "jpg", response.getOutputStream()); } } }

WXPayService程式碼

    @Service
    @Transactional(readOnly = true)
    public class WXPayService {

    / **
     * 呼叫微信支付介面返回URL
     * @param param 訂單價格,訂單顯示內容,訂單號
     * @param request
     * @return
     * @throws Exception
     */
    public Map<String, String> doUnifiedOrder(Map<String, Object> param, HttpServletRequest request) throws Exception {

        String appid    = PayConfigUtil.APP_ID;      // appid
        String mch_id   = PayConfigUtil.MCH_ID;      // 商戶號
        String key      = PayConfigUtil.API_KEY;     // key

        String trade_type = "NATIVE";
        String spbill_create_ip = PayCommonUtil.getIpAddress(request);      // 獲取發起電腦 ip
        String notify_url = PayConfigUtil.NOTIFY_URL;                       // 回撥介面

        String currTime = PayCommonUtil.getCurrTime();
        String strTime = currTime.substring(8, currTime.length());
        String strRandom = PayCommonUtil.buildRandom(4) + "";
        String nonce_str = strTime + strRandom;                             // 隨機字串

        String order_price = (String) param.get("order_price");             // 價格   注意:價格的單位是分
        String body = (String) param.get("body");                           // 商品名稱
        String out_trade_no = (String) param.get("out_trade_no");           // 訂單號

        String attach = (String) param.get("attach");                       // 附加引數,這裡傳的是我們的訂單號orderId

        SortedMap<Object,Object> packageParams = new TreeMap<>();
        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);
        packageParams.put("attach", attach);

        // 簽名
        String sign = PayCommonUtil.createSign("UTF-8", packageParams, key);
        packageParams.put("sign", sign);

        // 微信支付介面傳輸資料使用xml方式進行的,此處將引數裝換為xml
        // map --> xml
        String requestXML = PayCommonUtil.getRequestXml(packageParams);
        System.out.println("---------- Request XML: " + requestXML);

        String resXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML);
        System.out.println("---------- Response XML: " + resXml);

        // xml --> map
        return XMLUtil.doXMLParse(resXml);
    }   
}

使用者掃描二維碼支付完成,微信回撥我們的介面, 完成相應業務

使用者支付成功後,微信會通知此介面傳給我們必要的引數,此處使用者處理我們的邏輯如:給使用者配貨發貨等,本部分包含了微信支付模式二的10、12步,也可以不使用本介面通過模式二第11步主動查詢微信支付結果

在WXPayController中新增微信支付成功後的回撥介面及相應的方法

  /**
     *  微信支付成功回撥方法,在此方法中修改訂單為已付款給使用者發貨等操作,將此URL配置到微信公眾號的支付成功的回撥介面中處理支付成功的業務邏輯
     *
     * @param request
     * @param response
     * @throws Exception
     */
    @RequestMapping(value = "wxnotify")
    public void wxnotify(HttpServletRequest request, HttpServletResponse response) throws Exception{

        System.out.println("-------------------------- wxnotify ---------------------------------");

        //讀取引數
        StringBuffer sb = new StringBuffer();
        InputStream inputStream = request.getInputStream();
        BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));

        String s ;
        while ((s = in.readLine()) != null){
            sb.append(s);
        }
        in.close();
        inputStream.close();

        //解析xml成map
        Map<String, String> m = XMLUtil4jdom.doXMLParse(sb.toString());
        if (null == m){
            throw new NullPointerException("微信伺服器未返回任何資料!");
        }

        //過濾空 設定 TreeMap
        SortedMap<Object,Object> packageParams = new TreeMap<>();
        Iterator it = m.keySet().iterator();
        while (it.hasNext()) {
            String parameter = (String) it.next();
            String parameterValue = m.get(parameter);

            String v = "";
            if(null != parameterValue) {
                v = parameterValue.trim();
            }
            packageParams.put(parameter, v);
        }
        logger.debug(packageParams.toString());

        // 賬號資訊
        String key = PayConfigUtil.API_KEY; //key

        //判斷簽名是否正確
        if(PayToolUtil.isTenpaySign("UTF-8", packageParams,key)) {
            //------------------------------
            //處理業務開始
            //------------------------------
            String resXml = "";
            if("SUCCESS".equals((String)packageParams.get("result_code"))){ // 支付成功
                try {
                    //////////執行自己的業務邏輯////////////////
                    //此處自行編碼完成相應的支付成功後的業務邏輯
                   wxPayService.wxnotify(packageParams);  
                } catch (Exception e){ // 退款
                    e.printStackTrace();
                    logger.error(e.getMessage());
                }
                //////////執行自己的業務邏輯////////////////

                //暫時使用最簡單的業務邏輯來處理:只是將業務處理結果儲存到session中
                //(根據自己的實際業務邏輯來調整,很多時候,我們會操作業務表,將返回成功的狀態保留下來)
                request.getSession().setAttribute("_PAY_RESULT", "OK");
                System.out.println("-------------------------- 支付成功 ---------------------------------");

                //通知微信.非同步確認成功.必寫.不然會一直通知後臺.八次之後就認為交易失敗了.
                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> ";

                System.out.println("-------------------------- 支付失敗 ---------------------------------");

            }
            //------------------------------
            //處理業務完畢
            //------------------------------
            BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
            out.write(resXml.getBytes());
            out.flush();
            out.close();
        } else{
            logger.debug("通知簽名驗證失敗");
        }
    }

在WXPayService中新增支付成功的業務邏輯

 @Transactional(readOnly = false)
    public void wxnotify(SortedMap<Object,Object> packageParams) throws Exception{

          String orderId = (String) packageParams.get("attach");
          String realPay = (String) packageParams.get("total_fee"); // 分為單位
          BigDecimal pay = new BigDecimal(realPay).divide(new BigDecimal(100));
          System.out.println("訂單" + orderId + "支付成功,支付金額為:¥" + pay.floatValue() + "元");
    }

如何通知前臺使用者已經支付

前臺web頁面可用通過ajax輪訓,根據訂單id檢測訂單的狀態是否是已經支付

程式碼中用到的工具類

PayConfigUtil工具類,儲存了一些微信支付的引數

public class PayConfigUtil {

  // 微信支付 公眾號ID 
  public static final String APP_ID = "wx3c3a******a41b"; 
  // 商戶號ID
  public static final String MCH_ID = "132****1";
  // key設定路徑:微信商戶平臺(pay.weixin.qq.com)-->賬戶設定-->API安全-->金鑰設定
  public static final String API_KEY = "12345678***********9012";

  // 支付成功回撥介面,我們在WXPayController中編寫的回撥介面的公網地址
  // # 微信支付成功回撥地址:微信公眾平臺(mp.weixin.qq.com) --> 微信支付 --> 掃碼支付 --> 支付回撥URL
  public static final String NOTIFY_URL = "http://1*0.2**.**.*2:80**/wxpay/wxnotify";

  // 微信支付官方介面
  public static final String UFDODER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";

}

XMLUtil4jdom類用於將map引數與xml互轉

public class XMLUtil4jdom {

    /**
     * 解析xml,返回第一級元素鍵值對。如果第一級元素有子節點,則此節點的值是子節點的xml資料。
     * @param strxml
     * @return
     * @throws JDOMException
     * @throws IOException
     */
    public static Map doXMLParse(String strxml) throws JDOMException, IOException {
        strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");

        if(null == strxml || "".equals(strxml)) {
            return null;
        }

        Map<String, String> m = new HashMap<String, String>();
        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 = XMLUtil4jdom.getChildrenText(children);
            }

            m.put(k, v);
        }

        //關閉流
        in.close();

        return m;
    }

    /**
     * 獲取子結點的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(XMLUtil4jdom.getChildrenText(list));
                }
                sb.append(value);
                sb.append("</" + name + ">");
            }
        }

        return sb.toString();
    }

}

PayCommonUtil類裡包裝了微信支付簽名演算法和一些小工具

public class PayCommonUtil {

    /**
     * 微信支付簽名演算法sign
     * @param characterEncoding
     * @param parameters
     * @return
     */
    @SuppressWarnings("unchecked")
    public static String createSign(String characterEncoding, SortedMap<Object,Object> parameters, String API_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=" + API_KEY);
        String sign = PayCommonUtil.MD5Encode(sb.toString(), characterEncoding).toUpperCase();

        return sign;
    }

    /**
     * @Description:將請求引數轉換為xml格式的string
     * @param parameters
     *            請求引數
     * @return
     */
    public static String getRequestXml(SortedMap<Object, Object> parameters) {
        StringBuffer sb = new StringBuffer();
        sb.append("<xml>");
        Set es = parameters.entrySet();
        Iterator it = es.iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.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();
    }

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

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

    /**
     * 獲取IP地址
     *
     * @param request
     * @return
     */
    public static String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    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];
    }

    /**
     * 計算字串MD5摘要值
     *
     * @param origin 原始字串
     * @param charsetname 字符集
     * @return
     */
    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" };
}

至此全部程式碼就已經完成了