Files
huachuang/docs/superpowers/plans/2026-07-15-task-operation-refactor.md
2026-07-20 13:49:32 +08:00

698 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 搬运任务操作路由表重构 + 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.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实现时在现有基础上叠加不要回退已有改动。