f4d89a262c
Канон PRODUCT-AI (Spec#22): пилотные метрики L1-подсказок для расчёта
голосового гейта L2 (>=20% активных пользователей с >=2 подтверждённых
ghost-бронирований в месяц). Локальные счётчики в Mnesia, без LLM.
- таблица ai_hint_metric (миграция 20260820180000): счётчики
{user, день UTC, pattern, kind}; ленивый prune при >500 строк на
пользователя, retention 92 дня
- POST /v1/ai/hint-metrics (Bearer): батч <=20 событий shown/confirm/dismiss,
202 {accepted:N}; ETS rate limit 30 req/min на пользователя
- GET /v1/admin/ai-metrics/stats (админ-порт): by_kind, active_users_30d,
users_with_confirms_ge2_30d, gate_ratio_30d, top_patterns_by_confirm
- eunit logic_ai_metrics_tests (5/5); синхронизация списка миграций
в migration_engine_tests
Refs EventHub/EventHubBack#78
432 lines
15 KiB
Erlang
Executable File
432 lines
15 KiB
Erlang
Executable File
%% ===================================================================
|
|
%% EventHub Records Definition
|
|
%% ===================================================================
|
|
|
|
%% ------------------- Пользователи и аутентификация -------------------
|
|
-record(user, {
|
|
id :: binary(),
|
|
email :: binary(),
|
|
password_hash :: binary(),
|
|
role :: user | bot,
|
|
status :: pending | active | frozen | deleted,
|
|
reason :: binary(),
|
|
nickname :: binary(),
|
|
avatar_url :: binary() | default,
|
|
timezone :: binary(),
|
|
language :: binary(),
|
|
social_links :: [binary()],
|
|
phone :: binary(),
|
|
preferences :: map(),
|
|
last_login :: calendar:datetime(),
|
|
created_at :: calendar:datetime(),
|
|
updated_at :: calendar:datetime()
|
|
}).
|
|
|
|
-record(session, {
|
|
token :: binary(), % JWT или refresh токен
|
|
user_id :: binary(),
|
|
expires_at :: calendar:datetime(),
|
|
type :: access | refresh
|
|
}).
|
|
|
|
-record(verification, {
|
|
token :: binary(),
|
|
user_id :: binary(),
|
|
expires_at :: calendar:datetime()
|
|
}).
|
|
|
|
-record(password_reset, {
|
|
token :: binary(),
|
|
user_id :: binary(),
|
|
expires_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% ------------------- АДМИНИСТРАТОРЫ ------------------------------------
|
|
-record(admin, {
|
|
id :: binary(),
|
|
email :: binary(),
|
|
password_hash :: binary(),
|
|
role :: superadmin | admin | moderator | support,
|
|
status :: active | blocked,
|
|
nickname :: binary(),
|
|
avatar_url :: binary() | default,
|
|
timezone :: binary(),
|
|
language :: binary(),
|
|
phone :: binary(),
|
|
preferences :: map(),
|
|
last_login :: calendar:datetime(),
|
|
created_at :: calendar:datetime(),
|
|
updated_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% ------------------- СЕССИИ АДМИНИСТРАТОРОВ ---------------------------
|
|
-record(admin_session, {
|
|
token :: binary(),
|
|
admin_id :: binary(),
|
|
expires_at :: calendar:datetime(),
|
|
type :: refresh
|
|
}).
|
|
|
|
%% ------------------- ЕДИНАЯ МОДЕЛЬ СЕССИЙ (refresh JWT) ----------------
|
|
-record(auth_session, {
|
|
session_id :: binary(),
|
|
family_id :: binary(),
|
|
subject_id :: binary(),
|
|
subject_type :: user | admin,
|
|
client_type :: binary(),
|
|
current_jti :: binary(),
|
|
expires_at :: calendar:datetime(),
|
|
revoked :: boolean(),
|
|
created_at :: calendar:datetime(),
|
|
updated_at :: calendar:datetime(),
|
|
device_name :: binary(),
|
|
user_agent :: binary()
|
|
}).
|
|
|
|
%% ------------------- Календари ---------------------------------------
|
|
-record(calendar, {
|
|
id :: binary(),
|
|
owner_id :: binary(),
|
|
title :: binary(),
|
|
description :: binary(),
|
|
short_name :: binary(),
|
|
category :: binary(),
|
|
color :: binary(),
|
|
image_url :: binary(),
|
|
settings :: map(),
|
|
tags :: [binary()],
|
|
type :: personal | commercial,
|
|
confirmation :: auto | manual | {timeout, integer()}, % секунд
|
|
rating_avg :: float(),
|
|
rating_count :: non_neg_integer(),
|
|
status :: active | frozen | deleted,
|
|
reason :: binary(),
|
|
created_at :: calendar:datetime(),
|
|
updated_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% PK = id. Уникальность пары calendar_id+user_id — в logic.
|
|
-record(calendar_share, {
|
|
id :: binary(),
|
|
calendar_id :: binary(),
|
|
user_id :: binary(),
|
|
rights :: read | write | admin,
|
|
mirror_to_default :: boolean()
|
|
}).
|
|
|
|
%% Приглашения к calendar_share (соредактор / заместитель).
|
|
-record(calendar_share_invite, {
|
|
id :: binary(),
|
|
calendar_id :: binary(),
|
|
inviter_id :: binary(),
|
|
invitee_user_id :: binary(), % <<>> если ещё неизвестен
|
|
invitee_email :: binary(), % <<>> если только user_id
|
|
rights :: read | write | admin,
|
|
status :: pending | accepted | declined | expired | cancelled,
|
|
token :: binary(),
|
|
created_at :: calendar:datetime(),
|
|
expires_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% Follow чужого календаря (не путать с платной subscription / calendar_share)
|
|
-record(calendar_follow, {
|
|
id :: binary(),
|
|
calendar_id :: binary(),
|
|
user_id :: binary(),
|
|
created_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% ------------------- Специалисты календаря ---------------------------
|
|
%% PK = id (несколько специалистов на календарь). Уникальность пары
|
|
%% calendar_id+user_id — на уровне logic.
|
|
-record(calendar_specialist, {
|
|
id :: binary(),
|
|
calendar_id :: binary(),
|
|
user_id :: binary(), % id пользователя-специалиста
|
|
name :: binary(), % отображаемое имя в этом календаре
|
|
specialization :: [binary()], % список специализаций (услуг)
|
|
status :: active | inactive,
|
|
added_at :: calendar:datetime(),
|
|
updated_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% ------------------- Приглашения специалистов ------------------------
|
|
%% PK = id. Pending-уникальность (calendar + user|email) — в logic.
|
|
-record(specialist_invite, {
|
|
id :: binary(),
|
|
calendar_id :: binary(),
|
|
inviter_id :: binary(),
|
|
invitee_user_id :: binary(), % <<>> если ещё неизвестен
|
|
invitee_email :: binary(), % <<>> если только user_id
|
|
name :: binary(),
|
|
specialization :: [binary()],
|
|
status :: pending | accepted | declined | expired | cancelled,
|
|
token :: binary(),
|
|
created_at :: calendar:datetime(),
|
|
expires_at :: calendar:datetime()
|
|
}).
|
|
|
|
-record(location, {
|
|
address :: binary(),
|
|
lat :: float(),
|
|
lon :: float()
|
|
}).
|
|
|
|
%% ------------------- События (включая повторяющиеся) ----------------
|
|
-record(event, {
|
|
id :: binary(),
|
|
calendar_id :: binary(),
|
|
title :: binary(),
|
|
description :: binary(),
|
|
event_type :: single | recurring,
|
|
start_time :: calendar:datetime(),
|
|
duration :: integer(), % минуты
|
|
recurrence_rule :: binary(),
|
|
master_id :: binary(),
|
|
is_instance :: boolean(),
|
|
specialist_id :: binary(),
|
|
location :: #location{},
|
|
tags :: [binary()],
|
|
capacity :: integer(),
|
|
online_link :: binary(),
|
|
status :: active | cancelled | completed | frozen | deleted,
|
|
reason :: binary(),
|
|
rating_avg :: float(),
|
|
rating_count :: non_neg_integer(),
|
|
attachments :: [binary()],
|
|
edit_history :: [map()],
|
|
created_at :: calendar:datetime(),
|
|
updated_at :: calendar:datetime()
|
|
}).
|
|
|
|
-record(recurrence_exception, {
|
|
master_id :: binary(),
|
|
original_start :: calendar:datetime(),
|
|
action :: cancel | reschedule,
|
|
new_start :: calendar:datetime()
|
|
}).
|
|
|
|
%% ------------------- Бронирования ------------------------------------
|
|
-record(booking, {
|
|
id :: binary(),
|
|
event_id :: binary(), % ссылка на конкретный экземпляр события
|
|
user_id :: binary(),
|
|
status :: pending | confirmed | cancelled | expired,
|
|
notes :: binary(),
|
|
reminder_sent :: boolean(),
|
|
confirmed_at :: calendar:datetime(),
|
|
created_at :: calendar:datetime(),
|
|
updated_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% ------------------- Лист ожидания (commercial, settings.waitlist_enabled) ----
|
|
-record(waitlist_entry, {
|
|
id :: binary(),
|
|
event_id :: binary(),
|
|
user_id :: binary(),
|
|
status :: waiting | promoted | left,
|
|
created_at :: calendar:datetime(),
|
|
updated_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% ------------------- Отзывы ------------------------------------------
|
|
-record(review, {
|
|
id :: binary(),
|
|
user_id :: binary(),
|
|
target_type :: calendar | event,
|
|
target_id :: binary(),
|
|
rating :: 1..5,
|
|
comment :: binary(),
|
|
status :: visible | hidden | deleted,
|
|
reason :: binary(),
|
|
likes :: non_neg_integer(),
|
|
dislikes :: non_neg_integer(),
|
|
created_at :: calendar:datetime(),
|
|
updated_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% Голос пользователя за отзыв (уникальность review_id+user_id — в транзакции)
|
|
-record(review_vote, {
|
|
id :: binary(),
|
|
review_id :: binary(),
|
|
user_id :: binary(),
|
|
value :: like | dislike,
|
|
created_at :: calendar:datetime(),
|
|
updated_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% ------------------- Жалобы и модерация ------------------------------
|
|
-record(report, {
|
|
id :: binary(),
|
|
reporter_id :: binary(),
|
|
target_type :: calendar | event | review,
|
|
target_id :: binary(),
|
|
reason :: binary(),
|
|
status :: pending | reviewed | dismissed,
|
|
created_at :: calendar:datetime(),
|
|
resolved_at :: calendar:datetime(),
|
|
resolved_by :: binary()
|
|
}).
|
|
|
|
-record(banned_word, {
|
|
id :: binary(),
|
|
word :: binary(),
|
|
added_by :: binary(), % id администратора, добавившего слово
|
|
added_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% ------------------- Автомодерация -----------------------------------
|
|
-record(automod_settings, {
|
|
id :: binary(), % singleton <<"default">>
|
|
keyword_action :: reject | flag | censor,
|
|
match_mode :: word_boundary | substring,
|
|
report_threshold :: pos_integer(),
|
|
updated_at :: calendar:datetime(),
|
|
updated_by :: binary()
|
|
}).
|
|
|
|
-record(automod_hit, {
|
|
id :: binary(),
|
|
entity_type :: calendar | event | review,
|
|
entity_id :: binary(),
|
|
trigger :: keyword | report_threshold,
|
|
matched_words :: [binary()],
|
|
action_taken :: reject | flag | censor | freeze | hide,
|
|
status :: open | approved | rejected,
|
|
created_at :: calendar:datetime(),
|
|
resolved_at :: calendar:datetime() | undefined,
|
|
resolved_by :: binary()
|
|
}).
|
|
|
|
%% ------------------- Баг-трекер --------------------------------------
|
|
-record(ticket, {
|
|
id :: binary(),
|
|
reporter_id :: binary(),
|
|
error_hash :: binary(),
|
|
error_message :: binary(),
|
|
stacktrace :: binary(),
|
|
context :: binary(),
|
|
count :: non_neg_integer(),
|
|
first_seen :: calendar:datetime(),
|
|
last_seen :: calendar:datetime(),
|
|
status :: open | in_progress | resolved | closed,
|
|
assigned_to :: binary(),
|
|
resolution_note :: binary(),
|
|
closed_at :: calendar:datetime() | undefined,
|
|
source :: binary() % <<"backend">> | <<"frontend">> | <<"manual">>
|
|
}).
|
|
|
|
%% ------------------- Подписки ----------------------------------------
|
|
-record(subscription, {
|
|
id :: binary(),
|
|
user_id :: binary(),
|
|
plan :: monthly | quarterly | biannual | annual,
|
|
status :: active | expired | cancelled,
|
|
trial_used :: boolean(),
|
|
started_at :: calendar:datetime(),
|
|
expires_at :: calendar:datetime(),
|
|
created_at :: calendar:datetime(),
|
|
updated_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% ------------------- Аудит -------------------------------------------
|
|
-record(admin_audit, {
|
|
id :: binary(),
|
|
admin_id :: binary(),
|
|
email :: binary(),
|
|
role :: atom(),
|
|
action :: binary(),
|
|
entity_type :: binary(),
|
|
entity_id :: binary(),
|
|
timestamp :: calendar:datetime(),
|
|
ip :: binary(),
|
|
reason :: binary()
|
|
}).
|
|
|
|
%% ------------------- Уведомления ------------------------
|
|
-record(notification, {
|
|
id :: binary(),
|
|
user_id :: binary(),
|
|
type :: booking_confirmed | event_reminder | event_cancelled |
|
|
specialist_invite | waitlist_promoted | custom,
|
|
title :: binary(),
|
|
body :: binary(),
|
|
is_read :: boolean(),
|
|
created_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% Web Push subscription (Back#75). Prefs live in user.preferences:
|
|
%% notify_email / notify_push (boolean, default true).
|
|
-record(push_subscription, {
|
|
id :: binary(),
|
|
user_id :: binary(),
|
|
endpoint :: binary(),
|
|
p256dh :: binary(),
|
|
auth :: binary(),
|
|
created_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% Upsert-счётчики админ-статистики (абсолютные значения).
|
|
%% key: atom() | {atom(), atom()} | {atom(), atom(), atom()}
|
|
%% users_total-подобные totals не храним (table_info),
|
|
%% размерности: {user_by_status, pending}, {event_by_type, single}, ...
|
|
-record(stats_counter, {
|
|
key :: term(),
|
|
value :: integer(),
|
|
updated_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% Дневные бакеты: key = {Metric :: atom(), Date :: calendar:date()}.
|
|
-record(stats_daily, {
|
|
key :: {atom(), calendar:date()},
|
|
value :: integer(),
|
|
updated_at :: calendar:datetime()
|
|
}).
|
|
|
|
-record(node_metric, {
|
|
timestamp :: calendar:datetime(),
|
|
node :: atom(),
|
|
memory_total :: non_neg_integer(),
|
|
memory_processes :: non_neg_integer(),
|
|
memory_atom :: non_neg_integer(),
|
|
memory_binary :: non_neg_integer(),
|
|
memory_ets :: non_neg_integer(),
|
|
process_count :: non_neg_integer(),
|
|
run_queue :: non_neg_integer(),
|
|
mnesia_commits :: non_neg_integer(),
|
|
mnesia_failures :: non_neg_integer(),
|
|
table_sizes :: map(),
|
|
active_sessions :: non_neg_integer(),
|
|
ws_connections :: non_neg_integer(),
|
|
io_in :: non_neg_integer(),
|
|
io_out :: non_neg_integer(),
|
|
memory_available :: non_neg_integer(),
|
|
cpu_utilization :: float()
|
|
}).
|
|
|
|
-record(month_snapshot, {
|
|
id :: {binary(), binary()},
|
|
calendar_id :: binary(),
|
|
year_month :: binary(),
|
|
payload :: binary(),
|
|
storage :: mnesia | file,
|
|
file_rel :: binary(),
|
|
created_at :: calendar:datetime()
|
|
}).
|
|
|
|
-record(schema_migration, {
|
|
version :: string(),
|
|
applied_at :: calendar:datetime()
|
|
}).
|
|
|
|
%% Пилотные метрики ИИ-подсказок (PRODUCT-AI, Spec#22): дневные счётчики
|
|
%% shown/confirm/dismiss по паттерну. Id = {user_id, day, pattern_key, kind}.
|
|
-record(ai_hint_metric, {
|
|
id :: {binary(), binary(), binary(), binary()},
|
|
user_id :: binary(),
|
|
day :: binary(),
|
|
pattern_key :: binary(),
|
|
kind :: binary(),
|
|
count :: non_neg_integer(),
|
|
updated_at :: calendar:datetime()
|
|
}). |