This commit is contained in:
2026-10-11 02:23:22 +07:00
parent 9d46fc0e13
commit 62b623d3ca
9 changed files with 1089 additions and 716 deletions
+104 -4
View File
@@ -145,8 +145,8 @@ MIME и SHA-256, некорректные связи данных и состо
сообщения API.
SHA-256 защищает от повреждения, но не подтверждает автора пакета. Подпись и
шифрование не реализованы. Кодек не блокирует игру и не делает импорт; повторные
импорты, сохранность QR на площадке и полный перенос проверяются на этапе 6.
шифрование не реализованы. Кодек не блокирует игру и не делает импорт. Импортёр этапа 6 использует его
проверенный результат; повторные импорты и сохранность QR описаны ниже.
Проверка: `go test ./internal/services/game_package_service -count=1` и полный
`go test ./...`. Тесты покрывают сохранение данных, автономное чтение, загрузку
@@ -154,8 +154,8 @@ SHA-256 защищает от повреждения, но не подтверж
## Экспорт со статусом офлайн-режима: этап 5
Только для тестовых игр до реализации возврата результатов. Импорт, интерфейс
передачи, отмена и возврат результатов реализуются следующими этапами.
Только для тестовых игр до реализации возврата результатов. Импорт реализован
на этапе 6; интерфейс передачи, отмена и возврат результатов остаются следующими этапами.
### API
@@ -246,3 +246,103 @@ maintenance очищает оставшиеся каталоги термина
Необязательный `TEST_S3_RESTART_MARKER` позволяет внешнему тестовому harness
перезапустить изолированный RustFS: после появления marker нужно дождаться health
и создать `<marker>.continue` в течение трёх минут. Это не production-проверка.
## Локальный импорт: этап 6
### API
`POST /api/games/import` принимает ZIP непосредственно в теле запроса с
`Content-Type: application/zip` и `Authorization: Bearer <JWT>`. Multipart и
Base64 не используются. Разрешены роли `admin`, `developer`, `organizer`;
операция доступна только в `APP_MODE=venue`.
Ответ HTTP 200: `{"gameID":100,"packageID":"…","repeated":false}`.
Повтор того же пакета возвращает `repeated:true` и прежние идентификаторы,
не перезаписывая локальные изменения. Повтор после удаления игры также
возвращает прежний результат и не восстанавливает удалённую игру.
Ошибки: 401/403 — авторизация/роль; 400 — неподходящий режим, повреждённый
пакет, версия или данные; 409 — конфликт ID, изменённые байты под прежним ID
пакета, другой пакет той же игры или ещё выполняющийся импорт; 413 — лимит
размера; 499/504 — отмена/timeout; 500 — безопасная внутренняя ошибка.
Ответы имеют `Cache-Control: no-store`.
Маршрут описан в protobuf как `ImportGame(HttpBody) → GameImportRsp`, но
HTTP-поток обслуживает отдельный handler перед grpc-gateway: ZIP размером до
1 ГиБ не проходит через gRPC-буфер. Прямой gRPC-вызов использует тот же сервис,
однако ограничен существующим лимитом сообщений. В обоих конфигурациях прокси
venue только `/api/games/import` получает лимит 1 ГиБ, отключённую буферизацию
request body и timeout 30 минут; другие маршруты сохраняют прежние лимиты.
### Перенос и конфликты
Импорт сохраняет ID игры, команд и билетов, пароли QR, хеши билетов, их
статусы/даты, подтверждения участия, email капитанов и ID доставки регистрации.
Занятый локальный ID игры/команды/билета или хеш билета приводят к отказу,
не затрагивая существующие записи. Конфликты проверяются до загрузки медиа и
повторно в транзакции публикации.
Сценарий, оба плейлиста и треки создаются отдельными копиями с новыми локальными
ID. Все назначения перепривязываются; сценарий принадлежит импортирующему
пользователю. `creator_id` команд и `used_by` билетов остаются NULL: совпадение
числового ID не связывает аккаунты двух серверов. Исходные связи и соответствия
ID сохраняются в закрытых метаданных `game_imports` для будущего возврата.
Формат и статус игры сохраняются. План экранов и карта переносятся без подключённых
устройств; воспроизведение музыки начинает со штатного состояния покоя.
Медиа загружаются в локальный RustFS потоково под отдельными именами попытки,
с заменой всех ссылок. Одинаковое медиа сценария используется повторно; треки
с одинаковыми байтами получают разные имена, если этого требует существующее
ограничение уникальности `playlist_tracks.filename`.
### Атомарность и восстановление
ZIP и распаковка находятся в приватном временном каталоге с правами 0700/0600.
Вся обработка ограничена 30 минутами. До загрузки файлов в БД фиксируются
ID попытки, исходный сервер, ID пакета, SHA-256 всего ZIP, оригинальный снимок,
манифест и ключи будущих объектов. Частичные уникальные индексы разрешают одну
активную попытку на ID пакета и исходный ID игры, включая завершённые импорты.
После загрузки всех файлов одна транзакция создаёт сценарий, плейлисты,
треки, игру, команды, билеты, план, карту и состояние `ready` со связями ID.
На время публикации вставки/изменения games/teams/tickets блокируются для
повторной проверки конфликтов и безопасного продвижения последовательностей.
Последовательности могут оставить пропуски после rollback; исходные ID
повторно не выдаются. При ошибке ни одна часть новой игры не видна.
Heartbeat каждые 30 секунд продлевает аренду на 2 минуты; истёкшая попытка
не может публиковать данные. Recovery проверяет БД каждые 5 секунд, переводит
прерванные попытки в `failed` и повторяет очистку их объектов, в том числе после
перезапуска и позднего завершения старой загрузки. Неоднозначный результат
commit не разрешает удаление: очистка допустима только для подтверждённого
`failed`. Ключи успешного импорта никогда не используются для этой очистки.
Временные каталоги прерванных запросов удаляются после 32 минут и проверки
отсутствия активной попытки. Ошибка допускает новую попытку с отдельными ключами.
### Проверка
`TEST_DATABASE_URL` включает проверки с реальными миграциями в отдельных схемах:
```sh
go test ./internal/repos/game_imports_repo ./internal/services/game_import_service ./internal/app -count=1
go test -race ./internal/repos/game_imports_repo ./internal/services/game_import_service -count=1
```
Проверяются сохранность данных/QR/билетов, локальные ID копий и исходные связи,
последовательности, повтор с локальными изменениями, изменённый ZIP под прежним
ID, конфликты до/после загрузки, конкурентный импорт, rollback всех записей,
ошибки загрузки/очистки, отмена, аренда/timeout и неопределённый ответ commit.
HTTP-тесты проверяют роли, ошибки, MIME, лимиты и отсутствие внутренних деталей.
Для изолированного RustFS дополнительно задать `TEST_S3_ENDPOINT`, `TEST_S3_USER`,
`TEST_S3_PASSWORD` и запустить `go test ./internal/services/game_import_service
-run TestImportRustFSHTTPAndRestart -count=1 -v`. Smoke проверяет настоящий HTTP
handler, файлы в отдельном bucket и повтор импорта. Необязательный
`TEST_S3_RESTART_MARKER` позволяет внешнему harness перезапустить только тестовый
RustFS и создать `<marker>.continue` в течение минуты. Bucket, файлы и тестовая
схема удаляются после проверки. Проверка без WAN проводится в отдельной Docker-сети
с `internal: true`; рабочая площадка и production для этого не изменяются.
Интерфейс передачи остаётся этапом 7; полный билетный и игровой прогон — этапом 8.
До готовности возврата результатов используются только тестовые игры.