简介:这是一套基于SSM、Vue与uniapp技术栈的外卖点餐微信小程序毕业设计项目,完整覆盖管理员、外卖员、餐厅、用户四种角色。前端为微信小程序端,后台使用Java的SSM框架管理,数据库采用MySQL,配套有可直接运行的源码、SQL文件与项目文档,适用于毕业设计、课程实训及小程序电商类项目的二次开发练习。包体共1420个文件,大小约17.26MB,其中包含vue小程序页面、java后台接口、sql数据库脚本、png/jpg界面素材以及js/wxml/wxss等小程序结构文件,目录分类清楚,便于按模块检索和部署。系统内置菜品分类管理、订单配送、评价留言、收藏下单、后台数据管理等完整业务闭环,餐厅可维护菜品并审核订单,外卖员可抢单,用户可收藏支付评价,能较好展现前后端交互与权限设计思路。目前已有1912人学习下载,对于需要快速搭建外卖类小程序毕业设计或希望参考SSM+uniapp整合方式的开发者,有不错的参考价值。
1. 外卖点餐系统小程序:一套能跑通“用户下单→餐厅接单→骑手配送→评价”闭环的SSM毕业设计源码
很多第一次拿到这套外卖点餐系统小程序源码的人,第一眼看到压缩包里那些.bat脚本和.vue.bak备份文件时,会以为部署很麻烦。但实际上,这是一套结构相当完整、角色划分清晰的毕业设计级项目,基于 SSM + Vue + uniapp 实现,覆盖管理员、餐厅、外卖员、用户四个身份。它的核心价值在于:小程序端和后台管理端共用一套 MySQL 数据库,把外卖业务里的订单、配送、评价、留言全部串成了一条可闭环的链路。无论是想拿它跑通毕业论文的完整演示,还是想学习 SSM 后端如何给 uniapp 小程序提供接口,这套源码都值得下载后好好拆一遍。
2. 项目结构拆解:SSM 后端、Vue 管理端、uniapp 小程序三端是怎么组织起来的
2.1 四个角色与业务边界:谁在管理、谁在接单、谁在下单
从摘要描述看,这套系统最突出的设计是“一库四端”:同一个后端服务,同时服务管理员后台、餐厅端小程序、外卖员端小程序和用户端小程序。四个角色没有共用一套界面,而是各自有独立的操作路径。
管理员的工作在浏览器后台完成,核心是基础数据维护:外卖员管理、餐厅管理、用户管理、菜品分类管理、菜品信息管理,这些全部是增删改查;同时还能查看外卖订单、订单配送、订单评价、在线留言,相当于整个平台的运营后台。
餐厅和外卖员都是在微信小程序端操作。餐厅负责自己店铺的菜品内容维护,包括上架、修改、删除、查询,还能审核用户下的订单,查看配送进度和用户评价,处理投诉反馈和在线留言。外卖员的核心动作是“抢单”,看到待配送的订单后认领,然后推进配送状态。
用户端的操作最贴近真实外卖 App 的体验:首页搜索菜品、浏览菜品、进入详情页收藏或下单、对订单支付、查看配送状态、确认后写评价。整个业务边界划分得很清楚,没有出现角色功能交叉混乱的问题。
2.2 技术选型:SSM 加 uniapp 为什么在毕业设计场景里依然能打
后端用的是 Java 的 SSM 框架,这是 Spring + SpringMVC + MyBatis 的组合。放在今天来看,它不像 Spring Boot 那样自带 Starter 和自动配置,但正因为依赖关系都是手动配出来的,反而适合论文里写“系统架构设计”这一章:控制层、业务层、持久层的分层结构非常标准,导师看起来也直观。
MySQL 作为本地数据库,存储用户、餐厅、外卖员、菜品、订单、配送、评价这些核心数据。小程序端用了 uniapp,一套代码编译到微信小程序,同时保留了扩展到 App 的可能性。后台管理端是 Vue 技术栈,源码里出现的 IndexMain.vue.bak、BreadCrumbs.vue.bak、IndexHeader.vue.bak 这些文件,能看出后台管理页面是基于 Vue 组件化方式搭的布局,整体结构接近常见的中后台管理模板。
这套组合的实用之处在于:SSM 负责接口和数据事务,Vue 负责后台管理界面,uniapp 负责 C 端小程序。三层各管一段,出了问题能顺着请求链路很快定位。这也是为什么毕业设计里大量外卖、商城类项目都选这套技术栈——不是因为它最潮,而是因为它结构清晰、参考资料多、工作量容易量化。
2.3 源码包文件反推:.bat 脚本和 .bak 备份文件透露了什么信息
拿到压缩包后,我一般会先不急着解压运行,而是看一眼文件清单。这套源码里有几个关键文件值得先摸清楚作用。
.classpath是 Eclipse Java 项目的工程配置文件,说明后端是一个可以直接导入 Eclipse 的 SSM 工程。如果你平时用 IDEA,导入时需要选择 Eclipse 格式的项目文件,或者在 IDEA 里新建一个空项目再关联源码,两种方式都行。
1-install.bat、2-run.bat、3-build.bat三兄弟是 Windows 下的批处理脚本。常见做法是:1-install 负责初始化环境,比如检查 MySQL 服务、导入 SQL 文件、执行 Maven 打包;2-run 负责启动后端服务;3-build 负责构建前端小程序或后台管理端。不同项目里脚本内容差异很大,建议先打开 bat 文件看里面实际执行了多少条命令,再决定要不要手工一步步跑。
.vue.bak和main.css.bak这些备份文件,通常是在原有文件上改动前留的副本。它们的存在说明这套系统在被整理成发行版之前,有人对后台管理端做过二次调整。这对我们是个提示:正式运行前,把这些 .bak 文件移出项目目录,否则 IDE 扫描时可能把它们当正式源码解析,出现莫名其妙的编译错误。
一个典型的项目目录结构应该长这样:
wx410外卖点餐系统小程序 ├── sql/ # 数据库初始化脚本 ├── server/ # SSM 后端工程 │ ├── src/main/java # Controller、Service、Mapper │ ├── src/main/resources # Spring 配置、MyBatis XML │ └── pom.xml ├── admin-web/ # Vue 后台管理端 │ ├── src/views/ # 各管理页面组件 │ └── package.json ├── miniapp/ # uniapp 小程序工程 │ ├── pages/ # 首页、订单、我的 │ └── utils/request.js # 请求封装 ├── 1-install.bat ├── 2-run.bat └── 3-build.bat这套结构的好处是三端都在同一个目录下管理,后端接口、后台页面、小程序页面可以对照着看。改一个功能时,数据库、后端、前端各跑一遍,链路是打通的。
3. 后端核心实现:订单、配送、评价的接口链路与数据流转
3.1 数据库设计:九张核心表与订单状态机的流转逻辑
一套外卖系统能不能跑起来,关键看订单状态怎么流转。根据摘要里的功能描述,我梳理出这套系统最核心的几张表:用户表、餐厅表、外卖员表、菜品分类表、菜品信息表、外卖订单表、订单配送表、订单评价表、在线留言表,再加上收藏相关的表。
订单状态是整个系统的主动脉。我推荐用整型字段存状态,比字符串更省空间,也方便写条件更新。常见枚举定义如下:
0 = 待支付(用户下单未付款) 1 = 已支付待审核(用户已付款,等餐厅确认) 2 = 待配送(餐厅已接单,等外卖员抢单) 3 = 配送中(外卖员已接单,正在路上) 4 = 已完成(用户确认收货) 5 = 已取消这个状态机里,每次状态变更都必须校验“当前状态是什么”。比如餐厅审核订单时,只有状态为 1 的订单才能变成 2;外卖员抢单时,只有状态为 2 的订单才能变成 3。如果跳过校验直接改状态,两个外卖员同时抢一单就会出现数据错乱。
数据库层面的常见做法是:在更新语句的 WHERE 条件里带上原状态字段,这样并发情况下只有第一个更新请求能成功,MySQL 的行锁会挡住第二个请求。
3.2 抢单接口:外卖员并发认领订单的 Service 层实现
外卖员抢单是这套系统里最需要谨慎处理的并发场景。两个外卖员同时点抢单,如果后端不做状态校验,订单可能被两个人同时认领。这里我用 MyBatis 的条件更新来解决,先看核心代码。
/** * 外卖员抢单:只有状态为待配送(2)的订单才能被认领 */ @Override @Transactional(rollbackFor = Exception.class) public boolean grabOrder(Long orderId, Long deliveryUserId) { Orders order = ordersMapper.selectById(orderId); // 订单不存在或者已经被抢,直接返回失败 if (order == null || !OrderStatus.WAIT_DELIVERY.equals(order.getStatus())) { return false; } // 条件更新:期望原状态是 2,目标状态是 3 int rows = ordersMapper.updateStatusAndDelivery( orderId, OrderStatus.WAIT_DELIVERY.getCode(), OrderStatus.DELIVERING.getCode(), deliveryUserId); // rows 为 1 说明当前外卖员抢单成功,为 0 说明被别人抢先了 return rows == 1; }这段代码的关键在最后一条 update 语句。updateStatusAndDelivery对应的 Mapper XML 如下:
<!-- 只有订单状态等于期望状态时才会执行更新 --> <update id="updateStatusAndDelivery"> UPDATE orders SET status = #{targetStatus}, delivery_user_id = #{deliveryUserId}, update_time = NOW() WHERE id = #{orderId} AND status = #{expectStatus} </update>这里 WHERE 条件里的status = #{expectStatus}是成败的核心。第一个外卖员执行更新后,订单状态变成了 3,第二个外卖员执行的 SQL 在 WHERE 判断时就匹配不上,影响行数为 0,抢单失败。这个写法比先 SELECT 再 UPDATE 的锁表方式效率高很多,也是这类订单系统里最常见的并发控制手段。
参数说明:orderId是订单主键,deliveryUserId是当前登录的外卖员 ID,expectStatus是调用前查询到的原状态,targetStatus是本次要推进到的目标状态。实际改代码时,这套方法的返回值应该直接暴露给 Controller,前端根据返回 true/false 决定是跳转配送页还是提示“手慢了”。
3.3 餐厅审核与配送状态推进:谁在什么节点动哪条数据
餐厅审核订单的逻辑和抢单类似,只是角色变了。餐厅端只能审核属于自己餐厅的订单,所以 Service 方法里要先做属主校验。
/** * 餐厅审核订单:通过则进入待配送,拒绝则订单取消 */ @Transactional public boolean auditOrder(Long orderId, Long restaurantId, Integer pass) { Orders order = ordersMapper.selectById(orderId); // 订单不属于当前餐厅,不允许审核 if (order == null || !order.getRestaurantId().equals(restaurantId)) { return false; } // 只有已支付待审核(1)的订单才能被审核 if (!OrderStatus.PAID.getCode().equals(order.getStatus())) { return false; } Integer targetStatus = pass == 1 ? OrderStatus.WAIT_DELIVERY.getCode() : OrderStatus.CANCELED.getCode(); // 同样用条件更新防止重复审核 return ordersMapper.updateStatus(orderId, order.getStatus(), targetStatus) == 1; }配送状态的推进链路是:餐厅审核通过后订单进入待配送,外卖员抢单后变成配送中,外卖员点击“送达”后变成已完成。和抢单一样,送达操作也必须做条件更新,否则用户已经点击收货了,订单状态还能被改成配送中,数据就乱了。
这套系统里,订单配送表是外卖订单表的辅助表,记录哪个外卖员在什么时间开始配送、什么时间送达。抢单时写两条数据:一条更新 orders 表的状态和配送员 ID,一条插入 order_delivery 表的开始时间。两条操作放在同一个事务里,避免订单状态改了、配送记录没插上的问题。
3.4 评价与留言闭环:订单完成之后的用户反馈路径
用户确认收货后,订单状态变成 4,此时前端才开放评价入口。评价接口同样要有状态校验,防止订单没完成就刷评价。
/** * 用户评价订单:订单必须是已完成状态 */ public boolean addComment(CommentDTO dto) { Orders order = ordersMapper.selectById(dto.getOrderId()); if (order == null || !OrderStatus.COMPLETED.getCode().equals(order.getStatus())) { return false; } // 一个订单只允许一条评价,插入前先查重 int count = orderCommentMapper.countByOrderId(dto.getOrderId()); if (count > 0) { return false; } OrderComment comment = new OrderComment(); comment.setOrderId(dto.getOrderId()); comment.setUserId(dto.getUserId()); comment.setRating(dto.getRating()); comment.setContent(dto.getContent()); return orderCommentMapper.insert(comment) > 0; }在线留言和投诉反馈走的是另一张表,餐厅可以在小程序端查看并回复。整体来看,后端的设计思路是“状态驱动”:所有核心业务操作都围绕订单状态展开,用条件更新保证并发安全,用事务保证多表一致性。理解了这条链路,再去跑项目,基本不会被业务逻辑绕晕。
4. 小程序前端实战:uniapp 页面从登录到下单的完整交互
4.1 登录注册与请求封装:小程序的网络层是怎么和后端对齐的
uniapp 端的网络请求如果不做统一封装,每个页面都写一遍uni.request,后期改接口地址会非常痛苦。这套项目里的常规做法是抽一个request.js,把 BASE_URL、token 注入、错误提示全部集中处理。
// utils/request.js —— uniapp 全局请求封装 const BASE_URL = 'http://localhost:8080/wx410-api'; export function request(options) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'token': uni.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 200) { resolve(res.data); } else { uni.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(res.data); } }, fail: (err) => { uni.showToast({ title: '网络异常,请检查后端服务', icon: 'none' }); reject(err); } }); }); }参数说明:BASE_URL是后端接口的根路径,本地联调时改成自己后端的端口;token从本地缓存读取,每次请求自动带上,后端通过拦截器校验登录状态。页面里调用时只需要关注业务参数,不用重复处理错误弹窗。
登录注册在摘要里写得很明确:用户需要先注册才能登录。小程序端一般是一个表单页,用户名、密码、手机号,注册成功后自动跳转登录。登录接口返回 userId 和 token,前端存到uni.setStorageSync里,供后续请求使用。
4.2 首页菜品流与关键词搜索:列表渲染和请求参数设计
首页是整个 C 端用户最先看到的功能模块。根据摘要描述,用户登录后可以在首页搜索菜品名称,也能直接浏览菜品列表。前端页面对应的核心逻辑是:加载分类列表、根据分类和关键词请求菜品数据、渲染到页面上。
// pages/index/index.vue —— 首页菜品加载 async loadFoods() { const res = await request({ url: '/food/list', method: 'GET', data: { keyword: this.keyword, // 搜索关键词,空字符串表示查全部 categoryId: this.categoryId, // 分类 ID,0 表示全部 page: this.page, // 从第 1 页开始 limit: 10 // 每页 10 条 } }); if (res.code === 200) { this.foodList = this.page === 1 ? res.data.records : this.foodList.concat(res.data.records); } }前端每一次点分类或输入关键词搜索,都会把categoryId和keyword传给后端。后端 Controller 接收这两个参数后,组合成动态 SQL 去菜品表里查。这里值得注意的是分页参数:page 从 1 开始,下拉加载时 page 自增,数据通过concat追加而不是覆盖。
菜品卡片上一般会显示图片、价格、月售数量。如果餐厅端维护了菜品的销售字段,这里会一起展示。首页底部导航可以切换到其他功能模块,符合小程序原生 tabBar 的行为。
4.3 商品详情页:收藏、下单、支付的三段式交互
摘要里对商品详情页的描述很细:点击菜品图片右下角的五角星是收藏操作,双击是取消收藏;点击右下角的下单按钮会进行下单操作。收藏功能的前端交互是即时反馈,但后端要保证幂等。
// pages/detail/detail.vue —— 收藏与取消收藏 async toggleCollect() { // 点击五角星先切换 UI 状态,失败再回滚 this.isCollect = !this.isCollect; const res = await request({ url: '/food/collect', method: 'POST', data: { foodId: this.foodId, userId: uni.getStorageSync('userId') } }); if (res.code !== 200) { this.isCollect = !this.isCollect; // 接口失败时回滚 return; } this.collectId = res.data.id || this.collectId; }下单流程比收藏复杂。用户点击下单后,前端带上菜品 ID、数量、收货地址、备注发请求创建订单。创建成功后弹出支付确认框,这里的支付通常是模拟支付——直接把订单状态从待支付改成已支付。整个交互链路是:创建订单 → 确认支付 → 跳转到订单列表 → 等待餐厅接单。
这种模拟支付逻辑在毕业设计里非常常见,它把微信支付的复杂流程简化成一个接口调用。如果要接入真实支付,需要在后端增加下单前的 prePay 接口和支付回调接口,订单状态以回调结果为准,前端不能直接改状态。
4.4 我的页面与后台 Vue 管理端的对应关系
“我的”页面是用户查看个人信息的入口。摘要里提到:登录后点击“我的”,会出现关于我的界面,可以修改个人信息,也可以点击其他功能模块。这个页面包含头像、昵称、手机号展示,以及我的订单、我的收藏、地址管理等入口。
后台管理端则是 Vue 技术栈实现的浏览器应用。前面提到的 IndexMain.vue、IndexHeader.vue、IndexAsideStatic.vue、BreadCrumbs.vue 这组组件,构成了后台管理界面的基础布局:左侧静态菜单、顶部标题栏、中间内容区、面包屑导航。管理员在浏览器里通过这些页面管理餐厅、外卖员、用户、菜品和订单。
后台管理端与小程序端共用同一个后端服务,只是接口路径不同。管理员操作的数据会实时反映到小程序端,比如管理员下架一个菜品,用户端首页刷新后这个菜就不展示了。这种三端共用后端的架构,在答辩演示时比较容易讲清楚数据流转。
5. 避坑指南:把这套源码跑起来最常见的 5 个问题
5.1 后台页面编译报错,元凶是 .bak 备份文件
现象:导入后台管理端后,编译或运行时报 Vue 文件语法错误,错误信息指向的文件名很奇怪,一看是 UpdatePassword.vue.bak、IndexMain.vue.bak 这类备份文件。页面白屏,甚至 dev server 起不来。
原因:.vue.bak是历史版本备份,但它们混在源码目录里,IDE 或构建工具扫描时会把它们当成正式源码解析。Vue 单文件组件对文件后缀敏感,后端 Java 的.classpath也一样,Eclipse 导入时可能因为工程描述文件路径和实际目录不一致而出错。
解决:解压后第一件事,把源码包内所有.bak文件移到一个单独的 backup 文件夹里,或者直接删除。移完后再启动后端和前端,基本能解决一大半诡异的编译报错。确认系统跑通之后,如果还需要某份备份文件的内容,再单独打开复制。
5.2 MySQL 8.x 连不上,提示 ClassNotFoundException
现象:启动 SSM 后端时控制台报ClassNotFoundException: com.mysql.jdbc.Driver,或者报Communications link failure,数据库连接池里全是红色异常。
原因:这个项目是 SSM 老工程,pom.xml 里的 MySQL 驱动可能是 5.x 旧版,驱动类名是com.mysql.jdbc.Driver。如果本机装的是 MySQL 8.x,旧驱动连不上,同时 8.x 的默认加密插件和验证方式也和老驱动不兼容。
解决:把 pom.xml 里的 mysql-connector-java 版本升到 8.0.x,驱动类名改成com.mysql.cj.jdbc.Driver,JDBC 连接串加上时区和证书参数。我一般会这样写:
jdbc.url=jdbc:mysql://localhost:3306/wx410?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=你的数据库密码 jdbc.driver=com.mysql.cj.jdbc.Driver注意allowPublicKeyRetrieval=true这个参数,MySQL 8.x 在非 SSL 连接下有时需要通过公钥获取认证信息,不加这个会直接报Public Key Retrieval is not allowed。
5.3 小程序请求后端接口报“不在合法域名列表”
现象:微信开发者工具里预览小程序,页面数据全空,控制台报https://xxx 不在以下 request 合法域名列表中,或者直接 fail。
原因:微信小程序线上环境要求请求地址必须是备案过的 HTTPS 域名。本地联调时用的http://localhost:8080显然不在白名单内,开发者工具默认会拦截。
解决:在微信开发者工具右上角点击“详情”,找到“本地设置”,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。这是本地开发联调的常用手段,只对开发工具生效,真机预览如果不开调试模式依然会被拦。真正上线时,需要把后端接口部署到有备案域名的服务器,并配置 HTTPS 证书。
5.4 订单状态卡住,餐厅审核后外卖员看不到单
现象:用户下单支付后,餐厅端能看到订单并审核通过,但外卖员端的待抢单列表始终是空的;或者用户端一直显示“等待商家接单”。
原因:状态枚举值对不上。订单表里的状态是整型数字,但前端判断时可能用了字符串,或者后端两个 Controller 里同一个状态值定义不一致。比如餐厅审核通过后写入的是 3,但外卖员列表查询条件写的是 2,自然查不到。
解决:先把 SQL 初始化脚本里的注释状态枚举对齐,再打开后端OrderStatus常量类和两个 Controller 的查询条件逐一对比。调试时可以在 Mapper XML 里临时输出查询条件,或者在后端接口返回的 SQL 日志里看实际传入的 status 值。这种问题定位很快,找到第一个不一致的地方,后面的就顺藤摸瓜了。
5.5 模拟支付导致重复下单,库存和销量被反复扣减
现象:联调阶段疯狂点支付,一个订单支付成功后还能重新支付,或者同一桌用户下了一堆重复订单,菜品的销量数据变得很大。
原因:项目里支付是模拟逻辑,前端点击支付直接调用后端支付接口,后端把订单状态改成已支付就结束了。没有微信支付回调验签,也没有订单号的幂等校验,接口重复调用就会重复改状态。
解决:短期内最有效的方式是在支付接口里加一个判断——订单状态不是待支付时直接拒绝支付请求。长期要接入正式支付,需要引入微信支付的下单、回调、验签逻辑,订单状态以回调为准。接口幂等这件事属于进入真实业务时避不开的坎,模拟阶段至少保证状态机不混乱。
6. 二次开发实战:给餐厅角色加一个“营业状态”开关
这个系统另一个很值得改的地方,是给餐厅增加营业状态控制。原版餐厅信息里没有“是否营业”的字段,用户下单时即使餐厅已经打烊了,订单还是会创建成功。改造思路很简单,三端各动一刀。
6.1 数据库加字段
在 restaurant 表新增status字段,1 表示营业中,0 表示已打烊,默认 1。
ALTER TABLE restaurant ADD COLUMN status TINYINT DEFAULT 1 COMMENT '营业状态:1营业中,0打烊';6.2 后端加接口
餐厅端小程序增加一个切换营业状态的入口,后端提供一个更新接口,同时用户端下单接口要拦截打烊餐厅。
/** * 更新餐厅营业状态 */ @PostMapping("/restaurant/updateStatus") public Result updateStatus(@RequestParam Long restaurantId, @RequestParam Integer status) { restaurantMapper.updateStatus(restaurantId, status); return Result.success(); }下单接口里增加校验:
Restaurant res = restaurantMapper.selectById(order.getRestaurantId()); if (res == null || res.getStatus() != 1) { return Result.fail("该餐厅已打烊,请改选其他餐厅"); }6.3 小程序端加开关
餐厅端“我的”页面加一个 switch 组件,绑定 status,切换时调接口。用户端首页餐厅卡片或菜品详情页展示“营业中 / 打烊”标识,打烊时隐藏下单按钮。
验证流程:先通过接口或 SQL 把某餐厅 status 改成 0,再用用户账号进入该餐厅菜品页,确认下单按钮不可点击;餐厅端账号把 status 改回 1,用户端刷新页面,下单恢复正常。整个改动涉及数据库一张表、后端两个接口、小程序两个页面,是这类系统里性价比很高的二次开发练手项。
这套项目拆到最后,我发现它最有价值的地方不在于某个功能有多复杂,而在于它把“用户下单、餐厅审核、骑手抢单、用户评价”这条主链路完整地落地在了一套代码里。从那以后我每次拿到这类毕业设计源码,第一件事就是先过一遍状态枚举和 .bak 文件,把数据流摸清再启动项目。希望这份拆解能帮你在自己的项目里少走几步弯路。
本文还有配套的精品资源,点击获取