微信相关文档:

官方文档:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html
以下使用测试号的地方都需要去测试号网址登陆,获取相关信息
测试号申请地址-https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
微信 JS 接口签名校验工具:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign
微信支付课程相关问题梳理: https://www.imooc.com/article/301820

导学

1. 为什么要学支付/分享

  1. 涉及业务比较广

image.png

  1. 应用场景多

image.png

  1. 公司业务当中需求强烈、前端初中级知识储备不足
  2. 前端开发成本高(SDK、签名、安全域名、支付目录、服务器、部署流程)

    2. 为何转向全栈

  • 支付链条长,少- -端都无法完成
  • 纯前端有局限,不利于我们职业发展和个人成长
  • 独立应对更多全栈业务、得心应手

    3. 课程主要讲什么

    image.png

    4. 可以学到什么

  • 掌握微信SDK的各种技能(授权、分享、支付)

  • 掌握前后端整体的支付闭环
  • 对项目架构、模块设计、开发规范、公共机制等多角度学习和掌握.
  • 掌握Vue2.6、 Node、小程序以及小程序云等前端技术栈
  • 学会Nginx部署、Node部署(前端非常 难接触的知识)

    5. 分享支付流程

    image.png

    6. 整体架构设计

    image.pngimage.png

    7. 公共机制

  • 目录结构

  • 工具函数
  • 开发规范
  • H5代理
  • log4.js
  • H5自适应
  • api封装
  • 路由封装
  • 支付封装
  • 。。。

    8. 目录:

  1. H5开发,接入微信公众号
  2. Express后台实现
  3. MongoDB使用
  4. 小程序授权分享
  5. 小程序云授权分享
  6. 小程序支付
  7. 小程序云支付
  8. 公众号H5支付
  9. 项目部署

源码github:https://github.com/ynzy/wx_pay_share.git

一、H5开发

1. vue前端架构设计

  • 目录结构定义
  • 公共函数编写
  • 开发规范定义
  • 环境配置、统一请求处理、错误机制、Loading机制
  • 组件封装

    axios二次封装

    ```javascript import axios from ‘axios’ import { storage, sessionStorage } from ‘@/utils/storage’ // import { Message } from ‘element-ui’ //引入nprogress // import NProgress from ‘nprogress’ // import ‘nprogress/nprogress.css’

import router from ‘../router/index’

// NProgress.inc(0.2) // NProgress.configure({ easing: ‘ease’, speed: 500, showSpinner: false })

const service = axios.create({ baseURL: process.env.VUE_APP_BASE_URL, timeout: 5000, // 设置超时时间 headers: { ‘Content-Type’: ‘application/json; charset=utf-8’ } })

// http请求拦截器 service.interceptors.request.use( config => { // NProgress.start() config.headers[‘Authorization’] = sessionStorage.get(‘token’) config.contentType && (config.headers[‘Content-Type’] = config.contentType) return config }, error => { return Promise.reject(error) } ) //http 200 状态码下的异常map const erorrMap = { 0: ‘请求成功’, 200: ‘请求成功’, 201: ‘创建成功’, 204: ‘删除成功’, 400: ‘请求的地址不存在或者包含不支持的参数’, 401: ‘未授权’, 403: ‘被禁止访问’, 404: ‘请求的资源不存在’, 422: ‘[POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误’, 500: ‘内部错误’ } // http响应拦截器 service.interceptors.response.use( res => { // NProgress.done() //可以根据后端的系统而相应的做调整 // console.log(res.data) if (res.data.code == 0) { return res.data } return Promise.reject(res.data) }, async error => { if (error.request) { if (error.request.status === 0) { //超时 } } else if (error.response) { if (error.response.status === 400) { //请求参数有问题 // Message.error(error) } else if (error.response.status === 404) { //未找到资源 console.log(‘未找到资源’) } else if (error.response.status === 401) { //请先登录 console.log(‘请先登录’) } else if (error.response.status === 500) { //服务器异常 console.log(‘服务器异常’) } } return Promise.reject(error) } )

export default service

  1. <a name="aVuNt"></a>
  2. ### api请求封装:
  3. 后面涉及到前台的请求就不一一写入,都放在这里了,
  4. ```javascript
  5. // api/auth.js
  6. import request from '@/utils/http'
  7. import { awaitWrap } from '@/utils/util'
  8. /**
  9. * 获取微信配置
  10. * @param {*} href 路由地址 后台根据当前url地址进行签名
  11. */
  12. export const wechatConfig = href => {
  13. return awaitWrap(
  14. request({
  15. url: `/api/wechat/jssdk?url=${href}`,
  16. method: 'get'
  17. })
  18. )
  19. }
  20. /**
  21. * 获取用户信息
  22. * @param {*} href
  23. * @returns
  24. */
  25. export const getUserInfo = () => {
  26. return awaitWrap(
  27. request({
  28. url: `/api/wechat/getUserInfo`,
  29. method: 'get'
  30. })
  31. )
  32. }
  33. /**
  34. * 获取微信支付信息
  35. * @param {*} href
  36. * @returns
  37. */
  38. export const payWallet = () => {
  39. return awaitWrap(
  40. request({
  41. url: `/api/wechat/pay/payWallet`,
  42. method: 'get'
  43. })
  44. )
  45. }
  46. /**
  47. * 微信重定向
  48. * @param {*} url 重定向地址
  49. * encodeURIComponent('http://m.imooc.com/#/index')
  50. * http%3A%2F%2Fm.51purse.com%2F%23%2Findex
  51. * @returns
  52. */
  53. export const wechatRedirect = url => {
  54. url = window.encodeURIComponent(url)
  55. return `/api/wechat/redirect?url=${url}&scope=snsapi_userinfo`
  56. }
  57. /**
  58. * 获取当前地理位置信息
  59. * @param {*} href
  60. * @returns
  61. */
  62. export const getLocation = ({ ...params }) => {
  63. return awaitWrap(
  64. request({
  65. url: `/api/qqMap/getLocation`,
  66. method: 'get',
  67. params
  68. })
  69. )
  70. }

公共工具库

  1. // /utils/util
  2. /**
  3. * 处理await成功失败信息
  4. * @param {*} promise
  5. */
  6. export const awaitWrap = promise => {
  7. return promise.then(data => [null, data]).catch(err => [err, null])
  8. }
  9. /**
  10. * 处理空的参数
  11. * @param obj
  12. * @returns
  13. */
  14. export const cleanParams = function(obj) {
  15. return Object.keys(obj).map(key => (obj[key] = ''))
  16. }
  17. /**
  18. * 获取浏览器地址栏参数值
  19. * @param {*} name 参数名
  20. * @returns
  21. */
  22. export const getUrlParam = function(name) {
  23. let reg = new RegExp('(^|&)' + name + '=([^&]*)')
  24. let r = window.location.search.substr(1).match(reg)
  25. if (r != null) return decodeURIComponent(r[2])
  26. }

2. 微信授权流程讲解

2.1 概念:

  1. 业务域名、JS接口安全域名、网页授权域名
  2. 开发者工具(添加开发者微信号)、人员设置(添加运营者微信号)
  3. 网页授权access_token和普通access_toekn
  4. UnionID

    2.2 授权流程

  5. 配置授权回调地址,跳转微信授权页面

  6. 用户同意授权,获取code
  7. 通过code换取网页授权access_token
  8. 拉去用户信息(需scope为snsapi_userinfo)

    2. 配置

    2.3 JSSDK调用流程

  9. 绑定域名

  10. 引入js文件
  11. 通过config接口注入权限验证配置(接口签名)
  12. 通过ready接口处理成功验证

    3. h5添加接口代理、域名解析

    3.1 接口代理

  • 配置主机
  • 设置端口
  • 拦截请求

    1. // vue.config.js
    2. module.exports = {
    3. devServer: {
    4. // 设置主机地址
    5. host: 'm.51purse.com',
    6. // 设置默认端口
    7. port: 80,
    8. // 设置代理
    9. proxy: {
    10. /**
    11. * changeOrigin:true
    12. * /api/test
    13. * http://localhost:5000/test
    14. * changeOrigin:false
    15. * /api/test
    16. * http://localhost:5000/api/test
    17. */
    18. '/api': {
    19. // 设置目标API地址
    20. target: 'http://localhost:3000',
    21. // 如果要代理 websockets
    22. ws: false,
    23. // 将主机标头的原点改为目标URL
    24. changeOrigin: false
    25. }
    26. }
    27. }
    28. }

    3.2 Host域名解析

  • 修改本地host文件

    1. window: C:Windows\System32\drivers\etc\HOSTS
    2. MAC: vi /etc/hosts
  • 在host文件中添加如下

    1. 127.0.0.1 m.51purse.com
    2. 127.0.0.1 m.imooc.com

    image.png

  • 通过软件修改(尽量不要用,容易出莫名的问题)

    1. mac设置本地映射域名

    我在win上,按照上面的讲解配置是没有问题的,但是在mac上遇到了坑,具体说一下这里
    mac上不要用软件去进行修改,也不要用vscode,可能是我vscode配置有问题,修改了hosts文件之后,由于格式不正确,没法做到域名映射
    直接使用vim编辑器修改hosts文件,修改之前最好保存一份源本放到桌面保留

    1. cd /etc
    2. vim hosts
    1. ##
    2. # Host Database
    3. #
    4. # localhost is used to configure the loopback interface
    5. # when the system is booting. Do not change this entry.
    6. ##
    7. 127.0.0.1 localhost
    8. 255.255.255.255 broadcasthost
    9. ::1 localhost
    10. 127.0.0.1 api.example.com
    11. 127.0.0.1 m.imooc.com

    这里我配置了两个域名映射,开始win上用的也是m.imooc.com,但是这个是一个已经有的慕课网移动端地址,为了避免出现问题,配置了一个自定义域名api.example.com,后面都改用这个了

    2. 内网穿透设置外网访问

    除了本地映射域名以外,我们可以采用内网穿透达到本地开发使用域名访问的目的。并且可以在真机上进行调试
    参考文章:

  • https://juejin.im/post/5e7e241f6fb9a03c317608c8

  • https://www.jianshu.com/p/6b31605c0d91

实例代码:

  1. // vue.config.js
  2. const path = require('path') // 引入path模块
  3. const process = require('process')
  4. function resolve(dir) {
  5. return path.join(__dirname, dir) // path.join(__dirname)设置绝对路径
  6. }
  7. let host = null
  8. if (process.env.VUE_APP_BASE_URL) {
  9. host = process.env.VUE_APP_BASE_URL.split('//')[1]
  10. }
  11. module.exports = {
  12. lintOnSave: false, // 是否开启eslint
  13. outputDir: process.env.outputDir, // build输出目录
  14. chainWebpack: config => {
  15. config.resolve.alias
  16. // set第一个参数:设置的别名,第二个参数:设置的路径
  17. .set('@', resolve('./src'))
  18. // 发布模式
  19. config.when(process.env.NODE_ENV === 'production', config => {
  20. config.plugin('html').tap(args => {
  21. args[0].isProd = true
  22. return args
  23. })
  24. })
  25. // 开发模式
  26. config.when(process.env.NODE_ENV === 'development', config => {
  27. config.plugin('html').tap(args => {
  28. args[0].isProd = false
  29. return args
  30. })
  31. })
  32. },
  33. css: {
  34. extract: false
  35. },
  36. devServer: {
  37. host: host || '',
  38. disableHostCheck: true, // 绕过主机检查,解决Invalid Host header问题
  39. open: false, // 是否自动弹出浏览器页面
  40. port: '80',
  41. https: false, // 是否使用https协议
  42. hotOnly: true, // 是否开启热更新
  43. proxy: {
  44. /**
  45. * changeOrigin:true
  46. * /api/test
  47. * http://localhost:5000/test
  48. * changeOrigin:false
  49. * /api/test
  50. * http://localhost:5000/api/test
  51. */
  52. '/api': {
  53. // 设置目标API地址
  54. target: 'http://localhost:3000',
  55. // 如果要代理 websockets
  56. ws: false,
  57. // 将主机标头的原点改为目标URL
  58. changeOrigin: false
  59. }
  60. /* '/qqMap': {
  61. // 设置目标API地址
  62. target: 'http://localhost:3000',
  63. // 如果要代理 websockets
  64. ws: false,
  65. secure: true, // 使用的是http协议则设置为false,https协议则设置为true
  66. // 将主机标头的原点改为目标URL
  67. changeOrigin: false
  68. } */
  69. }
  70. }
  71. }

内网穿透时,host设置为空,我们将host设置成动态之后,这里就不需要改动了,我们只需要改动env环境里的base_url,
如果我们还是使用本地映射,env中配置为

  1. # 环境名
  2. NODE_ENV="dev"
  3. # 服务器地址
  4. VUE_APP_BASE_URL="http://api.example.com"
  5. # VUE_APP_BASE_URL=""

如果使用内网穿透配置为,url为空

  1. # 环境名
  2. NODE_ENV="dev"
  3. # 服务器地址
  4. # VUE_APP_BASE_URL="http://api.example.com"
  5. VUE_APP_BASE_URL=""

