Рефакторинг админских обработчиков - пагинация, сортировка, фильтрация. Часть 1

This commit is contained in:
2026-05-26 13:04:31 +03:00
parent 01dbc6d6cb
commit ed5b275efb
24 changed files with 1009 additions and 1126 deletions
+59 -72
View File
@@ -1,6 +1,11 @@
%%%-------------------------------------------------------------------
%%% @doc Административный обработчик отзывов.
%%% GET – список отзывов с пагинацией, фильтрацией и сортировкой.
%%% PATCH – массовое обновление статусов отзывов.
%%% @end
%%%-------------------------------------------------------------------
-module(admin_handler_reviews).
-behaviour(cowboy_handler).
-export([init/2]).
-export([trails/0]).
@@ -9,54 +14,40 @@
%%% cowboy_handler callback
init(Req, _Opts) ->
case cowboy_req:method(Req) of
<<"GET">> -> list_reviews(Req);
<<"GET">> -> list_reviews(Req);
<<"PATCH">> -> bulk_update_reviews(Req);
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
end.
%%% Swagger metadata
trails() ->
[
#{ % GET list
path => <<"/v1/admin/reviews">>,
method => <<"GET">>,
description => <<"List all reviews (admin)">>,
tags => [<<"Reviews">>],
parameters => [
#{name => <<"target_type">>, in => <<"query">>, schema => #{type => string}, description => <<"calendar or event">>},
#{name => <<"target_id">>, in => <<"query">>, schema => #{type => string}, description => <<"ID of target">>},
#{name => <<"user_id">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by user">>},
#{name => <<"status">>, in => <<"query">>, schema => #{type => string}, description => <<"visible, hidden, deleted, or all">>},
#{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>},
#{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>}
],
responses => #{
200 => #{
description => <<"Array of reviews">>,
content => #{<<"application/json">> => #{schema => #{
type => array,
items => review_schema()
}}}
}
}
},
#{ % PATCH bulk update
path => <<"/v1/admin/reviews">>,
method => <<"PATCH">>,
[ #{ % GET list
path => <<"/v1/admin/reviews">>,
method => <<"GET">>,
description => <<"List all reviews (admin)">>,
tags => [<<"Reviews">>],
parameters => [
#{name => <<"target_type">>, in => <<"query">>, schema => #{type => string}, description => <<"calendar or event">>},
#{name => <<"target_id">>, in => <<"query">>, schema => #{type => string}, description => <<"ID of target">>},
#{name => <<"user_id">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by user">>},
#{name => <<"status">>, in => <<"query">>, schema => #{type => string}, description => <<"visible, hidden, deleted, or all">>},
#{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>},
#{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>},
#{name => <<"sort">>, in => <<"query">>, schema => #{type => string, enum => [<<"created_at">>, <<"updated_at">>, <<"rating">>]}, description => <<"Sort field">>},
#{name => <<"order">>, in => <<"query">>, schema => #{type => string, enum => [<<"asc">>, <<"desc">>]}, description => <<"Sort direction">>}
],
responses => #{
200 => #{ description => <<"Array of reviews">>, content => #{<<"application/json">> => #{schema => #{ type => array, items => review_schema() }}} }
}
},
#{ % PATCH bulk update
path => <<"/v1/admin/reviews">>,
method => <<"PATCH">>,
description => <<"Bulk update review statuses">>,
tags => [<<"Reviews">>],
tags => [<<"Reviews">>],
requestBody => #{
required => true,
content => #{<<"application/json">> => #{schema => #{
type => array,
items => #{
type => object,
properties => #{
id => #{type => string},
status => #{type => string, enum => [<<"visible">>, <<"hidden">>, <<"deleted">>]}
}
}
}}}
content => #{<<"application/json">> => #{schema => #{ type => array, items => #{ type => object, properties => #{ id => #{type => string}, status => #{type => string, enum => [<<"visible">>, <<"hidden">>, <<"deleted">>]} } }}} }
},
responses => #{
200 => #{description => <<"Number of updated reviews">>}
@@ -65,21 +56,20 @@ trails() ->
].
review_schema() ->
#{
type => object,
#{ type => object,
properties => #{
id => #{type => string},
user_id => #{type => string},
id => #{type => string},
user_id => #{type => string},
target_type => #{type => string, enum => [<<"calendar">>, <<"event">>]},
target_id => #{type => string},
rating => #{type => integer, minimum => 1, maximum => 5},
comment => #{type => string},
status => #{type => string, enum => [<<"visible">>, <<"hidden">>, <<"deleted">>]},
reason => #{type => string, nullable => true},
likes => #{type => integer},
dislikes => #{type => integer},
created_at => #{type => string, format => <<"date-time">>},
updated_at => #{type => string, format => <<"date-time">>}
target_id => #{type => string},
rating => #{type => integer, minimum => 1, maximum => 5},
comment => #{type => string},
status => #{type => string, enum => [<<"visible">>, <<"hidden">>, <<"deleted">>]},
reason => #{type => string, nullable => true},
likes => #{type => integer},
dislikes => #{type => integer},
created_at => #{type => string, format => <<"date-time">>},
updated_at => #{type => string, format => <<"date-time">>}
}
}.
@@ -91,35 +81,40 @@ list_reviews(Req) ->
Qs = cowboy_req:parse_qs(Req1),
Filters0 = #{
target_type => normalize_target_type(proplists:get_value(<<"target_type">>, Qs)),
target_id => proplists:get_value(<<"target_id">>, Qs),
user_id => proplists:get_value(<<"user_id">>, Qs),
status => proplists:get_value(<<"status">>, Qs)
target_id => proplists:get_value(<<"target_id">>, Qs),
user_id => proplists:get_value(<<"user_id">>, Qs),
status => proplists:get_value(<<"status">>, Qs)
},
% Убираем ключи со значением undefined
Filters = maps:filter(fun(_, V) -> V =/= undefined end, Filters0),
Pagination = handler_utils:parse_pagination_params(Req1),
{ok, Total, Reviews} = logic_review:list_admin_reviews(Filters, Pagination),
% Убедимся, что в Pagination есть ключи sort и order со значениями по умолчанию
PaginationWithSort = Pagination#{
sort => maps:get(sort, Pagination, <<"created_at">>),
order => maps:get(order, Pagination, <<"desc">>)
},
{ok, Total, Reviews} = logic_review:list_admin_reviews(Filters, PaginationWithSort),
Json = [handler_utils:review_to_json(R) || R <- Reviews],
ExtraHeaders = pagination_headers(Pagination, Total),
ExtraHeaders = handler_utils:pagination_headers(Pagination, Total),
handler_utils:send_json(Req1, 200, Json, ExtraHeaders);
{error, Code, Msg, Req1} ->
handler_utils:send_error(Req1, Code, Msg)
end.
%% @private Преобразует бинарный target_type в атом.
normalize_target_type(<<"event">>) -> event;
normalize_target_type(<<"event">>) -> event;
normalize_target_type(<<"calendar">>) -> calendar;
normalize_target_type(Other) -> Other.
normalize_target_type(Other) -> Other.
bulk_update_reviews(Req) ->
case handler_utils:auth_admin(Req) of
{ok, _AdminId, Req1} ->
{ok, AdminId, Req1} ->
try
{ok, Body, Req2} = cowboy_req:read_body(Req1),
Operations = jsx:decode(Body, [return_maps]),
true = is_list(Operations),
case logic_review:bulk_update_status(Operations) of
{ok, UpdatedCount} ->
admin_utils:log_admin_action(AdminId, <<"bulk_update_reviews">>, <<"review">>, UpdatedCount, Req2),
handler_utils:send_json(Req2, 200, #{updated_count => UpdatedCount});
{error, Reason} ->
handler_utils:send_error(Req2, 400, Reason)
@@ -129,12 +124,4 @@ bulk_update_reviews(Req) ->
end;
{error, Code, Msg, Req1} ->
handler_utils:send_error(Req1, Code, Msg)
end.
pagination_headers(#{limit := Limit, offset := Offset}, Total) ->
RangeEnd = min(Offset + Limit - 1, Total - 1),
#{
<<"content-range">> => iolist_to_binary(io_lib:format("items ~B-~B/~B", [Offset, RangeEnd, Total])),
<<"x-total-count">> => integer_to_binary(Total),
<<"access-control-expose-headers">> => <<"Content-Range, X-Total-Count">>
}.
end.