feat(cms): expose public document api and trigger site revalidation

This commit is contained in:
2026-02-11 09:00:11 +08:00
parent b6e18a83ec
commit 4e7ef92d0b
10 changed files with 489 additions and 50 deletions

View File

@@ -0,0 +1,89 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\PublicDocumentCollectionResource;
use App\Http\Resources\PublicDocumentResource;
use App\Models\Document;
use Illuminate\Http\Request;
class PublicDocumentController extends Controller
{
/**
* List public documents for external site consumption.
*/
public function index(Request $request)
{
$query = Document::query()
->with(['category', 'currentVersion.uploadedBy'])
->where('status', 'active')
->where('access_level', 'public')
->whereNotNull('current_version_id')
->orderByDesc('updated_at');
if ($request->filled('search')) {
$search = trim((string) $request->input('search'));
$query->where(function ($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%")
->orWhere('document_number', 'like', "%{$search}%");
});
}
if ($request->filled('category')) {
$category = (string) $request->input('category');
if (ctype_digit($category)) {
$query->where('document_category_id', (int) $category);
} else {
$query->whereHas('category', function ($q) use ($category) {
$q->where('slug', $category);
});
}
}
$perPage = min(max($request->integer('per_page', 100), 1), 500);
return PublicDocumentCollectionResource::collection(
$query->paginate($perPage)->withQueryString()
);
}
/**
* Show a single public document with version history.
*/
public function show(string $uuid)
{
$document = Document::query()
->with([
'category',
'currentVersion.uploadedBy',
'versions.uploadedBy',
'versions.document',
'createdBy',
'lastUpdatedBy',
])
->where('public_uuid', $uuid)
->where('status', 'active')
->where('access_level', 'public')
->whereNotNull('current_version_id')
->firstOrFail();
$related = Document::query()
->with(['category', 'currentVersion.uploadedBy'])
->where('status', 'active')
->where('access_level', 'public')
->whereNotNull('current_version_id')
->where('id', '!=', $document->id)
->where('document_category_id', $document->document_category_id)
->orderByDesc('updated_at')
->limit(4)
->get();
return response()->json([
'data' => new PublicDocumentResource($document),
'related' => PublicDocumentCollectionResource::collection($related),
]);
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PublicDocumentCollectionResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'slug' => $this->public_uuid,
'public_uuid' => $this->public_uuid,
'title' => $this->title,
'document_number' => $this->document_number,
'summary' => $this->description,
'description' => $this->description,
'status' => $this->status,
'status_label' => $this->getStatusLabel(),
'access_level' => $this->access_level,
'access_level_label' => $this->getAccessLevelLabel(),
'published_at' => $this->created_at?->toIso8601String(),
'updated_at' => $this->updated_at?->toIso8601String(),
'expires_at' => $this->expires_at?->toDateString(),
'version_count' => $this->version_count,
'category' => $this->whenLoaded('category', function () {
return [
'id' => $this->category->id,
'name' => $this->category->name,
'slug' => $this->category->slug,
'icon' => $this->category->icon,
];
}),
'current_version' => $this->whenLoaded('currentVersion', function () {
if (! $this->currentVersion) {
return null;
}
return [
'id' => $this->currentVersion->id,
'version_number' => $this->currentVersion->version_number,
'version_notes' => $this->currentVersion->version_notes,
'is_current' => (bool) $this->currentVersion->is_current,
'original_filename' => $this->currentVersion->original_filename,
'mime_type' => $this->currentVersion->mime_type,
'file_extension' => $this->currentVersion->getFileExtension(),
'file_size' => $this->currentVersion->file_size,
'file_size_human' => $this->currentVersion->getFileSizeHuman(),
'file_hash' => $this->currentVersion->file_hash,
'uploaded_by' => $this->currentVersion->uploadedBy?->name,
'uploaded_at' => $this->currentVersion->uploaded_at?->toIso8601String(),
'download_url' => route('documents.public.download', $this->public_uuid),
];
}),
'links' => [
'api_url' => url('/api/v1/public-documents/'.$this->public_uuid),
'detail_url' => route('documents.public.show', $this->public_uuid),
'web_url' => route('documents.public.show', $this->public_uuid),
'download_url' => route('documents.public.download', $this->public_uuid),
],
'metadata' => [
'document_type' => $this->category?->name,
'expiration_status' => $this->getExpirationStatusLabel(),
'auto_archive_on_expiry' => (bool) $this->auto_archive_on_expiry,
'expiry_notice' => $this->expiry_notice,
],
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PublicDocumentResource extends JsonResource
{
public function toArray(Request $request): array
{
return array_merge(
(new PublicDocumentCollectionResource($this))->toArray($request),
[
'versions' => PublicDocumentVersionResource::collection($this->whenLoaded('versions')),
'audit' => [
'view_count' => $this->view_count,
'download_count' => $this->download_count,
'last_updated_by' => $this->lastUpdatedBy?->name,
'created_by' => $this->createdBy?->name,
],
]
);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PublicDocumentVersionResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'version_number' => $this->version_number,
'version_notes' => $this->version_notes,
'is_current' => (bool) $this->is_current,
'original_filename' => $this->original_filename,
'mime_type' => $this->mime_type,
'file_extension' => $this->getFileExtension(),
'file_size' => $this->file_size,
'file_size_human' => $this->getFileSizeHuman(),
'file_hash' => $this->file_hash,
'uploaded_by' => $this->uploadedBy?->name,
'uploaded_at' => $this->uploaded_at?->toIso8601String(),
'download_url' => route('documents.public.download-version', [
'uuid' => $this->document->public_uuid,
'version' => $this->id,
]),
];
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Observers;
use App\Models\Document;
use App\Services\SiteRevalidationService;
class DocumentObserver
{
private const RELEVANT_FIELDS = [
'title',
'document_number',
'description',
'document_category_id',
'access_level',
'status',
'archived_at',
'current_version_id',
'version_count',
'expires_at',
'auto_archive_on_expiry',
'expiry_notice',
'deleted_at',
];
public function created(Document $document): void
{
$this->revalidate($document);
}
public function updated(Document $document): void
{
if (! $document->wasChanged(self::RELEVANT_FIELDS)) {
return;
}
$this->revalidate($document);
}
public function deleted(Document $document): void
{
$this->revalidate($document);
}
public function restored(Document $document): void
{
$this->revalidate($document);
}
private function revalidate(Document $document): void
{
SiteRevalidationService::revalidateDocument($document->public_uuid);
}
}

View File

@@ -2,6 +2,8 @@
namespace App\Providers;
use App\Models\Document;
use App\Observers\DocumentObserver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@@ -19,6 +21,6 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
//
Document::observe(DocumentObserver::class);
}
}

View File

@@ -23,8 +23,20 @@ class SiteRevalidationService
static::revalidate('page', $slug);
}
/**
* Revalidate document cache on the Next.js frontend.
*/
public static function revalidateDocument(?string $slug = null): void
{
static::revalidate('document', $slug);
}
private static function revalidate(string $type, ?string $slug = null): void
{
if (app()->runningUnitTests()) {
return;
}
$url = config('services.nextjs.revalidate_url');
$token = config('services.nextjs.revalidate_token');