chen.lin
10 小时以前 82065a03737fa1370eb9f4f01ab5332933baf08a
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
@@ -4,12 +4,14 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.CoercionAction;
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.DateUtils;
import com.vincent.rsf.server.api.config.RemotesInfoProperties;
import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam;
import com.vincent.rsf.server.api.controller.erp.params.TaskInParam;
import com.vincent.rsf.server.api.entity.CommonResponse;
import com.vincent.rsf.server.api.entity.constant.RcsConstant;
@@ -18,6 +20,7 @@
import com.vincent.rsf.server.api.entity.params.WcsTaskParams;
import com.vincent.rsf.server.api.service.WcsService;
import com.vincent.rsf.server.common.constant.Constants;
import com.vincent.rsf.server.common.utils.QuantityUtils;
import com.vincent.rsf.server.manager.controller.params.LocToTaskParams;
import com.vincent.rsf.server.manager.controller.params.PakinItem;
import com.vincent.rsf.server.manager.enums.*;
@@ -25,6 +28,7 @@
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.api.utils.LocUtils;
import com.vincent.rsf.server.manager.controller.params.GenerateTaskParams;
import com.vincent.rsf.server.manager.entity.CloudWmsNotifyLog;
import com.vincent.rsf.server.manager.entity.*;
import com.vincent.rsf.server.manager.mapper.TaskMapper;
import com.vincent.rsf.server.manager.service.*;
@@ -123,6 +127,10 @@
    private RestTemplate restTemplate;
    @Autowired
    private RemotesInfoProperties.RcsApi rcsApi;
    @Autowired
    private CloudWmsNotifyLogService cloudWmsNotifyLogService;
    @Autowired
    private WarehouseService warehouseService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -459,16 +467,18 @@
        if (success.compareAndSet(false, true)) {
            Long loginUserId = SystemAuthUtils.getLoginUserId();
            for (Task task : tasks) {
                if (task.getTaskType().equals(TaskType.TASK_TYPE_IN.type)) {
                    //1.入库
                if (task.getTaskType().equals(TaskType.TASK_TYPE_IN.type) || task.getTaskType().equals(TaskType.TASK_TYPE_MERGE_IN.type)) {
                    //1.入库、54.并板再入库
                    complateInstock(task, loginUserId);
                } else if (task.getTaskType().equals(TaskType.TASK_TYPE_PICK_IN.type) || task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_IN.type)) {
                    //53.拣料再入库
                    //57.盘点再入库
                    //53.拣料再入库、57.盘点再入库
                    pickComplateInStock(task, loginUserId);
                } else if (task.getTaskType().equals(TaskType.TASK_TYPE_LOC_MOVE.type)) {
                    //移库
                    //11.库格移载
                    moveInStock(task, loginUserId);
                } else if (task.getTaskType().equals(TaskType.TASK_TYPE_EMPITY_IN.type)) {
                    //10.空板入库:与普通入库共用一个完成逻辑(若有组托则更新组托/库位并上报云仓)
                    complateInstock(task, loginUserId);
                }
            }
        }
@@ -605,9 +615,9 @@
            throw new CoolException("当前任务不是全版出库任务,无法执行此操作!!");
        }
        // 检查任务状态
        if (task.getTaskStatus().equals(TaskStsType.COMPLETE_OUT.id)) {
            throw new CoolException("任务已完成,无需重复完结!!");
        // 检查任务状态:必须是199(WAVE_SEED)状态才能手动完结
        if (!task.getTaskStatus().equals(TaskStsType.WAVE_SEED.id)) {
            throw new CoolException("任务状态不是等待确认状态(199),无法执行此操作!!当前状态:" + task.getTaskStatus());
        }
        // 查询库位
@@ -650,8 +660,8 @@
            }
        }
        // 更新任务状态为出库完成
        task.setTaskStatus(TaskStsType.COMPLETE_OUT.id)
        // 更新任务状态为库存更新完成(200)
        task.setTaskStatus(TaskStsType.UPDATED_OUT.id)
                .setUpdateBy(loginUserId)
                .setUpdateTime(new Date());
@@ -721,7 +731,7 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void moveToDeep(Long loginUserId, String curLoc) throws Exception {
        //浅库位,判断深对应深库位是否为空,如果为空生成一个移库任务,将托盘送入深库位
        //浅库位,判断深对应深库位是否为空,如果为空生成一个移库任务,将料箱送入深库位
        String deepLoc = LocUtils.getDeepLoc(curLoc);
        if (StringUtils.isBlank(deepLoc)) {
            throw new CoolException("数据异常,请联系管理员!");
@@ -804,7 +814,11 @@
                .set(Task::getTaskStatus, TaskStsType.UPDATED_IN.id))) {
            throw new CoolException("任务状态修改失败!!");
        }
        // 9.1 入/出库结果上报:库格移载完成后通知云仓(与定时任务闭环一致)
        List<TaskItem> moveTaskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()));
        if (!moveTaskItems.isEmpty()) {
            reportInOutResultToCloud(task, loc, moveTaskItems, null, true);
        }
    }
@@ -860,30 +874,37 @@
            throw new CoolException("任务明细不存在!!");
        }
        List<LocItem> items = new ArrayList<>();
        for (TaskItem taskItem : taskItems) {
            LocItem locItem = new LocItem();
            LocItemWorking locWorking = locItemWorkingService.getOne(new LambdaQueryWrapper<LocItemWorking>()
                    .eq(LocItemWorking::getTaskId, taskItem.getTaskId())
                    .eq(StringUtils.isNotBlank(taskItem.getFieldsIndex()), LocItemWorking::getFieldsIndex, taskItem.getFieldsIndex())
                    .eq(StringUtils.isNotEmpty(taskItem.getBatch()), LocItemWorking::getBatch, taskItem.getBatch())
                    .eq(LocItemWorking::getMatnrId, taskItem.getMatnrId()));
            if (Objects.isNull(locWorking)) {
               continue;
        if (TaskType.TASK_TYPE_PICK_IN.type.equals(task.getTaskType())) {
            // 拣料再入库:出库时已在 pickOrCheckTask 中扣减原库位(1100 -> 1089.899),
            // 入库完成时不再回写/累加库位明细,保持 1 条 1089.899,避免出现两条 1100
            // 仅更新库位状态、清理 LocItemWorking、任务状态及流水
        } else {
            // 盘点入库等:沿用原逻辑,从 LocItemWorking 回写并 saveBatch
            List<LocItem> items = new ArrayList<>();
            for (TaskItem taskItem : taskItems) {
                LocItem locItem = new LocItem();
                LocItemWorking locWorking = locItemWorkingService.getOne(new LambdaQueryWrapper<LocItemWorking>()
                        .eq(LocItemWorking::getTaskId, taskItem.getTaskId())
                        .eq(StringUtils.isNotBlank(taskItem.getFieldsIndex()), LocItemWorking::getFieldsIndex, taskItem.getFieldsIndex())
                        .eq(StringUtils.isNotEmpty(taskItem.getBatch()), LocItemWorking::getBatch, taskItem.getBatch())
                        .eq(LocItemWorking::getMatnrId, taskItem.getMatnrId()));
                if (Objects.isNull(locWorking)) {
                   continue;
                }
                if (task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_IN.type)) {
                    locWorking.setAnfme(taskItem.getAnfme());
                }
                BeanUtils.copyProperties(locWorking, locItem);
                locItem.setWorkQty(0.0).setQty(0.0).setLocCode(loc.getCode()).setLocId(loc.getId()).setId(null).setUpdateBy(loginUserId).setUpdateTime(new Date());
                //数量为零的不入库
                if (locItem.getAnfme().compareTo(0.0) > 0) {
                    items.add(locItem);
                }
            }
            if (task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_IN.type)) {
                locWorking.setAnfme(taskItem.getAnfme());
            }
            BeanUtils.copyProperties(locWorking, locItem);
            locItem.setWorkQty(0.0).setQty(0.0).setLocCode(loc.getCode()).setLocId(loc.getId()).setId(null).setUpdateBy(loginUserId).setUpdateTime(new Date());
            //数量为零的不入库
            if (locItem.getAnfme().compareTo(0.0) > 0) {
                items.add(locItem);
            }
        }
        if (!locItemService.saveBatch(items)) {
            if (!items.isEmpty() && !locItemService.saveBatch(items)) {
//            throw new CoolException("作业库存回写失败!!");
            }
        }
        TaskItem taskItem = taskItems.stream().findFirst().get();
@@ -896,7 +917,8 @@
        if (!taskService.updateById(task)) {
            throw new CoolException("任务状态修改失败!!");
        }
        // 9.1 入/出库结果上报:拣料再入库/盘点再入库完成后通知云仓(与定时任务闭环一致)
        reportInOutResultToCloud(task, loc, taskItems, null, true);
    }
    /**
@@ -952,7 +974,7 @@
                        batchNo = task.getTaskCode();
                    }
                }
                log.info("任务已下发到RCS,需要取消RCS任务 - 任务ID:{},任务编号:{},任务状态:{},托盘码:{}",
                log.info("任务已下发到RCS,需要取消RCS任务 - 任务ID:{},任务编号:{},任务状态:{},料箱码:{}",
                        task.getId(), task.getTaskCode(), task.getTaskStatus(), task.getBarcode());
            }
        }
@@ -960,11 +982,19 @@
        // 如果有任务已下发到RCS,先调用RCS取消接口
        boolean rcsCancelSuccess = false;
        if (!rcsTaskCodes.isEmpty()) {
            try {
                log.info("========== 开始取消RCS任务 ==========");
                log.info("需要取消的RCS任务编号:{}", rcsTaskCodes);
                String rcsUrl = rcsApi.getHost() + ":" + rcsApi.getPort() + RcsConstant.cancelTask;
                log.info("RCS取消任务请求地址:{}", rcsUrl);
            // 检查 RCS API 配置是否有效
            if (rcsApi == null || StringUtils.isBlank(rcsApi.getHost()) || StringUtils.isBlank(rcsApi.getPort())) {
                log.error("========== RCS任务取消失败 ==========");
                log.error("RCS API 配置无效!host: {}, port: {}",
                        rcsApi != null ? rcsApi.getHost() : "null",
                        rcsApi != null ? rcsApi.getPort() : "null");
                // 即使配置无效,也继续执行任务删除操作
            } else {
                try {
                    log.info("========== 开始取消RCS任务 ==========");
                    log.info("需要取消的RCS任务编号:{}", rcsTaskCodes);
                    String rcsUrl = rcsApi.getHost() + ":" + rcsApi.getPort() + RcsConstant.cancelTask;
                    log.info("RCS取消任务请求地址:{}", rcsUrl);
                
                // 如果没有批次编号,使用第一个任务编号作为批次编号
                if (StringUtils.isBlank(batchNo) && !rcsTaskCodes.isEmpty()) {
@@ -1010,12 +1040,13 @@
                    log.error("RCS取消任务失败:{}", result.getMsg());
                    throw new CoolException("RCS取消任务失败:" + result.getMsg());
                }
            } catch (JsonProcessingException e) {
                log.error("RCS取消任务响应解析失败:{}", e.getMessage(), e);
                throw new CoolException("RCS取消任务响应解析失败:" + e.getMessage());
            } catch (Exception e) {
                log.error("RCS取消任务异常:{}", e.getMessage(), e);
                throw new CoolException("RCS取消任务异常:" + e.getMessage());
                } catch (JsonProcessingException e) {
                    log.error("RCS取消任务响应解析失败:{}", e.getMessage(), e);
                    throw new CoolException("RCS取消任务响应解析失败:" + e.getMessage());
                } catch (Exception e) {
                    log.error("RCS取消任务异常:{}", e.getMessage(), e);
                    throw new CoolException("RCS取消任务异常:" + e.getMessage());
                }
            }
        }
        
@@ -1278,7 +1309,7 @@
//        if (Objects.isNull(locInfo)) {
//            throw new CoolException("获取库位失败!!");
//        }
        //希日上报物有情况,不需要获取新库位
        //上报物有情况,不需要获取新库位
        task.setTargLoc(task.getOrgLoc())
                .setOrgSite(task.getTargSite());
@@ -1287,12 +1318,71 @@
        }
        //获取因当前任务出库的所有物料信息
        List<LocItemWorking> tempLocs = locItemWorkingService.list(new LambdaQueryWrapper<LocItemWorking>().eq(LocItemWorking::getTaskId, task.getId()));
        if (tempLocs.isEmpty()) {
            throw new CoolException("数据错误,作业中库存数据丢失!!");
        }
        List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()));
        if (taskItems.isEmpty()) {
            throw new CoolException("数据错误:任务明细为空!!");
        }
        // 与查询一致:若无作业中库存但存在任务明细,用任务明细在内存中兜底,避免“能查到却无法确认”
        if (tempLocs.isEmpty()) {
            tempLocs = taskItems.stream().map(ti -> {
                LocItemWorking w = new LocItemWorking();
                w.setTaskId(task.getId());
                w.setFieldsIndex(ti.getFieldsIndex());
                w.setAnfme(ti.getAnfme());
                w.setMatnrId(ti.getMatnrId());
                w.setMaktx(ti.getMaktx());
                w.setMatnrCode(ti.getMatnrCode());
                w.setSpec(ti.getSpec());
                w.setBatch(ti.getBatch());
                w.setUnit(ti.getUnit());
                w.setModel(ti.getModel());
                return w;
            }).collect(Collectors.toList());
        }
        // 拣料入库:先算剩余数量并更新 taskItem.anfme,已拣数量 taskItem.qty = 原库位 - 剩余(保证 数量=100、已拣数量=1、库存明细=100)
        if (TaskType.TASK_TYPE_PICK_IN.type.equals(type)) {
            log.debug("[拣料入库] 开始处理 taskId={}, taskCode={}, orgLoc={}, tempLocs.size={}, taskItems.size={}",
                    task.getId(), task.getTaskCode(), task.getOrgLoc(), tempLocs.size(), taskItems.size());
            tempLocs.forEach(working -> {
                taskItems.forEach(taskItem -> {
                    if (Objects.equals(taskItem.getFieldsIndex(), working.getFieldsIndex())) {
                        // 已拣数量:优先用 taskItem.qty;为 0 时从出库单明细取 执行数(workQty) 或 订单数量(anfme),避免手动完结未填 qty 导致不扣减
                        Double pickedQty = taskItem.getQty() != null && QuantityUtils.isPositive(taskItem.getQty())
                                ? taskItem.getQty()
                                : 0.0;
                        log.debug("[拣料入库] taskItemId={}, fieldsIndex={}, working.anfme={}, taskItem.qty={}, taskItem.orderItemId={}, pickedQty(初)={}",
                                taskItem.getId(), taskItem.getFieldsIndex(), working.getAnfme(), taskItem.getQty(), taskItem.getOrderItemId(), pickedQty);
                        if (pickedQty <= 0 && taskItem.getOrderItemId() != null) {
                            WkOrderItem orderItem = asnOrderItemService.getById(taskItem.getOrderItemId());
                            log.debug("[拣料入库] 查出库单明细 orderItemId={}, orderItem={}, workQty={}, anfme={}",
                                    taskItem.getOrderItemId(), orderItem != null ? "存在" : "null",
                                    orderItem != null ? orderItem.getWorkQty() : null, orderItem != null ? orderItem.getAnfme() : null);
                            if (orderItem != null) {
                                if (orderItem.getWorkQty() != null && QuantityUtils.isPositive(orderItem.getWorkQty())) {
                                    pickedQty = orderItem.getWorkQty();
                                } else if (orderItem.getAnfme() != null && QuantityUtils.isPositive(orderItem.getAnfme())) {
                                    pickedQty = orderItem.getAnfme();
                                }
                            }
                        }
                        Double minQty = QuantityUtils.subtract(working.getAnfme(), pickedQty);
                        log.debug("[拣料入库] 计算后 pickedQty={}, minQty(剩余)={}, 将更新 taskItem.anfme={}, taskItem.qty={}",
                                pickedQty, minQty, minQty, pickedQty);
                        if (QuantityUtils.isNonNegative(minQty)) {
                            taskItem.setAnfme(minQty);
                            taskItem.setQty(pickedQty);
                            if (!taskItemService.updateById(taskItem)) {
                                throw new CoolException("任务明细修改失败!!");
                            }
                        } else {
                            log.warn("[拣料入库] minQty<0 未更新 taskItem, taskItemId={}", taskItem.getId());
                        }
                    }
                });
            });
            log.debug("[拣料入库] 即将扣减库位 locId={}, locCode={}", loc.getId(), loc.getCode());
            subtractLocItemByTaskItems(loc, taskItems, SystemAuthUtils.getLoginUserId());
        }
        tempLocs.forEach(working -> {
@@ -1300,19 +1390,19 @@
                if (Objects.equals(taskItem.getFieldsIndex(), working.getFieldsIndex())) {
                    Double minQty = taskItem.getAnfme();
                    if (!task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_IN.type)) {
                        // 计算剩余数量:从LocItemWorking中减去TaskItem的拣料数量
                        minQty = Math.round((working.getAnfme() - taskItem.getQty()) * 1000000) / 1000000.0;
                        // 计算剩余数量
                        minQty = QuantityUtils.subtract(working.getAnfme(), taskItem.getQty());
                    }
                    if (minQty.compareTo(0.0) >= 0) {
                    if (QuantityUtils.isNonNegative(minQty)) {
                        // 更新TaskItem的剩余数量
                        taskItem.setAnfme(minQty);
                        if (!taskItemService.updateById(taskItem)) {
                            throw new CoolException("任务明细修改失败!!");
                        }
                        // 更新LocItemWorking的剩余数量(非盘点入库时需要更新)
                        // 更新LocItemWorking的剩余数量(非盘点入库时需要更新);仅持久化记录才写库
                        if (!task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_IN.type)) {
                            working.setAnfme(minQty);
                            if (!locItemWorkingService.updateById(working)) {
                            if (working.getId() != null && !locItemWorkingService.updateById(working)) {
                                throw new CoolException("作业库存数量更新失败!!");
                            }
                        }
@@ -1367,6 +1457,10 @@
                    .setQty(0.0)
                    .setLocId(loc1.getId())
                    .setLocCode(loc1.getCode());
            // 拣料再入库:目标库位数量应为回库的剩余数量(taskItem.anfme),即原库位减掉本次拣出后的数量(如 1000 出库 10 → 990)
            if (TaskType.TASK_TYPE_PICK_IN.type.equals(task.getTaskType()) && taskItem.getAnfme() != null && taskItem.getAnfme().compareTo(0.0) > 0) {
                itemWorking.setAnfme(taskItem.getAnfme());
            }
            workings.add(itemWorking);
        });
@@ -1399,16 +1493,36 @@
        if (Objects.isNull(loc)) {
            throw new CoolException("库位不存在!!");
        }
        if (!loc.getUseStatus().equals(LocStsType.LOC_STS_TYPE_R.type)) {
            throw new CoolException("库位状态不处理于R.出库预约!!");
        }
        List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()));
        if (taskItems.isEmpty()) {
            throw new CoolException("任务明细不存在!!");
        }
        List<LocItem> locItems = locItemService.list(new LambdaQueryWrapper<LocItem>().eq(LocItem::getLocId, loc.getId()));
        // 如果库位状态不是R,检查是否已经处理过
        if (!loc.getUseStatus().equals(LocStsType.LOC_STS_TYPE_R.type)) {
            // 如果库位明细为空,说明已经处理过了,直接更新任务状态为199
            if (locItems.isEmpty()) {
                logger.warn("任务{}的库位{}状态为{},但库位明细为空,可能已经处理过,直接更新任务状态为199",
                        task.getId(), loc.getCode(), loc.getUseStatus());
                if (!this.update(new LambdaUpdateWrapper<Task>()
                        .eq(Task::getId, task.getId())
                        .set(Task::getUpdateBy, loginUserId)
                        .set(Task::getUpdateTime, new Date())
                        .set(Task::getTaskStatus, TaskStsType.WAVE_SEED.id))) {
                    throw new CoolException("任务状态更新失败!!");
                }
                return; // 跳过后续处理
            } else {
                // 库位明细不为空但状态不是R,跳过处理
                logger.error("任务{}的库位{}状态为{},不是R.出库预约状态,但库位明细不为空,跳过处理。任务编码:{},库位编码:{}",
                        task.getId(), loc.getCode(), loc.getUseStatus(), task.getTaskCode(), loc.getCode());
                return;
            }
        }
        // 如果库位明细为空,可能是已经被处理过了,允许继续执行
        if (!locItems.isEmpty()) {
            List<LocItemWorking> workings = new ArrayList<>();
@@ -1430,10 +1544,10 @@
            try {
                // 根据任务类型更新库位明细
                if (task.getTaskType().equals(TaskType.TASK_TYPE_OUT.type)) {
                    // 全版出库:删除所有库位明细
                    subtractLocItem(loc);
                } else {
                    // 部分出库(如拣料出库):根据TaskItem数量扣减库位明细
                    // 全版出库:不删除库位明细,等待PDA快速拣货确认时再删除
                    // subtractLocItem(loc); // 已移除,改为在completeFullOutStock中删除
                } else if (!TaskType.TASK_TYPE_PICK_AGAIN_OUT.type.equals(task.getTaskType())) {
                    // 部分出库(如盘点出库):根据TaskItem数量扣减库位明细;拣料出库在生成拣料入库单时扣减
                    subtractLocItemByTaskItems(loc, taskItems, loginUserId);
                }
            } catch (Exception e) {
@@ -1457,11 +1571,11 @@
                    if (Objects.isNull(waveItem)) {
                        throw new CoolException("波次明细不存在!!");
                    }
//                    try {
//                        saveOutStockItem(maps.get(key), null, waveItem, null, loginUserId);
//                    } catch (Exception e) {
//                        throw new CoolException(e.getMessage());
//                    }
                    try {
                        saveOutStockItem(maps.get(key), null, waveItem, null, loginUserId);
                    } catch (Exception e) {
                        throw new CoolException(e.getMessage());
                    }
                } else if (task.getResource().equals(TaskResouceType.TASK_RESOUCE_ORDER_TYPE.val)) {
                    WkOrderItem orderItem = asnOrderItemService.getById(key);
                    if (Objects.isNull(orderItem)) {
@@ -1493,6 +1607,8 @@
                throw new CoolException(e.getMessage());
            }
        }
        // 根据任务类型更新库位状态
        if (task.getTaskType().equals(TaskType.TASK_TYPE_PICK_AGAIN_OUT.type) || task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_OUT.type)) {
            /**修改为库位状态为S.预约入库,保留原有库位*/
            if (!locService.update(new LambdaUpdateWrapper<Loc>()
@@ -1503,6 +1619,9 @@
                    .eq(Loc::getId, loc.getId()))) {
                throw new CoolException("库位状态修改失败!!");
            }
        } else if (task.getTaskType().equals(TaskType.TASK_TYPE_OUT.type)) {
            // 全版出库:不更新库位状态为O,等待PDA快速拣货确认时再更新
            // 库位状态保持原样(R.出库预约状态)
        } else {
            /**修改为库位状态为O.空库*/
            if (!locService.update(new LambdaUpdateWrapper<Loc>()
@@ -1522,6 +1641,8 @@
                .set(Task::getTaskStatus, TaskStsType.WAVE_SEED.id))) {
            throw new CoolException("库存状态更新失败!!");
        }
        // 9.1 入/出库结果上报:出库完成后通知云仓
        reportInOutResultToCloud(task, loc, taskItems, null, false);
//        if (task.getTaskType().equals(TaskType.TASK_TYPE_PICK_AGAIN_OUT.type) || task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_OUT.type)) {
//            if (!this.update(new LambdaUpdateWrapper<Task>()
@@ -1582,8 +1703,35 @@
                .setUpdateTime(new Date())
                .setAnfme(sum);
        if (!Objects.isNull(waveItem)) {
            //TODO 生成波次时需要将波次号写入单据,通过物料,批次,动态字段等唯一值反查单据信息
            // 波次出库:更新出库单完成数量及出库单明细执行数量
            stock.setSourceId(waveItem.getId()).setType(OrderType.ORDER_OUT.type);
            Long orderId = waveItem.getOrderId();
            if (orderId != null) {
                WkOrder wkOrder = asnOrderService.getById(orderId);
                if (wkOrder != null) {
                    Double curQty = wkOrder.getQty() != null ? wkOrder.getQty() : 0.0;
                    Double newQty = QuantityUtils.roundToScale(curQty + sum);
                    wkOrder.setExceStatus(AsnExceStatus.OUT_STOCK_STATUS_TASK_WORKING.val).setQty(newQty);
                    if (!asnOrderService.updateById(wkOrder)) {
                        throw new CoolException("出库单完成数量更新失败!!");
                    }
                }
                // 按出库单明细汇总本次任务数量,更新各明细 workQty
                Map<Long, Double> sumByOrderItemId = taskItems.stream()
                        .filter(ti -> ti.getOrderItemId() != null)
                        .collect(Collectors.groupingBy(TaskItem::getOrderItemId,
                                Collectors.summingDouble(ti -> ti.getAnfme() != null ? ti.getAnfme() : 0.0)));
                for (Map.Entry<Long, Double> e : sumByOrderItemId.entrySet()) {
                    WkOrderItem oi = outStockItemService.getById(e.getKey());
                    if (oi != null) {
                        Double wq = oi.getWorkQty() != null ? oi.getWorkQty() : 0.0;
                        oi.setWorkQty(QuantityUtils.roundToScale(wq + e.getValue()));
                        if (!outStockItemService.updateById(oi)) {
                            throw new CoolException("出库单明细执行数量更新失败!!");
                        }
                    }
                }
            }
        } else if (!Objects.isNull(orderItem) && StringUtils.isNotBlank(orderItem.getId() + "")) {
            WkOrder wkOrder = asnOrderService.getById(orderItem.getOrderId());
            Double qty = Math.round((wkOrder.getQty() + sum) * 1000000) / 1000000.0;
@@ -1667,6 +1815,15 @@
    @Transactional(rollbackFor = Exception.class)
    public void pubTaskToWcs(List<Task> tasks) {
        /**任务下发接口*/
        // 检查 RCS API 配置是否有效
        if (rcsApi == null || StringUtils.isBlank(rcsApi.getHost()) || StringUtils.isBlank(rcsApi.getPort())) {
            log.error("========== RCS任务下发失败 ==========");
            log.error("RCS API 配置无效!host: {}, port: {}",
                    rcsApi != null ? rcsApi.getHost() : "null",
                    rcsApi != null ? rcsApi.getPort() : "null");
            return;
        }
        String pubTakUrl = rcsApi.getHost() + ":" + rcsApi.getPort() + RcsConstant.pubTask;
        
        for (Task task : tasks) {
@@ -1761,7 +1918,10 @@
                    // 为每个不同的库位创建一个TaskItemParam
                    for (String locCode : locCodes) {
                        TaskItemParam outItemParam = new TaskItemParam();
                        outItemParam.setTaskNo(task.getTaskCode());
                        String taskNo = locCodes.size() > 1
                                ? task.getTaskCode() + "_" + locCode
                                : task.getTaskCode();
                        outItemParam.setTaskNo(taskNo);
                        outItemParam.setPriority(1);
                        outItemParam.setOriLoc(locCode);
                        outItemParam.setDestSta(task.getTargSite());
@@ -1850,7 +2010,7 @@
            } catch (org.springframework.web.client.ResourceAccessException e) {
                long endTime = System.currentTimeMillis();
                log.error("========== RCS任务下发资源访问异常 ==========");
                log.error("请求RCS-资源访问异常(可能包含连接超时),耗时:{}ms,任务编码:{}", (endTime - startTime), task.getTaskCode(), e);
                log.error("请求RCS-资源访问异常,耗时:{}ms,任务编码:{}", (endTime - startTime), task.getTaskCode(), e);
                log.error("请求RCS-请求地址:{}", pubTakUrl);
                log.error("请求RCS-请求参数:{}", JSONObject.toJSONString(taskParams));
                // 检查是否是连接超时异常
@@ -1867,16 +2027,15 @@
            } catch (Exception e) {
                long endTime = System.currentTimeMillis();
                log.error("========== RCS任务下发异常 ==========");
                log.error("请求RCS-异常,耗时:{}ms,任务编码:{}", (endTime - startTime), task.getTaskCode(), e);
                log.error("请求RCS-地址:{}", pubTakUrl);
                log.error("请求RCS-参数:{}", JSONObject.toJSONString(taskParams));
                String errorMsg = e.getMessage();
                // 检查是否是连接超时相关的异常
                if (errorMsg != null && (errorMsg.contains("Connection timed out") || errorMsg.contains("timed out") || errorMsg.contains("timeout"))) {
                    log.error("RCS连接超时,任务下发失败!任务编码:{},错误信息:{}", task.getTaskCode(), errorMsg);
                log.error("请求RCS-连接超时异常,耗时:{}ms,任务编码:{},错误信息:{}-{}", (endTime - startTime), task.getTaskCode(), e, errorMsg);
                } else {
                    log.error("RCS任务下发异常!任务编码:{},错误信息:{}", task.getTaskCode(), errorMsg);
                log.error("请求RCS-异常,耗时:{}ms,任务编码:{},错误信息:{}-{}", (endTime - startTime), task.getTaskCode(), e, errorMsg);
                }
                log.error("请求RCS-地址:{}", pubTakUrl);
                log.error("请求RCS-参数:{}", JSONObject.toJSONString(taskParams));
                continue;
            }
            
@@ -2009,18 +2168,37 @@
    @Transactional(rollbackFor = Exception.class)
    public void subtractLocItemByTaskItems(Loc loc, List<TaskItem> taskItems, Long loginUserId) {
        for (TaskItem taskItem : taskItems) {
            // 查询对应的库位明细
            LocItem locItem = locItemService.getOne(new LambdaQueryWrapper<LocItem>()
            LambdaQueryWrapper<LocItem> locItemWrapper = new LambdaQueryWrapper<LocItem>()
                    .eq(LocItem::getLocId, loc.getId())
                    .eq(LocItem::getMatnrId, taskItem.getMatnrId())
                    .eq(StringUtils.isNotBlank(taskItem.getBatch()), LocItem::getBatch, taskItem.getBatch())
                    .eq(StringUtils.isNotBlank(taskItem.getFieldsIndex()), LocItem::getFieldsIndex, taskItem.getFieldsIndex()));
                    .eq(LocItem::getMatnrId, taskItem.getMatnrId());
            if (StringUtils.isNotBlank(taskItem.getBatch())) {
                locItemWrapper.eq(LocItem::getBatch, taskItem.getBatch());
            } else {
                locItemWrapper.and(w -> w.isNull(LocItem::getBatch).or().eq(LocItem::getBatch, ""));
            }
            if (StringUtils.isNotBlank(taskItem.getFieldsIndex())) {
                locItemWrapper.eq(LocItem::getFieldsIndex, taskItem.getFieldsIndex());
            } else {
                locItemWrapper.and(w -> w.isNull(LocItem::getFieldsIndex).or().eq(LocItem::getFieldsIndex, ""));
            }
            LocItem locItem = locItemService.getOne(locItemWrapper);
            log.info("[拣料入库-扣减库位] taskItemId={}, locId={}, matnrId={}, batch={}, fieldsIndex={}, locItem={}, locItem.anfme={}, taskItem.qty={}, taskItem.anfme={}",
                    taskItem.getId(), loc.getId(), taskItem.getMatnrId(), taskItem.getBatch(), taskItem.getFieldsIndex(),
                    locItem != null ? "存在" : "null", locItem != null ? locItem.getAnfme() : null, taskItem.getQty(), taskItem.getAnfme());
            if (Objects.nonNull(locItem)) {
                // 计算扣减后的数量
                Double newAnfme = Math.round((locItem.getAnfme() - taskItem.getQty()) * 1000000) / 1000000.0;
                if (newAnfme.compareTo(0.0) <= 0) {
                // 扣减量:优先用本次拣料数量 taskItem.getQty();若为 0 且 taskItem.anfme 已为剩余数量,则扣减 = 原库位 - 剩余(用 BigDecimal 避免 Double 精度问题)
                Double deductQty = QuantityUtils.isPositive(taskItem.getQty())
                        ? taskItem.getQty()
                        : (taskItem.getAnfme() != null && QuantityUtils.compare(taskItem.getAnfme(), locItem.getAnfme()) < 0
                                ? QuantityUtils.subtract(locItem.getAnfme(), taskItem.getAnfme())
                                : 0.0);
                Double newAnfme = QuantityUtils.subtract(locItem.getAnfme(), deductQty);
                log.info("[拣料入库-扣减库位] locItemId={}, deductQty={}, 原anfme={}, newAnfme={}, 操作={}",
                        locItem.getId(), deductQty, locItem.getAnfme(), newAnfme, QuantityUtils.isNonPositive(newAnfme) ? "删除" : "更新");
                if (QuantityUtils.isNonPositive(newAnfme)) {
                    // 数量小于等于0,删除库位明细
                    locItemService.removeById(locItem.getId());
                } else {
@@ -2032,6 +2210,9 @@
                        throw new CoolException("库位明细数量扣减失败!!");
                    }
                }
            } else {
                log.warn("[拣料入库-扣减库位] 未查到库位明细 locId={}, matnrId={}, batch={}, fieldsIndex={},未扣减",
                        loc.getId(), taskItem.getMatnrId(), taskItem.getBatch(), taskItem.getFieldsIndex());
            }
        }
    }
@@ -2075,7 +2256,7 @@
            List<TaskItem> items = orderMap.get(key);
            //保存入出库明细
            saveStockItems(items, task, pakinItem.getId(), pakinItem.getAsnCode(), pakinItem.getWkType(), pakinItem.getType(), loginUserId);
            //移出收货区库存, 修改组托状态(只有当source不为null时才需要移除收货区库存)
            // DirectWaitPakin 启用时组托来自收货区,入库完成后移出收货区库存;未启用时 source 为 null 不执行
            if (Objects.nonNull(pakinItem.getSource())) {
                removeReceiptStock(pakinItem, loginUserId);
            }
@@ -2102,6 +2283,8 @@
        if (!this.update(new LambdaUpdateWrapper<Task>().eq(Task::getId, task.getId()).set(Task::getTaskStatus, TaskStsType.UPDATED_IN.id))) {
            throw new CoolException("任务状态修改失败!!");
        }
        // 9.1 入/出库结果上报:入库完成后通知云仓
        reportInOutResultToCloud(task, loc, taskItems, pkinItemIds, true);
    }
    /**
@@ -2161,12 +2344,26 @@
                throw new CoolException("库位不存在!!");
            }
            LocItem item = new LocItem();
            LocItem locItem = locItemService.getOne(new LambdaQueryWrapper<LocItem>()
            // 构建查询条件:需要同时匹配物料ID、库位ID、批次和票号
            LambdaQueryWrapper<LocItem> locItemWrapper = new LambdaQueryWrapper<LocItem>()
                    .eq(LocItem::getMatnrId, taskItem.getMatnrId())
                    .eq(LocItem::getLocId, loc.getId())
                    .eq(StringUtils.isNotBlank(taskItem.getBatch()), LocItem::getBatch, taskItem.getBatch())
                    .eq(StringUtils.isNotBlank(taskItem.getFieldsIndex()), LocItem::getFieldsIndex, taskItem.getFieldsIndex())
            );
                    .eq(LocItem::getLocId, loc.getId());
            // 批次匹配:如果taskItem有批次,则必须匹配;如果taskItem没有批次,则查询批次为null或空字符串的记录
            if (StringUtils.isNotBlank(taskItem.getBatch())) {
                locItemWrapper.eq(LocItem::getBatch, taskItem.getBatch());
            } else {
                locItemWrapper.and(wrapper -> wrapper.isNull(LocItem::getBatch).or().eq(LocItem::getBatch, ""));
            }
            // 票号匹配:如果taskItem有票号,则必须匹配;如果taskItem没有票号,则查询票号为null或空字符串的记录
            if (StringUtils.isNotBlank(taskItem.getFieldsIndex())) {
                locItemWrapper.eq(LocItem::getFieldsIndex, taskItem.getFieldsIndex());
            } else {
                locItemWrapper.and(wrapper -> wrapper.isNull(LocItem::getFieldsIndex).or().eq(LocItem::getFieldsIndex, ""));
            }
            LocItem locItem = locItemService.getOne(locItemWrapper);
            if (Objects.isNull(locItem)) {
                // 库位明细不存在,创建新的库位明细
                BeanUtils.copyProperties(taskItem, item);
@@ -2256,4 +2453,72 @@
            }
        }
    }
    /**
     * 9.1 入/出库结果上报待办
     * @param isInbound true 入库完成,false 出库完成
     * @param pkinItemIds 入库时组托明细 ID 集合,用于查 asnCode 作为 orderNo;出库时传 null,用 taskItem.platOrderCode
     */
    private void reportInOutResultToCloud(Task task, Loc loc, List<TaskItem> taskItems, Set<Long> pkinItemIds, boolean isInbound) {
        try {
            String locId = isInbound ? task.getTargLoc() : task.getOrgLoc();
            String wareHouseId = null;
            if (loc.getWarehouseId() != null) {
                Warehouse wh = warehouseService.getById(loc.getWarehouseId());
                if (wh != null) {
                    wareHouseId = wh.getCode();
                }
            }
            if (wareHouseId == null) {
                log.warn("入/出库结果上报待办跳过:仓库编码为空,taskId={}", task.getId());
                return;
            }
            Map<Long, String> sourceToOrderNo = new HashMap<>();
            if (isInbound && pkinItemIds != null && !pkinItemIds.isEmpty()) {
                List<WaitPakinItem> pakinItems = waitPakinItemService.list(new LambdaQueryWrapper<WaitPakinItem>().in(WaitPakinItem::getId, pkinItemIds));
                for (WaitPakinItem p : pakinItems) {
                    if (p.getAsnCode() != null) {
                        sourceToOrderNo.put(p.getId(), p.getAsnCode());
                    }
                }
            }
            ObjectMapper om = new ObjectMapper();
            Date now = new Date();
            for (TaskItem item : taskItems) {
                String orderNo = isInbound ? sourceToOrderNo.get(item.getSource()) : (item.getPlatOrderCode() != null ? item.getPlatOrderCode() : item.getPlatWorkCode());
                if (orderNo == null && isInbound) {
                    orderNo = item.getPlatOrderCode() != null ? item.getPlatOrderCode() : item.getPlatWorkCode();
                }
                if (orderNo == null || item.getMatnrCode() == null) {
                    continue;
                }
                InOutResultReportParam param = new InOutResultReportParam()
                        .setOrderNo(orderNo)
                        .setPlanNo(item.getPlatWorkCode())
                        .setLineId(item.getPlatItemId())
                        .setWareHouseId(wareHouseId)
                        .setLocId(locId)
                        .setMatNr(item.getMatnrCode())
                        .setQty(item.getAnfme() != null ? String.valueOf(item.getAnfme()) : "0")
                        .setBatch(item.getBatch());
                try {
                    String requestBody = om.writeValueAsString(param);
                    CloudWmsNotifyLog notifyLog = new CloudWmsNotifyLog()
                            .setReportType(cloudWmsNotifyLogService.getReportTypeInOutResult())
                            .setRequestBody(requestBody)
                            .setNotifyStatus(cloudWmsNotifyLogService.getNotifyStatusPending())
                            .setRetryCount(0)
                            .setBizRef("taskId=" + task.getId() + ",orderNo=" + orderNo)
                            .setCreateTime(now)
                            .setUpdateTime(now);
                    cloudWmsNotifyLogService.fillFromConfig(notifyLog);
                    cloudWmsNotifyLogService.save(notifyLog);
                } catch (JsonProcessingException e) {
                    log.warn("入/出库结果上报待办落库失败(不影响主流程),taskId={},orderNo={}:{}", task.getId(), orderNo, e.getMessage());
                }
            }
        } catch (Exception e) {
            log.warn("入/出库结果上报待办失败,taskId={},isInbound={}:{}", task.getId(), isInbound, e.getMessage());
        }
    }
}