0 votes
in SoSci Survey (dt.) by s293220 (130 points)

Hallo,

ich habe ein Problem mit der Planung von Serienmails. Ich steuere die Planung über einen PHP-Code, der für Teilnehmende den Versand von Mails zu randomisierten Zeitpunkten innerhalb festgelegter Zeitslots planen soll. Die Studie soll über 10 Tage laufen, pro Tag vier Mails pro Teilnehmer planen.

Bei der ersten Planung schient alles zu funktionieren, wie es soll. Allerdings werden dann immer wieder zusätzliche Mails für schon bestehende Teilnehmer geplant. Und zwar immer direkt korrekt für jede Welle eine. Eine Logik dahinter erschließt sich mir nicht.

Das hier ist der PHP-Code, den ich verwende:

if (getRoute() != 'repeat') {

    $expireFinal   = 172800; // 2 Tage
    $expireRegular = 2820;   // 47 Minuten
    $minGap        = 3600;   // 1 Stunde Mindestabstand
    $mailId        = 2;

    $plannedTimes = [];
    $plannedMails = [];
    $abortAll     = false;

    $baseDate = strtotime('today');

    // Teilnehmer-Offset (0–5 Minuten)
    $participantOffset = time() % 300;

    // ======================================================
    // Phase 1: Slots berechnen (Sampling + Shuffle)
    // ======================================================
    for ($day = 1; $day <= 10; $day++) {

        if ($abortAll) {
            break;
        }

        $currentDate = strtotime("+" . $day . " day", $baseDate);
        if ($currentDate === false) {
            $abortAll = true;
            break;
        }

        $weekday = (int) date('N', $currentDate);

        // Tagesfenster
        $windows = [];
        if ($weekday <= 5) {
            $windows[] = [
                'start' => mktime(7,0,0,date('n',$currentDate),date('j',$currentDate),date('Y',$currentDate)),
                'end'   => mktime(7,55,0,date('n',$currentDate),date('j',$currentDate),date('Y',$currentDate))
            ];
            $windows[] = [
                'start' => mktime(14,0,0,date('n',$currentDate),date('j',$currentDate),date('Y',$currentDate)),
                'end'   => mktime(22,0,0,date('n',$currentDate),date('j',$currentDate),date('Y',$currentDate))
            ];
        } else {
            $windows[] = [
                'start' => mktime(10,0,0,date('n',$currentDate),date('j',$currentDate),date('Y',$currentDate)),
                'end'   => mktime(22,0,0,date('n',$currentDate),date('j',$currentDate),date('Y',$currentDate))
            ];
        }

        // Alle möglichen Slots sammeln
        $allSlots = [];
        foreach ($windows as $w) {
            for ($t = $w['start']; $t <= $w['end']; $t += 300) {
                if ($t > time()) {
                    $allSlots[] = min($t + $participantOffset, $w['end']);
                }
            }
        }

        if (count($allSlots) < 4) {
            $abortAll = true;
            break;
        }

        shuffle($allSlots);

        // 4 Slots mit Mindestabstand auswählen
        $daySlots = [];
        foreach ($allSlots as $slot) {

            $ok = true;

            foreach ($daySlots as $ds) {
                if (abs($slot - $ds) < $minGap) {
                    $ok = false;
                    break;
                }
            }

            if ($ok) {
                foreach ($plannedTimes as $pt) {
                    if (abs($slot - $pt) < $minGap) {
                        $ok = false;
                        break;
                    }
                }
            }

            if ($ok) {
                $daySlots[] = $slot;
                if (count($daySlots) === 4) {
                    break;
                }
            }
        }

        if (count($daySlots) !== 4) {
            $abortAll = true;
            break;
        }

        // Slots fixieren
        foreach ($daySlots as $candidateTime) {

            if ($mailId > 82) {
                $abortAll = true;
                break;
            }

            $plannedMails[] = [
                'mailId'      => $mailId,
                'plannedTime' => $candidateTime,
                'expire'      => $candidateTime + $expireRegular
            ];

            $plannedTimes[] = $candidateTime;

            $mailId++;
            if ($mailId >= 3 && $mailId <= 43) {
                $mailId = 44;
            }
        }
    }

    // ======================================================
    // Phase 2: Mails planen (unverändert)
    // ======================================================
    if (!$abortAll) {

        foreach ($plannedMails as $pm) {

            $plannedTime = $pm['plannedTime'];
            $expireAt    = $pm['expire'];

            if ($expireAt <= time()) {
                $expireAt = time() + $expireRegular;
            }

            mailSchedule(false, $pm['mailId'], $plannedTime, [
                'expire' => $expireAt
            ]);
        }

        // Abschlussfragebogen ID 84
        if (count($plannedTimes) > 0) {

            $lastMailTime = max($plannedTimes);
            $plannedTime  = $lastMailTime + $expireRegular;

            $lastDay = strtotime('+10 day', $baseDate);
            $weekday = (int) date('N', $lastDay);

            if ($weekday <= 5) {
                $windows = [
                    [
                        'start'=>mktime(7,0,0,date('n',$lastDay),date('j',$lastDay),date('Y',$lastDay)),
                        'end'  =>mktime(7,55,0,date('n',$lastDay),date('j',$lastDay),date('Y',$lastDay))
                    ],
                    [
                        'start'=>mktime(14,0,0,date('n',$lastDay),date('j',$lastDay),date('Y',$lastDay)),
                        'end'  =>mktime(22,0,0,date('n',$lastDay),date('j',$lastDay),date('Y',$lastDay))
                    ]
                ];
            } else {
                $windows = [
                    [
                        'start'=>mktime(10,0,0,date('n',$lastDay),date('j',$lastDay),date('Y',$lastDay)),
                        'end'  =>mktime(22,0,0,date('n',$lastDay),date('j',$lastDay),date('Y',$lastDay))
                    ]
                ];
            }

            $plannedTime = max($plannedTime, max($plannedTimes) + $minGap);
            $plannedTime = min($plannedTime, $windows[count($windows) - 1]['end']);

            $ok = mailSchedule(false, 84, $plannedTime, [
                'expire' => $plannedTime + $expireFinal
            ]);

            if ($ok !== false) {
                $plannedMails[] = [
                    'mailId'      => 84,
                    'plannedTime' => $plannedTime,
                    'expire'      => $plannedTime + $expireFinal
                ];
            } else {
                $abortAll     = true;
                $plannedTimes = [];
            }
        }
    }

} // Ende if getRoute() != 'repeat'

