147 lines
6.3 KiB
Erlang
147 lines
6.3 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% @doc Административный обработчик отзывов.
|
|
%%% GET – список отзывов с пагинацией, фильтрацией и сортировкой.
|
|
%%% PATCH – массовое обновление статусов отзывов с единой причиной.
|
|
%%% @end
|
|
%%%-------------------------------------------------------------------
|
|
-module(admin_handler_reviews).
|
|
-behaviour(cowboy_handler).
|
|
-export([init/2]).
|
|
-export([trails/0]).
|
|
|
|
-include("records.hrl").
|
|
|
|
%%% cowboy_handler callback
|
|
init(Req, _Opts) ->
|
|
case cowboy_req:method(Req) of
|
|
<<"GET">> -> list_reviews(Req);
|
|
<<"PATCH">> -> bulk_update_reviews(Req);
|
|
_ -> 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 => <<"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">>},
|
|
#{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">>,
|
|
description => <<"Bulk update review statuses with a single reason">>,
|
|
tags => [<<"Reviews">>],
|
|
requestBody => #{
|
|
required => true,
|
|
content => #{<<"application/json">> => #{schema => #{
|
|
type => object,
|
|
required => [<<"operations">>, <<"reason">>],
|
|
properties => #{
|
|
<<"operations">> => #{
|
|
type => array,
|
|
items => #{
|
|
type => object,
|
|
properties => #{
|
|
<<"id">> => #{type => string},
|
|
<<"status">> => #{type => string, enum => [<<"visible">>, <<"hidden">>, <<"deleted">>]}
|
|
}
|
|
}
|
|
},
|
|
<<"reason">> => #{type => string, description => <<"Reason applied to all updates">>}
|
|
}
|
|
}}}
|
|
},
|
|
responses => #{
|
|
200 => #{description => <<"Number of updated reviews">>},
|
|
400 => #{description => <<"Invalid request body or missing reason">>}
|
|
}
|
|
}
|
|
].
|
|
|
|
review_schema() ->
|
|
#{ type => object,
|
|
properties => #{
|
|
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">>}
|
|
}
|
|
}.
|
|
|
|
%%% Internal functions
|
|
|
|
list_reviews(Req) ->
|
|
case handler_utils:auth_admin(Req) of
|
|
{ok, _AdminId, Req1} ->
|
|
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 => normalize_status(proplists:get_value(<<"status">>, Qs))
|
|
},
|
|
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),
|
|
Json = [handler_utils:review_to_json(R) || R <- Reviews],
|
|
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(<<"calendar">>) -> calendar;
|
|
normalize_target_type(Other) -> Other.
|
|
|
|
%% @private Преобразует бинарный status в атом.
|
|
normalize_status(<<"visible">>) -> visible;
|
|
normalize_status(<<"hidden">>) -> hidden;
|
|
normalize_status(<<"deleted">>) -> deleted;
|
|
normalize_status(Other) -> Other.
|
|
|
|
bulk_update_reviews(Req) ->
|
|
case handler_utils:auth_admin(Req) of
|
|
{ok, AdminId, Req1} ->
|
|
try
|
|
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
|
#{<<"operations">> := Operations, <<"reason">> := Reason} = jsx:decode(Body, [return_maps]),
|
|
true = is_list(Operations),
|
|
case logic_review:bulk_update_status(Operations, Reason) 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, ReasonErr} ->
|
|
handler_utils:send_error(Req2, 400, ReasonErr)
|
|
end
|
|
catch
|
|
error:{badmatch, _} ->
|
|
handler_utils:send_error(Req1, 400, <<"Missing 'reason' field">>);
|
|
_:_ -> handler_utils:send_error(Req1, 400, <<"Invalid JSON body">>)
|
|
end;
|
|
{error, Code, Msg, Req1} ->
|
|
handler_utils:send_error(Req1, Code, Msg)
|
|
end. |