feat(geo): Photon proxy, address-only location, nearby search. Refs EventHub/EventHubBack#77
CI / test (push) Successful in 7m38s
CI / deploy-ift (push) Successful in 3m27s
CI / e2e-ift (push) Successful in 1m16s
CI / deploy-stage (push) Successful in 2m18s
CI / e2e-stage (push) Successful in 4m11s

This commit is contained in:
2026-08-17 23:23:10 +03:00
parent 501334e241
commit 6b70e1336d
14 changed files with 919 additions and 90 deletions
+59 -21
View File
@@ -32,7 +32,7 @@ trails() ->
#{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Maximum results per page">>},
#{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset for pagination">>},
#{name => <<"tags">>, in => <<"query">>, schema => #{type => string}, description => <<"Comma-separated tags">>},
#{name => <<"sort">>, in => <<"query">>, schema => #{type => string, enum => [<<"start_time">>, <<"created_at">>, <<"title">>]}, description => <<"Field to sort by">>},
#{name => <<"sort">>, in => <<"query">>, schema => #{type => string, enum => [<<"start_time">>, <<"created_at">>, <<"title">>, <<"distance">>]}, description => <<"Field to sort by">>},
#{name => <<"order">>, in => <<"query">>, schema => #{type => string, enum => [<<"asc">>, <<"desc">>]}, description => <<"Sort order">>},
#{name => <<"lat">>, in => <<"query">>, schema => #{type => number, format => float}, description => <<"Latitude for geo search">>},
#{name => <<"lon">>, in => <<"query">>, schema => #{type => number, format => float}, description => <<"Longitude for geo search">>},
@@ -79,18 +79,22 @@ search(Req) ->
Qs = cowboy_req:parse_qs(Req1),
Type = proplists:get_value(<<"type">>, Qs, undefined),
Query = proplists:get_value(<<"q">>, Qs, undefined),
Params = parse_params(Qs),
case logic_search:search(Type, Query, UserId, Params) of
{ok, Total, Results} ->
Response = #{
total => Total,
limit => maps:get(limit, Params, 20),
offset => maps:get(offset, Params, 0),
results => Results
},
handler_utils:send_json(Req1, 200, Response);
{error, _} ->
handler_utils:send_error(Req1, 500, <<"Search failed">>)
case parse_params(Qs) of
{error, invalid_radius} ->
handler_utils:send_error(Req1, 400, <<"invalid_radius">>);
Params ->
case logic_search:search(Type, Query, UserId, Params) of
{ok, Total, Results} ->
Response = #{
total => Total,
limit => maps:get(limit, Params, 20),
offset => maps:get(offset, Params, 0),
results => Results
},
handler_utils:send_json(Req1, 200, Response);
{error, _} ->
handler_utils:send_error(Req1, 500, <<"Search failed">>)
end
end;
{error, Code, Message, Req1} ->
handler_utils:send_error(Req1, Code, Message)
@@ -103,7 +107,8 @@ search(Req) ->
%% @private Собирает карту параметров для поискового движка.
%% Не кладёт sort/tags/geo/даты, если их нет в QS — иначе
%% logic_search:is_discovery_request/2 никогда не сработает (Back#50).
-spec parse_params(cowboy_req:qs()) -> map().
%% radius без ключа при lat/lon — без отсечения (GEO.md).
-spec parse_params(cowboy_req:qs()) -> map() | {error, invalid_radius}.
parse_params(Qs) ->
Params0 = #{
limit => parse_int_param(Qs, <<"limit">>, 20),
@@ -125,12 +130,22 @@ parse_params(Qs) ->
end,
Params1#{sort => Sort, order => Order}
end,
Params3 = case {parse_float_param(Qs, <<"lat">>), parse_float_param(Qs, <<"lon">>)} of
{{ok, Lat}, {ok, Lon}} ->
Radius = parse_int_param(Qs, <<"radius">>, 10),
Params2#{lat => Lat, lon => Lon, radius => Radius};
_ -> Params2
end,
case {parse_float_param(Qs, <<"lat">>), parse_float_param(Qs, <<"lon">>)} of
{{ok, Lat}, {ok, Lon}} ->
case parse_optional_radius(Qs) of
{error, invalid_radius} = E -> E;
Radius ->
Params3 = case Radius of
undefined -> Params2#{lat => Lat, lon => Lon};
R -> Params2#{lat => Lat, lon => Lon, radius => R}
end,
with_dates(Qs, Params3)
end;
_ ->
with_dates(Qs, Params2)
end.
with_dates(Qs, Params3) ->
case {parse_datetime_param(Qs, <<"from">>), parse_datetime_param(Qs, <<"to">>)} of
{{ok, From}, {ok, To}} -> Params3#{from => From, to => To};
{{ok, From}, error} -> Params3#{from => From};
@@ -138,6 +153,18 @@ parse_params(Qs) ->
_ -> Params3
end.
parse_optional_radius(Qs) ->
case proplists:get_value(<<"radius">>, Qs) of
undefined -> undefined;
<<>> -> undefined;
Val ->
N = handler_utils:parse_int_qs(Val, 0),
case N >= 1 andalso N =< 100 of
true -> N;
false -> {error, invalid_radius}
end
end.
-spec parse_int_param(cowboy_req:qs(), binary(), integer()) -> integer().
parse_int_param(Qs, Key, Default) ->
handler_utils:parse_int_qs(proplists:get_value(Key, Qs), Default).
@@ -146,7 +173,18 @@ parse_int_param(Qs, Key, Default) ->
parse_float_param(Qs, Key) ->
case proplists:get_value(Key, Qs) of
undefined -> error;
Val -> {ok, binary_to_float(Val)}
<<>> -> error;
Val ->
try binary_to_float(Val) of
F -> {ok, F}
catch
_:_ ->
try binary_to_integer(Val) of
I -> {ok, float(I)}
catch
_:_ -> error
end
end
end.
-spec parse_datetime_param(cowboy_req:qs(), binary()) -> {ok, calendar:datetime()} | error.