拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue图书管理系统源码实战:跑通、改造与部署指南

SpringBoot+Vue图书管理系统源码实战:跑通、改造与部署指南

1. 项目到底解决什么问题

先直接说结论:这是一套“带完整源码、可直接运行、能写进毕设/课设/简历”的图书管理系统,技术栈是SpringBoot + Vue + MySQL + MyBatis。标题里那个“html”其实指的是前端页面形态是 HTML 页面(Vue 单页应用打包后就是一套 HTML+JS+CSS),并不是说前端就只有一个静态 html 文件。

这类项目在 GitHub/Gitee 上一搜一大把,但大多数版本的问题在于:要么后端代码跑不起来,要么前端依赖装不上,要么 MySQL 版本不匹配,要么 MyBatis 配置和表结构对不上。我拿到这套源码之后,花了两天时间把它完整跑通,并且梳理了从环境准备 → 数据库导入 → 后端启动 → 前端启动 → 功能联调的完整流程。这篇文章不是照抄 README,而是以“实际动手做一遍”的角度,把每一步的坑和为什么这么做的原因都写清楚。

适合谁看:

  • 正在做毕业设计、课程设计,需要一套能答辩、能演示的图书管理系统的同学
  • 刚学完 SpringBoot 和 Vue,想找个完整项目练手、看懂真实项目结构的人
  • 需要快速搭建一套图书管理后台,评估这套源码能不能直接改造成自己业务的人

如果你是零基础、连 SpringBoot 和 Vue 是什么都还没搞明白,这篇文章也能看,但建议先把我标注的“基础概念补充”部分读一遍,否则容易卡在环境配置上。

2. 整体设计与技术选型拆解

2.1 为什么是 SpringBoot + Vue,而不是别的组合

先看图:

浏览器(Vue 页面) ↓ HTTP / JSON SpringBoot 后端(Controller → Service → Mapper) ↓ MyBatis 通过 JDBC MySQL 数据库

在 2024 年做这类管理系统,SpringBoot + Vue 几乎算得上“最省心”的组合,没有之一。原因有三点:

第一,前后端分离后,开发时两边互不干扰。后端同学只需要写好接口、返回 JSON,前端同学只需要对着接口文档写页面。如果是在校生做毕设,这种做法还能在论文里多写一章“前后端分离架构设计”,凑字数也好、展示能力也罢,都很方便。

第二,SpringBoot 把配置简化到了极致。传统 SSM 项目要写一堆 XML 配置、web.xml、spring-mvc.xml,SpringBoot 直接给你内置了 Tomcat,搞一个application.yml就搞定大部分设置。对新手来说,少配一项就少一个出错点。

第三,Vue 对新手极其友好。它的语法介于传统 HTML+JS 和现代前端框架之间,就算你只会 jQuery,看 Vue 的单文件组件也能猜个大概。再配合 Element UI 这类组件库,做表格、弹窗、表单几乎不用自己写 CSS,直接拿来用就行。

那为什么不推荐 JSP + Servlet 或者 Thymeleaf?没有说那些不能做,只是从“拿出去能找工作、能写进简历”的角度看,Vue 的普适性更高。现在随便打开一个招聘网站搜“Java 开发”,十有八九都要求会 Vue 或者至少了解一种前端框架。用这套源码跑一遍,至少能背下来一套前后端交互的完整链路。

2.2 后端分层的设计思路(为什么代码不糊在一坨)

这套源码的后端包结构大概是这样的:

src/main/java/com/xxx/library/ ├── controller/ // 接收前端请求,调用 service ├── service/ // 业务逻辑层(接口 + 实现) ├── mapper/ // MyBatis 的 mapper 接口 ├── entity/ // 实体类,对应数据库表 ├── config/ // 配置类,比如 CORS 跨域配置 └── common/ // 通用返回结果、工具类

很多新手拿到代码第一反应是:这分层好麻烦啊,我直接在 Controller 里写 SQL 不行吗?

