From 939fba85ffb37d25f61b24c1bb55e8b82d2278e7 Mon Sep 17 00:00:00 2001 From: let5sne Date: Fri, 3 Jul 2026 21:45:38 +0800 Subject: [PATCH] feat(aihr): support cancelling running local import tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 POST /api/knowledge/doc/import-tasks/{id}/cancel,后台目录导入任务 运行中可取消;资料处理页任务列表接取消按钮与状态刷新。 --- .../aihr/controller/AihrSopController.java | 6 +++ .../aihr/service/AihrSopSeedService.java | 42 ++++++++++++++++++- backend/script/sql/aihr_knowledge_mysql8.sql | 2 +- frontend/src/api/aihr/processing.ts | 7 ++++ frontend/src/api/aihr/sop.ts | 1 + frontend/src/views/knowledge/processing.vue | 29 ++++++++++++- 6 files changed, 83 insertions(+), 4 deletions(-) diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java index 38ee22a3..b4e58ace 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java @@ -13,6 +13,7 @@ import org.dromara.aihr.domain.AihrSopDto.VectorizeResponse; import org.dromara.aihr.service.AihrSopSeedService; import org.dromara.common.core.domain.R; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestPart; @@ -73,6 +74,11 @@ public class AihrSopController { return R.ok(sopSeedService.localImportTasks()); } + @PostMapping("/doc/import-tasks/{id}/cancel") + public R cancelLocalImportTask(@PathVariable Long id) { + return R.ok(sopSeedService.cancelLocalImportTask(id)); + } + @GetMapping("/processing/overview") public R processingOverview() { return R.ok(sopSeedService.processingOverview()); diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java index aee158d9..3d8b9950 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java @@ -253,6 +253,18 @@ public class AihrSopSeedService { } } + public LocalImportTaskResponse cancelLocalImportTask(Long taskId) { + if (taskId == null) { + throw new IllegalArgumentException("任务 ID 不能为空"); + } + jdbcTemplate.update(""" + update aihr_knowledge_import_task + set status = 4, current_file = '', finished_time = now(), update_time = now(), remark = '用户已取消' + where tenant_id = ? and id = ? and status = 1 + """, TENANT_ID, taskId); + return localImportTask(taskId).orElseThrow(() -> new IllegalArgumentException("导入任务不存在")); + } + private void runLocalImportTask(long taskId, LocalImportPlan plan) { try { if (plan.files().isEmpty()) { @@ -260,10 +272,16 @@ public class AihrSopSeedService { return; } LocalImportRun run = executeLocalImport(plan, taskId); + if (isLocalImportTaskCanceled(taskId)) { + return; + } boolean allFailed = run.failed() > 0 && run.imported() == 0; String remark = run.failed() == 0 ? "导入完成" : allFailed ? "全部文件导入失败,可重试同一目录" : "部分文件导入失败,可重试同一目录"; finishLocalImportTask(taskId, allFailed ? 3 : 2, run.imported(), run.failed(), remark); } catch (Exception e) { + if (isLocalImportTaskCanceled(taskId)) { + return; + } finishLocalImportTask(taskId, 3, 0, 0, e.getMessage()); } } @@ -273,6 +291,9 @@ public class AihrSopSeedService { int imported = 0; int failed = 0; for (Path path : plan.files()) { + if (taskId != null && isLocalImportTaskCanceled(taskId)) { + break; + } String fileName = path.getFileName().toString(); if (taskId != null) { updateLocalImportTask(taskId, 1, imported, failed, fileName, "处理中"); @@ -375,7 +396,7 @@ public class AihrSopSeedService { jdbcTemplate.update(""" update aihr_knowledge_import_task set status = ?, imported_files = ?, failed_files = ?, current_file = ?, update_time = now(), remark = ? - where tenant_id = ? and id = ? + where tenant_id = ? and id = ? and status <> 4 """, status, imported, failed, currentFile, limitRemark(remark), TENANT_ID, taskId); } @@ -383,10 +404,23 @@ public class AihrSopSeedService { jdbcTemplate.update(""" update aihr_knowledge_import_task set status = ?, imported_files = ?, failed_files = ?, current_file = '', finished_time = now(), update_time = now(), remark = ? - where tenant_id = ? and id = ? + where tenant_id = ? and id = ? and status <> 4 """, status, imported, failed, limitRemark(remark), TENANT_ID, taskId); } + private boolean isLocalImportTaskCanceled(long taskId) { + try { + Integer count = jdbcTemplate.queryForObject(""" + select count(1) + from aihr_knowledge_import_task + where tenant_id = ? and id = ? and status = 4 + """, Integer.class, TENANT_ID, taskId); + return count != null && count > 0; + } catch (DataAccessException e) { + return false; + } + } + private LocalImportTaskResponse importTaskResponse(long id, String directory, String category, int status, int total, int imported, int failed, String currentFile, Timestamp createTime, Timestamp updateTime, String remark) { return new LocalImportTaskResponse( id, @@ -1121,6 +1155,7 @@ public class AihrSopSeedService { case 1 -> "导入中"; case 2 -> failed > 0 ? "部分失败" : "已完成"; case 3 -> "失败"; + case 4 -> "已取消"; default -> "等待中"; }; } @@ -1132,6 +1167,9 @@ public class AihrSopSeedService { if (status == 2 && failed == 0) { return "success"; } + if (status == 4) { + return "info"; + } return status == 3 ? "danger" : "warning"; } diff --git a/backend/script/sql/aihr_knowledge_mysql8.sql b/backend/script/sql/aihr_knowledge_mysql8.sql index dc73fd7f..e22b54e1 100644 --- a/backend/script/sql/aihr_knowledge_mysql8.sql +++ b/backend/script/sql/aihr_knowledge_mysql8.sql @@ -73,7 +73,7 @@ CREATE TABLE IF NOT EXISTS `aihr_knowledge_import_task` ( `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', `directory` varchar(500) NOT NULL COMMENT '导入相对目录', `category` varchar(100) NOT NULL COMMENT '知识库分类', - `status` tinyint DEFAULT 0 COMMENT '任务状态: 0等待, 1运行中, 2完成, 3失败', + `status` tinyint DEFAULT 0 COMMENT '任务状态: 0等待, 1运行中, 2完成, 3失败, 4取消', `total_files` int DEFAULT 0 COMMENT '发现文件数', `imported_files` int DEFAULT 0 COMMENT '已导入文件数', `failed_files` int DEFAULT 0 COMMENT '失败文件数', diff --git a/frontend/src/api/aihr/processing.ts b/frontend/src/api/aihr/processing.ts index 0befe2ef..5fd46acd 100644 --- a/frontend/src/api/aihr/processing.ts +++ b/frontend/src/api/aihr/processing.ts @@ -135,3 +135,10 @@ export function listLocalKnowledgeImportTasks(): Promise> { + return request({ + url: `/api/knowledge/doc/import-tasks/${id}/cancel`, + method: 'post' + }); +} diff --git a/frontend/src/api/aihr/sop.ts b/frontend/src/api/aihr/sop.ts index c0a70a58..46af609d 100644 --- a/frontend/src/api/aihr/sop.ts +++ b/frontend/src/api/aihr/sop.ts @@ -63,6 +63,7 @@ export function uploadKnowledgeDoc(data: FormData): Promise - + @@ -237,6 +238,7 @@ import { import { ElMessage, ElMessageBox } from 'element-plus'; import { computed, onMounted, onUnmounted, ref } from 'vue'; import { + cancelLocalKnowledgeImportTask, getProcessingOverview, listLocalKnowledgeImportTasks, startLocalKnowledgeImportTask, @@ -474,6 +476,26 @@ async function retryImportTask(row: LocalImportTask) { } } +async function cancelImportTask(row: LocalImportTask) { + try { + await ElMessageBox.confirm(`确认取消导入任务「${row.directory}」?`, '取消导入', { + confirmButtonText: '取消任务', + cancelButtonText: '返回', + type: 'warning' + }); + } catch { + return; + } + importTaskLoading.value = true; + try { + await cancelLocalKnowledgeImportTask(row.id); + ElMessage.success('已取消导入任务'); + await Promise.all([loadImportTasks(), loadOverview()]); + } finally { + importTaskLoading.value = false; + } +} + function scheduleImportPoll() { clearImportPoll(); if (!runningImport.value) return; @@ -928,6 +950,11 @@ function fileBadgeClass(type: string) { background: #ffe8ea; } +.pill-info { + color: #657184; + background: #eef2f7; +} + .table-footer { display: flex; justify-content: space-between;