diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/AcsFeedbackReqDTO.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/AcsFeedbackReqDTO.java new file mode 100644 index 00000000..19f8b76c --- /dev/null +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/AcsFeedbackReqDTO.java @@ -0,0 +1,30 @@ +package cn.code.nl.module.task.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.Map; + +/** + * ACS 状态反馈请求 DTO + */ +@Schema(description = "ACS 状态反馈 Request DTO") +@Data +public class AcsFeedbackReqDTO { + + @Schema(description = "任务ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "任务ID不能为空") + private Long taskId; + + @Schema(description = "ACS反馈状态:EXECUTING/PICKED/FINISHED/CANCELLED", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "反馈状态不能为空") + private String status; + + @Schema(description = "事件ID(幂等键)") + private String eventId; + + @Schema(description = "ACS反馈扩展数据") + private Map payload; +} diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskCallbackResultReqDTO.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskCallbackResultReqDTO.java new file mode 100644 index 00000000..439a5bc6 --- /dev/null +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskCallbackResultReqDTO.java @@ -0,0 +1,29 @@ +package cn.code.nl.module.task.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * LMS/WMS 业务处理结果回调请求 DTO + */ +@Schema(description = "RPC - 业务结果回调 Request DTO") +@Data +public class TaskCallbackResultReqDTO { + + @Schema(description = "任务ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "任务ID不能为空") + private Long taskId; + + @Schema(description = "事件ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "事件ID不能为空") + private String eventId; + + @Schema(description = "处理结果:SUCCESS/FAILED", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "处理结果不能为空") + private String result; + + @Schema(description = "结果描述") + private String message; +} diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskEventMessage.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskEventMessage.java new file mode 100644 index 00000000..5f8ee3b4 --- /dev/null +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskEventMessage.java @@ -0,0 +1,39 @@ +package cn.code.nl.module.task.dto; + +import lombok.Data; + +import java.util.Map; + +/** + * 任务事件 MQ 消息体 + */ +@Data +public class TaskEventMessage { + + /** 事件ID */ + private String eventId; + + /** 事件类型:TASK_FINISHED / TASK_CANCELLED */ + private String eventType; + + /** 任务ID */ + private Long taskId; + + /** 任务编码 */ + private String taskCode; + + /** 业务归属服务:LMS / WMS */ + private String ownerService; + + /** 业务类型 */ + private String bizType; + + /** 业务侧标识 */ + private String bizId; + + /** 业务回调处理器编码 */ + private String handleCode; + + /** 扩展数据 */ + private Map payload; +} diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskStatusCallbackReqDTO.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskStatusCallbackReqDTO.java new file mode 100644 index 00000000..123aa1c3 --- /dev/null +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskStatusCallbackReqDTO.java @@ -0,0 +1,37 @@ +package cn.code.nl.module.task.dto; + +import lombok.Data; + +import java.util.Map; + +/** + * Task→LMS/WMS 同步状态回调请求 DTO + * LMS/WMS 需依此 DTO 实现回调端点 + */ +@Data +public class TaskStatusCallbackReqDTO { + + /** 任务ID */ + private Long taskId; + + /** 任务编码 */ + private String taskCode; + + /** 回调状态:当前为 PICKED(61) */ + private String status; + + /** 业务归属服务 */ + private String ownerService; + + /** 业务类型 */ + private String bizType; + + /** 业务侧标识 */ + private String bizId; + + /** 业务回调处理器编码(LMS/WMS 内部路由) */ + private String handleCode; + + /** 扩展数据 */ + private Map payload; +} diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskStatusCallbackRespDTO.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskStatusCallbackRespDTO.java new file mode 100644 index 00000000..bdd14817 --- /dev/null +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskStatusCallbackRespDTO.java @@ -0,0 +1,28 @@ +package cn.code.nl.module.task.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Task→LMS/WMS 同步状态回调响应 DTO + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TaskStatusCallbackRespDTO { + + /** 是否成功 */ + private Boolean success; + + /** 响应消息 */ + private String message; + + public static TaskStatusCallbackRespDTO success() { + return new TaskStatusCallbackRespDTO(true, "ok"); + } + + public static TaskStatusCallbackRespDTO fail(String message) { + return new TaskStatusCallbackRespDTO(false, message); + } +} diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TransportTaskCreateReqDTO.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TransportTaskCreateReqDTO.java new file mode 100644 index 00000000..96271cbf --- /dev/null +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TransportTaskCreateReqDTO.java @@ -0,0 +1,91 @@ +package cn.code.nl.module.task.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Map; + +/** + * LMS/WMS 创建搬运任务请求 DTO + */ +@Schema(description = "RPC - 搬运任务创建 Request DTO") +@Data +public class TransportTaskCreateReqDTO { + + @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "任务名称不能为空") + private String taskName; + + @Schema(description = "业务归属服务:LMS/WMS", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "业务归属服务不能为空") + private String ownerService; + + @Schema(description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "业务类型不能为空") + private String bizType; + + @Schema(description = "业务侧标识", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "业务侧标识不能为空") + private String bizId; + + @Schema(description = "业务回调处理器编码", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "业务回调处理器编码不能为空") + private String handleCode; + + @Schema(description = "ACS任务类型", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "ACS任务类型不能为空") + private String acsTaskType; + + @Schema(description = "AGV系统类型", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "AGV系统类型不能为空") + private String agvSystemType; + + @Schema(description = "取货点1") + private String pointCode1; + + @Schema(description = "放货点1") + private String pointCode2; + + @Schema(description = "取货点2") + private String pointCode3; + + @Schema(description = "放货点2") + private String pointCode4; + + @Schema(description = "载具编码") + private String vehicleCode; + + @Schema(description = "载具编码2") + private String vehicleCode2; + + @Schema(description = "优先级") + private String priority; + + @Schema(description = "生产区域") + private String productArea; + + @Schema(description = "是否自动下发") + private String isAutoIssue; + + @Schema(description = "任务类型") + private String taskType; + + @Schema(description = "载具类型") + private String vehicleType; + + @Schema(description = "载具数量") + private Long vehicleQty; + + @Schema(description = "车号") + private String carNo; + + @Schema(description = "任务组标识") + private Long taskGroupId; + + @Schema(description = "任务组顺序号") + private Long sortSeq; + + @Schema(description = "扩展参数(JSON)") + private Map requestParam; +} diff --git a/nl-module-task/nl-module-task-server/pom.xml b/nl-module-task/nl-module-task-server/pom.xml index 75e8ae62..b1b1ff58 100644 --- a/nl-module-task/nl-module-task-server/pom.xml +++ b/nl-module-task/nl-module-task-server/pom.xml @@ -115,6 +115,33 @@ cn.nl.cloud nl-spring-boot-starter-monitor + + + org.springframework.boot + spring-boot-starter-test + test + + + org.mockito + mockito-core + + + + + org.mockito + mockito-inline + test + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java index 1afb5264..0151356a 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java @@ -4,6 +4,7 @@ import java.util.*; import jakarta.validation.*; import cn.code.nl.module.task.controller.admin.transporttask.vo.*; import cn.code.nl.module.task.dal.dataobject.transporttask.TransportTaskDO; +import cn.code.nl.module.task.dto.TransportTaskCreateReqDTO; import cn.code.nl.framework.common.pojo.PageResult; import cn.code.nl.framework.common.pojo.PageParam; @@ -22,6 +23,14 @@ public interface TransportTaskService { */ Long createTransportTask(@Valid TransportTaskSaveReqVO createReqVO); + /** + * 通过 RPC 创建搬运任务(LMS/WMS 调用) + * + * @param reqDTO 创建请求 + * @return 任务ID + */ + Long createTransportTaskByRpc(@Valid TransportTaskCreateReqDTO reqDTO); + /** * 更新搬运任务 * diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java index 386de2bf..a4367c3d 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java @@ -6,6 +6,10 @@ import cn.code.nl.module.task.controller.admin.transporttask.vo.TransportTaskPag import cn.code.nl.module.task.controller.admin.transporttask.vo.TransportTaskSaveReqVO; import cn.code.nl.module.task.dal.dataobject.transporttask.TransportTaskDO; import cn.code.nl.module.task.dal.mysql.transporttask.TransportTaskMapper; +import cn.code.nl.module.task.dto.TransportTaskCreateReqDTO; +import cn.code.nl.module.task.enums.CallbackStatusEnum; +import cn.code.nl.module.task.enums.TransportTaskStatusEnum; +import cn.hutool.core.util.StrUtil; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -37,6 +41,31 @@ public class TransportTaskServiceImpl implements TransportTaskService { return transportTask.getTaskId(); } + @Override + public Long createTransportTaskByRpc(TransportTaskCreateReqDTO reqDTO) { + // 1. 幂等检查:同 bizType + bizId 下是否有未完结任务 + TransportTaskDO existing = transportTaskMapper.selectUnfinishedByBiz( + reqDTO.getBizType(), reqDTO.getBizId()); + if (existing != null) { + return existing.getTaskId(); + } + + // 2. 构建 DO 并保存 + TransportTaskDO task = BeanUtils.toBean(reqDTO, TransportTaskDO.class); + // 参数完整 → 待下发(040),否则 → 生成(010) + boolean paramComplete = StrUtil.isNotBlank(reqDTO.getPointCode1()) + && StrUtil.isNotBlank(reqDTO.getPointCode2()); + task.setTaskStatus(paramComplete + ? String.format("%03d", TransportTaskStatusEnum.READY.getCode()) + : String.format("%03d", TransportTaskStatusEnum.CREATED.getCode())); + task.setCallbackStatus(CallbackStatusEnum.PENDING.getCode()); + task.setCallbackRetryCount(0); + task.setCreateMode("RPC"); + + transportTaskMapper.insert(task); + return task.getTaskId(); + } + @Override public void updateTransportTask(TransportTaskSaveReqVO updateReqVO) { // 校验存在 diff --git a/nl-module-task/nl-module-task-server/src/test/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImplTest.java b/nl-module-task/nl-module-task-server/src/test/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImplTest.java new file mode 100644 index 00000000..80a320e2 --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/test/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImplTest.java @@ -0,0 +1,74 @@ +package cn.code.nl.module.task.service.transporttask; + +import cn.code.nl.module.task.dal.dataobject.transporttask.TransportTaskDO; +import cn.code.nl.module.task.dal.mysql.transporttask.TransportTaskMapper; +import cn.code.nl.module.task.dto.TransportTaskCreateReqDTO; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class TransportTaskServiceImplTest { + + @Mock + private TransportTaskMapper transportTaskMapper; + + @InjectMocks + private TransportTaskServiceImpl transportTaskService; + + @Test + void testCreateTransportTaskByRpc_shouldReturnTaskId() { + // given + TransportTaskCreateReqDTO req = new TransportTaskCreateReqDTO(); + req.setTaskName("测试任务"); + req.setOwnerService("LMS"); + req.setBizType("INBOUND"); + req.setBizId("INB-001"); + req.setHandleCode("inboundHandler"); + req.setAcsTaskType("MOVE"); + req.setAgvSystemType("AGV_TYPE_A"); + + when(transportTaskMapper.selectUnfinishedByBiz("INBOUND", "INB-001")).thenReturn(null); + doAnswer(inv -> { + inv.getArgument(0, TransportTaskDO.class).setTaskId(1L); + return 1; + }).when(transportTaskMapper).insert((TransportTaskDO) any()); + + // when + Long taskId = transportTaskService.createTransportTaskByRpc(req); + + // then + assertNotNull(taskId); + verify(transportTaskMapper, times(1)).insert((TransportTaskDO) any()); + } + + @Test + void testCreateTransportTaskByRpc_shouldReturnExistingWhenDuplicated() { + // given + TransportTaskCreateReqDTO req = new TransportTaskCreateReqDTO(); + req.setTaskName("测试任务"); + req.setOwnerService("LMS"); + req.setBizType("INBOUND"); + req.setBizId("INB-001"); + req.setHandleCode("inboundHandler"); + req.setAcsTaskType("MOVE"); + req.setAgvSystemType("AGV_TYPE_A"); + + TransportTaskDO existing = TransportTaskDO.builder().taskId(99L).taskStatus("040").build(); + when(transportTaskMapper.selectUnfinishedByBiz("INBOUND", "INB-001")).thenReturn(existing); + + // when + Long taskId = transportTaskService.createTransportTaskByRpc(req); + + // then + assertNotNull(taskId); + org.assertj.core.api.Assertions.assertThat(taskId).isEqualTo(99L); + verify(transportTaskMapper, never()).insert((TransportTaskDO) any()); + } +}