|
@@ -15,11 +15,15 @@ import org.apache.commons.lang3.StringUtils;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.stereotype.Service;
|
|
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.LocalDate;
|
|
|
import java.time.ZoneId;
|
|
import java.time.ZoneId;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.time.format.DateTimeFormatter;
|
|
|
import java.time.YearMonth;
|
|
import java.time.YearMonth;
|
|
|
import java.util.*;
|
|
import java.util.*;
|
|
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
|
|
|
|
/** 补填 TimeCard 专用服务;不复用正常汇总表写入目标。 */
|
|
/** 补填 TimeCard 专用服务;不复用正常汇总表写入目标。 */
|
|
|
@Slf4j
|
|
@Slf4j
|
|
@@ -42,6 +46,8 @@ public class TimeCardRepairService {
|
|
|
private static final String R_DAILY_SUBMITTED = "numberField_mmd1smem";
|
|
private static final String R_DAILY_SUBMITTED = "numberField_mmd1smem";
|
|
|
private static final String R_DAILY_APPROVED = "numberField_mmeakgi4";
|
|
private static final String R_DAILY_APPROVED = "numberField_mmeakgi4";
|
|
|
private static final String R_DAILY_CHANGE = "numberField_mmeakgi5";
|
|
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(
|
|
private static final List<String> R_TOTAL_FIELDS = Arrays.asList(
|
|
|
"numberField_mmacxew9", "numberField_mmacxewa", "numberField_mmacxewg", "numberField_mmad6jbc",
|
|
"numberField_mmacxew9", "numberField_mmacxewa", "numberField_mmacxewg", "numberField_mmad6jbc",
|
|
|
"numberField_mmad6jbx", "numberField_mmad6jbz", "numberField_mmad6jc1", "numberField_mmeakgia",
|
|
"numberField_mmad6jbx", "numberField_mmad6jbz", "numberField_mmad6jc1", "numberField_mmeakgia",
|
|
@@ -100,32 +106,90 @@ public class TimeCardRepairService {
|
|
|
McException.assertAccessException(request == null, "请求不能为空");
|
|
McException.assertAccessException(request == null, "请求不能为空");
|
|
|
validateMonthRequest(request.getSubmitterUserId(), request.getSourceMonthText());
|
|
validateMonthRequest(request.getSubmitterUserId(), request.getSourceMonthText());
|
|
|
McException.assertAccessException(!monthClosingService.isClosed(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<String, Object> projectGroups = request.getProjectGroups() == null ? Collections.emptyMap() : request.getProjectGroups();
|
|
|
Map other = request.getOtherGroup() == null ? Collections.emptyMap() : request.getOtherGroup();
|
|
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"));
|
|
double hours = num(row.get("hours"));
|
|
|
McException.assertAccessException(hours <= 0d || hours > 24d, "补填工时必须大于0且不超过24小时");
|
|
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);
|
|
assertSourceDateAvailable(request.getSubmitterUserId(), day);
|
|
|
assertRepairRowMutable(request.getSubmitterUserId(), day, row, category);
|
|
assertRepairRowMutable(request.getSubmitterUserId(), day, row, category);
|
|
|
}
|
|
}
|