Skip to content
本页目录

九、小程序端【登录 / 菜品】接口

9.1 构建小程序项目

通过 Hbuildx 构建uniapp项目 具体选项如下:

  1. 项目版本: vue3 + pinia + vite
  2. 项目名称: diancan-user

页面结构与UI样式

目前页面结构与UI样式在素材中已提供 大家可根据需求自行添加

项目页面分析

小程序点餐用户端主要页面有: 提示页 选择页 点餐页 订单页 订单列表页

  • 提示页 : 主要用于为扫码用户的提示界面 并未主逻辑
  • 选择页: 用于用户扫码后 选择用餐人数 以及 授权用户信息
  • 点餐页: 核心页面 根据菜品类别 加载菜品 项目的主要逻辑页 包含: 点餐 购物车 预览卡片 左右分类栏
  • 订单页: 用户下单后 看到的页面 主要显示 所点菜品信息以及加菜操作
  • 订单列表页: 显示用户以往所有的订单信息

项目基础功能配置

  • 配置Pinia -> main.ts

    ts
    import { createSSRApp } from 'vue'
    import * as Pinia from 'pinia'
    
    export function createApp() {
    	const app = createSSRApp(App)
    	const pinia = Pinia.createPinia()
    	
    	app.use(pinia);
    	return {
    		app,
    		Pinia, // 此处必须将 Pinia 返回
    	}
    }
    
  • 配置快捷弹窗 -> main.ts

    ts
    // 封装弹框的方法
    uni.$Toast = function(title = '数据请求失败!', duration = 1500) {
    	uni.showToast({
    		title,
    		duration,
    		icon: 'none'
    	})
    }
    
  • 配置请求API utils/request.ts

    ts
    class Request {
    	constructor(options = {}) {
    		// 请求的根路径
    		this.baseUrl = options.baseUrl || ''
    		// 请求的 url 地址
    		this.url = options.url || ''
    		// 请求方式
    		this.method = 'GET'
    		// 请求的参数对象
    		this.data = null
    		// header 请求头
    		this.header = options.header || {}
    		this.beforeRequest = null
    		this.afterRequest = null
    	}
    
    	get(url, data = {}) {
    		this.method = 'GET'
    		this.url = this.baseUrl + url
    		this.data = data
    		return this._()
    	}
    
    	post(url, data = {}) {
    		this.method = 'POST'
    		this.url = this.baseUrl + url
    		this.data = data
    		return this._()
    	}
    
    	put(url, data = {}) {
    		this.method = 'PUT'
    		this.url = this.baseUrl + url
    		this.data = data
    		return this._()
    	}
    
    	delete(url, data = {}) {
    		this.method = 'DELETE'
    		this.url = this.baseUrl + url
    		this.data = data
    		return this._()
    	}
    
    	_() {
    		// 清空 header 对象
    		this.header = {}
    		// 请求之前做一些事
    		this.beforeRequest && typeof this.beforeRequest === 'function' && this.beforeRequest(this)
    		// 发起请求
    		return new Promise((resolve, reject) => {
    			let weixin = wx
    			// 适配 uniapp
    			if ('undefined' !== typeof uni) {
    				weixin = uni
    			}
    			weixin.request({
    				url: this.url,
    				method: this.method,
    				data: this.data,
    				header: this.header,
    				success: (res) => {
    					const { data } = res
    					if (typeof res.data !== 'object') {
    					  wx.showToast({ title: '服务端异常', icon: 'none' })
    					  return reject(res)
    					}
    					if (data.code !== 200) {
                            uni.$Toast(data.msg || '请求异常')
    						return reject(data.msg || '请求错误')
    					}
    					resolve(data)
    				},
    				fail: (err) => {
    					reject(err)
    				},
    				complete: (res) => {
    					// 请求完成以后做一些事情
    					this.afterRequest && typeof this.afterRequest === 'function' && this
    						.afterRequest(res)
    				}
    			})
    		})
    	}
    }
    
    export const $http = new Request()
    
  • 创建页面 将需要的页面以及样式进行初始化

    • 提示页 saoma 扫码点餐
    • 选择页 index 选择人数
    • 点餐页 home xxx餐饮店
    • 订单页 order-detail 订单详情
    • 订单列表页 my-order 我的订单
  • 配置窗口表现 标题、导航颜色

    json
    "globalStyle": {
        "navigationBarTextStyle": "black",
        "navigationBarTitleText": "阿坤点餐",
        "navigationBarBackgroundColor": "#F8F8F8",
        "backgroundColor": "#F8F8F8"
    },
    

