| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |