ARTICLE DETAIL

资讯详情

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

Java开源企业考勤系统实战:从部署到二次开发避坑指南

Java开源企业考勤系统实战:从部署到二次开发避坑指南 简介这是一套基于Java的企业考勤管理系统解决方案面向需要规范员工考勤的企业管理者、HR以及希望学习Java Web开发的开发者。系统覆盖用户管理、多方式打卡、考勤规则设定、异常审批、报表统计、通知提醒等核心模块并支持与HRM、ERP系统集成可有效提升企业人力管理效率。资源包共含2个文件主要包括一个源码zip压缩包和一份readme.htm说明文档整体大小为5.32MB。源码包中提供项目源代码、数据库配置及部署相关材料说明文档则介绍项目结构、运行环境与二次开发要点便于开发者快速了解系统架构、完成本地部署并根据企业需求进行功能定制。目前已有1781人下载学习适合需要搭建或定制企业考勤系统的技术团队与个人开发者。通过该资源读者既能获得一套可实际部署的考勤系统基础也能深入理解考勤管理中的业务逻辑与Java Web项目实践为后续扩展功能、集成其他企业系统提供清晰起点。1. Java 开源企业考勤系统从排班到报表一套能直接落地的源码做 Java 后端这些年我拆过不下十个考勤系统其中 Java 开源企业考勤系统是最容易被新手低估的表面看只有打卡和统计真部署起来却要处理跨天班次、节假日、请假审批、加班调休和薪资接口五件事。这套源码把后端做成了 Spring Boot MyBatis 的标准结构前端是 Vue Element UI数据库用 MySQL基本覆盖了中小企业的日常考勤场景。适合三类人一是公司要上考勤系统但又不想买商业版的 IT 负责人二是刚入行想找一个完整业务案例的 Java 工程师三是需要把手动考勤改成自动化的信息部门。它解决的痛点很具体——排班、打卡、异常申诉、月度报表都能在一个后台里闭环跑完。2. 系统架构与数据模型先看清单再动手避免黑匣子拿到一套开源源码我从来不会直接mvn spring-boot:run而是先花半小时把架构和数据模型摸清楚。考勤系统最怕的是业务边界不清打卡记录存哪里请假单跟考勤怎么关联加班时长是从哪个表算出来的。这些不搞明白后面改一个字段可能连环崩。2.1 核心功能模块与权限边界这套系统的功能模块可以从登录后左侧菜单一眼看全我整理成一张模块清单模块主要能力默认角色员工管理员工档案、入职/离职、部门归属管理员、部门主管部门与排班管理多级部门、班次定义、周期性排班管理员打卡管理上下班打卡、外勤打卡、打卡记录查询员工、主管请假/加班审批申请单提交、审批流、状态跟踪员工、主管、管理员考勤统计迟到/早退/缺卡计算、月度汇总主管、管理员报表导出按月导出 Excel支持自定义日期段管理员权限边界是这套系统比较成熟的地方普通员工只能看到自己的考勤和申请单部门主管能看本部门数据管理员才能动排班和全局设置。这个三层角色是写死在表结构和拦截器里的如果你要在二次开发里加一个“子管理员”不能只加菜单权限还要注意数据权限的过滤条件。2.2 技术选型为什么是这个组合Spring Boot MyBatis 的组合在 Java 开源项目里属于“不会出错”的选择。Spring Boot 把配置收敛到几个 yml 文件里不用像老 SSH 项目那样堆一堆 XMLMyBatis 则让复杂查询直接写 SQL考勤统计这种强逻辑场景特别需要。前端用 Vue Element UI组件现成表格、日期选择器、审批弹窗都有成熟方案。数据库用 MySQL免费且运维成本低。这个选型有一个很实际的理由招人容易。市场上 Java 工程师基本都写过 Spring Boot 和 MyBatis你拿这套系统做二次开发不需要培训新人就能接手。Redis 在项目里是可选的如果公司人少几百人以内完全可以用数据库缓存替代把 Redis 关掉也能跑。2.3 数据表设计与关系核心表有十几张我挑最关键的六张表说明sys_user用户表包含账号、密码、部门 ID、角色类型dept部门表树形结构用 parent_id 关联上级shift班次表定义上下班时间、是否跨天schedule排班表一个员工某一天对应哪个班次attendance_record打卡记录表每一条打卡对应一个时间点和类型leave_apply请假申请表关联用户和审批状态下面这段 SQL 展示了打卡记录表的核心结构也是后面所有统计的源头CREATE TABLE attendance_record ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, user_id bigint(20) NOT NULL COMMENT 员工ID, attendance_date date NOT NULL COMMENT 考勤日期, shift_id bigint(20) DEFAULT NULL COMMENT 排班ID, clock_in_time datetime DEFAULT NULL COMMENT 上班打卡时间, clock_out_time datetime DEFAULT NULL COMMENT 下班打卡时间, status tinyint(4) DEFAULT 1 COMMENT 1正常 2迟到 3早退 4缺卡 5外勤, remark varchar(255) DEFAULT NULL COMMENT 备注, PRIMARY KEY (id), KEY idx_user_date (user_id, attendance_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT打卡记录表;这里要特别说明两个容易被忽略的设计一是attendance_date和clock_in_time是分开的因为跨天班次比如夜班 22:00 到次日 6:00的打卡日期和自然日不同你按clock_in_time分组统计就会出问题二是联合索引idx_user_date考勤统计基本上都是按员工和日期查这个索引直接影响报表查询速度。请假表与考勤记录的关联也很关键。请假单审批通过以后系统会把请假时间段写入一个leave_record表考勤统计时用它来排除迟到早退。有些开源版本偷懒只在leave_apply表里存状态统计时临时判断就导致已通过的请假单在月度报表里还是被算成缺卡。你拿到源码后第一件事就是查这三个表的关系leave_apply是否生成leave_recordattendance_record里的状态字段是否被回写。3. 从源码到能跑部署初始化与最小配置很多开源项目卡在部署第一步不是因为代码有问题而是环境细节没对齐。这套 Java 考勤系统我用下来环境要求不算高但有三处配置必须按实际情况改否则就算启动成功数据也是错的。3.1 环境准备与目录结构需要准备的工具JDK 8 或 11建议用 JDK 8兼容性最好Maven 3.6用于编译打包MySQL 5.78.0 也可以但要改驱动配置Redis 可选默认可以不装Node.js 14跑前端代码如果不打算改前端直接使用打包后的静态文件即可后端目录结构是标准的 Spring Boot 工程src/main/java ├── com/company/attendance │ ├── controller # 控制层接口入口 │ ├── service # 业务逻辑层 │ ├── mapper # MyBatis 数据访问接口 │ ├── entity # 实体类 │ ├── config # 配置类拦截器、权限相关 │ └── utils # 工具类 src/main/resources ├── mapper # MyBatis XML 文件 ├── application.yml # 主配置 └── sql # 数据库初始化脚本先看sql目录下的脚本注意版本号顺序不要跳着执行。通常有schema.sql建表和data.sql初始数据有些项目把它们合到一起。3.2 初始化数据库脚本执行顺序与字符集数据库初始化是最容易踩坑的地方。我一般这样做mysql -u root -p -e CREATE DATABASE attendance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -u root -p attendance src/main/resources/sql/schema.sql mysql -u root -p attendance src/main/resources/sql/data.sql创建数据库时一定要指定utf8mb4而非utf8因为考勤记录里的备注可能包含 emoji比如“地铁晚点”如果用 utf8 存储会报错或变问号。utf8mb4_general_ci是排序规则对中文和英文混合的查询影响不大但保持一致最省心。执行完脚本后先别急着启动登录 MySQL 检查两张表的数据量SELECT COUNT(*) FROM sys_user; SELECT COUNT(*) FROM dept;如果sys_user里没有账号或者dept是空的后面的登录验证就会卡住。有些项目把初始账号写在 README 里比如 admin/admin123但data.sql里没插入这就是典型的“文档和脚本不同步”。3.3 改配置application.yml 中的三处必改项打开src/main/resources/application.yml最常见的有三处spring: datasource: url: jdbc:mysql://127.0.0.1:3306/attendance?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver redis: host: 127.0.0.1 port: 6379 # password: servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true第一处必改项是url里的serverTimezoneAsia/Shanghai。如果服务器是 UTC 时区不写这个参数打卡时间会出现 8 小时偏差这是考勤系统的致命问题。第二处是数据库密码不用多说。第三处是 Redis如果你的环境没有 Redis除了注释掉 redis 的相关配置还要检查代码里是否有强依赖 Redis 的缓存工具类。我见过一些版本在application.yml里写了 Redis但RedisConfig里用了EnableCaching导致没有 Redis 直接启动失败。最简单的办法是启动前把 redis 装好比改代码更省事。参数解释map-underscore-to-camel-case: true是让数据库字段clock_in_time自动映射到 Java 属性clockInTime很多二次开发改字段后忘记同步这里导致查询结果全是 null这个问题下文会详细说。3.4 启动与登录验证后端启动有两种方式一种是在项目根目录执行mvn spring-boot:run另一种是打包后运行mvn clean package -DskipTests java -jar target/attendance.jar等日志出现Tomcat started on port(s): 8080就说明后端起来了。前端如果是 Vue 项目一般要单独启动cd frontend npm install npm run dev打开前端页面后用初始账号登录。如果登录接口返回 404先看一下控制层的RequestMapping前缀是否带了/api。有些前端代码所有请求都带/api而后端接口不带这就是典型的前后端路径不一致改前端代理比改后端接口更简单# vue.config.js 里的代理配置 proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } }启动成功后建议先做一次完整的打卡流程测试给某个员工排班模拟打卡然后查看考勤统计。不要只停留在登录页考勤系统真正的功能在排班和统计里登录只是入口。4. 二次开发与集成把开源考勤改成自己企业的形状开源系统很少能开箱即用总会有一些公司特有的字段和规则。这一章我把最常见的改造场景拆开讲从加字段到对接钉钉给出可照抄的步骤和代码。4.1 加一个加班餐补字段从数据库到接口全链路需求很常见公司规定加班超过 2 小时给 15 元餐补需要在考勤统计里体现。在开源系统上做这个改动不是只加一个字段那么简单要走完数据库、实体、Mapper、Service 四层。第一步给attendance_record表加字段ALTER TABLE attendance_record ADD COLUMN meal_allowance DECIMAL(8,2) DEFAULT 0.00 COMMENT 加班餐补金额;第二步在AttendanceRecord实体类里加属性private BigDecimal mealAllowance; // 对应餐补金额这里注意MyBatis 开启了驼峰映射数据库字段meal_allowance会自动对应mealAllowance所以实体类里可以直接用驼峰命名。第三步修改 Mapper XML 里的插入和更新语句update idupdateAllowance parameterTypemap UPDATE attendance_record SET meal_allowance #{allowance} WHERE id #{recordId} /update然后在 Service 里写计算逻辑public void calcMealAllowance(Long recordId, Integer overtimeMinutes) { BigDecimal allowance BigDecimal.ZERO; // 加班超过120分钟才发餐补 if (overtimeMinutes ! null overtimeMinutes 120) { allowance new BigDecimal(15.00); } attendanceRecordMapper.updateAllowance(recordId, allowance); }这段代码的逻辑说明加班分钟数从overtime_record表里查超过 2 小时才计算餐补。参数recordId是打卡记录 ID避免同时给多天记录写错。这样做的好处是拍平了餐补计算过程而不是在统计 SQL 里临时判断——临时判断会让月度报表无法回溯。4.2 对接钉钉打卡回调与映射的常规做法很多公司已经用钉钉/企业微信打卡这时候要做的不是让员工再打开考勤系统打一次卡而是把钉钉的打卡结果同步过来。常见做法是使用钉钉开放平台的“打卡事件回调”钉钉会把打卡数据推送给我们后端接口。步骤是在钉钉开放平台创建应用获取 AppKey 和 AppSecret。配置打卡事件订阅回调地址指向我们后端的/api/dingtalk/attendance接口。后端接受到回调后根据钉钉 userId 映射到本地sys_user写入attendance_record。后端接口的关键代码PostMapping(/api/dingtalk/attendance) public MapString, Object receiveAttendance(RequestBody DingTalkCallbackData data) { // 解密钉钉回调数据实际会先解密再解析这里省略解密过程 ListDingTalkAttendanceItem items data.getItems(); for (DingTalkAttendanceItem item : items) { Long localUserId userMapper.getIdByDingTalkUserId(item.getUserId()); if (localUserId null) { // 钉钉里有考勤但系统里没有对应用户记录日志并跳过 log.warn(未匹配到本地用户钉钉userId{}, item.getUserId()); continue; } attendanceRecordMapper.insertRecord(localUserId, item.getCheckTime(), item.getType()); } return Collections.singletonMap(success, true); }注意这里有一个业务边界钉钉回调的打卡事件可能包含多条记录必须做去重处理。最简单的方式是在attendance_record表上建立唯一索引比如uk_user_time (user_id, clock_in_time)重复写入会直接报错你在代码里捕获异常后跳过即可。否则钉钉重试回调时会产生重复打卡记录。4.3 考勤规则配置迟到/早退/加班阈值的参数表考勤系统最核心的是规则配置这些参数通常放在attendance_config表里而不是写死在代码中。我列一下最常见的配置项参数名含义典型值说明late_minutes允许迟到分钟数5超过 5 分钟算迟到early_minutes允许早退分钟数5超过 5 分钟算早退overtime_start加班起算时间18:30下班打卡后开始计加班overtime_rate加班工时折算1.01 小时加班算 1 小时night_shift_start跨天班次上班时间22:00用于判断跨天statistic_day月结日期25每月 25 号结算上月周期这里最容易踩坑的是night_shift_start。如果员工排的是夜班打卡时间在晚上那么attendance_record里的attendance_date应该是哪个自然日常见做法是夜班时间超过凌晨 2 点的把attendance_date记为班次开始日而不是打卡自然日。很多开源代码在这里判断不够完善导致夜班员工每月的第一天和最后一天考勤数据异常。你拿到源码后重点看ScheduleServiceImpl里对跨天班次的判断逻辑。4.4 报表导出的并发问题考勤报表导出是很常见的功能但并发一高就容易内存溢出。这类系统的报表导出通常用 Apache POI如果你用的是XSSFWorkbook当导出几百个人、几个月的数据时内存直接翻车。正确做法是用 SXSSFWorkbookSXSSFWorkbook workbook new SXSSFWorkbook(100); // 保留100行在内存其余写磁盘 SXSSFSheet sheet workbook.createSheet(考勤汇总); // 设置列宽、表头等写入数据 for (AttendanceReportRow row : rows) { SXSSFRow excelRow sheet.createRow(rowIndex); excelRow.createCell(0).setCellValue(row.getUserName()); // ... 其他列 } FileOutputStream fos new FileOutputStream(attendance.xlsx); workbook.write(fos); fos.close(); workbook.dispose();SXSSFWorkbook(100)的关键在于窗口大小。100 表示内存中最多保留 100 行数据其余部分写到临时文件这样导出 10 万条记录也不会 OOM。最后一定要调用dispose()方法清理临时文件否则磁盘会被占满。5. 避坑指南部署与使用中常见的五个翻车点这一章是我在实际部署这套 Java 考勤系统时踩过的坑每一条都花了不少时间排查。列出来给你当“后悔药”遇到类似问题能少走弯路。5.1 数据库中文乱码但启动没报错现象系统能正常启动员工姓名和部门名在页面上显示成问号打卡备注里的中文全部乱码。原因数据库连接 URL 里没有加characterEncodingutf8mb4或者数据库本身建库时用了latin1。Spring Boot 启动时不会校验字符集只有写入时才出问题。解决重建数据库并指定字符集CREATE DATABASE attendance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;连接 URL 中加入useUnicodetruecharacterEncodingutf8mb4。修改后重启服务再做一次打卡测试写入中文备注确认不乱码。5.2 登录后列表接口全部返回 401现象登录接口返回了 token但接下来查员工列表、查考勤记录请求全是 401 未授权。原因登录后携带 token 的方式不对。系统用的是自定义拦截器期望前端在每个请求头里带Authorization: Bearer token但前端代码实际只带了token参数。这是典型的 token 传递方式不一致。解决看后端拦截器的源码找到它读取 token 的位置String token request.getHeader(Authorization);如果是这种写法前端 axios 拦截器里统一加上config.headers[Authorization] Bearer localStorage.getItem(token);不要改成后端兼容多个 header保持统一风格后续加其他权限校验也更顺。5.3 打卡时间比实际时间慢了 8 小时现象凌晨 0 点打卡数据库里记录的是前一天 16:00。服务器时间、数据库时间都对但 Java 程序和 MySQL 之间差了 8 小时。原因MySQL 连接 URL 没有指定serverTimezone默认使用了 UTC 时区。StringBoot 在解析datetime类型时把 UTC 时间当成了本地时间。解决在数据源 URL 上加serverTimezoneAsia/Shanghai同时检查 MySQL 全局时区SELECT global.time_zone, session.time_zone;如果显示的是00:00执行SET global.time_zone 08:00; SET time_zone 08:00;这个坑很隐蔽因为日志里看不出任何报错。5.4 排班后员工打不了卡现象管理员给员工排好了班但员工在手机端/Web端打卡时提示“当前无排班”无法打卡。原因排班表里的班次日期没有和打卡记录对齐。常见的情况是排班保存时用了shift_date但打卡校验时查的是attendance_date两个字段之间存在时区或日期格式化差异。比如排班存的是2025-03-01 08:00:00打卡时传的是2025-03-01类型转换后查不到。解决在排班保存逻辑里把shift_date统一格式化为日期类型不要用 LocalDateTime 直接存储ShiftPlan plan new ShiftPlan(); plan.setShiftDate(LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE));打卡接口里也用LocalDate.now()和shift_date比较不要加时间部分。这样排班和打卡的日期基准就统一了。5.5 导出 Excel 提示“文件损坏”或直接卡死现象导出考勤报告时Excel 打开提示文件损坏或者页面请求迟迟不返回服务端日志出现OutOfMemory。原因用了旧版 POI 的HSSFWorkbook写.xlsx文件或者用XSSFWorkbook把所有数据放在内存。考勤数据超过几万行后内存就爆了。解决改用SXSSFWorkbook并在临时目录下输出server: tomcat: basedir: /tmp/tomcat_work同时确保导出接口是异步的否则 HTTP 连接会超时。最简单的方式是导出时先写到一个临时路径然后返回文件下载地址前端再发起一次下载请求。这样既避免长连接也方便排查写入时的问题。6. 进阶用法多部门多班次与月度归档的落地技巧当系统跑顺以后你会发现最大的问题不再是打卡而是月底统计越来越慢。几百个人的考勤数据攒一年可能有几十万条每次统计都要全表扫描这时候我一般会做一个月度归档表把每个员工的考勤结果在每月结算日固化下来。核心做法是写一个定时任务每月 25 号凌晨 2 点执行Scheduled(cron 0 0 2 25 * ?) public void monthlyArchive() { // 查询上个月所有员工的考勤汇总 // 插入考勤归档表 attendance_monthly_summary ListAttendanceSummary summaries reportService.summarizeMonth(lastMonth); archiveMapper.insertBatch(summaries); // 清理打卡明细中超过保留期限的中间数据 recordMapper.cleanupAfterArchive(archiveMonth); }归档表的结构不需要太多字段重点是员工 ID、月份、出勤天数、迟到次数、加班时长、餐补金额。有了这个表薪资系统拉取考勤数据时就不需要再算原始打卡记录查询速度从几十秒降到毫秒级。权限数据隔离也建议在归档阶段一起做。归档表里增加dept_id字段部门主管查统计时默认按这个字段过滤避免每次统计都去 join 员工表和部门表。还有一个验证技巧值得养成习惯每次发版后我会手动造一个“跨天夜班 请假半天”的测试用例跑完月度统计后人工核对结果。因为跨天和请假是最容易产生数据口径偏差的两类场景自动化测试很难覆盖全人工核对一次能发现很多隐藏逻辑问题。从那以后我每次部署这套考勤系统都会强制走一遍“排班→打卡→请假→月结”的完整链路确认数据和手工台账一致才交给业务方使用。考勤系统的坑大多不在代码本身而在你对业务规则的理解希望帮到你。本文还有配套的精品资源点击获取
返回列表