1. 程式人生 > >微信支付之Native掃碼支付功能

微信支付之Native掃碼支付功能

作者:陳惠,叩丁狼教育高階講師。原創文章,轉載請註明出處。

上一篇微信支付文章:https://www.jianshu.com/p/9c322b1a5274
實現了微信公眾號內H5頁面進行支付的功能,但是這種方式的缺點就是必須在微信中開啟付款頁面才能實現,所以並不適合所有的場景。那麼本篇文章,會以另外一種方式實現,使用掃碼的方式來進行支付。

需要注意的是,掃碼支付分兩種形式:

線下的掃碼支付:
這種方式非常簡單,商戶在微信申請付款二維碼貼紙,貼到收銀臺附近位置,客戶購買商品直接使用手機掃碼二維碼,輸入付款金額即可支付,這種方式不需要程式設計人員,大大減少商戶成本,比如常見的便利店,商場線下店等等。

叩丁狼教育.png

 

線上的掃碼支付:
這是本文選擇實現的方式,也叫做Native支付,這種方式適合PC端的網站,比如大眾點評、攜程、優酷等,如果使用的是電腦來訪問網頁,並需要支付相關費用,比如商品付款,充值VIP這些都是比較常見的場景,很多網站都會選用這種方式,總之最後付款的那一刻,就在網頁上展示付款碼,讓使用者去掃並付款即可,因為這種方式不是面對面付款,所以必須要保證客戶付款的金額是準確的,所以這個二維碼不是固定的,是根據賬單金額生成的,使用者掃碼之後就可以馬上看到需要付款的金額,確認無誤再進行付款。

叩丁狼教育.png

 

本文使用的是線上的掃碼支付方式,框架使用SpringMVC來實現。

準備工作

一.註冊商戶號
到微信支付商戶平臺https://pay.weixin.qq.com
提交商家相關資料,註冊一個商戶賬號,並開通Native支付功能。

叩丁狼教育.png

 

二.繫結公眾號或小程式
目的是要得到一個授權的APPID。

叩丁狼教育.png

 

三.設定API金鑰,登入商戶平臺——>賬戶中心——>API安全——>API金鑰
該金鑰在後面的程式碼中計算支付簽名的時候需要使用到。

叩丁狼教育.png

 

Native支付實現模式

官方提供了兩種模式實現,第一種模式比較複雜,還需要自己處理二維碼地址相關資訊,第二種比較簡單,微信可以直接把生成好的二維碼地址返回給我們,所以我們使用第二種實現。
具體區別可參考:

https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_3

實現的大致流程可參考官方提供的時序圖:

 

叩丁狼教育.png

但是流程有很多,不一一演示,我們選取核心的部分來實現即可。

開發流程

文中的例子為:開通叩丁狼VIP會員服務,並實現掃碼支付。

一.準備一個可以觸發下單操作的頁面

叩丁狼教育.png

 

二.點選"開通VIP"按鈕後進入controller的方法,接收商品引數並呼叫微信支付統一下單介面
正常的業務流程是在該方法中,獲取商品id,再通過id去查詢資料庫該商品的相關屬性,比如名稱,價格等等,然後再建立應用自身的業務訂單,再去呼叫微信支付的統一下單介面(讓微信生成預支付單,後續才可以進行支付),但此處重點在支付流程,商品的屬性值和訂單相關值,暫且先使用假資料。

介面以及引數可參考微信官方提供的統一下單文件:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1

根據文件介紹,使用Native方式呼叫統一下單介面時需要帶上相關必填的引數如下:

叩丁狼教育.png

程式碼如下:
把必填的引數封裝成對應的實體類:

/**
 * 微信統一下單實體類
 */
@Setter
@Getter
@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class WxOrderEntity {
  private String appid;
    private String body;
    private String device_info;
    private String mch_id;
    private String nonce_str;
    private String sign;
    private String out_trade_no;
    private int total_fee;
    private String trade_type;
    private String spbill_create_ip;
    private String openid;
    private String notify_url;
    private Long product_id;
}

呼叫介面成功後返回的結果也封裝成實體類:

/**
 * 微信統一下單返回結果實體類
 */
@Setter
@Getter
@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class WxOrderResultEntity {
    private String return_code;
    private String return_msg;
    private String result_code;
    private String appid;
    private String nonce_str;
    private String sign;
    private String trade_type;
    private String prepay_id;
    private String code_url;
}

該結果中最重要的是code_url引數,在生成付款二維碼時需要用到。

叩丁狼教育.png

注意:下單的業務邏輯,正常是需要抽取到業務層的,但是此處為了方便閱讀程式碼,直接寫到了控制器上。

@Controller
public class OrderController {

    @RequestMapping("order")
    public String save(Long productId, Model model, HttpServletRequest request) throws Exception {
        //根據商品id查詢商品詳細資訊(假資料)
        double price = 0.01;//(0.01元)
        String productName = "叩丁狼VIP會員";
        //生成訂單編號
        int number = (int)((Math.random()*9)*1000);//隨機數
        DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");//時間
        String orderNumber = dateFormat.format(new Date()) + number;
        //準備呼叫介面需要的引數
        WxOrderEntity order = new WxOrderEntity();
        //appid
        order.setAppid(WeChatUtil.APPID);
        //商戶號
        order.setMch_id(WeChatUtil.MCH_ID);
        //商品描述
        order.setBody(productName);
        //交易型別
        order.setTrade_type("NATIVE");
        //商戶訂單號
        order.setOut_trade_no(orderNumber);
        //支付金額(單位:分)
        order.setTotal_fee((int)(price*100));
        //使用者ip地址
        order.setSpbill_create_ip(RequestUtil.getIPAddress(request));
        //設定商品id
        order.setProduct_id(productId);
        //接收支付結果的地址
        order.setNotify_url("http:/www.xxxx.com/receive.do");
        //32位隨機數(UUID去掉-就是32位的)
        String uuid = UUID.randomUUID().toString().replace("-", "");
        order.setNonce_str(uuid);
        //生成簽名
        String sign = WeChatUtil.getPaySign(order);
        order.setSign(sign);
        //呼叫微信支付統一下單介面,讓微信也生成一個預支付訂單
        String xmlResult = HttpUtil.post(GET_PAY_URL, XMLUtil.toXmlString(order));
        //把返回的xml字串轉成物件
        WxOrderResultEntity entity = XMLUtil.toObject(xmlResult,WxOrderResultEntity.class);
        //微信預支付單成功建立
        if(entity.getReturn_code().equals("SUCCESS")&&entity.getResult_code().equals("SUCCESS")){
            //使用二維碼生成工具,把微信返回的codeUrl轉為二維碼圖片,儲存到磁碟
            String codeUrl = entity.getCode_url();
            //使用訂單號來作為二維碼的圖片名稱
            File file = new File(QRCodeUtil.PAY_PATH,orderNumber+".jpg");
            QRCodeUtil.createImage(codeUrl,new FileOutputStream(file));
            //把訂單號傳到支付頁面
            model.addAttribute("orderNumber",orderNumber);
        }
        //跳轉到支付頁進行支付
        return  "pay";
    }
}

統一下單介面呼叫成功後,微信會建立一個預支付單,此時即為未付款狀態:

叩丁狼教育.png

同時,微信還會返回codeUrl給我們,這個就是二維碼的連結,我們需要利用工具把該連結轉為二維碼

 

叩丁狼教育.png

二維碼生成工具,我使用的是google的Zxing
maven依賴:

 <dependency>
      <groupId>com.google.zxing</groupId>
      <artifactId>core</artifactId>
      <version>3.3.3</version>
    </dependency>

工具類:

public class QRCodeUtil {

    // 二維碼尺寸
    public static final int QRCODE_SIZE = 300;

    // 存放二維碼的路徑
    public static final String PAY_PATH = "c://pay";

    /**
     * 生成二維碼
     * @param content   源內容
     * @param outputStream 輸出流
     * @throws Exception
     */
    public static void createImage(String content, OutputStream outputStream) throws Exception {
        Hashtable hints = new Hashtable();
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
        hints.put(EncodeHintType.MARGIN, 1);
        BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE,
                hints);
        int width = bitMatrix.getWidth();
        int height = bitMatrix.getHeight();
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
            }
        }
        // 存到磁碟
        ImageIO.write(image, "jpg", outputStream);
    }

}

生成二維碼網上也有很多工具,有的人還會在二維碼中間加入logo,自己去找就可以啦。

下面是剛才統一下單的介面使用到的簽名演算法程式碼。

官方文件參考:
pay簽名:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3

 
    /**
     * 計算微信支付的簽名
     * @param obj
     * @return
     * @throws IllegalAccessException
     */
    public static String getPaySign(Object obj) throws IllegalAccessException, IOException {
        StringBuilder sb = new StringBuilder();
        //把物件轉為TreeMap集合(按照key的ASCII 碼從小到大排序)
        TreeMap<String, Object> map;
        if(!(obj instanceof Map)) {
            map = ObjectUtils.objectToMap(obj);
        }else{
            map = (TreeMap)obj;
        }
        Set<Map.Entry<String, Object>> entrySet = map.entrySet();
        //遍歷鍵值對
        for (Map.Entry<String, Object> entry : entrySet) {
            //如果值為空,不參與簽名
            if(entry.getValue()!=null) {
                //格式key1=value1&key2=value2…
                sb.append(entry.getKey() + "=" + entry.getValue() + "&");
            }
        }
        //最後拼接商戶的API金鑰
        String stringSignTemp = sb.toString()+"key="+WeChatUtil.KEY;
        //進行md5加密並轉為大寫
        return SecurityUtil.MD5(stringSignTemp).toUpperCase();
    }

三.支付頁面,展示付款二維碼
效果如下:

叩丁狼教育.png

 

頁面程式碼:

<div class="mod_layer_wxopen" style="display:block;">
    <iframe frameborder="0" class="iframe_mask"></iframe>
    <div class="ly_content">
        <div class="ly_bd cf">
            <div class="ly_ct">
                <div class="qr_list">
                    <h3 class="qr_tit">正在給微信帳號<span class="user js_wx_name">H</span><span class="js_txt">開通</span>VIP會員</h3>
                    <div class="qr_pic">
                        <img id="qr_img"  src="/getQrCode.do?orderNumber=${orderNumber}">
                        <span class="icon_wx"></span>
                    </div>
                    <p class="qr_txt js_qr_txt">使用微信掃描二維碼</p>
                    <p class="qr_tips">請使用微信掃碼支付</p>
                </div>
            </div>
        </div>
    </div>
</div>

重點的是img標籤,二維碼圖片是經過controller去找的。

獲取付款二維碼:

    @RequestMapping("getQrCode")
    public void getQrCode(String orderNumber,HttpServletResponse response) throws IOException {
        //從磁碟中獲取付款二維碼並輸出給response
        File file = new File(QRCodeUtil.PAY_PATH,orderNumber+".jpg");
        if(file.exists()){
            IOUtils.copy(new FileInputStream(file),response.getOutputStream());
        }
    }

有了付款二維碼,客戶就可以使用微信掃碼,並支付了。

四.支付結果的處理
當用戶支付後,微信會把支付結果傳送到我們之前指定的notify_url地址,我們可以根據支付結果來做相關的業務邏輯

把微信支付通知結果封裝成實體類

@Setter
@Getter
@XmlRootElement(name="xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class WxPayResultEntity {
    private String return_code;
    private String result_code;
    private String return_msg;
    private String transaction_id;//微信支付訂單號
    private String out_trade_no;//商戶訂單號
    private String total_fee;//訂單金額
    private String cash_fee;//現金支付金額
}

具體支付通知結果的引數可參考官方文章:
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8

接收支付結果並處理業務:

    @RequestMapping("receive")
    @ResponseBody
    public BaseResult receive(@RequestBody WxPayResultEntity result) throws IOException {
        //判斷是否支付成功
        if(result.getReturn_code().equals("SUCCESS")&&result.getResult_code().equals("SUCCESS")){
            //避免結果出現差異,安全起見,會再呼叫預支付訂單查詢的介面,檢查該訂單的狀態是否是已支付
            //程式碼省略
            //.....
        }
        //通知微信我們收到了,如果微信沒有收到回覆,會間隔一段時間又通知一遍,這樣的話容易出現業務重複處理操作
        BaseResult resp = new BaseResult();
        resp.setReturn_code("SUCCESS");
        resp.setReturn_msg("OK");
        return resp;
    }

主要邏輯是:
判斷微信返回的支付結果是否支付成功,如果是支付成功,還應呼叫查詢預支付訂單的介面,再次確認支付結果,如果確認無誤,我們就可以執行支付成功的業務邏輯,比如設定業務訂單狀態為已付款,商品發貨,或者設定為VIP會員等等,最後需要給微信返回應答,通知微信我們收到並處理了這個結果,如果微信沒有收到我們的回覆,會間隔一段時間又再次通知一遍,這樣的話容易出現業務重複處理的問題,可能導致商家資金損失。

查詢預支付訂單參考:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_2
處理支付結果參考:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7&index=8

五.頁面輪詢檢查支付結果
為了使用者體驗更好,頁面需要定時去獲取支付的結果,及時跳轉頁面或者顯示支付結果給客戶。

頁面帶上訂單號輪詢檢查:

  <script type="text/javascript">
        //每兩秒檢查一次
        setInterval(function() {
            $.get("/checkOrder.do?orderNumber=${orderNumber}",function(data) {
                if (data.success) {
                   alert("支付成功!");
                }
            });
        },2000)
  </script>

後臺查詢該訂單的支付狀態:
在該方法中,應查詢資料庫,檢查該業務訂單是否為已支付的狀態,如果是,返回success:true,頁面接收到結果,即可馬上提示支付成功或者跳轉到我的訂單頁面之類的業務處理。

    @RequestMapping("checkOrder")
    @ResponseBody
    public JSONObject checkOrder(String orderNumber) throws IOException {
        //檢查訂單狀態,確認已支付,返回success:true
        //邏輯省略....
        JSONObject json = new JSONObject();
        json.put("success",true);
        return json;
    }

想獲取更多技術乾貨,請前往叩丁狼官網:http://www.wolfcode.cn/all_article.html