Procházet zdrojové kódy

chore: prepare minglei project push

ht před 5 dny
rodič
revize
c35ae02e93

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 254 - 279
mjava-minglei/README.md


+ 38 - 20
mjava-minglei/src/main/java/com/malk/minglei/service/impl/yida/ApproveOrderFormPushHandler.java

@@ -17,7 +17,6 @@ import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.ZoneId;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -28,7 +27,7 @@ public class ApproveOrderFormPushHandler implements YidaFormPushHandler {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(ApproveOrderFormPushHandler.class);
     private static final String START_PROCESS_ENDPOINT = "/v1.0/yida/processes/instances/start";
-    public static final String TYPE = "approveOrder";
+    public static final String TYPE = "axmt500";
     private static final String FORM_NAME = "approveOrder";
     private static final String FORM_UUID = "FORM-8454FA17732746058A2D90B1C559CC19G5SR";
     private static final String PROCESS_CODE = "TPROC--9VD66QA18AJ8317KK3NXM54W1HIT30C7N28TM7";
@@ -95,7 +94,7 @@ public class ApproveOrderFormPushHandler implements YidaFormPushHandler {
 
     @Override
     public List<String> getTypes() {
-        return Arrays.asList(TYPE, "axmt500", FORM_UUID);
+        return Collections.singletonList(TYPE);
     }
 
     @Override
@@ -141,7 +140,17 @@ public class ApproveOrderFormPushHandler implements YidaFormPushHandler {
         putIfNotBlank(main, "gs", request.getGs());
         putIfNotBlank(main, "tjr", request.getTjr());
         Map<String, Object> detailWrapper = objectToMap(request.getDetailData());
-        List<Map<String, Object>> detailRows = toMapRows(detailWrapper.get("detailData"));
+        Object legacyRows = detailWrapper.get("detailData");
+        Object numberedRows1 = detailWrapper.get("detailData1");
+        Object numberedRows2 = detailWrapper.get("detailData2");
+        Object deliverySource = firstNonBlank(numberedRows1, legacyRows);
+        Object orderSource = firstNonBlank(numberedRows2, legacyRows);
+        if (isBlank(numberedRows2) && !isBlank(legacyRows) && !isBlank(numberedRows1)) {
+            deliverySource = legacyRows;
+            orderSource = numberedRows1;
+        }
+        List<Map<String, Object>> deliverySourceRows = toMapRows(deliverySource);
+        List<Map<String, Object>> orderSourceRows = toMapRows(orderSource);
         String orderNo = toText(value(main, "xmdadocno"));
         String title = toText(firstNonBlank(value(main, "title", "dataTitle"), orderNo == null ? null : "approveOrder-" + orderNo));
 
@@ -169,11 +178,11 @@ public class ApproveOrderFormPushHandler implements YidaFormPushHandler {
         putIfNotBlank(formData, APPROVAL_RESULT_FIELD_ID, firstNonBlank(value(main, "approvalResult"), "审核"));
         putIfNotBlank(formData, RESPONSIBILITY_FIELD_ID, toStringList(firstNonBlank(value(main, "responsibility"), "无"), null));
 
-        List<Map<String, Object>> deliveryRows = toDeliveryRows(detailRows);
+        List<Map<String, Object>> deliveryRows = toDeliveryRows(deliverySourceRows);
         if (!deliveryRows.isEmpty()) {
             formData.put(DELIVERY_TABLE_FIELD_ID, deliveryRows);
         }
-        List<Map<String, Object>> orderRows = toOrderRows(detailRows);
+        List<Map<String, Object>> orderRows = toOrderRows(orderSourceRows);
         if (!orderRows.isEmpty()) {
             formData.put(DETAIL_TABLE_FIELD_ID, orderRows);
         }
@@ -204,19 +213,19 @@ public class ApproveOrderFormPushHandler implements YidaFormPushHandler {
         List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
         for (Map<String, Object> source : sourceRows) {
             Map<String, Object> row = new LinkedHashMap<String, Object>();
-            putIfNotBlank(row, DETAIL_ENTERPRISE_NO_FIELD_ID, value(source, "xmdcent"));
-            putIfNotBlank(row, DETAIL_ORDER_NO_FIELD_ID, value(source, "xmdcdocno"));
-            putIfNotBlank(row, DETAIL_ORDER_ITEM_FIELD_ID, toDecimal(value(source, "xmdcseq")));
-            putIfNotBlank(row, DETAIL_ITEM_SEQ_FIELD_ID, toDecimal(value(source, "xmdcseq")));
-            putIfNotBlank(row, DETAIL_BATCH_SEQ_FIELD_ID, toDecimal(value(source, "xmdcseq")));
-            putIfNotBlank(row, DETAIL_MATERIAL_CODE_FIELD_ID, value(source, "xmdc001"));
+            putIfNotBlank(row, DETAIL_ENTERPRISE_NO_FIELD_ID, value(source, "xmddent", "xmdcent"));
+            putIfNotBlank(row, DETAIL_ORDER_NO_FIELD_ID, value(source, "xmdddocno", "xmdcdocno"));
+            putIfNotBlank(row, DETAIL_ORDER_ITEM_FIELD_ID, toDecimal(value(source, "xmddseq", "xmdcseq")));
+            putIfNotBlank(row, DETAIL_ITEM_SEQ_FIELD_ID, toDecimal(value(source, "xmddseq1", "xmdcseq")));
+            putIfNotBlank(row, DETAIL_BATCH_SEQ_FIELD_ID, toDecimal(value(source, "xmddseq2", "xmdcseq")));
+            putIfNotBlank(row, DETAIL_MATERIAL_CODE_FIELD_ID, value(source, "xmdd001", "xmdc001"));
             putIfNotBlank(row, DETAIL_ITEM_NAME_FIELD_ID, value(source, "imaal003"));
             putIfNotBlank(row, DETAIL_SPECIFICATION_FIELD_ID, value(source, "imaal004"));
-            putIfNotBlank(row, DETAIL_PRODUCT_FEATURE_FIELD_ID, value(source, "xmdc002"));
-            putIfNotBlank(row, DETAIL_SALES_UNIT_FIELD_ID, value(source, "xmdc006"));
-            putIfNotBlank(row, DETAIL_DELIVERY_TYPE_FIELD_ID, value(source, "xmdc050"));
-            putIfNotBlank(row, DETAIL_TOTAL_QUANTITY_FIELD_ID, value(source, "xmdc007"));
-            putIfNotBlank(row, DETAIL_PROMISED_DATE_FIELD_ID, toDateMillis(value(source, "xmdc012")));
+            putIfNotBlank(row, DETAIL_PRODUCT_FEATURE_FIELD_ID, value(source, "xmdd002", "xmdc002"));
+            putIfNotBlank(row, DETAIL_SALES_UNIT_FIELD_ID, value(source, "xmdd004", "xmdc006"));
+            putIfNotBlank(row, DETAIL_DELIVERY_TYPE_FIELD_ID, value(source, "xmdd009", "xmdc050"));
+            putIfNotBlank(row, DETAIL_TOTAL_QUANTITY_FIELD_ID, value(source, "xmdd005", "xmdc007"));
+            putIfNotBlank(row, DETAIL_PROMISED_DATE_FIELD_ID, toDateMillis(value(source, "xmdd011", "xmdc012")));
             copyYidaFieldValues(source, row);
             if (!row.isEmpty()) {
                 rows.add(row);
@@ -328,8 +337,7 @@ public class ApproveOrderFormPushHandler implements YidaFormPushHandler {
             return Collections.emptyMap();
         }
         if (value instanceof Map) {
-            Map result = (Map) value;
-            return result;
+            return copyMap((Map<?, ?>) value);
         }
         if (value instanceof ErpYidaPushMainData) {
             ErpYidaPushMainData mainData = (ErpYidaPushMainData) value;
@@ -344,7 +352,17 @@ public class ApproveOrderFormPushHandler implements YidaFormPushHandler {
             result.putAll(mainData.getExtraFields());
             return result;
         }
-        return JSON.parseObject(JSON.toJSONString(value), Map.class);
+        return JSON.parseObject(JSON.toJSONString(value));
+    }
+
+    private Map<String, Object> copyMap(Map<?, ?> source) {
+        Map<String, Object> result = new LinkedHashMap<String, Object>();
+        for (Map.Entry<?, ?> entry : source.entrySet()) {
+            if (entry.getKey() != null) {
+                result.put(String.valueOf(entry.getKey()), entry.getValue());
+            }
+        }
+        return result;
     }
 
     private boolean isBlank(Object value) {

+ 24 - 9
mjava-minglei/src/main/java/com/malk/minglei/service/impl/yida/Cpmp402YidaProcessHandler.java

@@ -8,14 +8,16 @@ import com.malk.minglei.service.yida.YidaProcessHandler;
 import com.malk.server.aliwork.YDConf;
 import com.malk.server.dingtalk.DDConf;
 import com.malk.service.aliwork.YDClient;
-import org.springframework.beans.factory.annotation.Qualifier;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDate;
 import java.time.ZoneId;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -103,12 +105,12 @@ public class Cpmp402YidaProcessHandler implements YidaProcessHandler {
 
     private Map<String, Object> toYidaFormData(YidaProcessStartRequest request, String title) {
         Map<String, Object> rawFields = request.getFields();
-        Map<String, Object> formData = new java.util.LinkedHashMap<String, Object>();
+        Map<String, Object> formData = new LinkedHashMap<String, Object>();
         addKnownYidaFields(rawFields, formData);
 
         Map<String, Object> mainData = objectToMap(rawFields.get("mainData"));
         formData.put(TITLE_FIELD_ID, title);
-        formData.put(APPLICANT_FIELD_ID, java.util.Collections.singletonList(request.getUserId()));
+        formData.put(APPLICANT_FIELD_ID, Collections.singletonList(request.getUserId()));
         formData.put(FORM_TITLE_FIELD_ID, title);
         putIfNotBlank(formData, COMPANY_FIELD_ID, mainData.get("gs"));
         putIfNotBlank(formData, APPLY_DATE_FIELD_ID, toDateMillis(mainData.get("rq")));
@@ -145,9 +147,9 @@ public class Cpmp402YidaProcessHandler implements YidaProcessHandler {
         if (rowSource instanceof List) {
             sourceRows = (List<?>) rowSource;
         } else if (rowSource == null) {
-            sourceRows = java.util.Collections.emptyList();
+            sourceRows = Collections.emptyList();
         } else {
-            sourceRows = java.util.Collections.singletonList(rowSource);
+            sourceRows = Collections.singletonList(rowSource);
         }
 
         List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
@@ -156,7 +158,7 @@ public class Cpmp402YidaProcessHandler implements YidaProcessHandler {
             if (source.isEmpty()) {
                 continue;
             }
-            Map<String, Object> row = new java.util.LinkedHashMap<String, Object>();
+            Map<String, Object> row = new LinkedHashMap<String, Object>();
             putIfNotBlank(row, DETAIL_PRICE_NO_FIELD_ID, source.get("hjdh"));
             putIfNotBlank(row, DETAIL_SUPPLIER_CODE_FIELD_ID, source.get("gysbh"));
             putIfNotBlank(row, DETAIL_SUPPLIER_NAME_FIELD_ID, source.get("gysmc"));
@@ -172,9 +174,22 @@ public class Cpmp402YidaProcessHandler implements YidaProcessHandler {
 
     private Map<String, Object> objectToMap(Object value) {
         if (value == null) {
-            return java.util.Collections.emptyMap();
+            return Collections.emptyMap();
+        }
+        if (value instanceof Map) {
+            return copyMap((Map<?, ?>) value);
+        }
+        return JSON.parseObject(JSON.toJSONString(value));
+    }
+
+    private Map<String, Object> copyMap(Map<?, ?> source) {
+        Map<String, Object> result = new LinkedHashMap<String, Object>();
+        for (Map.Entry<?, ?> entry : source.entrySet()) {
+            if (entry.getKey() != null) {
+                result.put(String.valueOf(entry.getKey()), entry.getValue());
+            }
         }
-        return JSON.parseObject(JSON.toJSONString(value), Map.class);
+        return result;
     }
 
     private Object firstNonBlank(Object first, Object second) {
@@ -223,7 +238,7 @@ public class Cpmp402YidaProcessHandler implements YidaProcessHandler {
     }
 
     private Map<String, Object> maskSensitive(YidaOperationParam parameter) {
-        Map<String, Object> body = JSON.parseObject(JSON.toJSONString(parameter), Map.class);
+        Map<String, Object> body = JSON.parseObject(JSON.toJSONString(parameter));
         if (body.containsKey("systemToken")) {
             body.put("systemToken", "******");
         }

+ 392 - 0
mjava-minglei/src/main/java/com/malk/minglei/service/impl/yida/CustomerReleaseFormPushHandler.java

@@ -0,0 +1,392 @@
+package com.malk.minglei.service.impl.yida;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.minglei.config.YidaApplicationsProperties;
+import com.malk.minglei.dto.ErpYidaPushMainData;
+import com.malk.minglei.dto.ErpYidaPushRequest;
+import com.malk.minglei.service.yida.YidaFormPushHandler;
+import com.malk.minglei.service.yida.YidaProcessOpenApiClient;
+import com.malk.server.common.McException;
+import com.malk.server.dingtalk.DDR_New;
+import com.malk.server.dingtalk.DDConf;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class CustomerReleaseFormPushHandler implements YidaFormPushHandler {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(CustomerReleaseFormPushHandler.class);
+    private static final String START_PROCESS_ENDPOINT = "/v1.0/yida/processes/instances/start";
+    public static final String TYPE = "axmt140";
+    private static final String FORM_NAME = "customerRelease";
+    private static final String FORM_UUID = "FORM-3BA9FE18494F4FDD85FE6ABF7DE741BAE30C";
+    private static final String PROCESS_CODE = "TPROC--6TG66381N8G8Y7L9GBEEY7QVXTUY2LBBGZ7TMU";
+    private static final String DEFAULT_ORIGINATOR_USER_ID = "41603502201288023";
+
+    private static final String TITLE_FIELD_ID = "textField_mtv1tgg1";
+    private static final String APPLICANT_FIELD_ID = "employeeField_msh186rr";
+    private static final String COMPANY_FIELD_ID = "selectField_mtv7uxgd";
+    private static final String DEPARTMENT_FIELD_ID = "departmentSelectField_msh186ry";
+    private static final String APPLY_DATE_FIELD_ID = "dateField_msh186rs";
+    private static final String FORM_TITLE_FIELD_ID = "textField_mspcpz92";
+    private static final String RELEASE_NO_FIELD_ID = "textField_mt7zi4fy";
+    private static final String RELEASE_DESC_FIELD_ID = "textField_mt7zi4g0";
+    private static final String NO_DELIVERY_REASON_FIELD_ID = "textField_mt7zi4fz";
+
+    private static final String DETAIL_TABLE_FIELD_ID = "tableField_msh186sv";
+    private static final String DETAIL_SEQ_FIELD_ID = "textField_mt7zi4g1";
+    private static final String DETAIL_NOTICE_NO_FIELD_ID = "textField_mt7zi4g2";
+    private static final String DETAIL_NOTICE_DATE_FIELD_ID = "dateField_mt7zi4g3";
+    private static final String DETAIL_CUSTOMER_CODE_FIELD_ID = "textField_mt6ur4w1";
+    private static final String DETAIL_CUSTOMER_NAME_FIELD_ID = "textField_mt7zi4g4";
+    private static final String DETAIL_TAX_AMOUNT_FIELD_ID = "numberField_mt7zi4g5";
+    private static final String DETAIL_SITE_CREDIT_FIELD_ID = "numberField_mt7zi4g6";
+    private static final String DETAIL_GROUP_CREDIT_FIELD_ID = "numberField_mt7zi4g7";
+    private static final String DETAIL_SHORT_REMARK_FIELD_ID = "textField_mt6ur4w8";
+
+    private final YidaProcessOpenApiClient yidaProcessOpenApiClient;
+    private final DDConf dingTalkConfiguration;
+    private final YidaApplicationsProperties applications;
+
+    public CustomerReleaseFormPushHandler(YidaProcessOpenApiClient yidaProcessOpenApiClient,
+                                          DDConf dingTalkConfiguration,
+                                          YidaApplicationsProperties applications) {
+        this.yidaProcessOpenApiClient = yidaProcessOpenApiClient;
+        this.dingTalkConfiguration = dingTalkConfiguration;
+        this.applications = applications;
+    }
+
+    @Override
+    public List<String> getTypes() {
+        return Collections.singletonList(TYPE);
+    }
+
+    @Override
+    public String push(ErpYidaPushRequest request) {
+        validateConfiguration();
+
+        String operatorUserId = resolveOperatorUserId(request);
+        String appType = requireText(applications.getCoordinationOffice().getAppType(), "YIDA_COORDINATION_OFFICE_APP_TYPE is not configured");
+        String systemToken = requireText(applications.getCoordinationOffice().getSystemToken(), "YIDA_COORDINATION_OFFICE_SYSTEM_TOKEN is not configured");
+        Map<String, Object> formData = toYidaFormData(request, operatorUserId);
+        String title = requireText(toText(formData.get(TITLE_FIELD_ID)), "Yida process title is not configured");
+        Map<String, Object> body = new LinkedHashMap<String, Object>();
+        body.put("appType", appType);
+        body.put("systemToken", systemToken);
+        body.put("userId", operatorUserId);
+        body.put("processCode", PROCESS_CODE);
+        body.put("formUuid", FORM_UUID);
+        body.put("formDataJson", JSON.toJSONString(formData));
+        body.put("executeExpression", true);
+        body.put("noExecuteExpression", false);
+        body.put("title", title);
+        body.put("dataTitle", title);
+        LOGGER.info("Yida process start request endpoint={} formName={} body={}",
+                START_PROCESS_ENDPOINT, FORM_NAME, JSON.toJSONString(maskSensitive(body)));
+
+        DDR_New response;
+        try {
+            response = yidaProcessOpenApiClient.startProcess(body);
+        } catch (McException exception) {
+            throw exception;
+        } catch (Exception exception) {
+            LOGGER.error("Yida process start failed formName={} body={}",
+                    FORM_NAME, JSON.toJSONString(maskSensitive(body)), exception);
+            throw new McException("DINGTALK_YIDA_ERROR", "Yida process start failed: " + rootMessage(exception));
+        }
+        return extractProcessInstanceId(response);
+    }
+
+    private Map<String, Object> toYidaFormData(ErpYidaPushRequest request, String operatorUserId) {
+        Map<String, Object> main = objectToMap(request.getMainData());
+        putIfNotBlank(main, "bm", request.getBm());
+        putIfNotBlank(main, "gs", request.getGs());
+        putIfNotBlank(main, "tjr", request.getTjr());
+        Map<String, Object> detailWrapper = objectToMap(request.getDetailData());
+        List<Map<String, Object>> detailRows = toMapRows(firstNonBlank(detailWrapper.get("detailData1"), detailWrapper.get("detailData")));
+        String docNo = toText(value(main, "xmfxdocno"));
+        String title = toText(firstNonBlank(value(main, "title", "dataTitle"), docNo == null ? null : "customerRelease-" + docNo));
+
+        Map<String, Object> formData = new LinkedHashMap<String, Object>();
+        putIfNotBlank(formData, TITLE_FIELD_ID, title);
+        putIfNotBlank(formData, APPLICANT_FIELD_ID, toStringList(operatorUserId, null));
+        putIfNotBlank(formData, COMPANY_FIELD_ID, firstNonBlank(value(main, "gs", "applyCompany", "company"), value(main, "xmfxent")));
+        putIfNotBlank(formData, DEPARTMENT_FIELD_ID, toStringList(value(main, "bm", "applyDepartment", "department", "deptId"), null));
+        putIfNotBlank(formData, APPLY_DATE_FIELD_ID, toDateMillis(firstNonBlank(value(main, "rq", "applyDate"), value(main, "xmfxdocdt"))));
+        putIfNotBlank(formData, FORM_TITLE_FIELD_ID, title);
+        putIfNotBlank(formData, RELEASE_NO_FIELD_ID, value(main, "xmfxdocno"));
+        putIfNotBlank(formData, RELEASE_DESC_FIELD_ID, value(main, "xmfx003"));
+        putIfNotBlank(formData, NO_DELIVERY_REASON_FIELD_ID, value(main, "xmfxud001"));
+
+        List<Map<String, Object>> rows = toDetailRows(detailRows);
+        if (!rows.isEmpty()) {
+            formData.put(DETAIL_TABLE_FIELD_ID, rows);
+        }
+        copyYidaFieldValues(main, formData);
+        return formData;
+    }
+
+    private List<Map<String, Object>> toDetailRows(List<Map<String, Object>> sourceRows) {
+        List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
+        for (Map<String, Object> source : sourceRows) {
+            Map<String, Object> row = new LinkedHashMap<String, Object>();
+            putIfNotBlank(row, DETAIL_SEQ_FIELD_ID, value(source, "xmfyseq"));
+            putIfNotBlank(row, DETAIL_NOTICE_NO_FIELD_ID, value(source, "xmfy002"));
+            putIfNotBlank(row, DETAIL_NOTICE_DATE_FIELD_ID, toDateMillis(value(source, "xmfy003")));
+            putIfNotBlank(row, DETAIL_CUSTOMER_CODE_FIELD_ID, value(source, "xmfy004"));
+            putIfNotBlank(row, DETAIL_CUSTOMER_NAME_FIELD_ID, value(source, "pmaal004"));
+            putIfNotBlank(row, DETAIL_TAX_AMOUNT_FIELD_ID, toDecimal(value(source, "xmfy005")));
+            putIfNotBlank(row, DETAIL_SITE_CREDIT_FIELD_ID, toDecimal(value(source, "xmfy006")));
+            putIfNotBlank(row, DETAIL_GROUP_CREDIT_FIELD_ID, toDecimal(value(source, "xmfy008")));
+            putIfNotBlank(row, DETAIL_SHORT_REMARK_FIELD_ID, value(source, "xmfy007"));
+            copyYidaFieldValues(source, row);
+            if (!row.isEmpty()) {
+                rows.add(row);
+            }
+        }
+        return rows;
+    }
+
+    private List<Map<String, Object>> toMapRows(Object value) {
+        List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
+        for (Object item : toList(value)) {
+            Map<String, Object> row = objectToMap(item);
+            if (!row.isEmpty()) {
+                rows.add(row);
+            }
+        }
+        return rows;
+    }
+
+    private List<?> toList(Object value) {
+        if (value instanceof List) {
+            return (List<?>) value;
+        }
+        if (value instanceof Iterable) {
+            List<Object> result = new ArrayList<Object>();
+            for (Object item : (Iterable<?>) value) {
+                result.add(item);
+            }
+            return result;
+        }
+        return value == null ? Collections.emptyList() : Collections.singletonList(value);
+    }
+
+    private List<String> toStringList(Object value, String fallback) {
+        Object actualValue = isBlank(value) ? fallback : value;
+        if (actualValue == null) {
+            return Collections.emptyList();
+        }
+        if (actualValue instanceof Iterable) {
+            List<String> result = new ArrayList<String>();
+            for (Object item : (Iterable<?>) actualValue) {
+                if (!isBlank(item)) {
+                    result.add(String.valueOf(item).trim());
+                }
+            }
+            return result;
+        }
+        return Collections.singletonList(String.valueOf(actualValue).trim());
+    }
+
+    private BigDecimal toDecimal(Object value) {
+        if (isBlank(value)) {
+            return null;
+        }
+        if (value instanceof BigDecimal) {
+            return (BigDecimal) value;
+        }
+        if (value instanceof Number) {
+            return new BigDecimal(String.valueOf(value));
+        }
+        return new BigDecimal(String.valueOf(value).trim());
+    }
+
+    private Long toDateMillis(Object value) {
+        if (isBlank(value)) {
+            return null;
+        }
+        String text = String.valueOf(value).trim();
+        if (text.matches("\\d+")) {
+            return Long.valueOf(text);
+        }
+        return LocalDate.parse(text).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+    }
+
+    private Object value(Map<String, Object> source, String firstKey, String... otherKeys) {
+        Object value = source.get(firstKey);
+        if (!isBlank(value)) {
+            return value;
+        }
+        for (String key : otherKeys) {
+            value = source.get(key);
+            if (!isBlank(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    private Object firstNonBlank(Object first, Object second) {
+        return isBlank(first) ? second : first;
+    }
+
+    private void putIfNotBlank(Map<String, Object> target, String fieldId, Object value) {
+        if (!isBlank(value)) {
+            target.put(fieldId, value);
+        }
+    }
+
+    private void copyYidaFieldValues(Map<String, Object> source, Map<String, Object> target) {
+        for (Map.Entry<String, Object> entry : source.entrySet()) {
+            if (entry.getKey() != null && entry.getKey().contains("Field_") && !isBlank(entry.getValue())) {
+                target.put(entry.getKey(), entry.getValue());
+            }
+        }
+    }
+
+    private Map<String, Object> objectToMap(Object value) {
+        if (value == null) {
+            return Collections.emptyMap();
+        }
+        if (value instanceof Map) {
+            return copyMap((Map<?, ?>) value);
+        }
+        if (value instanceof ErpYidaPushMainData) {
+            ErpYidaPushMainData mainData = (ErpYidaPushMainData) value;
+            Map<String, Object> result = new LinkedHashMap<String, Object>();
+            putIfNotBlank(result, "rq", mainData.getRq());
+            putIfNotBlank(result, "bm", mainData.getBm());
+            putIfNotBlank(result, "tjr", mainData.getTjr());
+            putIfNotBlank(result, "gs", mainData.getGs());
+            putIfNotBlank(result, "t100id", mainData.getT100id());
+            putIfNotBlank(result, "ent", mainData.getEnt());
+            putIfNotBlank(result, "t100bm", mainData.getT100bm());
+            result.putAll(mainData.getExtraFields());
+            return result;
+        }
+        return JSON.parseObject(JSON.toJSONString(value));
+    }
+
+    private Map<String, Object> copyMap(Map<?, ?> source) {
+        Map<String, Object> result = new LinkedHashMap<String, Object>();
+        for (Map.Entry<?, ?> entry : source.entrySet()) {
+            if (entry.getKey() != null) {
+                result.put(String.valueOf(entry.getKey()), entry.getValue());
+            }
+        }
+        return result;
+    }
+
+    private boolean isBlank(Object value) {
+        if (value == null) {
+            return true;
+        }
+        if (value instanceof Iterable) {
+            return !((Iterable<?>) value).iterator().hasNext();
+        }
+        if (value instanceof Map) {
+            return ((Map<?, ?>) value).isEmpty();
+        }
+        return String.valueOf(value).trim().isEmpty();
+    }
+
+    private String resolveOperatorUserId(ErpYidaPushRequest request) {
+        if (!isBlank(request.getOriginatorUserId())) {
+            return request.getOriginatorUserId().trim();
+        }
+        Map<String, Object> main = objectToMap(request.getMainData());
+        Object operator = firstNonBlank(value(main, "originatorUserId", "userId"), value(main, "applicant"));
+        return String.valueOf(isBlank(operator) ? DEFAULT_ORIGINATOR_USER_ID : firstItem(operator)).trim();
+    }
+
+    private Object firstItem(Object value) {
+        if (value instanceof Iterable) {
+            for (Object item : (Iterable<?>) value) {
+                if (!isBlank(item)) {
+                    return item;
+                }
+            }
+            return null;
+        }
+        return value;
+    }
+
+    private String toText(Object value) {
+        return isBlank(value) ? null : String.valueOf(value);
+    }
+
+    private Map<String, Object> maskSensitive(Map<String, Object> source) {
+        Map<String, Object> body = new LinkedHashMap<String, Object>(source);
+        if (body.containsKey("systemToken")) {
+            body.put("systemToken", "******");
+        }
+        return body;
+    }
+
+    private String extractProcessInstanceId(DDR_New response) {
+        if (response == null) {
+            throw new McException("YIDA_NULL_RESPONSE", "Yida process start failed: empty response");
+        }
+        LOGGER.info("Yida process start response formName={} success={} code={} result={} instanceId={} formInstId={}",
+                FORM_NAME, response.isSuccess(), response.getCode(), response.getResult(), response.getInstanceId(), response.getFormInstId());
+
+        Object result = firstNonBlank(firstNonBlank(response.getResult(), response.getInstanceId()), response.getFormInstId());
+        if (!isBlank(result)) {
+            return String.valueOf(result);
+        }
+        if (response.isSuccess()) {
+            return "SUCCESS";
+        }
+        String code = defaultText(response.getCode(), "YIDA_ERROR");
+        String message = defaultText(response.getMessage(), "Yida process start failed: no process instance id");
+        throw new McException(code, "Yida API [startProcess] failed: " + message);
+    }
+
+    private String defaultText(String value, String fallback) {
+        return value == null || value.trim().isEmpty() ? fallback : value.trim();
+    }
+
+    private String rootMessage(Throwable exception) {
+        Throwable current = exception;
+        while (current.getCause() != null && current.getCause() != current) {
+            current = current.getCause();
+        }
+        String message = current.getMessage();
+        if (message == null || message.trim().isEmpty()) {
+            message = exception.getMessage();
+        }
+        if (message == null || message.trim().isEmpty()) {
+            message = current.getClass().getSimpleName();
+        }
+        return message.trim();
+    }
+
+    private void validateConfiguration() {
+        require(dingTalkConfiguration.getAppKey(), "DINGTALK_APP_KEY is not configured");
+        require(dingTalkConfiguration.getAppSecret(), "DINGTALK_APP_SECRET is not configured");
+        require(applications.getCoordinationOffice().getAppType(), "YIDA_COORDINATION_OFFICE_APP_TYPE is not configured");
+        require(applications.getCoordinationOffice().getSystemToken(), "YIDA_COORDINATION_OFFICE_SYSTEM_TOKEN is not configured");
+    }
+
+    private String requireText(String value, String message) {
+        require(value, message);
+        return value.trim();
+    }
+
+    private void require(String value, String message) {
+        if (value == null || value.trim().isEmpty()) {
+            throw new IllegalStateException(message);
+        }
+    }
+}

+ 29 - 8
mjava-minglei/src/main/java/com/malk/minglei/service/impl/yida/ErpYidaPushServiceImpl.java

@@ -107,12 +107,21 @@ public class ErpYidaPushServiceImpl implements ErpYidaPushService {
         if (!(detailData instanceof Map)) {
             throw new IllegalArgumentException("ERP Yida push detailData must be an object containing detailData array");
         }
-        Object rows = ((Map<?, ?>) detailData).get("detailData");
-        if (!(rows instanceof List)) {
+        if (!hasDetailDataRows((Map<?, ?>) detailData)) {
             throw new IllegalArgumentException("ERP Yida push detailData.detailData must be an array");
         }
     }
 
+    private boolean hasDetailDataRows(Map<?, ?> detailData) {
+        for (Map.Entry<?, ?> entry : detailData.entrySet()) {
+            Object key = entry.getKey();
+            if (key != null && String.valueOf(key).matches("detailData\\d*") && entry.getValue() instanceof List) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private YidaProcessStartRequest toProcessStartRequest(ErpYidaPushRequest request, String originatorUserId) {
         YidaProcessStartRequest yidaRequest = new YidaProcessStartRequest();
         yidaRequest.setType(request.getType());
@@ -176,26 +185,38 @@ public class ErpYidaPushServiceImpl implements ErpYidaPushService {
 
     private void applyMainDataDefaults(ErpYidaPushRequest request, String originatorUserId) {
         ErpYidaPushMainData mainData = request.getMainData();
-        Map<String, Object> wrapper = new LinkedHashMap<String, Object>((Map<String, Object>) request.getDetailData());
+        Map<String, Object> wrapper = copyMap((Map<?, ?>) request.getDetailData());
 
-        Object rows = wrapper.get("detailData");
-        if (rows instanceof List) {
+        for (Map.Entry<String, Object> entry : new ArrayList<Map.Entry<String, Object>>(wrapper.entrySet())) {
+            if (entry.getKey() == null || !entry.getKey().matches("detailData\\d*") || !(entry.getValue() instanceof List)) {
+                continue;
+            }
             List<Object> normalizedRows = new ArrayList<Object>();
-            for (Object row : (List<?>) rows) {
+            for (Object row : (List<?>) entry.getValue()) {
                 if (row instanceof Map) {
-                    Map<String, Object> normalizedRow = new LinkedHashMap<String, Object>((Map<String, Object>) row);
+                    Map<String, Object> normalizedRow = copyMap((Map<?, ?>) row);
                     applyMainDataDefaults(normalizedRow, mainData, originatorUserId);
                     normalizedRows.add(normalizedRow);
                 } else {
                     normalizedRows.add(row);
                 }
             }
-            wrapper.put("detailData", normalizedRows);
+            wrapper.put(entry.getKey(), normalizedRows);
         }
 
         request.setDetailData(wrapper);
     }
 
+    private Map<String, Object> copyMap(Map<?, ?> source) {
+        Map<String, Object> result = new LinkedHashMap<String, Object>();
+        for (Map.Entry<?, ?> entry : source.entrySet()) {
+            if (entry.getKey() != null) {
+                result.put(String.valueOf(entry.getKey()), entry.getValue());
+            }
+        }
+        return result;
+    }
+
     private void applyMainDataDefaults(Map<String, Object> target, ErpYidaPushMainData mainData, String originatorUserId) {
         putIfMissing(target, "originatorUserId", originatorUserId);
         putIfMissing(target, "userId", originatorUserId);

+ 416 - 0
mjava-minglei/src/main/java/com/malk/minglei/service/impl/yida/MiscReceiptIssueFormPushHandler.java

@@ -0,0 +1,416 @@
+package com.malk.minglei.service.impl.yida;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.minglei.config.YidaApplicationsProperties;
+import com.malk.minglei.dto.ErpYidaPushMainData;
+import com.malk.minglei.dto.ErpYidaPushRequest;
+import com.malk.minglei.service.yida.YidaFormPushHandler;
+import com.malk.minglei.service.yida.YidaProcessOpenApiClient;
+import com.malk.server.common.McException;
+import com.malk.server.dingtalk.DDR_New;
+import com.malk.server.dingtalk.DDConf;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(MiscReceiptIssueFormPushHandler.class);
+    private static final String START_PROCESS_ENDPOINT = "/v1.0/yida/processes/instances/start";
+    public static final String TYPE = "cint301";
+    private static final String FORM_NAME = "miscReceiptIssue";
+    private static final String FORM_UUID = "FORM-4B08ADABCDAA45788C043729A855E03ED76G";
+    private static final String PROCESS_CODE = "TPROC--MQD66371C1N84FPSJZO4W79IN4BW2AE7I08TM0";
+    private static final String DEFAULT_ORIGINATOR_USER_ID = "41603502201288023";
+
+    private static final String TITLE_FIELD_ID = "textField_msh27unu";
+    private static final String APPLICANT_FIELD_ID = "employeeField_msh186rr";
+    private static final String COMPANY_FIELD_ID = "selectField_mso1em48";
+    private static final String DEPARTMENT_FIELD_ID = "departmentSelectField_msh186ry";
+    private static final String APPLY_DATE_FIELD_ID = "dateField_msh186rs";
+    private static final String FORM_TITLE_FIELD_ID = "textField_mspcpz92";
+    private static final String DATA_SOURCE_FIELD_ID = "textField_mt7zi4fy";
+    private static final String APPLICANT_NO_FIELD_ID = "textField_mt7zi4g0";
+    private static final String DEPARTMENT_NO_FIELD_ID = "textField_mt80nq6s";
+    private static final String REASON_CODE_FIELD_ID = "textField_mt80nq6t";
+    private static final String REASON_DESC_FIELD_ID = "textField_mt80nq6v";
+    private static final String REMARK_FIELD_ID = "textField_mt7zi4fz";
+
+    private static final String DETAIL_TABLE_FIELD_ID = "tableField_msh186sv";
+    private static final String DETAIL_SEQ_FIELD_ID = "textField_mt7zi4g1";
+    private static final String DETAIL_ORDER_NO_FIELD_ID = "textField_mt80nq6y";
+    private static final String DETAIL_ORDER_SEQ_FIELD_ID = "textField_mt80nq6z";
+    private static final String DETAIL_ITEM_CODE_FIELD_ID = "textField_mt80nq70";
+    private static final String DETAIL_ITEM_NAME_FIELD_ID = "textField_mt80nq71";
+    private static final String DETAIL_SPEC_FIELD_ID = "textField_mt80nq72";
+    private static final String DETAIL_PRODUCT_FEATURE_FIELD_ID = "textField_mt80nq73";
+    private static final String DETAIL_WAREHOUSE_FIELD_ID = "textField_mt80nq74";
+    private static final String DETAIL_LOCATION_FIELD_ID = "textField_mt80nq75";
+    private static final String DETAIL_LOT_NO_FIELD_ID = "textField_mt7zi4g2";
+    private static final String DETAIL_STOCK_FEATURE_FIELD_ID = "textField_mt80nq76";
+    private static final String DETAIL_QUANTITY_FIELD_ID = "numberField_mt80nq77";
+    private static final String DETAIL_UNIT_FIELD_ID = "textField_mt6ur4w1";
+    private static final String DETAIL_REASON_CODE_FIELD_ID = "textField_mt7zi4g4";
+    private static final String DETAIL_REASON_DESC_FIELD_ID = "textField_mt80nq79";
+    private static final String DETAIL_PROJECT_NO_FIELD_ID = "textField_mt6ur4w8";
+    private static final String DETAIL_PROJECT_DESC_FIELD_ID = "textField_mt80nq7a";
+    private static final String DETAIL_SOURCE_DOC_NO_FIELD_ID = "textField_mt80nq7b";
+
+    private final YidaProcessOpenApiClient yidaProcessOpenApiClient;
+    private final DDConf dingTalkConfiguration;
+    private final YidaApplicationsProperties applications;
+
+    public MiscReceiptIssueFormPushHandler(YidaProcessOpenApiClient yidaProcessOpenApiClient,
+                                           DDConf dingTalkConfiguration,
+                                           YidaApplicationsProperties applications) {
+        this.yidaProcessOpenApiClient = yidaProcessOpenApiClient;
+        this.dingTalkConfiguration = dingTalkConfiguration;
+        this.applications = applications;
+    }
+
+    @Override
+    public List<String> getTypes() {
+        return Collections.singletonList(TYPE);
+    }
+
+    @Override
+    public String push(ErpYidaPushRequest request) {
+        validateConfiguration();
+
+        String operatorUserId = resolveOperatorUserId(request);
+        String appType = requireText(applications.getCoordinationOffice().getAppType(), "YIDA_COORDINATION_OFFICE_APP_TYPE is not configured");
+        String systemToken = requireText(applications.getCoordinationOffice().getSystemToken(), "YIDA_COORDINATION_OFFICE_SYSTEM_TOKEN is not configured");
+        Map<String, Object> formData = toYidaFormData(request, operatorUserId);
+        String title = requireText(toText(formData.get(TITLE_FIELD_ID)), "Yida process title is not configured");
+        Map<String, Object> body = new LinkedHashMap<String, Object>();
+        body.put("appType", appType);
+        body.put("systemToken", systemToken);
+        body.put("userId", operatorUserId);
+        body.put("processCode", PROCESS_CODE);
+        body.put("formUuid", FORM_UUID);
+        body.put("formDataJson", JSON.toJSONString(formData));
+        body.put("executeExpression", true);
+        body.put("noExecuteExpression", false);
+        body.put("title", title);
+        body.put("dataTitle", title);
+        LOGGER.info("Yida process start request endpoint={} formName={} body={}",
+                START_PROCESS_ENDPOINT, FORM_NAME, JSON.toJSONString(maskSensitive(body)));
+
+        DDR_New response;
+        try {
+            response = yidaProcessOpenApiClient.startProcess(body);
+        } catch (McException exception) {
+            throw exception;
+        } catch (Exception exception) {
+            LOGGER.error("Yida process start failed formName={} body={}",
+                    FORM_NAME, JSON.toJSONString(maskSensitive(body)), exception);
+            throw new McException("DINGTALK_YIDA_ERROR", "Yida process start failed: " + rootMessage(exception));
+        }
+        return extractProcessInstanceId(response);
+    }
+
+    private Map<String, Object> toYidaFormData(ErpYidaPushRequest request, String operatorUserId) {
+        Map<String, Object> main = objectToMap(request.getMainData());
+        putIfNotBlank(main, "bm", request.getBm());
+        putIfNotBlank(main, "gs", request.getGs());
+        putIfNotBlank(main, "tjr", request.getTjr());
+        Map<String, Object> detailWrapper = objectToMap(request.getDetailData());
+        List<Map<String, Object>> detailRows = toMapRows(detailWrapper.get("detailData1"));
+        String docNo = toText(value(main, "inbadocno"));
+        String title = toText(firstNonBlank(value(main, "title", "dataTitle"), docNo == null ? null : "miscReceiptIssue-" + docNo));
+
+        Map<String, Object> formData = new LinkedHashMap<String, Object>();
+        putIfNotBlank(formData, TITLE_FIELD_ID, title);
+        putIfNotBlank(formData, APPLICANT_FIELD_ID, toStringList(operatorUserId, null));
+        putIfNotBlank(formData, COMPANY_FIELD_ID, firstNonBlank(value(main, "gs", "applyCompany", "company"), value(main, "inbaent")));
+        putIfNotBlank(formData, DEPARTMENT_FIELD_ID, toStringList(value(main, "bm", "applyDepartment", "department", "deptId"), null));
+        putIfNotBlank(formData, APPLY_DATE_FIELD_ID, toDateMillis(firstNonBlank(value(main, "rq", "applyDate"), value(main, "inbadocdt"))));
+        putIfNotBlank(formData, FORM_TITLE_FIELD_ID, title);
+        putIfNotBlank(formData, DATA_SOURCE_FIELD_ID, value(main, "inba006"));
+        putIfNotBlank(formData, APPLICANT_NO_FIELD_ID, value(main, "inba003"));
+        putIfNotBlank(formData, DEPARTMENT_NO_FIELD_ID, value(main, "inba004"));
+        putIfNotBlank(formData, REASON_CODE_FIELD_ID, value(main, "inba007"));
+        putIfNotBlank(formData, REASON_DESC_FIELD_ID, value(main, "oocql004"));
+        putIfNotBlank(formData, REMARK_FIELD_ID, value(main, "inba008"));
+
+        List<Map<String, Object>> rows = toDetailRows(detailRows);
+        if (!rows.isEmpty()) {
+            formData.put(DETAIL_TABLE_FIELD_ID, rows);
+        }
+        copyYidaFieldValues(main, formData);
+        return formData;
+    }
+
+    private List<Map<String, Object>> toDetailRows(List<Map<String, Object>> sourceRows) {
+        List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
+        for (Map<String, Object> source : sourceRows) {
+            Map<String, Object> row = new LinkedHashMap<String, Object>();
+            putIfNotBlank(row, DETAIL_SEQ_FIELD_ID, value(source, "inbbseq"));
+            putIfNotBlank(row, DETAIL_ORDER_NO_FIELD_ID, value(source, "inbbua001"));
+            putIfNotBlank(row, DETAIL_ORDER_SEQ_FIELD_ID, value(source, "inbbua002"));
+            putIfNotBlank(row, DETAIL_ITEM_CODE_FIELD_ID, value(source, "inbb001"));
+            putIfNotBlank(row, DETAIL_ITEM_NAME_FIELD_ID, value(source, "imaal003"));
+            putIfNotBlank(row, DETAIL_SPEC_FIELD_ID, value(source, "imaal004"));
+            putIfNotBlank(row, DETAIL_PRODUCT_FEATURE_FIELD_ID, value(source, "inbb002"));
+            putIfNotBlank(row, DETAIL_WAREHOUSE_FIELD_ID, value(source, "inbb007"));
+            putIfNotBlank(row, DETAIL_LOCATION_FIELD_ID, value(source, "inbb008"));
+            putIfNotBlank(row, DETAIL_LOT_NO_FIELD_ID, value(source, "inbb009"));
+            putIfNotBlank(row, DETAIL_STOCK_FEATURE_FIELD_ID, value(source, "inbb003"));
+            putIfNotBlank(row, DETAIL_QUANTITY_FIELD_ID, toDecimal(value(source, "inbb011")));
+            putIfNotBlank(row, DETAIL_UNIT_FIELD_ID, value(source, "inbb010"));
+            putIfNotBlank(row, DETAIL_REASON_CODE_FIELD_ID, value(source, "inbb016"));
+            putIfNotBlank(row, DETAIL_REASON_DESC_FIELD_ID, value(source, "oocql004"));
+            putIfNotBlank(row, DETAIL_PROJECT_NO_FIELD_ID, value(source, "inbb023"));
+            putIfNotBlank(row, DETAIL_PROJECT_DESC_FIELD_ID, value(source, "pjbal003"));
+            putIfNotBlank(row, DETAIL_SOURCE_DOC_NO_FIELD_ID, value(source, "inbb017"));
+            copyYidaFieldValues(source, row);
+            if (!row.isEmpty()) {
+                rows.add(row);
+            }
+        }
+        return rows;
+    }
+
+    private List<Map<String, Object>> toMapRows(Object value) {
+        List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
+        for (Object item : toList(value)) {
+            Map<String, Object> row = objectToMap(item);
+            if (!row.isEmpty()) {
+                rows.add(row);
+            }
+        }
+        return rows;
+    }
+
+    private List<?> toList(Object value) {
+        if (value instanceof List) {
+            return (List<?>) value;
+        }
+        if (value instanceof Iterable) {
+            List<Object> result = new ArrayList<Object>();
+            for (Object item : (Iterable<?>) value) {
+                result.add(item);
+            }
+            return result;
+        }
+        return value == null ? Collections.emptyList() : Collections.singletonList(value);
+    }
+
+    private List<String> toStringList(Object value, String fallback) {
+        Object actualValue = isBlank(value) ? fallback : value;
+        if (actualValue == null) {
+            return Collections.emptyList();
+        }
+        if (actualValue instanceof Iterable) {
+            List<String> result = new ArrayList<String>();
+            for (Object item : (Iterable<?>) actualValue) {
+                if (!isBlank(item)) {
+                    result.add(String.valueOf(item).trim());
+                }
+            }
+            return result;
+        }
+        return Collections.singletonList(String.valueOf(actualValue).trim());
+    }
+
+    private BigDecimal toDecimal(Object value) {
+        if (isBlank(value)) {
+            return null;
+        }
+        if (value instanceof BigDecimal) {
+            return (BigDecimal) value;
+        }
+        if (value instanceof Number) {
+            return new BigDecimal(String.valueOf(value));
+        }
+        return new BigDecimal(String.valueOf(value).trim());
+    }
+
+    private Long toDateMillis(Object value) {
+        if (isBlank(value)) {
+            return null;
+        }
+        String text = String.valueOf(value).trim();
+        if (text.matches("\\d+")) {
+            return Long.valueOf(text);
+        }
+        return LocalDate.parse(text).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+    }
+
+    private Object value(Map<String, Object> source, String firstKey, String... otherKeys) {
+        Object value = source.get(firstKey);
+        if (!isBlank(value)) {
+            return value;
+        }
+        for (String key : otherKeys) {
+            value = source.get(key);
+            if (!isBlank(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    private Object firstNonBlank(Object first, Object second) {
+        return isBlank(first) ? second : first;
+    }
+
+    private void putIfNotBlank(Map<String, Object> target, String fieldId, Object value) {
+        if (!isBlank(value)) {
+            target.put(fieldId, value);
+        }
+    }
+
+    private void copyYidaFieldValues(Map<String, Object> source, Map<String, Object> target) {
+        for (Map.Entry<String, Object> entry : source.entrySet()) {
+            if (entry.getKey() != null && entry.getKey().contains("Field_") && !isBlank(entry.getValue())) {
+                target.put(entry.getKey(), entry.getValue());
+            }
+        }
+    }
+
+    private Map<String, Object> objectToMap(Object value) {
+        if (value == null) {
+            return Collections.emptyMap();
+        }
+        if (value instanceof Map) {
+            return copyMap((Map<?, ?>) value);
+        }
+        if (value instanceof ErpYidaPushMainData) {
+            ErpYidaPushMainData mainData = (ErpYidaPushMainData) value;
+            Map<String, Object> result = new LinkedHashMap<String, Object>();
+            putIfNotBlank(result, "rq", mainData.getRq());
+            putIfNotBlank(result, "bm", mainData.getBm());
+            putIfNotBlank(result, "tjr", mainData.getTjr());
+            putIfNotBlank(result, "gs", mainData.getGs());
+            putIfNotBlank(result, "t100id", mainData.getT100id());
+            putIfNotBlank(result, "ent", mainData.getEnt());
+            putIfNotBlank(result, "t100bm", mainData.getT100bm());
+            result.putAll(mainData.getExtraFields());
+            return result;
+        }
+        return JSON.parseObject(JSON.toJSONString(value));
+    }
+
+    private Map<String, Object> copyMap(Map<?, ?> source) {
+        Map<String, Object> result = new LinkedHashMap<String, Object>();
+        for (Map.Entry<?, ?> entry : source.entrySet()) {
+            if (entry.getKey() != null) {
+                result.put(String.valueOf(entry.getKey()), entry.getValue());
+            }
+        }
+        return result;
+    }
+
+    private boolean isBlank(Object value) {
+        if (value == null) {
+            return true;
+        }
+        if (value instanceof Iterable) {
+            return !((Iterable<?>) value).iterator().hasNext();
+        }
+        if (value instanceof Map) {
+            return ((Map<?, ?>) value).isEmpty();
+        }
+        return String.valueOf(value).trim().isEmpty();
+    }
+
+    private String resolveOperatorUserId(ErpYidaPushRequest request) {
+        if (!isBlank(request.getOriginatorUserId())) {
+            return request.getOriginatorUserId().trim();
+        }
+        Map<String, Object> main = objectToMap(request.getMainData());
+        Object operator = firstNonBlank(value(main, "originatorUserId", "userId"), value(main, "applicant"));
+        return String.valueOf(isBlank(operator) ? DEFAULT_ORIGINATOR_USER_ID : firstItem(operator)).trim();
+    }
+
+    private Object firstItem(Object value) {
+        if (value instanceof Iterable) {
+            for (Object item : (Iterable<?>) value) {
+                if (!isBlank(item)) {
+                    return item;
+                }
+            }
+            return null;
+        }
+        return value;
+    }
+
+    private String toText(Object value) {
+        return isBlank(value) ? null : String.valueOf(value);
+    }
+
+    private Map<String, Object> maskSensitive(Map<String, Object> source) {
+        Map<String, Object> body = new LinkedHashMap<String, Object>(source);
+        if (body.containsKey("systemToken")) {
+            body.put("systemToken", "******");
+        }
+        return body;
+    }
+
+    private String extractProcessInstanceId(DDR_New response) {
+        if (response == null) {
+            throw new McException("YIDA_NULL_RESPONSE", "Yida process start failed: empty response");
+        }
+        LOGGER.info("Yida process start response formName={} success={} code={} result={} instanceId={} formInstId={}",
+                FORM_NAME, response.isSuccess(), response.getCode(), response.getResult(), response.getInstanceId(), response.getFormInstId());
+
+        Object result = firstNonBlank(firstNonBlank(response.getResult(), response.getInstanceId()), response.getFormInstId());
+        if (!isBlank(result)) {
+            return String.valueOf(result);
+        }
+        if (response.isSuccess()) {
+            return "SUCCESS";
+        }
+        String code = defaultText(response.getCode(), "YIDA_ERROR");
+        String message = defaultText(response.getMessage(), "Yida process start failed: no process instance id");
+        throw new McException(code, "Yida API [startProcess] failed: " + message);
+    }
+
+    private String defaultText(String value, String fallback) {
+        return value == null || value.trim().isEmpty() ? fallback : value.trim();
+    }
+
+    private String rootMessage(Throwable exception) {
+        Throwable current = exception;
+        while (current.getCause() != null && current.getCause() != current) {
+            current = current.getCause();
+        }
+        String message = current.getMessage();
+        if (message == null || message.trim().isEmpty()) {
+            message = exception.getMessage();
+        }
+        if (message == null || message.trim().isEmpty()) {
+            message = current.getClass().getSimpleName();
+        }
+        return message.trim();
+    }
+
+    private void validateConfiguration() {
+        require(dingTalkConfiguration.getAppKey(), "DINGTALK_APP_KEY is not configured");
+        require(dingTalkConfiguration.getAppSecret(), "DINGTALK_APP_SECRET is not configured");
+        require(applications.getCoordinationOffice().getAppType(), "YIDA_COORDINATION_OFFICE_APP_TYPE is not configured");
+        require(applications.getCoordinationOffice().getSystemToken(), "YIDA_COORDINATION_OFFICE_SYSTEM_TOKEN is not configured");
+    }
+
+    private String requireText(String value, String message) {
+        require(value, message);
+        return value.trim();
+    }
+
+    private void require(String value, String message) {
+        if (value == null || value.trim().isEmpty()) {
+            throw new IllegalStateException(message);
+        }
+    }
+}

+ 14 - 6
mjava-minglei/src/main/java/com/malk/minglei/service/impl/yida/MlWorkOrderCraftPriceFormPushHandler.java

@@ -17,7 +17,6 @@ import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.ZoneId;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -31,9 +30,9 @@ public class MlWorkOrderCraftPriceFormPushHandler implements YidaFormPushHandler
 
     private static final Logger LOGGER = LoggerFactory.getLogger(MlWorkOrderCraftPriceFormPushHandler.class);
     private static final String START_PROCESS_ENDPOINT = "/v1.0/yida/processes/instances/start";
-    public static final String TYPE = "ml_work_order_craft_price";
     private static final String FORM_NAME = "ML-工单工艺工价审定";
     private static final String FORM_UUID = "FORM-4403B45A7F184CB79088FCDAFD2FD925LZ3K";
+    public static final String TYPE = FORM_UUID;
     private static final String PROCESS_CODE = "TPROC--WG966BA1VWF8Q1K4OGF8H9KW47RR3ZREH0YSM2";
     private static final String DEFAULT_ORIGINATOR_USER_ID = "41603502201288023";
 
@@ -86,7 +85,7 @@ public class MlWorkOrderCraftPriceFormPushHandler implements YidaFormPushHandler
 
     @Override
     public List<String> getTypes() {
-        return Arrays.asList(TYPE, "work_order_craft_price", FORM_NAME, FORM_UUID);
+        return Collections.singletonList(TYPE);
     }
 
     @Override
@@ -340,10 +339,19 @@ public class MlWorkOrderCraftPriceFormPushHandler implements YidaFormPushHandler
             return Collections.emptyMap();
         }
         if (value instanceof Map) {
-            Map result = (Map) value;
-            return result;
+            return copyMap((Map<?, ?>) value);
+        }
+        return JSON.parseObject(JSON.toJSONString(value));
+    }
+
+    private Map<String, Object> copyMap(Map<?, ?> source) {
+        Map<String, Object> result = new LinkedHashMap<String, Object>();
+        for (Map.Entry<?, ?> entry : source.entrySet()) {
+            if (entry.getKey() != null) {
+                result.put(String.valueOf(entry.getKey()), entry.getValue());
+            }
         }
-        return JSON.parseObject(JSON.toJSONString(value), Map.class);
+        return result;
     }
 
     private Map<String, Object> toDetailRecord(Object detailData) {

+ 11 - 9
mjava-minglei/src/test/java/com/malk/minglei/service/impl/employee/EmployeeRosterServiceImplTest.java

@@ -48,6 +48,7 @@ class EmployeeRosterServiceImplTest {
     }
 
     @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
     void syncCollectsDepartmentUsersAndUpsertsRosterRecords() {
         DDClient dingTalkClient = mock(DDClient.class);
         DDClient_Contacts contactsClient = mock(DDClient_Contacts.class);
@@ -109,6 +110,7 @@ class EmployeeRosterServiceImplTest {
     }
 
     @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
     void syncKeepsJobTitleAndJobGradeNullWhenRosterFieldsAreMissing() {
         DDClient dingTalkClient = mock(DDClient.class);
         DDClient_Contacts contactsClient = mock(DDClient_Contacts.class);
@@ -151,7 +153,7 @@ class EmployeeRosterServiceImplTest {
         assertNull(records.get(0).getJobGradeId());
     }
 
-    private static Map user(String userId, String name, String jobNumber) {
+    private static Map<String, Object> user(String userId, String name, String jobNumber) {
         Map<String, Object> user = new LinkedHashMap<String, Object>();
         user.put("userid", userId);
         user.put("name", name);
@@ -160,32 +162,32 @@ class EmployeeRosterServiceImplTest {
         return user;
     }
 
-    private static Map userWithTitle(String userId, String name, String jobNumber, String title) {
-        Map user = user(userId, name, jobNumber);
+    private static Map<String, Object> userWithTitle(String userId, String name, String jobNumber, String title) {
+        Map<String, Object> user = user(userId, name, jobNumber);
         user.put("title", title);
         return user;
     }
 
-    private static Map mapOf(String key, Object value) {
+    private static Map<String, Object> mapOf(String key, Object value) {
         Map<String, Object> map = new LinkedHashMap<String, Object>();
         map.put(key, value);
         return map;
     }
 
-    private static Map mapOf(String key1, Object value1, String key2, Object value2) {
+    private static Map<String, Object> mapOf(String key1, Object value1, String key2, Object value2) {
         Map<String, Object> map = mapOf(key1, value1);
         map.put(key2, value2);
         return map;
     }
 
-    private static Map field(String fieldCode, String fieldName) {
+    private static Map<String, Object> field(String fieldCode, String fieldName) {
         Map<String, Object> field = new LinkedHashMap<String, Object>();
         field.put("field_code", fieldCode);
         field.put("field_name", fieldName);
         return field;
     }
 
-    private static Map roster(String userId, String jobTitle, String jobGrade, String department, String firstDepartment) {
+    private static Map<String, Object> roster(String userId, String jobTitle, String jobGrade, String department, String firstDepartment) {
         return mapOf("userid", userId, "field_data_list", Arrays.asList(
                 rosterField("sys00-jobTitle", jobTitle, "10040"),
                 rosterField("sys00-jobGrade", jobGrade, "10010"),
@@ -193,11 +195,11 @@ class EmployeeRosterServiceImplTest {
                 rosterField("sys00-firstDepartment", firstDepartment)));
     }
 
-    private static Map rosterField(String fieldCode, String value) {
+    private static Map<String, Object> rosterField(String fieldCode, String value) {
         return rosterField(fieldCode, value, value);
     }
 
-    private static Map rosterField(String fieldCode, String label, String value) {
+    private static Map<String, Object> rosterField(String fieldCode, String label, String value) {
         Map<String, Object> field = new LinkedHashMap<String, Object>();
         field.put("field_code", fieldCode);
         field.put("field_value_list", Collections.singletonList(mapOf("label", label, "value", value)));

+ 43 - 28
mjava-minglei/src/test/java/com/malk/minglei/service/impl/yida/ApproveOrderFormPushHandlerTest.java

@@ -23,6 +23,7 @@ import static org.mockito.Mockito.when;
 class ApproveOrderFormPushHandlerTest {
 
     @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
     void pushMapsCustomerApproveOrderPayloadToYidaFieldIds() {
         YidaProcessOpenApiClient yidaClient = mock(YidaProcessOpenApiClient.class);
         DDR_New response = new DDR_New();
@@ -51,26 +52,42 @@ class ApproveOrderFormPushHandlerTest {
         mainData.setExtraField("xmda042", "1234.56");
         mainData.setExtraField("xmda071", "remark-001");
 
-        Map<String, Object> detailRow = new LinkedHashMap<String, Object>();
-        detailRow.put("xmdcent", "87");
-        detailRow.put("xmdcdocno", "SO-001");
-        detailRow.put("xmdcseq", "10");
-        detailRow.put("xmdc001", "ITEM-001");
-        detailRow.put("xmdc002", "FEATURE-001");
-        detailRow.put("imaal003", "item-name");
-        detailRow.put("imaal004", "spec-001");
-        detailRow.put("xmdc006", "PCS");
-        detailRow.put("xmdc007", "100");
-        detailRow.put("xmdc012", "2026-09-30");
-        detailRow.put("xmdc015", "12.3");
-        detailRow.put("xmdc019", "child-feature");
-        detailRow.put("xmdc046", "1000");
-        detailRow.put("xmdc047", "1130");
-        detailRow.put("xmdc050", "normal-delivery");
-        detailRow.put("xmdcua003", "2026-09-20");
+        Map<String, Object> deliverySourceRow = new LinkedHashMap<String, Object>();
+        deliverySourceRow.put("xmdcent", "87");
+        deliverySourceRow.put("xmdcdocno", "SO-001");
+        deliverySourceRow.put("xmdcseq", "10");
+        deliverySourceRow.put("xmdc001", "ITEM-001");
+        deliverySourceRow.put("xmdc002", "FEATURE-001");
+        deliverySourceRow.put("imaal003", "item-name");
+        deliverySourceRow.put("imaal004", "spec-001");
+        deliverySourceRow.put("xmdc006", "PCS");
+        deliverySourceRow.put("xmdc007", "100");
+        deliverySourceRow.put("xmdc012", "2026-09-30");
+        deliverySourceRow.put("xmdc015", "12.3");
+        deliverySourceRow.put("xmdc019", "child-feature");
+        deliverySourceRow.put("xmdc046", "1000");
+        deliverySourceRow.put("xmdc047", "1130");
+        deliverySourceRow.put("xmdc050", "normal-delivery");
+        deliverySourceRow.put("xmdcua003", "2026-09-20");
+
+        Map<String, Object> orderSourceRow = new LinkedHashMap<String, Object>();
+        orderSourceRow.put("xmddent", "87");
+        orderSourceRow.put("xmdddocno", "SO-001");
+        orderSourceRow.put("xmddseq", "10");
+        orderSourceRow.put("xmddseq1", "11");
+        orderSourceRow.put("xmddseq2", "12");
+        orderSourceRow.put("xmdd001", "ITEM-002");
+        orderSourceRow.put("xmdd002", "FEATURE-002");
+        orderSourceRow.put("xmdd004", "BOX");
+        orderSourceRow.put("xmdd005", "200");
+        orderSourceRow.put("xmdd006", "80");
+        orderSourceRow.put("xmdd009", "delivery-type-2");
+        orderSourceRow.put("xmdd011", "2026-10-01");
+        orderSourceRow.put("xmdd014", "20");
 
         Map<String, Object> detailData = new LinkedHashMap<String, Object>();
-        detailData.put("detailData", Collections.singletonList(detailRow));
+        detailData.put("detailData1", Collections.singletonList(deliverySourceRow));
+        detailData.put("detailData2", Collections.singletonList(orderSourceRow));
 
         ErpYidaPushRequest request = new ErpYidaPushRequest();
         request.setType(ApproveOrderFormPushHandler.TYPE);
@@ -136,15 +153,13 @@ class ApproveOrderFormPushHandlerTest {
         assertEquals("87", orderRow.getString("textField_8yaaaieif"));
         assertEquals("SO-001", orderRow.getString("textField_8yabb4o9b"));
         assertEquals("10", orderRow.getString("numberField_mt82p0af"));
-        assertEquals("10", orderRow.getString("numberField_mt82p0ah"));
-        assertEquals("10", orderRow.getString("numberField_mt82p0ai"));
-        assertEquals("ITEM-001", orderRow.getString("textField_8yabff0mk"));
-        assertEquals("item-name", orderRow.getString("textField_8yabgv4yp"));
-        assertEquals("spec-001", orderRow.getString("textField_8yabhgci5"));
-        assertEquals("FEATURE-001", orderRow.getString("textField_8yabis8xh"));
-        assertEquals("PCS", orderRow.getString("textField_8yabjecf7"));
-        assertEquals("normal-delivery", orderRow.getString("textField_mt82p0aj"));
-        assertEquals("100", orderRow.getString("textField_8yabl1tlp"));
-        assertEquals(Long.valueOf(1790697600000L), orderRow.getLong("dateField_8yabm8rmr"));
+        assertEquals("11", orderRow.getString("numberField_mt82p0ah"));
+        assertEquals("12", orderRow.getString("numberField_mt82p0ai"));
+        assertEquals("ITEM-002", orderRow.getString("textField_8yabff0mk"));
+        assertEquals("FEATURE-002", orderRow.getString("textField_8yabis8xh"));
+        assertEquals("BOX", orderRow.getString("textField_8yabjecf7"));
+        assertEquals("delivery-type-2", orderRow.getString("textField_mt82p0aj"));
+        assertEquals("200", orderRow.getString("textField_8yabl1tlp"));
+        assertEquals(Long.valueOf(1790784000000L), orderRow.getLong("dateField_8yabm8rmr"));
     }
 }

+ 115 - 0
mjava-minglei/src/test/java/com/malk/minglei/service/impl/yida/CustomerReleaseFormPushHandlerTest.java

@@ -0,0 +1,115 @@
+package com.malk.minglei.service.impl.yida;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.malk.minglei.config.YidaApplicationsProperties;
+import com.malk.minglei.dto.ErpYidaPushMainData;
+import com.malk.minglei.dto.ErpYidaPushRequest;
+import com.malk.minglei.service.yida.YidaProcessOpenApiClient;
+import com.malk.server.dingtalk.DDR_New;
+import com.malk.server.dingtalk.DDConf;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class CustomerReleaseFormPushHandlerTest {
+
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void pushMapsCustomerReleasePayloadToYidaFieldIds() {
+        YidaProcessOpenApiClient yidaClient = mock(YidaProcessOpenApiClient.class);
+        DDR_New response = new DDR_New();
+        response.setResult("process-instance-1");
+        when(yidaClient.startProcess(org.mockito.ArgumentMatchers.anyMap())).thenReturn(response);
+
+        DDConf ddConf = new DDConf();
+        ddConf.setAppKey("app-key");
+        ddConf.setAppSecret("app-secret");
+
+        YidaApplicationsProperties applications = new YidaApplicationsProperties();
+        applications.getCoordinationOffice().setAppType("APP_RJJL69QUIZVQSV7YC8TL");
+        applications.getCoordinationOffice().setSystemToken("system-token");
+
+        ErpYidaPushMainData mainData = new ErpYidaPushMainData();
+        mainData.setExtraField("xmfxent", "87");
+        mainData.setExtraField("xmfxdocno", "CR-001");
+        mainData.setExtraField("xmfxdocdt", "2026-09-15");
+        mainData.setExtraField("xmfx001", "applicant-001");
+        mainData.setExtraField("xmfx002", "dept-code-001");
+        mainData.setExtraField("xmfx003", "release-desc");
+        mainData.setExtraField("xmfxud001", "cannot-deliver-reason");
+
+        Map<String, Object> detailRow = new LinkedHashMap<String, Object>();
+        detailRow.put("xmfyent", "87");
+        detailRow.put("xmfydocno", "CR-001");
+        detailRow.put("xmfyseq", "10");
+        detailRow.put("xmfy001", "DN");
+        detailRow.put("xmfy002", "DN-001");
+        detailRow.put("xmfy003", "2026-09-16");
+        detailRow.put("xmfy004", "CUS-001");
+        detailRow.put("pmaal004", "customer-name");
+        detailRow.put("xmfy005", "1000.5");
+        detailRow.put("xmfy006", "2000.5");
+        detailRow.put("xmfy007", "short-remark");
+        detailRow.put("xmfy008", "3000.5");
+
+        Map<String, Object> detailData = new LinkedHashMap<String, Object>();
+        detailData.put("detailData1", Collections.singletonList(detailRow));
+
+        ErpYidaPushRequest request = new ErpYidaPushRequest();
+        request.setType(CustomerReleaseFormPushHandler.TYPE);
+        request.setBm("dept-001");
+        request.setGs("ML");
+        request.setTjr("user-001");
+        request.setMainData(mainData);
+        request.setDetailData(detailData);
+
+        CustomerReleaseFormPushHandler handler = new CustomerReleaseFormPushHandler(yidaClient, ddConf, applications);
+
+        assertEquals(Collections.singletonList("axmt140"), handler.getTypes());
+        assertEquals("process-instance-1", handler.push(request));
+
+        ArgumentCaptor<Map> bodyCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(yidaClient).startProcess(bodyCaptor.capture());
+        Map body = bodyCaptor.getValue();
+        assertEquals("APP_RJJL69QUIZVQSV7YC8TL", body.get("appType"));
+        assertEquals("system-token", body.get("systemToken"));
+        assertEquals("41603502201288023", body.get("userId"));
+        assertEquals("TPROC--6TG66381N8G8Y7L9GBEEY7QVXTUY2LBBGZ7TMU", body.get("processCode"));
+        assertEquals("FORM-3BA9FE18494F4FDD85FE6ABF7DE741BAE30C", body.get("formUuid"));
+        assertEquals("customerRelease-CR-001", body.get("title"));
+        assertEquals("customerRelease-CR-001", body.get("dataTitle"));
+
+        JSONObject formData = JSONObject.parseObject(String.valueOf(body.get("formDataJson")));
+        assertEquals("customerRelease-CR-001", formData.getString("textField_mtv1tgg1"));
+        assertEquals("customerRelease-CR-001", formData.getString("textField_mspcpz92"));
+        assertEquals("41603502201288023", formData.getJSONArray("employeeField_msh186rr").getString(0));
+        assertEquals("ML", formData.getString("selectField_mtv7uxgd"));
+        assertEquals("dept-001", formData.getJSONArray("departmentSelectField_msh186ry").getString(0));
+        assertEquals(Long.valueOf(1789401600000L), formData.getLong("dateField_msh186rs"));
+        assertEquals("CR-001", formData.getString("textField_mt7zi4fy"));
+        assertEquals("release-desc", formData.getString("textField_mt7zi4g0"));
+        assertEquals("cannot-deliver-reason", formData.getString("textField_mt7zi4fz"));
+
+        JSONArray detailRows = formData.getJSONArray("tableField_msh186sv");
+        assertEquals(1, detailRows.size());
+        JSONObject row = detailRows.getJSONObject(0);
+        assertEquals("10", row.getString("textField_mt7zi4g1"));
+        assertEquals("DN-001", row.getString("textField_mt7zi4g2"));
+        assertEquals(Long.valueOf(1789488000000L), row.getLong("dateField_mt7zi4g3"));
+        assertEquals("CUS-001", row.getString("textField_mt6ur4w1"));
+        assertEquals("customer-name", row.getString("textField_mt7zi4g4"));
+        assertEquals("1000.5", row.getString("numberField_mt7zi4g5"));
+        assertEquals("2000.5", row.getString("numberField_mt7zi4g6"));
+        assertEquals("3000.5", row.getString("numberField_mt7zi4g7"));
+        assertEquals("short-remark", row.getString("textField_mt6ur4w8"));
+    }
+}

+ 55 - 5
mjava-minglei/src/test/java/com/malk/minglei/service/impl/yida/ErpYidaPushServiceImplTest.java

@@ -15,6 +15,7 @@ import org.slf4j.Logger;
 import java.util.Collections;
 import java.util.Arrays;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -65,9 +66,9 @@ class ErpYidaPushServiceImplTest {
         assertSame(mainData, yidaRequest.getFields().get("mainData"));
         assertSame(request.getDetailData(), yidaRequest.getFields().get("detailData"));
         assertEquals("dingtalk-user-001", request.getOriginatorUserId());
-        Map<String, Object> pushedDetailData = (Map<String, Object>) yidaRequest.getFields().get("detailData");
+        Map<String, Object> pushedDetailData = mapValue(yidaRequest.getFields().get("detailData"));
         assertEquals(Collections.singleton("detailData"), pushedDetailData.keySet());
-        Map<String, Object> pushedDetailRow = (Map<String, Object>) ((java.util.List<?>) pushedDetailData.get("detailData")).get(0);
+        Map<String, Object> pushedDetailRow = firstRow(pushedDetailData, "detailData");
         assertEquals("dingtalk-user-001", pushedDetailRow.get("originatorUserId"));
         assertEquals("2026-09-14", pushedDetailRow.get("applyDate"));
         assertEquals(Collections.singletonList("123456"), pushedDetailRow.get("applyDepartment"));
@@ -104,7 +105,7 @@ class ErpYidaPushServiceImplTest {
         assertNotNull(result);
         verify(formPushHandler, times(1)).push(request);
         assertEquals("dingtalk-user-001", request.getOriginatorUserId());
-        Map<String, Object> pushedDetailData = (Map<String, Object>) request.getDetailData();
+        Map<String, Object> pushedDetailData = mapValue(request.getDetailData());
         assertEquals(Collections.singleton("detailData"), pushedDetailData.keySet());
         verify(yidaProcessService, never()).startProcess(any(YidaProcessStartRequest.class));
         verify(logger).info(org.mockito.ArgumentMatchers.startsWith("ERP Yida push dispatched formInstanceId: {}"),
@@ -147,8 +148,48 @@ class ErpYidaPushServiceImplTest {
         assertEquals("top-dept-001", request.getMainData().getBm());
         assertEquals("top-company-001", request.getMainData().getGs());
         assertEquals("dingtalk-top-user-001", request.getOriginatorUserId());
-        Map<String, Object> pushedDetailData = (Map<String, Object>) request.getDetailData();
-        Map<String, Object> pushedDetailRow = (Map<String, Object>) ((java.util.List<?>) pushedDetailData.get("detailData")).get(0);
+        Map<String, Object> pushedDetailData = mapValue(request.getDetailData());
+        Map<String, Object> pushedDetailRow = firstRow(pushedDetailData, "detailData");
+        assertEquals("dingtalk-top-user-001", pushedDetailRow.get("originatorUserId"));
+        assertEquals(Collections.singletonList("top-dept-001"), pushedDetailRow.get("applyDepartment"));
+        assertEquals("top-company-001", pushedDetailRow.get("gs"));
+        verify(formPushHandler, times(1)).push(request);
+        verify(yidaProcessService, never()).startProcess(any(YidaProcessStartRequest.class));
+    }
+
+    @Test
+    void acceptsDetailDataOneForMatchingFormPushHandler() {
+        Logger logger = mock(Logger.class);
+        YidaProcessService yidaProcessService = mock(YidaProcessService.class);
+        EmployeeRosterService employeeRosterService = mock(EmployeeRosterService.class);
+        YidaFormPushHandler formPushHandler = mock(YidaFormPushHandler.class);
+        when(formPushHandler.getTypes()).thenReturn(Collections.singletonList(CustomerReleaseFormPushHandler.TYPE));
+        when(employeeRosterService.findByEmployeeNo("top-user-001")).thenReturn(employee("dingtalk-top-user-001"));
+
+        ErpYidaPushRequest request = new ErpYidaPushRequest();
+        request.setType(CustomerReleaseFormPushHandler.TYPE);
+        request.setBm("top-dept-001");
+        request.setGs("top-company-001");
+        request.setTjr("top-user-001");
+        request.setMainData(new ErpYidaPushMainData());
+        Map<String, Object> detailRow = new LinkedHashMap<String, Object>();
+        detailRow.put("xmfydocno", "CR-001");
+        Map<String, Object> detailData = new LinkedHashMap<String, Object>();
+        detailData.put("detailData1", Collections.singletonList(detailRow));
+        request.setDetailData(detailData);
+        when(formPushHandler.push(request)).thenReturn("form-instance-1");
+
+        ErpYidaPushServiceImpl service = new ErpYidaPushServiceImpl(
+                logger,
+                yidaProcessService,
+                Arrays.asList(formPushHandler),
+                employeeRosterService);
+
+        McR result = service.push(request);
+
+        assertNotNull(result);
+        Map<String, Object> pushedDetailData = mapValue(request.getDetailData());
+        Map<String, Object> pushedDetailRow = firstRow(pushedDetailData, "detailData1");
         assertEquals("dingtalk-top-user-001", pushedDetailRow.get("originatorUserId"));
         assertEquals(Collections.singletonList("top-dept-001"), pushedDetailRow.get("applyDepartment"));
         assertEquals("top-company-001", pushedDetailRow.get("gs"));
@@ -192,4 +233,13 @@ class ErpYidaPushServiceImplTest {
         request.setDetailData(detailData);
         return request;
     }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, Object> mapValue(Object value) {
+        return (Map<String, Object>) value;
+    }
+
+    private Map<String, Object> firstRow(Map<String, Object> detailData, String key) {
+        return mapValue(((List<?>) detailData.get(key)).get(0));
+    }
 }

+ 136 - 0
mjava-minglei/src/test/java/com/malk/minglei/service/impl/yida/MiscReceiptIssueFormPushHandlerTest.java

@@ -0,0 +1,136 @@
+package com.malk.minglei.service.impl.yida;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.malk.minglei.config.YidaApplicationsProperties;
+import com.malk.minglei.dto.ErpYidaPushMainData;
+import com.malk.minglei.dto.ErpYidaPushRequest;
+import com.malk.minglei.service.yida.YidaProcessOpenApiClient;
+import com.malk.server.dingtalk.DDR_New;
+import com.malk.server.dingtalk.DDConf;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class MiscReceiptIssueFormPushHandlerTest {
+
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void pushMapsCustomerMiscReceiptIssuePayloadToYidaFieldIds() {
+        YidaProcessOpenApiClient yidaClient = mock(YidaProcessOpenApiClient.class);
+        DDR_New response = new DDR_New();
+        response.setResult("process-instance-1");
+        when(yidaClient.startProcess(org.mockito.ArgumentMatchers.anyMap())).thenReturn(response);
+
+        DDConf ddConf = new DDConf();
+        ddConf.setAppKey("app-key");
+        ddConf.setAppSecret("app-secret");
+
+        YidaApplicationsProperties applications = new YidaApplicationsProperties();
+        applications.getCoordinationOffice().setAppType("APP_RJJL69QUIZVQSV7YC8TL");
+        applications.getCoordinationOffice().setSystemToken("system-token");
+
+        ErpYidaPushMainData mainData = new ErpYidaPushMainData();
+        mainData.setExtraField("inbaent", "87");
+        mainData.setExtraField("inbadocno", "MS-001");
+        mainData.setExtraField("inbadocdt", "2026-09-15");
+        mainData.setExtraField("inba003", "applicant-001");
+        mainData.setExtraField("inba004", "dept-code-001");
+        mainData.setExtraField("inba007", "RSN-001");
+        mainData.setExtraField("oocql004", "reason-desc-main");
+        mainData.setExtraField("inba008", "remark-001");
+        mainData.setExtraField("inba006", "ERP");
+
+        Map<String, Object> detailRow = new LinkedHashMap<String, Object>();
+        detailRow.put("inbbent", "87");
+        detailRow.put("inbbdocno", "MS-001");
+        detailRow.put("inbbseq", "10");
+        detailRow.put("inbbua001", "SO-001");
+        detailRow.put("inbbua002", "20");
+        detailRow.put("inbb001", "ITEM-001");
+        detailRow.put("imaal003", "item-name");
+        detailRow.put("imaal004", "spec-001");
+        detailRow.put("inbb002", "feature-001");
+        detailRow.put("inbb007", "WH-001");
+        detailRow.put("inbb008", "LOC-001");
+        detailRow.put("inbb009", "LOT-001");
+        detailRow.put("inbb003", "stock-feature-001");
+        detailRow.put("inbb011", "12.5");
+        detailRow.put("inbb010", "PCS");
+        detailRow.put("inbb016", "DRSN-001");
+        detailRow.put("oocql004", "reason-desc-detail");
+        detailRow.put("inbb023", "PROJ-001");
+        detailRow.put("pjbal003", "project-desc");
+        detailRow.put("inbb017", "SRC-001");
+
+        Map<String, Object> detailData = new LinkedHashMap<String, Object>();
+        detailData.put("detailData1", Collections.singletonList(detailRow));
+
+        ErpYidaPushRequest request = new ErpYidaPushRequest();
+        request.setType(MiscReceiptIssueFormPushHandler.TYPE);
+        request.setBm("dept-001");
+        request.setGs("ML");
+        request.setTjr("user-001");
+        request.setMainData(mainData);
+        request.setDetailData(detailData);
+
+        MiscReceiptIssueFormPushHandler handler = new MiscReceiptIssueFormPushHandler(yidaClient, ddConf, applications);
+
+        assertEquals("process-instance-1", handler.push(request));
+
+        ArgumentCaptor<Map> bodyCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(yidaClient).startProcess(bodyCaptor.capture());
+        Map body = bodyCaptor.getValue();
+        assertEquals("APP_RJJL69QUIZVQSV7YC8TL", body.get("appType"));
+        assertEquals("system-token", body.get("systemToken"));
+        assertEquals("41603502201288023", body.get("userId"));
+        assertEquals("TPROC--MQD66371C1N84FPSJZO4W79IN4BW2AE7I08TM0", body.get("processCode"));
+        assertEquals("FORM-4B08ADABCDAA45788C043729A855E03ED76G", body.get("formUuid"));
+        assertEquals("miscReceiptIssue-MS-001", body.get("title"));
+        assertEquals("miscReceiptIssue-MS-001", body.get("dataTitle"));
+
+        JSONObject formData = JSONObject.parseObject(String.valueOf(body.get("formDataJson")));
+        assertEquals("miscReceiptIssue-MS-001", formData.getString("textField_msh27unu"));
+        assertEquals("miscReceiptIssue-MS-001", formData.getString("textField_mspcpz92"));
+        assertEquals("41603502201288023", formData.getJSONArray("employeeField_msh186rr").getString(0));
+        assertEquals("ML", formData.getString("selectField_mso1em48"));
+        assertEquals("dept-001", formData.getJSONArray("departmentSelectField_msh186ry").getString(0));
+        assertEquals(Long.valueOf(1789401600000L), formData.getLong("dateField_msh186rs"));
+        assertEquals("ERP", formData.getString("textField_mt7zi4fy"));
+        assertEquals("applicant-001", formData.getString("textField_mt7zi4g0"));
+        assertEquals("dept-code-001", formData.getString("textField_mt80nq6s"));
+        assertEquals("RSN-001", formData.getString("textField_mt80nq6t"));
+        assertEquals("reason-desc-main", formData.getString("textField_mt80nq6v"));
+        assertEquals("remark-001", formData.getString("textField_mt7zi4fz"));
+
+        JSONArray detailRows = formData.getJSONArray("tableField_msh186sv");
+        assertEquals(1, detailRows.size());
+        JSONObject row = detailRows.getJSONObject(0);
+        assertEquals("10", row.getString("textField_mt7zi4g1"));
+        assertEquals("SO-001", row.getString("textField_mt80nq6y"));
+        assertEquals("20", row.getString("textField_mt80nq6z"));
+        assertEquals("ITEM-001", row.getString("textField_mt80nq70"));
+        assertEquals("item-name", row.getString("textField_mt80nq71"));
+        assertEquals("spec-001", row.getString("textField_mt80nq72"));
+        assertEquals("feature-001", row.getString("textField_mt80nq73"));
+        assertEquals("WH-001", row.getString("textField_mt80nq74"));
+        assertEquals("LOC-001", row.getString("textField_mt80nq75"));
+        assertEquals("LOT-001", row.getString("textField_mt7zi4g2"));
+        assertEquals("stock-feature-001", row.getString("textField_mt80nq76"));
+        assertEquals("12.5", row.getString("numberField_mt80nq77"));
+        assertEquals("PCS", row.getString("textField_mt6ur4w1"));
+        assertEquals("DRSN-001", row.getString("textField_mt7zi4g4"));
+        assertEquals("reason-desc-detail", row.getString("textField_mt80nq79"));
+        assertEquals("PROJ-001", row.getString("textField_mt6ur4w8"));
+        assertEquals("project-desc", row.getString("textField_mt80nq7a"));
+        assertEquals("SRC-001", row.getString("textField_mt80nq7b"));
+    }
+}

+ 2 - 0
mjava-minglei/src/test/java/com/malk/minglei/service/impl/yida/MlWorkOrderCraftPriceFormPushHandlerTest.java

@@ -26,6 +26,7 @@ import static org.mockito.Mockito.when;
 class MlWorkOrderCraftPriceFormPushHandlerTest {
 
     @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
     void pushMapsPayloadToWorkOrderCraftPriceFormFieldIds() {
         YidaProcessOpenApiClient yidaClient = mock(YidaProcessOpenApiClient.class);
         DDR_New response = new DDR_New();
@@ -89,6 +90,7 @@ class MlWorkOrderCraftPriceFormPushHandlerTest {
 
         MlWorkOrderCraftPriceFormPushHandler handler = new MlWorkOrderCraftPriceFormPushHandler(yidaClient, ddConf, applications);
 
+        assertEquals(Collections.singletonList("FORM-4403B45A7F184CB79088FCDAFD2FD925LZ3K"), handler.getTypes());
         assertEquals("process-instance-1", handler.push(request));
 
         ArgumentCaptor<Map> bodyCaptor = ArgumentCaptor.forClass(Map.class);