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.

266 lines
8.5 KiB

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\DictItem;
use App\Models\DictType;
use App\Models\Teacher;
use App\Models\University;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class RadarMapController extends Controller
{
use ApiResponse;
public function index(Request $request): JsonResponse
{
$universities = University::query()
->where('status', 1)
->whereNotNull('latitude')
->whereNotNull('longitude')
->whereHas('teachers')
->with(['teachers' => function ($q) {
$q->with(['starLevelItem', 'researchDirections'])->orderBy('name');
}])
->orderBy('name')
->get();
$bounds = $this->resolveBounds($universities);
$schools = $universities->map(function (University $u) use ($bounds) {
return [
'id' => $u->id,
'name' => $u->name,
'city' => $u->city,
'latitude' => (float) $u->latitude,
'longitude' => (float) $u->longitude,
'left_percent' => $this->toPercent($u->longitude, $bounds['min_lng'], $bounds['max_lng']),
'top_percent' => $this->toPercentInverted($u->latitude, $bounds['min_lat'], $bounds['max_lat']),
'teachers_count' => $u->teachers->count(),
'teachers' => $u->teachers->map(fn ($t) => [
'id' => $t->id,
'name' => $t->name,
'research_direction' => $t->researchDirections->pluck('name')->join('、') ?: null,
'research_directions' => $t->researchDirections->map(fn ($d) => [
'id' => $d->id,
'name' => $d->name,
])->values()->all(),
'star_level_item' => $t->starLevelItem ? [
'id' => $t->starLevelItem->id,
'label' => $t->starLevelItem->label,
'value' => $t->starLevelItem->value,
] : null,
])->values(),
];
})->values();
$summary = $this->buildSummary($universities);
$quality = $this->buildQualityStats();
$researchFields = $this->buildResearchFields($universities);
return $this->ok([
'refreshed_at' => now()->toIso8601String(),
'bounds' => $bounds,
'schools' => $schools,
'summary' => $summary,
'quality' => $quality,
'research_fields' => $researchFields,
]);
}
/**
* @param \Illuminate\Support\Collection<int, University> $universities
* @return array<string, int|float>
*/
protected function buildSummary($universities): array
{
$coveredSchools = University::query()
->where('status', 1)
->whereHas('teachers')
->count();
$mapTeachers = $universities->sum(fn (University $u) => $u->teachers->count());
$fiveStarId = $this->starLevelId('5');
$fiveStarTeachers = $fiveStarId
? Teacher::query()->where('star_level_dict_item_id', $fiveStarId)->count()
: 0;
$pendingCoords = University::query()
->where('status', 1)
->where(function ($q) {
$q->whereNull('latitude')->orWhereNull('longitude');
})
->count();
$maxStar = 0;
foreach ($universities as $u) {
foreach ($u->teachers as $t) {
$val = (int) ($t->starLevelItem?->value ?? 0);
if ($val > $maxStar) {
$maxStar = $val;
}
}
}
return [
'covered_schools' => $coveredSchools,
'map_teachers' => $mapTeachers,
'five_star_teachers' => $fiveStarTeachers,
'pending_coords' => $pendingCoords,
'visible_points' => $universities->count(),
'max_star' => $maxStar,
];
}
/**
* @return list<array{label:string, detail:string}>
*/
protected function buildQualityStats(): array
{
$withCoords = University::query()
->where('status', 1)
->whereNotNull('latitude')
->whereNotNull('longitude')
->count();
$pendingCoords = University::query()
->where('status', 1)
->where(function ($q) {
$q->whereNull('latitude')->orWhereNull('longitude');
})
->count();
$teacherTotal = Teacher::query()->count();
$linkedTeachers = Teacher::query()->whereNotNull('university_id')->count();
$linkRate = $teacherTotal > 0 ? (int) round($linkedTeachers / $teacherTotal * 100) : 0;
$noStar = Teacher::query()->whereNull('star_level_dict_item_id')->count();
$pendingPapers = \App\Models\Paper::query()
->where(function ($q) {
$q->whereNull('university_id')->orDoesntHave('teachers');
})
->count();
return [
[
'label' => '坐标完整率',
'detail' => "{$withCoords} 所已配置坐标,{$pendingCoords} 所待补充",
],
[
'label' => '老师关联率',
'detail' => "已入库老师中 {$linkRate}% 关联高校",
],
[
'label' => '星级完整率',
'detail' => "{$noStar} 位老师尚未完成星级评定",
],
[
'label' => '论文反查',
'detail' => "{$pendingPapers} 篇论文待关联高校或老师",
],
];
}
/**
* @param \Illuminate\Support\Collection<int, University> $universities
* @return list<array{label:string, count:int, percent:int}>
*/
protected function buildResearchFields($universities): array
{
$counts = [];
foreach ($universities as $u) {
foreach ($u->teachers as $t) {
$dirs = $t->researchDirections->pluck('name')->filter()->all();
if ($dirs === []) {
$counts['未填写'] = ($counts['未填写'] ?? 0) + 1;
continue;
}
foreach ($dirs as $dir) {
$counts[$dir] = ($counts[$dir] ?? 0) + 1;
}
}
}
if ($counts === []) {
return [];
}
arsort($counts);
$total = array_sum($counts);
$result = [];
foreach (array_slice($counts, 0, 5, true) as $label => $count) {
$result[] = [
'label' => $label,
'count' => $count,
'percent' => (int) round($count / $total * 100),
];
}
return $result;
}
protected function starLevelId(string $value): ?int
{
$typeId = DictType::query()->where('code', 'teacher_level')->value('id');
if (! $typeId) {
return null;
}
return DictItem::query()
->where('dict_type_id', $typeId)
->where('value', $value)
->where('status', 1)
->value('id');
}
/**
* @param \Illuminate\Support\Collection<int, University> $universities
* @return array{min_lat:float,max_lat:float,min_lng:float,max_lng:float}
*/
protected function resolveBounds($universities): array
{
if ($universities->isEmpty()) {
return [
'min_lat' => 30.0,
'max_lat' => 32.0,
'min_lng' => 120.0,
'max_lng' => 122.0,
];
}
return [
'min_lat' => (float) $universities->min('latitude'),
'max_lat' => (float) $universities->max('latitude'),
'min_lng' => (float) $universities->min('longitude'),
'max_lng' => (float) $universities->max('longitude'),
];
}
protected function toPercent(float $value, float $min, float $max): float
{
if ($max <= $min) {
return 50.0;
}
$ratio = ($value - $min) / ($max - $min);
return round(10 + $ratio * 80, 2);
}
protected function toPercentInverted(float $value, float $min, float $max): float
{
if ($max <= $min) {
return 50.0;
}
$ratio = ($max - $value) / ($max - $min);
return round(10 + $ratio * 80, 2);
}
}