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())
|
.tenantId(principal.tenantId())
|
.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("tenant_id", principal.tenantId());
|
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);
|
}
|
}
|
}
|