news 2026/10/27 16:13:50

基于微信小程序的校园订餐系统:从数据库到接口的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于微信小程序的校园订餐系统:从数据库到接口的完整实现

简介:面向计算机专业毕业设计、课程设计与期末大作业的微信小程序校园订餐系统完整项目包,前端基于微信小程序,后端代码与数据库脚本均已齐备,源码中附有注释,项目经过调试可正常运行,适合新手对照学习,也可直接用作毕设演示或二次开发基础。整个压缩包共4个文件,包含两个项目压缩包、一份SQL数据库脚本和一份部署说明TXT,总大小约28.53MB,其中还含约1.5万字的设计开发文档,方便撰写论文与整理答辩思路。目前已有2026人浏览学习。系统涵盖点餐、订单、结算、管理等常用业务模块,界面清晰,操作流程完整,能够覆盖校园订餐的主要场景。配套部署说明可帮助快速搭建环境,整体是一份高价值可落地的参考方案。

1. 一个校园订餐系统到底要交付什么:小程序、接口与数据库的三件套

毕业季的经典场面:导师把“基于微信小程序的校园订餐系统”这个课题交给你,你打开浏览器搜了一圈,发现模板很多、讲深的不多。这个课题与普通电商系统的差别在于,它的业务边界很清晰——学生登录、看菜单、加购物车、下单、付款、商家接单,几个核心用例就能把微信小程序、后端接口和数据库串成一条能跑通的完整链路。你要交付的不是一个页面 Demo,而是三件套:用户手心的小程序前端、提供数据和订单能力的后端服务、以及一份设计过的 MySQL 数据库脚本。对计算机、软件工程和电商方向的同学来说,这份工程能同时展示小程序生命周期、接口鉴权、数据库事务三类能力,也恰好是答辩时评委最容易追问的地方。谁能把这三点讲透,谁的项目就不只是“能运行”,而是“站得住”。

2. 技术选型与整体架构:为什么是原生小程序 + Express + MySQL

2.1 三组选型对比:原生还是 uni-app,Spring Boot 还是 Express

做小程序订餐系统,第一周最容易浪费时间的不是写业务,而是在“用哪个框架”上反复横跳。我先给出我自己带毕设时最常用的一组选择,再解释为什么。

微信小程序端,推荐原生开发,不推荐 uni-app。这个课题只需要跑在微信一个平台上,没有“一套代码多端发布”的硬需求。原生小程序语法直接、调试工具链完整,而且代码量摊开以后,WXML、WXSS、JS、JSON 四类文件对应的是前端三件套的变体,答辩时讲结构更容易,评委想看渲染逻辑时也能快速定位。uni-app 更适合你要同时出 H5 和 App 的场景,但对校园订餐来说属于引入额外的编译层。

后端接口,推荐 Node.js 的 Express。这个判断可能和很多“毕设必须用 Spring Boot”的惯性思路冲突,但我实际带项目的感受是:Express 可以把“接口层逻辑”保持得很薄,几十行代码就能把用户鉴权、菜品列表、下单接口串起来,学生能看懂每一行,而不像 Spring Boot 项目那样被配置类、依赖注入、Mapper 注解淹没。如果学校硬性要求 Java 技术栈,换成 Spring Boot 也完全能套用同样的数据库设计和接口语义,只是代码骨架厚一些。

数据库,推荐 MySQL,不推荐 SQLite。SQLite 在毕设演示时确实省事,但评委看到“数据库设计”章节时会追问索引、事务、并发扣库存这类问题,SQLite 给不了你太多可讲的空间。MySQL 有完整的事务隔离级别、行锁和 JSON 函数,正好支撑后面要做的订单并发扣库存。

三类选型定下来后,整体架构就是经典的三层:小程序端只做界面和请求;Express 提供 REST 接口并完成业务校验;MySQL 持久化用户、菜品、订单数据。层与层之间用 HTTP + JSON 通信,不引入消息队列,也不做微服务。

