Allow artworks to have one primary category and extra secondaries.

Keep artwork_category as membership, store a deterministic primary_category_id, and let upload plus studio pick up to five additional same-type categories without changing canonical URLs.
This commit is contained in:
test
2026-09-20 14:50:12 +02:00
parent 4d60a876af
commit 232880e01f
43 changed files with 2896 additions and 565 deletions
@@ -201,7 +201,7 @@ final class ArtworkStudioProvider implements CreatorStudioProvider
? 'scheduled'
: ((bool) $artwork->is_public ? 'published' : 'draft'));
$category = $artwork->categories->first();
$category = $artwork->resolvedPrimaryCategory();
$visibility = $artwork->visibility ?: ((bool) $artwork->is_public ? Artwork::VISIBILITY_PUBLIC : Artwork::VISIBILITY_PRIVATE);
return [
+18 -3
View File
@@ -211,9 +211,13 @@ final class StudioAiAssistService
}
$categoryId = $this->resolveCategoryId($payload);
$secondaryCategoryIds = array_key_exists('secondary_category_ids', $payload)
? array_values(array_map('intval', (array) $payload['secondary_category_ids']))
: null;
if ($categoryId !== null) {
$artwork->categories()->sync([$categoryId]);
app(\App\Services\Artworks\ArtworkCategoryService::class)
->sync($artwork, $categoryId, $secondaryCategoryIds, 'ai_applied', false);
$artwork->category_source = 'ai_applied';
$updated = true;
$applied[] = 'category';
@@ -294,10 +298,10 @@ final class StudioAiAssistService
*/
public function payloadFor(Artwork $artwork): array
{
$artwork->loadMissing(['artworkAiAssist', 'tags', 'categories.contentType']);
$artwork->loadMissing(['artworkAiAssist', 'tags', 'categories.contentType', 'primaryCategory.contentType']);
$assist = $artwork->artworkAiAssist;
$primaryCategory = $artwork->categories->first();
$primaryCategory = $artwork->resolvedPrimaryCategory();
if (! $assist) {
return [
@@ -515,6 +519,13 @@ final class StudioAiAssistService
'description' => (string) ($artwork->description ?? ''),
'tags' => $artwork->tags->pluck('slug')->values()->all(),
'category_id' => $primaryCategory?->id,
'primary_category_id' => $primaryCategory?->id,
'secondary_category_ids' => $artwork->categories
->reject(fn ($category): bool => (int) $category->id === (int) ($primaryCategory?->id ?? 0))
->pluck('id')
->map(fn ($id): int => (int) $id)
->values()
->all(),
'content_type_id' => $primaryCategory?->contentType?->id,
'sources' => [
'title' => $artwork->title_source ?: 'manual',
@@ -530,6 +541,10 @@ final class StudioAiAssistService
*/
private function resolveCategoryId(array $payload): ?int
{
if (isset($payload['primary_category_id']) && $payload['primary_category_id'] !== null) {
return (int) $payload['primary_category_id'];
}
if (isset($payload['category_id']) && $payload['category_id'] !== null) {
return (int) $payload['category_id'];
}
@@ -49,8 +49,9 @@ final class StudioAiCategoryMapper
$selectedCategory,
$this->confidenceForModel($categoryScores, $selectedCategory->id),
$categoryScores
->reject(fn (array $row): bool => (int) $row['model']->id === (int) $selectedCategory->id)
->take(3)
->reject(fn (array $row): bool => (int) $row['model']->id === (int) $selectedCategory->id
|| (int) $row['model']->content_type_id !== (int) $selectedCategory->content_type_id)
->take(max(0, (int) config('categories.max_secondary_categories', 5)))
->map(fn (array $row): array => $this->serializeCategory($row['model'], $row['confidence']))
->all()
) : null,
@@ -245,6 +246,7 @@ final class StudioAiCategoryMapper
'root_category_id' => (int) $rootCategory->id,
'sub_category_id' => $category->parent_id ? (int) $category->id : null,
'alternatives' => array_values($alternatives),
'secondary_categories' => array_values($alternatives),
];
}
}
@@ -117,7 +117,8 @@ final class StudioBulkActionService
throw new \InvalidArgumentException('category_id required for change_category');
}
$artwork->categories()->sync([(int) $params['category_id']]);
app(\App\Services\Artworks\ArtworkCategoryService::class)
->sync($artwork, (int) $params['category_id'], [], 'moderator');
}
private function addTags(Artwork $artwork, array $params): void