*/ public static function overview(array $params, int $adminId, array $adminInfo): array { $range = self::resolveRange((string) ($params['time_type'] ?? 'today')); $baseVisibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo); $allowedDeptSet = DataScopeService::getAllowedDeptIdSet($adminId, $adminInfo); $scopeValue = DataScopeService::getEffectiveScope($adminInfo); $selectedDeptId = max(0, (int) ($params['dept_id'] ?? 0)); $selectedAssistantId = max(0, (int) ($params['assistant_id'] ?? 0)); [$selectedDeptIds, $deptSelectionValid] = self::resolveSelectedDeptIds( $selectedDeptId, $allowedDeptSet ); $assistants = $deptSelectionValid ? self::assistantOptions($baseVisibleIds, $selectedDeptIds, $selectedDeptId) : []; $assistantIds = self::normalizeIds(array_column($assistants, 'id')); if ($selectedAssistantId > 0) { $assistantIds = in_array($selectedAssistantId, $assistantIds, true) ? [$selectedAssistantId] : []; } $departmentTree = DeptLogic::getAllDataScoped($adminId, $adminInfo); $departmentIndex = []; self::flattenDepartmentTree($departmentTree, $departmentIndex, 0); $assignment = self::buildAssistantDepartmentMap( $assistantIds, $departmentIndex, $selectedDeptIds, $selectedDeptId ); $appointmentDaily = self::loadAppointmentDaily( min($range['compare_start'], $range['start']), $range['day_after_tomorrow'], $assistantIds ); $registrationDaily = self::loadRegistrationDaily( min($range['compare_start'], $range['start']), $range['end'], $assistantIds ); $orderDaily = self::loadOrderDaily( min($range['compare_start'], $range['start']), $range['end'], $assistantIds ); $members = self::buildMemberRows( $assistants, $assistantIds, $assignment, $appointmentDaily, $registrationDaily, $orderDaily, $range ); $groups = self::buildDepartmentGroups($members, $departmentIndex); $summary = self::buildSummary($members, $range); $targetDeptIds = self::resolveTargetDeptIds( $adminId, $scopeValue, $selectedAssistantId, $selectedDeptIds, $selectedDeptId ); $target = self::buildTarget((int) date('Y'), $assistantIds, $targetDeptIds); $selectedDeptName = $selectedDeptId > 0 ? (string) ($departmentIndex[$selectedDeptId]['name'] ?? '') : ''; $selectedAssistantName = ''; if ($selectedAssistantId > 0) { foreach ($assistants as $assistant) { if ((int) ($assistant['id'] ?? 0) === $selectedAssistantId) { $selectedAssistantName = (string) ($assistant['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' => DataScopeService::scopeLabel($scopeValue), 'selected_dept_name' => $selectedDeptName, 'selected_assistant_name' => $selectedAssistantName, 'member_count' => count($assistantIds), 'registration_rule' => '支付时间在统计区间,状态为已支付且实收金额低于 10 元(大于 0 元),每笔支付订单计 1 个挂号', 'appointment_rule' => '预约日期在统计区间,状态为已预约、已完成或已过号,排除已取消', 'performance_rule' => '按订单创建时间和创建人统计,排除已取消、拒收和退款', ], 'filters' => [ 'departments' => $departmentTree, 'assistants' => $assistants, ], 'summary' => $summary, 'employee_rows' => $groups, 'rankings' => [ 'performance' => self::rankMembers($members, 'order_amount', 10), 'registrations' => self::rankMembers($members, 'registration_count', 10), 'appointments' => self::rankMembers($members, 'appointment_count', 10), ], 'departments' => self::departmentSummaryRows($groups), 'target' => $target, ]; } /** @return array */ private static function resolveRange(string $type): array { $today = date('Y-m-d'); $tomorrow = date('Y-m-d', strtotime('+1 day')); $dayAfterTomorrow = date('Y-m-d', strtotime('+2 days')); if ($type === 'week') { $start = date('Y-m-d', strtotime('monday this week')); return [ 'type' => 'week', 'label' => '本周', 'start' => $start, 'end' => $today, 'compare_start' => date('Y-m-d', strtotime($start . ' -7 days')), 'compare_end' => date('Y-m-d', strtotime($today . ' -7 days')), 'tomorrow' => $tomorrow, 'day_after_tomorrow' => $dayAfterTomorrow, ]; } if ($type === 'month') { $start = date('Y-m-01'); $previousStart = date('Y-m-01', strtotime('first day of previous month')); $previousLastDay = (int) date('t', strtotime($previousStart)); $day = min((int) date('j'), $previousLastDay); return [ 'type' => 'month', 'label' => '本月', 'start' => $start, 'end' => $today, 'compare_start' => $previousStart, 'compare_end' => date('Y-m-d', strtotime($previousStart . ' +' . max(0, $day - 1) . ' days')), 'tomorrow' => $tomorrow, 'day_after_tomorrow' => $dayAfterTomorrow, ]; } return [ 'type' => 'today', 'label' => '今日', 'start' => $today, 'end' => $today, 'compare_start' => date('Y-m-d', strtotime('-1 day')), 'compare_end' => date('Y-m-d', strtotime('-1 day')), 'tomorrow' => $tomorrow, 'day_after_tomorrow' => $dayAfterTomorrow, ]; } /** @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 !== []]; } /** @param int[]|null $visibleIds @param int[] $selectedDeptIds @return array */ private static function assistantOptions(?array $visibleIds, array $selectedDeptIds, int $selectedDeptId): 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); } if ($selectedDeptId > 0) { if ($selectedDeptIds === []) { return []; } $query->join('admin_dept ad', 'ad.admin_id = a.id') ->whereIn('ad.dept_id', $selectedDeptIds); } return $query->field('a.id, a.name')->distinct(true)->order('a.name', 'asc')->select()->toArray(); } /** @param array> $nodes @param array> $index */ private static function flattenDepartmentTree(array $nodes, array &$index, int $depth): void { foreach ($nodes as $node) { $id = (int) ($node['id'] ?? 0); if ($id <= 0) { continue; } $index[$id] = [ 'id' => $id, 'pid' => (int) ($node['pid'] ?? 0), 'name' => (string) ($node['name'] ?? '未命名部门'), 'sort' => (int) ($node['sort'] ?? 0), 'depth' => $depth, ]; self::flattenDepartmentTree( is_array($node['children'] ?? null) ? $node['children'] : [], $index, $depth + 1 ); } } /** @param int[] $assistantIds @param array> $deptIndex @param int[] $selectedDeptIds @return array */ private static function buildAssistantDepartmentMap( array $assistantIds, array $deptIndex, array $selectedDeptIds, int $selectedDeptId ): array { if ($assistantIds === []) { return []; } $allowed = $selectedDeptId > 0 ? array_fill_keys($selectedDeptIds, true) : null; $rows = AdminDept::whereIn('admin_id', $assistantIds) ->field('admin_id, dept_id') ->select() ->toArray(); $candidates = []; foreach ($rows as $row) { $aid = (int) ($row['admin_id'] ?? 0); $deptId = (int) ($row['dept_id'] ?? 0); if (!isset($deptIndex[$deptId]) || ($allowed !== null && !isset($allowed[$deptId]))) { continue; } $candidates[$aid][] = $deptId; } $out = []; foreach ($assistantIds as $aid) { $ids = $candidates[$aid] ?? []; usort($ids, static function (int $left, int $right) use ($deptIndex): int { $depthCompare = (int) ($deptIndex[$right]['depth'] ?? 0) <=> (int) ($deptIndex[$left]['depth'] ?? 0); if ($depthCompare !== 0) { return $depthCompare; } return (int) ($deptIndex[$right]['sort'] ?? 0) <=> (int) ($deptIndex[$left]['sort'] ?? 0); }); $out[$aid] = (int) ($ids[0] ?? 0); } return $out; } /** @param int[] $assistantIds @return array> */ private static function loadAppointmentDaily(string $startDate, string $endDate, array $assistantIds): array { if ($assistantIds === []) { return []; } $effective = '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)') ->whereRaw("({$effective}) IN (" . implode(',', $assistantIds) . ')'); $rows = $query ->field([ 'a.appointment_date AS date_label', Db::raw("({$effective}) AS assistant_id"), Db::raw('COUNT(*) AS item_count'), ]) ->group(['a.appointment_date', $effective]) ->select() ->toArray(); $out = []; foreach ($rows as $row) { $aid = (int) ($row['assistant_id'] ?? 0); $date = (string) ($row['date_label'] ?? ''); if ($aid > 0 && $date !== '') { $out[$aid][$date] = ['count' => (int) ($row['item_count'] ?? 0)]; } } return $out; } /** @param int[] $assistantIds @return array> */ private static function loadRegistrationDaily(string $startDate, string $endDate, array $assistantIds): array { if ($assistantIds === []) { return []; } $rows = Db::name('order')->alias('o') ->whereNull('o.delete_time') ->where('o.status', 2) ->where('o.amount', '>', 0) ->where('o.amount', '<', 10) ->whereBetweenTime( 'o.payment_time', $startDate . ' 00:00:00', $endDate . ' 23:59:59' ) ->whereIn('o.creator_id', $assistantIds) ->fieldRaw('o.creator_id AS assistant_id, DATE(o.payment_time) AS date_label, COUNT(*) AS item_count') ->group(['o.creator_id', 'date_label']) ->select() ->toArray(); $out = []; foreach ($rows as $row) { $aid = (int) ($row['assistant_id'] ?? 0); $date = (string) ($row['date_label'] ?? ''); if ($aid > 0 && $date !== '') { $out[$aid][$date] = ['count' => (int) ($row['item_count'] ?? 0)]; } } return $out; } /** @param int[] $assistantIds @return array> */ private static function loadOrderDaily(string $startDate, string $endDate, array $assistantIds): array { if ($assistantIds === []) { return []; } $query = Db::name('tcm_prescription_order')->alias('po') ->whereNull('po.delete_time') ->where('po.create_time', 'between', [ strtotime($startDate . ' 00:00:00'), strtotime($endDate . ' 23:59:59'), ]) ->whereIn('po.creator_id', $assistantIds); YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, 'po'); $rows = $query ->fieldRaw("po.creator_id AS assistant_id, FROM_UNIXTIME(po.create_time, '%Y-%m-%d') AS date_label, COUNT(*) AS item_count, SUM(po.amount) AS amount_sum") ->group(['po.creator_id', 'date_label']) ->select() ->toArray(); $out = []; foreach ($rows as $row) { $aid = (int) ($row['assistant_id'] ?? 0); $date = (string) ($row['date_label'] ?? ''); if ($aid > 0 && $date !== '') { $out[$aid][$date] = [ 'count' => (int) ($row['item_count'] ?? 0), 'amount' => round((float) ($row['amount_sum'] ?? 0), 2), ]; } } return $out; } /** @return array> */ private static function buildMemberRows( array $assistants, array $assistantIds, array $assignment, array $appointmentDaily, array $registrationDaily, array $orderDaily, array $range ): array { $assistantIndex = []; foreach ($assistants as $assistant) { $assistantIndex[(int) ($assistant['id'] ?? 0)] = (string) ($assistant['name'] ?? '未命名员工'); } $rows = []; foreach ($assistantIds as $aid) { $appointmentCount = self::sumDaily($appointmentDaily[$aid] ?? [], $range['start'], $range['end'], 'count'); $compareAppointmentCount = self::sumDaily($appointmentDaily[$aid] ?? [], $range['compare_start'], $range['compare_end'], 'count'); $registrationCount = self::sumDaily($registrationDaily[$aid] ?? [], $range['start'], $range['end'], 'count'); $compareRegistrationCount = self::sumDaily($registrationDaily[$aid] ?? [], $range['compare_start'], $range['compare_end'], 'count'); $orderCount = self::sumDaily($orderDaily[$aid] ?? [], $range['start'], $range['end'], 'count'); $orderAmount = self::sumDaily($orderDaily[$aid] ?? [], $range['start'], $range['end'], 'amount'); $rows[] = [ 'id' => 'admin-' . $aid, 'admin_id' => $aid, 'dept_id' => (int) ($assignment[$aid] ?? 0), 'name' => (string) ($assistantIndex[$aid] ?? '未命名员工'), 'row_type' => 'employee', 'registration_count' => (int) $registrationCount, 'compare_registration_count' => (int) $compareRegistrationCount, 'registration_compare_rate' => self::relativeChange($registrationCount, $compareRegistrationCount), 'appointment_count' => (int) $appointmentCount, 'compare_appointment_count' => (int) $compareAppointmentCount, 'appointment_compare_rate' => self::relativeChange($appointmentCount, $compareAppointmentCount), 'tomorrow_count' => (int) ($appointmentDaily[$aid][$range['tomorrow']]['count'] ?? 0), 'day_after_count' => (int) ($appointmentDaily[$aid][$range['day_after_tomorrow']]['count'] ?? 0), 'order_count' => (int) $orderCount, 'order_amount' => round((float) $orderAmount, 2), 'status' => 'normal', ]; } usort($rows, static fn (array $a, array $b): int => ($b['registration_count'] <=> $a['registration_count']) ?: ($b['appointment_count'] <=> $a['appointment_count']) ?: ($b['order_amount'] <=> $a['order_amount'])); return $rows; } /** @param array> $members @param array> $deptIndex @return array> */ private static function buildDepartmentGroups(array $members, array $deptIndex): array { $groups = []; foreach ($members as $member) { $deptId = (int) ($member['dept_id'] ?? 0); $key = $deptId > 0 ? $deptId : -2; if (!isset($groups[$key])) { $groups[$key] = [ 'id' => 'dept-' . $key, 'dept_id' => $key, 'name' => $key > 0 ? (string) ($deptIndex[$key]['name'] ?? '未命名部门') : '未分配部门', 'row_type' => 'department', 'member_count' => 0, 'registration_count' => 0, 'compare_registration_count' => 0, 'appointment_count' => 0, 'compare_appointment_count' => 0, 'tomorrow_count' => 0, 'day_after_count' => 0, 'order_count' => 0, 'order_amount' => 0.0, 'children' => [], '_sort' => $key > 0 ? (int) ($deptIndex[$key]['sort'] ?? 0) : -1, ]; } $groups[$key]['children'][] = $member; $groups[$key]['member_count']++; foreach (['registration_count', 'compare_registration_count', 'appointment_count', 'compare_appointment_count', 'tomorrow_count', 'day_after_count', 'order_count'] as $field) { $groups[$key][$field] += (int) ($member[$field] ?? 0); } $groups[$key]['order_amount'] += (float) ($member['order_amount'] ?? 0); } foreach ($groups as &$group) { $group['order_amount'] = round((float) $group['order_amount'], 2); $group['registration_compare_rate'] = self::relativeChange( (float) $group['registration_count'], (float) $group['compare_registration_count'] ); $group['appointment_compare_rate'] = self::relativeChange( (float) $group['appointment_count'], (float) $group['compare_appointment_count'] ); $group['status'] = 'normal'; } unset($group); $out = array_values($groups); usort($out, static fn (array $a, array $b): int => ($b['_sort'] <=> $a['_sort']) ?: strcmp((string) $a['name'], (string) $b['name'])); foreach ($out as &$row) { unset($row['_sort']); } unset($row); return $out; } /** @param array> $members @return array */ private static function buildSummary(array $members, array $range): array { $registrationCount = 0; $compareRegistrationCount = 0; $appointmentCount = 0; $compareAppointmentCount = 0; $orderCount = 0; $orderAmount = 0.0; foreach ($members as $member) { $registrationCount += (int) ($member['registration_count'] ?? 0); $compareRegistrationCount += (int) ($member['compare_registration_count'] ?? 0); $appointmentCount += (int) ($member['appointment_count'] ?? 0); $compareAppointmentCount += (int) ($member['compare_appointment_count'] ?? 0); $orderCount += (int) ($member['order_count'] ?? 0); $orderAmount += (float) ($member['order_amount'] ?? 0); } return [ 'registration_count' => $registrationCount, 'registration_compare_count' => $compareRegistrationCount, 'registration_compare_rate' => self::relativeChange($registrationCount, $compareRegistrationCount), 'appointment_count' => $appointmentCount, 'appointment_compare_count' => $compareAppointmentCount, 'appointment_compare_rate' => self::relativeChange($appointmentCount, $compareAppointmentCount), 'order_count' => $orderCount, 'order_amount' => round($orderAmount, 2), 'range_label' => $range['label'], ]; } /** @param array> $members @return array> */ private static function rankMembers(array $members, string $field, int $limit): array { $rows = $members; usort($rows, static fn (array $a, array $b): int => (($b[$field] ?? 0) <=> ($a[$field] ?? 0)) ?: strcmp((string) $a['name'], (string) $b['name'])); $out = []; foreach (array_slice($rows, 0, $limit) as $row) { $countField = match ($field) { 'order_amount' => 'order_count', 'registration_count' => 'registration_count', default => 'appointment_count', }; $out[] = [ 'admin_id' => (int) ($row['admin_id'] ?? 0), 'name' => (string) ($row['name'] ?? ''), 'value' => $field === 'order_amount' ? round((float) ($row[$field] ?? 0), 2) : (int) ($row[$field] ?? 0), 'count' => (int) ($row[$countField] ?? 0), ]; } return $out; } /** @param array> $groups @return array> */ private static function departmentSummaryRows(array $groups): array { $rows = []; foreach ($groups as $group) { $copy = $group; unset($copy['children']); $rows[] = $copy; } return $rows; } /** @param int[] $selectedDeptIds @return int[]|null */ private static function resolveTargetDeptIds( int $adminId, int $scopeValue, int $selectedAssistantId, array $selectedDeptIds, int $selectedDeptId ): ?array { if ($scopeValue === DataScopeService::SCOPE_SELF || $selectedAssistantId > 0) { return []; } $scopeDeptIds = null; if ($scopeValue !== DataScopeService::SCOPE_ALL) { $ownDeptIds = self::normalizeIds(AdminDept::where('admin_id', $adminId)->column('dept_id')); if ($scopeValue === DataScopeService::SCOPE_DEPT) { $scopeDeptIds = $ownDeptIds; } else { $set = []; foreach ($ownDeptIds as $deptId) { foreach (DeptLogic::getSelfAndDescendantIds($deptId) as $id) { $id = (int) $id; if ($id > 0) { $set[$id] = true; } } } $scopeDeptIds = array_map('intval', array_keys($set)); } } if ($selectedDeptId <= 0) { return $scopeDeptIds; } if ($scopeDeptIds === null) { return $selectedDeptIds; } return array_values(array_intersect($scopeDeptIds, $selectedDeptIds)); } /** @param int[] $assistantIds @param int[]|null $targetDeptIds @return array */ private static function buildTarget(int $year, array $assistantIds, ?array $targetDeptIds): array { $targetRows = []; if ($targetDeptIds !== []) { $query = Db::name('dept_performance_target')->whereLike('year_month', $year . '-%'); if ($targetDeptIds !== null) { $query->whereIn('dept_id', $targetDeptIds); } $targetRows = $query ->fieldRaw('`year_month`, SUM(`target_amount`) AS target_amount, COUNT(DISTINCT `dept_id`) AS dept_count') ->group('`year_month`') ->select() ->toArray(); } $actualRows = []; if ($assistantIds !== []) { $query = Db::name('tcm_prescription_order')->alias('po') ->whereNull('po.delete_time') ->whereIn('po.creator_id', $assistantIds) ->where('po.create_time', 'between', [ strtotime($year . '-01-01 00:00:00'), strtotime($year . '-12-31 23:59:59'), ]); YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, 'po'); $actualRows = $query ->fieldRaw("DATE_FORMAT(FROM_UNIXTIME(po.create_time), '%m') AS month_no, SUM(po.amount) AS actual_amount") ->group('month_no') ->select() ->toArray(); } $monthlyTarget = array_fill(1, 12, 0.0); $monthlyActual = array_fill(1, 12, 0.0); $deptCount = 0; foreach ($targetRows as $row) { $month = (int) substr((string) ($row['year_month'] ?? ''), 5, 2); if ($month >= 1 && $month <= 12) { $monthlyTarget[$month] = round((float) ($row['target_amount'] ?? 0), 2); $deptCount = max($deptCount, (int) ($row['dept_count'] ?? 0)); } } foreach ($actualRows as $row) { $month = (int) ($row['month_no'] ?? 0); if ($month >= 1 && $month <= 12) { $monthlyActual[$month] = round((float) ($row['actual_amount'] ?? 0), 2); } } $targetCumulative = []; $actualCumulative = []; $targetTotal = 0.0; $actualTotal = 0.0; for ($month = 1; $month <= 12; $month++) { $targetTotal = round($targetTotal + $monthlyTarget[$month], 2); $actualTotal = round($actualTotal + $monthlyActual[$month], 2); $targetCumulative[] = $targetTotal; $actualCumulative[] = $actualTotal; } return [ 'year' => $year, 'target_amount' => $targetTotal, 'actual_amount' => $actualTotal, 'completion_rate' => $targetTotal > 0 ? round($actualTotal / $targetTotal * 100, 2) : null, 'department_count' => $deptCount, 'scope_note' => $targetDeptIds === [] ? '当前为本人或单个员工范围,未设置个人目标' : '按当前可见部门汇总', 'months' => array_map(static fn (int $month): string => str_pad((string) $month, 2, '0', STR_PAD_LEFT) . '月', range(1, 12)), 'target_cumulative' => $targetCumulative, 'actual_cumulative' => $actualCumulative, ]; } /** @param array> $daily */ private static function sumDaily(array $daily, string $start, string $end, string $field): float { $sum = 0.0; foreach ($daily as $date => $values) { if ($date >= $start && $date <= $end) { $sum += (float) ($values[$field] ?? 0); } } return $sum; } private static function relativeChange(float $current, float $previous): ?float { if (abs($previous) < 0.00001) { return null; } return round(($current - $previous) / $previous * 100, 2); } /** @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))); } }