☰
JSP+Servlet网上书店系统:Java Web课程设计完整实现与二次开发
2026/10/20 18:12:13 网站建设 项目流程

简介:一套面向Web开发学习者的网上书店系统设计代码,以JSP技术为背景,演示B/S架构下在线图书查询、购买与后台管理功能。压缩包共60个文件,体积仅2.19MB,其中20个asp动态页面承载核心业务逻辑,21张jpg与8个gif提供页面视觉素材,3份doc文档包含设计说明与使用说明,另附mdb数据库及asa配置文件,便于还原完整运行环境。目前已有54人浏览学习。资源覆盖图书展示、分类检索、购物车管理、订单提交、用户注册与后台维护等模块,并配套可直接导入的数据库文件与说明文档,可帮助读者快速理解动态网站从前端页面、服务端处理到数据存储的完整链路。对于准备毕业设计或课程实训的开发者,还可从中借鉴B/S架构分层、会话状态维护、数据库连接与基础安全防护等实践思路。

1. 网上书店系统:课程设计里最经典的一套 Java Web 全流程

每年答辩季都能看到一批“网上书店系统”,但多数是只有增删改查的样板工程,图书列表写死、购物车存数据库、订单状态没法流转。这份 JSP 版网上书店系统设计代码不一样的地方在于,它把用户、图书、购物车、订单、后台管理五条链路完整接了起来,登录注册、图书分类检索、购物车结算、订单状态更新都能跑通,适合正在做 Java Web 课程设计或毕业设计的同学直接拿去二次开发。

项目整体采用 JSP + Servlet + JavaBean 的经典 Model 2 架构,数据层用 JDBC 操作 MySQL,服务器部署在 Tomcat 上。这类组合虽然不是当前企业级开发的主流,但恰恰是高校课程设计最看重的完整闭环——从前端页面到数据库表结构一条线走通,答辩时能讲清楚每一个请求的处理过程。如果你正在纠结课设题目,或者想找一个能快速跑起来又留有改造余地的 Java Web 项目,这份资源值得花一个晚上拆开看一遍。

2. 技术选型与目录结构:为什么选 JSP + Servlet 而不上框架

2.1 技术组合的合理性分析

选 JSP + Servlet 而不是 Spring Boot,很多同学会质疑“这都什么年代了还写 JSP”。但从课程设计的角度来看,这个组合恰恰是逻辑最透明的。Servlet 负责接收请求和控制转发,JSP 负责渲染页面,JavaBean 承担数据封装,每一个环节都能在代码里直接看到,不像 Spring Boot 那样被大量注解和自动配置包裹,反而说不清内部发生了什么。

具体到这份项目,前端展示层是 JSP 页面,控制层是一组 Servlet,业务层和数据访问层则是普通的 Java 类。没有引入 Spring、MyBatis 这类框架,数据库连接用的是 JDBC 原生 API,配合一个简单的连接工具类。这种做法的好处是依赖极少,部署包里只需要一个 WAR 文件和一个 MySQL 数据库,任何一台装了 JDK 和 Tomcat 的机器都能跑起来,对答辩演示来说非常友好。

我一般会建议同学保留这种原始结构作为课程设计的基线版本,等把请求流转和数据库操作彻底看懂之后,再考虑往 SSM 或 Spring Boot 方向迁移。直接上手框架容易陷入“配置能跑但不知道原理”的尴尬状态,而这份代码正好可以当做一个活的教材。

2.2 整体目录结构与包职责

解压源码后,标准的 Eclipse Dynamic Web Project 结构一目了然。前端页面放在 WebContent 目录下,Java 源码按照分层思想组织在 src 下的几个包中。下面是我整理后的目录树,和实际源码基本一致:

