# دستورالعمل اجرای مرحله چهارم: نسخه حداقلی مدیا

وضعیت: مبنای طراحی مرحله چهارم؛ پیاده‌سازی backend در نوبت بعد از تدوین سند انجام شده است. قراردادهای واقعی API، پیش‌نیازهای نصب و وضعیت بررسی‌ها در [راهنمای وب‌سرویس مدیا](MEDIA_API.fa.md) آمده‌اند. قسمت‌های پیشنهادی این سند، تاریخچه تصمیم طراحی‌اند؛ تنظیمات قابل تغییر در کد و راهنمای API مشخص‌اند.

تاریخ تدوین: ۲۰۲۶-۱۱-۰۲.

مبنای بررسی: کد فعلی `mahamyar_crm_backend`، تصمیم‌های تأییدشده کاربر، [مرز داده‌ها](DATA_BOUNDARIES.fa.md) و [قرارداد دیتابیس‌های عملیاتی](OPERATIONAL_DATABASES.fa.md). در این نوبت فقط مستند نوشته شده است؛ تست، مهاجرت و عملیات ذخیره‌سازی اجرا نشده‌اند.

## ۱. نتیجه بررسی و مرز نسخه اول

فهرست پیشنهادی مرحله چهارم مناسب است، با این تکمیل‌ها:

- Media سرویس مرکزی مشترک است؛ جدول‌ها و مدل‌های آن صریحاً از اتصال `central` استفاده می‌کنند. تعلق فایل به کسب‌وکار، آن را به جدول عملیاتی تبدیل نمی‌کند.
- فایل عمومی نیز روی دیسک خصوصی ذخیره می‌شود. عمومی‌بودن، اجازه تحویل از وب‌سرویس است و به معنی دسترسی مستقیم به پوشه نیست.
- برای جلوگیری از حذف فایل در حال استفاده، علاوه بر `media_files` یک جدول کوچک `media_attachments` لازم است. برای هر مصرف‌کننده جدول اختصاصی در Media ساخته نمی‌شود.
- لوگو نخستین مصرف‌کننده واقعی است. قرارداد عمومی مصرف‌کننده برای ماژول‌های آینده ساخته می‌شود؛ CRM، فروش و حسابداری در این مرحله پیاده نمی‌شوند.
- مسیر کسب‌وکار و مسیر مدیر پلتفرم، زمینه هویت و مجوز جدا دارند. مسیر مدیر پلتفرم نباید با نشست کسب‌وکار باز شود.
- دریافت و پخش خصوصی در مرورگر نیاز به قرارداد با Next.js دارد؛ URL خصوصی به‌تنهایی مجوز نیست.
- تبدیل ویدئو، تولید چند کیفیت، استخراج ZIP، بندانگشتی، آپلود چندتکه، اتصال مستقیم مرورگر به فضای ابری و کتابخانه مدیریت رسانه جدید جزو این نسخه نیستند.

پیشنهاد حداقلی برای جلوگیری از پیچیدگی و عمومی‌شدن ناخواسته: هر فایل فقط یک اتصال فعال یا رزرو هم‌زمان داشته باشد. برای دو مصرف مستقل، دو فایل ثبت شود؛ تکثیر مرجع و اشتراک یک باینری بین چند مصرف‌کننده به توسعه بعدی موکول شود. این محدودیت باید در API و مستندات روشن باشد.

## ۲. نقاط اتصال به کد موجود

| بخش موجود | تغییر لازم هنگام اجرا |
|---|---|
| `Modules/*/app/Providers` و `Modules/*/routes/api.php` | ساخت Media مطابق همان قرارداد ماژول‌ها؛ ثبت provider، route و مسیر مهاجرت مرکزی |
| `config/filesystems.php` | دیسک مستقل `media` با ریشه `storage/app/private/media`، دسترسی خصوصی، `serve=false` و خطای صریح عملیات ناموفق |
| `PublicBusinessController::show` | دریافت نشانی لوگو از قرارداد عمومی Media؛ حذف ساخت مسیر فیزیکی در کنترلر |
| `PublicBusinessController::logo` و مسیر `business.public-logo` | جایگزینی با مسیر عمومی Media پس از آماده‌شدن مسیر جدید و تعیین تکلیف فایل‌های موجود |
| `businesses.logo_asset_uid` | حفظ فیلد فعلی و همگام‌کردن آن با اتصال لوگو در تراکنش مرکزی |
| `UseBusinessContext` | استفاده از حالت `platform` به معنای «خدمت مرکزی برای کسب‌وکار»؛ این حالت، زمینه مدیر پلتفرم نیست |
| `AccessChecker` و `InfrastructurePermissionSeeder` | ثبت مجوزهای مشخص و بررسی فعال‌بودن هویت، عضویت و دامنه نقش |
| `foundation.platform_service_permissions` | ثبت دقیق مجوز خدمات مرکزی موردنیاز؛ این مسیر در کد فعلی مجوز با `module_key=core` انتظار دارد |
| `database/foundation-manifest.json` و `database/schema_comments_fa.php` | افزودن مهاجرت‌ها و توضیحات جاری مطابق سازوکار موجود؛ snapshot تاریخی کامنت‌ها تغییر نکند |
| `bootstrap/app.php` | نگاشت خطاهای مدیا به قالب یکسان API؛ کدهایی مثل پرشدن سهمیه در پاسخ عمومی از بین نروند |

