loadMissing('tracks'); $apps = Application::query() ->where('competition_id', $competition->id) ->where('status', 'submitted') ->get(); $eligibleApps = $apps->filter(fn (Application $a) => $a->isReviewEligible())->values(); $appIds = $apps->pluck('id')->map(fn ($id) => (int) $id)->all(); $reviewStats = $this->reviewStatsForApplicationIds($appIds); $reviewerCountsByTrack = ReviewerScope::query() ->select('track_code', DB::raw('COUNT(*) as total')) ->where('competition_id', $competition->id) ->groupBy('track_code') ->pluck('total', 'track_code'); $trackTitles = $competition->tracks->pluck('title', 'track_code'); $total = $apps->count(); $passed = $apps->where('review_result', 'passed')->count(); $rejected = $apps->where('review_result', 'rejected')->count(); $reviewedCount = 0; $eligibleReviewedCount = 0; foreach ($apps as $app) { $trackCode = (string) ($app->track ?? ''); $required = (int) ($reviewerCountsByTrack[$trackCode] ?? 0); $completed = (int) ($reviewStats[(int) $app->id]['submitted_review_count'] ?? 0); // 已评审:该项目所属赛道评委均已提交;跨全部赛道汇总 if ($required > 0 && $completed >= $required) { $reviewedCount += 1; if ($app->isReviewEligible()) { $eligibleReviewedCount += 1; } } } // 待评审 = 参与评审的项目数 − 其中已评审完成数(不含不参与评审) $pendingReview = max(0, $eligibleApps->count() - $eligibleReviewedCount); $passRate = $total > 0 ? round(($passed / $total) * 100, 1) : 0.0; $locationCounts = $this->eventLocationCounts($apps); return response()->json([ 'kpis' => [ ['label' => '项目总数', 'value' => $total, 'sub' => '已提交的有效项目数', 'type' => 'total'], ...$locationCounts, ['label' => '待评审数量', 'value' => $pendingReview, 'sub' => '参与评审项目中尚未评审完成的数量'], ['label' => '通过率', 'value' => $passRate.'%', 'sub' => '已通过 / 总项目'], ], 'track_counts' => $this->countEntries($apps, fn (Application $a) => $trackTitles->get($a->track ?? '') ?? ($a->track ?? '未填写')), 'degree_counts' => $this->countEntries($apps, fn (Application $a) => trim((string) ($a->degree ?? '')) ?: '未填写'), 'country_counts' => $this->countEntries($apps, fn (Application $a) => trim((string) ($a->location_country ?? '')) ?: '未填写'), 'province_counts' => $this->countEntries( $apps->filter(fn (Application $a) => trim((string) ($a->location_country ?? '')) === '中国'), fn (Application $a) => trim((string) ($a->location_province ?? '')) ?: '未填写', 6, ), 'school_top' => $this->topGroups($apps, fn (Application $a) => trim((string) ($a->school ?? '')) ?: '未填写'), 'city_top' => $this->topGroups($apps, fn (Application $a) => $this->cityLabel($a)), 'track_score_rank' => $this->trackScoreRank($apps, $reviewStats, $trackTitles, $reviewerCountsByTrack), 'track_top_projects' => $this->trackTopProjects($apps, $reviewStats, $trackTitles, $reviewerCountsByTrack), 'summary' => [ 'total' => $total, 'passed' => $passed, 'rejected' => $rejected, 'pending_review' => $pendingReview, 'reviewed' => $eligibleReviewedCount, ], ]); } /** * @param list $ids * @return array */ private function reviewStatsForApplicationIds(array $ids): array { if (count($ids) === 0) { return []; } return ApplicationReviewScore::query() ->select([ 'application_id', DB::raw('COUNT(*) as submitted_review_count'), DB::raw('AVG(line_total) as team_avg'), ]) ->whereIn('application_id', $ids) ->groupBy('application_id') ->get() ->mapWithKeys(fn ($row) => [ (int) $row->application_id => [ 'submitted_review_count' => (int) $row->submitted_review_count, 'team_avg' => $row->team_avg !== null ? (float) $row->team_avg : null, ], ]) ->all(); } /** * @param \Illuminate\Support\Collection $apps * @return list */ private function eventLocationCounts($apps): array { $counts = []; foreach ($apps as $app) { $code = trim((string) ($app->event_location ?? '')); if ($code === '') { continue; } $counts[$code] = ($counts[$code] ?? 0) + 1; } $cards = []; foreach (SignupOptionCatalog::eventLocations() as $code => $labels) { $label = (string) ($labels[PortalLocale::ZH] ?? $code); $title = str_contains($label, '赛区') ? $label : $label.'赛区'; $cards[] = [ 'label' => $title.'报名', 'value' => (int) ($counts[$code] ?? 0), 'sub' => '按意向参赛地点统计', 'type' => 'session', ]; } return $cards; } /** * @param \Illuminate\Support\Collection $apps * @return list */ private function countEntries($apps, callable $keyGetter, ?int $limit = null): array { $counts = []; foreach ($apps as $app) { $key = (string) $keyGetter($app); $counts[$key] = ($counts[$key] ?? 0) + 1; } arsort($counts); if ($limit !== null) { $counts = array_slice($counts, 0, $limit, true); } $total = max(1, $apps->count()); return collect($counts)->map(fn ($count, $label) => [ 'label' => (string) $label, 'count' => (int) $count, 'percent' => round(((int) $count / $total) * 100, 1), ])->values()->all(); } /** * @param \Illuminate\Support\Collection $apps * @return list */ private function topGroups($apps, callable $keyGetter, int $limit = 5): array { $groups = []; foreach ($apps as $app) { $key = (string) $keyGetter($app); if (! isset($groups[$key])) { $groups[$key] = ['total' => 0, 'pass' => 0]; } $groups[$key]['total'] += 1; if ($app->review_result === 'passed') { $groups[$key]['pass'] += 1; } } uasort($groups, fn ($a, $b) => $b['total'] <=> $a['total']); $top = array_slice($groups, 0, $limit, true); $rank = 1; $rows = []; foreach ($top as $name => $v) { $rate = $v['total'] > 0 ? round(($v['pass'] / $v['total']) * 100, 1).'%' : '-'; $rows[] = ['rank' => $rank++, 'name' => $name, 'total' => $v['total'], 'pass_rate' => $rate]; } return $rows; } private function cityLabel(Application $app): string { if (trim((string) ($app->location_country ?? '')) === '海外') { return trim((string) ($app->oversea_country ?? '')) ?: '海外'; } return trim((string) ($app->location_city ?? '')) ?: '未填写'; } /** * @param \Illuminate\Support\Collection $apps * @param array $reviewStats * @param \Illuminate\Support\Collection $trackTitles * @param \Illuminate\Support\Collection $reviewerCountsByTrack * @return list */ private function trackScoreRank($apps, array $reviewStats, $trackTitles, $reviewerCountsByTrack): array { $groups = []; foreach ($apps as $app) { $avg = $this->completedAvg($app, $reviewStats, $reviewerCountsByTrack); if ($avg === null) { continue; } $track = $trackTitles->get($app->track ?? '') ?? ($app->track ?? '未填写'); $groups[$track][] = $avg; } $rows = []; foreach ($groups as $track => $avgs) { $rows[] = [ 'track' => $track, 'avg' => round(array_sum($avgs) / count($avgs), 2), 'count' => count($avgs), ]; } usort($rows, fn ($a, $b) => $b['avg'] <=> $a['avg']); return array_map(fn ($row, $idx) => $row + ['rank' => $idx + 1], $rows, array_keys($rows)); } /** * @param \Illuminate\Support\Collection $apps * @return list}> */ private function trackTopProjects($apps, array $reviewStats, $trackTitles, $reviewerCountsByTrack): array { $byTrack = []; foreach ($apps as $app) { $avg = $this->completedAvg($app, $reviewStats, $reviewerCountsByTrack); if ($avg === null) { continue; } $track = $trackTitles->get($app->track ?? '') ?? ($app->track ?? '未填写'); $byTrack[$track][] = [ 'name' => trim((string) ($app->project_name ?? '')) ?: ('项目 #'.$app->id), 'avg' => $avg, ]; } ksort($byTrack); $result = []; foreach ($byTrack as $track => $items) { usort($items, fn ($a, $b) => $b['avg'] <=> $a['avg']); $result[] = [ 'track' => $track, 'items' => array_map( fn ($item, $idx) => ['rank' => $idx + 1, 'name' => $item['name'], 'avg' => round($item['avg'], 2)], array_slice($items, 0, 3), array_keys(array_slice($items, 0, 3)), ), ]; } return $result; } /** * @param array $reviewStats * @param \Illuminate\Support\Collection $reviewerCountsByTrack */ private function completedAvg(Application $app, array $reviewStats, $reviewerCountsByTrack): ?float { $trackCode = (string) ($app->track ?? ''); $required = (int) ($reviewerCountsByTrack[$trackCode] ?? 0); $stats = $reviewStats[(int) $app->id] ?? null; $completed = (int) ($stats['submitted_review_count'] ?? 0); if ($required <= 0 || $completed < $required) { return null; } return $stats['team_avg'] ?? null; } }