| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mdm.service.agent; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.module.mdm.controller.admin.agent.vo.MdmAgentImportCommitReqVO; |
| | | 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.agent.vo.MdmAgentImportReceiptRespVO; |
| | | import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemImportExcelVO; |
| | | import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO; |
| | | import cn.iocoder.yudao.module.mdm.dal.dataobject.agent.MdmAgentImportCommandDO; |
| | | import cn.iocoder.yudao.module.mdm.dal.dataobject.agent.MdmAgentImportCommandRowDO; |
| | | import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO; |
| | | import cn.iocoder.yudao.module.mdm.dal.mysql.agent.MdmAgentImportCommandMapper; |
| | | import cn.iocoder.yudao.module.mdm.dal.mysql.agent.MdmAgentImportCommandRowMapper; |
| | | 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 com.fasterxml.jackson.core.JsonProcessingException; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import jakarta.annotation.Resource; |
| | | import org.springframework.dao.DuplicateKeyException; |
| | | import org.springframework.http.HttpStatus; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.PlatformTransactionManager; |
| | | import org.springframework.transaction.support.TransactionTemplate; |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import java.security.MessageDigest; |
| | | import java.security.NoSuchAlgorithmException; |
| | | import java.time.Duration; |
| | | import java.time.LocalDateTime; |
| | | import java.util.*; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | /** |
| | | * ç©æå¯¼å
¥å¹çæäº¤ï¼åä¸ commandId/idempotencyKey éå¤æäº¤è¿åååæ§ï¼ç»ä¸éå¤å»ºç©æã |
| | | */ |
| | | @Service |
| | | @Validated |
| | | public class MdmAgentImportCommandService { |
| | | |
| | | public static final String STATUS_RUNNING = "RUNNING"; |
| | | public static final String STATUS_SUCCEEDED = "SUCCEEDED"; |
| | | public static final String STATUS_PARTIAL_FAILED = "PARTIAL_FAILED"; |
| | | public static final String STATUS_FAILED = "FAILED"; |
| | | public static final String STATUS_UNKNOWN = "UNKNOWN"; |
| | | |
| | | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); |
| | | private static final Duration LEASE_DURATION = Duration.ofMinutes(5); |
| | | |
| | | @Resource |
| | | private MdmAgentImportPreflightService preflightService; |
| | | @Resource |
| | | private MdmItemService itemService; |
| | | @Resource |
| | | private MdmAgentImportPolicy policy; |
| | | @Resource |
| | | private MdmAgentImportCommandMapper commandMapper; |
| | | @Resource |
| | | private MdmAgentImportCommandRowMapper commandRowMapper; |
| | | @Resource |
| | | private PlatformTransactionManager transactionManager; |
| | | |
| | | public MdmAgentImportReceiptRespVO commit(MdmAgentImportCommitReqVO reqVO, AgentPrincipal principal) { |
| | | if (!policy.isWriteEnabled()) { |
| | | throw new AgentBridgeException(HttpStatus.SERVICE_UNAVAILABLE, "WRITE_DISABLED", |
| | | "æºè½ä½åå
¥å¼å
³æªæå¼ï¼æäº¤å·²æç»"); |
| | | } |
| | | if (Boolean.TRUE.equals(reqVO.getSyncMes()) && !policy.isAllowMesSync()) { |
| | | throw new AgentBridgeException(HttpStatus.UNPROCESSABLE_ENTITY, "MES_SYNC_DISABLED", |
| | | "æå¡æªå¼æ¾åæ¥ MESï¼è¯·å
³é syncMes åéè¯"); |
| | | } |
| | | String requestDigest = buildRequestDigest(reqVO); |
| | | Long ownerUserId = principal.userId(); |
| | | |
| | | // 1. å¹çå½ä¸ï¼å½ä»¤ ID ä¼å
ï¼å
¶æ¬¡å¹çé® |
| | | MdmAgentImportCommandDO existing = commandMapper.selectByCommandId(ownerUserId, reqVO.getCommandId()); |
| | | if (existing != null) { |
| | | if (!existing.getRequestDigest().equals(requestDigest)) { |
| | | throw new AgentBridgeException(HttpStatus.CONFLICT, "COMMAND_ID_CONFLICT", |
| | | "åä¸å½ä»¤ ID æºå¸¦äºä¸åç请æ±å
容ï¼è¯·æ£æ¥ä»»å¡ç¶æåéè¯"); |
| | | } |
| | | return toReceipt(existing); |
| | | } |
| | | existing = commandMapper.selectByIdempotencyKey(ownerUserId, reqVO.getIdempotencyKey()); |
| | | if (existing != null) { |
| | | if (!existing.getRequestDigest().equals(requestDigest)) { |
| | | throw new AgentBridgeException(HttpStatus.CONFLICT, "IDEMPOTENCY_CONFLICT", |
| | | "åä¸å¹ç鮿ºå¸¦äºä¸åç请æ±å
容ï¼è¯·æ£æ¥ä»»å¡ç¶æåéè¯"); |
| | | } |
| | | return toReceipt(existing); |
| | | } |
| | | |
| | | // 2. 鿰颿£å¹¶æ¯å¯¹è®¡åæè¦ï¼é²æ¢ç¡®è®¤åæ°æ®å·²åå |
| | | MdmAgentImportPreflightRespVO preflight = preflightService.preflight(toPreflightRequest(reqVO), principal); |
| | | if (!reqVO.getPreflightHash().equalsIgnoreCase(preflight.getPreflightHash())) { |
| | | throw new AgentBridgeException(HttpStatus.CONFLICT, "PLAN_STALE", |
| | | "颿£è®¡åå·²ååï¼ç¼ç ç¶ææä¸»æ°æ®è¢«æ¹å¨ï¼ï¼è¯·éæ°é¢æ£å¹¶ç¡®è®¤", false, |
| | | Map.of("preflight", preflight)); |
| | | } |
| | | if (!Boolean.TRUE.equals(preflight.getCanCommit())) { |
| | | throw new AgentBridgeException(HttpStatus.UNPROCESSABLE_ENTITY, "PREFLIGHT_BLOCKED", |
| | | "颿£åå¨é»æé¡¹ï¼ä¸è½æäº¤", false, Map.of("preflight", preflight)); |
| | | } |
| | | |
| | | // 3. ç»è®°å½ä»¤ï¼å¯ä¸é®å
ä½å¹¶åéå¤æäº¤ï¼ |
| | | MdmAgentImportCommandDO command = MdmAgentImportCommandDO.builder() |
| | | .commandId(reqVO.getCommandId()) |
| | | .idempotencyKey(reqVO.getIdempotencyKey()) |
| | | .requestDigest(requestDigest) |
| | | .ownerUserId(ownerUserId) |
| | | .sessionId(reqVO.getSessionId()) |
| | | .attachmentId(reqVO.getAttachmentId()) |
| | | .sourceSha256(reqVO.getSourceSha256().toLowerCase(Locale.ROOT)) |
| | | .preflightHash(reqVO.getPreflightHash().toLowerCase(Locale.ROOT)) |
| | | .proposalHash(reqVO.getProposalHash()) |
| | | .syncMes(Boolean.TRUE.equals(reqVO.getSyncMes())) |
| | | .status(STATUS_RUNNING) |
| | | .totalCount(reqVO.getRows().size()) |
| | | .successCount(0).failedCount(0).skippedCount(0) |
| | | .leaseExpiresAt(LocalDateTime.now().plus(LEASE_DURATION)) |
| | | .build(); |
| | | try { |
| | | commandMapper.insert(command); |
| | | } catch (DuplicateKeyException ex) { |
| | | MdmAgentImportCommandDO raced = commandMapper.selectByCommandId(ownerUserId, reqVO.getCommandId()); |
| | | if (raced == null) { |
| | | raced = commandMapper.selectByIdempotencyKey(ownerUserId, reqVO.getIdempotencyKey()); |
| | | } |
| | | if (raced == null) { |
| | | throw ex; |
| | | } |
| | | if (!raced.getRequestDigest().equals(requestDigest)) { |
| | | throw new AgentBridgeException(HttpStatus.CONFLICT, "IDEMPOTENCY_CONFLICT", |
| | | "åä¸å½ä»¤/å¹ç鮿ºå¸¦äºä¸åç请æ±å
容ï¼è¯·æ£æ¥ä»»å¡ç¶æåéè¯"); |
| | | } |
| | | return toReceipt(raced); |
| | | } |
| | | |
| | | // 4. éè¡ç¬ç«äºå¡æ§è¡ï¼ä»»ä¸è¡å¤±è´¥ä¸å½±åå
¶ä»è¡ |
| | | int success = 0; |
| | | int failed = 0; |
| | | int skipped = 0; |
| | | TransactionTemplate rowTxTemplate = new TransactionTemplate(transactionManager); |
| | | List<MdmItemImportExcelVO> excelRows = reqVO.getRows().stream() |
| | | .map(MdmAgentImportCommandService::toExcelVO).toList(); |
| | | MdmItemImportContext context = itemService.buildImportContext(excelRows); |
| | | try { |
| | | for (int index = 0; index < reqVO.getRows().size(); index++) { |
| | | MdmAgentImportPreflightReqVO.Row row = reqVO.getRows().get(index); |
| | | MdmAgentImportPreflightRespVO.Row planned = preflight.getRows().get(index); |
| | | if ("SKIP".equals(planned.getAction())) { |
| | | commandRowMapper.insert(baseRow(command.getCommandId(), row) |
| | | .action("SKIP").status("SKIPPED") |
| | | .targetItemId(planned.getTargetItemId()) |
| | | .retryable(false).build()); |
| | | skipped++; |
| | | continue; |
| | | } |
| | | if (!"CREATE".equals(planned.getAction())) { |
| | | commandRowMapper.insert(baseRow(command.getCommandId(), row) |
| | | .action("CREATE").status("FAILED") |
| | | .errorCode("PREFLIGHT_BLOCKED").errorMessage("该è¡é¢æ£æªéè¿").retryable(false).build()); |
| | | failed++; |
| | | continue; |
| | | } |
| | | MdmItemRowResolution resolution = itemService.resolveImportRow(excelRows.get(index), context); |
| | | if (!resolution.isSuccess()) { |
| | | commandRowMapper.insert(baseRow(command.getCommandId(), row) |
| | | .action("CREATE").status("FAILED") |
| | | .errorCode("ROW_INVALID").errorMessage(trim(resolution.failureMessage(), 512)) |
| | | .retryable(false).build()); |
| | | failed++; |
| | | continue; |
| | | } |
| | | AtomicReference<Throwable> rowError = new AtomicReference<>(); |
| | | boolean[] concurrentSkip = {false}; |
| | | try { |
| | | rowTxTemplate.executeWithoutResult(status -> { |
| | | try { |
| | | // æäº¤å鿥ï¼ç¼©å°å¹¶åéå¤å»ºç©æççªå£ |
| | | List<MdmItemDO> current = itemService.getItemListByCode(row.getCode().trim()); |
| | | if (!current.isEmpty()) { |
| | | concurrentSkip[0] = true; |
| | | commandRowMapper.insert(baseRow(command.getCommandId(), row) |
| | | .action("SKIP").status("SKIPPED") |
| | | .targetItemId(current.get(0).getId()).retryable(false).build()); |
| | | return; |
| | | } |
| | | MdmItemSaveReqVO saveReqVO = resolution.saveReqVO(); |
| | | saveReqVO.setSyncMes(Boolean.TRUE.equals(reqVO.getSyncMes())); |
| | | Long itemId = itemService.createItem(saveReqVO); |
| | | commandRowMapper.insert(baseRow(command.getCommandId(), row) |
| | | .action("CREATE").status("SUCCESS").targetItemId(itemId) |
| | | .retryable(false).build()); |
| | | } catch (Throwable t) { |
| | | rowError.set(t); |
| | | throw t; |
| | | } |
| | | }); |
| | | if (concurrentSkip[0]) { |
| | | skipped++; |
| | | } else { |
| | | success++; |
| | | } |
| | | } catch (Exception ex) { |
| | | Throwable cause = rowError.get() != null ? rowError.get() : ex; |
| | | String message = cause instanceof ServiceException |
| | | ? ((ServiceException) cause).getMessage() : cause.getMessage(); |
| | | String errorCode = cause instanceof ServiceException ? "ITEM_CREATE_REJECTED" : "ITEM_CREATE_FAILED"; |
| | | commandRowMapper.insert(baseRow(command.getCommandId(), row) |
| | | .action("CREATE").status("FAILED") |
| | | .errorCode(errorCode).errorMessage(trim(message, 512)) |
| | | .retryable(!(cause instanceof ServiceException)).build()); |
| | | failed++; |
| | | } |
| | | } |
| | | } catch (Exception ex) { |
| | | // åºç¡è®¾æ½å¼å¸¸ä¸æï¼ä¿ç已宿è¡åæ§ï¼å½ä»¤æ è®° UNKNOWN ä¾å¯¹è´¦ |
| | | command.setStatus(STATUS_UNKNOWN); |
| | | command.setErrorCode("COMMIT_INTERRUPTED"); |
| | | command.setErrorMessage(trim(ex.getMessage(), 512)); |
| | | command.setSuccessCount(success); |
| | | command.setFailedCount(failed); |
| | | command.setSkippedCount(skipped); |
| | | commandMapper.updateById(command); |
| | | return toReceipt(command); |
| | | } |
| | | |
| | | command.setSuccessCount(success); |
| | | command.setFailedCount(failed); |
| | | command.setSkippedCount(skipped); |
| | | command.setStatus(failed == 0 ? STATUS_SUCCEEDED |
| | | : (success + skipped == 0 ? STATUS_FAILED : STATUS_PARTIAL_FAILED)); |
| | | command.setLeaseExpiresAt(LocalDateTime.now()); |
| | | commandMapper.updateById(command); |
| | | return toReceipt(command); |
| | | } |
| | | |
| | | public MdmAgentImportReceiptRespVO getCommand(String commandId, AgentPrincipal principal) { |
| | | MdmAgentImportCommandDO command = commandMapper.selectByCommandId( |
| | | principal.userId(), commandId); |
| | | if (command == null) { |
| | | throw new AgentBridgeException(HttpStatus.NOT_FOUND, "COMMAND_NOT_FOUND", "导å
¥å½ä»¤ä¸åå¨"); |
| | | } |
| | | return toReceipt(command); |
| | | } |
| | | |
| | | private MdmAgentImportReceiptRespVO toReceipt(MdmAgentImportCommandDO command) { |
| | | String status = command.getStatus(); |
| | | if (STATUS_RUNNING.equals(status) && command.getLeaseExpiresAt() != null |
| | | && command.getLeaseExpiresAt().isBefore(LocalDateTime.now())) { |
| | | // ç§çº¦è¿æè¯´ææ§è¡è¿ç¨å¯è½å·²ä¸æï¼ä¸æ¹åºï¼ä»
å¦å®åç¥è°ç¨æ¹ç»ææªç¥ |
| | | status = STATUS_UNKNOWN; |
| | | } |
| | | List<MdmAgentImportCommandRowDO> rowDOs = commandRowMapper.selectListByCommandId(command.getCommandId()); |
| | | List<MdmAgentImportReceiptRespVO.Row> rows = rowDOs.stream() |
| | | .map(row -> MdmAgentImportReceiptRespVO.Row.builder() |
| | | .candidateId(row.getCandidateId()) |
| | | .rowNo(row.getSourceRow()) |
| | | .code(row.getCode()) |
| | | .name(row.getName()) |
| | | .action(row.getAction()) |
| | | .status(row.getStatus()) |
| | | .targetItemId(row.getTargetItemId()) |
| | | .targetVersion(row.getTargetVersion()) |
| | | .errorCode(row.getErrorCode()) |
| | | .errorMessage(row.getErrorMessage()) |
| | | .retryable(row.getRetryable()) |
| | | .build()) |
| | | .toList(); |
| | | int success = (int) rowDOs.stream().filter(row -> "SUCCESS".equals(row.getStatus())).count(); |
| | | int failed = (int) rowDOs.stream().filter(row -> "FAILED".equals(row.getStatus())).count(); |
| | | int skipped = (int) rowDOs.stream().filter(row -> "SKIPPED".equals(row.getStatus())).count(); |
| | | int unknown = (int) rowDOs.stream().filter(row -> "UNKNOWN".equals(row.getStatus())).count(); |
| | | return MdmAgentImportReceiptRespVO.builder() |
| | | .commandId(command.getCommandId()) |
| | | .idempotencyKey(command.getIdempotencyKey()) |
| | | .status(status) |
| | | .attachmentId(command.getAttachmentId()) |
| | | .sessionId(command.getSessionId()) |
| | | .sourceSha256(command.getSourceSha256()) |
| | | .preflightHash(command.getPreflightHash()) |
| | | .proposalHash(command.getProposalHash()) |
| | | .summary(MdmAgentImportReceiptRespVO.Summary.builder() |
| | | .total(command.getTotalCount()) |
| | | .success(success).failed(failed).skipped(skipped).unknown(unknown) |
| | | .build()) |
| | | .errorCode(command.getErrorCode()) |
| | | .errorMessage(command.getErrorMessage()) |
| | | .rows(rows) |
| | | .createTime(command.getCreateTime()) |
| | | .completedTime(STATUS_RUNNING.equals(command.getStatus()) ? null : command.getUpdateTime()) |
| | | .build(); |
| | | } |
| | | |
| | | private static MdmAgentImportCommandRowDO.MdmAgentImportCommandRowDOBuilder baseRow( |
| | | String commandId, MdmAgentImportPreflightReqVO.Row row) { |
| | | return MdmAgentImportCommandRowDO.builder() |
| | | .commandId(commandId) |
| | | .candidateId(row.getCandidateId()) |
| | | .sourceRow(row.getRowNo()) |
| | | .code(row.getCode()) |
| | | .name(row.getName()); |
| | | } |
| | | |
| | | 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 MdmAgentImportPreflightReqVO toPreflightRequest(MdmAgentImportCommitReqVO reqVO) { |
| | | MdmAgentImportPreflightReqVO preflightReqVO = new MdmAgentImportPreflightReqVO(); |
| | | preflightReqVO.setAttachmentId(reqVO.getAttachmentId()); |
| | | preflightReqVO.setSessionId(reqVO.getSessionId()); |
| | | preflightReqVO.setSourceSha256(reqVO.getSourceSha256()); |
| | | preflightReqVO.setProposalRevision(reqVO.getProposalRevision()); |
| | | preflightReqVO.setSyncMes(reqVO.getSyncMes()); |
| | | preflightReqVO.setRows(reqVO.getRows()); |
| | | return preflightReqVO; |
| | | } |
| | | |
| | | /** |
| | | * è¯·æ±æè¦åªå
å«ä¸å¡å
容ï¼ä¸å« commandId/idempotencyKeyï¼ä¾¿äºè¯å«âåé®ä¸åå
容âã |
| | | */ |
| | | private static String buildRequestDigest(MdmAgentImportCommitReqVO reqVO) { |
| | | Map<String, Object> canonical = new LinkedHashMap<>(); |
| | | canonical.put("attachment_id", reqVO.getAttachmentId()); |
| | | canonical.put("session_id", reqVO.getSessionId()); |
| | | canonical.put("source_sha256", StrUtil.nullToEmpty(reqVO.getSourceSha256()).toLowerCase(Locale.ROOT)); |
| | | canonical.put("preflight_hash", StrUtil.nullToEmpty(reqVO.getPreflightHash()).toLowerCase(Locale.ROOT)); |
| | | canonical.put("proposal_hash", StrUtil.nullToEmpty(reqVO.getProposalHash())); |
| | | canonical.put("proposal_revision", StrUtil.nullToEmpty(reqVO.getProposalRevision())); |
| | | canonical.put("sync_mes", Boolean.TRUE.equals(reqVO.getSyncMes())); |
| | | List<Map<String, Object>> rows = new ArrayList<>(); |
| | | for (MdmAgentImportPreflightReqVO.Row row : reqVO.getRows()) { |
| | | Map<String, Object> item = new LinkedHashMap<>(); |
| | | item.put("candidate_id", row.getCandidateId()); |
| | | item.put("row_no", row.getRowNo()); |
| | | item.put("code", StrUtil.nullToEmpty(row.getCode())); |
| | | item.put("name", StrUtil.nullToEmpty(row.getName())); |
| | | item.put("specification", StrUtil.nullToEmpty(row.getSpecification())); |
| | | item.put("category_code", StrUtil.nullToEmpty(row.getCategoryCode())); |
| | | item.put("unit_measure_code", StrUtil.nullToEmpty(row.getUnitMeasureCode())); |
| | | rows.add(item); |
| | | } |
| | | canonical.put("rows", rows); |
| | | 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 (JsonProcessingException ex) { |
| | | throw new IllegalStateException("è¯·æ±æè¦çæå¤±è´¥", ex); |
| | | } |
| | | } |
| | | |
| | | private static String trim(String value, int maxLength) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | return value.length() <= maxLength ? value : value.substring(0, maxLength); |
| | | } |
| | | } |