基于RRULE的Task重复事件分页实现难题求助
重复任务分页实现方案
核心思路
避免全量查询+遍历的低效模式,通过时间范围锚定+分批次处理+合并排序实现分页,核心是只处理当前分页窗口内的任务数据。
具体实现步骤
1. 拆分任务查询逻辑
将任务分为两类分别处理:
- 非重复任务:直接按
due_date进行范围查询,无需额外处理 - 重复任务模板:先筛选出规则未过期、可能在当前分页时间窗口内生成实例的模板,避免无效计算
2. 确定分页时间窗口
因为分页按due_date升序排列,先通过小范围查询锚定时间边界:
- 先查询2倍页容量的非重复任务,获取时间范围的最小/最大值
- 用这个范围作为过滤条件,缩小后续查询的数据集
3. 生成目标范围内的重复实例
利用When库的范围生成能力,只生成当前时间窗口内的重复任务实例:
// 示例:只生成[minDate, maxDate]区间内的重复实例 $rrule = \When\When::parse($template->getRrule()); $instances = $rrule->getOccurrencesBetween($minDate, $maxDate);
4. 合并与排序
将非重复任务(转换为统一实例结构)和重复实例合并后,按规则排序:
- 逾期任务(
due_date < 当前时间)排最前 - 剩余任务按
due_date升序排列
5. 分页截取
对合并排序后的数据集,使用array_slice完成分页截取。
代码示例
仓储层代码
// 获取指定时间范围内的非重复任务 public function getNonRecurringTasks(\DateTime $startDate, \DateTime $endDate): array { return $this->createQueryBuilder('t') ->where('t.rrule IS NULL') ->andWhere('t.due_date BETWEEN :start AND :end') ->setParameter('start', $startDate) ->setParameter('end', $endDate) ->orderBy('t.due_date', 'ASC') ->getQuery() ->getResult(); } // 获取有效的重复任务模板(规则未过期) public function getRecurringTemplates(\DateTime $startDate): array { return $this->createQueryBuilder('t') ->where('t.rrule IS NOT NULL') ->andWhere('t.rrule_end_date IS NULL OR t.rrule_end_date >= :start') ->setParameter('start', $startDate) ->getQuery() ->getResult(); }
业务层分页逻辑
public function getPaginatedTasks(int $page, int $pageSize = 10): array { $offset = ($page - 1) * $pageSize; // 初始化时间范围,覆盖大概率的任务区间 $defaultStart = new \DateTime('-1 year'); $defaultEnd = new \DateTime('+1 year'); // 第一步:锚定时间窗口 $tempNonRecurring = $this->taskRepo->getNonRecurringTasks($defaultStart, $defaultEnd); $minDate = $tempNonRecurring[0]?->getDueDate() ?? $defaultStart; $maxDate = $tempNonRecurring[count($tempNonRecurring)-1]?->getDueDate() ?? $defaultEnd; // 第二步:获取目标范围内的非重复任务 $nonRecurring = $this->taskRepo->getNonRecurringTasks($minDate, $maxDate); // 转换为统一实例结构 $nonRecurringInstances = array_map(function($task) { return [ 'unique_id' => $task->getId(), 'title' => $task->getTitle(), 'due_date' => $task->getDueDate(), 'is_recurring' => false ]; }, $nonRecurring); // 第三步:生成重复任务实例 $recurringTemplates = $this->taskRepo->getRecurringTemplates($minDate); $recurringInstances = []; foreach ($recurringTemplates as $template) { $rrule = \When\When::parse($template->getRrule()); $occurrences = $rrule->getOccurrencesBetween($minDate, $maxDate); foreach ($occurrences as $date) { $recurringInstances[] = [ 'unique_id' => $template->getId() . '_' . $date->format('YmdHis'), 'title' => $template->getTitle(), 'due_date' => $date, 'is_recurring' => true ]; } } // 第四步:合并排序 $allInstances = array_merge($nonRecurringInstances, $recurringInstances); usort($allInstances, function($a, $b) { $now = new \DateTime(); $aOverdue = $a['due_date'] < $now; $bOverdue = $b['due_date'] < $now; // 逾期任务优先排序 if ($aOverdue !== $bOverdue) { return $aOverdue ? -1 : 1; } // 同状态按日期升序 return $a['due_date'] <=> $b['due_date']; }); // 第五步:分页截取 return array_slice($allInstances, $offset, $pageSize); }
进阶优化
- 游标分页替代offset:以上一页最后一条任务的
due_date和unique_id作为游标,避免数据偏移问题 - 缓存重复实例:对高频访问的时间范围,缓存生成的重复实例,减少计算开销
- 数据库层优化:若使用PostgreSQL,可借助
pg_rrule插件直接在数据库生成重复实例,减轻应用层压力
内容的提问来源于stack exchange,提问作者Nguyen Nhut Tien
相关产品推荐
相关产品推荐

