Руководство разработчика¶
Содержание¶
- Начало работы
- Структура проекта
- Архитектура
- Стандарты кодирования
- Работа с Git
- Тестирование
- Отладка
- Добавление новых функций
Начало работы¶
Предварительные требования¶
- Node.js 18.x или выше
- PostgreSQL 14.x или выше
- Git
- Редактор кода (рекомендуется VS Code)
Настройка окружения разработки¶
-
Клонирование репозитория
git clone <repository-url> cd library_systemISPRAV-main -
Установка зависимостей
# Frontend npm install # Backend cd server npm install cd .. -
Настройка переменных окружения
cp server/env.example server/.env cp env.example .env
Отредактируйте .env файлы с настройками для разработки.
- Настройка базы данных
cd server npm run migrate
Для чистой установки справочники подставляет миграция 030_seed_reference_data.sql.
Дополнительно (идемпотентно): node scripts/seed-reference-data.js.
- Запуск в режиме разработки
# Терминал 1 - Backend cd server npm run dev # Терминал 2 - Frontend npm run dev
Структура проекта¶
Frontend (src/)¶
src/
├── components/ # React компоненты
│ ├── dialogs/ # Модальные окна
│ ├── ui/ # Базовые UI компоненты (shadcn/ui)
│ └── ... # Другие компоненты
├── pages/ # Страницы приложения
├── hooks/ # Custom React hooks
├── lib/ # Утилиты и конфигурация
│ ├── api.ts # API клиент
│ ├── constants.ts # Константы
│ └── utils.ts # Вспомогательные функции
└── types/ # TypeScript типы
Backend (server/)¶
server/
├── config/ # Конфигурация
│ ├── database.js # Настройки БД
│ ├── env.js # Валидация переменных окружения
│ └── ldap.js # Настройки LDAP
├── models/ # Sequelize модели (Domain Layer)
├── repositories/ # Слой доступа к данным
│ ├── BaseRepository.js
│ ├── OperationQueryRepository.js
│ ├── OperationCommandRepository.js
│ └── ...
├── services/ # Бизнес-логика (Application Layer)
│ ├── operations/
│ └── ...
├── routes/ # API маршруты (Presentation Layer)
├── middleware/ # Express middleware
├── validators/ # Валидация данных
├── dto/ # Data Transfer Objects
├── utils/ # Утилиты
├── migrations/ # SQL миграции
└── scripts/ # Вспомогательные скрипты
Архитектура¶
Многослойная архитектура¶
Система следует принципам Layered Architecture:
- Presentation Layer (Routes)
- Обработка HTTP запросов
- Валидация входных данных
- Формирование ответов
-
НЕ содержит бизнес-логику
-
Application Layer (Services)
- Бизнес-логика
- Оркестрация операций
- Валидация бизнес-правил
-
Координация между репозиториями
-
Data Access Layer (Repositories)
- Доступ к данным
- Запросы к БД
-
Маппинг данных
-
Domain Layer (Models)
- Модели данных
- Валидация на уровне модели
CQRS Pattern¶
Для операций используется паттерн Command Query Responsibility Segregation:
- OperationQueryRepository - методы чтения (queries)
- OperationCommandRepository - методы изменения (commands)
- OperationRepository - фасад, объединяющий оба репозитория
Outbox Pattern¶
Для надёжной обработки очередей бронирований используется синхронное продвижение + Outbox Pattern:
- В той же транзакции, что и возврат/отмена брони — запись в
outbox_events(CopyBecameAvailable) - После commit —
BookingService.processQueueForBook(bookCopyId)(возврат и отмена) - Lazy expiration —
cancelExpiredPendingForCopyпри бронировании, постановке в очередь иprocessQueueForBook - Встроенный воркер
server/workers/outboxWorker.jsили контейнер outbox-worker (резерв) outboxProcessor.js→ повторныйprocessQueueForBookдля необработанных событий- После возврата:
ReturnService→triggerOutboxProcessing()для немедленного тика воркера
Переменные окружения:
| Переменная | Описание |
|---|---|
OUTBOX_WORKER_ENABLED |
true / false (по умолчанию включён в docker-compose.yml) |
OUTBOX_WORKER_INTERVAL_MS |
Интервал тика, мс (по умолчанию 10000) |
OUTBOX_WORKER_MAX_EVENTS |
Лимит событий за раунд |
OUTBOX_WORKER_MAX_ROUNDS |
Раундов за один тик воркера |
Скрипты:
node scripts/process-outbox-events.js # ручной / cron прогон
node scripts/reconcile-outbox-stuck.js # закрыть устаревшие события
node scripts/cleanup-expired-bookings.js # просроченные брони + outbox
Подробнее: architecture.md, DEPLOYMENT.md.
Валидация и маршруты¶
- Backend: Валидация сосредоточена в
server/validators/(express-validator). Файлы:bookValidators.js,userValidators.js,bookingValidators.js,operationValidators.js,referenceValidators.js,authValidators.js,userCategoryValidators.js,qrValidators.js,supplyValidators.js,supplierValidators.js. Реэкспорт изindex.js. Маршруты подключают валидаторы и вызывают сервисы (не репозитории напрямую, где это внедрено: suppliers, supply, userCategories, languages, books/import и др.). - Frontend: Схемы форм (Zod) в
src/lib/validators/:operationSchemas.ts,referenceSchemas.ts,userSchemas.ts,bookSchemas.ts,index.ts. В диалогах и страницах импортируйте схему и используйтеzodResolver(schema)без дублирования определений.
Стандарты кодирования¶
TypeScript (Frontend)¶
- Используйте строгую типизацию
- Избегайте
any, используйте конкретные типы - Используйте интерфейсы для объектов
- Документируйте сложные функции
// ✅ Хорошо
interface User {
id: string;
name: string;
}
function getUser(id: string): Promise<User> {
// ...
}
// ❌ Плохо
function getUser(id: any): Promise<any> {
// ...
}
JavaScript (Backend)¶
- Используйте ES6+ синтаксис
- Следуйте стилю кода проекта
- Используйте
async/awaitвместо callbacks - Обрабатывайте ошибки
// ✅ Хорошо
async function createLoan(data) {
try {
const result = await LoanService.createLoan(data);
return result;
} catch (error) {
logger.error('Error creating loan', { error, data });
throw error;
}
}
// ❌ Плохо
function createLoan(data, callback) {
LoanService.createLoan(data, (err, result) => {
if (err) {
// ...
}
callback(null, result);
});
}
Именование¶
- Переменные и функции:
camelCase - Классы:
PascalCase - Константы:
UPPER_SNAKE_CASE - Файлы:
PascalCase.tsxдля компонентов,camelCase.tsдля утилит
Комментарии¶
- Комментируйте сложную бизнес-логику
- Используйте JSDoc для функций и классов
- Объясняйте "почему", а не "что"
/**
* Создает операцию выдачи книги с автоматическим выполнением бронирования
* @param data - Данные для создания выдачи
* @returns Созданная операция выдачи
* @throws {Error} Если книга уже выдана или недоступна
*/
async function createLoan(data: CreateLoanData): Promise<Operation> {
// ...
}
Работа с Git¶
Ветвление¶
main- стабильная версия для productiondevelop- ветка разработкиfeature/*- новые функцииfix/*- исправления баговrefactor/*- рефакторинг
Коммиты¶
Используйте понятные сообщения коммитов:
feat: добавлена функция массовой выдачи книг
fix: исправлена ошибка при продлении выдачи
refactor: рефакторинг OperationRepository
docs: обновлена документация API
Pull Requests¶
- Создайте ветку от
develop - Внесите изменения
- Создайте Pull Request с описанием:
- Что изменено
- Почему изменено
- Как протестировано
Тестирование¶
CI (GitHub Actions)¶
Файл: .github/workflows/ci.yml
| Job | Шаги |
|---|---|
| Frontend build | npm ci → npm test → npm run build |
| Backend tests | PostgreSQL 16 → reset-test-db.js → npm test |
Особенности backend job:
NODE_ENV=test,CI=true,SKIP_INIT_SQL=trueREDIS_URL=""— rate-limit in-memory, без зависания на RedisOUTBOX_WORKER_ENABLED=false- Тестовая БД:
postgresql://library_user:library_test@localhost:5432/library_test
Тестовая база данных¶
Скрипты в server/scripts/:
| Команда | Назначение |
|---|---|
npm run reset:test-db |
DROP SCHEMA public CASCADE + миграции с нуля |
npm run setup:test-db |
Только применить миграции (без DROP) |
Важно: в CI и Jest init.sql не применяется — prod-дамп несовместим с построчным парсером (ломаются блоки $$...$$, pg_trgm, триггеры). Вместо этого:
- Таблицы создают миграции
001–049 - Справочники — миграция
030_seed_reference_data.sql(pending,loan,return,conditionsи др.)
Переменные тестового окружения: server/__tests__/env.js (загружается до всех тестов через Jest setupFiles).
Unit тесты¶
cd server
npm run test:unit
Критические сценарии (очередь, outbox, выдача/возврат, QR-лимиты)¶
cd server
npm run test:critical
Покрытие: booking-queue.test.js (lazy expiration, sync promotion после отмены, idempotency очереди, блокировка при удалении из очереди), critical-operations.test.js, outboxProcessor, loanEligibility.
Интеграционные тесты¶
Требуется БД library_test (см. TEST_DATABASE_URL в env.js):
cd server
npm run reset:test-db # рекомендуется перед полным прогоном
npm run test:integration
Все тесты¶
cd server
npm test
Frontend¶
npm test # Vitest
npm run build # проверка production-сборки
После деплоя frontend: при ошибке «Failed to fetch dynamically imported module» срабатывает авто-перезагрузка (src/lib/chunkLoadRecovery.ts) — устаревший кэш index.html после обновления.
Сессия между вкладками: Layout вызывает ensureSessionFresh(); logout в одной вкладке рассылается через authSync.ts (BroadcastChannel).
Ручное тестирование¶
- Проверьте все сценарии использования
- Протестируйте граничные случаи
- Проверьте обработку ошибок
- Убедитесь в корректности валидации
Отладка¶
Frontend¶
React DevTools¶
Установите расширение React DevTools для браузера.
Console логи¶
console.log('Debug info', { data });
console.error('Error', error);
Breakpoints¶
Используйте breakpoints в DevTools браузера.
Backend¶
Логирование¶
const logger = require('../utils/logger');
logger.debug('Debug message', { data });
logger.info('Info message', { data });
logger.warn('Warning message', { data });
logger.error('Error message', { error, data });
Debug режим¶
node --inspect index.js
Подключитесь через Chrome DevTools: chrome://inspect
База данных¶
-- Просмотр активных подключений
SELECT * FROM pg_stat_activity;
-- Просмотр блокировок
SELECT * FROM pg_locks;
-- Анализ запросов
EXPLAIN ANALYZE SELECT * FROM operations WHERE ...
Добавление новых функций¶
Шаг 1: Планирование¶
- Определите требования
- Спроектируйте API
- Определите изменения в БД (если нужны)
- Создайте план реализации
Шаг 2: Миграции БД¶
Если нужны изменения в схеме БД, создайте файл server/migrations/0NN_описание.sql (следующий номер после последней миграции).
Нумерованные миграции применяются через:
cd server
npm run migrate
Справочные данные для чистой установки — в 030_seed_reference_data.sql (или отдельный seed-скрипт с ON CONFLICT DO NOTHING).
Пример:
-- server/migrations/050_add_new_table.sql
CREATE TABLE new_table (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Шаг 3: Модель (если нужна)¶
// server/models/NewModel.js
module.exports = (sequelize, DataTypes) => {
const NewModel = sequelize.define('NewModel', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'new_table',
timestamps: true
});
return NewModel;
};
Шаг 4: Репозиторий¶
// server/repositories/NewRepository.js
const BaseRepository = require('./BaseRepository');
const { NewModel } = require('../models');
class NewRepository extends BaseRepository {
constructor() {
super(NewModel);
}
async findByName(name) {
return await this.model.findOne({
where: { name }
});
}
}
module.exports = new NewRepository();
Шаг 5: Сервис¶
// server/services/NewService.js
const NewRepository = require('../repositories/NewRepository');
const logger = require('../utils/logger');
class NewService {
static async createNew(data) {
// Валидация
if (!data.name) {
throw new Error('Name is required');
}
// Бизнес-логика
const existing = await NewRepository.findByName(data.name);
if (existing) {
throw new Error('Name already exists');
}
// Создание
const result = await NewRepository.create(data);
logger.info('New item created', { id: result.id });
return result;
}
static async getAll() {
return await NewRepository.findAll();
}
}
module.exports = NewService;
Шаг 6: Роут¶
// server/routes/new.js
const express = require('express');
const { body, validationResult } = require('express-validator');
const { authenticate, authorize } = require('../middleware/auth');
const NewService = require('../services/NewService');
const apiResponse = require('../utils/apiResponse');
const asyncHandler = require('../middleware/asyncHandler');
const router = express.Router();
router.post('/',
authenticate,
authorize('admin', 'librarian'),
body('name').trim().notEmpty(),
asyncHandler(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return apiResponse.unprocessableEntity(res, errors.array());
}
const result = await NewService.createNew(req.body);
return apiResponse.created(res, result);
})
);
router.get('/',
authenticate,
asyncHandler(async (req, res) => {
const items = await NewService.getAll();
return apiResponse.success(res, items);
})
);
module.exports = router;
Шаг 7: Frontend API¶
// src/lib/api.ts
export const api = {
// ...
createNew: (data: CreateNewData) =>
fetchAPI<NewItem>('/new', {
method: 'POST',
body: JSON.stringify(data)
}),
getNewItems: () =>
fetchAPI<NewItem[]>('/new'),
};
Шаг 8: Frontend компонент¶
// src/pages/NewPage.tsx
import { useState } from 'react';
import { api } from '@/lib/api';
import { toast } from 'sonner';
export default function NewPage() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const handleCreate = async (data) => {
setLoading(true);
try {
const newItem = await api.createNew(data);
toast.success('Item created');
setItems([...items, newItem]);
} catch (error) {
toast.error(error.message);
} finally {
setLoading(false);
}
};
// ...
}
Шаг 9: Тестирование¶
- Протестируйте API через Swagger или Postman
- Протестируйте UI компоненты
- Проверьте обработку ошибок
- Убедитесь в корректности валидации
Шаг 10: Документация¶
- Обновите API документацию
- Добавьте примеры использования
- Обновите руководство пользователя (если нужно)
Полезные команды¶
Backend¶
# Запуск в режиме разработки
npm run dev
# Запуск миграций
npm run migrate
# Первичный prod-дамп (только при миграции со старой БД)
npm run migrate:init
# Тестовая БД
npm run reset:test-db
npm run setup:test-db
# Запуск тестов
npm test
# Линтинг
npm run lint
# Форматирование
npm run format
Frontend¶
# Запуск в режиме разработки
npm run dev
# Сборка для production
npm run build
# Просмотр production сборки
npm run preview
# Линтинг
npm run lint
# Проверка типов
npm run type-check
Полезные ресурсы¶
- React документация
- TypeScript документация
- Express документация
- Sequelize документация
- Tailwind CSS документация
- shadcn/ui документация
Контакты и поддержка¶
При возникновении вопросов: 1. Проверьте документацию 2. Поищите в issues на GitHub 3. Создайте новый issue с описанием проблемы