From 088ba915ca3b68964ce1c7f4a3f73ae02b27d5f1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 12 Mar 2026 10:26:37 +0800 Subject: [PATCH] feat: emergency unlock queued tasks to hall --- .../controller/AdminTaskController.java | 22 ++ .../controller/OpenTaskController.java | 58 ++++- .../mapper/TaskUnlockStatusMapper.java | 8 + .../designhub/service/AdminTaskService.java | 22 ++ .../service/TaskApprovalService.java | 10 + .../designhub/service/TaskItemService.java | 40 +++ .../designhub/service/TaskUnlockService.java | 236 ++++++++++++++++-- frontend/src/views/Settings.vue | 105 ++++++++ 8 files changed, 478 insertions(+), 23 deletions(-) diff --git a/backend/src/main/java/com/designhub/controller/AdminTaskController.java b/backend/src/main/java/com/designhub/controller/AdminTaskController.java index 7fa93ed..9f6e469 100644 --- a/backend/src/main/java/com/designhub/controller/AdminTaskController.java +++ b/backend/src/main/java/com/designhub/controller/AdminTaskController.java @@ -225,4 +225,26 @@ public class AdminTaskController { return Result.success(result); } + + @Operation(summary = "应急解锁排队任务到大厅", description = "管理员可将排队中的任务(主任务编号/子任务编号)直接解锁为可抢单状态") + @PostMapping("/unlock/manual") + public Result> emergencyUnlockToHall( + @RequestBody EmergencyUnlockRequest request, + @RequestHeader("Authorization") String authHeader + ) { + String token = jwtUtil.extractTokenFromHeader(authHeader); + Long adminId = jwtUtil.getUserIdFromToken(token); + Map result = adminTaskService.emergencyUnlockQueuedTaskToHall( + request == null ? null : request.getTaskNoOrItemNo(), + adminId, + request == null ? null : request.getReason() + ); + return Result.success(result); + } + + @Data + public static class EmergencyUnlockRequest { + private String taskNoOrItemNo; + private String reason; + } } diff --git a/backend/src/main/java/com/designhub/controller/OpenTaskController.java b/backend/src/main/java/com/designhub/controller/OpenTaskController.java index de52071..ae3bde4 100644 --- a/backend/src/main/java/com/designhub/controller/OpenTaskController.java +++ b/backend/src/main/java/com/designhub/controller/OpenTaskController.java @@ -2,6 +2,7 @@ package com.designhub.controller; import com.designhub.common.Result; import com.designhub.service.SystemConfigService; +import com.designhub.service.TaskUnlockService; import com.designhub.service.TaskService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -39,9 +40,10 @@ public class OpenTaskController { private static final String KEY_IP_WHITELIST = "openapi.task_delete_ip_whitelist"; private final TaskService taskService; + private final TaskUnlockService taskUnlockService; private final SystemConfigService systemConfigService; - @Operation(summary = "开放接口删除任务", description = "供外部机器人按任务编号删除任务") + @Operation(summary = "开放接口删除/解锁任务", description = "供外部机器人按任务编号删除任务或解锁排队任务到大厅") @PostMapping("/delete") public Result> deleteTaskByTaskNo( @RequestBody OpenDeleteTaskRequest requestBody, @@ -83,22 +85,49 @@ public class OpenTaskController { return Result.error("签名已过期"); } - String payload = requestBody.getAppKey() + "|" + requestBody.getTaskNo().trim() + "|" + requestBody.getTimestamp(); - String expectedSign = hmacSha256Hex(configuredSecret, payload); - if (!secureEquals(expectedSign, requestBody.getSign().trim().toLowerCase(Locale.ROOT))) { + String action = normalizeAction(requestBody.getAction()); + String sign = requestBody.getSign().trim().toLowerCase(Locale.ROOT); + + // v1签名: appKey|taskNo|timestamp (兼容旧机器人,仅支持 delete) + String payloadV1 = requestBody.getAppKey() + "|" + requestBody.getTaskNo().trim() + "|" + requestBody.getTimestamp(); + String expectedSignV1 = hmacSha256Hex(configuredSecret, payloadV1); + + // v2签名: appKey|taskNo|action|timestamp (推荐,新功能 unlock 必须用 v2) + String payloadV2 = requestBody.getAppKey() + "|" + requestBody.getTaskNo().trim() + "|" + action + "|" + requestBody.getTimestamp(); + String expectedSignV2 = hmacSha256Hex(configuredSecret, payloadV2); + + boolean signOk; + if ("unlock".equals(action)) { + // unlock 必须包含 action 参与签名,防止篡改 action + signOk = secureEquals(expectedSignV2, sign); + } else { + // delete 兼容 v1/v2 + signOk = secureEquals(expectedSignV1, sign) || secureEquals(expectedSignV2, sign); + } + + if (!signOk) { log.warn("开放删除接口签名不匹配: traceId={}, ip={}, taskNo={}", traceId, clientIp, requestBody.getTaskNo()); return Result.error("签名校验失败"); } - taskService.deleteTaskByTaskNo(requestBody.getTaskNo().trim()); + Map actionResult; + if ("unlock".equals(action)) { + actionResult = taskUnlockService.emergencyUnlockToHallByTaskNo(requestBody.getTaskNo().trim()); + } else { + taskService.deleteTaskByTaskNo(requestBody.getTaskNo().trim()); + actionResult = new LinkedHashMap<>(); + actionResult.put("deleted", true); + } Map result = new LinkedHashMap<>(); result.put("taskNo", requestBody.getTaskNo().trim()); - result.put("deleted", true); + result.put("action", action); + result.putAll(actionResult); result.put("traceId", traceId); result.put("time", Instant.now().toString()); - log.info("开放删除接口执行成功: traceId={}, ip={}, taskNo={}", traceId, clientIp, requestBody.getTaskNo()); + log.info("开放任务接口执行成功: traceId={}, ip={}, action={}, taskNo={}", + traceId, clientIp, action, requestBody.getTaskNo()); return Result.success(result); } catch (IllegalArgumentException e) { return Result.error(e.getMessage()); @@ -174,11 +203,26 @@ public class OpenTaskController { return MessageDigest.isEqual(aa, bb); } + private String normalizeAction(String action) { + String value = action == null ? "" : action.trim().toLowerCase(Locale.ROOT); + if (!StringUtils.hasText(value) || "delete".equals(value)) { + return "delete"; + } + if ("unlock".equals(value)) { + return "unlock"; + } + throw new IllegalArgumentException("action参数错误,仅支持 delete/unlock"); + } + @Data public static class OpenDeleteTaskRequest { private String appKey; private String taskNo; private String timestamp; private String sign; + /** + * delete(默认) / unlock(解锁排队任务到大厅) + */ + private String action; } } diff --git a/backend/src/main/java/com/designhub/mapper/TaskUnlockStatusMapper.java b/backend/src/main/java/com/designhub/mapper/TaskUnlockStatusMapper.java index 6ea67df..c677f92 100644 --- a/backend/src/main/java/com/designhub/mapper/TaskUnlockStatusMapper.java +++ b/backend/src/main/java/com/designhub/mapper/TaskUnlockStatusMapper.java @@ -41,6 +41,14 @@ public interface TaskUnlockStatusMapper extends BaseMapper { "WHERE tus.dept_id = #{deptId} AND tus.is_unlocked = 1 AND ti.status = 'pending'") int countUnlockedPendingTasks(@Param("deptId") Long deptId); + /** + * 获取指定部门排队中且当前待接单的任务数 + */ + @Select("SELECT COUNT(*) FROM task_unlock_status tus " + + "INNER JOIN task_item ti ON tus.task_item_id = ti.id " + + "WHERE tus.dept_id = #{deptId} AND tus.is_unlocked = 0 AND ti.status = 'pending'") + int countQueuedPendingTasks(@Param("deptId") Long deptId); + /** * 批量解锁任务 */ diff --git a/backend/src/main/java/com/designhub/service/AdminTaskService.java b/backend/src/main/java/com/designhub/service/AdminTaskService.java index 3d77962..6b6a58f 100644 --- a/backend/src/main/java/com/designhub/service/AdminTaskService.java +++ b/backend/src/main/java/com/designhub/service/AdminTaskService.java @@ -43,6 +43,28 @@ public class AdminTaskService { private final TaskTransferLogMapper taskTransferLogMapper; private final TaskUnlockService taskUnlockService; + /** + * 应急解锁排队任务到大厅(支持主任务编号/子任务编号) + */ + @Transactional(rollbackFor = Exception.class) + public Map emergencyUnlockQueuedTaskToHall(String taskNoOrItemNo, Long adminId, String reason) { + if (!isAdminUser(adminId)) { + throw new BusinessException(ResultCode.FORBIDDEN, "只有管理员可以解锁排队任务"); + } + if (!StringUtils.hasText(taskNoOrItemNo)) { + throw new BusinessException(ResultCode.PARAM_ERROR, "任务编号不能为空"); + } + + Map unlockResult = taskUnlockService.emergencyUnlockToHall(taskNoOrItemNo.trim()); + log.info("管理员应急解锁到大厅: adminId={}, key={}, reason={}, result={}", + adminId, + taskNoOrItemNo, + StringUtils.hasText(reason) ? reason.trim() : "", + unlockResult + ); + return unlockResult; + } + /** * 管理员分配任务 */ diff --git a/backend/src/main/java/com/designhub/service/TaskApprovalService.java b/backend/src/main/java/com/designhub/service/TaskApprovalService.java index 083dd33..81e23de 100644 --- a/backend/src/main/java/com/designhub/service/TaskApprovalService.java +++ b/backend/src/main/java/com/designhub/service/TaskApprovalService.java @@ -48,6 +48,7 @@ public class TaskApprovalService { private final ObjectMapper objectMapper; private final DingTalkService dingTalkService; private final com.designhub.util.WorkHoursCalculator workHoursCalculator; + private final TaskUnlockService taskUnlockService; // ==================== 一级需求部门任务审批 ==================== @@ -524,6 +525,15 @@ public class TaskApprovalService { task.setCurrentApproverRole(null); taskMapper.updateById(task); + if (task.getIsPipeline() != null && task.getIsPipeline() == 1 && finalReceiverId == null) { + List pipelineItems = taskItemMapper.selectList( + new LambdaQueryWrapper() + .eq(TaskItem::getTaskId, task.getId()) + ); + taskUnlockService.onTaskApproved(task, pipelineItems); + log.info("任务审核通过后已写入排队池: taskId={}, items={}", task.getId(), pipelineItems.size()); + } + // 扣除部门积分 try { teamScorePoolService.deductScore(task.getPublisherDeptId(), task.getScore() != null ? task.getScore().intValue() : 0); diff --git a/backend/src/main/java/com/designhub/service/TaskItemService.java b/backend/src/main/java/com/designhub/service/TaskItemService.java index 56e0b73..ee05917 100644 --- a/backend/src/main/java/com/designhub/service/TaskItemService.java +++ b/backend/src/main/java/com/designhub/service/TaskItemService.java @@ -31,6 +31,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; @RequiredArgsConstructor public class TaskItemService { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TaskItemService.class); + private static final Long GRAPHIC_VISUAL_DEPT_ID = 109L; + private static final Long AI_VISUAL_DEPT_ID = 117L; private final TaskItemMapper taskItemMapper; private final TaskMapper taskMapper; @@ -279,6 +281,9 @@ public class TaskItemService { if (userDeptId == null || targetDeptId == null) { return false; } + if (isSharedGrabDept(userDeptId, targetDeptId)) { + return true; + } Long currentDeptId = userDeptId; while (currentDeptId != null) { if (targetDeptId.equals(currentDeptId)) { @@ -294,6 +299,27 @@ public class TaskItemService { return false; } + private boolean isSharedGrabDept(Long userDeptId, Long targetDeptId) { + return isDeptInTree(userDeptId, AI_VISUAL_DEPT_ID) + && isDeptInTree(targetDeptId, GRAPHIC_VISUAL_DEPT_ID); + } + + private boolean isDeptInTree(Long deptId, Long rootDeptId) { + Long currentDeptId = deptId; + while (currentDeptId != null) { + if (rootDeptId.equals(currentDeptId)) { + return true; + } + com.designhub.entity.Department currentDept = departmentMapper.selectById(currentDeptId); + if (currentDept == null || currentDept.getParentId() == null + || currentDeptId.equals(currentDept.getParentId())) { + break; + } + currentDeptId = currentDept.getParentId(); + } + return false; + } + /** * 获取进行中的子任务列表 * @param receiverId 接单人ID,null表示不按接单人过滤 @@ -940,6 +966,7 @@ public class TaskItemService { nextItem.setPrevDownloadUrl(prevDownloadUrlStr); nextItem.setUpdateTime(now); taskItemMapper.updateById(nextItem); + markPipelineItemUnlocked(nextItem); log.info("✅ 解锁预先指定的子任务: {} -> 设计师 {} (ID: {}),状态设为待接单", nextItem.getItemNo(), nextItem.getReceiverName(), nextItem.getReceiverId()); @@ -972,6 +999,7 @@ public class TaskItemService { nextItem.setPrevDownloadUrl(prevDownloadUrlStr); nextItem.setUpdateTime(now); taskItemMapper.updateById(nextItem); + markPipelineItemUnlocked(nextItem); log.info("✅ 解锁未指定的子任务: {} (部门序号: {}, ID: {}),待接单部门负责人审核", nextItem.getItemNo(), nextDeptIndex, nextItem.getId()); @@ -1997,6 +2025,18 @@ public class TaskItemService { log.debug("未找到配置的部门,返回 null"); return null; } + + private void markPipelineItemUnlocked(TaskItem item) { + Long targetDeptId = item.getReceiverDeptId(); + if (targetDeptId == null) { + targetDeptId = getTargetDeptIdFromTaskType(item.getTaskTypeId()); + } + if (targetDeptId == null) { + log.warn("流水线子任务 {} 无法确定接单部门,跳过已解锁标记", item.getItemNo()); + return; + } + taskUnlockService.onPipelineUnlock(item, targetDeptId); + } /** * 格式化工时显示(基于工作时间配置) diff --git a/backend/src/main/java/com/designhub/service/TaskUnlockService.java b/backend/src/main/java/com/designhub/service/TaskUnlockService.java index e3f5733..e415b22 100644 --- a/backend/src/main/java/com/designhub/service/TaskUnlockService.java +++ b/backend/src/main/java/com/designhub/service/TaskUnlockService.java @@ -1,6 +1,7 @@ package com.designhub.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.designhub.entity.*; import com.designhub.mapper.*; import lombok.extern.slf4j.Slf4j; @@ -172,6 +173,214 @@ public class TaskUnlockService { return unlocked; } + // ==================== 应急解锁(管理员/机器人调用) ==================== + + /** + * 应急解锁:将指定任务(主任务编号/子任务编号)从排队中直接解锁到大厅 + *

+ * - 如果传入的是子任务编号(itemNo),仅解锁该子任务(仅限 pending) + * - 如果传入的是主任务编号(taskNo),解锁该主任务下所有 pending 子任务(仅解锁排队中的) + *

+ */ + @Transactional(rollbackFor = Exception.class) + public Map emergencyUnlockToHall(String taskNoOrItemNo) { + String key = taskNoOrItemNo == null ? "" : taskNoOrItemNo.trim(); + if (key.isEmpty()) { + throw new IllegalArgumentException("任务编号不能为空"); + } + + TaskItem item = taskItemMapper.selectOne( + new LambdaQueryWrapper() + .eq(TaskItem::getItemNo, key) + .last("LIMIT 1") + ); + if (item != null) { + return emergencyUnlockTaskItemToHall(item); + } + + Task task = taskMapper.selectOne( + new LambdaQueryWrapper() + .eq(Task::getTaskNo, key) + .last("LIMIT 1") + ); + if (task != null) { + return emergencyUnlockTaskToHall(task); + } + + throw new IllegalArgumentException("任务不存在,请检查编号是否正确"); + } + + /** + * 应急解锁:按主任务编号解锁 + */ + @Transactional(rollbackFor = Exception.class) + public Map emergencyUnlockToHallByTaskNo(String taskNo) { + String key = taskNo == null ? "" : taskNo.trim(); + if (key.isEmpty()) { + throw new IllegalArgumentException("任务编号不能为空"); + } + Task task = taskMapper.selectOne( + new LambdaQueryWrapper() + .eq(Task::getTaskNo, key) + .last("LIMIT 1") + ); + if (task == null) { + throw new IllegalArgumentException("任务不存在,请检查任务编号是否正确"); + } + return emergencyUnlockTaskToHall(task); + } + + private Map emergencyUnlockTaskItemToHall(TaskItem item) { + if (item.getTaskId() == null) { + throw new IllegalArgumentException("子任务缺少主任务ID,无法解锁"); + } + + Task task = taskMapper.selectById(item.getTaskId()); + if (task == null) { + throw new IllegalArgumentException("主任务不存在,无法解锁"); + } + + if ("assigned".equals(task.getReleaseType())) { + Map result = new HashMap<>(); + result.put("type", "item"); + result.put("taskNo", task.getTaskNo()); + result.put("itemNo", item.getItemNo()); + result.put("taskItemId", item.getId()); + result.put("unlockedItemCount", 0); + result.put("message", "指定任务不受排队限制,无需解锁"); + return result; + } + + if (!"pending".equals(item.getStatus())) { + throw new IllegalArgumentException("子任务状态不是待接单,无法解锁到大厅"); + } + + List queuedRows = unlockStatusMapper.selectList( + new LambdaQueryWrapper() + .eq(TaskUnlockStatus::getTaskItemId, item.getId()) + .eq(TaskUnlockStatus::getIsUnlocked, 0) + ); + if (queuedRows == null || queuedRows.isEmpty()) { + Map result = new HashMap<>(); + result.put("type", "item"); + result.put("taskNo", task.getTaskNo()); + result.put("itemNo", item.getItemNo()); + result.put("taskItemId", item.getId()); + result.put("unlockedItemCount", 0); + result.put("message", "该子任务不在排队中或已解锁"); + return result; + } + + LocalDateTime now = LocalDateTime.now(); + int affectedRows = unlockStatusMapper.update( + null, + new LambdaUpdateWrapper() + .eq(TaskUnlockStatus::getTaskItemId, item.getId()) + .eq(TaskUnlockStatus::getIsUnlocked, 0) + .set(TaskUnlockStatus::getIsUnlocked, 1) + .set(TaskUnlockStatus::getUnlockTime, now) + .set(TaskUnlockStatus::getUpdateTime, now) + ); + + try { + notifyUnlockedTasks(Collections.singletonList(item.getId())); + } catch (Exception e) { + log.error("发送应急解锁通知失败: itemId={}, error={}", item.getId(), e.getMessage()); + } + + Map result = new HashMap<>(); + result.put("type", "item"); + result.put("taskNo", task.getTaskNo()); + result.put("itemNo", item.getItemNo()); + result.put("taskItemId", item.getId()); + result.put("deptId", queuedRows.get(0).getDeptId()); + result.put("unlockedItemCount", affectedRows > 0 ? 1 : 0); + result.put("affectedRows", affectedRows); + result.put("message", affectedRows > 0 ? "已解锁到大厅" : "解锁失败或已被解锁"); + return result; + } + + private Map emergencyUnlockTaskToHall(Task task) { + if ("assigned".equals(task.getReleaseType())) { + Map result = new HashMap<>(); + result.put("type", "task"); + result.put("taskNo", task.getTaskNo()); + result.put("taskId", task.getId()); + result.put("unlockedItemCount", 0); + result.put("message", "指定任务不受排队限制,无需解锁"); + return result; + } + + List pendingItems = taskItemMapper.selectList( + new LambdaQueryWrapper() + .eq(TaskItem::getTaskId, task.getId()) + .eq(TaskItem::getStatus, "pending") + ); + + Map result = new HashMap<>(); + result.put("type", "task"); + result.put("taskNo", task.getTaskNo()); + result.put("taskId", task.getId()); + result.put("pendingItemCount", pendingItems == null ? 0 : pendingItems.size()); + + if (pendingItems == null || pendingItems.isEmpty()) { + result.put("unlockedItemCount", 0); + result.put("message", "主任务下没有待接单子任务,无需解锁"); + return result; + } + + List itemIds = pendingItems.stream() + .map(TaskItem::getId) + .filter(Objects::nonNull) + .toList(); + + if (itemIds.isEmpty()) { + result.put("unlockedItemCount", 0); + result.put("message", "子任务ID为空,无法解锁"); + return result; + } + + List queuedRows = unlockStatusMapper.selectList( + new LambdaQueryWrapper() + .in(TaskUnlockStatus::getTaskItemId, itemIds) + .eq(TaskUnlockStatus::getIsUnlocked, 0) + ); + + if (queuedRows == null || queuedRows.isEmpty()) { + result.put("unlockedItemCount", 0); + result.put("message", "该主任务下没有排队中的待接单子任务"); + return result; + } + + List queuedItemIds = queuedRows.stream() + .map(TaskUnlockStatus::getTaskItemId) + .filter(Objects::nonNull) + .distinct() + .toList(); + + LocalDateTime now = LocalDateTime.now(); + int affectedRows = unlockStatusMapper.update( + null, + new LambdaUpdateWrapper() + .in(TaskUnlockStatus::getTaskItemId, queuedItemIds) + .eq(TaskUnlockStatus::getIsUnlocked, 0) + .set(TaskUnlockStatus::getIsUnlocked, 1) + .set(TaskUnlockStatus::getUnlockTime, now) + .set(TaskUnlockStatus::getUpdateTime, now) + ); + + try { + notifyUnlockedTasks(queuedItemIds); + } catch (Exception e) { + log.error("发送应急解锁通知失败: taskNo={}, error={}", task.getTaskNo(), e.getMessage()); + } + + result.put("unlockedItemCount", queuedItemIds.size()); + result.put("affectedRows", affectedRows); + result.put("message", queuedItemIds.isEmpty() ? "无可解锁子任务" : "已解锁到大厅"); + return result; + } + /** * 任务被抢单后,检查并补充解锁 */ @@ -484,20 +693,6 @@ public class TaskUnlockService { */ public Map getUnlockStats() { Map stats = new HashMap<>(); - - // 总待解锁数 - long totalQueued = unlockStatusMapper.selectCount( - new LambdaQueryWrapper() - .eq(TaskUnlockStatus::getIsUnlocked, 0) - ); - stats.put("totalQueued", totalQueued); - - // 总已解锁数 - long totalUnlocked = unlockStatusMapper.selectCount( - new LambdaQueryWrapper() - .eq(TaskUnlockStatus::getIsUnlocked, 1) - ); - stats.put("totalUnlocked", totalUnlocked); // 按部门统计 List depts = departmentMapper.selectList( @@ -507,14 +702,23 @@ public class TaskUnlockService { ); List> deptStats = new ArrayList<>(); + long totalQueued = 0; + long totalUnlocked = 0; for (Department dept : depts) { Map deptStat = new HashMap<>(); + int queuedCount = unlockStatusMapper.countQueuedPendingTasks(dept.getId()); + int unlockedPendingCount = unlockStatusMapper.countUnlockedPendingTasks(dept.getId()); deptStat.put("deptId", dept.getId()); deptStat.put("deptName", dept.getDeptName()); - deptStat.put("queuedCount", unlockStatusMapper.countQueuedTasks(dept.getId())); - deptStat.put("unlockedPendingCount", unlockStatusMapper.countUnlockedPendingTasks(dept.getId())); + deptStat.put("queuedCount", queuedCount); + deptStat.put("unlockedPendingCount", unlockedPendingCount); deptStats.add(deptStat); + totalQueued += queuedCount; + totalUnlocked += unlockedPendingCount; } + + stats.put("totalQueued", totalQueued); + stats.put("totalUnlocked", totalUnlocked); stats.put("deptStats", deptStats); return stats; diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index 6a8ebd5..b114564 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -2006,6 +2006,64 @@ style="margin-top: 16px;" row-key="deptId" /> + + + 应急解锁 + + + + + + + + + + + + + + + + + + + + 解锁到大厅 + + + + + + + @@ -5787,6 +5845,11 @@ const taskUnlockConfig = reactive({ const savingTaskUnlockConfig = ref(false) const loadingTaskUnlockStats = ref(false) const taskUnlockStats = ref(null) +const taskUnlockManualForm = reactive({ + taskNoOrItemNo: '', + reason: '' +}) +const unlockingQueuedTask = ref(false) // 任务排队统计表格列 const taskUnlockStatsColumns = [ @@ -5855,6 +5918,48 @@ const loadTaskUnlockStats = async () => { } } +// 应急解锁到大厅 +const handleEmergencyUnlockToHall = async () => { + const key = taskUnlockManualForm.taskNoOrItemNo?.trim() + if (!key) { + message.warning('请输入任务编号或子任务编号') + return + } + + unlockingQueuedTask.value = true + try { + const response = await fetch('/api/admin/tasks/unlock/manual', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify({ + taskNoOrItemNo: key, + reason: taskUnlockManualForm.reason?.trim() || '' + }) + }) + const result = await response.json() + + if (result.code === 200) { + const data = result.data || {} + const unlockedCount = data.unlockedItemCount ?? 0 + message.success(data.message || `已解锁到大厅(解锁节点数:${unlockedCount})`) + taskUnlockManualForm.taskNoOrItemNo = '' + taskUnlockManualForm.reason = '' + // 刷新统计 + await loadTaskUnlockStats() + } else { + message.error(result.message || '解锁失败') + } + } catch (error: any) { + console.error('应急解锁失败:', error) + message.error(error.message || '解锁失败') + } finally { + unlockingQueuedTask.value = false + } +} + // 加载延误扣分配置 const loadDelayPenaltyConfig = async () => { try {