-- 0008_ticket_photos — фотоотчёт мастера: файлы в S3, метаданные в БД. -- -- Мастер прикрепляет фото к заявке в работе (in_progress/done), клиент и -- админ их смотрят. Сами байты живут в объектном хранилище (S3-совместимом: -- локально MinIO в compose, в проде — любой S3): в таблице — только то, -- без чего нельзя проверить права и показать список. -- -- Правила, проверяемые в SQL, а не в хендлерах (INSERT/DELETE — CTE, -- см. photo.go): -- * загрузка только в статусах in_progress/done; -- * загрузивший — админ или мастер со статусом active в проекте -- заявки (тот же приём, что Assign в 0004/0005); -- * лимит файлов на заявку — счётчик в той же CTE, гонку ловит SQL; -- * удаление — своё фото и только пока заявка не закрыта, либо любое -- у админа; событие в журнале пишется той же операцией (как переходы -- статусов в 0006: либо обе части, либо ни одной). -- -- object_key генерирует сервер (uuid), клиент ключа не задаёт: лишняя -- точка взлома и путаницы с чужими ключами. UNIQUE — дубликат ключа -- невозможен даже при гонке. -- -- uploaded_by NULL возможен: аккаунт автора удалён (SET NULL, как -- actor_id в 0006) — фото и журнал остаются. -- -- Журнал: action расширяется двумя значениями photo_added/photo_removed -- (CHECK в той же миграции — расширение списка, не ослабление условий). -- Для этих событий from_status = to_status = текущий статус: статусная -- машина не двигалась, журнал лишь отмечает факт загрузки. В comment — -- исходное имя файла. -- -- Применённый файл не редактировать (checksum в schema_migrations). CREATE TABLE ticket_photos ( id bigserial PRIMARY KEY, ticket_id bigint NOT NULL REFERENCES tickets(id) ON DELETE CASCADE, -- NULL — аккаунт загрузившего удалён (ON DELETE SET NULL) uploaded_by uuid REFERENCES profiles(id) ON DELETE SET NULL, -- ключ объекта в хранилище, генерируется сервером object_key text NOT NULL UNIQUE, content_type text NOT NULL, size_bytes bigint NOT NULL CHECK (size_bytes > 0), -- имя файла у пользователя (для журнала и UI), может быть пустым original_name text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT now(), -- белый список форматов держится и здесь: даже обход валидации -- в хендлере не запишет в БД ничего кроме картинки CONSTRAINT ticket_photos_content_type_check CHECK (content_type IN ('image/jpeg', 'image/png', 'image/webp')), -- осмысленный потолок: мусор из 0 (теоретически возможен только -- обходом, но пусть и это проверит БД) CONSTRAINT ticket_photos_size_check CHECK (size_bytes <= 268435456) ); -- Список фото заявки: хронология по id. CREATE INDEX IF NOT EXISTS idx_ticket_photos_ticket ON ticket_photos (ticket_id, id); -- Журнал: два новых действия (расширение CHECK из 0006). ALTER TABLE ticket_events DROP CONSTRAINT ticket_events_action_check; ALTER TABLE ticket_events ADD CONSTRAINT ticket_events_action_check CHECK (action IN ('created', 'assigned', 'started', 'done', 'confirmed', 'rejected', 'canceled', 'photo_added', 'photo_removed'));