简介一套面向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-fileindex.jsp/welcome-file /welcome-file-list filter filter-nameencodingFilter/filter-name filter-classcom.bookstore.filter.EncodingFilter/filter-class /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingEncodingFilter 继承自 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) ) ENGINEInnoDB DEFAULT CHARSETutf8; CREATE TABLE category ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8;订单表里通过 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?useUnicodetruecharacterEncodingutf-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(); } } }注意连接参数里显式声明了 characterEncodingutf-8这是保证数据库读写中文不乱码的第二个关键点光有 EncodingFilter 还不够数据库连接串也要配套。URL 中的 useSSL 参数在不同版本的 MySQL 下可能要求不同MySQL 8.0 以上还需要额外加 serverTimezone 参数运行环境配置章节会说明。PageBean 分页工具类封装了当前页码、每页条数、总记录数、总页数四个属性并提供了一个计算总页数的静态方法。分页查询时先统计满足条件的总记录数再计算偏移量最后用 LIMIT 语句取当前页数据。这套逻辑在多个 DAO 里重复使用所以抽象成公共类是很自然的做法。3.3 BookDAO 核心方法带条件的分页查询图书 DAO 是整个数据访问层里最复杂的一个类因为它要同时处理分类筛选、关键字搜索和分页三个维度的条件。代码里使用了动态拼接 SQL 的方式把所有查询条件组合进同一条 SQL 语句然后在 PreparedStatement 上按顺序设置参数。public ListBook findByPage(Category category, String keyword, PageBean pageBean) { ListBook list new ArrayListBook(); StringBuilder sql new StringBuilder(SELECT * FROM book WHERE 11 ); ListObject params new ArrayListObject(); 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 11 这个“永远为真”的条件配合后面的动态追加片段省去了判断是否需要加 AND 的烦琐过程。第一次看到这种写法可能会觉得奇怪但这是 Java Web 动态 SQL 最原始也最稳妥的写法MyBatis 的 where 标签本质上就是对这种模式的封装。Params 列表的使用也是这段代码的亮点。由于 SQL 是动态拼出来的条件的个数不确定直接把参数值按条件顺序收集到一个 List 里最后统一循环设值。注意 JDBC 的参数索引从 1 开始所以循环里用 i1 作为 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); MapInteger, CartItem cart (MapInteger, CartItem) session.getAttribute(cart); if (cart null) { cart new HashMapInteger, 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, MapInteger, 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显著减少数据库交互次数对于 510 个商品的小订单来说性能提升非常明显。扣减库存时在 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 是否带 characterEncodingutf-8最后确认所有 JSP 文件开头都声明了 pageEncodingUTF-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 中追加 serverTimezoneAsia/Shanghai。同时把 pom.xml 或 lib 下的驱动包升级到 mysql-connector-java 8.0 以上的版本。现象图书列表能打开但点击第二页时 URL 变成 list.jsp?pageNo2导致搜索关键词和分类丢失。 原因HTML 分页链接里没有携带当前搜索条件后端在分页跳转时也没有重新接收这些参数。 解决在 book/list.jsp 的分页链接中拼上 keyword 和 categoryId 参数例如 list.jsp?pageNo2keyword${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 11 的逻辑改造后代码量会缩减 30% 左右。最后说一个我自己的习惯性操作——每次拿到一套课设代码第一件事不是直接跑起来而是先打开数据库初始化 SQL 文件读一遍表结构再打开核心 DAO 类扫一眼 SQL 语句。这套代码里订单表和订单明细表的设计以及购物车 Session 与数据库双写策略是我认为最值得抄下来的作业。从那以后我每次做电商类项目订单的“快照字段”和“状态字段”设计都会强制沿用这套思路省掉了不少返工的麻烦。希望这份网上书店系统的拆解能帮你在答辩前把项目真正吃透而不是只停留在“能跑起来”的层面。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?