292 lines
33 KiB
Markdown
292 lines
33 KiB
Markdown
# Инвентаризация backend и справочный gap-analysis с `tgFlaskForm`
|
||
|
||
**Назначение.** §1–§9 — полная карта того, что **сейчас живёт** в `backend/` (Node.js / Express). Используется в [Этапе 1](migration-final.md#этап-1-текущий--единый-стек-express--flask-react--jinja-внутри-testingwebapp) как чек-лист «что переписать на Flask внутри `flask_app/`». §10 — **справочный gap-analysis** между этим и уже готовым модулем `cabinet/testing` в `HR_TG_Bot/tgFlaskForm` — пригодится в [Этапе 2](migration-to-tgflaskform.md) при слиянии.
|
||
|
||
**Связано:** [migration-final.md](migration-final.md) (главный трекер двух этапов), [migration-to-tgflaskform.md](migration-to-tgflaskform.md) (план Этапа 2 — слияние с tgFlaskForm), [PROJECT_STATUS.md](PROJECT_STATUS.md), [СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md](СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md).
|
||
|
||
> **Прочитайте сначала [migration-final.md](migration-final.md)** — там зафиксировано разделение на Этап 1 (унификация стека внутри TestingWebApp, БД остаётся `clinic_tests`) и Этап 2 (слияние с HR-кабинетом, БД мигрирует на `hr_bot_test`). Содержимое §10 этого документа — материал для Этапа 2.
|
||
|
||
---
|
||
|
||
## 1. Карта HTTP-эндпоинтов (всё, что зовёт фронтенд)
|
||
|
||
Источник: `backend/src/app.js`, `backend/src/routes/*.js`. Перепроверено по фактическим вызовам из `frontend/src/**` (`frontend/src/api.js` — общий `fetch` с `credentials: 'include'`).
|
||
|
||
| # | Метод | Путь | Зовёт из фронта | Auth | Сервис(ы) | Особенности |
|
||
|---|------|------|----------------|------|----------|-------------|
|
||
| 1 | GET | `/api/health` | — | нет | inline | smoke-проверка |
|
||
| 2 | POST | `/api/auth/login` | `Login.jsx` | нет | `utils/auth`, `db/db`, `db/hrPool`, `utils/werkzeugPassword`, `utils/hrRoleMap`, `config/authConstants` | bcrypt (dev) **+** Werkzeug (HR) ветки; UPSERT `users` по `staff_id`; кладёт JWT в HTTP-only cookie |
|
||
| 3 | POST | `/api/auth/logout` | `CabinetLayout.jsx` | нет | inline | очистить cookie |
|
||
| 4 | GET | `/api/auth/me` | `CabinetLayout.jsx` | cookie JWT | `middleware/auth` + `featureFlags` | возвращает пользователя + флаги UI (`devUi`, `assignmentUi`) |
|
||
| 5 | GET | `/api/auth/dev/assignment-directory` | `TestDetail.jsx` (assign-блок) | cookie JWT, feature-flag | `services/assignmentDirectoryService` | сливает HR (`staff_members`, `employees_departments`) + `clinic_tests.users`; query: `q`, `department`, `clinic` |
|
||
| 6 | POST | `/api/tests/import/document` | `TestDetail.jsx` («Документ в вопросы») | cookie JWT | `services/documentExtractService` (PDF/DOCX/TXT), `services/documentGenService` → `services/llmClient` | `multer` (10 МБ, OS tmpdir); удаляет файл после извлечения |
|
||
| 7 | GET | `/api/tests` | `TestsList.jsx` | cookie JWT | `services/testAccessService.queryTestsVisibleToUser` + inline (hiddenByYou) | каталог + список «скрытые мной» |
|
||
| 8 | POST | `/api/tests` | `TestsList.jsx` | cookie JWT | `services/testDraftService.createTestWithVersion` | создаёт цепочку + версию 1 |
|
||
| 9 | GET | `/api/tests/:id/summary` | `TestDetail.jsx` | cookie JWT | inline + `testAccessService.userHasTestAccess` | карточка цепочки (одна строка) |
|
||
| 10 | GET | `/api/tests/:id/versions` | `TestDetail.jsx` | cookie JWT, **только автор** | inline + `testChainService.hasAnyAttemptForTest` | список версий + флаг `hasAttempts` |
|
||
| 11 | GET | `/api/tests/:id/editor` | `TestDetail.jsx` | cookie JWT, **только автор** | `testAttemptService.getEditorContent` | вопросы активной версии **с правильными ответами** |
|
||
| 12 | POST | `/api/tests/:id/ai/generate-test` | `TestDetail.jsx` (ИИ — целиком) | cookie JWT, **только автор** | `aiEditorService.generateFullTestByShape` → `llmClient` | строгая сетка `shape: [{optionsCount, hasMultipleAnswers}]`; до 40 вопросов |
|
||
| 13 | POST | `/api/tests/:id/ai/generate-question` | `TestDetail.jsx` (ИИ — один вопрос) | cookie JWT, **только автор** | `aiEditorService.generateOrRephraseQuestion` → `llmClient` | пустой текст → новый вопрос; непустой → переформулировка |
|
||
| 14 | POST | `/api/tests/:id/versions/:vid/activate` | `TestDetail.jsx` | cookie JWT, **только автор** | inline | транзакция; снимает `is_active` со всех версий, потом ставит на `:vid` |
|
||
| 15 | PATCH | `/api/tests/:id` | `TestDetail.jsx` | cookie JWT, **только автор** | inline | `chainActive` (true/false) — публикация в каталоге |
|
||
| 16 | POST | `/api/tests/:id/assign` | `TestDetail.jsx` (назначение) | cookie JWT, **только автор**, feature-flag | `assignmentUserService.ensureClinicUserIdForStaff` | принимает `userIds[]`/`staffIds[]`/legacy `userId`/`staffId`; одна строка `test_assignments` + N строк `test_assignment_targets` |
|
||
| 17 | POST | `/api/tests/:id/draft` | `TestDetail.jsx` («Сохранить») | cookie JWT, **только автор** | `testDraftService.saveTestDraft` | если есть попытки и переданы `questions` — fork новой версии (V.3) |
|
||
| 18 | POST | `/api/tests/:id/attempts/start` | `TestsList.jsx` | cookie JWT (доступ через `userHasTestAccess`) | `testAccessService.userHasTestAccess` + inline | новая попытка по активной версии |
|
||
| 19 | GET | `/api/tests/:id/attempts` | `TestDetail.jsx` («Прохождения») | cookie JWT, **только автор** | `testAttemptService.listTestAttemptsForAuthor` | до 200 попыток по всем версиям |
|
||
| 20 | GET | `/api/tests/:id/attempts/:aid/review` | `TestAttemptReview.jsx` | cookie JWT (владелец **или** автор) | `testAttemptService.getAttemptReviewForUser` → `buildReviewFromDb` | разбор попытки |
|
||
| 21 | GET | `/api/tests/:id/attempts/:aid/play` | `TestAttempt.jsx` | cookie JWT (владелец) | `testAttemptService.getPlayContent` | вопросы **без** правильных ответов |
|
||
| 22 | POST | `/api/tests/:id/attempts/:aid/submit` | `TestAttempt.jsx` | cookie JWT (владелец) | `testAttemptService.submitAttempt` | `FOR UPDATE`, проверка ответов, перезапись `user_answers`, статус `completed` |
|
||
| 23 | GET | `/api/tests/:id/chain-info` | `TestDetail.jsx` | cookie JWT (через `userHasTestAccess`) | `testAccessService` + `testChainService` | флаг `hasAnyAttempt` |
|
||
|
||
> **Итого: 22 функциональных эндпоинта** (без `/api/health`). Все ответы — JSON. Все входы — JSON или `multipart/form-data` (только `/import/document`).
|
||
|
||
---
|
||
|
||
## 2. Сервисный уровень (что должно появиться в Flask)
|
||
|
||
| Файл (Node) | Что делает | Что нужно в Python |
|
||
|------------|-----------|---------------------|
|
||
| `services/testAccessService.js` | Запросы каталога; `userHasTestAccess(testId, userId)` | SQLAlchemy/`psycopg2` версия двух запросов |
|
||
| `services/testDraftService.js` | `createTestWithVersion`, `saveTestDraft`, `forkNewVersion`, `replaceVersionContent`, `copyQuestionTree` | Транзакции на `psycopg2`/SQLAlchemy; следить за частичным уникальным индексом `uq_test_versions_one_active_per_test` |
|
||
| `services/testAttemptService.js` | `getEditorContent`, `getPlayContent`, `submitAttempt`, `buildReviewFromDb`, `getAttemptReviewForUser`, `listTestAttemptsForAuthor`; вычисление баллов | Самый объёмный модуль; внимательно с массивами UUID (`user_answers.selected_options uuid[]`) |
|
||
| `services/testChainService.js` | `hasAnyAttemptForTest` | Один SQL `EXISTS` |
|
||
| `services/aiEditorService.js` | `parseAndValidateShape`, `generateFullTestByShape`, `generateOrRephraseQuestion`, `assertDraftMatchesShape` | Чистый Python; зависит от `llmClient` и `documentGenService.validate*` |
|
||
| `services/documentGenService.js` | `parseJsonFromLlmText`, `validateAndNormalizeDraft`, `generationForImportDocument` | Чистый Python (`json.loads` + валидация формы) |
|
||
| `services/documentExtractService.js` | `resolveDocumentKind`, `extractTextFromFile`, `extractTextFromBuffer` | **Замены пакетов:** `mammoth` → `python-docx` или `mammoth.py`; `pdf-parse` → `pypdf` (или `pdfminer.six`) |
|
||
| `services/llmClient.js` | OpenAI-совместимый Chat Completions, JSON-mode, таймаут 120 с | `httpx` / `requests` + явный `timeout` |
|
||
| `services/assignmentDirectoryService.js` | Слияние `clinic_tests.users` ↔ HR (`staff_members`, `employees_departments`); фильтры `q/department/clinic` | Два пула; `psycopg2` достаточно |
|
||
| `services/assignmentUserService.js` | `ensureClinicUserIdForStaff` (UPSERT по `staff_id`) | UPSERT с `ON CONFLICT (staff_id)` |
|
||
| `utils/auth.js` | bcrypt + JWT + (Werkzeug fallback) | `passlib[bcrypt]` или `bcrypt`; **`flask-jwt-extended`** или ручной `pyjwt` |
|
||
| `utils/werkzeugPassword.js` | `scrypt:$N:$r:$p$salt$hex`, `pbkdf2:sha256:iter$salt$hex` | Уже **родной** Python: `werkzeug.security.check_password_hash` |
|
||
| `utils/hrRoleMap.js` | строка HR-роли → `'hr'/'manager'/'employee'` | Один `def map_hr_role()` |
|
||
| `middleware/auth.js` | `authenticate`, `requireRole`, `requireDepartment`, `optionalAuth` | Flask-декоратор `@login_required`/`@roles_required` (или `before_request`) |
|
||
| `config/featureFlags.js` | `isAssignmentFeatureEnabled` | Простая функция от env |
|
||
| `config/devAuthor.js` | `isTestAuthor(createdBy, userId)` | Один сравнительный helper |
|
||
| `config/authConstants.js` | `HR_MANAGED_PASSWORD_PLACEHOLDER`, `isHrAuthEnabled` | Без изменений |
|
||
| `messages/ru.js` | Текстовые сообщения API | `app/messages/ru.py` (dict) |
|
||
|
||
> **Особое внимание:** `testAttemptService.submitAttempt` использует `FOR UPDATE` и выгружает все `answer_options` версии разом. Простую построчную проверку «правильно/нет» делает Python-функция `same_selection(set, set)`. На больших тестах помогает индекс `idx_answer_options_question_id` — он уже есть.
|
||
|
||
---
|
||
|
||
## 3. Базы данных и схемы
|
||
|
||
### 3.1 Основная — `clinic_tests` (`DATABASE_URL`)
|
||
|
||
Из `backend/src/db/migrations/001_initial.sql`:
|
||
|
||
- `departments` (UUID, name)
|
||
- `users` (UUID, login UNIQUE, password_hash, full_name, role `user_role`, department_id FK, is_active, **`staff_id`** — миграция 003, **UNIQUE**)
|
||
- `tests` (UUID, title, description, passing_threshold, time_limit, allow_back, is_active, is_versioned, created_by FK)
|
||
- `test_versions` (UUID, test_id FK, version, is_active; миграция 002 добавляет `parent_id` + частичный уникальный индекс «одна активная версия на цепочку»)
|
||
- `questions` (UUID, test_version_id FK, text, question_order, has_multiple_answers)
|
||
- `answer_options` (UUID, question_id FK, text, is_correct, option_order)
|
||
- `test_assignments` (UUID, test_version_id FK, assigned_by FK, deadline DATE, max_attempts)
|
||
- `test_assignment_targets` (UUID, assignment_id FK, target_type `'department'|'user'`, target_id UUID)
|
||
- `test_attempts` (UUID, test_version_id FK, user_id FK, attempt_number, status `attempt_status`, started_at, completed_at, correct_count, total_questions, passed; UNIQUE(test_version_id, user_id, attempt_number))
|
||
- `user_answers` (UUID, attempt_id FK, question_id FK, selected_options **UUID[]**)
|
||
- `migrations` (служебная, имена применённых SQL)
|
||
|
||
**Расширения:** `uuid-ossp`. **Кастомные типы:** `user_role`, `target_type`, `attempt_status`.
|
||
|
||
### 3.2 Дополнительная — `hr_bot_test` (`HR_DATABASE_URL`, опционально)
|
||
|
||
Используется только на чтение через `db/hrPool.js`:
|
||
|
||
- `users` — для входа (HR_AUTH=1): `id`, `username`, `password_hash` (Werkzeug), `role`
|
||
- `staff_members` — `id`, `fio`, `web_login`
|
||
- `employees_departments` — `staff_id`, `department`
|
||
|
||
### 3.3 Миграции
|
||
|
||
3 SQL-файла в `backend/src/db/migrations/`. Простой `npm run migrate` пишет в служебную таблицу `migrations`. В Flask эквивалент: **Alembic** или ручной runner на `psycopg2`. Файлы `.sql` можно переиспользовать **как есть** — никакого Node-специфичного синтаксиса в них нет.
|
||
|
||
---
|
||
|
||
## 4. Зависимости Node → Python (план замены)
|
||
|
||
| Node-пакет | Python-замена | Комментарий |
|
||
|-----------|----------------|-------------|
|
||
| `express`, `cors`, `cookie-parser` | **Flask** + `flask-cors` | сессии встроены |
|
||
| `multer` | `Flask` (`request.files`) + `werkzeug.utils.secure_filename` + `tempfile` | лимит 10 МБ — `MAX_CONTENT_LENGTH` |
|
||
| `dotenv` | `python-dotenv` (уже есть в `flask_app/run.py`) | — |
|
||
| `pg` | `psycopg2-binary` (или `psycopg[binary]` v3) | — |
|
||
| `bcryptjs` | `bcrypt` (`pip install bcrypt`) | формат `$2b$…` совместим |
|
||
| `jsonwebtoken` | `pyjwt` или `flask-jwt-extended` | важен **тот же** алгоритм/секрет, чтобы старые cookie работали в переходный период |
|
||
| `mammoth` | `mammoth` (PyPI: `pip install mammoth`) или `python-docx` | API почти идентичен |
|
||
| `pdf-parse` | `pypdf` (`pip install pypdf`) или `pdfminer.six` | `pypdf.PdfReader().pages[].extract_text()` |
|
||
| `fetch` (LLM) | `httpx` (рекомендую — есть `timeout`, async) или `requests` | сохранить `Authorization: Bearer …`, `response_format: json_object` |
|
||
|
||
**Уже в `tgFlaskForm` есть готовое** — `werkzeug.security.check_password_hash` (бесплатно), Sentry, Jinja2-шаблоны, обмен с `staff_members`. Переиспользовать, если выберем сценарий «общий кабинет».
|
||
|
||
---
|
||
|
||
## 5. Переменные окружения (полный список)
|
||
|
||
| Переменная | Где читается | Назначение | Обязательная |
|
||
|-----------|--------------|-----------|--------------|
|
||
| `NODE_ENV` | `app.js`, `auth.js`, `featureFlags.js`, `db.js` | dev vs production | да (нет → dev) |
|
||
| `PORT` | `server.js` | Express, по умолчанию 3001 | нет |
|
||
| `FRONTEND_URL` | `app.js` (CORS) | разрешённый origin в prod | в prod — да |
|
||
| `DATABASE_URL` **или** `DB_HOST`/`DB_PORT`/`DB_NAME`/`DB_USER`/`DB_PASSWORD` | `db/poolConfig.js` | основная БД `clinic_tests` | да |
|
||
| `DB_POOL_MAX`, `DB_IDLE_TIMEOUT`, `DB_CONNECTION_TIMEOUT` | `db/poolConfig.js` | пул | нет |
|
||
| `HR_DATABASE_URL` | `db/poolConfig.js` (`getHrPoolConfig`) | hr_bot_test (read-only) | при HR_AUTH=1 — да |
|
||
| `HR_DB_POOL_MAX` | то же | пул HR | нет |
|
||
| `HR_AUTH` (1/true) | `config/authConstants.js` | вход по HR-логину | нет |
|
||
| `JWT_SECRET` | `utils/auth.js` | подпись JWT | **да** |
|
||
| `JWT_EXPIRES_IN` | `utils/auth.js` | дефолт `7d` | нет |
|
||
| `DEEPSEEK_API_KEY` | `services/llmClient.js` | LLM, приоритет №1 | для AI/импорта — да |
|
||
| `OPENAI_API_KEY` | то же | LLM, приоритет №2 | альтернатива |
|
||
| `LLM_BASE_URL` | то же | сменить хост (proxy / vLLM) | нет |
|
||
| `LLM_MODEL` | то же | по умолчанию `deepseek-chat` или `gpt-4o-mini` | нет |
|
||
| `LLM_NO_JSON` (1) | то же | отключить `response_format: json_object` (для моделей без поддержки) | нет |
|
||
| `CLINIC_ASSIGNMENT_ENABLED` (1) | `config/featureFlags.js` | прод: включить assign | в prod — да |
|
||
|
||
В `flask_app/.env` нужно перенести **те же ключи** (имена можно сохранить, чтобы не плодить вариации).
|
||
|
||
---
|
||
|
||
## 6. Что вызывает фронтенд (карта зависимостей React → API)
|
||
|
||
Используется один тонкий клиент `frontend/src/api.js`: `fetch` с `credentials: 'include'`, базовый путь — пустой (т.е. **`/api/...` относительно текущего origin**, что в dev резолвит Vite-proxy, а в prod — Nginx). Это значит:
|
||
|
||
- **Менять фронтенд при смене бэкенда не нужно**, если новый сервис отвечает по тем же путям.
|
||
- В dev сейчас `vite.config.js` проксирует `/api` на Express (`localhost:3001`). После переноса — заменить адрес/порт на Flask (см. `flask_app/run.py`, по умолчанию `3107`).
|
||
|
||
Список путей (отсортирован по убыванию использований):
|
||
|
||
```
|
||
/api/auth/login
|
||
/api/auth/logout
|
||
/api/auth/me
|
||
/api/auth/dev/assignment-directory?q&department&clinic
|
||
/api/tests
|
||
/api/tests/:id/summary
|
||
/api/tests/:id/versions
|
||
/api/tests/:id/editor
|
||
/api/tests/:id/chain-info
|
||
/api/tests/:id/draft
|
||
/api/tests/:id/assign
|
||
/api/tests/:id/ai/generate-test
|
||
/api/tests/:id/ai/generate-question
|
||
/api/tests/:id (PATCH)
|
||
/api/tests/:id/versions/:vid/activate
|
||
/api/tests/:id/attempts
|
||
/api/tests/:id/attempts/start
|
||
/api/tests/:id/attempts/:aid/play
|
||
/api/tests/:id/attempts/:aid/review
|
||
/api/tests/:id/attempts/:aid/submit
|
||
/api/tests/import/document
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Тесты, которые тянутся за собой
|
||
|
||
В `backend/src/**/*.test.js` (Node test runner):
|
||
|
||
- `apiSmoke.test.js` — smoke-проверки HTTP.
|
||
- `services/testChainService.test.js`
|
||
- `services/aiEditorService.test.js`
|
||
- `services/documentGenService.test.js`
|
||
- `services/documentExtractService.test.js`
|
||
- `utils/werkzeugPassword.test.js`
|
||
- `integration/v9card1.test.js` (требует `CLINIC_TESTS_INTEGRATION=1`)
|
||
|
||
После переноса нужны их Python-аналоги (`pytest`). Часть (валидация LLM-формы, разбор Werkzeug) тривиально превращается в юниты.
|
||
|
||
---
|
||
|
||
## 8. Чего сейчас в `flask_app/` НЕТ (чтобы не повторяться в Спринте 2)
|
||
|
||
`flask_app/` содержит только: `run.py`, `app/__init__.py` (с `/health` и `/`), `app/templates/`, `app/static/`. Не реализовано **ничего** из перечня §1–§4. Это значит, что в Спринте 2 нужно с нуля поднять:
|
||
|
||
1. Подключение к БД (минимум — основной пул `clinic_tests`).
|
||
2. Декоратор аутентификации (cookie JWT) и эндпоинты `/api/auth/*`.
|
||
3. Роуты `/api/tests/*` поэтапно — начать с **read-only** (list, summary, versions, editor, attempts, review), потом write (draft, activate, patch, assign, attempts/start/submit), потом AI/import.
|
||
4. CORS (в dev совпадает с тем, что у Express).
|
||
5. Запуск под Vite/Nginx — обновить proxy/upstream.
|
||
|
||
---
|
||
|
||
## 9. Критерии «можно удалять `backend/`» (Спринт 4)
|
||
|
||
- ETL-скрипт `HR_TG_Bot/tgFlaskForm/tools/migrate_clinic_tests_to_hr.py` прогнан **на копии прод**, агрегаты совпадают (тесты, версии, вопросы, попытки) — затем прогон на проде в окне.
|
||
- Все сценарии §1 (TestingWebApp) либо имеют **рабочий аналог** в `cabinet/testing/`, либо явно **признаны не-MVP** (см. §10).
|
||
- Внутренние ссылки и закладки сотрудников переключены на URL общего кабинета (`/cabinet/testing/...`).
|
||
- Документация (`README.md`, `DEV_CONTOUR_USER_GUIDE.md`, `шаги/*`) перестала ссылаться на `backend/` и `frontend/` TestingWebApp.
|
||
- Резерв: ветка `legacy/clinic-tests-node` (или `legacy/express-backend`) зафиксирована перед удалением.
|
||
|
||
---
|
||
|
||
## 10. Gap-analysis: `tgFlaskForm/cabinet/testing` vs Express
|
||
|
||
Сверка проведена по фактическому коду `HR_TG_Bot/tgFlaskForm/webApp/interfaces/testing/*` и `HR_TG_Bot/tgFlaskForm/db/queries/testing_queries.py` на 2026-04-27. Колонка «Что нужно сделать» — это и есть остаток Спринта 2 из [migration-to-tgflaskform.md](migration-to-tgflaskform.md).
|
||
|
||
### 10.1 Маппинг сущностей и ID
|
||
|
||
| TestingWebApp (`clinic_tests`) | tgFlaskForm (`hr_bot_test`, схема `testing_*`) | Замечание |
|
||
|---|---|---|
|
||
| `tests` (UUID) | `testing_tests` (Integer PK) | UUID → integer; маппинг хранится в `_clinic_tests_migration_map` |
|
||
| `test_versions` (UUID, `version`, `is_active`, `parent_id`) | `testing_test_versions` (Integer, `version_number`, `is_active_version`) — **+** `passing_score_percent`, `time_limit_minutes`, `allow_back_navigation` (нормализовано на версию) | В Express часть полей лежала на `tests` (`passing_threshold`, `time_limit`, `allow_back`); ETL поднимает их на версию |
|
||
| `questions` (`text`, `has_multiple_answers`, `question_order`) | `testing_questions` (`question_text`, `question_type` `'single'\|'multiple'`, `sort_order`) | `has_multiple_answers: true` ↔ `question_type='multiple'` |
|
||
| `answer_options` (`text`, `is_correct`, `option_order`) | `testing_answers` (`answer_text`, `is_correct`, `sort_order`) | прямой маппинг |
|
||
| `users` + `users.staff_id` | `staff_members.id` напрямую | Express держит «свою» строку `users` со ссылкой на `staff_id`; в HR — только `staff_members` |
|
||
| `test_assignments` + `test_assignment_targets` (target_type='user', target_id=UUID) | `testing_assignments` (одна строка = одна пара тест×сотрудник, `assigned_to=staff_id`) | **Существенное расхождение модели** — см. §10.3 |
|
||
| `test_attempts` (привязан к `test_version_id`, `user_id`) | `testing_attempts` (привязан к `assignment_id` + `test_version_id`, попытка от **сотрудника**) | Если в clinic попытка не имеет связанного assignment — ETL создаёт синтетическое назначение (`max_attempts=99`) |
|
||
| `user_answers` (`selected_options uuid[]`) | `testing_attempt_answers` (одна строка на каждый выбранный `answer_id`) | Развёртка массива в N строк |
|
||
| (нет аналога) | `testing_settings` (key-value), `testing_head_positions` (право назначения по должности) | в Express не было — RBAC проще |
|
||
|
||
### 10.2 Эндпоинты Express → роуты `tgFlaskForm`
|
||
|
||
| # | Express | Соответствие в `tgFlaskForm` | Статус | Что сделать |
|
||
|---|---------|------------------------------|--------|-------------|
|
||
| 1 | `POST /api/auth/login` | `webApp/auth/*` (общая сессия HR) | ✅ есть аналог | Express-сессии не переносим; пользователи входят как обычно в HR-кабинет |
|
||
| 2 | `POST /api/auth/logout` | `webApp/auth/*` | ✅ — | — |
|
||
| 3 | `GET /api/auth/me` | session + `helpers._get_staff_id` | ✅ — | — |
|
||
| 4 | `GET /api/auth/dev/assignment-directory` | `routes_assignments.assign_form` (страница) + `testing_get_employees_for_assignment` | ✅ — | На UI: добавить «Выбрать всех» (см. Спринт 3 React-кабинета), мульти-фильтры |
|
||
| 5 | `GET /api/health` | `webApp/__init__.py` | ✅ — | — |
|
||
| 6 | `GET /api/tests` (каталог + `hiddenByYou`) | `routes_tests.test_list` (по автору) + `routes_passing.my_tests` (по назначениям) | ⚠️ нет «скрытые мной» отдельным разделом | Добавить вкладку/фильтр «скрытые автором» в `cabinet/testing/test_list.html` |
|
||
| 7 | `POST /api/tests` (создать пустой) | `routes_tests.test_create` (требует full payload + ≥7 вопросов) | ⚠️ другой контракт | Решить продуктово: оставить ограничение `≥7` либо разрешить «пустой тест» как в Express. **Рекомендую** разрешить пустой и валидировать только при публикации |
|
||
| 8 | `GET /api/tests/:id/summary` | вшито в `test_edit` | ⚠️ нет отдельной summary | Добавить функцию `testing_get_test_summary(test_id)` если нужен лёгкий вариант для списка |
|
||
| 9 | `GET /api/tests/:id/versions` | `testing_get_test_for_edit` отдаёт активную; история — внутри `test_edit` | ⚠️ — | Вынести список версий в API/шаблон («История» в UX) |
|
||
| 10 | `GET /api/tests/:id/editor` | `routes_tests.test_edit` + `testing_get_test_for_edit` | ✅ — | сверить набор полей, отдаваемых шаблону, с тем что показывает React-редактор |
|
||
| 11 | `POST /api/tests/:id/ai/generate-test` (строгая `shape: [{optionsCount, hasMultipleAnswers}]`, до 40 вопросов) | `routes_ai.ai_generate` принимает `topic, question_count` (фикс. форма) | ❌ **gap** | Добавить вариант с явной формой (передавать массив `optionsCount/hasMultipleAnswers`); либо принять, что HR-кабинет генерирует «обобщённо» |
|
||
| 12 | `POST /api/tests/:id/ai/generate-question` (один вопрос: пусто=новый, текст=переформулировать) | `routes_ai.ai_improve` (улучшить) + `ai_distractors` (варианты) | ❌ **gap** | Добавить ручку «один вопрос со строгой сеткой» (объединить `improve` + `distractors` под единый сценарий) |
|
||
| 13 | `POST /api/tests/:id/versions/:vid/activate` | `routes_tests.test_version_activate` | ✅ — | — |
|
||
| 14 | `PATCH /api/tests/:id` (`chainActive`) | `routes_tests.test_toggle` | ✅ — | сверить семантику (toggle vs явный флаг) |
|
||
| 15 | `POST /api/tests/:id/assign` (массивы `userIds`/`staffIds`) | `routes_assignments.assign_submit` (`employee_ids = staff_id[]`) | ✅ совместимо | — |
|
||
| 16 | `POST /api/tests/:id/draft` (с авто-fork версии) | `routes_tests.test_update` (тоже фаркает при `has_attempts`) | ⚠️ контракт другой | Сверить, что в HR-кабинете тоже работает «правка без попыток = на месте, после попыток = новая версия». В коде есть, но нужна проверка |
|
||
| 17 | `POST /api/tests/:id/attempts/start` | `routes_passing.start_attempt(assignment_id)` | ⚠️ другой ключ | Привести к виду «start by test_id» **или** оставить как есть и в UI стартовать по `assignment_id` (логичнее) |
|
||
| 18 | `GET /api/tests/:id/attempts` | `routes_results.tracker` (общая лента) — без фильтра по тесту | ⚠️ нет per-test ленты | Добавить фильтр `test_id` или отдельный URL `/cabinet/testing/tests/:id/attempts` |
|
||
| 19 | `GET /api/tests/:id/attempts/:aid/review` | `routes_results.result(attempt_id)` | ✅ — | сверить, что разбор показывает выбранные/верные варианты по каждому вопросу |
|
||
| 20 | `GET /api/tests/:id/attempts/:aid/play` | `routes_passing.take_test(attempt_id)` | ✅ — | — |
|
||
| 21 | `POST /api/tests/:id/attempts/:aid/submit` | `routes_passing.finish_attempt(attempt_id)` (+ `save_answer`) | ⚠️ контракт другой | Express отправляет все ответы пакетом; HR — пошагово (`save_answer` × N + `finish`). Это **OK для UX** (продолжить позже), но фронт работает иначе |
|
||
| 22 | `POST /api/tests/import/document` | `routes_import.import_document` | ✅ — | сверить mime-типы и сообщения об ошибках |
|
||
| 23 | `GET /api/tests/:id/chain-info` (`hasAnyAttempt`) | вшито в `testing_get_test_for_edit['has_attempts']` | ✅ — | — |
|
||
|
||
> **Итог по gap-analysis:** 13 из 22 эндпоинтов закрыты «как есть» или близко; 4 требуют добавления (AI «один вопрос», AI «строгая сетка», per-test attempts, summary); 5 — расхождение контракта, надо принять решение или подровнять.
|
||
|
||
### 10.3 Расхождения, которые ETL уже обходит
|
||
|
||
- **Назначения на отдел:** в `clinic_tests.test_assignment_targets` могут быть строки `target_type='department'`. ETL разворачивает их в N строк `testing_assignments` по списку сотрудников отдела на момент миграции (см. шапку `tools/migrate_clinic_tests_to_hr.py`).
|
||
- **Попытки без явного назначения:** в Express `test_attempts.user_id` напрямую идёт от пользователя; в HR попытка обязательно имеет `assignment_id`. ETL для каждой пары (тест, сотрудник) создаёт **синтетическое** назначение `max_attempts=99` и привязывает попытку к нему.
|
||
- **UUID → Integer:** идемпотентный маппинг лежит в `public._clinic_tests_migration_map` (entity, old_uuid → new_id) — повторный прогон ETL безопасен.
|
||
|
||
### 10.4 Чего точно нет в `tgFlaskForm` и потребует доработки
|
||
|
||
1. **AI «один вопрос со строгой сеткой»** (`generate_or_rephrase_question` из `aiEditorService.js`).
|
||
2. **AI «целый тест по shape»** с проверкой `optionsCount` и `hasMultipleAnswers` для каждой строки.
|
||
3. **«Скрытые автором» цепочки** как отдельный список на UI.
|
||
4. **`/tests/:id/summary`** — лёгкая ручка для карточки в списке (если потребуется в новом UI).
|
||
5. **Per-test лента попыток** (UI «Прохождения» в аккордеоне «История»).
|
||
6. **Мобильный UX из [Спринта 3](СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md):** аккордеоны «О тесте / Вопросы / История / Показ в каталоге», фикс-футер, икон-кнопка удаления варианта, «Выбрать всех» в назначении и т.д. В Этапе 1 это переносится в Jinja-шаблоны `flask_app/app/templates/`. В Этапе 2 уже готовые шаблоны переезжают в `tgFlaskForm/webApp/templates/cabinet/testing/`.
|
||
|
||
### 10.5 Что точно НЕ переносим
|
||
|
||
- **Свой JWT/cookie/bcrypt** из `backend/src/utils/auth.js` и `middleware/auth.js`. Авторизация — через сессии общего кабинета.
|
||
- **Werkzeug-полифилл в Node** (`utils/werkzeugPassword.js`). В Python это родная функция `werkzeug.security.check_password_hash`.
|
||
- **Свои миграции SQL** из `backend/src/db/migrations/`. Целевая схема живёт в `tgFlaskForm/db/models.py` и сопутствующих DDL/Alembic.
|
||
- **`flask_app/`** в этом репозитории — каркас уже **не нужен**, в Спринте 4 решим: удалить или оставить как историческую заготовку.
|
||
- **`backend/src/**/*.test.js`** (Node test runner). Их предметная нагрузка покрывается **либо** уже существующими тестами `tgFlaskForm`, **либо** новыми pytest-юнитами.
|