diff --git a/app/Http/Controllers/Api/SystemController.php b/app/Http/Controllers/Api/SystemController.php index 22724a168..4d8760ff9 100755 --- a/app/Http/Controllers/Api/SystemController.php +++ b/app/Http/Controllers/Api/SystemController.php @@ -456,6 +456,24 @@ class SystemController extends AbstractController if ($all['modes']) { $all['modes'] = array_intersect($all['modes'], ['auto', 'manual', 'locat', 'face']); } + // 验证提前和延后时间是否重叠(跨天打卡支持) + if ($all['open'] === 'open') { + $times = is_array($all['time']) ? $all['time'] : Base::json2array($all['time']); + if (count($times) >= 2) { + $startMinutes = intval(substr($times[0], 0, 2)) * 60 + intval(substr($times[0], 3, 2)); + $endMinutes = intval(substr($times[1], 0, 2)) * 60 + intval(substr($times[1], 3, 2)); + $shiftDuration = $endMinutes - $startMinutes; + if ($shiftDuration <= 0) { + $shiftDuration += 24 * 60; // 处理跨天班次 + } + $advance = intval($all['advance']) ?: 120; + $delay = intval($all['delay']) ?: 120; + $maxAllowed = 24 * 60 - $shiftDuration; + if ($advance + $delay >= $maxAllowed) { + return Base::retError('提前和延后时间设置存在重叠,最大提前+延后时间不能超过 ' . ($maxAllowed - 1) . ' 分钟'); + } + } + } $setting = Base::setting('checkinSetting', Base::newTrim($all)); } else { $setting = Base::setting('checkinSetting'); @@ -1271,6 +1289,8 @@ class SystemController extends AbstractController // $secondStart = strtotime("2000-01-01 {$time[0]}") - strtotime("2000-01-01 00:00:00"); $secondEnd = strtotime("2000-01-01 {$time[1]}") - strtotime("2000-01-01 00:00:00"); + // 获取延后时间配置(用于跨天打卡导出) + $delaySeconds = (intval($setting['delay']) ?: 120) * 60; // $botUser = User::botGetOrCreate('system-msg'); if (empty($botUser)) { @@ -1279,7 +1299,7 @@ class SystemController extends AbstractController $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid); // $doo = Doo::load(); - go(function () use ($doo, $secondStart, $secondEnd, $time, $userid, $date, $user, $botUser, $dialog) { + go(function () use ($doo, $secondStart, $secondEnd, $time, $userid, $date, $user, $botUser, $dialog, $delaySeconds) { Coroutine::sleep(1); // $headings = []; @@ -1316,9 +1336,10 @@ class SystemController extends AbstractController $index++; $sameDate = date("Y-m-d", $startT); $sameTimes = $recordTimes[$sameDate] ?? []; - $sameCollect = UserCheckinRecord::atCollect($sameDate, $sameTimes); + $sameCollect = UserCheckinRecord::atCollect($sameDate, $sameTimes, $time[0]); $firstBetween = [Carbon::createFromTimestamp($startT), Carbon::createFromTimestamp($startT + $secondEnd - 1)]; - $lastBetween = [Carbon::createFromTimestamp($startT + $secondStart + 1), Carbon::createFromTimestamp($startT + 86400)]; + // 扩展下班打卡范围以支持跨天打卡 + $lastBetween = [Carbon::createFromTimestamp($startT + $secondStart + 1), Carbon::createFromTimestamp($startT + 86400 + $delaySeconds)]; $firstRecord = $sameCollect?->whereBetween("datetime", $firstBetween)->first(); $lastRecord = $sameCollect?->whereBetween("datetime", $lastBetween)->last(); $firstTimestamp = $firstRecord['timestamp'] ?: 0; diff --git a/app/Models/UserBot.php b/app/Models/UserBot.php index 92c960c13..56dbe0ab3 100644 --- a/app/Models/UserBot.php +++ b/app/Models/UserBot.php @@ -352,16 +352,47 @@ class UserBot extends AbstractModel $advance = (intval($setting['advance']) ?: 120) * 60; $delay = (intval($setting['delay']) ?: 120) * 60; // + $currentTime = Timer::time(); $nowDate = date("Y-m-d"); $nowTime = date("H:i:s"); + $yesterdayDate = date("Y-m-d", strtotime("-1 day")); // + // 今天的签到窗口 $timeStart = strtotime("{$nowDate} {$times[0]}"); $timeEnd = strtotime("{$nowDate} {$times[1]}"); $timeAdvance = max($timeStart - $advance, strtotime($nowDate)); - $timeDelay = min($timeEnd + $delay, strtotime("{$nowDate} 23:59:59")); + // 移除 23:59:59 限制,允许跨天 + $todayTimeDelay = $timeEnd + $delay; + // + // 昨天的延后窗口(用于判断凌晨打卡归属) + $yesterdayTimeEnd = strtotime("{$yesterdayDate} {$times[1]}"); + $yesterdayTimeDelay = $yesterdayTimeEnd + $delay; + // + // 判断签到归属哪天 + $targetDate = null; + $checkType = null; // 'up' 或 'down' + // + // 情况1:在今天的有效窗口内 + if ($currentTime >= $timeAdvance && $currentTime <= $todayTimeDelay) { + $targetDate = $nowDate; + if ($currentTime < $timeEnd) { + $checkType = 'up'; + } else { + $checkType = 'down'; + } + } + // 情况2:凌晨时段,检查是否在昨天的延后窗口内 + elseif ($currentTime < $timeAdvance && $currentTime <= $yesterdayTimeDelay) { + $targetDate = $yesterdayDate; + $checkType = 'down'; + } + // + // 构建错误消息 $errorTime = false; - if (Timer::time() < $timeAdvance || $timeDelay < Timer::time()) { - $errorTime = "不在有效时间内,有效时间为:" . date("H:i", $timeAdvance) . "-" . date("H:i", $timeDelay); + if (!$targetDate) { + $displayDelay = date("H:i", $todayTimeDelay % 86400); + $nextDay = ($todayTimeDelay > strtotime("{$nowDate} 23:59:59")) ? "(次日)" : ""; + $errorTime = "不在有效时间内,有效时间为:" . date("H:i", $timeAdvance) . "-{$nextDay}{$displayDelay}"; } // $macs = explode(",", $mac); @@ -375,7 +406,7 @@ class UserBot extends AbstractModel $array[] = [ 'userid' => $UserCheckinMac->userid, 'mac' => $UserCheckinMac->mac, - 'date' => $nowDate, + 'date' => $targetDate ?: $nowDate, ]; $checkins[] = [ 'userid' => $UserCheckinMac->userid, @@ -396,7 +427,7 @@ class UserBot extends AbstractModel $array[] = [ 'userid' => $UserInfo->userid, 'mac' => '00:00:00:00:00:00', - 'date' => $nowDate, + 'date' => $targetDate ?: $nowDate, ]; $checkins[] = [ 'userid' => $UserInfo->userid, @@ -431,7 +462,8 @@ class UserBot extends AbstractModel } return null; }; - $sendMsg = function($type, $checkin) use ($errorTime, $alreadyTip, $getJokeSoup, $botUser, $nowDate) { + $sendMsg = function($type, $checkin) use ($errorTime, $alreadyTip, $getJokeSoup, $botUser, $targetDate, $nowDate) { + $displayDate = $targetDate ?: $nowDate; $dialog = WebSocketDialog::checkUserDialog($botUser, $checkin['userid']); if (!$dialog) { return; @@ -448,12 +480,13 @@ class UserBot extends AbstractModel } return; } - // 判断已打卡 - $cacheKey = "Checkin::sendMsg-{$nowDate}-{$type}:" . $checkin['userid']; + // 判断已打卡(使用目标日期作为缓存键) + $cacheKey = "Checkin::sendMsg-{$displayDate}-{$type}:" . $checkin['userid']; $typeContent = $type == "up" ? "上班" : "下班"; if (Cache::get($cacheKey) === "yes") { if ($alreadyTip) { - $text = "今日已{$typeContent}打卡,无需重复打卡。"; + $dateHint = ($displayDate != $nowDate) ? "({$displayDate})" : "今日"; + $text = "{$dateHint}已{$typeContent}打卡,无需重复打卡。"; $text .= $checkin['remark'] ? " ({$checkin['remark']})": ""; WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [ 'type' => 'content', @@ -467,7 +500,8 @@ class UserBot extends AbstractModel $hi = date("H:i"); $remark = $checkin['remark'] ? " ({$checkin['remark']})": ""; $subcontent = $getJokeSoup($type, $checkin['userid']); - $title = "{$typeContent}打卡成功,打卡时间: {$hi}{$remark}"; + $dateInfo = ($displayDate != $nowDate) ? "(记录归属 {$displayDate})" : ""; + $title = "{$typeContent}打卡成功,打卡时间: {$hi}{$remark}{$dateInfo}"; WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [ 'type' => 'content', 'title' => $title, @@ -482,14 +516,13 @@ class UserBot extends AbstractModel ], ], $botUser->userid, false, false, $type != "up"); }; - if ($timeAdvance <= Timer::time() && Timer::time() < $timeEnd) { - // 上班打卡通知(从最早打卡时间 到 下班打卡时间) + // 根据打卡类型发送通知 + if ($checkType === 'up') { foreach ($checkins as $checkin) { $sendMsg('up', $checkin); } } - if ($timeEnd <= Timer::time() && Timer::time() <= $timeDelay) { - // 下班打卡通知(下班打卡时间 到 最晚打卡时间) + if ($checkType === 'down') { foreach ($checkins as $checkin) { $sendMsg('down', $checkin); } diff --git a/app/Models/UserCheckinRecord.php b/app/Models/UserCheckinRecord.php index 6a2545b41..d412026a4 100644 --- a/app/Models/UserCheckinRecord.php +++ b/app/Models/UserCheckinRecord.php @@ -88,16 +88,32 @@ class UserCheckinRecord extends AbstractModel /** * 时间收集 - * @param string $data - * @param array $times + * @param string $data 日期 + * @param array $times 签到时间数组 + * @param string|null $shiftStart 班次开始时间(如 "09:00"),用于判断跨天 * @return \Illuminate\Support\Collection */ - public static function atCollect($data, $times) + public static function atCollect($data, $times, $shiftStart = null) { - $sameTimes = array_map(function($time) use ($data) { + $shiftStartMinutes = null; + if ($shiftStart) { + $parts = explode(':', $shiftStart); + $shiftStartMinutes = intval($parts[0]) * 60 + intval($parts[1]); + } + + $sameTimes = array_map(function($time) use ($data, $shiftStartMinutes) { + $parts = explode(':', $time); + $timeMinutes = intval($parts[0]) * 60 + intval($parts[1]); + + // 如果签到时间早于班次开始时间,视为跨天打卡(属于次日凌晨) + $targetDate = $data; + if ($shiftStartMinutes !== null && $timeMinutes < $shiftStartMinutes) { + $targetDate = date("Y-m-d", strtotime($data . " +1 day")); + } + return [ - "datetime" => "{$data} {$time}", - "timestamp" => strtotime("{$data} {$time}") + "datetime" => "{$targetDate} {$time}", + "timestamp" => strtotime("{$targetDate} {$time}") ]; }, $times); return collect($sameTimes); diff --git a/resources/assets/js/pages/manage/setting/components/SystemCheckin.vue b/resources/assets/js/pages/manage/setting/components/SystemCheckin.vue index 0b38a1105..3ca362a87 100644 --- a/resources/assets/js/pages/manage/setting/components/SystemCheckin.vue +++ b/resources/assets/js/pages/manage/setting/components/SystemCheckin.vue @@ -411,11 +411,36 @@ export default { submitForm() { this.$refs.formData.validate((valid) => { if (valid) { + // 验证提前和延后时间是否重叠 + if (this.formData.open === 'open') { + const times = this.formData.time; + if (times && times.length >= 2) { + const startMinutes = this.timeToMinutes(times[0]); + const endMinutes = this.timeToMinutes(times[1]); + let shiftDuration = endMinutes - startMinutes; + if (shiftDuration <= 0) shiftDuration += 24 * 60; + + const advance = parseInt(this.formData.advance) || 120; + const delay = parseInt(this.formData.delay) || 120; + const maxAllowed = 24 * 60 - shiftDuration; + + if (advance + delay >= maxAllowed) { + $A.modalError('提前和延后时间设置存在重叠,最大提前+延后时间不能超过 ' + (maxAllowed - 1) + ' 分钟', {language: false}); + return; + } + } + } this.systemSetting(true); } }) }, + timeToMinutes(timeStr) { + if (!timeStr) return 0; + const parts = timeStr.split(':'); + return parseInt(parts[0]) * 60 + parseInt(parts[1]); + }, + resetForm() { this.formData = $A.cloneJSON(this.formDatum_bak); },