简介:基于微信小程序的点餐系统毕业设计源码包,是个人独立完成并获得老师高度认可的高分项目,面向计算机相关专业学生、小程序开发初学者及需要完成期末课程设计或课程大作业的开发者。压缩包内共520个文件,解压后约64.39MB,主要包含png界面图片、js业务逻辑、vue页面组件、html页面、css样式、wxss/wxml小程序结构文件、json配置、svg图标以及less、xml等辅助类型,从项目配置到前端展示、交互逻辑均有覆盖,结构清晰便于定位参考。目前已有527人学习下载。源码经过严格调试可稳定运行,实现了菜品浏览、分类点餐、购物车管理、订单提交等典型功能,并附有设计实现过程中的页面素材和样式文件,既能作为毕业设计的完整参考模板,也可用于学习微信小程序前端开发、组件间通信与整体项目组织方法,具有扎实的实践参考价值。
1. 为什么一份点餐系统源码值得拆开看
微信小程序点餐系统是典型的「全链路练手项目」:它既有小程序端完整的页面交互和本地缓存,又有后端的登录鉴权、数据库事务、订单状态流转。相比只查天气、只展示列表的 demo,点餐系统把「用户登录—浏览菜单—加购—下单—扣库存—订单查询」这条真实业务闭环走完了,恰好覆盖了毕设和课程设计里评委最关注的几个模块。很多人拿到源码后第一反应是「能跑就行」,但真正有提升价值的做法是顺着登录态和订单事务这两条主线逐行读下去,理解每个接口为什么这么设计。下文按登录链路、购物车与库存、部署排错、支付扩展四条线展开,代码可直接对照源码查看。
2. 登录态与会话管理:从 wx.login 到 code2session 的完整链路
点餐系统第一个要解决的问题就是「后端怎么知道这个订单是谁下的」。微信小程序不能像网页那样由前端直接传用户名和密码,而是通过wx.login获取临时凭证code,再由后端拿这个code去微信服务器换openid。理解这条链路是看懂整个项目用户模块的前提。
2.1 登录流程与 code2session 参数解析
小程序端在app.js的onLaunch里调用wx.login,拿到code后请求后端接口,后端再用appid + secret + code请求微信接口https://api.weixin.qq.com/sns/jscode2session。这个过程中要特别注意code是一次性的,用一次就失效,所以前端不能重复发送同一个code;同时session_key只能留在后端使用,不能返回给小程序端,否则会有会话安全问题。
以下是服务端 Node.js 处理登录的常见写法:
// routes/auth.js const axios = require('axios'); const crypto = require('crypto'); const WX_APPID = '你的AppID'; const WX_SECRET = '你的AppSecret'; // 生成自定义登录态 token function generateToken(openid) { return crypto.createHash('sha256') .update(openid + Date.now() + Math.random()) .digest('hex'); } exports.login = async (req, res) => { const { code } = req.body; if (!code) return res.status(400).json({ code: 1, msg: '缺少code' }); try { // code 换 openid 和 session_key const { data } = await axios.get('https://api.weixin.qq.com/sns/jscode2session', { params: { appid: WX_APPID, secret: WX_SECRET, js_code: code, grant_type: 'authorization_code' } }); if (data.errcode) { return res.status(500).json({ code: data.errcode, msg: data.errmsg }); } // openid 是用户唯一标识,session_key 不要下发 const token = generateToken(data.openid); // 将 openid 与 token 关联存入 Redis 或数据库,这里省略 res.json({ code: 0, data: { token, openid: data.openid } }); } catch (err) { res.status(500).json({ code: 1, msg: '登录失败' }); } };这段代码的核心逻辑是:前端传code,后端用axios请求微信接口,微信返回openid和session_key。这里有一个容易被忽略的细节:openid是用户在当前小程序下的唯一标识,同一个微信用户在不同小程序下openid不同,所以它只适合做单小程序的用户主键。后端拿到openid后生成一个自定义token返回给前端,后续请求都带这个token,而不是直接带openid,这样可以避免openid暴露在请求里被伪造。
| 参数 | 来源 | 说明 |
|---|---|---|
appid | 微信公众平台 | 小程序唯一标识,找管理员要或在 mp.weixin.qq.com 查看 |
secret | 微信公众平台 | 小程序密钥,和 appid 配对使用,泄露后可在平台重置 |
js_code | 前端wx.login | 临时登录凭证,有效期 5 分钟,只能用一次 |
grant_type | 固定值 | 必须为authorization_code |
openid | 微信返回 | 用户在当前小程序的唯一 ID |
session_key | 微信返回 | 会话密钥,用于解密手机号等敏感数据,不能下发前端 |
2.2 小程序端请求封装与 token 管理
拿到token后,小程序端需要把它存到wx.setStorageSync,并在每次请求时带上。点餐系统的所有接口(菜品列表、下单、订单查询)都要依赖这个登录态,所以一般会在utils/request.js里统一封装wx.request,避免每个页面重复写header和错误处理。
// utils/request.js const BASE_URL = 'http://localhost:3000/api'; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token'); wx.request({ url: BASE_URL + path, method, data, header: { 'Content-Type': 'application/json', 'Authorization': token ? 'Bearer ' + token : '' }, success(res) { if (res.statusCode === 401) { // token 过期,重新走登录流程 wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); reject(new Error('未登录')); return; } if (res.data.code === 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(new Error(res.data.msg)); } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } module.exports = { get: (p) => request(p), post: (p, d) => request(p, 'POST', d) };这里有两个设计值得借鉴:一是把token统一放在Authorization头里,后端拦截器可以统一校验,而不是每个接口单独解析;二是对401做全局处理,自动清除本地登录态并跳转登录页。很多点餐系统源码在登录态过期时会卡在下单失败,就是因为每个页面各自处理错误,没有走到统一入口。BASE_URL需要根据实际后端地址修改,本地调试用http://localhost:3000,真机预览时要改成局域网 IP 或已备案的 HTTPS 域名。
3. 点餐主流程:从购物车到订单库存的事务落地
登录态解决「用户是谁」,接下来核心业务是「用户点什么、怎么下单」。这一部分也是点餐系统和其他管理系统差异最大的地方:购物车在本地、订单在后端、库存扣减必须保证一致性。
3.1 数据库设计:四张表撑起点餐闭环
点餐系统的数据库设计一般用四张核心表:菜品分类表、菜品表、订单表、订单明细表。分类表和菜品表是 「一对多」 关系,订单表和订单明细表也是 「一对多」 关系,这是电商类系统最常见的关系模型。
CREATE TABLE categories ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort INT DEFAULT 0 ); CREATE TABLE dishes ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, image VARCHAR(255), stock INT DEFAULT 0, sales INT DEFAULT 0, status TINYINT DEFAULT 1, FOREIGN KEY (category_id) REFERENCES categories(id) ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2制作中 3已完成 4已关闭', remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME NULL ); CREATE TABLE order_items ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, dish_id INT NOT NULL, dish_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id) );dish_name冗余存储在order_items里,原因是菜品名称或价格修改后,历史订单仍然需要保留当时的下单快照。很多新手在设计时只存dish_id,订单查询时再去关联菜品表,结果菜品改价后历史订单金额也跟着变了,这在财务上是错误的。这个坑在点餐系统答辩时经常被问到,建议保留冗余字段。
3.2 购物车实现:为什么购物车数据存在本地
小程序点餐系统的购物车一般存在本地Storage,而不是实时写入后端。原因是购物车操作频率高、实时性要求低,用户可能几十秒内反复加减菜品,每次都请求后端会造成不必要的服务器压力。常见结构是wx.setStorageSync('cart', cartData),cartData用对象存储,键为菜品 ID,值为数量。从存储中读取并同步到页面的代码如下:
// 加购逻辑 addToCart(dish) { const cart = wx.getStorageSync('cart') || {}; if (cart[dish.id]) { cart[dish.id].quantity += 1; } else { cart[dish.id] = { dishId: dish.id, name: dish.name, price: dish.price, image: dish.image, quantity: 1 }; } wx.setStorageSync('cart', cart); this.setData({ cartCount: this.getCartCount(), cartTotal: this.getCartTotal() }); },之所以用对象而非数组存储,是因为cart[dish.id]的读写时间复杂度是 O(1),而数组的findIndex是 O(n)。当菜品数量较多时,对象存储配合Object.keys遍历效率更高。这里要注意库存校验不能只在前端做,用户可能通过篡改请求绕过页面限制,后端的下单接口必须重新校验库存和价格。
3.3 下单接口与库存扣减事务
下单是整个系统中对数据一致性要求最高的操作。用户点击「去结算」后,前端把购物车数据(菜品 ID 和数量)发给后端,后端重新从数据库读取菜品价格计算总价,然后开启事务同时写入订单表、订单明细表并扣减库存。后端必须使用事务保证「订单创建成功但库存扣减失败」或「库存扣减了但订单没建成」这两种情况都不会出现。
// routes/order.js const mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: 'localhost', user: 'root', password: '123456', database: 'ordering_system', waitForConnections: true }); async function createOrder(req, res) { const userId = req.user.id; const { items, remark } = req.body; // items: [{dishId, quantity}] // 事务必须从连接池获取单个连接 const conn = await pool.getConnection(); try { await conn.beginTransaction(); let totalAmount = 0; const orderItems = []; for (const item of items) { const [dishes] = await conn.query( 'SELECT id, name, price, stock FROM dishes WHERE id = ? FOR UPDATE', [item.dishId] ); if (!dishes.length) throw new Error('菜品不存在: ' + item.dishId); if (dishes[0].stock < item.quantity) { throw new Error('库存不足: ' + dishes[0].name); } totalAmount += dishes[0].price * item.quantity; orderItems.push([dishes[0].id, dishes[0].name, dishes[0].price, item.quantity]); await conn.query( 'UPDATE dishes SET stock = stock - ?, sales = sales + ? WHERE id = ?', [item.quantity, item.quantity, item.dishId] ); } const orderNo = 'ORD' + Date.now() + Math.floor(Math.random() * 1000); const [orderResult] = await conn.query( 'INSERT INTO orders (order_no, user_id, total_amount, remark) VALUES (?, ?, ?, ?)', [orderNo, userId, totalAmount, remark] ); const orderId = orderResult.insertId; for (const item of orderItems) { await conn.query( 'INSERT INTO order_items (order_id, dish_id, dish_name, price, quantity) VALUES (?, ?, ?, ?, ?)', [orderId, item[0], item[1], item[2], item[3]] ); } await conn.commit(); res.json({ code: 0, data: { orderNo, totalAmount } }); } catch (err) { await conn.rollback(); res.status(500).json({ code: 1, msg: err.message }); } finally { conn.release(); } }这段下单逻辑中有三个关键点值得细看。第一,查询菜品用了SELECT ... FOR UPDATE,这是对菜品行加排他锁,防止两个用户同时下单时都读到「库存还剩 1 份」然后都扣减成功。第二,总价完全由后端根据数据库价格计算,前端传的price字段在后端被忽略,这样即使用户修改请求中的价格也无法低价下单。第三,throw new Error后rollback会回滚整个事务,包括已经执行过的库存扣减和订单插入,保证数据库不会出现半成品数据。
4. 部署到真机前:配置、联调与常见异常排查
很多同学下载源码后在开发者工具里能跑通,一上真机就黑屏或请求失败,问题大多数出在域名配置和网络环境上。点餐系统涉及小程序端、后端服务、数据库三层,任一层配置错了都会连锁报错。
4.1 小程序端部署配置
在微信开发者工具中导入项目时,需要填写自己的AppID,不要用测试号,因为测试号无法调用wx.login换取openid。导入完成后,第一步修改utils/request.js里的BASE_URL,本地调试保持http://localhost:3000,但真机预览时必须改为电脑的局域网 IP,例如http://192.168.1.100:3000。同时需要在详情设置中勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」,否则真机调试时所有请求都会被拦截。
后端 Node.js 服务启动后,建议先单独测试接口是否连通。在浏览器或 Postman 中请求GET http://localhost:3000/api/dishes,能返回菜品列表 JSON 说明后端正常。如果请求失败,用curl -v查看具体错误信息,可能是端口被占用或后端服务还没有启动成功。小程序端和后端联调时最容易混淆的是端口号,点餐系统源码常见后端端口是3000或8080,注意和前端BASE_URL保持一致。
4.2 后端与数据库配置
后端项目一般需要先创建数据库并导入初始化 SQL 文件,然后在配置文件中修改数据库账号密码。以基于mysql2的后端为例,config.js或.env中配置DB_HOST、DB_USER、DB_PASSWORD和DB_NAME四个参数,任何一项不匹配都会导致连接池初始化失败。启动前确认 MySQL 服务已开启,并检查mysql命令行能否正常登录。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
请求返回timeout | 后端没启动或 IP 错误 | 检查node app.js是否运行,浏览器访问接口地址 |
微信接口返回40029 | code被重复使用 | 确认wx.login每次登录只调用一次,检查代码中是否有重试逻辑 |
微信接口返回40125 | secret错误 | 登录微信公众平台,重置AppSecret并同步到后端配置 |
数据库连接ECONNREFUSED | MySQL 未启动 | 执行systemctl status mysql查看服务状态 |
| 下单报「库存不足」 | 测试数据库存为 0 | 在数据库里把dishes.stock改成大于 0 的数字 |
4.3 微信登录常见错误的处理
登录接口是点餐系统排错的重灾区,尤其是40029和40125两个错误码。40029表示code无效,常见原因是前端在onLaunch和页面onLoad中重复调用了wx.login,导致第一次生成的code在第二次调用时已被标记为失效。处理方法是在app.js中用一个 Promise 统一管理登录状态,确保整个小程序生命周期内wx.login只执行一次。另外,微信开发者工具中的「本地缓存」清理后,旧的token仍然存在,而服务端可能已将它删除,此时请求会返回401,前端会跳转登录页。如果真机调试时发现循环跳转登录页,可以先清空小程序缓存再重新编译。
5. 从毕设项目到可运营外卖点餐:支付接入与超时状态机
毕设做到能点餐、能下单、能查订单已经合格,但要放到真实场景里运营,还有两个必须补齐的模块:订单状态自动流转和微信支付。这两个也是面试答辩时最能体现「对业务有完整思考」的点。
5.1 订单状态机与超时自动关闭
当前orders表的status字段有 0 到 4 五个状态,但如果只是在代码里if...else判断,很容易出现状态错乱。更好的做法是画一张状态流转表约束哪些状态可以跳转。前端也根据status渲染不同的操作按钮,避免用户在「已关闭」订单上看到「去支付」按钮。
| 当前状态 | 可流转状态 | 触发条件 |
|---|---|---|
| 0 待支付 | 1 已支付 / 4 已关闭 | 支付回调 / 15 分钟未支付定时关单 |
| 1 已支付 | 2 制作中 | 商家在管理端接单 |
| 2 制作中 | 3 已完成 | 商家出餐/配送完成 |
| 3 已完成 | 无 | 终态,可评价不可修改 |
| 4 已关闭 | 无 | 终态 |
超时关单一般用定时任务实现,常见做法是在下单时写一条带过期时间的记录到 Redis,EXPIRE设置为 900 秒,监听过期事件后把对应订单改为已关闭。如果项目没引入 Redis,也可以用 Node.js 的setTimeout,但服务重启后定时器会丢失,生产环境不推荐。
5.2 微信支付接入的关键处理
点餐系统接微信支付时,大部分人卡在签名和回调验签上。小程序端通过wx.requestPayment拉起收银台,前端拿到的是后端「统一下单」接口返回的timeStamp、nonceStr、package、signType和paySign五个参数。签名算法是 MD5 或 HMAC-SHA256,参与签名的字段需要按照字典序排列并用&拼接,再追加key=商户密钥,最后对拼接字符串做摘要。这块出错时,微信会返回sign error或invalid signature,大多是参数字段名写错或密钥配置不一致。支付成功后,微信会以 POST 方式回调后端接口,开发时必须先对回调参数验签,再修改订单状态并返回{"code": "SUCCESS"},否则微信会反复通知导致订单重复修改。
5.3 体验版预览与流量主开启
项目完成后,在微信开发者工具点击「上传」按钮把代码提交到微信后台,再到 mp.weixin.qq.com 中把上传版本设为体验版。测试人员通过扫码即可真机体验完整流程。注意体验版必须使用 HTTPS 域名且已在小程序后台配置request合法域名,不能关闭域名校验。如果只是个人学习,可以不开通支付,把下单状态改为直接置为已完成,不影响展示。点餐系统这类项目上线后还可以开通流量主,在小程序底部展示广告位,把开发成本赚回来,这也是很多学生开发者第一次「跑通完整商业闭环」的路径。
本文还有配套的精品资源,点击获取