SpringBoot+Vue3图书管理系统:前后端分离项目从零搭建到部署
2026/9/8 6:32:49 网站建设 项目流程

这段时间,我反复带人把 SpringBoot+Vue3 图书管理系统从零到能跑、能演示、能答辩的状态走了一遍。先说判断:这个项目的魅力不在“图书业务”本身,而在于它把后端接口、前端页面、数据库设计、前后端联调、部署发布这一整条链路压缩到了一个可以直接复制的模板里。对准备做毕业设计、课程设计,或者简历上缺一个“能讲清楚的项目”的人来说,它是性价比很高的起点。你真正要关注的并不是“怎么管理图书”,而是搞清楚一个前后端分离的项目,从启动到增删改查完整跑通,中间到底有哪些坑、哪些参数、哪些判断标准。

下面我按实际落地顺序拆一遍。环境以 Java + Spring Boot 后端、Vue3 + Vite 前端、MySQL 数据库为例,按常规配置说明。如果你下载到的源码带 SQL 脚本、前端 dist 包或部署文档,也可以拿这份思路去核对项目是否完整。

1. 核心价值判断:这个项目的难点不在图书业务,而在分离架构

1.1 前后端分离到底“分离”了什么

很多人第一次接触图书管理系统,以为难点是“书怎么分类、库存怎么扣、逾期怎么算”。但这类教学项目的重点其实不在业务算法,而在前后端分离的架构方式。

在单体时代,Java 页面里会直接写 JSP,前端和后端混在一起部署。它简单,但问题也很明显:前端改样式要重启后端,后端改接口要和前端页面绑在一起,多人协作时互相牵制。前后端分离后,后端只负责写接口、处理数据、返回 JSON,前端只负责渲染页面、调用接口、展示数据。两者通过 HTTP 接口通信,开发时各起各的进程,部署时也可以分开部署。

图书管理系统正好适合做这个演示。它业务简单,不需要复杂的权限模型,也不涉及高并发,但增删改查五个基础操作是全的。把这五个操作做成规范接口,再让 Vue3 页面调用这些接口,一条完整链路就出来了。

我经常提醒新手一句话:不要急着写代码,先把“后端在哪里、前端在哪里、数据库在哪里、它们之间怎么通信”这四个问题想明白。这个项目能跑通,核心标志就是后端返回 JSON,前端拿到 JSON 并把结果渲染到表格里。

1.2 一个 CRUD 项目能覆盖哪些面试考点

很多准备实习、找工作的同学会把图书管理系统写进简历。面试官通常不会只问“你做了什么功能”,而会往细节里问。

这个项目能带的考点很集中:

  • Spring Boot:自动配置原理、启动流程、依赖注入、Spring MVC 请求处理流程。
  • MyBatis 或 MyBatis-Plus:Mapper 接口如何映射 SQL、分页插件怎么配置、数据库表和实体类字段如何对应。
  • Vue3:组合式 API、ref 和 reactive 的区别、组件通信、生命周期、路由跳转。
  • Axios 与跨域:前端请求代理、响应拦截器、统一错误处理。
  • 项目部署:后端 jar 包怎么打、前端静态文件怎么托管、Nginx 如何反向代理。

所以,这个项目适合当“骨架项目”,而不是“最终目标”。你可以在图书管理基础上继续加用户登录、管理员权限、借阅记录、归还提醒、数据统计、Excel 导入导出等模块。每一个扩展模块,都是简历上的一个可讲点。

1.3 标题里说的“半小时搭建”是什么意思

半小时不是一个绝对承诺,而是一个节奏参考。如果你已经准备好环境,数据库建好表,源码结构完整,从头创建后端工程、初始化前端项目、跑通接口,确实可以在半小时内完成一次“从空目录到浏览器看到列表页面”的流程。

但如果你是第一次接触,我不建议为了追求半小时而跳过理解。更好的方式是这样分配时间:

  • 前 10 分钟:看数据库脚本,理解有哪些表、哪些字段。
  • 中间 10 分钟:启动后端,用接口测试工具确认分页查询能返回 JSON。
  • 后 10 分钟:启动前端,通过页面完成一次新增和删除。

这个顺序能让你在最短时间内验证项目是否完整,也方便你稍后向别人讲解。

2. 开发前准备清单:版本匹配比安装本身更关键

2.1 环境版本怎么选

