九、小程序端【登录 / 菜品】接口
9.1 构建小程序项目
通过 Hbuildx 构建uniapp项目 具体选项如下:
- 项目版本: vue3 + pinia + vite
- 项目名称: diancan-user
页面结构与UI样式
目前页面结构与UI样式在素材中已提供 大家可根据需求自行添加
项目页面分析
小程序点餐用户端主要页面有: 提示页
选择页
点餐页
订单页
订单列表页
- 提示页 : 主要用于为扫码用户的提示界面 并未主逻辑
- 选择页: 用于用户扫码后 选择用餐人数 以及 授权用户信息
- 点餐页: 核心页面 根据菜品类别 加载菜品 项目的主要逻辑页 包含:
点餐
购物车
预览卡片
左右分类栏
- 订单页: 用户下单后 看到的页面 主要显示 所点菜品信息以及加菜操作
- 订单列表页: 显示用户以往所有的订单信息
项目基础功能配置
配置Pinia ->
main.ts
tsimport { 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
tsclass 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 登录接口
登录接口 主要用于 小程序选择人数页(选择页) 用户点餐前的授权操作
因为本项目使用的微信授权模式 故不需要小程序用户进行用户名/手机登录 直接授权即可登录
服务器端根据授权信息 自动解析小程序用户信息
需要的逻辑: 小程序登录
解析小程序授权信息
添加用户信息
小程序微信授权模式流程:
- 小程序端进行wx.login登录 并 授权用户信息
- 发送 wxcode 与 授权信息至服务器端
- 服务器将wxcode 与 授权信息发送至微信后台进行登录 【核心】
- 通过微信后台返回的响应数据 解析 授权信息 【核心】
- 将解密后的信息 添加至 数据库
- 响应信息
小程序登录
该过程主要为 将小程序端wx.login
接口获得临时登录凭证 code 传到开发者服务器调用此接口完成登录流程。
从而实现小程序端静默登录 【授权是为了拿用户信息 后端自己做保留】
由于需要将数据发送至微信开发者服务器 所以需要使用 服务器端发送请求 在这里我们使用Axios
库
下载axios库: yarn add axios
实现步骤:
定义接口路由
/auth/login
校验参数:code
登录码js// * 小程序登录(获取OpenID) router.post('/login', ParamsValidator(wxLoginValidator), async (ctx) => {})
准备小程序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'
发送至微信开发者服务器 进行登录 -> 换取登录凭证【其他API需要】以及session_key【解析用户信息】
tsconst { 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 || '服务器错误')
解析小程序授权信息
由于微信会对这些开放数据做签名和加密处理。
开发者后台拿到开放数据后可以对数据进行校验签名和解密,来保证数据不被篡改。
解析步骤:
导入NodeJS解析小程序授权包
yarn add crypto
utils/WXBizDataCrypt.ts
tsimport 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
接收 小程序传递的参数 【小程序授权信息会加密在 encryptedData 以及 iv中】
tsconst { code, encryptedData, iv } = ctx.request.body
解析用户 将
appid
,session_key
作为解密keyencryptedData
,iv
作为解密数据tsconst { nickName, gender, city, avatarUrl } = new WXBizDataCrypt(wx_appid, session_key).decryptData(encryptedData, iv)
目前微信只返回 用户名、性别、城市、头像
添加用户信息
后台需要对登录过的用户做信息保留 这个用于后期用户分析很重要!!!
可以理解 记录使用过点餐小程序的用户数据
创建小程序用户表
表名:
wx_user
表字段:
字段名 字段类型 备注 id int id序列号 主键 自增 openid varchar 微信openid nickName varchar 微信用户名 avatarUrl varchar 微信头像 city varchar 地区 gender tinyint 性别 0男 1女 createTime varchar 创建时间
验证登录用户是否已添加
// 判断该用户是否注册过 【注册 -> 返回openid 未注册 -> 解析/注册用户&&返回openid 】
const wx_info = await flq.from('wx_user').where({ openid }).first()
if (wx_info) return ctx.success(openid)
添加用户信息
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 商家地址】
// * 加载商家信息
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. 将菜品列表按照类目分组
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('错误')
}
})