最近做了微信支付及退款一系列操作,微信文檔寫的也比較簡略,網上博客也並不詳細,也踩了一些坑,在這里記錄下。當然主要還是得根據微信小程序文檔一步一步來。
一、wx.requestPayment
發起微信支付。了解更多信息,請查看微信支付接口文檔
所謂的發起微信支付,指的是用戶側這邊喚起微信支付窗口的api,這個api需要按規范傳參數
wx.requestPayment({
timeStamp: '',
nonceStr: '',
package: '',
signType: 'MD5',
paySign: '',
success (res) { },
fail (res) { }
})
這些參數均需要從后台獲取。那么我們進入“微信支付接口文檔”查看是怎么個流程
二、微信支付具體流程
文檔也寫的很清楚,不細說,主要看下面這個流程
商戶系統和微信支付系統主要交互:
1、小程序內調用登錄接口,獲取到用戶的openid,api參見公共api【小程序登錄API】
2、商戶server調用支付統一下單,api參見公共api【統一下單API】
3、商戶server調用再次簽名,api參見公共api【再次簽名】
4、商戶server接收支付通知,api參見公共api【支付結果通知API】
5、商戶server查詢支付結果,api參見公共api【查詢訂單API】
1、調用wx.login獲取code,然后通過code,調取微信三方接口,獲取openid。如果用戶系統有openid記錄,可以省略這步操作。
主要是因為下面的統一下單api里的參數配置:
openid參數:trade_type=JSAPI,此參數必傳,用戶在商戶appid下的唯一標識。openid如何獲取,可參考【獲取openid】。
2、統一下單api、二次簽名api返回參數
看文檔里的參數,傳那些參數,調用微信三方接口即可。一般不會有啥問題,主要問題也會在於2次簽名。
實例代碼如下
// 統一下單
let unifiedorder = async (params = {}, ctx) => {
let body = '......' // 商品描述
let notify_url = 'https://....../wxPayBack' // 支付成功的回調地址 可訪問 不帶參數
let nonce_str = wxConfig.getNonceStr() // 隨機數
let out_trade_no = params.orderCode // 商戶訂單號(用戶系統自定義的商戶訂單號)
let total_fee = ctx.request.body.orderPay * 100 // 訂單價格 單位是 分
let bodyData = '<xml>'
bodyData += `<appid>${wxConfig.AppID}</appid>` // 小程序ID
bodyData += `<mch_id>${wxConfig.Mch_id}</mch_id>` // 商戶號
bodyData += `<body>${body}</body>` // 商品描述
bodyData += `<nonce_str>${nonce_str}</nonce_str>` // 隨機字符串
bodyData += `<notify_url>${notify_url}</notify_url>` // 支付成功的回調地址
bodyData += `<openid>${params.openid}</openid>` // 用戶標識(openid,JSAPI方式支付時必需傳該參數)
bodyData += `<out_trade_no>${out_trade_no}</out_trade_no>` // 商戶訂單號
bodyData += `<spbill_create_ip>${params.ip}</spbill_create_ip>` // 終端IP
bodyData += `<total_fee>${total_fee}</total_fee>` // 總金額 單位為分
bodyData += '<trade_type>JSAPI</trade_type>' // 交易類型 小程序取值:JSAPI
// 簽名(根據上面這些參數,有個簽名算法,文檔里也有描述)
var sign = wxConfig.paysignjsapi(
wxConfig.AppID,
body,
wxConfig.Mch_id,
nonce_str,
notify_url,
params.openid,
out_trade_no,
params.ip,
total_fee
);
bodyData += '<sign>' + sign + '</sign>'
bodyData += '</xml>'
// 微信小程序統一下單接口
var urlStr = 'https://api.mch.weixin.qq.com/pay/unifiedorder'
let option={
method:'POST',
uri: urlStr,
body:bodyData
}
let result = await rp(option)
let returnValue = {}
parseString(result, function(err,result){
if (result.xml.return_code[0] == 'SUCCESS') {
returnValue.out_trade_no = out_trade_no; // 商戶訂單號
// 小程序 客戶端支付需要 nonceStr,timestamp,package,paySign 這四個參數
returnValue.nonceStr = result.xml.nonce_str[0]; // 隨機字符串
returnValue.timeStamp = Math.round(new Date().getTime() / 1000) + '';
returnValue.package = 'prepay_id=' + result.xml.prepay_id[0]; // 統一下單接口返回的 prepay_id 參數值
returnValue.paySign = wxConfig.paysignjs(
wxConfig.AppID,
returnValue.nonceStr,
returnValue.package,
'MD5',
returnValue.timeStamp
) // 簽名
// emitToSocket(total_fee)
return ctx.response.body={
success: true,
msg: '操作成功',
data: returnValue
}
} else{
returnValue.msg = result.xml.return_msg[0]
return ctx.response.body={
success: false,
msg: '操作失敗',
data: returnValue
}
}
})
}
寫的一個微信支付的配置項
const cryptoMO = require('crypto') // MD5算法
/* 微信參數AppID 和 Secret */
const wxConfig = {
AppID: "......", // 小程序ID
Secret: "......", // 小程序Secret
Mch_id: "......", // 商戶號
Mch_key: "......", // 商戶key
// 生成商戶訂單號
getWxPayOrdrID: function(){
let myDate = new Date();
let year = myDate.getFullYear();
let mouth = myDate.getMonth() + 1;
let day = myDate.getDate();
let hour = myDate.getHours();
let minute = myDate.getMinutes();
let second = myDate.getSeconds();
let msecond = myDate.getMilliseconds(); //獲取當前毫秒數(0-999)
if(mouth < 10){ /*月份小於10 就在前面加個0*/
mouth = String(String(0) + String(mouth));
}
if(day < 10){ /*日期小於10 就在前面加個0*/
day = String(String(0) + String(day));
}
if(hour < 10){ /*時小於10 就在前面加個0*/
hour = String(String(0) + String(hour));
}
if(minute < 10){ /*分小於10 就在前面加個0*/
minute = String(String(0) + String(minute));
}
if(second < 10){ /*秒小於10 就在前面加個0*/
second = String(String(0) + String(second));
}
if (msecond < 10) {
msecond = String(String('00') + String(second));
} else if(msecond >= 10 && msecond < 100){
msecond = String(String(0) + String(second));
}
let currentDate = String(year) + String(mouth) + String(day) + String(hour) + String(minute) + String(second) + String(msecond);
return currentDate
},
//獲取隨機字符串
getNonceStr(){
return Math.random().toString(36).substr(2, 15)
},
// 統一下單簽名
paysignjsapi (appid,body,mch_id,nonce_str,notify_url,openid,out_trade_no,spbill_create_ip,total_fee) {
let ret = {
appid: appid,
body: body,
mch_id: mch_id,
nonce_str: nonce_str,
notify_url:notify_url,
openid:openid,
out_trade_no:out_trade_no,
spbill_create_ip:spbill_create_ip,
total_fee:total_fee,
trade_type: 'JSAPI'
}
let str = this.raw(ret, true)
str = str + '&key=' + wxConfig.Mch_key
let md5Str = cryptoMO.createHash('md5').update(str, 'utf-8').digest('hex')
md5Str = md5Str.toUpperCase()
return md5Str
},
raw (args, lower) {
let keys = Object.keys(args)
keys = keys.sort()
let newArgs = {}
keys.forEach(key => {
lower ? newArgs[key.toLowerCase()] = args[key] : newArgs[key] = args[key]
})
let str = ''
for(let k in newArgs) {
str += '&' + k + '=' + newArgs[k]
}
str = str.substr(1)
return str
},
//小程序支付簽名
paysignjs (appid, nonceStr, packages, signType, timeStamp) {
let ret = {
appId: appid,
nonceStr: nonceStr,
package: packages,
signType: signType,
timeStamp: timeStamp
}
let str = this.raw(ret)
str = str + '&key=' + this.Mch_key
let md5Str = cryptoMO.createHash('md5').update(str, 'utf-8').digest('hex')
md5Str = md5Str.toUpperCase()
return md5Str
},
// 校驗支付成功回調簽名
validPayBacksign (xml) {
let ret = {}
let _paysign = xml.sign[0]
for (let key in xml) {
if (key !== 'sign' && xml[key][0]) ret[key] = xml[key][0]
}
let str = this.raw(ret, true)
str = str + '&key=' + wxConfig.Mch_key
let md5Str = cryptoMO.createHash('md5').update(str, 'utf-8').digest('hex')
md5Str = md5Str.toUpperCase()
return _paysign === md5Str
},
// 確認退款簽名
refundOrderSign(appid,mch_id,nonce_str,op_user_id,out_refund_no,out_trade_no,refund_fee,total_fee) {
let ret = {
appid: appid,
mch_id: mch_id,
nonce_str: nonce_str,
op_user_id: op_user_id,
out_refund_no: out_refund_no,
out_trade_no: out_trade_no,
refund_fee: refund_fee,
total_fee: total_fee
}
let str = this.raw(ret, true)
str = str + '&key='+wxConfig.Mch_key
let md5Str = cryptoMO.createHash('md5').update(str, 'utf-8').digest('hex')
md5Str = md5Str.toUpperCase()
return md5Str
}
}
這個配置項里的就是raw方法得注意下,有個區分,有的簽名是key值全小寫,有的簽名就是支付二次簽名校驗的時候,key值是要保持駝峰,所以加了點區分。
當時在此處確實遇到了問題,查了很多博客,解決辦法都模棱兩可並沒有效。其實,微信提供了簽名校驗工具,可以將自己的參數傳入看和生成的是否一致,然后就可以單步調試看是哪里出了問題,比較方便快捷。(簽名校驗工具)
從上面代碼也可以看出流程:
根據文檔需要傳的參數 —— 生成下單簽名 —— 簽名與參數一起傳入 —— 調用微信統一下單api —— 返回下單接口的XML —— 解析XML返回數據參數,再次生成簽名 —— 數據返回前台供 wx.requestPayment() 調用
至此微信支付就可以正常喚起窗口付款了。但是還有個重要的問題,就是下單成功通知。也就是下統一下單里傳入的 notify_url:支付成功回答地址
3、支付成功結果通知
我們需要提供一個接口供微信支付成功回調:'POST /order/wxPayBack': wxPayBack, // 微信支付成功回調
const parseString = require('xml2js').parseString // xml轉js對象
let wxPayBack = async (ctx, next) => {
console.log('wxPayBack', ctx.request.body) // 我們可以打印看下微信返回的xml長啥樣
parseString(ctx.request.body, function (err, result) {
payBack(result.xml, ctx)
})
}
let payBack = async (xml, ctx) => {
if (xml.return_code[0] == 'SUCCESS') {
let out_trade_no = xml.out_trade_no[0] // 商戶訂單號
let total_free = xml.total_fee[0] // 付款總價
console.log('訂單:', out_trade_no, '價格:', total_free)
if (wxConfig.validPayBacksign(xml)) {
let out_order = await model.orderInfo.find({
where: {
orderCode: out_trade_no
}
})
if (out_order && (out_order.orderPay * 100) - total_free === 0 && out_order.orderState === 1) {
await model.orderInfo.update({ orderState: 2 }, {
where: {
orderCode: out_trade_no
}
})
// emitToSocket(total_fee)
return ctx.response.body = `<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml> `
}
}
}
return ctx.response.body = `<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[參數錯誤]]></return_msg></xml> `
}
wxConfig.validPayBacksign(xml),這里一定要校驗下支付成功的回調簽名。校驗規則就是微信返回的xml里除了 sign 不放入參數校驗外,其他的均要拿出 key - value 值進行生產 md5 加密,然后與微信返回的 sign 值比對即可。
校驗成功之后,修改訂單表對應數據的狀態即可。
三、申請退款和確認退款
申請退款其實沒什么說的,就是用戶側申請退款,然后更改用戶側訂單的狀態,主要說一下商家確認退款給買家的流程。
特別需要注意的是:請求需要雙向證書。 詳見證書使用
進入證書使用鏈接,去查看關於“3、API證書”相關的使用東西。也就是說需要從商戶號那邊下載一些證書,放在工程里,再調用微信三方提供的退款接口:https://api.mch.weixin.qq.com/secapi/pay/refund 時,需要校該證書,以確保安全。
實例代碼:
// 確認退款
let confirmRefund = async (ctx, next) => {
let _body = ctx.request.body
let out_trade_no = _body.orderCode // 商戶訂單號
let nonce_str = wxConfig.getNonceStr()
let total_fee = _body.orderPay * 100 // 訂單價格 單位是 分
let refund_fee = _body.orderPay * 100
let bodyData = '<xml>';
bodyData += '<appid>' + wxConfig.AppID + '</appid>';
bodyData += '<mch_id>' + wxConfig.Mch_id + '</mch_id>';
bodyData += '<nonce_str>' + nonce_str + '</nonce_str>';
bodyData += '<op_user_id>' + wxConfig.Mch_id + '</op_user_id>';
bodyData += '<out_refund_no>' + nonce_str + '</out_refund_no>';
bodyData += '<out_trade_no>' + out_trade_no + '</out_trade_no>';
bodyData += '<total_fee>' + total_fee + '</total_fee>';
bodyData += '<refund_fee>' + refund_fee + '</refund_fee>';
// 簽名
let sign = wxConfig.refundOrderSign(
wxConfig.AppID,
wxConfig.Mch_id,
nonce_str,
wxConfig.Mch_id,
nonce_str, // 商戶退款單號 給一個隨機字符串即可out_refund_no
out_trade_no,
refund_fee,
total_fee
)
bodyData += '<sign>' + sign + '</sign>'
bodyData += '</xml>'
let agentOptions = {
pfx: fs.readFileSync(path.join(__dirname,'/wx_pay/apiclient_cert.p12')),
passphrase: wxConfig.Mch_id,
}
// 微信小程序退款接口
let urlStr = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
let option={
method:'POST',
uri: urlStr,
body: bodyData,
agentOptions: agentOptions
}
let result = await rp(option)
parseString(result, function(err, result){
if (result.xml.result_code[0] == 'SUCCESS') {
refundBack(_body.id)
return ctx.response.body={
success: true,
msg: '操作成功'
}
} else{
return ctx.response.body={
success: false,
msg: result.xml.err_code_des[0]
}
}
})
}
let refundBack = async (orderId) => {
model.orderInfo.update({ orderState: 8 }, {
where: { id: orderId }
})
let orderfoods = await model.foodsOrder.findAll({
where: { orderId: orderId }
})
orderfoods.forEach(food => {
dealFood(food, 'plus')
})
}
可以看到:隨機字符串 nonce_str,商戶退款單號 out_refund_no,我們用的是同一個隨機串。
然后經過校驗之后,獲取證書內容 及 商戶號,作為參數傳給微信提供的申請退款接口接口。返回退款成功之后,做自己用戶側的相關業務處理即可。
