gaoluyang
6 天以前 0d0dd802605c57dad9bbfc05dc791bdbdf541608
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
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 + "]不存在或已被删除,请重新选择仓库。");
        }
    }
 
}