From e5cbed083e1904b8425870ab4631f7659355ca3c Mon Sep 17 00:00:00 2001 From: Syed Mustafa Ahmed Naqvi Date: Fri, 4 Sep 2026 12:42:06 +0500 Subject: [PATCH] feat: enhance Hanvon HTTP client for face push and log parsing Face-only upload, timeout config, enrollid-based attendance parse. --- HanvonHttpApiClient.cs | 485 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 460 insertions(+), 25 deletions(-) diff --git a/HanvonHttpApiClient.cs b/HanvonHttpApiClient.cs index 1a80472..4d4fa3d 100644 --- a/HanvonHttpApiClient.cs +++ b/HanvonHttpApiClient.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Configuration; using System.IO; +using System.Linq; using System.Net; using System.Text; using System.Web.Script.Serialization; @@ -32,7 +33,8 @@ namespace HanvonF710XAttendanceService public static HanvonHttpApiClient ForMachine(AttendanceMachine machine) { int httpPort = GetIntSetting("DEVICE_HTTP_PORT", 80); - return new HanvonHttpApiClient(machine.MachineIp, httpPort, MachineScope.GetDevicePassword()); + int timeoutMs = MachineUserDeleteSettings.GetHttpTimeoutMs(); + return new HanvonHttpApiClient(machine.MachineIp, httpPort, MachineScope.GetDevicePassword(), timeoutMs); } public bool TryLogin(out string error) @@ -311,6 +313,78 @@ namespace HanvonF710XAttendanceService } } + public bool TrySetFacePhotoOnly(int enrollId, string faceBase64, out string error) + { + error = null; + if (string.IsNullOrWhiteSpace(faceBase64)) + { + error = "face photo is empty"; + return false; + } + + try + { + var resp = Post(new Dictionary + { + { "cmd", "setuserinfo" }, + { "password", _password }, + { "enrollid", enrollId }, + { "face", faceBase64 } + }); + if (IsSuccess(resp)) return true; + error = GetMsg(resp) ?? "setuserinfo face push failed"; + return false; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + + public bool TrySetUserWithFacePhoto(int enrollId, string name, string faceBase64, out string error) + { + error = null; + if (string.IsNullOrWhiteSpace(faceBase64)) + { + error = "face photo is empty"; + return false; + } + + 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", 8 }, + { "birthday", "" }, + { "starttime", "" }, + { "endtime", "" }, + { "userprofile", "" }, + { "face", faceBase64 } + }); + if (IsSuccess(resp)) return true; + error = GetMsg(resp) ?? "setuserinfo face 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; @@ -346,6 +420,145 @@ namespace HanvonF710XAttendanceService } } + public bool TryGetUserProfile(int enrollId, out string faceflag, out string photourl, out string name, out string error) + { + faceflag = null; + photourl = null; + name = null; + error = null; + try + { + var resp = Post(new Dictionary + { + { "cmd", "getuserinfo" }, + { "password", _password }, + { "enrollid", enrollId } + }); + if (!IsSuccess(resp)) + { + error = GetMsg(resp) ?? "getuserinfo failed"; + return false; + } + + name = GetString(resp, "name"); + faceflag = GetString(resp, "faceflag"); + photourl = GetString(resp, "photourl"); + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + + public static bool ProfileIndicatesFaceEnrolled(string faceflag, string photourl) + { + if (!string.IsNullOrWhiteSpace(photourl)) + { + return true; + } + + if (string.IsNullOrWhiteSpace(faceflag)) + { + return false; + } + + if (string.Equals(faceflag, "0", StringComparison.OrdinalIgnoreCase) + || string.Equals(faceflag, "false", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (int.TryParse(faceflag.Trim(), out int flagValue)) + { + return flagValue > 0; + } + + return true; + } + + public bool UserHasFacePhoto(int enrollId, out string faceflag, out string photourl, out string error) + { + faceflag = null; + photourl = null; + error = null; + if (!TryGetUserProfile(enrollId, out faceflag, out photourl, out _, out error)) + { + return false; + } + + if (ProfileIndicatesFaceEnrolled(faceflag, photourl)) + { + return true; + } + + error = "no face enrolled"; + return false; + } + + public bool TryUploadDbToDeviceTemplate(int enrollId, string name, DbToDeviceUploadPlan plan, out string error) + { + error = null; + if (plan == null || !plan.HasUploadPayload) + { + error = "template payload is empty"; + return false; + } + + int delayMs = GetIntSetting("TEMPLATE_DEVICE_DELAY_MS", 300); + + if (string.Equals(plan.PushMode, "face", StringComparison.OrdinalIgnoreCase)) + { + if (!TryEnsureUser(enrollId, name, out error)) + { + return false; + } + + if (delayMs > 0) + { + System.Threading.Thread.Sleep(delayMs); + } + + return TrySetFacePhotoOnly(enrollId, plan.Payload, out error); + } + + if (!TryEnsureUser(enrollId, name, out error)) + { + return false; + } + + if (delayMs > 0) + { + System.Threading.Thread.Sleep(delayMs); + } + + if (!TrySetFaceTemplateRecord(enrollId, plan.Payload, plan.PushMode, out error)) + { + return false; + } + + return true; + } + + public bool VerifyDbToDeviceTemplate(int enrollId, DbToDeviceUploadPlan plan, out string faceflag, out string photourl, out string error) + { + faceflag = null; + photourl = null; + error = null; + if (plan != null && plan.UseProfileVerification) + { + return UserHasFacePhoto(enrollId, out faceflag, out photourl, out error); + } + + if (UserHasFaceTemplate(enrollId, out error)) + { + return true; + } + + return false; + } + public bool TryRestoreUserWithFaceTemplate(int enrollId, string name, string templatePayload, string pushMode, out string error) { error = null; @@ -383,33 +596,255 @@ namespace HanvonF710XAttendanceService return TryGetFaceRecord(enrollId, out _, out _, out error); } - private Dictionary Post(Dictionary payload) + public HttpExchangeResult PostDetailed(Dictionary payload) { - string body = _json.Serialize(payload); - var req = (HttpWebRequest)WebRequest.Create(_baseUrl); - req.Method = "POST"; - req.ContentType = "application/json; charset=utf-8"; - req.Timeout = _timeoutMs; - req.ReadWriteTimeout = _timeoutMs; - req.KeepAlive = false; - byte[] bytes = Encoding.UTF8.GetBytes(body); - req.ContentLength = bytes.Length; - using (var stream = req.GetRequestStream()) + var result = new HttpExchangeResult { - stream.Write(bytes, 0, bytes.Length); + Url = _baseUrl, + Method = "POST", + RequestContentType = "application/json; charset=utf-8" + }; + + string body = _json.Serialize(payload ?? new Dictionary()); + result.RequestBodySanitized = HanvonHttpDiagnostics.SanitizeJsonBody(body); + + try + { + var req = (HttpWebRequest)WebRequest.Create(_baseUrl); + req.Method = "POST"; + req.ContentType = result.RequestContentType; + req.Timeout = _timeoutMs; + req.ReadWriteTimeout = _timeoutMs; + req.KeepAlive = false; + byte[] bytes = Encoding.UTF8.GetBytes(body); + req.ContentLength = bytes.Length; + using (var stream = req.GetRequestStream()) + { + stream.Write(bytes, 0, bytes.Length); + } + + using (var resp = (HttpWebResponse)req.GetResponse()) + using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.UTF8)) + { + result.StatusCode = (int)resp.StatusCode; + result.ResponseHeaders = FormatResponseHeaders(resp); + string text = reader.ReadToEnd(); + result.ResponseBody = HanvonHttpDiagnostics.SanitizeJsonBody(text); + result.ParsedResponse = ParseResponseBody(text); + } + } + catch (WebException wex) + { + result.Error = wex.Message; + if (wex.Response is HttpWebResponse errResp) + { + result.StatusCode = (int)errResp.StatusCode; + result.ResponseHeaders = FormatResponseHeaders(errResp); + using (var reader = new StreamReader(errResp.GetResponseStream() ?? Stream.Null, Encoding.UTF8)) + { + string text = reader.ReadToEnd(); + result.ResponseBody = HanvonHttpDiagnostics.SanitizeJsonBody(text); + result.ParsedResponse = ParseResponseBody(text); + } + } + } + catch (Exception ex) + { + result.Error = ex.Message; } - using (var resp = (HttpWebResponse)req.GetResponse()) - using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.UTF8)) + return result; + } + + public void DiagnoseDbToDeviceEmployee( + int enrollId, + string empId, + string displayName, + string templatePayload, + string pushMode, + TextWriter logWriter) + { + if (logWriter == null) throw new ArgumentNullException(nameof(logWriter)); + + logWriter.WriteLine($"[DB_TO_DEVICE] DIAG emp={empId} enrollid={enrollId} target={_baseUrl}"); + logWriter.WriteLine($"[DB_TO_DEVICE] DIAG template {HanvonHttpDiagnostics.DescribeTemplatePayload(templatePayload, pushMode)}"); + + var login = PostDetailed(new Dictionary { - string text = reader.ReadToEnd(); - if (string.IsNullOrWhiteSpace(text)) - { - return new Dictionary(); - } - return _json.Deserialize>(text) - ?? new Dictionary(); + { "cmd", "login" }, + { "username", MachineScope.GetDeviceUsername() }, + { "password", _password }, + { "rememberMe", false } + }); + HanvonHttpDiagnostics.WriteExchange(logWriter, "LOGIN", empId, login); + + var listBefore = PostDetailed(new Dictionary + { + { "cmd", "getuserlist" }, + { "password", _password }, + { "stn", 1 } + }); + HanvonHttpDiagnostics.WriteExchange(logWriter, "USERLIST_BEFORE", empId, listBefore); + + var profileBefore = PostDetailed(new Dictionary + { + { "cmd", "getuserinfo" }, + { "password", _password }, + { "enrollid", enrollId } + }); + HanvonHttpDiagnostics.WriteExchange(logWriter, "VERIFY_PROFILE_BEFORE", empId, profileBefore); + + var recordBefore = PostDetailed(new Dictionary + { + { "cmd", "getuserinfo" }, + { "password", _password }, + { "enrollid", enrollId }, + { "backupnum", GetIntSetting("DEVICE_FACE_BACKUPNUM", 50) } + }); + HanvonHttpDiagnostics.WriteExchange(logWriter, "VERIFY_RECORD_BEFORE", empId, recordBefore); + + var ensurePayload = new Dictionary + { + { "cmd", "setuserinfo" }, + { "password", _password }, + { "enrollid", enrollId }, + { "name", displayName ?? enrollId.ToString() }, + { "department", "" }, + { "shiftid", 1 }, + { "admin", 0 }, + { "pwd", 0 }, + { "card", 0 }, + { "zoneid", 0 }, + { "groupid", 0 }, + { "access_times", 0 }, + { "verifymode", 0 }, + { "birthday", "" }, + { "starttime", "" }, + { "endtime", "" }, + { "userprofile", "" } + }; + var ensure = PostDetailed(ensurePayload); + HanvonHttpDiagnostics.WriteExchange(logWriter, "SEND ensure_user setuserinfo", empId, ensure); + + var templatePayloadReq = new Dictionary + { + { "cmd", "setuserinfo" }, + { "password", _password }, + { "enrollid", enrollId } + }; + string templateField; + if (string.Equals(pushMode, "face", StringComparison.OrdinalIgnoreCase)) + { + templatePayloadReq["face"] = templatePayload; + templateField = "face"; } + else + { + templatePayloadReq["backupnum"] = GetIntSetting("DEVICE_FACE_BACKUPNUM", 50); + templatePayloadReq["record"] = templatePayload; + templateField = "record"; + } + logWriter.WriteLine($"[DB_TO_DEVICE] emp={empId} SEND template_field={templateField} enrollid={enrollId} employee_number={displayName}"); + var push = PostDetailed(templatePayloadReq); + HanvonHttpDiagnostics.WriteExchange(logWriter, "SEND template setuserinfo", empId, push); + + int delayMs = GetIntSetting("TEMPLATE_DEVICE_DELAY_MS", 300); + if (delayMs > 0) System.Threading.Thread.Sleep(delayMs); + + var profileAfter = PostDetailed(new Dictionary + { + { "cmd", "getuserinfo" }, + { "password", _password }, + { "enrollid", enrollId } + }); + HanvonHttpDiagnostics.WriteExchange(logWriter, "VERIFY profile getuserinfo (no backupnum)", empId, profileAfter); + + var recordAfter = PostDetailed(new Dictionary + { + { "cmd", "getuserinfo" }, + { "password", _password }, + { "enrollid", enrollId }, + { "backupnum", GetIntSetting("DEVICE_FACE_BACKUPNUM", 50) } + }); + HanvonHttpDiagnostics.WriteExchange(logWriter, "VERIFY record getuserinfo backupnum=50", empId, recordAfter); + + var listAfter = PostDetailed(new Dictionary + { + { "cmd", "getuserlist" }, + { "password", _password }, + { "stn", 1 } + }); + HanvonHttpDiagnostics.WriteExchange(logWriter, "USERLIST_AFTER", empId, listAfter); + + var users = GetUserList(out string listErr); + var match = users.FirstOrDefault(u => + string.Equals(u.DeviceUserId, empId, StringComparison.OrdinalIgnoreCase) + || string.Equals(u.SerialNumber, empId, StringComparison.OrdinalIgnoreCase) + || string.Equals(u.Name, empId, StringComparison.OrdinalIgnoreCase)); + + string recordValue = GetString(recordAfter.ParsedResponse, "record"); + HanvonHttpDiagnostics.WriteResultSummary( + logWriter, + empId, + match != null, + match?.DeviceUserId, + match?.Name, + null, + IsSuccess(profileAfter.ParsedResponse), + GetString(profileAfter.ParsedResponse, "faceflag"), + GetString(profileAfter.ParsedResponse, "photourl"), + !string.IsNullOrWhiteSpace(recordValue), + recordValue?.Length ?? 0, + HanvonHttpDiagnostics.DescribeTemplatePayload(templatePayload, pushMode)); + + logWriter.WriteLine($"[DB_TO_DEVICE] emp={empId} SEND_OK_MEANS ensure_result={IsSuccess(ensure.ParsedResponse)} template_result={IsSuccess(push.ParsedResponse)} (Hanvon JSON result=true only, not proof of stored face)"); + logWriter.WriteLine($"[DB_TO_DEVICE] emp={empId} VERIFY_FAIL_REASON={(string.IsNullOrWhiteSpace(recordValue) ? "getuserinfo backupnum=50 returned empty record field" : "record present")}"); + if (!string.IsNullOrWhiteSpace(listErr)) + { + logWriter.WriteLine($"[DB_TO_DEVICE] getuserlist_err={listErr}"); + } + } + + private Dictionary Post(Dictionary payload) + { + var detailed = PostDetailed(payload); + if (detailed.ParsedResponse != null && detailed.ParsedResponse.Count > 0) + { + return detailed.ParsedResponse; + } + if (!string.IsNullOrWhiteSpace(detailed.Error) && detailed.StatusCode == 0) + { + throw new IOException(detailed.Error); + } + return detailed.ParsedResponse ?? new Dictionary(); + } + + private static Dictionary ParseResponseBody(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return new Dictionary(); + } + try + { + return new JavaScriptSerializer { MaxJsonLength = int.MaxValue } + .Deserialize>(text) ?? new Dictionary(); + } + catch + { + return new Dictionary(); + } + } + + private static string FormatResponseHeaders(HttpWebResponse resp) + { + if (resp?.Headers == null) return ""; + var pairs = new List(); + foreach (string key in resp.Headers.AllKeys) + { + pairs.Add(key + "=" + resp.Headers[key]); + } + return string.Join("; ", pairs); } private static bool IsSuccess(Dictionary resp) @@ -463,12 +898,12 @@ namespace HanvonF710XAttendanceService string name = GetString(row, "name") ?? ""; string timeStr = GetString(row, "time") ?? ""; if (!DateTime.TryParse(timeStr, out DateTime time)) return; - string acNo = !string.IsNullOrWhiteSpace(name) ? name.Trim() : enroll.Trim(); + string acNo = enroll.Trim(); if (string.IsNullOrWhiteSpace(acNo)) return; list.Add(new HttpLogRecord { - EnrollId = enroll, - Name = name, + EnrollId = enroll.Trim(), + Name = name.Trim(), AcNo = acNo, CheckTime = time, Mode = GetInt(row, "mode"),