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
},