101 lines
3.8 KiB
C#
101 lines
3.8 KiB
C#
using HikvisionAttendanceManager.App.Models;
|
|
|
|
namespace HikvisionAttendanceManager.App.Services;
|
|
|
|
public sealed class AttendanceSyncService(
|
|
HikvisionIsapiClient hikvision,
|
|
AttendanceLogRepository attendanceLog,
|
|
AttendanceMachineRepository machineRepository,
|
|
OperationHistoryService history)
|
|
{
|
|
public async Task<SyncHistoryEntry> SyncAsync(Device device, DateTime fromLocal, DateTime toLocal, bool useLastSyncCursor,
|
|
IProgress<AttendanceSyncProgress>? progress, CancellationToken cancellationToken)
|
|
{
|
|
var entry = new SyncHistoryEntry
|
|
{
|
|
Operation = SyncOperationKind.AttendanceSync,
|
|
SourceDevice = device.DisplayName,
|
|
Context = $"{fromLocal:dd-MMM-yyyy HH:mm} → {toLocal:dd-MMM-yyyy HH:mm}",
|
|
Status = "Running"
|
|
};
|
|
await history.AddAsync(entry, cancellationToken);
|
|
|
|
var fetched = 0;
|
|
var inserted = 0;
|
|
var skipped = 0;
|
|
var failed = 0;
|
|
DateTime? maxEventTime = null;
|
|
var allInsertsSucceeded = true;
|
|
|
|
try
|
|
{
|
|
if (useLastSyncCursor)
|
|
{
|
|
var cursor = await machineRepository.GetLastSyncDateAsync(device.IpAddress, cancellationToken);
|
|
if (cursor.HasValue && cursor.Value > fromLocal)
|
|
fromLocal = cursor.Value;
|
|
}
|
|
|
|
progress?.Report(new AttendanceSyncProgress(0, 0, 0, 0, "Fetching attendance records from device…"));
|
|
var punches = await hikvision.FetchAcsEventsAsync(device, fromLocal, toLocal, cancellationToken);
|
|
fetched = punches.Count;
|
|
|
|
for (var index = 0; index < punches.Count; index++)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
var punch = punches[index];
|
|
progress?.Report(new AttendanceSyncProgress(fetched, inserted, skipped, failed,
|
|
$"Processing {index + 1}/{punches.Count}: {punch.EmployeeNo}"));
|
|
|
|
try
|
|
{
|
|
if (await attendanceLog.ExistsAsync(punch.EmployeeNo, punch.CheckTime, device.MachineId, cancellationToken))
|
|
{
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
await attendanceLog.InsertAsync(punch.EmployeeNo, punch.CheckTime, device.MachineId, punch.InOutTypeId, device.IpAddress, cancellationToken);
|
|
inserted++;
|
|
if (!maxEventTime.HasValue || punch.CheckTime > maxEventTime.Value)
|
|
maxEventTime = punch.CheckTime;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
failed++;
|
|
allInsertsSucceeded = false;
|
|
AppLogger.Warning($"Attendance insert failed for {punch.EmployeeNo}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
if (allInsertsSucceeded)
|
|
{
|
|
var cursorTime = maxEventTime ?? toLocal;
|
|
await machineRepository.UpdateLastSyncDateAsync(device.IpAddress, cursorTime, cancellationToken);
|
|
}
|
|
|
|
entry.CompletedAt = DateTime.Now;
|
|
entry.Total = fetched;
|
|
entry.Success = inserted;
|
|
entry.Skipped = skipped;
|
|
entry.Failed = failed;
|
|
entry.Status = failed > 0 ? "Completed with errors" : "Completed";
|
|
await history.UpdateAsync(entry, cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
entry.CompletedAt = DateTime.Now;
|
|
entry.Total = fetched;
|
|
entry.Success = inserted;
|
|
entry.Skipped = skipped;
|
|
entry.Failed = failed + 1;
|
|
entry.Status = "Failed";
|
|
entry.Context += " — " + ex.Message;
|
|
await history.UpdateAsync(entry, cancellationToken);
|
|
throw;
|
|
}
|
|
|
|
return entry;
|
|
}
|
|
}
|