☰
JSP网上购书系统毕业设计实战:从三层架构到Tomcat部署全流程
2026/10/8 16:15:28 网站建设 项目流程

简介:一份基于JSP的网上购书系统毕业设计资源,面向计算机专业学生、毕业设计选题者以及想巩固Web开发技能的读者。项目以在线图书销售为场景,完整演示了JSP、Servlet、JavaBean与MVC模式的实际应用,涵盖用户注册登录、图书信息展示、购物车、订单提交等核心流程。压缩包约99.48MB,内含毕业设计报告、答辩PPT、源代码、数据库脚本、系统截图和部署辅导视频等,其中数据库部分设计了商品表、用户表、订单表、购物车表,并配有相应操作接口说明。已有722人学习浏览,既可以作为毕业设计完整参考方案,也可以按视频与文档完成环境部署、功能测试和二次开发,对理解Web应用分层结构与提升项目实践能力很有帮助。

1. 一整套JSP网上购书系统交付物,为什么值得你按这个顺序做

每年毕业季都有人拿到类似标题的压缩包:项目报告、答辩PPT、源代码、数据库、截图、部署视频,六样东西捆绑在一起交付。这其实是高校最常见的毕业设计形态——老师要的从来不只是「能跑的代码」,而是一套能证明你独立完成全流程的证据链。JSP技术栈虽然看起来老,但正因为老,参考资料充分、评估标准成熟、答辩时老师能问的问题也基本固定在数据库增删改查、会话管理、分页这几个方向,反而比盲目追新框架更容易顺利落地。这篇笔记按我实际做过的方式,把从数据库设计到部署视频录制的完整路径拆开讲清楚,给正在做这个题目的人一条可以直接照着走的路线。

2. 把JSP购书系统拆开看:JSP、Servlet、JDBC各管哪一段

2.1 三层架构怎么分:JSP页面、Servlet控制层与DAO的边界

网上购书系统虽然业务不复杂,但它天然适合拆成三层:JSP负责展示,Servlet负责接收请求和跳转,DAO负责操作数据库。很多应届生在简历上写「熟悉MVC」,真正答辩时却说不出自己项目里哪一段是M、哪一段是V、哪一段是C,根因就是一开始没把目录结构建对。

我一般会这样建包结构:

src/ com.bookstore.entity // 实体类:User、Book、Order、OrderItem com.bookstore.dao // 数据访问层:UserDAO、BookDAO、OrderDAO com.bookstore.service // 业务逻辑层:购物车计算、下单事务 com.bookstore.servlet // 控制器:LoginServlet、RegisterServlet、CartServlet com.bookstore.filter // 过滤器:字符编码、登录拦截 WebContent/ admin/ // 后台管理:图书增删改查、订单管理 css/ js/ images/ index.jsp // 图书列表入口 login.jsp register.jsp cart.jsp order.jsp WEB-INF/web.xml

逻辑说明:entity里的类与数据库表字段一一对应,避免在Servlet里直接操作ResultSet;DAO只负责SQL和参数绑定,不写业务判断;Servlet负责从request取参数、调DAO或Service、再根据结果转发到对应JSP页面。web.xml放在WEB-INF下,Tomcat启动时优先读它来加载Servlet映射和过滤器,这是JSP项目的地基,漏掉任何一个配置类都会出现404或500。

参数说明:目录名不强制固定,但「Entity、DAO、Servlet、JSP、WEB-INF」这五层缺一不可。很多坑其实都出在分层不彻底——比如有人在JSP里用scriptlet直接写JDBC代码,页面一多就乱成一锅粥,后期改需求时每改一次就要翻一遍所有页面。分层清晰的最大价值不是代码写得优雅,而是项目报告里能画出架构图,答辩时被问「你的项目怎么设计的」你能顺着包结构讲出三层调用关系。

2.2 数据库四张核心表:用户、图书、订单、购物车的DDL与关系

数据库设计是答辩时最容易被追问的部分。网上购书系统不需要设计得很花哨,但四张表的字段和关系必须自洽:用户表管登录者,图书表管商品,购物车表管加购状态,订单表和订单项表管下单记录。下面是我常用的MySQL建表脚本:

