feat: enhance Hanvon HTTP client for face push and log parsing

Face-only upload, timeout config, enrollid-based attendance parse.
main
SYED MUSTUFA AHMED NAQVI 2026-09-04 12:42:06 +05:00
parent 2719e53338
commit e5cbed083e
1 changed files with 460 additions and 25 deletions

View File

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Configuration; using System.Configuration;
using System.IO; using System.IO;
using System.Linq;
using System.Net; using System.Net;
using System.Text; using System.Text;
using System.Web.Script.Serialization; using System.Web.Script.Serialization;
@ -32,7 +33,8 @@ namespace HanvonF710XAttendanceService
public static HanvonHttpApiClient ForMachine(AttendanceMachine machine) public static HanvonHttpApiClient ForMachine(AttendanceMachine machine)
{ {
int httpPort = GetIntSetting("DEVICE_HTTP_PORT", 80); 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) 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<string, object>
{
{ "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<string, object>
{
{ "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) public bool TryGetFaceRecord(int enrollId, out string recordBase64, out string name, out string error)
{ {
recordBase64 = null; 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<string, object>
{
{ "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) public bool TryRestoreUserWithFaceTemplate(int enrollId, string name, string templatePayload, string pushMode, out string error)
{ {
error = null; error = null;
@ -383,12 +596,23 @@ namespace HanvonF710XAttendanceService
return TryGetFaceRecord(enrollId, out _, out _, out error); return TryGetFaceRecord(enrollId, out _, out _, out error);
} }
private Dictionary<string, object> Post(Dictionary<string, object> payload) public HttpExchangeResult PostDetailed(Dictionary<string, object> payload)
{
var result = new HttpExchangeResult
{
Url = _baseUrl,
Method = "POST",
RequestContentType = "application/json; charset=utf-8"
};
string body = _json.Serialize(payload ?? new Dictionary<string, object>());
result.RequestBodySanitized = HanvonHttpDiagnostics.SanitizeJsonBody(body);
try
{ {
string body = _json.Serialize(payload);
var req = (HttpWebRequest)WebRequest.Create(_baseUrl); var req = (HttpWebRequest)WebRequest.Create(_baseUrl);
req.Method = "POST"; req.Method = "POST";
req.ContentType = "application/json; charset=utf-8"; req.ContentType = result.RequestContentType;
req.Timeout = _timeoutMs; req.Timeout = _timeoutMs;
req.ReadWriteTimeout = _timeoutMs; req.ReadWriteTimeout = _timeoutMs;
req.KeepAlive = false; req.KeepAlive = false;
@ -402,14 +626,225 @@ namespace HanvonF710XAttendanceService
using (var resp = (HttpWebResponse)req.GetResponse()) using (var resp = (HttpWebResponse)req.GetResponse())
using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.UTF8)) using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.UTF8))
{ {
result.StatusCode = (int)resp.StatusCode;
result.ResponseHeaders = FormatResponseHeaders(resp);
string text = reader.ReadToEnd(); 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;
}
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, object>
{
{ "cmd", "login" },
{ "username", MachineScope.GetDeviceUsername() },
{ "password", _password },
{ "rememberMe", false }
});
HanvonHttpDiagnostics.WriteExchange(logWriter, "LOGIN", empId, login);
var listBefore = PostDetailed(new Dictionary<string, object>
{
{ "cmd", "getuserlist" },
{ "password", _password },
{ "stn", 1 }
});
HanvonHttpDiagnostics.WriteExchange(logWriter, "USERLIST_BEFORE", empId, listBefore);
var profileBefore = PostDetailed(new Dictionary<string, object>
{
{ "cmd", "getuserinfo" },
{ "password", _password },
{ "enrollid", enrollId }
});
HanvonHttpDiagnostics.WriteExchange(logWriter, "VERIFY_PROFILE_BEFORE", empId, profileBefore);
var recordBefore = PostDetailed(new Dictionary<string, object>
{
{ "cmd", "getuserinfo" },
{ "password", _password },
{ "enrollid", enrollId },
{ "backupnum", GetIntSetting("DEVICE_FACE_BACKUPNUM", 50) }
});
HanvonHttpDiagnostics.WriteExchange(logWriter, "VERIFY_RECORD_BEFORE", empId, recordBefore);
var ensurePayload = new Dictionary<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "cmd", "getuserinfo" },
{ "password", _password },
{ "enrollid", enrollId }
});
HanvonHttpDiagnostics.WriteExchange(logWriter, "VERIFY profile getuserinfo (no backupnum)", empId, profileAfter);
var recordAfter = PostDetailed(new Dictionary<string, object>
{
{ "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<string, object>
{
{ "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<string, object> Post(Dictionary<string, object> 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<string, object>();
}
private static Dictionary<string, object> ParseResponseBody(string text)
{
if (string.IsNullOrWhiteSpace(text)) if (string.IsNullOrWhiteSpace(text))
{ {
return new Dictionary<string, object>(); return new Dictionary<string, object>();
} }
return _json.Deserialize<Dictionary<string, object>>(text) try
?? new Dictionary<string, object>(); {
return new JavaScriptSerializer { MaxJsonLength = int.MaxValue }
.Deserialize<Dictionary<string, object>>(text) ?? new Dictionary<string, object>();
} }
catch
{
return new Dictionary<string, object>();
}
}
private static string FormatResponseHeaders(HttpWebResponse resp)
{
if (resp?.Headers == null) return "";
var pairs = new List<string>();
foreach (string key in resp.Headers.AllKeys)
{
pairs.Add(key + "=" + resp.Headers[key]);
}
return string.Join("; ", pairs);
} }
private static bool IsSuccess(Dictionary<string, object> resp) private static bool IsSuccess(Dictionary<string, object> resp)
@ -463,12 +898,12 @@ namespace HanvonF710XAttendanceService
string name = GetString(row, "name") ?? ""; string name = GetString(row, "name") ?? "";
string timeStr = GetString(row, "time") ?? ""; string timeStr = GetString(row, "time") ?? "";
if (!DateTime.TryParse(timeStr, out DateTime time)) return; 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; if (string.IsNullOrWhiteSpace(acNo)) return;
list.Add(new HttpLogRecord list.Add(new HttpLogRecord
{ {
EnrollId = enroll, EnrollId = enroll.Trim(),
Name = name, Name = name.Trim(),
AcNo = acNo, AcNo = acNo,
CheckTime = time, CheckTime = time,
Mode = GetInt(row, "mode"), Mode = GetInt(row, "mode"),