|
|
|
|
@ -1,5 +1,8 @@
|
|
|
|
|
package org.dromara.qms.service.impl;
|
|
|
|
|
|
|
|
|
|
import jakarta.annotation.PostConstruct;
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
import org.dromara.common.core.exception.ServiceException;
|
|
|
|
|
import org.dromara.common.core.utils.MapstructUtils;
|
|
|
|
|
import org.dromara.common.core.utils.StringUtils;
|
|
|
|
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|
|
|
|
@ -20,6 +23,7 @@ import org.dromara.qms.service.IQcInspectionTemplateService;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.Collection;
|
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 检测模板主Service业务层处理
|
|
|
|
|
@ -27,12 +31,20 @@ import java.util.Collection;
|
|
|
|
|
* @author zch
|
|
|
|
|
* @date 2025-07-14
|
|
|
|
|
*/
|
|
|
|
|
@Slf4j
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
@Service
|
|
|
|
|
public class QcInspectionTemplateServiceImpl implements IQcInspectionTemplateService {
|
|
|
|
|
|
|
|
|
|
private final QcInspectionTemplateMapper baseMapper;
|
|
|
|
|
|
|
|
|
|
// ========== 应用级缓存:所有质检模板 ==========
|
|
|
|
|
// 应用启动时一次性加载,避免运行时频繁查询数据库
|
|
|
|
|
private volatile List<QcInspectionTemplateVo> templateCache;
|
|
|
|
|
|
|
|
|
|
// 按检测类型分组的缓存(加速查找)
|
|
|
|
|
private volatile Map<String, List<QcInspectionTemplateVo>> templatesByTypeCache;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 查询检测模板主
|
|
|
|
|
*
|
|
|
|
|
@ -93,6 +105,7 @@ public class QcInspectionTemplateServiceImpl implements IQcInspectionTemplateSer
|
|
|
|
|
.like(StringUtils.isNotBlank(bo.getSupplierName()), QcInspectionTemplate::getSupplierName, bo.getSupplierName())
|
|
|
|
|
.eq(StringUtils.isNotBlank(bo.getDescription()), QcInspectionTemplate::getDescription, bo.getDescription())
|
|
|
|
|
.eq(StringUtils.isNotBlank(bo.getQcInspectionType()), QcInspectionType::getQcInspectionType, bo.getQcInspectionType())
|
|
|
|
|
.eq(StringUtils.isNotBlank(bo.getIsDefault()), QcInspectionTemplate::getIsDefault, bo.getIsDefault())
|
|
|
|
|
.orderByDesc(QcInspectionTemplate::getCreateTime);
|
|
|
|
|
return lqw;
|
|
|
|
|
}
|
|
|
|
|
@ -110,6 +123,7 @@ public class QcInspectionTemplateServiceImpl implements IQcInspectionTemplateSer
|
|
|
|
|
boolean flag = baseMapper.insert(add) > 0;
|
|
|
|
|
if (flag) {
|
|
|
|
|
bo.setTemplateId(add.getTemplateId());
|
|
|
|
|
refreshCache(); // 刷新应用级缓存
|
|
|
|
|
}
|
|
|
|
|
return flag;
|
|
|
|
|
}
|
|
|
|
|
@ -124,14 +138,178 @@ public class QcInspectionTemplateServiceImpl implements IQcInspectionTemplateSer
|
|
|
|
|
public Boolean updateByBo(QcInspectionTemplateBo bo) {
|
|
|
|
|
QcInspectionTemplate update = MapstructUtils.convert(bo, QcInspectionTemplate.class);
|
|
|
|
|
validEntityBeforeSave(update);
|
|
|
|
|
return baseMapper.updateById(update) > 0;
|
|
|
|
|
boolean flag = baseMapper.updateById(update) > 0;
|
|
|
|
|
if (flag) {
|
|
|
|
|
refreshCache(); // 刷新应用级缓存
|
|
|
|
|
}
|
|
|
|
|
return flag;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 保存前的数据校验
|
|
|
|
|
*/
|
|
|
|
|
private void validEntityBeforeSave(QcInspectionTemplate entity){
|
|
|
|
|
//TODO 做一些数据校验,如唯一约束
|
|
|
|
|
// 校验1:通用模板不能绑定物料/工序/工位
|
|
|
|
|
if ("1".equals(entity.getIsDefault())) {
|
|
|
|
|
boolean hasBinding = StringUtils.isNotBlank(entity.getMaterialCode())
|
|
|
|
|
|| StringUtils.isNotBlank(entity.getStationCode())
|
|
|
|
|
|| StringUtils.isNotBlank(entity.getProcessCode());
|
|
|
|
|
if (hasBinding) {
|
|
|
|
|
throw new ServiceException("通用模板不能绑定具体物料、工序或工位!");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 校验2:每种检测类型只能有一个通用模板
|
|
|
|
|
if ("1".equals(entity.getIsDefault()) && entity.getTypeId() != null) {
|
|
|
|
|
QcInspectionTemplateBo checkBo = new QcInspectionTemplateBo();
|
|
|
|
|
checkBo.setTypeId(entity.getTypeId());
|
|
|
|
|
checkBo.setIsDefault("1");
|
|
|
|
|
List<QcInspectionTemplateVo> existingDefaults = queryList(checkBo);
|
|
|
|
|
boolean hasOtherDefault = existingDefaults.stream()
|
|
|
|
|
.anyMatch(vo -> !vo.getTemplateId().equals(entity.getTemplateId()));
|
|
|
|
|
if (hasOtherDefault) {
|
|
|
|
|
throw new ServiceException("该检测类型已存在通用模板!");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 多级降级匹配策略获取质检模板(应用层缓存版,零数据库查询)
|
|
|
|
|
* 匹配优先级:物料+工位+工序 > 物料+工位 > 物料+工序 > 物料 > 工位+工序 > 工位 > 工序 > 通用模板
|
|
|
|
|
* 赋分规则:物料=200, 工位=20, 工序=2, 通用模板=0(兜底)
|
|
|
|
|
*
|
|
|
|
|
* @param materialCode 物料编码
|
|
|
|
|
* @param stationCode 工位编码
|
|
|
|
|
* @param processCode 工序编码
|
|
|
|
|
* @param qcInspectionType 检测类型(首检=0, 专检=1, 自检=2, 互检=3, 原材料检=4, 抽检=5, 成品检=6, 入库检=7,出库检=8)
|
|
|
|
|
* @return 匹配的质检模板
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public QcInspectionTemplateVo getMatchedTemplate(String materialCode, String stationCode,
|
|
|
|
|
String processCode, String qcInspectionType) {
|
|
|
|
|
if (StringUtils.isBlank(qcInspectionType)) {
|
|
|
|
|
throw new ServiceException("检测类型不能为空!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确保缓存已初始化
|
|
|
|
|
if (templatesByTypeCache == null) {
|
|
|
|
|
synchronized (this) {
|
|
|
|
|
if (templatesByTypeCache == null) {
|
|
|
|
|
refreshCache();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 从缓存获取该检测类型的所有模板
|
|
|
|
|
List<QcInspectionTemplateVo> typeTemplates = templatesByTypeCache.get(qcInspectionType);
|
|
|
|
|
if (typeTemplates == null || typeTemplates.isEmpty()) {
|
|
|
|
|
throw new ServiceException("检测类型[" + qcInspectionType + "]无可用检测模板!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 【修复】阶段1:在非通用模板中找最佳匹配(分数>0)
|
|
|
|
|
QcInspectionTemplateVo bestMatch = typeTemplates.stream()
|
|
|
|
|
.filter(t -> !"1".equals(t.getIsDefault())) // 排除通用模板
|
|
|
|
|
.max((t1, t2) -> {
|
|
|
|
|
int score1 = calculateMatchScore(t1, materialCode, stationCode, processCode);
|
|
|
|
|
int score2 = calculateMatchScore(t2, materialCode, stationCode, processCode);
|
|
|
|
|
return Integer.compare(score1, score2);
|
|
|
|
|
})
|
|
|
|
|
.filter(t -> calculateMatchScore(t, materialCode, stationCode, processCode) > 0)
|
|
|
|
|
.orElse(null);
|
|
|
|
|
|
|
|
|
|
// 【修复】阶段2:如果没有非通用模板匹配,使用通用模板兜底
|
|
|
|
|
if (bestMatch == null) {
|
|
|
|
|
bestMatch = typeTemplates.stream()
|
|
|
|
|
.filter(t -> "1".equals(t.getIsDefault())) // 只查找通用模板
|
|
|
|
|
.findFirst()
|
|
|
|
|
.orElse(null);
|
|
|
|
|
|
|
|
|
|
if (bestMatch != null) {
|
|
|
|
|
log.info("使用通用模板兜底:{},检测类型:{}", bestMatch.getTemplateName(), qcInspectionType);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (bestMatch == null) {
|
|
|
|
|
throw new ServiceException("[" +
|
|
|
|
|
(StringUtils.isNotBlank(materialCode) ? "物料=" + materialCode + " " : "") +
|
|
|
|
|
(StringUtils.isNotBlank(stationCode) ? "工位=" + stationCode + " " : "") +
|
|
|
|
|
(StringUtils.isNotBlank(processCode) ? "工序=" + processCode + " " : "") +
|
|
|
|
|
"检测类型=" + qcInspectionType + "]无匹配模板!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int score = calculateMatchScore(bestMatch, materialCode, stationCode, processCode);
|
|
|
|
|
log.info("匹配到模板:{},分数:{}", bestMatch.getTemplateName(), score);
|
|
|
|
|
return bestMatch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 计算非通用模板的匹配分数
|
|
|
|
|
* 赋分规则:物料=200, 工位=20, 工序=2, 通用模板=0(不参与分数计算,单独处理)
|
|
|
|
|
*
|
|
|
|
|
* 分数计算示例:
|
|
|
|
|
* - 物料+工位+工序 = 200+20+2 = 222分(最高优先级)
|
|
|
|
|
* - 物料+工位 = 200+20 = 220分
|
|
|
|
|
* - 物料+工序 = 200+2 = 202分
|
|
|
|
|
* - 物料 = 200 = 200分
|
|
|
|
|
* - 工位+工序 = 20+2 = 22分
|
|
|
|
|
* - 工位 = 20 = 20分
|
|
|
|
|
* - 工序 = 2 = 2分
|
|
|
|
|
* - 通用模板 = 0 = 0分(兜底,不参与分数比较)
|
|
|
|
|
*/
|
|
|
|
|
private int calculateMatchScore(QcInspectionTemplateVo template,
|
|
|
|
|
String materialCode, String stationCode, String processCode) {
|
|
|
|
|
// 通用模板返回0分,不参与分数计算,单独作为兜底策略
|
|
|
|
|
if ("1".equals(template.getIsDefault())) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int score = 0;
|
|
|
|
|
|
|
|
|
|
// 物料匹配(最高权重:200分)
|
|
|
|
|
if (StringUtils.isNotBlank(materialCode) && materialCode.equals(template.getMaterialCode())) {
|
|
|
|
|
score += 200;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 工位匹配(中等权重:20分)
|
|
|
|
|
if (StringUtils.isNotBlank(stationCode) && stationCode.equals(template.getStationCode())) {
|
|
|
|
|
score += 20;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 工序匹配(较低权重:2分)
|
|
|
|
|
if (StringUtils.isNotBlank(processCode) && processCode.equals(template.getProcessCode())) {
|
|
|
|
|
score += 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return score;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 初始化缓存(应用启动时自动调用)
|
|
|
|
|
*/
|
|
|
|
|
@PostConstruct
|
|
|
|
|
public synchronized void initCache() {
|
|
|
|
|
refreshCache();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 刷新缓存(模板增删改时调用)
|
|
|
|
|
*/
|
|
|
|
|
public synchronized void refreshCache() {
|
|
|
|
|
log.info("开始加载质检模板缓存...");
|
|
|
|
|
|
|
|
|
|
// 一次性查询所有模板
|
|
|
|
|
QcInspectionTemplateBo bo = new QcInspectionTemplateBo();
|
|
|
|
|
templateCache = queryList(bo);
|
|
|
|
|
|
|
|
|
|
// 按检测类型分组,加速后续查找
|
|
|
|
|
templatesByTypeCache = templateCache.stream()
|
|
|
|
|
.collect(Collectors.groupingBy(t -> {
|
|
|
|
|
String type = t.getQcInspectionType();
|
|
|
|
|
return StringUtils.isNotBlank(type) ? type : "";
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
log.info("质检模板缓存加载完成,共{}条,检测类型数:{}",
|
|
|
|
|
templateCache.size(), templatesByTypeCache.size());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -146,6 +324,10 @@ public class QcInspectionTemplateServiceImpl implements IQcInspectionTemplateSer
|
|
|
|
|
if(isValid){
|
|
|
|
|
//TODO 做一些业务上的校验,判断是否需要校验
|
|
|
|
|
}
|
|
|
|
|
return baseMapper.deleteByIds(ids) > 0;
|
|
|
|
|
boolean flag = baseMapper.deleteByIds(ids) > 0;
|
|
|
|
|
if (flag) {
|
|
|
|
|
refreshCache(); // 刷新应用级缓存
|
|
|
|
|
}
|
|
|
|
|
return flag;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|