'未开始', 2 => '进行中', 3 => '已结束', default => '未开始', }; } public static function formatTimeValue(mixed $time): ?string { if ($time === null || $time === '') { return null; } $str = (string) $time; return strlen($str) >= 5 ? substr($str, 0, 5) : $str; } public static function timeRange(?string $start, ?string $end): ?string { $start = self::formatTimeValue($start); $end = self::formatTimeValue($end); if ($start && $end) { return "{$start}-{$end}"; } return $start ?: $end; } public static function serializeDictItem(?DictItem $item): ?array { if (! $item) { return null; } return [ 'id' => $item->id, 'label' => $item->label, 'value' => $item->value, ]; } public static function serializeCourseMedia(?CourseMedia $media, ?string $legacyUrl = null): ?array { if ($media) { return $media->toApiArray(); } if ($legacyUrl === null || $legacyUrl === '') { return null; } return [ 'id' => null, 'url' => $legacyUrl, 'category' => null, ]; } public static function resolveCourseProgressStatus(Course $course): int { return ScheduleProgressStatus::resolve( $course->teach_start_date?->toDateString(), $course->teach_end_date?->toDateString(), ); } public static function resolveActivityProgressStatus(Activity $activity): int { return ScheduleProgressStatus::resolve( $activity->event_start_date?->toDateString(), $activity->event_end_date?->toDateString(), ); } public static function isWithinSignupWindow(?string $start, ?string $end): bool { $today = now()->startOfDay(); if ($start) { $startDate = Carbon::parse($start)->startOfDay(); if ($today->lt($startDate)) { return false; } } if ($end) { $endDate = Carbon::parse($end)->startOfDay(); if ($today->gt($endDate)) { return false; } } return true; } public static function canSignupCourse(Course $course, int $signupsCount): bool { if ((int) $course->published !== 1) { return false; } if (self::resolveCourseProgressStatus($course) === 3) { return false; } if (! self::isWithinSignupWindow( $course->signup_start_date?->toDateString(), $course->signup_end_date?->toDateString() )) { return false; } $capacity = (int) $course->capacity; if ($capacity > 0 && $signupsCount >= $capacity) { return false; } return true; } public static function activityDisplayStatus(Activity $activity): string { if (self::isWithinSignupWindow( $activity->signup_start_date?->toDateString(), $activity->signup_end_date?->toDateString() )) { return '报名中'; } return self::progressStatusLabel(self::resolveActivityProgressStatus($activity)); } public static function serializeCourseList(Course $course, ?MiniappUser $user = null): array { $signupsCount = (int) ($course->signups_count ?? 0); $mainSpeaker = is_array($course->main_speakers) ? ($course->main_speakers[0] ?? null) : null; $hasSignedUp = $user ? $course->signups()->where('miniapp_user_id', $user->id)->exists() : false; $progressStatus = self::resolveCourseProgressStatus($course); return [ 'id' => $course->id, 'title' => $course->title, 'category' => $course->courseSystemItem?->label, 'course_system_dict_item_id' => $course->course_system_dict_item_id, 'course_system_item' => self::serializeDictItem($course->courseSystemItem), 'teach_start_date' => $course->teach_start_date?->toDateString(), 'teach_end_date' => $course->teach_end_date?->toDateString(), 'teach_start_time' => self::formatTimeValue($course->teach_start_time), 'teach_end_time' => self::formatTimeValue($course->teach_end_time), 'time_range' => self::timeRange( self::formatTimeValue($course->teach_start_time), self::formatTimeValue($course->teach_end_time) ), 'location' => $course->location, 'teacher' => is_array($mainSpeaker) ? ($mainSpeaker['name'] ?? null) : null, 'teacher_title' => is_array($mainSpeaker) ? ($mainSpeaker['title'] ?? null) : null, 'university' => is_array($mainSpeaker) ? ($mainSpeaker['university'] ?? null) : null, 'main_speakers' => $course->main_speakers ?? [], 'recruit_targets' => $course->recruit_targets ?? [], 'signup_start_date' => $course->signup_start_date?->toDateString(), 'signup_end_date' => $course->signup_end_date?->toDateString(), 'capacity' => (int) $course->capacity, 'signups_count' => $signupsCount, 'progress_status' => $progressStatus, 'progress_status_label' => self::progressStatusLabel($progressStatus), 'intro_html' => $course->intro_html, 'cover' => self::serializeCourseMedia($course->coverMedia, $course->cover_url ?? null), 'promo' => self::serializeCourseMedia($course->promoMedia, $course->promo_url ?? null), 'can_signup' => self::canSignupCourse($course, $signupsCount) && ! $hasSignedUp, 'has_signed_up' => $hasSignedUp, ]; } public static function serializeActivityList(Activity $activity, ?MiniappUser $user = null): array { $hasSignedUp = $user ? $activity->signups()->where('miniapp_user_id', $user->id)->exists() : false; $progressStatus = self::resolveActivityProgressStatus($activity); return [ 'id' => $activity->id, 'title' => $activity->title, 'activity_type_dict_item_id' => $activity->activity_type_dict_item_id, 'activity_type_item' => self::serializeDictItem($activity->activityTypeItem), 'event_start_date' => $activity->event_start_date?->toDateString(), 'event_end_date' => $activity->event_end_date?->toDateString(), 'signup_start_date' => $activity->signup_start_date?->toDateString(), 'signup_end_date' => $activity->signup_end_date?->toDateString(), 'location' => $activity->location, 'intro_html' => $activity->intro_html, 'progress_status' => $progressStatus, 'progress_status_label' => self::progressStatusLabel($progressStatus), 'display_status' => self::activityDisplayStatus($activity), 'signups_count' => (int) ($activity->signups_count ?? 0), 'has_signed_up' => $hasSignedUp, 'can_signup' => self::canSignupActivity($activity, $user), ]; } public static function canSignupActivity(Activity $activity, ?MiniappUser $user = null): bool { if ((int) $activity->published !== 1) { return false; } if (self::activityDisplayStatus($activity) !== '报名中') { return false; } $sessions = $activity->relationLoaded('sessions') ? $activity->sessions : $activity->sessions()->orderBy('sort')->orderBy('id')->get(); if ($sessions->isEmpty()) { return false; } $userId = $user?->id; foreach ($sessions as $session) { $signed = $session->signups()->count(); $capacity = $session->capacity !== null ? (int) $session->capacity : 0; $isFull = $capacity > 0 && $signed >= $capacity; $hasSignedUp = $userId ? $session->signups()->where('miniapp_user_id', $userId)->exists() : false; if (! $isFull && ! $hasSignedUp) { return true; } } return false; } public static function serializeActivitySession(ActivitySession $session, ?MiniappUser $user = null): array { $signed = $session->signups()->count(); $capacity = $session->capacity !== null ? (int) $session->capacity : 0; $hasSignedUp = $user ? $session->signups()->where('miniapp_user_id', $user->id)->exists() : false; return [ 'id' => $session->id, 'title' => $session->title, 'date' => $session->starts_at?->toDateString(), 'end_date' => $session->ends_at?->toDateString(), 'time' => self::timeRange( $session->starts_at?->format('H:i'), $session->ends_at?->format('H:i') ), 'venue' => $session->venue, 'capacity' => $capacity, 'signed' => $signed, 'remaining' => $capacity > 0 ? max($capacity - $signed, 0) : null, 'is_full' => $capacity > 0 && $signed >= $capacity, 'has_signed_up' => $hasSignedUp, ]; } public static function serializeBanner(Banner $banner): array { $target = match ($banner->type) { Banner::TYPE_COURSE => [ 'target_type' => 'course', 'target_id' => $banner->course_id, 'title' => $banner->course?->title, ], Banner::TYPE_ACTIVITY => [ 'target_type' => 'activity', 'target_id' => $banner->activity_id, 'title' => $banner->activity?->title, ], Banner::TYPE_NEWS => [ 'target_type' => 'news', 'target_id' => $banner->news_id, 'title' => $banner->news?->title, ], default => [ 'target_type' => 'custom', 'target_id' => $banner->id, 'title' => $banner->title, ], }; $display = self::bannerDisplayMeta($banner); return [ 'id' => $banner->id, 'type' => $banner->type, 'title' => $banner->title ?: $target['title'], 'cover_url' => self::resolveBannerCoverUrl($banner), 'content_html' => $banner->content_html, 'sort' => (int) $banner->sort, 'kicker' => $display['kicker'], 'status_label' => $display['status_label'], 'seats_text' => $display['seats_text'], 'datetime_text' => $display['datetime_text'], ...$target, ]; } /** * @return array{kicker: ?string, status_label: ?string, seats_text: ?string, datetime_text: ?string} */ private static function bannerDisplayMeta(Banner $banner): array { if ($banner->type === Banner::TYPE_COURSE && $banner->course) { $course = $banner->course; $signupsCount = (int) ($course->signups_count ?? 0); $canSignup = self::canSignupCourse($course, $signupsCount); return [ 'kicker' => $course->courseSystemItem?->label ?: '热门课程', 'status_label' => $canSignup ? '报名中' : self::progressStatusLabel(self::resolveCourseProgressStatus($course)), 'seats_text' => self::formatBannerSeats($signupsCount, (int) $course->capacity), 'datetime_text' => self::formatBannerDatetime( $course->teach_start_date?->toDateString(), $course->teach_end_date?->toDateString(), self::timeRange( self::formatTimeValue($course->teach_start_time), self::formatTimeValue($course->teach_end_time) ) ), ]; } if ($banner->type === Banner::TYPE_ACTIVITY && $banner->activity) { $activity = $banner->activity; $canSignup = self::canSignupActivity($activity); return [ 'kicker' => $activity->activityTypeItem?->label ?: '热门活动', 'status_label' => $canSignup ? '报名中' : self::progressStatusLabel(self::resolveActivityProgressStatus($activity)), 'seats_text' => ((int) ($activity->signups_count ?? 0)) > 0 ? ((int) $activity->signups_count).'人已报名' : null, 'datetime_text' => self::formatBannerDatetime( $activity->event_start_date?->toDateString(), $activity->event_end_date?->toDateString(), null ), ]; } if ($banner->type === Banner::TYPE_NEWS && $banner->news) { $news = $banner->news; return [ 'kicker' => $news->categoryItem?->label ?: '资讯', 'status_label' => null, 'seats_text' => null, 'datetime_text' => $news->published_at?->format('n月j日'), ]; } return [ 'kicker' => '推荐', 'status_label' => null, 'seats_text' => null, 'datetime_text' => null, ]; } private static function resolveBannerCoverUrl(Banner $banner): ?string { if ($banner->type === Banner::TYPE_COURSE && $banner->course) { $courseCover = self::serializeCourseMedia( $banner->course->coverMedia, $banner->course->cover_url ?? null ); $courseCoverUrl = is_array($courseCover) ? ($courseCover['url'] ?? null) : null; if ($courseCoverUrl !== null && $courseCoverUrl !== '') { return $courseCoverUrl; } } if ($banner->type === Banner::TYPE_NEWS && $banner->news) { $newsCover = trim((string) ($banner->news->cover_url ?? '')); if ($newsCover !== '') { return $newsCover; } } $bannerCover = trim((string) ($banner->cover_url ?? '')); return $bannerCover !== '' ? $bannerCover : null; } private static function formatBannerSeats(int $signed, int $capacity): ?string { if ($capacity <= 0) { return $signed > 0 ? "{$signed}人已报名" : null; } return "{$signed}/{$capacity}人"; } private static function formatBannerDateText(?string $startDate, ?string $endDate): ?string { if ($startDate === null || $startDate === '') { return null; } $startText = Carbon::parse($startDate)->format('n月j日'); if ($endDate === null || $endDate === '' || $endDate === $startDate) { return $startText; } $endText = Carbon::parse($endDate)->format('n月j日'); return "{$startText}-{$endText}"; } private static function formatBannerDatetime(?string $startDate, ?string $endDate, ?string $timeRange): ?string { $dateText = self::formatBannerDateText($startDate, $endDate); if ($dateText === null || $dateText === '') { return $timeRange; } $isSingleDay = $endDate === null || $endDate === '' || $endDate === $startDate; if (! $isSingleDay || $timeRange === null || $timeRange === '') { return $dateText; } $start = explode('-', $timeRange)[0] ?? $timeRange; return trim("{$dateText} {$start}"); } public static function serializeNewsList(News $news): array { return [ 'id' => $news->id, 'title' => $news->title, 'tag' => $news->categoryItem?->label, 'category_dict_item_id' => $news->category_dict_item_id, 'category_item' => self::serializeDictItem($news->categoryItem), 'summary' => $news->summary, 'date' => $news->published_at?->toDateString(), 'published_at' => $news->published_at?->toIso8601String(), ]; } public static function serializeNewsDetail(News $news): array { $row = self::serializeNewsList($news); $row['content_html'] = $news->content_html; $row['content'] = strip_tags((string) $news->content_html); $row['source'] = $news->source ?: $news->source_site; return $row; } public static function userPayload(MiniappUser $user): array { $user->loadMissing(['teacher:id,is_partner,name', 'researchDirections:id,name']); $directions = $user->researchDirections; return [ 'id' => $user->id, 'nickname' => $user->nickname, 'avatar_url' => $user->avatar_url, 'name' => $user->name, 'mobile' => $user->mobile, 'company' => $user->company, 'job_title' => $user->job_title, 'research_direction' => $directions->pluck('name')->implode('、') ?: null, 'research_direction_ids' => $directions->pluck('id')->values()->all(), 'is_partner' => (bool) ($user->teacher?->is_partner ?? false), 'teacher_id' => $user->teacher_id, ]; } }