diff --git a/DbToDevicePlanning.cs b/DbToDevicePlanning.cs
new file mode 100644
index 0000000..a4589f5
--- /dev/null
+++ b/DbToDevicePlanning.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace HanvonF710XAttendanceService
+{
+ ///
+ /// Pure planning logic for DB_TO_DEVICE jobs (testable without device/DB I/O).
+ ///
+ internal static class DbToDevicePlanning
+ {
+ public const int TemplateLoadBatchSize = 50;
+
+ public static bool ShouldIgnoreSourceDevice(TemplateTransferMode mode)
+ {
+ return mode == TemplateTransferMode.DB_TO_DEVICE;
+ }
+
+ public static List NormalizeEmployeeIds(IEnumerable employeeIds)
+ {
+ if (employeeIds == null)
+ {
+ return new List();
+ }
+
+ return employeeIds
+ .Where(id => !string.IsNullOrWhiteSpace(id))
+ .Select(id => id.Trim())
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+
+ ///
+ /// Resolves which employee serial numbers should be synced from HRMS DB to target devices.
+ ///
+ public static List ResolveEmployeeIds(
+ TemplateTransferJob job,
+ Func, List> loadByDepartmentIds,
+ Func> loadRegisteredOnTargets)
+ {
+ if (job == null)
+ {
+ return new List();
+ }
+
+ if (job.EmpIds != null && job.EmpIds.Count > 0)
+ {
+ return NormalizeEmployeeIds(job.EmpIds);
+ }
+
+ if (job.DepartmentIds != null && job.DepartmentIds.Count > 0)
+ {
+ if (loadByDepartmentIds != null)
+ {
+ var fromDepartments = loadByDepartmentIds(job.DepartmentIds);
+ if (fromDepartments != null && fromDepartments.Count > 0)
+ {
+ return NormalizeEmployeeIds(fromDepartments);
+ }
+ }
+ }
+
+ return NormalizeEmployeeIds(loadRegisteredOnTargets != null ? loadRegisteredOnTargets() : null);
+ }
+
+ public static int ComputeTemplateLoadBatchCount(int employeeCount)
+ {
+ if (employeeCount <= 0)
+ {
+ return 0;
+ }
+
+ return (employeeCount + TemplateLoadBatchSize - 1) / TemplateLoadBatchSize;
+ }
+
+ public static int ComputeLastTemplateLoadBatchSize(int employeeCount)
+ {
+ if (employeeCount <= 0)
+ {
+ return 0;
+ }
+
+ int remainder = employeeCount % TemplateLoadBatchSize;
+ return remainder == 0 ? TemplateLoadBatchSize : remainder;
+ }
+ }
+}
+
diff --git a/DbToDevicePlanningTests.cs b/DbToDevicePlanningTests.cs
new file mode 100644
index 0000000..2325e1f
--- /dev/null
+++ b/DbToDevicePlanningTests.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+
+namespace HanvonF710XAttendanceService
+{
+ ///
+ /// Lightweight unit tests for DB_TO_DEVICE planning logic (no MSTest dependency).
+ ///
+ internal static class DbToDevicePlanningTests
+ {
+ public static int RunAll()
+ {
+ int failed = 0;
+ failed += Run("1 DB_TO_DEVICE ignores source device", TestDbToDeviceIgnoresSource);
+ failed += Run("2 DEVICE_TO_DB requires source device", TestDeviceToDbRequiresSource);
+ failed += Run("3 DB_TO_DEVICE allows empty SourceIp planning", TestDbToDeviceAllowsEmptySourceIp);
+ failed += Run("4 EmpIds only selection", TestEmpIdsFilter);
+ failed += Run("5 DepartmentIds selection", TestDepartmentIdsFallback);
+ failed += Run("6 empty EmpIds uses registered employees", TestEmptyEmpIdsUsesRegistered);
+ failed += Run("7 normalize employee ids", TestNormalizeEmployeeIds);
+ failed += Run("8 DEVICE_TO_DEVICE requires source device", TestDeviceToDeviceRequiresSource);
+ failed += Run("9 template load batches for 673 employees", TestTemplateLoadBatchCount673);
+ return failed;
+ }
+
+ private static int Run(string name, Func test)
+ {
+ try
+ {
+ if (!test())
+ {
+ Console.WriteLine("FAIL: " + name);
+ return 1;
+ }
+ Console.WriteLine("PASS: " + name);
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("FAIL: " + name + " err=" + ex.Message);
+ return 1;
+ }
+ }
+
+ private static bool TestDbToDeviceIgnoresSource()
+ {
+ return DbToDevicePlanning.ShouldIgnoreSourceDevice(TemplateTransferMode.DB_TO_DEVICE);
+ }
+
+ private static bool TestDeviceToDbRequiresSource()
+ {
+ return !DbToDevicePlanning.ShouldIgnoreSourceDevice(TemplateTransferMode.DEVICE_TO_DB);
+ }
+
+ private static bool TestDeviceToDeviceRequiresSource()
+ {
+ return !DbToDevicePlanning.ShouldIgnoreSourceDevice(TemplateTransferMode.DEVICE_TO_DEVICE);
+ }
+
+ private static bool TestDbToDeviceAllowsEmptySourceIp()
+ {
+ var job = new TemplateTransferJob("1", true, "", new List { "192.168.90.223" },
+ new List { "5" }, null);
+ var ids = DbToDevicePlanning.ResolveEmployeeIds(job, _ => new List(), () => new List { "5" });
+ return ids.Count == 1 && ids[0] == "5";
+ }
+
+ private static bool TestOtherModesRequireSource()
+ {
+ return TestDeviceToDbRequiresSource() && TestDeviceToDeviceRequiresSource();
+ }
+
+ private static bool TestEmpIdsFilter()
+ {
+ var job = new TemplateTransferJob("1", true, "10.0.1.3", new List { "192.168.90.223" },
+ new List { "5", "55", "96" }, null);
+
+ var ids = DbToDevicePlanning.ResolveEmployeeIds(
+ job,
+ _ => new List { "999" },
+ () => new List { "999" });
+
+ return ids.Count == 3
+ && ids.Contains("5")
+ && ids.Contains("55")
+ && ids.Contains("96");
+ }
+
+ private static bool TestDepartmentIdsFallback()
+ {
+ var job = new TemplateTransferJob("1", true, "", new List { "192.168.90.223" },
+ null, new List { "10", "20" });
+
+ bool deptCalled = false;
+ var ids = DbToDevicePlanning.ResolveEmployeeIds(
+ job,
+ departmentIds =>
+ {
+ deptCalled = true;
+ return new List { "100", "200" };
+ },
+ () => new List { "300" });
+
+ return deptCalled && ids.Count == 2 && ids.Contains("100") && ids.Contains("200");
+ }
+
+ private static bool TestEmptyEmpIdsUsesRegistered()
+ {
+ var job = new TemplateTransferJob("1", true, "", new List { "192.168.90.223" },
+ null, null);
+
+ var ids = DbToDevicePlanning.ResolveEmployeeIds(
+ job,
+ _ => new List(),
+ () => new List { "5", "55" });
+
+ return ids.Count == 2 && ids.Contains("5") && ids.Contains("55");
+ }
+
+ private static bool TestNormalizeEmployeeIds()
+ {
+ var ids = DbToDevicePlanning.NormalizeEmployeeIds(new[] { " 5 ", "5", "", null, "96" });
+ return ids.Count == 2 && ids[0] == "5" && ids[1] == "96";
+ }
+
+ private static bool TestTemplateLoadBatchCount673()
+ {
+ return DbToDevicePlanning.ComputeTemplateLoadBatchCount(673) == 14
+ && DbToDevicePlanning.ComputeLastTemplateLoadBatchSize(673) == 23;
+ }
+ }
+}
diff --git a/DeviceSettingsConfig.cs b/DeviceSettingsConfig.cs
index 7edfbb4..f2cdafe 100644
--- a/DeviceSettingsConfig.cs
+++ b/DeviceSettingsConfig.cs
@@ -97,7 +97,7 @@ namespace HanvonF710XAttendanceService
var enabled = TryParseBool((string)jobEl.Attribute("Enabled"), out var jEnabled) && jEnabled;
var sourceIp = (string)jobEl.Attribute("SourceIp") ?? (string)jobEl.Attribute("SourceIP") ?? "";
- if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(sourceIp))
+ if (string.IsNullOrWhiteSpace(id))
{
continue;
}
@@ -124,7 +124,20 @@ namespace HanvonF710XAttendanceService
.ToList();
}
- ttConfig.Jobs.Add(new TemplateTransferJob(id.Trim(), enabled, sourceIp.Trim(), targets, empIds));
+ var departmentIdsAttr = (string)jobEl.Attribute("DepartmentIds")
+ ?? (string)jobEl.Attribute("DepartmentIDs")
+ ?? (string)jobEl.Attribute("DeptIds");
+ List departmentIds = null;
+ if (!string.IsNullOrWhiteSpace(departmentIdsAttr))
+ {
+ departmentIds = departmentIdsAttr
+ .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(v => v.Trim())
+ .Where(v => v.Length > 0)
+ .ToList();
+ }
+
+ ttConfig.Jobs.Add(new TemplateTransferJob(id.Trim(), enabled, sourceIp.Trim(), targets, empIds, departmentIds));
}
parsed.TemplateTransfer = ttConfig;
@@ -188,13 +201,14 @@ namespace HanvonF710XAttendanceService
internal sealed class TemplateTransferJob
{
- public TemplateTransferJob(string id, bool enabled, string sourceIp, List targetIps, List empIds)
+ public TemplateTransferJob(string id, bool enabled, string sourceIp, List targetIps, List empIds, List departmentIds = null)
{
Id = id ?? "";
Enabled = enabled;
SourceIp = sourceIp ?? "";
TargetIps = targetIps ?? new List();
EmpIds = empIds ?? new List();
+ DepartmentIds = departmentIds ?? new List();
}
public string Id { get; }
@@ -202,6 +216,7 @@ namespace HanvonF710XAttendanceService
public string SourceIp { get; }
public List TargetIps { get; }
public List EmpIds { get; }
+ public List DepartmentIds { get; }
}
}