src/main/java/com/zy/asrs/service/impl/ExternalTaskFacadeServiceImpl.java
@@ -3,6 +3,7 @@ import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.core.common.Cools; import com.core.common.R; import com.zy.asrs.entity.LocDetl; import com.zy.asrs.entity.param.MesToCombParam; import com.zy.asrs.entity.param.OpenOrderPakoutExecuteParam; import com.zy.asrs.entity.param.OutTaskParam; @@ -92,8 +93,8 @@ if (autoConfirm && Cools.isEmpty(param.getStationId())) { return R.error("stationId不能为空"); } int countLoc = locDetlService.selectCount(new EntityWrapper<com.zy.asrs.entity.LocDetl>().eq("zpallet", param.getPalletId())); if (countLoc == 0) { LocDetl locDetl = locDetlService.selectOne(new EntityWrapper<LocDetl>().eq("zpallet", param.getPalletId())); if (locDetl == null) { return R.error("库存中不存在该托盘:" + param.getPalletId()); } @@ -101,15 +102,7 @@ // 设备直调通常是单托盘出库,没有 ERP 顺序号;0 表示无序,和 /outOrder 的校验语义一致。 param.setSeq(0); } if (Cools.isEmpty(param.getBatchSeq())) { // batchSeq 是接口原始字段,明细里会保存;实际生成任务时低站点仍按 orderId 作为批次键。 param.setBatchSeq(param.getOrderId()); } if (autoConfirm && isHighStation(param.getStationId()) && Cools.isEmpty(param.getEntryWmsCode())) { // IoT 直调常见为单托盘任务,没有 ERP 进仓编号;用 orderId 作为批次键, // 这样既满足高站点订单明细校验,也能让执行后 WrkMast.batchSeq 保持可追溯。 param.setEntryWmsCode(param.getOrderId()); } fillOutboundEntryWmsCode(param, locDetl); // IoT/MQTT 默认只预创建订单。没有站点的明细会被后台生成任务逻辑跳过, // 直到人工在出库页面选择站点并执行。 @@ -125,14 +118,17 @@ return openService.pakoutOrderExecute(executeParam); } private boolean isHighStation(String stationId) { if (Cools.isEmpty(stationId)) { return false; private void fillOutboundEntryWmsCode(OutTaskParam param, LocDetl locDetl) { String entryWmsCode = param.getEntryWmsCode(); if (Cools.isEmpty(entryWmsCode) && locDetl != null) { entryWmsCode = locDetl.getStandby1(); } try { return Integer.valueOf(stationId) > 600; } catch (NumberFormatException ignored) { return false; if (Cools.isEmpty(entryWmsCode)) { entryWmsCode = param.getOrderId(); } param.setEntryWmsCode(entryWmsCode); if (Cools.isEmpty(param.getBatchSeq())) { param.setBatchSeq(entryWmsCode); } } } src/main/java/com/zy/integration/iot/biz/impl/IotInstructionServiceImpl.java
@@ -14,7 +14,6 @@ import com.zy.asrs.service.WrkDetlService; import com.zy.asrs.utils.LocAliasUtils; import com.zy.integration.iot.biz.IotInstructionService; import com.zy.iot.config.IotProperties; import com.zy.iot.constant.IotConstants; import com.zy.iot.entity.IotFeedbackMessage; import com.zy.iot.entity.IotInstructionMessage; @@ -32,7 +31,6 @@ import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Objects; /** @@ -54,8 +52,6 @@ @Autowired private IotPublishRecordService iotPublishRecordService; @Autowired private IotProperties iotProperties; @Autowired private IotDbConfigService iotDbConfigService; @Autowired @@ -135,7 +131,6 @@ OutTaskParam param = new OutTaskParam(); param.setPalletId(message.getContainerId()); param.setOrderId(resolveReferenceId(message)); param.setBatchSeq("amazon"+new Date().getTime()); param.setSeq(0); R result = externalTaskFacadeService.createOutboundTask(param, false); if (result == null || !Objects.equals(result.get("code"), 200)) { @@ -492,16 +487,106 @@ } private IotPublishRecord buildPickOutboundRecord(WrkMast wrkMast, String containerId) { List<String> destinationLocationIds = resolvePickCompletionDestinationLocationIds(wrkMast, containerId); if (Cools.isEmpty(destinationLocationIds)) { log.warn("skip iot pick completion without original destinationLocationIds, wrkNo={}, containerId={}", wrkMast.getWrkNo(), containerId); return null; } IotInstructionMessage payload = new IotInstructionMessage(); payload.setInstructionId(IotInstructionIdGenerator.generate(String.valueOf(wrkMast.getWrkNo()))); payload.setContainerId(containerId); payload.setSourceLocationId(wrkMast.getSourceLocNo()); payload.setDestinationLocationIds(Collections.singletonList(resolveRemoteStationId(wrkMast.getStaNo()))); payload.setDestinationLocationIds(destinationLocationIds); if (!Cools.isEmpty(wrkMast.getUserNo())) { payload.setReferenceId(wrkMast.getUserNo()); } payload.setCreationTime(System.currentTimeMillis()); return initOutboundRecord(wrkMast, IotConstants.MESSAGE_TYPE_PICK, iotDbConfigService.getEffectiveTopics().getIngressPick(), payload); } private List<String> resolvePickCompletionDestinationLocationIds(WrkMast wrkMast, String containerId) { IotPublishRecord inboundRecord = findProcessedInboundPickRecord(wrkMast, containerId); return inboundRecord == null ? Collections.<String>emptyList() : parseDestinationLocationIds(inboundRecord.getDestinationLocationIds()); } private IotPublishRecord findProcessedInboundPickRecord(WrkMast wrkMast, String containerId) { IotPublishRecord record = findProcessedInboundRecordByWrkNo(wrkMast.getWrkNo(), IotConstants.MESSAGE_TYPE_PICK); if (record != null) { return record; } String orderNo = wrkMast.getUserNo(); if (Cools.isEmpty(orderNo)) { orderNo = resolveOrderNo(loadWrkDetls(wrkMast)); } return findProcessedInboundRecordByBusinessKey(IotConstants.MESSAGE_TYPE_PICK, containerId, orderNo); } private IotPublishRecord findProcessedInboundRecordByWrkNo(Integer wrkNo, String messageType) { if (wrkNo == null || Cools.isEmpty(messageType)) { return null; } List<IotPublishRecord> records = iotPublishRecordService.selectList(new EntityWrapper<IotPublishRecord>() .eq("direction", IotConstants.DIRECTION_INBOUND) .eq("message_type", messageType) .eq("process_status", IotConstants.PROCESS_STATUS_PROCESSED) .eq("wrk_no", wrkNo) .orderBy("message_creation_time", false) .orderBy("create_time", false) .orderBy("id", false)); return Cools.isEmpty(records) ? null : records.get(0); } private IotPublishRecord findProcessedInboundRecordByBusinessKey(String messageType, String containerId, String bizNo) { if (Cools.isEmpty(messageType) || Cools.isEmpty(containerId) || Cools.isEmpty(bizNo)) { return null; } IotPublishRecord record = findProcessedInboundRecord(messageType, containerId, "order_no", bizNo); if (record != null) { return record; } record = findProcessedInboundRecord(messageType, containerId, "reference_id", bizNo); if (record != null) { return record; } return findProcessedInboundRecord(messageType, containerId, "instruction_id", bizNo); } private IotPublishRecord findProcessedInboundRecord(String messageType, String containerId, String column, String value) { List<IotPublishRecord> records = iotPublishRecordService.selectList(new EntityWrapper<IotPublishRecord>() .eq("direction", IotConstants.DIRECTION_INBOUND) .eq("message_type", messageType) .eq("process_status", IotConstants.PROCESS_STATUS_PROCESSED) .eq("container_id", containerId) .eq(column, value) .orderBy("message_creation_time", false) .orderBy("create_time", false) .orderBy("id", false)); return Cools.isEmpty(records) ? null : records.get(0); } private List<String> parseDestinationLocationIds(String destinationLocationIds) { if (Cools.isEmpty(destinationLocationIds)) { return Collections.emptyList(); } try { List<String> parsed = JSON.parseArray(destinationLocationIds, String.class); if (Cools.isEmpty(parsed)) { return Collections.emptyList(); } List<String> result = new ArrayList<String>(); for (String destinationLocationId : parsed) { if (!Cools.isEmpty(destinationLocationId)) { result.add(destinationLocationId); } } return result; } catch (Exception e) { log.warn("parse iot destinationLocationIds failed, value={}", destinationLocationIds, e); return Collections.emptyList(); } } private IotPublishRecord initOutboundRecord(WrkMast wrkMast, String messageType, String publishTopic, IotInstructionMessage payload) { @@ -541,18 +626,6 @@ } } return null; } private String resolveRemoteStationId(Integer staNo) { if (staNo == null) { return null; } for (Map.Entry<String, Integer> entry : iotProperties.getPickStationMappings().entrySet()) { if (Objects.equals(entry.getValue(), staNo)) { return entry.getKey(); } } return String.valueOf(staNo); } private String resolveAmazonLocationId(String locNo) { src/test/java/com/zy/asrs/service/impl/ExternalTaskFacadeServiceImplTest.java
@@ -1,6 +1,7 @@ package com.zy.asrs.service.impl; import com.core.common.R; import com.zy.asrs.entity.LocDetl; import com.zy.asrs.entity.param.OutTaskParam; import com.zy.asrs.service.LocDetlService; import com.zy.asrs.service.OpenService; @@ -19,6 +20,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -48,12 +50,28 @@ @Test void mqttPickPreCreateUsesAppendModeAndKeepsOrderNotExecutable() { OutTaskParam param = outTask("PKR1007723398", "GSL010863"); when(locDetlService.selectCount(any())).thenReturn(1); when(locDetlService.selectOne(any())).thenReturn(locDetl(null)); when(openService.outOrderCreatePakoutOrder(eq(Collections.singletonList(param)), eq(false), eq(true))).thenReturn(R.ok()); R result = service.createOutboundTask(param, false); assertEquals(200, ((Number) result.get("code")).intValue()); assertEquals("PKR1007723398", param.getEntryWmsCode()); assertEquals("PKR1007723398", param.getBatchSeq()); verify(openService).outOrderCreatePakoutOrder(eq(Collections.singletonList(param)), eq(false), eq(true)); } @Test void mqttPickPreCreateUsesStockEntryWmsCodeWhenAvailable() { OutTaskParam param = outTask("PKR1007723398", "GSL010863"); when(locDetlService.selectOne(any())).thenReturn(locDetl("ENTRY-001")); when(openService.outOrderCreatePakoutOrder(eq(Collections.singletonList(param)), eq(false), eq(true))).thenReturn(R.ok()); R result = service.createOutboundTask(param, false); assertEquals(200, ((Number) result.get("code")).intValue()); assertEquals("ENTRY-001", param.getEntryWmsCode()); assertEquals("ENTRY-001", param.getBatchSeq()); verify(openService).outOrderCreatePakoutOrder(eq(Collections.singletonList(param)), eq(false), eq(true)); } @@ -61,15 +79,28 @@ void autoConfirmKeepsReplaceModeThenExecutesOrder() { OutTaskParam param = outTask("PKR1007723398", "GSL010863"); param.setStationId("601"); when(locDetlService.selectCount(any())).thenReturn(1); when(locDetlService.selectOne(any())).thenReturn(locDetl(null)); when(openService.outOrderCreatePakoutOrder(eq(Collections.singletonList(param)), eq(true), eq(false))).thenReturn(R.ok()); when(openService.pakoutOrderExecute(any())).thenReturn(R.ok()); R result = service.createOutboundTask(param, true); assertEquals(200, ((Number) result.get("code")).intValue()); assertEquals("PKR1007723398", param.getEntryWmsCode()); assertEquals("PKR1007723398", param.getBatchSeq()); verify(openService).outOrderCreatePakoutOrder(eq(Collections.singletonList(param)), eq(true), eq(false)); verify(openService).pakoutOrderExecute(any()); } @Test void returnsErrorWhenPalletNotInStock() { OutTaskParam param = outTask("PKR1007723398", "GSL010863"); when(locDetlService.selectOne(any())).thenReturn(null); R result = service.createOutboundTask(param, false); assertEquals(500, ((Number) result.get("code")).intValue()); verify(openService, never()).outOrderCreatePakoutOrder(any(), anyBoolean(), anyBoolean()); } private static OutTaskParam outTask(String orderId, String palletId) { @@ -78,4 +109,10 @@ param.setPalletId(palletId); return param; } private static LocDetl locDetl(String standby1) { LocDetl locDetl = new LocDetl(); locDetl.setStandby1(standby1); return locDetl; } } src/test/java/com/zy/integration/iot/IotInstructionServiceImplPickTest.java
@@ -2,9 +2,12 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.mapper.Wrapper; import com.core.common.R; import com.zy.asrs.entity.WrkMast; import com.zy.asrs.entity.param.OutTaskParam; import com.zy.asrs.service.ExternalTaskFacadeService; import com.zy.asrs.service.WrkDetlService; import com.zy.integration.iot.biz.impl.IotInstructionServiceImpl; import com.zy.iot.constant.IotConstants; import com.zy.iot.entity.IotInstructionMessage; @@ -21,6 +24,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import java.util.Arrays; import java.util.Collections; import static org.mockito.ArgumentMatchers.any; @@ -37,6 +41,8 @@ private IotDbConfigService iotDbConfigService; @Mock private ExternalTaskFacadeService externalTaskFacadeService; @Mock private WrkDetlService wrkDetlService; private IotInstructionServiceImpl service; @@ -46,6 +52,7 @@ ReflectionTestUtils.setField(service, "iotPublishRecordService", iotPublishRecordService); ReflectionTestUtils.setField(service, "iotDbConfigService", iotDbConfigService); ReflectionTestUtils.setField(service, "externalTaskFacadeService", externalTaskFacadeService); ReflectionTestUtils.setField(service, "wrkDetlService", wrkDetlService); } @Test @@ -80,6 +87,8 @@ OutTaskParam outboundParam = outboundParamCaptor.getValue(); Assertions.assertEquals("PALLET-A001", outboundParam.getPalletId()); Assertions.assertEquals("OUT-ORDER-001", outboundParam.getOrderId()); Assertions.assertNull(outboundParam.getBatchSeq()); Assertions.assertNull(outboundParam.getEntryWmsCode()); Assertions.assertNull(outboundParam.getStationId()); ArgumentCaptor<IotPublishRecord> updateCaptor = ArgumentCaptor.forClass(IotPublishRecord.class); @@ -102,4 +111,46 @@ Assertions.assertEquals(IotConstants.FEEDBACK_SUCCESS, feedbackPayload.getString("status")); Assertions.assertNull(feedbackPayload.getString("errorCode")); } @Test public void shouldReportOriginalDestinationLocationIdsForPickCompletion() { WrkMast wrkMast = new WrkMast(); wrkMast.setWrkNo(3001); wrkMast.setIoType(101); wrkMast.setBarcode("PALLET-A001"); wrkMast.setUserNo("OUT-ORDER-001"); wrkMast.setSourceLocNo("SRC-01"); wrkMast.setStaNo(66); IotPublishRecord inboundRecord = new IotPublishRecord(); inboundRecord.setDestinationLocationIds(JSON.toJSONString(Arrays.asList("amazon-out-01", "amazon-out-02"))); IotTopicConfig topics = new IotTopicConfig(); topics.setIngressPick("ingressPick"); when(iotDbConfigService.isMqttEnabled()).thenReturn(true); when(iotDbConfigService.getEffectiveTopics()).thenReturn(topics); when(wrkDetlService.selectList(any(Wrapper.class))).thenReturn(Collections.emptyList()); when(iotPublishRecordService.selectCount(any(Wrapper.class))).thenReturn(0, 1, 0); when(iotPublishRecordService.selectList(any(Wrapper.class))) .thenReturn(Collections.emptyList(), Collections.singletonList(inboundRecord)); service.queueWorkCompletion(wrkMast); ArgumentCaptor<IotPublishRecord> recordCaptor = ArgumentCaptor.forClass(IotPublishRecord.class); verify(iotPublishRecordService).insert(recordCaptor.capture()); IotPublishRecord record = recordCaptor.getValue(); Assertions.assertEquals(IotConstants.DIRECTION_OUTBOUND, record.getDirection()); Assertions.assertEquals(IotConstants.MESSAGE_TYPE_PICK, record.getMessageType()); Assertions.assertEquals("ingressPick", record.getPublishTopic()); Assertions.assertEquals("[\"amazon-out-01\",\"amazon-out-02\"]", record.getDestinationLocationIds()); JSONObject payload = JSON.parseObject(record.getPublishPayload()); Assertions.assertEquals("PALLET-A001", payload.getString("containerId")); Assertions.assertEquals("SRC-01", payload.getString("sourceLocationId")); Assertions.assertEquals("OUT-ORDER-001", payload.getString("referenceId")); Assertions.assertEquals("amazon-out-01", payload.getJSONArray("destinationLocationIds").getString(0)); Assertions.assertEquals("amazon-out-02", payload.getJSONArray("destinationLocationIds").getString(1)); } }