
做过JavaWeb课设的同学应该对JSPServlet这套“土生土长”的技术栈不陌生。早餐外卖店管理系统是我带过多次的项目选题它覆盖了一个典型Web系统所需要的大部分知识点注册登录、用户会话、菜单管理、购物车、订单提交、数据库事务再加上JSP页面的数据渲染和JavaScript的前端交互。这篇文章我就基于自己实际跑通的这套“javaweb mysql的jspservlet早餐外卖店管理系统”详细聊聊设计思路、数据库规划、代码实现和踩坑记录。无论你是正在准备课程设计还是刚开始接触JavaWeb希望从零把项目跑起来这篇文章都可以直接当作一份实操笔记来用。1. 项目整体设计与技术选型思路1.1 JSPServlet这套组合的定位现在JavaWeb框架铺天盖地Spring Boot、MyBatis、Vue前后端分离已经成了主流。但课程设计、毕业设计和很多学校实验项目里仍然有一大批题目要求用JSPServletMySQL手写。原因也很简单这套东西能让你看清楚HTTP请求从浏览器出发经过Servlet控制器再在JSP里渲染成HTML的完整过程而不是被框架封装成一个黑盒。我在这套系统里把Servlet当作控制器负责接收请求、调用JDBC操作数据库、再把数据塞进request或session最后转发到JSP页面展示。JSP只做展示和简单逻辑JavaBean或工具类负责数据封装和数据库访问。这种分层虽然简陋但逻辑非常清晰也很适合新手照着理解MVC思想。技术栈选型的另一个考虑是部署简单。项目结构维护得正常的话IDEA配置好Tomcat和MySQL驱动直接就能运行。不需要额外安装Spring依赖也不会被自动配置、注解扫描这些概念绕晕。对演示和学习来说把时间花在核心业务上更划算。1.2 早餐外卖店的功能模块和页面流转早餐外卖店场景特点是小程序式的快速浏览、下单、支付简单不需要复杂的营销模块。我把系统拆成两个大身份。用户端提供注册、登录、退出登录浏览早餐分类和菜品列表按分类或关键词筛选早餐加入购物车、修改数量、清空购物车下单生成订单并清空当前购物车查看个人订单列表和订单详情。管理端提供分类管理与菜品管理包括新增、编辑、删除、上下架图片上传和管理用来展示菜品图片订单管理查看所有订单、修改订单状态比如从“已下单”改为“配送中”再改成“已完成”。页面流转上我采用典型的JSP转发方式。用户访问首页时IndexServlet从数据库查询所有在售菜品和分类转发到index.jsp渲染。用户点击“登录”LoginServlet接收账号密码成功后跳转首页失败则返回错误提示。管理员入口独立AdminLoginServlet验证后进入admin/main.jsp后续的菜品新增、编辑操作都通过独立的AdminServlet处理。URL路径建议统一设计。比如/index、/login、/logout、/cart/add、/order/submit、/admin/dish/list。这样不仅代码里好写JSP里的表单提交地址也一目了然。1.3 开发环境、IDEA配置和依赖整理开发工具我推荐用IDEA装上Tomcat插件后运行JavaWeb项目非常方便。如果你是首次在IDEA里运行JavaWeb项目有几个配置点必须弄清楚否则很容易卡在部署环节。项目结构建议直接创建Maven Web项目主要是因为依赖管理省心。在pom.xml里加入以下核心依赖dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies如果你没有用Maven手动从官网下载MySQL Connector/J把jar包复制到WEB-INF/lib目录下同样可以运行。需要特别注意的是Servlet和JSP的依赖scope要写成provided因为Tomcat已经自带了这两套API如果强行打包进项目反而可能在部署时出现类冲突。IDEA里配置Tomcat的步骤我习惯的顺序是先确保本机已经安装Tomcat 8或9然后在IDEA的Run Configuration里新增Tomcat Server Local点击Configure选择Tomcat目录Deployment页签添加Artifact。Artifact类型要选war exploded这样调试时可以直接访问项目下生成的target目录修改JSP后刷新浏览器就能看见效果不用反复重启Tomcat。2. 数据库设计与底层细节2.1 早餐外卖店的核心表结构数据库是整个系统最值得花心思的部分。以早餐外卖店为例光有“菜品”和“用户”两张表远远不够。我最终把表拆成了六张分别为用户表user、分类表category、菜品表dish、购物车表cart、订单表orders、订单明细表order_item。用户表字段id、username、password、phone、address、create_time。密码不能明文存至少做一次MD5加密哪怕课程演示也别图省事。分类表做简单两级分类比如“粥粉面”“包子点心”“豆浆油条”字段为id、name、sort。菜品表关联分类表字段包括id、category_id、name、price、image、description、status其中status用来标记是否上架。购物车表设计的重点是要绑定用户和菜品。cart表字段为id、user_id、dish_id、quantity、create_time。如果只是演示购物车也可以完全放在Session里但存数据库的好处是用户换台电脑也能恢复购物车而且订单逻辑更好统一。订单表需要单独维护订单状态。常见状态字段可以定义为status取值范围0待支付、1待配送、2配送中、3已完成、4已取消。订单明细表则记录下单时的快照信息菜品名、单价、数量、小计。为什么说快照因为菜品表价格以后会改但已经下的单不能跟着改否则后台订单金额和用户下单时看到的就对不上了。所以订单明细表里必须冗余一份价格和菜名而不是只存dish_id让用户去关联实时菜品表。2.2 MySQL 8.0安装与JDBC连接配置数据库安装本身不复杂但版本选择容易踩坑。我用的是MySQL 8.0因为它比较稳定官方还提供了LTS版本。安装时有MSI安装包和ZIP解压版两种方式。新手建议用MSI图形界面省事。如果喜欢折腾或者不想污染系统环境也可以下载ZIP包解压后执行mysqld --initialize-insecure mysqld --install net start mysql初始化时要注意MySQL 8.0对密码策略默认比较严格。用--initialize-insecure会生成一个无密码的root账号登录后立刻设置新密码比较稳妥。如果你下载的是5.7.44这一类老版本安装过程和8.0略有差别但连接方式基本一样。JDBC连接参数我这里用了8.0的驱动类com.mysql.cj.jdbc.Directory。很多老教程写的是com.mysql.jdbc.Driver那是旧驱动在MySQL 8下虽然也能用但会有提示不建议再用。DBUtil工具类可以直接抄public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/breakfast?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD yourpassword; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }连接URL里我一共加了三个参数useSSLfalse是关闭SSL握手否则MySQL 8默认开着SSL本地开发经常报错serverTimezoneAsia/Shanghai是处理时区问题characterEncodingutf8是保证中文写入不乱码。这三个参数组合基本能解决九成以上的连接报错。2.3 字符集、事务和排序处理JSP页面、Servlet、数据库、JDBC连接四处字符集必须保持一致。我统一使用UTF-8。JSP页面顶部写% page contentTypetext/html;charsetUTF-8 languagejava %Servlet处理POST请求时先执行request.setCharacterEncoding(UTF-8)再用response.setContentType(text/html;charsetUTF-8)。建表语句里默认字符集写成utf8mb4因为utf8mb4比utf8更完整支持emoji外卖菜品描述里偶尔会有特殊符号。订单创建是整个系统里唯一涉及多表写入的操作也是我建议一定要用事务的地方。用户在提交订单那一刻需要同时完成三件事往orders表插一条订单记录、往order_item表插多条明细、清空cart表里该用户的记录。这三件事要么全部成功要么全部失败绝对不能出现订单生成了但明细没写入的情况。JDBC事务的标准写法是Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 插入订单获取自增id // 循环插入订单明细 // 删除购物车记录 conn.commit(); } catch (Exception e) { conn.rollback(); e.printStackTrace(); } finally { conn.setAutoCommit(true); conn.close(); }我一开始没写事务测试时故意在插入明细前抛了个异常结果订单表多了一条废数据。虽然只是课程设计数据库脏了后查问题非常头疼。所以凡是涉及多次写操作的业务都建议顺手加上事务。排序方面菜品列表按照sort字段升序排同一分类下的菜品按照price升序或create_time降序都可以。管理员订单列表默认按照create_time降序让新订单排在最前面。SQL就是SELECT * FROM dish WHERE status 1 ORDER BY sort ASC, id DESC;3. JSP与Servlet的核心实现3.1 用户登录、Session管理和权限控制登录功能是Web系统的标配。我在LoginServlet里处理方式如下接收表单里的username和password密码做MD5后去user表查询查不到就返回“用户名或密码错误”查到则把用户对象放进session再重定向到首页。重定向和转发的选择这里有个细节。如果用request.getRequestDispatcher(index.jsp).forward()浏览器地址栏不会变用户刷新页面时会重新提交表单容易造成重复登录或注册。所以登录成功后应该用response.sendRedirect(index)让浏览器重新发起一次GET请求。Session管理无非是存和取。用户成功登录后HttpSession session request.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60);JSP页面里判断用户是否登录可以直接c:if test${empty sessionScope.loginUser} a hreflogin.jsp登录/a /c:if c:if test${not empty sessionScope.loginUser} 欢迎${sessionScope.loginUser.username} /c:if退出登录就是session.invalidate()然后跳回首页。需要提醒的是管理员后台同样要做登录校验不能只靠隐藏入口来保护。我在AdminBaseServlet里写了一个简单的检查如果session里没有adminUser直接重定向到登录页。这个操作虽然代码只有几行但能避免管理接口被裸访问。3.2 菜品新增、图片上传与访问映射早餐外卖店最看重的就是菜品展示图片处理是绕不开的坑。前端表单里文件上传的form必须设置enctypemultipart/form-data否则后端拿不到上传内容form actionadmin/dish/add methodpost enctypemultipart/form-data input typetext namename placeholder菜品名称 input typenumber nameprice placeholder价格 input typefile nameimage button typesubmit保存/button /formServlet端不建议手动解析multipart数据。Tomcat 8后内置了Part接口直接用request.getPart(image)就能拿到文件流。我习惯先把文件写到项目的upload/目录下然后数据库image字段只存文件名比如baozi.png页面展示时拼接成${pageContext.request.contextPath}/upload/baozi.png。这里有个很容易踩的坑IDEA里直接运行Tomcat时项目部署在target/项目名/目录下代码里request.getServletContext().getRealPath(/upload)拿到的是这个临时目录。如果你本地调试时上传了图片下回重启Tomcat或重新部署图片文件可能会被清掉。我后来选择了绝对路径上传把图片存到Tomcat外部的一个固定目录再在IDEA里配置Tomcat的虚拟映射访问/upload/xxx时映射到这个外部目录。这样课设演示时图片不会莫名消失。如果你不想搞虚拟映射另一个简单方案是把图片转成Base64字符串存MySQL但这样数据库会变得很大且查询渲染性能不好。我建议还是用文件存储更贴近真实项目。3.3 购物车与订单创建流程购物车实现我选择了数据库版本因为早餐外卖店的购物车需要支持用户在不同设备上同步并且演示时显得更完整。购物车表按user_id dish_id唯一索引来避免同一商品重复加入。加入购物车的Servlet逻辑Integer userId user.getId(); int dishId Integer.parseInt(request.getParameter(dishId)); // 查询购物车是否已存在该商品 // 存在则 update quantity quantity 1 // 不存在则 insert quantity 1购物车页面展示时用JOIN查询把dish表的名称、价格和图片带出来。用户修改数量时我通过onchange事件提交到cart/update接口刷新后重新计算总金额。订单创建流程是这套系统的核心。用户提交订单时前端传来的可能是一个购物车商品ID数组。后端先算出总价插入orders表拿到生成的自增主键再遍历订单明细插入order_item最后删除购物车对应记录。这一段代码一定要整体放在事务里前面我讲数据库时已经强调过了。库存或者上下架状态也要在提交订单前检查一遍。外卖早餐有时候卖完就下架了如果用户A已经在购物车里管理员B在后台下架了商品那A提交订单时就应该提示“部分商品已下架”。我的做法是在订单事务开启前先查一遍所有菜品状态如果存在status ! 1直接返回失败提示不再执行后续插入。4. JavaScript与前端交互细节4.1 JSP里的表单校验和购物车小计计算JSP页面里的JavaScript基本都在做三件事表单校验、局部刷新、动态计算。这正好也是网课里常考的几个点。登录注册表单我会在提交前校验用户名长度和手机号格式。注册页面JSP中写一个简单函数function checkForm() { var username document.getElementById(username).value.trim(); var phone document.getElementById(phone).value.trim(); var pwd document.getElementById(password).value; if (username.length 2) { alert(用户名至少2个字符); return false; } if (!/^1[3-9]\d{9}$/.test(phone)) { alert(手机号格式不正确); return false; } if (pwd.length 6) { alert(密码长度至少6位); return false; } return true; }写JSP里的JavaScript要特别小心EL表达式和JS的冲突。比如${dis.price}在JSP里会被渲染成数字但如果JS代码里有${}这种模板字符串写法两者会打架。我一般混用时尽量给EL表达式加引号或者把动态数据放在隐藏域里读取。购物车页面最常用的动态计算是数量增减后小计实时变化。我用了onchange事件让用户修改数量后立刻计算当前行小计和总金额function updateSubtotal(input, price) { var quantity parseInt(input.value); if (isNaN(quantity) || quantity 1) { alert(数量必须大于0); input.value 1; quantity 1; } var subtotal (quantity * price).toFixed(2); document.getElementById(subtotal_ input.getAttribute(data-dish-id)).innerText subtotal; calculateTotal(); }toFixed(2)是为了保留两位小数这是JavaScript里处理金额时的小细节。不要直接让quantity * price显示一长串浮点数否则会出现0.1 0.2这类精度问题。虽然展示层计算只是为了让用户看得方便真正结算仍然以后端算出的总价为准但前端至少看起来舒服。4.2 图片坐标定位、页面接管刷新和事件处理说到“jsp图片如何对坐标定位”我猜大家多半是在做图片地图或者是详情页里点击菜品图上的位置查看介绍。其实最简单的方式是HTML的map标签可以在不引入任何前端框架的情况下给图片不同区域绑定点击事件。举个例子img srcupload/breakfast.jpg usemap#breakfastMap alt早餐套餐 map namebreakfastMap area shaperect coords10,10,80,80 hrefdetail?dishId1 alt粥 area shapecircle coords120,50,30 hrefdetail?dishId2 alt包子 /mapcoords里填的就是图片上的像素坐标矩形用左上角x,左上角y,右下角x,右下角y圆形用圆心x,圆心y,半径。如果需要在JavaScript里动态获取图片元素的位置也可以用document.querySelector(img).getBoundingClientRect()拿到页面坐标。但要注意这个坐标是相对页面视口的不是相对图片本身两者要区分清楚。JSP页面加载完后刷新一次这类需求看起来奇怪但确实有人会问。我提醒一下不要贸然在window.onload里写location.reload()否则页面会陷入无限刷新循环。合理的做法是如果需要在数据更新后刷新页面应该在Servlet端完成操作后response.sendRedirect到目标页面如果只是想到期后刷新页面比如订单支付完成倒计时跳转可以用meta http-equivrefresh content3;urlorderList.jspJavaScript事件处理部分初始化函数不要直接写在head里容易在DOM未加载完成时报错。我习惯统一用window.addEventListener(DOMContentLoaded, function() { // 绑定事件、初始化数据 });这样不管脚本放在页面前面还是后面都能保证DOM已经准备好。遇到IE老版本浏览器addEventListener可能不兼容但本地开发用的浏览器基本都可以忽略这个问题。5. 常见问题排查与避坑实录5.1 IDEA运行报404和类无法加载很多同学第一次在IDEA里跑JavaWeb项目都会遇到404。这个问题的根源往往不是代码而是Tomcat部署路径或Artifact配置。检查顺序我整理成一张表现象大概率原因解决办法访问index.jsp报404项目没有正确部署打开Run Configuration看Deployment里有没有添加ArtifactServlet可以访问但访问/报404欢迎页配置不对检查web.xml里welcome-file-list页面出现ClassNotFoundExceptionjar包没进WEB-INF/lib如果用Maven确认Maven依赖已deploy如果手导jar确认在WEB-INF/lib下修改Servlet代码后不生效没有重新编译IDEA里执行Build或者重启Tomcat还有一类常见问题是Servlet类上的WebServlet注解没有正常生效。Tomcat 8及以上支持注解但如果你在web.xml里又配置了servlet-class两边就冲突了。我建议统一使用注解结构更清爽。如果用了web.xml配置那注解就直接去掉二选一。5.2 MySQL连接报SSL错误与账号密码问题本地开发最常见的三句报错Establishing SSL connection without servers identity verification is not recommended是老版本驱动的警告加上useSSLfalse即可Public Key Retrieval is not allowed是Connector/J 8.0连接MySQL 8时的经典错误同样在URL里加allowPublicKeyRetrievaltrueAccess denied for user rootlocalhost通常是密码错检查DBUtil里的密码是否和安装时设置的一致。MySQL 8默认的认证插件是caching_sha2_password老驱动或者某些可视化工具不兼容。如果你手头有老项目可以执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;不过这条命令只作为兼容方案新项目直接使用8.0对应驱动就好没必要折腾认证插件。5.3 JSP乱码、重复提交和JS报错乱码问题十有八九是字符集没统一。有一个很隐蔽的坑Tomcat 8及以上默认URI编码是UTF-8但如果你用Tomcat 7GET请求的中文参数会乱码。解决方式是在server.xml的Connector里加上URIEncodingUTF-8。重复提交问题主要出现在注册和下单两个动作。注册Servlet里用户第一次POST注册成功后如果刷新页面浏览器会再次提交表单。解决思路有两种注册成功后用sendRedirect跳转或者用Session令牌方式在表单里放一个随机token提交一次就失效。课程设计做到前者就足够了。JS报错一般直接看浏览器控制台。最常见的错误是document.getElementById(xxx)返回null偏偏代码继续执行属性赋值。出现这种问题先检查页面里是否真的有这个id再检查JSP条件渲染有没有把节点隐藏掉。比如管理员菜单只有登录后才显示JS初始化时没判断就直接绑定事件就会报错。这种问题其实不是语法问题而是用户状态和页面结构的匹配没有处理好。这套系统我做下来最大的体会是JSPServlet虽然过时但作为Web入门来说反而能让人理解很多“框架替你隐藏”的真相。比如事务、请求转发与重定向、Session生命周期、文件上传数据流这些概念在Spring Boot里往往被封装成一行注解而手写一遍之后以后再学框架会快很多。如果你也在做类似的管理系统建议不要一直复制粘贴网上的Demo代码把数据库表结构和Servlet控制器的流程自己画一遍再动手写哪怕慢一点最后收获会明显不一样。