This commit is contained in:
2026-10-11 12:08:25 +07:00
parent b733efe8e2
commit 26c65062e9
10 changed files with 1703 additions and 0 deletions
+90
View File
@@ -346,3 +346,93 @@ RustFS и создать `<marker>.continue` в течение минуты. Buc
Интерфейс передачи остаётся этапом 7; полный билетный и игровой прогон — этапом 8.
До готовности возврата результатов используются только тестовые игры.
## Пакет результатов, версия 1 (ED-029, этап 9)
Это отдельный контракт: ZIP содержит ровно `manifest.json` и `results.json`,
без сценария и файлов. Он ещё не применяется онлайн и не доступен через API/UI.
`game_results_package_service.NewGameResultsPackageService(repo, mode).Prepare(ctx,
gameID, output)` доступен только в `venue`, для существующей игры из готового
импорта со статусом `finish` и заполненными датами проведения. Репозиторий
читает исходный реестр и текущие игровые данные в read-only транзакции
`REPEATABLE READ`. Подготовка ничего не меняет и не блокирует редактирование.
Повторный вызов создаёт новый ID и снимок, включая последующее оценивание.
Для работы без БД используются `Pack(ctx, manifest, data, output)` и
`Unpack(ctx, input io.ReaderAt, size)`. `Pack` задаёт версию, новый 128-битный
случайный ID, UTC-время, размер и SHA-256 `results.json`. При любой ошибке caller
отбрасывает выходной файл. `Unpack` возвращает только полностью проверенный
`Bundle`, читает ограниченные JSON в память, не извлекает файлы и не обращается
к сети/БД. Отмена контекста сохраняется в цепочке ошибки.
### manifest.json
| Поле | Значение |
| --- | --- |
| `version` | `1` |
| `package_id`, `created_at` | ID именно пакета результатов, время подготовки |
| `source_server`, `game_id` | Онлайн-источник и исходный ID игры |
| `source_package_id`, `source_sha256` | ID и SHA-256 всего исходного ZIP игры из реестра импорта |
| `size`, `sha256` | Размер и SHA-256 байтов `results.json` |
SHA-256 защищает от повреждения; это не подпись и не доказательство подлинности.
Онлайн-применение должно сверять источник и исходный экспорт на этапе 10.
### results.json
Все массивы обязательны и являются полным снимком; пустой массив — `[]`,
пропущенный или `null` массив не допускается.
| Поле | Содержимое |
| --- | --- |
| `source` | Исходные `teams`/`tickets` из сохранённого `game.json`, `scenario_id`, SHA-256 сериализованного исходного `story` в `scenario_sha256`, `materials` и `question_codes` |
| `game` | `id`, `status=finish`, неизменённый `format`, `started_at`, `ended_at` |
| `teams` | Итоговые поля команды из контракта игры плюс `source_id` и необязательный `local_creator_id` |
| `tickets` | Итоговые поля билета из контракта игры плюс `source_id` и необязательный `local_used_by` |
| `deleted_team_ids`, `deleted_ticket_ids` | Все исходные ID, отсутствующие в итоговом снимке |
| `actions` | Локальные `id`, `team_id`, `code`, `via_mcp`, `created_at`; порядок по команде, времени и ID сохраняет повторные и неизвестные коды |
| `applications` | Локальные `id`, `team_id`, исходные `name`/`image`/`file_type`, `status` (`need`/`done`), `created_at` |
| `answers` | `team_id`, `question_code`, `answer`, nullable `score`, `created_at`, `updated_at` |
`source_id` совпадает с `id` для исходной записи и равен `0` для новой локальной.
Все ссылки прогресса используют итоговый локальный `team_id`. Локальные ID новых
команд, билетов, ходов и улик не являются свободными ID онлайн-базы: следующий
этап должен выделять ID и сохранять соответствия.
`creator_id` и `registration_delivery_id` итоговой команды берутся только из
исходного снимка; для новой команды они отсутствуют. Текущий локальный создатель
сохраняется отдельно как `local_creator_id`. `used_by` билета восстанавливается
из исходного снимка только для ранее использованного билета без локального
исполнителя; текущий локальный исполнитель хранится в `local_used_by`.
Эти пространства пользователей нельзя автоматически связывать или переносить
с ними права. Пароли QR и хеши билетов сохраняются; SMTP-задания, токены писем,
JWT, refresh-токены и локальные аккаунты в пакет не входят.
Материал — тройка `name`, `image`, `file_type`; имя очищается штатным cleaner,
тип при отсутствии определяется штатным file storage. `image` указывает на
`files/...` исходного пакета игры, а не на адрес/ключ локального RustFS.
При сборке проверяется идентичность текущего игрового `story` исходному после
штатного переименования медиа при импорте, а также ID сценария и формат игры.
Изменённый сценарий и не связанные с ним материалы отклоняются. Музыка, карта,
экраны и оформление не возвращаются. Баллы не пересчитываются: сохраняются
выставленные значения, включая `0` и `null`, также в формате `offline`.
### Проверка и ограничения
Архив и распакованный объём ограничены 1 ГиБ; каждый из двух JSON — 8 МиБ
(фактический максимальный объём данных меньше общего предела). Проверяются
точный состав ZIP, обычные нешифрованные записи, отсутствие дублей, версия,
размеры, CRC и SHA-256, обязательные поля, исходные/локальные идентификаторы,
полнота удалений, уникальность хешей билетов, ссылки прогресса, материалы,
вопросы и порядок ходов. Неизвестные поля и лишние JSON-документы отклоняются.
Проверки кодека: `go test ./internal/services/game_results_package_service`.
Полный PostgreSQL/RustFS-flow: `python3 tests/venue/flow.py --results` из корня
workspace. Стенд проверяет оба формата, свежую оценку и конкурентные снимки,
сохранность прежнего ZIP, повторную подготовку после перезапуска и отсутствие
изменений исходных игр. Пакеты и тестовые ресурсы удаляются после прогона.
Применение онлайн, разрешение конфликтов и идемпотентность возврата — этап 10;
подготовка/скачивание и импорт через интерфейс — этап 11. До завершения возврата
используются только тестовые игры.