package cn.iocoder.yudao.module.mes.service.wm.stockwarning;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.module.mes.controller.admin.wm.stockwarning.vo.MesWmStockWarningAlertRespVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.wm.stockwarning.vo.MesWmStockWarningRulePageReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.wm.stockwarning.vo.MesWmStockWarningRuleRespVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.wm.stockwarning.vo.MesWmStockWarningRuleSaveReqVO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stockwarning.MesWmStockWarningRuleDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.stockwarning.MesWmStockWarningRuleMapper;
|
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
|
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
|
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
|
import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
|
import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO;
|
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
|
import jakarta.annotation.Resource;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.stereotype.Service;
|
import org.springframework.validation.annotation.Validated;
|
|
import java.math.BigDecimal;
|
import java.util.*;
|
import java.util.stream.Collectors;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
|
|
/**
|
* MES 库存预警 Service 实现类
|
*/
|
@Service
|
@Validated
|
@Slf4j
|
public class MesWmStockWarningServiceImpl implements MesWmStockWarningService {
|
|
/** 预警类型:低于下限 */
|
private static final Integer WARNING_BELOW_MIN = 1;
|
/** 预警类型:高于上限 */
|
private static final Integer WARNING_ABOVE_MAX = 2;
|
/** 站内信模板编码 */
|
private static final String NOTIFY_TEMPLATE_CODE = "stock_quantity_warning";
|
|
@Resource
|
private MesWmStockWarningRuleMapper ruleMapper;
|
@Resource
|
private MesMdmItemService mdmItemService;
|
@Resource
|
private MesMdUnitMeasureService unitMeasureService;
|
@Resource
|
private MesWmWarehouseService warehouseService;
|
@Resource
|
private NotifyMessageSendApi notifyMessageSendApi;
|
@Resource
|
private AdminUserApi adminUserApi;
|
|
@Override
|
public Long createRule(MesWmStockWarningRuleSaveReqVO createReqVO) {
|
validateItem(createReqVO.getItemId());
|
validateWarehouse(createReqVO.getWarehouseId());
|
MesWmStockWarningRuleDO rule = BeanUtils.toBean(createReqVO, MesWmStockWarningRuleDO.class);
|
ruleMapper.insert(rule);
|
return rule.getId();
|
}
|
|
@Override
|
public void updateRule(MesWmStockWarningRuleSaveReqVO updateReqVO) {
|
validateRuleExists(updateReqVO.getId());
|
validateItem(updateReqVO.getItemId());
|
validateWarehouse(updateReqVO.getWarehouseId());
|
MesWmStockWarningRuleDO updateObj = BeanUtils.toBean(updateReqVO, MesWmStockWarningRuleDO.class);
|
ruleMapper.updateById(updateObj);
|
}
|
|
@Override
|
public void deleteRule(Long id) {
|
validateRuleExists(id);
|
ruleMapper.deleteById(id);
|
}
|
|
@Override
|
public MesWmStockWarningRuleDO getRule(Long id) {
|
return ruleMapper.selectById(id);
|
}
|
|
@Override
|
public PageResult<MesWmStockWarningRuleRespVO> getRulePage(MesWmStockWarningRulePageReqVO pageReqVO) {
|
PageResult<MesWmStockWarningRuleDO> pageResult = ruleMapper.selectPage(pageReqVO);
|
if (CollUtil.isEmpty(pageResult.getList())) {
|
return PageResult.empty(pageResult.getTotal());
|
}
|
List<MesWmStockWarningRuleRespVO> list = BeanUtils.toBean(pageResult.getList(),
|
MesWmStockWarningRuleRespVO.class);
|
// 拼接物料 / 仓库 / 接收人名称
|
Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
|
convertSet(list, MesWmStockWarningRuleRespVO::getItemId));
|
Map<Long, MesWmWarehouseDO> warehouseMap = warehouseService.getWarehouseMap(
|
convertSet(list, MesWmStockWarningRuleRespVO::getWarehouseId));
|
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
|
convertSet(list, MesWmStockWarningRuleRespVO::getNotifyUserId));
|
list.forEach(vo -> {
|
MesMdmItemDO item = itemMap.get(vo.getItemId());
|
if (item != null) {
|
vo.setItemCode(item.getCode());
|
vo.setItemName(item.getName());
|
}
|
MesWmWarehouseDO warehouse = warehouseMap.get(vo.getWarehouseId());
|
if (warehouse != null) {
|
vo.setWarehouseName(warehouse.getName());
|
}
|
AdminUserRespDTO user = userMap.get(vo.getNotifyUserId());
|
if (user != null) {
|
vo.setNotifyUserName(user.getNickname());
|
}
|
});
|
return new PageResult<>(list, pageResult.getTotal());
|
}
|
|
@Override
|
public List<MesWmStockWarningAlertRespVO> getAlertList(Long itemId, Long warehouseId) {
|
// 1. 查询启用中的规则
|
List<MesWmStockWarningRuleDO> rules = ruleMapper.selectList(
|
new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX<MesWmStockWarningRuleDO>()
|
.eqIfPresent(MesWmStockWarningRuleDO::getItemId, itemId)
|
.eqIfPresent(MesWmStockWarningRuleDO::getWarehouseId, warehouseId)
|
.eq(MesWmStockWarningRuleDO::getEnabled, true));
|
if (CollUtil.isEmpty(rules)) {
|
return Collections.emptyList();
|
}
|
// 2. 按物料汇总当前库存
|
List<MesWmStockWarningRuleMapper.StockQuantitySum> sumList =
|
ruleMapper.selectStockQuantitySumList(convertSet(rules, MesWmStockWarningRuleDO::getItemId));
|
Map<Long, BigDecimal> itemTotalMap = new HashMap<>();
|
Map<Long, Map<Long, BigDecimal>> itemWarehouseMap = new HashMap<>();
|
for (MesWmStockWarningRuleMapper.StockQuantitySum sum : sumList) {
|
BigDecimal quantity = sum.getQuantity() != null ? sum.getQuantity() : BigDecimal.ZERO;
|
itemTotalMap.merge(sum.getItemId(), quantity, BigDecimal::add);
|
itemWarehouseMap.computeIfAbsent(sum.getItemId(), key -> new HashMap<>())
|
.merge(sum.getWarehouseId(), quantity, BigDecimal::add);
|
}
|
// 3. 逐条规则判断是否预警
|
Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
|
convertSet(rules, MesWmStockWarningRuleDO::getItemId));
|
// 物料单位来自计量单位表,缺失时前端会显示「--」,这里补齐便于仓管直接判断缺口大小
|
Map<Long, MesMdUnitMeasureDO> unitMap = unitMeasureService.getUnitMeasureMap(
|
convertSet(itemMap.values(), MesMdmItemDO::getUnitMeasureId));
|
Map<Long, MesWmWarehouseDO> warehouseMap = warehouseService.getWarehouseMap(
|
convertSet(rules, MesWmStockWarningRuleDO::getWarehouseId));
|
List<MesWmStockWarningAlertRespVO> alertList = new ArrayList<>();
|
for (MesWmStockWarningRuleDO rule : rules) {
|
BigDecimal current = rule.getWarehouseId() == null
|
? itemTotalMap.getOrDefault(rule.getItemId(), BigDecimal.ZERO)
|
: itemWarehouseMap.getOrDefault(rule.getItemId(), Collections.emptyMap())
|
.getOrDefault(rule.getWarehouseId(), BigDecimal.ZERO);
|
Integer warningType = judgeWarningType(rule, current);
|
if (warningType == null) {
|
continue;
|
}
|
MesWmStockWarningAlertRespVO alert = new MesWmStockWarningAlertRespVO();
|
alert.setRuleId(rule.getId());
|
alert.setItemId(rule.getItemId());
|
alert.setWarehouseId(rule.getWarehouseId());
|
alert.setCurrentQuantity(current);
|
alert.setMinQuantity(rule.getMinQuantity());
|
alert.setMaxQuantity(rule.getMaxQuantity());
|
alert.setWarningType(warningType);
|
MesMdmItemDO item = itemMap.get(rule.getItemId());
|
if (item != null) {
|
alert.setItemCode(item.getCode());
|
alert.setItemName(item.getName());
|
MesMdUnitMeasureDO unit = unitMap.get(item.getUnitMeasureId());
|
if (unit != null) {
|
alert.setUnitName(unit.getName());
|
}
|
}
|
MesWmWarehouseDO warehouse = warehouseMap.get(rule.getWarehouseId());
|
if (warehouse != null) {
|
alert.setWarehouseName(warehouse.getName());
|
}
|
alertList.add(alert);
|
}
|
// 4. 先低于下限、再高于上限,便于优先处理缺货
|
alertList.sort(Comparator.comparing(MesWmStockWarningAlertRespVO::getWarningType)
|
.thenComparing(MesWmStockWarningAlertRespVO::getItemName,
|
Comparator.nullsLast(Comparator.naturalOrder())));
|
return alertList;
|
}
|
|
@Override
|
public void checkAfterStockChange(Long itemId, Long warehouseId, BigDecimal deltaQuantity) {
|
if (itemId == null || warehouseId == null) {
|
return;
|
}
|
List<MesWmStockWarningRuleDO> rules = ruleMapper.selectListByItemIds(Collections.singletonList(itemId));
|
// 只保留「全部仓库」或该仓库的规则
|
rules = rules.stream()
|
.filter(rule -> rule.getWarehouseId() == null || rule.getWarehouseId().equals(warehouseId))
|
.collect(Collectors.toList());
|
if (CollUtil.isEmpty(rules)) {
|
return;
|
}
|
// 重新汇总变动后的库存
|
Map<Long, BigDecimal> itemTotalMap = new HashMap<>();
|
Map<Long, BigDecimal> warehouseTotalMap = new HashMap<>();
|
for (MesWmStockWarningRuleMapper.StockQuantitySum sum :
|
ruleMapper.selectStockQuantitySumList(Collections.singletonList(itemId))) {
|
BigDecimal quantity = sum.getQuantity() != null ? sum.getQuantity() : BigDecimal.ZERO;
|
itemTotalMap.merge(itemId, quantity, BigDecimal::add);
|
if (warehouseId.equals(sum.getWarehouseId())) {
|
warehouseTotalMap.merge(warehouseId, quantity, BigDecimal::add);
|
}
|
}
|
BigDecimal afterTotal = itemTotalMap.getOrDefault(itemId, BigDecimal.ZERO);
|
BigDecimal afterWarehouse = warehouseTotalMap.getOrDefault(warehouseId, BigDecimal.ZERO);
|
BigDecimal delta = deltaQuantity != null ? deltaQuantity : BigDecimal.ZERO;
|
for (MesWmStockWarningRuleDO rule : rules) {
|
BigDecimal current = rule.getWarehouseId() == null ? afterTotal : afterWarehouse;
|
Integer warningType = judgeWarningType(rule, current);
|
if (warningType == null) {
|
continue;
|
}
|
// 变动前数量 = 变动后数量 - 本次变动量;仅在「本次变动导致越界」时通知,避免每次出入库都重复推送
|
BigDecimal before = current.subtract(delta);
|
if (!isCrossed(rule, before, current)) {
|
continue;
|
}
|
notifyWarning(rule, current, warningType);
|
}
|
}
|
|
// ==================== 私有方法 ====================
|
|
private Integer judgeWarningType(MesWmStockWarningRuleDO rule, BigDecimal current) {
|
if (current == null) {
|
return null;
|
}
|
if (rule.getMinQuantity() != null && current.compareTo(rule.getMinQuantity()) < 0) {
|
return WARNING_BELOW_MIN;
|
}
|
if (rule.getMaxQuantity() != null && current.compareTo(rule.getMaxQuantity()) > 0) {
|
return WARNING_ABOVE_MAX;
|
}
|
return null;
|
}
|
|
/**
|
* 判断变动前是否在阈值内、变动后越界,即「本次变动导致越界」
|
*/
|
private boolean isCrossed(MesWmStockWarningRuleDO rule, BigDecimal before, BigDecimal after) {
|
if (rule.getMinQuantity() != null && after.compareTo(rule.getMinQuantity()) < 0) {
|
return before.compareTo(rule.getMinQuantity()) >= 0;
|
}
|
if (rule.getMaxQuantity() != null && after.compareTo(rule.getMaxQuantity()) > 0) {
|
return before.compareTo(rule.getMaxQuantity()) <= 0;
|
}
|
return false;
|
}
|
|
private void notifyWarning(MesWmStockWarningRuleDO rule, BigDecimal current, Integer warningType) {
|
try {
|
// 1. 确定接收人:规则指定 > 仓库负责人
|
Long recipientId = rule.getNotifyUserId();
|
if (recipientId == null && rule.getWarehouseId() != null) {
|
MesWmWarehouseDO warehouse = warehouseService.getWarehouse(rule.getWarehouseId());
|
recipientId = warehouse != null ? warehouse.getChargeUserId() : null;
|
}
|
if (recipientId == null) {
|
log.warn("库存预警[规则{}]触发,但未配置通知接收人且仓库无负责人,已跳过站内通知", rule.getId());
|
return;
|
}
|
// 2. 组装通知内容
|
MesMdmItemDO item = mdmItemService.getItemMap(Collections.singletonList(rule.getItemId()))
|
.get(rule.getItemId());
|
String itemName = item != null ? item.getName() : "ID=" + rule.getItemId();
|
String warehouseName = "全部仓库合计";
|
if (rule.getWarehouseId() != null) {
|
MesWmWarehouseDO warehouse = warehouseService.getWarehouse(rule.getWarehouseId());
|
warehouseName = warehouse != null ? warehouse.getName() : "ID=" + rule.getWarehouseId();
|
}
|
Map<String, Object> params = new HashMap<>();
|
params.put("itemName", itemName);
|
params.put("warehouseName", warehouseName);
|
params.put("actual", current.stripTrailingZeros().toPlainString());
|
params.put("min", rule.getMinQuantity() != null
|
? rule.getMinQuantity().stripTrailingZeros().toPlainString() : "-");
|
params.put("max", rule.getMaxQuantity() != null
|
? rule.getMaxQuantity().stripTrailingZeros().toPlainString() : "-");
|
params.put("warningType", WARNING_BELOW_MIN.equals(warningType) ? "低于库存下限" : "高于库存上限");
|
NotifySendSingleToUserReqDTO message = new NotifySendSingleToUserReqDTO();
|
message.setUserId(recipientId);
|
message.setTemplateCode(NOTIFY_TEMPLATE_CODE);
|
message.setTemplateParams(params);
|
notifyMessageSendApi.sendSingleMessageToAdmin(message);
|
} catch (Exception ex) {
|
// 通知失败不影响业务事务
|
log.warn("库存预警通知发送失败 rule={} item={}", rule.getId(), rule.getItemId(), ex);
|
}
|
}
|
|
private void validateRuleExists(Long id) {
|
MesWmStockWarningRuleDO rule = ruleMapper.selectById(id);
|
if (rule == null) {
|
throw exception(WM_STOCK_WARNING_RULE_NOT_EXISTS);
|
}
|
}
|
|
private void validateItem(Long itemId) {
|
MesMdmItemDO item = mdmItemService.getItemMap(Collections.singletonList(itemId)).get(itemId);
|
if (item == null) {
|
throw new ServiceException(WM_STOCK_WARNING_ITEM_NOT_EXISTS.getCode(),
|
"所选物料[ID=" + itemId + "]不存在或已被删除,请重新选择物料。");
|
}
|
}
|
|
private void validateWarehouse(Long warehouseId) {
|
if (warehouseId == null) {
|
return;
|
}
|
MesWmWarehouseDO warehouse = warehouseService.getWarehouse(warehouseId);
|
if (warehouse == null) {
|
throw new ServiceException(WM_STOCK_WARNING_WAREHOUSE_NOT_EXISTS.getCode(),
|
"所选仓库[ID=" + warehouseId + "]不存在或已被删除,请重新选择仓库。");
|
}
|
}
|
|
}
|