JSP+Servlet+JavaBean实现二手交易平台:从源码部署到数据库设计全解析
2026/9/24 20:54:48 网站建设 项目流程

简介:这是一份基于JSP的二手交易平台管理系统完整源码,面向Java Web学习者、毕业设计学生以及需要快速搭建交易类项目的开发者。系统涵盖用户注册登录、商品发布浏览、订单交易、后台管理等核心模块,并注重数据库设计、安全机制、异常处理等关键实现,能够完整体现一个Web应用从页面展示到数据持久化的全过程。压缩包共2000个文件,以JSP页面、Java类、XML配置、JS/CSS前端资源、PNG图标及SQL脚本为主,大小约52.57MB,目录结构清晰,便于按控制层、服务层、数据访问层分层查阅。当前已有90人学习浏览,借助该源码可深入理解JSP+Servlet+JDBC的经典开发模式,掌握前端交互、业务逻辑与数据访问的协作方式,也能参考其性能优化和AJAX异步更新思路,为毕业设计或项目答辩提供可运行、可扩展的完整范例。

1. 二手交易平台源码不是拿来跑通的,是拿来改的

拿到一个"基于JSP的二手交易平台管理系统源码.zip",多半是要交Java课程设计或毕业设计。可很多人解压后第一反应是"东西在哪、怎么打开",第二反应是"跑起来了,但看上去很粗糙"。这个标题背后真正值钱的部分,不是那几行JSP标签,而是整个JavaWeb的经典分层:JSP做展示、Servlet做控制、JavaBean做模型,数据库落在MySQL上。这套结构是JavaWeb课设最稳的骨架,也是面试时最容易被追问的"MVC到底烂熟没有"。

这篇笔记我就按实际开发顺序讲:怎么把zip还原成能跑的项目,表怎么建,交易链路怎么做,最后用一条一条踩坑经历告诉你什么东西会翻车。你不需要懂太多Spring,只要会Servlet语法,就能把这个项目改成答辩时拿得出手的样子。

2. 先立住技术底座:JSP+Servlet+JavaBean这套结构,决定了你后面好不好改

2.1 从zip到能响应的最小环境:JDK、Tomcat、MySQL三件套怎么配

二手交易平台这个规模,我推荐你直接用最经典的一组版本:JDK 8、Tomcat 9.x、MySQL 5.7。这三个版本都是"老而稳",网上资料最多,报错随便搜都有答案。JDK 11以上不是不行,但有的老项目用了内部API,切换版本后会出现莫名其妙的反射报错,没必要为了图新给自己挖坑。

解压zip后,你会看到一个典型的JavaWeb工程目录。Eclipse导入时选Existing Projects into Workspace,IDEA里选Open直接定位到源码根目录。结构一般是这种样子:

secondhand-platform/ │ src/ │ com/platform/bean/ # 实体类 │ com/platform/dao/ # 数据库操作 │ com/platform/servlet/ # 控制器 │ com/platform/filter/ # 编码/登录过滤器 │ WebContent/ │ index.jsp │ login.jsp │ register.jsp │ goods_list.jsp │ goods_detail.jsp │ cart.jsp │ order.jsp │ admin/ # 后台管理页面 │ static/ # css/js/images │ WEB-INF/web.xml └── sql/ └── secondhand.sql # 建库建表脚本

导入之后第一件事是把JRE和Tomcat关联起来。在Eclipse里Servers窗口新建Tomcat v9.0 Server,Runtime Environment指向你的Tomcat解压路径。IDEA用户更简单,Run Configuration里选Tomcat Server,Deployment里把项目打成的war包加进去。这一步最容易出错的是JDK版本不匹配,我一般习惯把Compiler compliance level统一设成1.8,省得后面报Bad version 55.0之类的错。

2.2 看懂WEB-INF这一整套目录:哪层负责控制、哪层负责展示、哪层负责数据

如果你把项目翻开了,看到一个Servlet的doGet方法里面全是out.println()输出HTML,先别急着嫌弃。这是很多早期课设项目的通病:把Java代码和HTML混在一起。规范的JSP项目应该是"低内聚、高分层":

  • JSP页面只负责渲染,用<c:forEach>${}取值,不写复杂的Java逻辑。
  • Servlet接收请求,调用DAO层方法处理业务,然后把结果放到request或session里forward到JSP。
  • Bean层是纯数据载体,字段名跟数据库列名一一对应。
  • DAO层只有一个职责:获取Connection、执行SQL、关闭资源。

