*/ public static function overview(array $params, int $adminId, array $adminInfo): array { $range = self::resolveRange((string) ($params['time_type'] ?? 'month')); $scopeValue = DataScopeService::getEffectiveScope($adminInfo); $roleIds = self::normalizeIds(Db::name('admin_role')->where('admin_id', $adminId)->column('role_id')); $isRoot = (int) ($adminInfo['root'] ?? 0) === 1; $doctorSelf = !$isRoot && $scopeValue === DataScopeService::SCOPE_SELF && in_array(self::DOCTOR_ROLE_ID, $roleIds, true); $activeOnly = (int) ($params['active_only'] ?? 1) !== 0; $selectedDeptId = $doctorSelf ? 0 : max(0, (int) ($params['dept_id'] ?? 0)); $selectedDoctorId = max(0, (int) ($params['doctor_id'] ?? 0)); $threshold = min(100.0, max(1.0, (float) ($params['alert_threshold'] ?? 15))); $allDoctorOptions = self::doctorOptions($activeOnly, $doctorSelf ? $adminId : 0); $doctorIds = self::normalizeIds(array_column($allDoctorOptions, 'id')); if ($selectedDoctorId > 0) { $doctorIds = in_array($selectedDoctorId, $doctorIds, true) ? [$selectedDoctorId] : []; } $allowedDeptSet = DataScopeService::getAllowedDeptIdSet($adminId, $adminInfo); [$selectedDeptIds, $deptSelectionValid] = self::resolveSelectedDeptIds( $selectedDeptId, $allowedDeptSet ); $assistantIds = self::resolveAssistantScope( $adminId, $adminInfo, $doctorSelf, $selectedDeptId, $selectedDeptIds, $deptSelectionValid ); $stats = DoctorDailyStatsLogic::overview( [ 'start_date' => $range['start'], 'end_date' => $range['end'], ], $adminId, $adminInfo, $doctorIds, $assistantIds ); $doctorDeptNames = self::doctorDepartmentNames($doctorIds); $doctorStatus = self::doctorStatusMap($doctorIds); $rows = self::enrichRows( is_array($stats['rows'] ?? null) ? $stats['rows'] : [], $doctorDeptNames, $doctorStatus ); $summary = self::buildSummary($rows); $trend = self::buildAmountTrend($doctorIds, $assistantIds); $selectedDeptName = $selectedDeptId > 0 ? (string) (Db::name('dept')->where('id', $selectedDeptId)->whereNull('delete_time')->value('name') ?? '') : ''; $selectedDoctorName = ''; if ($selectedDoctorId > 0) { foreach ($allDoctorOptions as $doctor) { if ((int) ($doctor['id'] ?? 0) === $selectedDoctorId) { $selectedDoctorName = (string) ($doctor['name'] ?? ''); break; } } } return [ 'meta' => [ 'time_type' => $range['type'], 'time_label' => $range['label'], 'start_date' => $range['start'], 'end_date' => $range['end'], 'generated_at' => date('Y-m-d H:i:s'), 'scope_value' => $scopeValue, 'scope_label' => $doctorSelf ? '医生本人' : DataScopeService::scopeLabel($scopeValue), 'scope_kind' => $doctorSelf ? 'doctor_self' : ($assistantIds === null ? 'all' : 'assistant_scope'), 'selected_dept_name' => $selectedDeptName, 'selected_doctor_name' => $selectedDoctorName, 'doctor_count' => count($rows), 'appointment_rule' => '总挂号包含已预约、已取消、已完成和已过号;面诊取状态为已完成的挂号', 'performance_rule' => '诊单按订单创建时间统计,排除已取消、拒收、全额退款及部分退款,金额归属处方开方医生', ], 'filters' => [ 'departments' => $doctorSelf ? [] : DeptLogic::getAllDataScoped($adminId, $adminInfo), 'doctors' => $allDoctorOptions, 'can_filter_department' => !$doctorSelf, ], 'summary' => $summary, 'rankings' => [ 'amounts' => self::ranking($rows, 'deal_amount', 8), 'conversion' => self::ranking($rows, 'receive_conversion_rate', 8), ], 'funnel' => [ ['key' => 'appointment', 'label' => '挂号', 'value' => (int) $summary['appointment_total']], ['key' => 'interview', 'label' => '面诊', 'value' => (int) $summary['interview_count']], ['key' => 'receive', 'label' => '接诊', 'value' => (int) $summary['order_count']], ['key' => 'deal', 'label' => '成交', 'value' => (int) $summary['order_count']], ], 'trend' => $trend, 'alerts' => self::alertRows($rows, $threshold), 'alert_threshold' => $threshold, 'rows' => $rows, ]; } /** @return array */ private static function resolveRange(string $type): array { $today = date('Y-m-d'); if ($type === 'today') { return ['type' => 'today', 'label' => '今日', 'start' => $today, 'end' => $today]; } if ($type === 'week') { return [ 'type' => 'week', 'label' => '本周', 'start' => date('Y-m-d', strtotime('monday this week')), 'end' => $today, ]; } return ['type' => 'month', 'label' => '本月', 'start' => date('Y-m-01'), 'end' => $today]; } /** @return array */ private static function doctorOptions(bool $activeOnly, int $selfDoctorId = 0): array { $query = Db::name('admin')->alias('a') ->join('admin_role ar', 'ar.admin_id = a.id') ->where('ar.role_id', self::DOCTOR_ROLE_ID) ->whereNull('a.delete_time'); if ($activeOnly) { $query->where('a.disable', 0); } if ($selfDoctorId > 0) { $query->where('a.id', $selfDoctorId); } return $query->field('a.id, a.name, a.disable') ->distinct(true) ->order('a.disable', 'asc') ->order('a.name', 'asc') ->select() ->toArray(); } /** @param array|null $allowedSet @return array{0:array,1:bool} */ private static function resolveSelectedDeptIds(int $selectedDeptId, ?array $allowedSet): array { if ($selectedDeptId <= 0) { return [[], true]; } $ids = self::normalizeIds(DeptLogic::getSelfAndDescendantIds($selectedDeptId)); if ($allowedSet !== null) { $ids = array_values(array_filter($ids, static fn (int $id): bool => isset($allowedSet[$id]))); } return [$ids, $ids !== []]; } /** * null 表示医生本人或 ALL,不附加医助过滤;数组表示必须按这些医助经手的数据收窄。 * * @param int[] $selectedDeptIds * @return int[]|null */ private static function resolveAssistantScope( int $adminId, array $adminInfo, bool $doctorSelf, int $selectedDeptId, array $selectedDeptIds, bool $deptSelectionValid ): ?array { if ($doctorSelf) { return null; } if (!$deptSelectionValid) { return []; } $visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo); $assistantIds = self::activeAssistantIds($visibleIds); if ($selectedDeptId <= 0) { return $visibleIds === null ? null : $assistantIds; } $deptAssistantIds = self::activeAssistantIdsByDepartment($selectedDeptIds); if ($visibleIds === null) { return $deptAssistantIds; } return array_values(array_intersect($assistantIds, $deptAssistantIds)); } /** @param int[]|null $visibleIds @return int[] */ private static function activeAssistantIds(?array $visibleIds): array { if ($visibleIds === []) { return []; } $query = Db::name('admin')->alias('a') ->join('admin_role ar', 'ar.admin_id = a.id') ->where('ar.role_id', self::ASSISTANT_ROLE_ID) ->where('a.disable', 0) ->whereNull('a.delete_time'); if ($visibleIds !== null) { $query->whereIn('a.id', $visibleIds); } return self::normalizeIds($query->column('a.id')); } /** @param int[] $deptIds @return int[] */ private static function activeAssistantIdsByDepartment(array $deptIds): array { if ($deptIds === []) { return []; } return self::normalizeIds(Db::name('admin')->alias('a') ->join('admin_role ar', 'ar.admin_id = a.id') ->join('admin_dept ad', 'ad.admin_id = a.id') ->where('ar.role_id', self::ASSISTANT_ROLE_ID) ->whereIn('ad.dept_id', $deptIds) ->where('a.disable', 0) ->whereNull('a.delete_time') ->distinct(true) ->column('a.id')); } /** @param int[] $doctorIds @return array */ private static function doctorDepartmentNames(array $doctorIds): array { if ($doctorIds === []) { return []; } $rows = AdminDept::alias('ad') ->join('dept d', 'd.id = ad.dept_id AND d.delete_time IS NULL') ->whereIn('ad.admin_id', $doctorIds) ->field('ad.admin_id, d.name') ->order('d.sort', 'desc') ->select() ->toArray(); $out = []; foreach ($rows as $row) { $id = (int) ($row['admin_id'] ?? 0); $name = trim((string) ($row['name'] ?? '')); if ($id > 0 && $name !== '' && !isset($out[$id])) { $out[$id] = $name; } } return $out; } /** @param int[] $doctorIds @return array */ private static function doctorStatusMap(array $doctorIds): array { if ($doctorIds === []) { return []; } $rows = Db::name('admin')->whereIn('id', $doctorIds)->field('id, disable')->select()->toArray(); $out = []; foreach ($rows as $row) { $out[(int) $row['id']] = (int) ($row['disable'] ?? 0); } return $out; } /** @param array> $rows @return array> */ private static function enrichRows(array $rows, array $deptNames, array $statusMap): array { $out = []; foreach ($rows as $row) { $id = (int) ($row['admin_id'] ?? 0); $appointmentTotal = (int) ($row['appointment_total'] ?? 0); $interviewCount = (int) ($row['appointment_completed'] ?? 0); $orderCount = (int) ($row['deal_order_count'] ?? 0); $out[] = array_merge($row, [ 'doctor_id' => $id, 'department_name' => (string) ($deptNames[$id] ?? '未分配部门'), 'interview_count' => $interviewCount, 'order_count' => $orderCount, 'appointment_completion_rate' => $appointmentTotal > 0 ? round($interviewCount / $appointmentTotal * 100, 2) : null, 'receive_conversion_rate' => $interviewCount > 0 ? round($orderCount / $interviewCount * 100, 2) : null, 'status' => (int) ($statusMap[$id] ?? 0) === 0 ? 'active' : 'disabled', ]); } usort($out, static fn (array $a, array $b): int => (($b['deal_amount'] ?? 0) <=> ($a['deal_amount'] ?? 0)) ?: strcmp((string) $a['doctor_name'], (string) $b['doctor_name'])); return $out; } /** @param array> $rows @return array */ private static function buildSummary(array $rows): array { $appointmentTotal = 0; $interviewCount = 0; $orderCount = 0; $dealAmount = 0.0; $missed = 0; $cancelled = 0; foreach ($rows as $row) { $appointmentTotal += (int) ($row['appointment_total'] ?? 0); $interviewCount += (int) ($row['interview_count'] ?? 0); $orderCount += (int) ($row['order_count'] ?? 0); $dealAmount += (float) ($row['deal_amount'] ?? 0); $missed += (int) ($row['appointment_missed'] ?? 0); $cancelled += (int) ($row['appointment_cancelled'] ?? 0); } return [ 'appointment_total' => $appointmentTotal, 'interview_count' => $interviewCount, 'order_count' => $orderCount, 'deal_amount' => round($dealAmount, 2), 'avg_order_amount' => $orderCount > 0 ? round($dealAmount / $orderCount, 2) : null, 'appointment_completion_rate' => $appointmentTotal > 0 ? round($interviewCount / $appointmentTotal * 100, 2) : null, 'receive_conversion_rate' => $interviewCount > 0 ? round($orderCount / $interviewCount * 100, 2) : null, 'missed_count' => $missed, 'cancelled_count' => $cancelled, ]; } /** @param array> $rows @return array> */ private static function ranking(array $rows, string $field, int $limit): array { $ranked = $rows; usort($ranked, static fn (array $a, array $b): int => (($b[$field] ?? 0) <=> ($a[$field] ?? 0)) ?: strcmp((string) $a['doctor_name'], (string) $b['doctor_name'])); $out = []; foreach (array_slice($ranked, 0, $limit) as $row) { $out[] = [ 'doctor_id' => (int) ($row['doctor_id'] ?? 0), 'name' => (string) ($row['doctor_name'] ?? ''), 'value' => round((float) ($row[$field] ?? 0), 2), 'interview_count' => (int) ($row['interview_count'] ?? 0), 'order_count' => (int) ($row['order_count'] ?? 0), ]; } return $out; } /** @param int[] $doctorIds @param int[]|null $assistantIds @return array */ private static function buildAmountTrend(array $doctorIds, ?array $assistantIds): array { $endDate = date('Y-m-d'); $startDate = date('Y-m-d', strtotime('-' . (self::TREND_DAYS - 1) . ' days')); $amountByDate = []; if ($doctorIds !== [] && $assistantIds !== []) { $query = Db::name('tcm_prescription_order')->alias('o') ->join('tcm_prescription rx', 'rx.id = o.prescription_id AND rx.delete_time IS NULL', 'INNER') ->whereNull('o.delete_time') ->whereIn('rx.creator_id', $doctorIds) ->where('o.diagnosis_id', '>', 0) ->where('o.create_time', 'between', [ strtotime($startDate . ' 00:00:00'), strtotime($endDate . ' 23:59:59'), ]); YejiStatsLogic::applyPrescriptionOrderEffectiveAmountQuery($query, 'o'); if ($assistantIds !== null) { $query->whereIn('o.creator_id', $assistantIds); } $rows = $query ->fieldRaw("FROM_UNIXTIME(o.create_time, '%Y-%m-%d') AS date_label, SUM(o.amount) AS amount_sum") ->group('date_label') ->order('date_label', 'asc') ->select() ->toArray(); foreach ($rows as $row) { $date = (string) ($row['date_label'] ?? ''); if ($date !== '') { $amountByDate[$date] = round((float) ($row['amount_sum'] ?? 0), 2); } } } $dates = []; $labels = []; $amounts = []; for ($offset = 0; $offset < self::TREND_DAYS; $offset++) { $date = date('Y-m-d', strtotime($startDate . ' +' . $offset . ' days')); $dates[] = $date; $labels[] = date('m-d', strtotime($date)); $amounts[] = (float) ($amountByDate[$date] ?? 0); } return [ 'start_date' => $startDate, 'end_date' => $endDate, 'dates' => $dates, 'labels' => $labels, 'amounts' => $amounts, ]; } /** @param array> $rows @return array> */ private static function alertRows(array $rows, float $threshold): array { $alerts = array_values(array_filter($rows, static function (array $row) use ($threshold): bool { $interviews = (int) ($row['interview_count'] ?? 0); $rate = $row['receive_conversion_rate'] ?? null; return $interviews > 0 && ($rate === null || (float) $rate < $threshold); })); usort($alerts, static fn (array $a, array $b): int => (($a['receive_conversion_rate'] ?? -1) <=> ($b['receive_conversion_rate'] ?? -1)) ?: (($b['interview_count'] ?? 0) <=> ($a['interview_count'] ?? 0))); return array_map(static function (array $row) use ($threshold): array { $rate = (float) ($row['receive_conversion_rate'] ?? 0); return [ 'doctor_id' => (int) ($row['doctor_id'] ?? 0), 'doctor_name' => (string) ($row['doctor_name'] ?? ''), 'department_name' => (string) ($row['department_name'] ?? ''), 'interview_count' => (int) ($row['interview_count'] ?? 0), 'order_count' => (int) ($row['order_count'] ?? 0), 'rate' => round($rate, 2), 'severity' => $rate < $threshold / 2 ? 'high' : 'medium', 'suggestion' => (int) ($row['order_count'] ?? 0) === 0 ? '当前有面诊但无接诊诊单,建议核对诊单及跟进记录' : '接诊转化低于预警线,建议复盘患者需求与沟通记录', ]; }, $alerts); } /** @param array $ids @return int[] */ private static function normalizeIds(array $ids): array { return array_values(array_unique(array_filter(array_map('intval', $ids), static fn (int $id): bool => $id > 0))); } }