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

fix(workhours): floor historical required hours at zero

malk пре 2 недеља
родитељ
комит
103e26af87

+ 2 - 2
mjava-akdsbeisen/src/main/java/com/malk/service/workhours/RepairApprovalWriteBackService.java

@@ -144,12 +144,12 @@ public class RepairApprovalWriteBackService {
             final double sourceHours = entry.getValue();
             final int sourceStep = stepIndex++;
             completedSteps = runStep(other, instanceId, totalSteps, completedSteps, sourceStep,
-                    () -> adjustmentWriter.required(user, instanceId, sourceStep, sourceDay, -sourceHours, true));
+                    () -> adjustmentWriter.required(user, instanceId, sourceStep, sourceDay, -sourceHours, true, submitDay));
         }
         final double submittedHours = dayTotals.values().stream().mapToDouble(Double::doubleValue).sum();
         final int submitStep = stepIndex;
         runStep(other, instanceId, totalSteps, completedSteps, submitStep,
-                () -> adjustmentWriter.required(user, instanceId, submitStep, submitDay, submittedHours, false));
+                () -> adjustmentWriter.required(user, instanceId, submitStep, submitDay, submittedHours, false, submitDay));
     }
 
     private static List<WorkHoursAdjustmentWriter.Line> adjustmentLines(List<Row> rows) {

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

@@ -283,7 +283,7 @@ public class TimeCardChangeApprovalWriteBackService {
             boolean targetDay = !submitDay.equals(day);
             final int stepIndex = step++;
             completedSteps = runStep(other, instanceId, totalSteps, completedSteps, stepIndex,
-                    () -> adjustmentWriter.required(rows.get(0).user, instanceId, stepIndex, day, delta, targetDay));
+                    () -> adjustmentWriter.required(rows.get(0).user, instanceId, stepIndex, day, delta, targetDay, submitDay));
         }
     }
 

+ 6 - 3
mjava-akdsbeisen/src/main/java/com/malk/service/workhours/WorkHoursAdjustmentWriter.java

@@ -53,14 +53,17 @@ public class WorkHoursAdjustmentWriter {
         }
     }
 
