半年前跟老師做項(xiàng)目的時(shí)候遇到需要接入支付寶進(jìn)行支付。這一年以來(lái)接觸前端覺(jué)得涉獵的東西太多了,現(xiàn)在好像什么都不會(huì)。。。 ~還是把??炒一下,實(shí)際接入場(chǎng)景可能根據(jù)業(yè)務(wù)都有所區(qū)別,這里通過(guò)以前做的項(xiàng)目作為示例,業(yè)務(wù)的內(nèi)容不需要糾結(jié),只需要從用法上理解即可。
示例
(前端是有PC端的web以及手機(jī)RN(android端),后端是koa2+leancloud)
接入支付寶支付的先置條件
個(gè)人無(wú)法申請(qǐng),目前只有企業(yè)可以進(jìn)行申請(qǐng)
1.在支付寶注冊(cè)企業(yè)賬號(hào)
2.需要在螞蟻金服開(kāi)放平臺(tái)申請(qǐng)應(yīng)用

完善資料——包括進(jìn)行授權(quán)回調(diào)的地址和應(yīng)用的網(wǎng)關(guān)以及接口加簽方式,生成好的證書(shū)需要好好保管,這不僅是進(jìn)行后續(xù)開(kāi)發(fā)需要使用的,而且更關(guān)系到該應(yīng)用的數(shù)據(jù)安全。
后進(jìn)行簽約并提交審核

PS: 簽約的時(shí)候?qū)Σ煌緩接胁煌囊?,包括手機(jī)app支付需要上傳關(guān)于app的文檔(內(nèi)含必須要有app界面截圖!不然會(huì)被打回)
接入支付寶
業(yè)務(wù)思路
無(wú)論是支付寶又或者是第三方支付都是以這個(gè)為核心

接入方式
可以通過(guò)第三方進(jìn)行快捷接入,也可以使用原生接口/SDK進(jìn)行接入
在網(wǎng)頁(yè)H5中使用的是beecloud 依然是只有企業(yè)用戶可以使用,不對(duì)個(gè)人進(jìn)行開(kāi)放.第三方支付的好處就是統(tǒng)一入口,支持大量的支付途徑如

