
简介本资源是一套面向计算机专业本科生的毕业设计级校园二手交易平台实战源码采用JSPServletMySQL经典Java Web技术栈构建完整覆盖用户管理、商品发布与浏览、交易流程、消息通知及基础安全防护等核心模块适用于课程设计、毕设开发与Web全栈入门实践。压缩包共5个文件含数据库脚本market.sql用于初始化数据、README.md提供项目说明、LICENSE明确授权协议、毕设说明书.doc详述设计思路与实现过程以及主源码压缩包market.zip整体大小7.95MB结构清晰、开箱即用。已有883人学习下载资源附带可直接运行的完整工程结构、分层代码组织MVC模式、典型SQL注入/XSS防护示例及配套文档便于读者快速理解前后端协作逻辑、掌握数据库建模与Servlet控制器开发要点并复现真实业务场景。1. 这不是又一个“学生练手项目”JSPServletMySQL校园二手交易平台解决的是真实场景下的数据一致性、会话隔离与轻量级事务闭环很多开发者看到“毕业设计”四个字就下意识划走——但这个基于 JSPServletMySQL 的校园二手交易平台恰恰卡在了企业级 Web 开发的“临界点”上它不用 Spring Boot 自动装配掩盖底层细节不靠 MyBatis 封装屏蔽 SQL 执行路径而是用最原始的HttpServlet生命周期、RequestDispatcher转发链、Connection手动管理把用户登录态绑定、商品状态流转上架→已售→下架、订单生成与库存扣减的原子性控制全部摊开在 JDBC 的try-catch-finally里。它适合两类人一是刚学完 Java Web 基础、正卡在“为什么 form 提交后页面没跳转”“为什么 session.setAttribute 后另一个 servlet 取不到值”的初学者二是想回溯 Servlet 容器本质、排查web.xml中load-on-startup与ServletContextListener初始化顺序问题的进阶者。它不追求高并发但每一步操作都对应 HTTP 协议状态码、Servlet 规范中明确规定的线程安全边界和 MySQL 事务隔离级别实测行为。2. 从零启动JSP 页面渲染、Servlet 控制流与 MySQL 数据库三者的协同机制2.1 JSP 不是静态 HTML理解 pageContext、request、session 三大作用域的实际生命周期JSP 在运行时被容器编译为 Servlet 类其本质是HttpServlet的子类。关键在于三个隐式对象的作用域边界request仅在一次 HTTP 请求内有效如从login.jsp表单提交到LoginServlet再forward回index.jsp时仍可取值session绑定到客户端 Cookie 中的JSESSIONID只要浏览器未关闭且未超时默认 30 分钟跨多个请求持续存在pageContext仅限当前 JSP 文件内有效无法被include或forward传递。在本项目中用户登录成功后LoginServlet执行// LoginServlet.java HttpSession session request.getSession(true); session.setAttribute(user, user); // user 是 User 对象含 id、username、role request.setAttribute(message, 登录成功); RequestDispatcher dispatcher request.getRequestDispatcher(index.jsp); dispatcher.forward(request, response);注意request.setAttribute()必须在forward()前调用且目标 JSP如index.jsp中需用% request.getAttribute(message) %或 JSTLc:out value${requestScope.message}/输出。若误用session.setAttribute()存储临时提示信息会导致多次刷新页面重复显示“登录成功”。2.2 Servlet 的标准处理链doGet/doPost 分离、URL 映射与请求参数解析本项目所有业务逻辑均通过继承HttpServlet实现严格区分doGet()用于展示页面、查询数据和doPost()用于表单提交、数据修改。以发布二手商品为例PublishServlet的核心逻辑如下// PublishServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); // 必须在获取参数前设置否则中文乱码 String title request.getParameter(title); // input nametitle String description request.getParameter(description); String priceStr request.getParameter(price); int price Integer.parseInt(priceStr); // 简单校验生产环境应加 try-catch 和范围检查 int userId (int) request.getSession().getAttribute(user_id); // 构建 SQL 插入语句此处应使用 PreparedStatement 防注入源码中已实现 String sql INSERT INTO goods (title, description, price, user_id, status, create_time) VALUES (?, ?, ?, ?, on_sale, NOW()); try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { pstmt.setString(1, title); pstmt.setString(2, description); pstmt.setInt(3, price); pstmt.setInt(4, userId); int affectedRows pstmt.executeUpdate(); if (affectedRows 0) { throw new SQLException(创建商品失败); } // 获取自增主键 try (ResultSet rs pstmt.getGeneratedKeys()) { if (rs.next()) { int goodsId rs.getInt(1); request.setAttribute(goodsId, goodsId); request.setAttribute(message, 商品发布成功); } } } catch (SQLException e) { request.setAttribute(error, 数据库操作异常 e.getMessage()); request.getRequestDispatcher(publish.jsp).forward(request, response); return; } request.getRequestDispatcher(my_goods.jsp).forward(request, response); }提示request.setCharacterEncoding(UTF-8)必须放在getParameter()之前否则 POST 提交的中文参数会变成??。DBUtil.getConnection()是项目封装的工具类内部使用DriverManager.getConnection()连接字符串形如jdbc:mysql://localhost:3306/secondhand?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8。2.3 MySQL 数据库设计围绕校园场景的 5 张核心表与外键约束实践本项目数据库共 5 张表结构精简但覆盖核心业务users用户、goods商品、orders订单、order_items订单明细、messages站内信。其中goods表的user_id字段与users.id建立外键orders.user_id和order_items.goods_id同理。建表语句示例如下-- users 表 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- 存储 BCrypt 加密后的密文 real_name VARCHAR(30), phone VARCHAR(20), email VARCHAR(100), role ENUM(student, admin) DEFAULT student, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- goods 表关键字段含状态枚举 CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL, user_id INT NOT NULL, status ENUM(on_sale, sold, off_shelf) DEFAULT on_sale, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE );关键点说明status字段使用ENUM类型而非VARCHAR既节省存储空间又通过数据库层强制约束合法值ON DELETE CASCADE表明当用户被删除时其发布的所有商品自动级联删除避免孤儿记录update_time的ON UPDATE CURRENT_TIMESTAMP确保每次UPDATE操作自动更新时间戳无需在 Java 代码中手动赋值。3. 核心功能落地用户登录态管理、商品状态机与订单生成的完整事务控制3.1 登录态与权限拦截Filter 实现统一访问控制避免每个 Servlet 重复校验项目未使用 Spring Security而是通过自定义LoginCheckFilter实现登录拦截。该 Filter 配置在web.xml中对/user/*、/admin/*等路径生效!-- web.xml -- filter filter-nameLoginCheckFilter/filter-name filter-classfilter.LoginCheckFilter/filter-class /filter filter-mapping filter-nameLoginCheckFilter/filter-name url-pattern/user/*/url-pattern url-pattern/admin/*/url-pattern /filter-mappingLoginCheckFilter的doFilter方法核心逻辑// LoginCheckFilter.java public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest (HttpServletRequest) request; HttpServletResponse httpResponse (HttpServletResponse) response; HttpSession session httpRequest.getSession(false); // false 表示不创建新 session // 检查 session 中是否存在 user 对象 if (session null || session.getAttribute(user) null) { // 未登录重定向到登录页并保存原请求路径用于登录后跳转 String originalUrl httpRequest.getRequestURI(); if (httpRequest.getQueryString() ! null) { originalUrl ? httpRequest.getQueryString(); } session.setAttribute(originalUrl, originalUrl); httpResponse.sendRedirect(httpRequest.getContextPath() /login.jsp); return; } // 已登录放行 chain.doFilter(request, response); }注意session.getAttribute(user)是判断登录的核心依据而非检查username字符串。originalUrl的保存机制使得用户从/user/my_goods.jsp被重定向到/login.jsp后登录成功即可自动跳回原页面提升体验。3.2 商品状态机从“上架”到“已售”的状态流转与并发安全控制商品状态变更如买家点击“立即购买”必须保证原子性防止超卖。本项目采用 MySQL 的UPDATE ... WHERE status on_sale语句实现乐观锁// BuyServlet.java String updateSql UPDATE goods SET status sold, buyer_id ? WHERE id ? AND status on_sale; try (PreparedStatement pstmt conn.prepareStatement(updateSql)) { pstmt.setInt(1, buyerId); pstmt.setInt(2, goodsId); int rowsAffected pstmt.executeUpdate(); if (rowsAffected 0) { // 更新失败说明商品已被他人抢购或状态非 on_sale request.setAttribute(error, 商品已被抢购或状态异常请刷新页面重试); request.getRequestDispatcher(goods_detail.jsp?id goodsId).forward(request, response); return; } } // 状态更新成功继续创建订单...原理说明WHERE status on_sale是关键。当两个请求同时尝试购买同一商品时数据库会按行锁InnoDB 默认锁定该记录第二个请求必须等待第一个事务提交或回滚。若第一个请求已将status改为sold第二个请求的UPDATE将匹配 0 行executeUpdate()返回 0从而在应用层捕获并发冲突而非让数据库报错。3.3 订单生成与库存扣减跨表插入的事务边界与回滚策略一笔订单包含主表orders和明细表order_items必须保证两者同时成功或同时失败。本项目在OrderServlet中显式开启事务// OrderServlet.java Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 1. 插入 orders 主表 String orderSql INSERT INTO orders (user_id, total_amount, status, create_time) VALUES (?, ?, pending, NOW()); try (PreparedStatement orderPstmt conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS)) { orderPstmt.setInt(1, userId); orderPstmt.setBigDecimal(2, totalAmount); orderPstmt.executeUpdate(); // 获取订单 ID int orderId; try (ResultSet rs orderPstmt.getGeneratedKeys()) { rs.next(); orderId rs.getInt(1); } // 2. 插入 order_items 明细循环插入多件商品 String itemSql INSERT INTO order_items (order_id, goods_id, quantity, price) VALUES (?, ?, 1, ?); try (PreparedStatement itemPstmt conn.prepareStatement(itemSql)) { for (GoodsItem item : cartItems) { // cartItems 来自 session 购物车 itemPstmt.setInt(1, orderId); itemPstmt.setInt(2, item.getGoodsId()); itemPstmt.setBigDecimal(3, item.getPrice()); itemPstmt.addBatch(); } itemPstmt.executeBatch(); } } conn.commit(); // 全部成功提交事务 request.setAttribute(message, 订单创建成功); } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); // 任一环节失败回滚整个事务 } catch (SQLException rollbackEx) { log.error(事务回滚失败, rollbackEx); } } request.setAttribute(error, 订单创建失败 e.getMessage()); } finally { if (conn ! null) { try { conn.setAutoCommit(true); // 恢复自动提交 conn.close(); } catch (SQLException e) { log.error(连接关闭失败, e); } } }关键参数说明conn.setAutoCommit(false)是事务开始的标志Statement.RETURN_GENERATED_KEYS用于获取自增主键executeBatch()批量执行明细插入比单条executeUpdate()效率更高finally块中恢复autoCommit并关闭连接是资源释放的兜底保障。4. 部署与调试Tomcat 本地运行、JSP 修改热加载失效排查与 MySQL 连接验证4.1 Tomcat 8.5 环境配置确认 JDK 版本、CATALINA_HOME 与 webapps 目录结构本项目要求 JDK 8 或 JDK 11因 JSP 编译依赖tools.jarJDK 9 已移除故推荐 JDK 8u202。Tomcat 版本需 8.5.x 或 9.0.x避免使用 10.xServlet 4.0 规范对web.xml有更严格校验。部署步骤解压 Tomcat 到无中文、无空格路径如D:\apache-tomcat-8.5.93设置系统环境变量CATALINA_HOMED:\apache-tomcat-8.5.93将项目文件夹含WEB-INF目录整体复制到CATALINA_HOME\webapps\secondhand启动CATALINA_HOME\bin\startup.batWindows或startup.shLinux/Mac访问http://localhost:8080/secondhand/login.jsp。注意若启动后页面 404首先检查webapps\secondhand\WEB-INF\web.xml是否存在且格式正确XML 标签闭合、无非法字符其次确认webapps\secondhand\WEB-INF\lib下是否包含mysql-connector-java-5.1.47.jar或 8.0.x 版本缺失则导致Class.forName(com.mysql.jdbc.Driver)报ClassNotFoundException。4.2 JSP 修改不生效定位work\Catalina\localhost\secondhand缓存与重新编译机制JSP 第一次访问时Tomcat 会将其编译为.java和.class文件存于CATALINA_HOME\work\Catalina\localhost\secondhand\org\apache\jsp\目录下。修改 JSP 后不生效常见原因及解决方法现象原因解决方案修改index.jsp后刷新仍是旧内容Tomcat 未检测到文件变化或web.xml中init-param配置了developmentfalse在CATALINA_HOME\conf\web.xml中找到servlet标签内的jspservlet添加或修改init-paraminit-paramparam-namedevelopment/param-nameparam-valuetrue/param-value/init-paramwork目录下无对应.java文件JSP 语法错误编译失败查看CATALINA_HOME\logs\catalina.out搜索JasperException定位具体行号和错误类型如Unterminated lt;%修改后需重启 Tomcat 才生效web.xml中reloadablefalse在CATALINA_HOME\conf\server.xml的Context标签中确保reloadabletrue仅开发环境启用生产禁用提示最彻底的清理方式是停止 Tomcat删除work目录和temp目录再启动。work目录完全由 Tomcat 管理删除后首次访问 JSP 会自动重建。4.3 MySQL 连接验证用命令行直连、检查用户权限与时区配置当DBUtil.getConnection()报Communications link failure或Access denied时需分层验证命令行直连 MySQLmysql -h localhost -P 3306 -u root -p # 输入密码后执行 SHOW DATABASES; -- 确认 secondhand 库存在 USE secondhand; DESCRIBE users; -- 确认表结构 SELECT * FROM users LIMIT 1; -- 确认有测试数据检查用户权限假设应用使用secondhand_user用户-- 创建用户并授权MySQL 5.7 CREATE USER secondhand_userlocalhost IDENTIFIED BY password123; GRANT SELECT, INSERT, UPDATE, DELETE ON secondhand.* TO secondhand_userlocalhost; FLUSH PRIVILEGES;验证时区配置避免java.sql.SQLException: The server time zone value ... is unrecognized-- 在 MySQL 命令行执行 SELECT global.time_zone, session.time_zone; -- 若返回 SYSTEM需在连接 URL 中显式指定 -- jdbc:mysql://localhost:3306/secondhand?serverTimezoneAsia/Shanghai关键参数表web.xml中context-param配置数据库连接信息部分项目将此抽离为db.properties参数名示例值说明jdbc.drivercom.mysql.cj.jdbc.DriverMySQL 8.0 必须用cj包5.1.x 用com.mysql.jdbc.Driverjdbc.urljdbc:mysql://localhost:3306/secondhand?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8useSSLfalse关闭 SSL本地开发可省略但必须显式声明jdbc.usernamesecondhand_user数据库用户名jdbc.passwordpassword123数据库密码5. 进阶技巧用 JSTL 替代 Scriptlet、JSON 数据交互与 MySQL 存储过程优化查询5.1 用 JSTL 标签库重构 JSP消除% %脚本提升可维护性与安全性原始 JSP 中大量使用% %嵌入 Java 代码Scriptlet易出错且难维护。本项目已集成 JSTL 1.2可在index.jsp中用c:forEach替代for循环% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % !-- 原始 Scriptlet 方式不推荐 -- % ListGoods goodsList (ListGoods) request.getAttribute(goodsList); for (Goods g : goodsList) { % div classgoods-item h3% g.getTitle() %/h3 p% g.getDescription() %/p span¥% g.getPrice() %/span /div % } % !-- JSTL 方式推荐 -- c:forEach items${goodsList} varg div classgoods-item h3${g.title}/h3 p${g.description}/p span¥fmt:formatNumber value${g.price} pattern#,##0.00//span /div /c:forEach优势说明${g.title}是 EL 表达式自动调用getTitle()getterfmt:formatNumber提供千分位和小数精度控制JSTL 标签在编译期检查语法避免运行时NullPointerException更重要的是它天然防御 XSS${g.title}会自动 HTML 转义而% g.getTitle() %若未手动StringEscapeUtils.escapeHtml4()则存在风险。5.2 Servlet 与前端 JSON 交互用 Jackson 将 Java 对象转为 JSON 响应当需要通过 AJAX 加载商品列表如首页轮播图数据Servlet 可直接输出 JSON// GoodsApiServlet.java protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(application/json;charsetUTF-8); ListGoods goodsList goodsService.findTop4ByStatus(on_sale); // 使用 Jackson需引入 jackson-databind-2.13.4.2.jar ObjectMapper mapper new ObjectMapper(); String json mapper.writeValueAsString(goodsList); response.getWriter().write(json); }前端 JavaScript 调用fetch(/secondhand/api/goods/top4) .then(response response.json()) .then(data { data.forEach(goods { const item document.createElement(div); item.innerHTML h3${goods.title}/h3p¥${goods.price}/p; document.getElementById(carousel).appendChild(item); }); });注意response.setContentType(application/json;charsetUTF-8)必须在getWriter()前设置否则响应头无效Jackson 的writeValueAsString()会自动处理Date类型默认转为毫秒时间戳若需 ISO 格式需配置mapper.registerModule(new JavaTimeModule())。5.3 MySQL 存储过程优化高频查询封装“我的待发货订单”统计逻辑对于频繁执行的聚合查询如统计用户待发货订单数可将 SQL 逻辑下沉至 MySQL 存储过程中减少网络传输与应用层解析开销DELIMITER $$ CREATE PROCEDURE GetUserPendingOrders(IN p_user_id INT, OUT p_count INT) BEGIN SELECT COUNT(*) INTO p_count FROM orders o INNER JOIN order_items oi ON o.id oi.order_id WHERE o.user_id p_user_id AND o.status pending AND oi.status unshipped; END$$ DELIMITER ;在OrderService.java中调用String sql {CALL GetUserPendingOrders(?, ?)}; try (CallableStatement cstmt conn.prepareCall(sql)) { cstmt.setInt(1, userId); cstmt.registerOutParameter(2, Types.INTEGER); cstmt.execute(); int pendingCount cstmt.getInt(2); request.setAttribute(pendingCount, pendingCount); }适用场景说明存储过程适合逻辑复杂、多表关联、且被多个应用模块调用的查询。本例中UserServlet、MyOrdersServlet、首页index.jsp都可能需要该统计值统一由存储过程提供避免 SQL 重复编写与 JDBC 参数绑定开销。但需注意过度使用存储过程会增加数据库负担且不利于版本控制SQL 代码散落在数据库中。本文还有配套的精品资源点击获取