简介面向高校计算机专业学生与Java初学者的一套图书馆管理系统实训项目完整工作区围绕图书信息管理、读者信息维护、借阅与归还、图书查询统计等核心模块展开可用于课程设计、实训答辩以及Java面向对象综合练习。压缩包共1988个文件、约6MB以676个xmi建模文件、128个class字节码、30个java源码以及27个xml配置、27个launch运行配置为主另含prefs偏好设置与log运行日志能较完整地还原项目在IDE中的工程结构与建模过程。已有230人学习下载。读者可以从中获得可导入IDE直接运行的工程骨架、数据库表设计与借阅业务的实现参考也可以结合借阅记录、权限校验、异常处理等模块理解MVC分层思路还能对照xmi模型梳理实体关系按需裁剪后复用到自己的实训作业中。1. 图书馆管理系统的真正难点在借阅柜台那一下实训任务发下来多数人看到「图书馆管理系统」六个字的第一反应是一张图书表的增删改查两三天能收工。真坐到电脑前第三天卡住的地方往往不是界面而是借阅柜台那一下——两个窗口同时点「借出」库存从 1 变成 -1读者还书时手抖点两下可借数超过了总藏书量应还日期拿 Date 加 30 天碰上跨月或闰年就算错一天。图书馆 图书 管理系统这道题考的是业务规则能不能被代码严格守住窗体画得漂不漂亮反而是次要的。这套方案按 Java 实训的交付标准来搭分层结构、五张核心表、借阅事务、罚金计算、验收用例。做毕设或课设的同学可以直接照这套结构交作业需要把它改造成 Web 版交付的从业者也能拿它当业务底稿。下面从需求拆解一路讲到答辩现场怎么自证系统真的能跑。2. 需求拆解与技术选型三层架构怎么切分六条业务线2.1 把「图书管理」拆成六条业务线和一个角色矩阵「图书管理系统」是个筐什么都能往里装。不先切边界最后一定做成一个所有按钮都堆在主窗体上的大杂烩。我一般按业务动作拆成六条线每条线对应一组界面和一组服务方法登录与鉴权账号、密码、角色、会话状态。管理员能进全部菜单馆员只能进借还和查询。图书档案入库、修改、下架、分类维护、按 ISBN 查重。读者档案办证、修改联系方式、挂失、注销、借阅上限与借期配置。借阅与归还核心线。谁借了什么、什么时候该还、还了没有、罚了多少钱。续借与超期处理续借次数上限、超期名单导出、罚金结算。查询统计按书名/作者/ISBN 模糊检索、按分类统计藏书量、按月份统计借阅次数。角色矩阵用一张表定死比在代码里到处写if (role.equals(admin))靠谱得多功能系统管理员馆员账号管理增删改查仅改自己密码图书档案全部入库、修改、下架读者档案全部办证、修改、挂失借阅归还可操作可操作参数配置借期、上限、罚金可改只读统计报表可看可看参数配置这一栏最容易被忽略。借期 30 天、上限 5 本、每天罚 0.1 元这三个数一旦写死在 Java 常量里验收时老师问一句「教师借期能不能长一点」就得改代码重编译。正确的做法是放进t_config表或config.properties启动时加载成内存对象界面上给出修改入口。2.2 为什么是 Java SE Swing JDBC而不是直接上框架技术选型不是越新越好是看这门课/这个交付要考什么。实训任务的考核点集中在 Java 语言本身集合、异常、IO、多线程、JDBC、面向对象。上 Spring Boot 会把大量逻辑藏进注解和自动配置里答辩时老师问「事务是怎么起的」答「加了个 Transactional 注解」是拿不到分的。三条路线摆在一起对比路线技术栈上手成本暴露给评审的能力适用场景桌面版Java SE Swing JDBC MySQL低SQL、事务、分层、并发语言类实训、课设传统 WebServlet JSP JDBC中多一层 HTTP 与会话需要浏览器演示框架 WebSpring Boot MyBatis中高框架熟练度毕业设计偏工程这道题我建议走桌面版。理由有三条Swing 省掉前端工程化注意力能全放在业务规则上手写 JDBC 逼着把事务边界想清楚这份能力换到任何框架都通用本机跑答辩现场不依赖网络和服务器少一个翻车源。JDK 用 8 或 17 都行MySQL 用 5.7 或 8.0驱动包mysql-connector-java记得和数据库版本对上。如果后续要迁 Web 版改动集中在ui包把 Swing 窗体换成 Servlet 或 Controllerservice和dao两层几乎原样搬过去。这也是分层的价值——换皮不换骨。2.3 包结构与分层职责UI 里出现一句 SQL 就是设计事故包结构定成这样后面所有代码都往这七个格子里放com.library ├── ui // 窗体、事件监听、输入校验提示 ├── service // 业务规则、事务边界、跨表编排 ├── dao // SQL 语句、结果集到实体的映射 ├── model // 实体类Book、Reader、BorrowRecord、Admin ├── util // 数据库连接、日期工具、密码哈希 ├── config // 参数加载借期、上限、罚金 └── App.java // 启动入口调用方向单向ui → service → dao谁都不许反向调ui不许 importjava.sql.*。这条规矩听起来死板但它决定了你后期能不能改。举个最常见的场景图书列表要加一列「当前可借」如果列表是直接在窗体里拼 SQL 查的这一列得改三处如果走BookService.list()改一处就行。连接管理用一个工具类收口所有 DAO 从它拿连接禁止各自DriverManager.getConnectionpublic final class DbUtil { private static String url; private static String user; private static String password; // 类加载时读一次配置避免每次取连接都读文件 static { Properties p new Properties(); try (InputStream in DbUtil.class.getResourceAsStream(/db.properties)) { p.load(in); url p.getProperty(jdbc.url); user p.getProperty(jdbc.user); password p.getProperty(jdbc.password); Class.forName(p.getProperty(jdbc.driver)); } catch (Exception e) { throw new ExceptionInInitializerError(数据库配置加载失败 e.getMessage()); } } private DbUtil() {} /** 调用方负责 close()推荐配合 try-with-resources 使用 */ public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } }db.properties外置的好处是换机器只改一个文件不动代码jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.userroot jdbc.password123456连接串里那三个参数不是装饰characterEncodingutf8决定中文书名会不会变成问号serverTimezone决定LocalDate存进去会不会差一天useSSLfalse让本机连接不弹警告。这三处是后面避坑章要反复提到的源头。service层的方法签名统一抛SQLException和自定义的BizException前者是系统故障后者是业务拒绝库存不足、超上限、读者证异常界面层分开提示——「系统繁忙请重试」和「该读者已达借阅上限 5 本」给用户的感受完全是两回事。3. 数据库设计五张表撑起借阅、库存、罚金三条主线3.1 表结构与建表语句五张表分类、图书、读者、借阅记录、账号。参数配置可以放第六张t_config也可以走 properties二选一我倾向后者省一次连库。图书表的关键在total_copies和available_copies这一对字段。前者是馆藏总量入库时定下就基本不动后者是当前可借数每借一次减一、每还一次加一。读者表的关键是max_borrow和borrow_days不同类型读者给不同配额。CREATE TABLE t_book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL, title VARCHAR(120) NOT NULL, author VARCHAR(60) NOT NULL, publisher VARCHAR(80), category_id INT NOT NULL, price DECIMAL(8,2) NOT NULL DEFAULT 0.00, total_copies INT NOT NULL DEFAULT 1, available_copies INT NOT NULL DEFAULT 1, status VARCHAR(16) NOT NULL DEFAULT ON_SHELF, -- ON_SHELF / OFF_SHELF create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_isbn (isbn), KEY idx_title (title), CONSTRAINT ck_avail CHECK (available_copies 0 AND available_copies total_copies) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_borrow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_id BIGINT NOT NULL, reader_id BIGINT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE NULL, status VARCHAR(16) NOT NULL DEFAULT BORROWED, -- BORROWED / RETURNED / OVERDUE renew_count INT NOT NULL DEFAULT 0, fine DECIMAL(8,2) NOT NULL DEFAULT 0.00, KEY idx_reader_status (reader_id, status), KEY idx_book_status (book_id, status), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES t_book(id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES t_reader(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;t_reader除了常规的证件号、姓名、电话多两个业务字段max_borrow默认 5和borrow_days学生 30、教师 60。t_admin存username、password_hash、salt、role四列密码绝不存明文具体做法在后一节讲。t_category就是 id name两级分类够用了别一开始就上树形结构。CHECK约束要 MySQL 8.0.16 以上才真正生效5.7 会静默忽略。不确定版本就靠代码守别指望数据库。3.2 冗余一个 available_copies是权衡不是偷懒有人会问可借数量为什么不直接SELECT COUNT(*) FROM t_borrow WHERE book_id? AND statusBORROWED算出来非要冗余一列因为借阅记录是只增不删的历史表一个热门书三年下来几百条记录每次打开图书列表都要为每一行做一次子查询页面会肉眼可见地卡。冗余字段是常规做法代价是一致性必须靠代码守一旦某次事务回滚没处理好库存就会和实际借出数对不上。所以配套要有一个对账 SQL做成管理员菜单里的「数据校验」按钮随时能查-- 找出库存与实际借出数不一致的图书正常应返回 0 行 SELECT b.id, b.title, b.total_copies, b.available_copies, COUNT(r.id) AS borrowed FROM t_book b LEFT JOIN t_borrow r ON r.book_id b.id AND r.status IN (BORROWED, OVERDUE) GROUP BY b.id, b.title, b.total_copies, b.available_copies HAVING b.available_copies b.total_copies - COUNT(r.id);这条 SQL 在开发阶段能帮你抓出大部分事务 bug比一行行点界面快得多。我一般在每次改完借还逻辑后都跑一遍返回空结果才算过关。3.3 约束与索引能让数据库拒的就别只靠 Java 判断三层校验各管一段界面层管格式ISBN 不能为空、手机号 11 位Service 层管业务库存够不够、读者有没有超限数据库管底线唯一、非空、外键、CHECK。只做前两层一旦有人直接连库改数据脏数据就进去了。索引就建三个t_book上title的普通索引模糊检索用前置通配符%java%用不上索引但前缀匹配java%能用t_borrow上(reader_id, status)联合索引查某读者当前在借几本走这条最快(book_id, status)同理。别给每个字段都建索引写时会拖慢实训规模的数据量下多余的索引纯属负担。外键用RESTRICT而非CASCADE。图书下架走statusOFF_SHELF逻辑删除物理删除会连带把历史借阅记录清掉统计报表直接缺行——这是验收时最容易被追问的一个点。4. 核心业务实现借阅事务、归还幂等与超期罚金4.1 借阅流程的事务边界与行锁一次借阅要动两张表往t_borrow插一条记录把t_book的可借数减一。这两步必须同生共死所以setAutoCommit(false)开事务任何一步失败整体回滚。事务边界放在 Service 层不放在 DAO——DAO 只负责单条 SQL它不知道这次操作还牵扯到别的表。并发问题是这道题的分水岭。两个借阅窗口同时操作同一本书都读到「可借 1 本」都判断通过都去减一结果库存变成 -1两个人各借走一本。解决办法是在读图书那一行时加排他锁public void borrow(long bookId, long readerId) throws SQLException, BizException { Connection conn DbUtil.getConnection(); try { conn.setAutoCommit(false); // 事务开始 // 1. 锁住图书行SELECT ... FOR UPDATE后到的事务在这里排队 Book book bookDao.lockById(conn, bookId); if (book null || !ON_SHELF.equals(book.getStatus())) { throw new BizException(图书不存在或已下架); } if (book.getAvailableCopies() 0) { throw new BizException(《 book.getTitle() 》已全部借出); } // 2. 读者状态与借阅上限校验 Reader reader readerDao.getById(conn, readerId); if (reader null || !NORMAL.equals(reader.getStatus())) { throw new BizException(读者证状态异常无法借阅); } int current borrowDao.countActive(conn, readerId); if (current reader.getMaxBorrow()) { throw new BizException(已达借阅上限 reader.getMaxBorrow() 本); } // 3. 写借阅记录应还日期由读者类型决定 LocalDate today LocalDate.now(); LocalDate due today.plusDays(reader.getBorrowDays()); borrowDao.insert(conn, bookId, readerId, today, due); // 4. 扣库存带条件更新影响行数为 0 说明被别的线程抢先了 int affected bookDao.decreaseAvailable(conn, bookId); if (affected ! 1) { throw new BizException(库存不足借阅失败); } conn.commit(); // 全部成功才提交 } catch (SQLException | BizException e) { conn.rollback(); // 任一步失败整体回滚 throw e; } finally { conn.setAutoCommit(true); conn.close(); } }配套的两条 SQL注意第二句的条件// BookDao.lockById加行锁读必须在事务内调用 String sql SELECT id, title, status, total_copies, available_copies, price FROM t_book WHERE id ? FOR UPDATE; // BookDao.decreaseAvailable条件更新把并发判断交给数据库 String sql UPDATE t_book SET available_copies available_copies - 1 WHERE id ? AND available_copies 0;FOR UPDATE和WHERE available_copies 0两道保险叠着用不是浪费。前者防同一行被两个事务同时读到旧值后者在锁失效比如有人忘了开事务时兜底让更新影响行数变成 0代码就能感知到异常。这段逻辑写对之后把库存改成 1 本、开两个客户端同时借只能成功一个库存归 0。BizException是自定义的受检异常或运行时异常专门承载业务拒绝信息。别用RuntimeException(失败)一把梭界面层没法区分该弹红色警告还是提示重试。4.2 归还与续借幂等判断怎么写归还要处理的核心是重复提交。读者或馆员手快点两下「确认归还」如果代码只会无脑执行库存就加两次最终超过馆藏总量。所以第一步先查记录状态第二步更新时再带一次状态条件双保险public BigDecimal returnBook(long borrowId) throws SQLException, BizException { Connection conn DbUtil.getConnection(); try { conn.setAutoCommit(false); BorrowRecord r borrowDao.lockById(conn, borrowId); // 同样加行锁 if (r null) { throw new BizException(借阅记录不存在); } if (RETURNED.equals(r.getStatus())) { throw new BizException(该记录已归还请勿重复操作); // 幂等拦截 } LocalDate back LocalDate.now(); BigDecimal fine FineCalculator.calc(r.getDueDate(), back, r.getBookPrice()); // 更新语句自带状态条件影响行数必须是 1 int affected borrowDao.markReturned(conn, borrowId, back, fine); if (affected ! 1) { throw new BizException(归还状态已变更请刷新后重试); } bookDao.increaseAvailable(conn, r.getBookId()); conn.commit(); return fine; } catch (SQLException | BizException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }-- BorrowDao.markReturned状态条件保证只有第一次执行会生效 UPDATE t_borrow SET status RETURNED, return_date ?, fine ? WHERE id ? AND status IN (BORROWED, OVERDUE)续借的逻辑简单一些但要卡两个上限续借次数不超过 2 次且新应还日期从「原应还日期」往后推不是从今天推。从今天推会让读者通过反复续借无限延期这是业务上的漏洞。4.3 超期罚金与日期计算全部换成 LocalDate罚金规则定成逾期每天 0.1 元罚金上限为书价的 50%避免一本几十块的书罚出天价。日期一律用java.time.LocalDate不用java.util.Date——后者带时分秒算天数差要先归一化跨时区和闰年特别容易出错。public final class FineCalculator { private static final BigDecimal PER_DAY new BigDecimal(0.10); // 每日罚金 private static final BigDecimal CAP_RATE new BigDecimal(0.50); // 罚金上限比例 private FineCalculator() {} /** * param due 应还日期 * param back 实际归还日期 * param price 图书定价用于计算罚金上限 */ public static BigDecimal calc(LocalDate due, LocalDate back, BigDecimal price) { long days ChronoUnit.DAYS.between(due, back); if (days 0) { return BigDecimal.ZERO; // 未超期或当天归还 } BigDecimal fine PER_DAY.multiply(BigDecimal.valueOf(days)); BigDecimal cap price.multiply(CAP_RATE).setScale(2, RoundingMode.HALF_UP); return fine.min(cap).setScale(2, RoundingMode.HALF_UP); } }ChronoUnit.DAYS.between直接给出两个LocalDate之间的整天数跨月、跨年、闰年都不用自己判断。金额一律BigDecimal别用double——0.1 在二进制里是无限循环小数累加十次会得到 0.9999999999999999账面上差一分钱也是错。超期状态靠一个定时任务或启动时扫描刷新把due_date CURDATE()且statusBORROWED的记录改成OVERDUE。实训规模下不需要真起定时器程序启动时执行一次UPDATE就够但你要能说清楚这个设计意图。4.4 分页查询与模糊检索图书列表一定要分页一页 20 条。数据量小的时候不分页看不出问题答辩时导入一份几百本的测试数据一次性查全表再往 JTable 里塞界面会明显卡顿。-- 分页 模糊检索? 依次是 keyword、keyword、keyword、limit、offset SELECT id, title, author, isbn, available_copies, total_copies, status FROM t_book WHERE status ON_SHELF AND (title LIKE ? OR author LIKE ? OR isbn LIKE ?) ORDER BY id DESC LIMIT ? OFFSET ?Java 侧拼参数时注意通配符要自己加并且对用户输入里的%和_做转义否则输入一个%就变成全表匹配String kw % keyword.replace(\\, \\\\) .replace(%, \\%) .replace(_, \\_) %; int offset (page - 1) * pageSize; // ps.setString(1, kw); ps.setString(2, kw); ps.setString(3, kw); // ps.setInt(4, pageSize); ps.setInt(5, offset);另配一条SELECT COUNT(*)用同样的WHERE条件算总数用来生成页码。两条 SQL 的条件必须完全一致否则会出现「显示 3 页但翻到第 3 页是空列表」的尴尬。条件复杂时用StringBuilder拼一个公共的 where 片段两处复用。5. 避坑与排查五个最容易在演示当天翻车的点5.1 借阅与库存相关的三个坑坑一可借数量变成负数或者 5 本库存借出 6 本。现象是连续点两次借阅按钮或者开两个窗口操作同一本书库存字段出现 -1。原因是「先查再改」之间没有锁两个事务读到同一份旧值各自判断「还有库存」然后各自减一。解决方式是 4.1 节那套组合事务内SELECT ... FOR UPDATE锁行扣减语句带AND available_copies 0并检查影响行数表上再挂CHECK约束兜底。三样缺一样都可能漏。坑二还一本书库存加了两次可借数超过馆藏总量。现象是读者点「确认归还」时手抖或界面卡顿重复提交available_copies从 3 变成 5而total_copies只有 4。原因是更新语句没有状态条件同一行被更新了两次。解决办法是把状态判断写进 SQL 的WHERE里status IN (BORROWED,OVERDUE)影响行数等于 1 才去加库存界面上的归还按钮点击后立即置灰防住第一层。坑三删掉一本旧书历史借阅统计里那几条记录凭空消失。现象是管理员在图书档案里点删除之前的借阅记录跟着没了月度报表数字对不上。原因是用了物理DELETE外键又是ON DELETE CASCADE。正确做法是status改成OFF_SHELF做逻辑下架下架后不出现在借阅列表的检索结果里但历史记录和外键都保留。外键声明改成RESTRICT从数据库层面禁止误删。5.2 环境、编码与演示环节的两个坑坑四本机跑得好好的换台机器书名全变成问号。现象是图书入库时中文正常换一台电脑打开列表里全是???。原因有两个可能连接串没写characterEncodingutf8或者建表时没指定CHARSETutf8mb4用了服务器默认的 latin1。排查顺序是先看表结构SHOW CREATE TABLE t_book确认末尾是utf8mb4再看db.properties里三个参数是否齐全。两处都对了还乱码检查 IDE 的文件编码是不是 UTF-8。坑五答辩现场双击 jar 包报 Communications link failure。现象是在自己电脑上稳如泰山到教室打开就连接失败。原因通常是配置写死了本机路径、依赖本机已启动的 MySQL 服务、或者数据库里只有空表没有演示数据。规避方式是三件事配置外置成db.properties并放在 jar 同级目录准备一份init.sql包含建表语句和至少 30 本图书、10 个读者、若干条借阅记录的种子数据现场导入提前 30 分钟到场在目标机器上完整跑一遍借书—还书—超期三条链路。演示环境的问题代码写得再好也救不回来。6. 验收与答辩用一张用例表证明系统真的能跑功能做完不等于能交。我习惯在提交前跑一遍固定用例逐条勾勾不满就不提交。这套用例覆盖了业务规则里所有「边界」也是老师最爱抽查的地方编号用例前置条件预期结果TC-01正常借阅库存 3读者未超限库存变 2记录 BORROWED应还日期 今天 借期TC-02零库存借阅可借数为 0提示「已全部借出」库存不变不写记录TC-03超上限借阅读者已借满 5 本拒绝并提示上限不写记录TC-04正常归还一条 BORROWED 记录状态 RETURNED库存 1罚金 0TC-05重复归还TC-04 后再次归还提示已归还库存不变TC-06超期归还应还日期改为 10 天前罚金 1.00 元0.10 × 10TC-07罚金封顶逾期 300 天书价 40 元罚金 20.00 元书价 50%TC-08并发借阅库存 1两窗口同时借仅一个成功库存 0TC-09模糊检索分页导入 200 条数据按 20 条/页返回总数正确TC-10数据对账任意时刻3.2 节的对账 SQL 返回 0 行TC-08 这条最值得在答辩时主动演示。开两个客户端把库存调成 1同时点借阅一个成功一个提示库存不足比任何口头解释都有说服力。老师问「你怎么保证并发安全」直接跑一遍再指一下FOR UPDATE和条件更新那两行代码。答辩常见的问题还有几个提前准备好答案为什么库存要冗余字段见 3.2讲清楚用空间换查询效率代价靠事务和对账 SQL 守住事务边界为什么在 Service 层DAO 只管单条 SQL跨表编排属于业务如果改成 Web 版要动哪里只换ui包service/dao原样迁密码为什么不能明文存t_admin存password_hash和salt用PBKDF2WithHmacSHA256加盐迭代登录时用同样盐值算一遍比对别用 MD5。那套用例表和init.sql从那以后我每次带实训都要求学生在提交前把数据库删干净、用脚本从零重建、再逐条跑一遍。跑不通的问题八成在环境不在代码跑得通的答辩时心里有底。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?