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