但是依然是每個(gè)途徑需要自己去開(kāi)通相關(guān)的應(yīng)用。
beecloud h5 秒支付
beecloud 秒支付的運(yùn)作方式是將數(shù)據(jù)渲染在他提供的模板頁(yè)面上,模板頁(yè)面校驗(yàn)數(shù)據(jù)后跳轉(zhuǎn)到支付寶支付頁(yè)面,最后通過(guò)回調(diào)通知后臺(tái)。官方文檔
通過(guò)使用beecloud的秒支付button可以直接適配pc端和手機(jī)端的h5支付。
(在先置條件中必須進(jìn)行PC端支付的簽約才能繼續(xù)使用)
在開(kāi)通快捷支付后會(huì)得到以下內(nèi)容
const appid='';
const secret='';
const masterSecret = '';
再在beecloud中填入回調(diào)地址后,支付成功會(huì)通知該地址,即有請(qǐng)求附帶數(shù)據(jù)發(fā)送到該url中。
從用戶訪問(wèn)頁(yè)面開(kāi)始看代碼,由于數(shù)據(jù)都是儲(chǔ)存到leancloud的云數(shù)據(jù)庫(kù)中,所以會(huì)看到AV(??不是你們想的那個(gè)!)類的相關(guān)內(nèi)容,只需要知道該類是作為數(shù)據(jù)儲(chǔ)存獲取所用即可,以下是訪問(wèn)充值頁(yè)面的路由。
var fun_payBalance = async(ctx, next) => {
var user = await AV.User.current();
var requestData = ctx.request.query;
var amount = requestData.amount;
if (user == null) {
ctx.response.redirect('/personal/recharge');
} else {
if (!amount) {
ctx.response.redirect('/personal/recharge');
}
}
var result = Pay.rechargeBalance(amount, '余額充值');
//這個(gè)是進(jìn)行生成訂單包括儲(chǔ)存到數(shù)據(jù)庫(kù)以及生成signStr進(jìn)行校驗(yàn)
if (result) {
var signStr = result.signStr;
var outTradeNo = result.outTradeNo;
//將數(shù)據(jù)傳到模板頁(yè)面
ctx.render('paytest.html', {
title: '余額充值',
OutTradeNo: outTradeNo,
Sign: signStr,
amount: amount
});
} else {
console.log('error');
ctx.response.redirect('/personal/recharge');
}
}
Pay.rechargeBalance的內(nèi)容
rechargeBalance:(amount,title)=>{
var user = AV.User.current();
if(!user){
return ;
}
var pay = new pays();
var outTradeNo=uuid.v4();
outTradeNo = outTradeNo.replace(/-/g, '');
amount = parseFloat(amount);
var data = appid + title + amount + outTradeNo + secret;
pay.set('orderNumber',outTradeNo);
pay.set('lines',amount);
pay.set('info',title);
pay.set('user',user);
//將訂單存入數(shù)據(jù)庫(kù)
pay.save();
return {
signStr:sign.createHash('md5').update(data, 'utf8').digest("hex"),
'outTradeNo':outTradeNo,
};
},
支付頁(yè)面模板內(nèi)容,核心的是Bc.click method
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<title>{{ title }}</title>
<link rel='stylesheet' href='stylesheets/style.css' />
</head>
<body>
<div class="description">
支付單號(hào):{{ OutTradeNo}}
支付內(nèi)容:{{title}}
支付金額:{{amount}}
{% if card %}
卡類型:{{card.typeInfo.get('cardName')}}
卡編號(hào):{{card.get('cardId')}}
{%endif%}
<p><center><button id="test" class="button">點(diǎn)擊支付</button></center></p>
</div>
<script id='spay-script' src='https://jspay.beecloud.cn/1/pay/jsbutton/returnscripts?appId=af1df3a3-ab4b-48d8-b71a-8dbd60a92451'></script>
<script>
document.getElementById("test").onclick = function() {
asyncPay();
};
function bcPay() {
var out_trade_no = "{{ OutTradeNo}}";
var sign = "{{ Sign }}";
console.log(sign);
console.log(out_trade_no);
/**
* click調(diào)用錯(cuò)誤返回:默認(rèn)行為console.log(err)
*/
BC.err = function(data) {
//注冊(cè)錯(cuò)誤信息接受
alert(data["ERROR"]);
}
/**
* 3. 調(diào)用BC.click 接口傳遞參數(shù)
*/
BC.click({
"title": "{{ title }}",
"amount": "{{amount}}",
"out_trade_no": out_trade_no, //唯一訂單號(hào)
"sign" : sign,
/**
* optional 為自定義參數(shù)對(duì)象,目前只支持基本類型的key =》 value, 不支持嵌套對(duì)象;
* 回調(diào)時(shí)如果有optional則會(huì)傳遞給webhook地址,webhook的使用請(qǐng)查閱文檔
*/
"optional": {"test": "willreturn"}
});
}
function asyncPay() {
if (typeof BC == "undefined"){
if( document.addEventListener ){
document.addEventListener('beecloud:onready', bcPay, false);
}else if (document.attachEvent){
document.attachEvent('beecloud:onready', bcPay);
}
}else{
bcPay();
}
}
</script>
</body>
</html>
支付成功的回調(diào),回調(diào)路由
'use strict';
const AV = require('leanengine');
const Pay = require('../pay-service')
var beecloudWebhook = async(ctx,next)=>{
var data = ctx.request.body;
Pay.webhook(data);
}
module.exports={
'POST /webhook/back':beecloudWebhook,
}
回調(diào)的業(yè)務(wù)函數(shù)
webhook:async(data)=>{
var signKey = data.signature;
var signData = appid+data.transaction_id+data.transaction_type + data.channel_type + data.transaction_fee + masterSecret;
var signStr = sign.createHash('md5').update(signData, 'utf8').digest("hex")
//校驗(yàn)signStr 如果不符合則說(shuō)明回調(diào)來(lái)源不合法
if(signStr!=signKey){console.log("key failure");}
//查詢訂單,在請(qǐng)求支付的時(shí)候會(huì)產(chǎn)生一個(gè)訂單號(hào)
var query = new AV.Query('pays');
query.equalTo('orderNumber',data.transaction_id);
//查詢第一條符合記錄
var pay = await query.first();
//如果該訂單已經(jīng)完成了,說(shuō)明回調(diào)重復(fù)了 退出
if(pay&&pay.get('status')===1){return {
"result":-1,
"message":"had been finished"
};}
else{
//如果訂單不存在 退出
if(typeof pay ==="undefined"){
return{
"result":-1,
"message":"pay do not exit"};
}
//check the transaction_fee(這里可以進(jìn)一步根據(jù)數(shù)據(jù)庫(kù)中的訂單信息檢查金額是否正確) 這個(gè)歷史版本,懶了就沒(méi)做了orz
// about the pay result
if(data.transaction_type == "PAY"){
var user = await AV.User.current();
//如果支付成功了
var status = data.trade_success;
if(status){
//update the pay status,更新訂單信息
pay.set('status',1);
var payId = pay.id;
await pay.save();
var pay_record =null;
let card_Id = pay.get('cardId');
//后面是業(yè)務(wù)內(nèi)容了 不需要再看了
if(card_Id){
pay_record = new rechargeCard();
pay_record.set('cardId',pay.cardId);
let card = new AV.Query('Cards');
var fee = data.transaction_fee;
//測(cè)試模式訂單金額擴(kuò)大1000
if(envMode=="test"){
fee*=1000;
}
card.get(card_Id).then(function(findedCard){
findedCard.set(findedcard.get('cardBalance')+fee);
findedCard.save();
console.log(findedCard);
})
}else{
pay_record = new rechargeBalance();
var fee = data.transaction_fee;
// if the test mode the fee * 1000
if(envMode=="test"){
fee*=100;
}
console.log("before"+user.get('balance'));
user.set('balance',user.get('balance')+fee);
user.save();
console.log("after"+user.get('balance'));
}
if(pay_record){
pay_record.set('user',user);
pay_record.set('payLines',data.transaction_fee);
pay_record.set('payId',payId);
pay_record.save();
}
return {
"result":1,
"message":"success"
};
}else{
console.log("status failure");
}
}
}
},
至此第三方支付的??已經(jīng)結(jié)束,可能夾雜了一些業(yè)務(wù)邏輯在里面,但是總體思路還是跟上面的圖是一致的。




