☰
Spring Boot图书管理系统实战:从建表到答辩全流程
2026/10/2 19:50:41 网站建设 项目流程

简介:面向Java开发学习者,这份基于Spring Boot的图书管理系统完整项目代码,适用于课程设计、毕业设计及企业级开发入门。系统采用Spring Boot+Vue+MyBatisPlus+MySQL技术栈,涵盖用户信息管理、图片素材管理、视频素材管理、公告信息管理等模块,完整演示了从数据库设计到前后端联调的项目流程。压缩包共743个文件,整体约19.72MB,除java源码、vue组件、js脚本、css样式、svg图标外,还包含数据库脚本、maven配置和install/run等启动批处理,便于本地快速部署运行。项目结构按后端、前端、数据库、部署脚本分层,便于按需查阅,并附有说明文档辅助理解设计思路。目前已有104人学习下载,可作为理解B/S架构、RESTful接口开发以及ElementUI界面集成的参考范本,帮助读者快速掌握图书管理场景下的业务逻辑与代码组织方式。

1. 图书管理系统不是玩具项目:Spring Boot 毕设为什么值得认真做

图书馆、课程设计、毕业设计,这三个词放一起,很多人第一反应是"又是一个老掉牙的 CRUD"。但基于 Springboot 的图书管理系统恰恰是 Java 后端入门到进阶之间最稳的跳板:需求边界清楚,又有库存、借阅状态、超期计算这些天然的业务规则,比纯增删改查能多聊出三层东西。同样是做毕设,有人交上去的是"表加查询",有人交的是"一个能讲清楚状态流转和事务边界的系统",分数差距就是这么拉开的。

这篇文章适合两类人:一类是拿它当 java 课程设计案例源码来参考的学生,想从零跑通一个完整项目再往里加自己的东西;另一类是已经会写 Spring Boot CRUD、但想搞明白借阅状态机、分页插件配置、事务失效这些"看起来会、一查全是坑"的细节的人。我会把建表、实体映射、借还书事务、超期罚金、分页配置到答辩前收尾的完整路径走一遍,所有代码照着抄就能在本地起服务。后面每一章解决一个具体问题,中途踩的坑我也会用真实案例讲清楚。

2. 先立骨架:为什么选 Spring Boot + MyBatis-Plus,以及工程怎么摆

2.1 技术栈选型的理由:Spring Boot 3.x/2.x + MyBatis-Plus 为什么是主流答案

图书管理系统在 GitHub 和各类毕设仓库里出现频率最高的组合是 Spring Boot + MyBatis-Plus + MySQL。早些年的教材还在教 SSM(Spring + Spring MVC + MyBatis),配置 XML 文件能写一屏。Spring Boot 把自动配置做掉之后,你只需要管业务代码和少量自定义配置,这对一个要在一学期里同时应付论文、答辩和找工作的学生来说,省下来的时间非常可观。

MyBatis-Plus 存在的理由是消灭单表 CRUD 的样板代码。图书、读者、借阅记录这三张核心表,增删改查全是套路,MyBatis-Plus 的 BaseMapper 直接帮你把方法生成了,你只需要写那些真正有业务规则的 SQL,比如"扣库存""查超期记录"。它的分页插件也是现成的,配合 Page 对象一行就能搞定分页查询,这也是它比原生 MyBatis 更合适做课设的原因——不是炫技,是省时间。

对比一下几套老方案:

技术栈配置成本CRUD 效率业务代码自由度答辩时能讲的东西
SSH(Struts2 + Spring + Hibernate)高,XML 多中受框架约束多基本只剩历史包袱
SSM中,要手动整合中较高讲整合过程,但内容偏老
Spring Boot + MyBatis-Plus低,自动配置高高能专注讲事务、状态、性能优化

我的建议是直接用 Spring Boot 2.x 或 3.x 都行,配合 mysql-connector-j 8.x 驱动和 MySQL 8.0 数据库。如果你所在学校的实验环境还在用 MySQL 5.7,代码层面基本不用改,只有连接 URL 里的时区参数要注意,这个我在第五章会专门讲。

