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.

256 lines
10 KiB

<?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();
$pendingReview = 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) {
$pendingReview += 1;
}
}
$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;
}
}