press-releases: Publish-Rechte, Quota-Races & Slug vereinheitlichen
- Publish/Reject/Archive und changeStatus erzwingen konsistent press-releases:publish; changeStatus schloss die Lücke, über die das Status-Modal die Prüfung umging (Fix#1/H2). - publish()/changeStatusFromAdmin sperren PM- und User-Zeile transaktional; QuotaExceededException wird in Admin-Publish und im Auto-Publish-Job abgefangen (Warnung statt 500 / bleibt in Review, Fix#3/M2). - Slug-Erzeugung nur noch über HasUniqueSlug (generateUniqueSlug + saveWithUniqueSlugRetry), redundanter Controller-Helper entfernt (M3). - API-/Legacy-Text wird vor der Persistenz sanitisiert (M6); Scheduler/Legacy-Tracking ignorieren soft-gelöschte PMs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7043ea8ad5
commit
0b35c3b04d
19 changed files with 536 additions and 91 deletions
|
|
@ -3,15 +3,16 @@
|
|||
namespace App\Http\Controllers\Api\Legacy;
|
||||
|
||||
use App\Enums\AiTextDeclaration;
|
||||
use App\Enums\PressReleaseEventType;
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AppSetting;
|
||||
use App\Models\Category;
|
||||
use App\Models\Company;
|
||||
use App\Models\PressRelease;
|
||||
use App\Scopes\PortalScope;
|
||||
use App\Services\PressRelease\BlacklistViolationException;
|
||||
use App\Services\PressRelease\BookingRequiredException;
|
||||
use App\Services\PressRelease\PressReleaseHtmlSanitizer;
|
||||
use App\Services\PressRelease\PressReleaseService;
|
||||
use App\Services\PressRelease\QuotaExceededException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
|
@ -19,6 +20,8 @@ use Illuminate\Http\Request;
|
|||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
use function Illuminate\Support\defer;
|
||||
|
||||
/**
|
||||
* Alt-Vertrag POST/GET /service/pressreleases (Paritäts-Doku §1/§4):
|
||||
*
|
||||
|
|
@ -37,8 +40,13 @@ class LegacyPressReleaseController extends Controller
|
|||
|
||||
public const SETTING_AI_REVIEW = 'legacy_api_ai_review';
|
||||
|
||||
public function store(Request $request, PressReleaseService $service): JsonResponse
|
||||
{
|
||||
public function store(
|
||||
Request $request,
|
||||
PressReleaseService $service,
|
||||
PressReleaseHtmlSanitizer $sanitizer,
|
||||
): JsonResponse {
|
||||
abort_unless($request->user()->tokenCan('press-releases:write'), 403);
|
||||
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'press_release' => ['required', 'array'],
|
||||
|
|
@ -77,13 +85,16 @@ class LegacyPressReleaseController extends Controller
|
|||
}
|
||||
|
||||
$language = strtolower(substr((string) ($validated['language'] ?? 'de'), 0, 2)) ?: 'de';
|
||||
$text = $sanitizer->isHtml($validated['text'])
|
||||
? $sanitizer->clean($validated['text'])
|
||||
: $validated['text'];
|
||||
|
||||
$pressRelease = new PressRelease([
|
||||
'company_id' => $company->id,
|
||||
'category_id' => $category->id,
|
||||
'language' => $language,
|
||||
'title' => $validated['title'],
|
||||
'text' => $validated['text'],
|
||||
'text' => $text,
|
||||
'backlink_url' => $validated['backlink_url'] ?? null,
|
||||
'keywords' => $validated['keywords'] ?? null,
|
||||
'status' => PressReleaseStatus::Draft->value,
|
||||
|
|
@ -99,7 +110,10 @@ class LegacyPressReleaseController extends Controller
|
|||
'portal' => $pressRelease->portal,
|
||||
'language' => $language,
|
||||
]);
|
||||
$pressRelease->save();
|
||||
$pressRelease->saveWithUniqueSlugRetry($pressRelease->title, [
|
||||
'portal' => $pressRelease->portal,
|
||||
'language' => $language,
|
||||
]);
|
||||
|
||||
if (! empty($validated['contacts_list'])) {
|
||||
$ownContactIds = $company->contacts()->whereIn('contacts.id', $validated['contacts_list'])->pluck('contacts.id');
|
||||
|
|
@ -137,7 +151,7 @@ class LegacyPressReleaseController extends Controller
|
|||
|
||||
public function show(Request $request, int $pressRelease): JsonResponse
|
||||
{
|
||||
$pressRelease = PressRelease::withoutGlobalScopes()
|
||||
$pressRelease = PressRelease::withoutGlobalScope(PortalScope::class)
|
||||
->whereKey($pressRelease)
|
||||
->with(['company'])
|
||||
->first();
|
||||
|
|
@ -150,11 +164,10 @@ class LegacyPressReleaseController extends Controller
|
|||
return $this->legacyError(404, ['Not found.']);
|
||||
}
|
||||
|
||||
// Hit-Zählung wie im Altsystem (dort auch für API-Abrufe).
|
||||
PressRelease::withoutGlobalScopes()
|
||||
->whereKey($pressRelease->getKey())
|
||||
->update(['hits' => ($pressRelease->hits ?? 0) + 1]);
|
||||
$pressRelease->events()->create(['type' => PressReleaseEventType::View]);
|
||||
// Hit-Zählung wie im Altsystem (dort auch für API-Abrufe), aber erst
|
||||
// nach der Antwort und atomar, damit parallele Polls keine Hits verlieren.
|
||||
$pressReleaseId = $pressRelease->getKey();
|
||||
defer(static fn () => PressRelease::recordDeferredView($pressReleaseId));
|
||||
|
||||
$spokeUrl = $this->spokeBaseUrl($pressRelease->portal?->value);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue