十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot签到打卡系统设计:数据库、防重与定时任务实战

SpringBoot签到打卡系统设计:数据库、防重与定时任务实战 简介这是一份基于SpringBoot的签到打卡系统完整毕业设计源码主要面向Java方向毕业生、课程设计学生以及希望快速搭建签到场景的开发者。项目包括前端HTML/CSS/JavaScript页面、后端Java业务逻辑以及数据库初始化脚本覆盖用户登录、签到记录、打卡统计等常见模块代码注释齐全便于理清控制层、服务层与数据访问层的调用关系。压缩包共88个文件其中42个Java源码、11个JavaScript、7个HTML、6个CSS、SQL脚本等构成了主体内容另有YML配置文件和说明文档辅助项目配置整体压缩包仅2.1MB部署轻量。目前已有163人学习下载。资源内配有数据库脚本、文档说明和经过调试的源码既适合作为毕业设计或期末大作业提交也能帮助SpringBoot学习者通过实际项目掌握签到类系统的开发流程与目录结构。1. 用 SpringBoot 做签到打卡系统究竟在解决什么问题很多第一次做 Java 毕业设计的人会低估签到打卡系统——表面看只是进来点一下、出去点一下。真正动手才发现一个能过审的出勤系统要处理时间窗口判定、跨天补卡、请假抵消缺卡、月末统计合并这一连串业务约束。SpringBoot 的价值就是把约束组织成清晰的模块边界权限、打卡、请假、统计各管一摊互不干扰。这个选题技术点不新但覆盖面很广SpringBoot 自动装配、MyBatis Plus 持久层、Redis 幂等控制、定时任务状态修正全都能在这套源码里找到对应位置。对毕业生来说它是难度适中但能展示完整工程能力的题目对评审者来说表设计有没有考虑并发、打卡规则是硬编码还是可配置一眼就能看出水平。下面从数据库表结构讲起——这是拿到源码后最该先看懂的部分。2. 签到打卡系统的模块划分与数据库表设计2.1 按业务流拆模块权限、打卡、请假、统计签到打卡系统按源码常见结构分为五个功能域用户与角色管理、签到打卡、请假审批、出勤统计、系统参数配置。对应到 SpringBoot 工程里就是 controller / service / mapper / entity 四层结构下五条独立的业务链路。这样拆分的好处是修改打卡规则时不需要动统计模块统计口径变化时也不影响打卡写入耦合被尽量限制在表层面而不是代码层面。权限设计上员工或学生角色与管理员分离用 Spring Security JWT 做登录鉴权。角色只分两种时一个PreAuthorize(hasRole(ADMIN))方法级注解就能控制管理端接口不必引入 RBAC 多表联查。如果源码里出现了独立的 role 表多半是导师要求扩展属于可选项而非必需项。还有一个容易被忽略的点密码存储必须用 BCrypt 哈希任何把密码明文存进数据库的做法在代码评审阶段都会被直接打回。2.2 核心表结构用户表、打卡流水表、请假表数据库设计是整个系统最值得花时间的部分也是 java 面试题里常被追问的知识点。以 MySQL 8.0 为例三张核心表的职责划分如下表名职责关键字段sys_user用户与角色id, username, password, nickname, roleattendance_record打卡流水核心id, user_id, check_date, punch_in_time, punch_out_time, statusleave_request请假单据id, user_id, start_date, end_date, reason, status打卡流水表的设计要点是一天一行而不是一次打卡一行。理由很直接统计这个月出勤几天时按日期分组远比把多条打卡记录拼成一天再判断要快SQL 也能保持简单。建表 SQL 如下CREATE TABLE attendance_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, user_id BIGINT NOT NULL COMMENT 用户ID关联 sys_user.id, check_date DATE NOT NULL COMMENT 打卡日期, punch_in_time DATETIME NULL COMMENT 上班打卡时间, punch_out_time DATETIME NULL COMMENT 下班打卡时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-正常 1-迟到 2-早退 3-缺卡, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 记录创建时间, UNIQUE KEY uk_user_date (user_id, check_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT打卡流水表;uk_user_date唯一索引是这条 SQL 的关键Service 层的判重只防正常流程直接对数据库发起的并发请求最终会被唯一索引兜底拦住。punch_in_time和punch_out_time允许为 NULL因为只打了上班卡没打下班卡是合法业务场景缺的是下班时间而不是整条记录。status存计算结果而非原始时间统计模块按月分组后按 status 计数即可不需要再做时间比较。2.3 请假表与统计表的设计边界请假表结构简单但有一个容易踩的坑请假跨天。start_date和end_date按 DATE 存比如周五 18:00 请到下周一 09:00业务上需要逐日拆分并跳过周末。稳妥做法是单独建请假明细表按天展开或者干脆在 Service 层遍历日期逐天判断。对毕设规模来说Service 层遍历更直观答辩时也更容易讲清楚为什么这样处理跨天。统计结果要不要落表我的建议是不落。出勤统计是派生数据每天凌晨由定时任务根据 attendance_record 重算或用户查看时实时计算。派生表一旦落库就要处理补录打卡后统计不更新的同步问题——睡前修改一条打卡记录早上的报表就对不上了这种数据不一致在答辩演示时几乎是灾难性的。派生数据随时可重算保持无状态是这里唯一正确的选择。3. 打卡状态判定与 Service 层防重设计3.1 迟到、早退、缺卡的判定规则状态判定是签到打卡系统的业务核心也是答辩时最容易追问的点。先明确规则每个工作日有两次打卡动作上班打卡时间晚于 09:00 判定迟到下班打卡时间早于 18:00 判定早退整天只有一次打卡或没有打卡判定缺卡。实际项目中 09:00 和 18:00 不应硬编码进 Java 代码而是从配置表读取规则参数如下参数名默认值说明work_start09:00上班时间work_end18:00下班时间late_grace5迟到宽限分钟数收到打卡请求后Service 层的处理顺序是先查当天是否已有记录再根据当前时间和配置时间比较最后写入并更新状态。宽限时间的正确理解是上班时间加 5 分钟在 09:00 到 09:05 之间打卡不算迟到而不是迟到后 5 分钟内补卡两者在产品语义上有本质区别前者是规则后者是补救。3.2 核心打卡 Service 实现Override Transactional(rollbackFor Exception.class) public CheckinResult checkin(Long userId, LocalDateTime now) { LocalDate today now.toLocalDate(); AttendanceRecord record attendanceMapper.selectOne( new LambdaQueryWrapperAttendanceRecord() .eq(AttendanceRecord::getUserId, userId) .eq(AttendanceRecord::getCheckDate, today) ); LocalTime configStart configService.getWorkStart(); // 09:00 LocalTime configEnd configService.getWorkEnd(); // 18:00 if (record null) { record new AttendanceRecord(); record.setUserId(userId); record.setCheckDate(today); record.setPunchInTime(now); if (now.toLocalTime().isAfter(configStart.plusMinutes(5))) { record.setStatus(1); // 迟到 } else { record.setStatus(0); // 正常 } attendanceMapper.insert(record); return CheckinResult.success(上班打卡成功, record.getStatus()); } record.setPunchOutTime(now); if (record.getPunchInTime() ! null now.toLocalTime().isBefore(configEnd)) { record.setStatus(2); // 下班早退 } attendanceMapper.updateById(record); return CheckinResult.success(下班打卡成功, record.getStatus()); }这段代码用Transactional保证插入和更新要么全部成功要么回滚。LambdaQueryWrapper是 MyBatis Plus 的条件构造器避免手写字符串拼接 SQL也顺带绕开了 SQL 注入风险。两个细节需要说明一是selectOne依赖uk_user_date唯一索引如果表里少了这个索引并发请求会直接抛 DuplicateKeyException 而不是返回单条记录二是迟到判断用isAfter而不是compareTo时间比较在精确到秒时边界条件最容易出 bug统一用一个方法比混用更稳。3.3 跨请求重复提交Redis 幂等键Transactional管不住两个同时到达的 HTTP 请求——它们各自开启事务可能都在数据库层查到没有记录然后分别执行 insert最后被唯一索引拦住一个。此时业务上正确的表现是第二个请求提示重复打卡而不是抛 500。常见做法是在 Controller 层用 Redis 做前置过滤PostMapping(/checkin) public Result? checkin(RequestHeader(Authorization) String token) { Long userId userContext.getUserId(token); String key checkin: userId : LocalDate.now(); Boolean first redisTemplate.opsForValue() .setIfAbsent(key, 1, Duration.ofHours(24)); if (Boolean.FALSE.equals(first)) { return Result.error(今日已完成签到请勿重复提交); } try { return checkinService.checkin(userId, LocalDateTime.now()); } catch (Exception e) { redisTemplate.delete(key); // 业务失败释放键允许重试 throw e; } }setIfAbsent对应 Redis 的 SETNX 命令原子完成判断不存在才写入天然适合做幂等标记。两个细节值得注意键里带LocalDate.now()第二天自动生成新键无需手动清理业务抛异常时主动删除键否则用户第一次打卡因数据库异常失败后当天会被 Redis 拦在门外。很多源码会漏掉 catch 里的删除逻辑评审时能主动说出这一点含金量比报一堆注解名高得多。3.4 定时任务修正隔天状态如果配置了跨天值班或补卡场景光靠打卡时的即时判断不够需要每天凌晨跑定时任务核对前一天有 record 但缺 punch_out_time 的置为缺卡请假已通过的日期跳过不统计。用 Spring 自带的Scheduled即可毕设规模不需要引入 XXL-Job 这类分布式任务调度框架Scheduled(cron 0 0 2 * * ?) public void dailyCheck() { ListAttendanceRecord records attendanceMapper.selectList( new LambdaQueryWrapperAttendanceRecord() .eq(AttendanceRecord::getCheckDate, LocalDate.now().minusDays(1)) ); for (AttendanceRecord record : records) { if (record.getPunchOutTime() null !hasApprovedLeave(record)) { record.setStatus(3); // 缺卡 attendanceMapper.updateById(record); } } }cron 0 0 2 * * ?表示每天凌晨 2 点执行。定时任务要保证幂等性如果任务跑了两遍第二遍不应产生额外影响。这段代码天然幂等因为缺卡是最终状态重复执行只是把 status 从 3 再设回 3。如果写成每次执行统计值加 1之类的逻辑就必须先查状态再决定是否更新否则跑两次数据就错了。4. application.yml 配置与源码启动排错4.1 关键配置项拆解拿到源码的第一步是改配置90% 的启动失败都发生在这个环节。一个典型的 SpringBoot 配置文件如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/checkin_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver data: redis: host: localhost port: 6379 database: 0 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0serverTimezoneAsia/Shanghai必须显式声明否则 MySQL 驱动会报无法解析服务器时区的错。map-underscore-to-camel-case开启后数据库字段check_date自动映射到 Java 属性checkDate这是 MyBatis Plus 的核心配置。logic-delete-field: deleted要特别注意只有当表里真的存在deleted字段时才配置否则所有查询都会被自动追加WHERE deleted0出现明明有数据却查不到的诡异现象。多环境部署时建议把数据库密码放到application-dev.yml单独维护避免生产配置误提交。4.2 数据库初始化与验证源码通常会附带sql/init.sql数据库脚本命令行和图形化工具都可以执行mysql -u root -p sql/init.sqlmysql -u root -p checkin_db sql/backup.sql第一条命令建库建表第二条导入测试数据。顺序不能反库不存在时第二条会失败。导入后先跑一条验证 SQLSELECT check_date, COUNT(*) FROM attendance_record GROUP BY check_date ORDER BY check_date DESC LIMIT 7;返回最近 7 天每天都有记录说明数据初始化成功可以启动应用验证接口。如果脚本文件字符集不是 utf8mb4导入中文会出现乱码用--default-character-setutf8mb4参数重跑即可。管理员初始账号通常也是脚本里 INSERT 进去的默认密码大多是admin123之类的固定值登录前先确认数据库里存的 BCrypt 哈希和代码里的加密方式一致。4.3 三个高频启动报错对照报错信息原因处理方式Access denied for user rootlocalhost数据库账号或密码错误核对 yml 与 MySQL 实际密码Unknown database checkin_db数据库还未创建先执行 init.sql 中的 CREATE DATABASEFailed to configure a DataSource数据源配置缺失或拼写错误检查 spring.datasource 段落排查启动问题看异常栈有个技巧真正原因在Caused by之后的几行上面所有at com.xxx.xxx都只是调用链记录。另一个常见适配问题是 MySQL 驱动版本与数据库版本不一致8.0 的 MySQL 必须用com.mysql.cj.jdbc.Driver不要在 pom.xml 里手动写旧版驱动坐标。让项目继承 Spring Boot 父工程统一管理依赖版本比逐个钉死版本号省心得多也顺带规避了高版本 SpringBoot 与旧驱动不兼容的坑。5. 答辩加分项二维码签到与并发验证5.1 二维码签到复用 Redis 校验 token毕设答辩时纯 CRUD 的签到系统很难拿高分。二维码签到是投入小、效果直观的增强方案管理员后台生成今日签到码后端用 ZXing 库把随机 token 编码成二维码图片展示在前端学生扫码后把 token 随打卡请求提交Service 校验通过后再写打卡记录String token UUID.randomUUID().toString().replace(-, ); redisTemplate.opsForValue().set(qrcode: today, token, Duration.ofMinutes(60)); // 前端再把 token 封装成 JSON 内容用 ZXing 生成二维码图片打卡接口里先比对请求携带的 token 与 Redis 中的值不匹配直接返回签到码已失效。整条链路同时演示了 Redis 的过期策略和业务鉴权比单纯讲解 CRUD 有说服力得多。注意 token 校验失败时不能把 Redis 里的 token 删掉否则一个学生误操作会把所有人的签到码都弄失效。5.2 并发验证用 JMeter 证明防重生效最后一个技巧是验证。别只点 Postman 手工测JMeter 开 50 个线程同时请求/api/checkin观察返回结果预期只有少数请求成功其余被 Redis 幂等键拦截并返回请勿重复提交。把聚合报告里异常率字段和成功请求的数量一起截图放进毕设文档——它直接证明数据库唯一索引和 Redis 双重防线在并发下的真实表现这份测试证据比任何口头描述都有力。本文还有配套的精品资源点击获取
返回列表