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({