CREATE DATABASE bookstore_db DEFAULT CHARACTER SET utf8mb4; USE bookstore_db; CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL, `email` VARCHAR(100), `phone` VARCHAR(20), `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `book` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `title` VARCHAR(200) NOT NULL, `author` VARCHAR(100), `publisher` VARCHAR(100), `price` DECIMAL(10,2) NOT NULL, `stock` INT NOT NULL DEFAULT 0, `category` VARCHAR(50), `cover` VARCHAR(255), `description` TEXT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `cart` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `book_id` INT NOT NULL, `quantity` INT NOT NULL DEFAULT 1, UNIQUE KEY `uk_user_book` (`user_id`, `book_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `orders` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL UNIQUE, `user_id` INT NOT NULL, `total_price` DECIMAL(10,2) NOT NULL, `status` TINYINT DEFAULT 0, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `order_item` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `order_id` INT NOT NULL, `book_id` INT NOT NULL, `quantity` INT NOT NULL, `price` DECIMAL(10,2) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:uk_user_book这个唯一索引很关键,它保证同一个用户同一本书在购物车里只有一条记录,重复加购时用ON DUPLICATE KEY UPDATE quantity = quantity + 1即可,这是购物车模块最常见的数据库操作,也是「数据库增删改查」里最有技术含量的一次更新。

参数说明:DECIMAL(10,2)存价格避免浮点误差,TINYINT存订单状态(0待付款、1已付款、2已发货、3已完成),DATETIME用数据库默认时间戳。订单表和订单项表为什么拆两张?因为一个订单可能包含多本不同的书,如果只放一张表会出现大量重复的订单信息,第二张表用order_id外键关联它。记住一个原则:订单项表里只存下单时的快照价格,不实时去查图书表当前价格——否则图书改价后历史订单就全乱了。

2.3 为什么不换SSM或Spring Boot:先算清楚毕业设计的账

很多人在做之前会犹豫:既然Spring Boot这么流行,为什么还要写JSP?这个问题我建议你从两个角度算账。第一是时间账:毕业设计通常只有三个月左右,你还要写报告、做PPT、录视频。JSP+Servlet+JDBC的项目从零搭到能下单,熟练的话一周就能跑通,而Spring Boot光是理解starter、自动配置、依赖注入这一套概念就要花不少时间,而且SSM类项目答辩时老师更喜欢往深了问。第二是评估账:大部分指导老师看的是「功能完整、逻辑清楚、能答上问题」,JSP项目里每一个页面都能肉眼看到对应代码,工作量可视化程度极高。

下面这张对比表是我给学生的建议:

维度JSP+Servlet+JDBCSSM(Spring+SpringMVC+MyBatis)Spring Boot
上手速度快,一周可跑通主流程中,需要理解容器和AOP中上,配置少了但概念多
答辩提问深度基本固定在会话、分页、增删改查会追问IoC、AOP、Mapper代理会追问自动配置和微服务
源码可读性每个类职责单一,好讲分层多,讲清楚需要时间同样需要讲清楚自动配置
出坑后的救援资源论坛里十年前的帖子都能参考中等更新快但版本差异大
工作量呈现代码量大,页面对应关系直观代码量少,需要靠设计补代码量更少,需靠架构补

逻辑上要明白,毕业设计的核心目标是「可解释」,不是「炫技」。JSP项目里的每个请求从JSP页面到Servlet再到DAO,链路短、依赖少,老师问「这个数据是怎么查出来的」,你可以拿着代码一行一行讲清楚;换成Spring Boot,请求要过DispatcherServlet、HandlerMapping、参数解析器好几层,讲不明白反而显得掌握不扎实。

3. 从空项目到能下订单:环境配置与核心模块实现

3.1 环境准备:JDK、Tomcat、MySQL的版本组合与三条常用命令

我实际推荐的环境组合是JDK 1.8、Tomcat 8.5或9.x、MySQL 5.7或8.0,开发工具用Eclipse或IDEA都行。JDK 1.8与Tomcat 8.5的兼容性最稳,MySQL 5.7的认证协议对老驱动比较友好,但不建议为了避免问题去装老版本数据库——直接上MySQL 8.0也能跑,只是驱动要配套,这个坑放到第5章详细说。

