optimizations

This commit is contained in:
2026-03-28 19:15:39 +01:00
parent 0b25d9570a
commit cab4fbd83e
509 changed files with 1016804 additions and 1605 deletions

View File

@@ -0,0 +1,360 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Artwork;
use App\Models\Collection;
use App\Models\CollectionQualitySnapshot;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class CollectionHealthService
{
public function evaluate(Collection $collection): array
{
$metadataCompleteness = $this->metadataCompletenessScore($collection);
$freshness = $this->freshnessScore($collection);
$engagement = $this->engagementScore($collection);
$readiness = $this->editorialReadinessScore($collection, $metadataCompleteness, $freshness, $engagement);
$flags = $this->flags($collection, $metadataCompleteness, $freshness, $engagement, $readiness);
$healthState = $this->healthStateFromFlags($flags);
$healthScore = $this->healthScore($metadataCompleteness, $freshness, $engagement, $readiness, $flags);
$placementEligibility = $this->placementEligibility($collection, $healthState, $readiness);
return [
'metadata_completeness_score' => $metadataCompleteness,
'freshness_score' => $freshness,
'engagement_score' => $engagement,
'editorial_readiness_score' => $readiness,
'health_score' => $healthScore,
'health_state' => $healthState,
'health_flags_json' => $flags,
'readiness_state' => $this->readinessState($placementEligibility, $flags),
'placement_eligibility' => $placementEligibility,
'duplicate_cluster_key' => $this->duplicateClusterKey($collection),
'trust_tier' => $this->trustTier($collection, $healthScore),
'last_health_check_at' => now(),
];
}
public function refresh(Collection $collection, ?User $actor = null, string $reason = 'refresh'): Collection
{
$payload = $this->evaluate($collection->fresh());
$snapshotDate = now()->toDateString();
$collection->forceFill($payload)->save();
$snapshot = CollectionQualitySnapshot::query()
->where('collection_id', $collection->id)
->whereDate('snapshot_date', $snapshotDate)
->first();
if ($snapshot) {
$snapshot->forceFill([
'quality_score' => $collection->quality_score,
'health_score' => $payload['health_score'],
'metadata_completeness_score' => $payload['metadata_completeness_score'],
'freshness_score' => $payload['freshness_score'],
'engagement_score' => $payload['engagement_score'],
'readiness_score' => $payload['editorial_readiness_score'],
'flags_json' => $payload['health_flags_json'],
])->save();
} else {
CollectionQualitySnapshot::query()->create([
'collection_id' => $collection->id,
'snapshot_date' => $snapshotDate,
'quality_score' => $collection->quality_score,
'health_score' => $payload['health_score'],
'metadata_completeness_score' => $payload['metadata_completeness_score'],
'freshness_score' => $payload['freshness_score'],
'engagement_score' => $payload['engagement_score'],
'readiness_score' => $payload['editorial_readiness_score'],
'flags_json' => $payload['health_flags_json'],
]);
}
$fresh = $collection->fresh();
app(CollectionHistoryService::class)->record(
$fresh,
$actor,
'health_refreshed',
sprintf('Collection health refreshed via %s.', $reason),
null,
[
'health_state' => $fresh->health_state,
'readiness_state' => $fresh->readiness_state,
'placement_eligibility' => (bool) $fresh->placement_eligibility,
'health_score' => (float) ($fresh->health_score ?? 0),
'flags' => $fresh->health_flags_json,
]
);
return $fresh;
}
public function summary(Collection $collection): array
{
return [
'health_state' => $collection->health_state,
'readiness_state' => $collection->readiness_state,
'health_score' => $collection->health_score !== null ? (float) $collection->health_score : null,
'metadata_completeness_score' => $collection->metadata_completeness_score !== null ? (float) $collection->metadata_completeness_score : null,
'editorial_readiness_score' => $collection->editorial_readiness_score !== null ? (float) $collection->editorial_readiness_score : null,
'freshness_score' => $collection->freshness_score !== null ? (float) $collection->freshness_score : null,
'engagement_score' => $collection->engagement_score !== null ? (float) $collection->engagement_score : null,
'placement_eligibility' => (bool) $collection->placement_eligibility,
'flags' => is_array($collection->health_flags_json) ? $collection->health_flags_json : [],
'duplicate_cluster_key' => $collection->duplicate_cluster_key,
'canonical_collection_id' => $collection->canonical_collection_id ? (int) $collection->canonical_collection_id : null,
'last_health_check_at' => optional($collection->last_health_check_at)?->toISOString(),
];
}
private function metadataCompletenessScore(Collection $collection): float
{
$score = 0.0;
$score += filled($collection->title) ? 18.0 : 0.0;
$score += filled($collection->summary) ? 18.0 : 0.0;
$score += filled($collection->description) ? 14.0 : 0.0;
$score += $this->hasStrongCover($collection) ? 18.0 : ($collection->resolvedCoverArtwork(false) ? 8.0 : 0.0);
$score += (int) $collection->artworks_count >= 6 ? 16.0 : ((int) $collection->artworks_count >= 4 ? 10.0 : ((int) $collection->artworks_count >= 2 ? 5.0 : 0.0));
$score += $collection->usesPremiumPresentation() ? 8.0 : 0.0;
$score += filled($collection->campaign_key) || filled($collection->event_key) || filled($collection->season_key) ? 8.0 : 0.0;
return round(min(100.0, $score), 2);
}
private function freshnessScore(Collection $collection): float
{
$reference = $collection->last_activity_at ?: $collection->updated_at ?: $collection->published_at;
if ($reference === null) {
return 0.0;
}
$days = max(0, now()->diffInDays($reference));
if ($days >= 45) {
return 0.0;
}
return round(max(0.0, 100.0 - (($days / 45) * 100.0)), 2);
}
private function engagementScore(Collection $collection): float
{
$weighted = ((int) $collection->likes_count * 3.0)
+ ((int) $collection->followers_count * 4.5)
+ ((int) $collection->saves_count * 4.0)
+ ((int) $collection->comments_count * 2.0)
+ ((int) $collection->shares_count * 2.5)
+ ((int) $collection->views_count * 0.08);
return round(min(100.0, $weighted), 2);
}
private function editorialReadinessScore(Collection $collection, float $metadataCompleteness, float $freshness, float $engagement): float
{
$score = ($metadataCompleteness * 0.45) + ($freshness * 0.2) + ($engagement * 0.2);
$score += $collection->moderation_status === Collection::MODERATION_ACTIVE ? 10.0 : -20.0;
$score += $collection->visibility === Collection::VISIBILITY_PUBLIC ? 10.0 : -10.0;
$score += in_array((string) $collection->workflow_state, [Collection::WORKFLOW_APPROVED, Collection::WORKFLOW_PROGRAMMED], true) ? 10.0 : 0.0;
return round(max(0.0, min(100.0, $score)), 2);
}
private function flags(Collection $collection, float $metadataCompleteness, float $freshness, float $engagement, float $readiness): array
{
$flags = [];
$artworksCount = (int) $collection->artworks_count;
if ($metadataCompleteness < 55) {
$flags[] = Collection::HEALTH_NEEDS_METADATA;
}
if ($artworksCount < 6) {
$flags[] = Collection::HEALTH_LOW_CONTENT;
}
if (! $this->hasStrongCover($collection)) {
$flags[] = Collection::HEALTH_WEAK_COVER;
}
if ($freshness <= 0.0 && $collection->isPubliclyAccessible()) {
$flags[] = Collection::HEALTH_STALE;
}
if ($engagement < 15 && $collection->isPubliclyAccessible() && $collection->published_at?->lt(now()->subDays(21))) {
$flags[] = Collection::HEALTH_LOW_ENGAGEMENT;
}
if ($collection->moderation_status !== Collection::MODERATION_ACTIVE || (string) $collection->workflow_state === Collection::WORKFLOW_IN_REVIEW) {
$flags[] = Collection::HEALTH_NEEDS_REVIEW;
}
if ($this->brokenItemsRatio($collection) > 0.25) {
$flags[] = Collection::HEALTH_BROKEN_ITEMS;
}
if ($this->hasDuplicateRisk($collection)) {
$flags[] = Collection::HEALTH_DUPLICATE_RISK;
}
if ($collection->canonical_collection_id !== null) {
$flags[] = Collection::HEALTH_MERGE_CANDIDATE;
}
if ($readiness < 45 && $collection->type === Collection::TYPE_EDITORIAL && $artworksCount < 6) {
$flags[] = Collection::HEALTH_ATTRIBUTION_INCOMPLETE;
}
return array_values(array_unique($flags));
}
private function healthStateFromFlags(array $flags): string
{
foreach ([
Collection::HEALTH_MERGE_CANDIDATE,
Collection::HEALTH_DUPLICATE_RISK,
Collection::HEALTH_NEEDS_REVIEW,
Collection::HEALTH_BROKEN_ITEMS,
Collection::HEALTH_LOW_CONTENT,
Collection::HEALTH_WEAK_COVER,
Collection::HEALTH_NEEDS_METADATA,
Collection::HEALTH_STALE,
Collection::HEALTH_LOW_ENGAGEMENT,
Collection::HEALTH_ATTRIBUTION_INCOMPLETE,
] as $flag) {
if (in_array($flag, $flags, true)) {
return $flag;
}
}
return Collection::HEALTH_HEALTHY;
}
private function healthScore(float $metadataCompleteness, float $freshness, float $engagement, float $readiness, array $flags): float
{
$score = ($metadataCompleteness * 0.35) + ($freshness * 0.2) + ($engagement * 0.2) + ($readiness * 0.25);
$score -= count($flags) * 6.5;
return round(max(0.0, min(100.0, $score)), 2);
}
private function placementEligibility(Collection $collection, string $healthState, float $readiness): bool
{
if ($collection->visibility !== Collection::VISIBILITY_PUBLIC) {
return false;
}
if ($collection->moderation_status !== Collection::MODERATION_ACTIVE) {
return false;
}
if (! in_array($collection->lifecycle_state, [Collection::LIFECYCLE_PUBLISHED, Collection::LIFECYCLE_FEATURED], true)) {
return false;
}
if (in_array($healthState, [Collection::HEALTH_BROKEN_ITEMS, Collection::HEALTH_MERGE_CANDIDATE], true)) {
return false;
}
if ($collection->workflow_state === Collection::WORKFLOW_IN_REVIEW) {
return false;
}
return $readiness >= 45.0;
}
private function readinessState(bool $placementEligibility, array $flags): string
{
if (! $placementEligibility) {
return Collection::READINESS_BLOCKED;
}
if ($flags !== []) {
return Collection::READINESS_NEEDS_WORK;
}
return Collection::READINESS_READY;
}
private function duplicateClusterKey(Collection $collection): ?string
{
$existing = trim((string) ($collection->duplicate_cluster_key ?? ''));
return $existing !== '' ? $existing : null;
}
private function trustTier(Collection $collection, float $healthScore): string
{
if ($collection->type === Collection::TYPE_EDITORIAL) {
return 'editorial';
}
if ($healthScore >= 80) {
return 'high';
}
if ($healthScore >= 50) {
return 'standard';
}
return 'limited';
}
private function brokenItemsRatio(Collection $collection): float
{
if ($collection->isSmart() || (int) $collection->artworks_count === 0) {
return 0.0;
}
$visibleCount = DB::table('collection_artwork as ca')
->join('artworks as a', 'a.id', '=', 'ca.artwork_id')
->where('ca.collection_id', $collection->id)
->whereNull('a.deleted_at')
->where('a.is_public', true)
->where('a.is_approved', true)
->whereNotNull('a.published_at')
->where('a.published_at', '<=', now())
->count();
return max(0.0, ((int) $collection->artworks_count - $visibleCount) / max(1, (int) $collection->artworks_count));
}
private function hasDuplicateRisk(Collection $collection): bool
{
return Collection::query()
->where('id', '!=', $collection->id)
->where('user_id', $collection->user_id)
->whereRaw('LOWER(title) = ?', [mb_strtolower(trim((string) $collection->title))])
->exists();
}
private function hasStrongCover(Collection $collection): bool
{
if (! $collection->cover_artwork_id) {
return false;
}
$cover = $collection->relationLoaded('coverArtwork')
? $collection->coverArtwork
: $collection->coverArtwork()->first();
if (! $cover instanceof Artwork) {
return false;
}
if ($collection->isPubliclyAccessible() && ! $cover->published_at) {
return false;
}
$width = (int) ($cover->width ?? 0);
$height = (int) ($cover->height ?? 0);
return $width >= 320 && $height >= 220;
}
}