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;
|
}
|
}
|