1 Answer

0 votes
by SoSci Survey (369k points)

Nun, der Code läuft jedesmal, wenn die Person auf die Seite kommt. Es sind also durchaus Situationen denkbar, wenn durch das wiederholte Ausführen des Codes zusätzliche Mails geplant werden.

Hinzu kommt, dass Sie in dem Code viele Schleifen verschachteln, deren Sinn sich mir auf den ersten Blick nicht erschließt. Ich tippe auf eine komplexe Randomisierung ... aber die Verwendung von Abbruchbedingungen (hier die Variable $ok) sorgt für einige Komplexität. Vielleicht wäre ein Zufallsgenerator einfacher.

Die gute Nachricht: Es wird pro Serienmail nur die erste Mail verschickt, die eingeplant ist. Das ist für die Randomisierung evtl. nicht optimal, aber zumindest werden die Befragten nicht mehrfach mit derselben Mail bombardiert.

by s293220 (130 points)
Danke erstmal für die Antwort. Leider ist die doppelte Planung auch passiert, ohne, dass Teilnehmende ein zweites Mal die Seite mit Code aufgerufen haben. Zumindest nach eigenen Angaben.

Können die Schleifen und Abbruchsbedingungen das Problem denn verursachen? ich dachte, ich nutze eigentlich eine Zufallsgenerator-Logik mit Shuffle.

