如果你正在筹备 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。
连接池我推荐 HikariCP,Spring Boot 2.x 默认就是它,不需要额外引入。配置时注意maximum-pool-size,毕设项目并发量不高,默认 10 就够了,别为了展示知识点把它设成 100。连接池不是越大越好,大会增加数据库端连接开销,反而容易拖慢系统。另一个参数是connection-timeout,默认 30000 毫秒,如果你的 SQL 执行很慢,超时会先出现在连接池而不是 SQL 层面,调试时别搞错了方向。
2.4 环境版本搭配:最容易出幺蛾子的地方
版本混乱是毕设跑不起来的最大元凶。我自己踩过一个大坑:本机 JDK 是 17,Spring Boot 用的是 2.7,结果 Lombok 版本不兼容,编译时 getter/setter 全部消失,排查了很久才发现是 Lombok 版本太旧。
给你一份我验证过的组合:
| 组件 | 版本 |
|---|---|
| JDK | 1.8 或 11 |
| Spring Boot | 2.7.18 |
| MyBatis Spring Boot Starter | 2.3.2 |
| MySQL | 8.0.33 |
| mysql-connector-java | 8.0.33 |
| Thymeleaf | 由 Spring Boot 管理 |
| Lombok | 1.18.30 |
JDK 8 虽然老,但是兼容性最好,许多老教程里的代码都能直接用。JDK 11 也完全可以。不要为了追求 JDK 17 或 21 去碰 Spring Boot 3.x,Spring 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。另一种更稳妥的方式是在应用层加锁:借书前先查是否存在status='BORROWING'的同用户同书记录,有则拒绝。但应用层检查有并发窗口,还是不够严谨。
我在实际项目里用了“应用层检查 + 数据库唯一约束”的双保险。唯一约束用(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 里直接返回Map<String, Object>。这种方式前期快,后期很痛苦,因为所有调用者都要靠字符串 key 取值,一改包你出现一堆运行时错误。建议定义Result<T>统一返回结构和PageResult<T>分页结构,刚开始多写几行,后面所有接口都能复用。
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 动态 SQL:
<select id="searchBooks" resultType="com.example.library.entity.Book"> SELECT * FROM book <where> <if test="keyword != null and keyword != ''"> AND (book_name LIKE CONCAT('%', #{keyword}, '%') OR author LIKE CONCAT('%', #{keyword}, '%') OR isbn LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="categoryId != 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,每天扫描status='BORROWING' 或 '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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/ShanghaiuseUnicode=true&characterEncoding=utf8这组参数必须加,否则 JDBC 驱动会使用 MySQL 服务端的默认字符集去处理文本。很多教程里写的是characterEncoding=UTF-8,但 MySQL Connector/J 8.x 更推荐utf8。加好之后,重新插入中文就正常了。
5.2 图书数量出现负数:并发场景下的超借问题
我在本地测试时,开了两个浏览器窗口同时点击“借阅”,结果图书的available_stock变成了 -1。原因很简单:我先SELECT available_stock判断大于 0,再执行 UPDATE 扣减,这个窗口期足够让另一个请求也读到同样的库存,两个人同时通过判断,然后都执行扣减。
修复就是我前面说的,把扣减逻辑改成一条条件 UPDATE:
int 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,那href="css/style.css"会被解析成/book/css/style.css,自然 404。
正确做法是在 JSP 页面里用绝对路径:
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/bootstrap.min.css">在 Thymeleaf 中则用:
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">顺便说一个容易被忽略的地方: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 中加serverTimezone=Asia/Shanghai,让数据库和 Java 应用统一使用东八区时间。
5.5 懒加载引发的 LazyInitializationException
我用 MyBatis 时没有直接遇到 JPA 的懒加载问题,但如果你参考的资料里有 Hibernate/JPA 写法,就会碰到LazyInitializationException。这个异常的本质是:在 Service 事务外访问了关联对象的懒加载属性。
比如用户查询借阅记录列表时,borrowRecord实体里关联了Book对象,但你在 Controller 层调用record.getBook().getBookName(),此时 Session 已关闭,就会抛异常。解决方案有两种:一是使用 DTO 投影,在查询时就把需要的字段查出来,不要在实体关联里绕;二是配置spring.jpa.open-in-view=false并通过事务封闭查询。毕设项目里建议用 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?v=1.0.0}。这种做法也方便你在演示时强制刷新看到最新效果。
6. 让毕设从“能用”变成“好看”:测试、文档与答辩演示
项目代码写完之后,直接影响分数的是测试完整度、文档规范和答辩演示。这几个环节我吃了不少亏,整理出来帮你提前避雷。
6.1 功能测试清单:照着这个表走一遍,心里有底
不要等到项目快提交才手动点一遍页面。建议在做完每个模块后立刻按清单测试,并记录结果。我的功能测试清单大概长这样:
| 模块 | 测试场景 | 预期结果 | 实际结果 |
|---|---|---|---|
| 用户模块 | 注册时用户名重复 | 提示用户名已存在,不创建记录 | 通过 |
| 用户模块 | 登录时密码错误 | 提示用户名或密码错误 | 通过 |
| 图书模块 | 添加图书时 ISBN 重复 | 提示 ISBN 已存在,拒绝添加 | 通过 |
| 借阅模块 | 库存为0时借书 | 提示库存不足 | 通过 |
| 借阅模块 | 用户已借同一本未还,再次借阅 | 提示还有未还记录 | 通过 |
| 还书模块 | 归还后有预约者 | 预约状态变为可领取 | 通过 |
| 罚款模块 | 手动把 dueTime 改成昨天,再触发扫描 | 记录变为逾期,生成罚款单 | 通过 |
测试时不要只测正常流程,一定要测试异常分支和边界条件,比如分页的最后一页、搜索无结果、超长时间文本等。这些都是答辩老师喜欢问的。
6.2 项目文档要写什么:需求说明书、数据库设计文档、测试报告
很多同学觉得文档是凑字数,其实文档可以成为答辩助手。我建议至少准备三份文档:
- 需求说明书:包含项目背景、角色分析、用例图、功能需求列表、非功能需求。
- 数据库设计文档:包含 ER 图、表结构说明、核心查询逻辑、字段解释。
- 测试报告:包含测试环境、测试用例、测试结果、缺陷修复记录。
写文档时不用追求长篇大论,关键是图表和逻辑清楚。特别是 ER 图和状态图,可以手工画好放进文档,答辩时老师一眼就能看懂你的系统结构。比用一堆文字描述清楚得多。
6.3 答辩演示脚本:3分钟讲清楚你的系统
演示得不好,项目做得再好也容易白费。我建议按这个顺序演示:
- 登录页面:演示普通用户登录,说明密码是加密存储。
- 图书检索:输入关键字,展示分页结果和条件查询。
- 借书流程:选中一本书点击借书,展示库存减少。
- 还书流程:归还刚才借的书,展示库存回升。
- 预约流程:借出一本书后再预约,再用管理员视角归还,展示预约可领取状态。
- 逾期罚款:展示管理后台的罚款记录列表。
整个演示控制在 5 分钟内,每一步只讲“做了什么”和“体现了什么设计”。不要一直敲代码或翻数据库,老师想看的是系统能跑,以及你对系统设计的理解。可以把数据库表结构和关键 SQL 准备好放在另一页,等老师问到再展示。
6.4 最后给学弟学妹的真心话
图书租借系统是一个性价比很高的毕设题目,它足够简单到你能独立完成,又足够复杂到能展示工程能力。我个人做完这个项目最大的体会是:不要迷信新技术,要把业务逻辑和数据结构想清楚。项目的核心竞争力不是用了多酷的框架,而是你能不能在借书还书这种高频操作中保证数据不错、状态不乱。
如果你时间紧张,我建议从“借阅记录”这张表入手,先把主流程跑通,再考虑预约和罚款这些扩展功能。代码写完一定要在别人的电脑上部署一次,倒不是为了检查跨平台,而是验证你的部署文档有没有缺步骤。我见过太多同学在演示现场因为application.yml里数据库账号密码不对导致项目启动失败,提前准备一个启动说明能救你一命。最后想说,做完一个项目,最重要的不是代码多少,而是你能把每个设计决策的理由讲清楚。把这个逻辑理清了,答辩基本就稳了。