Implement device-to-device template sync service flow

Add the service logic for copying users and face templates from one Hikvision device to another.

- Rename DbToDevice sync method to DeviceToDevice
- Support source-device face download when source is connected
- Fall back to stored DB face templates when source is unavailable
- Register transferred users on the target machine
- Record Device → Device operation history
main
SYED MUSTUFA AHMED NAQVI 2026-08-28 12:45:18 +05:00
parent ab084ebb50
commit 78b8a9bc86
1 changed files with 64 additions and 36 deletions

View File

@ -72,22 +72,22 @@ public sealed class TemplateSyncService(
return entry; return entry;
} }
public async Task<SyncHistoryEntry> DbToDeviceAsync(Device source, Device target, IReadOnlyList<string> serialNumbers, public async Task<SyncHistoryEntry> DeviceToDeviceAsync(Device source, Device target, IReadOnlyList<DeviceTransferUser> users,
IProgress<UserSyncProgress>? progress, CancellationToken cancellationToken) bool sourceConnected, IProgress<UserSyncProgress>? progress, CancellationToken cancellationToken)
{ {
AppLogger.TraceEnter("TEMPLATE_SYNC", nameof(DbToDeviceAsync), AppLogger.TraceEnter("TEMPLATE_SYNC", nameof(DeviceToDeviceAsync),
$"source={source.IpAddress} target={target.IpAddress} count={serialNumbers.Count}"); $"source={source.IpAddress} target={target.IpAddress} count={users.Count} sourceConnected={sourceConnected}");
if (string.Equals(source.MachineId, target.MachineId, StringComparison.OrdinalIgnoreCase) && if (string.Equals(source.MachineId, target.MachineId, StringComparison.OrdinalIgnoreCase) &&
string.Equals(source.IpAddress, target.IpAddress, StringComparison.OrdinalIgnoreCase)) string.Equals(source.IpAddress, target.IpAddress, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Source and target devices must be different."); throw new InvalidOperationException("Source and target devices must be different.");
var entry = new SyncHistoryEntry var entry = new SyncHistoryEntry
{ {
Operation = SyncOperationKind.DbToDevice, Operation = SyncOperationKind.DeviceToDevice,
SourceDevice = source.DisplayName, SourceDevice = source.DisplayName,
TargetDevice = target.DisplayName, TargetDevice = target.DisplayName,
Context = $"{serialNumbers.Count} user(s)", Context = $"{users.Count} user(s)",
Total = serialNumbers.Count, Total = users.Count,
Status = "Running" Status = "Running"
}; };
await history.AddAsync(entry, cancellationToken); await history.AddAsync(entry, cancellationToken);
@ -96,18 +96,18 @@ public sealed class TemplateSyncService(
var faces = 0; var faces = 0;
var skipped = 0; var skipped = 0;
var failed = 0; var failed = 0;
var registered = 0;
for (var index = 0; index < serialNumbers.Count; index++) for (var index = 0; index < users.Count; index++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var serial = serialNumbers[index]; var user = users[index];
progress?.Report(new UserSyncProgress(index, serialNumbers.Count, serial, "Creating user on target…", created, 0, faces, 0, skipped, failed)); var serial = user.SerialNumber;
var name = user.EmployeeName;
progress?.Report(new UserSyncProgress(index, users.Count, serial, "Creating user on target…", created, 0, faces, 0, skipped, failed));
try try
{ {
var dbUsers = await machineUsers.GetActiveByMachineAsync(source.MachineId, cancellationToken); var userCreatedNow = false;
var dbUser = dbUsers.FirstOrDefault(u => string.Equals(u.SerialNumber, serial, StringComparison.OrdinalIgnoreCase));
var name = dbUser?.EmployeeName ?? serial;
if (!await hikvision.UserExistsAsync(target, serial, cancellationToken)) if (!await hikvision.UserExistsAsync(target, serial, cancellationToken))
{ {
var create = await hikvision.CreateUserAsync(target, serial, name, cancellationToken); var create = await hikvision.CreateUserAsync(target, serial, name, cancellationToken);
@ -116,49 +116,77 @@ public sealed class TemplateSyncService(
failed++; failed++;
continue; continue;
} }
created++; created++;
userCreatedNow = true;
} }
var template = await faceTemplates.TryGetActiveAsync(serial, cancellationToken); byte[]? faceBytes = null;
if (template?.Template is null or { Length: 0 }) if (sourceConnected)
{ {
skipped++; progress?.Report(new UserSyncProgress(index, users.Count, serial, "Downloading face from source device…", created, 0, faces, 0, skipped, failed));
continue; var download = await hikvision.DownloadFaceTemplateAsync(source, serial, cancellationToken);
if (download.Success && HikvisionIsapiClient.IsUploadableFaceTemplate(download.Bytes))
faceBytes = download.Bytes;
}
else
{
var template = await faceTemplates.TryGetActiveAsync(serial, cancellationToken);
if (template?.Template is { Length: > 0 } && HikvisionIsapiClient.IsUploadableFaceTemplate(template.Template))
faceBytes = template.Template;
} }
if (!HikvisionIsapiClient.IsUploadableFaceTemplate(template.Template)) if (faceBytes is null)
{ {
skipped++;
AppLogger.Info($"DB→Device skipped non-JPEG template for {serial}.");
continue;
}
progress?.Report(new UserSyncProgress(index, serialNumbers.Count, serial, "Uploading face template…", created, 0, faces, 0, skipped, failed));
var upload = await hikvision.UploadFaceAsync(target, serial, template.Template, cancellationToken);
if (upload.Success)
{
faces++;
await machineUsers.UpsertAsync(target.MachineId, serial, name, cancellationToken); await machineUsers.UpsertAsync(target.MachineId, serial, name, cancellationToken);
registered++;
skipped++;
AppLogger.Info($"Device→Device skipped face for {serial} (no uploadable template from {(sourceConnected ? "source device" : "DB")}).");
continue;
} }
else failed++;
progress?.Report(new UserSyncProgress(index, users.Count, serial, "Uploading face to target…", created, 0, faces, 0, skipped, failed));
var upload = await hikvision.UploadFaceAsync(target, serial, faceBytes, cancellationToken);
if (!upload.Success)
{
if (userCreatedNow)
await machineUsers.UpsertAsync(target.MachineId, serial, name, cancellationToken);
failed++;
continue;
}
progress?.Report(new UserSyncProgress(index, users.Count, serial, "Verifying face enrollment…", created, 0, faces, 0, skipped, failed));
var verify = await hikvision.VerifyFaceAsync(target, serial, cancellationToken);
if (!verify.Success)
{
if (userCreatedNow)
await machineUsers.UpsertAsync(target.MachineId, serial, name, cancellationToken);
failed++;
continue;
}
faces++;
registered++;
await machineUsers.UpsertAsync(target.MachineId, serial, name, cancellationToken);
await faceTemplates.UpsertAsync(serial, faceBytes, DateTime.Now, true, target.MachineId, target.IpAddress, cancellationToken);
} }
catch (Exception ex) catch (Exception ex)
{ {
failed++; failed++;
AppLogger.Warning($"DB→Device failed for {serial}: {ex.Message}"); AppLogger.Warning($"Device→Device failed for {serial}: {ex.Message}");
} }
} }
entry.CompletedAt = DateTime.Now; entry.CompletedAt = DateTime.Now;
entry.Success = created; entry.Success = registered;
entry.Failed = failed; entry.Failed = failed;
entry.Skipped = skipped; entry.Skipped = skipped;
entry.Context = $"{serialNumbers.Count} user(s), {faces} face(s) uploaded"; entry.Context = $"{users.Count} selected, {registered} registered on target, {faces} face(s) verified";
entry.Status = failed > 0 ? "Completed with errors" : "Completed"; entry.Status = failed > 0 ? "Completed with errors" : "Completed";
await history.UpdateAsync(entry, cancellationToken); await history.UpdateAsync(entry, cancellationToken);
progress?.Report(new UserSyncProgress(serialNumbers.Count, serialNumbers.Count, "", "Completed", created, 0, faces, 0, skipped, failed)); progress?.Report(new UserSyncProgress(users.Count, users.Count, "", "Completed", created, 0, faces, 0, skipped, failed));
AppLogger.TraceExit("TEMPLATE_SYNC", nameof(DbToDeviceAsync), AppLogger.TraceExit("TEMPLATE_SYNC", nameof(DeviceToDeviceAsync),
$"created={created} faces={faces} failed={failed} skipped={skipped}"); $"registered={registered} created={created} faces={faces} failed={failed} skipped={skipped}");
return entry; return entry;
} }
} }