公眾號支付流程
- 在支付授權(quán)目錄調(diào)用統(tǒng)一下單 API 獲取預(yù)付單信息
prepay_id
- 調(diào)用
wx.chooseWXPay
發(fā)起支付請求 - 在回調(diào)目錄中響應(yīng)微信支付通知饼记,并執(zhí)行相關(guān)業(yè)務(wù)(保存交易記錄和商戶系統(tǒng)連接起來等等)
統(tǒng)一下單
先把配置做出來
const wxPayCfg = {
"appid": "wx1234675",
"mch_id": "100001",
"device_info": "WEB",
"nonce_str": "",
"body": "公眾號-商品",
"out_trade_no": "", //商家訂單號
"total_fee": 0,
"spbill_create_ip":"", //終端 ip --用戶IP
"notify_url": "http://yourserver.com/wxpay/notice/", //回調(diào)地址
"trade_type": "JSAPI",
"openid": "" //公眾號支付必須
}
微信支付有嚴格的授權(quán)目錄要求蔚舀,不能超過三個,所以我的選擇是將所有支付發(fā)起都定在一個中間頁上携龟,通過傳參確定支付的訂單號和發(fā)起的用戶,價格也可以通過參數(shù)傳入,當然也可以通過自己商家系統(tǒng)來查詢獲得垢粮。服務(wù)端代碼大致如下
發(fā)起微信支付請求需要傳遞的參數(shù)要求是這樣的
wx.chooseWXPay({
timestamp: 0, // 支付簽名時間戳采幌,注意微信jssdk中的所有使用timestamp字段均為小寫劲够。但最新版的支付后臺生成簽名使用的timeStamp字段名需大寫其中的S字符
nonceStr: '', // 支付簽名隨機串,不長于 32 位
package: '', // 統(tǒng)一支付接口返回的prepay_id參數(shù)值休傍,提交格式如:prepay_id=***)
signType: '', // 簽名方式征绎,默認為'SHA1',使用新版支付需傳入'MD5'
paySign: '', // 支付簽名
success: function (res) {// 支付成功后的回調(diào)函數(shù)
}
});
備注:prepay_id 通過微信支付統(tǒng)一下單接口拿到磨取,paySign 采用統(tǒng)一的微信支付 Sign 簽名生成方法人柿,注意這里 appId 也要參與簽名,appId 與 config 中傳入的 appId 一致忙厌,即最后參與簽名的參數(shù)有appId, timeStamp, nonceStr, package, signType凫岖。
// 假定你的支付授權(quán)目錄為 http://yourserver.com/wxpay/
let wxpayController = async (ctx, next)=> {
const { orderNum } = ctx.query
let amount = await getOrderAmount(orderNum) //假定這是一個拿訂單價格的方法
let unifiedOrderResult = await invokeUnifiedOrder.call(ctx, amount)
if (unifiedOrderResult.return_code == 'SUCCESS' && unifiedOrderResult.result_code== 'SUCCESS' ) {
//統(tǒng)一下單請求成功后處理支付請求的配置
//這里有一點坑的地方是,這邊傳遞的參數(shù)是駝峰寫法逢净,而上面統(tǒng)一下單請求是全小寫
let payCfg = {
appId: wxPayCfg.appid,
timeStamp: parseInt(new Date().getTime() / 1000)+'',
nonceStr: createNonceStr(20),
package: 'prepay_id='+unifiedOrderResult.prepay_id
signType: 'MD5'
}
payCfg.paySign = generateSign(payCfg)
//這里就拿到支付請求的配置項哥放,渲染到客戶端就好了
await ctx.render('paypage', {
//渲染參考歼指,這里用的是 `koa-ejs`模塊,自己看一下實現(xiàn)
//客戶端需要渲染一個 `wx.config` 和 `wx.chooseWXPay`
payCfg
})
}
}
這里用到了一個createNonceStr
生成隨機字符甥雕,實現(xiàn)如下踩身。另外generateSign
為生成簽名方法,具體的實現(xiàn)在更下面社露,ctrl+f
用起來吧惰赋。
/**
*@params len Number 需要的長度
*@return nonce_str String 字符串
*/
function createNonceStr(len=32) {
let str = 'abcdefghijklmnopqrstuvwxyz'
str += str.toUpperCase()
str += '0123456789'
let arr = Array(len).fill('')
let strArr = arr.map(item=>str[Math.floor( Math.random()*str.length ) ])
return strArr.join('')
}
后端拿到請求的參數(shù) openid, orderNum, amount 的之后調(diào)用統(tǒng)一下單 API ,這里實現(xiàn)的方法是 invokeUnifiedOrder.call(ctx)
方便拿到 ctx.query
傳參的值呵哨,統(tǒng)一下單方法大致長這樣
/**
*@params amt Number 訂單價格
*@return Promise 請求下單API的Promise
*/
function invokeUnifiedOrder(amt) {
let clientIp = this.request.ip.match(/\d+\.\d+\.\d+\.\d/)[0]
let obj = Object.assign({}, wxPayCfg)
obj.total_fee = amt
obj.nonce_str = createNonceStr()
obj.out_trade_no = this.query.orderNum
obj.openid = this.query.openid
obj.spbill_create_ip = clientIp
obj.sign = generateSign(obj)
let postXml = renderUnifiedPostXml(obj)
let rp = require('request-promise')
let opotions = {
uri: 'https://api.mch.weixin.qq.com/pay/unifiedorder',
method: 'POST'
body: postXml
}
return rp(options)
}
簽名算法
這個請求包含了兩個方法generateSign
用于生成簽名 赁濒。renderUnifiedPostXml
用于將對象轉(zhuǎn)為微信規(guī)范的Xml,我是用 ejs 模塊渲染孟害,更簡單的方法是直接拼接字符串拒炎,這里就不實現(xiàn)了,主要實現(xiàn)一個這個簽名的算法挨务。
//簽名算法中最重要的是將對象排序拼接成字符串击你。
function generateSign(obj) {
let string = raw(obj) //將對象排序拼接為字符串
return md5(string).toUpperCase()
}
function raw(obj, lowerCase=false) {
//拼接字符串的方法,這里比較重要谎柄,微信有個官方的實現(xiàn)方法丁侄。
//但是有點點坑,復(fù)用性也不是太強朝巫,我稍微修改了一下鸿摇。
//這個方法可以用在發(fā)送的數(shù)據(jù),也可以用在接收的數(shù)據(jù)上
//要點① 字典排序
//要點② 值為空的屬性不參與簽名
//要點③ sign 參數(shù)不參與簽名
//要點④ 真的要認認真真看文檔
var keys = Object.keys(args);
keys = keys.sort()
var newArgs = {};
keys.forEach(function (key) {
if(key !='sign' && args[key] && ((''+args[key]).length > 0)) {
if(lowerCase) {
newArgs[key.toLowerCase()] = args[key];
} else {
newArgs[key] = args[key];
}
}
});
var string = '';
for (var k in newArgs) {
string += '&' + k + '=' + newArgs[k];
}
string = string.substr(1);
return string;
}
function md5(str) {
//對字符串進行 md5 加密,返回簽名
let crypto = require('crypto')
let decipher = crypto.createHash('md5');
return decipher.update(str).digest('hex')
}
接收支付通知
在已經(jīng)實現(xiàn)簽名算法的前提下劈猿,其實支付通知倒是非常簡單的一步拙吉,無非是路由設(shè)置好,接收到請求后校驗一下簽名和金額之類的揪荣。大致寫一下這個中間件吧筷黔。
這里順便提一下,Koa社區(qū)中接收 Post 請求的參數(shù)仗颈,如果參數(shù)類型是 json
佛舱、form
、text
挨决,一般是都是用 koa-bodyparser 這個中間件请祖,解析 xml
有一個中間件是 koa-xml-body,我用的不多凰棉,比較常用的是 raw-body损拢。話說這些優(yōu)秀的中間件陌粹,無一例外都是下載量極高撒犀,由于足夠穩(wěn)定和優(yōu)秀,反而 star 很少。
接收支付通知有三個步驟需要做的
- 校驗簽名或舞,防止收到偽造的消息之類的荆姆。
- 檢查這個通知是否已經(jīng)處理過,微信有可能會多次通知同一個支付結(jié)果映凳。如果處理過直接返回結(jié)果成功胆筒。
- 數(shù)據(jù)鎖做并發(fā)控制(我暫時沒有實現(xiàn))
- 校驗完金額,返回結(jié)果诈豌。
const getRawBody = require('raw-body')
const wxPayNotice = async (ctx, next) => {
let receivedXml = await getRawBody(ctx.req,{
length:ctx.req.headers['content-length'],
limit:'1mb',
encoding:ctx.charset
})
let receivedObj = await parseXmlToJs(receivedXml)
if (receivedObj.return_code == 'FAIL') {
ctx.status = 200
ctx.body = '<xml><return_code><![CDATA[FAIL]]></return_code></xml>'
return;
}
let checkSign = generateSign(receivedObj) //這個方法已經(jīng)封裝成自動剔除 sign 參數(shù)了仆救。
if (checkSign !== receivedObj.sign ) return;
if(receivedObj.result_code == 'FAIL') {
//支付失敗的情況,日志記錄一下矫渔,返回結(jié)果就好
logger(`pay error!! ${receivedObj.transaction_id}: { errcode:${receivedObj.err_code},err_desc:${receivedObj.err_code_des} }` )
ctx.status = 200
ctx.body = '<xml><return_code><![CDATA[FAIL]]></return_code></xml>'
return;
}
let isRepeatedNotic = await databaseExist(receivedObj.transaction_id)
if(isRepeatedNotic) {
ctx.status = 200
ctx.body = '<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>'
return
}
let returnCode='SUCCESS', returnResult='OK';
let replyXmlTpl = '<xml>'+
'<return_code><![CDATA[%returnCode%]]></return_code>'+
'<return_msg><![CDATA[%returnResult%]]></return_msg>'+
'</xml>'
let checkResult = checkWxPayResult(receivedObj) //根據(jù)訂單號什么的去看看金額等是不是正確的彤蔽,和自己的商戶系統(tǒng)查詢,這個自己按情況做就好了
if (!checkResult) { returnCode = 'FAIL'; returnResult= '支付結(jié)果校驗失敗'}
let replyXml = replyXmlTpl
.replace(/%returnCode%/, returnCode)
.replace(/%returnResult%/庙洼,returnResult)
ctx.status = 200
ctx.body = replyXml
}
到這里就寫完了顿痪,至于這個數(shù)據(jù)鎖的問題,我還要研究一下是應(yīng)該在數(shù)據(jù)庫操作還是在服務(wù)端操作油够,學(xué)習(xí)中蚁袭。不好意思啦