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.
495 lines
18 KiB
495 lines
18 KiB
<?php
|
|
|
|
namespace App\Support\Miniapp;
|
|
|
|
use App\Models\Activity;
|
|
use App\Models\ActivitySession;
|
|
use App\Models\Banner;
|
|
use App\Models\Course;
|
|
use App\Models\CourseMedia;
|
|
use App\Models\DictItem;
|
|
use App\Models\MiniappUser;
|
|
use App\Models\News;
|
|
use App\Support\ScheduleProgressStatus;
|
|
use Carbon\Carbon;
|
|
|
|
class MiniappPresenter
|
|
{
|
|
public static function progressStatusLabel(int $status): string
|
|
{
|
|
return match ($status) {
|
|
1 => '未开始',
|
|
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,
|
|
];
|
|
}
|
|
}
|