chen.lin
1 天以前 98d88ac8caf7f0991d741079474c262f1e252927
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/WcsServiceImpl.java
@@ -28,12 +28,18 @@
import com.vincent.rsf.server.api.utils.SlaveProperties;
import com.vincent.rsf.server.manager.entity.*;
import com.vincent.rsf.server.manager.service.*;
import com.vincent.rsf.server.manager.controller.params.PakinItem;
import com.vincent.rsf.server.manager.controller.params.WaitPakinParam;
import com.vincent.rsf.server.manager.service.impl.LocServiceImpl;
import com.vincent.rsf.server.system.constant.GlobalConfigCode;
import com.vincent.rsf.server.system.entity.Config;
import com.vincent.rsf.server.system.service.ConfigService;
import com.vincent.rsf.server.system.utils.SystemAuthUtils;
import com.vincent.rsf.server.system.constant.SerialRuleCode;
import com.vincent.rsf.server.manager.enums.LocStsType;
import com.vincent.rsf.server.system.utils.SerialRuleUtils;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -47,6 +53,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@@ -87,6 +94,16 @@
    private RestTemplate restTemplate;
    @Autowired
    private RemotesInfoProperties.RcsApi rcsApi;
    @Autowired
    private ConfigService configService;
    @Autowired
    private MatnrService matnrService;
    @Autowired
    private AsnOrderService asnOrderService;
    @Autowired
    private AsnOrderItemService asnOrderItemService;
    @Autowired
    private com.vincent.rsf.server.api.service.MobileService mobileService;
    @Override
