ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SpringBoot综合签到系统:从表结构到幂等控制的设计实践

SpringBoot综合签到系统:从表结构到幂等控制的设计实践 简介这是一份面向计算机专业应届毕业生与课程设计学习者的 SpringBoot 综合签到打卡系统毕业设计资料包含项目源码与配套数据库脚本可用于毕业设计选题落地、课程作业参考或 Java Web 入门练习。项目以 Spring Boot 为核心框架结合前端页面与数据库完成签到打卡主流程适合作为从需求梳理到代码实现的整包参考。压缩包共 88 个文件约 2.16MB42 个 Java 源文件承担后端业务与接口实现JavaScript、HTML、CSS 构成前端交互页面yml 负责项目与数据源配置sql 提供建表与初始化数据整体轻量便于本地导入运行与二次改造。目前已有 212 人学习下载。读者可获得一套可运行的签到打卡项目结构了解控制层、服务层、实体与持久层的组织方式以及签到记录、用户权限等模块的实现思路数据库脚本可直接导入便于对照表结构理解业务字段配置文件与页面资源齐全方便替换界面或扩展统计功能适合作为答辩演示与功能扩展的起点。1. 一个签到打卡系统为什么值得认真做一遍综合签到打卡系统这类题目看着像课程设计实际上把 Java 面试里最常考的几个点都串在了一起。打卡动作要处理重复提交考勤规则要处理时间边界统计报表需要条件聚合 SQL再加上用户、部门这类基础数据正好覆盖 SpringBoot 框架从配置到接口的完整链路。这套源码和数据库脚本的价值不在于功能有多花哨而在于它给你一个可以对照的完整工程能看懂表结构能跑通接口能解释清楚为什么这样设计。适合正在准备毕业设计的学生也适合刚学完 SpringBoot、想补一个完整项目经验的人。2. SpringBoot 综合签到系统的模块划分与数据模型2.1 先看库再看代码三个核心业务域怎么切拿到项目压缩包不要急着用 IDEA 打开整个工程。我一般先解压花 10 分钟看一遍 sql 目录下的数据库脚本再决定从哪块代码切入。综合签到打卡系统从业务上可以切成三个域签到域负责“记一笔”考勤域负责“算一笔”统计域负责“看结果”。切成三个域之后Controller 只做参数接收和响应封装Service 管业务判定Mapper 管纯 SQL这就是分层思想在真实项目里的落点。切分的时候要注意一个边界迟到、早退的判定不该放在签到接口里。签到接口只负责写入原始打卡时间状态字段可以留空由考勤模块后续回算。这样做的原因有两个。第一考勤规则随时可能改如果把判定写死在签到接口里规则一改就要动核心链路。第二打卡时刻判定需要同时知道上班时间和下班时间签到时只有一边数据判断不了早退。所以模块拆分不只是代码整洁的问题它直接决定规则变更时需要改多少个文件。2.2 表结构设计唯一索引解决一天只能打一次卡下面这份建表脚本是这类系统常见的设计去掉了部门表、审批表这些次级模块保证跑通核心逻辑不需要理解太多前置概念。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, username VARCHAR(64) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(128) NOT NULL COMMENT BCrypt加密后的密码, real_name VARCHAR(32) DEFAULT NULL COMMENT 姓名, dept_id BIGINT DEFAULT NULL COMMENT 部门id预留字段, status TINYINT DEFAULT 1 COMMENT 1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE attend_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 用户id, sign_date DATE NOT NULL COMMENT 打卡日期, sign_in_time DATETIME DEFAULT NULL COMMENT 上班打卡时间, sign_out_time DATETIME DEFAULT NULL COMMENT 下班打卡时间, status TINYINT DEFAULT 0 COMMENT 0正常 1迟到 2早退 3缺卡, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_date (user_id, sign_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT打卡记录表; CREATE TABLE attend_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, rule_name VARCHAR(64) NOT NULL COMMENT 规则名称, start_time TIME NOT NULL COMMENT 上班时间, end_time TIME NOT NULL COMMENT 下班时间, late_minutes INT DEFAULT 30 COMMENT 迟到宽容分钟数, is_active TINYINT DEFAULT 1 COMMENT 是否启用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考勤规则表;这里的核心设计在attend_record上的唯一索引uk_user_date。它表达了一个约束“一个用户一天只能存在一条打卡记录”。这个约束放在数据库层比写在 Java 代码里可靠得多。并发场景下两条请求同时查到“今天没记录”都往里面插入没有唯一索引就会产生两条。有了唯一索引第二条插入直接抛 DuplicateKeyException再配合ON DUPLICATE KEY UPDATE就能优雅地降级成更新。字段类型上注意两点。第一sign_date用 DATE 而不是 VARCHAR这样在 SQL 里做范围查询时可以直接走索引。第二late_minutes放在规则表里而不是写死在代码里需求方哪天说“迟到超过 30 分钟才算迟到”改一条数据库记录就行。2.3 数据库脚本与种子数据怎么组织zip 里的数据库脚本常见组织方式是三个文件schema.sql只放建表语句data.sql放测试账号和考勤规则init.sql用 source 命令按顺序引入。这样区分的好处是正式环境中可以分开执行。解压后先看 data.sql 里有什么正常会有一个 admin 账号和两条测试规则。脚本文件内容执行时机schema.sql全部建表语句第一次创建数据库data.sql测试用户、考勤规则初始化或重置环境init.sql引入 schema 和 data完整初始化注意 data.sql 里的测试密码如果是明文登录时大概率用的是 BCrypt 加密后的密文需要先跑一次注册接口生成密文再替换回去。如果项目里预留了注册功能直接用注册接口创建测试账号省去手工加密的步骤。3. 核心打卡接口幂等、时间判定与统计 SQL3.1 打卡接口的幂等控制连点两次也不能产生两条记录用户在打卡页面等网络慢习惯性连点两次这是签到系统最高频的异常输入。处理这个需求不能只依赖前端按钮加 loading接口层必须有能力兜住。后端经验不足的人会写成“先 select 判断再 insert”并发量 1 的时候没问题压测一上来就会出现同时通过判断的情况产生两条当天记录。把前端、后端两层防护拆开看前端禁用按钮降低触发概率后端用唯一索引拦截真正的并发写入。在 SpringBoot 的 Service 层打卡方法推荐写成这样Service RequiredArgsConstructor public class AttendService { private final AttendRecordMapper attendRecordMapper; private final AttendRuleMapper attendRuleMapper; /** * 上班签到 * param userId 当前登录用户id */ Transactional(rollbackFor Exception.class) public AttendRecord signIn(Long userId) { LocalDate today LocalDate.now(); LocalDateTime now LocalDateTime.now(); AttendRecord record new AttendRecord(); record.setUserId(userId); record.setSignDate(today); record.setSignInTime(now); // 第一次插入重复点击时转成对签入时间的更新 attendRecordMapper.insertWithDuplicateUpdate(record); return attendRecordMapper.selectByUserIdAndDate(userId, today); } }对应的 Mapper 语句写在 XML 里insert idinsertWithDuplicateUpdate INSERT INTO attend_record (user_id, sign_date, sign_in_time, status) VALUES (#{userId}, #{signDate}, #{signInTime}, 0) ON DUPLICATE KEY UPDATE sign_in_time VALUES(sign_in_time) /insert人工点击速度远低于数据库写入速度所以实际效果是第一次点插入第二次点触发唯一键冲突走更新分支把 sign_in_time 覆盖成最新时间。这里有一个隐藏的坑如果用户改完手机时间想“穿越打卡”后端拿的是LocalDateTime.now()的服务器时间不接收前端传的时间这样就堵住了改本地时间的漏洞。如果部署环境不在同一时区application.yml 里要把时区写成Asia/Shanghai否则拿到的世界协调时间会比北京时间差八小时。3.2 迟到早退判定不应该发生在签到时点签到接口完成时只有 sign_in_time 一半数据下班签退前无法判断早退所以把迟到早退的判定放在接口里会形成逻辑死角。观察常见实现签到接口只看 start_time 判断是否迟到签退接口再看 end_time 判断是否早退中间规则变化时直接改业务代码最麻烦。更稳的做法是考勤状态推迟到“回算”阶段。每天凌晨用定时任务扫昨天的记录把考勤规则查出来逐一比对再统一更新 status 字段。下面是回算迟到逻辑的一个片段public void recalc(LocalDate date) { ListAttendRule rules attendRuleMapper.findActiveRules(); for (AttendRule rule : rules) { // 找出所有应用该规则的用户的打卡记录 ListAttendRecord records attendRecordMapper.findBySignDateAndRule(date, rule.getId()); for (AttendRecord record : records) { LocalTime startTime rule.getStartTime().toLocalTime(); boolean late record.getSignInTime() ! null record.getSignInTime().toLocalTime().isAfter(startTime); if (late record.getStatus() ! 3) { record.setStatus(1); attendRecordMapper.updateStatus(record); } } } }回算逻辑里有一个业务细节要跟需求方确认迟到了之后早退还算不算不同企业的口径不一样有的迟到一次就不算全勤有的只记录当天异常。这套设计把“什么时候判定”和“判定口径”分开了后面改口径不用动签到接口。判断迟到用isAfter(startTime)而不是用分钟差避免跨天时取负数status 初始值为 0缺卡由回算任务单独把没有打卡记录的用户补成 3回算任务要加幂等保护重复执行时不能把正常状态改成异常3.3 统计报表的 SQL避免在 Java 里循环查询报表模块经常被问到的点是“出勤率怎么算”。粗浅的实现是先把用户列表查出来再循环查每个用户的天数N 个用户就产生 N1 条 SQL这就是常见面试题里说的 N1 查询问题。避免循环查询的正确写法是先把用户一次性查出来再用一条 SQL 按 user_id 聚合。SELECT u.id AS user_id, u.real_name, DATE_FORMAT(a.sign_date, %Y-%m) AS stat_month, COUNT(*) AS should_days, SUM(CASE WHEN a.status 0 THEN 1 ELSE 0 END) AS normal_days, SUM(CASE WHEN a.status 1 THEN 1 ELSE 0 END) AS late_times, SUM(CASE WHEN a.status 2 THEN 1 ELSE 0 END) AS early_times FROM sys_user u LEFT JOIN attend_record a ON a.user_id u.id AND a.sign_date BETWEEN #{startDate} AND #{endDate} WHERE u.status 1 GROUP BY u.id, u.real_name, DATE_FORMAT(a.sign_date, %Y-%m)LEFT JOIN 保证了没有打卡记录的用户也会出现在结果里should_days 为 0方便前端展示。统计 SQL 里必须设置 startDate 和 endDate否则全表扫描数据量上去以后报表接口会明显变慢。顺手加一个(user_id, sign_date)的联合索引这个查询能稳定走索引。4. 从 zip 到能跑的数据库导入脚本与 SpringBoot 联调4.1 用命令行导入数据库脚本解压之后第一步是建库。项目里的 init.sql 如果规范会在开头写上 CREATE DATABASE 语句但保险起见还是手动建库再引入mysql -u root -p -e CREATE DATABASE IF NOT EXISTS attend DEFAULT CHARACTER SET utf8mb4; mysql -u root -p attend sql/init.sql第一条命令指定 utf8mb4 字符集避免中文乱码第二条命令把建表和种子数据一起引入。如果用了 Navicat可以右键数据库选择“运行 SQL 文件”效果一样。执行完后用SHOW TABLES验证三张核心表是否都建出来了再执行SELECT * FROM sys_user看看种子用户有没有进去。4.2 application.yml 三个最容易配错的地方数据库连不上是所有 SpringBoot 项目联调时最先暴露的问题。检查配置的顺序一般是URL 里的时区、账号密码、驱动类。下面这份配置可以直接对照修改spring: datasource: url: jdbc:mysql://localhost:3306/attend?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.attend.entity server: port: 8080配置项常见错误正确的意义serverTimezone不写或写成 UTC决定 LocalDateTime 序列化时用的时区allowPublicKeyRetrieval缺失MySQL 8 使用 caching_sha2_password 时需要mapper-locations写错目录必须和 resources/mapper 路径一致4.3 日志定位与启动失败的排查如果启动后访问接口报 500先看控制台日志里最后一个 Caused by。最常见的是三种表不存在、字段不存在、驱动类报 CommunicationsException。第一种是脚本没执行成功回去补执行 sql第二种是实体字段和数据库字段下划线映射没开在配置里加mybatis.configuration.map-underscore-to-camel-case: true第三种是 MySQL 8 的驱动版本和数据库版本不匹配检查 pom 里 mysql-connector-j 的版本号。用 springboot 框架的默认版本管理时要注意它内部锁定的驱动版本是否兼容本地数据库。5. 让系统抗住并发Redis 缓存与补签考勤的边界处理5.1 用 Redis 拦截当天的重复点击数据库唯一索引已经能保证数据正确性但每次重复点击都会打一次 MySQL高峰时段压力不小。常见做法是在 Service 层前面加一层 Redis用setIfAbsent控制当天是否已经打过卡public AttendRecord signIn(Long userId) { String key attend:sign: userId : LocalDate.now(); Boolean first stringRedisTemplate.opsForValue() .setIfAbsent(key, 1, 1, TimeUnit.DAYS); if (Boolean.FALSE.equals(first)) { throw new BusinessException(今日已签到请勿重复操作); } // 后续插入数据库 }这个方案的优点是接口层可以直接拒绝第二次请求数据库压力小缺点是 Redis 和数据库之间需要处理极端情况下的数据一致性比如 Redis 设置了但数据库插入失败就要手动删除 key。5.2 补签接口与审批状态补签是这个项目里值得展开的功能点。补签接口真正难的地方在于权限什么时候允许补签、由谁审批、审批通过后统计要不要算进去。主流方案是建一张补签记录表包含 target_date、reason、approval_status 三个核心字段统计模块计算考勤时排除未审批的补签记录。CREATE TABLE attend_supplement ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 申请人, target_date DATE NOT NULL COMMENT 补签日期, reason VARCHAR(255) NOT NULL COMMENT 补签原因, approval_status TINYINT DEFAULT 0 COMMENT 0待审批 1通过 2拒绝, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT补签记录表;补签审批通过后后台会往 attend_record 里补一条记录同时打上“由补签生成”的来源标记避免和正常打卡混在一起这也是综合签到打卡系统区别于 demo 项目的一个细节。5.3 写一个不依赖前端就能跑的接口测试springboot 项目自带的测试依赖已经够用。补一个核心接口测试验证连点两次只产生一条打卡记录SpringBootTest AutoConfigureMockMvc Transactional class AttendControllerTest { Autowired private MockMvc mockMvc; Autowired private AttendRecordMapper attendRecordMapper; Test void signInTwiceShouldReturnOnlyOneRecord() throws Exception { mockMvc.perform(post(/api/attend/signIn) .param(userId, 1)) .andExpect(status().isOk()); mockMvc.perform(post(/api/attend/signIn) .param(userId, 1)) .andExpect(status().isOk()); Long count attendRecordMapper.countByUserIdAndDate(1L, LocalDate.now()); assertEquals(1, count); } }这个测试用 Transactional 保证结束后回滚不会污染本地数据。面试时能主动说出“用什么方式防止重复打卡”“为什么唯一索引比先查再插可靠”比背几道 springboot 面试题更能体现项目是真正做过而不是下载下来改个名字的。本文还有配套的精品资源点击获取
返回列表