Und genau, für die Randomisierung ist die Logik mit der ersten Mail, die verschickt wird, leider ein Problem. Eine bessere Lösung als die hier hab ich dafür leider bisher nicht gefunden.
by s293220 (130 points)
Nochmal als Nachtrag: Wäre es möglich im PHP-Code zu prüfen, ob schon Mails mit den entsprechenden ID für einen TN geplant wurden? Und falls dem so ist, eine erneute Planung zu verhindern? Ich sehe im Backend ja die Adressliste und in der Planung die Mails. Aber die Daten sind nicht personenbezogen, also tauchen die Mobilnummern und auch die Planungszeiten nicht im Datensatz auf.
by SoSci Survey (369k points)
> Wäre es möglich im PHP-Code zu prüfen, ob schon Mails mit den entsprechenden ID für einen TN geplant wurden?

Sie könnten innerhalb desselben Fragebogen mittels registerVariable() und isset() sicherstellen, dass der Code nur einmal läuft.

Und Sie könnten mittels mailRevoke() bestehende Versandtermine löschen, bevor Sie bzw. der Code neue plant.

Eine explizite Prüfung wäre über Einträge in der Datenbank für Inhalte möglich.

> Können die Schleifen und Abbruchsbedingungen das Problem denn verursachen?

Nicht spezifisch, aber komplexer Code neigt dazu, unter gewissen Randbedingungen komische Dinge zu tun, mit denen man vielleicht nicht gerechnet hat.

> Leider ist die doppelte Planung auch passiert, ohne, dass Teilnehmende ein zweites Mal die Seite mit Code aufgerufen haben.

Haben Sie einen Zurück-Knopf im Fragebogen? Auch das könnte zum mehrfachen Aufruf der Seite führen. Ebenfalls denbar, dass jemand auf der Seite abbricht und dann mit dem personalisierten Link nochmal von vorne beginnt, weil zu viele Antworten gefehlt hatten.

> Eine bessere Lösung als die hier hab ich dafür leider bisher nicht gefunden.

Sie haben bisher nicht ausgeführt, was die Randomisierung genau erreichen soll. Wenn sie möchten, erklären Sie das gerne einmal. Womöglich fällt mir eine einfachere und robustere Lösung ein.
by s293220 (130 points)
Gerade ist die Logik so (wie auch vorgeschlagen), dass ein Opt In im ersten Fragebogen die Mobilnummern erfasst und mit Bestätigung der Nummer dann automatisch ein Fragebogen aufgerufen wird, der nur den PHP-Code und eine end Seite enthält. Geplant werden dann 40 Mails mit dem identischen Fragebogen (experience sampling, 4 Fragebögen pro Tag über 10 Tage). Ich habe für jeden Fragebogen eine Serienmail angelegt (also ins. 40 plus eine Abschlussmail).

-> d.h. ich könnte mit register variable und isset im Fragebogen des PHP-Codes hinterlegen, ob für einen Teilnehmer (nach Personenkennung?) der Code schon gelaufen ist?

Zur Randomisierung: Die Versandzeitpunkte selbst sollen randomisiert werden, dabei aber Bedingungen einhalten:

- An Wochentagen Versand zwischen 7 und 7:55 sowie 14 Uhr und 22 Uhr
- An Wochenenden Versand zwischen 10 und 22 Uhr
- Alle Links sollen 47 Minuten nach Versand auslaufen
- Zwischen zwei SMS eines Teilnehmers sollen min. 60 Minuten Abstand sein
- min. 2 Slots sollen über die 10 Tage pro TN zwischen 7 und 7:55 liegen

Ich habe mit der Randomisierung viel experimentiert, damit sie die Befragten auch wirklich zu möglichst verschiedenen Zeitpunkten erwischt und auf die Befragten auch random wirkt.
by s293220 (130 points)
Nachtrag: Gibt es eine Funktion, mit der ich prüfen kann, ob für den aktuellen Teilnehmer bereits Versandtermine existieren – ohne sie zu löschen? Ich möchte den Planungs-Code nur ausführen, wenn noch keine Termine vorhanden sind.

-> Das müsste ja eigentlich auch im PHP-Code gehen und würde dann mit einer eindeutigen Kennung auch verhindern, dass der Code für Teilnehmende, die nochmals auf den Bestätigungslink klicken erneut läuft.

Einen Zurück-Button gibt es nicht.
by SoSci Survey (369k points)
edited by SoSci Survey
> Gibt es eine Funktion, mit der ich prüfen kann, ob für den aktuellen Teilnehmer bereits Versandtermine existieren – ohne sie zu löschen?

Nein, dieser Bedarf bestand bisher nicht.

Sie müssten dafür (aber schon beim ersten Aufruf) per dbSet() einen Eintrag in der Datenbank für Inhalte vornehmen.

Auch die Kombination aus registerVariable() und isset() würde erfordern, dass man den Code gleich von vornherein einbaut. Auch das ist nicht auf eine nachträgliche Behebung ausgelegt.

> Einen Zurück-Button gibt es nicht.

Gut, dann haben wir eine Fehlerquelle schonmal ausgeschlossen.

Ich kann Ihnen anbieten, dass ich für einen einzelnen Adresslisten-Eintrag mal alle Versandtermine heraussuche und Ihnen schicke - dann können Sie prüfen, ob einfach für jede Serienmail mehrere vorliegen (das würde für eine mehrfache Ausführung sprechen) oder ob ein anderes Muster auf andere Probleme im Code hindeutet.

> Ich habe mit der Randomisierung viel experimentiert, damit sie die Befragten auch wirklich zu möglichst verschiedenen Zeitpunkten erwischt und auf die Befragten auch random wirkt.

Hmm, ich hätte in Betracht gezogen, die Zufälligkeit ein wenig zu reduzieren - mit so vielen Randbedingungen ist es in der Tat nicht ganz trivial.

Mein Herangehen - wenn man wirklich unbeschränkt randomdisieren will - wäre hier gewesen, eine Liste mit rein zufälligen Terminen zu erstellen und alle, die gegen eine der Regeln verstoßen, zu entfernen - und so lange neue Termine zu erzeugen, bis genug "erlaubte" vorliegen. Diese Liste würde ich dann erst ganz am Ende auf die 40 Serienmails abbilden. Dadurch wäre sichergestellt, dass definitiv nur 40 Serienmails geplant werden.
by s293220 (130 points)
Wenn das möglich ist, gerne. Ich habe die geplanten Mails allerdings gestern schon wieder gelöscht und die Feldphase abgebrochen, die Adresseinträge selbst gibt es aber noch.

Das heißt aber auch: Ich könnte den Code jetzt nochmal verändern. Und mit der db set, sb get Logik und case serial eine Mehrfachausführung verhindern, richtig?

Ist caseSerial() im PHP-Code-Block des zweiten Fragebogens verfügbar, wenn der Teilnehmer automatisch vom ersten Fragebogen weitergeleitet wurde? Oder bezieht sie sich immer nur auf den aktuellen Fragebogen?

Könnte ich Ihnen meinen überarbeiten Code schicken?
by SoSci Survey (369k points)
> Das heißt aber auch: Ich könnte den Code jetzt nochmal verändern. Und mit der db set, sb get Logik und case serial eine Mehrfachausführung verhindern, richtig?

Ja, das ist korrekt.

> Ist caseSerial() im PHP-Code-Block des zweiten Fragebogens verfügbar, wenn der Teilnehmer automatisch vom ersten Fragebogen weitergeleitet wurde?

Normalerweise schon, wenn Sie keine besonders ungewöhnliche Art der Weiterleitung gewählt haben. Sie können ja einfach in Ihren Testdaten im Datensatz überprüfen, ob die Variable SERIAL den richtigen Wert hat.

Willkommen im Online-Support von SoSci Survey.

Hier bekommen Sie schnelle und fundierte Antworten von anderen Projektleitern und direkt von SoSci Survey.

→ Eine Frage stellen


Welcome to the SoSci Survey online support.

Simply ask a question to quickly get answers from other professionals, and directly from SoSci Survey.

→ Ask a Question

...