package cn.iocoder.yudao.module.mes.service.trace.integrity;
|
|
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.crypto.digest.DigestUtil;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.module.mes.controller.admin.trace.integrity.vo.MesTraceIntegrityRecordPageReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.trace.integrity.vo.MesTraceIntegrityVerifyRespVO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.trace.consumer.MesTraceConsumerScanEventDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.trace.integrity.MesTraceIntegrityRecordDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDetailDO;
|
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.trace.consumer.MesTraceConsumerScanEventMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.trace.integrity.MesTraceIntegrityRecordMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesDetailMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesMapper;
|
import cn.iocoder.yudao.module.mes.enums.trace.MesTraceIntegrityBizTypeEnum;
|
import jakarta.annotation.Resource;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.stereotype.Service;
|
import org.springframework.validation.annotation.Validated;
|
|
import java.time.LocalDateTime;
|
import java.util.Comparator;
|
import java.util.List;
|
import java.util.Objects;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.MES_TRACE_INTEGRITY_BIZ_NOT_EXISTS;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.MES_TRACE_INTEGRITY_BIZ_TYPE_INVALID;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.MES_TRACE_INTEGRITY_RECORD_NOT_EXISTS;
|
|
/**
|
* 数据完整性 Service 实现类
|
* <p>
|
* 摘要仅覆盖不可变核心字段,避免正常业务流转(草稿→生产中→完成、出库后状态更新等)导致误报。
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
@Validated
|
@Slf4j
|
public class MesTraceIntegrityServiceImpl implements MesTraceIntegrityService {
|
|
@Resource
|
private MesTraceIntegrityRecordMapper integrityRecordMapper;
|
@Resource
|
private MesProBatchMapper batchMapper;
|
@Resource
|
private MesProBagCodeMapper bagCodeMapper;
|
@Resource
|
private MesWmProductSalesMapper productSalesMapper;
|
@Resource
|
private MesWmProductSalesDetailMapper productSalesDetailMapper;
|
@Resource
|
private MesTraceConsumerScanEventMapper scanEventMapper;
|
|
@Override
|
public void recordBatchIntegrity(Long batchId) {
|
try {
|
MesProBatchDO batch = batchMapper.selectById(batchId);
|
if (batch == null) {
|
return;
|
}
|
upsertRecord(MesTraceIntegrityBizTypeEnum.BATCH.getType(), batch.getId(), batch.getCode(), batchHash(batch));
|
} catch (Exception ex) {
|
log.warn("[recordBatchIntegrity][批次摘要生成失败] batchId={}", batchId, ex);
|
}
|
}
|
|
@Override
|
public void recordSalesIntegrity(Long salesId) {
|
try {
|
MesWmProductSalesDO sales = productSalesMapper.selectById(salesId);
|
if (sales == null) {
|
return;
|
}
|
String hash = salesHash(sales, productSalesDetailMapper.selectListBySalesId(salesId));
|
upsertRecord(MesTraceIntegrityBizTypeEnum.SALES.getType(), sales.getId(), sales.getCode(), hash);
|
} catch (Exception ex) {
|
log.warn("[recordSalesIntegrity][出库摘要生成失败] salesId={}", salesId, ex);
|
}
|
}
|
|
@Override
|
public void recordScanIntegrity(MesTraceConsumerScanEventDO event) {
|
if (event == null || event.getId() == null) {
|
return;
|
}
|
try {
|
upsertRecord(MesTraceIntegrityBizTypeEnum.SCAN.getType(), event.getId(), event.getNormalizedCode(), scanHash(event));
|
} catch (Exception ex) {
|
log.warn("[recordScanIntegrity][扫码摘要生成失败] scanEventId={}", event.getId(), ex);
|
}
|
}
|
|
@Override
|
public MesTraceIntegrityVerifyRespVO verify(String bizType, Long bizId) {
|
if (!MesTraceIntegrityBizTypeEnum.isValid(bizType)) {
|
throw exception(MES_TRACE_INTEGRITY_BIZ_TYPE_INVALID);
|
}
|
MesTraceIntegrityRecordDO record = integrityRecordMapper.selectByBiz(bizType, bizId);
|
if (record == null) {
|
throw exception(MES_TRACE_INTEGRITY_RECORD_NOT_EXISTS);
|
}
|
String currentHash;
|
String bizCode = record.getBizCode();
|
String bagSetHash = null;
|
if (MesTraceIntegrityBizTypeEnum.BATCH.getType().equals(bizType)) {
|
MesProBatchDO batch = batchMapper.selectById(bizId);
|
if (batch == null) {
|
throw exception(MES_TRACE_INTEGRITY_BIZ_NOT_EXISTS);
|
}
|
bizCode = batch.getCode();
|
currentHash = batchHash(batch);
|
bagSetHash = bagSetHash(bizId);
|
} else if (MesTraceIntegrityBizTypeEnum.SALES.getType().equals(bizType)) {
|
MesWmProductSalesDO sales = productSalesMapper.selectById(bizId);
|
if (sales == null) {
|
throw exception(MES_TRACE_INTEGRITY_BIZ_NOT_EXISTS);
|
}
|
bizCode = sales.getCode();
|
currentHash = salesHash(sales, productSalesDetailMapper.selectListBySalesId(bizId));
|
} else {
|
MesTraceConsumerScanEventDO event = scanEventMapper.selectById(bizId);
|
if (event == null) {
|
throw exception(MES_TRACE_INTEGRITY_BIZ_NOT_EXISTS);
|
}
|
bizCode = event.getNormalizedCode();
|
currentHash = scanHash(event);
|
}
|
boolean matched = StrUtil.equals(record.getContentHash(), currentHash);
|
return new MesTraceIntegrityVerifyRespVO()
|
.setBizType(bizType)
|
.setBizId(bizId)
|
.setBizCode(bizCode)
|
.setMatched(matched)
|
.setStoredHash(record.getContentHash())
|
.setCurrentHash(currentHash)
|
.setBagSetHash(bagSetHash)
|
.setMessage(matched ? "数据完整,未被篡改" : "数据不一致,可能存在篡改");
|
}
|
|
@Override
|
public PageResult<MesTraceIntegrityRecordDO> getIntegrityPage(MesTraceIntegrityRecordPageReqVO reqVO) {
|
return integrityRecordMapper.selectPage(reqVO);
|
}
|
|
// ==================== 私有方法 ====================
|
|
/**
|
* 批次摘要:仅覆盖创建后不再变更的核心字段,不含状态、袋码数量等可变字段
|
*/
|
private String batchHash(MesProBatchDO batch) {
|
StringBuilder sb = new StringBuilder();
|
sb.append(StrUtil.nullToEmpty(batch.getCode())).append('|')
|
.append(nvl(batch.getItemId())).append('|')
|
.append(StrUtil.nullToEmpty(batch.getLineCode())).append('|')
|
.append(nvl(batch.getProduceDate())).append('|')
|
.append(nvl(batch.getPlanQuantity()));
|
return DigestUtil.sha256Hex(sb.toString());
|
}
|
|
/**
|
* 出库摘要:明细袋码/托盘码 id 排序后拼入,保证明细顺序不影响摘要
|
*/
|
private String salesHash(MesWmProductSalesDO sales, List<MesWmProductSalesDetailDO> details) {
|
List<Long> codeIds = details.stream()
|
.map(d -> d.getBagCodeId() != null ? d.getBagCodeId() : d.getPalletCodeId())
|
.filter(Objects::nonNull)
|
.sorted(Comparator.naturalOrder())
|
.toList();
|
StringBuilder sb = new StringBuilder();
|
sb.append(StrUtil.nullToEmpty(sales.getCode())).append('|')
|
.append(nvl(sales.getClientId())).append('|')
|
.append(nvl(sales.getSalesDate())).append('|')
|
.append(codeIds);
|
return DigestUtil.sha256Hex(sb.toString());
|
}
|
|
/**
|
* 扫码事件摘要:事件仅 insert 不改,全字段安全
|
*/
|
private String scanHash(MesTraceConsumerScanEventDO event) {
|
StringBuilder sb = new StringBuilder();
|
sb.append(StrUtil.nullToEmpty(event.getNormalizedCode())).append('|')
|
.append(StrUtil.nullToEmpty(event.getSourceType())).append('|')
|
.append(StrUtil.nullToEmpty(event.getProvince())).append('|')
|
.append(StrUtil.nullToEmpty(event.getCity())).append('|')
|
.append(nvl(event.getSuccess())).append('|')
|
.append(nvl(event.getEventTime()));
|
return DigestUtil.sha256Hex(sb.toString());
|
}
|
|
/**
|
* 批次当前袋码集合摘要(实时计算,供人工比对袋码防篡改,不入库)
|
*/
|
private String bagSetHash(Long batchId) {
|
List<String> codes = bagCodeMapper.selectListByBatchId(batchId).stream()
|
.map(MesProBagCodeDO::getCode)
|
.filter(StrUtil::isNotBlank)
|
.sorted()
|
.toList();
|
return DigestUtil.sha256Hex(String.join("|", codes));
|
}
|
|
/**
|
* 摘要不存在时更新,否则覆盖(同一业务记录只保留最新摘要)
|
*/
|
private void upsertRecord(String bizType, Long bizId, String bizCode, String hash) {
|
MesTraceIntegrityRecordDO record = integrityRecordMapper.selectByBiz(bizType, bizId);
|
if (record == null) {
|
integrityRecordMapper.insert(MesTraceIntegrityRecordDO.builder()
|
.bizType(bizType)
|
.bizId(bizId)
|
.bizCode(bizCode)
|
.contentHash(hash)
|
.eventTime(LocalDateTime.now())
|
.build());
|
} else {
|
integrityRecordMapper.updateById(MesTraceIntegrityRecordDO.builder()
|
.id(record.getId())
|
.bizCode(bizCode)
|
.contentHash(hash)
|
.eventTime(LocalDateTime.now())
|
.build());
|
}
|
}
|
|
private String nvl(Object value) {
|
return value == null ? "" : String.valueOf(value);
|
}
|
|
}
|