diff --git a/MachineUserDeleteBatcher.cs b/MachineUserDeleteBatcher.cs new file mode 100644 index 0000000..6ede35a --- /dev/null +++ b/MachineUserDeleteBatcher.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; + +namespace HanvonF710XAttendanceService +{ + internal sealed class MachineUserDeleteItem + { + public MachineUserDeleteItem(string serialNumber, string deviceEnrollId) + { + SerialNumber = serialNumber ?? ""; + DeviceEnrollId = deviceEnrollId ?? ""; + } + + public string SerialNumber { get; } + public string DeviceEnrollId { get; } + } + + internal static class MachineUserDeleteBatcher + { + public static int CountBatches(int itemCount, int batchSize) + { + if (itemCount <= 0) + { + return 0; + } + + int size = batchSize > 0 ? batchSize : MachineUserDeleteSettings.DefaultBatchSize; + return (itemCount + size - 1) / size; + } + + public static List> SplitIntoBatches(IReadOnlyList items, int batchSize) + { + var batches = new List>(); + if (items == null || items.Count == 0) + { + return batches; + } + + int size = batchSize > 0 ? batchSize : MachineUserDeleteSettings.DefaultBatchSize; + for (int i = 0; i < items.Count; i += size) + { + int take = Math.Min(size, items.Count - i); + var batch = new List(take); + for (int j = 0; j < take; j++) + { + batch.Add(items[i + j]); + } + + batches.Add(batch); + } + + return batches; + } + } +} diff --git a/MachineUserDeleteSettings.cs b/MachineUserDeleteSettings.cs new file mode 100644 index 0000000..3582b60 --- /dev/null +++ b/MachineUserDeleteSettings.cs @@ -0,0 +1,39 @@ +using System; +using System.Configuration; + +namespace HanvonF710XAttendanceService +{ + internal static class MachineUserDeleteSettings + { + public const int DefaultBatchSize = 20; + public const int DefaultHttpTimeoutMs = 60000; + + public static int GetBatchSize() + { + return GetPositiveInt("DEVICE_DELETE_BATCH_SIZE", DefaultBatchSize); + } + + public static int GetHttpTimeoutMs() + { + return GetPositiveInt("DEVICE_HTTP_TIMEOUT_MS", DefaultHttpTimeoutMs); + } + + private static int GetPositiveInt(string key, int defaultValue) + { + try + { + var raw = ConfigurationManager.AppSettings[key]; + if (string.IsNullOrWhiteSpace(raw)) + { + return defaultValue; + } + + return int.TryParse(raw.Trim(), out int value) && value > 0 ? value : defaultValue; + } + catch + { + return defaultValue; + } + } + } +} diff --git a/MachineUserDeleteSync.cs b/MachineUserDeleteSync.cs new file mode 100644 index 0000000..1173110 --- /dev/null +++ b/MachineUserDeleteSync.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; + +namespace HanvonF710XAttendanceService +{ + internal delegate bool MachineUserDeleteBatchDelegate(IReadOnlyList deviceEnrollIds, out string error); + + internal delegate void MachineUserDeletedDelegate(string serialNumber); + + internal sealed class MachineUserDeleteSyncResult + { + public int TotalPending { get; set; } + public int SuccessCount { get; set; } + public int FailedCount { get; set; } + } + + internal static class MachineUserDeleteSync + { + public static MachineUserDeleteSyncResult Execute( + string machineId, + string targetIp, + IReadOnlyList pendingItems, + int batchSize, + MachineUserDeleteBatchDelegate deleteBatch, + MachineUserDeletedDelegate markDeleted, + Action log) + { + var result = new MachineUserDeleteSyncResult(); + if (pendingItems == null || pendingItems.Count == 0) + { + return result; + } + + result.TotalPending = pendingItems.Count; + var batches = MachineUserDeleteBatcher.SplitIntoBatches(pendingItems, batchSize); + int totalBatches = batches.Count; + + log?.Invoke($"[USER_DELETE] machine={machineId} target={targetIp} pending={result.TotalPending} batchSize={batchSize} batches={totalBatches}"); + + for (int batchIndex = 0; batchIndex < batches.Count; batchIndex++) + { + var batch = batches[batchIndex]; + int batchNumber = batchIndex + 1; + var deviceIds = new List(batch.Count); + foreach (var item in batch) + { + if (!string.IsNullOrWhiteSpace(item.DeviceEnrollId)) + { + deviceIds.Add(item.DeviceEnrollId); + } + } + + log?.Invoke($"[USER_DELETE] batch={batchNumber}/{totalBatches} count={batch.Count}"); + + if (deviceIds.Count == 0) + { + log?.Invoke($"[USER_DELETE] batch={batchNumber}/{totalBatches} result=FAILED err=no_device_ids"); + result.FailedCount += batch.Count; + continue; + } + + string error = null; + if (deleteBatch == null || !deleteBatch(deviceIds, out error)) + { + log?.Invoke($"[USER_DELETE] batch={batchNumber}/{totalBatches} result=FAILED err={error ?? "deleteusers failed"}"); + result.FailedCount += batch.Count; + continue; + } + + foreach (var item in batch) + { + markDeleted?.Invoke(item.SerialNumber); + } + + result.SuccessCount += batch.Count; + log?.Invoke($"[USER_DELETE] batch={batchNumber}/{totalBatches} result=SUCCESS"); + } + + log?.Invoke($"[USER_DELETE] machine={machineId} total={result.TotalPending} success={result.SuccessCount} failed={result.FailedCount}"); + return result; + } + } +} diff --git a/MachineUserDeleteSyncTests.cs b/MachineUserDeleteSyncTests.cs new file mode 100644 index 0000000..d2971a4 --- /dev/null +++ b/MachineUserDeleteSyncTests.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace HanvonF710XAttendanceService +{ + internal static class MachineUserDeleteSyncTests + { + public static int RunAll() + { + int failed = 0; + failed += Run("651 IDs split into 33 batches of 20", Test651IdsSplitInto33Batches); + failed += Run("exactly 20 IDs is one batch", TestExactly20IdsOneBatch); + failed += Run("fewer than 20 IDs is one batch", TestFewerThan20IdsOneBatch); + failed += Run("empty deletion list", TestEmptyDeletionList); + failed += Run("successful batch marks DB deleted", TestSuccessfulBatchMarksDeleted); + failed += Run("failed batch remains pending", TestFailedBatchRemainsPending); + 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 Test651IdsSplitInto33Batches() + { + var ids = BuildIds(651); + var batches = MachineUserDeleteBatcher.SplitIntoBatches(ids, 20); + return batches.Count == 33 + && batches.Take(32).All(b => b.Count == 20) + && batches[32].Count == 11 + && MachineUserDeleteBatcher.CountBatches(651, 20) == 33; + } + + private static bool TestExactly20IdsOneBatch() + { + var ids = BuildIds(20); + var batches = MachineUserDeleteBatcher.SplitIntoBatches(ids, 20); + return batches.Count == 1 && batches[0].Count == 20; + } + + private static bool TestFewerThan20IdsOneBatch() + { + var ids = BuildIds(7); + var batches = MachineUserDeleteBatcher.SplitIntoBatches(ids, 20); + return batches.Count == 1 && batches[0].Count == 7; + } + + private static bool TestEmptyDeletionList() + { + var batches = MachineUserDeleteBatcher.SplitIntoBatches(new List(), 20); + var result = MachineUserDeleteSync.Execute( + "105", + "192.168.90.223", + new List(), + 20, + null, + null, + null); + return batches.Count == 0 + && result.TotalPending == 0 + && result.SuccessCount == 0 + && result.FailedCount == 0; + } + + private static bool TestSuccessfulBatchMarksDeleted() + { + var pending = new List + { + new MachineUserDeleteItem("1001", "1001"), + new MachineUserDeleteItem("1002", "1002"), + new MachineUserDeleteItem("1003", "1003") + }; + var marked = new List(); + var logs = new List(); + + var result = MachineUserDeleteSync.Execute( + "105", + "192.168.90.223", + pending, + 20, + (IReadOnlyList ids, out string error) => + { + error = null; + return ids.Count == 3; + }, + serial => marked.Add(serial), + logs.Add); + + return result.TotalPending == 3 + && result.SuccessCount == 3 + && result.FailedCount == 0 + && marked.Count == 3 + && marked.Contains("1001") + && marked.Contains("1002") + && marked.Contains("1003") + && logs.Any(l => l.Contains("result=SUCCESS")); + } + + private static bool TestFailedBatchRemainsPending() + { + var pending = BuildDeleteItems(40); + var marked = new List(); + int callCount = 0; + + var result = MachineUserDeleteSync.Execute( + "105", + "192.168.90.223", + pending, + 20, + (IReadOnlyList ids, out string error) => + { + callCount++; + if (callCount == 2) + { + error = "The operation has timed out"; + return false; + } + + error = null; + return true; + }, + serial => marked.Add(serial), + _ => { }); + + return result.TotalPending == 40 + && result.SuccessCount == 20 + && result.FailedCount == 20 + && marked.Count == 20 + && !marked.Contains("21") + && marked.Contains("1") + && marked.Contains("20"); + } + + private static List BuildIds(int count) + { + var ids = new List(count); + for (int i = 1; i <= count; i++) + { + ids.Add(i.ToString()); + } + + return ids; + } + + private static List BuildDeleteItems(int count) + { + var items = new List(count); + for (int i = 1; i <= count; i++) + { + items.Add(new MachineUserDeleteItem(i.ToString(), i.ToString())); + } + + return items; + } + } +}