共享自习室签到管理系统:Spring Boot+Vue全栈实践
发布时间:2026/9/9 16:12:18 作者:尧图编辑部 阅读量:1,286

在共享自习室这个场景里签到和座位管理是每天高频发生、又最容易扯皮的环节。纸质签到簿存在代签和信息模糊问题部分商业平台提供的扫码签到方案又往往捆绑会员和支付体系对小型自习室或高校众创空间并不友好单纯用 Excel 记录虽然能积累数据却无法实时反映某个座位当前是否空闲。因此我决定自己动手做一个基于 Spring Boot Vue 的 Web 版共享自习室签到管理系统用两周左右的业余时间把核心流程跑通用户选座、扫码签到、签退结算、管理员实时查看座位状态。这个项目对两类人很有价值一类是自习室运营者它能以极低成本替代纸质登记和人工排座另一类是前后端学习者它能完整体验从需求分析、接口设计到部署上线的项目闭环。1. 需求拆解共享自习室里每天高频发生的三类问题1.1 座位占用不透明是第一个要解决的问题传统自习室最常见的矛盾是“我选了这个座位来了却发现被别人占了”。这种现象的根源在于座位状态没有实时同步。共享自习室签到管理系统要做的第一件事就是把座位抽象成带状态的对象空闲、使用中、停用、预约中。每个座位在数据库中对应一条记录前端页面通过接口实时读取这些状态用户选座和签到都基于同一份数据源而不是靠管理员手动更新。座位状态变化的核心路径很清晰用户进入选座页时看到空闲座位点击签到后该座位变成使用中用户离开并签退后座位恢复为空闲。这条路径看起来简单但真正实现时要考虑异常场景比如用户签到了却一直不走、用户离开时忘记签退、两个用户同时点击同一个空闲座位。这些异常场景决定了系统不能只做简单的状态修改必须引入状态机和防重机制。1.2 到馆时长和付费统计决定系统要不要做账如果自习室是免费开放给本校学生使用的统计到馆时长只是为了生成学习报告那计费模块可以简化。但如果是商业运营的共享自习室按时长计费是核心营收手段签到管理系统就必须精确记录每次入座的开始时间、结束时间和总时长。我建议在项目设计初期就区分两种模式免费模式和计费模式。免费模式只需要记录签到时间、签退时间、总时长。计费模式要额外引入余额、充值、消费明细、欠费状态。技术实现上前端页面在登录时展示用户余额签到成功时先做余额预检查签退时再按实际时长扣费。为了避免复杂的财务对账第一版可以不做实时扣费改为每天凌晨定时任务统计前一天的签到记录并批量结算这样的实现成本低运营方也能接受。1.3 管理端看板是运营者真正离不开的功能共享自习室运营者最关心的不是某个用户什么时候来而是整体座位利用率今天有多少人签到、平均每人坐多久、哪些时段上座率最高、哪些座位长期闲置。这些数据需要从签到记录中聚合出来。第一版的管理端看板可以只做三个核心指标今日签到次数、当前在馆人数、今日总时长。然后按小时统计上座率变化用折线图展示在管理员首页。数据统计不要在前端循环算而是后端写 SQL 聚合接口减少前端计算压力。对于运营者来说看到“晚上 19 点到 21 点是高峰期需要增加临时座位”这样的结论比看到一堆原始签到流水更有价值。2. 技术选型Spring Boot Vue Web 这套组合的取舍2.1 为什么不用更轻的方案也不选更重的方案开发这种管理系统可选的路有很多用原生 PHP 加页面、用 Python Flask 加模板、用 Node.js 加 React甚至直接用低代码平台。我最终选择 Spring Boot Vue 前后端分离主要原因是这套组合在中小型系统里扩展空间最大踩坑资料也最全。Spring Boot 的价值在于它已经把 Web 开发中大量繁琐的配置自动化了。内嵌 Tomcat、Starter 依赖管理、自动配置数据源能让开发者把精力集中在业务逻辑而不是环境搭建上。项目一旦需要接入消息队列、定时任务、分布式缓存Spring Boot 生态都有成熟方案。Vue 则解决了 DOM 操作和页面状态管理的效率问题。选座页面是一个座位网格每个座位有不同状态用户点击某个空闲座位时页面局部更新状态并弹出确认框这种交互用原生 JS 写非常容易出错用 Vue 的响应式数据驱动则很自然。2.2 客户端形态选 Web 而不是小程序或 App我也认真考虑过小程序方案毕竟共享自习室用户扫码签到在小程序里体验更顺畅。但小程序需要注册开发者账号、审核、维护类目对于技术学习者或校园内部项目来说门槛偏高。Web 方案最大的优势是免安装、低成本电脑浏览器和手机浏览器都能打开配合浏览器中的“添加到主屏幕”功能也能获得近似 App 的入口体验。Web 方案的另一个优势是后端接口可以完全复用。以后如果真要出小程序版只需要重新做一层小程序前端Spring Boot 接口几乎不用改。所以在设计接口时我尽量保持接口的通用性不在接口层写任何针对特定前端的逻辑。2.3 版本选型里最容易被忽略的坑JDK 版本匹配Spring Boot 的版本升级带来一个很现实的兼容问题。Spring Boot 3.x 强制要求 JDK 17 及以上如果团队开发环境的 JDK 还是 8盲目引入最新版 Spring Boot 会导致项目启动直接报错。热词里出现“springboot版本太高”说的就是这种场景。我第一版用的 Spring Boot 2.7.x因为它支持 JDK 8兼容性最好而且能正常引入 Spring Data JPA、Spring Security、Redis 等核心依赖。如果从零开始新项目且开发机已经装了 JDK 17可以直接上 Spring Boot 3.x后续维护周期更长。记住一点先确认 JDK 版本再决定 Spring Boot 大版本不要用 IDEA 默认的 Spring Initializr 直接生成最新版。由于这里提到 JDK 1.8 打包 Docker 的问题建议后续如果需要容器化部署使用 Dockerfile 指定eclipse-temurin:8-jre基础镜像来规避宿主机 JDK 版本不一致的问题。2.4 前后端分离架构下的部署拓扑系统部署时前端构建出的 dist 目录是纯静态资源可以用 Nginx 托管后端是一个可执行 Jar 包运行在服务器上。用户在浏览器访问 Nginx 的 80 端口Nginx 把/api开头的请求反向代理到后端的 8080 端口其它请求直接返回静态页面。这样一个域名就能搞定不用处理跨域问题是最省心的做法。3. 后端落地座位状态机与签到防重是核心3.1 数据库建模从用户表到签到记录表我先梳理四张核心表用户表、座位表、签到记录表、预约表。用户表保存手机号、昵称、密码哈希、余额座位表保存座位编号、所在区域、状态签到记录表保存用户 ID、座位 ID、签到时间、签退时间、时长、费用预约表保存用户对某个座位的预约状态。座位表的状态字段我建议用整型而不是字符串0 表示空闲1 表示使用中2 表示停用3 表示预约中。整型字段占空间小查询快代码里用常量类读起来也很清晰。签到记录表的关键索引是用户 ID 加签到时间用于快速查询某个用户的签到时序另一个索引是座位 ID 加状态用于判断座位当前是否被占用。CREATE TABLE seat ( id BIGINT PRIMARY KEY AUTO_INCREMENT, seat_no VARCHAR(20) NOT NULL, area VARCHAR(50) DEFAULT A区, status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1使用中 2停用 3预约中, version INT NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.2 接口设计要做好统一返回结构和鉴权前后端分离项目中最忌讳每个接口返回的数据格式都不一样。我定义了一个统一的返回体ResultT包含code、message、data三个字段。成功时 code 为 200业务失败时 code 为 500参数校验失败时 code 为 400。前端拿到统一的返回结构后只需要在 Axios 响应拦截器里统一判断 code不必每个页面重复处理异常状态。签到系统的鉴权使用 Token 方案。用户登录成功后后端生成一个 JWT Token 返回给前端前端把它存在 localStorage 里每次请求在请求头Authorization字段携带。后端拦截器解析 Token把用户 ID 放到 ThreadLocal 中这样接口里直接通过UserContext.getUserId()拿到当前登录用户。RestController RequestMapping(/api/checkin) public class CheckinController { Resource private CheckinService checkinService; PutMapping(/{seatId}) public ResultString startCheckin(PathVariable Long seatId) { Long userId UserContext.getUserId(); checkinService.checkin(userId, seatId); return Result.success(签到成功); } }3.3 高并发场景下如何避免同一座位被重复签到这是整个系统最核心的技术难点。两个用户同时点击同一个座位如果后端只做简单的“先查状态再改状态”会出现两个请求都查到座位是空闲然后都执行更新最终两个用户都签到成功座位状态却只能被一个用户占用。解决这个问题有三种常用手段。第一种是在数据库层面加唯一约束或乐观锁。座位签到记录表可以增加一个唯一索引由座位 ID 和签到日期组成保证同一座位同一天只能存在一条有效的使用中记录。不过这种方式只能防止重复记录不能防止两个请求都走到更新座位状态的逻辑。第二种是使用数据库行锁在更新座位状态前先查询并锁定该行SELECT ... FOR UPDATE这样第二个请求必须等待第一个请求提交后才能读取能真正保证状态同步。第三种是使用 Redis 分布式锁用 seatId 作为 key抢到锁的用户才能执行签到逻辑。我实际采用的是“Redis 分布式锁 数据库状态校验”双重方案。Redis 锁保证同一时刻只有一个请求能操作同一个座位数据库状态校验保证即使锁过期或 Redis 崩溃也不会把已使用中的座位再次分配给他人。实现思路上两个用户在选座时都会看到空闲但点击签到的接口中会先执行setIfAbsent(seatKey, userId, 10, TimeUnit.SECONDS)抢锁抢到后才能继续查询座位状态并执行更新。3.4 超时未签退的自动处理机制用户在座位上一坐就是好几个小时如果中途离开忘记签退座位就一直处于使用中状态管理员必须手动修改。这种人工干预多了系统就失去了自动化的意义。我的做法是设计一个定时任务每隔 5 分钟扫描一次签到记录表发现“签到时间超过连续在线时长上限但未签退”的记录就自动执行签退逻辑并根据实际在线时长计算费用。这里的连续在线时长是可配置参数共享自习室一般设置为 4 小时到 8 小时避免用户通宵占座。自动签退前最好给用户一个提醒但短信通知在早期项目里接入成本高可以简化成前端轮询接口当系统检测到即将超时时在用户当前页面弹窗提醒。如果用户未响应定时任务到期后就强制执行签退同时释放座位。3.5 用户主动签退的业务一致性主动签退时后端要同时完成三件事更新座位状态为空闲、更新签到记录的结束时间和时长、计算费用。这三件事必须放在同一个数据库事务里任何一步失败都要整体回滚。我在签退接口上加了Transactional并先更新签到记录再更新座位状态这个顺序可以避免座位已经释放但签到记录还没落库导致的账实不符。4. 前端实现从选座页到管理看板的页面串联4.1 用 Vite 初始化并规划目录结构前端项目直接使用 Vite 创建 Vue 3 项目速度快热更新体验比 Vue CLI 好。目录结构按业务模块划分src/api放所有接口请求函数src/views放页面组件src/router放路由配置src/store放全局状态。在项目初期就划分好目录会让后续维护轻松很多。组件设计上座位网格是一个关键组件。每个座位渲染成一个小方块颜色表示状态绿色是空闲红色是使用中灰色是停用。用户点击绿色方块时弹出确认提示确认后调用签到接口。签到成功后方块变红页面顶部显示当前已签到状态。4.2 Axios 拦截器与路由守卫Axios 拦截器负责两件事携带 Token 和统一处理 401。请求拦截器里从 localStorage 读取 Token附加到请求头。响应拦截器里如果发现返回状态码是 401说明 Token 过期或未登录直接跳转到登录页并清空本地用户信息。这样每个接口调用都不需要重复写鉴权逻辑。路由守卫控制页面访问权限。系统有两类角色普通用户和管理员。普通用户不能进入/admin开头的路由管理员可以进入任意页面。我在路由元信息里设置requiresAdmin字段然后在全局前置守卫中判断当前用户角色是否匹配。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) return } if (to.meta.requiresAdmin store.user.role ! ADMIN) { next(/) return } next() })4.3 选座与签到交互要兼顾手机端体验共享自习室的签到场景大部分发生在手机浏览器上所以页面布局必须适配窄屏。选座页用 CSS Grid 布局座位网格每行座位数量根据屏幕宽度自适应。手机端更实用的交互是“扫码签到”在自习室每个座位张贴二维码二维码内容指向当前系统域名下的签到页链接参数携带座位编号。用户用手机扫码后自动跳到签到确认页页面显示座位号和用户名点击确认即完成签到。二维码功能可以用 Vue 的二维码库在前端生成也可以在后台管理系统里为每个座位生成二维码图片并导出打印。我推荐在后台管理端生成因为二维码里包含的是完整 URL前端网络环境中和后端接口域名不同由后端生成更能保证二维码指向正确地址。4.4 管理端看板的图表展示管理员的首页放一张选座实时状态图用不同颜色展示每个座位的状态。下方放签到趋势折线图统计最近七天的每日签到人次。图表库我选择 ECharts它功能全面文档丰富。在 Vue 3 中使用 ECharts 需要注意生命周期销毁问题在组件卸载时调用echarts.dispose释放实例避免页面切换后内存泄漏。后端统计接口返回的数据结构要和图表需求对齐。比如折线图需要返回[{date: 2025-01-01, count: 120}, {date: 2025-01-02, count: 96}]这样的数组。前端拿到后直接填入 ECharts 的 series data 就行不要在前端做复杂的日期补全。5. 联调、部署与几个印象深刻的坑5.1 开发环境的跨域问题处理前后端分离开发时前端运行在 Vite 的 5173 端口后端运行在 8080 端口浏览器直接请求后端接口会被 CORS 拦截。最简单的处理方式是在 Vite 配置中设置代理所有/api请求都转发到http://localhost:8080。这种代理方式的好处是开发环境不需要后端开启跨域支持更贴近生产环境的 Nginx 反向代理模式。如果后端也临时允许跨域可以使用 Spring Boot 的CorsFilter配置但在开发阶段建议统一用 Vite 代理减少混乱。5.2 使用 Nginx 部署前端静态资源前端打包后的 dist 目录直接复制到服务器的/usr/share/nginx/html目录下然后修改 Nginx 配置。核心配置是把/api路径反向代理到后端服务的地址。如果不做这个代理前端页面能打开但所有接口都会 404。server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index 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_pass后面是否带/会影响转发路径。如果后端接口路径是/api/checkin/list那么 Nginx 配置proxy_pass http://127.0.0.1:8080;会把完整路径原样转发给后端。如果写成proxy_pass http://127.0.0.1:8080/;实际的转发路径会变成/checkin/list后端就会处理不了。我建议保持不带斜杠的写法并在后端 Controller 的 RequestMapping 中统一使用/api前缀这样逻辑更清晰。5.3 后端 Jar 包部署时内存不足的问题服务器内存如果是 2G 以下Jar 包默认的 JVM 参数可能会因为内存不足而启动失败或运行中频繁 GC导致接口响应很慢。我通常在启动命令中显式指定堆内存nohup java -Xms256m -Xmx512m -jar checkin-system.jar --spring.profiles.activeprod app.log 21 这样设置后Spring Boot 应用即使运行在 1G 内存的小服务器上也能稳定工作。注意不要把-Xmx设置得过大否则服务器整体内存不够时会触发操作系统层面的 OOM整个机器都会卡顿。5.4 一次真实的并发故障排查过程系统上线后测试同学反馈同时用两个账号点击同一个座位的“签到”按钮两个账号竟然都显示签到成功。我马上查看数据库发现座位状态是使用中但签到记录表里确实存在两条不同的用户签到记录。问题就出在我当时第一版没有加 Redis 锁也没有唯一索引两个请求都通过了“状态为空闲”的校验然后都执行了插入记录操作和座位状态更新。定位过程很快因为我在 Service 层打印了每个请求的执行日志日志里能看到两个请求几乎同一毫秒进入方法。修复方案就是我前面说的 Redis 锁加数据库状态双重校验。经过修复后我做了多轮并发测试用 JMeter 模拟 50 个线程同时请求同一座位最终只有一个请求成功其余请求都返回“座位已被占用”。这里还把 seat 表增加了version字段后续可接入乐观锁让并发控制更稳妥。5.5 关于部署环境的两个提醒热词里提到“springboot jdk1.8打包到docker desktop”如果你也希望用 Docker 部署后端注意选择与 JDK 版本匹配的基础镜像。项目若使用 JDK 8 构建就使用eclipse-temurin:8-jre若使用 JDK 17 构建就使用eclipse-temurin:17-jre。不要在 Docker 容器里再装 SDK直接复用运行镜像即可镜像体积会小很多。另外数据库容器和应用容器如果都部署在同一台服务器上建议使用 Docker Compose 统一管理MySQL 数据目录挂载到宿主机避免容器删除后数据丢失。这种部署方式对后续学习也有帮助能更直观地理解容器间网络配置。6. 从“能跑”到“好用”我补上的几个运营侧细节6.1 离座保护功能让座位状态更真实用户签到后中途去接水、上厕所座位仍然是使用中状态这时候如果被别人看到座位没人会引起误会。我在系统里增加了一个“临时离开”功能用户可以主动把座位标记为临时离开离开期间座位状态变为“暂离”在外观上区别于正常使用中。达到可配置的时间上限后座位自动释放原用户的签到记录自动签退。这个功能解决了自习室中常见的“人去楼空但占着座位”的尴尬。实现这个功能并不复杂在座位状态字段中增加一个 4 表示暂离在签到记录表增加一个字段区分正常在线和暂离。前端轮询当前签到状态时如果发现处于暂离状态就显示倒计时提示用户尽快返回。6.2 签到提醒和统计周报提升用户粘性系统上线后运营者最常问的一个问题是“怎么让用户每天都能来”。我加了定时任务每天晚上八点检查当天未签到的用户给他们绑定的微信消息模板推送一条提醒。不过微信模板消息需要服务号权限个人项目无法直接接入所以我没有真正推到微信而是做了站内消息提醒用户打开系统时首页弹窗提示昨天的学习时长和本周连续签到天数。对于用户来说能够看到自己的累计学习时长和连续签到记录会比冷冰冰的消费记录更有动力。我在管理端增加了一个简单的用户排行列表展示今日时长 Top10进一步增强使用者之间的良性竞争。6.3 座位状态实时感知与打印机联动后台管理端上我增加了“座位概览”页面通过颜色区分状态并实时刷新。管理员可以一眼看出哪些座位释放了哪些座位即将到达临时离开上限。如果自习室有打印机还可以通过后端生成二维码导出功能批量打印座位二维码一张 A4 纸放 20 个二维码贴到对应桌角即可。这个设计听起来简单但对实际运营效果帮助很大。没有二维码时用户需要在系统里找到座位号再主动签到扫码则把签到动作从“搜索座位”变成了“扫一下贴纸”路径短了很多。第一版上线后我观察用户习惯变化扫码签到的比例远远大于手动选座签到。6.4 给同一项目接入 Spring Boot Admin 做运行监控当系统跑了一段时间后我开始担心接口响应变慢或者内存泄漏。这时候可以引入 Spring Boot Admin这是一个轻量级监控组件能展示应用的内存、线程、接口调用情况。它不需要侵入业务代码只在 pom 中引入依赖并在配置文件中指定服务地址就能在管理页面实时看到健康状态。对于共享自习室这种中小型系统没有必要引入微服务全套监控Spring Boot Admin 已经足够。我把它作为一个可选的扩展点推荐给学习这个项目的同学等你的系统用户量从几十涨到几百时你会发现能直观看到堆内存使用情况和 GC 频率对排查问题非常有帮助。6.5 如果重新做一次我会把时间花在哪里整个项目开发过程中前端页面、选座动效和图表展示对视觉冲击力最强也最容易让人有成就感。但如果让我重新做一次我会把更多精力放在接口幂等、座位状态一致性和自动化测试上。因为前端页面再炫一旦后端出现座位状态错乱运营人员就需要频繁手动清理数据用户体验会急转直下。我最后的实操体会是在后端 Service 层把座位状态变更的审计日志完整打印出来日志里带上当前操作用户 ID、座位 ID、变更前状态、变更后状态和操作类型。这样一旦出现状态异常你可以直接通过日志快速定位是哪个接口、哪个操作导致的排查效率提升非常明显。对于这种业务状态类系统一份清晰的状态变更日志比任何文档都有价值。