(int) strtotime($dt); /** 基础输入:订单 09-20 10:00 下单、28 天 */ $baseInput = static function (array $override = []) use ($ts): array { return array_replace([ 'diagnosis_id' => 1, 'assistant_id' => 7, 'patient_name' => '张三', 'latest_order' => ['id' => 100, 'create_time' => $ts('2026-09-20 10:00:00'), 'medication_days' => 28, 'prescription_id' => 55], 'valid_order_count' => 3, 'offset' => 0, 'rx_usage_days' => null, 'sign_ts' => 0, 'ship_ts' => 0, 'appointments' => [], 'first_touch_ts' => $ts('2026-03-02 09:00:00'), 'total_medication_days' => 84, ], $override); }; // ---------- 锚点回退链 ---------- $f = C::computeFacts($baseInput(['sign_ts' => $ts('2026-09-22 15:30:00'), 'ship_ts' => $ts('2026-09-21 08:00:00')]), $cfg, '2026-10-01'); expect($f['anchor_src'] === C::ANCHOR_SIGN && $f['anchor_ts'] === $ts('2026-09-22 15:30:00'), '锚点:有签收优先签收'); expect($f['due_date'] === '2026-10-20', '锚点签收 09-22 + 28 天 = 10-20'); $f = C::computeFacts($baseInput(['ship_ts' => $ts('2026-09-21 08:00:00')]), $cfg, '2026-10-01'); expect($f['anchor_src'] === C::ANCHOR_SHIP && $f['due_date'] === '2026-10-19', '锚点:无签收回落发货 09-21 → 10-19'); $f = C::computeFacts($baseInput(), $cfg, '2026-10-01'); expect($f['anchor_src'] === C::ANCHOR_CREATE && $f['due_date'] === '2026-10-18', '锚点:都没有用下单日 09-20 → 10-18'); $f = C::computeFacts($baseInput(['sign_ts' => $ts('2026-09-22 15:30:00')]), ['anchor_chain' => ['create']], '2026-10-01'); expect($f['anchor_src'] === C::ANCHOR_CREATE, 'anchor_chain 收窄为 create 时忽略签收'); $f = C::computeFacts($baseInput(['sign_ts' => $ts('2026-09-22 15:30:00')]), ['anchor_chain' => ['ship']], '2026-10-01'); expect($f['anchor_src'] === C::ANCHOR_CREATE, '链耗尽无结果时兜底下单时间'); $f = C::computeFacts($baseInput(['sign_ts' => $ts('2026-09-22 15:30:00')]), ['anchor_chain' => 'ship,sign'], '2026-10-01'); expect($f['anchor_src'] === C::ANCHOR_SIGN, 'anchor_chain 支持逗号字符串'); // ---------- 天数回落 ---------- $f = C::computeFacts($baseInput(['rx_usage_days' => 14]), $cfg, '2026-10-01'); expect($f['medication_days'] === 28 && $f['days_src'] === C::DAYS_SRC_ORDER, '天数:订单有值用订单'); $f = C::computeFacts($baseInput(['latest_order' => ['id' => 100, 'create_time' => $ts('2026-09-20 10:00:00'), 'medication_days' => 0, 'prescription_id' => 55], 'rx_usage_days' => 14]), $cfg, '2026-10-01'); expect($f['medication_days'] === 14 && $f['days_src'] === C::DAYS_SRC_RX && $f['due_date'] === '2026-10-04', '天数:订单缺失回落处方 14'); $f = C::computeFacts($baseInput(['latest_order' => ['id' => 100, 'create_time' => $ts('2026-09-20 10:00:00'), 'medication_days' => null, 'prescription_id' => 55], 'rx_usage_days' => null]), $cfg, '2026-10-01'); expect($f['medication_days'] === 0 && $f['days_src'] === C::DAYS_SRC_NONE && $f['due_date'] === null, '天数:都缺失 → 0 且无 due_date'); expect($f['closed_state_auto'] === C::CLOSED_OPEN, '缺天数仍为进行中(由读取侧推导 no_days)'); $f = C::computeFacts($baseInput(['latest_order' => ['id' => 100, 'create_time' => $ts('2026-09-20 10:00:00'), 'medication_days' => 0, 'prescription_id' => 55], 'rx_usage_days' => 14]), ['days_fallback_rx' => false], '2026-10-01'); expect($f['medication_days'] === 0, 'days_fallback_rx=false 不回落处方'); $f = C::computeFacts($baseInput(['latest_order' => ['id' => 100, 'create_time' => $ts('2026-09-20 10:00:00'), 'medication_days' => 0, 'prescription_id' => 0]]), ['days_default' => 7], '2026-10-01'); expect($f['medication_days'] === 7 && $f['days_src'] === C::DAYS_SRC_NONE && $f['due_date'] === '2026-09-27', 'days_default>0 时兜底天数且 days_src 仍为 0'); // ---------- 档位 ---------- $f = C::computeFacts($baseInput(['latest_order' => ['id' => 100, 'create_time' => $ts('2026-09-20 10:00:00'), 'medication_days' => 15, 'prescription_id' => 55]]), $cfg, '2026-10-01'); expect($f['rule_days'] === 0 && $f['due_date'] === '2026-10-05', '15 天不归 14 档,到期仍按实际天数'); $f = C::computeFacts($baseInput(), $cfg, '2026-10-01'); expect($f['rule_days'] === 28, '28 天命中档位'); $f = C::computeFacts($baseInput(), ['grace_days' => 2], '2026-10-01'); expect($f['due_date'] === '2026-10-20', 'grace_days 顺延'); // ---------- 诊次 / 首次触达 / 失效 ---------- $f = C::computeFacts($baseInput(['offset' => 2]), $cfg, '2026-10-01'); expect($f['current_slot'] === 5 && $f['next_slot'] === 6, 'current_slot = 有效单 3 + 偏移 2'); expect($f['first_touch_date'] === '2026-03-02', 'first_touch_date 由时间戳转日期'); $f = C::computeFacts($baseInput(['offset' => 99]), $cfg, '2026-10-01'); expect($f['current_slot'] === 23, '偏移钳到 20'); $f = C::computeFacts($baseInput(['latest_order' => null, 'valid_order_count' => 0]), $cfg, '2026-10-01'); expect($f['closed_state_auto'] === C::CLOSED_INVALID && $f['source_order_id'] === 0 && $f['due_date'] === null, '无有效订单 → 失效'); $f = C::computeFacts($baseInput(['assistant_id' => 0]), $cfg, '2026-10-01'); expect($f['closed_state_auto'] === C::CLOSED_INVALID, '无医助 → 失效'); $f = C::computeFacts($baseInput(['diagnosis_active' => false]), $cfg, '2026-10-01'); expect($f['closed_state_auto'] === C::CLOSED_INVALID, '诊单删除/禁用 → 失效'); // ---------- 挂号选择 ---------- $appts = [ ['id' => 1, 'status' => 3, 'appointment_date' => '2026-09-19', 'appointment_time' => '10:00:00'], // 订单日之前,忽略 ['id' => 2, 'status' => 2, 'appointment_date' => '2026-10-21', 'appointment_time' => '10:00:00'], // 已取消,忽略 ['id' => 3, 'status' => 1, 'appointment_date' => '2026-10-23', 'appointment_time' => ''], ['id' => 4, 'status' => 1, 'appointment_date' => '2026-10-23', 'appointment_time' => '09:30:00'], ]; $f = C::computeFacts($baseInput(['appointments' => $appts]), $cfg, '2026-10-21'); expect($f['appointment_state'] === 1 && $f['appointment_date'] === '2026-10-23' && $f['booked_appointment_id'] === 4, '挂号取锚点日之后最新一条(空时间视为 00:00:00,排在 09:30 之前)'); expect($f['closed_state_auto'] === C::CLOSED_BOOKED, '已约且日期未过 → 自动关闭 2'); $f = C::computeFacts($baseInput(['appointments' => $appts]), $cfg, '2026-10-24'); expect($f['appointment_state'] === 1 && $f['closed_state_auto'] === C::CLOSED_OPEN, '已约但日期已过(cron 未跑)→ 进行中'); $f = C::computeFacts($baseInput(['appointments' => [['id' => 9, 'status' => 3, 'appointment_date' => '2026-10-23', 'appointment_time' => '10:00:00']]]), $cfg, '2026-10-24'); expect($f['appointment_state'] === 3 && $f['closed_state_auto'] === C::CLOSED_VISITED && $f['booked_appointment_id'] === 9, '已面诊 → 自动关闭 3'); $f = C::computeFacts($baseInput(['appointments' => [['id' => 9, 'status' => 4, 'appointment_date' => '2026-10-23', 'appointment_time' => '10:00:00']]]), $cfg, '2026-10-23'); expect($f['appointment_state'] === 4 && $f['closed_state_auto'] === C::CLOSED_OPEN, '过号 → 进行中'); // 下单当天(及更早)的面诊是产生这笔药的那次面诊,不算复诊;次日起才算(appointment_min_gap_days 默认 1) $f = C::computeFacts($baseInput(['appointments' => [['id' => 11, 'status' => 3, 'appointment_date' => '2026-09-20', 'appointment_time' => '09:30:00']]]), $cfg, '2026-09-21'); expect($f['appointment_state'] === 0 && $f['closed_state_auto'] === C::CLOSED_OPEN && $f['booked_appointment_id'] === 0, '下单当天的已面诊不计入,周期保持进行中'); $f = C::computeFacts($baseInput(['appointments' => [['id' => 12, 'status' => 3, 'appointment_date' => '2026-09-21', 'appointment_time' => '09:30:00']]]), $cfg, '2026-09-22'); expect($f['appointment_state'] === 3 && $f['closed_state_auto'] === C::CLOSED_VISITED, '下单次日的已面诊计入 → 自动关闭 3'); $f = C::computeFacts($baseInput(['appointments' => [['id' => 12, 'status' => 3, 'appointment_date' => '2026-09-21', 'appointment_time' => '09:30:00']]]), ['appointment_min_gap_days' => 2] + $cfg, '2026-09-22'); expect($f['appointment_state'] === 0, 'appointment_min_gap_days=2 时下单次日的面诊也不计入'); // ---------- deriveState ---------- $row = static function (array $override = []): array { return array_replace([ 'closed_state' => 0, 'assistant_id' => 7, 'source_order_id' => 100, 'valid_order_count' => 3, 'medication_days' => 28, 'due_date' => '2026-10-20', 'appointment_state' => 0, 'appointment_date' => null, 'promised_date' => null, 'snooze_count' => 0, ], $override); }; $s = C::deriveState($row(['appointment_state' => 3, 'appointment_date' => '2026-10-23']), '2026-10-30', $cfg); expect($s['state'] === C::STATE_VISITED && $s['state_text'] === '已面诊 10-23' && $s['severe'] === 0, 'visited 优先且不深红'); $s = C::deriveState($row(['appointment_state' => 1, 'appointment_date' => '2026-10-23']), '2026-10-21', $cfg); expect($s['state'] === C::STATE_BOOKED && $s['state_text'] === '已约 10-23', 'booked'); $s = C::deriveState($row(['appointment_state' => 1, 'appointment_date' => '2026-10-21']), '2026-10-21', $cfg); expect($s['state'] === C::STATE_BOOKED, 'booked:预约日 = today 仍为已约'); $s = C::deriveState($row(['appointment_state' => 4, 'appointment_date' => '2026-10-23']), '2026-10-23', $cfg); expect($s['state'] === C::STATE_MISSED && $s['state_text'] === '过号 10-23', 'missed:status=4'); $s = C::deriveState($row(['appointment_state' => 1, 'appointment_date' => '2026-10-22']), '2026-10-23', $cfg); expect($s['state'] === C::STATE_MISSED, 'missed:已约日期已过(cron 未跑兜底)'); $s = C::deriveState($row(['appointment_state' => 4, 'appointment_date' => '2026-10-02', 'due_date' => '2026-10-20']), '2026-10-02', $cfg); expect($s['state'] === C::STATE_MISSED && $s['days_left'] === 18 && $s['overdue_days'] === 0, 'missed 不受 due_date 限制(到期前过号同样标红)'); $s = C::deriveState($row(['appointment_state' => 4, 'appointment_date' => '2026-10-23', 'promised_date' => '2026-10-30']), '2026-10-23', $cfg); expect($s['state'] === C::STATE_MISSED, 'missed 优先于 contacted'); $s = C::deriveState($row(['appointment_state' => 4, 'appointment_date' => '2026-10-28']), '2026-10-28', $cfg); expect($s['state'] === C::STATE_MISSED && $s['overdue_days'] === 8 && $s['severe'] === 1, 'missed 且超期 ≥7 天 → 深红'); $s = C::deriveState($row(['promised_date' => '2026-10-25']), '2026-10-22', $cfg); expect($s['state'] === C::STATE_CONTACTED && $s['state_text'] === '已联系·约 10-25' && $s['severe'] === 0, 'contacted 覆盖 overdue'); $s = C::deriveState($row(['promised_date' => '2026-10-22']), '2026-10-22', $cfg); expect($s['state'] === C::STATE_CONTACTED, 'contacted:承诺日 = today 仍为已联系'); $s = C::deriveState($row(['promised_date' => '2026-10-21']), '2026-10-22', $cfg); expect($s['state'] === C::STATE_OVERDUE, '承诺日已过 → 回到 overdue'); $s = C::deriveState($row(), '2026-10-20', $cfg); expect($s['state'] === C::STATE_OVERDUE && $s['state_text'] === '今日应挂号' && $s['overdue_days'] === 0 && $s['days_left'] === 0 && $s['severe'] === 0, 'overdue:到期日当天'); $s = C::deriveState($row(), '2026-10-26', $cfg); expect($s['state'] === C::STATE_OVERDUE && $s['state_text'] === '超期 6 天' && $s['severe'] === 0, 'overdue 6 天未深红'); $s = C::deriveState($row(), '2026-10-27', $cfg); expect($s['state'] === C::STATE_OVERDUE && $s['overdue_days'] === 7 && $s['severe'] === 1, 'overdue 7 天深红'); $s = C::deriveState($row(), '2026-10-27', ['severe_overdue_days' => [14, 21, 28, 30]]); expect($s['severe'] === 0, 'severe 阶梯数组取最小阈值 14:7 天不深红'); $s = C::deriveState($row(), '2026-11-03', ['severe_overdue_days' => [14, 21, 28, 30]]); expect($s['severe'] === 1, 'severe 阶梯数组:14 天深红'); $s = C::deriveState($row(), '2026-10-19', $cfg); expect($s['state'] === C::STATE_WARNING && $s['state_text'] === '剩 1 天' && $s['days_left'] === 1, 'warning:剩 1 天'); $s = C::deriveState($row(), '2026-10-17', $cfg); expect($s['state'] === C::STATE_WARNING && $s['days_left'] === 3, 'warning 边界:剩 3 天 = warn_days'); $s = C::deriveState($row(), '2026-10-16', $cfg); expect($s['state'] === C::STATE_PENDING && $s['state_text'] === '剩 4 天' && $s['overdue_days'] === 0, 'pending 边界:剩 4 天'); $s = C::deriveState($row(), '2026-10-16', ['warn_days' => 5]); expect($s['state'] === C::STATE_WARNING, 'warn_days 可配'); $s = C::deriveState($row(['due_date' => null, 'medication_days' => 0]), '2026-10-16', $cfg); expect($s['state'] === C::STATE_NO_DAYS && $s['state_text'] === '缺开药天数' && $s['days_left'] === null && $s['overdue_days'] === null, 'no_days'); $s = C::deriveState($row(['due_date' => '', 'medication_days' => 0, 'appointment_state' => 4, 'appointment_date' => '2026-10-10']), '2026-10-10', $cfg); expect($s['state'] === C::STATE_MISSED, '缺天数但过号 → missed 优先'); $s = C::deriveState($row(['valid_order_count' => 0, 'source_order_id' => 0]), '2026-10-16', $cfg); expect($s['state'] === C::STATE_NONE && $s['state_text'] === '' && $s['days_left'] === null, 'none:无有效订单'); $s = C::deriveState($row(['assistant_id' => 0]), '2026-10-16', $cfg); expect($s['state'] === C::STATE_NONE, 'none:无医助'); $s = C::deriveState($row(['closed_state' => 5]), '2026-10-16', $cfg); expect($s['state'] === C::STATE_NONE, 'none:closed_state=5'); $s = C::deriveState($row(['closed_state' => 4]), '2026-10-27', $cfg); expect($s['state'] === C::STATE_OVERDUE && $s['state_text'] === '已关闭·超期 7 天' && $s['severe'] === 0, '人工关闭保留事实状态、前缀文案、不深红'); $s = C::deriveState($row(['closed_state' => 4, 'appointment_state' => 3, 'appointment_date' => '2026-10-23']), '2026-10-27', $cfg); expect($s['state'] === C::STATE_VISITED && $s['state_text'] === '已面诊 10-23', '人工关闭后已面诊文案不加前缀'); $s = C::deriveState($row(['closed_state' => 2, 'appointment_state' => 1, 'appointment_date' => '2026-10-23']), '2026-10-24', $cfg); expect($s['state'] === C::STATE_MISSED, 'closed_state=2 但预约日已过 → 事实推导 missed'); // ---------- escalationLevel ---------- expect(C::escalationLevel(0) === 0, 'level 0'); expect(C::escalationLevel(1) === 1 && C::escalationLevel(2) === 1, 'level 1 (1~2)'); expect(C::escalationLevel(3) === 2 && C::escalationLevel(4) === 2, 'level 2 (3~4)'); expect(C::escalationLevel(5) === 3 && C::escalationLevel(99) === 3, 'level 3 (>=5)'); expect(C::escalationLevel(-1) === 0, '负数视为 0'); // ---------- 日期工具 ---------- expect(C::dayDiff('2026-10-20', '2026-10-27') === 7 && C::dayDiff('2026-10-27', '2026-10-20') === -7 && C::dayDiff('2026-10-20', '2026-10-20') === 0, 'dayDiff 正负零'); expect(C::dayDiff('2026-02-27', '2026-03-02') === 3, 'dayDiff 跨月(2026 非闰年)'); expect(C::addDays('2026-09-22', 28) === '2026-10-20' && C::addDays('2026-12-31', 1) === '2027-01-01' && C::addDays('2026-01-01', -1) === '2025-12-31', 'addDays 跨月跨年'); expect(C::dayDiff('bad', '2026-10-20') === 0 && C::addDays('bad', 3) === 'bad', '非法日期容错'); // ---------- normalizeConfig ---------- $n = C::normalizeConfig(['anchor_chain' => ['SIGN', 'x', 'create', 'create'], 'day_buckets' => ['14', 21, 0], 'warn_days' => '2']); expect($n['anchor_chain'] === ['sign', 'create'] && $n['day_buckets'] === [14, 21] && $n['warn_days'] === 2, 'normalizeConfig 归一'); expect($n['severe_overdue_days'] === 7 && $n['days_fallback_rx'] === true, 'normalizeConfig 补默认值'); echo "RevisitDueCalculatorTest OK ({$checks} checks)\n";