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) ? "已处理" : "未处理";
}
}