这些年校园、社区里的“共享书角”越来越多,一个书架、几本旧书就能撑起一个角落,但真正运营起来才发现问题一大堆:谁借了哪本书、借了多久、该还了没有、书在哪个人手里,全靠一本纸质登记本或者Excel表格,管理员累,借书的人也不方便。我自己就经历过帮社团管理图书角,翻登记本翻到崩溃的时期,所以才下定决心动手做一个真正能用的图书借还管理系统。
这个项目就是用SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0这套主流技术栈,从零开发的一套前后端分离的图书借还管理系统。管理员可以维护书库、管理读者、处理借书还书、查看逾期记录;普通用户可以在线浏览书单、查询个人借阅历史。整个项目包括完整的源码、数据库脚本和配套文档,拿来直接运行、二次开发,或者当毕业设计、练手项目都非常合适。适合正在学Java Web的开发者、做课程设计的学生,以及确实需要一套轻量图书管理工具的个人或小团体。
1. “共享书角”到底在解决什么问题
1.1 纸质登记模式的三宗罪
先别急着聊技术,你得先搞清楚这个系统服务的对象是谁。共享书角的特点是:书不多,几十到几百本;场地随意,走廊角落、活动室、咖啡吧都有可能;管理员通常是兼职,可能是学生会干事、前台小姐姐、社团负责人,不可能天天盯着系统做复杂操作。
纸质登记模式有几个致命痛点。第一,借还记录不实时,一本书被借走之后,其他人根本不知道这本书当前在哪里,很容易出现“书架上明明有位置但书没了”的情况。第二,逾期不还全靠管理员记忆力,借出去一个月的书没人催,等想起来的时候书已经找不到了。第三,盘点极痛苦,年底想统计一下哪些书最受欢迎、哪些书丢失了,对着登记本手工数,基本等于重新整理一遍。
这些问题落到技术上,其实就是三个需求:图书信息的数字化管理、借还流程的规范化记录、数据统计与逾期提醒的自动化。这也是“共享书角”管理系统最核心的价值所在。
1.2 功能拆解:从借书到统计的一条完整链路
做系统最忌讳一上来就写代码,我习惯先把用户故事走一遍。这套系统我从角色和动作两个维度拆解,功能就非常清晰了。
管理员侧的核心动作:
- 图书管理:新增书籍、编辑信息、下架损坏或丢失的书、按书名/作者/ISBN检索
- 读者管理:录入读者信息、查看借阅历史、冻结违规账户
- 借阅处理:登记借书、登记还书、处理续借、标记逾期
- 统计看板:图书总量、借出数量、逾期数量、热门图书排行
用户侧的核心动作:
- 浏览书单:通过分类、关键字筛选想看的书
- 查看详情:图书简介、库存状态、可借数量
- 个人中心:自己的借阅记录、当前在借图书、历史记录、逾期状态
这里面有一个容易被忽略但是很重要的设计点:借书这个动作由谁来操作?我在最初设计时纠结过“用户自助借书”还是“管理员代登记”。后来实际走访了几个书角的运营场景,发现如果是开放式书角,用户自助借书会导致极大的丢书率——人都有侥幸心理,自己扫码登记万一漏了没人知道。所以最终方案是:用户提交借阅申请,管理员审核之后确认借出,形成完整的闭环责任链。这个决策对系统的权限模型影响很大,后面会细说。
1.3 角色权限与数据模型的关系
权限模型是这个系统最基础的地基。我用的方案是经典的RBAC(基于角色的访问控制)简化版,不做细粒度的权限点,只区分两个角色:管理员和普通读者。
用户表里用一个 role 字段区分角色,比如 0 表示普通读者,1 表示管理员。这个模型在初期完全够用,而且实现成本极低。读者只能操作自己的借阅记录和个人信息,管理员拥有全部业务操作权限。前端路由根据角色做动态过滤,后端接口用拦截器做统一鉴权,双重保证。
这里分享一下权限设计的心得:很多初学者喜欢一开始就上 Spring Security + JWT + RBAC 完整权限模型,做了一大堆角色表、菜单表、权限表,结果业务还没开始写,光权限配置就劝退了自己。小项目就应该用小项目的做法,先用一个简单的拦截器 + 用户角色字段搞定,等系统真正需要多角色、细粒度权限时再升级。架构是为业务服务的,不是为了炫技。
2. 为什么坚持选这套技术栈
2.1 SpringBoot2还是SpringBoot3,要看你敢不敢冒险
标题里写的是SpringBoot2,很多人会问:现在SpringBoot3都出来这么久了,为什么还选2?这个问题的答案其实很现实。
SpringBoot3底层依赖的是JDK17,而SpringBoot2.7是JDK8的最后一个主要版本。JDK8在企业的装机量依然恐怖,很多学校的机房、公司老服务器上都还是JDK8。如果你的目标用户是学生做毕设、中小企业内部工具,JDK8+SpringBoot2的兼容性最稳妥,部署到服务器上不会因为JDK版本不一致直接起不来。
另外,SpringBoot2.7本身已经是2.x的最终版本,功能上非常成熟,该踩的坑网上全都有答案。结合2.7内置的Spring Framework 5.3,配合MyBatis-Plus、Druid连接池、Swagger文档生成这些老牌组件,稳定性极高。对于图书借还这种并发量极低的管理系统,SpringBoot2.7的性能余量绰绰有余。
我这个项目用的是SpringBoot 2.7.x + JDK8 + Maven,这也是目前国内Java Web项目最常见、最不容易出问题的一套组合。
2.2 MyBatis-Plus凭什么叫“效率神器”
MyBatis-Plus(简称MP)和原生MyBatis的区别,一句话总结就是:MyBatis让你自己写SQL,MyBatis-Plus帮你把80%的增删改查都自动生成好了。
你要是用过原生MyBatis,一定写过类似的痛苦代码:写一个Mapper接口,再写对应的XML文件,里面是一堆重复性极高的selectById、insert、updateById。一个图书模块就要写小几十行样板SQL,三个模块下来全是体力活。
MyBatis-Plus的BaseMapper接口直接内置了insert、deleteById、updateById、selectById、selectList、selectPage等常用方法。你的Mapper接口只需要继承BaseMapper,CRUD就全有了。比如图书管理的Mapper,核心代码就一行:
public interface BookMapper extends BaseMapper<Book> { }加上MyBatis-Plus的条件构造器QueryWrapper和LambdaQueryWrapper,多条件查询、模糊搜索、排序、分页都变得非常优雅。比如按书名字段模糊搜索:
LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), Book::getTitle, keyword) .eq(Book::getStatus, 1) .orderByDesc(Book::getCreateTime); List<Book> bookList = bookMapper.selectList(wrapper);LambdaQueryWrapper的一大好处是类型安全,字段名用方法引用Book::getTitle而不是字符串"title",编译阶段就能发现字段名拼写错误,重构的时候也不容易漏改。这个项目里所有复杂查询我都优先用Lambda方式,代码可读性和维护性都提高了不止一个档次。
另外MyBatis-Plus的分页插件也很省心。配置一个PaginationInnerInterceptor,然后用Page对象一包,分页数据就出来了:
Page<Book> page = new Page<>(current, size); IPage<Book> result = bookMapper.selectPage(page, wrapper);返回结果里total、pages、records全都齐了,前端分页组件直接对接。
2.3 Vue3组合式API带来的前端体验升级
前端我选的是Vue3 + Vite + Element Plus + Pinia这套组合。Vue3和Vue2最核心的区别,就是组合式API(Composition API)的引入。
在Vue2里写一个页面的逻辑,你得把data、methods、computed、watch分散在不同选项里。功能少还好,功能一多,一个组件几百行代码,你要看一个业务逻辑的完整流程,得在四个选项之间来回跳,非常痛苦。
Vue3组合式API把同一业务的变量和方法聚在一起,代码组织方式从“按类型分”变成了“按业务分”。比如图书列表中跟加载数据相关的逻辑可以写在一起:
const bookList = ref([]) const loading = ref(false) async function loadBookList() { loading.value = true try { const res = await getBookList({ current: page.value, size: pageSize.value }) bookList.value = res.records total.value = res.total } finally { loading.value = false } }变量、函数、生命周期都集中在一起,模块化程度更高。配合Vite的热更新,开发体验和Vue2时代完全不是一个量级。
前端状态管理用的Pinia是Vue3官方推荐的新一代状态管理库,相比Vuex语法更简洁,去掉了mutations概念,直接可以在actions里同步修改state。配合setup store的写法,写起来就跟写普通组合式函数一样自然。这个项目里主要用Pinia存储用户登录状态和角色信息:
export const useUserStore = defineStore('user', () => { const token = ref('') const userInfo = ref({}) function setToken(value) { token.value = value localStorage.setItem('token', value) } return { token, userInfo, setToken } })UI组件库用的Element Plus,表格、表单、弹窗、分页、消息提示这些后台管理系统的高频组件都有现成的,样式也统一,改起来非常省事。
2.4 MySQL8.0有哪些真正用得上的新特性
数据库选MySQL8.0,最直接的感受就是安装和使用的便利性提高了。MySQL8.0的安装包在Windows和Linux上都有很完善的向导流程,默认字符集已经是utf8mb4,不需要像5.7时代那样手动改配置文件支持emoji和生僻字。
另外几个8.0特性在日常开发中很实用。窗口函数(Window Function)让排行榜、累计值这类查询变得异常简单。比如统计图书借阅排行,按借阅次数排个名次,一条SQL就搞定了:
SELECT title, borrow_count, RANK() OVER (ORDER BY borrow_count DESC) AS rank_no FROM book这在5.7里写起来就得用复杂的临时表或者子查询,而在8.0里直接内置支持。事务方面,8.0默认的事务隔离级别是REPEATABLE READ,同时InnoDB的MVCC机制保证了读写不互相阻塞,对于图书借还这种读多写少的场景完全够用。
还一个被很多人忽略的点是MySQL8.0官方文档的质量很高,遇到问题去查官方参考手册,很多参数都有详细的行为说明,排查问题效率高很多。
3. 数据库设计与后端核心实现
3.1 不绕弯子的四张核心表
图书借还系统的数据结构不复杂,核心就是四张表:用户表、图书表、借阅记录表、分类表。我单独加上分类表是因为图书分类在共享书角场景下是强需求,读者浏览时喜欢按分类筛选,如果只靠书名搜索,体验会很差。
用户表(sys_user)核心字段:
CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', password VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)', real_name VARCHAR(50) COMMENT '姓名', role TINYINT NOT NULL DEFAULT 0 COMMENT '角色:0-读者 1-管理员', phone VARCHAR(20), status TINYINT DEFAULT 1 COMMENT '状态:1-正常 0-冻结', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '系统用户表';图书表(book)核心字段:
CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT '书名', author VARCHAR(100) COMMENT '作者', isbn VARCHAR(30) COMMENT 'ISBN号', category_id BIGINT COMMENT '分类ID', publisher VARCHAR(100) COMMENT '出版社', total_count INT DEFAULT 1 COMMENT '总数量', available_count INT DEFAULT 1 COMMENT '可借数量', status TINYINT DEFAULT 1 COMMENT '1-在架 0-下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '图书表';借阅记录表(borrow_record)是整个系统的业务核心:
CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_id BIGINT NOT NULL COMMENT '图书ID', user_id BIGINT NOT NULL COMMENT '借阅人ID', borrow_time DATETIME NOT NULL COMMENT '借出时间', due_time DATETIME NOT NULL COMMENT '应还时间', return_time DATETIME COMMENT '实际归还时间', status TINYINT NOT NULL DEFAULT 0 COMMENT '0-借出中 1-已归还 2-逾期未还 3-已续借', renew_count INT DEFAULT 0 COMMENT '续借次数', operator_id BIGINT COMMENT '操作管理员ID' ) COMMENT '借阅记录表';这里有个容易踩的坑:图书的“总数量”和“可借数量”必须分开。因为共享书角的书经常存在同一本书多册的情况,比如《三体》三部曲三册,或者同一本书捐赠了两本。借出时 available_count 减一,归还时加一,同时要保证 available_count 永远大于等于0,这个约束在后端业务代码里必须做校验。
借阅记录表的status字段我设计了几个状态,借出中、已归还、逾期未还、已续借。为什么把逾期未还单独作为一个状态而不是用借出中加逾期标记?因为查询“当前有哪些书逾期了”是这个系统最高频的管理端操作之一,独立状态字段可以直接走索引查询,效率更高,业务逻辑也更直白。
3.2 登录鉴权的正确姿势:拦截器+JWT
登录鉴权这块,我用的是JWT + Spring MVC拦截器方案,没有引入Spring Security。理由前面说过,小系统用简单方案,降低理解和维护成本。
前端在登录页输入账号密码,后端验证通过后签发一个JWT,包含用户ID、用户名、角色三样信息,有效期24小时。前端拿到token存到localStorage,每次请求通过axios拦截器自动加到请求头:
service.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers['Authorization'] = 'Bearer ' + userStore.token } return config })后端写一个JwtInterceptor实现HandlerInterceptor接口,在preHandle方法里校验token:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); try { Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { // token过期或无效,返回401 } } response.setStatus(401); return false; }为什么用拦截器而不是过滤器?因为拦截器可以获取HandlerMethod信息,更方便地做接口级别的精细控制。另外,还要在WebMvcConfigurer里配置拦截器的拦截路径和放行路径:
registry.addInterceptor(jwtInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/api/auth/register");这样一个简单可靠的鉴权链路就完成了。密码加密用的是BCrypt,不是MD5。MD5加盐虽然也能用,但BCrypt是一种自适应哈希算法,通过内置随机盐和计算强度参数,能有效抵抗彩虹表攻击,而且Spring Security的BCryptPasswordEncoder可以直接拿来单独使用,不需要引入整套安全框架。
3.3 借阅和归还的核心事务逻辑
借书和还书是整个系统业务逻辑最重的两个接口,也是我必须写事务的地方。
借书接口的逻辑是这样的:
- 校验读者状态是否正常
- 校验图书是否存在且在架
- 校验可借数量大于0
- 检查该读者是否有未归还的同名图书(防止恶意多借)
- 创建借阅记录,借出时间now,应还时间now+30天
- 图书可借数量减1
- 如果读者借阅数量超过上限(默认5本),拒绝借出
这些步骤必须包在同一个事务里,任何一步失败都要整体回滚,否则会出现“借阅记录创建了但库存没减”这种脏数据。用@Transactional注解搞定:
@Transactional(rollbackFor = Exception.class) public void borrowBook(Long bookId, Long userId) { User user = userMapper.selectById(userId); if (user == null || user.getStatus() != 1) { throw new BusinessException("读者不存在或已被冻结"); } Book book = bookMapper.selectById(bookId); if (book == null || book.getStatus() != 1) { throw new BusinessException("图书不存在或已下架"); } if (book.getAvailableCount() <= 0) { throw new BusinessException("该图书暂无可借数量"); } Long activeBorrowCount = borrowRecordMapper.getActiveBorrowCount(userId); if (activeBorrowCount >= MAX_BORROW_LIMIT) { throw new BusinessException("已达到最大借阅数量"); } // 同一本书只能借一本 Long sameBookCount = borrowRecordMapper.getActiveBorrowCountByBook(userId, bookId); if (sameBookCount > 0) { throw new BusinessException("您已借过这本书,请先归还"); } BorrowRecord record = new BorrowRecord(); record.setBookId(bookId); record.setUserId(userId); record.setBorrowTime(new Date()); record.setDueTime(DateUtils.addDays(new Date(), 30)); record.setStatus(0); borrowRecordMapper.insert(record); book.setAvailableCount(book.getAvailableCount() - 1); bookMapper.updateById(book); }这里我单独封装了BusinessException作为业务异常,配合全局异常处理器@RestControllerAdvice,可以统一返回前端友好的错误格式,而不是一长串的堆栈信息。这个习惯建议所有Java Web开发者养成,对接口联调和后期维护帮助巨大。
还书逻辑相对简单,但也要注意状态一致性:只能归还“借出中”的记录,归还时设置return_time和status=1,图书可借数量加1。同时要处理一种特殊情况——逾期归还,也要正常走还书流程,但系统需要额外记录一条逾期标记,方便以后统计。最好的做法是还书时判断当前时间是否晚于due_time,如果是,在返回信息里携带一个overdue字段,前端弹窗提示“已逾期X天”。
4. 前端实现与联调要点
4.1 前端工程搭建和目录结构
前端用的是Vite + Vue3。创建项目一条命令就搞定:
npm create vite@latest book-frontend -- --template vue然后安装项目依赖:
cd book-frontend npm install npm install element-plus @element-plus/icons-vue npm install axios pinia vue-router npm install sass -D目录结构我习惯按模块分包而不是按文件类型分包。src下分api、router、stores、views、components五个目录。api目录按业务模块拆文件,比如book.js、borrow.js、user.js,每个文件导出对应的请求函数。views目录下的页面组件跟路由一一对应,看一眼目录结构就知道有哪些页面。
在实际开发中,这种组织方式比按“components/xxx.vue”平铺要容易维护得多。找代码的时候,先去views找页面,页面里引用的接口去api目录找,状态去stores找,思路非常清晰。
4.2 图书管理列表页的完整实现
图书管理页面是整个前端最核心的页面,涉及条件搜索、分页、表格展示、新增编辑弹窗、下架操作等完整功能。我用它来展示Vue3组合式API的组织方式。
[[views/book/BookList.vue]]里先写列表加载逻辑:
<script setup> import { ref, onMounted } from 'vue' import { getBookPage, deleteBook } from '@/api/book' import { ElMessage, ElMessageBox } from 'element-plus' const loading = ref(false) const bookList = ref([]) const total = ref(0) const queryParams = ref({ current: 1, size: 10, keyword: '', categoryId: null, status: null }) async function loadBookList() { loading.value = true try { const { data } = await getBookPage(queryParams.value) bookList.value = data.records total.value = data.total } finally { loading.value = false } } function handleSearch() { queryParams.value.current = 1 loadBookList() } function handleReset() { queryParams.value = { current: 1, size: 10, keyword: '', categoryId: null, status: null } loadBookList() } onMounted(loadBookList) </script>模板结构就是el-form搜索区、el-table数据展示区、el-pagination分页区三大块。这个页面涉及的组件交互比较多,我把新增和编辑共用一个弹窗组件,通过一个isEdit标志区分,避免两个弹窗组件逻辑重复。
这里要重点说一个实战技巧:表格里的图书封面、状态标签、库存颜色这些信息,在接口返回时最好让后端把展示文本一起返回。比如status字段返回数字的话,前端需要自己维护一个映射对象:
const statusMap = { 0: { text: '已下架', type: 'info' }, 1: { text: '在架', type: 'success' } }而更省心的方式是后端在VO(视图对象)里直接带statusName字段,前端直接渲染。这两种方案我权衡过,小项目其实都行,但如果你希望前端代码尽量简单,推荐后端多做一个字段映射。前端就变成了一行模板渲染:
<el-tag :type="row.statusName === '在架' ? 'success' : 'info'"> {{ row.statusName }} </el-tag>4.3 前后端联调时的接口规范与跨域处理
联调阶段,接口规范越早定越好。我约定所有接口统一返回R对象结构:
{ "code": 200, "message": "success", "data": { ... } }前端axios响应拦截器统一处理:
- code为200时直接返回data部分,业务代码不需要每个接口都做一次解包
- code为401时跳转登录页
- code为500时ElMessage弹出后端返回的错误信息
这样一来,业务代码里拿到的直接就是有效数据,大量重复的错误处理被收敛到拦截器里,代码清爽非常多。
跨域问题是前后端分离项目逃不掉的一关。前端开发时Vite服务器跑在5173端口,后端接口在8080端口,浏览器的同源策略会直接拦下请求。解决方式有两种:后端加CORS全局配置,或者前端用Vite代理。
我推荐前端Vite代理方案,因为开发时前端配置proxy,后端完全不需要感知跨域问题,生产环境通过Nginx做同源反代,这是最标准的姿势。Vite配置:
// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })在这个配置下,前端请求/api/xxx会被代理转发到后端8080端口,浏览器看到的请求是同源的,跨域问题直接消失。
5. 环境搭建、部署与典型问题排查
5.1 从安装到跑起来的全套步骤
如果你从零开始搭这套环境,我按实际操作的先后顺序给你列一遍:
第一步,安装MySQL 8.0。Windows下用安装包一路点下一步就行,安装时注意选择字符集为utf8mb4,设置root密码,最好单独创建一个普通用户供应用连接,不要所有应用都拿root连接数据库。数据库创建之后导入项目中的init.sql脚本,表结构和初始数据(包含一个默认管理员账号)就都有了。
第二步,安装JDK8和Maven。JDK8建议用官方版本或Adoptium的OpenJDK发行版,配好JAVA_HOME环境变量。Maven用3.8.x版本,settings.xml配置好阿里云镜像仓库,否则依赖下载会等到怀疑人生。
第三步,后端启动。用IDEA打开后端项目,等Maven导入依赖完成后,修改application.yml里的数据库连接信息:
spring: datasource: url: jdbc:mysql://localhost:3306/book_corner?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password运行主启动类BookCornerApplication,看到Spring Boot启动成功的日志后,用接口测试工具验证登录接口是否正常。
第四步,前端启动。进入book-frontend目录,npm install安装依赖,npm run dev启动开发服务器,浏览器访问localhost:5173,前端代理会把/api请求转发到8080后端。
这里有一个特别提醒:MySQL8.0默认的驱动类已经从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver,连接URL需要显式带上serverTimezone参数,否则会报时区相关的错误。另外SpringBoot2.7默认自带的是mysql-connector-j 8.0版本驱动,不需要额外手动引入驱动依赖,如果你在pom里看到旧版的mysql-connector-java,最好去掉以免版本冲突。
5.2 常见问题与排查技巧速查
我整理了一份问题排查速查表,这些问题全部来自实际开发中踩过的坑:
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 启动时报数据库连接失败 | 数据库未启动、连接URL错误、密码不对 | 先确保MySQL服务已启动,用客户端工具测试连接 |
| 登录接口返回401 | JWT生成和校验的盐值不一致、token过期 | 检查application.yml中jwt配置,确认前后端请求头字段名一致 |
| 跨域请求被拦截 | 未走Vite代理、Nginx未配置 | 开发环境检查vite.config.js的proxy配置,生产环境检查Nginx location配置 |
| 中文乱码 | 数据库字符集不是utf8mb4、连接URL缺少encoding参数 | 检查建库语句和连接URL,确保CHARACTER SET utf8mb4 |
| 前端页面打不开 | Node版本过老或过新、npm install失败 | 确认Node版本在16.x以上,删除node_modules重新install |
| 返回的查询结果多了deleted字段 | MyBatis-Plus逻辑删除配置生效 | 检查实体类是否有@TableLogic注解,符合预期则无需处理 |
5.3 几个容易忽略但是很重要的坑
第一个坑是MyBatis-Plus的更新操作自动填充策略。比如创建时间和更新时间,很多人的做法是在插入时手动set,但这样每个新增的地方都要写一遍,少写一处就会出现空字段。正确做法是在实体类字段上使用@TableField(fill = FieldFill.INSERT)和@TableField(fill = FieldFill.INSERT_UPDATE),然后实现一个MetaObjectHandler处理器统一处理:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }第二个坑是数据库的关键字冲突。表名或者字段名如果用了MySQL的保留关键字会直接报SQL语法错误。我踩过的是把一张表命名为group,结果所有查询都报错。解决办法是建表时用反引号包裹,或者干脆起名时避开保留字。图书借还系统的表都比较安全,但以后自己扩展字段时要有这个意识,比如description、order、level这些词在MySQL里都有特殊含义,使用时需要格外小心。
第三个坑是前端路由使用history模式后,刷新页面出现404。这是因为前端路由的路径是虚拟的,比如/admin/books,浏览器刷新时服务器找不到这个路径对应的静态文件。开发环境Vite已经处理过这个问题,但生产环境如果用Nginx部署,必须配置try_files:
location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }这行配置的意思是:当请求的路径在磁盘上找不到对应文件时,统一返回index.html,由前端路由接管。少了这行配置,部署之后一刷新页面就白屏,这是前端部署最常见的坑,没有之一。
5.4 功能扩展还能怎么玩
这套系统的架构做完了,后续扩展空间很大。我个人觉得最有价值的方向是图书预约功能:当某本书的所有可借数量都被借出时,读者可以排队预约,有书归还时系统自动通知排在最前面的人。这个功能在现代图书管理系统中几乎是标配,但在共享书角场景里尤其实用。
第二个方向是数据可视化。MySQL8.0的窗口函数可以轻松统计借阅趋势、热门分类、读者活跃度等数据,前端用ECharts画几个图表,整个系统的档次就上去了。对于毕设项目或者简历上的亮点展示,这个功能性价比极高。
第三个方向是消息通知。可以引入简单的邮件发送或企业微信/钉钉机器人通知,逾期自动提醒读者还书,管理员也能收到催还汇总。不需要上消息队列,直接用Spring的@Scheduled定时任务扫一遍借阅记录表,把逾期的记录聚合后批量通知即可。
这些扩展方向都在现有表结构的能力范围内,不会推倒重来,这也是当初表设计时把状态字段分开的原因。
这次从零做完这套共享书角图书借还管理系统,我最大的感受是:技术选型不是越新越好,而是越适合越好。SpringBoot2 + Vue3 + MyBatis-Plus这套组合,对中小型管理系统来说刚刚好,开发效率高、社区资料全、部署门槛低。如果你也想搞一套类似的管理系统,或者正在为课程设计、毕业设计发愁,完全可以参考这个项目的思路,先把业务梳理清楚、表设计合理,再动手写代码,你会发现在一套结构清晰的项目里加功能,是一件无比爽快的事。