ht hace 4 días
padre
commit
9a0c9a2bb5

+ 27 - 0
src/main/java/com/malk/minglei/config/WebCorsConfiguration.java

@@ -0,0 +1,27 @@
+package com.malk.minglei.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+import java.util.Arrays;
+
+@Configuration
+public class WebCorsConfiguration {
+
+    @Bean
+    public CorsFilter corsFilter() {
+        CorsConfiguration configuration = new CorsConfiguration();
+        configuration.setAllowedOrigins(Arrays.asList("*"));
+        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
+        configuration.setAllowedHeaders(Arrays.asList("*"));
+        configuration.setAllowCredentials(false);
+        configuration.setMaxAge(Long.valueOf(3600L));
+
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        source.registerCorsConfiguration("/api/**", configuration);
+        return new CorsFilter(source);
+    }
+}

+ 255 - 0
src/main/java/com/malk/minglei/service/impl/yida/Ampt100FormPushHandler.java

@@ -0,0 +1,255 @@
+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.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class Ampt100FormPushHandler implements YidaFormPushHandler {
+
+    public static final String TYPE = "ampt100";
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(Ampt100FormPushHandler.class);
+    private static final String FORM_NAME = "\u4f9b\u5e94\u5546\u5bfc\u5165\u6d41\u7a0b";
+    private static final String FORM_UUID = "FORM-9EE14CA14D8D4AEE8102944E669E6B0CFUKS";
+    private static final String PROCESS_CODE = "TPROC--75G66PD15YG82766OFSWD8BS4ZJ33YPRFOZSM2";
+
+    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 = "textField_mu1zpoe4";
+    private static final String COMPANY_SELECT_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 APPLY_TYPE_FIELD_ID = "textField_mszogjgu";
+    private static final String SUPPLIER_NAME_FIELD_ID = "textField_mszogjgv";
+    private static final String SUPPLIER_CODE_FIELD_ID = "textField_mszogjgw";
+    private static final String TAX_NUMBER_FIELD_ID = "textField_mszogjgx";
+    private static final String REGISTERED_CAPITAL_FIELD_ID = "textField_mszogjgy";
+    private static final String ESTABLISH_DATE_FIELD_ID = "dateField_mszogjgz";
+    private static final String ADDRESS_FIELD_ID = "textField_mszogjh0";
+    private static final String CONTACT_FIELD_ID = "textField_mszogjh4";
+    private static final String CONTACT_PHONE_FIELD_ID = "textField_mszogjh5";
+    private static final String MAIN_PRODUCTS_FIELD_ID = "textField_mszogjh6";
+    private static final String FACTORY_SCALE_FIELD_ID = "textField_mszogjh7";
+    private static final String LIFE_CYCLE_FIELD_ID = "textField_mszogjj9";
+
+    private static final Map<String, String> ERP_FIELD_MAPPING = createErpFieldMapping();
+
+    private final YidaProcessOpenApiClient yidaProcessOpenApiClient;
+    private final DDConf dingTalkConfiguration;
+    private final YidaApplicationsProperties applications;
+
+    public Ampt100FormPushHandler(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();
+
+        Map<String, Object> mainData = toMainDataMap(request.getMainData());
+        String operatorUserId = requireText(request.getOriginatorUserId(), "ampt100 requires mainData.tjr");
+        String title = requireText(toText(firstNonBlank(mainData.get("lcbh"), mainData.get("docno"))),
+                "ampt100 requires mainData.lcbh or mainData.docno");
+        Map<String, Object> formData = toYidaFormData(mainData, operatorUserId, title);
+
+        Map<String, Object> body = new LinkedHashMap<String, Object>();
+        body.put("appType", requireText(applications.getCoordinationOffice().getAppType(), "YIDA_COORDINATION_OFFICE_APP_TYPE is not configured"));
+        body.put("systemToken", requireText(applications.getCoordinationOffice().getSystemToken(), "YIDA_COORDINATION_OFFICE_SYSTEM_TOKEN is not configured"));
+        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", FORM_NAME + "-" + title);
+        body.put("dataTitle", FORM_NAME + "-" + title);
+
+        LOGGER.info("Yida process start request formName={} body={}", FORM_NAME, JSON.toJSONString(maskSensitive(body)));
+        try {
+            return extractProcessInstanceId(yidaProcessOpenApiClient.startProcess(body));
+        } catch (McException exception) {
+            throw exception;
+        } catch (Exception exception) {
+            throw new McException("DINGTALK_YIDA_ERROR", "Yida process start failed: " + rootMessage(exception));
+        }
+    }
+
+    private Map<String, Object> toYidaFormData(Map<String, Object> mainData, String operatorUserId, String documentNo) {
+        Map<String, Object> formData = new LinkedHashMap<String, Object>();
+        String title = FORM_NAME + "-" + documentNo;
+        Object company = firstNonBlank(mainData.get("gs"), mainData.get("gst100"));
+
+        putIfNotBlank(formData, TITLE_FIELD_ID, title);
+        putIfNotBlank(formData, APPLICANT_FIELD_ID, Collections.singletonList(operatorUserId));
+        putIfNotBlank(formData, COMPANY_FIELD_ID, company);
+        putIfNotBlank(formData, COMPANY_SELECT_FIELD_ID, company);
+        putIfNotBlank(formData, DEPARTMENT_FIELD_ID, toStringList(mainData.get("bm")));
+        putIfNotBlank(formData, APPLY_DATE_FIELD_ID, toDateMillis(firstNonBlank(mainData.get("rq"), mainData.get("sqrq"))));
+        putIfNotBlank(formData, FORM_TITLE_FIELD_ID, title);
+
+        for (Map.Entry<String, String> entry : ERP_FIELD_MAPPING.entrySet()) {
+            putIfNotBlank(formData, entry.getValue(), mainData.get(entry.getKey()));
+        }
+        putIfNotBlank(formData, ESTABLISH_DATE_FIELD_ID, toDateMillis(mainData.get("clsj")));
+        putIfNotBlank(formData, LIFE_CYCLE_FIELD_ID, firstNonBlank(mainData.get("smzq"), mainData.get("pmba083")));
+        return formData;
+    }
+
+    private Map<String, Object> toMainDataMap(ErpYidaPushMainData mainData) {
+        if (mainData == null) {
+            return Collections.emptyMap();
+        }
+        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());
+        result.putAll(mainData.getExtraFields());
+        return result;
+    }
+
+    private static Map<String, String> createErpFieldMapping() {
+        Map<String, String> mapping = new LinkedHashMap<String, String>();
+        mapping.put("sqlx", APPLY_TYPE_FIELD_ID);
+        mapping.put("gysmc", SUPPLIER_NAME_FIELD_ID);
+        mapping.put("gysbm", SUPPLIER_CODE_FIELD_ID);
+        mapping.put("sh", TAX_NUMBER_FIELD_ID);
+        mapping.put("gczczj", REGISTERED_CAPITAL_FIELD_ID);
+        mapping.put("gsdz", ADDRESS_FIELD_ID);
+        mapping.put("lxr", CONTACT_FIELD_ID);
+        mapping.put("lxfs", CONTACT_PHONE_FIELD_ID);
+        mapping.put("zysccp", MAIN_PRODUCTS_FIELD_ID);
+        mapping.put("gcgm", FACTORY_SCALE_FIELD_ID);
+        mapping.put("pmbb033all", "textField_mszogjjd");
+        mapping.put("pmbb034all", "textField_mszogjjv");
+        mapping.put("pmbb053all", "textField_mszogjk5");
+        mapping.put("pmbb054all", "textField_mszogjk7");
+        mapping.put("pmbb035all", "textField_mszogjk9");
+        mapping.put("pmbb036all", "textField_mszogjkh");
+        mapping.put("pmbb056all", "textField_mszogjkj");
+        mapping.put("pmbb037all", "textField_mszogjkl");
+        mapping.put("pmbb055all", "textField_mszogjkt");
+        mapping.put("pmbb057all", "textField_mszogjkv");
+        mapping.put("pmbb058all", "textField_mszogjl0");
+        mapping.put("pmbb033", "textField_mszogjlk");
+        mapping.put("pmbb034", "textField_mszogjls");
+        mapping.put("pmbb053", "textField_mszogjlu");
+        mapping.put("pmbb054", "textField_mszogjlw");
+        mapping.put("pmbb035", "textField_mszogjly");
+        mapping.put("pmbb036", "textField_mszogjm9");
+        mapping.put("pmbb056", "textField_mszogjmb");
+        mapping.put("pmbb037", "textField_mszogjmd");
+        mapping.put("pmbb055", "textField_mszogjml");
+        mapping.put("pmbb057", "textField_mszogjmn");
+        mapping.put("pmbb058", "textField_mszogjmp");
+        return Collections.unmodifiableMap(mapping);
+    }
+
+    private List<String> toStringList(Object value) {
+        String text = toText(value);
+        return text == null ? Collections.<String>emptyList() : Collections.singletonList(text);
+    }
+
+    private Long toDateMillis(Object value) {
+        String text = toText(value);
+        if (text == null) {
+            return null;
+        }
+        if (text.matches("\\d+")) {
+            return Long.valueOf(text);
+        }
+        return LocalDate.parse(text.replace('/', '-')).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+    }
+
+    private String toText(Object value) {
+        return isBlank(value) ? null : String.valueOf(value).trim();
+    }
+
+    private Object firstNonBlank(Object first, Object second) {
+        return isBlank(first) ? second : first;
+    }
+
+    private boolean isBlank(Object value) {
+        return value == null || String.valueOf(value).trim().isEmpty();
+    }
+
+    private void putIfNotBlank(Map<String, Object> target, String key, Object value) {
+        if (!isBlank(value)) {
+            target.put(key, value);
+        }
+    }
+
+    private String extractProcessInstanceId(DDR_New response) {
+        if (response == null) {
+            throw new McException("YIDA_NULL_RESPONSE", "Yida process start failed: empty response");
+        }
+        Object result = firstNonBlank(firstNonBlank(response.getResult(), response.getInstanceId()), response.getFormInstId());
+        if (!isBlank(result)) {
+            return String.valueOf(result);
+        }
+        if (response.isSuccess()) {
+            return "SUCCESS";
+        }
+        throw new McException(defaultText(response.getCode(), "YIDA_ERROR"),
+                "Yida process start failed: " + defaultText(response.getMessage(), "no process instance id"));
+    }
+
+    private Map<String, Object> maskSensitive(Map<String, Object> source) {
+        Map<String, Object> body = new LinkedHashMap<String, Object>(source);
+        body.put("systemToken", "******");
+        return body;
+    }
+
+    private String rootMessage(Throwable exception) {
+        Throwable current = exception;
+        while (current.getCause() != null && current.getCause() != current) {
+            current = current.getCause();
+        }
+        return defaultText(current.getMessage(), current.getClass().getSimpleName());
+    }
+
+    private String defaultText(String value, String fallback) {
+        return value == null || value.trim().isEmpty() ? fallback : value.trim();
+    }
+
+    private String requireText(String value, String message) {
+        if (value == null || value.trim().isEmpty()) {
+            throw new IllegalArgumentException(message);
+        }
+        return value.trim();
+    }
+
+    private void validateConfiguration() {
+        requireText(dingTalkConfiguration.getAppKey(), "DINGTALK_APP_KEY is not configured");
+        requireText(dingTalkConfiguration.getAppSecret(), "DINGTALK_APP_SECRET is not configured");
+        requireText(applications.getCoordinationOffice().getAppType(), "YIDA_COORDINATION_OFFICE_APP_TYPE is not configured");
+        requireText(applications.getCoordinationOffice().getSystemToken(), "YIDA_COORDINATION_OFFICE_SYSTEM_TOKEN is not configured");
+    }
+}

