微信公衆號支付開發手記(node)

微信支付

前言

總結一下最近業務開發中對微信公衆號支付的開發過程,微信支付的開發前提是已經具有可上線微信公衆號開發的基礎上進行的,若是你的開發階段目前停留在起步,建議參考這篇文章開始php

好了,來聊一聊微信支付。不管是今天的分享,仍是網上其餘的分享,開頭老是在吐槽微信的文檔。我也不例外,剛開始老是以爲文檔寫的不夠具體,寫的模棱兩可。後來發現一個是本身太浮躁,不能沉下心去分析文檔的細節,另外一方面是習慣性先去網上找相關的教程,而後發現教程傳遞過來的第一感覺就是——微信開發是個大坑。css

網上的經驗分享仍是頗有幫助的,可是首先要清楚地明白微信支付的整個流程以及本身目前的進展,這樣纔能有目的的去找到本身須要的東西。html

微信支付開發和微信其餘功能開發有一個共同點,就是須要耐心。而這彷佛也是微信團隊的初衷,經過零散的文檔,不清晰的說明來過濾掉一批缺少耐心的程序員(幫微信團隊圓個場)。因此開始接觸微信開發以前,會有人告訴你這裏會有不少坑,會怎麼樣怎麼樣,其實徹底不用擔憂,由於這裏的坑並非在考驗技術。前端

關於微信支付的調試過程,因爲微信支付對安全要求較高,不能作內網穿透本機測試,因此須要先將項目部署到線上,開發調試不是很方便。可能會有一些本地支付沙箱之類的工具,我沒有去研究,但願有作過此類工做的小夥伴留個言提醒一下。並且微信支付是不可以經過微信開發者工具測試的,只能在真機上跑。若是在開發者工具上遇到報錯,不妨在手機上跑一下。node

微信支付文檔分析

無論什麼開發,都要從官方文檔開始。關於公衆號支付的部分,須要關注文檔的兩個部分,一個是「公衆號支付」一個是「API列表」,其餘部分不是重點。git

公衆號支付程序員

  • 場景介紹
  • 案例介紹
  • 開發步驟
  • 業務流程
  • 獲取微信版本號
  • 微信內H5調起支付
  • 收穫地址共享
  • 支持常見問題

這一級的目錄,重點是業務流程微信內H5調起支付,由於這兩塊內容搞明白了,整個支付流程就清晰了。github

首先須要瞭解一下公衆號支付的具體場景,也就是須要閱讀第一部分「場景介紹」,瞭解一下這個場景是否符合具體業務。算法

若是這就是你要的,那就來到重點了,也就是「業務流程」。數據庫

看到業務流程的流程圖,估計有計算機專業背景的朋友會很熟悉,也很容易理解。不理解也不要緊,我這裏準備了一副含有分析過程的流程圖,結合實際業務,來幫助理解。

微信支付流程圖分析

先來看黃色的甬道,這兩塊也就是實際的前臺頁面和後臺服務,咱們的閱讀順序是自上而下,流程圖中的文字是微信官方提供的,右邊的說明文字是我根據業務寫的,看哪部分均可以。我把整個流程用顏色分爲了三大塊幫助理解。

從紅色部分開始,紅色部分主要工做是後臺生成預付款單,而後經過回調信息將內容發送到前臺。

接下來是藍色部分,這一部分包含兩塊,一個是付款前,一個是付款後。

首先前臺拿到紅色部分由後臺發來的信息,而後再微信內H5頁面調起支付,此時頁面的付款都是由微信來控制。

付款結束後,會發送兩個回調,一個發送給後臺服務,也就是圖片藍色部分中的綠色區塊,告訴後臺具體哪一個訂單如今的完成狀態。另外一個發送給客戶前端,通知前端交易狀態。

從這部份內容瞭解到,須要先了解統一下單API,以後是微信H5內調起支付,而後處理支付結果通知

開始開發

準備

微信服務配置

