☰
JSP+MySQL校园二手交易系统:源码复现与避坑实践指南
2026/10/7 3:01:10 网站建设 项目流程

简介:JSP+MySQL 校园二手物品交易系统源码,面向毕业设计、课程设计及 Java Web 学习者,采用 B/S 架构和 SSM 框架,完整实现前台与后台:支持用户注册登录、物品发布搜索、订单管理、后台分类等,贴合校园闲置交易场景。包体为 7z,共 202 个文件、约 12.63MB,含 40 个 Java 源文件、17 个 JSP 页面、40 个 class 文件及图片、CSS、JS、XML、SQL 等资源,既有可运行项目也便于源码研读。已有 21 人学习,适合参考其中的 SSM 整合流程、订单模块编码和数据库表设计,对完成课程设计或毕设很有帮助。通过源码可快速掌握前后台交互逻辑、分类管理方式,理顺物品发布、订单流转和用户权限等关键环节,基于 SQL 脚本直接部署并扩展为自己的毕业设计作品。

1. 校园二手交易系统源码:JSP+MySQL 到底能复现什么

这套源码我拆过不止一次。评审老师最常问的四个问题——密码存没存明文、商品图片怎么传上去、订单下了商品状态怎么变、数据库连接断了页面怎么表现——在这份基于 JSP + MySQL 的校园二手物品交易系统里都能找到对应代码。它不是 Spring Boot 那种半自动 MVC,而是 JSP + Servlet + JDBC 的经典 Java Web 结构,登录、注册、商品发布、列表搜索、购物车、下单、后台管理都有独立页面,适合毕业设计和课程设计这类必须把原理讲清楚的场景。如果你是第一次拿这个工程,我的建议是先把它当成一套能改的骨架:表结构齐全、三层边界清楚、扩展点好找,这比直接当商城上线要靠谱得多。

2. 架构与数据表:三层结构怎么分,五张表怎么落

开始复现之前,先摸结构。JSP 项目最容易劝退新手的不是语法,而是不知道哪些代码负责接请求、哪些代码负责查库、哪些页面负责展示。先搞清楚这三层,后面部署和改功能都会顺很多。

2.1 为什么 JSP + Servlet + JDBC 反而是好选择

很多买家第一眼看到 SSM、Spring Boot 就觉得更“高级”,但我给毕设写代码更看重一句话能讲完的链路:JSP 页面发起请求,Servlet 接收参数并调用 DAO,DAO 用 JDBC 拼 SQL,结果再回到 JSP。中间没有 Spring 容器,也没有 MyBatis 的 Mapper 代理,代码是“死”的,但也是透明的。评审追问“登录状态存在哪”,你可以直接指给他看 session;追问“SQL 注入怎么防”,你能当场打开 DAO 里的 PreparedStatement。

拿这套校园二手交易系统来说,发布商品、搜索、加购物车、下订单,走的都是同一条链路。它不像前后端分离项目那样要同时调接口、跨域、处理 token,从头到尾只有一个 Java Web 应用。毕设场景下这样做的好处是:好写、好讲、好改。你换成 Spring Boot 反而要解释自动配置和框架约定,绕一圈又回到业务本身。

如果你是新手,真正值得注意的不是“用不用框架”,而是别把 DAO 代码写进 JSP。我见过不少 JSP 里直接 Class.forName 查数据库的版本,跑起来没问题,答辩被问“MVC 是什么”就露馅。这套源码里如果 servlet 包和 dao 包分得清楚,那基本就是规范的三层结构,你需要做的只是把每个包的作用读透。

2.2 拿到源码先看这三个目录

我拿到一个 Java Web 工程,第一步不是急着启动,而是先看三个地方:web.xml、数据库工具类、sql 脚本。web.xml 决定所有 Servlet 映射,数据库工具类决定连接参数,sql 脚本决定表结构。这三处能对上,项目 80% 能直接跑。

目录/文件作用先看什么
src/com/xx/entity实体类字段是否和数据表列名一一对应
src/com/xx/daoDAO 层有没有写 PreparedStatement,连接是否关闭
src/com/xx/servlet控制层请求路径和 web.xml 的 url-pattern 是否一致
webapp(WebContent)JSP 页面页面里有没有直接写 Java 代码
sql/campus_shop.sql建表脚本字符集、外键、初始管理员账号

