云
2 天以前 87733eb3623593da247c051593d64b8ff9e1d3a4
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/scan/MesProScanEventServiceImpl.java
@@ -1,5 +1,7 @@
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;
@@ -17,6 +19,7 @@
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;
@@ -30,8 +33,16 @@
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
@@ -55,20 +66,33 @@
    @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 = req.getBagCode().equals(existing.getBagCode())
                    && (req.getDeviceCode() == null || req.getDeviceCode().equals(existing.getDeviceCode()));
            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) {
                return failed(existing, "MES_SCAN_EVENT_ID_CONFLICT", "事件号已存在但内容不一致");
                throw new ServiceException(409, "事件号已存在但内容或操作人不一致,请核实原操作");
            }
            return existing;
        }
        String scanType = (req.getScanType() == null || req.getScanType().isBlank()) ? SCAN_TYPE_BAG : req.getScanType();
        MesProScanEventDO event = new MesProScanEventDO();
        event.setEventId(req.getEventId()).setBagCode(req.getBagCode()).setScanType(scanType)
        event.setEventId(req.getEventId()).setBagCode(scanCode).setScanType(scanType)
                .setQuantity(req.getQuantity()).setDeviceCode(req.getDeviceCode()).setLineCode(req.getLineCode())
                .setRemark(req.getRemark()).setStatus(MesProScanEventStatusEnum.FAILED).setReceivedTime(LocalDateTime.now());
                .setRemark(req.getRemark()).setLocationId(req.getLocationId()).setAreaId(req.getAreaId())
                .setStatus(MesProScanEventStatusEnum.FAILED).setReceivedTime(LocalDateTime.now());
        eventMapper.insert(event);
        // 1. 按扫码类型解析扫码目标与入库数量(托盘按已装袋数、批次按批次袋数整批/整托入库)
        Long targetId;
@@ -79,37 +103,36 @@
        Long bagCodeId = null;
        String bagCode = null;
        if (SCAN_TYPE_PALLET.equals(scanType)) {
            MesProPalletDO pallet = palletMapper.selectByCode(req.getBagCode());
            if (pallet == null) {
                pallet = palletMapper.selectByTempCode(req.getBagCode());
            }
            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();
            quantity = pallet.getBagCount() != null && pallet.getBagCount() > 0
                    ? BigDecimal.valueOf(pallet.getBagCount())
                    : BigDecimal.valueOf(bagCodeMapper.selectCountByPalletId(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.selectByCode(req.getBagCode());
            if (batch == null) {
                batch = batchMapper.selectByTempCode(req.getBagCode());
            }
            batch = batchMapper.selectByScanCodeForUpdate(scanCode);
            if (batch == null) return failed(event, "MES_SCAN_BATCH_NOT_EXISTS", "生产批次不存在");
            if (batch.getInboundEventId() != null) return failed(event, "MES_SCAN_BATCH_ALREADY_INBOUND", "批次已整批入库");
            batchId = batch.getId();
            targetId = batch.getId();
            quantity = batch.getBagQuantity() != null && batch.getBagQuantity() > 0
                    ? BigDecimal.valueOf(batch.getBagQuantity())
                    : BigDecimal.valueOf(bagCodeMapper.selectCountByBatchId(batch.getId()));
            quantity = BigDecimal.valueOf(bagCodeMapper.selectPendingInboundCountByBatchId(batch.getId()));
        } else {
            MesProBagCodeDO bag = bagCodeMapper.selectByCodeForUpdate(req.getBagCode());
            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 = req.getBagCode();
            // 库存流水保持原追溯码为规范码,业务码仅用于查找同一袋,避免双码拆成两份库存。
            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", "生产批次不存在");
@@ -117,7 +140,13 @@
            targetId = bag.getId();
            quantity = req.getQuantity();
        }
        event.setBatchId(batchId).setBatchCode(batch.getCode()).setItemId(batch.getItemId());
        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());
@@ -143,17 +172,120 @@
                palletMapper.markInbound(targetId, event.getId());
                bagCodeMapper.markInboundByPalletId(targetId, event.getId(), req.getLineCode());
            } else if (SCAN_TYPE_BATCH.equals(scanType)) {
                batchMapper.markBatchInbound(targetId, event.getId(), req.getLineCode(), batch.getBagQuantity());
                bagCodeMapper.markInboundByBatchId(targetId, event.getId(), req.getLineCode());
            } else {
                bagCodeMapper.markInbound(targetId, event.getId(), req.getDeviceCode(), req.getLineCode());
                batchMapper.markInbound(batchId, 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(), req.getBagCode(), batchId, batch.getItemId(), ex);
                    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));
        }
    }
@@ -164,13 +296,31 @@
        return event;
    }
    @Override public MesProScanEventDO getByEventId(String eventId) { return eventMapper.selectOne(MesProScanEventDO::getEventId, eventId); }
    @Override public PageResult<MesProScanEventDO> getPage(MesProScanEventPageReqVO reqVO) { return eventMapper.selectPage(reqVO); }
    @Override public MesProScanEventDO getLatest(String deviceCode) { return eventMapper.selectLatest(deviceCode); }
    @Override public List<MesProScanEventDO> getByBatchId(Long batchId) { return eventMapper.selectListByBatchId(batchId); }
    @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<>();
        result.put("latest", eventMapper.selectLatest(null));
        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)));