131 lines
4.9 KiB
PHP
131 lines
4.9 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\Request;
|
|
use App\Models\Branch;
|
|
use App\Models\AttendanceEmployee;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class AttendanceSyncController extends Controller
|
|
{
|
|
public function sync(Request $request)
|
|
{
|
|
// 1. Verify API Key
|
|
$apiKey = $request->header('X-API-Key') ?? $request->input('api_key');
|
|
// You should store a master API key in your .env like ZKTECO_SYNC_KEY=xxx
|
|
$validKey = env('ZKTECO_SYNC_KEY', 'default_secure_sync_key_123');
|
|
|
|
if ($apiKey !== $validKey) {
|
|
Log::warning('Unauthorized ZKTeco sync attempt', ['ip' => $request->ip()]);
|
|
return response()->json(['error' => 'Unauthorized'], 401);
|
|
}
|
|
|
|
$branchId = $request->input('branch_id');
|
|
$zktecoIp = $request->input('zkteco_ip');
|
|
$attendances = $request->input('attendances', []);
|
|
|
|
if (empty($attendances)) {
|
|
return response()->json(['message' => 'No attendance records provided'], 400);
|
|
}
|
|
|
|
// 2. Find the branch
|
|
$branchQuery = Branch::query();
|
|
if ($branchId) {
|
|
$branchQuery->where('id', $branchId);
|
|
} elseif ($zktecoIp) {
|
|
$branchQuery->where('zkteco_ip', $zktecoIp);
|
|
} else {
|
|
return response()->json(['error' => 'Must provide branch_id or zkteco_ip'], 400);
|
|
}
|
|
|
|
$branch = $branchQuery->first();
|
|
|
|
if (!$branch) {
|
|
return response()->json(['error' => 'Branch not found'], 404);
|
|
}
|
|
|
|
// 3. Process the logs into a standardized format for the dashboard
|
|
$formattedAttendances = [];
|
|
foreach ($attendances as $record) {
|
|
$empCode = $record['deviceUserId'] ?? $record['id'] ?? null;
|
|
$rawPunchTime = $record['recordTime'] ?? $record['timestamp'] ?? null;
|
|
$punchTime = $this->parseDateSafe($rawPunchTime);
|
|
$uid = $record['userSn'] ?? $record['uid'] ?? null;
|
|
$type = $record['state'] ?? $record['type'] ?? 0;
|
|
|
|
if ($empCode && $punchTime) {
|
|
// We format it identically to how ZKTeco hardware outputs `$raw_attendance`
|
|
// so BiometricAttendanceController can process it natively.
|
|
$formattedAttendances[] = [
|
|
'uid' => $uid,
|
|
'id' => (string) $empCode,
|
|
'timestamp' => $punchTime->toDateTimeString(), // Clean Y-m-d H:i:s format
|
|
'type' => $type
|
|
];
|
|
}
|
|
}
|
|
|
|
// Save the formatted records to a JSON file cache for the branch
|
|
if (!empty($formattedAttendances)) {
|
|
$filePath = storage_path("app/biometric_cache_branch_{$branch->id}.json");
|
|
|
|
// If the file exists, we merge the new records with the old ones, keeping only unique punches
|
|
if (file_exists($filePath)) {
|
|
$existing = json_decode(file_get_contents($filePath), true) ?? [];
|
|
$merged = array_merge($existing, $formattedAttendances);
|
|
|
|
// Remove exact duplicates by using a unique key
|
|
$unique = collect($merged)->unique(function ($item) {
|
|
return $item['id'] . '_' . $item['timestamp'];
|
|
})->values()->toArray();
|
|
|
|
// Keep only the last 3000 records to prevent file bloat
|
|
if (count($unique) > 3000) {
|
|
$unique = array_slice($unique, -3000);
|
|
}
|
|
|
|
file_put_contents($filePath, json_encode($unique));
|
|
} else {
|
|
file_put_contents($filePath, json_encode($formattedAttendances));
|
|
}
|
|
}
|
|
|
|
$processed = count($formattedAttendances);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => "Successfully pushed {$processed} records to the Biometric Attendance waiting room for Branch {$branch->id}.",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Safely parse various date formats coming from ZKTeco devices or JS agents.
|
|
*/
|
|
private function parseDateSafe($dateStr)
|
|
{
|
|
if (!$dateStr) {
|
|
return null;
|
|
}
|
|
|
|
// Handle numeric timestamps (Unix seconds or milliseconds)
|
|
if (is_numeric($dateStr)) {
|
|
if (strlen((string)$dateStr) > 10) {
|
|
// It's likely in milliseconds
|
|
return \Carbon\Carbon::createFromTimestampMs($dateStr);
|
|
}
|
|
return \Carbon\Carbon::createFromTimestamp($dateStr);
|
|
}
|
|
|
|
// Remove JS-specific timezone names like "(China Standard Time)" to prevent Carbon parse errors
|
|
$cleanDateStr = trim(explode('(', $dateStr)[0]);
|
|
|
|
try {
|
|
return \Carbon\Carbon::parse($cleanDateStr);
|
|
} catch (\Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|