简介:这是一份基于Spring Boot的前后端分离图书借阅管理系统成品源码,附带配套毕业设计论文,适合计算机相关专业学生用于课程设计、毕业设计或二次开发。系统功能完整,涵盖图书录入与维护、读者信息管理、图书借阅与归还、逾期处理、图书检索以及借阅统计等模块;支持按书名、作者、ISBN等条件快速查找图书。后端采用Spring Boot整合JWT实现接口鉴权,前端基于现代Web技术栈(如Vue.js或JavaScript)构建交互界面,代码分层清晰,便于修改界面和文案。压缩包共2000个文件,大小约139.18MB,其中以1692个Markdown文档为主,用于记录项目说明和开发笔记;另有Java源码、JS脚本、XML配置、JSON数据、docx运行须知及properties配置文件,覆盖系统代码、配置、部署说明与论文素材。目前已有66人学习浏览,资源可直接配置运行。拿到该资源后,既能作为可运行的成品系统快速部署,也能通过学习源码深入理解Spring Boot项目结构、前后端分离架构、权限认证流程和图书借阅业务逻辑,同时配套文档可为论文写作提供充足参考。
1. 为什么我建议你从这套图书借阅管理系统入门 Spring Boot
如果你正卡在“跟着教程敲了个 CRUD,但一打开真实项目还是懵”的状态,springboot 图书馆管理系统这套前后端分离的图书借阅管理系统,是我见过最适合用来补课的项目。它不是一个炫技的后台模板,而是把 Spring Boot 后端、Vue 前端、MySQL 存储、JWT 鉴权、借还书业务闭环、论文文档全部串起来的完整工程。你能从里面看到真实项目怎么拆模块、怎么定接口、怎么处理借书还书这类有状态流转的业务,而不是一个个孤立 Demo。
这套系统对三类人特别对路:准备毕业设计的学生,想从前端或安卓转 Java 后端的开发者,以及公司里需要快速搭一套内部图书管理后台的工程师。它的复杂度刚好卡在“看得懂”和“有东西可挖”之间——比商城少一层支付对账,比博客多一层库存和借阅状态管理。接下来我会从架构、表设计、后端核心代码、前端联调一路拆到避坑和论文写作,全程按可复现的标准来。
2. 前后端分离的图书借阅系统:架构拆分与表设计
2.1 单体 JSP 项目 vs 前后端分离:选型背后的权衡
很多学校给的图书管理系统模板还是 JSP + Servlet 那套老架构,页面写在 Java 代码里,改个按钮都要重启 Tomcat。而标题里明确写了“前后端分离版本”,这意味着前端工程和后端工程是两个独立应用:后端只提供 JSON 接口,前端用 Vue 或 React 通过 HTTP 请求拿数据渲染页面。
我一般会建议把这套系统当成 Spring Boot 前后端分离的入门样板来看,原因有三个。第一,职责边界清楚:后端写业务逻辑和数据库交互,前端管页面交互和路由跳转,出了问题能直接定位到工程,而不是在 JSP 的 HTML 碎片里翻 Java 代码。第二,接口风格是现在企业里最主流的 RESTful,你做完这套再去看公司项目的接口文档,不会觉得陌生。第三,它的部署方式灵活——开发时前端走 Node 代理,生产时直接打成静态文件让 Spring Boot 托管,一条命令起服务。
但这套架构也有代价。最直观的是跨域问题:前端跑在 8080 端口,后端跑在 8081 端口,浏览器会拦截非同源的请求,你得配 CorsFilter 或者走代理。另外,两个工程意味着两套启动流程、两套环境变量,刚接触的人容易在“前端页面白屏但后端日志正常”这种状态下卡住。我的建议是:如果你只是为了交作业或快速跑通,别纠结微服务、Nacos 这些词,老老实实把前后端分离的“分离”二字吃透,比堆技术名词值钱得多。
2.2 数据库表设计:用户、图书、借阅记录、还书延期怎么建模
图书借阅系统的核心表,按我做过这类项目的习惯,最少要拆五张:用户表、角色表、图书表、借阅记录表、还书记录表(也可以把还书合并进借阅记录)。很多初学者喜欢把角色直接写成用户表里的一个字符串字段,比如role = 'admin',这样做小 Demo 没问题,但一旦要扩展权限粒度,比如“图书管理员只能管图书,不能管用户”,就得回头改表结构。分离角色表是成本最低的扩展预留。
借阅记录表是这套系统的业务核心,字段设计会影响后面所有接口的写法。我的经验是至少要有这几列:id、user_id、book_id、borrow_time(借出时间)、due_time(应还时间)、return_time(实际归还时间,未还则为 NULL)、status(0 在借 / 1 已还 / 2 逾期)。把“应还时间”和“实际归还时间”分开存,而不是只存一个状态值,这样计算逾期天数时直接做日期减法,不需要额外记一笔“逾期几天”。图书表里除了书名、作者、ISBN、分类,必须有一个stock字段,表示当前可借数量,每次借书成功就减一,还书就加一,这就是后面要讲的库存并发控制的基础。
用户表我建议保留create_time和update_time两个通用字段,虽然小系统里看似没用,但后面写论文画 E-R 图、做数据统计时这两个字段能派上大用场。表结构设计阶段最忌讳的是“等写到接口再补字段”,因为你一旦把数据填进去、写好接口,再回头加列就要改一堆 SQL 和实体类,血泪经验。
2.3 用 Navicat 初始化 MySQL 库表:SQL 脚本与参数说明
打开 Navicat 新建数据库时,字符集我建议直接选utf8mb4,排序规则选utf8mb4_general_ci。不要用utf8,因为 MySQL 的utf8是阉割版,存不了 Emoji 和生僻字;utf8mb4才是完整的四字节 UTF-8。下面是核心建表脚本,你直接复制到查询窗口执行即可:
-- 用户表 CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录名', `password` varchar(100) NOT NULL COMMENT 'BCrypt加密后的密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `role` varchar(20) NOT NULL DEFAULT 'USER' COMMENT '角色,ADMIN或USER', `phone` varchar(20) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 图书表 CREATE TABLE `book` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `isbn` varchar(20) DEFAULT NULL COMMENT '国际标准书号', `name` varchar(200) NOT NULL COMMENT '书名', `author` varchar(100) DEFAULT NULL, `category` varchar(50) DEFAULT NULL COMMENT '分类,如Java/文学/历史', `stock` int(11) NOT NULL DEFAULT 0 COMMENT '当前可借数量', `total_stock` int(11) NOT NULL DEFAULT 0 COMMENT '总库存', `publish_date` date DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 借阅记录表 CREATE TABLE `borrow_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `book_id` bigint(20) NOT NULL, `borrow_time` datetime DEFAULT NULL COMMENT '借出时间', `due_time` datetime DEFAULT NULL COMMENT '应还时间', `return_time` datetime DEFAULT NULL COMMENT '实际归还时间', `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0在借 1已还 2逾期', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_book_id` (`book_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;参数说明:sys_user表把username设为唯一索引,避免同一登录名重复注册;password字段长度设 100,因为 BCrypt 加密后的字符串长度约 60,设太短会导致插入失败。borrow_record里对user_id和book_id都建了普通索引,因为查询“某个用户借了哪些书”和“某本书被谁借走”是最常见的两个检索路径,没有索引的话数据量一上去就会全表扫描。status字段用tinyint而不是字符串,是为了省空间和方便条件查询,配合 Java 端的枚举类做映射即可。
注意一个细节:due_time的默认值不要设置成CURRENT_TIMESTAMP,因为它是业务字段,由后端根据借书日期加 30 天计算后写入,而不是记录创建时间。很多新手在这里偷懒直接复制上一张表的结构,导致每本借出的书应还时间都是当前时间,后面做逾期判断全部出错。
3. Spring Boot 后端落地:从自动装配到借阅核心流程
3.1 项目骨架与依赖:为什么建议选 Spring Boot 2.x 而不是 3.x
这个标题既然出现“springboot开源项目”,那项目骨架大概率是 Spring Initializr 生成的,但具体版本需要你自己拿主意。我的建议是:如果你的电脑装的是 JDK 8,直接用 Spring Boot 2.7.x;如果强行用 Spring Boot 3.x,它底层要求 JDK 17,很多学校机房和老服务器根本没装,编译阶段就会卡住。这里顺带提一句 springboot 自动装配原理:Spring Boot 的spring-boot-starter-parent帮你锁定了大量依赖版本,@SpringBootApplication注解里的@EnableAutoConfiguration会扫描META-INF/spring.factories中的配置类,按条件装配。你不需要背这些,但面试或写论文时能解释清楚“为什么引入一个 starter 就能用”是加分项。
pom.xml 里核心依赖一般是这几个:spring-boot-starter-web(Web 容器和内嵌 Tomcat)、spring-boot-starter-security(做登录鉴权,注意要关闭默认的表单登录)、mybatis-plus-boot-starter(ORM 框架,比原生 MyBatis 少写大量 XML 映射)、mysql-connector-j(MySQL 驱动)、jjwt(生成和解析 JWT token)。MyBatis-Plus 在图书管理系统里特别好用的一点是,它内置了分页插件和条件构造器,查“某分类下库存大于 0 的书”这种需求不需要手写动态 SQL。
3.2 用 JWT 做登录鉴权:拦截器配置与 token 校验逻辑
前后端分离的项目不能依赖 Session,因为前端和后端不在同一个域名下,Session 的 Cookie 默认行为会导致每次请求都带不上会话 ID。常见做法是后端生成 JWT token 返回给前端,前端存在本地存储里,每次请求在请求头加一个Authorization: Bearer <token>。
登录接口的核心逻辑很直接:接收用户名密码 -> 用 BCrypt 校验密码 -> 生成 JWT -> 返回给前端。注意密码绝对不能用 MD5,MD5 撞库太容易了,Spring Security 自带的 BCryptPasswordEncoder 就是干这个的。JWT 的生成代码我用的是 jjwt 0.11.x 版本,写法如下:
// JwtUtil.java - 生成与解析 JWT import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; public class JwtUtil { // 密钥至少要 32 个字符,生产环境请放到配置文件中且不要提交到 Git private static final String SECRET_KEY = "your-secret-key-please-change-to-long-random-string"; private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000; // 24小时有效 // 生成 token:把用户 id 和角色放进 payload public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } // 解析 token:校验签名和过期时间,返回 Claims,失败则抛异常 public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } }逻辑说明:sub字段存用户 ID,role作为自定义 claim 存角色名,后续拦截器从Claims里取角色做权限判断。parseToken抛出的ExpiredJwtException和SignatureException要由全局异常处理器捕获,转换成 HTTP 401 返回给前端,而不是直接抛出堆栈给用户看。EXPIRE_TIME设 24 小时是折中方案:太短用户老掉线,太长安全风险高,图书管理系统这个内部工具性质,24 小时完全够用。
拦截器方面,继承HandlerInterceptorAdapter或实现HandlerInterceptor,在preHandle里放行登录接口和静态资源,其余接口全部校验Authorization头。别忘了在WebMvcConfigurer里注册拦截器并配置excludePathPatterns,否则你会遇到一个经典翻车现场:登录接口本身被拦截器拦住,前端登录请求直接返回 401,页面白屏。
提示:JWT 是无状态的,服务端不知道 token 是否被注销。如果要做“管理员强制下线用户”这种功能,需要在库里维护一个 token 黑名单或者引入 Redis,小系统前期用不着,但心里要有数。
3.3 借书与还书接口:事务、状态机与并发控制
借书接口是这套系统里最容易写翻车的业务逻辑,也是论文里值得拿出来当核心模块写的点。它的流程不是“往 borrow_record 插一条记录”这么简单,必须同时做三件事:检查图书库存大于 0、扣减库存、插入借阅记录并计算应还时间。这三件事必须放在同一个事务里,任何一步失败都要回滚,否则会出现“记录插成功了但库存没减”这种脏数据。
写借书接口时,我建议用@Transactional注解标注在 service 方法上,并且注意它只对 public 方法生效。下面是关键代码,我用 MyBatis-Plus 的UpdateWrapper做条件更新,这是解决超借问题最入门也最有效的手段:
// BorrowService.java - 借书核心逻辑 @Transactional(rollbackFor = Exception.class) public boolean borrowBook(Long userId, Long bookId) { // 第一层:查询图书是否存在且可借 Book book = bookMapper.selectById(bookId); if (book == null || book.getStock() <= 0) { throw new BusinessException("图书不存在或库存不足"); } // 第二层:条件更新库存,stock > 0 是乐观锁的核心 // 这一步会返回影响行数,如果为 0 说明并发下库存已被扣完 int rows = bookMapper.update(null, new UpdateWrapper<Book>() .eq("id", bookId) .gt("stock", 0) // 关键条件:库存必须大于 0 .setSql("stock = stock - 1")); if (rows == 0) { throw new BusinessException("库存不足,借阅失败"); } // 第三层:插入借阅记录,应还时间默认借书日期 + 30 天 BorrowRecord record = new BorrowRecord(); record.setUserId(userId); record.setBookId(bookId); record.setBorrowTime(new Date()); record.setDueTime(DateUtils.addDays(new Date(), 30)); record.setStatus(0); borrowRecordMapper.insert(record); return true; }参数说明:setSql("stock = stock - 1")是让数据库自己执行加减法而不是先查出来减完再更新,避免“读-改-写”三步中的并发空隙。gt("stock", 0)是乐观锁的简化版,它的原理是:即使两个请求同时进到方法里,数据库层面也只有一个能更新成功,另一个影响行数为 0,直接抛业务异常。如果你追求更严格的并发控制,可以在表里加一个version字段用 MyBatis-Plus 的@Version注解做完整版乐观锁,但图书管理系统的并发量远没到那个程度,gt("stock", 0)已经能解决 99% 的问题。
还书接口的逻辑稍简单,但状态流转要注意:先查借阅记录,确认状态是“在借”,然后更新return_time为当前时间、status改为“已还”,最后把图书库存加回去。这里同样需要事务。逾期判断我建议在查询列表时动态计算,而不是由一个定时任务去批量改状态——这两个方案的区别在于:定时任务存在“今天没跑就漏判”的风险,而动态计算每次请求都能拿到最新结果。
3.4 自定义返回体与全局异常:统一 API 规范
前后端分离的联调效率,很大程度上取决于返回结构是否统一。我见过最痛苦的对接是:有的接口返回{code: 200, data: {...}},有的接口出错直接返回后端默认的 whitelabel 错误页,前端要写一堆 if else 判断。从第一行代码开始,就要把返回体锁死。
我一般定义Result<T>类,包含三个字段:code(200 表示成功,401 表示未登录,500 表示服务端异常)、message(人话描述)、data(业务数据)。同时配合@ControllerAdvice写全局异常处理器,把业务异常、参数校验异常、未知异常全部转成这个结构。这样前端拿到任何响应都能无脑判断code,不需要关心 HTTP 状态码和响应体格式是不是对得上。
全局异常处理器里最值得注意的是ExceptionHandler的优先级:子类异常的处理方法优先级高于父类。比如BusinessException和MethodArgumentNotValidException(参数校验)要分开写,不能只写一个Exception兜底,否则前端收到的一定是“系统异常”而不是具体原因。这套机制做好之后,前端联调会非常舒服,接口报错信息能直接弹到页面上,而不是连到后端控制台看日志。
4. Vue 前端对接与跨域处理:让页面把接口串起来
4.1 Vue + Element UI 目录结构与页面规划
前端部分按标题里的“前后端分离”,搭一个 Vue 2 + Element UI 的工程是稳妥选择。不要选 Vue 3 + Element Plus,不是它不好,而是网上图书管理系统的参考代码、博客踩坑记录绝大多数基于 Vue 2,你遇到问题能搜到现成答案。别在这里给自己增加不确定成本。
页面规划上,最少要有:登录页、图书列表页(带分页和分类筛选)、借阅记录页、我的借阅页、用户管理页(管理员)、图书新增/编辑对话框。路由要配beforeEach登录守卫,没带 token 就强制跳转登录页。我见过很多半成品项目,页面做了一大堆,路由守卫没写,结果用户直接在地址栏敲/admin就能进后台,这放在论文和答辩里是个会被追问的漏洞。
4.2 axios 封装与跨域配置:开发态、生产态两种玩法
前端调接口必须封装一个统一的 request 工具,而不是在每个页面里写一堆 axios 实例。核心作用是:请求拦截器里带上 token,响应拦截器里统一处理 401 跳登录页。示例代码如下:
// src/utils/request.js import axios from 'axios' import { Message } from 'element-ui' // 创建实例并设置超时,baseURL 走环境变量 const request = axios.create({ baseURL: process.env.VUE_APP_BASE_API || 'http://localhost:8081', timeout: 10000 }) // 请求拦截器:从 localStorage 取 token 放进请求头 request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) // 响应拦截器:统一处理 code,401 时跳回登录页 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } else { Message.error('网络异常,请稍后重试') } return Promise.reject(error) } ) export default request逻辑说明:baseURL走.env.development和.env.production两个环境文件,开发时指向后端 8081 端口,生产时用相对路径/api,这样打包后能直接放在 Spring Boot 的static目录下。Authorization头的格式Bearer <token>要和后端拦截器解析时取的 header 名保持一致,后端取AUTHORIZATION或Authorization都可以,但要注意 Spring Boot 的HttpServletRequest.getHeader()是不区分大小写的,前端写标准驼峰即可。
跨域问题分两种环境处理。开发环境下,最省事的方案不是在后端写 CorsFilter,而是在 Vue 根目录新建vue.config.js,配置 devServer 代理:
// vue.config.js module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', // 后端地址 changeOrigin: true } } } }这样前端请求/api/user/login会被代理转发到http://localhost:8081/api/user/login,浏览器看到的是同源请求,跨域问题直接从源头消失。生产环境下,前后端部署在同一端口(Vue 打包后的静态文件由 Spring Boot 托管),天然同源,根本不存在跨域。所以我一般只在后端保留一个最基础的 CorsFilter 用于特殊场景,开发主力走代理。
4.3 把 Vue 打包放进 Spring Boot:单包部署与页面刷新 404
这里重点说“vue打包放进springboot中”这个热词对应的操作。先在前端工程根目录下建一个.env.production文件,写入VUE_APP_BASE_API = '/api',然后执行npm run build,生成dist目录。接下来有两种方式放进 Spring Boot:直接把dist里的所有文件复制到src/main/resources/static/下,或者配置 Maven 插件在构建时自动拷贝。手动复制虽然笨,但最直观,适合理解原理。
但这一步做完,你马上会遇到一个经典问题:路由用的 history 模式时,在首页点着点着没毛病,一到刷新或直接访问/books这种二级路径就 404。原因是刷新时浏览器把/books发给后端,后端去找books这个 Controller 找不到,就返回 404,而前端路由是虚拟的,只有 index.html 一个入口。解决办法是在后端写一个转发规则,把所有非静态资源的路径转发到 index.html:
// WebConfig.java - 解决 Vue history 路由刷新 404 @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { // 把根路径转发到 index.html,交给 Vue 路由自行渲染 registry.addViewController("/").setViewName("forward:/index.html"); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 指定静态资源位置,Spring Boot 默认的 static 目录不够用时扩展 registry.addResourceHandler("/static/**") .addResourceLocations("classpath:/static/"); } }但如果你用的是 Spring Boot 2.6 以上版本,还要小心一个坑:当接口路径和静态资源路径冲突时,Spring Boot 的PathPattern匹配规则可能会导致静态资源请求被误判。我一般建议后端所有接口统一加/api前缀,这样前端静态资源和后端接口在路径上彻底分开,互不干扰。后面你反代到 Nginx 上也省心——直接按/api前缀转发到后端服务,其他走静态文件。
5. 系统避坑指南:从数据库乱码到图书库存超借
5.1 数据库连接乱码:Unicode 参数漏写导致中文全变问号
现象:前端页面新增的图书书名含中文,后端保存成功,但数据库里存的是???或者乱码。
原因:数据库表字符集是utf8mb4没错,但 JDBC 连接串没指定编码。MySQL 驱动默认用系统字符集跟服务端通信,Windows 下经常是 GBK,中文就被转坏了。
解决:改application.yml里的 JDBC URL,加characterEncoding=utf8和serverTimezone=Asia/Shanghai两个参数,重启后端再测试。另外注意连接串里的useSSL=false也要加上,否则高版本 MySQL 驱动会报 SSL 警告,虽然不是致命错误,但日志刷屏影响排查。
5.2 图书超借:库存校验放在纯前端,并发请求直接穿透
现象:管理员在前端页面看到某本书库存还剩 1 本,同时两个用户快速点击借阅,最终借出了 2 本,库存变成负数。
原因:前端在发请求之前就判断了book.stock > 0,但这是基于页面上的静态数据做的判断,并发场景下两个请求几乎同时到达后端,后端如果只做了“查库存再更新”的两步操作,就会存在时间差。
解决:按第 3.3 节的做法,把库存扣减写成 SQL 层面的条件更新,UPDATE book SET stock = stock - 1 WHERE id = ? AND stock > 0,并检查影响行数。影响行数为 0 就说明并发下库存已经没了,直接抛业务异常。单纯在代码里先 select 再 update,怎么加锁都有缝。
5.3 跨域配置失效:Filter 与拦截器顺序导致预检请求被拦
现象:前端axios请求后端接口,控制台报CORS policy: No 'Access-Control-Allow-Origin',后端明明配置了 CorsFilter 也不管用。
原因:Spring Boot 里如果有自定义 Filter(比如 JWT 登录过滤器),它执行顺序在 CorsFilter 之前,预检请求(OPTIONS请求)没带 token,被自定义 Filter 直接拦下返回 401,根本没走到 CorsFilter 那一步。
解决:自定义权限 Filter 的doFilter里,对OPTIONS请求无条件放行。同时可以用@Order(Ordered.HIGHEST_PRECEDENCE)给 CorsFilter 标记最高优先级。如果你在接入 Spring Security,还要注意http.cors()要开启,否则 Security 的过滤器链也会把预检请求吞掉。
5.4 时间字段偏移:Jackson 序列化导致的 8 小时时区差
现象:前端展示的借书时间是 2025-06-24 02:00,而数据库里存的是 2025-06-24 10:00,差了 8 个小时。
原因:Jackson 默认使用 GMT 时区序列化时间,而国内是 UTC+8,数据库时间被读出来后序列化时减了 8 小时。这是前后端分离项目里非常高频的一个问题,和数据库存没存对无关。
解决:在application.yml里配置两件事——第一,JDBC 连接串加serverTimezone=Asia/Shanghai;第二,设置 Jackson 的时区:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai如果用了 MyBatis-Plus 的@TableField(fill = FieldFill.INSERT)自动填充时间,额外检查一下实体类里时间字段的类型是java.util.Date而不是java.sql.Date,java.sql.Date在序列化时拿不到时分秒,会直接把时间截断到 00:00:00。
6. 给论文配图与答辩准备:把系统讲成一份技术方案
6.1 用例图、时序图、ER 图怎么画才不像是抄的
标题里那句“加论文”,实际上很多同学拿到手的是一份已经写好的 Word 文档,但它只能当模板,不能直接交。你照着它改的时候,最容易被老师盯住的就是图。如果你直接截图别人的博客配图,数据库表字段名和你的代码对不上,答辩时一问细节就露馅。我建议至少自己重画三张图。
第一张是总体用例图,画出三种角色——管理员、图书管理员、普通用户,以及他们各自能做的操作。第二张是借书时序图,重点画清楚用户、前端页面、Controller、Service、Mapper、数据库之间的调用顺序,这张图是答辩时讲核心流程的道具。第三张是 E-R 图,按第 2 章的表结构画,标清楚主键外键和一对多关系。工具直接用 PlantUML 或 ProcessOn 都行,别花时间折腾太复杂的绘图软件,重点是字段和你的代码一致。
论文的目录结构不要照搬网上那些“国内外研究现状”凑字数的大模板,按真实项目的顺序写反而更好:需求分析 -> 系统设计 -> 数据库设计 -> 核心功能实现 -> 系统测试。把 3.2 的 JWT 鉴权和 4.3 的部署配置好好写进核心功能实现里,这两个点能跟评委讲出实际的技术细节,比背十页“Java 是一种跨平台语言”有用得多。
6.2 答辩前必查的四个逻辑点:从功能演示到数据一致性
最后说四个我在指导别人答辩时一定会追问的地方。第一,演示时把后端日志窗口和数据库表都打开,借一本书,让评委看到库存从 5 变成 4、借阅记录多了一行,这比单纯点页面更有说服力。第二,主动讲一下“库存超借”你是怎么处理的,哪怕你只是在 SQL 里加了个stock > 0的条件,也要把这个设计意图说出来,这能直接体现你对业务问题的思考。第三,把 JWT 的有效期、密钥配置位置、退出登录时前端怎么删 token 这几个问题准备好答案,它们是高频追问点。第四,如果实在被问到不会的问题,不要慌着编,坦诚说“这个点我在当前版本里还没深入实现,但我了解它的基本思路是……”——这种回答比强行解释要体面得多。
我自己的习惯是每做完一个功能点,随手记一条“为什么这么做”的笔记,最后写论文和准备答辩时全部用上了,省掉大量回忆和翻代码的时间。这套图书借阅系统虽然不算什么高并发高可用的明星项目,但它把你从零到一完整搭建一个前后端分离业务系统的路径完整走了一遍,里面每个坑都是真实会踩的。希望帮到你。
本文还有配套的精品资源,点击获取