549 lines
19 KiB
C#
549 lines
19 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Configuration;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Text;
|
|
using System.Web.Script.Serialization;
|
|
|
|
namespace HanvonF710XAttendanceService
|
|
{
|
|
/// <summary>
|
|
/// F710X / AI Biometric web API client (POST http://{ip}/api).
|
|
/// Auth: each request includes the device web UI password.
|
|
/// </summary>
|
|
internal sealed class HanvonHttpApiClient
|
|
{
|
|
private readonly string _baseUrl;
|
|
private readonly string _password;
|
|
private readonly int _timeoutMs;
|
|
private readonly JavaScriptSerializer _json = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
|
|
|
|
public HanvonHttpApiClient(string machineIp, int httpPort = 80, string password = null, int timeoutMs = 60000)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(machineIp)) throw new ArgumentException("machineIp required");
|
|
int port = httpPort > 0 ? httpPort : 80;
|
|
_baseUrl = "http://" + machineIp.Trim() + (port == 80 ? "" : ":" + port) + "/api";
|
|
_password = password ?? MachineScope.GetDevicePassword() ?? "";
|
|
_timeoutMs = timeoutMs > 0 ? timeoutMs : 60000;
|
|
}
|
|
|
|
public static HanvonHttpApiClient ForMachine(AttendanceMachine machine)
|
|
{
|
|
int httpPort = GetIntSetting("DEVICE_HTTP_PORT", 80);
|
|
return new HanvonHttpApiClient(machine.MachineIp, httpPort, MachineScope.GetDevicePassword());
|
|
}
|
|
|
|
public bool TryLogin(out string error)
|
|
{
|
|
error = null;
|
|
try
|
|
{
|
|
var resp = Post(new Dictionary<string, object>
|
|
{
|
|
{ "cmd", "login" },
|
|
{ "username", MachineScope.GetDeviceUsername() },
|
|
{ "password", _password },
|
|
{ "rememberMe", false }
|
|
});
|
|
if (IsSuccess(resp)) return true;
|
|
error = GetMsg(resp) ?? "login failed";
|
|
return false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = ex.Message;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public List<HttpLogRecord> GetLogs(DateTime? from, DateTime? to, out string error)
|
|
{
|
|
error = null;
|
|
var all = new List<HttpLogRecord>();
|
|
try
|
|
{
|
|
int index = 0;
|
|
for (int guard = 0; guard < 10000; guard++)
|
|
{
|
|
var req = new Dictionary<string, object>
|
|
{
|
|
{ "cmd", "getlog" },
|
|
{ "password", _password },
|
|
{ "index", index }
|
|
};
|
|
if (from.HasValue) req["from"] = from.Value.ToString("yyyy-MM-dd HH:mm:ss");
|
|
if (to.HasValue) req["to"] = to.Value.ToString("yyyy-MM-dd HH:mm:ss");
|
|
|
|
var resp = Post(req);
|
|
if (!IsSuccess(resp))
|
|
{
|
|
error = GetMsg(resp) ?? "getlog failed";
|
|
return all;
|
|
}
|
|
|
|
var batch = ParseLogRecords(resp);
|
|
all.AddRange(batch);
|
|
|
|
int count = GetInt(resp, "count");
|
|
int toIdx = GetInt(resp, "to");
|
|
if (count <= 0 || toIdx <= 0 || count <= toIdx || batch.Count == 0)
|
|
{
|
|
break;
|
|
}
|
|
index = toIdx + 1;
|
|
}
|
|
return all;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = ex.Message;
|
|
return all;
|
|
}
|
|
}
|
|
|
|
public List<HttpUserRecord> GetUserList(out string error)
|
|
{
|
|
error = null;
|
|
var all = new List<HttpUserRecord>();
|
|
try
|
|
{
|
|
int stn = 1;
|
|
for (int guard = 0; guard < 10000; guard++)
|
|
{
|
|
var resp = Post(new Dictionary<string, object>
|
|
{
|
|
{ "cmd", "getuserlist" },
|
|
{ "password", _password },
|
|
{ "stn", stn }
|
|
});
|
|
if (!IsSuccess(resp))
|
|
{
|
|
error = GetMsg(resp) ?? "getuserlist failed";
|
|
return all;
|
|
}
|
|
|
|
var batch = ParseUserRecords(resp);
|
|
all.AddRange(batch);
|
|
|
|
int count = GetInt(resp, "count");
|
|
int toIdx = GetInt(resp, "to");
|
|
if (count <= 0 || batch.Count == 0 || (toIdx > 0 && toIdx >= count))
|
|
{
|
|
break;
|
|
}
|
|
stn = 0;
|
|
}
|
|
return all;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = ex.Message;
|
|
return all;
|
|
}
|
|
}
|
|
|
|
public bool DeleteUsers(IEnumerable<string> enrollIds, out string error)
|
|
{
|
|
error = null;
|
|
try
|
|
{
|
|
var list = new List<object>();
|
|
foreach (var id in enrollIds)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(id)) continue;
|
|
if (int.TryParse(id.Trim(), out int n)) list.Add(n);
|
|
else list.Add(id.Trim());
|
|
}
|
|
if (list.Count == 0) return true;
|
|
|
|
var resp = Post(new Dictionary<string, object>
|
|
{
|
|
{ "cmd", "deleteusers" },
|
|
{ "password", _password },
|
|
{ "list", list }
|
|
});
|
|
if (IsSuccess(resp)) return true;
|
|
error = GetMsg(resp) ?? "deleteusers failed";
|
|
return false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = ex.Message;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public bool CleanLog(out string error)
|
|
{
|
|
error = null;
|
|
try
|
|
{
|
|
var resp = Post(new Dictionary<string, object>
|
|
{
|
|
{ "cmd", "cleanlog" },
|
|
{ "password", _password }
|
|
});
|
|
if (IsSuccess(resp)) return true;
|
|
error = GetMsg(resp) ?? "cleanlog failed";
|
|
return false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = ex.Message;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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<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", 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<string, object>
|
|
{
|
|
{ "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;
|
|
name = null;
|
|
error = null;
|
|
try
|
|
{
|
|
var resp = Post(new Dictionary<string, object>
|
|
{
|
|
{ "cmd", "getuserinfo" },
|
|
{ "password", _password },
|
|
{ "enrollid", enrollId },
|
|
{ "backupnum", GetIntSetting("DEVICE_FACE_BACKUPNUM", 50) }
|
|
});
|
|
if (!IsSuccess(resp))
|
|
{
|
|
error = GetMsg(resp) ?? "getuserinfo failed";
|
|
return false;
|
|
}
|
|
name = GetString(resp, "name");
|
|
recordBase64 = GetString(resp, "record");
|
|
if (string.IsNullOrWhiteSpace(recordBase64))
|
|
{
|
|
error = "no face/record payload";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = ex.Message;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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<string, object> Post(Dictionary<string, object> 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())
|
|
{
|
|
stream.Write(bytes, 0, bytes.Length);
|
|
}
|
|
|
|
using (var resp = (HttpWebResponse)req.GetResponse())
|
|
using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.UTF8))
|
|
{
|
|
string text = reader.ReadToEnd();
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return new Dictionary<string, object>();
|
|
}
|
|
return _json.Deserialize<Dictionary<string, object>>(text)
|
|
?? new Dictionary<string, object>();
|
|
}
|
|
}
|
|
|
|
private static bool IsSuccess(Dictionary<string, object> resp)
|
|
{
|
|
if (resp == null) return false;
|
|
if (!resp.TryGetValue("result", out object r) || r == null) return false;
|
|
if (r is bool b) return b;
|
|
return string.Equals(r.ToString(), "true", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static string GetMsg(Dictionary<string, object> resp)
|
|
{
|
|
return GetString(resp, "msg") ?? GetString(resp, "reason");
|
|
}
|
|
|
|
private static string GetString(Dictionary<string, object> resp, string key)
|
|
{
|
|
if (resp == null || !resp.TryGetValue(key, out object v) || v == null) return null;
|
|
return v.ToString();
|
|
}
|
|
|
|
private static int GetInt(Dictionary<string, object> resp, string key)
|
|
{
|
|
if (resp == null || !resp.TryGetValue(key, out object v) || v == null) return 0;
|
|
if (v is int i) return i;
|
|
if (v is long l) return (int)l;
|
|
if (v is decimal d) return (int)d;
|
|
int.TryParse(v.ToString(), out int n);
|
|
return n;
|
|
}
|
|
|
|
private static List<HttpLogRecord> ParseLogRecords(Dictionary<string, object> resp)
|
|
{
|
|
var list = new List<HttpLogRecord>();
|
|
if (resp == null || !resp.TryGetValue("record", out object rec) || rec == null) return list;
|
|
if (rec is ArrayList arr)
|
|
{
|
|
foreach (var item in arr) AddLog(list, item as Dictionary<string, object>);
|
|
}
|
|
else if (rec is object[] oa)
|
|
{
|
|
foreach (var item in oa) AddLog(list, item as Dictionary<string, object>);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private static void AddLog(List<HttpLogRecord> list, Dictionary<string, object> row)
|
|
{
|
|
if (row == null) return;
|
|
string enroll = GetString(row, "enrollid") ?? "";
|
|
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();
|
|
if (string.IsNullOrWhiteSpace(acNo)) return;
|
|
list.Add(new HttpLogRecord
|
|
{
|
|
EnrollId = enroll,
|
|
Name = name,
|
|
AcNo = acNo,
|
|
CheckTime = time,
|
|
Mode = GetInt(row, "mode"),
|
|
InOut = GetInt(row, "inout"),
|
|
Event = GetInt(row, "event")
|
|
});
|
|
}
|
|
|
|
private static List<HttpUserRecord> ParseUserRecords(Dictionary<string, object> resp)
|
|
{
|
|
var list = new List<HttpUserRecord>();
|
|
if (resp == null || !resp.TryGetValue("record", out object rec) || rec == null) return list;
|
|
|
|
IEnumerable items = null;
|
|
if (rec is ArrayList arr) items = arr;
|
|
else if (rec is object[] oa) items = oa;
|
|
if (items == null) return list;
|
|
|
|
foreach (var item in items)
|
|
{
|
|
var row = item as Dictionary<string, object>;
|
|
if (row == null) continue;
|
|
string id = GetString(row, "id") ?? "";
|
|
string name = GetString(row, "name") ?? "";
|
|
string serial = !string.IsNullOrWhiteSpace(name) ? name.Trim() : id.Trim();
|
|
if (string.IsNullOrWhiteSpace(serial)) continue;
|
|
list.Add(new HttpUserRecord
|
|
{
|
|
DeviceUserId = id.Trim(),
|
|
Name = name.Trim(),
|
|
SerialNumber = serial
|
|
});
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private static int GetIntSetting(string key, int defaultValue)
|
|
{
|
|
try
|
|
{
|
|
var raw = ConfigurationManager.AppSettings[key];
|
|
if (string.IsNullOrWhiteSpace(raw)) return defaultValue;
|
|
return int.TryParse(raw.Trim(), out var v) && v > 0 ? v : defaultValue;
|
|
}
|
|
catch { return defaultValue; }
|
|
}
|
|
}
|
|
|
|
internal sealed class HttpLogRecord
|
|
{
|
|
public string EnrollId { get; set; }
|
|
public string Name { get; set; }
|
|
public string AcNo { get; set; }
|
|
public DateTime CheckTime { get; set; }
|
|
public int Mode { get; set; }
|
|
public int InOut { get; set; }
|
|
public int Event { get; set; }
|
|
}
|
|
|
|
internal sealed class HttpUserRecord
|
|
{
|
|
public string DeviceUserId { get; set; }
|
|
public string Name { get; set; }
|
|
public string SerialNumber { get; set; }
|
|
}
|
|
|
|
internal static class DeviceProtocol
|
|
{
|
|
public static bool UseHttp()
|
|
{
|
|
var p = ConfigurationManager.AppSettings["DEVICE_PROTOCOL"] ?? "HTTP";
|
|
return string.Equals(p.Trim(), "HTTP", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(p.Trim(), "WEB", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(p.Trim(), "API", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|
|
}
|