
这套图书管理系统算是我接触过的前后端分离项目里比较有代表性的一个技术栈很经典SpringBoot扛后端Vue负责界面MyBatis处理数据库交互MySQL存数据。对于正在学Java后端、或者准备做毕业设计、又或者想搞一个能完整跑通前后端分离流程的练手项目的人来说这套组合是绕不开的标配。我把整个项目的源码思路、数据库设计、接口实现、前端页面、部署流程以及我自己踩过的坑从头到尾梳理一遍照着这个思路走你不仅能把这个项目跑起来还能真正理解每个环节为什么这么做。1. 技术选型与整体架构拆解1.1 为什么是这套技术栈先聊选型。图书管理系统这种业务说白了就是典型的CRUD加一点关联查询业务复杂度不高但又需要覆盖登录鉴权、分页搜索、借还书流程这些常见功能。用SpringBootVueMyBatisMySQL这套组合在国内Java开发圈子里几乎是最主流、资料最多、遇到问题最好搜方案的一套搭配。SpringBoot选它的理由不用多讲简化配置、内嵌Tomcat、起步依赖机制省掉了传统SSH或SSM那一堆XML配置的麻烦。它能让一个后端服务从零到跑起来只用几分钟这对学习阶段非常重要因为精力应该花在业务逻辑上而不是耗在环境配置里。前端选Vue是因为它组件化开发和响应式数据绑定的思路非常符合管理后台这种交互密集的场景。图书管理系统的页面基本就是表格、表单、弹窗、分页这一套用Vue的组件系统拆完之后代码组织会很清晰维护起来也舒服。MyBatis在这套系统里的角色很关键。它不是全自动ORMSQL还是要自己写但正因为这样你对SQL的执行过程、结果映射、动态SQL的拼装逻辑会掌握得更扎实。图书管理里经常需要按书名、作者、分类、状态做组合条件查询用MyBatis的动态SQL可以很优雅地处理这类条件不确定的场景比在Java代码里手动拼字符串SQL要安全可靠得多。MySQL作为数据库没什么好争议的开源免费、部署简单、资料丰富。图书管理这种规模的数据量MySQL的性能绰绰有余配合Navicat或命令行工具管理起来也顺手。1.2 前后端分离的核心交互逻辑了解前后端分离这几个字背后的运行机制比跑通项目本身更重要。所谓分离是指前端和后端作为两个独立的应用分别开发和部署前端跑在浏览器里后端跑在服务器上二者通过HTTP接口通信数据格式统一用JSON。具体到图书管理系统你在浏览器里打开Vue开发服务器默认8080端口页面上的操作比如点查询图书前端会向后端接口发一个HTTP请求后端SpringBoot应用默认8081端口或你自己配的端口接收到请求后通过Controller接收参数、调用Service处理业务逻辑、再通过Mapper接口配合MyBatis去操作MySQL数据库最后把查询结果转成JSON返回给前端前端拿到JSON后更新页面表格数据。整个过程中前端不直接操作数据库数据库的访问权限完全收口在后端这是分离架构最核心的安全边界。分离开发带来的一个直接问题就是跨域。前端8080端口请求后端8081端口的接口浏览器会拦截这种跨域请求。解决方案通常两种后端配置CORS跨域资源共享过滤器在前端开发环境下配代理转发。1.3 项目目录结构规划项目拿到手先别急着跑把目录结构理清楚后面改代码会省很多事。一个标准的前后端分离项目通常是两个独立目录book-manage/ ├── backend/ # SpringBoot后端 │ ├── src/main/java/com/example/bookmanage/ │ │ ├── controller/ # 接口层 │ │ ├── service/ # 业务逻辑层 │ │ ├── mapper/ # MyBatis Mapper接口 │ │ ├── entity/ # 实体类 │ │ ├── common/ # 通用返回结果、异常处理 │ │ └── config/ # 跨域、拦截器配置 │ └── src/main/resources/ │ ├── mapper/ # MyBatis的XML映射文件 │ └── application.yml └── frontend/ # Vue前端 ├── src/ │ ├── api/ # 接口请求封装 │ ├── router/ # 路由配置 │ ├── views/ # 页面组件 │ ├── components/ # 通用组件 │ ├── store/ # 状态管理Vuex/Pinia │ └── utils/ # axios封装等工具 └── package.json这样的分层逻辑很清晰后端Controller只做参数接收和数据返回Service处理业务规则Mapper只负责和数据库打交道前端每个页面视图对应api目录下的一个接口模块改页面不影响接口逻辑改接口不影响页面结构。项目扩展新功能的时候比如加一个图书分类管理只需在后端加一套Controller-Service-Mapper在前端加一个页面组件和对应的api方法其他代码不用动。2. 数据库设计与MyBatis实战要点2.1 核心表结构设计图书管理系统的数据模型不算复杂但表关系设计好不好直接影响后面代码好不好写。我的建议是最少设计四张表用户表、图书表、借阅记录表、分类表可选但推荐。先看用户表userCREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(255) NOT NULL COMMENT 密码存储加密后的密文, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, role TINYINT DEFAULT 1 COMMENT 角色0管理员 1普通用户, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密码字段务必用加密存储我见过很多新手直接存明文这是非常危险的习惯。密码加密可以用BCryptSpring Security里自带这个工具也可以用MD5加盐。推荐BCrypt它会自动加盐而且每次hash的结果都不一样安全性远高于裸MD5。图书表book设计时要注意几个字段CREATE TABLE book ( id INT AUTO_INCREMENT PRIMARY KEY, isbn VARCHAR(20) DEFAULT NULL COMMENT ISBN号, book_name VARCHAR(100) NOT NULL COMMENT 书名, author VARCHAR(50) DEFAULT NULL COMMENT 作者, publisher VARCHAR(100) DEFAULT NULL COMMENT 出版社, category_id INT DEFAULT NULL COMMENT 分类ID, stock INT DEFAULT 0 COMMENT 库存总量, available INT DEFAULT 0 COMMENT 可借数量, status TINYINT DEFAULT 1 COMMENT 状态1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里我特别要强调stock和available两个字段的区别。stock是这本书一共多少本available是当前还有多少本可借。借书时available减1还书时available加1stock不变。如果只用一个数量字段就没法知道一本书丢没丢、有多少本在读者手里统计起来很费劲。借阅记录表是业务核心外键关联用户和图书CREATE TABLE borrow_record ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL COMMENT 借阅人ID, book_id INT NOT NULL COMMENT 图书ID, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 借书时间, due_time DATETIME COMMENT 应还时间, return_time DATETIME DEFAULT NULL COMMENT 实际归还时间, status TINYINT DEFAULT 1 COMMENT 1借出 2已还 3逾期 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;特别注意借阅和归还都是这个表。归还时不是删掉记录而是把return_time写入、把status改成已还这样每本书的借阅历史就有迹可循。四张表建好后给borrow_record表的user_id和book_id分别加上索引给book表的book_name加上普通索引查询性能会有明显提升。数据量小的时候感觉不出来但好习惯要养起来。2.2 MyBatis的XML映射与动态SQLMyBatis的使用中最核心的是Mapper接口和XML映射文件的配合。我习惯把复杂的SQL写在XML里简单查询用注解不过图书管理系统里的组合条件查询用XML的动态SQL最合适。一个很典型的需求场景图书列表页的搜索功能用户可能只输入书名可能只输入作者也可能什么都不输入直接点查询。这时候后端不能写死SQL必须根据前端传来的参数动态拼SQL。MyBatis提供了if、where、set、foreach这些标签专门解决这类问题。看这个例子select idsearchBooks resultTypecom.example.bookmanage.entity.Book SELECT * FROM book where if testbookName ! null and bookName ! AND book_name LIKE CONCAT(%, #{bookName}, %) /if if testauthor ! null and author ! AND author LIKE CONCAT(%, #{author}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if teststatus ! null AND status #{status} /if /where ORDER BY create_time DESC /selectwhere标签会自动处理AND前缀的问题如果第一个条件不成立而第二个条件成立where会自动去掉SQL里多余的AND不需要你在每个if标签里手动处理AND这对防止SQL语法错误非常有用。分页查询我建议用PageHelper插件这是MyBatis生态里非常成熟的分页方案。用法很简单在查询前调用PageHelper.startPage(pageNum, pageSize)然后紧跟的第一次查询会被自动拼上LIMIT语句。返回的PageInfo对象里包含了总记录数、总页数、当前页等所有分页信息直接传给前端即可。使用PageHelper有一个必须注意的坑startPage必须紧跟查询语句中间不能插入其他SQL操作否则分页会失效。我看到过有人在这个方法之前先做了个count查询结果分页条件set到了count那条SQL上查出来的数据完全不对。2.3 数据初始化与测试数据准备导入建表SQL后需要准备一批测试数据。图书表可以手动插入二十条左右的记录涵盖不同分类、不同库存状态的典型情况。用户表至少准备两个账号一个管理员账号role0一个普通用户账号role1方便测试不同权限下的接口行为。密码字段要处理一下。如果你用的是BCrypt可以在后端写一个临时的单元测试方法调用BCryptPasswordEncoder.encode(123456)生成密文再把这个密文INSERT到数据库里。有些人图省事直接在数据库里存明文123456开发阶段跑通没问题但一定要有这个意识上线前必须全部换成加密后的密文这不是可选项。3. 后端SpringBoot接口开发实践3.1 项目初始化与基础配置后端工程我习惯用Spring Initializr创建选Java 8或Java 11都行依赖勾选Spring Web、MyBatis、MySQL Driver如果你是IDEA也可以直接在IDEA里用Spring Initializr生成。生成之后pom.xml里要确保包含以下核心依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency注意MyBatis的SpringBoot Starter版本要和你的SpringBoot版本兼容比如SpringBoot 2.7.x配合MyBatis Starter 2.3.x是最稳的。网上有不少人遇到启动报错八成就是starter版本和boot版本不匹配。application.yml里最需要关注的是数据源配置和MyBatis配置spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/book_manage?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的密码 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.bookmanage.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置我建议一定要开它能自动把数据库里的下划线字段book_name映射成Java实体类中的bookName省掉大量写resultMap的时间。如果数据库字段命名不规范比如全大写、中文这个功能就帮不上忙了所以建表时一定要用规范的下划线命名。3.2 统一返回结果与全局异常处理前后端分离项目里接口返回的数据格式需要统一约定这样前端处理起来才不用针对每个接口单独判断。我习惯定义一个通用的响应体类包含三个字段code状态码、message提示信息、data业务数据。后端所有接口都返回这个结构前端axios封装的拦截器里统一判断code非0时弹出错误提示业务代码不需要到处写try-catch全局异常处理器兜底代码会清爽很多。全局异常处理是用SpringBoot的RestControllerAdvice注解实现的在类里写一个方法专门捕获Exception返回统一格式的错误信息。实际开发中我遇到过给前端返回500错误时浏览器控制台报CORS错误的奇葩情况原因就是全局异常没有处理跨域响应头。解决办法是在异常处理方法上也加上CrossOrigin注解或者在过滤器里统一给所有响应设置CORS头这个细节新手很容易忽略。3.3 登录鉴权与拦截器图书管理系统的登录逻辑要分角色处理管理员可以管理图书、查看所有借阅记录普通用户只能查询图书、借书还书、查看自己的借阅记录。这需要登录接口返回当前用户的角色信息前端根据角色渲染不同的菜单和按钮。JWTJSON Web Token是目前前后端分离项目最常用的登录凭证方案。用户登录成功后后端生成一个Token返回给前端前端存到localStorage里之后每次请求都在请求头带上Authorization字段后端通过拦截器解析Token、识别用户身份。这样做的好处是服务端不需要保存会话状态天然适合水平扩展。Token生成可以用jjwt库核心逻辑大概是把用户ID、用户名、角色放进Token的payload里加上过期时间用密钥签名。密钥一定不要硬编码在代码里至少放到配置文件中生产环境更要妥善保管。写拦截器时有一个坑前端Vue项目的预检请求OPTIONS方法会先于真实请求到达后端如果拦截器把所有请求都拦截下来校验Token预检请求会因为没带Token被拒掉前端会看到跨域请求失败的报错。正确做法是在拦截器里判断如果是OPTIONS请求直接放行。3.4 图书CRUD与借还业务图书的新增、修改、删除接口本身不难就是基本的Mapper方法调用。但删除功能要考虑一个业务边界如果这本书存在未归还的借阅记录直接物理删除会导致关联数据悬空。我在设计时通常采用逻辑删除的方式status字段从1置为0前端列表不再展示但借阅记录里的数据还能正常关联查询。借书接口是整个系统的核心亮点也是一个极容易出现并发隐患的地方。简单的写法是这样Book book bookMapper.findById(bookId); if (book.getAvailable() 0) { throw new BusinessException(库存不足); } book.setAvailable(book.getAvailable() - 1); bookMapper.updateById(book); borrowRecordMapper.insert(new BorrowRecord(userId, bookId));在单用户测试时这个逻辑没有任何问题但并发场景下两个用户同时借同一本书可能都读到available1然后都执行减1最后库存变成-1。解决思路是加事务和行锁查询时使用SELECT ... FOR UPDATE锁定该行记录事务提交后再释放锁。select idfindByIdForUpdate resultTypecom.example.bookmanage.entity.Book SELECT * FROM book WHERE id #{id} FOR UPDATE /select调用这个方法的Service方法加上Transactional注解就能保证同一时间只有一个事务能修改这本书的库存。图书管理系统的并发量可能没那么高但通过这个案例理解并发场景下的数据一致性这个核心概念才是写这个项目最大的收获之一。4. 前端Vue项目实战4.1 Vue环境准备与创建项目前端部分我用的Vue CLI创建项目命令很简单npm install -g vue/cli vue create book-manage-frontend创建过程中选择Vue 3也可以选Vue 2看你的学习方向如果刚入门建议直接Vue 3然后勾选Router、Vuex/Pinia、Axios这些插件。装完之后进入目录跑npm run serve本地开发服务器就起来了。网上创建Vue项目踩的最多的坑就是Node版本问题。Vue CLI要求Node.js版本不能太低一般要求12以上版本太老会各种报错版本太新的Node配合老版本的node-sass也会报错建议直接看官方文档确认版本要求。如果遇到node-sass安装失败一个更省事的方案是卸载掉改用sassdart-sass兼容性更好。4.2 axios封装与请求拦截器前端所有的HTTP请求建议统一走axios封装。原因是真实的项目里你几乎一定要做两件事在请求头里自动加Token、统一处理错误提示。如果每个页面都自己调axios然后自己处理代码重复严重也容易出纰漏。我习惯的封装方式是这样的在utils/request.js里创建一个axios实例import axios from axios import { ElMessage } from element-plus 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) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.message) return Promise.reject(error) } ) export default request这里有两个细节值得注意。第一个是baseURL设为/api配合前面说到的跨域代理前端代码里只写相对路径后面部署到生产环境时Nginx做一个/api的反向代理到后端服务前端代码一行都不用改。第二个是401状态码的统一处理Token过期时自动跳回登录页这是管理系统必备的用户体验。4.3 路由配置与登录守卫Vue Router的配置里图书管理系统应该有这几类页面登录页、图书列表页、图书编辑页、借阅管理页、个人借阅记录页、用户管理页管理员可见。路由需要分级权限控制。基础做法是在路由配置的meta字段里标记需要的角色{ path: /book, name: BookList, component: () import(/views/BookList.vue), meta: { requiresAuth: true } }, { path: /borrow, name: BorrowManage, component: () import(/views/BorrowManage.vue), meta: { requiresAuth: true, roles: [admin] } }然后在router.beforeEach导航守卫里做校验router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.meta.requiresAuth !token) { next(/login) } else if (to.meta.roles !to.meta.roles.includes(role)) { ElMessage.warning(没有权限访问) next(false) } else { next() } })注意路由懒加载的写法() import(...)这样打包时会自动按路由拆分成多个chunk首屏加载速度会比打包成一个大的JS文件快很多。项目小的时候差别不明显但养成习惯很重要。4.4 核心页面实现思路图书列表页是前端最复杂的页面包含搜索表单、表格、分页、新增弹窗、编辑弹窗、删除确认这些模块。我习惯用Element Plus组件库来搭建el-table绑定列表数据el-pagination控制分页el-dialog做表单弹窗el-form配合校验规则实现表单验证。有一点要提醒表单弹窗里的数据新增和编辑虽然共用一个组件但打开时务必区分初始状态。新增时清空表单编辑时根据当前行数据回填。很多人在点编辑弹窗里还是上一次新增的数据这个bug上浪费过时间根源就是没在open方法里正确重置表单。借阅管理页需要展示借阅记录包括用户ID、书名、借书时间、应还时间、状态这些列。管理员可以在这里点确认归还普通用户在个人中心只能看到自己的记录。这些页面不复杂但要注意时间格式处理。后端返回的时间格式通常是2025-01-15T10:30:00这种带T的格式要处理好格式转换否则页面上显示的时间非常难看前端可以写一个formatDate公共方法统一处理。5. 联调、打包与部署上线5.1 开发环境跨域配置实操前端开发服务器默认是localhost:8080后端是localhost:8081直接请求必然跨域。我推荐的做法是在Vue项目的vue.config.js里配置devServer的代理module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } } }这个配置的效果是前端发请求到/api/user/login开发服务器会把它转发到http://localhost:8081/user/login同时changeOrigin: true会修改请求头里的Host让后端认为请求来自同源从根源上规避了跨域问题。这种方式比在后端加CrossOrigin注解好在哪里最大的优势是生产环境可以无缝切换。开发环境下代码用相对路径/api/xxx部署到生产环境后Nginx同样配一个/api的反向代理前端代码不需要做任何修改。而如果代码里硬编码了http://localhost:8081这种绝对路径部署时要么改代码重新打包要么做环境变量替换麻烦得多。5.2 前后端生产环境打包后端打包比较简单在项目根目录执行mvn clean package -DskipTests打包成功后target目录下会生成book-manage-0.0.1-SNAPSHOT.jar文件这就是一个可以直接运行的后端服务。在服务器上执行java -jar book-manage-0.0.1-SNAPSHOT.jar如果不指定端口默认8080。但需要注意如果前后端部署在同一台机器上后端端口不能和Nginx的端口冲突建议在application.yml里把server.port改成8081避免直接占用80或8080。前端打包执行npm run build打包完成后dist目录就是所有的静态文件HTML、CSS、JS。接下来用Nginx托管这些静态文件并把/api的请求代理到后端server { listen 80; server_name yourdomain.com; root /usr/share/nginx/html/book-manage; index index.html; location /api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }这个配置里最关键的是try_files $uri $uri/ /index.html这一行。因为Vue是单页应用路由用的history模式直接访问/book这种路径时服务器上并没有对应的物理文件必须重定向到index.html让前端路由接管页面渲染。如果不写这行刷新页面就会出现404。5.3 常见问题排查与避坑实录把这个项目从头到尾跑一遍我遇到过的典型问题整理了一下给你排雷问题现象可能原因解决方案后端启动报数据库连接失败MySQL服务未启动、密码错误、URL里的库名拼错先确认MySQL能正常连接再检查application.yml配置前端请求接口报404代理路径或Controller路径不匹配先看后端Swagger或直接浏览器访问接口URL确认通不通再查代理配置前端请求接口报500Mapper XML路径不对、SQL语法错误看后端控制台详细异常栈重点检查mapper-locations配置和XML里SQL登录接口返回401Token生成或校验逻辑有误检查拦截器是否放行了登录接口、密钥是否一致页面刷新404前后端路由history配置问题Nginx配try_files兜底或用hash模式路由分页数据不对PageHelperstartPage与查询之间插入了其他SQL确保startPage紧邻要分页的查询语句还有一个很经典的问题数据库里的时间字段比如create_time带了T字母。这是JDBC驱动处理DATETIME时按ISO 8601格式序列化的结果解决办法是在application.yml里配置JSON序列化格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai配置之后接口返回的时间就变成2025-01-15 10:30:00前端显示就没有那个讨厌的T了。5.4 系统扩展与进阶优化方向这个图书管理系统跑通之后如果想继续往深了学有几个方向值得探索。第一个方向是把权限体系升级成Spring Security JWT的方式。现在很多后台接口只用拦截器校验Token代码简单但功能有限。Spring Security提供了完整的认证授权框架方法级权限控制、密码编码器、会话管理这些都是现成的学习曲线陡一些但弄懂了之后写任何管理系统都会轻松很多。第二个方向是加入Redis做缓存。比如说图书列表的热门分类查询可以把查询结果缓存到Redis里设置过期时间下次请求直接走缓存减轻数据库压力。这个优化思路在真实项目中非常重要也是面试常问的点值得专门研究。第三个方向是引入MyBatis-Plus。这个增强工具提供了通用Mapper、通用Service、条件构造器等轮子能把CRUD代码量再砍掉一大截。我见过不少用MyBatis-Plus做图书管理系统的案例代码结构比纯MyBatis简洁不少如果你在学MyBatis的同时也了解一下Plus将来工作直接用得上。回到最初的话题为什么我建议把图书管理系统作为前后端分离练手项目来认真做一遍因为它的业务边界足够清晰、数据模型简单但不简陋你能在这个项目里完整经历从建库到接口到前端页面再到部署上线的全过程。这中间踩的每一个坑、解决的每一个报错都会变成你下一段技术路上的宝贵经验。你在实际动手时可能会遇到跟我上面描述略有出入的情况这是很正常的搜索引擎和报错日志是你最好的老师项目做完了你的进步会比想象中大得多。