如果不这样设置会出现api调用请求接口地址加了两遍URL的错误,目前还不知道原因

3.3 以测试号为例完成2.2的微信授权流程

1. 配置授权回调页面域名:

在测试号页面找到设置网页账号-修改image.png
填写回调页面域名:此域名可以是服务端域名,或者在添加的本地域名解析地址
image.png

2. 配置vue.config.js

以上已经配置过了,这里就不做配置了
重新启动项目,80端口可能启动不了,
win端使用管理员命令行启动
mac授权管理员启动

  1. sudo yarn serve

3. 页面中填写回调地址

在vue的mounted函数中可以直接测试
为了方便查看,使用变量把需要填写的参数进行定义,也可以直接把以上参数拼接到href上

  1. let appid = 'wx9790364d20b47d95'
  2. let url = window.encodeURIComponent('http://m.imooc.com')
  3. let scope = 'snsapi_userinfo'
  4. window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${url}&response_type=code&scope=${scope}&state=STATE#wechat_redirect`
  • appid: 公众号的appid

image.png

  • url:回调地址,也就是要跳转回你要显示的网页路径,使用encodeURIComponent进行编码
  • scope: 只需要设定为snsapi_userinfo,用来获取用户基本信息

设置好之后会跳转到一下页面路径
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx9790364d20b47d95&redirect_uri=http%3A%2F%2Fm.imooc.com&response_type=code&scope=snsapi_userinfo&state=STATE&uin=MTMwNTU4MzAzMA%3D%3D&key=4e8265c6a6df76ae82d0f21c8cb070f7e5b78c942db23c8b97f19b4245c61fe4aae158fa1ddfced7417d80e75a240697&pass_ticket=UyCd4WPsXsQM4l+/IVspaqIe3dRSfIpO9QD2RaNU3OiETR9uL9ec2meiZ4U0wzZEZjwRb5AcQ1jLPgkx1oWt5g==
image.png
点击同意会跳回我们的回调页面,跳回之后可以拿到code值

  1. http://m.imooc.com/?code=011SlWBd1jGdgw0xNiAd13FeCd1SlWBl&state=STATE#/index

image.png

4. H5接入微信分享

  1. 定义请求地址 ```javascript import request from ‘@/utils/http’ import { awaitWrap } from ‘@/utils/util’

/**

  • 获取微信配置
  • @param {} href 路由地址 后台根据当前url地址进行签名 / export const wechatConfig = href => { return awaitWrap( request({ url: /api/wechat/jssdk?url=${href}, method: ‘get’ }) ) } ```
  1. 微信授权、注入openId

前台通过cookie获取openId
后台通过授权回调链接获取openId设置token

  1. 获取签名信息配置config

    1. let [err, res] = await wxchatConfig(location.href.split('#')[0])
    2. if (err) {
    3. console.log(err)
    4. // return this.$message.error(err.message || '获取微信配置失败')
    5. }
    6. console.log(res)
    7. let data = res.data
    8. wx.config({
    9. debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
    10. appId: data.appId, // 必填,公众号的唯一标识
    11. timestamp: data.timestamp, // 必填,生成签名的时间戳
    12. nonceStr: data.nonceStr, // 必填,生成签名的随机串
    13. signature: data.signature, // 必填,签名
    14. jsApiList: data.jsApiList // 必填,需要使用的JS接口列表
    15. })
    16. wx.ready(() => {
    17. // initShareInfo(wx)
    18. })
  2. 定义分享公共信息 ```javascript // tuils/wx.js export const initShareInfo = function(wx) { let shareInfo = { title: ‘慕课支付分享专项课程’, // 分享标题 desc: ‘欢迎学习慕课支付分享专项课程’, // 分享描述 link: ‘http://m.imooc.com/#/index‘, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致 imgUrl: ‘’ // 分享图标 } // wx.onMenuShareAppMessage(shareInfo) // wx.onMenuShareTimeline(shareInfo) // wx.onMenuShareQQ(shareInfo) // wx.onMenuShareQZone(shareInfo) wx.updateAppMessageShareData(shareInfo) wx.updateTimelineShareData(shareInfo) }

  1. <a name="kmck1"></a>
  2. # 二、Express后台实现
  3. <a name="a7Ufn"></a>
  4. ## 1. 初始化Express项目
  5. ```javascript
  6. Npm Install express-generator-g
  7. express - h
  8. express Imooc-pay_server
  9. npm i & node bin/www || pm2 start bin/www

生成项目之后,修改运行项目命令
先全局安装nodemon: npm install -g nodemon,此插件可以监听文件改动,自动重启服务

  1. "scripts": {
  2. "start": "node ./bin/www",
  3. "serve": "nodemon ./bin/www"
  4. },

在bin/www执行文件中输入我们的项目地址可以方便我们查看我们的服务器地址

  1. function onListening() {
  2. //note: 输出项目地址
  3. console.log(`启动服务,地址:http://localhost:${port}/`);
  4. var addr = server.address();
  5. var bind = typeof addr === 'string'
  6. ? 'pipe ' + addr
  7. : 'port ' + addr.port;
  8. debug('Listening on ' + bind);
  9. }

2. 微信用户授权

  • Request, memory-cache

    • Request是后台用来请求接口用的,相当于前台的ajax
    • memory-cache是后台简单的缓存管理模块,相当于Redis
      1. yarn add request memory-cache --save--dev
  • Vue 调用 Node, Node 调用微信

    • vue通过调用node接口,完成授权获取code
    • node通过授权的code调取微信接口,获取用户信息(主要是openid)
  • 微信授权跳转
    • 授权成功之后,跳转到前端的回调页面地址
  • 根据 code 获取 openid 信息向客户端写入 Cookie
    • 前端通过cookie里有没有openid来判断用户是否有授权

vue端:

  1. // app.vue
  2. <template>
  3. <div id="app">
  4. <router-view />
  5. </div>
  6. </template>
  7. <script>
  8. import { wechatRedirect, wechatConfig } from '@/api/auth'
  9. import wx from 'weixin-js-sdk'
  10. import { initShareInfo } from '@/utils/wx'
  11. export default {
  12. data() {
  13. return {}
  14. },
  15. methods: {
  16. // 检查用户是否授权过
  17. checkUserAuth() {
  18. let openId = this.$cookie.get('openId')
  19. if (!openId) {
  20. // location.origin 是我们的回调地址 http://api.example.com
  21. console.log(wechatRedirect(location.origin))
  22. // 请求后台接口进行授权回调
  23. window.location.href = wechatRedirect(location.origin)
  24. return
  25. // 前台模拟微信授权回调
  26. let appid = 'wx9790364d20b47d95'
  27. let url = window.encodeURIComponent('http://m.imooc.com')
  28. let scope = 'snsapi_userinfo'
  29. // window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${url}&response_type=code&scope=${scope}&state=STATE#wechat_redirect`
  30. } else {
  31. this.getWechatConfig()
  32. }
  33. },
  34. // 获取微信配置信息
  35. async getWechatConfig() {
  36. let [err, res] = await wechatConfig(location.href.split('#')[0])
  37. if (err) {
  38. console.log(err)
  39. // return this.$message.error(err.message || '获取微信配置失败')
  40. }
  41. console.log(res)
  42. return
  43. let data = res.data
  44. wx.config({
  45. debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
  46. appId: data.appId, // 必填,公众号的唯一标识
  47. timestamp: data.timestamp, // 必填,生成签名的时间戳
  48. nonceStr: data.nonceStr, // 必填,生成签名的随机串
  49. signature: data.signature, // 必填,签名
  50. jsApiList: data.jsApiList // 必填,需要使用的JS接口列表
  51. })
  52. wx.ready(() => {
  53. // initShareInfo(wx)
  54. })
  55. // this.$message.success(res.message || '获取成功')
  56. }
  57. },
  58. mounted() {
  59. this.checkUserAuth()
  60. var url = window.encodeURIComponent('http://m.imooc.com')
  61. },
  62. components: {}
  63. }
  64. </script>
  65. <style lang="less" scoped>
  66. /* @import url(); 引入css类 */
  67. </style>

node端:

  1. // routes/pay/config.js
  2. /**
  3. * 微信相关配置参数
  4. */
  5. module.exports = {
  6. mch:{
  7. mch_id: '',
  8. key:''
  9. },
  10. // 微信公众号
  11. wx:{
  12. appId:'wx9790364d20b47d95',
  13. appSecret:'efc5c7ef02bd193ed130c79c13e99e3b'
  14. },
  15. mp:{
  16. appId:'wxc447e673378bdd41',
  17. appSecret:''
  18. }
  19. }
  1. // routes/pay/wx.js
  2. /**
  3. * 微信开发
  4. */
  5. let express = require('express')
  6. let router = express.Router()
  7. let cache = require('memory-cache');
  8. let {wx:config} = require('./config');
  9. let request = require('request');
  10. router.get('/test', function (req, res) {
  11. res.json({
  12. code: 0,
  13. data: 'test',
  14. message: ''
  15. })
  16. })
  17. /**
  18. * 用户授权重定向
  19. * @query url
  20. * @query scope
  21. * @return
  22. */
  23. router.get('/redirect', function (req, res, next) {
  24. let redirectUrl = req.query.url // 最终重定向的地址->跳转回前端的页面
  25. let scope = req.query.scope // 作用域
  26. // console.log(redirectUrl);
  27. let callback = `${redirectUrl}/api/wechat/getOpenId`; // 授权回调地址,用来获取openId
  28. cache.put('redirectUrl', redirectUrl); // 通过cache 缓存重定向地址
  29. 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`;
  30. res.redirect(authorizeUrl) //服务端重定向
  31. });
  32. /**
  33. * 根据code获取用户的OpenId
  34. * @query code 用户授权之后获取到的code
  35. */
  36. router.get('/getOpenId', async function (req, res) {
  37. let code = req.query.code;
  38. console.log("code:" + code);
  39. if (!code) {
  40. res.json({
  41. code: 1001,
  42. data: '',
  43. message: '当前未获取授权code码'
  44. })
  45. return
  46. }
  47. let access_token_url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${config.appId}&secret=${config.appSecret}&code=${code}&grant_type=authorization_code`
  48. request.get(access_token_url, function (err,response,body) {
  49. // 这里的response参数命名不要和router中的res冲突,不然会覆盖res
  50. if (err || response.statusCode != '200') {
  51. // 请求失败
  52. return
  53. }
  54. console.log('body:',body);
  55. let data = JSON.parse(body)
  56. // console.log(data);
  57. // 请求成功,将openid存储到cookie
  58. let expire_time = 1000 * 60 * 1 // 过期时间
  59. console.log(data.openid);
  60. res.cookie('openId', data.openid, { maxAge: expire_time });
  61. let redirectUrl = cache.get('redirectUrl') //获取缓存中的重定向地址
  62. res.redirect(redirectUrl) // 重定向跳回前端页面
  63. })
  64. })
  65. module.exports = router

重构微信用户授权node端代码

目前为止我们完成了微信授权从前台->后台->微信服务器->后台->前台的整个授权流程,但是这样把代码全都放在router里面显得很臃肿也不清晰,我们需要重构
代码分为

  • router
  • 调取微信的部分抽离成微信公共方法
  • 请求返回值封装为公共方法

处理请求成功失败的封装:

  1. // utils/util.js
  2. module.exports = {
  3. /**
  4. * 处理微信请求返回信息统一处理
  5. * @param {*} err
  6. * @param {*} response
  7. * @param {*} body
  8. */
  9. handleResponse(err, response, body) {
  10. if (err || response.statusCode != '200') {
  11. // 微信服务器请求失败
  12. this.handleFail(err, 10009)
  13. return
  14. }
  15. // console.log('body:', body)
  16. let data = JSON.parse(body)
  17. if (data && data.errcode) {
  18. // 请求失败
  19. console.log(`请求微信服务失败:errcode:${data.errcode},errmsg:${data.errmsg}`)
  20. this.handleFail(data.errmsg, data.errcode)
  21. return
  22. }
  23. // 请求成功
  24. return this.handleSuccess(data)
  25. },
  26. /**
  27. * 处理成功的信息
  28. *
  29. * @param {string} [data='']
  30. * @returns
  31. */
  32. handleSuccess(data = '') {
  33. return {
  34. code: 0,
  35. data,
  36. message: '成功'
  37. }
  38. },
  39. /**
  40. * 处理失败的信息
  41. *
  42. * @param {string} [message='']
  43. * @param {number} [code=10001]
  44. * 10009 微信服务器请求失败
  45. * 10001 处理错误
  46. * @returns
  47. */
  48. handleFail(message = '', code = 10001) {
  49. return {
  50. code,
  51. data: '',
  52. message
  53. }
  54. }
  55. }

