简介:这是一份面向计算机专业本科生的Java Web毕业设计实战项目,聚焦餐厅点餐系统开发,适用于Spring框架入门与全栈工程实践学习。项目基于IntelliJ IDEA开发,采用Maven标准化构建,后端以Spring为核心实现依赖注入与分层架构,集成MySQL数据库(含菜品、订单、用户等实体表设计),前端融合Bootstrap、jQuery、Font Awesome及Material Design组件库,支持响应式界面与基础交互功能。压缩包共297个文件,涵盖57个Java业务逻辑与实体类、97个JS交互脚本、36个CSS样式文件、29个PNG图标资源及16个XML配置文件,整体2.61MB,结构清晰体现典型MVC分层与前后端资源组织规范。目前已有128人学习下载,读者可直接导入IDE运行调试,完整掌握从环境搭建、模块开发(菜单展示、购物车、订单管理、用户登录)、数据库映射到静态资源集成的全流程实践能力。
1. 这不是又一个“学生管理系统”:一个真实跑在 IntelliJ IDEA 里的 Maven 餐厅点餐系统,后端用 Spring Boot + MyBatis + MySQL,前端是 Thymeleaf 模板——它能直接部署、支持多角色(顾客/服务员/管理员)、完成从菜单浏览→下单→厨房接单→状态更新→结账全链路
你可能已经点开过十几个标着“Java 毕业设计”的压缩包,解压后发现:没有pom.xml、src/main/java下空空如也、数据库脚本里连建表语句都缺字段、README 里写着“运行前请自行配置 Tomcat”却没说配哪几个参数……这不是资源,这是玄学考题。而这个项目不同:它是一个完整可启动、可交互、有业务闭环的轻量级 Web 应用,不是 Demo,不是截图集合,更不是 PPT 生成器。它用最主流的 Java Web 技术栈(Spring Boot 2.7.x + MyBatis 3.4.x + MySQL 5.7+),所有模块按分层架构组织清晰,Maven 依赖无冲突、无废弃坐标,IntelliJ IDEA 导入即识别,连 Lombok 的@Data和@AllArgsConstructor都已正确启用。适合两类人:一是大三下刚学完 Spring Boot 的同学,想拿一个不改核心逻辑就能交毕设、还能现场演示下单流程的基线项目;二是刚转 Java 后端的新人,需要一个没有黑匣子、每个 Controller 方法都能顺藤摸瓜到 SQL 和页面跳转的练手样本。它不追求高并发或微服务,但把“餐厅点餐”这个场景里最易被忽略的细节——比如订单状态机流转(待接单→制作中→已完成)、菜品库存扣减时机、服务员与管理员权限隔离——都落到了代码行里。
2. 从解压到启动:五步走通 IntelliJ IDEA + Maven 环境下的完整运行链路
2.1 解压与项目结构初判:确认这不是“伪 Maven 项目”
拿到S.zip后,先别急着导入 IDEA。用任意解压工具打开,重点检查以下 5 个文件/目录是否存在且结构合理:
pom.xml:必须位于根目录,且<parent>节点应指向spring-boot-starter-parent(版本建议 2.7.18,兼容 JDK 8/11)src/main/java/com/example/restaurant/:包路径需含controller、service、mapper、entity、config五个标准子包src/main/resources/application.yml:必须存在,且含spring: datasource:和mybatis:配置段src/main/resources/static/与templates/:Thymeleaf 项目必备,前者放 CSS/JS,后者放.html模板sql/restaurant_init.sql(或类似命名):建库建表+初始化数据脚本,非空文件
提示:若
pom.xml中<packaging>是jar(而非war),说明它内嵌 Tomcat,无需额外配服务器——这是现代 Spring Boot 项目的标准做法,也是本项目可一键启动的前提。
2.2 IntelliJ IDEA 导入:拒绝“Add as Maven Project”陷阱
很多新手卡在这一步:右键 → “Add as Maven Project”,结果 IDEA 报错Cannot resolve symbol 'SpringBootApplication'。正确操作是:
# 在终端中进入解压后的项目根目录(含 pom.xml 的那一层) cd /path/to/restaurant-system # 执行 Maven 初始化(确保本地有 Maven 3.6+) mvn clean compile -Dmaven.test.skip=true然后在 IDEA 中:
- File → Open → 选择项目根目录(不是 zip 文件,也不是 src 文件夹)
- 弹窗中勾选"Auto-import"和"Create module groups for multi-module projects"
- 点击 OK,等待右下角 "Importing project..." 完成(约 30~90 秒,取决于网络和本地仓库)
此时pom.xml左上角应出现蓝色 Maven 图标,src/main/java下包名应可展开,且@SpringBootApplication注解无红色波浪线。
2.3 数据库准备:MySQL 5.7+ 创建库、执行初始化脚本
本项目默认使用 MySQL,不兼容 SQLite 或 H2 内存库(因涉及多表事务与外键约束)。按以下步骤操作:
- 启动本地 MySQL 服务(如使用 XAMPP/MAMP,请确保 MySQL 已 start)
- 新建数据库(字符集强制设为
utf8mb4,避免中文乱码):CREATE DATABASE restaurant_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - 找到项目中的 SQL 初始化文件(通常在
sql/目录下),用 MySQL 客户端执行:mysql -u root -p restaurant_db < sql/restaurant_init.sql参数说明:
-u root是登录用户名(若改过密码,执行时会提示输入);restaurant_db必须与application.yml中spring.datasource.url的库名一致;<表示重定向执行 SQL 文件。
执行成功后,应看到t_user、t_menu、t_order、t_order_item四张表,且t_user中已有 3 条测试数据(id=1 管理员、id=2 服务员、id=3 顾客)。
2.4 application.yml 配置:三处必改项与两处易错点
打开src/main/resources/application.yml,修改以下位置(其他保持默认):
spring: datasource: url: jdbc:mysql://localhost:3306/restaurant_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_mysql_password # ← 此处填你 MySQL 的实际密码 thymeleaf: cache: false # ← 开发期务必关掉模板缓存,否则改 HTML 不生效 encoding: UTF-8 mybatis: mapper-locations: classpath:mapper/*.xml # ← 确保路径匹配 src/main/resources/mapper/ 下的 XML 文件 configuration: map-underscore-to-camel-case: true关键参数说明:
serverTimezone=Asia/Shanghai:解决 MySQL 8.0+ 因时区导致的java.sql.SQLException: The server time zone value '...' is unrecognized错误;allowPublicKeyRetrieval=true:适配 MySQL 8.0 默认开启的caching_sha2_password认证插件;cache: false:Thymeleaf 模板热加载开关,不关则改完 HTML 必须重启应用。
2.5 启动与首屏验证:访问http://localhost:8080/login看到登录页即成功
在 IDEA 中找到RestaurantApplication.java(含@SpringBootApplication的主类),右键 →Run 'RestaurantApplication.main()'。
观察控制台输出:
- 出现
Tomcat started on port(s): 8080 (http)表示 Web 容器启动成功; - 出现
Started RestaurantApplication in X.XXX seconds表示 Spring 上下文加载完毕; - 若有
Caused by: java.lang.IllegalArgumentException: Invalid bound statement (not found),说明mapper-locations路径错误或 XML 文件名与@MapperScan不匹配。
启动成功后,在浏览器访问:
👉http://localhost:8080/login
应看到一个简洁的登录表单,含“用户名”、“密码”、“角色选择(管理员/服务员/顾客)”三个字段。
用测试账号尝试登录:
- 管理员:
admin/123456 - 服务员:
waiter/123456 - 顾客:
customer/123456
登录后跳转至对应首页(管理员看后台管理、服务员看待处理订单、顾客看菜单页),即证明整个 MVC 链路跑通。
3. 核心业务模块拆解:从菜单展示到订单状态机,代码级还原餐厅点餐逻辑
3.1 菜单展示与搜索:Thymeleaf + MyBatis 多条件查询实战
顾客端首页/menu展示所有菜品,并支持按名称模糊搜索、按分类筛选。关键代码在MenuController.java:
@GetMapping("/menu") public String showMenu(Model model, @RequestParam(value = "keyword", required = false) String keyword, @RequestParam(value = "category", required = false) String category) { List<Menu> menuList = menuService.findMenus(keyword, category); model.addAttribute("menuList", menuList); model.addAttribute("keyword", keyword != null ? keyword : ""); model.addAttribute("category", category != null ? category : ""); return "menu"; // 对应 templates/menu.html }对应MenuMapper.java接口:
@Mapper public interface MenuMapper { List<Menu> findMenus(@Param("keyword") String keyword, @Param("category") String category); }其 XML 实现(resources/mapper/MenuMapper.xml):
<select id="findMenus" resultType="com.example.restaurant.entity.Menu"> SELECT * FROM t_menu <where> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> <if test="category != null and category != ''"> AND category = #{category} </if> </where> ORDER BY sort_order ASC </select>逻辑说明:
<where>标签自动处理 SQL 中的AND前缀,避免手动拼接导致语法错误;<if>实现动态条件,当keyword为空时不加入LIKE子句;sort_order字段用于控制菜品在页面上的显示顺序(数值越小越靠前),这是真实餐厅系统中常被忽略但极其实用的设计。
3.2 下单流程:事务边界在哪?库存扣减何时发生?
顾客点击“加入购物车” → 提交订单,是整个系统最关键的事务点。代码位于OrderController.java:
@PostMapping("/order/submit") @Transactional(rollbackFor = Exception.class) // ← 事务注解在此 public String submitOrder(@RequestParam("items") String itemsJson, @RequestParam("note") String note, Model model, HttpSession session) { User currentUser = (User) session.getAttribute("user"); try { Order order = orderService.createOrder(currentUser.getId(), itemsJson, note); model.addAttribute("order", order); return "redirect:/order/success?id=" + order.getId(); } catch (Exception e) { model.addAttribute("error", "下单失败:" + e.getMessage()); return "menu"; } }OrderService.createOrder()方法内部执行:
- 解析
itemsJson(格式如[{"menuId":1,"quantity":2},{"menuId":3,"quantity":1}]) - 校验库存:对每个菜品,查
t_menu.stock是否 ≥ quantity - 扣减库存:执行
UPDATE t_menu SET stock = stock - ? WHERE id = ? - 插入订单主表:
INSERT INTO t_order (...) VALUES (...) - 插入订单明细:
INSERT INTO t_order_item (...) VALUES (...)
关键设计点:事务注解
@Transactional加在 Service 方法上,而非 Controller,确保“校验-扣减-写库”原子性;库存扣减发生在订单创建之前,避免超卖;t_menu.stock字段为INT类型,初始值由restaurant_init.sql设定(如宫保鸡丁 stock=50),这是防止并发下单超卖的最简有效手段。
3.3 订单状态机:从“待接单”到“已完成”,状态流转由谁驱动?
本项目采用显式状态字段 + 服务端控制,而非消息队列。状态定义在OrderStatusEnum.java:
public enum OrderStatusEnum { WAITING(0, "待接单"), // 顾客提交后初始状态 COOKING(1, "制作中"), // 服务员点击“开始制作” COMPLETED(2, "已完成"), // 服务员点击“完成” CANCELLED(-1, "已取消"); // 管理员或超时自动取消 }状态变更入口在OrderController.java的两个接口:
/order/accept/{id}:服务员接单,将status从WAITING改为COOKING/order/complete/{id}:服务员完成,将status从COOKING改为COMPLETED
@PostMapping("/order/accept/{id}") public String acceptOrder(@PathVariable Long id) { orderService.updateStatus(id, OrderStatusEnum.COOKING.getValue()); return "redirect:/waiter/orders"; }注意:状态变更不触发页面自动刷新,需服务员手动点击“刷新”按钮(或 F5),这是简化版设计。若需实时推送,可后续集成 WebSocket,但本项目未引入,避免复杂度溢出。
3.4 权限隔离:如何让管理员看到所有订单,而服务员只能看自己接的单?
权限控制未使用 Spring Security,而是基于User.role字段 + 查询条件过滤,轻量且直观。以订单列表为例:
管理员访问
/admin/orders,调用AdminOrderController.listAllOrders():List<Order> orders = orderMapper.findAll(); // 查全部服务员访问
/waiter/orders,调用WaiterOrderController.listMyOrders():List<Order> orders = orderMapper.findByWaiterId(waiterId); // 只查 assigned_waiter_id = ?
对应 XML 中的findByWaiterId:
<select id="findByWaiterId" resultType="com.example.restaurant.entity.Order"> SELECT o.*, u.username AS waiter_name FROM t_order o LEFT JOIN t_user u ON o.assigned_waiter_id = u.id WHERE o.assigned_waiter_id = #{waiterId} ORDER BY o.create_time DESC </select>优势:无学习成本,SQL 直观可调;劣势:无法细粒度到按钮级(如“删除订单”按钮对服务员隐藏)。若需升级,可在 Thymeleaf 模板中用
sec:authorize="hasRole('ADMIN')"控制按钮显示,但本项目未预置 Spring Security 依赖,故采用更直接的 Controller 层过滤。
4. 避坑指南:五个真实踩过的雷,省下你三天调试时间
4.1 现象:启动时报java.lang.ClassNotFoundException: javax.servlet.Filter
原因:项目使用了 Spring Boot 2.7.x,但本地 JDK 是 17+,而javax.servlet.*包在 Jakarta EE 9+ 中已迁移至jakarta.servlet.*,Spring Boot 2.7.x 默认仍依赖旧包。
解决:在pom.xml中强制指定 Servlet API 版本,并排除冲突传递依赖:
<properties> <servlet-api.version>4.0.1</servlet-api.version> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>${servlet-api.version}</version> <scope>provided</scope> </dependency> <!-- 排除 spring-boot-starter-web 传递进来的 jakarta.servlet --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> </dependencies>4.2 现象:登录后跳转 404,URL 变成http://localhost:8080/undefined
原因:LoginController.java中登录成功后的return "redirect:" + redirectUrl,但redirectUrl为空字符串或 null,导致拼接出redirect:。
解决:在LoginController.login()方法末尾添加空值保护:
String redirectUrl = (String) session.getAttribute("redirectUrl"); if (redirectUrl == null || redirectUrl.trim().isEmpty()) { redirectUrl = "/home"; // 默认首页 } session.removeAttribute("redirectUrl"); return "redirect:" + redirectUrl;4.3 现象:菜单页图片不显示,控制台报GET http://localhost:8080/images/dish1.jpg 404
原因:项目中图片存放在src/main/resources/static/images/,但application.yml未配置静态资源路径,或前端 HTML 中写死了images/而非 Thymeleaf 的@{/images/}。
解决:
- 确认图片文件在
src/main/resources/static/images/下; - 在
menu.html中,将<img src="images/dish1.jpg">改为<img th:src="@{/images/dish1.jpg}">; - (可选)在
application.yml中显式声明:spring: web: resources: static-locations: classpath:/static/
4.4 现象:下单时提示“库存不足”,但数据库里t_menu.stock明明是 100
原因:MenuService.findMenus()查询时未 selectstock字段,导致前端显示的“剩余库存”是 0(对象未赋值),但实际扣减逻辑用的是数据库真实值。
解决:检查MenuMapper.xml的resultMap,确保stock字段被映射:
<resultMap id="MenuResultMap" type="com.example.restaurant.entity.Menu"> <id property="id" column="id"/> <result property="name" column="name"/> <result property="price" column="price"/> <result property="stock" column="stock"/> <!-- ← 此行必须存在 --> <result property="category" column="category"/> </resultMap>4.5 现象:管理员修改菜品价格后,历史订单明细中的单价未变,但新订单变了
原因:这是正确行为,而非 Bug。订单明细表t_order_item在创建时已将item_price字段固化为下单瞬间的价格(INSERT ... SELECT price FROM t_menu WHERE id = ?),保证财务数据不随菜单价变动而漂移。
验证:查t_order_item表,item_price列值应与当时t_menu.price一致,且独立于t_menu后续更新。若需求是“价格联动”,则需去掉item_price字段,查询时JOIN t_menu,但本项目按电商通用实践做了快照。
5. 进阶技巧:三招让这个毕设项目从“能跑”变成“值得讲”
5.1 把 Thymeleaf 模板抽成可复用组件:减少重复代码的硬核写法
当前header.html和footer.html在每个页面都用th:replace引入,但导航栏的“当前高亮”逻辑(如/menu页面时“菜单”文字加粗)分散在各 Controller 中传参。更优雅的做法是用 Thymeleaf 的fragments+th:fragment:
在
templates/layout.html中定义:<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head th:fragment="common-head"> <title th:text="${title} ?: '餐厅点餐系统'">系统标题</title> </head> <body> <nav th:fragment="top-nav"> <a th:classappend="${active == 'menu'} ? 'active'" th:href="@{/menu}">菜单</a> <a th:classappend="${active == 'order'} ? 'active'" th:href="@{/order/cart}">购物车</a> <a th:classappend="${active == 'history'} ? 'active'" th:href="@{/order/history}">订单历史</a> </nav> <div th:fragment="content" th:remove="tag">页面主体内容</div> </body> </html>在
menu.html中引用:<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::body}, ~{::title}, ~{::content})}"> <head><title>菜单页</title></head> <body> <div th:fragment="content"> <div th:replace="~{layout :: top-nav(active='menu')}"></div> <!-- 具体菜单列表 --> </div> </body> </html>
效果:所有页面共享同一套导航逻辑,“当前页高亮”由
active参数驱动,Controller 中只需model.addAttribute("active", "menu"),彻底消灭重复判断。
5.2 用 Actuator + Prometheus 快速接入基础监控(零侵入)
虽然项目轻量,但加个/actuator/health和/actuator/metrics就能体现工程素养。只需两步:
在
pom.xml中添加依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>在
application.yml中暴露端点:management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always
启动后访问http://localhost:8080/actuator/health返回{"status":"UP"},访问http://localhost:8080/actuator/metrics可看到 JVM 内存、HTTP 请求计数等指标。若后续想可视化,可配合 Grafana + Prometheus,但本项目仅需暴露端点即达标。
5.3 生成可执行 JAR 包:让答辩老师双击就能运行
毕业答辩时,老师大概率不会装 IDEA 和 Maven。你需要一个restaurant.jar,双击或java -jar restaurant.jar即可启动。
- 确保
pom.xml中<packaging>为jar(默认就是); - 在项目根目录执行:
mvn clean package -Dmaven.test.skip=true - 找到
target/restaurant-system-0.0.1-SNAPSHOT.jar(文件名以pom.xml中<artifactId>和<version>为准); - 测试运行:
java -jar target/restaurant-system-0.0.1-SNAPSHOT.jar
关键参数说明:
-Dmaven.test.skip=true跳过测试(避免因测试数据缺失失败);生成的 JAR 包内含所有依赖(fat jar),无需额外 classpath;若需指定配置文件,可:java -jar restaurant.jar --spring.config.location=file:./application-prod.yml
从那以后我每次交付毕设项目,都会在README.md里用加粗字体写清楚:“双击运行:java -jar target/*.jar”,并附上默认账号密码。不是怕老师不会用,而是把‘能用’这件事,做到比‘会用’更确定。希望帮到你。
本文还有配套的精品资源,点击获取