diff --git a/doc/handler-code-dispatch-example.md b/doc/handler-code-dispatch-example.md new file mode 100644 index 00000000..44e4ea46 --- /dev/null +++ b/doc/handler-code-dispatch-example.md @@ -0,0 +1,405 @@ +# handler_code 分发到对应 Handler 案例代码 + +## 1. 核心链路 + +```text +MQ/RPC 收到 Task 事件 + -> 读取 handlerCode + -> handlerMap.get(handlerCode) + -> 根据 eventType 调用 onTaskFinished / onTaskCancelled +``` + +旧版: + +```text +handle_class -> Class.forName -> StockAreaCallTubeTask#updateTaskStatus +``` + +新版: + +```text +handler_code -> handlerMap -> StockAreaCallTubeTaskHandler#onTaskFinished +``` + +## 2. 消息 DTO + +```java +package cn.code.nl.module.lms.taskcallback.dto; + +import lombok.Data; +import java.util.Map; + +@Data +public class TaskEventMessage { + private String eventId; + private String eventType; // TASK_FINISHED / TASK_CANCELLED / TASK_EXECUTING + private Long taskId; + private String taskNo; + private String ownerService; // LMS / WMS + private String bizType; + private String bizId; + private String handlerCode; + private Map payload; +} +``` + +## 3. 统一 Handler 接口 + +```java +package cn.code.nl.module.lms.taskcallback.handler; + +import cn.code.nl.module.lms.taskcallback.dto.TaskEventMessage; + +public interface TaskBizCallbackHandler { + String getHandlerCode(); + void onTaskFinished(TaskEventMessage message); + void onTaskCancelled(TaskEventMessage message); + + default void onTaskExecuting(TaskEventMessage message) { + } +} +``` + +## 4. StockAreaCallTubeTaskHandler 示例 + +对应旧版 `StockAreaCallTubeTask#updateTaskStatus`。 + +```java +package cn.code.nl.module.lms.taskcallback.handler.impl; + +import cn.code.nl.module.lms.taskcallback.dto.TaskEventMessage; +import cn.code.nl.module.lms.taskcallback.handler.TaskBizCallbackHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class StockAreaCallTubeTaskHandler implements TaskBizCallbackHandler { + + public static final String HANDLER_CODE = "LMS_STOCK_AREA_CALL_TUBE"; + + // private final BstIvtStockingivtService stockingivtService; + // private final PdmBiSlittingproductionplanService slittingproductionplanService; + // private final LmsTaskBizRecordService lmsTaskBizRecordService; + + @Override + public String getHandlerCode() { + return HANDLER_CODE; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void onTaskFinished(TaskEventMessage message) { + Map payload = message.getPayload(); + String startPointCode = getString(payload, "startPointCode"); + String endPointCode = getString(payload, "endPointCode"); + String vehicleCode = getString(payload, "vehicleCode"); + String toMaterial = getString(payload, "to_material"); + + log.info("备货区送纸管任务完成回调,taskId={}, bizId={}, start={}, end={}, vehicle={}", + message.getTaskId(), message.getBizId(), startPointCode, endPointCode, vehicleCode); + + /* + * 迁移旧版 FINISHED 分支逻辑: + * 1. 查询起点备货位 + * 2. 查询终点备货位 + * 3. 终点设置为有货并写入 vehicle_code + * 4. 起点清空 vehicle_code 并设置为空 + * 5. 根据 to_material 恢复分切计划 is_paper_ok + * 6. 更新 LMS 业务记录为完成 + */ + + // BstIvtStockingivt startPoint = stockingivtService.getPointByCode(startPointCode, false); + // BstIvtStockingivt endPoint = stockingivtService.getPointByCode(endPointCode, false); + // endPoint.setIvtStatus("1"); + // endPoint.setVehicleCode(vehicleCode); + // stockingivtService.updateById(endPoint); + // startPoint.setVehicleCode(""); + // startPoint.setIvtStatus("0"); + // stockingivtService.updateById(startPoint); + // if (StrUtil.isNotBlank(toMaterial)) { + // List tubes = Arrays.asList(toMaterial.split(",")); + // slittingproductionplanService.restorePaperOkByMaterials(tubes); + // } + // lmsTaskBizRecordService.finish(message.getBizId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void onTaskCancelled(TaskEventMessage message) { + log.info("备货区送纸管任务取消回调,taskId={}, bizId={}", + message.getTaskId(), message.getBizId()); + + /* + * 迁移旧版 cancel 或取消分支逻辑: + * 1. 恢复 LMS 业务记录状态 + * 2. 释放目标点位预占 + * 3. 恢复起点资源状态 + * 4. 记录取消原因 + */ + + // lmsTaskBizRecordService.cancel(message.getBizId(), "Task 服务通知取消"); + } + + private String getString(Map payload, String key) { + return payload == null || payload.get(key) == null ? null : String.valueOf(payload.get(key)); + } +} +``` + +## 5. 另一个 Handler 示例 + +```java +@Component +@RequiredArgsConstructor +public class SendAirShaftAgvTaskHandler implements TaskBizCallbackHandler { + + public static final String HANDLER_CODE = "LMS_SEND_AIR_SHAFT_AGV"; + + @Override + public String getHandlerCode() { + return HANDLER_CODE; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void onTaskFinished(TaskEventMessage message) { + // 处理送气涨轴 AGV 任务完成逻辑 + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void onTaskCancelled(TaskEventMessage message) { + // 处理送气涨轴 AGV 任务取消逻辑 + } +} +``` + +## 6. Handler 注册表 + +Spring 启动时自动收集所有 `TaskBizCallbackHandler`。 + +```java +package cn.code.nl.module.lms.taskcallback.handler; + +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class TaskBizCallbackHandlerRegistry { + + private final List handlers; + private final Map handlerMap = new HashMap<>(); + + public TaskBizCallbackHandlerRegistry(List handlers) { + this.handlers = handlers; + } + + @PostConstruct + public void init() { + for (TaskBizCallbackHandler handler : handlers) { + String handlerCode = handler.getHandlerCode(); + if (handlerMap.containsKey(handlerCode)) { + throw new IllegalStateException("重复的任务业务处理器编码:" + handlerCode); + } + handlerMap.put(handlerCode, handler); + log.info("注册任务业务处理器:{} -> {}", handlerCode, handler.getClass().getName()); + } + } + + public TaskBizCallbackHandler getRequiredHandler(String handlerCode) { + TaskBizCallbackHandler handler = handlerMap.get(handlerCode); + if (handler == null) { + throw new IllegalArgumentException("未找到任务业务处理器,handlerCode=" + handlerCode); + } + return handler; + } +} +``` + +注册结果类似: + +```text +LMS_STOCK_AREA_CALL_TUBE -> StockAreaCallTubeTaskHandler +LMS_SEND_AIR_SHAFT_AGV -> SendAirShaftAgvTaskHandler +``` + +## 7. 分发器 + +```java +package cn.code.nl.module.lms.taskcallback.service; + +import cn.code.nl.module.lms.taskcallback.dto.TaskEventMessage; +import cn.code.nl.module.lms.taskcallback.handler.TaskBizCallbackHandler; +import cn.code.nl.module.lms.taskcallback.handler.TaskBizCallbackHandlerRegistry; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TaskBizCallbackDispatcher { + + private final TaskBizCallbackHandlerRegistry handlerRegistry; + + public void dispatch(TaskEventMessage message) { + TaskBizCallbackHandler handler = handlerRegistry.getRequiredHandler(message.getHandlerCode()); + + switch (message.getEventType()) { + case "TASK_FINISHED": + handler.onTaskFinished(message); + break; + case "TASK_CANCELLED": + handler.onTaskCancelled(message); + break; + case "TASK_EXECUTING": + handler.onTaskExecuting(message); + break; + default: + throw new IllegalArgumentException("不支持的任务事件类型:" + message.getEventType()); + } + } +} +``` + +## 8. MQ 消费者使用分发器 + +LMS 订阅: + +```text +Topic = TASK_EVENT_TOPIC +Tag = LMS +``` + +消费者: + +```java +package cn.code.nl.module.lms.taskcallback.mq; + +import cn.code.nl.module.lms.taskcallback.dto.TaskEventMessage; +import cn.code.nl.module.lms.taskcallback.service.TaskBizCallbackDispatcher; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LmsTaskEventConsumer { + + private final TaskBizCallbackDispatcher dispatcher; + + public void onMessage(TaskEventMessage message) { + if (!"LMS".equals(message.getOwnerService())) { + log.warn("LMS 收到非 LMS 任务事件,ownerService={}, taskId={}", + message.getOwnerService(), message.getTaskId()); + return; + } + dispatcher.dispatch(message); + } +} +``` + +RocketMQ 伪代码: + +```java +@RocketMQMessageListener( + topic = "TASK_EVENT_TOPIC", + consumerGroup = "lms-task-event-consumer-group", + selectorExpression = "LMS" +) +@Component +@RequiredArgsConstructor +public class LmsTaskEventRocketConsumer implements RocketMQListener { + + private final TaskBizCallbackDispatcher dispatcher; + + @Override + public void onMessage(TaskEventMessage message) { + dispatcher.dispatch(message); + } +} +``` + +## 9. 幂等建议 + +生产环境建议在 `dispatch` 前做消费幂等: + +```text +1. 根据 eventId 查询消费日志 +2. 如果已经 SUCCESS,直接 return +3. 如果没有,则插入 PROCESSING +4. 执行业务 handler +5. 更新消费日志为 SUCCESS +6. 异常时回滚或标记 FAILED,等待 MQ 重试 +``` + +伪代码: + +```java +@Transactional(rollbackFor = Exception.class) +public void dispatch(TaskEventMessage message) { + // if (consumeLogService.isSuccess(message.getEventId())) { + // return; + // } + // consumeLogService.markProcessing(message); + + TaskBizCallbackHandler handler = handlerRegistry.getRequiredHandler(message.getHandlerCode()); + + if ("TASK_FINISHED".equals(message.getEventType())) { + handler.onTaskFinished(message); + } else if ("TASK_CANCELLED".equals(message.getEventType())) { + handler.onTaskCancelled(message); + } else if ("TASK_EXECUTING".equals(message.getEventType())) { + handler.onTaskExecuting(message); + } else { + throw new IllegalArgumentException("不支持的任务事件类型:" + message.getEventType()); + } + + // consumeLogService.markSuccess(message.getEventId()); +} +``` + +## 10. 完整执行链路 + +以 `StockAreaCallTubeTask` 为例: + +```text +ACS 反馈任务完成 + -> Task 服务接收反馈 + -> Task 查询 task_task + -> owner_service = LMS + -> handler_code = LMS_STOCK_AREA_CALL_TUBE + -> Task 发布 MQ:TASK_EVENT_TOPIC, Tag = LMS + -> LMS 消费消息 + -> LmsTaskEventConsumer.onMessage + -> TaskBizCallbackDispatcher.dispatch + -> TaskBizCallbackHandlerRegistry.getRequiredHandler("LMS_STOCK_AREA_CALL_TUBE") + -> 找到 StockAreaCallTubeTaskHandler + -> 调用 StockAreaCallTubeTaskHandler.onTaskFinished + -> 执行业务完成逻辑 +``` + +最终,新版不是: + +```text +handle_class -> Class.forName -> updateTaskStatus +``` + +而是: + +```text +handler_code -> handlerMap -> onTaskFinished / onTaskCancelled +``` diff --git a/doc/task-create-and-dispatch-payload-design.md b/doc/task-create-and-dispatch-payload-design.md new file mode 100644 index 00000000..18768031 --- /dev/null +++ b/doc/task-create-and-dispatch-payload-design.md @@ -0,0 +1,631 @@ +# Task 服务创建任务与外部系统下发参数转换设计方案 + +## 1. 问题背景 + +旧版 LMS 中,类似 `StockAreaCallTubeTask` 这样的任务子类会实现: + +```java +public List addTask() +``` + +旧版 `addTask` 的核心职责是: + +```text +查询 SCH_BASE_Task + -> 读取任务字段、request_param、point_code、vehicle_code 等 + -> 组装 AcsTaskDto + -> 下发 ACS +``` + +也就是说,在旧架构中: + +```text +业务任务创建、任务表保存、外部系统 DTO 组装、ACS 下发 +``` + +都在 LMS 内的具体 `xxxTask` 子类中完成。 + +新架构中,`Task` 服务独立出来: + +```text +LMS/WMS + -> 调用 Task 服务创建任务 +Task 服务 + -> 保存任务 + -> 下发外部系统 +``` + +此时会遇到一个关键问题: + +> LMS/WMS 创建任务时,传给 Task 服务的是业务任务信息,不一定已经是 `AcsTaskDto`。但 Task 服务下发 ACS 时需要 `AcsTaskDto` 或类似外部系统 DTO。这个转换逻辑应该放在哪里? + +如果让 LMS/WMS 直接传 `AcsTaskDto`,会有两个问题: + +1. `AcsTaskDto` 是外部系统下发模型,不适合作为业务创建任务模型; +2. Task 服务还需要保存标准任务表,不能只保存一个外部 DTO。 + +因此需要重新设计“创建任务模型”和“下发模型”的边界。 + +## 2. 核心结论 + +建议将旧版 `addTask` 拆成两部分: + +```text +LMS/WMS:负责构建业务任务创建请求 TaskCreateReqDTO +Task 服务:负责保存标准任务数据,并在下发前转换成外部系统 DTO +``` + +也就是说: + +```text +业务服务不要直接传 AcsTaskDto +Task 服务不要依赖 LMS/WMS 业务类 +双方通过标准 TaskCreateReqDTO 交互 +Task 服务内部通过 ExternalTaskPayloadBuilder 构建 AcsTaskDto +``` + +推荐架构: + +```text +LMS/WMS + -> TaskCreateReqDTO 标准创建请求 +Task 服务 + -> 保存 task_task 标准字段 + -> 保存 request_payload 扩展字段 + -> 根据 external_system + external_task_type + dispatch_template_code 选择构建器 + -> 转换为 AcsTaskDto / WcsTaskDto / StackerTaskDto + -> 下发外部系统 +``` + +## 3. 新旧职责对照 + +| 职责 | 旧版 | 新版 | +| --- | --- | --- | +| 业务校验 | `xxxTask#createTask` | LMS/WMS 业务服务 | +| 任务保存 | LMS `SCH_BASE_Task` | Task 服务 `task_task` | +| 业务回调定位 | `handle_class` | `owner_service + handler_code` | +| 外部 DTO 组装 | `xxxTask#addTask` | Task 服务 `ExternalTaskPayloadBuilder` | +| 外部系统下发 | `AbstractAcsTask#notifyAcs` | Task 服务 `ExternalTaskDispatcher` | +| 完成/取消业务逻辑 | `xxxTask#updateTaskStatus` | LMS/WMS `TaskBizCallbackHandler` | + +## 4. 为什么不建议 LMS/WMS 直接传 AcsTaskDto + +不建议让 LMS/WMS 创建任务时直接传 `AcsTaskDto`,原因如下: + +### 4.1 业务创建模型和外部下发模型不是一回事 + +业务创建任务时关心的是: + +```text +我要搬什么 +从哪里搬 +搬到哪里 +归属哪个业务 +完成后回调哪个 handler +业务扩展参数是什么 +``` + +外部系统下发时关心的是: + +```text +ACS task_type +start_device_code +next_device_code +vehicle_code +agv_system_type +route_plan_code +interaction_json +``` + +两者不应该强耦合。 + +### 4.2 Task 服务需要标准化保存任务 + +Task 服务需要保存: + +```text +task_id +task_no +owner_service +biz_type +biz_id +handler_code +external_system +status +start_point_code +end_point_code +vehicle_code +request_payload +``` + +如果只收到 `AcsTaskDto`,Task 服务会丢失业务归属、回调 handler、业务 ID 等关键字段。 + +### 4.3 外部系统 DTO 可能变化 + +未来可能不止 ACS: + +```text +ACS +WCS +AGV +STACKER +PLC +``` + +如果业务服务直接构建外部 DTO,会导致业务服务感知所有外部系统协议,不利于统一下发和协议升级。 + +## 5. 推荐数据模型 + +### 5.1 TaskCreateReqDTO + +LMS/WMS 调用 Task 服务创建任务时,传标准创建请求。 + +```java +@Data +public class TaskCreateReqDTO { + private String ownerService; + private String bizType; + private String bizId; + private String handlerCode; + + private String externalSystem; + private String externalTaskType; + private String dispatchTemplateCode; + + private String startPointCode; + private String endPointCode; + private String startPointCode2; + private String endPointCode2; + + private String vehicleCode; + private String vehicleCode2; + private Integer priority; + private String productArea; + + private Map requestPayload; +} +``` + +字段说明: + +| 字段 | 说明 | +| --- | --- | +| `ownerService` | LMS/WMS,决定完成取消回调发给谁 | +| `bizType` | 业务类型,例如 `STOCK_AREA_CALL_TUBE` | +| `bizId` | 业务侧 ID | +| `handlerCode` | 业务回调 handler 编码 | +| `externalSystem` | 外部系统,例如 ACS/WCS/STACKER | +| `externalTaskType` | 外部系统任务类型 | +| `dispatchTemplateCode` | 下发参数构建模板编码 | +| `startPointCode` | 起点 | +| `endPointCode` | 终点 | +| `vehicleCode` | 载具、架子、托盘等 | +| `requestPayload` | 扩展参数 | + +### 5.2 TaskDO 保存字段 + +Task 服务保存标准字段和扩展 JSON: + +```text +id +task_no +owner_service +biz_type +biz_id +handler_code +external_system +external_task_type +dispatch_template_code +start_point_code +end_point_code +start_point_code2 +end_point_code2 +vehicle_code +vehicle_code2 +priority +product_area +status +request_payload +dispatch_payload +result_payload +``` + +## 6. 下发参数转换设计 + +### 6.1 定义统一构建器接口 + +Task 服务内部定义: + +```java +public interface ExternalTaskPayloadBuilder { + + /** + * 外部系统,例如 ACS/WCS/STACKER + */ + String getExternalSystem(); + + /** + * 下发模板编码,例如 STOCK_AREA_CALL_TUBE + */ + String getDispatchTemplateCode(); + + /** + * 根据 TaskDO 构建外部系统 DTO + */ + T build(TaskDO task); +} +``` + +### 6.2 ACS 纸管搬运构建器 + +旧版 `StockAreaCallTubeTask#addTask` 逻辑迁移到 Task 服务的构建器中。 + +```java +@Component +public class AcsStockAreaCallTubePayloadBuilder implements ExternalTaskPayloadBuilder { + + @Override + public String getExternalSystem() { + return "ACS"; + } + + @Override + public String getDispatchTemplateCode() { + return "STOCK_AREA_CALL_TUBE"; + } + + @Override + public AcsTaskDto build(TaskDO task) { + Map payload = task.getRequestPayloadMap(); + + return AcsTaskDto.builder() + .extTaskId(String.valueOf(task.getId())) + .taskCode(task.getTaskNo()) + .taskType(task.getExternalTaskType()) + .startDeviceCode(task.getStartPointCode()) + .nextDeviceCode(task.getEndPointCode()) + .startDeviceCode2(task.getStartPointCode2()) + .nextDeviceCode2(task.getEndPointCode2()) + .vehicleCode(task.getVehicleCode()) + .interactionJson(payload) + .agvSystemType("2") + .priority(String.valueOf(task.getPriority())) + .productArea(task.getProductArea()) + .build(); + } +} +``` + +对应旧版逻辑: + +```text +StockAreaCallTubeTask#addTask + -> 查询任务 + -> 构建 AcsTaskDto +``` + +新版变为: + +```text +AcsStockAreaCallTubePayloadBuilder#build + -> 根据 TaskDO 构建 AcsTaskDto +``` + +### 6.3 通用 ACS 构建器 + +如果很多 ACS 任务字段差不多,也可以提供一个通用构建器: + +```java +@Component +public class AcsDefaultPayloadBuilder implements ExternalTaskPayloadBuilder { + + @Override + public String getExternalSystem() { + return "ACS"; + } + + @Override + public String getDispatchTemplateCode() { + return "DEFAULT"; + } + + @Override + public AcsTaskDto build(TaskDO task) { + return AcsTaskDto.builder() + .extTaskId(String.valueOf(task.getId())) + .taskCode(task.getTaskNo()) + .taskType(task.getExternalTaskType()) + .startDeviceCode(task.getStartPointCode()) + .nextDeviceCode(task.getEndPointCode()) + .vehicleCode(task.getVehicleCode()) + .priority(String.valueOf(task.getPriority())) + .productArea(task.getProductArea()) + .interactionJson(task.getRequestPayloadMap()) + .build(); + } +} +``` + +特殊任务再单独实现专用 Builder。 + +## 7. Builder 注册表 + +Task 服务启动时收集所有 Builder。 + +```java +@Component +public class ExternalTaskPayloadBuilderRegistry { + + private final Map> builderMap = new HashMap<>(); + + public ExternalTaskPayloadBuilderRegistry(List> builders) { + for (ExternalTaskPayloadBuilder builder : builders) { + String key = buildKey(builder.getExternalSystem(), builder.getDispatchTemplateCode()); + if (builderMap.containsKey(key)) { + throw new IllegalStateException("重复的下发参数构建器:" + key); + } + builderMap.put(key, builder); + } + } + + public ExternalTaskPayloadBuilder getRequiredBuilder(String externalSystem, String dispatchTemplateCode) { + String key = buildKey(externalSystem, dispatchTemplateCode); + ExternalTaskPayloadBuilder builder = builderMap.get(key); + if (builder == null) { + throw new IllegalArgumentException("未找到下发参数构建器:" + key); + } + return builder; + } + + private String buildKey(String externalSystem, String dispatchTemplateCode) { + return externalSystem + ":" + dispatchTemplateCode; + } +} +``` + +例如: + +```text +ACS:DEFAULT -> AcsDefaultPayloadBuilder +ACS:STOCK_AREA_CALL_TUBE -> AcsStockAreaCallTubePayloadBuilder +STACKER:INBOUND -> StackerInboundPayloadBuilder +``` + +## 8. 外部系统下发器设计 + +Builder 只负责构建 DTO,下发器负责调用外部系统。 + +```java +public interface ExternalTaskDispatcher { + String getExternalSystem(); + DispatchResult dispatch(TaskDO task, Object payload); + CancelExternalResult cancelExternal(TaskDO task); +} +``` + +ACS 下发器示例: + +```java +@Component +public class AcsTaskDispatcher implements ExternalTaskDispatcher { + + @Override + public String getExternalSystem() { + return "ACS"; + } + + @Override + public DispatchResult dispatch(TaskDO task, Object payload) { + AcsTaskDto acsTask = (AcsTaskDto) payload; + // 调用 ACS HTTP 接口 + // acsClient.createTask(Collections.singletonList(acsTask)); + return DispatchResult.success(); + } + + @Override + public CancelExternalResult cancelExternal(TaskDO task) { + // 调用 ACS 取消接口 + return CancelExternalResult.success(); + } +} +``` + +## 9. Task 下发流程 + +Task 服务下发任务时: + +```text +1. 查询 READY 状态任务 +2. 根据 external_system + dispatch_template_code 找 Builder +3. Builder 根据 TaskDO 构建 AcsTaskDto/WcsTaskDto +4. 保存 dispatch_payload +5. 根据 external_system 找 Dispatcher +6. Dispatcher 调用外部系统 +7. 成功后状态改为 ISSUED +``` + +伪代码: + +```java +public void dispatchTask(Long taskId) { + TaskDO task = taskMapper.selectById(taskId); + + ExternalTaskPayloadBuilder builder = builderRegistry.getRequiredBuilder( + task.getExternalSystem(), + task.getDispatchTemplateCode() + ); + + Object externalPayload = builder.build(task); + task.setDispatchPayload(JsonUtils.toJsonString(externalPayload)); + task.setStatus(TaskStatus.ISSUING); + taskMapper.updateById(task); + + ExternalTaskDispatcher dispatcher = dispatcherRegistry.getRequiredDispatcher(task.getExternalSystem()); + DispatchResult result = dispatcher.dispatch(task, externalPayload); + + if (result.isSuccess()) { + task.setStatus(TaskStatus.ISSUED); + } else { + task.setStatus(TaskStatus.FAILED); + task.setErrorMsg(result.getMessage()); + } + taskMapper.updateById(task); +} +``` + +## 10. StockAreaCallTubeTask 新版完整链路 + +### 10.1 LMS 创建任务 + +LMS 不传 `AcsTaskDto`,而是传标准 `TaskCreateReqDTO`: + +```json +{ + "ownerService": "LMS", + "bizType": "STOCK_AREA_CALL_TUBE", + "bizId": "LMS-BIZ-10001", + "handlerCode": "LMS_STOCK_AREA_CALL_TUBE", + "externalSystem": "ACS", + "externalTaskType": "3", + "dispatchTemplateCode": "STOCK_AREA_CALL_TUBE", + "startPointCode": "STOCK_A01", + "endPointCode": "ROBOT_STOCK_A01", + "startPointCode2": "", + "endPointCode2": "", + "vehicleCode": "TUBE_CAR_001", + "priority": 1, + "productArea": "BLK", + "requestPayload": { + "to_material": "M001,M002" + } +} +``` + +### 10.2 Task 保存任务 + +Task 保存为标准任务记录: + +```text +owner_service = LMS +handler_code = LMS_STOCK_AREA_CALL_TUBE +external_system = ACS +dispatch_template_code = STOCK_AREA_CALL_TUBE +request_payload = {to_material: ...} +``` + +### 10.3 Task 下发 ACS + +Task 根据: + +```text +external_system = ACS +dispatch_template_code = STOCK_AREA_CALL_TUBE +``` + +找到: + +```text +AcsStockAreaCallTubePayloadBuilder +``` + +构建: + +```text +AcsTaskDto +``` + +再由: + +```text +AcsTaskDispatcher +``` + +下发 ACS。 + +### 10.4 ACS 完成后业务回调 + +ACS 完成后: + +```text +Task 接收反馈 + -> 发布 TASK_FINISHED + -> owner_service = LMS + -> handler_code = LMS_STOCK_AREA_CALL_TUBE + -> LMS 消费 + -> StockAreaCallTubeTaskHandler#onTaskFinished +``` + +## 11. 设计重点 + +### 11.1 TaskCreateReqDTO 不是 AcsTaskDto + +`TaskCreateReqDTO` 是任务服务标准创建模型。 + +`AcsTaskDto` 是外部系统 ACS 的下发模型。 + +二者不要混用。 + +### 11.2 addTask 职责迁移到 Builder + +旧版: + +```text +xxxTask#addTask -> AcsTaskDto +``` + +新版: + +```text +ExternalTaskPayloadBuilder#build -> AcsTaskDto +``` + +### 11.3 特殊字段放 request_payload + +如果某个任务需要特殊参数,例如: + +```text +interaction_json +route_plan_code +agv_system_type +paper_array +truss_type +``` + +可以通过两种方式处理: + +1. 常用字段提升为 TaskDO 标准字段; +2. 特殊字段放入 `request_payload`,由专用 Builder 读取。 + +### 11.4 dispatch_template_code 决定怎么转 DTO + +同样是 ACS,不同任务可能 DTO 组装方式不同,所以需要: + +```text +external_system + dispatch_template_code +``` + +联合定位 Builder。 + +## 12. 最终建议 + +不要让 LMS/WMS 直接封装 `AcsTaskDto`。 + +推荐方式是: + +```text +LMS/WMS + -> TaskCreateReqDTO +Task 服务 + -> 保存标准任务字段 + request_payload + -> ExternalTaskPayloadBuilder 转换外部 DTO + -> ExternalTaskDispatcher 下发外部系统 +``` + +这样既能让 Task 服务保存标准任务数据,又能保留旧版 `addTask` 对不同任务差异化组装外部 DTO 的能力。 + +一句话总结: + +```text +旧 addTask 不放 LMS/WMS,也不让业务服务直接传 AcsTaskDto; +旧 addTask 的 DTO 转换职责迁移到 Task 服务中的 ExternalTaskPayloadBuilder。 +``` diff --git a/doc/task-event-routing-to-business-handler-design.md b/doc/task-event-routing-to-business-handler-design.md new file mode 100644 index 00000000..8f2a211b --- /dev/null +++ b/doc/task-event-routing-to-business-handler-design.md @@ -0,0 +1,630 @@ +# 新版本任务完成/取消如何精准路由到具体业务 Handler + +## 1. 问题背景 + +旧版兰州 LMS 中,任务类例如: + +```text +StockAreaCallTubeTask +备货区送纸管到机械手旁边的备货区 - AGV任务 +``` + +它继承 `AbstractAcsTask`,创建任务时写入: + +```text +handle_class = org.nl.b_lms.sch.tasks.slitter.StockAreaCallTubeTask +``` + +当 ACS 请求任务完成时,旧系统会根据任务表里的 `handle_class` 找到对应 Spring Bean,然后执行: + +```text +StockAreaCallTubeTask#updateTaskStatus(taskObj, status) +``` + +所以旧版的路由链路是: + +```text +ACS 反馈完成 + -> LMS 接收反馈 + -> 查询 SCH_BASE_Task + -> 读取 handle_class + -> Class.forName(handle_class) + -> Spring 获取 StockAreaCallTubeTask Bean + -> 调用 updateTaskStatus +``` + +新版本中,任务模块独立为 `Task` 服务。`Task` 服务负责接收 ACS 反馈,但 `StockAreaCallTubeTask` 这类业务逻辑应该放在 `LMS` 服务中,而不是放在 `Task` 服务中。 + +因此新版本要解决两个路由问题: + +1. Task 服务如何知道这个任务完成后应该通知 LMS 还是 WMS? +2. LMS/WMS 收到消息后,如何精准定位到原来类似 `StockAreaCallTubeTask` 的业务逻辑? + +## 2. 核心结论 + +新版本不再依赖 Java 类名 `handle_class` 跨服务反射,而是使用: + +```text +owner_service + handler_code + event_type +``` + +来完成路由。 + +对应关系: + +| 旧版 | 新版 | +| --- | --- | +| `handle_class = StockAreaCallTubeTask` | `handler_code = LMS_STOCK_AREA_CALL_TUBE` | +| LMS 内反射 Java 类 | LMS 内部 handler 注册表分发 | +| `updateTaskStatus(taskObj, status)` | `onTaskFinished / onTaskCancelled` | +| 单体内方法调用 | Task 服务发 MQ 或 RPC 到业务服务 | + +新版本完整链路: + +```text +ACS 反馈任务完成 + -> Task 服务接收反馈 + -> Task 查询任务记录 + -> 得到 owner_service = LMS + -> 得到 handler_code = LMS_STOCK_AREA_CALL_TUBE + -> Task 发布 MQ:TaskFinishedEvent + -> LMS 消费属于自己的任务事件 + -> LMS 根据 handler_code 找到 StockAreaCallTubeCallbackHandler + -> 执行 onTaskFinished + -> LMS 处理完成后回调 Task 或发送处理结果事件 + -> Task 更新任务最终状态 +``` + +## 3. 任务创建时必须保存路由信息 + +新版本能不能精准定位业务 handler,关键取决于创建任务时保存的信息是否完整。 + +以 `StockAreaCallTubeTask` 为例,旧版创建任务保存: + +```text +handle_class = org.nl.b_lms.sch.tasks.slitter.StockAreaCallTubeTask +``` + +新版本创建任务时,LMS 应调用 Task 服务并传入: + +```json +{ + "ownerService": "LMS", + "bizType": "STOCK_AREA_CALL_TUBE", + "bizId": "LMS业务记录ID", + "handlerCode": "LMS_STOCK_AREA_CALL_TUBE", + "externalSystem": "ACS", + "externalTaskType": "3", + "startPointCode": "备货区起点", + "endPointCode": "机械手旁备货区终点", + "vehicleCode": "纸管载具号", + "requestPayload": { + "to_material": "xxx,xxx", + "product_area": "BLK" + } +} +``` + +其中最关键的是: + +| 字段 | 作用 | +| --- | --- | +| `ownerService` | 告诉 Task 服务后续业务回调发给谁,例如 LMS/WMS | +| `handlerCode` | 告诉业务服务内部该由哪个 handler 处理 | +| `bizType` | 业务类型,用于查询、统计、权限和辅助路由 | +| `bizId` | 业务侧记录 ID,handler 可用它查询业务数据 | +| `requestPayload` | 保存旧任务中 `request_param` 类似的扩展数据 | + +## 4. MQ Topic 应该如何设计 + +### 4.1 推荐方案:统一 Topic + owner_service 作为 Tag + +推荐使用一个统一 Topic: + +```text +TASK_EVENT_TOPIC +``` + +不同业务服务通过 Tag 区分: + +```text +Tag = LMS +Tag = WMS +``` + +消息体里包含完整路由字段: + +```json +{ + "eventId": "EVT-10001", + "eventType": "TASK_FINISHED", + "taskId": 10001, + "taskNo": "T202607130001", + "ownerService": "LMS", + "bizType": "STOCK_AREA_CALL_TUBE", + "bizId": "LMS-BIZ-10001", + "handlerCode": "LMS_STOCK_AREA_CALL_TUBE", + "externalSystem": "ACS", + "externalTaskNo": "ACS-001", + "payload": { + "to_material": "A,B,C" + } +} +``` + +LMS 只订阅: + +```text +Topic = TASK_EVENT_TOPIC +Tag = LMS +``` + +WMS 只订阅: + +```text +Topic = TASK_EVENT_TOPIC +Tag = WMS +``` + +优点: + +- Topic 数量少; +- Task 事件模型统一; +- 新增业务服务只需要新增 Tag; +- 消息结构统一,便于日志、监控、重试。 + +### 4.2 备选方案:按服务拆 Topic + +也可以拆成: + +```text +LMS_TASK_EVENT_TOPIC +WMS_TASK_EVENT_TOPIC +``` + +这种方式简单直接,但 Topic 会随着服务增多而增多。适合服务数量固定、消息隔离要求强的场景。 + +### 4.3 不推荐按任务类型拆 Topic + +不推荐: + +```text +STOCK_AREA_CALL_TUBE_TOPIC +WMS_INBOUND_TOPIC +... +``` + +因为任务类型会越来越多,Topic 管理会失控。任务类型应该放在消息体的 `bizType` 或 `handlerCode` 中,而不是通过 Topic 区分。 + +## 5. Task 服务发布事件的逻辑 + +当 ACS 反馈任务完成时,Task 服务处理步骤: + +```text +1. 接收 ACS 反馈 +2. 根据 taskId 查询 task_task +3. 幂等校验 external_event_id +4. 将任务状态改为 FINISHED_PENDING_CALLBACK +5. 根据 owner_service 决定消息 Tag +6. 发布 TASK_FINISHED 事件 +7. 等待 LMS/WMS 业务处理结果 +``` + +伪代码: + +```java +public void handleAcsFeedback(AcsFeedbackRequest req) { + TaskDO task = taskMapper.selectById(req.getTaskId()); + + checkFeedbackIdempotent(req); + + if (req.isFinished()) { + task.setStatus(TaskStatus.FINISHED_PENDING_CALLBACK); + taskMapper.updateById(task); + + TaskEvent event = TaskEvent.builder() + .eventType("TASK_FINISHED") + .taskId(task.getId()) + .taskNo(task.getTaskNo()) + .ownerService(task.getOwnerService()) + .bizType(task.getBizType()) + .bizId(task.getBizId()) + .handlerCode(task.getHandlerCode()) + .payload(task.getRequestPayload()) + .build(); + + mqProducer.send("TASK_EVENT_TOPIC", task.getOwnerService(), event); + } +} +``` + +注意:Task 服务只负责发出“这个任务完成了”的事实事件,不直接调用 `StockAreaCallTubeTask`。 + +## 6. LMS 如何精准定位到 `StockAreaCallTubeTask` 业务 + +LMS 消费到消息后,不是通过 Java 类名反射,而是通过 `handlerCode` 查本服务内注册的 handler。 + +### 6.1 定义统一 Handler 接口 + +```java +public interface TaskBizEventHandler { + String getHandlerCode(); + void onTaskFinished(TaskEventMessage message); + void onTaskCancelled(TaskEventMessage message); +} +``` + +### 6.2 StockAreaCallTube 对应新 Handler + +旧版类: + +```text +StockAreaCallTubeTask +``` + +新版建议拆为: + +```text +StockAreaCallTubeTaskCreator,可选,负责创建任务前业务校验和调用 Task 服务 +StockAreaCallTubeTaskHandler,负责完成/取消业务回调 +``` + +例如: + +```java +@Component +public class StockAreaCallTubeTaskHandler implements TaskBizEventHandler { + + @Override + public String getHandlerCode() { + return "LMS_STOCK_AREA_CALL_TUBE"; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void onTaskFinished(TaskEventMessage message) { + // 旧 StockAreaCallTubeTask#updateTaskStatus FINISHED 的业务逻辑迁移到这里 + // 1. 查询 LMS 业务记录或根据 taskId 查询业务关联 + // 2. 更新终点备货位 ivt_status = 有货 + // 3. 设置终点 vehicle_code + // 4. 清空起点 vehicle_code + // 5. 设置起点 ivt_status = 空 + // 6. 根据 payload.to_material 恢复分切计划 is_paper_ok + // 7. 记录业务完成日志 + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void onTaskCancelled(TaskEventMessage message) { + // 旧 StockAreaCallTubeTask#cancel 或 updateTaskStatus 取消逻辑迁移到这里 + // 注意:旧版取消只是把任务状态置完成,新版应明确业务取消语义 + } +} +``` + +### 6.3 LMS 内部 Handler 注册表 + +LMS 启动时把所有 handler 收集成 Map: + +```java +@Component +public class TaskBizEventHandlerRegistry { + + private final Map handlerMap; + + public TaskBizEventHandlerRegistry(List handlers) { + this.handlerMap = handlers.stream() + .collect(Collectors.toMap(TaskBizEventHandler::getHandlerCode, h -> h)); + } + + public TaskBizEventHandler getRequiredHandler(String handlerCode) { + TaskBizEventHandler handler = handlerMap.get(handlerCode); + if (handler == null) { + throw new IllegalArgumentException("未找到任务业务处理器:" + handlerCode); + } + return handler; + } +} +``` + +### 6.4 LMS 消费者分发 + +```java +@Component +public class LmsTaskEventConsumer { + + @Autowired + private TaskBizEventHandlerRegistry registry; + + public void onMessage(TaskEventMessage message) { + if (!"LMS".equals(message.getOwnerService())) { + return; + } + + TaskBizEventHandler handler = registry.getRequiredHandler(message.getHandlerCode()); + + if ("TASK_FINISHED".equals(message.getEventType())) { + handler.onTaskFinished(message); + } else if ("TASK_CANCELLED".equals(message.getEventType())) { + handler.onTaskCancelled(message); + } + } +} +``` + +这样即使 LMS 里有很多任务处理器,例如: + +```text +LMS_STOCK_AREA_CALL_TUBE +LMS_SEND_AIR_SHAFT_AGV +LMS_SLITTER_DOWN_AGV +LMS_TRUSS_CALL_SHAFT_CACHE +LMS_STOCK_AREA_SEND_VEHICLE +``` + +也能通过 `handlerCode` 精准定位到对应 handler。 + +## 7. 新旧机制对照 + +以 `StockAreaCallTubeTask` 为例: + +| 步骤 | 旧版 | 新版 | +| --- | --- | --- | +| 创建任务 | LMS 内 `StockAreaCallTubeTask#createTask` | LMS 创建业务记录后调用 Task.createTask | +| 路由字段 | `handle_class = Java类名` | `owner_service = LMS`, `handler_code = LMS_STOCK_AREA_CALL_TUBE` | +| 下发 ACS | `AbstractAcsTask#immediateTaskNotifyAcs` | Task 服务 `AcsTaskDispatcher` | +| ACS 完成反馈 | LMS 接收 | Task 服务接收 | +| 找处理类 | `Class.forName(handle_class)` | LMS 消费 MQ 后用 `handlerCode` 查 Map | +| 完成逻辑 | `StockAreaCallTubeTask#updateTaskStatus` | `StockAreaCallTubeTaskHandler#onTaskFinished` | +| 取消逻辑 | `StockAreaCallTubeTask#cancel/updateTaskStatus` | `StockAreaCallTubeTaskHandler#onTaskCancelled` | + +## 8. 消息中必须携带哪些字段 + +为了精准路由,消息至少包含: + +```json +{ + "eventId": "事件ID", + "eventType": "TASK_FINISHED", + "taskId": 10001, + "ownerService": "LMS", + "handlerCode": "LMS_STOCK_AREA_CALL_TUBE", + "bizType": "STOCK_AREA_CALL_TUBE", + "bizId": "业务ID", + "payload": {} +} +``` + +字段解释: + +| 字段 | 是否必需 | 作用 | +| --- | --- | --- | +| `eventId` | 是 | 幂等消费 | +| `eventType` | 是 | 区分完成、取消、执行中 | +| `taskId` | 是 | Task 服务任务 ID | +| `ownerService` | 是 | 路由到 LMS/WMS | +| `handlerCode` | 是 | LMS/WMS 内部精准定位 handler | +| `bizType` | 建议 | 查询、统计、辅助判断 | +| `bizId` | 建议 | 业务 handler 回查业务数据 | +| `payload` | 建议 | 传递旧 request_param/result_param | + +## 9. 业务处理成功后如何通知 Task + +MQ 模式下,LMS/WMS 处理完成后建议再通知 Task。 + +有两种方式。 + +### 9.1 方式一:业务服务 RPC 回调 Task + +LMS 处理成功后调用: + +```text +POST /rpc-api/task/callback-result +``` + +```json +{ + "taskId": 10001, + "eventId": "EVT-10001", + "result": "SUCCESS", + "message": "StockAreaCallTube 完成业务处理成功" +} +``` + +Task 收到后: + +```text +FINISHED_PENDING_CALLBACK -> FINISHED +``` + +### 9.2 方式二:业务服务发布处理结果 MQ + +LMS 发布: + +```text +TASK_CALLBACK_RESULT_TOPIC +``` + +Task 消费后更新最终状态。 + +第一阶段建议使用 RPC 回调 Task,链路更直观。 + +## 10. 取消逻辑如何路由 + +取消和完成是同一套路由机制,只是事件类型不同。 + +Task 取消流程: + +```text +用户取消任务 + -> Task 判断状态 + -> 如已下发则调用 ACS 取消 + -> 外部取消成功 + -> Task 状态改为 CANCEL_PENDING_CALLBACK + -> 发布 TASK_CANCELLED 事件,Tag = LMS + -> LMS 消费消息 + -> 根据 handlerCode = LMS_STOCK_AREA_CALL_TUBE 找 handler + -> 执行 onTaskCancelled + -> LMS 通知 Task 取消业务处理成功 + -> Task 状态改为 CANCELLED +``` + +消息示例: + +```json +{ + "eventType": "TASK_CANCELLED", + "taskId": 10001, + "ownerService": "LMS", + "handlerCode": "LMS_STOCK_AREA_CALL_TUBE", + "bizType": "STOCK_AREA_CALL_TUBE", + "bizId": "LMS-BIZ-10001", + "payload": { + "cancelReason": "人工取消" + } +} +``` + +## 11. 为什么不是只靠 Topic 区分任务类 + +Topic 或 Tag 只能解决第一层路由:发给 LMS 还是 WMS。 + +它不能解决 LMS 内部多个任务类的问题。 + +例如 LMS 可能同时有: + +```text +StockAreaCallTubeTask +StockAreaSendVehicleTask +SendAirShaftAgvTask +SlitterDownAgvTask +MoveVehicleAgvTask +``` + +如果只用 Topic/Tag,只能知道这是一条 LMS 消息,不能知道该执行哪一个业务逻辑。 + +所以必须在消息体中携带: + +```text +handlerCode +``` + +真正精准定位依赖的是: + +```text +handlerCode -> LMS 内部 handlerMap -> 具体 Handler Bean +``` + +## 12. StockAreaCallTubeTask 迁移建议 + +旧版 `StockAreaCallTubeTask` 可以拆成两个类。 + +### 12.1 创建侧 + +```text +StockAreaCallTubeTaskCreator +``` + +职责: + +- 校验备货区纸管搬运条件; +- 计算起点、终点、载具、product_area; +- 生成 LMS 业务记录; +- 调用 Task.createTask; +- 传入 `handlerCode = LMS_STOCK_AREA_CALL_TUBE`。 + +### 12.2 回调侧 + +```text +StockAreaCallTubeTaskHandler +``` + +职责: + +- 处理 `TASK_FINISHED`; +- 处理 `TASK_CANCELLED`; +- 迁移旧 `updateTaskStatus` 中的业务逻辑。 + +旧版完成逻辑迁移点: + +```text +1. 更新终点备货位状态为有货 +2. 将 vehicle_code 写入终点 +3. 清空起点 vehicle_code +4. 起点状态改为空 +5. 读取 to_material +6. 恢复对应分切计划 is_paper_ok +7. 更新业务日志 +``` + +## 13. 幂等设计 + +### 13.1 Task 服务幂等 + +Task 接收 ACS 反馈时按以下维度幂等: + +```text +taskId + externalEventId + status +``` + +同一个 ACS 完成反馈不能重复发布 `TASK_FINISHED`。 + +### 13.2 LMS/WMS 消费幂等 + +业务服务按以下维度幂等: + +```text +eventId +``` + +或: + +```text +taskId + eventType +``` + +建议增加业务消费日志表: + +```text +biz_task_event_consume_log +``` + +字段: + +```text +event_id +task_id +handler_code +event_type +status +error_msg +``` + +如果已成功消费,再次收到直接返回成功。 + +## 14. 最终建议 + +新版本从 ACS 完成反馈到具体任务业务逻辑,不再靠跨服务反射 Java 类,而是两级路由: + +```text +第一级:owner_service / MQ Tag + 决定消息给 LMS 还是 WMS + +第二级:handler_code + 决定 LMS/WMS 内部哪个业务 handler 执行 +``` + +以 `StockAreaCallTubeTask` 为例: + +```text +ACS 完成反馈 + -> Task 服务查询任务 + -> owner_service = LMS + -> handler_code = LMS_STOCK_AREA_CALL_TUBE + -> 发布 TASK_FINISHED,Tag = LMS + -> LMS 消费消息 + -> handlerMap.get("LMS_STOCK_AREA_CALL_TUBE") + -> StockAreaCallTubeTaskHandler#onTaskFinished +``` + +这套设计既解决了 Task 服务不能依赖 LMS/WMS 的问题,又保留了旧版每种任务有自己业务处理类的扩展能力。 diff --git a/doc/task-service-business-callback-design.md b/doc/task-service-business-callback-design.md new file mode 100644 index 00000000..2bc07678 --- /dev/null +++ b/doc/task-service-business-callback-design.md @@ -0,0 +1,366 @@ +# Task 服务独立后的任务业务回调设计方案 + +## 1. 背景 + +旧版 LMS 中,任务、业务逻辑和 `xxxTask` 都在同一个服务内,`SCH_BASE_Task.handle_class` 保存 Java 类名,`TaskService` 根据该类名获取 Spring Bean 后调用 `cancel`、`forceFinish`、`updateTaskStatus`。 + +新版本中,任务模块独立为 `nl-module-task`,LMS、WMS 会调用 Task 服务创建任务;Task 服务负责下发外部系统,后续完成、取消也先进入 Task 服务。此时如果继续让 Task 服务通过 `handle_class` 调用 LMS/WMS 的业务类,会破坏微服务边界。 + +## 2. 核心结论 + +建议改为: + +```text +Task 服务负责通用任务生命周期 +LMS/WMS 负责各自业务完成和取消 +Task 不引用 lms-server / wms-server +旧 handle_class 改为 handler_code +Task 通过 RPC 或 MQ 通知业务服务回调 +``` + +也就是说: + +- Task 服务:任务创建、状态机、调度、下发、外部反馈、日志、重试、幂等; +- LMS 服务:LMS 入库、出库、分配、点位、库存等业务完成/取消; +- WMS 服务:WMS 库存、库位、出入库单据等业务完成/取消; +- 跨服务不要存 Java 类名,要存稳定业务编码。 + +推荐任务记录保存: + +```text +owner_service = LMS / WMS +biz_type = TWO_IN / OUTBOUND / MOVE_STORE +biz_id = 业务侧 ID +handler_code = LMS_TWO_IN_TASK / WMS_OUTBOUND_TASK +external_system = ACS / WCS / AGV +``` + +## 3. 不推荐的设计 + +### 3.1 不推荐把 LMS/WMS 的 `xxxTask` 放到 Task 服务 + +如果把 `TwoInTask`、`InTask`、`OutTask` 等业务类放到 Task 服务,Task 服务就会依赖 LMS/WMS 的业务表、Service、枚举和业务规则,最终变成新的业务单体。 + +### 3.2 不推荐 Task 服务引用 LMS/WMS server + +不要设计成: + +```text +task-server -> lms-server -> 注入 LmsTwoInTask +``` + +Task 服务最多依赖 `lms-api`、`wms-api` 中的 Feign/RPC 接口和 DTO,不应依赖 server 实现。 + +### 3.3 不推荐继续使用 Java 类名作为 `handle_class` + +旧值: + +```text +org.nl.b_lms.sch.tasks.TwoInTask +``` + +新架构下 Task 服务本地没有这个类,即使通过依赖引入也会破坏边界。建议改为: + +```text +handler_code = LMS_TWO_IN_TASK +owner_service = LMS +``` + +## 4. 推荐架构 + +```text +LMS/WMS 创建任务 + -> Task 服务保存任务 +Task 服务下发 ACS/WCS/AGV +外部系统反馈完成/取消 + -> Task 服务维护任务状态 + -> Task 服务通知 LMS/WMS +LMS/WMS 根据 handler_code 找本地 handler + -> 执行业务完成/取消 + -> Task 服务更新最终状态 +``` + +服务职责: + +| 服务 | 职责 | +| --- | --- | +| Task | 任务主数据、状态机、外部系统下发、反馈接收、重试、幂等 | +| LMS | LMS 业务完成/取消,例如入库确认、分配回滚、点位处理 | +| WMS | WMS 业务完成/取消,例如库存、库位、单据处理 | + +## 5. 任务表字段建议 + +| 字段 | 示例 | 说明 | +| --- | --- | --- | +| `id` | `10001` | Task 任务 ID | +| `task_no` | `T202607130001` | 任务编号 | +| `owner_service` | `LMS` | 业务归属服务 | +| `biz_type` | `TWO_IN` | 业务任务类型 | +| `biz_id` | `1000001` | 业务侧 ID | +| `handler_code` | `LMS_TWO_IN_TASK` | 业务处理器编码 | +| `external_system` | `ACS` | 外部系统 | +| `external_task_type` | `7` | 外部任务类型 | +| `status` | `ISSUED` | Task 状态 | +| `callback_status` | `PENDING/SUCCESS/FAILED` | 业务回调状态 | +| `request_payload` | JSON | 创建扩展参数 | +| `dispatch_payload` | JSON | 下发参数 | +| `result_payload` | JSON | 外部反馈参数 | + +## 6. 推荐流程 + +### 6.1 创建任务 + +```text +LMS/WMS -> Task.createTask -> Task 保存任务 -> 返回 task_id/task_no +``` + +创建请求示例: + +```json +{ + "ownerService": "LMS", + "bizType": "TWO_IN", + "bizId": "1000001", + "handlerCode": "LMS_TWO_IN_TASK", + "externalSystem": "ACS", + "externalTaskType": "7", + "startPointCode": "IN_001", + "endPointCode": "A01_01_01", + "vehicleCode": "P001", + "priority": 1, + "requestPayload": {} +} +``` + +### 6.2 下发任务 + +```text +Task 定时任务/手动触发 + -> 查询可下发任务 + -> 组装外部系统 DTO + -> 调用 ACS/WCS/AGV + -> 成功后 status = ISSUED +``` + +如果下发参数依赖业务数据,优先由 LMS/WMS 创建任务时传给 Task,减少 Task 反查业务服务。 + +### 6.3 完成任务 + +```text +外部系统反馈完成 + -> Task 幂等校验 + -> status = FINISHED_PENDING_CALLBACK + -> 通知 LMS/WMS 执行业务完成 + -> 业务成功后 status = FINISHED +``` + +不要外部一反馈完成就直接设为最终 `FINISHED`,因为业务确认可能失败。 + +### 6.4 取消任务 + +```text +用户取消 + -> Task 判断是否允许取消 + -> 如已下发,先取消外部任务 + -> status = CANCEL_PENDING_CALLBACK + -> 通知 LMS/WMS 执行业务取消回滚 + -> 业务成功后 status = CANCELLED +``` + +## 7. 业务回调方式 + +### 7.1 第一阶段推荐:RPC 同步回调 + +Task 根据 `owner_service` 调用业务服务接口: + +```text +POST /rpc-api/lms/task-callback/handle +POST /rpc-api/wms/task-callback/handle +``` + +请求示例: + +```json +{ + "taskId": 10001, + "taskNo": "T202607130001", + "eventType": "FINISH", + "bizType": "TWO_IN", + "bizId": "1000001", + "handlerCode": "LMS_TWO_IN_TASK", + "payload": {} +} +``` + +优点:实现简单、链路清晰、Task 能立即知道业务成功或失败。缺点是业务服务不可用时需要重试。 + +### 7.2 第二阶段演进:MQ 事件回调 + +```text +Task 发布 TaskFinishedEvent / TaskCancelledEvent + -> LMS/WMS 消费 + -> 执行业务逻辑 + -> 回调 Task 确认处理结果 +``` + +优点是解耦更彻底,缺点是最终一致性和排查复杂度更高。 + +## 8. LMS/WMS 内部 handler 设计 + +业务服务内部定义统一接口: + +```java +public interface TaskBizCallbackHandler { + String getHandlerCode(); + void onTaskFinished(TaskCallbackRequest request); + void onTaskCancelled(TaskCallbackRequest request); +} +``` + +LMS 示例: + +```java +@Component +public class LmsTwoInTaskCallbackHandler implements TaskBizCallbackHandler { + @Override + public String getHandlerCode() { + return "LMS_TWO_IN_TASK"; + } + + @Override + public void onTaskFinished(TaskCallbackRequest request) { + // 入库确认、分配明细完成、库存/点位处理 + } + + @Override + public void onTaskCancelled(TaskCallbackRequest request) { + // 入库取消、分配回滚、点位释放 + } +} +``` + +业务服务内用 `handler_code` 分发到对应 handler。核心原则是:**谁拥有业务数据,谁实现业务 handler**。 + +## 9. Task 服务内部抽象 + +旧版 `AbstractAcsTask` 职责太多,新架构建议拆分: + +### Task 服务内:外部系统下发策略 + +```java +public interface ExternalTaskDispatcher { + String getExternalSystem(); + DispatchResult dispatch(TaskDO task); + CancelExternalResult cancelExternal(TaskDO task); +} +``` + +实现类例如:`AcsTaskDispatcher`、`WcsTaskDispatcher`、`AgvTaskDispatcher`。 + +### LMS/WMS 内:业务回调策略 + +```java +public interface TaskBizCallbackHandler { + String getHandlerCode(); + void onTaskFinished(TaskCallbackRequest request); + void onTaskCancelled(TaskCallbackRequest request); +} +``` + +这样旧版一个大 `AbstractAcsTask` 被拆成两部分: + +```text +Task 服务:负责外部任务下发 +业务服务:负责完成/取消后的业务处理 +``` + +## 10. 状态机建议 + +| 状态 | 含义 | +| --- | --- | +| `CREATED` | 已创建 | +| `READY` | 可下发 | +| `ISSUING` | 下发中 | +| `ISSUED` | 已下发 | +| `EXECUTING` | 执行中 | +| `FINISHED_PENDING_CALLBACK` | 外部已完成,等待业务完成 | +| `FINISHED` | 最终完成 | +| `CANCEL_PENDING_EXTERNAL` | 等待外部系统取消 | +| `CANCEL_PENDING_CALLBACK` | 等待业务取消回滚 | +| `CANCELLED` | 最终取消 | +| `FAILED` | 失败 | + +中间状态用于区分:外部完成不等于业务完成,外部取消不等于业务回滚完成。 + +## 11. 幂等和补偿 + +- Task 接收外部反馈时,按 `task_id + external_event_id + event_type` 幂等; +- LMS/WMS 业务回调按 `task_id + event_type` 幂等; +- 业务服务建议记录回调日志; +- Task 服务增加定时补偿,扫描 `FINISHED_PENDING_CALLBACK`、`CANCEL_PENDING_CALLBACK`、`ISSUING` 超时等任务。 + +## 12. 代码结构建议 + +Task API: + +```text +nl-module-task-api + api/TaskApi.java + dto/TaskCreateReqDTO.java + dto/TaskCallbackReqDTO.java + dto/TaskCancelReqDTO.java + enums/TaskStatusEnum.java +``` + +Task Server: + +```text +nl-module-task-server + service/TaskService.java + service/TaskDispatchService.java + service/TaskBizCallbackNotifyService.java + dispatcher/ExternalTaskDispatcher.java + dispatcher/AcsTaskDispatcher.java + job/TaskScheduleJob.java + job/TaskCallbackRetryJob.java +``` + +LMS Server: + +```text +nl-module-lms-server + api/TaskCallbackApiImpl.java + taskcallback/TaskBizCallbackHandler.java + taskcallback/TaskBizCallbackDispatcher.java + taskcallback/handler/LmsTwoInTaskCallbackHandler.java +``` + +## 13. 对问题的直接回答 + +### 抽象类子类放 Task 还是 LMS? + +如果子类包含 LMS 入库、出库、分配、库存、点位等业务逻辑,就放 LMS。如果只负责外部系统下发,不访问业务数据,可以放 Task。 + +更推荐拆成: + +```text +Task 服务:ExternalTaskDispatcher +LMS/WMS:TaskBizCallbackHandler +``` + +### handler 放 LMS,Task 是否要引用 LMS? + +Task 不引用 `lms-server`。可以依赖 `lms-api` 做 RPC,或发布 MQ 事件让 LMS 消费。 + +### 旧 handle_class 怎么办? + +不再存 Java 类名,改为: + +```text +owner_service = LMS +handler_code = LMS_TWO_IN_TASK +``` + +## 14. 总结 + +Task 服务独立后,`handle_class` 不应该再表示 Java 类,也不应该让 Task 服务持有 LMS/WMS 的业务类。正确拆法是:Task 服务负责通用任务生命周期和外部系统交互;LMS/WMS 负责各自业务完成和取消;Task 通过 `owner_service + handler_code` 定位业务回调目标;业务服务内部再通过 `handler_code` 分发到本地 handler。 diff --git a/doc/task-service-new-architecture-development-design.md b/doc/task-service-new-architecture-development-design.md new file mode 100644 index 00000000..877e4576 --- /dev/null +++ b/doc/task-service-new-architecture-development-design.md @@ -0,0 +1,579 @@ +# Task 服务新架构详细开发设计方案 + +## 1. 业务背景与定位 + +新架构中,系统拆分为通用 `Task` 服务、`LMS` 服务、`WMS` 服务。 + +其中: + +- `LMS`:生产内部搬运业务,例如 AGV 搬运某个架子、托盘、料车到某个生产站点、缓存位、对接位; +- `WMS`:仓储出入库业务,例如入库、出库、移库、堆垛机搬运、AGV 和立库设备联动; +- `Task`:统一任务服务,负责任务创建记录、任务状态流转、调度下发外部系统、接收外部反馈、任务日志、重试和补偿。 + +旧架构中 `AbstractAcsTask` 同时承担了任务创建、点位计算、外部下发、完成业务、取消业务等职责。新架构建议将其拆分,避免 Task 服务反向依赖 LMS/WMS 的业务实现。 + +## 2. 总体设计目标 + +1. Task 服务成为通用任务中台,只处理任务生命周期和外部系统交互; +2. LMS/WMS 保留各自业务语义和业务数据处理能力; +3. 不再跨服务使用 Java 类名 `handle_class`; +4. 使用稳定的 `handler_code` 标识业务处理器; +5. 任务完成、取消时,由 Task 服务通知业务归属服务执行业务回调; +6. 支持 AGV、堆垛机、ACS、WCS 等多种外部系统; +7. 支持同步 RPC 回调,也能平滑演进到 MQ 事件回调; +8. 支持幂等、重试、补偿和人工干预。 + +## 3. 核心拆分思想 + +旧版一个 `AbstractAcsTask` 拆成三层能力: + +```text +业务任务创建层:LMS/WMS + 负责业务校验、点位计算、业务参数准备、调用 Task 创建任务 + +通用任务执行层:Task + 负责任务状态、任务下发、外部反馈、任务日志、重试补偿 + +业务结果处理层:LMS/WMS + 负责完成/取消后的业务确认、库存、单据、点位、分配明细处理 +``` + +对应代码抽象: + +```text +LMS/WMS:BusinessTaskCreator,可选 +Task:ExternalTaskDispatcher +LMS/WMS:TaskBizCallbackHandler +``` + +## 4. 服务职责边界 + +### 4.1 LMS 服务职责 + +LMS 负责生产内部搬运任务的业务语义,例如: + +- 判断某个架子是否允许搬运; +- 判断目标站点、缓存位、对接位是否可用; +- 生成生产搬运业务记录; +- 调用 Task 服务创建搬运任务; +- 任务完成后更新生产业务状态; +- 任务取消后回滚生产占用、点位锁定、业务单据状态。 + +### 4.2 WMS 服务职责 + +WMS 负责仓储出入库任务的业务语义,例如: + +- 入库、出库、移库业务校验; +- 库位分配、库存预占、出入库单据状态维护; +- 调用 Task 服务创建 AGV/堆垛机/输送线任务; +- 任务完成后确认出入库或移库; +- 任务取消后释放库位、回滚库存预占、恢复单据状态。 + +### 4.3 Task 服务职责 + +Task 服务只处理通用任务能力: + +- 保存任务主表; +- 保存任务明细或任务步骤; +- 管理任务状态机; +- 根据 `external_system` 选择外部系统下发器; +- 下发 AGV/ACS/WCS/堆垛机任务; +- 接收外部系统反馈; +- 根据 `owner_service` 通知 LMS/WMS 做业务回调; +- 记录任务日志; +- 处理重试、超时和补偿。 + +Task 服务不直接处理 LMS/WMS 的业务表,不直接注入 LMS/WMS 的业务 Service。 + +## 5. 模块依赖设计 + +推荐依赖方向: + +```text +lms-server -> task-api +wms-server -> task-api + +task-server -> task-api +task-server -> lms-api,可选,仅 RPC 回调需要 +task-server -> wms-api,可选,仅 RPC 回调需要 +``` + +禁止依赖: + +```text +task-server -> lms-server +task-server -> wms-server +``` + +如果采用 MQ 事件方式,Task 服务甚至可以不依赖 `lms-api`、`wms-api`,只发布标准任务事件。 + +## 6. 新版任务标识设计 + +旧版: + +```text +handle_class = org.nl.b_lms.sch.tasks.TwoInTask +``` + +新版: + +```text +owner_service = LMS +biz_type = PRODUCTION_TRANSFER +handler_code = LMS_RACK_TO_STATION +biz_id = LMS 业务记录 ID +``` + +或 WMS: + +```text +owner_service = WMS +biz_type = INBOUND +handler_code = WMS_INBOUND_STACKER +biz_id = WMS 入库单/分配明细 ID +``` + +字段含义: + +| 字段 | 含义 | +| --- | --- | +| `owner_service` | 任务归属业务服务,决定完成/取消回调给谁 | +| `biz_type` | 业务任务类型,用于查询、统计和权限控制 | +| `handler_code` | 业务回调处理器编码,业务服务内部使用 | +| `biz_id` | 业务侧记录 ID,用于回查业务数据 | +| `external_system` | 外部执行系统,例如 AGV、ACS、WCS、STACKER | +| `external_task_type` | 外部系统任务类型 | + +## 7. 数据模型建议 + +### 7.1 task_task 主表 + +| 字段 | 说明 | +| --- | --- | +| `id` | Task 服务任务 ID | +| `task_no` | 任务编号 | +| `owner_service` | LMS/WMS | +| `biz_type` | 业务类型 | +| `biz_id` | 业务侧 ID | +| `handler_code` | 业务回调处理器编码 | +| `external_system` | 外部系统 | +| `external_task_type` | 外部任务类型 | +| `status` | 任务状态 | +| `priority` | 优先级 | +| `start_point_code` | 起点 | +| `end_point_code` | 终点 | +| `vehicle_code` | 载具、架子、托盘、料车等 | +| `callback_status` | 回调状态 | +| `request_payload` | 创建任务扩展参数 | +| `dispatch_payload` | 下发外部系统参数 | +| `result_payload` | 外部反馈参数 | +| `error_msg` | 异常信息 | + +### 7.2 task_task_log 日志表 + +记录每次状态变化、下发、反馈、回调、取消、重试。 + +关键字段: + +| 字段 | 说明 | +| --- | --- | +| `task_id` | 任务 ID | +| `event_type` | CREATE/DISPATCH/FEEDBACK/CALLBACK/CANCEL/RETRY | +| `from_status` | 原状态 | +| `to_status` | 新状态 | +| `request_json` | 请求参数 | +| `response_json` | 响应参数 | +| `success` | 是否成功 | +| `error_msg` | 错误信息 | + +### 7.3 task_callback_log 回调日志表 + +用于保证业务回调幂等和补偿。 + +| 字段 | 说明 | +| --- | --- | +| `task_id` | 任务 ID | +| `owner_service` | LMS/WMS | +| `handler_code` | 业务处理器编码 | +| `event_type` | FINISH/CANCEL/EXECUTING | +| `status` | PROCESSING/SUCCESS/FAILED | +| `retry_count` | 重试次数 | +| `error_msg` | 失败原因 | + +## 8. 状态机设计 + +推荐 Task 服务状态: + +| 状态 | 含义 | +| --- | --- | +| `CREATED` | 已创建 | +| `READY` | 可下发 | +| `ISSUING` | 下发中 | +| `ISSUED` | 已下发 | +| `EXECUTING` | 外部执行中 | +| `FINISHED_PENDING_CALLBACK` | 外部完成,等待业务完成回调 | +| `FINISHED` | 业务完成后最终完成 | +| `CANCEL_PENDING_EXTERNAL` | 等待外部系统取消 | +| `CANCEL_PENDING_CALLBACK` | 外部取消成功,等待业务取消回调 | +| `CANCELLED` | 业务取消后最终取消 | +| `FAILED` | 失败 | + +核心原则: + +```text +外部系统完成 != 业务完成 +外部系统取消 != 业务取消完成 +``` + +因此必须有 `PENDING_CALLBACK` 中间状态。 + +## 9. 接口设计 + +### 9.1 Task 创建接口 + +```text +POST /rpc-api/task/create +``` + +请求核心字段: + +```json +{ + "ownerService": "LMS", + "bizType": "PRODUCTION_TRANSFER", + "bizId": "LMS-BIZ-10001", + "handlerCode": "LMS_RACK_TO_STATION", + "externalSystem": "AGV", + "externalTaskType": "MOVE", + "startPointCode": "CACHE_A01", + "endPointCode": "STATION_01", + "vehicleCode": "RACK_001", + "priority": 1, + "requestPayload": {} +} +``` + +WMS 入库示例: + +```json +{ + "ownerService": "WMS", + "bizType": "INBOUND", + "bizId": "IN-DIS-10001", + "handlerCode": "WMS_INBOUND_STACKER", + "externalSystem": "STACKER", + "externalTaskType": "INBOUND", + "startPointCode": "IN_PORT_01", + "endPointCode": "A010101", + "vehicleCode": "PALLET_001", + "priority": 5, + "requestPayload": {} +} +``` + +### 9.2 外部系统反馈接口 + +```text +POST /api/task/feedback +``` + +```json +{ + "taskId": 10001, + "externalTaskNo": "ACS-TASK-001", + "externalEventId": "EVT-001", + "status": "FINISHED", + "payload": {} +} +``` + +### 9.3 取消任务接口 + +```text +POST /rpc-api/task/cancel +``` + +```json +{ + "taskId": 10001, + "reason": "人工取消" +} +``` + +### 9.4 业务回调接口 + +第一阶段建议 RPC,同步调用业务服务: + +```text +POST /rpc-api/lms/task-callback/handle +POST /rpc-api/wms/task-callback/handle +``` + +```json +{ + "taskId": 10001, + "taskNo": "T202607130001", + "eventType": "FINISH", + "bizType": "PRODUCTION_TRANSFER", + "bizId": "LMS-BIZ-10001", + "handlerCode": "LMS_RACK_TO_STATION", + "payload": {} +} +``` + +## 10. Task 服务核心代码抽象 + +### 10.1 外部系统下发器 + +```java +public interface ExternalTaskDispatcher { + String getExternalSystem(); + DispatchResult dispatch(TaskDO task); + CancelExternalResult cancelExternal(TaskDO task); +} +``` + +实现示例: + +```text +AgvTaskDispatcher +AcsTaskDispatcher +WcsTaskDispatcher +StackerTaskDispatcher +``` + +Task 服务根据 `external_system` 选择对应 dispatcher。 + +### 10.2 业务回调通知器 + +```java +public interface TaskBizCallbackNotifier { + CallbackResult notifyFinish(TaskDO task); + CallbackResult notifyCancel(TaskDO task); + CallbackResult notifyExecuting(TaskDO task); +} +``` + +第一阶段可实现为 RPC: + +```text +RpcTaskBizCallbackNotifier +``` + +第二阶段可实现为 MQ: + +```text +MqTaskBizCallbackNotifier +``` + +## 11. LMS/WMS 业务回调 handler 设计 + +业务服务内部定义: + +```java +public interface TaskBizCallbackHandler { + String getHandlerCode(); + void onTaskFinished(TaskCallbackRequest request); + void onTaskCancelled(TaskCallbackRequest request); + default void onTaskExecuting(TaskCallbackRequest request) {} +} +``` + +LMS 示例: + +```java +@Component +public class LmsRackToStationCallbackHandler implements TaskBizCallbackHandler { + @Override + public String getHandlerCode() { + return "LMS_RACK_TO_STATION"; + } + + @Override + public void onTaskFinished(TaskCallbackRequest request) { + // 更新生产搬运记录完成 + // 更新架子当前位置 + // 释放起点占用 + // 占用目标站点或更新目标站点状态 + } + + @Override + public void onTaskCancelled(TaskCallbackRequest request) { + // 回滚生产搬运记录 + // 释放目标站点锁定 + // 恢复架子原状态 + } +} +``` + +WMS 示例: + +```java +@Component +public class WmsInboundStackerCallbackHandler implements TaskBizCallbackHandler { + @Override + public String getHandlerCode() { + return "WMS_INBOUND_STACKER"; + } + + @Override + public void onTaskFinished(TaskCallbackRequest request) { + // 入库确认 + // 更新库存 + // 更新库位状态 + // 更新入库单和分配明细 + } + + @Override + public void onTaskCancelled(TaskCallbackRequest request) { + // 回滚入库分配 + // 释放库位预占 + // 恢复入库单状态 + } +} +``` + +业务服务内建立分发器: + +```java +@Service +public class TaskBizCallbackDispatcher { + private final Map handlerMap; + + public void dispatch(TaskCallbackRequest request) { + TaskBizCallbackHandler handler = handlerMap.get(request.getHandlerCode()); + if (handler == null) { + throw new IllegalArgumentException("未找到处理器:" + request.getHandlerCode()); + } + if ("FINISH".equals(request.getEventType())) { + handler.onTaskFinished(request); + } else if ("CANCEL".equals(request.getEventType())) { + handler.onTaskCancelled(request); + } else if ("EXECUTING".equals(request.getEventType())) { + handler.onTaskExecuting(request); + } + } +} +``` + +## 12. 典型流程 + +### 12.1 LMS 生产搬运任务 + +```text +LMS 判断架子 RACK_001 要去 STATION_01 + -> LMS 锁定目标站点 + -> LMS 创建生产搬运业务记录 + -> LMS 调用 Task.createTask,handler_code = LMS_RACK_TO_STATION + -> Task 下发 AGV + -> AGV 反馈执行中,Task 更新 EXECUTING,可选通知 LMS + -> AGV 反馈完成,Task 更新 FINISHED_PENDING_CALLBACK + -> Task 回调 LMS + -> LMS 更新架子位置、站点状态、业务记录完成 + -> Task 更新 FINISHED +``` + +### 12.2 WMS 入库任务 + +```text +WMS 入库单分配库位 A010101 + -> WMS 锁定库位,生成入库分配 + -> WMS 调用 Task.createTask,handler_code = WMS_INBOUND_STACKER + -> Task 下发堆垛机/AGV + -> 外部系统反馈完成 + -> Task 更新 FINISHED_PENDING_CALLBACK + -> Task 回调 WMS + -> WMS 确认入库、更新库存、更新库位、更新单据 + -> Task 更新 FINISHED +``` + +### 12.3 取消任务 + +```text +用户取消任务 + -> Task 判断状态 + -> 如果已下发,调用外部系统取消 + -> 外部取消成功后 Task 更新 CANCEL_PENDING_CALLBACK + -> Task 回调 LMS/WMS + -> LMS/WMS 执行业务回滚 + -> Task 更新 CANCELLED +``` + +## 13. 幂等与事务 + +### 13.1 Task 幂等 + +外部反馈按以下维度幂等: + +```text +task_id + external_event_id + status +``` + +同一个完成反馈不能重复触发业务完成。 + +### 13.2 业务幂等 + +LMS/WMS 按以下维度幂等: + +```text +task_id + event_type +``` + +如果业务回调已经成功,再次收到直接返回成功。 + +### 13.3 跨服务一致性 + +不做分布式事务,采用最终一致性: + +1. Task 更新为 `PENDING_CALLBACK`; +2. Task 调用 LMS/WMS; +3. LMS/WMS 在本地事务内完成业务更新和回调日志; +4. Task 更新最终状态; +5. 失败则进入重试或人工补偿。 + +## 14. 开发落地步骤 + +### 第一阶段:基础 RPC 版本 + +1. 在 `task-api` 定义任务创建、取消、回调 DTO; +2. 在 `task-server` 建任务主表、任务日志表、回调日志表; +3. 实现 Task 创建接口; +4. 实现 `ExternalTaskDispatcher` 和 AGV/ACS/堆垛机下发器; +5. 实现外部反馈接口; +6. 实现 RPC 业务回调通知器; +7. 在 LMS/WMS 实现 `TaskBizCallbackHandler` 和分发器; +8. 打通 LMS 生产搬运和 WMS 入库两个典型任务。 + +### 第二阶段:增强可靠性 + +1. 增加回调失败重试定时任务; +2. 增加外部下发失败重试; +3. 增加人工补偿页面; +4. 增加状态流转校验; +5. 增加任务日志和链路追踪。 + +### 第三阶段:MQ 演进 + +1. Task 服务发布任务完成/取消事件; +2. LMS/WMS 消费事件执行业务; +3. 业务服务回调 Task 确认处理结果; +4. RPC 和 MQ 可按任务类型配置切换。 + +## 15. 最终建议 + +不要把 LMS/WMS 的业务 `xxxTask` 子类放到 Task 服务,也不要让 Task 服务依赖 LMS/WMS server。 + +推荐把旧 `AbstractAcsTask` 拆成: + +```text +Task 服务:ExternalTaskDispatcher,负责外部系统下发和取消 +LMS/WMS:TaskBizCallbackHandler,负责业务完成和业务取消 +``` + +旧 `handle_class` 改为: + +```text +owner_service + handler_code +``` + +这样既保留了旧版“不同任务走不同处理器”的扩展性,又满足新架构下服务边界清晰、依赖方向正确、任务服务通用化的目标。 diff --git a/nl-framework/nl-common/src/main/java/cn/code/nl/framework/common/enums/RpcConstants.java b/nl-framework/nl-common/src/main/java/cn/code/nl/framework/common/enums/RpcConstants.java index d8cb385f..aba002d7 100644 --- a/nl-framework/nl-common/src/main/java/cn/code/nl/framework/common/enums/RpcConstants.java +++ b/nl-framework/nl-common/src/main/java/cn/code/nl/framework/common/enums/RpcConstants.java @@ -37,4 +37,10 @@ public interface RpcConstants { */ String INFRA_PREFIX = RPC_API_PREFIX + "/infra"; + + /** + * task 服务的前缀 + */ + String TASK_PREFIX = RPC_API_PREFIX + "/task"; + } diff --git a/nl-gateway/src/main/resources/application-dev.yaml b/nl-gateway/src/main/resources/application-dev.yaml index 2e8adfa8..eebce4c2 100644 --- a/nl-gateway/src/main/resources/application-dev.yaml +++ b/nl-gateway/src/main/resources/application-dev.yaml @@ -3,7 +3,7 @@ spring: cloud: nacos: - server-addr: 192.168.10.29:8848 # Nacos 服务器地址 + server-addr: 192.168.10.2:8848 # Nacos 服务器地址 username: password: discovery: # 【配置中心】配置项 diff --git a/nl-module-infra/nl-module-infra-server/src/main/resources/application-dev.yaml b/nl-module-infra/nl-module-infra-server/src/main/resources/application-dev.yaml index 5434bed6..771254be 100644 --- a/nl-module-infra/nl-module-infra-server/src/main/resources/application-dev.yaml +++ b/nl-module-infra/nl-module-infra-server/src/main/resources/application-dev.yaml @@ -3,7 +3,7 @@ spring: cloud: nacos: - server-addr: 192.168.10.29:8848 # Nacos 服务器地址 + server-addr: 192.168.10.2:8848 # Nacos 服务器地址 username: # Nacos 账号 password: # Nacos 密码 discovery: # 【配置中心】配置项 @@ -57,14 +57,14 @@ spring: primary: master datasource: master: - url: jdbc:mysql://127.0.0.1:3306/huachuang_lms?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 12356 + password: root123 slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:mysql://127.0.0.1:3306/huachuang_lms?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 12356 + password: root123 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: @@ -95,9 +95,9 @@ spring: xxl: job: admin: - addresses: http://192.168.10.29:8080/xxl-job-admin # 调度中心部署跟地址 + addresses: http://192.168.10.2:8080/xxl-job-admin # 调度中心部署跟地址 executor: - ip: 192.168.10.29 + ip: 192.168.10.2 port: 9991 --- #################### 服务保障相关配置 #################### @@ -134,4 +134,4 @@ spring: # 芋道配置项,设置当前项目所有自定义的配置 nl: - demo: true # 开启演示模式 + demo: false # 开启演示模式 diff --git a/nl-module-lms/nl-module-lms-server/src/main/resources/application-dev.yaml b/nl-module-lms/nl-module-lms-server/src/main/resources/application-dev.yaml index 10810337..f3c6b3c9 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/resources/application-dev.yaml +++ b/nl-module-lms/nl-module-lms-server/src/main/resources/application-dev.yaml @@ -3,7 +3,7 @@ spring: cloud: nacos: - server-addr: http://192.168.10.29:8848 # Nacos 服务器地址 + server-addr: 192.168.81.193:8848 # Nacos 服务器地址 username: # Nacos 账号 password: # Nacos 密码 discovery: # 【配置中心】配置项 @@ -57,14 +57,14 @@ spring: primary: master datasource: master: - url: jdbc:mysql://127.0.0.1:3306/huachuang_lms?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 12356 + password: root123 slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:mysql://127.0.0.1:3306/huachuang_lms?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 12356 + password: root123 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: @@ -95,9 +95,9 @@ spring: xxl: job: admin: - addresses: http://192.168.10.29:8080/xxl-job-admin # 调度中心部署跟地址 + addresses: http://192.168.10.2:8080/xxl-job-admin # 调度中心部署跟地址 executor: - ip: 192.168.10.29 + ip: 192.168.10.2 port: 9992 --- #################### 服务保障相关配置 #################### diff --git a/nl-module-lms/nl-module-lms-server/src/main/resources/application-local.yaml b/nl-module-lms/nl-module-lms-server/src/main/resources/application-local.yaml index 10810337..f3c6b3c9 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/resources/application-local.yaml +++ b/nl-module-lms/nl-module-lms-server/src/main/resources/application-local.yaml @@ -3,7 +3,7 @@ spring: cloud: nacos: - server-addr: http://192.168.10.29:8848 # Nacos 服务器地址 + server-addr: 192.168.81.193:8848 # Nacos 服务器地址 username: # Nacos 账号 password: # Nacos 密码 discovery: # 【配置中心】配置项 @@ -57,14 +57,14 @@ spring: primary: master datasource: master: - url: jdbc:mysql://127.0.0.1:3306/huachuang_lms?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 12356 + password: root123 slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:mysql://127.0.0.1:3306/huachuang_lms?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 12356 + password: root123 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: @@ -95,9 +95,9 @@ spring: xxl: job: admin: - addresses: http://192.168.10.29:8080/xxl-job-admin # 调度中心部署跟地址 + addresses: http://192.168.10.2:8080/xxl-job-admin # 调度中心部署跟地址 executor: - ip: 192.168.10.29 + ip: 192.168.10.2 port: 9992 --- #################### 服务保障相关配置 #################### diff --git a/nl-module-system/nl-module-system-server/src/main/resources/application-dev.yaml b/nl-module-system/nl-module-system-server/src/main/resources/application-dev.yaml index 8282b941..49b30f08 100644 --- a/nl-module-system/nl-module-system-server/src/main/resources/application-dev.yaml +++ b/nl-module-system/nl-module-system-server/src/main/resources/application-dev.yaml @@ -3,7 +3,7 @@ spring: cloud: nacos: - server-addr: http://192.168.10.29:8848 # Nacos 服务器地址 + server-addr: 192.168.81.193:8848 # Nacos 服务器地址 username: # Nacos 账号 password: # Nacos 密码 discovery: # 【配置中心】配置项 @@ -57,14 +57,14 @@ spring: primary: master datasource: master: - url: jdbc:mysql://127.0.0.1:3306/huachuang_lms?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 12356 + password: root123 slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:mysql://127.0.0.1:3306/huachuang_lms?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 12356 + password: root123 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: @@ -95,9 +95,9 @@ spring: xxl: job: admin: - addresses: http://192.168.10.29:8080/xxl-job-admin # 调度中心部署跟地址 + addresses: http://192.168.10.2:8080/xxl-job-admin # 调度中心部署跟地址 executor: - ip: 192.168.10.29 + ip: 192.168.10.2 port: 9992 --- #################### 服务保障相关配置 #################### diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/ApiConstants.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/ApiConstants.java new file mode 100644 index 00000000..87391972 --- /dev/null +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/ApiConstants.java @@ -0,0 +1,23 @@ +package cn.code.nl.module.task.enums; + +import cn.code.nl.framework.common.enums.RpcConstants; + +/** + * API 相关的枚举 + * + * @author 芋道源码 + */ +public class ApiConstants { + + /** + * 服务名 + * + * 注意,需要保证和 spring.application.name 保持一致 + */ + public static final String NAME = "task-server"; + + public static final String PREFIX = RpcConstants.RPC_API_PREFIX + "/task"; + + public static final String VERSION = "1.0.0"; + +} diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/ErrorCodeConstants.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/ErrorCodeConstants.java new file mode 100644 index 00000000..b153a00f --- /dev/null +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/ErrorCodeConstants.java @@ -0,0 +1,12 @@ +package cn.code.nl.module.task.enums; + +import cn.code.nl.framework.common.exception.ErrorCode; + +/** + * + * @Author: liyongde + * @Date: 2026/7/14 13:03 + */ +public interface ErrorCodeConstants { + ErrorCode TRANSPORT_TASK_NOT_EXISTS = new ErrorCode(1, "搬运任务不存在"); +} diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/TaskServerApplication.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/TaskServerApplication.java index a4ce3b93..9ebc95f3 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/TaskServerApplication.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/TaskServerApplication.java @@ -25,15 +25,16 @@ public class TaskServerApplication { String port = env.getProperty("server.port"); String hostAddress = InetAddress.getLocalHost().getHostAddress(); - log.info("\n-------------------------华创Task服务启动成功------------------------------\n\t" + - "服务名称: '{}' is running! 访问网址:\n\t" + - "本地 localhost: \thttp://localhost:{}\n\t" + - "本机服务 IP: \thttp://{}:{}\n\t" + + String startupMsg = String.format("\n-------------------------华创Task服务启动成功------------------------------\n\t" + + "服务名称: '%s' is running! 访问网址:\n\t" + + "本地 localhost: \thttp://localhost:%s\n\t" + + "本机服务 IP: \thttp://%s:%s\n\t" + "----------------------------------------------------------", applicationName, port, hostAddress, port ); + System.out.println(startupMsg); } } diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/api/package-info.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/api/package-info.java new file mode 100644 index 00000000..e139e570 --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/api/package-info.java @@ -0,0 +1,6 @@ +/** + * api实现类 + * @Author: liyongde + * @Date: 2026/7/13 13:55 + */ +package cn.code.nl.module.task.api; \ No newline at end of file diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/TransportTaskController.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/TransportTaskController.java new file mode 100644 index 00000000..7db3cd0f --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/TransportTaskController.java @@ -0,0 +1,104 @@ +package cn.code.nl.module.task.controller.admin.transporttask; + +import org.springframework.web.bind.annotation.*; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.security.access.prepost.PreAuthorize; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; + +import jakarta.validation.constraints.*; +import jakarta.validation.*; +import jakarta.servlet.http.*; +import java.util.*; +import java.io.IOException; + +import cn.code.nl.framework.common.pojo.PageParam; +import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.framework.common.pojo.CommonResult; +import cn.code.nl.framework.common.util.object.BeanUtils; +import static cn.code.nl.framework.common.pojo.CommonResult.success; + +import cn.code.nl.framework.excel.core.util.ExcelUtils; + +import cn.code.nl.framework.apilog.core.annotation.ApiAccessLog; +import static cn.code.nl.framework.apilog.core.enums.OperateTypeEnum.*; + +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.service.transporttask.TransportTaskService; + +@Tag(name = "管理后台 - 搬运任务") +@RestController +@RequestMapping("/task/transport-task") +@Validated +public class TransportTaskController { + + @Resource + private TransportTaskService transportTaskService; + + @PostMapping("/create") + @Operation(summary = "创建搬运任务") + @PreAuthorize("@ss.hasPermission('task:transport-task:create')") + public CommonResult createTransportTask(@Valid @RequestBody TransportTaskSaveReqVO createReqVO) { + return success(transportTaskService.createTransportTask(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新搬运任务") + @PreAuthorize("@ss.hasPermission('task:transport-task:update')") + public CommonResult updateTransportTask(@Valid @RequestBody TransportTaskSaveReqVO updateReqVO) { + transportTaskService.updateTransportTask(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除搬运任务") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('task:transport-task:delete')") + public CommonResult deleteTransportTask(@RequestParam("id") Long id) { + transportTaskService.deleteTransportTask(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除搬运任务") + @PreAuthorize("@ss.hasPermission('task:transport-task:delete')") + public CommonResult deleteTransportTaskList(@RequestParam("ids") List ids) { + transportTaskService.deleteTransportTaskListByIds(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得搬运任务") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('task:transport-task:query')") + public CommonResult getTransportTask(@RequestParam("id") Long id) { + TransportTaskDO transportTask = transportTaskService.getTransportTask(id); + return success(BeanUtils.toBean(transportTask, TransportTaskRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得搬运任务分页") + @PreAuthorize("@ss.hasPermission('task:transport-task:query')") + public CommonResult> getTransportTaskPage(@Valid TransportTaskPageReqVO pageReqVO) { + PageResult pageResult = transportTaskService.getTransportTaskPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, TransportTaskRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出搬运任务 Excel") + @PreAuthorize("@ss.hasPermission('task:transport-task:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportTransportTaskExcel(@Valid TransportTaskPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = transportTaskService.getTransportTaskPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "搬运任务.xls", "数据", TransportTaskRespVO.class, + BeanUtils.toBean(list, TransportTaskRespVO.class)); + } + +} \ No newline at end of file diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskPageReqVO.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskPageReqVO.java new file mode 100644 index 00000000..fb85db3f --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskPageReqVO.java @@ -0,0 +1,122 @@ +package cn.code.nl.module.task.controller.admin.transporttask.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.code.nl.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static cn.code.nl.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 搬运任务分页 Request VO") +@Data +public class TransportTaskPageReqVO extends PageParam { + + @Schema(description = "任务编码") + private String taskCode; + + @Schema(description = "任务名称", example = "张三") + private String taskName; + + @Schema(description = "业务归属服务:LMS/WMS,用于完成取消事件一级路由") + private String ownerService; + + @Schema(description = "业务类型", example = "2") + private String bizType; + + @Schema(description = "业务侧标识", example = "31008") + private String bizId; + + @Schema(description = "业务回调处理器编码") + private String handleCode; + + @Schema(description = "任务类型", example = "2") + private String taskType; + + @Schema(description = "任务状态", example = "2") + private String taskStatus; + + @Schema(description = "ACS任务类型", example = "1") + private String acsTaskType; + + @Schema(description = "AGV系统类型", example = "1") + private String agvSystemType; + + @Schema(description = "ACS外部任务号") + private String externalTaskNo; + + @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 = "载具类型", example = "2") + private String vehicleType; + + @Schema(description = "载具数量") + private Long vehicleQty; + + @Schema(description = "载具编码") + private String vehicleCode; + + @Schema(description = "载具编码2") + private String vehicleCode2; + + @Schema(description = "车号") + private String carNo; + + @Schema(description = "优先级") + private String priority; + + @Schema(description = "生产区域") + private String productArea; + + @Schema(description = "是否自动下发") + private String isAutoIssue; + + @Schema(description = "任务组标识", example = "21169") + private Long taskGroupId; + + @Schema(description = "任务组顺序号") + private Long sortSeq; + + @Schema(description = "任务完成类型", example = "1") + private String finishedType; + + @Schema(description = "业务回调状态:PENDING/SUCCESS/FAILED", example = "2") + private String callbackStatus; + + @Schema(description = "业务回调重试次数", example = "2146") + private Integer callbackRetryCount; + + @Schema(description = "业务回调失败原因") + private String callbackErrorMsg; + + @Schema(description = "生成方式") + private String createMode; + + @Schema(description = "创建任务请求参数") + private String requestParam; + + @Schema(description = "下发ACS的AcsTaskDto扩展报文") + private String dispatchParam; + + @Schema(description = "ACS反馈参数") + private String resultParam; + + @Schema(description = "备注", example = "你说的对") + private String remark; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskRespVO.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskRespVO.java new file mode 100644 index 00000000..387e8aa4 --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskRespVO.java @@ -0,0 +1,162 @@ +package cn.code.nl.module.task.controller.admin.transporttask.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import cn.idev.excel.annotation.*; +import cn.code.nl.framework.excel.core.annotations.DictFormat; +import cn.code.nl.framework.excel.core.convert.DictConvert; + +@Schema(description = "管理后台 - 搬运任务 Response VO") +@Data +@ExcelIgnoreUnannotated +public class TransportTaskRespVO { + + @Schema(description = "任务标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "11528") + @ExcelProperty("任务标识") + private Long taskId; + + @Schema(description = "任务编码", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("任务编码") + private String taskCode; + + @Schema(description = "任务名称", example = "张三") + @ExcelProperty("任务名称") + private String taskName; + + @Schema(description = "业务归属服务:LMS/WMS,用于完成取消事件一级路由", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("业务归属服务:LMS/WMS,用于完成取消事件一级路由") + private String ownerService; + + @Schema(description = "业务类型", example = "2") + @ExcelProperty("业务类型") + private String bizType; + + @Schema(description = "业务侧标识", example = "31008") + @ExcelProperty("业务侧标识") + private String bizId; + + @Schema(description = "业务回调处理器编码") + @ExcelProperty("业务回调处理器编码") + private String handleCode; + + @Schema(description = "任务类型", example = "2") + @ExcelProperty("任务类型") + private String taskType; + + @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @ExcelProperty("任务状态") + private String taskStatus; + + @Schema(description = "ACS任务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty("ACS任务类型") + private String acsTaskType; + + @Schema(description = "AGV系统类型", example = "1") + @ExcelProperty("AGV系统类型") + private String agvSystemType; + + @Schema(description = "ACS外部任务号") + @ExcelProperty("ACS外部任务号") + private String externalTaskNo; + + @Schema(description = "取货点1") + @ExcelProperty("取货点1") + private String pointCode1; + + @Schema(description = "放货点1") + @ExcelProperty("放货点1") + private String pointCode2; + + @Schema(description = "取货点2") + @ExcelProperty("取货点2") + private String pointCode3; + + @Schema(description = "放货点2") + @ExcelProperty("放货点2") + private String pointCode4; + + @Schema(description = "载具类型", example = "2") + @ExcelProperty("载具类型") + private String vehicleType; + + @Schema(description = "载具数量", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("载具数量") + private Long vehicleQty; + + @Schema(description = "载具编码") + @ExcelProperty("载具编码") + private String vehicleCode; + + @Schema(description = "载具编码2") + @ExcelProperty("载具编码2") + private String vehicleCode2; + + @Schema(description = "车号") + @ExcelProperty("车号") + private String carNo; + + @Schema(description = "优先级", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("优先级") + private String priority; + + @Schema(description = "生产区域") + @ExcelProperty("生产区域") + private String productArea; + + @Schema(description = "是否自动下发", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("是否自动下发") + private String isAutoIssue; + + @Schema(description = "任务组标识", example = "21169") + @ExcelProperty("任务组标识") + private Long taskGroupId; + + @Schema(description = "任务组顺序号", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("任务组顺序号") + private Long sortSeq; + + @Schema(description = "任务完成类型", example = "1") + @ExcelProperty("任务完成类型") + private String finishedType; + + @Schema(description = "业务回调状态:PENDING/SUCCESS/FAILED", example = "2") + @ExcelProperty("业务回调状态:PENDING/SUCCESS/FAILED") + private String callbackStatus; + + @Schema(description = "业务回调重试次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2146") + @ExcelProperty("业务回调重试次数") + private Integer callbackRetryCount; + + @Schema(description = "业务回调失败原因") + @ExcelProperty("业务回调失败原因") + private String callbackErrorMsg; + + @Schema(description = "生成方式", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty(value = "生成方式", converter = DictConvert.class) + @DictFormat("user_type") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private String createMode; + + @Schema(description = "创建任务请求参数") + @ExcelProperty("创建任务请求参数") + private String requestParam; + + @Schema(description = "下发ACS的AcsTaskDto扩展报文") + @ExcelProperty("下发ACS的AcsTaskDto扩展报文") + private String dispatchParam; + + @Schema(description = "ACS反馈参数") + @ExcelProperty("ACS反馈参数") + private String resultParam; + + @Schema(description = "备注", example = "你说的对") + @ExcelProperty("备注") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskSaveReqVO.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskSaveReqVO.java new file mode 100644 index 00000000..5e2eb2a4 --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskSaveReqVO.java @@ -0,0 +1,126 @@ +package cn.code.nl.module.task.controller.admin.transporttask.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import jakarta.validation.constraints.*; + +@Schema(description = "管理后台 - 搬运任务新增/修改 Request VO") +@Data +public class TransportTaskSaveReqVO { + + private Long taskId; + + @Schema(description = "任务编码", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "任务编码不能为空") + private String taskCode; + + @Schema(description = "任务名称", example = "张三") + private String taskName; + + @Schema(description = "业务归属服务:LMS/WMS,用于完成取消事件一级路由", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "业务归属服务:LMS/WMS,用于完成取消事件一级路由不能为空") + private String ownerService; + + @Schema(description = "业务类型", example = "2") + private String bizType; + + @Schema(description = "业务侧标识", example = "31008") + private String bizId; + + @Schema(description = "业务回调处理器编码") + private String handleCode; + + @Schema(description = "任务类型", example = "2") + private String taskType; + + @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotEmpty(message = "任务状态不能为空") + private String taskStatus; + + @Schema(description = "ACS任务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotEmpty(message = "ACS任务类型不能为空") + private String acsTaskType; + + @Schema(description = "AGV系统类型", example = "1") + private String agvSystemType; + + @Schema(description = "ACS外部任务号") + private String externalTaskNo; + + @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 = "载具类型", example = "2") + private String vehicleType; + + @Schema(description = "载具数量", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "载具数量不能为空") + private Long vehicleQty; + + @Schema(description = "载具编码") + private String vehicleCode; + + @Schema(description = "载具编码2") + private String vehicleCode2; + + @Schema(description = "车号") + private String carNo; + + @Schema(description = "优先级", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "优先级不能为空") + private String priority; + + @Schema(description = "生产区域") + private String productArea; + + @Schema(description = "是否自动下发", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "是否自动下发不能为空") + private String isAutoIssue; + + @Schema(description = "任务组标识", example = "21169") + private Long taskGroupId; + + @Schema(description = "任务组顺序号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "任务组顺序号不能为空") + private Long sortSeq; + + @Schema(description = "任务完成类型", example = "1") + private String finishedType; + + @Schema(description = "业务回调状态:PENDING/SUCCESS/FAILED", example = "2") + private String callbackStatus; + + @Schema(description = "业务回调重试次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2146") + @NotNull(message = "业务回调重试次数不能为空") + private Integer callbackRetryCount; + + @Schema(description = "业务回调失败原因") + private String callbackErrorMsg; + + @Schema(description = "生成方式", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "生成方式不能为空") + private String createMode; + + @Schema(description = "创建任务请求参数") + private String requestParam; + + @Schema(description = "下发ACS的AcsTaskDto扩展报文") + private String dispatchParam; + + @Schema(description = "ACS反馈参数") + private String resultParam; + + @Schema(description = "备注", example = "你说的对") + private String remark; + +} \ No newline at end of file diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/package-info.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/package-info.java new file mode 100644 index 00000000..6f4584de --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 控制层包 + * @Author: liyongde + * @Date: 2026/7/13 13:56 + */ +package cn.code.nl.module.task.controller; \ No newline at end of file diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/dataobject/transporttask/TransportTaskDO.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/dataobject/transporttask/TransportTaskDO.java new file mode 100644 index 00000000..687fbc6c --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/dataobject/transporttask/TransportTaskDO.java @@ -0,0 +1,170 @@ +package cn.code.nl.module.task.dal.dataobject.transporttask; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.code.nl.framework.mybatis.core.dataobject.BaseDO; + +/** + * 搬运任务 DO + * + * @author 诺力管理员 + */ +@TableName("task_transport_job") +@KeySequence("task_transport_job_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TransportTaskDO extends BaseDO { + + /** + * 任务标识 + */ + @TableId + private Long taskId; + /** + * 任务编码 + */ + private String taskCode; + /** + * 任务名称 + */ + private String taskName; + /** + * 业务归属服务:LMS/WMS,用于完成取消事件一级路由 + */ + private String ownerService; + /** + * 业务类型 + */ + private String bizType; + /** + * 业务侧标识 + */ + private String bizId; + /** + * 业务回调处理器编码 + */ + private String handleCode; + /** + * 任务类型 + */ + private String taskType; + /** + * 任务状态 + */ + private String taskStatus; + /** + * ACS任务类型 + */ + private String acsTaskType; + /** + * AGV系统类型 + */ + private String agvSystemType; + /** + * ACS外部任务号 + */ + private String externalTaskNo; + /** + * 取货点1 + */ + private String pointCode1; + /** + * 放货点1 + */ + private String pointCode2; + /** + * 取货点2 + */ + private String pointCode3; + /** + * 放货点2 + */ + private String pointCode4; + /** + * 载具类型 + */ + private String vehicleType; + /** + * 载具数量 + */ + private Long vehicleQty; + /** + * 载具编码 + */ + private String vehicleCode; + /** + * 载具编码2 + */ + private String vehicleCode2; + /** + * 车号 + */ + private String carNo; + /** + * 优先级 + */ + private String priority; + /** + * 生产区域 + */ + private String productArea; + /** + * 是否自动下发 + */ + private String isAutoIssue; + /** + * 任务组标识 + */ + private Long taskGroupId; + /** + * 任务组顺序号 + */ + private Long sortSeq; + /** + * 任务完成类型 + */ + private String finishedType; + /** + * 业务回调状态:PENDING/SUCCESS/FAILED + */ + private String callbackStatus; + /** + * 业务回调重试次数 + */ + private Integer callbackRetryCount; + /** + * 业务回调失败原因 + */ + private String callbackErrorMsg; + /** + * 生成方式 + * + * 枚举 {@link TODO user_type 对应的类} + */ + private String createMode; + /** + * 创建任务请求参数 + */ + private String requestParam; + /** + * 下发ACS的AcsTaskDto扩展报文 + */ + private String dispatchParam; + /** + * ACS反馈参数 + */ + private String resultParam; + /** + * 备注 + */ + private String remark; + + +} \ No newline at end of file diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/mysql/transporttask/TransportTaskMapper.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/mysql/transporttask/TransportTaskMapper.java new file mode 100644 index 00000000..0a4b932c --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/mysql/transporttask/TransportTaskMapper.java @@ -0,0 +1,60 @@ +package cn.code.nl.module.task.dal.mysql.transporttask; + +import java.util.*; + +import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX; +import cn.code.nl.module.task.dal.dataobject.transporttask.TransportTaskDO; +import org.apache.ibatis.annotations.Mapper; +import cn.code.nl.module.task.controller.admin.transporttask.vo.*; + +/** + * 搬运任务 Mapper + * + * @author 诺力管理员 + */ +@Mapper +public interface TransportTaskMapper extends BaseMapperX { + + default PageResult selectPage(TransportTaskPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(TransportTaskDO::getTaskCode, reqVO.getTaskCode()) + .likeIfPresent(TransportTaskDO::getTaskName, reqVO.getTaskName()) + .eqIfPresent(TransportTaskDO::getOwnerService, reqVO.getOwnerService()) + .eqIfPresent(TransportTaskDO::getBizType, reqVO.getBizType()) + .eqIfPresent(TransportTaskDO::getBizId, reqVO.getBizId()) + .eqIfPresent(TransportTaskDO::getHandleCode, reqVO.getHandleCode()) + .eqIfPresent(TransportTaskDO::getTaskType, reqVO.getTaskType()) + .eqIfPresent(TransportTaskDO::getTaskStatus, reqVO.getTaskStatus()) + .eqIfPresent(TransportTaskDO::getAcsTaskType, reqVO.getAcsTaskType()) + .eqIfPresent(TransportTaskDO::getAgvSystemType, reqVO.getAgvSystemType()) + .eqIfPresent(TransportTaskDO::getExternalTaskNo, reqVO.getExternalTaskNo()) + .eqIfPresent(TransportTaskDO::getPointCode1, reqVO.getPointCode1()) + .eqIfPresent(TransportTaskDO::getPointCode2, reqVO.getPointCode2()) + .eqIfPresent(TransportTaskDO::getPointCode3, reqVO.getPointCode3()) + .eqIfPresent(TransportTaskDO::getPointCode4, reqVO.getPointCode4()) + .eqIfPresent(TransportTaskDO::getVehicleType, reqVO.getVehicleType()) + .eqIfPresent(TransportTaskDO::getVehicleQty, reqVO.getVehicleQty()) + .eqIfPresent(TransportTaskDO::getVehicleCode, reqVO.getVehicleCode()) + .eqIfPresent(TransportTaskDO::getVehicleCode2, reqVO.getVehicleCode2()) + .eqIfPresent(TransportTaskDO::getCarNo, reqVO.getCarNo()) + .eqIfPresent(TransportTaskDO::getPriority, reqVO.getPriority()) + .eqIfPresent(TransportTaskDO::getProductArea, reqVO.getProductArea()) + .eqIfPresent(TransportTaskDO::getIsAutoIssue, reqVO.getIsAutoIssue()) + .eqIfPresent(TransportTaskDO::getTaskGroupId, reqVO.getTaskGroupId()) + .eqIfPresent(TransportTaskDO::getSortSeq, reqVO.getSortSeq()) + .eqIfPresent(TransportTaskDO::getFinishedType, reqVO.getFinishedType()) + .eqIfPresent(TransportTaskDO::getCallbackStatus, reqVO.getCallbackStatus()) + .eqIfPresent(TransportTaskDO::getCallbackRetryCount, reqVO.getCallbackRetryCount()) + .eqIfPresent(TransportTaskDO::getCallbackErrorMsg, reqVO.getCallbackErrorMsg()) + .eqIfPresent(TransportTaskDO::getCreateMode, reqVO.getCreateMode()) + .eqIfPresent(TransportTaskDO::getRequestParam, reqVO.getRequestParam()) + .eqIfPresent(TransportTaskDO::getDispatchParam, reqVO.getDispatchParam()) + .eqIfPresent(TransportTaskDO::getResultParam, reqVO.getResultParam()) + .eqIfPresent(TransportTaskDO::getRemark, reqVO.getRemark()) + .betweenIfPresent(TransportTaskDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(TransportTaskDO::getTaskId)); + } + +} \ No newline at end of file diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/package-info.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/package-info.java new file mode 100644 index 00000000..5c586563 --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/package-info.java @@ -0,0 +1,6 @@ +/** + * 数据库字段层 + * @Author: liyongde + * @Date: 2026/7/13 13:58 + */ +package cn.code.nl.module.task.dal; \ No newline at end of file diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/framework/package-info.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/framework/package-info.java new file mode 100644 index 00000000..39c06f4a --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 封装framework + * @Author: liyongde + * @Date: 2026/7/13 14:05 + */ +package cn.code.nl.module.task.framework; \ No newline at end of file diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/framework/security/config/SecurityConfiguration.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/framework/security/config/SecurityConfiguration.java new file mode 100644 index 00000000..08562973 --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,43 @@ +package cn.code.nl.module.task.framework.security.config; + +import cn.code.nl.framework.security.config.AuthorizeRequestsCustomizer; +import cn.code.nl.module.infra.enums.ApiConstants; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; + +/** + * Infra 模块的 Security 配置 + */ +@Configuration(proxyBeanMethods = false, value = "taskSecurityConfiguration") +public class SecurityConfiguration { + + @Bean("taskAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + // Swagger 接口文档 + registry.requestMatchers("/v3/api-docs/**").permitAll() + .requestMatchers("/webjars/**").permitAll() + .requestMatchers("/swagger-ui").permitAll() + .requestMatchers("/swagger-ui/**").permitAll(); + // Spring Boot Actuator 的安全配置 + registry.requestMatchers("/actuator").permitAll() + .requestMatchers("/actuator/**").permitAll(); + // Druid 监控 + registry.requestMatchers("/druid/**").permitAll(); + // 文件读取 + registry.requestMatchers(buildAdminApi("/task/file/*/get/**")).permitAll(); + + // TODO 芋艿:这个每个项目都需要重复配置,得捉摸有没通用的方案 + // RPC 服务的安全配置 + registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll(); + } + + }; + } + +} diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/framework/security/core/package-info.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/framework/security/core/package-info.java new file mode 100644 index 00000000..91d8d44f --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.code.nl.module.task.framework.security.core; diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/job/TaskScheduleJob.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/job/TaskScheduleJob.java new file mode 100644 index 00000000..3fc005da --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/job/TaskScheduleJob.java @@ -0,0 +1,28 @@ +package cn.code.nl.module.task.job; + +import cn.code.nl.framework.tenant.core.job.TenantJob; +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.handler.annotation.XxlJob; +import org.springframework.stereotype.Component; + +/** + * 任务相关的定时任务 + * @Author: liyongde + * @Date: 2026/7/13 14:01 + */ +@Component +public class TaskScheduleJob { + + @XxlJob("autoTaskAssignmentJob") + @TenantJob + public void autoTaskAssignmentJob() { + XxlJobHelper.log("自动下发任务开始"); + + // todo: 具体业务 + + String msg = "自动下发任务成功"; + XxlJobHelper.log(msg); + XxlJobHelper.handleSuccess(msg); + } + +} diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/job/package-info.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/job/package-info.java new file mode 100644 index 00000000..f93c19d0 --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/job/package-info.java @@ -0,0 +1,6 @@ +/** + * xxljob任务 + * @Author: liyongde + * @Date: 2026/7/13 14:00 + */ +package cn.code.nl.module.task.job; \ No newline at end of file diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/package-info.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/package-info.java new file mode 100644 index 00000000..237ea11e --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/package-info.java @@ -0,0 +1,6 @@ +/** + * + * @Author: liyongde + * @Date: 2026/7/14 13:02 + */ +package cn.code.nl.module.task.service; \ No newline at end of file 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 new file mode 100644 index 00000000..1afb5264 --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java @@ -0,0 +1,62 @@ +package cn.code.nl.module.task.service.transporttask; + +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.framework.common.pojo.PageResult; +import cn.code.nl.framework.common.pojo.PageParam; + +/** + * 搬运任务 Service 接口 + * + * @author 诺力管理员 + */ +public interface TransportTaskService { + + /** + * 创建搬运任务 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createTransportTask(@Valid TransportTaskSaveReqVO createReqVO); + + /** + * 更新搬运任务 + * + * @param updateReqVO 更新信息 + */ + void updateTransportTask(@Valid TransportTaskSaveReqVO updateReqVO); + + /** + * 删除搬运任务 + * + * @param id 编号 + */ + void deleteTransportTask(Long id); + + /** + * 批量删除搬运任务 + * + * @param ids 编号 + */ + void deleteTransportTaskListByIds(List ids); + + /** + * 获得搬运任务 + * + * @param id 编号 + * @return 搬运任务 + */ + TransportTaskDO getTransportTask(Long id); + + /** + * 获得搬运任务分页 + * + * @param pageReqVO 分页查询 + * @return 搬运任务分页 + */ + PageResult getTransportTaskPage(TransportTaskPageReqVO pageReqVO); + +} \ No newline at end of file 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 new file mode 100644 index 00000000..386de2bf --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java @@ -0,0 +1,80 @@ +package cn.code.nl.module.task.service.transporttask; + +import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.framework.common.util.object.BeanUtils; +import cn.code.nl.module.task.controller.admin.transporttask.vo.TransportTaskPageReqVO; +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 jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.code.nl.module.task.enums.ErrorCodeConstants.*; + +/** + * 搬运任务 Service 实现类 + * + * @author 诺力管理员 + */ +@Service +@Validated +public class TransportTaskServiceImpl implements TransportTaskService { + + @Resource + private TransportTaskMapper transportTaskMapper; + + @Override + public Long createTransportTask(TransportTaskSaveReqVO createReqVO) { + // 插入 + TransportTaskDO transportTask = BeanUtils.toBean(createReqVO, TransportTaskDO.class); + transportTaskMapper.insert(transportTask); + + // 返回 + return transportTask.getTaskId(); + } + + @Override + public void updateTransportTask(TransportTaskSaveReqVO updateReqVO) { + // 校验存在 + validateTransportTaskExists(updateReqVO.getTaskId()); + // 更新 + TransportTaskDO updateObj = BeanUtils.toBean(updateReqVO, TransportTaskDO.class); + transportTaskMapper.updateById(updateObj); + } + + @Override + public void deleteTransportTask(Long id) { + // 校验存在 + validateTransportTaskExists(id); + // 删除 + transportTaskMapper.deleteById(id); + } + + @Override + public void deleteTransportTaskListByIds(List ids) { + // 删除 + transportTaskMapper.deleteByIds(ids); + } + + + private void validateTransportTaskExists(Long id) { + if (transportTaskMapper.selectById(id) == null) { + throw exception(TRANSPORT_TASK_NOT_EXISTS); + } + } + + @Override + public TransportTaskDO getTransportTask(Long id) { + return transportTaskMapper.selectById(id); + } + + @Override + public PageResult getTransportTaskPage(TransportTaskPageReqVO pageReqVO) { + return transportTaskMapper.selectPage(pageReqVO); + } + +} diff --git a/nl-module-task/nl-module-task-server/src/main/resources/application-dev.yaml b/nl-module-task/nl-module-task-server/src/main/resources/application-dev.yaml index 10810337..bf2df653 100644 --- a/nl-module-task/nl-module-task-server/src/main/resources/application-dev.yaml +++ b/nl-module-task/nl-module-task-server/src/main/resources/application-dev.yaml @@ -3,15 +3,15 @@ spring: cloud: nacos: - server-addr: http://192.168.10.29:8848 # Nacos 服务器地址 - username: # Nacos 账号 - password: # Nacos 密码 - discovery: # 【配置中心】配置项 + server-addr: 192.168.81.193:8848 # Nacos 服务器地址 + username: nacos + password: nacos + discovery: # 【注册中心】配置项 namespace: dev # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP metadata: version: 1.0.0 # 服务实例的版本号,可用于灰度发布 - config: # 【注册中心】配置项 + config: # 【配置中心】配置项 namespace: dev # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP @@ -57,14 +57,14 @@ spring: primary: master datasource: master: - url: jdbc:mysql://127.0.0.1:3306/huachuang_lms?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 12356 + password: root123 slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:mysql://127.0.0.1:3306/huachuang_lms?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 12356 + password: root123 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: @@ -95,9 +95,9 @@ spring: xxl: job: admin: - addresses: http://192.168.10.29:8080/xxl-job-admin # 调度中心部署跟地址 + addresses: http://192.168.10.2:8080/xxl-job-admin # 调度中心部署跟地址 executor: - ip: 192.168.10.29 + ip: 192.168.10.2 port: 9992 --- #################### 服务保障相关配置 #################### diff --git a/nl-module-task/nl-module-task-server/src/main/resources/application-local.yaml b/nl-module-task/nl-module-task-server/src/main/resources/application-local.yaml index 10810337..f3c6b3c9 100644 --- a/nl-module-task/nl-module-task-server/src/main/resources/application-local.yaml +++ b/nl-module-task/nl-module-task-server/src/main/resources/application-local.yaml @@ -3,7 +3,7 @@ spring: cloud: nacos: - server-addr: http://192.168.10.29:8848 # Nacos 服务器地址 + server-addr: 192.168.81.193:8848 # Nacos 服务器地址 username: # Nacos 账号 password: # Nacos 密码 discovery: # 【配置中心】配置项 @@ -57,14 +57,14 @@ spring: primary: master datasource: master: - url: jdbc:mysql://127.0.0.1:3306/huachuang_lms?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 12356 + password: root123 slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:mysql://127.0.0.1:3306/huachuang_lms?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 12356 + password: root123 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: @@ -95,9 +95,9 @@ spring: xxl: job: admin: - addresses: http://192.168.10.29:8080/xxl-job-admin # 调度中心部署跟地址 + addresses: http://192.168.10.2:8080/xxl-job-admin # 调度中心部署跟地址 executor: - ip: 192.168.10.29 + ip: 192.168.10.2 port: 9992 --- #################### 服务保障相关配置 #################### diff --git a/nl-server/src/main/java/cn/code/nl/server/controller/DefaultController.java b/nl-server/src/main/java/cn/code/nl/server/controller/DefaultController.java index 4f8dba1e..6a8cbb61 100644 --- a/nl-server/src/main/java/cn/code/nl/server/controller/DefaultController.java +++ b/nl-server/src/main/java/cn/code/nl/server/controller/DefaultController.java @@ -20,80 +20,6 @@ import static cn.code.nl.framework.common.exception.enums.GlobalErrorCodeConstan @Slf4j public class DefaultController { - @RequestMapping("/admin-api/bpm/**") - public CommonResult bpm404() { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[工作流模块 nl-module-bpm - 已禁用][参考 https://doc.iocoder.cn/bpm/ 开启]"); - } - - @RequestMapping("/admin-api/mp/**") - public CommonResult mp404() { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[微信公众号 nl-module-mp - 已禁用][参考 https://doc.iocoder.cn/mp/build/ 开启]"); - } - - @RequestMapping(value = { "/admin-api/product/**", // 商品中心 - "/admin-api/trade/**", // 交易中心 - "/admin-api/promotion/**" }) // 营销中心 - public CommonResult mall404() { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[商城系统 nl-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]"); - } - - @RequestMapping("/admin-api/erp/**") - public CommonResult erp404() { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[ERP 模块 nl-module-erp - 已禁用][参考 https://doc.iocoder.cn/erp/build/ 开启]"); - } - - @RequestMapping(value = { "/admin-api/wms/**"}) - public CommonResult wms404() { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[WMS 仓库管理系统 nl-module-wms - 已禁用][参考 https://doc.iocoder.cn/wms/build/ 开启]"); - } - - @RequestMapping("/admin-api/crm/**") - public CommonResult crm404() { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[CRM 模块 nl-module-crm - 已禁用][参考 https://doc.iocoder.cn/crm/build/ 开启]"); - } - - @RequestMapping(value = { "/admin-api/mes/**"}) - public CommonResult mes404() { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[MES 系统 nl-module-mes - 已禁用][参考 https://doc.iocoder.cn/mes/build/ 开启]"); - } - - @RequestMapping(value = { "/admin-api/im/**"}) - public CommonResult im404() { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[IM 即时通讯 nl-module-im - 已禁用][参考 https://doc.iocoder.cn/im/build/ 开启]"); - } - - @RequestMapping(value = { "/admin-api/report/**"}) - public CommonResult report404() { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[报表模块 nl-module-report - 已禁用][参考 https://doc.iocoder.cn/report/ 开启]"); - } - - @RequestMapping(value = { "/admin-api/pay/**"}) - public CommonResult pay404() { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[支付模块 nl-module-pay - 已禁用][参考 https://doc.iocoder.cn/pay/build/ 开启]"); - } - - @RequestMapping(value = { "/admin-api/ai/**"}) - public CommonResult ai404() { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[AI 大模型 nl-module-ai - 已禁用][参考 https://doc.iocoder.cn/ai/build/ 开启]"); - } - - @RequestMapping(value = { "/admin-api/iot/**"}) - public CommonResult iot404() { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[IoT 物联网 nl-module-iot - 已禁用][参考 https://doc.iocoder.cn/iot/build/ 开启]"); - } - /** * 测试接口:打印 query、header、body */ diff --git a/nl-server/src/main/resources/application-dev.yaml b/nl-server/src/main/resources/application-dev.yaml index 38ac2498..ba8e8a6a 100644 --- a/nl-server/src/main/resources/application-dev.yaml +++ b/nl-server/src/main/resources/application-dev.yaml @@ -47,14 +47,14 @@ spring: primary: master datasource: master: - url: jdbc:mysql://127.0.0.1:3306/huachuang_lms?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 12356 + password: root123 slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:mysql://127.0.0.1:3306/huachuang_lms?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root - password: 12356 + password: root123 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: @@ -70,10 +70,10 @@ xxl: job: enabled: false # 是否开启调度中心,默认为 true 开启 admin: - addresses: http://192.168.10.29:8080/xxl-job-admin # 调度中心部署跟地址 + addresses: http://192.168.10.2:8080/xxl-job-admin # 调度中心部署跟地址 executor: appname: nl-lms-server - ip: 192.168.10.29 + ip: 192.168.10.2 port: 9992 --- #################### 消息队列相关 #################### diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/task/transporttask/index.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/task/transporttask/index.ts new file mode 100644 index 00000000..ca32be74 --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/task/transporttask/index.ts @@ -0,0 +1,87 @@ +import type { PageParam, PageResult } from '@vben/request'; + +import { requestClient } from '#/api/request'; + +export namespace TaskTransportTaskApi { + /** 搬运任务信息 */ + export interface TransportTask { + taskCode?: string; // 任务编码 + taskName: string; // 任务名称 + ownerService?: string; // 业务归属服务:LMS/WMS,用于完成取消事件一级路由 + bizType: string; // 业务类型 + bizId: string; // 业务侧标识 + handleCode: string; // 业务回调处理器编码 + taskType: string; // 任务类型 + taskStatus?: string; // 任务状态 + acsTaskType?: string; // ACS任务类型 + agvSystemType: string; // AGV系统类型 + externalTaskNo: string; // ACS外部任务号 + pointCode1: string; // 取货点1 + pointCode2: string; // 放货点1 + pointCode3: string; // 取货点2 + pointCode4: string; // 放货点2 + vehicleType: string; // 载具类型 + vehicleQty?: number; // 载具数量 + vehicleCode: string; // 载具编码 + vehicleCode2: string; // 载具编码2 + carNo: string; // 车号 + priority?: string; // 优先级 + productArea: string; // 生产区域 + isAutoIssue?: string; // 是否自动下发 + taskGroupId: number; // 任务组标识 + sortSeq?: number; // 任务组顺序号 + finishedType: string; // 任务完成类型 + callbackStatus: string; // 业务回调状态:PENDING/SUCCESS/FAILED + callbackRetryCount?: number; // 业务回调重试次数 + callbackErrorMsg: string; // 业务回调失败原因 + createMode?: string; // 生成方式 + requestParam: string; // 创建任务请求参数 + dispatchParam: string; // 下发ACS的AcsTaskDto扩展报文 + resultParam: string; // ACS反馈参数 + remark: string; // 备注 + } +} + +/** 查询搬运任务分页 */ +export function getTransportTaskPage(params: PageParam) { + return requestClient.get>( + '/task/transport-task/page', + { params }, + ); +} + +/** 查询搬运任务详情 */ +export function getTransportTask(id: number) { + return requestClient.get( + `/task/transport-task/get?id=${id}`, + ); +} + +/** 新增搬运任务 */ +export function createTransportTask(data: TaskTransportTaskApi.TransportTask) { + return requestClient.post('/task/transport-task/create', data); +} + +/** 修改搬运任务 */ +export function updateTransportTask(data: TaskTransportTaskApi.TransportTask) { + return requestClient.put('/task/transport-task/update', data); +} + +/** 删除搬运任务 */ +export function deleteTransportTask(id: number) { + return requestClient.delete(`/task/transport-task/delete?id=${id}`); +} + +/** 批量删除搬运任务 */ +export function deleteTransportTaskList(ids: number[]) { + return requestClient.delete( + `/task/transport-task/delete-list?ids=${ids.join(',')}`, + ); +} + +/** 导出搬运任务 */ +export function exportTransportTask(params: any) { + return requestClient.download('/task/transport-task/export-excel', { params }); +} + + diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/task/transporttask/data.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/task/transporttask/data.ts new file mode 100644 index 00000000..c197f26b --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/task/transporttask/data.ts @@ -0,0 +1,707 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { TaskTransportTaskApi } from '#/api/task/transporttask'; + +import { DICT_TYPE } from '@vben/constants'; +import { getDictOptions } from '@vben/hooks'; + +import { getRangePickerDefaultProps } from '#/utils'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'taskId', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'taskCode', + label: '任务编码', + rules: 'required', + component: 'Input', + componentProps: { + placeholder: '请输入任务编码', + }, + }, + { + fieldName: 'taskName', + label: '任务名称', + component: 'Input', + componentProps: { + placeholder: '请输入任务名称', + }, + }, + { + fieldName: 'ownerService', + label: '业务归属服务', + rules: 'required', + component: 'Input', + componentProps: { + placeholder: '请输入业务归属服务:LMS/WMS', + }, + }, + { + fieldName: 'bizType', + label: '业务类型', + component: 'Select', + componentProps: { + options: [], + placeholder: '请选择业务类型', + }, + }, + { + fieldName: 'bizId', + label: '业务侧标识', + component: 'Input', + componentProps: { + placeholder: '请输入业务侧标识', + }, + }, + { + fieldName: 'handleCode', + label: '业务回调处理器编码', + component: 'Input', + componentProps: { + placeholder: '请输入业务回调处理器编码', + }, + }, + { + fieldName: 'taskType', + label: '任务类型', + component: 'Select', + componentProps: { + options: [], + placeholder: '请选择任务类型', + }, + }, + { + fieldName: 'taskStatus', + label: '任务状态', + rules: 'required', + component: 'RadioGroup', + componentProps: { + options: [], + buttonStyle: 'solid', + optionType: 'button', + }, + }, + { + fieldName: 'acsTaskType', + label: 'ACS任务类型', + rules: 'required', + component: 'Select', + componentProps: { + options: [], + placeholder: '请选择ACS任务类型', + }, + }, + { + fieldName: 'agvSystemType', + label: 'AGV系统类型', + component: 'Select', + componentProps: { + options: [], + placeholder: '请选择AGV系统类型', + }, + }, + { + fieldName: 'externalTaskNo', + label: 'ACS外部任务号', + component: 'Input', + componentProps: { + placeholder: '请输入ACS外部任务号', + }, + }, + { + fieldName: 'pointCode1', + label: '取货点1', + component: 'Input', + componentProps: { + placeholder: '请输入取货点1', + }, + }, + { + fieldName: 'pointCode2', + label: '放货点1', + component: 'Input', + componentProps: { + placeholder: '请输入放货点1', + }, + }, + { + fieldName: 'pointCode3', + label: '取货点2', + component: 'Input', + componentProps: { + placeholder: '请输入取货点2', + }, + }, + { + fieldName: 'pointCode4', + label: '放货点2', + component: 'Input', + componentProps: { + placeholder: '请输入放货点2', + }, + }, + { + fieldName: 'vehicleType', + label: '载具类型', + component: 'Select', + componentProps: { + options: [], + placeholder: '请选择载具类型', + }, + }, + { + fieldName: 'vehicleQty', + label: '载具数量', + rules: 'required', + component: 'Input', + componentProps: { + placeholder: '请输入载具数量', + }, + }, + { + fieldName: 'vehicleCode', + label: '载具编码', + component: 'Input', + componentProps: { + placeholder: '请输入载具编码', + }, + }, + { + fieldName: 'vehicleCode2', + label: '载具编码2', + component: 'Input', + componentProps: { + placeholder: '请输入载具编码2', + }, + }, + { + fieldName: 'carNo', + label: '车号', + component: 'Input', + componentProps: { + placeholder: '请输入车号', + }, + }, + { + fieldName: 'priority', + label: '优先级', + rules: 'required', + component: 'Input', + componentProps: { + placeholder: '请输入优先级', + }, + }, + { + fieldName: 'productArea', + label: '生产区域', + component: 'Input', + componentProps: { + placeholder: '请输入生产区域', + }, + }, + { + fieldName: 'isAutoIssue', + label: '是否自动下发', + rules: 'required', + component: 'Input', + componentProps: { + placeholder: '请输入是否自动下发', + }, + }, + { + fieldName: 'taskGroupId', + label: '任务组标识', + component: 'Input', + componentProps: { + placeholder: '请输入任务组标识', + }, + }, + { + fieldName: 'sortSeq', + label: '任务组顺序号', + rules: 'required', + component: 'Input', + componentProps: { + placeholder: '请输入任务组顺序号', + }, + }, + { + fieldName: 'finishedType', + label: '任务完成类型', + component: 'Select', + componentProps: { + options: [], + placeholder: '请选择任务完成类型', + }, + }, + { + fieldName: 'callbackStatus', + label: '业务回调状态:PENDING/SUCCESS/FAILED', + component: 'RadioGroup', + componentProps: { + options: [], + buttonStyle: 'solid', + optionType: 'button', + }, + }, + { + fieldName: 'callbackRetryCount', + label: '业务回调重试次数', + rules: 'required', + component: 'Input', + componentProps: { + placeholder: '请输入业务回调重试次数', + }, + }, + { + fieldName: 'callbackErrorMsg', + label: '业务回调失败原因', + component: 'Input', + componentProps: { + placeholder: '请输入业务回调失败原因', + }, + }, + { + fieldName: 'createMode', + label: '生成方式', + rules: 'required', + component: 'Select', + componentProps: { + options: getDictOptions(DICT_TYPE.USER_TYPE, 'string'), + placeholder: '请选择生成方式', + }, + }, + { + fieldName: 'requestParam', + label: '创建任务请求参数', + component: 'Input', + componentProps: { + placeholder: '请输入创建任务请求参数', + }, + }, + { + fieldName: 'dispatchParam', + label: '下发ACS的AcsTaskDto扩展报文', + component: 'Input', + componentProps: { + placeholder: '请输入下发ACS的AcsTaskDto扩展报文', + }, + }, + { + fieldName: 'resultParam', + label: 'ACS反馈参数', + component: 'Input', + componentProps: { + placeholder: '请输入ACS反馈参数', + }, + }, + { + fieldName: 'remark', + label: '备注', + component: 'Input', + componentProps: { + placeholder: '请输入备注', + }, + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'taskCode', + label: '任务编码', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入任务编码', + }, + }, + { + fieldName: 'taskName', + label: '任务名称', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入任务名称', + }, + }, + { + fieldName: 'ownerService', + label: '业务归属服务', + component: 'Select', + componentProps: { + allowClear: true, + options: getDictOptions(DICT_TYPE.TASK_OWNER_SERVICE), + placeholder: '请输入业务归属服务', + }, + }, + { + fieldName: 'bizType', + label: '业务类型', + component: 'Select', + componentProps: { + allowClear: true, + options: [], + placeholder: '请选择业务类型', + }, + }, + { + fieldName: 'handleCode', + label: '处理器编码', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入业务回调处理器编码', + }, + }, + { + fieldName: 'taskType', + label: '任务类型', + component: 'Select', + componentProps: { + allowClear: true, + options: [], + placeholder: '请选择任务类型', + }, + }, + { + fieldName: 'taskStatus', + label: '任务状态', + component: 'Select', + componentProps: { + allowClear: true, + options: [], + placeholder: '请选择任务状态', + }, + }, + { + fieldName: 'acsTaskType', + label: 'ACS任务类型', + component: 'Select', + componentProps: { + allowClear: true, + options: [], + placeholder: '请选择ACS任务类型', + }, + }, + { + fieldName: 'pointCode1', + label: '取货点1', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入取货点1', + }, + }, + { + fieldName: 'pointCode2', + label: '放货点1', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入放货点1', + }, + }, + { + fieldName: 'pointCode3', + label: '取货点2', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入取货点2', + }, + }, + { + fieldName: 'pointCode4', + label: '放货点2', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入放货点2', + }, + }, + { + fieldName: 'vehicleType', + label: '载具类型', + component: 'Select', + componentProps: { + allowClear: true, + options: [], + placeholder: '请选择载具类型', + }, + }, + { + fieldName: 'vehicleCode', + label: '载具编码', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入载具编码', + }, + }, + { + fieldName: 'vehicleCode2', + label: '载具编码2', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入载具编码2', + }, + }, + { + fieldName: 'carNo', + label: '车号', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入车号', + }, + }, + { + fieldName: 'productArea', + label: '生产区域', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入生产区域', + }, + }, + { + fieldName: 'taskGroupId', + label: '任务组标识', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入任务组标识', + }, + }, + { + fieldName: 'createMode', + label: '生成方式', + component: 'Select', + componentProps: { + allowClear: true, + options: getDictOptions(DICT_TYPE.USER_TYPE, 'string'), + placeholder: '请选择生成方式', + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { type: 'checkbox', width: 40 }, + { + field: 'taskId', + title: '任务标识', + minWidth: 120, + }, + { + field: 'taskCode', + title: '任务编码', + minWidth: 120, + }, + { + field: 'taskName', + title: '任务名称', + minWidth: 120, + }, + { + field: 'ownerService', + title: '业务归属服务', + minWidth: 120, + }, + { + field: 'bizType', + title: '业务类型', + minWidth: 120, + }, + { + field: 'bizId', + title: '业务侧标识', + minWidth: 120, + }, + { + field: 'handleCode', + title: '业务回调处理器编码', + minWidth: 120, + }, + { + field: 'taskType', + title: '任务类型', + minWidth: 120, + }, + { + field: 'taskStatus', + title: '任务状态', + minWidth: 120, + }, + { + field: 'acsTaskType', + title: 'ACS任务类型', + minWidth: 120, + }, + { + field: 'agvSystemType', + title: 'AGV系统类型', + minWidth: 120, + }, + { + field: 'externalTaskNo', + title: 'ACS外部任务号', + minWidth: 120, + }, + { + field: 'pointCode1', + title: '取货点1', + minWidth: 120, + }, + { + field: 'pointCode2', + title: '放货点1', + minWidth: 120, + }, + { + field: 'pointCode3', + title: '取货点2', + minWidth: 120, + }, + { + field: 'pointCode4', + title: '放货点2', + minWidth: 120, + }, + { + field: 'vehicleType', + title: '载具类型', + minWidth: 120, + }, + { + field: 'vehicleQty', + title: '载具数量', + minWidth: 120, + }, + { + field: 'vehicleCode', + title: '载具编码', + minWidth: 120, + }, + { + field: 'vehicleCode2', + title: '载具编码2', + minWidth: 120, + }, + { + field: 'carNo', + title: '车号', + minWidth: 120, + }, + { + field: 'priority', + title: '优先级', + minWidth: 120, + }, + { + field: 'productArea', + title: '生产区域', + minWidth: 120, + }, + { + field: 'isAutoIssue', + title: '是否自动下发', + minWidth: 120, + }, + { + field: 'taskGroupId', + title: '任务组标识', + minWidth: 120, + }, + { + field: 'sortSeq', + title: '任务组顺序号', + minWidth: 120, + }, + { + field: 'finishedType', + title: '任务完成类型', + minWidth: 120, + }, + { + field: 'callbackStatus', + title: '业务回调状态:PENDING/SUCCESS/FAILED', + minWidth: 120, + }, + { + field: 'callbackRetryCount', + title: '业务回调重试次数', + minWidth: 120, + }, + { + field: 'callbackErrorMsg', + title: '业务回调失败原因', + minWidth: 120, + }, + { + field: 'createMode', + title: '生成方式', + minWidth: 120, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.USER_TYPE }, + }, + }, + { + field: 'requestParam', + title: '创建任务请求参数', + minWidth: 120, + }, + { + field: 'dispatchParam', + title: '下发ACS的AcsTaskDto扩展报文', + minWidth: 120, + }, + { + field: 'resultParam', + title: 'ACS反馈参数', + minWidth: 120, + }, + { + field: 'remark', + title: '备注', + minWidth: 120, + }, + { + field: 'createTime', + title: '创建时间', + minWidth: 120, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 200, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} + diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/task/transporttask/index.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/task/transporttask/index.vue new file mode 100644 index 00000000..3eb7c686 --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/task/transporttask/index.vue @@ -0,0 +1,179 @@ + + + diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/task/transporttask/modules/form.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/task/transporttask/modules/form.vue new file mode 100644 index 00000000..927fe193 --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/task/transporttask/modules/form.vue @@ -0,0 +1,82 @@ + + + \ No newline at end of file diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts index 8433b6cd..eb37fc77 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts @@ -280,6 +280,11 @@ const WMS_DICT = { WMS_SHIPMENT_ORDER_TYPE: 'wms_shipment_order_type', // WMS 出库单类型 } as const; +/** ========== TASK - 任务管理模块 ========== */ +const TASK_DICT = { + TASK_OWNER_SERVICE: 'task_owner_service', // TASK 任务回调服务提供类型 +} as const; + /** 字典类型枚举 - 统一导出 */ const DICT_TYPE = { ...AI_DICT, @@ -297,6 +302,7 @@ const DICT_TYPE = { ...MALL_DICT, ...SYSTEM_DICT, ...COMMON_DICT, + ...TASK_DICT, } as const; export { DICT_TYPE }; diff --git a/sql/mysql/add26071001.sql b/sql/mysql/add26071001.sql index 78aa0f27..7f4ce7e4 100644 --- a/sql/mysql/add26071001.sql +++ b/sql/mysql/add26071001.sql @@ -12,4 +12,7 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', null, '1', '2025-12-16 19:25:51', '1', '2025-12-17 09:46:15', false); INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3037, 42, 'Vben5.0 Antdv Next Schema 模版', '42', 'infra_codegen_front_type', 0, '', '', '', '1', '2026-05-23 13:52:25', '1', '2026-05-23 13:52:25', false); INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3038, 43, 'Vben5.0 Antdv Next 标准模版', '43', 'infra_codegen_front_type', 0, '', '', '', '1', '2026-05-23 13:52:25', '1', '2026-05-23 13:52:25', false); -INSERT INTO system_dict_type (id, name, type, status, remark, creator, create_time, updater, update_time, deleted, deleted_time) VALUES (168, '代码生成的前端类型', 'infra_codegen_front_type', 0, '', '1', '2023-04-12 23:57:52', '1', '2023-04-12 23:57:52', false, '1970-01-01 00:00:00'); \ No newline at end of file +INSERT INTO system_dict_type (id, name, type, status, remark, creator, create_time, updater, update_time, deleted, deleted_time) VALUES (168, '代码生成的前端类型', 'infra_codegen_front_type', 0, '', '1', '2023-04-12 23:57:52', '1', '2023-04-12 23:57:52', false, '1970-01-01 00:00:00'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1145, 1, '管理后台', '1', 'infra_codegen_scene', 0, '', '', '代码生成的场景枚举 - 管理后台', '1', '2022-02-02 13:15:06', '1', '2022-03-10 16:32:59', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1146, 2, '用户 APP', '2', 'infra_codegen_scene', 0, '', '', '代码生成的场景枚举 - 用户 APP', '1', '2022-02-02 13:15:19', '1', '2022-03-10 16:33:03', b'0'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (144, '代码生成的场景枚举', 'infra_codegen_scene', 0, '代码生成的场景枚举', '1', '2022-02-02 13:14:45', '1', '2022-03-10 16:33:46', b'0', NULL);