ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue网上点餐系统毕设全攻略:从数据库设计到答辩高分

SpringBoot+Vue网上点餐系统毕设全攻略:从数据库设计到答辩高分 简介基于SpringBoot与Vue.js的网上点餐系统毕业设计资源面向计算机相关专业本科生适用于课程设计、期末实践或毕业设计参考能帮助理解前后端分离项目的整体开发流程。系统以高分通过评审并在两代Windows系统上完成部署验证压缩包内整合了后端源码、前端页面、数据库脚本、设计论文、答辩演示文稿、部署指南以及操作录屏内容覆盖从系统设计到运行实现的全过程。整个资料包共九百一十五个文件约六十六兆含源码、样式、脚本、配置、备份、文档和视频等类型其中前端资源与后端逻辑分层明显样式、脚本与备份文件均按功能模块归位便于查阅。已有二十九人学习。读者可参照批处理启动命令和演示视频快速复现再结合论文与部署文档掌握数据库初始化、接口调用及前后端交互细节对同类课题的二次开发、课程教学以及扩展改造均具有较高参考价值。1. 网上点餐系统毕业设计SpringBoot与Vue为什么能拿高分每年毕业设计选题网上点餐系统都是被问得最多的方向之一但多数人交上去的版本只有一张菜品列表加一个“下单”按钮答辩一问就接不住话。真正能拿到高分的作品靠的是在 SpringBoot 与 Vue 这套前后端分离组合上把用户、菜品、购物车、订单、状态流转、后台管理这些环节串成完整闭环SQL 脚本、接口文档、演示流程这些配套资源一个不少。这个方向适合想用可控成本体验一遍“真实企业项目”的本科生也适合准备春招前拿开源项目练手的前端同学。接下来就按我自己的落地顺序把设计思路、复现步骤和踩坑记录一次讲透。2. 先把骨架立住点餐系统的数据模型与SpringBoot后端设计很多网上点餐系统源码看起来像静态网页根因是数据库只有菜品表和订单表两张表。真正到了答辩现场老师追问“购物车数据存哪里”“取消订单后库存怎么算”就卡壳。所以这一章先把数据模型拆开讲再落到三层代码怎么写最后讲订单状态机这个高频追问点。2.1 数据表怎么拆用户、菜品、购物车、订单明细的关系常见做法是拆六张表左右而不是三张表。用户表单独存在因为点餐系统虽然看起来只有普通用户但复用到商家端时role 字段决定你能进哪个后台页面。菜品表要挂分类但不挂商户毕设场景里商户就是管理员自己拆商户表会增加不必要的联表复杂度。购物车单独建表而不是存前端 localStorage原因后面讲并发超卖时会明白后端能看到真实库存和真实下单意图才能做校验。订单这里要特别注意拆成主表和明细表。订单主表存用户、总额、状态订单明细表存每道菜的 ID、名称、单价快照、数量。如果你图省事在订单主表里塞一个 JSON 字段存菜品列表那“统计某个菜卖了多少份”这种答辩常问的指标就完全算不出来老师一听就知道你没做过真实报表。-- 用户表一个表同时支持普通用户和商家管理员 CREATE TABLE user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(64) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(128) NOT NULL COMMENT BCrypt加密后的密码, nickname VARCHAR(64) DEFAULT COMMENT 显示名, role TINYINT NOT NULL DEFAULT 0 COMMENT 0-用户 1-商家管理员, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 用户表; -- 菜品表分类ID直接挂在菜品上避免多一张关联表 CREATE TABLE dish ( id BIGINT AUTO_INCREMENT PRIMARY KEY, category_id BIGINT NOT NULL COMMENT 关联分类, name VARCHAR(128) NOT NULL COMMENT 菜品名, price DECIMAL(8,2) NOT NULL COMMENT 售价, image_url VARCHAR(255) DEFAULT , status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, stock INT NOT NULL DEFAULT 0 COMMENT 库存 ) COMMENT 菜品表;这里有两个字段值得你在答辩时主动讲password 用 VARCHAR(128) 是因为 BCrypt 加密结果固定 60 字符左右留了富余price 用 DECIMAL 而不是 FLOAT因为金额用浮点会出现 0.1 加 0.2 不等于 0.3 的精度问题演示时万一总金额对不上会非常尴尬。status 字段也不要省下架菜品不删记录只改状态这背后是“软删除”思想比物理删除更稳。订单主表和明细表是典型的主从关系。明细表里的 price 存的是下单时的快照价不是实时去菜品表 join。否则菜品改价之后历史订单的金额账目就跟着变了这属于隐藏 bug真实业务里绝对不允许。dish_name 也冗余到明细表按“空间换时间”的思路保留一份既避免菜品改名后历史订单失真又省一次联表查询。-- 订单主表 CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单号对外展示用, user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2制作中 3已完成 4已取消, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 订单主表; -- 订单明细表 CREATE TABLE order_item ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL COMMENT 关联orders.id, dish_id BIGINT NOT NULL, dish_name VARCHAR(128) NOT NULL COMMENT 冗余菜名防止历史订单失真, price DECIMAL(8,2) NOT NULL COMMENT 下单时单价快照, quantity INT NOT NULL DEFAULT 1 ) COMMENT 订单明细表;order_no 单独建而不是直接用自增 id 对外展示是为了生成类似“20250601001”的业务号这在演示订单列表时更真实。你可以用日期加随机数拼也可以用数据库序列但不要在 Controller 里散落生成逻辑封装成一个工具方法更好。能说清楚“订单号为什么不能直接用 id”的人答辩印象分会高一个档。2.2 SpringBoot三层架构Controller、Service、Mapper的职责边界网上点餐系统的代码量不大但分层必须干净。常见做法是 Controller 收参、Service 写业务、Mapper 写 SQL。很多毕设源码把业务逻辑写在 Controller 里代码是少了几行但答辩时老师一问“事务在哪控制的”就卡壳。RestController RequestMapping(/api/order) public class OrderController { Autowired private OrderService orderService; // 创建订单接收DTO不写业务 PostMapping(/create) public Result create(RequestBody Valid OrderCreateDTO dto) { OrderVO vo orderService.createOrder(dto); return Result.ok(vo); } }Controller 只做两件事接收请求参数、调用 Service。DTO 里的 Valid 触发参数校验比在方法里手写 if 判断整洁。Result 是统一返回包装把 code、msg、data 固定成 JSON 结构前端 axios 拦截器才能统一处理错误码。如果你拿到的源码里没有这个包装类建议自己补上这是企业级项目的基础规范。Transactional public OrderVO createOrder(OrderCreateDTO dto) { // 1. 查购物车空车直接抛业务异常 ListCartItem cartItems cartMapper.selectByUserId(dto.getUserId()); if (cartItems.isEmpty()) { throw new BizException(购物车是空的); } // 2. 计算总额用BigDecimal不用double BigDecimal total cartItems.stream() .map(item - item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); // 3. 插订单主表 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); // 4. 把购物车每一项转成订单明细 for (CartItem item : cartItems) { OrderItem detail new OrderItem(); detail.setOrderId(order.getId()); detail.setDishId(item.getDishId()); detail.setDishName(item.getDishName()); detail.setPrice(item.getPrice()); detail.setQuantity(item.getQuantity()); orderItemMapper.insert(detail); } // 5. 清空购物车 cartMapper.deleteByUserId(dto.getUserId()); return buildVO(order); }Transactional 是这段代码的核心第 2 到第 5 步任一步失败整个订单都不落库。常见误用是只在单条 insert 上加大事务多表写入时会出现订单建了、购物车没清的问题。另外注意购物车里的单价从哪里来我建议购物车表字段本身就存单价快照这样前端展示价格稳定下单时按快照算总额和菜品实时价格脱钩。这一步做好你就能避免“购物车加购时的价格和下单时价格不同”的尴尬。2.3 订单状态机状态字段的流转规则点餐系统最容易“伪”的地方就是订单状态只有“未支付/已支付”中间过程全靠前端页面自己猜。常见的做法是把状态定义成枚举并明确每一状态允许流向哪里这既是业务规则也是答辩台上的展示点。状态值状态名允许流向触发动作0待支付1、4用户支付 / 超时取消1已支付2、4商家接单 / 用户申请退款2制作中3商家出餐3已完成无用户确认收货4已取消无终态这个表意味着后端不能用一次 update 从 0 直接跳到 3。想实现初恋最直接的方式是加一个“条件更新”而不是先查再改。条件更新的核心原理放在这里状态变化的动作也集中在一处而不是散落在各个 Controller 里。public void updateStatus(Long orderId, int fromStatus, int toStatus) { int rows orderMapper.compareAndSetStatus(orderId, fromStatus, toStatus); if (rows 0) { throw new BizException(订单状态已变化请刷新后重试); } }对应的 SQL 在 Mapper 里这样写UPDATE orders SET status #{toStatus} WHERE id #{orderId} AND status #{fromStatus}WHERE 里的 fromStatus 是乐观锁思路。在点餐场景里用户和商家同时操作同一个订单不加这句校验会出现“用户点了取消商家同时点了接单最终状态由后写入者说了算”的翻车现场。这道逻辑值得你单独在答辩时讲 3 分钟从“为什么不用 Redis 锁”到“为什么这里条件更新就够了”能讲清楚说明你真的理解了并发控制的门槛。3. 把整套系统跑起来环境准备、源码导入与前后端联调很多同学下载源码后卡在第一步项目跑不起来。问题大多不是代码坏了而是 JDK 版本和 SpringBoot 版本不匹配、Node 版本太高导致依赖装不上、MySQL 连接串写错。下面按我自己的启动顺序来梳理照着做二十分钟内能看到登录页。3.1 环境版本怎么选JDK、MySQL、Node.js的搭配建议拿到源码后先看 README 里的环境要求没有 README 就打开 pom.xml 看 spring-boot-starter-parent 的版本号。SpringBoot 2.x 推荐 JDK 8 或 11不要直接上 17。SpringBoot 2.x 在 JDK 17 下能跑但老版本 Lombok 会报一堆 module 错误初学者排查起来非常折磨这玩意有时候确实有点玄学。如果是 SpringBoot 3.xJDK 17 是硬性要求别用 8 去试。MySQL 建议 8.0 以上5.7 也能用但两者驱动名不一样5.7 用 com.mysql.jdbc.Driver8.0 用 com.mysql.cj.jdbc.Driver。源码里如果写的是前者而本机是 8.0启动会直接报驱动类找不到。遇到这种问题把配置统一换成 8.0 的驱动名最省事。Vue 这块Vue 2 配 Node 14 或 16Vue 3 配 Node 16 或 18尽量不要用最新的 Node 20 以上跑老项目node-sass 这类原生模块大概率装不上。判断方法很简单看 package.json 里 vue 字段是 2.x 还是 3.x。装依赖如果报 ERESOLVE 错误常见做法是先删掉 node_modules 和 package-lock.json再重新 npm install。组件推荐版本说明JDK8 或 11SpringBoot 2.x/ 17SpringBoot 3.x以 pom.xml 为准MySQL8.0驱动用 com.mysql.cj.jdbc.DriverNode.jsVue2 用 14/16Vue3 用 16/18别图新稳定优先Maven3.6IDEA 自带 Maven 也能用开发工具IDEA / VS Code后端建议 IDEA 社区版以上这里多提醒一句环境版本和別人发来的源码不一致时改环境比改源码更快。别为了迁就一个老项目去装老 JDK优先看源码能不能调整依赖版本。3.2 后端启动全流程创建数据库、改配置、跑通第一个接口第一步创建数据库。源码包一般会附带 SQL 脚本常见文件名是 init.sql 或 ordering.sql。不要直接在可视化工具里双击运行先建库指定 utf8mb4再导入脚本能规避后面大部分中文乱码问题。mysql -u root -p -e CREATE DATABASE IF NOT EXISTS ordering_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql -u root -p ordering_system init.sqlutf8mb4 和 utf8mb4_unicode_ci 是配套使用的。前者支持 emoji 和生僻字符后者决定排序规则若菜品名带特殊符号用老的 utf8 三字节编码会直接存不进去。这是源码里最隐蔽的坑如果你发现导入报错和字符集相关优先检查这里。第二步改后端配置。打开 src/main/resources/application.yml重点盯 spring.datasource 这一段。spring: datasource: url: jdbc:mysql://localhost:3306/ordering_system? useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.DriverserverTimezone 是高频坑。MySQL 8.0 默认时区和本地不一定一致不加这个参数启动时要么直接报错要么所有时间字段差 8 小时。useSSLfalse 让本地调试不走证书校验减少一次握手失败。driver-class-name 直接写 8.0 的驱动名避免 5.7 和 8.0 混用。第三步启动后端。在 IDEA 里打开 pom.xml 等 Maven 下载依赖找到带 SpringBootApplication 注解的主类右键运行。控制台出现 Started 开头的一行日志就算成功。验证接口可以直接在浏览器访问一个 GET 接口比如 http://localhost:8080/api/dish/list返回 JSON 数组说明后端链路通了。如果不想把后端当黑匣子可以提前打开日志级别把 SQL 输出打开这样每个接口请求对应哪些 SQL 都看得见排查问题会容易得多。3.3 前端启动全流程安装依赖、配置接口转发、打开登录页后端起来了前端才能进入联调阶段。先进入前端目录项目里通常叫 front 或 vue-web。cd front npm installnpm install 卡住时优先怀疑网络源的问题。常见做法是用 npm config set registry 换成国内镜像然后重新安装。装完依赖后不要直接点运行先把开发服务器的接口转发配上否则前端请求 /api 永远打不到后端。在 Vue CLI 项目里配置 vue.config.jsVite 项目则配置 vite.config.js作用相同module.exports { devServer: { port: 8000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };这段配置把 /api 开头的请求转发到后端的 8080 端口changeOrigin 让后端收到请求时认为是来自 8080 而不是 8000避免因为端口不同产生跨域拦截。注意这里的 proxy 是开发服务器的接口转发和后端的 CORS 配置解决的是两类问题开发阶段用 devServer 转发最省事生产阶段用 Nginx 转发后端跨域注解只是兜底方案。前端起服务的命令一般是 npm run serve 或 npm run dev。启动后访问 http://localhost:8000能看到登录页用 SQL 脚本里初始化的账号登录就能看到菜品列表和购物车。到这里前后端已经打通后续再做页面改动都会经过这套路径。3.4 配套资源的使用顺序SQL脚本、接口文档、演示视频怎么用标题里既然写了“配套资源”这部分要提醒你按顺序使用。先看压缩包结构常见资源包括数据库脚本、接口文档、演示视频、说明文档。建议顺序是第一步看 README 确定环境版本第二步执行 SQL第三步启动后端并用浏览器调通一个接口第四步启动前端联调。很多人跳步先跑前端页面空白或接口失败就开始怀疑代码改来改去最后发现是数据库没导入。按上面顺序走每一步都有明确验证点出了问题也知道回退到哪一步。接口文档是联调时的对照表看它比看代码快得多每个接口的路径、请求方式、参数结构、返回结构都在里面。演示视频的用处是帮你快速理解完整流程别把它当成全部也别指着它来答辩。4. 让系统在答辩现场不翻车5个必须提前排掉的坑这一章的系统都在本地跑通了但离“答辩现场稳定演示”还有距离。下面五个坑是我见过的复现率最高的问题每个按“现象 → 原因 → 解决”整理你可以在答辩 PPT 的“项目中遇到的问题与解决”一页直接引用。4.1 前端一调接口就报跨域错误CORS问题怎么排查现象后端单独启动后浏览器访问接口正常前端一启动F12 里同一个接口变红控制台报 Access-Control-Allow-Origin 相关错误。原因前端页面跑在 8000 端口后端在 8080端口不同就构成跨域浏览器默认拦截响应。这不是后端接口挂了而是浏览器安全行为。解决开发阶段用 vue.config.js 的 proxy 转发如果非要直接跨域访问后端加一个 CORS 配置类。Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意这里用 addAllowedOriginPattern()。SpringBoot 2.4 之后再用 addAllowedOrigin() 会与 allowCredentials 冲突导致请求直接失败。这段代码只在开发环境需要前端构建后打进后端 jar 就没有跨域问题了所以不要满项目到处贴 CrossOrigin 注解配置一个全局过滤器就够了。4.2 菜品中文名全部变成问号字符集层层排查现象后端日志里查出来的菜品名正常前端页面显示成 ??或者反过来数据库里看全是问号。原因库、表、连接串、前端页面四处里某层字符集不一致。最常见的是建库时用了默认 latin1或者连接串没带 characterEncodingutf8。解决按数据库、连接串、前端三处逐层排查。先确认数据库和表的字符集。SHOW CREATE DATABASE ordering_system; SHOW CREATE TABLE dish;这两条命令会显示当前库和表的 charset。如果不是 utf8mb4用下面的语句转换ALTER DATABASE ordering_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE dish CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;连接串再核对一遍必须包含 useUnicodetrue 和 characterEncodingutf8。注意 MySQL 8.0 里 utf8 实际是 utf8mb3 的简称存 emoji 会失败直接写 utf8mb4 更稳妥。前端页面如果用了外链 JS 文件也要保证文件本身是 UTF-8 编码IDE 右下角可以切换。4.3 并发下单把库存扣成负数条件更新的原子性现象两个人同时点同一道限量菜都看到有库存都下单成功最后数据库里这道菜库存变成负数。原因扣库存逻辑是“先查出库存Java 里判断够不够再 UPDATE”两个请求都通过了判断然后依次扣减。解决把判断放进 SQL 的 WHERE 条件里。public boolean deductStock(Long dishId, Integer quantity) { int rows dishMapper.deductStockIfEnough(dishId, quantity); return rows 0; }UPDATE dish SET stock stock - #{quantity} WHERE id #{dishId} AND stock #{quantity}WHERE 里的 stock #{quantity} 是关键。这道 SQL 是原子操作数据库保证同一时刻只有一个请求能真正扣减成功返回影响行数为 0 就说明库存不足。再配合创建订单外的 Transactional 事务下单和扣库存要么都成功要么都回滚。这个场景单机环境很难复现但属于面试和答辩都爱问的考点写出来就是亮点。4.4 Vue打包后放进SpringBoot的static目录页面白屏现象前端开发模式一切正常npm run build 之后把 dist 目录复制到 SpringBoot 的 src/main/resources/static 下重启后端访问首页白屏或者在某个子页面刷新后 404。原因Vue 默认的 history 路由模式在服务器端没有对应的回退配置。请求 /order/list 这个路径时后端在 static 下找不到同名静态文件就返回 404Vue 路由自然无法接管页面。解决两种方案。第一种是 Vue 路由改成 hash 模式URL 变成带 # 的格式刷新不会 404代价是 URL 不美观。第二种是后端加一个 forward 规则我推荐这种因为 URL 更干净而且能体现你对路由原理的理解。Controller public class IndexController { RequestMapping(value {/, /{path:[^\\.]*}, /{path:^(?!api).*}/**}) public String forward() { return forward:/index.html; } }这段配置把不带后缀的路径转发到 index.html带 .js 或 .css 的静态资源不受影响/api 开头的接口请求也不会被错误拦截。另一个配套问题是 publicPath打包部署若不在根路径需要在构建配置里设置 publicPath: ./否则打包后的 JS 引用地址是绝对路径同样会白屏。4.5 接口返回的时间比北京时间慢8小时时区问题现象订单创建时间在数据库里看着正常但接口返回给前端的时间少了 8 小时或者前后端显示不一致。原因MySQL 连接串没写 serverTimezone驱动用了默认时区与北京时间东八区不一致。解决连接串加 serverTimezoneAsia/Shanghai同时后端 JSON 序列化统一格式。spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8再检查 MySQL 本身时区SELECT NOW(); SHOW VARIABLES LIKE %time_zone%;如果 now() 返回的时间不对再设置会话时区SET GLOBAL time_zone 08:00; SET SESSION time_zone 08:00;注意这是临时生效重启 MySQL 后会被重置。一劳永逸的办法是改 my.cnf 的 default-time-zone。这个坑平时不容易发现因为开发时不怎么关注时间精度但答辩现场数据一对比差 8 小时实在太明显建议提前一晚上验证一次。血泪经验别问我是怎么知道的。5. 答辩前做对这一步给系统补一个商家端管理页面如果时间只够做一件事我建议给系统加一个商家管理页面而不是去调特效、换皮肤。网上点餐系统拿高分的关键是业务闭环用户下单要有人接单、出餐缺了商家视角整套系统就停在半成品状态。加这个页面的成本很低后端复用现有接口前端补两个路由页面就足够。路由部分常见做法是在 Vue Router 里给 /admin 挂一个独立布局通过 meta 标记权限用全局守卫做角色拦截。// router/index.js { path: /admin, component: () import(/views/admin/Layout.vue), meta: { roles: [admin] }, children: [ { path: dish, component: () import(/views/admin/DishManage.vue) }, { path: orders, component: () import(/views/admin/OrderManage.vue) } ] }// 路由守卫角色不对就回首页 router.beforeEach((to, from, next) { const user JSON.parse(localStorage.getItem(user) || {}); const needAdmin to.matched.some(r r.meta.roles); if (needAdmin user.role ! 1) { next(/); } else { next(); } });后端这边菜品管理的增删改查接口直接复用 DishController在 Service 层加一次角色校验就行。订单管理页需要两个接口分页查询全部订单、把订单状态从“已支付”改成“制作中”状态变更走 2.3 里的 compareAndSetStatus保证不会出现用户取消和商家接单同时生效的乱局。答辩演示时开一个浏览器两个标签页一个登录普通用户下单一个登录管理员接单、出餐。两边页面状态同步变化评委看到的是一套完整闭环而不是孤立的页面堆砌。这道加分题的本质不是代码量而是状态联动设计你只要把状态变化都收敛到 Service 层的方法里演示时就能从容地一步步操作。说一个真实教训。我第一年带学弟做类似题目时他把演示视频录好后就不再碰代码结果现场视频解码失败五个人围着电脑修了十分钟。后来我要求所有项目必须能在命令行从零启动这条操作流程也成了每次答辩前必须演练的动作。源码能跑只是起点能熟练复现并讲清每个状态变化背后的取舍才是高分的关键。希望帮到你。本文还有配套的精品资源点击获取
返回列表