news 2026/10/22 3:22:58

基于SpringBoot+Vue3的疫情下图书馆管理系统设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于SpringBoot+Vue3的疫情下图书馆管理系统设计与实现

疫情把图书馆逼到了墙角,这件事在2020年之后做图书馆信息化的人应该都有同感。传统的图书管理系统管的是"书"——编目、借出、还回、逾期,逻辑再复杂也就那样。可疫情一来,图书馆的运营逻辑一下子多出了好几条硬约束:进馆人数必须控制、座位必须保证间距、归还的书不能直接上架、读者进场要先确认状态。这些需求传统系统根本覆盖不了,硬在老系统上改也改不动——因为表结构、业务流程、前端交互全都要动。

我做的这套"Java Web 疫情下图书馆管理系统",就是用 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 把"传统借阅管理"和"防疫场景约束"一次性揉进同一个系统里。它不是一个只加了预约功能的旧系统,而是把入馆预约、限流座位、图书消毒状态流转这些疫情特有流程,作为一等公民设计进数据库和业务层。整套源码包含完整项目文档,读者端和管理端分离,拿来即用。这篇文章我把整个系统的设计思路和实现细节拆开讲,适合正在做类似毕设、课设、或者真要给单位图书馆做改造的同学参考。

1. 疫情时代的图书馆管理,到底缺了什么:从需求反推系统边界

1.1 传统图书管理系统的能力死角

先别急着谈技术选型。我见过太多人拿到这种题目第一反应是"先建张图书表,再建张借阅表,CRUD一写,完事"。这种做法的确在2020年之前是够用的,但疫情场景下你很快会发现几个传统系统无法回答的问题:

第一,入馆容量怎么控。图书馆不可能把门一关不让进,但也不能让人扎堆。需要按时间段预约,每个时间段设人数上限,读者来之前就得约好,到馆后凭证入馆。传统系统里根本没有"时间段"和"预约配额"这两个概念。

第二,座位怎么管。自习区座位必须隔座开放,哪些座位可用、哪些座位被占用、预约了的人有没有按时到,这些都需要实时状态。传统图书系统管书不管座,座位管理完全是空白。

第三,图书归还后的消毒周期。疫情期间很多图书馆规定:归还的图书必须经过消毒并静置若干小时才能再次流通。这意味着还书之后,书不能立刻变成"可借"状态,它要经过一个"待消毒 -> 已消毒 -> 可上架"的状态流转。传统系统的状态机里只有"在馆/借出",根本没有中间态。

第四,读者健康状态登记。进馆前需要登记健康状况,这类信息敏感且有时效性,系统只需要保存用户自行申报的结果,不需要也不应该对接任何健康数据平台。

所以这套系统的需求边界,不是"图书管理系统 + 一个预约模块",而是"借阅管理 + 空间管理 + 防疫流程管理"三条线交叉。我把它们理成了下面这张功能地图。

1.2 系统角色与核心流程

角色上我划分成三类:系统管理员、图书管理员、普通读者。

  • 系统管理员:管理图书管理员账号、查看全馆数据统计、维护公告。
  • 图书管理员:图书录入与维护、审核借还操作、执行还书消毒操作、管理座位与预约、处理入馆预约单。
  • 普通读者:注册登录、检索图书、借书还书、预约入馆、预约座位、查看借阅历史。

三条核心业务主流程,对应了系统里最重要的几个服务:

  1. 借阅流程:读者检索图书 -> 确认在馆且无消毒待处理状态 -> 提交借阅申请 -> 管理员审核通过 -> 借阅记录生效 -> 借出周期开始。
  2. 还书与消毒流程:读者归还 -> 图书状态变为"待消毒" -> 管理员登记消毒 -> 状态变为"已消毒/静置中" -> 静置时间达标 -> 自动变为"可借"。
  3. 入馆与座位流程:读者按日期时间段预约入馆 -> 系统检查该时段剩余名额 -> 预约成功生成入馆凭证 -> 读者到馆后管理员核销 -> 同步可选预约座位 -> 到馆签到后座位锁定。

