news 2026/9/11 13:58:41

Java图书进销存管理系统:MySQL表设计与JDBC事务实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java图书进销存管理系统:MySQL表设计与JDBC事务实践

简介:这是一份面向计算机专业学生与Java初学者的图书进销存管理系统完整源码包,源自大三期末大作业,经导师指导并获得99分高分评价,可解决课程设计、期末大作业缺乏可运行项目的问题。压缩包共92个文件,容量仅272KB,包含8个Java源文件、2个SQL数据库脚本、2个XML配置以及大量PNG截图,另有README与UML类调用关系图,便于快速理解项目结构与数据库设计。项目基于Maven构建,涵盖图书采购、销售、库存等核心模块,代码结构清晰、注释到位,小白也能按文档导入运行并二次扩展。目前已有63人学习浏览,适合需要实战练习或参考完整项目流程的读者。

1. 图书进销存管理系统:先看清业务,再谈 Java 源码与 MySQL 数据库

拿到一个“java 图书进销存管理系统(Java 源码 + MySQL 数据库)”的压缩包,第一反应不应该是解压、改配置、跑起来,而是先想清楚这个标题背后是哪套业务:采购入库、销售出库、库存盘点。图书有 ISBN、出版社、定价这些标准字段,天然适合做关系型数据建模,因此这类项目长期是数据库课程设计的高频选题,也是理解 Java 与 MySQL 协作方式的最小闭环——从表设计、JDBC 数据层、事务控制到报表导出,每一层都能独立验证。下面按这个顺序把关键设计讲透,你手头源码里的每一段代码都能在这里找到对应。

2. 图书进销存系统的 MySQL 表设计:建表语句与索引选择

2.1 进销存围绕三类单据转:主从表结构怎么拆

进销存系统的所有业务动作最终都落成单据:采购单记录从哪个供应商进了哪些书、进价多少;销售单记录卖了哪几本书、卖价多少;库存是单据落库之后算出来的账。课程设计里最常见的错误是把一张采购单当成一条记录,同一单多本书就拆成多条重复的单号,后续做明细查询和退货处理都会很别扭。正规做法是主从表:一张单据拆成主表和明细表,主表放单号、日期、供应商、经办人这些单据头信息,明细表放书号、数量、单价这些行信息。

表名类型作用关键字段
user基础表登录用户与角色username, password, role
book基础表图书信息与当前库存isbn, book_name, stock, price
supplier基础表供应商档案supplier_name, contact, tel
purchase_order单据主表采购单头order_no, supplier_id, total_amount, order_date
purchase_item单据明细采购单行order_id, book_id, quantity, unit_price
sale_order单据主表销售单头order_no, total_amount, order_date, operator_id
sale_item单据明细销售单行order_id, book_id, quantity, sale_price

这七张表的关系画成 ER 图就是:user 和 supplier 是独立实体,book 与两张明细表构成 1:N,供应商与采购主表构成 1:N,明细表通过 order_id 回到各自的主表。课程设计报告里的 ER 图按这个关系画,评审基本挑不出结构问题。

这里必须解释一个设计取舍:book 表里的 stock 是冗余字段。理论上库存可以由采购明细累加再减去销售明细实时算出来,但每个界面都要显示当前库存,低于库存下限还要标红,实时聚合性能差、代码也啰嗦。所以正常项目都在 book 表冗余 stock,用事务保证它与单据同步更新。答辩时“为什么冗余库存字段”几乎是必问题,答清楚这一点,比背十条建表规范都管用。

2.2 建表 SQL:引擎、字段类型与索引的 3 个注意点

建表统一用 InnoDB 引擎和 utf8mb4 字符集。主键用自增 id,ISBN 单独建唯一索引——ISBN 可能为空或录入不规范,不适合当主键。下面这段是图书表和采购两张表的完整建表语句,销售表结构与之对称:

-- 图书信息表,stock 为冗余库存字段 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NULL, book_name VARCHAR(200) NOT NULL, author VARCHAR(100) DEFAULT '', publisher VARCHAR(100) DEFAULT '', price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, min_stock INT NOT NULL DEFAULT 10, category VARCHAR(50) DEFAULT '', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_isbn (isbn), KEY idx_category (category) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 采购单主表 CREATE TABLE purchase_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, supplier_id INT NOT NULL, total_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00, order_date DATETIME NOT NULL, operator_id INT NOT NULL, KEY idx_supplier (supplier_id), KEY idx_order_date (order_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 采购单明细表 CREATE TABLE purchase_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, KEY idx_order (order_id), KEY idx_book (book_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

字段类型的选择有几个容易踩的点:价格用 DECIMAL(10,2) 不用 FLOAT,FLOAT 是近似存储,累计对账会出现 0.01 级的差异;数量用 INT;日期用 DATETIME 而不要用 VARCHAR,否则范围查询和 DATE_FORMAT 格式化都要做隐式转换。索引按查询模式建:进销存查询集中在“按日期范围看单据”“按书名或 ISBN 查图书”“按供应商查采购”,所以 idx_order_date、idx_book、idx_supplier 三个就够,明细表上再多索引反而拖慢写入。物理外键课程设计可以加,但不少源码和生产环境为了导入方便不建,一致性由应用层事务保证。

2.3 初始化数据与 SQL 导入顺序

源码包里的 .sql 一般包含建库、建表、初始数据三部分。用 Navicat 或 MySQL Workbench 导入时注意执行顺序:连接后先手动建好 bookms 库再执行 sql 文件,很多导入报 1046 没选库的错误,就是因为文件里没有 USE 语句而工具又没切换默认数据库。初始数据至少要有一条管理员账号、两个供应商、十本以上图书,否则后面的入库出库报表都测不出来。管理员密码建议存 MD5 摘要而不是明文,答辩时被问安全性的概率很高。有些源码会用 MySQL 存储过程生成初始化数据,把批量插入封装成一次过程调用,但大多数课程设计直接写在 SQL 文件里,两种做法对系统本身没有影响。

3. Java 连接 MySQL 写数据层:JDBC 连接参数、DBUtil 与增删改查

3.1 MySQL 8 的 JDBC 连接参数:驱动、URL、时区怎么配

源码跑不起来,八成问题出在连接阶段。MySQL 5.x 用 com.mysql.jdbc.Driver,MySQL 8.x 必须换成 com.mysql.cj.jdbc.Driver,URL 里还要多带时区参数。按 mysql 安装配置教程装完 MySQL 8.0 之后,最不容易出问题的连接配置是这样一份 db.properties:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/bookms?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456

参数逐个说清楚:characterEncoding=utf8 保证中文写入不乱码,要和建表时的 utf8mb4 配套;serverTimezone=Asia/Shanghai 是 MySQL 8 驱动的硬性要求,缺了直接抛时区相关的 SQLException;useSSL=false 在本机测试时省掉 TLS 握手;allowPublicKeyRetrieval=true 解决 MySQL 8 默认 caching_sha2_password 认证方式下“不允许获取公钥”的报错。第一次连不上的排查顺序是:先用telnet 127.0.0.1 3306确认端口通,再用 Navicat 用同一账号密码试连,排除网络和账号问题后,剩下的基本就是驱动版本和 URL 参数。

3.2 DBUtil 连接工具类与 ResultSet 到对象映射

桌面端 Swing 项目最常规的数据层结构是:一个 DBUtil 管连接,一组 DAO 类管单表的增删改查。DBUtil 用静态代码块加载配置并注册驱动,对外只暴露 getConnection 和 close:

public class DBUtil { private static String url; private static String username; private static String password; static { // 静态块只执行一次:加载配置并注册驱动 try (InputStream in = DBUtil.class.getResourceAsStream("/db.properties")) { Properties props = new Properties(); props.load(in); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); Class.forName(props.getProperty("jdbc.driver")); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, Statement st, ResultSet rs) { // 先关结果集,再关语句,最后关连接 if (rs != null) try { rs.close(); } catch (SQLException ignored) {} if (st != null) try { st.close(); } catch (SQLException ignored) {} if (conn != null) try { conn.close(); } catch (SQLException ignored) {} } }

逻辑说明:静态块在类第一次被引用时执行一次,把配置文件读进来并注册驱动;getConnection 每次用 DriverManager 新建连接,课程设计的并发量下够用,没必要上连接池。close 的释放顺序是先 ResultSet、再 Statement、最后 Connection,反了在连接池环境会直接泄漏连接。源码里如果看到把 Class.forName 写在 getConnection 里每次执行,那是老写法,能跑但没必要。

DAO 层以图书的模糊查询举例,参数是关键字和分页偏移量,返回的是 JTable 直接能渲染的 List:

public List<Book> search(String keyword, int offset, int limit) throws SQLException { String sql = "SELECT id, isbn, book_name, author, publisher, price, stock, min_stock " + "FROM book WHERE book_name LIKE ? OR isbn LIKE ? " + "ORDER BY id DESC LIMIT ?, ?"; List<Book> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 关键字两端拼 %,实现模糊匹配 ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); ps.setInt(3, offset); ps.setInt(4, limit); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book book = new Book(); book.setId(rs.getInt("id")); book.setIsbn(rs.getString("isbn")); book.setBookName(rs.getString("book_name")); book.setPrice(rs.getBigDecimal("price")); book.setStock(rs.getInt("stock")); list.add(book); } } } return list; }