2.2 工程骨架与启动顺序:拿到源码包先看这几个目录

一个规范的毕设工程,目录应该让人一眼知道从哪里看起。我习惯把工程分成miniprogram(小程序)、server(后端)、sql(数据库脚本)、docs(教程与说明)四块。下面是一个标准的目录结构示例:

campus-ordering/ ├── miniprogram/ # 微信小程序前端 │ ├── app.js # 小程序入口,含全局会话状态 │ ├── app.json # 页面路由与窗口配置 │ ├── app.wxss # 全局样式 │ ├── pages/ │ │ ├── index/ # 首页菜单列表 │ │ ├── cart/ # 购物车 │ │ ├── order/ # 订单确认与列表 │ │ └── mine/ # 个人中心 │ └── utils/ │ └── request.js # 封装 wx.request,统一带上 token ├── server/ # Node.js 后端 │ ├── app.js # Express 实例与路由挂载 │ ├── routes/ # 按业务拆分的路由 │ ├── controllers/ # 业务处理逻辑 │ ├── db/ # 数据库连接池 │ └── package.json ├── sql/ │ └── campus_ordering.sql # 建库建表脚本 └── docs/ └── 部署教程.md # 环境配置与启动步骤

启动顺序有讲究:先把sql/campus_ordering.sql在 MySQL 里执行,确保表结构存在;再启动server,用node app.js把接口跑起来;最后用微信开发者工具导入miniprogram目录,在工具里配置不校验合法域名。这个顺序不能乱,否则小程序一进来就请求失败,你还会误以为是登录代码写错了。

2.3 数据库连接和全局配置:连接池参数不要照抄默认值

后端server/db/index.js里一般这样创建 MySQL 连接池:

const mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: '127.0.0.1', port: 3306, user: 'root', password: 'your_password', database: 'campus_ordering', waitForConnections: true, connectionLimit: 10, // 连接池上限 queueLimit: 0, // 队列不设上限 dateStrings: true, // 日期以字符串返回,避免时区转换坑 }); module.exports = pool;

逻辑说明:这里用了mysql2/promise而不是老牌的mysql包,因为前者支持 async/await,写接口时不用套一层回调,代码更短,学生也更容易读懂。connectionLimit是连接池同时保持的最大连接数,毕设并发量不大,设 10 到 20 足够,千万别设成几百,否则本地 MySQL 会报“Too many connections”。

参数说明:waitForConnections表示连接池满时请求排队等待而不是直接报错;dateStrings: true值得单独讲一句,Node 的mysql2默认会把 DATETIME 转换成 JavaScript Date 对象,在 JSON 序列化时会变成带时区的时间字符串,经常和前端显示的时间差 8 小时,设置成字符串模式可以避开这个经典暗坑。

在app.js里把 CORS 和 JSON 解析配好,这个后端骨架就能跑了:

const express = require('express'); const app = express(); app.use(express.json()); // 解析 JSON 请求体 app.use(require('cors')()); // 允许小程序端跨域调用 // 统一接口前缀,方便后面按模块挂路由 app.use('/api/user', require('./routes/user')); app.use('/api/dish', require('./routes/dish')); app.use('/api/order', require('./routes/order')); app.listen(3000, () => { console.log('server running at http://127.0.0.1:3000'); });

这段配置里有三个实际项目里很容易漏的点。一是express.json()必须写在路由挂载之前,否则req.body永远拿不到数据。二是本地开发时 CORS 用cors()默认配置就够了,但上到正式服务器后要改成白名单模式,只允许小程序的 request 合法域名来源。三是监听地址要写127.0.0.1而不是localhost,在部分 Windows 环境里localhost会被解析成 IPv6 地址,导致本机测试时接口迟迟连不上。

3. 把用户、菜品、订单变成表:数据库设计与建表 SQL

3.1 从业务流程到 E-R 模型:六张表足够支撑整个系统

