66 lines
4.3 KiB
SQL
66 lines
4.3 KiB
SQL
-- 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'));
|