開始開發前,須要對現有項目設置支付目錄和設置受權域名,具體能夠參考這裏另外須要注意的是,也是微信文檔裏沒有提到的地方。須要在微信支付平臺設置API密鑰。須要提醒的是,微信支付平臺的配置須要超級管理員帳號登陸才能夠進行配置操做。

微信支付API密鑰

收集信息

上一步配置完成後,須要收集一些信息爲後續開發作準備。

  • token 微信公衆號後臺取得
  • appid 微信公衆號後臺取得
  • appsecret 微信公衆號後臺取得
  • encodingAESKey 微信公衆號後臺取得
  • mch_id 商戶號(微信支付平臺取得)
  • notifyUrl 微信支付回調地址(服務端後臺接口:POST)
  • partnerKey 微信支付API密鑰(微信支付平臺取得)

準備好以上信息後,就能夠開始着手寫代碼了。

開工

首先,須要準備一個前臺界面,模擬用戶訪問商品頁面點擊購買。

image

後端部分

後臺這邊,node開發微信支付有不少現成的封裝庫可使用,這裏使用wechat-pay

首先在項目開始處初始化wechat-pay

下單
const Payment = require('wechat-pay').Payment;
const initConfig = {
  partnerKey: config.wechat.partnerKey,
  appId: config.wechat.appid,
  mchId: config.wechat.mch_id,
  notifyUrl: config.wechat.notifyUrl,
};
const payment = new Payment(initConfig);
複製代碼

而後編寫前臺界面用戶點擊購買後的接口業務代碼:

async genAdvanceOrder(ctx) {
    try {
      // 1. 經過前臺發來的商品ID查詢商品
      const { user } = ctx.state;
      const { product_id, comment, school_id } = ctx.request.body;
      const product = await prodDao.findOneProduct(product_id);

      if (product.length <= 0) {
        return ctx.body = new Error(C.ERROR_CODE.QUERY_EMPTY, '沒有找到商品');
      }
      
      
      // 2. 經過查詢結果填寫預付款單
      // 獲取client ip地址
      const clientIp = getClientIp(ctx.req);
      const order = {
        body: product[0].course_title,        // 商品描述
        attach: product[0].comment,           // 商品附加數據
        out_trade_no: UUID.v1().replace(/\-/g, ''),   // 商戶系統內部訂單號,本身生成32位隨機串 unique
        total_fee: product[0].price,          // 費用(單位:分) 
        spbill_create_ip: clientIp,           // 客戶端IP
        openid: user.openid,                  // 用戶openid
        trade_type: 'JSAPI'
      };

      // 3. 根據預付款單回調結果往數據庫插入數據(判斷錯誤碼,修改訂單狀態)
      // 向微信請求生成預付款單
      let payargs = await payment.getBrandWCPayRequestParams(order);
      await orderDao.add({
        user_id: user.id,
        school_id: school_id, // TODO: 增長並分配學校ID,業務邏輯須要變更
        scene: product[0].scene,
        product_id: product_id,
        price: product[0].price,
        product_price: product[0].price,
        status: C.PAY_STATUS.NO_PAY,
        wx_open_id: user.openid,
        wx_out_trade_no: order.out_trade_no,
        wx_prepay_id: payargs.package.split('=')[1], // 取prepay_id
        comment: comment
      });

      // 4. 發送預付款單內容
      ctx.body = new Success(payargs);

    } catch (e) {
      ctx.body = new Error('', '未知錯誤', e)
    }
  }
  
function getClientIp(req) {
    const ip = req.headers['x-forwarded-for'] ||
    req.connection.remoteAddress ||
    req.socket.remoteAddress ||
    req.connection.socket.remoteAddress;
    return ip.replace(/:|\wf/g, '');
}
複製代碼

這裏須要注意的是填寫預付款單這裏的操做。

out_trade_no是要本身生成32位隨機字符串,至關因而保存在本身數據庫中的訂單惟一值,之後在微信回執付款信息時也會用到這個字段。

total_fee的單位是分,在開發過程當中,也應當使用分做爲數據庫價錢的單位,這樣能夠有效避免浮點數精讀損失問題。

spbill_create_ip是用戶端下單設備的ip,是必填項,微信那邊處於安全要求每份訂單都必需要填寫。這個也很好獲取(上面代碼中貼出來了),只不過要作一些處理,由於直接經過koa ctx.ip獲取的地址可能會被Nginx或者其餘服務器配置服務轉發成127.0.0.1。這就不是咱們須要的真實的客戶端ip。最後要處理字符串前綴,通常直接拿到ip的格式是:fff:54.00.00.1

支付通知

這裏就是以前的流程圖中,藍色區塊中的綠色部分。能夠對比流程圖理解支付流程。

微信開發中,大量來自微信發送的通知都是xml格式,因此爲了方便使用,須要先增長如下中間件來幫助開發。

const bodyParser = require('koa-bodyparser');
const xmlParser = require('koa-xml-body');

app.use(xmlParser({
  key: 'body'
}));
// app.use(bodyParser());
app.use(bodyParser({
  enableTypes: ['json', 'form', 'text'],
  extendTypes: {
    text: ['text/xml', 'application/xml']
  }
}));
複製代碼

以後是通知部分的代碼:

async wxPayNotify(ctx) {
    // TODO: 安全驗證 簽名驗證 並校驗返回的訂單金額是否與商戶側的訂單金額一致
    /* // 微信發送通知的內容 { appid: [ '**********' ], attach: [ '附加內容' ], bank_type: [ 'CFT' ], cash_fee: [ '1' ], fee_type: [ 'CNY' ], is_subscribe: [ 'Y' ], mch_id: [ '1498496372' ], nonce_str: [ '4KewHbQvsQPaGsaeoICLbKD1ySFDlPdL' ], openid: [ 'oZZUx0X2LSM1j652P6r2R*******' ], out_trade_no: [ '8ff9fdd0e33411e8a07c833c43c4e4e7' ], result_code: [ 'SUCCESS' ], return_code: [ 'SUCCESS' ], sign: [ '6A14538FE1651CECDFCDFE375383B9AA' ], time_end: [ '20181108165920' ], total_fee: [ '1' ], trade_type: [ 'JSAPI' ], transaction_id: [ '4200000235201811***********' ] } */
    try {

      // 1. 經過回調信息查詢訂單
      const content = ctx.request.body['xml'];
      const order = await orderDao.selectOne({ wx_out_trade_no: content['out_trade_no'][0] });

      if (!order) {
      // TODO: 處理查詢不到訂單的通知
      }

      // 2. 安全驗證,對比簽名和訂單金額
      if (checkWeChatPaySign(content) && order.price === parseInt(content['total_fee'][0])) {
        await orderDao.update({
          wx_notify_backup: JSON.stringify(content),
          status: C.PAY_STATUS.PAID,
          wx_transaction_id: content['transaction_id'][0]
        }, { wx_out_trade_no: content['out_trade_no'][0] })
      } else {
      // TODO: 處理驗證不經過的通知
      }

      // 3. 回調通知微信
      ctx.body = '<xml>' +
        '<return_code><![CDATA[SUCCESS]]></return_code>' +
        '<return_msg><![CDATA[OK]]></return_msg>' +
        '</xml>'
    } catch (e) {
    // TODO: 收款回調出錯通知
    }
  }
  
function checkWeChatPaySign(obj) {
  // 1.字典排序數據集合
  let arr = [];
  for (let [k, v] of Object.entries(obj)) {
    let string = '';
    // 排除 sign 字段
    if (k === 'sign') continue;
    string += k + '=' + v[0];
    arr.push(string);
  }
  // 按字典排序
  arr.sort();
  // 2.拼接上key獲得stringSignTemp字符串
  arr.push('key=' + config.wechat.partnerKey);
  const stringSignTemp = arr.join('&');
  const md5String = md5(stringSignTemp).toUpperCase();
  // 3.比較md5String 與 sign字段
  return md5String === obj.sign[0];
}
複製代碼