环境准备是这类项目里最容易出问题的地方。很多报错并不是代码问题,而是 JDK、Node.js、MySQL 版本之间互相不兼容。

我按常见稳定组合给出建议,具体版本以你实际下载的源码为准。

组件建议版本说明
JDK8、11 或 17Spring Boot 2.x 常见用 JDK 8/11,Spring Boot 3.x 要求 JDK 17
Maven3.6 以上用来拉依赖和打包
MySQL5.7 或 8.0差别不大,关键看数据库连接驱动
Node.js16 以上Vue3 配合 Vite 需要较新版本
包管理器npm 或 pnpm安装依赖时用,建议 npm

如果你的项目是 Spring Boot 2.7 + JDK 8,那就不要手动升级到 Spring Boot 3.x,否则很多依赖坐标和配置方式都会变。如果前端是 Vue3 + Vite,Node 版本过老会导致 Vite 无法启动。这里最稳妥的做法是:先看项目里pom.xmlpackage.json标明的版本范围,再决定本地环境用什么。

2.2 后端工程初始化

后端工程可以通过 Spring Initializr 创建,也可以直接复用已有源码。如果是手工创建,核心依赖一般包括 Spring Web、MySQL 驱动、MyBatis-Plus、Lombok。不同 Spring Boot 版本下,MyBatis-Plus 的 starter 坐标写法会有差异,安装时注意。

一个典型依赖块长这样:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>

注意,MyBatis-Plus 的版本号需要和 Spring Boot 版本兼容。如果启动时报Invalid value type for attribute 'factoryBeanObjectType',基本就是 MyBatis-Plus 版本与 Spring Boot 3.x 不匹配,或者依赖没有被正确引入。

2.3 数据库设计:先建表再动手

图书管理系统的核心表一般就是图书表。常见的字段包括:

CREATE TABLE `book` ( `id` bigint NOT NULL AUTO_INCREMENT, `isbn` varchar(32) DEFAULT NULL COMMENT 'ISBN编号', `book_name` varchar(100) NOT NULL COMMENT '书名', `author` varchar(100) DEFAULT NULL COMMENT '作者', `publisher` varchar(100) DEFAULT NULL COMMENT '出版社', `category` varchar(50) DEFAULT NULL COMMENT '分类', `price` decimal(10,2) DEFAULT NULL COMMENT '价格', `stock` int DEFAULT '0' COMMENT '库存', `publish_date` date DEFAULT NULL COMMENT '出版日期', `status` tinyint DEFAULT '1' COMMENT '状态:1上架 0下架', `cover_url` varchar(255) DEFAULT NULL COMMENT '封面图地址', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书信息表';

这个表结构基本覆盖了常见字段类型:整数、字符串、小数、日期、时间。比单纯id + name + price三字段的表更接近真实项目。

为什么用utf8mb4?因为它能存储 emoji 表情和其他特殊字符。如果项目里涉及书名、作者名中的冷僻字,这个设置能减少一部分乱码问题。

建表时还有个建议:不要过度设计。有些源码会在每张表里都加create_timeupdate_timedeleted字段,这种是通用做法,没问题。但如果业务上根本没用逻辑删除,则不需要为每个接口硬编码deleted=0查询条件,否则反而会降低可读性。

3. 后端 Spring Boot 实现:把增删改查做成标准接口

3.1 配置文件与统一返回结果

后端启动的第一步,是确保能连上数据库。看application.yml里的配置时,重点检查四个位置:

  • 数据库地址
  • 用户名
  • 密码
  • 时区

一个常见的配置:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/library_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 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: 0

这里map-underscore-to-camel-case: true能让数据库字段book_name自动映射为实体类属性bookName。如果不开这个配置,要么手动写 ResultMap,要么在查询语句里给字段加别名。

前端和后端分离之后,后端返回的数据格式最好固定。一般会封装一个Result<T>对象,包含状态码、提示信息和数据。这样前端可以统一判断请求是否成功。

public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("success"); 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; } }

3.2 实体类、Mapper 接口和 Service 层

后端代码的分层一般是这样:Controller 接收请求,Service 处理业务逻辑,Mapper 访问数据库。对应图书模块,就是 BookController、BookService、BookMapper。

使用了 MyBatis-Plus 后,实体类可以这样写:

