diff --git a/backend/src/main/java/com/designhub/controller/TaskController.java b/backend/src/main/java/com/designhub/controller/TaskController.java index 4539723..3a28974 100644 --- a/backend/src/main/java/com/designhub/controller/TaskController.java +++ b/backend/src/main/java/com/designhub/controller/TaskController.java @@ -476,6 +476,24 @@ public class TaskController { return Result.success("任务已驳回"); } + @Operation(summary = "撤回任务", description = "发起人撤回未被抢单的任务进行编辑,撤回后任务变为草稿状态") + @PostMapping("/{id}/withdraw") + public Result withdrawTask( + @PathVariable Long id, + @RequestHeader("Authorization") String authHeader) { + + log.info("撤回任务: taskId={}", id); + String token = jwtUtil.extractTokenFromHeader(authHeader); + Long userId = jwtUtil.getUserIdFromToken(token); + + try { + taskService.withdrawTask(id, userId); + return Result.success("任务已撤回,可以进行编辑"); + } catch (RuntimeException e) { + return Result.error(e.getMessage()); + } + } + @Operation(summary = "根据任务编号删除任务", description = "管理员根据任务编号删除任务及其所有子任务(仅管理员可用)") @DeleteMapping("/by-task-no/{taskNo}") public Result deleteTaskByTaskNo( diff --git a/backend/src/main/java/com/designhub/service/TaskService.java b/backend/src/main/java/com/designhub/service/TaskService.java index f7f05c3..697b724 100644 --- a/backend/src/main/java/com/designhub/service/TaskService.java +++ b/backend/src/main/java/com/designhub/service/TaskService.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.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.designhub.common.ResultCode; @@ -2134,6 +2135,93 @@ public class TaskService { log.info("任务 {} 评分成功: {} 星", taskId, rating); } + /** + * 撤回任务(用于编辑后重新发布) + * 只有任务发起人可以撤回,且任务必须未被抢单 + */ + @Transactional(rollbackFor = Exception.class) + public void withdrawTask(Long taskId, Long userId) { + log.info("用户 {} 撤回任务 {} 进行编辑", userId, taskId); + + Task task = getTaskById(taskId); + + // 验证权限(必须是任务发布人) + if (!userId.equals(task.getPublisherId())) { + throw new BusinessException(ResultCode.FORBIDDEN, "只有任务发布人可以撤回任务"); + } + + // 验证任务状态 + // 可以撤回的状态:pending(待接单)、pending_demand_approval(待需求部门审核)、pending_receiving_approval(待接单部门审核) + String status = task.getStatus(); + String approvalStatus = task.getApprovalStatus(); + + boolean canWithdraw = false; + boolean needRefundScore = false; // 是否需要返还积分 + + // 待接单状态可以撤回(已审核通过,需要返还积分) + if ("pending".equals(status)) { + canWithdraw = true; + needRefundScore = true; // pending状态说明已审核通过,积分已扣除,需要返还 + } + + // 待审核状态也可以撤回(未审核通过,不需要返还积分) + if (approvalStatus != null && (approvalStatus.contains("pending"))) { + canWithdraw = true; + // 待审核状态积分还未扣除,不需要返还 + } + + if (!canWithdraw) { + throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "只有待接单或待审核的任务可以撤回"); + } + + // 检查任务是否已被抢单(receiverId 不为空说明已被抢单) + if (task.getReceiverId() != null) { + throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "任务已被接单,无法撤回"); + } + + // 检查流水线任务的子任务是否有被抢单的 + if (task.getIsPipeline() != null && task.getIsPipeline() == 1) { + List items = taskItemMapper.selectList( + new LambdaQueryWrapper() + .eq(TaskItem::getTaskId, taskId) + .isNotNull(TaskItem::getReceiverId) + ); + if (!items.isEmpty()) { + throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "部分子任务已被接单,无法撤回"); + } + } + + // 返还积分(只有pending状态的任务需要返还,因为审核通过后才会扣除积分) + if (needRefundScore && task.getPublisherDeptId() != null && task.getScore() != null) { + int scoreToRefund = task.getScore().intValue(); + teamScorePoolService.addScore(task.getPublisherDeptId(), scoreToRefund); + log.info("任务撤回,返还部门积分: taskId={}, deptId={}, score={}", + taskId, task.getPublisherDeptId(), scoreToRefund); + } + + // 更新任务状态为草稿 + task.setStatus("draft"); + task.setApprovalStatus(null); // 清除审批状态 + task.setCurrentApproverId(null); + task.setCurrentApproverRole(null); + task.setAuditStatus("draft"); // 重置审核状态 + task.setUpdateTime(LocalDateTime.now()); + + taskMapper.updateById(task); + + // 如果是流水线任务,也需要重置子任务状态 + if (task.getIsPipeline() != null && task.getIsPipeline() == 1) { + taskItemMapper.update(null, + new LambdaUpdateWrapper() + .eq(TaskItem::getTaskId, taskId) + .set(TaskItem::getStatus, "pending") + .set(TaskItem::getUpdateTime, LocalDateTime.now()) + ); + } + + log.info("任务 {} 已撤回为草稿状态,可以进行编辑", taskId); + } + /** * 取消任务 */ diff --git a/frontend/src/api/task.ts b/frontend/src/api/task.ts index a5ebe86..754e89b 100644 --- a/frontend/src/api/task.ts +++ b/frontend/src/api/task.ts @@ -671,6 +671,16 @@ export const confirmTaskProductFilled = (taskId: number) => { } +// ==================== 任务撤回 ==================== + +/** + * 撤回任务(用于编辑后重新发布) + * 只有任务发起人可以撤回,且任务必须未被抢单 + */ +export const withdrawTask = (taskId: number) => { + return request.post(`/tasks/${taskId}/withdraw`) +} + // ==================== 管理员任务管理 ==================== /** diff --git a/frontend/src/views/TaskHall.vue b/frontend/src/views/TaskHall.vue index ff784b2..fb53c08 100644 --- a/frontend/src/views/TaskHall.vue +++ b/frontend/src/views/TaskHall.vue @@ -1452,6 +1452,19 @@ + +
+ + + 撤回编辑 + +
+
(null) const userDeptName = ref('') const userDeptType = ref('') const exportLoading = ref(false) // 导出加载状态 +const withdrawingTaskId = ref(null) // 正在撤回的任务ID // 获取当前有效的部门类型(根据选择的视角) const effectiveDeptType = computed(() => { @@ -5033,6 +5047,77 @@ const handleDownloadPrevAssets = (task: TaskOrSubTask) => { } } +// 判断任务是否可以撤回 +const canWithdrawTask = (task: Task) => { + // 已被驳回或草稿状态的任务不显示撤回按钮(它们有自己的编辑按钮) + if (['rejected', 'draft'].includes(task.status) || task.approvalStatus?.includes('rejected')) { + return false + } + + // 已完成、已取消的任务不能撤回 + if (['completed', 'cancelled'].includes(task.status)) { + return false + } + + // 已被抢单的任务不能撤回(receiverId 不为空) + if (task.receiverId) { + return false + } + + // 进行中的任务不能撤回 + if (task.status === 'in_progress') { + return false + } + + // 待接单或待审核的任务可以撤回 + if (task.status === 'pending') { + return true + } + + // 审批状态中包含 pending 的可以撤回 + if (task.approvalStatus?.includes('pending')) { + return true + } + + return false +} + +// 撤回任务 +const handleWithdrawTask = async (task: Task) => { + if (!task.id) return + + Modal.confirm({ + title: '撤回任务', + content: h('div', [ + h('p', `确认撤回任务【${task.title}】?`), + h('p', { style: 'color: #999; font-size: 13px; margin-top: 8px;' }, '撤回后任务将变为草稿状态,您可以进行编辑后重新发布。') + ]), + okText: '确认撤回', + cancelText: '取消', + async onOk() { + try { + withdrawingTaskId.value = task.id! + await taskApi.withdrawTask(task.id!) + message.success('任务已撤回,正在跳转到编辑页面...') + + // 撤回成功后跳转到编辑页面 + router.push({ + path: '/tasks/create', + query: { + editId: task.id, + mode: 'edit' + } + }) + } catch (error: any) { + console.error('撤回任务失败:', error) + message.error(error.response?.data?.message || error.message || '撤回任务失败') + } finally { + withdrawingTaskId.value = null + } + } + }) +} + const handleEditRejectedTask = (task: Task) => { // 跳转到创建任务页面,并传入任务ID作为编辑标识 router.push({