SpringBoot+Vue高校食堂点餐配送系统全栈开发实战拆解
从去年开始我一直在做一套基于SpringBoot Vue的高校食堂点餐配送系统项目代号就叫hx0762。起因很简单学校食堂一到下课高峰人就挤成粥排队十分钟、吃饭五分钟很多人都盼着能提前点好、到点取餐或者干脆让配送员送到宿舍楼下。这个系统就是干这个用的学生在线选菜下单食堂窗口接单制作配送员接单配送管理员在后台统一管食堂、菜品、订单和配送。如果你正打算用SpringBoot和Vue练手或者在做课设、毕设这套系统的完整拆解应该能直接帮到你。这类前后端分离项目最大的价值不在某个功能多炫而是把你平时零散学的东西串成一条完整的业务链路。做的时候你会同时接触到表结构设计、接口规范、前端组件、联调排错、部署上线踩一遍坑下来Shell和Java基础都熟练不少。下面我就按照我当时从0到1的推进顺序把整套项目的需求拆解、技术选型、后端实现、前端页面、并发处理和问题排查挨个讲清楚。1. 项目整体设计与技术选型1.1 高校食堂场景下四类角色和一条核心链路必须先理清开工之前最忌讳上来就建表写代码我第一周基本都在梳理需求。高校食堂点餐配送系统说白了要服务四类人学生用户、食堂窗口商家、配送员、系统管理员。学生用户干的事情最直观注册登录、逛食堂看菜品、加购物车、提交订单、模拟支付、等餐或等配送、确认收货、写评价。食堂窗口这边要维护菜品信息包括名称、图片、价格、库存、分类和上架状态然后处理学生订单接单后把订单状态改成制作中做好后改成待配送。配送员看到待配送订单后接单送到之后更新成配送中学生确认收货流程才算走完。管理员则管全局食堂和窗口的入驻审核、用户禁用、菜品上下架、订单数据查看、配送员指派都是后台页面的活。核心业务链路我用一句话概括浏览菜品→加入购物车→提交订单→支付→食堂接单→制作→配送→确认收货→评价。所有表设计和接口设计都围绕这条链路展开。拆解之后你会发现真正的难点集中在两处第一是订单状态在多个角色之间流转必须靠清晰的状态机约束第二是高峰期大量学生同时下单库存扣减和支付超时处理不好就出问题。这两块我会在后面专门展开这里先记住结论整个系统的地基就是这条主链路。1.2 为什么选SpringBoot Vue前后端分离版本怎么定技术选型这块SpringBoot Vue基本是当前高校项目和个人练手的主流搭配不是因为它最潮而是因为它最稳、资料最多、遇到问题搜得到答案。后端用SpringBoot核心原因是它把配置简化到了极致内嵌Tomcat一个jar包就能跑。配合MyBatis-Plus操作数据库日常CRUD基本不用写SQL分页查询一行搞定我大概三天就把基础架子搭完了。版本上我个人建议学习阶段用SpringBoot 2.7 JDK8或11原因很实际网上的教程和踩坑文章八成以上都是这个组合你用3.x遇到问题反而难搜。如果你一定要用SpringBoot 3.x记得JDK版本至少17而且原来的javax.servlet包会变成jakarta.servlet很多老代码要改。前端选Vue是看中它的组件化开发思路和庞大的生态。版本上我更推荐Vue3 Vite Element PlusVite冷启动极快改代码热更新几乎不用等写起来比Webpack时代的Vue CLI舒服太多。Element Plus的表格、表单、对话框、消息提示组件都是现成的后台管理页面十有八九靠它拼出来。前端路由用Vue Router状态管理用PiniaHTTP请求用Axios这几件套足够应付整个项目。既然项目代号是hx0762我建议你的数据库名、后端服务名、前端目录名统一带上这个标识后面部署定位问题会省很多事。1.3 模块划分与整体架构前后端目录先规划好整个系统我按业务分成六大模块用户认证模块、食堂与菜品模块、购物车模块、订单模块、配送模块、后台管理模块。后端代码用经典分层结构controller负责接参数和返回结果service写业务逻辑mapper操作数据库entity对应表结构。正是因为前后端分离后端只提供JSON接口前端不管数据来源只负责渲染和交互。前端目录我习惯这样规划views放页面组件api放接口请求方法router放路由表store放全局状态utils放工具函数components放复用组件。页面路由也按角色分普通用户的点餐页、订单页、个人中心配送员的配送任务页管理员的食堂管理页、菜品管理页、订单管理页。部署结构上前端打包后的静态文件交给Nginx托管后端Java项目打成jar包运行两者通过HTTP接口通信。Nginx在这里还承担了一件事把/api开头的请求转发到后端服务地址这样前端在浏览器里访问的是同源地址能绕开一部分跨域问题。图上画出来大概是浏览器→Nginx(静态页面) →反向代理/api→SpringBoot应用→MySQL数据库。2. 数据库设计与后端核心实现2.1 避开混乱订单表设计时一定要保留这几个关键字段数据库是整个系统的地基表设计得烂后面业务写起来全是坑。我核心建了八张表用户表、食堂表、窗口表、菜品表、购物车表、订单表、订单项表、配送表外加一张评价表。用户表字段包含id、用户名、密码、手机号、角色、宿舍楼栋、状态、创建时间。角色我用整数存1是学生2是食堂窗口3是配送员4是管理员比字符串省空间也好扩展。密码字段一定要存BCrypt加密后的密文千万不能明文。订单表是重头戏我把关键字段列出来你感受一下字段名类型说明idbigint主键order_novarchar(32)订单号唯一索引user_idbigint下单用户window_idbigint食堂窗口total_amountdecimal(10,2)订单总金额statustinyint订单状态0待支付1待接单2制作中3待配送4配送中5已完成6已取消addressvarchar(100)配送地址楼栋宿舍号remarkvarchar(255)备注pay_timedatetime支付时间finish_timedatetime完成时间create_timedatetime创建时间两个容易忽略的点一是total_amount必须在下单那一刻把菜品单价算好存进去不能等后面再去菜品表查价格否则食堂改价之后历史订单金额就乱套了二是订单表要建联合索引(user_id, create_time)学生查看“我的订单”列表时按用户和时间倒序查没索引表一大就卡。菜品表里我加了stock字段做库存category字段做分类status表示上架还是下架。订单项表则记录订单下的每个菜品快照包括菜品名、单价、数量、小计方便对账。2.2 登录认证与权限控制JWT加拦截器前后端分离的标准解法前后端分离之后服务端不存Session登录状态一般都靠JWT解决。流程很简单用户提交用户名密码后端校验通过后生成一个token串返回给前端前端把它存到localStorage每次请求在请求头里带上Authorization: Bearer token后端拦截器解析token拿到用户身份放行或拒绝。JWT有三个部分组成头部、载荷、签名签名是为了防止token被篡改。我在生成token时会把用户id、用户名、角色塞进载荷里后端后续接口直接从中取用户信息不用每次查数据库。密钥和过期时间放在配置文件里token过期时间我设的是24小时学生一天的活跃期基本够用。拦截器代码不复杂核心逻辑就是放行白名单解析token校验角色public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); } try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }角色权限怎么控制我的做法是在需要管理员或配送员身份的接口上手动校验也可以自己写个RequireRole注解配合拦截器处理效果一样。注意给菜品浏览、食堂列表这些接口放在白名单里不然用户还没登录就没法逛食堂。2.3 订单状态机与配送流程状态乱跳是新手最容易犯的错学生提交订单只是起点订单在食堂、配送员、学生之间流转状态一旦能乱跳页面显示就会完全对不上。我把订单状态设计成一组枚举并明确谁可以把它从哪个状态改成哪个状态public enum OrderStatus { PENDING_PAYMENT(0, 待支付), PENDING_ACCEPT(1, 待接单), COOKING(2, 制作中), PENDING_DELIVERY(3, 待配送), DELIVERING(4, 配送中), COMPLETED(5, 已完成), CANCELLED(6, 已取消); }状态流转规则我在service层严格判断每次更新状态前先查当前状态不满足前置状态就直接报错。比如只有待支付才能取消支付成功才能变成待接单食堂接单后变成制作中制作完成变成待配送配送员接单变成配送中学生确认收货才变成已完成。配送模块我第一版做得简单直接一个订单对应一条配送任务订单进入待配送状态后配送员在配送任务列表里看到点“接单”就把配送表里配送员id写上状态变成配送中。到达后配送员点“已送达”学生确认收货订单完成。如果后面要做批量顺路配送那才是真正涉及算法的地方第一版没必要。3. 前端页面搭建与Vue实战要点3.1 Vue项目初始化与环境配置用Vite创建项目装好依赖前端项目我用的Vite初始化Vue3项目比Vue CLI快非常多。创建命令很简单npm create vuelatest执行后它会问你项目名、要不要TypeScript、要不要Vue Router、要不要Pinia按需勾选就行。项目初始化好之后进入目录安装基础依赖npm install npm install axios element-plusElement Plus是Vue3的UI组件库完整引入方式在main.js里一次性use到底个人项目不做性能优化完全没问题。要注意的是Element Plus组件默认是英文文案日期组件和分页组件需要自己配中文语言包我第一次没配分页器显示英文差点没笑死。Vite开发环境的代理配置是个重点。后端接口跑在8080端口前端开发端口是5173浏览器直接跨域。最省心的做法是在vite.config.js里配置代理让前端把/api开头的请求转发到后端server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端代码里写axios请求就只用写/api/xxx不用管后端地址等部署的时候再把代理配置搬到Nginx里思路一致。3.2 前后端请求封装与token处理Axios拦截器一次搞定前端所有接口请求我都通过一个封装的request.js发出去没有在每个页面里裸用axios这是很关键的一个习惯。封装要做三件事设置基础URL、请求拦截器带token、响应拦截器统一处理错误。import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code 200) { return res.data } ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) }, error { if (error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(网络异常) return Promise.reject(error) } ) export default request这里有个前后端约定细节我让后端所有接口都返回统一格式的JSON结构是{code: 200, msg: success, data: {...}}。前端拦截器看到code为200就直接把data丢给业务代码其他code统一弹错误提示。这样业务代码里就不用到处写try catch和if判断了。token过期处理很容易漏但一旦处理不好用户会看到一堆莫名其妙的数据报错。我在响应拦截器里统一判断401状态码token过期就清掉本地token再跳登录页用户重新登录就能继续用体验舒服很多。3.3 关键页面实现点餐页、购物车与订单状态操作点餐页是学生用得最多的页面布局我参考了外卖App左侧是食堂窗口列表右侧是窗口下的菜品卡片。窗口列表从后端接口获取点击窗口就切换菜品列表。菜品卡片展示图片、名称、价格、月销量底部有加号按钮加入购物车。购物车状态我用Pinia的store管理因为点餐页和购物车弹窗是不同组件如果各自维护一份数据数量不同步会非常折磨。购物车store里维护一个mapkey是菜品idvalue是数量加减操作和结算金额都从store里读。提交订单时把购物车数据整理成订单项数组传给后端后端创建订单和订单项成功后前端清空购物车并跳转订单页。订单管理页分角色展示。学生端的“我的订单”按时间倒序列出订单每个订单卡片显示状态和菜品摘要待支付订单有去支付按钮已完成订单有评价按钮。食堂端的订单页是所有属于该窗口的订单每个订单有接单、完成制作按钮。配送员端的配送任务页显示待配送和配送中的订单接单后按钮变成已送达。前端按钮的状态控制直接跟后端返回的status字段绑定后端说能操作就显示按钮不能操作就置灰双端判断才不会乱。4. 配送排序与并发处理的几个硬骨头4.1 库存扣减与超卖问题不能光减库存要带条件判断食堂菜品库存如果处理成“下单就减、取消就加”高并发下有超卖风险。假设某个菜品只剩1份两个学生同时下单两个请求都先查到库存为1都判断可以通过再各自执行库存减1最后库存变成-1菜品却卖出去了两份。最基础的解法是乐观锁方式把扣减SQL写成带条件的形式库存大于0才更新UPDATE dish SET stock stock - 1 WHERE id #{dishId} AND stock 0;执行后看影响行数是1说明扣减成功是0说明库存不足直接告诉用户卖完了。这样数据库层面就把并发问题挡掉了代码简单性能也不错学生项目完全够用。更大的并发场景才需要引入Redis预扣减、消息队列异步处理这类方案做课设毕设不建议上来就整分布式复杂度会把你拖垮。下单维度还有一个幂等性问题不能忽略。用户手快连点两次提交订单就会产生两个一模一样订单。我做了三重防护前端提交按钮点击后立刻置灰禁用后端生成订单号时用订单号做唯一索引同时在下单接口里加了个简单的防重判断短时间内同一用户对同一窗口的相同菜品列表不能重复提交。教材里不会讲这些细节但实际开发全是这套东西。4.2 配送任务分配先做最简单的指派方案别一上来就写算法配送任务分配很容易想复杂什么路径规划、订单聚类、骑手负载均衡这些对一个练手项目来说纯属自找麻烦。我的第一版方案就是管理员指派加配送员自取代码量小逻辑也直观。订单支付成功后自动生成配送任务状态为待配送。管理员在后台的配送管理页看到待配送订单列表选一个空闲配送员点指派任务就绑定到人状态变配送中。同时配送员的配送任务列表也会出现这条记录他送完后点确认送达。这就是最简单的闭环。如果你想让配送员的参与感更强可以改成抢单模式待配送任务对全体空闲配送员可见谁先点击接单谁获得这个任务。抢单在代码上就是更新配送表时加一个条件配送员id为空防止两个配送员同时抢到同一条和库存扣减的思路异曲同工。等业务跑顺了再考虑按宿舍楼栋和送餐时间做聚合分配把同一个楼栋的订单派给同一个人效率高不少。4.3 订单超时与支付模拟定时任务要记得回滚库存真实支付要对接微信或支付宝个人项目办不了商户号我直接做了模拟支付。学生点击去支付后端把订单状态改成已支付再跳到模拟支付成功页整个过程一秒完成。如果你想更像真的可以在支付页放一张收款二维码图片学生扫码后点“我已支付”本质还是模拟。超时未支付订单一定要处理。我设置15分钟未支付自动取消后端用Spring的Scheduled写了一个定时任务每两分钟扫一次待支付订单超过15分钟的把状态改成已取消同时恢复菜品库存Scheduled(fixedDelay 120000) public void cancelExpiredOrders() { LocalDateTime deadline LocalDateTime.now().minusMinutes(15); ListOrder orders orderMapper.selectPendingOrdersBefore(deadline); for (Order order : orders) { order.setStatus(OrderStatus.CANCELLED.getCode()); orderMapper.updateById(order); restoreStock(order.getOrderItems()); } }定时任务方案简单直接缺点是服务重启时触发时机可能延迟但对学习项目来说够用了。更优雅的方案是RabbitMQ延迟队列或Redisson延迟队列微信支付还要处理回调通知和关单那又是另一套复杂度了。准备面试的时候可以顺手把这些方案都了解一遍面试官很爱问。5. 前后端联调与常见问题排查实录5.1 跨域问题前后端联调第一关配置要二选一前后端联调时最常遇到的就是跨域浏览器控制台报CORS错误接口请求根本发不出去。原因很简单前端跑在5173端口后端跑在8080端口不同源浏览器默认拦截。解决方案有两个方向。一个是后端开启CORS配置在SpringBoot里写个配置类允许所有来源访问另一个是前端用代理把跨域请求转发到同源地址也就是3.1里配置的vite代理。这两种方式建议二选一如果两边同时配有时候会在实际请求里出现重复的Access-Control-Allow-Origin头反而触发浏览器报错。我实际开发中只用了前端代理原因是部署到生产环境时也用Nginx做同源代理前后端联调阶段和生产环境的网络路径一致出问题的概率更小。后端代码里就不用额外开CORS了保持接口侧的纯粹性。5.2 经典前端坑Vue对象赋值页面不更新Vue3用reactive定义响应式对象后如果直接对对象里原本不存在的属性做赋值页面可能不会刷新。我遇到的真实场景是这样的订单列表里每个订单对象初始化时只有id、status这些字段后端某个查询接口在返回时给订单对象塞了一个新字段deliveryName前端表格页面没反应。原因是reactive的代理在对象创建时就确定了哪些属性是响应式的后加属性虽然能赋值但视图不会自动更新。解决方法是初始化对象时就把可能用到的字段都声明好或者用Object.assign一次性赋值触发整体更新// 错误写法页面不更新 order.deliveryName data.deliveryName // 正确写法 Object.assign(order, { deliveryName: data.deliveryName })Vue2那边用的是Vue.set方法Vue3里这套已经废弃了排查问题前先确认版本不然照着老教程改半天都没用。数组也是一样按索引直接改数组项不会触发视图更新要用splice或者整体替换数组。5.3 后端联调常见异常时间格式、空指针、统一返回体后端出问题一般集中在几个地方。第一个是时间格式Java后端返回的LocalDateTime默认序列化格式是2025-01-06T12:30:00这种带T的前端直接显示很丑。我在配置里加了全局Jackson格式化spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8time-zone必须设成GMT8否则服务器是UTC时区时前端看到的时间会差8个小时。第二个是空指针。查询菜品详情时如果菜品id不存在service层直接调用getById返回null再取属性就会空指针接口直接500。我的处理是统一封装一个业务异常类参数查不到时抛出异常由全局异常处理器返回友好的提示信息前端弹个“菜品不存在”而不是“网络异常”。第三个是MyBatis-Plus自动填充。createTime和updateTime如果每次插入都要手动set字段一多特别容易漏。我配置了MetaObjectHandler做自动填充插入时自动填创建时间和更新时间更新时自动更新更新时间省心很多。注意数据库字段类型用的是datetimeJava实体用LocalDateTime两端对应好就不会出问题。5.4 安全与部署上线前必须检查的几个细节个人项目上线之前安全层面至少做三件事。第一是密码加密登录注册接口里密码一律用BCrypt加密再入库登录时用BCrypt校验绝不能明文存储也千万别自己写个MD5就以为安全了。第二是JWT密钥不要硬编码在Java代码里放到application.yml配置文件中部署时用环境变量注入防止代码泄露后token也能被别人伪造。第三是SQL注入和敏感信息泄露MyBatis-Plus的Wrapper参数化查询基本能挡住注入但日志里不要打印完整的token和密码字段。部署流程分前后端两块。前端先执行npm run build把生成的dist目录里的静态文件上传到服务器的Nginx站点目录再在location /api块配反向代理指向后端服务。后端用Maven打jar包直接java -jar运行。数据库连接配置里的密码不要写在配置文件里明文保存至少放到环境变量中引用。还有一个容易被忽略的点生产环境写日志时不要打印数据库连接串和请求参数里的敏感字段如果保存了JVM堆转储文件里面可能包含运行时的密码和token这类文件要禁止外部访问。安全意识早点培养对后面找工作面试也有加分。6. 复盘总结与进一步扩展方向6.1 个人收获这个项目做完SpringBoot和Vue的面试题不再抽象做完整套系统再回头复习SpringBoot和Vue的面试题会发现很多以前死记硬背的东西突然有了画面。SpringBoot的自动配置原来就是在项目启动时按条件加载了一堆组件前后端分离的认证流程就是JWT加拦截器这套组合Vue的响应式原理也不再只是概念而是你真真切切踩过“对象赋值不更新”的坑之后才理解的。时间安排上我大概用了四周第一周梳理需求和设计表结构第二周完成后端主体接口第三周做前端页面和联调第四周处理配送、超时和部署。如果想压缩时间后端建议优先做认证、菜单菜品、订单这三块这是主流程。评价和后台统计可以往后放。踩过的坑里印象最深的是订单状态乱跳问题初期没有严格做状态前置校验测试时比如制作中的订单可以被打回待接单页面逻辑直接错乱。后来把所有状态流转收敛到service层的一个方法里任何状态变更都经过统一校验问题才根治。这个经验很重要做任何状态类业务都适用。6.2 后续可以这样扩展从练手项目到接近商用这个系统的扩展空间非常大每一步都可以作为一个独立的学习题。热度最高的方向是引入Redis把甜品窗口的热门菜品、食堂列表做缓存并处理登录token的分布式存储引入消息队列下单成功后发送通知给食堂窗口超时订单通过延迟队列自动取消把前端换一套移动端适配方案或者直接做小程序端复用后端接口。技术上更进一步的话可以接入真实的微信支付、对接第三方地图做配送路径规划在菜品推荐上根据历史订单做简单的协同过滤给每个学生推荐口味接近的菜。这些方向随便选一个深入下去都能把项目从“课设水平”提升到“简历上有话可说的水平”。我在实际开发中最大的体会是做这种系统不要想着把所有功能一次性堆上去先把核心主流程跑通让一个用户可以完整地走完从点餐到收货的全过程再去优化细节、增加功能。后面每次加新模块前先问问自己它是不是真正服务了这条主链路如果不是优先级往后放。希望这篇复盘能帮你少走一些我走过的弯路动手做的时候遇到问题也别慌数据库加条索引、前端改个响应式处理、后端加个状态校验都是正常操作跑起来就赢了。