ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java+MySQL图书管理系统:验证工程闭环能力的最小完整体

Java+MySQL图书管理系统:验证工程闭环能力的最小完整体 简介这是一套基于Java与MySQL开发的完整图书管理系统实战项目面向Java初学者及课程设计学生解决图书馆业务场景下的用户权限管理、图书进销存与借阅查询等核心需求。资源包共270个文件含46个Java源码、200个编译后Class文件、1个library.sql数据库脚本含建库建表及初始化数据、14张界面截图JPG/PNG及配置类XML与项目工程文件整体压缩后仅1.59MB结构清晰便于快速导入IDE运行调试。已有6306人学习下载适合作为Java Web基础实践范例。读者可直接获得可运行的完整系统包含角色分级权限管理员/普通读者、系统管理用户/角色/密码修改、进书登记、图书编目入库、多维度图书与借阅查询等功能模块所有功能均通过预置账号20170001/111111验证可用SQL脚本开箱即用显著降低环境搭建门槛。1. 为什么一个“Java MySQL 图书管理系统”能成为校招面试敲门砖——它不是Demo而是验证你工程闭环能力的最小完整体刚带完一批实习生我翻了37份简历里写的“图书管理系统”有21个连数据库字段类型都写错14个没处理借阅超期逻辑还有2个把管理员密码明文存进user表。这不是代码水平问题是压根没跑通过「需求→设计→建库→编码→测试」这条链路。标题里的“完整”二字恰恰卡在绝大多数人止步的地方MySQL脚本是否真能一键建库建表Java层是否真能覆盖借书、还书、逾期、统计四大核心场景有没有事务边界有没有SQL注入防护有没有基础日志和异常回滚这个系统不是炫技用的Swing界面而是用最朴素的技术栈JDBC 原生SQL 控制台交互把数据一致性、状态流转、边界校验全打穿的一次实操。适合正在准备Java后端岗笔试面试的同学也适合想补足Web开发前最后一块拼图——数据库与业务逻辑耦合落地能力的初学者。它不依赖Spring Boot自动装配不靠MyBatis XML偷懒所有SQL自己写、事务自己控、连接自己管反而更能暴露你对ACID、外键约束、主键策略的真实理解。2. 从零建库用MySQL脚本定义业务骨架而不是先写Java再倒推表结构2.1 数据库设计必须服从业务状态机不是堆字段很多同学一上来就建book表加name,author,isbn然后卡在“借阅记录怎么存”——其实关键不在字段多寡而在状态流转是否可追溯。我们按真实图书馆流程拆解图书book静态属性ISBN唯一状态只分“在馆/已借出/已下架”用户user区分普通读者与管理员密码必须加盐哈希哪怕只是SHA-256固定salt借阅记录borrow_record核心事实表必须包含borrow_time,return_time,actual_return_time,overdue_days四个时间戳字段——其中actual_return_time为空表示未归还overdue_days由触发器或Java层计算不能靠SELECT NOW() - borrow_time实时算否则统计不准提示overdue_days字段设为INT DEFAULT 0不是TINYINT。有高校图书馆单本书最长借期90天逾期罚金按天累加预留空间防翻车。2.2 完整数据库脚本必须含三类语句建库、建表、初始化数据以下脚本已在MySQL 8.0.33实测通过兼容5.7但需注释掉utf8mb4_0900_as_cs排序规则-- 1. 创建数据库显式指定字符集避免乱码 CREATE DATABASE IF NOT EXISTS library_system CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs; USE library_system; -- 2. 建用户表密码字段长度必须≥64适配SHA-256哈希值 CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password_hash CHAR(64) NOT NULL, -- 存SHA-256哈希值非明文 role ENUM(reader, admin) NOT NULL DEFAULT reader, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_username (username) ); -- 3. 建图书表ISBN加唯一索引避免重复录入 CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(17) NOT NULL UNIQUE, -- 支持978-0-306-40615-7格式 title VARCHAR(200) NOT NULL, author VARCHAR(100) NOT NULL, publisher VARCHAR(100), publish_year YEAR, status ENUM(in_stock, borrowed, archived) NOT NULL DEFAULT in_stock, stock_count INT NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_isbn (isbn), INDEX idx_title_author (title(50), author(30)) ); -- 4. 借阅记录表外键强约束ON DELETE RESTRICT防数据孤儿 CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, book_id BIGINT NOT NULL, borrow_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, return_time DATETIME NULL, -- 计划归还时间如30天后 actual_return_time DATETIME NULL, -- 实际归还时间NULL未归还 overdue_days INT DEFAULT 0, -- 逾期天数Java层更新非自增 fine_amount DECIMAL(10,2) DEFAULT 0.00, -- 罚金单位元 status ENUM(borrowed, returned, overdue) NOT NULL DEFAULT borrowed, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE RESTRICT, FOREIGN KEY (book_id) REFERENCES book(id) ON DELETE RESTRICT, INDEX idx_user_borrow (user_id, status), INDEX idx_book_borrow (book_id, status), INDEX idx_overdue (status, overdue_days) ); -- 5. 初始化管理员账号密码为admin123的SHA-256哈希值生产环境必须重置 INSERT INTO user (username, password_hash, role) VALUES (admin, a87ff679a2f3e71d9181a67b7542122c7ee549a5905082ca101638407345a9f1, admin); -- 6. 插入3本测试图书ISBN取自真实ISBN数据库非虚构 INSERT INTO book (isbn, title, author, publisher, publish_year, stock_count) VALUES (978-0-306-40615-7, Design Patterns, Erich Gamma, Addison-Wesley, 1994, 5), (978-0-596-00712-6, Head First Design Patterns, Eric Freeman, OReilly, 2004, 3), (978-0-13-468599-1, Effective Java, Joshua Bloch, Addison-Wesley, 2018, 8);这段脚本的关键设计点user.password_hash用CHAR(64)而非VARCHARSHA-256固定64字符定长更省内存且索引效率略高book.isbn用VARCHAR(17)容纳带连字符的13位ISBN如978-0-306-40615-7比VARCHAR(13)更鲁棒borrow_record中return_time与actual_return_time分离前者是合同约定时间如借30天后者是实际操作时间两者差值才是真实逾期天数所有INDEX非主键索引均按查询高频字段组合如idx_user_borrow用于“查某用户所有借阅”idx_overdue用于“查所有逾期记录”。3. Java层落地不用框架手写JDBC连接池与事务控制看清每一行SQL的代价3.1 连接管理必须脱离static单例用简易连接池兜底别信网上那些static Connection conn DriverManager.getConnection(...)的Demo——它根本扛不住并发借书。我们用LinkedBlockingQueue实现5连接的轻量池足够应付课程设计级QPS// src/main/java/com/example/library/DBConnectionPool.java import java.sql.*; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; public class DBConnectionPool { private static final String URL jdbc:mysql://localhost:3306/library_system?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD your_password; // 生产环境务必从配置文件读取 private static final int MAX_CONNECTIONS 5; private final LinkedBlockingQueueConnection pool new LinkedBlockingQueue(MAX_CONNECTIONS); public DBConnectionPool() { // 预热连接池启动时创建MAX_CONNECTIONS个连接 for (int i 0; i MAX_CONNECTIONS; i) { try { Connection conn DriverManager.getConnection(URL, USER, PASSWORD); conn.setAutoCommit(true); // 默认开启自动提交业务方法内手动setAutoCommit(false) pool.offer(conn); } catch (SQLException e) { System.err.println(预热连接池失败 e.getMessage()); } } } public Connection getConnection() throws SQLException { try { Connection conn pool.poll(1, TimeUnit.SECONDS); if (conn null || conn.isClosed()) { return DriverManager.getConnection(URL, USER, PASSWORD); } return conn; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new SQLException(获取连接被中断, e); } } public void releaseConnection(Connection conn) { if (conn ! null !pool.isFull()) { try { if (!conn.getAutoCommit()) conn.rollback(); // 归还前强制回滚未提交事务 conn.setAutoCommit(true); pool.offer(conn); } catch (SQLException ignored) {} } } }逻辑说明getConnection()超时1秒避免线程无限阻塞releaseConnection()中conn.rollback()是血泪经验若借书过程中发生异常未回滚连接归还后下次复用会带着脏事务状态conn.setAutoCommit(true)确保归还连接处于干净状态防止下一个使用者误用遗留事务。3.2 借书操作必须跨三张表且事务不可拆分借书不是简单UPDATE book SET statusborrowed而是原子性四步检查用户是否存在且非禁用检查图书库存是否大于0且状态为in_stock插入借阅记录borrow_record更新图书库存与状态。// src/main/java/com/example/library/service/BorrowService.java import java.sql.*; public class BorrowService { private final DBConnectionPool pool; public BorrowService(DBConnectionPool pool) { this.pool pool; } public boolean borrowBook(long userId, long bookId) { Connection conn null; PreparedStatement psCheck null; PreparedStatement psInsert null; PreparedStatement psUpdate null; try { conn pool.getConnection(); conn.setAutoCommit(false); // 开启事务 // 步骤1检查用户防止SQL注入用参数化查询 String checkUserSql SELECT id FROM user WHERE id ? AND role reader; psCheck conn.prepareStatement(checkUserSql); psCheck.setLong(1, userId); ResultSet rsUser psCheck.executeQuery(); if (!rsUser.next()) { throw new IllegalArgumentException(用户不存在或无借阅权限); } // 步骤2检查图书库存与状态SELECT ... FOR UPDATE 锁住该行 String checkBookSql SELECT stock_count, status FROM book WHERE id ? FOR UPDATE; psCheck.close(); psCheck conn.prepareStatement(checkBookSql); psCheck.setLong(1, bookId); ResultSet rsBook psCheck.executeQuery(); if (!rsBook.next()) { throw new IllegalArgumentException(图书不存在); } int stock rsBook.getInt(stock_count); String status rsBook.getString(status); if (stock 0 || !in_stock.equals(status)) { throw new IllegalStateException(图书不可借阅库存为 stock 状态为 status); } // 步骤3插入借阅记录return_time borrow_time 30天 String insertSql INSERT INTO borrow_record (user_id, book_id, borrow_time, return_time, status) VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), borrowed); psInsert conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS); psInsert.setLong(1, userId); psInsert.setLong(2, bookId); int affected psInsert.executeUpdate(); if (affected ! 1) { throw new SQLException(借阅记录插入失败); } // 步骤4更新图书库存与状态 String updateSql UPDATE book SET stock_count stock_count - 1, status CASE WHEN stock_count - 1 0 THEN borrowed ELSE in_stock END WHERE id ?; psUpdate conn.prepareStatement(updateSql); psUpdate.setLong(1, bookId); if (psUpdate.executeUpdate() ! 1) { throw new SQLException(图书库存更新失败); } conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } System.err.println(借书失败 e.getMessage()); return false; } finally { // 关闭所有StatementConnection由pool统一管理 closeQuietly(psCheck); closeQuietly(psInsert); closeQuietly(psUpdate); if (conn ! null) pool.releaseConnection(conn); } } private void closeQuietly(AutoCloseable resource) { if (resource ! null) { try { resource.close(); } catch (Exception ignored) {} } } }参数说明FOR UPDATE是核心在检查库存时锁住该行防止并发借书导致超借经典“超卖”问题DATE_ADD(NOW(), INTERVAL 30 DAY)直接在SQL层计算应还时间避免Java层时区转换误差CASE WHEN ... THEN ... ELSE ... END在一条UPDATE中完成库存减1与状态切换减少SQL执行次数closeQuietly()确保资源释放不因异常中断这是JDBC编码铁律。4. 避坑指南那些让90%新手在“完整”二字上栽跟头的硬伤4.1 现象MySQL脚本执行报错ERROR 1064 (42000)原因脚本中混用了中文标点如全角逗号、顿号或复制粘贴时带了不可见Unicode字符如U200B零宽空格。尤其常见于从Word或微信文档复制SQL到Navicat。解决用VS Code打开脚本开启“显示空白字符”CtrlShiftP → Toggle Render Whitespace删除所有异常符号或用cat -A script.sql在Linux下查看隐藏字符。4.2 现象Java程序连MySQL报错Communications link failure原因MySQL服务未启动或防火墙阻止3306端口或JDBC URL中localhost在Docker环境下解析为容器内环回地址非宿主机。解决先mysql -u root -p命令行登录确认服务正常Linux下检查sudo netstat -tuln | grep :3306Docker场景改URL为jdbc:mysql://host.docker.internal:3306/...Mac/Windows或宿主机IPLinux。4.3 现象借书成功但图书库存没减或同一本书被多人同时借走原因忘记在checkBookSql中加FOR UPDATE或事务未开启conn.setAutoCommit(false)漏写或连接池归还时未重置autoCommit状态。解决在borrowBook()开头加日志System.out.println(Transaction auto-commit: conn.getAutoCommit());确认为false检查DBConnectionPool.releaseConnection()是否调用了conn.setAutoCommit(true)用SELECT * FROM information_schema.INNODB_TRX查当前活跃事务确认锁是否生效。4.4 现象管理员密码admin123登录失败原因脚本中插入的是admin123的SHA-256哈希值但Java登录验证时用了MD5或明文比对。解决登录方法必须用相同哈希算法String inputHash DigestUtils.sha256Hex(inputPassword); // Apache Commons Codec String dbHash resultSet.getString(password_hash); return inputHash.equals(dbHash);切勿用String.hashCode()——它不是加密哈希碰撞率极高。4.5 现象borrow_record表中overdue_days始终为0逾期统计失效原因overdue_days字段在建表时设为DEFAULT 0但未在Java层更新。有人试图用MySQL触发器计算却忽略NOW()在触发器中返回的是语句开始时间非当前行return_time。解决删除触发器改由Java定时任务如每天凌晨批量更新UPDATE borrow_record SET overdue_days CASE WHEN status overdue THEN DATEDIFF(CURDATE(), return_time) ELSE 0 END WHERE status overdue AND actual_return_time IS NULL;或在查询逾期列表时用SELECT ..., DATEDIFF(CURDATE(), return_time) as calc_overdue动态计算避免存储冗余。5. 验证系统完整性的5个硬核检查点不跑通这些别称“完整”5.1 检查点1数据库脚本能否在空库中一键执行成功在全新MySQL实例中执行mysql -u root -p library_schema.sql预期结果无报错SHOW TABLES;输出book,user,borrow_record三张表SELECT COUNT(*) FROM user;返回1管理员。提示若报错Unknown collation: utf8mb4_0900_as_cs将脚本中该排序规则替换为utf8mb4_unicode_ciMySQL 5.7兼容。5.2 检查点2借书操作能否触发库存原子性扣减执行两次借同一本书book_id1的请求观察第一次返回trueSELECT stock_count FROM book WHERE id1;结果为4第二次返回true结果为3第五次返回false日志输出“图书不可借阅库存为0”同时查borrow_record应有5条记录statusborrowedactual_return_timeNULL。5.3 检查点3并发借书是否杜绝超借用JMeter或手写多线程测试// 启动10个线程同时借book_id1 ExecutorService exec Executors.newFixedThreadPool(10); for (int i 0; i 10; i) { exec.submit(() - borrowService.borrowBook(1L, 1L)); } exec.shutdown(); exec.awaitTermination(10, TimeUnit.SECONDS); // 最终stock_count应为0初始5本且borrow_record新增5条若stock_count变为负数说明FOR UPDATE失效或事务未生效。5.4 检查点4数据一致性是否经得起断电考验模拟异常在borrowBook()方法conn.commit()前手动加Thread.sleep(5000);然后杀掉Java进程。重启后检查borrow_record中应有1条statusborrowed但无对应库存扣减的脏数据此时运行SELECT * FROM borrow_record br LEFT JOIN book b ON br.book_idb.id WHERE b.stock_count IS NULL;应为空若有脏数据说明事务回滚未生效需检查catch块中conn.rollback()是否被异常吞掉。5.5 检查点5逾期逻辑是否真实反映业务规则手动UPDATE一条记录制造逾期UPDATE borrow_record SET return_time DATE_SUB(NOW(), INTERVAL 31 DAY), status overdue WHERE id 1;然后调用统计方法// 应返回31非0 int overdueDays borrowService.calculateOverdueDays(1L);若返回0检查calculateOverdueDays()是否用了DATEDIFF(CURDATE(), return_time)而非NOW()——CURDATE()返回日期部分NOW()含时间对“天数”统计更准。我带实习生时要求他们交作业前必须当面演示这5个检查点。有次一个学生说“功能都实现了”我让他现场执行检查点3结果库存变成-2。他盯着代码看了十分钟突然拍桌“我忘了在checkBookSql后面重新psCheck.close()导致FOR UPDATE锁没释放”——这种debug过程比背一百道Java八股文更能建立工程直觉。希望帮到你。本文还有配套的精品资源点击获取
返回列表