前端微信H5支付,支付宝内及第三方H5支付,各平台分享总结

微信支付

微信内 H5 支付(公众号支付)

走统一引入 JSSDK 的新方式实现支付逻辑。

  • JSSDK 文档 地址
  • 网页授权(获取 access_tokenopenid) 文档 地址
  • 统一下单 文档 地址
  • 微信内 H5 调起支付 文档 地址

明确两种 access_token

  • 网页授权 access_token:基于 Oauth 2.0,需要 code 换取,用于维持登录状态。
  • 普通 access_token:后台可直接生成,换取 jsapi_ticket 进而加密生成 signature 作为 wx.config 参数。

整个流程

支付统一下单接口需要 openid,所以必须要授权获取。因为此处只需要 openid,所以 snsapi_basescope 走静默授权。(为了方便起见,此处每次进入都获取 code 走静默授权。)

  1. 进入页面判断链接上是否有 code,没有则请求接口得到 url 并重定向:

    1
    2
    3
    4
    {
    url: 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx1232123123&redirect_uri=带参数(包括锚点)的编码后的重定向的回调地址&response_type=code&scope=snsapi_base&connect_redirect=1&state=xxxx#wechat_redirect';
    }
    // connect_redirect=1 表示只只触发一次请求
  2. 微信内部多次重定向之后,最后带上 codestate 301 重定向回设置的回调地址:

    1
    301: https://www.flqin.com/test.html?id=135price=1322&code=ASDJIAJD13D823D&state=13212313#/
  3. 前端判断到链接有 code,将 code 及一些其他页面需要的参数一起传给后端,后端拿到 code 换取网页授权 access_tokenopenid,此处后端可将 access_token/refresh_token 存入 cookie 或者通过其他方式 jwt 维持登录状态,就无需重复获取 code

  4. openid 为统一下单 jsapi 接口必传参数,用于得到 prepay_id 参数值,即为 package 参数。最后接口统一返回 wx.confgwx.chooseWXPay 所需参数,前端调用 JSSDK 即可。

  5. wx.chooseWXPay 的成功、完成回调仅代表支付完成,

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
async queryWxCode() {
//...
if (!code) {
const { data } = await api.wxToPay({url:'当前页面完整链接'}).catch((e) => e); //获取重定向链接(微信授权地址)
data && (window.location.href = data);
return;
}
return api.wxPay({code,url:'当前页面包含参数的url'}); //获得wx.config、wx.choosepay全部参数
},
async registerWx() {
const { data } = await this.queryWxCode().catch((e) => {});
this.wxSdkInfo = data;
wx.config({
debug: false, // 开启调试模式
appId: data.appId, // 必填,公众号的唯一标识
timestamp: data.timeStamp, // 必填,生成签名的时间戳
nonceStr: data.nonceStr, // 必填,生成签名的随机串
signature: data.signature, // 必填,签名 通过普通access_token生成,无需授权
jsApiList: ['chooseWXPay', 'hideMenuItems'] // 必填,需要使用的JS接口列表
});
wx.ready(() => {
this.hideWxMenus();
this.createWxpay();
});
},
hideWxMenus() {
// 禁用支付分享, 防止订单未生成而进入该页面导致的报错
const menuList = ['menuItem:share:appMessage', 'menuItem:share:timeline', 'menuItem:copyUrl'];
wx.hideMenuItems({
menuList // 要隐藏的菜单项,只能隐藏“传播类”和“保护类”按钮,所有menu项见附录3
});
},
createWxpay() {
const data = this.wxSdkInfo;
wx.chooseWXPay({
timestamp: data.timeStamp, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: data.nonceStr, // 支付签名随机串,不长于 32 位
package: `prepay_id=${data.prepayId}`, // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*) 该参数需要openid
signType: data.signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
paySign: data.paySign, // 支付签名 参与签名的参数为:appId、timeStamp、nonceStr、package、signType
success: this.queryPayment, //公共查询结果接口
fail: this.showFail, //失败处理
cancel: this.showRefail, //取消处理
complete({ errMsg }) {
const SUCCESS = /:ok/gi.test(errMsg);
const CANCEL = /:cancel/gi.test(errMsg);
if (SUCCESS) this.queryPayment();
else if (CANCEL) this.showRefail();
else this.showFail();
}
});
},