校园订餐的核心参与者是学生和商家,围绕“点餐”产生的关系并不复杂。我把整个系统的表拆成六张:学生用户表、菜品表、购物车表、订单表、订单明细表,再加一张商家表。这里没有单独设计管理员表,因为毕业设计阶段“商家端”就可以兼任管理员的角色,少一张表就少一套权限逻辑,答辩时也能把精力集中在点餐主链路。

业务对应关系大致是:一个学生可以拥有多个订单;一个订单包含多个菜品,所以订单和菜品之间通过订单明细表建立多对多关系;购物车表本质上是临时容器,保存“用户-菜品-数量”三元组,下单成功后清空。菜品表中有一个shop_id外键指向商家表,这就是把“哪个窗口出的菜”落到数据层面。

设计表结构之前先想清楚约束:谁在什么状态下能做什么操作。订单状态字段是核心,我用status枚举值表示,0 待支付、1 已支付待接单、2 商家已接单制作中、3 已完成、4 已取消。这组状态从用户视角出发足够清晰,不建议拆出“已退款”,因为没有真实支付流水时,退款逻辑会给毕设增加不必要的复杂度。

3.2 建库建表 SQL:字段类型、索引与外键的设计理由

下面是我常用的建表脚本,压缩掉注释后可以直接在 MySQL 8.0 上执行。这张表结构里每个字段的选择都有具体原因,后续写后端接口时也完全依赖它:

CREATE DATABASE IF NOT EXISTS campus_ordering DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE campus_ordering; CREATE TABLE user ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, openid VARCHAR(64) NOT NULL UNIQUE COMMENT '微信 openid', nickname VARCHAR(50) DEFAULT '' COMMENT '用户昵称', avatar_url VARCHAR(255) DEFAULT '' COMMENT '头像地址', phone VARCHAR(20) DEFAULT '' COMMENT '手机号(可脱敏)', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_phone (phone) ) ENGINE=InnoDB COMMENT='学生用户表'; CREATE TABLE shop ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1营业 0休息', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='商家/窗口表'; CREATE TABLE dish ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, shop_id INT UNSIGNED NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL COMMENT '单价,单位元', stock INT NOT NULL DEFAULT 0 COMMENT '每日库存', image_url VARCHAR(255) DEFAULT '', status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_shop (shop_id), CONSTRAINT fk_dish_shop FOREIGN KEY (shop_id) REFERENCES shop(id) ) ENGINE=InnoDB COMMENT='菜品表'; CREATE TABLE cart ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, dish_id INT UNSIGNED NOT NULL, quantity INT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_dish (user_id, dish_id), KEY idx_dish (dish_id), CONSTRAINT fk_cart_user FOREIGN KEY (user_id) REFERENCES user(id), CONSTRAINT fk_cart_dish FOREIGN KEY (dish_id) REFERENCES dish(id) ) ENGINE=InnoDB COMMENT='购物车表'; CREATE TABLE orders ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '业务订单号', user_id INT UNSIGNED NOT NULL, shop_id INT UNSIGNED NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2制作中 3已完成 4已取消', remark VARCHAR(255) DEFAULT '', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, paid_at DATETIME DEFAULT NULL, KEY idx_user (user_id), KEY idx_shop (shop_id), KEY idx_status (status), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES user(id), CONSTRAINT fk_order_shop FOREIGN KEY (shop_id) REFERENCES shop(id) ) ENGINE=InnoDB COMMENT='订单主表'; CREATE TABLE order_item ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id INT UNSIGNED NOT NULL, dish_id INT UNSIGNED NOT NULL, dish_name VARCHAR(100) NOT NULL COMMENT '下单时的菜品名快照', price DECIMAL(10,2) NOT NULL COMMENT '下单时的单价快照', quantity INT NOT NULL, KEY idx_order (order_id), CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders(id), CONSTRAINT fk_item_dish FOREIGN KEY (dish_id) REFERENCES dish(id) ) ENGINE=InnoDB COMMENT='订单明细表';