這裏比較很差理解的是驗證簽名,而微信文檔也沒有給出樣例代碼,因此比較混亂。並且拼簽名拼串驗證又容易出錯,多一個空格少一個字符都不同。這裏就得結合官方給出的簽名算法效驗工具耐着性子調試了。

前端部分

而後就回到咱們的前端部分。

因爲微信H5支付是基於騰訊瀏覽器的,因此只有在手機微信中或者開發者工具中打開的網址,才能調用到WeixinJSBridge

我這邊前端是拿Angular寫的,不過代碼不復雜,着重理解業務流程。

import { Component, OnInit } from '@angular/core';
import {UserService} from '../../../services/user.service';
import {OrderService} from '../../../services/order.service';
import {ActivatedRoute} from '@angular/router';

@Component({
  selector: 'app-wx-pay-test',
  templateUrl: './wx-pay-test.component.html',
  styleUrls: ['./wx-pay-test.component.scss']
})
export class WxPayTestComponent implements OnInit {
  wxBridge;
  logs = [];
  productId;

  constructor(
    private orderService: OrderService, // API
    private activateRouter: ActivatedRoute
  ) { }

  // 頁面初始化就會執行的鉤子函數, React 應該使用ComponentDidMount
  ngOnInit() {
    // 這裏是爲了獲取WeixinJSBridge
    if (typeof window['WeixinJSBridge'] === 'undefined') {
      if (document.addEventListener ) {
        document.addEventListener('WeixinJSBridgeReady', this.onBridgeReady, false);
      } else if (document['attachEvent']) {
        document['attachEvent']('WeixinJSBridgeReady', this.onBridgeReady);
        document['attachEvent']('onWeixinJSBridgeReady', this.onBridgeReady);
      }
    } else {
      this.onBridgeReady();
    }
    // 獲取url中的參數
    this.activateRouter.params.subscribe( params => {
      if (params.id) {
        this.productId = params.id;
      }
    });
  }

  onBridgeReady = () => {
    this.wxBridge = window['WeixinJSBridge'];
  }
  // 點擊購買觸發該函數
  genPreOrder(ev) {
    const that = this;
    // 一、向後臺發送請求 對應後臺 genAdvanceOrder
    this.orderService.genAdvanceOrder({
      product_id: this.productId
    }).then(data => {
      // 二、拿到服務端回執數據,調用invoke,請求微信支付
      that.wxBridge.invoke('getBrandWCPayRequest', data, function(res) {
        // 三、處理微信支付回執結果
        if (res.err_msg === 'get_brand_wcpay_request:ok') {
          // TODO: 顯示支付成功頁面
          alert('支付成功');
          // 這裏能夠跳轉到訂單完成頁面向用戶展現
        } else {
          // TODO: 顯示支付失敗頁面
          alert('支付失敗,請重試');
        }
      });
    }).catch(err => {
      console.log(err);
    });
  }
}
複製代碼

前端的代碼仍是相對容易理解的,前提是理解文章開頭部分的流程圖。知道每一步是處理什麼問題,須要幹什麼。

在調用微信支付後,返回的結果代碼中我只處理了支付成功的部分,可是回調還會有支付失敗、超時等等,就不一一列舉。

結語

文章到這裏,大概也就講清楚了微信公衆號支付的整個環節。可是在流程圖中最後一部分灰色區塊的業務沒有講,由於以爲前兩部分是最主要的,後面能夠自行理解流程圖,根據具體業務開發。

折騰微信支付這塊內容大概也有幾天了,總結一下整個開發流程,分享一下,但願可以幫助你們理解整個支付業務。

要說難嗎其實也不難,主要就是考察耐心吧。

相關文章
相關標籤/搜索