1.3 功能清单的取舍:哪些必须做、哪些坚决不做

这一节我想多说几句,因为做项目最容易死在不切实际的功能清单上。当时的取舍原则有三条:

  • 凡是需要和外部平台对接的功能,一律不做真实对接,只预留字段和模拟流程。比如健康状态申报,就是读者端一个自查表单,数据入库,不接任何外部系统。
  • 凡是会造成无限复杂度但又没有实际演示价值的功能,简化处理。比如消息通知,我统一做成了站内公告 + 借阅到期提醒,没有短信邮件推送。
  • 凡是核心业务链路上必须的功能,必须做扎实。借还事务、预约防冲突、座位并发控制、消毒状态机,这些是系统区别于"玩具项目"的关键,后面我会重点展开。

2. 技术栈的每一环都有讲究:SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0

2.1 后端框架:为什么选 SpringBoot 2.x 而不是 3.x

这个问题在我考虑选型时确实纠结过,毕竟现在写新项目很多人直接上 3.x。最终定在 SpringBoot 2.x,原因很实际:

  • 国内大量生产环境的 JDK 还停留在 8 或 11,SpringBoot 3.x 强制要求 JDK 17,对部署环境不友好。如果你是在校生,实验室服务器和云服务器上大概率还是 JDK 8,选 2.7.x 版本避免了"代码写好了,服务器跑不起来"的尴尬。
  • MyBatis-Plus 对 SpringBoot 3 的适配虽然已经跟进,但很多旧版本的生成器、代码插件在 3.x 下会踩兼容坑。而 2.7.x 搭配 MP 3.5.x 是经过无数项目验证过的稳定组合。
  • 疫情管理系统这类业务,核心是业务逻辑的清晰表达,不是新语法特性。SpringBoot 2.7 足够支撑。

我用的版本组合是:SpringBoot 2.7.18 + MyBatis-Plus 3.5.3.1 + MySQL Connector/J 8.0.33 + JJWT 0.11.5 + Hutool 5.8.x。其中 Hutool 帮我省了大量工具类代码,后面会看到。

2.2 持久层:MyBatis-Plus 的价值与代价

MyBatis-Plus 在这套系统里的角色是"把简单 CRUD 的成本降到最低"。借阅管理、座位管理、预约管理这些表,大部分操作就是单表查询、条件筛选、分页列表,用 MP 的 BaseMapper 和 LambdaQueryWrapper 可以少写至少三分之一的数据访问层代码。

代价是什么?代价是多表关联查询不能完全依赖 MP。比如借阅列表需要关联图书名、读者名、操作管理员名,这种场景我用 MP 的@Select注解直接写 SQL,而不是试图用 Wrapper 硬拼 join。我的原则是:单表操作用 MP,复杂查询写 SQL,两者结合而不是二选一。

2.3 前端与数据库:Vue3 + MySQL 8.0 的组合逻辑

前端选 Vue3 主要是考虑到组件组织和状态管理。管理端页面多——图书管理、读者管理、借阅审核、座位管理、预约管理、消毒管理,页面之间共享"当前登录用户""图书分类列表"这类全局状态,Vue3 的组合式 API 配合 Pinia 比 Options API 写起来顺手得多。UI 组件库我用的 Element Plus,表格、表单、日期选择器这些现成组件能直接覆盖管理系统 80% 的界面需求。

数据库选 MySQL 8.0 的原因更直接:8.0 的窗口函数、JSON 类型、公共表表达式让统计报表和复杂查询的实现简洁很多。比如后面要做的"每日入馆人数趋势图",用窗口函数一条 SQL 就能算出来,放到 5.7 里就得写临时表,麻烦不少。另外 8.0 默认字符集是 utf8mb4,中文和生僻字存储没烦恼。

3. 数据库设计:一张张表把借阅和防疫串起来

