В рамках хакатона Smolathon 2025 команда Сет представляет комплексное решение информационной системы для Центра организации дорожного движения (ЦОДД) г. Смоленск.
Проект влючает в себя фронтенд сайта ЦОДД, модульную систему для создания и редактирования статических и динамических страниц редактором из личного кабинета, инструменты доступа к открытой статистике и платформу для хранения и анализа данных из архива и базы данных, поддерживающую сравнение и прогнозирование из многих источников одновременно.
Поддерживаются любые типы статистики с возможностью представления их в виде шаблонов и валидацией при загрузке новых данных.
В основе лежит бекенд на python с использованием FastAPI и SQLAlchemy. В качестве базы данных используется PostgreSQL. В основе платформы анализа данных лежат пакеты pandas, scikit-learn и Prophet. Фронтенд написан с использованием React. Итоговое решение представлено в виде Docker-контейнера.
В системе выделены три основных блока:
-
Системные данные
- Пользователи, роли, права доступа
- Разделы сайта и посты, сохраняемые в JSON
-
Файловая система
- Загрузка и хранение документов
- Поддержка форматов: изображения и видео, DOC, PDF, CSV, XLSX
- Редактирование контента в удобном редакторе WYSIWYG
-
Статистические данные
- Создание гибких шаблонов для разных видов статистики
- Реестр баз данных с проверкой загружаемой информации и настройкой публичного доступа
---
config:
layout: elk
---
erDiagram
direction TB
StatsDBRegistry {
INTEGER id PK ""
VARCHAR db_name ""
VARCHAR display_name ""
INTEGER templateid FK ""
TIMESTAMP created_at ""
INTEGER created_by FK ""
TIMESTAMP updated_at ""
INTEGER updated_by FK ""
BOOLEAN is_public ""
}
Fines {
INTEGER id PK ""
INTEGER violations_count ""
INTEGER fines_count ""
NUMERIC fines_sum ""
NUMERIC fines_collected_sum ""
}
Evacuations {
INTEGER id PK ""
INTEGER evacuators_count ""
INTEGER call_count ""
INTEGER evacuations_count ""
NUMERIC collected_sum ""
}
EvacuationRoutes {
INTEGER id PK ""
VARCHAR month ""
VARCHAR route ""
}
Crossings {
INTEGER id PK ""
VARCHAR address ""
VARCHAR ppid ""
}
Templates {
INTEGER id PK ""
VARCHAR template_name ""
TEXT fields_db ""
TEXT field_types ""
TEXT fields_natural ""
}
Documents {
INTEGER id PK ""
VARCHAR filename ""
VARCHAR original_name ""
INTEGER file_size ""
TIMESTAMP uploaded_at ""
INTEGER uploaded_by FK ""
VARCHAR document_type ""
}
Users {
INTEGER id PK ""
VARCHAR first_name ""
VARCHAR last_name ""
VARCHAR middle_name ""
VARCHAR role ""
BOOLEAN is_active ""
VARCHAR login ""
VARCHAR password_hashed ""
}
TrafficLights {
INTEGER id PK ""
VARCHAR ppid ""
VARCHAR address ""
VARCHAR traffic_light_type ""
INTEGER installed_year ""
}
Sections {
INTEGER id PK ""
VARCHAR name ""
VARCHAR section_type ""
}
Throughput {
INTEGER id PK ""
VARCHAR crossing ""
INTEGER car_count ""
}
Posts {
INTEGER id PK ""
VARCHAR name ""
INTEGER sectionid FK ""
TEXT contents ""
TIMESTAMP created_at ""
INTEGER created_by FK ""
TIMESTAMP uploaded_at ""
INTEGER uploaded_by FK ""
}
Templates||--o{StatsDBRegistry:"id -> templateid"
StatsDBRegistry||--||Fines:"db_name <-> ."
StatsDBRegistry||--||Evacuations:"db_name <-> ."
StatsDBRegistry||--||EvacuationRoutes:"db_name <-> ."
StatsDBRegistry||--||Crossings:"db_name <-> ."
StatsDBRegistry||--||TrafficLights:"db_name <-> ."
StatsDBRegistry||--||Throughput:"db_name <-> ."
Users||--o{Documents:"id -> uploaded_by"
Sections||--o{Posts:"section_type -> sectionid"
Users||--o{Posts:"created_by <- id<br style='--tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-gradient-from-position: ; --tw-gradient-via-position: ; --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: fl°fff¶ß --tw-ring-color: rgb(59 130 246 / .5); --tw-ring-offset-shadow: 0 0 fl°°0000¶ß --tw-ring-shadow: 0 0 fl°°0000¶ß --tw-shadow: 0 0 fl°°0000¶ß --tw-shadow-colored: 0 0 fl°°0000¶ß --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; --tw-contain-size: ; --tw-contain-layout: ; --tw-contain-paint: ; --tw-contain-style: ;'>updated_by <- id"
Users||--o{StatsDBRegistry:"created_by <- id</br>updated_by <- id"
Система поддерживает загрузку и обработку внешних данных из файлов:
- CSV (
.csv) - Excel (
.xls,.xlsx)
Загруженные таблицы могут быть:
- сохранены в файловой системе сервера,
- проверены на корректность формата,
- преобразованы в новые шаблоны,
- проверены на соответствие шаблону с поиском наложений в общей базе данных,
- выгружены в общую базу данных,
- использованы на платформе аналитики в качестве источника,
- выгружены за выбранный период в поддерживаемых форматах.
Это позволяет быстро подключать новые источники данных и расширять аналитические возможности системы без дополнительной разработки.
Система позволяет редакторам вести как динамические, так и статические разделы:
-
Динамические:
- Новости
- Проекты
- События
- Любые другие регулярно обновляемые материалы
-
Статические:
- Команда
- О нас
- Контактная информация
- Другие постоянные разделы
Редакторский интерфейс основан на WYSIWYG-редакторе.
Все тексты и структуры разделов сохраняются в формате JSON в файловой системе сервера, что облегчает управление и поддержку контента.
Платформа обеспечивает полный цикл работы с данными — от загрузки до прогнозирования:
- Поддержка различных источников данных (CSV, XLS, XLSX)
- Проверка и валидация входящих данных по шаблонам
- Хранение данных в PostgreSQL с возможностью масштабирования
- Визуализация данных на фронтенде (React + JS)
Функциональность:
- 📊 построение интерактивных графиков и таблиц
- 📈 прогнозирование по архивным и актуальным данным
- 📂 сравнение и объединение данных из разных источников
flowchart LR
Guest["Гость / Пользователь"] -- зайти на страницу --> Frontend["Frontend<br>React"]
Frontend -- запрос статического раздела --> ContentSvc["Контент сервис"]
ContentSvc -- ответ --> Frontend
Frontend --> StaticSection["Статический раздел<br>например, контакты"] & DynamicSection["Динамический раздел<br>Новости / Проекты"] & SinglePost["Страница поста<br>"]
StaticSection -- рендер --> Guest
Frontend -- запрос динамического раздела первый --> ContentSvc
DynamicSection -- рендер 5 --> Guest
Guest -- клик 'показать ещё' --> Frontend
Frontend -- "получить следующие пять, сдвиг = N" --> ContentSvc
PostsStore["Posts<br>JSON / Posts БД"] --> ContentSvc
Frontend -- добавить следующие --> DynamicSection
Guest -- клик на пост --> Frontend
Frontend -- "получить пост с id = x" --> ContentSvc
SinglePost --> Guest
ContentSvc --> PostsStore
ContentSvc:::boxes
PostsStore:::boxes
classDef boxes fill:#111,stroke:#fff,color:#fff
flowchart TD
Editor["Редактор"] --> Choice["Создать новую<br>или редактировать существующую?"] & Preview["Предпросмотр"]
Choice --> NewPost["Создать новую статью"] & EditPost["Выбрать существующую статью"]
NewPost --> OpenEditor["Открыть WYSIWYG-редактор"]
EditPost --> OpenEditor
OpenEditor --> EditContent["Ввести / редактировать контент<br>(текст, метаданные)"]
EditContent --> UploadMedia["Загрузить файл<br>(изображение / документ)"] & Preview
UploadMedia --> FileTypeCheck["Определить тип загружаемого файла"]
FileTypeCheck -- media img, doc, pdf --> CloudDrive["Файловая система: сохраняет файл<br>возвращает удобный URL"]
FileTypeCheck -- analytics csv, xls, xlsx --> AnalyticsBlocked["Загрузка аналитических файлов<br>недоступна для редактора"]
CloudDrive --> InsertURL["Вставить URL в контент"]
InsertURL --> EditContent
Preview --> Validate["Проверка обязательных полей"]
Validate -- Ошибка --> OpenEditor
AnalyticsBlocked -. <br> .-> Editor
Publish["Опубликовать → сохранить в Posts"] --> Site["Запись доступна на сайте (публично)"]
Validate --> n1["Преобразовать в JSON"]
n1 --> Publish
n1@{ shape: rect}
CloudDrive:::serviceNodes
Publish:::serviceNodes
classDef serviceNodes fill:#111,stroke:#fff,color:#fff
flowchart TD
Analyst["Администратор"] --> Upload["Загрузить файл .csv / .xls / .xlsx"]
Upload --> AnalyticsRecv["Платформа аналитики: прием файла"] & StoreAnalytics["Хранение в файловой системе<br>"]
AnalyticsRecv --> Decision{"Запись в основную БД?"}
TemplateSel["Выбор шаблона<br>или создание нового"] --> FieldMap["Авто или ручное сопоставление полей"]
FieldMap --> Validate["Валидация по шаблону"]
Validate -- Ошибка --> AnalyticsRecv
ImportDB["Импорт в Postgres<br>"] --> Visualize["Визуализация / Сравнение / Экспорт"]
UpdateRegistry["Обновление реестра Stats Registry"] --> ImportDB
Decision -- Нет --> StoreAnalytics
StoreAnalytics --> Visualize
Visualize -- можно брать данные из: --> VisualData["Основной БД подтверждённых данных или из файловой системы"]
VisualData --> Visualize
CheckDup["Проверка на дубликаты / совпадающие даты"] --> n1["Новый шаблон?"]
Decision -- Да --> TemplateSel
Validate --> CheckDup
n1 -- Да --> UpdateRegistry
n1 -- Нет --> ImportDB
StoreAnalytics -- Записать --> Decision
Analyst --> Visualize
n1@{ shape: diam}
7caee3ad6977e3682a01328d35714906a635591b