最近后台一直有人问我同样的问题:想找一套 SpringBoot + Vue 的前后端分离项目练手或者做课设,最好源码完整、还能直接跑起来。问的人多了,我发现大家真正想要的其实不只是一份代码,而是一个能看懂、能改、能讲清楚的完整项目。今天就拿这套智慧图书管理系统来展开聊聊。
它用 SpringBoot 提供后端接口,Vue 负责页面交互,MyBatis 作为持久层框架操作数据库,MySQL 存业务数据,整套链路非常典型。所谓"智慧",体现在借阅逾期自动计算、热门图书统计、库存预警、多条件组合检索这些细节上,而不是单纯做一个增删改查的 demo。无论你是准备毕业设计、课程设计,还是想通过一个完整项目把 SpringBoot、Vue、MyBatis、MySQL 这几个技术栈串起来,这套系统的源码结构都值得认真过一遍。下面我会从项目全局设计、环境搭建、后端实现、前端页面、联调部署到二次开发,把里面的关键细节和踩坑点一次讲清楚。
1. 项目全景:这套系统到底在做什么
很多同学拿到源码第一件事就是点运行,能跑起来就算完事,这样其实浪费了整套代码的价值。先搞清楚项目整体在解决什么问题,才是正确打开方式。
1.1 为什么是 SpringBoot + Vue 这套组合
图书管理这类业务系统,天然适合前后端分离架构。前端要处理检索、翻页、弹窗、表单校验这些交互,后端要处理权限、事务、数据持久化。如果混在一起写 JSP 或 Thymeleaf,页面逻辑和业务逻辑相互纠缠,后期改一个字段都要前后翻找,非常痛苦。
SpringBoot 的价值在于"约定大于配置",内嵌 Tomcat,一个 jar 包就能起服务,配合 MyBatis 做 SQL 映射,写接口的效率很高。Vue 这边则靠组件化和响应式数据,把图书列表、借阅弹窗、统计图表这些 UI 片段独立开,页面结构清晰,和数据交互也直观。前后端通过 JSON 通信,后端查完数据库把结果包装成统一结构返回,前端拿到数据再渲染到表格和卡片上。
从学习角度讲,这套组合还有一个很大的好处:就业市场认可度高。SpringBoot 是 Java 后端的主流框架,Vue 是国内前端占有率很高的框架,MyBatis 和 MySQL 在企业项目中同样常见。做过一个完整的图书管理系统,再去看其他业务系统,比如进销存、点餐、音乐管理,会发现核心套路都一样,只是业务字段不同。
1.2 "智慧"两个字体现在哪里
普通图书管理系统就是图书表的增删改查,借书就 insert 一条记录,还书就 update 一条记录。但这套系统能叫"智慧",是因为它在基础 CRUD 之上做了几件很实用的事:
- 多条件组合检索:图书名称、作者、分类、ISBN 可以任意组合模糊查询,而不是只能按一个固定字段搜。
- 逾期自动计算:还书时自动对比应还日期和实际归还日期,按天计算逾期费用,前端直接展示明细。
- 借阅热榜统计:通过聚合 SQL 统计哪些图书被借次数最多,首页展示热门图书 Top 榜。
- 库存联动与预警:借书成功扣减库存,还书归还库存,当剩余库存低于设定阈值时,后台首页给出醒目的提示。
这些功能的本质并不复杂,核心就是 SQL 的聚合函数、日期比较、条件判断,以及后端对业务规则的封装。但正是这些功能组合在一起,让"图书管理系统"升级为"智慧图书管理系统"。在答辩或者做项目展示的时候,这几个亮点比单纯说"我做了增删改查"要有说服力得多。
1.3 核心功能清单与模块划分
从结构上看,系统通常分为几个角色和模块:管理员负责图书维护、读者管理、借阅审核;读者可以检索图书、借书、还书、查看个人借阅记录;系统本身提供统计分析和数据可视化。为了方便你对照源码,我把常见功能模块整理成下面这张表。
| 功能模块 | 核心页面 | 后端接口说明 |
|---|---|---|
| 登录认证 | 登录页 | 校验用户名密码,发放 token |
| 图书管理 | 图书列表/编辑弹窗 | 图书增删改查、上架下架、库存修改 |
| 读者管理 | 读者列表/启用停用 | 读者信息维护、状态管理 |
| 借阅管理 | 借书/还书/续借 | 借阅记录新增、归还更新、逾期计算 |
| 统计分析 | 首页大屏/统计报表 | 借阅量统计、热门图书、分类占比 |
| 个人中心 | 我的借阅 | 当前借阅、历史记录、逾期费用 |
拿到源码后,先对照这张表在代码里找到对应的 controller 和 vue 页面,把映射关系理清楚,再去看具体实现,思路会顺畅很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:先把项目在本地跑起来
不管你的目的是学习还是做毕设,第一步永远是让项目在本地运行起来。这一步卡住的人特别多,多数问题不是代码有问题,而是环境版本不匹配。
2.1 工具链清单与版本选择
我建议按下面这份清单准备环境,版本不必完全一致,但要注意兼容性:
| 工具 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | SpringBoot 2.x 系列搭配 JDK 8 最稳;如果源码是 SpringBoot 3.x,则需要 JDK 17 以上 |
| Maven | 3.6+ | 用 IDEA 自带或单独安装都可以 |
| Node.js | 14 以上即可,16/18 更稳 | 过高的 Node 版本偶尔会遇到 node-sass 兼容问题,建议优先用 16 |
| MySQL | 5.7 或 8.0 | 8.0 注意驱动和时区配置 |
| IDE | IDEA 或 VSCode | 后端建议 IDEA,前端用 VSCode 更轻量 |
这里有个常见的坑:MySQL 8.0 的驱动类名和连接 URL 参数和 5.7 不一样,8.0 要用 com.mysql.cj.jdbc.Driver,而且 URL 上最好加上 serverTimezone=Asia/Shanghai,否则跑起来可能报时区错误。这个细节虽然小,但能省下你半小时排错时间。
2.2 数据库初始化:建库脚本与字符集
源码里一般会带 sql 目录,里面放着初始化脚本。拿到脚本后,先打开看一眼表结构,确认数据库名是什么,然后执行脚本。执行的时候重点确认字符集,通常建议建库时指定:
sql复制CREATE DATABASE IF NOT EXISTS smart_library DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;
为什么用 utf8mb4 而不是 utf8?因为 utf8mb4 是 utf8 的超集,能完整支持中文,还能存 emoji 表情和其他生僻字符,前端表单万一输入了特殊符号,不会出现"数据插不进去"这种莫名其妙的问题。
核心的几张表结构你应该重点关注一下,这里给一个简化的图书表和借阅记录表作为参考:
sql复制CREATE TABLE `book` (
`id` bigint NOT NULL AUTO_INCREMENT,
`book_name` varchar(200) NOT NULL COMMENT '书名',
`author` varchar(100) DEFAULT NULL COMMENT '作者',
`publisher` varchar(100) DEFAULT NULL COMMENT '出版社',
`isbn` varchar(50) DEFAULT NULL COMMENT 'ISBN',
`category` varchar(50) DEFAULT NULL COMMENT '分类',
`stock` int DEFAULT '0' COMMENT '当前库存',
`total_stock` int DEFAULT '0' COMMENT '总库存',
`status` tinyint DEFAULT '1' COMMENT '状态 1上架 0下架',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `borrow_record` (
`id` bigint NOT NULL AUTO_INCREMENT,
`book_id` bigint NOT NULL COMMENT '图书ID',
`reader_id` bigint NOT NULL COMMENT '读者ID',
`borrow_date` date DEFAULT NULL COMMENT '借书日期',
`due_date` date DEFAULT NULL COMMENT '应还日期',
`return_date` date DEFAULT NULL COMMENT '实际归还日期',
`fine_amount` decimal(10,2) DEFAULT '0.00' COMMENT '逾期费用',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
可以看到借阅记录里面特意设计了 due_date 和 return_date 两个字段,逾期费用就是通过这两个日期的差值计算出来的。这就是前面说的"智慧"功能的数据基础。
2.3 前端环境配置与 npm 镜像
前端部分如果是 Vue2 工程,通常用的 vue-cli 脚手架;如果是 Vue3 工程,可能是 Vite 或 vue-cli。不管哪种,第一步都是安装依赖。国内直接 npm install 经常容易卡住,建议先配一下镜像源:
bash复制npm config set registry https://registry.npmmirror.com
然后再执行 npm install。如果安装过程中报 node-sass 相关的错误,首先检查 Node 版本是不是太高了,node-sass 对 Node 版本非常敏感;这类老项目也可以考虑用 npm install --legacy-peer-deps 跳过依赖冲突检查。
依赖装完后,先别急着 npm run dev,先把开发环境代理配置好。Vue 工程里一般有个 vue.config.js 文件,里面的 devServer 可以配置代理,把前端的 /api 请求转发到后端端口,避免开发阶段遇到跨域问题。
js复制const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
devServer: {
port: 8081,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
})
这一段配置的意思是:前端跑在 8081 端口,所有以 /api 开头的请求,都会转发到本地 8080 端口的后端服务,并且把 /api 前缀去掉。很多同学后端接口路径写的 /api/book/list,但其实后端 controller 的 RequestMapping 是 /book/list,如果不做 rewrite,后端就会直接报 404,这一点要格外留意。
3. 后端核心实现:SpringBoot + MyBatis + MySQL
环境跑通之后,就可以开始啃后端代码了。后端是整套系统的中枢,理解了这层,你的收获会非常大。
3.1 分层架构与代码目录
一个标准的 SpringBoot 后端工程会分成 controller、service、mapper、entity 四层。很多初学者觉得分层麻烦,但真实项目必须这么干。你可以用开餐厅来类比:controller 是前台服务员,只负责接单和上菜,不关心菜怎么做;service 是后厨,负责具体业务逻辑,比如炒菜、炖汤;mapper 是仓库管理员,负责去数据库冰箱里拿食材和记录原料使用情况。
以图书查询接口为例,调用链是这样的:
java复制// Controller 层:接收前端请求
@RestController
@RequestMapping("/book")
public class BookController {
@Autowired
private BookService bookService;
@GetMapping("/list")
public Result list(@RequestParam Integer pageNum,
@RequestParam Integer pageSize,
BookQuery query) {
return Result.success(bookService.pageQuery(pageNum, pageSize, query));
}
}
java复制// Service 层:处理业务逻辑,比如分页要传给 PageHelper
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookMapper bookMapper;
@Override
public PageResult pageQuery(Integer pageNum, Integer pageSize, BookQuery query) {
PageHelper.startPage(pageNum, pageSize);
List<Book> books = bookMapper.selectBookList(query);
PageInfo<Book> pageInfo = new PageInfo<>(books);
return PageResult.of(pageInfo);
}
}
java复制// Mapper 层:定义数据库操作接口
@Mapper
public interface BookMapper {
List<Book> selectBookList(BookQuery query);
}
对应的 XML 文件放在 resources/mapper 目录下,里面写具体的 SQL。可以看到,controller 里没有任何 SQL 的影子,service 里也不直接操作数据库,每一层各司其职。这样改业务时只动 service 层,改查询条件时只动 mapper 层,页面字段变化只动 controller 层,互不干扰。
3.2 MyBatis 动态 SQL:多条件组合查询的关键
图书管理系统的检索功能要求支持书名、作者、分类、ISBN 的任意组合查询。如果用 JDBC 拼 SQL,要写一堆 if 判断字符串拼接,容易出 SQL 注入风险。MyBatis 的动态 SQL 帮你干掉了这个麻烦:
xml复制<select id="selectBookList" resultType="com.example.entity.Book">
SELECT * FROM book
<where>
<if test="bookName != null and bookName != ''">
AND book_name LIKE CONCAT('%', #{bookName}, '%')
</if>
<if test="author != null and author != ''">
AND author LIKE CONCAT('%', #{author}, '%')
</if>
<if test="category != null and category != ''">
AND category = #{category}
</if>
<if test="isbn != null and isbn != ''">
AND isbn = #{isbn}
</if>
</where>
ORDER BY id DESC
</select>
<where> 标签会自动处理条件前面的 AND 关键字,如果所有条件都为空,它不会生成多余的 WHERE,返回的就是全表查询。这里有一个非常重要的经验:模糊查询一定要用 CONCAT('%', #{value}, '%') 的写法,而不是直接在 XML 里写 LIKE '%${value}%'。前者是预编译参数,能防 SQL 注入;后者是字符串替换,一旦用户在搜索框里输入特殊字符,整个 SQL 结构都可能被改变,存在很大的安全隐患。虽然在课设场景下攻击风险不高,但是简历上出现"使用预编译防止 SQL 注入"这种描述,在面试官眼里是加分项。
3.3 MyBatis 分页插件:PageHelper 的标准用法
做后端时,列表页基本都要分页。MySQL 的原生分页是 LIMIT offset, size,但你不能每一页查询都手动算 offset,那太原始了。MyBatis 生态里最常用的分页插件是 PageHelper,用法非常固定,一共三步:
第一步,在 pom.xml 中引入依赖:
xml复制<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.7</version>
</dependency>
第二步,在查询前调用 PageHelper.startPage(pageNum, pageSize):
java复制PageHelper.startPage(pageNum, pageSize);
List<Book> list = bookMapper.selectBookList(query);
PageInfo<Book> pageInfo = new PageInfo<>(list);
第三步,把 PageInfo 返回给前端。PageInfo 里面已经帮你封装好了 total(总记录数)、list(当前页数据)、pageNum、pageSize、pages(总页数)等字段,前端表格和分页组件拿这些字段直接渲染即可。
PageHelper 的原理其实是用拦截器在 MyBatis 执行 SQL 之前,把原 SQL 拦截下来,自动改写成带 LIMIT 的版本,同时执行一条 COUNT 查询来获取总记录数。所以它有一个关键要求:PageHelper.startPage() 必须紧跟第一条查询语句,中间不能插入其他查询。很多人踩过这个坑——startPage 之后又调了另一个 mapper 的方法,结果分页跑到了那个查询上,数据全乱。正确做法是让分页紧贴目标查询,并且不要在一个方法里连续 startPage 两次。
3.4 application.yml 关键配置与调试技巧
后端工程的 src/main/resources 目录下有个 application.yml,这里面有几个配置直接决定项目能不能跑起来:
yaml复制server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/smart_library?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
这里三个细节非常值得说。第一个是 map-underscore-to-camel-case: true,它让你不用在 resultMap 里手写每一列的映射关系。数据库字段是 book_name,Java 属性是 bookName,开启驼峰映射后 MyBatis 自动完成转换,代码能少写一大半。
第二个是 log-impl,设置为 StdOutImpl 后,控制台会把 MyBatis 执行的每一条 SQL 和查到的参数打印出来。这个在调试阶段极其有用,前端传了参数但查不到数据,或者 SQL 写错了,一眼就能在日志里看到真实执行的语句。
第三个是 MySQL 8.0 的驱动类名。5.7 时代常用的是 com.mysql.jdbc.Driver,8.0 之后改成了 com.mysql.cj.jdbc.Driver。如果你的源码基于 8.x 版本,驱动类写错会在启动时就报 "ClassNotFoundException",这个报错信息非常典型,直接修改驱动类即可。
4. 前端实现:Vue 组件化与核心页面交互
后端跑通了,前端才能真正调用数据。前端部分重点理解组件化设计、路由配置和数据请求封装。Vue 的核心思想是"数据驱动视图":你只需要维护一份数据,页面会自动跟着变,不需要手动操作 DOM。
4.1 路由配置与页面骨架
使用 vue-router 管理页面之间的跳转。图书管理系统典型的页面包括登录页、主布局页面(嵌套着图书管理、读者管理、借阅管理、统计报表等子页面)。在 vue-router 中配置嵌套路由时,主布局通常是一个包含侧边栏和顶栏的 Layout 组件,子页面通过 <router-view> 渲染在内容区。
js复制const routes = [
{ path: '/login', component: Login },
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{ path: 'dashboard', name: 'Dashboard', component: () => import('@/views/Dashboard.vue') },
{ path: 'book', name: 'Book', component: () => import('@/views/BookManage.vue') },
{ path: 'borrow', name: 'Borrow', component: () => import('@/views/BorrowManage.vue') }
]
}
]
注意这里子路由组件用的是箭头函数动态 import,这叫路由懒加载,页面首次加载时不解析这些组件,只有访问对应路由时才加载 JS 文件。系统页面多的时候,懒加载能明显减少首屏加载时间,这也是一个值得写进简历的优化点。
4.2 axios 封装与请求拦截器
不要在每个页面里直接调 axios,应该统一封装请求模块。这么做的好处是:所有接口出错时只用处理一次;需要携带 token 认证时只用写一次;后端返回结构变了只用改一处。
js复制import axios from 'axios'
import { Message } from 'element-ui'
import router from '@/router'
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
})
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')
router.push('/login')
}
Message.error('网络异常,请稍后重试')
return Promise.reject(error)
}
)
export default request
这段代码里面有两个很有价值的细节。第一个是请求拦截器里带 token,实现登录认证的统一处理;第二个是响应拦截器里统一解包 code,后端只需要把所有接口包装成 { code: 200, data: ..., msg: ... } 结构,前端所有页面拿到的都是 data 部分,业务代码非常干净。401 状态码的出现频率虽然不高,但处理了之后,用户登录过期时会被自动踢回登录页,而不是看着页面一直转圈。
4.3 表格 + 分页 + 搜索表单的联动实现
图书管理页面是典型的前后端交互场景。页面结构从上到下依次是:搜索表单(书名、分类、作者)、表格(图书数据)、分页组件。三者的联动逻辑是:搜索按钮触发查询并重置页码为 1,表格数据加载当前页数据,分页组件切换页码时重新请求数据。Vue 里实现这一步并不复杂:
js复制data() {
return {
queryParams: { bookName: '', category: '', author: '' },
pageNum: 1,
pageSize: 10,
total: 0,
tableData: []
}
},
methods: {
loadData() {
request.get('/book/list', {
params: { ...this.queryParams, pageNum: this.pageNum, pageSize: this.pageSize }
}).then(res => {
this.tableData = res.data.list
this.total = res.data.total
})
},
handleSearch() {
this.pageNum = 1
this.loadData()
},
handlePageChange(page) {
this.pageNum = page
this.loadData()
}
},
created() {
this.loadData()
}
这套写法在后台管理类项目中非常通用,学会了这一个页面,其他任何管理页面只是换一下接口路径和字段名。如果你做过音乐管理系统、订单管理系统,会发现几乎一模一样。
4.4 弹窗组件与父子组件通信
借书、还书、新增图书这些操作通常用弹窗(Dialog)完成。弹窗本身是一个子组件,需要从父组件接收当前选中行的数据,操作完成后通知父组件刷新列表。Vue2 里用 props 和 $emit 实现,Vue3 里用 props 和 defineEmits,核心思想是一样的。
子组件接收父组件传过来的图书对象,父组件监听子组件发出的事件:
vue复制<el-dialog :visible.sync="dialogVisible" title="借书确认" @close="resetForm">
<el-form :model="borrowForm">
<el-form-item label="图书名称">
<el-input v-model="borrowForm.bookName" :disabled="true" />
</el-form-item>
<el-form-item label="借阅天数">
<el-input-number v-model="borrowForm.days" :min="1" :max="30" />
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitBorrow">确认借书</el-button>
</span>
</el-dialog>
借书按钮点击后,前端把图书 ID 和借阅天数传给后端,后端在 service 层里完成插入借阅记录和扣减库存两步操作,并且包在事务里。这里有个核心知识点:如果插入记录成功但扣库存失败,数据就脏了,所以这两个写操作必须加上 @Transactional 注解,保证要么都成功,要么都失败回滚。这种细节面试官特别喜欢问,答上来就是加分项。
5. 联调部署与问题排查实录
装环境、写代码都是顺理成章的事情,真正让人崩溃的是前后端联调和部署阶段。这一节把常见的坑按出现频率整理出来。
5.1 跨域问题的两种解决方式
当前端跑在 8081,后端跑在 8080,浏览器会拦截跨域请求。解决方式有两种,二选一即可。
第一种是后端配置全局跨域,推荐在 SpringBoot 启动类或单独的配置类中加一个 CorsFilter:
java复制@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
第二种是前端在 vue.config.js 里配置代理,就是前面环境准备阶段提到的方式。本地开发阶段我强烈推荐用代理这种方式,因为它完全不改后端代码,而且网络请求看起来就像同源请求,不会出幺蛾子。后端 CORS 配置更适合部署在服务器上时使用。
5.2 典型问题速查表
我把实际操作中遇到的常见问题整理成一张表,遇到相同报错直接对照排查:
| 报错现象 | 根本原因 | 解决方法 |
|---|---|---|
| 启动报 ClassNotFoundException: com.mysql.jdbc.Driver | MySQL 8.0 驱动类名变更 | 改为 com.mysql.cj.jdbc.Driver |
| 查询数据中文乱码 | 数据库连接 URL 缺少字符集参数 | URL 加上 characterEncoding=utf8 |
| 分页数据错乱或分页不生效 | PageHelper.startPage 和查询之间插入了其他查询 | 拆分方法,让 startPage 紧跟查询 |
| 前端请求 /api/book/list 报 404 | 代理未做 pathRewrite 或后端路径不匹配 | 检查 vue.config.js 和 controller 注解 |
| 控制台看不到 SQL | 未配置 MyBatis 日志 | log-impl 设置为 StdOutImpl |
| npm install 报 node-sass 错误 | Node 版本过高或依赖冲突 | 降低 Node 版本,或使用 legacy-peer-deps |
| 登录后刷新页面状态丢失 | token 只存在内存未持久化 | 存入 localStorage,路由守卫校验 |
| 借书成功但库存没变化 | 两个写操作未加事务 | service 方法上加 @Transactional |
这张表不是我凭空想出来的,每一个都是实际项目里反复出现的经典问题。尤其是分页不生效和控制台看不到 SQL 这两条,几乎每周都会有人问。遇到问题不要慌,记得一个原则:后端看不到日志就先配日志,前端报错就打开浏览器 F12 看 Network 面板,大部分问题都能定位。
5.3 打包部署:本地开发到服务器上线
开发完成后,部署方式也很标准。后端在项目根目录执行:
bash复制mvn clean package -DskipTests
执行完成后 target 目录下会生成一个 jar 包,用 java -jar xxx.jar 启动即可。如果服务器内存有限,可以指定堆内存上限:
bash复制java -Xms256m -Xmx512m -jar xxx.jar
前端执行 npm run build,构建产物生成在 dist 目录。部署时有两种常见方案。简单方案是把 dist 里的静态资源复制到 SpringBoot 的 static 目录下,直接和后端打成同一个 jar;规范方案是用 Nginx 托管前端静态文件,并把 /api 请求转发到后端端口。后者的好处是前后端可以独立升级,Nginx 配置大致如下:
nginx复制server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
配置中的 try_files 是为了解决 Vue Router 的 history 模式下刷新页面 404 的问题,部署单页面应用时这段配置几乎是必须的。如果你面试或者答辩时能把这一层部署逻辑讲清楚,含金量会明显高于"我本地能跑"。
6. 拿到源码后怎么学与二次开发方向
最后说说我认为最有价值的部分:源码的正确打开方式,以及你还能往哪些方向升级。
6.1 学习源码的正确顺序
很多人拿到源码喜欢从第一个文件读到最后一个文件,读不了十分钟就放弃了。我更推荐"先跑通、再对照、后修改"三步法。
第一步强行把项目跑起来,不要管每个文件是什么意思。第二步按照"页面触发接口、接口调 Service、Service 调 Mapper、Mapper 执行 SQL"这条链路,随便选一个功能从头到尾走一遍。比如点一下"借书"按钮,跟着前端代码找到后端接口,再跟着后端代码找到 SQL 语句,最后在数据库里看执行结果。走通一个完整链路后,你会突然理解整套架构是怎么回事。第三步才是动手改代码,一开始可以改小地方,比如把图书列表的排序方式换一下,把某个查询条件改成下拉框,再尝试加一个字段、加一张表。
这里我要说一个不容易被注意到的点:拿到源码后,先看 pom.xml 和 package.json,确认版本号。不同版本的 SpringBoot 在配置项和依赖坐标上有差异,如果你搜解决办法时看到别人的版本和你不一样,大概率得调整思路。另外,源码里的账号密码、数据库密码这些连接信息基本是开发环境配置,上线前必须改成强密码,否则就是裸奔状态。
6.2 可以扩展的三个实用方向
如果课设或者毕设要求做创新点,或者你手痒想练手,我建议在现有源码基础上做这几个方向的升级,难度和含金量都会提升一个台阶:
第一个方向是引入 Redis 做缓存。热门图书榜单、图书分类统计这类读取多、更新少的数据,可以缓存到 Redis,降低数据库压力。面试时聊到性能优化,这个点很实用。
第二个方向是接入 ECharts 做可视化大屏。现在的系统虽然有统计功能,但展示形式比较朴素。用 ECharts 把借阅趋势、分类占比、读者活跃度做成图表,放在首页大屏上,"智慧图书管理系统"的观感会立刻不一样。
第三个方向是引入扫码借还书功能,使用户在移动端也能方便地完成借还操作。图书入库时为每本书生成二维码,借书时扫码识别图书 ID,还书时同样扫码完成归还。这个方向涉及到二维码生成、扫码解析和移动端适配,技术链条更长,做出来之后不管用于展示还是写进简历,都有很好的效果。
之后再去看其他同类型项目,比如音乐管理系统、在线考试系统,会发现前端页面换成相应业务字段,后端换成不同数据表,核心骨架完全一致,到那时候,你就真正入门了。
我自己的体会是,第一次啃这种完整项目源码时,最大的障碍不是技术难度,而是"看不懂就想放弃"的焦虑。其实没必要强求一次看懂所有细节,先把主链路跑通,再一点点往外扩展,很多困惑会在你动手修改代码的时候自动解开。这套图书管理系统的每一层代码都值得你反复翻看,尤其是 MyBatis 的动态 SQL 和 PageHelper 分页这两块,看懂了它们,你的后端开发就算真正上路了。