数据库是整个系统最值得花时间的地方。我的做法是先把实体关系画清楚,再落表。整套系统一共 11 张核心表,我分三类说。

3.1 基础四表:用户、图书、图书分类、借阅记录

用户表 (sys_user)是三类角色共用的,用role字段区分,读者和管理员都放在同一张表里。这么做的好处是登录鉴权只查一张表,逻辑简单。字段上除了常规的账号密码(密码用 BCrypt 加密存储)、姓名、学号/工号、手机号之外,我加了一个health_status字段,用于存读者最近一次健康状态自查结果,这就是前面说的"防疫流程只做内部闭环"的落点。

图书表 (book)是系统里字段最多的表。除了书名、ISBN、作者、出版社、分类、价格、库存总量这些常规字段外,关键设计在状态字段上:

字段说明
total_count图书总册数
available_count当前可借册数
status0-可借 1-全部借出 2-待消毒 3-停用
location馆藏位置
create_time/update_time创建与更新时间

注意status是"整本书"的状态,而库存变化是靠available_count维护的。同一本书有多册,借走一册就扣一,还回来一册就加一。至于"每一册在哪个流程",我通过借阅记录和消毒记录来追踪,不在图书表里拆册,这是为了控制表数量和维护成本。

图书分类表 (book_category)很简单,就 id、分类名、父分类 id,做一级分类足够,不需要递归树。

借阅记录表 (borrow_record)是核心流水表:

CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '读者ID', book_id BIGINT NOT NULL COMMENT '图书ID', borrow_time DATETIME NOT NULL COMMENT '借出时间', due_time DATETIME NOT NULL COMMENT '应还时间', return_time DATETIME DEFAULT NULL COMMENT '实际归还时间', status TINYINT NOT NULL COMMENT '0-借出中 1-已归还 2-逾期', operator_id BIGINT DEFAULT NULL COMMENT '办理管理员ID', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id), INDEX idx_book (book_id), INDEX idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表';

外键我全都没建,只建了索引。原因是业务层有事务控制,外键约束在互联网项目里反而容易成为性能瓶颈和迁移障碍。这在教学项目里可能有人不习惯,但实际开发经验告诉我,逻辑外键比物理外键好维护得多。

3.2 疫情相关表:入馆预约、座位预约、消毒记录

这是这套系统的特色部分,也是和普通图书管理系统拉开差距的地方。

入馆预约表 (entry_appointment):核心字段是预约日期appointment_date、时间段time_slot、预约状态status(0-待核销 1-已核销 2-已取消 3-爽约)、配额编号quota_id。配额设计我单独做了一张入馆配额表 (entry_quota),字段是日期、时间段、最大人数、已预约人数。预约的时候事务内检查"已预约人数 < 最大人数",通过则加一,这样避免直接在高并发下对预约表做 count 再判断。

座位表 (seat)和座位预约表 (seat_reservation)是另一条线。座位表字段包括区域、座位编号、x/y 坐标(前端座位图渲染用)、是否启用。座位预约表记录哪个读者约了哪个座位、哪个时间段、状态如何。关键约束是"同一座位同一时间段只能有一个有效预约",这个我用唯一索引加事务来解决。

消毒记录表 (disinfection_record):字段是图书 id、归还记录 id、消毒方式(紫外线/臭氧/静置)、消毒时间、预计静置结束时间、操作管理员 id、状态。这张表把"还回来的书经没经过消毒"完整记录在案,每本书都有消毒履历,这在疫情场景下是很有说服力的功能点。

3.3 索引与字段设计的细节考量