+ 360 - 0
src/main/java/com/malk/minglei/service/impl/yida/Asft800FormPushHandler.java

@@ -0,0 +1,360 @@
+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
+
+Asft800FormPushHandler implements YidaFormPushHandler {
+
+    public static final String TYPE = "asft800";
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(Asft800FormPushHandler.class);
+    private static final String FORM_NAME = "\u5305\u88c5\u5de5\u5355\u53d8\u66f4\u5ba1\u6279\u6d41\u7a0b";
+    private static final String FORM_UUID = "FORM-40D6FBBBF4A94864A2AF9535AAC7924CRBH2";
+    private static final String PROCESS_CODE = "TPROC--U2I66F719HF854X6PIGK5A2AP9VC2LODB7YSM1";
+    private static final String TITLE_FIELD_ID = "textField_msh27unu";
+    private static final String APPLICANT_FIELD_ID = "employeeField_msh186rr";
+    private static final String COMPANY_TEXT_FIELD_ID = "textField_mu1z5kaf";
+    private static final String COMPANY_SELECT_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 WORK_ORDER_NO_FIELD_ID = "textField_msy7cb7z";
+    private static final String CHANGE_SEQUENCE_FIELD_ID = "textField_msy7cb80";
+    private static final String CHANGE_DATE_FIELD_ID = "dateField_msy7cb81";
+    private static final String WORK_ORDER_TYPE_FIELD_ID = "textField_msy7cb82";
+    private static final String PRODUCTION_ITEM_FIELD_ID = "textField_msy7cb86";
+    private static final String ITEM_NAME_FIELD_ID = "textField_msy7cb87";
+    private static final String PRODUCTION_QUANTITY_FIELD_ID = "numberField_msy7cb88";
+    private static final String SPECIFICATION_FIELD_ID = "textField_msy7cb89";
+    private static final String SOURCE_DOCUMENT_FIELD_ID = "textField_msy7cb8g";
+    private static final String PRODUCTION_ORDER_FIELD_ID = "textField_msy7cb8h";
+    private static final String CHANGE_REMARK_FIELD_ID = "textField_msy7cb8i";
+    private static final String SALES_PERSON_FIELD_ID = "employeeField_msy7cb8j";
+    private static final String DETAIL_TABLE_FIELD_ID = "tableField_msy7cb8k";
+    private static final String DETAIL_ENTERPRISE_FIELD_ID = "textField_msy7cb9k";
+    private static final String DETAIL_WORK_ORDER_FIELD_ID = "textField_msy7cb9l";
+    private static final String DETAIL_SEQUENCE_FIELD_ID = "textField_msy7cb8l";
+    private static final String DETAIL_CHANGE_TYPE_FIELD_ID = "textField_msy7cb8m";
+    private static final String DETAIL_ISSUE_ITEM_FIELD_ID = "textField_msy7cb8n";
+    private static final String DETAIL_ITEM_NAME_FIELD_ID = "textField_msy7cb8o";
+    private static final String DETAIL_SPECIFICATION_FIELD_ID = "textField_msy7cb8p";
+    private static final String DETAIL_REQUIRED_QUANTITY_FIELD_ID = "numberField_msy7cb8q";
+    private static final String DETAIL_UNIT_FIELD_ID = "textField_msy7cb8r";
+    private static final String DETAIL_PREVIOUS_ITEM_FIELD_ID = "textField_msy7cb8s";
+    private static final String DETAIL_PREVIOUS_ITEM_NAME_FIELD_ID = "textField_msy7cb8t";
+    private static final String DETAIL_PREVIOUS_SPECIFICATION_FIELD_ID = "textField_msy7cb8u";
+    private static final String DETAIL_PREVIOUS_QUANTITY_FIELD_ID = "numberField_msy7cb8v";
+    private static final String T100_ID_FIELD_ID = "textField_msy7cb9d";
+    private static final String ENTERPRISE_FIELD_ID = "textField_msy7cb9e";
+    private static final String CALLBACK_CHANGE_SEQUENCE_FIELD_ID = "textField_msy7cb9f";
+
+    private final YidaProcessOpenApiClient yidaProcessOpenApiClient;
+    private final DDConf dingTalkConfiguration;
+    private final YidaApplicationsProperties applications;
+
+    public Asft800FormPushHandler(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();
+
+        Map<String, Object> mainData = toMainDataMap(request.getMainData());
+        String operatorUserId = requireText(resolveOperatorUserId(request, mainData), "asft800 requires mainData.ooagud004 or mainData.tjr");
+        Map<String, Object> formData = toYidaFormData(request, mainData, operatorUserId);
+        String title = requireText(toText(formData.get(TITLE_FIELD_ID)), "asft800 requires mainData.sfkadocno");
+
+        Map<String, Object> body = new LinkedHashMap<String, Object>();
+        body.put("appType", requireText(applications.getCoordinationOffice().getAppType(), "YIDA_COORDINATION_OFFICE_APP_TYPE is not configured"));
+        body.put("systemToken", requireText(applications.getCoordinationOffice().getSystemToken(), "YIDA_COORDINATION_OFFICE_SYSTEM_TOKEN is not configured"));
+        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 formName={} body={}", FORM_NAME, JSON.toJSONString(maskSensitive(body)));
+        try {
+            return extractProcessInstanceId(yidaProcessOpenApiClient.startProcess(body));
+        } catch (McException exception) {
+            throw exception;
+        } catch (Exception exception) {
+            throw new McException("DINGTALK_YIDA_ERROR", "Yida process start failed: " + rootMessage(exception));
+        }
+    }
+
+    private Map<String, Object> toYidaFormData(ErpYidaPushRequest request,
+                                               Map<String, Object> mainData,
+                                               String operatorUserId) {
+        List<Map<String, Object>> detailRows = toMapRows(objectToMap(request.getDetailData()).get("detailData1"));
+        Object changeSequence = firstNonBlank(mainData.get("sfka900"), firstValue(detailRows, "sfkg900"));
+        Object company = toCompanyName(firstNonBlank(mainData.get("gs"), mainData.get("sfkaent")));
+        String workOrderNo = toText(mainData.get("sfkadocno"));
+        String title = workOrderNo == null ? null : FORM_NAME + "-" + workOrderNo;
+
+        Map<String, Object> formData = new LinkedHashMap<String, Object>();
+        putIfNotBlank(formData, TITLE_FIELD_ID, title);
+        putIfNotBlank(formData, APPLICANT_FIELD_ID, Collections.singletonList(operatorUserId));
+        putIfNotBlank(formData, COMPANY_TEXT_FIELD_ID, company);
+        putIfNotBlank(formData, COMPANY_SELECT_FIELD_ID, company);
+        putIfNotBlank(formData, DEPARTMENT_FIELD_ID, toStringList(mainData.get("bm")));
+        putIfNotBlank(formData, APPLY_DATE_FIELD_ID, toDateMillis(firstNonBlank(mainData.get("rq"), mainData.get("sfka902"))));
+        putIfNotBlank(formData, FORM_TITLE_FIELD_ID, title);
+        putIfNotBlank(formData, WORK_ORDER_NO_FIELD_ID, workOrderNo);
+        putIfNotBlank(formData, CHANGE_SEQUENCE_FIELD_ID, changeSequence);
+        putIfNotBlank(formData, CHANGE_DATE_FIELD_ID, toDateMillis(mainData.get("sfka902")));
+        putIfNotBlank(formData, WORK_ORDER_TYPE_FIELD_ID, mainData.get("sfka057"));
+        putIfNotBlank(formData, PRODUCTION_ITEM_FIELD_ID, mainData.get("sfka010"));
+        putIfNotBlank(formData, ITEM_NAME_FIELD_ID, mainData.get("imaal003"));
+        putIfNotBlank(formData, PRODUCTION_QUANTITY_FIELD_ID, toDecimal(mainData.get("sfka012")));
+        putIfNotBlank(formData, SPECIFICATION_FIELD_ID, mainData.get("imaal004"));
+        putIfNotBlank(formData, SOURCE_DOCUMENT_FIELD_ID, mainData.get("sfka006"));
+        putIfNotBlank(formData, PRODUCTION_ORDER_FIELD_ID, mainData.get("sfkaud009"));
+        putIfNotBlank(formData, CHANGE_REMARK_FIELD_ID, mainData.get("sfka906"));
+        putIfNotBlank(formData, SALES_PERSON_FIELD_ID, Collections.singletonList(operatorUserId));
+        putIfNotBlank(formData, T100_ID_FIELD_ID, mainData.get("t100id"));
+        putIfNotBlank(formData, ENTERPRISE_FIELD_ID, mainData.get("sfkaent"));
+        putIfNotBlank(formData, CALLBACK_CHANGE_SEQUENCE_FIELD_ID, changeSequence);
+
+        List<Map<String, Object>> rows = toYidaDetailRows(detailRows);
+        if (!rows.isEmpty()) {
+            formData.put(DETAIL_TABLE_FIELD_ID, rows);
+        }
+        return formData;
+    }
+
+    private List<Map<String, Object>> toYidaDetailRows(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_ENTERPRISE_FIELD_ID, source.get("sfkgent"));
+            putIfNotBlank(row, DETAIL_WORK_ORDER_FIELD_ID, source.get("sfkgdocno"));
+            putIfNotBlank(row, DETAIL_SEQUENCE_FIELD_ID, source.get("sfkgseq"));
+            putIfNotBlank(row, DETAIL_CHANGE_TYPE_FIELD_ID, source.get("sfkg901"));
+            putIfNotBlank(row, DETAIL_ISSUE_ITEM_FIELD_ID, source.get("sfkg006"));
+            putIfNotBlank(row, DETAIL_ITEM_NAME_FIELD_ID, source.get("imaal003"));
+            putIfNotBlank(row, DETAIL_SPECIFICATION_FIELD_ID, source.get("imaal004"));
+            putIfNotBlank(row, DETAIL_REQUIRED_QUANTITY_FIELD_ID, toDecimal(source.get("sfkg013")));
+            putIfNotBlank(row, DETAIL_UNIT_FIELD_ID, source.get("sfkg014"));
+            putIfNotBlank(row, DETAIL_PREVIOUS_ITEM_FIELD_ID, source.get("sfba006"));
+            putIfNotBlank(row, DETAIL_PREVIOUS_ITEM_NAME_FIELD_ID, source.get("o_imaal003"));
+            putIfNotBlank(row, DETAIL_PREVIOUS_SPECIFICATION_FIELD_ID, source.get("o_imaal004"));
+            putIfNotBlank(row, DETAIL_PREVIOUS_QUANTITY_FIELD_ID, toDecimal(source.get("sfba013")));
+            if (!row.isEmpty()) {
+                rows.add(row);
+            }
+        }
+        return rows;
+    }
+
+    private String resolveOperatorUserId(ErpYidaPushRequest request, Map<String, Object> mainData) {
+        return toText(firstNonBlank(request.getOriginatorUserId(), mainData.get("ooagud004")));
+    }
+
+    private Map<String, Object> toMainDataMap(ErpYidaPushMainData mainData) {
+        if (mainData == null) {
+            return Collections.emptyMap();
+        }
+        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;
+    }
+
+    private Map<String, Object> objectToMap(Object value) {
+        if (value instanceof Map) {
+            Map<String, Object> result = new LinkedHashMap<String, Object>();
+            for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
+                if (entry.getKey() != null) {
+                    result.put(String.valueOf(entry.getKey()), entry.getValue());
+                }
+            }
+            return result;
+        }
+        return Collections.emptyMap();
+    }
+
+    private List<Map<String, Object>> toMapRows(Object value) {
+        List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
+        if (!(value instanceof Iterable)) {
+            return rows;
+        }
+        for (Object item : (Iterable<?>) value) {
+            Map<String, Object> row = objectToMap(item);
+            if (!row.isEmpty()) {
+                rows.add(row);
+            }
+        }
+        return rows;
+    }
+
+    private Object firstValue(List<Map<String, Object>> rows, String key) {
+        for (Map<String, Object> row : rows) {
+            if (!isBlank(row.get(key))) {
+                return row.get(key);
+            }
+        }
+        return null;
+    }
+
+    private Object firstNonBlank(Object first, Object second) {
+        return isBlank(first) ? second : first;
+    }
+
+    private List<String> toStringList(Object value) {
+        if (isBlank(value)) {
+            return Collections.emptyList();
+        }
+        if (value instanceof Iterable) {
+            List<String> values = new ArrayList<String>();
+            for (Object item : (Iterable<?>) value) {
+                if (!isBlank(item)) {
+                    values.add(String.valueOf(item).trim());
+                }
+            }
+            return values;
+        }
+        return Collections.singletonList(String.valueOf(value).trim());
+    }
+
+    private String toCompanyName(Object value) {
+        String text = toText(value);
+        if (text == null) {
+            return null;
+        }
+        if ("87".equals(text) || "ML".equalsIgnoreCase(text) || "zh_CN".equalsIgnoreCase(text)) {
+            return "\u660e\u78ca";
+        }
+        if ("VN".equalsIgnoreCase(text) || "vi_VN".equalsIgnoreCase(text)) {
+            return "\u8d8a\u5357";
+        }
+        return text;
+    }
+
+    private Long toDateMillis(Object value) {
+        String text = toText(value);
+        if (text == null) {
+            return null;
+        }
+        if (text.matches("\\d+")) {
+            return Long.valueOf(text);
+        }
+        return LocalDate.parse(text.replace('/', '-')).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+    }
+
+    private BigDecimal toDecimal(Object value) {
+        String text = toText(value);
+        return text == null ? null : new BigDecimal(text);
+    }
+
+    private String toText(Object value) {
+        return isBlank(value) ? null : String.valueOf(value).trim();
+    }
+
+    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 extractProcessInstanceId(DDR_New response) {
+        if (response == null) {
+            throw new McException("YIDA_NULL_RESPONSE", "Yida process start failed: empty response");
+        }
+        Object result = firstNonBlank(firstNonBlank(response.getResult(), response.getInstanceId()), response.getFormInstId());
+        if (!isBlank(result)) {
+            return String.valueOf(result);
+        }
+        if (response.isSuccess()) {
+            return "SUCCESS";
+        }
+        throw new McException(defaultText(response.getCode(), "YIDA_ERROR"),
+                "Yida process start failed: " + defaultText(response.getMessage(), "no process instance id"));
+    }
+
+    private Map<String, Object> maskSensitive(Map<String, Object> source) {
+        Map<String, Object> body = new LinkedHashMap<String, Object>(source);
+        body.put("systemToken", "******");
+        return body;
+    }
+
+    private String rootMessage(Throwable exception) {
+        Throwable current = exception;
+        while (current.getCause() != null && current.getCause() != current) {
+            current = current.getCause();
+        }
+        return defaultText(current.getMessage(), current.getClass().getSimpleName());
+    }
+
+    private String defaultText(String value, String fallback) {
+        return value == null || value.trim().isEmpty() ? fallback : value.trim();
+    }
+
+    private String requireText(String value, String message) {
+        if (value == null || value.trim().isEmpty()) {
+            throw new IllegalArgumentException(message);
+        }
+        return value.trim();
+    }
+
+    private void putIfNotBlank(Map<String, Object> target, String key, Object value) {
+        if (!isBlank(value)) {
+            target.put(key, value);
+        }
+    }
+
+    private void validateConfiguration() {
+        requireText(dingTalkConfiguration.getAppKey(), "DINGTALK_APP_KEY is not configured");
+        requireText(dingTalkConfiguration.getAppSecret(), "DINGTALK_APP_SECRET is not configured");
+        requireText(applications.getCoordinationOffice().getAppType(), "YIDA_COORDINATION_OFFICE_APP_TYPE is not configured");
+        requireText(applications.getCoordinationOffice().getSystemToken(), "YIDA_COORDINATION_OFFICE_SYSTEM_TOKEN is not configured");
+    }
+}

