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 historymain
parent
ab084ebb50
commit
78b8a9bc86
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue