
简介这份毕业设计资源是一套基于JSP的网上玩具销售系统完整项目包面向计算机相关专业学生与Java Web初学者可用于课程设计、毕业设计参考或电商类项目练手。系统采用JSP结合Servlet、JavaBeans实现服务端逻辑涵盖商品浏览、购物车、用户中心、订单与支付接口等典型电商模块并配套数据库脚本与项目报告便于理解前后端交互与数据库设计思路。压缩包为rar格式共1059个文件约23.2MB包含76个jsp页面、98个html、29个css及226个js等前端资源另有7个java源文件、7个class、5个jar与1个sql脚本支撑后端与数据层图片素材以gif、jpg、png为主。目前已有390人学习下载适合需要完整赛题方案、模块化代码参考与排错思路的读者可据此快速搭建运行环境并二次开发。1. 基于 JSP 的网上玩具销售系统从选题到跑通一个毕设项目的完整落地路径每年到了毕设季计算机方向的学生最头疼的不是写代码而是选一个“能跑通、能答辩、工作量够”的题目。基于 JSP 的网上玩具销售系统就是这样一个经典选题——它属于 Java Web 方向技术栈成熟资料多但真正动手做的时候很多人卡在环境搭建、数据库设计、页面跳转和部署上。这个题目的核心价值在于它覆盖了 Java Web 开发的全链路从 JSP 页面渲染、Servlet 请求处理、JDBC 数据库操作到 Session 会话管理一个项目就能把整个技术栈串起来。适合计算机科学与技术、软件工程、信息管理等相关专业的本科生也适合那些想通过一个完整项目入门 Java Web 的开发者。下面我会按实际做项目的顺序把选型、环境、数据库、核心功能、避坑和进阶技巧一层层拆开讲清楚。2. 技术选型与环境搭建为什么用 JSP 而不是 Spring Boot2.1 选 JSP 做毕设的真实理由很多人一上来就问都 2025 年了为什么还用 JSP这个问题在毕设场景下有非常现实的答案。JSP 是 Java Web 的“原教旨”技术它把 Java 代码直接嵌入 HTML学习曲线平缓不需要理解复杂的依赖注入、AOP 切面、自动配置这些 Spring 生态的概念。对于毕设来说导师看的是你能不能独立完成一个完整的 Web 项目而不是你用了多新的框架。JSP Servlet JDBC 这套组合代码量适中逻辑清晰答辩的时候每一行代码你都能讲清楚来龙去脉。另一个现实因素是资料丰富度。基于 JSP 的毕设选题在各大高校的题库里存在了十几年网上能找到大量的参考代码、论文模板和答辩问题。你遇到的大多数问题比如中文乱码、数据库连接失败、Tomcat 启动报错都有现成的解决方案。相比之下如果你选了一个冷门的框架组合光是环境配置就能耗掉两周最后代码没写几行时间全花在查文档上了。当然JSP 也有它的边界。它不适合做前后端分离的大型项目页面逻辑和业务逻辑容易混在一起代码维护性差。但对于一个玩具销售系统这种中小型毕设来说这些缺点在可控范围内。我的建议是用 JSP 做页面展示和表单提交用 Servlet 做控制器用 JDBC 做数据访问分层清晰答辩的时候也能体现出你的设计思想。2.2 开发环境搭建IDEA Tomcat MySQL 的最小可用组合环境搭建是第一个拦路虎。我见过太多人在这里翻车所以把每一步都写清楚。你需要准备三样东西JDK建议 1.8 或 11、Tomcat建议 8.5 或 9.0、MySQL建议 5.7 或 8.0。IDE 用 IntelliJ IDEA 社区版就够不需要旗舰版。先装 JDK配置好 JAVA_HOME 环境变量。然后在 IDEA 里新建项目选择 Java Enterprise勾选 Web Application。这里注意IDEA 社区版没有 Java Enterprise 选项你需要用旗舰版或者手动创建 Maven 项目再添加 Web 框架支持。手动创建的方式更通用下面给出 Maven 的 pom.xml 配置。project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.toy/groupId artifactIdtoy-shop/artifactId version1.0-SNAPSHOT/version packagingwar/packaging properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies !-- Servlet APITomcat 已提供scope 设为 provided -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API同样由 Tomcat 提供 -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- JSTL 标签库用于页面循环和判断 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies build finalNametoy-shop/finalName plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-war-plugin/artifactId version3.3.2/version /plugin /plugins /build /project这段配置里几个关键点packaging必须是war因为要部署到 TomcatServlet 和 JSP 的 API 依赖 scope 设为provided意思是编译时需要但打包时不带进去因为 Tomcat 容器里已经有这些类了JSTL 和 MySQL 驱动需要打包进 war 文件。版本号方面MySQL 驱动 8.0.x 对应 MySQL 8.0 数据库如果你用的是 MySQL 5.7驱动版本换成 5.1.49。配置好 pom.xml 后在 IDEA 右侧 Maven 面板点刷新依赖会自动下载。然后在src/main下创建webapp目录再在webapp下创建WEB-INF目录WEB-INF下放web.xml。完整的目录结构是这样的toy-shop/ ├── pom.xml ├── src/ │ └── main/ │ ├── java/ │ │ └── com/toy/ │ │ ├── dao/ # 数据访问层 │ │ ├── entity/ # 实体类 │ │ ├── servlet/ # 控制器 │ │ └── util/ # 工具类 │ └── webapp/ │ ├── WEB-INF/ │ │ └── web.xml │ ├── css/ │ ├── js/ │ ├── images/ │ ├── index.jsp │ └── ...Tomcat 的配置在 IDEA 里通过 Run/Debug Configurations 添加。点左上角 Add Configuration选 Tomcat Server → Local在 Application server 里指定你解压的 Tomcat 目录然后在 Deployment 标签页点加号选 Artifact → toy-shop:war exploded。Application context 设为/toy-shop。这样启动后访问http://localhost:8080/toy-shop就能看到你的页面。注意Tomcat 10 及以上版本把 javax.servlet 包名改成了 jakarta.servlet如果你用 Tomcat 10上面的 pom.xml 需要把 javax.servlet 全部替换成 jakarta.servlet否则启动会报 ClassNotFoundException。毕设建议用 Tomcat 9稳定且资料多。3. 数据库设计与实体建模玩具销售系统需要哪些表3.1 六张核心表的设计与字段说明网上玩具销售系统的数据库不需要太复杂但也不能太简单否则答辩的时候工作量不够。我一般会设计六张表用户表、商品表、商品分类表、订单表、订单明细表、购物车表。下面给出建表 SQL字段类型和约束都经过实际验证。-- 用户表 CREATE TABLE user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, email VARCHAR(100), phone VARCHAR(20), address VARCHAR(255), role TINYINT DEFAULT 0 COMMENT 0-普通用户 1-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 商品分类表 CREATE TABLE category ( category_id INT PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(50) NOT NULL, sort_order INT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 商品表 CREATE TABLE product ( product_id INT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(100) NOT NULL, category_id INT, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, image_url VARCHAR(255), description TEXT, status TINYINT DEFAULT 1 COMMENT 0-下架 1-上架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES category(category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE orders ( order_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, order_no VARCHAR(32) NOT NULL UNIQUE, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0-待付款 1-已付款 2-已发货 3-已完成 4-已取消, receiver_name VARCHAR(50), receiver_phone VARCHAR(20), receiver_address VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 CREATE TABLE order_item ( item_id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, unit_price DECIMAL(10,2) NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(order_id), FOREIGN KEY (product_id) REFERENCES product(product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 购物车表 CREATE TABLE cart ( cart_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, product_id INT NOT NULL, quantity INT DEFAULT 1, add_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(user_id), FOREIGN KEY (product_id) REFERENCES product(product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字符集统一用utf8mb4这样商品名称里如果有特殊符号或者 emoji 也不会乱码。订单表里冗余了收货人信息这是有意为之——用户修改个人地址后历史订单的收货信息不应该跟着变。订单明细表里存了unit_price也是同样的道理商品调价后历史订单的金额不能变。这些细节在答辩的时候能体现出你对业务的理解。3.2 实体类与 JDBC 工具类的编写数据库建好后在 Java 侧创建对应的实体类。以 Product 为例package com.toy.entity; import java.math.BigDecimal; import java.util.Date; public class Product { private Integer productId; private String productName; private Integer categoryId; private BigDecimal price; private Integer stock; private String imageUrl; private String description; private Integer status; private Date createTime; // 省略 getter 和 setterIDEA 里 AltInsert 一键生成 }实体类字段名和数据库列名对应但用驼峰命名。price用BigDecimal而不是double因为金额计算不能用浮点数这是血泪经验——用 double 做金额加减会出现 0.1 0.2 0.30000000000000004 这种问题订单金额对不上答辩的时候被老师问住就尴尬了。接下来是 JDBC 工具类负责获取数据库连接和释放资源package com.toy.util; import java.sql.*; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/toy_shop?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }URL 里的serverTimezoneAsia/Shanghai必须加否则 MySQL 8.0 会报时区错误。characterEncodingutf8保证中文不乱码。useSSLfalse在本地开发时关掉 SSL避免额外的证书配置。这些参数看着不起眼但少一个就可能启动失败。提示数据库密码不要硬编码在 Java 文件里更不要提交到 Git。毕设项目至少把密码放在db.properties文件里用Properties类读取。答辩的时候老师如果问“你的数据库密码怎么管理的”这就是加分项。4. 核心功能实现从商品展示到下单的完整链路4.1 商品列表页与分页查询的 JSP 实现商品列表是用户进入系统后看到的第一个核心页面。它需要从数据库读取商品数据在 JSP 页面上循环展示并且支持分页。先看 Servlet 侧的分页查询逻辑package com.toy.servlet; import com.toy.entity.Product; import com.toy.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.*; import java.util.ArrayList; import java.util.List; WebServlet(/productList) public class ProductListServlet extends HttpServlet { private static final int PAGE_SIZE 8; // 每页显示 8 个商品 Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int page 1; String pageStr req.getParameter(page); if (pageStr ! null !pageStr.isEmpty()) { try { page Integer.parseInt(pageStr); } catch (NumberFormatException ignored) {} } if (page 1) page 1; ListProduct products new ArrayList(); int totalCount 0; try (Connection conn DBUtil.getConnection()) { // 查询总数 String countSql SELECT COUNT(*) FROM product WHERE status 1; try (PreparedStatement ps conn.prepareStatement(countSql); ResultSet rs ps.executeQuery()) { if (rs.next()) totalCount rs.getInt(1); } // 分页查询 String sql SELECT * FROM product WHERE status 1 ORDER BY create_time DESC LIMIT ? OFFSET ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, PAGE_SIZE); ps.setInt(2, (page - 1) * PAGE_SIZE); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Product p new Product(); p.setProductId(rs.getInt(product_id)); p.setProductName(rs.getString(product_name)); p.setPrice(rs.getBigDecimal(price)); p.setStock(rs.getInt(stock)); p.setImageUrl(rs.getString(image_url)); products.add(p); } } } } catch (SQLException e) { throw new ServletException(查询商品列表失败, e); } int totalPages (int) Math.ceil((double) totalCount / PAGE_SIZE); req.setAttribute(products, products); req.setAttribute(currentPage, page); req.setAttribute(totalPages, totalPages); req.getRequestDispatcher(/productList.jsp).forward(req, resp); } }这段代码里几个关键参数PAGE_SIZE控制每页显示数量我设的是 8你可以根据页面布局调整LIMIT ? OFFSET ?是 MySQL 的分页语法第一个参数是每页条数第二个参数是偏移量计算公式是(page - 1) * PAGE_SIZEstatus 1只查上架商品下架商品不展示给用户。对应的 JSP 页面用 JSTL 标签循环渲染% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % html head title玩具商城 - 商品列表/title link relstylesheet hrefcss/style.css /head body div classproduct-grid c:forEach items${products} varp div classproduct-card img src${p.imageUrl} alt${p.productName} h3${p.productName}/h3 p classpricefmt:formatNumber value${p.price} pattern#,##0.00//p p classstock库存${p.stock}/p a hrefproductDetail?id${p.productId} classbtn查看详情/a /div /c:forEach /div div classpagination c:if test${currentPage 1} a hrefproductList?page${currentPage - 1}上一页/a /c:if span第 ${currentPage} / ${totalPages} 页/span c:if test${currentPage totalPages} a hrefproductList?page${currentPage 1}下一页/a /c:if /div /body /htmlc:forEach是 JSTL 的循环标签items指定要遍历的集合var是每次循环的变量名。fmt:formatNumber用来格式化金额保留两位小数并加千分位分隔符。分页部分用c:if判断当前页是否在第一页或最后一页决定是否显示上一页/下一页链接。这种写法比在 JSP 里写 Java 脚本片段% %更清晰也更符合 MVC 的设计原则。4.2 购物车与订单提交的事务处理购物车和订单是玩具销售系统里业务逻辑最复杂的部分。用户把商品加入购物车然后在购物车页面点“去结算”系统生成订单并清空购物车。这个过程涉及多张表的写操作必须用事务保证一致性。先看加入购物车的 ServletWebServlet(/addToCart) public class AddToCartServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session req.getSession(); User user (User) session.getAttribute(user); if (user null) { resp.sendRedirect(login.jsp); return; } int productId Integer.parseInt(req.getParameter(productId)); int quantity Integer.parseInt(req.getParameter(quantity)); try (Connection conn DBUtil.getConnection()) { // 先查购物车里是否已有该商品 String checkSql SELECT cart_id, quantity FROM cart WHERE user_id ? AND product_id ?; try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setInt(1, user.getUserId()); ps.setInt(2, productId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { // 已有则更新数量 int newQty rs.getInt(quantity) quantity; String updateSql UPDATE cart SET quantity ? WHERE cart_id ?; try (PreparedStatement ups conn.prepareStatement(updateSql)) { ups.setInt(1, newQty); ups.setInt(2, rs.getInt(cart_id)); ups.executeUpdate(); } } else { // 没有则插入新记录 String insertSql INSERT INTO cart (user_id, product_id, quantity) VALUES (?, ?, ?); try (PreparedStatement ips conn.prepareStatement(insertSql)) { ips.setInt(1, user.getUserId()); ips.setInt(2, productId); ips.setInt(3, quantity); ips.executeUpdate(); } } } } } catch (SQLException e) { throw new ServletException(加入购物车失败, e); } resp.sendRedirect(cartList); } }这段逻辑是“先查后写”先查购物车里有没有这个商品有就累加数量没有就插入新记录。注意这里没有用事务因为单条 INSERT 或 UPDATE 本身就是原子操作。但到了下单环节就必须用事务了。WebServlet(/submitOrder) public class SubmitOrderServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session req.getSession(); User user (User) session.getAttribute(user); if (user null) { resp.sendRedirect(login.jsp); return; } String receiverName req.getParameter(receiverName); String receiverPhone req.getParameter(receiverPhone); String receiverAddress req.getParameter(receiverAddress); Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 查询购物车中选中的商品 String cartSql SELECT c.product_id, c.quantity, p.price, p.stock FROM cart c JOIN product p ON c.product_id p.product_id WHERE c.user_id ?; ListOrderItem items new ArrayList(); BigDecimal totalAmount BigDecimal.ZERO; try (PreparedStatement ps conn.prepareStatement(cartSql)) { ps.setInt(1, user.getUserId()); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { int stock rs.getInt(stock); int qty rs.getInt(quantity); if (stock qty) { throw new RuntimeException(商品库存不足); } OrderItem item new OrderItem(); item.setProductId(rs.getInt(product_id)); item.setQuantity(qty); item.setUnitPrice(rs.getBigDecimal(price)); items.add(item); totalAmount totalAmount.add( rs.getBigDecimal(price).multiply(new BigDecimal(qty))); } } } if (items.isEmpty()) { throw new RuntimeException(购物车为空); } // 2. 插入订单主表 String orderNo System.currentTimeMillis() user.getUserId(); String orderSql INSERT INTO orders (user_id, order_no, total_amount, status, receiver_name, receiver_phone, receiver_address) VALUES (?, ?, ?, 0, ?, ?, ?); int orderId; try (PreparedStatement ps conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS)) { ps.setInt(1, user.getUserId()); ps.setString(2, orderNo); ps.setBigDecimal(3, totalAmount); ps.setString(4, receiverName); ps.setString(5, receiverPhone); ps.setString(6, receiverAddress); ps.executeUpdate(); try (ResultSet rs ps.getGeneratedKeys()) { rs.next(); orderId rs.getInt(1); } } // 3. 插入订单明细并扣减库存 String itemSql INSERT INTO order_item (order_id, product_id, quantity, unit_price) VALUES (?, ?, ?, ?); String stockSql UPDATE product SET stock stock - ? WHERE product_id ? AND stock ?; for (OrderItem item : items) { try (PreparedStatement ps conn.prepareStatement(itemSql)) { ps.setInt(1, orderId); ps.setInt(2, item.getProductId()); ps.setInt(3, item.getQuantity()); ps.setBigDecimal(4, item.getUnitPrice()); ps.executeUpdate(); } try (PreparedStatement ps conn.prepareStatement(stockSql)) { ps.setInt(1, item.getQuantity()); ps.setInt(2, item.getProductId()); ps.setInt(3, item.getQuantity()); int affected ps.executeUpdate(); if (affected 0) { throw new RuntimeException(库存扣减失败可能已被其他订单占用); } } } // 4. 清空购物车 try (PreparedStatement ps conn.prepareStatement(DELETE FROM cart WHERE user_id ?)) { ps.setInt(1, user.getUserId()); ps.executeUpdate(); } conn.commit(); // 提交事务 resp.sendRedirect(orderSuccess.jsp?orderNo orderNo); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new ServletException(下单失败 e.getMessage(), e); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }这段代码是整个系统里最需要仔细看的部分。conn.setAutoCommit(false)开启事务后所有 SQL 操作都在同一个事务里任何一步失败都会触发rollback()保证数据一致性。库存扣减用了UPDATE product SET stock stock - ? WHERE product_id ? AND stock ?这个AND stock ?条件很关键——它防止了并发下单时库存被扣成负数。如果executeUpdate()返回 0说明库存不足直接抛异常回滚。订单号生成用了System.currentTimeMillis() user.getUserId()简单但够用。正式项目里应该用 UUID 或者雪花算法但毕设场景下时间戳加用户 ID 已经能保证唯一性。注意事务里的异常处理要区分RuntimeException和SQLException。业务异常库存不足、购物车为空用RuntimeException抛出数据库异常用SQLException捕获。两者都要触发回滚但错误提示信息不同。答辩的时候老师如果问“你的系统怎么保证库存不超卖”这段代码就是答案。5. 避坑与排查JSP 毕设项目最常见的五个翻车现场5.1 中文乱码从请求到响应到数据库的三层排查中文乱码是 JSP 项目里出现频率最高的问题没有之一。它可能出现在三个环节请求参数乱码、响应页面乱码、数据库存储乱码。排查的时候要一层一层看。现象表单提交后商品名称显示成??????或者森高这样的乱码。原因请求参数的编码格式和服务器解析格式不一致。GET 请求的参数编码由 Tomcat 的server.xml里 Connector 的URIEncoding决定POST 请求的编码由request.setCharacterEncoding()决定。解决POST 请求在 Servlet 里第一行加req.setCharacterEncoding(UTF-8)GET 请求在 Tomcat 的server.xml里给 Connector 加URIEncodingUTF-8。响应页面在 JSP 顶部加% page contentTypetext/html;charsetUTF-8 languagejava %。数据库连接 URL 里加characterEncodingutf8。三层都设成 UTF-8乱码问题基本就解决了。5.2 数据库连接失败驱动版本与时区参数的坑现象Tomcat 启动时报java.sql.SQLException: No suitable driver found或者The server time zone value ?D1ú±ê×?ê±?? is unrecognized。原因第一种是 MySQL 驱动 jar 包没有放到WEB-INF/lib目录下或者 Maven 依赖没有正确打包。第二种是 MySQL 8.0 的时区参数没有配置。解决检查pom.xml里 MySQL 驱动的 scope 是不是compile默认值如果是provided就不会打包进 war。时区问题在 JDBC URL 里加serverTimezoneAsia/Shanghai。如果还是报错把 URL 里的useSSL改成falseallowPublicKeyRetrieval改成true。5.3 JSP 页面修改后不生效Tomcat 缓存与热部署现象改了 JSP 文件刷新浏览器还是旧页面重启 Tomcat 才生效。原因Tomcat 默认会缓存编译后的 JSP Servlet修改 JSP 后不会自动重新编译。另外 IDEA 的 war exploded 模式有时候不会同步更新文件。解决在 Tomcat 的conf/web.xml里找到jspServlet 的配置把development参数设为truemodificationTestInterval设为0。这样 Tomcat 会定期检查 JSP 文件是否修改修改后自动重新编译。IDEA 里在 Run/Debug Configurations 的 Server 标签页把On Update action设为Update classes and resourcesOn frame deactivation也设为Update classes and resources。5.4 购物车数量为负数前端校验缺失的后果现象用户在商品详情页把数量改成-1或者0提交后购物车里出现负数数量订单金额算出来是负的。原因前端input typenumber的min属性只是浏览器层面的约束用户可以通过开发者工具修改 HTML 绕过。后端没有对 quantity 做校验。解决在 AddToCartServlet 里加校验if (quantity 0) { resp.sendRedirect(productDetail?id productId errorquantity); return; }。同样在提交订单的 Servlet 里也要校验购物车中每条记录的数量是否大于 0。前端校验是体验后端校验是安全两者缺一不可。5.5 订单重复提交刷新页面导致的重复下单现象用户下单后在订单成功页面按 F5 刷新系统又生成了一笔一模一样的订单。原因下单请求是 POST但刷新页面时浏览器会重新提交最后一次 POST 请求。Servlet 没有做幂等性处理。解决用重定向代替转发。下单成功后执行resp.sendRedirect(orderSuccess.jsp?orderNo orderNo)而不是req.getRequestDispatcher(...).forward(...)。重定向会让浏览器发起一个新的 GET 请求刷新页面时只会重新加载订单成功页不会重复提交订单。这是 PRGPost-Redirect-Get模式Web 开发里的经典解法。6. 从能跑到能答辩三个让毕设加分的技术细节6.1 用 Filter 统一处理编码和登录拦截系统里每个 Servlet 都要写req.setCharacterEncoding(UTF-8)很繁琐而且容易漏。更好的做法是用 Filter 统一处理WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); String uri request.getRequestURI(); // 放行登录页、注册页、静态资源 if (uri.contains(login) || uri.contains(register) || uri.contains(.css) || uri.contains(.js) || uri.contains(.jpg)) { chain.doFilter(req, resp); return; } // 检查登录状态 if (request.getSession().getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }这个 Filter 做了两件事设置编码和检查登录。WebFilter(/*)表示拦截所有请求。放行规则里包含了登录、注册和静态资源其他请求都要求登录。这样你就不用在每个 Servlet 里重复写编码设置了代码更干净答辩的时候也能体现出你对 Filter 的理解。6.2 用 JSTL 和 EL 表达式替代 Java 脚本片段很多 JSP 教程里还在用% %写 Java 代码比如% for(Product p : list) { %。这种写法在毕设里能用但不推荐。JSTL 和 EL 表达式更简洁也更容易维护。对比一下!-- 不推荐Java 脚本片段 -- % ListProduct list (ListProduct) request.getAttribute(products); for (Product p : list) { % div% p.getProductName() %/div % } % !-- 推荐JSTL EL -- c:forEach items${products} varp div${p.productName}/div /c:forEachJSTL 的写法没有 Java 代码页面只负责展示逻辑都在 Servlet 里。答辩的时候老师如果问“你的系统是不是 MVC 模式”你可以指着 JSP 说“页面里没有一行 Java 代码全部用 EL 表达式取值”这就是加分项。6.3 打包部署从 IDEA 到独立 Tomcat 的 war 包开发的时候用 IDEA 的 war exploded 模式很方便但答辩的时候可能需要你把项目部署到独立 Tomcat 上。这时候需要打 war 包。在pom.xml里已经配置了maven-war-plugin在项目根目录执行mvn clean package执行完后target目录下会生成toy-shop.war。把这个文件复制到 Tomcat 的webapps目录下启动 Tomcat它会自动解压并部署。访问http://localhost:8080/toy-shop就能看到系统。如果部署后报 404检查三个地方war 包是否完整用解压工具打开看看WEB-INF/lib下有没有 MySQL 驱动和 JSTLTomcat 的logs/catalina.out里有没有报错web.xml里的welcome-file是否配置了index.jsp。提示打包前把数据库连接信息改成答辩环境的值。如果答辩教室的 MySQL 密码和你开发机不一样war 包部署后会连不上数据库。我一般会在db.properties里放两套配置打包时用 Maven 的 profile 切换但毕设项目手动改一下也行。做这个项目最大的体会是JSP 毕设的难点不在技术本身而在细节的完整性。一个能跑的系统和一个能答辩的系统之间差的就是编码统一、事务处理、登录拦截、PRG 模式这些看似不起眼但老师一定会问的点。把这几处做扎实答辩的时候底气完全不一样。希望帮到你。本文还有配套的精品资源点击获取