2.2 用 IDEA 新建 Spring Boot 项目的三步走:避开初始化器的两个坑

常见做法是打开 IDEA 的 Spring Initializr 直接生成工程,但有两个坑。第一个坑是 Initializr 默认选择的 Spring Boot 版本可能很新,和本机 JDK 版本不匹配,启动直接报 UnsupportedClassVersionError;第二个坑是很多人勾选了 Web 依赖就忘掉数据库驱动,后面连数据库时才发现少了 jar。我一般会先确认本机 JDK 版本,再决定选哪个 Spring Boot 版本,JDK 8 就选 2.x,JDK 17 以上可以选 3.x。

项目生成之后,建议把 pom.xml 里依赖精简一下,删掉用不到的。最小依赖集合长这样:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这段配置里没有写具体版本号,是因为 Spring Boot 的父 POM 已经统一管理了版本,写版本反而可能引发冲突。Lombok 是为了省掉 getter/setter,如果你们学院不允许用 Lombok,就去掉这个依赖,自己生成方法。MyBatis-Plus 的 starter 会自动引入 MyBatis 和 Spring JDBC,不需要再额外加 spring-boot-starter-jdbc。

生成完项目,第一件事应该是先把 application.yml 配好。下面这个配置是最小可用版本:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto

注意 driver-class-name 是 com.mysql.cj.jdbc.Driver,不是老项目里的 com.mysql.jdbc.Driver,8.x 驱动里后者已经废弃。url 里必须带 serverTimezone=Asia/Shanghai,否则连接 MySQL 8.0 会直接报 CST 时区错误。log-impl 打开是为了开发时能直接在控制台看到 MyBatis 执行的 SQL,答辩前记得关掉,不然演示时满屏日志很掉价。

2.3 包结构别乱堆:按"实体-映射-服务-控制"四层摆

很多翻车项目的问题不是代码写不出来,而是所有类都堆在 controller 里,一个 Controller 文件三四百行。图书管理系统的业务虽然不复杂,但借阅、归还、续借、罚款这些规则交织在一起,不分层后期改需求会让你想骂人。我会按下面这个结构组织:

com.example.library ├── LibraryApplication.java ├── controller │ ├── BookController.java │ ├── ReaderController.java │ └── BorrowController.java ├── service │ ├── BookService.java │ ├── BorrowService.java │ └── impl │ ├── BookServiceImpl.java │ └── BorrowServiceImpl.java ├── mapper │ ├── BookMapper.java │ ├── ReaderMapper.java │ └── BorrowRecordMapper.java ├── entity │ ├── Book.java │ ├── Reader.java │ └── BorrowRecord.java ├── config │ ├── MybatisPlusConfig.java │ └── CorsConfig.java └── common ├── Result.java └── BusinessException.java

Controller 层只做参数接收和结果包装,业务判断全部放到 Service 层。mapper 层只放数据库交互,不要在 mapper 里写 if else。common 包里放统一返回体 Result 和自定义异常 BusinessException,后面借书库存不足、读者不存在这些情况都要靠它返回前端友好提示。这个结构看起来简单,但它决定了你答辩时能不能从容地说出"我的项目分层清晰"。

3. 把表建对,后面能少改 80% 的代码

3.1 五张核心表的字段设计:从图书、读者到借阅记录

图书管理系统的表结构,我见过至少三种版本。最省事的是三张表(图书、读者、借阅记录),但一旦要加分类统计就得改表;最臃肿的是七八张表,加了权限、操作日志、预约,结果自己都理不清关系。我的建议是五张表:图书分类表、图书表、读者表、借阅记录表、管理员表。再多就不适合课设体量,再少有些业务没法完整覆盖。

图书表和分类表是主从关系,借阅记录表同时关联图书和读者,这是整个系统的枢纽。下面这张表是借阅记录表的核心字段设计:

