Избавление от undefined, всем необязательным полям присваиваются дефолтные значения #22
This commit is contained in:
+203
-61
@@ -1,52 +1,83 @@
|
||||
-module(core_user).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([create/2, get_by_id/1, get_by_email/1, update/2, update_status/3, delete/1, update_last_login/1]).
|
||||
-export([create/2, get_by_id/1, get_by_email/1, update/2, update_status/3,
|
||||
delete/1, update_last_login/1]).
|
||||
-export([email_exists/1]).
|
||||
-export([list_users/0]).
|
||||
-export([block/2, unblock/2]).
|
||||
-export([count_users/0, count_users_by_date/2, list_all/0]).
|
||||
-export([create_bot/2, delete_bot/1]).
|
||||
|
||||
%% Создание пользователя
|
||||
%% ─────────────────────────────────────────────────────────────────
|
||||
%% Значения по умолчанию для необязательных полей
|
||||
%% ─────────────────────────────────────────────────────────────────
|
||||
-define(DEFAULT_REASON, <<>>).
|
||||
-define(DEFAULT_AVATAR_URL, <<"default">>).
|
||||
-define(DEFAULT_TIMEZONE, <<>>).
|
||||
-define(DEFAULT_LANGUAGE, <<>>).
|
||||
-define(DEFAULT_SOCIAL_LINKS, []).
|
||||
-define(DEFAULT_PHONE, <<>>).
|
||||
-define(DEFAULT_PREFERENCES, #{}).
|
||||
-define(DEFAULT_LAST_LOGIN, {{1970,1,1},{0,0,0}}).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Создание обычного пользователя.
|
||||
%%% Если email уже существует, возвращает ошибку `{error, email_exists}`.
|
||||
%%% Все необязательные поля инициализируются значениями по умолчанию
|
||||
%%% (пустые строки, пустой список и т.д.), чтобы избежать `undefined` в базе.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec create(Email :: binary(), Password :: binary()) ->
|
||||
{ok, #user{}} | {error, email_exists | term()}.
|
||||
create(Email, Password) ->
|
||||
% Проверяем, существует ли email
|
||||
case email_exists(Email) of
|
||||
true ->
|
||||
{error, email_exists};
|
||||
false ->
|
||||
Id = infra_utils:generate_id(16),
|
||||
{ok, PasswordHash} = logic_auth:hash_password(Password),
|
||||
|
||||
Now = calendar:universal_time(),
|
||||
User = #user{
|
||||
id = Id,
|
||||
email = Email,
|
||||
id = Id,
|
||||
email = Email,
|
||||
password_hash = PasswordHash,
|
||||
role = user,
|
||||
status = active,
|
||||
created_at = calendar:universal_time(),
|
||||
updated_at = calendar:universal_time()
|
||||
role = user,
|
||||
status = active,
|
||||
reason = ?DEFAULT_REASON,
|
||||
nickname = extract_nickname(Email),
|
||||
avatar_url = ?DEFAULT_AVATAR_URL,
|
||||
timezone = ?DEFAULT_TIMEZONE,
|
||||
language = ?DEFAULT_LANGUAGE,
|
||||
social_links = ?DEFAULT_SOCIAL_LINKS,
|
||||
phone = ?DEFAULT_PHONE,
|
||||
preferences = ?DEFAULT_PREFERENCES,
|
||||
last_login = ?DEFAULT_LAST_LOGIN,
|
||||
created_at = Now,
|
||||
updated_at = Now
|
||||
},
|
||||
|
||||
F = fun() ->
|
||||
mnesia:write(User),
|
||||
{ok, User}
|
||||
end,
|
||||
|
||||
F = fun() -> mnesia:write(User), {ok, User} end,
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, Result} -> Result;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end
|
||||
end.
|
||||
|
||||
%% Получение пользователя по ID
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Получить пользователя по идентификатору.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec get_by_id(Id :: binary()) -> {ok, #user{}} | {error, not_found}.
|
||||
get_by_id(Id) ->
|
||||
case mnesia:dirty_read(user, Id) of
|
||||
[] -> {error, not_found};
|
||||
[User] -> {ok, User}
|
||||
end.
|
||||
|
||||
%% Получение пользователя по email
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Получить пользователя по email.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec get_by_email(Email :: binary()) -> {ok, #user{}} | {error, not_found}.
|
||||
get_by_email(Email) ->
|
||||
Match = #user{email = Email, _ = '_'},
|
||||
case mnesia:dirty_match_object(Match) of
|
||||
@@ -54,31 +85,46 @@ get_by_email(Email) ->
|
||||
[User] -> {ok, User}
|
||||
end.
|
||||
|
||||
%% Проверка существования email
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Проверить, существует ли email в базе.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec email_exists(Email :: binary()) -> boolean().
|
||||
email_exists(Email) ->
|
||||
case get_by_email(Email) of
|
||||
{ok, _} -> true;
|
||||
{error, _} -> false
|
||||
end.
|
||||
|
||||
%% Обновление пользователя
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Обновить поля пользователя.
|
||||
%%% `Updates` – список пар `{atom(), term()}`.
|
||||
%%% Поля, не указанные в обновлении, сохраняют прежние значения.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec update(Id :: binary(), Updates :: [{atom(), term()}]) ->
|
||||
{ok, #user{}} | {error, not_found | term()}.
|
||||
update(Id, Updates) ->
|
||||
F = fun() ->
|
||||
case mnesia:read(user, Id) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[] -> {error, not_found};
|
||||
[User] ->
|
||||
UpdatedUser = apply_updates(User, Updates),
|
||||
mnesia:write(UpdatedUser),
|
||||
{ok, UpdatedUser}
|
||||
end
|
||||
end,
|
||||
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, Result} -> Result;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Обновить время последнего входа пользователя.
|
||||
%%% Устанавливает `last_login` в текущее UTC-время.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec update_last_login(Id :: binary()) -> {ok, #user{}} | {error, not_found}.
|
||||
update_last_login(Id) ->
|
||||
case get_by_id(Id) of
|
||||
{ok, User} ->
|
||||
@@ -88,76 +134,140 @@ update_last_login(Id) ->
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Изменить статус и причину пользователя.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec update_status(Id :: binary(), Status :: atom(), Reason :: binary()) ->
|
||||
{ok, #user{}} | {error, not_found}.
|
||||
update_status(Id, Status, Reason) ->
|
||||
case get_by_id(Id) of
|
||||
{ok, User} ->
|
||||
Updated = User#user{status = Status, reason = Reason, updated_at = calendar:universal_time()},
|
||||
Updated = User#user{status = Status, reason = Reason,
|
||||
updated_at = calendar:universal_time()},
|
||||
mnesia:dirty_write(Updated),
|
||||
{ok, Updated};
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%% Удаление пользователя (soft delete)
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Мягкое удаление пользователя (установка статуса `deleted`).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec delete(Id :: binary()) -> {ok, #user{}} | {error, not_found | term()}.
|
||||
delete(Id) ->
|
||||
update(Id, [{status, deleted}]).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Получить список активных пользователей (без удалённых).
|
||||
%%% Результат – список карт с ключами `id`, `email`, `role`, `status`, `reason`,
|
||||
%%% `created_at`, `updated_at`.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec list_users() -> {ok, [map()]}.
|
||||
list_users() ->
|
||||
Users = mnesia:dirty_match_object(#user{_ = '_'}),
|
||||
ActiveUsers = [U || U <- Users, U#user.status =/= deleted],
|
||||
{ok, [user_to_map(U) || U <- ActiveUsers]}.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Преобразование записи пользователя в map.
|
||||
%%% Используется для API‑ответов.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec user_to_map(#user{} | map()) -> map().
|
||||
user_to_map(User) when is_map(User) ->
|
||||
#{
|
||||
id => maps:get(id, User),
|
||||
email => maps:get(email, User),
|
||||
role => maps:get(role, User, <<"user">>),
|
||||
status => maps:get(status, User, <<"active">>),
|
||||
reason => maps:get(reason, User, undefined),
|
||||
created_at => maps:get(created_at, User),
|
||||
updated_at => maps:get(updated_at, User)
|
||||
id => maps:get(id, User),
|
||||
email => maps:get(email, User),
|
||||
role => maps:get(role, User, <<"user">>),
|
||||
status => maps:get(status, User, <<"active">>),
|
||||
reason => maps:get(reason, User, ?DEFAULT_REASON),
|
||||
created_at => maps:get(created_at, User),
|
||||
updated_at => maps:get(updated_at, User)
|
||||
};
|
||||
|
||||
user_to_map(User) ->
|
||||
#{
|
||||
id => User#user.id,
|
||||
email => User#user.email,
|
||||
role => atom_to_binary(User#user.role, utf8),
|
||||
status => atom_to_binary(User#user.status, utf8),
|
||||
reason => User#user.reason,
|
||||
created_at => User#user.created_at,
|
||||
updated_at => User#user.updated_at
|
||||
id => User#user.id,
|
||||
email => User#user.email,
|
||||
role => atom_to_binary(User#user.role, utf8),
|
||||
status => atom_to_binary(User#user.status, utf8),
|
||||
reason => User#user.reason,
|
||||
created_at => User#user.created_at,
|
||||
updated_at => User#user.updated_at
|
||||
}.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Заблокировать пользователя с указанием причины.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec block(Id :: binary(), Reason :: binary()) ->
|
||||
{ok, #user{}} | {error, not_found}.
|
||||
block(Id, Reason) ->
|
||||
case get_by_id(Id) of
|
||||
{ok, User} ->
|
||||
Updated = User#user{status = blocked, reason = Reason, updated_at = calendar:universal_time()},
|
||||
Updated = User#user{status = blocked, reason = Reason,
|
||||
updated_at = calendar:universal_time()},
|
||||
mnesia:dirty_write(Updated),
|
||||
{ok, Updated};
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Разблокировать пользователя с указанием причины.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec unblock(Id :: binary(), Reason :: binary()) ->
|
||||
{ok, #user{}} | {error, not_found}.
|
||||
unblock(Id, Reason) ->
|
||||
case get_by_id(Id) of
|
||||
{ok, User} ->
|
||||
Updated = User#user{status = active, reason = Reason, updated_at = calendar:universal_time()},
|
||||
Updated = User#user{status = active, reason = Reason,
|
||||
updated_at = calendar:universal_time()},
|
||||
mnesia:dirty_write(Updated),
|
||||
{ok, Updated};
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Извлекает локальную часть email (до символа `@`).
|
||||
%%% Если `@` отсутствует, возвращает email целиком.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec extract_nickname(Email :: binary()) -> binary().
|
||||
extract_nickname(Email) ->
|
||||
case binary:split(Email, <<"@">>) of
|
||||
[LocalPart, _Domain] -> LocalPart;
|
||||
_ -> Email
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Количество всех записей в таблице `user`.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec count_users() -> non_neg_integer().
|
||||
count_users() ->
|
||||
mnesia:table_info(user, size).
|
||||
|
||||
%% Административный список (все пользователи, без фильтрации)
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Получить всех пользователей (включая удалённых).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec list_all() -> [#user{}].
|
||||
list_all() ->
|
||||
mnesia:dirty_match_object(#user{_ = '_'}).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Подсчёт пользователей, созданных в заданном временном диапазоне.
|
||||
%%% Возвращает список кортежей `{{Year,Month,Day}, Count}`, отсортированный по дате.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec count_users_by_date(From :: calendar:datetime(), To :: calendar:datetime()) ->
|
||||
[{{pos_integer(), pos_integer(), pos_integer()}, non_neg_integer()}].
|
||||
count_users_by_date(From, To) ->
|
||||
All = mnesia:dirty_match_object(#user{_ = '_'}),
|
||||
Filtered = lists:filter(fun(U) ->
|
||||
U#user.created_at >= From andalso U#user.created_at =< To
|
||||
end, All),
|
||||
Filtered = lists:filter(fun(U) -> U#user.created_at >= From andalso
|
||||
U#user.created_at =< To end, All),
|
||||
Counts = lists:foldl(fun(U, Acc) ->
|
||||
Day = date_part(U#user.created_at),
|
||||
case lists:keyfind(Day, 1, Acc) of
|
||||
@@ -167,18 +277,35 @@ count_users_by_date(From, To) ->
|
||||
end, [], Filtered),
|
||||
lists:sort(Counts).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Выделить из datetime только дату `{Y,M,D}`.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec date_part(calendar:datetime()) -> {pos_integer(), pos_integer(), pos_integer()}.
|
||||
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Применить список обновлений к записи пользователя.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec apply_updates(#user{}, [{atom(), term()}]) -> #user{}.
|
||||
apply_updates(User, Updates) ->
|
||||
Updated = lists:foldl(fun({Field, Value}, U) ->
|
||||
set_field(Field, Value, U)
|
||||
end, User, Updates),
|
||||
Updated#user{updated_at = calendar:universal_time()}.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Установить одно поле записи. Неизвестные поля игнорируются.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec set_field(atom(), term(), #user{}) -> #user{}.
|
||||
set_field(email, Value, U) -> U#user{email = Value};
|
||||
set_field(password_hash, Value, U) -> U#user{password_hash = Value};
|
||||
set_field(role, Value, U) when Value =:= user; Value =:= admin; Value =:= bot -> U#user{role = Value};
|
||||
set_field(status, Value, U) when Value =:= active; Value =:= frozen; Value =:= deleted -> U#user{status = Value};
|
||||
set_field(role, Value, U) when Value =:= user; Value =:= admin; Value =:= bot ->
|
||||
U#user{role = Value};
|
||||
set_field(status, Value, U) when Value =:= active; Value =:= frozen; Value =:= deleted ->
|
||||
U#user{status = Value};
|
||||
set_field(reason, Value, U) -> U#user{reason = Value};
|
||||
set_field(nickname, Value, U) -> U#user{nickname = Value};
|
||||
set_field(avatar_url, Value, U) -> U#user{avatar_url = Value};
|
||||
@@ -189,12 +316,12 @@ set_field(phone, Value, U) -> U#user{phone = Value};
|
||||
set_field(preferences, Value, U) -> U#user{preferences = Value};
|
||||
set_field(_, _, U) -> U.
|
||||
|
||||
%% ------------------------------------------------------------------
|
||||
%% API для ботов
|
||||
%% ------------------------------------------------------------------
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Создание бота (аналогично пользователю, но с ролью `bot`).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec create_bot(Email :: binary(), Password :: binary()) ->
|
||||
{ok, User :: #user{}} | {error, duplicate_email | invalid_email}.
|
||||
{ok, #user{}} | {error, email_exists | invalid_email}.
|
||||
create_bot(Email, Password) ->
|
||||
case email_exists(Email) of
|
||||
true ->
|
||||
@@ -204,14 +331,24 @@ create_bot(Email, Password) ->
|
||||
[] ->
|
||||
{ok, PasswordHash} = logic_auth:hash_password(Password),
|
||||
Id = infra_utils:generate_id(16),
|
||||
Now = calendar:universal_time(),
|
||||
User = #user{
|
||||
id = Id,
|
||||
email = Email,
|
||||
id = Id,
|
||||
email = Email,
|
||||
password_hash = PasswordHash,
|
||||
role = bot,
|
||||
status = active,
|
||||
created_at = calendar:universal_time(),
|
||||
updated_at = calendar:universal_time()
|
||||
role = bot,
|
||||
status = active,
|
||||
reason = ?DEFAULT_REASON,
|
||||
nickname = extract_nickname(Email),
|
||||
avatar_url = ?DEFAULT_AVATAR_URL,
|
||||
timezone = ?DEFAULT_TIMEZONE,
|
||||
language = ?DEFAULT_LANGUAGE,
|
||||
social_links = ?DEFAULT_SOCIAL_LINKS,
|
||||
phone = ?DEFAULT_PHONE,
|
||||
preferences = ?DEFAULT_PREFERENCES,
|
||||
last_login = ?DEFAULT_LAST_LOGIN,
|
||||
created_at = Now,
|
||||
updated_at = Now
|
||||
},
|
||||
ok = mnesia:dirty_write(User),
|
||||
{ok, User};
|
||||
@@ -220,11 +357,16 @@ create_bot(Email, Password) ->
|
||||
end
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Удаление бота (физическое удаление записи, не мягкое).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec delete_bot(Id :: binary()) -> ok | {error, not_found}.
|
||||
delete_bot(Id) ->
|
||||
case mnesia:dirty_read({user, Id}) of
|
||||
[#user{role = bot}] ->
|
||||
mnesia:dirty_delete({user, Id}),
|
||||
ok;
|
||||
[] -> {error, not_found}
|
||||
[] ->
|
||||
{error, not_found}
|
||||
end.
|
||||
Reference in New Issue
Block a user