This commit is contained in:
2026-04-20 21:04:16 +03:00
parent b24cbc97f3
commit 19f82768e4
18 changed files with 1851 additions and 131 deletions
+18 -8
View File
@@ -1,11 +1,15 @@
-module(logic_calendar).
-include("records.hrl").
-export([create_calendar/4, get_calendar/2, list_calendars/1,
-export([create_calendar/3, create_calendar/4, get_calendar/2, list_calendars/1,
update_calendar/3, delete_calendar/2]).
-export([can_access/2, can_edit/2]).
%% Создание календаря
%% Создание календаря с политикой по умолчанию (manual)
create_calendar(UserId, Title, Description) ->
create_calendar(UserId, Title, Description, manual).
%% Создание календаря с указанной политикой подтверждения
create_calendar(UserId, Title, Description, Confirmation) ->
case core_user:get_by_id(UserId) of
{ok, User} ->
@@ -41,7 +45,6 @@ update_calendar(UserId, CalendarId, Updates) ->
{ok, Calendar} ->
case can_edit(UserId, Calendar) of
true ->
% Валидация обновлений
ValidUpdates = validate_updates(Updates),
core_calendar:update(CalendarId, ValidUpdates);
false ->
@@ -66,13 +69,20 @@ delete_calendar(UserId, CalendarId) ->
end.
%% Проверка прав доступа (просмотр)
can_access(UserId, #calendar{owner_id = UserId, status = active}) -> true;
can_access(_UserId, #calendar{type = commercial, status = active}) -> true;
can_access(_UserId, _) -> false.
can_access(UserId, #calendar{owner_id = UserId, status = active}) ->
true;
can_access(_UserId, #calendar{type = commercial, status = active}) ->
true;
can_access(_UserId, _) ->
false.
%% Проверка прав редактирования
can_edit(UserId, #calendar{owner_id = UserId, status = active}) -> true;
can_edit(_, _) -> false.
can_edit(UserId, #calendar{owner_id = UserId, status = active}) ->
true;
can_edit(_UserId, #calendar{owner_id = _OwnerId}) ->
false;
can_edit(_, _) ->
false.
%% Валидация полей обновления
validate_updates(Updates) ->
+39 -30
View File
@@ -3,7 +3,7 @@
-export([create_event/5, create_recurring_event/6, get_event/2, list_events/2,
update_event/3, delete_event/2]).
-export([validate_event_time/1, get_occurrences/3, cancel_occurrence/3]).
-export([validate_event_time/1, validate_event_time/2, get_occurrences/3, cancel_occurrence/3]).
-export([materialize_for_booking/3]).
%% Создание одиночного события
@@ -12,7 +12,7 @@ create_event(UserId, CalendarId, Title, StartTime, Duration) ->
{ok, Calendar} ->
case logic_calendar:can_edit(UserId, Calendar) of
true ->
case validate_event_time(StartTime) of
case validate_event_time(StartTime, UserId) of
ok ->
core_event:create(CalendarId, Title, StartTime, Duration);
{error, _} = Error ->
@@ -31,7 +31,7 @@ create_recurring_event(UserId, CalendarId, Title, StartTime, Duration, RRule) ->
{ok, Calendar} ->
case logic_calendar:can_edit(UserId, Calendar) of
true ->
case validate_event_time(StartTime) of
case validate_event_time(StartTime, UserId) of
ok ->
case logic_recurrence:validate_rrule(RRule) of
true ->
@@ -49,7 +49,6 @@ create_recurring_event(UserId, CalendarId, Title, StartTime, Duration, RRule) ->
Error
end.
%% Получение вхождений повторяющегося события в диапазоне
%% Получение вхождений повторяющегося события в диапазоне
get_occurrences(UserId, MasterId, RangeEnd) ->
case get_event(UserId, MasterId) of
@@ -61,18 +60,12 @@ get_occurrences(UserId, MasterId, RangeEnd) ->
end,
{ok, ParsedRule} = logic_recurrence:parse_rrule(RRuleMap),
% Генерируем вхождения
Occurrences = logic_recurrence:generate_occurrences(
Event#event.start_time, ParsedRule, RangeEnd
),
% Получаем исключения
Exceptions = get_exceptions(MasterId),
% Фильтруем отменённые вхождения
ValidOccurrences = filter_cancelled(Occurrences, Exceptions),
% Проверяем материализованные вхождения (могут иметь изменения)
FinalOccurrences = merge_materialized(MasterId, ValidOccurrences),
{ok, FinalOccurrences};
@@ -90,7 +83,6 @@ cancel_occurrence(UserId, MasterId, OccurrenceStart) ->
{ok, Calendar} ->
case logic_calendar:can_edit(UserId, Calendar) of
true ->
% Добавляем исключение
Exception = #recurrence_exception{
master_id = MasterId,
original_start = OccurrenceStart,
@@ -144,7 +136,7 @@ update_event(UserId, EventId, Updates) ->
{ok, Calendar} ->
case logic_calendar:can_edit(UserId, Calendar) of
true ->
ValidUpdates = validate_updates(Updates),
ValidUpdates = validate_updates(Updates, UserId),
core_event:update(EventId, ValidUpdates);
false ->
{error, access_denied}
@@ -175,36 +167,53 @@ delete_event(UserId, EventId) ->
Error
end.
%% Валидация времени события
%% Валидация времени события (без учёта пользователя)
validate_event_time(StartTime) ->
Now = calendar:universal_time(),
case StartTime > Now of
true -> ok;
false -> {error, event_in_past}
validate_event_time(StartTime, undefined).
%% Валидация времени события с учётом роли пользователя
validate_event_time(StartTime, UserId) ->
case is_admin(UserId) of
true ->
ok;
false ->
Now = calendar:universal_time(),
case StartTime > Now of
true -> ok;
false -> {error, event_in_past}
end
end.
%% Проверка, является ли пользователь администратором
is_admin(undefined) -> false;
is_admin(UserId) ->
case core_user:get_by_id(UserId) of
{ok, User} -> User#user.role =:= admin;
_ -> false
end.
%% Внутренние функции
validate_updates(Updates) ->
lists:filter(fun validate_update/1, Updates).
validate_updates(Updates, UserId) ->
lists:filter(fun(Update) -> validate_update(Update, UserId) end, Updates).
validate_update({title, Value}) when is_binary(Value) -> true;
validate_update({description, Value}) when is_binary(Value) -> true;
validate_update({start_time, Value}) ->
case validate_event_time(Value) of
validate_update({title, Value}, _) when is_binary(Value) -> true;
validate_update({description, Value}, _) when is_binary(Value) -> true;
validate_update({start_time, Value}, UserId) ->
case validate_event_time(Value, UserId) of
ok -> true;
_ -> false
end;
validate_update({duration, Value}) when is_integer(Value), Value > 0 -> true;
validate_update({specialist_id, Value}) when is_binary(Value) -> true;
validate_update({location, Value}) ->
validate_update({duration, Value}, _) when is_integer(Value), Value > 0 -> true;
validate_update({specialist_id, Value}, _) when is_binary(Value) -> true;
validate_update({location, Value}, _) ->
case Value of
#location{} -> true;
_ -> false
end;
validate_update({tags, Value}) when is_list(Value) -> true;
validate_update({capacity, Value}) when is_integer(Value), Value > 0 -> true;
validate_update({online_link, Value}) when is_binary(Value) -> true;
validate_update(_) -> false.
validate_update({tags, Value}, _) when is_list(Value) -> true;
validate_update({capacity, Value}, _) when is_integer(Value), Value > 0 -> true;
validate_update({online_link, Value}, _) when is_binary(Value) -> true;
validate_update(_, _) -> false.
get_exceptions(MasterId) ->
Match = #recurrence_exception{master_id = MasterId, _ = '_'},
+241
View File
@@ -0,0 +1,241 @@
-module(logic_search).
-include("records.hrl").
-export([search/4]).
-define(DEFAULT_LIMIT, 20).
-define(MAX_LIMIT, 100).
-define(EARTH_RADIUS_KM, 6371.0).
%% Поиск событий и календарей
search(Type, Query, UserId, Params) ->
Limit = min(maps:get(limit, Params, ?DEFAULT_LIMIT), ?MAX_LIMIT),
Offset = maps:get(offset, Params, 0),
case Type of
<<"event">> -> search_events(Query, UserId, Params, Limit, Offset);
<<"calendar">> -> search_calendars(Query, UserId, Params, Limit, Offset);
_ -> search_all(Query, UserId, Params, Limit, Offset)
end.
%% ============ Поиск событий ============
search_events(Query, UserId, Params, Limit, Offset) ->
AllEvents = get_all_events(),
AccessibleEvents = filter_accessible_events(AllEvents, UserId),
Filtered = apply_event_filters(AccessibleEvents, Query, Params),
Sorted = sort_events(Filtered, Params),
Paginated = paginate(Sorted, Limit, Offset),
{ok, length(Filtered), format_events(Paginated)}.
%% ============ Поиск календарей ============
search_calendars(Query, UserId, Params, Limit, Offset) ->
AllCalendars = get_all_calendars(),
AccessibleCalendars = filter_accessible_calendars(AllCalendars, UserId),
Filtered = apply_calendar_filters(AccessibleCalendars, Query, Params),
Paginated = paginate(Filtered, Limit, Offset),
{ok, length(Filtered), format_calendars(Paginated)}.
%% ============ Поиск всего ============
search_all(Query, UserId, Params, Limit, Offset) ->
{ok, EventsTotal, Events} = search_events(Query, UserId, Params, Limit, Offset),
{ok, CalendarsTotal, Calendars} = search_calendars(Query, UserId, Params, Limit, Offset),
{ok, EventsTotal + CalendarsTotal, #{
events => Events,
calendars => Calendars
}}.
%% ============ Получение данных ============
get_all_events() ->
Match = #event{status = active, is_instance = false, _ = '_'},
mnesia:dirty_match_object(Match).
get_all_calendars() ->
Match = #calendar{status = active, _ = '_'},
mnesia:dirty_match_object(Match).
%% ============ Фильтрация по доступности ============
filter_accessible_events(Events, UserId) ->
lists:filter(fun(Event) ->
case core_calendar:get_by_id(Event#event.calendar_id) of
{ok, Calendar} ->
CanAccess = logic_calendar:can_access(UserId, Calendar),
case CanAccess of
false ->
io:format("Access denied for user ~p to calendar ~p (type: ~p, owner: ~p, status: ~p)~n",
[UserId, Calendar#calendar.id, Calendar#calendar.type,
Calendar#calendar.owner_id, Calendar#calendar.status]);
true -> ok
end,
CanAccess;
_ -> false
end
end, Events).
filter_accessible_calendars(Calendars, UserId) ->
lists:filter(fun(Calendar) ->
logic_calendar:can_access(UserId, Calendar)
end, Calendars).
%% ============ Применение фильтров ============
apply_event_filters(Events, Query, Params) ->
Events1 = filter_by_text(Events, Query),
Events2 = filter_by_tags(Events1, Params),
Events3 = filter_by_date_range(Events2, Params),
filter_by_location(Events3, Params).
apply_calendar_filters(Calendars, Query, Params) ->
Calendars1 = filter_by_text(Calendars, Query),
filter_by_tags(Calendars1, Params).
filter_by_text(Items, undefined) -> Items;
filter_by_text(Items, <<>>) -> Items;
filter_by_text(Items, Query) ->
QueryLower = string:lowercase(Query),
lists:filter(fun(Item) ->
Title = get_title(Item),
Description = get_description(Item),
string:find(string:lowercase(Title), QueryLower) =/= nomatch orelse
string:find(string:lowercase(Description), QueryLower) =/= nomatch
end, Items).
filter_by_tags(Items, Params) ->
case maps:get(tags, Params, undefined) of
undefined -> Items;
TagsStr ->
Tags = [string:trim(T) || T <- string:split(TagsStr, ",", all)],
lists:filter(fun(Item) ->
ItemTags = get_tags(Item),
has_any_tag(ItemTags, Tags)
end, Items)
end.
filter_by_date_range(Events, Params) ->
From = maps:get(from, Params, undefined),
To = maps:get(to, Params, undefined),
case {From, To} of
{undefined, undefined} -> Events;
_ ->
lists:filter(fun(Event) ->
StartTime = Event#event.start_time,
(From =:= undefined orelse StartTime >= From) andalso
(To =:= undefined orelse StartTime =< To)
end, Events)
end.
filter_by_location(Events, Params) ->
case {maps:get(lat, Params, undefined), maps:get(lon, Params, undefined)} of
{undefined, _} -> Events;
{_, undefined} -> Events;
{Lat, Lon} ->
Radius = maps:get(radius, Params, 10),
lists:filter(fun(Event) ->
case Event#event.location of
undefined -> false;
#location{lat = EventLat, lon = EventLon} ->
distance(Lat, Lon, EventLat, EventLon) =< Radius
end
end, Events)
end.
%% ============ Вспомогательные функции ============
get_title(#event{title = Title}) -> Title;
get_title(#calendar{title = Title}) -> Title.
get_description(#event{description = Desc}) -> Desc;
get_description(#calendar{description = Desc}) -> Desc.
get_tags(#event{tags = Tags}) -> Tags;
get_tags(#calendar{tags = Tags}) -> Tags.
has_any_tag(ItemTags, SearchTags) ->
lists:any(fun(Tag) -> lists:member(Tag, ItemTags) end, SearchTags).
%% ============ Гео-вычисления ============
distance(Lat1, Lon1, Lat2, Lon2) ->
DLat = deg_to_rad(Lat2 - Lat1),
DLon = deg_to_rad(Lon2 - Lon1),
A = math:sin(DLat / 2) * math:sin(DLat / 2) +
math:cos(deg_to_rad(Lat1)) * math:cos(deg_to_rad(Lat2)) *
math:sin(DLon / 2) * math:sin(DLon / 2),
C = 2 * math:atan2(math:sqrt(A), math:sqrt(1 - A)),
?EARTH_RADIUS_KM * C.
deg_to_rad(Deg) -> Deg * math:pi() / 180.
%% ============ Сортировка ============
sort_events(Events, Params) ->
SortBy = maps:get(sort, Params, <<"start_time">>),
Order = maps:get(order, Params, <<"asc">>),
Sorted = case SortBy of
<<"start_time">> ->
lists:sort(fun(A, B) -> A#event.start_time =< B#event.start_time end, Events);
<<"rating">> ->
lists:sort(fun(A, B) -> A#event.rating_avg >= B#event.rating_avg end, Events);
<<"created_at">> ->
lists:sort(fun(A, B) -> A#event.created_at =< B#event.created_at end, Events);
_ -> Events
end,
case Order of
<<"desc">> -> lists:reverse(Sorted);
_ -> Sorted
end.
%% ============ Пагинация ============
paginate(List, Limit, Offset) ->
lists:sublist(List, Offset + 1, Limit).
%% ============ Форматирование ============
format_events(Events) ->
lists:map(fun format_event/1, Events).
format_event(Event) ->
Location = case Event#event.location of
undefined -> null;
#location{address = Addr, lat = Lat, lon = Lon} ->
#{address => Addr, lat => Lat, lon => Lon}
end,
#{
id => Event#event.id,
calendar_id => Event#event.calendar_id,
title => Event#event.title,
description => Event#event.description,
event_type => Event#event.event_type,
start_time => datetime_to_iso8601(Event#event.start_time),
duration => Event#event.duration,
location => Location,
tags => Event#event.tags,
capacity => Event#event.capacity,
rating_avg => Event#event.rating_avg,
rating_count => Event#event.rating_count,
status => Event#event.status
}.
format_calendars(Calendars) ->
lists:map(fun format_calendar/1, Calendars).
format_calendar(Calendar) ->
#{
id => Calendar#calendar.id,
owner_id => Calendar#calendar.owner_id,
title => Calendar#calendar.title,
description => Calendar#calendar.description,
type => Calendar#calendar.type,
tags => Calendar#calendar.tags,
rating_avg => Calendar#calendar.rating_avg,
rating_count => Calendar#calendar.rating_count,
status => Calendar#calendar.status
}.
datetime_to_iso8601({{Y, M, D}, {H, Min, S}}) ->
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
[Y, M, D, H, Min, S])).