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() ->where(function ($q) { $q->whereNull('university_id') ->orDoesntHave('teachers'); }) ->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() ->where(function ($q) { $q->whereNull('university_id') ->orDoesntHave('teachers'); }) ->where('created_at', '>=', $today->copy()->subDays(30)) ->count(); $todoTotal = $teacherFollowTodo + $demandTodo + $paperTodo; $courseSessions = $this->countCourseScheduleDaysInMonth($monthStart, $monthEnd); $courseDraft = Course::query()->where('published', false)->count(); $activitySessions = $this->countActivityScheduleDaysInMonth($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 = []; /** @var array> */ $courseScheduledDates = []; /** @var array> */ $activityScheduledDates = []; $push = function ( string $date, string $type, string $title, int $entityId, string $route, ) use (&$events, &$seen): void { $title = $this->shortCalendarTitle($title); if ($title === '') { return; } $key = "{$date}|{$type}|{$entityId}|{$title}"; if (isset($seen[$key])) { return; } $seen[$key] = true; $events[] = [ 'date' => $date, 'type' => $type, 'title' => $title, 'entity_id' => $entityId, 'route' => $route, ]; }; $markCourseDate = function (int $courseId, string $date) use (&$courseScheduledDates): void { $courseScheduledDates[$courseId][$date] = true; }; $markActivityDate = function (int $activityId, string $date) use (&$activityScheduledDates): void { $activityScheduledDates[$activityId][$date] = true; }; $hasCourseDate = function (int $courseId, string $date) use (&$courseScheduledDates): bool { return isset($courseScheduledDates[$courseId][$date]); }; $hasActivityDate = function (int $activityId, string $date) use (&$activityScheduledDates): bool { return isset($activityScheduledDates[$activityId][$date]); }; CourseSession::query() ->with('course:id,title') ->whereBetween('starts_at', [$monthStart, $monthEnd->endOfDay()]) ->orderBy('starts_at') ->get() ->each(function (CourseSession $session) use ($push, $markCourseDate) { if (! $session->starts_at || ! $session->course) { return; } $date = $session->starts_at->toDateString(); $markCourseDate((int) $session->course_id, $date); $push( $date, 'course', $session->title ?: $session->course->title, (int) $session->course_id, '/courses', ); }); CourseCheckinDay::query() ->with('course:id,title') ->whereBetween('teach_date', [$monthStart->toDateString(), $monthEnd->toDateString()]) ->orderBy('teach_date') ->get() ->each(function (CourseCheckinDay $day) use ($push, $markCourseDate) { if (! $day->course) { return; } $date = $day->teach_date->toDateString(); $markCourseDate((int) $day->course_id, $date); $push( $date, 'course', $day->course->title, (int) $day->course_id, '/courses', ); }); 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 ($push, $monthStart, $monthEnd) { foreach ($this->eachDateInMonthRange( $course->teach_start_date, $course->teach_end_date ?? $course->teach_start_date, $monthStart, $monthEnd, ) as $date) { $push($date, 'course', $course->title, (int) $course->id, '/courses'); } }); ActivitySession::query() ->with('activity:id,title') ->whereBetween('starts_at', [$monthStart, $monthEnd->endOfDay()]) ->orderBy('starts_at') ->get() ->each(function (ActivitySession $session) use ($push, $markActivityDate) { if (! $session->starts_at || ! $session->activity) { return; } $date = $session->starts_at->toDateString(); $activityTitle = $session->activity->title; $sessionLabel = trim((string) $session->title) !== '' ? $session->title : '场次'; $markActivityDate((int) $session->activity_id, $date); $push( $date, 'activity', $activityTitle.'·'.$sessionLabel, (int) $session->activity_id, '/activities', ); }); 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()]) ->orWhereBetween('signup_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()); }); }) ->get(['id', 'title', 'event_start_date', 'event_end_date', 'signup_end_date']) ->each(function (Activity $activity) use ($push, $monthStart, $monthEnd, $hasActivityDate) { if ($activity->event_start_date) { foreach ($this->eachDateInMonthRange( $activity->event_start_date, $activity->event_end_date ?? $activity->event_start_date, $monthStart, $monthEnd, ) as $date) { if ($hasActivityDate((int) $activity->id, $date)) { continue; } $push($date, 'activity', $activity->title, (int) $activity->id, '/activities'); } } if ($activity->signup_end_date && $activity->signup_end_date->gte($monthStart) && $activity->signup_end_date->lte($monthEnd)) { $push( $activity->signup_end_date->toDateString(), 'activity', $activity->title.'·报名截止', (int) $activity->id, '/activities', ); } }); usort($events, fn ($a, $b) => [$a['date'], $a['type'], $a['title']] <=> [$b['date'], $b['type'], $b['title']]); return $events; } protected function countCourseScheduleDaysInMonth(Carbon $monthStart, Carbon $monthEnd): int { $dates = []; CourseSession::query() ->whereBetween('starts_at', [$monthStart, $monthEnd->endOfDay()]) ->get(['starts_at']) ->each(function (CourseSession $s) use (&$dates) { if ($s->starts_at) { $dates[$s->starts_at->toDateString()] = true; } }); CourseCheckinDay::query() ->whereBetween('teach_date', [$monthStart->toDateString(), $monthEnd->toDateString()]) ->get(['teach_date']) ->each(function (CourseCheckinDay $d) use (&$dates) { $dates[$d->teach_date->toDateString()] = true; }); 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(['teach_start_date', 'teach_end_date']) ->each(function (Course $c) use (&$dates, $monthStart, $monthEnd) { foreach ($this->eachDateInMonthRange( $c->teach_start_date, $c->teach_end_date ?? $c->teach_start_date, $monthStart, $monthEnd, ) as $date) { $dates[$date] = true; } }); return count($dates); } protected function countActivityScheduleDaysInMonth(Carbon $monthStart, Carbon $monthEnd): int { $dates = []; /** @var array> */ $activityScheduledDates = []; ActivitySession::query() ->whereBetween('starts_at', [$monthStart, $monthEnd->endOfDay()]) ->get(['activity_id', 'starts_at']) ->each(function (ActivitySession $s) use (&$dates, &$activityScheduledDates) { if (! $s->starts_at) { return; } $date = $s->starts_at->toDateString(); $dates[$date] = true; $activityScheduledDates[(int) $s->activity_id][$date] = true; }); 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()]) ->orWhereBetween('signup_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()); }); }) ->get(['id', 'event_start_date', 'event_end_date', 'signup_end_date']) ->each(function (Activity $a) use (&$dates, &$activityScheduledDates, $monthStart, $monthEnd) { if ($a->event_start_date) { foreach ($this->eachDateInMonthRange( $a->event_start_date, $a->event_end_date ?? $a->event_start_date, $monthStart, $monthEnd, ) as $date) { if (isset($activityScheduledDates[(int) $a->id][$date])) { continue; } $dates[$date] = true; } } if ($a->signup_end_date && $a->signup_end_date->gte($monthStart) && $a->signup_end_date->lte($monthEnd)) { $dates[$a->signup_end_date->toDateString()] = true; } }); return count($dates); } /** * @return list Y-m-d */ protected function eachDateInMonthRange( ?Carbon $start, ?Carbon $end, Carbon $monthStart, Carbon $monthEnd, ): array { if (! $start) { return []; } $from = $start->copy()->startOfDay()->max($monthStart->copy()->startOfDay()); $to = ($end ?? $start)->copy()->startOfDay()->min($monthEnd->copy()->startOfDay()); if ($from->gt($to)) { return []; } $dates = []; for ($cursor = $from->copy(); $cursor->lte($to); $cursor->addDay()) { $dates[] = $cursor->toDateString(); if (count($dates) >= 31) { break; } } return $dates; } protected function shortCalendarTitle(string $title, int $max = 20): string { $title = trim($title); if ($title === '') { return ''; } return mb_strlen($title) > $max ? mb_substr($title, 0, $max) : $title; } }