资讯动态

Spring Boot餐厅点餐系统实战:从Maven搭建到事务下单

发布时间:2026/10/13 14:21:06 来源:尧图企业网站定制
简介这是一份面向计算机专业本科生的Java Web毕业设计实战项目聚焦餐厅点餐系统开发适用于Spring框架入门与全栈工程实践学习。项目基于IntelliJ IDEA开发采用Maven标准化构建后端以Spring为核心实现分层架构集成数据库操作、用户认证与订单管理等典型业务模块前端融合Bootstrap、jQuery、Font Awesome及Material Design组件库提供响应式界面支持。压缩包共297个文件含57个Java核心业务类、97个JS交互脚本、36个CSS样式文件及29个图片资源结构清晰体现MVC分层设计总大小仅2.61MB轻量易导入。已有128人学习下载资源附带完整可运行代码、多层级配置文件XML/Properties/FTL、数据库表结构隐含逻辑及典型UI组件资源SVG/EOT/WOFF字体、PNG/GIF图标特别适合理解Spring依赖注入、前后端协同开发与中小型Web系统工程化落地。1. 一个能跑通、能改、能交的餐厅点餐系统为什么用 IntelliJ IDEA Maven Spring Boot 搭建比“抄源码压缩包”强十倍你手头那个标着“精品毕业设计”的S.zip解压后大概率是目录混乱的 Java 文件堆、没注释的 Controller、连数据库密码都硬编码在application.properties里的“成品”。这不是项目是定时炸弹——答辩前两天改个菜品分类字段整个订单模块崩掉老师问“怎么保证并发下单不超卖”你翻三分钟代码也找不到事务边界。而真正值得投入的毕业设计不是拼凑功能是构建一个可调试、可验证、可解释的最小闭环系统。本文讲的就是如何用 IntelliJ IDEA 作为唯一开发环境通过 Maven 精确管理依赖与生命周期以 Spring Boot 为骨架而非“S.zip”里模糊的“后台采用S”从零搭起一个带用户登录、菜单浏览、下单结算、订单查询四核心链路的餐厅点餐系统。它不追求炫酷前端但每个 HTTP 接口都能 curl 测试每笔数据库操作都有事务日志可查每次修改都有明确的编译/启动/验证路径。适合所有被“别人写的项目跑不起来”折磨过的大四学生——你不需要懂微服务但必须清楚Transactional写在哪、pom.xml里spring-boot-starter-web和spring-boot-starter-jdbc的分工、以及为什么mvn clean package后生成的 jar 包能直接java -jar运行。这不仅是交差是第一次亲手把“软件工程”四个字焊进自己的肌肉记忆。2. 从空项目开始用 IntelliJ IDEA 初始化 Spring Boot 工程并配置 Maven 依赖2.1 在 IntelliJ IDEA 中创建标准 Spring Boot 项目非导入 ZIP很多同学直接双击打开S.zip结果发现 IDEA 报错“Project SDK is not defined”或“Maven home path is not configured”——这是典型把 ZIP 当项目用的误区。正确路径是新建标准 Spring Boot 项目再按需移植业务逻辑。打开 IntelliJ IDEA → New Project → 左侧选Spring Initializr确保联网IDEA 会自动调用 start.spring.io→ 设置 Project SDK推荐 JDK 11 或 17避免 JDK 8 兼容问题→ Group 填com.exampleArtifact 填restaurant-order-system→ Next。提示不要勾选“Use default Maven settings”务必点击右侧齿轮图标手动指定本地已安装的 Maven 路径如C:\Program Files\apache-maven-3.8.6否则后续依赖下载会失败。在 Dependencies 页面只勾选以下 4 项精简是稳定的前提Spring Web提供 REST 接口能力Spring Boot DevTools开发时热重启省去反复重启服务器时间Spring Data JDBC轻量级数据库操作比 JPA 简单比纯 JDBC 易维护H2 Database嵌入式内存数据库无需额外安装 MySQL开箱即用适合毕业设计验证逻辑点击 FinishIDEA 会自动生成标准 Maven 结构src/main/java源码、src/main/resources配置、pom.xml依赖清单。此时项目已具备完整构建能力mvn compile必然成功。2.2 理解pom.xml的关键依赖与版本锁定逻辑pom.xml是 Maven 项目的“宪法”不能靠复制粘贴。以下是本项目最简但完备的依赖块替换 IDEA 自动生成的dependencies内容dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId !-- 不写 version由 parent pom 统一管理 -- /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope !-- 仅运行时需要编译不依赖 -- /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-devtools/artifactId optionaltrue/optional !-- 避免打包进最终 jar -- /dependency /dependencies关键参数说明scoperuntime/scopeH2 驱动只在程序运行时加载编译阶段不参与避免污染编译类路径optionaltrue/optionalDevTools 仅用于开发环境设为 optional 可确保mvn package生成的 jar 包不含它减小体积绝不手动写versionSpring Boot 官方 Parent POM 已锁定所有 Starter 的兼容版本如 Spring Boot 2.7.x 对应 Spring Framework 5.3.x手动指定易引发NoSuchMethodError。验证方式在 IDEA 右侧 Maven 面板中展开Dependencies确认spring-boot-starter-web下有spring-webmvc、spring-boot-starter-jdbc下有spring-jdbc且无红色报错图标。若有右键项目 →Maven→Reload project强制刷新。2.3 创建第一个可访问的 REST 接口验证环境是否真正就绪在src/main/java/com/example/restaurantordersystem下新建包controller再创建类MenuController.javapackage com.example.restaurantordersystem.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; RestController public class MenuController { GetMapping(/api/menu) public MapString, Object listMenu() { return Map.of( code, 200, message, success, data, Map.of(items, new String[]{宫保鸡丁, 麻婆豆腐, 清炒时蔬}) ); } }逻辑说明RestControllerControllerResponseBody表示该类所有方法返回值直接转为 JSON 响应体GetMapping(/api/menu)定义 GET 请求路径符合 RESTful 规范避免用/getMenu这类动词命名Map.of()是 Java 9 的不可变 Map 工厂方法简洁安全无需引入额外工具类。启动应用点击RestaurantOrderSystemApplication.java旁的绿色三角形 ▶️观察控制台输出。当看到Tomcat started on port(s): 8080 (http)且无 ERROR 日志即启动成功。验证命令终端执行curl -X GET http://localhost:8080/api/menu预期返回{code:200,message:success,data:{items:[宫保鸡丁,麻婆豆腐,清炒时蔬]}}若返回 404请检查① 类是否在com.example.restaurantordersystem主包下Spring Boot 默认扫描主类同包及子包②RestController注解是否拼写正确③ IDEA 是否开启了 “Build project automatically”File → Settings → Build → Compiler。3. 数据库建模与 JDBC 操作用 H2 实现菜品、用户、订单三张表的增删改查3.1 设计符合点餐场景的三张核心表SQL 脚本直贴可用H2 支持内存模式jdbc:h2:mem:testdb和文件模式jdbc:h2:file:./data/testdb。毕业设计推荐文件模式重启 IDEA 后数据不丢失方便演示连续操作。在src/main/resources下新建schema-h2.sql建表语句和>-- 菜品表存储餐厅所有可售菜品 CREATE TABLE IF NOT EXISTS dish ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, description VARCHAR(255), status TINYINT DEFAULT 1 -- 1上架, 0下架 ); -- 用户表存储顾客信息简化版无密码加密 CREATE TABLE IF NOT EXISTS customer ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, phone VARCHAR(20) UNIQUE NOT NULL ); -- 订单表记录每一笔下单行为 CREATE TABLE IF NOT EXISTS order_header ( id BIGINT AUTO_INCREMENT PRIMARY KEY, customer_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status VARCHAR(20) DEFAULT PENDING, -- PENDING/SUCCESS/FAILED created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (customer_id) REFERENCES customer(id) ); -- 订单明细表记录订单中每个菜品及数量 CREATE TABLE IF NOT EXISTS order_item ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL, dish_id BIGINT NOT NULL, quantity INT NOT NULL, price_per_unit DECIMAL(10,2) NOT NULL, FOREIGN KEY (order_id) REFERENCES order_header(id) ON DELETE CASCADE, FOREIGN KEY (dish_id) REFERENCES dish(id) );>-- 插入3个测试菜品 INSERT INTO dish (name, price, description, status) VALUES (宫保鸡丁, 38.00, 花生、鸡肉、辣椒, 1), (麻婆豆腐, 22.00, 牛肉末、豆瓣酱、花椒, 1), (清炒时蔬, 18.00, 时令蔬菜清炒, 1); -- 插入1个测试顾客 INSERT INTO customer (name, phone) VALUES (张三, 13800138000);为什么这样设计order_header与order_item分离是数据库范式要求避免订单中多个菜品重复存储价格price_per_unit在明细表中快照化防止菜品调价影响历史订单status字段用字符串PENDING/SUCCESS而非数字1/2可读性高后期扩展状态如 CANCELLED无需改字段类型ON DELETE CASCADE确保删除订单头时关联明细自动清理避免脏数据。3.2 配置application.yml启用 H2 控制台与自动建表在src/main/resources/application.yml中覆盖默认配置删除原有内容粘贴以下spring: datasource: url: jdbc:h2:file:./data/testdb;DB_CLOSE_ON_EXITFALSE;AUTO_SERVERTRUE username: sa password: driver-class-name: org.h2.Driver h2: console: enabled: true path: /h2-console sql: init: mode: always schema-locations: classpath:schema-h2.sql >package com.example.restaurantordersystem.service; import com.example.restaurantordersystem.model.Dish; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; Service public class DishService { private final JdbcTemplate jdbcTemplate; Autowired public DishService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } // 查询所有上架菜品 public ListDish findAvailableDishes() { String sql SELECT id, name, price, description FROM dish WHERE status 1; return jdbcTemplate.query(sql, new DishRowMapper()); } // 根据ID查询单个菜品 public Dish findById(Long id) { String sql SELECT id, name, price, description FROM dish WHERE id ?; return jdbcTemplate.queryForObject(sql, new DishRowMapper(), id); } // 更新菜品状态上架/下架 public int updateStatus(Long id, boolean available) { String sql UPDATE dish SET status ? WHERE id ?; return jdbcTemplate.update(sql, available ? 1 : 0, id); } // 内部 RowMapper将 ResultSet 映射为 Dish 对象 private static class DishRowMapper implements RowMapperDish { Override public Dish mapRow(ResultSet rs, int rowNum) throws SQLException { return new Dish( rs.getLong(id), rs.getString(name), rs.getBigDecimal(price), rs.getString(description) ); } } }关键设计点Service标记业务层与Controller表现层、Repository数据层职责分离虽未显式声明Repository但JdbcTemplate本身已封装了数据访问逻辑jdbcTemplate.queryForObject()用于查单条query()用于查列表绝不混用——查列表时若结果为空queryForObject()会抛EmptyResultDataAccessException而query()返回空List更易处理updateStatus方法返回int影响行数可用于判断更新是否成功如返回 0 表示 ID 不存在。在MenuController中注入并使用RestController public class MenuController { private final DishService dishService; public MenuController(DishService dishService) { this.dishService dishService; } GetMapping(/api/menu) public MapString, Object listMenu() { ListDish dishes dishService.findAvailableDishes(); return Map.of(code, 200, message, success, data, dishes); } }此时访问/api/menu返回的data将是包含id、name、price等字段的 JSON 数组而非硬编码字符串。4. 订单全流程实现从下单、扣库存到状态查询的事务控制与异常处理4.1 定义订单领域模型与 DTO避免直接暴露数据库实体在src/main/java/com/example/restaurantordersystem下新建包model创建OrderRequest.java接收前端下单参数package com.example.restaurantordersystem.model; import java.math.BigDecimal; import java.util.List; public class OrderRequest { private Long customerId; // 顾客ID private ListOrderItemRequest items; // 订单明细列表 // getter/setter 省略IDEA 可自动生成AltInsert public static class OrderItemRequest { private Long dishId; // 菜品ID private Integer quantity; // 数量 public OrderItemRequest(Long dishId, Integer quantity) { this.dishId dishId; this.quantity quantity; } // getter/setter } }新建OrderResponse.java返回给前端的订单结果package com.example.restaurantordersystem.model; import java.math.BigDecimal; import java.time.LocalDateTime; public class OrderResponse { private Long orderId; private String status; private BigDecimal totalAmount; private LocalDateTime createdAt; public OrderResponse(Long orderId, String status, BigDecimal totalAmount, LocalDateTime createdAt) { this.orderId orderId; this.status status; this.totalAmount totalAmount; this.createdAt createdAt; } // getter/setter }为什么不用OrderHeader实体直接传参OrderHeader包含id、created_at等数据库字段前端无需也不应设置OrderRequest明确约束了必填字段customerId,items避免null值入库OrderItemRequest将dishId和quantity绑定比用两个平行数组dishIds[],quantities[]更安全。4.2 编写OrderService实现下单核心逻辑Transactional的精准使用在service包下创建OrderService.javapackage com.example.restaurantordersystem.service; import com.example.restaurantordersystem.model.OrderRequest; import com.example.restaurantordersystem.model.OrderResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; Service public class OrderService { private final JdbcTemplate jdbcTemplate; private final DishService dishService; Autowired public OrderService(JdbcTemplate jdbcTemplate, DishService dishService) { this.jdbcTemplate jdbcTemplate; this.dishService dishService; } Transactional // 关键整个方法在同一个数据库事务中执行 public OrderResponse createOrder(OrderRequest request) { // 1. 校验顾客是否存在 Long customerId request.getCustomerId(); Integer customerCount jdbcTemplate.queryForObject( SELECT COUNT(*) FROM customer WHERE id ?, Integer.class, customerId); if (customerCount 0 || customerCount null) { throw new IllegalArgumentException(Customer not found: customerId); } // 2. 计算总金额 校验菜品库存此处简化为“菜品存在且上架” BigDecimal totalAmount BigDecimal.ZERO; for (OrderRequest.OrderItemRequest item : request.getItems()) { // 查询菜品价格和状态 Dish dish dishService.findById(item.getDishId()); if (dish null || dish.getStatus() ! 1) { throw new IllegalArgumentException(Dish unavailable: item.getDishId()); } totalAmount totalAmount.add(dish.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 3. 插入订单头 String insertHeaderSql INSERT INTO order_header (customer_id, total_amount, status, created_at) VALUES (?, ?, ?, ?); KeyHolder keyHolder new GeneratedKeyHolder(); jdbcTemplate.update(connection - { PreparedStatement ps connection.prepareStatement(insertHeaderSql, Statement.RETURN_GENERATED_KEYS); ps.setLong(1, customerId); ps.setBigDecimal(2, totalAmount); ps.setString(3, PENDING); ps.setTimestamp(4, Timestamp.valueOf(LocalDateTime.now())); return ps; }, keyHolder); Long orderId keyHolder.getKey().longValue(); // 4. 插入订单明细循环插入 String insertItemSql INSERT INTO order_item (order_id, dish_id, quantity, price_per_unit) VALUES (?, ?, ?, ?); for (OrderRequest.OrderItemRequest item : request.getItems()) { Dish dish dishService.findById(item.getDishId()); jdbcTemplate.update(insertItemSql, orderId, item.getDishId(), item.getQuantity(), dish.getPrice()); } // 5. 返回响应 return new OrderResponse(orderId, PENDING, totalAmount, LocalDateTime.now()); } // 查询订单详情含菜品名称、数量、单价 public OrderDetailResponse findOrderDetail(Long orderId) { // 主查询订单头信息 String headerSql SELECT oh.id, oh.customer_id, oh.total_amount, oh.status, oh.created_at, c.name as customer_name FROM order_header oh JOIN customer c ON oh.customer_id c.id WHERE oh.id ?; OrderHeaderWithCustomer header jdbcTemplate.queryForObject(headerSql, new OrderHeaderRowMapper(), orderId); // 子查询订单明细 String itemSql SELECT oi.id, oi.dish_id, oi.quantity, oi.price_per_unit, d.name as dish_name FROM order_item oi JOIN dish d ON oi.dish_id d.id WHERE oi.order_id ?; ListOrderItemDetail items jdbcTemplate.query(itemSql, new OrderItemRowMapper(), orderId); return new OrderDetailResponse(header, items); } // 内部 RowMapper 省略与 DishRowMapper 类似映射复合对象 }Transactional的避坑要点必须加在public方法上且调用方必须是 Spring 容器管理的 Bean即通过Autowired注入而非new OrderService()默认传播行为REQUIRED意味着如果当前已有事务如 Controller 层调用则加入该事务若无则新建事务绝不放在 Controller 层Controller 应只做参数校验、调用 Service、包装响应事务边界必须在 Service 内部清晰定义。4.3 在 Controller 中暴露下单与查询接口在controller包下创建OrderController.javapackage com.example.restaurantordersystem.controller; import com.example.restaurantordersystem.model.OrderRequest; import com.example.restaurantordersystem.model.OrderResponse; import com.example.restaurantordersystem.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.Map; RestController RequestMapping(/api/order) public class OrderController { private final OrderService orderService; Autowired public OrderController(OrderService orderService) { this.orderService orderService; } PostMapping public MapString, Object createOrder(RequestBody OrderRequest request) { try { OrderResponse response orderService.createOrder(request); return Map.of(code, 200, message, success, data, response); } catch (IllegalArgumentException e) { return Map.of(code, 400, message, e.getMessage(), data, null); } catch (Exception e) { // 记录错误日志实际项目应使用 SLF4J e.printStackTrace(); return Map.of(code, 500, message, System error, data, null); } } GetMapping(/{id}) public MapString, Object getOrderDetail(PathVariable Long id) { try { OrderDetailResponse detail orderService.findOrderDetail(id); return Map.of(code, 200, message, success, data, detail); } catch (Exception e) { return Map.of(code, 404, message, Order not found, data, null); } } }测试下单流程curl 命令# 下单请求注意customerId1 来自>plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration includeSystemScopetrue/includeSystemScope /configuration /plugin6. 答辩演示与代码交付技巧如何让老师一眼看懂你的技术深度6.1 构建一个“可演示”的最小可运行包不依赖 IDEA毕业设计答辩时老师很可能要求“在另一台电脑上运行看看”。此时mvn spring-boot:run依赖 IDEA 环境不可靠。正确做法是生成独立 jar 包在 IDEA 终端执行mvn clean package -DskipTests进入target/目录找到restaurant-order-system-0.0.1-SNAPSHOT.jar文件名由pom.xml中version决定复制该 jar 包 同目录下的data/文件夹H2 数据库存储位置到目标电脑在目标电脑终端执行java -jar restaurant-order-system-0.0.1-SNAPSHOT.jar关键点data/文件夹必须与 jar 包同级因为application.yml中jdbc:h2:file:./data/testdb的./指向 jar 包所在目录。若漏掉data/H2 会新建空库演示时看不到初始化数据。6.2 准备三组“黄金测试用例”应对老师提问老师常问“如果两个用户同时抢最后一份宫保鸡丁会超卖吗”——这考察你对并发的理解。提前准备好三组 curl 命令现场演示场景命令预期结果技术点正常下单curl -X POST ... {dishId:1,quantity:1}返回 successorder_item表新增一行基础功能验证并发下单模拟开两个终端同时执行下单命令dishId1, quantity1且库存只剩1份只有一个成功另一个报错Dish unavailable事务隔离 业务校验双重保障状态查询curl http://localhost:8080/api/order/1返回完整订单详情含菜品名、数量、单价关联查询能力提示在DishService.findById()中故意加Thread.sleep(1000)模拟慢查询可放大并发冲突概率让演示更直观。6.3 代码注释与 README.md 的“答辩友好型”写法不要写“本系统实现了点餐功能”要写老师能快速抓重点的注释在OrderService.createOrder()方法上方加/** * 下单核心方法采用数据库事务保证一致性。 * 步骤1) 校验顾客存在2) 循环校验菜品可用性并计算总价3) 插入订单头4) 批量插入订单明细。 * 注意事务边界在此方法内任何步骤失败均回滚全部操作。 */在项目根目录README.md中用表格列出技术选型理由比“用了Spring Boot”有力得多技术组件为什么选它替代方案为什么不选H2 Database嵌入式内存/文件数据库零安装数据持久化支持标准SQLMySQL需额外安装配置SQLite不支持多连接纯内存Map无法演示“数据落地”Spring Boot DevTools开发时热重启修改Java代码后秒级生效极大提升调试效率手动重启Tomcat耗时30秒以上答辩演示时无法承受JdbcTemplate轻量级JDBC封装无ORM学习成本SQL完全可控便于老师审查每条查询逻辑JPA/Hibernate抽象层过厚老师问“这条SQL怎么生成的”难以回答最后我的习惯是每次git commit前用mvn clean compile和curl测试所有核心接口确保提交的每一版代码都是“可演示状态”。这比写一百行注释更能体现工程素养——毕竟答辩台上跑不通的代码再漂亮的架构图也是空中楼阁。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