很多做 Java 课程设计或者期末大作业的同学,最后都会撞上同一个题目:点餐系统。这个题目几乎成了 Java Web 方向最常见的“常青树”,每年都有人做,每年都有大量“附源码”的资源流传。
但这里有一个非常典型的割裂现象:资源确实满天飞,很多标题还写着“拿走直接用”“十分钟搞定”,可真到了自己电脑上,不少人会卡在环境配置、数据库连接、Tomcat 启动这些环节,一卡就是两三天。更麻烦的是,就算程序跑起来了,到了答辩环节,老师随口问一句“你这个订单表为什么这么设计”“事务在哪里控制的”,如果只是照着下载的源码跑通却没搞懂内部逻辑,场面会非常尴尬。
这篇文章不打算重复“下载源码→导出导入→点运行”式的保姆教程,那类东西已经太多了。我更想站在一个“课设裁判”和“工程实践”之间的角度,把这套点餐系统真正涉及的知识链路拆开:数据库怎么设计、JDBC 连接怎么写、Servlet 怎么处理登录和下单、JSP 怎么展示数据、跑起来之后又该怎么验证和排错。如果你手头正好有一套源码却不知道怎么消化,或者想自己从零搭建一个能答辩的点餐系统,这篇文章应该能帮你把思路理顺。
1. 这篇文章真正要解决的问题
先给一个明确判断:标题里的“十分钟搞定”有可能成立,但它成立的边界很窄。
如果你拿到的是一个环境完整、数据库脚本齐全、导入步骤写得很详细的工程,并且你的 JDK、Tomcat、MySQL、IDE 版本恰好和作者一致,那么确实可能十分钟内跑起来。但现实往往是:大部分下载到的课设源码年代比较久远,用的可能是 Eclipse + Tomcat 8 + MySQL 5.7,而你电脑上装的是新版 IDEA + JDK 17 + MySQL 8,这时“白嫖”的成本就体现出来了——你至少要会处理版本差异、驱动兼容、数据库连接串修改这类问题。
所以这篇文章真正想解决的是三个层面的问题:
- 快速跑通:用自己的电脑环境把项目运行起来,而不是被环境卡住。
- 读懂核心链路:从用户浏览菜单、登录、下单,到数据库插入订单记录,这条主流程在代码层面究竟是怎么走的。
- 答辩和改造:除了能运行,还要知道老师可能问什么、项目怎么改出个人特色。
对读者来说,最直接的收益就是你不再害怕“下了一套源码却跑不起来”这件事。跑不起来的原因其实高度集中在几个固定点上:数据库没建、驱动版本不匹配、字符集乱码、Tomcat 端口冲突。这些后面都会逐一拆解。
另一个需要说清楚的事是:点餐系统项目本身并没有特别高深的技术含量,它之所以流行,是因为它在业务上完整覆盖了 Java Web 开发里最常见的三层结构、登录鉴权、CRUD 操作、表关联查询和事务处理。把点餐系统彻底吃透,比刷二十道网上找的面试题更能帮你理解 Java Web 后端到底是怎么运作的。
2. 点餐系统的业务场景与技术架构拆解
2.1 点餐系统在解决什么业务问题
先把业务想清楚。一个最基本的餐厅点餐流程是这样发生的:
- 顾客进店或者扫码,看到菜品菜单。
- 顾客选择菜品,加入购物车。
- 顾客提交订单,生成一条待处理订单。
- 后厨看到订单后开始制作,顾客完成支付。
落实到课设级别的点餐系统里,通常还会简化掉支付流程,只保留“下单生成订单”这一步。这样整个系统就聚焦在两个角色上:
- 普通用户(顾客):注册、登录、浏览菜品、加入购物车、提交订单、查看我的订单。
- 管理员:登录后台、管理菜品分类、维护菜品信息(上架/下架/改价格)、查看用户订单。
从产品功能看,这个系统麻雀虽小,但五脏俱全。它天然需要 CRUD 操作,需要登录会话管理,需要订单表和订单明细表之间的关联,还需要一组稳定的数据库表结构。这也是为什么老师总爱让做点餐系统——它几乎把 Java Web 课设所有必考知识点全部覆盖了。
2.2 Java Web 三层架构
点餐系统的代码组织方式,最经典的是三层架构,也叫 MVC 思路。对于初学者,可以这样理解:
- View 层(视图层):在传统 JSP 项目中,这一层是
.jsp文件,负责展示菜品列表、订单信息、购物车页面。它直接面向用户浏览器。 - Controller 层(控制层):一般由 Servlet 充当,负责接收浏览器发来的请求(比如用户点了“登录”按钮),调度后面的业务代码,再决定页面跳转到哪里。
- Service + DAO 层(业务层和数据访问层):负责真正的业务逻辑和数据库操作。比如下单时要先判断库存是否充足,然后把订单写入数据库。
如果用一句话概括:浏览器发起请求,Servlet 拦截并调度,Java 代码操作 MySQL,最后把结果渲染回 JSP 页面。
网上能找到的点餐系统,有些用 JSP + Servlet + JDBC 直接写,有些用了 SSM(Spring + SpringMVC + MyBatis),有些用了 Spring Boot。但无论哪种框架,底层思路都是上面这条链路。理解了这条链路,你跑通任何一个版本都不会心虚。
2.3 不同版本怎么选
从源码选择的实用角度出发,我的建议是:
- 如果只是想快速交一个课程设计,尽量选JSP + Servlet + JDBC的版本。这类项目结构简单,没有复杂的 Maven 依赖解析和框架配置,导入门槛最低。
- 如果课程要求里面有“使用框架”,那就选SSM 或 Spring Boot版本。但要注意,这类版本通常要求 Maven 联网下载依赖,环境准备时间更长。
- 如果你想借这次课设学点现在工业界在用的东西,Spring Boot + MyBatis 会比纯粹的 JSP + Servlet 更值得投入时间。
结论:选源码,不要只看界面好不好看,先看技术栈。技术栈越新,跑起来的隐性成本越高;技术栈越传统,代码越容易读懂。
| 版本类型 | 技术栈 | 上手门槛 | 答辩友好度 | 适合人群 |
|---|---|---|---|---|
| JSP + Servlet + JDBC | JSP、Servlet、JDBC、MySQL | 低 | 高,逻辑直观易说明 | 刚学完 Java Web 基础 |
| SSM 版本 | Spring + SpringMVC + MyBatis | 中 | 中,需理解框架原理 | 学过框架但未实战 |
| Spring Boot 版本 | Spring Boot + MyBatis/自动配置 | 中高 | 中高,需知道自动配置原理 | 想贴近企业开发节奏 |
3. 环境准备与版本选择
先把环境统一一下。点餐系统属于传统 Java Web 项目,最稳妥的环境组合建议按下表准备,具体版本号以你下载的源码为准,这里只说通用原则:
| 环境项 | 推荐选择 | 说明 |
|---|---|---|
| JDK | 1.8 或 8+ | 老版本 JSP/Servlet 工程用 JDK 8 最稳,新版 JDK 可能遇到外部库不兼容 |
| Tomcat | Tomcat 8.5 或 9.0 | 支持 Servlet 3.1/4.0,适合大多数课设工程 |
| MySQL | 5.7 或 8.0 | 注意驱动版本要和 MySQL 版本匹配 |
| IDE | Eclipse 或 IntelliJ IDEA | IDEA 导入 Eclipse 项目时需选择对应导入方式 |
| 项目管理 | 有的用 Maven,有的直接 lib 引 JAR | 看清楚源码里有没有 pom.xml |
有几个高频细节需要提醒。
第一,JDK 版本别盲目追求新版。很多老课设源码是基于 JDK 8 写的,换到 JDK 17 后,可能会出现Unable to make field accessible这类反射报错。如果源码本身没有在升级后验证过,建议直接安装 JDK 8,并把 IDE 的项目 SDK 指定到它。
第二,Tomcat 和 IDEA 的集成。IDEA 社区版默认不带 Tomcat 插件,需要手动配置 Smart Tomcat 或者使用 Tomcat 官方启动脚本部署 war 包。Eclipse 则相对顺手,直接在 Servers 视图里添加 Tomcat 并运行项目即可。
第三,MySQL 8 和旧驱动之间的坑。如果你用的是 MySQL 8,但源码里的 JDBC 驱动还是com.mysql.jdbc.Driver,并用的旧版mysql-connector-java,连接时会报Public Key Retrieval is not allowed或者Loading class ... is deprecated。解决办法是换成com.mysql.cj.jdbc.Driver,并在连接串后面加上useSSL=false&serverTimezone=Asia/Shanghai。
环境准备这块没什么高深内容,但每个细节都可能导致项目直接起不来。我的建议是:先确认源码里的lib目录或pom.xml,再决定要不要调整自己的环境。
4. 数据库设计与初始化脚本
点餐系统的核心在数据库设计。很多同学课设被扣分,不是程序不能跑,而是表结构设计得太随意,字段命名乱,关联关系乱,没有任何注释。所以这一节值得认真对待。
一个合格的点餐系统,最少需要五张表:
- 用户表(user):保存用户和管理员的账号信息,用角色字段区分身份。
- 菜品分类表(category):保存“川菜”“饮品”“主食”等分类。
- 菜品表(dish):保存菜名、价格、图片、描述、所属分类。
- 订单表(orders):保存每一次下单的整体信息,包括下单用户、下单时间、总金额、订单状态。
- 订单明细表(order_item):保存订单里每一条具体菜品记录,包括菜品名、数量、单价、小计。
订单主表和订单明细表为什么要分开?这是一个高频答辩题。原因在于:一张订单可能包含多个菜品,如果所有内容塞在同一行,会有大量冗余且无法反映“一个订单 N 个菜品”的真实关系。用主表 + 明细表的方式,每个订单可以在明细表里对应多行,查询时通过order_id关联即可。
下面给出一个可以直接用的建库建表脚本,字符集和字段类型都是按课设标准写的:
-- 数据库初始化脚本 CREATE DATABASE IF NOT EXISTS restaurant DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE restaurant; -- 用户表 CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL, `real_name` VARCHAR(50) DEFAULT NULL, `phone` VARCHAR(20) DEFAULT NULL, `role` TINYINT DEFAULT 1 COMMENT '1-普通用户,0-管理员', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 菜品分类表 CREATE TABLE `category` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 菜品表 CREATE TABLE `dish` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `category_id` INT NOT NULL, `name` VARCHAR(100) NOT NULL, `price` DECIMAL(10,2) NOT NULL, `image` VARCHAR(255) DEFAULT NULL, `description` VARCHAR(500) DEFAULT NULL, `status` TINYINT DEFAULT 1 COMMENT '1-上架,0-下架', KEY `idx_category` (`category_id`), CONSTRAINT `fk_dish_category` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表 CREATE TABLE `orders` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `total_price` DECIMAL(10,2) NOT NULL, `status` TINYINT DEFAULT 0 COMMENT '0-未处理,1-已接单,2-已完成', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, KEY `idx_user` (`user_id`), CONSTRAINT `fk_orders_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表 CREATE TABLE `order_item` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `order_id` INT NOT NULL, `dish_id` INT NOT NULL, `dish_name` VARCHAR(100) NOT NULL, `price` DECIMAL(10,2) NOT NULL, `quantity` INT NOT NULL, `subtotal` DECIMAL(10,2) NOT NULL, KEY `idx_order` (`order_id`), CONSTRAINT `fk_item_order` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 初始化管理员账号 INSERT INTO `user` (`username`, `password`, `real_name`, `role`) VALUES ('admin', '123456', '管理员', 0); -- 初始化分类和菜品数据 INSERT INTO `category` (`name`) VALUES ('热菜'), ('凉菜'), ('主食'), ('饮品'); INSERT INTO `dish` (`category_id`, `name`, `price`, `status`) VALUES (1, '宫保鸡丁', 28.00, 1), (1, '鱼香肉丝', 26.00, 1), (2, '凉拌黄瓜', 12.00, 1), (3, '米饭', 2.00, 1), (4, '可乐', 4.00, 1);这段 SQL 里值得关注的几个设计点:
- 外键约束
fk_dish_category保证了菜品必须属于一个存在的分类,避免脏数据。 order_item表里冗余保存了dish_name和price,即使菜品信息被修改,历史订单依然能保持当时的快照信息。user表里用role字段区分管理员和普通用户,而不是创建两张表,这是一种常见且简洁的角色设计。
如果你是导入现成源码,项目里通常自带XXX.sql文件。但注意:很多源码里的 SQL 文件是用旧电脑的编辑器导出的,文件编码可能是 GBK,导入后中文变成乱码。建议使用 Navicat 或 DBeaver 导入前先把 SQL 文件转成 UTF-8 编码。
5. 项目核心代码实现
环境有了,数据库也建好了,下面进入整篇文章最关键的代码部分。我会挑四个核心模块来拆解:数据库连接工具、用户登录、菜品列表查询、下单事务。
5.1 数据库连接工具类 DBUtil
传统 JSP + Servlet + JDBC 项目里,最基础的代码是数据库连接工具类。它的作用是把创建连接、关闭连接的重复代码收敛到一起,业务代码只关心 SQL 操作。
// 文件路径:src/com/example/util/DBUtil.java package com.example.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/restaurant?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; private static final String USERNAME = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); if (stmt != null) stmt.close(); if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这个类里最值得注意的配置有两点。一是characterEncoding=utf8,如果不设置,数据库连接时的字符集可能和表结构不一致,导致中文乱码。二是serverTimezone=Asia/Shanghai,MySQL 8 对时区要求更严格,缺了它会报The server time zone value相关错误。
当然,这个写法用的是最原始的 JDBC。如果项目是 Maven 管理,还可以用连接池,比如 C3P0、Druid 或者 HikariCP。课设阶段用DriverManager足够,但如果你在代码里看到Class.forName,也别觉得低级——它帮你把 JDBC 驱动加载、数据库连接这些底层细节串起来了,答辩时讲清楚反而加分。
5.2 用户登录模块
登录模块是点餐系统的入口,它展示了 Servlet 如何处理表单提交、查询用户表、管理 Session。下面是登录 Servlet 的核心逻辑:
// 文件路径:src/com/example/servlet/LoginServlet.java package com.example.servlet; import com.example.util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); // 1. 非空校验 if (username == null || username.trim().isEmpty() || password == null || password.isEmpty()) { request.setAttribute("error", "用户名和密码不能为空"); request.getRequestDispatcher("login.jsp").forward(request, response); return; } // 2. 查询用户 String sql = "SELECT id, username, real_name, role FROM user WHERE username = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { HttpSession session = request.getSession(); session.setAttribute("userId", rs.getInt("id")); session.setAttribute("username", rs.getString("username")); session.setAttribute("role", rs.getInt("role")); // 管理员跳后台,用户跳首页 if (rs.getInt("role") == 0) { response.sendRedirect(request.getContextPath() + "/admin/home.jsp"); } else { response.sendRedirect(request.getContextPath() + "/index.jsp"); } } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } } } catch (SQLException e) { e.printStackTrace(); request.setAttribute("error", "系统异常,请稍后再试"); request.getRequestDispatcher("login.jsp").forward(request, response); } } }这段代码有两个非常重要的工程习惯。
第一,全程使用PreparedStatement而不是拼接字符串,这是防 SQL 注入最基本的手段。如果写成"SELECT ... WHERE username = '" + username + "'",用户输入一段' or '1'='1就能绕过口令检查,这在答辩时大概率会被老师抓住。
第二,用try-with-resources自动释放数据库资源,不需要手动调用close。这是 JDK 7 之后的标准写法,比在finally里做空判断干净得多。
真正的坑其实在第 1 步:很多课设项目登录失败后都用response.sendRedirect跳回登录页,导致错误信息丢在 request 里显示不出来。上面代码用的是forward,错误提示才能通过request.getAttribute("error")呈现在 JSP 页面上。如果你发现自己的项目登录失败没有任何提示,优先检查这里。
5.3 菜品列表查询
用户登录成功后进入首页,最重要的是展示菜品。菜品按分类展示的基础 SQL 是这样的:
// 文件路径:src/com/example/servlet/DishListServlet.java package com.example.servlet; import com.example.util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @WebServlet("/dishList") public class DishListServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String sql = "SELECT d.id, d.name, d.price, d.description, c.name AS categoryName " + "FROM dish d JOIN category c ON d.category_id = c.id " + "WHERE d.status = 1 ORDER BY c.id, d.id"; List<Map<String, Object>> dishList = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Map<String, Object> dish = new HashMap<>(); dish.put("id", rs.getInt("id")); dish.put("name", rs.getString("name")); dish.put("price", rs.getBigDecimal("price")); dish.put("description", rs.getString("description")); dish.put("categoryName", rs.getString("categoryName")); dishList.add(dish); } } catch (Exception e) { e.printStackTrace(); } request.setAttribute("dishList", dishList); request.getRequestDispatcher("index.jsp").forward(request, response); } }这里用JOIN把菜品表和分类表连起来,拿到菜品的同时拿到分类名。很多初学者会写两次查询,先在分类表查出全部分类,再遍历分类查询菜品,这种做法会造成 N+1 查询问题,数据量小无所谓,数据量大了性能会很差。用一条JOIN查询就能解决。
从这条 SQL 出发,还可以扩展出“按分类筛选”“按关键词搜索”“分页查询”等功能。大多数点餐系统源码里,菜品列表页做到“查出来展示”就停了,如果你有时间,给菜品查询加上模糊搜索关键字段WHERE d.name LIKE CONCAT('%', ?, '%'),立刻就能在答辩时多一个技术亮点。
5.4 下单与订单生成——事务处理的核心
点餐系统里最容易做错的就是下单模块。为什么?因为一次下单涉及两条 SQL 操作:
- 往
orders表插入一条订单记录。 - 把订单中的每个菜品逐条插入
order_item表。
这两个操作必须是一个整体:要么都成功,要么都失败。比如订单主表插入成功但明细表插入到一半数据库宕机,餐厅就会收到一笔没有菜品明细的幽灵订单。保证这个整体性的技术叫事务。
JDBC 里控制事务的标准写法是这样:
// 文件路径:src/com/example/servlet/OrderServlet.java(下单核心逻辑) Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入订单主表 String insertOrder = "INSERT INTO orders (user_id, total_price, status) VALUES (?, ?, 0)"; PreparedStatement psOrder = conn.prepareStatement(insertOrder, PreparedStatement.RETURN_GENERATED_KEYS); psOrder.setInt(1, userId); psOrder.setBigDecimal(2, totalPrice); psOrder.executeUpdate(); // 获取自增主键 orderId ResultSet keys = psOrder.getGeneratedKeys(); int orderId = 0; if (keys.next()) { orderId = keys.getInt(1); } // 2. 逐条插入订单明细 String insertItem = "INSERT INTO order_item (order_id, dish_id, dish_name, price, quantity, subtotal) " + "VALUES (?, ?, ?, ?, ?, ?)"; PreparedStatement psItem = conn.prepareStatement(insertItem); for (CartItem cartItem : cartItems) { psItem.setInt(1, orderId); psItem.setInt(2, cartItem.getDishId()); psItem.setString(3, cartItem.getDishName()); psItem.setBigDecimal(4, cartItem.getPrice()); psItem.setInt(5, cartItem.getQuantity()); psItem.setBigDecimal(6, cartItem.getSubtotal()); psItem.addBatch(); // 批量添加到执行队列 } psItem.executeBatch(); conn.commit(); // 全部成功,提交事务 } catch (Exception e) { if (conn != null) { try { conn.rollback(); // 任何一步失败,回滚全部操作 } catch (Exception ex) { ex.printStackTrace(); } } e.printStackTrace(); } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (Exception e) { e.printStackTrace(); } } }这段代码的正确执行顺序值得反复理解:
conn.setAutoCommit(false):关闭自动提交,让后续多条 SQL 处于同一个事务里。- 插入主表后,用
getGeneratedKeys()拿到数据库自动生成的自增orderId。这一步很关键,否则明细表的order_id不知道填什么。 - 明细表插入时使用
addBatch(),把多条 SQL 攒在一起执行,避免逐条与数据库通信。 conn.commit()在全部成功后执行。- 捕获到任何异常,立即
conn.rollback(),保证不会留下半成品数据。 finally里恢复自动提交并关闭连接。
如果有源码,建议你直接在这个文件里搜索setAutoCommit。找不到的话,说明这套源码大概率用了更简单的方式(比如只用一条 SQL 插入订单,根本不管明细表事务),这时候你反而可以自己动手补上——这会是答辩时最大的加分项之一。
6. 前端页面与交互实现
传统 JSP 项目的前端并不复杂,核心就是 JSP 里利用 JSTL 和 EL 表达式把后端传来的数据循环展示。以菜品列表页为例,最关键的代码是这样的:
<%-- 文件路径:web/index.jsp 中的菜品展示部分 --%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> ... <div class="dish-grid"> <c:forEach items="${dishList}" var="dish"> <div class="dish-card"> <h3>${dish.name}</h3> <p>品类:${dish.categoryName}</p> <p>价格:¥${dish.price}</p> <p>${dish.description}</p> <form action="${pageContext.request.contextPath}/addToCart" method="post"> <input type="hidden" name="dishId" value="${dish.id}" /> <input type="number" name="quantity" value="1" min="1" /> <button type="submit">加入购物车</button> </form> </div> </c:forEach> </div>${dishList}是后端通过request.setAttribute("dishList", dishList)存进去的集合,${dish.name}会调用对象或 Map 的取值逻辑。这套写法的好处是页面代码很干净,几乎没有 Java 脚本片段。
不过很多老课设源码里还会看到这种写法:
<% List<Map<String, Object>> list = (List<Map<String, Object>>) request.getAttribute("dishList"); for (Map<String, Object> map : list) { %>它不是错的,只是把 Java 代码直接塞进了 JSP 里,维护起来很痛苦。如果你拿到的源码是这种风格,不影响使用,但自己写新页面时建议用 JSTL。
购物车交互通常有两种实现方式:一种是把购物车存在 Session 里,用户点“加入购物车”后更新 Session 中的列表,提交订单时一次性写入数据库;另一种是每一次点“加入购物车”都直接操作数据库。第一种更常见,因为购物车本质上是用户在当前会话里的临时数据,没必要频繁读写数据库。
前端的核心价值是“串起来”:用户看到的每一个按钮,最终都要对应到某个 Servlet 或者 JSP 动作。如果你发现在源码里一个页面点击后完全没反应,优先检查按钮所在form的action地址和后端@WebServlet的路径是否匹配。
7. 运行验证与项目启动
代码讲完,回到最实际的操作:怎么把这个项目跑起来,并确认它真的在工作。
假设你下载的工程已经存在,且数据库 SQL 已经导入成功,下面是课设项目的通用启动流程:
- 在 IDE 中打开项目,确认项目使用的 SDK 是 JDK 8。
- 在
lib目录下确认mysql-connector-java驱动包存在;如果是 Maven 项目,确认pom.xml里的依赖没有报红。 - 修改
DBUtil.java里的用户名和密码为你本机 MySQL 的用户名密码。 - 启动 Tomcat:IDEA 中通过 Tomcat 配置运行,Eclipse 通过 Servers 视图运行。
- 浏览器访问项目根路径,比如
http://localhost:8080/restaurant/。 - 先用管理员账号登录
admin / 123456,确认后台管理页面能打开。 - 注册一个普通用户,登录后浏览菜品,点几样菜加入购物车,提交订单。
- 回到 MySQL 里执行
SELECT * FROM orders;和SELECT * FROM order_item;,检查两条表里是否都有数据,且order_item.order_id指向同一笔订单。
验证成功的标志不是“页面能打开”,而是完整走通一次用户下单流程,并且拿到真实入库的数据。很多同学跑项目只看了首页,老师问“订单数据存哪了”就答不上来,这就是因为没做端到端验证。
如果启动过程中失败,最常见的几个场景看下一节。
8. 常见问题与排查方法
下面这个表整理的是点餐系统课设项目里出现频率最高的几类问题,基本覆盖了“白嫖源码跑不起来”的九成原因:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 项目启动后访问 404 | 部署的 context path 不对,或访问路径没加项目名 | 查看 IDEA/Eclipse 里 Tomcat 配置的 Application context | 访问时补上项目路径,例如/restaurant/ |
数据库操作报Access denied for user | MySQL 用户名或密码与代码不一致 | 在命令行用相同账号密登录 MySQL | 修改 DBUtil 中的 username / password |
连接数据库报Could not create connection | MySQL 连接串格式不对或驱动版本不匹配 | 检查日志里完整异常堆栈 | MySQL 8 换成com.mysql.cj.jdbc.Driver并加serverTimezone参数 |
| 页面中文全部乱码 | 数据库字符集与项目字符集不一致 | 检查 SQL 文件编码和 JSP 头部的pageEncoding | 统一使用 UTF-8,导入 SQL 前转码 |
| Tomcat 端口被占用 | 8080 端口被其他程序占用 | 命令行执行netstat -ano | findstr 8080 | 关闭占用程序,或修改 Tomcat 的 connector port |
| 上传到 Tomcat 的 war 包无法部署 | Tomcat 版本太高或太低 | 查看 localhost 日志中的详细报错 | 换 Tomcat 8.5 或 9.0,避免新旧 API 不兼容 |
| 页面打开但登录后没有跳转反应 | Servlet 路径与 form action 不匹配 | 浏览器 F12 看提交地址,对照注解路径 | 修正 action 地址为实际@WebServlet路径 |
| 下单提示成功但数据库没有订单 | 代码可能没开启事务,主表插入失败后没回滚 | 在代码里加打印日志,逐步查看执行位置 | 使用前面第 5.4 节的事务写法 |
如果你手头源码的报错不在表里,第一原则是去看 Tomcat 目录下的logs/localhost.2025-xxxx.log,不是看浏览器页面。Tomcat 日志里每一行异常堆栈都会告诉你真正的问题是什么,比到处搜“点餐系统报错”要高效得多。
9. 答辩亮点与项目改造建议
能跑通是底线,能讲清楚是及格线,能说出个人特色才是高分线。下面是针对点餐系统最实用的几个改造方向,工作量不大,但答辩效果很好。
方向一:给密码加密存储。现在代码里很可能是明文密码,直接password = '123456'。你可以引入 MD5 或者 BCrypt,对注册用户的密码做哈希处理。然后解释“数据库就算泄露,攻击者也拿不到原始密码”。这一个改动,几乎必然会成为答辩加分项。
方向二:菜品分页 + 模糊搜索。在DishListServlet里加上LIMIT ?, ?分页参数,并在WHERE条件里用LIKE CONCAT('%', ?, '%')实现按名称搜索。代码量不多,但可以讲清楚“为什么不能直接拼 SQL”。
方向三:订单状态与销量统计。给orders表加一个status字段的更多状态(已接单、已上菜、已完成),管理员后台显示不同状态的订单数量。有精力的话,用一条SELECT dish_name, SUM(quantity) FROM order_item GROUP BY dish_name ORDER BY SUM(quantity) DESC生成销量排行,这就是最简单的数据报表。
方向四:增加历史订单页。普通用户登录后,可以查自己的历史订单。核心 SQL 是SELECT * FROM orders WHERE user_id = ?,再在订单详情里关联查询order_item表。这个功能做完,整个系统的“用户闭环”就完整了。
答辩时常见的提问和思路我列一下:
- “订单和菜品之间是什么关系?” 答:多对多关系,通过订单明细表拆成一对多。
- “下单时如何保证数据完整?” 答:使用事务,两个表要么同时写入,要么同时回滚。
- “为什么要把订单明细里的菜品名和价格存一份?” 答:快照设计,防止菜品修改后历史订单被影响。
- “Session 和 Cookie 有什么区别?” 答:Session 在服务端保存会话状态,Cookie 在浏览器端;点餐系统的登录状态存在 Session 里。
这些问题不需要背答案,只要你把 4、5 两章的内容真正理解了,自然能答出来。
10. 从课设源码到工程思维
最后说一点容易被忽略的东西。很多人做点餐系统课设,目标是“交上去、过答辩”,但如果你未来想找 Java 后端相关的工作,这套课设源码其实可以变成你简历上的话术素材。
面试官看到一个“点餐系统”项目时,他们不关心界面好不好看,也懒得去看你的 CSS,他们只会问几类问题:你设计了几张表?遇到过什么技术难点?怎么做权限控制?数据库连接怎么管理?会不会防止 SQL 注入?这些问题在这篇文章的代码里都已经覆盖了。
建议你在交付前抽出两小时做一件事:打开核心代码文件,逐行加上注释。不是为老师加,是为你自己加。当你能够把DBUtil每一行配置讲明白,把OrderServlet里事务的边界画清楚,把login.jsp的表单提交链路说顺,这个课设就不再是一个“下载并运行”的作业,而是一个真正属于你的 Java Web 项目。
点餐系统虽然名字朴素,但它把 Java Web 开发最核心的三层架构、JDBC 操作、Session 管理、事务控制、前端展示全部串了起来。对大部分 Java 学习者而言,把这类项目吃透,比追着新技术盲目写 demo 要有价值得多。我也建议你之后顺着两条线继续深入:一条是后端框架路线,把 JDBC 换到 MyBatis,把 Servlet 换到 Spring MVC 或 Spring Boot;另一条是前端分离路线,把 JSP 页面换成 Vue 或 React 应用,让后端专注提供 JSON 接口。当你能从 JSP + Servlet 平滑迁移到这两种架构时,你的课设项目就已经升级成有实际工程价值的完整系统了。