该网站不安全,请不要输入密码 |
op=>operation: openid判断是否登录授权 op2=>operation: 根据wx.login获取code op3=>operation: 调用服务端根据code换取openid op4=>operation: 通过用户授权,获取信息,存到数据库 op->op2->op3->op4 复制代码 如果你从来没有阅读过小程序登录授权的文档,建议你看一下下面的地址: 服务端官方文档 客户端文档 |
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 = require('request'); let config = require('./config'); let uril = require('./../../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, response, body) res.json(result) }) })复制代码 登录接口 的编写 // 小程序授权登录 router.get('/login',async function(req,res){ let userInfo = JSON.parse(req.query.userInfo); if (!userInfo){ // 如果接口没有信息,则返回错误信息 res.json(util.handleFail('用户信息不能为空',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,'users_mp'); if (insertData.code == 0){ let result = await dao.query({ openid: userInfo.openid }, '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的登录授权略有不同。如果用户登录授权页面,发现该用户没有登录授权,则需要跳转到授权页面。 官方文档 给出的流程如下: 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%2F%23%2Findex&scope=snsapi_userinfo',复制代码 |
nodejs 对登录授权回调接口的实现主要是拿到客户端的请求参数,请求微信提供的 接口 // 用户授权重定向 router.get('/redirect',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端实现的,具体内容如下: // 用code换取access_token的方法 exports.getAccessToken = function(code){ let token_url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${config.appId}&secret=${config.appSecret}&code=${code}&grant_type=authorization_code`; return new Promise((resolve, reject) => { request.get(token_url, function (err, response, body) { let result = util.handleResponse(err, response, body); resolve(result); }) }); } // 根据code获取用户的OpenId router.get('/getOpenId',async function(req,res){ let code = req.query.code; console.log("code:"+code); if(!code){ res.json(util.handleFail('当前未获取到授权code码')); }else{ // 用code换取access_token let result = await common.getAccessToken(code); if(result.code == 0){ // 换取access_token成功 let data = result.data; let expire_time = 1000 * 60 * 60 * 2; // 往客户端写入cookie:openId res.cookie('openId', data.openid, { maxAge: expire_time }); let openId = data.openid; let userRes = await dao.query({ 'openid': openId },'users'); if (userRes.code == 0){ if (userRes.data.length>0){ // 从数据库查找到用户信息后,回调到客户端的页面 let redirectUrl = cache.get('redirectUrl'); res.redirect(redirectUrl); }else{ let userData = await common.getUserInfo(data.access_token, openId); let insertData = await dao.insert(userData.data,'users'); if (insertData.code == 0){ // 从数据库查找到用户信息后,回调到客户端的页面 let redirectUrl = cache.get('redirectUrl'); res.redirect(redirectUrl); }else{ // 返回错误信息 res.json(insertData); } } }else{ // 返回错误信息 res.json(userRes); } }else{ // 返回错误信息 res.json(result); } } }) 复制代码 「注意」:上面的代码为了简单,删除了一些不必要的代码,如有兴趣,访问gitHub。 |
同样,如果你没有阅读过微信H5开发的 官方文档 ,建议你先阅读。关于分享,你应该阅读以下内容: 当再次回调到页面的时候,从cookie已经拿到openId了。客户端会继续执行下面的代码。获取到服务端返回的配置信息,从而初始化分享的功能。 在这之前,你需要 npm install wx-jssdk // 这个信息统一定义在api.js中,这里为了方便,放在前面,便于查看。 API.wechatConfig: /api/wechat/jssdk // 获取微信配置信息 getWechatConfig(){ this.$http.get(API.wechatConfig+'?url='+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, // 必填,公众号的唯一标识 timestamp: data.timestamp, // 必填,生成签名的时间戳 nonceStr: data.nonceStr, // 必填,生成签名的随机串 signature: data.signature,// 必填,签名 jsApiList: data.jsApiList // 必填,需要使用的JS接口列表 }) wx.ready(()=>{ util.initShareInfo(wx); }) } }) }复制代码 util/index.js 里面对分享的功能进行了封装。 export default { //获取浏览器地址栏参数值 getUrlParam(name){ let reg = new RegExp('(^|&)'+name+'=([^&]*)'); let r = window.location.search.substr(1).match(reg); if(r!=null)return decodeURIComponent(r[2]); }, initShareInfo(wx){ let shareInfo = { title: 'xxxx', // 分享标题 desc: 'xxxx', // 分享描述 link: 'http://m.51purse.com/#/index', // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致 imgUrl: '', // 分享图标 } wx.onMenuShareAppMessage(shareInfo); wx.onMenuShareTimeline(shareInfo); wx.onMenuShareQQ(shareInfo); wx.onMenuShareQZone(shareInfo); // 下面两种方法为新的方法,上面的方法将会被淘汰。 wx.updateAppMessageShareData(shareInfo); wx.updateTimelineShareData(shareInfo); } } 复制代码 nodejs端对 /wechat/jssdk 接口的实现如下: // common.getToken()方法 获取基础接口的Token exports.getToken = function(){ let token = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.appId}&secret=${config.appSecret}`; return new Promise((resolve, reject)=>{ request.get(token, function (err, response, body) { let result = util.handleResponse(err, response, body); resolve(result); }) }) } ---- router.get('/jssdk',async function(req,res){ let url = req.query.url; let result = await common.getToken(); if (result.code == 0){ let token = result.data.access_token; let params = { // 生成随机字符串 noncestr:util.createNonceStr(), // 生成时间戳 timestamp:util.createTimeStamp(), url } let str = util.raw(params); console.log('str:::' + JSON.stringify(params)) let sign = createHash('sha1').update(str).digest('hex'); res.json(util.handleSuc({ appId: config.appId, // 必填,公众号的唯一标识 timestamp: params.timestamp, // 必填,生成签名的时间戳 nonceStr: params.noncestr, // 必填,生成签名的随机串 signature: sign,// 必填,签名 jsApiList: [ 'updateAppMessageShareData', 'updateTimelineShareData', 'onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareQZone', 'chooseWXPay' ] // 必填,需要使用的JS接口列表 })) } }else{ res.json(result); } })复制代码 以上代码主要获得基础的 token ,然后用基础 token 结合签名、时间戳、随机数等相关的参数,返回给客户端相应的参数。 需要注意的是, 基础token 和 accessToken 的区别。建议 参考文章 。 到此,微信H5接入jssdk实现分享就已经完成了。 |
小程序支付前端流程
后端支付流程
支付的主要逻辑在服务端 下面把服务端的流程通过代码的方式表述出来。首先在util中封装了一些支付需要的公共方法 /** * 公共函数定义 */ let createHash = require('create-hash'); module.exports = { // 生成随机数 createNonceStr(){ return Math.random().toString(36).substr(2,15); }, // 生成时间戳 createTimeStamp(){ return parseInt(new Date().getTime() / 1000) + '' }, // 生成签名 getSign(params, key){ let string = this.raw(params) + '&key=' + key; let sign = createHash('md5').update(string).digest('hex'); return sign.toUpperCase(); }, // 生成系统的交易订单号 getTradeId(type='wx'){ let date = new 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; }, // 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 = ''; for(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, attach, body, openid, total_fee, notify_url, ip, nonce_str, out_trade_no); // 通过参数和签名组装xml数据,用以调用统一下单接口 let sendData = this.wxSendData(appid, attach, body, openid, total_fee, notify_url, ip, nonce_str, 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, response, 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, attach, body, openid, total_fee, notify_url, ip, nonce_str, out_trade_no) { let params = { appid, attach, body, mch_id: config.mch_id, nonce_str, notify_url, openid, out_trade_no, spbill_create_ip: ip, total_fee, 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, attach, body, openid, total_fee, notify_url, ip, nonce_str, out_trade_no,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方法。 // 小程序支付 router.get('/pay/payWallet',function(req,res){ 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,attach,body,openId,total_fee,notify_url,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, nonceStr: res.nonceStr, package: res.package, signType: res.signType, paySign: res.paySign, success: function (errmsg) { if (errmsg == 'requestPayment:ok') { wx.showToast({ title: '支付成功', icon: 'success' }); } }, fail: function (res) { if (res.errMsg == 'requestPayment:fail cancel') { wx.showToast({ title: '支付取消', icon: 'none' }); } else { wx.showToast({ title: res.errmsg, icon: 'none' }); } } }) }); } 复制代码 到这里,小程序端的支付功能就已经实现了。 |