简介:基于Java的图书仓储管理系统设计与实现文档,主要面向计算机相关专业学生、毕业设计人员及Java Web开发初学者,聚焦图书仓储中的库存混乱、查找困难、更新不及时等典型难题。资源包内共1个docx文件,大小仅1.05MB,内容紧凑且完整,涵盖绪论、开发目的与意义、开发工具介绍、需求分析、功能模块设计及系统实现说明等章节。文档详细阐述Java语言、SSM框架、JSP技术、MySQL数据库的选择依据和整合方式,并围绕人员管理、库位管理、图书管理、图书报废管理、图书退回管理五大模块展开业务梳理,还特别说明基于MVC架构的模块化设计思路,兼顾系统安全性、扩展性和易用性。已有287人学习,适合在撰写课程设计报告、毕业设计论文或开发图书管理类系统时参考,可帮助读者快速理解项目整体结构、核心业务流程与SSM整合要点,是Java Web仓储类项目的一份完整设计蓝本。
1. 基于 java 图书仓储管理系统设计与实现:先搞清楚你要交付的是什么
每年答辩季都有相当一批人提交同一个题目:基于 java 图书仓储管理系统设计与实现。这个题目之所以长盛不衰,是因为它把图书入库、库存管理、出库登记、盘点查询串成了一条完整的业务链路,技术上正好覆盖 JavaSE 后半程的面向对象封装、数据库设计、JDBC 连接和事务处理这几个核心考点,规模也适合一个人独立完成。配套的课程设计文档(也就是标题里那份 .docx)通常按需求分析、数据库设计、系统实现、测试验收四章组织,所以动手写代码之前先把文档框架想清楚,能少走很多弯路。
这个系统真正解决的是"库存流水对不上账"的问题:图书入库时库存增加,出库和报损时库存减少,每一笔变动都要留痕。它比"图书管理系统"更贴近真实仓储业务,也更适合拿来做 java 面试题里关于事务和数据一致性的实战案例。适合的人群很直接:正在写 Java 课程设计或毕业设计、刚学完 JavaSE 想拿完整项目练手、或者想给 java 面试八股文攒一个真实场景的在校生和自学者。
2. 需求边界与数据库设计:仓储系统的核心是"流水",不是"书"
2.1 先划定业务边界:图书仓储管理系统不是图书管理系统
很多人把"仓储"和"图书管理"混在一起,最后功能越做越散。这两个系统的业务模型有本质区别:图书管理系统侧重"借阅—归还",面向读者;图书仓储管理系统侧重"入库—出库—盘点",面向仓库管理员。你把读者表、借阅表塞进仓储项目,老师一眼就能看出来你没理解题目。
对标真实仓库场景,图书仓储管理的核心就四条链路:
- 采购入库——图书到货,录入品种和数量,库存增加,写入库流水;
- 部门领用/调拨出库——图书离开仓库,库存减少,写出库流水;
- 盘点——管理员清点实物库存,纠正账面与实际的差异;
- 查询统计——按书名、ISBN、出版社、分类、库存量组合查询。
关键认知在于:仓库里的同一种书有 N 本,没有唯一的一本书对应一个 ID,"品种 + 数量"才是正确的建模方式。如果沿用图书管理系统的"一本书一条记录"思路,会把库存设计成几百条重复的书目数据,统计时还要 count,这是前期最容易翻车的设计决策。
2.2 数据表设计:四张核心表 + 两张支撑表
我一般这样拆表,拆完基本不需要返工:
books(图书基本信息表):存静态信息,每一种书一条记录,用 ISBN 做自然主键,天然去重。字段包括 title、author、publisher、category_id、price、publish_date。
stock(库存表):与 books 一对一,存当前在库数量,外加一个 warning_line 预警线字段。预警线是后期做库存提醒的伏笔,一开始不建,后面要加就得改表。
inbound_records(入库流水表):记录每次入库的 ISBN、数量、操作人、时间、备注。流水只增不改,是后续对账的唯一依据。
outbound_records(出库流水表):结构和入库流水对应,记录每次出库的 ISBN、数量、操作人、时间、备注。
categories(分类表):存分类名称,单独建表而不是在 books 里冗余一个分类名字符串,是为了统计维度统一。
users(用户表):存登录账号,username 唯一,password 存密文。角色字段 role 区分 admin 和 operator,这是答辩时老师一定会问的权限控制点。
为什么 books 和 stock 不合并成一张表?合并后每次查书都要带着当前库存字段,将来做历史盘点、预警阈值调整都得在这张大表上折腾。把静态信息和动态数量分开,查询时可以按需 JOIN,写起来更符合"仓储"这个业务语义。
2.3 建表 SQL 落地:注意字段类型和约束
建表 SQL 如下(MySQL 8.0,InnoDB,utf8mb4):
CREATE DATABASE IF NOT EXISTS book_warehouse DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE book_warehouse; CREATE TABLE categories ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE COMMENT '分类名' ) ENGINE=InnoDB; CREATE TABLE books ( isbn VARCHAR(20) PRIMARY KEY COMMENT 'ISBN,主键', title VARCHAR(200) NOT NULL COMMENT '书名', author VARCHAR(100) COMMENT '作者', publisher VARCHAR(100) COMMENT '出版社', category_id INT COMMENT '分类ID', price DECIMAL(10,2) DEFAULT 0 COMMENT '定价', publish_date DATE COMMENT '出版日期', CONSTRAINT fk_books_category FOREIGN KEY (category_id) REFERENCES categories(id) ) ENGINE=InnoDB; CREATE TABLE stock ( book_isbn VARCHAR(20) PRIMARY KEY, quantity INT NOT NULL DEFAULT 0 COMMENT '当前库存量', warning_line INT NOT NULL DEFAULT 10 COMMENT '库存预警线', CONSTRAINT fk_stock_book FOREIGN KEY (book_isbn) REFERENCES books(isbn) ) ENGINE=InnoDB; CREATE TABLE inbound_records ( id INT PRIMARY KEY AUTO_INCREMENT, book_isbn VARCHAR(20) NOT NULL, quantity INT NOT NULL COMMENT '入库数量', operator VARCHAR(50) COMMENT '操作人', inbound_time DATETIME DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(255), CONSTRAINT fk_inbound_book FOREIGN KEY (book_isbn) REFERENCES books(isbn) ) ENGINE=InnoDB; CREATE TABLE outbound_records ( id INT PRIMARY KEY AUTO_INCREMENT, book_isbn VARCHAR(20) NOT NULL, quantity INT NOT NULL COMMENT '出库数量', operator VARCHAR(50) COMMENT '操作人', outbound_time DATETIME DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(255), CONSTRAINT fk_outbound_book FOREIGN KEY (book_isbn) REFERENCES books(isbn) ) ENGINE=InnoDB; CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL COMMENT '建议存密文,不要存明文', role VARCHAR(20) NOT NULL DEFAULT 'operator' ) ENGINE=InnoDB;这段 SQL 有几个必须理解的参数:
- utf8mb4 是必须的。MySQL 8.0 默认字符集就是它,但 JDBC 连接串也要同时带 characterEncoding=utf8,否则中文入库照样乱码;
- books 用 isbn 做主键,好处是重复录入同一本书时会被主键约束挡住,不用额外写查重逻辑;
- stock 表的 warning_line 默认值给 10,后做预警不用 ALTER TABLE;
- DECIMAL(10,2) 存价格,float/double 在金额计算上会有精度误差,这是 java 基础面试题里常考的"浮点数为什么不能用于金额"。
外键在这个规模下建议保留。生产环境往往因为性能拆掉外键,但课程设计保留外键能体现数据库规范化设计,MySQL 还能帮你拦截一部分脏数据。
3. 用 Swing 搭界面:三层架构下的模块划分与核心代码
3.1 为什么选 Swing 而不是 JSP 或 Spring Boot
这两年的课程设计开始分化出两个方向:桌面版(Swing/JavaFX)和 Web 版(Servlet/JSP 或 Spring Boot)。如果你的课程还没到框架阶段,老老实实选 Swing,理由有三个:
一是知识结构自洽。Swing + JDBC 正好覆盖 JavaSE 后半程的重点内容,不需要引入 Tomcat 容器和 HTTP 协议的知识。二是演示方便。双击直接跑一个窗口给老师看,不用配置容器,不用处理浏览器会话。三是代码量可控。课程设计规模下 Swing 版大概 2500 到 4500 行,Spring Boot 版至少要翻一倍,还涉及前端页面和接口文档。
有同学会问:Java 服务器页面(JSP)更贴近真实开发,选它行不行?行,如果你已经选定了 Web 方向,把登录逻辑换成 Filter + Session 也能跑。但下面这套代码按 Swing 版写,因为这是"图书仓储管理系统"最经典、也最容易让答辩老师看懂的落地形态。
3.2 登录模块:从 ActionListener 到业务分层
登录是第一个也是最好的分层演示点。不推荐把数据库连接、SQL 查询、业务判断全堆在 LoginFrame 一个类里——代码能跑,但答辩时老师问"换一个数据库怎么改",你很难解释。
我一般拆三层:view 层放 Swing 界面,service 层放登录校验和业务规则,dao 层放 JDBC 操作。登录界面核心代码如下:
// view/LoginFrame.java public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; private UserService userService = new UserService(); public LoginFrame() { setTitle("图书仓储管理系统 - 登录"); setSize(400, 250); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); // 布局代码省略,只看核心事件 loginButton.addActionListener(e -> { String username = usernameField.getText().trim(); String password = new String(passwordField.getPassword()); // 调 service,不在 view 里写 SQL User user = userService.login(username, password); if (user != null) { new MainFrame(user).setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, "用户名或密码错误"); } }); } }// service/UserService.java public class UserService { private UserDao userDao = new UserDao(); public User login(String username, String password) { if (username == null || username.isEmpty() || password == null || password.isEmpty()) { return null; } return userDao.findByUsernameAndPassword(username, password); } }// dao/UserDao.java public class UserDao { public User findByUsernameAndPassword(String username, String password) { String sql = "SELECT id, username, role FROM users " + "WHERE username = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setRole(rs.getString("role")); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }为什么要强调这三段代码的写法:
- PreparedStatement 的
?占位符不是可选项。用户输入一旦进入 SQL 字符串拼接,就是 SQL 注入,java 面试题里凡是"用户输入参与查询"的场景,答 PreparedStatement 是标准答案; - service 层做非空校验、dao 层做数据访问,view 层只负责把结果渲染出来。这个单向依赖关系,答辩时一句话就能讲清楚;
- try-with-resources 在 Java 7 之后可用,连接、PreparedStatement、ResultSet 会自动关闭,不用在 finally 里手动写一堆 close。
3.3 主界面图书列表:JTable 模型绑定与刷新
主界面用 JTable 展示图书列表,最常见的坑是"数据库改了但表格不刷新"。标准做法是用 AbstractTableModel 封装数据源,改完数据后调用 fireTableDataChanged() 通知重绘。
// view/BookTableModel.java public class BookTableModel extends AbstractTableModel { private String[] columns = {"ISBN", "书名", "作者", "出版社", "库存", "类别"}; private List<BookStockVO> data = new ArrayList<>(); public void setData(List<BookStockVO> newData) { this.data = newData; fireTableDataChanged(); } @Override public int getRowCount() { return data.size(); } @Override public int getColumnCount() { return columns.length; } @Override public Object getValueAt(int rowIndex, int columnIndex) { BookStockVO vo = data.get(rowIndex); switch (columnIndex) { case 0: return vo.getIsbn(); case 1: return vo.getTitle(); case 2: return vo.getAuthor(); case 3: return vo.getPublisher(); case 4: return vo.getQuantity(); case 5: return vo.getCategoryName(); default: return ""; } } @Override public String getColumnName(int column) { return columns[column]; } }这里有个细节:BookStockVO 是"图书 + 库存"的合并查询对象。它的作用是不让 view 层去组装多表 JOIN 的结果——JTable 每行拿到的就是完整数据,而不是先拿 books 再手动去查 stock。查询方法在 BookDao 中实现:
public List<BookStockVO> searchBooks(String keyword) { String sql = "SELECT b.isbn, b.title, b.author, b.publisher, " + "s.quantity, c.name AS category_name " + "FROM books b " + "LEFT JOIN stock s ON b.isbn = s.book_isbn " + "LEFT JOIN categories c ON b.category_id = c.id " + "WHERE b.title LIKE ? OR b.isbn LIKE ?"; List<BookStockVO> result = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { String like = "%" + keyword + "%"; ps.setString(1, like); ps.setString(2, like); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { BookStockVO vo = new BookStockVO(); vo.setIsbn(rs.getString("isbn")); vo.setTitle(rs.getString("title")); vo.setAuthor(rs.getString("author")); vo.setPublisher(rs.getString("publisher")); vo.setQuantity(rs.getInt("quantity")); vo.setCategoryName(rs.getString("category_name")); result.add(vo); } } } catch (SQLException e) { e.printStackTrace(); } return result; }LIKE 查询的边界在于:%通配符必须放在参数里,不能拼进 SQL 字符串。另外,LEFT JOIN 保证即使某本书还没有库存记录,图书信息也能显示出来,库存字段显示 0——这是仓储系统里"先有书后入库"场景的兜底。
4. JDBC 连接与数据一致性:扣库存的正确事务姿势
4.1 JDBC 参数配置:能连上只是第一步
JDK 8 搭配 MySQL 8,驱动用 mysql-connector-java 8.0.x。课程设计一般直接下载 jar 包放进 lib 目录,Maven 项目则加依赖。DBUtil 工具类用静态方法就够:
// util/DBUtil.java public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/book_warehouse" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }三个参数必须理解:
- serverTimezone=Asia/Shanghai,MySQL 8 不加会报 CST 时区识别失败,这是最常见的首次连接报错;
- characterEncoding=utf8 对应建库时的 utf8mb4,保证中文不乱码;
- useSSL=false 关闭 SSL 握手,本地开发不加会刷一堆 SSL 警告但也能跑,加上更干净。
DriverManager.getConnection 每次调用都创建新连接,课程设计规模下够用。如果想扩展成真实可用的系统,换成 HikariCP 连接池就行——service 层和 dao 层完全不用动,只需要把 DBUtil.getConnection() 改成 dataSource.getConnection()。这正好是分层设计带来的好处。
4.2 入库操作:流水和库存必须在一个事务里
入库是仓储系统最核心的业务动作。需求是:往 inbound_records 插一条流水,同时把 stock 表的库存数量加上去。如果两次操作各用一个数据库连接,就会出现"流水记录了但库存没加"的不一致问题。
java 面试题里"怎么保证数据一致性"的标准答案就是:同一个 Connection,手动开事务,全部成功才 commit,任何一步失败就 rollback。
// dao/InboundDao.java public boolean inboundBook(String isbn, int quantity, String operator, String remark) { if (quantity <= 0) { throw new IllegalArgumentException("入库数量必须大于 0"); } String insertRecord = "INSERT INTO inbound_records (book_isbn, quantity, operator, remark) " + "VALUES (?, ?, ?, ?)"; String updateStock = "INSERT INTO stock (book_isbn, quantity, warning_line) " + "VALUES (?, ?, 10) " + "ON DUPLICATE KEY UPDATE quantity = quantity + ?"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement psRecord = conn.prepareStatement(insertRecord); PreparedStatement psStock = conn.prepareStatement(updateStock)) { psRecord.setString(1, isbn); psRecord.setInt(2, quantity); psRecord.setString(3, operator); psRecord.setString(4, remark); psRecord.executeUpdate(); psStock.setString(1, isbn); psStock.setInt(2, quantity); psStock.setInt(3, quantity); psStock.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); e.printStackTrace(); return false; } } catch (SQLException e) { e.printStackTrace(); return false; } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这段代码的逻辑说明:
- conn.setAutoCommit(false) 是事务开启的开关,两条 executeUpdate 从此成为原子操作;
- ON DUPLICATE KEY UPDATE 处理两种场景:第一次入库时 stock 表没有记录,走 INSERT 插入初始数量;已有记录时走 UPDATE 累加库存。写这一条 SQL 比"先 SELECT 判断再 INSERT/UPDATE"更省事,也避免了两条 SQL 之间的并发竞态;
- psStock 的第 3 个参数对应 UPDATE 子句里的
?,传入的是本次入库数量,和 INSERT 的第一个?共用同一个值; - finally 里恢复 setAutoCommit(true) 再 close,是为了连接归还连接池时不带着未提交事务状态。
4.3 出库操作:库存扣成负数就是设计缺陷
出库反向执行,但有个额外约束:库存不足不能扣成负数。常见错误写法是先 SELECT 查当前库存,判断够不够再 UPDATE。这种"先查后改"在单会话下没问题,两个窗口同时出库同一本书时就会超卖。
更稳的写法是把判断条件直接写进 UPDATE 的 WHERE 子句:
// dao/OutboundDao.java public boolean outboundBook(String isbn, int quantity, String operator, String remark) { String insertRecord = "INSERT INTO outbound_records (book_isbn, quantity, operator, remark) " + "VALUES (?, ?, ?, ?)"; String deductStock = "UPDATE stock SET quantity = quantity - ? " + "WHERE book_isbn = ? AND quantity >= ?"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement psRecord = conn.prepareStatement(insertRecord); PreparedStatement psStock = conn.prepareStatement(deductStock)) { psRecord.setString(1, isbn); psRecord.setInt(2, quantity); psRecord.setString(3, operator); psRecord.setString(4, remark); psRecord.executeUpdate(); psStock.setInt(1, quantity); psStock.setString(2, isbn); psStock.setInt(3, quantity); int rows = psStock.executeUpdate(); if (rows == 0) { conn.rollback(); return false; } conn.commit(); return true; } catch (SQLException e) { conn.rollback(); e.printStackTrace(); return false; } } catch (SQLException e) { e.printStackTrace(); return false; } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这里的巧妙之处在于:WHERE quantity >= ?把"库存是否充足"的判断和"扣减"合并成了同一条 SQL 的原子操作。如果影响行数为 0,说明当前库存小于本次出库量,直接回滚,连查都不用查。这个条件更新的写法也是真实系统里处理并发扣减库存的常用方案,答辩时能讲清楚这一点,老师会确认你真的理解并发边界。
5. 避坑实录:Java 图书仓储系统里让人翻车的五个典型问题
5.1 双击 jar 包启动报"找不到或无法加载主类"
现象:在 IDEA 里运行完全正常,导出可运行 jar 包后双击没反应,命令行java -jar app.jar报错找不到主类。
原因:导出 jar 时没有指定 Main-Class,或者 IDEA 的 Artifacts 配置里主类没选对。还有一种可能是依赖的 mysql-connector jar 没有打进去,运行时缺类。
解决:在项目的 META-INF/MANIFEST.MF 里补上Main-Class: view.LoginFrame这一行,或者在 Project Structure → Artifacts 里把 Main Class 手动选到 view.LoginFrame。依赖问题用 Maven 的 maven-assembly-plugin 打 fat jar,或者用 IDEA 的 Jar 打包选项里把第三方依赖选为 "Extract to Output",把驱动类解包合并进产物。
5.2 数据库中文全部变成问号
现象:JDBC 插入中文后,表里显示???,但在 Navicat 里手动写中文正常。
原因:三处字符集不统一。数据库建库用了 utf8mb4,但 JDBC 连接串没带 characterEncoding=utf8;或者 MySQL 服务端 character_set_server 仍是 latin1;或者建表时字段被单独指定了 latin1。
解决:按顺序排查。先执行SHOW VARIABLES LIKE 'character_set_%';确认 character_set_server 是 utf8mb4;再检查 JDBC URL 里是否带characterEncoding=utf8;最后如果表已经建好了,执行ALTER TABLE books CONVERT TO CHARACTER SET utf8mb4;修复已有表。这类问题跟 Java 环境变量配置、JDK 安装这类环境问题一样,属于排查半小时、原因却只有一个字符的典型坑。
5.3 库存被扣成了负数
现象:同一本书连续出库两次,第二次无论如何都提示成功,甚至库存在界面上显示为 -3。
原因:代码里先 SELECT 查出库存,再 UPDATE 设置新值。两个会话同时操作时,后一次 UPDATE 覆盖了前一次的结果,或者扣减前根本就没判断库存够不够。
解决:把判断和扣减合并成一条条件 UPDATE:
UPDATE stock SET quantity = quantity - ? WHERE book_isbn = ? AND quantity >= ?返回值为 0 表示条件不满足,说明库存不足。这个方案同时解决了并发超卖和负数库存两个问题,比先查后改更可靠。
5.4 窗口关了进程还在后台跑
现象:点右上角 X 关闭主窗口,IDEA 控制台的红色停止按钮还亮着,任务管理器里 java.exe 进程仍然存在。
原因:JFrame 的默认关闭行为是 HIDE_ON_CLOSE,只是隐藏窗口,JVM 不会退出。如果你的登录窗口 new 出来后没有设置关闭行为,或者主窗口没设 EXIT_ON_CLOSE,就会一直挂着。
解决:在所有独立业务窗口的构造器里显式设置:
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);登录窗跳主窗的逻辑应该是登录窗 dispose(),主窗持有 EXIT_ON_CLOSE。课程设计里最忌讳的写法是在主窗体按钮里写System.exit(0)却不提前释放资源,虽然能退出,但显得很初级。
5.5 导出报表时路径含中文直接报错
现象:用 JFileChooser 选择"导出/报表"目录,路径是D:/课程设计/导出,点保存后程序直接抛异常,说找不到路径。
原因: JFileChooser 返回的 File 对象路径本身没问题,常见原因是目标目录不存在,而 FileOutputStream 不会帮你自动创建父目录。另外文件后缀和内容格式不匹配也会报错,比如用 XSSFWorkbook 写出 .xls 后缀文件。
解决:写文件之前先判断并创建父目录:
File parent = file.getParentFile(); if (parent != null && !parent.exists()) { parent.mkdirs(); }这个 3 行代码能省掉一大半的"导出失败"问题。
6. 从"能交作业"到"能讲清楚":答辩演示与库存预警的加分写法
6.1 答辩时你只需要讲清一条主线
演示不用把每个窗口点一遍,老师爱问的其实就三件事:表结构为什么这样拆——基础信息、当前库存、入库流水、出库流水各司其职,流水只增不改;事务怎么用——入库时同一个 Connection 里插流水再更新库存,失败就回滚;SQL 注入怎么防——所有用户输入都走 PreparedStatement 参数占位。把这三句话讲透,比把每个按钮点一遍有效得多。
6.2 用 POI 把盘点报表导出成 Excel
仓储系统经常要留盘点凭证,常见做法是用 Apache POI 的 XSSFWorkbook 生成 .xlsx 文件。核心代码很短:
Workbook wb = new XSSFWorkbook(); Sheet sheet = wb.createSheet("库存盘点"); Row header = sheet.createRow(0); header.createCell(0).setCellValue("ISBN"); header.createCell(1).setCellValue("书名"); header.createCell(2).setCellValue("库存"); // 逐行写入数据的循环省略 try (FileOutputStream fos = new FileOutputStream(filePath)) { wb.write(fos); }注意 XSSFWorkbook 只能写 .xlsx 后缀,写成 .xls 会打不开。文件父目录不存在时先 mkdirs,否则抛的 FileNotFoundException 容易让人误判为路径错误。
6.3 给低库存行标红:别改表格文本,改渲染器
把库存低于预警线的行标红,是最便宜的加分功能。常见错误是在 getValueAt 里返回"缺货"这样的字符串,导致数字列变成文本,排序和比较全乱。正确做法是自定义渲染器:
table.getColumnModel().getColumn(4).setCellRenderer( (t, value, isSelected, hasFocus, row, col) -> { JLabel label = new JLabel(String.valueOf(value)); if ((int) value <= 10) { label.setForeground(Color.RED); } return label; });这里的列索引 4 对应库存列的位置,如果表格加了列,索引要跟着迁移。预警阈值 10 可以改成读 stock 表里的 warning_line 字段,这样配置就放在数据库里而不是写在代码中。
做这个项目的最终体会是:数据库设计和事务逻辑比界面代码重要得多。把入库和出库两条链路反复走通,把库存不足、重复入库、连接断开这些异常路径都跑一遍,答辩时自然有底气说"这个系统我在真实场景里验证过"。我自己就是靠这个思路在答辩时把"数据一致性"讲清楚的。希望帮到你。
本文还有配套的精品资源,点击获取