Справочник событий пользователя (user_events)
Документ описывает все типы событий, регистрируемых через UserEventService, их параметры и структуру JSON-данных.
Предназначен для аналитиков для построения запросов к таблице user_events.
Источники событий: проекты stradarium.govern и stradarium.payment.
1. Схема таблицы
Таблица: user_events
Активные колонки
| Колонка | Тип | Ограничения | Описание |
|---|---|---|---|
id |
VARCHAR(36) | PK, NOT NULL | Уникальный идентификатор записи (UUID) |
username |
VARCHAR(255) | NOT NULL | Имя пользователя (email), всегда в нижнем регистре |
event |
VARCHAR(255) | NOT NULL | Тип события — значение enum UserEventType, хранится как строка |
timestamp_tz |
TIMESTAMPTZ | NOT NULL | Время регистрации события, устанавливается автоматически при вставке |
details_json |
JSONB | nullable | Дополнительные параметры события в формате JSON. Может быть null для событий без параметров |
Устаревшие колонки
Данные колонки присутствуют в таблице, но не используются текущим кодом. Могут содержать данные для старых записей.
| Колонка | Тип | Описание |
|---|---|---|
timestamp |
TIMESTAMP WITHOUT TIME ZONE | Заменена колонкой timestamp_tz после миграции на поддержку часовых поясов |
details |
VARCHAR | Заменена колонкой details_json после миграции на JSONB |
Индексы
Единственный индекс — первичный ключ по колонке id. Дополнительных индексов нет.
Запросы с фильтрацией по username, event или timestamp_tz выполняют полное сканирование таблицы.
Устаревший формат details
Устаревший метод register(String, UserEventType, String) оборачивал строковый параметр в JSON вида:
{"message": "...", "_version": 1}
В текущем коде этот метод не используется, но в исторических записях могут встречаться строки с ключом _version. Наличие _version: 1 — маркер записи в устаревшем формате.
2. Каталог типов событий
2.1. Аутентификация
SUCCESSFUL_LOGIN
Успешный вход пользователя в личный кабинет.
| Ключ | Тип | Описание |
|---|---|---|
| — | — | Параметры не передаются, details_json = null |
Источник: govern — controller.hello.UsersController#login
UNSUCCESSFUL_LOGIN_ATTEMPT
Неудачная попытка входа (неверные учётные данные).
| Ключ | Тип | Описание |
|---|---|---|
| — | — | Параметры не передаются, details_json = null |
Источник: govern — controller.hello.UsersController#login
SUCCESSFUL_PASSWORD_RECOVERY
Успешное восстановление пароля.
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
Источник: govern — controller.hello.UsersController#recover
UNSUCCESSFUL_PASSWORD_RECOVERY
Неудачная попытка восстановления пароля (пользователь не найден).
| Ключ | Тип | Описание |
|---|---|---|
| — | — | Параметры не передаются, details_json = null |
Источник: govern — controller.hello.UsersController#recover
2.2. Просмотр контента
LIST_COURSES
Пользователь запросил список курсов в личном кабинете.
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
total |
Long | Количество курсов в результате |
productType |
String | Тип продукта (значение enum ProductType) |
Источник: govern — controller.hello.CoursesController#list
LIST_PRODUCTS
Пользователь запросил список доступных продуктов.
| Ключ | Тип | Описание |
|---|---|---|
| — | — | Параметры не передаются, details_json = null |
Источник: govern — controller.hello.ProductsController#list
COURSE_DETAILS
Пользователь просмотрел детали курса.
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
courseId |
String | Идентификатор курса |
Источник: govern — controller.hello.CoursesController#details
MEETING_DETAILS
Пользователь просмотрел детали занятия (лекции).
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
meetingId |
String | Идентификатор занятия |
Источник: govern — controller.hello.MeetingsController#details
MEETING_VIEW_UPDATE
Пользователь отметил занятие как просмотренное или снял отметку.
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
meetingId |
String | Идентификатор занятия |
meetingName |
String | Название занятия |
viewed |
Boolean | true — отмечено как просмотренное, false — отметка снята |
Источник: govern — service.MeetingViewService#markViewed, service.MeetingViewService#unmarkViewed
2.3. Профиль пользователя
USER_BIRTHDATE_UPDATE
Пользователь указал свою дату рождения.
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
birthdate |
String | Дата рождения (формат строки) |
Источник: govern — service.ProfileService#updateBirthdate
USER_INTERESTS_UPDATE
Пользователь обновил список своих интересов.
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
interests |
String | Список интересов через запятую |
Источник: govern — service.UserInterestsService#updateUserInterests
2.4. Администрирование
ENTITY_UPDATE
Администратор создал или обновил сущность (пользователь, регистрация на продукт).
Набор ключей зависит от типа сущности:
При обновлении пользователя (UserService#update):
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
entityType |
String | Всегда "user" |
userId |
String | Идентификатор пользователя |
username |
String | Имя пользователя |
action |
String | Всегда "update" |
При отключении пользователя (UserService#disable):
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
entityType |
String | Всегда "user" |
userId |
String | Идентификатор пользователя |
action |
String | Всегда "disable" |
При создании/обновлении регистрации на продукт:
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
entityType |
String | Всегда "registration" |
registrationId |
UUID | Идентификатор регистрации |
username |
String | Имя пользователя |
productName |
String | Название продукта |
productId |
UUID | Идентификатор продукта |
Источники: govern — service.UserService#update, service.UserService#disable, service.RegistrationService#saveOrUpdateProductRegistration
ENTITY_COPY
Администратор скопировал курс или продукт.
Набор ключей зависит от типа сущности:
При копировании курса:
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
entityType |
String | Всегда "course" |
copyCourseId |
String | Идентификатор новой копии |
originalCourseId |
String | Идентификатор оригинального курса |
originalCourseName |
String | Название оригинального курса |
При копировании продукта:
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
entityType |
String | Всегда "product" |
copyProductId |
UUID | Идентификатор новой копии |
originalProductId |
UUID | Идентификатор оригинального продукта |
originalProductName |
String | Название оригинального продукта |
Источники: govern — service.CoursesService#copy, service.ProductsService#copy
GIFT_CERTIFICATE_DOWNLOAD
Скачивание подарочного сертификата.
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
productRegistrationId |
String | Идентификатор регистрации на продукт |
productId |
UUID (как String) | Идентификатор продукта |
productType |
String | Тип продукта (значение enum ProductType) |
productName |
String | Название продукта |
Источник: govern — controller.admin.CertificateController#certificate
2.5. Уведомления и email
EMAIL
Отправка email-уведомления пользователю. Регистрируется из нескольких мест в обоих проектах.
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
userEmail |
String | Email получателя |
eventType |
String | Тип уведомления. В большинстве случаев — значение enum UserNotificationEventType (см. ниже). Исключение: в govern UserService#createUser передаётся строка "Доступ в личный кабинет" (константа NEW_USER_REGISTERED_SUBJECT) |
productName |
String | Название продукта (присутствует не во всех вызовах) |
productId |
UUID | Идентификатор продукта (присутствует не во всех вызовах) |
activationCode |
String | Код активации подарочного сертификата (только для подарочных регистраций в payment) |
registrationId |
String | Идентификатор регистрации (только для подарочных регистраций в payment) |
Возможные значения eventType (enum UserNotificationEventType):
| Значение | Проект | Описание |
|---|---|---|
USER_REGISTRATION |
govern, payment | Регистрация нового пользователя |
COURSE_REGISTRATION |
govern, payment | Регистрация на курс |
COURSE_INFORMATION |
govern, payment | Информационное письмо по курсу |
GIFT_CERTIFICATE |
govern, payment | Подарочный сертификат |
PASSWORD_RECOVER |
govern | Восстановление пароля |
VETKA_REGISTRATION |
payment | Регистрация на продукт типа «Ветка» |
Дополнительные значения enum, не встречающиеся в событиях EMAIL: MEETING_RECORD_LOADED, SCHEDULED, MANUAL.
Источники:
- govern — controller.admin.RegistrationsController#mail (2 вызова), service.UserService#createUser
- payment — service.UserService#createUser, service.notify.CourseProductNotifier#notify (3 вызова), service.notify.VetkaProductNotifier#notify (2 вызова)
USER_NOTIFICATION_CREATION
Администратор создал новое уведомление для пользователей.
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
notificationId |
String | Идентификатор уведомления |
courseId |
String | Идентификатор курса |
eventType |
String | Тип события уведомления (значение enum) |
notificationType |
String | Тип уведомления (значение enum) |
Источник: govern — controller.admin.UserNotificationsController#save
USER_NOTIFICATION_BROADCAST
Массовая рассылка email-уведомлений пользователям курса (по расписанию).
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
registrationsAmount |
Integer | Количество регистраций, по которым выполнена рассылка |
notificationId |
String | Идентификатор уведомления |
Источник: govern — schedule.UserNotifier#broadcast
STATISTICS_NOTIFICATION
Отправка статистических отчётов в Telegram (по расписанию).
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
type |
String | Тип отчёта: "timepad" или "courseRegistrationDaily" |
transport |
String | Канал отправки: "telegram" |
format |
String | Формат отчёта: "html" или "pdf" |
Источники: govern — schedule.TimepadStatisticsNotifier#executeTelegramMessage, schedule.TimepadStatisticsNotifier#executePdf, schedule.CourseRegistrationStatisticsNotifier#execute
2.6. Платежи и подписки
SUBSCRIPTION
Подписка или отписка пользователя от email-рассылки через Unisender.
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
action |
String | Действие: "subscribe" или "exclude" |
email |
String | Email пользователя |
listId |
String | Идентификатор списка рассылки в Unisender |
Источник: payment — service.SubscriptionService#subscribe, service.SubscriptionService#exclude
DISCOUNT_PERIOD_EXTENDED
Автоматическое продление периода скидки для продукта (по расписанию).
| Ключ | Тип | Описание |
|---|---|---|
message |
String | Описание действия |
productId |
UUID | Идентификатор продукта |
productType |
String | Тип продукта (значение enum ProductType) |
productName |
String | Название продукта |
paymentDetailsId |
String (UUID формат) | Идентификатор платёжных данных |
discountStrategy |
String | Стратегия скидки (значение enum) |
newBestBefore |
String | Новая дата окончания скидки |
Источник: govern — service.DiscountService#processDaily
3. Источники событий
По проектам
| Тип события | govern | payment |
|---|---|---|
SUCCESSFUL_LOGIN |
+ | |
UNSUCCESSFUL_LOGIN_ATTEMPT |
+ | |
SUCCESSFUL_PASSWORD_RECOVERY |
+ | |
UNSUCCESSFUL_PASSWORD_RECOVERY |
+ | |
LIST_COURSES |
+ | |
LIST_PRODUCTS |
+ | |
COURSE_DETAILS |
+ | |
MEETING_DETAILS |
+ | |
MEETING_VIEW_UPDATE |
+ | |
USER_BIRTHDATE_UPDATE |
+ | |
USER_INTERESTS_UPDATE |
+ | |
ENTITY_UPDATE |
+ | |
ENTITY_COPY |
+ | |
GIFT_CERTIFICATE_DOWNLOAD |
+ | |
EMAIL |
+ | + |
USER_NOTIFICATION_CREATION |
+ | |
USER_NOTIFICATION_BROADCAST |
+ | |
STATISTICS_NOTIFICATION |
+ | |
SUBSCRIPTION |
+ | |
DISCOUNT_PERIOD_EXTENDED |
+ |
По классам
| Класс | Проект | Типы событий | Кол-во вызовов |
|---|---|---|---|
controller.hello.UsersController |
govern | SUCCESSFUL_LOGIN, UNSUCCESSFUL_LOGIN_ATTEMPT, SUCCESSFUL_PASSWORD_RECOVERY, UNSUCCESSFUL_PASSWORD_RECOVERY | 4 |
controller.hello.CoursesController |
govern | LIST_COURSES, COURSE_DETAILS | 2 |
controller.hello.ProductsController |
govern | LIST_PRODUCTS | 1 |
controller.hello.MeetingsController |
govern | MEETING_DETAILS | 1 |
controller.admin.UserNotificationsController |
govern | USER_NOTIFICATION_CREATION | 1 |
controller.admin.CertificateController |
govern | GIFT_CERTIFICATE_DOWNLOAD | 1 |
controller.admin.RegistrationsController |
govern | 2 | |
service.UserService |
govern | ENTITY_UPDATE, EMAIL | 3 |
service.ProfileService |
govern | USER_BIRTHDATE_UPDATE | 1 |
service.MeetingViewService |
govern | MEETING_VIEW_UPDATE | 2 |
service.DiscountService |
govern | DISCOUNT_PERIOD_EXTENDED | 1 |
service.CoursesService |
govern | ENTITY_COPY | 1 |
service.ProductsService |
govern | ENTITY_COPY | 1 |
service.UserInterestsService |
govern | USER_INTERESTS_UPDATE | 1 |
service.RegistrationService |
govern | ENTITY_UPDATE | 1 |
schedule.TimepadStatisticsNotifier |
govern | STATISTICS_NOTIFICATION | 2 |
schedule.CourseRegistrationStatisticsNotifier |
govern | STATISTICS_NOTIFICATION | 1 |
schedule.UserNotifier |
govern | USER_NOTIFICATION_BROADCAST | 1 |
service.UserService |
payment | 1 | |
service.SubscriptionService |
payment | SUBSCRIPTION | 2 |
service.notify.CourseProductNotifier |
payment | 3 | |
service.notify.VetkaProductNotifier |
payment | 2 |
Итого: 35 вызовов (27 в govern, 8 в payment) из 22 классов (18 в govern, 4 в payment).
4. Примеры SQL-запросов
Количество событий по типу за период
SELECT event, COUNT(*) AS cnt
FROM user_events
WHERE timestamp_tz BETWEEN '2025-01-01' AND '2025-02-01'
GROUP BY event
ORDER BY cnt DESC;
Все попытки входа конкретного пользователя
SELECT event, timestamp_tz, details_json
FROM user_events
WHERE username = 'user@example.com'
AND event IN ('SUCCESSFUL_LOGIN', 'UNSUCCESSFUL_LOGIN_ATTEMPT')
ORDER BY timestamp_tz DESC;
Выборка по JSONB-полю — все письма определённого типа
SELECT username, timestamp_tz, details_json
FROM user_events
WHERE event = 'EMAIL'
AND details_json->>'eventType' = 'COURSE_REGISTRATION'
ORDER BY timestamp_tz DESC;
Количество отправленных email по дням
SELECT DATE(timestamp_tz) AS day, COUNT(*) AS emails_sent
FROM user_events
WHERE event = 'EMAIL'
GROUP BY DATE(timestamp_tz)
ORDER BY day DESC;
Все подписки и отписки пользователя
SELECT timestamp_tz,
details_json->>'action' AS action,
details_json->>'listId' AS list_id
FROM user_events
WHERE event = 'SUBSCRIPTION'
AND details_json->>'email' = 'user@example.com'
ORDER BY timestamp_tz DESC;
Копирование сущностей — какие курсы и продукты были скопированы
SELECT timestamp_tz,
username,
details_json->>'entityType' AS entity_type,
COALESCE(details_json->>'originalCourseName', details_json->>'originalProductName') AS original_name
FROM user_events
WHERE event = 'ENTITY_COPY'
ORDER BY timestamp_tz DESC;
Поиск записей в устаревшем формате (с маркером _version)
SELECT id, username, event, timestamp_tz, details_json
FROM user_events
WHERE details_json ? '_version'
LIMIT 100;