支付宝支付

支付宝内 H5 支付

必须接入支付宝 JSAPI

  • H5 开发文档 地址
  • Alipay JSSDK 地址
  • 支付文档 地址

注意点:

  • 不涉及读取用户优惠券之类的都走普通无单号支付,即无需授权。
  • 授权过程与微信大致一致。

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
async registerAlipay() {
const alipayInit = async () => {
await this.queryAliOrder().catch((e) => e);
this.createAlipay();
};
if (window.AlipayJSBridge) alipayInit();
else document.addEventListener('AlipayJSBridgeReady', alipayInit, false);
},
async queryAliOrder() {
// 无单号支付
const {data: orderStr } = await api.aliPayH5({orderId:1}).catch((e) => e);//请求接口获取 orderStr
},
async queryAliCode() {
// 有单号支付tradeNo(后台开通当面付),同微信授权流程
const { href, search } = window.location;
const query = search && search.replace(/[?\/]/g, '');
const { auth_code: code } = qs.parse(query);
if (!code) {
const { data } = await api.aliToPay(href).catch((e) => e); //请求接口获取支付宝授权地址
data && (window.location.href = data);
return;
}
const {data: tradeNO } = await api.aliPayH5({orderId:1,code}).catch((e) => e); //请求接口获取 tradeNO
},
async createAlipay() {
AlipayJSBridge.call('tradePay', { 'orderStr/tradeNO' }, ({ resultCode }) => {
const SUCCESS_CODES = ['9000', '8000', '6004'];
const UNKNOW_CODES = ['7001', '6001', '6002'];
if (~SUCCESS_CODES.indexOf(resultCode)) _this.queryPayment(); //支付查询
else if (~UNKNOW_CODES.indexOf(resultCode)) _this.showRefail();
else _this.showFail();
});
},

第三方浏览器中 支付宝支付

  • 支付文档 地址
  • 参数说明 地址

无需授权,直接将 url 和订单号传给后端,后端返回一个 form 表单添加到页面即可唤起支付宝 APP,支付完成后,根据后端配置支付宝会自动回跳到支付结果页并携带一堆参数。此时需要查询公共结果接口来确定支付是否成功。

注意点:

  • 通过链接上是否有参数 alipay.trade.wap.pay.return 来判断是否是支付回调回来的页面。

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
registerH5() {
const { origin, pathname, search } = window.location;
const { method } = qs.parse(search.replace(/[?\/]/g, ''));
if (method === 'alipay.trade.wap.pay.return') {
this.queryPayment(); //执行公共查询结果接口
return;
}
const { data: form } = await api.aliPayH5(orderId,url).catch((e) => e); //请求接口返回form表单
if (!form) return;
const div = document.createElement('div');
div.innerHTML = form;
document.body.appendChild(div);
document.forms[0].submit();
}

公共查询结果 queryPayment

需要轮询支付结果。

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async queryPayment() {
const TIMES_REACH = 0;
const TIMES_MAX = 5;
const SUCCESS_CODE = '1'; // 0待支付,需要继续查询 1支付成功 -1异常
const WAITING_CODE = '0';
if (this.qTimes === TIMES_REACH) { //qTimes为最大查询次数
this.qTimes = TIMES_MAX;
return;
}
const { data } = await api.payStatus(orderId).catch((e) => e); // 查询支付结果
if (data == SUCCESS_CODE) { //成功状态
this.showSuccess();
this.qTimes = TIMES_MAX;
} else if (data == WAITING_CODE) { //等待状态
console.log('查询支付结果中...');
clearTimeout(this.timer);
this.timer = setTimeout(async () => {
await this.queryPayment(); //递归轮询
this.qTimes -= 1;
}, 1000);
} else {
this.showFail();
}
},

