Избавление от undefined, всем необязательным полям присваиваются дефолтные значения #22

This commit is contained in:
2026-05-27 16:18:33 +03:00
parent ae6fe21ce6
commit b78df927dd
21 changed files with 2037 additions and 1088 deletions
+214 -116
View File
@@ -1,186 +1,247 @@
-module(core_event).
-include("records.hrl").
-export([create/4, create_recurring/5, get_by_id/1, list_by_calendar/1,
update/2, delete/1, materialize_occurrence/3]).
-export([create/4, create_recurring/5, get_by_id/1, list_by_calendar/1, update/2, delete/1,
materialize_occurrence/3]).
-export([count_events/0, count_events_by_date/2]).
-export([freeze/2, unfreeze/2]).
-export([list_all/0]).
%% Создание одиночного события
%% ─────────────────────────────────────────────────────────────────
%% Значения по умолчанию для необязательных полей
%% ─────────────────────────────────────────────────────────────────
-define(DEFAULT_RECURRENCE_RULE, <<>>).
-define(DEFAULT_MASTER_ID, <<>>).
-define(DEFAULT_SPECIALIST_ID, <<>>).
-define(DEFAULT_LOCATION, #location{address = <<>>, lat = 0.0, lon = 0.0}).
-define(DEFAULT_TAGS, []).
-define(DEFAULT_CAPACITY, 0).
-define(DEFAULT_ONLINE_LINK, <<>>).
-define(DEFAULT_REASON, <<>>).
-define(DEFAULT_ATTACHMENTS, []).
-define(DEFAULT_EDIT_HISTORY, []).
%%%-------------------------------------------------------------------
%%% @doc Создание одиночного события.
%%% Все необязательные поля инициализируются значениями по умолчанию.
%%% @end
%%%-------------------------------------------------------------------
-spec create(CalendarId :: binary(), Title :: binary(),
StartTime :: calendar:datetime(), Duration :: non_neg_integer()) ->
{ok, #event{}} | {error, term()}.
create(CalendarId, Title, StartTime, Duration) ->
Id = infra_utils:generate_id(16),
Now = calendar:universal_time(),
Event = #event{
id = Id,
calendar_id = CalendarId,
title = Title,
description = <<"">>,
event_type = single,
start_time = StartTime,
duration = Duration,
recurrence_rule = undefined,
master_id = undefined,
is_instance = false,
specialist_id = undefined,
location = undefined,
tags = [],
capacity = undefined,
online_link = undefined,
status = active,
rating_avg = 0.0,
rating_count = 0,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
id = Id,
calendar_id = CalendarId,
title = Title,
description = <<"">>,
event_type = single,
start_time = StartTime,
duration = Duration,
recurrence_rule = ?DEFAULT_RECURRENCE_RULE,
master_id = ?DEFAULT_MASTER_ID,
is_instance = false,
specialist_id = ?DEFAULT_SPECIALIST_ID,
location = ?DEFAULT_LOCATION,
tags = ?DEFAULT_TAGS,
capacity = ?DEFAULT_CAPACITY,
online_link = ?DEFAULT_ONLINE_LINK,
status = active,
reason = ?DEFAULT_REASON,
rating_avg = 0.0,
rating_count = 0,
attachments = ?DEFAULT_ATTACHMENTS,
edit_history = ?DEFAULT_EDIT_HISTORY,
created_at = Now,
updated_at = Now
},
F = fun() ->
mnesia:write(Event),
{ok, Event}
end,
F = fun() -> mnesia:write(Event), {ok, Event} end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
end.
%% Создание повторяющегося события (мастер-запись)
%%%-------------------------------------------------------------------
%%% @doc Создание повторяющегося события (мастер-запись).
%%% @end
%%%-------------------------------------------------------------------
-spec create_recurring(CalendarId :: binary(), Title :: binary(),
StartTime :: calendar:datetime(), Duration :: non_neg_integer(),
RRule :: map()) ->
{ok, #event{}} | {error, term()}.
create_recurring(CalendarId, Title, StartTime, Duration, RRule) ->
Id = infra_utils:generate_id(16),
Now = calendar:universal_time(),
Event = #event{
id = Id,
calendar_id = CalendarId,
title = Title,
description = <<"">>,
event_type = recurring,
start_time = StartTime,
duration = Duration,
recurrence_rule = jsx:encode(RRule),
master_id = undefined,
is_instance = false,
specialist_id = undefined,
location = undefined,
tags = [],
capacity = undefined,
online_link = undefined,
status = active,
rating_avg = 0.0,
rating_count = 0,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
id = Id,
calendar_id = CalendarId,
title = Title,
description = <<"">>,
event_type = recurring,
start_time = StartTime,
duration = Duration,
recurrence_rule = jsx:encode(RRule),
master_id = ?DEFAULT_MASTER_ID,
is_instance = false,
specialist_id = ?DEFAULT_SPECIALIST_ID,
location = ?DEFAULT_LOCATION,
tags = ?DEFAULT_TAGS,
capacity = ?DEFAULT_CAPACITY,
online_link = ?DEFAULT_ONLINE_LINK,
status = active,
reason = ?DEFAULT_REASON,
rating_avg = 0.0,
rating_count = 0,
attachments = ?DEFAULT_ATTACHMENTS,
edit_history = ?DEFAULT_EDIT_HISTORY,
created_at = Now,
updated_at = Now
},
F = fun() ->
mnesia:write(Event),
{ok, Event}
end,
F = fun() -> mnesia:write(Event), {ok, Event} end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
end.
%% Материализация вхождения повторяющегося события
%%%-------------------------------------------------------------------
%%% @doc Материализация вхождения повторяющегося события.
%%% @end
%%%-------------------------------------------------------------------
-spec materialize_occurrence(MasterId :: binary(),
OccurrenceStart :: calendar:datetime(),
SpecialistId :: binary()) ->
{ok, #event{}} | {error, term()}.
materialize_occurrence(MasterId, OccurrenceStart, SpecialistId) ->
case mnesia:dirty_read(event, MasterId) of
[] ->
{error, master_not_found};
[Master] when Master#event.event_type =:= recurring ->
% Проверяем, не существует ли уже материализованное вхождение
Existing = mnesia:dirty_match_object(
#event{master_id = MasterId, start_time = OccurrenceStart, is_instance = true, _ = '_'}
#event{master_id = MasterId, start_time = OccurrenceStart,
is_instance = true, _ = '_'}
),
case Existing of
[] ->
% Создаём новый экземпляр
InstanceId = infra_utils:generate_id(16),
Now = calendar:universal_time(),
Instance = #event{
id = InstanceId,
calendar_id = Master#event.calendar_id,
title = Master#event.title,
description = Master#event.description,
event_type = single,
start_time = OccurrenceStart,
duration = Master#event.duration,
recurrence_rule = undefined,
master_id = MasterId,
is_instance = true,
specialist_id = SpecialistId,
location = Master#event.location,
tags = Master#event.tags,
capacity = Master#event.capacity,
online_link = Master#event.online_link,
status = active,
rating_avg = 0.0,
rating_count = 0,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
id = InstanceId,
calendar_id = Master#event.calendar_id,
title = Master#event.title,
description = Master#event.description,
event_type = single,
start_time = OccurrenceStart,
duration = Master#event.duration,
recurrence_rule = ?DEFAULT_RECURRENCE_RULE,
master_id = MasterId,
is_instance = true,
specialist_id = SpecialistId,
location = Master#event.location,
tags = Master#event.tags,
capacity = Master#event.capacity,
online_link = Master#event.online_link,
status = active,
reason = ?DEFAULT_REASON,
rating_avg = 0.0,
rating_count = 0,
attachments = ?DEFAULT_ATTACHMENTS,
edit_history = ?DEFAULT_EDIT_HISTORY,
created_at = Now,
updated_at = Now
},
F = fun() ->
mnesia:write(Instance),
{ok, Instance}
end,
F = fun() -> mnesia:write(Instance), {ok, Instance} end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
end;
[Instance] ->
% Уже существует, возвращаем его
{ok, Instance}
end;
[_] ->
{error, not_recurring}
end.
%% Получение события по ID
%%%-------------------------------------------------------------------
%%% @doc Получить событие по идентификатору.
%%% @end
%%%-------------------------------------------------------------------
-spec get_by_id(Id :: binary()) -> {ok, #event{}} | {error, not_found}.
get_by_id(Id) ->
case mnesia:dirty_read(event, Id) of
[] -> {error, not_found};
[Event] -> {ok, Event}
end.
%% Список событий календаря (только мастер-записи и одиночные)
%%%-------------------------------------------------------------------
%%% @doc Список активных событий календаря.
%%% @end
%%%-------------------------------------------------------------------
-spec list_by_calendar(CalendarId :: binary()) -> {ok, [#event{}]}.
list_by_calendar(CalendarId) ->
Match = #event{calendar_id = CalendarId, status = active, is_instance = false, _ = '_'},
Events = mnesia:dirty_match_object(Match),
{ok, Events}.
%% Обновление события
%%%-------------------------------------------------------------------
%%% @doc Обновить поля события.
%%% `Updates` список пар `[{atom(), term()}]`.
%%% Поля, не указанные в обновлении, сохраняют прежние значения.
%%% @end
%%%-------------------------------------------------------------------
-spec update(Id :: binary(), Updates :: [{atom(), term()}]) ->
{ok, #event{}} | {error, not_found | term()}.
update(Id, Updates) ->
F = fun() ->
case mnesia:read(event, Id) of
[] ->
{error, not_found};
[] -> {error, not_found};
[Event] ->
UpdatedEvent = apply_updates(Event, Updates),
mnesia:write(UpdatedEvent),
{ok, UpdatedEvent}
end
end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
end.
%% Удаление события (soft delete)
%%%-------------------------------------------------------------------
%%% @doc Мягкое удаление события (установка статуса `deleted`).
%%% @end
%%%-------------------------------------------------------------------
-spec delete(Id :: binary()) -> {ok, #event{}} | {error, not_found | term()}.
delete(Id) ->
update(Id, [{status, deleted}]).
%%%-------------------------------------------------------------------
%%% @doc Количество записей в таблице `event`.
%%% @end
%%%-------------------------------------------------------------------
-spec count_events() -> non_neg_integer().
count_events() ->
mnesia:table_info(event, size).
%%%-------------------------------------------------------------------
%%% @doc Получить список всех активных мастер-событий.
%%% @end
%%%-------------------------------------------------------------------
-spec list_all() -> [#event{}].
list_all() ->
Match = #event{status = active, is_instance = false, _ = '_'},
mnesia:dirty_match_object(Match).
%%%-------------------------------------------------------------------
%%% @doc Подсчёт событий, созданных в заданном временном диапазоне.
%%% Возвращает список кортежей `{{Year,Month,Day}, Count}`, отсортированный по дате.
%%% @end
%%%-------------------------------------------------------------------
-spec count_events_by_date(From :: calendar:datetime(), To :: calendar:datetime()) ->
[{{pos_integer(), pos_integer(), pos_integer()}, non_neg_integer()}].
count_events_by_date(From, To) ->
All = mnesia:dirty_match_object(#event{_ = '_'}),
Filtered = lists:filter(fun(E) ->
E#event.created_at >= From andalso E#event.created_at =< To
end, All),
Filtered = lists:filter(fun(E) -> E#event.created_at >= From andalso
E#event.created_at =< To end, All),
Counts = lists:foldl(fun(E, Acc) ->
Day = date_part(E#event.created_at),
case lists:keyfind(Day, 1, Acc) of
@@ -190,32 +251,69 @@ count_events_by_date(From, To) ->
end, [], Filtered),
lists:sort(Counts).
%%%-------------------------------------------------------------------
%%% @doc Выделить из datetime только дату `{Y,M,D}`.
%%% @end
%%%-------------------------------------------------------------------
-spec date_part(calendar:datetime()) -> {pos_integer(), pos_integer(), pos_integer()}.
date_part({{Y,M,D}, _}) -> {Y,M,D}.
%%%-------------------------------------------------------------------
%%% @doc Применить список обновлений к записи события.
%%% @end
%%%-------------------------------------------------------------------
-spec apply_updates(#event{}, [{atom(), term()}]) -> #event{}.
apply_updates(Event, Updates) ->
Updated = lists:foldl(fun({Field, Value}, E) ->
set_field(Field, Value, E)
end, Event, Updates),
Updated = lists:foldl(fun({Field, Value}, E) -> set_field(Field, Value, E) end,
Event, Updates),
Updated#event{updated_at = calendar:universal_time()}.
%%%-------------------------------------------------------------------
%%% @doc Заморозить событие с указанием причины.
%%% @end
%%%-------------------------------------------------------------------
-spec freeze(Id :: binary(), Reason :: binary()) ->
{ok, #event{}} | {error, not_found | term()}.
freeze(Id, Reason) ->
update(Id, [{status, frozen}, {reason, Reason}]).
%%%-------------------------------------------------------------------
%%% @doc Разморозить событие с указанием причины.
%%% @end
%%%-------------------------------------------------------------------
-spec unfreeze(Id :: binary(), Reason :: binary()) ->
{ok, #event{}} | {error, not_found | term()}.
unfreeze(Id, Reason) ->
update(Id, [{status, active}, {reason, Reason}]).
%% ── ОБНОВЛЁННАЯ set_field ──────────────────────────────────
set_field(title, Value, E) -> E#event{title = Value};
set_field(description, Value, E) -> E#event{description = Value};
set_field(start_time, Value, E) -> E#event{start_time = Value};
set_field(duration, Value, E) -> E#event{duration = Value};
set_field(specialist_id, Value, E) -> E#event{specialist_id = Value};
set_field(location, Value, E) -> E#event{location = Value};
set_field(tags, Value, E) -> E#event{tags = Value};
set_field(capacity, Value, E) -> E#event{capacity = Value};
set_field(online_link, Value, E) -> E#event{online_link = Value};
set_field(status, Value, E) -> E#event{status = Value};
set_field(reason, Value, E) -> E#event{reason = Value};
set_field(rating_avg, Value, E) -> E#event{rating_avg = Value};
set_field(rating_count, Value, E) -> E#event{rating_count = Value};
%%%===================================================================
%%% ВНУТРЕННИЕ ФУНКЦИИ
%%%===================================================================
%%%-------------------------------------------------------------------
%%% @doc Установить одно поле записи события.
%%% Неизвестные поля игнорируются.
%%% @end
%%%-------------------------------------------------------------------
-spec set_field(atom(), term(), #event{}) -> #event{}.
set_field(title, Value, E) -> E#event{title = Value};
set_field(description, Value, E) -> E#event{description = Value};
set_field(start_time, Value, E) -> E#event{start_time = Value};
set_field(duration, Value, E) -> E#event{duration = Value};
set_field(specialist_id, Value, E) -> E#event{specialist_id = Value};
set_field(location, Value, E) -> E#event{location = Value};
set_field(tags, Value, E) -> E#event{tags = Value};
set_field(capacity, Value, E) -> E#event{capacity = Value};
set_field(online_link, Value, E) -> E#event{online_link = Value};
set_field(status, Value, E) -> E#event{status = Value};
set_field(reason, Value, E) -> E#event{reason = Value};
set_field(rating_avg, Value, E) -> E#event{rating_avg = Value};
set_field(rating_count, Value, E) -> E#event{rating_count = Value};
set_field(attachments, Value, E) -> E#event{attachments = Value};
set_field(edit_history, Value, E) -> E#event{edit_history = Value};
set_field(recurrence_rule, Value, E) -> E#event{recurrence_rule = Value};
set_field(master_id, Value, E) -> E#event{master_id = Value};
set_field(is_instance, Value, E) -> E#event{is_instance = Value};
set_field(event_type, Value, E) -> E#event{event_type = Value};
set_field(calendar_id, Value, E) -> E#event{calendar_id = Value};
set_field(_, _, E) -> E.