در کد فعلی، مسیر لوگو روی دیسک `public` است و APIهای نشست موجود کسب‌وکاری‌اند. این دو مورد باید در پیاده‌سازی صریحاً تغییر کنند، نه اینکه آماده فرض شوند.

## ۳. ساختار پیشنهادی ماژول

ساختار با مولد موجود `nwidart/laravel-modules` ایجاد و با ماژول‌های فعلی تطبیق داده شود:

```text
Modules/Media/
  app/
    Actions/             UploadFile, ReadFile, DeleteFile, ReserveAttachment,
                         ConfirmAttachment, ReleaseAttachment, CleanupFiles
    Models/              MediaFile, MediaAttachment
    PublicContracts/     MediaStorage, MediaAttachments, MediaConsumer
    Http/Controllers/    مسیرهای کسب‌وکار، پلتفرم و عمومی
    Http/Requests/       اعتبارسنجی ورودی
    Providers/           MediaServiceProvider
  config/config.php
  database/migrations/
  routes/api.php
  tests/
```

نام کلاس‌ها راهنمای مسئولیت است؛ برای هر متد، لایه Repository یا interface جدید ساخته نشود. `MediaStorage` قرارداد مصرف دیگر ماژول‌هاست، نه بازنویسی adapterهای Laravel. کار با فایل از `Storage::disk($storedDisk)` انجام شود. جزئیات HTTP مربوط به نمایش فایل در یک بخش داخلی محدود بماند تا جایگزینی محل ذخیره‌سازی، کد مصرف‌کنندگان را عوض نکند.

## ۴. جداول مرکزی و محدودیت‌های داده

### `media_files`

| ستون | مفهوم و قاعده |
|---|---|
| `id` | شناسه داخلی bigint identity |
| `uid` | UUID عمومی، یکتا و غیرقابل تغییر |
| `scope_type` | `PLATFORM` یا `BUSINESS`؛ تعیین‌شده توسط سرور |
| `business_id` | برای BUSINESS اجباری و برای PLATFORM تهی؛ FK مرکزی به businesses |
| `uploaded_by_user_id` | هویت آپلودکننده؛ FK مرکزی به users |
| `uploaded_by_membership_id` | عضویت عامل برای ممیزی؛ در آپلود مدیر برای کسب‌وکار می‌تواند عضویت PLATFORM باشد |
| `purpose` | کاربرد ثبت‌شده، در شروع `attachment` و `business_logo`؛ انتخاب فقط از فهرست مجاز |
| `disk` | نام دیسک معتبر سمت سرور؛ فعلاً `media` |
| `storage_key` | کلید نسبی تصادفی؛ مستقل از نام ارسالی کاربر |
| `original_name` | نام پالایش‌شده صرفاً برای نمایش و دانلود؛ طول محدود |
| `mime_type` | نوع تشخیص‌داده‌شده سمت سرور |
| `extension` | پسوند استاندارد تأییدشده |
| `size_bytes` | حجم رزروشده هنگام UPLOADING و حجم واقعی تأییدشده هنگام READY؛ مثبت |
| `status` | `UPLOADING`، `READY`، `DELETING` یا `DELETED` |
| `first_attached_at` | زمان نخستین اتصال قطعی؛ پس از آزادسازی پاک نشود |
| `expires_at` | پایان نگهداری آپلود متصل‌نشده؛ مبنای حذف خودکار رزرو اتصال نیست |
| `deleted_at` | زمان تأیید حذف فیزیکی؛ فقط برای DELETED |
| `created_at`, `updated_at` | زمان‌ها با منطقه زمانی |

قواعد: یکتایی `(disk, storage_key)`؛ CHECK دامنه مالکیت، وضعیت و اندازه؛ ایندکس برای سهمیه و پاک‌سازی روی مالک/وضعیت/زمان؛ منع تغییر UID، مالکیت، دیسک و کلید از API عمومی. تطابق user و membership عامل با FK ترکیبی مناسب یا کنترل مرکزی معتبر، مطابق قیود موجود عضویت اجرا شود. تعلق عضویت آپلودکننده به کسب‌وکار فایل برای مدیر پلتفرم فرض نشود.

در نسخه اول ستون آزاد `is_public` قابل ویرایش توسط کاربر نداریم. مجازبودن تحویل عمومی از اتصال فعال به مصرف‌کننده عمومیِ ثبت‌شده، مانند لوگوی جاری کسب‌وکار، به دست می‌آید؛ این کار از باقی‌ماندن مجوز عمومی پس از تعویض لوگو جلوگیری می‌کند.

### `media_attachments`

| ستون | مفهوم و قاعده |
|---|---|
| `id`, `uid` | شناسه داخلی و UUID عمومی اتصال |
| `media_file_id` | FK به فایل؛ حذف آبشاری ممنوع |
| `consumer_type` | کلید ثابت ثبت‌شده مانند `business_logo`؛ نام کلاس یا نام جدول دلخواه پذیرفته نشود |
| `consumer_uid` | UUID موجودیت مقصد؛ همراه با مالک فایل و نوع مصرف‌کننده تفسیر شود |
| `operation_uid` | شناسه یکتای عملیات برای تکرارپذیری؛ در تلاش مجدد ثابت بماند |
| `status` | `RESERVED`، `ACTIVE` یا `RELEASED` |
| `activated_at`, `released_at` | زمان تأیید و آزادسازی، nullable با CHECK سازگاری |
| `created_at`, `updated_at` | زمان‌های دارای منطقه زمانی |

