Przeglądaj źródła

feat(workhours): 增加重复记录保留规则

malk 3 tygodni temu
rodzic
commit
1596fa0265

+ 153 - 0
mjava-akdsbeisen/src/main/java/com/malk/service/workhours/WorkHoursDuplicateResolver.java

@@ -0,0 +1,153 @@
+package com.malk.service.workhours;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 应填报工时重复记录解析器:同一员工+日期只保留最完整、最近更新的一条。
+ */
+final class WorkHoursDuplicateResolver {
+
+    private static final Comparator<Candidate> KEEPER_ORDER = new Comparator<Candidate>() {
+        @Override
+        public int compare(Candidate left, Candidate right) {
+            int result = Integer.compare(right.getCompleteness(), left.getCompleteness());
+            if (result != 0) return result;
+            result = Long.compare(right.getModifiedAt(), left.getModifiedAt());
+            if (result != 0) return result;
+            result = Long.compare(right.getCreatedAt(), left.getCreatedAt());
+            if (result != 0) return result;
+            return left.getInstanceId().compareTo(right.getInstanceId());
+        }
+    };
+
+    private WorkHoursDuplicateResolver() {
+    }
+
+    static Resolution resolve(List<Candidate> candidates) {
+        Map<String, List<Candidate>> grouped = new LinkedHashMap<>();
+        int skippedInvalidKey = 0;
+        if (candidates != null) {
+            for (Candidate candidate : candidates) {
+                if (candidate == null || candidate.getKey() == null || candidate.getKey().isEmpty()) {
+                    skippedInvalidKey++;
+                    continue;
+                }
+                grouped.computeIfAbsent(candidate.getKey(), key -> new ArrayList<>()).add(candidate);
+            }
+        }
+
+        List<DuplicateGroup> duplicateGroups = new ArrayList<>();
+        List<String> deleteInstanceIds = new ArrayList<>();
+        for (Map.Entry<String, List<Candidate>> entry : grouped.entrySet()) {
+            List<Candidate> group = entry.getValue();
+            if (group.size() <= 1) continue;
+            Collections.sort(group, KEEPER_ORDER);
+            Candidate keeper = group.get(0);
+            List<String> groupDeleteIds = new ArrayList<>();
+            for (int i = 1; i < group.size(); i++) {
+                groupDeleteIds.add(group.get(i).getInstanceId());
+            }
+            deleteInstanceIds.addAll(groupDeleteIds);
+            duplicateGroups.add(new DuplicateGroup(entry.getKey(), keeper.getInstanceId(), groupDeleteIds));
+        }
+        return new Resolution(grouped.size(), skippedInvalidKey, duplicateGroups, deleteInstanceIds);
+    }
+
+    static final class Candidate {
+        private final String instanceId;
+        private final String key;
+        private final int completeness;
+        private final long modifiedAt;
+        private final long createdAt;
+
+        Candidate(String instanceId, String key, int completeness, long modifiedAt, long createdAt) {
+            this.instanceId = instanceId;
+            this.key = key;
+            this.completeness = completeness;
+            this.modifiedAt = modifiedAt;
+            this.createdAt = createdAt;
+        }
+
+        String getInstanceId() {
+            return instanceId;
+        }
+
+        String getKey() {
+            return key;
+        }
+
+        int getCompleteness() {
+            return completeness;
+        }
+
+        long getModifiedAt() {
+            return modifiedAt;
+        }
+
+        long getCreatedAt() {
+            return createdAt;
+        }
+    }
+
+    static final class DuplicateGroup {
+        private final String key;
+        private final String keepInstanceId;
+        private final List<String> deleteInstanceIds;
+
+        DuplicateGroup(String key, String keepInstanceId, List<String> deleteInstanceIds) {
+            this.key = key;
+            this.keepInstanceId = keepInstanceId;
+            this.deleteInstanceIds = new ArrayList<>(deleteInstanceIds);
+        }
+
+        String getKey() {
+            return key;
+        }
+
+        String getKeepInstanceId() {
+            return keepInstanceId;
+        }
+
+        List<String> getDeleteInstanceIds() {
+            return Collections.unmodifiableList(deleteInstanceIds);
+        }
+    }
+
+    static final class Resolution {
+        private final int uniqueKeys;
+        private final int skippedInvalidKey;
+        private final List<DuplicateGroup> groups;
+        private final List<String> deleteInstanceIds;
+
+        Resolution(int uniqueKeys,
+                   int skippedInvalidKey,
+                   List<DuplicateGroup> groups,
+                   List<String> deleteInstanceIds) {
+            this.uniqueKeys = uniqueKeys;
+            this.skippedInvalidKey = skippedInvalidKey;
+            this.groups = new ArrayList<>(groups);
+            this.deleteInstanceIds = new ArrayList<>(deleteInstanceIds);
+        }
+
+        int getUniqueKeys() {
+            return uniqueKeys;
+        }
+
+        int getSkippedInvalidKey() {
+            return skippedInvalidKey;
+        }
+
+        List<DuplicateGroup> getGroups() {
+            return Collections.unmodifiableList(groups);
+        }
+
+        List<String> getDeleteInstanceIds() {
+            return Collections.unmodifiableList(deleteInstanceIds);
+        }
+    }
+}

+ 53 - 0
mjava-akdsbeisen/src/test/java/com/malk/service/workhours/WorkHoursDuplicateResolverTest.java

@@ -0,0 +1,53 @@
+package com.malk.service.workhours;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class WorkHoursDuplicateResolverTest {
+
+    @Test
+    public void resolveShouldKeepMostCompleteCandidate() {
+        WorkHoursDuplicateResolver.Candidate sparse = candidate("old", "u1|2026-07-15", 2, 200L, 200L);
+        WorkHoursDuplicateResolver.Candidate complete = candidate("complete", "u1|2026-07-15", 5, 100L, 100L);
+
+        WorkHoursDuplicateResolver.Resolution result = WorkHoursDuplicateResolver.resolve(
+                Arrays.asList(sparse, complete));
+
+        assertEquals(Collections.singletonList("old"), result.getDeleteInstanceIds());
+        assertEquals("complete", result.getGroups().get(0).getKeepInstanceId());
+    }
+
+    @Test
+    public void resolveShouldKeepMostRecentlyModifiedWhenCompletenessMatches() {
+        WorkHoursDuplicateResolver.Candidate older = candidate("older", "u1|2026-07-15", 5, 100L, 100L);
+        WorkHoursDuplicateResolver.Candidate newer = candidate("newer", "u1|2026-07-15", 5, 200L, 100L);
+
+        WorkHoursDuplicateResolver.Resolution result = WorkHoursDuplicateResolver.resolve(
+                Arrays.asList(older, newer));
+
+        assertEquals(Collections.singletonList("older"), result.getDeleteInstanceIds());
+    }
+
+    @Test
+    public void resolveShouldPreserveCandidatesWithoutReliableKey() {
+        WorkHoursDuplicateResolver.Resolution result = WorkHoursDuplicateResolver.resolve(
+                Collections.singletonList(candidate("invalid", null, 5, 200L, 100L)));
+
+        assertEquals(1, result.getSkippedInvalidKey());
+        assertTrue(result.getDeleteInstanceIds().isEmpty());
+    }
+
+    private static WorkHoursDuplicateResolver.Candidate candidate(String instanceId,
+                                                                   String key,
+                                                                   int completeness,
+                                                                   long modifiedAt,
+                                                                   long createdAt) {
+        return new WorkHoursDuplicateResolver.Candidate(
+                instanceId, key, completeness, modifiedAt, createdAt);
+    }
+}