JSP+MySQL图书销售管理系统实战:从跑通到进阶改造

发布时间:2026/10/9 8:24:37
JSP+MySQL图书销售管理系统实战:从跑通到进阶改造
简介这是一套基于JSPMySQL的JavaWeb图书销售管理系统网上书店完整项目源码与数据库面向计算机相关专业正在做期末大作业、课程设计的学生以及需要项目实战练习的学习者可作为高分课程设计参考方案。资源包共286个文件约6.93MB其中53个java文件承载后端业务逻辑52个jsp页面负责前台展示与后台管理40个js与19个css配合bootstrap、layui等前端框架完成界面交互另含10个jar依赖、1个sql数据库脚本及图片、字体等静态资源结构完整、层次清晰。项目经导师指导并认可通过涵盖图书展示、购物车、订单管理等网上书店核心模块读者可据此快速理解JavaWeb分层开发思路、数据库表设计与前后端交互流程也可直接用于课程设计答辩或二次开发练习。目前已有233人学习下载适合需要完整可运行项目案例的初学者与进阶学习者参考。1. 从零手搓一套 JSPMySQL 图书销售管理系统这套老技术栈为什么还值得动手如果你正在搜「基于JSPMySQL的JavaWeb图书销售管理系统网上书店项目源码数据库」大概率是三种人之一要交课程设计的学生、需要一套能跑通的 JavaWeb 完整案例来练手的转行者、或者被安排维护一套遗留 JSP 系统的初级工程师。这套技术栈听起来确实不新SpringBoot4 和 MyBatis 满天飞的年代JSP 像是上个时代的产物。但现实是大量高校课程设计、中小企业内部管理系统、老项目二次开发仍然跑在 JSPServletMySQL 这套组合上而且它把「请求怎么进来、数据怎么落库、页面怎么渲染」这条链路暴露得最彻底没有框架帮你藏黑匣子。这篇文章不讲空泛的架构演进只讲一件事拿到一套图书销售管理系统的源码和数据库之后怎么在本地把它跑起来、怎么理解它的分层、怎么改出一个能用的网上书店功能、以及哪些坑会让你卡一整天。适合能看懂 Java 基础语法、装过 MySQL、但没独立跑通过一个完整 JavaWeb 项目的人。熟手可以直接跳到第 4 章的避坑清单和第 5 章的进阶技巧。2. 图书销售管理系统的分层拆解从表结构到页面渲染的完整链路一套典型的 JSP 网上书店代码结构基本是固定的entity 层放实体类dao 层写 JDBC 操作service 层做业务组装servlet 层接请求jsp 层做展示。理解这条链路比背代码重要因为改需求的时候你得知道改哪一层。2.1 数据库表设计图书、分类、订单、用户四张核心表怎么定图书销售管理系统的数据库通常围绕四张主表展开先把表结构定清楚后面写 DAO 才不会反复改。下面是我一般会用的建表语句字段做了精简但保留了实际项目里必须有的部分。-- 图书表核心字段是库存和价格价格用 DECIMAL 避免浮点误差 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 书名, author VARCHAR(100) COMMENT 作者, category_id INT COMMENT 分类外键, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0 COMMENT 库存, cover_img VARCHAR(255) COMMENT 封面图路径, description TEXT COMMENT 简介, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表状态字段用 tinyint0待付款 1已付款 2已发货 3已完成 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表一本书一条记录价格要冗余存防止图书改价后历史订单金额变化 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, unit_price DECIMAL(10,2) NOT NULL COMMENT 下单时单价 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个血泪经验订单明细表里的unit_price必须冗余存储。我见过有人图省事订单明细只存 book_id 和数量金额靠关联 book 表实时算结果图书一改价历史订单金额全变了对账直接翻车。价格快照是电商类系统的铁律别省这个字段。字符集统一用utf8mb4不要用utf8。MySQL 的utf8其实是阉割版存不了 emoji 和部分生僻字图书简介里出现特殊符号就会报错或者变问号。这个坑在 MySQL 5.7 和 8.0 上都存在建库时就要定好。2.2 JDBC 工具类与 DAO 层为什么必须手写连接池而不是 DriverManager新手写 JDBC 最常见的写法是每次操作都DriverManager.getConnection()然后 close。这套写法在本地测试没问题一上并发就崩因为每次建立连接的开销很大而且连接数不受控。正确做法是写一个工具类内部维护连接池。// DBUtil.java用 HikariCP 做连接池配置文件读 db.properties public class DBUtil { private static HikariDataSource dataSource; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); HikariConfig config new HikariConfig(); config.setJdbcUrl(props.getProperty(jdbc.url)); config.setUsername(props.getProperty(jdbc.username)); config.setPassword(props.getProperty(jdbc.password)); config.setMaximumPoolSize(10); // 连接池上限按并发量调 config.setMinimumIdle(2); // 最小空闲连接 config.setConnectionTimeout(30000); // 30秒拿不到连接就抛异常 dataSource new HikariDataSource(config); } catch (Exception e) { throw new ExceptionInInitializerError(数据库连接池初始化失败); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }db.properties放在src/main/resources下内容大致是jdbc.urljdbc:mysql://localhost:3306/bookstore?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8。注意serverTimezone这个参数MySQL 8.0 的驱动如果不指定时区连接时会报The server time zone value is unrecognized这是新手最常卡住的报错之一。DAO 层就基于这个工具类写增删改查。以图书查询为例public ListBook findByCategory(int categoryId, int page, int size) { String sql SELECT * FROM book WHERE category_id ? LIMIT ?, ?; ListBook list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, categoryId); ps.setInt(2, (page - 1) * size); // 分页偏移量 ps.setInt(3, size); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); // 把结果集映射成实体 } } } catch (SQLException e) { e.printStackTrace(); } return list; }用PreparedStatement而不是Statement拼接字符串一是防 SQL 注入二是预编译语句在 MySQL 端有缓存重复执行更快。分页用LIMIT offset, sizeoffset 从 0 开始所以是(page-1)*size。数据量大的时候深分页会慢这个在第 5 章讲优化。2.3 Servlet 请求分发与 JSP 渲染一个下单流程走完五层Servlet 是这套架构的入口。以「加入购物车」为例请求从浏览器到数据库再回到页面完整链路是这样的cart.jsp表单提交到/cart/add→CartServlet的doPost接收 → 调CartService.add()→CartDao.insert()写库 → 重定向回购物车页面。WebServlet(/cart/add) public class CartServlet extends HttpServlet { private CartService cartService new CartServiceImpl(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session req.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } int bookId Integer.parseInt(req.getParameter(bookId)); int quantity Integer.parseInt(req.getParameter(quantity)); cartService.addToCart(user.getId(), bookId, quantity); resp.sendRedirect(req.getContextPath() /cart/list); } }这里有个容易忽略的点req.getParameter拿到的永远是字符串转 int 之前最好做空值和格式校验否则用户传个abc进来直接NumberFormatException页面报 500。生产代码里我会包一层工具方法做安全转换。JSP 页面负责渲染用 JSTL 标签遍历数据不要在里面写 Java 脚本片段% %那是老写法维护性差。用c:forEach配合 EL 表达式${book.title}更清晰。JSP 顶部记得加% page contentTypetext/html;charsetUTF-8 %否则中文乱码。3. 本地跑通这套源码环境配置、导入与启动的完整步骤拿到源码和数据库文件之后能不能在半小时内跑起来取决于环境配置对不对。这一章按顺序走一遍每一步都给出验证方法。3.1 MySQL 安装与数据库导入从下载到执行 SQL 脚本MySQL 版本选择上5.7 和 8.0 都能跑这套系统但驱动包不一样。5.7 用mysql-connector-java-5.1.x8.0 用mysql-connector-java-8.0.x混用会报No suitable driver或者时区错误。我一般推荐 8.0因为 5.7 已经停止官方维护了。安装过程不展开重点说导入。假设数据库脚本叫bookstore.sql命令行导入# 先登录 MySQL mysql -u root -p # 创建数据库并指定字符集 CREATE DATABASE bookstore DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 退出后用 source 导入注意路径用正斜杠或双反斜杠 mysql -u root -p bookstore D:/project/bookstore.sql导入完成后验证USE bookstore; SHOW TABLES;应该能看到 book、orders、user 等表。如果导入报Unknown character set说明脚本里用了utf8mb4_0900_ai_ci这种 8.0 特有的排序规则而你装的是 5.7把脚本里的排序规则改成utf8mb4_general_ci即可。注意Windows 下路径分隔符是反斜杠在命令行里要么用正斜杠要么用双反斜杠转义单反斜杠会被当成转义字符。3.2 IDEA 导入 JavaWeb 项目Tomcat 配置与常见启动报错IDEA 导入项目后第一件事是检查 Project Structure 里的 JDK 版本和 Language Level源码如果是老项目可能是 JDK 8你本地装的是 JDK 17编译会报一堆错。统一成 8 或者 11 最稳。配置 Tomcat 的步骤Run → Edit Configurations → 加号 → Tomcat Server → Local → 在 Deployment 标签页添加 Artifact通常是xxx:war exploded→ Application context 设成/bookstore。启动后访问http://localhost:8080/bookstore/。常见启动报错对照报错信息原因解决ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动包没放进 WEB-INF/lib把 jar 复制到 lib 目录或 Maven 加依赖No suitable driver found驱动版本和 MySQL 版本不匹配5.7 用 5.1.x8.0 用 8.0.xThe server time zone value is unrecognized连接串没指定时区URL 加serverTimezoneAsia/Shanghai404 访问不到页面Application context 配错检查访问路径是否带对项目名Port 8080 already in use端口被占用改 Tomcat 端口或杀掉占用进程3.3 数据库连接配置与首次登录验证db.properties里的账号密码要和你本地 MySQL 一致。如果 MySQL 是 8.0root 用户默认用caching_sha2_password认证插件老版本驱动连不上会报Unable to load authentication plugin。解决办法有两个升级驱动到 8.0.x或者改用户认证方式ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;启动成功后先用系统自带的测试账号登录通常在 user 表里有一条 admin 记录密码可能是明文或者 MD5。登录进去能看到图书列表、能加购物车、能下单说明主链路通了。如果登录一直失败先查 user 表里密码字段存的是什么格式再对一下登录代码里的加密逻辑是否一致这是新手最容易忽略的地方。4. 跑起来之后必踩的坑乱码、连接泄漏、分页错位的排查清单系统能启动只是第一步真正让人抓狂的是那些「看起来能跑但就是不对」的问题。这一章列的都是我实际踩过的按现象、原因、解决三段式写。4.1 中文乱码POST 和 GET 请求的两种处理方式现象表单提交中文书名数据库里存进去变成???或者乱码。原因Tomcat 默认用 ISO-8859-1 解码请求参数而页面和数据库都是 UTF-8编码不一致。POST 和 GET 的处理方式还不一样。解决POST 请求在 Servlet 最前面加req.setCharacterEncoding(UTF-8)。GET 请求的参数在 URL 里Tomcat 8 以后默认 URIEncoding 就是 UTF-8但老版本 Tomcat 需要在server.xml的 Connector 标签加URIEncodingUTF-8。数据库连接串也要带characterEncodingutf8。三处都对齐了才不会乱码。4.2 数据库连接泄漏连接池耗尽后页面直接卡死现象系统跑一段时间后所有页面都打不开日志里报Connection is not available, request timed out。原因DAO 里拿了 Connection 没关或者关在了 catch 块外面异常一发生就漏一个连接。连接池上限 10漏十次就满了。解决所有 JDBC 资源用 try-with-resources 写法Connection、PreparedStatement、ResultSet 都放在 try 的括号里JVM 会自动关。这是最省心的方式别手动在 finally 里关容易漏。如果已经泄漏了重启 Tomcat 能临时恢复但根治还得改代码。4.3 分页查询的边界问题最后一页和空结果现象点最后一页报错或者总数算出来多一页空白。原因分页的 offset 计算没考虑边界。比如总共 25 条每页 10 条应该是 3 页但如果用total % size 0 ? total/size : total/size 1算总页数25/102 余 5总页数 3没问题。但如果 total 是 2020/102总页数 2也对。真正出错的是查询时 offset 超出范围MySQL 返回空结果集代码里如果直接取rs.next()的第一个元素就会空指针。解决总页数用(total size - 1) / size这个公式一行搞定。查询结果为空时页面显示「暂无数据」不要假设一定有记录。4.4 订单号重复高并发下的唯一性保证现象偶尔出现两笔订单订单号一样数据库唯一索引报错。原因订单号用时间戳生成同一毫秒内两个请求拿到相同值。解决订单号用「时间戳 用户ID后四位 随机数」组合或者直接用 UUID。更稳妥的是数据库层面加唯一索引插入失败就重试。别指望时间戳能保证唯一并发场景下毫秒级碰撞很常见。4.5 JSP 页面缓存导致数据不刷新现象改了数据库里的图书信息刷新页面还是旧数据。原因浏览器缓存了 JSP 渲染结果或者 Tomcat 的工作目录里有编译好的旧 class。解决开发阶段在 JSP 顶部加% page sessionfalse %并在响应头设置Cache-Control: no-cache。更彻底的是清理 Tomcat 的 work 目录重新编译。生产环境该缓存就缓存但开发时别被这个坑浪费时间。5. 把这套系统改造成能写进简历的项目三个进阶方向跑通只是及格线要让这套图书销售管理系统在面试或者答辩里拿得出手得往上加东西。这一章给三个方向都是投入产出比高的。5.1 用过滤器统一处理编码和登录校验现在每个 Servlet 里都写一遍setCharacterEncoding和登录判断重复且容易漏。抽一个 Filter 出来WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }登录校验同理判断 session 里有没有 loginUser没有就跳登录页放行登录和注册相关的路径。这样业务代码里就不用管这些横切逻辑了。过滤器是 JavaWeb 里最实用的组件之一面试也常问。5.2 把 JDBC 换成 MyBatis不改业务代码的迁移路径如果想让项目看起来更现代可以把 DAO 层换成 MyBatisService 和 Servlet 基本不用动。步骤是加 MyBatis 依赖、写mybatis-config.xml、把每个 DAO 接口改成 Mapper 接口、SQL 挪到 XML 映射文件里。以图书查询为例select idfindByCategory resultTypecom.bookstore.entity.Book SELECT * FROM book WHERE category_id #{categoryId} LIMIT #{offset}, #{size} /selectMapper 接口方法签名保持和原来 DAO 一致Service 层注入方式从new BookDaoImpl()改成sqlSession.getMapper(BookMapper.class)。这样迁移风险最小也顺便把 MyBatis 这个高频考点练了。5.3 深分页优化与库存扣减的原子操作数据量上万之后LIMIT 10000, 10这种深分页会越来越慢因为 MySQL 要扫描前 10010 行再丢掉前 10000 行。优化方式是记住上一页最后一条的 id用WHERE id lastId LIMIT 10走主键索引速度稳定。库存扣减是另一个容易出问题的地方。下单时如果先查库存再更新两个请求同时查到库存 1都以为够结果超卖。正确做法是用一条 SQL 做原子扣减UPDATE book SET stock stock - #{quantity} WHERE id #{bookId} AND stock #{quantity}判断影响行数如果是 0 说明库存不足回滚订单。这一条 SQL 把检查和扣减合并成原子操作靠数据库的行锁保证并发安全。这个技巧在面试里讲出来比背八股文有说服力。我自己的习惯是每跑通一个老项目就挑一个模块用新方式重写一遍对比两种实现的差异。这套 JSPMySQL 的图书销售管理系统把过滤器、MyBatis 迁移、原子扣减这三处改完代码质量和简历分量都能上一个台阶。希望帮到你。本文还有配套的精品资源点击获取