去年我接手了一个图书馆管理系统项目,需求方是一所高校图书馆——不是普通的借还书系统,而是在特殊时期需要兼顾"预约入馆、限流管控、健康信息登记、隔座自习"这些刚性需求的管理平台。折腾了几个月把整套源码跑通之后,身边陆续有人来问这套 SpringBoot + Vue + MyBatis + MySQL 的架构怎么落地。今天就以这个项目为例,把我在实际开发中总结的经验、踩过的坑、以及真正有价值的代码设计思路完整梳理出来。不管是学生做毕业设计、初级开发练手,还是想快速给自己的小型业务系统搭一套前后端分离的骨架,这篇内容应该都能帮到你。
1. 疫情下图书馆管理系统:到底解决了哪些业务痛点
很多人在看到"疫情下"这三个字时,下意识觉得这只是给普通图书管理系统加上了个时代表签。真做过之后我才意识到,这种系统的核心价值根本不是"图书管理"本身,而是围绕"特殊时期的物理空间安全管控"建立了一套新的业务流程。
普通图书馆管理系统关注的是:书目编目、库存管理、读者借阅、逾期计费。而疫情下的图书馆管理系统,优先级完全变了。需求方当时提的需求清单我印象很深——入馆前要预约、预约要控制名额、到馆要扫码核销、自习座位要隔座分配、归还的图书要进入消毒流程、读者离开后场地要记录消毒时间。这些需求在传统系统里根本不存在。
所以这套系统的本质,是一个加了"空间安全管控层"的图书管理系统。它在传统图书业务之外,重新定义了三条全新的业务闭环:
第一条是"入馆预约闭环":读者在线上查看可预约时段和剩余名额,提交预约申请,后台根据场馆容量约束自动校验是否允许预约,到馆后管理员核销预约码,读者入馆。现场满员时直接拒绝,不再依赖人工劝返。
第二条是"自习座位管控闭环":读者预约入馆时同步选择自习座位,系统通过"隔座禁用"规则把相邻座位锁定,物理上保证人与人之间的安全间距要求,管理员可以在后台实时查看座位占用热力图。
第三条是"图书流转安全闭环":读者归还的图书先进入"待消毒"状态,工作人员批量处理后再将状态置为"可外借",整个过程在系统里有明确的状态流转记录,每一本书的接触链条都可追溯。
我没有纠结"这个需求是不是只属于特殊时期",而是按照"业务闭环 + 空间状态 + 人员轨迹"三个维度来建模型。后来发现这个思路非常管用,因为哪怕以后这三条闭环的需求减弱了,系统里的预约能力、座位管理能力和流程追踪能力照样可以迁移到其他场景——比如大型活动场馆预约、自习室抢座系统、实验室空间管理等。技术上的复用价值远远大于业务本身。
搞清楚了这个定位,下面所有模块设计和表结构设计就有了明确依据。
2. 技术选型拆解:SpringBoot、Vue、MyBatis、MySQL这个组合为什么顺手
选型这件事,很多教程一句"技术栈采用SpringBoot+Vue"就带过去了,但真正动手的时候你会发现,选型决定的不光是能不能跑起来,而是后面每一个功能的开发效率和排查成本。我基于实际项目经验逐一拆一下。
2.1 后端SpringBoot:生态成熟、上手快、部署友好
SpringBoot在这类项目里几乎是"零思考"的第一选择。原因很现实:它把Spring生态里面最繁琐的配置自动化掉了,内嵌Tomcat,打包成一个JAR直接扔到服务器上就能跑。对于需要快速交付的项目来说,这太重要了——我们不需要花时间折腾外部容器配置,一个java -jar就能把服务顶起来。
另外SpringBoot的生态太齐全了。安全认证有Spring Security,任务调度有自带的@Scheduled注解,参数校验有Hibernate Validator,文件上传、邮件通知、模板渲染都有成熟的starter。对于一个图书馆管理系统,这些能力基本覆盖了绝大多数场景,不需要东拼西凑去找第三方库。
我用的是SpringBoot 2.7.x版本,配套JDK 1.8。有人问为什么不用JDK 17或者SpringBoot 3.x,我的建议是如果你不是从零起步且有充足时间适配,尽量选JDK 1.8 + SpringBoot 2.x的稳定组合,因为很多网上现成的工具类、教程、排错经验都是围绕这个版本积累的,遇到问题搜起来答案多,踩坑概率低。
2.2 前端Vue:双向绑定和组件化让表单类页面开发效率拉满
图书馆管理系统长得其实就是一堆"表格 + 表单 + 弹窗"。Vue在这类页面上优势非常明显:数据双向绑定让填表单、筛选、联动查询的代码量大幅减少;组件化机制把头部导航、侧边栏、表格、分页这些重复内容拆成公共组件,写一次到处用。
项目里我用的是Vue2 + Element UI的组合。这属于比较经典的搭配,Element UI的表格组件、表单校验、弹窗、日期选择器都做得非常完整,几乎不需要自己写复杂的UI逻辑。如果你未来没有特殊需求、想快速把页面堆出来,我建议别在Vue3和Element Plus上纠结太久——Vue2+Element UI依然是很稳的选择,网上的坑和解决方案也更多。
2.3 MyBatis:SQL可控性高,适合复杂多表统计
MyBatis在这个项目里承担的不只是CRUD,还负责几块难啃的统计逻辑。比如"座位占用率统计""各时段预约人数趋势""图书借阅排行榜",这些查询多多少少涉及多表关联和条件动态拼接,用MyBatis的XML文件手写SQL反而觉得更踏实。
它最大的好处是SQL语句完全由你自己掌控——你清楚每一条SQL是干什么的、会不会走索引、有没有笛卡尔积风险。相比一些自动生成的ORM框架,MyBatis在排查慢查询和优化SQL时的透明度要高得多。当然代价是多写一些XML映射文件,但在这个量级的系统里,这点成本完全值得。
2.4 MySQL:数据量级和事务特性完全够用
一个高校图书馆系统,读者几千人、馆藏几万册、每日预约几百条,这种量级的读写压力MySQL搭配InnoDB引擎应对起来绰绰有余。InnoDB提供行级锁、外键约束和事务支持,正好满足预约扣减、借阅状态流转这类需要原子性的操作。我在数据初始化时把整个建表脚本跑一遍,加上批量测试数据,整个过程没有遇到任何性能瓶颈。
2.5 技术栈组合的效果评估
我整理了一个简单的选型对比表,方便你用同样思路去判断自己的项目。
| 维度 | 这套组合的表现 | 适合场景 |
|---|---|---|
| 上手门槛 | 技术栈通用、学习资源丰富、社区活跃 | 学生、初级开发者、个人项目 |
| 开发速度 | 前后端分离并行开发,Vue组件复用高,SpringBoot约定大于配置 | 周期在1-3个月内的中小型系统 |
| 部署成本 | 后端JAR一键启动,前端打包成静态文件Nginx托管 | 只需一台普通服务器 |
| 扩展空间 | 可平滑接入Redis、RabbitMQ等中间件 | 后期需要增加缓存、消息队列时不会推倒重来 |
| 查询可控性 | MyBatis手写SQL,支持动态SQL,复杂统计易排查 | 多条件筛选、多表聚合报表 |
选型一贯的原则就是"不过度设计"。这套组合匹配这类系统的真实复杂度,它不炫技,但每一样都踩在点子上。
3. 数据库设计:从十几张表看这套业务怎么落地
老话讲"业务再复杂,看表结构就能猜出七八分"。这套系统的表结构设计我自认为是最值得复盘的一部分,因为它把前面说的业务闭环彻底固化到了数据模型里。
3.1 用户与权限体系设计
系统分三种角色:系统管理员、图书馆工作人员、普通读者。我没有用复杂的RBAC权限框架,而是简化为一张user表存基础信息,一张role表存角色定义,一张user_role表存关联关系。接口拦截时通过用户角色做简单判断,这在这个量级的系统里完全够用,理解起来也没有负担。
用户表的核心字段包括:用户名、密码(BCrypt加密后的密文)、姓名、学号/工号、手机号、学院/部门、证件照片路径、账号状态(正常/禁用)、创建时间。这里特别说一下"学号/工号"我单独建了唯一索引,原因是在预约入馆这个场景,实体证件和系统账号要严格一一对应,防止一个读者用不同手机号注册多个账号绕过预约限制。
3.2 图书与借阅核心表
这部分跟传统图书管理系统大体一致:book表存书目信息(ISBN、书名、作者、出版社、分类号、馆藏位置、总册数、可借册数)、book_copy表存每一本物理图书的实例(条形码、状态:在馆/借出/待消毒/消毒中/下架)、borrow_record表存借阅流水(读者ID、图书实例ID、借出时间、应还时间、实际归还时间、是否续借)。
book_copy表一定要单独拆出来,这对后面做"归还图书消毒流程"至关重要。如果只靠book表的总册数字段,区分不了具体哪一本书需要消毒、哪一本已经可借,流程根本没法追踪。
3.3 预约与限流核心表
这是"疫情下"场景新增的重头戏,也是整个系统的业务亮点。
reservation表是预约主表。核心字段包括:读者ID、预约日期、入馆时段(上午/下午/晚间)、预约状态(待核销/已入馆/已取消/已过期)、核销时间、创建时间。设计时我给"读者ID+预约日期+入馆时段"加了一个唯一索引——这就从数据库层面保证了一个读者一天内一个时段只能有一个有效预约,避免并发情况下刷出两条预约。
seat表存座位基础信息:座位编号、所在楼层、区域、是否靠窗、是否靠近电源、是否支持预约、状态(空闲/占用/维护中)。
seat_reservation表存座位预约明细:座位ID、预约ID、预约日期、开始时间、结束时间。这里用了"隔座禁用"的分配逻辑,我会在下一章重点讲。
3.4 特殊时期的信息追踪表
健康信息方面,我建了一张health_info表,存读者每日填报的健康状态、体温、是否接触过风险人群、健康码颜色(通过外部信息录入本地系统)、填报日期。入馆核销时,系统会校验当天这份记录是否有效,状态非正常直接拦截。
消毒记录方面,建了disinfection_record表,字段包括:图书实例ID、操作人ID、操作类型(入库消毒/归还消毒)、消毒方式、开始时间、完成时间、备注。归还流程里,图书状态从"借出"变成"待消毒",消毒操作完成后刷成"可借"。
3.5 表关系全景速览
我把核心表之间的关系简单梳理一下:
user与role多对多,通过user_role关联book与book_copy一对多,一本书有多个实例book_copy与borrow_record一对多,一个实例有多条借阅记录reservation与seat_reservation一对多,一个预约可选多个座位reservation与user多对一,一个读者可有多条预约记录seat与seat_reservation一对多,一个座位在不同时段可被多次预约
设计完成之后,你会发现系统天然分成了两条彼此独立又互相咬合的脉络:一条是图书流转脉络,一条是人员空间流转脉络。业务里的任何操作,最终都能落到某几张表的状态迁移上,排查问题和写统计报表都很有方向感。
4. 核心代码逻辑拆解:预约限流、隔座分配、健康校验的实现方式
表结构搭好了,真正的难点在于把业务规则写成代码。这一章挑几个核心逻辑详细展开,完整代码会按照源码包的模块结构存放在对应位置,这里讲清思路和关键代码片段。
4.1 预约入馆的限流算法
限流是这套系统里最容易出并发问题的环节。如果一个时段只剩5个名额,同时有10个人发起预约请求,最后可能10个人全部预约成功,超卖。
我的处理思路分两步:先校验基础条件,再用数据库原子操作扣减名额。基础条件包括读者状态是否正常、当天是否已有预约、预约日期是否在允许范围内。这一步用普通的select查询即可。
扣减名额这一步,我没有单独建一张"名额表"做update,而是直接在一个reservation_limit表中维护字段used_count(已预约人数)和max_count(时段最大容量),扣减名额时使用下面的SQL:
UPDATE reservation_limit SET used_count = used_count + 1 WHERE limit_date = #{date} AND time_slot = #{timeSlot} AND used_count < max_count;这条SQL的巧妙之处在于AND used_count < max_count这个条件——如果当前已使用人数已经等于上限,UPDATE不会更新任何记录,影响行数为0,程序通过判断影响行数就知道预约失败。同时因为UPDATE会自动对目标行加锁,并发情况下多个请求排队执行,天然不会超卖。这是我在多次测试后确定的方案,比"先select再update"的老办法稳妥得多。
核心伪代码如下:
@Transactional public boolean createReservation(ReservationDTO dto) { // 1. 基础校验:读者状态、预约日期合法性、是否重复预约 if (!basicCheck(dto)) { throw new BusinessException("不满足预约条件"); } // 2. 原子扣减名额 int rows = reservationLimitMapper.decrementQuota(dto.getDate(), dto.getTimeSlot()); if (rows == 0) { throw new BusinessException("该时段名额已满"); } // 3. 创建预约记录 reservationMapper.insert(...); return true; }有一个细节要提醒:这里我加了一个"补偿逻辑"。如果第二步扣减成功但第三步插入预约记录失败,事务会回滚,但used_count因为是在同一个事务里执行的UPDATE,也会随事务一起回滚,所以数据依然是安全的。
4.2 座位隔座分配算法
隔座分配是"自习座位管控闭环"最关键的一环。核心规则是:同一个区域内,已经被预约的座位的左右相邻座位不可再被预约。
我在实现时给seat表加了三个辅助设计:row_no(排号)、col_no(列号)、room_id(房间/区域ID)。这样判断相邻关系就非常直观了——同一房间内,排号相同且列号相差1的就是左右相邻座位。
public List<SeatVO> getAvailableSeats(Integer roomId, String date, String timeSlot) { // 1. 查询该时段已预约的座位集合 List<Integer> occupiedSeatIds = seatReservationMapper .getOccupiedSeatIds(roomId, date, timeSlot); // 2. 查询房间全部座位 List<Seat> allSeats = seatMapper.selectByRoomId(roomId); // 3. 过滤已占用座位及其左右相邻座位 Set<Integer> blockedSeatIds = new HashSet<>(occupiedSeatIds); for (Seat seat : allSeats) { if (occupiedSeatIds.contains(seat.getId())) { // 将同一排、列号±1的座位加入禁用集合 allSeats.stream() .filter(s -> s.getRowNo().equals(seat.getRowNo())) .filter(s -> Math.abs(s.getColNo() - seat.getColNo()) == 1) .forEach(s -> blockedSeatIds.add(s.getId())); } } // 3. 返回未被禁用的座位 return allSeats.stream() .filter(s -> !blockedSeatIds.contains(s.getId())) .map(SeatVO::fromEntity) .collect(Collectors.toList()); }如果你想往前排和后排也扩展"前后隔离"规则,只需要在过滤条件里加上"列号相同且排号相差1"这种判断即可,代码结构完全支持。实际项目里我们只做了左右隔离,因为后排一般已经有桌挡,风险可控。
4.3 入馆核销时的健康状态校验
核销是读者到馆后由工作人员扫描二维码完成的动作。核销接口返回给工作人员的不只是"成功/失败",还会有明确的失败原因提示。
public CheckInResult checkIn(Long reservationId) { Reservation reservation = reservationMapper.selectById(reservationId); if (reservation == null) { return CheckInResult.fail("预约记录不存在"); } if (!"待核销".equals(reservation.getStatus())) { return CheckInResult.fail("该预约已被处理,状态异常"); } HealthInfo healthInfo = healthInfoMapper.selectByUserAndDate( reservation.getUserId(), reservation.getReserveDate()); if (healthInfo == null || !"正常".equals(healthInfo.getHealthStatus())) { return CheckInResult.fail("健康信息未填报或状态异常,禁止入馆"); } reservation.setStatus("已入馆"); reservation.setCheckInTime(LocalDateTime.now()); reservationMapper.updateById(reservation); return CheckInResult.success("核销成功"); }这块逻辑不复杂,但它是业务上的一道硬闸门。任何跳过校验的核销都是在给风险留口子,所以我把它放在Service层最前面,并且所有入馆相关的统计报表都以"已入馆"状态为口径,保证数据自洽。
4.4 逾期未到馆预约的自动清理
实际运营中经常会遇到读者预约了但没来,如果不做自动清理,明天的名额会被"幽灵预约"占着。解决办法是用SpringBoot自带的定时任务,每个整点扫一次当天已过期未核销的预约。
@Component public class ReservationCleanTask { @Scheduled(cron = "0 0 * * * ?") // 每小时执行一次 public void cleanExpiredReservations() { // 找出当前时间已超过截止核销时间且状态仍为"待核销"的预约 List<Reservation> expiredList = reservationMapper.selectExpired(); for (Reservation reservation : expiredList) { reservation.setStatus("已过期"); reservationMapper.updateById(reservation); // 同步释放对应预约的名额 reservationLimitMapper.releaseQuota( reservation.getReserveDate(), reservation.getTimeSlot()); } } }这个任务有两个隐藏的坑:一是服务器时区要和业务时区保持一致,否则定时任务可能跑到错误的时间点;二是释放名额时不能用"used_count - 1"这种累减SQL,必须像扣减那样加AND used_count > 0条件防止负数,我在这上面出过问题,注意避开。
5. 前端页面的整体结构与关键交互设计
后端逻辑再完善,最终使用者接触的还是页面。这个项目的前端页面承担了两类完全不同的使用者需求:读者端要"快、顺、清晰",管理端要"全、准、可操作"。我分别说一下设计思路。
5.1 读者端页面:以预约动线为主线
读者端我设计成单页应用,核心页面就是"首页 + 预约中心 + 个人中心"三个大模块。
预约中心是最关键的页面。组件结构大致是:顶部展示可预约日期(横向滚动日期选择器)和当前选择日期下的时段名额卡片,中部是座位选择区域(用表格格子表示座位,绿色可预约、灰色已占用、黄色被隔座禁用),底部是提交预约的确认栏。用户操作路径非常短:选日期、选时段、选座位、提交,四步完成。
有一点要提的是"可预约日期"的展示逻辑,后端接口返回的除了日期列表,还要带上每个日期的每个时段剩余名额,这样前端不需要再发额外的请求去拼名额数据,接口设计上少一次网络往返。
5.2 管理端页面:核心是状态可视化
管理端我重点做了两块。一块是"预约核销台",工作人员通过扫码枪或手动输入预约编号快速核销,页面只是一个大的状态反馈区域——显示读者姓名、预约时段、座位编号、核销结果,颜色区分明显,方便现场快速反应。
另一块是"座次总览大屏",按楼层、区域展示所有座位,实时用不同颜色标记已预约、已入馆、空闲、维护中,管理人员瞄一眼就知道当前场馆饱和度,方便现场调配人力。这块用Element UI的table组件实现,加上2秒轮询刷新就够用。
5.3 权限路由与请求拦截
前端在路由配置里加了一个全局前置守卫,根据用户登录后的角色信息动态放行或拦截:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (!token && to.path !== '/login') { next('/login'); } else if (token && to.meta.roles && !to.meta.roles.includes(store.state.role)) { next('/403'); } else { next(); } });后端接口同样有角色拦截,前端只是优化体验,真正的安全控制必须依赖后端,这个原则在这个项目里始终没有动摇。
5.4 与后端对接的一些细节
前后端联调时最容易出问题的就是跨域配置和日期格式传输。我在后端统一加了CORS配置允许前端开发服务器访问,同时在后端全局配置了JSON序列化格式,保证LocalDateTime以yyyy-MM-dd HH:mm:ss格式输出,避免前端解析ISO格式字符串带来麻烦。这些小约定花不了多少时间,但能省掉联调阶段的大量沟通成本。这也是我后来做任何前后端分离项目都坚持的第一步。
6. 从源码到可运行:环境配置、初始化数据与启动步骤
源码拿到手或自己搭建完成后,最让人头疼的往往是"跑不起来"。这一章我把从零启动整套系统的步骤完整梳理一遍,照着走基本不会卡住。
6.1 环境准备清单
| 软件 | 版本建议 | 说明 |
|---|---|---|
| JDK | 1.8及以上 | 后端运行环境 |
| Maven | 3.6及以上 | 后端依赖管理 |
| MySQL | 5.7或8.0 | 数据库 |
| Node.js | 14.x及以上 | 前端构建环境 |
| npm | 6.14及以上 | 前端依赖管理 |
| Nginx | 1.18及以上 | 生产环境前端部署 |
前端开发调试阶段可以不装Nginx,直接用Vue开发服务器;部署上线时再抽Nginx托管静态文件并配置反向代理。
6.2 数据库初始化
在MySQL中创建数据库,建议字符集用utf8mb4,然后执行源码包里的sql目录下的初始化脚本。脚本分为三部分:
schema.sql:建库建表,全部表结构和索引定义data.sql:初始化管理员账号、角色数据、基础图书数据、座位数据demo_data.sql:造一批测试读者和借阅数据,方便你把系统玩起来
执行顺序不要乱,我见过有人先跑demo_data.sql导致外键关联报错,实际原因就是基础表还没建好。
6.3 后端启动流程
用IDEA打开后端源码目录,等待Maven下载依赖完成后:
- 修改
application.yml中的数据库连接配置,包括URL、用户名、密码 - 检查
application.yml中的服务器端口,默认我用的8080 - 启动
LibraryApplication.java主类 - 看到类似
Started LibraryApplication in xx seconds的日志即启动成功
整个过程最容易踩的坑是数据库版本引起的驱动差异。MySQL 8.0需要配置serverTimezone=Asia/Shanghai,否则时间字段会报错;如果连接报Public Key Retrieval is not allowed,在JDBC URL加allowPublicKeyRetrieval=true参数即可。
6.4 前端启动流程
- 进入前端源码目录
- 执行
npm install安装依赖,这个步骤耗时较长,可以顺手把后端日志打开观察 - 修改
src/config/index.js中的baseURL,指向后端接口地址,本地调测一般为http://localhost:8080 - 执行
npm run serve启动开发服务器 - 浏览器访问
http://localhost:8081进入系统
这里有个小建议:前后端联调时把浏览器开发者工具打开,重点看Network面板的响应状态码。401表示未登录或Token失效,403表示无权限,500多是后端异常,定位问题会快很多。
6.5 一键部署到服务器的建议
本地开发跑通后部署上线,我不建议用IDE直接跑后端。一套干净且省心的做法是:
- 后端执行
mvn package打成JAR包,丢到服务器上用nohup java -jar library.jar > log.out 2>&1 &方式运行 - 前端执行
npm run build生成dist目录,用Nginx托管,并配置反向代理把/api开头的请求转发到后端端口
server { listen 80; server_name your-domain.com; location / { root /opt/library/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files那一行是Vue单页应用路由的关键配置,不写的话刷新页面容易404。
7. 二次开发避坑指南:并发、定时任务、事务与安全配置
最后这部分是我最想分享的,因为这堆问题在写代码的时候一个都没想过,全是跑到生产环境前后被实际场景逼出来的。每一条都是真金白银的教训。
7.1 并发预约的"超卖"问题
前面提到用UPDATE ... AND used_count < max_count来扣减名额,这是我在被并发打穿之后才做出的调整。最初的版本是用select查剩余名额,Java代码里判断,再update,结果压测并发一上来就翻车。
这个坑的延伸思考是:任何"先查后改"的操作在高并发下都有问题,解决手段不一定是上分布式锁,很多时候一条带条件的原子SQL就够了。如果还要再稳一点,可以给reservation_limit表加版本号字段做乐观锁,但对这个场景来说,条件更新已经足够。
7.2 定时任务的"丢执行"问题
项目里除了清理过期预约,还有一个定时任务统计每日入馆人数并生成报表。一开始我把两个任务塞在同一个@Scheduled方法里串行执行,结果统计数据偶尔延迟,排查半天才发现是任务执行顺序和耗时互相影响。
后来把任务拆分到了两个独立的类里,并给每个任务标注了明确的cron表达式,互不干扰。另外一个教训是:定时任务的逻辑一定要做幂等设计,比如清理过期预约时,执行前先判断状态是否仍为"待核销",避免同一个预约被重复清理造成统计数据错乱。
7.3 事务边界问题
预约创建逻辑里,我用了@Transactional注解。刚开始一段代码把整个方法都注释上事务,感觉万事大吉,直到有一天我看到日志里"名额扣了但预约记录没插入"的诡异现象,分析半天才意识到问题出在事务方法内部的try-catch。
如果你在事务方法内部自己catch了异常,事务的rollback触发点可能被吞掉,SQL异常被吃掉后,事务照样提交。正确做法是:事务方法内的业务异常要throw出去,或者在catch块里调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()主动标记回滚。这个知识点重要且容易错。
7.4 MySQL时区与LocalDateTime的坑
系统上线不到一周,定时清理预约的任务开始乱套,数据老是提前或延后一小时。查下来是数据库连接参数里没有加serverTimezone,MySQL默认用了系统时区,而服务器时间又是UTC,导致时间偏移。
经验是:建库加压测阶段就要第一时间把serverTimezone=Asia/Shanghai配好,并且后端的LocalDateTime、数据库的datetime、前端的日期显示三者的时区口径完全对齐,不要指望框架自动帮你搞定时区。
7.5 接口安全不能只靠前端
我在项目里给后端接口加了基础的安全拦�辑,用拦截器统一校验Token和角色,而非法请求在进入Controller之前就会被拦截。很多自学的读者容易忽略这件事,觉得"页面不显示这个按钮就不存在这个功能",实际上完全不是这样——接口地址可以通过浏览器开发者工具直接看到,别人绕过前端直接调后端接口,没有后端校验的话等于大门敞开。
另外密码存储必须用BCrypt加密,所有新增用户的默认密码也要在Service层统一走加密逻辑。数据库里明文密码,一次备份泄露就可能引发连锁安全事件。
一些照片与页面截图备注
项目源码里附带了几张关键页面的运行效果截图,分别是读者预约中心页面、管理端核销台页面、座次总览页面和后台借阅管理页面。截图能帮你快速直观地看到这套系统的界面形态,部署成功后将你本地跑出来的界面和截图比对一下,也能快速判断哪些功能还没启动成功。想深度理解系统的读者,强烈建议跟着这几张截图把代码一步步走一遍,页面背后对应的那张表、那一条SQL链,对照着看会清晰得多。
这套系统做完之后我有一个非常大的感受:一个项目的价值不完全在于代码写了多少,而在于能否准确还原业务方真实的需求场景,并将场景落成清晰的数据模型和可靠的流程控制。SpringBoot、Vue、MyBatis、MySQL这些东西都只是工具,能把这些工具用在该用的地方,并且提前想到并发、事务、安全性这些生产问题,才是真正让系统从"能跑"变成"能投入使用"的分水岭。如果这篇梳理能让你在自己搭建或复用这套系统时少走几个弯路,那这一整篇文字就没白写。