支付总结

  • 微信支付宝均需要配置合法域名,可精确到文件夹路径。
  • 开发可用微信开发工具,结合 alert 调试,直接把代码发到已配置好域名的测试环境测试最佳。
  • 注意参数的大小写及传递的内容,支付主要复杂度都集中在后端,前端需要配合后端传参。

分享

微信- > 微信(微信联系人,微信朋友圈,QQ 联系人,QQ 空间)

必须引入 JSSDK,否则分享出来无法设置分享描述及分享图片。

  • JSSDK 文档:地址
  • 获取普通 access_token 文档:地址

需要给后端传入带有参数的当前页面 url(无需编码),让后端返回 wx.config 所需参数即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const { data } = await api.getWxConfig({ url: '当前页面包含参数的url' }); //接口返回 appId,timeStamp,nonceStr,signature.
wx.config({
debug: true,
appId: data.appId, // 必填,公众号的唯一标识
timestamp: data.timeStamp, // 必填,生成签名的时间戳
nonceStr: data.nonceStr, // 必填,生成签名的随机串
signature: data.signature, // 必填,签名
jsApiList: ['onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareQZone'], // 必填,需要使用的JS接口列表
});
wx.ready(() => {
const shareList = ['onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareQZone'];
shareList.forEach((item) => {
wx[item]({
title: this.shareInfo.title, // 分享标题
desc: this.shareInfo.description, // 分享描述
link: window.location.href,
imgUrl: this.shareInfo.image, // 分享图标
success: function () {
// 设置成功
},
});
});
});

注意点:

  • 首先需要在微信后台设置接口安全域名:www.xxxx.com,无需到具体路径及参数。
  • 前端仅需传给后台当前页面带参数的 url 即可,甚至接口可以自己取请求页面带参数的 url,前端无需传。
  • 注意 timeStamp 的大小写。
  • 注意 jsApiList 选择即将废弃的分享接口,新分享接口反而不好用。
  • 分享无需授权,因:参数 nonceStr <- jsapi_ticket <- access_token <- appid,secret。此 access_token 为普通 access_tokenappidsecret 均在微信后台获取。区别于支付等网页授权 access_token

QQ(TIM) -> QQ(QQ 联系人,QQ 空间,微信联系人,微信朋友圈)

需要引入 api

1
<script src="//open.mobile.qq.com/sdk/qqapi.js"></script>

js 执行:

1
2
3
4
5
6
7
const share = {
title: '分享标题,最大45字节',
desc: '分享内容,最大60字节',
image_url: '图片URL,最小需要200 * 200',
share_url: '分享链接与页面URL同',
};
mqq.data.setShareInfo(share, callback);

mqq.ui.showShareMenu(); 可直接唤起 QQ 分享面板。

支付宝 -> 支付宝(朋友动态,联系人)

直接按如下设置 meta 即可:

1
2
3
4
<meta name="Alipay:title" content="分享标题" />
<meta name="Alipay:imgUrl" content="分享图片url" />
<meta name="Alipay:desc" content="分享描述" />
<meta name="Alipay:link" content="分享链接" />

微博,头条,知乎等其他平台浏览器 -> 微信,QQ

微博可注册轻应用使用 JS-SDK 完成分享设置。收益不大就没做了。
知乎实测 IOS 可以取写死的描述及动态标题,图片无法设置,安卓只能设置标题。

对于其他平台分享,统一兜底处理方式:

分享标题及描述设置 head 元素:

1
2
3
4
<head>
<title>分享标题</title>
<meta name="description" content="分享描述" />
</head>

分享图及描述设置(一般默认取页面第一张大于 300px 图及第一段描述):

1
2
3
4
5
6
<body>
<div style="display: none;">
<p>分享描述</p>
<img src="图片地址url" />
</div>
</body>

使用 ogp

1
2
3
4
5
6
7
<head>
<meta property="og:type" content="website">
<meta property="og:title" content="分享标题">
<meta property="og:description" content="分享描述">
<meta property="og:img" content="完整的分享图片链接">
<meta property="og:url" content="完整的分享页面地址">
</head>