资讯详情

微信小程序点餐系统毕业设计实战:源码+后台+数据库全链路实现

📅 2026/10/22 4:05:27 | 华诺云谱 👁 阅读
微信小程序点餐系统毕业设计实战:源码+后台+数据库全链路实现
简介这份资源是面向高校计算机相关专业学生与小程序开发初学者的微信小程序点餐系统实战项目适用于毕业设计、期末大作业与课程设计等场景。项目包含小程序前端、后台管理系统与数据库脚本三部分代码带有详细注释新手也能对照理解整体业务逻辑与实现思路部署流程相对简单下载后按说明配置即可运行使用。压缩包共210个文件约1.84MB以72个Java后台源码、51张界面与素材图片、16个wxss样式与16个json配置、15个js脚本、13个wxml页面结构为主另含ftl模板、sql建表脚本及少量yml、properties等配置文件覆盖点餐、订单、菜品管理等核心模块。目前已有984人学习下载说明该方案在同类选题中具备一定参考价值。对于需要快速搭建可演示系统、梳理前后端交互流程与数据库设计的读者可借助完整源码与注释理清项目结构作为二次开发或答辩展示的基础。1. 从一份能跑起来的点餐小程序说起为什么“源码后台数据库”才是毕业设计的硬通货很多同学做毕业设计时第一反应是去搜“微信小程序点餐系统”然后找到一堆只有前端页面的演示点两下就没了。真正能拿去答辩、能演示完整业务闭环的必须同时具备三个东西小程序端、后台管理端、数据库。这三者缺一个你的系统就是个半成品。我见过太多“翻车”案例前端页面做得花里胡哨结果后台连菜品数据都改不了数据库表结构一团糟答辩老师一问“订单状态怎么流转的”就卡壳。这个标题里的“实战项目源码后台数据库”核心价值就在于它是一条完整的链路顾客扫码点餐、下单支付、后厨接单、管理员管理菜品和订单。适合谁适合正在做毕业设计、需要一套可运行、可讲解、可扩展的参考实现的同学。接下来我会按实际开发顺序把环境搭建、数据库设计、后端接口、小程序端对接、联调避坑这几个环节拆开讲每一步都给出可复现的命令和代码。2. 环境准备与项目结构把“能跑”作为第一目标2.1 开发工具与运行环境选型做微信小程序点餐系统你至少需要四样东西微信开发者工具、一个后端运行环境、一个数据库、一个接口调试工具。后端我推荐用 Node.js Express 或者 Python Flask原因很简单轻量、上手快、和前端 JSON 交互直接。数据库用 MySQL 8.0 就够了别一上来就上 MongoDB点餐系统的订单、菜品、用户关系是典型的关系型数据用 MySQL 更稳。微信开发者工具去官方下载稳定版即可安装后用小程序的测试号就能开发不需要一开始就注册企业主体。具体版本上Node.js 选 18 LTS 或 20 LTSMySQL 选 8.0.x微信开发者工具选最新稳定版。这些版本组合在我自己的机器和实验室的电脑上都跑通过兼容性没问题。接口调试用 Postman 或者 Apifox用来在写小程序页面之前先把后端接口调通这一步能帮你省掉大量“到底是前端还是后端的问题”的排查时间。2.2 项目目录结构与初始化命令一个清晰的项目结构能让你在答辩时讲清楚“哪部分是前端、哪部分是后端、哪部分是数据库”。我一般会这样组织# 创建项目根目录 mkdir wx-order-system cd wx-order-system # 后端目录 mkdir server cd server npm init -y npm install express mysql2 cors body-parser jsonwebtoken # 小程序端目录在微信开发者工具中创建这里只展示结构 # miniprogram/ # pages/ # index/ # 首页点餐 # order/ # 订单确认 # mine/ # 个人中心 # utils/ # request.js # 封装请求 # app.js # app.json # 数据库脚本目录 mkdir ../database上面这段命令做了三件事初始化后端 Node 项目、安装 Express 和 MySQL 驱动、预留数据库脚本目录。mysql2是 MySQL 的 Node.js 驱动比老的mysql包性能更好支持 Promise。cors用来解决小程序请求后端时的跨域问题虽然小程序本身没有浏览器同源策略但在开发者工具里调试时仍然需要。jsonwebtoken用来生成登录令牌后面用户登录和订单查询都要用。2.3 数据库建表五张核心表撑起整个点餐流程点餐系统的数据库不需要几十张表五张核心表就能跑通用户表、菜品分类表、菜品表、订单表、订单明细表。下面是我常用的建表 SQL你可以直接复制到 MySQL 里执行-- 创建数据库 CREATE DATABASE IF NOT EXISTS wx_order DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE wx_order; -- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL UNIQUE COMMENT 微信用户唯一标识, nickname VARCHAR(64) DEFAULT , avatar VARCHAR(255) DEFAULT , phone VARCHAR(20) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 菜品分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, sort INT DEFAULT 0 ) ENGINEInnoDB; -- 菜品表 CREATE TABLE dish ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, image VARCHAR(255) DEFAULT , description VARCHAR(255) DEFAULT , status TINYINT DEFAULT 1 COMMENT 1上架 0下架, FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB; -- 订单表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, order_no VARCHAR(32) NOT NULL UNIQUE, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已完成 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB; -- 订单明细表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, dish_id INT NOT NULL, dish_name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (dish_id) REFERENCES dish(id) ) ENGINEInnoDB;这里有几个设计点值得注意。user表的openid加了唯一索引因为同一个微信用户只能有一条记录。orders表的order_no是业务订单号用时间戳加随机数生成不要用自增 ID 直接暴露给前端。order_item表里冗余了dish_name和price这是故意的——菜品可能改价或下架但历史订单必须保留当时的价格和名称这是血泪经验答辩时老师很可能问“如果菜品涨价了历史订单金额会变吗”你答“不会因为明细表存了快照”就是加分项。3. 后端接口开发把点餐业务逻辑写成可调试的 API3.1 数据库连接池与 Express 入口配置后端的第一件事是连上数据库并且用连接池而不是单连接。单连接在并发请求下会直接崩掉连接池能复用连接、控制并发数。下面是我常用的db.js和app.js// server/db.js const mysql require(mysql2); const pool mysql.createPool({ host: localhost, user: root, password: your_password, // 换成你自己的密码 database: wx_order, waitForConnections: true, connectionLimit: 10, // 连接池最大连接数 queueLimit: 0 }); module.exports pool.promise();// server/app.js const express require(express); const cors require(cors); const bodyParser require(body-parser); const db require(./db); const app express(); app.use(cors()); app.use(bodyParser.json()); // 测试数据库连接 app.get(/api/ping, async (req, res) { try { const [rows] await db.query(SELECT 1); res.json({ code: 0, msg: pong }); } catch (err) { res.status(500).json({ code: 500, msg: err.message }); } }); app.listen(3000, () { console.log(Server running on http://localhost:3000); });connectionLimit: 10是经验值毕业设计演示场景下 10 个连接足够设太大反而浪费资源。queueLimit: 0表示不限制排队请求数但实际生产环境要设上限。/api/ping这个接口看起来没用但它是我调试时的“后悔药”——每次改完配置先访问它能快速确认数据库连没连上而不是等到业务接口报错才去排查。3.2 菜品列表与分类接口小程序首页的数据来源小程序首页需要按分类展示菜品所以后端要提供两个接口获取分类列表、根据分类获取菜品。代码如下// server/routes/dish.js const express require(express); const router express.Router(); const db require(../db); // 获取所有分类 router.get(/categories, async (req, res) { const [rows] await db.query(SELECT * FROM category ORDER BY sort ASC); res.json({ code: 0, data: rows }); }); // 根据分类获取菜品status1 表示上架 router.get(/dishes, async (req, res) { const { category_id } req.query; let sql SELECT * FROM dish WHERE status 1; const params []; if (category_id) { sql AND category_id ?; params.push(category_id); } const [rows] await db.query(sql, params); res.json({ code: 0, data: rows }); }); module.exports router;在app.js里挂载路由app.use(/api, require(./routes/dish));。这样小程序端请求/api/categories就能拿到分类请求/api/dishes?category_id1就能拿到对应菜品。参数category_id是可选的不传就返回所有上架菜品。注意status 1这个条件必须加否则下架菜品也会出现在小程序里这是常见的翻车点。3.3 下单接口与订单号生成事务保证数据一致性下单是整个系统最核心的接口它要同时写orders表和order_item表必须用事务。如果只写订单表不写明细或者明细写了一半失败数据就乱了。下面是我常用的下单逻辑// server/routes/order.js const express require(express); const router express.Router(); const db require(../db); // 生成订单号时间戳 4位随机数 function genOrderNo() { const ts Date.now().toString(); const rand Math.floor(Math.random() * 9000 1000); return ts rand; } router.post(/order/create, async (req, res) { const { user_id, items } req.body; // items 格式[{ dish_id, quantity }] if (!items || items.length 0) { return res.json({ code: 1, msg: 购物车为空 }); } const conn await db.getConnection(); try { await conn.beginTransaction(); // 1. 查询菜品真实价格防止前端篡改 const dishIds items.map(i i.dish_id); const [dishes] await conn.query( SELECT id, name, price FROM dish WHERE id IN (?) AND status 1, [dishIds] ); const dishMap {}; dishes.forEach(d { dishMap[d.id] d; }); // 2. 计算总价并组装明细 let total 0; const orderItems []; for (const item of items) { const dish dishMap[item.dish_id]; if (!dish) throw new Error(菜品 ${item.dish_id} 不存在或已下架); total dish.price * item.quantity; orderItems.push([dish.id, dish.name, dish.price, item.quantity]); } // 3. 写入订单表 const orderNo genOrderNo(); const [orderResult] await conn.query( INSERT INTO orders (user_id, order_no, total_price, status) VALUES (?, ?, ?, 0), [user_id, orderNo, total] ); const orderId orderResult.insertId; // 4. 批量写入订单明细 const values orderItems.map(oi [orderId, ...oi]); await conn.query( INSERT INTO order_item (order_id, dish_id, dish_name, price, quantity) VALUES ?, [values] ); await conn.commit(); res.json({ code: 0, data: { order_id: orderId, order_no: orderNo, total_price: total } }); } catch (err) { await conn.rollback(); res.json({ code: 1, msg: err.message }); } finally { conn.release(); } }); module.exports router;这段代码的关键点有三个。第一价格从数据库查不信任前端传过来的价格否则用户可以把 30 块的菜改成 3 块。第二用beginTransaction和commit/rollback保证订单和明细要么都成功要么都失败。第三conn.release()放在finally里确保连接一定归还连接池否则连接池很快会被耗尽。订单号生成用时间戳加随机数毕业设计场景够用生产环境建议用雪花算法。4. 小程序端对接从请求封装到页面渲染4.1 封装 request 工具与全局配置小程序端不要在每个页面里直接写wx.request那样代码重复且难维护。我一般会封装一个request.js// miniprogram/utils/request.js const BASE_URL http://localhost:3000/api; function request(options) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json }, success: (res) { if (res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };BASE_URL在开发者工具里可以写localhost但真机调试时必须换成电脑的局域网 IP比如http://192.168.1.100:3000/api并且手机和电脑要在同一个 WiFi 下。这是新手最容易卡住的地方很多人真机预览时请求失败就是因为还在用localhost。4.2 首页点餐页面分类切换与购物车逻辑首页需要展示分类 tab 和对应菜品点击“”加入购物车。核心逻辑是维护一个购物车对象键是菜品 ID值是数量。代码片段如下// miniprogram/pages/index/index.js const { request } require(../../utils/request); Page({ data: { categories: [], dishes: [], currentCategory: 0, cart: {}, // { dishId: quantity } totalPrice: 0 }, onLoad() { this.loadCategories(); }, async loadCategories() { const categories await request({ url: /categories }); this.setData({ categories }); if (categories.length 0) { this.loadDishes(categories[0].id); } }, async loadDishes(categoryId) { const dishes await request({ url: /dishes?category_id${categoryId} }); this.setData({ dishes, currentCategory: categoryId }); }, addToCart(e) { const dish e.currentTarget.dataset.dish; const cart this.data.cart; cart[dish.id] (cart[dish.id] || 0) 1; this.setData({ cart }); this.calcTotal(); }, calcTotal() { let total 0; this.data.dishes.forEach(d { if (this.data.cart[d.id]) { total d.price * this.data.cart[d.id]; } }); this.setData({ totalPrice: total.toFixed(2) }); } });这里cart用对象而不是数组查找和更新都是 O(1)。calcTotal每次重新计算总价避免累加误差。实际项目中购物车数据还会存到wx.setStorageSync防止页面刷新丢失但毕业设计演示时内存态就够用。4.3 订单提交与支付模拟把流程跑通比真支付更重要毕业设计不要求真的接入微信支付那需要企业资质但订单流程必须完整。提交订单时把购物车转成后端需要的格式// miniprogram/pages/order/order.js const { request } require(../../utils/request); Page({ data: { cartItems: [], totalPrice: 0 }, onLoad() { const cart wx.getStorageSync(cart) || {}; const dishes wx.getStorageSync(dishes) || []; const cartItems dishes .filter(d cart[d.id]) .map(d ({ dish_id: d.id, name: d.name, price: d.price, quantity: cart[d.id] })); const totalPrice cartItems.reduce((s, i) s i.price * i.quantity, 0).toFixed(2); this.setData({ cartItems, totalPrice }); }, async submitOrder() { const items this.data.cartItems.map(i ({ dish_id: i.dish_id, quantity: i.quantity })); const res await request({ url: /order/create, method: POST, data: { user_id: 1, items } // 演示时固定 user_id1 }); wx.showToast({ title: 下单成功, icon: success }); wx.removeStorageSync(cart); // 跳转到订单详情或订单列表 wx.navigateTo({ url: /pages/order/detail?id${res.order_id} }); } });user_id在真实项目里从登录态取演示时固定为 1 即可。下单成功后清空本地购物车并跳转到订单详情页。支付环节可以用一个“模拟支付”按钮点击后调用后端接口把订单状态从 0 改成 1这样整个“待支付→已支付”的流转就能演示了。5. 避坑与排查那些让我熬夜的常见问题5.1 小程序请求后端失败报“不在以下 request 合法域名列表中”现象开发者工具里请求正常真机预览时所有接口都失败控制台提示域名不合法。原因小程序真机环境要求请求的域名必须在小程序后台配置为合法域名且必须是 HTTPS。毕业设计阶段没有备案域名解决办法是在开发者工具中勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”然后真机预览时打开调试模式。注意这个选项只在开发版和体验版有效正式版必须配置合法域名。5.2 数据库连接报“Too many connections”现象后端运行一段时间后所有接口都报错重启后又正常。原因每次请求都创建了新连接没有释放。解决用连接池并且确保每个getConnection都有对应的release。我习惯把release放在finally块里这样即使业务代码抛异常连接也能归还。另外检查connectionLimit是否设得太小演示场景 10 就够但如果有并发压测需求可以调到 20。5.3 订单金额和前端显示不一致现象前端算出来 58 块后端返回 57.99 或 58.01。原因JavaScript 浮点数精度问题0.1 0.2 ! 0.3。解决金额在数据库用DECIMAL(10,2)后端计算时用整数分做单位或者用toFixed(2)后再转数字。更稳妥的做法是后端返回分为单位的整数前端展示时除以 100。我在order/create里直接返回total_price保留两位小数前端不再做二次计算避免两边不一致。5.4 菜品图片不显示控制台报 403现象小程序里菜品图片是空白开发者工具 Network 面板显示 403。原因图片存在本地服务器但小程序请求图片时没有携带正确的 referer 或者服务器配置了防盗链。解决毕业设计阶段把图片放在后端public目录下用express.static托管并且不要开防盗链。如果图片放在云存储检查存储桶的权限是否为“公有读”。另外图片 URL 要用完整路径不要用相对路径。5.5 订单状态流转混乱取消后还能支付现象用户取消订单后再点支付按钮订单状态又变成已支付。原因后端更新订单状态时没有校验当前状态直接UPDATE orders SET status 1 WHERE id ?。解决更新时加条件比如UPDATE orders SET status 1 WHERE id ? AND status 0这样只有待支付的订单才能改成已支付。取消订单同理只有待支付状态才能取消。这个细节在答辩时经常被问到加上条件判断能体现你对业务状态机的理解。6. 进阶技巧让毕业设计从“能跑”到“能讲”6.1 用视图简化订单列表查询后台管理端需要展示订单列表包含用户昵称、订单号、金额、状态、下单时间。如果每次都在代码里拼 SQL容易出错。我一般会建一个视图CREATE VIEW v_order_list AS SELECT o.id, o.order_no, o.total_price, o.status, o.create_time, u.nickname, u.phone FROM orders o LEFT JOIN user u ON o.user_id u.id ORDER BY o.create_time DESC;然后后端直接SELECT * FROM v_order_list代码干净答辩时还能讲“我用视图做了查询优化”。注意视图不存储数据每次查询都是实时的所以性能取决于底层表的索引。orders.user_id和orders.create_time建议加索引。6.2 给关键接口加一个简单的日志中间件调试阶段最怕接口报错但不知道入参是什么。加一个日志中间件把请求方法、路径、入参、耗时打印出来// server/middleware/logger.js module.exports (req, res, next) { const start Date.now(); const { method, url, body, query } req; res.on(finish, () { const cost Date.now() - start; console.log([${new Date().toISOString()}] ${method} ${url} ${cost}ms); if (Object.keys(body).length) console.log( body:, JSON.stringify(body)); if (Object.keys(query).length) console.log( query:, JSON.stringify(query)); }); next(); };在app.js里app.use(require(./middleware/logger));即可。这个中间件不依赖任何第三方库输出到控制台排查问题时一眼就能看到是哪个请求、传了什么参数、花了多少时间。我习惯在联调阶段一直开着上线前再关掉。6.3 用环境变量管理数据库密码和端口不要把数据库密码硬编码在db.js里否则代码发给别人或者上传到代码托管平台就泄露了。用dotenv管理npm install dotenv// server/db.js require(dotenv).config(); const mysql require(mysql2); const pool mysql.createPool({ host: process.env.DB_HOST || localhost, user: process.env.DB_USER || root, password: process.env.DB_PASSWORD, database: process.env.DB_NAME || wx_order, connectionLimit: 10 }); module.exports pool.promise();然后在server目录下建.env文件DB_HOSTlocalhost DB_USERroot DB_PASSWORD你的密码 DB_NAMEwx_order PORT3000.env要加到.gitignore里不要提交。答辩演示时如果老师问“数据库密码怎么管理的”你可以说“用环境变量隔离代码里不出现明文密码”这是一个很实在的加分点。6.4 一个我反复用的验证习惯每次改完数据库表结构或者后端接口我不会直接去小程序里点而是先用 Postman 或者 curl 调一遍。比如验证下单接口curl -X POST http://localhost:3000/api/order/create \ -H Content-Type: application/json \ -d {user_id:1,items:[{dish_id:1,quantity:2},{dish_id:2,quantity:1}]}返回{code:0,data:{order_id:1,order_no:...,total_price:...}}就说明后端没问题再去调小程序页面。这个习惯帮我省了很多“到底是前端还是后端”的扯皮时间。另外每次改完表结构我会把建表 SQL 重新整理到一个init.sql文件里答辩前直接执行一遍就能重建整个数据库不用手动改来改去。希望这些经验能帮到你少走点我当年走过的弯路。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