微信支付之小程式支付
微信的支付方式有以下幾種,不同的支付方式适用于不同的支付場景,而今天要給大家講的就是 小程式支付 方式
說到支付功能就要涉及到金錢交易,必定是有比較嚴格的規範及流程,如要求小程式必須具備企業性質,必須擁有微信支付商戶平台的賬号
PS:申請微信支付商戶平台需要一個微信小程式或公衆号等,建議按照以下流程進行操作
準備工作
1、申請微信小程式賬号
申請成功可拿到 AppID(小程式 id)和 AppSecret(小程式密鑰)
申請類型為企業性質,否則無法接入微信支付
2、微信小程式認證
通過認證的小程式才能接入微信支付和綁定商戶平台
3、申請商戶平台賬号
需要第一步申請的 AppID
申請成功可拿到 MchID(商戶 id)和 MchKey(商戶密鑰)
4、信小程式關聯商戶号
微信和商戶都認證成功後,在微信背景微信支付菜單中進行關聯
5、接入微信支付
在微信背景微信支付菜單中進行接入
小程式支付流程
簡要支付流程如下:
- 使用者發起支付請求
- 後端調用統一下單接口得到 prepay_id
- 把支付所需參數傳回前端
- 前端調用支付接口進行支付操作
- 支付結果通知
- 前端根據不同的支付結果給使用者不同的提示
PS:難點在第 2、3、5 步,一定要仔細檢視相關接口文檔,否則容易出錯,接下來我們按照以上 6 個步驟詳細講解在微信小程式中的支付流程
支付前的操作
因為嚴格意義上來說這不屬于支付流程中的步驟,但支付過程中需要用到使用者唯一辨別openid,是以建議在使用者進入小程式時就進行這一步的操作
- 調用wx.login()接口擷取 code,并把code傳到伺服器
-
後端伺服器拿到 code 後調用code2Session 接口擷取 openid 和 session_key
建議把openid存入資料庫,友善随時擷取,下面的步驟也會用到
- 後端伺服器保好 appid, secret, mch_id, mch_key(這些資料分别在小程式背景和商戶平台中獲得,我是把它們做成 commonjs 子產品并儲存在config/wx.js檔案中以友善調用)
PS:開發者需要自行維護使用者登入狀态(使用者登入狀态的維護本文不做展開,請自行查閱相關資料)
1、小程式端:使用者向商戶伺服器發起支付請求
這步沒什麼好說的,當使用者點選支付按鈕時,給我們自己的後端接口發起一個請求,攜帶必要的參數(如:body,total_fee 等),接口位址需要自行編寫,如我的接口位址為/payment/order
// http對象為wx.request()的二次封裝import http from "../utils";// 向後端發請請求const res = await http.post("/payment/order", { body: "騰訊QQ-購買會員", // 商品描述 total_fee: 998, // 總金額,機關為分});if (res.status === 200) { try { // 得到接口傳回的資料,向微信發起支付 const result = await wx.requestPayment({ ...res.data, }); wx.showToast({ title: "支付成功", }); console.log("支付結果:", result); } catch (err) { wx.showToast({ title: "支付失敗", }); }}
PS:可能會有小夥伴産生疑惑,為什麼不直接通過 wx.requestPayment() 在小程式端發起請求而要先請求商戶自己的伺服器呢?原因很簡單,安全性問題,wx.requestPayment()需要 2 個重要參數paySign和package,需要 appid,secret,openid,mch_key 等私密資料,這些私密的資料不應該在前端暴露出來,而是放在自己的伺服器中更安全,是以需要向自己的伺服器發起這個請求拿到這些參數,下一步才能真正發起支付。接下來我們來看看後端是如果操作的
2、商戶後端伺服器:簽名+生成預支付辨別
後端代碼使用 egg 架構(基于 NodeJS+Koa)實作,文中涉及到 egg 用法和 koa 的用法不再額外說明,請自行查閱相關資料
調用統一下單接口擷取 預支付會話辨別 prepay_id
注意:該接口需要發送 xml 格式參數,同時傳回 xml 格式資料,需自行轉換(我使用的是xml-js第三方子產品)
-
- 該接口必填參數:appid,mch_id,nonce_str,sign_type,body,out_trade_no,total_fee,spbill_create_ip,notify_url,trade_type,sign,其中 sign 為前面所有參數加密後的字元
async order(ctx) { // egg架構寫法 const { service, request } = ctx; // 擷取前端傳入參數 const { userid, total_fee, body } = request.body; // 引入微信配置參數(上面準備工作中儲存的config/wx.js檔案,包含小程式id,密鑰,商戶id,商戶密鑰) const { config } = require("../../config/wx"); // 生成訂單号(保證唯一性:我采用時間戳拼6位随機數的方式) const tradeNo = Date.now() + '' + randomCode(100000, 999999); // 統一下單簽名參數 const orderParams = { appid: config.appid, // 小程式id mch_id: config.mch_id, // 商戶id nonce_str: service.wx.randomStr(), // 自定義生成随機字元方法 sign_type: "MD5", // 加密類型 body, // 商品簡單描述,有格式要求 out_trade_no: tradeNo, // 訂單号 total_fee, // 機關:分 spbill_create_ip: "121.34.253.98", // 伺服器ip notify_url: "https://你的伺服器域名/payment/wxnotify", // 支付成功通知位址 trade_type: "JSAPI", // 支付方式(小程式支付選JSAPI) openid: user.openid, // 使用者openid,步驟0儲存的資料 }; // 簽名:對上面所有參數加密(簽名算法請檢視接口文檔,下同) const orderSign = service.wx.sign(orderParams); // json->xml const xmlData = convert.js2xml( { xml: { ...orderParams, sign: orderSign } }, { compact: true } ); // 調用統一下單接口(接口沒說明,但必須為post請求) const { data } = await ctx.curl( "https://api.mch.weixin.qq.com/pay/unifiedorder", { method: "post", data: xmlData, } ); // xml->js const result = convert.xml2js(data, { compact: true }); if (result.prepay_id) { // 此處可以把訂單資訊儲存到資料庫 // 傳回prepay_id後,接着就是把參數傳回前端 // =>為了更清晰,我把這裡的代碼寫在下一步 // ... } }
3、給前端傳回支付參數+簽名
// 支付簽名參數 const payParams = { appId: config.appid, // 商戶 id timeStamp: Date.now(), // 時間戳 nonceStr: this.randomStr(), // 随機字元 package: "prepay_id=" + result.prepay_id, //預支付會話辨別(格式為:prepay_id=統一下單接口傳回資料) signType: "MD5", //簽名類型(必須與上面的統一下單接口一緻) }; // 簽名 const paySign = service.wx.sign(payParams); // 把參數+簽名傳回給前端 ctx.body = formatData({ data: { timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: payParams.signType, paySign, }, });
附上封裝好的簽名方法sign()和生成随機字元串的方法randomStr(),我寫在service/wx.js
"use strict"; const { Service } = require("egg"); const crypto = require("crypto"); // 微信基本配置 const { weapp } = require("../../config/wx"); class wxService extends Service { randomStr(len = 24) { const str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let result = ""; for (let i = 0; i < len; i++) { result += str[Math.floor(Math.random() * str.length)]; } return result; } sign(data, signType = "MD5") { const keys = []; for (const key in data) { if (data[key] !== undefined) { keys.push(key); } } // 字典排序=>key=value const stringA = keys .sort() .map((key) => `${key}=${decodeURIComponent(data[key])}`) .join("&"); // 拼接商戶key const stringSignTemp = stringA + "&key=" + weapp.mch_key; console.log("stringSignTemp", stringSignTemp); // 加密 let hash; if (signType === "MD5") { hash = crypto.createHash("md5"); } else { hash = crypto.createHmac("sha256", "laoxie"); } hash.update(stringSignTemp); const paySign = hash.digest("hex").toUpperCase(); return paySign; } } module.exports = wxService;
4、小程式端:向微信伺服器發起請求
第 1 步的資料傳回後,向微信伺服器接口wx.requestPayment()發請求,喚起支付界面,請檢視第一步 try...catch 中的代碼
5、微信伺服器:支付結果通知
在第 2 步向統一下單接口發起請求時附帶了一個notify_url,此位址一定要是可外網通路的接口位址(商戶自行編寫),由微信伺服器調用該接口,不管支付成功與否,此接口都會調用,并傳回相應資料(檢視接口資料),是以商戶可以在此接口中編寫相關業務邏輯、如支付成功後寫入資料庫等操作
注意:商戶需要在此接口中做接收處理,并向微信伺服器傳回應答(按接口規範傳回特定資料)。如果微信收到商戶的應答不是成功或逾時,微信會認為通知失敗,微信會通過一定的政策定期重新發起通知,通知頻率為:15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h,但微信不保證通知最終一定能成功。
"use strict"; const Controller = require("egg").Controller; const getRawBody = require("raw-body"); const contentType = require("content-type"); const { formatData, randomCode, params, formatParams } = require("../utils"); class PaymentController extends Controller { // 微信支付回調位址 async notify(ctx) { const { req } = ctx; // 微信調用該接口時傳入的資料為xml,是以先轉換 const data = await getRawBody(req, { length: req.headers["content-length"], limit: "1mb", encoding: contentType.parse(req).parameters.charset, }); const result = params.xml2js(data); // 驗簽:微信傳入的除sign外的所有資料進行簽名,拒後與sign進行對比是否一緻 // 一緻說明支付成功,否則支付失敗 // 并根據不同的結果通知微信伺服器(響應不同的xml資料,如下) const resultSign = result.sign; delete result.sign; const mySign = ctx.service.wx.sign(result); console.log("sign:", resultSign, mySign); ctx.set("content-type", "text/plain"); if (resultSign === mySign) { // 修改商戶訂單狀态 const { device_info, openid, trade_type, bank_type, total_fee, settlement_total_fee, fee_type, transaction_id, time_end, attach, } = result; // 格式化自定義參數 let myattach = {}; if (attach) { myattach = params.parse(attach); } // 格式化支付時間:20200423161017=>2020/04/23 16:10:17 let pay_time = time_end.replace( /(d{4})(d{2})(d{2})(d{2})(d{2})(d{2})/, "$1/$2/$3 $4:$5:$6" ); pay_time = new Date(pay_time); // 根據訂單号更新資料庫中的訂單狀态 const newData = { device_info, openid, trade_type, bank_type, total_fee, settlement_total_fee, fee_type, transaction_id, pay_time, status: 1, ...myattach, }; db.update( "purchase", { out_trade_no: result.out_trade_no, }, { $set: newData, } ); ctx.body = ``; } else { ctx.body = ``; } } } module.exports = PaymentController;
附上以上代碼中會用的封裝好的方法parse()、xml2js()、js2xml(),我寫在utils/index.js中
const params = { parse(queryString) { // 'a=1&b=2' => {a:1,b:2} return queryString.split("&").reduce((res, item) => { const arr = item.split("="); res[arr[0]] = arr[1]; return res; }, {}); }, js2xml(data) { return convert.js2xml({ xml: data }, { compact: true }); }, xml2js(xml) { const result = convert.xml2js(xml, { compact: true, textKey: "value", cdataKey: "value", }).xml; const data = {}; for (const key in result) { data[key] = result[key].value; } return data; }, }; module.exports = { params }
到此微信支付之小程式支付就完成了,過程比較繁雜,一定要一步步去實作,也許會踩坑,但相信我,這是每個程式員的必經這路,面對它,勇敢地走過去,你對能到達勝利的彼岸。
注意事項
- appid、appsecret、mchid、mchkey、openid 為小程式或商戶私密資訊,應儲存在服務端
- 注意參數大小寫:每個接口大小寫可能不同
- 簽名算法:請檢視接口文檔
- 一定要注意看文檔,根據我多冷踩坑的經曆,90%以上的問題都是沒有仔細看文檔所緻
參考網址與接口
- 微信支付商戶平台:https://pay.weixin.qq.com
- 微信公衆平台:https://mp.weixin.qq.com
- 微信支付接口:https://developers.weixin.qq.com/miniprogram/dev/api/open-api/payment/wx.requestPayment.html
- 統一下單接口:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1
- 支付結果通知接口:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_7&index=8
- 簽名算法:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3
[mp] https://mp.weixin.qq.com
[pay] https://pay.weixin.qq.com
[payment] https://developers.weixin.qq.com/miniprogram/dev/api/open-api/payment/wx.requestPayment.html
[notify] https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_7&index=8
[login] https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html
[unifiedorder] https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1
[code2session] https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html
[sign] https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3