简介:这份资源是面向计算机相关专业毕业生与Java初学者的一套图书馆管理系统毕业设计完整资料,包含论文正文与配套源码,可帮助读者解决选题、系统设计与论文撰写等实际问题。压缩包内共1个doc文件,约917KB,内容涵盖绪论、文献综述、需求分析、系统设计、实现与测试等章节,并配有图书信息表等数据库设计说明。论文围绕图书管理员与读者两类角色,详细描述了图书管理、借阅管理、读者管理等模块的功能设计,采用Java语言及相关框架完成系统实现,同时给出单元测试与集成测试的验证思路。目前已有115人学习下载,适合需要参考完整毕设结构、梳理需求建模与数据库设计流程、或对照论文框架进行二次开发的读者,也可作为课程设计选题与答辩准备的实用范本。
1. 从一份“论文+源码”的毕业设计说起:图书馆管理系统到底在做什么
每年毕业季,总有一批同学卡在同一个地方:论文写完了,系统跑不起来;或者系统能跑,论文里的“需求分析”和代码里的实现完全对不上。我见过太多“优秀毕业设计论文+源码”打包下载后,打开一看,数据库连不上、依赖缺一半、论文里的 UML 图和实际类结构是两套东西。基于 Java 的图书馆管理系统,几乎是高校毕业设计里出现频率最高的题目之一,原因很直接:业务边界清晰、数据表不多、功能模块好拆分,而且答辩时老师一听就懂。但“好懂”不等于“好做”,真正动手时你会发现,图书借阅的并发控制、逾期罚款的计算精度、读者与图书的多对多关系映射,每一个都能让新手翻车。这篇文章不聊虚的,就围绕一个能跑通、能写进论文、能通过答辩的 Java 图书馆管理系统,把技术选型、数据库设计、核心代码、论文与源码的对应关系,以及那些只有踩过才知道的坑,一次讲透。适合正在做毕设的本科生,也适合想拿一个完整项目练手的 Java 初学者。
2. 技术选型与数据库设计:为什么我劝你别一上来就上微服务
2.1 技术栈的“够用”原则:Spring Boot + MyBatis + MySQL 的组合逻辑
毕业设计不是企业级项目,评审老师看的是你能不能把业务逻辑讲清楚、代码结构是否合理、数据库设计是否符合范式。我一般会推荐这套组合:后端用 Spring Boot 2.7.x(别追最新版,很多教程和依赖还没跟上),持久层用 MyBatis 而不是 JPA,前端用 Thymeleaf 或者简单的 Vue 2 + Element UI,数据库 MySQL 5.7 或 8.0 都行。为什么是 MyBatis?因为图书馆管理系统的查询条件往往很灵活——按书名模糊查、按分类查、按借阅状态查、按读者类型查,这些动态 SQL 用 MyBatis 的<if>标签写起来比 JPA 的 Specification 直观得多,而且论文里贴 SQL 也方便解释。
Spring Boot 的版本选择有个血泪经验:如果你用的是 JDK 8,Spring Boot 2.7.x 是最后一个支持 JDK 8 的大版本;如果你装了 JDK 17,那就用 Spring Boot 3.x,但要注意javax.*包全部变成了jakarta.*,很多老教程里的代码直接复制会报错。我建议毕设统一用 JDK 8 + Spring Boot 2.7.18,这是最稳的组合,网上能搜到的解决方案也最多。
依赖方面,除了spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java,还需要加lombok(简化实体类)、pagehelper(分页查询,图书列表和借阅记录列表必用)、hutool(工具类,处理日期和字符串很方便)。别小看分页,答辩时老师大概率会问“如果有一万本书,你怎么展示”,这时候 PageHelper 就是你的标准答案。
2.2 数据库表设计:从 ER 图到建表 SQL 的落地细节
图书馆管理系统的核心表其实就五张:book(图书)、reader(读者)、borrow_record(借阅记录)、book_category(图书分类)、admin(管理员)。但每张表的字段设计都有讲究,我见过太多论文里写着“图书状态:在馆/借出”,结果代码里用int存 0 和 1,到了前端展示又要转换,平添麻烦。
下面是我常用的建表 SQL,直接可以抄:
-- 图书分类表 CREATE TABLE `book_category` ( `category_id` int NOT NULL AUTO_INCREMENT COMMENT '分类ID', `category_name` varchar(50) NOT NULL COMMENT '分类名称', `category_desc` varchar(200) DEFAULT NULL COMMENT '分类描述', PRIMARY KEY (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书分类表'; -- 图书表 CREATE TABLE `book` ( `book_id` int NOT NULL AUTO_INCREMENT COMMENT '图书ID', `isbn` varchar(20) NOT NULL COMMENT 'ISBN号', `book_name` varchar(100) NOT NULL COMMENT '书名', `author` varchar(50) DEFAULT NULL COMMENT '作者', `publisher` varchar(100) DEFAULT NULL COMMENT '出版社', `publish_date` date DEFAULT NULL COMMENT '出版日期', `price` decimal(10,2) DEFAULT NULL COMMENT '价格', `category_id` int DEFAULT NULL COMMENT '分类ID', `total_count` int NOT NULL DEFAULT 1 COMMENT '总库存', `available_count` int NOT NULL DEFAULT 1 COMMENT '可借数量', `book_status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:1在架 0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`book_id`), UNIQUE KEY `uk_isbn` (`isbn`), KEY `fk_book_category` (`category_id`), CONSTRAINT `fk_book_category` FOREIGN KEY (`category_id`) REFERENCES `book_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书表'; -- 读者表 CREATE TABLE `reader` ( `reader_id` int NOT NULL AUTO_INCREMENT COMMENT '读者ID', `reader_no` varchar(20) NOT NULL COMMENT '读者证号', `reader_name` varchar(50) NOT NULL COMMENT '姓名', `reader_type` tinyint NOT NULL DEFAULT 1 COMMENT '类型:1学生 2教师', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `max_borrow` int NOT NULL DEFAULT 5 COMMENT '最大可借数', `borrow_days` int NOT NULL DEFAULT 30 COMMENT '可借天数', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`reader_id`), UNIQUE KEY `uk_reader_no` (`reader_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='读者表'; -- 借阅记录表 CREATE TABLE `borrow_record` ( `record_id` int NOT NULL AUTO_INCREMENT COMMENT '记录ID', `book_id` int NOT NULL COMMENT '图书ID', `reader_id` int NOT NULL COMMENT '读者ID', `borrow_date` date NOT NULL COMMENT '借出日期', `due_date` date NOT NULL COMMENT '应还日期', `return_date` date DEFAULT NULL COMMENT '实际归还日期', `fine` decimal(10,2) DEFAULT 0.00 COMMENT '罚款金额', `record_status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:1借阅中 2已归还 3逾期', PRIMARY KEY (`record_id`), KEY `fk_borrow_book` (`book_id`), KEY `fk_borrow_reader` (`reader_id`), CONSTRAINT `fk_borrow_book` FOREIGN KEY (`book_id`) REFERENCES `book` (`book_id`), CONSTRAINT `fk_borrow_reader` FOREIGN KEY (`reader_id`) REFERENCES `reader` (`reader_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表';这段 SQL 里有几个关键设计点,答辩时老师很可能追问。第一,book表里同时有total_count和available_count,而不是只存一个“是否在馆”的状态。为什么?因为同一本书可能有多本复本,比如《Java编程思想》买了 5 本,借出去 2 本,还剩 3 本可借。如果只存状态,就没法处理复本场景。第二,borrow_record表里borrow_date、due_date、return_date三个日期字段分开存,而不是只存一个借阅时间然后计算。这样做的好处是查询逾期记录时可以直接用due_date < CURDATE() AND return_date IS NULL,不用在 SQL 里做日期运算,索引也能用上。第三,外键约束我建议在毕设里加上,虽然企业里经常为了性能去掉外键,但论文里体现“数据完整性”是加分项。
2.3 实体类与 Mapper 的映射:别让字段名成为你的第一个坑
数据库用下划线命名(book_name),Java 实体类用驼峰命名(bookName),这是标准做法。但 MyBatis 默认不会自动映射,你需要在application.yml里开启驼峰映射:
mybatis: configuration: map-underscore-to-camel-case: true mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.library.entity如果忘了这一行,查询结果里bookName永远是 null,而数据库里明明有值。这个坑我见过至少十个人踩过,排查半天以为是 SQL 写错了,其实就是配置少了一行。
实体类用 Lombok 的@Data注解,但注意decimal类型的price和fine要用BigDecimal,不要用double。答辩时如果老师问“为什么不用 double”,你就说“浮点数计算会有精度损失,金额必须用 BigDecimal”,这是标准答案。
3. 核心业务代码:借书、还书、逾期计算的三段实现
3.1 借书逻辑:库存扣减与并发安全的处理
借书看起来简单——查读者能不能借、查图书有没有库存、插入借阅记录、扣减库存。但如果你按这个顺序写,并发场景下一定出问题。比如两个读者同时借同一本书,都查到available_count = 1,然后都扣减,结果库存变成 -1。
我一般用两种方案。第一种是数据库乐观锁:
UPDATE book SET available_count = available_count - 1 WHERE book_id = #{bookId} AND available_count > 0然后在 Service 层判断update返回的影响行数,如果是 0,说明库存不足,抛出异常回滚。第二种是SELECT ... FOR UPDATE悲观锁,在查询图书时就锁住行。毕设里我推荐乐观锁,因为代码简洁,而且论文里可以写“通过数据库行级锁和条件更新保证并发安全”,听起来很专业。
完整的借书 Service 方法:
@Service @Transactional(rollbackFor = Exception.class) public class BorrowService { @Autowired private BookMapper bookMapper; @Autowired private ReaderMapper readerMapper; @Autowired private BorrowRecordMapper borrowRecordMapper; public void borrowBook(Integer readerId, Integer bookId) { // 1. 检查读者是否存在及可借数量 Reader reader = readerMapper.selectById(readerId); if (reader == null) { throw new BusinessException("读者不存在"); } int borrowingCount = borrowRecordMapper.countByReaderIdAndStatus(readerId, 1); if (borrowingCount >= reader.getMaxBorrow()) { throw new BusinessException("已达最大可借数量"); } // 2. 检查图书库存并扣减(乐观锁) Book book = bookMapper.selectById(bookId); if (book == null || book.getBookStatus() == 0) { throw new BusinessException("图书不存在或已下架"); } int affected = bookMapper.reduceStock(bookId); if (affected == 0) { throw new BusinessException("库存不足"); } // 3. 插入借阅记录 BorrowRecord record = new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowDate(LocalDate.now()); record.setDueDate(LocalDate.now().plusDays(reader.getBorrowDays())); record.setRecordStatus(1); borrowRecordMapper.insert(record); } }这段代码的关键在@Transactional注解和reduceStock的条件更新。@Transactional保证如果插入借阅记录失败,库存扣减会回滚。reduceStock对应的 SQL 就是上面那条带available_count > 0的 UPDATE。参数说明:reader.getMaxBorrow()默认是 5,教师可以设成 10;reader.getBorrowDays()默认 30 天,教师 60 天。这些参数在读者表里存着,不同读者类型可以灵活配置。
3.2 还书与逾期罚款:日期计算和金额精度的双重考验
还书逻辑比借书多了一个逾期判断。核心是计算due_date和实际归还日期之间的天数差,如果超过 0 天,按每天 0.5 元(或者你设定的费率)计算罚款。
public void returnBook(Integer recordId) { BorrowRecord record = borrowRecordMapper.selectById(recordId); if (record == null || record.getRecordStatus() == 2) { throw new BusinessException("借阅记录不存在或已归还"); } LocalDate returnDate = LocalDate.now(); record.setReturnDate(returnDate); // 计算逾期天数 long overdueDays = ChronoUnit.DAYS.between(record.getDueDate(), returnDate); if (overdueDays > 0) { BigDecimal fine = BigDecimal.valueOf(overdueDays) .multiply(new BigDecimal("0.5")); record.setFine(fine); record.setRecordStatus(3); // 逾期归还 } else { record.setFine(BigDecimal.ZERO); record.setRecordStatus(2); // 正常归还 } borrowRecordMapper.updateById(record); // 库存加回 bookMapper.increaseStock(record.getBookId()); }这里有个容易翻车的地方:ChronoUnit.DAYS.between计算的是两个日期之间的完整天数,如果due_date是 1 号,return_date是 2 号,结果是 1 天,正确。但如果due_date是 1 号,return_date也是 1 号,结果是 0,不罚款,也正确。但如果你用Date类的getTime()相减再除以 86400000,遇到夏令时或者跨月就会出玄学问题。所以统一用LocalDate和ChronoUnit,这是 Java 8 之后处理日期的标准做法。
罚款金额用BigDecimal的multiply方法,不要用overdueDays * 0.5,因为0.5是 double 字面量,乘出来会有精度问题。new BigDecimal("0.5")用字符串构造,才能保证精确。
3.3 查询与分页:PageHelper 的配置和三个必调参数
图书列表、借阅记录列表、读者列表都需要分页。PageHelper 的用法很简单:
public PageInfo<BookVO> queryBooks(BookQuery query, int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List<BookVO> list = bookMapper.selectByCondition(query); return new PageInfo<>(list); }但有三个参数必须注意。第一,PageHelper.startPage必须紧跟在查询方法之前,中间不能插入其他数据库操作,否则分页会作用到错误的查询上。第二,pageSize要设一个上限,比如 100,防止前端传pageSize=10000把数据库拖垮。第三,返回的PageInfo对象里包含了total、pages、list等信息,前端直接用就行,不用自己再封装。
对应的 Mapper XML 里,查询条件用<if>动态拼接:
<select id="selectByCondition" resultType="com.example.library.vo.BookVO"> SELECT b.*, c.category_name FROM book b LEFT JOIN book_category c ON b.category_id = c.category_id <where> <if test="bookName != null and bookName != ''"> AND b.book_name LIKE CONCAT('%', #{bookName}, '%') </if> <if test="categoryId != null"> AND b.category_id = #{categoryId} </if> <if test="bookStatus != null"> AND b.book_status = #{bookStatus} </if> </where> ORDER BY b.create_time DESC </select>注意LIKE CONCAT('%', #{bookName}, '%')这种写法,不要用'%${bookName}%',后者有 SQL 注入风险,答辩时老师如果问“你怎么防止 SQL 注入”,这就是你的答案。
4. 论文与源码的对应:让答辩老师一眼看到你的工作量
4.1 论文结构怎么搭:从需求分析到测试用例的完整链条
很多同学的论文和源码是两张皮,论文里写“采用三层架构”,代码里所有逻辑都堆在 Controller。答辩时老师翻到论文第 3 章“系统设计”,再打开你的 IDEA 看包结构,对不上就尴尬了。
我的建议是论文按这个结构写:第 1 章绪论(研究背景和意义,别抄太多,3000 字以内);第 2 章需求分析(用用例图展示读者、管理员两个角色,每个用例写清楚前置条件和后置条件);第 3 章系统设计(画 ER 图、架构图、类图,这里要和代码包结构一一对应);第 4 章系统实现(贴核心代码,每段代码配文字说明);第 5 章系统测试(写测试用例表,包括正常流程和异常流程);第 6 章总结与展望。
关键在第 3 章和第 4 章的对应关系。比如论文里写“系统分为 Controller、Service、Mapper 三层”,那你的代码包结构就应该是com.example.library.controller、com.example.library.service、com.example.library.mapper。论文里写“图书管理模块包含新增、修改、删除、查询四个功能”,那你的BookController里就应该有add、update、delete、query四个方法。这种对应关系不需要多高深的技术,但能让老师看到你的逻辑是自洽的。
4.2 源码目录结构:一个让老师觉得“规范”的包命名方案
我见过太多毕设源码,所有 Java 文件都堆在src/main/java下面,没有包结构。这种代码老师一看就皱眉。下面是我常用的目录结构,直接照着建就行:
src/main/java/com/example/library/ ├── LibraryApplication.java // 启动类 ├── common/ │ ├── Result.java // 统一返回结果 │ ├── BusinessException.java // 业务异常 │ └── GlobalExceptionHandler.java // 全局异常处理 ├── controller/ │ ├── BookController.java │ ├── ReaderController.java │ ├── BorrowController.java │ └── AdminController.java ├── service/ │ ├── BookService.java │ ├── ReaderService.java │ └── BorrowService.java ├── mapper/ │ ├── BookMapper.java │ ├── ReaderMapper.java │ └── BorrowRecordMapper.java ├── entity/ │ ├── Book.java │ ├── Reader.java │ └── BorrowRecord.java ├── vo/ │ ├── BookVO.java │ └── BorrowRecordVO.java └── config/ └── MyBatisConfig.javacommon包里的Result类统一返回格式,比如{code: 200, msg: "成功", data: {...}},前端处理起来方便,论文里也可以写“采用统一响应格式,便于前后端分离”。GlobalExceptionHandler用@RestControllerAdvice注解捕获全局异常,避免每个 Controller 都写 try-catch。这些细节在论文里都是加分项。
4.3 测试用例怎么写:覆盖正常流程和异常流程的表格法
论文第 5 章的测试用例,不要只写“输入正确用户名密码,登录成功”这种。要覆盖异常流程。下面是一个测试用例表的示例:
| 用例编号 | 测试功能 | 前置条件 | 输入数据 | 预期结果 | 实际结果 |
|---|---|---|---|---|---|
| TC-01 | 借书-正常 | 读者可借数未满,图书有库存 | 读者ID=1,图书ID=1 | 借阅成功,库存减1 | 一致 |
| TC-02 | 借书-库存不足 | 图书可借数为0 | 读者ID=1,图书ID=2 | 提示“库存不足” | 一致 |
| TC-03 | 借书-超限 | 读者已借5本 | 读者ID=2,图书ID=1 | 提示“已达最大可借数量” | 一致 |
| TC-04 | 还书-正常 | 借阅记录存在且未归还 | 记录ID=1 | 归还成功,库存加1 | 一致 |
| TC-05 | 还书-逾期 | 应还日期已过 | 记录ID=2 | 归还成功,计算罚款 | 一致 |
这种表格在论文里占不了多少篇幅,但能体现你考虑了边界情况。答辩时老师问“如果读者借书时库存刚好被另一个人借走了怎么办”,你就翻到 TC-02,说“系统会提示库存不足,因为扣减库存的 SQL 带了available_count > 0条件”。
5. 避坑与排查:那些让系统跑不起来的常见问题
5.1 数据库连接失败:从 URL 参数到驱动版本的排查清单
现象:启动 Spring Boot 时报Communications link failure或者Access denied for user。原因通常有三个:第一,MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,不是com.mysql.jdbc.Driver,而且 URL 要加serverTimezone=Asia/Shanghai,否则会报时区错误。第二,数据库用户名密码不对,或者该用户没有远程访问权限。第三,MySQL 服务没启动,或者端口不是 3306。
解决:先检查application.yml里的配置:
spring: datasource: url: jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver如果还连不上,用mysql -u root -p在命令行登录,执行SELECT user, host FROM mysql.user;看有没有root@localhost。如果没有,执行CREATE USER 'root'@'localhost' IDENTIFIED BY 'your_password';和GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost';。
5.2 前端页面 404:Thymeleaf 模板位置和 Controller 返回值的对应关系
现象:访问http://localhost:8080/book/list报 404,但 Controller 里明明写了@GetMapping("/book/list")。原因通常是 Thymeleaf 模板文件放错了位置。Spring Boot 默认从src/main/resources/templates/目录找模板,而且 Controller 方法返回的字符串要对应模板文件名(不带.html)。
解决:确认模板文件在src/main/resources/templates/book/list.html,Controller 方法返回"book/list"。如果用的是@RestController而不是@Controller,那返回的是 JSON 而不是页面,也会 404。检查注解有没有写错。
5.3 借阅记录查询为空:MyBatis 驼峰映射和字段别名的双重检查
现象:数据库里明明有借阅记录,但查询返回的BorrowRecordVO里bookName和readerName都是 null。原因有两个:第一,没开驼峰映射,book_name映射不到bookName。第二,SQL 里用了SELECT *,但borrow_record表里没有book_name字段,需要 JOINbook表并给字段起别名。
解决:在application.yml里加map-underscore-to-camel-case: true,然后在 Mapper XML 里写:
<select id="selectRecordVO" resultType="com.example.library.vo.BorrowRecordVO"> SELECT br.*, b.book_name AS bookName, r.reader_name AS readerName FROM borrow_record br LEFT JOIN book b ON br.book_id = b.book_id LEFT JOIN reader r ON br.reader_id = r.reader_id WHERE br.record_id = #{recordId} </select>注意AS bookName这种别名,如果驼峰映射开了,其实写AS book_name也能映射,但显式写别名更保险。
5.4 逾期罚款计算错误:日期格式和时区导致的“差一天”
现象:读者明明按时还书,系统却算了一天逾期。原因通常是due_date存的是2024-01-01,return_date存的是2024-01-01,但ChronoUnit.DAYS.between返回 0,不应该罚款。但如果你的due_date是从数据库读出来的java.sql.Date,而return_date是LocalDate.now(),两者类型不一致,比较时可能出问题。
解决:统一用LocalDate。从数据库读date类型时,MyBatis 会自动转成LocalDate(需要 JDBC 4.2+ 和 MyBatis 3.4.5+)。如果版本不够,就在实体类里把字段类型改成java.time.LocalDate,并在 Mapper XML 里用jdbcType=DATE。另外,LocalDate.now()用的是系统默认时区,如果服务器时区不对,也会差一天。在application.yml里加spring.jackson.time-zone=Asia/Shanghai和spring.jackson.date-format=yyyy-MM-dd。
5.5 打包部署后静态资源丢失:Spring Boot 的 static 目录和打包插件配置
现象:在 IDEA 里跑一切正常,打成 jar 包后 CSS、JS、图片全部 404。原因是静态资源放在了src/main/webapp目录,Spring Boot 打包成 jar 后不会包含这个目录。正确做法是把静态资源放在src/main/resources/static/目录下,Spring Boot 会自动映射。
解决:把 CSS、JS、图片移到src/main/resources/static/下,Thymeleaf 模板里用th:href="@{/css/style.css}"引用。另外,pom.xml里要加spring-boot-maven-plugin:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>打包命令用mvn clean package -DskipTests,生成的 jar 在target目录下,用java -jar xxx.jar运行。
6. 进阶技巧:让系统从“能跑”到“能拿优秀”的三个加分项
第一个加分项是操作日志。在borrow_record表之外,再加一张operation_log表,记录谁在什么时间做了什么操作。用 Spring AOP 实现,定义一个@Log注解,在 Service 方法上标注,AOP 切面里插入日志。论文里可以写“系统具备操作审计功能,满足图书馆管理的追溯需求”。代码量不大,但答辩时老师会觉得你考虑得比一般学生多。
第二个加分项是数据导出。用 EasyExcel 或者 Apache POI 把借阅记录导出成 Excel,方便管理员做统计。EasyExcel 的用法很简单:
@GetMapping("/export") public void export(HttpServletResponse response) throws IOException { List<BorrowRecordVO> list = borrowService.queryAll(); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=borrow_records.xlsx"); EasyExcel.write(response.getOutputStream(), BorrowRecordVO.class) .sheet("借阅记录") .doWrite(list); }注意BorrowRecordVO类里要用@ExcelProperty("书名")注解标注每个字段的列名。这个功能在论文里可以放在“系统特色”一节,比那些只有增删改查的系统高一个档次。
第三个加分项是定时任务。用@Scheduled注解每天凌晨跑一次,把逾期未还的记录状态更新为“逾期”,并计算罚款。这样管理员不用手动刷新,系统自动维护状态。配置@EnableScheduling在启动类上,然后写:
@Component public class OverdueTask { @Autowired private BorrowRecordMapper borrowRecordMapper; @Scheduled(cron = "0 0 1 * * ?") public void updateOverdue() { borrowRecordMapper.updateOverdueStatus(); } }对应的 SQL 是UPDATE borrow_record SET record_status = 3 WHERE due_date < CURDATE() AND return_date IS NULL AND record_status = 1。这个功能在论文里可以写“系统具备自动化逾期管理能力”,听起来就很完整。
最后说一个我自己的教训:别在答辩前一天才打包部署。我见过太多同学在 IDEA 里跑得好好的,一打成 jar 就各种报错,然后通宵排查。提前一周把 jar 包在另一台电脑上跑一遍,把数据库脚本、配置文件、启动命令写成一个 README,和源码一起提交。这样即使老师要现场运行,你也能从容应对。希望帮到你。
本文还有配套的精品资源,点击获取