From 5424d0abb82d00fec4c7172a2139553c450bfa62 Mon Sep 17 00:00:00 2001 From: Syed Mustafa Ahmed Naqvi Date: Fri, 24 Jul 2026 09:44:29 +0500 Subject: [PATCH] Fix DB_TO_DEVICE HTTP restore so deleted users get face templates on device. Use EmpIds from config when restoring from DB, and push templates via a two-step setuserinfo (create user, then backupnum/record or face) matching the F710X web API. Co-authored-by: Cursor --- App.config | 5 +- AttendanceMachineUserDAO.cs | 18 +++ DeviceSettings.xml | 10 +- HanvonHttpApiClient.cs | 152 ++++++++++++++++++++++++ TemplateTransferRunner.cs | 228 +++++++++++++++++++++++++++--------- 5 files changed, 351 insertions(+), 62 deletions(-) diff --git a/App.config b/App.config index 8abbea1..0062c48 100644 --- a/App.config +++ b/App.config @@ -6,12 +6,13 @@ providerName="MySql.Data.MySqlClient" /> - - + + + diff --git a/AttendanceMachineUserDAO.cs b/AttendanceMachineUserDAO.cs index 731cac3..999ebf6 100644 --- a/AttendanceMachineUserDAO.cs +++ b/AttendanceMachineUserDAO.cs @@ -104,6 +104,24 @@ namespace HanvonF710XAttendanceService } } + public string GetEmployeeNameForMachine(MySqlConnection connection, string machineId, string serialNumber) + { + if (string.IsNullOrWhiteSpace(machineId) || string.IsNullOrWhiteSpace(serialNumber)) return null; + const string query = + "SELECT employee_name FROM hrms.attendance_machine_user " + + "WHERE machine_id = @MachineId AND serial_number = @SerialNumber AND is_deleted = 0 LIMIT 1"; + using (var cmd = new MySqlCommand(query, connection)) + { + cmd.CommandTimeout = 60; + cmd.Parameters.AddWithValue("@MachineId", machineId); + cmd.Parameters.AddWithValue("@SerialNumber", serialNumber.Trim()); + var result = cmd.ExecuteScalar(); + if (result == null || result == DBNull.Value) return null; + var name = result.ToString()?.Trim(); + return string.IsNullOrWhiteSpace(name) ? null : name; + } + } + private List getDBUsers(MySqlDataReader reader) { List DbUsers = new List(); diff --git a/DeviceSettings.xml b/DeviceSettings.xml index d4edda9..96b2915 100644 --- a/DeviceSettings.xml +++ b/DeviceSettings.xml @@ -1,13 +1,13 @@ - + - + - + SourceIp="192.168.90.89" + EmpIds="15399,17003"> + diff --git a/HanvonHttpApiClient.cs b/HanvonHttpApiClient.cs index 0b6751e..1a80472 100644 --- a/HanvonHttpApiClient.cs +++ b/HanvonHttpApiClient.cs @@ -196,6 +196,121 @@ namespace HanvonF710XAttendanceService } } + public static string NormalizeFaceTemplatePayload(byte[] blob, out string pushMode) + { + pushMode = "record"; + if (blob == null || blob.Length == 0) return null; + + if (blob.Length >= 3 && blob[0] == 0xFF && blob[1] == 0xD8 && blob[2] == 0xFF) + { + pushMode = "face"; + return Convert.ToBase64String(blob); + } + + string text = Encoding.UTF8.GetString(blob).Trim(); + if (string.IsNullOrWhiteSpace(text)) return null; + + if (text.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + { + int comma = text.IndexOf(','); + if (comma >= 0) text = text.Substring(comma + 1); + } + + text = text.Replace("\r", "").Replace("\n", "").Trim(); + if (string.IsNullOrWhiteSpace(text)) return null; + + try + { + byte[] decoded = Convert.FromBase64String(text); + if (decoded.Length >= 3 && decoded[0] == 0xFF && decoded[1] == 0xD8 && decoded[2] == 0xFF) + { + pushMode = "face"; + } + } + catch + { + // Biometric templates from getuserinfo may not decode as standard base64. + } + + return text; + } + + public bool TryEnsureUser(int enrollId, string name, out string error) + { + error = null; + try + { + var resp = Post(new Dictionary + { + { "cmd", "setuserinfo" }, + { "password", _password }, + { "enrollid", enrollId }, + { "name", name ?? enrollId.ToString() }, + { "department", "" }, + { "shiftid", 1 }, + { "admin", 0 }, + { "pwd", 0 }, + { "card", 0 }, + { "zoneid", 0 }, + { "groupid", 0 }, + { "access_times", 0 }, + { "verifymode", 0 }, + { "birthday", "" }, + { "starttime", "" }, + { "endtime", "" }, + { "userprofile", "" } + }); + if (IsSuccess(resp)) return true; + error = GetMsg(resp) ?? "setuserinfo failed"; + return false; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + + public bool TrySetFaceTemplateRecord(int enrollId, string templatePayload, string pushMode, out string error) + { + error = null; + if (string.IsNullOrWhiteSpace(templatePayload)) + { + error = "template record is empty"; + return false; + } + + try + { + var payload = new Dictionary + { + { "cmd", "setuserinfo" }, + { "password", _password }, + { "enrollid", enrollId } + }; + + if (string.Equals(pushMode, "face", StringComparison.OrdinalIgnoreCase)) + { + payload["face"] = templatePayload; + } + else + { + payload["backupnum"] = GetIntSetting("DEVICE_FACE_BACKUPNUM", 50); + payload["record"] = templatePayload; + } + + var resp = Post(payload); + if (IsSuccess(resp)) return true; + error = GetMsg(resp) ?? "setuserinfo template push failed"; + return false; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + public bool TryGetFaceRecord(int enrollId, out string recordBase64, out string name, out string error) { recordBase64 = null; @@ -231,6 +346,43 @@ namespace HanvonF710XAttendanceService } } + public bool TryRestoreUserWithFaceTemplate(int enrollId, string name, string templatePayload, string pushMode, out string error) + { + error = null; + if (string.IsNullOrWhiteSpace(templatePayload)) + { + error = "template record is empty"; + return false; + } + + if (!TryEnsureUser(enrollId, name, out error)) + { + return false; + } + + int delayMs = GetIntSetting("TEMPLATE_DEVICE_DELAY_MS", 300); + if (delayMs > 0) + { + System.Threading.Thread.Sleep(delayMs); + } + + if (!TrySetFaceTemplateRecord(enrollId, templatePayload, pushMode, out error)) + { + string altMode = string.Equals(pushMode, "face", StringComparison.OrdinalIgnoreCase) ? "record" : "face"; + if (!TrySetFaceTemplateRecord(enrollId, templatePayload, altMode, out error)) + { + return false; + } + } + + return true; + } + + public bool UserHasFaceTemplate(int enrollId, out string error) + { + return TryGetFaceRecord(enrollId, out _, out _, out error); + } + private Dictionary Post(Dictionary payload) { string body = _json.Serialize(payload); diff --git a/TemplateTransferRunner.cs b/TemplateTransferRunner.cs index 6f1e900..06c07be 100644 --- a/TemplateTransferRunner.cs +++ b/TemplateTransferRunner.cs @@ -128,24 +128,50 @@ namespace HanvonF710XAttendanceService continue; } - if (!TryGetEmployeeIdsFromDevice(sourceMachine, out var employeeIds, logs, job.Id)) + if (mode == TemplateTransferMode.DB_TO_DEVICE) + { + List idsForTransfer; + 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; + } + + if (!TryGetEmployeeIdsFromDevice(sourceMachine, out var deviceEmployeeIds, logs, job.Id)) { continue; } - if (employeeIds.Count == 0) + if (deviceEmployeeIds.Count == 0) { logs.Add($"[TemplateJob {job.Id}] Source device returned 0 employee IDs."); continue; } - if (mode == TemplateTransferMode.DB_TO_DEVICE) + if (mode == TemplateTransferMode.DEVICE_TO_DEVICE) { - TransferDbToDevice(job, employeeIds, targetMachines, connection, logs); - } - else if (mode == TemplateTransferMode.DEVICE_TO_DEVICE) - { - TransferDeviceToDevice(job.Id, sourceMachine, employeeIds, targetMachines, logs); + TransferDeviceToDevice(job.Id, sourceMachine, deviceEmployeeIds, targetMachines, logs); } } } @@ -678,9 +704,6 @@ namespace HanvonF710XAttendanceService foreach (var target in targets) { - string targetDevInfo = target.Machine.GetDeviceInfo(); - var callback = new CallBack(Program.BeCalled); - int ok = 0; int fail = 0; int verifyOk = 0; @@ -688,53 +711,71 @@ namespace HanvonF710XAttendanceService logs.Add($"[TemplateTransfer] job={job.Id} target_ip={target.Machine.MachineIp} total_templates={totalTemplates} batch_size={batchSize} total_batches={batches.Count}"); - for (int bi = 0; bi < batches.Count; bi++) + if (DeviceProtocol.UseHttp()) { - var swBatch = Stopwatch.StartNew(); - int bAttempted = 0; - int bOk = 0; - int bFail = 0; - int bVerifyOk = 0; - int bVerifyFail = 0; - - foreach (var kvp in batches[bi]) + var httpClient = HanvonHttpApiClient.ForMachine(target.Machine); + if (!httpClient.TryLogin(out string loginErr)) { - bAttempted++; - string empId = kvp.Key; - string templateStr = Encoding.UTF8.GetString(kvp.Value ?? Array.Empty()); - if (!TemplateParser.TryBuildSetEmployeeCommand(templateStr, out var setCmd)) - { - fail++; - bFail++; - 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; - } + 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); + 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)) - { - fail++; - bFail++; - 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 + var machineUserDao = new AttendanceMachineUserDAO(); + + for (int bi = 0; bi < batches.Count; bi++) + { + var swBatch = Stopwatch.StartNew(); + int bAttempted = 0; + int bOk = 0; + int bFail = 0; + int bVerifyOk = 0; + int bVerifyFail = 0; + + foreach (var kvp in batches[bi]) { + bAttempted++; + string empId = kvp.Key; + byte[] templateBlob = kvp.Value ?? Array.Empty(); + string recordBase64 = HanvonHttpApiClient.NormalizeFaceTemplatePayload(templateBlob, out string pushMode); + if (string.IsNullOrWhiteSpace(recordBase64)) + { + fail++; + bFail++; + logs.Add($"[TemplateJob {job.Id}] Empty template payload. emp={empId} blob_bytes={templateBlob.Length} target={target.Machine.MachineIp}"); + Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL reason=EmptyTemplate blob_bytes={templateBlob.Length}"); + continue; + } + + if (!int.TryParse(empId, out int enrollId)) + { + fail++; + bFail++; + 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; + } + + string displayName = machineUserDao.GetEmployeeNameForMachine(connection, target.Machine.MachineId, empId) + ?? empId; + + if (!httpClient.TryRestoreUserWithFaceTemplate(enrollId, displayName, recordBase64, pushMode, out string pushErr)) + { + fail++; + 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}"); + continue; + } + ok++; bOk++; + logs.Add($"[TemplateJob {job.Id}] HTTP template push ok. emp={empId} name={displayName} mode={pushMode} payload_len={recordBase64.Length} target={target.Machine.MachineIp}"); 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 success = vrc == 0 && verifyResp != null && verifyResp.IndexOf("success", StringComparison.OrdinalIgnoreCase) >= 0; - if (success) + if (httpClient.UserHasFaceTemplate(enrollId, out string verifyErr)) { verifyOk++; bVerifyOk++; @@ -744,19 +785,96 @@ namespace HanvonF710XAttendanceService { verifyFail++; bVerifyFail++; - Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=FAIL rc={vrc}"); + logs.Add($"[TemplateJob {job.Id}] HTTP verify failed after push. emp={empId} target={target.Machine.MachineIp} err={verifyErr}"); + Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=FAIL err={verifyErr}"); } } } - } - if (delayMs > 0) + 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} ok={bOk} fail={bFail} 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++) { - System.Threading.Thread.Sleep(delayMs); - } + var swBatch = Stopwatch.StartNew(); + int bAttempted = 0; + int bOk = 0; + int bFail = 0; + int bVerifyOk = 0; + int bVerifyFail = 0; - 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)}"); + foreach (var kvp in batches[bi]) + { + bAttempted++; + string empId = kvp.Key; + string templateStr = Encoding.UTF8.GetString(kvp.Value ?? Array.Empty()); + if (!TemplateParser.TryBuildSetEmployeeCommand(templateStr, out var setCmd)) + { + fail++; + bFail++; + 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)) + { + fail++; + bFail++; + 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 + { + ok++; + 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 success = vrc == 0 && verifyResp != null && verifyResp.IndexOf("success", StringComparison.OrdinalIgnoreCase) >= 0; + if (success) + { + verifyOk++; + bVerifyOk++; + Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=OK"); + } + else + { + verifyFail++; + bVerifyFail++; + 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} ok={bOk} fail={bFail} verify_ok={bVerifyOk} verify_fail={bVerifyFail} duration_sec={Math.Round(swBatch.Elapsed.TotalSeconds, 1)}"); + } } if (verifyAfterPush)