newsletter: jeden Empfänger genau einmal, atomarer Start, Wiederanlauf (Audit Block 2, Deploy B)
F05: chunkById hinter der created_at-Sortierung der Vorschau las ab der
zweiten Seite Empfänger doppelt und ließ die übrigen aus. Auf der Dev-
Kopie erreichte eine Kampagne an 59.705 Nutzer 500 Adressen, davon 480
doppelt. Die Sortierung wird vor dem ID-Chunking verworfen (E4: die
Versandreihenfolge hat keine fachliche Bedeutung), ein Unique-Index auf
(Kampagne, User) fängt jede weitere Doppelung ab; die Migration räumt
vorhandene Doppelungen vorher weg (auf der Dev-Kopie 470 Zeilen).
F14: Der Start übernimmt den Entwurf per bedingtem UPDATE draft→sending;
nur der Gewinner friert Empfänger ein (insertOrIgnore) und plant Jobs
aus dem Protokoll ein. Abbruch beim Einfrieren gibt den Entwurf frei.
F15: Der Versandjob beansprucht die Zeile pending→sending, schreibt
Empfängerstatus, Zähler und Kampagnenabschluss in einer Transaktion und
gibt den Claim bei Versandfehler zurück. Tries und Backoff am Job.
Zusatzbefund: Adressen auf der Sperrliste galten als „gesendet“, weil der
Rückgabewert von Mail::send nicht ausgewertet wurde — jetzt Status
„suppressed“ ohne Zähler.
Neu: php artisan newsletter:resume {kampagne} [--failed]
[--stuck-minutes=30] plant ausstehende Empfänger einer laufenden
Kampagne neu ein; der Circuit-Breaker-Hinweis nennt ihn statt
queue:retry, das Newsletter-Empfänger nicht nachholt.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
1d76f5a733
commit
eeb1cde48d
9 changed files with 752 additions and 63 deletions
|
|
@ -51,6 +51,11 @@ new #[Layout('components.layouts.app'), Title('Newsletter-Kampagne')] class exte
|
|||
'recipients' => $recipients,
|
||||
'filterSummary' => app(CampaignRecipients::class)->describe($campaign->filters ?? []),
|
||||
'recipientStatuses' => NewsletterRecipientStatus::cases(),
|
||||
// Gesperrte Adressen zählen in keinen Kampagnenzähler — sie
|
||||
// werden aus dem Protokoll abgeleitet.
|
||||
'suppressedCount' => $campaign->recipients()
|
||||
->where('status', NewsletterRecipientStatus::Suppressed->value)
|
||||
->count(),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
|
@ -84,7 +89,7 @@ new #[Layout('components.layouts.app'), Title('Newsletter-Kampagne')] class exte
|
|||
<x-slot:meta>{{ $campaign->sent_at ? __('abgeschlossen :date', ['date' => $campaign->sent_at->format('d.m.Y H:i')]) : __('läuft…') }}</x-slot:meta>
|
||||
</x-portal.stat-card>
|
||||
<x-portal.stat-card variant="muted" :label="__('Übersprungen')" :value="number_format($campaign->skipped_count)">
|
||||
<x-slot:meta>{{ __('abgemeldet') }}</x-slot:meta>
|
||||
<x-slot:meta>{{ __('abgemeldet') }}{{ $suppressedCount > 0 ? ' · '.__(':count gesperrt', ['count' => number_format($suppressedCount)]) : '' }}</x-slot:meta>
|
||||
</x-portal.stat-card>
|
||||
<x-portal.stat-card variant="muted" :label="__('Fehlgeschlagen')" :value="number_format($campaign->failed_count)">
|
||||
<x-slot:meta>{{ __('siehe Protokoll') }}</x-slot:meta>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue