|
|
@@ -0,0 +1,964 @@
|
|
|
+package com.malk.service.workhours;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.malk.server.aliwork.YDConf;
|
|
|
+import com.malk.server.aliwork.YDParam;
|
|
|
+import com.malk.server.common.McException;
|
|
|
+import com.malk.server.dingtalk.DDR_New;
|
|
|
+import com.malk.server.workhours.MonthClosingCheckRequest;
|
|
|
+import com.malk.server.workhours.MonthClosingStatusResult;
|
|
|
+import com.malk.server.workhours.TimeCardApprovalStartRequest;
|
|
|
+import com.malk.server.workhours.TimeCardRepairSubmitRequest;
|
|
|
+import com.malk.server.workhours.TimeCardRepairSummaryUpsertRequest;
|
|
|
+import com.malk.server.workhours.WHConf;
|
|
|
+import com.malk.service.aliwork.YDClient;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+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.YearMonth;
|
|
|
+import java.time.ZoneId;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.Iterator;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.LinkedHashSet;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Locale;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.UUID;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
+
|
|
|
+/** TimeCard 变更专用服务;只操作共用汇总表中工时类型为“变更”的记录。 */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class TimeCardChangeService {
|
|
|
+ private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
|
|
|
+ private static final DateTimeFormatter DAY = DateTimeFormatter.ofPattern("yyyyMMdd");
|
|
|
+ private static final DateTimeFormatter MONTH = DateTimeFormatter.ofPattern("yyyyMM");
|
|
|
+ private static final String CHANGE_TYPE = "变更";
|
|
|
+ private static final String DEFAULT_MANAGER_ID = "634735129";
|
|
|
+ private static final double EPSILON = 1e-6d;
|
|
|
+ private static final long SUBMIT_GUARD_TTL_MILLIS = 30_000L;
|
|
|
+ private static final ConcurrentHashMap<String, SubmitGuard> SUBMIT_GUARDS = new ConcurrentHashMap<>();
|
|
|
+
|
|
|
+ private static final String R_USER = "textField_mmbffvd9";
|
|
|
+ private static final String R_DAY = "textField_mmbffvdb";
|
|
|
+ private static final String R_MONTH = "textField_mmbffvda";
|
|
|
+ private static final String R_USER_EMPLOYEE = "employeeField_mmacxew4";
|
|
|
+ private static final String R_MONTH_DATE = "dateField_mmacxewf";
|
|
|
+ private static final String R_DAY_DATE = "dateField_mmbffvd8";
|
|
|
+ private static final String R_DAILY_APPROVED = "numberField_mmeakgi4";
|
|
|
+ private static final String R_BILLABLE_TABLE = "tableField_mmczo634";
|
|
|
+ private static final String R_NON_BILLABLE_TABLE = "tableField_mmczo63h";
|
|
|
+ private static final String R_OTHER_TABLE = "tableField_mmeakgid";
|
|
|
+
|
|
|
+ private static final TableDef BILLABLE = new TableDef(
|
|
|
+ R_BILLABLE_TABLE, "textField_mmacxew3", "textField_mmad6jcj", "employeeField_mkoxpswf",
|
|
|
+ "numberField_mmczo635", "numberField_mmczo636", "numberField_mmczo637", "numberField_mmd80zae");
|
|
|
+ private static final TableDef NON_BILLABLE = new TableDef(
|
|
|
+ R_NON_BILLABLE_TABLE, "textField_mmczo639", "textField_mmczo63g", "employeeField_mmczo63a",
|
|
|
+ "numberField_mmczo63b", "numberField_mmczo63c", "numberField_mmczo63d", "numberField_mmeakgi9");
|
|
|
+ private static final TableDef OTHER = new TableDef(
|
|
|
+ R_OTHER_TABLE, "selectField_mmeakgie", "", "",
|
|
|
+ "numberField_mmeakgik", "numberField_mmeakgij", "numberField_mmeakgii", "numberField_mmeakgim");
|
|
|
+ private static final List<TableDef> TABLES = Arrays.asList(BILLABLE, NON_BILLABLE, OTHER);
|
|
|
+
|
|
|
+ @Autowired private YDClient ydClient;
|
|
|
+ @Autowired private WHConf whConf;
|
|
|
+ @Autowired private MonthClosingService monthClosingService;
|
|
|
+ @Autowired private ReSubmitApprovalService reSubmitApprovalService;
|
|
|
+
|
|
|
+ /** 保存变更草稿;共享表中的已审批工时始终保存当前最终结果。 */
|
|
|
+ public boolean upsert(TimeCardRepairSummaryUpsertRequest request) {
|
|
|
+ validateUpsertRequest(request);
|
|
|
+ Map original = loadEligibleOriginal(request.getSubmitterUserId(), request.getDayText());
|
|
|
+ List<Map> existing = queryChangeRecords(request.getSubmitterUserId(), null, request.getDayText());
|
|
|
+ McException.assertAccessException(existing.size() > 1, duplicateMessage(request));
|
|
|
+ Map current = existing.isEmpty() ? Collections.emptyMap() : detail(instanceId(existing.get(0)));
|
|
|
+ assertDraftMutable(current, request.getSubmitterUserId(), request.getDayText());
|
|
|
+ Map baseline = current.isEmpty() ? original : current;
|
|
|
+ Map<String, Object> data = buildDraftData(request.getFormData(), baseline, current.isEmpty(),
|
|
|
+ request.getSubmitterUserId(), request.getDayText());
|
|
|
+ upsertChangeSummary(request.getSubmitterUserId(), request.getDayText(), data);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 提交变更审批;审批单现有工时字段直接保存目标结果。 */
|
|
|
+ public Map<String, Object> submit(TimeCardRepairSubmitRequest request) {
|
|
|
+ McException.assertAccessException(request == null, "请求不能为空");
|
|
|
+ validateMonthRequest(request.getSubmitterUserId(), request.getSourceMonthText());
|
|
|
+ assertCurrentMonthOpen();
|
|
|
+ Map<String, Object> projectGroups = safeMap(request.getProjectGroups());
|
|
|
+ Map otherGroup = safeMap(request.getOtherGroup());
|
|
|
+ String guardKey = submitGuardKey(request, projectGroups, otherGroup);
|
|
|
+ SubmitGuard guard = acquireSubmitGuard(guardKey);
|
|
|
+ Map<String, Object> cached = guard.cachedResult();
|
|
|
+ if (cached != null) return cached;
|
|
|
+ try {
|
|
|
+ Map<String, Object> result = doSubmit(request, projectGroups, otherGroup);
|
|
|
+ guard.complete(result);
|
|
|
+ return result;
|
|
|
+ } catch (RuntimeException ex) {
|
|
|
+ SUBMIT_GUARDS.remove(guardKey, guard);
|
|
|
+ throw ex;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> doSubmit(TimeCardRepairSubmitRequest request,
|
|
|
+ Map<String, Object> projectGroups, Map otherGroup) {
|
|
|
+ String batch = UUID.randomUUID().toString();
|
|
|
+ LocalDate submitDay = LocalDate.now(ZONE);
|
|
|
+ long submitDate = submitDay.atStartOfDay(ZONE).toInstant().toEpochMilli();
|
|
|
+ int changedRows = prepareSubmittedRows(request, projectGroups, otherGroup, batch, submitDate);
|
|
|
+ McException.assertAccessException(changedRows == 0, "没有发生变化的工时");
|
|
|
+ startApprovals(request, projectGroups, otherGroup, batch, submitDate);
|
|
|
+ return mapOf("batch", batch, "submitDate", submitDate, "status", "审批中");
|
|
|
+ }
|
|
|
+
|
|
|
+ private int prepareSubmittedRows(TimeCardRepairSubmitRequest request,
|
|
|
+ Map<String, Object> projectGroups, Map otherGroup,
|
|
|
+ String batch, long submitDate) {
|
|
|
+ int changed = 0;
|
|
|
+ Iterator<Map.Entry<String, Object>> groups = projectGroups.entrySet().iterator();
|
|
|
+ while (groups.hasNext()) {
|
|
|
+ Map.Entry<String, Object> entry = groups.next();
|
|
|
+ Object rawGroup = entry.getValue();
|
|
|
+ if (!(rawGroup instanceof Map)) {
|
|
|
+ groups.remove();
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Map group = new LinkedHashMap((Map) rawGroup);
|
|
|
+ entry.setValue(group);
|
|
|
+ String managerId = normalizeManagerId(text(group.get("managerId")));
|
|
|
+ List<Map> billableRows = prepareCategoryRows(request, list(group.get("billableRows")),
|
|
|
+ BILLABLE, managerId, batch, submitDate);
|
|
|
+ List<Map> nonBillableRows = prepareCategoryRows(request, list(group.get("nonBillableRows")),
|
|
|
+ NON_BILLABLE, managerId, batch, submitDate);
|
|
|
+ group.put("billableRows", billableRows);
|
|
|
+ group.put("nonBillableRows", nonBillableRows);
|
|
|
+ changed += billableRows.size() + nonBillableRows.size();
|
|
|
+ if (billableRows.isEmpty() && nonBillableRows.isEmpty()) groups.remove();
|
|
|
+ }
|
|
|
+ String managerId = normalizeManagerId(text(otherGroup.get("managerId")));
|
|
|
+ List<Map> otherRows = prepareCategoryRows(request, list(otherGroup.get("otherRows")),
|
|
|
+ OTHER, managerId, batch, submitDate);
|
|
|
+ otherGroup.put("otherRows", otherRows);
|
|
|
+ changed += otherRows.size();
|
|
|
+ if (otherRows.isEmpty()) otherGroup.clear();
|
|
|
+ return changed;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map> prepareCategoryRows(TimeCardRepairSubmitRequest request, List<Map> rows,
|
|
|
+ TableDef table, String managerId,
|
|
|
+ String batch, long submitDate) {
|
|
|
+ List<Map> changed = new ArrayList<>();
|
|
|
+ for (Map row : rows) {
|
|
|
+ if (prepareSubmittedRow(request, row, table, managerId, batch, submitDate)) {
|
|
|
+ changed.add(row);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return changed;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean prepareSubmittedRow(TimeCardRepairSubmitRequest request, Map row,
|
|
|
+ TableDef table, String managerId,
|
|
|
+ String batch, long submitDate) {
|
|
|
+ String dayText = rowDay(row);
|
|
|
+ double target = validateTargetRow(request, row, table, dayText);
|
|
|
+ Map original = loadEligibleOriginal(request.getSubmitterUserId(), dayText);
|
|
|
+ List<Map> records = queryChangeRecords(request.getSubmitterUserId(), null, dayText);
|
|
|
+ McException.assertAccessException(records.size() != 1,
|
|
|
+ "变更汇总完整记录不存在或重复,请先保存后提交 userId="
|
|
|
+ + request.getSubmitterUserId() + ", day=" + dayText);
|
|
|
+ String id = instanceId(records.get(0));
|
|
|
+ Map summary = detail(id);
|
|
|
+ assertDraftMutable(summary, request.getSubmitterUserId(), dayText, batch);
|
|
|
+ Map child = findRow(list(summary.get(table.tableField)), table.keyField, targetKey(row, table));
|
|
|
+ McException.assertAccessException(child == null, "变更汇总明细不存在 day=" + dayText
|
|
|
+ + ", key=" + targetKey(row, table));
|
|
|
+ double baseline = num(child.get(table.approvedField));
|
|
|
+ double delta = target - baseline;
|
|
|
+ if (Math.abs(delta) <= EPSILON) return false;
|
|
|
+ updateSubmittedSummary(request, summary, child, table, target, delta,
|
|
|
+ managerId, dayText, batch, submitDate, original, id);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void updateSubmittedSummary(TimeCardRepairSubmitRequest request, Map summary,
|
|
|
+ Map child, TableDef table, double target, double delta,
|
|
|
+ String managerId, String dayText, String batch,
|
|
|
+ long submitDate, Map original, String id) {
|
|
|
+ List<Map> children = writableRows(summary.get(table.tableField));
|
|
|
+ Map writableChild = findRow(children, table.keyField, text(child.get(table.keyField)));
|
|
|
+ writableChild.put(table.submittedField, target);
|
|
|
+ writableChild.put(table.changeField, delta);
|
|
|
+ putManager(writableChild, table, managerId);
|
|
|
+ Map<String, Object> update = commonSubmittedFields(request, dayText, batch, submitDate, original);
|
|
|
+ update.put(table.tableField, children);
|
|
|
+ Map snapshot = new LinkedHashMap(summary);
|
|
|
+ snapshot.put(table.tableField, children);
|
|
|
+ TimeCardRepairService.applySummaryTotals(update, snapshot);
|
|
|
+ updateAndVerifySummary(id, update, table, text(child.get(table.keyField)), target, delta);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> commonSubmittedFields(TimeCardRepairSubmitRequest request,
|
|
|
+ String dayText, String batch,
|
|
|
+ long submitDate, Map original) {
|
|
|
+ Map<String, Object> update = new LinkedHashMap<>();
|
|
|
+ update.put(R_USER, request.getSubmitterUserId());
|
|
|
+ update.put(R_DAY, dayText);
|
|
|
+ update.put(R_MONTH, dayText.substring(0, 6));
|
|
|
+ update.put(whConf.getRepairSummaryFillTypeField(), CHANGE_TYPE);
|
|
|
+ update.put(whConf.getRepairSummarySyncStatusField(), "审批中");
|
|
|
+ update.put(whConf.getRepairSummarySubmitDateField(), submitDate);
|
|
|
+ update.put(whConf.getRepairSummaryBatchField(), batch);
|
|
|
+ update.put(whConf.getRepairSummarySourceMonthField(), request.getSourceMonthText());
|
|
|
+ update.put(R_USER_EMPLOYEE, Collections.singletonList(request.getSubmitterUserId()));
|
|
|
+ update.put(R_MONTH_DATE, parseDay(dayText.substring(0, 6) + "01"));
|
|
|
+ update.put(R_DAY_DATE, parseDay(dayText));
|
|
|
+ copyIfPresent(original, update, "employeeField_mh8xhqc3");
|
|
|
+ copyIfPresent(original, update, "textField_mmoazo1b");
|
|
|
+ return update;
|
|
|
+ }
|
|
|
+
|
|
|
+ private double validateTargetRow(TimeCardRepairSubmitRequest request, Map row,
|
|
|
+ TableDef table, String dayText) {
|
|
|
+ McException.assertAccessException(!dayText.matches("\\d{8}")
|
|
|
+ || !dayText.startsWith(request.getSourceMonthText()),
|
|
|
+ "变更日期必须属于目标月份");
|
|
|
+ parseDay(dayText);
|
|
|
+ double target = num(row.get("hours"));
|
|
|
+ McException.assertAccessException(!Double.isFinite(target) || target < 0d || target > 24d,
|
|
|
+ "变更后工时必须在0到24小时之间");
|
|
|
+ McException.assertAccessException(StringUtils.isBlank(targetKey(row, table)),
|
|
|
+ table == OTHER ? "其他工时必须填写Activity" : "项目工时必须填写项目编号");
|
|
|
+ return target;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> buildDraftData(Map<String, Object> requested, Map baseline,
|
|
|
+ boolean originalBaseline, String userId,
|
|
|
+ String dayText) {
|
|
|
+ McException.assertAccessException(requested == null, "变更工时数据不能为空");
|
|
|
+ Map<String, Object> data = copyMainFields(requested);
|
|
|
+ for (TableDef table : TABLES) {
|
|
|
+ data.put(table.tableField, buildDraftRows(
|
|
|
+ requested.get(table.tableField), baseline.get(table.tableField), table, originalBaseline));
|
|
|
+ }
|
|
|
+ TimeCardRepairService.applySummaryTotals(data, data);
|
|
|
+ data.put(R_USER, userId);
|
|
|
+ data.put(R_DAY, dayText);
|
|
|
+ data.put(R_MONTH, dayText.substring(0, 6));
|
|
|
+ data.put(R_USER_EMPLOYEE, Collections.singletonList(userId));
|
|
|
+ data.put(R_MONTH_DATE, parseDay(dayText.substring(0, 6) + "01"));
|
|
|
+ data.put(R_DAY_DATE, parseDay(dayText));
|
|
|
+ data.put(whConf.getRepairSummaryFillTypeField(), CHANGE_TYPE);
|
|
|
+ data.put(whConf.getRepairSummarySyncStatusField(), "暂存");
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map> buildDraftRows(Object requestedRaw, Object baselineRaw,
|
|
|
+ TableDef table, boolean originalBaseline) {
|
|
|
+ Map<String, Map> baselines = indexRows(baselineRaw, table.keyField);
|
|
|
+ List<Map> result = new ArrayList<>();
|
|
|
+ for (Map requested : list(requestedRaw)) {
|
|
|
+ String key = text(requested.get(table.keyField));
|
|
|
+ if (StringUtils.isBlank(key)) continue;
|
|
|
+ Map source = baselines.get(key);
|
|
|
+ double approved = source == null ? 0d : num(source.get(table.approvedField));
|
|
|
+ if (originalBaseline && source != null) approved += num(source.get(table.changeField));
|
|
|
+ Map row = writableRow(requested);
|
|
|
+ mergeMissingMetadata(row, source, table);
|
|
|
+ row.put(table.draftField, num(requested.get(table.draftField)));
|
|
|
+ row.put(table.submittedField, approved);
|
|
|
+ row.put(table.approvedField, approved);
|
|
|
+ row.put(table.changeField, 0d);
|
|
|
+ result.add(row);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> copyMainFields(Map<String, Object> requested) {
|
|
|
+ Map<String, Object> data = new LinkedHashMap<>();
|
|
|
+ for (Map.Entry<String, Object> entry : requested.entrySet()) {
|
|
|
+ String key = entry.getKey();
|
|
|
+ if (!key.startsWith("tableField_") && !key.startsWith("numberField_")
|
|
|
+ && !key.endsWith("_value")) {
|
|
|
+ data.put(key, entry.getValue());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void mergeMissingMetadata(Map target, Map source, TableDef table) {
|
|
|
+ if (source == null) return;
|
|
|
+ copyIfMissing(source, target, table.keyField);
|
|
|
+ copyIfMissing(source, target, table.nameField);
|
|
|
+ copyIfMissing(source, target, table.managerField);
|
|
|
+ for (String field : Arrays.asList("selectField_mmad6jci", "selectField_mmczo638",
|
|
|
+ "textField_mkov5oz9", "radioField_mkov5oz7", "textField_mmczo63e",
|
|
|
+ "radioField_mmczo63f")) {
|
|
|
+ copyIfMissing(source, target, field);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void upsertChangeSummary(String userId, String dayText, Map<String, Object> data) {
|
|
|
+ Map<String, Object> search = mapOf(R_USER, userId, R_DAY, dayText,
|
|
|
+ whConf.getRepairSummaryFillTypeField(), CHANGE_TYPE);
|
|
|
+ ydClient.operateData(YDParam.builder().appType(whConf.getYidaAppType())
|
|
|
+ .systemToken(whConf.getYidaSystemToken())
|
|
|
+ .formUuid(whConf.getFormUuidRepairWorkHoursSummary())
|
|
|
+ .searchCondition(JSON.toJSONString(search)).formDataJson(JSON.toJSONString(data))
|
|
|
+ .noExecuteExpression(false).build(), YDConf.FORM_OPERATION.upsert);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map loadEligibleOriginal(String userId, String dayText) {
|
|
|
+ MonthClosingStatusResult status = currentMonthStatus();
|
|
|
+ List<Map> records = query(whConf.getFormUuidWorkHoursSummary(), mapOf(R_USER, userId, R_DAY, dayText));
|
|
|
+ McException.assertAccessException(records.size() != 1,
|
|
|
+ "目标日期正式工时汇总不存在或重复 userId=" + userId + ", day=" + dayText);
|
|
|
+ Map original = detail(instanceId(records.get(0)));
|
|
|
+ TimeCardChangePolicy.Availability availability = TimeCardChangePolicy.evaluate(
|
|
|
+ LocalDate.now(ZONE), dayText, status, nullableNumber(original.get(R_DAILY_APPROVED)));
|
|
|
+ McException.assertAccessException(!availability.isAvailable(), availability.getReason());
|
|
|
+ return original;
|
|
|
+ }
|
|
|
+
|
|
|
+ private MonthClosingStatusResult currentMonthStatus() {
|
|
|
+ String monthText = YearMonth.now(ZONE).format(MONTH);
|
|
|
+ return monthClosingService.status(MonthClosingCheckRequest.builder().monthText(monthText).build());
|
|
|
+ }
|
|
|
+
|
|
|
+ private void assertCurrentMonthOpen() {
|
|
|
+ MonthClosingStatusResult status = currentMonthStatus();
|
|
|
+ McException.assertAccessException(status == null || status.isClosed()
|
|
|
+ || status.getRunningClosingCount() > 0,
|
|
|
+ "当前月份已封账或封账/启封流程审批中,不可发起工时变更");
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateUpsertRequest(TimeCardRepairSummaryUpsertRequest request) {
|
|
|
+ McException.assertAccessException(request == null, "请求不能为空");
|
|
|
+ String dayText = request.getDayText();
|
|
|
+ McException.assertAccessException(StringUtils.isBlank(dayText) || !dayText.matches("\\d{8}"),
|
|
|
+ "日期必须为yyyyMMdd");
|
|
|
+ parseDay(dayText);
|
|
|
+ validateMonthRequest(request.getSubmitterUserId(), dayText.substring(0, 6));
|
|
|
+ assertCurrentMonthOpen();
|
|
|
+ McException.assertAccessException(StringUtils.isAnyBlank(
|
|
|
+ whConf.getFormUuidWorkHoursSummary(), whConf.getFormUuidRepairWorkHoursSummary(),
|
|
|
+ whConf.getRepairSummaryFillTypeField(), whConf.getRepairSummarySyncStatusField()),
|
|
|
+ "变更汇总配置不完整");
|
|
|
+ }
|
|
|
+
|
|
|
+ private void assertDraftMutable(Map summary, String userId, String dayText) {
|
|
|
+ assertDraftMutable(summary, userId, dayText, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void assertDraftMutable(Map summary, String userId, String dayText, String currentBatch) {
|
|
|
+ if (summary == null || summary.isEmpty()) return;
|
|
|
+ String status = text(summary.get(whConf.getRepairSummarySyncStatusField()));
|
|
|
+ boolean sameSubmittingBatch = "审批中".equals(status)
|
|
|
+ && StringUtils.isNotBlank(currentBatch)
|
|
|
+ && currentBatch.equals(text(summary.get(whConf.getRepairSummaryBatchField())));
|
|
|
+ McException.assertAccessException(("审批中".equals(status) && !sameSubmittingBatch)
|
|
|
+ || "部分成功".equals(status),
|
|
|
+ "该日期工时变更正在审批或回写中,不能再次修改 userId=" + userId + ", day=" + dayText);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map> queryChangeRecords(String userId, String monthText, String dayText) {
|
|
|
+ Map<String, Object> search = mapOf(R_USER, userId,
|
|
|
+ whConf.getRepairSummaryFillTypeField(), CHANGE_TYPE);
|
|
|
+ if (StringUtils.isNotBlank(monthText)) search.put(R_MONTH, monthText);
|
|
|
+ if (StringUtils.isNotBlank(dayText)) search.put(R_DAY, dayText);
|
|
|
+ return query(whConf.getFormUuidRepairWorkHoursSummary(), search);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void startApprovals(TimeCardRepairSubmitRequest request, Map<String, Object> groups,
|
|
|
+ Map other, String batch, long submitDate) {
|
|
|
+ for (Object raw : groups.values()) {
|
|
|
+ if (!(raw instanceof Map)) continue;
|
|
|
+ Map group = (Map) raw;
|
|
|
+ startProjectApproval(request, group, batch, submitDate);
|
|
|
+ }
|
|
|
+ if (other != null && !list(other.get("otherRows")).isEmpty()) {
|
|
|
+ startOtherApproval(request, other, batch, submitDate);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void startProjectApproval(TimeCardRepairSubmitRequest request, Map group,
|
|
|
+ String batch, long submitDate) {
|
|
|
+ Map<String, Object> data = approvalMainFields(request, batch, submitDate, false);
|
|
|
+ double billable = sumTargets(group.get("billableRows"));
|
|
|
+ double nonBillable = sumTargets(group.get("nonBillableRows"));
|
|
|
+ data.put("numberField_mmd2z155", billable);
|
|
|
+ data.put("numberField_mmd2z156", nonBillable);
|
|
|
+ data.put("numberField_mmd5b5gl", billable + nonBillable);
|
|
|
+ data.put("employeeField_mkoxpswf", Collections.singletonList(
|
|
|
+ normalizeManagerId(text(group.get("managerId")))));
|
|
|
+ data.put("tableField_mmae8t99", approvalRows(group.get("billableRows"), BILLABLE, request));
|
|
|
+ data.put("tableField_mmd1wu9h", approvalRows(group.get("nonBillableRows"), NON_BILLABLE, request));
|
|
|
+ startApproval(whConf.getFormUuidApproval(), whConf.getApprovalProcessCode(), data);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void startOtherApproval(TimeCardRepairSubmitRequest request, Map other,
|
|
|
+ String batch, long submitDate) {
|
|
|
+ Map<String, Object> data = approvalMainFields(request, batch, submitDate, true);
|
|
|
+ data.put("numberField_mmd5b5gl", sumTargets(other.get("otherRows")));
|
|
|
+ data.put("employeeField_mh8xhqc3", Collections.singletonList(
|
|
|
+ normalizeManagerId(text(other.get("managerId")))));
|
|
|
+ data.put("tableField_mmd1wu9h", approvalRows(other.get("otherRows"), OTHER, request));
|
|
|
+ startApproval(whConf.getFormUuidOtherApproval(), whConf.getOtherApprovalProcessCode(), data);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> approvalMainFields(TimeCardRepairSubmitRequest request,
|
|
|
+ String batch, long submitDate, boolean other) {
|
|
|
+ Map<String, Object> data = new LinkedHashMap<>();
|
|
|
+ data.put("dateField_mmacxewf", parseDay(request.getSourceMonthText() + "01"));
|
|
|
+ data.put("employeeField_moi9pvt1", Collections.singletonList(request.getSubmitterUserId()));
|
|
|
+ data.put(whConf.getApprovalFillTypeField(), CHANGE_TYPE);
|
|
|
+ data.put(other ? whConf.getOtherApprovalRepairSubmitDateField()
|
|
|
+ : whConf.getApprovalRepairSubmitDateField(), submitDate);
|
|
|
+ data.put(other ? whConf.getOtherApprovalRepairBatchField()
|
|
|
+ : whConf.getApprovalRepairBatchField(), batch);
|
|
|
+ data.put(other ? whConf.getOtherApprovalRepairSourceMonthField()
|
|
|
+ : whConf.getApprovalRepairSourceMonthField(), request.getSourceMonthText());
|
|
|
+ data.put("textField_mmd2lv0y", request.getSourceMonthText());
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map<String, Object>> approvalRows(Object raw, TableDef table,
|
|
|
+ TimeCardRepairSubmitRequest request) {
|
|
|
+ List<Map<String, Object>> rows = new ArrayList<>();
|
|
|
+ for (Map row : list(raw)) {
|
|
|
+ String dayText = rowDay(row);
|
|
|
+ Map<String, Object> out = new LinkedHashMap<>();
|
|
|
+ boolean billable = table == BILLABLE;
|
|
|
+ out.put(billable ? "dateField_mmbffvd8" : "dateField_mmd2z150", parseDay(dayText));
|
|
|
+ out.put(billable ? "textField_mmd8g654" : "textField_mmd8g655", dayText);
|
|
|
+ out.put(billable ? "textField_mmd1ozk4" : "textField_mmd1wu9f", request.getSubmitterUserId());
|
|
|
+ out.put(billable ? "employeeField_mmacxew4" : "employeeField_mmd1wu9b",
|
|
|
+ Collections.singletonList(request.getSubmitterUserId()));
|
|
|
+ out.put(billable ? "textField_mmdbi2n2" : "textField_mmdbi2n0", request.getSourceMonthText());
|
|
|
+ out.put(billable ? "numberField_mmacxew9" : "numberField_mmd1wu9e", num(row.get("hours")));
|
|
|
+ putApprovalIdentity(out, row, table);
|
|
|
+ out.put(table == NON_BILLABLE ? "radioField_mpwhk295" : "radioField_mpwhk294", "同意");
|
|
|
+ rows.add(out);
|
|
|
+ }
|
|
|
+ return rows;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void putApprovalIdentity(Map<String, Object> out, Map row, TableDef table) {
|
|
|
+ if (table == BILLABLE) {
|
|
|
+ out.put("textField_mmacxew3", text(row.get("projectCode")));
|
|
|
+ out.put("textField_mkov5ozd", text(row.get("projectName")));
|
|
|
+ } else if (table == NON_BILLABLE) {
|
|
|
+ out.put("textField_mmd1wu9d", text(row.get("projectCode")));
|
|
|
+ out.put("textField_mmd1wu9c", text(row.get("projectName")));
|
|
|
+ } else {
|
|
|
+ out.put("selectField_mmeakgie", text(row.get("activityName")));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void startApproval(String formUuid, String processCode, Map<String, Object> data) {
|
|
|
+ TimeCardApprovalStartRequest request = new TimeCardApprovalStartRequest();
|
|
|
+ request.setFormUuid(formUuid);
|
|
|
+ request.setProcessCode(processCode);
|
|
|
+ request.setFormDataJson(JSON.toJSONString(data));
|
|
|
+ reSubmitApprovalService.startApproval(request);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 变更审批补发:只扫描工时类型为“变更”的近期审批中记录。 */
|
|
|
+ public ChangeResubmitResult resubmitRecentlyModifiedPending(int lookbackDays) {
|
|
|
+ String[] window = modifiedDateWindow(lookbackDays);
|
|
|
+ List<Map> records = queryModified(whConf.getFormUuidRepairWorkHoursSummary(), window[0], window[1]);
|
|
|
+ Map<String, ChangeBatch> batches = buildPendingBatches(records);
|
|
|
+ int started = 0;
|
|
|
+ int failed = 0;
|
|
|
+ for (ChangeBatch batch : batches.values()) {
|
|
|
+ try {
|
|
|
+ Set<String> covered = loadApprovalCoverage(batch);
|
|
|
+ started += submitMissingApprovals(batch, covered);
|
|
|
+ } catch (RuntimeException ex) {
|
|
|
+ failed++;
|
|
|
+ log.error("[变更审批补发] 处理失败 batch={}", batch.batchId, ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return new ChangeResubmitResult(started, failed);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, ChangeBatch> buildPendingBatches(List<Map> records) {
|
|
|
+ Map<String, ChangeBatch> batches = new LinkedHashMap<>();
|
|
|
+ for (Map item : records) {
|
|
|
+ Map data = formData(item);
|
|
|
+ if (!CHANGE_TYPE.equals(text(data.get(whConf.getRepairSummaryFillTypeField())))
|
|
|
+ || !"审批中".equals(text(data.get(whConf.getRepairSummarySyncStatusField())))) continue;
|
|
|
+ ChangeBatch batch = pendingBatch(batches, data);
|
|
|
+ if (batch == null) continue;
|
|
|
+ collectPendingRows(batch, data, BILLABLE);
|
|
|
+ collectPendingRows(batch, data, NON_BILLABLE);
|
|
|
+ collectPendingRows(batch, data, OTHER);
|
|
|
+ }
|
|
|
+ return batches;
|
|
|
+ }
|
|
|
+
|
|
|
+ private ChangeBatch pendingBatch(Map<String, ChangeBatch> batches, Map data) {
|
|
|
+ String userId = text(data.get(R_USER));
|
|
|
+ String monthText = text(data.get(R_MONTH));
|
|
|
+ String batchId = text(data.get(whConf.getRepairSummaryBatchField()));
|
|
|
+ long submitDate = (long) num(data.get(whConf.getRepairSummarySubmitDateField()));
|
|
|
+ if (StringUtils.isAnyBlank(userId, monthText, batchId) || submitDate <= 0) return null;
|
|
|
+ String key = userId + "|" + monthText + "|" + batchId;
|
|
|
+ return batches.computeIfAbsent(key, ignored -> new ChangeBatch(
|
|
|
+ batchId, userId, text(data.get("textField_mmoazo1b")), monthText, submitDate));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void collectPendingRows(ChangeBatch batch, Map data, TableDef table) {
|
|
|
+ for (Map child : list(data.get(table.tableField))) {
|
|
|
+ double submitted = num(child.get(table.submittedField));
|
|
|
+ double approved = num(child.get(table.approvedField));
|
|
|
+ if (Math.abs(submitted - approved) <= EPSILON) continue;
|
|
|
+ if (table == OTHER) {
|
|
|
+ String manager = normalizeManagerId(firstUserId(data, "employeeField_mh8xhqc3"));
|
|
|
+ batch.otherGroup.put("managerId", manager);
|
|
|
+ changeRows(batch.otherGroup, "otherRows").add(mapOf(
|
|
|
+ "dateStr", text(data.get(R_DAY)), "activityName", text(child.get(table.keyField)),
|
|
|
+ "hours", submitted));
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String manager = normalizeManagerId(firstUserId(child, table.managerField));
|
|
|
+ Map<String, Object> group = changeGroup(batch, manager);
|
|
|
+ changeRows(group, table == BILLABLE ? "billableRows" : "nonBillableRows").add(mapOf(
|
|
|
+ "dateStr", text(data.get(R_DAY)), "projectCode", text(child.get(table.keyField)),
|
|
|
+ "projectName", text(child.get(table.nameField)), "hours", submitted));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private int submitMissingApprovals(ChangeBatch batch, Set<String> covered) {
|
|
|
+ removeCoveredRows(batch, covered);
|
|
|
+ if (batch.projectGroups.isEmpty() && batch.otherGroup.isEmpty()) return 0;
|
|
|
+ TimeCardRepairSubmitRequest request = new TimeCardRepairSubmitRequest();
|
|
|
+ request.setSourceMonthText(batch.monthText);
|
|
|
+ request.setSubmitterUserId(batch.userId);
|
|
|
+ request.setSubmitterName(batch.submitterName);
|
|
|
+ request.setProjectGroups(batch.projectGroups);
|
|
|
+ request.setOtherGroup(batch.otherGroup);
|
|
|
+ startApprovals(request, batch.projectGroups, batch.otherGroup, batch.batchId, batch.submitDate);
|
|
|
+ return batch.projectGroups.size() + (batch.otherGroup.isEmpty() ? 0 : 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Set<String> loadApprovalCoverage(ChangeBatch batch) {
|
|
|
+ Set<String> covered = new HashSet<>();
|
|
|
+ Map<String, Object> search = mapOf(whConf.getApprovalFillTypeField(), CHANGE_TYPE,
|
|
|
+ "textField_mmd2lv0y", batch.monthText);
|
|
|
+ collectApprovalCoverage(queryProcesses(whConf.getFormUuidApproval(), search), false,
|
|
|
+ batch.batchId, covered);
|
|
|
+ collectApprovalCoverage(queryProcesses(whConf.getFormUuidOtherApproval(), search), true,
|
|
|
+ batch.batchId, covered);
|
|
|
+ return covered;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void collectApprovalCoverage(List<Map> items, boolean other,
|
|
|
+ String batchId, Set<String> covered) {
|
|
|
+ for (Map item : items) {
|
|
|
+ String status = text(item.get("instanceStatus")).toUpperCase(Locale.ROOT);
|
|
|
+ if (!"RUNNING".equals(status) && !"COMPLETED".equals(status)) continue;
|
|
|
+ Map data = formData(item);
|
|
|
+ String approvalBatch = text(data.get(other
|
|
|
+ ? whConf.getOtherApprovalRepairBatchField()
|
|
|
+ : whConf.getApprovalRepairBatchField()));
|
|
|
+ if (!batchId.equals(approvalBatch)) continue;
|
|
|
+ if (other) {
|
|
|
+ addCoverage(covered, list(data.get("tableField_mmd1wu9h")), "other",
|
|
|
+ "textField_mmd1wu9f", "textField_mmd8g655", "selectField_mmeakgie");
|
|
|
+ } else {
|
|
|
+ addCoverage(covered, list(data.get("tableField_mmae8t99")), "billable",
|
|
|
+ "textField_mmd1ozk4", "textField_mmd8g654", "textField_mmacxew3");
|
|
|
+ addCoverage(covered, list(data.get("tableField_mmd1wu9h")), "nonBillable",
|
|
|
+ "textField_mmd1wu9f", "textField_mmd8g655", "textField_mmd1wu9d");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addCoverage(Set<String> covered, List<Map> rows, String category,
|
|
|
+ String userField, String dayField, String keyField) {
|
|
|
+ for (Map row : rows) {
|
|
|
+ covered.add(coverageKey(category, text(row.get(userField)),
|
|
|
+ text(row.get(dayField)), text(row.get(keyField))));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void removeCoveredRows(ChangeBatch batch, Set<String> covered) {
|
|
|
+ Iterator<Map.Entry<String, Object>> groups = batch.projectGroups.entrySet().iterator();
|
|
|
+ while (groups.hasNext()) {
|
|
|
+ Map group = (Map) groups.next().getValue();
|
|
|
+ removeCovered(batch.userId, list(group.get("billableRows")), "billable", covered);
|
|
|
+ removeCovered(batch.userId, list(group.get("nonBillableRows")), "nonBillable", covered);
|
|
|
+ if (list(group.get("billableRows")).isEmpty()
|
|
|
+ && list(group.get("nonBillableRows")).isEmpty()) groups.remove();
|
|
|
+ }
|
|
|
+ removeCovered(batch.userId, list(batch.otherGroup.get("otherRows")), "other", covered);
|
|
|
+ if (list(batch.otherGroup.get("otherRows")).isEmpty()) batch.otherGroup.clear();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void removeCovered(String userId, List<Map> rows, String category, Set<String> covered) {
|
|
|
+ rows.removeIf(row -> covered.contains(coverageKey(category, userId, rowDay(row),
|
|
|
+ "other".equals(category) ? text(row.get("activityName")) : text(row.get("projectCode")))));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String coverageKey(String category, String userId, String day, String key) {
|
|
|
+ return category + "|" + userId + "|" + day + "|" + key;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void updateAndVerifySummary(String id, Map<String, Object> update, TableDef table,
|
|
|
+ String key, double submitted, double delta) {
|
|
|
+ ydClient.operateData(YDParam.builder().appType(whConf.getYidaAppType())
|
|
|
+ .systemToken(whConf.getYidaSystemToken()).formInstanceId(id)
|
|
|
+ .updateFormDataJson(JSON.toJSONString(update)).ignoreEmpty(false).useLatestVersion(true).build(),
|
|
|
+ YDConf.FORM_OPERATION.update);
|
|
|
+ Map actual = detail(id);
|
|
|
+ Map row = findRow(list(actual.get(table.tableField)), table.keyField, key);
|
|
|
+ McException.assertAccessException(row == null
|
|
|
+ || !sameHours(num(row.get(table.submittedField)), submitted)
|
|
|
+ || !sameHours(num(row.get(table.changeField)), delta),
|
|
|
+ "变更汇总提交回读不一致 instanceId=" + id + ", key=" + key);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map> query(String formUuid, Map<String, Object> search) {
|
|
|
+ McException.assertAccessException(StringUtils.isBlank(formUuid), "工时表单未配置");
|
|
|
+ DDR_New response = ydClient.queryData(YDParam.builder().appType(whConf.getYidaAppType())
|
|
|
+ .systemToken(whConf.getYidaSystemToken()).formUuid(formUuid)
|
|
|
+ .searchFieldJson(JSON.toJSONString(search)).currentPage(1).pageSize(100).build(),
|
|
|
+ YDConf.FORM_QUERY.retrieve_search_form);
|
|
|
+ McException.assertAccessException(response == null || !(response.getData() instanceof List),
|
|
|
+ "查询工时数据失败");
|
|
|
+ List<Map> records = (List<Map>) response.getData();
|
|
|
+ McException.assertAccessException(records.size() >= 100, "工时记录超过单次查询上限");
|
|
|
+ return records;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map detail(String id) {
|
|
|
+ McException.assertAccessException(StringUtils.isBlank(id), "工时实例ID为空");
|
|
|
+ DDR_New response = ydClient.queryData(YDParam.builder().appType(whConf.getYidaAppType())
|
|
|
+ .systemToken(whConf.getYidaSystemToken()).formInstanceId(id).build(),
|
|
|
+ YDConf.FORM_QUERY.retrieve_id);
|
|
|
+ McException.assertAccessException(response == null || response.getFormData() == null,
|
|
|
+ "工时实例详情不存在 id=" + id);
|
|
|
+ return response.getFormData();
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map> queryModified(String formUuid, String fromDate, String toDate) {
|
|
|
+ List<Map> result = new ArrayList<>();
|
|
|
+ for (int page = 1; page <= 200; page++) {
|
|
|
+ Object raw = ydClient.queryData(YDParam.builder().appType(whConf.getYidaAppType())
|
|
|
+ .systemToken(whConf.getYidaSystemToken()).formUuid(formUuid)
|
|
|
+ .modifiedFromTimeGMT(fromDate).modifiedToTimeGMT(toDate)
|
|
|
+ .currentPage(page).pageSize(100).build(),
|
|
|
+ YDConf.FORM_QUERY.retrieve_search_form).getData();
|
|
|
+ List<Map> rows = raw instanceof List ? (List<Map>) raw : Collections.emptyList();
|
|
|
+ result.addAll(rows);
|
|
|
+ if (rows.size() < 100) break;
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map> queryProcesses(String formUuid, Map<String, Object> search) {
|
|
|
+ List<Map> result = new ArrayList<>();
|
|
|
+ for (int page = 1; page <= 200; page++) {
|
|
|
+ Object raw = ydClient.queryData(YDParam.builder().appType(whConf.getYidaAppType())
|
|
|
+ .systemToken(whConf.getYidaSystemToken()).formUuid(formUuid)
|
|
|
+ .searchFieldJson(JSON.toJSONString(search)).pageNumber(page).pageSize(100).build(),
|
|
|
+ YDConf.FORM_QUERY.retrieve_search_process).getData();
|
|
|
+ List<Map> rows = raw instanceof List ? (List<Map>) raw : Collections.emptyList();
|
|
|
+ result.addAll(rows);
|
|
|
+ if (rows.size() < 100) break;
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String[] modifiedDateWindow(int lookbackDays) {
|
|
|
+ int days = Math.max(1, lookbackDays);
|
|
|
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
+ LocalDate today = LocalDate.now(ZONE);
|
|
|
+ return new String[]{today.minusDays(days - 1L).format(formatter), today.plusDays(1L).format(formatter)};
|
|
|
+ }
|
|
|
+
|
|
|
+ private String submitGuardKey(TimeCardRepairSubmitRequest request,
|
|
|
+ Map<String, Object> groups, Map other) {
|
|
|
+ String payload = request.getSubmitterUserId() + "|" + request.getSourceMonthText()
|
|
|
+ + "|" + JSON.toJSONString(groups) + "|" + 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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) {
|
|
|
+ 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 void validateMonthRequest(String userId, String monthText) {
|
|
|
+ McException.assertAccessException(StringUtils.isAnyBlank(userId, monthText)
|
|
|
+ || !monthText.matches("\\d{6}"), "员工和月份不能为空且月份必须为yyyyMM");
|
|
|
+ try {
|
|
|
+ YearMonth.parse(monthText, MONTH);
|
|
|
+ } catch (RuntimeException ex) {
|
|
|
+ McException.assertAccessException(true, "月份必须为有效的yyyyMM");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map<String, Map> indexRows(Object raw, String keyField) {
|
|
|
+ Map<String, Map> result = new LinkedHashMap<>();
|
|
|
+ for (Map row : list(raw)) {
|
|
|
+ String key = text(row.get(keyField));
|
|
|
+ if (StringUtils.isNotBlank(key)) result.put(key, row);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<Map> writableRows(Object raw) {
|
|
|
+ List<Map> result = new ArrayList<>();
|
|
|
+ for (Map row : list(raw)) result.add(writableRow(row));
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map writableRow(Map source) {
|
|
|
+ Map result = new LinkedHashMap();
|
|
|
+ for (Object rawKey : source.keySet()) {
|
|
|
+ String key = text(rawKey);
|
|
|
+ if (key.endsWith("_value")) continue;
|
|
|
+ if (key.endsWith("_id") && key.startsWith("employeeField_")) {
|
|
|
+ result.put(key.substring(0, key.length() - 3), source.get(rawKey));
|
|
|
+ } else if (!key.startsWith("employeeField_") || source.get(key + "_id") == null) {
|
|
|
+ result.put(key, source.get(rawKey));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map findRow(List<Map> rows, String keyField, String key) {
|
|
|
+ for (Map row : rows) if (key.equals(text(row.get(keyField)))) return row;
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String targetKey(Map row, TableDef table) {
|
|
|
+ return table == OTHER ? text(row.get("activityName")) : text(row.get("projectCode"));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String rowDay(Map row) {
|
|
|
+ String day = text(row.get("dateStr"));
|
|
|
+ return StringUtils.isBlank(day) ? text(row.get("dayText")) : day;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void putManager(Map row, TableDef table, String managerId) {
|
|
|
+ if (table != OTHER && StringUtils.isNotBlank(managerId)) {
|
|
|
+ row.put(table.managerField, Collections.singletonList(managerId));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map<String, Object> safeMap(Map<String, Object> value) {
|
|
|
+ return value == null ? new LinkedHashMap<>() : new LinkedHashMap<>(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void copyIfPresent(Map source, Map<String, Object> target, String field) {
|
|
|
+ if (source != null && source.get(field) != null) target.put(field, source.get(field));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void copyIfMissing(Map source, Map target, String field) {
|
|
|
+ if (StringUtils.isNotBlank(field) && target.get(field) == null && source.get(field) != null) {
|
|
|
+ target.put(field, source.get(field));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map<String, Object> changeGroup(ChangeBatch batch, String managerId) {
|
|
|
+ return (Map<String, Object>) batch.projectGroups.computeIfAbsent(managerId, ignored -> {
|
|
|
+ Map<String, Object> group = new LinkedHashMap<>();
|
|
|
+ group.put("managerId", managerId);
|
|
|
+ group.put("billableRows", new ArrayList<Map>());
|
|
|
+ group.put("nonBillableRows", new ArrayList<Map>());
|
|
|
+ return group;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<Map<String, Object>> changeRows(Map group, String key) {
|
|
|
+ return (List<Map<String, Object>>) group.computeIfAbsent(key, ignored -> new ArrayList<Map<String, Object>>());
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String instanceId(Map item) {
|
|
|
+ String id = text(item.get("formInstId"));
|
|
|
+ return StringUtils.isBlank(id) ? text(item.get("formInstanceId")) : id;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map formData(Map item) {
|
|
|
+ Object data = item == null ? null : item.get("formData");
|
|
|
+ if (!(data instanceof Map)) data = item == null ? null : item.get("data");
|
|
|
+ return data instanceof Map ? (Map) data : (item == null ? Collections.emptyMap() : item);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String firstUserId(Map data, String field) {
|
|
|
+ Object value = data == null ? null : data.get(field + "_id");
|
|
|
+ if (!(value instanceof List) || ((List) value).isEmpty()) value = data == null ? null : data.get(field);
|
|
|
+ if (value instanceof List && !((List) value).isEmpty()) return text(((List) value).get(0));
|
|
|
+ return text(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String normalizeManagerId(String managerId) {
|
|
|
+ return StringUtils.isBlank(managerId) ? DEFAULT_MANAGER_ID : managerId;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static long parseDay(String dayText) {
|
|
|
+ try {
|
|
|
+ return LocalDate.parse(dayText, DAY).atStartOfDay(ZONE).toInstant().toEpochMilli();
|
|
|
+ } catch (RuntimeException ex) {
|
|
|
+ throw new IllegalArgumentException("日期必须为有效的yyyyMMdd", ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Double nullableNumber(Object value) {
|
|
|
+ if (value == null || StringUtils.isBlank(text(value))) return null;
|
|
|
+ try {
|
|
|
+ return Double.valueOf(text(value));
|
|
|
+ } catch (NumberFormatException ex) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean sameHours(double left, double right) {
|
|
|
+ return Math.abs(left - right) <= EPSILON;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static double sumTargets(Object raw) {
|
|
|
+ double total = 0d;
|
|
|
+ for (Map row : list(raw)) total += num(row.get("hours"));
|
|
|
+ return total;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String duplicateMessage(TimeCardRepairSummaryUpsertRequest request) {
|
|
|
+ return "变更汇总记录重复,已停止保存 userId=" + request.getSubmitterUserId()
|
|
|
+ + ", day=" + request.getDayText();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map<String, Object> mapOf(Object... values) {
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ for (int i = 0; i < values.length; i += 2) result.put(String.valueOf(values[i]), values[i + 1]);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<Map> list(Object raw) {
|
|
|
+ return raw instanceof List ? (List<Map>) raw : new ArrayList<>();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String text(Object value) {
|
|
|
+ return value == null ? "" : String.valueOf(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static double num(Object value) {
|
|
|
+ try {
|
|
|
+ return value == null ? 0d : Double.parseDouble(String.valueOf(value));
|
|
|
+ } catch (NumberFormatException ex) {
|
|
|
+ return 0d;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final class TableDef {
|
|
|
+ private final String tableField;
|
|
|
+ private final String keyField;
|
|
|
+ private final String nameField;
|
|
|
+ private final String managerField;
|
|
|
+ private final String draftField;
|
|
|
+ private final String submittedField;
|
|
|
+ private final String approvedField;
|
|
|
+ private final String changeField;
|
|
|
+
|
|
|
+ private TableDef(String tableField, String keyField, String nameField, String managerField,
|
|
|
+ String draftField, String submittedField, String approvedField, String changeField) {
|
|
|
+ this.tableField = tableField;
|
|
|
+ this.keyField = keyField;
|
|
|
+ this.nameField = nameField;
|
|
|
+ this.managerField = managerField;
|
|
|
+ this.draftField = draftField;
|
|
|
+ this.submittedField = submittedField;
|
|
|
+ this.approvedField = approvedField;
|
|
|
+ this.changeField = changeField;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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) {
|
|
|
+ result = new LinkedHashMap<>(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private synchronized Map<String, Object> cachedResult() {
|
|
|
+ return result == null ? null : new LinkedHashMap<>(result);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final class ChangeBatch {
|
|
|
+ private final String batchId;
|
|
|
+ private final String userId;
|
|
|
+ private final String submitterName;
|
|
|
+ private final String monthText;
|
|
|
+ private final long submitDate;
|
|
|
+ private final Map<String, Object> projectGroups = new LinkedHashMap<>();
|
|
|
+ private final Map<String, Object> otherGroup = new LinkedHashMap<>();
|
|
|
+
|
|
|
+ private ChangeBatch(String batchId, String userId, String submitterName,
|
|
|
+ String monthText, long submitDate) {
|
|
|
+ this.batchId = batchId;
|
|
|
+ this.userId = userId;
|
|
|
+ this.submitterName = submitterName;
|
|
|
+ this.monthText = monthText;
|
|
|
+ this.submitDate = submitDate;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static final class ChangeResubmitResult {
|
|
|
+ private final int started;
|
|
|
+ private final int failed;
|
|
|
+
|
|
|
+ public ChangeResubmitResult(int started, int failed) {
|
|
|
+ this.started = started;
|
|
|
+ this.failed = failed;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int getStarted() {
|
|
|
+ return started;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int getFailed() {
|
|
|
+ return failed;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|