当然可以,但那样写出来的东西不叫系统,叫“接口集合”。分层的目的不是让你多敲几行代码,而是:如果将来要改业务逻辑,不需要动 Controller 和数据库层;如果将来要换数据库,只需要改 mapper 层;如果将来要加权限验证,只需要在 service 层统一处理。

举个实际例子:假设你要加一个“借书前检查该用户是否还有未归还的图书”的逻辑。在分层设计里,你只需要在BorrowService里加一个判断方法,然后去BookMapper或BorrowMapper写一条查询 SQL。前端完全不用改,Controller 也基本不用动。如果你是全局在一个 Controller 里写 SQL,那就得把整个方法重写,测试风险高得多。

这套源码的分层虽然简单,但五脏俱全。Controller 负责参数接收和结果返回,Service 负责业务判断,Mapper 负责 SQL 操作。照着这个结构,你可以很轻松地拓展新功能,比如加一个“公告管理”模块,那就是照抄图书模块的四层结构,20 分钟能搞定。

2.3 前端路由与页面组织(Vue 是怎么把多个页面串起来的)

前端部分不是那种一个 HTML 文件里写死的单页,而是用了 Vue Router 做前端路由。页面之间通过路由跳转,像是在一个 HTML 外壳里动态替换内容。

前端的主要目录结构:

src/ ├── main.js // 入口文件,创建 Vue 实例 ├── router/ │ └── index.js // 路由表:URL 路径对应哪个组件 ├── views/ │ ├── Login.vue // 登录页 │ ├── Layout.vue // 主布局(顶栏+侧边栏+内容区) │ ├── BookManage.vue // 图书管理页 │ ├── BorrowManage.vue // 借阅管理页 │ └── UserManage.vue // 用户管理页 ├── api/ │ └── request.js // 封装 axios 请求

登录之后进入 Layout,左侧是导航菜单,右侧是根据路由切换的内容区。这种设计的好处是:新增页面不需要动其他页面,只要在 router 里注册一个路由,再写一个 Vue 文件即可。

如果之前没接触过 Vue 的“组件化”概念,可以这样类比:把页面当成积木。Layout.vue是一个固定的拼图底板,菜单和顶栏是底板的一部分,中间的内容区域是个“插座”。每个路由组件(比如BookManage.vue)就是一块可以插进插座的积木,切换路由时,插座里的积木被换掉了,但底板和菜单位置不动。这就是为什么前端常见的后台管理系统都长一个样——顶部栏和侧边栏几乎是固定的,变的只是中间的业务页面。

3. 基于源码的核心功能拆解与实操要点

3.1 图书管理模块(最核心的增删改查)

图书管理模块基本就是一套标准的“增删改查 + 分页 + 条件搜索”。实体类字段大体如下:

字段含义字段名类型说明
图书IDidint自增主键
书名book_namevarchar可模糊搜索
ISBNisbnvarchar图书唯一标识,可搜索
作者authorvarchar可模糊搜索
出版社publishervarchar普通字段
库存量stockint借阅时会关联判断
状态statusvarchar在馆/下架

后端对应BookController里的几个接口大致是:

@RestController @RequestMapping("/api/book") public class BookController { @GetMapping("/list") public Result page(BookQuery query) { ... } @PostMapping("/add") public Result add(@RequestBody Book book) { ... } @PutMapping("/update") public Result update(@RequestBody Book book) { ... } @DeleteMapping("/delete/{id}") public Result delete(@PathVariable Integer id) { ... } }

实操时要注意几个点:

  • 分页参数不要写死。前端会传pageNum和pageSize,后端用 PageHelper 或者手写 LIMIT 做分页。如果你改成了LIMIT 10写死,前端翻页就会失效。
  • 搜索条件用 Map 或者 Query 对象接收。别一个参数一个参数地接,以后加查询条件会非常痛苦。
  • 删除图书要关联判断。如果有未归还的借阅记录关联着这本书,直接删除会导致数据不完整。严谨的做法是删除前先查一下借阅表有没有status=借出且book_id=当前id的记录。

