news 2026/9/24 15:37:09

SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0智慧图书管理系统开发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0智慧图书管理系统开发实战

从立项到交付,这套 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 的智慧图书管理系统源码,前前后后折腾了将近两个月。最初其实是帮朋友的一个社区书屋做内部管理工具,做着做着发现纯 CRUD 根本扛不住真实的借阅场景——图书状态不同步、读者借阅超时没人管、高峰期排队借书效率极低。于是我把整套系统推翻重来,最终沉淀出了这套包含完整前后端源码和项目文档的方案。这篇博文就把整个项目的设计思路、核心代码逻辑、踩坑过程和部署经验一次讲透,适合正在做 Java Web 课程设计、毕业设计,或者想快速上手 SpringBoot2 + Vue3 前后端分离项目的开发者参考。

1. 为什么做这套"智慧图书管理系统":从体力活到脑力活

先说说最开始的需求。社区书屋的图书管理还停留在 Excel 表格时代,书籍登记靠手工录入,借还记录靠本子签名,库存盘点要一本一本对。等我真正去现场蹲了半天,发现最痛的还不是录入慢,而是信息不同步——读者在手机上查到的"可借"状态,到现场发现那本书根本不在书架上,因为有人借走后忘了登记。这种数据不一致的问题,靠人盯人永远解决不了,必须从系统层面重新设计。

1.1 真实场景中的三大痛点

我梳理了一下,发现无论多大的图书管理系统,本质上都逃不开这三个核心矛盾:

库存与借阅状态的实时一致性。一本书被借走,库存要不要立刻减?如果减了,预约的读者怎么办?如果不减,现场读者就会白跑一趟。这套系统里的做法是:库存数量不变,增加"在馆数量"字段,每次借出和归还都在事务内更新,同时通过 MyBatis-Plus 的乐观锁插件防止并发下超借。

借阅期限的自动追踪。人工记忆借阅期限完全不靠谱。系统需要每天定时扫描借阅记录,自动把超期未还的书籍状态置为"逾期",并给读者生成罚款记录。这个功能我一开始用 Spring 的 @Scheduled 硬跑,后来发现多实例部署时会重复执行,重构时引入了分布式锁才彻底解决。

数据检索必须快。图书多了以后,按书名、作者、ISBN 模糊查询会越来越慢。MySQL8.0 的全文索引其实够用,但后来书量到了几万条,我还是给 title、author 字段加了双重索引策略,再配合前端输入防抖,实测查询响应稳定在 200ms 以内。

1.2 "智慧"二字到底落在了哪里

很多图书管理系统只做到了"能用",但离"好用"差得很远。这套系统的智慧点主要体现在三块:

一是借阅看板。前端首页用 Vue3 的 Composition API 聚合了七个统计卡片——今日借出、今日归还、逾期未还、热门图书 Top5、分类占比、月度借阅趋势、读者活跃榜。这些数据不再靠人肉统计,后端用了一条聚合 SQL 加三次 count 查询就能把数据全部查出来。

二是自动邮件提醒。基于 SpringBoot2 的 JavaMailSender,在借阅到期前三天自动给读者发提醒邮件。这个功能投入产出比极高,上线后逾期率直接降了一半。

三是操作留痕。所有图书新增、修改、下架操作都会写入操作日志表,管理员可以按时间倒序查审计记录。当时设计这个功能是出于社区书屋的公益属性,需要向社会公开管理透明度,结果成了很多人答辩时的加分项。

2. 技术栈敲定过程:SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 为什么这么搭配

技术选型没有绝对的最好,只有最适合当前场景的搭配。这套系统的技术栈组合不是随手拼的,每一项都是针对具体问题做的取舍。

2.1 后端为什么是 SpringBoot2 而不是 SpringBoot3

首先,SpringBoot2 是目前生产环境占有率最高的版本,生态最成熟,网上能查到的资料最多。SpringBoot3 出来以后,底层是 Jakarta EE 9+,javax 包名全换成了 jakarta,很多老项目的一堆依赖都要跟着改。对于图书管理系统这种业务型项目,稳定压倒一切,完全没必要去当新版本的小白鼠。

