User Panel: Phase-8-Abschluss, Titelbild/Lizenzen/Zeitzonen und KI-Pruef-Pipeline
Phase 8 (Rest) + Umbauten vom 10./11.06.: - Ein Titelbild pro PM (Cover 1280x580), SVG-Platzhalter-Set + Picker, PressReleaseCoverImage-Resolver - Lizenz-/Rechteformular nach "Lizenztyp Bildupload" (7 Lizenztypen, Personen-/Sachrechte-Status, bedingte Pflichtfelder, Risikohinweise) - Veroeffentlichungs-Box vereinfacht (Embargo aus der Form-UI entfernt), geplante Termine in Europe/Berlin (Speicherung UTC, DISPLAY_TIMEZONE) - Quota-Stub (users.press_release_quota) + monatlicher Reset-Command - Einreichungs-Modal einheitlich in Show/Create/Edit; Ghost-Buttons auf filled; PM-Editor-Layout responsive entkoppelt (.pr-editor-layout) KI-Pruef-Pipeline (Phasen 1-5 des Entwicklungsplans): - API-Haertung: status nicht mehr per API setzbar, eigene Submit-Route durch denselben Funnel (Blacklist, Quota, Status-Log) - Klassifikation Rot/Gelb/Gruen asynchron (Queue classification, OpenAI-Treiber + deterministischer Fallback), ki_audits-Audit-Log - Routing: Rot -> rejected + Mail, Gelb -> Review-Queue, Gruen -> Auto-Publish; Scheduler publiziert nur gruene faellige PMs - Content-Score 0-100 -> Stufe (Standard/Geprueft/Hochwertig) inkl. Editor-Panel und Badges; Re-Klassifikation/-Score bei Aenderung - Admin: KI-Badge + Filter, On-Demand-Pruefung mit Anbieter-Override Suite: 442 passed, 4 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0efabaf446
commit
a000238ca8
141 changed files with 5922 additions and 1001 deletions
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
namespace App\Services\PressRelease;
|
||||
|
||||
use App\Enums\PressReleaseClassification;
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Jobs\ClassifyPressRelease;
|
||||
use App\Jobs\ScorePressRelease;
|
||||
use App\Mail\PressReleasePublished;
|
||||
use App\Mail\PressReleaseRejected;
|
||||
use App\Models\AdminPreset;
|
||||
|
|
@ -43,9 +46,101 @@ class PressReleaseService
|
|||
|
||||
$pressRelease->update(['status' => PressReleaseStatus::Review->value]);
|
||||
$this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Review, null, 'customer');
|
||||
|
||||
// Quota-Stub: zählt den Monatsverbrauch des Autors hoch. Wird vom
|
||||
// echten Tarif-Modul später abgelöst (Schnittstelle bleibt stabil).
|
||||
$pressRelease->user?->increment('press_release_quota_used_this_month');
|
||||
|
||||
// KI-Klassifikation asynchron anstoßen (Red Flag). Das Routing anhand
|
||||
// des Ergebnisses übernimmt der Job über routeByClassification().
|
||||
ClassifyPressRelease::dispatch($pressRelease->id)->onQueue('classification');
|
||||
|
||||
// Content-Score parallel berechnen (Qualität, ohne Statuswirkung).
|
||||
ScorePressRelease::dispatch($pressRelease->id)->onQueue('classification');
|
||||
}
|
||||
|
||||
public function publish(PressRelease $pressRelease, string $source = 'admin'): void
|
||||
/**
|
||||
* Stößt eine erneute KI-Klassifikation an, wenn die PM bereits einmal
|
||||
* klassifiziert wurde (Konzept §15.1: „Bei Änderung wird neu klassifiziert").
|
||||
*
|
||||
* Läuft als Re-Check **ohne Routing**: Bewertung + Audit werden
|
||||
* aktualisiert, der Status bleibt unverändert. So führt das bloße
|
||||
* Bearbeiten nie zu einer überraschenden automatischen Veröffentlichung
|
||||
* oder Ablehnung – die Entscheidung bleibt beim regulären Workflow/Admin.
|
||||
*/
|
||||
public function reclassifyIfClassified(PressRelease $pressRelease): void
|
||||
{
|
||||
if ($pressRelease->classification === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClassifyPressRelease::dispatch($pressRelease->id, route: false)->onQueue('classification');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stößt eine erneute Content-Score-Berechnung an, wenn die PM bereits
|
||||
* einmal bewertet wurde (Konzept §15.2: „bei jeder Änderung neu berechnet").
|
||||
*/
|
||||
public function rescoreIfScored(PressRelease $pressRelease): void
|
||||
{
|
||||
if ($pressRelease->content_score === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScorePressRelease::dispatch($pressRelease->id)->onQueue('classification');
|
||||
}
|
||||
|
||||
/**
|
||||
* Routet eine frisch klassifizierte PM anhand des KI-Ergebnisses
|
||||
* (Konzept §15.1). Wird vom ClassifyPressRelease-Job aufgerufen.
|
||||
*
|
||||
* - Rot → Ablehnung mit Begründung an den Autor
|
||||
* - Gelb → bleibt in der manuellen Review-Queue
|
||||
* - Grün → automatische Veröffentlichung (sofort bzw. zum geplanten Termin)
|
||||
*
|
||||
* Greift nur, solange die PM noch im Status `review` steht; manuelle
|
||||
* Admin-Eingriffe in der Zwischenzeit haben damit Vorrang.
|
||||
*/
|
||||
public function routeByClassification(PressRelease $pressRelease, PressReleaseClassification $classification, ?string $reason = null): void
|
||||
{
|
||||
if ($pressRelease->status !== PressReleaseStatus::Review) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($classification === PressReleaseClassification::Red) {
|
||||
$this->reject($pressRelease, $reason ?: 'Automatische Ablehnung durch die KI-Prüfung.', 'ki');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($classification === PressReleaseClassification::Green) {
|
||||
$this->autoPublishGreen($pressRelease);
|
||||
}
|
||||
|
||||
// Gelb: keine Aktion – bleibt zur manuellen Prüfung im Status „review".
|
||||
}
|
||||
|
||||
/**
|
||||
* Veröffentlicht eine grün klassifizierte PM automatisch.
|
||||
*
|
||||
* Liegt ein Veröffentlichungstermin in der Zukunft, übernimmt der
|
||||
* Scheduler die Publikation zum Termin. Andernfalls wird sofort
|
||||
* publiziert – optional mit einem Sicherheitsfenster
|
||||
* (scoring.classification.green_delay_minutes).
|
||||
*/
|
||||
private function autoPublishGreen(PressRelease $pressRelease): void
|
||||
{
|
||||
if ($pressRelease->scheduled_at && $pressRelease->scheduled_at->isFuture()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$delayMinutes = (int) config('scoring.classification.green_delay_minutes', 0);
|
||||
$publishedAtOverride = $delayMinutes > 0 ? now()->addMinutes($delayMinutes) : null;
|
||||
|
||||
$this->publish($pressRelease, 'ki', $publishedAtOverride);
|
||||
}
|
||||
|
||||
public function publish(PressRelease $pressRelease, string $source = 'admin', ?Carbon $publishedAtOverride = null): void
|
||||
{
|
||||
$this->assertStatus($pressRelease, [PressReleaseStatus::Review]);
|
||||
|
||||
|
|
@ -63,7 +158,7 @@ class PressReleaseService
|
|||
|
||||
$pressRelease->update([
|
||||
'status' => PressReleaseStatus::Published->value,
|
||||
'published_at' => $this->resolvePublishedAt($pressRelease),
|
||||
'published_at' => $this->resolvePublishedAt($pressRelease, $publishedAtOverride),
|
||||
]);
|
||||
|
||||
$this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Published, null, $source);
|
||||
|
|
@ -83,14 +178,18 @@ class PressReleaseService
|
|||
* Damit wirken sowohl Scheduling als auch Embargo automatisch über den
|
||||
* vorhandenen Sichtbarkeits-Filter `where(published_at <= now())` im
|
||||
* öffentlichen Listing.
|
||||
*
|
||||
* `$override` setzt einen abweichenden Sofort-Zeitpunkt (z.B. das
|
||||
* Grün-Sicherheitsfenster) und wirkt nur, wenn kein `scheduled_at` gesetzt
|
||||
* ist – ein geplanter Termin hat stets Vorrang.
|
||||
*/
|
||||
private function resolvePublishedAt(PressRelease $pressRelease): Carbon
|
||||
private function resolvePublishedAt(PressRelease $pressRelease, ?Carbon $override = null): Carbon
|
||||
{
|
||||
if ($pressRelease->published_at) {
|
||||
return $pressRelease->published_at;
|
||||
}
|
||||
|
||||
$base = $pressRelease->scheduled_at ?: now();
|
||||
$base = $pressRelease->scheduled_at ?: ($override ?? now());
|
||||
|
||||
if ($pressRelease->embargo_at && $pressRelease->embargo_at->greaterThan($base)) {
|
||||
return $pressRelease->embargo_at;
|
||||
|
|
@ -99,7 +198,7 @@ class PressReleaseService
|
|||
return $base;
|
||||
}
|
||||
|
||||
public function reject(PressRelease $pressRelease, ?string $reason = null): void
|
||||
public function reject(PressRelease $pressRelease, ?string $reason = null, string $source = 'admin'): void
|
||||
{
|
||||
$this->assertStatus($pressRelease, [PressReleaseStatus::Review]);
|
||||
|
||||
|
|
@ -107,7 +206,7 @@ class PressReleaseService
|
|||
|
||||
$pressRelease->update(['status' => PressReleaseStatus::Rejected->value]);
|
||||
|
||||
$this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Rejected, $reason, 'admin');
|
||||
$this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Rejected, $reason, $source);
|
||||
$this->notifyAuthor($pressRelease, 'rejected', $reason);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue