mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-25 11:58:12 +00:00
feat: 支持跨天打卡和时间重叠验证
- 允许签到"最晚可延后"时间超过 23:59:59,支持员工凌晨下班打卡 - 凌晨打卡记录自动归属前一天 - 前后端新增提前/延后时间重叠验证,防止产生歧义时间窗口 - 优化导出逻辑以正确处理跨天打卡记录 - 打卡消息提示归属日期信息
This commit is contained in:
parent
d4547cbe97
commit
6bdefc4f03
@ -456,6 +456,24 @@ class SystemController extends AbstractController
|
|||||||
if ($all['modes']) {
|
if ($all['modes']) {
|
||||||
$all['modes'] = array_intersect($all['modes'], ['auto', 'manual', 'locat', 'face']);
|
$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));
|
$setting = Base::setting('checkinSetting', Base::newTrim($all));
|
||||||
} else {
|
} else {
|
||||||
$setting = Base::setting('checkinSetting');
|
$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");
|
$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");
|
$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');
|
$botUser = User::botGetOrCreate('system-msg');
|
||||||
if (empty($botUser)) {
|
if (empty($botUser)) {
|
||||||
@ -1279,7 +1299,7 @@ class SystemController extends AbstractController
|
|||||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
|
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
|
||||||
//
|
//
|
||||||
$doo = Doo::load();
|
$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);
|
Coroutine::sleep(1);
|
||||||
//
|
//
|
||||||
$headings = [];
|
$headings = [];
|
||||||
@ -1316,9 +1336,10 @@ class SystemController extends AbstractController
|
|||||||
$index++;
|
$index++;
|
||||||
$sameDate = date("Y-m-d", $startT);
|
$sameDate = date("Y-m-d", $startT);
|
||||||
$sameTimes = $recordTimes[$sameDate] ?? [];
|
$sameTimes = $recordTimes[$sameDate] ?? [];
|
||||||
$sameCollect = UserCheckinRecord::atCollect($sameDate, $sameTimes);
|
$sameCollect = UserCheckinRecord::atCollect($sameDate, $sameTimes, $time[0]);
|
||||||
$firstBetween = [Carbon::createFromTimestamp($startT), Carbon::createFromTimestamp($startT + $secondEnd - 1)];
|
$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();
|
$firstRecord = $sameCollect?->whereBetween("datetime", $firstBetween)->first();
|
||||||
$lastRecord = $sameCollect?->whereBetween("datetime", $lastBetween)->last();
|
$lastRecord = $sameCollect?->whereBetween("datetime", $lastBetween)->last();
|
||||||
$firstTimestamp = $firstRecord['timestamp'] ?: 0;
|
$firstTimestamp = $firstRecord['timestamp'] ?: 0;
|
||||||
|
|||||||
@ -352,16 +352,47 @@ class UserBot extends AbstractModel
|
|||||||
$advance = (intval($setting['advance']) ?: 120) * 60;
|
$advance = (intval($setting['advance']) ?: 120) * 60;
|
||||||
$delay = (intval($setting['delay']) ?: 120) * 60;
|
$delay = (intval($setting['delay']) ?: 120) * 60;
|
||||||
//
|
//
|
||||||
|
$currentTime = Timer::time();
|
||||||
$nowDate = date("Y-m-d");
|
$nowDate = date("Y-m-d");
|
||||||
$nowTime = date("H:i:s");
|
$nowTime = date("H:i:s");
|
||||||
|
$yesterdayDate = date("Y-m-d", strtotime("-1 day"));
|
||||||
//
|
//
|
||||||
|
// 今天的签到窗口
|
||||||
$timeStart = strtotime("{$nowDate} {$times[0]}");
|
$timeStart = strtotime("{$nowDate} {$times[0]}");
|
||||||
$timeEnd = strtotime("{$nowDate} {$times[1]}");
|
$timeEnd = strtotime("{$nowDate} {$times[1]}");
|
||||||
$timeAdvance = max($timeStart - $advance, strtotime($nowDate));
|
$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;
|
$errorTime = false;
|
||||||
if (Timer::time() < $timeAdvance || $timeDelay < Timer::time()) {
|
if (!$targetDate) {
|
||||||
$errorTime = "不在有效时间内,有效时间为:" . date("H:i", $timeAdvance) . "-" . date("H:i", $timeDelay);
|
$displayDelay = date("H:i", $todayTimeDelay % 86400);
|
||||||
|
$nextDay = ($todayTimeDelay > strtotime("{$nowDate} 23:59:59")) ? "(次日)" : "";
|
||||||
|
$errorTime = "不在有效时间内,有效时间为:" . date("H:i", $timeAdvance) . "-{$nextDay}{$displayDelay}";
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$macs = explode(",", $mac);
|
$macs = explode(",", $mac);
|
||||||
@ -375,7 +406,7 @@ class UserBot extends AbstractModel
|
|||||||
$array[] = [
|
$array[] = [
|
||||||
'userid' => $UserCheckinMac->userid,
|
'userid' => $UserCheckinMac->userid,
|
||||||
'mac' => $UserCheckinMac->mac,
|
'mac' => $UserCheckinMac->mac,
|
||||||
'date' => $nowDate,
|
'date' => $targetDate ?: $nowDate,
|
||||||
];
|
];
|
||||||
$checkins[] = [
|
$checkins[] = [
|
||||||
'userid' => $UserCheckinMac->userid,
|
'userid' => $UserCheckinMac->userid,
|
||||||
@ -396,7 +427,7 @@ class UserBot extends AbstractModel
|
|||||||
$array[] = [
|
$array[] = [
|
||||||
'userid' => $UserInfo->userid,
|
'userid' => $UserInfo->userid,
|
||||||
'mac' => '00:00:00:00:00:00',
|
'mac' => '00:00:00:00:00:00',
|
||||||
'date' => $nowDate,
|
'date' => $targetDate ?: $nowDate,
|
||||||
];
|
];
|
||||||
$checkins[] = [
|
$checkins[] = [
|
||||||
'userid' => $UserInfo->userid,
|
'userid' => $UserInfo->userid,
|
||||||
@ -431,7 +462,8 @@ class UserBot extends AbstractModel
|
|||||||
}
|
}
|
||||||
return null;
|
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']);
|
$dialog = WebSocketDialog::checkUserDialog($botUser, $checkin['userid']);
|
||||||
if (!$dialog) {
|
if (!$dialog) {
|
||||||
return;
|
return;
|
||||||
@ -448,12 +480,13 @@ class UserBot extends AbstractModel
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 判断已打卡
|
// 判断已打卡(使用目标日期作为缓存键)
|
||||||
$cacheKey = "Checkin::sendMsg-{$nowDate}-{$type}:" . $checkin['userid'];
|
$cacheKey = "Checkin::sendMsg-{$displayDate}-{$type}:" . $checkin['userid'];
|
||||||
$typeContent = $type == "up" ? "上班" : "下班";
|
$typeContent = $type == "up" ? "上班" : "下班";
|
||||||
if (Cache::get($cacheKey) === "yes") {
|
if (Cache::get($cacheKey) === "yes") {
|
||||||
if ($alreadyTip) {
|
if ($alreadyTip) {
|
||||||
$text = "今日已{$typeContent}打卡,无需重复打卡。";
|
$dateHint = ($displayDate != $nowDate) ? "({$displayDate})" : "今日";
|
||||||
|
$text = "{$dateHint}已{$typeContent}打卡,无需重复打卡。";
|
||||||
$text .= $checkin['remark'] ? " ({$checkin['remark']})": "";
|
$text .= $checkin['remark'] ? " ({$checkin['remark']})": "";
|
||||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||||
'type' => 'content',
|
'type' => 'content',
|
||||||
@ -467,7 +500,8 @@ class UserBot extends AbstractModel
|
|||||||
$hi = date("H:i");
|
$hi = date("H:i");
|
||||||
$remark = $checkin['remark'] ? " ({$checkin['remark']})": "";
|
$remark = $checkin['remark'] ? " ({$checkin['remark']})": "";
|
||||||
$subcontent = $getJokeSoup($type, $checkin['userid']);
|
$subcontent = $getJokeSoup($type, $checkin['userid']);
|
||||||
$title = "{$typeContent}打卡成功,打卡时间: {$hi}{$remark}";
|
$dateInfo = ($displayDate != $nowDate) ? "(记录归属 {$displayDate})" : "";
|
||||||
|
$title = "{$typeContent}打卡成功,打卡时间: {$hi}{$remark}{$dateInfo}";
|
||||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||||
'type' => 'content',
|
'type' => 'content',
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
@ -482,14 +516,13 @@ class UserBot extends AbstractModel
|
|||||||
],
|
],
|
||||||
], $botUser->userid, false, false, $type != "up");
|
], $botUser->userid, false, false, $type != "up");
|
||||||
};
|
};
|
||||||
if ($timeAdvance <= Timer::time() && Timer::time() < $timeEnd) {
|
// 根据打卡类型发送通知
|
||||||
// 上班打卡通知(从最早打卡时间 到 下班打卡时间)
|
if ($checkType === 'up') {
|
||||||
foreach ($checkins as $checkin) {
|
foreach ($checkins as $checkin) {
|
||||||
$sendMsg('up', $checkin);
|
$sendMsg('up', $checkin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($timeEnd <= Timer::time() && Timer::time() <= $timeDelay) {
|
if ($checkType === 'down') {
|
||||||
// 下班打卡通知(下班打卡时间 到 最晚打卡时间)
|
|
||||||
foreach ($checkins as $checkin) {
|
foreach ($checkins as $checkin) {
|
||||||
$sendMsg('down', $checkin);
|
$sendMsg('down', $checkin);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,16 +88,32 @@ class UserCheckinRecord extends AbstractModel
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 时间收集
|
* 时间收集
|
||||||
* @param string $data
|
* @param string $data 日期
|
||||||
* @param array $times
|
* @param array $times 签到时间数组
|
||||||
|
* @param string|null $shiftStart 班次开始时间(如 "09:00"),用于判断跨天
|
||||||
* @return \Illuminate\Support\Collection
|
* @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 [
|
return [
|
||||||
"datetime" => "{$data} {$time}",
|
"datetime" => "{$targetDate} {$time}",
|
||||||
"timestamp" => strtotime("{$data} {$time}")
|
"timestamp" => strtotime("{$targetDate} {$time}")
|
||||||
];
|
];
|
||||||
}, $times);
|
}, $times);
|
||||||
return collect($sameTimes);
|
return collect($sameTimes);
|
||||||
|
|||||||
@ -411,11 +411,36 @@ export default {
|
|||||||
submitForm() {
|
submitForm() {
|
||||||
this.$refs.formData.validate((valid) => {
|
this.$refs.formData.validate((valid) => {
|
||||||
if (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);
|
this.systemSetting(true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
timeToMinutes(timeStr) {
|
||||||
|
if (!timeStr) return 0;
|
||||||
|
const parts = timeStr.split(':');
|
||||||
|
return parseInt(parts[0]) * 60 + parseInt(parts[1]);
|
||||||
|
},
|
||||||
|
|
||||||
resetForm() {
|
resetForm() {
|
||||||
this.formData = $A.cloneJSON(this.formDatum_bak);
|
this.formData = $A.cloneJSON(this.formDatum_bak);
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user