其次,SpringBoot2 内置的 Tomcat 9 和 JDK8 完美兼容。我手头几台服务器都是 JDK8,如果上 SpringBoot3 就必须升 JDK17,运维成本会增加不少。选择 SpringBoot 2.7.x 系列,它在 2.x 里集成了 springdoc-openapi 对接口文档的支持,体验已经很接近 3.x 了。

2.2 MyBatis-Plus 和 Spring Data JPA:我为什么选了前者

这里有个很常见的纠结。Spring Data JPA 的开发效率确实高,Entity 定义完基本不用写 SQL,但遇到复杂报表查询时非常痛苦——JPQL 写起来绕,原生 SQL 又违背了 JPA 的初衷。MyBatis-Plus 在这方面灵活得多,简单 CRUD 直接用 BaseMapper 内置方法,复杂查询就自己写 XML,完全可控。

图书系统里最典型的一个功能是"按分类统计图书数量":

// 直接用 LambdaQueryWrapper 实现 LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>(); wrapper.select(Book::getCategory, count(Book::getId).as("total")) .groupBy(Book::getCategory) .orderByDesc("total"); List<Map<String, Object>> list = bookMapper.selectMaps(wrapper);

这种写法不用写一行 XML,代码量少,可读性也强。而且 MyBatis-Plus 的分页插件是我见过最省心的——只需配置一个 MybatisPlusInterceptor,分页查询自动帮你拼接 LIMIT 语句,还不会破坏原来的 SQL 语义。

另外一个隐藏优势是 MyBatis-Plus 的逻辑删除。图书系统里"删除"一本书往往是假删除,方便后续追溯下架记录。在实体字段上加 @TableLogic 注解后,所有删除操作自动变成 UPDATE deleted = 1,查询时自动过滤,几行配置就解决问题。

2.3 Vue3 相比 Vue2 的实质提升

Vue3 对比 Vue2 不光是性能提升了,组合式 API带来的组织代码方式变化才是核心。图书管理后台的图书编辑弹窗,涉及表单校验、分类联动、ISBN 自动识别、封面上传四个功能,在 Vue2 里要分别定义 data、methods、computed、watch,一个文件能写到两百行。Vue3 里我用 setup 语法糖按功能拆分:

<script setup> // 图书列表相关 const { bookList, loading, fetchBooks } = useBookList() // 分类联动的逻辑 const { categoryTree, handleCategoryChange } = useCategory() // ISBN 扫码自动识别 const { recognizeISBN } = useISBNScanner() </script>

每个 useXxx 函数自成一体,后续维护基本不用通读整个文件就能定位问题。另外 Vue3 的响应式系统基于 Proxy,彻底解决了 Vue2 里数组修改无法触发视图更新的历史痛点,图书列表里批量勾选、动态增删行这种操作再也不用写 $set 了。

2.4 MySQL8.0 带来的具体收益

既然技术栈写的是 MySQL8.0,就要把 8.0 的威力真正用起来。我在这套系统里吃了三个 8.0 版本的红利:

窗口函数。图书借阅趋势统计需要计算"每个月的借阅量以及和上个月的环比变化",用窗口函数 LAG 一行 SQL 就搞定了,这在 5.7 里要写复杂的子查询和临时表。

SELECT DATE_FORMAT(borrow_time, '%Y-%m') AS month, COUNT(*) AS total, LAG(COUNT(*), 1, 0) OVER (ORDER BY DATE_FORMAT(borrow_time, '%Y-%m')) AS last_month_total FROM borrow_record GROUP BY DATE_FORMAT(borrow_time, '%Y-%m');

通用表表达式 CTE。做图书分类的树形展示时,从分类表递归查出所有子分类,WITH RECURSIVE 语法写起来非常优雅,Java 层不用再手动处理树形组装。

utf8mb4 字符集。MySQL8.0 默认就是 utf8mb4,存放 emoji 表情和生僻作者名都不再报错。要知道图书封面描述里经常带 emoji,这在 5.7 时代是个老坑。

注意:MySQL8.0 的默认认证插件改成了 caching_sha2_password,如果项目里用 5.x 版本的 JDBC 驱动连接会直接报错。一定要用 mysql-connector-java 8.0.x 以上版本,否则密码认证过不去。

