package com.vincent.rsf.server.manager.service.impl; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.vincent.rsf.framework.common.R; import com.vincent.rsf.framework.exception.CoolException; import com.vincent.rsf.server.api.controller.erp.params.TaskInParam; import com.vincent.rsf.server.api.entity.dto.InTaskMsgDto; import com.vincent.rsf.server.api.service.WcsService; import com.vincent.rsf.server.manager.controller.params.PakinItem; import com.vincent.rsf.server.manager.controller.params.RcsTestParams; import com.vincent.rsf.server.manager.controller.params.WaitPakinParam; import com.vincent.rsf.server.manager.controller.params.LocToTaskParams; import com.vincent.rsf.server.manager.entity.*; import com.vincent.rsf.server.manager.enums.*; import com.vincent.rsf.server.manager.mapper.RcsTestConfigMapper; import com.vincent.rsf.server.manager.service.*; import com.vincent.rsf.server.common.constant.Constants; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; import java.util.Random; @Slf4j @Service public class RcsTestServiceImpl extends ServiceImpl implements RcsTestService { @Autowired private MatnrService matnrService; @Autowired private WaitPakinService waitPakinService; @Autowired private WcsService wcsService; @Autowired private TaskService taskService; @Autowired private LocService locService; @Autowired private LocItemService locItemService; @Autowired private DeviceSiteService deviceSiteService; @Autowired private BasStationService basStationService; @Override @Transactional(rollbackFor = Exception.class) public R executeRcsTest(RcsTestParams params, Long userId) { log.info("========== 开始执行RCS全流程自动测试 =========="); Map result = new HashMap<>(); List steps = new ArrayList<>(); try { // 1. 加载配置(如果提供了configId) RcsTestConfig config = null; if (params.getConfigId() != null) { config = this.getById(params.getConfigId()); if (config == null) { throw new CoolException("配置不存在,配置ID:" + params.getConfigId()); } // 使用配置中的参数覆盖请求参数 if (StringUtils.isBlank(params.getInboundStation()) && StringUtils.isNotBlank(config.getInboundStation())) { params.setInboundStation(config.getInboundStation()); } if (StringUtils.isBlank(params.getOutboundStation()) && StringUtils.isNotBlank(config.getOutboundStation())) { params.setOutboundStation(config.getOutboundStation()); } // 加载入库库位号数组 if ((params.getInboundLocNos() == null || params.getInboundLocNos().isEmpty()) && StringUtils.isNotBlank(config.getInboundLocNos())) { try { params.setInboundLocNos(JSON.parseArray(config.getInboundLocNos(), String.class)); } catch (Exception e) { log.warn("解析入库库位号数组失败: {}", e.getMessage()); } } // 兼容旧数据:如果inboundLocNos为空但locNo有值,使用locNo if ((params.getInboundLocNos() == null || params.getInboundLocNos().isEmpty()) && StringUtils.isNotBlank(config.getLocNo())) { params.setInboundLocNos(Collections.singletonList(config.getLocNo())); } // 加载出库库位号数组 if ((params.getOutboundLocNos() == null || params.getOutboundLocNos().isEmpty()) && StringUtils.isNotBlank(config.getOutboundLocNos())) { try { params.setOutboundLocNos(JSON.parseArray(config.getOutboundLocNos(), String.class)); } catch (Exception e) { log.warn("解析出库库位号数组失败: {}", e.getMessage()); } } if (params.getCheckStock() == null && config.getCheckStock() != null) { params.setCheckStock(config.getCheckStock() == 1); } if (StringUtils.isBlank(params.getInboundApiType()) && StringUtils.isNotBlank(config.getInboundApiType())) { params.setInboundApiType(config.getInboundApiType()); } if (params.getRandomMaterialCount() == null && config.getRandomMaterialCount() != null) { params.setRandomMaterialCount(config.getRandomMaterialCount()); } if (params.getAutoOutbound() == null && config.getAutoOutbound() != null) { params.setAutoOutbound(config.getAutoOutbound() == 1); } if (params.getMatnrCodes() == null || params.getMatnrCodes().isEmpty()) { if (StringUtils.isNotBlank(config.getMatnrCodes())) { params.setMatnrCodes(JSON.parseArray(config.getMatnrCodes(), String.class)); } } } // 2. 验证参数 // 出库测试不需要物料编号,只有入库测试或全流程测试才需要物料编号 // 如果 autoOutbound 为 true 且没有物料编号,说明是纯出库测试,跳过物料验证 boolean isPureOutboundTest = Boolean.TRUE.equals(params.getAutoOutbound()) && (params.getMatnrCodes() == null || params.getMatnrCodes().isEmpty()); if (!isPureOutboundTest && (params.getMatnrCodes() == null || params.getMatnrCodes().isEmpty())) { throw new CoolException("物料编号组不能为空!"); } // 3. 处理入库流程(仅当非纯出库测试时) String barcode = null; String locNo = null; String taskCode = null; String requestedLocNo = null; WaitPakin waitPakin = null; if (!isPureOutboundTest) { // 3.1 随机选择物料 List selectedMatnrCodes = selectRandomMaterials(params.getMatnrCodes(), params.getRandomMaterialCount()); steps.add("随机选择物料:" + selectedMatnrCodes); log.info("随机选择的物料:{}", selectedMatnrCodes); // 3.2 查询物料信息 List matnrs = matnrService.list(new LambdaQueryWrapper() .in(Matnr::getCode, selectedMatnrCodes)); if (matnrs.isEmpty()) { throw new CoolException("未找到物料信息!"); } steps.add("查询到物料数量:" + matnrs.size()); // 3.3 生成随机托盘码 barcode = generateBarcode(); steps.add("生成托盘码:" + barcode); log.info("生成的托盘码:{}", barcode); // 3.4 自动组托 WaitPakinParam waitPakinParam = new WaitPakinParam(); waitPakinParam.setBarcode(barcode); List pakinItems = new ArrayList<>(); for (Matnr matnr : matnrs) { PakinItem item = new PakinItem(); item.setMatnrId(matnr.getId()); item.setReceiptQty(10.0 + Math.random() * 90); // 随机数量10-100 pakinItems.add(item); } waitPakinParam.setItems(pakinItems); waitPakin = waitPakinService.mergeItems(waitPakinParam, userId); if (waitPakin == null) { throw new CoolException("组托失败,返回结果为空!"); } steps.add("组托成功,组托编码:" + waitPakin.getCode()); log.info("组托成功,组托编码:{},托盘码:{}", waitPakin.getCode(), barcode); // 3.5 自动入库 String inboundStation = params.getInboundStation(); if (StringUtils.isBlank(inboundStation)) { // 获取默认入库站点 List deviceSites = deviceSiteService.list(new LambdaQueryWrapper() .eq(DeviceSite::getType, TaskType.TASK_TYPE_IN.type) .last("limit 1")); if (deviceSites.isEmpty()) { throw new CoolException("未找到入库站点!"); } inboundStation = deviceSites.get(0).getSite(); } // 处理入库库位号:如果指定了多个,随机选择一个 List inboundLocNos = params.getInboundLocNos(); if (inboundLocNos != null && !inboundLocNos.isEmpty()) { // 验证库位是否存在 List validLocNos = new ArrayList<>(); for (String locCode : inboundLocNos) { Loc loc = locService.getOne(new LambdaQueryWrapper() .eq(Loc::getCode, locCode) .eq(Loc::getDeleted, 0)); if (loc != null) { validLocNos.add(locCode); } else { steps.add("警告:指定的入库库位号不存在:" + locCode); } } if (!validLocNos.isEmpty()) { // 随机选择一个库位 Collections.shuffle(validLocNos); requestedLocNo = validLocNos.get(0); if (validLocNos.size() > 1) { steps.add("从" + validLocNos.size() + "个入库库位中随机选择:" + requestedLocNo); } else { steps.add("使用指定的入库库位号:" + requestedLocNo); } } } // 兼容旧参数:如果inboundLocNos为空但locNo有值 if (requestedLocNo == null && StringUtils.isNotBlank(params.getLocNo())) { requestedLocNo = params.getLocNo(); } if ("location_allocate".equals(params.getInboundApiType())) { // 使用 location_allocate 接口(内部调用createInTask) R allocateResult = wcsService.allocateLocation(barcode, inboundStation, 1); if (allocateResult != null) { Object dataObj = allocateResult.get("data"); if (dataObj != null) { if (dataObj instanceof JSONObject) { JSONObject data = (JSONObject) dataObj; locNo = data.getString("locNo"); } else if (dataObj instanceof Map) { @SuppressWarnings("unchecked") Map data = (Map) dataObj; locNo = (String) data.get("locNo"); } } } if (StringUtils.isNotBlank(requestedLocNo) && !requestedLocNo.equals(locNo)) { steps.add("使用location_allocate接口申请入库,系统分配库位号:" + locNo + "(用户指定:" + requestedLocNo + ")"); } else { steps.add("使用location_allocate接口申请入库,库位号:" + (locNo != null ? locNo : "未分配")); } } else { // 使用 create_in_task 接口 TaskInParam taskInParam = new TaskInParam(); taskInParam.setBarcode(barcode); taskInParam.setSourceStaNo(inboundStation); taskInParam.setIoType(TaskType.TASK_TYPE_IN.type); taskInParam.setLocType1(1); taskInParam.setUser(userId); InTaskMsgDto inTaskMsgDto = wcsService.createInTask(taskInParam); locNo = inTaskMsgDto.getLocNo(); taskCode = inTaskMsgDto.getWorkNo(); if (StringUtils.isNotBlank(requestedLocNo) && locNo != null && !requestedLocNo.equals(locNo)) { steps.add("使用create_in_task接口创建入库任务,系统分配库位号:" + locNo + "(用户指定:" + requestedLocNo + "),任务编码:" + taskCode); } else { steps.add("使用create_in_task接口创建入库任务,库位号:" + (locNo != null ? locNo : "未分配") + ",任务编码:" + taskCode); } } } else { steps.add("纯出库测试,跳过物料选择、组托和入库流程"); } result.put("barcode", barcode); result.put("locNo", locNo); result.put("requestedLocNo", requestedLocNo); // 记录用户指定的库位号 if (waitPakin != null) { result.put("waitPakinCode", waitPakin.getCode()); } if (taskCode != null) { result.put("inboundTaskCode", taskCode); } // 8. 自动出库(如果配置了) if (params.getAutoOutbound() != null && params.getAutoOutbound()) { String outboundStation = params.getOutboundStation(); if (StringUtils.isBlank(outboundStation)) { // 获取默认出库站点 List deviceSites = deviceSiteService.list(new LambdaQueryWrapper() .eq(DeviceSite::getType, TaskType.TASK_TYPE_OUT.type) .last("limit 1")); if (!deviceSites.isEmpty()) { outboundStation = deviceSites.get(0).getSite(); } } if (StringUtils.isNotBlank(outboundStation)) { List outboundLocNos = params.getOutboundLocNos(); List selectedOutboundLocs = new ArrayList<>(); if (outboundLocNos != null && !outboundLocNos.isEmpty()) { // 使用用户指定的出库库位号 for (String locCode : outboundLocNos) { Loc loc = locService.getOne(new LambdaQueryWrapper() .eq(Loc::getCode, locCode) .eq(Loc::getDeleted, 0)); if (loc != null) { // 如果检查库存,验证是否有库存 if (params.getCheckStock() != null && params.getCheckStock()) { List locItems = locItemService.list(new LambdaQueryWrapper() .eq(LocItem::getLocId, loc.getId())); if (!locItems.isEmpty()) { selectedOutboundLocs.add(loc); } } else { // 不检查库存,直接添加 selectedOutboundLocs.add(loc); } } } if (selectedOutboundLocs.isEmpty()) { steps.add("指定的出库库位号均无库存或不存在,跳过自动出库"); log.warn("指定的出库库位号均无库存或不存在,跳过自动出库"); } } else { // 没有指定出库库位号,查找有库存的库位 if (params.getCheckStock() != null && params.getCheckStock()) { // 检查库存 List locsWithStock = locService.list(new LambdaQueryWrapper() .eq(Loc::getUseStatus, LocStsType.LOC_STS_TYPE_F.type) .last("limit 10")); for (Loc loc : locsWithStock) { List locItems = locItemService.list(new LambdaQueryWrapper() .eq(LocItem::getLocId, loc.getId())); if (!locItems.isEmpty()) { selectedOutboundLocs.add(loc); } } if (selectedOutboundLocs.isEmpty()) { steps.add("未找到有库存的库位,跳过自动出库"); log.warn("未找到有库存的库位,跳过自动出库"); } } else { // 不检查库存,随机选择一个库位(后续会使用固定模拟物料000267) List locs = locService.list(new LambdaQueryWrapper() .eq(Loc::getUseStatus, LocStsType.LOC_STS_TYPE_F.type) .last("limit 10")); if (!locs.isEmpty()) { Collections.shuffle(locs); selectedOutboundLocs.add(locs.get(0)); steps.add("不检查库存,随机选择库位:" + locs.get(0).getCode() + ",将使用固定模拟物料000267"); } } } // 处理出库:随机选择单个或多个库位组合 if (!selectedOutboundLocs.isEmpty()) { Random random = new Random(); // 随机决定:单库位出库 或 多库位组合出库(最多3个库位) boolean useMultipleLocs = random.nextBoolean() && selectedOutboundLocs.size() > 1; int locCount = useMultipleLocs ? Math.min(random.nextInt(selectedOutboundLocs.size()) + 1, 3) : 1; // 随机选择库位 Collections.shuffle(selectedOutboundLocs); List finalOutboundLocs = selectedOutboundLocs.subList(0, Math.min(locCount, selectedOutboundLocs.size())); if (finalOutboundLocs.size() == 1) { steps.add("随机选择单库位出库:" + finalOutboundLocs.get(0).getCode()); } else { steps.add("随机选择" + finalOutboundLocs.size() + "个库位组合出库:" + finalOutboundLocs.stream().map(Loc::getCode).collect(java.util.stream.Collectors.joining(", "))); } // 为每个库位生成出库任务 List outboundTaskCodes = new ArrayList<>(); List matnrCodes = params.getMatnrCodes(); boolean hasMatnrFilter = matnrCodes != null && !matnrCodes.isEmpty(); boolean checkStockFlag = params.getCheckStock() != null && params.getCheckStock(); for (Loc outboundLoc : finalOutboundLocs) { try { LocToTaskParams taskParams = new LocToTaskParams(); List locItems; if (hasMatnrFilter && checkStockFlag) { // 如果指定了物料组且检查库存,只查找包含这些物料的库存 List matnrIds = new ArrayList<>(); for (String matnrCode : matnrCodes) { Matnr matnr = matnrService.getOne(new LambdaQueryWrapper() .eq(Matnr::getCode, matnrCode) .eq(Matnr::getDeleted, 0) .last("limit 1")); if (matnr != null) { matnrIds.add(String.valueOf(matnr.getId())); } } if (!matnrIds.isEmpty()) { // 将String列表转换为Long列表 List matnrIdLongs = new ArrayList<>(); for (String matnrIdStr : matnrIds) { try { matnrIdLongs.add(Long.parseLong(matnrIdStr)); } catch (NumberFormatException e) { log.warn("物料ID格式错误: {}", matnrIdStr); } } if (!matnrIdLongs.isEmpty()) { locItems = locItemService.list(new LambdaQueryWrapper() .eq(LocItem::getLocId, outboundLoc.getId()) .in(LocItem::getMatnrId, matnrIdLongs) .last("limit 1")); } else { locItems = new ArrayList<>(); } } else { locItems = new ArrayList<>(); } } else if (!checkStockFlag) { // 不检查库存,使用固定模拟物料000267 Matnr fixedMatnr = matnrService.getOne(new LambdaQueryWrapper() .eq(Matnr::getCode, "000267") .eq(Matnr::getDeleted, 0) .last("limit 1")); if (fixedMatnr != null) { // 创建一个模拟的LocItem用于生成任务 locItems = new ArrayList<>(); LocItem mockLocItem = new LocItem(); mockLocItem.setLocId(outboundLoc.getId()); mockLocItem.setMatnrId(fixedMatnr.getId()); mockLocItem.setQty(1.0); // 默认数量 locItems.add(mockLocItem); steps.add("不检查库存,使用固定模拟物料:000267"); } else { // 如果找不到000267,尝试从库位中获取任意一个库存项 locItems = locItemService.list(new LambdaQueryWrapper() .eq(LocItem::getLocId, outboundLoc.getId()) .last("limit 1")); if (locItems.isEmpty()) { steps.add("警告:库位 " + outboundLoc.getCode() + " 无库存,且未找到固定模拟物料000267"); } } } else { // 检查库存但没有指定物料组,查找任意库存 locItems = locItemService.list(new LambdaQueryWrapper() .eq(LocItem::getLocId, outboundLoc.getId()) .last("limit 1")); } if (!locItems.isEmpty()) { taskParams.setItems(locItems); taskParams.setSiteNo(outboundStation); taskParams.setType(Constants.TASK_TYPE_OUT_STOCK); taskParams.setTarLoc(outboundLoc.getCode()); locItemService.generateTask(TaskResouceType.TASK_RESOUCE_ORDER_TYPE.val, taskParams, userId); steps.add("生成出库任务成功,源库位:" + outboundLoc.getCode() + ",目标站点:" + outboundStation); log.info("生成出库任务成功,源库位:{},目标站点:{}", outboundLoc.getCode(), outboundStation); // 查询刚生成的任务并下发到RCS List outTasks = taskService.list(new LambdaQueryWrapper() .eq(Task::getOrgLoc, outboundLoc.getCode()) .eq(Task::getTargSite, outboundStation) .eq(Task::getTaskStatus, TaskStsType.GENERATE_OUT.id) .orderByDesc(Task::getCreateTime) .last("limit 1")); if (!outTasks.isEmpty()) { taskService.pubTaskToWcs(outTasks); outboundTaskCodes.add(outTasks.get(0).getTaskCode()); steps.add("出库任务已下发到RCS,任务编码:" + outTasks.get(0).getTaskCode()); } } else { steps.add("库位 " + outboundLoc.getCode() + " 无符合条件的库存,跳过生成出库任务"); } } catch (Exception e) { log.error("生成出库任务失败,库位:{}", outboundLoc.getCode(), e); steps.add("生成出库任务失败,库位:" + outboundLoc.getCode() + ",错误:" + e.getMessage()); } } if (!outboundTaskCodes.isEmpty()) { result.put("outboundTaskCodes", outboundTaskCodes); result.put("outboundTaskCode", outboundTaskCodes.get(0)); // 兼容旧字段 } } } } result.put("steps", steps); result.put("success", true); log.info("========== RCS全流程自动测试完成 =========="); return R.ok(result); } catch (Exception e) { log.error("========== RCS全流程自动测试失败 ==========", e); steps.add("测试失败:" + e.getMessage()); result.put("steps", steps); result.put("success", false); result.put("error", e.getMessage()); return R.error("测试失败:" + e.getMessage()).add(result); } } /** * 随机选择物料 */ private List selectRandomMaterials(List matnrCodes, Integer count) { if (matnrCodes == null || matnrCodes.isEmpty()) { return new ArrayList<>(); } List shuffled = new ArrayList<>(matnrCodes); Collections.shuffle(shuffled); int selectCount = Math.min(count != null ? count : 1, shuffled.size()); return shuffled.subList(0, selectCount); } /** * 生成随机托盘码 */ private String generateBarcode() { return "TEST_" + System.currentTimeMillis() + "_" + (int)(Math.random() * 10000); } }