liu
昨天 43aab58c9ecb1203770ba382efbdbae299ca87ec
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);
        }
    }
}