Преглед изворни кода

fix(timecard): block approvals while month closing is running

malk пре 1 месец
родитељ
комит
f33f60e1ca

+ 12 - 0
mjava-akdsbeisen/src/main/java/com/malk/service/workhours/MonthClosingService.java

@@ -81,6 +81,18 @@ public class MonthClosingService {
         return status(request).isClosed();
     }
 
+    /**
+     * 正常工时写入锁:封账流程中与已审批封账均禁止新增或追加审批。
+     *
+     * @param monthText 工时所属月份,格式 yyyyMM
+     * @return true 表示当月已经锁定
+     */
+    public boolean isLocked(String monthText) {
+        MonthClosingStatusResult result = status(MonthClosingCheckRequest.builder()
+                .monthText(monthText).build());
+        return result.isClosed() || result.getRunningClosingCount() > 0;
+    }
+
     /**
      * 校验目标月份是否允许发起封账。
      *

+ 38 - 0
mjava-akdsbeisen/src/main/java/com/malk/service/workhours/ReSubmitApprovalService.java

@@ -60,6 +60,9 @@ public class ReSubmitApprovalService {
     @Autowired
     private WHConf whConf;
 
+    @Autowired
+    private MonthClosingService monthClosingService;
+
     @Autowired
     private DDClient ddClient;
 
@@ -264,6 +267,10 @@ public class ReSubmitApprovalService {
                                            String submitterUserId,
                                            boolean dryRun) {
         McException.assertAccessException(StringUtils.isBlank(monthText), "月份不能为空");
+        if (!dryRun) {
+            // prd 封账流程进入审批后,当月不得补发起或追加正常工时审批。
+            assertMonthOpen(monthText);
+        }
         McException.assertAccessException(StringUtils.isBlank(whConf.getApprovalProcessCode()),
                 "工时审批 processCode 未配置(workhours.approvalProcessCode)");
         McException.assertAccessException(StringUtils.isBlank(whConf.getOtherApprovalProcessCode()),
@@ -460,6 +467,9 @@ public class ReSubmitApprovalService {
 
         Map<String, Object> formData = JSON.parseObject(
                 request.getFormDataJson(), Map.class);
+        McException.assertAccessException(formData == null, "审批表单数据不能为空");
+        // prd 正常审批看填报月份,补填审批看提交月份;两者共用本发起入口。
+        assertMonthOpen(approvalGuardMonth(request.getFormUuid(), formData));
         String fillTypeField = whConf.getApprovalFillTypeField();
         boolean repair = StringUtils.isNotBlank(fillTypeField)
                 && "补填".equals(str(formData, fillTypeField));
@@ -1293,6 +1303,7 @@ public class ReSubmitApprovalService {
         }
         updateData.put(A_VIEWERS, new ArrayList<>(viewerSet));
 
+        assertMonthOpen(monthText);
         ydClient.operateData(YDParam.builder()
                 .appType(whConf.getYidaAppType())
                 .systemToken(whConf.getYidaSystemToken())
@@ -1510,6 +1521,7 @@ public class ReSubmitApprovalService {
         }
         updateData.put(O_VIEWERS, new ArrayList<>(viewerSet));
 
+        assertMonthOpen(monthText);
         ydClient.operateData(YDParam.builder()
                 .appType(whConf.getYidaAppType())
                 .systemToken(whConf.getYidaSystemToken())
@@ -1743,6 +1755,7 @@ public class ReSubmitApprovalService {
                                       String processCode,
                                       Map<String, Object> formData,
                                       String deptId) {
+        assertMonthOpen(approvalGuardMonth(formUuid, formData));
         String fillTypeField = whConf.getApprovalFillTypeField();
         if (StringUtils.isNotBlank(fillTypeField)
                 && StringUtils.isBlank(str(formData, fillTypeField))) {
@@ -1760,6 +1773,31 @@ public class ReSubmitApprovalService {
                 .build(), YDConf.FORM_OPERATION.start);
     }
 
+    /**
+     * @param monthText 需要校验封账的月份
+     */
+    private void assertMonthOpen(String monthText) {
+        MonthClosingService.validateMonthText(monthText);
+        McException.assertAccessException(monthClosingService.isLocked(monthText),
+                "工时月份已封账或封账流程审批中,不可新增或追加审批");
+    }
+
+    /**
+     * @param formUuid 审批表单 ID
+     * @param formData 审批主表数据
+     * @return 需要校验封账的业务月份
+     */
+    private String approvalGuardMonth(String formUuid, Map<String, Object> formData) {
+        if (!"补填".equals(str(formData, whConf.getApprovalFillTypeField()))) {
+            return str(formData, A_MONTH_TEXT);
+        }
+        String submitDateField = whConf.getFormUuidApproval().equals(formUuid)
+                ? whConf.getApprovalRepairSubmitDateField()
+                : whConf.getOtherApprovalRepairSubmitDateField();
+        McException.assertAccessException(StringUtils.isBlank(submitDateField), "补填提交日期字段未配置");
+        return MonthClosingService.monthFromDateValue(formData.get(submitDateField));
+    }
+
     /**
      * 查该月 + PM/deptManager + RUNNING 的审批单;分页取全
      */

