几周前帮朋友捣鼓了一个校园跑腿点餐的小项目,前后端加小程序端折腾了大概三天。最近后台数据涨得不错,顺手把整套实现思路整理出来。这篇文章不会讲太多花哨的架构设计,全是实际能跑通的代码路径和踩坑记录,希望对正在做类似毕业设计或者个人项目的朋友有帮助。
这个项目说白了就是三件事:小程序端给用户点餐,Vue后台给商家管菜单和订单,Node.js做中间的数据中转和业务处理。说真的,这套组合在目前的个人项目和中小型公司里太常见了,因为每一环都有大量成熟方案可以抄,组合起来又没有什么明显的硬伤。
如果你正准备做网上订餐类的系统,或者手头有个小程序项目不知道怎么管理后台,这篇文章可以当作一份完整的地图来参考。就算你不是做订餐的,里面关于Vue+Node.js+小程序的配合方式、鉴权思路、订单状态机设计,也能直接搬到其他业务场景。
1. 项目整体设计与技术选型思路
1.1 为什么是 Vue + Node.js + 小程序 这套组合
先说选型。我之前也纠结过要不要用Java后端加Vue全家桶,后来发现Node.js的优势在这个项目里特别明显:前后端都是JavaScript,数据格式自然就是JSON,完全不需要做任何序列化转换。这对于快速开发太关键了,尤其是订单、菜品这类嵌套结构比较多的数据,用JavaScript对象直接传递,省掉了一大堆DTO转换的样板代码。
Vue这边我选的是Vue 2 + Element UI,没上Vue 3,原因很简单:Element UI对Vue 2的支持最成熟,网上案例最多,遇到问题一搜就有答案。对个人项目来说,能快速解决问题比用什么新特性更重要。当然如果你打算长期维护,Vue 3 + Element Plus也不差,API更现代,类型推导更好,但开发效率上提升不大。
小程序端就是微信原生小程序,没有用uni-app或者Taro。原因是这个项目的页面结构比较简单,就一个点餐首页、商品详情、购物车、订单列这几个页面,原生的写法完全够用。而且原生小程序调试起来最方便,而且踩坑资料最多,没必要引入一层跨端框架增加心智负担。
1.2 系统整体架构与模块划分
整个系统分成三部分,对应三个独立的项目工程:
- 小程序端:用户下单入口,负责菜单浏览、商品加购、下单支付、订单查询
- Vue管理后台:商家运营入口,负责菜品管理、订单处理、配送状态更新
- Node.js后端:提供RESTful API,负责用户身份认证、订单业务逻辑、数据持久化
这三者之间的数据流向是:小程序调用后端接口,后端操作数据库,Vue后台也调用后端接口。后端是整个系统的核心枢纽,所有业务逻辑都收敛在这里。
数据库我选了MySQL,没有上MongoDB。客观讲,订单这类事务性强的数据确实需要关系型数据库来保证一致性。虽然MongoDB在开发时不用建表很爽,但后面做订单统计、按时间查询这些需求时,SQL的灵活性和性能都不错。项目早期用MySQL,长期维护也放心。
1.3 开发环境准备的三个关键坑
环境配置这一节我放在最前面讲,因为估计有很多人卡在这一步。这几个问题我在配环境时几乎全踩了一遍,而且群里也有不少朋友反复问。
第一个坑:Node.js版本选择。我刚开始装的是最新的Node.js 20,结果发现有些老项目跑不起来。后来看了下npm官方支持说明,果断换成了Node.js 16 LTS版本。对于个人项目,尽量选LTS版本,不要追新,因为很多依赖包还没有适配最新版,折腾半天都是依赖兼容问题。
第二个坑:npm权限问题。Windows系统上经常遇到“npm无法加载文件npm.ps1,因为在此系统上禁止运行脚本”的报错,这其实是PowerShell执行策略的限制。解决方案是:以管理员身份打开PowerShell,运行Set-ExecutionPolicy RemoteSigned命令,然后选择“Y”确认。这个问题在Windows下太典型了,几乎每个用npm的人都会遇到。
第三个坑:微信小程序开发者工具的AppID。注册小程序需要企业资质或个人资质,如果没有AppID,可以使用测试号。用测试号有很多限制,比如不能使用微信支付,所以如果是要做完整系统,建议提前申请好AppID。我在开发初期为了省事儿用的测试号,结果后面做支付联调时又得重新配置,白白浪费了一些时间。
1.4 Vue后台管理端的脚手架选择
Vue后台我用的vue-element-admin这个开源模板,准确说是基于它的精简版。这个框架网上一抓一大把,用来做后台管理非常顺手,已经集成了登录、权限验证、动态路由、面包屑、标签页这些常用功能,比自己从零开始搭要快太多了。
不过要注意的是,vue-element-admin本身集成的功能很多,直接拿来用会有很多用不上的模块,我当时就花了一晚上做减法,把不相关的页面删掉,只保留登录、Dashboard、商品管理、订单管理这几个核心模块。做减法这步一定不能省,否则后台页面会非常臃肿,自己后面维护也会很累。
2. 数据库设计:订单系统的地基
2.1 核心数据表结构详解
订餐系统的数据库设计是整个项目最需要动脑子的部分。我先梳理一下实体关系:用户要能下单,商家要能管理菜品,每个菜品要归属于某个商家,订单要有菜品明细,订单还要有配送地址和状态变化记录。
我设计了以下这几张核心表:
users(用户表):
- id:主键,自增
- openid:微信openid,唯一索引
- nickname:用户昵称
- avatar:头像URL
- phone:手机号
- create_time:创建时间
restaurants(商家表):
- id:主键
- name:店名
- address:地址
- logo:店铺logo
- status:营业状态,0打烊,1营业
- notice:公告信息
dishes(菜品表):
- id:主键
- restaurant_id:所属商家ID,外键
- name:菜名
- price:价格,用Decimal(10,2)
- image:图片URL
- category:分类,如热菜、凉菜、饮品
- status:上架状态
- description:描述
- stock:库存
orders(订单表):
- id:主键
- order_no:订单编号,唯一
- user_id:用户ID
- restaurant_id:商家ID
- total_amount:订单总金额
- address:收货地址
- contact_name:联系人
- contact_phone:联系电话
- status:订单状态,0待支付,1已支付,2制作中,3配送中,4已完成,5已取消
- remark:备注信息
- create_time:下单时间
order_items(订单明细表):
- id:主键
- order_id:订单ID
- dish_id:菜品ID
- dish_name:菜品名称(冗余存储,防止菜品信息变化后无法追溯)
- price:购买时的单价
- quantity:购买数量
- subtotal:小计金额
delivery_records(配送记录表):
- id:主键
- order_id:订单ID
- delivery_user_id:配送员ID
- status:配送状态
- update_time:更新时间
- location:位置信息
2.2 为什么订单明细要冗余字段
这里有一个值得展开说说的地方,就是订单明细表里为什么还要存dish_name和price快照。刚开始做的时候,我天真地以为直接关联菜品表查名字和价格就行,结果后来发现了一个问题:如果商家改了菜品的价格或者菜品名称,历史订单就会显示出错误的数据。
举个例子,用户周三下单时红烧肉是38元,周四商家涨到45元。等用户周五查看订单记录,如果实时关联菜品表,看到的就是45元,而实际他支付的是38元。这个坑很隐蔽,如果不处理,后面查账会对不上。解决方案就是在下单时把菜品的名称、单价复制一份存入订单明细表,用空间换正确性。
用户下单后商家菜品如何变化,历史订单都不会受影响。这也是正规电商系统的通用做法,不是我这个项目独有的。
2.3 订单状态机的流转设计
订单状态是我这个系统里逻辑最复杂的部分,一定要在设计阶段就规划好。我在项目里定义了一组状态流转规则,各端严格按照这个规则来更新数据:
待支付 → 已支付 → 制作中 → 配送中 → 已完成
每个状态之间的跳转都必须通过后端接口完成,前端不管直接写死状态值。例如用户点击支付,前端只调用后端接口,由后端判断当前状态是否为“待支付”,如果是才允许更新为“已支付”,否则直接返回错误。
这里我还有一个加料的设计:状态变更记录表。每次订单状态变化就插入一条记录,记录订单编号、旧状态、新状态、操作人和操作时间。这个表看起来简单,但后面出问题追责或者用户投诉时,就是最好的排查依据。
3. Node.js后端接口与业务逻辑实现
3.1 项目初始化和核心依赖
后端我用的Express框架,没有用NestJS这种重型框架。原因是项目规模不大,Express的中间件机制足够灵活,社区教程也多。初始化一个Node.js项目很简单,几步就搞定:
mkdir delivery-backend cd delivery-backend npm init -y npm install express mysql2 sequelize jsonwebtoken bcryptjs cors npm install nodemon --save-dev这些依赖各自的用途:
- express:web框架,路由和中间件核心
- mysql2:连接MySQL数据库的驱动,支持Promise
- sequelize:ORM框架,避免手写SQL
- jsonwebtoken:生成和验证JWT登录令牌
- bcryptjs:密码加密(管理端用户要用)
- cors:解决跨域问题
- nodemon:开发时热重载,改完代码不用手动重启
3.2 数据库连接与模型定义
我用Sequelize作为ORM,它最大的好处是不用写原生SQL。定义模型的代码看起来是这样的:
const { DataTypes } = require('sequelize'); const sequelize = new Sequelize('delivery_db', 'root', 'password', { host: 'localhost', dialect: 'mysql', timezone: '+08:00' }); const Dish = sequelize.define('Dish', { name: { type: DataTypes.STRING(100), allowNull: false }, price: { type: DataTypes.DECIMAL(10, 2), allowNull: false }, category: { type: DataTypes.STRING(50) }, image: { type: DataTypes.STRING(255) }, status: { type: DataTypes.INTEGER, defaultValue: 1 }, stock: { type: DataTypes.INTEGER, defaultValue: 0 } });有几点值得注意。第一,时间戳字段要设置timezone: '+08:00',否则Sequelize默认使用UTC时区,存入数据库的时间会比北京时间少8个小时,后面查订单时间会对不上。第二,价格字段一定要用DECIMAL,不能用FLOAT,浮点数计算金额时会有精度丢失的问题,这属于常识级别的问题了。
3.3 JWT用户鉴权流程
小程序的用户登录流程和普通网站不一样,没有密码这一说。流程是这样的:小程序调用wx.login获取一个code,然后把code发给后端,后端拿着这个code去微信接口换取openid,再用openid作为唯一标识去查找或创建用户,最后返回一个JWT令牌给小程序。
router.post('/login', async (req, res) => { const { code, nickname, avatar } = req.body; // 用code换取openid const appid = '你的appid'; const secret = '你的secret'; const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appid}&secret=${secret}&js_code=${code}&grant_type=authorization_code`; const result = await axios.get(url); const { openid } = result.data; // 查找或创建用户 let user = await User.findOne({ where: { openid } }); if (!user) { user = await User.create({ openid, nickname, avatar }); } // 生成JWT令牌 const token = jwt.sign({ id: user.id }, 'your-secret-key', { expiresIn: '7d' }); res.json({ token, user }); });JWT令牌有效期我设置的7天,这样用户不用每次都重新登录,体验比较好。密钥一定要放到环境变量里,不要写死在代码中,虽然个人项目影响不大,但养成好习惯总没错。
3.4 下单与支付的核心接口实现
下单接口是整个系统的核心,里面涉及事务处理。什么场景下必须用事务?就是同时写多张表的场景。下单这个操作要写订单表、订单明细表,还要更新菜品库存,这三个操作必须绑在一起,要么全部成功,要么全部失败,否则就会出现订单建了但库存没扣,或者库存扣了但订单没成的脏数据。
const transaction = await sequelize.transaction(); try { // 创建订单 const order = await Order.create({ order_no: generateOrderNo(), user_id: userId, restaurant_id: restaurantId, total_amount: totalAmount, status: 0 }, { transaction }); // 批量创建订单明细 const items = cartItems.map(item => ({ order_id: order.id, dish_id: item.dishId, dish_name: item.dishName, price: item.price, quantity: item.quantity })); await OrderItem.bulkCreate(items, { transaction }); // 扣减库存 for (const item of cartItems) { await Dish.decrement('stock', { by: item.quantity, where: { id: item.dishId } }); } await transaction.commit(); } catch (error) { await transaction.rollback(); res.status(500).json({ message: '下单失败' }); }订单号生成这块也有讲究。我用的是时间戳加随机数的方式:yyyyMMddHHmmss + 4位随机数,基本保证唯一。当然如果要更严谨,可以加一个用户ID的片段,进一步降低重复概率。千万不要用自增ID当订单号,一是容易暴露订单量,二是多表关联时凭订单号看不出时间信息。
真实支付我做了个模拟接口,直接让用户选择模拟支付成功,然后后端把订单状态从“待支付”改为“已支付”。如果要接入微信支付,需要企业资质、商户号,开通流程比较长,个人项目建议先用模拟支付把整个流程跑通,后面有条件再替换。
3.5 配送状态更新的实现
配送功能是订餐系统区别于普通商城的一个重点。我在设计与逻辑上比较倾向于简化角色,把配送员和商家统一成后台管理端的角色,即商家自己处理接单和配送。这样在小规模试点时可以省掉一个配送员的角色,等业务大了再把配送员拆出来。
配送状态更新接口的业务规则是这样的:只有订单当前状态为“已支付”时,商家才能点“开始制作”,把状态改成“制作中”;只有“制作中”才能改成“配送中”;只有“配送中”才能改成“已完成”。这其实就是前面说的状态机,每个接口进来先校验当前状态,不满足条件直接返回错误信息。
4. 小程序端点餐流程与购物车实现
4.1 项目目录结构与页面规划
小程序端我按照业务模块划分目录,每个页面独立文件夹,结构清晰:
miniprogram/ ├── pages/ │ ├── index/ // 首页:店铺列表、菜品列表 │ ├── detail/ // 菜品详情页 │ ├── cart/ // 购物车页面 │ ├── order/ // 订单确认页 │ ├── order-list/ // 订单列表 │ ├── order-detail/ // 订单详情页 │ └── user/ // 个人中心 ├── utils/ │ ├── request.js // 封装wx.request请求 │ ├── auth.js // 登录鉴权相关 │ └── cart.js // 购物车本地缓存管理 ├── app.js // 小程序入口文件 ├── app.json // 全局配置 └── app.wxss // 全局样式首页的逻辑是:进入后先请求后端接口获取所有营业中的商家,点击某个商家后,再获取该商家的菜品列表,按照分类显示。我用的是九宫格式布局,菜品以卡片形式展示,点击进入详情页。
4.2 购物车状态管理的两种方案
购物车是小程序端的核心状态,我用了本地缓存+全局数据的组合方案。具体来说:
- 用
wx.setStorageSync把购物车数据持久化到本地,这样即使小程序被杀掉重新打开,购物车数据也不会丢 - 用
app.globalData.cart保存内存中的购物车数据,这样页面切换时不需要反复读缓存,性能更好 - 每次增删商品时,同时更新内存数据和本地缓存,两个地方保持同步
购物车的数据结构是这样的:
{ "restaurantId": 1, "items": [ { "dishId": 10, "name": "回锅肉", "price": 28, "quantity": 2, "image": "..." } ] }这里有一个很重要的规则:购物车只能同时包含一个商家的菜品。用户如果加了A商家的菜,再去加B商家的菜,我会弹窗提示他“购物车已有其他商家的商品,是否清空并重新添加”。因为一次配送只能对应一个商家,如果跨商家下单,配送费用和配送逻辑都会变得非常麻烦。这个限制从用户视角看也完全合理。
4.3 小程序端请求封装与登录流程串联
小程序的wx.request每次都要写一大堆参数,所以我封装了一个request工具函数,把公共逻辑抽出来:
const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method, data, header: { 'Content-Type': 'application/json', 'Authorization': getToken() // 从storage中取token }, success: (res) => { if (res.statusCode === 401) { // token过期,重新登录 loginAndRequest(url, method, data).then(resolve).catch(reject); } else { resolve(res.data); } }, fail: (err) => reject(err) }); }); };登录时序是小程序端最容易疏忽的地方。正确的流程是:小程序启动时先检查storage里有没有token,有就直接用;没有的话调用wx.login获取code,发给后端换token,然后存起来。这个流程要在app.js的onLaunch里执行,并且要确保在后续接口调用前登录完成。
我用了Promise把登录包装成异步操作,所有请求先等待登录状态就绪再发出。具体实现思想是:假如用户第一次打开就快速点击点餐,此时登录还没完成,我们不能直接发未带token的请求。我的方案是维护一个loginPromise变量,初始为null,第一次调用login时赋值Promise,后续的调用都复用同一个Promise,保证登录请求只发一次。
4.4 订单确认页与支付流程的细节
订单确认页需要展示以下内容:
- 收货人信息:姓名、电话、详细地址
- 商品清单:从购物车数据读取,这里数据不重新请求后端,直接用本地缓存
- 配送费:按距离或固定金额计算,我这边做的是满30元免配送费,不满收5元
- 订单备注:用户填写的备注信息,一起传到后端
用户点击“提交订单”后,先调用后端创建订单接口,拿到订单号和订单ID,然后进入支付环节。我做的模拟支付是弹出一个确认框,点击确认后调用支付接口模拟成功,然后跳转到订单列表,并把购物车清空。
这个环节容易出的问题是重复点击提交按钮。网络慢的情况下用户连续点两次,可能创建两个一模一样的订单。我处理的方式是按钮点击后立即置disabled状态并显示“提交中...”,等接口返回后再恢复。这属于前端交互的基本功,但很多人会忽略。
4.5 订单状态轮询实现
用户下单后,订单状态的更新是由商家在后台操作的。小程序端如何及时感知状态变化?我用了简单的轮询机制:在订单详情页每10秒请求一次订单详情接口,如果状态发生变化就刷新页面数据。
轮询实现很简单,在onShow里启动定时器:
onShow() { this.startPolling(); }, onHide() { this.stopPolling(); }, methods: { startPolling() { this.pollingTimer = setInterval(() => { this.fetchOrderDetail(); }, 10000); }, stopPolling() { if (this.pollingTimer) { clearInterval(this.pollingTimer); } } }定时器的启动和停止一定要记得绑定页面的onShow和onHide生命周期。有一次我就是只开了定时器,忘记在onHide里关掉,页面退回列表后定时器还在跑,一直在疯狂请求接口,被同事笑称为“内存泄漏教科书案例”。
如果追求更好的实时性,可以用微信小程序的WebSocket能力,服务端主动推送状态变化。但WebSocket会增加后端复杂度,对个人项目来说没必要,10秒轮询完全够用。
5. Vue后台管理端的实现
5.1 路由和权限控制
Vue后台我基于vue-element-admin模板改造,路由表分为两部分:一部分是固定的路由(登录页、404页),另一部分是动态路由(需要登录后根据角色权限动态添加)。我这边给后台做了一个简单的管理员表,没有做路由级权限控制,默认只有一种“管理员”角色,登录后所有管理页面都可以访问。
如果你需要更细致的权限控制,可以在前端定义好页面和角色的映射关系,登录后根据后台返回的角色信息过滤出可见路由,再用router.addRoutes动态添加。这个设计方案是vue-element-admin的经典思路,网上的资料很多。
5.2 菜品管理页面的表格和表单校验
菜品管理页是典型的表格+弹窗表单组合:页面主体是一个表格,展示所有菜品信息,每行有“编辑”和“下架”按钮;点击“新增”或“编辑”按钮时,弹出一个弹窗表单,填写菜品信息。
表单校验我用了Element UI自带的校验规则,比如:
- 菜名必填,长度2~20个字符
- 价格必填,且必须是大于0的数字,保留两位小数
- 库存必填,且必须是大于等于0的整数
- 分类必须选择
价格和库存的校验要用自定义函数实现,Element UI自带的type: 'number'在某些版本下存在bug,会无法正确校验小数。
图片上传用Element UI的el-upload组件,通过action属性指定后端的上传接口。后端用multer中间件处理文件上传,把图片保存到本地目录,然后返回图片访问URL。要注意的是,Nginx部署时要配置好静态文件的访问路径,不然图片是上传成功了,但前端访问不了。
5.3 订单处理的操作流转设计
后台订单管理页面的核心功能是处理订单状态流转。我设计的是每行订单都显示一个操作按钮,根据当前状态显示可执行的操作:
- 待支付状态:显示“取消订单”
- 已支付状态:显示“开始制作”和“取消订单”
- 制作中状态:显示“开始配送”
- 配送中状态:显示“确认完成”
每次操作前先调用后端接口更新状态,成功后再刷新表格数据。在操作按钮的地方加了二次确认的MessageBox,避免误操作导致订单状态错误。
对于商家来说,订单列表要支持按状态筛选,因为订单量大了以后如果所有订单都混在一起,处理效率会非常低。我用的是el-tabs的方式把不同状态的订单分开展示,待处理的放前面,已完成的折叠起来。
5.4 数据统计与可视化
这个项目我还加了一个简单的数据统计页面,用ECharts展示几个关键指标:
- 近7天订单量折线图
- 菜品销量排行TOP10柱状图
- 订单状态分布饼状图
统计功能的后端接口,用SQL的GROUP BY和DATE_FORMAT函数就能实现。比如近7天订单量,只需要分组聚合:
SELECT DATE(create_time) as date, COUNT(*) as count FROM orders WHERE create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY DATE(create_time) ORDER BY date ASC;这个统计页面对商家的参考价值挺高的,可以看出哪些菜卖得最好,哪些时间段订单最多,方便调整备货和营业时间。
6. 前后端联调与部署上线的常见问题
6.1 跨域问题的一劳永逸解决方案
前后端分离开发时,Vue后台跑在http://localhost:9527,后端跑在http://localhost:3000,两者端口不同,就会遇到跨域问题。
我的方案是后端使用cors中间件全局开启跨域支持:
const cors = require('cors'); app.use(cors());这样配置后,开发环境和生产环境都不需要额外处理跨域。当然这只适用于没有敏感凭证的API场景。如果涉及Cookie会话,就需要配置CORS的credentials选项,并指定白名单域名,要复杂一些。
小程序的请求不存在浏览器跨域问题,因为小程序是原生客户端请求,不受浏览器同源策略限制。但在微信开发者工具里,需要勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”选项,否则会报域名校验错误。
6.2 小程序预览时接口地址怎么配
开发小程序时会遇到一个经典问题:API请求地址怎么配。开发阶段,小程序跑在微信开发者工具里,手机预览时,请求要发到本机电脑的Node.js服务。这时候要注意:
- 如果使用微信开发者工具的模拟器,可以直接用http://localhost:3000访问本机服务
- 如果使用真机预览,手机不能直接访问电脑的localhost,需要换成电脑在局域网中的IP地址
- 同时要保证手机和电脑连接的是同一个WiFi
更麻烦的是,微信小程序正式版本要求所有请求地址必须是HTTPS域名,且域名要备案并在小程序后台配置为合法域名。个人开发阶段用开发者工具的“不校验域名”选项跳过校验没关系,但要上线的话必须解决域名和HTTPS证书的问题。
我建议的路径是:开发阶段用局域网IP,测试阶段用测试域名加自签名证书,上线前再切换正式HTTPS域名。
6.3 两个部署场景的方案参考
部署后端我提供了两种方案:
方案一:本机直接跑。适合个人开发和内网测试。用pm2管理Node.js进程,确保进程崩溃后自动重启,开机自启动可以用pm2-windows-service或pm2 startup命令实现。数据库直接用本机安装的MySQL。
方案二:云服务器部署。我是用一台Linux服务器,nginx做反向代理,把/api路径转发到Node.js的3000端口。MySQL装在同一台服务器上,方便管理。部署脚本可以写成这样的格式:
cd /var/www/delivery-backend git pull origin main npm install --production pm2 restart ecosystem.config.js前端Vue后台打包后,把dist目录下的静态文件放到nginx的web目录,通过nginx直接服务。小程序端不用部署,微信审核通过后会自动发布到微信服务器。
6.4 开发过程中最容易踩的几个坑
这一节我专门整理一下开发过程中反复踩到的坑,按出现频率排序:
第一,npm相关的问题。除了前面说到的PowerShell脚本执行策略问题外,还有npm install时经常遇到权限错误或网络超时。我的经验是先把npm registry切到国内镜像源,安装速度会快很多,也基本不会出现timeout问题:
npm config set registry https://registry.npmmirror.com第二,微信开发者工具提示“开发版小程序已过期”。这个提示通常是因为上一次的预览码过期了,重新点击开发者工具工具栏的“预览”按钮,生成新的预览码扫码即可。如果在手机上还是打不开,关掉小程序后台进程重新进入。
第三,小程序动态设置顶部标题。如果需要在页面运行过程中修改导航栏标题,不能直接修改app.json里的navigationBarTitleText,而要在页面的onLoad或onShow生命周期里调用wx.setNavigationBarTitle接口,动态修改标题。
第四,前后端字段名不一致。我有一个惨痛教训:后端返回的字段是createTime(驼峰命名),前端表单提交时用了create_time(下划线命名),结果数据一直绑定不上。后来在Sequelize配置里加了underscored: true,自动把模型属性转换为下划线风格,这个问题才算解决。前端联调时一定要先确认好字段命名规范,最好有一个统一的对照文档。
第五,Node.js进程挂了。开发阶段我用nodemon,但上线部署后nodemon就不合适了。至少有两次线上服务挂了,是因为没有用pm2守护进程。后来在pm2的配置文件里配置了监听模式,才能解决。
6.5 接口响应速度优化心得
最后聊一下接口性能优化。订餐系统虽然不算高并发场景,但接口响应快慢直接影响用户体验。
我做的优化主要有几点:
- 数据库查询一定要加索引。orders表的user_id、restaurant_id、status字段,order_items表的order_id字段,都要建索引。没有索引的时候,订单量过千就明显变慢,加了索引后查询基本在毫秒级。
- 列表接口用分页。菜品列表和订单列表都实现分页,每页限制20条。后端用Sequelize的limit和offset实现,前端加一个加载更多的按钮。
- 图片走CDN。菜品图如果自己服务器带宽不够,前端加载会特别慢。我是把图片传到对象存储服务上,然后通过CDN加速访问,效果立竿见影。
- 首页数据做缓存。菜单列表和商家列表这种变化不频繁的数据,在后端加一个简单的内存缓存,5分钟刷新一次,能显著减少数据库压力。
7. 写在最后的经验总结
项目做完之后,我最大的感受是:快捷交付的真正技术难点,不在于某一个单独技术的深挖,而在于把三个端口的业务逻辑串起来。数据从什么页面开始、走到哪个接口、改变什么状态,这些链条一旦打通,整个系统就活了。
我个人在Node.js后端加了一个日志中间件,每次有接口请求都打印请求路径、参数、响应耗时,调试时候反馈非常快。也强烈建议你加一个类似的东西。还有一个习惯,就是每次接口写完,先用Postman把边界情况测一遍,比如菜品库存为0时下单、订单状态异常时流转,等接口稳定了再对接前端,这样可以少走很多弯路。
这个项目后续要扩展的话,方向也蛮清晰:一是接入真实微信支付,替换掉模拟支付;二是增加配送员角色和订单派单逻辑;三是做一个基于位置的门店推荐,用wx.getLocation获取用户位置后按距离排序。希望这套思路能帮到你。