You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 13:10:48