从2020年之后,图书馆这类公共场所的运营方式发生了很大变化,限流、预约、无接触借还成了常态。当时我正好在做一个面向高校图书馆的信息管理系统,技术栈就是标题里那套经典组合:SpringBoot后端、Vue前端、MySQL数据库,整套源码现在也能直接跑起来。这篇博文就把这个项目的核心设计思路、关键实现细节、环境搭建步骤和实际踩过的坑整理出来,给正在做毕业设计、课程设计,或者想快速上手前后端分离项目的朋友一个参考。
1. 项目概述与需求拆解
1.1 疫情催生的图书馆业务痛点
传统的图书馆管理系统通常只考虑图书录入、借还登记、读者管理这些基础功能,但在疫情背景下,业务场景多了几个硬性要求:
- 读者入馆需要预约,且图书馆要控制同一时段在馆人数。
- 图书借阅最好能线上预约,到馆后无接触取书,减少在馆停留时间。
- 读者点击图书详情时,需要明确看到馆藏数量、可预约数量、当前借出数量。
- 管理员需要快速处理借阅审核、预约核销、逾期记录等操作。
所以这套系统在常规借阅管理之外,专门设计了预约模块和入馆登记模块,这也是它区别于普通图书管理Demo的关键点。整个项目可以拆成用户端和管理员端两个视角:用户端负责注册登录、检索图书、预约借书、查看个人借阅记录;管理员端负责图书管理、读者管理、借阅审核、预约处理和公告发布。前后端分离之后,两端通过JSON格式的接口通信,工作量清晰,也方便后期扩展移动端。
1.2 核心功能模块划分
我把系统的功能模块整理成了一张清单,做项目的时候照着这个清单开发就不会乱:
- 用户模块:注册、登录、个人信息修改、密码修改。
- 图书模块:图书列表、按书名/作者/ISBN检索、图书详情、馆藏状态展示。
- 预约模块:读者预约图书、取消预约、管理员审核预约、预约超时处理。
- 借阅模块:管理员登记借出、读者在线续借、到期归还登记、逾期记录。
- 公告模块:管理员发布公告,用户端首页展示最新公告。
- 统计模块:按月统计借阅量、图书分类占比、读者借阅排行。
其中预约模块是这次疫情背景下特别加重的部分,也是这个项目和早期图书管理系统最大的区别。预约功能的逻辑并不复杂,但涉及图书库存状态的变更,需要考虑并发情况,属于后端实现中的重点。
1.3 项目的学习价值与适用人群
这个项目很适合作为JavaWeb方向的练手项目,原因有三个:第一,技术栈主流,SpringBoot加Vue是当前中小型管理系统的主流组合,简历上写这个不落伍;第二,功能复杂度适中,既有CRUD,又有预约状态流转、借阅记录查询这类稍微带点逻辑的功能,不会太简单也不会难到劝退;第三,MySQL表结构清晰,字段设计贴近实际业务,可以用来练习数据库设计能力。
如果你是准备秋招的在校生,或者正在做毕业设计,这套源码可以作为一个不错的起点。不过我不建议直接拿代码交差,最好是把它跑起来,读懂核心逻辑,再改几个自己感兴趣的功能点,这样面试被问的时候才答得上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型解析:为什么是SpringBoot加Vue加MySQL
2.1 后端选SpringBoot的理由
早些年做这类管理系统,Java后端常用SSM,也就是Spring加SpringMVC加MyBatis,需要写大量XML配置,一个web.xml就能劝退不少人。SpringBoot把这些配置自动化了,内嵌Tomcat,打成的Jar包直接java -jar就能启动,这个体验对新手非常友好。
SpringBoot最核心的价值是自动配置和Starter生态。引入spring-boot-starter-web就拥有了SpringMVC加内嵌Tomcat,引入mybatis-plus-boot-starter就能直接用MyBatis-Plus的单表CRUD方法,省掉大量重复的Mapper XML。对于图书馆管理系统这种业务以单表操作为主、少量多表联查的项目,这套组合开发效率非常高,而且社区资料多,遇到问题基本都能搜到解决方案。
2.2 前端选Vue的理由
图书馆管理系统的前端主要是表格、表单、弹窗、分页这类中后台界面,Vue加Element UI组件库是这类场景的经典搭配。Vue的响应式机制让我们只需要维护数据状态,不用像用jQuery那样手动操作DOM,开发效率提升明显。
Vue Router负责页面路由,比如路由守卫可以判断用户没有登录时自动跳转到登录页;Vuex或Pinia负责全局状态管理,比如在登录后保存用户信息和token。这套源码里我用的是Vue 2加Vue Router 3加Vuex 3的组合,原因是这套组合在Element UI的兼容性上最稳定,组件库报错的情况最少。
2.3 数据库选MySQL的理由
MySQL在这个项目里的角色是唯一的持久层存储,图书数据、用户数据、借阅记录、预约记录全部落在MySQL里。选择它是很自然的事情:开源免费,云端和本地部署都方便;InnoDB存储引擎支持事务和外键,借书、还书这类操作有事务保障会更安全;社区资料极多,遇到中文乱码、时区报错、连接数打满这类问题,解决方案一搜一大把。
另外单表数据量在几十万级别以内时,MySQL配合合适的索引,性能完全够用。图书馆管理系统的数据量通常不会特别大,没必要引入Redis做缓存、引入Elasticsearch做全文检索这类重型组件。当然如果后续要扩展热门图书排行榜、高频检索词统计,再引入Redis来做热点缓存也是顺手的事情。
2.4 版本选择上的经验教训
版本选择是我在这次项目中实际吃过亏的地方。最开始图新鲜用了SpringBoot 2.7加JDK 17,结果MyBatis-Plus版本跟不上,动态SQL老报错,后来退回SpringBoot 2.3.12.RELEASE加JDK 1.8才稳定下来。
推荐的开发版本组合如下:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8或11 | 1.8兼容性最好,大多数教程都基于这个版本 |
| SpringBoot | 2.3.x或2.5.x | 稳定,资料多,避免用3.x这种太新的版本 |
| MySQL | 5.7或8.0 | 5.7保守,8.0注意驱动和时区配置 |
| Node.js | 12.x或14.x | 对Vue 2项目兼容性好 |
| Maven | 3.6.x或3.8.x | 常规版本即可 |
| Vue | 2.6.x加Element UI 2.15.x | 组件生态最稳 |
如果你们老师的教学环境或者公司的生产环境用了较新的版本,也别慌,按我后面第6章的环境搭建步骤来,把版本对齐基本就没问题。核心代码本身没有用到太特殊的语法,跨版本兼容性还是不错的。
3. 数据库设计:关键表结构与字段说明
3.1 用户表设计
用户表是整个系统的基石,考虑到了读者和管理员的区分,用role字段来标记身份:
sql复制CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录账号',
`password` varchar(100) NOT NULL COMMENT '密码,BCrypt加密',
`nickname` varchar(50) DEFAULT NULL COMMENT '姓名/昵称',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`role` tinyint(4) DEFAULT '1' COMMENT '角色:1读者,2管理员',
`status` tinyint(4) DEFAULT '1' COMMENT '状态:1正常,0禁用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
username设置唯一索引,注册的时候避免重复账号;密码不要存明文,用BCrypt加密,即使数据库泄露,密码也不会轻易被还原。这个项目里我没把用户信息拆成读者表和管理员表,而是统一放到一张表里用role区分,省去了多表联查的麻烦,对这个体量的系统来说是划算的设计。
3.2 图书表设计
图书表需要考虑两个层面的信息:一是图书本身的元数据,比如书名、作者、出版社;二是馆藏状态,比如总库存、当前可借数量、被借出的数量、被预约的数量。
sql复制CREATE TABLE `t_book` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`isbn` varchar(30) DEFAULT NULL COMMENT 'ISBN编号',
`book_name` varchar(200) NOT NULL COMMENT '书名',
`author` varchar(100) DEFAULT NULL COMMENT '作者',
`publisher` varchar(100) DEFAULT NULL COMMENT '出版社',
`category` varchar(50) DEFAULT NULL COMMENT '分类',
`total_stock` int(11) DEFAULT '0' COMMENT '总库存',
`available_stock` int(11) DEFAULT '0' COMMENT '可借库存',
`borrowed_count` int(11) DEFAULT '0' COMMENT '已借出数量',
`reserved_count` int(11) DEFAULT '0' COMMENT '被预约数量',
`location` varchar(100) DEFAULT NULL COMMENT '馆藏位置',
`status` tinyint(4) DEFAULT '1' COMMENT '1上架,0下架',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_book_name` (`book_name`),
KEY `idx_category` (`category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
一个细节:available_stock这个字段,可以在读者搜索图书列表时直接展示“可借”或“已被预约完”,不用每次去关联借阅表统计。代价是借出、归还、预约、取消预约时都要同步更新这个数字,属于用冗余字段换取查询效率,这在中小型项目里是常见做法。
3.3 借阅与预约表设计
借阅记录表是核心业务表,记录了每次借出和归还的流水:
sql复制CREATE TABLE `t_borrow_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '借阅人ID',
`book_id` bigint(20) NOT NULL COMMENT '图书ID',
`borrow_time` datetime DEFAULT NULL COMMENT '借出时间',
`due_time` datetime DEFAULT NULL COMMENT '应还时间,一般借出后30天',
`return_time` datetime DEFAULT NULL COMMENT '实际归还时间',
`status` tinyint(4) DEFAULT '0' COMMENT '0借出中,1已归还,2续借过',
`renew_count` int(11) DEFAULT '0' COMMENT '续借次数',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_book_id` (`book_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
预约表则是疫情期间新增的重点:
sql复制CREATE TABLE `t_reservation` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '预约人ID',
`book_id` bigint(20) NOT NULL COMMENT '图书ID',
`reserve_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '预约时间',
`pickup_time` datetime DEFAULT NULL COMMENT '到馆取书时间',
`status` tinyint(4) DEFAULT '0' COMMENT '0待处理,1已确认,2已取书,3已取消,4超时',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_book_id` (`book_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
状态字段是这里最容易出问题的点。预约的状态不是简单的是否完成,而是经历待处理、已确认、已取书、已取消、超时这几个阶段。每个阶段对应前端界面上不同的按钮状态和展示文案,后端接口里也要做状态校验,比如“已取消的预约不能再次确认”。
3.4 数据初始化与测试账号
项目里我放了一个init_data.sql,里面除了建表语句,还会插入几条测试数据:管理员账号admin,密码是123456;读者账号lisi,密码也是123456;图书数据准备了十几条不同分类的记录,涵盖Java、前端、文学、历史等类别,方便测试检索功能。
这里有一个经验:初始化SQL里不要用真实手机号,测试数据尽量用13800000000这种明显带有测试性质的号码,避免后期部署到公网后被别有用心的人拿去社工。另外图书封面的URL字段我用的都是本地静态资源路径,如果读者自己部署后图片显示不出来,把图片放到前端项目的public/images目录下就行。
4. 后端核心实现:SpringBoot接口的设计思路
4.1 项目分层结构与统一返回体
后端代码分了标准的四层:Controller接收请求,Service写业务逻辑,Mapper操作数据库,Entity对应表结构。这个分层方式在SpringBoot项目里非常常见,代码结构清晰,排查问题的时候顺着请求链路一层层看就行。
为了统一前端数据解析格式,我封装了一个Result返回体:
java复制public class Result<T> {
private Integer code; // 200成功,500失败
private String msg; // 提示信息
private T data; // 数据体
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("操作成功");
result.setData(data);
return result;
}
public static <T> Result<T> error(String msg) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMsg(msg);
return result;
}
}
没有这个统一返回体的时候,每个接口返回的数据格式都不一样,前端接口封装会非常痛苦。统一之后,前端axios拦截器只需要判断code字段是不是200,不是就直接弹错误提示,省了每个接口单独处理错误逻辑的麻烦。
4.2 登录认证与JWT实现
图书馆管理系统的登录不能和普通查询接口放在一起处理,需要无状态的认证机制。我用的是JWT,也就是JSON Web Token,服务端登录成功后签发一个带过期时间的token字符串给前端,前端每次请求放在请求头Authorization字段里,后端拦截器负责校验。
JWT的核心代码大概是这样的:
java复制public String generateToken(Integer userId, String role) {
return Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("role", role)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
实际项目中我建议把secretKey放到application.yml配置文件里,不要写死在代码中,这样打包后如果想换盐值,改配置就行,不用重新发包。另外JWT是无状态的,token一旦签发没法主动失效,对于“修改密码后禁止旧token继续使用”这种需求,可以结合token版本号或者黑名单实现,但这套源码里没有做这么复杂,读者知道这个局限就好。
4.3 图书检索与分页接口
图书检索是用户使用频率最高的接口。我用MyBatis-Plus的Page对象来实现分页,配合LambdaQueryWrapper做条件构造:
java复制@GetMapping("/list")
public Result<Page<Book>> list(@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keyword) {
Page<Book> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(keyword)) {
wrapper.like(Book::getBookName, keyword)
.or().like(Book::getAuthor, keyword)
.or().like(Book::getIsbn, keyword);
}
wrapper.eq(Book::getStatus, 1);
wrapper.orderByDesc(Book::getCreateTime);
bookService.page(page, wrapper);
return Result.success(page);
}
分页参数pageNum和pageSize都是前端传的,前端表格控件每次切换页码或者改变每页条数时重新请求一次。有一个细节要注意:多条件模糊查询用or()时要加括号,否则SQL拼接逻辑会变成book_name like ? or author like ? and isbn like ?,关键字的匹配范围就比预期大了。这也是我建议优先用MyBatis-Plus的LambdaQueryWrapper而不是自己拼SQL的原因,它能避免很多低级错误。
4.4 预约借阅流程的实现细节
预约借阅的流程涉及多张表的数据变更,是最容易出并发问题的地方。以“读者提交预约申请”为例,后端Service里做了这几件事:
java复制@Transactional(rollbackFor = Exception.class)
public Result<Void> reserveBook(Integer userId, Integer bookId) {
Book book = bookMapper.selectById(bookId);
if (book == null || book.getStatus() != 1) {
return Result.error("图书不存在或已下架");
}
if (book.getAvailableStock() <= 0) {
return Result.error("该图书暂无可借库存");
}
// 防止同一读者重复预约同一本书
Integer count = reservationMapper.selectCount(
new LambdaQueryWrapper<Reservation>()
.eq(Reservation::getUserId, userId)
.eq(Reservation::getBookId, bookId)
.ne(Reservation::getStatus, 3)
.ne(Reservation::getStatus, 4));
if (count > 0) {
return Result.error("你已预约过这本书,请勿重复预约");
}
// 扣减可借库存,增加预约数量
book.setAvailableStock(book.getAvailableStock() - 1);
book.setReservedCount(book.getReservedCount() + 1);
bookMapper.updateById(book);
Reservation reservation = new Reservation();
reservation.setUserId(userId);
reservation.setBookId(bookId);
reservation.setStatus(0);
reservationMapper.insert(reservation);
return Result.success(null);
}
这个接口加上了@Transactional注解,保证扣库存和插入预约记录要么都成功,要么都失败。即便如此,理论上有并发情况下两个读者同时请求最后一本可借图书的预约,可能都会通过库存判断,然后都执行扣减,导致库存变成负数。解决这个问题通常有两种思路:一是数据库层面加乐观锁或悲观锁,二是把“扣库存”改成“先判断再扣减”的SQL原子操作。作为中小型图书馆系统来说,并发量不高,上面的代码在大多数场景下已经够用了,但如果你想让代码更健壮,可以用UPDATE t_book SET available_stock = available_stock - 1 WHERE id = ? AND available_stock > 0这样的原子更新来替代先查询再更新。
4.5 application.yml配置注意事项
后端的application.yml配置是项目能否跑起来的关键,我这份配置里可以直接参考:
yaml复制server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
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
这里的serverTimezone=Asia/Shanghai特别重要,MySQL 8.0以上版本如果不设置时区,连接时会报The server time zone value is unrecognized。还有useSSL=false,本地开发时跳过SSL验证,避免一些环境下的警告提示。password这里你要改成自己本地MySQL的密码。
5. 前端实现:Vue页面与接口对接的实战经验
5.1 前端项目目录结构
前端项目用的是Vue CLI生成的标准目录,我做了简单的模块划分:
- views:页面级组件,比如Login.vue、BookList.vue、BorrowRecord.vue、AdminBook.vue。
- components:公用组件,比如分页组件、搜索栏组件、图书状态标签组件。
- router:路由配置,含路由守卫。
- store:Vuex状态管理,存放用户信息和token。
- api:接口请求封装,每个模块一个文件,比如book.js、user.js、borrow.js。
这样分组的好处是后期维护时,看到一个需求能快速定位到对应文件。比如要改借阅记录列表字段,直接去BorrowRecord.vue看,如果涉及接口地址变化,再去api/borrow.js修改,不用在大几十个文件中翻来翻去。
5.2 axios二次封装与跨域处理
前端所有接口请求我都走了一个统一的axios实例,这样配置拦截器和公共请求头非常方便。核心代码思路是这样的:
javascript复制import axios from 'axios'
import { Message } from 'element-ui'
import router from '@/router'
const request = axios.create({
baseURL: process.env.VUE_APP_BASE_API || '/api',
timeout: 10000
})
// 请求拦截器:自动附带token
request.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = token
}
return config
})
// 响应拦截器:统一处理返回结果
request.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
Message.error(res.msg || '请求出错')
return Promise.reject(new Error(res.msg))
}
return res
},
error => {
if (error.response && error.response.status === 401) {
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
router.push('/login')
}
Message.error(error.message || '网络异常')
return Promise.reject(error)
}
)
export default request
关于跨域,开发环境最简单的方式是配置Vue CLI的代理。在vue.config.js里这样写:
javascript复制module.exports = {
devServer: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
}
前端所有请求都以/api开头,开发服务器把请求代理到后端的8080端口,同时去掉路径中的/api前缀,这样前端代码里不用写完整的后端URL,后期后端换地址只需要改代理配置。有个坑是:后端Controller里不要也加了/api前缀,否则pathRewrite之后再和后端的/api拼接,会出现接口404,这种问题排查起来很费时间。
生产环境部署时没有http-proxy-middleware这个代理层,我后文会介绍Nginx方案。
5.3 核心页面实现说明
登录页面最关键的逻辑是登录成功后的处理:调用/user/login接口拿到token信息,把token和用户信息存到localStorage,然后用Vuex做一次状态同步,最后根据用户角色跳转到不同的首页。
图书列表页面是功能最丰富的页面,包含搜索表单、分页表格、预约或借阅按钮。这里有个交互细节要处理好:图书状态要用标签颜色区分,可借显示绿色,预约中显示橙色,已借完显示红色。这个判断可以封装成一个计算属性或者过滤器,但要注意数据源是后端返回的availableStock和reservedCount字段,不要在前端自己拼逻辑判断库存,以后端数据为准。
管理员台的图书管理页面用到了Element UI的el-table加el-dialog,新增和编辑共用一个弹窗组件,初始值通过props传入,弹窗打开时判断是新增还是编辑,分别做表单初始化和校验规则设置。我踩过一个坑:编辑弹窗打开后表单数据没有重置,导致上一次编辑残留数据影响下一次新增,解决方案是在弹窗关闭事件里调用resetFields()方法。
5.4 路由守卫与按钮权限控制
路由守卫是前端权限控制的第一道门。没登录用户直接访问管理后台页面,会被重定向到登录页,这个功能通过router.beforeEach实现:
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.meta.requiresAuth && !token) {
next('/login')
} else {
next()
}
})
按钮级别的权限控制,比如只有管理员能看到“删除图书”按钮,我用的是自定义指令v-permission。具体实现是读取Vuex中的userInfo,判断角色是否为管理员,不是管理员就直接把这个按钮从DOM上移除。这个方案比单纯用v-if写判断更统一,不会在模板中散布大量重复的条件判断。
顺带说一句,前端权限控制只是用户体验层面的东西,真正安全的后端接口一定要做鉴权。比如删除图书的接口,后端拦截器不能只校验token存在,还要校验token里携带的角色是管理员。否则别人手工调用接口就能删掉数据。
6. “可直接运行”环境搭建与部署实操
6.1 环境准备清单
“可直接运行”这四个字,我理解的意思是拿到源码后不需要改太多代码就能在本地把前后端跑起来。要达到这个效果,环境准备得先对齐。需要安装的软件和版本建议如下:
| 软件 | 推荐版本 | 用途 |
|---|---|---|
| JDK | 1.8 | 编译运行后端Java代码 |
| Maven | 3.6.3 | 管理后端依赖 |
| MySQL | 5.7或8.0 | 存数据 |
| Node.js | 12或14 | 运行前端构建工具 |
| IDEA | 2020.3及以上 | 打开后端项目 |
| VSCode | 最新版 | 打开前端项目 |
环境的安装顺序建议MySQL先装,因为后端启动时会自动连接数据库,如果数据库没启动,后端启动就会报错。我第一次演示项目的时候就犯了顺序错误,先启动了后端再装MySQL,结果一连串的连接异常,排查了好久才反应过来。
6.2 数据库初始化与配置修改
拿到源码后,第一步是执行init_data.sql。命令行方式是这样:
bash复制mysql -u root -p < init_data.sql
也可以用Navicat或MySQL Workbench可视化导入,双击打开SQL文件,然后执行即可。执行完可以验证一下,use library_db; show tables;应该能看到t_user、t_book、t_borrow_record、t_reservation、t_notice这些表。
第二步是修改后端application.yml里的数据库连接配置。把password改成你自己MySQL的密码,如果MySQL端口不是默认的3306,url中的端口也需要一起改。这里最需要注意的就是serverTimezone=Asia/Shanghai,我之前用8.0版本MySQL时,不配置时区启动必报错。
6.3 后端启动步骤
后端启动有三种方式,任选一种都行:
方式一,IDEA导入项目后,等待Maven自动下载依赖,然后找到LibraryApplication.java这个启动类,右键点击“Run”。这种方式最适合开发调试。
方式二,命令行方式:
bash复制cd 项目根目录
mvn clean package -DskipTests
java -jar target/library-0.0.1-SNAPSHOT.jar
第一次执行mvn命令会下载大量依赖,等个几分钟很正常,不要中途强制关闭。打包成功后,target目录下会生成jar文件,使用java -jar启动即可。
启动成功的标志是控制台出现SpringBoot的Banner图案和Started LibraryApplication in x.xxx seconds这样的日志。此时可以访问http://localhost:8080测试后端是否正常,因为项目里配置了欢迎页,能在浏览器看到提示信息说明后端没问题。
6.4 前端启动步骤
前端启动相比后端要简单一些。打开终端进入前端项目目录,按顺序执行:
bash复制npm install
npm run serve
npm install是下载依赖包的过程,如果网络慢可以换成国内镜像源,在项目根目录新建.npmrc文件,内容写registry=https://registry.npmmirror.com,这样下载速度会快很多。
npm run serve启动成功后,终端会显示一个本地访问地址,通常是http://localhost:3000。这时浏览器打开这个地址,能看到系统首页。如果登录后调接口报跨域错误,先检查vue.config.js中的代理配置是否正确,再看后端启动端口是否和代理目标端口一致。
这套前后端分离项目本地开发时,实际上需要同时跑两个进程。我先开后端再开前端,这就是日常工作流。
6.5 生产环境最小部署方案
如果你要把这个项目部署到服务器上做一个演示环境,前端需要先打包:
bash复制npm run build
打包完成后,前端项目根目录会生成一个dist目录。部署方案上,我推荐使用Nginx托管前端静态文件,同时反向代理后端接口:
nginx复制server {
listen 80;
server_name localhost;
# 前端静态文件
location / {
root /usr/share/nginx/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
# 后端接口反向代理
location /api/ {
proxy_pass http://localhost:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
注意try_files这一行,前端使用Vue Router的history模式时必不可少,否则刷新页面会出现404。如果前端用的是hash模式,也就是URL带#号,那不需要这行配置,但看起来不够美观,所以我还是选的history模式。
后端部署直接运行jar包:
bash复制nohup java -jar library-0.0.1-SNAPSHOT.jar > log.log 2>&1 &
用nohup让jar包在后台运行,日志输出到log.log文件。这时候浏览器直接访问服务器的IP地址,就能看到系统页面,前端接口请求经由Nginx代理到后端8080端口,整个链路就通了。
7. 常见问题与排查技巧实录
7.1 数据库连接失败的排查
数据库连接失败是新手遇到最多的报错,错误提示通常是Access denied for user或Communications link failure。这两种报错含义不一样:
Access denied说明用户名或密码不对,去application.yml里检查username和password字段。Communications link failure说明数据库服务没启动或者端口不对,先确认MySQL服务有没有启动,再确认url中的端口是不是3306。
另外JDBC驱动版本和MySQL版本不匹配也会导致连接异常。如果用MySQL 8.x,driver-class-name要写com.mysql.cj.jdbc.Driver,不能用老版的com.mysql.jdbc.Driver,否则会提示找不到驱动类。
7.2 前端跨域问题与接口404的区分
前端登录后,如果浏览器控制台出现blocked by CORS policy,说明跨域问题没解决。优先检查开发环境中vue.config.js的proxy配置是否生效,代理配置修改后需要重启npm run serve才能生效,这个很多人会忘记。
如果请求发出去了,但返回404,那不是跨域问题,而是接口路径不匹配。检查前端api/目录下的请求URL和后端Controller的@RequestMapping路径是否一致。我见过一个情况:前端请求/api/user/login,后端Controller的路径是/user/login,结果前端代理配置里的pathRewrite写错了,导致/api前缀没有去掉,后端一直收不到请求,这个问题用浏览器开发者工具看Network的请求URL就能一眼定位。
7.3 端口占用导致启动失败
SpringBoot默认端口是8080,如果本机已经有一个应用占用了8080,后端启动就会报Port 8080 was already in use。解决方案有两个:一是找到占用进程并结束它,二是改配置文件里的server.port。
命令行查端口占用的方式:
bash复制netstat -ano | findstr 8080
taskkill /PID 对应进程号 /F
如果不想杀进程,直接把server.port改成8081或者别的端口,同时把前端vue.config.js和Nginx配置里的对应端口也改掉,保持链路一致。
7.4 npm install失败与依赖版本冲突
npm install失败的原因通常是网络问题或者依赖版本不兼容。网络问题换镜像源就能解决。依赖版本不兼容的表现是安装完成后npm run serve报错,往往和Node版本有关。
我的项目在Node 18环境上编译时会报digital envelope routines::unsupported这个错,原因是Webpack 4和Node 18的OpenSSL版本不兼容。解决方法有两种,第一种是降Node版本到14;第二种是在package.json里的启动命令改成:
json复制"scripts": {
"serve": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
"build": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build"
}
这个是小众解法,但也确实管用,分享给版本环境对不齐的朋友。
7.5 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 后端启动报时区错误 | MySQL 8.0时区未设置 | url加serverTimezone=Asia/Shanghai |
| 前端请求跨域 | 代理未生效 | 检查vue.config.js,重启dev server |
| 接口返回404 | 路径不匹配 | 检查请求路径和Controller映射 |
| 登录成功但跳转不了 | 路由守卫判断异常 | 检查token是否写入localStorage |
| 图片显示不出来 | 静态资源路径错误 | 图片放public目录,用绝对路径访问 |
| 中文乱码 | 数据库字符集不对 | 建库时使用utf8mb4 |
| 无法连接数据库 | 用户名密码错误 | 检查application.yml配置 |
7.6 一个典型的“正常现象”
最后说一个容易被误认为Bug的“正常现象”:后端启动的时候,控制台会打印很长一段SQL日志。这是因为MyBatis-Plus配置文件里开启了StdOutImpl日志实现,所有执行的SQL都会输出到控制台。有人在网上提问说“项目是不是有安全漏洞,把SQL都打印出来了”,其实不是,这只是本地调试用的配置。如果部署生产环境,把log-impl改成org.apache.ibatis.logging.slf4j.Slf4jImpl,约束一下日志级别,SQL就不会刷屏了。这个细节挺多新手会踩坑,以为项目异常了。
8. 项目扩展方向与个人心得
这套系统跑通之后,如果想继续深入,我建议从三个方向去扩展。第一个是数据统计可视化,目前管理端的统计模块只做了简单的折线图和饼图,可以继续加“分类借阅排行”“逾期趋势图”这些模块,前端用ECharts,后端写聚合查询SQL,考验数据库聚合函数和前端图表配置的能力。第二个是消息通知,目前公告是管理员手动发布的,可以扩展成借阅到期自动发短信或邮件提醒,这块需要引入定时任务和第三方消息服务。第三个是电子资源关联,疫情让很多人习惯了线上查阅资料,可以在图书详页面关联PDF试读、电子版资源、二维码扫码借阅等入口,让系统不只是个图书台账,更符合智慧图书馆的方向。
个人经验方面,我最大的体会是:前后端分离的项目,接口文档比代码本身更重要。最初做这个项目的时候,前端和后端是一个人写,接口路径和返回结构都在脑子里,写完就能跑通。但后来我发现,哪怕隔了一个月再打开这个项目,想改一个功能,都得先翻Controller代码确认接口的入参和返回结构,浪费时间且容易漏改。如果一开始就用Swagger注解把接口文档自动生成出来,或者在api目录里把每个接口函数的注释写清楚,后期维护会轻松很多。这套源码的接口注释还算完整,但我仍然建议你在二次开发时把“改接口就同步更新文档”当成一条纪律来执行。只有这样,一个毕业设计级别的项目,才有机会在后续迭代中慢慢变成一个有体系、能真正落地的产品。
