Рефакторинг backend/frontend: слои, оптимизация конвейера, тесты и CI - #12
Open
Gaudialis55 wants to merge 21 commits into
Open
Рефакторинг backend/frontend: слои, оптимизация конвейера, тесты и CI#12Gaudialis55 wants to merge 21 commits into
Gaudialis55 wants to merge 21 commits into
Conversation
added 21 commits
July 11, 2026 13:38
Полный перечень найденных проблем (31 пункт) с файл:строка, категорией и серьёзностью. Ключевые critical: DELETE→500 из-за FK без каскада, падение сборки frontend (COPY .env.production), тройной конвейер Celery. Baseline снят запуском проекта и сквозным прогоном сценария.
Послойное разделение (core/storage/repositories/services/api/tasks), единый конфиг и engine, неблокирующий I/O. Отдельно описана неочевидная оптимизация (конвейер 3 задачи -> 1: было/стало/эффект) и осознанное изменение DELETE (500 -> 204 через ON DELETE CASCADE).
- core/config.py: типизированный Settings как единый источник настроек вместо голого os.environ (устраняет молчаливый None в DB_URL) - core/database.py: единственный engine + async_sessionmaker, get_session (DI) и session_scope (для воркера) - service.py/tasks.py/migrations перестают дублировать engine и URL - брокер Celery берётся из CELERY_BROKER_URL (был рассинхрон с REDIS_URL) Закрывает AUDIT: A1, C1, C2, M2.
- storage.py: FileStorage поверх anyio (без новой зависимости); запись загрузки идёт чанками и не держит файл целиком в памяти, чтение/удаление/проверка вынесены в поток и не блокируют event loop - service/app/tasks больше не трогают pathlib и диск напрямую - удалён мёртвый get_file_path (его логику дублировал роутер download) Закрывает AUDIT: I1, I2, I3, M1 (частично A4).
- repositories/ (File, Alert): весь доступ к данным инкапсулирован - services/ (FileService, AlertService): бизнес-логика поверх репозиториев и хранилища; вместо HTTPException — доменные исключения (exceptions.py) - api/ (deps, files, alerts): тонкие роутеры, сессия и сервисы через DI - app.py -> create_app(): middleware, роутеры, обработчики исключений (коды 400/404 и detail-сообщения сохранены 1:1) - удалён service.py и мёртвый create_alert Закрывает AUDIT: A2, A3, A4, A5, A6.
Главная неочевидная оптимизация. Линейный конвейер (без ветвления и параллелизма) был разбит на 3 Celery-задачи: 3 обращения к брокеру, 3 сессии, 3 выборки одной записи по PK на файл. Теперь это одна задача process_file поверх ProcessingService: 1 брокер, 1 сессия, 1 выборка (объект переиспользуется, expire_on_commit=False), переходы статусов сохранены коммитами. Поведение конвейера 1:1. Также починен воркер: engine создаётся в том же event loop, что и выполняет корутины, и пересоздаётся вместе с ним (исключает 'Future attached to a different loop'). Закрывает AUDIT: P1, P2, D3.
Отдельная ревизия добавляет индексы: - files.created_at и alerts.created_at (обе выборки сортируют по created_at DESC) - alerts.file_id (Postgres не индексирует FK автоматически) Модели помечены index=True для согласованности со схемой. Закрывает AUDIT: D1, D2.
Осознанное изменение поведения (задокументировано в ARCHITECTURE.md). Раньше DELETE обработанного файла падал с 500 (ForeignKeyViolationError), т.к. FK не имел каскада, а у файла после конвейера всегда есть алерт. Теперь удаление файла каскадно удаляет его алерты и возвращает 204 — как и предполагает контракт DELETE. 500 не был частью контракта. Закрывает AUDIT: B1.
Оба сервиса используют брокер Redis, но depends_on его не включал — порядок старта относительно redis был не гарантирован. Закрывает AUDIT: C5.
- удалены артефакты не по теме: Dockerfile.bun, app.json (Cloud Run), public/vercel.svg, boilerplate README.md - Dockerfile: убран COPY несуществующего .env.production (ломал сборку) - favicon перенесён в src/app/ (файловая конвенция Next) — теперь корректно отдаётся по /test/favicon.ico с учётом basePath Закрывает AUDIT: B2, B3, F2, F3, F4, F5.
Монолитный page.tsx (~367 строк) разбит на слои: - domain/types: общие типы FileItem, AlertItem - lib/api: типизированный API-клиент, базовый URL из NEXT_PUBLIC_API_BASE_URL (был захардкожен http://localhost:8000) - lib/format, lib/status: форматтеры и маппинг статусов в варианты - hooks/useDashboard, hooks/useFileUpload: загрузка, состояния, мутации - components/FilesTable, AlertsTable, UploadModal: презентационные компоненты - page.tsx: тонкий контейнер, собирающий хуки и компоненты Внешний вид и поведение сохранены. Закрывает AUDIT: F1, F6.
13 тестов на pytest + httpx (ASGITransport), реальный Postgres: - ProcessingService: clean/suspicious/большой файл/pdf/mime-mismatch/failed - API: 201/400/404, list, patch, download, каскадное удаление, скрытие stored_name dev-зависимости вынесены в отдельную группу (в прод-образ не попадают).
Сохранены исходные команды запуска; добавлены ссылки на AUDIT.md и ARCHITECTURE.md, описание слоёв бэкенда и инструкция по тестам.
- lifespan в create_app с await engine.dispose() на shutdown — корректное закрытие пула соединений (ранее engine не освобождался) - настройки обёрнуты в @lru_cache get_settings() (тестируемость) - новый GET /health с пингом БД (200 ok / 503 unavailable) — readiness-проба, публичный контракт не затронут По мотивам замечания ревьюера про жизненный цикл БД и практики PR sputnik-llc#6/sputnik-llc#8.
Логика сканирования вынесена из ProcessingService._scan в чистую функцию evaluate_scan (scan_rules.py) — тестируется изолированно, поведение идентично. Добавлен юнит-тест на правила (6 кейсов).
- ruff добавлен в dev-зависимости, настроен в pyproject (E/F/I/UP/B, line-length 100; B008 игнорируется в api/ — идиома FastAPI) - отсортированы импорты, разбиты длинные строки, применён ruff format - поведение кода не изменено Инженерная гигиена по мотивам PR sputnik-llc#6/sputnik-llc#7.
Добавлен Vitest + тесты на чистую логику фронтенда: - lib/format: formatSize (границы B/KB/MB), formatDate - lib/status: маппинг уровней и статусов в bootstrap-варианты Скрипт npm test. Сборка next build не затронута (тесты вне бандла). Закрывает пробел из сравнения с PR sputnik-llc#6/sputnik-llc#5.
- .github/workflows/ci.yml: линт (ruff) + pytest бэкенда (сервис Postgres), npm test (Vitest) + next build фронтенда — на каждый push/PR - .pre-commit-config.yaml: ruff (lint + format) для backend/ Инженерная оснастка по мотивам PR sputnik-llc#6/sputnik-llc#7.
- .env.dev вынесен в .gitignore (локальные секреты не коммитятся) - добавлен шаблон .env.dev.example (пароль-плейсхолдер) - в README добавлен шаг: cp .env.dev.example .env.dev перед запуском
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Что сделано
Рефакторинг без изменения бизнес-логики и публичного контракта API (единственное осознанное отклонение — поведение
DELETE, см. ниже). Каждое решение задокументировано, история — маленькими логичными коммитами поверх исходного состояния.Документы:
AUDIT.md— 31 найденная проблема (файл:строка, категория, серьёзность, статус обработки).ARCHITECTURE.md— целевая архитектура, обоснование решений и разбор неочевидной оптимизации (было → стало → эффект).Бэкенд — послойная архитектура:
core(конфигурация на pydantic-settings + единый async-engine с DI,lifespan/dispose),storage(неблокирующий потоковый I/O на anyio),repositories,services(доменные исключения вместоHTTPException),api(тонкие роутеры + DI),tasks, app factory. Устранены дублирование engine, разрозненныйos.environ, блокирующий I/O.Неочевидная оптимизация: линейный конвейер
scan → metadata → alertиз трёх Celery-задач объединён в одну — 1 обращение к брокеру / 1 сессия / 1 выборка записи по PK вместо 3. Поведение конвейера сохранено 1:1.БД: индексы под сортировки (
created_at) и внешний ключ (alerts.file_id);ON DELETE CASCADEдля алертов.Фронтенд: монолитный
page.tsx(~367 строк) разнесён на слои — типы, API-клиент (base URL из env), хуки (useDashboard,useFileUpload), презентационные компоненты, утилиты форматирования.Тесты и оснастка: pytest (конвейер обработки + контракт API + scan-правила), Vitest (форматтеры/статусы фронта), health-эндпоинт
GET /health, линтер и форматтер ruff, CI (GitHub Actions), pre-commit.Осознанное изменение поведения
DELETE /files/{id}для обработанного файла раньше падал с 500 (ForeignKeyViolationError— FK без каскада), поэтому обработанный файл (у которого всегда есть алерт) удалить было нельзя. Теперь FK пересоздан сON DELETE CASCADE, удаление возвращает 204 и каскадно убирает алерты — как и предполагает контрактDELETE. 500 не был частью контракта.Проверка
Проект поднимается с нуля (
docker compose -f docker-compose.dev.yml up+docker exec -it backend alembic upgrade head), сквозной сценарий (текст/pdf/exe/файл >10 МБ/пустой) даёт тот же результат, что и baseline; фронт открывается на/test, бэк — на/docs. Тесты зелёные.