@TableName("book") public class Book { @TableId(type = IdType.AUTO) private Long id; private String isbn; private String bookName; private String author; private String publisher; private String category; private BigDecimal price; private Integer stock; private LocalDate publishDate; private Integer status; private String coverUrl; private LocalDateTime createTime; private LocalDateTime updateTime; }

Mapper 接口可以非常简单:

public interface BookMapper extends BaseMapper<Book> { }

为什么这里可以缩这么短?因为 MyBatis-Plus 在启动时会自动为BaseMapper提供常用的单表方法,例如selectByIdinsertupdateByIddeleteByIdselectPage。它们对图书管理系统这种单表 CRUD 来说已经足够。

Service 层可以继续组合这些方法。如果项目追求分层清晰,可以写接口和实现类;如果只是为了毕设演示,直接写一个BookServiceImpl也完全够用。我更建议分清接口和实现,因为面试时这是一个可以说的点。

3.3 Controller 接口设计与参数说明

图书管理系统的接口一般包含五个:

功能请求方式路径参数
分页查询GET/api/book/pagecurrent、size、keyword
查询详情GET/api/book/{id}路径参数 id
新增图书POST/api/bookJSON 数据
修改图书PUT/api/bookJSON 数据
删除图书DELETE/api/book/{id}路径参数 id

一个简化版 Controller 示例:

@RestController @RequestMapping("/api/book") public class BookController { @Resource private BookService bookService; @GetMapping("/page") public Result<Page<Book>> page(@RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String keyword) { Page<Book> page = bookService.pageBooks(current, size, keyword); return Result.success(page); } @PostMapping public Result<Boolean> save(@RequestBody @Validated Book book) { return Result.success(bookService.saveBook(book)); } @PutMapping public Result<Boolean> update(@RequestBody @Validated Book book) { return Result.success(bookService.updateBook(book)); } @DeleteMapping("/{id}") public Result<Boolean> delete(@PathVariable Long id) { return Result.success(bookService.deleteBook(id)); } }

这里有几个点可以展开理解。

第一,为什么分页用 GET?因为查询不修改数据,参数可以放在 URL 上,也方便在浏览器里直接验证。

第二,为什么新增和修改用 POST 和 PUT 区分?这是 REST 风格约定。如果项目里团队约定不严格,也可以用 POST 做新增、PUT 做修改。重要的是前后端保持一致,不要前端写 POST,后端接口实际是 GET。

第三,为什么参数要用@RequestBody?因为前端提交的是 JSON 字符串,Spring Boot 需要把 JSON 反序列化为 Book 对象。如果不用这个注解,参数可能取不到值。

3.4 条件查询不要用拼接自己骗自己

分页接口里的 keyword 常用来做模糊搜索。常规思路是在 Service 里构造一个 LambdaQueryWrapper。

public Page<Book> pageBooks(Integer current, Integer size, String keyword) { Page<Book> page = new Page<>(current, size); LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.hasText(keyword)) { wrapper.like(Book::getBookName, keyword) .or() .like(Book::getAuthor, keyword); } return bookMapper.selectPage(page, wrapper); }

这个写法简单,面试时也能讲清楚。注意不要写成一堆 if 嵌套去改 SQL 字符串。用 LambdaQueryWrapper 的好处是类型安全,字段名在编译期就能被检查,比手写字符串列名更不容易出错。

如果项目需要更复杂的 SQL,比如多表联查、统计报表,再考虑写 XML 里的自定义 SQL。图书管理系统到这一步通常不需要。

4. 前端 Vue3 工程:从初始化到页面可交互

4.1 用 Vite 初始化项目

Vue3 现在主流的脚手架是 Vite,创建命令在不同 npm 版本下略有差异。一般可以这样操作:

npm create vite@latest book-manager-ui -- --template vue cd book-manager-ui npm install

这里要提醒一个容易踩的坑:npm create vite@latest过程中可能会提示你选择框架和变体。如果命令行交互不顺利,可以直接选择 Vue 和 JavaScript 模板,不要选 TypeScript,除非你熟悉 TS。图书管理系统用 JS 足够。

安装完成后,再安装额外依赖:

npm install axios vue-router@4 element-plus

4.2 页面结构与路由规划

建议把页面拆成几个模块,不要把所有内容塞在一个组件里。

常见的结构是:

src/ api/ -- 后端接口封装 router/ -- 前端路由 views/ -- 页面组件 components/ -- 公共组件 layout/ -- 布局组件