BookStore/ ├── src/ │ ├── com.bookstore.entity/ # 实体类 │ │ ├── User.java │ │ ├── Book.java │ │ ├── Category.java │ │ ├── CartItem.java │ │ ├── Order.java │ │ └── OrderItem.java │ ├── com.bookstore.dao/ # 数据访问层 │ │ ├── UserDAO.java │ │ ├── BookDAO.java │ │ ├── CategoryDAO.java │ │ └── OrderDAO.java │ ├── com.bookstore.service/ # 业务层 │ │ ├── UserService.java │ │ ├── BookService.java │ │ └── OrderService.java │ ├── com.bookstore.servlet/ # 控制器层 │ │ ├── LoginServlet.java │ │ ├── RegisterServlet.java │ │ ├── BookListServlet.java │ │ ├── CartServlet.java │ │ └── OrderServlet.java │ ├── com.bookstore.filter/ # 过滤器 │ │ └── EncodingFilter.java │ └── com.bookstore.util/ # 工具类 │ ├── DBUtil.java │ └── PageBean.java └── WebContent/ ├── index.jsp # 首页 ├── user/ # 用户相关页面 │ ├── login.jsp │ └── register.jsp ├── book/ # 图书展示页面 │ ├── list.jsp │ └── detail.jsp ├── cart/ │ └── cart.jsp ├── order/ │ ├── confirm.jsp │ └── list.jsp ├── admin/ # 后台管理页面 │ ├── book_manage.jsp │ └── order_manage.jsp ├── css/ ├── js/ └── WEB-INF/ ├── web.xml └── lib/

从目录结构上可以看出,这是典型的 Controller → Service → DAO 三层架构。Servlet 不直接操作数据库,而是先调用 Service,再由 Service 调 DAO。有些同学图省事在 Servlet 里写 JDBC,短项目看不出问题,一旦业务复杂起来,代码会迅速腐化到没法维护。这份代码在分层上守住了底线,值得学习。

2.3 几个关键配置文件的说明

WEB-INF/web.xml 里配置了 Servlet 的映射路径和过滤器。对比一下两种常见做法:有的项目用 @WebServlet 注解免去 web.xml 配置,有的项目坚持在 web.xml 里写映射。这份代码选择了注解方式,但 web.xml 仍然存在,主要用于配置欢迎页面和 EncodingFilter。如果你从 Eclipse 导入后需要调整路径,两处必须同时检查,这是第一个容易踩坑的地方。

web.xml 中的欢迎页面配置为 index.jsp,而 index.jsp 并不是静态页面,它通过 jsp:forward 转发到 BookListServlet,再由这个 Servlet 查询数据库后跳转到 book/list.jsp 展示图书列表。这种“JSP 做跳板、Servlet 做控制”的写法在课程设计里非常常见。核心配置代码类似这样:

<welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <filter> <filter-name>encodingFilter</filter-name> <filter-class>com.bookstore.filter.EncodingFilter</filter-class> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

EncodingFilter 继承自 Filter 接口,在 doFilter 方法里统一设置请求和响应的字符编码为 UTF-8,避免每个 Servlet 里重复设置。这在中文环境下尤其重要,后续避坑章节会详细展开。这个过滤器是整个项目里最不起眼但最关键的类之一。

3. 数据库设计与 DAO 层:从建表 SQL 到分页查询实现

3.1 表结构设计:六张表支撑完整业务

网上书店系统的表结构设计是这份代码的核心资产之一。设计合理的表结构能让后续的订单流转、库存管理都顺畅很多。项目里一共设计了六张数据表,分别是用户表、图书分类表、图书表、购物车条目表、订单表和订单明细表。其中购物车条目表存在数据库里,但实际购物车运行时是以 Session 形式存储的,这张表更多是为了保存用户“下次登录还要看到”的持久化购物车。

CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(50) NOT NULL, `email` varchar(100) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `category` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `book` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL, `author` varchar(50) DEFAULT NULL, `price` decimal(10,2) NOT NULL, `stock` int(11) NOT NULL DEFAULT 0, `category_id` int(11) DEFAULT NULL, `image` varchar(255) DEFAULT NULL, `description` text, `sales` int(11) DEFAULT 0, PRIMARY KEY (`id`), KEY `category_id` (`category_id`), CONSTRAINT `book_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `user_id` int(11) NOT NULL, `total_price` decimal(10,2) NOT NULL, `status` tinyint(4) DEFAULT 0, `create_time` datetime DEFAULT NULL, `pay_time` datetime DEFAULT NULL, `deliver_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `order_item` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_id` int(11) NOT NULL, `book_id` int(11) NOT NULL, `book_name` varchar(100) DEFAULT NULL, `price` decimal(10,2) NOT NULL, `quantity` int(11) NOT NULL, PRIMARY KEY (`id`), KEY `order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