字段设计的几个关键理由:openid必须建唯一索引,因为后续INSERT ... ON DUPLICATE KEY UPDATE依赖它做幂等;orders.order_no用业务订单号而不是自增主键对外展示,避免暴露一天的订单量;order_item里存了dish_name和price快照字段,这样菜品改价或下架后,历史订单依然能还原出当时的成交内容;所有价格字段用DECIMAL(10,2),避免 FLOAT 的精度误差。

外键在毕设项目里建议保留,虽然很多生产环境会去掉外键换应用层保证,但答辩时外键能直观体现表间关系,这是加分项。唯一需要留意的是,删表时要按依赖顺序 DROP,否则外键约束会挡路。

3.3 数据库增删改查的核心范式:下单事务与幂等

数据库设计完成后,真正的功夫在写入逻辑。校园订餐系统最需要讲清楚的是“下单”这一段事务。它涉及三步:先校验菜品库存和上架状态;再写订单主表和明细表;最后扣减库存、清空购物车。这三步要么全成功,要么全失败,不能出现订单生成了但库存没扣的情况。

START TRANSACTION; -- 锁定菜品行,防止并发下同时读到同一份库存 SELECT stock FROM dish WHERE id = 1 FOR UPDATE; -- 扣库存,仅当库存足够时更新成功 UPDATE dish SET stock = stock - 1 WHERE id = 1 AND stock >= 1; -- 插入订单主表 INSERT INTO orders (order_no, user_id, shop_id, total_amount, status) VALUES ('202501010001', 1, 1, 25.00, 1); -- 插入订单明细 INSERT INTO order_item (order_id, dish_id, dish_name, price, quantity) VALUES (LAST_INSERT_ID(), 1, '红烧鸡腿饭', 25.00, 1); -- 清空该用户的购物车 DELETE FROM cart WHERE user_id = 1; COMMIT;

逻辑说明:第一句SELECT ... FOR UPDATE是并发控制的关键,它把菜品这行数据加了行锁,后续事务再操作同一行时必须等待。第UPDATE语句里的stock >= 1条件再兜一层,即使两个请求同时发起,也只有一个能更新成功,另一个受影响行数为 0,业务代码据此返回“库存不足”。LAST_INSERT_ID()取到的是当前连接里最新插入的自增 ID,这一步保证了主表和明细表通过同一事务关联。

参数说明:事务隔离级别默认是 REPEATABLE READ,在 MySQL InnoDB 下这个场景不会出问题。真正容易翻车的是事务里混入外部 HTTP 请求,比如先调微信支付接口再执行上述 SQL,这会把事务拖到秒级以上,连接池很容易被打满。正确做法是:先本地落一个“待支付”订单,支付回调成功后再开启事务改状态、扣库存。

4. 小程序端从登录到下单:页面与接口怎么串起来

4.1 微信小程序登录获取手机号:code2session 与降级方案

小程序端的第一关是登录。微信官方推荐的静默登录流程是这样:前端调wx.login()拿到临时 code,把 code 发给后端;后端拿着 code 调用微信的code2session接口,换回 openid 和 session_key;再用 openid 查库里有没有这个用户,没有就自动注册。

// miniprogram/utils/request.js 中封装的登录调用 wx.login({ success: async (res) => { if (res.code) { const loginRes = await wx.request({ url: 'http://127.0.0.1:3000/api/user/login', method: 'POST', data: { code: res.code } }); // 后端返回的 token 存起来,后续请求头带上 wx.setStorageSync('token', loginRes.data.token); } } });

逻辑说明:这里wx.login拿到的 code 有效期只有五分钟且只能用一次。后端拿到 code 后向微信服务器发起请求换 openid,这一步不能在前端做,因为需要 AppSecret,一旦放进小程序代码就等同于泄露。登录成功后后端签发一个自定义 token 存到本地存储,后续每次请求在 header 里带Authorization: Bearer <token>,后端用中间件解析出用户身份。

