176 lines
5.0 KiB
PHP
176 lines
5.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Artworks;
|
|
|
|
use App\Models\ActivityEvent;
|
|
use App\Models\Artwork;
|
|
use App\Jobs\IndexArtworkJob;
|
|
use App\Services\Activity\UserActivityService;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class ArtworkPublicationService
|
|
{
|
|
public function publishNow(Artwork $artwork, ?Carbon $publishedAt = null): Artwork
|
|
{
|
|
$publishedAt ??= now()->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
|
|
{
|
|
$artworkId = (int) $artwork->id;
|
|
|
|
$sync = function () use ($artworkId): void {
|
|
try {
|
|
IndexArtworkJob::dispatchSync($artworkId);
|
|
} catch (\Throwable $exception) {
|
|
Log::error('ArtworkPublicationService immediate Meilisearch sync failed; queueing fallback job.', [
|
|
'artwork_id' => $artworkId,
|
|
'error' => $exception->getMessage(),
|
|
]);
|
|
|
|
IndexArtworkJob::dispatch($artworkId);
|
|
}
|
|
};
|
|
|
|
if (DB::transactionLevel() > 0) {
|
|
DB::afterCommit($sync);
|
|
|
|
return;
|
|
}
|
|
|
|
$sync();
|
|
}
|
|
|
|
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) {
|
|
}
|
|
}
|
|
} |