+ 36 - 0
src/test/java/com/malk/minglei/config/WebCorsConfigurationTest.java

@@ -0,0 +1,36 @@
+package com.malk.minglei.config;
+
+import com.malk.minglei.controller.YidaProcessController;
+import com.malk.minglei.service.yida.MingleiDocumentService;
+import com.malk.minglei.service.yida.YidaProcessService;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import static org.mockito.Mockito.mock;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+class WebCorsConfigurationTest {
+
+    @Test
+    void acceptsCrossOriginPreflightRequestsForApiEndpoints() throws Exception {
+        YidaProcessController controller = new YidaProcessController(
+                mock(YidaProcessService.class),
+                mock(MingleiDocumentService.class));
+        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller)
+                .addFilters(new WebCorsConfiguration().corsFilter())
+                .build();
+
+        mockMvc.perform(MockMvcRequestBuilders.options("/api/yida/minglei/departments/users")
+                        .header(HttpHeaders.ORIGIN, "https://frontend.example.com")
+                        .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST")
+                        .header(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "content-type,authorization"))
+                .andExpect(status().isOk())
+                .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"))
+                .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, org.hamcrest.Matchers.containsString("POST")))
+                .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, org.hamcrest.Matchers.containsString("authorization")));
+    }
+}

+ 94 - 0
src/test/java/com/malk/minglei/service/impl/yida/Ampt100FormPushHandlerTest.java

