简介:本资源是一套面向计算机专业本科生的毕业设计级校园二手交易平台完整实现,采用JSP+Servlet+MySQL经典Java Web技术栈,覆盖用户管理、商品发布与浏览、交易流程、消息通知及基础安全防护等核心模块,适用于课程设计、毕设参考与Web开发入门实践。压缩包共5个文件,含数据库脚本(market.sql)用于初始化表结构与示例数据,README.md提供项目说明与运行指引,.doc格式的本科毕业设计说明书详述需求分析、系统设计与实现过程,LICENSE明确授权信息,主源码以zip形式封装,整体大小7.95MB,结构精炼、开箱即用。已有883人学习下载,资源提供可直接部署运行的全量源码、规范化的文档支撑及典型MVC分层实现范例,有助于深入理解前后端协作逻辑、Servlet请求处理机制与MySQL数据持久化实践。
1. 这不是“过时技术堆砌”,而是一套可跑通、可调试、可扩展的校园二手交易最小可行系统
你打开market.zip解压后看到的不是一堆静态 HTML 和空数据库脚本,而是一个能立刻部署到 Tomcat 8.5+、连上 MySQL 5.7+ 就能注册登录、发布商品、完成模拟交易的完整 Web 应用。它没有 Spring Boot 的自动配置魔法,但每一行 JSP 的<% %>脚本、每一个HttpServlet子类、每一条PreparedStatement参数绑定,都直白地暴露了 HTTP 请求如何被解析、用户会话如何维持、SQL 查询如何防注入、事务边界如何划定——这正是 Java Web 基础链路最真实的毛细血管。它适合两类人:一类是正在写毕业设计、需要在两周内跑通全流程并能讲清每个模块调用关系的本科生;另一类是已工作 3~5 年、想回溯 Servlet 容器生命周期、理解web.xml中<filter>与<servlet-mapping>如何协同拦截请求的开发者。项目不追求高并发或微服务架构,但把“用户登录态校验”“商品分页查询”“图片路径安全存储”“订单状态机流转”四个关键闭环做实了,且所有 SQL 语句均使用预编译参数化,market.sql中明确建有user,product,order_info,message四张表及外键约束,不是示意性结构。
2. 从web.xml入口开始:Servlet 生命周期与请求路由的显式控制
2.1web.xml是整个系统的中枢神经,而非历史遗迹
该平台未采用注解驱动(如@WebServlet),所有 Servlet 映射、Filter 链、监听器注册均通过WEB-INF/web.xml显式声明。这是理解请求如何进入系统的起点:
<!-- web.xml 片段 --> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.example.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping> <filter> <filter-name>AuthFilter</filter-name> <filter-class>com.example.filter.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/user/*</url-pattern> </filter-mapping>提示:
<url-pattern>/user/*</url-pattern>表示所有以/user/开头的请求(如/user/profile.jsp,/user/orderlist)都会先经过AuthFilter。该 Filter 在doFilter()方法中检查HttpSession.getAttribute("user")是否为非 null,若为空则重定向至/login.jsp。这种显式声明比 Spring Security 的@PreAuthorize更底层,也更易调试——你可以在AuthFilter.java第 32 行加断点,观察每次请求进来时 session 的实际内容。
2.2LoginServlet的三步验证逻辑:从表单解析到会话建立
LoginServlet继承HttpServlet,其doPost()方法执行标准三步:获取参数 → 查询数据库 → 设置会话。关键代码如下:
// LoginServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username").trim(); // 1. 获取表单字段 String password = request.getParameter("password"); // 2. 使用 PreparedStatement 防 SQL 注入(非拼接字符串) String sql = "SELECT id, username, realname, role FROM user WHERE username = ? AND password = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, username); // 参数索引从 1 开始 pstmt.setString(2, MD5Util.md5(password)); // 密码经 MD5 加密存储 ResultSet rs = pstmt.executeQuery(); if (rs.next()) { // 3. 创建用户会话对象,存入必要字段(非全量用户数据) User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setRealname(rs.getString("realname")); user.setRole(rs.getString("role")); // 'student' or 'admin' request.getSession().setAttribute("user", user); // 会话绑定 response.sendRedirect("user/home.jsp"); // 重定向至主页 } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } }- 参数说明:
request.getParameter("username")获取的是 HTML 表单中name="username"的输入值;MD5Util.md5()是项目自定义工具类,对明文密码做不可逆哈希,避免数据库泄露导致密码直接暴露; - 关键细节:
request.getSession().setAttribute("user", user)将User对象存入 session,后续所有受保护页面(如/user/profile.jsp)均可通过session.getAttribute("user")获取当前登录者信息,无需重复查库; - 失败排查:若登录后跳转至空白页,首先检查
market.sql中user表是否已插入测试数据(如INSERT INTO user VALUES (1,'test','张三','student',md5('123456'));),再确认LoginServlet中conn数据库连接是否已由DBUtil正确初始化。
2.3ProductListServlet实现带条件的分页查询:JSP 与 Servlet 的职责分离
商品列表页/product/list.jsp不直接访问数据库,而是由ProductListServlet承担数据组装职责。该 Servlet 接收pageNo=1&pageSize=10&keyword=手机等参数,执行带LIMIT和WHERE的查询,并将结果集与分页元数据一并传给 JSP:
// ProductListServlet.java String keyword = request.getParameter("keyword"); int pageNo = Integer.parseInt(request.getParameter("pageNo")); int pageSize = 10; // 构建动态 SQL(注意:此处 keyword 仍需预编译,避免拼接) String sql = "SELECT p.id, p.title, p.price, p.status, u.realname " + "FROM product p JOIN user u ON p.seller_id = u.id " + "WHERE p.status = 'on_sale'"; if (keyword != null && !keyword.trim().isEmpty()) { sql += " AND p.title LIKE ?"; } sql += " ORDER BY p.create_time DESC LIMIT ?, ?"; PreparedStatement pstmt = conn.prepareStatement(sql); int paramIndex = 1; if (keyword != null && !keyword.trim().isEmpty()) { pstmt.setString(paramIndex++, "%" + keyword.trim() + "%"); } pstmt.setInt(paramIndex++, (pageNo - 1) * pageSize); // LIMIT offset, size pstmt.setInt(paramIndex, pageSize); ResultSet rs = pstmt.executeQuery(); List<Product> productList = new ArrayList<>(); while (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setTitle(rs.getString("title")); p.setPrice(rs.getBigDecimal("price")); p.setStatus(rs.getString("status")); p.setSellerName(rs.getString("realname")); productList.add(p); } // 计算总记录数(用于分页导航) String countSql = "SELECT COUNT(*) FROM product p JOIN user u ON p.seller_id = u.id WHERE p.status = 'on_sale'"; if (keyword != null && !keyword.trim().isEmpty()) { countSql += " AND p.title LIKE ?"; } PreparedStatement countPstmt = conn.prepareStatement(countSql); if (keyword != null && !keyword.trim().isEmpty()) { countPstmt.setString(1, "%" + keyword.trim() + "%"); } ResultSet countRs = countPstmt.executeQuery(); countRs.next(); int totalCount = countRs.getInt(1); // 封装分页对象并转发 PageBean<Product> pageBean = new PageBean<>(pageNo, pageSize, totalCount, productList); request.setAttribute("pageBean", pageBean); request.getRequestDispatcher("/product/list.jsp").forward(request, response);- 逻辑说明:
PageBean是项目自定义的分页实体类,包含pageNo,pageSize,totalPages,list等字段;list.jsp通过 JSTL<c:forEach items="${pageBean.list}" var="p">遍历展示商品; - 参数说明:
LIMIT ?, ?中第一个?是偏移量(pageNo-1)*pageSize,第二个?是每页条数pageSize;LIKE ?的参数值为"%" + keyword + "%",实现模糊搜索; - 性能注意:
COUNT(*)查询未加WHERE条件时会全表扫描,生产环境应为title字段添加索引:ALTER TABLE product ADD INDEX idx_title (title);
3. JSP 页面层:从login.jsp到product/detail.jsp的渐进式交互实现
3.1login.jsp:表单提交与错误反馈的同步处理模式
该页面采用传统同步提交(非 AJAX),核心在于form的action指向/login,且通过request.getAttribute("error")渲染错误提示:
<!-- login.jsp --> <form action="/login" method="post"> <div class="form-group"> <label>用户名</label> <input type="text" name="username" required> </div> <div class="form-group"> <label>密码</label> <input type="password" name="password" required> </div> <button type="submit">登录</button> </form> <!-- 错误提示区域 --> <c:if test="${not empty error}"> <div class="alert alert-danger">${error}</div> </c:if>- 关键机制:当
LoginServlet验证失败时,调用request.setAttribute("error", "xxx")并forward回login.jsp,此时request对象仍存活,JSP 中${error}可直接取值;若使用sendRedirect(),则request属性丢失,错误信息需改用session.setAttribute()并在重定向后立即清除; - 安全实践:
<input type="password">防止密码明文显示;required属性提供前端基础校验,但后端LoginServlet仍需trim()和非空判断,因前端校验可被绕过。
3.2product/detail.jsp:商品详情与购买按钮的状态联动
详情页不仅展示数据,还根据当前用户身份和商品状态动态渲染操作按钮。核心逻辑在 JSP 脚本片段中:
<!-- product/detail.jsp --> <c:choose> <c:when test="${empty sessionScope.user}"> <a href="/login.jsp" class="btn btn-primary">请先登录</a> </c:when> <c:when test="${sessionScope.user.id == product.sellerId}"> <span class="badge bg-warning">您是卖家</span> <a href="/product/edit?id=${product.id}" class="btn btn-outline-secondary">编辑商品</a> </c:when> <c:otherwise> <c:choose> <c:when test="${product.status == 'on_sale'}"> <form action="/order/create" method="post"> <input type="hidden" name="productId" value="${product.id}"> <button type="submit" class="btn btn-success">立即购买</button> </form> </c:when> <c:otherwise> <span class="badge bg-secondary">已下架</span> </c:otherwise> </c:choose> </c:otherwise> </c:choose>- 状态驱动:
<c:choose>根据sessionScope.user(登录用户)、product.sellerId(商品所属用户 ID)、product.status(商品状态)三者关系,决定显示“登录入口”“编辑按钮”或“购买表单”; - 参数传递:购买表单通过
hidden输入域传递productId,OrderCreateServlet接收后校验库存、生成订单号、更新商品状态为sold_out,全程在一次 HTTP POST 中完成,避免分布式事务复杂度; - 用户体验:
<span class="badge">提供即时状态反馈,比纯文字更直观;Bootstrap 类名(btn-success,bg-warning)表明项目已集成前端框架,降低样式开发成本。
3.3user/profile.jsp:个人信息展示与安全更新的分离设计
用户个人资料页采用“读写分离”策略:展示区直接读取sessionScope.user,编辑区则提交至独立 Servlet:
<!-- user/profile.jsp --> <div class="card"> <div class="card-header">个人信息</div> <div class="card-body"> <p><strong>用户名:</strong>${sessionScope.user.username}</p> <p><strong>真实姓名:</strong>${sessionScope.user.realname}</p> <p><strong>角色:</strong><span class="badge bg-info">${sessionScope.user.role}</span></p> <a href="/user/edit.jsp" class="btn btn-link">修改资料</a> </div> </div>- 安全考量:
profile.jsp仅展示sessionScope.user中的字段,不暴露password或id等敏感属性;edit.jsp是独立页面,其表单提交至/user/update,由UpdateUserServlet处理,该 Servlet 明确限定只允许更新realname和phone字段,且对phone格式进行正则校验(^1[3-9]\\d{9}$); - 防重放:
update.jsp表单中嵌入隐藏域<input type="hidden" name="timestamp" value="<%=System.currentTimeMillis()%>">,UpdateUserServlet验证时间戳是否在 5 分钟内,防止链接被恶意复用。
4. MySQL 数据库设计:四张核心表的约束与关联逻辑
4.1user表:身份标识与权限基点
market.sql中user表定义如下,其主键、唯一约束及默认值设计直指业务需求:
CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', `password` VARCHAR(32) NOT NULL COMMENT 'MD5加密密码', `realname` VARCHAR(100) NOT NULL COMMENT '真实姓名', `phone` VARCHAR(11) DEFAULT NULL COMMENT '手机号', `role` ENUM('student','admin') NOT NULL DEFAULT 'student' COMMENT '角色', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `status` TINYINT(1) DEFAULT 1 COMMENT '1-启用,0-禁用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;- 关键约束:
username设为UNIQUE,确保账号唯一性;role使用ENUM类型,将角色值固化为'student'或'admin',避免字符串误写;status默认1(启用),新注册用户无需额外激活; - 索引建议:为高频查询字段添加索引,如
CREATE INDEX idx_username_status ON user(username, status);,加速登录验证(WHERE username=? AND status=1)。
4.2product表:商品状态机与外键完整性
商品表通过status字段实现简单状态机,并强制关联user表:
CREATE TABLE `product` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `title` VARCHAR(200) NOT NULL COMMENT '商品标题', `description` TEXT COMMENT '商品描述', `price` DECIMAL(10,2) NOT NULL COMMENT '价格', `seller_id` INT NOT NULL COMMENT '卖家ID', `status` ENUM('on_sale','sold_out','deleted') NOT NULL DEFAULT 'on_sale' COMMENT '状态', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (`seller_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;- 外键行为:
ON DELETE CASCADE表示若某用户被删除,其发布的所有商品自动级联删除,避免孤儿数据;ON UPDATE CASCADE未启用,因user.id为主键,不应被更新; - 状态流转:
status允许值为'on_sale'(在售)、'sold_out'(已售罄)、'deleted'(软删除)。OrderCreateServlet在生成订单后执行UPDATE product SET status='sold_out' WHERE id=?,而非物理删除,便于后台审计。
4.3order_info表:交易原子性与买家/卖家双关联
订单表同时关联买家和卖家,体现交易双边性:
CREATE TABLE `order_info` ( `id` VARCHAR(32) PRIMARY KEY COMMENT '订单号,格式:ORD20240520123456', `buyer_id` INT NOT NULL COMMENT '买家ID', `seller_id` INT NOT NULL COMMENT '卖家ID', `product_id` INT NOT NULL COMMENT '商品ID', `amount` DECIMAL(10,2) NOT NULL COMMENT '成交金额', `status` ENUM('pending','paid','shipped','completed','cancelled') NOT NULL DEFAULT 'pending' COMMENT '订单状态', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (`buyer_id`) REFERENCES `user`(`id`), FOREIGN KEY (`seller_id`) REFERENCES `user`(`id`), FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;- 订单号生成:
id为字符串类型,由OrderCreateServlet生成,格式为ORD+yyyyMMdd+6位随机数(如ORD20240520123456),保证全局唯一且可读; - 双重外键:
buyer_id和seller_id均引用user.id,确保订单中买卖双方均为有效用户;product_id引用product.id,保证订单对应真实商品; - 状态扩展性:
status预留'shipped'(已发货)、'completed'(已完成)等状态,为后续物流对接留出接口。
5. 部署与调试实战:从本地 Tomcat 到数据库连接的完整链路验证
5.1 Tomcat 8.5+ 部署步骤与常见 404 错误定位
将market文件夹整体复制到 Tomcat 的webapps/目录下,启动 Tomcat 后访问http://localhost:8080/market/login.jsp。若遇 404,按以下顺序排查:
- 检查项目结构:确认
market/WEB-INF/web.xml存在且格式正确(无 XML 语法错误);market/WEB-INF/classes/下应有编译后的.class文件(如com/example/servlet/LoginServlet.class); - 验证 Servlet 映射:
web.xml中<servlet-mapping>的<url-pattern>必须与浏览器请求路径匹配。例如,访问/market/login时,<url-pattern>/login</url-pattern>是正确的,而<url-pattern>/servlet/login</url-pattern>会导致 404; - 日志分析:查看
tomcat/logs/catalina.out,搜索SEVERE关键字。常见错误如ClassNotFoundException: com.mysql.cj.jdbc.Driver,表明 MySQL JDBC 驱动未放入market/WEB-INF/lib/目录。
注意:Tomcat 9+ 默认使用 Jakarta EE 命名空间,若项目中
web.xml的xmlns仍为http://java.sun.com/xml/ns/javaee,需升级为https://jakarta.ee/xml/ns/jakartaee,否则容器可能忽略web.xml配置。
5.2 MySQL 连接配置与DBUtil工具类解析
数据库连接由com.example.util.DBUtil类统一管理,其核心方法如下:
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/market?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "123456"; public static Connection getConnection() throws SQLException { try { Class.forName("com.mysql.cj.jdbc.Driver"); // 加载驱动 } catch (ClassNotFoundException e) { throw new SQLException("MySQL Driver not found", e); } return DriverManager.getConnection(URL, USER, PASSWORD); } }- URL 参数说明:
useSSL=false:禁用 SSL(本地开发环境通常无需);serverTimezone=Asia/Shanghai:解决时区不一致导致的时间字段错乱;allowPublicKeyRetrieval=true:适配 MySQL 8.0+ 的新认证插件;
- 密码安全:生产环境应将
USER和PASSWORD移至外部配置文件(如db.properties),并通过ResourceBundle.getBundle("db")加载,避免硬编码。
5.3 一个关键调试技巧:在AuthFilter中打印请求链路
当遇到“登录后无法访问/user/home.jsp”问题时,在AuthFilter.java的doFilter()方法开头添加日志:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; System.out.println(">>> AuthFilter: " + req.getRequestURL() + "?" + req.getQueryString()); System.out.println(">>> Session ID: " + req.getSession().getId()); System.out.println(">>> User in session: " + req.getSession().getAttribute("user")); if (req.getSession().getAttribute("user") == null) { System.out.println(">>> Redirecting to /login.jsp"); req.getRequestDispatcher("/login.jsp").forward(request, response); return; } chain.doFilter(request, response); }- 作用:在控制台输出每次请求的完整 URL、Session ID 及其中的
user对象,可快速判断是登录未成功(user为 null)、Session ID 不一致(跨请求 Session 丢失),还是 Filter 链未正确配置; - 验证方法:启动 Tomcat 后,依次访问
/login.jsp→ 提交表单 → 观察控制台输出,确认user对象是否在登录后被正确写入 Session。
本文还有配套的精品资源,点击获取