-    void required(String user, String instance, int step, String day, double delta, boolean source) {
+    void required(String user, String instance, int step, String day, double delta, boolean source,
+                  String submitDay) {
+        boolean historicalSource = source && LocalDate.parse(day, DAY).withDayOfMonth(1)
+                .isBefore(LocalDate.parse(submitDay, DAY).withDayOfMonth(1));
         synchronized (TimeCardChangeApprovalWriteBackService.writeBackLock(user)) {
             journal.execute(user, instance, step, () -> requiredState(user, day, source), before -> {
                 Map<String, Object> after = new LinkedHashMap<>(before);
                 if (!Boolean.TRUE.equals(before.get("present"))) return after;
                 double value = number(before.get("hours")) + delta;
-                // prd 保留原有有符号应报转移口径,不增加本轮未约定的负数限制。
-                after.put("hours", value);
+                // prd 保留有符号应报转移;历史来源月增加最低0保护,提交日仍按完整批准量调整。
+                after.put("hours", historicalSource ? Math.max(0d, value) : value);
                 return after;
             }, after -> writeRequired(user, day, number(after.get("hours"))));
         }

+ 100 - 0
mjava-akdsbeisen/src/test/java/com/malk/service/workhours/TimeCardChangeApprovalWriteBackServiceTest.java

@@ -9,6 +9,7 @@ import com.malk.service.aliwork.YDClient;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.junit.jupiter.params.provider.CsvSource;
 import com.malk.server.common.McException;
 import org.springframework.test.util.ReflectionTestUtils;
 
@@ -32,6 +33,105 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class TimeCardChangeApprovalWriteBackServiceTest {
+    @ParameterizedTest
+    @CsvSource({"false,8,2,6", "false,8,8,0", "false,8,12,0", "false,0,12,0",
+            "false,20,12,8", "false,-4,2,0", "true,8,2,6", "true,8,8,0",
+            "true,8,12,0", "true,0,12,0", "true,20,12,8", "true,-4,2,0"})
+    void shouldFloorHistoricalRequiredHoursAndKeepFullApprovedTransfer(
+            boolean repair, double current, double approved, double expected) {
+        WriteBackFixture fixture = positiveApprovalFixture(repair, approved);
+        fixture.db.get("required-target").put("numberField_mmd8onl6", current);
+        writePositiveApproval(fixture, repair);
+        assertHours(expected, fixture.db.get("required-target").get("numberField_mmd8onl6"));
+        assertHours(8d + approved, fixture.db.get("required-submit").get("numberField_mmd8onl6"));
+        assertHours(approved, fixture.db.get("shared-1").get("numberField_mmeakgi4"));
+        Map original = ((List<Map>) fixture.db.get("original-submit").get("tableField_mmczo634")).get(0);
+        assertHours(1d + approved, original.get("numberField_mmd80zae"));
+        assertHours(8d, original.get("numberField_mmczo637"));
+        String persisted = JSON.toJSONString(fixture.db);
+        writePositiveApproval(fixture, repair);
+        assertEquals(persisted, JSON.toJSONString(fixture.db));
+    }
+
+    @ParameterizedTest
+    @CsvSource({"20260920,true,0", "20260820,true,-4", "20260720,true,-4",
+            "20260920,false,-4"})
+    void shouldUsePersistedSubmissionMonthAndOnlyFloorHistoricalSources(
+            String submitDay, boolean source, double expected) {
+        WriteBackFixture fixture = writeBackFixture(true);
+        WorkHoursAdjustmentWriter writer = (WorkHoursAdjustmentWriter)
+                ReflectionTestUtils.getField(fixture.service, "adjustmentWriter");
+        writer.required("user-1", "required-only", 0, "20260818", -12d, source, submitDay);
+        assertHours(expected, fixture.db.get("required-target").get("numberField_mmd8onl6"));
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void shouldRecoverClampedTargetAcrossReadbackFailureAndRestart(boolean repair) {
+        WriteBackFixture fixture = positiveApprovalFixture(repair, 12d);
+        YDClient client = (YDClient) ReflectionTestUtils.getField(fixture.service, "ydClient");
+        boolean[] failReadback = {true};
+        int[] targetWrites = {0};
+        when(client.queryData(any(), any())).thenAnswer(call -> {
+            YDParam request = call.getArgument(0);
+            if ("required-target".equals(request.getFormInstanceId()) && failReadback[0]
+                    && targetWrites[0] > 0) throw new IllegalStateException("readback unavailable");
+            return queryFixture(fixture.db, request, call.getArgument(1));
+        });
+        when(client.operateData(any(), any())).thenAnswer(call -> {
+            YDParam request = call.getArgument(0);
+            fixture.db.get(request.getFormInstanceId()).putAll(JSON.parseObject(request.getUpdateFormDataJson()));
+            if ("required-target".equals(request.getFormInstanceId())) targetWrites[0]++;
+            return true;
+        });
+        assertThrows(IllegalStateException.class, () -> writePositiveApproval(fixture, repair));
+        assertHours(0d, fixture.db.get("required-target").get("numberField_mmd8onl6"));
+        assertHours(8d, fixture.db.get("required-submit").get("numberField_mmd8onl6"));
+        TimeCardChangeApprovalWriteBackService.clearLocalProgress("approval-1");
+        WorkHoursWriteBackJournal old = (WorkHoursWriteBackJournal)
+                ReflectionTestUtils.getField(fixture.service, "writeBackJournal");
+        WorkHoursWriteBackJournal restarted = new WorkHoursWriteBackJournal(
+                (java.nio.file.Path) ReflectionTestUtils.getField(old, "directory"));
+        WHConf conf = (WHConf) ReflectionTestUtils.getField(fixture.service, "whConf");
+        ReflectionTestUtils.setField(fixture.service, "writeBackJournal", restarted);
+        ReflectionTestUtils.setField(fixture.service, "adjustmentWriter",
+                new WorkHoursAdjustmentWriter(client, conf, null, restarted));
+        failReadback[0] = false;
+        writePositiveApproval(fixture, repair);
+        writePositiveApproval(fixture, repair);
+        assertEquals(1, targetWrites[0]);
+        assertHours(0d, fixture.db.get("required-target").get("numberField_mmd8onl6"));
+        assertHours(20d, fixture.db.get("required-submit").get("numberField_mmd8onl6"));
+        assertEquals("全部成功", fixture.db.get("approval-1").get("sync-status"));
+    }
+
+    private static WriteBackFixture positiveApprovalFixture(boolean repair, double approved) {
+        WriteBackFixture fixture = writeBackFixture(true);
+        Map child = ((List<Map>) fixture.db.get("shared-1").get("tableField_mmczo634")).get(0);
+        child.put("numberField_mmczo635", approved);
+        child.put("numberField_mmczo636", approved);
+        if (repair) child.remove("numberField_rtcChangeBaseBillable");
+        else child.put("numberField_rtcChangeBaseBillable", 0d);
+        fixture.db.get("shared-1").put("fill-type", repair ? "补填" : "变更");
+        Map approval = fixture.db.get("approval-1");
+        ((List<Map>) approval.get("tableField_mmae8t99")).get(0).put("numberField_mmacxew9", approved);
+        approval.put("submit-date", LocalDate.of(2026, 9, 20).atStartOfDay(ZoneId.of("Asia/Shanghai"))
+                .toInstant().toEpochMilli());
+        return fixture;
+    }
+
+    private static void writePositiveApproval(WriteBackFixture fixture, boolean repair) {
+        if (!repair) {
+            fixture.service.writeBack("approval-1", 1);
+            return;
+        }
+        RepairApprovalWriteBackService service = new RepairApprovalWriteBackService();
+        for (String field : new String[]{"ydClient", "whConf", "writeBackJournal", "adjustmentWriter"}) {
+            ReflectionTestUtils.setField(service, field, ReflectionTestUtils.getField(fixture.service, field));
+        }
+        service.writeBack("approval-1", 1);
+    }
+
     @ParameterizedTest
     @ValueSource(booleans = {false, true})
     void shouldCarryProjectMetadataToNewAndExistingRows(boolean existing) {