
简介面向高校毕业设计、课程设计与期末大作业场景这份基于微信小程序的校园订餐系统项目包适合具备基础编程知识、需要快速完成可演示项目的学生。系统前后端代码齐备包含用户点餐、订单管理、餐品展示与后台管理等核心模块代码附有注释运行逻辑清晰新手也能快速上手项目经过严格调试可稳定运行。资源包共4个文件、28.53MB包含两个zip压缩包、一份sql数据库脚本和一份txt部署说明压缩包内含项目源码与设计开发文档论文部分约1.5万字数据库脚本可直接导入部署说明列出环境配置与启动步骤便于本地复现。已有2026人浏览学习适合作为课程设计或毕业设计的完整参考方案。此外还附有软件工具与项目说明能帮助使用者从零搭建微信小程序开发环境直接借鉴系统设计与实现思路。1. 校园订餐小程序毕设包先搞清它解决什么问题再决定要不要动手每年三到五月都会有一批人拿到这样一个压缩包基于微信小程序的校园订餐系统的设计与开发附带数据库脚本、完整源码和一份教程。它的典型场景是毕业设计——学生端选餐下单、商家端接单出餐、管理端看数据外加一整套 MySQL 表结构和后台接口。如果你正在找毕设方向或者刚拿到这类包不知道从哪下手这篇笔记就把拆包、跑通、改深度这三件事讲透。这个标题里真正值钱的不是小程序页面而是订单状态机和数据库表设计这两块也是答辩时老师最爱追问的地方。2. 拆业务再碰代码订餐系统的角色、流程与数据库表设计2.1 三种角色和一条订单状态机做数据库设计前先画一张图校园订餐和普通电商外卖的差异在于场景封闭用户是校内学生商家是食堂或校内商户配送通常不是骑手抢单而是「学生到窗口取餐」或「商家集中配送」。这个差异直接决定了订单状态机的设计也决定了数据库表里哪些字段该留、哪些字段可以砍掉。先把角色拆清楚。学生端负责浏览菜品、加入购物车、下单、支付毕设里常见的是模拟支付、查看订单状态商家端负责接单、制作、出餐、更新订单进度平台管理员负责商家入驻审核、菜品上下架、查看统计报表。三端共用同一套后端接口只是权限维度不同数据库设计时不需要拆三套库一张用户表加一个 role 字段就够。订单状态机是这张图里最核心的一条线。常见状态流是待支付 → 已支付待接单 → 制作中 → 待取餐 → 已完成此外还有已取消和退款中。对学生来说最关心的状态是「我下单了商家到底看到没有」对商家来说最关心的是「这个单子该不该我做」。这些状态直接映射到订单表里的 status 字段后端所有接口都在围绕这个字段做流转所以建表之前先把这个状态图画清楚比先写页面重要得多。2.2 核心表怎么建用户、菜品、购物车、订单与订单项一套标准的校园订餐数据库最少需要六张表用户表、商家表、菜品表、购物车表、订单表、订单项表。其中订单表加订单项表的组合是必考内容几乎所有答辩老师都会问「为什么订单要有主表和明细表两张不能一张搞定」。先看用户表和菜品表CREATE TABLE user ( id int NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 微信openid, nickname varchar(64) DEFAULT COMMENT 昵称, phone varchar(20) DEFAULT COMMENT 手机号, role tinyint NOT NULL DEFAULT 0 COMMENT 0学生 1商家 2管理员, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE dish ( id int NOT NULL AUTO_INCREMENT, merchant_id int NOT NULL COMMENT 所属商家, name varchar(64) NOT NULL COMMENT 菜品名, price decimal(10,2) NOT NULL COMMENT 单价, image_url varchar(255) DEFAULT COMMENT 图片, stock int NOT NULL DEFAULT 0 COMMENT 当日库存, status tinyint NOT NULL DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id), KEY idx_merchant (merchant_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品表;openid 必须加唯一索引这是小程序登录体系的基础。金额用 decimal(10,2) 而不是 floatfloat 在累加和比较时会出现精度误差这在订单金额上是不能忍的。菜品表单独留了 stock 字段目的是支持「当日库存」这种简单的限售逻辑避免学生下单后商家才发现没货。订单主表和订单项表是典型的父子表结构CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id int NOT NULL, merchant_id int NOT NULL, total_amount decimal(10,2) NOT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2制作中 3待取餐 4已完成 5已取消, remark varchar(255) DEFAULT COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_merchant_status (merchant_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; CREATE TABLE order_item ( id bigint NOT NULL AUTO_INCREMENT, order_id bigint NOT NULL, dish_id int NOT NULL, dish_name varchar(64) NOT NULL COMMENT 下单时的菜品名快照, price decimal(10,2) NOT NULL COMMENT 下单时单价快照, quantity int NOT NULL DEFAULT 1, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;明细表里有两个字段值得注意dish_name 和 price 都叫「快照」。菜品改价或改名后历史订单仍然要能看到当时买的是什么、付了多少钱所以不能在下单时只存一个 dish_id 然后去菜品表里关联查——那查出来的可能是改价之后的新价格。这套「主表存订单状态明细表存商品快照」的模式在答辩时可以主动讲出来是很加分的细节。2.3 技术栈选型Spring Boot MySQL 原生小程序为什么是毕设主流标题里明确写了「数据库」说明这个毕设的评分点包含数据库设计。这个前提下技术栈的选择会影响你后面几个月的劳动量。市面这类项目最常见的组合是 Spring Boot MyBatis MySQL 原生微信小程序其次有 Node.js Express MySQL、Python Flask/Django MySQL、以及微信云开发。我一般会优先推荐 Spring Boot MySQL原因很现实一是网上资料最全遇到问题搜索成本低二是导师认可度高绝大多数高校的毕设评分标准里Spring Boot 属于「主流企业级框架」答辩时不会在技术选型上被挑战三是部署简单一个 jar 包加一行 java -jar 就能跑不会因为环境问题在答辩现场翻车。微信云开发则要谨慎。云开发的亮点是快不用自己搭后端数据库直接调用云函数适合原型展示。但问题是它把 MySQL 表设计这个环节完全抹掉了而标题里「数据库」大概率是评分项你总不能答辩时说「数据库在微信那边托管着我主要写前端」。如果选题要求必须包含数据库设计和 SQL 实现云开发会吃亏。如果导师允许那另说。3. 从微信登录到下单的核心链路接口设计与关键代码3.1 微信登录wx.login 换 openid为什么还要自建登录态小程序端登录和传统网页登录最大的不同是前端拿不到用户身份只能通过 wx.login 拿到一个临时的 code把这个 code 发给后端由后端拿着 appid 和 secret 去微信服务器换 openid 和 session_key。这个 code 是一次性的而且有效期很短通常几分钟。后端拿到 openid 后不要每次都去调微信接口正确做法是拿 openid 去查本地用户表查到就发一个自己的 token查不到就先注册再发 token。小程序端登录代码// utils/login.js function wxLogin() { return new Promise((resolve, reject) { wx.login({ success: async (res) { if (!res.code) { reject(new Error(登录失败未拿到 code)); return; } try { const loginRes await new Promise((resolve2, reject2) { wx.request({ url: http://localhost:8080/api/login, method: POST, data: { code: res.code }, success: resolve2, fail: reject2 }); }); const { token, userInfo } loginRes.data.data; wx.setStorageSync(token, token); wx.setStorageSync(userInfo, userInfo); resolve(loginRes.data.data); } catch (e) { reject(new Error(后端登录接口调用失败)); } }, fail: () reject(new Error(wx.login 调用失败)) }); }); }调 wx.request 时我用的是 success 回调套 Promise 的写法而不是直接 async/await 包 wx.request原因是部分目标基础库版本下 wx.request 的 Promise 行为不完全一致回调写法兼容性最稳。前端拿到 token 后存进本地缓存后续所有请求在 header 里带 Authorization 字段后端用拦截器统一校验不用每个接口都重复写登录判断。后端对应接口RestController public class LoginController { Autowired private UserService userService; PostMapping(/api/login) public Result login(RequestBody MapString, String params) { String code params.get(code); // 用 code 换 openid注意 code 只能使用一次 String openid wechatService.code2Session(code); if (openid null) { return Result.error(登录凭证无效); } User user userService.findByOpenid(openid); if (user null) { user userService.register(openid); } String token tokenService.generateToken(user.getId()); return Result.success(Map.of(token, token, userInfo, user)); } }这里有一个必须注意的安全点session_key 绝对不能返回给前端也不能存在前端缓存里。session_key 是用来解密手机号等敏感信息的钥匙如果下发到前端等于把钥匙交给了别人。后端拿到 session_key 后只在需要时用一下用完即弃。这也是答辩时老师常问的「登录安全」的考点之一。3.2 购物车结算与下单事务里的四步操作下单接口是整个系统里最容易出 bug 的地方因为涉及多张表的写入生成订单主表、批量生成订单明细、扣减库存、清空购物车。这四步必须在一个事务里完成任何一步失败都要全部回滚否则会出现「订单建了但明细没了」或者「钱扣了库存没减」这类问题。Controller 层只做参数接收业务逻辑放在 Service 里用 Transactional 标注Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private OrderItemMapper orderItemMapper; Autowired private DishMapper dishMapper; Autowired private CartMapper cartMapper; Transactional(rollbackFor Exception.class) public OrderVO createOrder(Long userId, Long merchantId, ListCartDTO items, String remark) { // 第一步计算总价同时校验菜品是否存在且上架 BigDecimal total BigDecimal.ZERO; for (CartDTO item : items) { Dish dish dishMapper.findById(item.getDishId()); if (dish null || dish.getStatus() ! 1) { throw new BizException(菜品已下架: item.getDishId()); } if (dish.getStock() item.getQuantity()) { throw new BizException(库存不足: dish.getName()); } total total.add(dish.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 第二步生成订单号并插入主表 String orderNo generateOrderNo(); Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setMerchantId(merchantId); order.setTotalAmount(total); order.setStatus(0); order.setRemark(remark); orderMapper.insert(order); // 第三步插入明细表 for (CartDTO item : items) { Dish dish dishMapper.findById(item.getDishId()); OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setDishId(dish.getId()); orderItem.setDishName(dish.getName()); orderItem.setPrice(dish.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); } // 第四步扣减库存并清空购物车 for (CartDTO item : items) { dishMapper.reduceStock(item.getDishId(), item.getQuantity()); } cartMapper.clearByUserAndMerchant(userId, merchantId); // 查询完整订单返回前端 return orderMapper.findDetail(order.getId()); } }订单创建里的一个关键操作是库存扣减。代码里我写的是 dishMapper.reduceStock(dishId, quantity)对应 SQL 是UPDATE dish SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}而不是先 SELECT 再在 Java 里计算。原因很简单并发场景下两条线程同时读到 stock1各自算出 0最后都执行 UPDATE 会超卖。把判断条件写进 UPDATE 的 WHERE 里让数据库保证原子性这是防超卖最常见的做法。支付环节在毕设里通常没有真实微信支付。原因很现实个人主体的小程序申请不了微信支付商户号需要企业资质。所以这类项目里最常见的方案是做一个「模拟支付」按钮前端弹出确认框后端直接把订单状态从 0 更新到 1并写入 pay_time。你可以在演示时说「这里接微信支付的话只需要替换支付接口的调用」答辩老师普遍接受这个说法。3.3 商家端接单与状态流转更新订单别怕 WHERE 条件订单状态从学生下单到取餐每一步流转都是后端接口在更新 orders.status 字段。最常见也最容易错的写法是先查订单在 Java 里判断当前状态是否符合预期然后 UPDATE。// 商家接单仅当订单处于「已支付」状态时才能接单 Transactional(rollbackFor Exception.class) public void acceptOrder(Long merchantId, Long orderId) { int rows orderMapper.updateStatusIfMatch( orderId, merchantId, OrderStatus.PAID, // 期望当前状态 OrderStatus.ACCEPTED // 目标状态 ); if (rows 0) { throw new BizException(订单状态已变化请刷新后重试); } }对应的 MyBatis SQLUPDATE orders SET status #{targetStatus} WHERE id #{orderId} AND merchant_id #{merchantId} AND status #{expectStatus}这段 SQL 的本质是把「状态校验」和「状态更新」合并成一条原子语句。两个学生同时操作同一个订单的场景不多但商家端多个窗口同时打开后台、或者前端重复点击接单按钮时不带 WHERE status 条件的更新就会出问题——比如商家的两个窗口都点了「接单」第一次更新成功第二次本来应该提示「订单已处理」结果却把已经进入制作中的订单又往后推了一步。加了条件更新后第二次执行 rows0直接给前端返回「订单状态已变化」业务上才是正确的。状态流转的每个节点都建议用同一个模式期望状态 目标状态两个参数写一个通用方法。不要每个接口手写一套 if else状态多了以后会乱成一团。4. 把环境跑起来从建库到小程序开发者工具的配置4.1 导入数据库与启动后端一条命令的事卡住多在配置拿到这类毕设包后第一件事不是打开小程序开发者工具而是先把数据库导进去、把后端跑起来。这类包常见的目录结构是database/ 放 SQL 脚本server/ 放后端工程miniprogram/ 放小程序前端doc/ 放说明文档。先用文本编辑器打开 server 下的配置文件确认数据库连接信息再动手。MySQL 导入脚本用命令行最简单不要打开图形工具去手动执行一大段文本mysql -u root -p database/order_system.sql执行完用mysql -u root -p -e use campus_order; show tables;查一下表是否齐全。常见的翻车点是 SQL 脚本里有创建数据库的语句而你直接用了 root 之外的账号权限不够导致建库失败。这时候要么换 root 执行要么手动先建库再导入。后端启动前先改配置文件# server/src/main/resources/application.yml server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_order?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 5MBJava 后端如果是 Maven 工程cd server mvn spring-boot:run如果包里有打包好的 jar 包java -jar target/order-system.jar。启动后看到 Tomcat started on port 8080 字样说明后端起来了。用浏览器或 curl 访问一下健康检查接口确认接口通curl http://localhost:8080/api/health我一般会在这时候把配置里的端口、数据库密码、支付回调地址全部改成自己的避免拿着默认配置跑去问别人「为什么连不上」。改配置时留意时区参数mysql 8 的驱动对 serverTimezone 要求很严格传错会直接报连接异常。4.2 小程序端连接本地后端appid、request 域名与开发者工具后端起来后用微信开发者工具导入 miniprogram 目录。这里有两个设置直接影响能不能跑通一是 AppID二是 request 合法域名。如果你没有注册小程序可以先在开发者工具里选择「测试号」页面基本功能能看但 wx.login 走不了真实流程因为测试号没有真实的 appid 和 secret。这类毕设包一般在 doc 目录里有申请说明需要你自己去微信公众平台注册一个个人小程序拿到 appid 填入 project.config.json 或开发者工具中。登录接口的 appid 和 secret 同时要填到后端配置里前后端必须一致。本地开发还有最坑的一关小程序正式环境中 wx.request 请求的域名必须是备案过的 https 域名并且要在小程序后台配置到 request 合法域名里。本地开发连的是 http://localhost:8080如果不做任何处理请求直接失败。解决方式有两种开发阶段在开发者工具右上角详情里勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」这样可以连本地接口等到真正部署时再把后端放到带 https 域名的服务器上并在小程序后台配置合法域名。4.3 教程没写的一步编译失败先看这两个日志大多数毕设包自带的教程会写到「运行步骤」为止但实际跑起来会遇到教程没覆盖的问题。第一次在小程序开发者工具里编译如果页面白屏或接口报错我先看两处一是开发者工具 Network 面板里请求的状态码二是后端控制台输出的异常堆栈。前后端分离的项目前端报错是结果后端控制台才是原因。比如前端报 500后端日志大概率会告诉你 SQL 语法错误、字段不存在或者空指针。空指针在毕设代码里是重灾区常见原因是查询结果返回 null 后没判断直接取属性。定位方法很简单后端日志里搜索 Exception 关键字找到第一行自己的业务代码往上翻三行基本就是出错的那行调用。如果后端依赖 Maven 下载失败优先检查本机 Maven 仓库配的镜像源。国内网络环境下访问 Maven 中央仓库经常超时改一下 settings.xml 的 mirrors 指向阿里云镜像仓库依赖下载速度立刻不一样。这个坑不踩一次等它卡住的时候会以为是自己代码写错了白白浪费一个下午。5. 避坑清单这套毕设源码里最高频的 5 个翻车点5.1 登录报错 openid 为空code 只能用一次现象连续多次点击登录按钮第一次成功第二次失败或者后端日志报「invalid code」。原因wx.login 生成的 code 是一次性的后端拿去换 openid 之后立即失效。如果前端把同一个 code 重复提交给后端第二次必然失败。解决确保每次登录都重新调 wx.login后端不要缓存 code也不要设计「用同一个 code 刷新登录态」的逻辑。前端登录按钮在请求期间要加 loading 状态防止用户连点。这类问题在演示时最容易暴露学生演示时紧张地双击了登录按钮结果第一次失败观感很差。5.2 获取手机号失败个人主体小程序根本拿不到现象按照源码里的 getPhoneNumber 实现了获取手机号功能但真机测试时拿不到手机号接口一直返回错误。原因微信的 getPhoneNumber 能力从 2023 年起要求小程序必须完成微信认证且仅对企业主体开放个人主体的小程序无法调用这个接口获取真实手机号。解决毕设场景里最稳妥的做法是放弃自动获取改成学生手动输入手机号。如果确实需要用微信手机号快捷填写的交互效果可以在答辩时说清楚「该功能在企业主体下可用当前环境受限于主体类型」绝大多数老师能理解。不要为了演示效果去用第三方模拟工具那会给答辩带来不必要的解释成本。5.3 数据库连接被拒和中文乱码90% 是字符集和时区现象后端启动时报 Communications link failure或者插入的中文数据变成问号。原因前者一般是数据库密码或 URL 配错后者是连接串没有指定 characterEncoding或者表结构默认用了 latin1。解决连接串统一写成useUnicodetruecharacterEncodingutf8建表语句统一指定CHARSETutf8mb4。utf8 和 utf8mb4 的区别也值得注意emoji 表情在 utf8 字符集下存不进去会报 Incorrect string value改成 utf8mb4 之后一切正常。另外 MySQL 8 驱动要求连接串带 serverTimezone否则会抛时区异常。改完配置后记得重启后端而不是只刷新页面。这套问题排查顺序写下来能覆盖数据库层绝大多数启动失败。5.4 图片上传后刷新就丢本地存储与合法域名的双坑现象商家后台上传的菜品图片当时能看到重启后端或过几天再打开就没了。原因这类源码里图片保存通常写死在本地磁盘某个目录比如 uploads/后端重启不丢文件但如果你运行的目录变了或者重新导入了数据库图片路径和实际文件对不上就会显示不出来。解决排查时先看数据库里的 image_url 存的是相对路径还是完整 URL再看后端有没有配置静态资源映射。常见做法是后端加一个资源映射把 /uploads/** 指向本地目录前端用完整地址访问。部署到服务器后本地存储的图片还要考虑备份问题如果正式上线更建议换成云对象存储数据库里只存 key。毕设阶段把静态映射配置好就够用但要知道这个方案的边界在哪里。5.5 订单状态被覆盖并发场景下的条件更新现象商家两个窗口同时处理同一个订单最后订单状态停在错误的位置比如「已完成」又变回了「制作中」。原因代码里先查状态再更新两步之间没有原子性后执行的更新覆盖了先执行的结果。解决把状态校验写进 UPDATE 的 WHERE 条件也就是第 3 章里演示的条件更新写法。这里要形成一个习惯所有订单状态流转一律用UPDATE orders SET status 目标状态 WHERE id ? AND status 期望状态受影响行数为 0 时直接报错返回。这个模式不仅在订餐系统里适用库存扣减、余额变更、任务状态推进凡是并发场景下的状态修改都应该这样写。答辩时能讲清楚这一点比多写十个页面更能证明你理解业务。6. 让答辩多讲三分钟订阅消息、库存预扣与验证方法如果你时间充裕想在这套系统上再加点工作量我建议优先做三件事它们的投入产出比最高。第一是订阅消息学生支付成功后申请一次订阅授权商家更新订单状态为「待取餐」时推一条取餐通知。订阅消息和普通模板消息的差别在于需要用户主动授权而且是「一次性订阅」每次推送前都需要重新授权这个机制本身就值得在答辩时展开讲。第二是库存预扣下单后先锁定库存超时未支付则释放这是电商系统里非常经典的库存设计和 3.2 节里的防超卖 SQL 可以连成一套完整的库存方案。第三是数据统计用一个带日期筛选的接口跑出当日订单数、营业额、菜品销量排行后端几条 SQL 就能出数据但演示时非常直观。验证方法上我会建议准备两个手机一个学生端账号走完「选菜 → 下单 → 模拟支付 → 查看订单进度」的完整链路一个商家端账号同时操作观察订单状态从待支付到已完成每一步的界面反馈。另外把小程序开发者工具的 Network 面板打开逐个接口确认状态码不是红色重点看登录接口和下单接口的响应时长。数据库里同步查订单表和明细表确认金额、数量、快照字段都正确。最后做一次异常操作学生端在支付页面断网再恢复看订单是否会卡死在待支付状态这能验证后端的超时处理能力。我每次接手这种带数据库的毕设项目第一件事永远是先从头执行一遍 SQL 脚本再打开 Network 面板把登录链路看一遍。这两个动作做完这个包能不能用、差多少工作量、答辩时会被追问哪里心里基本有数。设计上的取舍和数据一致性永远是这套系统最值得花时间的部分功能堆得再多订单状态逻辑乱了演示时一定会翻车。希望帮到你。本文还有配套的精品资源点击获取