Stage 4
This commit is contained in:
@@ -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])).
|
||||
Reference in New Issue
Block a user