feat: orchestrate DB_TO_DEVICE transfer with skip, photo, and summary

Full push flow in runner: skip, mapping, portal/NEDO, duplicates, reports.
main
SYED MUSTUFA AHMED NAQVI 2026-09-04 12:43:10 +05:00
parent e5cbed083e
commit d7b8a4057d
1 changed files with 549 additions and 138 deletions

View File

@ -90,92 +90,191 @@ namespace HanvonF710XAttendanceService
foreach (var job in enabledJobs) foreach (var job in enabledJobs)
{ {
logs.Add($"[TemplateJob {job.Id}] Mode={mode} SourceIp={job.SourceIp} Targets={job.TargetIps.Count}");
if (!_machineDao.TryGetAttendanceMachineByIp(connection, job.SourceIp, out var sourceMachine, out var sourceType))
{
logs.Add($"[TemplateJob {job.Id}] Source machine not found in DB. ip={job.SourceIp}");
continue;
}
if (!MachineScope.IsHanvonMachineType(sourceType))
{
logs.Add($"[TemplateJob {job.Id}] Source machine_type={sourceType} is not a Hanvon F710X device. Skipping.");
continue;
}
var targetMachines = new List<(AttendanceMachine Machine, string Type)>();
foreach (var targetIp in job.TargetIps.Distinct(StringComparer.OrdinalIgnoreCase))
{
if (!_machineDao.TryGetAttendanceMachineByIp(connection, targetIp, out var targetMachine, out var targetType))
{
logs.Add($"[TemplateJob {job.Id}] Target machine not found in DB. ip={targetIp}");
continue;
}
if (!MachineScope.IsHanvonMachineType(targetType))
{
logs.Add($"[TemplateJob {job.Id}] Target ip={targetIp} machine_type={targetType} is not a Hanvon F710X device. Skipping this target.");
continue;
}
targetMachines.Add((targetMachine, targetType));
}
if (targetMachines.Count == 0)
{
logs.Add($"[TemplateJob {job.Id}] No valid targets to transfer to.");
continue;
}
if (mode == TemplateTransferMode.DB_TO_DEVICE) if (mode == TemplateTransferMode.DB_TO_DEVICE)
{ {
List<string> idsForTransfer; RunDbToDeviceJob(job, connection, logs);
if (job.EmpIds != null && job.EmpIds.Count > 0)
{
// Restore from DB even when user was deleted from source device.
idsForTransfer = job.EmpIds
.Where(id => !string.IsNullOrWhiteSpace(id))
.Select(id => id.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
logs.Add($"[TemplateJob {job.Id}] DB_TO_DEVICE using EmpIds from config (source device list skipped): {string.Join(",", idsForTransfer)}");
}
else
{
if (!TryGetEmployeeIdsFromDevice(sourceMachine, out var employeeIds, logs, job.Id))
{
continue;
}
if (employeeIds.Count == 0)
{
logs.Add($"[TemplateJob {job.Id}] Source device returned 0 employee IDs.");
continue;
}
idsForTransfer = employeeIds;
}
TransferDbToDevice(job, idsForTransfer, targetMachines, connection, logs);
continue; continue;
} }
if (!TryGetEmployeeIdsFromDevice(sourceMachine, out var deviceEmployeeIds, logs, job.Id)) RunDeviceSourceTransferJob(job, mode, connection, logs);
{
continue;
}
if (deviceEmployeeIds.Count == 0)
{
logs.Add($"[TemplateJob {job.Id}] Source device returned 0 employee IDs.");
continue;
}
if (mode == TemplateTransferMode.DEVICE_TO_DEVICE)
{
TransferDeviceToDevice(job.Id, sourceMachine, deviceEmployeeIds, targetMachines, logs);
}
} }
} }
private void RunDbToDeviceJob(TemplateTransferJob job, MySqlConnection connection, List<string> logs)
{
logs.Add($"[TemplateJob {job.Id}] Mode=DB_TO_DEVICE Targets={job.TargetIps.Count}");
if (!string.IsNullOrWhiteSpace(job.SourceIp))
{
logs.Add($"[DB_TO_DEVICE] SourceIp={job.SourceIp} ignored. Users/templates will be loaded from HRMS DB.");
Program.WriteInternalLog($"[DB_TO_DEVICE] SourceIp={job.SourceIp} ignored because transfer mode is DB_TO_DEVICE.");
}
else
{
logs.Add("[DB_TO_DEVICE] SourceIp not specified. Users/templates will be loaded from HRMS DB.");
Program.WriteInternalLog("[DB_TO_DEVICE] SourceIp ignored because transfer mode is DB_TO_DEVICE.");
}
var targetMachines = ResolveHanvonTargetMachines(job, connection, logs);
if (targetMachines.Count == 0)
{
return;
}
foreach (var target in targetMachines)
{
logs.Add($"[DB_TO_DEVICE] Target device={target.Machine.MachineIp}");
Program.WriteInternalLog($"[DB_TO_DEVICE] Target={target.Machine.MachineIp}");
}
logs.Add("[DB_TO_DEVICE] Loading employees/templates from HRMS DB...");
Program.WriteInternalLog("[DB_TO_DEVICE] Loading employees/templates from HRMS DB...");
var targetMachineIds = targetMachines
.Select(t => t.Machine.MachineId)
.Where(id => !string.IsNullOrWhiteSpace(id))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
List<string> idsForTransfer;
try
{
idsForTransfer = DbToDevicePlanning.ResolveEmployeeIds(
job,
departmentIds =>
{
try
{
return _templateDao.GetActiveSerialNumbersByDepartmentIds(connection, departmentIds, targetMachineIds);
}
catch (Exception exDept)
{
logs.Add($"[DB_TO_DEVICE] DepartmentIds lookup failed; falling back to registered employees on targets. err={exDept.Message}");
Program.WriteInternalLog("[DB_TO_DEVICE] DepartmentIds lookup failed: " + exDept.Message);
return new List<string>();
}
},
() => LoadRegisteredEmployeeIdsOnTargets(connection, targetMachines, logs, job.Id));
}
catch (Exception exResolve)
{
logs.Add($"[DB_TO_DEVICE] Failed to resolve employees from HRMS DB. err={exResolve.Message}");
Program.WriteInternalLog("[DB_TO_DEVICE] Failed to resolve employees from HRMS DB: " + exResolve.Message);
return;
}
int requestedCount = job.EmpIds != null && job.EmpIds.Count > 0
? job.EmpIds.Count
: (job.DepartmentIds != null && job.DepartmentIds.Count > 0 ? job.DepartmentIds.Count : idsForTransfer.Count);
logs.Add($"[DB_TO_DEVICE] Employee filter requested={requestedCount} willSync={idsForTransfer.Count}");
Program.WriteInternalLog($"[DB_TO_DEVICE] Employee filter requested={requestedCount} willSync={idsForTransfer.Count}");
if (idsForTransfer.Count == 0)
{
logs.Add($"[DB_TO_DEVICE] No employees resolved from HRMS DB for job {job.Id}.");
Program.WriteInternalLog($"[DB_TO_DEVICE] No employees resolved from HRMS DB for job {job.Id}.");
return;
}
TransferDbToDevice(job, idsForTransfer, targetMachines, connection, logs);
}
private void RunDeviceSourceTransferJob(TemplateTransferJob job, TemplateTransferMode mode, MySqlConnection connection, List<string> logs)
{
logs.Add($"[TemplateJob {job.Id}] Mode={mode} SourceIp={job.SourceIp} Targets={job.TargetIps.Count}");
if (string.IsNullOrWhiteSpace(job.SourceIp))
{
logs.Add($"[TemplateJob {job.Id}] SourceIp is required for mode={mode}.");
return;
}
if (!_machineDao.TryGetAttendanceMachineByIp(connection, job.SourceIp, out var sourceMachine, out var sourceType))
{
logs.Add($"[TemplateJob {job.Id}] Source machine not found in DB. ip={job.SourceIp}");
return;
}
if (!MachineScope.IsHanvonMachineType(sourceType))
{
logs.Add($"[TemplateJob {job.Id}] Source machine_type={sourceType} is not a Hanvon F710X device. Skipping.");
return;
}
var targetMachines = ResolveHanvonTargetMachines(job, connection, logs);
if (targetMachines.Count == 0)
{
return;
}
if (!TryGetEmployeeIdsFromDevice(sourceMachine, out var deviceEmployeeIds, logs, job.Id))
{
return;
}
if (deviceEmployeeIds.Count == 0)
{
logs.Add($"[TemplateJob {job.Id}] Source device returned 0 employee IDs.");
return;
}
if (mode == TemplateTransferMode.DEVICE_TO_DEVICE)
{
TransferDeviceToDevice(job.Id, sourceMachine, deviceEmployeeIds, targetMachines, logs);
}
}
private List<(AttendanceMachine Machine, string Type)> ResolveHanvonTargetMachines(
TemplateTransferJob job,
MySqlConnection connection,
List<string> logs)
{
var targetMachines = new List<(AttendanceMachine Machine, string Type)>();
foreach (var targetIp in job.TargetIps.Distinct(StringComparer.OrdinalIgnoreCase))
{
if (!_machineDao.TryGetAttendanceMachineByIp(connection, targetIp, out var targetMachine, out var targetType))
{
logs.Add($"[TemplateJob {job.Id}] Target machine not found in DB. ip={targetIp}");
continue;
}
if (!MachineScope.IsHanvonMachineType(targetType))
{
logs.Add($"[TemplateJob {job.Id}] Target ip={targetIp} machine_type={targetType} is not a Hanvon F710X device. Skipping this target.");
continue;
}
targetMachines.Add((targetMachine, targetType));
}
if (targetMachines.Count == 0)
{
logs.Add($"[TemplateJob {job.Id}] No valid targets to transfer to.");
}
return targetMachines;
}
private List<string> LoadRegisteredEmployeeIdsOnTargets(
MySqlConnection connection,
List<(AttendanceMachine Machine, string Type)> targetMachines,
List<string> logs,
string jobId)
{
var ids = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var target in targetMachines)
{
var registered = _templateDao.GetActiveTemplatesForRegisteredMachine(connection, target.Machine.MachineId);
foreach (var serial in registered.Keys)
{
ids.Add(serial);
}
logs.Add($"[DB_TO_DEVICE] job={jobId} target={target.Machine.MachineIp} registered_with_templates={registered.Count}");
}
return ids.ToList();
}
private bool TryGetEmployeeIdsFromDevice(AttendanceMachine machine, out List<string> employeeIds, List<string> logs, string jobId) private bool TryGetEmployeeIdsFromDevice(AttendanceMachine machine, out List<string> employeeIds, List<string> logs, string jobId)
{ {
employeeIds = new List<string>(); employeeIds = new List<string>();
@ -665,49 +764,116 @@ namespace HanvonF710XAttendanceService
} }
} }
private Dictionary<string, byte[]> LoadTemplatesForDbToDevice(
MySqlConnection connection,
List<string> employeeIds,
List<string> logs)
{
var accumulated = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase);
if (employeeIds == null || employeeIds.Count == 0)
{
return accumulated;
}
var batches = SplitIntoBatches(employeeIds, DbToDevicePlanning.TemplateLoadBatchSize).ToList();
int totalBatches = batches.Count;
for (int batchIndex = 0; batchIndex < batches.Count; batchIndex++)
{
var batch = batches[batchIndex];
int batchNumber = batchIndex + 1;
logs.Add($"[DB_TO_DEVICE] Loading template batch {batchNumber}/{totalBatches} employees={batch.Count}");
Program.WriteInternalLog($"[DB_TO_DEVICE] Loading template batch {batchNumber}/{totalBatches} employees={batch.Count}");
Dictionary<string, byte[]> batchTemplates;
try
{
batchTemplates = _templateDao.GetActiveTemplatesBySerialNos(connection, batch);
}
catch (Exception exBatch)
{
logs.Add($"[DB_TO_DEVICE] Template batch {batchNumber} FAILED employees={batch.Count} err={exBatch.Message}");
Program.WriteInternalLog($"[DB_TO_DEVICE] Template batch {batchNumber} FAILED employees={batch.Count} err={exBatch}");
continue;
}
foreach (var kvp in batchTemplates)
{
accumulated[kvp.Key] = kvp.Value ?? Array.Empty<byte>();
}
logs.Add($"[DB_TO_DEVICE] Template batch {batchNumber} loaded={batchTemplates.Count}");
Program.WriteInternalLog($"[DB_TO_DEVICE] Template batch {batchNumber} loaded={batchTemplates.Count}");
}
return accumulated;
}
internal Dictionary<string, byte[]> LoadTemplatesForDbToDeviceTest(
MySqlConnection connection,
List<string> employeeIds,
List<string> logs)
{
return LoadTemplatesForDbToDevice(connection, employeeIds, logs);
}
private void TransferDbToDevice(TemplateTransferJob job, List<string> employeeIds, List<(AttendanceMachine Machine, string Type)> targets, MySqlConnection connection, List<string> logs) private void TransferDbToDevice(TemplateTransferJob job, List<string> employeeIds, List<(AttendanceMachine Machine, string Type)> targets, MySqlConnection connection, List<string> logs)
{ {
bool verifyAfterPush = GetBoolAppSetting("TEMPLATE_VERIFY_AFTER_PUSH", true); bool verifyAfterPush = GetBoolAppSetting("TEMPLATE_VERIFY_AFTER_PUSH", true);
int delayMs = GetIntAppSetting("TEMPLATE_DEVICE_DELAY_MS", 0); int delayMs = GetIntAppSetting("TEMPLATE_DEVICE_DELAY_MS", 0);
var effectiveEmployeeIds = employeeIds ?? new List<string>(); var effectiveEmployeeIds = DbToDevicePlanning.NormalizeEmployeeIds(employeeIds);
var templates = LoadTemplatesForDbToDevice(connection, effectiveEmployeeIds, logs);
// Optional filtering by EmpIds configured on the job (for safe, small-scope tests) int missingTemplateCount = effectiveEmployeeIds.Count - templates.Count;
if (job.EmpIds != null && job.EmpIds.Count > 0) logs.Add($"[DB_TO_DEVICE] Template loading complete requested={effectiveEmployeeIds.Count} loaded={templates.Count} missing={missingTemplateCount}");
{ Program.WriteInternalLog($"[DB_TO_DEVICE] Template loading complete requested={effectiveEmployeeIds.Count} loaded={templates.Count} missing={missingTemplateCount}");
var filter = new HashSet<string>(job.EmpIds, StringComparer.OrdinalIgnoreCase);
effectiveEmployeeIds = effectiveEmployeeIds.Where(id => filter.Contains(id)).ToList();
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} source={job.SourceIp} employees_filtered={effectiveEmployeeIds.Count}/{employeeIds.Count}");
}
var templates = _templateDao.GetActiveTemplatesBySerialNos(connection, effectiveEmployeeIds); logs.Add("[DB_TO_DEVICE] Creating/updating users on target...");
logs.Add($"[TemplateJob {job.Id}] DB templates fetched. count={templates.Count}"); Program.WriteInternalLog("[DB_TO_DEVICE] Creating/updating users on target...");
// Log whether each requested employee has a template in DB
foreach (var empId in effectiveEmployeeIds) foreach (var empId in effectiveEmployeeIds)
{ {
if (templates.ContainsKey(empId)) if (templates.ContainsKey(empId))
{ {
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} source={job.SourceIp} emp={empId} template=FOUND"); Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} emp={empId} template=FOUND");
} }
else else
{ {
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} source={job.SourceIp} emp={empId} template=NOT_FOUND"); logs.Add($"[DB_TO_DEVICE] employee={empId} template=MISSING no active face template in HRMS DB");
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} emp={empId} template=NOT_FOUND");
} }
} }
if (missingTemplateCount > 0)
{
logs.Add($"[DB_TO_DEVICE] Employees missing templates in HRMS DB: {missingTemplateCount}/{effectiveEmployeeIds.Count}");
}
// Batch per target machine. // Batch per target machine.
var templatePairs = templates.ToList(); var templatePairs = templates.ToList();
int totalTemplates = templatePairs.Count; int totalTemplates = templatePairs.Count;
int batchSize = ChooseBatchSize(totalTemplates); int batchSize = ChooseBatchSize(totalTemplates);
var batches = SplitIntoBatches(templatePairs, batchSize).ToList(); var batches = SplitIntoBatches(templatePairs, batchSize).ToList();
var missingTemplateSerials = effectiveEmployeeIds
.Where(id => !templates.ContainsKey(id))
.ToList();
foreach (var target in targets) foreach (var target in targets)
{ {
int ok = 0; int success = 0;
int fail = 0; int duplicateFace = 0;
int uploadFailed = 0;
int verifyOk = 0; int verifyOk = 0;
int verifyFail = 0; int verifyFail = 0;
int alreadyRegistered = 0;
var duplicateFaceEntries = new List<DbToDeviceDuplicateFaceEntry>();
var summary = new DbToDeviceTargetSummary
{
MachineId = target.Machine.MachineId ?? "",
MachineIp = target.Machine.MachineIp ?? "",
MachineType = string.IsNullOrWhiteSpace(target.Type) ? MachineScope.MachineTypeHanvon : target.Type
};
logs.Add($"[TemplateTransfer] job={job.Id} target_ip={target.Machine.MachineIp} total_templates={totalTemplates} batch_size={batchSize} total_batches={batches.Count}"); logs.Add($"[TemplateTransfer] job={job.Id} target_ip={target.Machine.MachineIp} total_templates={totalTemplates} batch_size={batchSize} total_batches={batches.Count}");
@ -718,77 +884,254 @@ namespace HanvonF710XAttendanceService
{ {
logs.Add($"[TemplateJob {job.Id}] HTTP login failed for target. ip={target.Machine.MachineIp} err={loginErr}"); logs.Add($"[TemplateJob {job.Id}] HTTP login failed for target. ip={target.Machine.MachineIp} err={loginErr}");
Program.RecordUnreachableMachine(target.Machine, "DB_TO_DEVICE HTTP login: " + loginErr); Program.RecordUnreachableMachine(target.Machine, "DB_TO_DEVICE HTTP login: " + loginErr);
WriteDbToDeviceTargetSummary(job.Id, summary, logs);
continue; continue;
} }
var machineUserDao = new AttendanceMachineUserDAO(); var machineUserDao = new AttendanceMachineUserDAO();
var employeeDao = new HrmsEmployeeDAO();
HashSet<string> registeredSerials;
try
{
registeredSerials = machineUserDao.GetActiveSerialNumbersForMachine(connection, target.Machine.MachineId);
}
catch (Exception exReg)
{
registeredSerials = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
Program.WriteInternalLog($"[DB_TO_DEVICE] registered lookup failed machine={target.Machine.MachineId} err={exReg.Message}");
}
foreach (var missingSerial in missingTemplateSerials)
{
if (DbToDeviceSkipLogic.ShouldSkipAlreadyRegistered(registeredSerials, missingSerial))
{
alreadyRegistered++;
summary.AlreadyRegistered++;
Program.WriteInternalLog($"[DB_TO_DEVICE] employee_serial={missingSerial} machine_id={target.Machine.MachineId} machine_ip={target.Machine.MachineIp} status=SKIPPED_ALREADY_REGISTERED");
continue;
}
HrmsEmployeeInfo missingEmp = null;
try { employeeDao.TryGetByDeviceSerial(connection, missingSerial, out missingEmp); } catch { }
if (missingEmp == null)
{
summary.AddMissingMapping(missingSerial);
}
summary.AddFailure(
missingSerial,
missingEmp?.ConcatenatedName ?? "",
missingEmp?.Department ?? "",
"Missing template in HRMS DB");
uploadFailed++;
}
for (int bi = 0; bi < batches.Count; bi++) for (int bi = 0; bi < batches.Count; bi++)
{ {
var swBatch = Stopwatch.StartNew(); var swBatch = Stopwatch.StartNew();
int bAttempted = 0; int bAttempted = 0;
int bOk = 0; int bOk = 0;
int bFail = 0; int bDuplicate = 0;
int bUploadFailed = 0;
int bVerifyOk = 0; int bVerifyOk = 0;
int bVerifyFail = 0; int bVerifyFail = 0;
int bSkipped = 0;
foreach (var kvp in batches[bi]) foreach (var kvp in batches[bi])
{ {
bAttempted++; bAttempted++;
string empId = kvp.Key; string empId = kvp.Key; // device serial / Hanvon enroll id
byte[] templateBlob = kvp.Value ?? Array.Empty<byte>();
string recordBase64 = HanvonHttpApiClient.NormalizeFaceTemplatePayload(templateBlob, out string pushMode); if (DbToDeviceSkipLogic.ShouldSkipAlreadyRegistered(registeredSerials, empId))
if (string.IsNullOrWhiteSpace(recordBase64))
{ {
fail++; alreadyRegistered++;
bFail++; summary.AlreadyRegistered++;
logs.Add($"[TemplateJob {job.Id}] Empty template payload. emp={empId} blob_bytes={templateBlob.Length} target={target.Machine.MachineIp}"); bSkipped++;
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL reason=EmptyTemplate blob_bytes={templateBlob.Length}"); Program.WriteInternalLog($"[DB_TO_DEVICE] employee_serial={empId} machine_id={target.Machine.MachineId} machine_ip={target.Machine.MachineIp} status=SKIPPED_ALREADY_REGISTERED");
continue;
}
byte[] templateBlob = kvp.Value ?? Array.Empty<byte>();
var uploadPlan = DbToDeviceUploadPlan.FromBlob(templateBlob);
bool portalPhotoEnabled = uploadPlan.Format == DbToDeviceTemplateFormat.NedoXml
&& EmployeePhotoSourceSettings.IsEnabled();
HrmsEmployeeInfo hrmsEmployee = null;
try
{
employeeDao.TryGetByDeviceSerial(connection, empId, out hrmsEmployee);
}
catch (Exception exEmp)
{
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} hrms_lookup=FAILED err={exEmp.Message}");
}
if (hrmsEmployee == null)
{
summary.AddMissingMapping(empId);
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} hrms_lookup=MISS");
}
string displayName = !string.IsNullOrWhiteSpace(hrmsEmployee?.ConcatenatedName)
? hrmsEmployee.ConcatenatedName
: empId;
string department = hrmsEmployee?.Department ?? "";
if (!uploadPlan.HasUploadPayload && !portalPhotoEnabled)
{
string reason = uploadPlan.Format == DbToDeviceTemplateFormat.NedoXml
? (uploadPlan.NedoPhotoStatus == NedoPhotoExtractResult.Invalid
? "NEDO photo invalid"
: "NEDO photo missing")
: "Empty template payload";
uploadFailed++;
bUploadFailed++;
summary.AddFailure(empId, displayName, department, reason);
logs.Add($"[TemplateJob {job.Id}] {reason}. emp={empId} target={target.Machine.MachineIp}");
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} face_upload=FAILED response={reason}");
continue; continue;
} }
if (!int.TryParse(empId, out int enrollId)) if (!int.TryParse(empId, out int enrollId))
{ {
fail++; uploadFailed++;
bFail++; bUploadFailed++;
summary.AddFailure(empId, displayName, department, "Invalid enroll id");
logs.Add($"[TemplateJob {job.Id}] Invalid enroll id for HTTP setuserinfo. emp={empId} target={target.Machine.MachineIp}"); logs.Add($"[TemplateJob {job.Id}] Invalid enroll id for HTTP setuserinfo. emp={empId} target={target.Machine.MachineIp}");
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL reason=InvalidEnrollId"); Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL reason=InvalidEnrollId");
continue; continue;
} }
string displayName = machineUserDao.GetEmployeeNameForMachine(connection, target.Machine.MachineId, empId) if (hrmsEmployee != null)
?? empId;
if (!httpClient.TryRestoreUserWithFaceTemplate(enrollId, displayName, recordBase64, pushMode, out string pushErr))
{ {
fail++; Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} hrms_id={hrmsEmployee.EmployeeId} name={displayName}");
bFail++; }
logs.Add($"[TemplateJob {job.Id}] HTTP template push failed. emp={empId} mode={pushMode} payload_len={recordBase64.Length} target={target.Machine.MachineIp} err={pushErr}");
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} mode={pushMode} payload_len={recordBase64.Length} send=FAIL err={pushErr}"); Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} template=FOUND format={uploadPlan.FormatLabel}");
if (uploadPlan.Format == DbToDeviceTemplateFormat.NedoXml)
{
bool hadNedoPayload = uploadPlan.HasUploadPayload;
if (portalPhotoEnabled)
{
string photoEmployeeId = hrmsEmployee?.EmployeeId;
if (uploadPlan.TryResolveEmployeePhoto(photoEmployeeId, out string resolveErr))
{
if (string.Equals(uploadPlan.PhotoSourceLabel, "HRMS_PORTAL", StringComparison.OrdinalIgnoreCase))
{
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} hrms_id={photoEmployeeId} photo_source=HRMS_PORTAL photo_url={uploadPlan.PhotoSourceUrl} photo_size={uploadPlan.PhotoByteLength} width={uploadPlan.PreparedWidth} height={uploadPlan.PreparedHeight}");
}
else if (hadNedoPayload)
{
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} photo_source=HRMS_PORTAL FAILED err={resolveErr ?? "unknown"}");
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} photo_source=NEDO_XML FALLBACK");
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} nedo_photo=FOUND base64_len={uploadPlan.PhotoBase64Length} size={uploadPlan.PhotoByteLength} blob_len={uploadPlan.SourceBlobLength}");
}
}
else
{
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} photo_source=HRMS_PORTAL FAILED err={resolveErr ?? "unknown"}");
}
}
else if (uploadPlan.HasUploadPayload)
{
uploadPlan.PhotoSourceLabel = "NEDO_XML";
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} nedo_photo=FOUND base64_len={uploadPlan.PhotoBase64Length} size={uploadPlan.PhotoByteLength} blob_len={uploadPlan.SourceBlobLength}");
}
}
if (!uploadPlan.HasUploadPayload)
{
uploadFailed++;
bUploadFailed++;
string reason = hrmsEmployee == null
? "Employee photo not found (missing HRMS mapping and no NEDO photo)"
: "Employee photo not found";
summary.AddFailure(empId, displayName, department, reason);
logs.Add($"[TemplateJob {job.Id}] No usable face photo after HRMS/NEDO resolution. emp={empId} target={target.Machine.MachineIp}");
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} face_upload=FAILED response=no_photo_available");
continue; continue;
} }
ok++; if (!uploadPlan.TryPrepareFacePayload(out string prepareErr))
bOk++; {
logs.Add($"[TemplateJob {job.Id}] HTTP template push ok. emp={empId} name={displayName} mode={pushMode} payload_len={recordBase64.Length} target={target.Machine.MachineIp}"); uploadFailed++;
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=OK"); bUploadFailed++;
summary.AddFailure(empId, displayName, department, "Photo prepare failed: " + (prepareErr ?? "unknown"));
logs.Add($"[DB_TO_DEVICE] employee={empId} target={target.Machine.MachineIp} face_prepare=FAILED err={prepareErr}");
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} format={uploadPlan.FormatLabel} photo=INVALID err={prepareErr}");
continue;
}
if (!string.IsNullOrWhiteSpace(uploadPlan.PrepareNote))
{
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} nedo_photo=PREPARED note={uploadPlan.PrepareNote} width={uploadPlan.PreparedWidth} height={uploadPlan.PreparedHeight} size={uploadPlan.PhotoByteLength}");
}
if (!httpClient.TryUploadDbToDeviceTemplate(enrollId, displayName, uploadPlan, out string pushErr))
{
if (DbToDeviceFaceErrors.TryParseDuplicateFace(pushErr, out string existingDeviceId))
{
duplicateFace++;
bDuplicate++;
duplicateFaceEntries.Add(new DbToDeviceDuplicateFaceEntry(empId, existingDeviceId, target.Machine.MachineIp));
summary.AddFailure(empId, displayName, department, "DUPLICATE_FACE existing_device_id=" + existingDeviceId);
logs.Add($"[DB_TO_DEVICE] employee={empId} face=DUPLICATE existing_device_id={existingDeviceId} target={target.Machine.MachineIp}");
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} face=DUPLICATE existing_device_id={existingDeviceId}");
continue;
}
uploadFailed++;
bUploadFailed++;
summary.AddFailure(empId, displayName, department, "UPLOAD_FAILED: " + (pushErr ?? "unknown"));
logs.Add($"[DB_TO_DEVICE] employee={empId} target={target.Machine.MachineIp} user=UPLOAD_FAILED face=FAILED err={pushErr}");
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} target={target.Machine.MachineIp} face_upload=FAILED response={pushErr}");
continue;
}
if (verifyAfterPush) if (verifyAfterPush)
{ {
if (httpClient.UserHasFaceTemplate(enrollId, out string verifyErr)) bool faceVerified = httpClient.VerifyDbToDeviceTemplate(
{ enrollId,
verifyOk++; uploadPlan,
bVerifyOk++; out string verifyFaceflag,
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=OK"); out string verifyPhotoUrl,
} out string verifyErr);
else if (!faceVerified)
{ {
verifyFail++; verifyFail++;
bVerifyFail++; bVerifyFail++;
summary.AddFailure(empId, displayName, department, "VERIFY_FAILED: " + (verifyErr ?? "unknown"));
logs.Add($"[TemplateJob {job.Id}] HTTP verify failed after push. emp={empId} target={target.Machine.MachineIp} err={verifyErr}"); logs.Add($"[TemplateJob {job.Id}] HTTP verify failed after push. emp={empId} target={target.Machine.MachineIp} err={verifyErr}");
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} verify=FAILED faceflag={verifyFaceflag ?? ""} photourl={verifyPhotoUrl ?? ""} err={verifyErr}");
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=FAIL err={verifyErr}"); Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=FAIL err={verifyErr}");
continue;
} }
verifyOk++;
bVerifyOk++;
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} verify faceflag={verifyFaceflag ?? ""} photourl={verifyPhotoUrl ?? ""} face=OK");
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=OK");
} }
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} target={target.Machine.MachineIp} face_upload=OK photo_source={uploadPlan.PhotoSourceLabel ?? uploadPlan.FormatLabel} push_mode={uploadPlan.PushMode} payload_len={uploadPlan.Payload?.Length ?? 0}");
try
{
machineUserDao.Add(new AttendanceMachineUser(target.Machine.MachineId, empId, displayName), connection);
registeredSerials.Add(empId);
}
catch (Exception exPersist)
{
logs.Add($"[DB_TO_DEVICE] employee={empId} target={target.Machine.MachineIp} user=CREATED face=UPLOADED db_user_persist=WARN err={exPersist.Message}");
Program.WriteInternalLog($"[DB_TO_DEVICE] attendance_machine_user persist warning emp={empId} err={exPersist.Message}");
}
success++;
summary.NewlyRegistered++;
bOk++;
logs.Add($"[DB_TO_DEVICE] employee={empId} target={target.Machine.MachineIp} user=CREATED face=UPLOADED");
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=OK");
} }
if (delayMs > 0) if (delayMs > 0)
@ -797,7 +1140,7 @@ namespace HanvonF710XAttendanceService
} }
swBatch.Stop(); swBatch.Stop();
logs.Add($"[TemplateTransferBatch] job={job.Id} target_ip={target.Machine.MachineIp} batch={bi + 1}/{batches.Count} attempted={bAttempted} ok={bOk} fail={bFail} verify_ok={bVerifyOk} verify_fail={bVerifyFail} duration_sec={Math.Round(swBatch.Elapsed.TotalSeconds, 1)}"); logs.Add($"[TemplateTransferBatch] job={job.Id} target_ip={target.Machine.MachineIp} batch={bi + 1}/{batches.Count} attempted={bAttempted} success={bOk} skipped={bSkipped} duplicate={bDuplicate} upload_fail={bUploadFailed} verify_ok={bVerifyOk} verify_fail={bVerifyFail} duration_sec={Math.Round(swBatch.Elapsed.TotalSeconds, 1)}");
} }
} }
else else
@ -810,7 +1153,7 @@ namespace HanvonF710XAttendanceService
var swBatch = Stopwatch.StartNew(); var swBatch = Stopwatch.StartNew();
int bAttempted = 0; int bAttempted = 0;
int bOk = 0; int bOk = 0;
int bFail = 0; int bUploadFailed = 0;
int bVerifyOk = 0; int bVerifyOk = 0;
int bVerifyFail = 0; int bVerifyFail = 0;
@ -821,8 +1164,9 @@ namespace HanvonF710XAttendanceService
string templateStr = Encoding.UTF8.GetString(kvp.Value ?? Array.Empty<byte>()); string templateStr = Encoding.UTF8.GetString(kvp.Value ?? Array.Empty<byte>());
if (!TemplateParser.TryBuildSetEmployeeCommand(templateStr, out var setCmd)) if (!TemplateParser.TryBuildSetEmployeeCommand(templateStr, out var setCmd))
{ {
fail++; uploadFailed++;
bFail++; bUploadFailed++;
summary.AddFailure(empId, empId, "", "Build SetEmployee failed");
logs.Add($"[TemplateJob {job.Id}] Build SetEmployee failed. emp={empId} target={target.Machine.MachineIp}"); logs.Add($"[TemplateJob {job.Id}] Build SetEmployee failed. emp={empId} target={target.Machine.MachineIp}");
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL reason=BuildSetEmployee"); Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL reason=BuildSetEmployee");
continue; continue;
@ -833,14 +1177,16 @@ namespace HanvonF710XAttendanceService
int rc = Program.test(targetDevInfo, targetDevInfo.Length, setCmd, setCmd.Length, ref response, ref recvLen, callback); int rc = Program.test(targetDevInfo, targetDevInfo.Length, setCmd, setCmd.Length, ref response, ref recvLen, callback);
if (rc != 0 || (response != null && response.IndexOf("fail", StringComparison.OrdinalIgnoreCase) >= 0)) if (rc != 0 || (response != null && response.IndexOf("fail", StringComparison.OrdinalIgnoreCase) >= 0))
{ {
fail++; uploadFailed++;
bFail++; bUploadFailed++;
summary.AddFailure(empId, empId, "", "SetEmployee failed rc=" + rc);
logs.Add($"[TemplateJob {job.Id}] SetEmployee failed. emp={empId} target={target.Machine.MachineIp}"); logs.Add($"[TemplateJob {job.Id}] SetEmployee failed. emp={empId} target={target.Machine.MachineIp}");
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL rc={rc}"); Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL rc={rc}");
} }
else else
{ {
ok++; success++;
summary.NewlyRegistered++;
bOk++; bOk++;
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=OK"); Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=OK");
@ -850,8 +1196,8 @@ namespace HanvonF710XAttendanceService
string verifyResp = ""; string verifyResp = "";
uint verifyRecvLen = 0; uint verifyRecvLen = 0;
int vrc = Program.test(targetDevInfo, targetDevInfo.Length, verifyCmd, verifyCmd.Length, ref verifyResp, ref verifyRecvLen, callback); int vrc = Program.test(targetDevInfo, targetDevInfo.Length, verifyCmd, verifyCmd.Length, ref verifyResp, ref verifyRecvLen, callback);
bool success = vrc == 0 && verifyResp != null && verifyResp.IndexOf("success", StringComparison.OrdinalIgnoreCase) >= 0; bool verifiedOk = vrc == 0 && verifyResp != null && verifyResp.IndexOf("success", StringComparison.OrdinalIgnoreCase) >= 0;
if (success) if (verifiedOk)
{ {
verifyOk++; verifyOk++;
bVerifyOk++; bVerifyOk++;
@ -861,6 +1207,7 @@ namespace HanvonF710XAttendanceService
{ {
verifyFail++; verifyFail++;
bVerifyFail++; bVerifyFail++;
summary.AddFailure(empId, empId, "", "VERIFY_FAILED rc=" + vrc);
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=FAIL rc={vrc}"); Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=FAIL rc={vrc}");
} }
} }
@ -873,18 +1220,82 @@ namespace HanvonF710XAttendanceService
} }
swBatch.Stop(); swBatch.Stop();
logs.Add($"[TemplateTransferBatch] job={job.Id} target_ip={target.Machine.MachineIp} batch={bi + 1}/{batches.Count} attempted={bAttempted} ok={bOk} fail={bFail} verify_ok={bVerifyOk} verify_fail={bVerifyFail} duration_sec={Math.Round(swBatch.Elapsed.TotalSeconds, 1)}"); logs.Add($"[TemplateTransferBatch] job={job.Id} target_ip={target.Machine.MachineIp} batch={bi + 1}/{batches.Count} attempted={bAttempted} success={bOk} upload_fail={bUploadFailed} verify_ok={bVerifyOk} verify_fail={bVerifyFail} duration_sec={Math.Round(swBatch.Elapsed.TotalSeconds, 1)}");
} }
} }
if (verifyAfterPush) WriteDbToDeviceDuplicateReport(job.Id, duplicateFaceEntries, logs);
WriteDbToDeviceTargetSummary(job.Id, summary, logs);
int failed = summary.Failed;
logs.Add($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} summary SUCCESS={success} ALREADY_REGISTERED={alreadyRegistered} MISSING_TEMPLATE={missingTemplateCount} DUPLICATE_FACE={duplicateFace} UPLOAD_FAILED={uploadFailed} VERIFY_FAILED={verifyFail}");
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} created={success} faceUploaded={success} alreadyRegistered={alreadyRegistered} duplicateFace={duplicateFace} missingTemplate={missingTemplateCount} failed={failed}");
}
}
private static void WriteDbToDeviceTargetSummary(string jobId, DbToDeviceTargetSummary summary, List<string> logs)
{
if (summary == null)
{
return;
}
string report = summary.FormatReport();
foreach (var line in report.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None))
{
if (string.IsNullOrEmpty(line) && logs.Count > 0 && logs[logs.Count - 1] == "")
{ {
logs.Add($"[TemplateJob {job.Id}] DB_TO_DEVICE target={target.Machine.MachineIp} ok={ok} fail={fail} verify_ok={verifyOk} verify_fail={verifyFail}"); continue;
} }
else
logs.Add(line);
Program.WriteInternalLog(line);
}
try
{
string dir = ApplicationPaths.InternalLogs;
Directory.CreateDirectory(dir);
string path = Path.Combine(
dir,
"DbToDeviceSummary_" + jobId + "_" + (summary.MachineId ?? "machine") + "_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".txt");
File.WriteAllText(path, report, Encoding.UTF8);
logs.Add($"[DB_TO_DEVICE] Summary report written. path={path}");
Program.WriteInternalLog($"[DB_TO_DEVICE] Summary report written. path={path}");
}
catch (Exception ex)
{
logs.Add($"[DB_TO_DEVICE] Summary report write failed. err={ex.Message}");
Program.WriteInternalLog($"[DB_TO_DEVICE] Summary report write failed. err={ex.Message}");
}
}
private static void WriteDbToDeviceDuplicateReport(string jobId, IReadOnlyList<DbToDeviceDuplicateFaceEntry> entries, List<string> logs)
{
if (entries == null || entries.Count == 0)
{
return;
}
try
{
string dir = ApplicationPaths.InternalLogs;
Directory.CreateDirectory(dir);
string path = Path.Combine(dir, "DbToDeviceDuplicates_" + jobId + "_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".txt");
var lines = new List<string> { "employee_id | existing_device_id | target_device" };
foreach (var entry in entries)
{ {
logs.Add($"[TemplateJob {job.Id}] DB_TO_DEVICE target={target.Machine.MachineIp} ok={ok} fail={fail}"); lines.Add($"{entry.EmployeeId} | {entry.ExistingDeviceId} | {entry.TargetDevice}");
} }
File.WriteAllLines(path, lines, Encoding.UTF8);
logs.Add($"[DB_TO_DEVICE] Duplicate face report written. path={path} count={entries.Count}");
Program.WriteInternalLog($"[DB_TO_DEVICE] Duplicate face report written. path={path} count={entries.Count}");
}
catch (Exception ex)
{
logs.Add($"[DB_TO_DEVICE] Duplicate face report write failed. err={ex.Message}");
Program.WriteInternalLog($"[DB_TO_DEVICE] Duplicate face report write failed. err={ex.Message}");
} }
} }
@ -967,7 +1378,7 @@ namespace HanvonF710XAttendanceService
{ {
try try
{ {
string filepath = AppDomain.CurrentDomain.BaseDirectory + "\\TemplateRawLogs\\TemplateRaw_" + DateTime.Now.Date.ToShortDateString().Replace('/', '_') + ".txt"; string filepath = ApplicationPaths.DatedFile(ApplicationPaths.TemplateRawLogs, "TemplateRaw");
LogService.EnqueueLine(filepath, "----- " + DateTime.Now + " -----"); LogService.EnqueueLine(filepath, "----- " + DateTime.Now + " -----");
LogService.EnqueueLine(filepath, header); LogService.EnqueueLine(filepath, header);
LogService.EnqueueLine(filepath, raw ?? ""); LogService.EnqueueLine(filepath, raw ?? "");