許多騎行團體都有定期騎行活動 - 週一早上的通勤車隊、每月第一個週六的冒險騎行,或週三晚上的社交騎行。到目前為止,組織者必須手動創建每個活動。借助我們新的週期性派對功能,您可以一次設置時間表,讓 Party Onbici 自動生成實例。

一次性活動的問題

社區騎行團體通常有可預測的時間表:

  • “每週二和週四早上 6:30”
  • “每月第一個週日”
  • “隔週週六早上”

手動創建這些活動既繁瑣又容易出錯,而且難以在各次活動中保持一致的路線和設置。

介紹 iCalendar RRULE

我們基於 iCalendar RRULE 標準(RFC 5545)構建了週期性活動系統。這是 Google 日曆、Apple 日曆和 Outlook 使用的相同格式 - 確保與更廣泛的日曆生態系統兼容。

週期性模式示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR
  → 每週一、三、五

RRULE:FREQ=MONTHLY;BYDAY=1SA
  → 每月第一個週六

RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU
  → 隔週週日

RRULE:FREQ=DAILY;COUNT=10
  → 每天,共 10 次

數據模型

RecurringParty 作為生成 Party 實例的模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class RecurringParty(models.Model):
    name = models.CharField(max_length=255)

    # 時間表配置
    start_date = models.DateField()
    end_date = models.DateField(null=True, blank=True)
    max_occurrences = models.PositiveIntegerField(null=True, blank=True)
    recurrence = RecurrenceField()  # RRULE 模式

    # 模板字段(複製到每個實例)
    departure_time = models.TimeField()
    arrival_time = models.TimeField()
    origin = PointField()
    destination = PointField()
    route = models.JSONField()
    difficulty = models.CharField(choices=DIFFICULTY_CHOICES)
    # ... 其他派對屬性

    is_active = models.BooleanField(default=True)

每個生成的 Party 實例都連接回其父級:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Party(models.Model):
    # 現有字段...

    # 週期性派對支持
    scheduled_date = models.DateField(null=True, blank=True)
    recurring_parent = models.ForeignKey(
        RecurringParty,
        on_delete=models.SET_NULL,
        null=True,
        related_name="instances"
    )
    is_cancelled = models.BooleanField(default=False)

實例生成

當創建週期性派對或時間表更改時,我們為即將到來的時期生成實例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def generate_instances(self, lookahead_days: int = 30) -> list[Party]:
    """為即將發生的事件生成派對實例。"""
    occurrences = self.recurrence.between(
        datetime.now().date(),
        datetime.now().date() + timedelta(days=lookahead_days),
    )

    created = []
    for date in occurrences:
        # 如果實例已存在則跳過
        if self.instances.filter(scheduled_date=date).exists():
            continue

        # 從模板創建新的派對實例
        party = Party.objects.create(
            name=f"{self.name} - {date.strftime('%B %d')}",
            scheduled_date=date,
            recurring_parent=self,
            departure_time=self.departure_time,
            arrival_time=self.arrival_time,
            origin=self.origin,
            destination=self.destination,
            route=self.route,
            # ... 複製其他模板字段
        )
        created.append(party)

    return created

使用 Celery Beat 自動生成

每日 Celery 任務保持實例最新:

1
2
3
4
5
6
7
8
9
@shared_task
def generate_recurring_party_instances():
    """為所有活躍的週期性派對生成派對實例。"""
    for recurring_party in RecurringParty.objects.filter(is_active=True):
        created = recurring_party.generate_instances(lookahead_days=30)
        if created:
            logger.info(
                f"為 {recurring_party.name} 生成了 {len(created)} 個實例"
            )

Celery Beat 計劃在午夜運行:

1
2
3
4
5
6
CELERY_BEAT_SCHEDULE = {
    "generate-recurring-party-instances": {
        "task": "party_onbici.apps.party.tasks.generate_recurring_party_instances",
        "schedule": crontab(hour=0, minute=0),  # 每天午夜
    },
}

用戶界面

創建週期性派對

表單使用 django-recurrence 小部件構建 RRULE 模式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class RecurringPartyForm(forms.ModelForm):
    recurrence = RecurrenceField(
        label="週期性模式",
        help_text="設置此派對應何時重複。",
        required=True,
    )

    class Meta:
        model = RecurringParty
        fields = [
            "name", "start_date", "end_date", "recurrence",
            "departure_time", "arrival_time",
            "origin", "destination", "description",
            "difficulty", "gender", "privacy",
        ]

小部件為常見模式提供直觀的界面:

  • 每天 / 每週 / 每月 / 每年
  • 一週中的特定日子
  • 序數模式(第一、第二、最後)
  • 自定義間隔(每 2 週)
  • 結束條件(到日期、N 次後、或永不)

管理實例

組織者可以查看所有生成的實例並:

  • 編輯單個實例 - 更改一次活動的路線或時間
  • 取消實例 - 將特定日期標記為已取消
  • 重新生成實例 - 手動觸發接下來 30 天的生成

處理邊緣情況

時區感知

所有日期都存儲在組織者配置的時區中,並以查看者的本地時間顯示:

1
2
3
# 使用時區感知日期生成實例
local_tz = get_current_timezone()
today = timezone.now().astimezone(local_tz).date()

假日和例外處理

用戶可以取消單個實例而不影響週期性模式:

1
2
3
party = recurring_party.instances.get(scheduled_date=holiday_date)
party.is_cancelled = True
party.save()

已取消的實例仍會在列表中顯示,帶有視覺指示器,防止混淆。

孤立實例

如果刪除週期性派對,生成的實例可以選擇保留:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def form_valid(self, form):
    delete_instances = request.POST.get("delete_instances") == "true"

    if delete_instances:
        # 軟刪除未來實例
        recurring_party.instances.filter(
            scheduled_date__gte=date.today(),
            deleted_at__isnull=True,
        ).update(deleted_at=timezone.now())

    recurring_party.delete()

用戶體驗

創建每週騎行只需幾個步驟:

  1. 前往 儀表板 → 週期性派對 → 創建
  2. 設置您的派對詳情(路線、時間、難度)
  3. 配置週期性模式(例如,“每週六早上 8 點”)
  4. 設置開始日期和可選的結束日期
  5. 保存 - 實例會自動生成!

系統提前 30 天創建派對實例,Celery 會隨著時間推移繼續生成新實例。

騎行社區的好處

  • 設置後無需操心 - 不再需要每週創建活動
  • 一致性 - 相同的路線、相同的時間、相同的設置
  • 靈活性 - 編輯或取消單個活動
  • 發現 - 騎行者可以輕鬆找到定期的團體騎行
  • 日曆整合 - 在您的日曆應用中訂閱週期性活動

準備好設置您的定期騎行了嗎?創建週期性派對,讓我們來處理排程!