字段名类型说明
idBIGINT主键,自增
reader_idBIGINT读者 ID,外键关联读者表
book_idBIGINT图书 ID,外键关联图书表
borrow_timeDATETIME借出时间
due_timeDATETIME应还时间,借出时自动计算
return_timeDATETIME实际归还时间,空表示未还
statusTINYINT0 在借,1 已还,2 超期未还
fineDECIMAL(10,2)超期罚金,未超期为 0

3.2 建表 SQL:为什么借阅记录要单独建一个表

直接把借阅信息塞在图书表里是最常见的错误设计,比如在图书表加一个"借阅人"字段。这样做图书一次性只能被一个人借,而且历史记录全丢了。正确的做法是把借阅行为抽成独立的记录表,图书表只保留库存字段,每次借还都往借阅记录表插一行,这样同一本书的不同借阅历史都能追溯。

下面这份 SQL 是整套系统的地基,我加了注释,照着执行即可:

CREATE TABLE t_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '分类名', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '书名', author VARCHAR(50) COMMENT '作者', isbn VARCHAR(20) COMMENT 'ISBN 号', category_id BIGINT COMMENT '分类 ID', stock INT NOT NULL DEFAULT 0 COMMENT '库存数量', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id) ); CREATE TABLE t_reader ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '读者姓名', phone VARCHAR(20) COMMENT '手机号', max_borrow INT DEFAULT 5 COMMENT '最大可借数量', status TINYINT DEFAULT 1 COMMENT '1 正常,0 停用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, reader_id BIGINT NOT NULL, book_id BIGINT NOT NULL, borrow_time DATETIME NOT NULL, due_time DATETIME NOT NULL, return_time DATETIME, status TINYINT DEFAULT 0 COMMENT '0 在借,1 已还', fine DECIMAL(10,2) DEFAULT 0.00, KEY idx_reader (reader_id), KEY idx_book (book_id) ); CREATE TABLE t_admin ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );

建表时有几个细节值得注意。所有表名都加了 t_ 前缀,避免和 MySQL 保留字冲突。借阅记录表没有建立外键约束,只加了普通索引,原因是课设阶段用逻辑关联足够,物理外键会在删除测试数据时带来一堆麻烦,答辩时可以主动说"我用了逻辑外键,保证扩展性",这是一个加分回答。due_time 在插入记录时由程序计算,一般默认 30 天,不要放在 SQL 里写死,因为不同的读者类型可能允许不同的借阅时长。

3.3 实体类与 MyBatis-Plus 映射:三行注解搞定字段对应

数据库字段用下划线命名,Java 字段用驼峰命名,MyBatis-Plus 默认开启了驼峰映射,所以 book_name 会自动对应 bookName,不需要写 @TableField。只有主键和表名需要显式指定。下面以 Book 实体为例:

@Data @TableName("t_book") public class Book { @TableId(type = IdType.AUTO) private Long id; private String name; private String author; private String isbn; private Long categoryId; private Integer stock; private LocalDateTime createTime; }

@TableName 把实体类和 t_book 表绑定,@TableId(type = IdType.AUTO) 表示主键自增,和数据库的自增主键对应。剩下字段不用加注解,MyBatis-Plus 会自动把驼峰转下划线。LocalDateTime 对应数据库的 DATETIME 类型,这是 Java 8 时间类型的标准做法,别再用 java.util.Date 了,后面算超期天数会用 LocalDateTime 的 API,比 Date 好用得多。

借阅记录表的实体类同理,但有一个字段要单独处理:

@Data @TableName("t_borrow_record") public class BorrowRecord { @TableId(type = IdType.AUTO) private Long id; private Long readerId; private Long bookId; private LocalDateTime borrowTime; private LocalDateTime dueTime; private LocalDateTime returnTime; private Integer status; private BigDecimal fine; }

BigDecimal 是金额字段的标准类型,千万别用 Double 或者 Float,超期罚金的精度问题会让你在答辩时被问到哑口无言。前端展示时如果想直接看到书名和读者名,常见做法是让 VO 类里额外加两个非数据库字段,用 @TableField(exist = false) 标注,再在 Service 层手动填充,这个技巧后面分页查询时会用到。

4. 让业务转起来:借书、还书、超期与分页的核心代码

4.1 借书逻辑:扣库存和插记录必须在一个事务里

借书这个动作看起来简单,前端传一个 bookId 和 readerId,后端做三件事:查库存、减库存、插借阅记录。如果这三步没有事务保护,减库存成功但插记录失败,图书就凭空少了一本。这个 bug 在并发请求下才暴露,平时单用户测试根本发现不了。

我给出的 Service 实现是这样的:

@Service public class BorrowServiceImpl implements BorrowService { @Resource private BookMapper bookMapper; @Resource private BorrowRecordMapper borrowRecordMapper; @Override @Transactional(rollbackFor = Exception.class) public void borrowBook(Long bookId, Long readerId) { // 1. 查图书,锁定库存 Book book = bookMapper.selectById(bookId); if (book == null || book.getStock() <= 0) { throw new BusinessException("图书不存在或库存不足"); } // 2. 扣减库存 int updated = bookMapper.reduceStock(bookId); if (updated == 0) { throw new BusinessException("库存扣减失败"); } // 3. 插入借阅记录,默认借期 30 天 BorrowRecord record = new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowTime(LocalDateTime.now()); record.setDueTime(LocalDateTime.now().plusDays(30)); record.setStatus(0); record.setFine(BigDecimal.ZERO); borrowRecordMapper.insert(record); } }

注意几个关键点。@Transactional(rollbackFor = Exception.class) 里写不写 rollbackFor 差别很大,Spring 默认只对 RuntimeException 回滚,自定义的 BusinessException 如果继承的是 Exception,不加 rollbackFor 就不会触发回滚。reduceStock 是写在 BookMapper 里的一条自定义 SQL:

UPDATE t_book SET stock = stock - 1 WHERE id = #{id} AND stock > 0

这种写法比先查出来再 updateById 安全,因为 "stock > 0" 条件能防止超卖,这也是答辩时能拿出来讲的细节。updateById 是 MyBatis-Plus 自带的,但 reduceStock 这种带条件的更新必须自己写 SQL。到这里借书的核心就闭环了。

4.2 还书逻辑:超期天数与罚金的计算方式

还书比借书多一个业务点:判断是否超期,超期则计算罚金。计算规则一般有两种,一种是按自然日算,一种是按工作日算。课设阶段按自然日算就够了,规则简单,答辩好解释。罚金标准我习惯设 0.5 元/天,这个值放在常量里,方便后期改。

@Override @Transactional(rollbackFor = Exception.class) public void returnBook(Long recordId) { BorrowRecord record = borrowRecordMapper.selectById(recordId); if (record == null || record.getStatus() != 0) { throw new BusinessException("借阅记录不存在或已归还"); } LocalDateTime now = LocalDateTime.now(); record.setReturnTime(now); record.setStatus(1); // 判断是否超期 if (record.getDueTime().isBefore(now)) { long overdueDays = ChronoUnit.DAYS.between(record.getDueTime(), now); record.setFine(BigDecimal.valueOf(overdueDays).multiply(OVERDUE_FINE_PER_DAY)); } else { record.setFine(BigDecimal.ZERO); } // 更新记录并恢复库存 borrowRecordMapper.updateById(record); bookMapper.increaseStock(record.getBookId()); }

ChronoUnit.DAYS.between 是计算两个 LocalDateTime 之间天数的标准 API,between 方法直接算整数的天数差。这里有个细节:如果归还时间比应还时间晚 1 小时,between 返回 0 而不是 1,也就是说"当天还"不算超期。如果规则要求哪怕超一小时也要算一天,可以先把时间转成 LocalDate 再比较,或者用 plusDays(1) 的边界判断。增加库存的 SQL 是:

UPDATE t_book SET stock = stock + 1 WHERE id = #{id}

这个方法返回 int,如果影响行数为 0,说明图书记录被删了,理论上不该发生,但写上判断总没坏处。超期未还的记录还要单独提供一个查询接口,用 status = 2 过滤,这部分可以写成接口给前端展示"超期列表"。

4.3 分页查询:MyBatis-Plus 分页插件的正确打开方式

分页是图书管理系统列表页的刚需,MyBatis-Plus 默认的分页是假分页,如果不注册分页插件,Page 对象只会查出全部数据再内存里切,数据量一大就卡。网上很多半吊子教程没这步,导致分页看着能跑,实际上是假分页。

分页插件要先注册成 Spring Bean:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(50L); interceptor.addInnerInterceptor(pagination); return interceptor; } }

DbType.MYSQL 必须和数据库类型一致,不写或写错会导致 SQL 方言拼接错误,Oracle 和 MySQL 的分页语法完全不同。setMaxLimit(50L) 的意思是单页最大 50 条,防一手别人传一个 pageSize=999999 直接把数据库打崩,这属于防御性编程,答辩时提一句很加分。

Service 层的分页查询长这样:

@Override public IPage<BookVO> pageBooks(int pageNum, int pageSize, String keyword) { Page<Book> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), Book::getName, keyword) .orderByDesc(Book::getCreateTime); IPage<Book> bookPage = bookMapper.selectPage(page, wrapper); // 转换为带分类名称的 VO IPage<BookVO> voPage = bookPage.convert(book -> { BookVO vo = new BookVO(); BeanUtils.copyProperties(book, vo); return vo; }); return voPage; }

