axe/internal/db/migrations/0008_ticket_photos.sql

66 lines
4.3 KiB
MySQL
Raw Permalink Normal View History

2026-10-18 19:07:25 +00:00
-- 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'));