环境装好后先用三条MySQL常用命令验证数据库可用,再决定进入代码环节:

# 登录本地MySQL,-u指定用户,-p后面会提示输入密码 mysql -u root -p # 执行SQL文件,把书店的建表脚本和测试数据导入 mysql -u root -p bookstore_db < /path/to/bookstore_db.sql # 查看表结构,确认导入是否完整 mysql -u root -p -e "USE bookstore_db; SHOW TABLES;"

逻辑说明:第一行进入交互式命令行,适合挨个执行语句;第二行用来导入完整的SQL脚本,这里建议平时就把建库、建表、插入测试数据写进同一个bookstore_db.sql文件,这样部署视频和项目报告里展示的就是同一份脚本,不会出现「截图里的表和答辩时的表对不上」的尴尬;第三行通过-e参数直接执行命令并返回结果,适合快速检查表数量。

参数说明:<不是SQL语法,是操作系统的输入重定向,告诉MySQL从这个文件读命令。导入时如果报错,优先看文件里的CREATE DATABASE语句是否带了IF NOT EXISTS,以及文件编码是不是UTF-8。端口默认3306,如果本机装了多个MySQL,可以在登录时加-P 3307切换端口。Tomcat默认端口是8080,启动命令在Tomcat安装目录的bin/startup.bat(Windows)或bin/startup.sh(Linux/macOS),启动后浏览器访问http://localhost:8080能看到Tomcat欢迎页,这一步过了再部署项目。

3.2 登录注册模块:用Session保存会话状态的Java实现

登录模块是答辩时必演示的功能,也是最容易讲清楚的模块。核心逻辑是:表单提交用户名和密码→Servlet验证→验证通过后把用户信息放进Session→跳转首页;Session可以通过getSession(false)判断用户是否已登录,从而在JSP页面上决定显示「你好,xxx」还是「请登录」。

下面是一个最简但完整的LoginServlet:

package com.bookstore.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import com.bookstore.dao.UserDAO; import com.bookstore.entity.User; @WebServlet("/login") public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; private UserDAO userDAO = new UserDAO(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); // 基础校验:空值和长度上限 if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) { response.sendRedirect("login.jsp?error=empty"); return; } User user = userDAO.findByUsernameAndPassword(username, password); if (user == null) { // 登录失败,回到登录页并带错误码 response.sendRedirect("login.jsp?error=badcred"); return; } // 登录成功:把用户对象放进Session HttpSession session = request.getSession(); session.setAttribute("loginUser", user); session.setMaxInactiveInterval(30 * 60); response.sendRedirect("index.jsp"); } }

逻辑说明:@WebServlet("/login")是Servlet 3.0之后提供的注解式映射,省去在web.xml里写一长串<servlet>和<servlet-mapping>配置。findByUsernameAndPassword是UserDAO里的查询方法,内部用PreparedStatement拼条件SQL,返回null就说明账号或密码不对。特别注意session.setMaxInactiveInterval(30 * 60)这行,它表示Session在30分钟内无操作会自动失效,这个参数在项目报告和答辩时可以主动讲出来,属于「加分细节」。

参数说明:redirect是重定向,浏览器地址栏会变成目标地址,getSession()不带参数时会自动创建一个新Session。千万不要把密码以明文的形式打印在日志里,也不要直接存成明文,虽然课程设计里很多人偷懒,但至少在答辩时能说出「生产环境应该用哈希加盐存储」这句话,老师对你的印象会完全不同。登录之后的「个人信息展示页面」用${sessionScope.loginUser.username}在JSP里取值即可,注意这是EL表达式,比<% out.println(...) %>这种scriptlet干净得多。

3.3 图书分页与购物车:查询参数怎么传、加购状态怎么放

图书列表如果一次性把几百本全查出来,页面会卡、SQL也会慢,所以分页是必考知识点。分页的核心是两个参数:当前页码page和每页条数size,SQL用LIMIT ? OFFSET ?实现。这里有一个至少一半人都会踩的坑:MySQL的LIMIT参数不能用字符串拼接,必须用PreparedStatement的setInt绑定,否则既存在SQL注入风险,又可能在某种字符集下报错。

购物车有两种实现路线:Session购物车和数据库购物车。课程设计我建议用Session存购物车,因为逻辑简单、不用频繁操作数据库,演示时也直观;但报告里要写清楚这两种方案的取舍,显示你思考过。Session购物车的实现思路是把Map<Integer, Integer>放进Session,key是图书ID,value是数量:

package com.bookstore.servlet; import java.io.IOException; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; @WebServlet("/cart") public class CartServlet extends HttpServlet { private static final long serialVersionUID = 1L; @SuppressWarnings("unchecked") protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); HttpSession session = request.getSession(); Map<Integer, Integer> cart = (Map<Integer, Integer>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<Integer, Integer>(); } String action = request.getParameter("action"); int bookId = Integer.parseInt(request.getParameter("bookId")); if ("add".equals(action)) { // 已存在则数量+1,否则新增 cart.put(bookId, cart.getOrDefault(bookId, 0) + 1); } else if ("remove".equals(action)) { cart.remove(bookId); } else if ("clear".equals(action)) { cart.clear(); } session.setAttribute("cart", cart); response.sendRedirect("cart.jsp"); } }

逻辑说明:getOrDefault是Java 8的Map默认方法,避免了「先查有没有再手动赋值」的啰嗦代码。每次操作完把cart重新放回Session,因为Session存的是对象引用,虽然不重新放也能通过引用看到改动,但显式setAttribute能让代码意图更清晰。购物车页面用sessionScope.cart遍历即可显示图书ID和数量,要显示书名和价格还得再查一次图书表,这是Session购物车的天然缺陷——需要把图书信息和数量合并展示。

参数说明:这里用Map<Integer, Integer>而不是List<CartItem>,是为了代码最简;如果想让购物车支持「用户未登录也能加购,登录后合并」,就要引入数据库购物车表。URL里通过action区分增删改查操作,是Servlet项目常见的传参方式。分页查询的完整DAO代码可以这样写:SELECT * FROM book ORDER BY id LIMIT ? OFFSET ?,第一页是LIMIT 10 OFFSET 0,第二页是LIMIT 10 OFFSET 10,算OFFSET用(page - 1) * size。总数用SELECT COUNT(*) FROM book查一次即可,避免每次都全表扫描。

3.4 下单与库存扣减:JDBC事务和防止超卖的条件更新

下单涉及两个表:orders和order_item,还要扣减图书库存。整个过程必须在一个事务里完成,因为任何一个步骤失败,都不能出现「订单生成了但库存没扣」或「库存扣了但订单没生成」这种数据不一致。JDBC的事务控制其实很简单:Connection.setAutoCommit(false)之后手动commit()和rollback()。

下面这段代码是下单模块的核心,我特意把库存扣减的SQL写成了条件更新,这是防止超卖的关键:

public boolean createOrder(int userId, Map<Integer, Integer> cart) throws Exception { Connection conn = null; PreparedStatement ps = null; boolean success = false; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 生成订单号并插入订单主表 String orderNo = "B" + System.currentTimeMillis(); ps = conn.prepareStatement( "INSERT INTO orders(order_no, user_id, total_price) VALUES(?,?,?)"); ps.setString(1, orderNo); ps.setInt(2, userId); ps.setBigDecimal(3, computeTotal(cart)); ps.executeUpdate(); int orderId = DBUtil.getGeneratedKey(ps); // 拿到自增ID // 2. 逐条插入订单项,同时扣减库存 for (Map.Entry<Integer, Integer> e : cart.entrySet()) { int bookId = e.getKey(); int qty = e.getValue(); // 条件更新:库存足够才扣减,影响行数为0说明库存不足 ps = conn.prepareStatement( "UPDATE book SET stock = stock - ? " + "WHERE id = ? AND stock >= ?"); ps.setInt(1, qty); ps.setInt(2, bookId); ps.setInt(3, qty); int rows = ps.executeUpdate(); if (rows == 0) { throw new RuntimeException("库存不足: " + bookId); } ps = conn.prepareStatement( "INSERT INTO order_item(order_id, book_id, quantity, price) " + "VALUES(?,?,?,?)"); ps.setInt(1, orderId); ps.setInt(2, bookId); ps.setInt(3, qty); ps.setBigDecimal(4, getBookPrice(bookId)); ps.executeUpdate(); } conn.commit(); // 全部成功才提交 success = true; } catch (Exception ex) { if (conn != null) { conn.rollback(); // 任何一步失败,整体回滚 } throw ex; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } return success; }

逻辑说明:关键在UPDATE book SET stock = stock - ? WHERE id = ? AND stock >= ?这一句。stock >= ?这个条件让数据库在更新时原子地校验库存,两个并发请求同时下单时,只有一条更新能成功,另一条影响行数为0并抛异常,从而避免了先查库存再扣减造成的超卖。这就是「数据库并发锁」思维在课程设计里的最小实践,答辩时把它讲出来,含金量会明显不一样。

参数说明:setAutoCommit(false)之后所有SQL都在同一个事务里,commit()之前任何一个异常都会触发rollback(),回滚到事务开始前的状态。DBUtil.getGeneratedKey(ps)是获取自增主键的封装,底层调ps.getGeneratedKeys(),没有这行代码后面订单项就不知道挂在哪个订单下。使用PreparedStatement的?参数绑定,既是防SQL注入的标准做法,也是让数据库能复用执行计划的关键。注意事务里不要做耗时太长的外部调用,否则会放大锁的持有时间,在高并发下可能引发数据库死锁问题;提升并发安全性的常见做法是对事务内的多表操作保持固定的SQL执行顺序,能有效降低死锁概率。

4. 把代码翻译成项目报告和答辩PPT:工作量如何可视化

4.1 项目报告的结构:从需求分析到测试,每一章对应哪段代码

代码写完只是第一步,项目报告才是决定你能不能顺利过审的关键。我见过太多人代码写得不错,报告却把「系统采用了B/S架构」「数据库选用了MySQL」翻来覆去写三遍,老师看到这类空话连篇的报告,还没翻到测试就没了耐心。报告的核心任务是把代码里的决策过程翻译成文字,每一节都要能对应到具体类、具体表或具体页面。

下面是一份符合主流要求的章节结构对照表:

报告章节该写什么对应源码/资源
需求分析用户角色(普通用户、管理员)、功能需求(注册、登录、浏览、加购、下单、订单管理)、非功能需求(并发、安全)用例描述、功能模块图
系统设计三层架构图、请求处理流程、技术选型理由包结构、Servlet映射
数据库设计ER图、表结构说明、字段含义、表关系bookstore_db.sql
详细设计核心模块的流程图或时序图、关键代码片段LoginServlet、CartServlet、订单事务
系统测试测试环境、测试用例表、测试结果、缺陷修复记录运行截图、测试数据
总结完成的工作、遇到的问题及解决过程对应第5章的避坑记录

逻辑说明:测试章节是拉开分差的地方。不要只写「经测试系统运行正常」一句带过,而是列一个测试用例表,每条记录「功能模块、操作步骤、预期结果、实际结果、是否通过」。比如「重复用户名注册」的预期结果是「提示用户名已存在且不跳转首页」,实际结果一致就标记通过。测试数据要真实,能在部署视频里复现,千万别在报告里写「测试10次全部通过」但没有一次演示能对上。

参数说明:ER图不要直接用数据库工具导出的那种密密麻麻的图,建议用Draw.io或Visio重画,每张实体表只要字段名+主键即可,重点画清楚cart.user_id、orders.user_id、order_item.order_id、order_item.book_id四条外键关系。数据库设计部分一定要写明「为什么订单项表要冗余存一份price」这类决策,老师看到一个字段就能展开问,这是展示你思考深度的机会。

4.2 答辩PPT的讲述顺序:ER图、时序图、演示截图怎么排

答辩PPT页数控制在12到15页,讲的时间只有5到8分钟,每页只能放一个核心信息。PPT最忌照搬报告标题,最理想的顺序是「先用一张业务流程图让老师知道系统干什么,再直接现场演示,最后用ER图和时序图解释技术实现」。

具体页面顺序我建议这样排:

  1. 封面:题目、姓名、学号、指导老师。
  2. 选题背景:两三句话说明网上购书系统解决什么问题,别写「随着互联网的发展」这类空话,直接说「线下购书效率低,需要一个在线浏览和下单的平台」。
  3. 功能结构图:一张图展示用户端和管理员端的功能树,这是需求分析可视化的核心。
  4. 技术架构图:三层架构的方块图,标注JSP、Servlet、JDBC、MySQL的位置。
  5. 现场演示:这里放录屏或直接切到浏览器,演示注册登录、图书分页浏览、加购、下单、后台管理五个主流程。
  6. 数据库设计:放重画过的ER图,讲三句话——「有哪几张表」「表之间怎么关联」「为什么这样设计」。
  7. 核心实现:放订单事务那段代码截图,讲清楚setAutoCommit(false)和条件更新的作用。
  8. 测试结果:放测试用例表截图,讲一个发现的Bug和修复过程。

逻辑说明:第5页推荐放一段提前录好的操作视频而不是现场连数据库演示,是因为答辩现场的无线网络和投影设备经常出幺蛾子,提前录一个1分钟的高清视频,重点操作配上放大镜效果,比现场手忙脚乱切换窗口可靠得多。现场演示容易翻车的主要原因是浏览器缓存了旧的Session,导致登录状态混乱,提前用无痕窗口打开系统能规避绝大多数演示事故。

参数说明:PPT里放代码时最多放10行,并用手画框圈出关键行,比如conn.setAutoCommit(false)和WHERE id = ? AND stock >= ?。不要整段代码截图往PPT上一贴,字太小老师看不到,字号小于16pt的代码在投影上是阅读灾难。演示视频的分辨率用1920x1080,帧率30即可,视频长度控制在2分钟以内,重点操作可以适当放慢速度,让老师看清你点了哪个按钮、页面发生了什么变化。

5. 部署、录视频与避坑:从本机跑通到答辩前的最后一步

5.1 部署视频录制顺序:从启动Tomcat到下单成功的六步流程

部署视频的目的是向老师证明「这个系统在我手里是能独立跑起来的」,所以视频里要展示完整的启动链路,而不是只录浏览器操作。我建议按下面这个顺序来录,每一步都放个2到3秒的静止画面让操作可被看清:

第1步:启动MySQL服务,命令行登录mysql,执行 source 导入bookstore_db.sql 第2步:打开Eclipse/IDEA,展示项目目录结构,展开src和WebContent 第3步:配置Tomcat,添加项目到Server,点击运行按钮 第4步:打开浏览器无痕窗口,访问 http://localhost:8080/bookstore/index.jsp 第5步:注册新账号 -> 登录 -> 浏览图书 -> 添加购物车 -> 提交订单 第6步:打开mysql命令行,查询orders表和order_item表,证明数据落库

逻辑说明:第1步和第6步是很多人容易漏掉的。第1步证明数据库是真的导入而不是用了内置数据,第6步证明下单操作真的写进了数据库,这两步恰好呼应了报告里的「数据库设计」和「系统测试」章节。录屏工具用OBS或Windows自带的录屏都行,Xbox Game Bar按Win+G就能录制,但注意它默认不会录制桌面图标之外的区域,需要手动框选浏览器窗口范围。

参数说明:视频总时长控制在5到10分钟,超过10分钟老师会失去耐心。用OBS时把比特率设置在2500Kbps以上,否则文字会糊;视频里不需要配音,但可以在关键步骤添加文字标注。如果视频中途操作失误,不要剪辑拼凑,直接重录,拼接痕迹太明显容易被质疑。浏览器一定要用无痕窗口,否则上一个Session会干扰演示结果,这种细节反而能让老师觉得你考虑周全。

5.2 常见问题排查:启动失败、数据库连接失败、500错误的定位方法

这套系统跑不起来,90%的问题集中在三个方面:Tomcat启动失败、数据库连不上、页面报500。别急着面向搜索引擎编程,先按下面的顺序手动定位,这也是答辩老师最欣赏的排查思路。

Tomcat启动失败的常见现象是双击startup.bat后窗口一闪而过。此时不要只看一闪而过的窗口,去Tomcat的logs/catalina.out(实际按日期命名的日志,比如catalina.2025-04-01.log)里看最后几行异常。最常见的单行异常是Port 8080 required by Tomcat v9.0 Server at localhost is already in use,另外的典型情况是环境变量JAVA_HOME没配好,用命令行执行echo %JAVA_HOME%验证,没输出就是环境变量有问题。

数据库连接失败分成两类。一类是ClassNotFoundException: com.mysql.jdbc.Driver,原因是驱动jar包没放进WEB-INF/lib目录,或者放的版本不对;另一类是Communications link failure并带着serverTimezone字样,说明URL里的时区参数缺失,MySQL 8.0的驱动要求连接串写成jdbc:mysql://localhost:3306/bookstore_db?useSSL=false&serverTimezone=Asia/Shanghai。

页面报500时,别急着甩锅给代码。先看浏览器地址,是/index.jsp报错还是/login报错,再去IDE的控制台看堆栈。500的常见源头有三类:

  • 空指针异常:Session里取不到属性,检查request.getSession(false)的写法。
  • SQL语法异常:报错信息里会带出具体SQL,复制到navicat里单独执行一下,八成是字段名或表名拼错。
  • 类型转换异常:Integer.parseInt接了半天用户输入的非数字,比如购物车数量传成了空字符串。

定位方法讲完后,有一个「玄学」问题值得单独提醒:明明代码没改,昨天能跑今天报404。这种大概率有三种原因——项目没重新部署到Tomcat的webapps目录、浏览器缓存了旧页面、或者Tomcat里挂了多个项目导致上下文路径冲突。清理Tomcat的work目录再重启服务,能解决一大半此类问题。

5.3 避坑记录:四个典型「翻车」场景的现象、原因与解决

这部分内容是我做同类项目反复遇到、也看学生经常重蹈覆辙的地方,每条都按「现象→原因→解决」的顺序写,可以直接对应到报告里的「问题与解决」章节,答辩时主动讲出来反而是加分项。

第一个坑是驱动版本与MySQL认证协议不兼容。现象是代码没变,环境从MySQL 5.7换到8.0后连接报Unable to load authentication plugin 'caching_sha2_password',或者说不支持这个认证插件。原因是MySQL 8.0默认的认证插件换了,老版本的mysql-connector-java 5.x不认识。解决方式是把驱动jar换成mysql-connector-java 8.0.x,同时连接串里加上useSSL=false&serverTimezone=Asia/Shanghai,Class.forName的类名也从com.mysql.jdbc.Driver改成com.mysql.cj.jdbc.Driver。

第二个坑是中文乱码。现象是注册时填的「张三」保存到数据库后变成「???」,或者页面上显示乱码。原因是Tomcat默认解码用ISO-8859-1,而页面的字符编码是UTF-8,两边对不上。解决方式不是把每个Servlet里都乱加setCharacterEncoding,而是在web.xml里配置一个Filter,并且注意这个Filter要放在所有Servlet映射之前,在doFilter里先调用request.setCharacterEncoding("UTF-8")再执行后面的链。这个顺序极重要:如果先执行了getParameter再调setCharacterEncoding,参数已经被ISO-8859-1解码过一次,之后再怎么设置都救不回来,属于典型的「后悔药无效」场景。

第三个坑是并发下单导致库存超卖。现象是数据库里stock字段出现负数,或者两个人同时买同一本书都显示成功但库存对不上。原因是下单逻辑里先SELECT stock检查再UPDATE stock,两步之间存在时间差,两个请求都读到库存还剩1本,各自扣减后变成-1。解决方式就是第3.4节里用的条件更新,把检查合并进UPDATE的WHERE条件里,让数据库在更新时原子地判断库存是否充足。理解这个方案的关键点在于:数据库的行锁在UPDATE执行时生效,条件不满足时影响行数为0,通过rows == 0就能发现冲突并回滚。

第四个坑是Tomcat端口被占用导致闪退。现象是双击启动脚本后窗口秒退,或者IDE里启动报Port 8080 already in use。原因是之前有残留的Tomcat进程占着8080端口,或者本机装了其他Web服务。解决方式是在命令行执行netstat -ano | findstr 8080,从最后一列拿到占用进程的PID,再去任务管理器结束该进程;如果不想清理进程,直接把conf/server.xml里的8080改成8090也行,但要记得之后访问URL也改成新端口,否则会一直出现连接被拒绝的假象。这个问题在实验室电脑上尤其常见,因为前一届学生留下的项目可能还挂载在同一个Tomcat上。

6. 让这个购书系统能写进简历:三个低成本升级方向与验证方法

6.1 用Filter统一处理登录拦截和字符编码,把「半成品」变成「工程品」

基础版项目跑通后,加一个Filter的成本不到五十行代码,却能同时在两个维度提升项目完成度。Filter的核心思想是「请求在处理之前先过一道拦截」,把登录校验和编码处理从每个Servlet里抽出来集中管理。登录拦截的典型场景是:未登录用户直接访问/cart或/order时,强制跳转回login.jsp。

package com.bookstore.filter; import java.io.IOException; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.*; @WebFilter("/*") public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); String uri = request.getRequestURI(); // 放行登录、注册、静态资源和首页,其它请求必须登录 if (uri.endsWith("login.jsp") || uri.endsWith("register.jsp") || uri.endsWith(".css") || uri.endsWith(".js") || uri.endsWith(".jpg") || uri.endsWith(".png") || uri.contains("/login") || uri.contains("/register") || uri.endsWith("index.jsp")) { chain.doFilter(req, res); return; } HttpSession session = request.getSession(false); if (session == null || session.getAttribute("loginUser") == null) { response.sendRedirect("login.jsp?error=needlogin"); return; } chain.doFilter(req, res); } }

逻辑说明:@WebFilter("/*")表示拦截所有请求,白名单通过uri.endsWith判断。这个Filter同时承担了字符编码和登录拦截两件事,在答辩时可以把它讲成「统一入口治理」——听起来比零散地到处加编码设置专业得多。注意request.getSession(false)与getSession()的区别:不传参数时如果Session不存在会新建一个,传false则返回null,这样才能准确判断「是否真的登录过」。

参数说明:白名单的匹配逻辑是弱点,你可能会漏掉某个JSP导致登录后仍然跳转。更稳妥的方式是在配置类里维护一个Set<String>白名单集合,把不需要登录就能访问的路径全放进去。升级到SSM或Spring Boot后再看这套思路,发现它就是Spring拦截器的前身,这一点写进简历能衔接后续技术栈。

6.2 验证方法与收尾:JUnit测DAO、JMeter看并发、Git管源代码

项目做完了,不能只靠「点一遍没报错」来证明它可靠。最低成本的验证是给DAO层写几个JUnit测试:UserDAO的增删改查、BookDAO的分页查询、OrderDAO在库存不足时的回滚行为。这些测试不需要启动Tomcat,直接跑maven或Eclipse里的JUnit即可,测试通过后截一张绿色进度条,放进报告测试章节。并发层面可以用JMeter开10个线程同时下单同一本书,看数据库库存是否仍然正确,这是对第3.4节事务代码最直接的验证,也是「数据库并发锁」在真实环境中的体检。

整个项目从第一天起就用Git做源代码管理,每次大模块跑通打一个commit,答辩前打一个版本标签,这样即使改坏了也有后悔药。交付时把.gitignore写好,排除掉target、.idea和*.iml这类编译产物,只保留源码、SQL脚本和README。最后我的习惯是重新拿一份全新的Tomcat和MySQL,按第5.1节的六步从头录一遍部署视频,这一步能暴露环境依赖问题——很多项目在开发机上跑得飞起,换台电脑立刻原形毕露。做这套毕业设计最大的教训就是:永远不要用开发环境的状态代替交付环境的状态。希望这份从数据库设计到答辩验证的完整路径能帮到你,少走一段弯路。

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

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

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

立即咨询