考勤这事,看着简单,里面的坑是真不少。我刚接手公司那个考勤项目的时候,前任留下一套用Excel手工统计的流程,每个月月底那几天,行政的小姑娘对着几百号人的打卡记录,眼睛都能看花。迟到早退、漏打卡、请假调休,全挤在一张张表里,对不上号是常事。所以后来我干脆用PHP从零搭了一套考勤系统,也就是这套“PHP广软考勤系统”的由来。源码整理了一下,版本号挂在00588,方便后面迭代。
这套系统核心解决三件事:让员工打卡不再依赖纸质单据,让管理员月底能一键生成统计报表,让异常考勤数据能被自动标记出来。不管你是刚接触PHP的学生,还是公司里需要一套内部工具的技术负责人,这套系统的设计思路和实现细节,应该都能给你一些参考。
1. 系统整体设计与技术选型
1.1 为什么是PHP而不是其他语言
选PHP不是因为它最时髦,而是因为它最合适。我评估过几个方案:用Python写后端,开发效率是高,但公司现有的服务器环境是经典的LNMP,运维同事对PHP的部署最熟,出了问题响应最快;用Node.js做实时打卡推送,技术上没问题,但为了一个内部考勤系统去引入一套新的运行时环境,后续维护成本划不来。
PHP在这里的优势是业务逻辑直观、部署简单、跟MySQL配合默契。考勤系统的核心是数据的收集、计算和展示,PHP的数组处理和字符串函数非常强大,处理打卡时间戳、计算工时、判断迟到早退这类操作,代码写起来很顺手。而且PHP的语法门槛低,后来接手维护的同事不用花太多时间就能上手改需求。
还有一个现实原因:这套系统的目标用户是公司内部员工,并发量撑死几百人同时打卡,PHP默认的FPM模式完全扛得住,没必要为了高并发去引入复杂的架构。技术选型不是越新越好,而是要看场景的复杂度。
1.2 考勤系统的核心需求拆解
动手写代码之前,我花了两天时间梳理需求,跟行政、人事、财务几个部门反复确认,最后归纳成了四个方面:
打卡记录收集:员工每天上下班各打一次卡,记录精确到秒。这个看起来简单,但需要考虑异常情况,比如员工一天内多次打卡,以哪个为准;跨天打卡(晚上加班到第二天凌晨)算哪一天的记录。
考勤规则配置:公司实行的是固定上班时间,上午九点到下午六点,中午休息一小时,但不同部门有差异。比如客服部是排班制,研发部弹性半小时。所以考勤规则必须可配置,不能写死在代码里。
异常自动判定:系统要能自动判断哪些记录是正常的、哪些是迟到、早退、缺卡、加班,并且把异常数据单独列出来,让管理员重点审核。光是这块的判定逻辑,我就改了三版,后面会详细讲。
统计报表输出:月底要根据每天的考勤结果,汇总出每个员工的出勤天数、迟到次数、早退次数、请假天数、加班时长,供薪酬核算使用。报表要能导出成Excel,不然财务那边没法用。
1.3 目录结构与代码组织
这套系统的代码组织,我参照了经典的MVC思路,但没引入重型框架,用的是原生PHP加一个极简的路由。目录结构是这样:
attendance_system/ ├── admin/ # 后台管理端 │ ├── controllers/ # 控制器层 │ ├── models/ # 数据模型层 │ ├── views/ # 视图层模板 │ └── index.php # 后台入口,负责路由分发 ├── api/ # 前端打卡接口(手机端和PC端用) │ ├── checkin.php # 打卡接口 │ ├── query.php # 查询个人考勤 │ └── auth.php # 登录鉴权 ├── config/ │ └── database.php # 数据库连接配置 ├── public/ # 静态资源 │ ├── css/ │ ├── js/ │ └── uploads/ # 头像等上传文件 └── install/ └── attendance.sql # 数据库初始化脚本这样分层的目的是保持逻辑清晰和后续可维护。控制器只负责接收参数和调用模型,模型只管数据库读写,视图只做HTML渲染。不做成微服务那样复杂的架构,因为内部系统没必要,但基本的职责分离必须做到。
2. 数据库设计与核心表结构
2.1 数据表关系总览
考勤系统的数据表不多,但每张表之间的关联关系需要设计清楚。我总共建了6张核心表:员工表、部门表、考勤规则表、打卡记录表、考勤汇总表、请假申请单。
这6张表的关系有点像一个树状结构:部门挂在组织架构下,员工挂在部门下,打卡记录属于员工,考勤汇总表是每天都由定时任务生成的"快照",请假单又跟考勤汇总表交叉影响。设计表结构时最重要的是让打卡记录表尽量精简,因为它会是数据量最大的表,一天几百人打卡,一年下来就是十几万条记录。
2.2 员工表与部门表
员工表其实就是用户表的扩展,我去掉了跟考勤无关的字段,保留了最关键的几个:
CREATE TABLE `emp_employee` ( `id` int(11) NOT NULL AUTO_INCREMENT, `emp_no` varchar(20) NOT NULL COMMENT '员工工号', `emp_name` varchar(50) NOT NULL COMMENT '员工姓名', `dept_id` int(11) NOT NULL COMMENT '所属部门ID', `position` varchar(50) DEFAULT NULL COMMENT '职位', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `hire_date` date DEFAULT NULL COMMENT '入职日期', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1在职 0离职', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `emp_no` (`emp_no`), KEY `dept_id` (`dept_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工信息表';这里要注意的是工号字段必须唯一,因为后续所有跟员工相关的查询都靠它关联。有些系统喜欢用自增ID关联,但工号是员工能记住的、能口口相传的标识,用它做唯一键更合理。
部门表很简单,主要字段就是部门名称、上级部门ID、负责人ID。部门表跟考勤规则是绑定的,后面会讲到。
2.3 考勤规则表的设计
考勤规则表是整个系统的配置中枢,我是这样设计的:
CREATE TABLE `attendance_rule` ( `id` int(11) NOT NULL AUTO_INCREMENT, `rule_name` varchar(50) NOT NULL COMMENT '规则名称', `dept_id` int(11) DEFAULT NULL COMMENT '适用部门ID,NULL为全公司', `work_start_time` time NOT NULL COMMENT '上班时间', `work_end_time` time NOT NULL COMMENT '下班时间', `late_threshold` int(11) DEFAULT '0' COMMENT '迟到判定宽限分钟数', `early_threshold` int(11) DEFAULT '0' COMMENT '早退判定宽限分钟数', `workday_type` tinyint(1) DEFAULT '1' COMMENT '1周一至周五 2周六日 3自定义', `custom_workdays` varchar(100) DEFAULT NULL COMMENT '自定义工作日,逗号分隔1-7', `status` tinyint(1) NOT NULL DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考勤规则表';这个表最灵活的地方是支持部门级覆盖。比如研发部实行弹性工作制,上班时间可以晚到9点30,那我就在部门表里绑一条独立的规则。如果没有部门级规则,就使用全公司默认规则。代码里用一条SQL就能实现这个逻辑:
SELECT * FROM attendance_rule WHERE status = 1 AND (dept_id = ? OR dept_id IS NULL) ORDER BY dept_id IS NULL ASC LIMIT 1dept_id IS NULL在排序里排在最后,所以如果存在部门专属规则,会优先命中部门规则,否则就落到全公司规则。
2.4 打卡记录表
打卡记录表是数据量最大的表,设计时我特别注意了索引:
CREATE TABLE `attendance_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `emp_id` int(11) NOT NULL COMMENT '员工ID', `record_date` date NOT NULL COMMENT '打卡日期', `check_in_time` datetime DEFAULT NULL COMMENT '上班打卡时间', `check_out_time` datetime DEFAULT NULL COMMENT '下班打卡时间', `source` tinyint(1) DEFAULT '1' COMMENT '打卡来源 1PC 2手机 3指纹机导入', `remark` varchar(200) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `emp_id_date_idx` (`emp_id`, `record_date`), KEY `record_date_idx` (`record_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日打卡记录表';这里我采用了一个比较"反直觉"的设计:每条每日打卡记录只有一行,而不是每次打卡插一行。为什么?因为考勤统计的核心粒度是"人/天",上班打卡和下班打卡合并在一行里,后面做月统计时只需要对这一行进行判断,不用做两次打卡的配对,SQL写起来简单太多。
当然这带来一个代价:如果一个人一天内打了多次卡,比如早上来了打了卡,中途出去又回来再打一次,那就需要额外逻辑来处理"最后一次打卡"。我的处理方式是:同一个员工同一天内,第一次打卡记为上班时间,最后一次打卡记为下班时间。这样既保证数据简洁,又不会丢失多次打卡的信息。
2.5 考勤汇总表与请假表
考勤汇总表不是实时计算的,而是每天凌晨由定时任务统一生成前一天的数据快照。这样做的好处是月底统计时不需要临时去跑几千条打卡记录,直接查汇总表就行,速度快很多。汇总表长这样:
CREATE TABLE `attendance_summary` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `emp_id` int(11) NOT NULL, `summary_date` date NOT NULL, `status` tinyint(1) DEFAULT '0' COMMENT '0正常 1迟到 2早退 3缺卡 4请假 5加班', `work_hours` decimal(5,2) DEFAULT '0.00' COMMENT '标准工时', `actual_hours` decimal(5,2) DEFAULT '0.00' COMMENT '实际工时', `overtime_hours` decimal(5,2) DEFAULT '0.00' COMMENT '加班时长', `late_minutes` int(11) DEFAULT '0' COMMENT '迟到分钟数', `early_minutes` int(11) DEFAULT '0' COMMENT '早退分钟数', PRIMARY KEY (`id`), UNIQUE KEY `emp_date_unique` (`emp_id`, `summary_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日考勤汇总表';请假申请表相对简单,就是记录请假类型(事假、病假、年假等)、开始时间、结束时间、审批状态。真正复杂的是请假跟考勤判定之间的交互逻辑,后面在核心逻辑里详细说。
3. 核心功能模块实现与踩坑记录
3.1 打卡接口的实现细节
打卡接口是整个系统的门面,员工每天都要用,必须稳定且快速。我写的接口很简单,前端传员工ID和打卡类型,后端做校验,然后写入打卡记录表。
// api/checkin.php 核心代码 $emp_id = intval($_POST['emp_id'] ?? 0); $type = $_POST['type'] ?? ''; // in 上班打卡, out 下班打卡 if ($emp_id <= 0 || !in_array($type, ['in', 'out'])) { echo json_encode(['code' => 400, 'msg' => '参数错误']); exit; } $today = date('Y-m-d'); $now = date('Y-m-d H:i:s'); // 查询当天是否已有记录 $record = $db->query("SELECT * FROM attendance_record WHERE emp_id = {$emp_id} AND record_date = '{$today}'")->fetch(); try { if ($type === 'in') { if (!$record) { // 第一次上班打卡,插入记录 $db->exec("INSERT INTO attendance_record (emp_id, record_date, check_in_time) VALUES ({$emp_id}, '{$today}', '{$now}')"); } else if ($record['check_in_time'] === null) { // 当天还没打上班卡,补上 $db->exec("UPDATE attendance_record SET check_in_time = '{$now}' WHERE id = {$record['id']}"); } // 如果已经打过上班卡,则忽略重复的上班打卡请求 } else { // 下班打卡逻辑 if (!$record) { // 只有下班卡没有上班卡,插入记录并标记异常,后面补判定 $db->exec("INSERT INTO attendance_record (emp_id, record_date, check_out_time) VALUES ({$emp_id}, '{$today}', '{$now}')"); } else { // 已有记录,更新下班时间为最新一次打卡时间 $db->exec("UPDATE attendance_record SET check_out_time = '{$now}' WHERE id = {$record['id']}"); } } echo json_encode(['code' => 200, 'msg' => '打卡成功']); } catch (Exception $e) { echo json_encode(['code' => 500, 'msg' => '系统繁忙,请稍后再试']); // 记录错误日志 error_log($e->getMessage()); }这段代码踩了好几个坑:
第一个坑是并发重复提交。员工可能手快,双击了打卡按钮,或者APP网络重试,导致同一个人同一秒钟调用了两次接口。如果没有唯一约束,就会出现两条记录。解决方法是在插入前加一个短时间窗口的判断——如果当前时间距该员工最后一次操作时间不足5秒,直接返回"请勿重复打卡"。更稳妥的做法是在数据库层面加唯一索引,但那样会牺牲灵活性,所以我在代码层做了限制。
第二个坑是加班跨天打卡。员工晚上加班到凌晨,这时候打的卡算哪一天的?我的处理方式是:打卡时间在凌晨4点之前的,归到前一天的下班记录。用时间来判断,比简单的"今天日期"更准确。
3.2 考勤判定逻辑——最烧脑的部分
这部分是整个系统的灵魂。判定逻辑我迭代了三版才理清所有边界条件,核心思路是这样:
提取员工的打卡记录和考勤规则,然后按以下顺序判断:
- 如果当天有请假记录且审批通过,直接标记为"请假",不参与迟到早退判定。
- 如果没有上下班打卡记录,标记为"缺卡",进入异常待处理列表。
- 如果上班打卡时间晚于规则里配置的上班时间加上宽限分钟数,标记为"迟到"。
- 如果下班打卡时间早于规则里配置的下班时间减去宽限分钟数,标记为"早退"。
- 正常打卡的情况下,计算实际工时。
- 如果实际工时超过标准工时,多出的部分标记为"加班"。
这里最麻烦的是请假和缺卡的重叠。比如员工上午请假,下午正常来上班——那他上午没打卡不算缺卡,但下午必须有一次打卡。我的判定逻辑里,请假按时间分成全天、上午、下午三种。半天请假时,只检查对应半天的打卡情况,另外半天还是要正常判定的。
还有一个必须处理的边界:员工出差或外勤。出差期间不能打卡,如果直接判定缺卡,员工和HR都会崩溃。所以考勤规则里还配了一个"外勤免打卡"选项。匹配到出差申请单的员工,在出差日期范围内,考勤状态直接标记为"外勤",不参与任何迟到早退缺卡判定。
// 考勤判定核心伪代码 function judgeAttendance($emp_id, $date, $rule) { $record = getRecord($emp_id, $date); $leave = getApprovedLeave($emp_id, $date); $businessTrip = getApprovedTrip($emp_id, $date); // 1. 外勤优先判断 if ($businessTrip) { return ['status' => '外勤', 'hours' => 0]; } // 2. 全天请假 if ($leave && $leave['type'] == 'full_day') { return ['status' => '请假', 'hours' => 0]; } // 3. 半天请假,上班时段请假则不判迟到 $isLate = false; $isEarly = false; if (!$record || (!$record['check_in_time'] && !$leave_half_morning)) { return ['status' => '缺卡', 'hours' => 0]; } if ($record['check_in_time'] && $record['check_in_time'] > $rule['work_start_time'] + $rule['late_threshold']) { $isLate = true; } // 早退判断逻辑类似 $workHours = calculateHours($record['check_in_time'], $record['check_out_time'], $rule); return [ 'status' => $isLate ? '迟到' : ($isEarly ? '早退' : '正常'), 'hours' => $workHours ]; }这套逻辑有个大坑,我踩得很深:htmlspecialchars之类的转义问题先放一边,单说工时的算法。中午休息一小时要不要从工时里扣?很多公司中午12点到1点是纯休息。我的算法是:如果员工在中午12点到1点半之间有打卡记录,视为午休打卡,中午这段不记入工时。如果员工没打午休卡,就按规则里的固定午休时间扣除。这块要跟公司行政确认清楚,不同公司差异很大。
3.3 定时任务生成每日汇总
每天晚上11点50分,我用crontab跑一个PHP脚本,把当天所有人的考勤汇总到attendance_summary表里:
50 23 * * * php /www/wwwroot/attendance_system/admin/cron/daily_summary.php这个脚本的核心逻辑是:遍历所有在职员工,对每个人调用上面的判定函数,然后把结果写入汇总表。如果汇总表里已经有当天的记录,先删除再重新生成,保证幂等性。
实际跑起来后我发现一个问题:脚本跑到一半,有人还在加班打卡。晚上11点50分生成汇总时,有些员工可能还没打下班卡,或者加班到更晚。所以我把生成时间从23点50分调整到第二天凌晨1点——这时候绝大多数人都打完卡了,数据准确性高很多。
如果加班特别晚,凌晨1点以后才打下班卡,那这个卡就归到第二天去匹配了。对此我的处理是:每天凌晨2点再跑一次增量更新,针对凌晨1点到2点之间的新打卡记录,重新判定并更新对应的汇总行。这样能覆盖绝大部分加班场景。
3.4 后台管理界面:让行政用着顺手
考勤系统最终使用者是行政和HR,她们不是程序员,界面必须简单直接。后台我做了四个菜单:
当日考勤:默认打开就是今天的考勤总览,绿色代表正常,黄色代表迟到,红色代表缺卡。点进去能看到每个人的打卡时间明细。
异常处理:系统自动判定为异常(迟到、早退、缺卡)的记录集中显示在这里。行政人员可以逐条处理,比如员工漏打卡但有OA审批,可以直接把异常标记为"已处理"并备注原因。处理过的异常会在月度报表里排除。
排班管理:对于客服部等排班制的部门,可以在这里单独设置某个人在某天的上下班时间,覆盖掉默认规则。这个功能一开始没做,后来客服部同事提需求,说她们有早班晚班之分,才加的。
月度报表:按月选择,系统自动汇总每个人的出勤数据,支持导出Excel。
这里有一个很重要的设计经验:所有对考勤结果的"人工干预",都必须留痕。行政改了什么、备注了什么,系统都要记录下来。不然月底对账时,员工质疑"我那天明明是外勤,为什么算缺卡",没有修改记录就说不清了。
4. 源码部署与配置避坑指南
4.1 环境要求与初始化
这套系统对环境的要求很基础,PHP 7.0以上,MySQL 5.6以上,Nginx或Apache都行。我本地开发用的是PHP 7.4,线上是PHP 7.2,跑起来没任何问题。PHP 8.0我也测过,核心功能兼容,但有些老写法会报警告,建议生产环境还是用7.x。
部署步骤很简单:
- 把代码上传到服务器Web目录。
- 创建数据库,导入
install/attendance.sql。 - 修改
config/database.php,填入数据库账号密码。 - 配置Nginx伪静态,把请求都转到入口文件。
- 设置
cron定时任务。
一个经常出问题的点是文件权限。public/uploads目录要给写权限,不然员工上传头像会失败。还有日志目录,建议单独建一个,别跟系统目录混在一起,方便排查问题。
4.2 Nginx伪静态配置
如果你用Nginx,需要配置一个伪静态规则,不然所有请求都会404:
location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/tmp/php-cgi.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; }核心就是try_files那行:当请求的路径不是真实文件时,统一交给index.php处理,由PHP路由来分发。Apache的话用.htaccess文件也能实现类似效果。
4.3 数据库连接池与性能优化
小规模考勤系统不太需要连接池,PHP每次请求结束就会自动释放MySQL连接。但我做了一个单例模式的数据库连接类,避免同一个请求里多次new连接对象:
class Database { private static $instance = null; private $conn; private function __construct($config) { $this->conn = new PDO( "mysql:host={$config['host']};dbname={$config['dbname']};charset=utf8mb4", $config['user'], $config['pass'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] ); } public static function getInstance($config) { if (self::$instance === null) { self::$instance = new Database($config); } return self::$instance; } }这里要坚持用PDO预处理,防注入是一方面,更实际的好处是SQL语句能复用,执行效率更高。我在代码里所有跟数据库交互的地方都用prepare+execute,很少直接字符串拼接SQL。
性能方面,打卡记录表是数据量增长最快的,我按月做了分表策略。每年1月自动创建attendance_record_2025_01这种格式的分表,查询时根据日期自动路由到对应的分表。这个优化在上线后第五个月就体现出了价值——那时主表已经积累了十几万条记录,分表后查询速度基本没变化。
5. 常见问题排查与月度对账实战
5.1 员工打卡签到/签退状态不对
这是上线初期被问得最多的问题。我排查了一圈,发现绝大多数情况都是员工搞混了"上班卡"和"下班卡"的按钮。PC端界面我把两个按钮做得一样大,色调区分不明显,员工着急时就点错了。
解决方式是重新设计界面:上班打卡按钮用绿色,下班打卡按钮用蓝色,加文字提示,并且在打卡成功后弹出明显的提示框,告诉员工现在是几点、打的是哪种卡。UI的小调整,直接让错误率降了八成。
5.2 定时任务为什么没跑
有次月底做报表,发现每天的汇总数据有几天是空的。查了半天,发现是crontab脚本里的PHP路径写的是相对路径,导致定时任务执行时找不到文件。
# 错误写法 50 23 * * * php daily_summary.php # 正确写法 50 23 * * * php /www/wwwroot/attendance_system/admin/cron/daily_summary.php吃一堑长一智,后来我在脚本开头加了一步自动切换工作目录:
chdir(__DIR__); // 切换到脚本所在目录这样即使crontab里的路径写得不规范,脚本也能正常运行。另外建议定时任务的日志单独存文件,出问题方便排查:
50 23 * * * php /www/wwwroot/attendance_system/admin/cron/daily_summary.php >> /var/log/attendance.log 2>&15.3 月度对账:数据对不上怎么办
月底对账时最容易出现的矛盾集中在两类:迟到次数对不上和加班时长有争议。
迟到对不上的原因,多半是行政人工改过考勤状态,但系统里没有留痕。比如某天员工堵车迟到了10分钟,行政通融一下把状态改成了正常,但原始打卡记录里还是显示迟到时间。等到月底汇总时,系统按原始记录统计,跟行政的Excel记录就对不上了。
解决这个问题的思路很直接:既然人工干预了,就必须记录下来。我在异常处理流程里加了"修改人"和"修改时间"字段,每次人工修改都强制填备注。月底导出报表时,可以选择"含人工调整"或"仅原始记录"两个版本,对账时拿调整后的版本,就不会有争议。
加班时长的争议更微妙。很多公司规定加班要提前申请,不是打了卡就算加班。系统里我接入了加班申请单:只有提前提交且审批通过的加班时间段,才会被计入加班时长。员工实际打卡时间和申请时间有出入的,取两者交集。这样虽然对员工来说严格一些,但财务、法务都说好,因为合规。
5.4 排班制的坑:正则表达式日期匹配
客服部三班倒的排班,一开始是用PHP的date('N')函数判断星期几来匹配规则,后来发现遇到节假日调休就全乱了。
比如国庆节期间,国家规定周一到周五放假,但周六日要调休上班。考勤规则里"周一至周五上班"到了调休日就不适用。我加了一个特殊工作日配置表,专门存放调休安排。节假日规律是这样的:
- 平时:按设定的工作日类型(周一至周五)判定
- 遇到法定节假日:读取特殊工作日表,节假日标为休息日,调休日标为工作日
- 排班制部门:直接按员工个人的排班表判定,特殊工作日表不适用
这个坑让我意识到,考勤系统的灵活度远比想象中重要。不同类型的公司、甚至同一公司不同部门,考勤规则都可能完全不同。设计时就要预留"特殊日期覆盖"这种升级空间,不要写死逻辑。
6. 系统上线后的优化与扩展方向
6.1 从指纹机到手机定位打卡
其实最早公司用的是一台老旧的指纹机,员工排队打卡,经常因为手指出汗识别不了。后来我们接入了手机定位打卡,用的是微信企业号的JS-SDK,员工在公司500米范围内就能定位打卡。
这个方案的背后逻辑是用GPS定位替代物理打卡机,解决的问题是"人必须到现场",对销售、外勤这类岗位特别友好。唯一需要考虑的是隐私——定位数据只保留在系统里,不做其他用途,并且员工可以在后台看到自己的定位历史,透明化反而减少抵触。
6.2 离职员工的考勤数据归档
考勤数据本质上是员工的劳动记录,涉及法律合规,不能随便删。我的做法是:员工离职后,其考勤记录自动归档到archived_attendance表,保留5年。原表里的数据标记emp_status=0,不再参与任何月度统计。这样可以保证:历史报表永远不可变,不管后续出什么问题,都能追溯到原始数据。
6.3 后续想做的方向
下一步计划加两个功能:第一个是智能异常提醒,如果员工连续三天都迟到,系统自动给部门负责人发通知,让管理者提前关注,不用等月底才发现。第二个是考勤Open API,对接内部的OA系统和薪酬系统,让考勤结果直接推送到薪酬核算系统里,彻底消灭Excel手工对账。
这两个方向都是从实际业务痛点出发的。做内部系统久了你会发现,技术永远不是瓶颈,业务理解才是。一套考勤系统,看似只是记录上下班时间,实则考验的是对公司管理逻辑的理解深度。
写在最后的一点体会
把这套系统从零搭到上线,中间踩过的坑、改过的需求、被同事吐槽过的体验细节,都记在源码的注释里了。我最想分享给同样在做内部系统的人两个经验:
第一,先跟业务方对齐规则,再写代码。考勤规则这东西,每个公司都不一样,甚至同一个公司的不同部门都不一样的。前期多花几天时间跟行政、人事、财务反复确认规则边界,比后期返工划算得多。我最大的教训就是一开始按自己的想法设计了"统一规则",结果客服部一上线就报错,重新排班管理模块又花了一周。
第二,考勤系统的灵魂在异常处理,不在正常打卡。正常打卡人人都一样,没什么好做的。真正拉开系统好用程度差距的,是那些"特殊情况"处理得够不够灵活——漏打卡了怎么办,外勤了怎么办,请假和加班重叠了怎么算。这些场景覆盖得越全,行政同事的月度工作量就越小。
这套源码我整理了完整版放在了00588这个版本里,数据库脚本、后台前后端、定时任务、文档注释都是齐全的。有需要的朋友可以直接拉下来跑,跑通了再对着本文的说明去理解每一块逻辑,应该很快就能上手改造成适合你自己公司的那一套。