+ 4 - 4
mjava-akdsbeisen/src/main/java/com/malk/timer/ReSubmitApprovalTimer.java

@@ -123,8 +123,8 @@ public class ReSubmitApprovalTimer {
             }
             for (Map.Entry<String, String> target : retryQueue) {
                 try {
-                    if (monthClosingService.isClosed(target.getKey())) {
-                        log.warn("[补发起兜底] 补试前月份已封账,跳过 month={} submitter={}",
+                    if (monthClosingService.isLocked(target.getKey())) {
+                        log.warn("[补发起兜底] 补试前月份已锁定,跳过 month={} submitter={}",
                                 target.getKey(), target.getValue());
                         continue;
                     }
@@ -166,8 +166,8 @@ public class ReSubmitApprovalTimer {
         for (Map.Entry<String, Set<String>> monthEntry : targets.entrySet()) {
             String monthText = monthEntry.getKey();
             try {
-                if (monthClosingService.isClosed(monthText)) {
-                    log.info("[补发起兜底] 月份已封账,跳过本月全部目标 month={} submitters={}",
+                if (monthClosingService.isLocked(monthText)) {
+                    log.info("[补发起兜底] 月份已锁定,跳过本月全部目标 month={} submitters={}",
                             monthText, monthEntry.getValue().size());
                     continue;
                 }

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

@@ -91,6 +91,28 @@ class MonthClosingServiceTest {
         assertTrue(result.getMessage().contains("项目工时 1 张"));
     }
 
+    @Test
+    void shouldLockMonthWhileClosingFlowIsRunning() {
+        YDClient ydClient = mock(YDClient.class);
+        when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process)))
+                .thenAnswer(invocation -> {
+                    YDParam param = invocation.getArgument(0);
+                    if (CLOSING_FORM.equals(param.getFormUuid())
+                            && "RUNNING".equals(param.getInstanceStatus())) {
+                        Map<String, Object> data = new HashMap<>();
+                        data.put(CLOSING_MONTH_FIELD, "2026-09-01");
+                        Map<String, Object> item = new HashMap<>();
+                        item.put("formData", data);
+                        return response(Collections.singletonList(item));
+                    }
+                    return response(Collections.emptyList());
+                });
+        MonthClosingService service = service(ydClient);
+
+        assertTrue(service.isLocked("202609"));
+        assertFalse(service.isLocked("202608"));
+    }
+
     @Test
     void shouldTreatApprovedClosingProcessAsClosed() {
         YDClient ydClient = mock(YDClient.class);

+ 76 - 5
mjava-akdsbeisen/src/test/java/com/malk/service/workhours/ReSubmitApprovalServiceTest.java

@@ -2,6 +2,7 @@ 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.TimeCardApprovalStartRequest;
 import com.malk.server.workhours.WHConf;
@@ -84,12 +85,13 @@ class ReSubmitApprovalServiceTest {
         ReSubmitApprovalService service = new ReSubmitApprovalService();
         ReflectionTestUtils.setField(service, "ydClient", ydClient);
         ReflectionTestUtils.setField(service, "whConf", whConf);
+        attachOpenMonth(service);
 
         TimeCardApprovalStartRequest request = new TimeCardApprovalStartRequest();
         request.setFormUuid("FORM-APPROVAL");
         request.setProcessCode("PROCESS-APPROVAL");
         request.setDeptId("dept-request");
-        request.setFormDataJson("{\"textField_test\":\"value\"}");
+        request.setFormDataJson("{\"textField_mmd2lv0y\":\"202609\",\"textField_test\":\"value\"}");
 
         assertTrue(service.startApproval(request));
 
@@ -116,16 +118,17 @@ class ReSubmitApprovalServiceTest {
         ReSubmitApprovalService service = new ReSubmitApprovalService();
         ReflectionTestUtils.setField(service, "ydClient", ydClient);
         ReflectionTestUtils.setField(service, "whConf", whConf);
+        attachOpenMonth(service);
         TimeCardApprovalStartRequest request = new TimeCardApprovalStartRequest();
         request.setFormUuid("FORM-APPROVAL");
         request.setProcessCode("PROCESS-APPROVAL");
-        request.setFormDataJson("{\"textField_test\":\"same-hours\"}");
+        request.setFormDataJson("{\"textField_mmd2lv0y\":\"202609\",\"textField_test\":\"same-hours\"}");
 
         assertTrue(service.startApproval(request));
         assertTrue(service.startApproval(request));
         verify(ydClient, times(1)).operateData(any(YDParam.class), eq(YDConf.FORM_OPERATION.start));
 
-        request.setFormDataJson("{\"textField_test\":\"changed-hours\"}");
+        request.setFormDataJson("{\"textField_mmd2lv0y\":\"202609\",\"textField_test\":\"changed-hours\"}");
         assertTrue(service.startApproval(request));
         verify(ydClient, times(2)).operateData(any(YDParam.class), eq(YDConf.FORM_OPERATION.start));
     }
@@ -150,10 +153,11 @@ class ReSubmitApprovalServiceTest {
         ReSubmitApprovalService service = new ReSubmitApprovalService();
         ReflectionTestUtils.setField(service, "ydClient", ydClient);
         ReflectionTestUtils.setField(service, "whConf", whConf);
+        attachOpenMonth(service);
         TimeCardApprovalStartRequest request = new TimeCardApprovalStartRequest();
         request.setFormUuid("FORM-APPROVAL");
         request.setProcessCode("PROCESS-APPROVAL");
-        request.setFormDataJson("{\"textField_test\":\"same-hours\"}");
+        request.setFormDataJson("{\"textField_mmd2lv0y\":\"202609\",\"textField_test\":\"same-hours\"}");
 
         CompletableFuture<Boolean> first = CompletableFuture.supplyAsync(() -> service.startApproval(request));
         try {
@@ -166,6 +170,57 @@ class ReSubmitApprovalServiceTest {
         verify(ydClient, times(1)).operateData(any(YDParam.class), eq(YDConf.FORM_OPERATION.start));
     }
 
+    @Test
+    void shouldBlockNewApprovalAndResubmitWhileMonthIsLocked() {
+        YDClient ydClient = mock(YDClient.class);
+        MonthClosingService monthClosingService = mock(MonthClosingService.class);
+        when(monthClosingService.isLocked("202609")).thenReturn(true);
+        WHConf whConf = new WHConf();
+        whConf.setFormUuidApproval("FORM-APPROVAL");
+        whConf.setApprovalProcessCode("PROCESS-APPROVAL");
+        whConf.setApprovalOriginatorUserId("system-user");
+        ReSubmitApprovalService service = new ReSubmitApprovalService();
+        ReflectionTestUtils.setField(service, "ydClient", ydClient);
+        ReflectionTestUtils.setField(service, "whConf", whConf);
+        ReflectionTestUtils.setField(service, "monthClosingService", monthClosingService);
+
+        TimeCardApprovalStartRequest request = new TimeCardApprovalStartRequest();
+        request.setFormUuid("FORM-APPROVAL");
+        request.setProcessCode("PROCESS-APPROVAL");
+        request.setFormDataJson("{\"textField_mmd2lv0y\":\"202609\"}");
+
+        assertThrows(McException.class, () -> service.startApproval(request));
+        assertThrows(McException.class, () -> service.resubmit("202609", "user-1", false));
+        verifyNoInteractions(ydClient);
+    }
+
+    @Test
+    void shouldBlockRepairApprovalBySubmitMonthWhileSourceMonthIsOpen() {
+        YDClient ydClient = mock(YDClient.class);
+        MonthClosingService monthClosingService = mock(MonthClosingService.class);
+        when(monthClosingService.isLocked("202609")).thenReturn(true);
+        WHConf whConf = new WHConf();
+        whConf.setFormUuidApproval("FORM-APPROVAL");
+        whConf.setApprovalProcessCode("PROCESS-APPROVAL");
+        whConf.setApprovalOriginatorUserId("system-user");
+        whConf.setApprovalFillTypeField("radioField_fill_type");
+        whConf.setApprovalRepairSubmitDateField("dateField_repair_submit");
+        ReSubmitApprovalService service = new ReSubmitApprovalService();
+        ReflectionTestUtils.setField(service, "ydClient", ydClient);
+        ReflectionTestUtils.setField(service, "whConf", whConf);
+        ReflectionTestUtils.setField(service, "monthClosingService", monthClosingService);
+
+        TimeCardApprovalStartRequest request = new TimeCardApprovalStartRequest();
+        request.setFormUuid("FORM-APPROVAL");
+        request.setProcessCode("PROCESS-APPROVAL");
+        request.setFormDataJson("{\"textField_mmd2lv0y\":\"202608\",\"radioField_fill_type\":\"补填\",\"dateField_repair_submit\":\"2026-09-22\"}");
+
+        assertThrows(McException.class, () -> service.startApproval(request));
+        verify(monthClosingService).isLocked("202609");
+        verify(monthClosingService, org.mockito.Mockito.never()).isLocked("202608");
+        verifyNoInteractions(ydClient);
+    }
+
     @Test
     void shouldKeepExplicitRepairFillTypeWhenStartingApproval() {
         YDClient ydClient = mock(YDClient.class);
@@ -176,15 +231,17 @@ class ReSubmitApprovalServiceTest {
         whConf.setApprovalProcessCode("PROCESS-APPROVAL");
         whConf.setApprovalOriginatorUserId("system-user");
         whConf.setApprovalFillTypeField("radioField_fill_type");
+        whConf.setApprovalRepairSubmitDateField("dateField_repair_submit");
 
         ReSubmitApprovalService service = new ReSubmitApprovalService();
         ReflectionTestUtils.setField(service, "ydClient", ydClient);
         ReflectionTestUtils.setField(service, "whConf", whConf);
+        MonthClosingService monthClosingService = attachOpenMonth(service);
 
         TimeCardApprovalStartRequest request = new TimeCardApprovalStartRequest();
         request.setFormUuid("FORM-APPROVAL");
         request.setProcessCode("PROCESS-APPROVAL");
-        request.setFormDataJson("{\"radioField_fill_type\":\"补填\"}");
+        request.setFormDataJson("{\"textField_mmd2lv0y\":\"202608\",\"radioField_fill_type\":\"补填\",\"dateField_repair_submit\":\"2026-09-22\"}");
 
         assertTrue(service.startApproval(request));
 
@@ -193,6 +250,8 @@ class ReSubmitApprovalServiceTest {
         Map started = com.alibaba.fastjson.JSON.parseObject(
                 captor.getValue().getFormDataJson(), Map.class);
         assertEquals("补填", started.get("radioField_fill_type"));
+        verify(monthClosingService, org.mockito.Mockito.times(2)).isLocked("202609");
+        verify(monthClosingService, org.mockito.Mockito.never()).isLocked("202608");
     }
 
     @Test
@@ -316,8 +375,10 @@ class ReSubmitApprovalServiceTest {
         ReSubmitApprovalService service = new ReSubmitApprovalService();
         ReflectionTestUtils.setField(service, "ydClient", ydClient);
         ReflectionTestUtils.setField(service, "whConf", whConf);
+        attachOpenMonth(service);
 
         Map<String, Object> formData = new HashMap<>();
+        formData.put("textField_mmd2lv0y", "202609");
         List<Map<String, Object>> rows = new ArrayList<>();
         for (int index = 0; index < 450; index++) {
             rows.add(Collections.singletonMap("numberField_mmacxew9", 1));
@@ -351,8 +412,10 @@ class ReSubmitApprovalServiceTest {
         ReSubmitApprovalService service = new ReSubmitApprovalService();
         ReflectionTestUtils.setField(service, "ydClient", ydClient);
         ReflectionTestUtils.setField(service, "whConf", whConf);
+        attachOpenMonth(service);
 
         Map<String, Object> formData = new HashMap<>();
+        formData.put("textField_mmd2lv0y", "202609");
         List<Map<String, Object>> rows = new ArrayList<>();
         for (int index = 0; index < 501; index++) {
             Map<String, Object> row = new HashMap<>();
@@ -394,8 +457,10 @@ class ReSubmitApprovalServiceTest {
         ReSubmitApprovalService service = new ReSubmitApprovalService();
         ReflectionTestUtils.setField(service, "ydClient", ydClient);
         ReflectionTestUtils.setField(service, "whConf", whConf);
+        attachOpenMonth(service);
 
         Map<String, Object> formData = new HashMap<>();
+        formData.put("textField_mmd2lv0y", "202609");
         List<Map<String, Object>> rows = new ArrayList<>();
         for (int index = 0; index < 501; index++) {
             rows.add(Collections.singletonMap("numberField_mmd1wu9e", 2));
@@ -505,6 +570,12 @@ class ReSubmitApprovalServiceTest {
         assertEquals(DEFAULT_MANAGER_ID, service.resolveProjectManager("pm-left"));
     }
 
+    private static MonthClosingService attachOpenMonth(ReSubmitApprovalService service) {
+        MonthClosingService monthClosingService = mock(MonthClosingService.class);
+        ReflectionTestUtils.setField(service, "monthClosingService", monthClosingService);
+        return monthClosingService;
+    }
+
     private ReSubmitApprovalService managerResolutionService(DDClient ddClient,
                                                               DDClient_Contacts contacts,
                                                               YDClient ydClient) {

+ 5 - 5
mjava-akdsbeisen/src/test/java/com/malk/timer/ReSubmitApprovalTimerTest.java

@@ -35,7 +35,7 @@ class ReSubmitApprovalTimerTest {
     }
 
     @Test
-    void shouldSkipAllTargetsWhenMonthIsClosed() {
+    void shouldSkipAllTargetsWhenMonthIsLocked() {
         ReSubmitApprovalService reSubmitService = mock(ReSubmitApprovalService.class);
         MonthClosingService monthClosingService = mock(MonthClosingService.class);
         TimeCardRepairService timeCardRepairService = mock(TimeCardRepairService.class);
@@ -46,8 +46,8 @@ class ReSubmitApprovalTimerTest {
         targets.put("202608", new LinkedHashSet<>(Arrays.asList("user-1", "user-2")));
         targets.put("202609", new LinkedHashSet<>(Arrays.asList("user-3")));
         when(reSubmitService.findRecentlyModifiedTargets(3)).thenReturn(targets);
-        when(monthClosingService.isClosed("202608")).thenReturn(true);
-        when(monthClosingService.isClosed("202609")).thenReturn(false);
+        when(monthClosingService.isLocked("202608")).thenReturn(true);
+        when(monthClosingService.isLocked("202609")).thenReturn(false);
         when(reSubmitService.resubmit("202609", "user-3"))
                 .thenReturn(ReSubmitApprovalResult.builder().build());
         when(timeCardRepairService.resubmitRecentlyModifiedPending(3))
@@ -61,8 +61,8 @@ class ReSubmitApprovalTimerTest {
 
         timer.scanAndReSubmit();
 
-        verify(monthClosingService).isClosed("202608");
-        verify(monthClosingService).isClosed("202609");
+        verify(monthClosingService).isLocked("202608");
+        verify(monthClosingService).isLocked("202609");
         verify(reSubmitService, never()).resubmit("202608", "user-1");
         verify(reSubmitService, never()).resubmit("202608", "user-2");
         verify(reSubmitService).resubmit("202609", "user-3");