Forráskód Böngészése

fix(workhours): align repair closing and submission checks

malk 1 hónapja%!(EXTRA string=óta)
szülő
commit
0abf302eb0

+ 48 - 3
mjava-akdsbeisen/src/main/java/com/malk/service/workhours/MonthClosingService.java

@@ -40,6 +40,7 @@ public class MonthClosingService {
     private static final String APPROVAL_MONTH_FIELD = "textField_mmd2lv0y";
     private static final String SUMMARY_MONTH_FIELD = "textField_mmbffvda";
     private static final String SUMMARY_STATUS_IN_APPROVAL = "审批中";
+    private static final String REPAIR_SUMMARY_STATUS_PARTIAL = "部分成功";
     private static final String WRITE_BACK_SUCCESS = "全部成功";
     private static final String STATUS_CLOSED = "封账";
     private static final String STATUS_REOPENED = "启封";
@@ -112,12 +113,21 @@ public class MonthClosingService {
                 whConf.getFormUuidApproval(), APPROVAL_MONTH_FIELD, monthText, "COMPLETED"));
         List<Map> otherCompleted = normalApprovalProcesses(queryProcesses(
                 whConf.getFormUuidOtherApproval(), APPROVAL_MONTH_FIELD, monthText, "COMPLETED"));
+        projectRunning.addAll(repairApprovalProcesses(whConf.getFormUuidApproval(),
+                whConf.getApprovalRepairSubmitDateField(), monthText, "RUNNING"));
+        otherRunning.addAll(repairApprovalProcesses(whConf.getFormUuidOtherApproval(),
+                whConf.getOtherApprovalRepairSubmitDateField(), monthText, "RUNNING"));
+        projectCompleted.addAll(repairApprovalProcesses(whConf.getFormUuidApproval(),
+                whConf.getApprovalRepairSubmitDateField(), monthText, "COMPLETED"));
+        otherCompleted.addAll(repairApprovalProcesses(whConf.getFormUuidOtherApproval(),
+                whConf.getOtherApprovalRepairSubmitDateField(), monthText, "COMPLETED"));
 
         int writeBackPendingCount = countWriteBackPending(
                 projectCompleted, whConf.getApprovalSyncStatusField())
                 + countWriteBackPending(
                 otherCompleted, whConf.getOtherApprovalSyncStatusField());
-        int summaryPendingCount = querySummaryPendingCount(monthText);
+        int summaryPendingCount = querySummaryPendingCount(monthText)
+                + queryRepairSummaryPendingCount(monthText);
         return buildValidationResult(
                 monthText,
                 STATUS_CLOSED,
@@ -128,7 +138,7 @@ public class MonthClosingService {
                 summaryPendingCount);
     }
 
