fix: refactor ZKTeco API receiver to group by day and use correct AttendanceRecord table, and limit agent to last 15 days

This commit is contained in:
2026-05-19 14:17:48 +08:00
parent cfb183f8f7
commit 5696c98bb5
4 changed files with 94 additions and 59 deletions

Binary file not shown.

View File

@@ -46,60 +46,84 @@ class AttendanceSyncController extends Controller
return response()->json(['error' => 'Branch not found'], 404);
}
// 3. Process the logs (same logic as BiometricAttendanceController)
// 3. Process the logs (match BiometricAttendanceController logic)
// Ensure we handle arrays with 'recordTime' or 'timestamp'
$formattedAttendances = [];
foreach ($attendances as $record) {
$empCode = $record['deviceUserId'] ?? $record['id'] ?? null;
$punchTime = $record['recordTime'] ?? $record['timestamp'] ?? null;
$uid = $record['userSn'] ?? $record['uid'] ?? null;
if ($empCode && $punchTime) {
$formattedAttendances[] = [
'id' => $uid,
'emp_code' => (string) $empCode,
'punch_time' => $punchTime,
'branch_id' => $branch->id
];
}
}
$groupedAttendances = collect($formattedAttendances)->groupBy(function ($item) {
return $item['emp_code'] . '_' . date('Y-m-d', strtotime($item['punch_time']));
});
$processed = 0;
$inserted = 0;
foreach ($attendances as $record) {
foreach ($groupedAttendances as $key => $dayEntries) {
$processed++;
// Typical payload from zklib-js: { userSn: 1, deviceUserId: '1', recordTime: '2023-10-14 08:30:00' }
$empCode = $record['deviceUserId'] ?? $record['id'] ?? null;
$punchTime = $record['recordTime'] ?? $record['timestamp'] ?? null;
if (!$empCode || !$punchTime) {
continue;
}
// Find employee by emp_code inside this branch's company
$employee = \App\Models\Employee::where('emp_code', $empCode)
$sorted = $dayEntries->sortBy('punch_time');
$firstEntry = $sorted->first();
$lastEntry = $sorted->last();
// Note: Since this is an API, there is no Auth::user(). We use the Branch's creator.
// BiometricAttendanceController uses whereIn('created_by', getCompanyAndUsersId())
// We just use where('created_by', $branch->created_by) or similar.
$employee = \App\Models\Employee::with('user')
->where('created_by', $branch->created_by)
->where('biometric_emp_id', $firstEntry['emp_code'])
->first();
if (!$employee) {
continue;
}
// Parse date and time
$ts = strtotime($punchTime);
$date = date('Y-m-d', $ts);
$time = date('H:i:s', $ts);
// Check if record already exists to prevent duplicates
$exists = AttendanceEmployee::where('employee_id', $employee->id)
->where('date', $date)
->where('clock_in', $time)
->exists();
if (!$exists) {
AttendanceEmployee::create([
'employee_id' => $employee->id,
'date' => $date,
'status' => 'Present',
'clock_in' => $time,
'clock_out' => '00:00:00',
'late' => '00:00:00',
'early_leaving' => '00:00:00',
'overtime' => '00:00:00',
'total_rest' => '00:00:00',
'created_by' => $branch->created_by,
]);
$inserted++;
if ($employee && $sorted->count() > 1) {
$attedanceDate = date('Y-m-d', strtotime($firstEntry['punch_time']));
$clockInTime = date('H:i:s', strtotime($firstEntry['punch_time']));
$clockOutTime = date('H:i:s', strtotime($lastEntry['punch_time']));
$exists = \App\Models\AttendanceRecord::where('employee_id', $employee->user_id)
->where('date', $attedanceDate)
->exists();
if (!$exists) {
$shift = \App\Models\Shift::where('id', $employee->shift_id)->where('status', 'active')->first()
?? \App\Models\Shift::where('created_by', $branch->created_by)->where('status', 'active')->first();
$policy = \App\Models\AttendancePolicy::where('id', $employee->attendance_policy_id)->where('status', 'active')->first()
?? \App\Models\AttendancePolicy::where('created_by', $branch->created_by)->where('status', 'active')->first();
$attendance = new \App\Models\AttendanceRecord();
$attendance->employee_id = $employee->user_id;
$attendance->biometric_id = $firstEntry['id'];
$attendance->shift_id = $shift?->id;
$attendance->attendance_policy_id = $policy?->id;
$attendance->date = $attedanceDate;
$attendance->clock_in = $clockInTime;
$attendance->clock_out = $clockOutTime;
$attendance->created_by = $branch->created_by;
$attendance->save();
$attendance->fresh();
$attendance->processAttendance();
$inserted++;
}
}
}
return response()->json([
'success' => true,
'message' => "Processed {$processed} records, Inserted {$inserted} new attendances.",
'message' => "Processed {$processed} unique daily records, Inserted {$inserted} new attendances.",
]);
}
}

Binary file not shown.

View File

@@ -15,33 +15,38 @@ async function syncAttendance() {
const zkInstance = new ZKLib(ZKTECO_IP, ZKTECO_PORT, 10000, 4000);
try {
// Connect to the device
await zkInstance.createSocket();
console.log('✅ Connected to ZKTeco device.');
// Get Attendance records
console.log('Fetching attendance records...');
const attendances = await zkInstance.getAttendances();
if (attendances && attendances.data && attendances.data.length > 0) {
console.log(`✅ Found ${attendances.data.length} records.`);
console.log(`✅ Found total ${attendances.data.length} records on device.`);
// Push to Server
console.log(`Pushing to Cloud API: ${SERVER_API_URL}`);
const response = await axios.post(SERVER_API_URL, {
api_key: API_KEY,
branch_id: BRANCH_ID,
zkteco_ip: ZKTECO_IP,
attendances: attendances.data
// Filter to last 15 days
const fifteenDaysAgo = new Date();
fifteenDaysAgo.setDate(fifteenDaysAgo.getDate() - 15);
const recentLogs = attendances.data.filter(log => {
const logDate = new Date(log.recordTime);
return logDate >= fifteenDaysAgo;
});
console.log('✅ Sync Successful:', response.data);
// Optional: Clear attendance on device to avoid re-sending old logs
// await zkInstance.clearAttendanceLog();
// console.log('✅ Cleared attendance logs from device.');
console.log(`✅ Filtered to ${recentLogs.length} records from the last 15 days.`);
if (recentLogs.length > 0) {
console.log(`Pushing to Cloud API: ${SERVER_API_URL}`);
const response = await axios.post(SERVER_API_URL, {
api_key: API_KEY,
branch_id: BRANCH_ID,
zkteco_ip: ZKTECO_IP,
attendances: recentLogs
});
console.log('✅ Sync Successful:', response.data);
}
} else {
console.log(' No new attendance records found.');
console.log(' No attendance records found on device.');
}
} catch (e) {
@@ -54,6 +59,12 @@ async function syncAttendance() {
await zkInstance.disconnect();
console.log('Disconnected from ZKTeco device.');
} catch (err) {}
// Keep window open for user to read logs
console.log('\nPress any key to exit...');
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on('data', process.exit.bind(process, 0));
}
}