hikvision-sync-service/Data/MySqlRepositories.cs

642 lines
29 KiB
C#

using System;
using System.Collections.Generic;
using MySql.Data.MySqlClient;
namespace HikvisionAttendanceService.Data;
internal sealed class MySqlAttendanceMachineRepository : IAttendanceMachineRepository
{
private readonly IHrmsDbConnectionFactory _factory;
public MySqlAttendanceMachineRepository(IHrmsDbConnectionFactory factory) { _factory = factory; }
public List<AttendanceMachineRow> GetActiveMachines(string? machineTypeFilter, out string error)
{
error = "";
var rows = new List<AttendanceMachineRow>();
if (!_factory.TryBuildConnectionString(out var cs, out error))
return rows;
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
string sql = "SELECT machine_id,machine_ip,port_number,machine_name,site_id,machine_status,machine_type,status,last_sync_date,total_users " +
"FROM attendance_machine WHERE (machine_status='active' OR machine_status='1' OR machine_status=1)";
if (!string.IsNullOrWhiteSpace(machineTypeFilter))
sql += " AND UPPER(TRIM(machine_type))=UPPER(TRIM(@machineType))";
using var cmd = new MySqlCommand(sql, conn);
if (!string.IsNullOrWhiteSpace(machineTypeFilter))
cmd.Parameters.AddWithValue("@machineType", machineTypeFilter);
using var rd = cmd.ExecuteReader();
while (rd.Read())
{
rows.Add(new AttendanceMachineRow
{
MachineId = rd["machine_id"]?.ToString() ?? "",
MachineIp = rd["machine_ip"]?.ToString() ?? "",
PortNumber = rd["port_number"] == DBNull.Value ? 8000 : Convert.ToInt32(rd["port_number"]),
MachineName = rd["machine_name"]?.ToString() ?? "",
SiteId = rd["site_id"] == DBNull.Value ? (int?)null : Convert.ToInt32(rd["site_id"]),
MachineStatus = rd["machine_status"]?.ToString() ?? "",
MachineType = rd["machine_type"]?.ToString() ?? "",
Status = rd["status"]?.ToString() ?? "",
LastSyncDate = rd["last_sync_date"] == DBNull.Value ? (DateTime?)null : Convert.ToDateTime(rd["last_sync_date"]),
TotalUsers = rd["total_users"] == DBNull.Value ? 0 : Convert.ToInt32(rd["total_users"])
});
}
}
catch (Exception ex) { error = ex.Message; }
return rows;
}
public bool TryGetMachineByIp(string machineIp, out AttendanceMachineRow? row, out string error)
{
row = null;
error = "";
var wanted = (machineIp ?? "").Trim();
if (wanted.Length == 0)
{
error = "empty machine_ip";
return false;
}
if (!_factory.TryBuildConnectionString(out var cs, out error))
return false;
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
using var cmd = new MySqlCommand(
"SELECT machine_id,machine_ip,port_number,machine_name,site_id,machine_status,machine_type,status,last_sync_date,total_users " +
"FROM attendance_machine WHERE TRIM(machine_ip)=@machine_ip LIMIT 1", conn);
cmd.Parameters.AddWithValue("@machine_ip", wanted);
using var rd = cmd.ExecuteReader();
if (!rd.Read())
return false;
row = ReadAttendanceMachineRow(rd);
return true;
}
catch (Exception ex)
{
error = ex.Message;
return false;
}
}
public bool TryGetMachineByMachineId(string machineId, out AttendanceMachineRow? row, out string error)
{
row = null;
error = "";
var wanted = (machineId ?? "").Trim();
if (wanted.Length == 0)
{
error = "empty machine_id";
return false;
}
if (!_factory.TryBuildConnectionString(out var cs, out error))
return false;
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
using var cmd = new MySqlCommand(
"SELECT machine_id,machine_ip,port_number,machine_name,site_id,machine_status,machine_type,status,last_sync_date,total_users " +
"FROM attendance_machine WHERE TRIM(machine_id)=@machine_id LIMIT 1", conn);
cmd.Parameters.AddWithValue("@machine_id", wanted);
using var rd = cmd.ExecuteReader();
if (!rd.Read())
return false;
row = ReadAttendanceMachineRow(rd);
return true;
}
catch (Exception ex)
{
error = ex.Message;
return false;
}
}
private static AttendanceMachineRow ReadAttendanceMachineRow(MySqlDataReader rd) =>
new AttendanceMachineRow
{
MachineId = rd["machine_id"]?.ToString() ?? "",
MachineIp = rd["machine_ip"]?.ToString() ?? "",
PortNumber = rd["port_number"] == DBNull.Value ? 8000 : Convert.ToInt32(rd["port_number"]),
MachineName = rd["machine_name"]?.ToString() ?? "",
SiteId = rd["site_id"] == DBNull.Value ? (int?)null : Convert.ToInt32(rd["site_id"]),
MachineStatus = rd["machine_status"]?.ToString() ?? "",
MachineType = rd["machine_type"]?.ToString() ?? "",
Status = rd["status"]?.ToString() ?? "",
LastSyncDate = rd["last_sync_date"] == DBNull.Value ? (DateTime?)null : Convert.ToDateTime(rd["last_sync_date"]),
TotalUsers = rd["total_users"] == DBNull.Value ? 0 : Convert.ToInt32(rd["total_users"])
};
public void UpdateMachineSyncState(string machineIp, string status, DateTime? lastSyncDate, int? totalUsers, out string error)
{
error = "";
if (!_factory.TryBuildConnectionString(out var cs, out error))
return;
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
using var cmd = new MySqlCommand(
"UPDATE attendance_machine SET status=@status,last_sync_date=COALESCE(@last_sync_date,last_sync_date),total_users=COALESCE(@total_users,total_users) WHERE machine_ip=@machine_ip",
conn);
cmd.Parameters.AddWithValue("@status", status ?? "");
cmd.Parameters.AddWithValue("@last_sync_date", (object?)lastSyncDate ?? DBNull.Value);
cmd.Parameters.AddWithValue("@total_users", (object?)totalUsers ?? DBNull.Value);
cmd.Parameters.AddWithValue("@machine_ip", machineIp ?? "");
cmd.ExecuteNonQuery();
}
catch (Exception ex) { error = ex.Message; }
}
public bool TryGetMachineLastSyncDate(string machineIp, out DateTime? lastSyncDate, out string error)
{
lastSyncDate = null;
error = "";
if (!_factory.TryBuildConnectionString(out var cs, out error))
return false;
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
using var cmd = new MySqlCommand(
"SELECT last_sync_date FROM attendance_machine WHERE machine_ip=@machine_ip LIMIT 1",
conn);
cmd.Parameters.AddWithValue("@machine_ip", machineIp ?? "");
var obj = cmd.ExecuteScalar();
if (obj == null || obj == DBNull.Value)
return true;
// MySQL DATETIME has no timezone — keep clock components as site wall-clock.
var dt = Convert.ToDateTime(obj);
lastSyncDate = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, DateTimeKind.Unspecified);
return true;
}
catch (Exception ex)
{
error = ex.Message;
return false;
}
}
public bool UpdateMachineLastSyncDate(string machineIp, DateTime lastSyncDate, out string error)
{
error = "";
if (!_factory.TryBuildConnectionString(out var cs, out error))
return false;
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
using var cmd = new MySqlCommand(
"UPDATE attendance_machine SET last_sync_date=@last_sync_date WHERE machine_ip=@machine_ip",
conn);
cmd.Parameters.AddWithValue("@last_sync_date",
new DateTime(lastSyncDate.Year, lastSyncDate.Month, lastSyncDate.Day,
lastSyncDate.Hour, lastSyncDate.Minute, lastSyncDate.Second, DateTimeKind.Unspecified));
cmd.Parameters.AddWithValue("@machine_ip", machineIp ?? "");
cmd.ExecuteNonQuery();
return true;
}
catch (Exception ex)
{
error = ex.Message;
return false;
}
}
}
internal sealed class MySqlAttendanceLogRepository : IAttendanceLogRepository
{
private readonly IHrmsDbConnectionFactory _factory;
public MySqlAttendanceLogRepository(IHrmsDbConnectionFactory factory) { _factory = factory; }
// Confirmed via SHOW CREATE TABLE / DESCRIBE hrms.attendance_log
private static readonly (string Param, string Column, string MySqlType)[] AttendanceLogParamMeta =
{
("@ac_no", "ac_no", "varchar(24) NOT NULL"),
("@checktime", "checktime", "datetime NOT NULL"),
("@processed", "processed", "int NULL"),
("@machine_id", "machine_id", "varchar(45) NULL"),
("@in_out_type_id", "in_out_type_id", "int NULL"),
("@machine_ip", "machine_ip", "varchar(50) NULL"),
("@date", "date", "datetime NULL"),
};
public bool InsertAttendance(
string acNo,
int acNoInt,
DateTime checkTime,
int processed,
string machineId,
int inOutTypeId,
string machineIp,
DateTime dateOnly,
Action<string>? log,
out string error)
{
error = "";
if (!_factory.TryBuildConnectionString(out var cs, out error))
return false;
// Guard: empty ac_no breaks generated column ac_no_int = CAST(ac_no AS UNSIGNED)
// ("Truncated incorrect INTEGER value: ''").
if (string.IsNullOrWhiteSpace(acNo))
{
error = "refused insert: ac_no is empty (would break generated ac_no_int CAST)";
log?.Invoke("[AttendanceInsert] REFUSED: " + error);
return false;
}
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
// Plain INSERT (not REPLACE) until field mapping is confirmed.
const string sql =
"INSERT INTO attendance_log (ac_no,checktime,processed,machine_id,in_out_type_id,machine_ip,date) " +
"VALUES (@ac_no,@checktime,@processed,@machine_id,@in_out_type_id,@machine_ip,@date)";
using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.Add("@ac_no", MySqlDbType.VarChar, 24).Value = acNo;
cmd.Parameters.Add("@checktime", MySqlDbType.DateTime).Value = checkTime;
cmd.Parameters.Add("@processed", MySqlDbType.Int32).Value = processed;
cmd.Parameters.Add("@machine_id", MySqlDbType.VarChar, 45).Value =
string.IsNullOrWhiteSpace(machineId) ? (object)DBNull.Value : machineId;
cmd.Parameters.Add("@in_out_type_id", MySqlDbType.Int32).Value = inOutTypeId;
cmd.Parameters.Add("@machine_ip", MySqlDbType.VarChar, 50).Value =
string.IsNullOrWhiteSpace(machineIp) ? (object)DBNull.Value : machineIp;
cmd.Parameters.Add("@date", MySqlDbType.DateTime).Value = dateOnly.Date;
log?.Invoke("[AttendanceInsert] SQL=" + sql);
log?.Invoke("[AttendanceInsert] NOTE: for_date is GENERATED STORED from checktime; ac_no_int is GENERATED VIRTUAL CAST(ac_no AS UNSIGNED). Neither is bound. cardNo is NOT inserted.");
log?.Invoke("[AttendanceInsert] Parsed employee int (source employeeNoString) acNoInt=" + acNoInt +
" (not a bound param; drives @ac_no string value)");
foreach (var meta in AttendanceLogParamMeta)
{
var p = cmd.Parameters[meta.Param];
var val = p.Value;
var runtimeType = val == null || val == DBNull.Value ? "DBNull" : val.GetType().FullName;
var display = val == null || val == DBNull.Value ? "<DBNull>" : Convert.ToString(val, System.Globalization.CultureInfo.InvariantCulture);
log?.Invoke(
"[AttendanceInsert] param=" + meta.Param +
" value=\"" + display + "\"" +
" csharpType=" + runtimeType +
" column=" + meta.Column +
" mysqlType=" + meta.MySqlType);
}
cmd.ExecuteNonQuery();
return true;
}
catch (Exception ex)
{
error = ex.Message;
log?.Invoke("[AttendanceInsert] FAILED err=" + ex.Message +
" ac_no=\"" + (acNo ?? "") + "\" acNoInt=" + acNoInt +
" machine_id=\"" + (machineId ?? "") + "\" machine_ip=\"" + (machineIp ?? "") + "\"" +
" checktime=" + checkTime.ToString("yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture) +
" in_out_type_id=" + inOutTypeId);
return false;
}
}
public bool InsertTerryAttendance(
string acNo,
DateTime checkTime,
int processed,
string machineId,
int inOutTypeId,
string machineIp,
DateTime dateOnly,
DateTime forDate,
Action<string>? log,
out bool wasDuplicate,
out string error)
{
wasDuplicate = false;
error = "";
if (!_factory.TryBuildConnectionString(out var cs, out error))
return false;
if (string.IsNullOrWhiteSpace(acNo))
{
error = "refused insert: ac_no is empty";
log?.Invoke("[TerryAttendanceInsert] REFUSED: " + error);
return false;
}
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
// Same punch identity as attendance_log conceptual duplicate protection (ac_no + checktime + machine),
// persisted so contractor punches are not re-inserted across polling cycles / restarts.
const string existsSql =
"SELECT 1 FROM terry_attendance_log " +
"WHERE ac_no=@ac_no AND checktime=@checktime " +
"AND ((machine_id IS NULL AND @machine_id IS NULL) OR machine_id=@machine_id) " +
"LIMIT 1";
using (var existsCmd = new MySqlCommand(existsSql, conn))
{
existsCmd.Parameters.Add("@ac_no", MySqlDbType.VarChar, 24).Value = acNo;
existsCmd.Parameters.Add("@checktime", MySqlDbType.DateTime).Value = checkTime;
existsCmd.Parameters.Add("@machine_id", MySqlDbType.VarChar, 45).Value =
string.IsNullOrWhiteSpace(machineId) ? (object)DBNull.Value : machineId;
var exists = existsCmd.ExecuteScalar();
if (exists != null && exists != DBNull.Value)
{
wasDuplicate = true;
error = "duplicate punch already in terry_attendance_log";
log?.Invoke("[TerryAttendanceInsert] DUPLICATE ac_no=\"" + acNo +
"\" checktime=" + checkTime.ToString("yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture) +
" machine_id=\"" + (machineId ?? "") + "\"");
return false;
}
}
const string sql =
"INSERT INTO terry_attendance_log " +
"(checktime,ac_no,processed,machine_id,in_out_type_id,machine_ip,date,for_date) " +
"VALUES (@checktime,@ac_no,@processed,@machine_id,@in_out_type_id,@machine_ip,@date,@for_date)";
using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.Add("@checktime", MySqlDbType.DateTime).Value = checkTime;
cmd.Parameters.Add("@ac_no", MySqlDbType.VarChar, 24).Value = acNo;
cmd.Parameters.Add("@processed", MySqlDbType.Int32).Value = processed;
cmd.Parameters.Add("@machine_id", MySqlDbType.VarChar, 45).Value =
string.IsNullOrWhiteSpace(machineId) ? (object)DBNull.Value : machineId;
cmd.Parameters.Add("@in_out_type_id", MySqlDbType.Int32).Value = inOutTypeId;
cmd.Parameters.Add("@machine_ip", MySqlDbType.VarChar, 50).Value =
string.IsNullOrWhiteSpace(machineIp) ? (object)DBNull.Value : machineIp;
cmd.Parameters.Add("@date", MySqlDbType.DateTime).Value = dateOnly.Date;
cmd.Parameters.Add("@for_date", MySqlDbType.Date).Value = forDate.Date;
log?.Invoke("[TerryAttendanceInsert] SQL=" + sql);
log?.Invoke("[TerryAttendanceInsert] ac_no=\"" + acNo +
"\" checktime=" + checkTime.ToString("yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture) +
" machine_id=\"" + (machineId ?? "") + "\" machine_ip=\"" + (machineIp ?? "") + "\"" +
" in_out_type_id=" + inOutTypeId +
" date=" + dateOnly.Date.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture) +
" for_date=" + forDate.Date.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture));
cmd.ExecuteNonQuery();
return true;
}
catch (Exception ex)
{
error = ex.Message;
log?.Invoke("[TerryAttendanceInsert] FAILED err=" + ex.Message +
" ac_no=\"" + (acNo ?? "") + "\"" +
" machine_id=\"" + (machineId ?? "") + "\" machine_ip=\"" + (machineIp ?? "") + "\"" +
" checktime=" + checkTime.ToString("yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture));
return false;
}
}
}
internal sealed class MySqlEmployeeLookupRepository : IEmployeeLookupRepository
{
private readonly IHrmsDbConnectionFactory _factory;
public MySqlEmployeeLookupRepository(IHrmsDbConnectionFactory factory) { _factory = factory; }
public bool TryGetWorkerTypeBySerialNumberInt(int serialNumberInt, out string? workerType, out string error)
{
workerType = null;
error = "";
if (!_factory.TryBuildConnectionString(out var cs, out error))
return false;
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
using var cmd = new MySqlCommand(
"SELECT worker_type FROM employee WHERE serial_number_int=@serial_number_int LIMIT 1",
conn);
cmd.Parameters.AddWithValue("@serial_number_int", serialNumberInt);
var result = cmd.ExecuteScalar();
if (result == null || result == DBNull.Value)
return false;
workerType = Convert.ToString(result, System.Globalization.CultureInfo.InvariantCulture)?.Trim();
return true;
}
catch (Exception ex)
{
error = ex.Message;
return false;
}
}
}
internal sealed class MySqlAttendanceMachineUserRepository : IAttendanceMachineUserRepository
{
private readonly IHrmsDbConnectionFactory _factory;
public MySqlAttendanceMachineUserRepository(IHrmsDbConnectionFactory factory) { _factory = factory; }
public List<AttendanceMachineUserRow> GetActiveUsersByMachine(string machineId, out string error) =>
QueryUsers(machineId, false, out error);
public List<AttendanceMachineUserRow> GetPendingDeletionUsersByMachine(string machineId, out string error) =>
QueryUsers(machineId, true, out error);
private List<AttendanceMachineUserRow> QueryUsers(string machineId, bool deletionRequested, out string error)
{
error = "";
var rows = new List<AttendanceMachineUserRow>();
if (!_factory.TryBuildConnectionString(out var cs, out error))
return rows;
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
using var cmd = new MySqlCommand(
"SELECT machine_id,serial_number,employee_name,is_deleted,is_deletion_requested FROM attendance_machine_user " +
"WHERE machine_id=@machine_id AND is_deleted=0 AND is_deletion_requested=@is_deletion_requested",
conn);
cmd.Parameters.AddWithValue("@machine_id", machineId ?? "");
cmd.Parameters.AddWithValue("@is_deletion_requested", deletionRequested ? 1 : 0);
using var rd = cmd.ExecuteReader();
while (rd.Read())
{
rows.Add(new AttendanceMachineUserRow
{
MachineId = rd["machine_id"]?.ToString() ?? "",
SerialNumber = rd["serial_number"]?.ToString() ?? "",
EmployeeName = rd["employee_name"]?.ToString() ?? "",
IsDeleted = Convert.ToInt32(rd["is_deleted"]) != 0,
IsDeletionRequested = Convert.ToInt32(rd["is_deletion_requested"]) != 0
});
}
}
catch (Exception ex) { error = ex.Message; }
return rows;
}
public bool UpsertMachineUser(string machineId, string serialNumber, string employeeName, string updatedBy, out string error)
{
error = "";
if (!_factory.TryBuildConnectionString(out var cs, out error))
return false;
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
using var cmd = new MySqlCommand(
"REPLACE INTO attendance_machine_user (machine_id,serial_number,employee_name,is_deleted,is_deletion_requested,updated_by,updated_at) " +
"VALUES (@machine_id,@serial_number,@employee_name,0,0,@updated_by,NOW())", conn);
cmd.Parameters.AddWithValue("@machine_id", machineId ?? "");
cmd.Parameters.AddWithValue("@serial_number", serialNumber ?? "");
cmd.Parameters.AddWithValue("@employee_name", employeeName ?? "");
cmd.Parameters.AddWithValue("@updated_by", string.IsNullOrWhiteSpace(updatedBy) ? "hikvision-service" : updatedBy.Trim());
cmd.ExecuteNonQuery();
return true;
}
catch (Exception ex) { error = ex.Message; return false; }
}
public bool MarkDeleted(string machineId, string serialNumber, string updatedBy, out string error)
{
error = "";
if (!_factory.TryBuildConnectionString(out var cs, out error))
return false;
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
using var cmd = new MySqlCommand(
"UPDATE attendance_machine_user SET is_deleted=1,is_deletion_requested=0,updated_by=@updated_by,updated_at=NOW() " +
"WHERE machine_id=@machine_id AND serial_number=@serial_number", conn);
cmd.Parameters.AddWithValue("@updated_by", updatedBy ?? "hikvision-service");
cmd.Parameters.AddWithValue("@machine_id", machineId ?? "");
cmd.Parameters.AddWithValue("@serial_number", serialNumber ?? "");
cmd.ExecuteNonQuery();
return true;
}
catch (Exception ex) { error = ex.Message; return false; }
}
}
internal sealed class MySqlAttendanceMachineFaceTemplateRepository : IAttendanceMachineFaceTemplateRepository
{
private readonly IHrmsDbConnectionFactory _factory;
public MySqlAttendanceMachineFaceTemplateRepository(IHrmsDbConnectionFactory factory) { _factory = factory; }
public bool UpsertFaceTemplate(string serialNo, byte[] template, DateTime createdDate, bool isActive, string? sourceMachineId, string? sourceMachineIp, out bool createdNew, out string error)
{
error = "";
createdNew = true;
if (!_factory.TryBuildConnectionString(out var cs, out error))
return false;
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
// Skip rewrite when an active non-empty template is already stored for this employee.
using (var existsCmd = new MySqlCommand(
"SELECT 1 FROM attendance_machine_face_templates " +
"WHERE serial_no=@serial_no AND is_active=1 AND template IS NOT NULL AND LENGTH(template)>0 " +
"LIMIT 1", conn))
{
existsCmd.Parameters.AddWithValue("@serial_no", serialNo ?? "");
if (existsCmd.ExecuteScalar() != null)
{
createdNew = false;
return true; // already present — do not UPDATE / re-INSERT
}
}
// Insert new, or fill/replace an empty/inactive row for the same serial.
using (var existsAnyCmd = new MySqlCommand(
"SELECT 1 FROM attendance_machine_face_templates WHERE serial_no=@serial_no LIMIT 1", conn))
{
existsAnyCmd.Parameters.AddWithValue("@serial_no", serialNo ?? "");
createdNew = existsAnyCmd.ExecuteScalar() == null;
}
if (createdNew)
{
using var insert = new MySqlCommand(
"INSERT INTO attendance_machine_face_templates (serial_no,template,created_date,is_active) " +
"VALUES (@serial_no,@template,@created_date,@is_active)", conn);
insert.Parameters.AddWithValue("@serial_no", serialNo ?? "");
insert.Parameters.Add("@template", MySqlDbType.Blob).Value = (object?)template ?? Array.Empty<byte>();
insert.Parameters.AddWithValue("@created_date", createdDate);
insert.Parameters.AddWithValue("@is_active", isActive ? 1 : 0);
insert.ExecuteNonQuery();
}
else
{
using var update = new MySqlCommand(
"UPDATE attendance_machine_face_templates SET template=@template, created_date=@created_date, is_active=@is_active " +
"WHERE serial_no=@serial_no", conn);
update.Parameters.AddWithValue("@serial_no", serialNo ?? "");
update.Parameters.Add("@template", MySqlDbType.Blob).Value = (object?)template ?? Array.Empty<byte>();
update.Parameters.AddWithValue("@created_date", createdDate);
update.Parameters.AddWithValue("@is_active", isActive ? 1 : 0);
update.ExecuteNonQuery();
createdNew = true; // treat fill of empty row as a new save for caller logging
}
// Best-effort source metadata update for schemas that include these columns.
if (!string.IsNullOrWhiteSpace(sourceMachineId) || !string.IsNullOrWhiteSpace(sourceMachineIp))
{
try
{
using var metaCmd = new MySqlCommand(
"UPDATE attendance_machine_face_templates SET source_machine_id=@source_machine_id, source_machine_ip=@source_machine_ip WHERE serial_no=@serial_no",
conn);
metaCmd.Parameters.AddWithValue("@source_machine_id", (object?)sourceMachineId ?? DBNull.Value);
metaCmd.Parameters.AddWithValue("@source_machine_ip", (object?)sourceMachineIp ?? DBNull.Value);
metaCmd.Parameters.AddWithValue("@serial_no", serialNo ?? "");
metaCmd.ExecuteNonQuery();
}
catch
{
// Older schemas may not have source metadata columns; keep core template write successful.
}
}
return true;
}
catch (Exception ex) { error = ex.Message; return false; }
}
public bool TryGetActiveFaceTemplateBySerial(string serialNo, out AttendanceMachineFaceTemplateRow? row, out string error)
{
row = null;
error = "";
if (!_factory.TryBuildConnectionString(out var cs, out error))
return false;
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
using var cmd = new MySqlCommand(
"SELECT serial_no,template,created_date,is_active FROM attendance_machine_face_templates " +
"WHERE serial_no=@serial_no AND is_active=1 ORDER BY created_date DESC LIMIT 1", conn);
cmd.Parameters.AddWithValue("@serial_no", serialNo ?? "");
using var rd = cmd.ExecuteReader();
if (!rd.Read())
return false;
row = new AttendanceMachineFaceTemplateRow
{
SerialNo = rd["serial_no"]?.ToString() ?? "",
Template = rd["template"] == DBNull.Value ? Array.Empty<byte>() : (byte[])rd["template"],
CreatedDate = rd["created_date"] == DBNull.Value ? DateTime.MinValue : Convert.ToDateTime(rd["created_date"]),
IsActive = rd["is_active"] != DBNull.Value && Convert.ToInt32(rd["is_active"]) != 0
};
return true;
}
catch (Exception ex) { error = ex.Message; return false; }
}
}