|
@@ -80,6 +80,161 @@ public class WorkHoursCalcServiceTest {
|
|
|
assertEquals(2d, result.get("employee-2|2026-09-30"), 0.000001d);
|
|
assertEquals(2d, result.get("employee-2|2026-09-30"), 0.000001d);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ public void summaryChangeHoursShouldRetryOnlyFailedPage() {
|
|
|
|
|
+ YDClient ydClient = mock(YDClient.class);
|
|
|
|
|
+ LocalDate workDay = LocalDate.of(2026, 9, 30);
|
|
|
|
|
+ long totalCount = YDConf.PAGE_SIZE_LIMIT + 1L;
|
|
|
|
|
+ when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.singletonList(
|
|
|
|
|
+ summaryRecord("employee-1", workDay, 1d)), totalCount))
|
|
|
|
|
+ .thenThrow(new McException("TEMPORARY_FAILURE", "temporary failure"))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.singletonList(
|
|
|
|
|
+ summaryRecord("employee-1", workDay, 2d)), totalCount));
|
|
|
|
|
+
|
|
|
|
|
+ WorkHoursCalcService service = workHoursService(ydClient);
|
|
|
|
|
+ List<Long> retryDelays = new ArrayList<>();
|
|
|
|
|
+ ReflectionTestUtils.setField(service, "existingQueryRetrySleeper",
|
|
|
|
|
+ (WorkHoursCalcService.RetrySleeper) retryDelays::add);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Double> result = ReflectionTestUtils.invokeMethod(
|
|
|
|
|
+ service, "querySummaryChangeHours", workDay, workDay);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(3d, result.get("employee-1|2026-09-30"), 0.000001d);
|
|
|
|
|
+ assertEquals(Collections.singletonList(2_000L), retryDelays);
|
|
|
|
|
+ verify(ydClient, times(3)).queryData(
|
|
|
|
|
+ any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void summaryChangeHoursShouldNotRetryPermanentFailure() {
|
|
|
|
|
+ YDClient ydClient = mock(YDClient.class);
|
|
|
|
|
+ LocalDate workDay = LocalDate.of(2026, 9, 30);
|
|
|
|
|
+ when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
|
|
|
+ .thenThrow(new McException("INVALID_PARAMETER", "formUuid 参数错误"));
|
|
|
|
|
+ WorkHoursCalcService service = workHoursService(ydClient);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ ReflectionTestUtils.invokeMethod(service, "querySummaryChangeHours", workDay, workDay);
|
|
|
|
|
+ fail("永久性错误应立即抛出");
|
|
|
|
|
+ } catch (McException e) {
|
|
|
|
|
+ assertEquals("INVALID_PARAMETER", e.getCode());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ verify(ydClient, times(1)).queryData(
|
|
|
|
|
+ any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void summaryChangeHoursShouldRejectInvalidDayBeforeWriting() {
|
|
|
|
|
+ YDClient ydClient = mock(YDClient.class);
|
|
|
|
|
+ LocalDate workDay = LocalDate.of(2026, 9, 30);
|
|
|
|
|
+ Map<String, Object> invalid = summaryRecord("employee-1", workDay, 4d);
|
|
|
|
|
+ ((Map<String, Object>) invalid.get("formData")).put("textField_mmbffvdb", "20260230");
|
|
|
|
|
+ invalid.put("formInstanceId", "summary-invalid-1");
|
|
|
|
|
+ when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.singletonList(invalid)));
|
|
|
|
|
+ WorkHoursCalcService service = workHoursService(ydClient);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ ReflectionTestUtils.invokeMethod(service, "querySummaryChangeHours", workDay, workDay);
|
|
|
|
|
+ fail("非法汇总日期应停止本轮任务");
|
|
|
|
|
+ } catch (IllegalStateException e) {
|
|
|
|
|
+ assertTrue(e.getMessage().contains("summary-invalid-1"));
|
|
|
|
|
+ assertTrue(e.getMessage().contains("20260230"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ verify(ydClient, never()).operateData(any(YDParam.class), any(YDConf.FORM_OPERATION.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void summaryChangeHoursShouldRejectEmptyDayBeforeWriting() {
|
|
|
|
|
+ YDClient ydClient = mock(YDClient.class);
|
|
|
|
|
+ LocalDate workDay = LocalDate.of(2026, 9, 30);
|
|
|
|
|
+ Map<String, Object> invalid = summaryRecord("employee-1", workDay, 4d);
|
|
|
|
|
+ ((Map<String, Object>) invalid.get("formData")).put("textField_mmbffvdb", "");
|
|
|
|
|
+ invalid.put("formInstanceId", "summary-empty-day-1");
|
|
|
|
|
+ when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.singletonList(invalid)));
|
|
|
|
|
+ WorkHoursCalcService service = workHoursService(ydClient);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ ReflectionTestUtils.invokeMethod(service, "querySummaryChangeHours", workDay, workDay);
|
|
|
|
|
+ fail("汇总日期为空时应停止本轮任务");
|
|
|
|
|
+ } catch (IllegalStateException e) {
|
|
|
|
|
+ assertTrue(e.getMessage().contains("summary-empty-day-1"));
|
|
|
|
|
+ assertTrue(e.getMessage().contains("dayText="));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ verify(ydClient, never()).operateData(any(YDParam.class), any(YDConf.FORM_OPERATION.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void summaryChangeHoursShouldRejectMissingEmployeeBeforeWriting() {
|
|
|
|
|
+ YDClient ydClient = mock(YDClient.class);
|
|
|
|
|
+ LocalDate workDay = LocalDate.of(2026, 9, 30);
|
|
|
|
|
+ Map<String, Object> invalid = summaryRecord("", workDay, 4d);
|
|
|
|
|
+ invalid.put("formInstanceId", "summary-empty-employee-1");
|
|
|
|
|
+ when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.singletonList(invalid)));
|
|
|
|
|
+ WorkHoursCalcService service = workHoursService(ydClient);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ ReflectionTestUtils.invokeMethod(service, "querySummaryChangeHours", workDay, workDay);
|
|
|
|
|
+ fail("汇总填报人为空时应停止本轮任务");
|
|
|
|
|
+ } catch (IllegalStateException e) {
|
|
|
|
|
+ assertTrue(e.getMessage().contains("summary-empty-employee-1"));
|
|
|
|
|
+ assertTrue(e.getMessage().contains("employeeId 为空"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ verify(ydClient, never()).operateData(any(YDParam.class), any(YDConf.FORM_OPERATION.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void summaryChangeHoursShouldRejectDayOutsideQueryRange() {
|
|
|
|
|
+ YDClient ydClient = mock(YDClient.class);
|
|
|
|
|
+ LocalDate workDay = LocalDate.of(2026, 9, 30);
|
|
|
|
|
+ Map<String, Object> mismatched = summaryRecord("employee-1", workDay.plusDays(1), 4d);
|
|
|
|
|
+ mismatched.put("formInstanceId", "summary-mismatch-1");
|
|
|
|
|
+ when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.singletonList(mismatched)));
|
|
|
|
|
+ WorkHoursCalcService service = workHoursService(ydClient);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ ReflectionTestUtils.invokeMethod(service, "querySummaryChangeHours", workDay, workDay);
|
|
|
|
|
+ fail("汇总日期文本与查询范围不一致时应停止本轮任务");
|
|
|
|
|
+ } catch (IllegalStateException e) {
|
|
|
|
|
+ assertTrue(e.getMessage().contains("summary-mismatch-1"));
|
|
|
|
|
+ assertTrue(e.getMessage().contains("20261001"));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void repairRequiredHoursCreationShouldNotReadSummaryChanges() {
|
|
|
|
|
+ YDClient ydClient = mock(YDClient.class);
|
|
|
|
|
+ LocalDate workDay = LocalDate.of(2026, 9, 30);
|
|
|
|
|
+ Map<String, Object> personnel = personnelRecord("employee-1", "在职", null);
|
|
|
|
|
+ Map<String, Object> personnelData = (Map<String, Object>) personnel.get("formData");
|
|
|
|
|
+ personnelData.put("radioField_mkow4ydo", "内部");
|
|
|
|
|
+ when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.emptyList()))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.emptyList()))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.singletonList(personnel)));
|
|
|
|
|
+ WorkHoursCalcService service = workHoursService(ydClient);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = service.syncOneEmployeeOneDayForRepair("employee-1", workDay);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(true, result.get("success"));
|
|
|
|
|
+ verify(ydClient, times(3)).queryData(
|
|
|
|
|
+ any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form));
|
|
|
|
|
+ ArgumentCaptor<YDParam> paramCaptor = ArgumentCaptor.forClass(YDParam.class);
|
|
|
|
|
+ verify(ydClient).operateData(paramCaptor.capture(), eq(YDConf.FORM_OPERATION.upsert));
|
|
|
|
|
+ Map<String, Object> written = JSON.parseObject(
|
|
|
|
|
+ paramCaptor.getValue().getFormDataJson(), Map.class);
|
|
|
|
|
+ assertEquals(8d, ((Number) written.get("numberField_mmd8onl6")).doubleValue(), 0.000001d);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@Test
|
|
@Test
|
|
|
public void concurrentUpsertShouldWriteBasePlusSummaryChange() {
|
|
public void concurrentUpsertShouldWriteBasePlusSummaryChange() {
|
|
|
YDClient ydClient = mock(YDClient.class);
|
|
YDClient ydClient = mock(YDClient.class);
|
|
@@ -158,6 +313,50 @@ public class WorkHoursCalcServiceTest {
|
|
|
assertEquals("workDay 在 today 之后, 按业务规则不写入未来数据", result.get("error"));
|
|
assertEquals("workDay 在 today 之后, 按业务规则不写入未来数据", result.get("error"));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void syncOneEmployeeOneDayShouldSkipExistingRequiredHours() {
|
|
|
|
|
+ YDClient ydClient = mock(YDClient.class);
|
|
|
|
|
+ LocalDate workDay = LocalDate.now().minusDays(1);
|
|
|
|
|
+ when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.singletonList(
|
|
|
|
|
+ requiredHoursRecord("required-1", "employee-1", workDay))));
|
|
|
|
|
+ WorkHoursCalcService service = workHoursService(ydClient);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = service.syncOneEmployeeOneDay("employee-1", workDay);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(true, result.get("success"));
|
|
|
|
|
+ assertEquals(true, result.get("skippedExisting"));
|
|
|
|
|
+ verify(ydClient, never()).operateData(any(YDParam.class), any(YDConf.FORM_OPERATION.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void syncBatchEmployeesShouldSkipExistingRequiredHours() {
|
|
|
|
|
+ YDClient ydClient = mock(YDClient.class);
|
|
|
|
|
+ LocalDate targetMonth = LocalDate.now().withDayOfMonth(1);
|
|
|
|
|
+ Map<String, Object> personnel = personnelRecord("employee-1", "在职", null);
|
|
|
|
|
+ Map<String, Object> personnelData = (Map<String, Object>) personnel.get("formData");
|
|
|
|
|
+ personnelData.put("radioField_mkow4ydo", "内部");
|
|
|
|
|
+ List<Map<String, Object>> existing = new ArrayList<>();
|
|
|
|
|
+ LocalDate monthEnd = targetMonth.plusMonths(1).minusDays(1);
|
|
|
|
|
+ for (LocalDate day = targetMonth; !day.isAfter(monthEnd); day = day.plusDays(1)) {
|
|
|
|
|
+ if (day.getDayOfWeek().getValue() <= 5) {
|
|
|
|
|
+ existing.add(requiredHoursRecord("required-" + day, "employee-1", day));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.singletonList(personnel)))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.emptyList()))
|
|
|
|
|
+ .thenReturn(pageOf(existing))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.emptyList()));
|
|
|
|
|
+ WorkHoursCalcService service = workHoursService(ydClient);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = service.syncBatchEmployees(1, targetMonth);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(existing.size(), result.get("existingCount"));
|
|
|
|
|
+ assertEquals(0, result.get("success"));
|
|
|
|
|
+ verify(ydClient, never()).operateData(any(YDParam.class), any(YDConf.FORM_OPERATION.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@Test
|
|
@Test
|
|
|
public void syncOneInternalEmployeeShouldNotRequireProject() {
|
|
public void syncOneInternalEmployeeShouldNotRequireProject() {
|
|
|
YDClient ydClient = mock(YDClient.class);
|
|
YDClient ydClient = mock(YDClient.class);
|
|
@@ -167,7 +366,9 @@ public class WorkHoursCalcServiceTest {
|
|
|
formData.put("radioField_mkow4ydo", "内部");
|
|
formData.put("radioField_mkow4ydo", "内部");
|
|
|
formData.put("employeeField_mh8xhqc3_id", Collections.singletonList("manager-1"));
|
|
formData.put("employeeField_mh8xhqc3_id", Collections.singletonList("manager-1"));
|
|
|
when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
|
- .thenReturn(pageOf(Collections.singletonList(personnel)));
|
|
|
|
|
|
|
+ .thenReturn(pageOf(Collections.emptyList()))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.singletonList(personnel)))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.emptyList()));
|
|
|
|
|
|
|
|
WorkHoursCalcService service = new WorkHoursCalcService();
|
|
WorkHoursCalcService service = new WorkHoursCalcService();
|
|
|
ReflectionTestUtils.setField(service, "ydClient", ydClient);
|
|
ReflectionTestUtils.setField(service, "ydClient", ydClient);
|
|
@@ -190,7 +391,9 @@ public class WorkHoursCalcServiceTest {
|
|
|
formData.put("radioField_mkow4ydo", "内部");
|
|
formData.put("radioField_mkow4ydo", "内部");
|
|
|
formData.put("employeeField_mh8xhqc3_id", Collections.singletonList("manager-from-personnel"));
|
|
formData.put("employeeField_mh8xhqc3_id", Collections.singletonList("manager-from-personnel"));
|
|
|
when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
|
- .thenReturn(pageOf(Collections.singletonList(personnel)));
|
|
|
|
|
|
|
+ .thenReturn(pageOf(Collections.emptyList()))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.singletonList(personnel)))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.emptyList()));
|
|
|
|
|
|
|
|
WorkHoursCalcService service = new WorkHoursCalcService();
|
|
WorkHoursCalcService service = new WorkHoursCalcService();
|
|
|
ReflectionTestUtils.setField(service, "ydClient", ydClient);
|
|
ReflectionTestUtils.setField(service, "ydClient", ydClient);
|
|
@@ -212,7 +415,9 @@ public class WorkHoursCalcServiceTest {
|
|
|
Map<String, Object> formData = (Map<String, Object>) personnel.get("formData");
|
|
Map<String, Object> formData = (Map<String, Object>) personnel.get("formData");
|
|
|
formData.put("radioField_mkow4ydo", "内部");
|
|
formData.put("radioField_mkow4ydo", "内部");
|
|
|
when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
|
- .thenReturn(pageOf(Collections.singletonList(personnel)));
|
|
|
|
|
|
|
+ .thenReturn(pageOf(Collections.emptyList()))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.singletonList(personnel)))
|
|
|
|
|
+ .thenReturn(pageOf(Collections.emptyList()));
|
|
|
|
|
|
|
|
WorkHoursCalcService service = new WorkHoursCalcService();
|
|
WorkHoursCalcService service = new WorkHoursCalcService();
|
|
|
ReflectionTestUtils.setField(service, "ydClient", ydClient);
|
|
ReflectionTestUtils.setField(service, "ydClient", ydClient);
|
|
@@ -511,6 +716,7 @@ public class WorkHoursCalcServiceTest {
|
|
|
formData.put("radioField_mkow4ydo", "外部");
|
|
formData.put("radioField_mkow4ydo", "外部");
|
|
|
when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
|
.thenReturn(
|
|
.thenReturn(
|
|
|
|
|
+ pageOf(Collections.emptyList()),
|
|
|
pageOf(Collections.singletonList(personnel)),
|
|
pageOf(Collections.singletonList(personnel)),
|
|
|
pageOf(Collections.emptyList()));
|
|
pageOf(Collections.emptyList()));
|
|
|
|
|
|
|
@@ -1029,8 +1235,12 @@ public class WorkHoursCalcServiceTest {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private static DDR_New<Object> pageOf(List<Map<String, Object>> records) {
|
|
private static DDR_New<Object> pageOf(List<Map<String, Object>> records) {
|
|
|
|
|
+ return pageOf(records, records.size());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static DDR_New<Object> pageOf(List<Map<String, Object>> records, long totalCount) {
|
|
|
DDR_New<Object> page = new DDR_New<>();
|
|
DDR_New<Object> page = new DDR_New<>();
|
|
|
- page.setTotalCount(records.size());
|
|
|
|
|
|
|
+ page.setTotalCount(totalCount);
|
|
|
page.setData(records);
|
|
page.setData(records);
|
|
|
return page;
|
|
return page;
|
|
|
}
|
|
}
|
|
@@ -1039,6 +1249,7 @@ public class WorkHoursCalcServiceTest {
|
|
|
WHConf conf = new WHConf();
|
|
WHConf conf = new WHConf();
|
|
|
conf.setYidaAppType("app");
|
|
conf.setYidaAppType("app");
|
|
|
conf.setYidaSystemToken("token");
|
|
conf.setYidaSystemToken("token");
|
|
|
|
|
+ conf.setFormUuidHoliday("holiday");
|
|
|
conf.setFormUuidPersonnel("personnel");
|
|
conf.setFormUuidPersonnel("personnel");
|
|
|
conf.setFormUuidRequiredHours("required-hours");
|
|
conf.setFormUuidRequiredHours("required-hours");
|
|
|
conf.setFormUuidWorkHoursSummary("work-hours-summary");
|
|
conf.setFormUuidWorkHoursSummary("work-hours-summary");
|