路由可以这样配置:

import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: () => import('@/layout/Layout.vue'), children: [ { path: 'books', component: () => import('@/views/BookList.vue') } ] } ] }) export default router

4.3 Axios 统一封装

Axios 封装的核心目的,是让每个页面不用重复写baseURLheaders、错误处理逻辑。一个常用的最小封装是:

import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.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 }, (error) => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default request

这里把baseURL设为/api,看起来像是请求当前前端的同源路径。实际上在开发环境要靠 Vite 的代理转发到后端,这样可以避免跨域问题。

4.4 图书列表页面:表格、分页、弹窗表单

图书列表页面是这个项目的核心界面。我一般会把页面拆成三层:

  • 顶层是搜索表单,包含书名关键字和分类选择。
  • 中间是数据表格,展示图书列表。
  • 底部是分页器。
  • 新增和编辑共用一个弹窗表单。

表格列通常包括:

字段
书名bookName
作者author
ISBNisbn
价格price
库存stock
状态status
操作编辑、删除

在 Vue3 里,关键状态可以这样管理:

import { ref, reactive, onMounted } from 'vue' import { getBookPage, createBook, updateBook, deleteBook } from '@/api/book' const loading = ref(false) const bookList = ref([]) const total = ref(0) const queryParams = reactive({ current: 1, size: 10, keyword: '' })

这里refreactive的区别是新手容易迷惑的地方。简单理解:ref适合包裹基本类型或整个对象,访问时要用.valuereactive适合包对象,模板里访问时不需要.value。两种写法都能用,团队里保持统一就好。

5. 联调阶段:端口、代理和增删改查完整验证

5.1 前端代理配置

开发时,后端默认跑在8080,前端 Vite 默认跑在5173。前端页面直接请求http://localhost:8080/api/book/page会遇到跨域问题。

一种常见的解决方式是在 Vite 配置文件里加代理:

server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

这样,前端请求/api/book/page时,Vite 开发服务器会把请求转发到http://localhost:8080/api/book/page。浏览器里看到的请求域名还是当前前端域名,所以不会触发跨域拦截。

为什么我更推荐用代理而不是直接在后端开启全局 CORS?因为开发环境和生产环境可以共用同一种前端路径,部署后只要调整代理规则,前端代码不需要改接口地址。这也是很多实际项目的做法。

5.2 完整测试顺序

联调阶段不要一上来就用页面测所有功能。我建议按这个顺序验证。

第一,单独测后端接口。用浏览器直接访问后端分页接口地址,看能不能返回 JSON。不能返回就说明后端起不来或数据库配置有问题。

第二,单独测前端页面。启动 Vite,访问前端页面,看页面是否渲染。此时列表可能是空的,这没关系。

第三,测列表联动。打开前端页面,按 F12 看 Network 面板。如果请求成功,表格里应该看到图书数据;如果请求失败,就看后端日志和前端 Console。

第四,测新增。在弹窗里填数据,提交后看列表是否刷新。刷新逻辑有两种:提交后调用列表接口重新查第一页,或者重新加载当前页。我用后者,这样更贴近真实操作习惯。

第五,测修改。修改一条数据后,确认表格里的值已经变化,同时数据库里对应字段有更新。

第六,测删除。删除一条数据后,确认总条数减一,再次刷新页面时数据不会“复活”。

5.3 成功结果长什么样

这里的成功结果不是“页面没白屏”这么简单。我给你一个更明确的判断标准:

  • 后端日志出现 SQL 查询记录。
  • 前端 Network 面板请求状态 200。
  • 返回 JSON 的 code 是 200。
  • 表格显示的数据和数据库记录一致。
  • 新增、修改、删除后,刷新页面,数据仍然正确。

只要以上五点全部满足,前后端分离链路就算真的通了。如果只满足页面显示,但新增后刷新数据丢失,那不叫跑通,叫“看着能跑”。

6. 从单条管理到批量场景:扩展、部署和性能边界

6.1 批量导入和批量删除能不能做

图书管理系统做到一定程度,自然会有批量需求。比如管理员想把一批书一次性导进去,或者在列表里勾选多条记录批量下架。

批量操作最容易踩的坑是前端循环发请求。比如你有 100 条数据,不应该在 for 循环里调 100 次POST /api/book。更合理的做法是后端提供一个批量接口,一次接收一个数组,然后循环插入。这样数据库连接、事务控制和失败回滚都更好处理。

