From e72758f313f01d51d12b57800c074e5b01b5e707 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Mon, 13 Jul 2026 23:04:36 +0000 Subject: [PATCH] =?UTF-8?q?fix(task):=20=E5=B7=B2=E5=88=A0=E9=99=A4/?= =?UTF-8?q?=E5=B7=B2=E5=BD=92=E6=A1=A3=E9=A1=B9=E7=9B=AE=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=91=A8=E6=9C=9F=E4=BB=BB=E5=8A=A1=EF=BC=8C?= =?UTF-8?q?=E6=AE=8B=E7=95=99=E4=BB=BB=E5=8A=A1=E8=AF=A6=E6=83=85=E5=8F=AF?= =?UTF-8?q?=E6=89=93=E5=BC=80=E5=8F=AF=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LoopTask 周期生成仅处理未删除、未归档项目的任务,从源头杜绝残留繁殖 - userTask 兜底改用 withTrashed,任务相关成员可打开已删项目下的任务详情 - task/one 返回 project_deleted/project_archived 标记,详情顶部横幅提示并提供删除按钮 - task/remove 对已删除/已归档项目放行删除(限项目负责人、任务负责人/协助人/创建人) - 顺手修复 addTask 优先级匹配 reset() 传临时值的 notice 隐患,改用 collect()->first() - 同步 ai-kb(recurring/delete-restore)与国际化文案 --- .../Controllers/Api/ProjectController.php | 30 +++++++++++++--- app/Models/ProjectTask.php | 4 +-- app/Tasks/LoopTask.php | 5 ++- language/original-api.txt | 1 + language/original-web.txt | 2 ++ .../ai-kb/zh/howto/task/delete-restore.md | 4 ++- resources/ai-kb/zh/howto/task/recurring.md | 4 ++- .../js/pages/manage/components/TaskDetail.vue | 36 ++++++++++++++++++- 8 files changed, 76 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index abbd7392a..029a5ba7d 100755 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -2271,9 +2271,12 @@ class ProjectController extends AbstractController } // $data = $task->toArray(); - $data['department_readonly'] = UserDepartment::isDepartmentReadonlyProject($departmentView, intval($task->project_id)); - $data['project_name'] = $task->project?->name; + $taskProject = Project::withTrashed()->find($task->project_id); + $data['project_name'] = $taskProject?->name; + $data['project_deleted'] = (!$taskProject || $taskProject->trashed()) ? 1 : 0; + $data['project_archived'] = $taskProject?->archived_at ? 1 : 0; $data['column_name'] = $task->projectColumn?->name; + $data['department_readonly'] = UserDepartment::isDepartmentReadonlyProject($departmentView, intval($task->project_id)); $data['visibility_appointor'] = $task->visibility == 1 ? [0] : ProjectTaskVisibilityUser::whereTaskId($task_id)->pluck('userid'); return Base::retSuccess('success', $data); } @@ -3205,8 +3208,27 @@ class ProjectController extends AbstractController // $task = ProjectTask::userTask($task_id, null, $type !== 'recovery'); // - $project = Project::userProject($task->project_id); - ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_REMOVE, $task); + try { + $project = Project::userProject($task->project_id); + ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_REMOVE, $task); + } catch (\Throwable $e) { + if ($type == 'recovery') { + throw $e; + } + // 项目已删除/已归档时放行删除操作(限:管理员、原项目负责人、任务负责人/协助人/创建人) + $project = Project::withTrashed()->find($task->project_id); + $projectInvalid = !$project || $project->trashed() || $project->archived_at; + if (!$projectInvalid) { + throw $e; + } + $isProjectOwner = ProjectUser::whereProjectId($task->project_id) + ->whereUserid(Doo::userId()) + ->whereIn('owner', [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY]) + ->exists(); + if (!$isProjectOwner && !$task->permission(3)) { + throw new ApiException('仅项目负责人或任务相关成员删除'); + } + } // if ($type == 'recovery') { $task->restoreTask(); diff --git a/app/Models/ProjectTask.php b/app/Models/ProjectTask.php index 37ba5d1c6..808d72528 100644 --- a/app/Models/ProjectTask.php +++ b/app/Models/ProjectTask.php @@ -482,7 +482,7 @@ class ProjectTask extends AbstractModel if ($parent_id == 0) { $priorityList = Setting::normalizeTaskPriorityList(Base::setting('priority')); if ($p_level > 0) { - $matched = reset(array_filter($priorityList, fn($item) => intval($item['priority']) === $p_level)) ?: null; + $matched = collect($priorityList)->first(fn($item) => intval($item['priority']) === $p_level); } else { $matched = Setting::getDefaultTaskPriorityItem($priorityList); } @@ -2246,7 +2246,7 @@ class ProjectTask extends AbstractModel $project = Project::userProject($task->project_id); } catch (\Throwable $e) { if ($task->owner !== null || $task->permission(4)) { - $project = Project::find($task->project_id); + $project = Project::withTrashed()->find($task->project_id); if (empty($project)) { throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002); } diff --git a/app/Tasks/LoopTask.php b/app/Tasks/LoopTask.php index 04c891b77..92e664381 100644 --- a/app/Tasks/LoopTask.php +++ b/app/Tasks/LoopTask.php @@ -23,7 +23,10 @@ class LoopTask extends AbstractTask ProjectTask::whereBetween('loop_at', [ Carbon::now()->subMinutes(10), Carbon::now() - ])->chunkById(100, function ($list) { + ])->whereHas('project', function ($query) { + // 仅处理未删除、未归档项目的任务(Project 软删除由全局作用域排除) + $query->whereNull('archived_at'); + })->chunkById(100, function ($list) { /** @var ProjectTask $item */ foreach ($list as $item) { if ($item->parent_id > 0) { diff --git a/language/original-api.txt b/language/original-api.txt index dcf5577ec..1f46fc375 100644 --- a/language/original-api.txt +++ b/language/original-api.txt @@ -1040,3 +1040,4 @@ AI 助手 scene 暂未实现:(*) upload_id 不能为空 合并繁忙,请稍后再试 +仅项目负责人或任务相关成员删除 diff --git a/language/original-web.txt b/language/original-web.txt index a3092a8cf..9022a4d44 100644 --- a/language/original-web.txt +++ b/language/original-web.txt @@ -2525,3 +2525,5 @@ AI任务分析 选择要使用的授权 当前设备使用中 确定授权 +当前任务所属项目已被删除 +当前任务所属项目已归档 diff --git a/resources/ai-kb/zh/howto/task/delete-restore.md b/resources/ai-kb/zh/howto/task/delete-restore.md index 5ee636b9f..c1dc34d87 100644 --- a/resources/ai-kb/zh/howto/task/delete-restore.md +++ b/resources/ai-kb/zh/howto/task/delete-restore.md @@ -19,7 +19,8 @@ negative: - 删除后默认仅在回收站近期记录中保留,超过保留窗口会被定时清理 - 父任务删除会级联软删所有子任务 - 已删除任务不能编辑、不能完成、不能加子任务,必须先恢复 -last_verified: v1.7.90 + - 所属项目已删除或已归档的任务不能恢复,只能删除 +last_verified: v1.8.64 --- # 删除任务与回收站恢复 @@ -30,6 +31,7 @@ DooTask 任务使用软删除(`deleted_at` + `deleted_userid`),删除并 ## 入口 - 桌面端:任务详情页 → 「⋯」 → 「删除」 - 桌面端列表视图:右键任务行 → 「删除」 +- 所属项目已被删除或已归档的任务:任务详情页顶部横幅 → 「删除任务」按钮(限管理员、原项目负责人、任务负责人/协助人/创建人) - 恢复入口:项目设置 → 「回收站」或全局「我删除的任务」 ## 删除操作步骤 diff --git a/resources/ai-kb/zh/howto/task/recurring.md b/resources/ai-kb/zh/howto/task/recurring.md index 7a9519c0f..e2ffb9f10 100644 --- a/resources/ai-kb/zh/howto/task/recurring.md +++ b/resources/ai-kb/zh/howto/task/recurring.md @@ -19,7 +19,8 @@ negative: - 重复任务不能在子任务上独立设置(subtask 不支持 loop) - 重复任务的下一份在「上一份被标记完成时」生成,不是按日历预生成 - 修改循环周期不会回填已生成的实例 -last_verified: v1.7.90 + - 所属项目已被删除或已归档时,不再自动生成新的周期任务 +last_verified: v1.8.64 --- # 重复 / 循环任务 @@ -51,3 +52,4 @@ DooTask 任务支持循环重复(字段 `loop` + `loop_at`),常用于「 - 不能为同一任务并行启用多个周期 - 循环不复制附件 - 子任务不能独立设置循环 +- 所属项目已被删除或已归档的任务不再自动生成新周期任务;这类残留任务可在任务详情顶部横幅点「删除任务」清理 diff --git a/resources/assets/js/pages/manage/components/TaskDetail.vue b/resources/assets/js/pages/manage/components/TaskDetail.vue index dac4f7f4f..7551aa32b 100755 --- a/resources/assets/js/pages/manage/components/TaskDetail.vue +++ b/resources/assets/js/pages/manage/components/TaskDetail.vue @@ -115,7 +115,11 @@ {{$L('当前为负责人视角,并参与讨论,但不能编辑任务。')}} - + + {{taskDetail.project_deleted ? $L('当前任务所属项目已被删除') : $L('当前任务所属项目已归档')}} + + + {{$L('该任务尚未被领取,点击这里')}} 0 }, @@ -1136,6 +1146,30 @@ export default { this.receiveShow = true; }, + deleteInvalidTask() { + const typeTask = this.taskDetail.parent_id > 0 ? '子任务' : '任务'; + $A.modalConfirm({ + title: '删除' + typeTask, + content: '你确定要删除' + typeTask + '【' + this.taskDetail.name + '】吗?', + loading: true, + onOk: () => { + if (this.removeLoad > 0) { + return; + } + this.removeLoad++; + return new Promise((resolve, reject) => { + this.$store.dispatch("removeTask", {task_id: this.taskDetail.id}).then(({msg}) => { + resolve(msg); + }).catch(({msg}) => { + reject(msg); + }).finally(() => { + this.removeLoad--; + }); + }) + } + }); + }, + within24Hours(date) { return ($A.dayjs(date).unix() - this.keepUnix) < 86400 },