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

<?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,
];
}
}