前言
对于前端来说,微信的 支付
、 分享
、 登录
是一定要掌握的,今天这篇文章,主要对这三方面的流程进行详细的介绍。主要内容如下:
域名相关知识介绍
该网站不安全,请不要输入密码
微信小程序授权登录流程
op=>operation: openid判断是否登录授权
op2=>operation: 根据wx.login获取code
op3=>operation: 调用服务端根据code换取openid
op4=>operation: 通过用户授权,获取信息,存到数据库
op->op2->op3->op4
复制代码
如果你从来没有阅读过小程序登录授权的文档,建议你看一下下面的地址:
nodejs + 小程序实现授权登录
前端部分
- 根据本地是否有userId判断是否登录,如果没有登录,则获取用户的openid
onLoad() {
if(!this.data.userId) {
this.getSession()
}
},getSession() {
wx.login({
success: (res) => {
if (res.code) {
app.get(Api.getSession,{
code: res.code
}).then(res => {
store.setItem('openid',res.openid)
})
}
}
})
}复制代码
- 点击授权按钮,发起登录请求。
getUserInfo(e) {
let userInfo = e.detail.userInfo;
userInfo.openid = store.getItem('openid')
app.get(Api.login,{
userInfo
}).then(res => {
store.setItem('userId',res.data.userId)
this.setData({
userId: res.userId
})
})
}复制代码
服务端部分
在 config
里面,定义公用的 appid
和 appsecret
module.exports = {
wx: {
appId: 'wx0ef10432747d8f57',appsecret: 'cc47a6127687e999a1dffa756ff83c0e'
},mp: {
appId: 'wx0691f1dcf6e5e231',appSecret: 'c7ed875e338120f15f49476a6596eb4f'
}
}复制代码
然后通过调用小程序 官方文档 的接口,获取到 appid
传给客户端
let express = require('express');
let router = express.Router();
let request = 'request');
let config = './config');
let uril = './../../util/index')
config = Object.assign({},config.mp);
router.get('/getSession',(req,res) => {
let code = req.query.code
if (!code) {
res.json(uril.handleFail('code不能为空',10001))
}
let sessionUrl = `https://api.weixin.qq.com/sns/jscode2session?appid=${config.appId}&secret=${config.appSecret}&js_code=${code}&grant_type=authorization_code`;
request(sessionUrl,(err,response,body) => {
let result = util.handleResponse(err,body)
res.json(result)
})
})复制代码
登录接口
的编写
// 小程序授权登录
router.get('/login',51); font-weight: 700;">async function(req,res){
let userInfo = JSON.parse(req.query.userInfo);
if (!userInfo){
// 如果接口没有信息,则返回错误信息
res.json(util.handleFail('用户信息不能为空',0);">10002))
}else{
// 查询当前用户是否已经注册
let userRes = await dao.query({ openid: userInfo.openid},'users_mp');
if (userRes.code == 0){
// 如果已经注册,直接把查出来的信息返回给客户端
if (userRes.data.length >0){
res.json(util.handleSuc({
userId: userRes.data[0]._id
}))
}else{
// 如果这个用户之前没有注册,则在数据库插入用户信息
let insertData = await dao.insert(userInfo,0);">'users_mp');
if (insertData.code == 0){
let result = await dao.query({ openid: userInfo.openid },0);">'users_mp');
res.json(util.handleSuc({
userId: result.data[0]._id
}))
}else{
res.json(insertData);
}
}
}else{
res.json(userRes);
}
}
})复制代码
上述代码的 handleFail
和 handleResponse
是封装的对数据的统一处理,如果有兴趣,参见 github
地址。这里不展示代码。
需要注意的是,这种实现方式,获取 openid
的行为放在后端实现了。如果放在前端实现也可以,但是会相对比较麻烦一点。此时,suerId就已经在数据库存储,并且在本地保存了,下次登录的时候,如果有userId存在就不需要再次登录了。
H5的登录授权和分享流程
H5的登录授权略有不同。如果用户登录授权页面,发现该用户没有登录授权,则需要跳转到授权页面。 官方文档 给出的流程如下:
1 第一步:用户同意授权,获取code2 第二步:通过code换取网页授权access_token3 第三步:刷新access_token(如果需要)4 第四步:拉取用户信息(需scope为 snsapi_userinfo)5 附:检验授权凭证(access_token)是否有效
在项目中代码如下:(这里代码没有实现刷新access_token和拉取用户信息)
页面加载的时候,判断是否已经授权。
mounted(){
this.checkUserAuth();
},methods:{
// 检查用户是否授权过
checkUserAuth(){
let openId = this.$cookie.get('openId');
if(!openId){
// 如果没有登录授权,则跳转到微信提供的跳转页面。
window.location.href = API.wechatRedirect;
}else{
// 如果用户已经授权,则调用获取微信配置信息接口
this.getWechatConfig();
}
},复制代码
API.wechatRedirect:
wechatRedirect:'/api/wechat/redirect?url=http%3A%2F%2Fm.51purse.com%23%2Findex&scope=snsapi_userinfo',复制代码
「注意」
- url地址需要
encodeURIComponent
编码才可以。 -
m.51purse.com
需要与你在微信公众号后台配置的授权域名
一致!
nodejs 对登录授权回调接口的实现主要是拿到客户端的请求参数,请求微信提供的 接口
,function (req,res) {
let redirectUrl = req.query.url,scope = req.query.scope,callback = 'http://m.51purse.com/api/wechat/getOpenId';
cache.put('redirectUrl',redirectUrl);
// 获取到客户端带过来的数据,请求微信接口
let authorizeUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${config.appId}&redirect_uri=${callback}&response_type=code&scope=${scope}&state=STATE#wechat_redirect`;
res.redirect(authorizeUrl);
})
复制代码
当用户点击 确认授权
之后,会执行跳转 callbacl:http://m.51purse.com/api/wechat/getOpenId
。而这个接口也是node端实现的,具体内容如下:
,51); font-weight: 700;">let code = req.query.code;
console.log("code:"+code);
if(!code){
res.json(util.handleFail('当前未获取到授权code码'));
}// 用code换取access_token
await common.getAccessToken(code);
if(result.code == // 换取access_token成功
let data = result.data;
let expire_time = 1000 * 60 * 2;
// 往客户端写入cookie:openId
res.cookie('openId',data.openid,{ maxAge: expire_time });
let openId = data.openid;
await dao.query({ 'openid': openId },0);">'users');
0){
if (userRes.data.length>0){
// 从数据库查找到用户信息后,回调到客户端的页面
let redirectUrl = cache.get('redirectUrl');
res.redirect(redirectUrl);
}else{
let userData = await common.getUserInfo(data.access_token,openId);
await dao.insert(userData.data,0);">'users');
0){
// 从数据库查找到用户信息后,回调到客户端的页面
'redirectUrl');
res.redirect(redirectUrl);
}else{
// 返回错误信息
res.json(insertData);
}
}
}// 返回错误信息
res.json(userRes);
}
}else{
// 返回错误信息
res.json(result);
}
}
})
复制代码
「注意」:上面的代码为了简单,删除了一些不必要的代码,如有兴趣,访问gitHub。
H5分享流程
同样,如果你没有阅读过微信H5开发的 官方文档 ,建议你先阅读。关于分享,你应该阅读以下内容:
当再次回调到页面的时候,从cookie已经拿到openId了。客户端会继续执行下面的代码。获取到服务端返回的配置信息,从而初始化分享的功能。
在这之前,你需要 npm install wx-jssdk
+location.href.split('#')[0]).then(function(response){
let res = response.data;
if(res.code == 0){
let data = res.data;
wx.config({
debug: true,// 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: data.appId,136);">// 必填,公众号的唯一标识
timestamp: data.timestamp,136);">// 必填,生成签名的时间戳
nonceStr: data.nonceStr,136);">// 必填,生成签名的随机串
signature: data.signature,136);">// 必填,签名
jsApiList: data.jsApiList // 必填,需要使用的JS接口列表
})
wx.ready(()=>{
util.initShareInfo(wx);
})
}
})
}复制代码
util/index.js 里面对分享的功能进行了封装。
+name+'=([^&]*)');
let r = window.location.search.substr(1).match(reg);
if(r!=null)return decodeURIComponent(r[2]);
},initShareInfo(wx){
let shareInfo = {
title: 'xxxx',136);">// 分享标题
desc: // 分享描述
link: 'http://m.51purse.com/#/index',136);">// 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: '',136);">// 分享图标
}
wx.onMenuShareAppMessage(shareInfo);
wx.onMenuShareTimeline(shareInfo);
wx.onMenuShareQQ(shareInfo);
wx.onMenuShareQZone(shareInfo);
// 下面两种方法为新的方法,上面的方法将会被淘汰。
wx.updateAppMessageShareData(shareInfo);
wx.updateTimelineShareData(shareInfo);
}
}
复制代码
nodejs端对 /wechat/jssdk
接口的实现如下:
,51); font-weight: 700;">let url = req.query.url;
await common.getToken();
if (result.code == 0){
let token = result.data.access_token;
let params = {
// 生成随机字符串
noncestr:util.createNonceStr(),136);">// 生成时间戳
timestamp:util.createTimeStamp(),url
}
let str = util.raw(params);
'str:::' + JSON.stringify(params))
let sign = createHash('sha1').update(str).digest('hex');
res.json(util.handleSuc({
appId: config.appId,136);">// 必填,公众号的唯一标识
timestamp: params.timestamp,136);">// 必填,生成签名的时间戳
nonceStr: params.noncestr,136);">// 必填,生成签名的随机串
signature: sign,136);">// 必填,签名
jsApiList: [
'updateAppMessageShareData',0);">'updateTimelineShareData',0);">'onMenuShareTimeline',0);">'onMenuShareAppMessage',0);">'onMenuShareQQ',0);">'onMenuShareQZone',0);">'chooseWXPay'
] // 必填,需要使用的JS接口列表
}))
}
}else{
res.json(result);
}
})复制代码
以上代码主要获得基础的 token
,然后用基础 token
结合签名、时间戳、随机数等相关的参数,返回给客户端相应的参数。
需要注意的是, 基础token
和 accessToken
的区别。建议 参考文章 。
到此,微信H5接入jssdk实现分享就已经完成了。
小程序支付
小程序支付前端流程
- 获取openId
- 调起数字签名
后端支付流程
- 拼接常规参数
- 生成签名
- 拼接xml数据
- 调用下单接口
- 获取预支付Id:prepay_id
- 生成支付sdk
- 定义回调接口,接受微信支付消息
支付的主要逻辑在服务端
下面把服务端的流程通过代码的方式表述出来。首先在util中封装了一些支付需要的公共方法
);
module.exports = {
// 生成随机数
createNonceStr(){
Math.random().toString(36).substr(2,0);">15);
},136);">// 生成时间戳
createTimeStamp(){
parseInt(Date().getTime() / 1000) + ''
},136);">// 生成签名
getSign(params,key){
let string = this.raw(params) + '&key=' + key;
'md5').update(string).digest('hex');
return sign.toUpperCase();
},136);">// 生成系统的交易订单号
getTradeId(type='wx'){
let date = Date().getTime().toString();
let text = '';
let possible = '0123456789';
for(let i=0;i<5;i++){
text += possible.charAt(Math.floor(Math.random() * possible.length))
}
return (type == 'wx'?'ImoocWxJuZi':'ImoocMpJuZi') + date + text;
},136);">// Object 转换成json并排序
raw(args){
let keys = Object.keys(args).sort();
let obj = {};
keys.forEach((key)=>{
obj[key] = args[key];
})
// {a:1,b:2} => &a=1&b=2
// 将对象转换为&分割的参数
let val = let k in obj){
val += '&' + k + '=' +obj[k];
}
return val.substr(1);
}
}复制代码
下面是对支付的方法的封装,其中调用了util中的函数。客户端调用的就是下面的 order
方法。
/**
* 微信小程序、H5通用支付封装
*/
let config = require('./../pay/config')
let request = require('request')
let util = require('../../util/util')
let createHash = require('create-hash')
let xml = require('xml2js')
config = config.mch;
module.exports = {
order: function (appid,attach,body,openid,total_fee,notify_url,ip){
return new Promise((resolve,reject)=>{
let nonce_str = util.createNonceStr();
let out_trade_no = util.getTradeId('mp');
// 支付前需要先获取支付签名
let sign = this.getPrePaySign(appid,ip,nonce_str,out_trade_no);
// 通过参数和签名组装xml数据,用以调用统一下单接口
let sendData = this.wxSendData(appid,out_trade_no,sign);
let self = this;
let url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
request({
url,method: 'POST',body: sendData
},function (err,body) {
if (!err && response.statusCode == 200) {
xml.parseString(body.toString('utf-8'),(error,res)=>{
if(!error){
let data = res.xml;
console.log('data:' + JSON.stringify(data));
if (data.return_code[0] == 'SUCCESS' && data.result_code[0] == 'SUCCESS'){
// 获取预支付的ID
let prepay_id = data.prepay_id || [];
let payResult = self.getPayParams(appid,prepay_id[0]);
resolve(payResult);
}
}
})
} else {
resolve(util.handleFail(err));
}
})
})
},// 生成预支付的签名
getPrePaySign: function (appid,out_trade_no) {
let params = {
appid,mch_id: config.mch_id,spbill_create_ip: ip,trade_type: 'JSAPI'
}
let string = util.raw(params) + '&key=' + config.key;
let sign = createHash('md5').update(string).digest('hex');
return sign.toUpperCase();
},// 签名成功后 ,根据参数拼接组装XML格式的数据,调用下单接口
wxSendData: function (appid,sign) {
let data = '<xml>' +
'<appid><![CDATA[' + appid + ']]></appid>' +
'<attach><![CDATA[' + attach + ']]></attach>' +
'<body><![CDATA[' + body + ']]></body>' +
'<mch_id><![CDATA[' + config.mch_id + ']]></mch_id>' +
'<nonce_str><![CDATA[' + nonce_str + ']]></nonce_str>' +
'<notify_url><![CDATA[' + notify_url + ']]></notify_url>' +
'<openid><![CDATA[' + openid + ']]></openid>' +
'<out_trade_no><![CDATA[' + out_trade_no + ']]></out_trade_no>' +
'<spbill_create_ip><![CDATA[' + ip + ']]></spbill_create_ip>' +
'<total_fee><![CDATA[' + total_fee + ']]></total_fee>' +
'<trade_type><![CDATA[JSAPI]]></trade_type>' +
'<sign><![CDATA['+sign+']]></sign>' +
'</xml>'
return data;
},getPayParams:function(appId,prepay_id){
let params = {
appId,timeStamp:util.createTimeStamp(),nonceStr:util.createNonceStr(),package: 'prepay_id=' + prepay_id,signType:'MD5'
}
let paySign = util.getSign(params,config.key);
params.paySign = paySign;
return params;
}
}复制代码
最后定义 /pay/payWallet
的支付接口,里面调用公用的order方法。
,51); font-weight: 700;">let openId = req.query.openId;//用户的openid
let appId = config.appId;//应用的ID
let attach = "小程序支付课程体验";//附加数据
let body = "欢迎学习首门支付专项课程";//支付主体内容
let total_fee = req.query.money;//支付总金额
let notify_url = "http://localhost:3000/api/mp/pay/callback"
let ip = "123.57.2.144";
wxpay.order(appId,openId,ip).then((result)=>{
res.json(util.handleSuc(result));
}).catch((result)=>{
res.json(util.handleFail(result.toString()))
});
})复制代码
这里的流程请参见 官方描述 。官方描述的非常清楚,这儿就不描述更多了,其实主要就是拼接一些参数,获取 签名
。然后根据签名加上其他需要的 参数
(参见上述代码)再凭借xml的数据。然后再调用统一下单接口 https://api.mch.weixin.qq.com/pay/unifiedorder
。生成 prepay_id
之后,生成小程序端需要的一些参数,然后把这些参数返回个小程序客户端,供小程序的客户端调用微信小程序的支付功能。
小程序前端支付非常简单,只是简单的调用服务端提供的 payWallet
接口,传入 openId
和 money
即可。然后获取到相应的参数,调用微信提供的 requestPayment
拉起支付即可。
主要代码逻辑如下:
pay() {
app.get(Api.payWallet,{
openId: Store.getItem('openId'),money: this.data.index
}).then((res) => {
// 支付
wx.requestPayment({
timeStamp: res.timeStamp,51); font-weight: 700;">nonceStr: res.nonceStr,51); font-weight: 700;">package: res.package,51); font-weight: 700;">signType: res.signType,51); font-weight: 700;">paySign: res.paySign,51); font-weight: 700;">success: function (errmsg) {
if (errmsg == 'requestPayment:ok') {
wx.showToast({
title: '支付成功',51); font-weight: 700;">icon: 'success'
});
}
},51); font-weight: 700;">fail: function (res) {
if (res.errMsg == 'requestPayment:fail cancel') {
.showToast({
'支付取消',0);">'none'
});
} else {
title: res.errmsg,0);">'none'
});
}
}
})
});
}
复制代码
到这里,小程序端的支付功能就已经实现了。