$observedAt) { continue; } // 从未分配重新指派时,写入侧可能把 from 回填为原医助,形成 A→A。 // 以实际时间线判断持有人是否变化,保留释放后的重新承接。 $previous = $lastTarget[$did] ?? null; if (($previous !== null && $previous === $aid) || ($previous === null && $from === $aid)) { continue; } $lastTarget[$did] = $aid; $isRollback = isset($rollbackKeySet[RevisitRateCalculator::rollbackKey( $did, $time, (int) ($r['operator_admin_id'] ?? 0) )]); $eventType = (int) ($r['is_inherit'] ?? 0) === 1 || $isRollback ? self::TYPE_INHERIT : self::TYPE_NEW; $selected = $aid > 0 && $time >= $startTs && $time <= $endTs && $eventType === $type && !isset($refundRejectSet[$did]); $timeline[$did][] = [ 'assistant_id' => $aid, 'time' => $time, 'selected' => $selected, 'type' => $eventType, ]; if (!$selected) { continue; } if (!isset($pairs[$aid][$did])) { $pairs[$aid][$did] = [ 'diagnosis_id' => $did, 'assistant_id' => $aid, 'assign_type' => $type, 'assign_count' => 0, 'first_assign_time' => $time, 'last_assign_time' => $time, 'is_rollback' => 0, ]; } $pairs[$aid][$did]['assign_count']++; $pairs[$aid][$did]['last_assign_time'] = $time; $pairs[$aid][$did]['is_rollback'] |= $isRollback ? 1 : 0; } $orders = []; $curDid = 0; $globalSeq = 0; $segmentSeq = 0; $ptr = 0; $offset = 0; $event = null; foreach ($orderRows as $r) { $did = (int) ($r['diagnosis_id'] ?? 0); $ct = (int) ($r['create_time'] ?? 0); if ($did <= 0 || $ct > $observedAt) { continue; } if ($curDid !== $did) { $curDid = $did; $globalSeq = 0; $segmentSeq = 0; $ptr = 0; $event = null; $offset = RevisitVisitSlotService::resolveRevisitSlotStartOffset($did, $offsetMap); } $globalSeq++; $tl = $timeline[$did] ?? []; while ($ptr < \count($tl) && $tl[$ptr]['time'] <= $ct) { $event = $tl[$ptr++]; $segmentSeq = 0; } if ($event === null) { continue; } $segmentSeq++; if (!$event['selected']) { continue; } $slot = $type === self::TYPE_INHERIT ? $segmentSeq + 1 : $globalSeq + $offset; if ($slot < 2 || $slot > RevisitRateCalculator::MAX_VISIT_SLOT) { continue; } $aid = $event['assistant_id']; // 重复承接同一患者时,明细保留首次达到该诊次的订单,与人数指标逐项对账。 if (isset($orders[$aid][$slot][$did])) { continue; } $r['assistant_id'] = $aid; $r['visit_slot'] = $slot; $r['global_visit_slot'] = $globalSeq + $offset; $r['cohort_assign_time'] = $event['time']; $orders[$aid][$slot][$did] = $r; } return ['pairsByAssistant' => $pairs, 'ordersByAssistant' => $orders, 'cohort_type' => $type]; } /** 部门与总计按诊单去重;每个诊次保留同一患者最早一笔订单。 */ public static function scopedOrders(array $calc, array $assistantIds, int $slot = 0): array { $out = []; foreach ($assistantIds as $aid) { foreach ($calc['ordersByAssistant'][$aid] ?? [] as $s => $byDiag) { if ($slot > 0 && $s !== $slot) { continue; } foreach ($byDiag as $did => $r) { $previous = $out[$s][$did] ?? null; if ($previous === null || [(int) $r['create_time'], (int) $r['id']] < [(int) $previous['create_time'], (int) $previous['id']]) { $out[$s][$did] = $r; } } } } return $out; } public static function metricPack(array $calc, array $assistantIds, array $slots): array { $diagSet = []; foreach ($assistantIds as $aid) { foreach ($calc['pairsByAssistant'][$aid] ?? [] as $did => $_) { $diagSet[$did] = true; } } $count = \count($diagSet); $inherit = $calc['cohort_type'] === self::TYPE_INHERIT; $pack = [ 'assigned_count' => $inherit ? 0 : $count, 'inherit_count' => $inherit ? $count : 0, 'assigned_total_count' => $count, ]; $orders = self::scopedOrders($calc, $assistantIds); $all = array_sum(array_map('count', $orders)); foreach ($slots as $slot) { $num = \count($orders[$slot] ?? []); $pack['visit' . $slot . '_count'] = $num; $pack['visit' . $slot . '_den'] = $count; $pack['visit' . $slot . '_rate'] = $count > 0 ? round($num / $count * 100, 2) : null; } $pack['visit_all_count'] = $all; $pack['visit_all_den'] = $count; $pack['visit_all_rate'] = $count > 0 ? round($all / $count * 100, 2) : null; return $pack; } /** 严格校验日期,避免非法日期被 strtotime 自动滚到次月。 */ public static function dateRange(array $params, int $now): array { $start = trim((string) ($params['start_date'] ?? '')); $end = trim((string) ($params['end_date'] ?? '')); if ($start === '' && $end === '') { $month = trim((string) ($params['month'] ?? '')); if (preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month) === 1) { $start = $month . '-01'; $end = date('Y-m-t', (int) strtotime($start)); } else { $start = date('Y-m-01', $now); $end = date('Y-m-d', $now); } } foreach ([$start, $end] as $date) { $parsed = \DateTimeImmutable::createFromFormat('!Y-m-d', $date); if ($parsed === false || $parsed->format('Y-m-d') !== $date) { throw new \InvalidArgumentException('请选择有效的开始日期和结束日期'); } } if ($start > $end) { throw new \InvalidArgumentException('开始日期不能晚于结束日期'); } return [$start, $end, (int) strtotime($start . ' 00:00:00'), (int) strtotime($end . ' 23:59:59')]; } }