简介:面向Java毕业设计与课程设计的基于SpringBoot签到打卡系统项目包,适合需要快速搭建完整签到功能的后端学习者,也可作为期末大作业或课程设计的直接参考方案。包内涵盖系统源码、数据库脚本与文档说明,共88个文件,以42个Java源码文件为核心,辅以11个JS脚本、7个HTML页面、6个CSS样式及4个YML配置,并附带SQL初始化脚本,项目结构清晰,下载后简单配置即可运行。系统功能覆盖签到打卡的常见业务场景,界面美观、操作便捷,代码注释详细,能帮助新手理解SpringBoot整合流程与业务模块划分,缩短开发周期。目前已有163人学习下载,项目经严格调试可稳定运行,个人手打高分项目,导师认可度高,非常适合作为毕业设计或课程设计的高质量参考。
1. 签到打卡系统,SpringBoot 毕业设计里最值得拆的一类项目
Java 毕业设计选型时,基于 SpringBoot 的签到打卡系统是少有的“看着简单,拆开有货”的项目。如果只做 CRUD 和登录注册,答辩老师通常会追问“业务逻辑难点在哪”;而签到打卡恰好卡在中间:既有用户、权限、打卡记录这类基础模块,又有日期边界、防重复提交、补卡审批这类真实约束。这套源码把 SpringBoot 后台、MySQL 数据库与前端管理页面打包在一起,开箱能跑、改起来也有依据,适合课程设计、期末大作业,也适合想在简历上写完整业务流的开发者。下面从源码结构入手,拆层看。
2. 源码结构先行:从 pom.xml 到 face_sign_in.sql 看项目分层
2.1 拿到压缩包后先看这几个文件
很多同学解压毕业设计源码后,习惯直接找application.yml改数据库密码,这容易踩坑。我一般会先打开pom.xml,确认 Spring Boot 版本和持久层框架。这套系统里能明显看到 Spring Boot 作为基础框架,配套test目录存放接口冒烟测试,face_sign_in.sql是初始化脚本,.erupt与upms.loaded则提示了后台管理模块使用了 Erupt 这类通用权限配置。先看清楚这些,后续改业务才不会被报错带偏节奏。
提示:
upms.loaded这类文件名通常是权限初始化完成后生成的标记文件,删除后重启服务可能重新执行初始化逻辑,部署时不要随意清理。
下面是一份该项目的标准结构对照,拿到其他 SpringBoot 毕设源码也可以用同样方式去套。
| 路径/文件 | 典型作用 | 关注点 |
|---|---|---|
src/main/java | 控制器、服务、实体、Mapper | 分包是否清晰,controller 是否直接写 SQL |
src/main/resources | application.yml、Mapper XML、静态资源 | 数据源配置、mybatis 映射路径 |
face_sign_in.sql | 建库建表与初始数据 | 表字段是否覆盖用户、签到、打卡规则 |
test | 接口自测 | Spring Boot Test 环境是否能脱离前端独立跑通 |
.erupt | Erupt 后台注解配置 | 管理界面的菜单、权限点定义 |
pom.xml里常用的依赖组合是spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java和lombok。用 MyBatis-Plus 而不是原生 MyBatis 的好处是单表 CRUD 不用写 XML,毕业设计里常见的用户维护、签到记录分页直接在 Service 层调用IService自带方法即可。face_sign_in.sql如果命名为face_sign_in,说明库名可能与签到场景绑定,但也不排除作者只是沿用了一个固定库名,以脚本内容为准。
2.2 用 face_sign_in.sql 反推业务表关系
打开 SQL 脚本,重点看四张核心表:系统用户表、签到/打卡记录表、班级或部门表、签到规则表。用户表一般有id、username、password、role;签到记录表会有user_id、sign_time、status和location之类字段。参数含义我一般这样记:状态字段用来区分“正常签到、迟到、请假、补卡”,时间字段用datetime而非timestamp,否则 2038 年问题会让答辩变成大型翻车现场。
-- 典型签到记录表结构(以脚本为基准按需调整) CREATE TABLE `sign_record` ( `id` bigint NOT NULL 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, PRIMARY KEY (`id`), KEY `idx_user_date` (`user_id`, `sign_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里的联合索引idx_user_date是关键。签到系统最常见的查询是“某个用户某一天或者某段时间的记录”,如果只在id上建主键,数据量到十万级别后分页查询会明显变慢。查询逻辑上,sign_date使用日期类型而不是 varchar,能直接用BETWEEN做范围检索,也方便后续用DATE_FORMAT生成报表。
2.3 业务上为什么要拆“签到”和“打卡”两套概念
很多复刻这个项目的同学会模糊掉两个动作:签到(Sign In)指进入系统确认到场,打卡(Check In/Out)则记录具体时间点。真实场景里,管理员需要分别看到“今天谁没签到”和“谁迟到早退”。因此在代码里往往存在两个服务:SignService负责写第一次进场状态,AttendService负责处理上下班时间和补卡申请。这样拆解后,将来加一个钉钉回调或企业微信通知,只需在对应 Service 层加方法,不会把定时任务和接口逻辑混在一起。
到这里,你已经能从文件结构和数据库表上确认项目大概能做什么。下一章直接进入部署,让浏览器先跑起来。
3. 环境准备与一键启动:让 SpringBoot 签到系统在本地跑起来
3.1 需要的工具版本
准备 JDK 1.8 或更高版本、Maven 3.6+、MySQL 5.7/8.0。如果你的机器上装了太高版本的 Spring Boot(比如 3.x),注意 JDK 要对应 17,否则启动直接报UnsupportedClassVersionError。这套源码我建议先用 JDK 8 + Spring Boot 2.x 组合跑,兼容性最好。用 IDEA 打开pom.xml,等待 Maven 导入依赖,首次下载可能需要几分钟,如果公司内网有私服,优先替换成内网镜像。
# 检查本机环境 java -version mvn -version mysql --version我一般会在启动前先执行这三条命令,确认环境变量没有串版本。很多“项目跑不起来”的第一现场其实不是代码问题,而是 JDK 或 Maven 指向了错误的目录。注意 Maven 的settings.xml里镜像地址不要写成中央仓库,不然下载慢到怀疑人生。
3.2 导入数据库脚本
在 MySQL 中先创建一个空库,再导入face_sign_in.sql。建议库名就叫face_sign_in,避免改了库名后配置文件不同步。导入前注意 MySQL 客户端和数据库服务端要能连通,如果远程连接报Access denied,先确认 root 账号是否允许当前主机登录。
mysql -uroot -p --default-character-set=utf8mb4 -e "CREATE DATABASE face_sign_in DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p face_sign_in < face_sign_in.sql这里的--default-character-set=utf8mb4很关键。如果本机数据库默认字符集是latin1,SQL 里的中文备注和初始化数据在导入后会变成乱码,后端查询返回的数据也就跟着乱。建议导入完成后执行SHOW TABLES;查看核心表是否存在,再用SELECT抽查用户表是否已有默认账号。
3.3 修改数据源配置
打开src/main/resources/application.yml,重点是数据源几项。密码部分如果本地没有密码就留空,不要写成“root”之外的臆造值。如果源码里同时存在application-dev.yml和application-prod.yml,记得确认当前激活的是哪个 profile,否则改了半天发现没走你配置的那份文件。
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/face_sign_in?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root servlet: multipart: max-file-size: 10MB参数说明:characterEncoding=utf8保证中文不乱码;useSSL=false避免本机 MySQL 证书导致的告警;serverTimezone=Asia/Shanghai解决数据库时区比北京时间早 8 小时的问题,否则签到时间会差 8 小时。max-file-size通常用于头像上传,如果项目里有 OCR 或人脸比对图片,调大上限即可。
3.4 启动并验证
mvn spring-boot:run看到类似Tomcat started on port(s): 8080的日志就说明启动完成。然后访问登录接口,常见的默认路径是/login。不要只在浏览器里看页面,先用返回 JSON 的接口验证后端:
curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}'这里用curl发一个 POST 请求,-d指定请求体。如果返回体里有token或者登录成功标志,说明数据库连接、SpringMVC 路由、Service 层链路都是通的。如果返回401,优先检查数据库中的密码是否用 BCrypt 加密,以及初始账号是否为admin/123456。更多细节可以看README.md,但不要迷信里面的文字,以实际 SQL 为准。
3.5 常见启动报错排查
如果启动时报Failed to configure a DataSource,一般是application.yml没被加载或数据源参数错误。先执行mvn clean清理target目录,再重新编译,看配置是否进入classes。另一个常见问题是Invalid bound statement,说明 MyBatis 的 Mapper XML 路径没对上,检查mybatis.mapper-locations是否配置成classpath*:mapper/**/*.xml。如果登录时空指针,多半是初始账号的密码字段不是 BCrypt 结构,先用SELECT * FROM sys_user;确认落到数据库里的值长什么样。
4. 核心链路拆解:签到、打卡、统计报表是怎么串起来的
4.1 登录与权限校验
这个是所有业务模块的前置。项目里常见的做法是用 Spring Security 或 JWT 拦截器。以 JWT 为例,登录成功后会生成一个带过期时间的 token,前端每次请求都把它放在Authorization头里。源码中如果看到OncePerRequestFilter的实现类,说明 token 校验是在过滤器里完成的。
@Component public class JwtAuthFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { // 解析 token 并放入 SecurityContext,后续 Controller 里通过 @AuthenticationPrincipal 获取用户 } chain.doFilter(request, response); } }逻辑说明:每次请求先进过滤器,Authorization头部有值且是Bearer开头才去解析;解析出来的用户信息放到当前线程上下文,后续接口才能拿到“当前操作人”。参数说明:Bearer前缀是 JWT 的通行写法,注意中间有一个空格,少了空格整个 token 解析会失败。
4.2 签到接口的防重复与边界判断
签到接口的核心不是插入一条记录,而是“同一用户在同一个日期只能签到一次,且签到时间必须符合规则”。代码常见写法是先用日期查询,再决定新增或更新。
public SignResult signIn(Long userId, LocalDate signDate, LocalTime signTime) { SignRecord record = signRecordMapper.selectOne( new LambdaQueryWrapper<SignRecord>() .eq(SignRecord::getUserId, userId) .eq(SignRecord::getSignDate, signDate)); if (record == null) { record = new SignRecord(); record.setUserId(userId); record.setSignDate(signDate); record.setSignInTime(LocalTime.now()); record.setStatus(signTime.isAfter(cutoffTime) ? 1 : 0); // 1表示迟到 signRecordMapper.insert(record); } else { // 已经签到,直接返回已有记录,不重复写入 return SignResult.alreadySigned(record); } return SignResult.success(record); }这里的cutoffTime是签到截止时间,比如早上 09:00:00。如果signTime晚于它,状态自动置为迟到。参数上LocalDate和LocalTime分别对应日期与时间,比Date更清晰。但要注意一点:多节点部署时,这种“先查再插入”会存在并发漏洞,两个请求同时查到null,就会插入两条。毕业设计用单机跑没问题,但如果和面试官聊到高并发,可以主动提“使用数据库唯一索引(user_id, sign_date)兜底,配合INSERT ... ON DUPLICATE KEY UPDATE或 Redis 分布式锁来保证幂等”。
4.3 打卡时间段统计与迟到早退计算
打卡记录生成后,报表模块需要按天、按周、按月统计。常见的 SQL 会用到CASE WHEN和DATE_FORMAT,这两个函数也是面试里经常被追问的聚合写法。
SELECT DATE_FORMAT(sign_date, '%Y-%m') AS month, COUNT(DISTINCT user_id) AS total_users, SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS late_count, SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) AS leave_early_count FROM sign_record WHERE sign_date BETWEEN '2025-01-01' AND '2025-01-31' GROUP BY DATE_FORMAT(sign_date, '%Y-%m');这个聚合查询直接从明细表算每个月的迟到人数。COUNT(DISTINCT user_id)是去重统计;SUM(CASE WHEN...)是把条件判断转化为数值累加,比先查全部记录再在 Java 里循环统计高效很多。实际项目里,报表页会提供起止日期参数,前端传过来的是字符串,后端可以用@RequestParam接收后继续拼接查询条件。
4.4 前端页面与管理后台的联动
这套系统除了给普通用户签到,还会给管理员一个管理后台。由于项目里有.erupt配置,后台菜单很可能由 Erupt 注解生成,不需要单独写一堆管理页面。这意味着你改权限时只需要调整实体类上的注解,不用碰到前端 Vue 文件。如果在源码里看到@EruptField之类注解,说明这一层已经和通用后台框架绑定。普通用户端通常会配套一个 H5 或 Vue 页面,通过接口与后端对接。前后端分离下,注意跨域配置:在配置类里允许http://localhost:9528这类开发端口,否则前端页面的请求会被浏览器拦截。
提示:不要把用户表密码用明文存库里。如果 SQL 脚本里已有 BCrypt 加密后的值,保持原样;如果本地测试想快速登录,直接修改数据库密码字段并不安全,建议在代码里写一个临时的
CommandLineRunner来生成加密字符串。
5. 答辩加分技巧:给签到系统加上 Redis 防重复与补卡提醒
如果想让这个项目在答辩时更有说头,不必重写业务,做两次“小手术”就够。这两个改动都不涉及页面调整,却能把重复提交、定时任务、缓存过期这些实际生产问题覆盖到,讲起来也自然。
第一个是引入 Redis 解决“双击签到”问题。在签到接口里先用用户 ID 拼接当天日期作为 key,执行setIfAbsent,只有第一次请求能拿到 true,后面的请求直接返回“请勿重复签到”。
String redisKey = "sign:" + userId + ":" + signDate; Boolean first = redisTemplate.opsForValue().setIfAbsent(redisKey, "1", 24, TimeUnit.HOURS); if (Boolean.TRUE.equals(first)) { // 执行签到逻辑 } else { return SignResult.alreadySigned(null); }这里的24是过期时间,单位由TimeUnit.HOURS指定。注意 key 里带上日期,保证每天第一次操作都生成新 key,不会出现昨天签到过今天签不了的情况。Redis key 过期后,用户又能正常签到,自然形成日级幂等。
第二个技巧是加一个定时任务,每天 18 点扫描当天完全没签到的人。在 Spring Boot 里用@Scheduled(cron = "0 0 18 * * ?")标注一个方法,调用signRecordMapper查当天没有记录的用户列表,把数据写入一张待提醒表或直接对接邮件接口。
@Component public class SignRemindTask { @Scheduled(cron = "0 0 18 * * ?") public void remindMissedSign() { List<Long> missedUserIds = signRecordMapper.selectMissedUserIds(LocalDate.now()); // 逐条发送站内信或邮件,避免大循环影响数据库连接 } }cron表达式里0 0 18 * * ?表示每天 18 点 0 分 0 秒执行,问号位置表示“不指定星期几”,是 Quartz/Spring Schedule 的常见写法。注意要在启动类上加@EnableScheduling,否则定时器根本不生效。验证方法很简单:把 cron 改成每分钟一次,插入一条当天缺少记录的用户数据,观察日志是否输出提醒内容,确认后再改回生产频率。
把这两个改动提交到 Git 后,再重新跑一遍mvn spring-boot:run,用两个浏览器标签同时点击签到,只有一次能成功插入记录。
本文还有配套的精品资源,点击获取