-    /** 补填审批不参与“正常填报是否全部结束”的封账前置校验。 */
+    /** 正常填报沿用填报月份口径;历史空类型按正常填报处理。 */
     private List<Map> normalApprovalProcesses(List<Map> processes) {
         List<Map> result = new ArrayList<>();
         String fillTypeField = whConf.getApprovalFillTypeField();
@@ -141,6 +151,22 @@ public class MonthClosingService {
         return result;
     }
 
+    /** 补填审批按提交日期归属封账月份,来源月份仅用于定位被补录的工时。 */
+    private List<Map> repairApprovalProcesses(String formUuid, String submitDateField,
+                                               String monthText, String instanceStatus) {
+        McException.assertAccessException(StringUtils.isAnyBlank(
+                whConf.getApprovalFillTypeField(), submitDateField), "补填审批查询配置不完整");
+        List<Map> result = new ArrayList<>();
+        for (Map item : queryProcesses(formUuid, null, null, instanceStatus)) {
+            Map data = extractFormData(item);
+            if ("补填".equals(text(data.get(whConf.getApprovalFillTypeField())))
+                    && monthText.equals(monthFromDateValue(data.get(submitDateField)))) {
+                result.add(item);
+            }
+        }
+        return result;
+    }
+
     private MonthClosingValidationResult validateReopening(
             String monthText,
             MonthClosingStatusResult closingStatus) {
@@ -268,6 +294,25 @@ public class MonthClosingService {
         return count;
     }
 
+    private int queryRepairSummaryPendingCount(String monthText) {
+        McException.assertAccessException(StringUtils.isAnyBlank(
+                whConf.getFormUuidRepairWorkHoursSummary(),
+                whConf.getRepairSummarySubmitDateField(),
+                whConf.getRepairSummarySyncStatusField()), "补填汇总查询配置不完整");
+        int count = 0;
+        // fixme 补填提交日期是复制表新增字段;完整分页后本地匹配月份,避免查询索引漏掉待回写记录。
+        for (Map item : queryForms(whConf.getFormUuidRepairWorkHoursSummary(), new HashMap<>())) {
+            Map data = extractFormData(item);
+            String status = text(data.get(whConf.getRepairSummarySyncStatusField()));
+            if (monthText.equals(monthFromDateValue(data.get(whConf.getRepairSummarySubmitDateField())))
+                    && (SUMMARY_STATUS_IN_APPROVAL.equals(status)
+                    || REPAIR_SUMMARY_STATUS_PARTIAL.equals(status))) {
+                count++;
+            }
+        }
+        return count;
+    }
+
     private List<Map> queryProcesses(
             String formUuid,
             String monthField,
@@ -530,7 +575,7 @@ public class MonthClosingService {
             return monthText + " 有 " + writeBackPendingCount + " 张审批单尚未成功回写工时汇总";
         }
         if (summaryPendingCount > 0) {
-            return monthText + " 仍有 " + summaryPendingCount + " 条工时汇总处于审批中";
+            return monthText + " 仍有 " + summaryPendingCount + " 条工时汇总尚未完成审批回写";
         }
         return monthText + " 工时审批已完成,可以提交封账";
     }

+ 11 - 1
mjava-akdsbeisen/src/main/java/com/malk/service/workhours/TimeCardRepairService.java

@@ -4,6 +4,8 @@ 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.workhours.MonthClosingCheckRequest;
+import com.malk.server.workhours.MonthClosingStatusResult;
 import com.malk.server.workhours.TimeCardApprovalStartRequest;
 import com.malk.server.workhours.TimeCardRepairContextRequest;
 import com.malk.server.workhours.TimeCardRepairSubmitRequest;
@@ -106,6 +108,14 @@ public class TimeCardRepairService {
         McException.assertAccessException(request == null, "请求不能为空");
         validateMonthRequest(request.getSubmitterUserId(), request.getSourceMonthText());
         McException.assertAccessException(!monthClosingService.isClosed(request.getSourceMonthText()), "补填只能提交已封账月份");
+        LocalDate submitDay = LocalDate.now(ZONE);
+        String submitMonthText = submitDay.format(MONTH);
+        MonthClosingStatusResult submitMonthStatus = monthClosingService.status(
+                MonthClosingCheckRequest.builder().monthText(submitMonthText).build());
+        // prd 补填写入当前月变更工时;当前月进入封账流程或已封账后禁止新增提交。
+        McException.assertAccessException(submitMonthStatus.isClosed()
+                        || submitMonthStatus.getRunningClosingCount() > 0,
+                "当前月份已封账或封账流程审批中,不可提交补填");
         Map<String, Object> projectGroups = request.getProjectGroups() == null ? Collections.emptyMap() : request.getProjectGroups();
         Map other = request.getOtherGroup() == null ? Collections.emptyMap() : request.getOtherGroup();
         String guardKey = buildSubmitGuardKey(request, projectGroups, other);
@@ -114,7 +124,7 @@ public class TimeCardRepairService {
         if (cachedResult != null) return cachedResult;
         try {
             String batch = UUID.randomUUID().toString();
-            long submitDate = LocalDate.now(ZONE).atStartOfDay(ZONE).toInstant().toEpochMilli();
+            long submitDate = submitDay.atStartOfDay(ZONE).toInstant().toEpochMilli();
             int rowCount = validateSubmitRows(request, projectGroups, other);
             McException.assertAccessException(rowCount == 0, "没有需要提交的补填工时");
             for (Object rawGroup : projectGroups.values()) {

+ 154 - 0
mjava-akdsbeisen/src/test/java/com/malk/service/workhours/MonthClosingServiceTest.java

@@ -37,10 +37,17 @@ class MonthClosingServiceTest {
     private static final String PROJECT_FORM = "FORM-PROJECT-APPROVAL";
     private static final String OTHER_FORM = "FORM-OTHER-APPROVAL";
     private static final String SUMMARY_FORM = "FORM-SUMMARY";
+    private static final String REPAIR_SUMMARY_FORM = "FORM-REPAIR-SUMMARY";
     private static final String CLOSING_MONTH_FIELD = "dateField_closing_month";
     private static final String CLOSING_STATUS_FIELD = "radioField_closing_status";
     private static final String SYNC_STATUS_FIELD = "selectField_sync_status";
     private static final String SUMMARY_STATUS_FIELD = "selectField_summary_status";
+    private static final String APPROVAL_MONTH_FIELD = "textField_mmd2lv0y";
+    private static final String FILL_TYPE_FIELD = "radioField_fill_type";
+    private static final String PROJECT_REPAIR_SUBMIT_DATE_FIELD = "dateField_project_repair_submit";
+    private static final String OTHER_REPAIR_SUBMIT_DATE_FIELD = "dateField_other_repair_submit";
+    private static final String REPAIR_SUMMARY_SUBMIT_DATE_FIELD = "dateField_repair_summary_submit";
+    private static final String REPAIR_SUMMARY_STATUS_FIELD = "selectField_repair_summary_status";
 
     @Test
     void shouldRejectInvalidMonthBeforeQueryingYida() {
@@ -207,6 +214,147 @@ class MonthClosingServiceTest {
         assertEquals(1, result.getWriteBackPendingCount());
     }
 
+    @Test
+    void shouldAssignRunningRepairApprovalToSubmitMonthInsteadOfSourceMonth() {
+        YDClient ydClient = mock(YDClient.class);
+        when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
+                .thenAnswer(invocation -> {
+                    YDParam param = invocation.getArgument(0);
+                    if (PROJECT_FORM.equals(param.getFormUuid())
+                            && "RUNNING".equals(param.getInstanceStatus())) {
+                        return response(Collections.singletonList(
+                                repairApproval(PROJECT_REPAIR_SUBMIT_DATE_FIELD,
+                                        "2026-09-15", "", "")));
+                    }
+                    return response(Collections.emptyList());
+                });
+        MonthClosingService service = service(ydClient);
+
+        MonthClosingValidationResult september = service.validate(request("202609"));
+        MonthClosingValidationResult august = service.validate(request("202608"));
+
+        assertFalse(september.isAllowed());
+        assertEquals(1, september.getProjectRunningCount());
+        assertTrue(august.isAllowed());
+    }
+
+    @Test
+    void shouldBlockClosingWhenOtherRepairApprovalAwaitsWriteBack() {
+        YDClient ydClient = mock(YDClient.class);
+        when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
+                .thenAnswer(invocation -> {
+                    YDParam param = invocation.getArgument(0);
+                    if (OTHER_FORM.equals(param.getFormUuid())
+                            && "COMPLETED".equals(param.getInstanceStatus())) {
+                        return response(Collections.singletonList(
+                                repairApproval(OTHER_REPAIR_SUBMIT_DATE_FIELD,
+                                        "2026-09-15", "agree", "部分失败")));
+                    }
+                    return response(Collections.emptyList());
+                });
+        MonthClosingService service = service(ydClient);
+
+        MonthClosingValidationResult result = service.validate(request("202609"));
+
+        assertFalse(result.isAllowed());
+        assertEquals(1, result.getWriteBackPendingCount());
+    }
+
+    @Test
+    void shouldAllowClosingAfterRepairApprovalAndSummaryWriteBackFinish() {
+        YDClient ydClient = mock(YDClient.class);
+        when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
+                .thenAnswer(invocation -> {
+                    YDParam param = invocation.getArgument(0);
+                    if (OTHER_FORM.equals(param.getFormUuid())
+                            && "COMPLETED".equals(param.getInstanceStatus())) {
+                        return response(Collections.singletonList(
+                                repairApproval(OTHER_REPAIR_SUBMIT_DATE_FIELD,
+                                        "2026-09-15", "agree", "全部成功")));
+                    }
+                    if (REPAIR_SUMMARY_FORM.equals(param.getFormUuid())) {
+                        Map<String, Object> data = new HashMap<>();
+                        data.put(REPAIR_SUMMARY_SUBMIT_DATE_FIELD, "2026-09-15");
+                        data.put(REPAIR_SUMMARY_STATUS_FIELD, "全部成功");
+                        Map<String, Object> item = new HashMap<>();
+                        item.put("formData", data);
+                        return response(Collections.singletonList(item));
+                    }
+                    return response(Collections.emptyList());
+                });
+        MonthClosingService service = service(ydClient);
+
+        MonthClosingValidationResult result = service.validate(request("202609"));
+
+        assertTrue(result.isAllowed());
+        assertEquals(0, result.getWriteBackPendingCount());
+        assertEquals(0, result.getSummaryPendingCount());
+    }
+
+    @Test
+    void shouldBlockClosingWhileRepairSummaryWriteBackIsPartial() {
+        YDClient ydClient = mock(YDClient.class);
+        when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
+                .thenAnswer(invocation -> {
+                    YDParam param = invocation.getArgument(0);
+                    if (REPAIR_SUMMARY_FORM.equals(param.getFormUuid())) {
+                        Map<String, Object> data = new HashMap<>();
+                        data.put(REPAIR_SUMMARY_SUBMIT_DATE_FIELD, "2026-09-15");
+                        data.put(REPAIR_SUMMARY_STATUS_FIELD, "部分成功");
+                        Map<String, Object> item = new HashMap<>();
+                        item.put("formData", data);
+                        return response(Collections.singletonList(item));
+                    }
+                    return response(Collections.emptyList());
+                });
+        MonthClosingService service = service(ydClient);
+
+        MonthClosingValidationResult result = service.validate(request("202609"));
+        MonthClosingValidationResult previousMonth = service.validate(request("202608"));
+
+        assertFalse(result.isAllowed());
+        assertEquals(1, result.getSummaryPendingCount());
+        assertTrue(previousMonth.isAllowed());
+    }
+
+    @Test
+    void shouldKeepBlockingClosingForNormalSummaryInApproval() {
+        YDClient ydClient = mock(YDClient.class);
+        when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
+                .thenAnswer(invocation -> {
+                    YDParam param = invocation.getArgument(0);
+                    if (SUMMARY_FORM.equals(param.getFormUuid())) {
+                        Map<String, Object> data = new HashMap<>();
+                        data.put("textField_mmbffvda", "202609");
+                        data.put(SUMMARY_STATUS_FIELD, "审批中");
+                        Map<String, Object> item = new HashMap<>();
+                        item.put("formData", data);
+                        return response(Collections.singletonList(item));
+                    }
+                    return response(Collections.emptyList());
+                });
+        MonthClosingService service = service(ydClient);
+
+        MonthClosingValidationResult result = service.validate(request("202609"));
+
+        assertFalse(result.isAllowed());
+        assertEquals(1, result.getSummaryPendingCount());
+    }
+
+    private static Map<String, Object> repairApproval(String submitDateField,
+                                                       String submitDate, String result,
+                                                       String syncStatus) {
+        Map<String, Object> formData = new HashMap<>();
+        formData.put(APPROVAL_MONTH_FIELD, "202608");
+        formData.put(FILL_TYPE_FIELD, "补填");
+        formData.put(submitDateField, submitDate);
+        formData.put(SYNC_STATUS_FIELD, syncStatus);
+        Map<String, Object> item = new HashMap<>();
+        item.put("approvedResult", result);
+        item.put("formData", formData);
+        return item;
+    }
+
     private DDR_New queryResponse(InvocationOnMock invocation) {
         YDParam param = invocation.getArgument(0);
         YDConf.FORM_QUERY queryType = invocation.getArgument(1);
@@ -227,9 +375,15 @@ class MonthClosingServiceTest {
         whConf.setFormUuidApproval(PROJECT_FORM);
         whConf.setFormUuidOtherApproval(OTHER_FORM);
         whConf.setFormUuidWorkHoursSummary(SUMMARY_FORM);
+        whConf.setFormUuidRepairWorkHoursSummary(REPAIR_SUMMARY_FORM);
         whConf.setApprovalSyncStatusField(SYNC_STATUS_FIELD);
         whConf.setOtherApprovalSyncStatusField(SYNC_STATUS_FIELD);
         whConf.setSummaryApprovalStatusField(SUMMARY_STATUS_FIELD);
+        whConf.setApprovalFillTypeField(FILL_TYPE_FIELD);
+        whConf.setApprovalRepairSubmitDateField(PROJECT_REPAIR_SUBMIT_DATE_FIELD);
+        whConf.setOtherApprovalRepairSubmitDateField(OTHER_REPAIR_SUBMIT_DATE_FIELD);
+        whConf.setRepairSummarySubmitDateField(REPAIR_SUMMARY_SUBMIT_DATE_FIELD);
+        whConf.setRepairSummarySyncStatusField(REPAIR_SUMMARY_STATUS_FIELD);
 
         MonthClosingConf closingConf = new MonthClosingConf();
         closingConf.setFormUuid(CLOSING_FORM);

+ 68 - 1
mjava-akdsbeisen/src/test/java/com/malk/service/workhours/TimeCardRepairServiceTest.java

@@ -2,29 +2,96 @@ package com.malk.service.workhours;
 
 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.WHConf;
 import com.malk.service.aliwork.YDClient;
 import org.junit.jupiter.api.Test;
 import org.springframework.test.util.ReflectionTestUtils;
 
+import java.time.YearMonth;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
-import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 class TimeCardRepairServiceTest {
 
+    @Test
+    void shouldBlockRepairSubmissionWhenCurrentMonthClosingIsRunning() {
+        assertCurrentMonthBlocksSubmit(false, 1);
+    }
+
+    @Test
+    void shouldBlockRepairSubmissionWhenCurrentMonthIsClosed() {
+        assertCurrentMonthBlocksSubmit(true, 0);
+    }
+
+    @Test
+    void shouldAllowRepairSubmissionPastClosingCheckWhenCurrentMonthIsOpen() {
+        MonthClosingService closingService = mock(MonthClosingService.class);
+        YDClient client = mock(YDClient.class);
+        TimeCardRepairSubmitRequest request = repairSubmitRequest();
+        String currentMonth = YearMonth.now(ZoneId.of("Asia/Shanghai"))
+                .format(DateTimeFormatter.ofPattern("yyyyMM"));
+        when(closingService.isClosed(request.getSourceMonthText())).thenReturn(true);
+        when(closingService.status(any(MonthClosingCheckRequest.class)))
+                .thenReturn(MonthClosingStatusResult.builder().monthText(currentMonth).build());
+        TimeCardRepairService service = new TimeCardRepairService();
+        ReflectionTestUtils.setField(service, "monthClosingService", closingService);
+        ReflectionTestUtils.setField(service, "ydClient", client);
+
+        McException error = assertThrows(McException.class, () -> service.submit(request));
+
+        assertEquals("没有需要提交的补填工时", error.getMessage());
+        verify(closingService).status(org.mockito.ArgumentMatchers.argThat(
+                value -> currentMonth.equals(value.getMonthText())));
+        verifyNoInteractions(client);
+    }
+
+    private static void assertCurrentMonthBlocksSubmit(boolean closed, int runningCount) {
+        MonthClosingService closingService = mock(MonthClosingService.class);
+        YDClient client = mock(YDClient.class);
+        TimeCardRepairSubmitRequest request = repairSubmitRequest();
+        when(closingService.isClosed(request.getSourceMonthText())).thenReturn(true);
+        when(closingService.status(any(MonthClosingCheckRequest.class)))
+                .thenReturn(MonthClosingStatusResult.builder()
+                        .closed(closed).runningClosingCount(runningCount).build());
+        TimeCardRepairService service = new TimeCardRepairService();
+        ReflectionTestUtils.setField(service, "monthClosingService", closingService);
+        ReflectionTestUtils.setField(service, "ydClient", client);
+
+        McException error = assertThrows(McException.class, () -> service.submit(request));
+
+        assertEquals("当前月份已封账或封账流程审批中,不可提交补填", error.getMessage());
+        verifyNoInteractions(client);
+    }
+
+    private static TimeCardRepairSubmitRequest repairSubmitRequest() {
+        TimeCardRepairSubmitRequest request = new TimeCardRepairSubmitRequest();
+        request.setSubmitterUserId("user-1");
+        request.setSourceMonthText(YearMonth.now(ZoneId.of("Asia/Shanghai"))
+                .minusMonths(1).format(DateTimeFormatter.ofPattern("yyyyMM")));
+        return request;
+    }
+
     @Test
     void shouldAllowRepairWhenOriginalSummaryOnlyHasDraftHours() {
         Map<String, Object> formData = new HashMap<>();