Browse Source

fix(timecard): preserve submitted-hours edit locks

malk 1 month ago
parent
commit
0d2170a1e2

+ 32 - 10
mjava-akdsbeisen/src/main/java/com/malk/service/workhours/TimeCardRepairService.java

@@ -88,11 +88,30 @@ public class TimeCardRepairService {
         String monthText = request.getDayText().substring(0, 6);
         validateMonthRequest(request.getSubmitterUserId(), monthText);
         McException.assertAccessException(!monthClosingService.isClosed(monthText), "补填只能保存已封账月份");
+        McException.assertAccessException(StringUtils.isBlank(whConf.getFormUuidWorkHoursSummary()), "原工时汇总表未配置");
         McException.assertAccessException(StringUtils.isBlank(whConf.getFormUuidRepairWorkHoursSummary()), "补填汇总表未配置");
         McException.assertAccessException(StringUtils.isBlank(whConf.getRepairSummarySyncStatusField()), "补填汇总状态字段未配置");
         McException.assertAccessException(request.getFormData() == null, "补填工时数据不能为空");
         assertSourceDateAvailable(request.getSubmitterUserId(), request.getDayText());
+        List<Map> existing = query(whConf.getFormUuidRepairWorkHoursSummary(),
+                mapOf(R_SUBMITTER, request.getSubmitterUserId(), R_DAY, request.getDayText()));
+        McException.assertAccessException(existing.size() > 1,
+                "补填汇总记录重复,已停止保存 userId=" + request.getSubmitterUserId() + ", day=" + request.getDayText());
+        McException.assertAccessException(existing.stream().anyMatch(record ->
+                        TimeCardSummaryService.isRepairRecordLocked(record, whConf.getRepairSummarySyncStatusField())),
+                "该日期补填工时已提交或已审批,不能再填写 userId=" + request.getSubmitterUserId()
+                        + ", day=" + request.getDayText());
         Map<String, Object> data = new LinkedHashMap<>(request.getFormData());
+        // fixme 草稿仅接收明细暂存工时;汇总由服务端重算,不能信任页面传入的审批合计。
+        for (String table : Arrays.asList(R_BIL_TABLE, R_NON_TABLE, R_OTHER_TABLE)) {
+            for (Map child : list(data.get(table))) {
+                McException.assertAccessException(
+                        TimeCardSummaryService.hasSubmittedOrApprovedHours(mapOf(table,
+                                Collections.singletonList(child))),
+                        "补填草稿不能包含已提交、已审批或变更工时");
+            }
+        }
+        TimeCardRepairService.applySummaryTotals(data, data);
         data.put(R_SUBMITTER, request.getSubmitterUserId());
         data.put(R_DAY, request.getDayText());
         data.put(R_MONTH, request.getDayText().substring(0, 6));
@@ -484,9 +503,15 @@ public class TimeCardRepairService {
     private long parseDay(String day) { return LocalDate.parse(day, DateTimeFormatter.ofPattern("yyyyMMdd")).atStartOfDay(ZONE).toInstant().toEpochMilli(); }
     private List<Map> query(String formUuid, Map<String, Object> search) {
         if (StringUtils.isBlank(formUuid)) return new ArrayList<>();
-        Object data = 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).getData();
-        return data instanceof List ? (List<Map>) data : new ArrayList<>();
+        com.malk.server.dingtalk.DDR_New result = 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(result == null || !(result.getData() instanceof List),
+                "查询现有工时失败,已停止保存");
+        List<Map> records = (List<Map>) result.getData();
+        McException.assertAccessException(records.size() >= 100, "现有工时记录过多,已停止保存");
+        return records;
     }
     private void validateMonthRequest(String userId, String month) {
         McException.assertAccessException(StringUtils.isAnyBlank(userId, month)
@@ -544,17 +569,14 @@ public class TimeCardRepairService {
     private void assertSourceDateAvailable(String userId, String day) {
         List<Map> original = query(whConf.getFormUuidWorkHoursSummary(), mapOf(R_SUBMITTER, userId, R_DAY, day));
         boolean hasApprovedOrChangedHours = original.stream()
-                .anyMatch(TimeCardRepairService::hasApprovedOrChangedHours);
+                .anyMatch(TimeCardSummaryService::isFormalRecordLocked);
         McException.assertAccessException(hasApprovedOrChangedHours,
-                "来源日期已有已审批或变更工时,不能补填 userId=" + userId + ", day=" + day);
+                "来源日期已有已提交、已审批或变更工时,不能补填 userId=" + userId + ", day=" + day);
     }
 
-    /** 仅原汇总当天已审批与变更工时的实际合计大于 0 时禁止补填;暂存草稿不占用补填资格。 */
+    /** 原汇总当天任一已审批或变更工时均禁止补填;暂存草稿不占用补填资格。 */
     static boolean hasApprovedOrChangedHours(Map record) {
-        Object nested = record == null ? null : record.get("formData");
-        Map formData = nested instanceof Map ? (Map) nested : record;
-        if (formData == null) return false;
-        return num(formData.get(R_DAILY_APPROVED)) + num(formData.get(R_DAILY_CHANGE)) > 1e-6;
+        return TimeCardSummaryService.hasApprovedOrChangedHours(record);
     }
 
     private void assertRepairRowMutable(String userId, String day, Map row, String category) {

+ 118 - 0
mjava-akdsbeisen/src/main/java/com/malk/service/workhours/TimeCardSummaryService.java

@@ -31,6 +31,20 @@ public class TimeCardSummaryService {
 
     private static final String SUBMITTER_ID_FIELD = "textField_mmbffvd9";
     private static final String DAY_TEXT_FIELD = "textField_mmbffvdb";
+    private static final String DAILY_APPROVED_FIELD = "numberField_mmeakgi4";
+    private static final String DAILY_CHANGE_FIELD = "numberField_mmeakgi5";
+    private static final String[] APPROVED_FIELDS = {
+            DAILY_APPROVED_FIELD, "numberField_mmacxewg", "numberField_mmad6jc1", "numberField_mmd1smes"};
+    private static final String[] CHANGE_FIELDS = {
+            DAILY_CHANGE_FIELD, "numberField_mmad6jbc", "numberField_mmeakgia", "numberField_mmeakgic"};
+    private static final String[] CHILD_TABLES = {
+            "tableField_mmczo634", "tableField_mmczo63h", "tableField_mmeakgid"};
+    private static final String[] CHILD_APPROVED_FIELDS = {
+            "numberField_mmczo637", "numberField_mmczo63d", "numberField_mmeakgii"};
+    private static final String[] CHILD_CHANGE_FIELDS = {
+            "numberField_mmd80zae", "numberField_mmeakgi9", "numberField_mmeakgim"};
+    private static final String[] CHILD_SUBMITTED_FIELDS = {
+            "numberField_mmczo636", "numberField_mmczo63c", "numberField_mmeakgij"};
 
     @Autowired
     private YDClient ydClient;
@@ -57,6 +71,18 @@ public class TimeCardSummaryService {
                 request.getFormData() == null || request.getFormData().isEmpty(),
                 "工时数据不能为空");
 
+        String userId = request.getSubmitterUserId();
+        String dayText = request.getDayText();
+        boolean approved = queryDailyRecords(whConf.getFormUuidWorkHoursSummary(), userId, dayText)
+                .stream().anyMatch(TimeCardSummaryService::isFormalRecordLocked);
+        if (!approved && StringUtils.isNotBlank(whConf.getFormUuidRepairWorkHoursSummary())) {
+            approved = queryDailyRecords(whConf.getFormUuidRepairWorkHoursSummary(), userId, dayText)
+                    .stream().anyMatch(record -> isRepairRecordLocked(record,
+                            whConf.getRepairSummarySyncStatusField()));
+        }
+        McException.assertAccessException(approved,
+                "该日期已有已提交或已审批工时,不能再填写 userId=" + userId + ", day=" + dayText);
+
         Map<String, Object> formData = new LinkedHashMap<>(request.getFormData());
         // fixme 唯一键由服务端覆盖,防止请求条件与实际写入字段不一致。
         formData.put(SUBMITTER_ID_FIELD, request.getSubmitterUserId());
@@ -77,6 +103,98 @@ public class TimeCardSummaryService {
         return true;
     }
 
+    @SuppressWarnings("unchecked")
+    List<Map> queryDailyRecords(String formUuid, String userId, String dayText) {
+        Map<String, Object> search = new LinkedHashMap<>();
+        search.put(SUBMITTER_ID_FIELD, userId);
+        search.put(DAY_TEXT_FIELD, dayText);
+        DDR_New result = ydClient.queryData(YDParam.builder()
+                .appType(whConf.getYidaAppType())
+                .systemToken(whConf.getYidaSystemToken())
+                .formUuid(formUuid)
+                .searchFieldJson(JSON.toJSONString(search))
+                .pageNumber(1)
+                .pageSize(100)
+                .build(), YDConf.FORM_QUERY.retrieve_search_form);
+        McException.assertAccessException(result == null || !(result.getData() instanceof List),
+                "查询现有工时失败,已停止保存");
+        List<Map> records = (List<Map>) result.getData();
+        McException.assertAccessException(records.size() >= 100,
+                "现有工时记录过多,已停止保存");
+        List<Map> matching = new ArrayList<>();
+        for (Map record : records) {
+            Map data = formData(record);
+            if (userId.equals(String.valueOf(data.get(SUBMITTER_ID_FIELD)))
+                    && dayText.equals(String.valueOf(data.get(DAY_TEXT_FIELD)))) {
+                matching.add(record);
+            }
+        }
+        return matching;
+    }
+
+    static boolean hasApprovedOrChangedHours(Map record) {
+        Map data = formData(record);
+        if ("已审批".equals(String.valueOf(data.get("selectField_mre1xz9g")))) return true;
+        if (hasHours(data, APPROVED_FIELDS) || hasHours(data, CHANGE_FIELDS)) return true;
+        for (int index = 0; index < CHILD_TABLES.length; index++) {
+            Object children = data.get(CHILD_TABLES[index]);
+            if (!(children instanceof List)) continue;
+            for (Object child : (List) children) {
+                if (!(child instanceof Map)) continue;
+                Map row = (Map) child;
+                if (hasHours(row, CHILD_APPROVED_FIELDS[index])
+                        || hasHours(row, CHILD_CHANGE_FIELDS[index])) return true;
+            }
+        }
+        return false;
+    }
+
+    static boolean isRepairRecordLocked(Map record, String statusField) {
+        Map data = formData(record);
+        String status = String.valueOf(data.get(statusField));
+        if ("审批中".equals(status) || "已审批".equals(status)) return true;
+        return hasSubmittedOrApprovedHours(data);
+    }
+
+    static boolean isFormalRecordLocked(Map record) {
+        Map data = formData(record);
+        String status = String.valueOf(data.get("selectField_mre1xz9g"));
+        return "审批中".equals(status) || "已审批".equals(status)
+                || hasSubmittedOrApprovedHours(data);
+    }
+
+    static boolean hasSubmittedOrApprovedHours(Map record) {
+        Map data = formData(record);
+        if (hasApprovedOrChangedHours(data) || hasHours(data, "numberField_mmd1smem",
+                "numberField_mmacxewa", "numberField_mmad6jbz", "numberField_mmd1smeq")) return true;
+        for (int index = 0; index < CHILD_TABLES.length; index++) {
+            Object children = data.get(CHILD_TABLES[index]);
+            if (!(children instanceof List)) continue;
+            for (Object child : (List) children) {
+                if (child instanceof Map && hasHours((Map) child, CHILD_SUBMITTED_FIELDS[index])) return true;
+            }
+        }
+        return false;
+    }
+
+    private static Map formData(Map record) {
+        Object nested = record == null ? null : record.get("formData");
+        return nested instanceof Map ? (Map) nested : record == null ? Collections.emptyMap() : record;
+    }
+
+    private static boolean hasHours(Map data, String... fields) {
+        for (String field : fields) {
+            Object raw = data.get(field);
+            if (raw == null) continue;
+            try {
+                if (Math.abs(Double.parseDouble(String.valueOf(raw))) > 1e-6) return true;
+            } catch (NumberFormatException ignored) {
+                // 非数值不表示已有工时;宜搭字段校验会处理非法输入。
+            }
+        }
+        return false;
+    }
+
     /**
      * 按显式清单校验并清理重复汇总记录;默认 dry-run,单批最多 30 条。
      *

+ 87 - 2
mjava-akdsbeisen/src/test/java/com/malk/service/workhours/TimeCardRepairServiceTest.java

@@ -1,5 +1,6 @@
 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;
@@ -8,9 +9,11 @@ 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 org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 import org.springframework.test.util.ReflectionTestUtils;
 
 import java.time.YearMonth;
@@ -27,6 +30,7 @@ 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.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
@@ -112,12 +116,93 @@ class TimeCardRepairServiceTest {
     }
 
     @Test
-    void shouldAllowRepairWhenChangeFullyOffsetsApprovedHours() {
+    void shouldBlockRepairWhenChangeFullyOffsetsApprovedHours() {
         Map<String, Object> formData = new HashMap<>();
         formData.put("numberField_mmeakgi4", 8d);
         formData.put("numberField_mmeakgi5", -8d);
 
-        assertFalse(TimeCardRepairService.hasApprovedOrChangedHours(formData));
+        assertTrue(TimeCardRepairService.hasApprovedOrChangedHours(formData));
+    }
+
+    @Test
+    void shouldBlockRepairWhenOriginalApprovalStatusIsApproved() {
+        Map<String, Object> formData = new HashMap<>();
+        formData.put("selectField_mre1xz9g", "已审批");
+        assertTrue(TimeCardRepairService.hasApprovedOrChangedHours(formData));
+    }
+
+    @Test
+    void shouldNotSaveDraftOverApprovedRepairWithCorruptedDraftStatus() {
+        YDClient client = mock(YDClient.class);
+        MonthClosingService closingService = mock(MonthClosingService.class);
+        WHConf conf = new WHConf();
+        conf.setFormUuidWorkHoursSummary("original-summary");
+        conf.setFormUuidRepairWorkHoursSummary("repair-summary");
+        conf.setRepairSummarySyncStatusField("repair-status");
+        when(closingService.isClosed("202608")).thenReturn(true);
+        DDR_New<Object> noOriginal = new DDR_New<>();
+        noOriginal.setData(Collections.emptyList());
+        Map<String, Object> child = new HashMap<>();
+        child.put("numberField_mmeakgii", 8d);
+        Map<String, Object> formData = new HashMap<>();
+        formData.put("repair-status", "暂存");
+        formData.put("textField_mmbffvd9", "user-1");
+        formData.put("textField_mmbffvdb", "20260818");
+        formData.put("tableField_mmeakgid", Collections.singletonList(child));
+        Map<String, Object> record = new HashMap<>();
+        record.put("formData", formData);
+        DDR_New<Object> approvedRepair = new DDR_New<>();
+        approvedRepair.setData(Collections.singletonList(record));
+        when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
+                .thenReturn(noOriginal, approvedRepair);
+        TimeCardRepairService service = new TimeCardRepairService();
+        ReflectionTestUtils.setField(service, "ydClient", client);
+        ReflectionTestUtils.setField(service, "whConf", conf);
+        ReflectionTestUtils.setField(service, "monthClosingService", closingService);
+        TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest();
+        request.setSubmitterUserId("user-1");
+        request.setDayText("20260818");
+        request.setFormData(Collections.singletonMap("numberField_mmd1smel", 8d));
+
+        assertThrows(McException.class, () -> service.upsert(request));
+        verify(client, never()).operateData(any(YDParam.class), eq(YDConf.FORM_OPERATION.upsert));
+    }
+
+    @Test
+    void shouldCalculateRepairDraftTotalsFromRowsInsteadOfPageTotals() {
+        YDClient client = mock(YDClient.class);
+        MonthClosingService closingService = mock(MonthClosingService.class);
+        WHConf conf = new WHConf();
+        conf.setFormUuidWorkHoursSummary("original-summary");
+        conf.setFormUuidRepairWorkHoursSummary("repair-summary");
+        conf.setRepairSummarySyncStatusField("repair-status");
+        when(closingService.isClosed("202608")).thenReturn(true);
+        DDR_New<Object> empty = new DDR_New<>();
+        empty.setData(Collections.emptyList());
+        when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
+                .thenReturn(empty);
+        TimeCardRepairService service = new TimeCardRepairService();
+        ReflectionTestUtils.setField(service, "ydClient", client);
+        ReflectionTestUtils.setField(service, "whConf", conf);
+        ReflectionTestUtils.setField(service, "monthClosingService", closingService);
+        Map<String, Object> child = new HashMap<>();
+        child.put("numberField_mmczo635", 5d);
+        Map<String, Object> payload = new HashMap<>();
+        payload.put("tableField_mmczo634", Collections.singletonList(child));
+        payload.put("numberField_mmeakgi4", 9d);
+        payload.put("numberField_mmd1smel", 99d);
+        TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest();
+        request.setSubmitterUserId("user-1");
+        request.setDayText("20260818");
+        request.setFormData(payload);
+
+        assertTrue(service.upsert(request));
+        ArgumentCaptor<YDParam> captor = ArgumentCaptor.forClass(YDParam.class);
+        verify(client).operateData(captor.capture(), eq(YDConf.FORM_OPERATION.upsert));
+        Map<String, Object> written = JSON.parseObject(captor.getValue().getFormDataJson(), Map.class);
+        assertEquals(5d, ((Number) written.get("numberField_mmd1smel")).doubleValue());
+        assertEquals(0d, ((Number) written.get("numberField_mmeakgi4")).doubleValue());
+        assertEquals("暂存", written.get("repair-status"));
     }
 
     @Test

+ 71 - 0
mjava-akdsbeisen/src/test/java/com/malk/service/workhours/TimeCardSummaryServiceTest.java

@@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSON;
 import com.malk.server.aliwork.YDConf;
 import com.malk.server.aliwork.YDParam;
 import com.malk.server.dingtalk.DDR_New;
+import com.malk.server.common.McException;
 import com.malk.server.workhours.TimeCardDuplicateCleanupRequest;
 import com.malk.server.workhours.TimeCardDuplicateCleanupResult;
 import com.malk.server.workhours.TimeCardSummaryUpsertRequest;
@@ -47,6 +48,10 @@ class TimeCardSummaryServiceTest {
         request.setSubmitterUserId("user-1");
         request.setDayText("20260716");
         request.setFormData(Collections.singletonMap("numberField_hours", 8));
+        DDR_New<Object> empty = new DDR_New<>();
+        empty.setData(Collections.emptyList());
+        when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
+                .thenReturn(empty);
 
         assertTrue(service.upsert(request));
 
@@ -63,6 +68,72 @@ class TimeCardSummaryServiceTest {
         assertEquals(8, ((Number) formData.get("numberField_hours")).intValue());
     }
 
+    @Test
+    void shouldNotOverwriteApprovedSummaryEvenWhenDailyTotalIsZero() {
+        YDClient ydClient = mock(YDClient.class);
+        WHConf whConf = new WHConf();
+        whConf.setFormUuidWorkHoursSummary("FORM-SUMMARY");
+        TimeCardSummaryService service = new TimeCardSummaryService();
+        ReflectionTestUtils.setField(service, "ydClient", ydClient);
+        ReflectionTestUtils.setField(service, "whConf", whConf);
+
+        Map<String, Object> item = summaryItem("APPROVED-ID", "user-1", "20260716");
+        Map<String, Object> data = (Map<String, Object>) item.get("formData");
+        data.put("numberField_mmeakgi4", 0);
+        Map<String, Object> child = new LinkedHashMap<>();
+        child.put("numberField_mmczo637", 8);
+        data.put("tableField_mmczo634", Collections.singletonList(child));
+        DDR_New<Object> existing = new DDR_New<>();
+        existing.setData(Collections.singletonList(item));
+        when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
+                .thenReturn(existing);
+        TimeCardSummaryUpsertRequest request = new TimeCardSummaryUpsertRequest();
+        request.setSubmitterUserId("user-1");
+        request.setDayText("20260716");
+        request.setFormData(Collections.singletonMap("numberField_mmd1smel", 8));
+
+        assertThrows(McException.class, () -> service.upsert(request));
+        verify(ydClient, never()).operateData(any(YDParam.class), eq(YDConf.FORM_OPERATION.upsert));
+    }
+
+    @Test
+    void shouldNotOverwriteApprovedRepairFromNormalSummary() {
+        YDClient ydClient = mock(YDClient.class);
+        WHConf whConf = new WHConf();
+        whConf.setFormUuidWorkHoursSummary("FORM-SUMMARY");
+        whConf.setFormUuidRepairWorkHoursSummary("FORM-REPAIR");
+        whConf.setRepairSummarySyncStatusField("repair-status");
+        TimeCardSummaryService service = new TimeCardSummaryService();
+        ReflectionTestUtils.setField(service, "ydClient", ydClient);
+        ReflectionTestUtils.setField(service, "whConf", whConf);
+        DDR_New<Object> empty = new DDR_New<>();
+        empty.setData(Collections.emptyList());
+        Map<String, Object> repair = summaryItem("REPAIR-ID", "user-1", "20260716");
+        ((Map<String, Object>) repair.get("formData")).put("repair-status", "已审批");
+        DDR_New<Object> approved = new DDR_New<>();
+        approved.setData(Collections.singletonList(repair));
+        when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
+                .thenReturn(empty, approved);
+        TimeCardSummaryUpsertRequest request = new TimeCardSummaryUpsertRequest();
+        request.setSubmitterUserId("user-1");
+        request.setDayText("20260716");
+        request.setFormData(Collections.singletonMap("numberField_mmd1smel", 8));
+
+        assertThrows(McException.class, () -> service.upsert(request));
+        verify(ydClient, never()).operateData(any(YDParam.class), eq(YDConf.FORM_OPERATION.upsert));
+    }
+
+    @Test
+    void shouldLockWholeFormalDayWhenOneChildIsSubmitted() {
+        Map<String, Object> record = summaryItem("SUBMITTED-ID", "user-1", "20260716");
+        Map<String, Object> child = new LinkedHashMap<>();
+        child.put("numberField_mmczo636", 8);
+        ((Map<String, Object>) record.get("formData"))
+                .put("tableField_mmczo634", Collections.singletonList(child));
+
+        assertTrue(TimeCardSummaryService.isFormalRecordLocked(record));
+    }
+
     @Test
     void shouldValidateDuplicateCleanupWithoutDeletingInDryRun() {
         YDClient ydClient = mock(YDClient.class);