fix: ACS、PC对任务状态的变化

This commit is contained in:
2026-07-16 14:11:30 +08:00
parent f24e01c7dd
commit be46cf49c5
37 changed files with 2349 additions and 268 deletions

View File

@@ -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;
/**
* 任务操作类型枚举
* <p>
* 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<TaskOperationTypeEnum, BiConsumer<TransportTaskDO, AcsFeedbackReqDTO>> 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<TransportTaskDO, AcsFeedbackReqDTO> 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<String, Object> 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 反馈处理服务实现
* <p>
* 只做 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` 及其相关 importTaskCommonApi、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<Boolean> 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`
- [ ] **步骤 1import 块补充 `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实现时在现有基础上叠加不要回退已有改动。

View File

@@ -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 声明式 PipelineGroovy/ Maven / DockerLinux 构建机)
**规格:** `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 个 stageCheckout / 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 未就绪,健康检查会超时失败并打印容器日志。

View File

@@ -0,0 +1,252 @@
# TransportTaskApi RPC 查询接口 实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development推荐或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 补全 `TransportTaskApi` 两个 todo——根据 taskId / taskCode 查询任务,返回 `TaskInfoDTO`,自动过滤逻辑删除。
**架构:** ApiFeign GET→ ApiImpl委托→ TransportTaskService查询 + MapStruct 转换,查不到返回 null→ TransportTaskMapperselectById / selectByTaskCode`@TableLogic` 自动滤删)。新建 `TransportTaskConvert`MapStruct字段同名零配置
**技术栈:** Java 17 / Spring Boot / MyBatis-PlusBaseMapperX/ 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<TaskInfoDTO> getTaskById(@RequestParam("taskId") Long taskId);
@GetMapping(PREFIX + "/getTaskByCode")
@Operation(summary = "根据 taskCode 查询任务LMS/WMS 调用)")
CommonResult<TaskInfoDTO> 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<TaskInfoDTO> getTaskById(Long taskId) {
return success(transportTaskService.getTaskInfoById(taskId));
}
@Override
public CommonResult<TaskInfoDTO> 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 "featTransportTaskApi 新增按 taskId/taskCode 查询任务 RPC 接口"
```
---
## 风险与注意事项
1. **工作区有未提交改动**`TransportTaskService(Impl)``TransportTaskMapper` 均已有本地改动(含上一需求的路由表代码),实现时在现有基础上叠加,不要回退。
2. **MapStruct 与 Lombok 顺序**:模块继承的编译配置已被 infra 等模块的 Convert 验证可用,若编译报 MapStruct 处理器缺失,检查是否误改了 pom本计划不改 pom
3. **`TransportTaskServiceImplTest`**test 目录引用了 `TransportTaskApi``compile` 阶段不编译测试代码,不受影响。