feat(task): TransportTaskService 扩展 RPC 创建方法(TDD)

- 新增 TransportTaskService.createTransportTaskByRpc() 接口方法
- 实现幂等检查(同 bizType+bizId 下未完结任务返回已有任务ID)
- 参数完整时状态置为待下发(040),否则生成(010)
- 添加单元测试覆盖正常创建和幂等返回两种场景
- 添加服务器模块测试依赖(JUnit 5, Mockito, AssertJ)
- 将 DTO 移至正确的 Maven 子模块位置
This commit is contained in:
2026-07-14 17:35:04 +08:00
parent 5684575abe
commit c6dffbcb61
10 changed files with 393 additions and 0 deletions

View File

@@ -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<String, Object> payload;
}

View File

@@ -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;
}

View File

@@ -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<String, Object> payload;
}

View File

@@ -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<String, Object> payload;
}

View File

@@ -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);
}
}

View File

@@ -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<String, Object> requestParam;
}

View File

@@ -115,6 +115,33 @@
<groupId>cn.nl.cloud</groupId>
<artifactId>nl-spring-boot-starter-monitor</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@@ -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);
/**
* 更新搬运任务
*

View File

@@ -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) {
// 校验存在

View File

@@ -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());
}
}