using MySql.Data.MySqlClient; using System; using System.Collections.Generic; using System.Linq; namespace HanvonF710XAttendanceService { internal sealed class AttendanceMachineFaceTemplateDAO { public bool ExistsBySerialNo(MySqlConnection connection, string serialNo) { const string checkQuery = "SELECT COUNT(*) FROM attendance_machine_face_templates WHERE serial_no = @SerialNumber"; using (var cmd = new MySqlCommand(checkQuery, connection)) { cmd.Parameters.AddWithValue("@SerialNumber", serialNo); object result = cmd.ExecuteScalar(); if (result == null || result == DBNull.Value) { return false; } if (int.TryParse(result.ToString(), out int count)) { return count > 0; } return false; } } public int InsertTemplate(MySqlConnection connection, string serialNo, byte[] templateBytes, DateTime createdDate) { const string insertQuery = "INSERT INTO attendance_machine_face_templates (serial_no, template, created_date) " + "VALUES (@SerialNumber, @FaceTemplate, @created_date)"; using (var cmd = new MySqlCommand(insertQuery, connection)) { cmd.Parameters.AddWithValue("@SerialNumber", serialNo); cmd.Parameters.Add("@FaceTemplate", MySqlDbType.Blob).Value = templateBytes ?? Array.Empty(); cmd.Parameters.AddWithValue("@created_date", createdDate); return cmd.ExecuteNonQuery(); } } public Dictionary GetActiveTemplatesBySerialNos(MySqlConnection connection, IReadOnlyList serialNos) { var result = new Dictionary(StringComparer.OrdinalIgnoreCase); if (serialNos == null || serialNos.Count == 0) { return result; } // Chunk to keep query size reasonable. const int chunkSize = 200; for (int offset = 0; offset < serialNos.Count; offset += chunkSize) { var chunk = serialNos.Skip(offset).Take(chunkSize).ToArray(); var parameterNames = new List(chunk.Length); for (int i = 0; i < chunk.Length; i++) { parameterNames.Add("@SerialNumber" + i); } string inClause = string.Join(",", parameterNames); string query = $"SELECT serial_no, template FROM attendance_machine_face_templates " + $"WHERE serial_no IN ({inClause}) AND is_active = 1"; using (var cmd = new MySqlCommand(query, connection)) { cmd.CommandTimeout = 300; for (int i = 0; i < chunk.Length; i++) { cmd.Parameters.AddWithValue(parameterNames[i], chunk[i]); } using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { var serial = reader.IsDBNull(0) ? null : reader.GetString(0); if (string.IsNullOrWhiteSpace(serial)) { continue; } byte[] blob = null; if (!reader.IsDBNull(1)) { blob = (byte[])reader["template"]; } result[serial] = blob ?? Array.Empty(); } } } } return result; } /// All active template serial numbers (CENTRAL hub scope). public List GetActiveSerialNumbersAll(MySqlConnection connection) { var list = new List(); const string query = "SELECT serial_no FROM hrms.attendance_machine_face_templates WHERE is_active = 1"; using (var cmd = new MySqlCommand(query, connection)) { cmd.CommandTimeout = 120; using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { if (reader.IsDBNull(0)) continue; var s = reader.GetString(0); if (!string.IsNullOrWhiteSpace(s)) { list.Add(s.Trim()); } } } } return list; } /// /// Serial numbers that have an active template and appear on scoped machines (SITE deployment). /// public List GetActiveSerialNumbersForScopedMachines(MySqlConnection connection, IReadOnlyList machineIds) { var result = new HashSet(StringComparer.OrdinalIgnoreCase); if (machineIds == null || machineIds.Count == 0) { return new List(); } const int chunkSize = 80; for (int offset = 0; offset < machineIds.Count; offset += chunkSize) { var chunk = machineIds.Skip(offset).Take(chunkSize).ToArray(); var parameterNames = new List(chunk.Length); for (int i = 0; i < chunk.Length; i++) { parameterNames.Add("@Mid" + i); } string inClause = string.Join(",", parameterNames); string query = "SELECT DISTINCT t.serial_no " + "FROM hrms.attendance_machine_face_templates t " + "INNER JOIN hrms.attendance_machine_user u ON u.serial_number = t.serial_no AND u.is_deleted = 0 " + "WHERE t.is_active = 1 AND u.machine_id IN (" + inClause + ")"; using (var cmd = new MySqlCommand(query, connection)) { cmd.CommandTimeout = 120; for (int i = 0; i < chunk.Length; i++) { cmd.Parameters.AddWithValue(parameterNames[i], chunk[i]); } using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { if (reader.IsDBNull(0)) continue; var s = reader.GetString(0); if (!string.IsNullOrWhiteSpace(s)) { result.Add(s.Trim()); } } } } } return result.ToList(); } /// /// Active template serial numbers for employees in the given departments (HRMS employees table). /// When department lookup is unavailable, returns an empty list and the caller should fall back. /// public List GetActiveSerialNumbersByDepartmentIds( MySqlConnection connection, IReadOnlyList departmentIds, IReadOnlyList allowedMachineIds) { var result = new HashSet(StringComparer.OrdinalIgnoreCase); if (departmentIds == null || departmentIds.Count == 0) { return new List(); } var machineIds = (allowedMachineIds ?? Array.Empty()) .Where(id => !string.IsNullOrWhiteSpace(id)) .Select(id => id.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); const int chunkSize = 80; for (int offset = 0; offset < departmentIds.Count; offset += chunkSize) { var deptChunk = departmentIds.Skip(offset).Take(chunkSize).ToArray(); var deptParamNames = new List(deptChunk.Length); for (int i = 0; i < deptChunk.Length; i++) { deptParamNames.Add("@Dept" + i); } string deptInClause = string.Join(",", deptParamNames); string machineJoin = ""; string machineFilter = ""; if (machineIds.Count > 0) { var machineParamNames = new List(machineIds.Count); for (int i = 0; i < machineIds.Count; i++) { machineParamNames.Add("@Mid" + i); } machineJoin = " INNER JOIN hrms.attendance_machine_user u ON u.serial_number = t.serial_no AND u.is_deleted = 0 "; machineFilter = " AND u.machine_id IN (" + string.Join(",", machineParamNames) + ") "; } string query = "SELECT DISTINCT t.serial_no " + "FROM hrms.attendance_machine_face_templates t " + "INNER JOIN hrms.employee e ON CAST(e.emp_id AS CHAR) = t.serial_no " + machineJoin + "WHERE t.is_active = 1 AND e.department_id IN (" + deptInClause + ")" + machineFilter; using (var cmd = new MySqlCommand(query, connection)) { cmd.CommandTimeout = 120; for (int i = 0; i < deptChunk.Length; i++) { cmd.Parameters.AddWithValue(deptParamNames[i], deptChunk[i]); } for (int i = 0; i < machineIds.Count; i++) { cmd.Parameters.AddWithValue("@Mid" + i, machineIds[i]); } using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { if (reader.IsDBNull(0)) continue; var s = reader.GetString(0); if (!string.IsNullOrWhiteSpace(s)) { result.Add(s.Trim()); } } } } } return result.ToList(); } /// /// Active templates for employees registered on a specific Nedo machine. /// Registration-based target selection: attendance_machine_user (is_deleted=0) drives which employees are allowed. /// public Dictionary GetActiveTemplatesForRegisteredMachine(MySqlConnection connection, string machineId) { var result = new Dictionary(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrWhiteSpace(machineId)) { return result; } const string query = "SELECT t.serial_no, t.template " + "FROM hrms.attendance_machine_face_templates t " + "INNER JOIN hrms.attendance_machine_user u ON u.serial_number = t.serial_no " + "WHERE t.is_active = 1 AND u.is_deleted = 0 AND u.machine_id = @MachineId"; using (var cmd = new MySqlCommand(query, connection)) { cmd.CommandTimeout = 120; cmd.Parameters.AddWithValue("@MachineId", machineId); using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { if (reader.IsDBNull(0)) continue; var serial = reader.GetString(0); if (string.IsNullOrWhiteSpace(serial)) continue; byte[] blob = Array.Empty(); if (!reader.IsDBNull(1)) { blob = (byte[])reader["template"]; } result[serial.Trim()] = blob; } } } return result; } /// /// For each serial_no, return machine_ids where the employee is registered (attendance_machine_user), /// restricted to the allowedMachineIds list (scope) and to Nedo machines (exclude ZKTECO by machine_type). /// public Dictionary> GetRegisteredTargetMachineIdsForSerialNos( MySqlConnection connection, IReadOnlyList serialNos, IReadOnlyList allowedMachineIds) { var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); if (serialNos == null || serialNos.Count == 0) { return result; } var allowedSet = new HashSet(StringComparer.OrdinalIgnoreCase); if (allowedMachineIds != null) { foreach (var id in allowedMachineIds) { if (!string.IsNullOrWhiteSpace(id)) { allowedSet.Add(id.Trim()); } } } if (allowedSet.Count == 0) { return result; } const int serialChunkSize = 200; var allowedList = allowedSet.ToList(); // Keep allowed-machine IN clause reasonably small. const int allowedChunkSize = 100; for (int offset = 0; offset < serialNos.Count; offset += serialChunkSize) { var serialChunk = serialNos.Skip(offset).Take(serialChunkSize).ToArray(); var serialParamNames = new List(serialChunk.Length); for (int i = 0; i < serialChunk.Length; i++) { serialParamNames.Add("@S" + i); } string serialInClause = string.Join(",", serialParamNames); for (int ao = 0; ao < allowedList.Count; ao += allowedChunkSize) { var allowedChunk = allowedList.Skip(ao).Take(allowedChunkSize).ToArray(); var allowedParamNames = new List(allowedChunk.Length); for (int i = 0; i < allowedChunk.Length; i++) { allowedParamNames.Add("@A" + i); } string allowedInClause = string.Join(",", allowedParamNames); const string query = "SELECT u.serial_number, u.machine_id " + "FROM hrms.attendance_machine_user u " + "INNER JOIN hrms.attendance_machine m ON m.machine_id = u.machine_id " + "WHERE u.is_deleted = 0 " + "AND (m.machine_type IS NULL OR m.machine_type <> 'ZKTECO') " + "AND u.serial_number IN ({SERIAL_IN}) " + "AND u.machine_id IN ({ALLOWED_IN})"; string q = query .Replace("{SERIAL_IN}", serialInClause) .Replace("{ALLOWED_IN}", allowedInClause); using (var cmd = new MySqlCommand()) { cmd.Connection = connection; cmd.CommandTimeout = 120; string typeClause = MachineScope.BuildMachineTypeWhereClause(cmd, "m.machine_type", "mtreg" + offset + ao); cmd.CommandText = q.Replace("AND (m.machine_type IS NULL OR m.machine_type <> 'ZKTECO')", "AND " + typeClause); for (int i = 0; i < serialChunk.Length; i++) { cmd.Parameters.AddWithValue(serialParamNames[i], serialChunk[i]); } for (int i = 0; i < allowedChunk.Length; i++) { cmd.Parameters.AddWithValue(allowedParamNames[i], allowedChunk[i]); } using (var reader = cmd.ExecuteReader()) { // temp set per serial to avoid duplicates from allowed-chunk partitioning var serialToSet = new Dictionary>(StringComparer.OrdinalIgnoreCase); while (reader.Read()) { if (reader.IsDBNull(0) || reader.IsDBNull(1)) continue; var serial = reader.GetString(0); var mid = reader.GetString(1); if (string.IsNullOrWhiteSpace(serial) || string.IsNullOrWhiteSpace(mid)) continue; if (!serialToSet.TryGetValue(serial, out var set)) { set = new HashSet(StringComparer.OrdinalIgnoreCase); serialToSet[serial] = set; } set.Add(mid.Trim()); } foreach (var kvp in serialToSet) { if (!result.TryGetValue(kvp.Key, out var list)) { list = new List(); result[kvp.Key] = list; } foreach (var mid in kvp.Value) { if (!list.Contains(mid)) { list.Add(mid); } } } } } } } return result; } } }