9.2 登录接口

登录接口 主要用于 小程序选择人数页(选择页) 用户点餐前的授权操作

因为本项目使用的微信授权模式 故不需要小程序用户进行用户名/手机登录 直接授权即可登录

服务器端根据授权信息 自动解析小程序用户信息

需要的逻辑: 小程序登录 解析小程序授权信息 添加用户信息

小程序微信授权模式流程:

  1. 小程序端进行wx.login登录 并 授权用户信息
  2. 发送 wxcode 与 授权信息至服务器端
  3. 服务器将wxcode 与 授权信息发送至微信后台进行登录 【核心】
  4. 通过微信后台返回的响应数据 解析 授权信息 【核心】
  5. 将解密后的信息 添加至 数据库
  6. 响应信息
img

小程序登录

该过程主要为 将小程序端wx.login 接口获得临时登录凭证 code 传到开发者服务器调用此接口完成登录流程。

从而实现小程序端静默登录 【授权是为了拿用户信息 后端自己做保留】

由于需要将数据发送至微信开发者服务器 所以需要使用 服务器端发送请求 在这里我们使用Axios

下载axios库: yarn add axios

实现步骤:

  1. 定义接口路由 /auth/login 校验参数: code 登录码

    js
    // * 小程序登录(获取OpenID)
    router.post('/login', ParamsValidator(wxLoginValidator), async (ctx) => {})
    
  2. 准备小程序sk 与 appid 以及 小程序请求根地址 【微信后台服务器需要作为验证标准 -> 主要判断用户登录了哪一个小程序】

    config.ts

    ts
    // 小程序appid
    export const wx_appid = 'wx5f96f51bdf9d3676'
    // 小程序secret
    export const wx_secret = 'a913748c69e5d94654e58ae29cf0f525'
    // 小程序请求根地址
    export const wx_url = 'https://api.weixin.qq.com'
    
  3. 发送至微信开发者服务器 进行登录 -> 换取登录凭证【其他API需要】以及session_key【解析用户信息】

    ts
    const { code } = ctx.request.body
    // 1. 请求参数
    const params = { appid: wx_appid, secret: wx_secret, js_code: code , grant_type: 'authorization_code' }
    // 2. 请求地址
    const url = `${wx_url}/sns/jscode2session`
    // 3. 发起请求
    const { data } = await Axios.get(url, { params })
    // 4. 判断请求是否成功
    const { errcode, errmsg, openid, session_key } = data
    if (errcode) return ctx.fail(errmsg || '服务器错误')
    

解析小程序授权信息

由于微信会对这些开放数据做签名和加密处理。

开发者后台拿到开放数据后可以对数据进行校验签名和解密,来保证数据不被篡改。

img

解析步骤:

  1. 导入NodeJS解析小程序授权包 yarn add crypto

    utils/WXBizDataCrypt.ts

    ts
    import crypto from 'crypto'
    
    function WXBizDataCrypt(appId, sessionKey) {
      this.appId = appId
      this.sessionKey = sessionKey
    }
    
    WXBizDataCrypt.prototype.decryptData = function (encryptedData, iv) {
      // base64 decode
      var sessionKey = Buffer.from(this.sessionKey, 'base64')
      encryptedData = Buffer.from(encryptedData, 'base64')
      iv = Buffer.from(iv, 'base64')
    
      try {
         // 解密
        var decipher = crypto.createDecipheriv('aes-128-cbc', sessionKey, iv)
        // 设置自动 padding 为 true,删除填充补位
        decipher.setAutoPadding(true)
        var decoded = decipher.update(encryptedData, 'binary', 'utf8')
        decoded += decipher.final('utf8')
        
        decoded = JSON.parse(decoded)
    
      } catch (err) {
        throw new Error('Illegal Buffer')
      }
    
      // @ts-ignore
      if (decoded.watermark.appid !== this.appId) {
        throw new Error('Illegal Buffer')
      }
    
      return decoded
    }
    
    export default WXBizDataCrypt
    
  2. 接收 小程序传递的参数 【小程序授权信息会加密在 encryptedData 以及 iv中】

    ts
    const { code, encryptedData, iv } = ctx.request.body
    
  3. 解析用户 将appid ,session_key 作为解密key encryptedData, iv作为解密数据

    ts
    const { nickName, gender, city, avatarUrl } = new WXBizDataCrypt(wx_appid, session_key).decryptData(encryptedData, iv)
    

    目前微信只返回 用户名、性别、城市、头像

添加用户信息

后台需要对登录过的用户做信息保留 这个用于后期用户分析很重要!!!

可以理解 记录使用过点餐小程序的用户数据

创建小程序用户表

  • 表名: wx_user

  • 表字段:

    字段名字段类型备注
    idintid序列号 主键 自增
    openidvarchar微信openid
    nickNamevarchar微信用户名
    avatarUrlvarchar微信头像
    cityvarchar地区
    gendertinyint性别 0男 1女
    createTimevarchar创建时间

验证登录用户是否已添加

ts
// 判断该用户是否注册过 【注册 ->  返回openid     未注册 -> 解析/注册用户&&返回openid  】
const wx_info = await flq.from('wx_user').where({ openid }).first()
if (wx_info) return ctx.success(openid)

添加用户信息

ts
const createTime = dayjs().format('YYYY-DD-MM hh:mm:ss')
await flq.from('wx_user').value({ openid, nickName, avatarUrl, city, gender, createTime }).add()
ctx.success(openid)

9.3 菜品接口

菜品相关接口 主要根据 对应商家 查询该商家的 菜品类目 - 菜品信息

需要的接口: 获取商家信息 商家菜品数据【菜品类目、菜品】

获取商家信息

用于小程序扫码后 商家基础信息的显示【后续加载菜品需要uid】

  • 请求地址: /dish/shop-info
  • 请求方法:GET
  • 请求参数: uid
  • 逻辑: 1. 根据uid查询商家信息 2. 返回部分字段【商家名称 商家Logo 商家uid 商家地址】
ts
// * 加载商家信息
router.get('/shop-info', ParamsValidator(shopInfo3Rules), async (ctx) => {
  // 解析商家ID
  const { uid } = ctx.query as Record<string, string>
  try {
    // 获取商家信息
    const info = await flq.from('shop_info').where({ uid }).first()
    if (!info) return ctx.fail('uid有误', 202)
    ctx.success(info)
  } catch (error) {
    ctx.error('加载失败')
  }
})

商家菜品数据

用于小程序点餐界面的 菜品与类目的显示 该接口会一次性返回所有类目以及类目下对应的菜品

响应结构: { res_cate: 类目, res_dish: 菜品 [{ cid: 类目id, category: 类目名, list: 菜品 }] }

  • 请求地址: /dish/list
  • 请求方法:GET
  • 请求参数: uid
  • 逻辑: 1. 根据uid查询菜品类目 2. 根据uid查询菜品列表 3. 将菜品列表按照类目分组
ts
router.get('/list', ParamsValidator(shopInfo3Rules), async (ctx) => {
  // 解析商家ID
  const { uid } = ctx.query as Record<string, string>
  try {
    // 获取分类列表(不分页)
    const category = await flq.from('dishes_category').where({ uid }).find()
    // 获取菜品列表
    const dish = await flq.from('dishes_data').where({ uid }).find()
    // 优化菜品列表格式
    const res_dish = category.map(cate => ({
      cid: cate.id,
      category: cate.label,
      list: dish.filter(item => item.cid === cate.id)
    }))
    ctx.success({ res_cate: category, res_dish })
  } catch (error) {
    ctx.error('错误')
  }
})