处理微信接口的统一封装

  1. // routers/common/index.js
  2. /**
  3. * @author
  4. * @description 微信接口统一封装处理
  5. */
  6. let request = require('request')
  7. let config = require('./../pay/config')
  8. let util = require('../../utils/util')
  9. config = config.wx
  10. /**
  11. * 获取accessToken
  12. * https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
  13. */
  14. exports.getAccessToken = function (code) {
  15. let access_token_url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${config.appId}&secret=${config.appSecret}&code=${code}&grant_type=authorization_code`
  16. return new Promise((resolve, reject) => {
  17. request.get(access_token_url, function (err, response, body) {
  18. let result = util.handleResponse(err, response, body)
  19. // console.log(result)
  20. resolve(result)
  21. // 这里都使用resolve是因为封装了处理请求,最后都是返回成功了信息,只是code码不一样
  22. /* if (result.code != 0) {
  23. // 请求失败
  24. resolve(result)
  25. return
  26. }
  27. resolve(
  28. util.handleSuccess({
  29. openId: result.openid
  30. })
  31. ) */
  32. })
  33. })
  34. }

重构router

  1. // routers/pay/wx.js
  2. /**
  3. * 微信开发
  4. */
  5. let express = require('express')
  6. let router = express.Router()
  7. let cache = require('memory-cache')
  8. let { wx: config } = require('./config')
  9. let common = require('./../common/index.js')
  10. let util = require('../../utils/util')
  11. router.get('/test', function (req, res) {
  12. res.json({
  13. code: 0,
  14. data: 'test',
  15. message: ''
  16. })
  17. })
  18. /**
  19. * 用户授权重定向
  20. * @query url
  21. * @query scope
  22. * @return
  23. */
  24. router.get('/redirect', function (req, res, next) {
  25. let redirectUrl = req.query.url // 最终重定向的地址->跳转回前端的页面
  26. let scope = req.query.scope // 作用域
  27. // console.log(redirectUrl)
  28. let callback = `${redirectUrl}/api/wechat/getOpenId` // 授权回调地址,用来获取openId
  29. cache.put('redirectUrl', redirectUrl) // 通过cache 缓存重定向地址
  30. 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`
  31. // console.log(authorizeUrl)
  32. res.redirect(authorizeUrl) //服务端重定向
  33. })
  34. /**
  35. * 根据code获取用户的OpenId
  36. * @query code 用户授权之后获取到的code
  37. */
  38. router.get('/getOpenId', async function (req, res) {
  39. let code = req.query.code
  40. // console.log('请求code成功')
  41. // console.log('code:' + code)
  42. if (!code) {
  43. res.json(util.handleFail('当前未获取到授权的code码'))
  44. return
  45. }
  46. let result = await common.getAccessToken(code)
  47. if (result.code != 0) {
  48. console.log(result)
  49. // 请求失败
  50. res.json(result)
  51. return
  52. }
  53. let data = result.data
  54. // 请求成功,将openid存储到cookie
  55. let expire_time = 1000 * 60 * 1 // 过期时间
  56. // console.log(data.openid)
  57. res.cookie('openId', data.openid, { maxAge: expire_time })
  58. let redirectUrl = cache.get('redirectUrl') //获取缓存中的重定向地址
  59. res.redirect(redirectUrl)
  60. })
  61. module.exports = router

我们把请求成功失败的信息进行封装,减少重复代码工作,也更好维护,
将微信api的请求单独封装,这个router结构只处理接口请求信息,显得更简洁。

3. 获取用户信息

  • 确保 scope 为 scope_userinfo
  • 根据 access_token/ openid 拉取用户的信息

前端:

  1. // views/index.vue
  2. <template>
  3. <div class="index">
  4. <img class="header" src="../assets/image/header.png" />
  5. <div class="user_info">
  6. <div class="avator_img">
  7. <img :src="userInfo.headimgurl" alt="" />
  8. </div>
  9. <div class="nickname">{{ userInfo.nickname }}</div>
  10. </div>
  11. <div class="btn-group">
  12. <button class="btn">分享</button>
  13. <button class="btn btn-primary btn-pay">体验</button>
  14. <button class="btn">活动详情</button>
  15. </div>
  16. <!--<div class="share">
  17. <img src="./../assets/images/share_guide.png" alt="">
  18. <img src="./../assets/images/share.png" alt="">
  19. </div>-->
  20. </div>
  21. </template>
  22. <script>
  23. import { getUserInfo } from '@/api/auth'
  24. export default {
  25. name: 'index',
  26. data() {
  27. return {
  28. userInfo: {},
  29. showShare: false
  30. }
  31. },
  32. methods: {
  33. // 获取用户信息
  34. async getUser() {
  35. let [err, res] = await getUserInfo()
  36. if (err) {
  37. console.log(err)
  38. return
  39. }
  40. console.log(res)
  41. this.userInfo = res.data
  42. }
  43. },
  44. mounted() {
  45. if (this.$cookie.get('openId')) {
  46. this.getUser()
  47. }
  48. }
  49. }
  50. </script>
  51. <style lang="less">
  52. .index {
  53. background-color: #ffc93a;
  54. height: 100vh;
  55. }
  56. .btn-group {
  57. padding-top: 0.34rem;
  58. text-align: center;
  59. }
  60. .btn-group .btn-pay {
  61. margin-top: 0.5rem;
  62. margin-bottom: 0.5rem;
  63. }
  64. .share {
  65. position: fixed;
  66. top: 0;
  67. left: 0;
  68. right: 0;
  69. bottom: 0;
  70. background-color: rgba(0, 0, 0, 0.75);
  71. }
  72. .share img {
  73. width: 100%;
  74. }
  75. .user_info {
  76. position: absolute;
  77. top: 10px;
  78. right: 20px;
  79. // transform: translate(-50%, 0);
  80. // width: 100%;
  81. height: 80px;
  82. }
  83. .avator_img {
  84. width: 40px;
  85. height: 40px;
  86. img {
  87. width: 100%;
  88. height: 100%;
  89. }
  90. }
  91. .nickname {
  92. margin-top: 10px;
  93. text-align: center;
  94. font-size: 15px;
  95. }
  96. </style>

node端:

  1. // common/index.js
  2. /**
  3. * 拉取用户信息(需scope为 snsapi_userinfo)
  4. * https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html#3
  5. * @param access_token
  6. * @param openId
  7. */
  8. exports.getUserInfo = function (access_token, openId) {
  9. let userInfo_url = `https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openId}&lang=zh_CN`
  10. return new Promise((resolve, reject) => {
  11. request.get(userInfo_url, function (err, response, body) {
  12. let result = util.handleResponse(err, response, body)
  13. // console.log(result)
  14. resolve(result)
  15. })
  16. })
  17. }
  1. /**
  2. * 根据code获取用户的OpenId
  3. * @query code 用户授权之后获取到的code
  4. */
  5. router.get('/getOpenId', async function (req, res) {
  6. let code = req.query.code
  7. // console.log('请求code成功')
  8. // console.log('code:' + code)
  9. if (!code) {
  10. res.json(util.handleFail('当前未获取到授权的code码'))
  11. return
  12. }
  13. let result = await common.getAccessToken(code)
  14. if (result.code != 0) {
  15. console.log(result)
  16. // 请求失败
  17. res.json(result)
  18. return
  19. }
  20. let data = result.data
  21. let expire_time = 1000 * 60 * 60 * 2 // 过期时间 2个小时
  22. // 将openId,taccess_token存储到缓存里
  23. cache.put('access_token', data.access_token, expire_time)
  24. cache.put('openId', data.openid, expire_time)
  25. // console.log(data.openid)
  26. // 请求成功,将openid存储到cookie
  27. res.cookie('openId', data.openid, { maxAge: expire_time })
  28. let redirectUrl = cache.get('redirectUrl') //获取缓存中的重定向地址
  29. res.redirect(redirectUrl)
  30. })
  31. /**
  32. * 获取用户信息
  33. */
  34. router.get('/getUserInfo', async function (req, res) {
  35. let access_token = cache.get('access_token')
  36. let openId = cache.get('openId')
  37. let result = await common.getUserInfo(access_token, openId)
  38. res.json(result)
  39. })

4. 生成JS-SDK签名算法

