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 实现类 *
* 摘要仅覆盖不可变核心字段,避免正常业务流转(草稿→生产中→完成、出库后状态更新等)导致误报。
*
* @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