这里的常见错误是 IDEA 导入 Eclipse 工程后,Web 根目录没设置,选中了项目根目录而不是 webapp,启动 Tomcat 永远找不到页面。先看清楚目录结构再动手,比反复重启 Tomcat 有用得多。典型工程的目录长这样:

campus_shop/ ├─ src/com/campus/entity/User.java ├─ src/com/campus/dao/UserDao.java ├─ src/com/campus/servlet/LoginServlet.java ├─ src/com/campus/util/DBUtil.java ├─ webapp/login.jsp ├─ webapp/index.jsp ├─ webapp/admin/manage_user.jsp └─ sql/campus_shop.sql

目录本身不会直接决定能否运行,但它决定你排错时往哪个文件找。实体类对应表字段,工具类对应连接配置,webapp 对应页面展示,三块分清楚后,任何报错都能立刻定位到具体层。

2.3 核心表结构:五张表一次看懂

校园二手交易系统拆到数据库层面,核心就是用户、分类、商品、订单、留言。购物车要不要建表?我的建议是不建,session 就够,理由在后面单独说。下面这一套建表脚本是常见做法,字段名可能和源码里有出入,但业务含义基本一致。

CREATE DATABASE campus_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE campus_shop; CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL COMMENT '登录名', `password` VARCHAR(64) NOT NULL COMMENT '当前源码存的是明文,改造后存 MD5 值', `nickname` VARCHAR(50) DEFAULT NULL, `phone` VARCHAR(20) DEFAULT NULL, `email` VARCHAR(100) DEFAULT NULL, `role` TINYINT NOT NULL DEFAULT 0 COMMENT '0普通用户,1管理员', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `category` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL COMMENT '分类名:教材、数码、生活', `sort` INT DEFAULT 0 COMMENT '排序,越小越靠前', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类表'; CREATE TABLE `goods` ( `id` INT NOT NULL AUTO_INCREMENT, `uid` INT NOT NULL COMMENT '卖家id', `category_id` INT DEFAULT NULL, `title` VARCHAR(100) NOT NULL, `description` TEXT, `price` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `original_price` DECIMAL(10,2) DEFAULT NULL, `image` VARCHAR(200) DEFAULT NULL COMMENT '上传后的图片相对路径', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0在售,1已售,2下架', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_uid` (`uid`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='二手商品表'; CREATE TABLE `orders` ( `id` INT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '订单号:时间戳+随机数', `buyer_id` INT NOT NULL, `seller_id` INT NOT NULL, `goods_id` INT NOT NULL, `goods_title` VARCHAR(100) DEFAULT NULL COMMENT '下单时的商品标题快照', `price` DECIMAL(10,2) NOT NULL COMMENT '下单时的成交价', `status` TINYINT DEFAULT 0 COMMENT '0待确认,1已完成,2已取消', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'; CREATE TABLE `comment` ( `id` INT NOT NULL AUTO_INCREMENT, `goods_id` INT NOT NULL, `from_user` INT NOT NULL, `content` VARCHAR(255) NOT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_goods` (`goods_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品留言表';

这段脚本里有几个字段选择值得注意。价格用 DECIMAL(10,2) 而不是 float,是为了避免浮点精度问题;商品状态用 TINYINT 而不是字符串,查询时用 where status=0 效率更高;订单号带唯一索引,因为下单时经常要用它查状态。字符集统一用 utf8mb4,它能存 emoji 表情,普通 utf8 在个别同学粘贴特殊符号时会报字符集不匹配。

外键我只在商品表上加了一个 uid 引用,订单表不设外键。原因很现实:课程设计阶段经常要删数据重测,外键过多会导致删不掉用户、改不了分类。订单表里的 goods_title 和 price 是有意冗余的,查询订单时不用每次 join 商品表,而且商品后来被修改或删除,历史订单仍然保留当时的信息。

2.4 订单表里为什么冗余商品标题和价格

直观设计订单表只有 goods_id,看订单时 join 商品表拿到标题和价格。但二手交易场景里,商品卖出后可能被下架甚至删掉,如果卖家改动价格,历史订单显示也会跟着变。把 goods_title 和 price 直接存在订单表里,相当于给这笔交易拍了快照,无论商品表怎么改,订单永远是下单时的样子。

这是交易系统里很常见的“快照”设计,答辩时可以主动讲出来。购物车不建表也是类似的取舍:二手交易订单小而少,购物车用 session 里的 Map<Integer, Integer> 就够了,数量存在会话里,刷新不丢,关闭浏览器才清空。硬要建购物车表,等于多维护一套增删改查,功能收益却很小。

3. 本机部署:JDK/Tomcat/MySQL 版本搭配与三个必改配置

MySQL 安装教程到处都有,但真正卡住人的是版本组合。这一章给出的环境组合能跑通绝大多数 JSP 老项目,照着配就行,不要追新版本。

3.1 先定版本,再谈部署

我自己复现 JSP 项目一向是 JDK 1.8 + Tomcat 8.5 + MySQL 5.7,原因有两个:一是 Tomcat 8.5 还用 javax.servlet 包,和源码里的 import 完全兼容;二是 MySQL 5.7 对 JDBC URL 的要求比 8.0 简单,不需要 serverTimezone。如果本地已经装了 MySQL 8.0,不是不能用,但要把驱动类从 com.mysql.jdbc.Driver 换成 com.mysql.cj.jdbc.Driver,URL 里加时区参数,这个后面会列出来。

组件推荐版本理由
JDK1.8Tomcat 8.5 官方支持,Java 8 语法在源码里最常见
Tomcat8.5使用的还是 javax.servlet,与老 JSP 项目完全兼容
MySQL5.7驱动、URL 最简单,免去时区参数
mysql-connector-java5.1.49对应 MySQL 5.7;MySQL 8.0 用 8.x 驱动
IDEEclipse / IntelliJ IDEA都能跑,IDEA 注意 Web 根目录设置

这套环境跑校园二手交易这种多点登录、发布、下单的老项目绰绰有余。版本越新越容易碰到兼容性报错,Tomcat 10 就是最典型的例子,第五章会专门讲。先按这个组合跑通,再考虑升级任何东西。

3.2 数据库导入:一条命令和两处检查

把根目录下的 sql 脚本找到,一般在 sql/ 或 database/ 目录,文件名常见是 campus_shop.sql 或 campus.sql,内容不外乎建库、建表、插管理员。命令行导入最直接:

mysql --default-character-set=utf8mb4 -u root -p < campus_shop.sql

导入后进入 MySQL 做两件事:确认表出现、确认管理员存在。

mysql -u root -p
CREATE DATABASE IF NOT EXISTS campus_shop DEFAULT CHARACTER SET utf8mb4; USE campus_shop; SOURCE /完整路径/campus_shop.sql; SHOW TABLES; SELECT id, username, role FROM `user`;

SHOW TABLES 应该能看到 user、category、goods、orders、comment 这一批表;SELECT 结果里应该有一条 role=1 的管理员,没有的话后台登录入口进不去。数据库导入报 utf8 错,大多是 MySQL 客户端字符集和脚本不一致,加 --default-character-set=utf8mb4 重跑一遍基本能解决。密码字段如果能看到值,说明当前源码是明文存储,这是初始状态,不要因此认为项目坏了。

3.3 数据库连接配置:改一处还是改三处

很多项目把连接参数写在数据库工具类的 getConnection() 方法里,也有的抽成 db.properties。两种方式本质一样,关键是全局只有一份配置。改错地方是最常见的启动失败原因——你改了 A 文件,代码却在读 B 文件。先全局搜索 jdbc:mysql,确认落点在哪,再动手。

jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/campus_shop?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=你自己的密码

如果本地是 MySQL 8.0,改成下面这套:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/campus_shop?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=你自己的密码

参数含义要说清楚:useUnicode=true 和 characterEncoding=utf8 管中文,useSSL=false 是为了省掉 SSL 握手警告,serverTimezone=Asia/Shanghai 是 MySQL 8.0 驱动强制要求的,不加会报 CST 时区无法识别。驱动 jar 必须出现在 WEB-INF/lib 或 IDEA 的 Libraries 里,只把 jar 下载到桌面不算数。检查办法是展开项目依赖,能看到 mysql-connector-java-5.1.49.jar 才算挂上。

提示:如果项目里找不到 properties 文件,这些参数通常会直接写在数据库工具类里,改的时候保持同一份配置即可,不要同时改两个地方。

3.4 Tomcat 部署:war 包和 IDEA 两种跑法

部署要么把项目打成 war 丢进 Tomcat 的 webapps 目录,要么在 IDEA 里配置 Tomcat 运行。毕业设计建议用 IDEA 方式,调试时能看到报错堆栈,改动页面后不必手动重启。步骤是这样的:

  1. 打开项目,把 Project Structure 里的 Project SDK 设置为 1.8,Language level 设为 8。
  2. Project Structure → Artifacts → 加号 → Web Application: Exploded,名字保持 campus_shop。
  3. Run → Edit Configurations → 添加 Tomcat Server → Local,Tomcat 路径选本地 8.5 安装目录。
  4. Deployment 页签添加刚才的 Artifact,Application context 填 /campus_shop。
  5. 启动后访问 http://localhost:8080/campus_shop。

Application context 每次启动都会被 Tomcat 当作请求前缀,我习惯固定成 /campus_shop,不写 / 也不用带中文的目录名,否则后面涉及 Cookie 作用域或图片虚拟路径时会多出很多坑。如果用 war 包方式,把 IDEA 打好的 war 复制到 Tomcat/webapps,启动后会自动解压,访问路径就是 war 文件名。

war 包里不能套一层同名目录,否则会访问 /campus_shop/campus_shop/index.jsp 才能进首页。如果页面 404,先看 Tomcat 的 webapps 目录下解压出来的结构是否合理,再回 IDEA 检查 Artifact 输出。

3.5 页面全 404 时先查这三个地方

输入地址后页面直接 404,我按顺序查三处:第一,Tomcat 的 webapps 下是否有解压出来的项目目录,目录名和 context 是否一致;第二,IDEA 的 Artifact 输出目录是否包含 jsp 和 WEB-INF;第三,web.xml 里的 welcome-file 是不是 index.jsp,以及 index.jsp 是否真的在 webapp 根目录。

很多 JSP 项目默认欢迎页不是 index.jsp,而是 list.jsp 或 main.jsp,直接访问根路径 404 不代表项目坏了,改成访问具体页面即可。拿到源码先跑通,跑通之后每一处 404 都对应一个明确的配置问题。数据库配置不在这三个检查项里,如果页面能打开但数据加载失败,才需要回看 3.3 的连接参数。

4. 核心业务代码走读:登录、图片上传、购物车与订单事务

跑通环境只是开始,答辩和改需求都在代码里。这一章挑四个最核心的功能点走读:登录、发布商品、购物车、下单。看懂这四个点,整个系统的业务逻辑就通了。

4.1 登录:请求转发和重定向的分工

登录是所有功能的前提。JSP 项目最常见的做法是 LoginServlet 接收表单提交,查库,把用户对象放进 session。下面是简化后的核心代码,实际工程里会多几行非空校验,但逻辑一样。

@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) { req.setAttribute("msg", "用户名或密码不能为空"); req.getRequestDispatcher("login.jsp").forward(req, resp); return; } User loginUser = new UserDao().findByUsernameAndPassword(username, password); if (loginUser == null) { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("login.jsp").forward(req, resp); return; } HttpSession session = req.getSession(); session.setAttribute("loginUser", loginUser); session.setMaxInactiveInterval(30 * 60); // 30分钟无操作失效 resp.sendRedirect(req.getContextPath() + "/index.jsp"); } }

这段代码有三点值得看。第一,失败用 forward,成功用 sendRedirect:forward 可以把错误信息带回登录页,sendRedirect 会重新发起一次 GET,避免用户刷新页面时重复提交表单。第二,session 存的是 User 对象而不是用户名,因为发布商品页要显示昵称、后台要读 role 字段判断管理员,只存字符串后面还得再查一次库。第三,setMaxInactiveInterval(30 * 60) 把无操作超时设成 30 分钟,校园机房里这个时长刚好。

密码在这里是明文比对,源码初始状态大多如此。答辩时不要说这是安全设计,直接承认这是待改进点,然后按第六章的方法改成 MD5 加盐,反而能变成加分项。

4.2 发布商品:图片上传不能写死真实路径

商品发布是二手交易系统里最容易翻车的功能,难点在图片上传。JSP 表单一旦带上 enctype="multipart/form-data",request.getParameter 就拿不到普通字段了,必须用专门组件解析。这套源码大多用 Apache Commons FileUpload,核心逻辑如下:

@WebServlet("/publish") public class PublishServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { User user = (User) req.getSession().getAttribute("loginUser"); if (user == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } DiskFileItemFactory factory = new DiskFileItemFactory(); ServletFileUpload upload = new ServletFileUpload(factory); upload.setFileSizeMax(2 * 1024 * 1024); // 单张图片最大 2MB upload.setSizeMax(5 * 1024 * 1024); // 整个请求最大 5MB String title = null, priceText = null, categoryId = null, description = null, image = null; try { List<FileItem> items = upload.parseRequest(req); for (FileItem item : items) { if (item.isFormField()) { switch (item.getFieldName()) { case "title": title = item.getString("UTF-8"); break; case "price": priceText = item.getString("UTF-8"); break; case "categoryId": categoryId = item.getString("UTF-8"); break; case "description": description = item.getString("UTF-8"); break; } } else if (item.getName() != null && !item.getName().isEmpty()) { String ext = item.getName().contains(".") ? item.getName().substring(item.getName().lastIndexOf(".")) : ".jpg"; String imageName = UUID.randomUUID().toString().replace("-", "") + ext; String realPath = getServletContext().getRealPath("/upload"); File dir = new File(realPath); if (!dir.exists()) { dir.mkdirs(); } item.write(new File(dir, imageName)); image = "upload/" + imageName; } } new GoodDao().insertGood(user.getId(), title, description, new BigDecimal(priceText), Integer.parseInt(categoryId), image); resp.sendRedirect(req.getContextPath() + "/goods_list.jsp"); } catch (Exception e) { throw new ServletException("商品发布失败:" + e.getMessage(), e); } } }

这里的两个参数很关键:setFileSizeMax(2 * 1024 * 1024) 限制单张图片 2MB,setSizeMax(5 * 1024 * 1024) 限制整个请求 5MB,能挡掉恶意大文件。图片名用 UUID 拼原始扩展名,是为了避免同一目录下文件名冲突,否则两个人各传一张“手机照片.jpg”就会互相覆盖。getRealPath("/upload") 返回的是 webapp/upload 目录,本地调试没问题,但 war 部署时它指向 Tomcat 临时解压目录,重启可能被清掉,这个坑第五章会专门处理。

注意表单字段的取值全部用 item.getString("UTF-8"),因为 multipart 请求里 req.getParameter 拿不到值,而且默认编码可能不是 UTF-8。这里少写一个编码参数,中文标题就会乱码。

4.3 购物车用 Session:代码最短但最稳

二手交易不需要和电商比购物车复杂度。一份源码里如果给购物车建表存数据库,反而把问题放大:并发更新、脏数据、登录态失效全来了。课程设计阶段,购物车存 session 完全够,结构是一个 Map<商品ID, 数量>。

@WebServlet("/addCart") public class AddCartServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int goodsId = Integer.parseInt(req.getParameter("goodsId")); HttpSession session = req.getSession(); Map<Integer, Integer> cart = (Map<Integer, Integer>) session.getAttribute("cart"); if (cart == null) { cart = new LinkedHashMap<>(); session.setAttribute("cart", cart); } // merge:商品已存在则数量+1,不存在则放入并初始化为1 cart.merge(goodsId, 1, Integer::sum); resp.sendRedirect(req.getContextPath() + "/cart.jsp"); } }

cart.merge(goodsId, 1, Integer::sum) 是 Java 8 的写法:商品第一次加入时数量是 1,已存在时加 1。如果项目编译级别低于 Java 8,这里会编译失败,改成三行 if 判断效果一样。购物车页面要遍历这个 Map,根据 key 回查商品表,再把价格乘数量合计。这里特意用 LinkedHashMap 而不是 HashMap,因为 LinkedHashMap 保持插入顺序,页面展示时商品不会乱跳。

有人会问:购物车不落库,关闭浏览器就丢,行不行?对课程设计来说行。答辩时你甚至可以主动说“session 购物车是为了减少无意义写库,只有确认下单才强制落库”,这种取舍比为了炫技而建表更好讲。购物车本来就不是交易系统的核心数据,订单才是。

4.4 下单事务:两张表要一起成功

如果只是单表 insert,JDBC 写起来很简单。但二手交易系统下单一定会改两张表:orders 插一条记录,goods.status 从 0 改成 1。这两步之间如果程序崩溃,会出现订单有了但商品还挂着卖的情况。解决办法是把两步包进一个事务。

Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启手动事务 String orderNo = String.valueOf(System.currentTimeMillis()) + String.format("%03d", new Random().nextInt(1000)); PreparedStatement ps1 = conn.prepareStatement( "INSERT INTO orders(order_no, buyer_id, seller_id, goods_id, goods_title, price) " + "VALUES(?,?,?,?,?,?)"); ps1.setString(1, orderNo); // 其余参数按 buyerId、sellerId、goodsId、goodsTitle、price 依次设置 ps1.executeUpdate(); PreparedStatement ps2 = conn.prepareStatement( "UPDATE goods SET status=1 WHERE id=? AND status=0"); ps2.setInt(1, goodsId); if (ps2.executeUpdate() != 1) { throw new RuntimeException("商品已被他人购买"); } conn.commit(); } catch (Exception e) { if (conn != null) { conn.rollback(); // 订单和商品状态一起回滚 } throw new ServletException("下单失败:" + e.getMessage(), e); } finally { DBUtil.close(conn); }

setAutoCommit(false) 表示开始手动事务;两次更新之间,如果 UPDATE 影响行数不是 1,说明商品已经被别人买走,直接抛异常;catch 块里 rollback 会撤销刚才插入的订单,保证两个表状态一致;finally 里统一关连接。事务粒度只包住两个更新,不要把前面的查询也包进来,否则连接占用时间变长,Tomcat 默认连接池只有 20 个连接,并发一高就开始排队。

用 UPDATE ... WHERE id=? AND status=0 而不是直接 UPDATE goods SET status=1,是防超卖的关键:两条请求同时下单同一件商品时,只有第一条能把 status 从 0 改成 1,第二条 UPDATE 影响行数为 0,事务回滚,商品不会重复卖出。这个细节值得在答辩时单独讲一句。

5. 避坑:JSP+MySQL 项目最常见的五个翻车场景

下面这五个问题是我复现同类源码时踩过的高频坑,每条按“现象 → 原因 → 解决”写。提前看完,能省下大半天排错时间。

5.1 启动报错:Could not create connection to database server

现象:项目启动后访问任意页面,Tomcat 日志出现 Could not create connection to database server 或 Access denied for user 'root'@'localhost'。

原因:数据库连接参数和实际环境对不上。最常见的是 root 密码写错、数据库名拼错、驱动 jar 没在 classpath。很多源码自带的是作者机器上的密码,比如 123456,你本地是 abc123,直接启动必然拒绝访问。

解决:先不要翻代码,直接在命令行用配置里的账号密码连一次库:

mysql -u root -p

能连上,再改 db.properties 或数据库工具类;连不上,说明凭据就是错的。连上后把 jdbc.url 里的库名、username、driver 类名三行打印出来核对。如果报 ClassNotFoundException: com.mysql.jdbc.Driver,那是驱动 jar 没挂到项目里,和密码错误是两回事,别混着排查。

5.2 MySQL 8.0 下时区报错或连接被重置

现象:本地装的 MySQL 8.0,用源码自带的 5.x 配置启动,报 Communications link failure,或者 The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。

原因:MySQL 8.0 改了驱动类名和默认时区要求。老驱动 com.mysql.jdbc.Driver 在 8.0 中已被弃用,连接 URL 里缺少 serverTimezone 参数,服务端返回的时区字段无法被 Java 序列化,连接建立阶段就断了。

解决:驱动 jar 换成 mysql-connector-java-8.0.x,配置按第三章 3.3 的 MySQL 8.0 版本改,url 里加 serverTimezone=Asia/Shanghai。如果你只加 timezone 参数而不换驱动,会继续报驱动类不兼容。不要试图改 MySQL 的全局时区来将就代码,改连接参数比改服务器配置影响面小得多,而且换一台电脑复现时,别人不需要跟着改 MySQL。

5.3 Tomcat 10 下大量 500:ClassNotFoundException javax.servlet.http.HttpServlet

现象:源码在别人机器上跑得好好的,你装了个最新版 Tomcat 10,启动后所有 Servlet 路径全 500,日志提示 ClassNotFoundException: javax.servlet.http.HttpServlet。

原因:Tomcat 10 的 Servlet 包从 javax.* 迁到了 jakarta.*,老项目里 import javax.servlet 在编译和运行时都找不到类。这不是代码 bug,而是服务器版本和源码使用的 API 版本不匹配。

解决:把 Tomcat 换回 8.5 或 9.0。如果老师指定必须用 Tomcat 10,那就得全局把所有 javax.servlet 替换为 jakarta.servlet,工作量不大但容易漏,而且部分老 taglib 也要跟着变。对毕业设计来说,换 Tomcat 版本是最快的后悔药,不要和框架版本较劲。

5.4 中文全变问号:请求、响应、数据库三层都要查

现象:注册页填写中文昵称,提交后后台和数据库里全是 ??;或者 JSP 页面显示正常,但搜索中文商品搜不到。

原因:字符集在四个环节断了链。JSP 的 pageEncoding 没设 UTF-8、Servlet 里没调 request.setCharacterEncoding、连接 URL 少了 characterEncoding=utf8、数据库表用了 latin1。只要一个环节不对,中文就乱。

解决:按这条链检查:JSP 头部 contentType="text/html; charset=UTF-8",pageEncoding="UTF-8";每个 Servlet 处理 POST 请求第一行写 request.setCharacterEncoding("UTF-8");连接 URL 带 useUnicode=true&characterEncoding=utf8;数据库建表用 utf8mb4。GET 请求的乱码比较隐蔽,Tomcat 8 默认 URIEncoding 已经是 UTF-8,但老项目如果改过 server.xml,就得回头确认一遍。按完三步还乱码,别继续调代码,先重启 Tomcat,有可能是 IDE 用 GBK 保存过 JSP 文件,文件本身坏了。

5.5 上传图片重启后全部 404

现象:商品发布后图片能显示,重启 Tomcat 或重新打包 war 后,再访问 /upload/xxx.jpg 就 404,数据库里的 image 字段还指向上一次路径。

原因:getRealPath("/upload") 返回的是应用目录,开发环境下可能是 target/exploded,编译或重新部署时旧文件被清掉;war 包模式下每次启动都是从压缩包解压,上传的图根本不在包里,自然会被覆盖或找不到。

解决:图片不要放应用内,改成磁盘固定目录,比如 D:/campus_upload,再让 Tomcat 把 /upload/** 映射到这个目录。用 IDEA 就在 Deployment 里加 External Source;用手动部署就改 conf/server.xml 加一个 Context,docBase 指向 D:/campus_upload,path 指向 /campus_shop/upload。这样重新部署只影响代码,不影响用户传过的图。答辩前最好把图片目录和数据库脚本一起拷到 U 盘,避免换机器演示时连环 404。

6. 验证与改造:答辩前两小时能做完的检查清单和加分项

跑通只是第一步,答辩前两小时,我会按下面这张链路表走三遍。每一遍都用同一个新注册账号,不能拿管理员账号混过去。

6.1 完整业务链路验证清单

步骤操作预期结果
注册注册一个普通用户自动登录或跳转成功,user 表多一条记录
登录新账号登录首页右上角显示昵称
发布发布一件带图片的商品列表页出现该商品,图片可显示
搜索按关键字搜索商品列表被过滤
购物车加入两件商品购物车数量合计正确
下单提交订单订单表新增记录,商品状态变已售
后台管理员登录能看到用户和订单数据
重复下单再对同一商品下单给出“已售出”提示,没有新订单

这张表跑完,功能闭环就成立了。中途任何一步失败,顺着第五章的对应场景去查,不要开着一堆浏览器标签到处试。每次失败都要在 MySQL 里看一眼数据,确认是页面问题还是数据根本没落库。

6.2 三个低成本改造,性价比最高的三个点

第一个改 PreparedStatement。如果源码里还有字符串拼 SQL,把参数全部改成 ? 占位,这是安全类问题,答辩命中率最高。

String sql = "SELECT * FROM user WHERE username=? AND password=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password);

第二个改密码存储,用 MD5 加盐。这个改动只涉及注册和登录两个 Servlet,范围可控,但讲解价值很高。

String salt = UUID.randomUUID().toString().substring(0, 8); String digest = DigestUtils.md5Hex(salt + password); // 数据库存 salt 和 digest,登录时先查盐再计算哈希比对

第三个改图片虚拟路径,方法在第五章 5.5。把上传目录挪到应用外,既是稳定性的加分项,也能证明你理解 Web 容器对静态资源的管理。这三个改动合计不超过两小时,比新加一个整页模块划算得多。

从那以后,我每次拿到 JSP+MySQL 的源码,都会先花十分钟确认三件事:数据库脚本能原样跑通、Tomcat 不是 10、上传目录没有写在临时解压路径里。这套校园二手交易系统里最常见的坑,基本都写在上面了,按这个顺序复现,你打开源码后可以直接进入业务逻辑,而不是先被环境问题磨掉耐心。希望帮到你。

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

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

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

立即咨询