我实际跑通之后,发现这套源码在删除借阅中的图书时没有做拦截,属于一个“功能完整但不够健壮”的小缺陷。如果是做毕设,建议在BookServiceImpl.delete里加一段判断:

int count = borrowMapper.countByBookIdAndStatus(bookId, "借出"); if (count > 0) { return Result.error("该书存在未归还的借阅记录,无法删除"); }

就这一小段逻辑,答辩时能成为你的加分项,因为你展示的不是“会抄代码”,而是“能发现问题并改进”。

3.2 借阅管理模块(最容易出 bug 的地方)

借阅模块的难度比图书管理高一档,因为它涉及两张表的数据联动:借阅记录表和图书表。

借书流程拆开来看是这样:

  1. 前端提交借书请求,携带参数:图书ID、用户ID(或读者证号)
  2. 后端先查图书是否存在、库存是否大于 0
  3. 再查该用户是否有未归还的同名图书(防止重复借同一本)
  4. 允许借出后,图书表的库存减一
  5. 在借阅表插入一条记录,状态为“借出”,记录借书时间
  6. 设置应还时间(一般是借书时间 + 30 天)

还书流程就是反过来:

  1. 查到这条借阅记录,确认状态是“借出”
  2. 将状态改为“已还”,写入实际还书时间
  3. 图书表的库存加一
  4. 如果超期,计算罚款金额(有的系统有,有的没有)

值得认真研究的是这个部分用了事务。在BorrowService的借书方法上会看到@Transactional注解:

