
简介基于JSPMySQL开发的图书销售管理系统是一套面向计算机专业学生的完整JavaWeb项目适用于课程设计、期末大作业或项目实战入门。系统覆盖用户注册登录、图书检索展示、购物车与订单管理以及后台图书、分类、库存和订单维护等常见业务代码与数据库配套齐全可部署运行后直接演示或继续二次开发。资源包共286个文件体积仅6.93MB包含53个Java源文件、52个JSP页面、78个GIF动态演示图以及JS、CSS、JAR依赖库和SQL脚本等。其中JSP与Java完成前端展示和业务处理JS/CSS负责页面交互与美化GIF可用于演示系统操作流程SQL脚本则帮助快速初始化数据库表结构。目前已有233人学习下载有助于期末集中攻关也能提升JavaWeb动手能力。借助完整项目文件可以梳理一个典型B/S图书商城从前端到数据库的设计脉络前端使用了Bootstrap、LayUI等常用框架搭配JSP标签和Java Servlet便于理解页面与逻辑的协作方式。内含动态演示和清晰目录结构节省踩坑时间适合需要快速完成课设报告和答辩演示的学生。1. 这个 JSPMySQL 的图书销售管理系统到底能拿来做什么如果你刚学完 JavaWeb 基础想找一个能完整跑通“前台买书 后台管书”的项目源码这个基于 JSPMySQL 的网上书店系统就是最经典的那种。它不是一个花哨的 SpringBoot 全栈玩具而是一套用 JSP/Servlet/JDBC 写出来的传统三层结构浏览器访问 JSP 页面Servlet 接收请求DAO 操作 MySQL 数据库。你可以用它应付毕业设计、写进简历作为电商类项目经历也可以把它当作理解“一个 JavaWeb 项目从源码到数据库的完整生命周期”的练手样本。这个标题里的“源码数据库”是最值钱的部分——拿到.sql初始化脚本和 Java 源码意味着你不是从零开始造轮子而是先站在别人搭好的骨架上做二次开发。适合的人群很具体正在做毕设的在校生、刚转行需要作品集的候选人、想补全“IDEA 运行 JavaWeb 项目配置”这一课的自学者。下面我会按真实项目落地的顺序把代码结构、数据库初始化、IDEA 部署、常见踩坑、功能改进一次讲清。2. 项目结构和数据库设计先看清它由哪几块拼成再动手改2.1 一个典型 JavaWeb 网上书店的模块划分拿到源码后不要急着点运行先按包名和目录把项目“读”一遍。传统 JavaWeb 项目的结构非常固定网上书店基本分成前台和后台两条线前台用户注册/登录、图书分类浏览、图书详情、购物车、提交订单、模拟支付、我的订单列表。后台管理员登录、图书增删改查、分类管理、订单状态修改、用户列表。对应到源码里的 Java 包常见命名是com.xxx.bookstore.servlet控制器层、com.xxx.bookstore.dao数据访问层、com.xxx.bookstore.entity实体类、com.xxx.bookstore.utilJDBC 工具类。JSP 页面则放在WebRoot或webapp目录下前台页面可能叫index.jsp、detail.jsp、cart.jsp后台页面集中在admin文件夹。我一般会先看web.xml它决定了整个项目的路由入口。如果是用 Servlet 3.0 以上的版本部分配置可能已经改用注解但老项目仍然会在web.xml里写 servlet-mapping。这里有一个很重要的判断项目用的是“人肉 Servlet 转发 JSP”还是“JSP 里直接嵌 Java 代码”。前者是学习典范后者会导致页面很乱但二次开发时你反而更容易看懂每一行在做什么。2.2 数据库表设计与 MySQL 5.7 初始化脚本这套系统的核心表一般有 6 张用户表、分类表、图书表、购物车表或不用购物车放 Session、订单表、订单项表。以 MySQL 5.7 为基准导入数据库脚本前先把字符集和排序规则选对否则后面会遇到中文乱码和SQLException: Illegal mix of collations。给你一个可以直接执行的初始化脚本我把关键字段和注释写在 SQL 里-- 创建数据库使用 utf8mb4 支持的字符集 CREATE DATABASE IF NOT EXISTS bookstore DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE bookstore; -- 用户表 CREATE TABLE user ( id INT(11) NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密文密码不建议存明文, email VARCHAR(100) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 分类表 CREATE TABLE category ( id INT(11) NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名例如 文学/技术, sort INT(11) DEFAULT 0 COMMENT 排序号越小越靠前, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 图书表 CREATE TABLE book ( id INT(11) NOT NULL AUTO_INCREMENT, category_id INT(11) NOT NULL, title VARCHAR(200) NOT NULL, author VARCHAR(100) DEFAULT NULL, price DECIMAL(10,2) NOT NULL COMMENT 单价用定点数不要用 float, stock INT(11) NOT NULL DEFAULT 0, cover VARCHAR(255) DEFAULT NULL COMMENT 封面图片相对路径, description TEXT, status TINYINT(4) DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id), KEY idx_category_id (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE orders ( id INT(11) NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, user_id INT(11) NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, receiver_name VARCHAR(50) NOT NULL, receiver_phone VARCHAR(20) NOT NULL, receiver_address VARCHAR(255) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单项表 CREATE TABLE order_item ( id INT(11) NOT NULL AUTO_INCREMENT, order_id INT(11) NOT NULL COMMENT 关联 orders.id, book_id INT(11) NOT NULL, book_title VARCHAR(200) NOT NULL COMMENT 下单时快照防止图书改名, price DECIMAL(10,2) NOT NULL, quantity INT(11) NOT NULL, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 插入管理员账号密码为 admin123 的 MD5仅示例生产环境建议加盐 -- 在项目里实际登录时会对输入做同样的 MD5 后比对 INSERT INTO user (username, password) VALUES (admin, MD5(admin123));这份脚本的逻辑有几个设计点值得你细看。图书表里price用了DECIMAL(10,2)而不是float/double是因为电商金额对精度要求高二进制浮点数会在计算总价时出现 0.10.20.30000000000000004 这种问题book_title在订单项表里做“快照”而不是外键关联是因为图书名称日后可以修改但历史订单必须保持购买时的信息。设计是否“最优”可以讨论但作为学习项目这些取舍能让你在面试时讲出画面感。2.3 为什么这套项目用 JSP MySQL而不是 SpringBoot/MyBatis当你把源码跑通后第一个会让你困惑的问题是这都什么年代了为什么还要学 JSP MySQL直接上 SpringBoot MyBatis-Plus 去做网上书店不是更简单吗答案是这套组合能让你看到 HTTP 最原始的样子。JSP 请求直接被 Tomcat 转发给 ServletServlet 里通过request.getParameter拿参数通过 JDBC 的PreparedStatement拼 SQL 查询 MySQL再request.setAttribute把结果送回 JSP。中间没有 Spring 帮你自动配置事务没有 MyBatis 帮你映射结果集你被迫手动处理每一条连接、每一次提交。正是这种“笨”做法让你把 JavaWeb 的核心原理吃透。另外从部署角度看老项目仍然大量存在于教育机构和中小型公司内部系统它们不需要微服务不需要容器化一台装好 JDK Tomcat MySQL 的服务器就能跑。你会读这种源码、会改这种源码反而比只会启动 SpringBoot 的新人更能处理遗留系统。所以我的建议是不要把 JSP 项目当成过时技术鄙视而要把它当成理解现代框架的解剖标本。3. 用 IDEA 把源码跑起来从导入项目到首次登录3.1 环境准备JDK 8、Tomcat、MySQL 5.7 的版本搭配拿到源码后最容易翻车的就是环境版本不匹配。我踩过的血泪经验是不要一上来就装 JDK 17 Tomcat 10 MySQL 8你的源码大概率是照着旧教材写的包名还带着javax.servlet。JDK 8 搭配 Tomcat 8.5/9 是最稳妥的组合MySQL 5.7 的初始化脚本和 JDBC 驱动也完全兼容。MySQL 8 虽然也能跑但需要把驱动换成com.mysql.cj.jdbc.Driver并且连接 URL 加上serverTimezoneAsia/Shanghai否则日期查询会报时区错误。如果你对安装 MySQL 5.7 不熟最省事的方式是直接下载压缩版免安装版解压到本地。配置好my.ini后用管理员权限执行# 初始化 MySQL 5.7 数据目录生成 root 临时密码 mysqld --initialize-insecure # 以控制台方式启动方便看到日志 mysqld --console # 另外开一个终端进入 MySQL 命令行 mysql -uroot -p--initialize-insecure这一步生成的 root 密码为空适合本地开发。启动后先执行第 2 章的建库脚本然后再去看源码里的配置文件。很多项目源码自带bookstore.sql我的习惯是绝不直接双击运行.bat或.sql一律打开看清楚字符集和库名再执行避免把已有本地库覆盖。3.2 导入项目并配置数据库连接从 .sql 到 db.properties打开 IDEA选择File - New - Project from Existing Sources找到源码目录里的pom.xml或.iml文件。经典 JSP 项目通常不是 Maven 工程而是一个 Web 项目这时选择打包方式为 Web。导入后第一件事是确认 Project Structure 里的Libraries是否有 Tomcat 的 Servlet API 包如果没有按快捷键CtrlShiftAltS在Modules - Dependencies里添加 Tomcat 提供的servlet-api.jar。数据库连接配置一般在src/db.properties或src/c3p0-config.xml。下面是两种最常见的写法# db.properties jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingUTF-8useSSLfalse jdbc.usernameroot jdbc.password123456!-- c3p0-config.xml -- c3p0-config named-config namebookstore property namedriverClasscom.mysql.jdbc.Driver/property property namejdbcUrljdbc:mysql://localhost:3306/bookstore?useUnicodetrueamp;characterEncodingUTF-8amp;useSSLfalse/property property nameuserroot/property property namepassword123456/property property nameinitialPoolSize3/property property namemaxPoolSize10/property /named-config /c3p0-config这里的useSSLfalse必须加。MySQL 5.7 默认协议要求客户端做 SSL 握手而本地开发通常没有证书缺了这个参数会出现Communications link failure或SSL connection error。characterEncodingUTF-8是第二个必加参数否则 JSP 页面提交的中文到数据库后会变成问号。把连接信息改成你自己的密码后启动前先写一个最简单的main方法测试连接import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; public class TestConnection { public static void main(String[] args) throws Exception { Class.forName(com.mysql.jdbc.Driver); String url jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingUTF-8useSSLfalse; Connection conn DriverManager.getConnection(url, root, 123456); PreparedStatement ps conn.prepareStatement(SELECT count(*) FROM user); ResultSet rs ps.executeQuery(); rs.next(); System.out.println(连接成功用户表记录数 rs.getInt(1)); rs.close(); ps.close(); conn.close(); } }这段代码里的Class.forName是告诉 JVM 加载 MySQL 驱动类DriverManager.getConnection的第三个参数是密码要和配置文件保持一致。如果这条测试能通过说明你的 MySQL 服务和库表都正常接下来才有必要继续配置 Tomcat 部署。3.3 部署到 Tomcat 并解决 404/500 的最小排查路径IDEA 里配置 Tomcat 的方法是Run - Edit Configurations - - Tomcat Server - Local。在Deployment标签页点击选择Artifact...选中项目的 exploded warApplication context 填/bookstore。这样启动后访问地址就是http://localhost:8080/bookstore/index.jsp。如果启动时报Error during artifact deployment常见原因是项目没有成功编译成 exploded 结构。我的排查路径固定为三步第一步看 IDEA 底部Build窗口确认BUILD SUCCESSFUL第二步重新 clean 再 build排除旧字节码干扰第三步检查 Tomcat 的conf/catalina.log。如果页面 404大概率是 context path 不对如果页面 500看 IDEA 控制台的Exception堆栈重点关注“找不到类”还是“空指针”。找到一条清晰的堆栈比瞎猜“页面为什么打不开”高效得多。这里特别提醒拷贝项目到另一台电脑时.idea文件夹里可能记录了旧环境的 Tomcat 路径导致打开项目后 Tomcat 配置是红色报错。不要再去修改旧路径直接新建一个 Local Tomcat 配置把旧的删掉这是最省时的方式。4. 改代码前必须避开的 5 个坑中文乱码、时间类型、密码、SQL注入、资源路径做 JavaWeb 项目最折磨人的不是业务逻辑而是环境类问题。这 5 个坑是我在给别人讲源码时被问得最多的每条都按“现象 → 原因 → 解决”写清楚你改代码前先对照一遍能少走两小时弯路。坑 1JSP 页面中文全部显示成???现象浏览器访问index.jsp所有中文标题变成问号从表单提交到数据库的文字也变成问号。原因分两层第一JSP 文件本身编码不是 UTF-8第二JDBC 连接 URL 没带characterEncodingUTF-8。解决方法是统一三处编码JSP 第一行写% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %IDEA 右下角的文件编码切换成 UTF-8数据库连接 URL 加上字符集参数。如果用了过滤器也要确认过滤器里设置了request.setCharacterEncoding(UTF-8)否则 POST 请求的中文在你读取参数前就已经乱掉。坑 2MySQL 5.7 连接报SSL connection error或Communications link failure现象启动 Tomcat 后控制台出现大量 SSL 握手失败堆栈偶尔能连上但非常慢。原因MySQL 5.7 的 Connector/J 默认开启 SSL而本地 MySQL 没有配置证书。解决在 JDBC URL 上加useSSLfalse并且把com.mysql.jdbc.Driver换成com.mysql.cj.jdbc.Driver同样有效。注意新旧驱动的类名不同老项目用的是com.mysql.jdbc.DriverMySQL 8 驱动下虽然会提示过时但依然能跑。// 正确连接方式显式关闭 SSL String url jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingUTF-8useSSLfalse;坑 3订单时间插入数据库后少了 8 小时或java.util.Date不能直接保存现象页面显示时间是下午 16:00存入 MySQL 后变成 08:00或者调用setTimestamp时报类型转换异常。原因MySQL 的DATETIME类型需要java.sql.Timestamp而实体类里用java.util.Date存了时间。解决写 DAO 时手动转换new Timestamp(entity.getCreateTime().getTime())。时区问题则是 JDBC 连接没有指定时区可以在 URL 上加serverTimezoneAsia/Shanghai。// 实体类中字段类型保留 java.util.Date private java.util.Date createTime; // DAO 中插入时转换 ps.setTimestamp(4, new Timestamp(order.getCreateTime().getTime()));坑 4密码用明文存数据库登录校验失败或被人拖库现象user表的password字段明文写着123456后台随便一个管理员都能看到所有用户密码。原因老源码为了演示方便省去了加密步骤。解决至少用 MD5 或 SHA-256 做散列登录时把用户输入做同样散列再比对。注意这是最基础的方案更安全的做法是加盐salt password再散列。// 注册时生成盐 String salt UUID.randomUUID().toString().substring(0, 6); String hashedPwd DigestUtils.md5Hex(salt rawPassword);坑 5JSP 里写死相对路径目录一变就 404现象项目部署后点击“登录”按钮提交到admin/LoginServlet结果 URL 直接变成了http://localhost:8080/admin/LoginServlet从/bookstore根路径跳走了。原因JSP 中的form actionadmin/LoginServlet是相对当前路径解析的在深层页面里就乱了。解决所有跳转都用绝对上下文路径JSP 顶部先取${pageContext.request.contextPath}再拼接业务地址。form action${pageContext.request.contextPath}/admin/LoginServlet methodpost以上 5 条基本覆盖了从“导入源码”到“第一次跑通业务”的所有拦路虎。如果你在自己机器上遇到的是其他异常记住一个原则不要只看一句话异常直接把完整堆栈的前 20 行贴到搜索引擎里通常能找到更精确的答案。5. 功能改进阶购物车计算、订单状态、MySQL 排序与分页5.1 为什么购物车放在 Session而不是单独建数据库表很多第一次做网上书店的人会纠结要不要建一张cart表把用户加购的商品持久化我的建议是对毕设或中小型系统购物车放 Session 就够了。原因是购物车是临时性数据用户可能加了书没下单就关闭浏览器如果每次都写数据库会产生大量孤儿数据。Session 方案在用户关闭浏览器后自动销毁不需要额外做定时清理。实现购物车时我一般在实体层定义一个CartItem再用HashMapInteger, CartItem作为购物车对象放在 Sessionpublic class CartItem { private Book book; private int quantity; public double getSubTotal() { // 金额计算必须用 BigDecimal否则会出现 0.10.2 的浮点误差 BigDecimal price BigDecimal.valueOf(book.getPrice()); BigDecimal qty BigDecimal.valueOf(quantity); return price.multiply(qty).doubleValue(); } }购物车操作没有数据库交互全靠 Servlet 里对 Session 的读写。常见逻辑是用户点击“加入购物车”Servlet 从 Session 取出MapInteger, CartItem如果已存在就quantity1否则put一个新对象。修改数量时也要先判断库存不要等到提交订单再校验前后端各拦一道。5.2 订单号生成与订单状态机的设计订单表的主键id适合作为内部自增标识但对外展示给用户的订单号最好用order_no避免别人通过订单号大小猜测你的日销量。我常用的生成方式是yyyyMMddHHmmss 用户ID 3位随机数再把整串转为字符串。如果同一秒内用户多可以再加一个 ThreadLocalRandom 生成的数字。String orderNo new SimpleDateFormat(yyyyMMddHHmmss).format(new Date()) String.format(%04d, userId) String.format(%03d, ThreadLocalRandom.current().nextInt(1000));订单状态字段建议用TINYINT存数字不要直接存字符串。因为数字可以轻松扩展而且方便做状态机。我的设计是0 待支付 → 1 已支付 → 2 已发货 → 3 已完成4 是已取消。前端页面显示中文状态时可以用一个 Map 做映射MapInteger, String STATUS_TEXT Map.of(0, 待支付, 1, 已支付, ...)。支付操作在真实系统会走第三方接口在这个项目里模拟为直接跳转到“支付成功”页并修改订单状态。提交订单是一个典型的多表事务场景插入orders表、批量插入order_item表、扣减图书库存。这里必须使用事务否则订单创建成功但库存没减会造成超卖。如果你用 JDBC 手动控制请在同一个连接上执行并确保conn.setAutoCommit(false)try { conn.setAutoCommit(false); // 1. 插入订单表拿到生成的订单ID // 2. 循环插入订单项表 // 3. 更新图书表库存 conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); }很多 JSP 老项目并没有统一事务每个 DAO 方法拿一个连接这样在业务层事务是不生效的。你要改造的话建议引入一个简单的TransactionFilter或用ThreadLocal保存同一个连接。5.3 列表分页与排序MySQL 的 LIMIT、ORDER BY 与参数绑定图书列表页是网上书店主入口数据量大了以后必须分页。JSP 项目最常用的分页写法是用LIMIT offset, size排序用ORDER BY sort ASC或ORDER BY create_time DESC。这里有一个我踩过的坑单纯对非唯一字段排序会导致分页数据重复或丢失。例如ORDER BY price但价格相同的记录之间顺序不稳定第一页和第二页可能同时出现同一条记录。解决办法是排序字段追加主键ORDER BY price ASC, id ASC保证排序的确定性。-- 第 2 页每页 10 条跳过前 10 条取后 10 条 SELECT * FROM book WHERE status 1 ORDER BY price ASC, id ASC LIMIT 10 OFFSET 10;对应的 JDBC 代码要习惯用PreparedStatement直接把 OFFSET 和 LIMIT 作为参数传入不要拼 SQL 字符串String sql SELECT * FROM book WHERE status ? ORDER BY price ASC, id ASC LIMIT ? OFFSET ?; PreparedStatement ps conn.prepareStatement(sql); ps.setInt(1, 1); // status 1 上架 ps.setInt(2, pageSize); ps.setInt(3, (currentPage - 1) * pageSize);这里OFFSET的计算要特别小心第 1 页的 OFFSET 是 0第 2 页是pageSize第 3 页是2 * pageSize。如果你把currentPage * pageSize传进去就会空掉第一页的一部分数据。同时首页的搜索和排序条件要隐藏在request.getParameter里翻页时通过?keywordxxxsortprice传回后端否则用户一翻页搜索条件就丢了。6. 把“能跑”变成“能答辩”演示数据、接口验证和最小排查习惯6.1 准备一套有说服力的演示数据空数据库跑出来的系统演示效果很差。我建议一次性插入 10 本图书、4 个分类、2 个测试用户和 1 份模拟订单。书名用真实出版物价格保留两位小数封面图片放在项目的images/目录。演示时要能快速展示三个场景搜索“Java”能出书、详情页价格正常、下单后库存减少。6.2 用浏览器开发者工具和日志验证按 F12 打开 Network 面板勾选“Disable cache”然后逐一点击页面操作。重点看每个请求的 URL、状态码、响应时间。如果你发现某个按钮点击后页面刷新但数据没变先看响应是 302 还是 200302 说明 Servlet 没有 forward 而是 sendRedirectURL 可能被截断。再配合 IDEA 控制台的 System.out 或日志把 DAO 的 SQL 打印出来一眼就能看出参数传错。6.3 维护一份自己的“最小运行清单”我会在项目根目录放一个README.md写下四行内容本地 MySQL 账号、数据库连接文件位置、Tomcat 端口、常用测试账号。这个习惯帮我避免每次换电脑都重新翻代码。另外改完 JSP 后如果不能立即看到效果先手动删除 Tomcat 的work/Catalina/localhost/bookstore目录这是 JSP 编译缓存很多人被这一条玄学问题卡了半小时。最后想跟你说的是这套 JSPMySQL 网上书店并不难但把环境配置、数据库设计、异常排查从头到尾走一遍之后你能建立的信心比背十个 SpringBoot 注解更扎实。希望帮到你。本文还有配套的精品资源点击获取