订单表里通过 status 字段区分订单状态:0 表示未支付、1 表示已支付、2 表示已发货、3 表示已完成,4 可以表示已取消。用数字而不是字符串的好处是节省存储、判断效率高,缺点是可读性差一点,所以代码里通常会定义一个常量类或者用注释说明每个数字的含义。

订单明细表的设计值得注意。表里冗余了 book_name 和 price 字段,这样做的原因是订单生成后,即使图书被删除或改价,历史订单依然能正确显示当时购买的商品信息。这是电商系统设计里的常见做法——“快照”概念,在课程设计里能体现出设计者对数据一致性的思考,答辩时可以主动提一下。

3.2 DBUtil 与 PageBean:被忽略的基础工具类

数据库连接工具类 DBUtil 负责加载驱动和获取连接。代码里使用的是 Class.forName 加载驱动的方式,而不是通过连接池。这个设计的取舍在于简化项目部署,避免额外引入 c3p0 或 Druid 的 jar 包。实训环境的机器普遍配置不高,数据库并发量也不大,直接用 DriverManager 足够应付。

public class DBUtil { private static final String DRIVER = "com.mysql.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=utf-8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } 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(); if (ps != null) ps.close(); if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }

注意连接参数里显式声明了 characterEncoding=utf-8,这是保证数据库读写中文不乱码的第二个关键点,光有 EncodingFilter 还不够,数据库连接串也要配套。URL 中的 useSSL 参数在不同版本的 MySQL 下可能要求不同,MySQL 8.0 以上还需要额外加 serverTimezone 参数,运行环境配置章节会说明。

PageBean 分页工具类封装了当前页码、每页条数、总记录数、总页数四个属性,并提供了一个计算总页数的静态方法。分页查询时先统计满足条件的总记录数,再计算偏移量,最后用 LIMIT 语句取当前页数据。这套逻辑在多个 DAO 里重复使用,所以抽象成公共类是很自然的做法。

3.3 BookDAO 核心方法:带条件的分页查询

图书 DAO 是整个数据访问层里最复杂的一个类,因为它要同时处理分类筛选、关键字搜索和分页三个维度的条件。代码里使用了动态拼接 SQL 的方式,把所有查询条件组合进同一条 SQL 语句,然后在 PreparedStatement 上按顺序设置参数。

public List<Book> findByPage(Category category, String keyword, PageBean pageBean) { List<Book> list = new ArrayList<Book>(); StringBuilder sql = new StringBuilder("SELECT * FROM book WHERE 1=1 "); List<Object> params = new ArrayList<Object>(); if (category != null && category.getId() > 0) { sql.append("AND category_id = ? "); params.add(category.getId()); } if (keyword != null && !keyword.trim().equals("")) { sql.append("AND name LIKE ? "); params.add("%" + keyword + "%"); } // 分页参数 int pageNo = pageBean.getPageNo(); int pageSize = pageBean.getPageSize(); sql.append("LIMIT ?, ?"); params.add((pageNo - 1) * pageSize); params.add(pageSize); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); ps = conn.prepareStatement(sql.toString()); for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } rs = ps.executeQuery(); while (rs.next()) { Book book = new Book(); book.setId(rs.getInt("id")); book.setName(rs.getString("name")); book.setAuthor(rs.getString("author")); book.setPrice(rs.getDouble("price")); book.setStock(rs.getInt("stock")); book.setSales(rs.getInt("sales")); list.add(book); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(conn, ps, rs); } return list; }

这段代码里最关键的设计是 WHERE 1=1 这个“永远为真”的条件,配合后面的动态追加片段,省去了判断是否需要加 AND 的烦琐过程。第一次看到这种写法可能会觉得奇怪,但这是 Java Web 动态 SQL 最原始也最稳妥的写法,MyBatis 的 where 标签本质上就是对这种模式的封装。

Params 列表的使用也是这段代码的亮点。由于 SQL 是动态拼出来的,条件的个数不确定,直接把参数值按条件顺序收集到一个 List 里,最后统一循环设值。注意 JDBC 的参数索引从 1 开始,所以循环里用 i+1 作为 setObject 的第一个参数。LIMIT 的两个参数依次是偏移量和行数,页码从 1 开始时偏移量计算公式是 (pageNo-1)*pageSize。

配套的总记录数统计方法 count 的 SQL 结构几乎一样,只是把 SELECT 字段换成了 COUNT(*),同样需要处理 category 和 keyword 两个可选条件。你会发现两个方法里拼接条件的代码是重复的,这是早期 JDBC 时代无法避免的冗余。后续在二次开发章节里,我会介绍如何用 MyBatis 简化这套逻辑。

4. 核心业务链路:购物车与订单的状态流转实现

4.1 购物车的实现机制:Session 还是数据库表

这份项目的购物车没有在运行时查数据库,而是直接用 Session 里的 Map 对象存储。键是图书 ID,值是 CartItem 对象,CartItem 里封装了图书信息和购买数量。这么做的好处是购物车操作极快,不给数据库增加压力,坏处是用户关闭浏览器后购物车就丢了。

登录时将数据库里的购物车条目加载到 Session,结算时再将 Session 中的购物车写回数据库,这种做法兼顾了体验和持久化。我看过很多课程设计项目为了省事,购物车完全放在 Session 里,用户退出登录再进来购物车就空了,答辩时很容易被老师追问。这份代码的“Session 为主、数据库兜底”的策略是一个折中方案,既有演示效果又不增加复杂程度。

public class CartServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(); String action = request.getParameter("action"); Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<Integer, CartItem>(); session.setAttribute("cart", cart); } if ("add".equals(action)) { int bookId = Integer.parseInt(request.getParameter("bookId")); int quantity = Integer.parseInt(request.getParameter("quantity")); BookDAO dao = new BookDAO(); Book book = dao.findById(bookId); if (book != null) { CartItem item = cart.get(bookId); if (item == null) { item = new CartItem(); item.setBook(book); item.setQuantity(quantity); cart.put(bookId, item); } else { item.setQuantity(item.getQuantity() + quantity); } } response.sendRedirect("cart.jsp"); } else if ("update".equals(action)) { int bookId = Integer.parseInt(request.getParameter("bookId")); int quantity = Integer.parseInt(request.getParameter("quantity")); CartItem item = cart.get(bookId); if (item != null) { if (quantity <= 0) { cart.remove(bookId); } else { item.setQuantity(quantity); } } response.sendRedirect("cart.jsp"); } else if ("clear".equals(action)) { session.removeAttribute("cart"); response.sendRedirect("cart.jsp"); } } }