后端批量接口可以用类似下面的结构:

@PostMapping("/batch") public Result<Boolean> batchSave(@RequestBody List<Book> books) { return Result.success(bookService.saveBatch(books)); }

前端调用时把数组整体传给后端:

const res = await request.post('/api/book/batch', bookList)

批量操作前一定要做两件事:校验数据格式,给用户明确的进度反馈。否则批量导入 100 条,第 50 条数据格式错误,整个请求失败,用户完全不知道前面 49 条到底有没有入库。

6.2 分页、搜索和排序的参数边界

分页和搜索看起来简单,参数边界却容易出问题。

currentsize不要允许前端无限传大。比如一个恶意请求把 size 传成 100000,后端就要一次性查询十万条数据,没加限制时容易拖垮数据库。实际项目里可以给 size 加最大值校验,或在配置层限制。

模糊搜索字段也要控制范围。书名、作者可以做 like 查询,但不要对create_time这种时间字段单独做 like。时间字段更适合范围查询,比如传开始时间和结束时间。

如果要做排序,建议在排序字段和排序方向上做白名单校验,不要直接把前端传的排序字段拼接进 SQL。否则可能出现字段不存在或排序方向异常的问题。

6.3 打包部署:前端 dist 和后端 jar

本地能跑通后,部署是另一个话题。后端一般用 Maven 打包成可执行 jar:

mvn clean package

生成的target目录下会有一个xxx.jar,在服务器上执行:

java -jar xxx.jar

前端需要先构建静态文件:

npm run build

生成dist目录。部署方式有两种常见选择。

第一种,用 Nginx 托管前端静态文件,并把/api请求反向代理到后端服务。这是前后端分离项目比较标准的做法。

server { listen 80; server_name your-domain; root /opt/book-manager/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; } }

第二种,把前端dist目录拷贝到 Spring Boot 的src/main/resources/static下,然后重新打包成一个 jar。这种方式适合学习演示,但不符合真正的前后端分离部署习惯。

如果面试时被问到“你怎么部署”,建议讲第一种,更接近实际项目。

7. 常见报错和排查链路:不要一上来就改代码

7.1 排查顺序

这类项目报错时,我的第一反应不是看代码,而是按下述顺序排查。

先说输入:先复现一次,记录报错信息和操作步骤。很多问题并不是每次都出现,比如只有特定搜索关键字才报错,只有删除某一条数据才报错。

再看网络:打开浏览器 F12,切到 Network 面板,看请求是否发出、请求地址是否正确、响应状态码是多少。这一步能快速区分问题在前端还是后端。

再看后端日志:日志往往是最直接的线索。表不存在、字段不存在、SQL 语句绑定失败,这些在日志里都很明显。

再看配置:确认数据库连接、端口、前端代理、文件路径配置是否正确。

最后才去查代码逻辑:如果前四步都没问题,再怀疑接口参数传递、逻辑分支和状态处理。

7.2 高频问题清单

现象高频原因优先排查点
前端请求 404代理路径和后端接口路径不一致Network 面板实际请求地址
后端报 Unknown column实体字段与数据库列名映射失败驼峰映射配置、ResultMap
数据库连接失败MySQL 未启动、密码错误、时区问题application.yml 里连接配置
跨域报错前端直连后端域名/端口不同Vite proxy、CORS 配置
npm run dev 失败Node 版本过老、依赖没装完node -v,删除 node_modules 重装
jar 包无法启动端口被占用、依赖冲突检查java -jar报错日志
页面白屏路由模式或资源路径问题控制台报错,检查 history 模式和 base 路径

7.3 如果只是学习,默认配置够用

最后说一句边界判断。图书管理系统作为练习项目,默认配置、单机部署、少量数据,都够用。你不必为了炫技引入复杂的微服务架构、分布式缓存、消息队列。那些技术不是这个项目该承载的内容。

如果是为了长期维护或生产使用,就要补上用户登录、权限校验、操作日志、数据备份、接口限流这些安全与运维层面的东西。但那是另一个“生产化改造”的话题,和“让项目跑起来”是两回事。

我更建议先把单条增删改查跑稳,再把批量操作加上,再考虑部署。这三个阶段每完成一个,你都能在简历和面试里多讲一层。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询