3. 数据库设计:三张核心表如何撑起借阅闭环

图书管理系统的数据库设计说简单也简单,无非是书、人、借阅记录。但如果你直接建三张表开始写代码,后面改起来会想哭。我经历了两次重构才沉淀出现有的这套表结构。

3.1 核心表结构逐张拆解

book 图书表。除了基本的 title、author、isbn、publisher、publish_date、price 之外,我这里特别加了两个字段:total_count表示图书总册数,available_count表示当前可借数量。为什么不直接用库存相减?因为图书系统里"馆藏册数"和"可借册数"是两个概念——有些书在馆但被预约了,有些书已经下架但未出库,用两个字段才能准确表达业务状态。

reader 读者表。读者表除了姓名、手机号、邮箱这些常规字段,我加了max_borrow_countcurrent_borrow_count两个字段。前者记录该读者等级允许的最大借阅数量,后者实时更新当前在借数。借书时判断"当前在借数 >= 最大借阅数"就拒绝借出,这里用的是一个简单的业务校验,不需要上 Redis 计数器,省去了一套中间件运维。

borrow_record 借阅表。这张表是整个系统的核心,字段特别讲究。每一条记录包含 book_id、reader_id、borrow_time、due_time、return_time、status、penalty_amount。其中due_time 是在插入时就计算好的,默认为 borrow_time 加 30 天,而不是等到查询时再用 SQL 计算。这样做的原因是:定时任务扫描逾期记录时只需要比较 return_time IS NULL AND due_time < NOW(),索引利用效率极高。

3.2 索引设计与事务边界的考量

数据库设计最容易忽略的就是索引,等数据量上来再补索引,往往要锁表很久。我在这套系统里提前设计了三个关键索引:

索引名称包含字段设计理由
idx_book_isbnisbn图书检索最常用场景,精确匹配
idx_borrow_readerreader_id, status查询某读者当前借阅列表
idx_borrow_duedue_time, return_time定时任务扫描逾期记录

一个容易被忽视的细节是借书、还书操作必须是事务性的。借书时要同时插入 borrow_record 记录并更新 book 表的 available_count,还书时反之。这两个操作之间不能有间隙,否则就会出现"书借出去了但库存没减"的脏数据。

MyBatis-Plus 里开启事务很简单,在 Service 方法上加 @Transactional 注解就行。但有一点坑要注意:事务方法必须是 public 的,并且不能通过 this 调用自己——Spring 的事务是通过 AOP 代理实现的,自调用会绕过代理,导致事务完全失效。网上有人这么写踩了坑,我一开始也踩过。

关于并发:当两个读者同时借同一本只剩一册的书时,如果不加控制就会超借。我的做法是在借书时用乐观锁:

boolean success = updateBookAvailableCount(bookId, 1); // SQL: UPDATE book SET available_count = available_count - 1 // WHERE id = ? AND available_count > 0 if (!success) { throw new BizException("该图书已被借完"); }

这条 UPDATE 语句利用了数据库行锁的原子性,available_count > 0作为条件保证了永远不会减成负数。这样既避免了悲观锁的性能开销,又保证了数据一致性。

4. 后端落地:分层结构、JWT鉴权与借阅并发处理

数据库设计完,接下来是后端工程落地。我采用的仍然是经典的三层架构——Controller、Service、Mapper,但在细节上做了不少优化,让代码既好读又经得起压测。

4.1 工程目录结构与统一返回体

项目整体用 Maven 多模块还是单模块?图书管理系统这种体量,我建议单模块就够,不要为了结构而结构。但包结构一定要清晰:

com.smartlibrary ├── controller # 接口层 ├── service # 业务层(接口 + 实现) ├── mapper # MyBatis-Plus 的 Mapper 接口 ├── entity # 数据库实体类 ├── dto # 前端交互的数据传输对象 ├── vo # 视图对象 ├── config # 配置类(MyBatisPlus、Cors、拦截器等) ├── common # 通用类(返回体、异常、常量) └── utils # 工具类(JWT、日期等)

