feat: user API голосования за отзывы (like/dislike). Refs EventHub/EventHubBack#47
This commit is contained in:
@@ -111,8 +111,15 @@ update(Id, Updates) ->
|
||||
delete(Id) ->
|
||||
F = fun() ->
|
||||
case mnesia:read(review, Id) of
|
||||
[] -> {error, not_found};
|
||||
[Review] -> mnesia:delete_object(Review), {ok, deleted}
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[Review] ->
|
||||
Votes = mnesia:match_object(#review_vote{review_id = Id, _ = '_'}),
|
||||
lists:foreach(fun(#review_vote{id = VoteId}) ->
|
||||
mnesia:delete({review_vote, VoteId})
|
||||
end, Votes),
|
||||
mnesia:delete_object(Review),
|
||||
{ok, deleted}
|
||||
end
|
||||
end,
|
||||
case mnesia:transaction(F) of
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Хранение голосов пользователей за отзывы (like/dislike).
|
||||
%%% Счётчики на #review{} обновляются в той же транзакции.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(core_review_vote).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([upsert/3, delete/2, get_by_user_and_review/2, list_by_user/1,
|
||||
list_by_review/1, delete_by_review/1]).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Поставить или сменить голос. Идемпотентно при том же value.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec upsert(ReviewId :: binary(), UserId :: binary(), Value :: like | dislike) ->
|
||||
{ok, #review{}, like | dislike} | {error, term()}.
|
||||
upsert(ReviewId, UserId, Value) when Value =:= like; Value =:= dislike ->
|
||||
Now = calendar:universal_time(),
|
||||
F = fun() ->
|
||||
case mnesia:read(review, ReviewId) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[#review{status = Status}] when Status =/= visible ->
|
||||
{error, not_found};
|
||||
[#review{user_id = AuthorId}] when AuthorId =:= UserId ->
|
||||
{error, own_review};
|
||||
[Review] ->
|
||||
case find_vote(ReviewId, UserId) of
|
||||
[] ->
|
||||
Vote = #review_vote{
|
||||
id = infra_utils:generate_id(16),
|
||||
review_id = ReviewId,
|
||||
user_id = UserId,
|
||||
value = Value,
|
||||
created_at = Now,
|
||||
updated_at = Now
|
||||
},
|
||||
mnesia:write(Vote),
|
||||
Updated = bump(Review, Value, 1),
|
||||
mnesia:write(Updated),
|
||||
{ok, Updated, Value};
|
||||
[#review_vote{value = Value} = Vote] ->
|
||||
%% тот же голос — идемпотентно
|
||||
mnesia:write(Vote#review_vote{updated_at = Now}),
|
||||
{ok, Review, Value};
|
||||
[#review_vote{value = OldValue} = Vote] ->
|
||||
UpdatedVote = Vote#review_vote{value = Value, updated_at = Now},
|
||||
mnesia:write(UpdatedVote),
|
||||
Updated0 = bump(Review, OldValue, -1),
|
||||
Updated1 = bump(Updated0, Value, 1),
|
||||
mnesia:write(Updated1),
|
||||
{ok, Updated1, Value}
|
||||
end
|
||||
end
|
||||
end,
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, Result} -> Result;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Снять голос. Идемпотентно, если голоса нет.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec delete(ReviewId :: binary(), UserId :: binary()) ->
|
||||
{ok, #review{}, null} | {error, term()}.
|
||||
delete(ReviewId, UserId) ->
|
||||
F = fun() ->
|
||||
case mnesia:read(review, ReviewId) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[#review{status = Status}] when Status =/= visible ->
|
||||
{error, not_found};
|
||||
[#review{user_id = AuthorId}] when AuthorId =:= UserId ->
|
||||
{error, own_review};
|
||||
[Review] ->
|
||||
case find_vote(ReviewId, UserId) of
|
||||
[] ->
|
||||
{ok, Review, null};
|
||||
[#review_vote{value = Value, id = VoteId}] ->
|
||||
mnesia:delete({review_vote, VoteId}),
|
||||
Updated = bump(Review, Value, -1),
|
||||
mnesia:write(Updated),
|
||||
{ok, Updated, null}
|
||||
end
|
||||
end
|
||||
end,
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, Result} -> Result;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Голос пользователя за конкретный отзыв.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec get_by_user_and_review(UserId :: binary(), ReviewId :: binary()) ->
|
||||
{ok, #review_vote{}} | {error, not_found}.
|
||||
get_by_user_and_review(UserId, ReviewId) ->
|
||||
case mnesia:dirty_match_object(
|
||||
#review_vote{review_id = ReviewId, user_id = UserId, _ = '_'}) of
|
||||
[] -> {error, not_found};
|
||||
[Vote] -> {ok, Vote};
|
||||
[Vote | _] -> {ok, Vote}
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Все голоса пользователя.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec list_by_user(UserId :: binary()) -> [#review_vote{}].
|
||||
list_by_user(UserId) ->
|
||||
mnesia:dirty_match_object(#review_vote{user_id = UserId, _ = '_'}).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Все голоса за отзыв.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec list_by_review(ReviewId :: binary()) -> [#review_vote{}].
|
||||
list_by_review(ReviewId) ->
|
||||
mnesia:dirty_match_object(#review_vote{review_id = ReviewId, _ = '_'}).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Удалить все голоса отзыва (при физическом удалении review).
|
||||
%%% Вызывать внутри транзакции или отдельно.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec delete_by_review(ReviewId :: binary()) -> ok.
|
||||
delete_by_review(ReviewId) ->
|
||||
F = fun() ->
|
||||
lists:foreach(fun(#review_vote{id = Id}) ->
|
||||
mnesia:delete({review_vote, Id})
|
||||
end, list_by_review_tx(ReviewId)),
|
||||
ok
|
||||
end,
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, ok} -> ok;
|
||||
{aborted, Reason} -> error({delete_votes_failed, Reason})
|
||||
end.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Внутренние
|
||||
%%%===================================================================
|
||||
|
||||
find_vote(ReviewId, UserId) ->
|
||||
mnesia:match_object(#review_vote{review_id = ReviewId, user_id = UserId, _ = '_'}).
|
||||
|
||||
list_by_review_tx(ReviewId) ->
|
||||
mnesia:match_object(#review_vote{review_id = ReviewId, _ = '_'}).
|
||||
|
||||
-spec bump(#review{}, like | dislike, integer()) -> #review{}.
|
||||
bump(#review{likes = L} = R, like, Delta) ->
|
||||
R#review{likes = max(0, L + Delta), updated_at = calendar:universal_time()};
|
||||
bump(#review{dislikes = D} = R, dislike, Delta) ->
|
||||
R#review{dislikes = max(0, D + Delta), updated_at = calendar:universal_time()}.
|
||||
Reference in New Issue
Block a user