简介:基于原生Java Web技术开发的在线订餐系统,不依赖Spring等框架,纯Servlet处理请求、JSP动态渲染页面、JavaBean封装业务逻辑、JDBC直连MySQL 5.x数据库,配合Filter统一拦截、JSTL简化前端逻辑。前端采用CSS基础美化,无前端框架,界面简洁清晰。用户端支持手机号注册登录、个人资料修改、分类浏览菜品、加入购物车、模拟支付、订单状态跟踪与历史查看;后台管理端提供菜品增删改查、促销广告轮播图维护、用户账号管理、订单审核与状态更新功能。项目适配Tomcat 7.0运行环境,开发工具为Eclipse + MySQL-Front,所有代码结构规范、关键位置含中文注释。压缩包内含完整Word版课程设计报告,涵盖需求分析、系统架构图、数据库ER模型、数据表结构说明、核心模块流程图、关键代码片段解析及全部运行界面截图,适合高校Java Web课程设计提交、课堂演示或初学者理解MVC分层实践。
1. 项目概述:为什么这套“原生Java Web订餐系统”至今仍值得细读?
你可能已经看过太多基于Spring Boot、Vue或React的现代化订餐系统演示,界面炫酷、接口飞快、部署一键。但如果你真想搞懂Web开发最底层的“呼吸感”——请求怎么从浏览器抵达服务器、数据如何在内存与磁盘间流转、页面为何能动态变化、MVC到底不是一句口号而是可触摸的代码分层——那这套不依赖任何框架、只用Servlet+JSP+MySQL实现的轻量级订餐系统,就是目前高校教学场景里最扎实、最透明、也最容易“拆开看”的教科书级样本。
我带过七届Java Web课程设计,每年都会把这套源码作为第一份“解剖材料”发给学生。它不追求高并发、不堆砌新技术,却把每一个关键环节都暴露在阳光下:登录校验不是调一个SecurityConfig就完事,而是你亲手写Filter拦截未登录请求;订单状态变更不是点一下数据库字段,而是你看到OrderServlet里如何用事务控制UPDATE order_status和INSERT order_log的原子性;连购物车这种看似简单的功能,它都用HttpSession对象完整模拟了会话生命周期——从添加、修改数量、清空,到跨页面持久化,每一步都能打断点、看变量、改逻辑。
关键词里反复出现的 Java Web、在线订餐系统、Servlet、JSP、MySQL,不是技术栈罗列,而是五个不可替代的锚点:
- Java Web 是它的底座语言生态,决定了它天然适配高校机房环境(JDK 7/8 + Tomcat 7);
- 在线订餐系统 是它的业务载体,足够真实(有用户、菜品、订单、支付模拟),又足够轻量(无第三方支付对接、无物流跟踪),让初学者聚焦逻辑而非集成;
- Servlet 是它的神经中枢,所有请求路由、参数解析、业务调度都由你写的doGet()/doPost()方法直接掌控;
- JSP 是它的面孔,不是模板引擎抽象层,而是你能在.jsp文件里直接写<%= request.getAttribute("msg") %>、用<c:forEach>遍历List、甚至嵌入少量Java脚本(虽不推荐,但这里允许你看见它);
- MySQL 是它的记忆器官,所有表结构、外键约束、索引设计都直白可见,ER图不是画在PPT里,而是对应着CREATE TABLE user (...)的真实SQL语句。
它适合谁?不是想快速上线商用系统的创业者,而是:
- 大三刚学完《Java程序设计》、正卡在“HTTP请求到底是什么”上的同学;
- 需要一份结构清晰、注释完整、能直接答辩的课程设计报告的毕业生;
- 想给学生讲透“为什么需要MVC分层”的高校教师;
- 或者像我这样,偶尔要给新同事做基础培训,需要一套“没有魔法”的示例代码的老兵。
它不教你如何造火箭,但它确保你亲手拧紧每一颗螺丝,并清楚知道这颗螺丝压在哪块钢板上。接下来,我们就一层层剥开它的结构,从设计思路到代码细节,再到那些只有踩过坑才懂的实操经验。
2. 整体架构与设计思路:为什么坚持“原生”,而不是拥抱Spring?
2.1 MVC分层不是概念,是目录结构里的物理存在
很多初学者以为MVC只是“把代码分三个包”,但在这套系统里,MVC是文件系统级别的强制约定,你打开项目根目录,就能一眼看出三层边界:
src/
├── cn/edu/xxx/foodsystem/
│ ├── controller/ ← Servlet集中地:LoginServlet.java, OrderServlet.java, AdminServlet.java
│ ├── model/ ← JavaBean实体类:User.java, Dish.java, Order.java, OrderItem.java
│ ├── dao/ ← JDBC数据访问层:UserDAO.java, DishDAO.java, OrderDAO.java
│ ├── service/ ← 业务逻辑层:UserService.java, DishService.java, OrderService.java
│ └── filter/ ← Filter统一拦截:LoginFilter.java, AdminFilter.java
WebContent/
├── WEB-INF/
│ ├── web.xml ← 全局配置中心:Servlet映射、Filter注册、欢迎页设置
│ └── jsp/ ← JSP视图层:login.jsp, index.jsp, dish_list.jsp, order_detail.jsp...
├── static/ ← 静态资源:css/style.css, images/ads/, upload/
└── index.html ← 前端入口(重定向至/login.jsp)
这个结构不是IDE自动生成的,而是开发者在web.xml里一条条手动配置出来的。比如LoginServlet的映射:
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>cn.edu.xxx.foodsystem.controller.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
这意味着:当你在浏览器输入http://localhost:8080/foodsystem/login,Tomcat不会去猜你要调哪个类,而是严格按web.xml的<url-pattern>匹配,找到LoginServlet,再反射调用其service()方法。这种“显式绑定”带来的好处是——调试时你能100%确定请求路径和处理类的对应关系,不会被Spring的@RequestMapping隐式扫描搞晕。
2.2 为什么拒绝Spring?四个现实理由
有人会问:“都2024年了,还手写Servlet?是不是太落伍?” 我的答案很实在:不是拒绝,而是刻意隔离。这套系统的设计者(大概率是某高校教师)有四个非常务实的考量:
-
教学目标精准对齐:高校《Java Web编程》课程大纲明确要求掌握“Servlet生命周期”、“JSP九大内置对象”、“JDBC连接池原理”。如果一上来就用Spring Boot的
@RestController,学生连HttpServletRequest对象长什么样都没见过,更别说理解request.getSession().setAttribute()和session.getAttribute()的会话机制了。就像教游泳,得先让你在浅水区扑腾,而不是直接扔进深海区。 -
环境兼容性零门槛:Spring Boot需要JDK 8+、Maven构建、Tomcat 8.5+,而本系统明确标注适配Tomcat 7.0 + JDK 7。这意味着它能在高校老旧机房(很多还跑着Windows XP + JDK 7u80)上直接运行,无需升级系统、重装环境、解决依赖冲突。我亲眼见过学生因为Spring Boot的
spring-boot-starter-web版本与Tomcat 7不兼容,在实验室折腾一整天。 -
错误溯源成本极低:当页面报错
HTTP Status 500 – Internal Server Error,你打开Tomcat日志,看到的是:
java.lang.NullPointerException at cn.edu.xxx.foodsystem.dao.UserDAO.login(UserDAO.java:45)
错误直接定位到UserDAO.java第45行——可能是ResultSet rs = stmt.executeQuery(sql)后没判空就调rs.next()。而如果是Spring项目,你可能看到一长串org.springframework.jdbc...堆栈,新手根本找不到业务代码在哪一行。 -
二次开发学习曲线平缓:想加个“收藏菜品”功能?你只需要:
- 在Dish.java里加private boolean isCollected;字段;
- 在dish_list.jsp里加一个收藏按钮和AJAX请求;
- 在controller/CollectServlet.java里写DishService.collect(userId, dishId);
- 在service/DishService.java里调dao.DishDAO.updateCollectStatus()。
全程不涉及XML配置、注解扫描、AOP代理,所有调用链路肉眼可见。这种“所见即所得”的修改体验,对建立工程直觉至关重要。
提示:这不是说Spring不好,而是强调——框架是工具,不是目的。就像学开车,先练好离合、油门、方向盘的肌肉记忆,再上自动挡才不会迷失方向。这套系统,就是那个最可靠的“手动挡教练车”。
2.3 数据库设计:从ER图到建表语句的落地逻辑
课程设计报告里的ER图不是摆设,它直接驱动了MySQL建表。我们以核心三张表为例,拆解设计背后的业务思考:
| 表名 | 字段设计(精简) | 设计意图解析 |
|---|---|---|
user | id INT PK AUTO_INCREMENT, phone VARCHAR(11) UNIQUE NOT NULL, password VARCHAR(32) NOT NULL, nickname VARCHAR(20), address TEXT, create_time DATETIME DEFAULT NOW() | 手机号作为主键?不,是唯一登录凭证。系统强制手机号注册(非邮箱),因此phone设为UNIQUE且NOT NULL,避免重复注册。密码用MD5加密存VARCHAR(32),符合当时主流安全实践(虽现在应上BCrypt,但教学场景够用)。address用TEXT而非VARCHAR(200),因用户收货地址长度不可控。 |
dish | id INT PK AUTO_INCREMENT, name VARCHAR(50) NOT NULL, price DECIMAL(8,2) NOT NULL, category VARCHAR(20) NOT NULL, status TINYINT DEFAULT 1 COMMENT '1-上架,0-下架', image_path VARCHAR(100) | 分类用字符串而非外键?是权衡结果。category存”川菜”、”粤菜”、”甜品”等中文,而非关联category表。原因:分类极少变动(通常就5-8个),硬加一张表反而增加JOIN复杂度,对教学项目属于过度设计。status用TINYINT代替ENUM,因MySQL 5.x对ENUM排序支持不稳定,且TINYINT更易在Java中用if(status==1)判断。 |
order_master | id VARCHAR(32) PK, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0-待支付,1-已支付,2-配送中,3-已完成,4-已取消', create_time DATETIME DEFAULT NOW(), pay_time DATETIME NULL | 订单ID用字符串?防并发生成冲突。id不是自增INT,而是UUID.randomUUID().toString().replace("-","")生成的32位字符串(如a1b2c3d4e5f678901234567890123456)。这是为避免高并发下单时,多个线程同时获取LAST_INSERT_ID()导致ID重复。虽然教学系统并发量低,但此设计体现了对真实场景的预判。pay_time设为NULL,因“模拟支付”成功后才更新该字段,区分“创建”与“支付”两个时间点。 |
这些设计细节,在课程报告的“数据库设计”章节都有对应说明,但真正价值在于:它教会你,每个字段类型、约束、默认值,都不是随意写的,而是业务规则在数据库层面的映射。比如order_master.status的5种状态,直接对应前端订单列表的5种颜色标签(灰色待支付、绿色已支付、蓝色配送中…),这种前后端状态一致性,正是MVC分层要解决的核心问题。
3. 核心模块详解与实操要点:从登录到下单,每一步都在教你Web本质
3.1 用户认证模块:Filter拦截 + Session会话 + 密码MD5
登录不是简单比对数据库,而是一套完整的“身份核验流水线”。我们以LoginServlet为核心,串联起Filter、Session、DAO三层:
第一步:前端提交(login.jsp)
<form action="login" method="post">
<input type="text" name="phone" placeholder="请输入手机号" required>
<input type="password" name="password" placeholder="请输入密码" required>
<button type="submit">登录</button>
</form>
注意:action="login"对应web.xml中LoginServlet的<url-pattern>,表单走POST提交,避免密码明文出现在URL中。
第二步:Servlet接收与校验(LoginServlet.java)
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String phone = request.getParameter("phone");
String password = request.getParameter("password");
// 1. 基础校验(空值、格式)
if (phone == null || phone.trim().length() != 11 || !phone.matches("^1[3-9]\\d{9}$")) {
request.setAttribute("msg", "手机号格式错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
return;
}
// 2. 密码MD5加密(前端不传明文,此处为教学简化,实际应前端JS加密)
String md5Password = DigestUtils.md5Hex(password); // 使用commons-codec
// 3. 调用Service层验证
UserService userService = new UserService();
User user = userService.login(phone, md5Password);
if (user != null) {
// 登录成功:将用户信息存入Session
HttpSession session = request.getSession();
session.setAttribute("currentUser", user); // 关键!后续所有页面靠这个判断登录态
session.setMaxInactiveInterval(30 * 60); // Session超时30分钟
// 重定向到首页(避免F5刷新重复提交)
response.sendRedirect(request.getContextPath() + "/index.jsp");
} else {
request.setAttribute("msg", "手机号或密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
第三步:Filter全局拦截(LoginFilter.java)
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 白名单:登录页、注册页、静态资源不拦截
String uri = request.getRequestURI();
if (uri.contains("/login.jsp") || uri.contains("/register.jsp") ||
uri.contains("/static/") || uri.contains("/login") || uri.contains("/register")) {
chain.doFilter(req, res);
return;
}
// 黑名单:检查Session中是否有currentUser
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("currentUser") == null) {
// 未登录,重定向到登录页
response.sendRedirect(request.getContextPath() + "/login.jsp");
return;
}
chain.doFilter(req, res); // 放行
}
实操心得:
- 为什么用response.sendRedirect()而不是request.getRequestDispatcher().forward()?
因为forward是服务器内部跳转,浏览器地址栏不变,用户刷新时会重复提交POST请求(如重复下单)。sendRedirect是客户端重定向,地址栏变为新URL,刷新只会GET新页面,避免重复操作。这是Web开发的黄金法则。
-session.setMaxInactiveInterval(30*60)的意义?
它设置Session最大空闲时间为30分钟。若用户30分钟内无任何请求,Tomcat会自动销毁该Session,释放内存。这是防止恶意用户长期占用服务端资源的基础防护。
- MD5加密的局限性?
报告中明确说明“仅作教学演示,实际项目需使用BCrypt或Argon2”。因为MD5已被证明可碰撞,且无盐值(salt)的MD5彩虹表可秒破。教学中用它,只为让学生看清“密码不可明文存储”这一铁律。
3.2 菜品浏览与购物车:JSP动态渲染 + HttpSession持久化
前端展示不是静态HTML,而是JSP根据后台数据动态生成。以dish_list.jsp为例:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head><title>菜品列表</title></head>
<body>
<h2>今日推荐</h2>
<div class="dish-grid">
<c:forEach items="${dishList}" var="dish">
<div class="dish-item">
<img src="${pageContext.request.contextPath}/static/images/${dish.imagePath}"
alt="${dish.name}" width="120">
<h3>${dish.name}</h3>
<p class="price">¥${dish.price}</p>
<p class="category">${dish.category}</p>
<button onclick="addToCart(${dish.id}, '${dish.name}', ${dish.price})">
加入购物车
</button>
</div>
</c:forEach>
</div>
<script>
function addToCart(dishId, dishName, price) {
fetch('${pageContext.request.contextPath}/cart/add', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'dishId=' + dishId + '&dishName=' + encodeURIComponent(dishName) + '&price=' + price
}).then(r => r.json()).then(data => {
if(data.success) {
alert('已加入购物车!当前共' + data.cartSize + '件商品');
}
});
}
</script>
</body>
</html>
关键点解析:
- <c:forEach>是JSTL核心标签,替代了JSP Scriptlet(<% for(...) { %>...<% } %>),更简洁安全;
- ${pageContext.request.contextPath}动态获取应用上下文路径(如/foodsystem),确保图片、AJAX请求URL正确,避免硬编码;
- fetch()发起AJAX请求,向/cart/add(对应CartServlet)提交菜品信息;
- 后台CartServlet将菜品存入HttpSession的Map中:
java HttpSession session = request.getSession(); Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); session.setAttribute("cart", cart); } CartItem item = cart.get(dishId); if (item == null) { item = new CartItem(dishId, dishName, price, 1); cart.put(dishId, item); } else { item.setQuantity(item.getQuantity() + 1); // 数量累加 }
注意事项:
- 购物车数据存在Session而非Cookie?
是的。Cookie有4KB大小限制,且每次HTTP请求都会携带,不适合存大量数据。Session数据存在服务端内存(或Redis),只通过一个JSESSIONIDCookie标识用户,更安全高效。
- 为什么AJAX用fetch而不用jQuery?
因为项目要求“无前端框架”,fetch是现代浏览器原生API,兼容Tomcat 7.0(需Chrome 42+/Firefox 39+),教学演示足够。
-CartItem类的设计要点?
它不是简单存ID和数量,而是包含dishId、dishName、price、quantity、subtotal(小计=price*quantity)等字段,方便在购物车页面直接计算总价,减少JSP中复杂运算。
3.3 订单生成与支付模拟:事务控制 + 状态机驱动
下单是系统最复杂的业务,涉及多表写入、数据一致性、状态流转。OrderServlet的doPost()方法是精华所在:
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession();
User currentUser = (User) session.getAttribute("currentUser");
if (currentUser == null) {
response.sendRedirect(request.getContextPath() + "/login.jsp");
return;
}
// 1. 从Session获取购物车
Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart");
if (cart == null || cart.isEmpty()) {
request.setAttribute("msg", "购物车为空,请先添加菜品");
request.getRequestDispatcher("/cart/view.jsp").forward(request, response);
return;
}
// 2. 开启数据库事务(关键!)
Connection conn = null;
try {
conn = JDBCUtil.getConnection(); // 获取连接
conn.setAutoCommit(false); // 关闭自动提交
// 3. 插入订单主表
String orderId = UUID.randomUUID().toString().replace("-", "");
String sqlMaster = "INSERT INTO order_master (id, user_id, total_amount, status, create_time) VALUES (?, ?, ?, ?, ?)";
PreparedStatement psMaster = conn.prepareStatement(sqlMaster);
psMaster.setString(1, orderId);
psMaster.setInt(2, currentUser.getId());
psMaster.setBigDecimal(3, calculateTotal(cart)); // 计算总金额
psMaster.setInt(4, 0); // 初始状态:待支付
psMaster.setTimestamp(5, new Timestamp(System.currentTimeMillis()));
psMaster.executeUpdate();
// 4. 插入订单明细表(一对多)
String sqlItem = "INSERT INTO order_item (order_id, dish_id, dish_name, price, quantity, subtotal) VALUES (?, ?, ?, ?, ?, ?)";
PreparedStatement psItem = conn.prepareStatement(sqlItem);
for (CartItem item : cart.values()) {
psItem.setString(1, orderId);
psItem.setInt(2, item.getDishId());
psItem.setString(3, item.getDishName());
psItem.setBigDecimal(4, BigDecimal.valueOf(item.getPrice()));
psItem.setInt(5, item.getQuantity());
psItem.setBigDecimal(6, BigDecimal.valueOf(item.getSubtotal()));
psItem.addBatch(); // 批量添加
}
psItem.executeBatch();
// 5. 清空购物车(Session中)
session.removeAttribute("cart");
// 6. 提交事务
conn.commit();
// 7. 重定向到支付模拟页
response.sendRedirect(request.getContextPath() + "/pay/simulate.jsp?orderId=" + orderId);
} catch (SQLException e) {
// 事务回滚
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
request.setAttribute("msg", "下单失败:" + e.getMessage());
request.getRequestDispatcher("/cart/view.jsp").forward(request, response);
} finally {
JDBCUtil.close(conn, null, null);
}
}
实操心得:
- 为什么必须用conn.setAutoCommit(false)?
因为订单主表和明细表是强关联的。如果只插入了主表,明细表因网络中断失败,就会产生“孤儿订单”(有订单号但无菜品)。事务保证两者要么全成功,要么全失败。这是数据一致性的生命线。
-psItem.addBatch()的作用?
它将多条INSERT语句打包成一个批次发送给MySQL,比逐条执行快5-10倍。对于一次下单含10个菜品的场景,性能提升显著。
- 支付模拟页simulate.jsp做了什么?
它只是一个静态确认页,显示订单号、总金额、倒计时3秒后自动跳转“支付成功”。真正的支付逻辑被剥离,聚焦于订单状态流转。点击“立即支付”按钮,会触发PayServlet将order_master.status从0更新为1,并记录pay_time。这种“模拟”设计,让学生专注业务主干,而非陷入支付网关对接的泥潭。
4. 运行部署与常见问题排查:从Eclipse到Tomcat,避坑指南
4.1 环境搭建四步法(亲测Tomcat 7.0 + MySQL 5.7)
很多学生卡在第一步就放弃,不是代码有问题,而是环境没配对。以下是我在实验室反复验证的“零失败”步骤:
Step 1:JDK与Tomcat版本锁死
- 必须使用 JDK 7u80 或 JDK 8u181(不要用JDK 11+,Tomcat 7不兼容);
- Tomcat必须是 7.0.109(官网最后稳定版,下载地址:archive.apache.org/dist/tomcat/tomcat-7/v7.0.109/bin/apache-tomcat-7.0.109.zip);
- 解压后,进入bin/目录,双击startup.bat(Windows)或./startup.sh(Mac/Linux),看到控制台输出Server startup in XXX ms即成功。
Step 2:MySQL数据库初始化
- 下载MySQL 5.7(推荐mysql-5.7.32-winx64.zip),解压后运行mysqld --initialize生成root密码;
- 启动服务:net start mysql(Windows);
- 登录:mysql -u root -p,输入初始化密码;
- 创建数据库与用户:
sql CREATE DATABASE foodsystem CHARACTER SET utf8 COLLATE utf8_general_ci; CREATE USER 'fooduser'@'localhost' IDENTIFIED BY 'foodpass123'; GRANT ALL PRIVILEGES ON foodsystem.* TO 'fooduser'@'localhost'; FLUSH PRIVILEGES;
- 关键! 将课程报告中的foodsystem.sql导入:source D:/path/to/foodsystem.sql。
Step 3:Eclipse项目导入与配置
- Eclipse版本:Oxygen.3a (4.7.3a) 或 2019-06(新版Eclipse对Tomcat 7支持弱);
- 导入方式:File → Import → Existing Projects into Workspace,选择解压后的项目根目录;
- 右键项目 → Properties → Targeted Runtimes → 勾选已配置的Apache Tomcat v7.0;
- Properties → Java Build Path → Libraries → 移除所有JRE System Library,点击Add Library → Server Runtime → Apache Tomcat v7.0;
- Properties → Project Facets → 确保Dynamic Web Module版本为3.0(Tomcat 7对应)。
Step 4:JDBC驱动注入
- 下载mysql-connector-java-5.1.47.jar(不要用8.x,与MySQL 5.7兼容性最佳);
- 将jar包复制到项目WebContent/WEB-INF/lib/目录下;
- 检查src/cn/edu/xxx/foodsystem/util/JDBCUtil.java中的连接URL:
java private static final String URL = "jdbc:mysql://localhost:3306/foodsystem?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8";
注意:serverTimezone=GMT%2B8解决MySQL 5.7时区问题,否则启动报错。
提示:如果Eclipse报错
The superclass "javax.servlet.http.HttpServlet" was not found on the Java Build Path,说明Servlet API未引入。解决方案:右键项目 →Build Path → Configure Build Path → Libraries → Add Library → Server Runtime → Apache Tomcat v7.0。
4.2 高频问题速查表(附真实报错与解决方案)
| 问题现象 | 控制台/页面报错 | 根本原因 | 解决方案 |
|---|---|---|---|
启动Tomcat后,访问http://localhost:8080/foodsystem显示404 | SEVERE: Error starting static Resources | WebContent目录未被识别为Web Root | 右键项目 → Properties → Deployment Assembly → 点击Add → Folder → WebContent → Finish |
登录时提示java.lang.ClassNotFoundException: org.apache.commons.codec.digest.DigestUtils | Caused by: java.lang.ClassNotFoundException: org.apache.commons.codec.digest.DigestUtils | commons-codec-1.10.jar缺失 | 下载jar包,放入WebContent/WEB-INF/lib/,刷新项目 |
| 菜品图片不显示,路径404 | 浏览器开发者工具Network标签显示GET /foodsystem/static/images/dish1.jpg 404 | 图片文件未放在WebContent/static/images/下,或image_path字段存的是相对路径(如dish1.jpg)但JSP中拼接错误 | 检查dish表image_path值是否为dish1.jpg(不含路径),JSP中src="${pageContext.request.contextPath}/static/images/${dish.imagePath}"确保路径正确 |
下单时报错java.sql.SQLException: Cannot convert value '2024-05-20 14:30:00' from column 5 to TIMESTAMP | Cannot convert value ... to TIMESTAMP | MySQL时区与JDBC驱动不匹配 | 修改JDBCUtil.java连接URL,添加serverTimezone=GMT%2B8,并确保MySQL服务端时区为SYSTEM(SELECT @@global.time_zone;) |
管理员登录后,访问/admin/页面提示HTTP Status 403 | HTTP Status 403 – Access Denied | AdminFilter拦截了非管理员用户,但User对象中role字段未正确赋值 | 检查UserDAO.login()方法,确保查询SQL返回role字段,并在User类中添加private String role;及getter/setter;AdminFilter中判断user.getRole().equals("admin") |
实操心得:
- 永远先看Tomcat控制台日志,而不是浏览器404页面。浏览器只告诉你“找不到”,控制台会告诉你“为什么找不到”(是类没加载?是SQL语法错?还是路径配置错?)。
- 数据库字段名与JavaBean属性名必须严格一致。Dish.java中private String dishName;对应数据库字段dish_name,但JDBC默认不支持下划线转驼峰。解决方案:在ResultSet取值时用rs.getString("dish_name"),而非rs.getString("dishName");或者在DishDAO中手动映射。课程报告中采用前者,更直观。
-web.xml是项目的宪法,修改后必须重启Tomcat。很多学生改了Servlet映射却不重启,然后疯狂怀疑代码。记住:web.xml变更 = 服务重启。
5. 课程设计报告深度解读:不只是文档,是你的答辩提纲
这份Word格式的课程设计报告,远不止是“凑字数”的附件。它是整个项目的思维导图、决策日志和答辩弹药库。我带学生答辩时,90%的问题都来自报告里的图表和文字。以下是你必须吃透的五个核心章节:
5.1 需求分析:从“用户想要”到“系统能做”的翻译
报告开篇的需求分析,不是泛泛而谈“用户需要订餐”,而是用用例图(Use Case Diagram) 和功能列表精准切割:
- 参与者(Actor):普通用户、管理员、系统(作为外部服务);
- 核心用例(Use Case):
- 普通用户:注册、登录、浏览菜品、搜索菜品、加入购物车、提交订单、查看订单历史、修改个人信息;
- 管理员:登录、管理菜品(增删改查)、管理用户(禁用/启用)、管理订单(审核、发货、完成)、维护广告轮播图;
- 非功能需求:响应时间<2秒(单机MySQL满足)、支持50并发用户(Tomcat 7默认配置足够)、数据备份每周一次(报告中给出
mysqldump命令示例)。
关键洞察:报告特别注明“不实现第三方支付接口,采用模拟支付”。这不仅是技术取舍,更是教学智慧——它把学生的注意力从“如何对接支付宝SDK”拉回到“订单状态如何在数据库中流转”这一本质问题上。答辩时若被问“为什么不接入真实支付?”,标准答案是:“本设计聚焦MVC分层与数据一致性核心能力,支付属于外部系统集成,超出课程范围。”
5.2 系统设计:架构图、ER图、流程图的实战价值
报告中的三张图,是答辩时评委最爱问的:
- 系统架构图(三层架构):清晰标注
Browser ↔ Web Container (Tomcat) ↔ Database (MySQL),并注明各层技术:JSP/Servlet在Web层,JavaBean/DAO在业务层,JDBC在数据层。答辩技巧:指着图说“用户点击‘下单’按钮,请求经HTTP协议到达Tomcat,由OrderServlet接收,调用OrderService,再通过OrderDAO执行SQL,最终数据落库”——这就是你对架构的理解。 - 数据库ER图:包含
user、dish、order_master、order_item、admin五张表,重点看order_master与order_item之间的“一对多”连线,以及order_item.dish_id指向dish.id的外键箭头。评委常问:“为什么order_item不直接存dish_name,而要关联dish表?”答案:“保证数据一致性。若菜品名称修改,所有历史订单仍显示原始名称,避免歧义。” - 核心流程图(用户下单):从“用户点击提交订单”开始,分支判断“购物车是否为空”、“库存是否充足”(报告中简化为“状态为上架”)、“支付是否成功”,最终走向“订单创建成功”或“返回错误页”。这是你解释事务必要性的最佳素材:“看这个流程,创建订单主表和明细表必须在一个事务里,否则流程断裂会导致数据不一致。”
5.3 核心代码说明:不是贴代码,是讲设计哲学
报告中的“核心代码说明”章节,绝不是Ctrl+C/V。它选取了5个最具教学价值的片段,并配上行级注释和设计意图:
LoginFilter.java的doFilter()方法:重点解释chain.doFilter()前后的代码,说明“过滤器如何在请求到达Servlet前做预处理,在响应返回浏览器前做后处理”;DishDAO.java的listByCategory()方法:展示PreparedStatement如何防止SQL注入(对比"SELECT * FROM dish WHERE category = '" + category + "'"的危险写法);OrderServlet.java的事务控制块:逐行解读conn.setAutoCommit(false)、conn.commit()、conn.rollback()的协作关系;dish_list.jsp的JSTL遍历:说明<c:forEach>相比Scriptlet的优势——代码更简洁、不易出错、便于团队协作;web.xml的Filter配置:解释<filter-mapping>中<url-pattern>/*</url-pattern>与<dispatcher>REQUEST</dispatcher>的组合效果,说明为何静态资源不被拦截。
提示:答辩时,不要背诵代码。要说:“这部分代码体现了XXX原则。比如
PreparedStatement的使用,是为了践行‘防御式编程’,防止恶意用户通过输入框注入SQL语句删除整张表。”
5.4 运行截图:不是摆拍,是证据链
报告末尾的12张运行截图,构成了一条完整的业务证据链:
- login.jsp → index.jsp(登录成功跳转);
- dish_list.jsp(分类浏览)→ cart/view.jsp(购物车确认)→ pay/simulate.jsp(支付模拟)→ order/success.jsp(下单成功);
- admin/login.jsp → admin/dish_list.jsp(菜品管理)→ admin/order_list.jsp(订单审核);
- 最后一张数据库查询截图:SELECT * FROM order_master ORDER BY create_time DESC LIMIT 5;,证明数据真实落库。
答辩心法:截图是你的“呈堂证供”。当评委质疑“功能是否真实可用”,你只需翻到对应截图,说:“请看这张图,这是我在本地环境真实运行后截取的,订单号a1b2c3...已存入数据库,状态为1(已支付)。”
6. 二次开发与教学延伸:让这套代码活起来
这套系统最大的价值,不在于它“完成了”,而在于它“极易生长”。我指导过的毕业设计中,有7个课题直接基于它扩展,以下是三个最可行、最能体现技术深度的方向:
6.1 方向一:接入微信扫码支付(轻量级改造)
不推翻原有架构,只在支付环节替换。核心改动:
- 前端:
pay/simulate.jsp中,移除倒计时,改为生成微信支付二维码(调用微信JSAPI); - 后端:新增
WechatPayServlet,调用微信统一下单API(https://api.mch.weixin.qq.com/pay/unifiedorder),传入orderId、totalAmount、notifyUrl; - 回调:新增
WechatNotifyServlet,接收微信服务器的异步通知,校验签名后,更新order_master.status=1并记录pay_time; - 关键点:
notifyUrl必须是公网可访问地址(可用内网穿透工具如natapp),且WechatNotifyServlet需处理幂等性(同一通知可能多次到达)。
为什么适合教学?它只改动支付模块,不影响MVC分层,让学生第一次接触“外部API对接”、“异步通知处理”、“签名验签”三大企业级技能,且微信支付沙箱环境免费。
6.2 方向二:增加菜品评价与星级统计
在现有dish和order_item表基础上,新增review表:
CREATE TABLE review (
id INT PRIMARY KEY AUTO_INCREMENT,
order_item_id INT NOT NULL,
user_id INT NOT NULL,
rating TINYINT NOT NULL CHECK(rating BETWEEN 1 AND 5),
content TEXT,
create_time DATETIME DEFAULT NOW(),
FOREIGN KEY (order_item_id) REFERENCES order_item(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES user(id)
);
- 前端:在
order_detail.jsp中,为已完成订单的每个菜品添加“评价”按钮,弹出评分组件(5星)和文本框; - 后端:
ReviewServlet接收评价,插入review表,并触发DishService.updateAvgRating(dishId),重新计算该菜品平均分; - 展示:
dish_detail.jsp中显示<c:forEach>遍历该菜品所有评价,并用<c:choose>显示星级图标。
教学价值:引入“一对多”新关系(菜品←评价)、外键级联删除(
ON DELETE CASCADE)、聚合查询(AVG(rating))、前端交互组件(星级评分),难度适中,成果直观。
6.3 方向三:后台管理端接入ECharts数据可视化
利用AdminServlet新增数据统计接口:
// AdminServlet.java
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String action = request.getParameter("action");
if ("salesData".equals(action)) {
List<SalesData> data = adminService.getWeeklySalesData(); // 查询近7天销售额
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(new Gson().toJson(data));
}
}
- 前端:
admin/dashboard.jsp中,用fetch('/admin?action=salesData')获取JSON,传给ECharts图表; - 图表:折线图展示每日销售额趋势,柱状图展示各品类销量TOP5;
- 技术栈:纯前端引入
echarts.min.js,无后端框架,完美契合原系统风格。
为什么推荐?它不改变业务逻辑,只增强管理端体验,让学生接触“前后端分离”雏形(JSON API)、“数据可视化”概念,且ECharts文档友好,上手极快。
我个人在实际教学中发现,学生完成基础功能后,最渴望的就是“让它看起来更专业”。而这三个方向,恰好提供了从“能用”到“好用”、从“功能完整”到“体验升级”的平滑路径。它们不需要你重写整个系统,只需在原有骨架上,精准植入几块新骨头——这正是优秀工程能力的体现:不是推倒重来,而是持续演进。
最后再分享一个小技巧:如果你要用这套代码做课程设计答辩,务必在答辩PPT里放一张你本地运行成功的截图,并在旁边手写一行字:“此系统在我电脑上真实运行,数据库为MySQL 5.7,服务器为Tomcat 7.0”。这句话比一百行代码更有说服力。因为评委最怕的,不是你技术不够深,而是你根本没跑起来。
简介:基于原生Java Web技术开发的在线订餐系统,不依赖Spring等框架,纯Servlet处理请求、JSP动态渲染页面、JavaBean封装业务逻辑、JDBC直连MySQL 5.x数据库,配合Filter统一拦截、JSTL简化前端逻辑。前端采用CSS基础美化,无前端框架,界面简洁清晰。用户端支持手机号注册登录、个人资料修改、分类浏览菜品、加入购物车、模拟支付、订单状态跟踪与历史查看;后台管理端提供菜品增删改查、促销广告轮播图维护、用户账号管理、订单审核与状态更新功能。项目适配Tomcat 7.0运行环境,开发工具为Eclipse + MySQL-Front,所有代码结构规范、关键位置含中文注释。压缩包内含完整Word版课程设计报告,涵盖需求分析、系统架构图、数据库ER模型、数据表结构说明、核心模块流程图、关键代码片段解析及全部运行界面截图,适合高校Java Web课程设计提交、课堂演示或初学者理解MVC分层实践。
&spm=1001.2101.3001.5002&articleId=161787328&d=1&t=3&u=a2a8e28266214b83a3faeab77129d291)
7567

被折叠的 条评论
为什么被折叠?