所有接口统一返回Result<T>结构,包含 code、message、data 三个字段。为什么要统一?因为前端能用 Axios 的响应拦截器统一处理错误,不用每个页面都写 try-catch。我的约定是:code=200 表示成功,code=400 表示业务错误(比如"库存不足"),code=401 表示未登录,code=500 表示系统异常。

配合全局异常处理器,业务代码里只需要:

if (book.getAvailableCount() <= 0) { throw new BizException("库存不足,无法借出"); }

BizException 会被全局处理器捕获并自动转化为 Result(400, "库存不足,无法借出", null),Controller 层完全不需要处理异常逻辑。

4.2 JWT 鉴权的一次完整设计

前后端分离项目的第一步就是要解决登录状态共享问题。我选用 JWT 而不是 Session,原因是 SpringBoot2 + Vue3 通常部署在不同域名或端口下,Session 跨域麻烦,JWT 天然无状态,扩展性更好。

JWT 工具类封装了三个核心方法:生成 token、解析 token、校验 token。登录成功后生成 token,里面只放 userId 和 username,过期时间设为 2 小时。登录接口把 token 返回给前端,前端存在 localStorage 里,每次请求在请求头加上Authorization: Bearer <token>

后端用一个拦截器统一校验:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); Claims claims = JwtUtil.parseToken(token); if (claims != null) { request.setAttribute("userId", claims.get("userId")); return true; } } response.setStatus(401); return false; } }

注意区分角色权限。图书管理系统的角色我做了三种:系统管理员、图书管理员、普通读者。管理员可以操作图书 CRUD 和借还记录,读者只能查看自己的借阅情况。这个权限用 JWT 里的 role 字段判断,在拦截器里调用hasRole()方法进行校验。

