简介:这是一套基于SpringBoot和Vue框架构建的校园数字化图书馆系统毕业设计源码包,面向计算机相关专业毕业生或需要快速搭建完整项目的开发者。项目已通过导师指导并获答辩评审98分,功能覆盖图书检索、借阅、归还、预约、管理等完整业务流程,界面友好、操作简便,后端采用SpringBoot与MySQL数据库,前端使用Vue组件化开发,支持多用户同时在线操作。压缩包共627个文件、约41.36MB,包含133个Java源码、101个Vue组件、58个JS脚本、15个CSS样式,以及SQL数据库脚本、论文文档、系统演示视频、使用说明文档和多个启动脚本等,目录结构清晰,可直接部署运行。目前已有55人学习浏览,附带的论文和演示视频可帮助深入理解设计思路,使用说明文档和部署脚本降低了上手门槛,是完成毕业设计或课程作业的优质参考。
1. 校园数字化图书馆毕设:这套SpringBoot+Vue方案到底在解决什么问题
每年到了毕设季,图书馆管理系统都是Java方向的高频选题。这里说的基于SpringBoot+Vue校园数字化图书馆系统,是一份前后端分离的完整交付包:后端用SpringBoot+MyBatis-Plus连MySQL,前端用Vue做页面与交互,再配上数据库脚本、论文、演示视频和使用说明文档。它的价值不在页面做得有多花哨,而是答辩现场能不能把整条业务链讲清楚:图书录入、读者管理、借还书流程、超期统计、管理员与读者权限区分。适合三类人:手头只有半成品源码需要补全的,想靠系统数据给论文撑场子的,以及打算在SpringBoot+Vue这套主流栈上完整练一遍全流程的人。如果你拿到源码后只剩三天就要演示,这篇就按“解压后最短时间跑通并讲明白”的顺序来写。
2. 先把数据模型立住:图书馆系统的表设计与权限设计
2.1 前后端分离为什么是图书馆系统的合理选型
毕设里最常见的翻车方式是后端用服务端模板直接把页面渲染出来,页面逻辑和后端代码缠在一起,改一个按钮颜色都要重启应用。这套系统用SpringBoot+Vue,后端只出REST接口,前端独立运行和构建,好处是三层结构非常清楚:Vue负责页面和交互,SpringBoot负责接口和业务规则,MySQL负责数据落盘。答辩时老师问“你的系统架构是什么”,你可以直接画一条数据流向:浏览器请求到Vue路由,Vue通过axios把请求发到SpringBoot的Controller,Controller调用Service,Service操作Mapper访问数据库,结果再反向返回。这条链路每个环节都能对应到项目源码里的一个具体目录,比模板工程好讲得多。
另一个实际好处是联调方便。前端只需要知道接口返回什么样的JSON,后端只需要保证按约定返回数据,两边可以独立开发。对一个人单干的学生来说,前后端分离也意味着出问题时有明确边界:页面不显示数据,先看浏览器Network面板里接口通没通;接口404,再查Controller路径和启动日志;数据库没数据,再去核对SQL脚本。这套排查顺序,后面避坑章节还会反复用到。
2.2 六张核心数据表与借阅状态的字段设计
校园数字化图书馆的业务可以拆成两条线:一条是图书线,从图书信息录入到馆藏数量变化;一条是读者线,从读者注册到每次借还产生的借阅记录。围绕这两条线,最少需要六张表:管理员表、读者表、图书表、借阅记录表、图书分类表、公告表。
图书表里最容易被忽略的字段是“可借数量”和“馆藏数量”要分开,而不是只存一个总数。原因很简单:同一本书可能被多个读者同时借走副本,馆藏数量是物理存在的书,可借数量等于馆藏数量减去当前被借出的数量。这个字段如果只用一个total字段靠代码去减,并发一高就会出现超借。借阅记录表的核心是状态字段,常见设计是用int存0、1、2、3,分别对应待归还、已归还、逾期、已续借。这比存字符串“借出”“归还”更省空间,排序也快,但坏处是代码里到处是魔法数字。我的做法是在Java里定义枚举把数字和含义绑定,Controller返回给前端时再转换成文本,数据库层面简洁,页面层面可读。
2.3 MySQL建表脚本:从建库到借阅记录的完整SQL
下面这段SQL覆盖了library数据库的创建、核心表的建表语句和外键关系。实际交付包里一般会把所有表写在一个.sql脚本文件里,你拿到后先在MySQL里执行一次。
CREATE DATABASE IF NOT EXISTS library DEFAULT CHARSET utf8mb4; USE library; CREATE TABLE tb_admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role INT DEFAULT 1 COMMENT '1 超级管理员,2 普通管理员', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '管理员表'; CREATE TABLE tb_reader ( id INT PRIMARY KEY AUTO_INCREMENT, reader_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, phone VARCHAR(20), status INT DEFAULT 1 COMMENT '1 正常,0 禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '读者表'; CREATE TABLE tb_book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL, book_name VARCHAR(100) NOT NULL, author VARCHAR(50), category_id INT, total INT DEFAULT 1 COMMENT '馆藏总数', available INT DEFAULT 1 COMMENT '可借数量', location VARCHAR(50) COMMENT '存放位置', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id) ) COMMENT '图书表'; CREATE TABLE tb_borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_time DATETIME NOT NULL, due_time DATETIME NOT NULL, return_time DATETIME, status INT DEFAULT 0 COMMENT '0 待归还,1 已归还,2 逾期', INDEX idx_reader (reader_id), INDEX idx_book (book_id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES tb_book(id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES tb_reader(id) ) COMMENT '借阅记录表';这段脚本里的几个参数值得说明。建库时指定utf8mb4而不是utf8,是为了支持生僻字和特殊字符,图书书名和读者姓名里偶尔会出现这类字符;如果按utf8建库,后面改字符集要连表带库一起动,属于事后很麻烦的操作。索引方面,借阅记录表对book_id和reader_id都建了普通索引,因为高频查询是按读者查借阅历史,没有索引的话数据量到几万条后分页查询会明显变慢,面试官问性能优化时这就是一个能拿得出手的点。
外键约束在毕设里可以加,但线上项目通常会去掉。原因不复杂:外键在插入删除时有额外校验开销,拆库分表后外键也不太好用。这里保留外键是为了让E-R图更直观,同时MySQL Workbench能直接导出一张关系图。如果你后续要接MyBatis-Plus的删除接口,建议先删外键约束再动手,否则会出现“有借阅记录的图书删不掉”的情况,这个在避坑章节细说。
2.4 接口清单先行:后端要给前端提供哪些能力
表设计完成后,最好先用一张表把前端页面要调用的接口列清楚,再动手写代码。很多学生一上来就写Controller,写到一半发现缺字段,又回头改表结构,来回折腾。
| 功能模块 | 接口路径 | 方法 | 说明 |
|---|---|---|---|
| 管理员登录 | /api/admin/login | POST | 返回token和角色 |
| 读者管理 | /api/reader/page | GET | 分页查询读者列表 |
| 图书管理 | /api/book/page | GET | 分页查询图书,支持按书名/ISBN过滤 |
| 新增图书 | /api/book/save | POST | 新增或更新图书信息 |
| 借阅 | /api/borrow/add | POST | 创建借阅记录,同时减少可借数量 |
| 归还 | /api/borrow/return | PUT | 更新借阅记录,同时增加可借数量 |
| 借阅统计 | /api/borrow/stats | GET | 返回今日借还数量、逾期数 |
这份接口清单一般对应使用说明文档里的“系统功能说明”章节。演示视频里录的操作,前端每个按钮都要能追到这个表格里的一行。后面第3、4章就按这个清单来实现核心部分。
3. 后端落点:SpringBoot核心接口配置与图书借阅链路
3.1 pom.xml依赖清单与SpringBoot版本选择
SpringBoot的版本选择是很多新手的第一个坑。现在新建项目时脚手架默认拉到的SpringBoot可能已经到了3.x,而很多毕设教程和论文里的代码基于2.x早期版本写的,两者在javax/jakarta命名空间、自动配置类路径上都有差别,照抄旧代码大概率编译不过。
我在给这类校园图书馆项目整理依赖时,一般固定用SpringBoot 2.x的高版本,配合Java 8或Java 11,这两个版本组合在毕业设计里兼容性最好。核心依赖如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi2-spring-boot-starter</artifactId> <version>4.4.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>这里有一个容易踩的依赖冲突点。spring-boot-starter-security如果只是为了登录认证,可以直接用,但要注意它会默认拦住所有请求,必须写一个SecurityConfig放行登录接口和静态资源;如果不想引入复杂的过滤器链路,也可以只用一个JWT拦截器,这在小型毕设项目里更可控。另外,MyBatis-Plus的starter会和旧的mybatis-spring-boot-starter冲突,两者的SqlSessionFactory会打架,表现为启动时报告FactoryBean错误,解决方法是只保留其中一个。毕设场景下推荐直接用MyBatis-Plus,自带分页插件和条件构造器,写起来比手写XML省很多时间。
3.2 application.yml里的数据库与MyBatis-Plus参数
配置文件是打开项目后第二个要看的文件,很多“项目启动报错”其实就是配置文件和本机环境不一致。下面给出带注释的application.yml,参数值按本地开发环境填写:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 mapper-locations: classpath*:mapper/*.xml三个参数要特别留意。第一个是jdbc url里的serverTimezone=Asia/Shanghai,MySQL 8.x不带时区参数时,启动经常会报时区相关异常,属于特别常见的启动翻车点。第二个是map-underscore-to-camel-case,这个开关打开后,数据库里的book_name字段能自动映射到Java属性的bookName;如果不设置,MyBatis-Plus查出数据后会发现实体属性全是null,页面表格空白得让人以为SQL写错了。第三个是逻辑删除字段,如果数据表没有设计deleted字段,这个配置要删掉,否则传id删除时会默认去更新deleted列,表里没这列就直接报SQL错误。
knife4j依赖是这里额外加的,作用是用Swagger自动生成接口文档。启动项目后访问http://localhost:8080/doc.html,就能看到所有接口的调试页面,答辩现场给老师演示时用它来调用接口,比反复在postman里填URL显得更完整。如果你不想引入这个依赖,也可以只保留springfox老版本,但接口界面的体验会差一些。
3.3 图书借阅接口的后端实现
借阅接口是整个系统里最值得写的部分,它牵扯到两张表和两个数量字段的同步变更。如果只写“插入一条借阅记录”,那这本书就能被无限借走,答辩时老师问一句“系统怎么防止超借”就答不上来。这段代码用MyBatis-Plus的条件构造器和事务能力来实现:
@RestController @RequestMapping("/api/borrow") public class BorrowController { @Resource private BorrowRecordService borrowRecordService; @PostMapping("/add") @Transactional(rollbackFor = Exception.class) public Result borrow(@RequestBody BorrowDTO dto) { // 第一步:检查读者是否存在且可用 Reader reader = readerService.getById(dto.getReaderId()); if (reader == null || reader.getStatus() != 1) { return Result.error("读者不存在或已被禁用"); } // 第二步:检查图书可借数量 Book book = bookService.getById(dto.getBookId()); if (book == null || book.getAvailable() <= 0) { return Result.error("该书暂无可借副本"); } // 第三步:生成借阅记录,默认借期30天 BorrowRecord record = new BorrowRecord(); record.setBookId(dto.getBookId()); record.setReaderId(dto.getReaderId()); record.setBorrowTime(new Date()); record.setDueTime(DateUtil.offsetDay(new Date(), 30)); record.setStatus(0); borrowRecordService.save(record); // 第四步:扣减可借数量 book.setAvailable(book.getAvailable() - 1); bookService.updateById(book); return Result.success(record.getId()); } }代码的边界检查集中在前面两步:读者状态校验和可借数量校验。第三步里new出的BorrowRecord不手动设置id,因为表里是自增主键,save之后MyBatis-Plus会把生成的主键回填到record对象里,Result里直接返回record.getId()就能让前端拿到这条借阅记录的主键。这里插入操作和后面的updateById必须落在同一个事务里,所以方法上加@Transactional(rollbackFor = Exception.class),一旦扣减库存失败,借阅记录也会回滚,避免出现“借阅记录存在但库存没扣”的脏数据。
@RequestBody接收的BorrowDTO里只需要readerId和bookId两个字段,前端传JSON时字段名必须和DTO属性名一致,这是Jackson反序列化的默认规则。如果报HttpMessageNotReadableException,先去前端看传的字段名是不是写错了,而不是怀疑后端配置。这种小问题在联调阶段最浪费时间,本质是前后端约定不一致。
3.4 归还接口里最容易漏掉的逾期判断
归还接口在代码上比借阅还简单,但业务上多一个逾期判断。图书到期时间称为dueTime,归还时当前时间大于dueTime就认为逾期,状态置为2,否则状态置为1,同时把图书的available加回来。业务会延伸到“超期罚款”模块:可以按天计算罚款金额并写入借阅记录扩展字段,这笔钱在普通图书管理系统中不做账务处理,只作展示。实现时用LambdaUpdateWrapper写条件更新比较简洁,避免先查出整条记录再更新的两步开销。
这里提一个细节:归还操作不要和图书更新放在一个事务里就算了,还要考虑“重复归还”的情况,也就是同一本借阅记录被前端点了两次。解决方法是update时加上status限定条件,比如update ... set status = 1 where id = ? and status = 0,影响行数为0时说明已经归还过,直接返回“该记录已归还”。这样比先查后改更稳,也少了一次数据库往返。
4. 前端对接:Vue3路由、axios封装与页面交互细节
4.1 Vue项目创建与开发环境配置
毕设里前端最常见的两套选择是Vue2+ElementUI、Vue3+Element Plus。这套图书馆系统按Vue3作为推荐写法,因为现在新项目脚手架基本都是Vue3,npm install时默认拉到的依赖版本也更适配Vue3。创建项目用Vite,启动快、配置文件直观:
npm create vite@latest library-web -- --template vue cd library-web npm install npm install axios vue-router@4 element-plus这段命令背后有三个容易踩坑的地方。第一步如果node版本太老,npm create vite会直接报错,要求至少Node 16以上,最好直接装Node 18 LTS。第二步npm install在图书馆项目里通常会出现几句关于peer dependencies的警告,一般是Element Plus和Vue版本不完全匹配,只要npm能正常结束就可以继续,不用为了消除警告去乱加--force。第三步里的vue-router必须是4.x,Vue3不能配vue-router 3.x,去npm官网查版本时很容易选错。
依赖装完后,建议先配置npm run dev试启动一次再开始写页面。Vite默认端口5173,后端接口在8080,两者端口不同且前后端分离,所以需要给Vite配开发代理,把/api开头的请求转发到8080,这样浏览器的请求不会跨域,调试起来省掉很多麻烦。vite.config.js配置如下:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })代理配置的核心是changeOrigin: true。不设置这个值时,后端收到的请求头里Host还是localhost:5173,如果后端配了CORS白名单,可能直接拦掉;设置成true后,后端看到的是来自localhost:8080的请求,相当于从后端视角绕开了跨域。这种事情在毕设开发里有点像玄学,明明前后端都能跑,接口就是报跨域,改一下代理往往就通了。
4.2 axios封装与接口调用:让前端不再把URL写死在页面里
页面里直接写this.$http.get('/api/book/page')不是不能用,但项目一复杂就乱了。图书馆系统里所有请求的baseURL、超时时间、token注入可以统一放在一个request.js文件里,页面里只需要import这个封装好的axios实例。
// src/utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:把登录后存的token塞进请求头 request.interceptors.request.use(config => { const token = localStorage.getItem('library_token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) // 响应拦截器:统一处理错误提示 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default requestbaseURL这里写成'/api',配合Vite代理,请求自动落到8080后端。后端返回的JSON约定为{code:200,message:'ok',data:...},响应拦截器把data字段解出来返回给页面,页面里拿到的是纯净的data,不用在每个组件里再判断code。token用localStorage存是毕设常规做法,实践里更安全的是放内存变量或cookie,但图书馆管理系统的演示场景要求不高,刷新页面后还能保持登录状态是刚需,所以localStorage体验更好。
调用封装后的接口,页面的逻辑会非常短。比如图书列表页面:
import request from '@/utils/request' const fetchBooks = async (params) => { const list = await request.get('/book/page', { params }) return list }需要注意的是后端Controller路径如果是/api/book/page,这里baseURL已经带了/api,所以请求路径直接写/book/page。很多同学前端报404,原因要么是baseURL和路径重复写了两个/api,要么是Vite代理没重启。改完vite.config.js后必须重启npm run dev,代理配置不是热更新能生效的,这种小细节能在调试时省十分钟。
4.3 vue-router与页面权限控制
图书馆系统的页面分成读者端和管理员端。读者端看到的是借阅大厅和个人借阅记录,管理员端看到的是图书管理、读者管理、借阅审核、统计报表。前端不能真正保证安全,但可以通过路由守卫来控制能访问的页面,避免用户直接输URL进管理页。
// src/router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/admin', component: () => import('@/layout/AdminLayout.vue'), meta: { role: 'admin' }, children: [ { path: 'book', component: () => import('@/views/admin/BookList.vue') }, { path: 'reader', component: () => import('@/views/admin/ReaderList.vue') }, { path: 'borrow', component: () => import('@/views/admin/BorrowList.vue') } ] }, { path: '/reader', component: () => import('@/layout/ReaderLayout.vue'), children: [ { path: 'hall', component: () => import('@/views/reader/BookHall.vue') }, { path: 'records', component: () => import('@/views/reader/MyBorrow.vue') } ] } ] router.beforeEach((to, from, next) => { const token = localStorage.getItem('library_token') const role = localStorage.getItem('library_role') if (to.path === '/login') { next() } else if (!token) { next('/login') } else if (to.meta.role && to.meta.role !== role) { next('/404') } else { next() } }) export default router这段路由里大量使用了import函数动态引入组件,也就是路由懒加载。好处是首屏不用一次性加载所有页面,尤其图书列表和统计图表这类页面体积较大,懒加载后登录页秒开。路由守卫的逻辑:没有token一律回登录页,token里存的角色不是admin就不能进配置了meta: { role: 'admin' }的页面。注意前端权限控制只能做到页面入口层拦截,后端接口仍然要自己做鉴权,否则任何人从浏览器控制台照样能调接口拿数据。这个点在论文“系统安全设计”章节里要单独写。
4.4 不要让页面交互停留在“列表展示”
很多学生做完的图书馆系统页面实际只有表格加新增弹窗,答辩时老师点几下就问不出东西了。前端能快速加分的地方是搜索结果反馈和借阅流程提示。比如在图书大厅顶部放一个搜索框,输入书名或ISBN后用防抖去调接口,搜索时按钮置loading;借阅成功后弹出“借阅成功,应还日期为xx”,失败时把后端返回的具体原因直接展示。这些交互不需要额外组件库,就是v-loading指令和ElMessage的配合,但能让整个演示过程看起来像真实产品而不是课程作业。下一章讲的打包部署,也要在开发环境调通后再进行。
5. 避坑合集:数据库、Maven、Node三个层面的常见翻车与排查
5.1 SpringBoot版本太高导致JDK不兼容
现象:项目启动时报UnsupportedClassVersionError,命令行mvn spring-boot:run报错信息又长又乱。原因:SpringBoot 3.x要求JDK 17起步,你本机装的还是JDK 8或11,两者不匹配。很多人在官网下载的就是SpringBoot 3.x最新版,而论文里的参考代码是2.x写的,两个体系的javax.servlet和jakarta.servlet完全不同,越改越乱。
解决:毕设场景固定用SpringBoot 2.7.x。Idea里新建项目时Spring Initializr可以手动选Server URL为阿里云镜像,再把Java版本选成8。更省事的做法是直接改pom.xml里的parent版本为2.7.18,然后执行Maven Reload Project。如果报错信息里出现jakarta.servlet,基本可以确认是版本跨代问题,不要试图在代码里去改import,直接换版本重来。
5.2 MySQL表名大小写翻车与数据库修改结构时的连接池问题
现象:项目启动正常,但一到查询就报Table 'library.tb_book' doesn't exist,反复核对数据库里明明有这张表。原因:MySQL在Linux下默认大小写敏感,Windows下大小写不敏感。如果你在Windows上建的导出脚本,到Linux服务器上导入后表名是tb_book,而MyBatis-Plus根据实体类Book自动生成的SQL里表名大小写和库里的不一致。
解决:一个方法是建表时统一用下划线小写命名,实体类用@TableName("tb_book")显式指定表名,不依赖MyBatis-Plus的默认驼峰转换;另一个方法是改MySQL配置lower_case_table_names=1,重启MySQL后生效。注意这个参数改完会影响现有库的表名匹配,改之前先备份。
另外有个关联问题:手工执行修改表结构的SQL时,如果项目正在运行,连接池里的旧连接会持有失效的元数据,出现“Unknown column”错误,但其实表结构已经改了。解决方法是修改表结构后重启应用,或者让连接池重新连接。毕设阶段直接重启最干脆,不用深究连接池细节。
5.3 Maven依赖冲突:mybatis与mybatis-plus同时出现
现象:控制台报BeanCreationException,说SqlSessionFactory创建失败,或者MapperScan扫描到的接口无法生成代理对象。原因:pom.xml里既声明了mybatis-spring-boot-starter,又声明了mybatis-plus-boot-starter,两个starter都会尝试配置SqlSessionFactory,冲突得莫名其妙。还有种情况是传递依赖带进来的老版本mybatis与当前MyBatis-Plus不兼容。
解决:强制全局搜索pom里的mybatis关键字,只保留mybatis-plus-boot-starter,然后mvn clean。另外一个快速判断手段是在Idea右侧Maven面板里点Dependency Analyzer,直接能看到哪些依赖里带着旧版mybatis,找到后在对应依赖的exclusion里排除掉。这类问题在java面试题里很常被拿来问“你项目里遇到过依赖冲突吗”,你可以用这次排查经历作为真实案例。
5.4 Vue打包放进SpringBoot后路由失效
现象:开发环境Vite代理下一切正常,执行npm run build后把dist目录放进SpringBoot的src/main/resources/static里,访问首页正常,但刷新某个子路由直接404。原因:Vue Router默认用history模式,页面路径是真实URL路径,比如/admin/book,刷新时浏览器直接请求SpringBoot的/admin/book,但这个路径在后端没有对应的Controller,于是返回404。开发环境的Vite服务器对这种情况会回退到index.html,所以开发时根本发现不了。
解决:常见两种方案。第一种是路由模式改为createWebHashHistory,页面URL变成带#/admin/book的hash形式,刷新时浏览器只请求根路径index.html,后端不用做任何处理,这是毕设最省心的方案。第二种是保持createWebHistory,在SpringBoot里写一个转发Controller,把所有非/api开头的路径转发到index.html。另外把Vue打包放进SpringBoot里后,生产环境的静态资源访问路径要用相对路径,否则部署到服务器子目录时会白屏。
5.5 Node.js版本过高导致npm install失败
现象:前端项目在npm install时报ERESOLVE unable to resolve dependency tree,或者node-sass的主版本和node版本不匹配导致编译失败。原因:有些老毕设前端用的是node-sass,它对Node版本要求非常苛刻,Node 16以上基本装不上。新项目用Vite+Element Plus后,node-sass这类问题少了,但Element Plus某些版本对Vue版本也有要求,npm解析依赖树时就会报ERESOLVE。
解决:顺着报错提示往上翻,看是哪个包冲突。如果是老项目,最稳的是用nvm安装Node 14或Node 16,把node_modules整个删掉重新npm install。如果是新项目,先把package.json里所有依赖的版本范围改成不带^的精确版本,比如vue-router从^4.1.6改成4.1.6,让npm不用做范围解析,一般就能过。前端环境问题本质上是版本矩阵太大,没有银弹,但用nvm做切换可以回避大多数问题。
5.6 借阅数量并发超扣
现象:两个读者同时借同一本书,控制台打印了两条insert语句和两条update语句,但update的时候available可能从1减成了负数。原因:后端代码是“先查可用数量,再判断是否大于0,然后插入记录并扣减”,两个请求同时通过第一步检查后,第二步扣减就会出现超卖。这在演示时不容易触发,但答辩时老师问到并发场景就很难圆场。
解决:把扣减SQL改成带条件的一次性更新,例如update tb_book set available = available - 1 where id = ? and available > 0,如果影响行数为0说明库存已不足,再回滚事务。这样把“读判断”和“写扣减”合并成一个原子操作,不用引入分布式锁这种复杂度。这个细节能体现真实开发意识,论文的“系统可靠性与并发控制”小节也可以补上这一笔。
6. 答辩前最后一晚:按验收清单走一遍并补上最值钱的进阶点
6.1 一份可以直接照着检查的验收清单
拿到这套交付内容时,先别急着看论文,先按下面的顺序把系统跑起来验一遍,这比背稿有用得多。
| 检查项 | 操作 | 预期结果 |
|---|---|---|
| 数据库初始化 | 执行sql脚本 | 六张表全部生成,无报错 |
| 后端启动 | mvn spring-boot:run | 8080端口启动,日志无红字 |
| 管理员登录 | 前端账号admin登录 | 跳转到管理后台,菜单齐全 |
| 图书录入 | 新增一本图书 | 页面表格出现新纪录 |
| 借阅流程 | 读者账号借书 | 可借数量减1,借阅记录出现 |
| 归还流程 | 点击归还 | 可借数量加1,状态变已归还 |
| 逾期展示 | 把dueTime改早再查 | 状态显示逾期 |
| 打包部署 | npm run build后放static | 刷新页面不404 |
拿笔逐行打勾,任何一行卡住都回到对应章节排查。演示视频录制时,就读着这张表从头到尾点一遍,视频里每个界面都对应系统的真实功能,比临时录屏改代码靠谱得多。
6.2 从“能跑”到“能讲”:给答辩加分的三个小功能
如果你的时间还有富余,不要在现有页面堆功能,而是挑一个点做深。我的教训是当年为了显得功能多,硬塞了一个论坛模块,结果代码里到处是bug,答辩时被老师追问数据库关联关系,当场翻车。后来我带学弟学妹都让他们只加小而完整的东西:第一个是借阅趋势折线图,用ECharts按周统计借阅量,这是统计数据落地的体现;第二个是读者借阅排行,最上面的人显示“本月借阅之星”,很直观;第三个是图书封面上传并回显,用它把文件上传、静态资源映射、表单回显串起来。这三个功能分别覆盖数据可视化、排序查询、文件上传三个方向,任何一个都能讲出几分钟的完整技术点。
答辩前做一个动作就够了:把后端项目里的Swagger界面和前端页面并排截两张图放进PPT,一张讲接口设计,一张讲页面流程,然后口头说清楚数据从点击按钮到数据库落盘的每一步。能讲完整链路,比堆十张截图有说服力得多。这套SpringBoot+Vue校园数字化图书馆系统到了这一步,才算真正从一个zip变成了你自己的作品。希望帮到你。
本文还有配套的精品资源,点击获取