copy()->startOfMonth(); $monthEnd = $today->copy()->endOfMonth(); $universityQuery = University::query()->where('status', 1); $universityTotal = (clone $universityQuery)->count(); $yangtzeCount = (clone $universityQuery) ->where(function ($q) { $q->whereIn('province', ['上海', '江苏', '浙江']) ->orWhereIn('city', ['上海', '苏州', '南京', '杭州', '无锡', '宁波']); }) ->count(); $highValueUniversities = University::query() ->where('status', 1) ->whereHas('teachers', function ($q) { $q->whereHas('starLevelItem', fn ($sq) => $sq->where('value', '>=', '4')); }) ->count(); $teacherQuery = Teacher::query(); $teacherTotal = (clone $teacherQuery)->count(); $monthNewTeachers = (clone $teacherQuery) ->whereBetween('created_at', [$monthStart, $monthEnd->endOfDay()]) ->count(); $fiveStarTeachers = (clone $teacherQuery) ->whereHas('starLevelItem', fn ($q) => $q->where('value', '5')) ->count(); $paperTotal = Paper::query()->count(); $monthNewPapers = Paper::query() ->whereBetween('created_at', [$monthStart, $monthEnd->endOfDay()]) ->count(); $pendingLinkPapers = Paper::query()->pendingTeacherLink()->count(); $demandTotal = Demand::query()->count(); $doneStatusId = $this->demandStatusId('done'); $activeStatusId = $this->demandStatusId('active'); $demandDone = $doneStatusId ? Demand::query()->where('status_dict_item_id', $doneStatusId)->count() : 0; $demandActive = $activeStatusId ? Demand::query()->where('status_dict_item_id', $activeStatusId)->count() : 0; $fulfillmentRate = $demandTotal > 0 ? (int) round($demandDone / $demandTotal * 100) : 0; $teacherFollowTodo = Teacher::query() ->where('is_partner', false) ->whereNotNull('next_follow_date') ->where('next_follow_date', '<', $today->toDateString()) ->whereHas('starLevelItem', fn ($q) => $q->whereIn('value', ['4', '5'])) ->count(); $demandOverdue = $activeStatusId ? Demand::query() ->where('status_dict_item_id', $activeStatusId) ->where('submitted_at', '<', $today->copy()->subDays(7)) ->count() : 0; $demandWaiting = $demandActive; $demandTodo = $demandOverdue + min($demandWaiting, max(0, $demandWaiting - $demandOverdue)); $paperTodo = Paper::query() ->pendingTeacherLink() ->where('created_at', '>=', $today->copy()->subDays(30)) ->count(); $todoTotal = $teacherFollowTodo + $demandTodo + $paperTodo; $courseSessions = $this->countCoursesScheduledInMonth($monthStart, $monthEnd); $courseDraft = Course::query()->where('published', 0)->count(); $activitySessions = $this->countActivitiesScheduledInMonth($monthStart, $monthEnd); $activityOpen = Activity::query() ->where('published', 1) ->where('signup_end_date', '>=', $today->toDateString()) ->count(); $signupTotal = CourseSignup::query()->count() + ActivitySignup::query()->count(); $lastMonthSignup = CourseSignup::query() ->whereBetween('created_at', [$monthStart->copy()->subMonth(), $monthStart->copy()->subDay()->endOfDay()]) ->count() + ActivitySignup::query() ->whereBetween('created_at', [$monthStart->copy()->subMonth(), $monthStart->copy()->subDay()->endOfDay()]) ->count(); $monthSignup = CourseSignup::query() ->whereBetween('created_at', [$monthStart, $monthEnd->endOfDay()]) ->count() + ActivitySignup::query() ->whereBetween('created_at', [$monthStart, $monthEnd->endOfDay()]) ->count(); $signupDelta = $monthSignup - $lastMonthSignup; $teacherLeads = Teacher::query() ->whereBetween('created_at', [$monthStart, $monthEnd->endOfDay()]) ->count(); $demandLeads = Demand::query() ->whereBetween('created_at', [$monthStart, $monthEnd->endOfDay()]) ->count(); $calendarEvents = $this->buildCalendarEvents($monthStart, $monthEnd); return $this->ok([ 'overview' => [ 'universities' => [ 'total' => $universityTotal, 'yangtze' => $yangtzeCount, 'high_value' => $highValueUniversities, ], 'teachers' => [ 'total' => $teacherTotal, 'month_new' => $monthNewTeachers, 'five_star' => $fiveStarTeachers, ], 'papers' => [ 'total' => $paperTotal, 'month_new' => $monthNewPapers, 'pending_link' => $pendingLinkPapers, ], 'demands' => [ 'total' => $demandTotal, 'done' => $demandDone, 'active' => $demandActive, 'fulfillment_rate' => $fulfillmentRate, ], ], 'todos' => [ 'total' => $todoTotal, 'teacher_follow' => $teacherFollowTodo, 'demand_process' => $demandTodo, 'demand_overdue' => $demandOverdue, 'demand_waiting' => $demandWaiting, 'paper_data' => $paperTodo, ], 'events' => [ 'course_sessions' => $courseSessions, 'course_draft' => $courseDraft, 'activity_sessions' => $activitySessions, 'activity_open' => $activityOpen, 'signup_total' => $signupTotal, 'signup_delta' => $signupDelta, 'teacher_leads' => $teacherLeads, 'demand_leads' => $demandLeads, ], 'calendar' => [ 'year' => (int) $today->year, 'month' => (int) $today->month, 'today' => $today->toDateString(), 'events' => $calendarEvents, ], ]); } protected function demandStatusId(string $value): ?int { $typeId = DictType::query()->where('code', 'demand_status')->value('id'); if (! $typeId) { return null; } return DictItem::query() ->where('dict_type_id', $typeId) ->where('value', $value) ->where('status', 1) ->value('id'); } /** * @return list */ protected function buildCalendarEvents(Carbon $monthStart, Carbon $monthEnd): array { $events = []; $seen = []; $pushRange = function ( ?Carbon $start, ?Carbon $end, string $type, string $title, int $entityId, string $route, ) use (&$events, &$seen, $monthStart, $monthEnd): void { if (! $start) { return; } $title = $this->shortCalendarTitle($title); if ($title === '') { return; } $rangeStart = $start->copy()->startOfDay()->max($monthStart->copy()->startOfDay())->toDateString(); $rangeEnd = ($end ?? $start)->copy()->startOfDay()->min($monthEnd->copy()->startOfDay())->toDateString(); if ($rangeStart > $rangeEnd) { return; } $key = "{$type}|{$entityId}|{$title}|{$rangeStart}|{$rangeEnd}"; if (isset($seen[$key])) { return; } $seen[$key] = true; $events[] = [ 'start_date' => $rangeStart, 'end_date' => $rangeEnd, 'type' => $type, 'title' => $title, 'entity_id' => $entityId, 'route' => $route, ]; }; Course::query() ->whereNotNull('teach_start_date') ->where(function ($q) use ($monthStart, $monthEnd) { $q->whereBetween('teach_start_date', [$monthStart->toDateString(), $monthEnd->toDateString()]) ->orWhereBetween('teach_end_date', [$monthStart->toDateString(), $monthEnd->toDateString()]) ->orWhere(function ($q2) use ($monthStart, $monthEnd) { $q2->where('teach_start_date', '<=', $monthStart->toDateString()) ->where('teach_end_date', '>=', $monthEnd->toDateString()); }); }) ->get(['id', 'title', 'teach_start_date', 'teach_end_date']) ->each(function (Course $course) use ($pushRange) { $pushRange( $course->teach_start_date, $course->teach_end_date ?? $course->teach_start_date, 'course', $course->title, (int) $course->id, '/courses', ); }); Activity::query() ->where(function ($q) use ($monthStart, $monthEnd) { $q->whereBetween('event_start_date', [$monthStart->toDateString(), $monthEnd->toDateString()]) ->orWhereBetween('event_end_date', [$monthStart->toDateString(), $monthEnd->toDateString()]) ->orWhere(function ($q2) use ($monthStart, $monthEnd) { $q2->where('event_start_date', '<=', $monthStart->toDateString()) ->where('event_end_date', '>=', $monthEnd->toDateString()); }); }) ->whereNotNull('event_start_date') ->get(['id', 'title', 'event_start_date', 'event_end_date']) ->each(function (Activity $activity) use ($pushRange) { $pushRange( $activity->event_start_date, $activity->event_end_date ?? $activity->event_start_date, 'activity', $activity->title, (int) $activity->id, '/activities', ); }); usort( $events, fn ($a, $b) => [$a['start_date'], $a['type'], $a['title']] <=> [$b['start_date'], $b['type'], $b['title']], ); return $events; } /** 当月有排期的课程数量(按课程去重,非排期天数) */ protected function countCoursesScheduledInMonth(Carbon $monthStart, Carbon $monthEnd): int { return Course::query() ->whereNotNull('teach_start_date') ->where(function ($q) use ($monthStart, $monthEnd) { $q->whereBetween('teach_start_date', [$monthStart->toDateString(), $monthEnd->toDateString()]) ->orWhereBetween('teach_end_date', [$monthStart->toDateString(), $monthEnd->toDateString()]) ->orWhere(function ($q2) use ($monthStart, $monthEnd) { $q2->where('teach_start_date', '<=', $monthStart->toDateString()) ->where('teach_end_date', '>=', $monthEnd->toDateString()); }); }) ->count(); } /** 当月有排期的活动数量(按活动去重,非排期天数) */ protected function countActivitiesScheduledInMonth(Carbon $monthStart, Carbon $monthEnd): int { return Activity::query() ->whereNotNull('event_start_date') ->where(function ($q) use ($monthStart, $monthEnd) { $q->whereBetween('event_start_date', [$monthStart->toDateString(), $monthEnd->toDateString()]) ->orWhereBetween('event_end_date', [$monthStart->toDateString(), $monthEnd->toDateString()]) ->orWhere(function ($q2) use ($monthStart, $monthEnd) { $q2->where('event_start_date', '<=', $monthStart->toDateString()) ->where('event_end_date', '>=', $monthEnd->toDateString()); }); }) ->count(); } protected function shortCalendarTitle(string $title): string { return trim($title); } }