
每年到毕业设计选题SpringBoot图书管理系统这个组合总会出现在很多院校的题目清单里。它听上去“烂大街”但恰恰是这种看起来基础、做起来完整的题目最能让新手完整走一遍Java Web项目的流程需求分析、数据库建模、后端接口、联调测试、部署演示。站在从业者的角度看它适合作为第一个完整的SpringBoot实战项目也适合用来把SSM时代的老知识点重新串一遍。这篇不打算讲标准化课件的套话而是把自己做这个项目以及带别人做这个项目过程中踩过的坑、总结出来的经验直接摆出来从技术选型、数据表设计讲到核心模块代码、常见报错和答辩准备。无论你是准备拿它交课程设计还是想用SpringBoot练手都可以对照着搭。现在很多地方把这类题目包装得花里胡哨一套代码卖来卖去其实核心业务就那些管书、管人、管借还。先把地基打好后面想加Redis、Vue、Docker都是顺手的事。1. 项目整体设计与技术选型思路1.1 技术选型背后的取舍先说结论后端用SpringBoot持久层用MyBatis-Plus数据库用MySQL前端如果不想写太复杂就用Bootstrap加Thymeleaf或者直接做前后端分离用Vue3。这个组合在毕业设计里足够稳也是现在很多中小型管理系统的常见组成。选SpringBoot的原因大家都清楚但有个问题很多人会忽略版本。现在新项目用到SpringBoot 3.x已经比较普遍它要求JDK 17以上。如果你的电脑还是JDK 8要么升级环境要么退回到2.7版本。从课程设计角度两者都能出东西但我的建议是直接上3.x代码更贴近企业现状也省得答辩时被问“为什么还在用旧版本”。这里就有个现实痛点IDEA里通过Spring Initializr创建项目经常超时这个问题我在第4部分专门说两种解决办法。持久层为什么选MyBatis-Plus而不是纯MyBatis理由有两个一是内置分页插件和条件构造器能省掉大量重复的XML和Mapper代码特别适合一个人短时间写完整个系统二是LambdaQueryWrapper写起来跟原生SQL的语义差不多不容易出拼字符串的错。但我的态度是会用不等于依赖底层SQL还是要看得懂。否则答辩时老师问一句“你的分页是怎么实现的”你只说“用了插件”是不够的至少要知道LIMIT offset, pageSize的原理。1.2 功能模块怎么划分图书管理系统听起来简单但把功能拆清楚还是有一套逻辑的。我按通用管理系统的思路拆成下面这几个模块模块核心功能涉及数据表登录鉴权管理员登录、退出、密码加密admin图书档案图书增删改查、模糊搜索、分类筛选book、category分类管理图书分类的维护category读者管理读者档案、借书证号、冻结与解冻reader借阅管理借书、还书、续借、逾期标记borrow_record统计报表热门图书、借阅趋势、分类占比borrow_record、book、category每个模块为什么要单独拆出来背后的逻辑是“高内聚、低耦合”。比如读者管理只管读者状态跟借阅记录通过reader_id关联这样后面接权限控制或者扩展读者类型时不需要改动借阅逻辑。很多同学一上来就设计十几张表最后自己都理不清关系。我的建议是宁少勿多先把这五张表的核心关系跑通再考虑要不要拆。1.3 单体应用还是前后端分离这是很多人在动工之前纠结最久的问题。其实答案很简单看你还有多少时间。如果用Thymeleaf或者Bootstrap模板后端渲染页面所有东西都在一个SpringBoot工程里开发周期短演示也方便部署就是打一个jar包。这个方案对课程设计完全够重点能放在后端的业务逻辑上。如果题目强制要求前后端分离或者你想多展现一点前端能力Vue3加Element Plus搭配SpringBoot也成熟。但代价是要多写接口文档、处理跨域、联调工作量会明显增加。我的建议是除非你本身对Vue已比较熟或者时间很充裕不然第一次做这个题目先走单体会更稳。把后端逻辑写透比套一堆前端组件更能体现真实水平。后面有精力了再按第5部分的方式拆成前后端分离不会太痛苦。2. 核心数据表设计与关键字段2.1 表与字段的设计思路图书管理系统的核心实体是图书、读者、借阅记录再加上分类和管理员五张表就够。先看整体关系一本图书属于一个分类一个读者可以借多本书一次借阅会生成一条借阅记录管理员负责维护这些数据并在借还时操作记录。关于表之间用不用物理外键这里多说一句。很多教科书喜欢加FOREIGN KEY但实际项目尤其是并发量上来以后物理外键会让写入变慢迁移数据也麻烦所以现在主流做法是保留“逻辑外键”也就是表中存book_id、reader_id这些字段在业务代码里保证它们的关联正确。课程设计用逻辑外键还有一个好处删除数据时不会遇到“有外键约束不能删”的连环报错能省很多无谓的时间。2.2 建表SQL与约束直接给一套能跑通的建表语句表名前缀不用加简单清晰最重要。字符集统一utf8mb4避免中文乱码。CREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library_db; CREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名称, parent_id BIGINT DEFAULT 0 COMMENT 父分类ID0为顶级, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT 图书分类表; CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE COMMENT ISBN编号, name VARCHAR(200) NOT NULL COMMENT 书名, author VARCHAR(100) COMMENT 作者, publisher VARCHAR(200) COMMENT 出版社, category_id BIGINT COMMENT 分类ID, price DECIMAL(10,2) DEFAULT 0, total INT NOT NULL DEFAULT 0 COMMENT 馆藏总数量, stock INT NOT NULL DEFAULT 0 COMMENT 当前可借库存, cover_url VARCHAR(255) COMMENT 封面地址, status TINYINT DEFAULT 1 COMMENT 1在馆 0下架, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除标记, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_name (name) ) COMMENT 图书表; CREATE TABLE reader ( id BIGINT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) NOT NULL UNIQUE COMMENT 借书证号, name VARCHAR(50) NOT NULL, gender TINYINT COMMENT 1男 2女, phone VARCHAR(20), email VARCHAR(100), status TINYINT DEFAULT 1 COMMENT 1正常 0冻结, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 读者表; CREATE TABLE admin ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL COMMENT BCrypt加密存储, real_name VARCHAR(50), role VARCHAR(20) DEFAULT ADMIN, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 管理员表; CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_id BIGINT NOT NULL, book_name VARCHAR(200) NOT NULL COMMENT 借阅时书名快照, isbn VARCHAR(20) COMMENT 借阅时ISBN快照, reader_id BIGINT NOT NULL, reader_name VARCHAR(50) NOT NULL COMMENT 读者姓名快照, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP, due_time DATETIME NOT NULL COMMENT 应还时间, return_time DATETIME COMMENT 实际归还时间, status TINYINT DEFAULT 0 COMMENT 0借阅中 1已归还 2逾期未还, INDEX idx_reader (reader_id), INDEX idx_book (book_id), INDEX idx_due_time (due_time) ) COMMENT 借阅记录表;这里几个地方值得注意book表的isbn加了唯一索引因为同一本书的ISBN理论上是唯一的reader表的card_no也唯一因为借书证号就是读者的业务主键borrow_record表里的book_id、reader_id建了普通索引是为了按读者查历史、按图书查借阅时更快。2.3 让历史数据不丢“快照”的小设计borrow_record表里我特意冗余了book_name、isbn和reader_name这个设计看起来很“多余”但实际很关键。你可以想象一个场景一本书被归还后管理员把书名改了或者把图书下架了如果没有快照历史借阅记录里只有一串读不懂的book_id报表统计时根本不知道当时借的到底是什么读者改名也是同理。冗余这3个字段后无论图书和读者信息怎么变历史借阅记录都能还原出当时的业务事实。这类冗余在数据库设计里叫“业务快照”在很多订单类系统和财务系统里是标配。答辩时如果老师问“为什么borrow_record表里有重复字段”你能把快照理念讲清楚是非常加分的回答。还有一个好处是统计报表时可以直接查borrow_record不用每次回表去关联book和reader查询效率也会高一些。3. 项目搭建与核心功能模块实现3.1 初始化一个SpringBoot项目别在环境上浪费一天创建项目的痛点在前面提到过IDEA自带的Spring Initializr经常连不上官方地址。我常用的方案是直接改IDEA里的Server URL换成阿里云的镜像地址也就是https://start.aliyun.com。填好Group和Artifact依赖勾选Spring Web、MySQL Driver、LombokJDK版本选17或21创建完之后在pom里手动加MyBatis-Plus和Hutool的依赖这样比卡在转圈界面干等舒服得多。pom里最核心的依赖大概这样dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version /dependency如果用的是SpringBoot 3.xMySQL驱动的groupId是com.mysql老资料里写mysql-connector-java的在新版本里已经调整了注意区分。启动类上记得加MapperScan扫描Mapper接口所在的包。application.yml里的核心配置也直接给出来server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 你自己的密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0URL里的几个参数别删掉尤其是serverTimezone和characterEncoding这是后面不会乱码、不报时区错误的前提。allowPublicKeyRetrievaltrue是MySQL 8连接时如果不加容易报公钥检索错误加上省心。3.2 登录鉴权Session还是JWT我一直认为课程设计用Session就够了但如果你想把项目写得更有“企业级”感觉或者想让简历上有亮点用JWT加拦截器会是更好的选择。核心流程不复杂用户登录时校验用户名密码成功就签发一个JWT返回前端前端请求时放在Authorization头里后端写一个拦截器把需要登录的接口拦住校验token有效就放行顺便把当前登录人信息放进请求上下文。JWT的好处是无状态后端不用存session对水平扩展友好。对图书管理系统来说其实有点“杀鸡用牛刀”但回答“为什么用JWT”时能讲出无状态、适合前后端分离、天然支持多端登录这就够了。我习惯用jjwt库0.12版本的写法略有变化建议直接按官方文档来。一个简化版的JWT工具类可以长这样public class JwtUtil { private static final SecretKey KEY Keys.hmacShaKeyFor(你的秘钥长度至少32位.getBytes()); public static String createToken(Integer adminId, String username) { return Jwts.builder() .subject(username) .claim(id, adminId) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() 1000L * 60 * 60 * 24)) .signWith(KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .verifyWith(KEY) .build() .parseSignedClaims(token) .getPayload(); } }用的时候在Controller层通过HandlerMethodArgumentResolver或者拦截器里手动解析都行课程设计不用追求框架化够用就行。密码存储一定要用BCrypt加密Spring Security里单独提供BCryptPasswordEncoder但如果没引入Security用Hutool的BCrypt也能实现别明文存密码。3.3 图书管理的CRUD与分页搜索图书管理是系统里最基础的模块本质上就是对一张表做增删改查再配合分页和模糊搜索。MyBatis-Plus最省事的地方在条件构造器比如搜索接口可以这样写public PageBook searchBooks(int page, int size, String keyword, Long categoryId) { PageBook p new Page(page, size); LambdaQueryWrapperBook wrapper new LambdaQueryWrapper(); wrapper.and(StringUtils.hasText(keyword), w - w .like(Book::getName, keyword) .or() .like(Book::getAuthor, keyword)) .eq(categoryId ! null, Book::getCategoryId, categoryId) .orderByDesc(Book::getCreateTime); return bookMapper.selectPage(p, wrapper); }注意这里有个细节书名和作者两个条件用or连接时要用wrapper.and把里面的逻辑包起来否则SQL拼接出来可能是keyword and a or b这种错乱条件。条件构造器虽然方便但拼接逻辑很容易翻车写完最好把日志里的SQL打出来看一眼。MyBatis-Plus的配置里我开了StdOutImpl日志就是方便开发时直接观察生成SQL的。分页插件的注册也不要漏否则selectPage会查出全部数据再在内存里“假分页”数据量一大就出问题。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }3.4 借阅与归还事务让流程不出错借阅和归还是整个系统里最核心、最容易出问题的部分。借书不是简单insert一条记录它包含两个动作扣减图书库存、生成借阅记录。这两个动作必须在一个事务里否则就会出现“记录生成了库存没扣”或者反过来库存负数但记录没有。我见过好几个项目因为漏加Transactional跑一段时间后库存数据全乱了。借书的Service核心逻辑大概这样Transactional(rollbackFor Exception.class) public void borrow(Integer bookId, Integer readerId) { Book book bookMapper.selectByIdForUpdate(bookId); if (book null || book.getStock() 0) { throw new BusinessException(图书不存在或库存不足); } Reader reader readerMapper.selectById(readerId); if (reader null || reader.getStatus() ! 1) { throw new BusinessException(读者状态异常无法借阅); } book.setStock(book.getStock() - 1); bookMapper.updateById(book); BorrowRecord record new BorrowRecord(); record.setBookId(bookId); record.setBookName(book.getName()); record.setIsbn(book.getIsbn()); record.setReaderId(readerId); record.setReaderName(reader.getName()); record.setBorrowTime(LocalDateTime.now()); record.setDueTime(LocalDateTime.now().plusDays(30)); record.setStatus(0); borrowRecordMapper.insert(record); }selectByIdForUpdate是我自定义的一个查询对应的Mapper里用select ... for update把这一行数据锁住。目的是防止两个人同时借同一本书时都读到库存还剩1本结果都走到减库存变成负数。关于并发控制和锁的细节第4部分会专门展开。归还的逻辑正好相反把借阅记录的状态改成已归还填上return_time再把图书的stock加回去同样要在一个事务里。要注意根据recordId去查记录而不是凭前端传的bookId去还否则还错书的风险很高。归还时如果发现已经超过due_time可以顺手把status标记为2或者让报表查询时动态判断。3.5 统计报表把数据变成图表统计报表是很多同学最后才补的模块但它恰恰是最能在答辩时“撑场面”的部分。数据量不大时直接写SQL聚合就行。近30天借阅趋势就是一个按天分组SELECT DATE(borrow_time) AS day, COUNT(*) AS count FROM borrow_record WHERE borrow_time DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY DATE(borrow_time) ORDER BY day;热门图书Top5就是按图书分组计数SELECT book_id, book_name, COUNT(*) AS borrow_count FROM borrow_record GROUP BY book_id, book_name ORDER BY borrow_count DESC LIMIT 5;分类借阅占比需要联表SELECT c.name AS category_name, COUNT(b.id) AS count FROM borrow_record b JOIN book bk ON b.book_id bk.id JOIN category c ON bk.category_id c.id GROUP BY c.id, c.name;前端展示推荐用ECharts柱状图、饼图都简单后端把查询结果转成JSON返回就行。注意一个细节日期分组查询出来的day是java.sql.Date转JSON时格式要控制好用之前配置的spring.jackson.date-format统一成字符串前端就不会显示成一串时间戳。4. 常见问题与排查技巧实录4.1 高频报错速查表这部分整理了我在带人做这个题目时遇到频率最高的几个问题做成表格方便对号入座。现象原因解决办法IDEA创建项目一直转圈超时官方Initializr访问不稳定Server URL换成阿里云镜像 https://start.aliyun.comMaven依赖下载慢或报错没配国内镜像在settings.xml里配置阿里云mirror启动报Communications link failure数据库没启动或URL写错检查MySQL服务核对数据库名和端口启动报时区错误MySQL 8时区默认值问题URL加serverTimezoneAsia/Shanghai页面和数据库中文乱码连接字符集不是utf8URL加characterEncodingutf8mb4数据库建库用utf8mb4LocalDateTime返回前端变成数组Jackson没启用JavaTimeModule配置spring.jackson.date-format和time-zoneMyBatis-Plus分页不生效忘记注册分页拦截器配置PaginationInnerInterceptorSQL报关键字冲突表名或字段和MySQL关键字撞了改字段名或用反引号转义推荐改名删除数据总报外键约束建表加了物理外键改为逻辑外键配合业务代码判断测试类里Autowired注入为null测试类没有SpringBootTest注解检查测试类注解和包扫描路径4.2 现场还原时区、编码、版本这三个坑先说时区问题。MySQL 8默认时区设置经常导致连接时报“The server time zone value ... is unrecognized”或者更隐晦的通信失败。这个问题的本质是驱动和数据库之间对时间类型的处理有歧义解决办法就是在JDBC URL里明确指定serverTimezoneAsia/Shanghai。与之配套的字符集问题也一样URL里指定characterEncodingutf8mb4建库时也要指定两者缺一都可能出现中文乱码。再说版本问题这里容易踩的坑是依赖坐标变化。SpringBoot 3.x里MySQL驱动从mysql-connector-java变成了com.mysql:mysql-connector-j很多旧教程的依赖直接粘过来会报找不到类。另外Java 17之后javax.servlet被迁移到jakarta.servlet如果你引用了老版本的Servlet相关依赖也会冲突。遇到这类问题最直接的办法是去Spring Initializr看它生成的依赖版本然后用统一版本覆盖别自己在网上随手复制。第三个坑是IDEA创建项目超时。市面上很多教程还是让人直接下一步等它加载一旦超时新手就会反复重试、反复失败一上午就没了。正确做法是进入Settings找到HTTP Proxy或Project Structure里的Server URL改成阿里云镜像或者用Maven archetype手动创建目录再补pom。环境问题不该成为项目的第一道坎能绕就绕。4.3 并发借书如何保证库存不超卖这个点虽然课程设计不一定用得上但面试和答辩时被问到的概率很高。本质问题是两个请求同时读到stock1都以为自己能借最后库存变成-1。解决思路有三种层次。第一种是悲观锁也就是我在3.4节写的select ... for update。查询的时候先锁定这一行其他事务只能等当前事务提交后再操作。优点是实现简单、不会出错缺点是并发性能一般。但图书管理系统的并发量本来就不高这是最稳妥的方案。第二种是乐观锁在book表加一个version字段更新时带上version条件影响行数为0就说明数据被别人改过了让请求重试。这种方式适合读多写少的场景性能比悲观锁好但代码会复杂一点。第三种是条件更新不查询再更新而是直接把减库存SQL写成UPDATE book SET stock stock - 1 WHERE id ? AND stock 0影响行数为0就表示库存不足。这种方式写起来最简洁也不需要额外字段。我的建议是课程设计用第一种或第三种答辩时把三种都讲出来说明你知道不同的并发控制策略和适用场景这就超过绝大多数人。4.4 答辩时的高频问题与加分回答把答辩或面试时最常被问到的几个问题列一下每个问题的核心思路也写上。SpringBoot自动配置原理。这个几乎是必问。核心是SpringBootApplication里的EnableAutoConfiguration它通过导入AutoConfigurationImportSelector读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports里注册的自动配置类再配合ConditionalOnClass、ConditionalOnMissingBean这类条件注解实现“有对应依赖才自动装配”。为什么不用物理外键。这个前面讲过重点是逻辑外键更灵活物理外键在写入性能和迁移方面有代价实际项目中多用逻辑外键关联关系由应用层保证。事务失效的场景。这道题问得很刁钻。常见的几种场景方法不是public导致Spring无法代理同类内部方法互相调用绕过代理对象抛出的不是RuntimeException且没指定rollbackFor数据库表引擎不支持事务比如MyISAM。Transactional默认只在RuntimeException或Error时回滚自己捕获异常后没重新抛也会导致只读不写。JWT和Session的区别。核心区别是服务端有状态和无状态。Session把登录状态放在服务器内存里JWT把状态放在客户端token里服务端只需要验签。课设里两种都能用用了JWT就要能说清楚它的优势和局限。逾期记录怎么处理。比较常规的做法是查询时动态判断比如return_time is null and due_time now()就认为是逾期不需要每天定时任务去改状态。如果做得更完整可以用Scheduled定时扫描把逾期记录标记出来再给读者发提醒。能主动说出这两种方案的取舍老师会认为你确实考虑过业务细节。5. 进阶扩展让它从“课程设计”变成“作品”5.1 用Docker一键部署如果项目想放到简历上或者想在线上给面试官演示Docker部署是性价比很高的加分项。思路很简单把SpringBoot应用打成jar包写一个包含JDK环境的Dockerfile再用docker-compose把MySQL和应用编排起来。Dockerfile可以这么做FROM eclipse-temurin:17-jdk WORKDIR /app COPY target/library-system-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]docker-compose里定义两个服务mysql和app。app的spring.datasource.url里的主机名不要写localhost要写mysql这个服务名因为容器之间是通过服务名通信的。这个点不熟悉容器的人容易卡住。把整个compose文件放到项目根目录执行docker compose up -d就能一键启动无论是本机演示还是放到云服务器体验都很好。5.2 Redis缓存热门数据与定时处理逾期Redis在这个项目里有两个用武之地。一是缓存热门图书Top5管理员改图书数据时清理一下缓存否则就等它过期。二是缓存登录用户的token信息做logout时能主动失效掉弥补JWT不方便主动过期的短板。定时任务处理逾期也很简单SpringBoot里加EnableScheduling然后在方法上写Scheduled(cron 0 0 2 * * ?)表示每天凌晨2点执行一次扫描所有没还且到期时间小于当前时间的记录把状态从0改成2。这里的cron表达式不复杂记住“秒 分 时 日 月 周”写一个就行。要提醒的是定时任务默认是单机执行的如果以后部署多实例同一个任务会被执行多次这时就要考虑分布式锁不过课程设计不需要考虑这么深。5.3 扩展为前后端分离项目的小建议如果决定用Vue3重写前端后端要做几个调整。一是接口统一返回Result结构里面包含code、message、data让前端能统一处理成功和失败。二是跨域问题开发环境下前端在5173端口后端在8080端口需要在SpringBoot里配置CorsFilter或者用WebMvcConfigurer重写addCorsMappings允许对应来源。三是登录方式从Cookie会话改成Authorization头传token拦截器解析后放到ThreadLocal或请求属性里Controller直接拿当前登录人。这些改造本身不复杂但能极大提升你对“前后端如何协作”的理解。另外如果用了Knife4j或者SpringDoc生成接口文档调试起来会轻松很多答辩时也可以向老师展示接口文档观感会比一堆截图好得多。Swagger相关的依赖在SpringBoot 3.x里用springdoc-openapi那一套注意别引错版本就行。5.4 写在最后的一些心得我做了不少项目也带过不少人最大的感受是别怕题目老就怕代码糊。SpringBoot图书管理系统能做这么多年不淘汰是因为它切中了一个完整业务系统的所有核心环节增删改查、分页搜索、事务控制、并发处理、统计报表、部署上线。真正把它吃透的人换成超市管理系统、停车场管理系统、校园报修系统本质上都是一回事。哪怕最后只做了三张表只要把借还流程的事务问题、并发下的库存扣减、报表SQL这几个关键点搞明白你的收获一定比堆出来一个功能很多但业务一塌糊涂的“花架子”强得多。建议在写代码之前先把数据表和接口定义写出来再动手实现这样后面基本不会推翻重来。我自己做这类项目时有个习惯是一直保留一个简单的接口调试页面每写完一个接口就测一次不要等全部写完再联调否则报错都堆在一起很难排查。希望这篇整理能让你少踩几个坑把时间花在真正值得研究的地方。