Библиотечная платформа · 8 сервисов · 12 модулей
Нажмите на блок чтобы раскрыть API
Архитектура
12 модулей
Связи сервисов
Флоу: Читатель
Флоу: Книга
Флоу: Комплектование
Флоу: Выдача / Возврат
Флоу: Уведомления
el-workspace-frontend
React 19 · TypeScript · Vite · TanStack Query · Tailwind v4
:5173 (dev) ReadersPage CatalogingPage AcquisitionPage LoansPage + 8 модулей
GET/api/auth/me — текущий пользователь
POST/api/auth/login — вход
GET/api/reader/readers — список читателей
GET/api/workspace/cataloging/records — бибзаписи
GET/api/workspace/acquisition/invoices — накладные
GET/api/reader/loans — выдачи
GET/api/reader/reservations — бронирования
GET/api/workspace/events — мероприятия
GET/api/reports — каталог отчётов
el-public-frontend
Next.js · TypeScript · Tailwind v4 · next-intl (ru/kk/en)
:3000 Каталог книг Личный кабинет Новости
GET/api/public/catalog/search — поиск по каталогу
GET/api/public/catalog/books/{id} — карточка книги
GET/api/public/me/account — личный кабинет
POST/api/public/me/reservations — забронировать
POST/api/public/me/loans/{id}/renew — продлить
GET/api/public/content/news — новости
el-gateway-service
Spring Cloud Gateway · :8080 · маршрутизация, CORS
/api/auth/** → el-auth-service :9000
/api/workspace/** → el-workspace-service :8091
/api/reader/** → el-reader-service :8092
/api/public/** → el-public-service :8093
/api/reports/** → el-report-service :8095
/api/files/** → el-storage-service :8096
el-auth-service
:9000
JWT аутентификация, пользователи, роли
POST/api/auth/login
POST/api/auth/refresh
POST/api/auth/logout
GET/api/auth/me
el-workspace-service
:8091
Каталогизация, комплектование, фонд, периодика, книгообеспеченность, мероприятия, уведомления
GET/cataloging/records
POST/cataloging/records
GET/cataloging/external/lookup-all
GET/acquisition/invoices
POST/acquisition/invoices
POST/acquisition/invoices/{id}/workflow
GET/fund-acts
GET/periodicals/subscriptions
GET/book-supply/groups
GET/events
POST/notifications/campaigns
el-reader-service
:8092
Читатели, выдача, бронирование, штрафы
GET/readers
POST/readers
PUT/readers/{id}
PATCH/readers/{id}/active
GET/loans
POST/loans
POST/loans/{id}/return
POST/loans/{id}/renew
GET/reservations
POST/reservations
GET/catalog/copies/batch
GET/me/*
el-public-service
:8093
Публичный каталог, личный кабинет читателя, контент сайта
GET/catalog/search
GET/catalog/suggest
GET/catalog/books/{id}
GET/me/account
POST/me/reservations
POST/me/loans/{id}/renew
GET/content/news
GET/content/book-of-week
el-report-service
:8095
Генерация отчётов, экспорт в Excel
GET/reports
GET/reports/{code}/params
POST/reports/{code}/run
POST/reports/{code}/export
el-storage-service
:8096
Файлы, обложки, сканы — MinIO / S3
POST/files
GET/files/{id}
GET/files/{id}/url
DELETE/files/{id}
el-notification-service
:8094
Email рассылки через Kafka → SMTP
POST/email/send
POST/email/send-batch
← Kafka: notifications.email
el-eureka-server
:8761
Service discovery · Spring Cloud Eureka
Регистрация и обнаружение сервисов
GET /actuator/health
PostgreSQL 17
:5532 · схемы: app + refs + public · миграции: el-database-flyway
Elasticsearch
:9200 · индекс bib_records_v1 · публичный поиск
Kafka
:9092 · topic: notifications.email
Нажмите на любой блок для раскрытия API-эндпоинтов
el-workspace-service
el-reader-service
el-report-service
уведомления
Читатели
el-reader-service · /readers
reader
GET/readersсписок с пагинацией
GET/readers/{id}карточка читателя
POST/readersсоздать читателя
PUT/readers/{id}обновить данные
PATCH/readers/{id}/activeактивировать/блок.
GET/readers/{id}/loansкниги на руках
GET/readers/{id}/loans/summaryсводка выдач
Каталогизация
el-workspace-service · /cataloging
workspace
GET/cataloging/recordsсписок бибзаписей
GET/cataloging/records/{id}запись + экз.
POST/cataloging/recordsсоздать MARC
PUT/cataloging/records/{id}обновить
DELETE/cataloging/records/{id}удалить + экз.
GET/cataloging/records/{id}/copiesэкземпляры
PUT/cataloging/records/{id}/copies/{cId}обновить экз.
GET/cataloging/external/lookup-all?isbn=поиск по ISBN
GET/cataloging/suggest?q=подсказки
GET/cataloging/refs/fieldsполя MARC21
Комплектование
el-workspace-service · /acquisition
workspace
GET/acquisition/invoicesнакладные
POST/acquisition/invoicesсоздать (NEW)
PUT/acquisition/invoices/{id}обновить (NEW)
DELETE/acquisition/invoices/{id}удалить (NEW)
POST/acquisition/invoices/{id}/workflowсменить статус
GET/acquisition/refs/suppliersпоставщики
POST/acquisition/refs/suppliersсоздать поставщика
GET/acquisition/bib-copiesэкз. из накладных
Выдача книг
el-reader-service · /loans
reader
GET/loansсписок выдач
POST/loansвыдать книгу
POST/loans/{id}/returnвернуть книгу
POST/loans/{id}/renewпродлить срок
GET/loans/lookup/reader?card=найти по карте
GET/loans/lookup/copy?barcode=найти экземпляр
Бронирование
el-reader-service · /reservations
reader
GET/reservationsсписок броней
POST/reservationsзабронировать
POST/reservations/{id}/fulfill→ выдача
POST/reservations/{id}/cancelотменить
POST/reservations/{id}/expireистекла
Штрафные баллы
el-reader-service · /penalty
reader
GET/penalty/listдолжники
GET/penalty/readers/{id}баллы читателя
GET/penalty/readers/{id}/eventsистория событий
POST/penalty/readers/{id}/eventsручное начисление
POST/penalty/readers/{id}/restrictionsограничение
DELETE/penalty/restrictions/{id}снять ограничение
GET/penalty/policyполитика
PUT/penalty/policyизменить политику
Мероприятия
el-workspace-service · /events
workspace
GET/eventsсписок
POST/eventsсоздать
PUT/events/{id}обновить
DELETE/events/{id}удалить
POST/events/{id}/registrationsрегистрация
GET/events/statsстатистика
Движение фонда
el-workspace-service · /fund-acts
workspace
GET/fund-actsакты движения
POST/fund-actsсоздать акт
PATCH/fund-acts/{id}обновить
DELETE/fund-acts/{id}удалить
POST/fund-acts/{id}/applyприменить
POST/fund-acts/{id}/linesдобавить экз.
GET/bib-copies/{copyId}/movement-historyистория экз.
Периодика
el-workspace-service · /periodicals
workspace
GET/periodicals/subscriptionsподписки
POST/periodicals/subscriptionsдобавить
GET/periodicals/bindingsпереплёты
POST/periodicals/bindingsсоздать переплёт
GET/periodicals/issue-planплан поступления
POST/periodicals/issue-plan/{id}/receiveотметить получение
Книгообеспеченность
el-workspace-service · /book-supply
workspace
GET/book-supply/groupsгруппы читателей
POST/book-supply/groupsсоздать группу
POST/book-supply/groups/{id}/membersдобавить в группу
GET/book-supply/actsакты обеспеченности
POST/book-supply/actsсоздать акт
POST/book-supply/acts/{id}/applyприменить
Отчёты
el-report-service · /reports
report
GET/reportsкаталог отчётов
GET/reports/{code}/paramsпараметры
POST/reports/{code}/runвыполнить → грид
POST/reports/{code}/export→ Excel
Уведомления
el-workspace-service · /notifications
workspace
GET/notifications/recipientsпревью аудитории
POST/notifications/campaignsсоздать рассылку
→ el-notification-service доставляет через Kafka → SMTP
Создание читателя
Транзакция: user + role + profile + membership — атомарно
1
Frontend
Заполнить форму
ReadersPage → CreateReaderModal
2
POST /readers
el-reader-service :8092
{ email, password, fullName, phone, libraryCardNumber? }
BEGIN TX: user + role + profile + membership
pg_advisory_lock на номер карты
3
Карта создана
cardNumber = MAX+1 если не задан
→ ReaderDto { id, cardNumber, active: false }
4
PATCH /active
Активировать
/readers/{id}/active · { active: true }
5
Читатель активен
Может брать книги
Если email уже существует (читатель другой библиотеки) — пользователь переиспользуется, создаётся только новое членство. Повторное членство в той же библиотеке → HTTP 409.
Операции с читателем
После создания доступны следующие действия
Просмотр
GET /readers/{id}
ФИО, контакты, история
Обновить
PUT /readers/{id}
Изменить контакты, категорию
Книги на руках
GET /readers/{id}/loans
Список активных выдач
Штрафы
GET /penalty/readers/{id}
Баллы, ограничения
Заблокировать
PATCH /readers/{id}/active · { active: false }
Ограничение
POST /penalty/readers/{id}/restrictions
Запрет выдачи на период
Создание книги — MARC + экземпляры через комплектование
Прямого POST /copies нет — экземпляры создаются только через накладную
1
Ввести ISBN
CatalogingPage
2
Поиск по ISBN
el-workspace :8091
GET /cataloging/external/lookup-all?isbn=
→ ExternalBookDto[] (Open Library, ISBNdb)
3
POST /cataloging/records
Создать бибзапись
{ marc_json: { leader, fields:[...] } }
Обязательно: 245$a (заглавие)
→ BibRecord { id, title }
4
Накладная → экземпляры
Флоу: Комплектование
POST /acquisition/invoices
+ workflow → CLOSED
→ bib_copies status: AVAILABLE
5
Книга в фонде
AVAILABLE
Доступна для выдачи и бронирования
Жизненный цикл экземпляра
Все возможные переходы статуса bib_copies
INVENTORIED
создан накладной
AVAILABLE
накладная CLOSED
ON_LOAN
POST /loans
AVAILABLE
POST /loans/{id}/return
WRITTEN_OFF
POST /fund-acts (WRITE_OFF) + apply
LOST
POST /loans/{id}/return · { lost: true }
Комплектование — Накладная → Экземпляры
Полный флоу внутри el-workspace-service · внешних вызовов нет · 4 смены статуса
1
Создать накладную
Сотрудник → el-workspace-service :8091
POST /acquisition/invoices
Request: { supplierId, deliveryTypeId, invoiceDate, invoiceNumber }
→ DB: INSERT INTO app.invoices (status = 'NEW')
Response: { id, number, status: 'NEW' } · HTTP 201
2
Добавить позиции (книги)
el-workspace-service :8091 — повторить для каждой книги
POST /acquisition/invoices/{id}/records
Request: { bibRecordId, quantity: N, pricePerUnit }
→ DB: INSERT INTO app.invoice_records (N экземпляров)
Только накладные в статусе 'NEW' принимают новые позиции
3
Верификация
el-workspace-service :8091 — InvoiceWorkflowServiceImpl.verify()
POST /acquisition/invoices/{id}/workflow · { transition: "VERIFY" }
Проверка: сумма позиций = сумма накладной
Проверка: quantity > 0 у всех позиций
→ DB: UPDATE invoices SET status = 'VERIFIED'
4
Генерация инвентаря — создание экземпляров
el-workspace-service :8091 — InvoiceWorkflowServiceImpl.generateInventory()
POST /acquisition/invoices/{id}/workflow · { transition: "GENERATE_INVENTORY" }
Для каждой позиции накладной (invoice_record):
Для каждого экземпляра 1..quantity:
barcode = MAX(barcode_series) + offset
inventory_number = MAX(inventory_number) + offset
INSERT INTO app.bib_copies (bib_record_id, barcode,
inventory_number, status = 'INVENTORIED', invoice_id)
→ DB: UPDATE invoices SET status = 'INVENTORIED'
Внешних вызовов нет — el-workspace пишет app.bib_copies напрямую
5
Закрытие накладной — экземпляры становятся доступными
el-workspace-service :8091 — InvoiceWorkflowServiceImpl.close()
POST /acquisition/invoices/{id}/workflow · { transition: "CLOSE" }
→ DB: UPDATE app.bib_copies SET status = 'AVAILABLE' WHERE invoice_id = ?
→ DB: UPDATE invoices SET status = 'CLOSED'
Экземпляры видны читателям и доступны для выдачи через el-reader-service
Статусы накладной и экземпляра
NEW VERIFIED INVENTORIED CLOSED
INVENTORIED──(CLOSE)──→ AVAILABLE──(POST /loans)──→ ON_LOAN──(return)──→ AVAILABLE
Выдача книги — атомарная транзакция с защитой от гонки
Сканирование карты → сканирование штрихкода → атомарный INSERT + UPDATE
1
Найти читателя по карте
Сотрудник сканирует карту → el-reader-service :8092
GET /loans/lookup/reader?card={cardNumber}
→ DB: SELECT users JOIN memberships JOIN loans (ACTIVE) JOIN penalty_events
Response: { reader, activeLoans: N, restrictions: [...] }
active=false или restrictions → сотрудник видит предупреждение
2
Найти экземпляр по штрихкоду
Сотрудник сканирует штрихкод → el-reader-service :8092
GET /loans/lookup/copy?barcode={barcode}
→ DB: SELECT bib_copies JOIN bib_records WHERE barcode = ?
Response: { copy: { id, status, barcode }, record: { title, author } }
status ≠ 'AVAILABLE' → HTTP 409
3
Создать выдачу — атомарная транзакция
el-reader-service :8092 — LoanServiceImpl.create()
POST /loans · { readerCardNumber, copyBarcode, dueDate }
BEGIN TRANSACTION
INSERT INTO app.loans (copy_id, user_id, library_id, due_date, status = 'ACTIVE')
UPDATE app.bib_copies SET status = 'ON_LOAN'
WHERE copy_id = ? AND status = 'AVAILABLE' ← условие гонки
IF rows_affected = 0 → ROLLBACK → HTTP 409 "COPY_RACE_LOST"
COMMIT
Защита от гонки состояний
DB-уровень: UNIQUE INDEX uq_loans_active_copy ON app.loans(copy_id) WHERE status='ACTIVE'
App-уровень: условный UPDATE → 0 строк = экземпляр уже занят → HTTP 409
Итог: невозможно выдать один экземпляр двум читателям одновременно
HTTP 201 · { id, dueDate, status: 'ACTIVE' }
Возврат книги — цепочка событий
Возврат → штраф → продвижение очереди броней → email уведомление
1
Принять книгу
Сотрудник → el-reader-service :8092 — LoanServiceImpl.returnLoan()
POST /loans/{id}/return
BEGIN TRANSACTION
days_overdue = MAX(0, TODAY − loan.due_date)
fine = days_overdue × penalty_per_day
UPDATE loans SET status='RETURNED', returned_at=NOW(),
fine_amount=fine, days_overdue=days_overdue
UPDATE bib_copies SET status='AVAILABLE' WHERE copy_id = ?
SELECT FROM reservations WHERE bib_record_id = ?
AND status = 'PENDING' ORDER BY created_at ASC LIMIT 1
IF reservation EXISTS:
UPDATE reservations SET status='READY', expires_at=NOW()+48h
COMMIT
2
Email уведомление (после коммита)
el-reader-service — ReservationNotifier.notifyReady() · TransactionSynchronization.afterCommit
→ Feign → el-notification-service :8094
POST /email/send · { to: [reader_email],
subject: "Ваша бронь готова к выдаче",
body: "Книга «{title}» ждёт вас до {expires_at}" }
Response: HTTP 202 · ошибки игнорируются (best-effort)
afterCommit гарантирует: если TX откатилась, письмо не отправляется. Если письмо упало после коммита — книга уже доступна, но уведомление не придёт. Осознанный компромисс MVP.
3
Доставка через Kafka → SMTP
el-notification-service — EmailProducer → Kafka → EmailConsumer → SMTP
EmailProducer.publish() → KafkaTemplate.send("notifications.email", msg)
↓ async
EmailConsumer.onMessage() → JavaMailSender.send() → SMTP → Email читателю
Продление срока
Простая операция без внешних вызовов
1
Продлить выдачу
el-reader-service :8092 — LoanServiceImpl.renewLoan()
POST /loans/{id}/renew
Проверить: renewals_count < max_renewals (из policy)
Проверить: нет ограничений у читателя
UPDATE loans SET due_date = due_date + loan_period_days,
renewals_count = renewals_count + 1
Response: HTTP 200 · { dueDate: новая дата }
Кампания рассылки читателям
el-workspace → el-notification → Kafka → SMTP · батчи по 100
1
Создать кампанию
Библиотекарь → el-workspace-service :8091 — NotificationServiceImpl.sendCampaign()
POST /notifications/campaigns
Request: { subject, body, html: boolean, audience: { filter: ACTIVE | ALL | ... } }
2
Получить список получателей
el-workspace-service — RecipientRepository.findByFilter()
SELECT u.email FROM app.users u
JOIN app.reader_library_memberships rmb ON rmb.user_id = u.id
WHERE rmb.library_id = ?
AND u.email IS NOT NULL
AND rmb.extra->>'email_opt_in' = 'true'
AND [audience filter]
→ List<String> emails
3
Отправить батчами по 100
el-workspace → el-notification :8094 (Feign) — FeignNotificationClient
POST /email/send-batch · повторяется для каждого батча
Request: { requests: [{ to: [...100 emails], subject, body, html, fromName }] }
Response: { total: 100, sent: 100, failed: 0 } · HTTP 202
HTTP 202 = "принято в очередь", не "доставлено"
4
Поставить в очередь Kafka
el-notification-service — EmailProducer.publish()
Для каждого email в батче:
KafkaTemplate.send("notifications.email", EmailMessage { to, subject, body, html, fromName })
5
Доставка email
el-notification-service — EmailConsumer.onMessage() → JavaMailSender
@KafkaListener topic = "notifications.email"
EmailService.send() → JavaMailSender.send() → SMTP
Ошибки логируются, offset коммитится (нет retry / DLQ в MVP)
Dev: MailHog · Prod: MAIL_HOST env
Письмо доставлено читателю
Уведомление о готовности брони
Автоматически при возврате книги · срабатывает после коммита TX
1
Возврат книги
POST /loans/{id}/return
TX: UPDATE reservations SET status='READY' · COMMIT
2
ReservationNotifier
afterCommit() hook
Feign → el-notification :8094
POST /email/send
HTTP 202 · best-effort
3
Kafka
topic: notifications.email
EmailMessage → queue
4
Email читателю
"Книга готова.
Заберите до {expires_at}"