这段是标准的“连接、预编译、执行、映射、释放”五步。Java 8 以上的项目用 try-with-resources 省掉手动 close,连接、Statement、ResultSet 在代码块结束时自动释放。字段映射的规则是:rs.getXxx 里写数据库列名,Java 属性用驼峰命名,两者之间的转换集中在 Book 类里,不要在 DAO 里散落。

3.3 PreparedStatement 的类型映射:setXxx 与 getXxx 对照

进销存所有 SQL 都推荐 PreparedStatement,不推荐字符串拼接。单引号注入只是其中一个理由,更重要的是参数类型显式声明后,MySQL 能做类型校验和预编译。set 和 get 的类型映射按这张表对齐:

MySQL 列类型PreparedStatement 写参数ResultSet 读值
INT / BIGINTsetInt / setLonggetInt / getLong
VARCHAR / TEXTsetStringgetString
DECIMALsetBigDecimalgetBigDecimal
DATETIMEsetTimestampgetTimestamp
TINYINT(1)setBooleangetBoolean

DECIMAL 字段如果误用 setFloat,会造成精度波动;DATETIME 字段如果用 setString 传“2025-01-10 09:00:00”,会依赖数据库隐式转换,参数一旦带毫秒或时区格式就出错。这些对应关系也是 java 面试题里数据库部分的高频考点,把这张表记牢,读源码和做题都直接受用。

4. 图书进销存核心流程:入库、出库、盘点的 Java 事务实现

4.1 采购入库事务:写主表、明细与加库存必须原子完成

入库的完整动作是:插入采购主表、插入采购明细(可能多条)、把 book.stock 加上对应数量。三步之间任何一步失败都要整体回滚,否则会出现“单据在但库存没加”的坏账。事务边界必须由代码显式控制:

