1. 社区图书管理系统的真实痛点:从需求倒推技术选型
1.1 为什么毕业设计选"社区图书管理"这个主题
每年毕业季,计算机专业的同学都要面对选题这道坎。有的选电商、有的选点餐、有的选教务管理,结果往往扎堆做到一半就发现业务复杂度超出预期——订单状态十几套、优惠券叠加逻辑绕到晕。相比之下,社区图书管理系统是一个非常"聪明"的选题:业务域清晰、角色边界明确、功能伸缩空间大。
社区图书管理与校园图书馆管理系统有本质区别。它面向的是社区居民、街道文化站、小区阅览室这类场景,单馆藏书量通常在几千到两三万册,读者数量在几百到几千人,没有复杂的馆际互借、没有采访编目流程,核心就是"图书怎么入馆、读者怎么借阅、什么时候催还、破损丢失怎么处理"。这个规模决定了系统设计可以有足够深度,又不至于失控。
而且现在的社区图书管理有一个很现实的痛点:很多街道文化站还在用Excel表格记借阅,书被人借走之后到底在谁手里、逾期多久了,全靠管理员记忆。你要做的这套系统,本质上是在替管理员解决"人脑缓存溢出"的问题。理解了这一层,你在写论文背景和需求分析的时候就有话可说了,不至于张口就是"随着信息技术的发展"这种空话。
1.2 Spring Boot为主选框架的三层理由
选Spring Boot不是因为它流行,而是它在这个场景里确实匹配。我拆成三层来说:
第一层,开发效率。 社区图书管理系统最典型的功能是增删改查加一个借还流程,Spring Boot的自动配置机制把大量的基础设施配置变成了约定。你建一个Spring Initializr项目,引入spring-boot-starter-web、spring-boot-starter-data-jpa或者MyBatis-Plus,数据库连接池、事务管理器、JSON序列化这些全部自动装配好。相比传统的SSH(Spring MVC + Spring + Hibernate)那个时代的XML配置地狱,Spring Boot让你把精力集中在业务代码上,而不是在web.xml和applicationContext.xml之间来回切换。
第二层,生态成熟度。 做毕设一定会用到权限控制、文件上传、定时任务、Excel导出这些功能。Spring Boot的生态里,权限有Spring Security和Shiro两条成熟路径,文件上传有现成的starter支持,定时任务直接用@Scheduled注解就能跑,导出Excel有EasyExcel和POI。这些组件在社区里的资料量极大,遇到问题搜一下基本都有解决方案,对没有太多项目经验的同学来说,这比用一个冷门框架可靠得多。
第三层,答辩演示的稳定性。 你可能不信,很多毕设翻车不是代码写不出来,而是演示现场环境挂了。Spring Boot内嵌Tomcat,打包成可执行Jar直接跑,不需要在答辩电脑上单独装Tomcat、配环境变量。这一点在答辩现场的实战价值,经历过的人都懂。
1.3 技术栈全景:基础框架、数据库、前端、部署
一个典型的社区图书管理系统技术栈配置如下,直接可以作为选题参考:
| 层次 | 技术选型 | 核心理由 |
|---|---|---|
| 后端框架 | Spring Boot 2.x | 自动配置、生态成熟、社区资料多 |
| 持久层 | MyBatis-Plus | 单表CRUD零SQL,分页插件好用,适合毕设快速开发 |
| 权限控制 | Spring Security + JWT | 前后端分离场景下轻量、无状态 |
| 数据库 | MySQL 5.7/8.0 | 稳定、通用、面试常问、答辩好解释 |
| 前端 | Vue 3 + Element Plus | 管理后台界面组件齐全,表格表单直接复用 |
| 文件存储 | 本地磁盘存储 | 封面图、Excel导入模板,不需要额外引入OSS |
| 文档工具 | Knife4j | 接口文档可视化,答辩演示接口时非常好用 |
| 部署 | Docker + docker-compose | 一条命令跑起整套环境,演示与论文截图都省事 |
这里有个经验之谈:技术栈不要追新。Spring Boot 2.7 + MyBatis-Plus 3.5 + MySQL 8.0这个组合,是你能在网上找到最多现成代码和报错解决方案的版本组合。选Spring Boot 3.x意味着你要跟Jakarta EE的包名变更、Java 17的最低要求打交道,这些坑对毕设来说完全没必要主动踩。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构与核心模块边界:Spring Boot在社区场景下的落地形态
2.1 单体应用还是微服务:毕业设计尺度的架构取舍
我见过不少同学在毕设里强行上微服务——一个社区图书管理系统拆出用户服务、图书服务、借阅服务三个模块,再配上Nacos注册中心、OpenFeign远程调用、Sentinel限流。看起来技术含量很高,但实际上大部分代码是在处理分布式环境下的通信和数据一致性问题,业务本身的深度反而被稀释了。
社区图书管理系统的用户量级和业务复杂度,本质上就是一个单体应用的活。Spring Boot单体架构在这个场景下的优势非常明显:事务边界清晰、调试简单、部署包只有一个、逻辑链路直观。你完全不需要分布式事务来解决借书时"扣库存和加借阅记录"的一致性问题,一个@Transactional注解就搞定了。这在答辩时反而更经得起推敲——评委问"为什么不用微服务",你可以从业务规模、运维成本、开发效率三个角度回答,而不是被问"你这个分布式事务怎么保证一致性"然后卡壳。
2.2 模块划分:图书管理、借阅管理、读者管理、统计分析
虽然是单体应用,但包结构必须按模块划分清楚。我建议一个标准的分层结构:
code复制com.community.library
── controller // 接口层
│ ├── BookController
│ ├── BorrowController
│ ├── ReaderController
│ └── AdminController
── service // 业务逻辑层
│ ├── BookService
│ ├── BorrowService
│ └── ReaderService
── mapper // 持久层接口
── entity // 数据库实体
── dto // 请求/响应对象
── config // 配置类(安全、跨域、异常处理)
── common // 公共类(统一返回结果、状态枚举、工具类)
四个核心业务模块各自负责的边界要理清楚:
图书管理模块:图书录入(单本录入、批量导入)、图书编辑、馆藏状态管理(在馆、借出、下架、破损待修复)、图书分类管理(中图法分类或者自定义分类)、封面图上传。
借阅管理模块:借书、还书、续借、预约、逾期处理。这是整个系统的核心流程,后面我会重点展开。
读者管理模块:读者注册(管理员代录或者自助注册)、读者信息维护、借阅证状态管理(正常、挂失、冻结)、借阅历史查询。
统计分析模块:馆藏总量统计、借阅热度排行、月度借阅趋势、逾期率统计。这个模块工作量不大,但是答辩演示的时候非常出彩。
这四个模块的边界要写到论文的"系统功能结构图"里,每张二级功能图下面最好配一个数据流说明。我当时写论文的时候,把每个模块至少拆到三级功能树,明确标注了哪些是管理员功能、哪些是读者功能、哪些是游客功能,答辩时评委问功能的权限归属,直接就能答上来。
2.3 RESTful API设计的约定与命名规范
接口设计直接影响后期开发和论文编写效率。RESTful风格在这类系统里的落地,我总结了一套固定的约定:
- 资源名用复数名词:
/api/books、/api/readers、/api/borrows - 查询用GET,新增用POST,修改用PUT,删除用DELETE
- 状态码语义化:200表示成功,400表示参数错误,401表示未认证,403表示无权限,404表示资源不存在,500表示服务器内部错误
- 统一返回结构:
{ code: 200, message: "success", data: {...} }
这里有个细节值得注意:借书和还书这种业务操作,很多人会设计成POST /api/borrows 表示创建借阅记录、PUT /api/borrows/{id} 表示还书。但更合适的做法是为业务动作单独定义接口,比如POST /api/borrows/borrow和POST /api/borrows/return,方法名直接表达动作语义。这样做的原因是借书操作不单单是插入一条记录,还涉及库存扣减、读者借阅数量校验、借阅状态初始化等一组操作,它是一次完整的业务行为,而不是简单的资源创建。把业务行为显式化为接口,对前端调用和答辩讲解都友好得多。
3. 数据库建模:图书、借阅、读者三张核心表的字段抉择
3.1 图书表:从ISBN到库存状态的字段设计
数据库设计是这类管理系统最见功力的环节。我先给出图书表的推荐设计,然后逐个字段说理由:
sql复制CREATE TABLE book (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
isbn VARCHAR(20) NOT NULL COMMENT '国际标准书号',
book_name VARCHAR(200) NOT NULL COMMENT '书名',
author VARCHAR(100) COMMENT '作者',
publisher VARCHAR(100) COMMENT '出版社',
publish_date DATE COMMENT '出版日期',
category_id BIGINT COMMENT '分类ID,关联book_category表',
cover_url VARCHAR(500) COMMENT '封面图片地址',
total_count INT NOT NULL DEFAULT 1 COMMENT '馆藏总量',
stock_count INT NOT NULL DEFAULT 1 COMMENT '可借数量',
shelf_location VARCHAR(50) COMMENT '书架位置,如A区-03排-05层',
status TINYINT NOT NULL DEFAULT 1 COMMENT '1在架 2下架 3破损',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
几个关键字段的取舍思路:
total_count和stock_count为什么要分开存? 这是很多新手容易做错的地方。总馆藏量和当前可借量是两码事:一本书有5本复本,被借走了2本,total_count还是5,stock_count变成3。如果只存一个库存数字,那"馆藏总量"这个统计维度就丢了。每次借还操作更新stock_count,total_count保持不变,统计报表直接查total_count的SUM就能得到馆藏总量,不需要额外维护。
isbn区分同一本书的不同复本吗? 不区分。同一本ISBN的书可能有多本复本(多副本),它们共享一条book记录,用total_count和stock_count来管理。这比每本书一条记录(用book_id区分复本)更符合社区图书管理的实际规模。当然,如果你的系统需要追踪每一本实体书的借阅历史(比如某一本被人画了记号,下次借出的人投诉),那就需要book_item表来维护单册信息。毕设尺度下,复本管理模式就够用了。
shelf_location字段很多人会忽略。 但这是社区图书管理员的真实诉求——他们要拿着系统查到的位置去架子上找书。你可以用"区域-排-层"三级编码,比如"A-03-05"表示A区第3排第5层。这个字段在答辩演示时随便录入几条,就能展示系统的实用性,比那种只做线上流程、完全不考虑物理世界的系统高一个档次。
3.2 借阅记录表:外键策略、状态机、逾期计算
借阅记录表是系统的核心表,设计的质量直接决定代码写的顺不顺手:
sql复制CREATE TABLE borrow_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
book_id BIGINT NOT NULL COMMENT '图书ID',
reader_id BIGINT NOT NULL COMMENT '读者ID',
borrow_time DATETIME NOT NULL COMMENT '借出时间',
due_time DATETIME NOT NULL COMMENT '应还时间',
return_time DATETIME NULL COMMENT '实际归还时间',
status TINYINT NOT NULL DEFAULT 1 COMMENT '1借出中 2已归还 3已续借 4已逾期 5已挂失',
renew_count INT DEFAULT 0 COMMENT '已续借次数',
operator_id BIGINT COMMENT '操作管理员ID',
remark VARCHAR(500) COMMENT '备注(破损、丢失登记)'
);
外键要不要加?我的建议是:不加物理外键,只加逻辑外键。理由是MyBatis-Plus和很多ORM框架在物理外键存在时,插入顺序和级联操作容易出问题;而且答辩演示时一旦误操作触发外键约束报错,界面报错不好看。不加外键不意味着不建索引,book_id和reader_id上一定要建普通索引,借阅记录表最常出现的查询就是"某本书当前有没有被借走"和"某个读者借了哪些书",这两个索引是查询效率的保证。
status状态机的设计是重点。 很多人把"借出中"和"已逾期"混在一起,逾期之后只靠return_time和due_time的比较来判断,这样系统里没有"逾期"这个状态,催还、罚款功能就缺少依据。正确的做法是借出中(1)和逾期(4)是两个状态,还书操作发生的时候再根据实际情况计算是否产生费用。具体状态流转逻辑:
- 借出成功 → status=1,borrow_time=now,due_time=now+30天
- 距离应还日期超过3天未还 → 状态标记为4(定时任务每天扫描)
- 还书成功 → status=2,return_time=now
- 续借 → status=3,due_time=now+15天,renew_count+1
另外一个细节:due_time(应还时间)要存储为具体时间,而不是"借期30天"这种规则。这样查询逾期记录时直接比较due_time < NOW(),SQL简单,索引也能命中,逻辑清晰。
3.3 读者表与管理员表:权限模型下的表结构配套
读者表关注的是身份信息和借阅状态:
sql复制CREATE TABLE reader (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
reader_no VARCHAR(20) UNIQUE NOT NULL COMMENT '借阅证号',
name VARCHAR(50) NOT NULL COMMENT '姓名',
phone VARCHAR(20) COMMENT '手机号',
id_card VARCHAR(30) COMMENT '身份证号(可选保存)',
address VARCHAR(200) COMMENT '住址',
register_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
status TINYINT DEFAULT 1 COMMENT '1正常 2挂失 3冻结',
borrow_count INT DEFAULT 0 COMMENT '当前在借数量',
max_borrow_limit INT DEFAULT 5 COMMENT '最大可借数量'
);
注意borrow_count和max_borrow_limit这两个字段。前者是冗余存储,每次借书+1、还书-1,省去了每次借书时COUNT(*) FROM borrow_record的统计开销。后者是业务规则配置。把max_borrow_limit设计成字段而不是写死在代码里,是为了让管理员不用改代码就能调整借阅规则——这个设计在答辩时提一句"把业务规则从代码中剥离到数据层",就是一个亮点。
管理员表呢,我的建议是跟读者表分开,不要搞成一张用户表加一个角色字段。因为读者和管理员是两个完全不同的业务域:读者有借阅证号和借阅限额,管理员有权限角色和操作日志。强行合并成一张用户表,字段会非常杂乱。典型的管理员表:
sql复制CREATE TABLE admin_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL COMMENT '登录账号',
password VARCHAR(100) NOT NULL COMMENT 'BCrypt加密后的密码',
real_name VARCHAR(50) COMMENT '姓名',
role VARCHAR(20) DEFAULT 'ADMIN' COMMENT 'SUPER_ADMIN/ADMIN',
last_login_time DATETIME
);
这里给一个建议:密码一定要用BCrypt加密存储,不要用MD5。答辩的时候如果评委问"用户密码安全怎么保证",你回答"BCrypt加盐哈希,每次校验通过rehash比较,即使数据库泄露也无法反推出明文密码",这个答案的水平就上去了。
4. 借阅核心流程的代码实现:从并发控制到状态流转
4.1 借书接口:事务边界、库存扣减、并发锁如何加
借书是整个系统最核心的接口,它包含了一整套业务规则:读者是否存在、状态是否正常、当前借阅数量是否已达上限、图书是否在架、库存是否充足。我在实现这个接口时,踩过一个非常典型的并发坑。
先看最开始的写法:
java复制@Transactional
public Result borrowBook(BorrowRequest request) {
Book book = bookMapper.selectById(request.getBookId());
if (book.getStockCount() <= 0) {
return Result.error("库存不足");
}
book.setStockCount(book.getStockCount() - 1);
bookMapper.updateById(book);
BorrowRecord record = new BorrowRecord();
record.setBookId(request.getBookId());
record.setReaderId(request.getReaderId());
record.setBorrowTime(new Date());
record.setDueTime(DateUtil.offsetDay(new Date(), 30));
record.setStatus(1);
borrowRecordMapper.insert(record);
return Result.success();
}
代码逻辑看起来没问题,但是在压测环境下(或者答辩评委模拟两个人同时借同一本书的最后一本时),问题就暴露了:A事务和B事务同时读到stock_count为1,都通过了库存校验,A扣减为0并插入借阅记录,B也扣减为0并插入借阅记录,最后这本书被借出去两次,库存变成负数。
解决这个问题有几种方案,从简单到复杂:
方案一:乐观锁。 在book表加一个version字段,更新时带上版本号判断:
sql复制UPDATE book SET stock_count = stock_count - 1, version = version + 1
WHERE id = #{bookId} AND version = #{version} AND stock_count > 0
受影响行数为0说明更新失败,需要重试或提示库存不足。这个方案简单可靠,缺点是并发时可能频繁重试。
方案二:悲观锁。 查询时用SELECT ... FOR UPDATE锁住图书行,直到事务提交才释放,其他事务只能等。对于图书管理系统的并发量(最高几十个人同时操作),悲观锁是完全可行的,而且逻辑最直观:
java复制Book book = bookMapper.selectByIdForUpdate(request.getBookId());
在Mapper里加一条selectByIdForUpdate,SQL语句为SELECT * FROM book WHERE id = #{id} FOR UPDATE,通过显式锁在@Transactional事务范围内保护这一行直到提交或回滚。
方案三:数据库唯一约束兜底。 上面两种方案配合使用更稳健。对于毕设来说,乐观锁方案好写、好讲、能在论文里画出时序图展开分析锁机制与幂等性。我现在推荐直接用方案一,理由有三:一是实现成本低,一个version字段加一条带条件的UPDATE语句就行;二是对于"最后一本书同时被借"这种极端场景已经足够;三是答辩时你对锁机制的解释会更立体——先讲乐观锁,再主动提一句"如果并发量再大可以让扣库存操作改成Redis+Lua脚本原子化",这个知识深度会让评委感觉你对并发是有完整认识的。
4.2 还书接口:逾期费用的计算与状态回滚
还书的逻辑比借书琐碎,它要处理:根据借阅记录状态判断是否可还、计算是否逾期、如果逾期要计算费用、更新图书库存、更新读者的当前借阅数量。还书接口的代码框架我梳理成这样:
java复制@Transactional
public Result returnBook(ReturnRequest request) {
BorrowRecord record = borrowRecordMapper.selectById(request.getRecordId());
if (record == null || !record.getStatus().equals(BorrowStatus.BORROWED.getCode())) {
return Result.error("借阅记录不存在或已归还");
}
// 判断是否逾期
Date now = new Date();
boolean overdue = now.after(record.getDueTime());
// 更新借阅记录
record.setReturnTime(now);
record.setStatus(overdue ? BorrowStatus.RETURNED_OVERDUE.getCode() : BorrowStatus.RETURNED.getCode());
borrowRecordMapper.updateById(record);
// 归还库存
Book book = bookMapper.selectById(record.getBookId());
book.setStockCount(book.getStockCount() + 1);
bookMapper.updateById(book);
// 读者在借数量减一
Reader reader = readerMapper.selectById(record.getReaderId());
reader.setBorrowCount(reader.getBorrowCount() - 1);
readerMapper.updateById(reader);
// 逾期费用处理
if (overdue) {
long overdueDays = DateUtil.betweenDay(record.getDueTime(), now);
OverdueRecord overdueRecord = new OverdueRecord();
overdueRecord.setBorrowRecordId(record.getId());
overdueRecord.setReaderId(record.getReaderId());
overdueRecord.setOverdueDays(overdueDays);
overdueRecord.setFineAmount(overdueDays * FINE_PER_DAY);
overdueRecordMapper.insert(overdueRecord);
}
return Result.success();
}
这段代码里有一个常见的隐藏问题:逾期判断建议以"借阅记录本身状态"为主,而不是now.after(dueTime)这个瞬时判断。因为如果某条记录状态已经是"已逾期"(定时任务标记过),即使还书时用户刚好在逾期那一刻还回来,状态判断也应该基于记录当前状态来处理。所以更严谨的写法是:先看status是否为"借出中"(1),再看是否逾期(status为4或比较时间)。核心逻辑就一句话:状态更新要以当前状态为输入,避免多条SQL之间的相互覆盖——比如你更新库存时用的是自己查出来的book对象,而不是别人的旧值,这就是为什么要加锁或版本号的原因。
4.3 续借与预约:业务规则在Service层的落地
续借其实不太需要单独建表,在borrow_record表上操作即可。核心规则是:只能续借一次、必须未逾期才能续借、续借后应还时间顺延15天。但这里有个非常容易犯的错,我把它单独拎出来说:
第一次写续借代码时,很多同学会这样写:
java复制record.setDueTime(DateUtil.offsetDay(record.getDueTime(), 15));
这是错的。原因在于如果距离到期日只剩1天才续借,原due_time加15天没问题;但如果你在借出后第2天就允许续借,那实际可借时间就变成了30+15=45天,而常规规则应该是总借期不超过45天。更合理的推算方式是:从"当前时间"开始加15天,而不是从"原应还时间"开始加15天。这样即使读者在借出后第三天续借,截止时间也是借出后18天,而不是45天。规则设计在论文里要描述清楚,评审老师经常会追问这个细节。
预约功能可以作为扩展功能写进论文的"系统特色"部分,但不建议在代码里做太多复杂实现。如果要做,只需要一张appointment表记录读者和图书的预约关系,当目标图书库存从0变成1(有人还书)时,把这本书标记为"预约保留",通知预约者在一定时间内来馆借阅即可。这个功能的重点不在代码复杂度,而在业务闭环——预约-到书-保留期-过期释放,把这一条链路讲清楚即可。
4.4 检索功能的实现:模糊查询、分页与排序的整合
图书检索是社区图书管理系统的门面功能,因为管理员和读者用得最多的就是检索。它的核心需求说起来简单——按书名、作者、ISBN模糊查询,按分类筛选,分页展示——但实现时的细节决定体验。
用MyBatis-Plus实现一个带条件的分页查询,推荐这样做:
java复制public PageResult<BookVO> searchBooks(BookQuery query) {
LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(query.getBookName()), Book::getBookName, query.getBookName())
.like(StringUtils.isNotBlank(query.getAuthor()), Book::getAuthor, query.getAuthor())
.eq(StringUtils.isNotBlank(query.getIsbn()), Book::getIsbn, query.getIsbn())
.eq(query.getCategoryId() != null, Book::getCategoryId, query.getCategoryId())
.eq(query.getStatus() != null, Book::getStatus, query.getStatus())
.orderByDesc(Book::getCreateTime);
Page<Book> page = bookMapper.selectPage(new Page<>(query.getPageNum(), query.getPageSize()), wrapper);
// 转VO、关联分类名称、计算可借状态等
return convertToPageResult(page);
}
这里有一个容易被忽略的查询性能问题:前导模糊查询导致索引失效。like '%关键字%'会全表扫描,但如果表里也就几千本藏书,全表扫描毫无压力,所以不需要过度优化。真正需要注意的是分类下拉和"仅显示可借图书"这个条件。社区场景下读者最关心的就是"现在能借什么书",所以检索列表页要默认展示"可借数量大于0"的书,并且在前端用库存进行可借状态标识——这比把所有书都列出来再由读者一本本点进去看状态,使用体验好得多。
5. 从开发到答辩:毕设项目最容易踩的坑与应对方案
5.1 环境与依赖版本兼容问题
我见过太多人在开发中期突然被环境问题卡住——Spring Boot启动报错、数据库连不上、前端页面空白,一排查就是半天。说几个高频坑位和标准解法。
Spring Boot版本与JDK版本不匹配。 如果你用的是Spring Boot 2.7.x,对应JDK 8或11;如果你选Spring Boot 3.x,最低要求是JDK 17。很多同学电脑上装了JDK 8,却从网上复制了Spring Boot 3的依赖,启动直接报UnsupportedClassVersionError。所有报错的根因要看控制台第一行堆栈信息,不是看中间那一堆红色日志。
MyBatis-Plus与MyBatis的依赖冲突。 MyBatis-Plus自带MyBatis核心依赖,如果你再显式引入mybatis-spring-boot-starter,两个版本的MyBatis会在运行时打架,表现是Mapper接口扫描不到或者SQL执行报奇怪的异常。解决办法是只保留MyBatis-Plus的starter,去掉单独的MyBatis依赖。
MySQL 8.x和5.x的驱动配置差异。 MySQL 8的驱动类是com.mysql.cj.jdbc.Driver,还要在连接串后面加serverTimezone=Asia/Shanghai,不然插入时间会差8小时。这个夏令时一样的坑每年都在发生——问题不在时区本身,而在于你没有做统一的时间规范。我建议数据库连接串必须加serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8这几个参数,项目里统一用LocalDateTime,不用java.util.Date。
前端跨域问题。 Vue跑在5173端口,Spring Boot跑在8080端口,浏览器会拦截跨域请求。解决方式两个:后端加@CrossOrigin或全局CORS配置类,前端Vite配置代理。我的建议是后端做全局CORS配置,因为答辩演示时前端可能换电脑、换IP,代理配置容易失效,后端统一放开跨域最省事。
5.2 代码层面容易被评委追问的点
答辩的时候,评委不会只看你运行成功没有,他们最喜欢问的是"这个功能怎么实现的""如果遇到XX情况会怎样"。提前准备好这几个追问点,答辩就不会慌。
线程安全问题。 前面讲的借书接口并发控制,这是评委最爱的切入点。你要能画出一个简单的环境状态图:stock_count从1被两个请求同时读到,然后说明你用乐观锁如何解决。给一个完整实现示例:
java复制@Update("UPDATE book SET stock_count = stock_count - 1, version = version + 1 " +
"WHERE id = #{bookId} AND version = #{version} AND stock_count > 0")
int deductStock(@Param("bookId") Long bookId, @Param("version") Integer version);
然后封装借书逻辑:先查询book拿到当前version,尝试更新,返回0则重试一次或者直接返回"库存紧张,请刷新后重试"。这比在Service层做复杂的分布式锁好讲得多。
事务失效的各种姿势。 @Transactional默认只对RuntimeException回滚,如果代码里catch了异常再return Result.error(),事务是不会回滚的。这个问题很多人在面试时被问过,答辩时也经常踩。比如借书过程中插入了借阅记录,然后库存扣减抛了SQLException被catch住,结果借阅记录留下了、库存没变。正确写法是:
java复制@Transactional
public Result borrowBook(BorrowRequest request) {
// 校验略
try {
int rows = bookMapper.deductStock(...);
if (rows == 0) {
throw new BusinessException("库存不足");
}
borrowRecordMapper.insert(record);
} catch (BusinessException e) {
return Result.error(e.getMessage());
}
return Result.success();
}
注意BusinessException继承RuntimeException,catch它不会阻止事务回滚。
密码安全。 前面说过用BCrypt,这里再补一句:不要自己写哈希算法,也不要加盐拼接再MD5这种"民间方案"——用Spring Security提供的BCryptPasswordEncoder,官方、可靠、答辩时一句话讲清加盐机制。
5.3 演示数据、压测准备与论文撰写协同
这部分属于"不写进代码但决定项目质量"的软实力。
演示数据要真实。 有的同学在库里就插了十几条测试数据,值守人员名单全是"张三""李四"这类占位名,答辩演示的时候界面看起来很虚。我建议用Faker或者Python脚本生成几百条真实的图书数据(用真实书名、真实ISBN格式、真实分类),配上几十个读者和几十条有借有还的借阅记录,同时留几条"已逾期未还"的记录用于演示催还功能,留一本"库存为0但可预约"的书用于演示预约流程。演示数据本身就是对需求理解的体现。
提前准备一套"演示脚本"。 在答辩现场不要临场发挥乱点。按功能设计一条主线:登录 → 图书检索(演示分页、筛选)→ 新书上架(填表单、上传封面)→ 读者注册 → 借书(检索到的书借给新读者,观察库存减少)→ 还书(借阅记录里归还,观察库存增加)→ 统计分析(展示借阅热度报表和逾期列表)。这条主线走下来也就5分钟,但覆盖了系统全部核心功能。
论文里配的UML图和数据流图,要从代码实现出发。 我见过很多同学论文中的时序图与代码逻辑对不上,评审老师一眼就能看出来。正确做法是:先写代码,再根据实际的Controller-Service-Mapper调用链画时序图;先建表,再根据实际字段画ER图;先定状态流转,再画状态机图。图服务于代码,而不是反过来。
最后再分享一个操作中总结的实用技巧:把项目的版本控制尽早交给Git,每完成一个独立功能(比如借书、还书、预约)就commit一次。这个习惯对毕设的帮助不仅在于防手误,更在于写论文"系统实现"章节的时候,你可以通过Git提交记录精确回溯到每一块功能的开发时间点,论文里写"本项目采用增量式开发模式,按功能模块迭代推进"的时候,有真实数据支撑,比空口设计听着可信得多。
