ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

JSP图书销售系统实战:从建表到库存扣减的完整实现

2026/10/7 23:39:12 拓冰建站 浏览量
JSP图书销售系统实战:从建表到库存扣减的完整实现 简介这是一套面向Java Web初学者与课程设计者的网上图书销售系统源码基于JSP技术实现适合用于毕业设计、课程实训或自学练手。系统涵盖图书浏览、购物车、订单管理、用户与图书后台维护等核心模块下载后按说明书配置即可在本地运行。压缩包共243个文件约541KB其中58个java源文件与59个class编译文件构成业务逻辑主体50个jsp页面负责前台展示与后台交互另有css样式、gif与jpg图片资源、inc公共文件及mdb数据库文件结构完整、层次清晰。目前已有163人学习下载。读者可从中获得一套可直接运行的JSP项目实例理解Servlet与JSP协作方式、购物车与订单流程设计、数据库连接与后台增删改查实现并借助源码与页面文件对照调试快速掌握Java Web开发的基本套路与排错思路。1. 从一份 jsp.rar 说起图书销售系统到底该做成什么样如果你手里正躺着一个名为jsp.rar的压缩包解压后是一堆.jsp、.java、.xml和几张模糊的数据库截图标题写着「图书销售系统」那你大概率正面临一个经典局面要么是课程设计要交差要么是接了个小书店的进销存需求要么是想拿它当 Java Web 入门练手项目。这个标题背后真正要解决的问题不是「怎么把页面画好看」而是「怎么用 JSP 把图书的进、销、存、查这条链路跑通并且让代码结构不至于三天后就改不动」。它适合三类人刚学完 Servlet 想找个完整项目练手的在校生、需要快速交付一个小型图书管理后台的独立开发者、以及想理解传统 JSP 分层架构到底怎么落地的一线工程师。接下来我不谈空泛的「系统设计」只讲这套东西从建表到跑通的最小路径以及我踩过的那些坑。2. 图书销售系统的数据模型与 JSP 分层先想清楚再动手2.1 图书销售场景下必须落地的四张核心表很多教程一上来就让你建七八张表结果做到一半发现字段对不上。我一般会先把业务压缩到最小闭环图书、分类、订单、订单明细。用户表可以先用一张简单的t_user顶着权限只分普通用户和管理员。下面是我常用的建表 SQL字段类型按 MySQL 8 写字符集统一utf8mb4避免中文书名乱码。-- 图书分类表 CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名如计算机、文学, sort_order INT DEFAULT 0 COMMENT 排序权重 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 图书表 CREATE TABLE t_book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE COMMENT ISBN 唯一, title VARCHAR(200) NOT NULL, author VARCHAR(100), price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0 COMMENT 库存, category_id INT, cover_img VARCHAR(255) COMMENT 封面图路径, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES t_category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单主表 CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 业务订单号, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 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, unit_price DECIMAL(10,2) NOT NULL, FOREIGN KEY (order_id) REFERENCES t_order(id), FOREIGN KEY (book_id) REFERENCES t_book(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明t_book里的stock是库存扣减的关键字段下单时必须在同一个事务里先SELECT ... FOR UPDATE再更新否则并发下会超卖。t_order_item的unit_price要单独存不能只靠t_book.price关联查询因为图书调价后历史订单金额必须保持不变。参数上DECIMAL(10,2)比FLOAT更适合金额utf8mb4能存下生僻字和 emoji 书名。2.2 JSP 页面该放在哪一层MVC 在传统项目里的真实分工JSP 本身只是视图技术但很多人写着写着就把 JDBC 查询塞进% %脚本里最后页面又长又难调。我一般强制三层dao层只做数据库 CRUDservice层处理库存扣减和订单组装servlet或controller负责接收参数和跳转JSP 只负责用 EL 和 JSTL 渲染。下面是一个典型的图书列表 Servlet 片段。// BookListServlet.java WebServlet(/book/list) public class BookListServlet extends HttpServlet { private BookService bookService new BookServiceImpl(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String keyword req.getParameter(keyword); String categoryId req.getParameter(categoryId); // 分页参数默认第一页每页 10 条 int page Integer.parseInt(req.getParameter(page) null ? 1 : req.getParameter(page)); int size 10; PageResultBook result bookService.queryBooks(keyword, categoryId, page, size); req.setAttribute(pageResult, result); req.getRequestDispatcher(/WEB-INF/jsp/book_list.jsp).forward(req, resp); } }逻辑说明把 JSP 放在WEB-INF下是为了防止用户直接通过 URL 访问到未经过 Servlet 准备数据的页面这是 JSP 项目里最容易被忽略的安全习惯。参数page和size控制分页keyword和categoryId支持组合筛选。Service 层返回的PageResult里包含list、total、totalPageJSP 用 JSTL 的c:forEach遍历即可完全不需要再写 Java 脚本。2.3 用 JSTL 替代脚本片段让图书列表页干净到能维护下面这段 JSP 是我认为最值得抄作业的部分它把图书列表的渲染逻辑压缩到几乎没有 Java 代码。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % html headtitle图书列表/title/head body table border1 trthISBN/thth书名/thth作者/thth价格/thth库存/thth操作/th/tr c:forEach items${pageResult.list} varbook tr td${book.isbn}/td td${book.title}/td td${book.author}/td tdfmt:formatNumber value${book.price} pattern#,##0.00//td td${book.stock}/td td c:if test${book.stock 0} a href${pageContext.request.contextPath}/order/add?bookId${book.id}加入订单/a /c:if c:if test${book.stock 0}缺货/c:if /td /tr /c:forEach /table /body /html逻辑说明c:forEach的items直接取pageResult.listvar声明循环变量。fmt:formatNumber负责金额格式化避免页面上出现19.9和19.90混排。${pageContext.request.contextPath}是获取项目根路径的标准写法部署到 Tomcat 后无论上下文怎么改都不会 404。注意c:if判断库存时用的是 EL 表达式不要写成% book.getStock() 0 %否则又退回到脚本时代。3. 从零跑通图书销售系统环境、部署与核心功能实现3.1 本地环境搭建JDK、Tomcat 与 MySQL 的版本匹配我见过太多人卡在环境上不是 JDK 版本太高导致 Tomcat 起不来就是 MySQL 驱动包放错位置。下面是我验证过能跑通的组合直接照抄即可。组件推荐版本说明JDK8 或 11JSP 项目用 8 最稳11 需要确认 Tomcat 版本Tomcat8.5 或 9.09.0 对 Servlet 4.0 支持更好MySQL5.7 或 8.08.0 需要换com.mysql.cj.jdbc.Driver驱动包mysql-connector-java-8.0.x.jar放在WEB-INF/lib下IDEIntelliJ IDEA 或 EclipseIDEA 需配置 Artifacts 才能部署配置数据库连接时我一般把参数写在db.properties里而不是硬编码在 Java 类中。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordyour_password逻辑说明serverTimezone必须加否则 MySQL 8.0 会报时区错误。characterEncodingutf8要和建表时的utf8mb4配合防止中文乱码。读取时用Properties类加载再交给DruidDataSource或简单的DriverManager管理连接。3.2 图书查询与分页一条 SQL 搞定搜索和分类筛选分页查询是图书销售系统里最频繁的操作写不好就是全表扫描。我一般用「先查总数再查列表」的两段式SQL 如下。-- 查总数 SELECT COUNT(*) FROM t_book b WHERE b.status 1 AND (b.title LIKE CONCAT(%, ?, %) OR b.author LIKE CONCAT(%, ?, %)) AND (? IS NULL OR b.category_id ?); -- 查当前页列表 SELECT b.*, c.name AS category_name FROM t_book b LEFT JOIN t_category c ON b.category_id c.id WHERE b.status 1 AND (b.title LIKE CONCAT(%, ?, %) OR b.author LIKE CONCAT(%, ?, %)) AND (? IS NULL OR b.category_id ?) ORDER BY b.create_time DESC LIMIT ?, ?;逻辑说明(? IS NULL OR b.category_id ?)是一个常用技巧当分类参数为空时条件恒真避免在 Java 里拼接 SQL 字符串。LIMIT ?, ?的第一个参数是偏移量(page-1)*size第二个是每页条数。注意LIKE CONCAT(%, ?, %)会导致索引失效图书量超过十万时需要考虑全文索引或 Elasticsearch但小书店场景完全够用。3.3 下单与库存扣减事务边界和超卖问题的处理下单是图书销售系统里唯一不能出错的地方。我见过有人先查库存再更新中间隔了几百毫秒并发一上来就超卖。正确做法是在一个事务里锁行。// OrderServiceImpl.java public void createOrder(int userId, int bookId, int quantity) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 锁住图书行防止并发扣减 String lockSql SELECT stock, price FROM t_book WHERE id ? FOR UPDATE; PreparedStatement ps conn.prepareStatement(lockSql); ps.setInt(1, bookId); ResultSet rs ps.executeQuery(); if (!rs.next()) throw new RuntimeException(图书不存在); int stock rs.getInt(stock); BigDecimal price rs.getBigDecimal(price); if (stock quantity) throw new RuntimeException(库存不足); // 扣库存 String updateSql UPDATE t_book SET stock stock - ? WHERE id ?; PreparedStatement ups conn.prepareStatement(updateSql); ups.setInt(1, quantity); ups.setInt(2, bookId); ups.executeUpdate(); // 写订单主表和明细表 // ... 省略插入逻辑 conn.commit(); } catch (Exception e) { if (conn ! null) conn.rollback(); throw new RuntimeException(下单失败, e); } finally { DBUtil.close(conn); } }逻辑说明FOR UPDATE会在事务提交前锁住这一行其他并发请求会阻塞等待从而保证库存不会扣成负数。setAutoCommit(false)开启事务任何一步失败都rollback。参数quantity必须在前端和后端都做校验前端限制不能超过库存后端再兜底一次。注意FOR UPDATE必须在事务里才有意义如果自动提交模式下执行锁会立即释放。3.4 订单状态流转从待支付到完成的 JSP 页面跳转订单状态一般用数字表示但页面上要显示中文。我习惯在 JSP 里用c:choose做映射而不是在 Java 里写一堆 if-else。c:choose c:when test${order.status 0}span stylecolor:orange待支付/span/c:when c:when test${order.status 1}span stylecolor:blue已支付/span/c:when c:when test${order.status 2}span stylecolor:green已发货/span/c:when c:when test${order.status 3}span stylecolor:gray已完成/span/c:when c:otherwise未知状态/c:otherwise /c:choose逻辑说明c:choose相当于 Java 的 switchc:when是分支c:otherwise是默认。状态值定义在 Java 常量类里JSP 只负责展示两边约定好数字含义即可。如果状态超过五个建议改成在数据库里存状态描述或用一个Map在 Servlet 里转换好再传给 JSP。4. 图书销售系统避坑排查那些让我加班到凌晨的翻车现场4.1 中文乱码从 Tomcat 到 MySQL 的完整排查链现象图书标题在列表页显示正常但搜索「计算机」时查不到任何结果或者新增图书后数据库里是问号。原因乱码可能出现在三个环节——JSP 页面编码、Tomcat 请求编码、MySQL 连接编码。解决第一步每个 JSP 头部加% page contentTypetext/html;charsetUTF-8 %第二步在web.xml里配置CharacterEncodingFilter强制请求和响应都用 UTF-8第三步检查 JDBC URL 是否带characterEncodingutf8以及数据库和表的字符集是否为utf8mb4。三步缺一不可我一般用「新增一条中文书名 → 查数据库 → 页面搜索」这个链路逐段验证。4.2 数据库连接池耗尽Druid 配置里的三个关键参数现象系统跑一段时间后页面卡死日志报GetConnectionTimeoutException。原因连接池最大连接数设得太小或者代码里拿了连接没关闭。解决在druid.properties里把maxActive调到 20 以上maxWait设为 3000 毫秒同时开启removeAbandonedtrue和removeAbandonedTimeout180让池子自动回收泄漏的连接。更重要的是在 DAO 层用 try-with-resources 确保Connection、PreparedStatement、ResultSet都被关闭。我见过最隐蔽的泄漏是在catch块里直接return忘了在finally里关连接。4.3 JSP 页面报 404路径、部署与 WEB-INF 的常见误解现象Servlet 写好了web.xml也配了访问/book/list就是 404。原因通常有三个一是 IDEA 的 Artifacts 没把WEB-INF/lib和classes打进去二是WebServlet的路径和访问路径大小写不一致三是 JSP 放在WEB-INF下却直接用浏览器访问。解决先看 Tomcat 启动日志里有没有Deployment of web application archive成功信息再用 IDEA 的「Open in Browser」确认上下文路径。如果 JSP 在WEB-INF下必须通过 Servlet 的forward访问不能直接输 URL。4.4 库存扣成负数并发测试时暴露的事务漏洞现象用 JMeter 模拟 10 个用户同时买同一本书库存从 5 变成 -3。原因下单逻辑没有加行锁或者锁的范围不对。解决确保SELECT ... FOR UPDATE和UPDATE在同一个事务里并且事务隔离级别是REPEATABLE READ或更高。另外UPDATE t_book SET stock stock - ? WHERE id ? AND stock ?这种写法也能兜底但不如行锁直观。测试时我一般开两个终端手动并发或者用ab命令压一下确认库存不会为负才算过关。4.5 订单金额对不上浮点数与 DECIMAL 的取舍现象购物车里显示总价 59.90生成订单后变成 59.899999。原因Java 里用了double或float做金额运算。解决所有金额字段在 Java 里用BigDecimal数据库用DECIMAL(10,2)。BigDecimal的加减乘除要用add、subtract、multiply方法不要用、-、*。除法必须指定精度和舍入模式例如divide(new BigDecimal(3), 2, RoundingMode.HALF_UP)。这个坑在图书打折场景里特别容易翻车因为折扣率往往是0.85这种小数。5. 让图书销售系统再稳一点两个我常用的进阶技巧第一个技巧是给 JSP 页面加一个「加载完成后刷新一次」的兜底逻辑。有些老项目在 Tomcat 热部署后第一次访问会拿到旧的编译结果页面显示空白或报错。我一般会在列表页底部加一段极简的 JavaScript只在检测到表格为空且 URL 没有刷新标记时执行一次location.reload()并带上?refreshed1防止死循环。// 仅当表格无数据且未刷新过时刷新一次页面 window.onload function () { var rows document.querySelectorAll(table tr); var url new URL(window.location.href); if (rows.length 1 !url.searchParams.has(refreshed)) { url.searchParams.set(refreshed, 1); window.location.href url.toString(); } };逻辑说明rows.length 1表示只有表头没有数据行refreshed参数防止无限刷新。这个技巧在开发阶段特别省心但生产环境要慎用因为可能掩盖真正的空数据问题。第二个技巧是关于图书封面的坐标定位。有些图书封面需要在上传后裁剪JSP 页面里可以用一个隐藏的input存x、y、width、height后端用 Java 的BufferedImage按坐标裁剪。我一般把坐标存成x,y,w,h字符串解析时用split(,)并做边界校验防止用户传入负数导致RasterFormatException。这个功能在图书管理后台里不算核心但加上之后整个系统的完成度会高很多。最后说一个我自己的习惯每次改完 DAO 层的 SQL我一定会把PreparedStatement的参数按顺序打印到日志里确认占位符和setXxx的索引一一对应。这个习惯帮我省下了至少三次「明明 SQL 没错但就是查不到数据」的排查时间。图书销售系统这类项目难点从来不在 JSP 语法本身而在数据一致性和边界条件上。希望帮到你。本文还有配套的精品资源点击获取