资讯详情 图书馆管理系统实战:Spring Boot+MySQL+Vue前后端分离开发指南
📅 2026/10/6 3:46:07
简介图书馆管理系统完整源代码包面向需要了解B/S架构图书业务流转的开发者、计算机专业学生及相关运维人员可解决借阅、归还、预约、查询与统计等核心业务的自建需求。整套资源共175个文件压缩后仅482KB包含26个cs后端逻辑文件、23个aspx页面文件、17个js前端脚本、2个ascx自定义控件另有90个gif图片及mdf/ldf数据库文件、rpt报表等辅助内容便于对照学习各层代码调用关系。已有5713人学习下载在图书管理系统入门与课设复现场景中具有较高参考价值。通过源码可梳理Controller、Model、View的典型分层结合数据库脚本理解图书表、读者表、借阅记录表的设计从借书、还书、新增读者、管理员设置等业务页面可快速定位对应功能的实现入口。拿到手后可进行功能扩展、界面调整与数据统计优化也可直接作为Web开发课程设计或毕业设计的改造蓝本。1. 图书馆管理系统完整源代码到底在解决什么问题你搜到的“图书馆管理系统完整源代码”多半不是哪个高深算法而是一套围绕图书借还的 Web 工程后台管图书、读者、借阅记录三块基础数据前台把检索、借书、还书、逾期这几条链路做成页面。网上能下载到的版本很多从 JSP Servlet 到 Spring Boot Vue 都有真正能让你快速跑起来的往往不是功能最全的那个而是数据库设计最简单、接口边界最清楚的那个。这篇笔记按我常用的一套 Spring Boot MySQL Vue 前后端分离方案把从建表到部署的关键代码和坑位讲清楚。适合正在做课设、毕设或者第一次想独立接手一个完整业务系统的人。判断一份源码能不能用先看它的借阅表和库存扣减比看页面数量管用得多。2. 业务模型先说清图书馆管理系统的四张表与借还状态机做管理系统最忌讳上来就建 Controller。图书馆管理系统看起来是增删改查但真正要守住的是“借”和“还”这两条链路。借书时要判断读者能不能借、书有没有可借副本还书时要判断有没有逾期、要不要算罚款。把这两个动作拆开背后就是四张表加一个状态机。这一章先把表和状态定死后面写接口才不会左右改。2.1 四张表的字段设计为什么 ISBN 必须用 varchar图书馆管理系统的核心表我一般拆成 category分类、book书目、reader读者和 borrow借阅记录这四张。book 存的是“书目加库存”borrow 存的是“一次借阅行为”两者分开库存变动和流水变动不会互相锁死。字段上有几个容易踩的细节id 用 bigint 自增应付教学和中小型图书馆足够ISBN 要用 varchar(20)不能图省事用数值型因为 ISBN 可能带大写 X而且数值型会把 0 开头的号丢掉title 加普通索引而不是全文索引MySQL 对中文全文索引分词效果很一般中小型目录用 LIKE 配合索引已经够用available 表示当前可借副本数不能只盯着 stock因为 stock 里有一部分在读者手里。CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE library; CREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE COMMENT 分类名如文学/计算机, sort INT NOT NULL DEFAULT 0 COMMENT 排序权重小的靠前 ) ENGINEInnoDB; CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL COMMENT ISBN含X且可能有前导0禁止用整数, title VARCHAR(200) NOT NULL, author VARCHAR(100) DEFAULT , publisher VARCHAR(100) DEFAULT , category_id BIGINT DEFAULT NULL COMMENT 逻辑外键指向category.id, stock INT NOT NULL DEFAULT 0 COMMENT 馆藏总量, available INT NOT NULL DEFAULT 0 COMMENT 当前可借数必须stock, location VARCHAR(50) DEFAULT COMMENT 馆藏位置如A柜3排, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_isbn (isbn), INDEX idx_title (title), INDEX idx_category (category_id) ) ENGINEInnoDB;这段建表脚本里utf8mb4 是必须的它能存生僻字和特殊符号用 utf8 会在某些冷门书名上直接报错。available 和 stock 要一起维护借书成功时 available 减一还书时加一任何一端忘掉库存数就会悄悄飘走。isbn 和 title 这两个索引对应前端页面上最常见的两种检索方式扫码查 ISBN或者按书名关键词模糊搜。图书量到几十万条以后再把 LIKE 查询换成 Elasticsearch那是后话。继续看读者和借阅记录两张表。reader 表管借阅身份borrow 表每次都记录一本书和一个人之间的关系。CREATE TABLE reader ( id BIGINT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) NOT NULL UNIQUE COMMENT 读者证号借还时刷这个, name VARCHAR(50) NOT NULL, phone VARCHAR(20) DEFAULT , max_borrow INT NOT NULL DEFAULT 5 COMMENT 最大在借数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE borrow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_id BIGINT NOT NULL, reader_id BIGINT NOT NULL, borrow_time DATETIME NOT NULL, due_time DATETIME NOT NULL COMMENT 应还时间一般借期30天, return_time DATETIME NULL COMMENT 实际归还时间NULL表示未还, status TINYINT NOT NULL DEFAULT 1 COMMENT 1借出 2逾期 3已还 0取消, fine DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 累计罚款, INDEX idx_reader (reader_id, status), INDEX idx_due (due_time), INDEX idx_book (book_id, status) ) ENGINEInnoDB;borrow 表里最容易被忽略的是 status 字段的类型选择。我见过有人用 varchar 存“借出”“逾期”“已还”这样的中文看起来直白但统计和索引都很别扭SQL 里还得到处写字符串比较。用 TINYINT 加注释是最省事也最不容易写错的方案。2.2 借还状态机逾期不能用“算出来”的要用“跑出来”的borrow 表里的 status 就是一个状态机。初始状态是 1借出到了应还时间还没还就变成 2逾期读者还书后变成 3已还。为什么不在查询页面里实时算“是否逾期”因为统计页、列表页、催还提醒都要重复判断 due_time每处判断逻辑稍微不一致就会出现“列表显示逾期但详情页正常”的尴尬。我一般会写一个每天 0 点 5 分执行的定时任务把到期未还的记录批量置成 2同时把当天累计的逾期天数换算成罚款写进 fine 字段。这样业务代码读到 status 就能直接做展示和拦截查询也简单。-- 逾期标记每天定时执行一次 UPDATE borrow SET status 2, fine 0.10 * DATEDIFF(CURDATE(), due_time) WHERE status 1 AND return_time IS NULL AND due_time CURDATE();注意这条 SQL 有个边界如果定时任务当天已经跑过第二天再跑fine 会被 DATEDIFF 重新算成“从应还日到今天的总天数”而不是增量累加。所以罚款逻辑更稳的写法是把“标记逾期”和“计算罚款”拆成两步第一步只改 status第二步按每条记录实际逾期天数乘单价。别在状态更新的 SQL 里做累加运算这是这个表唯一值得记住的教训。2.3 最容易返工的设计读者账号体系拆不拆很多“完整源代码”会把用户、管理员、读者全塞进一张 user 表用 role 字段区分。这样做的代价是读者需要读者证、最大借阅数、状态管理员需要工号和权限分组两类字段混在一起一半是 NULL。接口层每拿一个用户还要先判断 role再决定能不能取借阅信息。我的建议是分两层user 表管登录id、username、password、rolereader 表管借阅身份card_no、name、max_borrow、status。管理员如果也要借书就额外建一条 reader 记录两表通过 user_id 关联。这个拆分从课程设计角度看不增加多少代码量却能让后面做“读者停用”“管理员操作日志”时少改一堆表。如果你拿到的源码里只有一张 user 表也别急着重写可以加一个 user_type 字段再单独建 reader_ext 扩展表属于给老系统留后悔药的常规操作。3. 后端接口落地用 Spring Boot MyBatis 跑通登录与借还先解释选型现在网上流传的图书馆管理系统技术栈大体分两类。一类是 JSP/Servlet 老古董能跑但页面和逻辑耦合答辩时加需求很难改另一类是前后端分离后端 Spring Boot前端 Vue数据接口清晰。我一般选后者因为借书、还书、统计这些动作都能用接口单测出了问题也好定位。下面以 Spring Boot 2.7 MyBatis MySQL 为例展示最小可运行的后端。3.1 pom.xml 依赖清单少了哪个都会在启动时报 ClassNotFound一个教学级的图书管理系统后端依赖不是越多越好。最少需要 web 启动器、MyBatis 和 MySQL 驱动、Lombok、参数校验、JWT 工具。Spring Security 可以不用因为课程设计和多数内部系统只需要“登录后放行带权限校验请求”两件事用拦截器足矣。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 scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.security/groupId artifactIdspring-security-crypto/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency /dependenciesmybatis-spring-boot-starter 的版本要和 Spring Boot 大版本匹配2.2.2 对应 Spring Boot 2.x如果你新建项目时选的是 Spring Boot 3.x要换成 mybatis-spring-boot-starter 的 3.x 版本。mysql-connector-java 在 Spring Boot 2.7 里会被自动管理版本不用自己写。spring-security-crypto 只用来做密码校验不用引入全套 Security。Lombok 只是省掉 getter/setter如果 IDEA 没装 Lombok 插件代码一编译就报找不到方法。这里有个玄学问题插件装完要重启 IDE否则经常显示“找不到符号”实际上代码没问题。3.2 登录接口与 JWT 签发不要把密码明文放在数据库里登录逻辑看似简单但这里通常有两条被答辩老师一眼看穿的毛病一是密码明文存储二是登录成功后什么都没有前端不知道该带什么凭证。我给出的最小方案是密码用 BCrypt 加密登录成功返回一个 token。RestController RequestMapping(/api/auth) public class AuthController { Resource private UserMapper userMapper; PostMapping(/login) public Result login(RequestBody Valid LoginReq req) { User user userMapper.findByUsername(req.getUsername()); if (user null || !BCrypt.checkpw(req.getPassword(), user.getPassword())) { return Result.error(账号或密码错误); } if (user.getStatus() ! 1) { return Result.error(账号已被停用); } String token JwtUtil.createToken(user.getId(), user.getRole()); MapString, Object data new HashMap(); data.put(token, token); data.put(role, user.getRole()); data.put(name, user.getRealName()); return Result.ok(data); } }BCrypt.checkpw 会自动从密文中取出盐做比对比 MD5 强在每次生成的密文都不一样就算数据库泄露也没法直接反查。createToken 里放入 userId 和 role过期时间设 24 小时。返回值里千万不要带密码字段把整个 User 对象序列化回给前端是很多项目泄露账号的高发点最好手动组装一个 Map 或建一个 LoginVO。JWT 工具类本身不复杂核心就是签名和过期时间。SECRET 一定要换掉默认值生产环境放到配置中心不要写死在代码里。public class JwtUtil { public static final String SECRET change-this-in-production; private static final long EXPIRE 24 * 60 * 60 * 1000L; public static String createToken(Long userId, String role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } }token 有了后端还要有拦截器去校验。我通常在 WebMvcConfigurer 里注册一个 HandlerInterceptor逻辑很简单读取 Authorization 头去掉 Bearer 前缀解析 JWT解析失败就返回 401。放行路径只加 /api/auth/login其余接口全部走拦截。这样前端只要带了合法的 token就能访问没带或者过期了统一走到 401 的分支上。3.3 借书接口为什么必须用条件更新库存并发问题的标准解法这是整个系统最值得抄的一段。很多源码的借书逻辑是先查出 book在 Java 里判断 available 是否大于 0然后 update。这在单用户测试时没问题一旦用 JMeter 模拟两百人同时借同一本书available 就会变成负数。原因很简单两个请求同时查到了 available1都认为自己能借先后执行减一最后变 -1。正确做法是把判断写进 SQL让数据库来做原子操作。Transactional public void borrow(Long readerId, Long bookId) { int maxBorrow readerMapper.getMaxBorrow(readerId); int current borrowMapper.countActive(readerId); if (current maxBorrow) { throw new BizException(达到最大借阅数 maxBorrow 本); } int updated bookMapper.decreaseAvailable(bookId); if (updated 0) { throw new BizException(这本书暂时没有可借副本); } LocalDateTime now LocalDateTime.now(); Borrow borrow new Borrow(); borrow.setBookId(bookId); borrow.setReaderId(readerId); borrow.setBorrowTime(now); borrow.setDueTime(now.plusDays(30)); borrow.setStatus(1); borrowMapper.insert(borrow); }!-- BookMapper.xml -- update iddecreaseAvailable UPDATE book SET available available - 1 WHERE id #{bookId} AND available 0 /update借书接口里的执行顺序是有讲究的先校验读者可借数再扣库存最后插借阅流水。扣库存用 UPDATE 影响行数做判断等于把“还有没有库存”这个判断委托给数据库行锁完成。decreaseAvailable 返回 0 说明 available 已经小于等于 0直接抛异常事务回滚。这里 Transactional 不能省略因为 decreaseAvailable 成功后、insert 失败时要保证库存加回来两个操作必须同进同退。注意MyBatis 的 update 返回值是影响行数affected0 一定不能继续往下插借阅记录否则库存没扣但流水多了。归还接口和它是镜像关系先查 borrow 记录并确认是当前读者借的然后更新 status 为 3、写 return_time再把 book 表的 available 加一。同样要防止“同一本书被两个人重复点归还”导致 available 溢出所以归还时的 UPDATE 也要带条件 WHERE id ? AND status IN (1,2)影响行数为 0 就提示“这条借阅记录不需要归还”。3.4 分页查询接口的参数约定page、size、keyword 三个参数怎么收图书列表是首页分页参数设计统一了前端就不用来回改。我习惯 page 从 1 开始size 默认 10、最大 50keyword 是书名或 ISBN 的模糊匹配。GetMapping(/api/books) public Result page(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size, RequestParam(required false) String keyword) { if (size 50) { size 50; } int offset (page - 1) * size; ListBookVO list bookMapper.page(keyword, offset, size); long total bookMapper.count(keyword); MapString, Object result new HashMap(); result.put(list, list); result.put(total, total); result.put(page, page); result.put(size, size); return Result.ok(result); }分页查询不要用 offset 特别大的场景比如跳转到第 1000 页MySQL 会扫描前面所有行。中小型图书馆没问题如果图书数据上万且翻页很深可以考虑“把上一页最后一条记录的 id 作为游标”。返回的 total 用来算前端分页组件的总页数list 里直接返回图书和分类名前端每个格子就不用再发一次请求查分类。4. 前端页面闭环用 Vue 把图书列表、借阅弹窗和统计卡片串起来后端接口就绪之后前端的工作是把它串成一个能操作的界面。图书馆管理系统的常见前端页面有登录页、图书列表页、借阅记录页和统计页。这里不需要什么炫酷动画关键是列表、弹窗、按钮状态这三件事的处理。4.1 用 Vite 初始化项目目录分 views、api、components 三块就够创建前端项目的命令是固定的Vite 已经是当下建 Vue 项目的默认选择它比 Vue CLI 启动快配置也直白。npm create vitelatest library-web -- --template vue cd library-web npm install npm install axios element-plus npm run dev命令最后的 --template vue 生成的是 Vue 3 项目。装 axios 用来发请求element-plus 是组件库弹窗、表格、分页、消息提示都靠它不用自己手写样式。目录结构我一般固定成三个文件夹src/api 放所有请求方法src/views 放页面src/components 放复用组件。组件库按需引入的配置在 Vite 插件里可以自动完成但为了不出幺蛾子我建议刚开始直接全局注册省去每写一个组件都要 import 的麻烦。4.2 Axios 封装baseURL、token 注入、401 跳转一次配好所有页面都会用到请求必须封装一个统一的 request。它要做三件事把 baseURL 统一成 /api方便后端代理请求拦截器里自动带 token让登录状态跟着每个请求走响应拦截器里统一处理 401免去每个页面自己判断“登录过期”。import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.hash #/login } return Promise.reject(error) } ) export const getBooks (params) request.get(/books, { params }) export const borrowBook (data) request.post(/borrow, data) export const returnBook (data) request.post(/return, data)baseURL 写成 /api 而不是完整的 http://localhost:8080是为了把跨域问题丢给开发服务器的代理去解决。timeout 设 10 秒图书馆系统没有大数据量接口超过 10 秒基本就是后端卡死或网络断了没必要让用户一直转圈。401 跳转用 hash 模式是因为我用的是 Hash 路由如果你用 history 路由这里要改成 router.push(/login)两个写法不能混。4.3 借书弹窗的边界处理库存为 0 禁用、提交防抖、成功后刷新图书列表页每一行通常有个“借书”按钮点击后弹窗确认读者。最容易犯的错是按钮永远可点点完发请求后才被后端拒绝。用户体验上应当让“没有可借副本”的书直接置灰已经提交的按钮进入 loading 状态防止二次点击。template el-table :databooks v-loadingloading el-table-column proptitle label书名 / el-table-column propisbn labelISBN / el-table-column propavailable label可借数 width100 template #default{ row } el-tag :typerow.available 0 ? success : danger {{ row.available }} /el-tag /template /el-table-column el-table-column label操作 width160 template #default{ row } el-button typeprimary :disabledrow.available 0 || submitting clickhandleBorrow(row) {{ submitting ? 借阅中… : 借书 }} /el-button /template /el-table-column /el-table /template借出操作提交之后要在 then 回调里做两件事刷新当前列表以及用 ElMessage 给一个明确结果。刷新列表会把 available 重新拉下来这样借完的书可借数即时减一不需要等用户手动刷新页面。submitting 这个布尔值在请求期间锁住整列按钮等接口返回后再解开比挨个绑定 row 的 loading 状态简单得多。前端还有一个容易被忽略的点列表请求参数要和后端对齐。后端分页接口接收的是 page 和 size但 Element Plus 的分页组件返回的是 current-page 和 page-size中间需要用一个 change 事件把组件参数翻译成接口参数这是新手经常对不上的地方。另外前端 disabled 只是体验后端接口仍然要校验库存因为别人可以绕过页面直接调接口这道防线只能在后端做。5. 部署运行避坑图书馆管理系统跑不起来的 5 个常见问题到这一章项目多半已经能在本地跑起来了下面这些坑是我部署各类源码时踩过的高频问题每一条都按“现象、原因、解决”三段写清楚复制党可以直接对号入座。5.1 MySQL 连接失败时区与驱动类名是新手第一个翻车点现象后端启动时报 Communications link failure或者提示 Unable to load authentication plugin caching_sha2_password。原因如果装的是 MySQL 8它默认的认证插件是 caching_sha2_password旧版驱动不认识另一个常见原因是 JDBC 连接串里没有写 serverTimezone驱动拿不到系统时区。解决换用 mysql-connector-java 8.x并把连接串补全。spring: datasource: url: jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driverdriver-class-name 必须是 com.mysql.cj.jdbc.Driver老教程写的是 com.mysql.jdbc.Driver在 MySQL 8 下会直接报驱动类过时。useSSLfalse 要留着本地开发没有证书true 会额外触发握手警告甚至连不上。如果你用的是 MySQL 5.7驱动和 URL 可以沿用上面的写法时区写成 GMT%2B8 也能识别但不如 Asia/Shanghai 直观。5.2 前端永远连不上后端跨域和代理的配置要放在一起看现象浏览器控制台报 CORS error或者请求发出去返回 404但后端日志里根本没有这个请求。原因前端跑在 5173后端跑在 8080端口不同就构成跨域。直接请求后端完整地址后端没开 CORS 就会被浏览器拦截。解决开发阶段用 Vite 的代理让浏览器始终请求前端自己由 Vite 把 /api 前缀的请求转发到后端。// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })代理配置里的 target 要和后端实际端口一致后端如果改了 server.port这里没同步改就会出现“前端请求 200返回的是 404 页面”的假象。还有一种情况是后端上线后部署在同一个端口通过 nginx 转发 /api那就把代理配置去掉直接把 baseURL 改成生产地址两条路线不要混用。5.3 图书库存变成负数先查后改是最隐蔽的业务 bug现象某本书明明显示库存只有 1两个人同时借最后 available 变成 -1借阅记录却插进去两条。原因借书接口在 Java 里先 select 再 update两个请求同时读到 available1都通过判断于是都执行了扣减。解决按第 3 章的写法把扣减语句改成带条件的 UPDATE并检查返回值。UPDATE book SET available available - 1 WHERE id #{bookId} AND available 0;affected0 时一定不能继续往下插借阅记录否则库存没扣但流水多了。验证方法是用 JMeter 或者写一个简单的多线程脚本对同一个 bookId 发 100 个并发请求断言 available 不小于 0。这类并发问题只在压力测试下才会显形单机手工点一万次也发现不了项目交付前做一轮并发冒烟是值得的。5.4 数据库中文乱码库、连接串、页面三处要同时到位现象页面录入的中文书名存到 MySQL 里变成??控制台打印出来也乱码。原因建的库是 latin1或者 JDBC 连接串漏了 characterEncoding又或者前端页面本身没声明 UTF-8。解决三处统一。建库语句显式写 utf8mb4连接串带 useUnicodetruecharacterEncodingutf8前端 index.html 加 meta charsetUTF-8。CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;如果库已经建好可以用 ALTER DATABASE library CHARACTER SET utf8mb4再对已有的表执行 ALTER TABLE book CONVERT TO CHARACTER SET utf8mb4。修改后要重启后端因为连接串里如果不带 characterEncoding已建立的连接还是会沿用旧编码。另一个冷门坑是 Maven 的 resources 插件在打包时可能把 application.properties 里的中文注释重新编码所以配置文件的注释尽量用英文或者干脆把配置写到 yml 里这类问题会少很多。5.5 端口占用导致的启动失败改端口比杀进程更快现象后端启动报 Port 8080 was already in use或者前端 npm run dev 报 EADDRINUSE。原因上上次启动的进程没关干净或者机器上另一个服务占用了默认端口。解决先看谁占用了端口再决定是杀进程还是换端口。Windows 用 netstat -ano | findstr 8080macOS 和 Linux 用 lsof -i:8080。如果占用的进程是残留的 Java 进程可以直接 kill如果端口上跑着别的服务改后端配置最省事。server: port: 8081前端 Vite 的默认端口是 5173如果也被占用在 vite.config.js 里把 server.port 改成 5174并加上 strictPort: true这样 Vite 就不会自动漂移到随机端口前端代理里的 target 也不会对不上。养成好习惯每次启动前后端之前先看一眼两个端口是否被占用能少跑半小时冤枉路。6. 从“能交差”到“能上线”日志、缓存与逾期处理的进阶前面的内容已经能撑起一个完整系统但如果你想让这份图书馆管理系统在答辩或面试时有话说下面几个小改造是性价比最高的。第一是给关键操作加日志。借书、还书、修改图书、删除读者这些动作都应该落到一张 operation_log 表里记录操作人、动作、对象 id、时间。实现上不建议在每个业务方法里手动 insert用一个 AOP 切面注解就能统一搞定。答辩时被问“数据出错了怎么排查”你掏出这张表比任何口头解释都有说服力。第二是给热门图书加缓存。图书列表是查询最频繁的接口一天几万次点击全打到 MySQL 上数据库压力不小。常见做法是把列表数据缓存到 Redis 里图书新增、修改、删除时再清缓存。一个小系统用 Redis 可能显得重但面试题里“缓存穿透、缓存击穿、缓存雪崩”就是从这里引出来的把热点 key 的逻辑讲清楚是很大的加分项。第三是给逾期任务加一个可观测的开关。前面定时任务每天跑还要关注它有没有成功执行。我会把每次标记的条数和消耗的时间写进日志并记录最后一次执行时间。Component public class OverdueTask { private static final Logger log LoggerFactory.getLogger(OverdueTask.class); Scheduled(cron 0 5 0 * * ?) public void markOverdue() { LocalDateTime start LocalDateTime.now(); int updated borrowMapper.markOverdue(LocalDate.now()); log.info(逾期标记完成本次处理 {} 条耗时 {} ms, updated, Duration.between(start, LocalDateTime.now()).toMillis()); } }cron 表达式里的“0 5 0 * * ?”表示每天零点五分执行错开零点整的流量高峰。这里要用 EnableScheduling 开启定时任务否则注解不生效。markOverdue 的 SQL 就是第 2 章那条 UPDATE我建议把罚款计算拆成独立任务每步都能单独重跑出问题也好补数据。这些年我做过不少管理系统最大的教训是代码能跑只是起点别人愿意继续用靠的是数据不容易出错、出错了能查、被人问了能说出所以然。这套图书馆管理系统做到借还闭环、并发安全、日志可查就已经超过大半网上流传的版本了。希望帮到你。本文还有配套的精品资源点击获取