LambdaQueryWrapper 的 like 条件里第一个参数是布尔值,keyword 为空就不拼接这个条件,这是 MyBatis-Plus 的推荐写法,能省掉一堆 if 判断。convert 方法把查询结果从 Book 转成 BookVO,转换逻辑里可以顺便查分类名填进去。分页插件做好之后,前端只需要传 current 和 size 两个参数。

5. 避坑与排查:从启动失败到答辩翻车的五个真实案例

5.1 数据库连接失败:mysql 8 驱动与 UTC 时区问题

现象:项目启动时控制台报 communications link failure 或者 The server time zone value 'CST' is unrecognized。

原因:MySQL 8.0 之后连接校验变了,同时驱动类名从 com.mysql.jdbc.Driver 改成了 com.mysql.cj.jdbc.Driver;另外连接串里没指定 serverTimezone 时,MySQL 8 会报 CST 无法识别的错误。

解决:驱动类名改成 com.mysql.cj.jdbc.Driver,url 后面拼 ?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8。如果数据库服务器不在本机,还要检查防火墙和 bind-address 配置,这个坑在局域网部署时会碰到。

5.2 分页查不到数据或数据错乱

现象:selectPage 执行后 total 一直是 0,或者第一页和第二页数据重复。翻车原因多半是分页插件没注册,或者注册了但 DbType 写成了 OTHER。

原因:MyBatis-Plus 的物理分页依赖 PaginationInnerInterceptor,精确匹配数据库方言。没有插件时,Page 参数会被当成普通参数传递,SQL 里不会有 LIMIT 语句,查询结果会是全表,total 自然不对。

解决:确认 MybatisPlusConfig 被 @Configuration 扫描到,检查 DbType.MYSQL 是否正确。验证方法很简单:打开 log-impl 的 SQL 日志,看控制台输出的 SQL 末尾有没有 LIMIT,没有就是插件没生效。

5.3 借书事务没生效:事务自调用是隐蔽杀手

现象:强行在 bookMapper.reduceStock 之后抛一个 BusinessException,结果发现库存竟然扣掉了,记录也没回滚。

原因:@Transactional 通过 AOP 代理生效,如果你在同一个类的另一个方法里直接调用 this.borrowBook(),调用发生在代理对象内部,事务注解根本不会生效。还有一种情况是 rollbackFor 没写,BusinessException 继承的是 Exception,默认不回滚。