几个容易踩坑的细节我列一下:

  • 时间字段统一用 DATETIME,别用 TIMESTAMP。MySQL 8.0 里 TIMESTAMP 有 2038 年问题,虽然短期无所谓,但 DATETIME 是 8.0 更稳的选择,而且不会有时区隐式转换的坑。
  • 状态字段用 TINYINT 不用 VARCHAR。数据库里存数字,Java 枚举里做映射,展示层再转成中文,这一条贯穿所有表。
  • 预约类表要加唯一约束。座位预约表我对(seat_id, time_slot, appointment_date, status)加了部分唯一索引的思路,实际实现时是用"先查再插 + 事务 + 唯一索引兜底"三重保证,防止同一座位被并发约两次。这个后面会详细讲。

4. 后端核心代码是如何落地的:鉴权、借还书与预约流程

4.1 项目目录结构与统一响应封装

后端工程我按 mvc 分层组织,目录结构是这样:

src/main/java/com/example/library/ ├── common/ // 统一响应、异常处理、常量 │ ├── Result.java │ ├── ResultCode.java │ ├── BusinessException.java │ └── GlobalExceptionHandler.java ├── config/ // 拦截器、MyBatis-Plus分页配置、跨域配置 │ ├── JwtInterceptor.java │ ├── MybatisPlusConfig.java │ └── CorsConfig.java ├── controller/ ├── entity/ ├── mapper/ ├── service/ │ └── impl/ ├── dto/ // 前端交互对象,避免实体直接裸露 ├── vo/ // 视图对象 └── utils/ // JWT工具、日期工具等

统一响应Result<T>是这个系统前后端协作的基石。它固定返回code、message、data三个字段,code 为 200 表示成功,其他为业务异常码。前端 axios 拦截器统一处理 code,不需要每个页面重复写错误弹窗逻辑。

public class Result<T> { private Integer code; private String message; private T data; // 静态方法 success() / error() ... }

异常处理我用@RestControllerAdvice全局捕获。业务里该抛异常就抛异常,不该在 controller 里写一堆 try-catch,这个是很多人写后端容易犯的毛病。

4.2 JWT 登录与拦截器方案

登录接口的逻辑很直接:账号密码查sys_user表,密码用 BCrypt 校验,通过后签发 JWT,有效期我设了 12 小时。JWT 里只放 userId 和 role 两个关键信息,不塞多余数据。

public class JwtUtils { private static final SecretKey KEY = Keys.hmacShaKeyFor( "your-256-bit-secret-key-here-change-in-production".getBytes()); public static String generateToken(Long userId, Integer role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + 12 * 60 * 60 * 1000)) .signWith(KEY, SignatureAlgorithm.HS256) .compact(); } }

拦截器的核心是"白名单 + 角色权限"双重校验。登录、注册、首页公告、图书检索是白名单,其他接口都需要 token;管理端相关接口额外校验 role 是否为管理员,不是就直接抛"无权限"异常。

这里有个小坑要提醒:JWT 密钥千万不要硬编码在代码里,我只是为了演示方便才这么写。实际项目要放到配置中心或者环境变量里,不然密钥泄露等于全站账号失守。

4.3 借书与还书的事务处理

借书是这套系统第一个"必须保证原子性"的业务。步骤拆开是这样的:

  1. 校验读者存在且未拉黑。
  2. 校验图书状态为"可借",且available_count > 0。
  3. 扣减available_count。
  4. 创建borrow_record,状态为借出中。
  5. 若扣减后为 0,更新图书status为"全部借出"。

这五步必须在一个事务里完成,任何一步失败都要回滚,否则会出现"记录建了但库存没扣"或者反过来。我的实现是 Service 里用@Transactional(rollbackFor = Exception.class),并且用"先查后改 + 乐观锁"来防并发超借。