قواعد: ایندکس یکتای جزئی روی `media_file_id` برای وضعیت‌های RESERVED و ACTIVE؛ یکتایی `operation_uid`؛ تلاش مجدد همان عملیات با ورودی متفاوت باید ۴۰۹ بدهد. زمینه مالکیت از فایل خوانده شود و مصرف‌کننده موظف به تأیید تعلق مقصد به همان زمینه است. UUID مقصد عملیاتی FK بین دیتابیس‌ها ندارد. اتصال RELEASED دوباره فعال نشود؛ عملیات جدید شناسه جدید می‌گیرد.

برای هر جدول و هر ستون، هم توضیح فارسی در مایگریشن و هم `COMMENT ON` واقعی PostgreSQL ثبت شود. ایجاد فایل مایگریشن مجوز اجرای آن روی دیتابیس پروژه نیست.

## ۵. انواع فایل، حجم و ذخیره‌سازی

| دسته | سقف تأییدشده | فهرست اولیه پیشنهادی پسوند |
|---|---:|---|
| لوگو | ۲ مگابایت | jpg، jpeg، png، webp، gif |
| تصویر | ۱۰ مگابایت | jpg، jpeg، png، webp، gif |
| سند | ۲۰ مگابایت | pdf، txt، csv |
| فایل فشرده | ۵۰ مگابایت | zip |
| صوت | ۵۰ مگابایت | mp3، wav، ogg، m4a |
| ویدئو | ۲۰۰ مگابایت | mp4، webm |

سقف حجم‌ها تصمیم تأییدشده کاربر است؛ فهرست پسوندها پیشنهاد نسخه اول است و از تنظیمات توسعه می‌یابد. برای محاسبه فنی، واحد حجم به‌روشنی تعریف شود؛ پیشنهاد `1 MiB = 1024 × 1024 bytes`. مجموع سهمیه نیز بر حسب byte باشد.

- برای هر پسوند، MIMEهای مجاز و شیوه تشخیص متناظر تعریف شود؛ صرف پسوند یا Content-Type ارسالی معتبر نیست. تصویر باید واقعاً قابل شناسایی باشد و محدودیت ابعاد/تعداد پیکسل نیز داشته باشد.
- HTML، SVG، فایل اجرایی و اسکریپت در فهرست اولیه نیستند. ZIP استخراج نمی‌شود. قالب‌های Office و آرشیوهای دیگر فقط با افزودن اعتبارسنجی متناظر فعال شوند؛ «و ...» به معنی پذیرش پسوند دلخواه نیست.
- اعتبارسنجی قالب به معنی اسکن ضدبدافزار نیست. اسکنر مستقل، اگر لازم شود، مرحله بعد است.
- دیسک اختصاصی از `Storage` لاراول استفاده کند. ریشه خصوصی هیچ symlink عمومی نداشته باشد. نام فایل، مسیر و URL کامل از کاربر پذیرفته نشوند.
- نمونه کلید: `business/{business_uid}/{file_uid}.{ext}` یا `platform/{file_uid}.{ext}`. نام کسب‌وکار یا نام اصلی فایل وارد کلید نشود.
- انتخاب دیسک فقط هنگام ایجاد رکورد از تنظیمات انجام شود؛ خواندن و حذف بعدی از `disk` همان رکورد استفاده کند. تغییر دیسک پیش‌فرض نباید فایل‌های قدیمی را به محل دیگری ارجاع دهد.
- آستانه PHP، وب‌سرور و Next.js برای بدنه multipart با سقف ۲۰۰ مگابایت و سربار آن هماهنگ شود؛ افزایش این تنظیمات جزو عملیات استقرار است. پاسخ بدنه بیش‌ازحد بزرگ ۴۱۳ باشد.