https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62

  • 什么是签名算法?
  • 为什么需要生成 js-sdk 的签名算法?
  • 如何生成?需要哪些步骤和流程?

    1. 获取通用的access_token

    https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
    注意:这里的access_token和我们之前获取用户信息的access_token不一样

    1. // common/index.js
    2. /**
    3. * 获取普通access_token
    4. * https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
    5. * @param access_token
    6. * @param openId
    7. */
    8. exports.getToken = function () {
    9. let token_url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.appId}&secret=${config.appSecret}`
    10. return new Promise((resolve, reject) => {
    11. request.get(token_url, function (err, response, body) {
    12. let result = util.handleResponse(err, response, body)
    13. resolve(result)
    14. })
    15. })
    16. }

    2. 根据access_token获取jsapi_ticket

    1. // common/index.js
    2. /**
    3. * 根据普通access_token获取jsapi_ticket
    4. * https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62
    5. * @param token 普通access_token
    6. */
    7. exports.getToken = function (token) {
    8. let jsapi_ticket_url = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${token}&type=jsapi`
    9. return new Promise((resolve, reject) => {
    10. request.get(jsapi_ticket_url, function (err, response, body) {
    11. let result = util.handleResponse(err, response, body)
    12. resolve(result)
    13. })
    14. })
    15. }

    3. 生成签名(签名算法)

  • 签名算法包括4个字段:随机字符串、jsapi_ticket、时间戳、url(前台传过来的,当前网页的URL,不包含#及其后面部分)location.href.split('#')[0] ```javascript // utils/util

// 生成随机数 createNonceStr() { // 生成随机数转化成36进制,截取2-15位 return Math.random().toString(36).substr(2, 15); }, // 生成随机戳 creatTimeStamp() { return parseInt(new Date().getTime()/1000) + ‘’ },

  1. - 有了以上字段之后需要对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)
  2. - 然后使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1
  3. - 注意的是所有参数名均为小写字符
  4. - string1sha1加密,字段名和字段值都采用原始值,不进行URL 转义即signature=sha1(string1)
  5. - 示例代码:
  6. ```javascript
  7. noncestr=Wm3WZYTPz0wzccnW
  8. jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg
  9. timestamp=1414587457
  10. url=http://mp.weixin.qq.com?params=value

排序转化后:

  1. jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg
  2. &noncestr=Wm3WZYTPz0wzccnW
  3. &timestamp=1414587457
  4. &url=http://mp.weixin.qq.com?params=value
  1. // utils/util.js
  2. /**
  3. * Object 转换成json 并排序
  4. */
  5. raw(args) {
  6. // 对对象中的key值进行排序
  7. let keys = Object.keys(args).sort()
  8. let obj = {}
  9. // 遍历key值赋值给新的对象
  10. keys.forEach(key => {
  11. obj[key] = args[key]
  12. })
  13. // 将对象转换为&分割的参数 {a:1,b:2} => a=1&b=2
  14. var val = ''
  15. for (let k in obj) {
  16. val += `&${k}=${obj[k]}` // &a=1&b=2
  17. }
  18. // 从字符串1的位置截取,去掉最开始的&符号
  19. return val.substring(1)
  20. },
  1. /**
  2. * 获取jssdk配置
  3. */
  4. router.get('/jssdk', async function (req, res) {
  5. let url = req.query.url
  6. let result = await common.getToken()
  7. if (result.code != 0) {
  8. console.log(result)
  9. return
  10. }
  11. // 有效期7200秒,开发者必须在自己的服务全局缓存access_token
  12. let token = result.data.access_token
  13. cache.put('token', token)
  14. let ticketRes = await common.getTicket(token)
  15. if (ticketRes != 0) {
  16. console.log(ticketRes)
  17. return
  18. }
  19. let ticket = ticketRes.data.ticket
  20. // 签名算法需要的值
  21. let params = {
  22. noncestr: util.createNonceStr(),
  23. jsapi_ticket: ticket,
  24. timestamp: util.creatTimeStamp(),
  25. url
  26. }
  27. // 排序转成字符串
  28. let str = util.raw(params)
  29. })
  • 对生成的字符串进行sha1签名

安装插件create-hash加密算法插件

  1. yarn add create-hash --save-dev
  1. // 排序转成字符串
  2. let str = util.raw(params)
  3. // 签名,进行sha1加密 最好加一个hex参数
  4. let sign = createHash('sha1').update(str).digest('hex')

完整的获取jssdk配置接口:

  1. // routers/pay/wx.js
  2. let express = require('express')
  3. let createHash = require('create-hash')
  4. let router = express.Router()
  5. let cache = require('memory-cache')
  6. let { wx: config } = require('./config')
  7. let common = require('./../common/index.js')
  8. let util = require('../../utils/util')
  9. /**
  10. * 获取jssdk配置
  11. */
  12. router.get('/jssdk', async function (req, res) {
  13. let url = req.query.url
  14. // 获取普通access_token
  15. let result = await common.getToken()
  16. if (result.code != 0) {
  17. console.log(result)
  18. return
  19. }
  20. // 有效期7200秒,开发者必须在自己的服务全局缓存access_token
  21. let token = result.data.access_token
  22. cache.put('token', token)
  23. // 获取ticket临时票据
  24. let ticketRes = await common.getTicket(token)
  25. if (ticketRes.code != 0) {
  26. console.log(ticketRes)
  27. return
  28. }
  29. let ticket = ticketRes.data.ticket
  30. // 签名算法需要的值
  31. let params = {
  32. noncestr: util.createNonceStr(),
  33. jsapi_ticket: ticket,
  34. timestamp: util.creatTimeStamp(),
  35. url
  36. }
  37. // 排序转成字符串
  38. let str = util.raw(params)
  39. // 进行sha1加密,生成签名 最好加一个hex参数
  40. let sign = createHash('sha1').update(str).digest('hex')
  41. res.json(
  42. util.handleSuccess({
  43. appId: config.appId, // 必填,公众号的唯一标识
  44. timestamp: params.timestamp, // 必填,生成签名的时间戳
  45. nonceStr: params.noncestr, // 必填,生成签名的随机串
  46. signature: sign, // 必填,签名
  47. jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData', 'chooseWXPay'] // 必填,需要使用的JS接口列表
  48. })
  49. )
  50. })

5. 前端vue获取JS-SDK配置

这里需要配置js接口安全域名,也就是告诉微信我们当前域名下可以使用jssdk的权限
image.png
否则会报以下错误:

  1. errMsg: "config:fail,Error: 系统错误,错误码:40048,invalid url domain [20200627 13:18:37][]"
  1. // 获取微信配置信息
  2. async getWechatConfig() {
  3. let [err, res] = await wechatConfig(location.href.split('#')[0])
  4. if (err) {
  5. console.log('configErr:', err)
  6. // return this.$message.error(err.message || '获取微信配置失败')
  7. }
  8. // console.log(res)
  9. let data = res.data
  10. wx.config({
  11. debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
  12. appId: data.appId, // 必填,公众号的唯一标识
  13. timestamp: data.timestamp, // 必填,生成签名的时间戳
  14. nonceStr: data.nonceStr, // 必填,生成签名的随机串
  15. signature: data.signature, // 必填,签名
  16. jsApiList: data.jsApiList // 必填,需要使用的JS接口列表
  17. })
  18. wx.ready(() => {
  19. initShareInfo(wx)
  20. })
  21. // this.$message.success(res.message || '获取成功')
  22. }

配置成功以后,因为开启了debug模式,会返回成功的提醒,并返回可以使用jssdk的api名字(目前是在后台设置,也可以前台设置)
image.png
调取jssdk接口成功也会返回相应信息
image.png

1. 我们可以通过微信 JS 接口签名校验工具进行签名校验

  1. // node端
  2. let params = {
  3. noncestr: util.createNonceStr(),
  4. jsapi_ticket: ticket,
  5. timestamp: util.creatTimeStamp(),
  6. url
  7. }
  8. console.log(params)
  9. //
  10. {
  11. noncestr: 'tvac6n2a2tm',
  12. jsapi_ticket: 'kgt8ON7yVITDhtdwci0qeTgB9ayI_2GiO6HU65PuFVz9upj8sUZQCjJwQS-0j_w7Qml4M-r1J46OfFg2AENaQQ',
  13. timestamp: '1593246702',
  14. url: 'http://api.example.com/'
  15. }

将上面输出的信息放到签名网址生成签名
image.png
比对我们自己生成的签名是否一致
image.png

2. 自定义vue插件,全局配置jssdk

我们获取jssdk配置成功之后,如果想要在组件中调取微信api的话要在组件中引入jssdk,很麻烦,而且会造成资源重复加载,
一种方式:可以在main中引入,挂载到vue原型上,就可以全局使用了
第二种方式:自定义vue插件进行引用
官方网址:https://cn.vuejs.org/v2/guide/plugins.html#%E5%BC%80%E5%8F%91%E6%8F%92%E4%BB%B6

  1. // utils/WechatPlugin.js
  2. import wx from 'weixin-js-sdk'
  3. const plugin = {
  4. install(Vue) {
  5. Vue.prototype.$wx = wx
  6. Vue.wx = wx
  7. },
  8. $wx: wx
  9. }
  10. export default plugin
  11. export const install = plugin.install
  1. // main.js
  2. import WechatPlugin from '@/utils/WechatPlugin'
  3. // 全局注册微信jsdk
  4. Vue.use(WechatPlugin)

这样我们在vue的实例上就能拿到$wx代替的jssdk,api了
调用:

  1. this.$wx.config({...})

3. 添加vconsole进行真机调试

真机调试的方式有很多,我们可以通过旧版(1.7以前的版本)微信开发者工具进行网页真机调试,
还可以通过vconsole进行真机调试
参考地址:https://blog.csdn.net/weixin_43232488/article/details/83014086
安装vconsole

  1. yarn add --save--dev vconsole
  1. // utils/vconsole.js
  2. import Vconsole from 'vconsole'
  3. const vConsole = new Vconsole()
  4. export default vConsole
  1. //main.js
  2. import '@/utils/vconsole.js'

引入之后,在开发网页时会多一个按钮
image.png
点击之后就可以看到控制台的输出日志了
image.png

6. 接入腾讯地图

尝试了一下接入腾讯地图显示位置信息(目前显示位置不正确)

6.1. 获取经纬度

我们通过jssdk获取经纬度:

  1. wx.getLocation({
  2. type: 'gcj02', // 默认为'wgs84'的gps坐标,如果要返回直接给openLocation用的火星坐标,可传入'gcj02'
  3. success: res => {
  4. console.log(res)
  5. var latitude = res.latitude // 纬度,浮点数,范围为90 ~ -90
  6. var longitude = res.longitude // 经度,浮点数,范围为180 ~ -180。
  7. var speed = res.speed // 速度,以米/每秒计
  8. var accuracy = res.accuracy // 位置精度
  9. console.log(`jssdk经纬度:纬度-${latitude},经度-${longitude}`)
  10. }
  11. })

jssdk通过经纬度显示地图

  1. wx.openLocation({
  2. latitude: latitude, // 纬度,浮点数,范围为90 ~ -90
  3. longitude: longitude, // 经度,浮点数,范围为180 ~ -180。
  4. name: '', // 位置名
  5. address: '', // 地址详情说明
  6. scale: 1, // 地图缩放级别,整形值,范围从1~28。默认为最大
  7. infoUrl: '' // 在查看位置界面底部显示的超链接,可点击跳转
  8. })

我们要接入腾讯地图api,就不使用jssdk的地图了

6.2. 坐标系

在开始之前,我们需要先了解一下地图坐标系
参考地址:https://www.jianshu.com/p/c39a2c72dc65?from=singlemessage
国内各地图API坐标系统比较

API 坐标系
百度地图API 百度坐标
腾讯搜搜地图API 火星坐标
搜狐搜狗地图API 搜狗坐标
阿里云地图API 火星坐标
图吧MapBar地图API 图吧坐标
高德MapABC地图API 火星坐标
灵图51ditu地图API 火星坐标

由于每种地图api坐标系都不一样,我们需要先进行转化,

6.3 转化经纬度供为腾讯地图坐标

腾讯地图也提供了相应的转化方法:
https://lbs.qq.com/service/webService/webServiceGuide/webServiceTranslate
有人可能也看到jssdk通过改变type类型为gcj02就是火星坐标,但是这里还是有一定的差距,我们往下看
后端node实现:后面用到的腾讯api也都封装在了这里

  1. // common/qqMapApi.js
  2. /**
  3. * 腾讯公共API统一封装
  4. */
  5. const request = require('request')
  6. const { qqMap: config } = require('../../config')
  7. const util = require('../../utils/util')
  8. /**
  9. * 实现从其它地图供应商坐标系或标准GPS坐标系,批量转换到腾讯地图坐标系。
  10. * https://lbs.qq.com/service/webService/webServiceGuide/webServiceTranslate
  11. * @param location 经纬度字符串 纬度,经度
  12. * @param type 输入的locations的坐标类型
  13. */
  14. exports.getLocationTranslate = function (location, type) {
  15. let translate_url = `https://apis.map.qq.com/ws/coord/v1/translate?locations=${location}&key=${config.key}&type=${type}`
  16. return new Promise((resolve, reject) => {
  17. request.get(translate_url, function (err, response, body) {
  18. let result = util.handleResponse(err, response, body)
  19. resolve(result)
  20. })
  21. })
  22. }
  23. /**
  24. * 根据经纬度获取地理位置信息
  25. * https://lbs.qq.com/service/webService/webServiceGuide/webServiceGcoder
  26. * @param location 经纬度字符串 纬度,经度
  27. */
  28. exports.getLocation = function (location) {
  29. let location_url = `https://apis.map.qq.com/ws/geocoder/v1/?location=${location}&key=${config.key}`
  30. return new Promise((resolve, reject) => {
  31. request.get(location_url, function (err, response, body) {
  32. let result = util.handleResponse(err, response, body)
  33. resolve(result)
  34. })
  35. })
  36. }
  37. /**
  38. * 根据描述位置获取位置信息
  39. * https://lbs.qq.com/service/webService/webServiceGuide/webServiceGcoder
  40. * @param address 位置信息
  41. */
  42. exports.getAddress = function (address) {
  43. let address_url = `https://apis.map.qq.com/ws/geocoder/v1/?address=${address}&key=${config.key}`
  44. return new Promise((resolve, reject) => {
  45. request.get(address_url, function (err, response, body) {
  46. let result = util.handleResponse(err, response, body)
  47. resolve(result)
  48. })
  49. })
  50. }
  1. /**
  2. * 腾讯地图开发
  3. */
  4. let express = require('express')
  5. const { getLocation, getLocationTranslate, getAddress } = require('../common/qqMapApi')
  6. let router = express.Router()
  7. router.get('/getLocation', async function (req, res) {
  8. // console.log(req.query)
  9. //中文需要转译
  10. // let addressRes = await getAddress('%E6%B2%B3%E5%8C%97%E7%9C%81%E4%BF%9D%E5%AE%9A%E5%B8%82%E7%AB%9E%E7%A7%80%E5%8C%BA%E4%B8%9C%E9%A3%8E%E4%B8%AD%E8%B7%AF9%E5%8F%B7')
  11. // console.log('地理位置信息:', addressRes.data.result.location)
  12. let { latitude, longitude } = req.query
  13. let location = `${latitude},${longitude}`
  14. // 将jssdk的经纬度转化为腾讯地图经纬度
  15. let translateRes = await getLocationTranslate(location, 1)
  16. console.log(translateRes.data.locations)
  17. let { lng, lat } = translateRes.data.locations[0]
  18. location = `${lat},${lng}`
  19. // 通过转化后的经纬度获取位置信息
  20. let result = await getLocation(location)
  21. // console.log(result)
  22. res.json(result)
  23. })
  24. module.exports = router

我们通过前台传过来的经纬度,先转化为腾讯坐标,然后获取此坐标的位置信息

前台实现:
通过jssdk经纬度调取后台接口获取转换后的经纬度

  1. // 获取 当前位置
  2. async getCuttentLoaction(latitude, longitude) {
  3. let [err, res] = await getLocation({ latitude, longitude })
  4. if (err) {
  5. console.log(err)
  6. return
  7. }
  8. // console.log(res)
  9. let location = res.data.result.location
  10. let address = res.data.result.address
  11. console.log(`转换后的经纬度:纬度-${location.lat},经度-${location.lng}`)
  12. console.log(address)
  13. // this.init(location.lat, location.lng)
  14. // this.init(38.87317, 115.48525)
  15. },

看下实际效果
image.png
转换后经纬度确实不一样了,如何确定转换后的经纬度是对的呢?

我们可以直接使用腾讯地图api获取当前经纬度
参考地址:https://www.jianshu.com/p/87429518c596
需要在index中引入对应的js

  1. // publie/index.html
  2. <script src="https://3gimg.qq.com/lightmap/components/geolocation/geolocation.min.js"></script>
  1. qqMapGeolocation() {
  2. var geolocation = new qq.maps.Geolocation('3NOBZ-YNLK6-AZHS5-EWI5D-OXS26-OWF4A', '达达-移动端')
  3. geolocation.getIpLocation(showPosition, showErr)
  4. // geolocation.getLocation(showPosition, showErr) //或者用getLocation精确度比较高
  5. function showPosition(position) {
  6. console.log('腾讯地图获取当前位置')
  7. console.log(position)
  8. }
  9. function showErr() {
  10. console.log('定位失败')
  11. this.qqMapGeolocation() //定位失败再请求定位,测试使用
  12. }
  13. },

image.png
转化后的经纬度和我们用腾讯地图获取到的当前位置的经纬度是一样的,转化成功。
也就是说,我们现在有两种方式来获取经纬度:

  1. 通过jssdk获取经纬度之后转化为腾讯地图坐标
  2. 直接通过腾讯地图api获取当前位置信息

但是这里有一个问题,我通过我的物理地址获取到的经纬度,和上面两种方式获取到的当前位置经纬度又不一样
后台通过调取腾讯地图接口获取到的信息
根据描述位置获取位置信息
https://lbs.qq.com/service/webService/webServiceGuide/webServiceGcoder

  1. //中文需要转译
  2. let addressRes = await getAddress('%E6%B2%B3%E5%8C%97%E7%9C%81%E4%BF%9D%E5%AE%9A%E5%B8%82%E7%AB%9E%E7%A7%80%E5%8C%BA%E4%B8%9C%E9%A3%8E%E4%B8%AD%E8%B7%AF9%E5%8F%B7')
  3. console.log('物理地址获取的位置信息:', addressRes.data.result.location)

image.png
经纬度查询:https://jingweidu.51240.com/
image.png

上面获取的当前位置信息通过地图显示的时候,实际上是不正确的,后面显示地图就可以看出来,目前不明白为什么。

6.4 显示地图

首先我们需要引入qqmap,来调取腾讯地图api
参考地址:https://www.cnblogs.com/Glant/p/12092866.html
这里还不完善,暂时先不写
vue接入腾讯、高德地图,https://github.com/ynzy/vue_map_demo

三、MongoDB使用

1. 安装MongoDB:

参考地址:
win: https://www.imooc.com/article/18438
mac: https://www.imooc.com/article/22733

1.1 windows安装:

  • 安装包: 百度链接:https://pan.baidu.com/s/1M7HhtTRW8fE1Oknb0o4TgA 提取码:kz8x
  • win版MongoDB安装教程
  • MongoDB配置环境变量里面写的不清楚,我在这里重写下,

    1. MONGO_HOME = C:\Program Files\MongoDB\Server\3.4\bin
    2. Path = %MONGO_HOME%
  • 推荐使用可视化工具管理数据库,清晰明了,我现找了一个,全是英文看不懂,但也能凑活用了,如果有更好的希望推荐下。

  • 教程网址Robo 3T,mongoDB可视化工具

    1.2 mac安装

    2. Mongo连接

    mongo操作方式,这里使用原生api

  • mongo原生API

  • mongoose框架

安装:

  1. yarn add mongodb --save-dev

创建一个imooc_pay数据库,创建users表,添加一条数据
image.png

  1. // routers/common/db.js
  2. /**
  3. * Mongodb公共文件,统一操作数据库
  4. */
  5. const MongoClient = require('mongodb').MongoClient;
  6. const util = require('./../../utils/util');
  7. const url = "mongodb://127.0.0.1:27017/imooc_pay";
  8. /**
  9. * 连接数据库
  10. * @params callback
  11. dbase 操作数据库的表
  12. db 关闭打开数据库
  13. */
  14. function connect(callback) {
  15. MongoClient.connect(url, function (err, db) {
  16. if (err) throw err;
  17. let dbase = db.db('imooc_pay');
  18. callback(dbase,db)
  19. })
  20. }
  21. /**
  22. * 查询数据
  23. * @param data 查询的条件
  24. * @param table 要查询的表
  25. */
  26. exports.query = function (data,table) {
  27. return new Promise((resolve, reject) => {
  28. connect(function (dbase, db) {
  29. // 连接数据库,根据data条件,查询table中符合条件的数据,返回一个数组,查询错误抛出错误
  30. dbase.collection(table).find(data).toArray(function (err, res) {
  31. if (err) throw err
  32. // 关闭数据库
  33. db.close()
  34. resolve(util.handleSuccess(res || []))
  35. })
  36. })
  37. })
  38. }
  39. /**
  40. * 插入数据
  41. * @param data 插入的数据
  42. * @param table 要操作的表
  43. */
  44. exports.insert = function (data,table) {
  45. return new Promise((resolve, reject) => {
  46. connect(function (dbase, db) {
  47. // 连接数据库,将data数据插入table中,返回一个数组,查询错误抛出错误
  48. dbase.collection(table).insertOne(data).toArray(function (err, res) {
  49. if (err) throw err
  50. // 关闭数据库
  51. db.close()
  52. resolve(util.handleSuccess(res || []))
  53. })
  54. })
  55. })
  56. }
  1. // routers/index.js
  2. // 模拟数据库查询操作
  3. router.get('/query', async function (req, res, next) {
  4. let result = await dao.query({ id: 1 }, 'users')
  5. console.log(result);
  6. res.json(result)
  7. })

3. 用户授权保存用户信息

在我们的’/getOpenId’接口添加保存用户信息操作

  1. /**
  2. * 根据code获取用户的OpenId
  3. * @query code 用户授权之后获取到的code
  4. */
  5. router.get('/getOpenId', async function (req, res) {
  6. let code = req.query.code
  7. // console.log('请求code成功')
  8. // console.log('code:' + code)
  9. if (!code) {
  10. res.json(util.handleFail('当前未获取到授权的code码'))
  11. return
  12. }
  13. let result = await common.getAccessToken(code)
  14. if (result.code != 0) {
  15. console.log(result)
  16. // 请求失败
  17. res.json('授权失败accessToken:', result)
  18. return
  19. }
  20. let data = result.data
  21. let openId = data.openid
  22. let expire_time = 1000 * 60 * 60 * 2 // 过期时间 2个小时
  23. // 将openId,taccess_token存储到缓存里
  24. cache.put('access_token', data.access_token, expire_time)
  25. cache.put('openId', openId, expire_time)
  26. // console.log(openId)
  27. // 请求成功,将openid存储到cookie
  28. res.cookie('openId', openId, { maxAge: expire_time })
  29. // 根据openId判断用户是否有注册
  30. let userRes = await dao.query({ 'openid': openId }, 'users')
  31. console.log(userRes);
  32. // 查询失败
  33. if (userRes.code !== 0) {
  34. res.json(userRes)
  35. return
  36. }
  37. // 没有此用户
  38. if (!userRes.data.length) {
  39. console.log('没有此用户');
  40. let userData = await common.getUserInfo(data.access_token,openId)
  41. let insertData = await dao.insert(userData.data, 'users')
  42. if (insertData.code != 0) {
  43. // 操作失败
  44. console.log(insertData);
  45. return
  46. }
  47. }
  48. // console.log('有此用户');
  49. // 有此用户
  50. let redirectUrl = cache.get('redirectUrl') //获取缓存中的重定向地址
  51. res.redirect(redirectUrl)
  52. })

四、小程序

1. 介绍

小程序分享优缺点:

  1. 体验友好
  2. 利于传播
  3. 只能分享好友
  4. 不能分享第三方应用 | 网页开发 | 小程序开发 | | —- | —- | | 运行浏览器中 | 运行JSCore | | 通过nginx等服务器访问 | 依托微信App | | 拥有DOM/BOM API | 没有DOM/BOM API | | 使用前端jquery,Vue等框架 | 无法使用前端框架库 |

2. 小程序注册和框架介绍

  • 官网通过邮箱进行注册
    • 每个邮箱只能注册一个小程序
    • 每个人只能注册6个小程序管理员
  • 通过公众号进行快速注册
    • 服务号
    • 订阅号
  • 小程序开发
    • 小程序入门
    • 小程序框架构成和原理
    • 小程序组件使用
    • 小程序API概念
    • 小程序服务端理解

项目结构:

  1. >wx_pay_mini -- UI主目录
  2. ├── assets -- 静态资源
  3. ├── images -- 图片
  4. └── wxss -- css
  5. ├── env -- 环境配置
  6. └── index.js -- 环境配置
  7. ├── http -- ajax请求
  8. ├── api.js -- 前后台交互接口
  9. ├──
  10. └── request.js -- 微信请求方法封装
  11. ├── page -- 页面
  12. ├── utils -- 工具库
  13. ├── router.js -- 页面路由管理
  14. ├── store.js -- 数据存储管理
  15. └── util.js -- 工具库
  16. ├── app.js -- 项目入口文件
  17. ├── app.js -- 项目总配置
  18. ├── app.wxss -- 项目公共css
  19. ├── project.config.json --
  20. └── sitemap.json --

3. 小程序公共机制

1. app.js - 入口配置文件

  1. /**
  2. * 小程序的入口
  3. */
  4. const Api = require('./http/api')
  5. const request = require('./http/request.js')
  6. let config = require('./env/index.js')
  7. const router = require('./utils/router.js')
  8. let env = 'Prod'
  9. App.version = '1.0.0' // 开发版本 后期做埋点统计,后台打印日志看目前处于哪个版本
  10. App.config = config[env] // 根据环境变量获取对应的配置信息
  11. App.config.env = env
  12. App.config.mockApi = config.mockApi
  13. App({
  14. config: config[env],
  15. Api, //全局注册api,供全局使用
  16. router,
  17. // 全局注册请求方法,
  18. get: request.fetch,
  19. post: (url, data, option) => {
  20. option.method = 'post'
  21. return request.fetch(url, data, option)
  22. },
  23. globalData: {
  24. userInfo: null
  25. },
  26. onLaunch: function () {
  27. }
  28. })

2. store.js - 通用的存储文件

  1. /**
  2. * Storage通用存储文件定义
  3. */
  4. const STORAGE_KEY = 'imooc-pay';
  5. module.exports = {
  6. /**
  7. * 存储数据
  8. *
  9. * @param {*} key
  10. * @param {*} value
  11. * @param {*} module_name 模块名称
  12. * {userInfo : { userId: 123, userName: 'jack' }}
  13. */
  14. setItem(key, value,module_name) {
  15. if (module_name) {
  16. let module_name_info = this.getItem(module_name);
  17. module_name_info[key] = value
  18. wx.setStorageSync(module_name, module_name_info);
  19. } else {
  20. wx.setStorageSync(key, value);
  21. }
  22. },
  23. /**
  24. * 获取数据
  25. *
  26. * @param {*} key
  27. * @param {*} module_name 模块名称
  28. */
  29. getItem(key,module_name) {
  30. if (module_name) {
  31. // 获取模块对象
  32. let val = this.getItem(module_name)
  33. // 如果有对应的key,取值
  34. if (val) return val[key]
  35. return ''
  36. } else {
  37. return wx.getStorageSync(key);
  38. }
  39. },
  40. /**
  41. * 清除指定数据 或 清除所有数据
  42. *
  43. * @param {*} key 删除对应key值数据
  44. */
  45. clear(key) {
  46. name?wx.removeStorageSync(key):wx.clearStorageSync();
  47. }
  48. }

3. router.js - 通用的路由文件

  1. /**
  2. * 通用的路由跳转文件
  3. */
  4. const routerPath = {
  5. 'index': "/pages/index/index",
  6. 'pay': "/pages/pay/index",
  7. 'activity': "/pages/activity/index"
  8. }
  9. module.exports = {
  10. /**
  11. * 页面跳转
  12. * push('index')
  13. * push({
  14. * path: '/index',
  15. * query: {
  16. * userId:123
  17. * }
  18. * })
  19. * @param {*} path
  20. * @param {*} option
  21. * query 传递参数
  22. * openType 跳转类型
  23. * duration 持续时间
  24. * backNum openType='back'时使用,返回上级页面(多级)
  25. */
  26. push(path, option = {}) {
  27. // 通过 push('index') 这种方式跳转
  28. if (typeof path == 'string') {
  29. option.path = path
  30. } else {
  31. option = path
  32. }
  33. let url = routerPath[option.path]
  34. // 传递参数 跳转类型 持续时间
  35. let { query = {}, openType, duration,backNum } = option
  36. let params = this.parse(query)
  37. url += `?${params}`
  38. duration ? setTimeout(() => {
  39. this.to(openType,url,backNum)
  40. }, duration) : this.to(openType,url,backNum)
  41. },
  42. /**
  43. * 路由跳转
  44. *
  45. * @param {*} openType 跳转类型
  46. * redirect,reLaunch,switchTab,back,navigateTo
  47. * @param {*} url 路由地址
  48. * @param {Number} backNum openType='back'时使用,返回上级页面(多级)
  49. */
  50. to(openType, url,backNum) {
  51. let obj = { url }
  52. switch (openType) {
  53. case 'redirect':
  54. wx.redirectTo(obj);
  55. break;
  56. case 'reLaunch':
  57. wx.reLaunch(obj);
  58. break;
  59. case 'switchTab':
  60. wx.switchTab(obj);
  61. break;
  62. case 'back':
  63. wx.navigateBack({
  64. delta: backNum || 1
  65. });
  66. break;
  67. default:
  68. wx.navigateTo(obj);
  69. break;
  70. }
  71. },
  72. /**
  73. * 对象转换为 & 符连接
  74. * @param {Object} data
  75. * @returns
  76. */
  77. parse(data) {
  78. let arr = []
  79. // let data = {a:1,b:2,c:3} a=1&b=2&c=3
  80. for (const key in data) {
  81. arr.push(`${key}=${data[key]}`)
  82. }
  83. return arr.join('&')
  84. }
  85. }

4. 小程序自适应

https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxss.html#%E5%B0%BA%E5%AF%B8%E5%8D%95%E4%BD%8D

  • 尺寸单位