public class PurchaseService { public void purchase(PurchaseOrder order, List<PurchaseItem> items) throws Exception { Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); // 事务开始 long orderId = insertOrder(conn, order); // 写主表,取回自增 id for (PurchaseItem item : items) { insertItem(conn, orderId, item); // 逐条写明细 updateStock(conn, item.getBookId(), item.getQuantity()); // 同步库存 } conn.commit(); // 全部成功才提交 } catch (SQLException e) { conn.rollback(); // 任何一步失败整体回滚 throw e; } finally { conn.setAutoCommit(true); conn.close(); } } }

insertOrder 的实现要点是取回自增主键,后面写明细要用这个 orderId:

private long insertOrder(Connection conn, PurchaseOrder order) throws SQLException { String sql = "INSERT INTO purchase_order(order_no, supplier_id, total_amount, order_date, operator_id) " + "VALUES(?,?,?,?,?)"; try (PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, order.getOrderNo()); ps.setInt(2, order.getSupplierId()); ps.setBigDecimal(3, order.getTotalAmount()); ps.setTimestamp(4, order.getOrderDate()); ps.setInt(5, order.getOperatorId()); ps.executeUpdate(); try (ResultSet keys = ps.getGeneratedKeys()) { keys.next(); return keys.getLong(1); } } }

prepareStatement 的第二个参数 RETURN_GENERATED_KEYS 是关键,没有它 getGeneratedKeys 拿不到自增 id。这里最容易犯的错是主表和明细各拿各的连接——insertOrder、insertItem、updateStock 必须共用同一个 Connection 参数,任何一层内部重新 getConnection,事务就断了,回滚只对最后一个连接生效。

提示:事务方法内部的所有 DAO 调用必须传递同一个 Connection 对象,这是进销存源码里最隐蔽的坑。

4.2 销售出库:把库存校验写进 UPDATE 的 WHERE 条件

出库比入库多一个约束:不能超卖。新手普遍先 SELECT stock 再判断再 UPDATE,这个写法在并发下会互相覆盖——两笔销售同时读到库存 5,都判断可卖,一起扣减,库存变成负数。把校验和扣减合并成一条条件更新是最简洁的解法:

