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 实现类
|
* <p>
|
* 低库存、临期、库龄三类预警。只针对合格库(正式库存),暂存库(待质检)与已出库不计入预警。
|
*/
|
@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<MesWmStockWarningRespVO> getWarningPage(MesWmStockWarningPageReqVO pageReqVO) {
|
PageResult<MesWmStockWarningDO> pageResult = warningMapper.selectPage(pageReqVO);
|
Map<Long, MesMdmItemDO> 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<MesWmStockWarningRespVO> getCurrentWarnings() {
|
List<MesWmStockWarningDO> warnings = computeCurrentWarnings();
|
if (CollUtil.isEmpty(warnings)) {
|
return List.of();
|
}
|
Map<Long, MesMdmItemDO> 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<MesWmStockWarningDO> current = computeCurrentWarnings();
|
// 2. 删除旧的未处理预警(已处理记录作为历史保留)
|
warningMapper.delete(new LambdaQueryWrapperX<MesWmStockWarningDO>()
|
.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<MesWmStockWarningDO> computeCurrentWarnings() {
|
List<MesWmStockWarningDO> result = new ArrayList<>();
|
// 仅扫描合格库(正式库存)
|
List<MesWmMaterialStockDO> stocks = materialStockService.getMaterialStockList(
|
new MesWmMaterialStockListReqVO().setStockState(MesWmStockStateEnum.QUALIFIED.getState()));
|
if (CollUtil.isEmpty(stocks)) {
|
return result;
|
}
|
LocalDateTime now = LocalDateTime.now();
|
Set<Long> itemIds = convertSet(stocks, MesWmMaterialStockDO::getItemId);
|
Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(itemIds);
|
Set<Long> warehouseIds = convertSet(stocks, MesWmMaterialStockDO::getWarehouseId);
|
Map<Long, MesWmWarehouseDO> 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) ? "已处理" : "未处理";
|
}
|
|
}
|