(int) strtotime($dt); $fmt = static fn (int $t): string => date('Y-m-d H:i:s', $t); $cfg = [ 'snooze_intervals' => [1 => 14400, 2 => 7200, 3 => 3600, 'default' => 1800], 'work_hours' => ['09:00', '20:00'], 'promise_max_days' => 14, 'contact_max_per_cycle' => 2, ]; // ---------- snoozeInterval ---------- expect(L::snoozeInterval(1, $cfg['snooze_intervals']) === 14400, '第 1 次延迟 4h'); expect(L::snoozeInterval(2, $cfg['snooze_intervals']) === 7200, '第 2 次延迟 2h'); expect(L::snoozeInterval(3, $cfg['snooze_intervals']) === 3600, '第 3 次延迟 1h'); expect(L::snoozeInterval(4, $cfg['snooze_intervals']) === 1800, '第 4 次起回落 default 30min'); expect(L::snoozeInterval(99, $cfg['snooze_intervals']) === 1800, '第 99 次仍 30min'); expect(L::snoozeInterval(1, ['1' => '7200', 'default' => '600']) === 7200, '字符串配置可被归一'); expect(L::snoozeInterval(5, [1 => 100]) === L::DEFAULT_SNOOZE_SECONDS, '缺 default 时回落常量 1800'); expect(L::snoozeInterval(1, null) === L::DEFAULT_SNOOZE_SECONDS, '非数组配置回落 1800'); expect(L::snoozeInterval(1, [1 => 0, 'default' => -5]) === L::DEFAULT_SNOOZE_SECONDS, '非正数回落 1800'); expect(L::snoozeInterval(0, $cfg['snooze_intervals']) === 1800, 'n=0 取 default'); // ---------- parseWorkHours ---------- expect(L::parseWorkHours(['09:00', '20:00']) === [540, 1200], '09:00~20:00 → 540~1200 分钟'); expect(L::parseWorkHours(['08:30', '18:00']) === [510, 1080], '08:30~18:00'); expect(L::parseWorkHours(['09:00', '24:00']) === [540, 1440], '结束允许 24:00'); expect(L::parseWorkHours(['20:00', '09:00']) === [540, 1200], '开始不早于结束 → 默认'); expect(L::parseWorkHours(['9', '20']) === [540, 1200], '非 HH:mm → 默认'); expect(L::parseWorkHours(null) === [540, 1200], 'null → 默认'); expect(L::parseWorkHours(['09:00']) === [540, 1200], '少于两项 → 默认'); // ---------- clampToWorkHours ---------- $wh = $cfg['work_hours']; expect(L::clampToWorkHours($ts('2026-10-13 14:00:00'), $wh) === $ts('2026-10-13 14:00:00'), '工作时段内原样返回'); expect(L::clampToWorkHours($ts('2026-10-13 20:30:00'), $wh) === $ts('2026-10-14 09:00:00'), '20:30 → 次日 09:00'); expect(L::clampToWorkHours($ts('2026-10-13 20:00:00'), $wh) === $ts('2026-10-13 20:00:00'), '恰好 20:00:00 不顺延'); expect(L::clampToWorkHours($ts('2026-10-13 20:00:01'), $wh) === $ts('2026-10-14 09:00:00'), '20:00:01 顺延次日 09:00'); expect(L::clampToWorkHours($ts('2026-10-13 03:00:00'), $wh) === $ts('2026-10-13 09:00:00'), '凌晨 → 当日 09:00'); expect(L::clampToWorkHours($ts('2026-10-13 08:59:59'), $wh) === $ts('2026-10-13 09:00:00'), '08:59:59 → 09:00:00'); expect(L::clampToWorkHours($ts('2026-10-13 09:00:00'), $wh) === $ts('2026-10-13 09:00:00'), '恰好 09:00 原样'); expect(L::clampToWorkHours($ts('2026-10-31 23:30:00'), $wh) === $ts('2026-11-01 09:00:00'), '跨月顺延'); expect(L::clampToWorkHours($ts('2026-12-31 21:00:00'), $wh) === $ts('2027-01-01 09:00:00'), '跨年顺延'); expect(L::clampToWorkHours($ts('2026-10-13 18:30:00'), ['08:30', '18:00']) === $ts('2026-10-14 08:30:00'), '自定义时段 18:30 → 次日 08:30'); expect(L::clampToWorkHours($ts('2026-10-13 23:59:59'), ['09:00', '24:00']) === $ts('2026-10-13 23:59:59'), '24:00 结束时深夜不顺延'); // ---------- nextRemindAfterSnooze ---------- expect($fmt(L::nextRemindAfterSnooze($ts('2026-10-13 09:30:00'), 1, $cfg)) === '2026-10-13 13:30:00', '09:30 第 1 次 → 13:30'); expect($fmt(L::nextRemindAfterSnooze($ts('2026-10-13 17:30:00'), 1, $cfg)) === '2026-10-14 09:00:00', '17:30 + 4h 越过 20:00 → 次日 09:00'); expect($fmt(L::nextRemindAfterSnooze($ts('2026-10-13 18:30:00'), 3, $cfg)) === '2026-10-13 19:30:00', '18:30 第 3 次 +1h → 19:30'); expect($fmt(L::nextRemindAfterSnooze($ts('2026-10-13 19:50:00'), 4, $cfg)) === '2026-10-14 09:00:00', '19:50 第 4 次 +30min → 次日 09:00'); expect($fmt(L::nextRemindAfterSnooze($ts('2026-10-13 20:30:00'), 1, $cfg)) === '2026-10-14 09:00:00', '20:30 点延迟 → 次日 09:00(验收 §11-4-2)'); expect($fmt(L::nextRemindAfterSnooze($ts('2026-10-13 19:00:00'), 5, $cfg)) === '2026-10-13 19:30:00', '第 5 次 30min 仍在时段内'); expect(L::nextRemindAfterSnooze($ts('2026-10-13 10:00:00'), 1, []) === $ts('2026-10-13 10:30:00'), '空配置:30min + 默认时段'); expect(L::nextRemindAfterSnooze($ts('2026-10-13 10:00:00'), 1, $cfg) > $ts('2026-10-13 10:00:00'), '下次提醒恒晚于 now(条件 UPDATE 幂等前提)'); // ---------- promisedRemindAt ---------- expect($fmt(L::promisedRemindAt('2026-10-16', $wh)) === '2026-10-16 09:00:00', '承诺日 09:00'); expect($fmt(L::promisedRemindAt('2026-10-16', ['08:30', '18:00'])) === '2026-10-16 08:30:00', '承诺日取时段开始'); expect(L::promisedRemindAt('2026-13-40', $wh) === 0, '非法日期 → 0'); expect(L::promisedRemindAt('', $wh) === 0, '空 → 0'); expect($fmt(L::promisedRemindAt('2026-10-16 12:00:00', $wh)) === '2026-10-16 09:00:00', '带时间的串只取日期'); // ---------- isPromisedDateInRange ---------- expect(L::isPromisedDateInRange('2026-10-13', '2026-10-13', 14), '今天可选'); expect(L::isPromisedDateInRange('2026-10-27', '2026-10-13', 14), '今天 +14 可选'); expect(!L::isPromisedDateInRange('2026-10-28', '2026-10-13', 14), '今天 +15 不可选'); expect(!L::isPromisedDateInRange('2026-10-12', '2026-10-13', 14), '昨天不可选'); expect(!L::isPromisedDateInRange('bad', '2026-10-13', 14), '非法日期不可选'); expect(!L::isPromisedDateInRange('2026-02-30', '2026-02-13', 30), '不存在的日期不可选'); expect(L::isPromisedDateInRange('2026-10-13', '2026-10-13', 0), 'max=0 仅今天'); // ---------- remindTimeText ---------- $now = $ts('2026-10-13 10:00:00'); expect(L::remindTimeText($ts('2026-10-13 14:30:00'), $now) === '14:30', '当日 HH:mm'); expect(L::remindTimeText($ts('2026-10-14 09:00:00'), $now) === '明日 09:00', '次日前缀'); expect(L::remindTimeText($ts('2026-10-16 09:00:00'), $now) === '10-16 09:00', '更远 MM-DD HH:mm'); expect(L::remindTimeText(0, $now) === '', '0 → 空'); // ---------- escalationLevel(与 Calculator 一致) ---------- expect(C::escalationLevel(1) === 1 && C::escalationLevel(2) === 1, '1~2 次 → level 1'); expect(C::escalationLevel(3) === 2 && C::escalationLevel(4) === 2, '3~4 次 → level 2'); expect(C::escalationLevel(5) === 3, '第 5 次 → level 3 居中模态'); // ---------- toastEligible ---------- $row = static fn (array $o = []): array => array_replace([ 'closed_state' => 0, 'appointment_state' => 0, 'due_date' => '2026-10-10', 'promised_date' => null, ], $o); expect(L::toastEligible($row(), '2026-10-13'), '到期未挂号进弹窗'); expect(L::toastEligible($row(['due_date' => '2026-10-13']), '2026-10-13'), '今日到期进弹窗'); expect(!L::toastEligible($row(['due_date' => '2026-10-14']), '2026-10-13'), '未到期不进'); expect(L::toastEligible($row(['appointment_state' => 4, 'due_date' => '2026-10-20']), '2026-10-13'), '过号不受到期日限制'); expect(L::toastEligible($row(['appointment_state' => 4, 'due_date' => null]), '2026-10-13'), '过号且缺天数仍进'); expect(!L::toastEligible($row(['due_date' => null]), '2026-10-13'), '缺天数不进'); expect(!L::toastEligible($row(['promised_date' => '2026-10-13']), '2026-10-13'), '承诺日=今天不进'); expect(!L::toastEligible($row(['promised_date' => '2026-10-16']), '2026-10-13'), '承诺日未到不进'); expect(L::toastEligible($row(['promised_date' => '2026-10-12']), '2026-10-13'), '承诺日已过再弹'); expect(!L::toastEligible($row(['closed_state' => 4]), '2026-10-13'), '人工关闭不进'); expect(!L::toastEligible($row(['closed_state' => 2]), '2026-10-13'), '已挂号不进'); expect(L::toastEligible($row(['promised_date' => '0000-00-00']), '2026-10-13'), '0000-00-00 视为无承诺'); echo "RevisitDueLogicTest OK ({$checks} checks)\n";