云
2 天以前 87733eb3623593da247c051593d64b8ff9e1d3a4
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
package cn.iocoder.yudao.module.mes.service.pro.scan;
 
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventCreateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.scan.MesProScanEventDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.scan.MesProScanEventMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.MesProScanEventStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.framework.qrcode.MesTraceCodeUtils;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
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.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
 
@Slf4j
@Service
@Validated
public class MesProScanEventServiceImpl implements MesProScanEventService {
    @Resource private MesProScanEventMapper eventMapper;
    @Resource private MesProBagCodeMapper bagCodeMapper;
    @Resource private MesProBatchMapper batchMapper;
    @Resource private MesProPalletMapper palletMapper;
    @Resource private MesWmTransactionService transactionService;
    @Resource private MesWmWarehouseService warehouseService;
    @Resource private MesWmBatchService mesWmBatchService;
 
    /** 扫码类型:袋码(默认) */
    private static final String SCAN_TYPE_BAG = "BAG";
    /** 扫码类型:托盘码(按托盘已装袋数整托入库) */
    private static final String SCAN_TYPE_PALLET = "PALLET";
    /** 扫码类型:批次码(按批次袋数整批入库) */
    private static final String SCAN_TYPE_BATCH = "BATCH";
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProScanEventDO createInbound(MesProScanEventCreateReqVO req) {
        // 追溯二维码内容是「追溯网址?code=码值」,扫码枪也可能直传整段网址,先统一提取码值
        String scanCode = MesTraceCodeUtils.extractCode(req.getBagCode());
        String scanType = (req.getScanType() == null || req.getScanType().isBlank()) ? SCAN_TYPE_BAG : req.getScanType();
        if (!List.of(SCAN_TYPE_BAG, SCAN_TYPE_PALLET, SCAN_TYPE_BATCH).contains(scanType)) {
            throw new ServiceException(400, "不支持的扫码类型");
        }
        MesProScanEventDO existing = eventMapper.selectByEventIdForUpdate(req.getEventId());
        if (existing != null) {
            boolean contentMatch = scanCode.equals(existing.getBagCode())
                    && Objects.equals(req.getDeviceCode(), existing.getDeviceCode())
                    && Objects.equals(req.getLineCode(), existing.getLineCode())
                    && Objects.equals(scanType, existing.getScanType())
                    && Objects.equals(req.getLocationId(), existing.getLocationId())
                    && Objects.equals(req.getAreaId(), existing.getAreaId())
                    && (!SCAN_TYPE_BAG.equals(scanType) || (existing.getQuantity() != null
                        && req.getQuantity().compareTo(existing.getQuantity()) == 0))
                    && Objects.equals(String.valueOf(SecurityFrameworkUtils.getLoginUserId()), existing.getCreator());
            if (!contentMatch) {
                throw new ServiceException(409, "事件号已存在但内容或操作人不一致,请核实原操作");
            }
            return existing;
        }
        MesProScanEventDO event = new MesProScanEventDO();
        event.setEventId(req.getEventId()).setBagCode(scanCode).setScanType(scanType)
                .setQuantity(req.getQuantity()).setDeviceCode(req.getDeviceCode()).setLineCode(req.getLineCode())
                .setRemark(req.getRemark()).setLocationId(req.getLocationId()).setAreaId(req.getAreaId())
                .setStatus(MesProScanEventStatusEnum.FAILED).setReceivedTime(LocalDateTime.now());
        eventMapper.insert(event);
        // 1. 按扫码类型解析扫码目标与入库数量(托盘按已装袋数、批次按批次袋数整批/整托入库)
        Long targetId;
        Long batchId;
        BigDecimal quantity;
        MesProBatchDO batch;
        // 单袋扫码入库时传递袋码,供库存袋码明细在入库第一时间建立;托盘/批次整批入库为批量,不逐袋建明细
        Long bagCodeId = null;
        String bagCode = null;
        if (SCAN_TYPE_PALLET.equals(scanType)) {
            MesProPalletDO pallet = palletMapper.selectByScanCodeForUpdate(scanCode);
            if (pallet == null) return failed(event, "MES_SCAN_PALLET_NOT_EXISTS", "托盘码不存在");
            if (MesProPalletStatusEnum.CANCELED.getStatus().equals(pallet.getStatus()))
                return failed(event, "MES_SCAN_PALLET_CANCELED", "托盘已作废,不可入库");
            if (pallet.getInboundEventId() != null) return failed(event, "MES_SCAN_PALLET_ALREADY_INBOUND", "托盘已入库");
            batch = batchMapper.selectByIdForUpdate(pallet.getBatchId());
            if (batch == null) return failed(event, "MES_SCAN_BATCH_NOT_EXISTS", "生产批次不存在");
            batchId = batch.getId();
            targetId = pallet.getId();
            event.setPalletId(pallet.getId())
                    .setPalletCode(StrUtil.blankToDefault(pallet.getCode(), pallet.getTempCode()));
            quantity = BigDecimal.valueOf(bagCodeMapper.selectPendingInboundCountByPalletId(pallet.getId()));
        } else if (SCAN_TYPE_BATCH.equals(scanType)) {
            batch = batchMapper.selectByScanCodeForUpdate(scanCode);
            if (batch == null) return failed(event, "MES_SCAN_BATCH_NOT_EXISTS", "生产批次不存在");
            batchId = batch.getId();
            targetId = batch.getId();
            quantity = BigDecimal.valueOf(bagCodeMapper.selectPendingInboundCountByBatchId(batch.getId()));
        } else {
            MesProBagCodeDO bag = bagCodeMapper.selectByScanCodeForUpdate(scanCode);
            if (bag == null) return failed(event, "MES_SCAN_BAG_NOT_EXISTS", "袋码不存在");
            if (MesProBagCodeStatusEnum.CANCELED.getStatus().equals(bag.getStatus()))
                return failed(event, "MES_SCAN_BAG_CANCELED", "袋码已作废,不可入库");
            event.setBagCodeId(bag.getId());
            if (bag.getPalletId() != null) {
                event.setPalletId(bag.getPalletId()).setPalletCode(bag.getPalletCode());
            }
            bagCodeId = bag.getId();
            // 库存流水保持原追溯码为规范码,业务码仅用于查找同一袋,避免双码拆成两份库存。
            bagCode = bag.getCode();
            if (bag.getInboundEventId() != null) return failed(event, "MES_SCAN_BAG_ALREADY_INBOUND", "袋码已入库");
            batch = batchMapper.selectByIdForUpdate(bag.getBatchId());
            if (batch == null) return failed(event, "MES_SCAN_BATCH_NOT_EXISTS", "生产批次不存在");
            batchId = batch.getId();
            targetId = bag.getId();
            quantity = req.getQuantity();
        }
        if (MesProBatchStatusEnum.CANCELED.getStatus().equals(batch.getStatus()))
            return failed(event, "MES_SCAN_BATCH_CANCELED", "生产批次已作废,不可入库");
        if (quantity.signum() <= 0)
            return failed(event, "MES_SCAN_NO_PENDING_BAGS", "没有待入库的有效袋码,请勿重复入库");
        // 批次未补录正式批次号时回退临时批次码,保证任何扫码类型的事件都带出批次码
        event.setBatchId(batchId).setBatchCode(StrUtil.blankToDefault(batch.getCode(), batch.getTempCode()))
                .setItemId(batch.getItemId()).setQuantity(quantity);
        MesWmWarehouseDO warehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE);
        if (warehouse == null) return failed(event, "MES_SCAN_WIP_WAREHOUSE_NOT_EXISTS", "WIP虚拟仓不存在");
        event.setWarehouseId(warehouse.getId()).setLocationId(req.getLocationId()).setAreaId(req.getAreaId());
        try {
            // 生产批次桥接仓库批次:库存/流水统一挂在 mes_wm_batch 上,扫码入暂存库待质检
            MesWmBatchDO wmBatch = mesWmBatchService.getOrCreateByProBatch(batch);
            if (wmBatch == null) {
                return failed(event, "MES_SCAN_WM_BATCH_NOT_EXISTS", "仓库批次桥接失败");
            }
            Long txId = transactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                    .setType(MesWmTransactionTypeEnum.IN.getType()).setBizType(MesBizTypeConstants.WM_PRODUCT_PRODUCE)
                    .setBizId(batchId).setBizCode(batch.getCode()).setBizLineId(targetId)
                    .setItemId(batch.getItemId()).setQuantity(quantity)
                    .setBatchId(wmBatch.getId()).setBatchCode(batch.getCode())
                    .setBagCodeId(bagCodeId).setBagCode(bagCode)
                    .setWarehouseId(warehouse.getId()).setLocationId(req.getLocationId()).setAreaId(req.getAreaId())
                    .setReceiptTime(LocalDateTime.now()).setCheckFlag(true)
                    .setStockState(MesWmStockStateEnum.TEMP.getState()));
            event.setTransactionId(txId).setStatus(MesProScanEventStatusEnum.SUCCESS).setEventTime(LocalDateTime.now());
            eventMapper.updateById(event);
            // 2. 回写入库标记(托盘/批次整托/整批入库时同步标记其下未入库袋码,保持状态一致)
            if (SCAN_TYPE_PALLET.equals(scanType)) {
                palletMapper.markInbound(targetId, event.getId());
                bagCodeMapper.markInboundByPalletId(targetId, event.getId(), req.getLineCode());
            } else if (SCAN_TYPE_BATCH.equals(scanType)) {
                bagCodeMapper.markInboundByBatchId(targetId, event.getId(), req.getLineCode());
            } else {
                bagCodeMapper.markInbound(targetId, event.getId(), req.getDeviceCode(), req.getLineCode());
            }
            palletMapper.markCompletedInboundByBatchId(batchId, event.getId());
            batchMapper.refreshInboundProgress(batchId, event.getId(), req.getDeviceCode(), req.getLineCode());
            return event;
        } catch (Exception ex) {
            log.error("[createInbound] 库存事务处理失败,eventId={}, bagCode={}, batchId={}, itemId={}",
                    req.getEventId(), scanCode, batchId, batch.getItemId(), ex);
            throw ex;
        }
    }
 
    /**
     * 为扫码事件补充托盘码:托盘码事件按扫描内容反查托盘,袋码事件取所属托盘;
     * 批次事件整批可能跨多个托盘,不归属单一托盘,保持为空。
     * <p>
     * 托盘码不落库,读取时实时反查,保证历史事件也能展示。
     */
    private void fillPalletInfo(List<MesProScanEventDO> events) {
        if (CollUtil.isEmpty(events)) {
            return;
        }
        // 1. 托盘码事件:扫描内容可能是业务码、正式码或临时码,统一反查
        List<String> palletScanCodes = events.stream()
                .filter(event -> SCAN_TYPE_PALLET.equals(event.getScanType()) && StrUtil.isNotBlank(event.getBagCode()))
                .map(MesProScanEventDO::getBagCode).distinct().toList();
        Map<String, MesProPalletDO> palletByScanCode = new HashMap<>();
        if (CollUtil.isNotEmpty(palletScanCodes)) {
            for (MesProPalletDO pallet : palletMapper.selectListByScanCodes(palletScanCodes)) {
                if (StrUtil.isNotBlank(pallet.getBusinessCode())) {
                    palletByScanCode.put(pallet.getBusinessCode(), pallet);
                }
                if (StrUtil.isNotBlank(pallet.getCode())) {
                    palletByScanCode.put(pallet.getCode(), pallet);
                }
                if (StrUtil.isNotBlank(pallet.getTempCode())) {
                    palletByScanCode.put(pallet.getTempCode(), pallet);
                }
            }
        }
        // 2. 袋码事件:按袋码编号或码值反查袋码,再取所属托盘
        List<Long> bagIds = events.stream()
                .filter(event -> SCAN_TYPE_BAG.equals(event.getScanType()))
                .map(MesProScanEventDO::getBagCodeId).filter(Objects::nonNull).distinct().toList();
        List<String> bagScanCodes = events.stream()
                .filter(event -> SCAN_TYPE_BAG.equals(event.getScanType()) && StrUtil.isNotBlank(event.getBagCode()))
                .map(MesProScanEventDO::getBagCode).distinct().toList();
        Map<Long, MesProBagCodeDO> bagById = new HashMap<>();
        Map<String, MesProBagCodeDO> bagByScanCode = new HashMap<>();
        if (CollUtil.isNotEmpty(bagIds)) {
            for (MesProBagCodeDO bag : bagCodeMapper.selectBatchIds(bagIds)) {
                bagById.put(bag.getId(), bag);
            }
        }
        if (CollUtil.isNotEmpty(bagScanCodes)) {
            for (MesProBagCodeDO bag : bagCodeMapper.selectListByScanCodes(bagScanCodes)) {
                bagByScanCode.putIfAbsent(bag.getCode(), bag);
                if (StrUtil.isNotBlank(bag.getBusinessCode())) {
                    bagByScanCode.putIfAbsent(bag.getBusinessCode(), bag);
                }
            }
        }
        // 3. 袋码冗余托盘码为空时,回查托盘取正式码/临时码
        Set<Long> missingPalletIds = new HashSet<>();
        for (MesProBagCodeDO bag : bagById.values()) {
            if (bag.getPalletId() != null && StrUtil.isBlank(bag.getPalletCode())) {
                missingPalletIds.add(bag.getPalletId());
            }
        }
        for (MesProBagCodeDO bag : bagByScanCode.values()) {
            if (bag.getPalletId() != null && StrUtil.isBlank(bag.getPalletCode())) {
                missingPalletIds.add(bag.getPalletId());
            }
        }
        Map<Long, MesProPalletDO> palletById = new HashMap<>();
        if (CollUtil.isNotEmpty(missingPalletIds)) {
            for (MesProPalletDO pallet : palletMapper.selectBatchIds(missingPalletIds)) {
                palletById.put(pallet.getId(), pallet);
            }
        }
        for (MesProScanEventDO event : events) {
            if (StrUtil.isNotBlank(event.getPalletCode())) {
                continue;
            }
            if (SCAN_TYPE_PALLET.equals(event.getScanType())) {
                MesProPalletDO pallet = palletByScanCode.get(event.getBagCode());
                if (pallet != null) {
                    event.setPalletId(pallet.getId())
                            .setPalletCode(StrUtil.blankToDefault(pallet.getCode(), pallet.getTempCode()));
                }
            } else if (SCAN_TYPE_BAG.equals(event.getScanType())) {
                MesProBagCodeDO bag = event.getBagCodeId() != null
                        ? bagById.get(event.getBagCodeId()) : bagByScanCode.get(event.getBagCode());
                if (bag == null || bag.getPalletId() == null) {
                    continue;
                }
                event.setPalletId(bag.getPalletId());
                if (StrUtil.isNotBlank(bag.getPalletCode())) {
                    event.setPalletCode(bag.getPalletCode());
                } else {
                    MesProPalletDO pallet = palletById.get(bag.getPalletId());
                    if (pallet != null) {
                        event.setPalletCode(StrUtil.blankToDefault(pallet.getCode(), pallet.getTempCode()));
                    }
                }
            }
        }
    }
 
    private void fillPalletInfo(MesProScanEventDO event) {
        if (event != null) {
            fillPalletInfo(List.of(event));
        }
    }
 
    private MesProScanEventDO failed(MesProScanEventDO event, String code, String message) {
        event.setStatus(MesProScanEventStatusEnum.FAILED).setErrorCode(code).setErrorMessage(message)
                .setFailedTime(LocalDateTime.now()).setEventTime(LocalDateTime.now());
        eventMapper.updateById(event);
        return event;
    }
 
    @Override public MesProScanEventDO getByEventId(String eventId) {
        MesProScanEventDO event = eventMapper.selectOne(MesProScanEventDO::getEventId, eventId);
        fillPalletInfo(event);
        return event;
    }
    @Override public PageResult<MesProScanEventDO> getPage(MesProScanEventPageReqVO reqVO) {
        PageResult<MesProScanEventDO> page = eventMapper.selectPage(reqVO);
        fillPalletInfo(page.getList());
        return page;
    }
    @Override public MesProScanEventDO getLatest(String deviceCode) {
        MesProScanEventDO event = eventMapper.selectLatest(deviceCode);
        fillPalletInfo(event);
        return event;
    }
    @Override public List<MesProScanEventDO> getByBatchId(Long batchId) {
        List<MesProScanEventDO> events = eventMapper.selectListByBatchId(batchId);
        fillPalletInfo(events);
        return events;
    }
    @Override public Map<String, Object> getDashboard() {
        Map<String, Object> result = new HashMap<>();
        MesProScanEventDO latest = eventMapper.selectLatest(null);
        fillPalletInfo(latest);
        result.put("latest", latest);
        result.put("total", eventMapper.selectCount(null));
        result.put("success", eventMapper.selectCount(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesProScanEventDO>().eq(MesProScanEventDO::getStatus, MesProScanEventStatusEnum.SUCCESS)));
        result.put("failed", eventMapper.selectCount(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesProScanEventDO>().eq(MesProScanEventDO::getStatus, MesProScanEventStatusEnum.FAILED)));
        return result;
    }
}