如何按天计算activities表中checkin/checkout打卡的小时分钟总时长
现有代码存在的核心缺陷
- 循环嵌套错误:将原本已是活动列表的
$activities额外包裹为单层数组传入循环,导致循环仅执行1次,根本没法遍历所有打卡记录 - 未排序数据:数据库查询返回的活动记录默认无固定时间排序,直接按索引配对打卡类型会出现checkin和checkout完全错位的问题
- 字段匹配错误:表结构仅包含
start时间字段,代码中错误读取不存在的end字段 - 边界逻辑错误:查询条件设置为
start <= 当天23:59,会漏掉当天23:59:00之后的有效打卡记录;同时$d1 = $d1++这类后置自增写法不会修改变量值,索引始终无法推进 - 无异常兜底:未对连续两次checkin、连续两次checkout、当天最后一次打卡无对应匹配项的异常场景做处理,计算结果会出现偏差
正确计算逻辑
计算规则
- 拉取指定员工在目标日期的所有打卡记录,强制按
start字段的时间值升序排序 - 按顺序遍历打卡列表:遇到类型为
checkin的记录时暂存为当前段的开始时间,遇到下一个相邻的checkout记录时,计算两个时间点的时间差,累加到总时长 - 若遍历到列表末尾仍有未匹配的checkin记录,直接截断为当天23:59的时间点计算段时长,避免跨天打卡导致的时长错配
- 累计所有段的时长后,可换算为总小时、总分钟数值:总分钟换算仅需将累计总秒数除以60即可,和小时计算逻辑完全一致
修正后的PHP实现
function sum_all_hours_by_day($fxemploye_id, $day){ $hours = 0; $date = date_create($day); $date_format = date_format($date, 'Y-m-d'); // 修正查询边界:覆盖当天00:00到次日00:00之前的全量记录 $day_start = $date_format . " 00:00:00"; $day_end = date('Y-m-d 00:00:00', strtotime($date_format . " +1 day")); // 查询活动并按打卡时间升序排序 $options = [ 'conditions' => [ 'fxemploye_id = ? and start >= ? and start < ?', $fxemploye_id, $day_start, $day_end ], 'order' => 'start ASC' ]; $activities = FxemployeHasActivity::all($options); $active_checkin_time = null; $total_seconds = 0; // 直接遍历全量活动列表 foreach ($activities as $activity) { $current_time = strtotime($activity->start); $current_type = $activity->type; if ($current_type == 'checkin') { // 遇到新的checkin,覆盖暂存的开始时间,避免连续checkin的异常 $active_checkin_time = $current_time; } elseif ($current_type == 'checkout' && !is_null($active_checkin_time)) { // 匹配到checkout,计算段时长 $total_seconds += $current_time - $active_checkin_time; // 清空暂存的checkin时间 $active_checkin_time = null; } } // 兜底:如果当天最后有未匹配的checkin,截断到当天结束时间计算 if (!is_null($active_checkin_time)) { $day_end_ts = strtotime($day_end); $total_seconds += ($day_end_ts - 1) - $active_checkin_time; } // 换算为小时(保留2位小数) $hours = round($total_seconds / 3600, 2); return $hours; }
可选优化:SQL层直接计算
如果使用的数据库支持窗口函数,可以直接在查询阶段配对相邻打卡记录,减少PHP层的计算开销,核心SQL逻辑为:
SELECT SUM(TIMESTAMPDIFF(SECOND, start, next_start)) / 3600 AS total_hours FROM ( SELECT type, start, LEAD(start) OVER (ORDER BY start ASC) AS next_start FROM activities WHERE fxemploye_id = 123 AND start >= '2020-11-30 00:00:00' AND start < '2020-12-01 00:00:00' ) t WHERE type = 'checkin';
注意:该SQL仅能计算有明确配对checkout的时段,若需兼容兜底逻辑需额外补充截断判断。
内容的提问来源于stack exchange,提问作者Met El Idrissi
相关产品推荐
相关产品推荐

