ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java+JSP校园二手交易平台毕设:从数据库设计到下单事务的完整实现

Java+JSP校园二手交易平台毕设:从数据库设计到下单事务的完整实现 简介这是一套面向计算机相关专业学生的校园二手物品交易平台完整项目采用Java语言与JSP技术开发遵循MVC设计模式适合作为毕业设计、期末大作业或项目实战练习的参考方案。系统涵盖用户注册登录、商品发布与浏览、交易沟通、订单管理、在线支付、评价反馈等核心模块数据库设计完整代码注重可读性与安全性对SQL注入、跨站脚本等风险做了防护处理。资源包共1305个文件约22.74MB包含128个java源文件、119个jsp页面、364个js脚本、146个css样式及大量png、gif、jpg图片素材另有sql建库脚本、doc开发文档、pptx演示文稿与properties配置文件源码经本地编译调试可直接运行。目前已有39人学习下载。随包附带的开发文档与数据文档详细说明了设计思路、开发流程与数据库结构便于快速上手并深入理解整个Web项目的实现脉络。1. 校园二手物品交易平台为什么用 Java JSP 做毕设依然是最稳的选择每年毕业季校园里最真实的场景不是拍毕业照而是宿舍楼下堆成山的二手书、自行车、小电器。信息不对称让这些东西要么被贱卖要么直接扔掉。做一个校园二手物品交易平台本质上是把「谁有闲置」和「谁需要」这两端用一套可信的账号体系和分类检索连起来。这个标题里的关键词是 Java、JSP、校园二手物品交易平台它指向的不是一个玩具 demo而是一套能跑通注册、发布、浏览、下单、留言的完整 Web 应用。适合谁适合正在做课程设计或毕设的计算机专业学生也适合想用 Java 技术栈练手一个真实业务闭环的入门开发者。JSP 虽然常被吐槽老但它的请求响应模型足够直白配合 Servlet 和 JDBC能把「一个请求从浏览器到数据库再回来」的全过程看得清清楚楚这对打基础比直接上框架更有价值。2. 技术选型与数据库设计JSP Servlet JDBC 的最小可用组合2.1 为什么不用 Spring Boot 一步到位很多同学一上来就想用 Spring Boot MyBatis觉得这样才「现代」。但毕设场景下答辩老师更关心你是否理解 MVC 的流转而不是你会不会配 starter。JSP Servlet JDBC 这套组合每个环节都是显式的浏览器发请求Servlet 接住调 DAODAO 用 JDBC 查 MySQL结果塞进 request 域转发给 JSP 渲染。没有自动装配的黑匣子出了问题你能顺着调用栈一路查到 SQL。常见做法是分层entity放实体类dao放数据库操作service放业务逻辑servlet放控制层util放数据库连接工具。这个结构不复杂但足够让代码有边界。选型上还有一个现实考虑JSP 对表单提交和页面跳转的支持是原生的request.getParameter()直接拿参数response.sendRedirect()直接跳转不需要额外学一套注解体系。对于「校园二手物品交易平台」这种以 CRUD 为主的应用这套组合的开发效率并不低。2.2 数据库表结构五张表撑起核心业务一个能跑通的二手平台最少需要五张表用户表、商品表、分类表、订单表、留言表。下面是我一般会用的建表语句字段类型和约束都按实际业务来定。-- 用户表区分普通用户和管理员 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), phone VARCHAR(20), role TINYINT DEFAULT 0, -- 0普通用户 1管理员 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 分类表二手物品的类别 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(30) NOT NULL, sort_order INT DEFAULT 0 ); -- 商品表核心表关联用户和分类 CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL, image_url VARCHAR(255), category_id INT, seller_id INT, status TINYINT DEFAULT 0, -- 0在售 1已售 2下架 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES category(id), FOREIGN KEY (seller_id) REFERENCES user(id) ); -- 订单表记录交易 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, goods_id INT NOT NULL, buyer_id INT NOT NULL, seller_id INT NOT NULL, amount DECIMAL(10,2), status TINYINT DEFAULT 0, -- 0待确认 1已完成 2已取消 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (goods_id) REFERENCES goods(id), FOREIGN KEY (buyer_id) REFERENCES user(id) ); -- 留言表商品下的提问和回复 CREATE TABLE message ( id INT PRIMARY KEY AUTO_INCREMENT, goods_id INT NOT NULL, user_id INT NOT NULL, content VARCHAR(500), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (goods_id) REFERENCES goods(id), FOREIGN KEY (user_id) REFERENCES user(id) );逻辑说明goods表用status字段控制商品生命周期而不是物理删除这样订单表的外键不会断。orders表同时冗余了buyer_id和seller_id查询「我买到的」和「我卖出的」都不用再连goods表绕一圈。参数上price用DECIMAL(10,2)而不是FLOAT避免金额计算出现0.1 0.2 0.30000000000000004这种经典翻车。create_time统一用DATETIME并给默认值插入时不用手动传。2.3 数据库连接工具类一个 JDBC 工具类解决重复代码每次查数据库都写DriverManager.getConnection是灾难。我一般会抽一个DBUtil用静态代码块加载驱动提供获取连接和关闭资源的方法。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/campus_trade?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD your_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, PreparedStatement ps, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (ps ! null) ps.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }逻辑说明Class.forName放在静态块里只执行一次避免每次获取连接都反射加载驱动。close方法按ResultSet → PreparedStatement → Connection的顺序关闭这是 JDBC 资源释放的标准顺序反过来关可能报错。参数上serverTimezoneAsia/Shanghai必须加否则 MySQL 8 驱动会抛时区异常characterEncodingutf8保证中文不乱码。注意连接池在生产环境是必须的但毕设阶段用直连足够先把业务跑通再考虑优化。3. 核心功能实现从发布商品到下单的完整链路3.1 商品发布文件上传与表单处理的三个关键点发布商品是整个平台最核心的写操作涉及文本字段和图片文件。JSP 表单必须用enctypemultipart/form-dataServlet 端用MultipartConfig注解接收文件。WebServlet(/goods/publish) MultipartConfig(maxFileSize 5 * 1024 * 1024, maxRequestSize 10 * 1024 * 1024) public class GoodsPublishServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String title req.getParameter(title); String priceStr req.getParameter(price); String categoryIdStr req.getParameter(categoryId); String description req.getParameter(description); // 参数校验空值和数字格式 if (title null || title.trim().isEmpty() || priceStr null) { req.setAttribute(msg, 标题和价格不能为空); req.getRequestDispatcher(/publish.jsp).forward(req, resp); return; } BigDecimal price; try { price new BigDecimal(priceStr); if (price.compareTo(BigDecimal.ZERO) 0) throw new NumberFormatException(); } catch (NumberFormatException e) { req.setAttribute(msg, 价格必须是正数); req.getRequestDispatcher(/publish.jsp).forward(req, resp); return; } // 处理图片上传 Part filePart req.getPart(image); String imageUrl null; if (filePart ! null filePart.getSize() 0) { String fileName System.currentTimeMillis() _ filePart.getSubmittedFileName(); String savePath getServletContext().getRealPath(/uploads); File dir new File(savePath); if (!dir.exists()) dir.mkdirs(); filePart.write(savePath File.separator fileName); imageUrl uploads/ fileName; } User user (User) req.getSession().getAttribute(user); Goods goods new Goods(); goods.setTitle(title); goods.setPrice(price); goods.setDescription(description); goods.setCategoryId(Integer.parseInt(categoryIdStr)); goods.setSellerId(user.getId()); goods.setImageUrl(imageUrl); new GoodsDao().insert(goods); resp.sendRedirect(req.getContextPath() /goods/list); } }逻辑说明MultipartConfig的maxFileSize限制单文件 5MBmaxRequestSize限制整个请求 10MB防止有人传超大文件把服务器磁盘打满。价格用BigDecimal接收并校验正数避免负数价格这种逻辑漏洞。图片文件名用System.currentTimeMillis()加前缀防止同名文件互相覆盖。getRealPath(/uploads)拿到的是 Web 应用部署目录下的绝对路径注意这个目录在重新部署时可能被清空生产环境应该存到应用外部路径。参数上req.setCharacterEncoding(UTF-8)必须在getParameter之前调用否则中文标题会乱码。filePart.write()是 Servlet 3.0 提供的便捷方法底层还是流拷贝大文件场景要自己写缓冲流。3.2 商品列表与分页别让全表查询拖垮页面商品列表页如果直接SELECT * FROM goods数据量一上来页面就卡死。分页是必须的核心是LIMIT offset, size。public ListGoods findByPage(int page, int size, Integer categoryId) { StringBuilder sql new StringBuilder( SELECT g.*, u.nickname AS sellerName, c.name AS categoryName FROM goods g LEFT JOIN user u ON g.seller_id u.id LEFT JOIN category c ON g.category_id c.id WHERE g.status 0 ); ListObject params new ArrayList(); if (categoryId ! null) { sql.append(AND g.category_id ? ); params.add(categoryId); } sql.append(ORDER BY g.create_time DESC LIMIT ?, ?); params.add((page - 1) * size); params.add(size); ListGoods list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Goods g new Goods(); g.setId(rs.getInt(id)); g.setTitle(rs.getString(title)); g.setPrice(rs.getBigDecimal(price)); g.setImageUrl(rs.getString(image_url)); g.setSellerName(rs.getString(sellerName)); g.setCategoryName(rs.getString(categoryName)); list.add(g); } } } catch (SQLException e) { throw new RuntimeException(分页查询失败, e); } return list; }逻辑说明用StringBuilder拼 SQL 是因为分类筛选是可选条件用WHERE 11再拼AND是常见做法但这里直接判断categoryId是否为空更清晰。LEFT JOIN保证即使卖家被删或分类被删商品记录依然能查出来。ORDER BY create_time DESC让最新发布的排前面符合二手平台的浏览习惯。参数上(page - 1) * size是偏移量计算第 1 页偏移 0第 2 页偏移 size。注意LIMIT的偏移量在数据量很大时性能会下降但校园平台的数据量级完全够用。3.3 下单流程用事务保证订单和商品状态一致下单涉及两个写操作插入订单记录、更新商品状态为已售。这两步必须在一个事务里否则可能出现「订单插入了但商品还在售」的数据不一致。public boolean createOrder(int goodsId, int buyerId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 检查商品是否在售并锁定该行 String checkSql SELECT seller_id, price, status FROM goods WHERE id ? FOR UPDATE; int sellerId; BigDecimal price; try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setInt(1, goodsId); try (ResultSet rs ps.executeQuery()) { if (!rs.next() || rs.getInt(status) ! 0) { conn.rollback(); return false; } sellerId rs.getInt(seller_id); price rs.getBigDecimal(price); } } // 2. 不能买自己的商品 if (sellerId buyerId) { conn.rollback(); return false; } // 3. 插入订单 String orderSql INSERT INTO orders (goods_id, buyer_id, seller_id, amount, status) VALUES (?, ?, ?, ?, 0); try (PreparedStatement ps conn.prepareStatement(orderSql)) { ps.setInt(1, goodsId); ps.setInt(2, buyerId); ps.setInt(3, sellerId); ps.setBigDecimal(4, price); ps.executeUpdate(); } // 4. 更新商品状态 String updateSql UPDATE goods SET status 1 WHERE id ? AND status 0; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setInt(1, goodsId); if (ps.executeUpdate() 0) { conn.rollback(); return false; } } conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw new RuntimeException(下单失败, e); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} } } }逻辑说明SELECT ... FOR UPDATE在事务中锁定商品行防止两个用户同时下单同一件商品。UPDATE goods SET status 1 WHERE id ? AND status 0这个条件更新是第二道保险如果商品已经被别人抢先改成已售executeUpdate()返回 0直接回滚。参数上setAutoCommit(false)开启事务commit()提交rollback()回滚finally里恢复自动提交并关闭连接。注意FOR UPDATE必须在事务里才有意义如果autoCommit是 true锁会立即释放。4. 避坑与排查JSP JDBC 项目最容易翻车的五个地方4.1 中文乱码POST 和 GET 要分开处理现象表单提交的中文标题在数据库里变成???或者页面显示乱码。原因POST 请求的编码由request.setCharacterEncoding控制GET 请求的编码由服务器server.xml的URIEncoding控制两者不是一回事。解决POST 在 Servlet 第一行写req.setCharacterEncoding(UTF-8)GET 在 Tomcat 的server.xml里给 Connector 加URIEncodingUTF-8。数据库连接 URL 里也要带characterEncodingutf8。JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 languagejava %。4.2 数据库连接泄漏连接关了但没完全关现象项目跑一段时间后报Too many connections。原因Connection、PreparedStatement、ResultSet三个资源只要有一个没关连接就不会真正归还。解决用 try-with-resources 语法把资源声明在 try 的括号里Java 会自动关闭。如果手写close必须放在finally块里并且每个资源单独 try-catch避免一个关闭失败导致后面的关不掉。4.3 JSP 页面报 404路径到底该怎么写现象表单提交后跳转 404或者图片加载不出来。原因JSP 里的相对路径是相对于当前页面 URL不是相对于项目根目录。比如/goods/detail.jsp里写srcuploads/1.jpg浏览器会请求/goods/uploads/1.jpg。解决所有路径用request.getContextPath()拼绝对路径比如img src${pageContext.request.contextPath}/uploads/${goods.imageUrl}。表单的action同理用${pageContext.request.contextPath}/goods/publish。4.4 重复下单并发场景下的经典翻车现象两个用户同时点「立即购买」结果生成两条订单商品状态却只改了一次。原因检查和更新之间有时间窗口两个线程都通过了检查。解决用SELECT ... FOR UPDATE锁行或者用条件更新UPDATE goods SET status 1 WHERE id ? AND status 0判断影响行数。更稳妥的做法是给orders表的goods_id加唯一索引数据库层面兜底。4.5 图片上传后不显示目录权限和路径映射现象上传成功但页面显示裂图。原因getRealPath(/uploads)指向的是部署目录如果 IDE 重新部署项目这个目录会被清空另外 Tomcat 对非 Web 目录的文件访问有限制。解决把上传目录配到项目外部比如D:/campus_uploads然后在 Tomcat 的server.xml里配一个Context docBaseD:/campus_uploads path/uploads/。或者用 Servlet 写一个图片读取接口从外部路径读流写回响应。5. 进阶技巧用过滤器统一编码和登录校验项目写到后面你会发现每个 Servlet 都写一遍setCharacterEncoding和登录判断代码重复且容易漏。我一般会加两个 Filter一个EncodingFilter统一设 UTF-8一个LoginFilter拦截未登录用户。WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } } WebFilter(urlPatterns {/goods/publish, /order/*, /user/profile}) public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; if (request.getSession().getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }逻辑说明EncodingFilter拦截/*在所有请求进入 Servlet 之前设好编码Servlet 里就不用再写。LoginFilter只拦截需要登录的路径用urlPatterns精确控制避免把登录页和商品列表页也拦了。参数上WebFilter是 Servlet 3.0 注解不需要在web.xml里配。注意多个 Filter 的执行顺序由类名首字母决定EncodingFilter要排在LoginFilter前面否则登录跳转的中文提示会乱码。验证方法很简单不登录直接访问/goods/publish应该被弹回登录页登录后发一个中文标题的商品数据库里应该存的是正常中文。我自己的习惯是每加一个 Filter就用浏览器无痕模式跑一遍完整流程因为无痕模式没有缓存和旧 Session最容易暴露问题。这套东西不复杂但把编码、登录、事务、分页这四个点做扎实一个校园二手物品交易平台就已经能拿得出手了。希望帮到你。本文还有配套的精品资源点击获取
返回列表