rpx(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。

设备 rpx换算px (屏幕宽度/750) px换算rpx (750/屏幕宽度)
iPhone5 1rpx = 0.42px 1px = 2.34rpx
iPhone6 1rpx = 0.5px 1px = 2rpx
iPhone6 Plus 1rpx = 0.552px 1px = 1.81rpx

5. env - 通用的环境设置文件

  1. module.exports = {
  2. mockApi: '',
  3. Dev: {
  4. baseApi: 'http://localhost:3000'
  5. },
  6. Test: {
  7. baseApi: 'http://test-node.51purse.com'
  8. },
  9. Slave: {
  10. baseApi: 'http://slave-node.51purse.com'
  11. },
  12. Prod: {
  13. baseApi: 'http://node.51purse.com'
  14. }
  15. }

6. request.js - 通用的请求文件

  1. let store = require('../utils/store')
  2. let system = store.getSystemInfo()
  3. // 客户端信息
  4. const clientInfo = {
  5. 'clientType': 'mp', // 客户端类型
  6. 'appnm': 'imoocpay', //项目名
  7. 'model': system.model, // 设备型号
  8. 'os': system.system, // 操作系统及版本
  9. 'screen': system.screenWidth + '*' + system.screenHeight, // 屏幕尺寸
  10. 'version': App.version, // 小程序版本
  11. 'channel': 'miniprogram' // 渠道
  12. }
  13. const errMsg = '服务异常,请稍后重试'
  14. module.exports = {
  15. fetch: (url, data = {}, { loading = true, toast = true, isMock = false, method = 'get' }) => {
  16. let env = isMock ? App.config.mockApi : App.config.baseApi;
  17. return new Promise((resolve, reject) => {
  18. loading && wx.showLoading({
  19. title: '加载中...',
  20. mask: true,
  21. });
  22. wx.request({
  23. url: env + url,
  24. data,
  25. header: {
  26. 'content-type': 'application/json',
  27. 'clientInfo': JSON.stringify(clientInfo)
  28. },
  29. method,
  30. dataType: 'json',
  31. responseType: 'text',
  32. success: (result) => {
  33. let res = result.data // {code:0,data:'',message:''}
  34. if (res.code == 0) {
  35. loading && wx.hideLoading();
  36. resolve(res.data)
  37. } else {
  38. // 如果有toast提示会直接结束loading
  39. toast ? wx.showToast({
  40. title: res.message,
  41. icon: 'none',
  42. mask: true
  43. }) : wx.hideLoading();
  44. // loading && wx.hideLoading();
  45. reject(res)
  46. }
  47. },
  48. fail: (e = { code: -1, msg: errMsg, errMsg }) => {
  49. let msg = e.errMsg
  50. if (msg == 'request:fail timeout') {
  51. msg = '服务请求超时,请稍后处理';
  52. }
  53. wx.showToast({
  54. title: msg,
  55. icon: 'none'
  56. })
  57. reject(e)
  58. },
  59. complete: () => {
  60. }
  61. });
  62. })
  63. }
  64. }

4. 小程序授权登录

https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
微信分享与支付专项课程(公众号、小程序、小程序云) - 图25

// pages/index/index.js onLoad: function () { // 判断用户是否登录 if (!this.data.userId) { this.getSession(); } }, // 获取登录的code getSession() { wx.login({ success: (res) => { if (res.code) { app.get(Api.getSession, { code: res.code }).then((res) => { console.log(res); store.setItem(‘openId’, res.openid); }).catch((res) => { console.log(‘error:’ + res.message) }) } } }) }, getUserInfo(e) { let userInfo = e.detail.userInfo; // 携带openId传给后端 userInfo.openid = store.getItem(‘openId’); app.get(Api.login, { userInfo }).then((res) => { store.setItem(‘userId’, res.userId); this.setData({ userId: res.userId }) }) },

  1. <a name="IAjgN"></a>
  2. ### 后端
  3. ```javascript
  4. /**
  5. * 小程序开发
  6. */
  7. let express = require('express');
  8. let router = express.Router();
  9. let request = require('request');
  10. let { mp: config } = require('../../config');
  11. let util = require('../../utils/util')
  12. let dao = require('../common/db')
  13. /**
  14. * 获取session接口
  15. * @param {*} code 小程序登陆code
  16. */
  17. router.get('/getSession', function (req, res) {
  18. let code = req.query.code
  19. // 获取code失败
  20. if (!code) {
  21. res.json(util.handleFail('code不能为空', 10001));
  22. return
  23. }
  24. // 获取code成功
  25. let sessionUrl = `https://api.weixin.qq.com/sns/jscode2session?appid=${config.appId}&secret=${config.appSecret}&js_code=${code}&grant_type=authorization_code`;
  26. request(sessionUrl, function (err, response, body) {
  27. let result = util.handleResponse(err, response, body);
  28. // console.log(result);
  29. // 返回openid给前端
  30. res.json(result);
  31. })
  32. })
  33. router.get('/login', async function (req, res) {
  34. let userInfo = JSON.parse(req.query.userInfo) // 字符串转对象
  35. // console.log(userInfo);
  36. // 获取用户信息失败
  37. if (!userInfo) {
  38. res.json(util.handleFail('用户信息不能为空', 10002))
  39. return
  40. }
  41. // 获取成功
  42. // 查询当前用户是否已经注册
  43. let userRes = await dao.query({ openid: userInfo.openid }, 'users_mp')
  44. console.log(userRes);
  45. // 查询数据库失败
  46. if (userRes.code != 0) {
  47. res.json(userRes)
  48. return
  49. }
  50. // 有此用户信息
  51. if (userRes.data.length > 0) {
  52. res.json(util.handleSuccess({
  53. userId: userRes.data[0]._id
  54. }))
  55. return
  56. }
  57. // 没有此用户信息,添加到数据库中
  58. let insertData = await dao.insert(userInfo, 'users_mp')
  59. // 添加失败
  60. if (insertData.code != 0) {
  61. res.json(insertData)
  62. return
  63. }
  64. // 添加成功
  65. res.json(util.handleSuccess({
  66. userId: insertData.data.insertedId
  67. }))
  68. })
  69. module.exports = router

五、小程序云

1. 介绍:

小程序云开发特点:

  • 节约开发和运维成本
  • 关注核心功能开发
  • 和普通的小程序开发一致
  • 拥有微信更多的能力

  • 什么是小程序云开发

    • 无需要搭建后台、无需购买域名和服务器,所有api通过云实现
  • 小程序云开发可以做什么
    • 独立开发小程序、小游戏,无需关注服务器,直接调用云能力
  • 小程序云开发都有哪些能力
    • 云函数:在云端运行的代码,微信私有协议天然鉴权,开发者只需编写自身业务逻辑代码
    • 数据库:一个既可以在小程序前端操作,也能在云函数中读写JSON数据库
    • 存储:在小程序前端直接上传/下载云端文件,在云开发控制台可视化管理
  • 小程序云和普通小程序对比 | 普通小程序 | 小程序云 | | —- | —- | | 需要开发后台 | 需要开发云函数 | | 需要服务器 | 无需关注 | | 需要证书 | 无需关注 | | 需要通过api鉴权 | 无需关注 | | 有测试号 | 无测试号 |

2. 小程序云创建

  • 通过邮箱注册小程序
  • 完善小程序资料并获取appId
  • 通过开发者工具创建项目

image.png
创建项目时我们要选择使用小程序云开发

  • 开通云服务

第一次创建的项目,需要先点击云开发,并开通云服务
https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/quickstart.html#_2-%E5%BC%80%E9%80%9A%E4%BA%91%E5%BC%80%E5%8F%91%E3%80%81%E5%88%9B%E5%BB%BA%E7%8E%AF%E5%A2%83
image.png

  • 完善静态页面
  • 创建云函数

创建一个云函数有两种方式:

  1. 本地创建云函数:
    • 右键云项目文件夹->新建一个node.js云函数->输入函数名字->自动创建云函数所需要的文件
    • 右键选择云函数文件夹->选择上传并部署->在云开发平台进行查看
  2. 云端创建云函数
    • 点击云开发->选择云函数->新建云函数->输入函数名称->此时创建的云函数是一个空的云函数,
    • 右键选择云项目文件夹->选择同步云函数列表
    • 注意:执行方法->执行方法目前不可更改,调用的时候调用云函数名称->index.js->main方法

image.png

  • 小程序调用云函数

wx.cloud.init初始化云函数:

  1. // app.js
  2. wx.cloud.init({
  3. // env 参数说明:
  4. // env 参数决定接下来小程序发起的云开发调用(wx.cloud.xxx)会默认请求到哪个云环境的资源
  5. // 此处请填入环境 ID, 环境 ID 可打开云控制台查看
  6. // 如不填则使用默认环境(第一个创建的环境)
  7. env: 'test-aewbf', //
  8. traceUser: true, // 是否在将用户访问记录到用户管理中,在控制台中可见
  9. })
  1. // pages/index/index
  2. // 小程序调用云函数
  3. wx.cloud.callFunction({
  4. name: 'getOpenId',
  5. data: {
  6. name:'jack'
  7. },
  8. success: res=>{
  9. console.log('云函数getOpenId调用成功');
  10. console.log(res);
  11. },
  12. fail: err =>{
  13. console.log('云函数调用失败');
  14. console.log(err);
  15. }
  16. })

3. 小程序云用户授权登录

小程序云:

  1. // login/index.js
  2. // 云函数入口文件
  3. const cloud = require('wx-server-sdk')
  4. cloud.init({
  5. // API 调用都保持和云函数当前所在环境一致
  6. env: cloud.DYNAMIC_CURRENT_ENV
  7. })
  8. // 云函数入口函数
  9. exports.main = async (event, context) => {
  10. // 获取上下文
  11. const wxContext = cloud.getWXContext()
  12. // 连接数据库
  13. const db = cloud.database({
  14. env: cloud.DYNAMIC_CURRENT_ENV
  15. })
  16. // 判断用户是否存在
  17. const result = await db.collection('users').where({
  18. openid: wxContext.OPENID
  19. }).limit(1).get()
  20. console.log('user:'+ JSON.stringify(result));
  21. if(result.data.length != 0){
  22. // 用户存在
  23. return {
  24. userId: result.data[0]._id
  25. }
  26. }
  27. // 用户不存在
  28. let params = {
  29. ...event.user,
  30. openid: wxContext.OPENID
  31. }
  32. // 添加用户到数据库
  33. const userData = await db.collection('users').add({
  34. data: params
  35. })
  36. console.log('添加用户成功:',userData);
  37. // 添加成功返回用户id,返回到前端
  38. return {
  39. userId: userData._id
  40. }
  41. }

小程序端:

  1. // pages/index/index.js
  2. getUserInfo(e){
  3. // 1. 获取用户信息
  4. let user = e.detail.userInfo
  5. // 2. 调取小程序云函数
  6. wx.cloud.callFunction({
  7. name: 'login',
  8. data: {
  9. user
  10. }
  11. }).then(res=>{
  12. console.log(res);
  13. let userId = res.result.userId
  14. store.setItem('userId',userId)
  15. this.setData({
  16. userId
  17. })
  18. }).catch(err=>{
  19. console.log(err);
  20. })
  21. }

六、支付介绍

1. 支付认证

  • 微信认证
    • 微信认证的特权:链接
    • 订阅号微信认证特权
      • 自定义菜单(可设置跳转外部链接,设置纯文本信息)
      • 可使用部分开发接口
      • 可以申请广告主功能
      • 可以申请卡券功能
      • 可以申请多客服功能
      • 公众号头像及详细资料会显示加“V”标识
    • 服务号微信认证特权
      • 全部高级开发接口
      • 可申请开通微信支付功能
      • 可申请开通微信小店
      • 可以申请广告主功能
      • 可以申请卡券功能
      • 可以申请多客服功能
      • 公众号头像及详细资料会显示加“V”标识
  • 微信支付认证
    • 注册商户号/关联商户号
    • H5-配置支付授权目录
    • 非H5-授权AppID
    • 设置API密钥
  • https证书

    2. 微信支付流程

    2.1 小程序端支付流程

    前端支付接入方式对比:
对比栏目 JSAPI JSSDK 小程序
统一下单 都需要先获取到OpenId,调用相同的API
调起数据签名 五个字段参与签名(区分大小写):appId,nonceStr,package,signType,timeStamp
调起支付页面协议 HTTP或HTTPS HTTP或HTTPS HTTPS
支付目录
授权域名
回调函数 seccess回调 success,fail,complete回调函数
  • wx.requestPayment

https://developers.weixin.qq.com/miniprogram/dev/api/open-api/payment/wx.requestPayment.html

2.2 后端支付流程

