utc(); $artwork->forceFill([ 'artwork_status' => 'published', 'publish_at' => null, 'artwork_timezone' => null, 'published_at' => $publishedAt, 'is_public' => $artwork->visibility !== Artwork::VISIBILITY_PRIVATE, ])->save(); $this->syncSearch($artwork); $this->recordActivity($artwork); return $artwork; } public function publishIfDue(Artwork $artwork, ?Carbon $now = null): Artwork { $now ??= now()->utc(); if (! $this->isDue($artwork, $now)) { return $artwork; } DB::transaction(function () use (&$artwork, $now): void { $locked = Artwork::query() ->lockForUpdate() ->find($artwork->id); if (! $locked || ! $this->isDue($locked, $now)) { if ($locked) { $artwork = $locked; } return; } $artwork = $this->publishNow($locked, $now); }); return $artwork->fresh() ?? $artwork; } public function publishDueScheduled(int $limit = 100, ?Carbon $now = null): array { $now ??= now()->utc(); $candidates = Artwork::query() ->where('artwork_status', 'scheduled') ->where('publish_at', '<=', $now) ->where('is_approved', true) ->orderBy('publish_at') ->limit($limit) ->get(['id', 'user_id', 'title', 'publish_at', 'artwork_status']); $published = collect(); foreach ($candidates as $candidate) { $result = null; DB::transaction(function () use ($candidate, $now, &$result): void { $locked = Artwork::query() ->lockForUpdate() ->where('id', $candidate->id) ->where('artwork_status', 'scheduled') ->first(); if (! $locked || ! $this->isDue($locked, $now)) { return; } $result = $this->publishNow($locked, $now); }); if ($result instanceof Artwork) { $published->push($result); } } return [ 'candidates' => $candidates, 'published' => $published, ]; } public function publishDueScheduledForUser(int $userId, int $limit = 100, ?Carbon $now = null): void { $now ??= now()->utc(); $candidateIds = Artwork::query() ->where('user_id', $userId) ->where('artwork_status', 'scheduled') ->where('publish_at', '<=', $now) ->where('is_approved', true) ->orderBy('publish_at') ->limit($limit) ->pluck('id'); foreach ($candidateIds as $candidateId) { $artwork = Artwork::query()->find((int) $candidateId); if ($artwork) { $this->publishIfDue($artwork, $now); } } } private function isDue(Artwork $artwork, Carbon $now): bool { return $artwork->artwork_status === 'scheduled' && $artwork->is_approved && $artwork->publish_at !== null && $artwork->publish_at->lte($now); } private function syncSearch(Artwork $artwork): void { if (! method_exists($artwork, 'searchable')) { return; } try { $artwork->searchable(); } catch (\Throwable $e) { Log::warning("PublishScheduled: scout reindex failed for #{$artwork->id}: {$e->getMessage()}"); } } private function recordActivity(Artwork $artwork): void { try { ActivityEvent::record( actorId: (int) $artwork->user_id, type: ActivityEvent::TYPE_UPLOAD, targetType: ActivityEvent::TARGET_ARTWORK, targetId: (int) $artwork->id, ); } catch (\Throwable) { } try { app(UserActivityService::class)->logUpload((int) $artwork->user_id, (int) $artwork->id); } catch (\Throwable) { } } }