购物车商品加入购物车时,先从数据库查出完整的图书信息,再把数量和图书对象封装成 CartItem。如果 Session 里已有同一种图书,就用累加数量的方式合并,而不是新建条目。这个分支逻辑保证了反复点击“加入购物车”时购物车条目数量不会爆炸,用户体验比较自然。

更新数量的分支里有个细节:当用户把数量改成 0 或负数时,直接移除该条目。这是购物车改数量的通用逻辑,既避免出现“买了 0 本”的尴尬数据,也让 cart.jsp 页面的渲染逻辑更简单——只需要遍历 Map 的 values 即可。

购物车的值对象里还封装了一个 getSubtotal 方法,返回 price 乘以 quantity 的结果,JSP 页面里可以直接调用 item.getSubtotal() 来显示小计金额,而不是在页面里写 Java 运算表达式。把计算逻辑放进实体类是 JSP 页面保持整洁的常用技巧。

4.2 订单生成:事务保证一致性

订单生成是整份代码里最需要严谨逻辑的模块,因为它涉及多张表的数据变更。一次下单操作要完成三件事:往 orders 表插入一条订单记录、往 order_item 表插入若干条明细、扣减图书库存。这三步只要有任何一步失败,整体就必须回滚,否则会出现“订单生成成功但库存没扣”或者“库存扣了但订单不见了”的数据不一致问题。

public int createOrder(Order order, Map<Integer, CartItem> cart) { Connection conn = null; int orderId = 0; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 生成订单号并插入订单表 String orderNo = UUID.randomUUID().toString().replace("-", "").substring(0, 20); order.setOrderNo(orderNo); order.setStatus(0); order.setCreateTime(new Date()); String sqlOrder = "INSERT INTO orders(order_no, user_id, total_price, status, create_time) VALUES(?,?,?,?,?)"; PreparedStatement ps = conn.prepareStatement(sqlOrder, Statement.RETURN_GENERATED_KEYS); ps.setString(1, orderNo); ps.setInt(2, order.getUserId()); ps.setDouble(3, order.getTotalPrice()); ps.setInt(4, order.getStatus()); ps.setTimestamp(5, new java.sql.Timestamp(order.getCreateTime().getTime())); ps.executeUpdate(); ResultSet rs = ps.getGeneratedKeys(); if (rs.next()) { orderId = rs.getInt(1); } // 2. 批量插入订单明细 String sqlItem = "INSERT INTO order_item(order_id, book_id, book_name, price, quantity) VALUES(?,?,?,?,?)"; PreparedStatement psItem = conn.prepareStatement(sqlItem); for (CartItem item : cart.values()) { psItem.setInt(1, orderId); psItem.setInt(2, item.getBook().getId()); psItem.setString(3, item.getBook().getName()); psItem.setDouble(4, item.getBook().getPrice()); psItem.setInt(5, item.getQuantity()); psItem.addBatch(); } psItem.executeBatch(); // 3. 批量扣减库存 String sqlStock = "UPDATE book SET stock = stock - ?, sales = sales + ? WHERE id = ? AND stock >= ?"; PreparedStatement psStock = conn.prepareStatement(sqlStock); for (CartItem item : cart.values()) { psStock.setInt(1, item.getQuantity()); psStock.setInt(2, item.getQuantity()); psStock.setInt(3, item.getBook().getId()); psStock.setInt(4, item.getQuantity()); psStock.addBatch(); } int[] results = psStock.executeBatch(); for (int r : results) { if (r == 0) { throw new SQLException("库存不足,回滚事务"); } } conn.commit(); return orderId; } catch (Exception e) { e.printStackTrace(); try { if (conn != null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return 0; } finally { try { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }

开启事务的关键动作是 conn.setAutoCommit(false),这表示接下来的所有 SQL 都在同一个事务里执行,直到 commit 或 rollback。这里使用了 JDBC 的批量执行功能,把订单明细和库存更新分别组装成 Batch,显著减少数据库交互次数,对于 5~10 个商品的小订单来说性能提升非常明显。

扣减库存时在 UPDATE 语句里带了 stock >= ? 这个额外条件,这是防止超卖的重要手段。如果库存不够,这条 SQL 最终受影响的行数为 0,代码检测到后直接抛出异常触发回滚,从而保证“不允许买超过库存的数量”。

订单号没有用自增 ID,而是用 UUID 去掉横线后截取前 20 位。虽然可读性差一点,但不会暴露每日订单量,也比自增 ID 拼接日期的方式更不容易撞车。如果你的项目有更严格的订单号规则,可以在二次开发章节里替换为“日期 + 随机数”的方案。

4.3 订单状态流转:常量定义和状态推进

订单表里的 status 字段搭配了三个时间字段分别记录支付、发货等状态变化的时间点。项目里没有专门建枚举类,而是在 Order 实体类中定义了四个常量:

public static final int STATUS_UNPAID = 0; public static final int STATUS_PAID = 1; public static final int STATUS_DELIVERED = 2; public static final int STATUS_COMPLETED = 3;

后台管理页面通过 select 下拉框选择要推进的状态,提交后由 OrderServlet 调用 OrderService.updateStatus 方法更新数据库。每推进一个状态,对应的 create_time、pay_time、deliver_time 字段也会一并更新。这样用户在前台订单列表里能看到完整的订单时间线。

这里有一个值得注意的设计细节:状态推进没有做“只能从 0 到 1、再从 1 到 2”的强制校验,Servlet 里只做了简单的数字赋值。这意味着如果用户手动拼接 URL 调接口,可以把订单从“未支付”直接跳到“已完成”。在课程设计里这可能不算硬伤,但如果你想把项目完善成生产级,就需要加入状态机的合法性校验——二次开发章节会具体展开。

5. 本地运行:从导入到部署的完整步骤与避坑手册

5.1 环境准备与初始化数据库

运行这个项目需要 JDK 8 或以上版本、Tomcat 8 或以上版本、MySQL 5.7 或 8.0。开始之前建议先确认本机的端口占用情况,Tomcat 默认 8080 端口经常被其他程序抢占,如果启动日志报端口被占用,可以修改 conf/server.xml 里的 Connector 端口或用命令释放端口。

数据库初始化不需要用命令行一条条敲 SQL,直接把项目提供的 bookstore.sql 导入即可。导入完成后,需要打开 DBUtil.java 核对数据库连接的账号密码是否与本机一致。这是一个非常基础的检查点,但翻车率极高,我见过太多同学项目跑不起来都是因为密码没改。

mysql -u root -p < bookstore.sql

导入后建议立刻执行一条 SELECT 语句验证建表和初始化数据是否成功:

SELECT COUNT(*) FROM book;

因为项目里内置了分类数据和演示图书数据,正常情况会返回几条记录。如果返回 0,说明 SQL 文件里的 INSERT 语句没有完整执行,常见原因是字符集编码问题导致 SQL 解析失败,用命令指定编码导入即可解决。

5.2 Eclipse / IDEA 导入与部署配置

Eclipse 用户直接 File -> Import -> Existing Projects into Workspace 选择源码目录即可,导入后需要把 MySQL 驱动 jar 包添加到 Build Path。IDEA 用户略有不同,选择 Import Project 后,IDEA 会自动识别为 Web 项目,但要确认 Artifacts 里是否已经包含 lib 目录下的驱动包,没有的话需要手动添加到 WEB-INF/lib 或通过 Project Structure 加 Library。

配置 Tomcat 时需要注意 Deployment 标签页里的 Application context,默认值是 /,访问地址是 http://localhost:8080/。如果你设置了上下文路径为 /bookstore,那所有页面的链接和 Servlet 跳转路径都需要带上这个前缀。项目里的跳转大多使用相对路径或根路径,改上下文路径可能引发 404,建议初次运行保持默认。

启动顺序上,先启动 MySQL 服务再启动 Tomcat,因为 Tomcat 启动过程中如果触发了数据库连接,会直接抛异常。把项目部署到 Tomcat 的 webapps 目录后启动,在浏览器输入 http://localhost:8080/index.jsp 就能看到首页。

5.3 常见问题排查手册

中文乱码问题是这个项目里出现频率最高的坑,我会把完整的排查链路写在这里。

现象:登录注册后页面显示中文乱码,数据库里查出来的图书名称也乱码。 原因:JSP 页面编码、Servlet 请求编码、数据库连接参数或表结构编码四处不匹配,任何一处不对就会乱码。 解决:先检查数据库表结构是否为 utf8,再检查 DBUtil 的 URL 是否带 characterEncoding=utf-8,最后确认所有 JSP 文件开头都声明了 pageEncoding="UTF-8"。三处同时改完后,重启 Tomcat 即可。

现象:MySQL 8.0 环境启动项目后报错 Unable to load authentication plugin 'caching_sha2_password'。 原因:MySQL 8.0 默认的加密方式不同,导致驱动的兼容问题。 解决:把 DBUtil 中的驱动类从 com.mysql.jdbc.Driver 换成 com.mysql.cj.jdbc.Driver,并在 URL 中追加 serverTimezone=Asia/Shanghai。同时把 pom.xml 或 lib 下的驱动包升级到 mysql-connector-java 8.0 以上的版本。

现象:图书列表能打开,但点击第二页时 URL 变成 list.jsp?pageNo=2,导致搜索关键词和分类丢失。 原因:HTML 分页链接里没有携带当前搜索条件,后端在分页跳转时也没有重新接收这些参数。 解决:在 book/list.jsp 的分页链接中拼上 keyword 和 categoryId 参数,例如 list.jsp?pageNo=2&keyword=${keyword}&categoryId=${categoryId}。同时在 BookListServlet 中读取这些参数并回传给页面。

现象:部署到服务器后,Tomcat 日志里报 NoClassDefFoundError 指向 Mysql 驱动。 原因:常见的部署方式是直接把整个项目文件夹拷贝到 webapps 下,但 lib 目录里的驱动包没有被 Tomcat 加载。 解决:把 mysql 驱动 jar 包拷贝一份到 Tomcat 的 lib 目录下,或者将项目重新打成 WAR 包部署,驱动会自动包含在 WEB-INF/lib 中。

现象:下单时提示“库存不足”,但重新查询库存时发现明明是够的。 原因:代码里扣库存的条件是 stock >= quantity,但你把库存表里初始化的数值设置成了字符串类型,MySQL 在做比较时触发了类型转换,部分情况下行为不符合预期。 解决:确认 book 表中的 price 是 decimal 类型、stock 是 int 类型,不要用 varchar 存数字字段。

6. 二次开发:从课设代码到可以展示的作品

拿到这份代码的正确用法,不是原封不动交上去,而是根据自己的选题方向做适度改造。最基础的改法是换肤和增删功能,进阶的改法是重构架构。如果答辩时需要展示“技术亮点”,我建议优先从下面几个方向入手。

第一件建议做的事是替换前端框架。当前 JSP 页面用的还是传统的 CSS 样式,视觉效果比较朴素。可以考虑引入 Bootstrap 5 或 Layui 来重构登录、注册、图书列表和管理后台页面。JSP 页面里 Java 代码和 HTML 混写是历史遗留问题,但引入前端框架后整体观感会提升一个档次,也能掩盖部分 JSP 页面过时的问题。具体的做法是把公共的导航栏和页脚抽成 include 片段,再在 css 目录里覆盖样式变量。

第二件有技术含量的事是加登录状态拦截器。当前的过滤器只做了编码处理,没有拦截未登录用户直接访问购物车和订单页面。实现思路是写一个 LoginCheckFilter,在 doFilter 方法里判断 Session 中是否存在 user 对象,不存在就重定向到 login.jsp。注意拦截范围要排除登录、注册和图书浏览相关的 URL,不然用户还没登录就什么都不让看了,反而影响体验。

第三件推荐做的是订单状态的严格校验。前文提到的状态机校验问题,可以加一个简单的数组校验逻辑——将当前状态作为索引,期望的下一状态作为值,如果提交的状态不等于期望值则拒绝更新:

private static final int[] NEXT_STATUS = {1, 2, 3, -1}; // 0未支付 -> 1已支付 -> 2已发货 -> 3已完成,-1表示终态 public boolean checkStatusTransition(int currentStatus, int targetStatus) { if (currentStatus < 0 || currentStatus >= NEXT_STATUS.length) { return false; } return NEXT_STATUS[currentStatus] == targetStatus; }

这段代码把状态流转规则收敛到一个数组里,比散落的 if-else 更容易维护。再配合后台管理页面里下拉框只渲染“可流转到”的状态,基本可以杜绝脏状态的产生。

如果时间允许,最好把 Service 层里的两个核心方法补上单元测试。很多同学觉得课设不需要测试,但使用 JUnit 测试 OrderService 里的 createOrder 方法,可以让你在不打开浏览器的情况下快速验证事务回滚逻辑。表结构里外键约束都齐全,直接构造一个库存为 1 的图书测试“买 2 本”的场景,看事务是否会正常回滚。

从迁移角度来说,如果后续想把这份代码升级到 SSM 或 Spring Boot,重点是三个环节的替换:DBUtil 换成 MyBatis 的数据源和 Mapper、Servlet 换成 Controller、JSP 页面里的 Java 代码用 EL 和 JSTL 替代。表结构基本可以原样保留,实体类可以加上 JPA 注解直接复用。MyBatis 的动态 SQL 可以完美替代第 3 章里手动拼接 WHERE 1=1 的逻辑,改造后代码量会缩减 30% 左右。

最后说一个我自己的习惯性操作——每次拿到一套课设代码,第一件事不是直接跑起来,而是先打开数据库初始化 SQL 文件读一遍表结构,再打开核心 DAO 类扫一眼 SQL 语句。这套代码里订单表和订单明细表的设计,以及购物车 Session 与数据库双写策略,是我认为最值得抄下来的作业。从那以后我每次做电商类项目,订单的“快照字段”和“状态字段”设计都会强制沿用这套思路,省掉了不少返工的麻烦。希望这份网上书店系统的拆解能帮你在答辩前把项目真正吃透,而不是只停留在“能跑起来”的层面。

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

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

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

立即咨询