最近后台收到好多读者留言,问学生项目或者课设到底怎么选技术栈,SpringBoot 和 SSM 有什么区别,前后端分离的项目怎么联调。正巧我自己手上完整跑过一个“基于 SpringBoot + Vue 的图书管理系统”,从数据库设计到后端接口,再到前端页面和打包部署,全程都走了不止一遍,踩了不少坑也总结了不少经验。今天就把这套系统怎么设计与实现,核心代码怎么写,关键点在哪里,一次性说清楚。
这套系统适合几类人:正在做毕业设计的学生、刚入门想搞懂 SpringBoot + MyBatis 整合的新人、以及想快速搭一套前后端分离项目当练手的开发者。很多教程只给源码不给思路,很多源码只讲结果不讲为什么,我这篇会把选型理由、表结构设计、接口实现、前端联调、部署避坑全部串起来讲,让拿到源码的人不仅会跑,还知道每个模块在做什么,这样答辩或者面试的时候才讲得出东西。
1. 为什么是这套技术栈:选型思路与整体设计
1.1 技术选型的真实考量
先说说为什么选 SpringBoot + Vue + MySQL + MyBatis 这套组合。很多人纠结要不要用 SpringCloud、要不要上 Redis、前端要不要用 TypeScript,其实对图书管理这类典型 CRUD 系统来说,核心诉求是:上手快、结构清晰、资料多、面试好讲。SpringBoot 简化了 Spring 繁琐的 XML 配置,内嵌 Tomcat,一个 jar 包就能跑;Vue 做前端开发效率高,组件化让页面代码可维护性大大提升;MyBatis 的 SQL 完全由自己掌控,比起 JPA 的“自动生成 SQL”更容易排查问题,尤其适合面试时被问“你 SQL 是怎么写的”。
有的同学可能会问,都 2025 年了,为什么不用 MyBatis-Plus?我的看法是,基础项目优先用原生 MyBatis,原因有两个:第一,MyBatis-Plus 的 LambdaQueryWrapper 确实很爽,但很多初学者用了之后连 ResultMap 都不认识了,底层原理问一下就露馅;第二,图书管理系统涉及多表联查、条件分页、动态 SQL,这些正是 MyBatis 的强项,练一遍 XML 写法,受益比直接用 MP 大得多。
这套系统在设计上采用前后端完全分离的模式:Vue 项目独立运行在 8080 端口(或自定义端口),SpringBoot 后端独立运行在 8081 端口,前后端通过 RESTful API 进行数据交互,JSON 作为统一数据格式。这种模式下,前端的页面渲染和后端的数据逻辑互不干扰,也方便以后拆分部署。
1.2 功能模块与角色权限划分
图书管理系统的功能从用户角度可以分成两类角色:管理员和普通读者。管理员负责图书的增删改查、分类管理、借阅审核、归还登记、读者管理;读者可以浏览图书、检索图书、发起借阅、查看个人借阅记录。我把模块拆成六个部分:登录注册模块、图书管理模块、读者管理模块、借阅管理模块、分类管理模块、统计看板模块。
业务上,借阅流程是最核心的部分。一个读者最多能借几本书?借期多久?逾期怎么算?这些规则我都把它放到后端统一校验,而不是在前端写死。比如我设定的规则是:普通读者最多同时借 5 本,借期为 30 天,逾期每天滞纳金 0.1 元。为什么放后端?因为前端校验可以被绕过,后端才是数据安全的最后一道关口。
角色权限我用的是最简单的拦截器方案:定义一个 Role 字段,管理员为 1,读者为 0。拦截器在请求进入 Controller 之前校验 Token 和角色的匹配关系,像添加图书、删除用户这类操作只允许管理员访问。业务简单的时候没必要直接上 Spring Security + JWT 全家桶,那套东西安全是安全,但对新手来讲反而很容易配错并把自己绕晕。
1.3 数据库设计:表结构与关系
数据库设计可以说是整个项目的地基。表关系没设计好,后面写 SQL 都是要还债的。我这套系统的表结构一共 5 张核心表:用户表 t_user、图书表 t_book、图书分类表 t_category、借阅记录表 t_borrow、以及一张评论表 t_comment(阅读扩展用,如果你不需要完全可以去掉)。
用户表字段包括 id、username、password、nickname、role、phone、create_time。注意 password 我存的是 MD5 加密后的值,绝不存明文。图书表字段有 id、book_name、author、publisher、isbn、category_id、total_stock、available_stock、description、cover_url。其中 category_id 关联分类表,available_stock 是可借库存,每次借阅成功会减一,归还时加回来。
借阅记录表是整个系统的“账本”,字段有 id、user_id、book_id、borrow_time、due_time、return_time、status、fine_amount。status 我设计了 0 待审核、1 借出中、2 已归还、3 已逾期这四个状态。为什么要单独维护 due_time 和 fine_amount?因为逾期费用的计算需要基于应还日,而不是基于当前时间动态算,否则用户每次刷新看到不同金额,体验极差。还书的时候一次算清楚,记录在表中,后续统计也方便。
CREATE TABLE t_book ( id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(100), isbn VARCHAR(20) UNIQUE, category_id INT, total_stock INT DEFAULT 0, available_stock INT DEFAULT 0, description TEXT, cover_url VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES t_category(id) );表结构设计时有一个特别值得说的经验:available_stock 这种“冗余字段”到底要不要加?从严格的数据库范式来讲,可借库存可以通过 total_stock 减去处于借出状态的记录数动态计算出来,属于冗余。但真实业务中,每次查询图书列表都要 count 一次借阅表,数据量大了性能非常难看。所以我在图书表里冗余一个可借库存字段,借书/还书时通过事务保证它和借阅记录同步更新。这种“用空间换时间”的做法是实际开发中很常见的取舍。
2. 后端动手实录:SpringBoot + MyBatis 核心实现
2.1 项目初始化与目录规划
后端项目的搭建我用的 Spring Initializr,依赖选配了 Spring Web、MyBatis Framework、MySQL Driver、Lombok。SpringBoot 版本用的 2.7.x,这里特别提示,不要一上来就用 3.x,因为 SpringBoot 3 要求 Java 17 起步,包名也从 javax 变成了 jakarta,很多网上的老教程、博客的代码在 3.x 下直接复制是跑不起来的。等到你把 2.7 这套逻辑吃透了,再升级也不迟。
项目包结构我习惯按“按功能模块分包”。com.library 下面拆出 controller、service、mapper、entity、common、config 这几个子包。Entity 放数据库实体类,Mapper 负责数据访问接口和 XML 映射,Service 写业务逻辑,Controller 只做参数接收和结果返回。很多新手把业务逻辑直接写在 Controller 里,图一时省事,后面想加一个校验或者复用一个逻辑的时候就要改很多处,所以分层还是得认真做。
application.yml 中的配置是绕不开的。数据库连接、MyBatis 映射、端口号都在这里。
server: port: 8081 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: yourpassword mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.library.entity configuration: map-underscore-to-camel-case: true这里有个我刚开始经常踩的坑:MySQL 8.0 的驱动类必须是 com.mysql.cj.jdbc.Driver,不是旧版的 com.mysql.jdbc.Driver。还有 serverTimezone 如果不设置,连接时会直接报 CST 时区错误。map-underscore-to-camel-case 设置成 true 后,数据库里 create_time 这种下划线字段就能自动映射成 Java 里的 createTime,省去大量手动配置 ResultMap 的功夫。
2.2 图书管理接口这样写最清晰
图书列表是一个典型的分页查询加多条件筛选接口。前端会传当前页码 pageNum、每页大小 pageSize、可选的书名关键字 bookName、分类 ID categoryId。后端需要返回一个包含总记录数和当前页数据的 PageResult 对象。
我先把 Mapper 接口定义出来:
@Mapper public interface BookMapper { List<Book> selectBookList(@Param("bookName") String bookName, @Param("categoryId") Integer categoryId, @Param("offset") int offset, @Param("limit") int limit); int countBookList(@Param("bookName") String bookName, @Param("categoryId") Integer categoryId); }对应的 XML 文件里重点展示动态 SQL 的写法。MyBatis 最厉害的地方就在于<if>标签可以根据条件动态拼接 SQL,这样就不需要为了不同查询条件编写不同的 SQL 语句了。
<select id="selectBookList" resultType="com.library.entity.Book"> SELECT * FROM t_book <where> <if test="bookName != null and bookName != ''"> AND book_name LIKE CONCAT('%', #{bookName}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{limit} </select>为什么 count 查询要单独写而不是用 selectBookList 的结果 size 代替?因为分页时我们查询的只是当前页的数据,例如每页 10 条,总共 100 条记录,selectBookList 查出来的集合 size 最多只有 10,远不是总数。前端分页组件需要知道总记录数才能计算总页数,所以必须有 count 查询。如果你想偷懒,也可以集成 PageHelper,但我建议自己手写一次分页,面试被问原理时才心里有底。
新增和修改图书的逻辑我会放在 Service 层。新增图书时先校验 ISBN 是否已存在,已存在直接抛出业务异常。修改图书时会涉及库存变化的场景,比如管理员把 total_stock 从 10 改成 8,那 available_stock 不能无脑跟着改,需要判断当前已借出数量,保证 available_stock 不小于已借数量,否则会出现“库存为负”的数据错误。
2.3 借阅归还的事务处理与状态机
借书这个操作涉及到两张表的变化:借阅记录表要新增一条待审核记录,图书表的 available_stock 要减一。这两步必须保证同时成功或同时失败,怎么保证?Spring 的@Transactional注解。我一开始也不理解事务到底有什么意义,直到有一次测试时故意让第二步 SQL 报错,结果发现借阅记录留下了,库存却没有减,数据直接对不上了,这才知道事务的重要性。
@Transactional(rollbackFor = Exception.class) public void borrowBook(Integer userId, Integer bookId) { // 1. 检查用户是否存在 // 2. 检查图书是否存在且可借库存大于0 // 3. 检查读者当前未归还数量是否达到上限 // 4. 插入借阅记录,状态为0(待审核) // 5. 更新图书 available_stock = available_stock - 1 }rollbackFor = Exception.class 这个参数不能省。Spring 的事务默认只对 RuntimeException 回滚,如果你抛的是普通 Exception,它不会回滚,数据就处于半更新状态。图书管理系统里我自定义了一个 BizException 继承 RuntimeException,业务校验不通过就抛它,配合全局异常处理器统一返回错误信息,代码非常干净。
借阅状态机的核心设计是:status 字段在整个生命周期中只能按照 0 → 1 → 2/3 的方向流转。为什么不让用户把 2 改成 0?因为状态回退会破坏业务记录的不可变性。例如一本书已经归还了,这时把状态改回借出中,库存对账就会出现问题。所以归还操作的后端逻辑是先校验当前状态必须是 1 或 3,然后更新状态为 2,同时把 available_stock 加一,并把实际归还时间写入 return_time。逾期金额在归还时根据 due_time 和 return_time 计算,得数入库。
2.4 登录鉴权的轻量实现
登录模块我用的方案是 JWT + 拦截器。用户登录成功后,后端生成一个包含用户 ID、用户名、角色的 Token 字符串返回给前端。前端把这个 Token 存在 localStorage 里,每次请求时在请求头加Authorization: token值。后端拦截器对除了登录注册外的所有接口做 Token 校验。
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !JwtUtil.verifyToken(token)) { response.setStatus(401); return false; } return true; } }JWT 有个特点是服务端不保存登录状态,Token 里本身就包含用户信息,解析后就能拿到 userId 和 role。对于图书管理系统这种体量完全够用。如果你要做强制下线或者踢人功能,JWT 就比较尴尬,那就得引入 Redis 做会话管理了,这个可以后续再扩展。
除了登录,还有一个容易被忽略的点,也是我实际开发中花费很多时间的地方:统一返回格式。我定义了一个 Result 类,包含 code、message、data 三个字段。所有 Controller 的返回值都包一层 Result,前端用响应拦截器统一判断 code 是否为 200,非 200 直接弹错误提示。这样做的好处是前后端约定非常清晰,不会出现前端拿到一个 list 后又得判断是不是 null 的混乱情况。
3. 前端实现与联调:Vue + Axios 完整落地
3.1 前端工程创建与基础配置
前端我用的是 Vue 2 的 Vue CLI 来创建的项目。为什么不推荐 Vue 3?倒不是说 Vue 3 不好,而是图书管理系统当前用到的 Element UI 组件库,在 Vue 2 下的资料最丰富,遇到问题搜索最快。如果你已经熟练了,用 Vue 3 + Element Plus 也完全没问题,原理是一样的。创建命令就两行:
vue create library-web npm install axios element-ui vue-router vuex环境配置上有一个值得注意的点:Node 版本不能太高也不能太低。之前我在一台装了 Node 20 的机器上跑 Vue 2 项目,编译直接报 OpenSSL 错误,后来把 Node 降到 16 才正常。如果你遇到error:0308010C:digital envelope routines::unsupported,可以考虑切换 Node 版本,或者在 package.json 的 scripts 里加上set NODE_OPTIONS=--openssl-legacy-provider。
项目的核心目录结构是 src 下面分 views、router、api、utils、components。Api 目录统一放 axios 请求方法,这是前后端分离项目的一个最佳实践。项目里所有接口都集中管理,后端改路径只需要改一个文件,不用去每个页面翻代码。
3.2 页面与路由设计
页面规划是:登录页、系统布局页(包含侧边栏和顶栏)、图书列表页、图书新增/编辑页、借阅记录页、读者管理页、个人中心页。路由配置用 Vue Router,在路由元信息 meta 里加 requireAuth 和 role 字段。前置守卫里判断:如果没有 Token 就跳转登录页,如果有 Token 但路由的 role 不匹配就提示无权限。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.meta.requireAuth && !token) { next('/login'); } else if (to.meta.role && to.meta.role !== store.state.user.role) { next('/403'); } else { next(); } });图书列表页看起来简单,实际做起来有很多细节。表格展示带上封面图,搜索栏是书名输入框加分类下拉框加搜索按钮。分页组件绑定 currentPage 和 pageSize,每次切换页码重新请求接口。还有一个重要的小功能:借阅按钮要根据 available_stock 判断是否可点,库存为 0 时按钮置灰并显示“不可借”。这些交互点虽然小,但直接影响用户体验。
编辑图书时我用一个对话框而不是独立页面,这样列表和编辑在同一个页面内交互,省去路由跳转的割裂感。表单验证用 Element UI 自带的 rules 规则,比如 ISBN 必填、库存必须是正整数。这里再强调一下:前端校验只是提升体验,真正必须校验的是后端。不要以为前端拦住了就万事大吉,别人用 Postman 照样可以绕过前端直接调你的接口。
3.3 与后端联调时的跨域与拦截器
前后端分离必须面对跨域问题。浏览器出于安全策略,默认禁止从一个域名的页面去请求另一个域名的接口。开发环境最简单的解法是配置一个 Vue CLI 的 devServer 代理:所有以/api开头的请求转发到http://localhost:8081。
// vue.config.js module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } } };这样做的好处不仅仅是解决跨域,接口地址也不用写死,上线时只要用 Nginx 配一个反向代理就好。我在开发时习惯所有后端接口路径都以 /api 开头,例如/api/book/list、/api/user/login,这样代理配置只需要写一次。
Axios 的封装也很重要。我项目里在 utils/request.js 中创建 axios 实例,设置 baseURL 为/api,然后添加请求拦截器和响应拦截器。请求拦截器里从 localStorage 取 Token 放到请求头;响应拦截器里判断 HTTP 状态码,401 时清除本地信息跳转登录页,业务错误码直接弹 Message 提示。封装完以后,页面里的请求代码就可以精简成一行:
getBookList(params).then(res => { this.tableData = res.data.records; this.total = res.data.total; });联调阶段最常见的报错就是 404 或者 500。404 大概率是后端接口路径和前端 api 方法里的路径拼错了,尤其是大小写问题,我踩过一次 BookName 和 bookName 不一致,排查了快一小时;500 一般是后端空指针或者 SQL 异常,解决办法是看后端控制台的完整堆栈,而不是只看前端返回的 error 信息。很多新人联调一报错就盯着前端看,方向就反了。
4. 环境搭建与部署实录
4.1 MySQL 8.0 安装与建库
先把数据库搞定。这里以 MySQL 8.0 为例,我在 Windows 和 Linux 上都部署过。Windows 上安装时最容易忽略的就是选择好字符集,我建议安装时直接选 utf8mb4,它可以完整支持中文和一些特殊字符。装好后打开命令行窗口执行mysql -u root -p登录,然后建库:
CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library_db; -- 然后执行建表 SQL初始化数据也别空着手写,我通常在根目录放一个 init.sql 文件,包含全部建表语句和基础的测试数据,比如几个分类、几本示例图书、一个管理员账号。拿到项目的人第一步就是执行这个 SQL,能少很多麻烦。这里有个小细节:我建库时没有任何敏感数据,也没有涉及任何政策法律相关内容,只是纯粹的测试数据,可以放心使用。
4.2 SpringBoot 配置要点
后端配置前面已经展示过一部分 application.yml,这里还想再提两个点。一个是多环境配置:我拆了 application-dev.yml 和 application-prod.yml,dev 环境数据库是本机的,prod 环境是服务器的,启动时用--spring.profiles.active=prod来指定。第二个是日志配置:MyBatis 打印 SQL 的开关在测试阶段一定要打开,这样你能看到每条查询实际执行的 SQL 语句。
logging: level: com.library.mapper: debug把这个配置加到 yml 中,MyBatis 的执行日志就会打印到控制台。排查 SQL 问题、确认参数传递是否正确,这个开关负责任地说,能帮你省一半的时间。上线时再把日志级别调回 info,避免性能损耗和日志刷屏。
打包部署也很简单。后端在项目根目录执行mvn clean package -DskipTests,就会生成一个 library-backend-0.0.1-SNAPSHOT.jar 文件。然后在服务器上执行nohup java -jar library-backend-0.0.1-SNAPSHOT.jar > app.log 2>&1 &,后端就跑起来了。前端执行npm run build,生成 dist 目录,用 Nginx 指向这个目录并配置好反向代理,整个系统就上线了。
4.3 一个可复用的部署检查清单
部署容易漏配置,下面这个清单是我每次部署都会过一遍的,每次都能检查出问题。数据库连接信息是否正确,账号密码是否匹配;前端构建时接口前缀是否指向线上地址,或者 Nginx 代理是否生效;后端启动端口是否被占用,如果被占用用netstat -tunlp | grep 8081查看;防火墙是否放行了相应端口;Nginx 配置中前端路由是否使用了 history 模式,如果用了 history 模式需要配置 try_files 防止刷新页面变成 404。
Nginx 单页应用配置的核心就一句,是所有做过前端部署的人都应该知道的:
location / { root /data/www/library; index index.html; try_files $uri $uri/ /index.html; }不加 try_files 这行,Vue Router 用 history 模式时,用户一旦手动刷新/book/edit/3这个页面,Nginx 就会去服务器找这个路径对应的文件,找不到就返回 404。加了 try_files 后,所有路径都回退到 index.html 交给前端路由处理。这个坑我当年部署时踩过一次,刷新页面直接白屏的体验真的很让人崩溃。
5. 常见问题与跟踪排查实录
5.1 项目开发中我踩过的坑
第一个坑是关于数据库字段下划线与 Java 驼峰命名的映射。表里写的 create_time,实体类写的 createTime,如果 yml 里没有开启 map-underscore-to-camel-case,MyBatis 查询出来的 createTime 永远是 null。一开始我还以为是数据库没数据,加了半天测试数据才发现是映射配置问题。解决办法就是我在前面提到的 yml 配置项。
第二个坑是事务不生效。我在一个 Service 里写了个方法进行借书操作,但故意让第二步 SQL 报错,一查数据发现借阅记录已经插进去了,库存却没减。查了资料才发现是调用了同类内部的另一个方法导致事务失效。Spring 事务基于 AOP 代理,同类内 this 调用不会经过代理对象,解决办法有两种:把内部方法移到另一个 Service,或者用 AopContext 获取代理类。我最后选择了拆成两个 Service,结构更清晰也更好理解。这个点面试时被问到的概率非常高,能讲出场景演示出来,印象分蹭蹭涨。
第三个坑是跨域配置。有时候开发环境用代理已经解决了,但在某些笔记本上代理不生效,接口请求一直报错。后来我发现是 devServer 配置写错了,把 target 的 localhost 写成了项目名称或错误 IP。启动前端后打开浏览器 F12,network 面板看实际请求的 URL,就知道哪里转发错了,比盲目改代码高效得多。
第四个坑是前端组件的数据绑定问题。Element UI 的表格数据经常需要:data="tableData"绑定,但 tableData 在异步请求返回后要重新赋值才能触发视图更新。我在操作借阅成功后,调用了this.getBookList()重新拉取列表,而不是手动修改表格里某一行的数据,这样可以保证视图和数据的一致性,也让代码逻辑更简单。记住一个原则:列表数据任何时候都从接口拉,不要试图在前端做源数据的修改。
5.2 面试常被问到的高频问题
做完这个项目后,不管是应聘 Java 开发实习生,还是本科毕业找工作,这个项目大概率是你简历上最拿得出手的东西。面试官也比较喜欢基于这个项目往深处问。我整理了几道常考的问题以及从项目出发的回答思路,这次分享在这里:
第一个问题:SpringBoot 的自动配置原理是什么。回答这个问题要结合项目,你可以说我在图书管理系统里引入了 mybatis-spring-boot-starter 依赖后,没有手动配置 SqlSessionFactory,因为它利用 SpringFactoriesLoader 机制加载了 MybatisAutoConfiguration 配置类,再通过 @ConditionalOnClass 和 @ConditionalOnMissingBean 这些条件注解自动创建好了需要的 Bean。只有讲出项目里用到的细节,面试官才会认定你是真的写得多,而不是背面试题。
第二个问题:MyBatis 中 #{} 和 ${} 的区别。这是一个送分题,但很多人都答不全。#{} 是预编译,会生成一个问号占位符,由 PreparedStatement 来赋值,能有效防止 SQL 注入;${} 是直接拼接字符串,通常用在表名、排序字段这种不能预编译的场景。在图书列表的动态查询里我用的是 #{}:WHERE book_name LIKE CONCAT('%', #{bookName}, '%')。注意 LIKE 查询不能直接写成LIKE '%#{bookName}%',那个不是语法错误,但结果绝不会是你预期的,因为参数根本没有被拼接到 LIKE 的 % 中间去。这也是一个隐藏很深的坑。
第三个问题:index 和事务。比如一次借书操作,我为什么用事务?怎么保证多张表的数据一致性?你可以回答利用 @Transactional,脏回滚就是有 RuntimeException 的时候全部回滚,保证借阅记录表和图书表的操作要么都成功要么都失败。这样的回答既有原理又有实践,比单纯背概念要扎实。
第四个问题:JWT 整体流程及优缺点。要结合项目讲清楚客户端携带 Token 访问受保护接口的完整流程,再讲每个 Token 过期处理方案,比如续期、刷新,以及怎么用拦截器只放行登录接口。如果面试官问 JWT 的缺点,你可以说服务端不能主动吊销 Token。这个优缺点讨论和项目实际做法结合起来,就是很出色的作答。
5.3 项目还能怎么扩展
如果做完基础版本还想加亮点,我推荐三个方向。第一个是引入 Redis 做热门图书缓存,每次查询列表时先查缓存,缓存没有再去查数据库,这就能引出缓存穿透、缓存击穿、缓存雪崩的一串问题,把项目的技术含量直接拉高一个档次。第二个是增加上传功能,让管理员能上传图书封面图片,涉及本地存储路径配置、静态资源映射。第三个是加入定时任务,每天凌晨扫描 overdue 状态的借阅记录,自动计算逾期费用并短信通知读者,这个可以通过 Spring 自带的 @Scheduled 定时注解实现,加上之后项目就有了“主动服务”的亮点。
我个人的体会是,图书管理系统虽然表面上是个 CRUD 项目,但把它的每一个模块都做深、把每一步的原理都搞清楚,你收获的绝对不只是“会跑一个项目”这么简单。从表设计到事务处理,从 JWT 鉴权到前后端跨域,再到打包部署,这一条线完整走下来,你对 Java 后端开发的全链路认知会清晰非常多。
最后再分享一个小技巧:项目写好以后,一定记得写一份 README,把技术栈、模块图、数据库脚本位置、启动步骤写清楚。这不只是给别人看,更是给三个月后的自己看。我见过很多人项目写完就扔,等到答辩或者面试要讲的时候,看着自己的代码一脸茫然。保持好复盘习惯,才能让这套源码真正变成你自己的东西。