ARTICLE DETAIL

资讯详情

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

JavaWeb+JSP+MySQL校园订餐系统实战:从源码到数据库完整方案

JavaWeb+JSP+MySQL校园订餐系统实战:从源码到数据库完整方案 简介这是一套面向计算机专业毕设学生与JavaWeb实战学习者的校园订餐前后台管理系统采用Java、Servlet、JSP、jQuery、CSS、JS与MySQL构建配套源码、项目文档和数据库脚本经过严格调试可直接运行适合作为毕业设计或课程设计参考。压缩包共723个文件约9.84MB包含68个jsp页面、32个java源文件、181个js脚本、18个css样式、15个jar依赖及1个sql数据库脚本另有大量gif、jpg等图片素材覆盖前端交互、后端逻辑与数据持久化各层。系统功能完整前台提供用户登录注册、忘记密码、菜品信息与查询、留言管理、会员中心及管理员登录后台涵盖管理首页、管理员管理、网站公告、菜品信息、注册会员、订餐信息、营业信息与留言管理并支持退出登录。目前已有88人学习读者可借此掌握JSPServletMySQL的典型开发流程理解前后台模块划分与数据库设计快速搭建可运行的订餐管理项目。1. 校园订餐系统用 JavaWebjspmysql 落地从源码到数据库一套能跑通的方案校园订餐这个场景说大不大说小也不小。一个食堂档口高峰期几百人排队学生想提前十分钟下单、到点取餐后台要能改菜单、看订单、管库存——这套需求用 JavaWebjspmysql 来实现是很多课程设计和中小型项目里最稳的组合。它不依赖复杂框架Tomcat 一挂就能跑数据库用 MySQL 5.7 或 8.0 都行源码结构清晰前后台分离在 JSP 层面就能完成。适合谁一是要交课程设计、需要完整源码和数据库的学生二是刚接触 JavaWeb、想拿一个真实业务练手 CRUD、分页、会话管理的开发者三是小团队想快速搭一个食堂订餐原型验证流程再决定要不要上 SpringBoot。这篇文章不讲空话直接按“环境怎么配、表怎么建、前后台怎么串、坑在哪”的顺序把一套能复现的路径写清楚。2. 环境选型与项目骨架为什么用 JSP 而不是前后端分离2.1 JavaWebjspmysql 这套组合的适用边界先明确一点JSP 不是过时技术它在“服务端渲染 少量交互”的场景里依然高效。校园订餐系统的核心动作是——学生浏览菜品、加购物车、提交订单管理员查看订单、修改菜品、上下架。这些操作对前端交互要求不高用 JSP 直接输出 HTML配合少量 JavaScript 做表单校验和局部刷新开发速度比 Vue3 后台管理系统快得多。热词里提到的 vue3 后台管理系统、基于 springboot vue 商品管理系统那是另一条路线适合团队里有专门前端、接口需要复用的场景。而 JavaWebjspmysql 的优势在于一个 Java 开发就能把前后台全包部署时只需要 Tomcat MySQL没有 Node.js 构建环节对课程设计和小型项目极其友好。但边界也要说清楚如果订餐系统要对接微信小程序、要做实时推送、要支撑上千并发JSP 的同步请求模型会吃力。这时候常见做法是保留 MySQL 数据库把后端换成 SpringBoot 提供 JSON 接口前端交给小程序或 Vue。所以这套源码的价值不只是“能跑”更是让你理解订单状态流转、库存扣减、会话保持这些业务逻辑后面换框架时这些逻辑照样用。2.2 开发环境搭建IDEA Tomcat MySQL 的版本匹配热词里 idea 运行 javaweb 项目配置、mysql 安装配置教程、mysql 5.7.44 安装过程详细 都是高频问题。我一般会固定一套版本组合避免玄学问题组件推荐版本说明JDK1.8 或 11JSP 项目用 1.8 最稳Tomcat 9 以下都兼容Tomcat8.5 或 9.0不要用 Tomcat 10Servlet 包名从 javax 变成 jakarta老项目直接 404MySQL5.7.44 或 8.05.7 和 8.0 驱动类名不同下面会讲IDEA2021 以上社区版够用注意配置 Artifacts驱动mysql-connector-java-5.1.495.7或 8.0.288.0版本不匹配会报时区或 SSL 错误安装 MySQL 时如果用 8.0记得在连接串里加serverTimezoneAsia/Shanghai否则报The server time zone value错误。5.7.44 是 5.7 系列最后一个版本热词里有人问为什么之后是 5.7.43其实 5.7.44 就是收尾版用 5.7.44 没问题。安装完成后用命令行验证mysql -u root -p # 输入密码后执行 show databases; # 能看到 information_schema、mysql、performance_schema 就正常IDEA 里配置 Tomcat 的步骤Run → Edit Configurations → 加 Tomcat Server → Local → 指定 Tomcat 安装目录 → 在 Deployment 里加 Artifactwar exploded。这里有个血泪经验Artifact 的 Application context 建议设成/否则访问路径会多一层项目名JSP 里的相对路径容易翻车。2.3 项目目录结构与关键文件说明一套典型的 JavaWeb 订餐系统目录如下src/ com.campus.order/ dao/ # 数据库操作JDBC 封装 entity/ # 实体类User、Dish、Order、OrderItem servlet/ # 控制器LoginServlet、DishServlet、OrderServlet util/ # DBUtil 连接池、MD5 工具 WebContent/ css/ js/ images/ admin/ # 后台 JSP菜品管理、订单管理 user/ # 前台 JSP菜单、购物车、我的订单 WEB-INF/ web.xml # Servlet 映射、过滤器配置 lib/ # mysql-connector-java.jar、jstl.jarWEB-INF/lib下必须放 MySQL 驱动和 JSTL 标签库否则 JSP 里用c:forEach会报找不到标签。DBUtil 里我一般用简单的 DriverManager 获取连接课程设计够用如果要做连接池加 Druid 或 C3P0 也行但要多引一个 jar。实体类字段和数据库表字段用驼峰对下划线映射手写或 MyBatis 都行这套源码里通常是手写 JDBC。3. 数据库设计与建表订单、菜品、用户三张核心表怎么定3.1 表结构设计与字段类型选择校园订餐系统的数据库不需要太复杂但几个关键点必须定好。下面是我常用的建表 SQL直接在 MySQL 里执行CREATE DATABASE campus_order DEFAULT CHARACTER SET utf8mb4; USE campus_order; -- 用户表学生和管理员共用用 role 区分 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存 MD5 后的值 real_name VARCHAR(50), phone VARCHAR(20), role TINYINT DEFAULT 0, -- 0 学生 1 管理员 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 菜品表 CREATE TABLE dish ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, -- 库存下单扣减 image VARCHAR(255), -- 图片相对路径 category VARCHAR(50), -- 分类主食/饮品/小吃 status TINYINT DEFAULT 1, -- 1 上架 0 下架 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 订单表 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, -- 0 待取餐 1 已完成 2 已取消 remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB; -- 订单明细表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, dish_id INT NOT NULL, dish_name VARCHAR(100), -- 冗余存名称防止菜品改名后订单显示错乱 price DECIMAL(10,2), quantity INT DEFAULT 1, FOREIGN KEY (order_id) REFERENCES orders(id) ) ENGINEInnoDB;字段说明password用 VARCHAR(64) 存 MD5 值不要明文dish.stock是下单扣减的关键后面会讲并发问题order_item里冗余dish_name和price这是血泪经验——如果只存 dish_id菜品改名或调价后历史订单显示会变对账就乱了。status用 TINYINT 而不是 VARCHAR查询和索引效率更高。3.2 初始化数据与管理员账号建完表后插入测试数据-- 管理员账号密码是 123456 的 MD5 值 INSERT INTO user (username, password, real_name, role) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, 管理员, 1); -- 几个菜品 INSERT INTO dish (name, price, stock, category, status) VALUES (红烧肉盖饭, 15.00, 50, 主食, 1), (宫保鸡丁盖饭, 14.00, 40, 主食, 1), (可乐, 3.50, 100, 饮品, 1), (薯条, 6.00, 30, 小吃, 1);MD5 值e10adc3949ba59abbe56e057f20f883e对应123456可以在 Java 里用DigestUtils.md5Hex(123456)生成或者直接抄这个值。注意 MySQL 8.0 默认字符集是 utf8mb4插入中文没问题如果是 5.7 且建库时没指定可能出现乱码用SHOW VARIABLES LIKE character%检查。3.3 JDBC 连接工具类与驱动版本坑DBUtil 是每个 Servlet 都要用的写一个静态方法获取连接package com.campus.order.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // MySQL 5.7 用 com.mysql.jdbc.Driver // MySQL 8.0 用 com.mysql.cj.jdbc.Driver private static final String DRIVER com.mysql.cj.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/campus_order ?useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动加载失败检查 WEB-INF/lib 下是否有 jar, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }逻辑说明Class.forName在静态块里只执行一次避免每次请求都加载驱动。URL 里的useSSLfalse解决 MySQL 8.0 的 SSL 连接错误热词里 mysql ssl 连接错误 多半就是这个原因serverTimezone解决时区报错。参数怎么改如果 MySQL 是 5.7DRIVER 换成com.mysql.jdbc.DriverURL 里可以去掉serverTimezone。如果连接池用 Druid把 DriverManager 换成 DruidDataSource 即可但课程设计没必要。4. 前后台功能实现从登录到下单的完整链路4.1 登录与权限过滤Session Filter 的配合登录 Servlet 的核心逻辑WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password DigestUtils.md5Hex(req.getParameter(password)); UserDao dao new UserDao(); User user dao.findByUsernameAndPassword(username, password); if (user null) { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } req.getSession().setAttribute(currentUser, user); // 根据角色跳不同后台 if (user.getRole() 1) { resp.sendRedirect(req.getContextPath() /admin/dishList.jsp); } else { resp.sendRedirect(req.getContextPath() /user/menu.jsp); } } }逻辑说明密码先 MD5 再比对数据库里不存明文。登录成功后把 User 对象放进 Session后续页面用session.getAttribute(currentUser)取。权限控制用一个 FilterWebFilter(urlPatterns {/admin/*, /user/*}) public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; User user (User) request.getSession().getAttribute(currentUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } // 后台路径只允许管理员 String uri request.getRequestURI(); if (uri.contains(/admin/) user.getRole() ! 1) { response.sendRedirect(request.getContextPath() /user/menu.jsp); return; } chain.doFilter(req, resp); } }参数说明urlPatterns拦截/admin/*和/user/*登录页和静态资源不拦。注意 Filter 里不要用req.getRequestDispatcher转发用sendRedirect避免路径混乱。4.2 菜品展示与购物车JSP 页面数据绑定前台菜单页用 JSTL 遍历菜品% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table c:forEach items${dishList} vardish tr tdimg src${pageContext.request.contextPath}/${dish.image} width80//td td${dish.name}/td td${dish.price}/td td库存${dish.stock}/td td c:if test${dish.stock 0} a href${pageContext.request.contextPath}/cart?actionadddishId${dish.id}加入购物车/a /c:if c:if test${dish.stock 0}已售罄/c:if /td /tr /c:forEach /table逻辑说明dishList由 Servlet 查询后req.setAttribute传入。图片路径用${pageContext.request.contextPath}拼相对路径避免部署路径变化导致 404。热词里 jsp 图片如何对坐标定位如果要做图片热区用img usemap或 CSSposition:absolute配合left/top但订餐系统一般不需要直接展示即可。购物车用 Session 存一个 Map// CartServlet 中 MapInteger, Integer cart (MapInteger, Integer) req.getSession().getAttribute(cart); if (cart null) cart new HashMap(); String action req.getParameter(action); int dishId Integer.parseInt(req.getParameter(dishId)); if (add.equals(action)) { cart.put(dishId, cart.getOrDefault(dishId, 0) 1); } else if (remove.equals(action)) { cart.remove(dishId); } req.getSession().setAttribute(cart, cart); resp.sendRedirect(cart.jsp);参数说明key 是 dishIdvalue 是数量。购物车不落库提交订单时才写orders和order_item。这样简单但要注意 Session 过期后购物车丢失属于可接受范围。4.3 下单与库存扣减事务处理不能省下单是最容易出问题的地方。先插订单再插明细最后扣库存三步必须在一个事务里public boolean createOrder(int userId, MapInteger, Integer cart, String remark) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 计算总价并检查库存 BigDecimal total BigDecimal.ZERO; for (Map.EntryInteger, Integer e : cart.entrySet()) { Dish dish dishDao.findById(conn, e.getKey()); if (dish.getStock() e.getValue()) { throw new RuntimeException(dish.getName() 库存不足); } total total.add(dish.getPrice().multiply(new BigDecimal(e.getValue()))); } // 2. 插入订单 int orderId orderDao.insert(conn, userId, total, remark); // 3. 插入明细并扣库存 for (Map.EntryInteger, Integer e : cart.entrySet()) { Dish dish dishDao.findById(conn, e.getKey()); orderItemDao.insert(conn, orderId, dish, e.getValue()); dishDao.reduceStock(conn, e.getKey(), e.getValue()); } conn.commit(); return true; } catch (Exception ex) { if (conn ! null) try { conn.rollback(); } catch (SQLException ignored) {} throw new RuntimeException(下单失败 ex.getMessage()); } finally { if (conn ! null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} } }逻辑说明setAutoCommit(false)开启事务任何一步失败都 rollback。库存检查放在扣减之前避免超卖。注意reduceStock的 SQL 要加条件UPDATE dish SET stock stock - ? WHERE id ? AND stock ?这样即使并发数据库层面也能保证不会扣成负数。热词里 mysql 事务处理 在这个场景就是必须的否则订单插入了、库存没扣或者库存扣了、订单没插数据就乱了。5. 避坑与排查JSPMySQL 项目最常见的 5 个翻车点5.1 中文乱码从请求到响应全链路排查现象页面输入中文存到数据库变成???或者查询出来显示乱码。原因通常有三层JSP 页面编码、请求编码、数据库编码。解决JSP 头部加% page contentTypetext/html;charsetUTF-8 %Servlet 里req.setCharacterEncoding(UTF-8)和resp.setContentType(text/html;charsetUTF-8)数据库连接串加useUnicodetruecharacterEncodingutf8建库时DEFAULT CHARACTER SET utf8mb4。Tomcat 8 以上默认 URI 编码是 UTF-8一般不用改 server.xml但如果用 GET 传中文还是建议在 server.xml 的 Connector 加URIEncodingUTF-8。5.2 404 与 500路径、驱动、jar 包三查现象访问 Servlet 报 404或者报 500 且堆栈提示ClassNotFoundException: com.mysql.jdbc.Driver。原因404 多半是WebServlet路径和访问路径不一致或者 web.xml 里 metadata-complete 设了 true 导致注解不生效500 驱动类找不到是WEB-INF/lib下没放 mysql-connector-java.jar或者 MySQL 8.0 用了 5.1 的驱动类名。解决检查WebServlet(/login)和访问的login是否一致确认 jar 在 lib 下且被 IDEA 识别为库8.0 用com.mysql.cj.jdbc.Driver5.7 用com.mysql.jdbc.Driver。5.3 时区与 SSL 报错连接串参数别漏现象启动时报The server time zone value ?D1ú±ê×?ê±?? is unrecognized或WARN: Establishing SSL connection without servers identity verification is not recommended。原因MySQL 8.0 默认时区是 UTC且默认要求 SSL。解决连接串加serverTimezoneAsia/ShanghaiuseSSLfalse。如果 MySQL 5.7 也报时区同样加serverTimezone。注意useSSLfalse只是关闭 SSL 警告生产环境另说课程设计够用。5.4 库存超卖并发下的扣减顺序现象两个学生同时下单最后一份红烧肉结果两份都成功了库存变成 -1。原因先查库存再扣减中间有时间窗口。解决扣减 SQL 加AND stock ?根据executeUpdate返回的影响行数判断是否成功返回 0 就抛异常回滚。这是最轻量的乐观锁不需要额外字段。如果并发更高可以用SELECT ... FOR UPDATE行锁但课程设计用条件更新就够了。5.5 JSP 页面刷新与重复提交现象下单成功后按 F5 刷新又提交了一次订单。原因表单提交后直接 forward 到结果页浏览器刷新会重复上一次 POST。解决提交成功后用resp.sendRedirect重定向到订单列表页而不是 forward。热词里 jsp 页面让加载完后刷新一次如果是想定时刷新订单状态用meta http-equivrefresh content30但要注意别在提交页用否则重复提交。6. 进阶技巧用一条 SQL 看订单统计与 JSP 分页的实用写法后台管理里管理员最常看的是“今日订单数、总金额、菜品销量排行”。这些不需要在 Java 里循环累加一条 SQL 就能出-- 今日订单统计 SELECT COUNT(*) AS order_count, IFNULL(SUM(total_price), 0) AS total_amount FROM orders WHERE DATE(create_time) CURDATE() AND status ! 2; -- 菜品销量排行 SELECT oi.dish_name, SUM(oi.quantity) AS sold FROM order_item oi JOIN orders o ON oi.order_id o.id WHERE o.status ! 2 GROUP BY oi.dish_name ORDER BY sold DESC LIMIT 10;IFNULL防止没有订单时返回 nullstatus ! 2排除已取消订单。这两条 SQL 直接放在 DAO 里返回 Map 或 ListJSP 用c:forEach渲染即可。分页也是后台必备。我一般用“每页 10 条 两个参数”的写法SELECT * FROM dish WHERE status 1 ORDER BY id DESC LIMIT ?, ?Java 里page从 1 开始offset (page - 1) * pageSize。总页数用SELECT COUNT(*)单独查。JSP 底部渲染页码c:forEach begin1 end${totalPages} varp a hrefdishList.jsp?page${p} class${p currentPage ? active : }${p}/a /c:forEach注意LIMIT的两个参数是“偏移量”和“条数”不是“页码”和“条数”这是新手最容易写错的地方。另外 MySQL 排序 如果按价格或时间记得给price、create_time加索引数据量大了分页才不会慢。最后说一个我自己的习惯每次改完 DAO 里的 SQL先在 MySQL 命令行里跑一遍确认结果对了再写进 Java。JSP 页面报错时先看 Tomcat 控制台堆栈再看浏览器 F12 的 Network 响应八成问题出在路径或编码上。这套校园订餐系统源码数据库核心价值不在代码多高级而在它把“用户-菜品-订单-库存”这条链路完整跑通了你把这套逻辑吃透换 SpringBoot 或 Vue3 只是换皮。希望帮到你。本文还有配套的精品资源点击获取
返回列表