@@ -124,75 +141,79 @@
        // 验证设备站点
        DeviceSite deviceSite = validateDeviceSite(param);
        // 提前定义waitPakin,避免作用域问题
        // 提前定义 waitPakin / waitPakinItems,供后续其他入库逻辑使用
        WaitPakin waitPakin = null;
        List<WaitPakinItem> waitPakinItems = Collections.emptyList();
        // 先验证组拖状态,获取组托明细信息(用于批号匹配和单号检查)
        waitPakin = validateWaitPakin(param.getBarcode());
        List<WaitPakinItem> waitPakinItems = waitPakinItemService.list(
                new LambdaQueryWrapper<WaitPakinItem>()
                        .eq(WaitPakinItem::getPakinId, waitPakin.getId()));
        // 先检查是否有拣料入库任务(需要同时匹配箱号和批号)
        // 1. 先查询任务管理中的拣料入库任务、盘点入库任务(仅按箱号+类型+状态,不依赖组托)
        Task pickInTask = null;
        // 检查是否有盘点入库任务(需要同时匹配箱号和批号)
        Task checkInTask = null;
        if (!waitPakinItems.isEmpty()) {
            // 获取组托明细中的批号列表(去重)
            List<String> batchList = waitPakinItems.stream()
        // 可选:若有组托则取批号,用于多任务时优先按批号匹配
        WaitPakin waitPakinForBatch = waitPakinService.getOne(new LambdaQueryWrapper<WaitPakin>()
                .eq(WaitPakin::getBarcode, param.getBarcode())
                .in(WaitPakin::getIoStatus, PakinIOStatus.PAKIN_IO_STATUS_DONE.val, PakinIOStatus.PAKIN_IO_STATUS_TASK_EXCE.val));
        List<String> batchList = Collections.emptyList();
        if (waitPakinForBatch != null) {
            List<WaitPakinItem> itemsForBatch = waitPakinItemService.list(
                    new LambdaQueryWrapper<WaitPakinItem>().eq(WaitPakinItem::getPakinId, waitPakinForBatch.getId()));
            batchList = itemsForBatch.stream()
                    .map(WaitPakinItem::getBatch)
                    .filter(Objects::nonNull)
                    .filter(batch -> !batch.trim().isEmpty())
                    .distinct()
                    .collect(Collectors.toList());
            if (!batchList.isEmpty()) {
                log.info("检查组托明细批号 - 批号列表:{}", batchList);
                log.info("检查组托明细批号(用于匹配拣料/盘点入库任务) - 批号列表:{}", batchList);
            }
        }
                // 查询拣料入库任务:箱号匹配且状态为1、2或199(RCS申请入库时,状态199需要变成2)
                List<Task> pickInTasks = taskService.list(new LambdaQueryWrapper<Task>()
                        .eq(Task::getBarcode, param.getBarcode())
                        .eq(Task::getTaskType, TaskType.TASK_TYPE_PICK_IN.type)
                        .in(Task::getTaskStatus, TaskStsType.GENERATE_IN.id, TaskStsType.WAVE_SEED.id)
                        .orderByDesc(Task::getCreateTime));
        // 查询任务管理:拣料入库任务(箱号+类型53+状态1/2/199)
        List<Task> pickInTasks = taskService.list(new LambdaQueryWrapper<Task>()
                .eq(Task::getBarcode, param.getBarcode())
                .eq(Task::getTaskType, TaskType.TASK_TYPE_PICK_IN.type)
                .in(Task::getTaskStatus, TaskStsType.GENERATE_IN.id, TaskStsType.WCS_EXECUTE_IN.id, TaskStsType.WAVE_SEED.id)
                .orderByDesc(Task::getCreateTime));
        for (Task task : pickInTasks) {
            if (batchList.isEmpty()) {
                pickInTask = task;
                log.info("找到匹配的拣料入库任务(按箱号) - 任务编码:{},箱号:{}", task.getTaskCode(), param.getBarcode());
                break;
            }
            List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>()
                    .eq(TaskItem::getTaskId, task.getId())
                    .in(TaskItem::getBatch, batchList));
            if (!taskItems.isEmpty()) {
                pickInTask = task;
                log.info("找到匹配的拣料入库任务(箱号和批号都匹配) - 任务编码:{},批号:{}", task.getTaskCode(), batchList);
                break;
            }
        }
                // 通过TaskItem的batch字段匹配批号
                for (Task task : pickInTasks) {
                    List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>()
                            .eq(TaskItem::getTaskId, task.getId())
                            .in(TaskItem::getBatch, batchList));
                    if (!taskItems.isEmpty()) {
                        pickInTask = task;
                        log.info("找到匹配的拣料入库任务(箱号和批号都匹配) - 任务编码:{},批号:{}",
                                task.getTaskCode(), batchList);
                        break;
                    }
        // 查询任务管理:盘点入库任务(箱号+类型+状态1/2/199)
        if (pickInTask == null) {
            List<Task> checkInTasks = taskService.list(new LambdaQueryWrapper<Task>()
                    .eq(Task::getBarcode, param.getBarcode())
                    .eq(Task::getTaskType, TaskType.TASK_TYPE_CHECK_IN.type)
                    .in(Task::getTaskStatus, TaskStsType.GENERATE_IN.id, TaskStsType.WCS_EXECUTE_IN.id, TaskStsType.WAVE_SEED.id)
                    .orderByDesc(Task::getCreateTime));
            for (Task task : checkInTasks) {
                if (batchList.isEmpty()) {
                    checkInTask = task;
                    log.info("找到匹配的盘点入库任务(按箱号) - 任务编码:{},箱号:{}", task.getTaskCode(), param.getBarcode());
                    break;
                }
                // 查询盘点入库任务:箱号匹配且状态为1、2或199(RCS申请入库时,状态199需要变成2)
                List<Task> checkInTasks = taskService.list(new LambdaQueryWrapper<Task>()
                        .eq(Task::getBarcode, param.getBarcode())
                        .eq(Task::getTaskType, TaskType.TASK_TYPE_CHECK_IN.type)
                        .in(Task::getTaskStatus, TaskStsType.GENERATE_IN.id, TaskStsType.WCS_EXECUTE_IN.id, TaskStsType.WAVE_SEED.id)
                        .orderByDesc(Task::getCreateTime));
                // 通过TaskItem的batch字段匹配批号
                for (Task task : checkInTasks) {
                    List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>()
                            .eq(TaskItem::getTaskId, task.getId())
                            .in(TaskItem::getBatch, batchList));
                    if (!taskItems.isEmpty()) {
                        checkInTask = task;
                        log.info("找到匹配的盘点入库任务(箱号和批号都匹配) - 任务编码:{},批号:{}",
                                task.getTaskCode(), batchList);
                        break;
                    }
                List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>()
                        .eq(TaskItem::getTaskId, task.getId())
                        .in(TaskItem::getBatch, batchList));
                if (!taskItems.isEmpty()) {
                    checkInTask = task;
                    log.info("找到匹配的盘点入库任务(箱号和批号都匹配) - 任务编码:{},批号:{}", task.getTaskCode(), batchList);
                    break;
                }
            }
        }
        // 如果是拣料入库任务,直接返回,不校验组托
        if (Objects.nonNull(pickInTask)) {
@@ -300,8 +321,49 @@
            return msgDto;
        }
        // 检查其他入库任务类型(用箱号查询,状态为1或2)
        // 注意:盘点入库已单独处理,不再包含在此列表中
        // 2. 若未命中拣料/盘点入库,再校验组托并继续其他入库逻辑
        if (pickInTask == null && checkInTask == null) {
            String barcode = param.getBarcode();
            // 该托盘已在库存中,不可重复申请入库
            List<Loc> inStock = locService.list(new LambdaQueryWrapper<Loc>().eq(Loc::getBarcode, barcode));
            if (!inStock.isEmpty()) {
                throw new CoolException("barcode=" + barcode + ": 该托盘已在库,不可重复申请入库");
            }
            // 该托盘出库中未完成,不可申请入库
            List<Integer> outboundTaskTypes = Arrays.asList(
                    TaskType.TASK_TYPE_OUT.type,
                    TaskType.TASK_TYPE_PICK_AGAIN_OUT.type,
                    TaskType.TASK_TYPE_MERGE_OUT.type,
                    TaskType.TASK_TYPE_CHECK_OUT.type,
                    TaskType.TASK_TYPE_EMPITY_OUT.type
            );
            Task outboundTask = taskService.getOne(new LambdaQueryWrapper<Task>()
                    .eq(Task::getBarcode, barcode)
                    .in(Task::getTaskType, outboundTaskTypes)
                    .lt(Task::getTaskStatus, TaskStsType.COMPLETE_OUT.id));
            if (outboundTask != null) {
                throw new CoolException("barcode=" + barcode + ": 该托盘出库中未完成,不可申请入库");
            }
            // 按 barcode 加锁,避免同一 barcode 并发请求重复自动组托、重复生成入库单
            String barcodeForLock = param.getBarcode();
            synchronized ((barcodeForLock != null ? barcodeForLock : "").intern()) {
                waitPakin = waitPakinService.getOne(new LambdaQueryWrapper<WaitPakin>()
                        .eq(WaitPakin::getBarcode, param.getBarcode())
                        .in(WaitPakin::getIoStatus, PakinIOStatus.PAKIN_IO_STATUS_DONE.val, PakinIOStatus.PAKIN_IO_STATUS_TASK_EXCE.val));
                // 空托盘无组托时:若配置启用则按 AUTO_FULL_OUT_MATNR_CODE 自动组托并生成入库单,再继续入库任务逻辑
                if (waitPakin == null) {
                    tryAutoPakinForBarcode(param.getBarcode());
                    waitPakin = waitPakinService.getOne(new LambdaQueryWrapper<WaitPakin>()
                            .eq(WaitPakin::getBarcode, param.getBarcode())
                            .in(WaitPakin::getIoStatus, PakinIOStatus.PAKIN_IO_STATUS_DONE.val, PakinIOStatus.PAKIN_IO_STATUS_TASK_EXCE.val));
                }
            }
            waitPakin = validateWaitPakin(param.getBarcode());
            waitPakinItems = waitPakinItemService.list(
                    new LambdaQueryWrapper<WaitPakinItem>().eq(WaitPakinItem::getPakinId, waitPakin.getId()));
        }
        // 检查该托盘号是否已有入库任务(含进行中、已完成),有则直接复用返回,拦截重复提交
        List<Integer> otherInboundTaskTypes = Arrays.asList(
                TaskType.TASK_TYPE_IN.type,
                TaskType.TASK_TYPE_MERGE_IN.type,
@@ -311,48 +373,35 @@
        Task existingInTask = taskService.getOne(new LambdaQueryWrapper<Task>()
                .eq(Task::getBarcode, param.getBarcode())
                .in(Task::getTaskType, otherInboundTaskTypes)
                .in(Task::getTaskStatus, TaskStsType.GENERATE_IN.id, TaskStsType.WCS_EXECUTE_IN.id)
                .orderByDesc(Task::getCreateTime)
                .last("LIMIT 1"));
        if (Objects.nonNull(existingInTask)) {
            log.info("找到匹配的其他入库任务 - 任务编码:{},任务类型:{},箱号:{}",
                    existingInTask.getTaskCode(), existingInTask.getTaskType(), param.getBarcode());
            log.info("找到该托盘号已有入库任务,复用并拦截重复提交 - 任务编码:{},箱号:{},状态:{}",
                    existingInTask.getTaskCode(), param.getBarcode(), existingInTask.getTaskStatus());
            // 检查组托明细是否有订单编码(任务编号)
            List<WaitPakinItem> itemsWithAsnCode = waitPakinItems.stream()
                    .filter(item -> StringUtils.isNotBlank(item.getAsnCode()))
                    .collect(Collectors.toList());
            if (!itemsWithAsnCode.isEmpty()) {
                log.info("组托档有任务编号,使用现有入库任务单号 - 任务编码:{},箱号:{},任务编号数量:{}",
                        existingInTask.getTaskCode(), param.getBarcode(), itemsWithAsnCode.size());
                // 更新入库站点信息(如果与当前申请的站点不同)
                if (StringUtils.isNotBlank(param.getSourceStaNo()) &&
                        !param.getSourceStaNo().equals(existingInTask.getOrgSite())) {
                    log.info("更新入库任务的入库站点 - 任务编码:{},原站点:{},新站点:{}",
                            existingInTask.getTaskCode(), existingInTask.getOrgSite(), param.getSourceStaNo());
                    existingInTask.setOrgSite(param.getSourceStaNo());
                    if (!taskService.updateById(existingInTask)) {
                        log.warn("更新入库任务的入库站点失败 - 任务编码:{}", existingInTask.getTaskCode());
                    }
            // 更新入库站点信息(如果与当前申请的站点不同)
            if (StringUtils.isNotBlank(param.getSourceStaNo()) &&
                    !param.getSourceStaNo().equals(existingInTask.getOrgSite())) {
                log.info("更新入库任务的入库站点 - 任务编码:{},原站点:{},新站点:{}",
                        existingInTask.getTaskCode(), existingInTask.getOrgSite(), param.getSourceStaNo());
                existingInTask.setOrgSite(param.getSourceStaNo());
                if (!taskService.updateById(existingInTask)) {
                    log.warn("更新入库任务的入库站点失败 - 任务编码:{}", existingInTask.getTaskCode());
                }
                // 返回现有入库任务的信息
                InTaskMsgDto msgDto = new InTaskMsgDto();
                msgDto.setWorkNo(existingInTask.getTaskCode());
                msgDto.setTaskId(existingInTask.getId());
                msgDto.setLocNo(existingInTask.getTargLoc());
                msgDto.setSourceStaNo(existingInTask.getOrgSite());
                msgDto.setStaNo(existingInTask.getTargSite());
                return msgDto;
            } else {
                log.info("组托档没有任务编号,继续创建新任务 - 箱号:{}", param.getBarcode());
            }
        } else {
            log.info("未找到匹配的其他入库任务,继续创建新任务 - 箱号:{}", param.getBarcode());
            // 直接返回已有任务信息,不再新建任务
            InTaskMsgDto msgDto = new InTaskMsgDto();
            msgDto.setWorkNo(existingInTask.getTaskCode());
            msgDto.setTaskId(existingInTask.getId());
            msgDto.setLocNo(existingInTask.getTargLoc());
            msgDto.setSourceStaNo(existingInTask.getOrgSite());
            msgDto.setStaNo(existingInTask.getTargSite());
            return msgDto;
        }
        log.info("未找到该托盘号已有入库任务,继续创建新任务 - 箱号:{}", param.getBarcode());
        // 生成任务编码
        String ruleCode = generateTaskCode();
@@ -365,6 +414,9 @@
            throw new RuntimeException(e);
        }
        if (waitPakin == null) {
            throw new CoolException("请检查组拖状态是否完成!!");
        }
        // 创建并保存任务
        Task task = createTask(ruleCode, locNo.getLocNo(), waitPakin.getBarcode(),
                deviceSite.getDeviceSite(), param.getSourceStaNo().toString(), param.getUser());
@@ -389,6 +441,103 @@
    /**
     * RCS 入库申请时若 barcode 无组托且配置启用:按 AUTO_FULL_OUT_MATNR_CODE 无订单组托并生成入库单,便于后续生成入库任务。
     */
    private void tryAutoPakinForBarcode(String barcode) {
        Config enabledConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_PAKIN_ON_ASN_ENABLED));
        if (enabledConfig == null || !Boolean.parseBoolean(enabledConfig.getVal())) {
            return;
        }
        Config matnrConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_MATNR_CODE));
        if (matnrConfig == null || StringUtils.isBlank(matnrConfig.getVal())) {
            return;
        }
        // 二次确认:已有该 barcode 的组托则直接返回,由外层复用,避免重复请求生成多条入库单
        WaitPakin existing = waitPakinService.getOne(new LambdaQueryWrapper<WaitPakin>()
                .eq(WaitPakin::getBarcode, barcode)
                .in(WaitPakin::getIoStatus, PakinIOStatus.PAKIN_IO_STATUS_DONE.val, PakinIOStatus.PAKIN_IO_STATUS_TASK_EXCE.val));
        if (existing != null) {
            log.info("[RCS入库申请-自动组托] barcode={} 已有组托,跳过自动组托", barcode);
            return;
        }
        Config qtyConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_PAKIN_QTY));
        double autoQty = 1.0;
        if (qtyConfig != null && StringUtils.isNotBlank(qtyConfig.getVal())) {
            try {
                autoQty = Double.parseDouble(qtyConfig.getVal().trim());
                if (autoQty <= 0) autoQty = 1.0;
            } catch (NumberFormatException e) {
                // ignore
            }
        }
        String matnrCode = matnrConfig.getVal().trim();
        Matnr matnr = matnrService.getOne(new LambdaQueryWrapper<Matnr>().eq(Matnr::getCode, matnrCode));
        if (matnr == null) {
            log.warn("[RCS入库申请-自动组托] 物料不存在: {}", matnrCode);
            return;
        }
        List<PakinItem> pakinItems = new ArrayList<>();
        PakinItem pi = new PakinItem();
        pi.setMatnrId(matnr.getId());
        pi.setReceiptQty(autoQty);
        pi.setAsnCode(null);
        pi.setId(null);
        pakinItems.add(pi);
        WaitPakinParam param = new WaitPakinParam();
        param.setBarcode(barcode);
        param.setItems(pakinItems);
        WaitPakin waitPakin;
        try {
            waitPakin = mobileService.mergeItems(param, 1L);
        } catch (Exception e) {
            log.warn("[RCS入库申请-自动组托] 组托失败, barcode={}: {}", barcode, e.getMessage());
            throw new CoolException("barcode=" + barcode + ": " + e.getMessage());
        }
        String ruleCode = SerialRuleUtils.generateRuleCode(SerialRuleCode.SYS_ASN_ORDER, null);
        if (StringUtils.isBlank(ruleCode)) {
            log.warn("[RCS入库申请-自动组托] 入库单编码规则未配置");
            return;
        }
//        val orderWorkTypeOtherIn = OrderWorkType.ORDER_WORK_TYPE_OTHER_IN.type;
        //                .setWkType(orderWorkTypeOtherIn)
        WkOrder order = new WkOrder();
        order.setCode(ruleCode)
                .setType(OrderType.ORDER_IN.type)
                .setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_UN_EXCE.val)
                .setAnfme(autoQty)
                .setWorkQty(0.0)
                .setQty(0.0)
                .setCreateBy(1L)
                .setUpdateBy(1L);
        if (!asnOrderService.save(order)) {
            throw new CoolException("入库主单保存失败");
        }
        WkOrderItem orderItem = new WkOrderItem();
        orderItem.setOrderId(order.getId())
                .setOrderCode(order.getCode())
                .setMatnrId(matnr.getId())
                .setMatnrCode(matnr.getCode())
                .setMaktx(matnr.getName())
                .setAnfme(autoQty)
                .setWorkQty(0.0)
                .setQty(0.0)
                .setStockUnit(matnr.getStockUnit() != null ? matnr.getStockUnit() : "个")
                .setPurUnit(matnr.getPurUnit() != null ? matnr.getPurUnit() : "个")
                .setFieldsIndex(matnr.getFieldsIndex())
                .setCreateBy(1L)
                .setUpdateBy(1L);
        if (!asnOrderItemService.save(orderItem)) {
            throw new CoolException("入库明细保存失败");
        }
        waitPakinItemService.update(new LambdaUpdateWrapper<WaitPakinItem>()
                .eq(WaitPakinItem::getPakinId, waitPakin.getId())
                .set(WaitPakinItem::getAsnId, order.getId())
                .set(WaitPakinItem::getAsnCode, order.getCode())
                .set(WaitPakinItem::getAsnItemId, orderItem.getId()));
        log.info("[RCS入库申请-自动组托] 已组托并生成入库单: {}, barcode: {}, 物料: {}, 数量: {}", order.getCode(), barcode, matnrCode, autoQty);
    }
    /**
     * 验证设备站点
     */
    private DeviceSite validateDeviceSite(TaskInParam param) {
@@ -408,7 +557,7 @@
    private WaitPakin validateWaitPakin(String barcode) {
        WaitPakin waitPakin = waitPakinService.getOne(new LambdaQueryWrapper<WaitPakin>()
                .eq(WaitPakin::getBarcode, barcode)
                .in(WaitPakin::getIoStatus, PakinIOStatus.PAKIN_IO_STATUS_DONE.val, PakinIOStatus.PAKIN_IO_STATUS_TASK_EXCE.val));
                    .in(WaitPakin::getIoStatus, PakinIOStatus.PAKIN_IO_STATUS_DONE.val, PakinIOStatus.PAKIN_IO_STATUS_TASK_EXCE.val));
        if (Cools.isEmpty(waitPakin)) {
            throw new CoolException("请检查组拖状态是否完成!!");
@@ -432,10 +581,18 @@
     */
    private Task createTask(String ruleCode, String targetLoc, String barcode,
                            String targetSite, String sourceSiteNo, Long loginUserId) {
        return createTask(ruleCode, targetLoc, barcode, targetSite, sourceSiteNo, loginUserId, TaskType.TASK_TYPE_IN.type);
    }
    /**
     * 创建并保存任务(支持指定任务类型,如空板入库)
     */
    private Task createTask(String ruleCode, String targetLoc, String barcode,
                            String targetSite, String sourceSiteNo, Long loginUserId, Integer taskType) {
        Task task = new Task();
        task.setTaskCode(ruleCode)
                .setTaskStatus(TaskStsType.GENERATE_IN.id)
                .setTaskType(TaskType.TASK_TYPE_IN.type)
                .setTaskType(taskType != null ? taskType : TaskType.TASK_TYPE_IN.type)
                .setWarehType(WarehType.WAREHOUSE_TYPE_CRN.val)
                .setTargLoc(targetLoc)
                .setBarcode(barcode)
@@ -461,6 +618,75 @@
        if (!updated) {
            throw new CoolException("库位预约失败!!");
        }
    }
    /**
     * 空板入库:RCS 申请时 full=true,无需组托,分配库位并创建 TASK_TYPE_EMPITY_IN 任务。
     * 需在设备站点中配置 type=10(空板入库)的站点路径。
     */
    private InTaskMsgDto createInTaskForEmptyPallet(String barcode, String staNo, Integer type) {
        TaskInParam param = new TaskInParam();
        param.setBarcode(barcode);
        param.setSourceStaNo(staNo);
        param.setLocType1(type != null ? type : 1);
        param.setIoType(TaskType.TASK_TYPE_EMPITY_IN.type);
        param.setUser(1L);
        // 校验设备站点(需配置 type=10 空板入库的站点)
        DeviceSite deviceSite = validateDeviceSite(param);
        // 检查该托盘号是否已有空板入库任务,有则复用
        Task existingInTask = taskService.getOne(new LambdaQueryWrapper<Task>()
                .eq(Task::getBarcode, barcode)
                .eq(Task::getTaskType, TaskType.TASK_TYPE_EMPITY_IN.type)
                .orderByDesc(Task::getCreateTime)
                .last("LIMIT 1"));
        if (existingInTask != null) {
            log.info("找到该托盘号已有空板入库任务,复用 - 任务编码:{},箱号:{}", existingInTask.getTaskCode(), barcode);
            if (StringUtils.isNotBlank(staNo) && !staNo.equals(existingInTask.getOrgSite())) {
                existingInTask.setOrgSite(staNo);
                taskService.updateById(existingInTask);
            }
            InTaskMsgDto msgDto = new InTaskMsgDto();
            msgDto.setWorkNo(existingInTask.getTaskCode());
            msgDto.setTaskId(existingInTask.getId());
            msgDto.setLocNo(existingInTask.getTargLoc());
            msgDto.setSourceStaNo(existingInTask.getOrgSite());
            msgDto.setStaNo(existingInTask.getTargSite());
            return msgDto;
        }
        // 该托盘已在库或出库中,不可重复申请空板入库
        List<Loc> inStock = locService.list(new LambdaQueryWrapper<Loc>().eq(Loc::getBarcode, barcode));
        if (!inStock.isEmpty()) {
            throw new CoolException("barcode=" + barcode + ": 该托盘已在库,不可重复申请入库");
        }
        Task outboundTask = taskService.getOne(new LambdaQueryWrapper<Task>()
                .eq(Task::getBarcode, barcode)
                .in(Task::getTaskType, Arrays.asList(TaskType.TASK_TYPE_OUT.type, TaskType.TASK_TYPE_EMPITY_OUT.type,
                        TaskType.TASK_TYPE_PICK_AGAIN_OUT.type, TaskType.TASK_TYPE_CHECK_OUT.type))
                .lt(Task::getTaskStatus, TaskStsType.COMPLETE_OUT.id));
        if (outboundTask != null) {
            throw new CoolException("barcode=" + barcode + ": 该托盘出库中未完成,不可申请入库");
        }
        InTaskMsgDto locNo;
        try {
            locNo = getLocNo(param);
        } catch (Exception e) {
            throw new CoolException("获取空板入库库位失败:" + e.getMessage());
        }
        if (locNo == null || StringUtils.isBlank(locNo.getLocNo())) {
            throw new CoolException("未找到可用的空库位,请检查库区与设备站点配置(空板入库需配置 type=10 的站点)");
        }
        String ruleCode = generateTaskCode();
        String targetSite = StringUtils.isNotBlank(deviceSite.getDeviceSite()) ? deviceSite.getDeviceSite() : staNo;
        Task task = createTask(ruleCode, locNo.getLocNo(), barcode, targetSite, staNo, param.getUser(), TaskType.TASK_TYPE_EMPITY_IN.type);
        updateLocStatus(task.getTargLoc(), barcode);
        locNo.setWorkNo(ruleCode);
        locNo.setTaskId(task.getId());
        log.info("[空板入库] 已创建任务: {}, 库位: {}, 料箱: {}", ruleCode, locNo.getLocNo(), barcode);
        return locNo;
    }
    /**
@@ -867,6 +1093,7 @@
                        throw new CoolException("任务状态修改失败!!当前任务状态:" + task.getTaskStatus() + ",目标状态:" + TaskStsType.COMPLETE_IN.id);
                    }
                    log.info("入库任务状态更新成功 - 任务编码:{}", task.getTaskCode());
                    // 入库完整闭环由定时任务完成:TaskSchedules.completeInStock 扫描 COMPLETE_IN,执行库位/组托/上报云仓
                }
            } else if (task.getTaskType().equals(TaskType.TASK_TYPE_OUT.type)
                    || task.getTaskType().equals(TaskType.TASK_TYPE_PICK_AGAIN_OUT.type)
@@ -909,33 +1136,7 @@
                        throw new CoolException("任务状态修改失败!!当前任务状态:" + task.getTaskStatus() + ",目标状态:" + TaskStsType.COMPLETE_OUT.id);
                    }
                    log.info("出库任务状态更新成功 - 任务编码:{}", task.getTaskCode());
                    // 全版出库在RCS回调后处理库存并设置为199,等待PDA快速拣货确认后更新为200
                    if (task.getTaskType().equals(TaskType.TASK_TYPE_OUT.type)) {
                        log.info("全版出库任务,开始处理库存并更新状态为199 - 任务编码:{}", task.getTaskCode());
                        try {
                            // 重新查询任务以获取最新状态(198)
                            task = taskService.getOne(new LambdaQueryWrapper<Task>().eq(Task::getTaskCode, task.getTaskCode()));
                            // 调用completeTask处理库存(会设置为199)
                            List<Task> taskList = new ArrayList<>();
                            taskList.add(task);
                            taskService.completeTask(taskList);
                            // 重新查询任务以获取最新状态(199)
                            task = taskService.getOne(new LambdaQueryWrapper<Task>().eq(Task::getTaskCode, task.getTaskCode()));
                            if (task.getTaskStatus().equals(TaskStsType.WAVE_SEED.id)) {
                                log.info("全版出库任务状态已更新为199(等待PDA快速拣货确认后更新为200) - 任务编码:{}", task.getTaskCode());
                            } else {
                                log.warn("全版出库任务状态更新为199失败 - 任务编码:{},当前状态:{}",
                                        task.getTaskCode(), task.getTaskStatus());
                            }
                        } catch (Exception e) {
                            log.error("全版出库任务处理失败 - 任务编码:{},错误:{}", task.getTaskCode(), e.getMessage(), e);
                            // 不抛出异常,避免影响RCS回调的正常返回
                        }
                    }
                    // 出库完整闭环(库存、出库单、9.1 上报云仓)由定时任务 TaskSchedules.complateOutStock 统一执行
                }
            }
        } else {
@@ -1355,9 +1556,19 @@
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R allocateLocation(String barcode, String staNo, Integer type) {
    public R allocateLocation(String barcode, String staNo, Integer type, Boolean full) {
        log.info("========== 开始申请入库任务,分配库位 ==========");
        log.info("料箱码:{},入库站点:{},入库类型:{}", barcode, staNo, type);
        log.info("料箱码:{},入库站点:{},入库类型:{},空板:{}", barcode, staNo, type, full);
        // full=true 时走空板入库(无需组托);否则走普通入库(需组托或自动组托)
        if (Boolean.TRUE.equals(full)) {
            InTaskMsgDto msgDto = createInTaskForEmptyPallet(barcode, staNo, type);
            JSONObject result = new JSONObject();
            result.put("locNo", msgDto.getLocNo());
            result.put("batchNo", msgDto.getWorkNo());
            result.put("taskNo", msgDto.getWorkNo());
            return R.ok(result);
        }
        // 构建 TaskInParam 参数,与 /wcs/create/in/task 接口参数一致
        TaskInParam param = new TaskInParam();