简介基于SpringBoot与Vue的图书管理系统完整项目包面向正在准备毕业设计、课程设计或期末大作业的Java学习者。系统覆盖用户管理、图书信息管理、借阅归还、统计报表等核心模块前端采用Vue.js后端使用SpringBoot并搭配MySQL关系型数据库能够帮助读者完整理解前后端分离开发与数据持久化流程。压缩包共78个文件以67个java源文件为主另有html页面、properties配置、pom.xml、README.md及PDF文档等整体大小约889KB其中Maven可执行脚本、版本控制忽略文件与项目说明一应俱全结构清晰便于直接导入开发环境。目前已有58人学习下载。通过研读源码与项目文档可以掌握Maven项目配置、SpringBoot接口编写、Vue页面交互、数据库表设计以及图书借还业务逻辑的实现思路既适合作为课程实践参考也能在此基础上扩展成可投入使用的图书管理解决方案。1. SpringBoot图书管理系统包到底包含什么先搞清楚它能不能在你电脑上跑起来我拿到过不少SpringBoot图书管理系统的压缩包这份的类型值得先说清楚它是一套能直接跑起来的图书管理系统源码包后端基于SpringBoot框架前端是Vue分离式页面或Thymeleaf模板渲染数据库脚本单独放在SQL文件里。它解决的是一类典型问题从零做课程设计或毕业设计时图书信息增删改查、分类管理、借阅归还、用户登录这几条主线功能你要能在一周内全部打通。适合的人群很明确要交期末大作业的学生、需要一套能讲清楚原理的毕设底子的开发者以及想快速理解SpringBoot项目结构但不想从Hello World慢慢堆的人。拿到包先别急着解压看代码第一件事是判断形态这决定了你后面所有操作路径。2. 技术选型与项目骨架从pom.xml和application.yml读懂这套SpringBoot的依赖组合2.1 版本选型为什么落在SpringBoot 2.7.18、MyBatis-Plus 3.5、JDK 8拆这个包之前先聊聊它为什么用这套组合。SpringBoot 3.x 已经发布很久了但我仍然建议课程设计和毕业设计优先落到 2.7.18 这个版本。原因很实际SpringBoot 3.x 强制要求 JDK 17 起步而大部分学校机房预装的 JDK 是 8老师给的旧项目模板也大量基于 JDK 8 编写。2.7.18 是 SpringBoot 2.x 系列的最后一个稳定版本JDK 8、JDK 11、JDK 17 都能跑兼容面最宽。如果你导入项目后发现一堆注解不生效、启动直接报错八成是 JDK 版本和 SpringBoot 版本没对上。ORM 层用的是 MyBatis-Plus 而不是原生 MyBatis这几乎成了当前 SpringBoot 毕设项目的默认选择。原生 MyBatis 的增删改查要写大量 XML一个图书表就要几十行纯模板代码价值不高。MyBatis-Plus 的 BaseMapper 已经把单表 CRUD、分页、逻辑删除封装好了项目里只需要保留多表关联和复杂查询的 XML。MyBatis-Plus 3.5.3.1 这个版本和 SpringBoot 2.7 的兼容性经过大量项目验证直接引入 starter 后不用额外配置。前端形态需要提前确认解压后如果有 package.json 和 src 目录走的是 Vue 前后端分离路线后端只出接口如果只有 src/main/resources/templates 和 static 目录走的是 Thymeleaf 模板渲染路线页面和后端在一个工程里。两种形态的启动方式完全不一样先确认这个后面所有路径判断都不会错。2.2 导入与启动pom.xml、application.yml、Maven镜像一次配齐用 IDEA 打开项目时如果是 Maven 工程直接选 pom.xml 导入不要选文件夹根目录否则 IDEA 可能识别成普通项目。导入后第一件事是检查 Maven 使用的是不是阿里云镜像否则第一次拉依赖会卡在中央仓库二十分钟都未必下完。settings.xml 里加这一段mirror idaliyunmaven/id mirrorOf*/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirrormirrorOf 写成*表示所有依赖都走这个镜像。如果你的 settings.xml 里已经有一个 mirror记得删掉旧的再粘贴否则 Maven 会报 mirror 冲突。核心依赖集中在下面这段 pom.xml 里版本号和上面说的选型一致parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependencyspring-boot-starter-parent 的作用是统一管理依赖版本所以下面的 starter 不需要写 version。lombok 必须标 optional防止它被打进最终 jar 包。mybatis-plus-boot-starter 引入后MyBatis-Plus 会通过 SpringBoot 的自动装配机制读取自身配置并注册 SqlSessionFactory这就是为什么你不用写一堆Bean配置就能直接用 BaseMapper。接下来是 application.yml这个文件决定了项目在你机器上能不能连上数据库。照着改这几个参数就行server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml 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: 0关键参数逐个说。serverTimezoneAsia/Shanghai不加的话MySQL 8 驱动会直接抛时区异常useSSLfalse跳过 SSL 握手本地开发能节省一次往返延迟allowPublicKeyRetrievaltrue是 MySQL 8 的坑不加的话连接时可能报 Public Key Retrieval is not allowed。map-underscore-to-camel-case把 book_name 自动映射成 bookName实体类不用写任何注解就能对上列名。logic-delete-field声明逻辑删除字段之后deleteById会变成 update 语句而不是物理删除。导入项目时还有一个高频翻车点lombok 插件没装。IDEA 里哪怕代码不报错编译阶段也可能提示找不到 getter/setter。到 Settings → Plugins 里搜 lombok装上并重启 IDEA。常见做法是先跑一遍mvn compile如果这里过了项目就导入成功了一半。2.3 包里只有 jar 没有源码先跑通验证再决定要不要反编译有可能你拿到压缩包后发现里面只有一个 jar 文件没有 src 目录。这种情况先别急着找反编译工具先试着把它跑起来确认这套系统真实可用。命令行执行java -jar library-system-0.0.1-SNAPSHOT.jar --spring.datasource.password你的密码前提是 MySQL 里已经建好了库和表。如果包里没有 SQL 脚本一个可行的办法是把 jar 用压缩工具打开里面的 BOOT-INF/classes 下通常能找到 schema.sql 或 data.sql。找到后先拖出来执行再启动 jar。我一般会建议在决定反编译之前想清楚目的如果你只是想看功能、跑通流程直接运行 jar 就够了如果你需要改代码交作业反编译工具可以还原大部分 Controller、Service、Mapper 的结构但注释、泛型信息、原始 XML 注释会丢失。IDEA 自带的反编译能力打开 .class 文件会自动反编译对这类 SpringBoot 项目足够还原度能到八成以上但别指望反编译出来的代码能直接重新编译通过通常要把泛型和 import 整理一遍。3. 数据库脚本与核心表设计从book表到borrow_record的字段取舍与分页实现3.1 四张核心表字段设计、外键策略、状态字段为什么用数字图书管理系统的数据库结构比较固定四张核心表基本覆盖了所有功能category 图书分类表、book 图书表、user 用户表、borrow_record 借阅记录表。下面这套建表脚本是按最常见的做法整理的也符合多数 SpringBoot 毕设项目的实际用法CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4; USE library; CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(50) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(100) NOT NULL, isbn VARCHAR(20) NOT NULL UNIQUE, author VARCHAR(50), publisher VARCHAR(100), category_id INT, stock INT NOT NULL DEFAULT 1, borrowed_count INT DEFAULT 0, status TINYINT DEFAULT 1 COMMENT 1可借 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0, KEY idx_category (category_id), KEY idx_status (status) ); CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role TINYINT DEFAULT 0 COMMENT 0学生 1管理员, real_name VARCHAR(50) ); CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, book_name VARCHAR(100), isbn VARCHAR(20), borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP, return_time DATETIME, status TINYINT DEFAULT 0 COMMENT 0借出 1已还 2逾期 );几个容易被答辩老师追问的取舍点。borrow_record 里冗余了 book_name 和 isbn而不是只存 book_id理由是借阅记录是历史数据如果图书后来改名或下架记录里的关联数据不能跟着变冗余字段让借阅历史保持当时的状态这是典型的数据仓库思路也是毕设答辩时能讲清楚的亮点。isbn 上加了 UNIQUE 约束保证同一本书不能重复录入。status 用 TINYINT 数字而不是 VARCHAR 字符串存储占用小、索引效率高前端通过枚举或字典映射成文字。代价是代码里需要解释数字含义所以建表脚本里要写 COMMENT。外键方面这套设计没有在 book.category_id 上加物理外键约束只建了普通索引。加物理外键的问题是删除分类时会被约束卡住数据清理很麻烦不加外键靠应用层保证引用完整性删除分类前先处理图书就行了。课程设计属于单人开发场景应用层控制就够理清这个边界也是答辩时能聊的点。3.2 分页查询落地MyBatis-Plus分页插件与参数限制图书列表页是系统里查询频率最高的接口分页插件必须配上。MyBatis-Plus 的分页插件和 PageHelper 不是一个思路PageHelper 是改写拦截器方式MyBatis-Plus 是通过内置分页物理插件实现。下面这是标配配置Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(100L); interceptor.addInnerInterceptor(pagination); return interceptor; } }setMaxLimit(100L)是必须写的一行。很多教程里会漏掉它后果是调用方把 pageSize 改成 10000一次就把整张表拖下来后端资源被耗尽。分页插件默认生成SELECT COUNT(*)和真正的分页语句两条 SQL控制台能看到日志调试时如果发现分页没生效优先检查这个配置类有没有被扫描到。Controller 里的分页接口一般长这样GetMapping(/book/page) public Result page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String keyword) { LambdaQueryWrapperBook wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.like(Book::getBookName, keyword) .or() .like(Book::getAuthor, keyword); } wrapper.orderByDesc(Book::getCreateTime); PageBook page bookMapper.selectPage(new Page(pageNum, pageSize), wrapper); return Result.success(page); }LambdaQueryWrapper 比 QueryWrapper 更安全列名通过方法引用传递而不存在字符串硬编码字段改名时编译期直接报错。like里用.or()要注意如果有多个条件叠加or会把整个 where 结构拆开容易查出无关数据。常见的做法是把 keyword 的多条件判断封装在一个and里比如wrapper.and(w - w.like(...).or().like(...))这样逻辑更可控。3.3 SQL脚本导入的坑编码、外键、清库重置拿到 SQL 脚本第一件事不是双击导入而是用编辑器打开看编码。脚本头有SET NAMES utf8mb4是保险做法直接执行即可。Navicat 导入时如果脚本里是 utf8mb4、而连接的库默认是 utf8中文注释和表注释会全部变成乱码这个问题不影响功能但很影响观感而且答辩时老师会盯屏幕。清库重置时最容易翻车。如果你要删掉所有表重新导入直接逐条 DROP TABLE 会因为外键约束报错。正确做法是执行SET FOREIGN_KEY_CHECKS 0; DROP TABLE IF EXISTS borrow_record; DROP TABLE IF EXISTS book; DROP TABLE IF EXISTS user; DROP TABLE IF EXISTS category; SET FOREIGN_KEY_CHECKS 1;FOREIGN_KEY_CHECKS 0的含义是临时关闭外键检查DROP 完成后再恢复为 1这样清库不会卡在约束上。如果你没有外键这条命令也无害但养成习惯后遇到有外键的库不会翻车。4. 前后端对接链路Vue页面调用SpringBoot接口的借阅闭环与跨域处理4.1 后端接口设计登录Token、借书、还书的Controller与事务边界后端接口的规范程度直接决定前端开发效率。很多学生项目把业务逻辑直接堆在 Controller 里一个方法几百行调试的时候只能从头看到尾。这套项目的常见结构是 Controller 收参数、Service 处理业务、Mapper 操作数据库。登录接口是最简单的切入点PostMapping(/user/login) public Result login(RequestBody LoginDTO dto) { User user userService.login(dto.getUsername(), dto.getPassword()); if (user null) { return Result.fail(用户名或密码错误); } String token UUID.randomUUID().toString().replace(-, ); tokenStore.put(token, user.getUsername()); return Result.success(token); }token 用 UUID 生成后存到一个 ConcurrentHashMap 里这是毕设项目最常见也最好解释的方案。生产环境应该换 Redis并设置过期时间但本地演示用内存缓存没问题。答辩被问“token 存哪”时你可以直接说这是演示环境方案生产环境用 Redis 并配置 TTL 过期这样既诚实又显得你清楚边界。借书接口是核心业务也是事务最容易出问题的地方PostMapping(/book/borrow) Transactional(rollbackFor Exception.class) public Result borrow(RequestBody BorrowDTO dto, RequestHeader(token) String token) { Book book bookMapper.selectById(dto.getBookId()); if (book null || book.getStatus() ! 1) { return Result.fail(图书不存在或已下架); } if (book.getStock() 0) { return Result.fail(库存不足); } book.setStock(book.getStock() - 1); bookMapper.updateById(book); BorrowRecord record new BorrowRecord(); record.setUserId(dto.getUserId()); record.setBookId(book.getId()); record.setBookName(book.getBookName()); record.setIsbn(book.getIsbn()); record.setStatus(0); borrowRecordMapper.insert(record); return Result.success(借阅成功); }Transactional(rollbackFor Exception.class)是必须显式声明的。SpringBoot 默认只在抛出 RuntimeException 时回滚事务如果漏写借阅业务中“扣库存成功但插入记录失败”时库存就悄悄少了这就是典型的血泪现场。rollbackFor 声明后任何异常都会触发回滚。还书接口逻辑上是镜像查到借阅记录把 status 改成已还把 book 的 stock 加回 1。实际开发时可以复用同一个 Service 方法不要各写各的。SpringBoot 常用注解在这个链路里也都能对上RestController 声明接口、GetMapping/PostMapping 限定请求方法、RequestHeader 取 token、RequestBody 接 JSON 参数。4.2 Vue端请求封装axios实例、Token拦截器与统一错误提示Vue 前端如果走前后端分离所有请求应该统一走封装好的 axios 实例而不是每个页面单独axios.get。下方代码是标准封装方式import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: http://localhost:8080, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[token] token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg) return Promise.reject(new Error(res.msg)) } return res.data }, error { ElMessage.error(网络异常请检查后端服务) return Promise.reject(error) } ) export default servicebaseURL 直接写http://localhost:8080在本地开发没问题但如果后端换端口或前端部署到服务器这里就要改成环境变量。timeout 设 10000本地调试时如果你的机器性能一般建议改成 30000否则查询慢一点就报超时。请求拦截器的核心逻辑是每次请求自动带上 token这样所有接口只需要在后端拦截器里统一校验。响应拦截器把res.code ! 200的响应统一弹错误提示前端各个页面就不需要重复处理错误分支。这套封装解决了前端工程里最琐碎的问题也保证了后端返回结构统一时前端能无脑取数。4.3 前端是Thymeleaf模板的另一种形态和Vue分离式差在哪如果你的包里没有 src 目录而是 templates 和 static那就是 Thymeleaf 模板渲染形态。区别在于后端 Controller 的方法返回的不是 JSON 而是视图名比如return book/list对应 templates/book/list.html。这种形态下不用配置跨域因为页面和接口同源不需要 token 参数因为登录状态直接存在 session 里。对比一下两种形态的关键差异对比项Vue 前后端分离Thymeleaf 模板渲染前端代码位置独立 src 目录resources/templates接口返回JSON视图或 JSON 混用登录态token 请求头session cookie跨域需要处理不需要启动方式后端 npm run serve只启动后端如果你拿到的包是 Thymeleaf 形态运行只需要启动 SpringBoot 一个进程浏览器直接访问 http://localhost:8080 即可。如果某些页面返回的是 JSON 数据比如异步加载表格Controller 上会加 ResponseBody这类混用模式在老项目中非常普遍。切换前端页面模板时注意要重启后端才会生效除非你加了 thymeleaf 热更新依赖。5. 启动运行避坑笔记五条血泪记录从端口占用到导入中文乱码5.1 端口 8080 被占用启动直接失败异常信息最迷惑现象IDEA 启动 SpringBoot 项目控制台报Web server failed to start. Port 8080 was already in use后面的堆栈很长第一次遇到容易误判成环境问题。原因本机某个进程已经占用了 8080 端口SpringBoot 内置 Tomcat 无法绑定。解决先查端口占用Windows 执行netstat -ano | findstr 8080取最后一列 PID再到任务管理器结束该进程。如果这个进程是别的开发服务比如前端 dev server不想杀就去 application.yml 里把 server.port 改成 8081前端 axios 的 baseURL 同步改成http://localhost:8081。从那以后我每次启动项目前会先看一眼端口习惯性地跑一次 netstat。5.2 Mapper 报 not foundMyBatis-Plus 扫描不到现象启动成功但调用图书接口时报Invalid bound statement (not found)或mapper not found而且日志里没打印 SQL。原因主启动类上没有加 MapperScan或者加了但扫描包路径不对另一个隐蔽原因是 Lombok 版本太旧在 JDK 11 环境下 getter 没生成导致 MyBatis 反射失败。解决主类上加MapperScan(com.example.library.mapper)路径换成你自己的 mapper 包。如果你已经加了检查包名是不是写错一级。Lombok 的话统一升到 1.18.30这个版本对 JDK 8 到 JDK 17 都兼容。注意 MapperScan 更适合在项目小的时候用如果以后拆多模块建议改成在 Mapper 接口上逐个加 Mapper避免扫描范围失控。5.3 MySQL 时区报错JDBC URL 缺了关键参数现象控制台报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized数据库密码明明对就是连不上。原因MySQL 8 驱动对时区敏感JDBC 连接串里没指定 serverTimezone驱动无法识别系统默认时区显示出来的乱码字符串。解决在 connection URL 后面拼上serverTimezoneAsia/Shanghai。这就是个玄学问题花半小时排查连接配置最后发现只是 URL 少一段参数。连接串完整应该包含useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse缺哪一个都可能在不同版本的驱动上报不同错误。5.4 Vue 页面接口全挂CORS 跨域被浏览器拦截现象Vue 项目启动后浏览器 F12 控制台刷屏Access-Control-Allow-Origin错误接口请求显示 failed但用 Postman 调同一个后端接口完全正常。原因浏览器同源策略拦截了来自不同端口的前端请求Postman 没有这个限制所以正常。解决后端加一个全局跨域配置或前端用 devServer 代理。我更推荐后端加 CORS 配置因为部署阶段更省心。配置如下Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意allowCredentials(true)时不能写allowedOrigins(*)必须用 allowedOriginPatterns(*)这是 SpringBoot 各版本间的经典差异写错会直接启动报错。如果后端不方便改另一个选择是 vue.config.js 里配置 devServer.proxy把/api代理到后端端口这样浏览器看到的请求是同源的。5.5 批量导入图书中文乱码Excel 解析和文件编码现象管理员在页面上传 Excel 批量导入图书表格里中文全部变成乱码或者导入失败提示格式错误。原因上传的 Excel 文件本身编码不是 UTF-8或者后端解析时没有正确处理文件流编码。还有可能是上传文件大小超过了 SpringBoot 默认的 1MB 限制报错信息被误读成格式错误。解决SpringBoot 的默认上传限制在 application.yml 里调大spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB解析 Excel 时用 EasyExcel 或 POI读取时指定字符集。用 EasyExcel 的话直接监听器处理行数据不要自己去 FileInputStream 里手动转字符串。顺带提一个被很多项目忽略的点如果系统里有全局过滤器处理 request body上传文件接口的参数解析顺序优先级要确认否则过滤器先消费了输入流Controller 里拿到的文件就是空的这个问题排查起来非常隐蔽。6. 答辩前的验收测试用Postman把登录、借阅、归还全流程验证一遍6.1 核心链路验证清单答辩现场翻车基本只有一个原因本地跑得好好的演示时某个环节顺序不对。要避免这种情况最好的办法是把核心链路在 Postman 里完整走一遍整理成一张可以逐项打勾的清单。步骤方法预期结果1. 登录POST /user/login返回 token2. 带 token 查分页GET /book/page返回图书列表和 total3. 新增分类POST /category/add返回成功4. 新增图书POST /book/add返回新图书 id5. 借书POST /book/borrow库存减 1生成借阅记录6. 还书POST /book/return借阅记录状态变为已还7. 不带 token 访问GET /book/page返回 401 或登录失效第 7 步不要跳过这是验证拦截器是否生效的关键一步。很多项目演示时先打开页面再操作因为浏览器里 token 已经在 localStorage 里了拦截器的作用完全看不出来。用 Postman 模拟一个不带 token 的请求如果接口返回了数据说明鉴权是摆设这是答辩老师最爱问的问题。6.2 关键curl命令如果你不想开 Postman直接用 curl 也能完成完整链路验证。登录接口拿 token 后写入变量后续请求带上 token 头# 登录并提取 token curl -X POST http://localhost:8080/user/login \ -H Content-Type: application/json \ -d {username:admin,password:123456} # 分页查询第一页每页10条 curl http://localhost:8080/book/page?pageNum1pageSize10 \ -H token: 上面拿到的token # 借阅图书 id1 curl -X POST http://localhost:8080/book/borrow \ -H Content-Type: application/json \ -H token: 上面拿到的token \ -d {userId:1,bookId:1}curl 的好处是能在服务器环境直接验证Postman 做不到这点。如果你把项目部署到云服务器测试用 curl 返回的结果判断接口是不是真恢复了比开浏览器更直接。6.3 关于 token 过期与 401 的补充说明这套验证清单里最容易忽略的是“token 过期”场景。如果后端 tokenStore 是内存实现的服务重启后所有已发放的 token 都会失效前端页面就会突然全部变成 401。答辩前重启了后端却忘了重新登录是最常见的人工事故。有一次我把项目发给同学他本地跑起来一直 401排查了半天发现是 Postman 里还存着昨天的旧 token而后端重启后内存缓存已经清空了。从那以后我每次把项目发给别人前都强制走一遍上面的清单登录拿 token、带 token 查一页书、借一本、还一本、再验证一次不带 token 被拦截全绿了才发出去。这个习惯救了我好几次希望帮到你。本文还有配套的精品资源点击获取
