axe/internal/db/migrations/0008_ticket_photos.sql
2026-10-18 22:07:25 +03:00

66 lines
4.3 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 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'));