@Transactional public Result borrowBook(BorrowDTO dto) { // 1. 校验库存 // 2. 扣减库存 // 3. 插入借阅记录 }

为什么必须有@Transactional?因为**“扣库存”和“插入借阅记录”必须同时成功或同时失败**。如果扣了库存但插入借阅记录失败,就会出现一本书凭空消失;如果插入了记录但没扣库存,就会出现库存对不上账。这个注解就是给这两个数据库操作上了一把锁:要么全做完,要么全回滚。

类比一下:你在支付宝转账,扣款和收款是两个动作,如果扣了款但对方没收到,你肯定不干。事务就是保证这种“要么都成功,要么都不成功”的机制。

3.3 用户登录与权限控制

这套系统提供了读者和管理员两类角色。登录成功后会返回一个 token(有的版本是直接返回用户信息存到 localStorage),前端根据角色控制菜单显示:管理员能看到用户管理、图书管理等全部菜单;普通读者只能看到图书查询、个人借阅记录等有限功能。

这里有一个很常见的实务点:后端接口不能只靠前端隐藏菜单来实现权限控制。什么意思?就是说如果前端菜单里看不到“用户管理”,但 someone 猜到/api/user/list这个接口地址,直接拿浏览器或者 Postman 发一个请求,后端如果没有任何拦截,那就泄露了数据。

正规的做法是在后端加一个拦截器(HandlerInterceptor)或者用一个轻量权限框架,比如 Sa-Token、Shiro、Spring Security。但这套源码里大概率只是做了简单的登录校验,没有做接口级别的权限控制,这是很多“毕设货架项目”的通病。

如果你想在答辩里展示“我思考过安全问题”,可以给后端加一个简单的拦截器,比如继承HandlerInterceptor:

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token"); if (token == null || !TokenUtils.verify(token)) { response.setStatus(401); return false; } return true; } }

然后在 WebMvcConfig 里注册拦截路径:

拦截所有 /api/** 的请求,放行 /api/login

这个改动不算大,但能让你在答辩时说清楚“我的系统不是只靠前端控制,后端的拦截器同样会校验登录状态”。

3.4 数据库设计的核心表关系

这套源码里的数据库至少有这几张表:

表名主要字段
userid, username, password, role, nickname
bookid, book_name, isbn, author, publisher, stock
borrow_recordid, user_id, book_id, borrow_time, return_time, status

表之间的关系:

  • user和borrow_record:一对多,一个用户有多条借阅记录
  • book和borrow_record:一对多,一本书被借多次(但如果限定了同一本书同时只能被一个人借,那就需要额外约束)

设计表的时候,外键并不一定非要在数据库层面建,逻辑外键(通过代码关联)在现在的企业项目中更常见,因为这样可以减少数据库锁竞争、方便分库分表和备份恢复。但作为教学项目,建议把外键建上,至少在论文里可以写“通过外键保证数据的引用完整性”。

实际操作时,如果你发现源码里的 SQL 文件只有一个sql文件,直接导入即可。导入前务必将数据库编码设置为utf8mb4,否则中文会乱码。如果是用 Navicat 导入,右键数据库 → 运行 SQL 文件,不要用复制粘贴到查询窗口的方式,否则容易因为编码问题产生“明明看着是对的,但查询就是报错”的情况。

4. 完整实操过程:从零到跑通

4.1 环境准备清单与版本避坑

先列一份我当时使用的环境,也是比较稳的组合:

组件推荐版本备注
JDK1.8 或 11这版代码用的是 Java 8 语法,太高也行
Maven3.6+用 IDEA 内置也可以
MySQL5.7 或 8.08.0 需要改驱动名和连接参数
Node.js14.x / 16.x别用太新的,18+ 有时装依赖会报 openssl 错误
Vue CLI4.x / 5.x具体看前端 package.json

最大的坑往往是版本不匹配。如果前端项目是用 Vue CLI 4 初始化的,Node 18 环境下安装依赖时十有八九会报Error: error:0308010C:digital envelope routines::unsupported。解决方式是修改package.json里的启动脚本:

"scripts": { "serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve" }

Windows 下这样写会出问题(Windows 的 cmd 不支持这种写法),需要改成:

"serve": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve"

或者更省事:直接用 Node 16,这个版本基本不会触发该报错。

4.2 后端启动详细步骤

第一步,导入源码到 IDEA。选择pom.xml,用 Maven 的 Import Project 方式,等待依赖下载完成。这里有两个情况:

  • 如果本地 Maven 仓库里缺依赖,而你的网络又比较差,等半小时是常事,可以考虑切换阿里云镜像。
  • 如果 pom.xml 里的依赖版本特别老,比如 SpringBoot 2.2.x,IDEA 下载时可能会报一些警告,不用管,只要最终右侧 Maven 窗口不报红即可。

第二步,修改application.yml。重点检查这三个配置:

spring: datasource: url: jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456

一定要把数据库名、用户名、密码改成你自己的。useSSL=false一定要加,否则 MySQL 8.0 会报 SSL 连接错误。serverTimezone一定要加,否则日期字段可能报时区错误。

第三步,确认 MyBatis 的 XML 文件路径。在application.yml里通常会看到:

mybatis: mapper-locations: classpath:mapper/*.xml

对应地,src/main/resources/mapper/目录下应该有BookMapper.xml、BorrowMapper.xml等文件。如果 XML 文件不在这个路径,启动时会报Invalid bound statement (not found)。

第四步,运行LibraryApplication.java的 main 方法,控制台显示类似:

Tomcat started on port(s): 8080

然后访问http://localhost:8080/api/...看看是否能返回 JSON。如果直接 404,别急,先确认你访问的路径和 Controller 里的@RequestMapping是不是一致,比如/api/book/list。

4.3 前端启动详细步骤

第一步,打开终端,cd到前端目录(一般是frontend或者vue-book)。

第二步,安装依赖:

npm install

如果你是在国内网络环境下,建议用淘宝镜像:

npm config set registry https://registry.npmmirror.com npm install

第三步,配置代理。在vue.config.js里找到类似这样的配置:

module.exports = { devServer: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

意思就是:前端页面上请求/api/xxx时,开发服务器会把请求转发给http://localhost:8080这个后端地址。如果你后端端口不是 8080,这里要改。这是前后端分离开发时最常用的手段,用来绕开跨域问题。

第四步,运行:

npm run serve

浏览器打开http://localhost:3000,看到登录页基本就成功了。输入测试账号(README 里一般会有 admin/admin123,有的版本是 admin/123456),进去之后能看到首页统计和各个管理菜单。

4.4 联调验证的关键接口

跑通之后,最快的验证方式是打开浏览器开发者工具(F12),切到 Network 面板,随便点一下“图书列表”页面,会看到类似这样的请求:

GET http://localhost:3000/api/book/list?pageNum=1&pageSize=10

对应返回的 JSON 结构大致是:

{ "code": 200, "message": "success", "data": { "total": 25, "list": [ { "id": 1, "bookName": "Java编程思想", "isbn": "9787111213826", "author": "Bruce Eckel", "stock": 10 } ] } }

如果看不到数据,按这个顺序排查:

  1. 数据库有没有数据(SELECT * FROM book)
  2. 后端接口直接访问能不能返回(浏览器访问http://localhost:8080/api/book/list)
  3. 前端代理有没有生效(Network 面板里请求的 URL 是不是http://localhost:3000/api/...)

还有一招比较省心:如果你不想每次都用开发模式,可以前端执行npm run build,生成dist目录,然后用 Nginx 托管前端、反向代理后端。这个操作更接近真实部署场景,毕设或项目演示时也更有说服力。

5. 常见问题与排查技巧实录(值得收藏的一章)

5.1 后端启动失败:“Invalid bound statement (not found)”

这个报错十有八九是 MyBatis 的 XML 文件没被扫描到。排查三步:

  1. 确认application.yml里mapper-locations对应的路径是否正确,比如classpath:mapper/*.xml,那么 XML 文件必须放在src/main/resources/mapper/下。
  2. 检查pom.xml里是否把 XML 文件排除掉了。有些模板的<build><resources>配置只打包了application.yml,没包含resources/mapper,需要加:
<resources> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.xml</include> <include>**/*.yml</include> </includes> </resource> </resources>
  1. 启动类上是否有@MapperScan,或者每个 Mapper 接口是否有@Mapper注解。两选其一即可。

5.2 MySQL 8.0 连接报错或时区问题

如果你用的 MySQL 是 8.0,驱动名要改成com.mysql.cj.jdbc.Driver,URL 必须带上serverTimezone=Asia/Shanghai。MySQL 5.7 用com.mysql.jdbc.Driver即可。另外,如果报Public Key Retrieval is not allowed,在 URL 后面加allowPublicKeyRetrieval=true。

完整示例:

url: jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true

5.3 前端 npm install 报错

最常见的两类:

一是网络问题。把 registry 切到淘宝镜像基本能解决,如果还不行就清 npm 缓存:npm cache clean --force。

二是node-sass编译失败。因为node-sass需要在安装时下载二进制文件,网络不稳定容易挂。解决方案是删除package-lock.json和node_modules,然后执行npm install前先设置 SASS 镜像:

npm config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/

如果这版源码用的不是node-sass而是sass(Dart Sass),安装起来就轻松多了,基本上不会有这套问题。

5.4 登录成功后跳转不到首页

前端登录逻辑一般是:登录接口返回 token → 存到 localStorage → 路由跳转到/layout或/home。如果一直停留登录页,常见原因:

  • 后端返回结构和前端约定不一致。比如前端期望{code: 200, token: "xxx"},后端返回却是{code: 200, data: {token: "xxx"}},前端里res.data.token取到 undefined,登录判断失败。
  • 路由守卫里写了if (!token) redirect login,但 token 存错 key 了。

解决建议:F12 看 Network 面板请求和响应,确认后端到底返回了什么,再去request.js和login.vue里比对取值逻辑。这种问题八成是字段名对不上。

5.5 前端请求接口跨域报错

如果是开发模式,用vue.config.js的 proxy 最省事,但注意:页面上请求的地址应该是/api/xxx,而不是http://localhost:8080/api/xxx。一旦写了完整的后端地址,就走不到代理,跨域报错就来了。

如果是部署模式,建议用 Nginx 统一解决:

server { listen 80; location /api/ { proxy_pass http://localhost:8080/api/; } location / { root /path/to/dist; index index.html; try_files $uri $uri/ /index.html; # 这个是 Vue Router history 模式必需的 } }

5.6 时间字段显示不对或格式乱

数据库里的datetime字段返回给前端时,Jackson 默认可能序列化成时间戳(一串数字),前端看到的就是 1690000000000 这种。解决方式是在application.yml里加:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

5.7 如何改造成自己的业务(比如“学生选课系统”)

这套图书管理系统的表结构和通用业务系统非常接近,改造成“学生选课系统”其实只需要动几张表:

  • book→course:课程ID、课程名称、教师、学分、人数上限
  • borrow_record→select_record:选课记录,含学生ID、课程ID、选课时间、状态
  • user不变:学生/管理员两种角色

后端实体类、Mapper XML 里的字段名全替换,Controller 和 Service 逻辑基本不用动。前端页面把表格列名和数据字段换掉,也能复用。这就是“模板化管理系统”的通用价值——你换换表和字段,就能交付出一个新的管理后台。

6. 部署上线时我踩过的几个坑

6.1 后端打包要注意的配置

后端打包命令是:

mvn clean package -DskipTests

生成target/xxx.jar。但这个 jar 能不能跑,取决于application.yml里的配置是不是生产环境配置。如果你打包时还是连本地的localhost:3306,上传到服务器后自然会连接失败。所以部署前先确认数据库连接配置。

还有一个常见错误:如果把后端跑在云服务器上,前端页面访问的是域名http://your-domain.com:8080/api/...还是 Nginx 代理的/api?如果是服务器直接对外开放 8080,需要注意安全组策略,否则容易被扫端口。建议前端打包成静态文件交给 Nginx,后端 jar 跑在 8080,通过 Nginx 反向代理访问。

6.2 服务器上如何把 SpringBoot 后台进程守住

直接用java -jar xxx.jar启动,一关终端进程就没了。推荐用nohup:

nohup java -jar xxx.jar > log.log 2>&1 &

更稳的做法是配 systemd 服务,不过对于毕设或演示,nohup已经够用了。建议每次启动时看一眼log.log,确认端口有没有被占用、数据库连接是否正常。

6.3 一个隐藏很深的坑:前后端认证机制不一致

如果后端的 token 校验用的是自定义拦截器,前端却在每次请求时使用不同的 header 名,比如后端读的是Authorization,前端设置的是token,那么即使你登录成功,后续请求也会 401。建议统一约定:所有经过登录鉴权的请求,都在 axios 拦截器里统一设置 header。

给一个 axios 拦截器的通用写法:

import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) export default request

前端所有页面 api 请求都通过这个封装好的request对象发出,避免各自的页面里手动加 header,减少出错率。

7. 最后再分享点实操体会

说实话,这种“图书管理系统”在技术上不算多难,但它真的能帮你把知识串起来。很多初学者单独学 SpringBoot、单独学 Vue 时都觉得明白,一看代码好像也都认识,但真正缺的是那张“网”——前端点击按钮到后端查询数据库,再把数据显示回页面上,这一整条链路如果没有亲手跑通一遍,就永远只是停留在“语法会了”的层面。

我实际操作下来,最大的心得是:拿到一套源码后,不要一上来就跑,而是先花 20 分钟把目录结构看一遍,理清前端和后端是怎么约定的(接口路径、返回结构、token 传递方式),再动手配置环境。这样就算后面出了问题,你也能定位到大概哪一层,而不是无头苍蝇一样乱试。

另外,这套源码毕竟不是企业级产品,你在研究时保持两个心态:第一,能用但不够健壮,很多边界条件没有处理;第二,这也恰恰是你能“二次加工”的地方。比如给系统加上借阅超期提醒、图书封面上传、Excel 导入导出这些功能,都能真正锻炼你对整条技术链路的掌控力。

如果遇到跑不起来的情况,先别急着改代码,按这个顺序排查:数据库导入有没有成功 → 后端配置对不对 → 浏览器直接访问后端接口通不通 → 前端代理转发有没有生效。这四条链路通了,系统自然就转起来了。祝你能顺利跑通,也欢迎在实操过程中带着具体报错来找我讨论。

返回列表