1395 lines
70 KiB
C#
1395 lines
70 KiB
C#
using MySql.Data.MySqlClient;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Configuration;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading;
|
|
|
|
namespace HanvonF710XAttendanceService
|
|
{
|
|
internal enum TemplateTransferMode
|
|
{
|
|
DEVICE_TO_DB,
|
|
DB_TO_DEVICE,
|
|
DEVICE_TO_DEVICE
|
|
}
|
|
|
|
internal sealed class TemplateTransferRunner
|
|
{
|
|
private readonly AttendanceMachineDAO _machineDao;
|
|
private readonly AttendanceMachineFaceTemplateDAO _templateDao;
|
|
|
|
public TemplateTransferRunner(AttendanceMachineDAO machineDao, AttendanceMachineFaceTemplateDAO templateDao)
|
|
{
|
|
_machineDao = machineDao ?? throw new ArgumentNullException(nameof(machineDao));
|
|
_templateDao = templateDao ?? throw new ArgumentNullException(nameof(templateDao));
|
|
}
|
|
|
|
public void RunJobs(DeviceSettingsConfig settings, TemplateTransferMode mode, MySqlConnection connection, List<string> logs)
|
|
{
|
|
if (mode == TemplateTransferMode.DEVICE_TO_DB)
|
|
{
|
|
// TEMPLATE SAVE (DEVICE_TO_DB) now follows the same scope model
|
|
// as attendance and machine user sync (CENTRAL vs SITE).
|
|
var machinesInScope = Program.GetMachinesForAttendanceAndUsers(_machineDao, connection, logs);
|
|
var scope = (ConfigurationManager.AppSettings["ATTENDANCE_SCOPE"] ?? "CENTRAL").ToUpperInvariant();
|
|
logs.Add($"Template DEVICE_TO_DB scope={scope}. machines_in_scope={machinesInScope.Count}");
|
|
bool autoDistributeAfterSave = GetBoolAppSetting("TEMPLATE_AUTO_DISTRIBUTE_AFTER_DEVICE_TO_DB", true);
|
|
|
|
if (machinesInScope.Count == 0)
|
|
{
|
|
logs.Add("Template DEVICE_TO_DB: no machines found in scope.");
|
|
return;
|
|
}
|
|
|
|
foreach (var m in machinesInScope)
|
|
{
|
|
if (!_machineDao.TryGetAttendanceMachineByIp(connection, m.MachineIp, out var sourceMachine, out var sourceType))
|
|
{
|
|
logs.Add($"[TemplateSave] source machine not found in DB. machine_id={m.MachineId} ip={m.MachineIp}");
|
|
continue;
|
|
}
|
|
|
|
if (!MachineScope.IsHanvonMachineType(sourceType))
|
|
{
|
|
logs.Add($"[TemplateSave] source machine_type={sourceType} not supported for Hanvon F710X service. machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp}. Skipping.");
|
|
continue;
|
|
}
|
|
|
|
logs.Add($"[TemplateSave] DEVICE_TO_DB start. machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp}");
|
|
bool saveOk = SaveTemplateDbFromDevice(sourceMachine, connection, logs, $"TemplateSave:{sourceMachine.MachineIp}");
|
|
if (saveOk && autoDistributeAfterSave)
|
|
{
|
|
DistributeMissingTemplatesAfterDeviceToDb(sourceMachine, machinesInScope, logs);
|
|
}
|
|
else if (!saveOk)
|
|
{
|
|
LogDistributionLine($"[TemplateDistribution] skipped for source_machine={sourceMachine.MachineId} ip={sourceMachine.MachineIp} (DEVICE_TO_DB did not complete; device unreachable or no employee list).", logs);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (settings?.TemplateTransfer == null || !settings.TemplateTransfer.Enabled)
|
|
{
|
|
logs.Add("TemplateTransfer is disabled in DeviceSettings.");
|
|
return;
|
|
}
|
|
|
|
var enabledJobs = settings.TemplateTransfer.Jobs.Where(j => j.Enabled).ToList();
|
|
|
|
if (enabledJobs.Count == 0)
|
|
{
|
|
logs.Add("No enabled TemplateTransfer jobs found.");
|
|
return;
|
|
}
|
|
|
|
foreach (var job in enabledJobs)
|
|
{
|
|
if (mode == TemplateTransferMode.DB_TO_DEVICE)
|
|
{
|
|
RunDbToDeviceJob(job, connection, logs);
|
|
continue;
|
|
}
|
|
|
|
RunDeviceSourceTransferJob(job, mode, connection, 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)
|
|
{
|
|
employeeIds = new List<string>();
|
|
|
|
if (DeviceProtocol.UseHttp())
|
|
{
|
|
var client = HanvonHttpApiClient.ForMachine(machine);
|
|
if (!client.TryLogin(out string loginErr))
|
|
{
|
|
logs.Add($"[TemplateJob {jobId}] HTTP login failed. source={machine.MachineIp} err={loginErr}");
|
|
Program.RecordUnreachableMachine(machine, "TemplateSave HTTP: " + loginErr);
|
|
return false;
|
|
}
|
|
var users = client.GetUserList(out string listErr);
|
|
if (!string.IsNullOrEmpty(listErr) && (users == null || users.Count == 0))
|
|
{
|
|
logs.Add($"[TemplateJob {jobId}] getuserlist failed. source={machine.MachineIp} err={listErr}");
|
|
Program.RecordUnreachableMachine(machine, "TemplateSave HTTP getuserlist: " + listErr);
|
|
return false;
|
|
}
|
|
foreach (var u in users)
|
|
{
|
|
// Prefer device enroll id for getuserinfo; also keep serial for DB key.
|
|
if (!string.IsNullOrWhiteSpace(u.DeviceUserId))
|
|
employeeIds.Add(u.DeviceUserId);
|
|
else if (!string.IsNullOrWhiteSpace(u.SerialNumber))
|
|
employeeIds.Add(u.SerialNumber);
|
|
}
|
|
logs.Add($"[TemplateJob {jobId}] HTTP getuserlist ok. parsed={employeeIds.Count}");
|
|
return true;
|
|
}
|
|
|
|
var callback = new CallBack(Program.BeCalled);
|
|
string devInfo = machine.GetDeviceInfo();
|
|
string cmd = "GetEmployeeID()";
|
|
string response = "";
|
|
uint recvLen = 0;
|
|
|
|
if (Program.test(devInfo, devInfo.Length, cmd, cmd.Length, ref response, ref recvLen, callback) != 0)
|
|
{
|
|
logs.Add($"[TemplateJob {jobId}] GetEmployeeID() failed. source={machine.MachineIp}");
|
|
Program.RecordUnreachableMachine(machine, "TemplateSave: device not connected");
|
|
return false;
|
|
}
|
|
|
|
employeeIds = TemplateParser.ParseEmployeeIdsFromGetEmployeeIdResponse(response);
|
|
try
|
|
{
|
|
logs.Add($"[TemplateJob {jobId}] GetEmployeeID() ok. total={Program.GetLoopCnt(response)} parsed={employeeIds.Count}");
|
|
}
|
|
catch
|
|
{
|
|
logs.Add($"[TemplateJob {jobId}] GetEmployeeID() ok. parsed={employeeIds.Count}");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool SaveTemplateDbFromDevice(AttendanceMachine sourceMachine, MySqlConnection connection, List<string> logs, string jobId)
|
|
{
|
|
var swTotal = Stopwatch.StartNew();
|
|
if (!TryGetEmployeeIdsFromDevice(sourceMachine, out var employeeIds, logs, jobId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int inserted = 0;
|
|
int skippedExisting = 0;
|
|
int failed = 0;
|
|
|
|
bool capturedRaw = false;
|
|
|
|
int totalIds = employeeIds.Count;
|
|
int batchSize = ChooseBatchSize(totalIds);
|
|
var batches = SplitIntoBatches(employeeIds, batchSize).ToList();
|
|
logs.Add($"[TemplateSave] machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp} total_ids={totalIds} batch_size={batchSize} total_batches={batches.Count}");
|
|
|
|
for (int bi = 0; bi < batches.Count; bi++)
|
|
{
|
|
var swBatch = Stopwatch.StartNew();
|
|
int attempted = 0;
|
|
int bInserted = 0;
|
|
int bExisting = 0;
|
|
int bFailed = 0;
|
|
|
|
foreach (var empId in batches[bi])
|
|
{
|
|
attempted++;
|
|
try
|
|
{
|
|
if (_templateDao.ExistsBySerialNo(connection, empId))
|
|
{
|
|
skippedExisting++;
|
|
bExisting++;
|
|
// Simple log line for SaveFaceTemplate logs (keep deep logs in InternalLogs as-is).
|
|
logs.Add($"Serial number-- {empId} already exists in the database");
|
|
continue;
|
|
}
|
|
|
|
if (DeviceProtocol.UseHttp())
|
|
{
|
|
if (!int.TryParse(empId, out int enrollId))
|
|
{
|
|
failed++;
|
|
bFailed++;
|
|
logs.Add($"[TemplateJob {jobId}] invalid enroll id for HTTP. emp={empId}");
|
|
continue;
|
|
}
|
|
var httpClient = HanvonHttpApiClient.ForMachine(sourceMachine);
|
|
if (!httpClient.TryGetFaceRecord(enrollId, out string recordB64, out string empName, out string faceErr))
|
|
{
|
|
failed++;
|
|
bFailed++;
|
|
logs.Add($"[TemplateJob {jobId}] HTTP getuserinfo failed. emp={empId} err={faceErr}");
|
|
continue;
|
|
}
|
|
// Store under employee name/serial when available.
|
|
string serialKey = !string.IsNullOrWhiteSpace(empName) ? empName.Trim() : empId;
|
|
if (_templateDao.ExistsBySerialNo(connection, serialKey))
|
|
{
|
|
skippedExisting++;
|
|
bExisting++;
|
|
logs.Add($"Serial number-- {serialKey} already exists in the database");
|
|
continue;
|
|
}
|
|
var faceBytes = Encoding.UTF8.GetBytes(recordB64);
|
|
int faceRows = _templateDao.InsertTemplate(connection, serialKey, faceBytes, DateTime.Now);
|
|
if (faceRows > 0) { inserted++; bInserted++; }
|
|
else { failed++; bFailed++; }
|
|
continue;
|
|
}
|
|
|
|
var callback = new CallBack(Program.BeCalled);
|
|
string devInfo = sourceMachine.GetDeviceInfo();
|
|
string cmd = "GetEmployee(id=\"" + empId + "\")";
|
|
string response = "";
|
|
uint recvLen = 0;
|
|
|
|
if (Program.test(devInfo, devInfo.Length, cmd, cmd.Length, ref response, ref recvLen, callback) != 0)
|
|
{
|
|
failed++;
|
|
bFailed++;
|
|
logs.Add($"[TemplateJob {jobId}] GetEmployee failed. emp={empId} source={sourceMachine.MachineIp}");
|
|
continue;
|
|
}
|
|
|
|
if (!capturedRaw && response != null && response.IndexOf("success", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
capturedRaw = true;
|
|
WriteRawTemplateLog($"[TemplateJob {jobId}] GetEmployee raw response emp={empId}", response);
|
|
}
|
|
|
|
if (response == null || response.IndexOf("success", StringComparison.OrdinalIgnoreCase) < 0)
|
|
{
|
|
failed++;
|
|
bFailed++;
|
|
logs.Add($"[TemplateJob {jobId}] GetEmployee returned non-success. emp={empId} source={sourceMachine.MachineIp}");
|
|
continue;
|
|
}
|
|
|
|
if (!TemplateParser.TryExtractTemplatePayloadFromGetEmployeeResponse(response, out var payload))
|
|
{
|
|
failed++;
|
|
bFailed++;
|
|
logs.Add($"[TemplateJob {jobId}] Failed to parse template payload. emp={empId}");
|
|
continue;
|
|
}
|
|
|
|
var bytes = Encoding.UTF8.GetBytes(payload);
|
|
int rows = _templateDao.InsertTemplate(connection, empId, bytes, DateTime.Now);
|
|
if (rows > 0)
|
|
{
|
|
inserted++;
|
|
bInserted++;
|
|
}
|
|
else
|
|
{
|
|
failed++;
|
|
bFailed++;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
failed++;
|
|
bFailed++;
|
|
logs.Add($"[TemplateJob {jobId}] SaveTemplateDB error. emp={empId} err={ex.Message}");
|
|
}
|
|
}
|
|
|
|
swBatch.Stop();
|
|
logs.Add($"[TemplateSaveBatch] machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp} batch={bi + 1}/{batches.Count} attempted={attempted} inserted={bInserted} existing={bExisting} failed={bFailed} duration_sec={Math.Round(swBatch.Elapsed.TotalSeconds, 1)}");
|
|
}
|
|
|
|
swTotal.Stop();
|
|
logs.Add($"[TemplateSave] machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp} completed inserted={inserted} existing={skippedExisting} failed={failed} duration_sec={Math.Round(swTotal.Elapsed.TotalSeconds, 1)}");
|
|
logs.Add($"[TemplateJob {jobId}] SaveTemplateDB done. inserted={inserted} existing={skippedExisting} failed={failed}");
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// After DEVICE_TO_DB, distribute templates to peer Nedo machines using registration-based target selection:
|
|
/// only push an employee template to machines where that employee is registered (attendance_machine_user, is_deleted=0).
|
|
/// Central truth is still DB templates; device lists are only used to skip already-present templates.
|
|
/// Uses a dedicated DB connection for query + template fetch (no shared connection with attendance job).
|
|
/// </summary>
|
|
private void DistributeMissingTemplatesAfterDeviceToDb(AttendanceMachine sourceMachine, List<AttendanceMachine> machinesInScope, List<string> logs)
|
|
{
|
|
if (sourceMachine == null || machinesInScope == null || machinesInScope.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var peers = machinesInScope.Where(m => !IsSameScopedMachine(m, sourceMachine)).ToList();
|
|
if (peers.Count == 0)
|
|
{
|
|
LogDistributionLine($"[TemplateDistributionPlan] source_machine={sourceMachine.MachineId} ip={sourceMachine.MachineIp} no peer Hanvon machines in scope; skipping.", logs);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
using (var distConn = new MySqlConnection(Program.GetConnectionString()))
|
|
{
|
|
distConn.Open();
|
|
|
|
// Targets to consider are already filtered by ATTENDANCE_SCOPE + DeviceSettings in machinesInScope.
|
|
// Now apply registration-based selection inside DB queries.
|
|
var targets = new List<AttendanceMachine>();
|
|
foreach (var t in peers)
|
|
{
|
|
if (t == null) continue;
|
|
if (string.IsNullOrWhiteSpace(t.MachineId) || string.IsNullOrWhiteSpace(t.MachineIp))
|
|
{
|
|
continue;
|
|
}
|
|
targets.Add(t);
|
|
}
|
|
|
|
if (targets.Count == 0)
|
|
{
|
|
LogDistributionLine("[TemplateDistribution] no valid peer Hanvon targets in scope; skipping.", logs);
|
|
return;
|
|
}
|
|
|
|
var allowedMachineIds = targets
|
|
.Select(t => t.MachineId)
|
|
.Where(id => !string.IsNullOrWhiteSpace(id))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
// 1) For each target machine, get already-present employee ids from the device,
|
|
// then get DB templates only for employees registered on that specific target.
|
|
// 2) missing = registeredTemplates - onDeviceIds
|
|
var toSendByTarget = new Dictionary<string, Dictionary<string, byte[]>>(StringComparer.OrdinalIgnoreCase);
|
|
var missingSerialsUnion = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var targetMachine in targets)
|
|
{
|
|
if (!TryGetEmployeeIdsFromDevice(targetMachine, out var onDeviceIds, logs, "TemplateDist:" + targetMachine.MachineIp))
|
|
{
|
|
continue; // unreachable or device communication error already logged
|
|
}
|
|
|
|
var onDevice = new HashSet<string>(onDeviceIds, StringComparer.OrdinalIgnoreCase);
|
|
|
|
// Registration-based: only templates whose employee is registered to THIS target machine.
|
|
var registeredTemplates = _templateDao.GetActiveTemplatesForRegisteredMachine(distConn, targetMachine.MachineId);
|
|
|
|
var missing = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var kvp in registeredTemplates)
|
|
{
|
|
if (!onDevice.Contains(kvp.Key))
|
|
{
|
|
missing[kvp.Key] = kvp.Value;
|
|
missingSerialsUnion.Add(kvp.Key);
|
|
}
|
|
}
|
|
|
|
toSendByTarget[targetMachine.MachineId] = missing;
|
|
}
|
|
|
|
if (missingSerialsUnion.Count == 0)
|
|
{
|
|
LogDistributionLine("[TemplateDistribution] nothing missing on peer Hanvon machines; registration-based distribution skipped.", logs);
|
|
return;
|
|
}
|
|
|
|
// Build serial -> list of registered target machine ids (restricted to current scope targets).
|
|
var serialToRegisteredTargets = _templateDao.GetRegisteredTargetMachineIdsForSerialNos(
|
|
distConn,
|
|
missingSerialsUnion.ToList(),
|
|
allowedMachineIds);
|
|
|
|
// Pre-compute pushed_to / skipped_existing per serial (based on which targets have missing templates).
|
|
var pushedTargetsBySerial = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var serial in missingSerialsUnion)
|
|
{
|
|
pushedTargetsBySerial[serial] = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
foreach (var kvp in toSendByTarget)
|
|
{
|
|
var targetMachineId = kvp.Key;
|
|
var missingTemplates = kvp.Value;
|
|
if (missingTemplates == null || missingTemplates.Count == 0) continue;
|
|
|
|
foreach (var serial in missingTemplates.Keys)
|
|
{
|
|
if (pushedTargetsBySerial.TryGetValue(serial, out var set))
|
|
{
|
|
set.Add(targetMachineId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Log registration targets + per-serial distribution summary.
|
|
foreach (var serial in missingSerialsUnion)
|
|
{
|
|
int registeredTargets = 0;
|
|
if (serialToRegisteredTargets.TryGetValue(serial, out var targetList) && targetList != null)
|
|
{
|
|
registeredTargets = targetList.Count;
|
|
}
|
|
|
|
int pushedTo = 0;
|
|
if (pushedTargetsBySerial.TryGetValue(serial, out var pushedSet) && pushedSet != null)
|
|
{
|
|
pushedTo = pushedSet.Count;
|
|
}
|
|
|
|
int skippedExisting = Math.Max(0, registeredTargets - pushedTo);
|
|
|
|
LogDistributionLine($"[TemplateRegistrationTargets] serial_no={serial} registered_targets={registeredTargets}", logs);
|
|
LogDistributionLine($"[TemplateDistributionSummary] serial_no={serial} total_targets={registeredTargets} pushed_to={pushedTo} skipped_existing={skippedExisting}", logs);
|
|
}
|
|
|
|
// 3) Push missing templates in batches per target machine.
|
|
foreach (var targetMachine in targets)
|
|
{
|
|
if (!toSendByTarget.TryGetValue(targetMachine.MachineId, out var missingTemplates) ||
|
|
missingTemplates == null ||
|
|
missingTemplates.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int batchSize = ChooseBatchSize(missingTemplates.Count);
|
|
int totalBatches = (missingTemplates.Count + batchSize - 1) / batchSize;
|
|
LogDistributionLine(
|
|
$"[TemplateDistributionPlan] target_machine={targetMachine.MachineId} missing_templates={missingTemplates.Count} batch_size={batchSize} total_batches={totalBatches}",
|
|
logs);
|
|
|
|
foreach (var serial in missingTemplates.Keys)
|
|
{
|
|
LogDistributionLine(
|
|
$"[TemplateDistributionPlan] serial_no={serial} target_machine={targetMachine.MachineId} already_present=false will_push=true",
|
|
logs);
|
|
}
|
|
|
|
PushDistributionBatches(targetMachine, missingTemplates, batchSize, logs, out _);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogDistributionLine("[TemplateDistribution] failed: " + ex.ToString(), logs);
|
|
}
|
|
}
|
|
|
|
private static void LogDistributionLine(string message, List<string> logs)
|
|
{
|
|
logs?.Add(message);
|
|
}
|
|
|
|
private static bool IsSameScopedMachine(AttendanceMachine a, AttendanceMachine b)
|
|
{
|
|
if (a == null || b == null) return false;
|
|
if (!string.IsNullOrWhiteSpace(a.MachineId) && !string.IsNullOrWhiteSpace(b.MachineId))
|
|
{
|
|
return string.Equals(a.MachineId.Trim(), b.MachineId.Trim(), StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
return string.Equals(a.MachineIp?.Trim(), b.MachineIp?.Trim(), StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
/// <summary>Send only missing templates; does not issue SetEmployee for serials already on device.</summary>
|
|
private int PushDistributionBatches(AttendanceMachine target, Dictionary<string, byte[]> templates, int batchSize, List<string> logs, out int failedTotal)
|
|
{
|
|
failedTotal = 0;
|
|
if (templates == null || templates.Count == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
bool verifyAfterPush = GetBoolAppSetting("TEMPLATE_VERIFY_AFTER_PUSH", true);
|
|
int delayMs = GetIntAppSetting("TEMPLATE_DEVICE_DELAY_MS", 0);
|
|
string targetDevInfo = target.GetDeviceInfo();
|
|
var callback = new CallBack(Program.BeCalled);
|
|
var pairs = templates.ToList();
|
|
var batches = SplitIntoBatches(pairs, batchSize).ToList();
|
|
int sent = 0;
|
|
|
|
for (int bi = 0; bi < batches.Count; bi++)
|
|
{
|
|
var swBatch = Stopwatch.StartNew();
|
|
int attempted = 0;
|
|
int success = 0;
|
|
int failed = 0;
|
|
|
|
foreach (var kvp in batches[bi])
|
|
{
|
|
attempted++;
|
|
string empId = kvp.Key;
|
|
string templateStr = Encoding.UTF8.GetString(kvp.Value ?? Array.Empty<byte>());
|
|
if (!TemplateParser.TryBuildSetEmployeeCommand(templateStr, out var setCmd))
|
|
{
|
|
failed++;
|
|
failedTotal++;
|
|
LogDistributionLine($"[TemplateDistribution] target={target.MachineIp} emp={empId} send=FAIL reason=BuildSetEmployee", logs);
|
|
continue;
|
|
}
|
|
|
|
string response = "";
|
|
uint recvLen = 0;
|
|
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))
|
|
{
|
|
failed++;
|
|
failedTotal++;
|
|
LogDistributionLine($"[TemplateDistribution] target={target.MachineIp} emp={empId} send=FAIL rc={rc}", logs);
|
|
continue;
|
|
}
|
|
|
|
success++;
|
|
sent++;
|
|
|
|
if (verifyAfterPush)
|
|
{
|
|
string verifyCmd = "GetEmployee(id=\"" + empId + "\")";
|
|
string verifyResp = "";
|
|
uint verifyRecvLen = 0;
|
|
int vrc = Program.test(targetDevInfo, targetDevInfo.Length, verifyCmd, verifyCmd.Length, ref verifyResp, ref verifyRecvLen, callback);
|
|
bool ok = vrc == 0 && verifyResp != null && verifyResp.IndexOf("success", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
LogDistributionLine(ok
|
|
? $"[TemplateDistribution] target={target.MachineIp} emp={empId} verify=OK"
|
|
: $"[TemplateDistribution] target={target.MachineIp} emp={empId} verify=FAIL rc={vrc}", logs);
|
|
}
|
|
}
|
|
|
|
if (delayMs > 0)
|
|
{
|
|
Thread.Sleep(delayMs);
|
|
}
|
|
|
|
swBatch.Stop();
|
|
LogDistributionLine(
|
|
$"[TemplateDistributionBatch] target_machine={target.MachineId} ip={target.MachineIp} batch={bi + 1}/{batches.Count} attempted={attempted} success={success} failed={failed} duration_sec={Math.Round(swBatch.Elapsed.TotalSeconds, 1)}",
|
|
logs);
|
|
}
|
|
|
|
return sent;
|
|
}
|
|
|
|
private static int ChooseBatchSize(int total)
|
|
{
|
|
if (total <= 0) return 1;
|
|
if (total <= 20) return 5;
|
|
if (total <= 100) return 10;
|
|
if (total <= 300) return 25;
|
|
if (total <= 1000) return 50;
|
|
return 100;
|
|
}
|
|
|
|
private static IEnumerable<List<T>> SplitIntoBatches<T>(IReadOnlyList<T> items, int batchSize)
|
|
{
|
|
if (items == null || items.Count == 0) yield break;
|
|
if (batchSize <= 0) batchSize = 1;
|
|
for (int i = 0; i < items.Count; i += batchSize)
|
|
{
|
|
int count = Math.Min(batchSize, items.Count - i);
|
|
var batch = new List<T>(count);
|
|
for (int j = 0; j < count; j++)
|
|
{
|
|
batch.Add(items[i + j]);
|
|
}
|
|
yield return batch;
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
bool verifyAfterPush = GetBoolAppSetting("TEMPLATE_VERIFY_AFTER_PUSH", true);
|
|
int delayMs = GetIntAppSetting("TEMPLATE_DEVICE_DELAY_MS", 0);
|
|
|
|
var effectiveEmployeeIds = DbToDevicePlanning.NormalizeEmployeeIds(employeeIds);
|
|
var templates = LoadTemplatesForDbToDevice(connection, effectiveEmployeeIds, logs);
|
|
|
|
int missingTemplateCount = effectiveEmployeeIds.Count - templates.Count;
|
|
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}");
|
|
|
|
logs.Add("[DB_TO_DEVICE] Creating/updating users on target...");
|
|
Program.WriteInternalLog("[DB_TO_DEVICE] Creating/updating users on target...");
|
|
|
|
foreach (var empId in effectiveEmployeeIds)
|
|
{
|
|
if (templates.ContainsKey(empId))
|
|
{
|
|
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} emp={empId} template=FOUND");
|
|
}
|
|
else
|
|
{
|
|
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.
|
|
var templatePairs = templates.ToList();
|
|
int totalTemplates = templatePairs.Count;
|
|
int batchSize = ChooseBatchSize(totalTemplates);
|
|
var batches = SplitIntoBatches(templatePairs, batchSize).ToList();
|
|
var missingTemplateSerials = effectiveEmployeeIds
|
|
.Where(id => !templates.ContainsKey(id))
|
|
.ToList();
|
|
|
|
foreach (var target in targets)
|
|
{
|
|
int success = 0;
|
|
int duplicateFace = 0;
|
|
int uploadFailed = 0;
|
|
int verifyOk = 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}");
|
|
|
|
if (DeviceProtocol.UseHttp())
|
|
{
|
|
var httpClient = HanvonHttpApiClient.ForMachine(target.Machine);
|
|
if (!httpClient.TryLogin(out string 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);
|
|
WriteDbToDeviceTargetSummary(job.Id, summary, logs);
|
|
continue;
|
|
}
|
|
|
|
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++)
|
|
{
|
|
var swBatch = Stopwatch.StartNew();
|
|
int bAttempted = 0;
|
|
int bOk = 0;
|
|
int bDuplicate = 0;
|
|
int bUploadFailed = 0;
|
|
int bVerifyOk = 0;
|
|
int bVerifyFail = 0;
|
|
int bSkipped = 0;
|
|
|
|
foreach (var kvp in batches[bi])
|
|
{
|
|
bAttempted++;
|
|
string empId = kvp.Key; // device serial / Hanvon enroll id
|
|
|
|
if (DbToDeviceSkipLogic.ShouldSkipAlreadyRegistered(registeredSerials, empId))
|
|
{
|
|
alreadyRegistered++;
|
|
summary.AlreadyRegistered++;
|
|
bSkipped++;
|
|
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;
|
|
}
|
|
|
|
if (!int.TryParse(empId, out int enrollId))
|
|
{
|
|
uploadFailed++;
|
|
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}");
|
|
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL reason=InvalidEnrollId");
|
|
continue;
|
|
}
|
|
|
|
if (hrmsEmployee != null)
|
|
{
|
|
Program.WriteInternalLog($"[DB_TO_DEVICE] employee={empId} hrms_id={hrmsEmployee.EmployeeId} name={displayName}");
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (!uploadPlan.TryPrepareFacePayload(out string prepareErr))
|
|
{
|
|
uploadFailed++;
|
|
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)
|
|
{
|
|
bool faceVerified = httpClient.VerifyDbToDeviceTemplate(
|
|
enrollId,
|
|
uploadPlan,
|
|
out string verifyFaceflag,
|
|
out string verifyPhotoUrl,
|
|
out string verifyErr);
|
|
if (!faceVerified)
|
|
{
|
|
verifyFail++;
|
|
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}");
|
|
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}");
|
|
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)
|
|
{
|
|
Thread.Sleep(delayMs);
|
|
}
|
|
|
|
swBatch.Stop();
|
|
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
|
|
{
|
|
string targetDevInfo = target.Machine.GetDeviceInfo();
|
|
var callback = new CallBack(Program.BeCalled);
|
|
|
|
for (int bi = 0; bi < batches.Count; bi++)
|
|
{
|
|
var swBatch = Stopwatch.StartNew();
|
|
int bAttempted = 0;
|
|
int bOk = 0;
|
|
int bUploadFailed = 0;
|
|
int bVerifyOk = 0;
|
|
int bVerifyFail = 0;
|
|
|
|
foreach (var kvp in batches[bi])
|
|
{
|
|
bAttempted++;
|
|
string empId = kvp.Key;
|
|
string templateStr = Encoding.UTF8.GetString(kvp.Value ?? Array.Empty<byte>());
|
|
if (!TemplateParser.TryBuildSetEmployeeCommand(templateStr, out var setCmd))
|
|
{
|
|
uploadFailed++;
|
|
bUploadFailed++;
|
|
summary.AddFailure(empId, empId, "", "Build SetEmployee failed");
|
|
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");
|
|
continue;
|
|
}
|
|
|
|
string response = "";
|
|
uint recvLen = 0;
|
|
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))
|
|
{
|
|
uploadFailed++;
|
|
bUploadFailed++;
|
|
summary.AddFailure(empId, empId, "", "SetEmployee failed rc=" + rc);
|
|
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}");
|
|
}
|
|
else
|
|
{
|
|
success++;
|
|
summary.NewlyRegistered++;
|
|
bOk++;
|
|
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=OK");
|
|
|
|
if (verifyAfterPush)
|
|
{
|
|
string verifyCmd = "GetEmployee(id=\"" + empId + "\")";
|
|
string verifyResp = "";
|
|
uint verifyRecvLen = 0;
|
|
int vrc = Program.test(targetDevInfo, targetDevInfo.Length, verifyCmd, verifyCmd.Length, ref verifyResp, ref verifyRecvLen, callback);
|
|
bool verifiedOk = vrc == 0 && verifyResp != null && verifyResp.IndexOf("success", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
if (verifiedOk)
|
|
{
|
|
verifyOk++;
|
|
bVerifyOk++;
|
|
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=OK");
|
|
}
|
|
else
|
|
{
|
|
verifyFail++;
|
|
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}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (delayMs > 0)
|
|
{
|
|
Thread.Sleep(delayMs);
|
|
}
|
|
|
|
swBatch.Stop();
|
|
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)}");
|
|
}
|
|
}
|
|
|
|
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] == "")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
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)
|
|
{
|
|
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}");
|
|
}
|
|
}
|
|
|
|
private static bool GetBoolAppSetting(string key, bool defaultValue)
|
|
{
|
|
try
|
|
{
|
|
var val = ConfigurationManager.AppSettings[key];
|
|
if (string.IsNullOrWhiteSpace(val)) return defaultValue;
|
|
return bool.TryParse(val.Trim(), out var parsed) ? parsed : defaultValue;
|
|
}
|
|
catch
|
|
{
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
private static int GetIntAppSetting(string key, int defaultValue)
|
|
{
|
|
try
|
|
{
|
|
var val = ConfigurationManager.AppSettings[key];
|
|
if (string.IsNullOrWhiteSpace(val)) return defaultValue;
|
|
return int.TryParse(val.Trim(), out var parsed) ? parsed : defaultValue;
|
|
}
|
|
catch
|
|
{
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
private void TransferDeviceToDevice(string jobId, AttendanceMachine source, List<string> employeeIds, List<(AttendanceMachine Machine, string Type)> targets, List<string> logs)
|
|
{
|
|
string sourceDevInfo = source.GetDeviceInfo();
|
|
var callback = new CallBack(Program.BeCalled);
|
|
|
|
foreach (var target in targets)
|
|
{
|
|
int ok = 0;
|
|
int fail = 0;
|
|
string targetDevInfo = target.Machine.GetDeviceInfo();
|
|
|
|
foreach (var empId in employeeIds)
|
|
{
|
|
string getCmd = "GetEmployee(id=\"" + empId + "\")";
|
|
string getResp = "";
|
|
uint recvLen = 0;
|
|
|
|
int rc = Program.test(sourceDevInfo, sourceDevInfo.Length, getCmd, getCmd.Length, ref getResp, ref recvLen, callback);
|
|
if (rc != 0 || getResp == null || getResp.IndexOf("success", StringComparison.OrdinalIgnoreCase) < 0)
|
|
{
|
|
fail++;
|
|
continue;
|
|
}
|
|
|
|
if (!TemplateParser.TryBuildSetEmployeeCommand(getResp, out var setCmd))
|
|
{
|
|
fail++;
|
|
continue;
|
|
}
|
|
|
|
string setResp = "";
|
|
uint setRecvLen = 0;
|
|
int rc2 = Program.test(targetDevInfo, targetDevInfo.Length, setCmd, setCmd.Length, ref setResp, ref setRecvLen, callback);
|
|
if (rc2 != 0 || (setResp != null && setResp.IndexOf("fail", StringComparison.OrdinalIgnoreCase) >= 0))
|
|
{
|
|
fail++;
|
|
}
|
|
else
|
|
{
|
|
ok++;
|
|
}
|
|
}
|
|
|
|
logs.Add($"[TemplateJob {jobId}] DEVICE_TO_DEVICE target={target.Machine.MachineIp} ok={ok} fail={fail}");
|
|
}
|
|
}
|
|
|
|
private static void WriteRawTemplateLog(string header, string raw)
|
|
{
|
|
try
|
|
{
|
|
string filepath = ApplicationPaths.DatedFile(ApplicationPaths.TemplateRawLogs, "TemplateRaw");
|
|
LogService.EnqueueLine(filepath, "----- " + DateTime.Now + " -----");
|
|
LogService.EnqueueLine(filepath, header);
|
|
LogService.EnqueueLine(filepath, raw ?? "");
|
|
LogService.EnqueueLine(filepath, "");
|
|
}
|
|
catch
|
|
{
|
|
// ignore logging failures
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|