*/ public static function overview(int $adminId, array $adminInfo): array { $today = date('Y-m-d'); $yesterday = date('Y-m-d', strtotime('-1 day')); $dayBeforeYesterday = date('Y-m-d', strtotime('-2 days')); $monthStart = date('Y-m-01'); $previousMonthStart = date('Y-m-01', strtotime('first day of previous month')); $previousMonthLastDay = (int) date('t', strtotime($previousMonthStart)); $comparisonDay = min((int) date('j'), $previousMonthLastDay); $previousMonthComparableEnd = date( 'Y-m-d', strtotime($previousMonthStart . ' +' . max(0, $comparisonDay - 1) . ' days') ); $trendStart = date('Y-m-d', strtotime('-' . (self::TREND_DAYS - 1) . ' days')); $scope = self::buildScopeContext($adminId, $adminInfo); /** @var array|null $visibleAdminIds */ $visibleAdminIds = $scope['_visible_admin_ids']; unset($scope['_visible_admin_ids']); $orderDaily = self::loadPerformanceOrderDaily($previousMonthStart, $today, $visibleAdminIds); $personalOrderDaily = self::loadPerformanceOrderDaily($monthStart, $today, [$adminId]); $monthAmount = self::sumDailyMetric($orderDaily, $monthStart, $today, 'amount'); $previousMonthAmount = self::sumDailyMetric( $orderDaily, $previousMonthStart, $previousMonthComparableEnd, 'amount' ); $yesterdayAmount = self::dailyMetric($orderDaily, $yesterday, 'amount'); $dayBeforeAmount = self::dailyMetric($orderDaily, $dayBeforeYesterday, 'amount'); $personalMonthAmount = self::sumDailyMetric($personalOrderDaily, $monthStart, $today, 'amount'); $todayOverview = ConversionLogic::overview([ 'dimension' => 'dept', 'time_type' => 'today', 'include_members' => 0, 'include_filters' => 0, 'exclude_cancelled_appointments' => 1, 'page_no' => 1, 'page_size' => 100, ], $adminId, $adminInfo); $todaySummary = is_array($todayOverview['summary'] ?? null) ? $todayOverview['summary'] : []; $yesterdayOverview = ConversionLogic::overview([ 'dimension' => 'dept', 'time_type' => 'yesterday', 'include_members' => 0, 'include_filters' => 0, 'exclude_cancelled_appointments' => 1, 'page_no' => 1, 'page_size' => 100, ], $adminId, $adminInfo); $yesterdaySummary = is_array($yesterdayOverview['summary'] ?? null) ? $yesterdayOverview['summary'] : []; // 业绩指标必须直接复用业绩页的权威聚合,不能使用 ConversionLogic 的“双审完成单”。 $todayPerformanceOverview = YejiStatsLogic::overview([ 'start_date' => $today, 'end_date' => $today, ], $adminId, $adminInfo); $appointmentRanking = self::buildAppointmentRanking($adminId, $adminInfo, $scope); $performanceRanking = self::buildPerformanceRanking( is_array($todayPerformanceOverview['rows'] ?? null) ? $todayPerformanceOverview['rows'] : [] ); $trendContext = YejiStatsLogic::resolveSharedYejiFilterContext([ 'start_date' => $trendStart, 'end_date' => $today, ], $adminId, $adminInfo); $trend = self::buildTrend($trendStart, $today, $visibleAdminIds, $orderDaily, $trendContext); $todayTrendIndex = max(0, count($trend['dates'] ?? []) - 1); $yesterdayTrendIndex = max(0, $todayTrendIndex - 1); $todayAddFansCount = (int) ($trend['leads'][$todayTrendIndex] ?? 0); $yesterdayAddFansCount = (int) ($trend['leads'][$yesterdayTrendIndex] ?? 0); $todayAppointmentCount = (int) ($trend['appointments'][$todayTrendIndex] ?? 0); $yesterdayAppointmentCount = (int) ($trend['appointments'][$yesterdayTrendIndex] ?? 0); $todayInterviewCount = (int) ($todaySummary['interview_count'] ?? 0); $yesterdayInterviewCount = (int) ($yesterdaySummary['interview_count'] ?? 0); $todayOrderCount = (int) self::dailyMetric($orderDaily, $today, 'count'); $yesterdayOrderCount = (int) self::dailyMetric($orderDaily, $yesterday, 'count'); $todayOrderAmount = self::dailyMetric($orderDaily, $today, 'amount'); $yesterdayOrderAmount = self::dailyMetric($orderDaily, $yesterday, 'amount'); $todayPaidAppointmentCount = self::paidAppointmentCountWithCenterRule($todayOverview); $yesterdayPaidAppointmentCount = self::paidAppointmentCountWithCenterRule($yesterdayOverview); $todayPaidAppointmentRate = self::percent($todayPaidAppointmentCount, $todayAddFansCount); $yesterdayPaidAppointmentRate = self::percent($yesterdayPaidAppointmentCount, $yesterdayAddFansCount); // 接诊卡片使用的是有效业务订单,接诊率必须使用同一订单口径,不能继续读取旧的双审完成单。 $todayInterviewReceiveRate = self::percent($todayOrderCount, $todayInterviewCount); $yesterdayInterviewReceiveRate = self::percent($yesterdayOrderCount, $yesterdayInterviewCount); $target = self::buildTargetProgress( $adminId, (int) ($scope['scope_value'] ?? DataScopeService::SCOPE_SELF), date('Y-m'), $monthAmount, $personalMonthAmount ); return [ 'scope' => $scope, 'performance' => [ 'month_amount' => round($monthAmount, 2), 'month_compare_rate' => self::relativeChange($monthAmount, $previousMonthAmount), 'month_compare_label' => '较上月同期', 'today_amount' => round($todayOrderAmount, 2), 'today_compare_rate' => self::relativeChange($todayOrderAmount, $yesterdayOrderAmount), 'today_compare_label' => '较昨日', 'yesterday_amount' => round($yesterdayAmount, 2), 'yesterday_compare_rate' => self::relativeChange($yesterdayAmount, $dayBeforeAmount), 'yesterday_compare_label' => '较前一日', 'personal_month_amount' => round($personalMonthAmount, 2), ], 'today' => [ 'add_fans_count' => $todayAddFansCount, 'appointment_total_count' => $todayAppointmentCount, 'interview_count' => $todayInterviewCount, // 保留原响应字段名以兼容已发布前端,数值含义已统一为“计入业绩的业务订单”。 'completed_order_count' => $todayOrderCount, 'completed_order_amount' => $todayOrderAmount, 'paid_appointment_count' => $todayPaidAppointmentCount, 'paid_appointment_rate' => $todayPaidAppointmentRate, 'interview_receive_rate' => $todayInterviewReceiveRate, 'comparisons' => [ 'add_fans_count' => self::buildComparison($todayAddFansCount, $yesterdayAddFansCount), 'appointment_total_count' => self::buildComparison( $todayAppointmentCount, $yesterdayAppointmentCount ), 'interview_count' => self::buildComparison($todayInterviewCount, $yesterdayInterviewCount), 'completed_order_count' => self::buildComparison($todayOrderCount, $yesterdayOrderCount), 'completed_order_amount' => self::buildComparison($todayOrderAmount, $yesterdayOrderAmount), 'paid_appointment_rate' => self::buildComparison( $todayPaidAppointmentRate, $yesterdayPaidAppointmentRate ), 'interview_receive_rate' => self::buildComparison( $todayInterviewReceiveRate, $yesterdayInterviewReceiveRate ), ], ], 'rankings' => [ 'appointments' => $appointmentRanking, 'performance' => [ 'title' => '今日部门业绩排行', 'scope_label' => (string) ($scope['label'] ?? ''), 'items' => $performanceRanking, ], ], 'trend' => $trend, 'target' => $target, 'meta' => [ 'generated_at' => date('Y-m-d H:i:s'), 'timezone' => date_default_timezone_get(), 'commission_note' => '本人业绩按当前账号创建的业务订单统计,排除已取消、拒收和退款订单。', 'rate_note' => '付费挂号率:一中心员工的有效挂号全部按付费挂号,其他部门按 5 元实付挂号;面诊接诊率:有效业务诊单数 / 已完成面诊数。近 7 天挂号排除已取消记录,诊单按订单创建时间统计并排除履约 4/9/10。', ], ]; } /** * 一中心员工的有效挂号全部视为付费;其它部门仍沿用 5 元实付挂号口径。 * 部门树的父节点已汇总子节点,因此命中“一中心”后不再向下递归,避免重复累计。 * * @param array $overview */ private static function paidAppointmentCountWithCenterRule(array $overview): int { $summary = is_array($overview['summary'] ?? null) ? $overview['summary'] : []; $paidTotal = (int) ($summary['paid_appointment_count'] ?? 0); $rows = is_array($overview['lists'] ?? null) ? $overview['lists'] : []; [$centerAppointments, $centerPaidAppointments] = self::namedDepartmentAppointmentMetrics( $rows, self::PAID_APPOINTMENT_DEPT_NAME ); return max(0, $paidTotal - $centerPaidAppointments + $centerAppointments); } /** * @param array> $rows * @return array{0:int,1:int} */ private static function namedDepartmentAppointmentMetrics(array $rows, string $departmentName): array { $appointmentCount = 0; $paidAppointmentCount = 0; foreach ($rows as $row) { if (trim((string) ($row['name'] ?? '')) === $departmentName) { $appointmentCount += (int) ($row['appointment_total_count'] ?? 0); $paidAppointmentCount += (int) ($row['paid_appointment_count'] ?? 0); continue; } $children = is_array($row['children'] ?? null) ? $row['children'] : []; if ($children === []) { continue; } [$childAppointments, $childPaidAppointments] = self::namedDepartmentAppointmentMetrics( $children, $departmentName ); $appointmentCount += $childAppointments; $paidAppointmentCount += $childPaidAppointments; } return [$appointmentCount, $paidAppointmentCount]; } /** * @return array */ private static function buildScopeContext(int $adminId, array $adminInfo): array { $scopeValue = DataScopeService::getEffectiveScope($adminInfo); $visibleAdminIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo); $roleIds = AdminRole::where('admin_id', $adminId)->column('role_id'); $roleIds = array_values(array_unique(array_filter(array_map('intval', $roleIds), static fn (int $id): bool => $id > 0))); $roleNames = []; if ($roleIds !== []) { $roleNames = SystemRole::whereIn('id', $roleIds) ->whereNull('delete_time') ->order('sort', 'desc') ->column('name'); $roleNames = array_values(array_filter(array_map('strval', $roleNames))); } $deptIds = AdminDept::where('admin_id', $adminId)->column('dept_id'); $deptIds = array_values(array_unique(array_filter(array_map('intval', $deptIds), static fn (int $id): bool => $id > 0))); $deptNames = []; if ($deptIds !== []) { $deptNames = Dept::whereIn('id', $deptIds) ->whereNull('delete_time') ->order('id', 'asc') ->column('name'); $deptNames = array_values(array_filter(array_map('strval', $deptNames))); } $isRoot = (int) ($adminInfo['root'] ?? 0) === 1; $scopeKey = $isRoot ? 'root' : [ DataScopeService::SCOPE_ALL => 'all', DataScopeService::SCOPE_DEPT_AND_CHILD => 'dept_children', DataScopeService::SCOPE_DEPT => 'dept', DataScopeService::SCOPE_SELF => 'self', ][$scopeValue] ?? 'self'; $scopeLabel = $isRoot ? '全部数据' : DataScopeService::scopeLabel($scopeValue); if ($visibleAdminIds === null) { $visibleMemberCount = (int) Db::name('admin')->whereNull('delete_time')->count(); } else { $visibleMemberCount = count($visibleAdminIds); } return [ 'key' => $scopeKey, 'scope_value' => $scopeValue, 'label' => $scopeLabel, 'is_limited' => $visibleAdminIds !== null, 'viewer_name' => (string) ($adminInfo['name'] ?? $adminInfo['account'] ?? ''), 'role_ids' => $roleIds, 'role_names' => $roleNames, 'department_names' => $deptNames, 'visible_member_count' => $visibleMemberCount, '_visible_admin_ids' => $visibleAdminIds, ]; } /** * @param array|null $visibleAdminIds * @return array */ private static function loadPerformanceOrderDaily(string $startDate, string $endDate, ?array $visibleAdminIds): array { if ($visibleAdminIds === []) { return []; } $startTs = (int) strtotime($startDate . ' 00:00:00'); $endTs = (int) strtotime($endDate . ' 23:59:59'); $query = Db::name('tcm_prescription_order') ->alias('po') ->whereNull('po.delete_time') ->where('po.create_time', 'between', [$startTs, $endTs]); YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, 'po'); if ($visibleAdminIds !== null) { $query->whereIn('po.creator_id', $visibleAdminIds); } $rows = $query ->fieldRaw("FROM_UNIXTIME(po.create_time, '%Y-%m-%d') AS date_label, SUM(po.amount) AS amount_sum, COUNT(*) AS order_count") ->group('date_label') ->order('date_label', 'asc') ->select() ->toArray(); $out = []; foreach ($rows as $row) { $date = (string) ($row['date_label'] ?? ''); if ($date === '') { continue; } $out[$date] = [ 'amount' => round((float) ($row['amount_sum'] ?? 0), 2), 'count' => (int) ($row['order_count'] ?? 0), ]; } return $out; } /** * @param array $daily */ private static function sumDailyMetric(array $daily, string $startDate, string $endDate, string $metric): float { $sum = 0.0; foreach ($daily as $date => $values) { if ($date < $startDate || $date > $endDate) { continue; } $sum += (float) ($values[$metric] ?? 0); } return round($sum, 2); } /** * @param array $daily */ private static function dailyMetric(array $daily, string $date, string $metric): float { return round((float) ($daily[$date][$metric] ?? 0), 2); } private static function percent(float $numerator, int $denominator): float { if ($denominator <= 0) { return 0.0; } return round(($numerator / $denominator) * 100, 2); } private static function relativeChange(float $current, float $previous): ?float { if (abs($previous) < 0.00001) { return null; } return round((($current - $previous) / $previous) * 100, 2); } /** * @return array{direction: string, rate: float|null, previous: float} */ private static function buildComparison(float $current, float $previous): array { $difference = $current - $previous; $direction = abs($difference) < 0.00001 ? 'flat' : ($difference > 0 ? 'up' : 'down'); return [ 'direction' => $direction, 'rate' => self::relativeChange($current, $previous), 'previous' => round($previous, 2), ]; } /** * @param array $scope * @return array */ private static function buildAppointmentRanking(int $adminId, array $adminInfo, array $scope): array { $roleIds = array_map('intval', $scope['role_ids'] ?? []); $isDoctorSelf = ($scope['key'] ?? '') === 'self' && in_array(1, $roleIds, true) && !in_array(2, $roleIds, true); if ($isDoctorSelf) { $doctorStats = DoctorDailyStatsLogic::overview([ 'start_date' => date('Y-m-d'), 'end_date' => date('Y-m-d'), ], $adminId, $adminInfo); $items = []; foreach (array_slice($doctorStats['rows'] ?? [], 0, 5) as $row) { $items[] = [ 'id' => (int) ($row['admin_id'] ?? 0), 'name' => (string) ($row['doctor_name'] ?? ''), // DoctorDailyStats 的 total 含已取消;驾驶舱实时排行只统计有效挂号。 'count' => max( 0, (int) ($row['appointment_total'] ?? 0) - (int) ($row['appointment_cancelled'] ?? 0) ), 'amount' => round((float) ($row['deal_amount'] ?? 0), 2), ]; } return [ 'title' => '实时挂号排行', 'kind' => 'doctor', 'scope_label' => (string) ($scope['label'] ?? ''), 'items' => $items, ]; } $rankingVisibleAdminIds = null; $rankingScopeLabel = (string) ($scope['label'] ?? ''); if ( (int) ($scope['scope_value'] ?? DataScopeService::SCOPE_SELF) === DataScopeService::SCOPE_SELF && in_array(2, $roleIds, true) ) { $departmentAssistantIds = self::directDepartmentAssistantIds($adminId); if ($departmentAssistantIds !== []) { $rankingVisibleAdminIds = $departmentAssistantIds; $rankingScopeLabel = '本人所属部门'; } } $assistantStats = ConversionLogic::overview([ 'dimension' => 'assistant', 'time_type' => 'today', 'include_filters' => 0, 'exclude_cancelled_appointments' => 1, 'page_no' => 1, 'page_size' => $rankingVisibleAdminIds !== null ? max(1, count($rankingVisibleAdminIds)) : 100, ], $adminId, $adminInfo, $rankingVisibleAdminIds); $rows = is_array($assistantStats['lists'] ?? null) ? $assistantStats['lists'] : []; usort($rows, static function (array $a, array $b): int { $byAppointment = (int) ($b['appointment_total_count'] ?? 0) <=> (int) ($a['appointment_total_count'] ?? 0); if ($byAppointment !== 0) { return $byAppointment; } $byAmount = (float) ($b['completed_order_amount'] ?? 0) <=> (float) ($a['completed_order_amount'] ?? 0); if ($byAmount !== 0) { return $byAmount; } return (int) ($a['id'] ?? 0) <=> (int) ($b['id'] ?? 0); }); $items = []; foreach (array_slice($rows, 0, 5) as $row) { $items[] = [ 'id' => (int) ($row['id'] ?? 0), 'name' => (string) ($row['name'] ?? ''), 'count' => (int) ($row['appointment_total_count'] ?? 0), 'amount' => round((float) ($row['completed_order_amount'] ?? 0), 2), ]; } return [ 'title' => '实时挂号排行', 'kind' => 'assistant', 'scope_label' => $rankingScopeLabel, 'items' => $items, ]; } /** * SELF 医助排行的卡片级例外:只扩展到当前账号所有有效直接部门内的有效医助。 * 不展开子部门,也不改变驾驶舱其它指标的数据范围。 * * @return int[] */ private static function directDepartmentAssistantIds(int $adminId): array { if ($adminId <= 0) { return []; } $activeAdmin = Db::name('admin') ->where('id', $adminId) ->whereNull('delete_time') ->value('id'); if ((int) $activeAdmin <= 0) { return []; } $deptIds = Db::name('admin_dept') ->alias('ad') ->join('dept d', 'd.id = ad.dept_id AND d.delete_time IS NULL', 'INNER') ->where('ad.admin_id', $adminId) ->column('ad.dept_id'); $deptIds = array_values(array_unique(array_filter( array_map('intval', $deptIds), static fn (int $id): bool => $id > 0 ))); if ($deptIds === []) { return []; } $assistantIds = Db::name('admin_dept') ->alias('ad') ->join('dept d', 'd.id = ad.dept_id AND d.delete_time IS NULL', 'INNER') ->join('admin a', 'a.id = ad.admin_id AND a.delete_time IS NULL', 'INNER') ->join('admin_role ar', 'ar.admin_id = a.id AND ar.role_id = 2', 'INNER') ->join('system_role sr', 'sr.id = ar.role_id AND sr.delete_time IS NULL', 'INNER') ->whereIn('ad.dept_id', $deptIds) ->distinct(true) ->column('a.id'); return array_values(array_unique(array_filter( array_map('intval', $assistantIds), static fn (int $id): bool => $id > 0 ))); } /** * @param array> $rows * @return array> */ private static function buildPerformanceRanking(array $rows): array { usort($rows, static function (array $a, array $b): int { $byAmount = (float) ($b['performance_amount'] ?? 0) <=> (float) ($a['performance_amount'] ?? 0); if ($byAmount !== 0) { return $byAmount; } $byCount = (int) ($b['deal_order_count'] ?? 0) <=> (int) ($a['deal_order_count'] ?? 0); if ($byCount !== 0) { return $byCount; } return (int) ($a['dept_id'] ?? 0) <=> (int) ($b['dept_id'] ?? 0); }); $items = []; foreach (array_slice($rows, 0, 5) as $row) { $items[] = [ 'id' => (int) ($row['dept_id'] ?? 0), 'name' => (string) ($row['dept_name'] ?? '未归属中心'), 'amount' => round((float) ($row['performance_amount'] ?? 0), 2), 'count' => (int) ($row['deal_order_count'] ?? 0), ]; } return $items; } /** * @param array|null $visibleAdminIds * @param array $orderDaily * @param array $trendContext * @return array */ private static function buildTrend( string $startDate, string $endDate, ?array $visibleAdminIds, array $orderDaily, array $trendContext ): array { $adminToPrimary = is_array($trendContext['adminToPrimary'] ?? null) ? $trendContext['adminToPrimary'] : []; $tableRowDeptIds = is_array($trendContext['tableRowDeptIds'] ?? null) ? array_values(array_map('intval', $trendContext['tableRowDeptIds'])) : []; $leadDaily = self::loadLeadDaily($startDate, $endDate, $adminToPrimary, $tableRowDeptIds); $appointmentDaily = self::loadAppointmentDaily( $startDate, $endDate, $visibleAdminIds, $adminToPrimary, $tableRowDeptIds ); $dates = []; $appointments = []; $leads = []; $orders = []; $cursor = strtotime($startDate); $end = strtotime($endDate); while ($cursor <= $end) { $date = date('Y-m-d', $cursor); $dates[] = date('m-d', $cursor); $appointments[] = (int) ($appointmentDaily[$date] ?? 0); $leads[] = (int) ($leadDaily[$date] ?? 0); $orders[] = (int) ($orderDaily[$date]['count'] ?? 0); $cursor = strtotime('+1 day', $cursor); } return [ 'date_range' => [$startDate, $endDate], 'dates' => $dates, 'appointments' => $appointments, 'leads' => $leads, 'orders' => $orders, ]; } /** * 与 YejiStatsLogic 的“进线”一致:只有能映射到当前业绩中心展示行的管理员事件才计入。 * * @param array $adminToPrimary * @param int[] $tableRowDeptIds * @return array */ private static function loadLeadDaily( string $startDate, string $endDate, array $adminToPrimary, array $tableRowDeptIds ): array { $rowFlip = array_flip($tableRowDeptIds); $mappedAdminIds = []; foreach ($adminToPrimary as $adminId => $deptId) { $adminId = (int) $adminId; $deptId = (int) $deptId; if ($adminId > 0 && isset($rowFlip[$deptId])) { $mappedAdminIds[] = $adminId; } } $mappedAdminIds = array_values(array_unique($mappedAdminIds)); if ($mappedAdminIds === []) { return []; } $query = Db::name('qywx_external_contact_event') ->alias('e') ->join('admin a', 'a.work_wechat_userid = e.user_id AND a.delete_time IS NULL', 'INNER') ->where('e.change_type', 'add_external_contact') ->whereIn('a.id', $mappedAdminIds) ->where('e.event_time', 'between', [ strtotime($startDate . ' 00:00:00'), strtotime($endDate . ' 23:59:59'), ]); $rows = $query ->fieldRaw("FROM_UNIXTIME(e.event_time, '%Y-%m-%d') AS date_label, COUNT(*) AS item_count") ->group('date_label') ->select() ->toArray(); $out = []; foreach ($rows as $row) { $date = (string) ($row['date_label'] ?? ''); if ($date !== '') { $out[$date] = (int) ($row['item_count'] ?? 0); } } return $out; } /** * @param array|null $visibleAdminIds * 与 YejiStatsLogic 的“预约诊单”一致:appointment_date,状态 1/3/4, * 归属优先挂号医助、再诊单医助,缺失时回退医生;受限账号只保留当前业绩中心展示行。 * * @param array|null $visibleAdminIds * @param array $adminToPrimary * @param int[] $tableRowDeptIds * @return array */ private static function loadAppointmentDaily( string $startDate, string $endDate, ?array $visibleAdminIds, array $adminToPrimary, array $tableRowDeptIds ): array { if ($visibleAdminIds === []) { return []; } $effectiveAssistantSql = 'COALESCE(NULLIF(a.assistant_id, 0), NULLIF(u.assistant_id, 0))'; $query = Db::name('doctor_appointment') ->alias('a') ->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id') ->where('a.appointment_date', 'between', [$startDate, $endDate]) ->whereIn('a.status', [1, 3, 4]) ->whereRaw('(u.id IS NULL OR u.delete_time IS NULL)'); $rows = $query ->field([ 'a.appointment_date AS date_label', Db::raw("({$effectiveAssistantSql}) AS effective_assistant_id"), 'a.doctor_id', Db::raw('COUNT(*) AS appointment_count'), ]) ->group(['a.appointment_date', $effectiveAssistantSql, 'a.doctor_id']) ->select() ->toArray(); $visibleFlip = $visibleAdminIds !== null ? array_flip($visibleAdminIds) : null; $rowFlip = array_flip($tableRowDeptIds); $out = []; foreach ($rows as $row) { $date = (string) ($row['date_label'] ?? ''); if ($date === '') { continue; } $effectiveAssistantId = (int) ($row['effective_assistant_id'] ?? 0); $doctorId = (int) ($row['doctor_id'] ?? 0); if ($visibleFlip !== null) { if ($effectiveAssistantId > 0) { if (!isset($visibleFlip[$effectiveAssistantId])) { continue; } } elseif ($doctorId <= 0 || !isset($visibleFlip[$doctorId])) { continue; } } $deptId = $effectiveAssistantId > 0 ? (int) ($adminToPrimary[$effectiveAssistantId] ?? 0) : 0; if ($deptId <= 0 && $doctorId > 0) { $deptId = (int) ($adminToPrimary[$doctorId] ?? 0); } if ($visibleFlip !== null && !isset($rowFlip[$deptId])) { continue; } $out[$date] = ($out[$date] ?? 0) + (int) ($row['appointment_count'] ?? 0); } return $out; } /** * @return array */ private static function buildTargetProgress( int $adminId, int $scopeValue, string $yearMonth, float $completedAmount, float $personalAmount ): array { $deptIds = self::targetDeptIds($adminId, $scopeValue); $query = Db::name('dept_performance_target')->where('year_month', $yearMonth); if ($deptIds !== null) { if ($deptIds === []) { return self::emptyTarget($yearMonth, $completedAmount, $personalAmount); } $query->whereIn('dept_id', $deptIds); } $rows = $query->field('dept_id, dept_name, target_amount')->select()->toArray(); $targetAmount = 0.0; foreach ($rows as $row) { $targetAmount += (float) ($row['target_amount'] ?? 0); } $targetAmount = round($targetAmount, 2); return [ 'year_month' => $yearMonth, 'target_amount' => $targetAmount, 'completed_amount' => round($completedAmount, 2), 'completion_rate' => $targetAmount > 0 ? round($completedAmount / $targetAmount * 100, 2) : null, 'personal_amount' => round($personalAmount, 2), 'personal_contribution_rate' => $completedAmount > 0 ? round($personalAmount / $completedAmount * 100, 2) : null, 'department_count' => count($rows), ]; } /** * @return array|null null 表示全部部门。 */ private static function targetDeptIds(int $adminId, int $scopeValue): ?array { if ($scopeValue === DataScopeService::SCOPE_ALL) { return null; } $ownDeptIds = AdminDept::where('admin_id', $adminId)->column('dept_id'); $ownDeptIds = array_values(array_unique(array_filter(array_map('intval', $ownDeptIds), static fn (int $id): bool => $id > 0))); if ($ownDeptIds === [] || $scopeValue !== DataScopeService::SCOPE_DEPT_AND_CHILD) { return $ownDeptIds; } $out = []; foreach ($ownDeptIds as $deptId) { foreach (DeptLogic::getSelfAndDescendantIds($deptId) as $id) { $id = (int) $id; if ($id > 0) { $out[$id] = true; } } } return array_keys($out); } /** * @return array */ private static function emptyTarget(string $yearMonth, float $completedAmount, float $personalAmount): array { return [ 'year_month' => $yearMonth, 'target_amount' => 0.0, 'completed_amount' => round($completedAmount, 2), 'completion_rate' => null, 'personal_amount' => round($personalAmount, 2), 'personal_contribution_rate' => $completedAmount > 0 ? round($personalAmount / $completedAmount * 100, 2) : null, 'department_count' => 0, ]; } }