https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1

  • 拼接常规参数
  • 生成签名
  • 拼接xml数据
  • 调用统一下单接口
  • 获取预支付ID:prepay_id
  • 生成SDK
  • 定义回调接口,接收微信支付消息
    1. // utils/util.js
    2. const createHash = require('create-hash')
    3. module.exports = {
    4. /**
    5. * 生成随机字符串
    6. */
    7. createNonceStr() {
    8. // 生成随机数转化成36进制,截取2-15位
    9. return Math.random().toString(36).substr(2, 15)
    10. },
    11. /**
    12. * 生成随机时间戳
    13. */
    14. creatTimeStamp() {
    15. return parseInt(new Date().getTime() / 1000) + ''
    16. },
    17. /**
    18. * 生成签名
    19. */
    20. getSign(params, key) {
    21. let str = this.raw(params) + '&key=' + key
    22. // 进行 md5 加密,生成签名 最好加一个hex参数
    23. let sign = createHash('md5').update(str).digest('hex')
    24. return sign.toUpperCase()
    25. },
    26. /**
    27. * 生成系统的交易订单号
    28. * @param type wx:微信 mp:小程序
    29. */
    30. getTradeId(type = 'wx') {
    31. let date = new Date().getTime().toString()
    32. let text = '';
    33. let possible = '0123456789'
    34. // 生成5位随机数
    35. for (let i = 0; i < 5; i++) {
    36. text += possible.charAt(Math.floor(Math.random() * possible.length))
    37. }
    38. return type == ('wx' ? 'Wx' : 'Mp') + date + text
    39. },
    40. /**
    41. * Object 转换成json 并排序
    42. */
    43. raw(args) {
    44. // 对对象中的key值进行排序
    45. let keys = Object.keys(args).sort()
    46. let obj = {}
    47. // 遍历key值赋值给新的对象
    48. keys.forEach(key => {
    49. obj[key] = args[key]
    50. })
    51. // 将对象转换为&分割的参数 {a:1,b:2} => a=1&b=2
    52. var val = ''
    53. for (let k in obj) {
    54. val += `&${k}=${obj[k]}` // &a=1&b=2
    55. }
    56. // 从字符串1的位置截取,去掉最开始的&符号
    57. return val.substring(1)
    58. },
    59. /**
    60. * 处理微信请求返回信息统一处理
    61. * @param {*} err
    62. * @param {*} response
    63. * @param {*} body
    64. */
    65. handleResponse(err, response, body) {
    66. if (err || response.statusCode != '200') {
    67. // 微信服务器请求失败
    68. this.handleFail(err, 10009)
    69. return
    70. }
    71. // console.log('body:', body)
    72. let data = JSON.parse(body)
    73. if (data && data.errcode) {
    74. // 请求失败
    75. console.log(`请求微信服务失败:errcode:${data.errcode},errmsg:${data.errmsg}`)
    76. this.handleFail(data.errmsg, data.errcode)
    77. return
    78. }
    79. // 请求成功
    80. return this.handleSuccess(data)
    81. },
    82. /**
    83. * 处理成功的信息
    84. *
    85. * @param {string} [data='']
    86. * @returns
    87. */
    88. handleSuccess(data = '') {
    89. return {
    90. code: 0,
    91. data,
    92. message: '成功'
    93. }
    94. },
    95. /**
    96. * 处理失败的信息
    97. *
    98. * @param {string} [message='']
    99. * @param {number} [code=10001]
    100. * 10009 微信服务器请求失败
    101. * 10001 处理错误
    102. * @returns
    103. */
    104. handleFail(message = '', code = 10001) {
    105. return {
    106. code,
    107. data: '',
    108. message
    109. }
    110. }
    111. }
    ```javascript // routers/common/wxpay.js /**
    • 微信支付通用封装 */ const request = require(‘request’) const createHash = require(‘create-hash’) const xml = require(‘xml2js’) const util = require(‘../../utils/util’) const { mch: config } = require(‘../../config’)

module.exports = { /**

  • 微信统一下单
  • https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1
  • @param {*} appid 应用id
  • @param {*} attach 附加数据
  • @param {*} body 主题内容
  • @param {*} mch_id 商户号
  • @param {*} out_trade_no 商户订单号
  • @param {*} nonce_str 随机数
  • @param {*} openid openid
  • @param {*} total_fee 支付金额(分)
  • @param {*} notify_url 回调地址
  • @param {} spbill_create_ip: ip 地址ip / order: function ({ appid, attach, body, openid, total_fee, notify_url, ip }) { return new Promise((resolve, reject) => { let _that = this; let nonce_str = util.createNonceStr() let out_trade_no = util.getTradeId(‘mp’) let mch_id = config.mch_id // 支付前需要先获取支付签名 let sign = this.getPrePaySign({ appid, attach, body, mch_id, nonce_str, out_trade_no, openid, total_fee, notify_url, ip }) // 通过参数和签名组装xml数据,用以调用统一下单接口 let sendData = this.wxSendData({ appid, attach, body, mch_id, nonce_str, out_trade_no, openid, total_fee, notify_url, ip, sign }) console.log(sendData); // 请求微信服务器统一下单接口 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) {
    1. // 请求错误
    2. console.log(err);
    3. resolve(util.handleFail(err))
    4. return
    } // 请求成功 // 将xml文件转化为js xml.parseString(body.toString(‘utf-8’), function (error, res) {
    1. if (error) {
    2. console.log('解析xml失败');
    3. console.log(error);
    4. return
    5. }
    6. let data = res.xml
    7. if (data.return_code[0] == 'SUCCESS' && data.result_code[0] == 'SUCCESS') {
    8. // 获取预支付的ID
    9. let prepay_id = data.prepay_id || []
    10. let payResult = _that.getPayParams(appid, prepay_id[0])
    11. resolve(payResult)
    12. }
    }) }) }) }, /**
  • 生成预支付签名
  • https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3
  • @param {*} appid 应用id
  • @param {*} attach 附加数据
  • @param {*} body 主题内容
  • @param {*} mch_id 商户号
  • @param {*} out_trade_no 商户订单号
  • @param {*} nonce_str 随机数
  • @param {*} openid openid
  • @param {*} total_fee 支付金额(分)
  • @param {*} notify_url 回调地址
  • @param {*} spbill_create_ip: ip 地址ip
  • @param {} trade_type 交易类型 ‘JSAPI’ / getPrePaySign: function ({ appid, attach, body, mch_id, nonce_str, out_trade_no, openid, total_fee, notify_url, ip }) { let params = { appid, attach, body, mch_id, nonce_str, out_trade_no, notify_url, openid, spbill_create_ip: ip, total_fee, trade_type: ‘JSAPI’ } // 排序拼接字符串 // stringSignTemp=stringA+”&key=192006250b4c09247ec02edce69f6a2d” //注:key为商户平台设置的密钥key return util.getSign(params, config.key) / let str = util.raw(params) + ‘&key=’ + config.key // 进行 md5 加密,生成签名 最好加一个hex参数 let sign = createHash(‘md5’).update(str).digest(‘hex’) return sign.toUpperCase() / }, /**
  • 签名成功之后,根据参数拼接组装XML格式的数据,调用下单接口 / wxSendData: function ({ appid, attach, body, mch_id, nonce_str, out_trade_no, openid, total_fee, notify_url, ip, sign }) { let data = <xml> <appid><![CDATA[${appid}]]></appid> <attach><![CDATA[${attach}]]></attach> <body><![CDATA[${body}]]></body> <mch_id><![CDATA[${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 }, /*
  • 生成微信支付参数
  • https://developers.weixin.qq.com/miniprogram/dev/api/open-api/payment/wx.requestPayment.html
  • @param appId 应用id
  • @param prepay_id 预支付id */ getPayParams: function (appId, prepay_id) { let params = { appId, timeStamp: util.creatTimeStamp(), nonceStr: util.createNonceStr(), package: prepay_id=${prepay_id}, signType: ‘MD5’, } let paySign = util.getSign(params, config.key) params.paySign = paySign return params } } javascript // router/pay/mp.js

/**

  • 小程序开发 */

let express = require(‘express’); let router = express.Router(); let request = require(‘request’); let { mp: config } = require(‘../../config’); let util = require(‘../../utils/util’) let dao = require(‘../common/db’); const { order } = require(‘../common/wxpay’); /**

  • 获取session接口
  • @param {} code 小程序登陆code / router.get(‘/getSession’, function (req, res) { let code = req.query.code // 获取code失败 if (!code) { res.json(util.handleFail(‘code不能为空’, 10001)); return } // 获取code成功 let sessionUrl = https://api.weixin.qq.com/sns/jscode2session?appid=${config.appId}&secret=${config.appSecret}&js_code=${code}&grant_type=authorization_code; request(sessionUrl, function (err, response, body) { let result = util.handleResponse(err, response, body); // console.log(result); // 返回openid给前端 res.json(result); }) }) /**
  • 授权登录
  • @param userInfo 用户信息+openid */ router.get(‘/login’, async function (req, res) { let userInfo = JSON.parse(req.query.userInfo) // 字符串转对象 // console.log(userInfo); // 获取用户信息失败 if (!userInfo) { res.json(util.handleFail(‘用户信息不能为空’, 10002)) return } // 获取成功 // 查询当前用户是否已经注册 let userRes = await dao.query({ openid: userInfo.openid }, ‘users_mp’) console.log(userRes); // 查询数据库失败 if (userRes.code != 0) { res.json(userRes) return } // 有此用户信息 if (userRes.data.length > 0) { res.json(util.handleSuccess({ userId: userRes.data[0]._id })) return } // 没有此用户信息,添加到数据库中 let insertData = await dao.insert(userInfo, ‘users_mp’) // 添加失败 if (insertData.code != 0) { res.json(insertData) return } // 添加成功 res.json(util.handleSuccess({ userId: insertData.data.insertedId })) })

/**

  • 支付回调通知 */ router.get(‘/pay/callback’, function (req, res) { res.json(util.handleSuccess()) })

/**

  • 小程序支付
  • @param {} openId / router.get(‘/pay/payWallet’, function (req, res) { let appid = config.appId //小程序id let openid = req.query.openId; // 用户的openid let attach = ‘小程序支付附加数据’ // 附加数据 let body = ‘小程序支付’ // 主体内容 let total_fee = req.query.money // 支付金额(分) let ontify_url = ‘http://localhost:3000/api/map/pay/callback‘ // 微信回调接口 let ip = ‘123.57.2.144’ // 终端ip let orderRes = order({ appid, attach, body, openid, total_fee, ontify_url, ip }).then(result => { res.json(util.handleSuccess(result)) }).catch((result) => { res.json(util.handleFail(result)) }) }) module.exports = router ```

    3. 小程序云函数本地调试

    3.1 小程序云函数使用

  • 云函数打印日志、本地日志
  • 创建本地云函数
  • 同步本地云函数到远程
  • 安装云函数插件

和node安装环境一样,在终端打开当前文件夹,使用npm安装想要安装的node包,比如:
image.png

  • 本地调试云函数

选择云函数文件->右键选择开启云函数本地调试
右侧的红框勾选开启本地调试,请求方式选择手动,即可调试代码
image.png

3.2 小程序云支付流程实现

安装依赖包

  1. "xml2js": "^0.4.23",
  2. "create-hash": "^1.2.0",
  3. "request": "^2.88.2",
  1. // order/config.js
  2. module.exports = {
  3. mchId: '',
  4. key: '',
  5. appId: 'wx77124add68e6adcb',
  6. appSecret: '',
  7. body:"欢迎学习慕课首门小程序云开发支付专项课程。",
  8. notifyUrl:"http://m.51purse.com/api/wechat/pay/callback",
  9. attach:"微信支付课程体验",
  10. ip:'127.0.0.1'
  11. }
  1. // order/util.js
  2. /**
  3. * 工具函数
  4. */
  5. let createHash = require('create-hash');
  6. module.exports = {
  7. // 生成随机数
  8. createNonceStr() {
  9. // 此处将0-1的随机数转换为36进制
  10. return Math.random().toString(36).substr(2, 15)
  11. },
  12. // 生成时间戳
  13. createTimeStamp() {
  14. return parseInt(new Date().getTime() / 1000) + ''
  15. },
  16. // Object转换为JSON并排序
  17. raw(args) {
  18. // key按字母排序
  19. let keys = Object.keys(args).sort();
  20. let obj = {};
  21. // 获取排序后的对象
  22. keys.forEach(function (key) {
  23. obj[key] = args[key]
  24. })
  25. // 将对象转换为&分割的参数
  26. let val = ''
  27. for (let k in obj) {
  28. val += '&' + k + '=' + obj[k]
  29. }
  30. return val.substr(1);
  31. },
  32. // 生成交易Id
  33. getTradeId() {
  34. var date = new Date().getTime().toString()
  35. var text = ""
  36. var possible = "0123456789"
  37. for (var i = 0; i < 5; i++) {
  38. text += possible.charAt(Math.floor(Math.random() * possible.length))
  39. }
  40. return 'ImoocMpCloud' + date + text;
  41. },
  42. // 根据对象生成签名
  43. getSign(params, key) {
  44. var string = this.raw(params);
  45. string = string + '&key=' + key
  46. var sign = createHash('md5').update(string).digest('hex')
  47. return sign.toUpperCase();
  48. },
  49. // 对请求结果统一处理
  50. handleResponse(err, response, body) {
  51. if (!err && response.statusCode === 200) {
  52. let data = JSON.parse(body);
  53. return this.handleSuc(data);
  54. } else {
  55. return this.handleFail(err, 1009);
  56. }
  57. },
  58. // 请求成功封装
  59. handleSuc(data = '') {
  60. console.log(JSON.stringify(data))
  61. return {
  62. code: 0,
  63. data,
  64. message: ''
  65. }
  66. },
  67. // 请求失败封装
  68. handleFail(message = '', code = 1006) {
  69. console.log(message);
  70. return {
  71. code,
  72. data: '',
  73. message
  74. }
  75. }
  76. }
  1. // order/index.js
  2. // 云函数入口文件
  3. let cloud = require('wx-server-sdk')
  4. cloud.init()
  5. //微信小程序支付封装,暂支持md5加密,不支持sha1
  6. /**
  7. ***create order by jianchep 2016/11/22
  8. **/
  9. let config = require('./config.js')
  10. let request = require("request");
  11. let util = require('./util');
  12. let xml = require('xml2js');
  13. // 支付前先获取预支付签名sign
  14. let getPrePaySign = function (appId, attach, body, mchId, nonceStr, notifyUrl, openId, tradeId, ip, totalFee) {
  15. var params = {
  16. appid: appId,
  17. attach: attach,
  18. body: body,
  19. mch_id: mchId,
  20. nonce_str: nonceStr,
  21. notify_url: notifyUrl,
  22. openid: openId,
  23. out_trade_no: tradeId,
  24. spbill_create_ip: ip,
  25. total_fee: totalFee,
  26. trade_type: 'JSAPI'
  27. }
  28. return util.getSign(params, config.key);
  29. };
  30. // 签名成功后,将数据拼接成功xml格式一起发送给微信下单接口
  31. let wxSendData = function (appId, attach, body, mchId, nonceStr, notifyUrl, openId, tradeId, ip, totalFee, sign) {
  32. let sendData = '<xml>' +
  33. '<appid><![CDATA[' + appId + ']]></appid>' +
  34. '<attach><![CDATA[' + attach + ']]></attach>' +
  35. '<body><![CDATA[' + body + ']]></body>' +
  36. '<mch_id><![CDATA[' + mchId + ']]></mch_id>' +
  37. '<nonce_str><![CDATA[' + nonceStr + ']]></nonce_str>' +
  38. '<notify_url><![CDATA[' + notifyUrl + ']]></notify_url>' +
  39. '<openid><![CDATA[' + openId + ']]></openid>' +
  40. '<out_trade_no><![CDATA[' + tradeId + ']]></out_trade_no>' +
  41. '<spbill_create_ip><![CDATA[' + ip + ']]></spbill_create_ip>' +
  42. '<total_fee><![CDATA[' + totalFee + ']]></total_fee>' +
  43. '<trade_type><![CDATA[JSAPI]]></trade_type>' +
  44. '<sign><![CDATA[' + sign + ']]></sign>' +
  45. '</xml>'
  46. return sendData
  47. };
  48. // 用生成的预支付ID再次签名,拼接对应的前端需要的字段
  49. let getPayParams = function (appId, prepayId) {
  50. let nonceStr = util.createNonceStr();
  51. let timeStamp = util.createTimeStamp();
  52. let package = 'prepay_id=' + prepayId;
  53. let params = {
  54. appId: appId,
  55. nonceStr: nonceStr,
  56. package: package,
  57. signType: 'MD5',
  58. timeStamp: timeStamp
  59. }
  60. let paySign = util.getSign(params, config.key);
  61. // 前端需要的所有数据, 都从这里返回过去
  62. params.paySign = paySign;
  63. return params;
  64. }
  65. exports.main = function (event, context) {
  66. const wxContext = cloud.getWXContext()
  67. console.log('event:' + JSON.stringify(event));
  68. let {appId, attach, body, mchId, notifyUrl, ip} = config;
  69. let openId = wxContext.OPENID;
  70. let totalFee = event.totalFee;
  71. let nonceStr = util.createNonceStr();
  72. let tradeId = util.getTradeId();
  73. // 支付前需要先获取签名
  74. let sign = getPrePaySign(appId, attach, body, mchId, nonceStr, notifyUrl, openId, tradeId, ip, totalFee);
  75. // 将签名一起组装成post数据,通过xml的格式发送给微信
  76. let sendData = wxSendData(appId, attach, body, mchId, nonceStr, notifyUrl, openId, tradeId, ip, totalFee, sign);
  77. console.log("sendData:::" + sendData);
  78. return new Promise((resolve,reject)=>{
  79. let url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
  80. request({
  81. url: url,
  82. method: 'POST',
  83. body: sendData
  84. }, function (err, response, body) {
  85. // 先将返回数据转换成json,否则handle方法会报错
  86. if (!err && response.statusCode === 200) {
  87. xml.parseString(body.toString('utf-8'), (error, res) => {
  88. // 获取xml根对象
  89. let data = res.xml;
  90. if (data.return_code[0] == 'SUCCESS' && data.result_code[0] == 'SUCCESS') {
  91. // 获取预支付ID
  92. var prepay_id = data.prepay_id[0];
  93. // 从新签名,获取前端支付SDK所需要的字段
  94. let payResult = getPayParams(appId, prepay_id);
  95. resolve(payResult);
  96. } else {
  97. let msg = data.return_msg || data.err_code_des;
  98. reject(msg);
  99. }
  100. });
  101. } else {
  102. reject(err);
  103. }
  104. })
  105. })
  106. }

4. H5支付流程

4.1. 资源准备

支付条件:

  • H5支付后台可以是http/https,需要配置支付授权目录
  • 小程序所有的接口都必须是https,无需配置授权目录
  • 小程序云开发只有云函数,无需配置授权目录

支付准备:

  • 域名、服务器、证书准备
  • 微信认证,企业/个人主体认证
  • 支付认证,开通商户平台
  • 公众号、小程序、小程序云绑定商户号

域名:

访问域名 服务器 https证书
域名筛选、注册 选择类型、购买 选择类型、购买
域名提交、备案 安装环境和软件 安装、nginx配置

image.png
服务器配置:

  • 系统: Ubuntu/CentOs/WindowServer
  • 宽带: >1M
  • 内存:>1G
  • 磁盘:>50G

微信认证:

微信商户绑定公众号:
image.png

4.2 H5支付实现

前端:

  1. <template>
  2. <div class="recharge">
  3. <div class="recharge-box">
  4. <h2 class="title">充值金额</h2>
  5. <div class="input-box">
  6. <input class="input" type="number" placeholder="请输入充值金额" v-model="money" />
  7. </div>
  8. <div class="item-num">
  9. <span class="num" :class="index == 1 ? 'checked' : ''" @click="choose(1)">1分</span>
  10. <span class="num" :class="index == 20 ? 'checked' : 'default'" @click="choose(20)"
  11. >2毛</span
  12. >
  13. </div>
  14. <div class="item-num">
  15. <span class="num" :class="index == 100 ? 'checked' : 'default'" @click="choose(100)"
  16. >1元</span
  17. >
  18. <span class="num" :class="index == 500 ? 'checked' : 'default'" @click="choose(500)"
  19. >5元</span
  20. >
  21. </div>
  22. <button class="btn btn-primary btn-recharge" @click="pay">充值</button>
  23. <p class="tip">
  24. 点击充值即表示已阅读并同意<a class="protocol" href="javascript:;">充值协议</a>
  25. </p>
  26. </div>
  27. </div>
  28. </template>
  29. <script>
  30. import wx from 'weixin-js-sdk'
  31. import { payWallet } from '../api/auth'
  32. export default {
  33. name: 'pay',
  34. data() {
  35. return {
  36. index: 1,
  37. money: ''
  38. }
  39. },
  40. methods: {
  41. choose(index) {
  42. this.index = index
  43. },
  44. async pay() {
  45. let _this = this
  46. if (this.money && !/^\d*$/g.test(this.money)) {
  47. alert('输入非法金额')
  48. return
  49. }
  50. /* this.$http.get(API.payWallet,{
  51. params:{
  52. money:this.money*100 || this.index
  53. }
  54. }).then((response) => {
  55. let result = response.data;
  56. if(result.code == 0){
  57. let res = result.data;
  58. }
  59. }); */
  60. let params = {
  61. money: this.money * 100 || this.index
  62. }
  63. let [err, res] = await payWallet(params)
  64. if (err) {
  65. console.log(err)
  66. return
  67. }
  68. let payInfo = res.data
  69. // 支付
  70. wx.chooseWXPay({
  71. timestamp: payInfo.timeStamp,
  72. nonceStr: payInfo.nonceStr,
  73. package: payInfo.package,
  74. signType: payInfo.signType,
  75. paySign: payInfo.paySign,
  76. success: function(res) {
  77. if (res.errMsg == 'chooseWXPay:ok') {
  78. _this.$route.push('/index')
  79. } else if (res.errMsg == 'chooseWXPay:fail') {
  80. alert('支付取消')
  81. }
  82. }.bind(this),
  83. cancel: function() {
  84. alert('支付取消')
  85. }.bind(this),
  86. fail: function(res) {
  87. alert(res.message || '支付失败')
  88. }.bind(this)
  89. })
  90. }
  91. }
  92. }
  93. </script>
  94. <style>
  95. .recharge {
  96. background: #ffffff;
  97. }
  98. .recharge-box {
  99. padding: 0 0.3rem;
  100. }
  101. .recharge-box .title {
  102. font-size: 0.34rem;
  103. color: #333333;
  104. margin-top: 0.69rem;
  105. }
  106. .recharge-box .input-box {
  107. text-align: center;
  108. margin-top: 0.5rem;
  109. margin-bottom: 0.3rem;
  110. }
  111. .recharge-box .input-box .input {
  112. display: block;
  113. width: 6.9rem;
  114. height: 1rem;
  115. border: 1px solid #d7d7d7;
  116. border-radius: 5px;
  117. font-size: 0.3rem;
  118. color: #333333;
  119. text-align: center;
  120. }
  121. .item-num {
  122. display: flex;
  123. justify-content: space-between;
  124. margin-bottom: 0.31rem;
  125. }
  126. .item-num .num {
  127. height: 1.3rem;
  128. background-color: #f1f3f5;
  129. color: #333333;
  130. font-size: 0.32rem;
  131. width: 3.35rem;
  132. text-align: center;
  133. line-height: 1.3rem;
  134. border-radius: 0.08rem;
  135. }
  136. .item-num .num.checked {
  137. background-color: #ffebe8;
  138. color: #ff3418;
  139. }
  140. .recharge-box .btn-recharge {
  141. /*此处3.9改成3,保证一屏可以看到,因为距离太大了*/
  142. display: block;
  143. margin: 3rem auto 0.48rem;
  144. color: #ffffff;
  145. font-weight: 400;
  146. box-shadow: 0px 10px 18px 0px rgba(255, 106, 106, 0.57);
  147. }
  148. .recharge-box .tip {
  149. font-size: 0.28rem;
  150. color: #999999;
  151. text-align: center;
  152. }
  153. .recharge-box .tip .protocol {
  154. color: #ff3418;
  155. }
  156. </style>

后端:

  1. // routes/pay/wx.js
  2. const { order } = require('../common/wxpay');
  3. /**
  4. * 微信支付
  5. */
  6. router.get('/pay/payWallet', function (req, res) {
  7. let appid = config.appId //小程序id
  8. let openid = req.cookies.openId; // 用户的openid
  9. let attach = '微信h5支付附加数据' // 附加数据
  10. let body = '小程序支付' // 主体内容
  11. let total_fee = req.query.money // 支付金额(分)
  12. let ontify_url = 'http://localhost:3000/api/wechat/pay/callback' // 微信回调接口
  13. let ip = '123.57.2.144' // 终端ip
  14. let orderRes = order({ appid, attach, body, openid, total_fee, ontify_url, ip }).then(result => {
  15. res.json(util.handleSuccess(result))
  16. }).catch((result) => {
  17. res.json(util.handleFail(result))
  18. })
  19. })
  20. /**
  21. * 此接口主要用于支付成功后的回掉,用于统计数据
  22. * 此处需要再app.js中设置请求头的接收数据格式
  23. */
  24. router.post('/pay/callback', function (req, res) {
  25. xml.parseString(req.rawBody.toString('utf-8'), async (error, xmlData) => {
  26. if (error) {
  27. logger.error(error);
  28. res.send('fail')
  29. return;
  30. }
  31. let data = xmlData.xml;
  32. let order = {
  33. openId: data.openid[0],
  34. totalFee: data.total_fee[0],
  35. isSubscribe: data.is_subscribe[0],
  36. orderId: data.out_trade_no[0],
  37. transactionId: data.transaction_id[0],
  38. tradeType: data.trade_type[0],
  39. timeEnd: data.time_end[0]
  40. }
  41. // 插入订单数据
  42. let result = await dao.insert(order, 'orders');
  43. if (result.code == 0) {
  44. // 向微信发送成功数据
  45. let data = '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
  46. res.send(data);
  47. } else {
  48. res.send('FAIl');
  49. }
  50. });
  51. })

七、部署流程

1. 小程序部署

  • 业务域名
  • 服务器域名
  • 线上最低基础库设置
  • 成员管理、反馈管理、统计
  • 预览、真机调试、清缓存、上传

    2. Node部署

  • 环境安装

  • 数据库安装
  • 代码上传
  • Nginx配置
  • Node服务配置