支付文档
该文档用作前端处理H5游戏支付参考文档,游戏引擎为egret
H5游戏支付不同于web支付,H5只有单界面,不能通过路由寻址的方式跳转到对应的游戏界面
因此,在支付中需要注意要在不打断游戏进程的情况下完成支付拉起,这个时候需要对支付流程
做特殊处理,不展示各平台的网关页面,而是从中抽取能够拉起支付的deeplink,
通过iframe来进行重定向,从而实现拉起,注意的是不同的平台,对不同的设备,在处理上有差异
需要单独做差异化的处理
支付流程(大概)
客户端 服务端 商户平台 支付平台
发起订单----------------------------->-------------->统一下单
订单记录<-----------订单信息<--------
获取订单数据<-------------------------
订单数据差异化处理
拉起支付-----------------------------------------------
支付结果通知<-----处理支付结果<-----支付结果重定向---------
支付结果处理
手机运行平台分类(暂不包括native端)
PC
WEB_ANDROID
WEB_IOS
WEB_ANDROID_WECHAT
WEB_IOS_WECHAT
微信支付
差异化处理(关于部分特殊浏览器的支付处理方案,在特殊问题中描述):
PC
展示支付二维码
WEB_ANDROID
抽取deepLink,iframe展示,具体见**deeplink抽取**
WEB_IOS
抽取deepLink,window.location.href替换,具体见**deeplink抽取**
WEB_ANDROID_WECHAT
WEB_IOS_WECHAT
接通jsspi,通过微信进行支付
支付宝支付
差异化处理(关于部分特殊浏览器的支付处理方案,在特殊问题中描述)
PC
展示支付二维码
WEB_ANDROID
抽取deepLink,iframe展示,具体见**deeplink抽取**
WEB_IOS
抽取deepLink,window.location.href替换,具体见**deeplink抽取**
WEB_ANDROID_WECHAT
WEB_IOS_WECHAT
待定
deepLink抽取
微信
微信需要抽取deeplink的部分,平台返回的数据为微信的网关页文本,核心内容为
<script type="text/javascript">
var is_postmsg="";
if( 0!==0 && is_postmsg=="1" )
{
parent.postMessage(JSON.stringify({
action : "send_deeplink_fail",
data : {
deeplink : ""
},
error : {
error_code : "0",
error_msg : "ok"
}
}), "");
}
if( 0===0)
{
window.onload=function()
{
// var fp=new Fingerprint2();
// fp.get(function(result)
{
// var fingerprint="";
/* if(fingerprint!=result && fingerprint)
{
document.getElementById("errpage").innerHTML='<div class="icon_area"><i class="icon_msg warn">!</i></div> \
<div class="text_area"> \
<h2 id="111" class="title"> '+result+'缃戠粶鐜鏈兘閫氳繃瀹夊叏楠岃瘉锛岃绋嶅悗鍐嶈瘯</h2> \
</div>';
return;
}*/
var is_postmsg="";
if(is_postmsg=="1")
{
parent.postMessage(JSON.stringify({
action : "send_deeplink",
data : {
deeplink : "weixin://wap/pay?prepayid%3Dwx18124723988137f5141f245333fe250000&package=1371992231&noncestr=1600404444&sign=a733c49509892bfc49cfd6299d9a87c3"
}
}), "");
}
else
{
var url="weixin://wap/pay?prepayid%3Dwx18124723988137f5141f245333fe250000&package=1371992231&noncestr=1600404444&sign=a733c49509892bfc49cfd6299d9a87c3";
var redirect_url="";
;
if(redirect_url)
{
setTimeout(
function(){
;
},
5000
);
}
else
{
setTimeout(
function(){
window.history.back();
},
5000);
}
}
}
// );
}
}
</script>
</body>
</html>
处理方案为:
```
private handleH5Response(response: PayParam) {
let data = response.Parameters;
let lines = data.split('\n');
for (let line of lines) {
if (line != "" && line.indexOf('deeplink') != -1 && line.indexOf('weixin://wap/pay') != -1) {
let index = line.indexOf(":");
line = line.slice(index + 1, line.length)
line = line.split(`"`)[1]
line = line.split(`"`)[0]
return line;
}
}
return undefined;
}
```
支付宝
支付宝支付会返回一个表单,需要把表单提交转化成post请求来获取到网关页,具体为:
```
let url = `https://openapi.alipay.com/gateway.do?charset=UTF-8`;
let xhr = new XMLHttpRequest()
xhr.open('post', url);
xhr.onreadystatechange = (e) => {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
//用iframe打开
this.openIframe(xhr.response);
}
}
}
let fd = new FormData();
//这一步是解析表单里面的数据,把参数解析出来
let datas = data.split("<input");
for (let i = 1; i < datas.length - 1; i++) {
let params = datas[i].split("/>")[0];
params = params.split(" type='hidden'")[1]
let values = params.split(" value=")
let names = values[0]
let value = values[1]
let name = names.split(" name=")[1]
name = name.trim().slice(1, name.length - 1)
value = value.trim().slice(1, value.length - 1)
fd.append(name, value);
}
xhr.send(fd);
```
其中,openIframe的部分实际包括两个部分
抽取deepLink以及差异化展示
本节重点是抽取deeplink
通过post请求返回的页面文本核心内容为
```
(function(){
var _AP = {}
var ua = navigator.userAgent.toLowerCase(),
locked = false,
domLoaded = document.readyState==='complete',
delayToRun;
function customClickEvent() {
var clickEvt;
if (window.CustomEvent) {
clickEvt = new window.CustomEvent('click', {
canBubble: true,
cancelable: true
});
} else {
clickEvt = document.createEvent('Event');
clickEvt.initEvent('click', true, true);
}
return clickEvt;
}
function getAndroidVersion() {
var match = ua.match(/android\s([0-9\.]*)/);
return match ? match[1] : false;
}
var noIntentTest = /aliapp|360 aphone|weibo|windvane|ucbrowser/.test(ua);
var hasIntentTest = /chrome|samsung/.test(ua);
var isAndroid = /android|adr/.test(ua) && !(/windows phone/.test(ua));
var canIntent = !noIntentTest && hasIntentTest && isAndroid;
var clientBtn
// 确定浏览器类型
var isChrome = false;
var isWebview = false;
if (ua.match(/(?:chrome|crios)\/([\d\.]+)/)) {
isChrome = true;
if (ua.match(/version\/[\d+\.]+\s*chrome/)) {
isWebview = true;
}
}
var isOriginalChrome = isAndroid && isChrome && !isWebview;
if (ua.indexOf('m353')>-1 && !noIntentTest) {
canIntent = false;
}
// 安卓走iframe方式唤起
if (ua.indexOf('android')>-1 && !noIntentTest) {
canIntent = false;
}
/**
* open client
*/
_AP.open = function (params) {
if (!domLoaded && (ua.indexOf('360 aphone')>-1 || canIntent)) {
var arg = arguments;
delayToRun = function () {
_AP.open.apply(null, arg);
delayToRun = null;
};
return;
}
if (locked) {
return;
}
locked = true;
var o;
if (typeof params === 'object') {
o = {
'ios': encodeURIComponent(JSON.stringify(params)),
'android': encodeURIComponent(params.dataString)
};
} else {
console.error('params error, pls use JSON format!')
}
// params fault tolerance
if (typeof o.ios !== 'string') {
o.ios = '';
} else if(typeof o.android !== 'string') {
o.android = '';
}
// nonsupport Android intent
if (!canIntent) {
if(isAndroid) {
var alipaysUrl = 'alipays://platformapi/startApp?appId=20000125&orderSuffix=' + o.android +'#Intent;scheme=alipays;package=com.eg.android.AlipayGphone;end';
}
//fix for iOS QQ browser
else if (ua.indexOf('mqqbrowser') > -1) {
var alipaysUrl = 'alipay://alipayclient/?' + o.android;
}
else {
var alipaysUrl = 'alipay://alipayclient/?' + o.ios;
}
//FIXME: 直接判断ios,不判断os版本号
if ( ua.indexOf('qq/') > -1 || ( ua.indexOf('safari') > -1 && ua.indexOf('os 9_') > -1 ) || ( ua.indexOf('safari') > -1 && ua.indexOf('os 10_') > -1 ) || ( ua.indexOf('safari') > -1 && ua.indexOf('os 11_') > -1 ) || ( ua.indexOf('safari') > -1 && ua.indexOf('os 12_') > -1 ) || ( ua.indexOf('safari') > -1 && ua.indexOf('os 13_') > -1 ) || ( ua.indexOf('safari') > -1 && ua.indexOf('os 14_') > -1 ) ) {
var openSchemeLink = document.getElementById('openSchemeLink');
if (!openSchemeLink) {
openSchemeLink = document.createElement('a');
openSchemeLink.id = 'openSchemeLink';
openSchemeLink.style.display = 'none';
document.body.appendChild(openSchemeLink);
}
//openSchemeLink.href = alipaysUrl;
// oppo浏览器兼容写法
openSchemeLink.onclick = function() {
window.location.href = alipaysUrl;
};
// trigger click
openSchemeLink.dispatchEvent(customClickEvent());
}
else {
var ifr = document.createElement('iframe');
ifr.src = alipaysUrl;
ifr.style.display = 'none';
document.body.appendChild(ifr);
}
$('.J-startapp').attr('href', alipaysUrl);
}
//support Android intent
else {
var packageKey = 'AlipayGphone';
var intentUrl = 'alipays://platformapi/startApp?appId=20000125&orderSuffix='+o.android+'#Intent;scheme=alipays;package=com.eg.android.'+ packageKey +';end';
var openIntentLink = document.getElementById('openIntentLink');
if (!openIntentLink) {
openIntentLink = document.createElement('a');
openIntentLink.id = 'openIntentLink';
openIntentLink.style.display = 'none';
document.body.appendChild(openIntentLink);
}
//openIntentLink.href = intentUrl;
// oppo浏览器兼容写法
openIntentLink.onclick = function() {
window.location.href = intentUrl;
};
// trigger click
openIntentLink.dispatchEvent(customClickEvent());
}
_AP.pay = function(param) {
_AP.open(param);
}
window._AP = _AP;
})();
try {
//唤起客户端快捷参数
var data = {"requestType":"SafePay","fromAppUrlScheme":"alipays","dataString":"h5_route_token=\"RZ42lyaFqrVMr2SudPI7WhDX5RA1iSmobilecashierRZ42\"&is_h5_route=\"true\"&need_invoke_app=\"true\""};
window.setTimeout(function(){
_AP.pay(data);
}, 50);
}catch(e){
window.console && window.console.log('e.name:' + e.name + ';e.message:' + e.message)
}
```
通过代码审计,抽取核心的拼接逻辑
```
private handleZfbResponse(data: string) {
//获取inData对象,存储的是核心的支付信息
let lindex = data.indexOf("inData");
let rindex = data.lastIndexOf("inData");
let response = data.slice(lindex, rindex)
let obj = response.split("{")[1].split("}")[0];
let objs = obj.split(",");
let inData = {
requestType: null,
fromAppUrlScheme: null,
dataString: null,
}
for (let param of objs) {
//这里做了一层封装,实际上是对字符串"key":"value"类型进行kv抽取
let kv = HttpResponseUtil.getParamKV(param, ":");
let v = kv.value.trim();
let k = kv.key.trim();
k = k.slice(1, k.length - 1)
v = v.slice(1, v.length - 1)
inData[k] = v
}
//注意,datsString中有个\的转义符,实际是不需要的,需要清除
let str = inData.dataString;
let datas = str.split("\\");
let newStr = ""
for (let dt of datas) {
newStr += dt;
}
inData.dataString = newStr
//获取到inData,拼接后缀
let o = {
'ios': encodeURIComponent(JSON.stringify(inData)),
'android': encodeURIComponent(inData.dataString)
};
if (typeof o.ios !== 'string') {
o.ios = '';
} else if (typeof o.android !== 'string') {
o.android = '';
}
//根据不同的运行平台解析出deeplink
if (DeviceUtil.isAndroid()) {
var alipaysUrl = 'alipays://platformapi/startApp?appId=20000125&orderSuffix=' + o.android + '#Intent;scheme=alipays;package=com.eg.android.AlipayGphone;end';
}
//ios的qq浏览器,这个兼容有问题,暂时忽略
else if (DeviceUtil.isBrowserQQ()) {
var alipaysUrl = 'alipay://alipayclient/?' + o.android;
}
//ios
else {
var alipaysUrl = 'alipay://alipayclient/?' + o.ios;
}
return alipaysUrl
}
```
特殊问题
QQ浏览器问题
1.android中,QQ浏览器从点击支付到支付拉起,过程不能超过1000ms,否则不能拉起
解决方案:针对QQ浏览器版本,获取到deepLink之后,再做一个支付信息确定提示,借此缩短拉起支付的时间
2.ipad中,QQ浏览器无法拉起支付,原因待定
iframe参考
```
const iframe = document.createElement('iframe')
iframe.style.position = "absolute";
iframe.style.display = 'none';
iframe.style.zIndex = "99999";
iframe.style.width = "100%"
iframe.style.height = "100%"
iframe.setAttribute('name', `zfbpay`);
iframe.setAttribute('sandbox', 'allow-top-navigation allow-scripts')
iframe.setAttribute('src', data)//data为deeplink
document.body.appendChild(iframe)
```
说明
1.在抽取deeplink中,仅很朴素的针对文本做了一定的处理操作,并没有用到其他的算法之类的
如果有好的处理思路,欢迎交流
2.仅作为参考,不代表各个项目实际运行情况,特殊操作应按实际项目需求进行
3.出文档的初衷是网上关于H5游戏支付处理这块的文档较少,实际操作中踩了不少的坑,故记录下处理过程
如有不妥,欢迎交流