package cn.iocoder.yudao.module.mes.service.wm.warning; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo.MesWmStockWarningHandleReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo.MesWmStockWarningPageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.warning.vo.MesWmStockWarningRespVO; import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warning.MesWmStockWarningDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.warning.MesWmStockWarningMapper; import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockWarningTypeEnum; import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService; import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService; import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_STOCK_WARNING_NOT_EXISTS; /** * MES 库存预警 Service 实现类 *

* 低库存、临期、库龄三类预警。只针对合格库(正式库存),暂存库(待质检)与已出库不计入预警。 */ @Service @Validated @Slf4j public class MesWmStockWarningServiceImpl implements MesWmStockWarningService { /** * 未处理 */ private static final Integer STATUS_UNHANDLED = 10; /** * 已处理 */ private static final Integer STATUS_HANDLED = 20; /** * 库龄预警阈值(天) */ private static final int AGING_DAYS = 365; @Resource private MesWmStockWarningMapper warningMapper; @Resource private MesWmMaterialStockService materialStockService; @Resource private MesMdmItemService mdmItemService; @Resource private MesWmWarehouseService warehouseService; @Resource private MesWmBatchService batchService; @Override public PageResult getWarningPage(MesWmStockWarningPageReqVO pageReqVO) { PageResult pageResult = warningMapper.selectPage(pageReqVO); Map itemMap = mdmItemService.getItemMap( convertSet(pageResult.getList(), MesWmStockWarningDO::getItemId)); return new PageResult<>(convertList(pageResult.getList(), warning -> toRespVO(warning, itemMap.get(warning.getItemId()))), pageResult.getTotal()); } @Override public List getCurrentWarnings() { List warnings = computeCurrentWarnings(); if (CollUtil.isEmpty(warnings)) { return List.of(); } Map itemMap = mdmItemService.getItemMap( convertSet(warnings, MesWmStockWarningDO::getItemId)); return convertList(warnings, warning -> toRespVO(warning, itemMap.get(warning.getItemId()))); } @Override @Transactional(rollbackFor = Exception.class) public int persistCurrentWarnings() { // 1. 实时计算当前预警 List current = computeCurrentWarnings(); // 2. 删除旧的未处理预警(已处理记录作为历史保留) warningMapper.delete(new LambdaQueryWrapperX() .eq(MesWmStockWarningDO::getStatus, STATUS_UNHANDLED)); // 3. 写入当前预警 if (CollUtil.isNotEmpty(current)) { current.forEach(warningMapper::insert); } log.info("[persistCurrentWarnings][库存预警落库 {} 条]", current.size()); return current.size(); } @Override public void handle(MesWmStockWarningHandleReqVO reqVO) { MesWmStockWarningDO warning = warningMapper.selectById(reqVO.getId()); if (warning == null) { throw exception(WM_STOCK_WARNING_NOT_EXISTS); } warningMapper.updateById(new MesWmStockWarningDO() .setId(warning.getId()) .setStatus(STATUS_HANDLED) .setHandleUserId(SecurityFrameworkUtils.getLoginUserId()) .setHandleTime(LocalDateTime.now()) .setHandleRemark(reqVO.getHandleRemark())); } // ==================== 私有方法 ==================== /** * 实时计算当前预警(低库存 / 临期 / 库龄),针对合格库存 */ private List computeCurrentWarnings() { List result = new ArrayList<>(); // 仅扫描合格库(正式库存) List stocks = materialStockService.getMaterialStockList( new MesWmMaterialStockListReqVO().setStockState(MesWmStockStateEnum.QUALIFIED.getState())); if (CollUtil.isEmpty(stocks)) { return result; } LocalDateTime now = LocalDateTime.now(); Set itemIds = convertSet(stocks, MesWmMaterialStockDO::getItemId); Map itemMap = mdmItemService.getItemMap(itemIds); Set warehouseIds = convertSet(stocks, MesWmMaterialStockDO::getWarehouseId); Map warehouseMap = warehouseService.getWarehouseMap(warehouseIds); for (MesWmMaterialStockDO stock : stocks) { MesMdmItemDO item = itemMap.get(stock.getItemId()); MesWmWarehouseDO warehouse = warehouseMap.get(stock.getWarehouseId()); if (item == null) { continue; } MesWmBatchDO batch = stock.getBatchId() != null ? batchService.getBatch(stock.getBatchId()) : null; BigDecimal quantity = stock.getQuantity() != null ? stock.getQuantity() : BigDecimal.ZERO; // 1. 低库存:在库数量 <= 最低库存 if (item.getMinStock() != null && quantity.compareTo(item.getMinStock()) <= 0) { result.add(buildWarning(stock, item, warehouse, MesWmStockWarningTypeEnum.LOW_STOCK, item.getMinStock(), quantity, batch != null ? batch.getExpireDate() : null)); } // 2. 临期:批次有有效期,且剩余天数 <= 物料配置的有效期天数 if (item.getExpiryDay() != null && batch != null && batch.getExpireDate() != null) { long remainDays = ChronoUnit.DAYS.between(now.toLocalDate(), batch.getExpireDate().toLocalDate()); if (remainDays <= item.getExpiryDay()) { result.add(buildWarning(stock, item, warehouse, MesWmStockWarningTypeEnum.EXPIRING, BigDecimal.valueOf(item.getExpiryDay()), BigDecimal.valueOf(Math.max(remainDays, 0)), batch.getExpireDate())); } } // 3. 库龄:入库时间超过阈值 if (stock.getReceiptTime() != null) { long ageDays = ChronoUnit.DAYS.between(stock.getReceiptTime().toLocalDate(), now.toLocalDate()); if (ageDays >= AGING_DAYS) { result.add(buildWarning(stock, item, warehouse, MesWmStockWarningTypeEnum.AGING, BigDecimal.valueOf(AGING_DAYS), BigDecimal.valueOf(ageDays), null)); } } } return result; } private MesWmStockWarningDO buildWarning(MesWmMaterialStockDO stock, MesMdmItemDO item, MesWmWarehouseDO warehouse, MesWmStockWarningTypeEnum type, BigDecimal threshold, BigDecimal current, LocalDateTime expireDate) { return MesWmStockWarningDO.builder() .itemId(item.getId()).itemCode(item.getCode()) .batchId(stock.getBatchId()).batchCode(stock.getBatchCode()) .warehouseId(stock.getWarehouseId()) .warehouseName(warehouse != null ? warehouse.getName() : null) .warningType(type.getType()).thresholdValue(threshold).currentValue(current) .expireDate(expireDate).receiptTime(stock.getReceiptTime()) .status(STATUS_UNHANDLED) .build(); } private MesWmStockWarningRespVO toRespVO(MesWmStockWarningDO warning, MesMdmItemDO item) { MesWmStockWarningRespVO vo = new MesWmStockWarningRespVO(); vo.setId(warning.getId()); vo.setItemId(warning.getItemId()); vo.setItemCode(warning.getItemCode()); vo.setItemName(item != null ? item.getName() : null); vo.setBatchId(warning.getBatchId()); vo.setBatchCode(warning.getBatchCode()); vo.setWarehouseId(warning.getWarehouseId()); vo.setWarehouseName(warning.getWarehouseName()); vo.setWarningType(warning.getWarningType()); vo.setWarningTypeName(warningTypeName(warning.getWarningType())); vo.setThresholdValue(warning.getThresholdValue()); vo.setCurrentValue(warning.getCurrentValue()); vo.setExpireDate(warning.getExpireDate()); vo.setReceiptTime(warning.getReceiptTime()); vo.setStatus(warning.getStatus()); vo.setStatusName(statusName(warning.getStatus())); vo.setHandleUserId(warning.getHandleUserId()); vo.setHandleTime(warning.getHandleTime()); vo.setHandleRemark(warning.getHandleRemark()); return vo; } private String warningTypeName(Integer type) { if (type == null) { return ""; } for (MesWmStockWarningTypeEnum e : MesWmStockWarningTypeEnum.values()) { if (e.getType().equals(type)) { return e.getName(); } } return String.valueOf(type); } private String statusName(Integer status) { if (status == null) { return ""; } return status.equals(STATUS_HANDLED) ? "已处理" : "未处理"; } }