استفاده از نام دیسک و کلید نسبی با قابلیت چند دیسک Laravel سازگار است؛ نیازی به ساخت adapter اختصاصی برای هر فضای آینده نیست. [مستند رسمی ذخیره‌سازی Laravel](https://laravel.com/framework/docs/13.x/filesystem)

تشخیص MIME از محتوای فایل در اعتبارسنجی Laravel پشتیبانی می‌شود؛ بررسی پسوند و محدودیت کاربرد نیز باید در کنار آن اعمال شود. [مستند رسمی اعتبارسنجی فایل](https://laravel.com/framework/docs/13.x/validation#validating-files)

## ۶. چرخه آپلود و سهمیه ساده

برای نسخه اول دو جدول بالا کافی‌اند. سهمیه از مجموع `size_bytes` رکوردهای حذف‌نشده محاسبه شود؛ جدول شمارنده و همگام‌سازی شمارنده اضافه نشود.

۱. هویت، دامنه درخواست، وضعیت کسب‌وکار و اجازه کاربرد فایل بررسی شود. در آپلود لوگو، همان مجوز تغییر لوگو لازم است.

۲. فایل موقت درخواست اعتبارسنجی و اندازه واقعی آن خوانده شود. فایل ناقص، صفر بایت، نوع نامجاز یا اندازه بیش‌ازحد پذیرفته نشود.

۳. در تراکنش کوتاه مرکزی، قفل سهمیه همان مالک گرفته شود. برای هماهنگی همه workerها از `pg_advisory_xact_lock` با فضای نام ثابت `media:quota:business:<id>` یا `media:quota:platform` استفاده شود. هم‌زمانی باید در دیتابیس کنترل شود، نه فقط در حافظه PHP.

۴. مجموع حجم وضعیت‌های UPLOADING، READY و DELETING به‌اضافه فایل جدید از سقف تجاوز نکند. سپس رکورد UPLOADING با UID، کلید و حجم معلوم ثبت و تراکنش بسته شود. به این ترتیب دو آپلود هم‌زمان هر دو از یک فضای باقیمانده استفاده نمی‌کنند.

۵. باینری با stream روی دیسک نوشته شود؛ کل فایل در حافظه بارگذاری نشود. هنگام انتقال باینری، تراکنش و قفل سهمیه باز نماند.

۶. با قفل ردیف فایل، وجود و اندازه باینری تأیید و وضعیت READY شود. پاسخ موفق فقط پس از READY صادر شود. اگر پاک‌سازی قبلاً فایل را DELETING کرده، آپلود دیررس نباید آن را به READY برگرداند و باید باینری خود را پاک کند.

۷. شکست ذخیره‌سازی، فایل را برای حذف قابل تلاش مجدد علامت بزند. تا تأیید حذف واقعی، ظرفیت آن آزاد نشود. قطع فرایند بین مراحل باید توسط پاک‌سازی قابل بازیابی باشد.

سقف کلی فضای کسب‌وکار و پلتفرم هنوز از کاربر عدد تأییدشده ندارد. پیشنهاد اولیه قابل تنظیم: ۱ GiB برای هر کسب‌وکار و ۵ GiB برای فضای خود پلتفرم؛ این اعداد «پیشنهاد» هستند، نه تصمیم قبلی. مقدار صفر یا تنظیم نامعتبر نباید بی‌صدا به فضای نامحدود تبدیل شود. فایل آپلودشده توسط مدیر برای یک کسب‌وکار از سهمیه همان کسب‌وکار کسر شود.

## ۷. قرارداد دسترسی و مصرف‌کننده

`MediaConsumer` باید عملیات زیر را برای نوع ثبت‌شده خود پاسخ دهد:

- اجازه اتصال فایل به مقصد برای عامل درخواست.
- اجازه دریافت فایل متصل، با کنترل تعلق مقصد و شعبه و مجوز فعلی عامل.
- اجازه آزادسازی اتصال برای عامل درخواست.
- عمومی‌بودن اتصال مشخص؛ نسخه اول فقط برای لوگوی جاری.
- نتیجه تطبیق عملیات: `Attached`، `DetachedFinal` یا `Unknown`؛ نبود اتصال لحظه‌ای، معادل لغو قطعی نیست.

Media از registry محدود مصرف‌کنندگان استفاده کند. نبود handler، نبود مجوز، لغو عضویت، قطع دیتابیس مصرف‌کننده یا پاسخ نامطمئن به معنی اجازه نیست. قطع زیرساخت می‌تواند ۵۰۳ امن بدهد؛ نبود فایل یا دسترسی خارج از زمینه پاسخ ۴۰۴ غیرقابل تشخیص بدهد.

فایل خصوصیِ هرگز متصل‌نشده فقط برای آپلودکننده با هویت و عضویت فعال در همان زمینه قابل دریافت و حذف است. داشتن همان user_id در کسب‌وکار دوم اجازه دسترسی نمی‌دهد. مدیر پلتفرم نیز صرف مدیر بودن، حق مشاهده همه پیوست‌های خصوصی را ندارد.

بعد از اتصال، دسترسی از مصرف‌کننده گرفته شود؛ اجازه مالک، آپلودکننده یا مدیر نباید به‌صورت عمومی جایگزین سیاست مصرف‌کننده شود. بعد از آزادسازی آخرین اتصال، فایل قبلاً متصل‌شده خودکار به «فایل خصوصی قابل مشاهده برای آپلودکننده» برنگردد؛ `first_attached_at` این تفاوت را حفظ کند. چنین فایلی طبق تصمیم آزادسازی، نامزد پاک‌سازی است و برای استفاده دوباره باید جریان مجاز جدید تعریف شود.

برای فایل عملیاتی، handler فقط هنگام ارزیابی مجوز وارد `TenantRuntime` شود و نتیجه مجوز را همان‌جا محاسبه کند. تراکنش عملیاتی تا پایان دانلود/پخش باز نماند و callback پخش بعداً به مدل عملیاتی یا زمینه پاک‌شده وابسته نباشد. هر درخواست جدید Range مجوز جدید می‌گیرد؛ لغو دسترسی الزاماً بایت‌های قبلاً تحویل‌شده را پس نمی‌گیرد.

## ۸. اتصال، آزادسازی و حذف هماهنگ

تمام مسیرهای رزرو، تأیید، آزادسازی، تغییر لوگو و حذف باید فایل را با قفل ردیفی مرکزی بررسی کنند. ترتیب قفل‌ها ثابت باشد؛ در تعویض چند فایل، فایل‌ها به ترتیب شناسه قفل شوند. قفل‌گرفتن در یک مسیر و بررسی بدون قفل در مسیر دیگر کافی نیست.

### مصرف‌کننده مرکزی، مانند لوگو

تغییر مرجع موجودیت، ثبت/آزادسازی attachment و `first_attached_at` در یک تراکنش `central` انجام شوند. عملیات باینری بیرون تراکنش بماند. فایل باید READY، متعلق به همان کسب‌وکار، با purpose مناسب و بدون اتصال یا رزرو دیگر باشد. لغو تراکنش نباید نیمه‌ای از مرجع لوگو را باقی بگذارد.

### مصرف‌کننده عملیاتی آینده

۱. پس از بررسی مجوز و مالکیت، Media قبل از ثبت عملیاتی، اتصال RESERVED و شناسه عملیات پایدار ایجاد کند؛ این رزرو حذف را مسدود می‌کند و هنوز مجوز دریافت از مصرف‌کننده ایجاد نمی‌کند.

۲. مصرف‌کننده در تراکنش دیتابیس خودش UUID فایل و شناسه عملیات را ثبت کند. تلاش مجدد عملیات نباید پیوست تکراری بسازد.

۳. پس از commit موفق، تأیید مرکزی اتصال انجام شود. شکست این تأیید، رزرو را حفظ می‌کند و با تلاش مجدد یا تطبیق بازیابی می‌شود. `TenantRuntime::afterCommit` فقط انتشار پس از commit را فراهم می‌کند؛ تضمین تحویل قطعی نیست.

۴. برای آزادسازی، مصرف‌کننده ابتدا مرجع عملیاتی را با مجوز مناسب حذف و commit کند؛ سپس Media اتصال را RELEASED کند. تا پیش از تأیید، اتصال همچنان حذف فایل را مسدود می‌کند.

۵. رزرو دارای وضعیت نامعلوم با گذشت زمان آزاد نشود. `DetachedFinal` فقط وقتی معتبر است که مصرف‌کننده پایان یا لغو قطعی همان عملیات را به‌صورت پایدار ثبت کرده باشد و اجرای دیررس دیگر نتواند آن را متصل کند. صرف پیدا نشدن ردیف یا خطای اتصال مجوز آزادسازی نیست.

در این مرحله قرارداد و ماشین وضعیت مرکزی ساخته می‌شود؛ هر ماژول عملیاتی آینده باید ثبت پایدار نتیجه عملیات و تطبیق خودش را همراه پیوست‌ها پیاده کند. نبود آن نباید با حذف خودکار رزرو پوشانده شود.

### حذف فیزیکی

با قفل فایل، نبود RESERVED/ACTIVE و اجازه حذف بررسی و وضعیت DELETING ثبت شود. از این لحظه رزرو یا اتصال جدید ممنوع است. سپس خارج تراکنش، فایل از دیسک ثبت‌شده حذف شود. فقط پس از تأیید نبود باینری، وضعیت DELETED ثبت و سهمیه آزاد شود. شکست حذف قابل تلاش مجدد است؛ رکورد و اطلاعات مسیر زودتر حذف نشوند. فراخوانی مجدد حذف برای DELETING/DELETED تکرارپذیر باشد.

## ۹. پاک‌سازی

فرمان محدود و دسته‌ای `media:cleanup` با حالت پیش‌فرض مشاهده طرح و گزینه اجرای صریح طراحی شود. زمان‌بند پس از استقرار مجاز فعال شود.

- پیشنهاد مهلت فایل READY هرگز متصل‌نشده: ۲۴ ساعت، قابل تنظیم. عدد نهایی در تنظیمات مستند شود.
- فایل UPLOADING قدیمی فقط پس از عبور از مهلتی بیش از حداکثر زمان مجاز آپلود، با قفل ردیف نامزد حذف شود؛ رقابت با پایان دیررس آپلود طبق بخش ۶ کنترل شود.
- فایل قبلاً متصل‌شده و آزادشده طبق مهلت نگهداری تعیین‌شده پاک شود؛ فایل دارای رزرو یا اتصال فعال هرگز وارد این مسیر نشود.
- فایل DELETING دوباره تلاش شود؛ خطا شمارش و با اطلاعات امن ثبت شود.
- تطبیق رزروهای نامطمئن از حذف فایل‌های رهاشده جدا باشد؛ پاسخ Unknown گزارش شود و فایل حفظ شود.
- فایل فیزیکی بدون رکورد فقط در ریشه اختصاصی Media و پس از مهلت امن، با کنترل دوباره نبود رکورد بررسی شود. پاک‌سازی کور کل storage یا دیسک عمومی ممنوع است.
- quota به تنهایی مانع پرشدن کل دیسک توسط تعداد زیاد کسب‌وکارها نیست؛ خطای فضای فیزیکی باید کنترل‌شده باشد و برای استقرار، حداقل فضای آزاد و پایش ظرفیت در نظر گرفته شود.

## ۱۰. هویت پلتفرم و مجوز تغییر لوگو

در کد فعلی `RequirePermission` به `CurrentBusiness` وابسته است. برای مدیر پلتفرم، middleware و context معتبر PLATFORM لازم است که به عضویت کسب‌وکار جعل‌شده وابسته نباشد.

پیش‌نیاز مسیرهای پلتفرمی: Identity باید نشست Passport با دامنه PLATFORM را صادر/اعتبارسنجی کند، آن را به هویت، عضویت پلتفرمی و نسخه اعتبار هویت متصل کند و ابطال را رعایت کند. توکن صادرشده برای یک کسب‌وکار، حتی برای کاربری که مدیر پلتفرم هم هست، نباید توکن پلتفرم تلقی شود. تکمیل حداقلی این قرارداد در Identity انجام شود؛ احراز هویت دوم داخل Media ساخته نشود. تا آماده‌شدن این پیش‌نیاز، مسیرهای پلتفرمی عمومی و قابل استفاده اعلام نشوند.

قانون تغییر لوگو:

۱. مالک واقعی همان کسب‌وکار بر اساس `owner_user_id`، همراه با هویت و زمینه معتبر؛ نام نقش «مالک» به‌تنهایی کافی نیست.

۲. عضو دارای مجوز مشخص `business.logo.update` در همان کسب‌وکار.

۳. عامل پلتفرمی دارای مجوز مشخص `platform.business.logo.update` و نشست PLATFORM معتبر.

مجوز کسب‌وکاری لوگو با دامنه BUSINESS و `module_key=core` تعریف شود. مجوز مدیر پلتفرم کلید مجزا و دامنه PLATFORM داشته باشد. عمومی‌بودن خواندن لوگو به معنی عمومی‌بودن تغییر آن نیست. مجوز عمومی مدیا نباید شرط اضافه‌ای باشد که مالک یا عضو دارای مجوز لوگو را از آپلود لوگو منع کند؛ آپلود با purpose لوگو همین سیاست را اجرا کند.

برای آپلود عمومی پیوست متصل‌نشده، مجوز `media.upload` با دامنه BUSINESS و `module_key=core` پیشنهاد می‌شود؛ فعال‌بودن عضویت به‌تنهایی اجازه مصرف فضای ذخیره‌سازی نیست. آپلود فایل متعلق به خود پلتفرم به `platform.media.upload` با دامنه PLATFORM نیاز دارد. تعریف این مجوزها به معنی اعطای خودکار آن‌ها به همه نقش‌ها نیست. دریافت/حذف فایل متصل‌نشده با قاعده آپلودکننده و دریافت/آزادسازی فایل متصل با قاعده مصرف‌کننده انجام شود؛ مجوز کلی «مشاهده همه فایل‌ها» در این نسخه تعریف نشود. نام نهایی مجوزها قبل از پیاده‌سازی با کاتالوگ پروژه تثبیت شود.

برای فایل‌های عمومی پلتفرم در آینده، handler مصرف‌کننده مستقل لازم است؛ API تغییر دلخواه visibility ساخته نشود. فایل بیزنسی آپلودشده توسط مدیر همچنان متعلق به همان کسب‌وکار است.

وضعیت کسب‌وکار برای سرویس مرکزی مطابق قرارداد موجود ACTIVE/PENDING_ACTIVATION/SUSPENDED بررسی شود؛ این قابلیت نباید بی‌دلیل به سلامت دیتابیس عملیاتی یا اشتراک CRM وابسته باشد. مجوز فایل متصل به ماژول عملیاتی همچنان از همان ماژول و سیاست اشتراک آن گرفته می‌شود. مجوزهای زیرساختی کسب‌وکاری مدیا که باید در تعلیق قابل استفاده باشند با `module_key=core` و فهرست خدمات مرکزی هماهنگ شوند؛ تمام ماژول‌ها به فهرست always_available اضافه نشوند.

## ۱۱. وب‌سرویس‌های پیشنهادی

مسیرهای فایل در `Modules/Media/routes/api.php` و مسیر تغییر لوگو در هسته کسب‌وکار باشد. هسته از قرارداد عمومی Media استفاده کند. URIهای زیر پیشنهاد قرارداد نسخه اول هستند:

| روش و مسیر | کاربرد |
|---|---|
| `POST /api/v1/tenant/media` | آپلود multipart شامل file و purpose؛ پاسخ ۲۰۱ پس از READY |
| `GET /api/v1/tenant/media/{file}` | مشخصات مجاز فایل پس از کنترل دسترسی |
| `GET /api/v1/tenant/media/{file}/content` | نمایش/پخش خصوصی پایه؛ همان کنترل دسترسی دریافت |
| `GET /api/v1/tenant/media/{file}/download` | دریافت با Content-Disposition attachment |
| `DELETE /api/v1/tenant/media/{file}` | حذف مجاز فایل استفاده‌نشده؛ ۲۰۴ اگر تمام شده، ۲۰۲ اگر DELETING |
| `GET /api/v1/media/public/{file}/content` | دریافت عمومی فقط برای اتصال عمومی معتبر، ابتدا لوگوی جاری |
| `PUT /api/v1/tenant/logo` | اتصال file_uid معتبر به لوگوی همان کسب‌وکار |
| `DELETE /api/v1/tenant/logo` | برداشتن لوگو و آزادسازی اتصال؛ حذف فیزیکی جداگانه |
| `POST /api/v1/platform/media` | آپلود در زمینه خود پلتفرم با نشست و مجوز PLATFORM |
| `GET /api/v1/platform/media/{file}` و زیرمسیرهای content/download، و `DELETE` | چرخه فایل پلتفرمی با سیاست متناظر |
| `POST /api/v1/platform/businesses/{business}/media` | آپلود مدیر برای زمینه کسب‌وکار مقصد، ابتدا کاربرد لوگو |
| `PUT /api/v1/platform/businesses/{business}/logo` و `DELETE` | مدیریت لوگوی کسب‌وکار هدف با مجوز پلتفرمی مشخص |

در نسخه اول API عمومی برای reserve/confirm/release دلخواه ساخته نشود. این‌ها قرارداد داخلی قابل فراخوانی توسط مصرف‌کنندگان معتبرند. مسیر مدیر برای کسب‌وکار مجوز مشاهده عمومی تمام فایل‌های آن کسب‌وکار ایجاد نمی‌کند.

درخواست‌های HEAD متناظر با GET محتوا همان کنترل دسترسی را اجرا کنند. مسیر کسب‌وکار از `business.host`، Passport، حساب فعال و `business.context:platform` استفاده کند. شناسه مالک از hostname و context معتبر می‌آید، نه body. در مسیر مدیر، business هدف پارامتر صریح مجاز است، اما دامنه و اجازه مدیریتی سرور بررسی می‌شود.

پاسخ آپلود حداقل: `id` عمومی، `name` پالایش‌شده، `mime_type`، `size_bytes`، `purpose` و `status`. disk، storage_key، مسیر فیزیکی، شناسه داخلی و اطلاعات سایر مصرف‌کنندگان در پاسخ نباشند. URL خصوصی در پاسخ، نیاز به احراز هویت دارد و لینک عمومی محسوب نمی‌شود.

خطاهای قراردادی: ۴۰۱ بدون هویت، ۴۰۴ فایل یا زمینه نامعتبر/غیرقابل دسترس، ۴۰۳ نداشتن مجوز یک عمل در زمینه شناخته‌شده، ۴۰۹ فایل در حال استفاده/وضعیت نامناسب/سهمیه ناکافی، ۴۱۳ بدنه بیش‌ازحد بزرگ، ۴۲۲ نوع یا محتوای نامعتبر، ۴۲۹ محدودیت نرخ و ۵۰۳ ذخیره‌سازی یا مصرف‌کننده ناموجود. کدهای ثابت مانند `MEDIA_IN_USE`، `MEDIA_QUOTA_EXCEEDED` و `MEDIA_STORAGE_UNAVAILABLE` در قالب خطای فعلی حفظ شوند. وضعیت و پیام خام storage یا دیتابیس به کاربر نرسد.

## ۱۲. تحویل، نمایش و پخش در Next.js

- دانلود و نمایش از یک منطق مجوز و یک سرویس تحویل استفاده کنند؛ تفاوت فقط disposition و قابلیت قالب باشد.
- برای فایل محلی از پاسخ فایل استاندارد Symfony/Laravel استفاده شود تا Range، HEAD، طول پاسخ، ۲۰۶ و ۴۱۶ درست مدیریت شوند؛ پخش دستی با خواندن کل فایل پیاده نشود. StreamedResponse معمولی به‌تنهایی اثبات پشتیبانی Range نیست.
- قالب تصویر تأییدشده و صوت/ویدئوی سازگار می‌توانند inline باشند. ZIP، txt و csv فقط دانلود شوند. PDF در نسخه حداقلی به‌صورت دانلود ارائه شود؛ نمایش در مرورگر، در صورت افزودن، با viewer/مبدأ مناسب و سیاست امنیتی مستقل انجام شود.
- پسوند mp4 به معنی پشتیبانی همه codecها در همه مرورگرها نیست؛ API تبدیل قالب انجام نمی‌دهد. قالب ناسازگار همچنان قابل دانلود است.
- Content-Type واقعی، Content-Disposition امن و `X-Content-Type-Options: nosniff` تنظیم شوند. نام اصلی در header بدون پالایش استفاده نشود.
- خصوصی: `Cache-Control: private, no-store`؛ پیش از پاسخ ۳۰۴، HEAD یا Range نیز مجوز کنترل شود. عمومی نسخه اول: revalidation در هر درخواست و بدون cache بلندمدت، تا برداشتن لوگو تا حد ممکن اعمال شود. فایل قبلاً دریافت‌شده قابل پس‌گرفتن نیست.
- مرورگر در تگ‌های معمول img/audio/video هدر دلخواه Bearer از برنامه دریافت نمی‌کند. مسیر پیشنهادی فعلی: Next.js BFF نشست امن کاربر را بررسی و درخواست محتوای خصوصی را با توکن سروری به Laravel بفرستد؛ سپس پاسخ را stream کند. توکن Passport در query string قرار نگیرد.
- BFF باید Range، If-Range و HEAD و پاسخ‌های ۲۰۰/۲۰۶/۴۱۶، Content-Range، Content-Length، Accept-Ranges و Content-Type را درست عبور دهد، پاسخ خصوصی را cache نکند و کل ویدئو را buffer نکند. مقصد upstream ثابت و معتبر باشد؛ ورودی کاربر URL آزاد برای proxy نباشد.
- پیاده‌سازی Next.js خارج از این پوشه backend است؛ تا اتصال BFF و مرورگر بررسی نشده، «پخش خصوصی در رابط کاربری آماده است» اعلام نشود. قرارداد backend و نمونه درخواست‌های دارای Authorization در مستندات تحویل داده شود.

پاسخ `BinaryFileResponse` استاندارد Symfony از Range و If-Range پشتیبانی می‌کند؛ پیش از استفاده جزئیات نسخه نصب‌شده نیز بررسی شود. برای فضای غیرمحلی آینده، تحویل می‌تواند به proxy یا URL موقت مجاز تغییر کند و قرارداد مصرف‌کنندگان ثابت بماند. [مستند رسمی تحویل فایل Symfony](https://symfony.com/doc/current/components/http_foundation.html#serving-files)

## ۱۳. جایگزینی لوگوی قدیمی

۱. ابتدا مسیر عمومی Media و مصرف‌کننده `business_logo` آماده شوند.

۲. پیش از افزودن FK جدید یا حذف endpoint قدیمی، وجود مرجع‌های فعلی `logo_asset_uid` و فایل‌های دیسک public بررسی شود. خالی‌بودن محیط نباید بدون بررسی فرض شود.

۳. اگر داده وجود دارد، یک فرایند انتقال تکرارپذیر و ابتدا فقط گزارش تهیه شود: خواندن و اعتبارسنجی فایل، کپی به دیسک خصوصی، ثبت metadata و اتصال مرکزی و سپس تغییر مرجع. فایل قبلی تا تأیید انتقال حذف نشود. اجرای انتقال به مجوز عملیات همان مرحله وابسته است.

۴. تغییر لوگو، قفل ردیف کسب‌وکار و فایل‌های قدیم/جدید را با ترتیب ثابت بگیرد. آزادسازی اتصال قدیمی، ثبت اتصال جدید و تغییر `logo_asset_uid` در یک تراکنش باشد. فراخوانی دوباره با همان فایل نتیجه ثابت بدهد.

۵. `/api/v1/tenant` فقط نشانی مسیر عمومی Media را در `logo_url` برگرداند؛ نبود لوگو برابر null. دسترسی به باینری همچنان از endpoint محتواست.

۶. endpoint قدیمی و منطق مسیر `business-logos` پس از تعیین تکلیف سازگاری مصرف‌کنندگان و انتقال حذف شوند. فایل‌های واقعاً عمومی قدیمی نیز پس از تأیید انتقال باید از دسترس عمومی خارج شوند؛ حذف route به‌تنهایی آن‌ها را خصوصی نمی‌کند.

## ۱۴. ترتیب اجرا و معیار پذیرش

| گام | کار قابل تحویل |
|---|---|
| ۱ | ثبت قراردادهای این سند، سیاست فایل و مقادیر تنظیم‌پذیر؛ تعیین تکلیف حداقل نشست معتبر پلتفرم در Identity |
| ۲ | ساخت ماژول، دو جدول مرکزی، محدودیت‌ها، کامنت فارسی و به‌روزرسانی مانیفست جاری |
| ۳ | دیسک خصوصی، اعتبارسنجی، سهمیه و چرخه UPLOADING تا READY |
| ۴ | کنترل دریافت فایل متصل‌نشده، تحویل/دانلود و Range با قرارداد Next.js |
| ۵ | registry مصرف‌کننده، رزرو/تأیید/آزادسازی تکرارپذیر و حذف هماهنگ |
| ۶ | مصرف‌کننده لوگو، مجوز مالک/عضو/مدیر، URL جدید و طرح انتقال قدیمی |
| ۷ | پاک‌سازی محدود، تلاش مجدد حذف، گزارش امن و مستندات API/خطاها |
| ۸ | پس از تکمیل کد، اجرای بررسی‌ها و تست‌ها فقط در محدوده مجاز همان نوبت |

سناریوهای پذیرش لازم برای پایان پیاده‌سازی:

- کاربر مشترک دو کسب‌وکار، فایل خصوصی کسب‌وکار اول را از زمینه دوم دریافت یا متصل نکند.
- عضو دیگر همان کسب‌وکار، فایل خصوصی متصل‌نشده آپلودکننده را نبیند؛ پس از اتصال، سیاست مصرف‌کننده اجرا شود.
- توکن کسب‌وکاری وارد مسیر مدیر پلتفرم نشود؛ مالک، عضو دارای مجوز لوگو و مدیر مجاز هرکدام فقط در زمینه مجاز عمل کنند.
- دو آپلود هم‌زمان سهمیه را دور نزنند؛ خطای ذخیره‌سازی ظرفیت را بدون تعیین تکلیف باینری آزاد نکند.
- رقابت attach/delete و cleanup/attach، فایل متصل یا رزروشده را حذف نکند.
- خطای بین ثبت عملیاتی و تأیید مرکزی، فایل را محفوظ و عملیات را قابل تطبیق نگه دارد؛ رزرو نامطمئن با زمان آزاد نشود.
- retry رزرو، تأیید، آزادسازی و حذف، نتیجه تکراری یا اتصال مجدد پس از آزادسازی نسازد.
- لغو مجوز، درخواست محتوای بعدی و Range بعدی را قطع کند؛ خطای دیتابیس مصرف‌کننده به دسترسی عمومی تبدیل نشود.
- دانلود، HEAD، Range معتبر، Range نامعتبر و محتوای حجیم با پاسخ و حافظه مناسب کار کنند.
- لوگوی جدید از مسیر عمومی مدیا بیاید؛ فایل خصوصی از همان مسیر عمومی قابل دسترسی نباشد؛ لوگوی قبلی پس از آزادسازی دیگر از آن مسیر ارائه نشود.
- همه جدول‌ها و ستون‌ها در PostgreSQL کامنت فارسی داشته باشند و مهاجرت مرکزی در مسیر عملیاتی قرار نگیرد.

تست‌های جداسازی، قفل‌ها و خطاهای فایل/دیتابیس نیاز به محیط موقت و مجاز دارند. نتایج تاریخی مرحله تننسی، تأیید این ماژول نیستند. این سند هیچ مجوز مستقلی برای migrate، seed، پاک‌سازی یا انتقال فایل‌های واقعی ایجاد نمی‌کند.
