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
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);
|
|
}
|
|
}
|