关于“微信小程序登录获取手机号”,这里要说清楚一个高频误解:wx.getPhoneNumber按钮组件确实能拿到手机号密文,但它要求小程序必须通过企业主体认证,个人开发者无法调用。很多毕设项目是个人主体注册的,因此我一般建议做一个降级方案:登录后允许用户在个人中心手动填写手机号,同时把getPhoneNumber的“快速填写”作为一个增强功能保留。这样演示时依然能展示“获取手机号”的交互,也不会因为主体限制卡住流程。

4.2 从菜单列表到购物车:渲染、加购与角标更新

菜单页是所有订餐系统的主入口。小程序端请求/api/dish/list获取菜品数组,用wx:for渲染卡片列表。每个卡片上有加号按钮,点击时把菜品加入购物车。这个交互如果只做本地数组push,会出现一个经典问题:切到购物车页数据就丢了。因为页面切换后 JS 变量不会被持久化。

Page({ data: { dishList: [], cartMap: {}, // 用 dishId 做 key,方便快速查数量 cartTotal: 0 }, async onShow() { const res = await request.get('/api/dish/list'); this.setData({ dishList: res.data }); }, addToCart(e) { const { id } = e.currentTarget.dataset; const cartMap = { ...this.data.cartMap }; cartMap[id] = (cartMap[id] || 0) + 1; const cartTotal = Object.values(cartMap).reduce((a, b) => a + b, 0); this.setData({ cartMap, cartTotal }); } });

逻辑说明:cartMap以dishId为 key、数量为 value,比数组去重再累加更易维护。addToCart里先浅拷贝cartMap,再修改对应数量,最后重新算总数。这里必须用浅拷贝再setData,不能直接this.data.cartMap[id] = 1后调用setData,因为小程序的数据驱动原理是基于setData的 diff,直接改对象引用后 setData 很可能不触发视图更新,这也是新手最常踩的交互不刷新问题。

参数说明:右上角购物车角标直接用cartTotal字段绑定在 tabBar 图标上。这里有一个体验细节:点击加号只更新角标,不同步跳转购物车页,给用户连续加菜的节奏感;真正下单时再一次性把cartMap传给后端生成订单。若后续要把购物车上报到服务器,就调用/api/cart/sync把cartMap整体存入 cart 表,这里表结构里设置UNIQUE KEY uk_user_dish的目的就是做幂等 upsert。

4.3 创建订单的请求设计:一次性提交还是逐条上传

购物车数据确认后,前端需要一个“提交订单”的接口。很多毕设版本做成了“逐条将购物车项 POST 到后端再生成订单”,这样不仅请求次数多,而且中途掉一个请求就会产生脏数据。更稳的做法是一次性提交整个购物车结构:

Page({ async submitOrder() { const cartMap = this.data.cartMap; const items = Object.keys(cartMap).map(dishId => ({ dish_id: Number(dishId), quantity: cartMap[dishId] })); const res = await request.post('/api/order/create', { shop_id: this.data.currentShopId, items, // 直接传数组 remark: this.data.remark }); if (res.code === 0) { this.setData({ cartMap: {}, cartTotal: 0 }); wx.navigateTo({ url: `/pages/order/detail?id=${res.data.order_id}` }); } } });

逻辑说明:items数组的每一项dish_id和quantity直接对应后端order_item表的两个字段。后端收到后开启事务,先查所有菜品的最新价格(不能信前端传来的 price),再计算total_amount,插入订单主表和明细表。前端在订单成功返回后立即清空cartMap和角标,避免用户重复提交。

参数说明:remark是备注字段,我在订单页放了一个多行输入框,学生可以填“不要辣”“多放米饭”。别小看这个字段,它让答辩时能讲“数据库设计覆盖了真实业务场景”。提交接口必须做防重复处理:后端在orders表用order_no唯一索引兜底,同一个用户短时间内的重复点击会因重复插入失败而拦截。order_no的生成规则我用的是时间戳 + 用户ID + 随机数,避免高并发下撞号。

5. 常见问题与避坑:毕业设计交付前必须自查的 5 个坑

5.1 用户头像昵称获取失败:wx.getUserProfile 返回“微信用户”

现象:小程序调用wx.getUserProfile后拿到的nickName是“微信用户”,avatarUrl是灰色默认头像,而不是自己设置的名字和照片。

原因:微信平台对用户隐私的保护策略不断收紧,从某一年开始,getUserProfile返回的昵称和头像不再真实,而是匿名化的默认值。想拿到真实信息必须引导用户自行填写,或使用头像昵称填写能力。

解决:把用户信息设计成“可编辑的个人资料”而不是“强制读取”。登录时先创建匿名会话,个人中心页给一个“编辑资料”入口,用户手动填昵称、传头像;同时后端user表里预留了nickname和avatar_url字段,走标准的上传接口更新即可。这样做用户数据是洁净的,不会因为接口调整突然失效。

5.2 个人主体做不了微信支付:用“模拟支付”兜住整个链路

现象:小程序里点击“去支付”,跳转wx.requestPayment后直接报错“支付权限未开通”,或者开发者工具提示“当前帐号未认证,无法使用支付”。

原因:微信支付要求小程序必须是企业主体或个体工商户,个人开发者无法开通。很多校园订餐毕设卡在这一步,后续订单状态机完全走不通。

解决:在订单确认页加一个“模拟支付”按钮,逻辑是直接调用后端/api/order/pay接口,把订单状态从“待支付”改为“已支付”。这个方案不是造假,而是把支付抽象成一个可替换的模块:演示时走模拟支付,未来接入真实支付时只需替换控制器里的支付渠道实现。答辩被问到时,明确说“当前使用模拟支付通道,支持后续替换为微信支付”,比硬接一个不可用的支付流程要诚实得多。

5.3 并发下单出现负数库存:没加行锁的 UPDATE 不是原子操作

现象:用两个微信开发者工具窗口同时下单同一个菜品,最终dish.stock变成负数,或者两个订单都创建成功但库存只剩 0 份。

原因:大多数毕设代码的扣库存写法是SELECT stock FROM dish WHERE id = 1,在 Java 或 Node 里判断if (stock > 0)再执行UPDATE dish SET stock = stock - 1。这里查和改是两个独立操作,并发时两个请求都读到 stock=1,于是都执行了扣减。

解决:把扣库存改成单条原子 SQL,或者使用 3.3 节的事务加FOR UPDATE方案。在这里最推荐的后端写法:

const [result] = await pool.execute( 'UPDATE dish SET stock = stock - 1 WHERE id = ? AND stock >= 1', [dishId] ); if (result.affectedRows === 0) { return { code: 1, msg: '库存不足' }; }

逻辑说明:UPDATE语句本身是原子的,stock >= 1条件保证不让库存减到负数。affectedRows为 0 时说明更新没生效,直接返回失败。不显式开事务也能保证单行更新不超卖,因为 InnoDB 对单条 UPDATE 自动加行锁。参数说明:如果一次购买多份,把stock - 1改成stock - ?,并在WHERE里写AND stock >= ?,两个?传同一个 quantity 值,这是最常见的 RBAC 后端实现细节。

5.4 自定义顶部导航栏高度:不同机型按钮位置对不齐

现象:在 iPhone 上调试时胶囊按钮正常,换到安卓真机后,自定义导航栏的标题偏上或偏下,页面整体错位。

原因:微信小程序默认导航栏在不同机型的胶囊位置不同,状态栏高度、胶囊按钮的 top 和 height 都不是固定值。若在app.json里配置了"navigationStyle": "custom",就必须自己计算。

解决:在app.js的 onLaunch 里读取系统信息:

const { statusBarHeight } = wx.getSystemInfoSync(); const rect = wx.getMenuButtonBoundingClientRect(); const navHeight = rect.top + rect.height + 4;

说明:statusBarHeight是状态栏高度,rect是胶囊按钮的位置信息。导航栏总高度 = 胶囊按钮的 top + 胶囊高度 + 上下留白。拿到后存到全局app.globalData.navHeight,页面里用style="padding-top: {{navHeight}}px"动态设置。这段代码能兼容 iPhone X 的刘海屏、灵动岛和安卓细长屏的差异,也是答辩时值得主动展示的适配细节。

5.5 真机预览连不上本地后端:localhost 指向的是手机自己

现象:开发者工具里一切正常,但点击“真机预览”后所有请求都报超时或 ECONNREFUSED。

原因:手机和电脑不在同一网络时,手机上的localhost指向手机本机,而你的 Express 服务跑在电脑上。即使同一 Wi-Fi,也需要用电脑的局域网 IP 而不是 127.0.0.1。

解决:把前端utils/request.js里的 baseURL 从http://127.0.0.1:3000改成电脑的局域网 IP,例如http://192.168.1.101:3000。同时检查 Windows 防火墙是否拦截了 3000 端口入站,Node 进程监听的是0.0.0.0还是仅127.0.0.1。最关键的是,小程序真机调试必须在开发者工具中勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,否则上线前的本地调试会被域名校验拦住。这个小坑嵌入在生产环境部署时同样要注意:上到正式服务器必须把 baseURL 换 HTTPS 域名,并到小程序后台配置 request 合法域名。

6. 从能跑到能答辩:数据准备、演示脚本与源码讲解顺序

6.1 模拟 50 单并发验证订单事务

交付前我习惯做一轮简单的压力验证,不用 JMeter,直接在 Node 里写个并发脚本:

const tasks = Array.from({ length: 50 }, (_, i) => fetch('http://127.0.0.1:3000/api/order/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: 1, items: [{ dish_id: 1, quantity: 1 }] }) }).then(res => res.json()) ); const results = await Promise.all(tasks); const successCount = results.filter(r => r.code === 0).length; console.log(`成功 ${successCount} 单,失败 ${50 - successCount} 单`);

逻辑说明:这个脚本模拟 50 个并发请求同时下单同一个菜品。跑完后检查数据库里dish.stock是否等于初始库存减 50,再统计成功数和失败数。如果库存为负数,说明 5.3 节的原子更新没写对;如果成功数超过库存数,说明事务或条件更新失效。这是验证“数据库事务能否撑住并发扣减”最直接的实验,也是答辩时可以拿出的漂亮测试数据。

6.2 演示数据要像真实食堂:菜品名、价格和图片都要真

很多项目把演示数据做成“菜品1、菜品2”,价格 1 元 2 元,图片用一张占位图反复复用。这样的演示效果会很单薄。我建议把菜品数据做成真实的食堂风格:红烧鸡腿饭 16.8、番茄牛腩面 18.5、小炒肉套餐 19.9,图片从免费图库或自己拍的实拍里选几类风格统一的,再给不同商家分配不同分组。价格不要全整数,小数价格在列表里看起来更像真实点餐应用。菜品库存不要全部设 999,给一两个热门菜设限量为 50 份,演示时可以现场展示“库存不足”的交互,反而比一路顺畅更有说服力。

6.3 答辩讲解顺序:从库表设计开始,再讲接口,最后展示小程序

答辩讲源码时不要从app.js第一行开始念,评委没耐心。我习惯推荐一个三段式顺序:先打开数据库,讲清楚六张表之间的关系,再打开后端项目,现场演示一次完整的下单请求日志,最后切到小程序演示真实操作。具体顺序是:打开 SQL 脚本,讲orders和order_item的主从关系,以及为什么用快照字段;启动后端,用 Postman 或小程序开发者工具的网络面板展示下单请求的请求体、响应体和 SQL 日志;切到小程序页面,从登录到下单走一遍正常流程,再现场把某个菜品库存改成 1,演示库存不足的异常分支。

这个顺序之所以有效,是因为它对应了毕设论文的“需求分析、系统设计、系统实现、系统测试”四个章节,评委每个问题都能落到你刚刚展示过的具体代码里。带着断点调试和网络请求日志做演示,比空口讲“系统做完了”更有说服力。我最想提醒的是:所有演示账号和菜品数据提前一天重置,现场不要做任何更新数据库的操作,包括加菜品、改价格。我曾见过答辩现场为了加一个菜打开 MySQL,写了一句语法错误的 UPDATE,三分钟内让整场演示变成了 Debug 课。这种低级事故一次就能毁掉整个毕设印象,养成“演示只读、不写库”的习惯,是你交付这套源码前最值得记住的一条血泪经验。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 16:13:25

Python与CANN结合:pyasc库提升AI开发效率

1. 项目概述&#xff1a;Python与CANN的化学反应在AI芯片开发领域&#xff0c;CANN&#xff08;Compute Architecture for Neural Networks&#xff09;作为主流的异构计算架构&#xff0c;长期面临着开发门槛高、调试周期长的问题。而pyasc库的出现&#xff0c;就像在底层硬件…

作者头像 李华
网站建设 2026/10/27 16:12:41

URL文件批量下载器实战:从URL清单清洗到并发下载与完整性校验

简介&#xff1a;这是一款面向频繁下载网络资源用户的URL文件批量下载工具&#xff0c;核心用途是将文本中多个URL链接一次性导入并自动下载&#xff0c;适用于图片、文档、音频等资源的批量获取&#xff0c;对内容创作者、数据分析师、网络管理员尤为实用。压缩包共5个文件&am…

作者头像 李华
网站建设 2026/10/27 16:11:55

t3code 深度解析:Electron 与 CLI 融合的 AI 编程调度层

1. 从 t3code 这个标题说起&#xff1a;它到底想解决什么问题第一次看到 t3code 这个名字&#xff0c;我下意识把它拆成了两半&#xff1a;t3 和 code。t3 在开发者圈子里通常指代某种“第三版”或者“轻量级”的命名习惯&#xff0c;而 code 直接点明了它的核心场景——写代码…

作者头像 李华
网站建设 2026/10/27 16:11:52

t3code 设计实践:低代码平台如何生成真正可维护的工程代码

接手这个需求之前&#xff0c;我手上已经跑过几套低代码平台&#xff0c;也写过无数套生成模板的脚本。但真正让我决定把“t3code”这套思路沉淀下来的&#xff0c;是一次很崩溃的线上经历&#xff1a;业务方用一个低代码平台搭了三十多个页面&#xff0c;积压了将近半年的迭代…

作者头像 李华
网站建设 2026/10/27 16:11:51

t3code实战指南:配置驱动的代码管理与自动化工具链

这两年开源工具链更新得特别快&#xff0c;我手里的个人项目也跟着迭代了好几轮。最近整理代码仓库时&#xff0c;发现一个叫t3code的项目被很多人问起&#xff0c;索性把我在实际使用和二次开发中积累的经验完整写出来。这篇文章不仅讲清楚 t3code 是什么、能解决什么问题&…

作者头像 李华
网站建设 2026/10/27 16:08:13

Win10阻止升级Win11的组策略与注册表方法

1. 为什么需要阻止Win10升级到Win11&#xff1f;作为一位从Windows 95时代就开始折腾系统的老用户&#xff0c;我完全理解大家想要停留在Win10的诉求。Win11虽然界面更现代&#xff0c;但强制TPM 2.0、取消任务栏拖放等改动让很多专业用户望而却步。特别是以下三类人群更需要这…

作者头像 李华