实际开发中有个常见误区:把所有接口都拦截,然后白名单一个个放行,结果漏了某个静态资源导致前端死活登录不上。我的建议是反着来:默认全部放行,只拦截需要鉴权的 /api/admin/** 和 /api/reader/** 路径,这样优先级更容易控制。

4.3 核心业务代码:借书、还书、续借

借书是整个系统最核心的业务,我把它的完整逻辑简化成四个步骤:

@Transactional(rollbackFor = Exception.class) public void borrowBook(Long bookId, Long readerId) { // 1. 校验读者状态和限额 Reader reader = readerMapper.selectById(readerId); if (reader.getStatus() != 1) { throw new BizException("该读者已被禁用"); } if (reader.getCurrentBorrowCount() >= reader.getMaxBorrowCount()) { throw new BizException("已达最大借阅数量"); } // 2. 乐观锁扣减库存 int rows = bookMapper.deductAvailableCount(bookId); if (rows == 0) { throw new BizException("该图书暂无余量"); } // 3. 插入借阅记录 BorrowRecord record = new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowTime(new Date()); record.setDueTime(DateUtil.addDays(new Date(), 30)); record.setStatus(BorrowStatus.BORROWED.getCode()); borrowRecordMapper.insert(record); // 4. 增加读者当前借阅数 readerMapper.incrementBorrowCount(readerId); }

续借功能的逻辑类似,但有一个隐藏业务规则:一本书只能续借一次。怎么判断是否续借过?借阅记录里有个 renew_count 字段,查询时先判断 renew_count >= 1 就拒绝续借。续借再把 due_time 往后延 15 天。

多表更新的事务范围需要注意:Service 方法里同时操作了 book、borrow_record、reader 三张表,必须保证原子性。这里不能把事务注解加在 Controller 方法上,因为 Controller 存在多线程安全问题,而且 AOP 代理如果配置不当还会失效。

4.4 定时任务与消息通知的实现

逾期提醒我用的 SpringBoot 自带的 @Scheduled 定时任务,每分钟执行一次扫描:

@Scheduled(cron = "0 0 8 * * ?") // 每天早上8点执行 public void checkOverdue() { List<BorrowRecord> overdueList = borrowRecordMapper.selectList( new LambdaQueryWrapper<BorrowRecord>() .eq(BorrowRecord::getStatus, BorrowStatus.BORROWED.getCode()) .isNull(BorrowRecord::getReturnTime) .lt(BorrowRecord::getDueTime, new Date()) ); overdueList.forEach(record -> { record.setStatus(BorrowStatus.OVERDUE.getCode()); borrowRecordMapper.updateById(record); emailService.sendOverdueNotice(record); }); }

这套逻辑在单机部署下没任何问题,但如果你用 Docker Compose 起了多个后端实例,多个实例会同时跑这个定时任务,导致重复发送提醒邮件,甚至重复更新状态。我的解决方案是引入一个简单的 Redis 分布式锁,用 SETNX 命令保证同一时刻只有一个实例能拿到任务执行权。如果你的部署环境没有 Redis,也可以把定时任务单独抽成一个模块,只部署单实例运行。

5. 前端工程化:Vue3 + Vite 从零搭出管理后台

Vue3 的前端模板我一开始试过 Vue CLI,后来全部迁移到了 Vite。原因很简单——Vite 的开发服务器冷启动只需几百毫秒,HMR 也是毫秒级,Vue CLI 在项目稍微大一点以后,每次改完代码等编译就要好几秒,非常影响调试效率。

5.1 目录组织与核心依赖选型

前端项目采用 Vite + Vue3 + Vue Router + Pinia + Element Plus 的组合。状态管理为什么用 Pinia 而不是 Vuex?Pinia 是 Vue 官方推荐的新一代状态管理库,API 更简洁,支持 Composition API 风格,而且对 TypeScript 的支持更友好。

src ├── api # 接口请求封装 ├── assets # 静态资源 ├── components # 公共组件 ├── layout # 后台框架布局 ├── router # 路由配置 ├── stores # Pinia 状态 ├── views # 页面视图 ├── utils # 工具函数 └── App.vue

这里要特别强调一下 api 目录的设计。负责前后端对接的同学最容易犯的错是每个页面直接调 axios,接口地址散落各处,后端一改路径前端全崩。我在项目里把所有接口集中到 api 模块:

// api/book.js import request from '@/utils/request' export function getBookList(params) { return request({ url: '/api/book/list', method: 'get', params }) } export function borrowBook(data) { return request({ url: '/api/book/borrow', method: 'post', data }) }

配合 axios 实例的统一封装,baseURL 和超时时间只配置一次,token 自动从 localStorage 取出塞进请求头,响应里 code 不等于 200 时自动弹出错误提示,所有页面都不用自己写错误处理。

5.2 路由守卫与权限控制

图书管理系统的路由分为三个层级:公开路由(登录页)、读者路由(图书查询、我的借阅)、管理路由(图书管理、借阅管理、统计报表)。前端路由守卫的作用是:未登录只能进登录页,已登录但角色不匹配不能访问对应模块。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') return } if (token && to.path === '/login') { next('/') return } const role = localStorage.getItem('role') if (to.meta.role && to.meta.role !== role) { next('/403') return } next() })

后端 JWT 拦截器和前端路由守卫是双保险关系。前端守卫管体验,拦截器管安全——就算有人绕过了前端路由直接调后端接口,后端也会因为角色不匹配返回 401,数据不会泄露。

5.3 几个提高开发效率的 Vue3 组件技巧

防抖搜索框。图书检索的搜索框输入时每敲一个字母就发一次请求,会打爆后端。用 Vue3 的 watch 加定时器实现防抖,300ms 内没有新输入才真正发请求:

const keyword = ref('') watch(keyword, (newVal) => { clearTimeout(timer) timer = setTimeout(() => { fetchBookList({ keyword: newVal, pageNum: 1, pageSize: 10 }) }, 300) })

表格的插槽用法。Element Plus 的 el-table 里,状态列、操作列是高度定制化的。用 #default="scope" 插槽可以很方便地拿到当前行数据:

<el-table-column label="状态" width="100"> <template #default="scope"> <el-tag :type="statusMap[scope.row.status].type"> {{ statusMap[scope.row.status].label }} </el-tag> </template> </el-table-column>

封装通用分页组件。后台列表页几乎都有分页需求,我封装了一个 Pagination 组件,统一了页码、每页条数、总条数三个参数,配合后端 PageResult 结构使用。这样所有列表页模板都是复制粘贴,只改 el-table 里的列就行。

实际开发中 Element Plus 还有个小坑:图标需要单独引入。如果你用全量引入的方式,打包体积会大不少,启动也慢。我的做法是按需引入,并且把常用图标集中注册:

import * as ElementPlusIconsVue from '@element-plus/icons-vue' for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) }

5.4 借阅看板的 ECharts 可视化

统计报表页我用 ECharts 5 做了三个图表:月度借阅趋势折线图、图书分类占比饼图、热门图书 Top5 条形图。Vue3 里使用 ECharts 的关键是处理图表实例的生命周期——组件销毁时要调用 dispose 方法释放资源,否则容易内存泄漏。

<script setup> import * as echarts from 'echarts' import { onMounted, onBeforeUnmount, watch } from 'vue' const chartRef = ref(null) let chart = null onMounted(() => { chart = echarts.init(chartRef.value) renderChart() }) watch(() => props.statisticsData, () => renderChart()) function renderChart() { chart.setOption({ xAxis: { type: 'category', data: props.statisticsData.months }, yAxis: { type: 'value' }, series: [{ type: 'line', data: props.statisticsData.counts, smooth: true }] }) } onBeforeUnmount(() => { chart?.dispose() }) </script>

这里需要注意一个自适应问题:图表容器尺寸变化时图表不会自动更新。我加了一个 window resize 监听,并在监听回调里调用 chart.resize(),防止浏览器窗口缩放后图表变形。

6. 联调阶段的高频坑:时区、雪花ID、跨域

这个项目从零到上线,最折磨人的不是业务逻辑,而是联调阶段遇到的环境问题。这些问题单个看都不难,但排查过程往往要花好几个小时,很多都是网上搜不到现成答案的。

6.1 MySQL8.0 时区与连接串配置

系统刚联调时,前端新增图书,控制台报了一个很诡异的错误:

Caused by: com.mysql.cj.exceptions.InvalidConnectionAttributeException: The server time zone value '�й���ʱ��' is unrecognized or represents more than one time zone.

问题原因是 MySQL8.0 的 JDBC 驱动对时区校验更严格,而服务器默认时区设置不对。解决方案是在 JDBC 连接串里显式指定时区:

spring.datasource.url=jdbc:mysql://localhost:3306/smart_library?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai

useSSL=false也很关键。MySQL8.0 默认开启 SSL 连接,但本地开发环境往往没有配置证书,开着 SSL 会导致连接变慢甚至报错。生产环境如果在内网部署,也建议关掉 SSL,性能更好。

另一个关于时区的坑是 Java 层面的。后端存入数据库的时间用了new Date(),取出来显示时会差 8 小时。这是因为 JVM 默认时区与数据库时区不一致。我的做法是在 SpringBoot 全局配置里统一:

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

同时数据库连接串带上 serverTimezone=Asia/Shanghai,保证三层时区一致,这个坑就算彻底填平了。

6.2 MyBatis-Plus 的自动填充与逻辑删除陷阱

MyBatis-Plus 有一个很方便的功能叫自动填充。create_time、update_time 这些字段,只要在实体上加上 @TableField(fill = FieldFill.INSERT) 注解,并实现 MetaObjectHandler 接口,插入和更新时就能自动写入时间,不用手动 set。这个功能用起来没问题,但有一个细节坑:更新操作时如果没有设置 update_time 字段,自动填充只在实体里有该字段时才会生效。如果实体里没写 updateTime,填充不会帮你加上去。

逻辑删除配 @TableLogic 也有讲究。逻辑删除的字段默认叫 deleted,但如果你在数据库里用的字段名是 is_deleted,需要在注解里显式说明:

@TableLogic(value = "0", delval = "1") private Integer deleted;

value 表示未删除的标记值,delval 表示已删除的标记值。这两个参数不写的话,MyBatis-Plus 默认认为 0 是未删除,1 是已删除,如果你的数据库约定反了,查询结果会反过来,排查很久才发现。

提示:使用逻辑删除后,唯一索引要万分小心。比如图书表的 ISBN 字段建了唯一索引,逻辑删除的书还占着这个 ISBN,新增图书时永远插入不进去。我的解决方案是删除唯一索引,改成普通索引,然后靠应用层做 ISBN 重复校验。

6.3 跨域问题的终极解法

前后端分离开发时,前端跑在 localhost:5173(Vite 默认端口),后端跑在 localhost:8080,浏览器会拦截跨域请求。这个问题我见过很多人用各种奇怪的姿势解决——JSONP、iframe 代理、改浏览器设置,其实 SpringBoot 后端加一个配置类就搞定:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

生产环境建议把 allowedOriginPatterns 改成真实域名,不要用 *。一个很容易踩的坑是:当 Spring Security 启用了 CSRF 时,跨域配置和 CSRF 配置会互相冲突。如果你用了 Spring Security,记得在 SecurityConfig 里额外放行 OPTIONS 预检请求,否则前端发的预检请求会被 401 拦截。

另外一个与跨域相关的经典问题是:Nginx 部署前端时忘记配置代理。我把前端打包后扔到 Nginx 的 html 目录,刷新页面 404,仔细排查发现是 Vue Router 用的 history 模式——刷新/book/list时,Nginx 找不到这个路径对应的文件。解决方法是加 try_files 配置:

location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }

7. 部署交付:Docker 装 MySQL8.0 与 Nginx 托管 Vue3

项目本地跑通不算完,能交付部署上线才是真本事。这套系统的部署方案我用的是 Docker Compose 编排,把 MySQL8.0、SpringBoot 后端、Nginx 前端三个容器一次性拉起来,数据持久化到宿主机目录。

7.1 Docker 方式部署 MySQL8.0 的完整过程

生产环境的 MySQL8.0 我推荐用 Docker 部署,省去手动安装的麻烦,而且容器与宿主机隔离,不会污染系统环境。完整命令如下:

docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=yourpassword \ -e MYSQL_DATABASE=smart_library \ -v /opt/mysql/data:/var/lib/mysql \ -v /opt/mysql/conf:/etc/mysql/conf.d \ -v /opt/mysql/log:/var/log/mysql \ --restart=always \ mysql:8.0

三个 -v 挂载分别对应数据文件、配置文件、日志文件。这里最需要注意的是配置文件挂载。MySQL8.0 的默认字符集不是 utf8mb4,虽然是 8.0 改进了很多,但建议还是在挂载的 my.cnf 里显式设置:

[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci default-time-zone=+08:00 [client] default-character-set=utf8mb4

连接容器里的 MySQL 时,如果用 Navicat 连接不上,大概率是 root 账号的 host 限制为 localhost。需要进容器执行:

CREATE USER 'smart'@'%' IDENTIFIED BY 'password'; GRANT ALL PRIVILEGES ON smart_library.* TO 'smart'@'%'; FLUSH PRIVILEGES;

7.2 SpringBoot 后端镜像构建要点

后端镜像的 Dockerfile 写得是否合理,直接影响镜像大小和启动速度。我推荐用多阶段构建的思路,编译阶段用 maven 镜像,运行阶段只保留 JRE:

FROM maven:3.8-openjdk-8 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests FROM openjdk:8-jre-alpine WORKDIR /app COPY --from=builder /app/target/smart-library.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]

有个细节:如果没有mvn dependency:go-offline这一步,每次构建都会去下载全量依赖,构建时间非常长。先把依赖缓存下来,第二步编译速度会快很多。

数据库连接信息我通过环境变量注入,不在镜像里写死。SpringBoot 的 application-prod.yml 里用${DB_HOST}这种方式引用环境变量,docker-compose 文件里传值:

services: backend: build: ./backend ports: - "8080:8080" environment: DB_HOST: mysql8 DB_PORT: 3306 DB_NAME: smart_library DB_USERNAME: smart DB_PASSWORD: password depends_on: - mysql8

7.3 Nginx 反向代理与前端部署

前端部署到 Nginx 的 Nginx.conf 需要同时处理静态文件托管和 API 反向代理。这样前端请求/api/...的路径会转发到后端容器,不会产生跨域问题:

server { listen 80; server_name lib.example.com; root /usr/share/nginx/html; index index.html; # 前端路由 history 模式 location / { try_files $uri $uri/ /index.html; } # API 反向代理 location /api/ { proxy_pass http://backend:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 60s; } # 静态资源缓存 location ~* \.(js|css|png|jpg|gif|svg)$ { expires 7d; } }

一个生产环境的真实问题是:如果后端接口响应比较慢、或者瞬时并发较高,Nginx 默认的 60 秒超时可能不够。图书管理系统里导出报表的接口如果数据量大,执行时间可能超过 60 秒。我这里给 proxy_read_timeout 单独设置为 120s,避免定时任务等长时间接口被 Nginx 中断。

部署结束后,验证流程建议按顺序走一遍:先访问前端页面确认静态资源配置没问题,再登录系统走一遍核心流程,最后看一下后端日志有没有报错。我用的验证 SQL 是这样的:

-- 验证借书后余量是否正确 SELECT b.id, b.title, b.available_count, COUNT(br.id) AS borrow_count FROM book b LEFT JOIN borrow_record br ON b.id = br.book_id AND br.return_time IS NULL WHERE b.id = 1 GROUP BY b.id;

结果里 available_count 加上在借数应该等于 total_count,对不上就是事务或者并发控制有问题。

7.4 项目文档的编写思路

标题里带了"【含文档】"三个字,说明文档对交付至关重要。我在项目里整理了三份文档:需求说明文档(业务背景、角色定义、核心功能清单)、数据库设计文档(ER图、表结构说明、字段注释)、接口文档(基于 Swagger 自动生成,再补充调用示例)。

写文档最忌讳空话套话,直接上干货。表结构文档我是直接从 MySQL 的 information_schema 里导出的,保证和实际数据库完全一致;接口文档上传到 Showdoc 或者 Apifox,前后端联调时大家看同一份文档,能避免很多因为接口字段名不一致导致的扯皮。

写在最后:几个让我少走弯路的经验

项目交付到现在已经稳定运行了三个多月,期间也断断续续根据反馈加了一些功能。回头复盘,想给做类似项目的朋友三个建议。

第一,先画清楚借阅状态流转图再写代码。我最初没画,直接写,结果还书、续借、逾期、挂失这些分支处理得一团糟,重构时痛苦不堪。后来的经验是:借阅状态必须做成枚举类,把每个状态允许的操作集中管理,比如"已逾期"状态下不允许续借只能先还书。

第二,前端封装要克制。前端代码抽组件、封装 API 是好事,但过度抽象反而增加维护成本。我的原则是:同一个模式出现三次才抽公共组件,否则宁可复制两份代码。

第三,文档一定要跟着代码走。我见过太多项目代码改了一百遍,文档还停留在第一版。现在我的习惯是:每次提交代码时如果接口有变化,顺手在文档里改一笔,后面交付时能省下大量解释成本。

这套系统的源码已经整理好,包含完整的 SQL 脚本、后端代码、前端代码和部署文档。如果你正在写类似的 Java Web 项目,直接拿来跑一遍,再参照自己的业务需求改一改,应该能节省不少开发时间。有问题可以在评论里聊聊,我看到了会尽量回复。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 15:33:14

Yii 2 框架核心包(yiisoft/yii2)安装指南与框架结构解析

后端Web框架 【免费下载链接】yii2 Yii 2: The Fast, Secure and Professional PHP Framework 项目地址&#xff1a; https://gitcode.com/gh_mirrors/yi/yii2 点击查看 免费下载 Yii 2 是目前 Yii 框架家族中应用最广的稳定版本&#xff0c;其核心框架代码以 yiisoft/yii2 包…

作者头像 李华
网站建设 2026/9/24 15:28:59

Falcon 2.0 迁移指南:破坏性变更、新特性与升级实战

后端Web框架API设计 【免费下载链接】falcon The no-magic web API and microservices framework for Python developers, with a focus on reliability and performance at scale. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/fa/falcon 点击查看 免费下载 导读 F…

作者头像 李华
网站建设 2026/9/24 15:27:07

Qt — 容器类控件

目录 1. Group Box 2. Table Widget 容器类控件&#xff1a;容器里面还可以容纳一些其它的控件 多元素控件&#xff1a;包含的内容&#xff0c;是一个一个的自定义好的 “Item”对象 容器类控件&#xff0c;包含的内容是前面已经讲述过的各种控件了&#xff0c;QPushButton…

作者头像 李华