From 43aab58c9ecb1203770ba382efbdbae299ca87ec Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期一, 26 十月 2026 15:12:31 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_business' into dev_business

---
 yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightService.java |  259 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 259 insertions(+), 0 deletions(-)

diff --git a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightService.java b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightService.java
new file mode 100644
index 0000000..238192d
--- /dev/null
+++ b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightService.java
@@ -0,0 +1,259 @@
+package cn.iocoder.yudao.module.mdm.service.agent;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.module.mdm.controller.admin.agent.vo.MdmAgentImportPreflightReqVO;
+import cn.iocoder.yudao.module.mdm.controller.admin.agent.vo.MdmAgentImportPreflightRespVO;
+import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemImportExcelVO;
+import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
+import cn.iocoder.yudao.module.mdm.service.item.MdmItemImportContext;
+import cn.iocoder.yudao.module.mdm.service.item.MdmItemRowResolution;
+import cn.iocoder.yudao.module.mdm.service.item.MdmItemService;
+import cn.iocoder.yudao.module.system.framework.agent.AgentBridgeException;
+import cn.iocoder.yudao.module.system.framework.agent.AgentPrincipal;
+import cn.iocoder.yudao.module.system.framework.agent.SystemAgentBridgeGuard;
+import cn.iocoder.yudao.module.system.service.storage.SystemAgentAttachmentRef;
+import cn.iocoder.yudao.module.system.service.storage.SystemAgentImportBridgeService;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.Resource;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+
+/**
+ * 鐗╂枡瀵煎叆鍙棰勬锛氭牎楠岄檮浠躲�佽В鏋愬垎绫�/鍗曚綅銆佸垽鏂紪鐮佹槸鍚﹀凡瀛樺湪锛屼笉鍐欎换浣曚笟鍔¤〃銆�
+ */
+@Service
+@Validated
+public class MdmAgentImportPreflightService {
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    @Resource
+    private SystemAgentImportBridgeService bridgeService;
+    @Resource
+    private MdmItemService itemService;
+    @Resource
+    private MdmAgentImportPolicy policy;
+
+    public MdmAgentImportPreflightRespVO preflight(MdmAgentImportPreflightReqVO reqVO, AgentPrincipal principal) {
+        if (reqVO.getRows().size() > policy.getMaxRows()) {
+            throw new AgentBridgeException(HttpStatus.PAYLOAD_TOO_LARGE, "ROW_LIMIT_EXCEEDED",
+                    "瀵煎叆琛屾暟瓒呰繃涓婇檺锛�" + policy.getMaxRows());
+        }
+        // 1. 闄勪欢涓庢憳瑕佸繀椤讳笌鏈浼氳瘽缁戝畾鐨勪竴鑷�
+        SystemAgentAttachmentRef attachment = bridgeService.requireOwnedAttachmentRef(reqVO.getAttachmentId());
+        if (reqVO.getSessionId() != null && !reqVO.getSessionId().equals(attachment.sessionId())) {
+            throw new AgentBridgeException(HttpStatus.CONFLICT, "SESSION_MISMATCH",
+                    "闄勪欢涓嶅睘浜庢寚瀹氳ˉ褰曚細璇濓紝璇烽噸鏂颁笂浼犲悗鍐嶈瘯", true, null);
+        }
+        String sourceSha256 = bridgeService.sha256OfAttachment(reqVO.getAttachmentId());
+        if (!sourceSha256.equalsIgnoreCase(reqVO.getSourceSha256().trim())) {
+            throw new AgentBridgeException(HttpStatus.CONFLICT, "SOURCE_DIGEST_MISMATCH",
+                    "闄勪欢鍐呭宸插彉鍖栵紙鎽樿涓嶄竴鑷达級锛岃閲嶆柊涓婁紶鍚庡啀璇�", true, null);
+        }
+        // 2. candidate_id 蹇呴』鍞竴锛屽惁鍒欏洖鎵ф棤娉曢�愯瀵瑰簲
+        Set<String> candidateIds = new HashSet<>();
+        for (MdmAgentImportPreflightReqVO.Row row : reqVO.getRows()) {
+            if (!candidateIds.add(row.getCandidateId())) {
+                throw new AgentBridgeException(HttpStatus.BAD_REQUEST, "CANDIDATE_ID_DUPLICATED",
+                        "瀵煎叆琛屾爣璇嗛噸澶嶏細" + row.getCandidateId());
+            }
+        }
+
+        // 3. 棰勭儹鍒嗙被/鍗曚綅鏄犲皠
+        List<MdmItemImportExcelVO> excelRows = reqVO.getRows().stream()
+                .map(MdmAgentImportPreflightService::toExcelVO).toList();
+        MdmItemImportContext context = itemService.buildImportContext(excelRows);
+
+        boolean syncMesRequested = Boolean.TRUE.equals(reqVO.getSyncMes());
+        List<MdmAgentImportPreflightRespVO.Issue> blockingItems = new ArrayList<>();
+        if (syncMesRequested && !policy.isAllowMesSync()) {
+            blockingItems.add(issue("MES_SYNC_DISABLED", "syncMes",
+                    "鏈嶅姟鏈紑鏀惧悓姝� MES锛岃鍏抽棴 syncMes 鍚庨噸璇�", false));
+        }
+
+        // 4. 鏂囦欢鍐呴噸澶嶇紪鐮侊細鎵�鏈夐噸澶嶈涓�璧烽樆鏂紝閬垮厤鈥滈琛屽缓銆佹琛岃烦鈥濈殑璇
+        Map<String, Long> codeCounts = reqVO.getRows().stream()
+                .map(MdmAgentImportPreflightReqVO.Row::getCode).filter(StrUtil::isNotBlank)
+                .map(String::trim).collect(java.util.stream.Collectors.groupingBy(code -> code,
+                        java.util.stream.Collectors.counting()));
+        Set<String> duplicateCodes = codeCounts.entrySet().stream()
+                .filter(entry -> entry.getValue() > 1).map(Map.Entry::getKey)
+                .collect(java.util.stream.Collectors.toSet());
+
+        // 5. 閫愯鍒ゅ畾 CREATE / SKIP / BLOCKED
+        List<MdmAgentImportPreflightRespVO.Row> rows = new ArrayList<>();
+        int createCount = 0;
+        int skipCount = 0;
+        int blockedCount = 0;
+        for (int index = 0; index < reqVO.getRows().size(); index++) {
+            MdmAgentImportPreflightReqVO.Row row = reqVO.getRows().get(index);
+            MdmItemImportExcelVO excelVO = excelRows.get(index);
+            List<MdmAgentImportPreflightRespVO.Issue> blockers = new ArrayList<>();
+            List<MdmAgentImportPreflightRespVO.Issue> warnings = new ArrayList<>();
+
+            if (StrUtil.isBlank(row.getCode())) {
+                blockers.add(issue("ROW_CODE_REQUIRED", "code", "鐗╂枡缂栫爜涓嶈兘涓虹┖", false));
+            } else if (duplicateCodes.contains(row.getCode().trim())) {
+                blockers.add(issue("DUPLICATE_CODE_IN_FILE", "code",
+                        "鏂囦欢鍐呯墿鏂欑紪鐮侀噸澶嶏細" + row.getCode().trim(), false));
+            }
+            if (StrUtil.isBlank(row.getName())) {
+                blockers.add(issue("ROW_NAME_REQUIRED", "name", "鐗╂枡鍚嶇О涓嶈兘涓虹┖", false));
+            }
+            if (StrUtil.isNotBlank(row.getCategoryCode())
+                    && !context.categoryCodeIdMap().containsKey(row.getCategoryCode())) {
+                blockers.add(issue("CATEGORY_NOT_FOUND", "categoryCode",
+                        "鐗╂枡鍒嗙被缂栫爜涓嶅瓨鍦細" + row.getCategoryCode(), false));
+            }
+            if (StrUtil.isNotBlank(row.getUnitMeasureCode())
+                    && !context.unitCodeIdMap().containsKey(row.getUnitMeasureCode())) {
+                blockers.add(issue("UNIT_NOT_FOUND", "unitMeasureCode",
+                        "璁¢噺鍗曚綅缂栫爜涓嶅瓨鍦細" + row.getUnitMeasureCode(), false));
+            }
+
+            MdmItemRowResolution resolution = itemService.resolveImportRow(excelVO, context);
+            if (!resolution.isSuccess() && blockers.isEmpty()) {
+                // 鐞嗚涓婁笂闈㈢殑閫愰」鏍¢獙宸茶鐩栵紱鍏滃簳淇濇寔涓� Excel 瀵煎叆涓�鑷寸殑鏂囨
+                blockers.add(issue("ROW_INVALID", null, resolution.failureMessage(), false));
+            }
+
+            String action;
+            Long targetItemId = null;
+            if (!blockers.isEmpty()) {
+                action = "BLOCKED";
+                blockedCount++;
+            } else {
+                List<MdmItemDO> existing = itemService.getItemListByCode(row.getCode().trim());
+                if (!existing.isEmpty()) {
+                    action = "SKIP";
+                    targetItemId = existing.get(0).getId();
+                    skipCount++;
+                    warnings.add(issue("ITEM_CODE_EXISTS", "code", "鐗╂枡缂栫爜宸插瓨鍦紝鏈樁娈靛彧鏂板缓涓嶆洿鏂帮紝灏嗚烦杩囪琛�", false));
+                } else {
+                    action = "CREATE";
+                    createCount++;
+                }
+            }
+
+            rows.add(MdmAgentImportPreflightRespVO.Row.builder()
+                    .candidateId(row.getCandidateId())
+                    .rowNo(row.getRowNo())
+                    .code(row.getCode())
+                    .name(row.getName())
+                    .action(action)
+                    .resolved(resolve(row, context))
+                    .blockers(blockers)
+                    .warnings(warnings)
+                    .targetItemId(targetItemId)
+                    .build());
+        }
+
+        boolean canCommit = blockingItems.isEmpty() && blockedCount == 0;
+        String preflightHash = buildPreflightHash(reqVO, principal, attachment, sourceSha256,
+                syncMesRequested, rows);
+        return MdmAgentImportPreflightRespVO.builder()
+                .preflightHash(preflightHash)
+                .source(MdmAgentImportPreflightRespVO.Source.builder()
+                        .attachmentId(attachment.attachmentId())
+                        .sessionId(attachment.sessionId())
+                        .sourceSha256(sourceSha256)
+                        .proposalRevision(reqVO.getProposalRevision())
+                        .build())
+                .authorization(MdmAgentImportPreflightRespVO.Authorization.builder()
+                        .userId(principal.userId())
+                        .permission(SystemAgentBridgeGuard.PERMISSION_ITEM_IMPORT)
+                        .policyRevision(MdmAgentImportPolicy.POLICY_REVISION)
+                        .build())
+                .policy(MdmAgentImportPreflightRespVO.Policy.builder()
+                        .updateAllowed(false)
+                        .mesSyncAllowed(policy.isAllowMesSync())
+                        .maxRows(policy.getMaxRows())
+                        .build())
+                .summary(MdmAgentImportPreflightRespVO.Summary.builder()
+                        .total(rows.size())
+                        .create(createCount)
+                        .skip(skipCount)
+                        .blocked(blockedCount)
+                        .build())
+                .canCommit(canCommit)
+                .sideEffects(canCommit
+                        ? (syncMesRequested
+                        ? List.of("mdm_item.create:" + createCount, "mes_md_item.sync:" + createCount)
+                        : List.of("mdm_item.create:" + createCount))
+                        : List.of())
+                .rows(rows)
+                .blockingItems(blockingItems)
+                .build();
+    }
+
+    private static MdmItemImportExcelVO toExcelVO(MdmAgentImportPreflightReqVO.Row row) {
+        return MdmItemImportExcelVO.builder()
+                .code(row.getCode())
+                .name(row.getName())
+                .specification(row.getSpecification())
+                .categoryCode(row.getCategoryCode())
+                .unitMeasureCode(row.getUnitMeasureCode())
+                .build();
+    }
+
+    private static MdmAgentImportPreflightRespVO.Resolved resolve(MdmAgentImportPreflightReqVO.Row row,
+                                                                 MdmItemImportContext context) {
+        Long categoryId = StrUtil.isBlank(row.getCategoryCode()) ? null
+                : context.categoryCodeIdMap().get(row.getCategoryCode());
+        String categoryName = StrUtil.isBlank(row.getCategoryCode()) ? null
+                : context.categoryCodeNameMap().get(row.getCategoryCode());
+        Long unitMeasureId = StrUtil.isBlank(row.getUnitMeasureCode()) ? null
+                : context.unitCodeIdMap().get(row.getUnitMeasureCode());
+        String unitMeasureName = StrUtil.isBlank(row.getUnitMeasureCode()) ? null
+                : context.unitCodeNameMap().get(row.getUnitMeasureCode());
+        return MdmAgentImportPreflightRespVO.Resolved.builder()
+                .categoryId(categoryId).categoryName(categoryName)
+                .unitMeasureId(unitMeasureId).unitMeasureName(unitMeasureName)
+                .build();
+    }
+
+    private static MdmAgentImportPreflightRespVO.Issue issue(String code, String field, String message,
+                                                             boolean retryable) {
+        return MdmAgentImportPreflightRespVO.Issue.builder()
+                .code(code).field(field).message(message).retryable(retryable).build();
+    }
+
+    /**
+     * 鎸夊浐瀹氬瓧娈甸『搴忔嫾瑁呰鑼� JSON 鍚庡彇 SHA-256锛氬悓涓�浠借鍒掑繀椤诲緱鍒板悓涓�涓� hash銆�
+     */
+    private static String buildPreflightHash(MdmAgentImportPreflightReqVO reqVO, AgentPrincipal principal,
+                                             SystemAgentAttachmentRef attachment, String sourceSha256,
+                                             boolean syncMesRequested, List<MdmAgentImportPreflightRespVO.Row> rows) {
+        Map<String, Object> canonical = new LinkedHashMap<>();
+        canonical.put("policy_revision", MdmAgentImportPolicy.POLICY_REVISION);
+        canonical.put("user_id", principal.userId());
+        canonical.put("attachment_id", attachment.attachmentId());
+        canonical.put("session_id", attachment.sessionId());
+        canonical.put("source_sha256", sourceSha256.toLowerCase(Locale.ROOT));
+        canonical.put("proposal_revision", StrUtil.blankToDefault(reqVO.getProposalRevision(), ""));
+        canonical.put("sync_mes", syncMesRequested);
+        List<Map<String, Object>> canonicalRows = new ArrayList<>();
+        for (MdmAgentImportPreflightRespVO.Row row : rows) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("candidate_id", row.getCandidateId());
+            item.put("row_no", row.getRowNo());
+            item.put("code", row.getCode());
+            item.put("name", row.getName());
+            canonicalRows.add(item);
+        }
+        canonical.put("rows", canonicalRows);
+        try {
+            byte[] payload = OBJECT_MAPPER.writeValueAsBytes(canonical);
+            return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(payload));
+        } catch (NoSuchAlgorithmException ex) {
+            throw new IllegalStateException("SHA-256 涓嶅彲鐢�", ex);
+        } catch (com.fasterxml.jackson.core.JsonProcessingException ex) {
+            throw new IllegalStateException("棰勬鎽樿鐢熸垚澶辫触", ex);
+        }
+    }
+}

--
Gitblit v1.9.3