简介:本资源是一套完整的基于JSP技术开发的毕业设计级网上花店销售系统,面向计算机专业本科生及Java Web初学者,解决课程设计、毕设选题与实战能力提升需求。压缩包共125个文件,涵盖35个JSP页面(实现前端交互与业务跳转)、21个Java类(含ManageServlet、IndexServlet等核心控制器及ProductDao、UserDao等数据访问层)、20个编译后Class文件、7张JPG与6张PNG界面截图、3段MP4演示录像、1个SQL数据库脚本及答辩PPT、项目报告等文档,整体容量438.35MB,结构完整、模块清晰。已有236人学习下载,内容覆盖用户管理、商品浏览、购物车、订单处理、公告发布等典型电商功能,配套源码可直接部署运行,演示视频直观展示操作流程,答辩PPT与报告提供规范写作范式,是Java Web全栈开发实践的优质参考模板。
1. 为什么一个“基于JSP的网上花店销售系统”至今仍是Java Web教学落地的硬核标尺?
你可能在毕业设计选题表里见过它,在导师推荐清单里扫过它,甚至在某份压缩包命名里被它击中过——“基于Jsp的网上花店销售系统设计与实现(项目报告+答辩PPT+源代码+数据库+截图+演示录像).zip”。别小看这个看似陈旧的标题:它不是过时的古董,而是Java Web工程能力的最小完整闭环验证体。它不依赖Spring Boot自动装配、不靠Vue单页应用掩盖后端逻辑、不靠云服务隐藏部署细节——它用最朴素的Servlet+JSP+JDBC三层结构,把用户注册登录、商品浏览下单、购物车管理、订单支付、后台商品/订单/用户管理等真实电商链路,全部压进一个Tomcat容器里跑通。我带过3届毕设学生,凡是能把这个系统从零搭起、改出业务逻辑、调通MySQL事务、导出可演示WAR包的人,后续学Spring MVC或微服务时,对请求生命周期、会话管理、SQL注入防护、JDBC连接池配置的理解,明显比只写过REST API的同学扎实得多。这不是怀旧,是用可控复杂度锤炼工程直觉:当你的Session失效不是因为JWT过期而是web.xml里<session-config>没配对,当购物车数据丢失不是Redis崩了而是JSP页面里request.setAttribute()写成了session.setAttribute()——这些“低级错误”,恰恰是Web开发底层契约最真实的指纹。适合谁?刚学完Servlet生命周期、能手写DAO但还没碰过框架的本科生;想补全Java Web全链路实操经验的转行者;或者需要快速验证某个老旧系统迁移可行性的运维同事。
2. 从零启动:用IDEA新建JSP项目并跑通首页的最小命令集
2.1 创建标准Java Web Module(非Maven)的4个关键动作
很多新手卡在第一步就翻车:用IDEA新建项目时误选“Maven Web Application”,结果引入了不必要的pom依赖和目录结构,导致JSP编译失败或EL表达式不生效。正确路径是绕过Maven,直建传统Web Module:
File → New → Project → Java Enterprise → Web Application(注意勾选“Create web.xml”)- JDK选1.8(JSP 2.3规范要求,高版本需额外配置兼容性)
- 关键一步:取消勾选“Add sample code”,避免生成冲突的index.jsp模板
- Finish后,在
Project Structure → Modules → Dependencies中手动添加Tomcat库(指向本地解压的apache-tomcat-8.5.x目录)
提示:不要用IDEA内置的Tomcat插件启动,必须用外部Tomcat。原因:IDEA的Embedded Tomcat对JSP编译器(Jasper)的classpath控制不透明,常出现
org.apache.jasper.JasperException: Unable to compile class for JSP却找不到具体报错行。
2.2 配置web.xml让JSP能访问静态资源与Servlet
默认生成的web.xml是空壳,必须补全三类基础配置,否则CSS/JS加载404、表单提交404、中文乱码频发:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- 1. 设置默认编码,解决POST中文乱码 --> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 2. 配置JSP编译器参数,避免EL表达式失效 --> <jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <el-ignored>false</el-ignored> <!-- 必须设为false --> <page-encoding>UTF-8</page-encoding> </jsp-property-group> </jsp-config> <!-- 3. 静态资源放行(否则css/js加载404) --> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.css</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.js</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.png</url-pattern> </servlet-mapping> </web-app>参数说明:
el-ignored=false是生死线:若设为true,<%= request.getAttribute("msg") %>这类表达式直接被当成纯文本输出,页面显示空白;SetCharacterEncodingFilter的forceEncoding=true确保所有请求强制UTF-8,否则GET参数(如URL中的中文)仍可能乱码;- 静态资源映射必须显式声明,因为Tomcat 8+默认关闭了
defaultServlet对*.css等扩展名的自动处理。
2.3 编写第一个JSP页面并验证EL表达式与JSTL
创建index.jsp放在web目录下,内容必须包含EL和JSTL双重验证:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>网上花店首页</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <h1>欢迎光临花语时光</h1> <!-- EL表达式验证 --> <p>当前时间:<%= new java.util.Date() %></p> <p>EL测试:<c:out value="${'Hello JSP!'}"/></p> <!-- JSTL循环验证 --> <c:set var="flowers" value="${['玫瑰','百合','康乃馨']}"/> <ul> <c:forEach items="${flowers}" var="f"> <li>${f}</li> </c:forEach> </ul> </body> </html>执行逻辑说明:
- 第一行
<%@ page contentType...%>声明页面编码,防止浏览器解析乱码; <%@ taglib ... %>引入JSTL核心标签库,若未配置JSTL依赖会报org.apache.jasper.JasperException: The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved;<c:out>用于安全输出,自动转义HTML特殊字符;<c:forEach>验证JSTL是否生效,若只显示<li>${f}</li>原始字符串,说明JSTL未加载成功。
JSTL依赖配置(易踩坑点):
将jstl-1.2.jar和standard-1.1.2.jar放入WEB-INF/lib目录(不是Maven依赖),这两个jar必须来自JSTL 1.2官方包,不能用Maven坐标javax.servlet:jstl替代——后者在Tomcat 8+中因包名变更会导致NoClassDefFoundError: javax/servlet/jsp/jstl/core/Config。
3. 数据库驱动与连接池:MySQL 8.0+下JDBC配置的3个致命陷阱
3.1 MySQL 8.0驱动类名变更与SSL握手失败
老教程教的com.mysql.jdbc.Driver在MySQL 8.0+已废弃,新驱动类名为com.mysql.cj.jdbc.Driver。若继续使用旧类名,启动时抛java.lang.ClassNotFoundException。更隐蔽的坑是SSL握手:MySQL 8.0默认启用SSL,而JDBC连接串未显式禁用会导致连接超时:
// 错误写法(无SSL参数,连接超时) String url = "jdbc:mysql://localhost:3306/flower_shop"; // 正确写法(显式禁用SSL并指定时区) String url = "jdbc:mysql://localhost:3306/flower_shop?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true";参数详解:
useSSL=false:关闭SSL握手(开发环境必需,生产环境应配证书);serverTimezone=Asia/Shanghai:解决The server time zone value 'XXX' is unrecognized错误,MySQL 8.0严格校验时区;allowPublicKeyRetrieval=true:应对Public Key Retrieval is not allowed异常,因MySQL 8.0默认禁用公钥检索。
3.2 手写DBUtil工具类实现连接池(不用第三方库)
为避免引入C3P0/HikariCP等依赖增加复杂度,本项目采用Tomcat自带的BasicDataSource(需commons-dbcp2支持)。在src下创建util/DBUtil.java:
package util; import org.apache.commons.dbcp2.BasicDataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { private static BasicDataSource dataSource = new BasicDataSource(); static { // 1. 设置驱动类名(MySQL 8.0+) dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); // 2. 设置JDBC URL(含SSL与时区参数) dataSource.setUrl("jdbc:mysql://localhost:3306/flower_shop?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"); // 3. 设置账号密码 dataSource.setUsername("root"); dataSource.setPassword("123456"); // 4. 连接池参数(关键!) dataSource.setInitialSize(5); // 初始连接数 dataSource.setMaxIdle(10); // 最大空闲连接 dataSource.setMinIdle(2); // 最小空闲连接 dataSource.setMaxOpenPreparedStatements(20); // 预编译语句缓存 dataSource.setTestOnBorrow(true); // 借用连接时检测有效性 dataSource.setValidationQuery("SELECT 1"); // 检测SQL } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void closeConnection(Connection conn) { if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }为什么不用DriverManager.getConnection()?
- 单次连接无复用,高并发下单页刷新10次就创建10个连接,MySQL报
Too many connections; BasicDataSource自动管理连接生命周期,testOnBorrow确保借出的连接有效,避免Connection closed异常;setMaxOpenPreparedStatements(20)缓存预编译语句,减少SQL重复解析开销。
3.3 数据库建表脚本与外键约束的实战取舍
flower_shop数据库需至少5张表:users(用户)、products(商品)、orders(订单)、order_items(订单项)、admin_users(管理员)。关键取舍点在于外键约束:
-- users表(主键id自增) CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, email VARCHAR(100), phone VARCHAR(20), address TEXT, reg_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- products表(无外键,降低耦合) CREATE TABLE products ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, description TEXT, image_path VARCHAR(200), category VARCHAR(50) ); -- orders表(外键指向users,但不指向products) CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, order_no VARCHAR(50) NOT NULL UNIQUE, total_amount DECIMAL(10,2) NOT NULL, status ENUM('pending','paid','shipped','completed') DEFAULT 'pending', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- order_items表(冗余商品信息,避免products表修改影响历史订单) CREATE TABLE order_items ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_name VARCHAR(100) NOT NULL, -- 冗余,不外键 product_price DECIMAL(10,2) NOT NULL, -- 冗余,不外键 quantity INT NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE );为什么order_items不外键products?
- 业务需求:订单生成后,商品价格/名称可能变更(如促销结束涨价),但历史订单需保留当时快照;
- 若设外键,
products表UPDATE会锁表,影响并发; - 冗余字段虽违反范式,但保障数据一致性——这是电商系统的经典权衡。
4. 购物车与订单流程:Session与数据库协同的3层状态管理
4.1 Session级购物车:避免重复添加与库存校验
购物车数据存在HttpSession中,但必须解决两个问题:
- 重复添加:同一商品多次点击“加入购物车”不应叠加数量,而应更新数量;
- 库存校验:添加时需实时查库存,防止超卖。
CartServlet.java核心逻辑:
@WebServlet("/cart") public class CartServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); HttpSession session = req.getSession(); List<CartItem> cart = (List<CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new ArrayList<>(); session.setAttribute("cart", cart); } if ("add".equals(action)) { int productId = Integer.parseInt(req.getParameter("productId")); int quantity = Integer.parseInt(req.getParameter("quantity")); // 1. 查询商品库存(关键!) Product product = ProductDao.findById(productId); if (product.getStock() < quantity) { req.setAttribute("errorMsg", "库存不足,仅剩" + product.getStock() + "件"); req.getRequestDispatcher("/error.jsp").forward(req, resp); return; } // 2. 检查购物车是否已存在该商品 boolean exists = false; for (CartItem item : cart) { if (item.getProductId() == productId) { item.setQuantity(item.getQuantity() + quantity); exists = true; break; } } if (!exists) { cart.add(new CartItem(productId, product.getName(), product.getPrice(), quantity)); } } // ... 其他action(remove/update/clear) resp.sendRedirect(req.getContextPath() + "/cart.jsp"); } }关键设计点:
ProductDao.findById()在添加前查库存,确保原子性(虽非数据库事务,但业务层拦截);CartItem对象封装商品ID、名称、单价、数量,名称与单价冗余存储,避免后续渲染时再查数据库;session.setAttribute("cart", cart)每次操作后更新Session,保证多窗口同步。
4.2 订单生成:从Session购物车到数据库持久化的事务边界
订单创建需跨多表操作(orders、order_items、users),必须用JDBC事务保证一致性:
public static boolean createOrder(int userId, List<CartItem> cartItems) { Connection conn = null; PreparedStatement psOrder = null; PreparedStatement psItem = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入orders主表 String sqlOrder = "INSERT INTO orders (user_id, order_no, total_amount, status) VALUES (?, ?, ?, ?)"; psOrder = conn.prepareStatement(sqlOrder, Statement.RETURN_GENERATED_KEYS); String orderNo = "ORD" + System.currentTimeMillis(); // 简单订单号 psOrder.setInt(1, userId); psOrder.setString(2, orderNo); psOrder.setBigDecimal(3, calculateTotal(cartItems)); psOrder.setString(4, "pending"); psOrder.executeUpdate(); // 2. 获取生成的order_id ResultSet rs = psOrder.getGeneratedKeys(); rs.next(); int orderId = rs.getInt(1); // 3. 批量插入order_items String sqlItem = "INSERT INTO order_items (order_id, product_name, product_price, quantity) VALUES (?, ?, ?, ?)"; psItem = conn.prepareStatement(sqlItem); for (CartItem item : cartItems) { psItem.setInt(1, orderId); psItem.setString(2, item.getProductName()); psItem.setBigDecimal(3, item.getProductPrice()); psItem.setInt(4, item.getQuantity()); psItem.addBatch(); } psItem.executeBatch(); conn.commit(); // 提交事务 return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); // 回滚 } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { DBUtil.closeConnection(conn); // 关闭ps... } }事务边界说明:
conn.setAutoCommit(false)是起点,conn.commit()是终点,中间任何一步异常都触发rollback();psOrder.getGeneratedKeys()获取自增主键,避免二次查询;psItem.addBatch()批量插入提升性能,100条订单项比逐条插入快5倍以上。
4.3 订单状态机:用ENUM字段实现状态流转的硬编码约束
orders.status定义为ENUM('pending','paid','shipped','completed'),而非INT类型,好处是:
- 数据库层强制约束,插入非法值(如
'cancel')直接报错; - 应用层无需维护状态码映射表,
if(status.equals("paid"))直观可读; - 支持SQL直接过滤:
SELECT * FROM orders WHERE status IN ('pending','paid')。
状态流转规则在OrderServlet中硬编码:
// 支付成功后更新状态 if ("pay".equals(action)) { int orderId = Integer.parseInt(req.getParameter("orderId")); // 仅允许从pending→paid if (OrderDao.updateStatus(orderId, "pending", "paid")) { req.setAttribute("msg", "支付成功!"); } else { req.setAttribute("errorMsg", "状态更新失败:当前状态不是待支付"); } }为什么不用状态模式?
- 本项目状态仅4个,且流转路径固定(pending→paid→shipped→completed),硬编码更轻量;
- 状态模式需定义State接口、ConcreteState类、Context类,增加5个类文件,对毕设项目属过度设计。
5. 避坑指南:JSP项目上线前必查的5个血泪故障点
5.1 现象:JSP页面中文显示为方框或问号
原因:
- 页面未声明
<%@ page contentType="text/html;charset=UTF-8" %>; web.xml中CharacterEncodingFilter未配置或forceEncoding=false;- MySQL连接URL缺少
characterEncoding=utf8参数(虽serverTimezone已设,但字符集需单独声明)。
解决:
三处统一UTF-8:JSP页面顶部、web.xml过滤器、JDBC URL追加&characterEncoding=utf8。
5.2 现象:登录后跳转首页,但Session中用户信息为空
原因:
- 登录Servlet中
request.getSession().setAttribute("user", user)后,重定向用了response.sendRedirect()而非request.getRequestDispatcher().forward(); sendRedirect()发起新请求,原Session属性虽存在,但新请求的request对象未携带;- 更隐蔽的是:
web.xml中<session-config><cookie-http-only>true</cookie-http-only></session-config>导致JS无法读取Cookie,但此问题不影响后端Session。
解决:
登录成功后必须用forward()转发到首页,或确保重定向后首页JSP从Session读取user对象(<%= session.getAttribute("user") %>)。
5.3 现象:Tomcat启动报错java.lang.NoClassDefFoundError: javax/servlet/http/HttpServletRequest
原因:
- IDEA项目未正确关联Tomcat Library,
Project Structure → Modules → Dependencies中缺失Tomcat库; - 或
WEB-INF/lib下误删了servlet-api.jar(Tomcat 8+已移除此jar,由容器提供,不应放入lib)。
解决:
在IDEA中右键项目→Add Framework Support → Java EE Web,选择对应Tomcat版本;绝对禁止将servlet-api.jar放入WEB-INF/lib。
5.4 现象:购物车数量更新后,页面显示仍为旧值
原因:
- JSP中使用
<%= cart.size() %>直接输出集合大小,但未在CartServlet中session.setAttribute("cart", cart)更新Session; - 或前端AJAX请求未处理响应,导致DOM未刷新。
解决:
每次购物车操作后,必须session.setAttribute("cart", cart);前端用location.reload()强制刷新,或AJAX成功后用JS更新DOM。
5.5 现象:打包WAR部署到Linux服务器后,图片路径404
原因:
- 开发时用
<img src="images/rose.jpg">,路径相对于WebContent根目录; - Linux服务器Tomcat的
appBase配置为/opt/tomcat/webapps,但WAR解压后images目录权限为root:root,且tomcat用户无读取权限; - 或
web.xml中静态资源映射未覆盖*.jpg。
解决:
- 在
web.xml中补充<servlet-mapping><servlet-name>default</servlet-name><url-pattern>*.jpg</url-pattern></servlet-mapping>; - 部署后执行
chown -R tomcat:tomcat /opt/tomcat/webapps/flower_shop; - 图片路径统一用
<img src="${pageContext.request.contextPath}/images/rose.jpg">,避免硬编码上下文路径。
6. 演示录像与答辩PPT:让评审老师一眼抓住技术亮点的3个镜头语言
6.1 录像脚本设计:用“问题-解决-验证”三幕剧结构
演示录像不是功能流水账,而是讲一个技术故事。我给学生定的10分钟脚本:
| 时间 | 镜头 | 台词要点 | 技术亮点 |
|---|---|---|---|
| 0:00-1:30 | 全景:IDEA界面+Tomcat控制台 | “这是系统架构:JSP做视图,Servlet做控制器,DAO层直连MySQL。启动时控制台显示‘Server startup in XXX ms’,证明容器正常。” | 展示环境纯净性(无Spring等框架干扰) |
| 1:31-4:00 | 分屏:左浏览器操作,右数据库Navicat | “注册用户后,看users表新增记录;加购时,cart表无变化(因存Session),但订单生成后,orders和order_items表同步写入——这验证了事务一致性。” | 突出Session与DB双状态管理 |
| 4:01-7:20 | 特写:浏览器开发者工具Network标签 | “点击支付按钮,抓包看到POST请求发送到/order?action=pay,响应返回JSON{success:true}。再看数据库,status从pending变为paid——状态机生效。” | 用抓包证明前后端交互可靠性 |
| 7:21-10:00 | 全景:PPT切换+代码片段高亮 | “关键代码在这里:DBUtil的连接池配置、CartServlet的库存校验逻辑、OrderDao的事务控制。所有源码已打包,数据库脚本在sql/目录。” | 引导评审关注代码质量而非界面美观 |
血泪经验:录像必须录真实操作过程,禁用剪辑拼接。曾有学生用录屏软件加速播放,结果评审老师暂停发现“点击注册按钮后,页面跳转延迟3秒”,当场质疑“为何响应这么慢?”,暴露了未配置连接池的问题。
6.2 PPT制作:一页一技术点,拒绝文字堆砌
答辩PPT不是Word文档投影,每页只传递一个信息。我坚持的3页铁律:
- 架构页:用Visio画简笔架构图,只标
Browser → JSP/Servlet → DAO → MySQL四层,箭头旁标注技术名词(如“JSP: EL表达式/JSTL”、“DAO: JDBC连接池”); - 核心代码页:截取
DBUtil.java中dataSource.setInitialSize(5)和setTestOnBorrow(true)两行,旁边加红框批注:“连接池最小连接数5,借用时检测有效性,避免空闲连接失效”; - 数据库页:表格对比
products与order_items字段,重点标红product_name和product_price列,批注:“订单项冗余商品信息,保障历史数据一致性”。
注意:PPT绝不放整段代码!评审老师不会读,只会看高亮部分。一页超过3行文字即失败。
6.3 答辩话术:用“为什么”代替“是什么”
学生常犯的错是描述功能:“用户可以注册登录”。正确话术是解释决策:“为什么用Session存购物车而不是数据库?因为购物车是临时状态,高频读写,存Session减少DB压力;为什么订单项不外键商品表?因为要保留历史快照,避免商品下架导致订单数据断裂。”
我训练学生的固定应答结构:
- 承认问题:“您提到的XX问题确实存在”;
- 解释权衡:“当时考虑了A方案(如用Redis),但B方案(Session)更符合毕设目标——验证Java Web基础能力”;
- 给出证据:“代码第XX行实现了XX逻辑,测试时用JMeter模拟100并发,平均响应时间<200ms”。
最后说一句实在话:这个项目的价值,从来不在它多炫酷,而在于你亲手拧紧每一颗螺丝时,对Web容器、HTTP协议、数据库事务的理解,像刻进骨头里的本能。希望帮到你。
本文还有配套的精品资源,点击获取