ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue图书管理系统实战:从数据库设计到前后端部署全解析

SpringBoot+Vue图书管理系统实战:从数据库设计到前后端部署全解析 开篇这个图书管理系统项目到底值不值得做先说结论如果你正在学 SpringBoot Vue 这套主流前后端分离技术栈图书管理系统绝对是最合适的练手项目没有之一。我见过太多人一上来就搞电商、搞支付、搞高并发抢购结果被各种中间件、分布式事务、消息队列劝退。图书管理的业务逻辑足够典型——有增删改查、有模糊搜索、有分页、有前后端交互但又足够简单不会让你陷入复杂的业务泥潭。它完美覆盖了企业级 Java 开发最核心的技能点SpringBoot 做接口、MyBatis 做持久层、MySQL 存数据、Vue 做页面交互这套组合放到任何一家中小型公司都是能直接上手干活的技术栈。这个项目解决的核心问题也很直接图书信息怎么管借阅记录怎么查分类统计怎么做从图书馆管理员的角度要能录入新书、修改价格、下架旧书从读者的角度要能按书名或作者搜索、查看详情从系统的角度要有分页检索、要有数据校验、要有合理的表结构。这些需求翻译成技术语言就是一张或多张数据表的设计、一组 RESTful 接口的编写、一套前端页面的渲染。谁适合参考这个项目刚学完 SpringBoot 基础、想找一个完整项目串联知识点的同学准备毕设、需要一套可运行源码参考的大四学生以及前端想入门、想搞清楚 Vue 怎么调后端接口的开发者。这篇文章我不打算教你那种复制粘贴跑起来就完事的玩法而是把整个系统从架构设计到实际编码、从环境搭建到踩坑排查完整拆开讲。1. 整体设计思路这不是一个简单的 CRUD 堆砌1.1 需求拆解与功能边界很多初学项目最大的问题不是功能太少而是功能太多、边界模糊。图书管理系统最常见的失控情况是想塞进去角色权限、消息通知、预约排队、逾期罚款等等最后每个模块都只做了个半成品。我建议第一版把需求收敛到四个核心模块图书管理书籍的增删改查包括书名、作者、ISBN、出版社、价格、库存、分类、封面图、简介。分类管理图书分类的维护比如文学、科技、历史、少儿支持树形结构或者简单的一级分类。借阅管理借书、还书、续借这三个动作需要记录借出时间、应还时间、实际归还时间。读者管理读者信息的维护包括姓名、学号/工号、手机号、借阅状态。这四个模块做好系统的主干就立住了。在此基础上再去考虑扩展比如用拦截器做简单的登录验证用 AOP 记录操作日志用定时任务统计借阅排行榜。架构上做加法功能上做减法这是我经过多个项目之后总结出的原则。1.2 为什么选 SpringBoot Vue 这套组合选技术栈不是因为它新而是因为它生态成熟、学习曲线平缓、招人需求大。后端选 SpringBoot 的理由不用多说——它比传统的 SSMSpring SpringMVC MyBatis省掉了大量 XML 配置内嵌 Tomcat 让部署变成打 jar 包 java -jar加上 Spring 生态的自动装配机制非常适合快速搭建中小型业务系统。而且 SpringBoot 的版本管理非常省心2.7.x 和 3.x 各有拥趸配合 MyBatis 都能稳定运行。前端选 Vue 的原因也很实在Vue 的模板语法直观、组件化开发模式清晰、官方脚手架 Vite 启动速度快而且 Vue 全家桶Vue Router Pinia Element Plus在管理后台这个场景下的成熟度极高。图书管理系统本质上是一个管理后台这种密集的表单 表格 弹窗交互正好是 Vue Element Plus 的主场。数据存储用 MySQL 是几乎所有 Java 后端岗位的标配要求它的事务机制、SQL 支持度、运维生态都是经过海量业务验证的。考虑到图书系统的数据量级万级以内MySQL 完全够用不需要引入 Redis 做缓存如果不做高并发排行的话也不需要引入 Elasticsearch 做全文搜索MySQL 的 LIKE 查询已经能满足中小规模检索。1.3 为什么用 MyBatis 而不是 Spring Data JPA这个问题我经常被问到。我的回答是看团队习惯和项目类型。图书管理系统这种业务逻辑简单、SQL 也比较直接的场景两种方案都能胜任。但 MyBatis 的优势在于SQL 完全可控、灵活度极高尤其是多表关联查询、分页查询、动态条件搜索这些场景写 SQL 比拼接 JPQL 或者用 Specification 更直观。举一个实际场景借阅记录查询需要关联图书表、读者表同时还要按借阅状态和日期范围过滤这种动态 SQL 用 MyBatis 的where标签和if标签写起来非常顺手逻辑清清楚楚。另外MyBatis 在面试中出现频率极高面试官很喜欢问一级缓存、二级缓存、#{} 和 ${} 的区别、如何实现多表查询等问题把 MyBatis 用熟练了对求职也有直接帮助。这本书的完整源码就是用了 MyBatis配合 MySQL整套持久层方案是经过大量生产环境验证的。2. 数据库设计与表结构这一步决定了系统能走多远2.1 核心表设计思路图书管理系统的数据库设计并不复杂但有几个细节必须注意。图书表book是系统的绝对核心字段至少包含id、book_name、author、isbn、publisher、publish_date、price、stock、category_id、cover_url、description、create_time、update_time。ISBN 字段要注意加上唯一索引因为同一个 ISBN 理论上对应的印刷版本是唯一的。价格字段用 decimal(10,2) 而不是 float/double这点太重要了浮点数在比较和计算时会有精度问题做图书金额统计时很容易出现 0.30000000000000004 这种尴尬数据。借阅表borrow_record需要关联 book_id 和 reader_id同时记录 borrow_time、due_time、return_time、status。这里有一个经验之谈不要把借阅状态冗余在图书表里否则会出现数据不一致的问题。比如你直接在 book 表里加一个 status 字段表示被借出那同一本书被两个人同时借阅时第二个人会被错误地允许借出。正确做法是每一条借阅记录一个 status 字段查询某本书是否可借时去 borrow_record 里查有没有未归还的记录。分类表category和读者表reader相对简单但分类表建议预留 parent_id 字段方便以后扩展二级分类。2.2 建表语句与索引优化要点核心建表 SQL 大概是这样的思路CREATE TABLE book ( id bigint(20) NOT NULL AUTO_INCREMENT, book_name varchar(100) NOT NULL COMMENT 书名, author varchar(100) DEFAULT NULL COMMENT 作者, isbn varchar(20) NOT NULL COMMENT ISBN, publisher varchar(100) DEFAULT NULL COMMENT 出版社, publish_date date DEFAULT NULL COMMENT 出版日期, price decimal(10,2) DEFAULT NULL COMMENT 价格, stock int(11) DEFAULT 0 COMMENT 库存, category_id bigint(20) DEFAULT NULL COMMENT 分类ID, cover_url varchar(255) DEFAULT NULL COMMENT 封面地址, description text COMMENT 简介, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_isbn (isbn), KEY idx_category (category_id), KEY idx_book_name (book_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表;注意几个细节第一字符集一定要用 utf8mb4不是 utf8因为 utf8 在 MySQL 里不兼容四字节的 Emoji 表情和某些生僻字第二InnoDB 引擎是必须的它支持事务和外键第三检索频繁的字段要加索引但不要对 text 类型的 description 字段做索引那会白白增加写入开销。关于书名搜索网上很多教程教你直接WHERE book_name LIKE %关键字%数据量小的时候没问题但当数据量超过几万条时这个查询会走全表扫描响应时间明显变慢。对于中小型图书系统合理的优化是先给 book_name 加普通索引然后业务层控制搜索频率必要时候可以考虑在 SQL 里限制扫描范围——比如配合分类过滤条件先缩小候选集。真要追求搜索体验后续再引入全文索引或者 Elasticsearch不是一个练手项目该操心的。2.3 事务与数据一致性借书这个动作必然涉及两步操作插入一条借阅记录 减少图书库存。这两步必须放到同一个事务里否则可能出现记录插入成功但库存没减或者反过来。Transactional(rollbackFor Exception.class) public void borrowBook(Long bookId, Long readerId) { // 1. 校验读者状态 Reader reader readerMapper.selectById(readerId); if (reader null || reader.getStatus() 1) { throw new BusinessException(读者不存在或已禁用); } // 2. 校验图书库存 Book book bookMapper.selectByIdForUpdate(bookId); if (book null || book.getStock() 0) { throw new BusinessException(图书库存不足); } // 3. 创建借阅记录 BorrowRecord record new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowTime(new Date()); record.setDueTime(DateUtil.addDays(new Date(), 30)); record.setStatus(0); borrowRecordMapper.insert(record); // 4. 扣减库存 bookMapper.decreaseStock(bookId); }这里我用了selectByIdForUpdate意思是在更新之前锁定这行记录防止两个用户同时借同一本书的最后库存出现超卖。这是乐观锁之外的一种策略适用于库存并发冲突较少的场景。事务注解里的 rollbackFor Exception.class 是必须的否则默认情况下 Spring 只在 RuntimeException 时回滚你的自定义异常可能不回滚到时候数据错乱你都不知道错在哪里。3. SpringBoot 后端核心实现从接口设计到业务层3.1 接口设计与 RESTful 规范图书管理系统的后端接口设计直接参考 RESTful 风格但也不必死守教条中小型系统实用优先。我习惯的分层思路是这样的功能方法路径说明分页查询图书GET/api/books?page1size10keywordxx支持按书名、ISBN 模糊搜索查询图书详情GET/api/books/{id}返回完整图书信息新增图书POST/api/books请求体为 JSON修改图书PUT/api/books/{id}全量更新删除图书DELETE/api/books/{id}逻辑删除或物理删除借阅图书POST/api/borrow/borrow参数bookId, readerId归还图书POST/api/borrow/return参数recordId分页查询借阅记录GET/api/borrow/records支持按读者、状态筛选分页参数统一叫 page 和 size返回结构统一用 Result 包装这是我强烈建议的做法。public class ResultT { private Integer code; // 200 成功500 失败 private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } }你会发现这套返回结构跟 Element Plus 的前端对接非常顺畅前端判断res.code 200来决定是否弹出成功提示。统一返回结构还有一个好处——出问题排查方便接口返回的 code 一眼就能看出是参数错误还是系统异常。3.2 Controller 层参数校验不能省很多源码项目里 Controller 层写得极其敷衍前端传什么用什么结果数据脏到没法看。以新增图书为例至少要校验必填字段PostMapping public Result? addBook(RequestBody Valid BookAddRequest request) { bookService.addBook(request); return Result.success(null); }配合 DTO 里的校验注解public class BookAddRequest { NotBlank(message 书名不能为空) private String bookName; NotBlank(message ISBN不能为空) Pattern(regexp ^[0-9]{13}$, message ISBN格式不正确) private String isbn; NotNull(message 价格不能为空) DecimalMin(value 0.01, message 价格必须大于0) private BigDecimal price; }这是JSR-303 参数校验的标准用法把校验规则放在 DTO 对象上Controller 里加一个Valid注解就会自动生效。再配合全局异常处理器校验失败时返回统一格式的报错信息前端拿到错误消息直接弹提示比自己在 Controller 里 if-else 判断要优雅得多。这里补充一句DTO 和 Entity 一定要分离不要直接把实体类当成接口入参否则以后加校验规则、改表结构都会牵连到接口层。3.3 Service 层业务规则放这里别放 Controller图书系统的业务逻辑虽然简单但仍然要遵守Controller 只做参数接收、Service 做业务判断的分层原则。最常见的错误是业务判断散落在 Controller 里导致多个接口之间的规则不一致。比如删除图书这个操作不能简单地执行DELETE FROM book WHERE id ?。实际业务里必须先去 borrow_record 表查询这本书有没有未归还的借阅记录如果有应该禁止删除并提示该书仍有未归还记录。这段判断逻辑必须在 Service 层写而且要用事务保护确保查询借阅记录和删除图书两步要么同时成功要么同时回滚。再比如还书操作要判断是否超期超期的话建议在 return_time 写入时间的同时把超期天数记录下来。这些规则看着简单但一旦后期要接入罚款功能你只需要在原有代码上扩展而不是重构这种面向演进的设计才是最值钱的。3.4 拦截器一个注解搞定登录鉴权图书管理系统一般不需要复杂的 Spring Security 权限模型那又是一个庞大的学习成本用拦截器 自定义注解做简单的登录校验就够了。核心思路是用户在登录接口成功后后端生成一个 token 返回前端把 token 存在 localStorage 里每次请求带上 Authorization 头拦截器拦截所有非登录接口校验 token 是否存在且有效。Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BusinessException(未登录或登录已过期); } // 校验 token比如从 Redis 或 JWT 中解析用户 return true; } }如果你想控制哪些接口需要登录、哪些接口放行可以在 WebConfig 中配置Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/user/login); } }第一次做拦截器最容易踩的坑是静态资源也被拦截前端部署的时候会通过/static/**加载 js/css 文件这些路径必须放进 excludePathPatterns。这个坑我在实战中至少踩过三次每次都能排查半小时才发现是拦截器把静态资源给挡住了。4. Vue 前端从零搭建到接口联调4.1 Vite Vue3 环境搭建前端部分我强烈建议使用 Vue3 Vite Element Plus 这条最新组合Vite 的启动速度和热更新体验比老旧的 VueCLI Webpack 好太多了。环境搭建大致三步走先安装 Node.js建议 18 或 20 LTS 版本太老的版本跑不动 Vite然后用 npm 初始化 Vite 项目最后安装 Vue Router、Pinia 和 Element Plus。# 创建项目 npm create vitelatest book-manage-front -- --template vue # 进入项目目录 cd book-manage-front # 安装依赖 npm install # 安装路由和状态管理 npm install vue-router4 pinia # 安装 Element Plus 和图标库 npm install element-plus element-plus/icons-vue # 启动开发服务器 npm run dev这个过程中最常遇到的问题就是 npm 安装依赖卡住或报 ERESOLVE 错误绝大多数情况是网络问题或者 node 版本问题。我建议先把 npm 源切到国内镜像npm config set registry https://registry.npmmirror.com如果还报错就删除 node_modules 和 package-lock.json 重新安装。一个容易被忽视的细节是 Vite 的开发代理配置。如果后端接口地址是 localhost:8080前端开发服务默认是 localhost:5173直接请求会跨域。最优雅的解决方式是在 vite.config.js 里配置代理export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端代码里统一请求/api/books开发时由 Vite 代理转发到后端打包部署到生产环境时再用 Nginx 或者后端静态代理做转发代码完全不用改。后端部分如果 Vite 打包后的 dist 目录直接丢进 SpringBoot 的 resources/staticSpringBoot 会自动托管这些静态文件但需要注意路由模式——Vue Router 的 history 模式刷新页面会 404需要在后端配置 fallback 到 index.html。4.2 前端页面结构与组件拆分图书管理系统的前端页面不需要太复杂按功能拆成四个核心页面就够了图书列表、图书编辑、分类管理、借阅记录。这里分享一个我自己的组件设计习惯每个页面对应一个 view 目录下的文件每个可复用的弹窗、表单、卡片抽象成 components 目录里的独立组件。图书列表页是主页面承担查询、分页、操作入口三个职能template div classbook-list el-card el-form inline el-form-item label关键字 el-input v-modelqueryParam.keyword placeholder书名/作者/ISBN clearable stylewidth: 240px / /el-form-item el-form-item el-button typeprimary clickhandleSearch查询/el-button el-button clickhandleReset重置/el-button el-button typesuccess clickopenAddDialog新增图书/el-button /el-form-item /el-form /el-card !-- 表格区域 -- el-table :datatableData v-loadingloading el-table-column propbookName label书名 min-width180 / el-table-column propauthor label作者 width120 / el-table-column propisbn labelISBN width160 / el-table-column propprice label价格 width80 / el-table-column propstock label库存 width80 / el-table-column label操作 width200 fixedright template #default{ row } el-button sizesmall clickhandleEdit(row)编辑/el-button el-button sizesmall typedanger clickhandleDelete(row)删除/el-button el-button sizesmall typewarning clickhandleBorrow(row)借阅/el-button /template /el-table-column /el-table !-- 分页组件 -- el-pagination v-model:current-pagequeryParam.page v-model:page-sizequeryParam.size :totaltotal :page-sizes[10, 20, 50] layouttotal, sizes, prev, pager, next size-changeloadData current-changeloadData / /div /templateElement Plus 的好处在这一层体现得很明显——表格自带排序、分页自带页码逻辑、弹窗表单组件齐全前端核心代码量比 jsp jstl 那套老古董少了至少一半。借阅和归还的操作我建议用 dialog 弹窗实现里面嵌一个表单提交时调用对应的 API。弹窗组件里要注意表单校验规则和提交后刷新列表的逻辑。4.3 Axios 封装与统一错误处理Vue 项目里直接在每个页面写 axios 请求的代码最终一定是一团乱麻。我建议第一天就把 axios 实例封装好统一处理 baseURL、请求头、响应拦截、错误提示。// request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动带上 token 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.data }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default request这一步封装的意义不在于省代码而在于统一错误处理。比如后端返回 500 或者登录过期前端不用在每个页面单独写 error 回调全局统一拦截弹消息。很多同学写完这个系统去面试被问到你怎么处理 token 过期这个问题时答不上来——拦截器就是标准答案。5. MyBatis 持久层从注解到 XML 的进化之路5.1 Mapper 接口与 XML 的配合MyBatis 有两种使用姿势全注解方式和 XML 方式。我个人的建议是简单 SQL 用注解复杂动态 SQL 用 XML。一个系统里两种方式共存没有任何问题MyBatis 内置了这种混合支持。先看一个简单的查询注解Select(SELECT * FROM book WHERE id #{id}) Book selectById(Long id);再看一个复杂的动态分页查询这种情况我会用 XML。图书列表页的搜索条件是可选的关键字 可选的分类搜索的关键字同时匹配书名、作者、ISBN 三个字段。SQL 这样写select idpageQuery resultTypecom.example.entity.Book SELECT * FROM book where if testkeyword ! null and keyword ! AND (book_name LIKE CONCAT(%, #{keyword}, %) OR author LIKE CONCAT(%, #{keyword}, %) OR isbn LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null AND category_id #{categoryId} /if /where ORDER BY create_time DESC LIMIT #{offset}, #{size} /select这里有两个细节值得细说。第一个细节是为什么用 CONCAT 而不是直接用${keyword}拼接。很多初学者会写LIKE %${keyword}%这会造成 SQL 注入漏洞用户只要传一个 OR 11 --就能查出所有数据。#{keyword} 是预编译参数MyBatis 会使用 JDBC 的 PreparedStatement传进去的值会被当作字符串字面量不会改变 SQL 语义。这是面试必问的 #{} 和 ${} 区别实际编码中永远用 #{}除非你在动态拼接表名或排序列名这种极端场景。第二个细节是 LIMIT 偏移量的计算。前端传 page1、size10后端不能直接把这个参数拼到 SQL 里而是要转换成 offset。SQL 里的 offset (page - 1) * size。这段逻辑可以写在 Service 层也经常有人用 PageHelper 这种分页插件自动处理前者更直观易懂适合学习阶段后者让代码更简洁适合生产环境快速开发。5.2 MyBatis 一级缓存和二级缓存用之前先搞清楚MyBatis 的缓存机制是面试高频点也是实际开发容易踩坑的地方。一级缓存是 SqlSession 级别的默认开启。简单理解就是同一个 SqlSession 内执行两次相同的查询第二次直接走缓存不发 SQL。但注意一级缓存的生命周期极短每次关联 Spring 的事务或方法调用后 SqlSession 就关闭了缓存随即失效所以它对性能的贡献微乎其微。只有在同一个事务里执行两次一模一样的查询时才走得上命中。二级缓存是 namespace 级别的跨 SqlSession 共享默认关闭。如果你决定开启二级缓存有一个必须注意的坑二级缓存只适合查询频率极高、更新频率极低的表。图书分类表还算适合但图书表如果库存频繁变动开启二级缓存极有可能导致数据不一致——用户在借书后库存已经扣减但另一个用户查到的还是缓存里的旧值。所以在图书管理系统里我建议直接关闭二级缓存靠 MySQL 自身的能力来支撑这个量级的查询就足够。如果是性能敏感的项目更推荐用独立的 Redis 做缓存这样缓存的数据一致性由你的业务代码显式控制比 MyBatis 二级缓存好排查得多。5.3 多表关联查询怎么写借阅记录页面需要显示书名和读者名但 borrow_record 表里只有 book_id 和 reader_id。解决方案有两种一是写一个联合查询的 VO把关联表的信息一次性查出来二是先查 borrow_record 列表再根据 book_id 批量查图书表做字段填充。方案一 SQL 直观、一次请求搞定方案二虽然多一次查询但代码结构清晰。数据量大时方案二反而更优因为避免了 JOIN 操作在大表上的性能损耗。select idselectBorrowRecords resultTypecom.example.vo.BorrowRecordVO SELECT r.id, r.book_id, r.reader_id, r.borrow_time, r.due_time, r.return_time, b.book_name, b.author, rd.name AS reader_name, rd.phone AS reader_phone FROM borrow_record r LEFT JOIN book b ON r.book_id b.id LEFT JOIN reader rd ON r.reader_id rd.id where if teststatus ! null AND r.status #{status} /if /where ORDER BY r.borrow_time DESC LIMIT #{offset}, #{size} /select这里必须用 LEFT JOIN左连接不能默认用 INNER JOIN。因为关联表没数据的场景太常见了——比如某条借阅记录对应的书被逻辑删除了或者读者被删除了如果用 INNER JOIN这条记录就从结果里消失了前端会看到数据莫名变少。5.4 MyBatis 配置打印与 SQL 优化在本地开发环境我强烈建议打开 MyBatis 的 SQL 日志每个 SQL 语句执行了多久、传了什么参数一目了然。配置方式在 application.yml 里mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity除此之外还有两个配置建议配上map-underscore-to-camel-case: true这样数据库的下划线字段名能自动映射到 Java 的驼峰属性不用每个字段手写 resultMapmapper-locations不要省略这是 MyBatis 找到 XML 文件的关键。遇到过最多的情况是 mapper-locations 没配置启动时直接报 Invalid bound statement (not found)你花半天检查 Mapper 接口也没用问题就出在 XML 文件没被扫描到。SQL 优化方面根据我多年排查性能问题的经验图书系统最容易出现的慢查询场景就三个第一没有索引的 LIKE 全表扫描第二数据量大时的深分页比如 LIMIT 100000, 20 这种第三在循环里调用单条 SQL 造成 N1 问题。N1 问题的典型例子是先用一条 SQL 查出 20 条图书记录然后循环 20 次每次根据 category_id 查一次分类名结果一共执行 21 条 SQL。解决方式很简单先查图书时把需要的分类 ID 集合一次性查出来然后用WHERE id IN (...)批量查分类代码里用 Map 做关联查询次数从 21 次降为 2 次。这种处理方式在系统数据量到了万级之后会表现出非常明显的性能差异。6. 环境搭建与部署实操从零到能跑通6.1 本地开发环境准备清单在启动项目之前先把环境准备齐全不然浪费在环境问题上的时间往往比写代码还多。完整清单如下组件版本建议备注JDK8 或 11SpringBoot 3.x 需要 17SpringBoot 版本决定了 Java 版本先确定版本再选 JDKMaven3.6依赖管理MySQL5.7 或 8.0我推荐 8.08.0 默认字符集就是 utf8mb4性能也有提升Node.js18 或 20一定要 LTS 版本IDEIntelliJ IDEA前后端用同一个 IDE 管理多模块更方便MySQL 的安装这里多说一句Windows 10/11 环境直接下载官方 msi 安装包即可安装过程中如果碰到缺少 Visual C Redistributable的报错去微软官网下载对应运行库装好就能解决。Linux 服务器上安装可以用 rpm 包或者 tar 包装完记得执行mysql_secure_installation做安全初始化。6.2 SpringBoot 配置多环境一个成熟的项目必须区分开发环境和生产环境SpringBoot 的多 profile 机制是标准做法# application.yml spring: profiles: active: dev# application-dev.yml spring: datasource: url: jdbc:mysql://localhost:3306/book_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4 username: root password: root123# application-prod.yml spring: datasource: url: jdbc:mysql://生产IP:3306/book_db?useSSLtrueserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4 username: prod_user password: ${MYSQL_PASSWORD} # 通过环境变量注入不要在文件里写死注意几个配置细节serverTimezoneAsia/Shanghai必须配置否则 MySQL 8.0 和 Java 之间的时区不一致会导致时间数据读取差 8 小时characterEncodingutf8mb4保证中文字符正确入库生产环境的密码通过系统环境变量${MYSQL_PASSWORD}注入不要把真实密码提交到 Git 仓库。这也是面试时能拉开差距的细节简历上写了 SpringBoot可能被问到生产环境配置管理。6.3 项目前后端联调与打包部署前端的开发服务器和后端 API 联调上面已经讲了通过 Vite 代理解决。真正要上线时先执行npm run buildVite 默认输出到 dist 目录然后把 dist 里的内容拷贝到 SpringBoot 的src/main/resources/static目录下打包时一并打进 jar 包。如果你用 Vue Router 的 history 模式会出现一个经典问题访问/books这种前端路由路径后端没有对应的接口SpringBoot 内部直接返回 404。解法是写一个 FallbackController 把所有非 api 路径的请求重定向到 index.htmlController public class PageForwardController { RequestMapping(value {/, /books/**, /borrow/**, /categories/**}) public String forward() { return forward:/index.html; } }当然更专业的方式是用 Nginx 做反向代理由 Nginx 托管前端静态文件把/api请求转发到后端 jar 端口。两种方式各有利弊打成单一 jar 包部署简单一键启动用 Nginx 分离部署前后端更容易扩展前端的 CDN 加速、接口的负载均衡都能做。练手项目我建议先走单一 jar 包部署把整个流程打通之后再研究 Nginx 部署方案——循序渐进比一步到位更符合真实学习曲线。6.4 一个常见部署坑端口冲突有一次我把前端打包放进 SpringBoot 后启动时一直报Port 8080 was already in use。排查下来是 Windows 系统的某个进程占用了 8080 端口命令行执行netstat -ano | findstr 8080找到 PID 后再在任务管理器里结束对应进程就行。服务器上遇到过的问题是防火墙阻止了外部访问 8080Linux 上要执行firewall-cmd --add-port8080/tcp或者在安全组里放行端口。这些问题看着琐碎但实际项目里遇到的概率非常高提前知道排查思路能省很多时间。7. 常见问题与排查技巧实录7.1 启动报错速查表错误现象根本原因解决办法Invalid bound statement (not found)Mapper XML 没被扫描检查 application.yml 的 mapper-locations 路径是否匹配Access denied for user root数据库密码不对或远程未授权本地检查密码远程执行 GRANT 授权命令Table doesnt exist数据库没建表导入项目提供的 SQL 脚本Unknown character set: utf8mb4MySQL 版本低于 5.5.3升级 MySQL或改字符集为 utf8Port 8080 was already in use端口被占用换端口或 kill 占用进程Failed to configure a DataSourcespring.datasource 配置缺失检查配置文件有没有写数据库连接参数中文乱码连接串没加 characterEncodingUTF-8在 jdbc url 加 characterEncodingutf8mb4npm install 报 ERESOLVE依赖树冲突删除 package-lock.json 后重装或用 npm install --legacy-peer-deps这里讲一下我印象最深的错误排查经过。有一次部署到云服务器接口返回的数据中文全是乱码前后端和数据库都检查了一遍最后发现是服务器上的 MySQL 默认字符集是 latin1建表的时候没有指定 CHARSETutf8mb4导致数据写入时就已经是乱的了。解决方案是把表和字段的字符集全部改一遍同时对连接串加上characterEncodingutf8mb4。这个案例想说明的是出现中文乱码要顺着客户端连接 → 数据传输 → 数据库存储 → 数据展示四个环节逐一排查不能只盯着前端代码看。7.2 Vue 前端常见问题前端最常遇到的问题集中在打包和路由两个方向。打包报错unexpected token ?大概率是语法兼容性问题——某个依赖用了可选链操作符?.或空值合并运算符??而打包目标环境不支持。解决方案是调整 Vite 的 build.target 为es2015或者调整需要兼容的浏览器版本。在 IDEA 或 VS Code 里写 Vue 代码经常遇到 vue-router 相关的 import 文件报错除了检查路径大小写还要确认 vue-router 版本是 4.xVue3 配套如果是 3.x 大概率是网上老教程的代码Vue 3 项目里用 vue-router 3 必炸。7.3 面试级问题项目细节怎么答既然标题带了源码我应该提醒一下如果你要把这个项目放进简历或者作为面试素材有几个问题一定要提前准备好答案。面试官大概率会问这个项目的数据库表是怎么设计的为什么这么设计 你要能说出每张表的核心字段、外键关系、索引设计和为什么选择物理外键还是逻辑外键。推荐在图书表和借阅表之间用逻辑外键不建 FOREIGN KEY 约束因为物理外键在插入、删除时会有额外的检查开销遇到大数据量的批量操作会有性能问题而且一旦线上要清理数据物理外键会拖后腿。中小团队基本都用逻辑外键。第二个大概率问题如果图书借阅量特别大你怎么优化 这个问题考察的是系统设计能力。我的推荐路线是第一步给查询加索引第二步引入 Redis 缓存热门图书信息第三步把借阅操作从同步改成异步消息队列——当然这是图纸级的方案实际练手项目不用真的引入 RabbitMQ但你要能把这套思路讲明白说明你对性能优化有完整的认知框架。8. 写在最后几个我踩过最深的坑最后分享三个实际操作中最容易伤到人的坑都不是能从官方文档直接看出来的知识。第一开发期间一定要保持前端代理和后端端口的一致性。我试过多次把 Vite 的代理端口从 8080 改成 8081但后端 IDEA 里的启动配置没有同步改结果前端一直访问 8081 端口报 404排查到怀疑人生才发现是代理配置指向了错误的端口。建议把前后端端口统一写在一个约定文档里或者直接用环境变量来管理。第二MyBatis 的 XML 文件路径大小写不能错。Windows 不区分大小写但 Linux 严格区分。你本地跑没问题打包上线在 Linux 上就报错找不到 mapper XML这就是典型的本地没毛病上服务器翻车。个人经验是所有 XML 文件名和 Mapper 接口名保持完全一致目录统一用 classpath:mapper/ 这种相对路径不要用绝对路径。第三不要迷信一键运行源码。电商源码、开源项目下载下来直接能跑的概率很低因为数据库初始化脚本、配置文件的路径、依赖版本都可能跟你的环境不匹配。真正熟练的做法是看到源码之后先读 README、看 SQL 脚本、看配置文件理解项目的启动链路后再执行而不是双击 start 脚本就完事。这个习惯决定你是合格的工程师还是一辈子只会复制粘贴的脚本小子。图书管理系统是我做过最简单、但也是最完整的练手项目之一。它麻雀虽小五脏俱全——有数据库设计、有接口设计、有前端交互、有部署配置。好好把一个 CRUD 系统吃透远比浮光掠影地刷十个项目有用。后续如果你想扩展可以考虑加一个图书封面上传用 MinIO 或者 OSS、加一个借阅排行榜统计用定时任务 Redis 的 ZSet、再加一个简单的管理员登录认证用 JWT。每一步扩展都是在巩固基础之上的能力跃迁。就这样祝顺利跑通有问题欢迎交流。
返回列表