ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue图书馆管理系统:从数据库设计到服务器部署全解析

SpringBoot+Vue图书馆管理系统:从数据库设计到服务器部署全解析 要说适合拿来练手 JavaWeb 前后端分离的项目图书馆管理系统绝对排得上前三。它正好是 SpringBoot Vue 这套技术栈的最佳练兵场既覆盖了登录鉴权、增删改查、借阅流转这些经典业务又不像电商系统那样动辄牵扯支付、库存、秒杀。这篇文章就是围绕这套图书馆管理系统的源码、部署文档和代码讲解展开的我会把从数据库设计、后端接口、前端页面到服务器上线的完整链路拆开讲一遍同时解释那些“为什么这么设计”的底层逻辑。适合正在做毕业设计、以及想从传统 Servlet/JSP 过渡到 SpringBoot Vue 的开发者参考。1. 先想清楚系统边界图书馆管理系统的三个角色与核心业务很多同学拿到“图书馆管理系统”这个题目第一反应就是建表、写增删改查结果做到一半发现角色权限乱了、还书流程对不上、逾期费不知道在哪算。所以我建议动手敲代码之前先花半天把业务边界画出来。这部分虽然不出成果但直接影响表结构和接口设计。1.1 角色设计管理员、图书管理员、读者各管哪一块在系统里我划分了三类角色而不是直接叫“管理员”和“用户”两个角色原因是图书馆场景天然存在“业务操作者”和“普通使用者”的区分。系统管理员管用户读者、管员工账号图书管理员、查所有操作日志、看统计报表。这套系统里最高权限。图书管理员处理图书录入、修改库存、办理借书/还书、处理逾期记录。是日常业务的主要操作者。读者检索图书、查看馆藏数量、借书、还书、查看个人借阅历史。这个划分直接决定后端的接口粒度。比如“借书”这个动作就不是读者自己点的而是图书管理员在线下扫描或录入后发起的操作读者侧只能看到自己的借阅记录。如果一开始没分清楚很容易把借书接口设计成“读者自助借书”后续还书、续借、逾期判断全都会跟着逻辑混乱。1.2 核心业务流程借书、还书、逾期三个闭环图书馆管理系统最核心的流程不是“图书 CRUD”而是借还流转。我把流程拆成三句话后面所有状态字段、库存扣减都是围绕这三句话展开。借书读者可借数校验 - 图书可借库存校验 - 创建借阅记录 - 可借数量减一 - 记录借出时间和应还时间。还书更新借阅记录 - 可借数量加一 - 记录实际归还时间 - 判断是否逾期逾期则生成逾期记录/费用。续借在未逾期、借阅次数未超过上限的前提下延长应还时间。这里有一个细节值得展开图书表里维护的“可借数量”并不是靠总数 - 借出数实时计算出来的而是每次借还操作时手工维护的冗余字段。这么做的好处是查询列表时不用join一堆借阅记录去 count性能更好代价是必须保证借还接口里库存字段的增减在同一事务内完成。否则一旦出现并发借同一本书的场景库存就错了。这个设计取舍我在文档里专门说明过算是代码讲解时的高频点。1.3 项目文档的成套写法需求说明、设计说明、部署说明缺一不可标题里提到“源码文档部署文档代码讲解”正好借这个机会说一下文档是怎么组织的。很多新手把文档当成凑字数的东西写实际上后端项目里的文档价值比代码还要大因为代码只告诉你怎么实现文档告诉你为什么这么实现。这套系统的文档我按照四个部分整理需求说明、数据库设计、接口文档、部署文档。需求说明里包括角色表格和业务流程图数据库设计除了建表 SQL还要写明每个字段的含义和状态枚举接口文档写清楚请求参数、响应结构和异常码部署文档则覆盖本机跑通和服务器上线两套流程。后面讲到部署的时候我会把部署文档里最关键的几个配置项单独拿出来分析。2. 数据库设计是根核心表结构与借阅状态状态机数据库对于这种管理系统来说就是地基。我见过不少人把用户表、图书表、借阅表三张表建完就开始写代码后续不停加字段、改类型最后布线一团乱。规范化早做后期会省很多事。2.1 核心表清单与字段说明在这个项目里我最终保留了 8 张核心表用户表、角色表、图书分类表、图书表、借阅记录表、公告表、操作日志表、参数配置表。下面挑三张最核心的表看字段设计。表名核心字段说明userid, username, password, real_name, role_id, phone, status, create_timestatus 控制启用/禁用禁用后不能登录bookid, isbn, title, author, publisher, category_id, total_count, available_count, cover, location, statusavailable_count 是可借库存status 表示上下架borrow_recordid, user_id, book_id, borrow_time, due_time, return_time, status, renew_countstatus 贯穿借还流程renew_count 限制续借次数图书表的isbn字段建议单独加唯一索引因为同一本书在系统里只能存在一条馆藏记录location字段记录书架位置比如A区-3排-2层封面图cover存的是图片路径不上传时使用默认封面。封面图上传这一步我当时用的是本地路径存储对毕业设计完全够了。如果你的需求是图片量很大或者将来多台机器部署可以再考虑把 MinIO 加入到 SpringBoot 里做对象存储这是后话但不影响现有表结构。2.2 借阅记录的 status 状态为什么逾期不建议单独用一个状态借阅记录表的status字段很容易被设计成0-借出、1-已还、2-逾期。但仔细想想会发现“逾期”不是一种独立状态而是“借出中”的一种属性。如果一个读者逾期了三天才来还书归还之后这条记录到底是“已还”还是“逾期”如果用独立状态归还时又会陷入改来改去的麻烦。所以我的方案是status只保存两个终态和一个中间态——0-借出中、1-已还、2-已取消/作废。是否逾期通过return_time IS NULL AND due_time NOW()这个查询条件实时判断。这样统计逾期记录时只需一条 SQL而不会产生状态混乱。这也解释了为什么我在文档里反复强调能通过计算得到的业务状态尽量不要用字段去硬存。实际的前端“借阅历史”列表里逾期状态是由后端在返回结果时根据due_time和return_time动态计算后附加给前端的不需要让前端自己去比较时间。2.3 数据库初始化脚本的性价比字符集、索引、时区一次配对部署文档里最容易被 Copy 过去就出错的就是数据库初始化脚本。我给的 SQL 脚本里做了三件事建库时指定utf8mb4字符集避免中文乱码给book.title、book.isbn、borrow_record.user_id加上索引外键只做逻辑关联不建物理外键。关于外键很多科班课程强调外键约束但真实生产项目里外键是能不用就不用的因为会拖累写入性能并且让分库分表变得困难。这个系统里“用户-借阅记录”“图书-借阅记录”都是靠代码层保证一致性的。脚本最后还写了SET time_zone 8:00配合 JDBC 连接串里的serverTimezoneAsia/Shanghai确保due_time这类时间字段不会因为 MySQL 时区差个八小时而显示错乱。3. 后端代码分层与鉴权细节不要把所有逻辑都堆在 Controller后端部分如果你照着 MVC 字面意思把代码全堆在 Controller 里项目一复杂就完了。我在代码讲解部分反复强调Controller 只做参数接收和结果封装业务逻辑必须下沉三层结构要明确。3.1 Controller、Service、Mapper 的分工与一个借书接口的完整代码Project 内部的分层结构是controller-service-mapper。实体类entity、数据传输对象dto、视图对象vo单独放。不建议在 Service 层直接返回 Entity 给前端因为很多内部字段比如密码哈希是不该暴露给前端的。下面这段是借书接口最核心的 Service 层逻辑我借它说明事务与事务失效问题。Transactional(rollbackFor Exception.class) public void borrowBook(BorrowDTO dto) { // 1. 校验读者是否存在且状态正常 UserEntity user userMapper.selectById(dto.getUserId()); if (user null || user.getStatus() ! 1) { throw new BusinessException(读者不存在或已禁用); } // 2. 校验图书库存使用行级锁避免超卖 BookEntity book bookMapper.selectByIdForUpdate(dto.getBookId()); if (book null || book.getTotalCount() 0) { throw new BusinessException(图书不存在或馆藏为零); } if (book.getAvailableCount() 0) { throw new BusinessException(当前无可借库存); } // 3. 创建借阅记录默认借期30天 BorrowRecordEntity record new BorrowRecordEntity(); record.setUserId(dto.getUserId()); record.setBookId(dto.getBookId()); record.setBorrowTime(LocalDateTime.now()); record.setDueTime(LocalDateTime.now().plusDays(30)); record.setStatus(0); borrowRecordMapper.insert(record); // 4. 扣减可借库存 book.setAvailableCount(book.getAvailableCount() - 1); bookMapper.updateById(book); }注意两个点第一Transactional(rollbackFor Exception.class)必须写rollbackFor默认情况下 RuntimeException 才会回滚而自定义的BusinessException如果继承了 Exception 但没写这个参数事务是不会回滚的。第二库存扣减的selectByIdForUpdate用了行锁这是为了两个管理员同时给同一个人借同一本书时不出错。这个锁粒度很细只锁一行对这个小系统足够了。3.2 JWT 登录鉴权与拦截器配置白名单要抓好系统采用 JWT 做登录态维护。登录成功后后端签发一个 token前端每次请求都放在Authorization请求头里后端用一个拦截器统一校验。工程上你还需要注意几点。登录接口、静态资源、健康检查这些接口要加入白名单否则会被拦截器拦死。拦截器校验通过后把当前登录用户的 ID 塞进ThreadLocal这样业务代码里随时能拿到“操作人”是谁AOP 日志也要用这个信息。跨域配置本机开发时前端在 5173 端口、后端在 8080 端口如果不配置 CORS浏览器直接报跨域错误。密码不能明文存储用 BCrypt 加密存储。很多同学的毕设直接存明文答辩时被问一次就露馅了。这段拦截器配置代码不算长但属于“部署文档里必须说明白”的内容因为很多同学遇到 401 或者跨域问题都是在这个环节踩坑。3.3 图书检索的实现列表、分类筛选、关键词搜索该怎么配合图书列表和检索是查询最频繁的接口。我的方案是GET /api/book/list?page1size10参数支持keyword、categoryId、publisher。MP 的Page对象分页Mapper XML 里写动态 SQL。select idselectBookPage resultTypecom.example.entity.BookEntity SELECT td.* FROM book td where if testkeyword ! null and keyword ! AND (td.title LIKE CONCAT(%, #{keyword}, %) OR td.author LIKE CONCAT(%, #{keyword}, %) OR td.isbn LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null AND td.category_id #{categoryId} /if /where ORDER BY td.create_time DESC /selectLIKE %关键字%在大数据量下肯定不走索引但对于图书馆这种几千条数据的小系统完全够用不用过分优化。如果你非要优化可以考虑给 title 建前缀索引或者引入全文索引但那个提升在小数据量下感知不明显。在代码讲解时我会如实说明这个查询方案是“正确、简单、够用”的不是“高性能”的随后给出量级判断让读者知道什么时候该换方案。3.4 MyBatis-Plus 的取舍CRUD 用玩具统计用 SQL这个项目用了 MyBatis-Plus 的BaseMapper因为单表的增删改查真的没必要写 XML。但统计类场景我仍然建议写原生 SQL比如“按月份统计借阅量”这种报表 SQLSELECT DATE_FORMAT(borrow_time, %Y-%m) AS month, COUNT(*) AS borrowCount FROM borrow_record WHERE borrow_time #{startTime} GROUP BY month ORDER BY month这就是典型的“复杂 SQL 交给 Mapper简单 CRUD 交给 MP”的做法。如果你的毕业设计想拿高分可以在答辩时主动展示这条 SQL并解释为什么不用 MP 的Wrapper拼因为 group by 这种聚合操作用 QueryWrapper 写出来可读性差且效率不如原生 SQL。4. 前端 Vue 工程化路由、请求封装、组件复用一个都不能少前端的复杂度和后端并不同步。图书馆管理系统的页面数量大概是登录页、读者端列表页、读者端详情页、管理端图书管理页、借阅管理页、用户管理页、统计图表页、个人中心页。如果每个页面独立开发你会写大量重复代码所以关键是工程化协同。4.1 角色动态路由vue-router 与菜单的动态生成前端路由分为静态路由和动态路由。静态路由指的是登录页、404 页剩下的管理端页面根据角色动态注入。流程是这样的登录成功后后端返回当前用户的角色和权限标识前端拿到后根据路由表过滤出有权限的路由再通过router.addRoute()动态添加。// 登录成功后根据角色生成动态路由 const permissionRoutes filterRoutes(asyncRoutes, role); permissionRoutes.forEach(route { router.addRoute(route); });同时全局前置守卫里要做两件事判断是否已经登录有无 token判断当前访问路径是否在允许的路由中。刷新页面时动态路由会丢失所以还需要把路由重新计算一遍。这个点是前端面试题的高频考点在讲解项目时我会专门提一句“动态路由”的实现思路。路由模式我是用 history 模式的但注意 history 模式打包放到服务器后刷新子页面会出现 404。这个坑在下一节部署部分再展开解决办法也有两种。4.2 axios 拦截器统一注入 token 与统一处理错误码前端请求如果没有统一封装每个页面都自己写 fetch 的话代码会非常凌乱。我的项目里只封装了一个request.js文件导出get/post方法所有 api 都通过它调用。service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; }); service.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); } );这个封装做完以后所有页面里只需要关心业务数据不用每个接口都去判断code和msg。全局错误提示与统一 401 跳转都收拢在一处。维护起来非常舒服。4.3 分页、搜索表单、表格弹窗的组件复用读者列表、借阅记录、图书管理三个页面长得几乎一样顶部一个搜索栏中间一张表格底部一个分页器。我用 Vue 的插槽把搜索条件和操作列抽象出来做一个通用页面模板。核心是把分页逻辑放进一个组合式函数usePagination里路由进去调方法翻页也调方法page 和 size 自动带上。前端开发里面最容易被忽视的是“翻页后搜索条件丢失”。如果搜索函数和分页函数各自独立用户点第二页时条件就没了。我的做法是在一个函数里集中处理search()会把 page 重置为 1onPageChange()会带上当前表单的搜索条件重新请求。这个细节很小但页面交互体验差别很大。4.4 开发环境的跨域代理与状态管理开发环境是前端 Vite 的 5173 端口访问后端的 8080 端口跨域问题通过 Vite 的 proxy 配置解决server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这里需要提一下这种代理只是开发环境的事情打包之后前端页面由后端服务器提供不存在跨域问题。我还用 Pinia 做了全局用户状态存储登录后把用户名、角色拉到 store 里缓存菜单栏根据 store 里的角色渲染。页面刷新时 store 数据会丢所以还需要从 token 解析或者重新拉取用户信息。这个坑我在注释里标得很清楚。5. 部署全流程复盘从 IDEA 配置到服务器上线部署是“看着简单做起来全是坑”的环节。我复盘一下本地跑通、打包、服务器上线三个环节几乎涵盖了 JavaWeb 项目里最容易翻车的位置。5.1 本地环境配置顺序JDK 版本、Maven、IDEA 运行配置本地跑通项目的顺序不要乱。JDK 版本要匹配 SpringBoot 版本我用的是 SpringBoot 2.7 JDK 8这是兼容性最稳的组合如果你用 JDK 17尽量配 SpringBoot 3.x但 3.x 的javax包改成了jakarta老代码直接编译过不了。这种“版本太高反而更难搭环境”的现象在毕设里很常见我的建议是新手上手直接参考代码里的 pom 版本不要自己随便升版本。Maven 项目构建方法里我用的是阿里云镜像仓库这一步对国内环境非常关键mirror idaliyunmaven/id mirrorOfcentral/mirrorOf urlhttps://maven.aliyun.com/repository/central/url /mirrorIdea 运行 JavaWeb 项目配置新版 IDIEA 里直接创建 SpringBoot 运行配置。重点有两个Main class 要选对启动类VM options 里填-Dfile.encodingUTF-8防止控制台中文乱码。如果你在 IDEA 里配了服务器数据源却没成功连接优先排查数据库 URL 的时区参数和 mysql-connector-java 的版本。5.2 打包环节的三个坑前端打包、history 路由、SpringBoot 静态资源打包这块前端同学通常会在这里出问题。打包命令很简单但有几个前置条件# 后端打包 mvn clean package -DskipTests # 前端打包 npm run build前后端合一的部署方式是把前端dist目录里的文件复制到 SpringBoot 的src/main/resources/static目录下然后重新打包 jar。但这样做有两个坑一是每次前端改动都要重新打一次后端 jar比较繁琐二是如果前端用了 history 路由子页面刷新请求的是后端路由后端没有对应的 Controller就会 404。处理方式常用两种要么前端改回 hash 模式路由URL 变成/#/book要么后端加一个转发规则将非api的路径转发到index.html。我选择的是后者因为 hash 模式看起来不专业。SpringBoot 里用一段简单的 WebMvcConfigurer 就能解决这也是我在部署文档里写得很详细的一个点。5.3 服务器部署jar 包后台运行与进程管理服务器环境只需要 JDK 和 MySQL。把后端 jar 上传后我用的是nohup java -jar library-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod library.log 21 这句命令的意思是后台运行并把日志输出到 library.log。如果项目里配置了生产环境的application-prod.yml用--spring.profiles.activeprod切换避免把测试环境的配置带到线上。最后记得检查服务器的安全组/防火墙把 8080 端口放开否则外部访问不了。5.4 上线前的体检清单端口、日志、数据库备份这个清单是我每次上线前都必过一遍的端口是否被占用冲突则改server.port。数据库连接 URL 有没有对应到生产库账号密码是不是最小权限。上传文件目录是否有写的权限图书封面、头像。服务器时区与 MySQL 时区是否一致避免时间字段显示错乱。是否配置了spring.servlet.multipart.max-file-size否则上传大图会被拒绝。每个错误日志是否有落盘路径方便线上排查。数据库备份脚本是否已配置数据是不可再生资产这条优先级最高。这一整套流程走完项目才算真正“可交付”而不只是在 IDEA 里点个运行就完事。6. 代码讲解中的高分点自动装配、AOP 日志与代理机制如果你需要答辩或者给同事做分享不能只停留在“这个功能怎么实现”还要能解释“框架为什么能自动做好这些事”。下面这几点都是 JavaWeb 项目讲解中的高频考点也是能让讲解档次拉开差距的地方。6.1 SpringBoot 自动装配原理为什么加个依赖就能用SpringBoot 自动装配的核心是SpringBootApplication这个组合注解它里面包含了EnableAutoConfiguration。SpringBoot 启动时会加载META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件SpringBoot 2.7 之前的版本是spring.factories把里面声明的自动配置类全部读出来。但并不是每个配置类都会生效每个配置类上面都有一堆条件注解ConditionalOnClass(DataSource.class) ConditionalOnMissingBean(DataSource.class)如果 classpath 里有DataSource且用户没有手动定义框架就自动生成一个默认的数据源配置。这个机制回答了一个问题为什么我们加个spring-boot-starter-web依赖不用写任何 XML就能跑起一个 Web 项目。在这个图书馆管理系统里自动装配最典型的例子就是 MyBatis-Plus 的SqlSessionFactory不需要手动配置注册。6.2 AOP 记录操作日志一个面向审计需求的设计图书馆管理系统有操作审计需求谁在什么时间借了几本书谁修改了哪条图书信息。如果用硬编码在每个 Service 方法里写日志代码膨胀不说还容易漏。我用 AOP 在方法上打了一个OperationLog注解切面统一处理。Aspect Component public class OperationLogAspect { Around(annotation(operationLog)) public Object around(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable { Long startTime System.currentTimeMillis(); Object result joinPoint.proceed(); long costTime System.currentTimeMillis() - startTime; // 记录操作人、操作模块、操作内容、耗时写入operation_log表 return result; } }这个切面一次实现全项目复用而且是“非侵入式”的不影响原有业务逻辑。讲解这个点时能清楚体现你对 Spring 核心思想的理解。6.3 为什么 SpringBoot 默认使用 CGLIB 代理不是所有 Bean 都有接口Spring AOP 底层有两种代理方式JDK 动态代理要求目标类必须实现接口CGLIB 通过生成子类的方式代理类。SpringBoot 默认使用 CGLIB 而不是 JDK 动态代理原因主要有三点一是 SpringBoot 提倡面向类而非接口编程大量 Service 类直接就是实现类没有接口二是 CGLIB 代理不需要接口只要类非 final 都可以代理三是可以避免“有接口走 JDK、没接口走 CGLIB”这种混乱的双轨逻辑。Spring Boot 2.x 之后spring.aop.proxy-target-class默认是 true就是强制走 CGLIB。这个知识点和上面那段Transactional代码直接挂钩Spring 事务本身就是 AOP 的典型应用。如果你的 Service 方法用了this去调用同类里的另一个Transactional方法事务仍然是失效的因为它绕过代理对象走的是 this 本身的调用。想触发事务必须通过注入的代理对象调用这个点就是 CGLIB/JDK 代理机制最经典的实战场景。我个人做完这个项目最大的体会是管理系统其实没有太复杂的技术难点真正的价值在于把业务状态、角色权限、部署流程这些东西真正理通。如果你做毕设我的建议是在把功能跑通后别急着加新功能先把部署文档补完整、把库表字段定义写清楚、把每个接口的校验逻辑过一遍这些投入比多写一个“导出 Excel”之类的功能更能经得住提问。最后再分享一个小技巧把系统的登录流程和借书流程画成时序图放进设计文档里无论是自己复盘还是答辩展示都能用更少的时间把项目讲得更有条理。
返回列表