From be46cf49c5e36678a5f4e25636671b7ad2923aa7 Mon Sep 17 00:00:00 2001 From: liyongde <1419499670@qq.com> Date: Thu, 16 Jul 2026 14:11:30 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20ACS=E3=80=81PC=E5=AF=B9=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E7=8A=B6=E6=80=81=E7=9A=84=E5=8F=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile | 87 +++ .../2026-07-15-task-operation-refactor.md | 697 ++++++++++++++++++ .../2026-07-16-jenkinsfile-monolith-deploy.md | 170 +++++ .../2026-07-16-transport-task-api-query.md | 252 +++++++ ...26-07-15-task-operation-refactor-design.md | 139 ++++ ...7-16-jenkinsfile-monolith-deploy-design.md | 73 ++ ...6-07-16-transport-task-api-query-design.md | 112 +++ .../execute/biz/{ => api}/TaskCommonApi.java | 5 +- .../biz/{ => api}/lms/LmsTaskCommonApi.java | 4 +- .../biz/{ => api}/wms/WmsTaskCommonApi.java | 4 +- .../biz/dto/TaskStatusCallApiReqDTO.java | 38 + .../config/NlExecuteRpcAutoConfiguration.java | 4 +- .../execute/core/TaskCommonApiFactory.java | 6 +- .../java/cn/code/nl/module/lms/DemoApi.java | 5 +- .../nl/module/task/api/TransportTaskApi.java | 11 + .../nl/module/task/dto/AcsFeedbackReqDTO.java | 8 +- .../code/nl/module/task/dto/TaskInfoDTO.java | 154 ++++ .../module/task/enums/ErrorCodeConstants.java | 2 + .../module/task/enums/FinishedTypeEnum.java | 23 + .../task/enums/TaskOperationTypeEnum.java | 55 ++ .../module/task/api/TransportTaskApiImpl.java | 11 + .../client/TransportTaskStatusClient.java | 1 + .../transporttask/AcsFeedbackController.java | 1 - .../TransportTaskController.java | 8 + .../vo/TransportTaskOperateReqVO.java | 25 + .../vo/TransportTaskPageReqVO.java | 6 +- .../transporttask/TransportTaskConvert.java | 23 + .../transporttask/TransportTaskMapper.java | 9 +- .../task/mq/message/TaskEventMessage.java | 3 + .../TransportTaskFeedbackServiceImpl.java | 175 +---- .../transporttask/TransportTaskService.java | 35 + .../TransportTaskServiceImpl.java | 251 ++++++- .../src/api/task/transporttask/index.ts | 9 + .../src/components/table-action/icons.ts | 2 + .../src/views/task/transporttask/data.ts | 148 ++-- .../src/views/task/transporttask/index.vue | 57 +- .../packages/constants/src/dict-enum.ts | 4 + 37 files changed, 2349 insertions(+), 268 deletions(-) create mode 100644 Jenkinsfile create mode 100644 docs/superpowers/plans/2026-07-15-task-operation-refactor.md create mode 100644 docs/superpowers/plans/2026-07-16-jenkinsfile-monolith-deploy.md create mode 100644 docs/superpowers/plans/2026-07-16-transport-task-api-query.md create mode 100644 docs/superpowers/specs/2026-07-15-task-operation-refactor-design.md create mode 100644 docs/superpowers/specs/2026-07-16-jenkinsfile-monolith-deploy-design.md create mode 100644 docs/superpowers/specs/2026-07-16-transport-task-api-query-design.md rename nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/{ => api}/TaskCommonApi.java (50%) rename nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/{ => api}/lms/LmsTaskCommonApi.java (82%) rename nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/{ => api}/wms/WmsTaskCommonApi.java (82%) create mode 100644 nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/dto/TaskStatusCallApiReqDTO.java create mode 100644 nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskInfoDTO.java create mode 100644 nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/FinishedTypeEnum.java create mode 100644 nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/TaskOperationTypeEnum.java create mode 100644 nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskOperateReqVO.java create mode 100644 nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/convert/transporttask/TransportTaskConvert.java diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..8d5f540c --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,87 @@ +// nl-server 单体部署流水线(构建机 = 运行机,Linux + JDK17+ + Maven + Docker) +pipeline { + agent any + + parameters { + choice(name: 'PROFILE', choices: ['test', 'dev', 'prod'], description: '运行环境(注入 SPRING_PROFILES_ACTIVE)') + string(name: 'JAVA_OPTS', defaultValue: '-Xms512m -Xmx512m -Djava.security.egd=file:/dev/./urandom', description: 'JVM 启动参数') + } + + environment { + IMAGE_NAME = 'nl-server' + CONTAINER_NAME = 'nl-server' + APP_PORT = '48080' + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Maven Build') { + steps { + // 只构建 nl-server 及其依赖模块,跳过测试 + sh 'mvn clean package -pl nl-server -am -DskipTests' + } + } + + stage('Docker Build') { + steps { + // 构建上下文为 nl-server 目录,复用其现有 Dockerfile + sh "docker build -t ${IMAGE_NAME}:${BUILD_NUMBER} -t ${IMAGE_NAME}:latest nl-server/" + } + } + + stage('Deploy') { + steps { + // 停删旧容器(首次部署无旧容器,容错处理),host 网络启动新容器 + sh """ + docker stop ${CONTAINER_NAME} || true + docker rm ${CONTAINER_NAME} || true + docker run -d --name ${CONTAINER_NAME} \\ + --restart always \\ + --network host \\ + -v /docker/nl-cloud/logs:/root/logs/ \\ + -e TZ=Asia/Shanghai \\ + -e SPRING_PROFILES_ACTIVE=${params.PROFILE} \\ + -e "JAVA_OPTS=${params.JAVA_OPTS}" \\ + ${IMAGE_NAME}:${BUILD_NUMBER} + """ + } + } + + stage('Health Check') { + steps { + // 最多等待 24 次 x 5s ≈ 120s,收到任意 HTTP 响应(含 404)即视为启动成功 + sh """ + echo "等待 nl-server 启动..." + for i in \$(seq 1 24); do + if curl -s -o /dev/null http://127.0.0.1:${APP_PORT}; then + echo "nl-server 启动成功" + exit 0 + fi + sleep 5 + done + echo "健康检查超时,输出容器日志:" + docker logs --tail 200 ${CONTAINER_NAME} + exit 1 + """ + } + } + } + + post { + success { + echo "部署成功:${IMAGE_NAME}:${BUILD_NUMBER},服务端口 ${APP_PORT}(历史镜像 tag 保留,可用于手动回滚)" + } + failure { + echo '部署失败,请检查上方日志' + } + always { + // 仅清理悬空镜像,保留历史 BUILD_NUMBER tag + sh 'docker image prune -f || true' + } + } +} diff --git a/docs/superpowers/plans/2026-07-15-task-operation-refactor.md b/docs/superpowers/plans/2026-07-15-task-operation-refactor.md new file mode 100644 index 00000000..d2bf1e46 --- /dev/null +++ b/docs/superpowers/plans/2026-07-15-task-operation-refactor.md @@ -0,0 +1,697 @@ +# 搬运任务操作路由表重构 + PC 端操作接口 实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 消灭 ACS 反馈的 if-else 分发,改为 `TransportTaskServiceImpl` 内的枚举路由表;新增 PC 端统一操作接口(完成/取消/强制完成)并打通前端按钮。 + +**架构:** `TaskOperationTypeEnum` 增加 acsAllowed/pcAllowed 属性与 `getByCode` 解析;`TransportTaskServiceImpl` 持有 `EnumMap<操作类型, BiConsumer>` 路由表,`handle*` 业务方法原样迁入;`TransportTaskFeedbackServiceImpl` 瘦身为 ACS 前置处理层后委托分发;PC 入口 `POST /task/transport-task/operate` 走同一路由表,差异仅在前置校验(抛异常)与记录 finishedType。 + +**技术栈:** Java 17 / Spring Boot / MyBatis-Plus(后端);Vue 3 + Vben + antdv-next(前端) + +**规格:** `docs/superpowers/specs/2026-07-15-task-operation-refactor-design.md` + +**约定(来自 CLAUDE.md 与 nl-java-style):** 注释/日志用中文;注入用 `@Resource`;不写测试;不频繁 commit(最后统一一次,需用户同意);枚举解析方法命名 `getByCode`。 + +--- + +## 文件结构 + +| 文件 | 操作 | 职责 | +|---|---|---| +| `nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/TaskOperationTypeEnum.java` | 修改 | 操作类型枚举:增加入口允许标记与 getByCode 解析 | +| `nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/ErrorCodeConstants.java` | 修改 | 新增 PC 操作两个错误码 | +| `nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskOperateReqVO.java` | 创建 | PC 操作请求 VO | +| `nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java` | 修改 | 接口新增 operateTransportTask / dispatchOperation | +| `nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java` | 修改 | 路由表 + handle* 迁入 + PC 入口 | +| `nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskFeedbackServiceImpl.java` | 修改 | 瘦身为 ACS 前置处理层 | +| `nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/TransportTaskController.java` | 修改 | 新增 POST /operate | +| `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/task/transporttask/index.ts` | 修改 | 新增 operateTransportTask API | +| `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/task/transporttask/index.vue` | 修改 | 三个按钮绑定 handleOperate | + +--- + +### 任务 1:枚举增强 `TaskOperationTypeEnum` + +**文件:** +- 修改:`nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/TaskOperationTypeEnum.java` + +- [ ] **步骤 1:整体替换为以下内容** + +```java +package cn.code.nl.module.task.enums; + +import lombok.Getter; + +/** + * 任务操作类型枚举 + *

