diff --git a/backend/src/main/java/com/designhub/controller/AdminTaskController.java b/backend/src/main/java/com/designhub/controller/AdminTaskController.java index 8ef6000..7fa93ed 100644 --- a/backend/src/main/java/com/designhub/controller/AdminTaskController.java +++ b/backend/src/main/java/com/designhub/controller/AdminTaskController.java @@ -138,6 +138,23 @@ public class AdminTaskController { return Result.success(result); } + @Operation(summary = "任务节点调拨到指定接单部门") + @PostMapping("/items/redirect-dept") + public Result> redirectTaskItemToDept( + @RequestBody RedirectTaskItemDeptRequest request, + @RequestHeader("Authorization") String authHeader + ) { + String token = jwtUtil.extractTokenFromHeader(authHeader); + Long adminId = jwtUtil.getUserIdFromToken(token); + Map result = adminTaskService.redirectTaskItemToDept( + request.getItemNo(), + request.getTargetDeptId(), + adminId, + request.getReason() + ); + return Result.success(result); + } + // DTO类 public static class TransferRequest { private Long newDesignerId; @@ -166,6 +183,13 @@ public class AdminTaskController { public void setReason(String reason) { this.reason = reason; } } + @Data + public static class RedirectTaskItemDeptRequest { + private String itemNo; + private Long targetDeptId; + private String reason; + } + // ==================== 任务排队解锁相关接口 ==================== @Operation(summary = "获取任务解锁状态统计") @@ -202,4 +226,3 @@ public class AdminTaskController { return Result.success(result); } } - diff --git a/backend/src/main/java/com/designhub/service/AdminTaskService.java b/backend/src/main/java/com/designhub/service/AdminTaskService.java index 2df2cd1..3d77962 100644 --- a/backend/src/main/java/com/designhub/service/AdminTaskService.java +++ b/backend/src/main/java/com/designhub/service/AdminTaskService.java @@ -4,13 +4,16 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.designhub.common.ResultCode; import com.designhub.entity.Department; import com.designhub.entity.Task; +import com.designhub.entity.TaskTransferLog; import com.designhub.entity.User; import com.designhub.exception.BusinessException; +import com.designhub.mapper.TaskTransferLogMapper; import com.designhub.mapper.TaskMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.time.LocalDateTime; @@ -37,6 +40,8 @@ public class AdminTaskService { private final DepartmentService departmentService; private final RoleService roleService; private final WorkHoursCalculator workHoursCalculator; + private final TaskTransferLogMapper taskTransferLogMapper; + private final TaskUnlockService taskUnlockService; /** * 管理员分配任务 @@ -400,6 +405,119 @@ public class AdminTaskService { return task; } + /** + * 管理员将子任务直接切换到指定接单部门可接单 + */ + @Transactional(rollbackFor = Exception.class) + public Map redirectTaskItemToDept(String itemNo, Long targetDeptId, Long adminId, String reason) { + if (!isAdminUser(adminId)) { + throw new BusinessException(ResultCode.FORBIDDEN, "只有管理员可以操作任务节点调拨"); + } + if (!StringUtils.hasText(itemNo)) { + throw new BusinessException(ResultCode.PARAM_ERROR, "子任务编号不能为空"); + } + if (targetDeptId == null) { + throw new BusinessException(ResultCode.PARAM_ERROR, "目标部门不能为空"); + } + + TaskItem item = taskItemMapper.selectOne( + new LambdaQueryWrapper() + .eq(TaskItem::getItemNo, itemNo.trim()) + .last("LIMIT 1") + ); + if (item == null) { + throw new BusinessException(ResultCode.NOT_FOUND, "子任务不存在"); + } + + Task task = taskMapper.selectById(item.getTaskId()); + if (task == null || task.getDeleted() != null && task.getDeleted() == 1) { + throw new BusinessException(ResultCode.NOT_FOUND, "主任务不存在"); + } + + Department targetDept = departmentService.getDepartmentById(targetDeptId); + if (targetDept == null) { + throw new BusinessException(ResultCode.PARAM_ERROR, "目标部门不存在"); + } + if (targetDept.getStatus() == null || targetDept.getStatus() != 1) { + throw new BusinessException(ResultCode.PARAM_ERROR, "目标部门已禁用"); + } + if (!"receiving".equals(targetDept.getDeptType())) { + throw new BusinessException(ResultCode.PARAM_ERROR, "目标部门必须是接单部门"); + } + + if ("completed".equals(item.getStatus())) { + throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "已完成子任务不允许调拨"); + } + if ("in_progress".equals(item.getStatus())) { + throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "进行中子任务请先转移接单人"); + } + + Long oldDeptId = item.getReceiverDeptId(); + Long oldReceiverId = item.getReceiverId(); + LocalDateTime oldDeadline = item.getDeadline(); + String oldStatus = item.getStatus(); + + item.setStatus("pending"); + item.setReceiverDeptId(targetDeptId); + item.setReceiverId(null); + item.setReceiverName(null); + item.setClaimTime(null); + item.setDeadline(null); + item.setIsLocked(0); + item.setUpdateTime(LocalDateTime.now()); + taskItemMapper.updateById(item); + + // 排队记录重建到新部门,确保可见且可抢 + taskUnlockService.deleteUnlockStatus(item.getId()); + if (taskUnlockService.isUnlockEnabled() && !"assigned".equals(task.getReleaseType())) { + taskUnlockService.onPipelineUnlock(item, targetDeptId); + } + + String transferReason = StringUtils.hasText(reason) ? reason.trim() : "管理员手动调拨子任务到指定接单部门"; + TaskTransferLog transferLog = new TaskTransferLog(); + transferLog.setTaskId(task.getId()); + transferLog.setTransferType("transfer"); + transferLog.setFromUserId(oldReceiverId); + transferLog.setToUserId(null); + transferLog.setOperatorId(adminId); + transferLog.setOldDeadline(oldDeadline); + transferLog.setNewDeadline(null); + transferLog.setOldPriority(task.getPriority()); + transferLog.setNewPriority(task.getPriority()); + transferLog.setReason(transferReason); + transferLog.setRemark(String.format("节点调拨:%s(%s)->%s(%s), itemNo=%s, oldStatus=%s", + oldDeptId == null ? "-" : oldDeptId, + resolveDeptName(oldDeptId), + targetDeptId, + targetDept.getDeptName(), + item.getItemNo(), + oldStatus + )); + transferLog.setCreateTime(LocalDateTime.now()); + taskTransferLogMapper.insert(transferLog); + + Map result = new HashMap<>(); + result.put("taskId", task.getId()); + result.put("taskNo", task.getTaskNo()); + result.put("itemId", item.getId()); + result.put("itemNo", item.getItemNo()); + result.put("oldStatus", oldStatus); + result.put("newStatus", item.getStatus()); + result.put("oldDeptId", oldDeptId); + result.put("oldDeptName", resolveDeptName(oldDeptId)); + result.put("newDeptId", targetDeptId); + result.put("newDeptName", targetDept.getDeptName()); + return result; + } + + private String resolveDeptName(Long deptId) { + if (deptId == null) { + return ""; + } + Department department = departmentService.getDepartmentById(deptId); + return department != null ? department.getDeptName() : ""; + } + /** * 获取优先级文本 */ @@ -517,4 +635,3 @@ public class AdminTaskService { return result; } } - diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts index c18d037..a5be189 100644 --- a/frontend/src/api/admin.ts +++ b/frontend/src/api/admin.ts @@ -71,3 +71,13 @@ export const cancelTask = (taskId: number, reason: string) => { }) } +/** + * 任务节点调拨到指定接单部门 + */ +export const redirectTaskItemToDept = (itemNo: string, targetDeptId: number, reason?: string) => { + return request.post>('/admin/tasks/items/redirect-dept', { + itemNo, + targetDeptId, + reason + }) +} diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index a7ad741..6a8ebd5 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -2037,6 +2037,78 @@ /> + + + + + + + + + + + + + + + {{ dept.deptName }} + + + + + + + + + 执行调拨 + + + + + + + + + + { } } +const handleRedirectTaskItemToDept = async () => { + const itemNo = taskItemDispatchForm.itemNo.trim() + if (!itemNo) { + message.warning('请输入子任务编号') + return + } + if (!taskItemDispatchForm.targetDeptId) { + message.warning('请选择目标接单部门') + return + } + + redirectingTaskItem.value = true + try { + const response = await redirectTaskItemToDept( + itemNo, + taskItemDispatchForm.targetDeptId, + taskItemDispatchForm.reason?.trim() || undefined + ) + const data = response?.data || {} + const deptName = data.newDeptName || '目标部门' + message.success(`调拨成功:${itemNo} 已切换到 ${deptName} 可接单`) + taskItemDispatchForm.itemNo = '' + taskItemDispatchForm.reason = '' + } catch (error: any) { + message.error(error?.response?.data?.message || error?.message || '调拨失败') + } finally { + redirectingTaskItem.value = false + } +} + // 第三方数据库配置相关 const thirdPartyDbConfigs = ref([]) const loadingThirdPartyDb = ref(false)