fix(task): 已删除/已归档项目不再生成周期任务,残留任务详情可打开可删除

- LoopTask 周期生成仅处理未删除、未归档项目的任务,从源头杜绝残留繁殖
- userTask 兜底改用 withTrashed,任务相关成员可打开已删项目下的任务详情
- task/one 返回 project_deleted/project_archived 标记,详情顶部横幅提示并提供删除按钮
- task/remove 对已删除/已归档项目放行删除(限项目负责人、任务负责人/协助人/创建人)
- 顺手修复 addTask 优先级匹配 reset() 传临时值的 notice 隐患,改用 collect()->first()
- 同步 ai-kb(recurring/delete-restore)与国际化文案
This commit is contained in:
kuaifan 2026-07-13 23:04:36 +00:00
parent 746a0bf9a1
commit e72758f313
8 changed files with 76 additions and 10 deletions

View File

@ -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();

View File

@ -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);
}

View File

@ -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) {

View File

@ -1040,3 +1040,4 @@ AI 助手
scene 暂未实现:(*)
upload_id 不能为空
合并繁忙,请稍后再试
仅项目负责人或任务相关成员删除

View File

@ -2525,3 +2525,5 @@ AI任务分析
选择要使用的授权
当前设备使用中
确定授权
当前任务所属项目已被删除
当前任务所属项目已归档

View File

@ -19,7 +19,8 @@ negative:
- 删除后默认仅在回收站近期记录中保留,超过保留窗口会被定时清理
- 父任务删除会级联软删所有子任务
- 已删除任务不能编辑、不能完成、不能加子任务,必须先恢复
last_verified: v1.7.90
- 所属项目已删除或已归档的任务不能恢复,只能删除
last_verified: v1.8.64
---
# 删除任务与回收站恢复
@ -30,6 +31,7 @@ DooTask 任务使用软删除(`deleted_at` + `deleted_userid`),删除并
## 入口
- 桌面端:任务详情页 → 「⋯」 → 「删除」
- 桌面端列表视图:右键任务行 → 「删除」
- 所属项目已被删除或已归档的任务:任务详情页顶部横幅 → 「删除任务」按钮(限管理员、原项目负责人、任务负责人/协助人/创建人)
- 恢复入口:项目设置 → 「回收站」或全局「我删除的任务」
## 删除操作步骤

View File

@ -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`),常用于「
- 不能为同一任务并行启用多个周期
- 循环不复制附件
- 子任务不能独立设置循环
- 所属项目已被删除或已归档的任务不再自动生成新周期任务;这类残留任务可在任务详情顶部横幅点「删除任务」清理

View File

@ -115,7 +115,11 @@
<Alert v-if="taskDetail.department_readonly" class="task-readonly-alert" type="info" show-icon>
{{$L('当前为负责人视角,并参与讨论,但不能编辑任务。')}}
</Alert>
<Alert v-if="!isDepartmentReadonly && taskDetail.task_user !== undefined && getOwner.length === 0" class="receive-box" type="warning">
<Alert v-if="projectInvalid" class="receive-box" type="error">
<span class="receive-text">{{taskDetail.project_deleted ? $L('当前任务所属项目已被删除') : $L('当前任务所属项目已归档')}}</span>
<Button v-if="!isDepartmentReadonly" class="receive-button" :loading="removeLoad > 0" size="small" type="error" @click="deleteInvalidTask">{{$L('删除任务')}}</Button>
</Alert>
<Alert v-if="!projectInvalid && !isDepartmentReadonly && taskDetail.task_user !== undefined && getOwner.length === 0" class="receive-box" type="warning">
<span class="receive-text">{{$L('该任务尚未被领取,点击这里')}}</span>
<EPopover
v-model="receiveShow"
@ -635,6 +639,8 @@ export default {
ownerData: {},
ownerLoad: 0,
removeLoad: 0,
receiveShow: false,
tagForce: false,
@ -989,6 +995,10 @@ export default {
return !!this.taskDetail?.department_readonly;
},
projectInvalid() {
return this.taskDetail.project_deleted == 1 || this.taskDetail.project_archived == 1;
},
isSubTask({taskDetail}) {
return taskDetail.parent_id > 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
},