+ * acsAllowed:是否允许 ACS 反馈触发;pcAllowed:是否允许 PC 端触发 + * + * @Author: liyongde + * @Date: 2026/7/15 19:11 + */ +@Getter +public enum TaskOperationTypeEnum { + + EXECUTING("EXECUTING", "执行中", true, false), + + PICKED("PICKED", "取货完成", true, false), + + FINISHED("FINISHED", "完成任务", true, true), + + CANCELLED("CANCELLED", "取消任务", true, true), + + APPLY_AGAIN("APPLY-AGAIN", "二次请求", true, false), + + FORCE_FINISH("FORCE-FINISH", "强制完成", false, true); + + /** 操作编码 */ + private final String code; + /** 操作名称 */ + private final String name; + /** 是否允许 ACS 反馈触发 */ + private final boolean acsAllowed; + /** 是否允许 PC 端触发 */ + private final boolean pcAllowed; + + TaskOperationTypeEnum(String code, String name, boolean acsAllowed, boolean pcAllowed) { + this.code = code; + this.name = name; + this.acsAllowed = acsAllowed; + this.pcAllowed = pcAllowed; + } + + /** + * 根据编码解析枚举(忽略大小写),找不到返回 null + */ + public static TaskOperationTypeEnum getByCode(String code) { + for (TaskOperationTypeEnum type : values()) { + if (type.getCode().equalsIgnoreCase(code)) { + return type; + } + } + return null; + } +} +``` + +--- + +### 任务 2:新增错误码 + +**文件:** +- 修改:`nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/ErrorCodeConstants.java` + +- [ ] **步骤 1:在 `TRANSPORT_TASK_RUNNING_ALREADY_EXIST` 之后追加两行** + +```java + ErrorCode TRANSPORT_TASK_OPERATION_NOT_SUPPORTED = new ErrorCode(5006, "不支持的任务操作类型"); + ErrorCode TRANSPORT_TASK_ALREADY_FINAL = new ErrorCode(5007, "任务已处于终态,不允许操作"); +``` + +--- + +### 任务 3:新增 `TransportTaskOperateReqVO` + +**文件:** +- 创建:`nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskOperateReqVO.java` + +- [ ] **步骤 1:创建文件** + +```java +package cn.code.nl.module.task.controller.admin.transporttask.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * PC 端搬运任务操作 Request VO + * + * @Author: liyongde + * @Date: 2026/7/15 + */ +@Schema(description = "管理后台 - PC 端搬运任务操作 Request VO") +@Data +public class TransportTaskOperateReqVO { + + @Schema(description = "任务ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "任务ID不能为空") + private Long taskId; + + @Schema(description = "操作类型:FINISHED/CANCELLED/FORCE-FINISH", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "操作类型不能为空") + private String operationType; +} +``` + +--- + +### 任务 4:`TransportTaskService` 接口新增方法 + +**文件:** +- 修改:`nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java` + +- [ ] **步骤 1:新增 import** + +在现有 import 区追加(`vo.*` 通配已覆盖新 VO,无需单独引入): + +```java +import cn.code.nl.module.task.dto.AcsFeedbackReqDTO; +import cn.code.nl.module.task.enums.TaskOperationTypeEnum; +``` + +- [ ] **步骤 2:在 `getTransportTaskPage` 方法之后新增两个方法声明** + +```java + /** + * PC 端操作搬运任务(完成/取消/强制完成) + * + * @param reqVO 操作请求 + */ + void operateTransportTask(@Valid TransportTaskOperateReqVO reqVO); + + /** + * 按操作类型分发处理任务(ACS 反馈与 PC 端操作共用路由表) + * + * @param task 任务 + * @param type 操作类型 + * @param reqDTO 反馈请求(PC 端为构造的精简对象) + */ + void dispatchOperation(TransportTaskDO task, TaskOperationTypeEnum type, AcsFeedbackReqDTO reqDTO); +``` + +--- + +### 任务 5:`TransportTaskServiceImpl` 路由表 + handle* 迁入 + PC 入口 + +**文件:** +- 修改:`nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java` + +**说明:** `handleExecuting/handlePicked/handleFinished/handleCancelled/publishEvent/buildCallbackReq` 六个方法从 `TransportTaskFeedbackServiceImpl` **原样迁入**(方法体一字不改),本任务只新增代码,删除动作在任务 6。 + +- [ ] **步骤 1:补充 import** + +在现有 import 区追加: + +```java +import cn.code.nl.framework.execute.biz.api.TaskCommonApi; +import cn.code.nl.framework.execute.core.TaskCommonApiFactory; +import cn.code.nl.module.task.dto.AcsFeedbackReqDTO; +import cn.code.nl.module.task.dto.TaskStatusCallbackReqDTO; +import cn.code.nl.module.task.enums.TaskEventTypeEnum; +import cn.code.nl.module.task.enums.TaskOperationTypeEnum; +import cn.code.nl.module.task.mq.message.TaskEventMessage; +import cn.code.nl.module.task.mq.producer.TaskEventProducer; +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +import java.util.EnumMap; +import java.util.Map; +import java.util.function.BiConsumer; + +import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_ALREADY_FINAL; +import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_OPERATION_NOT_SUPPORTED; +import static cn.code.nl.module.task.framework.common.util.TaskUtil.isAllowedFrom; +``` + +注意:`TRANSPORT_TASK_NOT_EXISTS` 的静态导入已存在,不要重复。 + +- [ ] **步骤 2:类上补充 `@Slf4j` 注解** + +```java +@Slf4j +@Service +@Validated +public class TransportTaskServiceImpl implements TransportTaskService { +``` + +- [ ] **步骤 3:新增依赖字段与路由表字段(放在 `transportTaskMapper` 字段之后)** + +```java + @Resource + private TaskEventProducer taskEventProducer; + + @Resource + private TaskCommonApiFactory taskCommonApiFactory; + + /** + * 操作类型 -> 处理方法 路由表(ACS 反馈与 PC 端操作共用) + */ + private final Map> operationHandlers = + new EnumMap<>(TaskOperationTypeEnum.class); + + /** + * 初始化操作类型路由表 + */ + @PostConstruct + public void initOperationHandlers() { + operationHandlers.put(TaskOperationTypeEnum.EXECUTING, (task, reqDTO) -> handleExecuting(task)); + operationHandlers.put(TaskOperationTypeEnum.PICKED, this::handlePicked); + operationHandlers.put(TaskOperationTypeEnum.FINISHED, this::handleFinished); + operationHandlers.put(TaskOperationTypeEnum.CANCELLED, this::handleCancelled); + // todo: 二次请求业务未开发 + operationHandlers.put(TaskOperationTypeEnum.APPLY_AGAIN, + (task, reqDTO) -> log.info("二次请求业务未开发, taskId={}", task.getTaskId())); + // todo: 强制完成业务未开发 + operationHandlers.put(TaskOperationTypeEnum.FORCE_FINISH, + (task, reqDTO) -> log.info("强制完成业务未开发, taskId={}", task.getTaskId())); + } +``` + +- [ ] **步骤 4:新增 PC 入口与分发方法(放在 `getTransportTaskPage` 方法之后)** + +```java + /** + * PC 端操作搬运任务(完成/取消/强制完成):先校验再走统一路由表分发 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void operateTransportTask(TransportTaskOperateReqVO reqVO) { + // 1. 校验任务存在 + TransportTaskDO task = transportTaskMapper.selectById(reqVO.getTaskId()); + if (task == null) { + throw exception(TRANSPORT_TASK_NOT_EXISTS); + } + // 2. 校验操作类型允许 PC 端触发 + TaskOperationTypeEnum type = TaskOperationTypeEnum.getByCode(reqVO.getOperationType()); + if (type == null || !type.isPcAllowed()) { + throw exception(TRANSPORT_TASK_OPERATION_NOT_SUPPORTED); + } + // 3. 终态校验:已完成/已取消不允许再操作 + if (TransportTaskStatusEnum.FINISHED.getCode().equals(task.getTaskStatus()) + || TransportTaskStatusEnum.CANCELLED.getCode().equals(task.getTaskStatus())) { + throw exception(TRANSPORT_TASK_ALREADY_FINAL); + } + // 4. PC 端与 ACS 的差异点:记录完成类型 + if (type == TaskOperationTypeEnum.FINISHED) { + task.setFinishedType("MANUAL"); + } else if (type == TaskOperationTypeEnum.FORCE_FINISH) { + task.setFinishedType("FORCE"); + } + // 5. 构造精简反馈对象,走统一路由表分发 + AcsFeedbackReqDTO reqDTO = new AcsFeedbackReqDTO(); + reqDTO.setTaskId(task.getTaskId()); + reqDTO.setStatus(type.getCode()); + dispatchOperation(task, type, reqDTO); + } + + /** + * 按操作类型查路由表分发(消灭 if-else) + */ + @Override + public void dispatchOperation(TransportTaskDO task, TaskOperationTypeEnum type, AcsFeedbackReqDTO reqDTO) { + BiConsumer handler = operationHandlers.get(type); + if (handler == null) { + log.warn("操作类型未注册处理器, taskId={}, type={}", task.getTaskId(), type.getCode()); + return; + } + handler.accept(task, reqDTO); + } +``` + +- [ ] **步骤 5:迁入六个业务方法(放在类末尾,方法体与原 `TransportTaskFeedbackServiceImpl` 完全一致)** + +```java + // ==================== 各状态处理方法(自 TransportTaskFeedbackServiceImpl 迁入,逻辑未改动) ==================== + + /** + * 处理执行中(60):直接更新状态,不做业务回调 + */ + private void handleExecuting(TransportTaskDO task) { + if (!isAllowedFrom(task.getTaskStatus(), "40", "50")) { + log.warn("执行中反馈状态校验不通过, taskId={}, currentStatus={}", + task.getTaskId(), task.getTaskStatus()); + return; + } + task.setTaskStatus(TransportTaskStatusEnum.EXECUTING.getCode()); + transportTaskMapper.updateById(task); + log.info("任务执行中, taskId={}", task.getTaskId()); + } + + /** + * 处理取货完成(61):更新状态 + 同步 HTTP 回调 LMS/WMS + * ownerService: 服务提供商 + */ + private void handlePicked(TransportTaskDO task, AcsFeedbackReqDTO reqDTO) { + if (!isAllowedFrom(task.getTaskStatus(), "60", "61")) { + log.warn("取货完成反馈状态校验不通过, taskId={}, currentStatus={}", + task.getTaskId(), task.getTaskStatus()); + return; + } + // 更新状态 + task.setTaskStatus(TransportTaskStatusEnum.PICKED.getCode()); + transportTaskMapper.updateById(task); + log.info("任务已取货, taskId={}", task.getTaskId()); + + // 同步请求业务 + TaskCommonApi byServerName = taskCommonApiFactory.getByServerName("lms-server"); + byServerName.doHandlePicked(); + } + + /** + * 处理完成(67):更新状态 + 异步 MQ 通知 LMS/WMS + */ + private void handleFinished(TransportTaskDO task, AcsFeedbackReqDTO reqDTO) { + if (!isAllowedFrom(task.getTaskStatus(), "10", "40", "45", "50", "60", "61", "67")) { + log.warn("完成反馈状态校验不通过, taskId={}, currentStatus={}", + task.getTaskId(), task.getTaskStatus()); + return; + } + // 已是 67 则只发 MQ,不改状态 + if (!TransportTaskStatusEnum.FINISHED_CALLBACK_PENDING.getCode().equals(task.getTaskStatus())) { + task.setTaskStatus(TransportTaskStatusEnum.FINISHED_CALLBACK_PENDING.getCode()); + } + task.setCallbackStatus(CallbackStatusEnum.PENDING.getCode()); + transportTaskMapper.updateById(task); + + publishEvent(task, TaskEventTypeEnum.TASK_FINISHED, reqDTO.getPayload()); + } + + /** + * 处理取消(69):更新状态 + 异步 MQ 通知 LMS/WMS + */ + private void handleCancelled(TransportTaskDO task, AcsFeedbackReqDTO reqDTO) { + if (!isAllowedFrom(task.getTaskStatus(), "10", "40", "45", "50", "60", "61", "67", "69")) { + log.warn("取消反馈状态校验不通过, taskId={}, currentStatus={}", + task.getTaskId(), task.getTaskStatus()); + return; + } + if (!TransportTaskStatusEnum.CANCEL_CALLBACK_PENDING.getCode().equals(task.getTaskStatus())) { + task.setTaskStatus(TransportTaskStatusEnum.CANCEL_CALLBACK_PENDING.getCode()); + } + task.setCallbackStatus(CallbackStatusEnum.PENDING.getCode()); + transportTaskMapper.updateById(task); + + publishEvent(task, TaskEventTypeEnum.TASK_CANCELLED, reqDTO.getPayload()); + } + + // ==================== 工具方法 ==================== + + /** + * 发布 MQ 事件。发送失败时记录 callbackStatus=FAILED + */ + private void publishEvent(TransportTaskDO task, TaskEventTypeEnum eventType, + Map payload) { + TaskEventMessage msg = new TaskEventMessage(); + msg.setEventType(eventType.getCode()); + msg.setTaskId(task.getTaskId()); + msg.setTaskCode(task.getTaskCode()); + msg.setOwnerService(task.getOwnerService()); + msg.setBizType(task.getBizType()); + msg.setBizId(task.getBizId()); + msg.setHandleCode(task.getHandleCode()); + msg.setPayload(payload); + + try { + taskEventProducer.publishEvent(msg); + } catch (Exception e) { + log.error("MQ 发布失败, taskId={}, eventType={}", + task.getTaskId(), eventType.getCode(), e); + task.setCallbackStatus(CallbackStatusEnum.FAILED.getCode()); + task.setCallbackErrorMsg(StrUtil.maxLength(e.getMessage(), 500)); + transportTaskMapper.updateById(task); + } + } + + /** + * 构建同步回调请求 DTO(当前无调用方,随迁保留) + */ + private TaskStatusCallbackReqDTO buildCallbackReq(TransportTaskDO task, + AcsFeedbackReqDTO reqDTO) { + TaskStatusCallbackReqDTO req = new TaskStatusCallbackReqDTO(); + req.setTaskId(task.getTaskId()); + req.setTaskCode(task.getTaskCode()); + req.setStatus(TransportTaskStatusEnum.PICKED.getCode()); + req.setOwnerService(task.getOwnerService()); + req.setBizType(task.getBizType()); + req.setBizId(task.getBizId()); + req.setHandleCode(task.getHandleCode()); + req.setPayload(reqDTO.getPayload()); + return req; + } +``` + +--- + +### 任务 6:`TransportTaskFeedbackServiceImpl` 瘦身 + +**文件:** +- 修改:`nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskFeedbackServiceImpl.java` + +- [ ] **步骤 1:整体替换为以下内容** + +```java +package cn.code.nl.module.task.service.transporttask; + +import cn.code.nl.module.task.dal.dataobject.transporttask.TransportTaskDO; +import cn.code.nl.module.task.dal.mysql.transporttask.TransportTaskMapper; +import cn.code.nl.module.task.dto.AcsFeedbackReqDTO; +import cn.code.nl.module.task.enums.TaskOperationTypeEnum; +import cn.code.nl.module.task.enums.TransportTaskStatusEnum; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import jakarta.annotation.Resource; + +/** + * ACS 反馈处理服务实现 + *

+ * 只做 ACS 前置处理(查任务/幂等/保存反馈参数),具体操作业务统一由 {@link TransportTaskService} 路由表分发 + * + * @author 诺力管理员 + */ +@Slf4j +@Service +@Validated +public class TransportTaskFeedbackServiceImpl implements TransportTaskFeedbackService { + + @Resource + private TransportTaskMapper transportTaskMapper; + + @Resource + private TransportTaskService transportTaskService; + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Override + @Transactional(rollbackFor = Exception.class) + public void receiveAcsFeedback(AcsFeedbackReqDTO reqDTO) { + // 1. 查询任务 + TransportTaskDO task = transportTaskMapper.selectById(reqDTO.getTaskId()); + if (task == null) { + log.warn("ACS 反馈 taskId 不存在, taskId={}, 直接返回成功", reqDTO.getTaskId()); + return; + } + + // 2. 幂等判断:已经是终态(完成/取消)则不重复处理 + String currentStatus = task.getTaskStatus(); + if (TransportTaskStatusEnum.FINISHED.getCode().equals(currentStatus) + || TransportTaskStatusEnum.CANCELLED.getCode().equals(currentStatus)) { + log.info("任务已处终态, taskId={}, currentStatus={}, 忽略反馈", + reqDTO.getTaskId(), currentStatus); + return; + } + + // 3. 保存 resultParam + if (reqDTO.getPayload() != null) { + try { + task.setResultParam(OBJECT_MAPPER.writeValueAsString(reqDTO.getPayload())); + } catch (Exception e) { + log.warn("resultParam 序列化失败, taskId={}", reqDTO.getTaskId(), e); + } + } + + // 4. 解析操作类型并校验允许 ACS 触发,走统一路由表分发 + TaskOperationTypeEnum type = TaskOperationTypeEnum.getByCode(reqDTO.getStatus()); + if (type == null || !type.isAcsAllowed()) { + log.warn("未知或不允许的 ACS 反馈状态, taskId={}, status={}", + reqDTO.getTaskId(), reqDTO.getStatus()); + return; + } + transportTaskService.dispatchOperation(task, type, reqDTO); + } +} +``` + +注意:原类中 `handle*`、`publishEvent`、`buildCallbackReq` 及其相关 import(TaskCommonApi、TaskCommonApiFactory、TaskEventMessage、TaskStatusCallbackReqDTO、CallbackStatusEnum、TaskEventTypeEnum、TaskEventProducer、StrUtil、isAllowedFrom、Map)随整体替换一并移除,已在任务 5 迁入 `TransportTaskServiceImpl`。 + +--- + +### 任务 7:`TransportTaskController` 新增 operate 接口 + +**文件:** +- 修改:`nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/TransportTaskController.java` + +- [ ] **步骤 1:在 `updateTransportTask` 方法之后新增(`vo.*` 通配 import 已覆盖新 VO)** + +```java + @PostMapping("/operate") + @Operation(summary = "PC 端操作搬运任务(完成/取消/强制完成)") + @PreAuthorize("@ss.hasPermission('task:transport-task:operate')") + public CommonResult operateTransportTask(@Valid @RequestBody TransportTaskOperateReqVO reqVO) { + transportTaskService.operateTransportTask(reqVO); + return success(true); + } +``` + +--- + +### 任务 8:后端编译验证 + +- [ ] **步骤 1:编译 task 模块** + +运行(仓库根目录): + +```bash +mvn -f nl-module-task/pom.xml compile -q +``` + +预期:BUILD SUCCESS,无编译错误。若因兄弟模块依赖未安装到本地仓库而失败,改用: + +```bash +mvn -pl nl-module-task -am compile -q +``` + +--- + +### 任务 9:前端 API 新增 `operateTransportTask` + +**文件:** +- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/task/transporttask/index.ts` + +- [ ] **步骤 1:在 `exportTransportTask` 之后新增** + +```ts +/** PC 端操作搬运任务(完成/取消/强制完成) */ +export function operateTransportTask(data: { + operationType: string; + taskId: number | string; +}) { + return requestClient.post('/task/transport-task/operate', data); +} +``` + +--- + +### 任务 10:`index.vue` 三个按钮绑定操作类型 + +**文件:** +- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/task/transporttask/index.vue` + +- [ ] **步骤 1:import 块补充 `operateTransportTask`** + +```ts +import { + deleteTransportTask, + deleteTransportTaskList, + exportTransportTask, + getTransportTaskPage, + operateTransportTask, +} from '#/api/task/transporttask'; +``` + +- [ ] **步骤 2:在 `handleDelete` 之前新增操作函数** + +```ts +/** PC 端操作搬运任务(完成/取消/强制完成) */ +async function handleOperate( + row: TaskTransportTaskApi.TransportTask, + operationType: string, + label: string, +) { + await confirm(`确认要${label}【${row.taskCode}】吗?`); + const hideLoading = message.loading({ + content: `正在${label}...`, + duration: 0, + }); + try { + await operateTransportTask({ taskId: row.taskId!, operationType }); + message.success(`${label}成功`); + handleRefresh(); + } finally { + hideLoading(); + } +} +``` + +- [ ] **步骤 3:替换三个按钮的 `onClick`(原来错绑 `handleEdit.bind(null, row)`)** + +```ts + { + label: '完成任务', + type: 'link', + icon: ACTION_ICON.SEND, + auth: ['task:transport-task:finish'], + disabled: ['79', '89', '99'].includes(row.taskStatus), + onClick: handleOperate.bind(null, row, 'FINISHED', '完成任务'), + }, + { + label: '取消任务', + type: 'link', + icon: ACTION_ICON.CANCEL, + auth: ['task:transport-task:cancel'], + disabled: ['79', '89', '99'].includes(row.taskStatus), + onClick: handleOperate.bind(null, row, 'CANCELLED', '取消任务'), + }, + { + label: '强制完成任务', + type: 'link', + icon: ACTION_ICON.CANCEL, + auth: ['task:transport-task:cancel'], + disabled: ['79', '89', '99'].includes(row.taskStatus), + onClick: handleOperate.bind(null, row, 'FORCE-FINISH', '强制完成任务'), + }, +``` + +说明:`auth`/`icon` 维持现状(强制完成的独立权限属规格范围外)。 + +--- + +### 任务 11:前端验证 + +- [ ] **步骤 1:类型检查(可选,monorepo 全量较慢)** + +运行(`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next` 目录): + +```bash +pnpm typecheck +``` + +预期:本次改动的两个文件无新增类型错误(仓库既有错误不属本次范围)。 + +--- + +### 任务 12:统一提交(可选,需用户同意) + +- [ ] **步骤 1:确认用户同意后提交** + +```bash +git add docs/superpowers/specs/2026-07-15-task-operation-refactor-design.md \ + docs/superpowers/plans/2026-07-15-task-operation-refactor.md \ + nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/TaskOperationTypeEnum.java \ + nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/ErrorCodeConstants.java \ + nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskOperateReqVO.java \ + nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/TransportTaskController.java \ + nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java \ + nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java \ + nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskFeedbackServiceImpl.java \ + nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/task/transporttask/index.ts \ + nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/task/transporttask/index.vue +git commit -m "feat:任务操作路由表重构 + PC 端完成/取消/强制完成操作" +``` + +--- + +## 风险与注意事项 + +1. **循环依赖**:依赖方向为 `FeedbackServiceImpl → TransportTaskService` 单向,`TransportTaskServiceImpl` 不得反向注入 `TransportTaskFeedbackService`。 +2. **事务**:`receiveAcsFeedback` 的 `@Transactional` 经跨 Bean 调用按 REQUIRED 传播覆盖 `dispatchOperation`;PC 入口自带 `@Transactional`。 +3. **FORCE-FINISH 的 finishedType**:PC 入口会 set 但 handler 为 todo 不落库,属规格已确认的预期行为。 +4. **工作区有未提交改动**:本计划涉及的部分文件已有本地修改(见 git status),实现时在现有基础上叠加,不要回退已有改动。 diff --git a/docs/superpowers/plans/2026-07-16-jenkinsfile-monolith-deploy.md b/docs/superpowers/plans/2026-07-16-jenkinsfile-monolith-deploy.md new file mode 100644 index 00000000..6a14d439 --- /dev/null +++ b/docs/superpowers/plans/2026-07-16-jenkinsfile-monolith-deploy.md @@ -0,0 +1,170 @@ +# Jenkinsfile 单体部署 nl-server 实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 在仓库根目录新增 `Jenkinsfile`,以 Docker 容器形式在 Jenkins 构建机本机部署 `nl-server` 单体。 + +**架构:** 声明式 Pipeline:参数化(PROFILE/NACOS_ADDR/NACOS_NAMESPACE/JAVA_OPTS)→ Checkout → `mvn package -pl nl-server -am -DskipTests` → 复用 `nl-server/Dockerfile` 构建镜像(BUILD_NUMBER + latest 双 tag)→ 停删旧容器后 host 网络 `docker run` → curl 循环健康检查;post 清理悬空镜像。 + +**技术栈:** Jenkins 声明式 Pipeline(Groovy)/ Maven / Docker(Linux 构建机) + +**规格:** `docs/superpowers/specs/2026-07-16-jenkinsfile-monolith-deploy-design.md` + +--- + +## 文件结构 + +| 文件 | 操作 | 职责 | +|---|---|---| +| `Jenkinsfile`(仓库根目录) | 创建 | 单体部署流水线全部逻辑 | + +--- + +### 任务 1:创建根目录 `Jenkinsfile` + +**文件:** +- 创建:`Jenkinsfile` + +- [ ] **步骤 1:创建文件,内容如下** + +```groovy +// nl-server 单体部署流水线(构建机 = 运行机,Linux + JDK17+ + Maven + Docker) +pipeline { + agent any + + parameters { + choice(name: 'PROFILE', choices: ['test', 'dev', 'prod'], description: '运行环境(注入 SPRING_PROFILES_ACTIVE)') + string(name: 'NACOS_ADDR', defaultValue: '127.0.0.1:8848', description: 'Nacos 配置/注册中心地址') + string(name: 'NACOS_NAMESPACE', defaultValue: '', description: 'Nacos 命名空间(public 留空)') + string(name: 'JAVA_OPTS', defaultValue: '-Xms512m -Xmx512m -Djava.security.egd=file:/dev/./urandom', description: 'JVM 启动参数') + } + + environment { + IMAGE_NAME = 'nl-server' + CONTAINER_NAME = 'nl-server' + APP_PORT = '48080' + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Maven Build') { + steps { + // 只构建 nl-server 及其依赖模块,跳过测试 + sh 'mvn clean package -pl nl-server -am -DskipTests' + } + } + + stage('Docker Build') { + steps { + // 构建上下文为 nl-server 目录,复用其现有 Dockerfile + sh "docker build -t ${IMAGE_NAME}:${BUILD_NUMBER} -t ${IMAGE_NAME}:latest nl-server/" + } + } + + stage('Deploy') { + steps { + // 停删旧容器(首次部署无旧容器,容错处理),host 网络启动新容器 + sh """ + docker stop ${CONTAINER_NAME} || true + docker rm ${CONTAINER_NAME} || true + docker run -d --name ${CONTAINER_NAME} \\ + --restart always \\ + --network host \\ + -v /docker/nl-cloud/logs:/root/logs/ \\ + -e TZ=Asia/Shanghai \\ + -e SPRING_PROFILES_ACTIVE=${params.PROFILE} \\ + -e SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR=${params.NACOS_ADDR} \\ + -e SPRING_CLOUD_NACOS_SERVER_ADDR=${params.NACOS_ADDR} \\ + -e SPRING_CLOUD_NACOS_CONFIG_NAMESPACE=${params.NACOS_NAMESPACE} \\ + -e SPRING_CLOUD_NACOS_DISCOVERY_NAMESPACE=${params.NACOS_NAMESPACE} \\ + -e "JAVA_OPTS=${params.JAVA_OPTS}" \\ + ${IMAGE_NAME}:${BUILD_NUMBER} + """ + } + } + + stage('Health Check') { + steps { + // 最多等待 24 次 x 5s ≈ 120s,收到任意 HTTP 响应(含 404)即视为启动成功 + sh """ + echo "等待 nl-server 启动..." + for i in \$(seq 1 24); do + if curl -s -o /dev/null http://127.0.0.1:${APP_PORT}; then + echo "nl-server 启动成功" + exit 0 + fi + sleep 5 + done + echo "健康检查超时,输出容器日志:" + docker logs --tail 200 ${CONTAINER_NAME} + exit 1 + """ + } + } + } + + post { + success { + echo "部署成功:${IMAGE_NAME}:${BUILD_NUMBER},服务端口 ${APP_PORT}(历史镜像 tag 保留,可用于手动回滚)" + } + failure { + echo '部署失败,请检查上方日志' + } + always { + // 仅清理悬空镜像,保留历史 BUILD_NUMBER tag + sh 'docker image prune -f || true' + } + } +} +``` + +--- + +### 任务 2:静态验证 + +本地无 Jenkins/Groovy 环境,做结构性静态检查: + +- [ ] **步骤 1:花括号配对计数** + +运行(仓库根目录): + +```bash +grep -o '{' Jenkinsfile | wc -l && grep -o '}' Jenkinsfile | wc -l +``` + +预期:两个数字相等(60 上下,以实际为准)。 + +- [ ] **步骤 2:结构核对清单(人工比对文件内容)** + +- [ ] `pipeline { agent any; parameters; environment; stages; post }` 五段齐全 +- [ ] 4 个参数名与规格一致:PROFILE / NACOS_ADDR / NACOS_NAMESPACE / JAVA_OPTS +- [ ] 5 个 stage:Checkout / Maven Build / Docker Build / Deploy / Health Check +- [ ] Deploy 中 6 个 `-e` 环境变量 + TZ + 日志卷挂载 + `--network host` + `--restart always` +- [ ] shell 中 `$(seq ...)` 已转义为 `\$(seq ...)`(避免被 Groovy 插值) +- [ ] `JAVA_OPTS` 注入带引号(值含空格) + +--- + +### 任务 3:统一提交(可选,需用户同意) + +- [ ] **步骤 1:确认用户同意后提交** + +```bash +git add Jenkinsfile \ + docs/superpowers/specs/2026-07-16-jenkinsfile-monolith-deploy-design.md \ + docs/superpowers/plans/2026-07-16-jenkinsfile-monolith-deploy.md +git commit -m "build:新增 Jenkinsfile 单体部署 nl-server" +``` + +--- + +## 风险与注意事项 + +1. **构建机前提**:Linux、JDK 17+、Maven、Docker、curl 已装且 Jenkins 用户有 docker 权限(在 docker 用户组)。 +2. **真实验证需在 Jenkins 执行**:建 Pipeline 任务指向本仓库运行;首次运行参数页尚未生成,会用默认参数跑一次。 +3. **Nacos 前置**:nl-server 为 Spring Cloud 应用,启动依赖 Nacos 可达;若 Nacos 未就绪,健康检查会超时失败并打印容器日志。 diff --git a/docs/superpowers/plans/2026-07-16-transport-task-api-query.md b/docs/superpowers/plans/2026-07-16-transport-task-api-query.md new file mode 100644 index 00000000..4a366660 --- /dev/null +++ b/docs/superpowers/plans/2026-07-16-transport-task-api-query.md @@ -0,0 +1,252 @@ +# TransportTaskApi RPC 查询接口 实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 补全 `TransportTaskApi` 两个 todo——根据 taskId / taskCode 查询任务,返回 `TaskInfoDTO`,自动过滤逻辑删除。 + +**架构:** Api(Feign GET)→ ApiImpl(委托)→ TransportTaskService(查询 + MapStruct 转换,查不到返回 null)→ TransportTaskMapper(selectById / selectByTaskCode,`@TableLogic` 自动滤删)。新建 `TransportTaskConvert`(MapStruct,字段同名零配置)。 + +**技术栈:** Java 17 / Spring Boot / MyBatis-Plus(BaseMapperX)/ MapStruct 1.6.3(经 nl-common 传递依赖,无需改 pom) + +**规格:** `docs/superpowers/specs/2026-07-16-transport-task-api-query-design.md` + +**约定:** 注释/日志中文;`@Resource` 注入;不写测试;不频繁 commit(最后统一一次,需用户同意)。 + +--- + +## 文件结构 + +| 文件 | 操作 | 职责 | +|---|---|---| +| `nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/api/TransportTaskApi.java` | 修改 | 新增两个 Feign GET 查询方法,删除两行 todo | +| `nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/convert/transporttask/TransportTaskConvert.java` | 创建 | DO→TaskInfoDTO MapStruct 转换 | +| `nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/mysql/transporttask/TransportTaskMapper.java` | 修改 | 新增 selectByTaskCode | +| `nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java` | 修改 | 接口新增 getTaskInfoById / getTaskInfoByCode | +| `nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java` | 修改 | 实现两个查询方法 | +| `nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/api/TransportTaskApiImpl.java` | 修改 | 实现两个 Api 方法(委托 service) | + +--- + +### 任务 1:`TransportTaskApi` 新增两个查询方法 + +**文件:** +- 修改:`nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/api/TransportTaskApi.java` + +- [ ] **步骤 1:补充 import** + +在现有 import 区追加: + +```java +import cn.code.nl.module.task.dto.TaskInfoDTO; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +``` + +- [ ] **步骤 2:删除两行 todo,替换为两个方法声明** + +将文件末尾的: + +```java + // todo 根据taskId查找任务(返回TaskInfoDTO、过滤删除) + + // todo 根据taskCode获取任务(返回TaskInfoDTO、过滤删除) +``` + +替换为: + +```java + @GetMapping(PREFIX + "/getTaskById") + @Operation(summary = "根据 taskId 查询任务(LMS/WMS 调用)") + CommonResult getTaskById(@RequestParam("taskId") Long taskId); + + @GetMapping(PREFIX + "/getTaskByCode") + @Operation(summary = "根据 taskCode 查询任务(LMS/WMS 调用)") + CommonResult getTaskByCode(@RequestParam("taskCode") String taskCode); +``` + +--- + +### 任务 2:新建 `TransportTaskConvert` + +**文件:** +- 创建:`nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/convert/transporttask/TransportTaskConvert.java` + +- [ ] **步骤 1:创建文件** + +```java +package cn.code.nl.module.task.convert.transporttask; + +import cn.code.nl.module.task.dal.dataobject.transporttask.TransportTaskDO; +import cn.code.nl.module.task.dto.TaskInfoDTO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 搬运任务 Convert + * + * @Author: liyongde + * @Date: 2026/7/16 + */ +@Mapper +public interface TransportTaskConvert { + + TransportTaskConvert INSTANCE = Mappers.getMapper(TransportTaskConvert.class); + + /** + * DO 转 RPC 全量信息 DTO(字段同名,零配置映射) + */ + TaskInfoDTO convert(TransportTaskDO bean); +} +``` + +--- + +### 任务 3:`TransportTaskMapper` 新增 `selectByTaskCode` + +**文件:** +- 修改:`nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/mysql/transporttask/TransportTaskMapper.java` + +- [ ] **步骤 1:在 `selectUnfinishedByBiz` 方法之前新增** + +```java + /** + * 根据任务编码查询任务(taskCode 业务唯一) + */ + default TransportTaskDO selectByTaskCode(String taskCode) { + return selectOne(TransportTaskDO::getTaskCode, taskCode); + } +``` + +说明:`selectOne(SFunction, Object)` 为 `BaseMapperX` 现有重载(BaseMapperX.java:105),`@TableLogic` 自动过滤 `deleted=1`。 + +--- + +### 任务 4:`TransportTaskService` 接口与实现新增查询方法 + +**文件:** +- 修改:`nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java` +- 修改:`nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java` + +- [ ] **步骤 1:接口补充 import 并新增两个声明** + +import 区追加(`AcsFeedbackReqDTO` 等已存在,勿重复): + +```java +import cn.code.nl.module.task.dto.TaskInfoDTO; +``` + +在 `dispatchOperation` 声明之后新增: + +```java + /** + * 根据 taskId 查询任务全量信息(RPC 用,自动过滤逻辑删除,查不到返回 null) + * + * @param taskId 任务ID + * @return 任务全量信息 + */ + TaskInfoDTO getTaskInfoById(Long taskId); + + /** + * 根据 taskCode 查询任务全量信息(RPC 用,自动过滤逻辑删除,查不到返回 null) + * + * @param taskCode 任务编码 + * @return 任务全量信息 + */ + TaskInfoDTO getTaskInfoByCode(String taskCode); +``` + +- [ ] **步骤 2:实现类补充 import 并新增两个实现(放在 `dispatchOperation` 方法之后)** + +import 区追加: + +```java +import cn.code.nl.module.task.convert.transporttask.TransportTaskConvert; +import cn.code.nl.module.task.dto.TaskInfoDTO; +``` + +新增实现: + +```java + /** + * 根据 taskId 查询任务全量信息:查不到返回 null,由调用方判断 + */ + @Override + public TaskInfoDTO getTaskInfoById(Long taskId) { + TransportTaskDO task = transportTaskMapper.selectById(taskId); + return task != null ? TransportTaskConvert.INSTANCE.convert(task) : null; + } + + /** + * 根据 taskCode 查询任务全量信息:查不到返回 null,由调用方判断 + */ + @Override + public TaskInfoDTO getTaskInfoByCode(String taskCode) { + TransportTaskDO task = transportTaskMapper.selectByTaskCode(taskCode); + return task != null ? TransportTaskConvert.INSTANCE.convert(task) : null; + } +``` + +--- + +### 任务 5:`TransportTaskApiImpl` 实现两个方法 + +**文件:** +- 修改:`nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/api/TransportTaskApiImpl.java` + +- [ ] **步骤 1:补充 import** + +```java +import cn.code.nl.module.task.dto.TaskInfoDTO; +``` + +- [ ] **步骤 2:在 `receiveCallbackResult` 方法之后新增** + +```java + @Override + public CommonResult getTaskById(Long taskId) { + return success(transportTaskService.getTaskInfoById(taskId)); + } + + @Override + public CommonResult getTaskByCode(String taskCode) { + return success(transportTaskService.getTaskInfoByCode(taskCode)); + } +``` + +--- + +### 任务 6:编译验证 + +- [ ] **步骤 1:编译(仓库根目录)** + +```bash +mvn -pl nl-module-task/nl-module-task-server -am compile +``` + +预期:`BUILD SUCCESS`(同时验证 MapStruct 注解处理器生成 `TransportTaskConvertImpl` 无报错)。 + +--- + +### 任务 7:统一提交(可选,需用户同意) + +- [ ] **步骤 1:确认用户同意后提交(仅本次相关文件)** + +```bash +git add docs/superpowers/specs/2026-07-16-transport-task-api-query-design.md \ + docs/superpowers/plans/2026-07-16-transport-task-api-query.md \ + nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/api/TransportTaskApi.java \ + nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/api/TransportTaskApiImpl.java \ + nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/convert/transporttask/TransportTaskConvert.java \ + nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/mysql/transporttask/TransportTaskMapper.java \ + nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java \ + nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java +git commit -m "feat:TransportTaskApi 新增按 taskId/taskCode 查询任务 RPC 接口" +``` + +--- + +## 风险与注意事项 + +1. **工作区有未提交改动**:`TransportTaskService(Impl)`、`TransportTaskMapper` 均已有本地改动(含上一需求的路由表代码),实现时在现有基础上叠加,不要回退。 +2. **MapStruct 与 Lombok 顺序**:模块继承的编译配置已被 infra 等模块的 Convert 验证可用,若编译报 MapStruct 处理器缺失,检查是否误改了 pom(本计划不改 pom)。 +3. **`TransportTaskServiceImplTest`**:test 目录引用了 `TransportTaskApi`,`compile` 阶段不编译测试代码,不受影响。 diff --git a/docs/superpowers/specs/2026-07-15-task-operation-refactor-design.md b/docs/superpowers/specs/2026-07-15-task-operation-refactor-design.md new file mode 100644 index 00000000..19f70faa --- /dev/null +++ b/docs/superpowers/specs/2026-07-15-task-operation-refactor-design.md @@ -0,0 +1,139 @@ +# 搬运任务操作重构 + PC 端操作功能设计 + +- 日期:2026-07-15 +- 分支:feature/20260713/task-module +- 状态:已评审通过(方案 A:单类路由表) + +## 1. 背景与目标 + +任务状态操作目前存在两个入口: + +- **ACS 入口**:`AcsFeedbackController` → `TransportTaskFeedbackServiceImpl.receiveAcsFeedback()`,用字符串 if-else 按 `status` 分发到 `handleExecuting/handlePicked/handleFinished/handleCancelled`,代码凌乱。 +- **PC 入口**:尚不存在。前端 `views/task/transporttask/index.vue` 已有"完成任务/取消任务/强制完成任务"三个按钮,但都错绑在 `handleEdit` 上,未传操作类型。 + +目标: + +1. 消灭 if-else 分发,改为表驱动策略(路由表)。 +2. 操作业务代码统一集中到 `TransportTaskServiceImpl`;`handleExecuting/handlePicked/handleFinished/handleCancelled/publishEvent` 方法体**原样迁移**,不改业务逻辑。 +3. 新增 PC 端统一操作接口 `POST /task/transport-task/operate`(taskId + operationType),支持 FINISHED / CANCELLED / FORCE-FINISH。 +4. `APPLY-AGAIN`(二次请求)、`FORCE-FINISH`(强制完成)业务未开发,路由表中注册 todo 占位(仅打日志)。 +5. 前端按钮补齐操作类型标识与调用逻辑。 + +## 2. 操作类型与入口矩阵 + +| 操作类型 | code | ACS 触发 | PC 触发 | 业务 | +|---|---|---|---|---| +| EXECUTING | EXECUTING | ✅ | ❌ | handleExecuting(已有) | +| PICKED | PICKED | ✅ | ❌ | handlePicked(已有) | +| FINISHED | FINISHED | ✅ | ✅ | handleFinished(已有) | +| CANCELLED | CANCELLED | ✅ | ✅ | handleCancelled(已有) | +| APPLY_AGAIN | APPLY-AGAIN | ✅ | ❌ | todo,未开发 | +| FORCE_FINISH | FORCE-FINISH | ❌ | ✅ | todo,未开发 | + +## 3. 后端设计 + +### 3.1 枚举增强 `TaskOperationTypeEnum`(task-api 模块) + +- 增加两个布尔属性:`acsAllowed`(允许 ACS 触发)、`pcAllowed`(允许 PC 触发),取值见上表。 +- 增加静态解析方法 `getByCode(String code)`:忽略大小写匹配 `code`,找不到返回 `null`。 + +### 3.2 `TransportTaskServiceImpl`:路由表 + PC 入口 + +- 从 `TransportTaskFeedbackServiceImpl` **原样迁入**:`handleExecuting`、`handlePicked`、`handleFinished`、`handleCancelled`、`publishEvent`、`buildCallbackReq`(当前无调用方,随迁保留);依赖 `TaskEventProducer`、`TaskCommonApiFactory`、`OBJECT_MAPPER` 相关部分随迁。 +- 路由表:`EnumMap>`,在 `@PostConstruct` 中注册六种类型;`APPLY_AGAIN`、`FORCE_FINISH` 注册为 todo 占位(打 log.info,不做业务)。 +- 新增接口方法 `dispatchOperation(TransportTaskDO task, TaskOperationTypeEnum type, AcsFeedbackReqDTO reqDTO)`:查路由表分发,查不到打 warn 日志。 +- 新增 PC 入口 `operateTransportTask(TransportTaskOperateReqVO reqVO)`,`@Transactional(rollbackFor = Exception.class)`: + 1. 查任务,不存在 → 抛 `TRANSPORT_TASK_NOT_EXISTS`。 + 2. `getByCode` 解析 operationType,为 `null` 或 `pcAllowed=false` → 抛新错误码 `TRANSPORT_TASK_OPERATION_NOT_SUPPORTED`。 + 3. 任务已终态(79 完成 / 89 取消)→ 抛新错误码 `TRANSPORT_TASK_ALREADY_FINAL`(PC 端需给用户明确反馈,区别于 ACS 入口的静默幂等返回)。 + 4. **PC 与 ACS 的差异点——记录完成类型/来源**:`FINISHED` → `task.setFinishedType("MANUAL")`(人工完成);`FORCE_FINISH` → `task.setFinishedType("FORCE")`(强制完成);`CANCELLED` 不设置。注意:`FORCE_FINISH` 的业务 handler 当前为 todo 占位、不执行 `updateById`,因此该 finishedType 设置在业务开发完成前不会实际落库,属预期行为。 + 5. 构造仅含 `taskId`、`status` 的 `AcsFeedbackReqDTO`,调用 `dispatchOperation` 走同一路由表(保持 handler 签名不变)。 +- 新错误码追加到 `ErrorCodeConstants`,编号顺延现有 task 模块段。 + +### 3.3 `TransportTaskFeedbackServiceImpl` 瘦身 + +只保留 ACS 前置处理,注入 `TransportTaskService` 后委托分发: + +1. 查任务(不存在打 warn 返回,保持现状)。 +2. 终态幂等判断(保持现状)。 +3. 保存 `resultParam`(保持现状)。 +4. 分发段替换为: + +```java +TaskOperationTypeEnum type = TaskOperationTypeEnum.getByCode(reqDTO.getStatus()); +if (type == null || !type.isAcsAllowed()) { + log.warn("未知或不允许的 ACS 反馈状态, taskId={}, status={}", reqDTO.getTaskId(), reqDTO.getStatus()); + return; +} +transportTaskService.dispatchOperation(task, type, reqDTO); +``` + +`handle*`、`publishEvent`、`buildCallbackReq` 从此类删除。依赖方向为 Feedback → TransportTaskService 单向,无循环依赖;`receiveAcsFeedback` 的 `@Transactional` 保持,跨 Bean 调用按 REQUIRED 传播沿用同一事务。 + +### 3.4 Controller / VO + +- `TransportTaskController` 新增: + +```java +@PostMapping("/operate") +@Operation(summary = "PC 端操作搬运任务(完成/取消/强制完成)") +@PreAuthorize("@ss.hasPermission('task:transport-task:operate')") +public CommonResult operateTransportTask(@Valid @RequestBody TransportTaskOperateReqVO reqVO) +``` + +- 新 VO `TransportTaskOperateReqVO`(controller vo 包):`@NotNull Long taskId`;`@NotEmpty String operationType`(取值 FINISHED/CANCELLED/FORCE-FINISH)。 + +## 4. 前端设计(web-antdv-next) + +### 4.1 API(`api/task/transporttask/index.ts`) + +```ts +/** PC 端操作搬运任务(完成/取消/强制完成) */ +export function operateTransportTask(data: { + operationType: string; + taskId: number; +}) { + return requestClient.post('/task/transport-task/operate', data); +} +``` + +### 4.2 页面(`views/task/transporttask/index.vue`) + +- 新增操作函数: + +```ts +async function handleOperate( + row: TaskTransportTaskApi.TransportTask, + operationType: string, + label: string, +) { + await confirm(`确认要${label}【${row.taskCode}】吗?`); + await operateTransportTask({ taskId: row.taskId!, operationType }); + message.success(`${label}成功`); + handleRefresh(); +} +``` + +- 三个按钮 `onClick` 替换错绑的 `handleEdit`: + - 完成任务 → `handleOperate(row, 'FINISHED', '完成任务')` + - 取消任务 → `handleOperate(row, 'CANCELLED', '取消任务')` + - 强制完成任务 → `handleOperate(row, 'FORCE-FINISH', '强制完成任务')` +- 备注:强制完成按钮当前 `auth` 复用 `task:transport-task:cancel`、icon 用 `CANCEL`,本次不调整权限(需菜单配套),仅修正绑定。 + +## 5. 错误处理 + +- PC 入口:任务不存在 / 操作类型不支持 / 任务已终态,均抛 ServiceException,由全局异常处理返回给前端提示。 +- ACS 入口:保持现状(不存在/终态/未知状态均打日志静默返回成功,避免 ACS 重试)。 +- MQ 发布失败:沿用 `publishEvent` 现有逻辑(callbackStatus=FAILED + 记录错误信息)。 + +## 6. 测试与验证 + +- 按项目约定本次不编写测试(CLAUDE.md:除非用户允许测试)。 +- 验证方式:后端 `mvn compile`(task 模块)通过;前端 lint/类型检查通过。 + +## 7. 范围外(明确不做) + +- APPLY-AGAIN、FORCE-FINISH 的具体业务实现(todo 占位)。 +- 强制完成按钮的独立权限与菜单配置。 +- PC 操作联动 ACS(如向 ACS 下发取消指令)。 +- `finishedType` 的字典配置与前端字典展示。 diff --git a/docs/superpowers/specs/2026-07-16-jenkinsfile-monolith-deploy-design.md b/docs/superpowers/specs/2026-07-16-jenkinsfile-monolith-deploy-design.md new file mode 100644 index 00000000..e0dc2a9d --- /dev/null +++ b/docs/superpowers/specs/2026-07-16-jenkinsfile-monolith-deploy-design.md @@ -0,0 +1,73 @@ +# Jenkinsfile 单体部署 nl-server 设计 + +- 日期:2026-07-16 +- 分支:feature/20260713/task-module +- 状态:已评审通过(Docker 容器 + 构建机=运行机 + 参数化多环境) + +## 1. 背景与目标 + +项目当前无任何 CI/CD 流水线。目标:在仓库根目录新增 `Jenkinsfile`,把 `nl-server`(单体聚合模块,内含 system/infra/lms/task 四个 server 模块)以 Docker 容器形式部署到 Jenkins 构建机本机,先跑通单服务单机部署。 + +## 2. 已确认的环境事实与决策 + +| 项 | 结论 | +|---|---| +| 部署形态 | Docker 容器(复用现有 `nl-server/Dockerfile`:temurin 21-jre、EXPOSE 48080、支持 `JAVA_OPTS`/`ARGS` 环境变量) | +| 构建机与运行机 | 同一台机器(Jenkins agent 本机 docker run) | +| Spring profile | 参数化(test/dev/prod,默认 test) | +| 构建产物 | `mvn package` 后 `nl-server/target/nl-server.jar`(pom finalName=nl-server,spring-boot repackage) | +| 前提 | 构建机已装 JDK 17+、Maven、Docker,Jenkins 任务类型为 Pipeline from SCM | +| 测试 | 构建跳过测试(`-DskipTests`,项目约定) | + +## 3. Jenkinsfile 设计(声明式 Pipeline) + +### 3.1 参数(parameters) + +| 参数 | 类型 | 默认值 | 用途 | +|---|---|---|---| +| `PROFILE` | choice(test/dev/prod) | test | 注入 `SPRING_PROFILES_ACTIVE` | +| `NACOS_ADDR` | string | `127.0.0.1:8848` | 注入 `SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR` 与 `SPRING_CLOUD_NACOS_SERVER_ADDR`(沿用现有 docker-compose 变量名风格) | +| `NACOS_NAMESPACE` | string | 空 | 注入 `SPRING_CLOUD_NACOS_CONFIG_NAMESPACE` 与 `SPRING_CLOUD_NACOS_DISCOVERY_NAMESPACE` | +| `JAVA_OPTS` | string | `-Xms512m -Xmx512m -Djava.security.egd=file:/dev/./urandom` | 覆盖 Dockerfile 内默认 JVM 参数(默认值与 Dockerfile 保持一致,避免参数化后丢失 egd 配置) | + +### 3.2 环境变量(environment) + +- `IMAGE_NAME = 'nl-server'` +- `CONTAINER_NAME = 'nl-server'` +- `APP_PORT = '48080'` + +### 3.3 Stages + +1. **Checkout**:`checkout scm`。 +2. **Maven Build**:`mvn clean package -pl nl-server -am -DskipTests`(只构建 nl-server 及依赖模块)。 +3. **Docker Build**:`docker build -t nl-server:${BUILD_NUMBER} -t nl-server:latest nl-server/`(构建上下文为 nl-server 目录,Dockerfile `COPY ./target/nl-server.jar`)。 +4. **Deploy**:`docker stop`/`docker rm` 旧容器(`|| true` 容错首次部署);`docker run -d --name nl-server --restart always --network host`,挂载日志卷 `-v /docker/nl-cloud/logs:/root/logs/`(与现有 compose 一致),注入 `TZ=Asia/Shanghai`、`SPRING_PROFILES_ACTIVE`、Nacos 四个变量、`JAVA_OPTS`,镜像用 `nl-server:${BUILD_NUMBER}`。 +5. **Health Check**:shell 循环最多 24 次、每次间隔 5s(约 120s)探测 `http://127.0.0.1:48080`,收到任意 HTTP 响应即通过;超时则 `docker logs --tail 200` 输出容器日志并使构建失败。 + +### 3.4 post 处理 + +- `success`:输出部署成功信息(镜像 tag、访问地址)。 +- `failure`:输出失败提示。 +- `always`:`docker image prune -f` 仅清理悬空镜像(历史 `nl-server:` tag 保留,用于手动回滚)。 + +### 3.5 网络模式说明 + +使用 `--network host`:与项目现有 `script/docker/docker-compose.yml` 一致;Nacos 在同机部署时容器内可直接访问 `127.0.0.1:8848`,无需端口映射(端口由应用自身监听 48080)。 + +## 4. 错误处理 + +- 任一 stage 命令非零退出即中止流水线(声明式默认行为)。 +- 停删旧容器 `|| true`,首次部署无旧容器不报错。 +- 健康检查失败:输出容器日志后 `exit 1`,构建标红;旧镜像 tag 仍在本机,可手动回滚。 + +## 5. 验证 + +- 本地无 Jenkins 环境,静态验证为主:Jenkinsfile 为 Groovy 声明式语法,检查括号/引号配对与 stage 结构;shell 片段用 `bash -n` 无法直接验证(内嵌),人工核对。 +- 真实验证需在 Jenkins 上建 Pipeline 任务指向仓库运行(由用户执行)。 + +## 6. 范围外(明确不做) + +- 微服务逐个部署(gateway/system/infra/task 独立容器)。 +- 推送镜像仓库、SSH 远程部署、多机部署。 +- 前端 nl-ui 的构建部署。 +- 自动回滚、通知(钉钉等)。 diff --git a/docs/superpowers/specs/2026-07-16-transport-task-api-query-design.md b/docs/superpowers/specs/2026-07-16-transport-task-api-query-design.md new file mode 100644 index 00000000..649d539d --- /dev/null +++ b/docs/superpowers/specs/2026-07-16-transport-task-api-query-design.md @@ -0,0 +1,112 @@ +# TransportTaskApi RPC 查询接口设计(补全 todo) + +- 日期:2026-07-16 +- 分支:feature/20260713/task-module +- 状态:已评审通过(方案 1:Api → Service → Mapper 标准分层) + +## 1. 背景与目标 + +`TransportTaskApi`(Task 服务 RPC 接口,LMS/WMS 经 Feign 调用)中有两个 todo 待实现: + +```java +// todo 根据taskId查找任务(返回TaskInfoDTO、过滤删除) +// todo 根据taskCode获取任务(返回TaskInfoDTO、过滤删除) +``` + +目标:补全这两个 RPC 查询接口,返回已存在的 `TaskInfoDTO`(35 字段全量数据),自动过滤逻辑删除的数据。 + +## 2. 关键事实(探索确认) + +- `TaskInfoDTO` 已存在于 `nl-module-task-api/.../dto/TaskInfoDTO.java`,字段与 `TransportTaskDO` 同名同义(vehicleQty=Long、callbackRetryCount=Integer 均一致),可零配置映射。 +- `BaseDO.deleted` 带 `@TableLogic`,MyBatis-Plus 的 `selectById`/`selectOne` 自动追加 `deleted=0`,"过滤删除"无需手写条件。 +- MapStruct 1.6.3 经 `nl-common` 传递依赖对 task-server 可用(infra 模块 `ConfigConvert` 即此模式),无需改 pom。 +- task 模块尚无 `convert` 包,需新建。 +- Mapper 已有 `selectOne` 简单查询先例(`selectUnfinishedByBiz`),团队规范允许简单查询用 MyBatis-Plus 自带方法。 + +## 3. 设计 + +### 3.1 `TransportTaskApi`(api 模块) + +删除两行 todo,新增两个 GET 方法(遵循"feign 方法名驼峰、url 与方法名一致"规则;注解风格与本文件现有方法保持一致): + +```java +@GetMapping(PREFIX + "/getTaskById") +@Operation(summary = "根据 taskId 查询任务(LMS/WMS 调用)") +CommonResult getTaskById(@RequestParam("taskId") Long taskId); + +@GetMapping(PREFIX + "/getTaskByCode") +@Operation(summary = "根据 taskCode 查询任务(LMS/WMS 调用)") +CommonResult getTaskByCode(@RequestParam("taskCode") String taskCode); +``` + +需补充 import:`GetMapping`、`RequestParam`、`TaskInfoDTO`。 + +### 3.2 `TransportTaskApiImpl`(server 模块) + +只做委托(与现有模式一致): + +```java +@Override +public CommonResult getTaskById(Long taskId) { + return success(transportTaskService.getTaskInfoById(taskId)); +} + +@Override +public CommonResult getTaskByCode(String taskCode) { + return success(transportTaskService.getTaskInfoByCode(taskCode)); +} +``` + +### 3.3 `TransportTaskService(Impl)` + +新增两个方法(中文 javadoc): + +- `TaskInfoDTO getTaskInfoById(Long taskId)`:`transportTaskMapper.selectById(taskId)`,为 null 返回 null,否则 `TransportTaskConvert.INSTANCE.convert(task)`。 +- `TaskInfoDTO getTaskInfoByCode(String taskCode)`:`transportTaskMapper.selectByTaskCode(taskCode)`,同上。 + +查不到(含已逻辑删除)统一返回 null,由调用方判断——不抛业务异常(已确认)。 + +### 3.4 新建 `TransportTaskConvert`(server 模块,仿 infra `ConfigConvert`) + +路径:`nl-module-task-server/src/main/java/cn/code/nl/module/task/convert/transporttask/TransportTaskConvert.java` + +```java +@Mapper +public interface TransportTaskConvert { + + TransportTaskConvert INSTANCE = Mappers.getMapper(TransportTaskConvert.class); + + TaskInfoDTO convert(TransportTaskDO bean); +} +``` + +字段全部同名,无需 `@Mapping` 配置。 + +### 3.5 `TransportTaskMapper` + +新增(taskCode 业务唯一,返回单对象;`selectOne` 自动滤删): + +```java +/** + * 根据任务编码查询任务 + */ +default TransportTaskDO selectByTaskCode(String taskCode) { + return selectOne(TransportTaskDO::getTaskCode, taskCode); +} +``` + +## 4. 错误处理 + +- 查不到 / 已逻辑删除:`success(null)`,调用方自行判断。 +- 参数校验:Long/String 由 Spring MVC `@RequestParam` 必填约束兜底(缺参即 400),不额外写判空。 + +## 5. 测试与验证 + +- 按项目约定不编写测试。 +- 验证:`mvn -pl nl-module-task/nl-module-task-server -am compile` BUILD SUCCESS(同时验证 MapStruct 注解处理器生成实现类无报错)。 + +## 6. 范围外(明确不做) + +- taskCode 数据库唯一索引调整。 +- LMS/WMS 调用方接入代码。 +- 其他 Api 方法的改动。 diff --git a/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/TaskCommonApi.java b/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/api/TaskCommonApi.java similarity index 50% rename from nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/TaskCommonApi.java rename to nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/api/TaskCommonApi.java index 044561d4..f393fccb 100644 --- a/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/TaskCommonApi.java +++ b/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/api/TaskCommonApi.java @@ -1,5 +1,6 @@ -package cn.code.nl.framework.execute.biz; +package cn.code.nl.framework.execute.biz.api; +import cn.code.nl.framework.execute.biz.dto.TaskStatusCallApiReqDTO; import org.springframework.web.bind.annotation.PostMapping; /** @@ -10,5 +11,5 @@ import org.springframework.web.bind.annotation.PostMapping; public interface TaskCommonApi { @PostMapping("/do-handle-picked") - void doHandlePicked(); + void doHandlePicked(TaskStatusCallApiReqDTO taskStatusCallApiReqDTO); } diff --git a/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/lms/LmsTaskCommonApi.java b/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/api/lms/LmsTaskCommonApi.java similarity index 82% rename from nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/lms/LmsTaskCommonApi.java rename to nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/api/lms/LmsTaskCommonApi.java index 70ec4969..3bd8ff04 100644 --- a/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/lms/LmsTaskCommonApi.java +++ b/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/api/lms/LmsTaskCommonApi.java @@ -1,7 +1,7 @@ -package cn.code.nl.framework.execute.biz.lms; +package cn.code.nl.framework.execute.biz.api.lms; import cn.code.nl.framework.common.enums.RpcConstants; -import cn.code.nl.framework.execute.biz.TaskCommonApi; +import cn.code.nl.framework.execute.biz.api.TaskCommonApi; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/wms/WmsTaskCommonApi.java b/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/api/wms/WmsTaskCommonApi.java similarity index 82% rename from nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/wms/WmsTaskCommonApi.java rename to nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/api/wms/WmsTaskCommonApi.java index dfe96de6..2e442ddb 100644 --- a/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/wms/WmsTaskCommonApi.java +++ b/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/api/wms/WmsTaskCommonApi.java @@ -1,7 +1,7 @@ -package cn.code.nl.framework.execute.biz.wms; +package cn.code.nl.framework.execute.biz.api.wms; import cn.code.nl.framework.common.enums.RpcConstants; -import cn.code.nl.framework.execute.biz.TaskCommonApi; +import cn.code.nl.framework.execute.biz.api.TaskCommonApi; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/dto/TaskStatusCallApiReqDTO.java b/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/dto/TaskStatusCallApiReqDTO.java new file mode 100644 index 00000000..fe2e4f7d --- /dev/null +++ b/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/biz/dto/TaskStatusCallApiReqDTO.java @@ -0,0 +1,38 @@ +package cn.code.nl.framework.execute.biz.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Map; + +/** + * 任务状态返回DTO + * @Author: liyongde + * @Date: 2026/7/16 14:01 + */ +@Data +public class TaskStatusCallApiReqDTO implements Serializable { + /** 任务ID */ + private Long taskId; + + /** 任务编码 */ + private String taskCode; + + /** 回调状态:当前为 PICKED(65) */ + private String status; + + /** 业务归属服务 */ + private String ownerService; + + /** 业务类型 */ + private String bizType; + + /** 业务侧标识 */ + private String bizId; + + /** 业务回调处理器编码(LMS/WMS 内部路由) */ + private String handleCode; + + /** 扩展数据 */ + private Map payload; +} diff --git a/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/config/NlExecuteRpcAutoConfiguration.java b/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/config/NlExecuteRpcAutoConfiguration.java index 44100c07..dd878f0d 100644 --- a/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/config/NlExecuteRpcAutoConfiguration.java +++ b/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/config/NlExecuteRpcAutoConfiguration.java @@ -1,7 +1,7 @@ package cn.code.nl.framework.execute.config; -import cn.code.nl.framework.execute.biz.lms.LmsTaskCommonApi; -import cn.code.nl.framework.execute.biz.wms.WmsTaskCommonApi; +import cn.code.nl.framework.execute.biz.api.lms.LmsTaskCommonApi; +import cn.code.nl.framework.execute.biz.api.wms.WmsTaskCommonApi; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.cloud.openfeign.EnableFeignClients; diff --git a/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/core/TaskCommonApiFactory.java b/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/core/TaskCommonApiFactory.java index ce0a049d..7a620b26 100644 --- a/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/core/TaskCommonApiFactory.java +++ b/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/core/TaskCommonApiFactory.java @@ -2,9 +2,9 @@ package cn.code.nl.framework.execute.core; import cn.code.nl.framework.common.enums.RpcConstants; import cn.code.nl.framework.common.exception.ServiceException; -import cn.code.nl.framework.execute.biz.TaskCommonApi; -import cn.code.nl.framework.execute.biz.lms.LmsTaskCommonApi; -import cn.code.nl.framework.execute.biz.wms.WmsTaskCommonApi; +import cn.code.nl.framework.execute.biz.api.TaskCommonApi; +import cn.code.nl.framework.execute.biz.api.lms.LmsTaskCommonApi; +import cn.code.nl.framework.execute.biz.api.wms.WmsTaskCommonApi; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/DemoApi.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/DemoApi.java index 5a55fafa..fcc5b893 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/DemoApi.java +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/DemoApi.java @@ -1,6 +1,7 @@ package cn.code.nl.module.lms; -import cn.code.nl.framework.execute.biz.lms.LmsTaskCommonApi; +import cn.code.nl.framework.execute.biz.api.lms.LmsTaskCommonApi; +import cn.code.nl.framework.execute.biz.dto.TaskStatusCallApiReqDTO; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RestController; @@ -13,7 +14,7 @@ import org.springframework.web.bind.annotation.RestController; @Validated public class DemoApi implements LmsTaskCommonApi { @Override - public void doHandlePicked() { + public void doHandlePicked(TaskStatusCallApiReqDTO taskStatusCallApiReqDTO) { } } diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/api/TransportTaskApi.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/api/TransportTaskApi.java index adc8f652..05928de0 100644 --- a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/api/TransportTaskApi.java +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/api/TransportTaskApi.java @@ -1,14 +1,17 @@ package cn.code.nl.module.task.api; import cn.code.nl.framework.common.pojo.CommonResult; +import cn.code.nl.module.task.dto.TaskInfoDTO; import cn.code.nl.module.task.dto.TransportTaskCreateReqDTO; import cn.code.nl.module.task.dto.TaskCallbackResultReqDTO; import cn.code.nl.module.task.enums.ApiConstants; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import jakarta.validation.Valid; @@ -30,4 +33,12 @@ public interface TransportTaskApi { @PostMapping(PREFIX + "/callback-result") @Operation(summary = "接收 LMS/WMS 业务处理结果") CommonResult receiveCallbackResult(@Valid @RequestBody TaskCallbackResultReqDTO reqDTO); + + @GetMapping(PREFIX + "/getTaskById") + @Operation(summary = "根据 taskId 查询任务") + CommonResult getTaskById(@RequestParam("taskId") Long taskId); + + @GetMapping(PREFIX + "/getTaskByCode") + @Operation(summary = "根据 taskCode 查询任务") + CommonResult getTaskByCode(@RequestParam("taskCode") String taskCode); } diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/AcsFeedbackReqDTO.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/AcsFeedbackReqDTO.java index 19f8b76c..c8050209 100644 --- a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/AcsFeedbackReqDTO.java +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/AcsFeedbackReqDTO.java @@ -18,13 +18,19 @@ public class AcsFeedbackReqDTO { @NotNull(message = "任务ID不能为空") private Long taskId; - @Schema(description = "ACS反馈状态:EXECUTING/PICKED/FINISHED/CANCELLED", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "任务编码") + private String taskCode; + + @Schema(description = "ACS反馈状态:EXECUTING/PICKED/FINISHED/CANCELLED/APPLYAGAIN", requiredMode = Schema.RequiredMode.REQUIRED) @NotEmpty(message = "反馈状态不能为空") private String status; @Schema(description = "事件ID(幂等键)") private String eventId; + @Schema(description = "车号") + private String carNo; + @Schema(description = "ACS反馈扩展数据") private Map payload; } diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskInfoDTO.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskInfoDTO.java new file mode 100644 index 00000000..b4be444f --- /dev/null +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TaskInfoDTO.java @@ -0,0 +1,154 @@ +package cn.code.nl.module.task.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 任务表的全量字段 + * @Author: liyongde + * @Date: 2026/7/16 9:38 + */ +@Data +public class TaskInfoDTO implements Serializable { + /** + * 任务标识 + */ + 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; + /** + * 生成方式 + */ + private String createMode; + /** + * 创建任务请求参数 + */ + private String requestParam; + /** + * 下发ACS的AcsTaskDto扩展报文 + */ + private String dispatchParam; + /** + * ACS反馈参数 + */ + private String resultParam; + /** + * 备注 + */ + private String remark; +} 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 index c01ed4f5..96d36590 100644 --- 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 @@ -13,5 +13,7 @@ public interface ErrorCodeConstants { ErrorCode TRANSPORT_TASK_STATUS_NOT_ALLOW = new ErrorCode(5003, "当前任务状态不允许此操作"); ErrorCode TRANSPORT_TASK_CALLBACK_SEND_FAILED = new ErrorCode(5004, "业务回调发送失败"); ErrorCode TRANSPORT_TASK_RUNNING_ALREADY_EXIST = new ErrorCode(5005, "已存在运行中的任务"); + ErrorCode TRANSPORT_TASK_OPERATION_NOT_SUPPORTED = new ErrorCode(5006, "不支持的任务操作类型"); + ErrorCode TRANSPORT_TASK_ALREADY_FINAL = new ErrorCode(5007, "任务已处于终态,不允许操作"); } diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/FinishedTypeEnum.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/FinishedTypeEnum.java new file mode 100644 index 00000000..8d8ba9af --- /dev/null +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/FinishedTypeEnum.java @@ -0,0 +1,23 @@ +package cn.code.nl.module.task.enums; + +import lombok.Getter; + +/** + * + * @Author: liyongde + * @Date: 2026/7/16 11:03 + */ +@Getter +public enum FinishedTypeEnum { + MANUAL("MANUAL", "人工完成"), + AUTO("AUTO", "自动完成"), + MANUAL_FORCE("MANUAL_FORCE", "人工强制完成"); + + private final String code; + private final String name; + + FinishedTypeEnum(String code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/TaskOperationTypeEnum.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/TaskOperationTypeEnum.java new file mode 100644 index 00000000..27cba873 --- /dev/null +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/enums/TaskOperationTypeEnum.java @@ -0,0 +1,55 @@ +package cn.code.nl.module.task.enums; + +import lombok.Getter; + +/** + * 任务操作类型枚举 + *

+ * acsAllowed:是否允许 ACS 反馈触发;pcAllowed:是否允许 PC 端触发 + * + * @Author: liyongde + * @Date: 2026/7/15 19:11 + */ +@Getter +public enum TaskOperationTypeEnum { + + EXECUTING("EXECUTING", "执行中", true, false), + + PICKED("PICKED", "取货完成", true, false), + + FINISHED("FINISHED", "完成任务", true, true), + + CANCELLED("CANCELLED", "取消任务", true, true), + + APPLY_AGAIN("APPLY-AGAIN", "二次请求", true, false), + + FORCE_FINISH("FORCE-FINISH", "强制完成", false, true); + + /** 操作编码 */ + private final String code; + /** 操作名称 */ + private final String name; + /** 是否允许 ACS 反馈触发 */ + private final boolean acsAllowed; + /** 是否允许 PC 端触发 */ + private final boolean pcAllowed; + + TaskOperationTypeEnum(String code, String name, boolean acsAllowed, boolean pcAllowed) { + this.code = code; + this.name = name; + this.acsAllowed = acsAllowed; + this.pcAllowed = pcAllowed; + } + + /** + * 根据编码解析枚举(忽略大小写),找不到返回 null + */ + public static TaskOperationTypeEnum getByCode(String code) { + for (TaskOperationTypeEnum type : values()) { + if (type.getCode().equalsIgnoreCase(code)) { + return type; + } + } + return null; + } +} diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/api/TransportTaskApiImpl.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/api/TransportTaskApiImpl.java index 0d5f2d7c..c397dcac 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/api/TransportTaskApiImpl.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/api/TransportTaskApiImpl.java @@ -1,6 +1,7 @@ package cn.code.nl.module.task.api; import cn.code.nl.framework.common.pojo.CommonResult; +import cn.code.nl.module.task.dto.TaskInfoDTO; import cn.code.nl.module.task.dto.TransportTaskCreateReqDTO; import cn.code.nl.module.task.dto.TaskCallbackResultReqDTO; import cn.code.nl.module.task.service.transporttask.TransportTaskCallbackService; @@ -34,4 +35,14 @@ public class TransportTaskApiImpl implements TransportTaskApi { callbackService.receiveCallbackResult(reqDTO); return success(true); } + + @Override + public CommonResult getTaskById(Long taskId) { + return success(transportTaskService.getTaskInfoById(taskId)); + } + + @Override + public CommonResult getTaskByCode(String taskCode) { + return success(transportTaskService.getTaskInfoByCode(taskCode)); + } } diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/client/TransportTaskStatusClient.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/client/TransportTaskStatusClient.java index 3eba30ab..9a54476b 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/client/TransportTaskStatusClient.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/client/TransportTaskStatusClient.java @@ -16,6 +16,7 @@ import jakarta.annotation.Resource; * LMS/WMS 同步状态回调 HTTP 客户端 * 通过 Nacos 服务发现 + RestTemplate 调用目标服务 */ +@Deprecated @Slf4j @Component public class TransportTaskStatusClient { diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/AcsFeedbackController.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/AcsFeedbackController.java index 6cf432f0..3e9a9590 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/AcsFeedbackController.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/AcsFeedbackController.java @@ -33,7 +33,6 @@ public class AcsFeedbackController { @Operation(summary = "接收 ACS 状态反馈") public CommonResult receiveAcsFeedback(@Valid @RequestBody AcsFeedbackReqDTO reqDTO) { feedbackService.receiveAcsFeedback(reqDTO); - // 始终返回成功,避免 ACS 因业务异常而重试 return success(true); } } 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 index 7db3cd0f..dc056006 100644 --- 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 @@ -53,6 +53,14 @@ public class TransportTaskController { return success(true); } + @PostMapping("/operate") + @Operation(summary = "PC 端操作搬运任务(完成/取消/强制完成)") + @PreAuthorize("@ss.hasPermission('task:transport-task:operate')") + public CommonResult operateTransportTask(@Valid @RequestBody TransportTaskOperateReqVO reqVO) { + transportTaskService.operateTransportTask(reqVO); + return success(true); + } + @DeleteMapping("/delete") @Operation(summary = "删除搬运任务") @Parameter(name = "id", description = "编号", required = true) diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskOperateReqVO.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskOperateReqVO.java new file mode 100644 index 00000000..1d56ada9 --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/controller/admin/transporttask/vo/TransportTaskOperateReqVO.java @@ -0,0 +1,25 @@ +package cn.code.nl.module.task.controller.admin.transporttask.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * PC 端搬运任务操作 Request VO + * + * @Author: liyongde + * @Date: 2026/7/15 + */ +@Schema(description = "管理后台 - PC 端搬运任务操作 Request VO") +@Data +public class TransportTaskOperateReqVO { + + @Schema(description = "任务ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "任务ID不能为空") + private Long taskId; + + @Schema(description = "操作类型:FINISHED/CANCELLED/FORCE-FINISH", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "操作类型不能为空") + private String operationType; +} 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 index fb85db3f..1576581b 100644 --- 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 @@ -34,8 +34,8 @@ public class TransportTaskPageReqVO extends PageParam { @Schema(description = "任务类型", example = "2") private String taskType; - @Schema(description = "任务状态", example = "2") - private String taskStatus; + @Schema(description = "任务状态(支持多选)", example = "[\"10\", \"50\"]") + private List taskStatus; @Schema(description = "ACS任务类型", example = "1") private String acsTaskType; @@ -112,7 +112,7 @@ public class TransportTaskPageReqVO extends PageParam { @Schema(description = "ACS反馈参数") private String resultParam; - @Schema(description = "备注", example = "你说的对") + @Schema(description = "备注") private String remark; @Schema(description = "创建时间") diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/convert/transporttask/TransportTaskConvert.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/convert/transporttask/TransportTaskConvert.java new file mode 100644 index 00000000..1f5ca666 --- /dev/null +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/convert/transporttask/TransportTaskConvert.java @@ -0,0 +1,23 @@ +package cn.code.nl.module.task.convert.transporttask; + +import cn.code.nl.module.task.dal.dataobject.transporttask.TransportTaskDO; +import cn.code.nl.module.task.dto.TaskInfoDTO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 搬运任务 Convert + * + * @Author: liyongde + * @Date: 2026/7/16 + */ +@Mapper +public interface TransportTaskConvert { + + TransportTaskConvert INSTANCE = Mappers.getMapper(TransportTaskConvert.class); + + /** + * DO 转 RPC 全量信息 DTO(字段同名,零配置映射) + */ + TaskInfoDTO convert(TransportTaskDO bean); +} 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 index 52c9baa6..8acdbfac 100644 --- 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 @@ -28,7 +28,7 @@ public interface TransportTaskMapper extends BaseMapperX { .eqIfPresent(TransportTaskDO::getBizId, reqVO.getBizId()) .eqIfPresent(TransportTaskDO::getHandleCode, reqVO.getHandleCode()) .eqIfPresent(TransportTaskDO::getTaskType, reqVO.getTaskType()) - .eqIfPresent(TransportTaskDO::getTaskStatus, reqVO.getTaskStatus()) + .inIfPresent(TransportTaskDO::getTaskStatus, reqVO.getTaskStatus()) .eqIfPresent(TransportTaskDO::getAcsTaskType, reqVO.getAcsTaskType()) .eqIfPresent(TransportTaskDO::getAgvSystemType, reqVO.getAgvSystemType()) .eqIfPresent(TransportTaskDO::getExternalTaskNo, reqVO.getExternalTaskNo()) @@ -59,6 +59,13 @@ public interface TransportTaskMapper extends BaseMapperX { .orderByDesc(TransportTaskDO::getTaskId)); } + /** + * 根据任务编码查询任务(taskCode 业务唯一) + */ + default TransportTaskDO selectByTaskCode(String taskCode) { + return selectOne(TransportTaskDO::getTaskCode, taskCode); + } + /** * 按业务归属查询未完结任务(幂等校验用) */ diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/mq/message/TaskEventMessage.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/mq/message/TaskEventMessage.java index d6446b4f..63ffe8a1 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/mq/message/TaskEventMessage.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/mq/message/TaskEventMessage.java @@ -25,6 +25,9 @@ public class TaskEventMessage { /** 业务归属服务:LMS / WMS */ private String ownerService; + /** 任务类型 */ + private String taskType; + /** 业务类型 */ private String bizType; diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskFeedbackServiceImpl.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskFeedbackServiceImpl.java index d51c5ad9..19032375 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskFeedbackServiceImpl.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskFeedbackServiceImpl.java @@ -1,18 +1,10 @@ package cn.code.nl.module.task.service.transporttask; -import cn.code.nl.framework.execute.biz.TaskCommonApi; -import cn.code.nl.framework.execute.core.TaskCommonApiFactory; -import cn.code.nl.module.task.client.TransportTaskStatusClient; import cn.code.nl.module.task.dal.dataobject.transporttask.TransportTaskDO; import cn.code.nl.module.task.dal.mysql.transporttask.TransportTaskMapper; import cn.code.nl.module.task.dto.AcsFeedbackReqDTO; -import cn.code.nl.module.task.mq.message.TaskEventMessage; -import cn.code.nl.module.task.dto.TaskStatusCallbackReqDTO; -import cn.code.nl.module.task.enums.CallbackStatusEnum; -import cn.code.nl.module.task.enums.TaskEventTypeEnum; +import cn.code.nl.module.task.enums.TaskOperationTypeEnum; import cn.code.nl.module.task.enums.TransportTaskStatusEnum; -import cn.code.nl.module.task.mq.producer.TaskEventProducer; -import cn.hutool.core.util.StrUtil; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -20,12 +12,11 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; -import java.util.Map; - -import static cn.code.nl.module.task.framework.common.util.TaskUtil.isAllowedFrom; /** * ACS 反馈处理服务实现 + *

+ * 只做 ACS 前置处理(查任务/幂等/保存反馈参数),具体操作业务统一由 {@link TransportTaskService} 路由表分发 * * @author 诺力管理员 */ @@ -38,13 +29,7 @@ public class TransportTaskFeedbackServiceImpl implements TransportTaskFeedbackSe private TransportTaskMapper transportTaskMapper; @Resource - private TransportTaskStatusClient transportTaskStatusClient; - - @Resource - private TaskEventProducer taskEventProducer; - - @Resource - private TaskCommonApiFactory taskCommonApiFactory; + private TransportTaskService transportTaskService; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -76,153 +61,13 @@ public class TransportTaskFeedbackServiceImpl implements TransportTaskFeedbackSe } } - // 4. 按状态路由 - String feedbackStatus = reqDTO.getStatus(); - if ("EXECUTING".equalsIgnoreCase(feedbackStatus)) { - handleExecuting(task); - } else if ("PICKED".equalsIgnoreCase(feedbackStatus)) { - handlePicked(task, reqDTO); - } else if ("FINISHED".equalsIgnoreCase(feedbackStatus)) { - handleFinished(task, reqDTO); - } else if ("CANCELLED".equalsIgnoreCase(feedbackStatus)) { - handleCancelled(task, reqDTO); - } else { - log.warn("未知 ACS 反馈状态, taskId={}, status={}", reqDTO.getTaskId(), feedbackStatus); - } - } - - // ==================== 各状态处理方法 ==================== - - /** - * 处理执行中(60):直接更新状态,不做业务回调 - */ - private void handleExecuting(TransportTaskDO task) { - if (!isAllowedFrom(task.getTaskStatus(), "40", "50")) { - log.warn("执行中反馈状态校验不通过, taskId={}, currentStatus={}", - task.getTaskId(), task.getTaskStatus()); + // 4. 解析操作类型并校验允许 ACS 触发,走统一路由表分发 + TaskOperationTypeEnum type = TaskOperationTypeEnum.getByCode(reqDTO.getStatus()); + if (type == null || !type.isAcsAllowed()) { + log.warn("未知或不允许的 ACS 反馈状态, taskId={}, status={}", + reqDTO.getTaskId(), reqDTO.getStatus()); return; } - task.setTaskStatus(TransportTaskStatusEnum.EXECUTING.getCode()); - transportTaskMapper.updateById(task); - log.info("任务执行中, taskId={}", task.getTaskId()); - } - - /** - * 处理取货完成(61):更新状态 + 同步 HTTP 回调 LMS/WMS - * ownerService: 服务提供商 - */ - private void handlePicked(TransportTaskDO task, AcsFeedbackReqDTO reqDTO) { - if (!isAllowedFrom(task.getTaskStatus(), "60", "61")) { - log.warn("取货完成反馈状态校验不通过, taskId={}, currentStatus={}", - task.getTaskId(), task.getTaskStatus()); - return; - } - // 更新状态 - task.setTaskStatus(TransportTaskStatusEnum.PICKED.getCode()); - transportTaskMapper.updateById(task); - log.info("任务已取货, taskId={}", task.getTaskId()); - - // 考虑可行性 - TaskCommonApi byServerName = taskCommonApiFactory.getByServerName("lms-server"); - byServerName.doHandlePicked(); - - - // todo: 改成 feign 的 rpc 调用 - // 同步 HTTP 回调 LMS/WMS -// TaskStatusCallbackReqDTO callbackReq = buildCallbackReq(task, reqDTO); -// TaskStatusCallbackRespDTO resp = transportTaskStatusClient.notifyStatus(callbackReq); -// -// if (resp != null && Boolean.TRUE.equals(resp.getSuccess())) { -// log.info("取货完成同步回调成功, taskId={}", task.getTaskId()); -// } else { -// // 回调失败:记录但不阻塞 ACS 返回 -// task.setCallbackStatus(CallbackStatusEnum.FAILED.getCode()); -// task.setCallbackErrorMsg(resp != null ? resp.getMessage() : "回调无响应"); -// transportTaskMapper.updateById(task); -// log.warn("取货完成同步回调失败, taskId={}, msg={}", -// task.getTaskId(), task.getCallbackErrorMsg()); -// } - } - - /** - * 处理完成(67):更新状态 + 异步 MQ 通知 LMS/WMS - */ - private void handleFinished(TransportTaskDO task, AcsFeedbackReqDTO reqDTO) { - if (!isAllowedFrom(task.getTaskStatus(), "10", "40", "45", "50", "60", "61", "67")) { - log.warn("完成反馈状态校验不通过, taskId={}, currentStatus={}", - task.getTaskId(), task.getTaskStatus()); - return; - } - // 已是 67 则只发 MQ,不改状态 - if (!TransportTaskStatusEnum.FINISHED_CALLBACK_PENDING.getCode().equals(task.getTaskStatus())) { - task.setTaskStatus(TransportTaskStatusEnum.FINISHED_CALLBACK_PENDING.getCode()); - } - task.setCallbackStatus(CallbackStatusEnum.PENDING.getCode()); - transportTaskMapper.updateById(task); - - publishEvent(task, TaskEventTypeEnum.TASK_FINISHED, reqDTO.getPayload()); - } - - /** - * 处理取消(69):更新状态 + 异步 MQ 通知 LMS/WMS - */ - private void handleCancelled(TransportTaskDO task, AcsFeedbackReqDTO reqDTO) { - if (!isAllowedFrom(task.getTaskStatus(), "10", "40", "45", "50", "60", "61", "67", "69")) { - log.warn("取消反馈状态校验不通过, taskId={}, currentStatus={}", - task.getTaskId(), task.getTaskStatus()); - return; - } - if (!TransportTaskStatusEnum.CANCEL_CALLBACK_PENDING.getCode().equals(task.getTaskStatus())) { - task.setTaskStatus(TransportTaskStatusEnum.CANCEL_CALLBACK_PENDING.getCode()); - } - task.setCallbackStatus(CallbackStatusEnum.PENDING.getCode()); - transportTaskMapper.updateById(task); - - publishEvent(task, TaskEventTypeEnum.TASK_CANCELLED, reqDTO.getPayload()); - } - - // ==================== 工具方法 ==================== - - /** - * 发布 MQ 事件。发送失败时记录 callbackStatus=FAILED - */ - private void publishEvent(TransportTaskDO task, TaskEventTypeEnum eventType, - Map payload) { - TaskEventMessage msg = new TaskEventMessage(); - msg.setEventType(eventType.getCode()); - msg.setTaskId(task.getTaskId()); - msg.setTaskCode(task.getTaskCode()); - msg.setOwnerService(task.getOwnerService()); - msg.setBizType(task.getBizType()); - msg.setBizId(task.getBizId()); - msg.setHandleCode(task.getHandleCode()); - msg.setPayload(payload); - - try { - taskEventProducer.publishEvent(msg); - } catch (Exception e) { - log.error("MQ 发布失败, taskId={}, eventType={}", - task.getTaskId(), eventType.getCode(), e); - task.setCallbackStatus(CallbackStatusEnum.FAILED.getCode()); - task.setCallbackErrorMsg(StrUtil.maxLength(e.getMessage(), 500)); - transportTaskMapper.updateById(task); - } - } - - /** - * 构建同步回调请求 DTO - */ - private TaskStatusCallbackReqDTO buildCallbackReq(TransportTaskDO task, - AcsFeedbackReqDTO reqDTO) { - TaskStatusCallbackReqDTO req = new TaskStatusCallbackReqDTO(); - req.setTaskId(task.getTaskId()); - req.setTaskCode(task.getTaskCode()); - req.setStatus(TransportTaskStatusEnum.PICKED.getCode()); - req.setOwnerService(task.getOwnerService()); - req.setBizType(task.getBizType()); - req.setBizId(task.getBizId()); - req.setHandleCode(task.getHandleCode()); - req.setPayload(reqDTO.getPayload()); - return req; + transportTaskService.dispatchOperation(task, type, reqDTO); } } diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java index 0151356a..68d7d413 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java @@ -4,7 +4,10 @@ import java.util.*; import jakarta.validation.*; import cn.code.nl.module.task.controller.admin.transporttask.vo.*; import cn.code.nl.module.task.dal.dataobject.transporttask.TransportTaskDO; +import cn.code.nl.module.task.dto.AcsFeedbackReqDTO; +import cn.code.nl.module.task.dto.TaskInfoDTO; import cn.code.nl.module.task.dto.TransportTaskCreateReqDTO; +import cn.code.nl.module.task.enums.TaskOperationTypeEnum; import cn.code.nl.framework.common.pojo.PageResult; import cn.code.nl.framework.common.pojo.PageParam; @@ -68,4 +71,36 @@ public interface TransportTaskService { */ PageResult getTransportTaskPage(TransportTaskPageReqVO pageReqVO); + /** + * PC 端操作搬运任务(完成/取消/强制完成) + * + * @param reqVO 操作请求 + */ + void operateTransportTask(@Valid TransportTaskOperateReqVO reqVO); + + /** + * 按操作类型分发处理任务(ACS 反馈与 PC 端操作共用路由表) + * + * @param task 任务 + * @param type 操作类型 + * @param reqDTO 反馈请求(PC 端为构造的精简对象) + */ + void dispatchOperation(TransportTaskDO task, TaskOperationTypeEnum type, AcsFeedbackReqDTO reqDTO); + + /** + * 根据 taskId 查询任务全量信息(RPC 用,自动过滤逻辑删除,查不到返回 null) + * + * @param taskId 任务ID + * @return 任务全量信息 + */ + TaskInfoDTO getTaskInfoById(Long taskId); + + /** + * 根据 taskCode 查询任务全量信息(RPC 用,自动过滤逻辑删除,查不到返回 null) + * + * @param taskCode 任务编码 + * @return 任务全量信息 + */ + TaskInfoDTO getTaskInfoByCode(String taskCode); + } \ 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 index 73cb10b4..e9fd5b80 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java @@ -2,27 +2,46 @@ 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.framework.execute.biz.api.TaskCommonApi; +import cn.code.nl.framework.execute.biz.dto.TaskStatusCallApiReqDTO; +import cn.code.nl.framework.execute.core.TaskCommonApiFactory; +import cn.code.nl.module.task.controller.admin.transporttask.vo.TransportTaskOperateReqVO; 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.convert.transporttask.TransportTaskConvert; import cn.code.nl.module.task.dal.dataobject.transporttask.TransportTaskDO; import cn.code.nl.module.task.dal.mysql.transporttask.TransportTaskMapper; +import cn.code.nl.module.task.dto.AcsFeedbackReqDTO; +import cn.code.nl.module.task.dto.TaskInfoDTO; import cn.code.nl.module.task.dto.TransportTaskCreateReqDTO; -import cn.code.nl.module.task.enums.CallbackStatusEnum; -import cn.code.nl.module.task.enums.TransportTaskStatusEnum; +import cn.code.nl.module.task.enums.*; +import cn.code.nl.module.task.mq.message.TaskEventMessage; +import cn.code.nl.module.task.mq.producer.TaskEventProducer; +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; +import java.util.EnumMap; import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_ALREADY_FINAL; import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_NOT_EXISTS; +import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_OPERATION_NOT_SUPPORTED; +import static cn.code.nl.module.task.framework.common.util.TaskUtil.isAllowedFrom; /** * 搬运任务 Service 实现类 * * @author 诺力管理员 */ +@Slf4j @Service @Validated public class TransportTaskServiceImpl implements TransportTaskService { @@ -30,6 +49,35 @@ public class TransportTaskServiceImpl implements TransportTaskService { @Resource private TransportTaskMapper transportTaskMapper; + @Resource + private TaskEventProducer taskEventProducer; + + @Resource + private TaskCommonApiFactory taskCommonApiFactory; + + /** + * 操作类型 -> 处理方法 路由表(ACS 反馈与 PC 端操作共用) + */ + private final Map> operationHandlers = + new EnumMap<>(TaskOperationTypeEnum.class); + + /** + * 初始化操作类型路由表 + */ + @PostConstruct + public void initOperationHandlers() { + operationHandlers.put(TaskOperationTypeEnum.EXECUTING, this::handleExecuting); + operationHandlers.put(TaskOperationTypeEnum.PICKED, this::handlePicked); + operationHandlers.put(TaskOperationTypeEnum.FINISHED, this::handleFinished); + operationHandlers.put(TaskOperationTypeEnum.CANCELLED, this::handleCancelled); + // todo: 二次请求业务未开发 + operationHandlers.put(TaskOperationTypeEnum.APPLY_AGAIN, + (task, reqDTO) -> log.info("二次请求业务未开发, taskId={}", task.getTaskId())); + // todo: 强制完成业务未开发 + operationHandlers.put(TaskOperationTypeEnum.FORCE_FINISH, + (task, reqDTO) -> handleForceFinish(task)); + } + @Override public Long createTransportTask(TransportTaskSaveReqVO createReqVO) { // 插入 @@ -96,4 +144,203 @@ public class TransportTaskServiceImpl implements TransportTaskService { return transportTaskMapper.selectPage(pageReqVO); } + /** + * PC 端操作搬运任务(完成/取消/强制完成):先校验再走统一路由表分发 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void operateTransportTask(TransportTaskOperateReqVO reqVO) { + // 1. 校验任务存在 + TransportTaskDO task = transportTaskMapper.selectById(reqVO.getTaskId()); + if (task == null) { + throw exception(TRANSPORT_TASK_NOT_EXISTS); + } + // 2. 校验操作类型允许 PC 端触发 + TaskOperationTypeEnum type = TaskOperationTypeEnum.getByCode(reqVO.getOperationType()); + if (type == null || !type.isPcAllowed()) { + throw exception(TRANSPORT_TASK_OPERATION_NOT_SUPPORTED); + } + // 3. 终态校验:已完成/已取消不允许再操作 + if (TransportTaskStatusEnum.FINISHED.getCode().equals(task.getTaskStatus()) + || TransportTaskStatusEnum.CANCELLED.getCode().equals(task.getTaskStatus())) { + throw exception(TRANSPORT_TASK_ALREADY_FINAL); + } + // 4. PC 端与 ACS 的差异点:记录完成类型 + if (type == TaskOperationTypeEnum.FINISHED) { + task.setFinishedType(FinishedTypeEnum.MANUAL.getCode()); + } else if (type == TaskOperationTypeEnum.FORCE_FINISH) { + task.setFinishedType(FinishedTypeEnum.MANUAL_FORCE.getCode()); + } + // 5. 构造精简反馈对象,走统一路由表分发 + AcsFeedbackReqDTO reqDTO = new AcsFeedbackReqDTO(); + reqDTO.setTaskId(task.getTaskId()); + reqDTO.setStatus(type.getCode()); + dispatchOperation(task, type, reqDTO); + } + + /** + * 按操作类型查路由表分发 + */ + @Override + public void dispatchOperation(TransportTaskDO task, TaskOperationTypeEnum type, AcsFeedbackReqDTO reqDTO) { + BiConsumer handler = operationHandlers.get(type); + if (handler == null) { + log.warn("操作类型未注册处理器, taskId={}, type={}", task.getTaskId(), type.getCode()); + return; + } + handler.accept(task, reqDTO); + } + + /** + * 根据 taskId 查询任务全量信息:查不到返回 null,由调用方判断 + */ + @Override + public TaskInfoDTO getTaskInfoById(Long taskId) { + TransportTaskDO task = transportTaskMapper.selectById(taskId); + return TransportTaskConvert.INSTANCE.convert(task); + } + + /** + * 根据 taskCode 查询任务全量信息:查不到返回 null,由调用方判断 + */ + @Override + public TaskInfoDTO getTaskInfoByCode(String taskCode) { + TransportTaskDO task = transportTaskMapper.selectByTaskCode(taskCode); + return TransportTaskConvert.INSTANCE.convert(task); + } + + // ==================== 各状态处理方法(自 TransportTaskFeedbackServiceImpl 迁入,逻辑未改动) ==================== + + /** + * 处理执行中(60):直接更新状态,不做业务回调 + */ + private void handleExecuting(TransportTaskDO task, AcsFeedbackReqDTO reqDTO) { + if (!isAllowedFrom(task.getTaskStatus(), TransportTaskStatusEnum.READY.getCode(), + TransportTaskStatusEnum.ISSUED.getCode())) { + log.warn("执行中反馈状态校验不通过, taskId={}, currentStatus={}", + task.getTaskId(), task.getTaskStatus()); + return; + } + task.setTaskStatus(TransportTaskStatusEnum.EXECUTING.getCode()); + task.setCarNo(reqDTO.getCarNo()); + transportTaskMapper.updateById(task); + log.info("任务执行中, taskId={}", task.getTaskId()); + } + + /** + * 处理取货完成(65):更新状态 + 同步回调 LMS/WMS + * ownerService: 服务提供商 + */ + private void handlePicked(TransportTaskDO task, AcsFeedbackReqDTO reqDTO) { + if (!isAllowedFrom(task.getTaskStatus(), TransportTaskStatusEnum.EXECUTING.getCode(), + TransportTaskStatusEnum.PICKED.getCode())) { + log.warn("取货完成反馈状态校验不通过, taskId={}, currentStatus={}", + task.getTaskId(), task.getTaskStatus()); + return; + } + // 更新状态 + task.setTaskStatus(TransportTaskStatusEnum.PICKED.getCode()); + transportTaskMapper.updateById(task); + log.info("任务已取货, taskId={}", task.getTaskId()); + + TaskStatusCallApiReqDTO req = new TaskStatusCallApiReqDTO(); + req.setTaskId(task.getTaskId()); + req.setTaskCode(task.getTaskCode()); + req.setStatus(TransportTaskStatusEnum.PICKED.getCode()); + req.setOwnerService(task.getOwnerService()); + req.setBizType(task.getBizType()); + req.setBizId(task.getBizId()); + req.setHandleCode(task.getHandleCode()); + req.setPayload(reqDTO.getPayload()); + + // 同步请求业务 + TaskCommonApi serverApi = taskCommonApiFactory.getByServerName(task.getOwnerService()); + serverApi.doHandlePicked(req); + } + + /** + * 处理完成(75):更新状态 + 异步 MQ 通知 LMS/WMS + */ + private void handleFinished(TransportTaskDO task, AcsFeedbackReqDTO reqDTO) { + if (!isAllowedFrom(task.getTaskStatus(), TransportTaskStatusEnum.CREATED.getCode(), + TransportTaskStatusEnum.READY.getCode(), TransportTaskStatusEnum.ISSUING.getCode(), + TransportTaskStatusEnum.ISSUED.getCode(), TransportTaskStatusEnum.EXECUTING.getCode(), + TransportTaskStatusEnum.PICKED.getCode(), + TransportTaskStatusEnum.FINISHED_CALLBACK_PENDING.getCode())) { + log.warn("完成反馈状态校验不通过, taskId={}, currentStatus={}", + task.getTaskId(), task.getTaskStatus()); + return; + } + // 已是 75 则只发 MQ,不改状态 + if (!TransportTaskStatusEnum.FINISHED_CALLBACK_PENDING.getCode().equals(task.getTaskStatus())) { + task.setTaskStatus(TransportTaskStatusEnum.FINISHED_CALLBACK_PENDING.getCode()); + } + task.setCallbackStatus(CallbackStatusEnum.PENDING.getCode()); + transportTaskMapper.updateById(task); + + publishEvent(task, TaskEventTypeEnum.TASK_FINISHED, reqDTO.getPayload()); + } + + /** + * 处理取消(85):更新状态 + 异步 MQ 通知 LMS/WMS + */ + private void handleCancelled(TransportTaskDO task, AcsFeedbackReqDTO reqDTO) { + if (!isAllowedFrom(task.getTaskStatus(), TransportTaskStatusEnum.CREATED.getCode(), + TransportTaskStatusEnum.READY.getCode(), TransportTaskStatusEnum.ISSUING.getCode(), + TransportTaskStatusEnum.ISSUED.getCode(), TransportTaskStatusEnum.EXECUTING.getCode(), + TransportTaskStatusEnum.PICKED.getCode(), + TransportTaskStatusEnum.FINISHED_CALLBACK_PENDING.getCode(), + TransportTaskStatusEnum.CANCEL_CALLBACK_PENDING.getCode())) { + log.warn("取消反馈状态校验不通过, taskId={}, currentStatus={}", + task.getTaskId(), task.getTaskStatus()); + return; + } + if (!TransportTaskStatusEnum.CANCEL_CALLBACK_PENDING.getCode().equals(task.getTaskStatus())) { + task.setTaskStatus(TransportTaskStatusEnum.CANCEL_CALLBACK_PENDING.getCode()); + } + task.setCallbackStatus(CallbackStatusEnum.PENDING.getCode()); + transportTaskMapper.updateById(task); + + publishEvent(task, TaskEventTypeEnum.TASK_CANCELLED, reqDTO.getPayload()); + } + + /** + * 处理强制完成(79):直接更新状态,不做业务回调 + */ + private void handleForceFinish(TransportTaskDO task) { + task.setTaskStatus(TransportTaskStatusEnum.FINISHED.getCode()); + task.setFinishedType(FinishedTypeEnum.MANUAL_FORCE.getCode()); + transportTaskMapper.updateById(task); + log.info("任务强制完成, taskId={}", task.getTaskId()); + } + + // ==================== 工具方法 ==================== + + /** + * 发布 MQ 事件。发送失败时记录 callbackStatus=FAILED + */ + private void publishEvent(TransportTaskDO task, TaskEventTypeEnum eventType, + Map payload) { + TaskEventMessage msg = new TaskEventMessage(); + msg.setEventType(eventType.getCode()); + msg.setTaskId(task.getTaskId()); + msg.setTaskCode(task.getTaskCode()); + msg.setOwnerService(task.getOwnerService()); + msg.setBizType(task.getBizType()); + msg.setBizId(task.getBizId()); + msg.setTaskType(task.getTaskType()); + msg.setHandleCode(task.getHandleCode()); + msg.setPayload(payload); + + try { + taskEventProducer.publishEvent(msg); + } catch (Exception e) { + log.error("MQ 发布失败, taskId={}, eventType={}", + task.getTaskId(), eventType.getCode(), e); + task.setCallbackStatus(CallbackStatusEnum.FAILED.getCode()); + task.setCallbackErrorMsg(StrUtil.maxLength(e.getMessage(), 500)); + transportTaskMapper.updateById(task); + } + } + } 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 index ca32be74..324cda8b 100644 --- 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 @@ -5,6 +5,7 @@ import { requestClient } from '#/api/request'; export namespace TaskTransportTaskApi { /** 搬运任务信息 */ export interface TransportTask { + taskId?: string; taskCode?: string; // 任务编码 taskName: string; // 任务名称 ownerService?: string; // 业务归属服务:LMS/WMS,用于完成取消事件一级路由 @@ -84,4 +85,12 @@ export function exportTransportTask(params: any) { return requestClient.download('/task/transport-task/export-excel', { params }); } +/** PC 端操作搬运任务(完成/取消/强制完成) */ +export function operateTransportTask(data: { + operationType: string; + taskId: number | string; +}) { + return requestClient.post('/task/transport-task/operate', data); +} + diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/components/table-action/icons.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/components/table-action/icons.ts index 578265f4..67e8ad4e 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/components/table-action/icons.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/components/table-action/icons.ts @@ -13,4 +13,6 @@ export const ACTION_ICON = { CLOSE: 'lucide:x', BOOK: 'lucide:book', AUDIT: 'lucide:file-check', + SEND: 'lucide:send', + CANCEL: 'lucide:ban', }; 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 index c197f26b..da5443dd 100644 --- 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 @@ -5,6 +5,8 @@ import type { TaskTransportTaskApi } from '#/api/task/transporttask'; import { DICT_TYPE } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; +import dayjs from 'dayjs'; + import { getRangePickerDefaultProps } from '#/utils'; /** 新增/修改的表单 */ @@ -380,7 +382,9 @@ export function useGridFormSchema(): VbenFormSchema[] { component: 'Select', componentProps: { allowClear: true, - options: [], + mode: 'multiple', + maxTagCount: 'responsive', + options: getDictOptions(DICT_TYPE.TASK_STATUS), placeholder: '请选择任务状态', }, }, @@ -390,7 +394,7 @@ export function useGridFormSchema(): VbenFormSchema[] { component: 'Select', componentProps: { allowClear: true, - options: [], + options: getDictOptions(DICT_TYPE.TASK_ACS_TASK_TYPE), placeholder: '请选择ACS任务类型', }, }, @@ -470,10 +474,11 @@ export function useGridFormSchema(): VbenFormSchema[] { { fieldName: 'productArea', label: '生产区域', - component: 'Input', + component: 'Select', componentProps: { allowClear: true, - placeholder: '请输入生产区域', + options: getDictOptions(DICT_TYPE.PRODUCT_AREA), + placeholder: '请选择生产区域', }, }, { @@ -485,20 +490,14 @@ export function useGridFormSchema(): VbenFormSchema[] { placeholder: '请输入任务组标识', }, }, - { - fieldName: 'createMode', - label: '生成方式', - component: 'Select', - componentProps: { - allowClear: true, - options: getDictOptions(DICT_TYPE.USER_TYPE, 'string'), - placeholder: '请选择生成方式', - }, - }, { fieldName: 'createTime', label: '创建时间', component: 'RangePicker', + defaultValue: [ + dayjs().subtract(3, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'), + dayjs().add(3, 'day').endOf('day').format('YYYY-MM-DD HH:mm:ss'), + ], componentProps: { ...getRangePickerDefaultProps(), allowClear: true, @@ -531,45 +530,14 @@ export function useGridColumns(): VxeTableGridOptions item.id!); + checkedIds.value = records.map((item) => item.taskId!); } /** 导出表格 */ @@ -154,11 +170,28 @@ const [Grid, gridApi] = useVbenVxeGrid({