react-native(android) 接入原生支付
這部分接入是接入是參照一個(gè)小姐姐寫的教程
這部分的核心內(nèi)容是,集成原生的android模塊
以及使用Ali Sdk nodejs第三方包
?
這部分以上說(shuō)說(shuō)的比較詳細(xì)了
其中遇到的問(wèn)題只有signStr 不正確的時(shí)候可能會(huì)出現(xiàn)
ALI 40247 在server端的 signStr 有問(wèn)題
暫時(shí)無(wú)法獲取訂單信息 —— 沒(méi)有傳遞金額

程序執(zhí)行完后必須打印輸出“success”(不包含引號(hào))。如果商戶反饋給支付寶的字符不是success這7個(gè)字符,支付寶服務(wù)器會(huì)不斷重發(fā)通知,直到超過(guò)24小時(shí)22分鐘。一般情況下,25小時(shí)以內(nèi)完成8次通知(通知的間隔頻率一般是:4m,10m,10m,1h,2h,6h,15h);
程序執(zhí)行完成后,該頁(yè)面不能執(zhí)行頁(yè)面跳轉(zhuǎn)。如果執(zhí)行頁(yè)面跳轉(zhuǎn),支付寶會(huì)收不到success字符,會(huì)被支付寶服務(wù)器判定為該頁(yè)面程序運(yùn)行出現(xiàn)異常,而重發(fā)處理結(jié)果通知;
需要注意在處理alipay webhook的時(shí)候需要注意,返回信息的問(wèn)題,在校驗(yàn)信息時(shí)候需要返回success,不然會(huì)一直發(fā)送信息。
koa2 中 使用上面提到的Ali Sdk nodejs第三方包,可以將這個(gè)ali對(duì)象存儲(chǔ)在ctx.state中。
暫時(shí)寫到這里,實(shí)際應(yīng)用中涉及支付業(yè)務(wù)的部分必須要進(jìn)行每天對(duì)賬才能保證安全。