ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue外卖系统课设全指南:从框架搭建到答辩避坑

SpringBoot+Vue外卖系统课设全指南:从框架搭建到答辩避坑 简介一套基于 SpringBoot 与 Vue 的外卖管理系统源码及配套数据库目标是帮助 Java 课程设计、期末大作业和毕业设计阶段的学生以较低门槛完成一个前后端分离且能稳定运行的管理系统。压缩包共 192 个文件整体 26.55MB内部包含 73 个 Java 源工程文件、20 个 HTML 页面、21 个 JavaScript 脚本、17 个 CSS 样式表、SQL 数据库脚本及常用配置文件后端业务逻辑与前端交互页面一一对应从建表脚本到接口访问再到页面渲染形成闭环目录结构清晰便于定位用户、菜品、订单等模块进行学习或修改。资源目前已有 346 人学习下载适合参考已有完成度较高的课设项目来缩短开发周期也适合在此基础上深挖代码细节准备答辩。核心价值在于代码完整、可直接运行项目中实现了菜品分类、购物下单、订单管理等外卖场景注释较详细纯手打原创结构清晰导入数据库、启动后端与前端即可在浏览器访问支持二次改造例如增加支付网关、数据统计、权限控制等功能满足课程设计的验收要求。1. 这门课设到底在评什么SpringBootVue外卖系统的及格线如果你是在赶 Java 课程设计手里刚好拿到一份「外卖管理系统」的 SpringBootVue 源码和数据库先别急着解压跑起来。课程设计和真实项目最大的区别是老师不关心你的系统能扛多少并发关心的是「技术栈全不全、业务流程通不通、答辩讲不讲得清」。SpringBoot 负责后端接口和业务逻辑Vue 负责前端页面和数据展示MySQL 存业务数据这条线本身就是 Java 课设的标准高分组合。这套系统的核心价值在于它不是一个 CRUD 堆砌的 demo而是覆盖了用户端下单、商家端接单、管理端维护菜品和订单状态的完整闭环。适合 Java 基础语法学完、正在做课设或准备 SpringBootVue 全栈入门的人拿来改造成自己的项目。拿到手第一件事不是看代码而是先看它能不能跑通「用户注册登录 → 浏览菜品 → 下单 → 商家接单 → 订单状态流转」这条主线。2. 先立框架SpringBootVue外卖系统的技术版图与三层选型2.1 前后端分离的划分为什么 Controller 不写业务外卖系统的代码结构通常分成三层Controller 只接收请求和返回结果Service 写真正的业务逻辑Mapper 负责和数据库打交道。很多课设代码最大的问题是把 SQL 拼在 Controller 里老师一问「订单金额怎么算的」就答不上来因为逻辑全黏在接口层。我拿到一套源码后会先看包结构Controller、Service、Mapper 分得清不清是第一个评分点。Controller 里应该是干净的接收参数、调用 Service、封装 Result 返回。Service 里才是下单、结算、状态流转这些核心逻辑。判断代码写得好不好有一个土办法——看一个下单接口的代码量如果 Controller 里超过二十行还要自己拼 SQL这代码基本不能给老师看。在真正动手改代码之前先确认 Maven 依赖里有没有 lombok如果没有实体类的 getter/setter 会写到你怀疑人生。SpringBoot 版本一般 2.x 就够配上 MyBatis-Plus 可以少写很多 XML 映射。数据库连接池用 Druid 还是 HikariCP 都行HikariCP 是 SpringBoot 默认的不需要额外配置就能跑课设答辩时被问到「连接池用的什么」可以直接答 HikariCP。2.2 数据库之外的隐藏选型JWT、拦截器、跨域外卖系统绕不开登录鉴权。课程设计里最常见的方案是 JWT后端登录成功后签发一个 token前端每次请求带上这个 token后端通过拦截器验证。比 Session 好讲也比 Session 好演示——你可以在答辩现场打开浏览器控制台把 token 删掉再刷新页面页面报 401 时老师能直观看到鉴权确实生效了。拦截器要放行登录接口、注册接口和菜品浏览接口其他接口都要验证 token。有个很容易翻车的点Vue 的 devServer 默认端口是 8080SpringBoot 默认也是 8080不改端口必冲突前端要多配一个代理把 /api 开头的请求转发到后端真实端口。另一个坑是跨域前端 8081 访问后端 8080 会触发 CORS 拦截解决方式是后端加一个 WebMvcConfigurer 配置允许跨域或者在前端 devServer 里配 proxy。Vue 端还要留意路由和状态管理。外卖系统的前端通常有用户端和管理端两套页面路由守卫需要根据登录状态和角色跳转登录页。axios 拦截器统一处理 token 注入和 401 响应这样前端的请求代码会干净很多。拿到的源码里如果有 vuex 或 pinia 的配置先看用户信息存在哪——存在 localStorage 里刷新不会丢存在内存里一刷新就没了这个细节答辩时经常被问到。3. 数据库设计把外卖业务的「关系」画清楚3.1 核心表结构用户、商家、菜品、订单外卖系统最核心的数据库表是用户表、商家表、菜品表、订单表和订单明细表。用户表要区分普通用户和管理员一般用 role 字段1 是用户、2 是商家、3 是管理员。商家表直接做成用户表的一个角色还是单独建表取决于源码的设计思路——独立建表的好处是商家可以有自己的营业时间、店铺公告这些字段封装成一个 user 表加一个 shop 表在答辩时更好讲。菜品表要绑定商家 ID这样商家登录后只能查到自己店铺的菜品。菜品字段里必有的是名称、图片、价格、分类、是否上架。图片建议存相对路径而不是完整 URL否则换一台电脑跑项目时图片全挂。订单表是整张数据库设计的核心需要包含下单用户 ID、商家 ID、订单金额、订单状态、下单时间、收货地址和联系电话。订单明细表单独建——别把菜品快照塞在订单表里因为订单里的价格是下单那一刻的价格菜品表的价格后续可能改。订单明细表记录菜品 ID、菜品名称、单价、数量这样订单历史里的金额永远是对的。数据库文件里一般会带一份初始化 SQL看建表语句时重点看外键和索引外键在课设里可以有但实际开发基本不用MyBatis-Plus 删数据时外键约束反而碍事。3.2 订单状态机的字段设计状态字段与时间戳外卖订单的状态流转是一条完整链路待支付 → 已支付/待接单 → 商家已接单 → 配送中 → 已完成另外还有已取消。源码里一般用一个整数字段 status 表示0 待支付、1 已支付、2 已接单、3 配送中、4 已完成、5 已取消。答辩时把这张表讲清楚老师就知道你不是只会增删改查。设计上有一个常见的坑状态字段只记录了「当前状态」没有记录「状态变更时间」导致演示时无法回答「这单是什么时候被商家接单的」。所以订单表里要么加多个时间字段比如 pay_time、accept_time、finish_time要么建一张订单状态日志表每一步操作都插入一条记录包含订单 ID、原状态、新状态、操作时间。后者设计上更工整课程设计用前者居多因为简单直接。另一个要提前想好的问题是并发下的库存扣减。如果订单表和菜品表是分开的下单时先查菜品库存再扣减如果两个用户同时下同一道菜会出现超卖。课设里一般不会压测并发但代码里至少要用事务——下单方法上加 Transactional把扣库存和生成订单放在同一个事务里任何一步失败就回滚。这个点在答辩现场一亮出来比单纯演示功能得分高得多。3.3 初始化数据让演示页一打开就有东西可看数据库脚本里除了建表语句一定还要有 INSERT 语句。没有初始化数据的外卖系统用户登录进去看到的是空荡荡的页面没法演示。我检查源码时第一件事就是数初始化数据量用户有几个、商家有几家、菜品有几道、有没有已完成的订单。太少了不行太少没法展示分页太多了也不行几百条垃圾数据会让查询变慢答辩时如果等列表加载观感很差。初始化数据的质量比数量重要。菜品图片路径要真实可访问价格要合理分类要覆盖「热销」「主食」「饮品」等。密码字段如果是 MD5 加密初始化数据里的密码必须是加密后的值否则你登录不进去。还有一点容易漏外卖系统的用户角色有三种初始化数据里三种角色都要有并且密码要统一告诉答辩老师比如都是 123456省得现场登录时试密码浪费时间。分页查询在课设里是必考项。菜品列表、订单列表都需要分页MyBatis-Plus 自带分页插件配置一个 MybatisPlusInterceptor 就行。数据库设计阶段就要想到分页字段的设计——order by 排序字段要稳定如果列表里有两个订单的下单时间完全相同不分页时不会暴露问题分页后会出现重复数据。建议订单表里加一个自增 ID 作为排序兜底字段下单时间相同就按 ID 倒序。4. 核心代码落地从登录鉴权到下单全链路4.1 登录与 JWT 拦截器课程设计里最常见的翻车点先写登录接口这是外卖系统所有功能的前置。用户输入用户名和密码后端校验通过后签发 JWT 返回前端。JWT 生成的代码一般用 jjwt 库非常短// 生成 JWT有效期 2 小时携带用户 ID 和角色 String token Jwts.builder() .setSubject(String.valueOf(user.getId())) // 主题存用户 ID .claim(role, user.getRole()) // 自定义字段存角色 .setExpiration(new Date(System.currentTimeMillis() 7200000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 用同一个密钥签名 .compact();这段代码里最容易翻车的是 SECRET_KEY 写得太短jjwt 要求 HS256 的密钥长度至少 256 位随便写一个「abc」会直接抛 WeakKeyException。密钥要单独放在常量类里不要散落在各个 Controller。另外签发 token 时把用户 ID 放到 Subject 里后面拦截器解析时直接用这是贯穿全系统的通行证。拦截器验证 token 的逻辑要写在 HandlerInterceptor 的 preHandle 里Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); } try { Claims claims Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); request.setAttribute(userId, claims.getSubject()); return true; // 验证通过放行 } catch (Exception e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); return false; } }这个拦截器里有三个细节要讲清楚。第一前端传的 token 一般带 Bearer 前缀后端切片时要先判断再截取不判断直接 substring 会越界报错。第二验证失败返回 401 时一定要设置 ContentType 为 JSON不然前端收到的是 HTML 错误页axios 统一处理时拿不到错误结构。第三请求里设置 userId 属性后Controller 里用 RequestAttribute 就能拿到当前登录用户——比如下单接口就是拿这个 userId 作为订单的用户 ID。拦截器还要注册到 WebMvcConfigurer 里并且指定放行路径Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor()) .addPathPatterns(/**) // 拦截所有请求 .excludePathPatterns(/api/user/login, /api/user/register, /api/shop/list, /api/dish/list); // 放行登录注册和浏览接口 }放行路径写少了登录页都打不开放行路径写多了未登录也能访问订单接口鉴权形同虚设。我一般会在放行列表里把用户端需要公开访问的接口列全其余统统拦截然后用一个多小时把所有接口逐个点一遍确保没有漏网之鱼。检查方法很简单登录一个用户把浏览器控制台里的 token 手动删掉然后逐个点击前端功能——所有需要登录的请求都应该返回 401。4.2 下单流程事务、库存扣减与订单明细下单是外卖系统的核心链路。前端的购物车数据传到后端后后端要做四件事校验菜品是否存在且已上架、计算订单总金额、扣减菜品库存、生成订单主表和订单明细表。这四步必须在一个事务里Transactional public Order createOrder(Long userId, CreateOrderRequest request) { // 1. 根据购物车里的菜品 ID 批量查询菜品 ListDish dishList dishMapper.selectBatchIds(request.getDishIds()); if (dishList.size() ! request.getDishIds().size()) { throw new BusinessException(部分菜品已下架请刷新购物车); } // 2. 计算总金额并扣减库存逐条操作方便定位问题 BigDecimal total BigDecimal.ZERO; for (OrderItemDTO item : request.getItems()) { Dish dish dishMap.get(item.getDishId()); if (dish.getStock() item.getQuantity()) { throw new BusinessException(菜品[ dish.getName() ]库存不足); } dish.setStock(dish.getStock() - item.getQuantity()); dishMapper.updateById(dish); // 生成订单明细 OrderDetail detail new OrderDetail(); detail.setOrderId(orderId); detail.setDishId(dish.getId()); detail.setDishName(dish.getName()); detail.setPrice(dish.getPrice()); detail.setQuantity(item.getQuantity()); orderDetailMapper.insert(detail); total total.add(dish.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity()))); } // 3. 生成订单主表 Order order new Order(); order.setUserId(userId); order.setShopId(request.getShopId()); order.setTotalAmount(total); order.setStatus(0); // 待支付 order.setCreateTime(new Date()); orderMapper.insert(order); return order; }这段代码里最容易漏的是金额计算。数据库里的金额字段我建议用 BigDecimal 而不是 doubledouble 算钱会出现 0.10.20.30000000000000004 这种问题答辩时演示算错金额属于硬伤。计算总价时用 BigDecimal 的 multiply 方法不要用 double 累加。库存扣减有个隐藏问题上面这段代码是先查库存再扣减在并发下会超卖。课程设计不会做并发压测但你可以在代码里用「乐观锁」的方式主动展示给老师看——更新库存的 SQL 加一个 where stock quantity 条件影响行数为 0 就说明库存已被别的订单扣完了。这个设计只需要改一条 SQL扣减的代码不用动。下单接口还有一个容易被忽略的点订单 ID 的生成。自增 ID 虽然简单但订单号如果直接返回给用户演示时一眼就能看出系统一天的订单量。课程设计阶段用自增没问题但如果想让订单号更「像真的」可以用时间戳加随机数生成一个 20 位以内的订单号。注意订单号字段别设计成 int要用 varchar否则数字溢出直接报错。4.3 Vue 端路由与接口对接axios 封装和动态菜单前端从登录页接登录接口开始。axios 需要统一封装否则每个页面都要写一遍 token 注入逻辑。先看 axios 实例的创建// src/utils/request.js import axios from axios import router from /router const request axios.create({ baseURL: /api, // 通过 devServer 代理转发避免跨域 timeout: 10000 }) // 请求拦截器自动携带 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理 401 和业务错误码 request.interceptors.response.use( response { const res response.data if (res.code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(未登录)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } ) export default requestbaseURL 写成 /api 而不是直接写后端地址是因为 Vue 的 devServer 可以把 /api 代理到 SpringBoot 的 8080 端口。这样做的好处是前端代码里不需要写死后端地址将来部署时直接把 /api 指到不同的环境。devServer 配置在 vue.config.js 里// vue.config.js module.exports { devServer: { port: 8081, // 前端端口避免和后端冲突 proxy: { /api: { target: http://localhost:8080, // 后端 SpringBoot 地址 changeOrigin: true } } } }前端路由需要分成用户端和管理端两块。用户端有首页、菜品列表、购物车、订单列表、个人中心管理端有菜品管理、订单管理、数据统计。路由守卫按角色控制访问// src/router/index.js router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.path /login) { next() // 登录页永远放行 } else if (!token) { next(/login) // 没有 token 一律回登录页 } else if (to.meta.requiresAdmin role ! 1) { next(/) // 访问管理端但没有管理员角色打回首页 } else { next() } })动态菜单的常见做法是登录后根据 role 字段渲染不同的菜单项。用户端和管理端共用一套布局组件菜单数据从后端接口拉取前端根据 role 过滤。这个方案比写两套页面省事而且演示时切换账号用同一个浏览器就能快速演示两个身份。5. 课程设计避坑清单答辩前必须检查的 5 个问题5.1 端口冲突与数据库版本不一致现象启动 SpringBoot 后控制台报 Port already in use或者启动成功但页面转圈加载不出来。原因SpringBoot 默认 8080如果本机装了其他 Java 服务占用了 8080或者前端 Vue 和后端 SpringBoot 都用了 8080后端起不来前端请求也全失败。数据库版本不一致则表现为启动时报 Unknown database 或 SQL 语法错误比如本机 MySQL 是 5.7源码 SQL 里用了 8.0 的语法。解决先把 application.yml 里的端口改成一个不常用的比如 8088再在前端 devServer 的 proxy 里同步改成 8088。数据库方面先查本机 MySQL 版本如果源码 SQL 运行报错把报错语句单独拎出来在 Navicat 里跑一遍能定位是语法问题还是字段类型问题。实在不兼容就重建表结构用建表语句同步出的表结构代替原有 SQL。5.2 菜品图片加载不出来现象列表页菜品名称、价格都正常只有图片区域是空的或裂图。原因数据库里存的图片路径是绝对路径比如C:/Users/xxx/Pictures/或者作者本机的地址。换一台电脑跑项目路径就失效了。另一种情况是图片来源用了外链网络不稳定时加载慢或被拦截。解决把所有图片路径改成相对路径图片文件放到 SpringBoot 的 src/main/resources/static 目录下数据库里只存/images/xxx.jpg这种相对路径。前端拼接时用 window.location.origin 或者直接用 Vue 的 baseURL。检查方法是打开浏览器 F12 看图片请求的完整 URL如果 URL 里包含 localhost:8080 以外的地址就要改。5.3 下单不扣库存、金额对不上现象下单成功后订单列表能看到订单金额也和前端显示一致但菜品表的库存没变。或者订单总金额比菜品单价乘以数量少了一部分。原因下单的事务没生效或者扣库存的代码写在了事务之外。金额对不上通常是前端计算总价传给后端后端直接信任前端传的 totalAmount 没有自己重新计算。如果前端的计算逻辑有 bug或者有人用 Postman 直接调接口传了个负数金额数据库里就会留下脏数据。解决先检查 Service 方法上有没有 Transactional 注解没加就加上加了还是不行检查是不是同类内部调用——同类里 A 方法调用 B 方法时事务注解不生效。金额必须后端重新计算前端传的 totalAmount 直接丢弃。校验逻辑写成遍历订单明细计算出总价和前端传的值比对不一致直接拒绝下单并把提示返回给前端。5.4 打包部署后页面白屏现象本地开发环境 npm run serve 一切正常但是 npm run build 之后把 dist 目录丢给老师打开 index.html 是白屏。原因Vue 打包后的静态资源路径默认是绝对路径/js/xxx.js在本地直接打开文件时找不到。另外前端打包后访问后端接口的地址写死了 localhost:8080老师换了电脑就请求不到。解决在 vue.config.js 里设置publicPath: ./让打包后的资源相对当前路径加载。接口地址改成相对路径 /api这样前端和后端部署在同一个域名下就能直接工作不用关心对方在什么端口。交付课程设计时把前后端的启动说明写清楚MySQL 导入 SQL → 启动 SpringBoot → 启动 Vue三步做完就能看到完整系统。5.5 演示现场数据准备不充分现象答辩演示时登录页进去了但首页空荡荡打开订单列表没有数据可以点现场网络不好图片一直加载。原因初始化数据太少的锅。设计数据库时只保证了功能能跑没有考虑演示效果。老师想看的是点开每个页面都有内容可操作而不是看着空列表无语。解决演示前在系统里手动补一套完整流程的数据注册一个用户 → 下一单 → 用商家账号接单 → 用管理员账号查看所有订单。这套数据留在数据库里不要删。同时提前把菜品图片准备好把初始化 SQL 里所有菜品都配上图片确保离线状态也能正常展示。最后在演示电脑上完整走一遍「用户下单选菜 → 提交订单 → 商家接单 → 完成订单」确认每个按钮都有响应。6. 让演示「说人话」评分视角下的三个加分技巧第一个加分技巧是「讲状态流转而不是讲 CRUD」。外卖系统的订单状态从待支付到已完成要经历多个环节普通人的演示只是在页面上点按钮高分演示是先在黑板上画出状态流转图然后告诉老师「这一步操作对应 status 从 2 变成 3代码在 OrderServiceImpl 的第几行」——让老师觉得你对业务链路有全局理解而不是会调用几个现成接口。第二个技巧是「主动暴露一个设计缺陷并给出改进方案」。比如在答辩时主动说「这个系统的库存扣减在高并发下可能超卖我目前用的是先查后扣如果产量化可以考虑用乐观锁加 version 字段SQL 改为 update dish set stock stock - 1 where id ? and stock 1」。这句话一出来比回答完所有提问都加分因为它展示了你对代码边界和优化方向的理解这是课程设计评分里「设计思想」那一档的分数。第三个技巧是「把数据库设计讲成业务故事」。讲解订单明细表时不要说「这张表有 5 个字段」而是说「订单表只存总金额和状态订单明细表存的是下单那一刻的菜品快照因为菜品价格可能会调整但历史订单必须保持原价」。这种讲法让老师知道你不是抄的表结构而是真的理解为什么订单主表和明细表要拆分。这三个技巧本质上都是把「你做了什么」转成「你理解了为什么这么做」哪怕代码是别人写的只要你能把这个「为什么」讲透这个系统在答辩语境下就是你的。我个人的习惯是答辩前一天把所有接口用 Postman 重新打一遍用 Excel 列一个「接口地址、参数、预期结果、实际情况」的对照表哪个接口返回异常当场修不留到答辩现场翻车。课设这东西功能做出来只是及格把设计思路讲得让老师点头才算真正做完。希望这些踩过的坑和总结的技巧能帮到你祝答辩顺利。本文还有配套的精品资源点击获取
返回列表