SpringBoot+Vue图书管理系统开发实战:从数据库设计到部署上线 做了几个月的图书管理系统从需求分析、数据库设计到前后端联调、部署上线整个过程踩了不少坑也积累了很多经验。这篇文章我把完整的实现思路、核心代码、数据库设计和踩坑记录都整理出来希望能帮到正在做类似项目的朋友。这套系统到底解决什么问题先说人话版本这是一个基于SpringBootVue的前后端分离图书馆管理系统后端用Java开发数据库用MySQL持久层框架选了MyBatis前端用Vue框架。系统管的事包括图书信息维护、分类管理、读者管理、借书还书流程、逾期罚款计算以及管理员登录和权限控制。为什么要做这个系统我当时的目标很明确一是为了彻底搞懂SpringBoot和Vue前后端分离项目的完整开发流程从零开始搭一套能跑的通的项目二是图书馆里传统的纸质登记借还书方式效率太低还容易出错图书库存、读者借了几本书、有没有逾期光靠Excel表格根本管不过来。这个系统相当于给图书管理员配了一个数字化助手。这套项目可能最适合三类人准备做毕业设计的计算机专业学生想从零体验前后端分离项目完整流程的Java初学者还有确实需要一个轻量版图书管理系统的个人或小型机构。技术栈不花哨代码结构清晰业务逻辑完整直接照着敲一遍收获会非常大。1. 先理清业务需求图书馆管理系统到底要管什么1.1 功能模块拆解先画清楚系统边界很多新手一上来就写代码结果写着写着发现这里缺张表那里缺个接口。我做这种管理系统第一步永远是坐下来把需求完整捋一遍。这个图书馆管理系统核心业务其实就四大块第一块是图书管理。图书的基本信息书名、ISBN、作者、出版社、分类、价格、库存数量、上架下架、图书分类的维护。这块必须支持增删改查注意图书的删除要做逻辑上的约束如果这本书还有未归还的借阅记录就不能物理删除否则历史数据全乱套。第二块是读者管理。读者信息维护、借书卡状态管理比如读者是否被冻结、当前已借数量、是否有未缴罚款。实际设计的时候这些状态字段直接冗余在读者表里查询起来非常方便。第三块是借阅管理这是整个系统的核心。借书、还书、续借、逾期处理每一步都涉及多张表的状态变更。这部分必须想清楚各种异常场景比如超借数量、重复借同一本书、读者有欠款被限制借书。第四块是系统管理。管理员登录、JWT鉴权、个人密码修改。这里的核心是权限控制不同角色看到的菜单和能调用的接口必须区别开。1.2 角色权限设计不是所有用户都该看到同一套菜单这个系统涉及两类角色管理员和读者。但实际设计时读者端是不需要单独开发一套系统的我只把读者查询和借阅行为放在了同一个后台里用菜单权限控制可见性。权限控制的落点是在后端接口层。前端隐藏菜单只是体验层面的东西真正防住越权调用必须靠后端鉴权。我的做法是定义了一个AuthInterceptor拦截器对除登录接口外的所有请求校验JWT令牌有效才能放行。如果需要更细的控制可以在方法上再加RequireRole注解用AOP或拦截器判断角色类型我项目里用了后者代码也不复杂。1.3 借阅流程的状态机设计从在架到已借出再到归还借阅流程是整个系统里最容易出bug的地方核心原因是它不是一个单表操作而是多张表的联动变更。借书操作的完整链路是读者发起借书请求系统校验读者状态正常、该读者未借数量没超上限、这本书状态是在架且库存大于0然后新增一条借阅记录借出日期、应还日期再把图书状态改为已借出、库存减一。还书操作正好相反找到对应的借阅记录、填写实际归还日期、判断是否逾期、逾期就自动生成罚款记录、然后把图书恢复为在架状态、库存加一。整个借还过程必须放在同一个事务里。所以我在Service层方法上加了Transactional任何一个环节报错所有写操作全部回滚否则会出现库存减了但借阅记录没插上的诡异数据。2. 技术选型的底层逻辑为什么是这个组合2.1 SpringBoot选型理由约定大于配置快速搭建接口SpringBoot最大的意义是省掉了SpringMVC时代那一大堆XML配置。对于这种管理系统场景相对固定控制器、服务、Mapper三层的结构用它来搭建几个注解就能搞定一个接口模块。依赖管理上我直接用spring-boot-starter-parent作为父工程统一管理版本号基本不需要手动指定依赖版本。需要注意的是SpringBoot 2.x和3.x差异比较大3.x基于Jakarta命名空间要求JDK17以上。我项目里用的是SpringBoot 2.7.x对应的JDK是1.8这套组合最稳定网上资料也多对新手最友好。2.2 MyBatis还是MyBatis-Plus我选了原生MyBatis题目里写了用MyBatis我就顺着这个来。但说实话生产环境里我更推荐MyBatis-Plus因为单表CRUD不用手写SQL内置的BaseMapper直接帮你把增删改查都实现了。不过原生MyBatis有个很大的好处你能清晰地看到每个SQL是怎么写的怎么映射的手写SQL和XML配置的对应关系一目了然。做这个项目的时候我故意全部用原生MyBatis就是为了把这一块彻底吃透。后面如果切换MyBatis-Plus其实就是把Mapper接口改成继承BaseMapper业务代码几乎不用动。分页这块我引入了PageHelper插件。用的时候只用在Mapper接口方法前加一行PageHelper.startPage(pageNum, pageSize)返回的PageInfo里包含了总条数、总页数、当前页数据这些信息前端直接传页码和页大小两个参数就能拿到分页结果。2.3 MySQL版本5.7和8.0的兼容性坑我开发的时候本地用的是MySQL 8.0但部署到公司服务器上发现是5.7。这两个版本的差异在JDBC驱动上坑过一次8.0的驱动类是全限定类名com.mysql.cj.jdbc.Driver5.7的驱动类是com.mysql.jdbc.Driver如果你用的是8.0驱动去连5.7的库一定要写对驱动类名。还有时区问题。MySQL连接串里如果不加serverTimezoneAsia/Shanghai8.0版本在连接时会直接报时区异常。这一点我在后面踩坑部分会详细讲这里只需要记住连接串必须带上useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai一套配齐。2.4 前端框架选型Vue2依旧是这类项目的稳妥选择标题指定了Vue具体版本我选了Vue2。原因很现实Vue2的生态最成熟ElementUI组件库就是为Vue2深度定制的后台管理页面用ElementUI的表格、表单、弹窗组件开发效率极高。Vue3当然也好Composition API写起来更爽搭配Element Plus也没问题。但考虑到这个项目的定位是教学和实操Vue2的模板语法、响应式原理、路由和状态管理网上资料最多遇到问题最容易查到解决方案。如果你已经比较熟练了用Vue3重写前端部分完全没问题后端接口不用动。3. 数据库设计八张表把借阅业务的来龙去脉串起来3.1 核心四张主表管理员、读者、分类、图书数据库是管理系统的基础表结构设计合理了后面写SQL会非常顺畅。我的library_db数据库里一共设计了八张表。管理员表t_admin很简单id主键、username用户名、password密码、real_name真实姓名、create_time创建时间。密码存储我不会用明文而是通过MD5加盐之后存储避免密码泄露风险。读者表t_reader字段稍微多一点id、card_no借书证号业务上唯一按规则自动生成、name姓名、phone手机号、status状态正常/冻结、borrow_count当前借阅数量、fine_amount未缴罚款金额、create_time。分类表t_category就三个字段id、category_name分类名称、description描述。图书表t_book字段较多id、book_name书名、isbnISBN号、author作者、publisher出版社、category_id分类ID关联分类表、price价格、stock库存、status状态在架/已借出、create_time。3.2 业务关联表借阅记录、罚款记录、公告第六张表是借阅记录表t_borrow_recordid、reader_id关联读者、book_id关联图书、borrow_date借出日期、due_date应还日期、return_date实际归还日期为空表示未还、status状态借出中/已归还/逾期。第七张表是罚款记录表t_fine_recordid、record_id关联借阅记录、reader_id关联读者、amount罚款金额、reason罚款原因、status未缴/已缴、create_time。第八张表是公告表t_noticeid、title标题、content内容、create_time。这张表不是核心流程必需的但现实中图书馆总得发个开馆通知、假期安排什么的加上之后系统更完整。3.3 设计细节冗余字段和逻辑删除我做表设计时有几个习惯一是必要的冗余字段要保留。比如读者表的borrow_count和fine_amount其实可以从借阅记录和罚款记录里统计出来。但如果每次查询读者列表都去统计一次数据量大了性能就很差。保留冗余字段借书时加一、还书时减一查询时直接读字段就行。这个设计在小系统里非常实用。二是尽量做逻辑删除。用户的问题看似是完整源码但实际系统交付后图书如果被删了历史借阅记录就查不到书名了。所以t_book表里我没有做物理删除而是通过状态字段控制上架下架。真要删数据也建议加一个deleted字段标记而不是直接DELETE FROM。三是外键尽量别在数据库层建。我用的是逻辑关联通过代码保证数据一致性。原因很简单数据库物理外键在数据量大了之后会影响插入删除性能而且维护起来很麻烦。系统层面控制逻辑关系配合事务完全够用。4. 后端接口实现SpringBootMyBatis的完整落地4.1 工程结构与依赖配置后端工程我命名为library-backend采用标准的Maven目录结构。包名按照com.abo.library组织下面细分controller、service、mapper、entity、common、config等子包。在pom.xml里我引入了这些核心依赖dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.2/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesapplication.yml配置是最容易出问题的地方我贴一下我的核心配置注意注解说明server: port: 8088 spring: datasource: url: jdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.abo.library.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true这行配置非常重要它能把数据库的create_time字段自动映射到实体类的createTime属性省去大量写resultMap的麻烦。日志输出配置StdOutImpl能让MyBatis在控制台打印执行的SQL调试阶段必须开。4.2 统一返回结果和全局异常处理前后端分离项目里接口返回的数据结构如果不统一前端处理起来会非常痛苦。我做了一个通用的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; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }全局异常处理我用RestControllerAdvice实现这样代码里不需要到处try-catch。业务异常统一抛出我自定义的BusinessException由全局异常处理器捕获并转化为统一的错误响应。数据库异常、空指针这类未预期异常也在这里兜底保证接口永远不会裸奔返回一堆莫名其妙的报错堆栈。4.3 JWT认证集成登录接口与拦截器登录接口的逻辑不复杂前端把用户名和密码传过来后端查t_admin表比对MD5加密后的密码通过则生成Token返回。Service public class AdminServiceImpl implements AdminService { Autowired private AdminMapper adminMapper; Override public String login(String username, String password) { String md5Password MD5Util.md5(password); Admin admin adminMapper.selectByUsernameAndPassword(username, md5Password); if (admin null) { throw new BusinessException(用户名或密码错误); } // 生成JWT有效期24小时 return JwtUtil.generateToken(admin.getId(), admin.getUsername()); } }JWT工具类里面就是jjwt库的标准用法生成Token时把用户ID和用户名放进去过期时间设置为一天。解析Token时如果签名不对或者过期了直接抛出异常拦截器捕获后返回401。前端拿到Token之后存在localStorage里每次请求在axios拦截器里加上Authorization: Bearer token请求头。拦截器核心代码public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 String uri request.getRequestURI(); if (uri.contains(/api/admin/login)) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); request.setAttribute(adminId, claims.get(adminId)); return true; } catch (Exception e) { // Token无效返回401 } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或Token已过期\}); return false; } }4.4 图书借阅的核心接口事务与状态校验借书接口是整个项目里业务逻辑最复杂的完整走一遍这个流程MyBatis的事务控制和多表操作就基本掌握了大半。Override Transactional(rollbackFor Exception.class) public void borrowBook(Long readerId, Long bookId) { // 1. 校验读者状态 Reader reader readerMapper.selectById(readerId); if (reader null) { throw new BusinessException(读者不存在); } if (reader.getStatus() 0) { throw new BusinessException(读者已被冻结无法借书); } if (reader.getBorrowCount() MAX_BORROW_COUNT) { throw new BusinessException(借阅数量已达上限请先归还部分图书); } if (reader.getFineAmount() 0) { throw new BusinessException(存在未缴纳罚款无法借书); } // 2. 校验图书状态 Book book bookMapper.selectById(bookId); if (book null || book.getStatus() 0) { throw new BusinessException(图书不存在或已下架); } if (book.getStock() 0) { throw new BusinessException(图书库存不足); } // 3. 校验是否重复借阅 int count borrowRecordMapper.countUnreturnedByReaderAndBook(readerId, bookId); if (count 0) { throw new BusinessException(该读者已借阅此书且未归还); } // 4. 创建借阅记录 BorrowRecord record new BorrowRecord(); record.setReaderId(readerId); record.setBookId(bookId); record.setBorrowDate(new Date()); record.setDueDate(DateUtil.addDays(new Date(), 30)); record.setStatus(1); // 借出中 borrowRecordMapper.insert(record); // 5. 更新图书库存和状态 bookMapper.decreaseStock(bookId); bookMapper.updateStatus(bookId, 1); // 6. 更新读者借阅数量 readerMapper.increaseBorrowCount(readerId); }Transactional(rollbackFor Exception.class)这行必须加上因为Spring默认只在遇到RuntimeException时才回滚而自定义的业务异常如果继承的是Exception不加这个参数就失效了。这是我踩过的坑特意写出来提醒大家。还书接口是借书的逆过程但多了一个逾期罚款的计算Override Transactional(rollbackFor Exception.class) public void returnBook(Long recordId) { BorrowRecord record borrowRecordMapper.selectById(recordId); if (record null || record.getStatus() 0) { throw new BusinessException(借阅记录不存在或已归还); } // 计算逾期费用 Date now new Date(); int overdueDays DateUtil.betweenDays(record.getDueDate(), now); if (overdueDays 0) { BigDecimal fine new BigDecimal(overdueDays).multiply(FINE_PER_DAY); record.setFineAmount(fine); // 插入罚款记录 FineRecord fineRecord new FineRecord(); fineRecord.setRecordId(recordId); fineRecord.setReaderId(record.getReaderId()); fineRecord.setAmount(fine); fineRecord.setStatus(0); fineRecordMapper.insert(fineRecord); // 读者表累加未缴罚款 readerMapper.addFineAmount(record.getReaderId(), fine); } record.setReturnDate(now); record.setStatus(0); borrowRecordMapper.updateById(record); // 图书库存回补 bookMapper.increaseStock(record.getBookId()); bookMapper.updateStatus(record.getBookId(), 0); // 读者借阅数量减一 readerMapper.decreaseBorrowCount(record.getReaderId()); }这一借一还两个接口写明白之后其他所有接口其实都是围绕这几个主表做增删改查难度一下子就降下来了。5. 前端页面与接口对接Vue组件化的实际写法5.1 前端工程结构与路由设计前端我用的Vue CLI创建工程命名library-frontend。核心目录结构是这样的src/ ├── api/ # 封装axios请求 │ ├── request.js # axios实例和拦截器 │ ├── book.js # 图书相关接口 │ ├── reader.js # 读者相关接口 │ └── borrow.js # 借阅相关接口 ├── router/ # 路由配置 │ └── index.js ├── store/ # Vuex状态管理 │ └── index.js ├── views/ # 页面组件 │ ├── Login.vue │ ├── Dashboard.vue │ ├── BookManage.vue │ ├── ReaderManage.vue │ └── BorrowManage.vue ├── components/ # 通用组件 └── App.vueapi目录里每个文件对应一组后端接口前端调用时不会散落在各个组件里维护起来非常清爽。5.2 axios封装与路由守卫登录状态的前端保障axios封装的核心是请求拦截器和响应拦截器。请求拦截器从localStorage里取Token加到请求头上响应拦截器统一处理错误码比如401就跳转到登录页。import axios from axios import router from /router const request axios.create({ baseURL: /api, // 通过vue.config.js代理转发 timeout: 10000 }) // 请求拦截器附加Token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, error { return Promise.reject(error) }) // 响应拦截器统一处理异常 request.interceptors.response.use(response { const res response.data if (res.code 200) { return res } else { router.push(/login) return Promise.reject(new Error(res.message)) } if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return res }, error { return Promise.reject(error) }) export default request这里有个实际开发中的细节前端开发服务器的端口是8080后端接口在8088跨域是绕不开的问题。我在vue.config.js里配置了devServer代理让前端把/api开头的请求都转发到http://localhost:8088。这样既能解决跨域又能在生产环境通过Nginx统一转发非常方便。5.3 图书管理页面表格加弹窗的开发模式图书管理页面是典型的列表加弹窗模式用ElementUI的el-table、el-dialog、el-form三个组件就能搞定。列表部分核心代码是这样的el-table :databookList border stripe v-loadingloading el-table-column propbookName label书名 min-width150 / el-table-column propisbn labelISBN width160 / el-table-column propauthor label作者 width120 / el-table-column propcategoryName label分类 width100 / el-table-column propstock label库存 width80 / el-table-column propstatus label状态 width80 template slot-scopescope el-tag :typescope.row.status 1 ? success : info {{ scope.row.status 1 ? 在架 : 已借出 }} /el-tag /template /el-table-column el-table-column label操作 width200 template slot-scopescope el-button sizemini typeprimary clickopenEditDialog(scope.row)编辑/el-button el-button sizemini typedanger clickdeleteBook(scope.row.id)删除/el-button /template /el-table-column /el-tableel-table-column里通过prop绑定字段名ElementUI会自动渲染数据。状态列用el-tag标签展示不同的颜色后台数据里的0和1到了前端变成用户看得懂的“在架”和“已借出”这个细节很影响使用体验。查询条件区就是一个输入框加按钮输入书名关键字点击查询后端接口通过MyBatis的动态SQL实现模糊查询。这个查询条件加进列表功能后管理几百本书的时候体验才叫好用。6. 部署运行中的典型报错与填坑实录6.1 数据库驱动的兼容性问题我在填坑过程中遇到的第一个问题就是MySQL驱动。本地MySQL是8.0用的是com.mysql.cj.jdbc.Driver项目启动正常。但后来我把代码部署到一台MySQL 5.7的服务器上发现驱动类报错找不到。排查后发现5.7的驱动类应该是com.mysql.jdbc.Driver。更坑的是新版MySQL驱动包里已经把旧类去掉了你拿着一套代码在两个版本之间切换驱动类名必须跟着改。更好的做法是尽量统一下环境版本要么都用8.0要么都用5.7别混着来。6.2 MyBatis的Mapper绑定异常MyBatis开发中最高频的报错就是Invalid bound statement (not found)。原因基本都是Mapper接口找到了但对应的XML映射文件没被扫描到。我的解决办法分两步第一步XML文件统一放在src/main/resources/mapper/目录下不要放在Java源码目录里第二步在application.yml里配置mybatis.mapper-locations: classpath:mapper/*.xml。这样配置之后Maven打包时会把XML文件拷到classes目录MyBatis才能加载到。还有一个容易忽略的点如果Mapper接口和XML文件中的namespace写的不一致运行时会报同样的错。每次新建Mapper时先确认namespace指向的接口全限定名和XML文件里的id对应的方法名完全一致。6.3 跨域配置不生效的坑前后端分离必然涉及跨域。我最初在后端写了一个全局跨域配置类但前端请求还是报跨域错误。后来一查才发现问题出在自定义拦截器上。SpringMVC的处理流程是先经过拦截器再进入控制器但跨域预检请求OPTIONS也会被拦截器拦下来结果还没走到跨域配置那里就被拦截了。解决方法是拦截器里放行OPTIONS请求if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; }放行之后跨域配置才能真正生效。这个问题排查起来比较隐蔽这里写出来帮大家少走弯路。6.4 前端npm依赖安装失败前端环境搭建时npm install报各种错是最常见的问题。我遇到最多的是node-sass安装失败原因多半是Node版本和node-sass版本不匹配。我的建议是尽量用dart-sass替代npm install sass1.32.13 --save-dev把项目里的node-sass换成sass兼容性好很多。另外npm install太慢或者报网络错误时把npm源切换到国内镜像npm config set registry https://registry.npmmirror.com这两个小操作能解决前端环境搭建中的绝大部分问题。6.5 文件上传大小限制如果系统需要支持上传图书封面图片SpringBoot默认的单文件上传大小是1MB稍微大一点的图片就会报MaxUploadSizeExceededException。需要在配置里调大限制spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB这个坑是因为一个封面图几MB很正常默认1MB根本不够用。从代码到完整项目还需要做哪些事纯代码写完离一个完整系统还有距离。几个我实际做的时候觉得同样重要的事第一SQL脚本必须完整。从建库、建表到初始数据我当时整理了一份完整的library_db.sql脚本别人拿到项目后执行一遍这个脚本就能把数据库跑起来。初始数据至少包含一个管理员账户和几本测试图书不然登录进去一片空白都不知道该干什么。第二接口文档要跟上。我这里说的是最少量的接口说明每个接口的URL、请求方式、入参、出参写清楚就行。不用上Swagger太重了一个Markdown文档足够。第三前端环境变量和后端配置要分离。数据库密码、接口地址这些别硬编码在代码里通过配置文件或环境变量注入换环境的时候只改配置不动代码。按照这个顺序一步步做下来一个能真正跑起来的图书馆管理系统就成了。套用同样的业务模型换一个领域比如会议室预订、器材管理代码骨架完全可以复用这就是做这类项目最大的收获。