private boolean deductStock(Connection conn, int bookId, int quantity) throws SQLException { // 受影响行数为 0 表示库存不足,返回 false 触发整单回滚 String sql = "UPDATE book SET stock = stock - ? WHERE id = ? AND stock >= ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, quantity); ps.setInt(2, bookId); ps.setInt(3, quantity); return ps.executeUpdate() == 1; } }

受影响的记录数为 0,说明库存不足,销售单直接回滚并提示用户;为 1 才继续写销售主表和明细。WHERE 条件里用stock >= ?而不是stock > 0,是因为一次可以买多本,校验的是本次扣减量而不是有没有货。三种库存扣减方案的取舍:

方案写法并发安全适用场景
先查后改SELECT → 判断 → UPDATE不安全单用户课程设计勉强可用
条件更新UPDATE ... WHERE stock >= ?安全图书进销存推荐
悲观锁SELECT ... FOR UPDATE 再 UPDATE安全但阻塞高并发且扣减逻辑复杂

图书进销存这种量级,条件更新足够了。不要为了显得高级去加 FOR UPDATE,行锁会让销售录入界面在并发时互相等待,体验更差,这也是数据库面试八股文里“并发扣库存”场景最常见的标准答案。

4.3 库存盘点:盘盈盘亏的批量同步

盘点对不上账时,常规做法是建一张 stock_check 盘点表,记录 book_id、账面数、实盘数和差异数,差异经管理员确认后再同步回 book.stock:

UPDATE book b JOIN stock_check c ON b.id = c.book_id SET b.stock = c.actual_qty WHERE c.check_date = '2025-01-10';

用 UPDATE ... JOIN 批量同步,比逐条循环 UPDATE 少很多次连接往返,也避免循环到一半程序崩掉留下的中间状态。盘亏(实盘数小于账面数)要记录原因字段,一般写报损、丢失或录入错误,这个字段在报表里能直接汇总损耗。盘点建议放在业务不忙的时段执行,盘点和销售出库共用 book 表的行锁,同一本书同时被盘点和售卖时,后到的要等行锁释放。

5. 进销存报表统计、CSV 导出与验收自查

5.1 一条 SQL 生成进销存月报

报表不用在 Java 里循环汇总,一条聚合 SQL 就能算清楚订单数和毛利:

SELECT DATE_FORMAT(o.order_date, '%Y-%m') AS ym, COUNT(DISTINCT o.id) AS order_count, SUM(o.total_amount) AS sale_amount, SUM(i.quantity * (i.sale_price - b.price)) AS gross_profit FROM sale_order o JOIN sale_item i ON o.id = i.order_id JOIN book b ON i.book_id = b.id GROUP BY ym ORDER BY ym DESC;

sale_price 减 book.price 的毛利口径是简化版,book.price 在这里当作成本价;进价波动大的话,成本要换成加权平均进价,或者在 book 表里单独维护 cost_price 字段。查询结果直接喂给 JTable 渲染,再传给导出方法。

5.2 导出 CSV 加 UTF-8 BOM,Excel 打开不乱码

Java 写出的 UTF-8 CSV 用 Excel 打开会乱码,因为 Excel 对无 BOM 文件按本地编码识别。在文件开头写三字节 BOM 解决:

try (FileOutputStream fos = new FileOutputStream("monthly_report.csv")) { // UTF-8 BOM 三字节,放在文件最开头 fos.write(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}); String header = "月份,订单数,销售额,毛利\r\n"; fos.write(header.getBytes(StandardCharsets.UTF_8)); for (Object[] row : rows) { String line = String.join(",", String.valueOf(row[0]), String.valueOf(row[1]), String.valueOf(row[2]), String.valueOf(row[3])) + "\r\n"; fos.write(line.getBytes(StandardCharsets.UTF_8)); } }

换行用 \r\n 是为了兼容 Excel;书名里如果带逗号,导出前要把字段用双引号包起来,否则列会错位。带 BOM 之后 Excel 能正确识别 UTF-8,而 Notepad、IDEA 等工具打开也不受影响。

5.3 验收前按三个点自查

  • 事务回滚:把明细表 INSERT 的表名临时改错,跑一次入库,确认主表和库存都没变;
  • SQL 注入:搜索框输入' OR '1'='1,返回空列表而不是全部数据,说明 PreparedStatement 生效;
  • 中文编码:乱码按连接串 characterEncoding、建表字符集、导出 BOM 三处排查,导出的 CSV 必须用带 BOM 的格式验证一遍。

注入没防住时,优先查项目里还有没有用 Statement 拼接 SQL 的地方,这种代码在报表和导出模块里出现得最多。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 13:58:39

OFDM系统CFO频偏估计仿真:原理、MATLAB实现与验证

简介&#xff1a;面向OFDM通信系统载波频偏估计&#xff08;CFO&#xff09;研究需求的MATLAB仿真工程&#xff0c;内嵌Classen、Moose、CP三种常用频偏估计算法&#xff0c;可直观对比不同方法在频偏补偿中的性能差异&#xff0c;匹配无线通信方向学生与工程师进行算法验证、性…

作者头像 李华
网站建设 2026/9/11 13:56:55

关键词矩阵驱动的搜索语料库采集引擎设计实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 13:56:19

2026年北京商事纠纷怎么选律师?这份靠谱指南帮你避坑避雷

摘要:2025年北京法院系统受理商事一审案件连续五年保持9%以上增速,全年新收突破21万件,占全部民商事案件的55%,其中合同纠纷占比47%、公司股权类纠纷占比18%、投融资争议占比12%。进入2026年,北京商事案件收案量同比再增约18%,朝阳、海淀、西城三区法院商事庭排期普遍在4到6个月…

作者头像 李华
网站建设 2026/9/11 13:53:48

Duix.Avatar 部署指南:数字人离线克隆到出片最短路径

Duix.Avatar 部署指南&#xff1a;数字人离线克隆到出片最短路径 【免费下载链接】Duix-Avatar &#x1f680; Truly open-source AI avatar(digital human) toolkit for offline video generation and digital human cloning. 项目地址: https://gitcode.com/GitHub_Trendin…

作者头像 李华
网站建设 2026/9/11 13:51:20

Qwen3源码静态审阅:大厂开源基础设施的工程化质量

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华