feat(sync): update user sync engine and departmental sync for face outcomes
Wires the sync engine and departmental sync to the new result model and face-enrollment verification behavior.main
parent
f1e20ddf0b
commit
c4bd561811
|
|
@ -11,6 +11,8 @@ public sealed class DepartmentalUserSyncService(
|
||||||
IProgress<UserSyncProgress>? progress, CancellationToken cancellationToken)
|
IProgress<UserSyncProgress>? progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var list = employees.ToList();
|
var list = employees.ToList();
|
||||||
|
AppLogger.TraceEnter("USER_SYNC", nameof(SyncAsync),
|
||||||
|
$"device={device.IpAddress} count={list.Count} context={contextSummary}");
|
||||||
var entry = new SyncHistoryEntry
|
var entry = new SyncHistoryEntry
|
||||||
{
|
{
|
||||||
Operation = SyncOperationKind.DepartmentalUserSync,
|
Operation = SyncOperationKind.DepartmentalUserSync,
|
||||||
|
|
@ -24,10 +26,12 @@ public sealed class DepartmentalUserSyncService(
|
||||||
var results = await engine.RunAsync(device, list, initialEnrollment: true, progress, cancellationToken);
|
var results = await engine.RunAsync(device, list, initialEnrollment: true, progress, cancellationToken);
|
||||||
entry.CompletedAt = DateTime.Now;
|
entry.CompletedAt = DateTime.Now;
|
||||||
entry.Success = results.Count(r => r.OverallResult is "SUCCESS" or "ALREADY_EXISTS");
|
entry.Success = results.Count(r => r.OverallResult is "SUCCESS" or "ALREADY_EXISTS");
|
||||||
entry.Failed = results.Count(r => r.OverallResult == "FAILED");
|
entry.Failed = results.Count(r => r.OverallResult is "FAILED" or "PARTIAL");
|
||||||
entry.Skipped = results.Count(r => r.OverallResult == "SKIPPED");
|
entry.Skipped = results.Count(r => r.OverallResult == "SKIPPED");
|
||||||
entry.Status = entry.Failed > 0 ? "Completed with errors" : "Completed";
|
entry.Status = entry.Failed > 0 ? "Completed with errors" : "Completed";
|
||||||
await history.UpdateAsync(entry, cancellationToken);
|
await history.UpdateAsync(entry, cancellationToken);
|
||||||
|
AppLogger.TraceExit("USER_SYNC", nameof(SyncAsync),
|
||||||
|
$"success={entry.Success} failed={entry.Failed} skipped={entry.Skipped}");
|
||||||
return (entry, results);
|
return (entry, results);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,47 +7,103 @@ public sealed class UserSyncEngine(HikvisionIsapiClient hikvision, EmployeePhoto
|
||||||
public async Task<IReadOnlyList<EmployeeSyncResult>> RunAsync(Device device, IEnumerable<HrmsEmployee> employees, bool initialEnrollment,
|
public async Task<IReadOnlyList<EmployeeSyncResult>> RunAsync(Device device, IEnumerable<HrmsEmployee> employees, bool initialEnrollment,
|
||||||
IProgress<UserSyncProgress>? progress, CancellationToken cancellationToken)
|
IProgress<UserSyncProgress>? progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var results = new List<EmployeeSyncResult>();
|
|
||||||
var source = employees.ToList();
|
var source = employees.ToList();
|
||||||
|
AppLogger.TraceEnter("USER_SYNC", nameof(RunAsync),
|
||||||
|
$"device={device.IpAddress} count={source.Count} initialEnrollment={initialEnrollment}");
|
||||||
|
var results = new List<EmployeeSyncResult>();
|
||||||
for (var index = 0; index < source.Count; index++)
|
for (var index = 0; index < source.Count; index++)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
var employee = source[index];
|
var employee = source[index];
|
||||||
|
AppLogger.TraceEnter("USER_SYNC", "ProcessEmployee", $"employee={employee.SerialNumber} index={index + 1}/{source.Count}");
|
||||||
progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Creating user…", results));
|
progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Creating user…", results));
|
||||||
var result = new EmployeeSyncResult { Employee = employee, EmployeeNumber = employee.SerialNumber, EmployeeName = employee.Name, Department = employee.DepartmentName };
|
var result = new EmployeeSyncResult { Employee = employee, EmployeeNumber = employee.SerialNumber, EmployeeName = employee.Name, Department = employee.DepartmentName };
|
||||||
|
var userOnDevice = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (await hikvision.UserExistsAsync(device, employee.SerialNumber, cancellationToken))
|
var exists = await hikvision.UserExistsAsync(device, employee.SerialNumber, cancellationToken);
|
||||||
|
if (exists)
|
||||||
|
{
|
||||||
|
userOnDevice = true;
|
||||||
|
result.UserCreated = true;
|
||||||
result.UserStatus = "Already exists";
|
result.UserStatus = "Already exists";
|
||||||
|
if (initialEnrollment)
|
||||||
|
{
|
||||||
|
var existingFaces = await hikvision.TryGetUserFaceCountAsync(device, employee.SerialNumber, cancellationToken);
|
||||||
|
if (existingFaces is >= 1)
|
||||||
|
{
|
||||||
|
result.FaceStatus = "Already enrolled";
|
||||||
|
result.VerificationStatus = "Verified";
|
||||||
|
result.FaceEnrolled = true;
|
||||||
|
result.OverallResult = "ALREADY_EXISTS";
|
||||||
|
results.Add(result);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var user = await hikvision.CreateUserAsync(device, employee, cancellationToken);
|
var user = await hikvision.CreateUserAsync(device, employee, cancellationToken);
|
||||||
if (!user.Success) { result.UserStatus = "Failed"; result.OverallResult = "FAILED"; result.Reason = user.Reason; results.Add(result); continue; }
|
if (!user.Success)
|
||||||
|
{
|
||||||
|
result.UserStatus = "Failed";
|
||||||
|
result.OverallResult = "FAILED";
|
||||||
|
result.Reason = user.Reason;
|
||||||
|
results.Add(result);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
userOnDevice = true;
|
||||||
|
result.UserCreated = true;
|
||||||
result.UserStatus = "Created";
|
result.UserStatus = "Created";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!initialEnrollment) { result.OverallResult = result.UserStatus == "Already exists" ? "ALREADY_EXISTS" : "SUCCESS"; results.Add(result); continue; }
|
if (!initialEnrollment)
|
||||||
|
{
|
||||||
|
result.OverallResult = result.UserStatus == "Already exists" ? "ALREADY_EXISTS" : "SUCCESS";
|
||||||
|
results.Add(result);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Downloading employee photo…", results));
|
progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Downloading employee photo…", results));
|
||||||
var photo = await photos.DownloadAndNormalizeAsync(employee, cancellationToken);
|
var photo = await photos.DownloadAndNormalizeAsync(employee, cancellationToken);
|
||||||
if (!photo.Success) { result.FaceStatus = "Failed"; result.OverallResult = "FAILED"; result.Reason = photo.Reason; results.Add(result); continue; }
|
if (!photo.Success)
|
||||||
|
{
|
||||||
|
FaceEnrollmentResults.Apply(result, userOnDevice, false, "Failed", "Not requested", photo.Reason);
|
||||||
|
results.Add(result);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Uploading face…", results));
|
progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Uploading face…", results));
|
||||||
var face = await hikvision.UploadFaceAsync(device, employee.SerialNumber, photo.Bytes, cancellationToken);
|
var face = await hikvision.UploadFaceAsync(device, employee.SerialNumber, photo.Bytes, cancellationToken);
|
||||||
if (!face.Success) { result.FaceStatus = "Failed"; result.OverallResult = "FAILED"; result.Reason = face.Reason; results.Add(result); continue; }
|
if (!face.Success)
|
||||||
|
{
|
||||||
|
FaceEnrollmentResults.Apply(result, userOnDevice, false, "Failed", "Not requested", face.Reason);
|
||||||
|
results.Add(result);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
result.FaceStatus = "Uploaded";
|
result.FaceStatus = "Uploaded";
|
||||||
|
|
||||||
progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Verifying face enrollment…", results));
|
progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Verifying face enrollment…", results));
|
||||||
var verification = await hikvision.VerifyFaceAsync(device, employee.SerialNumber, cancellationToken);
|
var verification = await hikvision.VerifyFaceAsync(device, employee.SerialNumber, cancellationToken);
|
||||||
result.VerificationStatus = verification.Success ? "Verified" : "Failed";
|
FaceEnrollmentResults.Apply(result, userOnDevice, verification.Success, result.FaceStatus,
|
||||||
result.OverallResult = verification.Success ? "SUCCESS" : "FAILED";
|
verification.Success ? "Verified" : "Failed", verification.Reason);
|
||||||
result.Reason = verification.Reason;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
result.OverallResult = "FAILED";
|
FaceEnrollmentResults.Apply(result, userOnDevice, false, result.FaceStatus, result.VerificationStatus,
|
||||||
result.Reason = ex is InvalidOperationException ? ex.Message : "Device unavailable or operation failed.";
|
ex is InvalidOperationException ? ex.Message : "Device unavailable or operation failed.");
|
||||||
|
if (!userOnDevice)
|
||||||
|
result.OverallResult = "FAILED";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppLogger.TraceExit("USER_SYNC", "ProcessEmployee",
|
||||||
|
$"employee={employee.SerialNumber} result={result.OverallResult}");
|
||||||
results.Add(result);
|
results.Add(result);
|
||||||
}
|
}
|
||||||
progress?.Report(CreateProgress(source.Count, source.Count, "", "Completed", results));
|
progress?.Report(CreateProgress(source.Count, source.Count, "", "Completed", results));
|
||||||
|
AppLogger.TraceExit("USER_SYNC", nameof(RunAsync),
|
||||||
|
$"success={results.Count(r => r.OverallResult is "SUCCESS" or "ALREADY_EXISTS")} partial={results.Count(r => r.OverallResult == "PARTIAL")} failed={results.Count(r => r.OverallResult == "FAILED")}");
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,5 +111,39 @@ public sealed class UserSyncEngine(HikvisionIsapiClient hikvision, EmployeePhoto
|
||||||
new(completed, total, employee, operation,
|
new(completed, total, employee, operation,
|
||||||
results.Count(r => r.UserStatus == "Created"), results.Count(r => r.UserStatus == "Already exists"),
|
results.Count(r => r.UserStatus == "Created"), results.Count(r => r.UserStatus == "Already exists"),
|
||||||
results.Count(r => r.FaceStatus == "Uploaded"), results.Count(r => r.FaceStatus == "Failed"),
|
results.Count(r => r.FaceStatus == "Uploaded"), results.Count(r => r.FaceStatus == "Failed"),
|
||||||
results.Count(r => r.OverallResult == "SKIPPED"), results.Count(r => r.OverallResult == "FAILED"));
|
results.Count(r => r.OverallResult == "SKIPPED"), results.Count(r => r.OverallResult is "FAILED" or "PARTIAL"));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class FaceEnrollmentResults
|
||||||
|
{
|
||||||
|
internal static void Apply(
|
||||||
|
EmployeeSyncResult result,
|
||||||
|
bool userOnDevice,
|
||||||
|
bool faceEnrolled,
|
||||||
|
string faceStatus,
|
||||||
|
string verificationStatus,
|
||||||
|
string? failureReason)
|
||||||
|
{
|
||||||
|
result.UserCreated = userOnDevice;
|
||||||
|
result.FaceEnrolled = faceEnrolled;
|
||||||
|
result.FaceStatus = faceStatus;
|
||||||
|
result.VerificationStatus = verificationStatus;
|
||||||
|
|
||||||
|
if (faceEnrolled)
|
||||||
|
{
|
||||||
|
result.OverallResult = "SUCCESS";
|
||||||
|
result.Reason = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userOnDevice)
|
||||||
|
{
|
||||||
|
result.OverallResult = "PARTIAL";
|
||||||
|
result.Reason = string.IsNullOrWhiteSpace(failureReason) ? "Face enrollment failed." : failureReason;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.OverallResult = "FAILED";
|
||||||
|
result.Reason = string.IsNullOrWhiteSpace(failureReason) ? "User creation failed." : failureReason;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue