看到这个标题,我第一反应是:又是一个典型的课程设计/毕业设计项目。说实话,SpringBoot + Vue这套组合在Java全栈开发里已经快成“标准答案”了,网上相关的源码、教程数不胜数,但真正能把来龙去脉讲清楚、能让人照着做出来的内容并不多。很多人下载了一套源码,跑不起来,或者跑起来了不知道怎么改、不知道怎么答辨,最后只能干瞪眼。这篇博客我就以图书管理系统为例,把从数据库设计到前后端联调这条线完整地拆开揉碎讲一遍,重点是那些代码注释里不会写的东西,希望你能真正把它变成自己的东西。
这个项目能解决什么问题?说白了就两件事:一是把图书的入库、检索、借阅、归还这些日常业务从Excel和手工记录里解放出来;二是给你一个麻雀虽小五脏俱全的全栈练手机会。适合谁看?正在做课设/毕设的学生、想从SSH/Servlet往SpringBoot迁移的Java初学者、以及想快速搭一套管理系统模板接私活的开发者。先给你吃颗定心丸:这套技术栈没有任何高深的地方,跟着这篇博客走,你不仅能跑起来,还能搞清楚每一行配置、每一个接口在干什么。
1. 项目整体设计与技术栈选型
1.1 为什么是SpringBoot + Vue这套组合
先说结论:这个组合不是性能最优解,也不是功能最全解,但它是当下Java后端入门和中小型管理系统开发中性价比最高的组合。
SpringBoot能成为主流,核心原因是它把Spring那一堆繁琐的XML配置几乎全部干掉,内置Tomcat意味着你不需要单独装容器,一个java -jar就能启动,这对课设场景来说是致命的便利——答辩的时候不用现场配置各种环境变量,少一个不安定因素。同时SpringBoot的自动配置机制(AutoConfiguration)会基于你引入的依赖帮你把常见组件(数据源、ORM、Web MVC)配置好,你只需要在application.yml里写少量自定义项。这在开发效率上的提升,只有经历过SSM时代的人才有切身体会。
Vue这边,我推荐用Vue 2 + Element UI,原因很简单:Vue 3 + Element Plus的组合在组件生态的成熟度和示例代码量上,对新手没有Vue 2友好。尤其图书管理这种系统,需要的表格、表单、弹窗、分页组件,Element UI基本都是开箱即用,百度一搜对应组件的用法也能立刻找到答案。如果为了追求新版而选Vue 3,遇到一个组件API变化排查半天,那就本末倒置了。当然,你要是已经对Vue 3很熟,直接用也没问题,后端的接口设计不区分前端版本。
MyBatis在这里的作用是持久层映射。有人会问,SpringBoot官方推荐Spring Data JPA,为什么不用?我的看法是:管理系统涉及大量多表联查和条件统计,MyBatis的SQL可控性和动态SQL能力更适合这种场景。JPA的自动建表和HQL在简单CRUD时很爽,但一旦遇到"根据多个非必填条件组合筛选图书"这种典型需求,你得拼Criteria或者写@Query,远不如MyBatis的<where>+<if>标签来得直观。而且国内公司用MyBatis的比例还是很高的,从学习到就业的角度,选MyBatis更实用。
1.2 功能模块划分:先画清楚边界再动手
很多新手拿到这种项目,上来就写代码,结果写着写着发现"读者管理"里的删除操作连带把历史借阅记录也搞乱了。这属于典型的没有做模块边界划分。
图书管理系统按角色划分,核心模块分为三大块:
- 图书管理模块:图书信息增删改查、分类管理、库存管理。操作者:管理员。
- 读者管理模块:读者信息维护、借阅证状态管理(挂失、注销)。操作者:管理员。
- 借阅管理模块:借书、还书、续借、逾期处理、借阅历史查询。操作者:管理员 + 读者(读者仅查询)。
我需要特别强调一个容易被忽略的设计点:借阅流程不能和图书表直接耦合。图书表只负责记录"当前库存数量",而借阅记录表负责保存"哪本书被谁借走了"。当你执行借书操作时,系统做两件事——往借阅记录表插入一条数据、把图书表的库存量减一。这两件事必须放在同一个数据库事务里,否则就会出现"记录插入了但库存没减"的数据不一致问题。这一点在第五章我会详细讲代码实现。
从技术实现角度,后端构建思路是标准的分层架构:
controller(接收请求、参数校验、返回JSON) ↓ service(业务逻辑处理、事务控制) ↓ mapper(MyBatis数据访问层) ↓ MySQL(数据存储)前端构建思路是组件化页面:
Vue页面组件(view层) ↓ 调用api工具模块(axios封装) ↓ 对接后端RESTful接口2. 数据库设计:建表是后面少踩坑的根基
2.1 核心表结构与字段设计
做过几个管理系统之后你会发现,建表设计其实有套路。以这个图书系统为例,最少需要四张核心表,再加两张辅助表。我把每张表的字段、类型、注释都整理出来,你可以直接抄作业。
用户表(t_user)——同时承载管理员和读者两种角色,通过role字段区分:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int(11) PRIMARY KEY AUTO_INCREMENT | 用户ID |
| username | varchar(50) UNIQUE | 登录用户名 |
| password | varchar(100) | 密码(建议BCrypt加密) |
| real_name | varchar(50) | 真实姓名 |
| role | int(2) | 角色:0=管理员,1=读者 |
| phone | varchar(20) | 联系电话 |
| status | int(1) | 状态:0=正常,1=锁定/挂失 |
图书表(t_book):
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int(11) PRIMARY KEY AUTO_INCREMENT | 图书ID |
| book_name | varchar(200) | 书名 |
| isbn | varchar(20) | ISBN号 |
| author | varchar(100) | 作者 |
| publisher | varchar(100) | 出版社 |
| category_id | int(11) | 分类ID,关联分类表 |
| total_stock | int(11) | 总库存 |
| available_stock | int(11) | 可借库存(关键字段) |
| location | varchar(50) | 馆藏位置(如A区3排) |
| create_time | datetime | 入库时间 |
借阅记录表(t_borrow_record):
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int(11) PRIMARY KEY AUTO_INCREMENT | 记录ID |
| book_id | int(11) | 图书ID |
| user_id | int(11) | 借阅人ID |
| borrow_time | datetime | 借出时间 |
| due_time | datetime | 应还时间 |
| return_time | datetime | 实际归还时间(NULL表示未还) |
| status | int(2) | 状态:0=借出中,1=已归还,2=已逾期 |
图书分类表(t_category):id、category_name、sort_order,就是标准的字典表,用来做下拉筛选和归类展示。
这套设计的核心思想是冗余可用字段 + 精确状态控制。比如total_stock和available_stock分开设计,虽然逻辑上可以只存一个总数再加加减减,但在列表展示和统计场景下,冗余字段能减少计算量,而且不容易出并发问题——一个字段同时被读和写,如果中间查询一次就拿到脏数据了。
2.2 建表SQL与索引设计要点
贴一段核心的建表SQL,注意我把索引和默认值都带上了:
CREATE DATABASE IF NOT EXISTS book_system DEFAULT CHARACTER SET utf8mb4; USE book_system; CREATE TABLE t_book ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '图书ID', book_name VARCHAR(200) NOT NULL COMMENT '书名', isbn VARCHAR(20) DEFAULT NULL COMMENT 'ISBN号', author VARCHAR(100) DEFAULT NULL COMMENT '作者', publisher VARCHAR(100) DEFAULT NULL COMMENT '出版社', category_id INT DEFAULT NULL COMMENT '分类ID', total_stock INT DEFAULT 0 COMMENT '总库存', available_stock INT DEFAULT 0 COMMENT '可借库存', location VARCHAR(50) DEFAULT NULL COMMENT '馆藏位置', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间', KEY idx_category (category_id), KEY idx_book_name (book_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书表';之所以用utf8mb4而不是utf8,是因为如果书名里有生僻字或者特殊字符,utf8会报错或者乱码。这个坑我见过不止一次,有些人图书信息导进去后一堆问号,就是因为数据库字符集不对。
索引方面,我的建议是少而精准。像book_name这种高频查询字段建立普通索引就够了,不必搞复合索引——你很难预判用户的筛选维度,索引建多了不仅浪费空间,还会拖慢INSERT和UPDATE的速度。业务量到了索引真正需要调优的量级,这套代码的重构重点也不是索引了。
2.3 状态字段用整型而不是字符串
实体类里的status字段,我非常推荐用int类型配合注释来做,而不是用"正常"、"借出中"这样的字符串。原因有三点:第一,整型在数据库存储和检索上比字符串快;第二,前端可以用数字直接做条件判断和映射到标签;第三,后续扩展状态值(比如加一个"已预约"状态)不需要改代码里的字符串常量,只需要加一个数字枚举值。
实际开发中,我会在后端定义一个常量类或枚举类:
public class BorrowStatus { public static final int BORROWED = 0; // 借出中 public static final int RETURNED = 1; // 已归还 public static final int OVERDUE = 2; // 已逾期 }还有一个小细节:数据库表字段命名统一用下划线风格,实体类用驼峰风格。虽然MyBatis可以配置下划线自动转驼峰,但如果你在写resultMap的时候搞混了大小写,排查起来很浪费生命。从一开始就约定好,通过设置map-underscore-to-camel-case: true,实体类和表字段之间的映射就交给框架,自己不用在XML里写几百行resultMap。
3. 后端核心模块:从接口到业务的完整链路
3.1 项目结构初始化与基础配置
创建SpringBoot项目的姿势我就不多说了,IDEA里直接Spring Initializr,或者去start.spring.io生成都行。必要的核心依赖如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency>这里我要单独聊一下PageHelper分页插件。管理系统的列表接口几乎全部是分页查询,手写LIMIT当然能实现,但每个接口都要向Service层传pageNum和pageSize,再手动计算总页数,重复劳动太多。PageHelper的用法极其简单,在查询执行前调用PageHelper.startPage(pageNum, pageSize),紧接着执行的Mapper查询就会被自动拦截并改写为带LIMIT的SQL,同时返回的PageInfo对象里会包含总记录数、总页数等现成数据。这个库在课设中使用频率很高,答辩的时候被问到"分页怎么实现的",你要能回答出"基于MyBatis拦截器对Executor进行拦截,改写SQL",这就算答到点子上了。
前后端分离模式下的配置还有三个关键点:
- 跨域配置。后端端口默认8080,前端Vue开发服务器端口通常是8081或5173,两者端口不同会产生跨域问题,需要在后端配置CORS跨域策略(或通过代理转发解决)。
- 统一响应体。定义一个
Result类,包含code、message、data三个字段,所有接口都返回这个结构,前端统一处理错误。这个习惯很多新手没有,到处乱返回Map,前端根本不知道后端是不是报错了。 - 接口路径规范。模块资源采用RESTful风格,比如
/api/book/list、/api/book/add、/api/borrow/doBorrow。虽然课程设计不强制要求,但良好的路径设计能让你答辩时加不少印象分。
3.2 登录鉴权与拦截器实现
图书管理系统需要登录后才能操作,但对课设这个体量来说,引入Spring Security + JWT可能有点大题小作,而且配置复杂度会劝退不少人。我的建议是用简单的拦截器(HandlerInterceptor) + Session或Token的方式来实现。
给一个基于Token的实现思路。前端登录成功后,后端生成一个Token(可以用UUID,也可以用JWT),把它存到内存Map或Redis里,然后返回给前端。前端在后续请求的Header里带上这个Token,后端写一个拦截器统一校验:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains("/user/login")) { return true; } String token = request.getHeader("token"); // 校验Token是否存在且有效,这里省略具体校验逻辑 if (token == null || !TokenStore.isValid(token)) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\"}"); return false; } return true; } }把拦截器注册到WebMvcConfigurer里,指定拦截路径为/**,排除掉登录接口和静态资源路径。
在登录密码加密方面,无论是不是课设,我都建议不要明文存密码。用BCryptPasswordEncoder,它不需要你手动加盐,每次哈希的结果都不相同,安全性足以应对这种业务场景。
3.3 图书管理接口设计:从CRUD到条件检索
图书管理是核心模块,接口设计上除了基础的增删改查,必须包含分页 + 多条件组合查询。这地方不夸张地说,是一道大分水岭——实现得漂不漂亮,直接决定你是及格还是优秀。
先定义接口清单:
| 功能 | 请求方式 | 路径 | 参数 |
|---|---|---|---|
| 分页查询图书 | GET | /api/book/list | pageNum, pageSize, bookName, categoryId |
| 新增图书 | POST | /api/book/add | 图书信息JSON |
| 修改图书 | PUT | /api/book/update | 图书信息JSON |
| 删除图书 | DELETE | /api/book/delete/{id} | 图书ID |
| 查询全部图书分类 | GET | /api/category/list | 无 |
查询接口用MyBatis动态SQL实现,Mapper XML里面长这样:
<select id="selectBookList" resultType="com.example.entity.Book"> SELECT * FROM t_book <where> <if test="bookName != null and bookName != ''"> AND book_name LIKE CONCAT('%', #{bookName}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> </where> ORDER BY id DESC </select>注意我在这里用的是resultType而不是resultMap。因为实体类字段和表字段是下划线驼峰一一对应的,配置好map-underscore-to-camel-case之后,resultType就够用了。只有遇到多表联查需要额外展示字段时(比如借阅列表要同时显示书名和读者姓名),才需要写resultMap做映射。这也是MyBatis开发中一个"懒人但正确"的习惯。
3.4 借阅归还:事务和数据一致性是核心考点
借书和还书是整个系统的业务核心,也是最容易被答辩老师追问的地方。借书的Service方法代码如下:
@Transactional(rollbackFor = Exception.class) public void borrowBook(Integer bookId, Integer userId) { // 1. 查询图书,校验库存 Book book = bookMapper.selectById(bookId); if (book == null) { throw new BusinessException("图书不存在"); } if (book.getAvailableStock() <= 0) { throw new BusinessException("库存不足"); } // 2. 插入借阅记录,状态为借出中(0) BorrowRecord record = new BorrowRecord(); record.setBookId(bookId); record.setUserId(userId); record.setBorrowTime(new Date()); record.setDueTime(DateUtil.addDays(new Date(), 30)); // 默认借期30天 record.setStatus(BorrowStatus.BORROWED); borrowRecordMapper.insert(record); // 3. 扣减库存 book.setAvailableStock(book.getAvailableStock() - 1); bookMapper.updateById(book); }注意@Transactional(rollbackFor = Exception.class)这段注解。Spring默认只回滚RuntimeException,如果业务方法里抛的是自定义异常且不继承运行时异常,事务是不会自动回滚的,所以一定要显式指定rollbackFor。这在答辩时十有八九会被问,背下来不吃亏。
还书流程反过来,把return_time设为当前时间、状态改为已归还(1)、对应图书的可借库存加一。同时判断实际归还时间和due_time做对比,超过就算逾期。
还有一个并发问题值得提出来。如果两个用户同时借同一本只剩一本库存的书,上面的代码可能两个请求都通过了库存校验,然后都去扣减库存,导致库存变成-1。这在真实的图书管理场景中概率极低,但如果老师在答辩时问"高并发下怎么保证不超卖",你可以回答:在扣减库存的SQL中加上条件判断,例如:
UPDATE t_book SET available_stock = available_stock - 1 WHERE id = #{bookId} AND available_stock > 0如果影响行数为0,说明库存被并发扣减完,抛异常回滚。这种基于乐观锁思想的处理方案,既简单又能自圆其说,比单纯讨论悲观锁更有说服力。
4. 前端Vue实现:从页面到路由再到接口对接
4.1 前端工程化和目录结构
Vue项目建议用Vue CLI创建(如果你对Vite熟悉,用Vite也行,但课设我用Vue CLI更多,因为它的webpack配置封装的比较完整,遇到问题百度起来案例更多):
npm install -g @vue/cli vue create book-frontend创建一个干净的目录结构:
src/ api/ # 后端接口封装 book.js user.js borrow.js router/ # 路由配置 index.js views/ # 页面组件 Login.vue Layout.vue BookList.vue BookAdd.vue BorrowList.vue ... store/ # Vuex状态管理(登录信息、用户信息) utils/ # 工具模块(axios封装) App.vue main.js4.2 axios统一封装:把错误处理做在前面
前端对接后端,我把axios封装到utils/request.js里,统一做三件事:设置baseURL、附加Token请求头、统一拦截错误码。
import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:附加Token request.interceptors.request.use(config => { const token = sessionStorage.getItem('token') if (token) { config.headers['token'] = token } return config }) // 响应拦截器:统一处理错误 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.message || '请求失败') if (res.code === 401) { router.push('/login') } return Promise.reject(new Error(res.message)) } return res }, error => { Message.error('网络异常,请稍后重试') return Promise.reject(error) } ) export default requestbaseURL这里直接写/api,需要在前端项目的vue.config.js里配置代理,将请求转发到后端8080端口:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }这就同时解决了开发环境的跨域问题,也算一个标准做法。真正到了生产环境,把前端打包后的静态资源放进后端项目里,就不存在代理了,因为前后端同源了。
4.3 核心页面拆解:图书列表与借阅弹窗
图书列表页是用户最常看到的页面,核心需求就三个:条件搜索框、数据表格、分页。
Element UI的el-table和el-pagination基本是固定搭配。绑定数据时用el-table-column展示书名、作者、出版社、库存等字段,库存少于5本的可以在列里用标签高亮显示:
<el-table-column label="可借库存" width="100"> <template slot-scope="scope"> <el-tag :type="scope.row.availableStock < 5 ? 'danger' : 'success'"> {{ scope.row.availableStock }} </el-tag> </template> </el-table-column>借阅操作弹窗,我用el-dialog内嵌表单实现。点击"借阅"按钮时,把当前行数据对象传进弹窗,表单里让管理员选择借阅人(从用户列表查询),然后提交到后端/api/borrow/doBorrow。
这里有一个交互上的小细节值得注意:提交成功后要刷新列表和库存数据。否则前端页面上那本书的可借库存还是旧值,用户就会误以为借阅没成功。最简单的做法是请求结束后再调一次getList()方法,不要手动去改表格里的数据,因为借阅操作可能影响多条数据,手动维护容易出错。
4.4 Vue Router路由与动态菜单
因为系统有管理员和读者两个角色,比较好的实践是根据角色动态生成路由表。管理员登录后能看到图书管理、借阅管理、读者管理三个菜单;读者登录后只能看到图书查询和个人借阅记录。完成这个效果,可以在路由守卫里根据当前用户的role字段做判断:
router.beforeEach((to, from, next) => { const userInfo = JSON.parse(sessionStorage.getItem('userInfo')) if (to.path === '/login') { next() } else { if (!userInfo) { next('/login') } else if (to.meta.roles && !to.meta.roles.includes(userInfo.role)) { next('/403') // 无权限提示页 } else { next() } } })这种操作也叫路由元信息控制,在meta里声明哪些角色可以访问。简单、直观、够用,不会像动态菜单一样把代码复杂度拉那么高。
5. 联调与踩坑实录:那些文档里不会告诉你的问题
5.1 跨域配置踩过的坑
跨域是前后端联调遇到最多的问题。前端报错常常是No 'Access-Control-Allow-Origin' header is present on the requested resource。解决办法有两个方向:
方向一,后端加配置类:
@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); } }注意allowCredentials(true)时,allowedOriginPatterns("*")在SpringBoot 2.4以上版本才支持。版本不同API不一样,这也是一个会浪费十分钟的细节。
方向二,用Nginx反向代理,把前端请求转发到后端。不过课设阶段我不建议引入Nginx,技术栈又多了一门,还增加部署复杂度,配置里一个location写错了排查半天得不偿失。
5.2 MyBatis的“列名不存在”问题
新手最常见的报错是### The error may exist in Mapper Mapper.xml和BadSqlGrammarException。原因两极分化:要么是SQL写错了,要么是表字段名不匹配。排查思路先看控制台打出的SQL,对比SELECT的字段是不是都在表里存在——不要凭记忆写字段,直接去数据库执行一下表结构命令。
还有一个情况:数据库字段是user_name,但实体类属性是username,MyBatis转驼峰失败。这时候需要检查全局配置是否生效,早年间SpringBoot集成MyBatis还没那么自动化时,这块没少折腾。现在只要在application.yml里设置:
mybatis: configuration: map-underscore-to-camel-case: true这个配置项就是所谓的“下划线转驼峰”,它匹配的核心规则是:数据库列名user_name到实体类字段userName的自动映射。
5.3 日期前后端传值格式问题
借阅记录的borrowTime、dueTime字段前后端传递时经常出问题——默认序列化出来是时间戳,前端要显示成2024-01-25 14:30:00的格式还得处理一下。
最简单的方案是在实体类的日期字段上注解:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date dueTime;这里timezone一定要写GMT+8才能保证不会出现时间偏移8小时的情况。如果前端还需要接收日期做控件回显,传输格式统一为yyyy-MM-dd更友好,但查询返回建议带时分秒,显示更完整。
5.4 PageHelper分页无效或数据错乱
PageHelper使用中有一个很重要的约定:PageHelper.startPage()必须紧跟要分页的Mapper查询方法,中间不能穿插其他查询。如果中间先执行了别的SQL,分页参数会被那个查询消费掉,导致你的目标查询没有分页效果。
这个问题我在帮别人排查代码时遇到过不少次。还有一个坑是:当你用了PageHelper.startPage后,如果紧接着的Mapper方法返回类型是List,它会被PageHelper包装成Page对象,里面带有分页信息。但如果你在这个方法后又执行了一个查询,第二个查询也会被分页生效。所以,为了保险,把分页查询和普通的查询方法拆开写在不同Mapper方法里,不要共用一个方法依赖分页参数动态判断,代码意图会清晰得多。
5.5 Maven依赖冲突与打包失败
依赖冲突的典型报错是ClassNotFoundException和NoSuchMethodError。以SpringBoot 2.x为例,引入mybatis-spring-boot-starter时不需要再单独引入mybatis和mybatis-spring,否则版本可能冲突。同样,mysql-connector-java的runtimescope记得加上,否则打包时会把数据库驱动也打进去,纯属浪费。
打包用mvn clean package,会生成一个jar包。如果打包时提示spring-boot-maven-plugin找不到主类,检查pom里有没有配置<mainClass>,或者编译后有没有target目录下出现class文件。大部分打包失败都是本地环境问题——JDK版本和项目编译版本不一致是最常见元凶,解决方式是保持IDEA的Project Structure里SDK版本和pom里的<java.version>一致。
6. 部署上线与项目答辩的实用经验
6.1 本地部署全流程
实际操作时,我建议按以下顺序跑通整个项目:
- 安装MySQL 5.7或8.0,执行建库建表脚本,导入初始测试数据(至少20本图书、5个用户)。
- 用IDEA打开后端项目,等待Maven下载依赖,修改
application.yml中的数据库账号密码。 - 直接运行
main方法启动后端,访问http://localhost:8080/api/...验证接口通不通。 - 用VSCode或WebStorm打开前端项目,执行
npm install安装依赖(网络不好的情况下这个步骤可能很漫长,建议提前执行)。 - 执行
npm run serve启动开发服务器,浏览器访问http://localhost:8081,登录系统走通核心流程。
有一说一,部署环节最容易出问题的地方是JDK或Node版本不兼容。npm install后报node-sass错误,基本是Node版本太高或太低;npm install慢、卡住,可以换国内镜像源:
npm config set registry https://registry.npmmirror.com6.2 项目演示顺序和拿分技巧
答辩演示不要上来就对着屏幕把功能全部点一遍,那样老师会困。我推荐的演示顺序是:
- 先登录,讲清楚三种角色权限的区别。
- 展示图书列表的分页检索功能,重点提"图书名模糊查询 + 分类精确筛选"背后的动态SQL实现。
- 现场借一本书,演示库存扣减、借阅记录生成。
- 还书,演示库存恢复,顺带展示逾期判断逻辑。
- 打开数据库,展示数据在库里真实变化的过程,这一招老师会比较认可——证明你不是只做了个Mock数据的前端界面。
扩展方面,这个系统至少可以往三个方向延伸:增加预约功能(图书被借光后提交预约请求)、引入Redis缓存热点书籍信息、把统计报表做成可视化图表。这些可以当“不足与展望”放在论文结尾,没有要求一定要实现,但能体现出你对系统演进方向有思考,加分项。
写在最后的话
经过这一整套流程,图书管理系统已经从登录页到数据库完整地跑通了。我能负责任地说,这套SpringBoot + Vue + MySQL + MyBatis的组合,在学业、工作、接私活三个场景下都有很强的迁移价值。前端的组件化思路,后端的分层边界、事务控制思路,换一个业务领域几乎可以原样复用——你把Book换成Product就是进销存,换成Course就是在线选课,换成Equipment就是设备管理系统。
最后再分享一个小技巧:拿到别人的源码跑不起来时,别急着道德审判代码,先看日志——80%的问题都出在数据库连接、端口占用、依赖缺失这三件事上。学会看堆栈第一行,比学会Google搜索更省时间。祝你的项目顺利跑通,答辩沉着应对。