我在这个项目里一般会先做两个基础类:DBUtil负责数据库连接,BaseServlet负责反射分发请求。分发的意义是减少Servlet类的数量——你不想一个登录功能就写一个LoginServlet,里面还要分doGet/doPost。做一个基础分发之后,一个模块对应一个Servlet,action参数决定走哪个方法,代码量能压缩三分之一。

2.3 为什么这套架构值得选,以及什么时候你会后悔选它

从项目本身来看,JSP这套技术确实老,但选它有一个非常现实的理由:课程设计和毕设的评分标准里,"功能覆盖完整度"比"技术先进性"占的比重更大。SpringBoot集成JSP虽然也能做,但配置路径、打包war部署的坑比单纯JSP项目多一圈,对不熟悉Maven的师弟师妹很不友好。JSP+Servlet的模型在Tomcat里天然可跑,右键Run一下就出页面,出错的概率低得多。

什么时候会后悔?当你真要上生产环境、面对高并发时,JSP这套就顶不住了。页面首次编译慢、前后端耦合深、和前端工程师协作困难。但这个平台是课设,不是电商双11,量级完全不同。真要被问到"为什么不选SpringBoot",你答一句"这个项目重点是理解HTTP请求如何贯穿JSP/Servlet/DAO三层"就够得体了。

3. 把数据库设计读透:二手交易平台到底要几张表

3.1 用户、商品、订单、留言:四张核心表的主键关系与字段设计

打开sql目录下的secondhand.sql,你会看到建库建表语句。大多数二手交易平台的设计逃不出这四张核心表:用户表、商品表、订单表、留言表。有些还会加一张收藏表或者分类表,但我先按最标准的四张来讲。

用户表的核心字段就这几个:id、username、password、phone、address、avatar、create_time。密码字段我建议用varchar(64),虽然课设阶段很少真正做加密,但至少不要在数据库里存明文。商品表是重头戏:id、seller_id、title、description、price、original_price、category、images、status、create_time,status字段用int表示,0在售、1已卖出、2下架,这套状态机放到下一小节细讲。订单表则有id、order_no、goods_id、buyer_id、seller_id、amount、status、create_time,order_no用时间戳加随机数生成。留言表最简单,id、user_id、goods_id、content、create_time。

主键全部用自增id,外键不加物理约束、只保留逻辑关联是有意的。二手交易平台的项目规模用不上ON DELETE CASCADE,加上外键之后删除用户会被约束卡住,课设演示时反而添乱。

3.2 商品状态与交易流程:从"在售"到"已卖出"的状态机怎么定

这是我见过最多人做错的地方。很多同学给商品表加一个is_sold布尔值,然后订单表里再存一个订单状态,两个状态互相打架:商品已卖出但订单还显示待付款,或者订单已取消但商品还挂着。一套严格的交易流程状态机应该是这样:

0 在售 -> 用户点击购买 -> 生成订单,状态变1(锁定) 1 锁定 -> 买家付款/确认 -> 状态变2(已卖出) 0 在售 -> 卖家主动下架 -> 状态变3(下架) 1 锁定 -> 买家超时未付/取消 -> 状态回0(重新在售)

这个设计里,"锁定"状态非常关键。它解决的是并发下单问题:A用户看到商品在售,下单后商品立刻变成锁定,B用户再来买时提示"商品已被人拍下"。如果没有这个状态,两个人同时下单,两个订单都生成,但商品只有一个,后面就要做复杂的库存校验。二手交易平台的每件商品都是独一无二的,锁定状态轻松解决了这个边界问题,你在答辩时把这套逻辑讲出来,老师会觉得很完善。

3.3 初始化SQL脚本:照着建库建表的注意事项

在MySQL中执行导入脚本前,有几个地方需要动手改。项目提供的secondhand.sql里,数据库名一般是secondhand_db,字符集用utf8mb4而不是utf8,因为utf8mb4能存下emoji和生僻字。执行命令我用这种方式:

-- 直接执行建库建表脚本,注意先设置编码 CREATE DATABASE IF NOT EXISTS secondhand_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE secondhand_db; -- 用户表 CREATE TABLE `t_user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL, `password` VARCHAR(64) NOT NULL, `phone` VARCHAR(20) DEFAULT NULL, `address` VARCHAR(255) DEFAULT NULL, `avatar` VARCHAR(255) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里几个细节值得注意。用户名加唯一索引uk_username,注册时数据库层面就挡住了重复账号,只靠Java代码里查一次并不保险。create_timeDEFAULT CURRENT_TIMESTAMP,插入时不用显式赋值。表引擎必须用InnoDB,外键和事务都依赖它,MyISAM在这个项目里没有存在意义。导入时如果报Unknown collation错误,十有八九是MySQL版本的问题导致的排序规则不一致,改成utf8mb4_general_ci立刻就好。

商品表和订单表的SQL就不逐条贴了,你打开脚本照着改即可。我特别提醒一句:脚本里如果有DROP TABLE IF EXISTS,执行前先掂量一下库里有没有开发数据,花半天填进去的测试数据被一条脚本清空的痛苦,我经历过。

4. 核心交易链路:从商品发布到下单成交,这几个Servlet必改

4.1 商品发布:表单到后台,文件上传与图片保存

几乎所有二手交易平台都要有发布商品页面,这里面最常被卡住的不是参数接收,而是图片上传。我在这类项目里最常用的上传组件就是commons-fileuploadcommons-io,在web.xml里不配任何东西,直接在Servlet里解析请求。核心代码长这样:

// 商品发布Servlet中的文件上传处理段 protected void publish(HttpServletRequest request, HttpServletResponse response) { // 1. 创建DiskFileItemFactory,设置内存缓冲区和临时目录 DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024); // 1MB 内存缓冲 factory.setRepository(new File("/tmp/upload_tmp")); // 2. 用ServletFileUpload解析请求 ServletFileUpload upload = new ServletFileUpload(factory); upload.setFileSizeMax(5 * 1024 * 1024); // 单个文件最大5MB // 3. 分别取出普通表单字段与文件字段 List<FileItem> items = upload.parseRequest(request); String title = null, price = null; String imageUrl = null; for (FileItem item : items) { if (item.isFormField()) { // 普通输入框,比如商品标题、价格 if ("title".equals(item.getFieldName())) { title = item.getString("UTF-8"); } if ("price".equals(item.getFieldName())) { price = item.getString("UTF-8"); } } else { // 文件字段:生成唯一文件名并保存到项目外部目录 String fileName = System.currentTimeMillis() + "_" + item.getName(); String saveDir = "D:/upload/secondhand"; File dir = new File(saveDir); if (!dir.exists()) dir.mkdirs(); item.write(new File(saveDir, fileName)); imageUrl = "/upload/" + fileName; } } // 之后调用DAO插入数据库 goodsDao.insert(title, price, imageUrl); }

这段代码有三个参数要按你的环境改:setRepository的临时目录在Linux/Windows上路径写法不同,Windows上直接写D盘路径时注意目录要存在。imageUrl存的是虚拟路径,但文件真正保存到了D:/upload这个项目外目录,所以你要在Tomcat里配一个虚拟映射——下面避坑章节会展开讲。

4.2 购物车与订单生成:事务边界放在哪一层

有了商品之后,交易链路的下一环是把商品加入购物车和下单。先明确一点:JSP课设项目的购物车,用一张数据库表实现是完全可以的,没必要用Session存集合。原因是Session购物车存到服务器内存,用户刷新一下就丢,数据库表干净且能保留用户的历史记录。核心表结构就是cart_iduser_idgoods_idadd_time,不需要amount字段,因为同一个商品只允许出现一次——加入购物车时先查是否已存在,存在则提示"购物车已有该商品"。

生成订单这一步,一定要用事务。如果先改商品状态、再插入订单记录,中间任何一步抛异常,就会出现"商品锁定了但订单不存在"这种事故。Servlet层直接调用DAO,我习惯在DAO里把两步包进同一个事务:

// 订单DAO中的事务控制段 public void createOrder(Order order) throws Exception { Connection conn = null; PreparedStatement ps1 = null; PreparedStatement ps2 = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 1. 插入订单记录 String sqlOrder = "INSERT INTO t_order (order_no, goods_id, buyer_id, seller_id, amount, status) " + "VALUES (?, ?, ?, ?, ?, 0)"; ps1 = conn.prepareStatement(sqlOrder); ps1.setString(1, order.getOrderNo()); ps1.setInt(2, order.getGoodsId()); ps1.setInt(3, order.getBuyerId()); ps1.setInt(4, order.getSellerId()); ps1.setBigDecimal(5, order.getAmount()); ps1.executeUpdate(); // 2. 锁定商品 String sqlGoods = "UPDATE t_goods SET status = 1 WHERE id = ? AND status = 0"; ps2 = conn.prepareStatement(sqlGoods); ps2.setInt(1, order.getGoodsId()); int rows = ps2.executeUpdate(); // 关键:如果商品已经是锁定状态,影响行数为0,必须回滚 if (rows == 0) { conn.rollback(); throw new RuntimeException("商品已被他人购买"); } conn.commit(); } catch (Exception e) { if (conn != null) conn.rollback(); throw e; } finally { DBUtil.close(conn, ps1, ps2); } }

这段代码思路上有两个值得答辩时讲清楚的点。第一,conn.setAutoCommit(false)之后所有的SQL都在同一个事务里,要么都成功要么都回滚,不会出现脏数据。第二,UPDATE语句里带上了AND status = 0这个条件,这是一种乐观锁思路——先尝试修改,通过executeUpdate()的返回值判断是否真的抢到了商品。如果两个用户同时下单,数据库层面的行锁会保证只有一个UPDATE成功,另一个影响行数为0,直接回滚。

4.3 我的发布/我的购买:两个查询页面的SQL写法差异

后台"我发布的商品"和前台"我买到的商品"两个页面看起来相似,但SQL逻辑完全不同,这是很多新手写串的地方。我的发布页查询的是商品表,条件是seller_id = 当前用户,查询结果是商品列表,展示的是商品图、标题、价格。而我的购买页查询的是订单表,条件是buyer_id = 当前用户,但你要展示的不仅仅是订单号,还要把商品标题、商品图片join出来。

-- 我的发布:直接查商品表 SELECT id, title, price, image_url, status, create_time FROM t_goods WHERE seller_id = ? ORDER BY create_time DESC; -- 我的购买:需要关联商品表获取商品信息 SELECT o.order_no, o.amount, o.status AS order_status, o.create_time, g.title, g.image_url FROM t_order o INNER JOIN t_goods g ON o.goods_id = g.id WHERE o.buyer_id = ? ORDER BY o.create_time DESC;

两个查询的差异决定了前端JSP页面的渲染方式。我的发布页可以有一个"下架/重新上架"按钮,操作的是商品表的状态字段;我的购买页则根据订单状态显示"待付款/已付款/已取消"的文字或按钮。把这两条SQL理清楚,前后台功能就不会打架了。

5. 运行与改造避坑:JSP项目最容易翻车的 5 个典型问题

5.1 页面改完不生效:是浏览器缓存还是Tomcat编译缓存

现象是改了JSP文件,刷新浏览器后页面没有任何变化,甚至改了Servlet重启Tomcat也没用。出现这个现象时先看浏览器是否有缓存——F12打开控制台,Network标签下勾选Disable cache,再刷新一次。如果还不行,那就是Tomcat的work目录里保留了JSP编译成Java和class的缓存文件。

原因与解决:Tomcat编译是增量式的,JSP文件的时间戳没变就不重新编译。找到Tomcat安装路径下的work/Catalina/localhost/项目名目录,整个删掉,再重启Tomcat,JSP就会被强制重新编译。IDEA部署时如果你改了Servlet代码但没触发HotSwap,也会出现旧class还在的情况,最稳妥的方式是Rebuild Project,不要依赖IDEA的热部署,它对这个老架构支持得很差。

5.2 中文乱码一条线全排查:数据库连接URL是第一嫌疑

现象是注册后查看用户列表,中文名字全是问号或者乱码。排在第一位的原因是数据库连接URL少了characterEncoding参数,导致Java向MySQL写入数据时用了错误的字符集。第二嫌疑是JSP页面本身没设置pageEncoding,第三嫌疑是POST请求的编码过滤器没生效。

// DBUtil中正确的JDBC连接串写法 private static final String URL = "jdbc:mysql://localhost:3306/secondhand_db" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai";

这段连接串里useUnicode=truecharacterEncoding=utf8必须成套出现,缺一不可。serverTimezone=Asia/Shanghai是MySQL 8必须的参数,MySQL 5.7不写也能跑。代码检查顺序我建议:先看web.xml里的CharacterEncodingFilter有没有配,再看连接串,最后看JSP顶部的<%@ page contentType="text/html;charset=UTF-8" %>。顺着这条线查,5分钟必定位。

5.3 图片上传后前端展示404:路径映射没配,数据库里存的不是真实路径

现象是上传成功后数据库里有/upload/xxx.jpg,但页面上图片裂开,浏览器访问http://localhost:8080/upload/xxx.jpg直接404。原因是图片被存到了项目外部的D:/upload目录,Tomcat默认只服务项目部署目录里的静态资源,它根本不知道D:/upload这个目录的存在。

解决方式是在Tomcat的conf/server.xml的Host节点里加一段Context配置:

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <!-- 虚拟目录映射:URL路径 /upload/ 对应磁盘目录 D:/upload/ --> <Context path="/upload" docBase="D:/upload" reloadable="false" /> </Host>

加了这段配置后,浏览器请求/upload/xxx.jpg时Tomcat就会去D:/upload目录查找物理文件。你还需要保证D:/upload目录存在且Tomcat进程有读取权限。Windows系统下路径中的反斜杠要写成D:/upload这种正斜杠格式,Tomcat对反斜杠处理在某些版本上会报错。如果你把项目打包war部署的时候不想改Tomcat配置文件,更优雅的方案是写一个自定义Servlet专门处理图片读取,但课设阶段用Context映射是最省事的。

5.4 前端页面JSP里到处是Java代码:显示逻辑和业务逻辑混杂

现象就是你打开一个JSP文件,里面<% if (...) { %>到处都是,接下来就是改一处页面要同时小心Java逻辑和HTML标签。这个问题的根源不是写法本身,而是"方便"。很多课设源码为了直白,直接在JSP里写Java代码取数据,不用EL表达式和JSTL标签。开发时确实快,但你做中期演示时Product Owner(也就是你的指导老师)提出"页面价格要显示红色加粗",你不得不在一堆Java代码里找HTML的位置。

解决方式推荐在JSP页面统一改用EL表达式和JSTL核心标签:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:forEach items="${goodsList}" var="g"> <div class="goods-card"> <img src="${g.imageUrl}" alt="商品图片" /> <h3>${g.title}</h3> <p class="price">¥${g.price}</p> <c:if test="${g.status == 0}"> <a href="cart?action=add&goodsId=${g.id}">加入购物车</a> </c:if> </div> </c:forEach>

${g.price}这种EL表达式会自动调用JavaBean的getPrice()方法,页面上没有任何Java脚本片段,前端看着清爽,你也不用担心JSP引入的Java代码编译失败连累整个页面无法显示。

5.5 数据库连不上、端口被占用、内存溢出:环境类问题汇总

这三个问题属于"打开一个老项目"时的经典三连。连不上数据库时先执行telnet localhost 3306看端口通不通,不通就检查MySQL服务有没有启动;通了再检查用户名密码是否为源码默认的root/123456,这种老项目十有八九配置写死了,搜索jdbc:mysql即可定位配置文件。Tomcat启动报端口被占用时,找到占用8080的进程关掉,或者改server.xml里的Connector端口。内存溢出绝大多数是JDK版本太高导致,换回JDK 8就安静了。

6. 部署上线前,用一套自查清单检验你的项目能不能撑住演示

项目做得差不多了,别急着打包交差。我最后一次验收课设项目时有一套固定的检查流程,按这个顺序过一遍,演示时基本不会翻车。

第一关是数据层。登录管理员账号,看后台的用户列表、商品列表是否正常显示。这里要特别留意空数据的情况——数据库里一条测试数据都没有时,页面会不会报空指针或者显示一堆空表格。一个顶多花10分钟就能修好的方案:在DAO层把查询结果为空时的null改成new ArrayList<>(),JSP页面上配上<c:if test="${empty list}">的友好提示。

第二关是编码与路径。找一台干净的电脑,按下面的步骤做一次模拟验收:把项目导出成war包放到Tomcat的webapps目录,启动后访问首页,注册一个新用户,发布一件二手商品带图片,再另注册一个账号去下单,确认商品在"我的发布"里状态从未售出变成已锁定。这一遍走完,前面所有的乱码、图片404、状态不同步问题都会暴露出来。

第三关是边界操作。反复刷新商品详情页,观察库存对应的状态有没有异常变化;快速双击"立即购买"按钮,看会不会生成两个订单。这些动作在课设答辩现场是高频出现的"意外",提前用一笔测试数据跑几遍,比到时候慌慌张张改代码体面得多。如果你还有精力,可以把分页加进去——商品列表超过20条时用LIMIT ? OFFSET ?做分页,这个是老师最爱问的加分点。

我自己的习惯是留一个备份SQL脚本放在项目外的安全位置,每次跑数据测试前先备份。JSP项目里一个PreparedStatement的?占位符数量写错,就可能让整张表的数据变得不可用。做二手交易平台,功能可以朴素,但这种稳定性和安全性上的小节,恰恰是拿分的关键。希望这篇笔记能帮你把这个经典JavaWeb项目从"能跑"做到"经得起问",祝顺利。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询