@@ -0,0 +1,94 @@
+package com.malk.minglei.service.impl.yida;
+
+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.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class Ampt100FormPushHandlerTest {
+
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void pushMapsSupplierImportPayloadToTheConfiguredYidaProcess() {
+        YidaProcessOpenApiClient yidaClient = mock(YidaProcessOpenApiClient.class);
+        DDR_New response = new DDR_New();
+        response.setResult("process-instance-1");
+        when(yidaClient.startProcess(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.setRq("2026-09-16");
+        mainData.setBm("123456");
+        mainData.setGs("ML");
+        mainData.setExtraField("sqlx", "NEW");
+        mainData.setExtraField("gysmc", "supplier-name");
+        mainData.setExtraField("gysbm", "SUP-001");
+        mainData.setExtraField("sh", "91330100TEST");
+        mainData.setExtraField("gczczj", "1000000.50");
+        mainData.setExtraField("clsj", "2026/09/16");
+        mainData.setExtraField("zysccp", "battery-pack");
+        mainData.setExtraField("gcgm", "120");
+        mainData.setExtraField("lcbh", "PM-001");
+        mainData.setExtraField("smzq", "ACTIVE");
+        mainData.setExtraField("pmbb033all", "CNY");
+        mainData.setExtraField("pmbb034all", "VAT");
+        mainData.setExtraField("pmbb033", "USD");
+        mainData.setExtraField("pmbb037", "NET30");
+        mainData.setExtraField("gst100", "ML");
+        mainData.setExtraField("lxr", "contact-name");
+        mainData.setExtraField("gsdz", "supplier-address");
+        mainData.setExtraField("lxfs", "13800000000");
+
+        ErpYidaPushRequest request = new ErpYidaPushRequest();
+        request.setType(Ampt100FormPushHandler.TYPE);
+        request.setMainData(mainData);
+        request.setDetailData(Collections.emptyMap());
+        request.setOriginatorUserId("dingtalk-user-001");
+
+        Ampt100FormPushHandler handler = new Ampt100FormPushHandler(yidaClient, ddConf, applications);
+
+        assertEquals(Collections.singletonList("ampt100"), 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("FORM-9EE14CA14D8D4AEE8102944E669E6B0CFUKS", body.get("formUuid"));
+        assertEquals("TPROC--75G66PD15YG82766OFSWD8BS4ZJ33YPRFOZSM2", body.get("processCode"));
+        assertEquals("供应商导入流程-PM-001", body.get("title"));
+        assertEquals("dingtalk-user-001", body.get("userId"));
+
+        JSONObject formData = JSONObject.parseObject(String.valueOf(body.get("formDataJson")));
+        assertEquals("supplier-name", formData.getString("textField_mszogjgv"));
+        assertEquals("SUP-001", formData.getString("textField_mszogjgw"));
+        assertEquals("1000000.50", formData.getString("textField_mszogjgy"));
+        assertEquals(Long.valueOf(1789488000000L), formData.getLong("dateField_mszogjgz"));
+        assertEquals("ACTIVE", formData.getString("textField_mszogjj9"));
+        assertEquals("CNY", formData.getString("textField_mszogjjd"));
+        assertEquals("USD", formData.getString("textField_mszogjlk"));
+        assertEquals("NET30", formData.getString("textField_mszogjmd"));
+        assertEquals("supplier-address", formData.getString("textField_mszogjh0"));
+        assertEquals("dingtalk-user-001", formData.getJSONArray("employeeField_msh186rr").getString(0));
+    }
+}

+ 115 - 0
src/test/java/com/malk/minglei/service/impl/yida/Asft800FormPushHandlerTest.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.ArgumentMatchers.anyMap;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class Asft800FormPushHandlerTest {
+
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void pushMapsAsft800PayloadToThePackagingWorkOrderChangeProcess() {
+        YidaProcessOpenApiClient yidaClient = mock(YidaProcessOpenApiClient.class);
+        DDR_New response = new DDR_New();
+        response.setResult("process-instance-1");
+        when(yidaClient.startProcess(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("sfkaent", "87");
+        mainData.setExtraField("sfkadocno", "WO-001");
+        mainData.setExtraField("sfka900", "2");
+        mainData.setExtraField("sfka902", "2026/09/16");
+        mainData.setExtraField("sfka057", "PACK");
+        mainData.setExtraField("sfka010", "ITEM-001");
+        mainData.setExtraField("imaal003", "item-name");
+        mainData.setExtraField("imaal004", "item-spec");
+        mainData.setExtraField("sfka012", "100.5");
+        mainData.setExtraField("sfka006", "SOURCE-001");
+        mainData.setExtraField("sfkaud009", "MO-001");
+        mainData.setExtraField("sfka906", "change-remark");
+        mainData.setExtraField("ooagud004", "dingtalk-user-001");
+
+        Map<String, Object> detailRow = new LinkedHashMap<String, Object>();
+        detailRow.put("sfkgent", "87");
+        detailRow.put("sfkgdocno", "WO-001");
+        detailRow.put("sfkg900", "2");
+        detailRow.put("sfkgseq", "10");
+        detailRow.put("sfkg901", "CHANGE");
+        detailRow.put("sfkg006", "MAT-002");
+        detailRow.put("imaal003", "material-name");
+        detailRow.put("imaal004", "material-spec");
+        detailRow.put("sfkg013", "101.5");
+        detailRow.put("sfkg014", "PCS");
+        detailRow.put("sfba006", "MAT-001");
+        detailRow.put("o_imaal003", "old-material-name");
+        detailRow.put("o_imaal004", "old-material-spec");
+        detailRow.put("sfba013", "100.0");
+        Map<String, Object> detailData = new LinkedHashMap<String, Object>();
+        detailData.put("detailData1", Collections.singletonList(detailRow));
+
+        ErpYidaPushRequest request = new ErpYidaPushRequest();
+        request.setType(Asft800FormPushHandler.TYPE);
+        request.setMainData(mainData);
+        request.setDetailData(detailData);
+
+        Asft800FormPushHandler handler = new Asft800FormPushHandler(yidaClient, ddConf, applications);
+
+        assertEquals(Collections.singletonList("asft800"), 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("dingtalk-user-001", body.get("userId"));
+        assertEquals("FORM-40D6FBBBF4A94864A2AF9535AAC7924CRBH2", body.get("formUuid"));
+        assertEquals("TPROC--U2I66F719HF854X6PIGK5A2AP9VC2LODB7YSM1", body.get("processCode"));
+        assertEquals("\u5305\u88c5\u5de5\u5355\u53d8\u66f4\u5ba1\u6279\u6d41\u7a0b-WO-001", body.get("title"));
+
+        JSONObject formData = JSONObject.parseObject(String.valueOf(body.get("formDataJson")));
+        assertEquals("WO-001", formData.getString("textField_msy7cb7z"));
+        assertEquals("2", formData.getString("textField_msy7cb80"));
+        assertEquals(Long.valueOf(1789488000000L), formData.getLong("dateField_msy7cb81"));
+        assertEquals("ITEM-001", formData.getString("textField_msy7cb86"));
+        assertEquals("item-name", formData.getString("textField_msy7cb87"));
+        assertEquals("100.5", formData.getString("numberField_msy7cb88"));
+        assertEquals("dingtalk-user-001", formData.getJSONArray("employeeField_msy7cb8j").getString(0));
+        assertEquals("87", formData.getString("textField_msy7cb9e"));
+        assertEquals("2", formData.getString("textField_msy7cb9f"));
+
+        JSONArray rows = formData.getJSONArray("tableField_msy7cb8k");
+        assertEquals(1, rows.size());
+        JSONObject row = rows.getJSONObject(0);
+        assertEquals("87", row.getString("textField_msy7cb9k"));
+        assertEquals("WO-001", row.getString("textField_msy7cb9l"));
+        assertEquals("10", row.getString("textField_msy7cb8l"));
+        assertEquals("CHANGE", row.getString("textField_msy7cb8m"));
+        assertEquals("101.5", row.getString("numberField_msy7cb8q"));
+        assertEquals("MAT-001", row.getString("textField_msy7cb8s"));
+        assertEquals("100.0", row.getString("numberField_msy7cb8v"));
+    }
+}