简介:这是一套面向高校计算机相关专业学生的JSP课程设计期末大作业资源,基于JSP(MVC模式)与MySQL实现网上图书购物系统,适合JavaWeb初学者、需要完成课程设计或期末大作业的同学参考与直接使用。压缩包共75个文件,约47.77MB,包含14个jsp页面、12个java源文件与12个class编译文件、3个css样式、3个xml配置、1个sql数据库脚本,以及项目展示视频、说明文档和图片素材,覆盖前端页面、后端控制、数据库与部署配置各环节。代码注释详尽,新手也能读懂,下载后简单部署即可运行。系统功能完善、界面美观、操作便捷,具备图书浏览、购物车、订单管理等完整流程,具有较高的实际应用价值。目前已有347人学习下载,可作为课程设计、期末大作业的完整参考方案,帮助读者快速理解MVC分层结构、JSP与MySQL交互方式及项目部署思路。
1. 从一份 JSP 课设说起:图书购物网站到底要交付什么
每年期末,总有一批人卡在同一个地方:题目写着“基于 JSP(MVC 模式)和 MySQL 的网上图书购物系统”,听起来像是把几个热搜词拼在一起,真动手才发现要交付的东西比想象中具体得多。它不是一个“能跑就行”的 demo,而是一套完整的 JavaWeb 项目:有前台图书浏览、购物车、下单,有后台图书管理和订单处理,有数据库表结构和初始数据,还要能打包成 war 丢进 Tomcat 跑起来。换句话说,这是一次把 MVC 三层架构、JSP 页面、Servlet 控制器、JDBC 数据访问和 MySQL 数据库串成一条线的完整训练。
这篇文章面向两类人:一类是正在做 JSP 课程设计、需要一套能照着复现的完整案例;另一类是想回头补 JavaWeb 基础、把“传统 JSP 项目怎么打包 war”“MVC 三层架构到底怎么分层”这些模糊概念落到代码上的从业者。我会按“先讲清结构,再动手搭环境,然后逐个模块实现,最后说坑”的顺序走,中间给到能直接抄的代码和参数。你不需要先看完一本 JavaWeb 教材,跟着做就能把图书购物网站的主干跑通。
2. 图书购物网站的 MVC 分层与数据库设计
2.1 为什么这个课设必须用 MVC 三层架构
很多人做 JSP 课设,第一反应是“一个 JSP 页面里既查数据库又显示 HTML”,写起来快,但改起来是灾难。MVC 三层架构的价值在这个图书购物网站里体现得特别明显:图书列表、图书详情、购物车、订单,每个功能都涉及“取数据—处理逻辑—展示页面”三段,如果混在一起,改一个价格显示格式要翻十几个文件。
常见做法是把项目分成三层。表现层用 JSP 负责渲染,控制器层用 Servlet 接收请求、调用业务、决定跳转,模型层用 JavaBean 承载数据、用 DAO 封装 JDBC 操作。这样图书的增删改查只改 DAO,页面样式只改 JSP,请求参数校验放在 Servlet。对课设来说,这个分层不是为了“显得专业”,而是让你在答辩时能指着包结构说清楚每一层在干什么。
包结构我一般这样组织:com.bookshop.entity放实体类,com.bookshop.dao放数据库访问,com.bookshop.service放业务逻辑,com.bookshop.servlet放控制器,com.bookshop.util放数据库连接工具。JSP 统一放在WEB-INF/views下,避免被直接访问。这个约定后面所有代码都按它来。
2.2 图书购物系统的数据库表怎么定
数据库是整个系统的地基,表结构定错,后面 DAO 和页面全要返工。图书购物网站最少需要五张表:用户表、图书表、分类表、购物车表、订单表(订单主表加订单明细表更规范,课设里可以合并成一张订单表加一个图书 ID 字段,但推荐拆开)。
下面是我常用的建表语句,字段类型和默认值都按 MySQL 8 的习惯来。注意价格用DECIMAL(10,2)而不是FLOAT,金额计算用浮点会出精度问题,这是血泪经验。
CREATE DATABASE bookshop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE bookshop; -- 用户表 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, phone VARCHAR(20), address VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 图书分类表 CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL ); -- 图书表 CREATE TABLE t_book ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, category_id INT, cover VARCHAR(200), description TEXT, FOREIGN KEY (category_id) REFERENCES t_category(id) ); -- 购物车表 CREATE TABLE t_cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, FOREIGN KEY (user_id) REFERENCES t_user(id), FOREIGN KEY (book_id) REFERENCES t_book(id) ); -- 订单主表 CREATE TABLE t_order ( 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 INT NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES t_user(id) ); -- 订单明细表 CREATE TABLE t_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, FOREIGN KEY (order_id) REFERENCES t_order(id), FOREIGN KEY (book_id) REFERENCES t_book(id) );参数说明:utf8mb4是为了支持中文书名和特殊符号;status用整数表示订单状态,0 待付款、1 已付款、2 已发货、3 已完成,比存字符串省空间也好比较;order_no加唯一索引,防止重复下单生成相同订单号。初始数据至少插几条分类和图书,否则前台页面是空的,演示效果很差。
提示:MySQL 8 默认认证插件是
caching_sha2_password,老版本 JDBC 驱动连接会报错。要么升级驱动到 8.x,要么建用户时指定mysql_native_password。
3. 用 IDEA 搭起可运行的 JSP 项目骨架
3.1 新建项目与目录结构
在 IDEA 里新建 JavaWeb 项目,很多人卡在“新建完没有 webapp 目录”。正确做法是:先建一个普通 Java 项目,再在项目结构里添加 Web 模块。具体步骤是 File → Project Structure → Modules → 加号 → Web,然后把 Web Resource Directory 指到src/main/webapp。这样 IDEA 才会识别 JSP 和WEB-INF/web.xml。
目录结构最终长这样:src/main/java放 Java 代码,src/main/webapp放 JSP、CSS、JS,webapp/WEB-INF/lib放 jar 包,webapp/WEB-INF/web.xml放配置。依赖需要三个:MySQL 驱动mysql-connector-j-8.x.jar、JSTL 的jstl.jar和standard.jar。把这三个丢进WEB-INF/lib,IDEA 里再 Add as Library,否则 JSP 里用<c:forEach>会报找不到标签。
3.2 数据库连接工具类与连接池
课设里最容易翻车的地方就是数据库连接。每个 Servlet 都DriverManager.getConnection会导致连接数暴涨,演示时点几下就卡死。常见做法是写一个工具类,用静态代码块加载驱动,并提供获取和关闭连接的方法。进阶一点用 Druid 或 C3P0 连接池,但课设用简单工具类也够。
package com.bookshop.util; import java.sql.*; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/bookshop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL 驱动加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (stmt != null) stmt.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }逻辑说明:静态代码块只执行一次,保证驱动只加载一次。URL 里的serverTimezone=Asia/Shanghai不加会报时区错误,useSSL=false避免本地连接时的 SSL 警告。close方法按 ResultSet、Statement、Connection 的顺序关闭,每个都判空,避免空指针。参数上,端口 3306 和库名 bookshop 按你本机改,密码换成你自己的。
注意:如果启动报
Error 2002 (HY000): Can't connect to local MySQL server,先确认 MySQL 服务是否启动,再确认端口没被占用。Windows 下用net start mysql80,Linux 下用systemctl status mysqld看状态。
3.3 实体类与 DAO 的写法
实体类就是和表字段一一对应的 JavaBean,属性名用驼峰,比如categoryId对应category_id。DAO 里每个方法只做一件事:查图书列表、按 ID 查图书、插入订单。下面以图书查询为例。
package com.bookshop.dao; import com.bookshop.entity.Book; import com.bookshop.util.DBUtil; import java.sql.*; import java.util.*; public class BookDao { public List<Book> findAll() { List<Book> list = new ArrayList<>(); String sql = "SELECT id, name, author, price, stock, category_id, cover FROM t_book ORDER BY id DESC"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book b = new Book(); b.setId(rs.getInt("id")); b.setName(rs.getString("name")); b.setAuthor(rs.getString("author")); b.setPrice(rs.getBigDecimal("price")); b.setStock(rs.getInt("stock")); b.setCategoryId(rs.getInt("category_id")); b.setCover(rs.getString("cover")); list.add(b); } } catch (SQLException e) { e.printStackTrace(); } return list; } }逻辑说明:用 try-with-resources 自动关闭连接,比手动 close 更安全。SQL 用PreparedStatement而不是Statement,防止 SQL 注入,这也是答辩常问的点。getBigDecimal对应DECIMAL类型,避免精度丢失。参数上,ORDER BY id DESC让新书排在前面,演示时更直观。
4. 前台购物流程与后台管理的 Servlet 实现
4.1 图书列表与详情页的请求链路
前台的核心链路是:浏览器请求/book/list,Servlet 调 DAO 拿数据,存进 request 域,转发到bookList.jsp,JSP 用 JSTL 遍历渲染。这个链路走通,后面购物车和订单就是复制这套模式。
package com.bookshop.servlet; import com.bookshop.dao.BookDao; import com.bookshop.entity.Book; import javax.servlet.*; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; import java.util.List; @WebServlet("/book/list") public class BookListServlet extends HttpServlet { private final BookDao bookDao = new BookDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { List<Book> books = bookDao.findAll(); req.setAttribute("books", books); req.getRequestDispatcher("/WEB-INF/views/bookList.jsp").forward(req, resp); } }逻辑说明:@WebServlet注解代替 web.xml 配置,路径/book/list就是访问地址。forward是服务器内部跳转,浏览器地址栏不变,request 域的数据能带到 JSP。如果用redirect,request 域数据会丢,这是新手最常见的翻车点。参数上,doGet处理列表查询,doPost留给表单提交,不要混用。
对应的 JSP 片段用 JSTL 渲染:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:forEach items="${books}" var="b"> <div class="book-item"> <img src="${pageContext.request.contextPath}/images/${b.cover}" alt="${b.name}"> <h3>${b.name}</h3> <p>作者:${b.author}</p> <p>价格:¥${b.price}</p> <a href="${pageContext.request.contextPath}/book/detail?id=${b.id}">查看详情</a> </div> </c:forEach>${pageContext.request.contextPath}用来拼上下文路径,项目部署名变了也不用改代码。<c:forEach>的items对应 Servlet 里 setAttribute 的 key,写错一个字母页面就是空白,排查时先看 key 是否一致。
4.2 购物车与下单的事务处理
购物车加购的逻辑是:先查该用户购物车里是否已有这本书,有就数量加一,没有就插一条。下单时要把购物车里的条目转成订单明细,同时扣减库存,这三步必须在一个事务里,否则会出现“订单生成了但库存没扣”的脏数据。
public String createOrder(int userId) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 String orderNo = "NO" + System.currentTimeMillis(); BigDecimal total = BigDecimal.ZERO; // 1. 查购物车 String cartSql = "SELECT c.book_id, c.quantity, b.price FROM t_cart c " + "JOIN t_book b ON c.book_id = b.id WHERE c.user_id = ?"; List<Object[]> items = new ArrayList<>(); try (PreparedStatement ps = conn.prepareStatement(cartSql)) { ps.setInt(1, userId); ResultSet rs = ps.executeQuery(); while (rs.next()) { items.add(new Object[]{rs.getInt("book_id"), rs.getInt("quantity"), rs.getBigDecimal("price")}); total = total.add(rs.getBigDecimal("price").multiply(new BigDecimal(rs.getInt("quantity")))); } } // 2. 插订单主表 String orderSql = "INSERT INTO t_order(order_no, user_id, total_price, status) VALUES(?,?,?,0)"; int orderId; try (PreparedStatement ps = conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, orderNo); ps.setInt(2, userId); ps.setBigDecimal(3, total); ps.executeUpdate(); ResultSet keys = ps.getGeneratedKeys(); keys.next(); orderId = keys.getInt(1); } // 3. 插明细并扣库存 String itemSql = "INSERT INTO t_order_item(order_id, book_id, quantity, price) VALUES(?,?,?,?)"; String stockSql = "UPDATE t_book SET stock = stock - ? WHERE id = ? AND stock >= ?"; for (Object[] it : items) { try (PreparedStatement ps = conn.prepareStatement(itemSql)) { ps.setInt(1, orderId); ps.setInt(2, (Integer) it[0]); ps.setInt(3, (Integer) it[1]); ps.setBigDecimal(4, (BigDecimal) it[2]); ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement(stockSql)) { ps.setInt(1, (Integer) it[1]); ps.setInt(2, (Integer) it[0]); ps.setInt(3, (Integer) it[1]); if (ps.executeUpdate() == 0) { throw new SQLException("库存不足,图书ID:" + it[0]); } } } conn.commit(); return orderNo; } catch (SQLException e) { if (conn != null) try { conn.rollback(); } catch (SQLException ignored) {} e.printStackTrace(); return null; } finally { if (conn != null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} } }逻辑说明:setAutoCommit(false)开启事务,任何一步抛异常都 rollback,保证订单、明细、库存三者一致。Statement.RETURN_GENERATED_KEYS拿自增主键,用来关联明细。扣库存的 SQL 带AND stock >= ?,更新行数为 0 说明库存不够,主动抛异常触发回滚。参数上,订单号用时间戳生成,课设够用;生产环境要用更严谨的分布式 ID。
4.3 后台图书管理的增删改查
后台管理是课设的加分项,也是答辩老师最爱问的部分。图书管理至少要有列表、新增、编辑、删除四个功能。新增和编辑共用一个表单页,靠 id 是否为空区分。删除用软删除还是物理删除?课设里物理删除就行,但要注意外键约束:如果这本书已经被订单引用,直接删会报外键错误。常见做法是先查订单明细里有没有这本书,有就提示“该图书已被订单引用,不能删除”。
@WebServlet("/admin/book/delete") public class BookDeleteServlet extends HttpServlet { private final BookDao bookDao = new BookDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int id = Integer.parseInt(req.getParameter("id")); if (bookDao.hasOrderItem(id)) { req.setAttribute("msg", "该图书已被订单引用,不能删除"); req.getRequestDispatcher("/admin/book/list").forward(req, resp); return; } bookDao.deleteById(id); resp.sendRedirect(req.getContextPath() + "/admin/book/list"); } }逻辑说明:先判断是否被订单引用,是则带提示信息转发回列表;否则删除后重定向。重定向用sendRedirect,避免刷新页面重复删除。参数上,id从 URL 取,要判空和数字格式,否则传个字母进来就抛NumberFormatException。
5. 部署打包与常见问题排查
5.1 传统 JSP 项目打包 war 与 Tomcat 部署
课设最终要交的是能跑的项目,打包成 war 是最稳的方式。IDEA 里 File → Project Structure → Artifacts → 加号 → Web Application: Archive,选到你的 webapp 目录,输出路径设成out/artifacts/bookshop_war。然后 Build → Build Artifacts,生成的 war 丢进 Tomcat 的webapps目录,启动 Tomcat 会自动解压。
访问地址是http://localhost:8080/bookshop/,其中 bookshop 是 war 包名。如果访问 404,先看 Tomcat 日志里 war 有没有解压成功,再看web.xml里 welcome-file 是否指向了正确的首页。常见做法是把首页设成index.jsp,里面用<jsp:forward>跳到图书列表。
提示:Tomcat 10 以上版本把
javax.servlet改成了jakarta.servlet,老项目直接部署会报 ClassNotFound。课设如果用的是 Tomcat 9,别升级到 10;如果必须用 10,所有 import 都要改。
5.2 避坑:JSP 课设里最容易翻车的五个点
现象一:页面中文全是乱码。原因是请求和响应编码没统一。解决:在 Servlet 里resp.setContentType("text/html;charset=utf-8")和req.setCharacterEncoding("utf-8"),JSP 页面顶部<%@ page contentType="text/html;charset=utf-8" %>,数据库连接 URL 加characterEncoding=utf8,三处缺一不可。
现象二:JSTL 标签不解析,页面直接显示<c:forEach>。原因是缺 jstl.jar 或 web.xml 版本太低。解决:确认WEB-INF/lib下有 jstl 和 standard 两个 jar,web.xml 的 schema 用 3.0 以上版本。
现象三:连接池耗尽,点几下就卡死。原因是 DAO 里 Connection 没关。解决:统一用 try-with-resources,或者用 DBUtil.close 在 finally 里关。检查方法是在 MySQL 里执行SHOW PROCESSLIST,看连接数是不是一直涨。
现象四:下单后库存没扣,或者扣成负数。原因是没加事务或扣库存 SQL 没加条件。解决:按 4.2 的写法,setAutoCommit(false)加AND stock >= ?,更新行数为 0 就回滚。
现象五:部署后图片 404。原因是图片路径写成了绝对路径或没放进 webapp。解决:图片统一放webapp/images,JSP 里用${pageContext.request.contextPath}/images/xxx.jpg,不要用C:\...这种本地路径。
6. 把课设做成能讲清楚的作品:验证与进阶技巧
课设做完不是终点,能讲清楚才是。答辩时老师最爱问三个问题:你的 MVC 怎么分的、事务怎么保证的、如果并发下单会怎样。前两个前面已经覆盖,第三个是拉开差距的地方。当前实现在高并发下,两个请求同时读到库存为 1,都通过stock >= 1判断,然后都扣减,库存变 -1。解决办法是在扣库存 SQL 上加行锁,或者用UPDATE t_book SET stock = stock - ? WHERE id = ? AND stock >= ?配合事务隔离级别,MySQL 的 InnoDB 在更新时会加行锁,第二个请求会等第一个提交后再判断,从而避免超卖。这个点讲出来,比单纯说“我用了事务”有说服力。
验证方法上,我习惯写一个简单的压测脚本,用ab或 JMeter 对下单接口发 50 个并发,然后查数据库库存和订单数是否对得上。命令是ab -n 100 -c 50 -p order.txt -T application/x-www-form-urlencoded http://localhost:8080/bookshop/order/create,其中 order.txt 放用户 ID 参数。跑完看SELECT stock FROM t_book WHERE id=1和SELECT COUNT(*) FROM t_order,如果库存没变负、订单数等于成功请求数,说明事务和锁生效了。
进阶技巧上,有两个方向值得花时间。一是把 JSP 里的 Java 代码彻底清掉,全部用 JSTL 和 EL 表达式,页面只负责展示,这样代码可读性上一个台阶。二是给 DAO 加一个简单的连接池,不用引入 Druid 那么重,自己写一个LinkedList<Connection>做池化,理解连接复用的原理。这两个改动不大,但能让你的课设从“能跑”变成“有设计”。
我自己做第一个 JSP 课设时,把数据库密码硬编码在 DBUtil 里,交上去被老师一眼看穿,后来改成读db.properties配置文件,虽然只是几行代码,但答辩时老师明显更认可。还有一次,订单号用new Date().getTime()生成,压测时同一毫秒生成了两个相同订单号,直接报唯一索引冲突,后来加了随机数才解决。这些坑踩过一次就忘不掉,希望帮到你。
本文还有配套的精品资源,点击获取