You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

260 lines
10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\ApplicationReviewScore;
use App\Models\Competition;
use App\Models\ReviewerScope;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
class ManageReportController extends Controller
{
public function show(Competition $competition): JsonResponse
{
$competition->loadMissing('tracks');
$apps = Application::query()
->where('competition_id', $competition->id)
->where('status', 'submitted')
->get();
$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;
$withCompany = 0;
foreach ($apps as $app) {
if (trim((string) ($app->company_name ?? '')) !== '') {
$withCompany += 1;
}
$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;
}
}
// 待评审 = 总项目数 − 已评审数(全赛事口径,非单一赛道)
$pendingReview = max(0, $total - $reviewedCount);
$passRate = $total > 0 ? round(($passed / $total) * 100, 1) : 0.0;
$companyRatio = $total > 0 ? round(($withCompany / $total) * 100, 1) : 0.0;
return response()->json([
'kpis' => [
['label' => '项目总数', 'value' => $total, 'sub' => '已提交的有效项目数'],
['label' => '待评审数量', 'value' => $pendingReview, 'sub' => '总项目数 − 已评审数(评审未全部完成)'],
['label' => '通过率', 'value' => $passRate.'%', 'sub' => '已通过 / 总项目'],
['label' => '企业化占比', 'value' => $companyRatio.'%', '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,
],
]);
}
/**
* @param list<int> $ids
* @return array<int, array{submitted_review_count: int, team_avg: float|null}>
*/
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<int, Application> $apps
* @return list<array{label: string, count: int, percent: float}>
*/
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<int, Application> $apps
* @return list<array{rank: int, name: string, total: int, pass_rate: string}>
*/
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<int, Application> $apps
* @param array<int, array{submitted_review_count: int, team_avg: float|null}> $reviewStats
* @param \Illuminate\Support\Collection<string, string> $trackTitles
* @param \Illuminate\Support\Collection<string, int> $reviewerCountsByTrack
* @return list<array{rank: int, track: string, avg: float, count: int}>
*/
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<int, Application> $apps
* @return list<array{track: string, items: list<array{rank: int, name: string, avg: float}>}>
*/
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<int, array{submitted_review_count: int, team_avg: float|null}> $reviewStats
* @param \Illuminate\Support\Collection<string, int> $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;
}
}