如果你正在筹备 Java 方向的毕业设计图书租借系统很可能是你绕不开的一道经典题目。它没有电商系统那么庞杂也没有纯管理系统那么枯燥但刚好能把 Java 后端开发的核心环节都覆盖一遍用户权限、业务状态流转、数据库事务、并发控制、分层架构、前端交互。我用 Java 完整做了一版图书租借系统从需求拆解到数据库设计再到代码落地踩了不少坑也整理出了一些可以复用的经验。这篇文章就按我实际开发的顺序来写适合准备做毕设的同学参考也适合想快速搭一个图书租赁系统练手的 Java 初学者。后面所有结论都不是空谈每段代码和每个配置我都实际跑过。1. 图书租借系统到底在做什么别急着写代码先想清楚这四件事很多同学拿到题目就打开 IDE 开始建表写接口结果做到一半发现业务逻辑前后矛盾。图书租借系统表面上是个 CRUD 项目但真正决定分数的是业务规则的完整性和一致性。开始之前我建议先从四个角度把需求想透。1.1 从“图书租借”四个字拆出真正的业务角色图书租借系统的角色并不复杂通常只有两类管理员和普通用户。普通用户能注册、登录、检索图书、查看详情、借书、还书、续借、预约管理员则维护图书信息、分类信息、用户状态、处理借阅记录、设置逾期规则和罚款金额。但角色少不等于用例少。你设计用例图的时候不要只画“用户管理”和“图书管理”这种粗粒度用例要拆到“管理员下架图书”“用户预约已被借出的图书”“系统自动生成逾期罚款单”这个级别。我第一次设计时就是用例太粗导致后面控制器方法不知道该放哪个模块代码越写越乱。还有一个容易被忽略的点是“未登录用户”。图书检索和查看详情应该允许游客访问否则用户第一次使用系统时需要先注册再浏览体验很差。我在系统里给 Controller 层加了一个简单的会话拦截器允许访问的路径白名单包括首页、图书列表、图书详情、注册和登录接口其余所有路径都需要登录。这个设计在答辩时也是一个很好的亮点说明你考虑过系统的可用性。1.2 租借流程是系统的命脉先把状态机画清楚图书在系统里不是只有“在馆”和“借出”两种状态。实际业务里一本书可能被预约可能因为逾期未还被锁定也可能管理员下架后就不能再借。完整的状态机至少有以下几条主线图书状态上架可借 → 被借出 → 归还 → 重新变为可借被借出期间可以被预约。预约状态等待中 → 可领取书已归还 → 已借出 → 取消。借阅记录状态借出中 → 已归还 → 已续借 → 逾期未还。这些状态之间要明确谁能触发迁移。比如“预约”状态不能由用户手动取消已借出的预约单只能由系统在用户领取或管理员取消时改变续借不能发生在已逾期的情况下这是很多毕设容易漏掉的规则。我当时把状态迁移逻辑单独抽了一个BorrowStatusMachine类所有状态修改都走这个类避免 Service 层到处直接改状态字段造成的不一致。1.3 为什么毕设里“权限管理”往往是拿高分的关键权限管理虽然听起来像 RBAC 里的高大上概念但在图书租借系统里你不需要做非常复杂的多角色权限模型只要做好“普通用户和管理员的操作访问控制”就足够了。最简单的方案是User 表加一个role字段值为ADMIN或USER通过拦截器判断请求路径前缀。可如果你只在 Controller 里用if(user.getRole().equals(ADMIN))来判断后续扩展会很麻烦。我建议用一种更清晰的思路定义RequiredRole注解标注在每个需要管理员权限的 Controller 方法上然后在拦截器里用反射读取注解并比对用户角色。这样做的好处是把权限规则集中到了一处答辩时你可以直接展示这个设计思路说明你理解“权限与业务解耦”的价值而不是只会写 if 判断。1.4 需求边界哪些功能一定要做哪些可以留给二期毕设时间是有限的需求必须分清优先级。我整理了一张需求清单按照“必须做”“进阶做”“可以不做的”来划分优先级功能模块说明必须做图书 CRUD、分类管理、用户注册登录、借书/还书/续借、借阅记录、分页检索系统主链路缺一个都说不完整进阶做预约借书、逾期罚款、图书续借次数限制、首页统计图表增加业务深度答辩时加分可以不做的在线支付、消息通知、扫码借阅、多租户、邮件推送超出毕设范围反而容易拖累进度我的经验是先把必须做的链路跑通再补进阶项。很多同学一上来就做预约和罚款结果主流程还没通数据库表关系就乱成一团。项目进度应该是“先能借书再能管书最后才能算账”。2. 技术选型不是越新越好Java 技术栈怎么挑才能稳过答辩技术选型是答辩时老师最容易追问的部分。不要只回答“用了 Spring Boot”要能说清楚“为什么用”“替代方案是什么”“在当前场景下有什么取舍”。2.1 Servlet JSP vs Spring Boot两种路线怎么选如果你的学校要求用纯 JSP/Servlet 做课程设计那不用纠结就用传统 Servlet JSP 路线。但如果你可以自由选择我更推荐 Spring Boot因为它能帮你省掉大量配置时间让你把精力花在业务逻辑上。不过我也遇到过同学用 Spring Boot 却连 Maven 都不熟最后连项目都跑不起来。所以选型原则是你会什么就选什么但要能讲出选它的理由。用 Servlet JSP 的好处是底层流程看得清清楚楚从 request 到 response 再到 Session每一步都是自己写的答辩时逻辑非常透明缺点是自己管理连接池、事务和拦截器代码量明显增加。Spring Boot 的好处是约定大于配置社区成熟遇到问题好查缺点是如果对底层不熟被问到“Spring Boot 自动配置原理”时容易卡壳。我的最终选择是 Spring Boot 2.7 MyBatis Thymeleaf理由很简单既能体现工程化开发思路又不至于像前后端分离那样需要在 Vue 上花额外时间。如果你时间紧这个组合很稳。2.2 前端方案JSP、Thymeleaf 还是 Vue 分离这是很多人的纠结点。我的建议是没有把握不要轻易做前后端分离。JSP 适合 Servlet 技术栈但 Spring Boot 直接支持 JSP 略显别扭需要额外配置jsp-354依赖和内嵌 Tomcat 的 jsp 解析器部署时也容易踩路径坑。Thymeleaf 是 Spring Boot 的原配模板引擎写法和 HTML 很接近后端把数据塞进 Model 后直接渲染页面非常适合毕设这种中小型管理系统。Vue RESTful API 前后端分离观赏性最好但需要单独做跨域处理、Token 鉴权、前端路由相当于同时做两个项目时间成本高。我最终选了 Thymeleaf因为页面里大量表格和表单需要后端数据渲染用模板引擎最直接。Thymeleaf 也支持简单的条件判断和循环足够管理后台用了。如果你想做一点现代化的 UI可以在 Thymeleaf 页面里引入 Bootstrap 或 AdminLTE不需要学前端框架就能做出能看的界面。2.3 数据库选型与连接池配置数据库方面MySQL 8.x 是大多数人的首选免费、稳定、资料多。这里有两个容易忽略的版本细节MySQL 8.0 默认认证插件是caching_sha2_password如果 JDBC 驱动版本太老连接时会出现认证失败所以 pom 里最好显式用mysql-connector-java 8.0.33或com.mysql:mysql-connector-j8.3.0。连接池我推荐 HikariCPSpring Boot 2.x 默认就是它不需要额外引入。配置时注意maximum-pool-size毕设项目并发量不高默认 10 就够了别为了展示知识点把它设成 100。连接池不是越大越好大会增加数据库端连接开销反而容易拖慢系统。另一个参数是connection-timeout默认 30000 毫秒如果你的 SQL 执行很慢超时会先出现在连接池而不是 SQL 层面调试时别搞错了方向。2.4 环境版本搭配最容易出幺蛾子的地方版本混乱是毕设跑不起来的最大元凶。我自己踩过一个大坑本机 JDK 是 17Spring Boot 用的是 2.7结果 Lombok 版本不兼容编译时 getter/setter 全部消失排查了很久才发现是 Lombok 版本太旧。给你一份我验证过的组合组件版本JDK1.8 或 11Spring Boot2.7.18MyBatis Spring Boot Starter2.3.2MySQL8.0.33mysql-connector-java8.0.33Thymeleaf由 Spring Boot 管理Lombok1.18.30JDK 8 虽然老但是兼容性最好许多老教程里的代码都能直接用。JDK 11 也完全可以。不要为了追求 JDK 17 或 21 去碰 Spring Boot 3.xSpring Boot 3 里javax.*包改成了jakarta.*很多教程、代码示例都还是旧写法对初学者极其不友好。如果老师不强制要求新版选稳的方案才是最优解。3. 数据库设计图书租借系统的核心是“状态”不是“表”数据库设计是图书租借系统成败的关键。不要一上来就建二十张表先把核心链路里的表现出来再逐步扩展。我最终的库表结构经过三次调整才稳定下来核心就是围绕“借阅记录”这一张状态表展开。3.1 六张核心表用户、图书、分类、借阅记录、预约、罚款这六张表基本能覆盖所有必做和进阶功能user用户表字段包括 id、username、password、real_name、role、phone、status、create_time。category分类表简化为一对多图书表通过category_id关联。book图书表字段包括 id、book_name、author、publisher、isbn、category_id、total_stock、available_stock、location、status、description。borrow_record借阅记录表字段包括 id、user_id、book_id、borrow_time、due_time、return_time、status、renew_count。reservation预约表字段包括 id、user_id、book_id、reserve_time、expire_time、status。fine罚款表字段包括 id、borrow_record_id、user_id、amount、fine_time、status。核心不是表的数量而是每张表的状态字段如何与业务规则对齐。比如borrow_record.status我定义的值只有四个BORROWING、RETURNED、RENEWED、OVERDUE。这里有个细节RENEWED并不是一个独立的新记录而是原借阅记录的due_time被延长记录这个状态方便统计续借次数和履约率。3.2 借阅记录表怎么避免“同一个人同时借同一本书”这是我在设计时反复思考的一个问题。业务规则通常是一个用户同一时间只能借同一本图书一次但可以借不同图书多本。若直接用(user_id, book_id)加唯一索引会有一个问题用户归还后再次借阅同一本书无法插入新记录唯一索引会阻止插入。正确做法是建一个“部分唯一索引”或者用状态字段配合生成列。MySQL 8 支持函数索引但不是所有环境都支持更通用的做法是加一个冗余字段active_flag当前有效借阅记录为1历史记录为0然后对(user_id, book_id, active_flag)建唯一索引。插入新借阅记录前需要先将旧记录的active_flag更新为0。另一种更稳妥的方式是在应用层加锁借书前先查是否存在statusBORROWING的同用户同书记录有则拒绝。但应用层检查有并发窗口还是不够严谨。我在实际项目里用了“应用层检查 数据库唯一约束”的双保险。唯一约束用(user_id, book_id, active_flag)字段组合这样即使两个请求同时进来数据库层也能拦住重复借阅。这个设计值得你在答辩时重点讲面试官一听就知道你考虑过并发下的数据一致性。3.3 库存与在馆数量的冗余设计图书表里我同时设计了total_stock和available_stock两个字段。total_stock是图书总藏书量available_stock是当前可借数量。这个冗余字段能极大简化借书判断逻辑借书时直接用UPDATE book SET available_stock available_stock - 1 WHERE id ? AND available_stock 0既能扣减库存又能防止超借。但冗余字段有个问题如果事务回滚库存可能没有恢复如果并发高扣减可能产生负数。解决办法是扣减库存和插入借阅记录必须放在同一个事务里而且扣减库存的 SQL 必须带上available_stock 0条件。返还书籍时使用UPDATE book SET available_stock available_stock 1 WHERE id ?。有了条件更新这个保证数据库层就不会出现负库存。预约功能的库存逻辑不太一样。预约不占用available_stock因为预约只是排队。当一本书被预约后要等上一本归还并处于“可领取”状态时预约者才能借出否则预约定时过期。如果把预约也直接扣库存系统会被预约单占满真正到馆的人反而借不到书。这一点我在设计时反复调整最终采用“预约不占用库存、可借状态由借阅记录驱动”的方式才让业务闭环跑通。3.4 用 SQL 验证设计的正确性建完表不要急着写 Java先用 SQL 把核心业务跑一遍能发现很多设计问题。我当时写了三个验证查询查询某个用户的当前借阅中记录SELECT b.book_name, br.borrow_time, br.due_time FROM borrow_record br JOIN book b ON br.book_id b.id WHERE br.user_id 1 AND br.status BORROWING;查询逾期未还图书的列表SELECT u.username, b.book_name, br.due_time FROM borrow_record br JOIN user u ON br.user_id u.id JOIN book b ON br.book_id b.id WHERE br.status IN (BORROWING, RENEWED) AND br.due_time NOW();查询被预约但还没被领取的书SELECT b.book_name, u.username, r.reserve_time, r.expire_time FROM reservation r JOIN book b ON r.book_id b.id JOIN user u ON r.user_id u.id WHERE r.status WAITING AND r.expire_time NOW();如果这三条 SQL 能准确查出来说明表结构和状态字段基本靠谱。如果查询需要大量JOIN且条件写得很复杂就要考虑字段设计是否合理。比如用户姓名和书名尽量不要只存 id一定要在查询时通过连表获取不要在业务表里冗余一大段文本否则后面统计和展示会很痛苦。4. 核心功能实现从登录到借书还书代码怎么组织才不烂代码组织决定了项目能撑到多大。图书租借系统虽然体量不大但如果所有逻辑都堆在 Controller 里后期改一个功能会牵出一堆 bug。我采用的是经典的三层架构并搭配了合理的包结构。4.1 三层架构与包结构项目基础包结构如下com.example.library ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── common │ ├── annotation │ ├── exception │ └── interceptor ├── config └── utilController 只负责参数接收和响应封装不直接写业务判断Service 接口定义业务动作Impl 里实现具体逻辑Mapper 层只负责 SQL 和数据库交互。实体类用 Lombok 的Data注解减少样板代码但要注意在实体类中不要塞进大批与数据库字段无关的展示属性展示用 DTO 单独承载。有一个常见误区为了省事在 Service 里直接返回MapString, Object。这种方式前期快后期很痛苦因为所有调用者都要靠字符串 key 取值一改包你出现一堆运行时错误。建议定义ResultT统一返回结构和PageResultT分页结构刚开始多写几行后面所有接口都能复用。4.2 用户注册/登录密码加密的几种方案密码不能明文存储这是基本要求。实现方案里MD5 和 SHA-256 虽然快但容易被彩虹表破解不建议直接使用。我更推荐 BCrypt 密码哈希Spring Security 里的BCryptPasswordEncoder可以直接拿来用不需要把整个 Spring Security 引入单独引一个spring-security-crypto依赖即可。登录校验代码大概是这样的Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Autowired private BCryptPasswordEncoder passwordEncoder; Override public User login(String username, String password) { User user userMapper.findByUsername(username); if (user null || !passwordEncoder.matches(password, user.getPassword())) { throw new BusinessException(用户名或密码错误); } if (DISABLED.equals(user.getStatus())) { throw new BusinessException(账号已被禁用); } return user; } }注册时用passwordEncoder.encode()存储。BCrypt 每次生成的哈希值都不一样这没关系matches方法会自动校验。答辩时被问到“为什么不用 MD5”你可以从加盐和计算成本两个角度解释BCrypt 自带随机盐且计算强度可调能有效抵抗暴力破解。这比单纯说“MD5 不安全”有说服力得多。4.3 图书检索关键字查询和分页的最佳实践图书检索是最常见的功能坑主要出在 SQL 拼接和分页上。我用的方式是 MyBatis 动态 SQLselect idsearchBooks resultTypecom.example.library.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 AND status ACTIVE /where ORDER BY id DESC LIMIT #{offset}, #{pageSize} /select注意这里使用了CONCAT(%, #{keyword}, %)而不是字符串拼接避免 SQL 注入。分页我手写了 LIMIT没有引入 PageHelper。为什么不用 PageHelper因为毕设项目分页逻辑简单手写 LIMIT 更容易讲清楚也不会遇到 PageHelper 线程复用导致的分页串数据问题。如果你想展示“高级”一点可以自己封装一个分页参数对象PageQuery将pageNum和pageSize映射成offset然后返回带total的PageResult。4.4 借书/还书/续借事务与并发控制借书和还书是必须保证事务的操作。我用Transactional标记 Service 方法里面有三步操作校验用户状态和图书状态扣减available_stock插入借阅记录。关键是扣库存的 SQL 用条件更新而不是先查再改。之前提过的UPDATE book SET available_stock available_stock - 1 WHERE id #{bookId} AND available_stock 0如果返回值小于 1说明库存不够或书已下架直接抛异常让事务回滚。这样就不存在两个用户同时请求导致库存变负数的问题。还书流程更复杂一些更新借阅记录状态为RETURNED增加available_stock如果该图书有预约记录且处于WAITING自动将预约状态改为CAN_BORROW设置预约有效期。这里有一个业务细节还书时不能只更新记录还要检查是否有排队预约者。我用了数据库事务保证了这三步要么全部成功要么全部失败。如果代码里是三个独立的 update很可能会出现“记录已归还但库存没增加”的不一致状态。4.5 预约与逾期罚款容易被忽略的业务细节预约功能是最容易做错的地方。预约和借书有一个天然冲突同一本书A 用户借走了B 用户预约A 归还时 B 有优先借阅权。但如果 B 在预约有效期内不来领取这本书就应该释放给普通用户。我的处理是预约记录保存时带一个expire_time默认从“可领取”状态开始 24 小时。当图书归还时预约单进入CAN_BORROW如果超过expire_time还没有通过预约借出系统需要定时任务扫描并将状态改为EXPIRED同时把对应的图书重新设置为完全可借。这一步可以通过 Spring 的Scheduled注解实现把扫描逻辑写成定时任务每 30 分钟执行一次。罚款功能同样需要定时任务配合。借阅记录设置了due_time每天扫描statusBORROWING 或 RENEWED且due_time NOW()的记录将其状态改为OVERDUE同时生成一条罚款记录。罚款金额我设成了每天 0.5 元可参数化配置。不要因为在管理界面可以手动改就在代码里写死金额后期调整会非常麻烦。这个定时任务既展示了你的任务调度能力也补齐了业务完整性。5. 踩坑实录我在做这个毕设时遇到的六个典型问题做项目的过程中一定会遇到奇怪的问题。这里记录几个我亲身踩过并且解决了的坑希望能帮你省下几十个小时的排查时间。5.1 数据库连接中文乱码不是编码的锅是连接参数有一次我将图书信息插入数据库后中文全部变成了???。一开始以为是 MySQL 表字符集不对检查发现表已经是utf8mb4又以为是页面传参编码问题设置了request.setCharacterEncoding(UTF-8)也没用。最后才发现是 JDBC 连接 URL 少了参数。spring: datasource: url: jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8这组参数必须加否则 JDBC 驱动会使用 MySQL 服务端的默认字符集去处理文本。很多教程里写的是characterEncodingUTF-8但 MySQL Connector/J 8.x 更推荐utf8。加好之后重新插入中文就正常了。5.2 图书数量出现负数并发场景下的超借问题我在本地测试时开了两个浏览器窗口同时点击“借阅”结果图书的available_stock变成了 -1。原因很简单我先SELECT available_stock判断大于 0再执行 UPDATE 扣减这个窗口期足够让另一个请求也读到同样的库存两个人同时通过判断然后都执行扣减。修复就是我前面说的把扣减逻辑改成一条条件 UPDATEint rows bookMapper.decreaseStock(bookId); if (rows 0) { throw new BusinessException(库存不足或图书不可借); }MyBatis 的decreaseStock执行的就是带available_stock 0条件的更新。这个方法从根上杜绝了超借。以后遇到类似“先查再改”的场景比如优惠券扣减、商品库存扣减都可以套用这个思路。5.3 JSP 页面找不到 CSS静态资源路径与项目部署路径如果你用 Thymeleaf这个坑会少一点但如果你用 JSP一定会遇到静态资源 404 的问题。原因是页面里的 CSS 路径默认是相对当前请求路径的如果当前请求是/book/detail那hrefcss/style.css会被解析成/book/css/style.css自然 404。正确做法是在 JSP 页面里用绝对路径link relstylesheet href${pageContext.request.contextPath}/css/bootstrap.min.css在 Thymeleaf 中则用link th:href{/css/bootstrap.min.css} relstylesheet顺便说一个容易被忽略的地方Spring Boot 默认静态资源放在classpath:/static/目录不要自己改路径否则所有引用都会乱。如果你把静态资源放在WEB-INF下容器会拒绝外部访问页面同样加载不了。5.4 日期处理预约保留时间的跨天问题预约功能需要精确到小时但又不想让用户半夜看到“可领取”却没法操作所以我的expire_time设置在“第二天 22:00 过期”而不是严格 24 小时。这个需求本身简单但日期计算容易出错。Java 8 以前的Date和Calendar操作琐碎时区问题多。我推荐所有时间字段统一用LocalDateTime数据库表也使用datetime类型。计算过期时间可以这样写LocalDateTime now LocalDateTime.now(); LocalDateTime expireTime now.plusDays(1).withHour(22).withMinute(0).withSecond(0);需要注意的是LocalDateTime不带时区如果你的应用部署在服务器服务器时区和本地不一致NOW()与 Java 时间可能差几个小时。解决办法是 JDBC URL 中加serverTimezoneAsia/Shanghai让数据库和 Java 应用统一使用东八区时间。5.5 懒加载引发的 LazyInitializationException我用 MyBatis 时没有直接遇到 JPA 的懒加载问题但如果你参考的资料里有 Hibernate/JPA 写法就会碰到LazyInitializationException。这个异常的本质是在 Service 事务外访问了关联对象的懒加载属性。比如用户查询借阅记录列表时borrowRecord实体里关联了Book对象但你在 Controller 层调用record.getBook().getBookName()此时 Session 已关闭就会抛异常。解决方案有两种一是使用 DTO 投影在查询时就把需要的字段查出来不要在实体关联里绕二是配置spring.jpa.open-in-viewfalse并通过事务封闭查询。毕设项目里建议用 DTO简单直接也方便控制返回字段。5.6 浏览器缓存导致页面看不到最新数据这个坑特别隐蔽。我修改了图书信息刷新页面后看到的还是旧数据一开始以为是后端缓存查了半天发现是浏览器 HTTP 缓存。浏览器在访问静态资源和 GET 请求时如果后端返回的响应头没有禁止缓存会默认缓存。解决方式是在 Controller 层对敏感的动态页面返回时设置response.setHeader(Cache-Control, no-cache, no-store, must-revalidate); response.setHeader(Pragma, no-cache); response.setDateHeader(Expires, 0);对于一个毕设项目这样写没关系。如果想更规范可以写一个拦截器统一设置响应头或者在使用 Thymeleaf 时给静态资源 URL 加版本号参数{/js/app.js?v1.0.0}。这种做法也方便你在演示时强制刷新看到最新效果。6. 让毕设从“能用”变成“好看”测试、文档与答辩演示项目代码写完之后直接影响分数的是测试完整度、文档规范和答辩演示。这几个环节我吃了不少亏整理出来帮你提前避雷。6.1 功能测试清单照着这个表走一遍心里有底不要等到项目快提交才手动点一遍页面。建议在做完每个模块后立刻按清单测试并记录结果。我的功能测试清单大概长这样模块测试场景预期结果实际结果用户模块注册时用户名重复提示用户名已存在不创建记录通过用户模块登录时密码错误提示用户名或密码错误通过图书模块添加图书时 ISBN 重复提示 ISBN 已存在拒绝添加通过借阅模块库存为0时借书提示库存不足通过借阅模块用户已借同一本未还再次借阅提示还有未还记录通过还书模块归还后有预约者预约状态变为可领取通过罚款模块手动把 dueTime 改成昨天再触发扫描记录变为逾期生成罚款单通过测试时不要只测正常流程一定要测试异常分支和边界条件比如分页的最后一页、搜索无结果、超长时间文本等。这些都是答辩老师喜欢问的。6.2 项目文档要写什么需求说明书、数据库设计文档、测试报告很多同学觉得文档是凑字数其实文档可以成为答辩助手。我建议至少准备三份文档需求说明书包含项目背景、角色分析、用例图、功能需求列表、非功能需求。数据库设计文档包含 ER 图、表结构说明、核心查询逻辑、字段解释。测试报告包含测试环境、测试用例、测试结果、缺陷修复记录。写文档时不用追求长篇大论关键是图表和逻辑清楚。特别是 ER 图和状态图可以手工画好放进文档答辩时老师一眼就能看懂你的系统结构。比用一堆文字描述清楚得多。6.3 答辩演示脚本3分钟讲清楚你的系统演示得不好项目做得再好也容易白费。我建议按这个顺序演示登录页面演示普通用户登录说明密码是加密存储。图书检索输入关键字展示分页结果和条件查询。借书流程选中一本书点击借书展示库存减少。还书流程归还刚才借的书展示库存回升。预约流程借出一本书后再预约再用管理员视角归还展示预约可领取状态。逾期罚款展示管理后台的罚款记录列表。整个演示控制在 5 分钟内每一步只讲“做了什么”和“体现了什么设计”。不要一直敲代码或翻数据库老师想看的是系统能跑以及你对系统设计的理解。可以把数据库表结构和关键 SQL 准备好放在另一页等老师问到再展示。6.4 最后给学弟学妹的真心话图书租借系统是一个性价比很高的毕设题目它足够简单到你能独立完成又足够复杂到能展示工程能力。我个人做完这个项目最大的体会是不要迷信新技术要把业务逻辑和数据结构想清楚。项目的核心竞争力不是用了多酷的框架而是你能不能在借书还书这种高频操作中保证数据不错、状态不乱。如果你时间紧张我建议从“借阅记录”这张表入手先把主流程跑通再考虑预约和罚款这些扩展功能。代码写完一定要在别人的电脑上部署一次倒不是为了检查跨平台而是验证你的部署文档有没有缺步骤。我见过太多同学在演示现场因为application.yml里数据库账号密码不对导致项目启动失败提前准备一个启动说明能救你一命。最后想说做完一个项目最重要的不是代码多少而是你能把每个设计决策的理由讲清楚。把这个逻辑理清了答辩基本就稳了。