
简介这套基于Java的考勤管理系统完整源码面向Java初学者、毕业设计学生及有中小型企业考勤管理需求的开发者。系统覆盖员工信息、考勤打卡、请假申请、会议管理、固定资产管理等核心业务模块后端Controller-Service-JavaBean分层清晰并含前端交互脚本适合作为SSM或Spring Boot项目的实战参考。压缩包共2000个文件、约55MB以Markdown笔记文档1336个为主辅以500个前端JS脚本、67个Java核心类、55个JSON配置文件及XML、SQL建表语句等涵盖后端逻辑、前端交互与数据库初始化便于按需取用。已有170人学习下载完整工程便于对照业务代码理解考勤规则实现也可移植改造用于课程设计或企业二次开发。1. Java考勤管理系统源码压缩包先看清它解决什么再决定要不要跑你大概率是带着“能不能跑起来、能不能改成自己的”这样的疑问点进来的。这个压缩包解开之后是一个围绕“员工打卡、请假、月度统计”展开的经典 Java Web 项目技术栈通常以 Spring Boot 或 Spring MVC 为骨架配合 MyBatis 操作 MySQL前端用模板页面渲染。它解决的实际问题很简单把行政手里那堆 Excel 考勤表换成一套能录入、能判断迟到早退、能按部门汇总的线上系统。适合两类人一类是课程设计或毕业设计需要完整源码做参考的学生另一类是想快速搭个内部考勤工具、准备二次开发的从业者。这里先说一个反直觉的结论这种项目最容易卡住你的不是考勤算法而是环境匹配和数据库初始化下面按落地顺序把这套路径拆开。2. 解压与项目结构先别急着点运行把代码分层认全2.1 拿到压缩包后的三个动作解压路径、导入方式、依赖检查拿到这种以“考勤管理系统”命名的 zip 包第一步不是急着找 README而是处理解压路径。我一般会先把它放到一个纯英文、无空格的目录下例如 D 盘下的attendance-project。原因很实在很多这类项目里写死了相对路径、日志输出路径或者上传目录如果路径里带中文跑起来会出现各种诡异的找不到文件报错。同时解压工具的编码也值得留意Windows 下用默认解压偶尔会把文件名的 UTF-8 中文拆成乱码建议用支持编码识别的解压工具解压后先看一眼目录名是否正常。mkdir -p ~/workspace/attendance-project cd ~/workspace/attendance-project unzip -q 考勤管理系统源码压缩包.zip -d ./ ls -l这段命令的作用是创建干净的工程目录、进入目录、把压缩包解开并查看解压后的文件列表。-q参数是减少解压过程中的输出避免刷屏-d指定解压目标目录。解压完成后的第一件事是确认有没有.java源文件、pom.xml或者.sql数据库脚本。如果只看到一堆 class 文件而没有源码那这个包只能当成品用没法二次修改。接下来是导入 IDE。这类 Maven 工程最常见的做法是用支持 Maven 的 Java IDE 直接导入pom.xml而不是新建空项目再把文件复制进去。导入后不要立刻点 Run先等右下角依赖下载进度条跑完。这个阶段常见的问题是私服或中央仓库下载缓慢国内网络环境建议在 Maven 的settings.xml里配置阿里云镜像否则卡在下载依赖这一步就能耗掉一小时。依赖下载完成的标准是左侧项目树里没有报红、External Libraries里能看到 Spring、MyBatis 等核心库。2.2 从 pom.xml 读出技术栈先弄清楚它是哪一代的 Spring在跑任何代码之前我会先打开pom.xml把依赖清单读一遍。这个动作能帮你避掉一个最重要的坑项目用的 Spring Boot 版本决定了 JDK 版本、Tomcat 版本和配置写法。很多课程设计项目还停留在老一代 Spring Boot 上如果你本地装的是新版本 JDK启动时大概率会报UnsupportedClassVersionError或者IllegalArgumentException。properties java.version1.8/java.version maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies上面是三段最关键的信息java.version声明了编译用哪个 JDK 版本web starter 表明项目是 MVC 结构、内嵌 TomcatMyBatis starter 说明数据库访问走的是 Mapper 接口加 XML 映射的方式。runtime作用域的 MySQL 驱动说明连接信息不在代码里而是放在配置文件里由运行时加载。至少你要确认一点本地装的 JDK 必须大于等于java.version声明的版本否则后面编译必然失败。多数情况下这种考勤项目会用内嵌 Tomcat 启动打成 jar 包后一个命令就能起服务。注意区分那些打成 war 包的老项目那种需要部署到外部 Tomcat 的 webapps 目录。判断方法是看 pom 里有没有packagingwar/packaging。如果有你就需要额外准备一个版本匹配的外部 Tomcat而不是依赖内嵌容器。2.3 目录结构拆解controller、service、mapper 各管哪一段跑项目之前把目录结构过一遍能让你在出问题时第一时间判断“该去哪个文件排查”而不是全局乱翻。这类考勤系统的包名通常围绕controller、service、mapper、entity或domain、config几层展开看下面的结构src/main/java ├── com/example/attendance │ ├── controller # 接收 HTTP 请求处理参数 │ │ ├── LoginController.java │ │ ├── EmployeeController.java │ │ └── AttendanceController.java │ ├── service # 业务逻辑迟到判断、月度汇总 │ │ ├── AttendanceService.java │ │ └── EmployeeService.java │ ├── mapper # MyBatis 接口对应 SQL 映射文件 │ │ ├── EmployeeMapper.java │ │ └── AttendanceMapper.java │ ├── entity # 数据表对应的实体类 │ │ ├── Employee.java │ │ └── AttendanceRecord.java │ └── config # 拦截器、跨域、静态资源配置 ├── resources │ ├── mapper # XML 里的 SQL 语句 │ ├── static # 前端 JS/CSS │ ├── templates # 页面模板 │ └── application.yml └── webapp # 部分老项目放 JSP 的目录controller只做参数接收和结果封装不写 SQLmapper只做数据查询不写业务判断service才是迟到早退、请假扣减这些规则真正落地的地方。如果你要改考勤规则优先去service层找不要在 controller 里翻。entity里的字段通常和数据库表字段一一对应当页面某个时间显示不出来先去看对应实体类有没有声明这个字段。resources/mapper里的 XML 是动态 SQL 所在涉及复杂联表查询的报表功能都在这层改。3. 数据库与配置文件建库、建表、初始化账号三步走3.1 建库脚本与三张核心表部门、员工、考勤记录数据库是这个项目能否跑起来的分水岭。最常见的情况是代码没问题、前端也正常但登录页提交后直接报数据库表不存在。我建议不管项目里有没有自带建库脚本都手动把三张核心表的结构过一遍。标准的考勤管理至少需要三类数据部门信息、员工信息、每日打卡记录三者是逐步关联的主外键关系。先建部门表再建员工表最后建考勤表顺序反了会报外键缺失。CREATE DATABASE IF NOT EXISTS attendance_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE attendance_db; CREATE TABLE sys_dept ( dept_id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(64) NOT NULL UNIQUE, dept_code VARCHAR(32) NOT NULL ); CREATE TABLE sys_employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) NOT NULL UNIQUE, emp_name VARCHAR(32) NOT NULL, dept_id INT NOT NULL, password VARCHAR(100) NOT NULL, hire_date DATE, status TINYINT DEFAULT 1, CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES sys_dept(dept_id) ); CREATE TABLE att_record ( record_id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_id INT NOT NULL, work_date DATE NOT NULL, clock_in_time DATETIME, clock_out_time DATETIME, status TINYINT COMMENT 1正常 2迟到 3早退 4缺卡 5请假, CONSTRAINT fk_att_emp FOREIGN KEY (emp_id) REFERENCES sys_employee(emp_id), UNIQUE KEY uk_emp_date (emp_id, work_date) );建库时指定utf8mb4是为了让部门名、姓名里的中文正常存储这个细节在旧版 MySQL 上很容易被忽略。员工表里的emp_no是工号通常作为登录账号用password字段的长度留100而不是传统32因为很多项目后来改成了 BCrypt 加密密文长度超过 32。考勤表里的UNIQUE KEY uk_emp_date (emp_id, work_date)这一行是关键它从数据库层面限制了同一人同一天只能有一条考勤记录防止脚本刷签到造成脏数据。3.2 application.yml 的检查清单端口、数据源、时区、静态资源映射配置文件是启动过程中的重灾区。这份 YAML 虽然不长但每一项都可能让你启动失败。我习惯把它按照“端口 → 数据源 → 字符集 → 时区”四部分依次排查不要跳着改否则出了错很难定位。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/attendance_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.attendance.entity logging: level: com.example.attendance.mapper: debugserver.port如果改成别的端口记得后续访问地址同步更新。数据源的driver-class-name如果是com.mysql.jdbc.Driver说明项目面向旧版 MySQL如果你的数据库是 MySQL 8 以上建议改用com.mysql.cj.jdbc.Driver。url里的characterEncodingutf8解决的是中文乱码serverTimezoneAsia/Shanghai解决的是日期时间差 8 小时的问题这两个参数缺一不可。mybatis.mapper-locations指定了 XML 映射文件的位置如果你的项目把 XML 放在了别处这个配置不匹配时 Mapper 接口会报Invalid bound statement (not found)。3.3 初始化数据默认账号从哪里找建完表之后下一步是找到管理员账号的初始化 SQL。这类项目一般会在sql目录或项目根目录放一个init.sql或data.sql里面包含部门的初始数据和一个 admin 账号。如果没有你就需要自己插入一条员工记录并弄清楚密码的加密方式。判断方法很简单打开源码里的登录逻辑如果它调用的是BCryptPasswordEncoder.matches()那么数据库里的密码必须是 BCrypt 密文如果直接用明文比对那你插入的密码也要是明文。INSERT INTO sys_dept (dept_name, dept_code) VALUES (技术部, TECH); INSERT INTO sys_employee (emp_no, emp_name, dept_id, password, hire_date) VALUES (admin, 系统管理员, 1, $2a$10$7QxVcVckYH8c3qQK1fQy1uZt9KzCbE/5vI2TZ2KzYbHq0Tf1zY9iK, CURDATE());这里特别提醒如果项目登录逻辑明确写着 BCrypt你却往密码字段里插了明文那么界面会一直提示“用户名或密码错误”因为这个错误信息很多时候不会告诉你真正原因是加密不匹配。如果你不确定密码怎么生成可以写一个临时 Java 类调用BCryptPasswordEncoder().encode(你要的密码)生成密文再把这段密文更新到库中。4. 核心业务流程把打卡、判异、月度汇总这条链路打通4.1 打卡接口Controller 到 Service 的参数校验链跑通前端页面之后要真正理解这个考勤系统必须跟一遍打卡接口的数据流。先看 controller 层接收什么数据、如何封装返回值再看 service 层做了哪些校验。以最常见的“上班打卡”接口为例前端会提交一个包含员工编号和时间的 JSON后端要做的事远不止“插一条记录”这么简单。RestController RequestMapping(/attendance) public class AttendanceController { Autowired private AttendanceService attendanceService; PostMapping(/clock/in) public Result clockIn(RequestBody ClockRequest req) { if (req.getEmpNo() null || req.getClockTime() null) { return Result.error(参数不完整); } return attendanceService.clockIn(req.getEmpNo(), req.getClockTime()); } }这段代码只做三件事接收请求体、做非空校验、转发给 service 层。注意Result是统一响应类后面前端页面判断请求成不成功都靠它的code字段并不只是靠 HTTP 状态码。req.getClockTime()是可选的如果前端不传后端可以用LocalDateTime.now()兜底但在实际使用中我建议由前端传打卡时间否则测试阶段很难模拟“补卡”场景。service 层才是业务重心。一个完整的签到逻辑应该包含四步确认员工存在且在职、检查当天是否已有记录、根据班次时间判断迟到、最后写入数据库。Transactional public Result clockIn(String empNo, LocalDateTime clockTime) { Employee emp employeeMapper.findByEmpNo(empNo); if (emp null || emp.getStatus() ! 1) { return Result.error(员工不存在或已离职); } LocalDate workDate clockTime.toLocalDate(); AttendanceRecord record attendanceMapper.findByEmpAndDate(emp.getEmpId(), workDate); if (record ! null) { return Result.error(今日已打卡请勿重复提交); } LocalTime shiftStart LocalTime.parse(09:00); int status clockTime.toLocalTime().isAfter(shiftStart) ? 2 : 1; AttendanceRecord insert new AttendanceRecord(); insert.setEmpId(emp.getEmpId()); insert.setWorkDate(workDate); insert.setClockInTime(clockTime); insert.setStatus(status); attendanceMapper.insert(insert); return Result.success(); }这段代码里的Transactional保证“查询校验”和“插入记录”在同一个事务里避免出现两个人同时提交时都通过校验、结果插入两条记录的极端情况。shiftStart是上班时间 9 点如果你项目里有“弹性打卡”或“晚到宽限”需求不要把 9 点写死在代码里而是在员工表或部门表里增加一个work_start_time字段让不同部门使用不同班次。4.2 迟到早退判定规则与状态机的常见写法考勤状态通常不会只有“正常”和“迟到”两种更完整的模型至少包含正常、迟到、早退、缺卡、请假。这里的关键在于迟到是“上班卡时间晚于班次开始时间”早退是“下班卡时间早于班次结束时间”但有一个边界情况经常被忽略员工只打了一次卡时没法判断早退只能标记为“缺卡”。public void evaluateStatus(AttendanceRecord record, LocalTime shiftStart, LocalTime shiftEnd) { if (record.getClockInTime() null) { record.setStatus(4); // 缺卡 return; } LocalTime clockIn record.getClockInTime().toLocalTime(); if (clockIn.isAfter(shiftStart)) { record.setStatus(2); // 迟到 } else if (record.getClockOutTime() null) { record.setStatus(4); // 有上班卡但无下班卡视为缺卡 } else if (record.getClockOutTime().toLocalTime().isBefore(shiftEnd)) { record.setStatus(3); // 早退 } else { record.setStatus(1); // 正常 } }这段判定的顺序有讲究先看有没有上班卡再看迟到再看有没有下班卡最后看早退。如果顺序反过来既迟到又早退的人会被只标记为早退统计结果会少一类。另一个要处理的边界是跨天打卡比如员工晚班从晚上 10 点到次日 6 点数据库里的work_date应该取上班日期而不是下班日期否则月度统计时记录会被归到第二天。4.3 月度汇总是写 SQL 还是用代码聚合月度报表是整个考勤系统里最容易被做烂的部分。拿到一批数据常见的实现方案有两种第一种在 Java 代码里遍历记录、按部门和员工分组统计第二种直接在 SQL 里用聚合函数一次性得到结果。我更推荐后者原因是代码简单、执行在数据库引擎内完成、不会因为数据量增大而拖垮应用内存。SELECT e.dept_id, d.dept_name, e.emp_id, e.emp_name, MONTH(r.work_date) AS work_month, COUNT(*) AS total_days, SUM(CASE WHEN r.status 1 THEN 1 ELSE 0 END) AS normal_days, SUM(CASE WHEN r.status 2 THEN 1 ELSE 0 END) AS late_days, SUM(CASE WHEN r.status 3 THEN 1 ELSE 0 END) AS early_leave_days, SUM(CASE WHEN r.status 4 THEN 1 ELSE 0 END) AS missing_days, SUM(CASE WHEN r.status 5 THEN 1 ELSE 0 END) AS leave_days FROM att_record r JOIN sys_employee e ON r.emp_id e.emp_id JOIN sys_dept d ON e.dept_id d.dept_id WHERE r.work_date BETWEEN 2024-01-01 AND 2024-01-31 GROUP BY e.dept_id, e.emp_id, e.emp_name, MONTH(r.work_date) ORDER BY d.dept_name, e.emp_name;这个查询把原来几十行 Java 循环才能完成的逻辑压缩成了几句聚合。SUM(CASE WHEN ... THEN 1 ELSE 0 END)是核心技巧它把“条件计数”写成了标准的 SQL 聚合方式比在代码里多次查库要高效得多。这里要注意GROUP BY的字段必须和SELECT里的非聚合字段保持一致比如d.dept_name也要出现在GROUP BY里否则在严格模式下会直接报错。5. Java考勤管理系统的常见坑与排查跑不起来时按这个顺序查5.1 端口被占用启动时报Port already in use现象点击启动后控制台出现Web server failed to start. Port 8080 was already in use之类的报错服务进程直接退出。原因本机已有其他进程占用了 8080 端口。这类考勤项目默认端口是 8080而不少开发者的电脑上同时跑着其他前端或后端服务难免冲突。解决要么改项目端口要么找到占用端口的进程并且结束它。我建议优先改端口毕竟未知进程可能是你正在用的另一个服务。lsof -i:8080 kill -9 PID如果改端口直接修改application.yml里的server.port改完后重启即可。这里提醒一下前端页面发请求时如果是绝对地址也要同步改否则会出现页面能打开但所有接口都请求失败的奇怪现象。5.2 数据库连接失败但你密码明明没错现象控制台报Cannot create PoolableConnectionFactory或者Access denied for user rootlocalhost你反复确认密码无误仍然连接失败。原因有两类第一类是 MySQL 驱动和数据库版本不匹配老驱动连接 MySQL 8 以上版本时认证协议不兼容第二类是数据库的 root 账号默认使用auth_socket或caching_sha2_password插件而驱动侧不认识新的认证方式。解决确认driver-class-name是com.mysql.cj.jdbc.Driver同时在连接url里加useSSLfalse和allowPublicKeyRetrievaltrue。后两个参数是 MySQL 8 连接时报错的高频解法缺了其中一个都会出现连接失败。5.3 中文乱码页面上显示一堆问号现象登录后员工列表里的中文名显示成“”或者写入数据库的中文变成乱码。原因整条链路里至少有一处字符集不是 UTF-8通常是数据库连接参数没带characterEncodingutf8或者建库时字符集用成了默认的latin1。解决两步同时检查。第一步在application.yml的数据源 URL 上加useUnicodetruecharacterEncodingutf8第二步重建数据库建库语句里明确指定DEFAULT CHARACTER SET utf8mb4。只改连接不改库或者只改库不改连接问题都不会消失。5.4 页面能打开但登录不进拦截器把请求挡了现象登录页能正常加载但输入账号密码点登录后只有一个空白提示或者报 401/405后台没有任何异常日志。原因这类项目为了做登录态校验会配置一个WebMvcConfigurer拦截器或过滤器拦截所有非登录接口。如果拦截器没有放行/login路径前端发出的登录请求会被挡在业务逻辑之外表现为“一切正常但进不去”。解决去config包里找拦截器配置检查addPathPatterns和excludePathPatterns。如果你项目里登录接口是/user/login那exclude里必须包含这个路径同时把静态资源路径也放行。5.5 时间对不上考勤记录比实际时间差了 8 小时现象打卡时间是早上 8 点数据库里存的时间却是凌晨 0 点或者前端页面显示的时间比数据库里多了 8 小时。原因这是典型的时区错位。JDBC 连接时如果没有指定serverTimezone驱动会用 JVM 默认时区跟数据库会话时区做换算而 MySQL 服务器默认时区可能是 UTC。解决在连接 URL 上加上serverTimezoneAsia/Shanghai并在应用启动时设置spring.jackson.time-zoneGMT8来保证 JSON 序列化输出时间也跟着对齐。6. 进阶把考勤系统从“能跑”改成“好用”的两个方向6.1 给考勤表加索引月度汇总从秒级变毫秒级数据量上去之后最先撑不住的是考勤记录表的查询。如果att_record表里只有主键索引那么每次跑月度汇总都是全表扫描。我的习惯是在建表时就把唯一索引建好在查询频繁的字段上再补两个普通索引。第一个必然要加的索引是(emp_id, work_date)它既控制唯一性也覆盖了单人的考勤查询第二个加在(work_date, status)上专门服务月度汇总时的范围过滤和分组统计。加完这两个索引后用EXPLAIN SELECT ...查看执行计划确认type不是ALL就是成功。6.2 打卡异常主动提醒比人工翻报表高效得多项目跑稳定后一个很有价值的增量功能是“异常考勤推送”。逻辑很简单每天固定时间点扫描当天的考勤记录找出迟到、缺卡的人拼成一条消息发到企业办公群。这个功能只需要写一个定时任务在 service 层查出异常名单再调用企业办公软件的机器人 Webhook 发送 JSON 消息。通用实现思路是先查后推一次查询拿到结果然后以 HTTP POST 形式把消息推送出去不需要引入复杂的消息队列。Scheduled(cron 0 0 10 * * *) public void pushDailyAbnormal() { ListAttendanceRecord abnormalList attendanceMapper.findAbnormalByDate(LocalDate.now()); if (abnormalList.isEmpty()) { return; } StringBuilder content new StringBuilder(今日考勤异常提醒\n); for (AttendanceRecord r : abnormalList) { content.append(r.getEmpName()).append( ).append(statusText(r.getStatus())).append(\n); } webhookClient.pushText(content.toString()); }这个定时任务的cron 0 0 10 * * *表示每天早上 10 点整触发一次正好是上班一小时后异常基本已经出现但及时性足够。findAbnormalByDate只查当天的迟到和缺卡记录效率很快因为你已经在考勤表上建了日期索引。Webhook 客户端封装成单独的webhookClient方便以后切换不同的企业通信工具。我自己做这类系统时养成了一个习惯先把数据库脚本跑通再碰代码最后才看页面。这个顺序能挡掉一半以上的启动问题而且排查成本最低。如果哪天你也被某个“只差最后一步就能启动”的问题卡住试着把控制台从第一行日志读起而不是只看最后一行红色报错。希望帮到你。本文还有配套的精品资源点击获取