mirror of
https://github.com/kuaifan/dootask.git
synced 2026-10-29 22:10:42 +00:00
fix(task): 修复周期任务到点后无限复制重复任务
Carbon 3 的 addDays() 不接受字符串,自定义天数周期(loop="90")在 refreshLoop() 中抛 TypeError。LoopTask 先提交副本再刷新周期,异常后 副本与原任务的 loop_at 都留在 10 分钟窗口内,每分钟翻倍并在同一轮内 被 chunkById 继续扫到。 - refreshLoop:自定义天数转 int 且必须大于 0,拒绝 0 天周期 - LoopTask:复制、刷新周期、关闭旧周期放入同一事务,失败整体回滚; 锁定并复核原任务防并发重入;refreshLoop 失败即抛出;只处理本轮 开始前已存在的任务 - ai-kb:更正周期任务按 loop_at 到点生成(而非完成时生成)、不依赖 end_at、会复制附件,并补充自定义天数须为正整数
This commit is contained in:
parent
1c90a11211
commit
5a10f59df8
@ -1177,8 +1177,10 @@ class ProjectTask extends AbstractModel
|
||||
$this->loop_at = null;
|
||||
break;
|
||||
default:
|
||||
if (Base::isNumber($this->loop)) {
|
||||
$this->loop_at = $base->addDays($this->loop);
|
||||
// 自定义天数须为正整数:Carbon 3 的 addDays 不接受字符串,0 天会让新任务立刻再次命中周期
|
||||
$days = Base::isNumber($this->loop) ? (int)$this->loop : 0;
|
||||
if ($days > 0) {
|
||||
$this->loop_at = $base->addDays($days);
|
||||
} else {
|
||||
$this->loop_at = null;
|
||||
$success = false;
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Models\AbstractModel;
|
||||
use App\Module\ProjectTaskHandoffRecord;
|
||||
use App\Models\ProjectFlow;
|
||||
use App\Models\ProjectFlowItem;
|
||||
@ -21,7 +23,9 @@ class LoopTask extends AbstractTask
|
||||
|
||||
public function start()
|
||||
{
|
||||
ProjectTask::whereBetween('loop_at', [
|
||||
// 只处理本轮开始前已存在的任务,本轮新生成的任务不会在同一轮里被再次扫到
|
||||
$maxId = (int)ProjectTask::withTrashed()->max('id');
|
||||
ProjectTask::where('id', '<=', $maxId)->whereBetween('loop_at', [
|
||||
Carbon::now()->subMinutes(10),
|
||||
Carbon::now()
|
||||
])->whereHas('project', function ($query) {
|
||||
@ -35,54 +39,7 @@ class LoopTask extends AbstractTask
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$task = $item->copyTask();
|
||||
ProjectTaskHandoffRecord::track($task, 'recurring', function () use ($task, $item) {
|
||||
// 工作流
|
||||
$projectFlow = ProjectFlow::whereProjectId($task->project_id)->orderByDesc('id')->first();
|
||||
if ($projectFlow) {
|
||||
$projectFlowItem = ProjectFlowItem::whereFlowId($projectFlow->id)->orderBy('sort')->get();
|
||||
// 赋一个开始状态
|
||||
foreach ($projectFlowItem as $flowItem) {
|
||||
if ($flowItem->status == 'start') {
|
||||
$task->flow_item_id = $flowItem->id;
|
||||
$task->flow_item_name = $flowItem->status . "|" . $flowItem->name . "|" . $flowItem->color;
|
||||
if ($flowItem->userids) {
|
||||
$userids = array_values(array_unique($flowItem->userids));
|
||||
foreach ($userids as $uid) {
|
||||
ProjectTaskUser::updateInsert([
|
||||
'task_id' => $task->id,
|
||||
'userid' => $uid,
|
||||
], [
|
||||
'project_id' => $task->project_id,
|
||||
'task_pid' => $task->id,
|
||||
'owner' => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 新任务时间、周期
|
||||
if ($task->start_at) {
|
||||
$diffSecond = (int)Carbon::parse($task->start_at)->diffInSeconds(Carbon::parse($task->end_at), true);
|
||||
$task->start_at = Carbon::parse($task->loop_at);
|
||||
$task->end_at = $task->start_at->clone()->addSeconds($diffSecond);
|
||||
}
|
||||
// 处理子任务
|
||||
$item->copySubTasks($task, [
|
||||
'reset_complete' => true,
|
||||
'sync_time' => true,
|
||||
]);
|
||||
//
|
||||
$task->refreshLoop(true);
|
||||
$task->addLog("创建任务来自周期任务ID:{$item->id}", [], $task->userid);
|
||||
});
|
||||
// 清空旧周期
|
||||
$item->loop = '';
|
||||
$item->loop_at = null;
|
||||
$item->save();
|
||||
$item->addLog("已创建新的周期任务ID:{$task->id},此任务关闭周期", [], $task->userid);
|
||||
$this->createLoopTask($item);
|
||||
} catch (\Throwable $e) {
|
||||
$item->addLog("生成重复任务失败:" . $e->getMessage(), [], $item->userid);
|
||||
}
|
||||
@ -90,6 +47,75 @@ class LoopTask extends AbstractTask
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 按周期生成新任务并关闭原任务周期
|
||||
* 复制、刷新周期、关闭旧周期在同一事务内,任一步失败整体回滚,不会留下 loop_at 仍在窗口内的残留副本
|
||||
* @param ProjectTask $item
|
||||
*/
|
||||
private function createLoopTask(ProjectTask $item)
|
||||
{
|
||||
AbstractModel::transaction(function () use ($item) {
|
||||
// 锁定并复核原任务:已被并发或上一轮处理(loop_at 已变化/清空)则跳过
|
||||
$source = ProjectTask::whereKey($item->id)->lockForUpdate()->first();
|
||||
if (!$source
|
||||
|| $source->parent_id > 0
|
||||
|| !$source->loop_at
|
||||
|| Carbon::parse($source->loop_at)->toDateTimeString() !== Carbon::parse($item->loop_at)->toDateTimeString()) {
|
||||
return;
|
||||
}
|
||||
$task = $source->copyTask();
|
||||
ProjectTaskHandoffRecord::track($task, 'recurring', function () use ($task, $source) {
|
||||
// 工作流
|
||||
$projectFlow = ProjectFlow::whereProjectId($task->project_id)->orderByDesc('id')->first();
|
||||
if ($projectFlow) {
|
||||
$projectFlowItem = ProjectFlowItem::whereFlowId($projectFlow->id)->orderBy('sort')->get();
|
||||
// 赋一个开始状态
|
||||
foreach ($projectFlowItem as $flowItem) {
|
||||
if ($flowItem->status == 'start') {
|
||||
$task->flow_item_id = $flowItem->id;
|
||||
$task->flow_item_name = $flowItem->status . "|" . $flowItem->name . "|" . $flowItem->color;
|
||||
if ($flowItem->userids) {
|
||||
$userids = array_values(array_unique($flowItem->userids));
|
||||
foreach ($userids as $uid) {
|
||||
ProjectTaskUser::updateInsert([
|
||||
'task_id' => $task->id,
|
||||
'userid' => $uid,
|
||||
], [
|
||||
'project_id' => $task->project_id,
|
||||
'task_pid' => $task->id,
|
||||
'owner' => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 新任务时间、周期
|
||||
if ($task->start_at) {
|
||||
$diffSecond = (int)Carbon::parse($task->start_at)->diffInSeconds(Carbon::parse($task->end_at), true);
|
||||
$task->start_at = Carbon::parse($task->loop_at);
|
||||
$task->end_at = $task->start_at->clone()->addSeconds($diffSecond);
|
||||
}
|
||||
// 处理子任务
|
||||
$source->copySubTasks($task, [
|
||||
'reset_complete' => true,
|
||||
'sync_time' => true,
|
||||
]);
|
||||
// 刷新新任务的下个周期,失败则抛出使整体回滚(否则副本会带着旧 loop_at 被再次复制)
|
||||
if (!$task->refreshLoop(true)) {
|
||||
throw new ApiException('重复周期设置无效');
|
||||
}
|
||||
$task->addLog("创建任务来自周期任务ID:{$source->id}", [], $task->userid);
|
||||
});
|
||||
// 清空旧周期
|
||||
$source->loop = '';
|
||||
$source->loop_at = null;
|
||||
$source->save();
|
||||
$source->addLog("已创建新的周期任务ID:{$task->id},此任务关闭周期", [], $task->userid);
|
||||
});
|
||||
}
|
||||
|
||||
public function end()
|
||||
{
|
||||
|
||||
|
||||
@ -1081,3 +1081,4 @@ WebDAV 未启用或你没有使用权限
|
||||
流转设置选项无效
|
||||
指派成功
|
||||
任务负责人最多不能超过10个
|
||||
重复周期设置无效
|
||||
|
||||
@ -16,9 +16,9 @@ related_pages: [task_detail, calendar]
|
||||
prerequisites: []
|
||||
negative:
|
||||
- DooTask 任务没有 time_estimate / time_log 字段,不能记录工时
|
||||
- 不支持只设开始时间不设结束时间(end_at 是计算到期与循环周期的依赖)
|
||||
- 不支持只设开始时间不设结束时间(end_at 是计算到期的依赖)
|
||||
- 跨时区显示按用户所在时区,但服务端统一存 UTC
|
||||
last_verified: v1.7.90
|
||||
last_verified: v1.9.36
|
||||
---
|
||||
|
||||
# 任务计划时间(start_at / end_at)
|
||||
@ -28,7 +28,7 @@ DooTask 任务用两个字段描述时间:
|
||||
- `start_at`:计划开始时间
|
||||
- `end_at`:计划截止时间(俗称 deadline)
|
||||
|
||||
两者都可空。**填入 end_at 才能启用循环重复、到期提醒、日历展示。**
|
||||
两者都可空。**填入 end_at 才能启用到期提醒、日历展示。** 重复循环不要求填 end_at。
|
||||
|
||||
## 在哪里能看到
|
||||
- 任务详情页 → 「计划时间」字段
|
||||
@ -42,12 +42,11 @@ DooTask 任务用两个字段描述时间:
|
||||
- 全天任务则 end_at 自动设为当天 23:59:59
|
||||
|
||||
## 与其他字段联动
|
||||
- **重复循环**([[task.recurring.howto]])依赖 end_at 计算下一次 loop_at
|
||||
- **重复循环**([[task.recurring.howto]])以 start_at 为基准计算下一次 loop_at,不依赖 end_at;新任务沿用原任务的时长
|
||||
- **到期提醒**(ProjectTaskPushLog)按 end_at 触发推送(默认到期日推送、超期再推送)
|
||||
- **日历视图** 只显示有 start_at 或 end_at 的任务
|
||||
- **完成时间**(complete_at)与计划时间无关,是否超期由 complete_at vs end_at 比较
|
||||
|
||||
## 不支持
|
||||
- 不能只设 start_at 不设 end_at 来启用循环
|
||||
- 不能录工时(time_estimate / time_log 字段不存在)
|
||||
- 修改 end_at 不会回填已生成的循环任务实例
|
||||
|
||||
@ -19,8 +19,8 @@ prerequisites:
|
||||
negative:
|
||||
- 父任务完成不会自动完成所有子任务,需要手动或 [[task.subtask.howto.create]] 后逐个处理
|
||||
- 完成不等于归档,归档要走 [[task.archive.howto]]
|
||||
- 重复任务在完成的瞬间生成下一份,详见 [[task.recurring.howto]]
|
||||
last_verified: v1.7.90
|
||||
- 完成任务不会触发生成下一份重复任务,重复任务按 loop_at 时间点自动生成,详见 [[task.recurring.howto]]
|
||||
last_verified: v1.9.36
|
||||
---
|
||||
|
||||
# 标记任务完成 / 取消完成
|
||||
@ -37,7 +37,6 @@ last_verified: v1.7.90
|
||||
1. 点击复选框 / 完成按钮
|
||||
2. 任务 `complete_at` 写入当前时间,整个卡片显示删除线 / 灰化
|
||||
3. 项目动态记一条「X 完成了任务 Y」日志
|
||||
4. 若该任务带 `loop`(循环),同时生成下一份新任务
|
||||
|
||||
## 取消完成
|
||||
1. 在已完成任务上再次点击复选框
|
||||
|
||||
@ -11,22 +11,25 @@ aliases:
|
||||
- 每天的任务
|
||||
- 周报任务自动生成
|
||||
- 怎么让任务自动重复
|
||||
related_tools: [create_task, update_task, complete_task]
|
||||
- 重复周期
|
||||
related_tools: [create_task, update_task]
|
||||
related_pages: [task_detail]
|
||||
prerequisites:
|
||||
- 任务已设置 end_at(截止时间),否则无法计算下一周期
|
||||
- 只能在顶级任务上设置,子任务不支持
|
||||
negative:
|
||||
- 重复任务不能在子任务上独立设置(subtask 不支持 loop)
|
||||
- 重复任务的下一份在「上一份被标记完成时」生成,不是按日历预生成
|
||||
- 下一份不是在任务被标记完成时生成,而是到 loop_at 时间点由系统定时任务自动生成,与原任务是否完成无关
|
||||
- 只在 loop_at 之后的 10 分钟内生成;服务停机超过 10 分钟错过的周期不会补生成
|
||||
- 自定义天数必须是正整数,0 或非数字会被拒绝
|
||||
- 修改循环周期不会回填已生成的实例
|
||||
- 所属项目已被删除或已归档时,不再自动生成新的周期任务
|
||||
last_verified: v1.8.64
|
||||
last_verified: v1.9.36
|
||||
---
|
||||
|
||||
# 重复 / 循环任务
|
||||
|
||||
## 是什么
|
||||
DooTask 任务支持循环重复(字段 `loop` + `loop_at`),常用于「每日站会」「每周周报」类型任务。每次任务被标记完成时,系统按 `loop` 自动生成下一份新任务,复制原任务字段并按 `loop_at` 推下一次计划时间。
|
||||
DooTask 任务支持循环重复(字段 `loop` 重复周期 + `loop_at` 下个周期时间),常用于「每日站会」「每周周报」类型任务。系统定时任务每分钟检查一次,到达 `loop_at` 时自动创建下一份新任务,原任务是否已完成不影响生成。
|
||||
|
||||
## 支持的周期
|
||||
- `day` 每天
|
||||
@ -35,21 +38,22 @@ DooTask 任务支持循环重复(字段 `loop` + `loop_at`),常用于「
|
||||
- `twoweeks` 双周
|
||||
- `month` 每月
|
||||
- `year` 每年
|
||||
- 自定义天数(输入数字,如 3 表示每 3 天)
|
||||
- 自定义天数(输入正整数,如 3 表示每 3 天;0 或非数字会被拒绝,提示「重复周期选择错误」)
|
||||
- `never` 不重复(默认)
|
||||
|
||||
## 入口
|
||||
- 桌面端:任务详情页 → 计划时间区 → 「循环」下拉
|
||||
- 必须先填 end_at(截止时间)才能启用循环
|
||||
- 桌面端:任务详情页 →「重复周期」项(未设置时在添加项菜单里选「重复周期」),选预设周期或「自定义」
|
||||
- 设置后鼠标悬停周期文字可看到「下个周期」时间
|
||||
- 不要求填截止时间:以开始时间为基准计算,未设开始时间则以当天 0 点为基准
|
||||
|
||||
## 工作原理
|
||||
1. 标记 [[task.complete.howto]] 完成当前任务
|
||||
2. 服务端基于 `loop` 与当前 `end_at` 计算下一个 `loop_at`
|
||||
3. 自动创建新任务并复制:标题、描述、负责人、协作者、标签、优先级、可见性、子任务
|
||||
4. 新任务的状态为未完成,按新计划时间出现在日历视图
|
||||
1. 设置或修改周期、计划时间时,服务端按 `loop` 和开始时间算出 `loop_at`
|
||||
2. 到 `loop_at` 后,系统创建新任务,复制:标题、描述、负责人和协助人员、附件、优先级、可见性、子任务
|
||||
3. 新任务开始时间等于原任务的 `loop_at`,时长与原任务相同;状态为未完成(项目有工作流则回到起始状态),子任务也重置为未完成
|
||||
4. 新任务自己算出下一个 `loop_at`;原任务的周期被关闭(`loop` 清空),并记一条日志
|
||||
5. 创建失败时整体回滚,不会留下半成品,任务日志记「生成重复任务失败:原因」,到期后 10 分钟内每分钟重试
|
||||
|
||||
## 不支持
|
||||
- 不能为同一任务并行启用多个周期
|
||||
- 循环不复制附件
|
||||
- 子任务不能独立设置循环
|
||||
- 所属项目已被删除或已归档的任务不再自动生成新周期任务;这类残留任务可在任务详情顶部横幅点「删除任务」清理
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user