简介:本资源为基于JAVA与SQL Server的图书管理系统课程设计完整文档,面向计算机相关专业学生及需要完成数据库课程设计的学习者,帮助解决图书馆借阅者与工作人员查找书目困难、管理效率低下的问题。文档围绕Java语言程序设计、SQL Server数据库管理、数据库设计(需求分析、E-R图、逻辑与物理结构设计)、系统前后端开发与测试、JAVA与SQL Server集成等核心知识点展开,并包含课程设计报告的编写规范与评估标准。压缩包内共1个doc文件,约672KB,内容涵盖课程设计目的、进度安排、参考文献、中英文摘要及成绩评定表等完整结构,可直接作为课程设计报告模板与开发思路参考。目前已有4270人学习下载,适合需要系统梳理数据库设计流程、掌握JAVA连接SQL Server实现图书管理功能的读者借鉴使用。
1. 图书管理系统课程设计:为什么“能跑”和“能过”是两回事
每年一到学期中后段,做「基于JAVA和SQL-Server图书管理系统课程设计」的人就会扎堆。多数人第一反应是去网上找一份现成源码,改个标题、换个配色,然后祈祷答辩老师不细看。但真正做过一轮的人都知道,这类系统最要命的不是功能多,而是数据一致性和业务闭环——借书时库存没扣、还书时罚款算错、并发借同一本书直接超卖,这些才是答辩现场被追问到哑口无言的地方。
这篇笔记面向的是正在做课程设计的学生,以及需要快速交付一个可演示、可讲解、可扩展的图书管理系统的开发者。我会把整个方案拆成数据库设计、Java后端实现、事务与并发控制、常见翻车点四个层面,每一步都给出可复现的代码和参数说明。你不需要有很深的框架经验,只要会基本的Java和SQL,就能跟着把一套能经得起追问的系统搭出来。核心思路只有一句话:先把数据库约束写死,再让Java层做业务编排,最后用事务兜底。
2. 数据库先立住:SQL-Server建表与约束的硬核细节
2.1 为什么建议把业务规则下沉到数据库层
很多人做课程设计时习惯把校验全写在Java里,比如“库存大于0才能借”。但课程设计答辩时,老师经常会问一句:“如果我不走你的程序,直接连数据库插一条借阅记录,你的系统能拦住吗?”这时候如果数据库没有任何约束,你就只能尴尬地说“正常不会这么操作”。
把关键规则下沉到SQL-Server层,好处有三个:第一,任何入口的写入都会被约束拦截;第二,Java层的代码可以更专注于流程编排;第三,答辩时你可以直接演示“绕过程序直接改库会被拒绝”,这是一个很加分的点。
具体来说,图书管理系统里适合放在数据库层的规则包括:库存不能为负、同一用户对同一本书不能有两条未归还记录、借阅日期不能晚于应还日期、罚款金额不能为负。这些用CHECK约束和UNIQUE约束就能覆盖大部分场景。
2.2 核心表结构与建表脚本
下面这套表结构是我在多个课程设计中反复打磨过的版本,字段不多但够用,关键是约束写得比较完整。数据库名用LibraryDB,排序规则选Chinese_PRC_CI_AS,避免中文乱码。
-- 创建数据库(如果不存在) IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = N'LibraryDB') BEGIN CREATE DATABASE LibraryDB COLLATE Chinese_PRC_CI_AS; END GO USE LibraryDB; GO -- 图书表:库存字段带CHECK约束,防止负数 CREATE TABLE Books ( BookID INT IDENTITY(1,1) PRIMARY KEY, ISBN VARCHAR(20) NOT NULL UNIQUE, Title NVARCHAR(100) NOT NULL, Author NVARCHAR(50) NOT NULL, Publisher NVARCHAR(80) NULL, TotalStock INT NOT NULL DEFAULT 0 CHECK (TotalStock >= 0), Available INT NOT NULL DEFAULT 0 CHECK (Available >= 0), -- 可用数量不能超过总库存 CONSTRAINT CK_Books_Available CHECK (Available <= TotalStock) ); GO -- 读者表:学生和教师用Type区分 CREATE TABLE Readers ( ReaderID INT IDENTITY(1,1) PRIMARY KEY, CardNo VARCHAR(20) NOT NULL UNIQUE, Name NVARCHAR(30) NOT NULL, ReaderType TINYINT NOT NULL DEFAULT 1 CHECK (ReaderType IN (1,2)), -- 1学生 2教师 MaxBorrow INT NOT NULL DEFAULT 5, Dept NVARCHAR(50) NULL, Status TINYINT NOT NULL DEFAULT 1 CHECK (Status IN (0,1)) -- 1正常 0冻结 ); GO -- 借阅记录表:这是业务核心,约束最多 CREATE TABLE BorrowRecords ( RecordID INT IDENTITY(1,1) PRIMARY KEY, BookID INT NOT NULL, ReaderID INT NOT NULL, BorrowDate DATETIME NOT NULL DEFAULT GETDATE(), DueDate DATETIME NOT NULL, ReturnDate DATETIME NULL, Fine DECIMAL(8,2) NOT NULL DEFAULT 0 CHECK (Fine >= 0), Status TINYINT NOT NULL DEFAULT 1 CHECK (Status IN (1,2,3)), -- 1借出 2已还 3逾期未还 CONSTRAINT FK_Borrow_Book FOREIGN KEY (BookID) REFERENCES Books(BookID), CONSTRAINT FK_Borrow_Reader FOREIGN KEY (ReaderID) REFERENCES Readers(ReaderID), -- 应还日期必须晚于借出日期 CONSTRAINT CK_Borrow_Date CHECK (DueDate > BorrowDate) ); GO -- 关键索引:按读者查未还记录、按图书查借阅历史 CREATE INDEX IX_Borrow_Reader_Status ON BorrowRecords(ReaderID, Status); CREATE INDEX IX_Borrow_Book_Status ON BorrowRecords(BookID, Status); GO这段脚本里有几个点值得展开说。Available <= TotalStock这个约束看起来简单,但它能防止“还书时把可用数量加超”这种低级错误。BorrowRecords表上的两个索引不是随便加的:IX_Borrow_Reader_Status服务于“查某个读者当前借了几本书”,IX_Borrow_Book_Status服务于“查某本书当前被借出几本”,这两个查询在借书和还书流程里都会高频出现。
2.3 用触发器还是用存储过程:课程设计里的取舍
课程设计里常见的做法是用触发器自动更新库存。比如在BorrowRecords上建一个AFTER INSERT触发器,插入借阅记录后自动把Books.Available减一。这样做的好处是Java层代码简单,坏处是调试困难,一旦触发器逻辑写错,问题会藏得很深。
我的建议是:课程设计阶段优先用存储过程显式控制库存变更,触发器只用来做日志记录。原因很直接——存储过程的逻辑是可见的、可单步调试的,答辩时你也能讲清楚每一步在做什么。触发器适合做“事后记录”,比如每次借阅后往一张OperationLog表里插一条日志,这个不影响主流程,出问题也好排查。
下面是一个借书存储过程的示例,把“检查库存、检查读者状态、插入记录、扣减库存”四步放在一个事务里:
CREATE PROCEDURE sp_BorrowBook @BookID INT, @ReaderID INT, @Days INT = 30, @Result INT OUTPUT, @Message NVARCHAR(100) OUTPUT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; -- 1. 检查读者状态 IF NOT EXISTS (SELECT 1 FROM Readers WHERE ReaderID=@ReaderID AND Status=1) BEGIN SET @Result = -1; SET @Message = N'读者不存在或已被冻结'; ROLLBACK TRANSACTION; RETURN; END -- 2. 检查当前借阅数量是否超限 DECLARE @CurrentBorrow INT, @MaxBorrow INT; SELECT @CurrentBorrow = COUNT(*) FROM BorrowRecords WHERE ReaderID=@ReaderID AND Status IN (1,3); SELECT @MaxBorrow = MaxBorrow FROM Readers WHERE ReaderID=@ReaderID; IF @CurrentBorrow >= @MaxBorrow BEGIN SET @Result = -2; SET @Message = N'已达到最大借阅数量'; ROLLBACK TRANSACTION; RETURN; END -- 3. 检查库存(用UPDLOCK防止并发超借) DECLARE @Avail INT; SELECT @Avail = Available FROM Books WITH (UPDLOCK) WHERE BookID=@BookID; IF @Avail IS NULL OR @Avail <= 0 BEGIN SET @Result = -3; SET @Message = N'图书库存不足'; ROLLBACK TRANSACTION; RETURN; END -- 4. 插入借阅记录并扣减库存 INSERT INTO BorrowRecords(BookID, ReaderID, BorrowDate, DueDate, Status) VALUES(@BookID, @ReaderID, GETDATE(), DATEADD(DAY, @Days, GETDATE()), 1); UPDATE Books SET Available = Available - 1 WHERE BookID=@BookID; COMMIT TRANSACTION; SET @Result = 0; SET @Message = N'借阅成功'; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; SET @Result = -99; SET @Message = ERROR_MESSAGE(); END CATCH END GO这里最关键的一行是SELECT @Avail = Available FROM Books WITH (UPDLOCK)。UPDLOCK会在读取时对行加更新锁,防止两个并发请求同时读到相同的库存值然后都执行扣减。没有这个锁,两个用户同时借同一本只剩一本的书,就会出现库存变成-1的情况。这个坑我在早期做课程设计时踩过,答辩演示时刚好被老师撞见,场面相当尴尬。
3. Java后端落地:JDBC连接、DAO分层与事务控制
3.1 为什么课程设计用JDBC比用框架更稳
很多教程一上来就推Spring Boot + MyBatis,但对于课程设计来说,框架的配置成本和版本兼容问题往往比业务逻辑本身还耗时。我一般建议:如果课程设计周期在两周以内,用原生JDBC + 手写DAO层。这样做的好处是每一行代码你都能讲清楚,答辩时老师问“事务是怎么控制的”,你可以直接指到Connection.setAutoCommit(false)那一行,而不是含糊地说“框架帮我们管了”。
当然,JDBC的样板代码多,所以需要做一层简单的封装。下面这个DBUtil类负责连接管理和资源释放,是整个后端的地基。
import java.sql.*; public class DBUtil { // SQL-Server默认端口1433,数据库名LibraryDB private static final String URL = "jdbc:sqlserver://localhost:1433;databaseName=LibraryDB;encrypt=false"; private static final String USER = "sa"; private static final String PASSWORD = "YourStrongPassword"; static { try { Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); } catch (ClassNotFoundException e) { throw new RuntimeException("SQL-Server驱动未找到", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 统一释放资源,顺序不能错:ResultSet -> Statement -> Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (stmt != null) stmt.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }连接字符串里的encrypt=false是为了避免本地开发时证书验证的麻烦。如果你用的SQL-Server版本较新,默认会要求加密连接,加上这个参数可以省去配置证书的步骤。生产环境当然不能这么写,但课程设计阶段以能跑通为优先。
3.2 借书业务的Java实现:事务边界要画在哪里
事务边界画在哪里,是课程设计里最能体现水平的一个细节。常见错误是把setAutoCommit(false)放在DAO方法内部,然后每个DAO方法各自提交。这样做的后果是:借书流程里“插入记录”和“扣减库存”两个操作不在同一个事务里,如果扣减库存失败,借阅记录已经写进去了,数据就脏了。
正确的做法是在Service层开启事务,把多个DAO操作包在同一个Connection里。下面这个BorrowService演示了完整的借书流程:
import java.sql.*; public class BorrowService { public String borrowBook(int bookId, int readerId, int days) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务,边界在Service层 // 1. 检查读者状态和借阅上限 ReaderDAO readerDAO = new ReaderDAO(); Reader reader = readerDAO.findById(conn, readerId); if (reader == null || reader.getStatus() != 1) { conn.rollback(); return "读者不存在或已被冻结"; } int current = readerDAO.countActiveBorrows(conn, readerId); if (current >= reader.getMaxBorrow()) { conn.rollback(); return "已达到最大借阅数量"; } // 2. 检查库存并锁定行 BookDAO bookDAO = new BookDAO(); Book book = bookDAO.findByIdForUpdate(conn, bookId); if (book == null || book.getAvailable() <= 0) { conn.rollback(); return "图书库存不足"; } // 3. 插入借阅记录 BorrowDAO borrowDAO = new BorrowDAO(); borrowDAO.insert(conn, bookId, readerId, days); // 4. 扣减库存 bookDAO.decreaseAvailable(conn, bookId); conn.commit(); return "借阅成功"; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ignored) {} return "系统异常:" + e.getMessage(); } finally { DBUtil.close(conn, null, null); } } }这段代码里有三个关键点。第一,conn.setAutoCommit(false)在Service层调用,所有DAO方法接收同一个conn参数,保证在同一个事务里。第二,findByIdForUpdate方法内部执行的SQL带WITH (UPDLOCK),和前面存储过程里的锁策略一致。第三,任何一步失败都调用rollback(),并且返回具体的错误信息,方便前端展示。
对应的BookDAO.findByIdForUpdate实现如下:
public Book findByIdForUpdate(Connection conn, int bookId) throws SQLException { String sql = "SELECT BookID, Title, Available FROM Books WITH (UPDLOCK) WHERE BookID = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, bookId); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Book b = new Book(); b.setBookId(rs.getInt("BookID")); b.setTitle(rs.getString("Title")); b.setAvailable(rs.getInt("Available")); return b; } } } return null; }注意WITH (UPDLOCK)必须和事务配合使用,如果autoCommit是true,锁会在语句执行后立即释放,起不到防并发的作用。这也是为什么我一直强调事务边界要放在Service层——锁的有效范围取决于事务的范围。
3.3 还书与罚款计算:日期处理里的隐藏坑
还书流程比借书多了一个罚款计算。规则通常是:逾期每天罚0.2元,上限20元。看起来简单,但日期处理有几个坑:第一,DueDate和ReturnDate都是DATETIME类型,直接相减得到的是天数差但可能带小数;第二,如果当天还书,不应该算逾期;第三,罚款金额要保留两位小数。
下面是一个经过验证的罚款计算方法:
import java.sql.Timestamp; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; public class FineCalculator { private static final double FINE_PER_DAY = 0.2; private static final double MAX_FINE = 20.0; public static double calculate(Timestamp dueDate, Timestamp returnDate) { LocalDateTime due = dueDate.toLocalDateTime(); LocalDateTime ret = returnDate.toLocalDateTime(); // 按自然日计算,当天还书不算逾期 long overdueDays = ChronoUnit.DAYS.between(due.toLocalDate(), ret.toLocalDate()); if (overdueDays <= 0) { return 0.0; } double fine = overdueDays * FINE_PER_DAY; fine = Math.min(fine, MAX_FINE); // 保留两位小数,避免浮点误差 return Math.round(fine * 100.0) / 100.0; } }这里用ChronoUnit.DAYS.between对LocalDate做计算,而不是直接对LocalDateTime做减法,目的是忽略具体时刻,只按自然日算。比如应还日期是1月1日23:59,实际还书是1月2日00:01,按自然日算逾期1天,这是合理的。如果用LocalDateTime直接减,得到的是2分钟,会被算成0天,反而不符合业务预期。
4. 并发借书与库存超卖:三个必须验证的测试场景
4.1 用JMeter或手写线程模拟并发借书
课程设计答辩时,如果老师问“你这个系统支持多人同时借书吗”,光说“支持”是不够的,最好能现场演示。我一般会准备一个简单的并发测试类,用CountDownLatch模拟10个线程同时借同一本只剩3本库存的书,预期结果是3个成功、7个失败。
import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; public class ConcurrentBorrowTest { public static void main(String[] args) throws InterruptedException { int threadCount = 10; CountDownLatch startGate = new CountDownLatch(1); CountDownLatch endGate = new CountDownLatch(threadCount); AtomicInteger success = new AtomicInteger(0); AtomicInteger fail = new AtomicInteger(0); BorrowService service = new BorrowService(); for (int i = 0; i < threadCount; i++) { new Thread(() -> { try { startGate.await(); // 所有线程在此等待,同时起跑 String result = service.borrowBook(1, 1, 30); if ("借阅成功".equals(result)) { success.incrementAndGet(); } else { fail.incrementAndGet(); } } catch (InterruptedException ignored) { } finally { endGate.countDown(); } }).start(); } startGate.countDown(); // 发令 endGate.await(); System.out.println("成功:" + success.get() + ",失败:" + fail.get()); } }测试前先把Books表里BookID=1的Available改成3,然后运行这个测试类。如果输出是“成功:3,失败:7”,说明锁策略生效了。如果成功数大于3,说明UPDLOCK没起作用,需要检查事务是否真的开启了。
4.2 库存扣减为负的排查路径
如果并发测试出现了库存为负的情况,按以下顺序排查:
第一,确认BookDAO.findByIdForUpdate里的SQL确实带了WITH (UPDLOCK),有时候复制粘贴会漏掉。第二,确认Service层调用了conn.setAutoCommit(false),并且所有DAO方法用的是同一个conn对象。第三,检查SQL-Server的隔离级别,默认是READ COMMITTED,配合UPDLOCK足够应对课程设计级别的并发。第四,如果用了连接池,确认连接池没有把autoCommit重置为true。
提示:SQL-Server的
UPDLOCK在READ COMMITTED隔离级别下会持有锁直到事务结束,这正是我们需要的行为。不要随意把隔离级别调到READ UNCOMMITTED,那会让锁失效。
4.3 还书时库存加超的另一种超卖
除了借书时的超卖,还书时也可能出问题:如果同一个借阅记录被重复还书,库存会被加两次。防止的方法是还书前先检查记录状态,只有Status=1或Status=3的记录才能还书,还书后把状态改成2。
-- 还书存储过程的关键片段 UPDATE BorrowRecords SET ReturnDate = GETDATE(), Status = 2, Fine = @Fine WHERE RecordID = @RecordID AND Status IN (1,3); IF @@ROWCOUNT = 0 BEGIN SET @Result = -1; SET @Message = N'该记录已归还或不存在'; ROLLBACK TRANSACTION; RETURN; END UPDATE Books SET Available = Available + 1 WHERE BookID = @BookID;WHERE子句里的Status IN (1,3)加上@@ROWCOUNT判断,保证了只有第一次还书操作会生效。这个模式在课程设计里很实用,答辩时也可以作为一个“防重复提交”的亮点来讲。
5. 课程设计避坑:从环境配置到答辩演示的五个血泪教训
5.1 坑一:SQL-Server驱动版本与JDK版本不匹配
现象:Java程序启动时报NoClassDefFoundError或ClassNotFoundException,提示找不到com.microsoft.sqlserver.jdbc.SQLServerDriver。
原因:SQL-Server的JDBC驱动分多个版本,mssql-jdbc-9.x需要JDK 8以上,mssql-jdbc-12.x需要JDK 11以上。如果JDK是8却用了12.x的驱动,就会报错。
解决:JDK 8用mssql-jdbc-9.4.1.jre8.jar,JDK 11及以上用mssql-jdbc-12.4.2.jre11.jar。在IDEA里通过Project Structure的Libraries添加jar包,不要只放在lib目录却不加入classpath。
5.2 坑二:中文乱码从数据库一路乱到前端
现象:图书标题里的中文在Java程序里显示正常,但存进数据库变成问号,或者从数据库读出来变成乱码。
原因:三个环节都可能出问题——数据库排序规则不是Chinese_PRC_CI_AS、JDBC连接字符串没指定字符集、Java源文件编码不是UTF-8。
解决:建库时指定COLLATE Chinese_PRC_CI_AS;连接字符串加上;sendStringParametersAsUnicode=true;IDEA里把File Encoding全部设为UTF-8。如果已经建了库,可以用ALTER DATABASE LibraryDB COLLATE Chinese_PRC_CI_AS修改,但已有数据可能需要重新导入。
5.3 坑三:事务没回滚导致连接池耗尽
现象:程序运行一段时间后卡死,所有数据库操作都超时,重启后恢复但过一会又卡。
原因:某个异常分支里没有调用rollback(),连接带着未提交的事务被归还到连接池,下一个请求拿到这个连接后一直等锁。
解决:在catch块里无条件调用rollback(),并且用try-finally保证连接一定被关闭。如果用了连接池,在归还连接前检查autoCommit状态并重置为true。
5.4 坑四:答辩演示时数据被改乱
现象:演示借书功能时,发现库存数量不对,或者某个读者显示已借10本书但上限是5本。
原因:开发过程中手动改过数据库,或者测试脚本没有清理数据。
解决:准备一个reset.sql脚本,在答辩前执行一次,把所有表清空并插入固定的演示数据。脚本里用TRUNCATE TABLE比DELETE更快,但要注意外键约束,先删子表再删主表。
-- 答辩前重置演示数据 USE LibraryDB; GO DELETE FROM BorrowRecords; DELETE FROM Books; DELETE FROM Readers; DBCC CHECKIDENT ('BorrowRecords', RESEED, 0); DBCC CHECKIDENT ('Books', RESEED, 0); DBCC CHECKIDENT ('Readers', RESEED, 0); INSERT INTO Readers(CardNo, Name, ReaderType, MaxBorrow, Dept, Status) VALUES ('S001', N'张三', 1, 5, N'计算机系', 1), ('S002', N'李四', 1, 5, N'计算机系', 1); INSERT INTO Books(ISBN, Title, Author, Publisher, TotalStock, Available) VALUES ('978-7-111-11111-1', N'Java编程思想', N'某作者', N'某出版社', 5, 5), ('978-7-111-22222-2', N'数据库系统概论', N'某作者', N'某出版社', 3, 3); GO5.5 坑五:只测了正常流程,没测边界条件
现象:答辩时老师让演示“借一本库存为0的书”,程序直接抛异常或者插入了一条错误记录。
原因:开发时只测了库存充足的情况,没有测库存为0、读者被冻结、借阅超限这些边界。
解决:在BorrowService里对每种失败情况返回不同的错误码和提示信息,前端根据错误码展示对应的提示。测试时至少覆盖以下场景:库存为0、读者不存在、读者被冻结、借阅已达上限、同一读者重复借同一本书(如果业务不允许)。
6. 进阶技巧:用视图和窗口函数做借阅统计报表
课程设计如果只做到增删改查,分数通常在中游。想往上走一步,可以加一个“借阅统计”模块,用SQL-Server的视图和窗口函数直接出报表,Java层只负责展示。这样做的好处是统计逻辑集中在数据库层,Java代码简洁,而且答辩时能展示你对SQL的掌握程度。
先建一个视图,把借阅记录、图书信息、读者信息关联起来:
CREATE VIEW v_BorrowDetail AS SELECT br.RecordID, b.Title AS BookTitle, b.ISBN, r.Name AS ReaderName, r.CardNo, br.BorrowDate, br.DueDate, br.ReturnDate, br.Status, br.Fine, CASE WHEN br.Status = 2 AND br.ReturnDate > br.DueDate THEN N'逾期归还' WHEN br.Status = 2 THEN N'正常归还' WHEN br.Status IN (1,3) AND GETDATE() > br.DueDate THEN N'已逾期' ELSE N'借阅中' END AS BorrowState FROM BorrowRecords br JOIN Books b ON br.BookID = b.BookID JOIN Readers r ON br.ReaderID = r.ReaderID; GO然后写一个统计查询,用窗口函数算出每本书的借阅次数排名,以及每个读者的借阅总量:
-- 借阅热度排名:每本书被借次数及排名 SELECT BookTitle, COUNT(*) AS BorrowCount, RANK() OVER (ORDER BY COUNT(*) DESC) AS RankNo FROM v_BorrowDetail GROUP BY BookTitle; -- 读者借阅统计:包含当前未还数量和累计罚款 SELECT ReaderName, CardNo, COUNT(*) AS TotalBorrows, SUM(CASE WHEN Status IN (1,3) THEN 1 ELSE 0 END) AS CurrentBorrows, SUM(Fine) AS TotalFine FROM v_BorrowDetail GROUP BY ReaderName, CardNo ORDER BY TotalBorrows DESC;这两个查询可以直接在SQL-Server Management Studio里执行,也可以封装成DAO方法供Java调用。RANK()函数在SQL-Server 2005以上都支持,课程设计环境一般不会有兼容问题。如果你想让报表更直观,可以在Java层用JTable或简单的HTML表格展示,不需要引入图表库。
还有一个实用技巧:把逾期未还的读者自动冻结。可以写一个定时任务或者手动执行的存储过程,每天检查一次v_BorrowDetail里BorrowState='已逾期'且逾期超过30天的记录,把对应读者的Status改成0。这样答辩时你可以演示“逾期冻结”功能,比单纯说“支持逾期罚款”更有说服力。
CREATE PROCEDURE sp_FreezeOverdueReaders AS BEGIN UPDATE Readers SET Status = 0 WHERE ReaderID IN ( SELECT DISTINCT ReaderID FROM BorrowRecords WHERE Status IN (1,3) AND DATEDIFF(DAY, DueDate, GETDATE()) > 30 ); END GO这个存储过程可以手动执行,也可以在Java里用ScheduledExecutorService每天调一次。课程设计里手动执行就够了,但你可以把定时调用的代码也写上,作为扩展点讲。
最后说一个我自己的习惯:每次改完数据库脚本,一定先在SSMS里单独跑一遍,确认没有语法错误再集成到Java里。早期我图省事直接在Java里改SQL字符串,结果一个拼写错误排查了半小时。后来养成“SQL先在SSMS验证,再复制到Java”的习惯,效率反而高了很多。希望帮到你。
本文还有配套的精品资源,点击获取