@Transactional(rollbackFor = Exception.class) public void borrowBook(BorrowRequest request) { Book book = bookMapper.selectById(request.getBookId()); if (book == null || book.getStatus() != 0 || book.getAvailableCount() <= 0) { throw new BusinessException("图书暂不可借"); } // 乐观锁更新:available_count > 0 作为条件 int updated = bookMapper.deductAvailable(book.getId()); if (updated == 0) { throw new BusinessException("库存不足,请刷新后重试"); } BorrowRecord record = new BorrowRecord(); // ... 填充字段 borrowRecordMapper.insert(record); }

deductAvailable对应的 SQL 是UPDATE book SET available_count = available_count - 1 WHERE id = ? AND available_count > 0,受影响行数为 0 就说明并发下库存已经被抢光了,直接抛业务异常。这是比"先 select 再 update"更可靠的并发控制方式。

还书流程正好相反,但多了一步防疫处理:归还后图书状态先置为"待消毒",而不是直接回到"可借"。此时available_count不恢复,等到消毒静置完成,才恢复库存。这样保证读者在前台搜索时,不会搜到一本"正在消毒室躺着"的书。

4.4 座位预约的防冲突设计

座位预约是个典型的并发冲突场景:两个读者同时预约同一个座位、同一个时间段。如果只靠先查后插,在并发下必然出现双约成功。我的方案是三层防护:

第一层,业务校验:预约前检查该座位该时段是否存在有效预约记录。

第二层,数据库唯一索引:在seat_reservation表上,对(seat_id, time_slot, appointment_date, status)做了一个特殊设计——正常思路是对这四列建唯一索引,但 status 会变化(取消后同一座位又能约),所以唯一索引不能包含 status。我的做法是拆出一列slot_key,值为seatId + date + timeSlot拼接的字符串,对这个字段建唯一索引。取消预约时删掉旧记录而不是改状态,这样唯一索引就能一直生效。

第三层,事务:插入操作放在事务里,唯一索引冲突时捕获DuplicateKeyException,转成友好的"该座位已被预约"提示。

这个方案实际压测下来,并发 100 个预约请求不会出现一例超卖。代价是多一个冗余字段,但换来的是数据安全,值。

5. 前端不是套壳:Vue3 组合式 API 下的管理端与读者端

5.1 工程初始化与请求封装

前端工程用 Vite 构建,npm 依赖主要是 vue、vue-router、pinia、axios、element-plus、echarts(统计图表用)。刚初始化完第一件事是封装 axios,这一步做得好,后面所有页面都省力。

// src/utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 15000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error('网络异常,请稍后重试') return Promise.reject(error) } ) export default request

这里踩过一个坑:开发环境的跨域必须在 vite.config.js 里配代理,不要把 baseURL 写成完整后端地址。写成http://localhost:8080虽然开发时也能通,但打包部署到服务器后要么得改代码要么得配 Nginx 跨域,麻烦得很。正确做法是 dev 环境代理解决,生产环境 Nginx 反代,前端始终只用相对路径。

5.2 图书管理和借阅列表的关键实现

图书管理页是管理端用得最多的页面。搜索条件有三个:书名关键字、ISBN、分类。我的做法是模糊搜索用LambdaQueryWrapper.like,分类用等值匹配,分页直接用 MP 的Page对象。

public PageResult<BookVO> pageBooks(BookQuery query) { Page<Book> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(query.getKeyword()), Book::getTitle, query.getKeyword()) .eq(query.getCategoryId() != null, Book::getCategoryId, query.getCategoryId()) .orderByDesc(Book::getCreateTime); Page<Book> result = bookMapper.selectPage(page, wrapper); // 转 VO、补充分类名称... }

前端表格用 Element Plus 的el-table,加上el-pagination分页组件。新增和编辑共用一个弹窗表单,通过表单里的 id 判断是新增还是更新,App.vue 里维护一个 visible 状态。这些属于常规操作,但有个交互细节值得提:库存扣减到 0 时,行内操作按钮要自动禁用,不能让管理员点了提交才报错,体验太差。我在表格渲染时根据availableCount和status动态计算按钮是否可点击。

借阅列表页类似,但复杂在需要联表显示书名和读者名。前端只需要展示,所以后端直接返回联表查好的 VO 对象,不做多次请求前端再拼接的傻事。借阅管理里还有个高频操作是"催还",实现就是在详情弹窗里显示借出时间和应还时间,超过应还时间则红色高亮,并显示"逾期 X 天"的徽标。

5.3 座位预约的交互细节

座位预约是前端交互最复杂的模块。读者端看到的是一个可视化座位图:用 CSS Grid 布局渲染座位格子,每个座位根据状态显示不同颜色——绿色可用、红色已占用、灰色停用、黄色是我已预约。

实现思路是后端返回该区域的座位列表,每个座位带status字段,前端渲染成格子:

// 座位图核心渲染逻辑(节选) const seatStatusClass = (seat) => { if (seat.status === 0) return 'seat-available' if (seat.status === 1) return 'seat-occupied' if (seat.status === 2) return 'seat-disabled' if (seat.status === 3) return 'seat-mine' return 'seat-unknown' }

点击可用座位后弹出时间段选择器,选完提交预约。提交前前端先做一次本地校验(不能选过去的时间),但最终判断还是以后端返回为准。预约成功后该座位立即在前端标记为"我预约",不用刷新页面。

这中间我踩了个体验问题:座位状态在后端是实时变化的,前端如果不刷新,格子颜色可能过期。比如别人刚约走一个座位,你的页面还显示绿色,点了提交才发现冲突。解决的折中方案是:点开座位图时拉一次全量数据,提交预约失败刷新一次,成功则本地更新。虽然没有做 WebSocket 推送,但实际单馆使用完全够用。

6. 防疫流程的功能闭环:预约入馆、座位限流、图书消毒

6.1 入馆预约的时间片与容量控制

入馆预约模块我把它做成独立的预约管理页。管理员先配置"日期 + 时间段 + 最大人数",比如上午 9:00-11:00 最多 100 人,下午 14:00-17:00 最多 120 人。配额配置完成后,读者端就能看到这些时间段,选择后提交预约。

关键代码是预约时的容量扣减,我用了和借书一样的"乐观扣减"策略:

@Transactional(rollbackFor = Exception.class) public EntryAppointment createAppointment(Long userId, Long quotaId) { // 先查配额 EntryQuota quota = entryQuotaMapper.selectById(quotaId); if (quota == null || quota.getAppointmentDate().isBefore(LocalDate.now())) { throw new BusinessException("预约时段不存在或已过期"); } // 判断该用户当天该时段是否已预约过 Long count = entryAppointmentMapper.countByUserAndSlot(userId, quotaId); if (count > 0) { throw new BusinessException("您已预约过该时段"); } // 乐观扣减 int updated = entryQuotaMapper.increaseBookedCount(quotaId); if (updated == 0) { throw new BusinessException("该时段名额已满"); } // 生成预约记录 EntryAppointment appointment = new EntryAppointment(); appointment.setUserId(userId); appointment.setQuotaId(quotaId); appointment.setStatus(0); // 待核销 entryAppointmentMapper.insert(appointment); return appointment; }

核销逻辑发生在读者到馆时:管理员输入读者预约编号或扫描凭证上的二维码标识,查到预约记录后把状态从"待核销"改成"已核销"。爽约判定我放在每晚的定时任务里,把当天"待核销"的记录统一置为"爽约"。定时任务用 Spring 自带的@Scheduled就能实现,不需要额外引入 xxl-job。

6.2 图书归还后的消毒状态流转

这是我认为整套系统最能体现"疫情场景"的模块,也是评审时容易被问到的点。它本质是一个状态机:

可借 --还书--> 待消毒 --登记消毒--> 静置中 --静置到期--> 可借

四个状态的流转我写在 Service 里,不用工作流引擎,因为状态只有四个、流转路径固定,用引擎反而增加复杂度。

状态变更的落地方式比较简单。还书时将图书 status 置为 2(待消毒),同时把available_count冻结。管理员执行消毒操作时,创建一条disinfection_record,填入消毒方式和预计静置结束时间,图书 status 变为 3(静置中)。然后由定时任务每分钟扫描所有"静置中"的记录,到达expected_end_time的,自动把图书恢复为可借状态、恢复库存。

这里有一个容易被忽略的细节:静置结束后要恢复available_count,同时要检查整本书是否还有处于借出状态的册。也就是说状态恢复不能无脑置为"可借",得先查一下该图书当前租借中的记录数。如果还有册借出在外,整本书状态应该是"部分借出"而不是"全部可借"。这个逻辑虽然不复杂,但最容易漏。

6.3 管理端的数据统计与导出

数据统计是给管理员和系统管理员看的"驾驶舱"。一个疫情时期的图书馆管理者最关心三件事:今天多少人进馆、哪些座位利用率高、图书借阅量有没有恢复。

我的统计页实现方式是:后端提供几个聚合接口,前端用 ECharts 渲染。例如"近 14 天入馆人次趋势",后端一条 SQL:

SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, COUNT(*) AS cnt FROM entry_appointment WHERE status IN (1, 2) AND create_time >= DATE_SUB(CURDATE(), INTERVAL 14 DAY) GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') ORDER BY day;

座位利用率则是已核销预约数 / (可用座位数 × 开放时段数)的近似计算。我没追求精确的全部口径,对管理场景来说趋势比绝对值更有意义。

导出功能我用 Hutool 的 ExcelWriter 实现,把统计列表数据直接写成 xlsx 返回给前端下载。Hutool 这块封装得很到位,避免了直接操作 POI 那套繁琐 API。

7. 本机跑通到部署上线的踩坑记录

7.1 MySQL 8.0 连接配置的时区坑

第一个坑相信每个用 MySQL 8.0 做 SpringBoot 项目的人都踩过:连接 URL 不配 serverTimezone 会报错。而且 8.0 推荐的时区写法是serverTimezone=Asia/Shanghai,不是UTC,不然查出来的时间会比本地时间晚 8 小时。我的application.yml配置如下:

spring: datasource: url: jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: ****** driver-class-name: com.mysql.cj.jdbc.Driver

allowPublicKeyRetrieval=true这个参数也很关键,MySQL 8.0 默认的 caching_sha2_password 认证插件下,不配这个参数某些客户端连接时会报"Public Key Retrieval is not allowed"。我一开始就被这个报错卡了半天,配完瞬间通过。

7.2 前后端联调的代理与跨域问题

联调时我遇到的是典型的代理配置问题。前端跑在 5173 端口,后端跑在 8080,直接请求必然跨域。解决方式是 Vite 代理:

// vite.config.js server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

注意我没有在后端单独配@CrossOrigin全局跨域,因为那样生产环境还得再处理一次。开发环境靠代理,生产环境靠 Nginx 反代,全程只需要相对路径,这是最干净的做法。

7.3 打包发布与服务器环境注意事项

后端打包用 Maven 的mvn clean package -DskipTests,打出来是标准的 jar 包,用java -jar library-server.jar启动。这里有两个问题需要注意:

第一,打包前检查配置文件是不是生产环境配置。我习惯在application.yml里用 profile 区分,application-dev.yml本地开发、application-prod.yml服务器部署,打包时指定 profile 避免把本地数据库地址打进去。

第二,服务器上别前台启动。直接java -jar启动,SSH 一断开进程就没了。我用的是最朴素的 nohup 方式:

nohup java -Xms256m -Xmx512m -jar library-server.jar --spring.profiles.active=prod > app.log 2>&1 &

前端打包是npm run build,产物是 dist 目录,直接扔给 Nginx。Nginx 里配一个反代规则,把/api开头的请求转发到后端 8080 端口,其余静态资源走本地文件,这事就齐了。我踩过的最后一个坑是 Nginx 反代时忘了配proxy_set_header Host $host,导致后端拿到的请求头不对,个别功能异常,加上这一行后恢复正常。

最后说说使用这套源码的个人体会

整套系统做完,我最满意的一点不是某个炫技功能,而是"借阅管理"和"防疫管理"两条业务线真正拧在了一起。还书的下一步自动衔接消毒流程,预约入馆同步解锁座位资源,这些联动不是堆功能堆出来的,而是在数据库设计和事务设计阶段就规划好的。如果让我重新做一遍,我会在座位预约的实时性上再做一层 WebSocket 推送,让前端座位图永不出现过期状态;也会给消毒静置规则做一个可视化的规则配置界面,而不是把静置时长写死在配置项里。

给正在做类似系统的同学一个最实在的建议:先把数据库的 ER 图画透,把所有状态流转列清楚,再写代码。我见过太多人一上来就写 CRUD,写到防疫流程时发现表和表之间对不上、状态字段不够用,推倒重来。这套源码里数据库脚本和文档都是完整的,直接跑起来看一遍数据流,比自己闭门造车快得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 3:22:43

模糊轨迹跟踪实战指南:从MATLAB仿真到参数调优与避坑

简介&#xff1a;面向自动化、机器人及智能控制学习者的模糊轨迹跟踪仿真资料&#xff0c;核心解决动态系统&#xff08;如无人驾驶平台、机械臂&#xff09;对预设轨迹的精确跟踪与鲁棒控制问题。资源共15个文件&#xff0c;以MATLAB脚本(m)为主&#xff0c;辅以Simulink模型(…

作者头像 李华
网站建设 2026/10/22 3:22:23

MATLAB实现802.11a/g标准OFDM基带链路仿真

简介&#xff1a;本资源是一份面向通信工程专业学生、无线技术初学者及MATLAB实践者的OFDM信号建模仿真脚本&#xff0c;聚焦802.11a/g/n标准中核心调制机制的理解与复现。压缩包仅含1个672B的MATLAB源文件&#xff08;.m&#xff09;&#xff0c;即OFDM signal.m&#xff0c;完…

作者头像 李华
网站建设 2026/10/22 3:22:16

自组织网络鲁棒性量化:从拓扑仿真到故障耐受曲线

简介&#xff1a;本资源为2020年本科毕业设计项目《基于机器学习的自组织网络的鲁棒性研究》的完整实现代码与实验成果包&#xff0c;面向人工智能、网络工程方向的高年级本科生及入门级科研实践者&#xff0c;聚焦无线传感器网络、Ad Hoc等无中心拓扑场景下&#xff0c;如何利…

作者头像 李华
网站建设 2026/10/22 3:22:01

Redis分布式架构设计:CAP、主从、哨兵与Cluster核心解析

这几年但凡涉及到分布式系统设计&#xff0c;无论是面试、架构评审还是技术方案选型&#xff0c;Redis 的架构设计都是绕不开的必考题。尤其是系统架构设计师这个层面&#xff0c;单纯会读写 Redis 已经不够了&#xff0c;真正的分水岭在于&#xff1a;你能不能讲清楚为什么单机…

作者头像 李华
网站建设 2026/10/22 3:21:39

脑电波分析实战指南:信号预处理、特征工程与模型验证全流程

简介&#xff1a;面向2020年研究生数学建模竞赛C题“脑电波分析”的竞赛资源包&#xff0c;适合参加电赛、数学建模竞赛的研究生和本科生&#xff0c;以及需要快速上手脑电信号处理与建模的开发者。包内共263个文件&#xff0c;总大小约118.59MB&#xff0c;其中22个py脚本与83…

作者头像 李华
网站建设 2026/10/22 3:21:35

SpringBoot+SSM农业信息管理系统实战:从数据库建模到答辩通关

这段时间陆续帮几个备考毕业设计的同学捋过类似的题目&#xff0c;其中农业信息管理系统出现频率相当高。说实话&#xff0c;这类系统看起来“传统”&#xff0c;但真正落地的时候涉及的知识点一点都不少&#xff0c;从框架整合到权限控制&#xff0c;从表结构设计到部署调试&a…

作者头像 李华