Просмотр исходного кода

fix(timecard): add repair submit debounce and project validation

malk 1 месяц назад
Родитель
Сommit
77fb569e84

+ 86 - 18
mjava-akdsbeisen/src/main/java/com/malk/service/workhours/TimeCardRepairService.java

@@ -15,11 +15,15 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.time.LocalDate;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.time.YearMonth;
 import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
 
 /** 补填 TimeCard 专用服务;不复用正常汇总表写入目标。 */
 @Slf4j
@@ -42,6 +46,8 @@ public class TimeCardRepairService {
     private static final String R_DAILY_SUBMITTED = "numberField_mmd1smem";
     private static final String R_DAILY_APPROVED = "numberField_mmeakgi4";
     private static final String R_DAILY_CHANGE = "numberField_mmeakgi5";
+    private static final long SUBMIT_GUARD_TTL_MILLIS = 30_000L;
+    private static final ConcurrentHashMap<String, SubmitGuard> SUBMIT_GUARDS = new ConcurrentHashMap<>();
     private static final List<String> R_TOTAL_FIELDS = Arrays.asList(
             "numberField_mmacxew9", "numberField_mmacxewa", "numberField_mmacxewg", "numberField_mmad6jbc",
             "numberField_mmad6jbx", "numberField_mmad6jbz", "numberField_mmad6jc1", "numberField_mmeakgia",
@@ -100,32 +106,90 @@ public class TimeCardRepairService {
         McException.assertAccessException(request == null, "请求不能为空");
         validateMonthRequest(request.getSubmitterUserId(), request.getSourceMonthText());
         McException.assertAccessException(!monthClosingService.isClosed(request.getSourceMonthText()), "补填只能提交已封账月份");
-        String batch = UUID.randomUUID().toString();
-        long submitDate = LocalDate.now(ZONE).atStartOfDay(ZONE).toInstant().toEpochMilli();
         Map<String, Object> projectGroups = request.getProjectGroups() == null ? Collections.emptyMap() : request.getProjectGroups();
         Map other = request.getOtherGroup() == null ? Collections.emptyMap() : request.getOtherGroup();
-        int rowCount = validateSubmitRows(request, projectGroups, other);
-        McException.assertAccessException(rowCount == 0, "没有需要提交的补填工时");
-        for (Object rawGroup : projectGroups.values()) {
-            if (!(rawGroup instanceof Map)) continue;
-            Map group = (Map) rawGroup;
-            String managerId = normalizeManagerId(text(group.get("managerId")));
-            for (String category : new String[]{"billableRows", "nonBillableRows"}) {
-                for (Object rawRow : list(group.get(category))) {
-                    if (rawRow instanceof Map) {
-                        upsertRepairRow(request, (Map) rawRow, category, managerId, batch, submitDate);
+        String guardKey = buildSubmitGuardKey(request, projectGroups, other);
+        SubmitGuard guard = acquireSubmitGuard(guardKey);
+        Map<String, Object> cachedResult = guard.cachedResult();
+        if (cachedResult != null) return cachedResult;
+        try {
+            String batch = UUID.randomUUID().toString();
+            long submitDate = LocalDate.now(ZONE).atStartOfDay(ZONE).toInstant().toEpochMilli();
+            int rowCount = validateSubmitRows(request, projectGroups, other);
+            McException.assertAccessException(rowCount == 0, "没有需要提交的补填工时");
+            for (Object rawGroup : projectGroups.values()) {
+                if (!(rawGroup instanceof Map)) continue;
+                Map group = (Map) rawGroup;
+                String managerId = normalizeManagerId(text(group.get("managerId")));
+                for (String category : new String[]{"billableRows", "nonBillableRows"}) {
+                    for (Object rawRow : list(group.get(category))) {
+                        if (rawRow instanceof Map) {
+                            upsertRepairRow(request, (Map) rawRow, category, managerId, batch, submitDate);
+                        }
                     }
                 }
             }
+            String otherManagerId = normalizeManagerId(text(other.get("managerId")));
+            for (Object rawRow : list(other.get("otherRows"))) {
+                if (rawRow instanceof Map) {
+                    upsertRepairRow(request, (Map) rawRow, "otherRows", otherManagerId, batch, submitDate);
+                }
+            }
+            startApprovals(request, projectGroups, other, batch, submitDate);
+            Map<String, Object> result = mapOf("batch", batch, "submitDate", submitDate, "status", "审批中");
+            guard.complete(result);
+            return result;
+        } catch (RuntimeException ex) {
+            SUBMIT_GUARDS.remove(guardKey, guard);
+            throw ex;
+        }
+    }
+
+    private String buildSubmitGuardKey(TimeCardRepairSubmitRequest request,
+                                       Map<String, Object> projectGroups, Map other) {
+        String payload = request.getSubmitterUserId() + "|" + request.getSourceMonthText()
+                + "|" + JSON.toJSONString(projectGroups) + "|" + JSON.toJSONString(other);
+        try {
+            byte[] digest = MessageDigest.getInstance("SHA-256")
+                    .digest(payload.getBytes(StandardCharsets.UTF_8));
+            StringBuilder hex = new StringBuilder(digest.length * 2);
+            for (byte value : digest) hex.append(String.format("%02x", value));
+            return hex.toString();
+        } catch (NoSuchAlgorithmException ex) {
+            throw new IllegalStateException("提交幂等键计算失败", ex);
         }
-        String otherManagerId = normalizeManagerId(text(other.get("managerId")));
-        for (Object rawRow : list(other.get("otherRows"))) {
-            if (rawRow instanceof Map) {
-                upsertRepairRow(request, (Map) rawRow, "otherRows", otherManagerId, batch, submitDate);
+    }
+
+    private SubmitGuard acquireSubmitGuard(String key) {
+        long now = System.currentTimeMillis();
+        synchronized (SUBMIT_GUARDS) {
+            SUBMIT_GUARDS.entrySet().removeIf(entry -> entry.getValue().expiresAt <= now);
+            SubmitGuard existing = SUBMIT_GUARDS.get(key);
+            if (existing != null && existing.expiresAt > now) {
+                if (existing.result != null) return existing;
+                McException.assertAccessException(true, "补填提交正在处理中,请勿重复提交");
             }
+            SubmitGuard guard = new SubmitGuard(now + SUBMIT_GUARD_TTL_MILLIS);
+            SUBMIT_GUARDS.put(key, guard);
+            return guard;
+        }
+    }
+
+    private static final class SubmitGuard {
+        private final long expiresAt;
+        private Map<String, Object> result;
+
+        private SubmitGuard(long expiresAt) {
+            this.expiresAt = expiresAt;
+        }
+
+        private synchronized void complete(Map<String, Object> value) {
+            this.result = new LinkedHashMap<>(value);
+        }
+
+        private synchronized Map<String, Object> cachedResult() {
+            return result == null ? null : new LinkedHashMap<>(result);
         }
-        startApprovals(request, projectGroups, other, batch, submitDate);
-        return mapOf("batch", batch, "submitDate", submitDate, "status", "审批中");
     }
 
     /**
@@ -398,6 +462,10 @@ public class TimeCardRepairService {
         }
         double hours = num(row.get("hours"));
         McException.assertAccessException(hours <= 0d || hours > 24d, "补填工时必须大于0且不超过24小时");
+        if (!"otherRows".equals(category)) {
+            McException.assertAccessException(StringUtils.isBlank(text(row.get("projectCode"))),
+                    ("billableRows".equals(category) ? "Billable" : "Non Billable") + "工时必须填写项目编号");
+        }
         assertSourceDateAvailable(request.getSubmitterUserId(), day);
         assertRepairRowMutable(request.getSubmitterUserId(), day, row, category);
     }