ARTICLE DETAIL

资讯详情

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

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

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 9javax 包名全换成了 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 实现 LambdaQueryWrapperBook wrapper new LambdaQueryWrapper(); wrapper.select(Book::getCategory, count(Book::getId).as(total)) .groupBy(Book::getCategory) .orderByDesc(total); ListMapString, 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_count和current_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、日期等所有接口统一返回ResultT结构包含 code、message、data 三个字段。为什么要统一因为前端能用 Axios 的响应拦截器统一处理错误不用每个页面都写 try-catch。我的约定是code200 表示成功code400 表示业务错误比如库存不足code401 表示未登录code500 表示系统异常。配合全局异常处理器业务代码里只需要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() { ListBorrowRecord overdueList borrowRecordMapper.selectList( new LambdaQueryWrapperBorrowRecord() .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 而不是 VuexPinia 是 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 里状态列、操作列是高度定制化的。用 #defaultscope 插槽可以很方便地拿到当前行数据el-table-column label状态 width100 template #defaultscope el-tag :typestatusMap[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.urljdbc:mysql://localhost:3306/smart_library?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiuseSSLfalse也很关键。MySQL8.0 默认开启 SSL 连接但本地开发环境往往没有配置证书开着 SSL 会导致连接变慢甚至报错。生产环境如果在内网部署也建议关掉 SSL性能更好。另一个关于时区的坑是 Java 层面的。后端存入数据库的时间用了new Date()取出来显示时会差 8 小时。这是因为 JVM 默认时区与数据库时区不一致。我的做法是在 SpringBoot 全局配置里统一spring.jackson.time-zoneGMT8 spring.jackson.date-formatyyyy-MM-dd HH:mm:ss同时数据库连接串带上 serverTimezoneAsia/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:5173Vite 默认端口后端跑在 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_PASSWORDyourpassword \ -e MYSQL_DATABASEsmart_library \ -v /opt/mysql/data:/var/lib/mysql \ -v /opt/mysql/conf:/etc/mysql/conf.d \ -v /opt/mysql/log:/var/log/mysql \ --restartalways \ mysql:8.0三个 -v 挂载分别对应数据文件、配置文件、日志文件。这里最需要注意的是配置文件挂载。MySQL8.0 的默认字符集不是 utf8mb4虽然是 8.0 改进了很多但建议还是在挂载的 my.cnf 里显式设置[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci default-time-zone08:00 [client] default-character-setutf8mb4连接容器里的 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 镜像运行阶段只保留 JREFROM 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 --frombuilder /app/target/smart-library.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar, --spring.profiles.activeprod]有个细节如果没有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: - mysql87.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 项目直接拿来跑一遍再参照自己的业务需求改一改应该能节省不少开发时间。有问题可以在评论里聊聊我看到了会尽量回复。
返回列表