|
|
|
@ -70,6 +70,7 @@ import java.util.stream.Collectors; |
|
|
|
@Slf4j |
|
|
|
public class QcServiceImpl implements QcService { |
|
|
|
private static final String TEMPLATE_SUPPLIER_WILDCARD = "*"; |
|
|
|
private static final String PART_ATTRIBUTE_TYPE_MATERIAL = "A"; |
|
|
|
|
|
|
|
//记录日志使用 |
|
|
|
private static final Logger logger = LoggerFactory.getLogger(HttpClientUtil.class); |
|
|
|
@ -1102,6 +1103,117 @@ public class QcServiceImpl implements QcService { |
|
|
|
return list; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 查询“检验模板复制”候选物料。 |
|
|
|
* |
|
|
|
* 关键约束: |
|
|
|
* 1) 只返回 status='Y' 的在用物料; |
|
|
|
* 2) 排除源物料自身,避免出现“复制到自己”; |
|
|
|
* 3) 不按供应商过滤,按用户要求支持“全在用物料”范围; |
|
|
|
* 4) 支持基准料号/物料名称/SKU/item/物料分类1~3名称筛选。 |
|
|
|
*/ |
|
|
|
@Override |
|
|
|
public List<QcPartAttributeTemplateCopyData> listPartAttributeTemplateCopyTargetParts(QcPartAttributeTemplateCopyData data) { |
|
|
|
normalizeAndValidateCopyContext(data, true); |
|
|
|
trimCopyQueryFields(data); |
|
|
|
return qcMapper.listPartAttributeTemplateCopyTargetParts(data); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 校验目标物料中已存在模板的冲突数据。 |
|
|
|
* |
|
|
|
* 返回值用途: |
|
|
|
* - 前端据此展示“以下物料已经有检验模板,是否覆盖”的确认提示。 |
|
|
|
*/ |
|
|
|
@Override |
|
|
|
public List<QcPartAttributeTemplateCopyData> checkPartAttributeTemplateCopyConflicts(QcPartAttributeTemplateCopyData data) { |
|
|
|
normalizeAndValidateCopyContext(data, true); |
|
|
|
List<String> targetPartNos = normalizeCopyTargetPartNos(data.getTargetPartNos(), data.getSourcePartNo()); |
|
|
|
if (targetPartNos.isEmpty()) { |
|
|
|
throw new RuntimeException("请至少选择一个目标物料"); |
|
|
|
} |
|
|
|
data.setTargetPartNos(targetPartNos); |
|
|
|
validateAllTargetPartsEnabled(data, targetPartNos); |
|
|
|
return qcMapper.listPartAttributeTemplateCopyConflicts(data); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 执行“模板复制”。 |
|
|
|
* |
|
|
|
* 业务流程: |
|
|
|
* 1) 校验源物料是否存在模板主记录; |
|
|
|
* 2) 校验目标物料均为在用物料,且不包含源物料; |
|
|
|
* 3) 非覆盖模式:若任一目标已有模板则直接拦截; |
|
|
|
* 4) 覆盖模式:先删目标模板主从记录,再复制源模板主从记录; |
|
|
|
* 5) 若目标物料尚未建立 qc_part_attribute 主记录,则自动补建,保证后续页面可见。 |
|
|
|
*/ |
|
|
|
@Override |
|
|
|
@Transactional |
|
|
|
public void copyPartAttributeTemplateToParts(QcPartAttributeTemplateCopyData data) { |
|
|
|
normalizeAndValidateCopyContext(data, true); |
|
|
|
String submitBy = StringUtils.trimToEmpty(data.getSubmitBy()); |
|
|
|
if (StringUtils.isBlank(submitBy)) { |
|
|
|
throw new RuntimeException("操作人不能为空"); |
|
|
|
} |
|
|
|
|
|
|
|
List<String> targetPartNos = normalizeCopyTargetPartNos(data.getTargetPartNos(), data.getSourcePartNo()); |
|
|
|
if (targetPartNos.isEmpty()) { |
|
|
|
throw new RuntimeException("请至少选择一个目标物料"); |
|
|
|
} |
|
|
|
data.setTargetPartNos(targetPartNos); |
|
|
|
|
|
|
|
// 防止前端绕过筛选条件手工构造请求:后端再次校验目标物料必须真实存在且为在用。 |
|
|
|
validateAllTargetPartsEnabled(data, targetPartNos); |
|
|
|
|
|
|
|
// 源模板必须存在,否则复制没有业务意义。 |
|
|
|
List<QcTemplateData> sourceTemplateRows = qcMapper.listPartAttributeTemplateMainByPartNo( |
|
|
|
data.getSite(), data.getBuNo(), data.getSourcePartNo()); |
|
|
|
if (sourceTemplateRows == null || sourceTemplateRows.isEmpty()) { |
|
|
|
throw new RuntimeException("源物料未维护检验模板,无法复制"); |
|
|
|
} |
|
|
|
|
|
|
|
List<QcPartAttributeTemplateCopyData> conflictRows = qcMapper.listPartAttributeTemplateCopyConflicts(data); |
|
|
|
boolean overwriteExisting = Boolean.TRUE.equals(data.getOverwriteExisting()); |
|
|
|
if (!overwriteExisting && conflictRows != null && !conflictRows.isEmpty()) { |
|
|
|
throw new RuntimeException("以下物料已经有检验模板,请确认覆盖后重试:" + joinPartNos(conflictRows)); |
|
|
|
} |
|
|
|
|
|
|
|
// 逐个目标复制,保证每个目标物料的主从记录完整迁移。 |
|
|
|
for (String targetPartNo : targetPartNos) { |
|
|
|
QcPartAttributeData targetPartAttribute = buildTargetPartAttributeForCopy(data, targetPartNo, submitBy); |
|
|
|
QcPartAttributeData existsPartAttribute = qcMapper.selectPartAttributeByNo(targetPartAttribute); |
|
|
|
if (existsPartAttribute == null) { |
|
|
|
// 补建物料属性主记录,避免“复制后在物料属性列表不可见”。 |
|
|
|
qcMapper.qcPartAttributeSave(targetPartAttribute); |
|
|
|
} |
|
|
|
|
|
|
|
if (overwriteExisting) { |
|
|
|
// 覆盖模式必须先删旧记录,避免历史模板残留导致重复/脏数据。 |
|
|
|
qcMapper.deletePartAttributeTemplate(targetPartAttribute); |
|
|
|
qcMapper.deleteQcAttributeTemplateItemByAttribute(targetPartAttribute); |
|
|
|
} |
|
|
|
|
|
|
|
int copiedMainCount = qcMapper.copyPartAttributeTemplateMainToTarget( |
|
|
|
data.getSite(), data.getBuNo(), data.getSourcePartNo(), targetPartNo); |
|
|
|
if (copiedMainCount <= 0) { |
|
|
|
throw new RuntimeException("复制失败:目标物料[" + targetPartNo + "]未生成模板主记录"); |
|
|
|
} |
|
|
|
|
|
|
|
// 子表(检验标准)按源物料覆盖表复制;若源物料未维护覆盖表,系统会回退到模板默认明细读取。 |
|
|
|
qcMapper.copyPartAttributeTemplateItemToTarget( |
|
|
|
data.getSite(), data.getBuNo(), data.getSourcePartNo(), targetPartNo, submitBy); |
|
|
|
|
|
|
|
// 统一刷新目标物料属性的更新时间,便于前端列表和审计追踪。 |
|
|
|
QcTemplateData updateTimeData = new QcTemplateData(); |
|
|
|
updateTimeData.setSite(data.getSite()); |
|
|
|
updateTimeData.setBuNo(data.getBuNo()); |
|
|
|
updateTimeData.setAttributeNo(targetPartNo); |
|
|
|
updateTimeData.setAttributeType(PART_ATTRIBUTE_TYPE_MATERIAL); |
|
|
|
updateTimeData.setUpdateBy(submitBy); |
|
|
|
qcMapper.updatePartAttributeTime(updateTimeData); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 新增物料属性模板 |
|
|
|
**/ |
|
|
|
@ -1295,7 +1407,7 @@ public class QcServiceImpl implements QcService { |
|
|
|
* 3) 校验放在 Service 层且与保存同事务,避免前端并发或绕过页面导致脏数据入库。 |
|
|
|
*/ |
|
|
|
private void validateSingleTemplatePerPartAndInspectionType(QcTemplateData partData) { |
|
|
|
if (!"A".equals(partData.getAttributeType())) { |
|
|
|
if (!PART_ATTRIBUTE_TYPE_MATERIAL.equals(partData.getAttributeType())) { |
|
|
|
return; |
|
|
|
} |
|
|
|
if (StringUtils.isBlank(partData.getInspectionTypeNo())) { |
|
|
|
@ -1311,7 +1423,7 @@ public class QcServiceImpl implements QcService { |
|
|
|
* 首次绑定模板时,将模板默认明细复制到物料模板明细覆盖表。 |
|
|
|
*/ |
|
|
|
private void copyPartAttributeTemplateItemsIfNeed(QcTemplateData partData, QcTemplateData requestData) { |
|
|
|
if (!"A".equals(partData.getAttributeType())) { |
|
|
|
if (!PART_ATTRIBUTE_TYPE_MATERIAL.equals(partData.getAttributeType())) { |
|
|
|
return; |
|
|
|
} |
|
|
|
QcTemplateData query = new QcTemplateData(); |
|
|
|
@ -1340,7 +1452,7 @@ public class QcServiceImpl implements QcService { |
|
|
|
return; |
|
|
|
} |
|
|
|
if (StringUtils.isBlank(data.getAttributeType())) { |
|
|
|
data.setAttributeType("A"); |
|
|
|
data.setAttributeType(PART_ATTRIBUTE_TYPE_MATERIAL); |
|
|
|
} |
|
|
|
Integer bindCount = qcMapper.countPartAttributeTemplateByTemplateKey(data); |
|
|
|
if (bindCount == null || bindCount == 0) { |
|
|
|
@ -1353,6 +1465,141 @@ public class QcServiceImpl implements QcService { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 标准化并校验复制上下文的基础参数。 |
|
|
|
* |
|
|
|
* @param requireSourcePart 是否强制要求传 sourcePartNo(列表查询/校验/复制场景均建议为 true) |
|
|
|
*/ |
|
|
|
private void normalizeAndValidateCopyContext(QcPartAttributeTemplateCopyData data, boolean requireSourcePart) { |
|
|
|
if (data == null) { |
|
|
|
throw new RuntimeException("复制参数不能为空"); |
|
|
|
} |
|
|
|
data.setSite(StringUtils.trimToEmpty(data.getSite())); |
|
|
|
data.setBuNo(StringUtils.trimToEmpty(data.getBuNo())); |
|
|
|
data.setSourcePartNo(StringUtils.trimToEmpty(data.getSourcePartNo())); |
|
|
|
if (StringUtils.isBlank(data.getSite())) { |
|
|
|
throw new RuntimeException("site不能为空"); |
|
|
|
} |
|
|
|
if (StringUtils.isBlank(data.getBuNo())) { |
|
|
|
throw new RuntimeException("buNo不能为空"); |
|
|
|
} |
|
|
|
if (requireSourcePart && StringUtils.isBlank(data.getSourcePartNo())) { |
|
|
|
throw new RuntimeException("源物料编码不能为空"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 统一去除查询字符串两端空格,避免“看起来相同但实际有空格”导致筛选偏差。 |
|
|
|
*/ |
|
|
|
private void trimCopyQueryFields(QcPartAttributeTemplateCopyData data) { |
|
|
|
data.setBasePartNo(StringUtils.trimToEmpty(data.getBasePartNo())); |
|
|
|
data.setPartNo(StringUtils.trimToEmpty(data.getPartNo())); |
|
|
|
data.setPartDesc(StringUtils.trimToEmpty(data.getPartDesc())); |
|
|
|
data.setItem(StringUtils.trimToEmpty(data.getItem())); |
|
|
|
data.setSku(StringUtils.trimToEmpty(data.getSku())); |
|
|
|
data.setCategoryName(StringUtils.trimToEmpty(data.getCategoryName())); |
|
|
|
data.setCategoryLevel2Name(StringUtils.trimToEmpty(data.getCategoryLevel2Name())); |
|
|
|
data.setCategoryLevel3Name(StringUtils.trimToEmpty(data.getCategoryLevel3Name())); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 规范化目标物料编码列表: |
|
|
|
* 1) 去空值; |
|
|
|
* 2) 去重(按不区分大小写); |
|
|
|
* 3) 排除源物料自身。 |
|
|
|
*/ |
|
|
|
private List<String> normalizeCopyTargetPartNos(List<String> rawTargetPartNos, String sourcePartNo) { |
|
|
|
if (rawTargetPartNos == null || rawTargetPartNos.isEmpty()) { |
|
|
|
return Collections.emptyList(); |
|
|
|
} |
|
|
|
String sourceKey = StringUtils.trimToEmpty(sourcePartNo).toUpperCase(Locale.ROOT); |
|
|
|
LinkedHashMap<String, String> normalizedMap = new LinkedHashMap<>(); |
|
|
|
for (String partNo : rawTargetPartNos) { |
|
|
|
String normalizedPartNo = StringUtils.trimToEmpty(partNo); |
|
|
|
if (StringUtils.isBlank(normalizedPartNo)) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
String key = normalizedPartNo.toUpperCase(Locale.ROOT); |
|
|
|
if (StringUtils.isNotBlank(sourceKey) && sourceKey.equals(key)) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
normalizedMap.putIfAbsent(key, normalizedPartNo); |
|
|
|
} |
|
|
|
return new ArrayList<>(normalizedMap.values()); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 校验目标物料是否全部为“在用物料”。 |
|
|
|
* |
|
|
|
* 失败场景: |
|
|
|
* - 目标编码不存在; |
|
|
|
* - 目标编码存在但 status != 'Y'; |
|
|
|
* - 前端绕过页面限制,手工提交非法编码。 |
|
|
|
*/ |
|
|
|
private void validateAllTargetPartsEnabled(QcPartAttributeTemplateCopyData data, List<String> targetPartNos) { |
|
|
|
if (targetPartNos == null || targetPartNos.isEmpty()) { |
|
|
|
throw new RuntimeException("请至少选择一个目标物料"); |
|
|
|
} |
|
|
|
List<QcPartAttributeTemplateCopyData> enabledRows = qcMapper.listEnabledPartRowsByPartNos(data); |
|
|
|
Set<String> enabledPartNoSet = new HashSet<>(); |
|
|
|
if (enabledRows != null) { |
|
|
|
for (QcPartAttributeTemplateCopyData row : enabledRows) { |
|
|
|
if (row != null && StringUtils.isNotBlank(row.getPartNo())) { |
|
|
|
enabledPartNoSet.add(row.getPartNo().trim().toUpperCase(Locale.ROOT)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
List<String> invalidPartNos = new ArrayList<>(); |
|
|
|
for (String targetPartNo : targetPartNos) { |
|
|
|
String key = StringUtils.trimToEmpty(targetPartNo).toUpperCase(Locale.ROOT); |
|
|
|
if (!enabledPartNoSet.contains(key)) { |
|
|
|
invalidPartNos.add(targetPartNo); |
|
|
|
} |
|
|
|
} |
|
|
|
if (!invalidPartNos.isEmpty()) { |
|
|
|
throw new RuntimeException("以下物料不存在或非在用状态,不允许复制:" + String.join("、", invalidPartNos)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 组装目标物料的 qc_part_attribute 主记录。 |
|
|
|
* |
|
|
|
* 注意: |
|
|
|
* - 仅当目标尚未维护主记录时才会插入; |
|
|
|
* - 插入时不覆盖业务备注,默认空字符串,避免误带源物料备注语义。 |
|
|
|
*/ |
|
|
|
private QcPartAttributeData buildTargetPartAttributeForCopy(QcPartAttributeTemplateCopyData data, |
|
|
|
String targetPartNo, |
|
|
|
String submitBy) { |
|
|
|
QcPartAttributeData targetPartAttribute = new QcPartAttributeData(); |
|
|
|
targetPartAttribute.setSite(data.getSite()); |
|
|
|
targetPartAttribute.setBuNo(data.getBuNo()); |
|
|
|
targetPartAttribute.setPartNo(targetPartNo); |
|
|
|
targetPartAttribute.setAttributeType(PART_ATTRIBUTE_TYPE_MATERIAL); |
|
|
|
targetPartAttribute.setAttributeRemark(""); |
|
|
|
targetPartAttribute.setExemptInspection(""); |
|
|
|
targetPartAttribute.setCreateBy(submitBy); |
|
|
|
targetPartAttribute.setUpdateBy(submitBy); |
|
|
|
return targetPartAttribute; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 冲突列表转文本,便于异常提示和日志定位。 |
|
|
|
*/ |
|
|
|
private String joinPartNos(List<QcPartAttributeTemplateCopyData> rows) { |
|
|
|
if (rows == null || rows.isEmpty()) { |
|
|
|
return ""; |
|
|
|
} |
|
|
|
List<String> partNos = rows.stream() |
|
|
|
.filter(Objects::nonNull) |
|
|
|
.map(QcPartAttributeTemplateCopyData::getPartNo) |
|
|
|
.filter(StringUtils::isNotBlank) |
|
|
|
.map(String::trim) |
|
|
|
.distinct() |
|
|
|
.collect(Collectors.toList()); |
|
|
|
return String.join("、", partNos); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 物料属性导入 |
|
|
|
**/ |
|
|
|
|