解决:事务方法必须从外部类调用,或者通过注入自身的代理对象调用。同时把 @Transactional(rollbackFor = Exception.class) 写全。可以在测试里故意抛异常验证,这是最朴素也最有效的事务自检方法。

5.4 前端传日期字符串,后端 LocalDateTime 解析 400

现象:还书接口用 POST 传一个 returnTime=2025-06-01 12:30:00,后端参数是 LocalDateTime,直接报解析失败。

原因:Spring 默认的 LocalDateTime 反序列化格式是 ISO 标准格式 2025-06-01T12:30:00,前端传的是带空格的格式,对不上。这个坑在前后端分离项目里几乎必现。

解决:加一个全局的 Jackson 配置类:

@Configuration public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder -> { builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); }; } }

这样前端传带空格的格式就能正常解析,返回给前端的日期也是带空格的,双方不用再为格式打架。

5.5 Vue 前端跨域报错:CORS 配置不是写在前端

现象:前端用 Vue 开发服务器访问 localhost:8080,浏览器控制台报 Access-Control-Allow-Origin 错误,企业微信或钉钉里打开的页面更是必现。

原因:跨域拦截发生在浏览器,后端没返回 CORS 响应头,前端做任何配置都解决不了。Spring Boot 里跨域要写一个过滤器或配置类,而不是在 Controller 上加 @CrossOrigin 注解一个个方法加。

解决:推荐加一个 WebMvcConfigurer 配置类:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

allowedOriginPatterns("") 配合 allowCredentials(true) 是现在的标准写法,直接用 allowedOrigins("") 会和 allowCredentials 冲突。答辩演示时如果是本地前后端分离,这一步不做,前端永远调不通接口。

6. 交付前的最后一公里:演示数据、答辩稿与二十分钟收尾技巧

系统能跑只是第一步,答辩和演示才是决定成绩的地方。我见过太多项目功能齐全但演示时手忙脚乱的案例,这里给你三个具体的收尾技巧,每天花二十分钟,连续做三天,效果立竿见影。

第一个技巧是造演示数据要"看起来真实"。不要用"书1""书2"这种测试数据,直接用真实书名,比如《深入理解Java虚拟机》《代码整洁之道》,读者起名也用"张伟""李娜"这种常见名字。演示时评委扫一眼页面,第一印象就是"这个学生认真做过"。造数据可以用一个简单的 for 循环往数据库插,也可以手写 SQL 插几十条,重点是让列表分页效果看起来饱满。

第二个技巧是准备一条"主动讲"的主线。借阅流程是必演项目,演示顺序建议是:先查一本书,确认库存大于 0;执行借阅,展示库存减一;再执行归还,展示库存恢复和借阅记录状态变化。整个过程最好控制在三分钟以内,语气不要念稿子,用"我先查一下这本书的库存,然后借出去,你们注意看库存字段的变化"这种自然表达。

第三个技巧是给系统加一个全局 XSS 过滤器。这个点很小,但很能体现安全意识。图书管理系统的书名、读者姓名都是用户输入,如果直接在页面上展示,存在存储型 XSS 的风险。常见的做法是定义一个 Filter,用 Jsoup 的 clean 方法清洗请求参数,去掉 script 标签:

@Component public class XssFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { XssHttpServletRequestWrapper wrappedRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request); chain.doFilter(wrappedRequest, response); } }

配合一个包装类重写 getParameter 和 getHeader 方法,在返回值前做去标签处理。不要小看这个过滤器,答辩时评委问"你怎么防 XSS",大多数人答不上来,你能说出 Jsoup 清洗方案,已经超过九成同行。

最后说一个我的习惯:每次部署完项目,我都会把 IDEA 的干净启动页面截图存一份,连同正常借还的录屏一起放桌面,答辩前一晚再完整走一遍流程。这不是玄学,是防止演示时数据库被改乱、服务起不来这种突发情况。这个项目值得认真投入,因为它的每段代码都能在答辩时讲出设计理由,而不是背课本。希望这份实践笔记帮到你,也祝你答辩顺利。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询