Избавление от undefined, всем необязательным полям присваиваются дефолтные значения #22
This commit is contained in:
+105
-6
@@ -1,9 +1,28 @@
|
|||||||
-module(core_admin).
|
-module(core_admin).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
-export([create/3, get_by_email/1, get_by_id/1, list_all/0,
|
-export([create/3, get_by_email/1, get_by_id/1, list_all/0, update_role/2, block/1, unblock/1,
|
||||||
update_role/2, block/1, unblock/1, update_last_login/1]).
|
update_last_login/1]).
|
||||||
-export([update/2, delete/1]).
|
-export([update/2, delete/1]).
|
||||||
|
|
||||||
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
|
%% Значения по умолчанию для необязательных полей
|
||||||
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
|
-define(DEFAULT_AVATAR_URL, <<"default">>).
|
||||||
|
-define(DEFAULT_TIMEZONE, <<>>).
|
||||||
|
-define(DEFAULT_LANGUAGE, <<>>).
|
||||||
|
-define(DEFAULT_PHONE, <<>>).
|
||||||
|
-define(DEFAULT_PREFERENCES, #{}).
|
||||||
|
-define(DEFAULT_LAST_LOGIN, {{1970,1,1},{0,0,0}}).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Создание администратора.
|
||||||
|
%%% Если email уже существует, возвращает ошибку `{error, email_exists}`.
|
||||||
|
%%% Все необязательные поля инициализируются значениями по умолчанию,
|
||||||
|
%%% nickname устанавливается в локальную часть email (до @).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec create(Email :: binary(), Password :: binary(), Role :: atom()) ->
|
||||||
|
{ok, #admin{}} | {error, email_exists | term()}.
|
||||||
create(Email, Password, Role) ->
|
create(Email, Password, Role) ->
|
||||||
case get_by_email(Email) of
|
case get_by_email(Email) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
@@ -18,6 +37,13 @@ create(Email, Password, Role) ->
|
|||||||
password_hash = Hash,
|
password_hash = Hash,
|
||||||
role = Role,
|
role = Role,
|
||||||
status = active,
|
status = active,
|
||||||
|
nickname = extract_nickname(Email),
|
||||||
|
avatar_url = ?DEFAULT_AVATAR_URL,
|
||||||
|
timezone = ?DEFAULT_TIMEZONE,
|
||||||
|
language = ?DEFAULT_LANGUAGE,
|
||||||
|
phone = ?DEFAULT_PHONE,
|
||||||
|
preferences = ?DEFAULT_PREFERENCES,
|
||||||
|
last_login = ?DEFAULT_LAST_LOGIN,
|
||||||
created_at = Now,
|
created_at = Now,
|
||||||
updated_at = Now
|
updated_at = Now
|
||||||
},
|
},
|
||||||
@@ -25,11 +51,19 @@ create(Email, Password, Role) ->
|
|||||||
{ok, Admin}
|
{ok, Admin}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Обновление администратора (любые поля)
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Обновление администратора (любые поля).
|
||||||
|
%%% `Updates` – список пар `[{atom(), term()}]`.
|
||||||
|
%%% Поля, не указанные в обновлении, сохраняют прежние значения.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update(AdminId :: binary(), Updates :: [{atom(), term()}]) ->
|
||||||
|
{ok, #admin{}} | {error, not_found | term()}.
|
||||||
update(AdminId, Updates) ->
|
update(AdminId, Updates) ->
|
||||||
F = fun() ->
|
F = fun() ->
|
||||||
case mnesia:read(admin, AdminId) of
|
case mnesia:read(admin, AdminId) of
|
||||||
[] -> {error, not_found};
|
[] ->
|
||||||
|
{error, not_found};
|
||||||
[Admin] ->
|
[Admin] ->
|
||||||
UpdatedAdmin = apply_updates(Admin, Updates),
|
UpdatedAdmin = apply_updates(Admin, Updates),
|
||||||
mnesia:write(UpdatedAdmin),
|
mnesia:write(UpdatedAdmin),
|
||||||
@@ -41,6 +75,11 @@ update(AdminId, Updates) ->
|
|||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить администратора по email.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_by_email(Email :: binary()) -> {ok, #admin{}} | {error, not_found}.
|
||||||
get_by_email(Email) ->
|
get_by_email(Email) ->
|
||||||
Match = #admin{email = Email, _ = '_'},
|
Match = #admin{email = Email, _ = '_'},
|
||||||
case mnesia:dirty_match_object(Match) of
|
case mnesia:dirty_match_object(Match) of
|
||||||
@@ -48,15 +87,31 @@ get_by_email(Email) ->
|
|||||||
[] -> {error, not_found}
|
[] -> {error, not_found}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить администратора по идентификатору.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_by_id(Id :: binary()) -> {ok, #admin{}} | {error, not_found}.
|
||||||
get_by_id(Id) ->
|
get_by_id(Id) ->
|
||||||
case mnesia:dirty_read({admin, Id}) of
|
case mnesia:dirty_read({admin, Id}) of
|
||||||
[Admin] -> {ok, Admin};
|
[Admin] -> {ok, Admin};
|
||||||
[] -> {error, not_found}
|
[] -> {error, not_found}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить список всех администраторов.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_all() -> [#admin{}].
|
||||||
list_all() ->
|
list_all() ->
|
||||||
mnesia:dirty_match_object(#admin{_ = '_'}).
|
mnesia:dirty_match_object(#admin{_ = '_'}).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Изменить роль администратора.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update_role(Id :: binary(), NewRole :: atom()) ->
|
||||||
|
{ok, #admin{}} | {error, not_found}.
|
||||||
update_role(Id, NewRole) when is_atom(NewRole) ->
|
update_role(Id, NewRole) when is_atom(NewRole) ->
|
||||||
case get_by_id(Id) of
|
case get_by_id(Id) of
|
||||||
{ok, Admin} ->
|
{ok, Admin} ->
|
||||||
@@ -66,6 +121,12 @@ update_role(Id, NewRole) when is_atom(NewRole) ->
|
|||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Обновить время последнего входа администратора.
|
||||||
|
%%% Устанавливает `last_login` в текущее UTC-время.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update_last_login(Id :: binary()) -> {ok, #admin{}} | {error, not_found}.
|
||||||
update_last_login(Id) ->
|
update_last_login(Id) ->
|
||||||
case get_by_id(Id) of
|
case get_by_id(Id) of
|
||||||
{ok, Admin} ->
|
{ok, Admin} ->
|
||||||
@@ -75,12 +136,28 @@ update_last_login(Id) ->
|
|||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Заблокировать администратора.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec block(Id :: binary()) -> {ok, #admin{}} | {error, not_found}.
|
||||||
block(Id) ->
|
block(Id) ->
|
||||||
update_status(Id, blocked).
|
update_status(Id, blocked).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Разблокировать администратора.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec unblock(Id :: binary()) -> {ok, #admin{}} | {error, not_found}.
|
||||||
unblock(Id) ->
|
unblock(Id) ->
|
||||||
update_status(Id, active).
|
update_status(Id, active).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Изменить статус администратора (внутренняя функция).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update_status(Id :: binary(), Status :: atom()) ->
|
||||||
|
{ok, #admin{}} | {error, not_found}.
|
||||||
update_status(Id, Status) ->
|
update_status(Id, Status) ->
|
||||||
case get_by_id(Id) of
|
case get_by_id(Id) of
|
||||||
{ok, Admin} ->
|
{ok, Admin} ->
|
||||||
@@ -90,7 +167,10 @@ update_status(Id, Status) ->
|
|||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Физическое удаление администратора из базы
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Физическое удаление администратора из базы.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
-spec delete(binary()) -> {ok, deleted} | {error, not_found}.
|
-spec delete(binary()) -> {ok, deleted} | {error, not_found}.
|
||||||
delete(AdminId) ->
|
delete(AdminId) ->
|
||||||
case get_by_id(AdminId) of
|
case get_by_id(AdminId) of
|
||||||
@@ -104,7 +184,13 @@ delete(AdminId) ->
|
|||||||
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|
||||||
apply_updates(Admin, []) -> Admin;
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Применить список обновлений к записи администратора.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec apply_updates(#admin{}, [{atom(), term()}]) -> #admin{}.
|
||||||
|
apply_updates(Admin, []) ->
|
||||||
|
Admin;
|
||||||
apply_updates(Admin, [{Field, Value} | Rest]) ->
|
apply_updates(Admin, [{Field, Value} | Rest]) ->
|
||||||
NewAdmin = case Field of
|
NewAdmin = case Field of
|
||||||
email -> Admin#admin{email = Value};
|
email -> Admin#admin{email = Value};
|
||||||
@@ -117,6 +203,19 @@ apply_updates(Admin, [{Field, Value} | Rest]) ->
|
|||||||
language -> Admin#admin{language = Value};
|
language -> Admin#admin{language = Value};
|
||||||
phone -> Admin#admin{phone = Value};
|
phone -> Admin#admin{phone = Value};
|
||||||
preferences -> Admin#admin{preferences = Value};
|
preferences -> Admin#admin{preferences = Value};
|
||||||
|
last_login -> Admin#admin{last_login = Value};
|
||||||
_ -> Admin
|
_ -> Admin
|
||||||
end,
|
end,
|
||||||
apply_updates(NewAdmin#admin{updated_at = calendar:universal_time()}, Rest).
|
apply_updates(NewAdmin#admin{updated_at = calendar:universal_time()}, Rest).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Извлекает локальную часть email (до символа `@`).
|
||||||
|
%%% Если `@` отсутствует, возвращает email целиком.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec extract_nickname(Email :: binary()) -> binary().
|
||||||
|
extract_nickname(Email) ->
|
||||||
|
case binary:split(Email, <<"@">>) of
|
||||||
|
[LocalPart, _Domain] -> LocalPart;
|
||||||
|
_ -> Email
|
||||||
|
end.
|
||||||
@@ -1,10 +1,37 @@
|
|||||||
-module(core_admin_audit).
|
-module(core_admin_audit).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
-export([log/7, log/8, list/0, list/1]). % ← добавили log/8
|
-export([log/7, log/8, list/0, list/1]).
|
||||||
-export([count_actions_by_admin/2]).
|
-export([count_actions_by_admin/2]).
|
||||||
|
|
||||||
|
-define(DEFAULT_REASON, <<>>). % причина по умолчанию, если не указана
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Записать событие аудита без указания причины.
|
||||||
|
%%% В поле `reason` будет записана пустая строка.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec log(AdminId :: binary(),
|
||||||
|
Email :: binary(),
|
||||||
|
Role :: atom(),
|
||||||
|
Action :: binary(),
|
||||||
|
EntityType :: binary(),
|
||||||
|
EntityId :: term(),
|
||||||
|
Ip :: binary()) -> {ok, #admin_audit{}}.
|
||||||
log(AdminId, Email, Role, Action, EntityType, EntityId, Ip) ->
|
log(AdminId, Email, Role, Action, EntityType, EntityId, Ip) ->
|
||||||
log(AdminId, Email, Role, Action, EntityType, EntityId, Ip, undefined).
|
log(AdminId, Email, Role, Action, EntityType, EntityId, Ip, ?DEFAULT_REASON).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Записать событие аудита с указанием причины.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec log(AdminId :: binary(),
|
||||||
|
Email :: binary(),
|
||||||
|
Role :: atom(),
|
||||||
|
Action :: binary(),
|
||||||
|
EntityType :: binary(),
|
||||||
|
EntityId :: term(),
|
||||||
|
Ip :: binary(),
|
||||||
|
Reason :: binary()) -> {ok, #admin_audit{}}.
|
||||||
log(AdminId, Email, Role, Action, EntityType, EntityId, Ip, Reason) ->
|
log(AdminId, Email, Role, Action, EntityType, EntityId, Ip, Reason) ->
|
||||||
Id = infra_utils:generate_id(9),
|
Id = infra_utils:generate_id(9),
|
||||||
Entry = #admin_audit{
|
Entry = #admin_audit{
|
||||||
@@ -22,9 +49,28 @@ log(AdminId, Email, Role, Action, EntityType, EntityId, Ip, Reason) ->
|
|||||||
mnesia:dirty_write(Entry),
|
mnesia:dirty_write(Entry),
|
||||||
{ok, Entry}.
|
{ok, Entry}.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить все записи аудита.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list() -> [#admin_audit{}].
|
||||||
list() ->
|
list() ->
|
||||||
mnesia:dirty_match_object(#admin_audit{_ = '_'}).
|
mnesia:dirty_match_object(#admin_audit{_ = '_'}).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить записи аудита с фильтрацией.
|
||||||
|
%%%
|
||||||
|
%%% Поддерживаемые ключи фильтрации (proplist):
|
||||||
|
%%% <ul>
|
||||||
|
%%% <li>`admin_id` – binary()</li>
|
||||||
|
%%% <li>`action` – binary()</li>
|
||||||
|
%%% <li>`date_from` – calendar:datetime()</li>
|
||||||
|
%%% <li>`date_to` – calendar:datetime()</li>
|
||||||
|
%%% </ul>
|
||||||
|
%%% Все фильтры опциональны, применяются по `AND`.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list(Filters :: proplists:proplist()) -> [#admin_audit{}].
|
||||||
list(Filters) ->
|
list(Filters) ->
|
||||||
All = list(),
|
All = list(),
|
||||||
lists:filter(fun(E) ->
|
lists:filter(fun(E) ->
|
||||||
@@ -49,6 +95,11 @@ list(Filters) ->
|
|||||||
end
|
end
|
||||||
end, All).
|
end, All).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчитать количество действий определённого типа для администратора.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_actions_by_admin(AdminId :: binary(), Action :: binary()) -> non_neg_integer().
|
||||||
count_actions_by_admin(AdminId, Action) ->
|
count_actions_by_admin(AdminId, Action) ->
|
||||||
Match = #admin_audit{admin_id = AdminId, action = Action, _ = '_'},
|
Match = #admin_audit{admin_id = AdminId, action = Action, _ = '_'},
|
||||||
length(mnesia:dirty_match_object(Match)).
|
length(mnesia:dirty_match_object(Match)).
|
||||||
@@ -1,34 +1,58 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Управление сессиями администраторов (refresh-токены).
|
||||||
|
%%%
|
||||||
|
%%% Запись `admin_session` хранит refresh-токен, идентификатор администратора,
|
||||||
|
%%% время истечения и тип токена. Все поля обязательны и инициализируются
|
||||||
|
%%% при создании, поэтому `undefined` в базе не возникает.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
-module(core_admin_session).
|
-module(core_admin_session).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
-export([create/2, validate/1, delete/1]).
|
-export([create/2, validate/1, delete/1]).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Создать новую сессию.
|
||||||
|
%%% Токен действителен 30 дней с момента создания.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
-spec create(AdminId :: binary(), RefreshToken :: binary()) -> ok.
|
-spec create(AdminId :: binary(), RefreshToken :: binary()) -> ok.
|
||||||
create(AdminId, RefreshToken) ->
|
create(AdminId, RefreshToken) ->
|
||||||
Session = #admin_session{
|
Session = #admin_session{
|
||||||
token = RefreshToken,
|
token = RefreshToken,
|
||||||
admin_id = AdminId,
|
admin_id = AdminId,
|
||||||
expires_at = calendar:gregorian_seconds_to_datetime(
|
expires_at = calendar:gregorian_seconds_to_datetime(
|
||||||
calendar:datetime_to_gregorian_seconds(calendar:universal_time()) + 30 * 24 * 3600),
|
calendar:datetime_to_gregorian_seconds(
|
||||||
|
calendar:universal_time()) + 30 * 24 * 3600),
|
||||||
type = refresh
|
type = refresh
|
||||||
},
|
},
|
||||||
mnesia:dirty_write(Session),
|
mnesia:dirty_write(Session),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
-spec validate(Token :: binary()) -> {ok, AdminId :: binary()} | {error, not_found | expired}.
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Проверить refresh-токен.
|
||||||
|
%%% Если токен действителен, возвращает `{ok, AdminId}`.
|
||||||
|
%%% Если истёк — удаляет запись и возвращает `{error, expired}`.
|
||||||
|
%%% Если не найден — `{error, not_found}`.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec validate(Token :: binary()) ->
|
||||||
|
{ok, AdminId :: binary()} | {error, not_found | expired}.
|
||||||
validate(Token) ->
|
validate(Token) ->
|
||||||
case mnesia:dirty_read({admin_session, Token}) of
|
case mnesia:dirty_read({admin_session, Token}) of
|
||||||
[Session] ->
|
[Session] ->
|
||||||
case Session#admin_session.expires_at > calendar:universal_time() of
|
case Session#admin_session.expires_at > calendar:universal_time() of
|
||||||
true ->
|
true -> {ok, Session#admin_session.admin_id};
|
||||||
{ok, Session#admin_session.admin_id};
|
|
||||||
false ->
|
false ->
|
||||||
mnesia:dirty_delete({admin_session, Token}),
|
mnesia:dirty_delete({admin_session, Token}),
|
||||||
{error, expired}
|
{error, expired}
|
||||||
end;
|
end;
|
||||||
[] ->
|
[] -> {error, not_found}
|
||||||
{error, not_found}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Удалить сессию (отзыв refresh-токена).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
-spec delete(Token :: binary()) -> ok.
|
-spec delete(Token :: binary()) -> ok.
|
||||||
delete(Token) ->
|
delete(Token) ->
|
||||||
mnesia:dirty_delete({admin_session, Token}),
|
mnesia:dirty_delete({admin_session, Token}),
|
||||||
|
|||||||
@@ -1,74 +1,89 @@
|
|||||||
-module(core_banned_words).
|
-module(core_banned_words).
|
||||||
-export([list_banned_words/0,
|
|
||||||
add_banned_word/2,
|
|
||||||
remove_banned_word/1,
|
|
||||||
update_banned_word/2]).
|
|
||||||
|
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
-export([list_banned_words/0, add_banned_word/2, remove_banned_word/1]).
|
||||||
|
-export([is_word_banned/1, update_banned_word/2]).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить список всех запрещённых слов.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_banned_words() -> [#banned_word{}].
|
||||||
list_banned_words() ->
|
list_banned_words() ->
|
||||||
mnesia:dirty_match_object(#banned_word{_ = '_'}).
|
mnesia:dirty_match_object(#banned_word{_ = '_'}).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Добавить слово в список запрещённых.
|
||||||
|
%%% Если слово уже существует, возвращает `{error, already_exists}`.
|
||||||
|
%%% Поле `added_by` заполняется идентификатором администратора.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec add_banned_word(Word :: binary(), AddedBy :: binary()) ->
|
||||||
|
{ok, #banned_word{}} | {error, already_exists}.
|
||||||
add_banned_word(Word, AddedBy) ->
|
add_banned_word(Word, AddedBy) ->
|
||||||
|
case is_word_banned(Word) of
|
||||||
|
true ->
|
||||||
|
{error, already_exists};
|
||||||
|
false ->
|
||||||
Id = infra_utils:generate_id(9),
|
Id = infra_utils:generate_id(9),
|
||||||
Now = calendar:universal_time(),
|
Now = calendar:universal_time(),
|
||||||
BW = #banned_word{id = Id, word = Word, added_by = AddedBy, added_at = Now},
|
BannedWord = #banned_word{
|
||||||
case mnesia:transaction(fun() ->
|
id = Id,
|
||||||
case mnesia:match_object(#banned_word{word = Word, _ = '_'}) of
|
word = Word,
|
||||||
[] -> mnesia:write(BW), {ok, BW};
|
added_by = AddedBy,
|
||||||
_ -> mnesia:abort(already_exists)
|
added_at = Now
|
||||||
end
|
},
|
||||||
end) of
|
mnesia:dirty_write(BannedWord),
|
||||||
{atomic, {ok, BWRec}} -> {ok, BWRec};
|
{ok, BannedWord}
|
||||||
{aborted, already_exists} -> {error, already_exists};
|
|
||||||
{aborted, Reason} -> {error, Reason}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Поиск запрещённого слова по его тексту
|
%%%-------------------------------------------------------------------
|
||||||
-spec get_by_word(binary()) -> {ok, #banned_word{}} | {error, not_found}.
|
%%% @doc Удалить слово из списка запрещённых.
|
||||||
get_by_word(Word) ->
|
%%% @end
|
||||||
case mnesia:dirty_match_object(#banned_word{word = Word, _ = '_'}) of
|
%%%-------------------------------------------------------------------
|
||||||
[Record] -> {ok, Record};
|
-spec remove_banned_word(Word :: binary()) -> {ok, deleted} | {error, not_found}.
|
||||||
[] -> {error, not_found}
|
remove_banned_word(Word) ->
|
||||||
end.
|
case is_word_banned(Word) of
|
||||||
|
|
||||||
%% Проверка существования слова (по тексту)
|
|
||||||
word_exists(Word) ->
|
|
||||||
case get_by_word(Word) of
|
|
||||||
{ok, _} -> true;
|
|
||||||
_ -> false
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Обновление запрещённого слова
|
|
||||||
update_banned_word(OldWord, NewWord) ->
|
|
||||||
case get_by_word(OldWord) of
|
|
||||||
{ok, Record} ->
|
|
||||||
case NewWord =:= OldWord orelse not word_exists(NewWord) of
|
|
||||||
true ->
|
true ->
|
||||||
NewRecord = Record#banned_word{word = NewWord},
|
Match = #banned_word{word = Word, _ = '_'},
|
||||||
F = fun() ->
|
[Record] = mnesia:dirty_match_object(Match),
|
||||||
%% Если первичный ключ — id, просто обновляем запись
|
mnesia:dirty_delete({banned_word, Record#banned_word.id}),
|
||||||
mnesia:write(NewRecord),
|
{ok, deleted};
|
||||||
ok
|
false ->
|
||||||
end,
|
{error, not_found}
|
||||||
case mnesia:transaction(F) of
|
end.
|
||||||
{atomic, ok} -> {ok, NewRecord};
|
|
||||||
{aborted, Reason} -> {error, Reason}
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Проверить, является ли слово запрещённым.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec is_word_banned(Word :: binary()) -> boolean().
|
||||||
|
is_word_banned(Word) ->
|
||||||
|
Match = #banned_word{word = Word, _ = '_'},
|
||||||
|
case mnesia:dirty_match_object(Match) of
|
||||||
|
[] -> false;
|
||||||
|
_ -> true
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Обновить запрещённое слово (переименовать).
|
||||||
|
%%% Если `NewWord` уже существует, возвращает `{error, already_exists}`.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update_banned_word(OldWord :: binary(), NewWord :: binary()) ->
|
||||||
|
{ok, #banned_word{}} | {error, not_found | already_exists}.
|
||||||
|
update_banned_word(OldWord, NewWord) ->
|
||||||
|
case is_word_banned(OldWord) of
|
||||||
|
true ->
|
||||||
|
case is_word_banned(NewWord) andalso OldWord =/= NewWord of
|
||||||
|
true ->
|
||||||
|
{error, already_exists};
|
||||||
|
false ->
|
||||||
|
Match = #banned_word{word = OldWord, _ = '_'},
|
||||||
|
[Record] = mnesia:dirty_match_object(Match),
|
||||||
|
Updated = Record#banned_word{word = NewWord},
|
||||||
|
mnesia:dirty_write(Updated),
|
||||||
|
{ok, Updated}
|
||||||
end;
|
end;
|
||||||
false ->
|
false ->
|
||||||
{error, already_exists}
|
{error, not_found}
|
||||||
end;
|
|
||||||
{error, _} = Error -> Error
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Удаление слова (используем get_by_word для надёжности)
|
|
||||||
remove_banned_word(Word) ->
|
|
||||||
case get_by_word(Word) of
|
|
||||||
{ok, Record} ->
|
|
||||||
F = fun() -> mnesia:delete({banned_word, Record#banned_word.id}), ok end,
|
|
||||||
case mnesia:transaction(F) of
|
|
||||||
{atomic, ok} -> {ok, Record};
|
|
||||||
{aborted, Reason} -> {error, Reason}
|
|
||||||
end;
|
|
||||||
{error, _} = Error -> Error
|
|
||||||
end.
|
end.
|
||||||
+117
-70
@@ -1,41 +1,127 @@
|
|||||||
-module(core_booking).
|
-module(core_booking).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
-export([create/3, get_by_id/1, list_by_event/1, list_by_user/1, update/2, delete/1]).
|
||||||
|
-export([count_bookings/0, get_by_event_and_user/2]).
|
||||||
|
-export([list_all/0]). % ← добавлено
|
||||||
|
|
||||||
-export([create/2, get_by_id/1, get_by_event_and_user/2, list_by_event/1, list_by_user/1]).
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
-export([update_status/2, delete/1]).
|
%% Значения по умолчанию для необязательных полей
|
||||||
-export([count_bookings/0]).
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
|
-define(DEFAULT_NOTES, <<>>).
|
||||||
|
-define(DEFAULT_REMINDER_SENT, false).
|
||||||
|
-define(DEFAULT_CONFIRMED_AT, {{1970,1,1},{0,0,0}}).
|
||||||
|
|
||||||
%% Создание бронирования
|
%%%-------------------------------------------------------------------
|
||||||
create(EventId, UserId) ->
|
%%% @doc Создание бронирования.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec create(EventId :: binary(), UserId :: binary(), Status :: atom()) ->
|
||||||
|
{ok, #booking{}} | {error, term()}.
|
||||||
|
create(EventId, UserId, Status) ->
|
||||||
Id = infra_utils:generate_id(16),
|
Id = infra_utils:generate_id(16),
|
||||||
|
Now = calendar:universal_time(),
|
||||||
Booking = #booking{
|
Booking = #booking{
|
||||||
id = Id,
|
id = Id,
|
||||||
event_id = EventId,
|
event_id = EventId,
|
||||||
user_id = UserId,
|
user_id = UserId,
|
||||||
status = pending,
|
status = Status,
|
||||||
confirmed_at = undefined,
|
notes = ?DEFAULT_NOTES,
|
||||||
created_at = calendar:universal_time(),
|
reminder_sent = ?DEFAULT_REMINDER_SENT,
|
||||||
updated_at = calendar:universal_time()
|
confirmed_at = ?DEFAULT_CONFIRMED_AT,
|
||||||
|
created_at = Now,
|
||||||
|
updated_at = Now
|
||||||
},
|
},
|
||||||
|
F = fun() -> mnesia:write(Booking), {ok, Booking} end,
|
||||||
F = fun() ->
|
|
||||||
mnesia:write(Booking),
|
|
||||||
{ok, Booking}
|
|
||||||
end,
|
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
case mnesia:transaction(F) of
|
||||||
{atomic, Result} -> Result;
|
{atomic, Result} -> Result;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получение бронирования по ID
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить бронирование по идентификатору.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_by_id(Id :: binary()) -> {ok, #booking{}} | {error, not_found}.
|
||||||
get_by_id(Id) ->
|
get_by_id(Id) ->
|
||||||
case mnesia:dirty_read(booking, Id) of
|
case mnesia:dirty_read(booking, Id) of
|
||||||
[] -> {error, not_found};
|
[] -> {error, not_found};
|
||||||
[Booking] -> {ok, Booking}
|
[Booking] -> {ok, Booking}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получение бронирования по событию и пользователю
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список бронирований для события.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_by_event(EventId :: binary()) -> {ok, [#booking{}]}.
|
||||||
|
list_by_event(EventId) ->
|
||||||
|
Match = #booking{event_id = EventId, _ = '_'},
|
||||||
|
Bookings = mnesia:dirty_match_object(Match),
|
||||||
|
{ok, Bookings}.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список бронирований пользователя.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_by_user(UserId :: binary()) -> {ok, [#booking{}]}.
|
||||||
|
list_by_user(UserId) ->
|
||||||
|
Match = #booking{user_id = UserId, _ = '_'},
|
||||||
|
Bookings = mnesia:dirty_match_object(Match),
|
||||||
|
{ok, Bookings}.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить все бронирования (административный режим).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_all() -> [#booking{}].
|
||||||
|
list_all() ->
|
||||||
|
mnesia:dirty_match_object(#booking{_ = '_'}).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Обновить поля бронирования.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update(Id :: binary(), Updates :: [{atom(), term()}]) ->
|
||||||
|
{ok, #booking{}} | {error, not_found | term()}.
|
||||||
|
update(Id, Updates) ->
|
||||||
|
F = fun() ->
|
||||||
|
case mnesia:read(booking, Id) of
|
||||||
|
[] -> {error, not_found};
|
||||||
|
[Booking] ->
|
||||||
|
UpdatedBooking = apply_updates(Booking, Updates),
|
||||||
|
mnesia:write(UpdatedBooking),
|
||||||
|
{ok, UpdatedBooking}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(F) of
|
||||||
|
{atomic, Result} -> Result;
|
||||||
|
{aborted, Reason} -> {error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Удаление бронирования (физическое).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec delete(Id :: binary()) -> ok | {error, not_found}.
|
||||||
|
delete(Id) ->
|
||||||
|
case mnesia:dirty_read(booking, Id) of
|
||||||
|
[] -> {error, not_found};
|
||||||
|
[_] -> mnesia:dirty_delete(booking, Id), ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Количество бронирований.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_bookings() -> non_neg_integer().
|
||||||
|
count_bookings() ->
|
||||||
|
mnesia:table_info(booking, size).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить бронирование по событию и пользователю.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_by_event_and_user(EventId :: binary(), UserId :: binary()) ->
|
||||||
|
{ok, #booking{}} | {error, not_found}.
|
||||||
get_by_event_and_user(EventId, UserId) ->
|
get_by_event_and_user(EventId, UserId) ->
|
||||||
Match = #booking{event_id = EventId, user_id = UserId, _ = '_'},
|
Match = #booking{event_id = EventId, user_id = UserId, _ = '_'},
|
||||||
case mnesia:dirty_match_object(Match) of
|
case mnesia:dirty_match_object(Match) of
|
||||||
@@ -43,58 +129,19 @@ get_by_event_and_user(EventId, UserId) ->
|
|||||||
[Booking] -> {ok, Booking}
|
[Booking] -> {ok, Booking}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Список бронирований события
|
%%%===================================================================
|
||||||
list_by_event(EventId) ->
|
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||||
Match = #booking{event_id = EventId, _ = '_'},
|
%%%===================================================================
|
||||||
Bookings = mnesia:dirty_match_object(Match),
|
|
||||||
{ok, Bookings}.
|
|
||||||
|
|
||||||
%% Список бронирований пользователя
|
apply_updates(Booking, Updates) ->
|
||||||
list_by_user(UserId) ->
|
Updated = lists:foldl(fun({Field, Value}, B) -> set_field(Field, Value, B) end,
|
||||||
Match = #booking{user_id = UserId, _ = '_'},
|
Booking, Updates),
|
||||||
Bookings = mnesia:dirty_match_object(Match),
|
Updated#booking{updated_at = calendar:universal_time()}.
|
||||||
{ok, Bookings}.
|
|
||||||
|
|
||||||
%% Обновление статуса бронирования
|
set_field(status, Value, B) -> B#booking{status = Value};
|
||||||
update_status(Id, Status) when Status =:= pending; Status =:= confirmed; Status =:= cancelled ->
|
set_field(notes, Value, B) -> B#booking{notes = Value};
|
||||||
F = fun() ->
|
set_field(reminder_sent, Value, B) -> B#booking{reminder_sent = Value};
|
||||||
case mnesia:read(booking, Id) of
|
set_field(confirmed_at, Value, B) -> B#booking{confirmed_at = Value};
|
||||||
[] ->
|
set_field(event_id, Value, B) -> B#booking{event_id = Value};
|
||||||
{error, not_found};
|
set_field(user_id, Value, B) -> B#booking{user_id = Value};
|
||||||
[Booking] ->
|
set_field(_, _, B) -> B.
|
||||||
Updated = Booking#booking{
|
|
||||||
status = Status,
|
|
||||||
confirmed_at = case Status of
|
|
||||||
confirmed -> calendar:universal_time();
|
|
||||||
_ -> Booking#booking.confirmed_at
|
|
||||||
end,
|
|
||||||
updated_at = calendar:universal_time()
|
|
||||||
},
|
|
||||||
mnesia:write(Updated),
|
|
||||||
{ok, Updated}
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
|
||||||
{atomic, Result} -> Result;
|
|
||||||
{aborted, Reason} -> {error, Reason}
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Удаление бронирования (hard delete)
|
|
||||||
delete(Id) ->
|
|
||||||
F = fun() ->
|
|
||||||
case mnesia:read(booking, Id) of
|
|
||||||
[] ->
|
|
||||||
{error, not_found};
|
|
||||||
[Booking] ->
|
|
||||||
mnesia:delete_object(Booking),
|
|
||||||
{ok, deleted}
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
|
||||||
{atomic, Result} -> Result;
|
|
||||||
{aborted, Reason} -> {error, Reason}
|
|
||||||
end.
|
|
||||||
|
|
||||||
count_bookings() -> mnesia:table_info(booking, size).
|
|
||||||
+100
-34
@@ -2,47 +2,60 @@
|
|||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
-export([create/4, create/5, get_by_id/1, list_by_owner/1, update/2, delete/1]).
|
-export([create/4, create/5, get_by_id/1, list_by_owner/1, update/2, delete/1]).
|
||||||
-export([count_calendars/0, list_all/0]).
|
-export([count_calendars/0, list_all/0]).
|
||||||
-export([freeze/2, unfreeze/2]). % ← новые функции
|
-export([freeze/2, unfreeze/2]).
|
||||||
|
|
||||||
%% Создание календаря
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
|
%% Значения по умолчанию для необязательных полей
|
||||||
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
|
-define(DEFAULT_SHORT_NAME, <<>>).
|
||||||
|
-define(DEFAULT_CATEGORY, <<>>).
|
||||||
|
-define(DEFAULT_COLOR, <<>>).
|
||||||
|
-define(DEFAULT_IMAGE_URL, <<>>).
|
||||||
|
-define(DEFAULT_SETTINGS, #{}).
|
||||||
|
-define(DEFAULT_TAGS, []).
|
||||||
|
-define(DEFAULT_REASON, <<>>).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Создание календаря с типом `personal` и подтверждением по умолчанию.
|
||||||
|
%%% Все необязательные поля инициализируются значениями по умолчанию.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec create(OwnerId :: binary(), Title :: binary(), Description :: binary(),
|
||||||
|
Confirmation :: auto | manual | {timeout, integer()}) ->
|
||||||
|
{ok, #calendar{}} | {error, term()}.
|
||||||
create(OwnerId, Title, Description, Confirmation) ->
|
create(OwnerId, Title, Description, Confirmation) ->
|
||||||
Id = infra_utils:generate_id(16),
|
create(OwnerId, Title, Description, Confirmation, personal).
|
||||||
Calendar = #calendar{
|
|
||||||
id = Id,
|
|
||||||
owner_id = OwnerId,
|
|
||||||
title = Title,
|
|
||||||
description = Description,
|
|
||||||
tags = [],
|
|
||||||
type = personal,
|
|
||||||
confirmation = Confirmation,
|
|
||||||
rating_avg = 0.0,
|
|
||||||
rating_count = 0,
|
|
||||||
status = active,
|
|
||||||
created_at = calendar:universal_time(),
|
|
||||||
updated_at = calendar:universal_time()
|
|
||||||
},
|
|
||||||
F = fun() -> mnesia:write(Calendar), {ok, Calendar} end,
|
|
||||||
case mnesia:transaction(F) of
|
|
||||||
{atomic, Result} -> Result;
|
|
||||||
{aborted, Reason} -> {error, Reason}
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Создание календаря с типом и политикой
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Создание календаря с указанием типа и политики подтверждения.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec create(OwnerId :: binary(), Title :: binary(), Description :: binary(),
|
||||||
|
Confirmation :: auto | manual | {timeout, integer()},
|
||||||
|
Type :: personal | commercial) ->
|
||||||
|
{ok, #calendar{}} | {error, term()}.
|
||||||
create(OwnerId, Title, Description, Confirmation, Type) ->
|
create(OwnerId, Title, Description, Confirmation, Type) ->
|
||||||
Id = infra_utils:generate_id(16),
|
Id = infra_utils:generate_id(16),
|
||||||
|
Now = calendar:universal_time(),
|
||||||
Calendar = #calendar{
|
Calendar = #calendar{
|
||||||
id = Id,
|
id = Id,
|
||||||
owner_id = OwnerId,
|
owner_id = OwnerId,
|
||||||
title = Title,
|
title = Title,
|
||||||
description = Description,
|
description = Description,
|
||||||
tags = [],
|
short_name = ?DEFAULT_SHORT_NAME,
|
||||||
|
category = ?DEFAULT_CATEGORY,
|
||||||
|
color = ?DEFAULT_COLOR,
|
||||||
|
image_url = ?DEFAULT_IMAGE_URL,
|
||||||
|
settings = ?DEFAULT_SETTINGS,
|
||||||
|
tags = ?DEFAULT_TAGS,
|
||||||
type = Type,
|
type = Type,
|
||||||
confirmation = Confirmation,
|
confirmation = Confirmation,
|
||||||
rating_avg = 0.0,
|
rating_avg = 0.0,
|
||||||
rating_count = 0,
|
rating_count = 0,
|
||||||
status = active,
|
status = active,
|
||||||
created_at = calendar:universal_time(),
|
reason = ?DEFAULT_REASON,
|
||||||
updated_at = calendar:universal_time()
|
created_at = Now,
|
||||||
|
updated_at = Now
|
||||||
},
|
},
|
||||||
F = fun() -> mnesia:write(Calendar), {ok, Calendar} end,
|
F = fun() -> mnesia:write(Calendar), {ok, Calendar} end,
|
||||||
case mnesia:transaction(F) of
|
case mnesia:transaction(F) of
|
||||||
@@ -50,20 +63,35 @@ create(OwnerId, Title, Description, Confirmation, Type) ->
|
|||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получение календаря по ID
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить календарь по идентификатору.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_by_id(Id :: binary()) -> {ok, #calendar{}} | {error, not_found}.
|
||||||
get_by_id(Id) ->
|
get_by_id(Id) ->
|
||||||
case mnesia:dirty_read(calendar, Id) of
|
case mnesia:dirty_read(calendar, Id) of
|
||||||
[] -> {error, not_found};
|
[] -> {error, not_found};
|
||||||
[Calendar] -> {ok, Calendar}
|
[Calendar] -> {ok, Calendar}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Список календарей владельца
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список активных календарей владельца.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_by_owner(OwnerId :: binary()) -> {ok, [#calendar{}]}.
|
||||||
list_by_owner(OwnerId) ->
|
list_by_owner(OwnerId) ->
|
||||||
Match = #calendar{owner_id = OwnerId, status = active, _ = '_'},
|
Match = #calendar{owner_id = OwnerId, status = active, _ = '_'},
|
||||||
Calendars = mnesia:dirty_match_object(Match),
|
Calendars = mnesia:dirty_match_object(Match),
|
||||||
{ok, Calendars}.
|
{ok, Calendars}.
|
||||||
|
|
||||||
%% Обновление календаря
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Обновить поля календаря.
|
||||||
|
%%% `Updates` – список пар `[{atom(), term()}]`.
|
||||||
|
%%% Поля, не указанные в обновлении, сохраняют прежние значения.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update(Id :: binary(), Updates :: [{atom(), term()}]) ->
|
||||||
|
{ok, #calendar{}} | {error, not_found | term()}.
|
||||||
update(Id, Updates) ->
|
update(Id, Updates) ->
|
||||||
F = fun() ->
|
F = fun() ->
|
||||||
case mnesia:read(calendar, Id) of
|
case mnesia:read(calendar, Id) of
|
||||||
@@ -79,13 +107,26 @@ update(Id, Updates) ->
|
|||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Удаление календаря (soft delete)
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Мягкое удаление календаря (установка статуса `deleted`).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec delete(Id :: binary()) -> {ok, #calendar{}} | {error, not_found | term()}.
|
||||||
delete(Id) ->
|
delete(Id) ->
|
||||||
update(Id, [{status, deleted}]).
|
update(Id, [{status, deleted}]).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Количество записей в таблице `calendar`.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_calendars() -> non_neg_integer().
|
||||||
count_calendars() ->
|
count_calendars() ->
|
||||||
mnesia:table_info(calendar, size).
|
mnesia:table_info(calendar, size).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить список всех календарей (включая удалённые).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
-spec list_all() -> [#calendar{}].
|
-spec list_all() -> [#calendar{}].
|
||||||
list_all() ->
|
list_all() ->
|
||||||
{atomic, Calendars} = mnesia:transaction(fun() ->
|
{atomic, Calendars} = mnesia:transaction(fun() ->
|
||||||
@@ -93,19 +134,44 @@ list_all() ->
|
|||||||
end),
|
end),
|
||||||
Calendars.
|
Calendars.
|
||||||
|
|
||||||
%% ── НОВЫЕ ФУНКЦИИ ──────────────────────────────────────────
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Заморозить календарь с указанием причины.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec freeze(Id :: binary(), Reason :: binary()) ->
|
||||||
|
{ok, #calendar{}} | {error, not_found | term()}.
|
||||||
freeze(Id, Reason) ->
|
freeze(Id, Reason) ->
|
||||||
update(Id, [{status, frozen}, {reason, Reason}]).
|
update(Id, [{status, frozen}, {reason, Reason}]).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Разморозить календарь с указанием причины.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec unfreeze(Id :: binary(), Reason :: binary()) ->
|
||||||
|
{ok, #calendar{}} | {error, not_found | term()}.
|
||||||
unfreeze(Id, Reason) ->
|
unfreeze(Id, Reason) ->
|
||||||
update(Id, [{status, active}, {reason, Reason}]).
|
update(Id, [{status, active}, {reason, Reason}]).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Применить список обновлений к записи календаря.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec apply_updates(#calendar{}, [{atom(), term()}]) -> #calendar{}.
|
||||||
apply_updates(Calendar, Updates) ->
|
apply_updates(Calendar, Updates) ->
|
||||||
Updated = lists:foldl(fun({Field, Value}, C) ->
|
Updated = lists:foldl(fun({Field, Value}, C) -> set_field(Field, Value, C) end,
|
||||||
set_field(Field, Value, C)
|
Calendar, Updates),
|
||||||
end, Calendar, Updates),
|
|
||||||
Updated#calendar{updated_at = calendar:universal_time()}.
|
Updated#calendar{updated_at = calendar:universal_time()}.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Установить одно поле записи календаря.
|
||||||
|
%%% Неизвестные поля игнорируются.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec set_field(atom(), term(), #calendar{}) -> #calendar{}.
|
||||||
set_field(title, Value, C) -> C#calendar{title = Value};
|
set_field(title, Value, C) -> C#calendar{title = Value};
|
||||||
set_field(description, Value, C) -> C#calendar{description = Value};
|
set_field(description, Value, C) -> C#calendar{description = Value};
|
||||||
set_field(short_name, Value, C) -> C#calendar{short_name = Value};
|
set_field(short_name, Value, C) -> C#calendar{short_name = Value};
|
||||||
|
|||||||
+161
-63
@@ -1,15 +1,36 @@
|
|||||||
-module(core_event).
|
-module(core_event).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
-export([create/4, create_recurring/5, get_by_id/1, list_by_calendar/1, update/2, delete/1,
|
||||||
-export([create/4, create_recurring/5, get_by_id/1, list_by_calendar/1,
|
materialize_occurrence/3]).
|
||||||
update/2, delete/1, materialize_occurrence/3]).
|
|
||||||
-export([count_events/0, count_events_by_date/2]).
|
-export([count_events/0, count_events_by_date/2]).
|
||||||
-export([freeze/2, unfreeze/2]).
|
-export([freeze/2, unfreeze/2]).
|
||||||
-export([list_all/0]).
|
-export([list_all/0]).
|
||||||
|
|
||||||
%% Создание одиночного события
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
|
%% Значения по умолчанию для необязательных полей
|
||||||
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
|
-define(DEFAULT_RECURRENCE_RULE, <<>>).
|
||||||
|
-define(DEFAULT_MASTER_ID, <<>>).
|
||||||
|
-define(DEFAULT_SPECIALIST_ID, <<>>).
|
||||||
|
-define(DEFAULT_LOCATION, #location{address = <<>>, lat = 0.0, lon = 0.0}).
|
||||||
|
-define(DEFAULT_TAGS, []).
|
||||||
|
-define(DEFAULT_CAPACITY, 0).
|
||||||
|
-define(DEFAULT_ONLINE_LINK, <<>>).
|
||||||
|
-define(DEFAULT_REASON, <<>>).
|
||||||
|
-define(DEFAULT_ATTACHMENTS, []).
|
||||||
|
-define(DEFAULT_EDIT_HISTORY, []).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Создание одиночного события.
|
||||||
|
%%% Все необязательные поля инициализируются значениями по умолчанию.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec create(CalendarId :: binary(), Title :: binary(),
|
||||||
|
StartTime :: calendar:datetime(), Duration :: non_neg_integer()) ->
|
||||||
|
{ok, #event{}} | {error, term()}.
|
||||||
create(CalendarId, Title, StartTime, Duration) ->
|
create(CalendarId, Title, StartTime, Duration) ->
|
||||||
Id = infra_utils:generate_id(16),
|
Id = infra_utils:generate_id(16),
|
||||||
|
Now = calendar:universal_time(),
|
||||||
Event = #event{
|
Event = #event{
|
||||||
id = Id,
|
id = Id,
|
||||||
calendar_id = CalendarId,
|
calendar_id = CalendarId,
|
||||||
@@ -18,34 +39,40 @@ create(CalendarId, Title, StartTime, Duration) ->
|
|||||||
event_type = single,
|
event_type = single,
|
||||||
start_time = StartTime,
|
start_time = StartTime,
|
||||||
duration = Duration,
|
duration = Duration,
|
||||||
recurrence_rule = undefined,
|
recurrence_rule = ?DEFAULT_RECURRENCE_RULE,
|
||||||
master_id = undefined,
|
master_id = ?DEFAULT_MASTER_ID,
|
||||||
is_instance = false,
|
is_instance = false,
|
||||||
specialist_id = undefined,
|
specialist_id = ?DEFAULT_SPECIALIST_ID,
|
||||||
location = undefined,
|
location = ?DEFAULT_LOCATION,
|
||||||
tags = [],
|
tags = ?DEFAULT_TAGS,
|
||||||
capacity = undefined,
|
capacity = ?DEFAULT_CAPACITY,
|
||||||
online_link = undefined,
|
online_link = ?DEFAULT_ONLINE_LINK,
|
||||||
status = active,
|
status = active,
|
||||||
|
reason = ?DEFAULT_REASON,
|
||||||
rating_avg = 0.0,
|
rating_avg = 0.0,
|
||||||
rating_count = 0,
|
rating_count = 0,
|
||||||
created_at = calendar:universal_time(),
|
attachments = ?DEFAULT_ATTACHMENTS,
|
||||||
updated_at = calendar:universal_time()
|
edit_history = ?DEFAULT_EDIT_HISTORY,
|
||||||
|
created_at = Now,
|
||||||
|
updated_at = Now
|
||||||
},
|
},
|
||||||
|
F = fun() -> mnesia:write(Event), {ok, Event} end,
|
||||||
F = fun() ->
|
|
||||||
mnesia:write(Event),
|
|
||||||
{ok, Event}
|
|
||||||
end,
|
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
case mnesia:transaction(F) of
|
||||||
{atomic, Result} -> Result;
|
{atomic, Result} -> Result;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Создание повторяющегося события (мастер-запись)
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Создание повторяющегося события (мастер-запись).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec create_recurring(CalendarId :: binary(), Title :: binary(),
|
||||||
|
StartTime :: calendar:datetime(), Duration :: non_neg_integer(),
|
||||||
|
RRule :: map()) ->
|
||||||
|
{ok, #event{}} | {error, term()}.
|
||||||
create_recurring(CalendarId, Title, StartTime, Duration, RRule) ->
|
create_recurring(CalendarId, Title, StartTime, Duration, RRule) ->
|
||||||
Id = infra_utils:generate_id(16),
|
Id = infra_utils:generate_id(16),
|
||||||
|
Now = calendar:universal_time(),
|
||||||
Event = #event{
|
Event = #event{
|
||||||
id = Id,
|
id = Id,
|
||||||
calendar_id = CalendarId,
|
calendar_id = CalendarId,
|
||||||
@@ -55,45 +82,49 @@ create_recurring(CalendarId, Title, StartTime, Duration, RRule) ->
|
|||||||
start_time = StartTime,
|
start_time = StartTime,
|
||||||
duration = Duration,
|
duration = Duration,
|
||||||
recurrence_rule = jsx:encode(RRule),
|
recurrence_rule = jsx:encode(RRule),
|
||||||
master_id = undefined,
|
master_id = ?DEFAULT_MASTER_ID,
|
||||||
is_instance = false,
|
is_instance = false,
|
||||||
specialist_id = undefined,
|
specialist_id = ?DEFAULT_SPECIALIST_ID,
|
||||||
location = undefined,
|
location = ?DEFAULT_LOCATION,
|
||||||
tags = [],
|
tags = ?DEFAULT_TAGS,
|
||||||
capacity = undefined,
|
capacity = ?DEFAULT_CAPACITY,
|
||||||
online_link = undefined,
|
online_link = ?DEFAULT_ONLINE_LINK,
|
||||||
status = active,
|
status = active,
|
||||||
|
reason = ?DEFAULT_REASON,
|
||||||
rating_avg = 0.0,
|
rating_avg = 0.0,
|
||||||
rating_count = 0,
|
rating_count = 0,
|
||||||
created_at = calendar:universal_time(),
|
attachments = ?DEFAULT_ATTACHMENTS,
|
||||||
updated_at = calendar:universal_time()
|
edit_history = ?DEFAULT_EDIT_HISTORY,
|
||||||
|
created_at = Now,
|
||||||
|
updated_at = Now
|
||||||
},
|
},
|
||||||
|
F = fun() -> mnesia:write(Event), {ok, Event} end,
|
||||||
F = fun() ->
|
|
||||||
mnesia:write(Event),
|
|
||||||
{ok, Event}
|
|
||||||
end,
|
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
case mnesia:transaction(F) of
|
||||||
{atomic, Result} -> Result;
|
{atomic, Result} -> Result;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Материализация вхождения повторяющегося события
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Материализация вхождения повторяющегося события.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec materialize_occurrence(MasterId :: binary(),
|
||||||
|
OccurrenceStart :: calendar:datetime(),
|
||||||
|
SpecialistId :: binary()) ->
|
||||||
|
{ok, #event{}} | {error, term()}.
|
||||||
materialize_occurrence(MasterId, OccurrenceStart, SpecialistId) ->
|
materialize_occurrence(MasterId, OccurrenceStart, SpecialistId) ->
|
||||||
case mnesia:dirty_read(event, MasterId) of
|
case mnesia:dirty_read(event, MasterId) of
|
||||||
[] ->
|
[] ->
|
||||||
{error, master_not_found};
|
{error, master_not_found};
|
||||||
[Master] when Master#event.event_type =:= recurring ->
|
[Master] when Master#event.event_type =:= recurring ->
|
||||||
% Проверяем, не существует ли уже материализованное вхождение
|
|
||||||
Existing = mnesia:dirty_match_object(
|
Existing = mnesia:dirty_match_object(
|
||||||
#event{master_id = MasterId, start_time = OccurrenceStart, is_instance = true, _ = '_'}
|
#event{master_id = MasterId, start_time = OccurrenceStart,
|
||||||
|
is_instance = true, _ = '_'}
|
||||||
),
|
),
|
||||||
|
|
||||||
case Existing of
|
case Existing of
|
||||||
[] ->
|
[] ->
|
||||||
% Создаём новый экземпляр
|
|
||||||
InstanceId = infra_utils:generate_id(16),
|
InstanceId = infra_utils:generate_id(16),
|
||||||
|
Now = calendar:universal_time(),
|
||||||
Instance = #event{
|
Instance = #event{
|
||||||
id = InstanceId,
|
id = InstanceId,
|
||||||
calendar_id = Master#event.calendar_id,
|
calendar_id = Master#event.calendar_id,
|
||||||
@@ -102,7 +133,7 @@ materialize_occurrence(MasterId, OccurrenceStart, SpecialistId) ->
|
|||||||
event_type = single,
|
event_type = single,
|
||||||
start_time = OccurrenceStart,
|
start_time = OccurrenceStart,
|
||||||
duration = Master#event.duration,
|
duration = Master#event.duration,
|
||||||
recurrence_rule = undefined,
|
recurrence_rule = ?DEFAULT_RECURRENCE_RULE,
|
||||||
master_id = MasterId,
|
master_id = MasterId,
|
||||||
is_instance = true,
|
is_instance = true,
|
||||||
specialist_id = SpecialistId,
|
specialist_id = SpecialistId,
|
||||||
@@ -111,76 +142,106 @@ materialize_occurrence(MasterId, OccurrenceStart, SpecialistId) ->
|
|||||||
capacity = Master#event.capacity,
|
capacity = Master#event.capacity,
|
||||||
online_link = Master#event.online_link,
|
online_link = Master#event.online_link,
|
||||||
status = active,
|
status = active,
|
||||||
|
reason = ?DEFAULT_REASON,
|
||||||
rating_avg = 0.0,
|
rating_avg = 0.0,
|
||||||
rating_count = 0,
|
rating_count = 0,
|
||||||
created_at = calendar:universal_time(),
|
attachments = ?DEFAULT_ATTACHMENTS,
|
||||||
updated_at = calendar:universal_time()
|
edit_history = ?DEFAULT_EDIT_HISTORY,
|
||||||
|
created_at = Now,
|
||||||
|
updated_at = Now
|
||||||
},
|
},
|
||||||
|
F = fun() -> mnesia:write(Instance), {ok, Instance} end,
|
||||||
F = fun() ->
|
|
||||||
mnesia:write(Instance),
|
|
||||||
{ok, Instance}
|
|
||||||
end,
|
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
case mnesia:transaction(F) of
|
||||||
{atomic, Result} -> Result;
|
{atomic, Result} -> Result;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end;
|
end;
|
||||||
[Instance] ->
|
[Instance] ->
|
||||||
% Уже существует, возвращаем его
|
|
||||||
{ok, Instance}
|
{ok, Instance}
|
||||||
end;
|
end;
|
||||||
[_] ->
|
[_] ->
|
||||||
{error, not_recurring}
|
{error, not_recurring}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получение события по ID
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить событие по идентификатору.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_by_id(Id :: binary()) -> {ok, #event{}} | {error, not_found}.
|
||||||
get_by_id(Id) ->
|
get_by_id(Id) ->
|
||||||
case mnesia:dirty_read(event, Id) of
|
case mnesia:dirty_read(event, Id) of
|
||||||
[] -> {error, not_found};
|
[] -> {error, not_found};
|
||||||
[Event] -> {ok, Event}
|
[Event] -> {ok, Event}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Список событий календаря (только мастер-записи и одиночные)
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список активных событий календаря.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_by_calendar(CalendarId :: binary()) -> {ok, [#event{}]}.
|
||||||
list_by_calendar(CalendarId) ->
|
list_by_calendar(CalendarId) ->
|
||||||
Match = #event{calendar_id = CalendarId, status = active, is_instance = false, _ = '_'},
|
Match = #event{calendar_id = CalendarId, status = active, is_instance = false, _ = '_'},
|
||||||
Events = mnesia:dirty_match_object(Match),
|
Events = mnesia:dirty_match_object(Match),
|
||||||
{ok, Events}.
|
{ok, Events}.
|
||||||
|
|
||||||
%% Обновление события
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Обновить поля события.
|
||||||
|
%%% `Updates` – список пар `[{atom(), term()}]`.
|
||||||
|
%%% Поля, не указанные в обновлении, сохраняют прежние значения.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update(Id :: binary(), Updates :: [{atom(), term()}]) ->
|
||||||
|
{ok, #event{}} | {error, not_found | term()}.
|
||||||
update(Id, Updates) ->
|
update(Id, Updates) ->
|
||||||
F = fun() ->
|
F = fun() ->
|
||||||
case mnesia:read(event, Id) of
|
case mnesia:read(event, Id) of
|
||||||
[] ->
|
[] -> {error, not_found};
|
||||||
{error, not_found};
|
|
||||||
[Event] ->
|
[Event] ->
|
||||||
UpdatedEvent = apply_updates(Event, Updates),
|
UpdatedEvent = apply_updates(Event, Updates),
|
||||||
mnesia:write(UpdatedEvent),
|
mnesia:write(UpdatedEvent),
|
||||||
{ok, UpdatedEvent}
|
{ok, UpdatedEvent}
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
case mnesia:transaction(F) of
|
||||||
{atomic, Result} -> Result;
|
{atomic, Result} -> Result;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Удаление события (soft delete)
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Мягкое удаление события (установка статуса `deleted`).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec delete(Id :: binary()) -> {ok, #event{}} | {error, not_found | term()}.
|
||||||
delete(Id) ->
|
delete(Id) ->
|
||||||
update(Id, [{status, deleted}]).
|
update(Id, [{status, deleted}]).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Количество записей в таблице `event`.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_events() -> non_neg_integer().
|
||||||
count_events() ->
|
count_events() ->
|
||||||
mnesia:table_info(event, size).
|
mnesia:table_info(event, size).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить список всех активных мастер-событий.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_all() -> [#event{}].
|
||||||
list_all() ->
|
list_all() ->
|
||||||
Match = #event{status = active, is_instance = false, _ = '_'},
|
Match = #event{status = active, is_instance = false, _ = '_'},
|
||||||
mnesia:dirty_match_object(Match).
|
mnesia:dirty_match_object(Match).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Подсчёт событий, созданных в заданном временном диапазоне.
|
||||||
|
%%% Возвращает список кортежей `{{Year,Month,Day}, Count}`, отсортированный по дате.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_events_by_date(From :: calendar:datetime(), To :: calendar:datetime()) ->
|
||||||
|
[{{pos_integer(), pos_integer(), pos_integer()}, non_neg_integer()}].
|
||||||
count_events_by_date(From, To) ->
|
count_events_by_date(From, To) ->
|
||||||
All = mnesia:dirty_match_object(#event{_ = '_'}),
|
All = mnesia:dirty_match_object(#event{_ = '_'}),
|
||||||
Filtered = lists:filter(fun(E) ->
|
Filtered = lists:filter(fun(E) -> E#event.created_at >= From andalso
|
||||||
E#event.created_at >= From andalso E#event.created_at =< To
|
E#event.created_at =< To end, All),
|
||||||
end, All),
|
|
||||||
Counts = lists:foldl(fun(E, Acc) ->
|
Counts = lists:foldl(fun(E, Acc) ->
|
||||||
Day = date_part(E#event.created_at),
|
Day = date_part(E#event.created_at),
|
||||||
case lists:keyfind(Day, 1, Acc) of
|
case lists:keyfind(Day, 1, Acc) of
|
||||||
@@ -190,21 +251,51 @@ count_events_by_date(From, To) ->
|
|||||||
end, [], Filtered),
|
end, [], Filtered),
|
||||||
lists:sort(Counts).
|
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}.
|
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Применить список обновлений к записи события.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec apply_updates(#event{}, [{atom(), term()}]) -> #event{}.
|
||||||
apply_updates(Event, Updates) ->
|
apply_updates(Event, Updates) ->
|
||||||
Updated = lists:foldl(fun({Field, Value}, E) ->
|
Updated = lists:foldl(fun({Field, Value}, E) -> set_field(Field, Value, E) end,
|
||||||
set_field(Field, Value, E)
|
Event, Updates),
|
||||||
end, Event, Updates),
|
|
||||||
Updated#event{updated_at = calendar:universal_time()}.
|
Updated#event{updated_at = calendar:universal_time()}.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Заморозить событие с указанием причины.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec freeze(Id :: binary(), Reason :: binary()) ->
|
||||||
|
{ok, #event{}} | {error, not_found | term()}.
|
||||||
freeze(Id, Reason) ->
|
freeze(Id, Reason) ->
|
||||||
update(Id, [{status, frozen}, {reason, Reason}]).
|
update(Id, [{status, frozen}, {reason, Reason}]).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Разморозить событие с указанием причины.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec unfreeze(Id :: binary(), Reason :: binary()) ->
|
||||||
|
{ok, #event{}} | {error, not_found | term()}.
|
||||||
unfreeze(Id, Reason) ->
|
unfreeze(Id, Reason) ->
|
||||||
update(Id, [{status, active}, {reason, Reason}]).
|
update(Id, [{status, active}, {reason, Reason}]).
|
||||||
|
|
||||||
%% ── ОБНОВЛЁННАЯ set_field ──────────────────────────────────
|
%%%===================================================================
|
||||||
|
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Установить одно поле записи события.
|
||||||
|
%%% Неизвестные поля игнорируются.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec set_field(atom(), term(), #event{}) -> #event{}.
|
||||||
set_field(title, Value, E) -> E#event{title = Value};
|
set_field(title, Value, E) -> E#event{title = Value};
|
||||||
set_field(description, Value, E) -> E#event{description = Value};
|
set_field(description, Value, E) -> E#event{description = Value};
|
||||||
set_field(start_time, Value, E) -> E#event{start_time = Value};
|
set_field(start_time, Value, E) -> E#event{start_time = Value};
|
||||||
@@ -218,4 +309,11 @@ set_field(status, Value, E) -> E#event{status = Value};
|
|||||||
set_field(reason, Value, E) -> E#event{reason = Value};
|
set_field(reason, Value, E) -> E#event{reason = Value};
|
||||||
set_field(rating_avg, Value, E) -> E#event{rating_avg = Value};
|
set_field(rating_avg, Value, E) -> E#event{rating_avg = Value};
|
||||||
set_field(rating_count, Value, E) -> E#event{rating_count = Value};
|
set_field(rating_count, Value, E) -> E#event{rating_count = Value};
|
||||||
|
set_field(attachments, Value, E) -> E#event{attachments = Value};
|
||||||
|
set_field(edit_history, Value, E) -> E#event{edit_history = Value};
|
||||||
|
set_field(recurrence_rule, Value, E) -> E#event{recurrence_rule = Value};
|
||||||
|
set_field(master_id, Value, E) -> E#event{master_id = Value};
|
||||||
|
set_field(is_instance, Value, E) -> E#event{is_instance = Value};
|
||||||
|
set_field(event_type, Value, E) -> E#event{event_type = Value};
|
||||||
|
set_field(calendar_id, Value, E) -> E#event{calendar_id = Value};
|
||||||
set_field(_, _, E) -> E.
|
set_field(_, _, E) -> E.
|
||||||
+101
-92
@@ -1,16 +1,27 @@
|
|||||||
-module(core_report).
|
-module(core_report).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
-export([create/4, get_by_id/1, list_all/0, list_by_reporter/1, update_status/3, count_reports/0]).
|
||||||
|
-export([list_by_target/2, get_count_by_target/2]).
|
||||||
|
-export([count_reports_by_status/1, count_reports_resolved_by_admin/2, avg_resolution_time/1]).
|
||||||
|
|
||||||
-export([create/4, get_by_id/1, list_by_target/2, list_by_reporter/1, list_all/0]).
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
-export([update_status/3, get_count_by_target/2]).
|
%% Значения по умолчанию для необязательных полей
|
||||||
-export([generate_id/0]).
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
-export([count_reports_by_status/1, count_reports_by_admin/2]).
|
-define(DEFAULT_RESOLVED_AT, {{1970,1,1},{0,0,0}}).
|
||||||
-export([count_reports_resolved_by_admin/2, avg_resolution_time/1]).
|
-define(DEFAULT_RESOLVED_BY, <<>>).
|
||||||
-export([delete/1, update/2]). % <-- добавлено
|
|
||||||
|
|
||||||
%% Создание жалобы
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Создание жалобы.
|
||||||
|
%%% Статус устанавливается в `pending`, поля `resolved_at` и `resolved_by`
|
||||||
|
%%% инициализируются значениями по умолчанию, чтобы избежать `undefined`.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec create(ReporterId :: binary(), TargetType :: calendar | event | review,
|
||||||
|
TargetId :: binary(), Reason :: binary()) ->
|
||||||
|
{ok, #report{}} | {error, term()}.
|
||||||
create(ReporterId, TargetType, TargetId, Reason) ->
|
create(ReporterId, TargetType, TargetId, Reason) ->
|
||||||
Id = infra_utils:generate_id(16),
|
Id = infra_utils:generate_id(16),
|
||||||
|
Now = calendar:universal_time(),
|
||||||
Report = #report{
|
Report = #report{
|
||||||
id = Id,
|
id = Id,
|
||||||
reporter_id = ReporterId,
|
reporter_id = ReporterId,
|
||||||
@@ -18,133 +29,131 @@ create(ReporterId, TargetType, TargetId, Reason) ->
|
|||||||
target_id = TargetId,
|
target_id = TargetId,
|
||||||
reason = Reason,
|
reason = Reason,
|
||||||
status = pending,
|
status = pending,
|
||||||
created_at = calendar:universal_time(),
|
created_at = Now,
|
||||||
resolved_at = undefined,
|
resolved_at = ?DEFAULT_RESOLVED_AT,
|
||||||
resolved_by = undefined
|
resolved_by = ?DEFAULT_RESOLVED_BY
|
||||||
},
|
},
|
||||||
|
F = fun() -> mnesia:write(Report), {ok, Report} end,
|
||||||
F = fun() ->
|
|
||||||
mnesia:write(Report),
|
|
||||||
{ok, Report}
|
|
||||||
end,
|
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
case mnesia:transaction(F) of
|
||||||
{atomic, Result} -> Result;
|
{atomic, Result} -> Result;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получение жалобы по ID
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить жалобу по идентификатору.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_by_id(Id :: binary()) -> {ok, #report{}} | {error, not_found}.
|
||||||
get_by_id(Id) ->
|
get_by_id(Id) ->
|
||||||
case mnesia:dirty_read(report, Id) of
|
case mnesia:dirty_read(report, Id) of
|
||||||
[] -> {error, not_found};
|
[] -> {error, not_found};
|
||||||
[Report] -> {ok, Report}
|
[Report] -> {ok, Report}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Список жалоб на цель
|
%%%-------------------------------------------------------------------
|
||||||
list_by_target(TargetType, TargetId) ->
|
%%% @doc Список всех жалоб.
|
||||||
Match = #report{target_type = TargetType, target_id = TargetId, _ = '_'},
|
%%% @end
|
||||||
Reports = mnesia:dirty_match_object(Match),
|
%%%-------------------------------------------------------------------
|
||||||
{ok, Reports}.
|
-spec list_all() -> [#report{}].
|
||||||
|
list_all() ->
|
||||||
|
mnesia:dirty_match_object(#report{_ = '_'}).
|
||||||
|
|
||||||
%% Список жалоб от пользователя
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список жалоб, поданных определённым пользователем.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_by_reporter(ReporterId :: binary()) -> [#report{}].
|
||||||
list_by_reporter(ReporterId) ->
|
list_by_reporter(ReporterId) ->
|
||||||
Match = #report{reporter_id = ReporterId, _ = '_'},
|
Match = #report{reporter_id = ReporterId, _ = '_'},
|
||||||
Reports = mnesia:dirty_match_object(Match),
|
mnesia:dirty_match_object(Match).
|
||||||
{ok, Reports}.
|
|
||||||
|
|
||||||
%% Список всех жалоб (для админов)
|
%%%-------------------------------------------------------------------
|
||||||
list_all() ->
|
%%% @doc Список жалоб на конкретную цель.
|
||||||
Match = #report{_ = '_'},
|
%%% @end
|
||||||
Reports = mnesia:dirty_match_object(Match),
|
%%%-------------------------------------------------------------------
|
||||||
{ok, Reports}.
|
-spec list_by_target(TargetType :: calendar | event | review, TargetId :: binary()) ->
|
||||||
|
[#report{}].
|
||||||
|
list_by_target(TargetType, TargetId) ->
|
||||||
|
Match = #report{target_type = TargetType, target_id = TargetId, _ = '_'},
|
||||||
|
mnesia:dirty_match_object(Match).
|
||||||
|
|
||||||
%% Обновление статуса жалобы
|
%%%-------------------------------------------------------------------
|
||||||
update_status(Id, Status, ResolvedBy) when Status =:= reviewed; Status =:= dismissed ->
|
%%% @doc Количество жалоб на конкретную цель.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_count_by_target(TargetType :: calendar | event | review, TargetId :: binary()) ->
|
||||||
|
non_neg_integer().
|
||||||
|
get_count_by_target(TargetType, TargetId) ->
|
||||||
|
length(list_by_target(TargetType, TargetId)).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Обновить статус жалобы.
|
||||||
|
%%% При переводе в `reviewed` или `dismissed` заполняются `resolved_by` и
|
||||||
|
%%% `resolved_at` текущим временем.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update_status(Id :: binary(), NewStatus :: reviewed | dismissed, AdminId :: binary()) ->
|
||||||
|
{ok, #report{}} | {error, not_found | term()}.
|
||||||
|
update_status(Id, NewStatus, AdminId) ->
|
||||||
F = fun() ->
|
F = fun() ->
|
||||||
case mnesia:read(report, Id) of
|
case mnesia:read(report, Id) of
|
||||||
[] ->
|
[] -> {error, not_found};
|
||||||
{error, not_found};
|
|
||||||
[Report] ->
|
[Report] ->
|
||||||
Updated = Report#report{
|
Updated = Report#report{
|
||||||
status = Status,
|
status = NewStatus,
|
||||||
resolved_at = calendar:universal_time(),
|
resolved_by = AdminId,
|
||||||
resolved_by = ResolvedBy
|
resolved_at = calendar:universal_time()
|
||||||
},
|
},
|
||||||
mnesia:write(Updated),
|
mnesia:write(Updated),
|
||||||
{ok, Updated}
|
{ok, Updated}
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
case mnesia:transaction(F) of
|
||||||
{atomic, Result} -> Result;
|
{atomic, Result} -> Result;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получить количество жалоб на цель
|
%%%-------------------------------------------------------------------
|
||||||
get_count_by_target(TargetType, TargetId) ->
|
%%% @doc Количество жалоб.
|
||||||
Match = #report{target_type = TargetType, target_id = TargetId, status = pending, _ = '_'},
|
%%% @end
|
||||||
Reports = mnesia:dirty_match_object(Match),
|
%%%-------------------------------------------------------------------
|
||||||
length(Reports).
|
-spec count_reports() -> non_neg_integer().
|
||||||
|
count_reports() ->
|
||||||
|
mnesia:table_info(report, size).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Количество жалоб с заданным статусом.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_reports_by_status(Status :: pending | reviewed | dismissed) ->
|
||||||
|
non_neg_integer().
|
||||||
count_reports_by_status(Status) ->
|
count_reports_by_status(Status) ->
|
||||||
Match = #report{status = Status, _ = '_'},
|
Match = #report{status = Status, _ = '_'},
|
||||||
length(mnesia:dirty_match_object(Match)).
|
length(mnesia:dirty_match_object(Match)).
|
||||||
|
|
||||||
count_reports_by_admin(AdminId, Status) ->
|
%%%-------------------------------------------------------------------
|
||||||
Match = #report{resolved_by = AdminId, status = Status, _ = '_'},
|
%%% @doc Количество жалоб, разрешённых конкретным администратором.
|
||||||
length(mnesia:dirty_match_object(Match)).
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_reports_resolved_by_admin(AdminId :: binary(), Status :: reviewed | dismissed) ->
|
||||||
|
non_neg_integer().
|
||||||
count_reports_resolved_by_admin(AdminId, Status) ->
|
count_reports_resolved_by_admin(AdminId, Status) ->
|
||||||
Match = #report{resolved_by = AdminId, status = Status, _ = '_'},
|
Match = #report{resolved_by = AdminId, status = Status, _ = '_'},
|
||||||
length(mnesia:dirty_match_object(Match)).
|
length(mnesia:dirty_match_object(Match)).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Среднее время разрешения жалоб заданного статуса (в часах).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec avg_resolution_time(Status :: reviewed | dismissed) -> float().
|
||||||
avg_resolution_time(Status) ->
|
avg_resolution_time(Status) ->
|
||||||
Match = #report{status = Status, _ = '_'},
|
Match = #report{status = Status, _ = '_'},
|
||||||
Reports = mnesia:dirty_match_object(Match),
|
Reports = mnesia:dirty_match_object(Match),
|
||||||
Resolved = lists:filter(fun(R) -> R#report.resolved_at =/= undefined end, Reports),
|
case Reports of
|
||||||
case Resolved of
|
[] -> 0.0;
|
||||||
[] -> 0;
|
|
||||||
_ ->
|
_ ->
|
||||||
TotalSeconds = lists:sum([calendar:datetime_to_gregorian_seconds(R#report.resolved_at) -
|
Total = lists:sum([calendar:datetime_to_gregorian_seconds(R#report.resolved_at) -
|
||||||
calendar:datetime_to_gregorian_seconds(R#report.created_at) || R <- Resolved]),
|
calendar:datetime_to_gregorian_seconds(R#report.created_at)
|
||||||
TotalSeconds / length(Resolved) / 3600.0
|
|| R <- Reports, R#report.resolved_at /= undefined]),
|
||||||
|
Total / length(Reports) / 3600.0
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Мягкое удаление жалобы (просто физически удаляем запись)
|
|
||||||
-spec delete(binary()) -> {ok, deleted} | {error, not_found}.
|
|
||||||
delete(Id) ->
|
|
||||||
case get_by_id(Id) of
|
|
||||||
{ok, _} ->
|
|
||||||
mnesia:dirty_delete(report, Id),
|
|
||||||
{ok, deleted};
|
|
||||||
Error -> Error
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Обновление произвольных полей жалобы (для административных целей)
|
|
||||||
-spec update(binary(), proplists:proplist()) -> {ok, #report{}} | {error, not_found}.
|
|
||||||
update(Id, Updates) ->
|
|
||||||
case get_by_id(Id) of
|
|
||||||
{ok, Report} ->
|
|
||||||
UpdatedReport = apply_updates(Report, Updates),
|
|
||||||
mnesia:dirty_write(UpdatedReport),
|
|
||||||
{ok, UpdatedReport};
|
|
||||||
Error -> Error
|
|
||||||
end.
|
|
||||||
|
|
||||||
%%%===================================================================
|
|
||||||
%%% Внутренние функции
|
|
||||||
%%%===================================================================
|
|
||||||
|
|
||||||
generate_id() ->
|
|
||||||
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
|
||||||
|
|
||||||
apply_updates(Report, []) -> Report;
|
|
||||||
apply_updates(Report, [{Field, Value} | Rest]) ->
|
|
||||||
NewReport = case Field of
|
|
||||||
status -> Report#report{status = Value};
|
|
||||||
resolved_at -> Report#report{resolved_at = Value};
|
|
||||||
resolved_by -> Report#report{resolved_by = Value};
|
|
||||||
reason -> Report#report{reason = Value};
|
|
||||||
_ -> Report
|
|
||||||
end,
|
|
||||||
apply_updates(NewReport, Rest).
|
|
||||||
+131
-43
@@ -1,14 +1,27 @@
|
|||||||
-module(core_review).
|
-module(core_review).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
-export([create/5, get_by_id/1, list_by_target/2, list_by_user/1, update/2, delete/1,
|
||||||
-export([create/5, get_by_id/1, list_by_target/2, list_by_user/1,
|
hide/2, unhide/2]).
|
||||||
update/2, delete/1, hide/2, unhide/2]).
|
|
||||||
-export([get_average_rating/2, has_user_reviewed/3]).
|
-export([get_average_rating/2, has_user_reviewed/3]).
|
||||||
-export([count_reviews/0, list_all/0]).
|
-export([count_reviews/0, list_all/0]).
|
||||||
|
|
||||||
%% Создание отзыва
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
|
%% Значения по умолчанию для необязательных полей
|
||||||
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
|
-define(DEFAULT_REASON, <<>>).
|
||||||
|
-define(DEFAULT_LIKES, 0).
|
||||||
|
-define(DEFAULT_DISLIKES, 0).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Создание отзыва.
|
||||||
|
%%% Все необязательные поля инициализируются значениями по умолчанию.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec create(UserId :: binary(), TargetType :: calendar | event, TargetId :: binary(),
|
||||||
|
Rating :: 1..5, Comment :: binary()) -> {ok, #review{}} | {error, term()}.
|
||||||
create(UserId, TargetType, TargetId, Rating, Comment) ->
|
create(UserId, TargetType, TargetId, Rating, Comment) ->
|
||||||
Id = infra_utils:generate_id(16),
|
Id = infra_utils:generate_id(16),
|
||||||
|
Now = calendar:universal_time(),
|
||||||
Review = #review{
|
Review = #review{
|
||||||
id = Id,
|
id = Id,
|
||||||
user_id = UserId,
|
user_id = UserId,
|
||||||
@@ -17,86 +30,121 @@ create(UserId, TargetType, TargetId, Rating, Comment) ->
|
|||||||
rating = Rating,
|
rating = Rating,
|
||||||
comment = Comment,
|
comment = Comment,
|
||||||
status = visible,
|
status = visible,
|
||||||
created_at = calendar:universal_time(),
|
reason = ?DEFAULT_REASON,
|
||||||
updated_at = calendar:universal_time()
|
likes = ?DEFAULT_LIKES,
|
||||||
|
dislikes = ?DEFAULT_DISLIKES,
|
||||||
|
created_at = Now,
|
||||||
|
updated_at = Now
|
||||||
},
|
},
|
||||||
|
F = fun() -> mnesia:write(Review), {ok, Review} end,
|
||||||
F = fun() ->
|
|
||||||
mnesia:write(Review),
|
|
||||||
{ok, Review}
|
|
||||||
end,
|
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
case mnesia:transaction(F) of
|
||||||
{atomic, Result} -> Result;
|
{atomic, Result} -> Result;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получение отзыва по ID
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получение отзыва по идентификатору.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_by_id(Id :: binary()) -> {ok, #review{}} | {error, not_found}.
|
||||||
get_by_id(Id) ->
|
get_by_id(Id) ->
|
||||||
case mnesia:dirty_read(review, Id) of
|
case mnesia:dirty_read(review, Id) of
|
||||||
[] -> {error, not_found};
|
[] -> {error, not_found};
|
||||||
[Review] -> {ok, Review}
|
[Review] -> {ok, Review}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Список отзывов для цели (событие или календарь)
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список видимых отзывов для заданной цели.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_by_target(TargetType :: calendar | event, TargetId :: binary()) ->
|
||||||
|
{ok, [#review{}]}.
|
||||||
list_by_target(TargetType, TargetId) ->
|
list_by_target(TargetType, TargetId) ->
|
||||||
Match = #review{target_type = TargetType, target_id = TargetId, status = visible, _ = '_'},
|
Match = #review{target_type = TargetType, target_id = TargetId,
|
||||||
|
status = visible, _ = '_'},
|
||||||
Reviews = mnesia:dirty_match_object(Match),
|
Reviews = mnesia:dirty_match_object(Match),
|
||||||
{ok, lists:sort(fun(A, B) -> A#review.created_at >= B#review.created_at end, Reviews)}.
|
{ok, lists:sort(fun(A, B) -> A#review.created_at >= B#review.created_at end,
|
||||||
|
Reviews)}.
|
||||||
|
|
||||||
%% Список отзывов пользователя
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список отзывов пользователя.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_by_user(UserId :: binary()) -> {ok, [#review{}]}.
|
||||||
list_by_user(UserId) ->
|
list_by_user(UserId) ->
|
||||||
Match = #review{user_id = UserId, _ = '_'},
|
Match = #review{user_id = UserId, _ = '_'},
|
||||||
Reviews = mnesia:dirty_match_object(Match),
|
Reviews = mnesia:dirty_match_object(Match),
|
||||||
{ok, Reviews}.
|
{ok, Reviews}.
|
||||||
|
|
||||||
%% Обновление отзыва
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Обновление отзыва.
|
||||||
|
%%% `Updates` – список пар `[{atom(), term()}]`.
|
||||||
|
%%% Поля, не указанные в обновлении, сохраняют прежние значения.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update(Id :: binary(), Updates :: [{atom(), term()}]) ->
|
||||||
|
{ok, #review{}} | {error, not_found | term()}.
|
||||||
update(Id, Updates) ->
|
update(Id, Updates) ->
|
||||||
F = fun() ->
|
F = fun() ->
|
||||||
case mnesia:read(review, Id) of
|
case mnesia:read(review, Id) of
|
||||||
[] ->
|
[] -> {error, not_found};
|
||||||
{error, not_found};
|
|
||||||
[Review] ->
|
[Review] ->
|
||||||
UpdatedReview = apply_updates(Review, Updates),
|
UpdatedReview = apply_updates(Review, Updates),
|
||||||
mnesia:write(UpdatedReview),
|
mnesia:write(UpdatedReview),
|
||||||
{ok, UpdatedReview}
|
{ok, UpdatedReview}
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
case mnesia:transaction(F) of
|
||||||
{atomic, Result} -> Result;
|
{atomic, Result} -> Result;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Удаление отзыва (hard delete)
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Удаление отзыва (физическое).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec delete(Id :: binary()) -> {ok, deleted} | {error, not_found | term()}.
|
||||||
delete(Id) ->
|
delete(Id) ->
|
||||||
F = fun() ->
|
F = fun() ->
|
||||||
case mnesia:read(review, Id) of
|
case mnesia:read(review, Id) of
|
||||||
[] ->
|
[] -> {error, not_found};
|
||||||
{error, not_found};
|
[Review] -> mnesia:delete_object(Review), {ok, deleted}
|
||||||
[Review] ->
|
|
||||||
mnesia:delete_object(Review),
|
|
||||||
{ok, deleted}
|
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
case mnesia:transaction(F) of
|
||||||
{atomic, Result} -> Result;
|
{atomic, Result} -> Result;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Скрытие отзыва (модерация)
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Скрытие отзыва с указанием причины.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec hide(Id :: binary(), Reason :: binary()) ->
|
||||||
|
{ok, #review{}} | {error, not_found | term()}.
|
||||||
hide(Id, Reason) ->
|
hide(Id, Reason) ->
|
||||||
update(Id, [{status, hidden}, {reason, Reason}]).
|
update(Id, [{status, hidden}, {reason, Reason}]).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Восстановление видимости отзыва с указанием причины.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec unhide(Id :: binary(), Reason :: binary()) ->
|
||||||
|
{ok, #review{}} | {error, not_found | term()}.
|
||||||
unhide(Id, Reason) ->
|
unhide(Id, Reason) ->
|
||||||
update(Id, [{status, visible}, {reason, Reason}]).
|
update(Id, [{status, visible}, {reason, Reason}]).
|
||||||
|
|
||||||
%% Получение среднего рейтинга цели
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Вычисление среднего рейтинга цели.
|
||||||
|
%%% Возвращает `{Average, Count}`.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_average_rating(TargetType :: calendar | event, TargetId :: binary()) ->
|
||||||
|
{Average :: float(), Count :: non_neg_integer()}.
|
||||||
get_average_rating(TargetType, TargetId) ->
|
get_average_rating(TargetType, TargetId) ->
|
||||||
Match = #review{target_type = TargetType, target_id = TargetId, status = visible, _ = '_'},
|
Match = #review{target_type = TargetType, target_id = TargetId,
|
||||||
|
status = visible, _ = '_'},
|
||||||
Reviews = mnesia:dirty_match_object(Match),
|
Reviews = mnesia:dirty_match_object(Match),
|
||||||
|
|
||||||
case length(Reviews) of
|
case length(Reviews) of
|
||||||
0 -> {0.0, 0};
|
0 -> {0.0, 0};
|
||||||
Count ->
|
Count ->
|
||||||
@@ -104,26 +152,66 @@ get_average_rating(TargetType, TargetId) ->
|
|||||||
{Total / Count, Count}
|
{Total / Count, Count}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Проверка, оставлял ли пользователь отзыв
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Проверить, оставлял ли пользователь отзыв на цель.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec has_user_reviewed(UserId :: binary(), TargetType :: calendar | event,
|
||||||
|
TargetId :: binary()) -> boolean().
|
||||||
has_user_reviewed(UserId, TargetType, TargetId) ->
|
has_user_reviewed(UserId, TargetType, TargetId) ->
|
||||||
Match = #review{user_id = UserId, target_type = TargetType, target_id = TargetId, _ = '_'},
|
Match = #review{user_id = UserId, target_type = TargetType,
|
||||||
|
target_id = TargetId, _ = '_'},
|
||||||
case mnesia:dirty_match_object(Match) of
|
case mnesia:dirty_match_object(Match) of
|
||||||
[] -> false;
|
[] -> false;
|
||||||
_ -> true
|
_ -> true
|
||||||
end.
|
end.
|
||||||
|
|
||||||
count_reviews() -> mnesia:table_info(review, size).
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Количество отзывов в таблице.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_reviews() -> non_neg_integer().
|
||||||
|
count_reviews() ->
|
||||||
|
mnesia:table_info(review, size).
|
||||||
|
|
||||||
list_all() -> mnesia:dirty_match_object(#review{_ = '_'}).
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить список всех отзывов.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_all() -> [#review{}].
|
||||||
|
list_all() ->
|
||||||
|
mnesia:dirty_match_object(#review{_ = '_'}).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Применить список обновлений к записи отзыва.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec apply_updates(#review{}, [{atom(), term()}]) -> #review{}.
|
||||||
apply_updates(Review, Updates) ->
|
apply_updates(Review, Updates) ->
|
||||||
Updated = lists:foldl(fun({Field, Value}, R) ->
|
Updated = lists:foldl(fun({Field, Value}, R) -> set_field(Field, Value, R) end,
|
||||||
set_field(Field, Value, R)
|
Review, Updates),
|
||||||
end, Review, Updates),
|
|
||||||
Updated#review{updated_at = calendar:universal_time()}.
|
Updated#review{updated_at = calendar:universal_time()}.
|
||||||
|
|
||||||
set_field(rating, Value, R) when Value >= 1, Value =< 5 -> R#review{rating = Value};
|
%%%-------------------------------------------------------------------
|
||||||
set_field(comment, Value, R) -> R#review{comment = Value};
|
%%% @doc Установить одно поле записи отзыва.
|
||||||
set_field(status, Value, R) when Value =:= visible; Value =:= hidden -> R#review{status = Value};
|
%%% Неизвестные поля игнорируются.
|
||||||
set_field(reason, Value, R) -> R#review{reason = Value};
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec set_field(atom(), term(), #review{}) -> #review{}.
|
||||||
|
set_field(rating, Value, R) when Value >= 1, Value =< 5 ->
|
||||||
|
R#review{rating = Value};
|
||||||
|
set_field(comment, Value, R) ->
|
||||||
|
R#review{comment = Value};
|
||||||
|
set_field(status, Value, R) when Value =:= visible; Value =:= hidden ->
|
||||||
|
R#review{status = Value};
|
||||||
|
set_field(reason, Value, R) ->
|
||||||
|
R#review{reason = Value};
|
||||||
|
set_field(likes, Value, R) ->
|
||||||
|
R#review{likes = Value};
|
||||||
|
set_field(dislikes, Value, R) ->
|
||||||
|
R#review{dislikes = Value};
|
||||||
set_field(_, _, R) -> R.
|
set_field(_, _, R) -> R.
|
||||||
@@ -1,20 +1,42 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Управление сессиями пользователей (refresh-токены).
|
||||||
|
%%%
|
||||||
|
%%% Запись `session` хранит refresh-токен, идентификатор пользователя,
|
||||||
|
%%% время истечения и тип токена. Все поля обязательны и инициализируются
|
||||||
|
%%% при создании, поэтому `undefined` в базе не возникает.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
-module(core_session).
|
-module(core_session).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
-export([create/2, validate/1, delete/1]).
|
-export([create/2, validate/1, delete/1]).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Создать новую сессию.
|
||||||
|
%%% Токен действителен 30 дней с момента создания.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
-spec create(UserId :: binary(), RefreshToken :: binary()) -> ok.
|
-spec create(UserId :: binary(), RefreshToken :: binary()) -> ok.
|
||||||
create(UserId, RefreshToken) ->
|
create(UserId, RefreshToken) ->
|
||||||
Session = #session{
|
Session = #session{
|
||||||
token = RefreshToken,
|
token = RefreshToken,
|
||||||
user_id = UserId,
|
user_id = UserId,
|
||||||
expires_at = calendar:gregorian_seconds_to_datetime(
|
expires_at = calendar:gregorian_seconds_to_datetime(
|
||||||
calendar:datetime_to_gregorian_seconds(calendar:universal_time()) + 30 * 24 * 3600),
|
calendar:datetime_to_gregorian_seconds(
|
||||||
|
calendar:universal_time()) + 30 * 24 * 3600),
|
||||||
type = refresh
|
type = refresh
|
||||||
},
|
},
|
||||||
mnesia:dirty_write(Session),
|
mnesia:dirty_write(Session),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
-spec validate(Token :: binary()) -> {ok, UserId :: binary(), User :: #user{}} | {error, not_found | expired}.
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Проверить refresh-токен.
|
||||||
|
%%% Если токен действителен, возвращает `{ok, UserId, User}`.
|
||||||
|
%%% Если истёк — удаляет запись и возвращает `{error, expired}`.
|
||||||
|
%%% Если не найден — `{error, not_found}`.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec validate(Token :: binary()) ->
|
||||||
|
{ok, UserId :: binary(), User :: #user{}} | {error, not_found | expired}.
|
||||||
validate(Token) ->
|
validate(Token) ->
|
||||||
case mnesia:dirty_read({session, Token}) of
|
case mnesia:dirty_read({session, Token}) of
|
||||||
[Session] ->
|
[Session] ->
|
||||||
@@ -22,19 +44,20 @@ validate(Token) ->
|
|||||||
true ->
|
true ->
|
||||||
% Получаем пользователя по ID из сессии
|
% Получаем пользователя по ID из сессии
|
||||||
case mnesia:dirty_read({user, Session#session.user_id}) of
|
case mnesia:dirty_read({user, Session#session.user_id}) of
|
||||||
[User] ->
|
[User] -> {ok, Session#session.user_id, User};
|
||||||
{ok, Session#session.user_id, User};
|
[] -> {error, not_found}
|
||||||
[] ->
|
|
||||||
{error, not_found}
|
|
||||||
end;
|
end;
|
||||||
false ->
|
false ->
|
||||||
mnesia:dirty_delete({session, Token}),
|
mnesia:dirty_delete({session, Token}),
|
||||||
{error, expired}
|
{error, expired}
|
||||||
end;
|
end;
|
||||||
[] ->
|
[] -> {error, not_found}
|
||||||
{error, not_found}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Удалить сессию (отзыв refresh-токена).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
-spec delete(Token :: binary()) -> ok.
|
-spec delete(Token :: binary()) -> ok.
|
||||||
delete(Token) ->
|
delete(Token) ->
|
||||||
mnesia:dirty_delete({session, Token}),
|
mnesia:dirty_delete({session, Token}),
|
||||||
|
|||||||
+105
-51
@@ -1,35 +1,32 @@
|
|||||||
-module(core_subscription).
|
-module(core_subscription).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
-export([create/3, get_by_id/1, get_active_by_user/1, list_by_user/1, list_all/0]).
|
-export([create/3, get_by_id/1, get_active_by_user/1, list_by_user/1, list_all/0]).
|
||||||
-export([update_status/2, check_expired/0]).
|
-export([update_status/2, check_expired/0]).
|
||||||
% --------------- новые обёртки для админки ------------------
|
-export([list_subscriptions/0, create_subscription/1, update_subscription/2, delete_subscription/1]).
|
||||||
-export([list_subscriptions/0,
|
|
||||||
create_subscription/1,
|
|
||||||
update_subscription/2,
|
|
||||||
delete_subscription/1
|
|
||||||
]).
|
|
||||||
-export([count_subscription/0]).
|
-export([count_subscription/0]).
|
||||||
|
|
||||||
-define(TRIAL_DAYS, 30).
|
-define(TRIAL_DAYS, 30).
|
||||||
|
|
||||||
%% Создание подписки
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Создание подписки.
|
||||||
|
%%% `TrialUsed` – `true`, если подписка платная; `false` для пробного периода.
|
||||||
|
%%% Все поля записи инициализированы, `undefined` не возникает.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec create(UserId :: binary(), Plan :: monthly | quarterly | biannual | annual,
|
||||||
|
TrialUsed :: boolean()) -> {ok, #subscription{}} | {error, term()}.
|
||||||
create(UserId, Plan, TrialUsed) ->
|
create(UserId, Plan, TrialUsed) ->
|
||||||
Id = infra_utils:generate_id(16),
|
Id = infra_utils:generate_id(16),
|
||||||
Now = calendar:universal_time(),
|
Now = calendar:universal_time(),
|
||||||
|
|
||||||
{StartDate, EndDate} = case TrialUsed of
|
{StartDate, EndDate} = case TrialUsed of
|
||||||
true ->
|
true ->
|
||||||
% Платная подписка
|
|
||||||
DurationMonths = plan_to_months(Plan),
|
DurationMonths = plan_to_months(Plan),
|
||||||
End = add_months(Now, DurationMonths),
|
End = add_months(Now, DurationMonths),
|
||||||
{Now, End};
|
{Now, End};
|
||||||
false ->
|
false ->
|
||||||
% Пробный период
|
|
||||||
End = add_days(Now, ?TRIAL_DAYS),
|
End = add_days(Now, ?TRIAL_DAYS),
|
||||||
{Now, End}
|
{Now, End}
|
||||||
end,
|
end,
|
||||||
|
|
||||||
Subscription = #subscription{
|
Subscription = #subscription{
|
||||||
id = Id,
|
id = Id,
|
||||||
user_id = UserId,
|
user_id = UserId,
|
||||||
@@ -41,25 +38,28 @@ create(UserId, Plan, TrialUsed) ->
|
|||||||
created_at = Now,
|
created_at = Now,
|
||||||
updated_at = Now
|
updated_at = Now
|
||||||
},
|
},
|
||||||
|
F = fun() -> mnesia:write(Subscription), {ok, Subscription} end,
|
||||||
F = fun() ->
|
|
||||||
mnesia:write(Subscription),
|
|
||||||
{ok, Subscription}
|
|
||||||
end,
|
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
case mnesia:transaction(F) of
|
||||||
{atomic, Result} -> Result;
|
{atomic, Result} -> Result;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получение подписки по ID
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить подписку по идентификатору.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_by_id(Id :: binary()) -> {ok, #subscription{}} | {error, not_found}.
|
||||||
get_by_id(Id) ->
|
get_by_id(Id) ->
|
||||||
case mnesia:dirty_read(subscription, Id) of
|
case mnesia:dirty_read(subscription, Id) of
|
||||||
[] -> {error, not_found};
|
[] -> {error, not_found};
|
||||||
[Subscription] -> {ok, Subscription}
|
[Subscription] -> {ok, Subscription}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получение активной подписки пользователя
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить активную подписку пользователя.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_active_by_user(UserId :: binary()) -> {ok, #subscription{}} | {error, not_found}.
|
||||||
get_active_by_user(UserId) ->
|
get_active_by_user(UserId) ->
|
||||||
Match = #subscription{user_id = UserId, status = active, _ = '_'},
|
Match = #subscription{user_id = UserId, status = active, _ = '_'},
|
||||||
case mnesia:dirty_match_object(Match) of
|
case mnesia:dirty_match_object(Match) of
|
||||||
@@ -67,24 +67,37 @@ get_active_by_user(UserId) ->
|
|||||||
[Subscription] -> {ok, Subscription}
|
[Subscription] -> {ok, Subscription}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Список всех подписок пользователя
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список всех подписок пользователя.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_by_user(UserId :: binary()) -> {ok, [#subscription{}]}.
|
||||||
list_by_user(UserId) ->
|
list_by_user(UserId) ->
|
||||||
Match = #subscription{user_id = UserId, _ = '_'},
|
Match = #subscription{user_id = UserId, _ = '_'},
|
||||||
Subscriptions = mnesia:dirty_match_object(Match),
|
Subscriptions = mnesia:dirty_match_object(Match),
|
||||||
{ok, lists:sort(fun(A, B) -> A#subscription.created_at >= B#subscription.created_at end, Subscriptions)}.
|
{ok, lists:sort(fun(A, B) -> A#subscription.created_at >= B#subscription.created_at end,
|
||||||
|
Subscriptions)}.
|
||||||
|
|
||||||
%% Список всех подписок (для админов)
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список всех подписок (для администраторов).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_all() -> {ok, [#subscription{}]}.
|
||||||
list_all() ->
|
list_all() ->
|
||||||
Match = #subscription{_ = '_'},
|
Match = #subscription{_ = '_'},
|
||||||
Subscriptions = mnesia:dirty_match_object(Match),
|
Subscriptions = mnesia:dirty_match_object(Match),
|
||||||
{ok, Subscriptions}.
|
{ok, Subscriptions}.
|
||||||
|
|
||||||
%% Обновление статуса подписки
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Обновить статус подписки.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update_status(Id :: binary(), Status :: active | expired | cancelled) ->
|
||||||
|
{ok, #subscription{}} | {error, not_found | term()}.
|
||||||
update_status(Id, Status) when Status =:= active; Status =:= expired; Status =:= cancelled ->
|
update_status(Id, Status) when Status =:= active; Status =:= expired; Status =:= cancelled ->
|
||||||
F = fun() ->
|
F = fun() ->
|
||||||
case mnesia:read(subscription, Id) of
|
case mnesia:read(subscription, Id) of
|
||||||
[] ->
|
[] -> {error, not_found};
|
||||||
{error, not_found};
|
|
||||||
[Subscription] ->
|
[Subscription] ->
|
||||||
Updated = Subscription#subscription{
|
Updated = Subscription#subscription{
|
||||||
status = Status,
|
status = Status,
|
||||||
@@ -94,23 +107,24 @@ update_status(Id, Status) when Status =:= active; Status =:= expired; Status =:=
|
|||||||
{ok, Updated}
|
{ok, Updated}
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
case mnesia:transaction(F) of
|
||||||
{atomic, Result} -> Result;
|
{atomic, Result} -> Result;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Проверка истёкших подписок (вызывается периодически)
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Проверка истёкших подписок (вызывается периодически).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec check_expired() -> ok.
|
||||||
check_expired() ->
|
check_expired() ->
|
||||||
Now = calendar:universal_time(),
|
Now = calendar:universal_time(),
|
||||||
Match = #subscription{status = active, _ = '_'},
|
Match = #subscription{status = active, _ = '_'},
|
||||||
ActiveSubscriptions = mnesia:dirty_match_object(Match),
|
ActiveSubscriptions = mnesia:dirty_match_object(Match),
|
||||||
|
|
||||||
lists:foreach(fun(Sub) ->
|
lists:foreach(fun(Sub) ->
|
||||||
case Sub#subscription.expires_at < Now of
|
case Sub#subscription.expires_at < Now of
|
||||||
true ->
|
true ->
|
||||||
update_status(Sub#subscription.id, expired),
|
update_status(Sub#subscription.id, expired),
|
||||||
% Если у пользователя нет другой активной подписки, понижаем календари
|
|
||||||
case get_active_by_user(Sub#subscription.user_id) of
|
case get_active_by_user(Sub#subscription.user_id) of
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
downgrade_user_calendars(Sub#subscription.user_id);
|
downgrade_user_calendars(Sub#subscription.user_id);
|
||||||
@@ -120,7 +134,11 @@ check_expired() ->
|
|||||||
end
|
end
|
||||||
end, ActiveSubscriptions).
|
end, ActiveSubscriptions).
|
||||||
|
|
||||||
%% Понижение календарей пользователя до personal при истечении подписки
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Понижение календарей пользователя до personal при истечении подписки.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec downgrade_user_calendars(UserId :: binary()) -> ok.
|
||||||
downgrade_user_calendars(UserId) ->
|
downgrade_user_calendars(UserId) ->
|
||||||
Match = #calendar{owner_id = UserId, type = commercial, _ = '_'},
|
Match = #calendar{owner_id = UserId, type = commercial, _ = '_'},
|
||||||
Calendars = mnesia:dirty_match_object(Match),
|
Calendars = mnesia:dirty_match_object(Match),
|
||||||
@@ -128,30 +146,47 @@ downgrade_user_calendars(UserId) ->
|
|||||||
core_calendar:update(Cal#calendar.id, [{type, personal}])
|
core_calendar:update(Cal#calendar.id, [{type, personal}])
|
||||||
end, Calendars).
|
end, Calendars).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% Вспомогательные функции
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec plan_to_months(monthly | quarterly | biannual | annual | trial) -> pos_integer().
|
||||||
plan_to_months(monthly) -> 1;
|
plan_to_months(monthly) -> 1;
|
||||||
plan_to_months(quarterly) -> 3;
|
plan_to_months(quarterly) -> 3;
|
||||||
plan_to_months(biannual) -> 6;
|
plan_to_months(biannual) -> 6;
|
||||||
plan_to_months(annual) -> 12;
|
plan_to_months(annual) -> 12;
|
||||||
plan_to_months(trial) -> 1. % пробный период на 1 месяц
|
plan_to_months(trial) -> 1.
|
||||||
|
|
||||||
|
-spec add_months(calendar:datetime(), pos_integer()) -> calendar:datetime().
|
||||||
add_months(DateTime, Months) ->
|
add_months(DateTime, Months) ->
|
||||||
Seconds = calendar:datetime_to_gregorian_seconds(DateTime),
|
Seconds = calendar:datetime_to_gregorian_seconds(DateTime),
|
||||||
Days = Seconds div 86400,
|
Days = Seconds div 86400,
|
||||||
NewDays = Days + (Months * 30),
|
NewDays = Days + (Months * 30),
|
||||||
calendar:gregorian_seconds_to_datetime(NewDays * 86400).
|
calendar:gregorian_seconds_to_datetime(NewDays * 86400).
|
||||||
|
|
||||||
|
-spec add_days(calendar:datetime(), pos_integer()) -> calendar:datetime().
|
||||||
add_days(DateTime, Days) ->
|
add_days(DateTime, Days) ->
|
||||||
Seconds = calendar:datetime_to_gregorian_seconds(DateTime),
|
Seconds = calendar:datetime_to_gregorian_seconds(DateTime),
|
||||||
calendar:gregorian_seconds_to_datetime(Seconds + (Days * 86400)).
|
calendar:gregorian_seconds_to_datetime(Seconds + (Days * 86400)).
|
||||||
|
|
||||||
% ================================================================
|
%%%===================================================================
|
||||||
% Новые обёртки для совместимости с admin_handler_subscriptions
|
%%% Новые обёртки для админки
|
||||||
% ================================================================
|
%%%===================================================================
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список подписок в виде списка записей.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_subscriptions() -> [#subscription{}].
|
||||||
list_subscriptions() ->
|
list_subscriptions() ->
|
||||||
{ok, Subs} = list_all(),
|
{ok, Subs} = list_all(),
|
||||||
Subs.
|
Subs.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Создание подписки из map-параметров.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec create_subscription(Data :: map()) -> {ok, #subscription{}} | {error, term()}.
|
||||||
create_subscription(Data) ->
|
create_subscription(Data) ->
|
||||||
UserId = maps:get(<<"user_id">>, Data),
|
UserId = maps:get(<<"user_id">>, Data),
|
||||||
Plan = case maps:get(<<"plan">>, Data, <<"monthly">>) of
|
Plan = case maps:get(<<"plan">>, Data, <<"monthly">>) of
|
||||||
@@ -165,6 +200,12 @@ create_subscription(Data) ->
|
|||||||
TrialUsed = maps:get(<<"trial_used">>, Data, false),
|
TrialUsed = maps:get(<<"trial_used">>, Data, false),
|
||||||
create(UserId, Plan, TrialUsed).
|
create(UserId, Plan, TrialUsed).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Обновление полей подписки (административная операция).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update_subscription(Id :: binary(), Updates :: map()) ->
|
||||||
|
{ok, #subscription{}} | {error, not_found}.
|
||||||
update_subscription(Id, Updates) ->
|
update_subscription(Id, Updates) ->
|
||||||
case get_by_id(Id) of
|
case get_by_id(Id) of
|
||||||
{ok, Sub} ->
|
{ok, Sub} ->
|
||||||
@@ -174,6 +215,11 @@ update_subscription(Id, Updates) ->
|
|||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Удаление подписки (физическое).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec delete_subscription(Id :: binary()) -> {ok, deleted} | {error, not_found}.
|
||||||
delete_subscription(Id) ->
|
delete_subscription(Id) ->
|
||||||
case get_by_id(Id) of
|
case get_by_id(Id) of
|
||||||
{ok, _Sub} ->
|
{ok, _Sub} ->
|
||||||
@@ -182,7 +228,11 @@ delete_subscription(Id) ->
|
|||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Применение обновлений к записи подписки
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Применение обновлений к записи подписки.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec apply_updates(#subscription{}, map()) -> #subscription{}.
|
||||||
apply_updates(Sub, Updates) ->
|
apply_updates(Sub, Updates) ->
|
||||||
lists:foldl(fun({Key, Value}, Acc) ->
|
lists:foldl(fun({Key, Value}, Acc) ->
|
||||||
case Key of
|
case Key of
|
||||||
@@ -193,8 +243,7 @@ apply_updates(Sub, Updates) ->
|
|||||||
<<"expired">> -> expired;
|
<<"expired">> -> expired;
|
||||||
Other -> Other
|
Other -> Other
|
||||||
end,
|
end,
|
||||||
Acc#subscription{status = NewStatus,
|
Acc#subscription{status = NewStatus, updated_at = calendar:universal_time()};
|
||||||
updated_at = calendar:universal_time()};
|
|
||||||
<<"plan">> ->
|
<<"plan">> ->
|
||||||
NewPlan = case Value of
|
NewPlan = case Value of
|
||||||
<<"monthly">> -> monthly;
|
<<"monthly">> -> monthly;
|
||||||
@@ -204,22 +253,22 @@ apply_updates(Sub, Updates) ->
|
|||||||
<<"annual">> -> annual;
|
<<"annual">> -> annual;
|
||||||
Other -> Other
|
Other -> Other
|
||||||
end,
|
end,
|
||||||
Acc#subscription{plan = NewPlan,
|
Acc#subscription{plan = NewPlan, updated_at = calendar:universal_time()};
|
||||||
updated_at = calendar:universal_time()};
|
|
||||||
<<"trial_used">> ->
|
<<"trial_used">> ->
|
||||||
Acc#subscription{trial_used = Value,
|
Acc#subscription{trial_used = Value, updated_at = calendar:universal_time()};
|
||||||
updated_at = calendar:universal_time()};
|
|
||||||
<<"expires_at">> ->
|
<<"expires_at">> ->
|
||||||
Acc#subscription{expires_at = parse_iso_datetime(Value),
|
Acc#subscription{expires_at = parse_iso_datetime(Value), updated_at = calendar:universal_time()};
|
||||||
updated_at = calendar:universal_time()};
|
|
||||||
<<"started_at">> ->
|
<<"started_at">> ->
|
||||||
Acc#subscription{started_at = parse_iso_datetime(Value),
|
Acc#subscription{started_at = parse_iso_datetime(Value), updated_at = calendar:universal_time()};
|
||||||
updated_at = calendar:universal_time()};
|
_ -> Acc
|
||||||
_ ->
|
|
||||||
Acc
|
|
||||||
end
|
end
|
||||||
end, Sub, maps:to_list(Updates)).
|
end, Sub, maps:to_list(Updates)).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Разбор ISO8601 строки в формат `calendar:datetime()`.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec parse_iso_datetime(binary() | term()) -> calendar:datetime() | term().
|
||||||
parse_iso_datetime(Bin) when is_binary(Bin) ->
|
parse_iso_datetime(Bin) when is_binary(Bin) ->
|
||||||
try
|
try
|
||||||
[DateStr, TimeStr] = string:split(Bin, "T"),
|
[DateStr, TimeStr] = string:split(Bin, "T"),
|
||||||
@@ -233,9 +282,14 @@ parse_iso_datetime(Bin) when is_binary(Bin) ->
|
|||||||
Minute = binary_to_integer(list_to_binary(MinuteStr)),
|
Minute = binary_to_integer(list_to_binary(MinuteStr)),
|
||||||
Second = binary_to_integer(list_to_binary(SecondStr)),
|
Second = binary_to_integer(list_to_binary(SecondStr)),
|
||||||
{{Year, Month, Day}, {Hour, Minute, Second}}
|
{{Year, Month, Day}, {Hour, Minute, Second}}
|
||||||
catch
|
catch _:_ -> Bin
|
||||||
_:_ -> Bin % если не получилось разобрать, оставляем как есть
|
|
||||||
end;
|
end;
|
||||||
parse_iso_datetime(Other) -> Other.
|
parse_iso_datetime(Other) -> Other.
|
||||||
|
|
||||||
count_subscription() -> mnesia:table_info(subscription, size).
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Количество подписок.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_subscription() -> non_neg_integer().
|
||||||
|
count_subscription() ->
|
||||||
|
mnesia:table_info(subscription, size).
|
||||||
+250
-120
@@ -1,138 +1,268 @@
|
|||||||
-module(core_ticket).
|
-module(core_ticket).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
-export([list_all/0,
|
-export([create/4, get_by_id/1, list_all/0, update/2, update_status/2, delete/1,
|
||||||
get_by_id/1,
|
assign/2, resolve/3, close/2]).
|
||||||
update_ticket/2,
|
-export([count_tickets/0]).
|
||||||
delete_ticket/1,
|
-export([create_ticket/1]).
|
||||||
stats/0,
|
-export([list_by_user/1]).
|
||||||
create_ticket/1,
|
-export([count_tickets_by_status/1, avg_resolution_time/0, count_tickets_by_admin/2]).
|
||||||
list_by_user/1]).
|
-export([update_ticket/2]).
|
||||||
-export([count_tickets_by_status/1, count_tickets_by_admin/2]).
|
-export([delete_ticket/1]). % ← для admin_handler_ticket_by_id
|
||||||
-export([avg_resolution_time/0]).
|
|
||||||
|
|
||||||
list_all() ->
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
mnesia:dirty_match_object(#ticket{_ = '_'}).
|
%% Значения по умолчанию для необязательных полей
|
||||||
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
|
-define(DEFAULT_CONTEXT, <<>>).
|
||||||
|
-define(DEFAULT_ASSIGNED_TO, <<>>).
|
||||||
|
-define(DEFAULT_RESOLUTION_NOTE, <<>>).
|
||||||
|
-define(DEFAULT_CLOSED_AT, {{1970,1,1},{0,0,0}}).
|
||||||
|
|
||||||
get_by_id(Id) ->
|
%%%-------------------------------------------------------------------
|
||||||
case mnesia:dirty_read({ticket, Id}) of
|
%%% @doc Создание тикета из map, переданного обработчиком.
|
||||||
[Ticket] -> {ok, Ticket};
|
%%% @end
|
||||||
[] -> {error, not_found}
|
%%%-------------------------------------------------------------------
|
||||||
end.
|
-spec create_ticket(Data :: map()) -> {ok, #ticket{}} | {error, term()}.
|
||||||
|
|
||||||
update_ticket(Id, Updates) ->
|
|
||||||
case get_by_id(Id) of
|
|
||||||
{ok, Ticket} ->
|
|
||||||
Updated = apply_updates(Ticket, Updates),
|
|
||||||
mnesia:dirty_write(Updated),
|
|
||||||
{ok, Updated};
|
|
||||||
Error -> Error
|
|
||||||
end.
|
|
||||||
|
|
||||||
delete_ticket(Id) ->
|
|
||||||
case get_by_id(Id) of
|
|
||||||
{ok, _Ticket} ->
|
|
||||||
mnesia:dirty_delete({ticket, Id}),
|
|
||||||
{ok, deleted};
|
|
||||||
Error -> Error
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% @doc Статистика по тикетам (используется в admin_handler_ticket_stats)
|
|
||||||
stats() ->
|
|
||||||
Tickets = list_all(),
|
|
||||||
#{
|
|
||||||
total => length(Tickets),
|
|
||||||
open => count_by_status(open, Tickets),
|
|
||||||
in_progress => count_by_status(in_progress, Tickets),
|
|
||||||
resolved => count_by_status(resolved, Tickets),
|
|
||||||
closed => count_by_status(closed, Tickets)
|
|
||||||
}.
|
|
||||||
|
|
||||||
%% ── новые функции ──────────────────────────────────────
|
|
||||||
create_ticket(Data) ->
|
create_ticket(Data) ->
|
||||||
Id = infra_utils:generate_id(9),
|
ReporterId = maps:get(<<"reporter_id">>, Data),
|
||||||
|
ErrorHash = maps:get(<<"error_hash">>, Data, <<>>),
|
||||||
|
ErrorMessage = maps:get(<<"error_message">>, Data),
|
||||||
|
Stacktrace = maps:get(<<"stacktrace">>, Data, <<>>),
|
||||||
|
Context = maps:get(<<"context">>, Data, ?DEFAULT_CONTEXT),
|
||||||
|
Id = infra_utils:generate_id(16),
|
||||||
Now = calendar:universal_time(),
|
Now = calendar:universal_time(),
|
||||||
Status0 = maps:get(<<"status">>, Data, open), %% <-- ИСПРАВЛЕНО: извлекаем сырое значение
|
|
||||||
Status = normalize_status(Status0), %% <-- ИСПРАВЛЕНО: приводим к атому
|
|
||||||
Ticket = #ticket{
|
Ticket = #ticket{
|
||||||
id = Id,
|
id = Id,
|
||||||
reporter_id = maps:get(<<"reporter_id">>, Data, undefined),
|
reporter_id = ReporterId,
|
||||||
error_hash = maps:get(<<"error_hash">>, Data, <<"">>),
|
error_hash = ErrorHash,
|
||||||
error_message = maps:get(<<"error_message">>, Data),
|
error_message = ErrorMessage,
|
||||||
stacktrace = maps:get(<<"stacktrace">>, Data, <<"">>),
|
stacktrace = Stacktrace,
|
||||||
context = maps:get(<<"context">>, Data, <<"">>),
|
context = Context,
|
||||||
count = 1,
|
count = 1,
|
||||||
first_seen = Now,
|
first_seen = Now,
|
||||||
last_seen = Now,
|
last_seen = Now,
|
||||||
status = Status, %% <-- ИСПРАВЛЕНО: сохраняем атом
|
status = open,
|
||||||
assigned_to = maps:get(<<"assigned_to">>, Data, undefined),
|
assigned_to = ?DEFAULT_ASSIGNED_TO,
|
||||||
resolution_note = maps:get(<<"resolution_note">>, Data, undefined),
|
resolution_note = ?DEFAULT_RESOLUTION_NOTE,
|
||||||
closed_at = undefined
|
closed_at = ?DEFAULT_CLOSED_AT
|
||||||
},
|
},
|
||||||
mnesia:dirty_write(Ticket),
|
F = fun() -> mnesia:write(Ticket), {ok, Ticket} end,
|
||||||
{ok, Ticket}.
|
case mnesia:transaction(F) of
|
||||||
|
{atomic, Result} -> Result;
|
||||||
list_by_user(UserId) ->
|
{aborted, Reason} -> {error, Reason}
|
||||||
mnesia:dirty_match_object(#ticket{reporter_id = UserId, _ = '_'}).
|
|
||||||
|
|
||||||
%% ── функции подсчёта с нормализацией статуса ─────────────
|
|
||||||
|
|
||||||
%% @private Подсчитывает тикеты с заданным статусом (атом или бинарный)
|
|
||||||
count_by_status(Status, Tickets) ->
|
|
||||||
length([T || T <- Tickets, normalize_status(T#ticket.status) =:= Status]).
|
|
||||||
|
|
||||||
%% @doc Количество тикетов по статусу (атом или бинарный)
|
|
||||||
count_tickets_by_status(Status) ->
|
|
||||||
Tickets = list_all(),
|
|
||||||
count_by_status(Status, Tickets).
|
|
||||||
|
|
||||||
%% @doc Количество тикетов, назначенных администратору, с заданным статусом
|
|
||||||
count_tickets_by_admin(AdminId, Status) ->
|
|
||||||
Tickets = list_all(),
|
|
||||||
length([T || T <- Tickets,
|
|
||||||
T#ticket.assigned_to =:= AdminId andalso
|
|
||||||
normalize_status(T#ticket.status) =:= Status]).
|
|
||||||
|
|
||||||
avg_resolution_time() ->
|
|
||||||
% Загружаем все тикеты (или можно только закрытые, если их мало – решите по нагрузке)
|
|
||||||
Tickets = mnesia:dirty_match_object(#ticket{_ = '_'}),
|
|
||||||
% Фильтруем закрытые с учётом нормализации статуса
|
|
||||||
ClosedTickets = [T || T <- Tickets,
|
|
||||||
normalize_status(T#ticket.status) =:= closed,
|
|
||||||
T#ticket.closed_at =/= undefined],
|
|
||||||
case ClosedTickets of
|
|
||||||
[] -> 0;
|
|
||||||
_ ->
|
|
||||||
TotalSeconds = lists:sum([
|
|
||||||
calendar:datetime_to_gregorian_seconds(T#ticket.closed_at) -
|
|
||||||
calendar:datetime_to_gregorian_seconds(T#ticket.first_seen)
|
|
||||||
|| T <- ClosedTickets]),
|
|
||||||
TotalSeconds / length(ClosedTickets) / 3600.0
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% ── внутренние ─────────────────────────────────────────
|
%%%-------------------------------------------------------------------
|
||||||
apply_updates(Ticket, Updates) ->
|
%%% @doc Создание тикета с явным указанием всех обязательных полей.
|
||||||
lists:foldl(fun({Key, Value}, Acc) ->
|
%%% @end
|
||||||
case Key of
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec create(ReporterId :: binary(), ErrorHash :: binary(),
|
||||||
|
ErrorMessage :: binary(), Stacktrace :: binary()) ->
|
||||||
|
{ok, #ticket{}} | {error, term()}.
|
||||||
|
create(ReporterId, ErrorHash, ErrorMessage, Stacktrace) ->
|
||||||
|
create_ticket(#{
|
||||||
|
<<"reporter_id">> => ReporterId,
|
||||||
|
<<"error_hash">> => ErrorHash,
|
||||||
|
<<"error_message">> => ErrorMessage,
|
||||||
|
<<"stacktrace">> => Stacktrace
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Обновление тикета через map (для admin_handler_ticket_by_id).
|
||||||
|
%%% Значение ключа `status` принудительно приводится к атому.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update_ticket(Id :: binary(), Updates :: map()) ->
|
||||||
|
{ok, #ticket{}} | {error, not_found | term()}.
|
||||||
|
update_ticket(Id, Updates) ->
|
||||||
|
TupleList = maps:fold(fun(Key, Value, Acc) ->
|
||||||
|
AtomKey = case Key of
|
||||||
|
<<"status">> -> status;
|
||||||
|
<<"assigned_to">> -> assigned_to;
|
||||||
|
<<"resolution_note">> -> resolution_note;
|
||||||
|
<<"closed_at">> -> closed_at;
|
||||||
|
_ -> Key
|
||||||
|
end,
|
||||||
|
AtomValue = case Key of
|
||||||
<<"status">> ->
|
<<"status">> ->
|
||||||
NewStatus = normalize_status(Value),
|
try binary_to_existing_atom(Value, utf8)
|
||||||
Acc1 = Acc#ticket{status = NewStatus},
|
catch _:_ -> Value
|
||||||
case NewStatus of
|
|
||||||
closed -> Acc1#ticket{closed_at = calendar:universal_time()};
|
|
||||||
_ -> Acc1
|
|
||||||
end;
|
end;
|
||||||
<<"assigned_to">> -> Acc#ticket{assigned_to = Value};
|
_ -> Value
|
||||||
<<"resolution_note">> -> Acc#ticket{resolution_note = Value};
|
end,
|
||||||
<<"error_message">> -> Acc#ticket{error_message = Value};
|
[{AtomKey, AtomValue} | Acc]
|
||||||
<<"stacktrace">> -> Acc#ticket{stacktrace = Value};
|
end, [], Updates),
|
||||||
<<"context">> -> Acc#ticket{context = Value};
|
update(Id, TupleList).
|
||||||
_ -> Acc
|
|
||||||
end
|
|
||||||
end, Ticket, maps:to_list(Updates)).
|
|
||||||
|
|
||||||
%% @private Преобразует бинарный статус в атом, если нужно.
|
%%%-------------------------------------------------------------------
|
||||||
%% Атомы возвращает без изменений.
|
%%% @doc Получить тикет по идентификатору.
|
||||||
normalize_status(Status) when is_atom(Status) -> Status;
|
%%% @end
|
||||||
normalize_status(Status) when is_binary(Status) ->
|
%%%-------------------------------------------------------------------
|
||||||
try binary_to_existing_atom(Status, utf8)
|
-spec get_by_id(Id :: binary()) -> {ok, #ticket{}} | {error, not_found}.
|
||||||
catch error:badarg -> Status
|
get_by_id(Id) ->
|
||||||
|
case mnesia:dirty_read(ticket, Id) of
|
||||||
|
[] -> {error, not_found};
|
||||||
|
[Ticket] -> {ok, Ticket}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список всех тикетов.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_all() -> [#ticket{}].
|
||||||
|
list_all() ->
|
||||||
|
mnesia:dirty_match_object(#ticket{_ = '_'}).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список тикетов, созданных пользователем.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_by_user(UserId :: binary()) -> [#ticket{}].
|
||||||
|
list_by_user(UserId) ->
|
||||||
|
Match = #ticket{reporter_id = UserId, _ = '_'},
|
||||||
|
mnesia:dirty_match_object(Match).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Обновить поля тикета (список кортежей).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update(Id :: binary(), Updates :: [{atom(), term()}]) ->
|
||||||
|
{ok, #ticket{}} | {error, not_found | term()}.
|
||||||
|
update(Id, Updates) ->
|
||||||
|
F = fun() ->
|
||||||
|
case mnesia:read(ticket, Id) of
|
||||||
|
[] -> {error, not_found};
|
||||||
|
[Ticket] ->
|
||||||
|
UpdatedTicket = apply_updates(Ticket, Updates),
|
||||||
|
mnesia:write(UpdatedTicket),
|
||||||
|
{ok, UpdatedTicket}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(F) of
|
||||||
|
{atomic, Result} -> Result;
|
||||||
|
{aborted, Reason} -> {error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Изменить статус тикета.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update_status(Id :: binary(), NewStatus :: open | in_progress | resolved | closed) ->
|
||||||
|
{ok, #ticket{}} | {error, not_found | term()}.
|
||||||
|
update_status(Id, NewStatus) ->
|
||||||
|
update(Id, [{status, NewStatus}]).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Физическое удаление тикета.
|
||||||
|
%%% Возвращает `{ok, deleted}` при успехе.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec delete(Id :: binary()) -> {ok, deleted} | {error, not_found}.
|
||||||
|
delete(Id) ->
|
||||||
|
case mnesia:dirty_read(ticket, Id) of
|
||||||
|
[] -> {error, not_found};
|
||||||
|
[_] -> mnesia:dirty_delete(ticket, Id), {ok, deleted}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Удаление тикета (синоним delete/1, для обратной совместимости).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec delete_ticket(Id :: binary()) -> {ok, deleted} | {error, not_found}.
|
||||||
|
delete_ticket(Id) ->
|
||||||
|
delete(Id).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Назначить тикет на администратора.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec assign(Id :: binary(), AdminId :: binary()) ->
|
||||||
|
{ok, #ticket{}} | {error, not_found | term()}.
|
||||||
|
assign(Id, AdminId) ->
|
||||||
|
update(Id, [{assigned_to, AdminId}, {status, in_progress}]).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Разрешить тикет с указанием примечания.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec resolve(Id :: binary(), AdminId :: binary(), Note :: binary()) ->
|
||||||
|
{ok, #ticket{}} | {error, not_found | term()}.
|
||||||
|
resolve(Id, AdminId, Note) ->
|
||||||
|
update(Id, [{status, resolved}, {resolution_note, Note},
|
||||||
|
{assigned_to, AdminId}]).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Закрыть тикет (устанавливает статус `closed` и `closed_at`).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec close(Id :: binary(), AdminId :: binary()) ->
|
||||||
|
{ok, #ticket{}} | {error, not_found | term()}.
|
||||||
|
close(Id, AdminId) ->
|
||||||
|
update(Id, [{status, closed}, {closed_at, calendar:universal_time()},
|
||||||
|
{assigned_to, AdminId}]).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Количество тикетов.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_tickets() -> non_neg_integer().
|
||||||
|
count_tickets() ->
|
||||||
|
mnesia:table_info(ticket, size).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Количество тикетов с заданным статусом.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_tickets_by_status(Status :: open | in_progress | resolved | closed) ->
|
||||||
|
non_neg_integer().
|
||||||
|
count_tickets_by_status(Status) ->
|
||||||
|
Match = #ticket{status = Status, _ = '_'},
|
||||||
|
length(mnesia:dirty_match_object(Match)).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Среднее время разрешения тикетов (в часах).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec avg_resolution_time() -> float().
|
||||||
|
avg_resolution_time() ->
|
||||||
|
Tickets = list_all(),
|
||||||
|
Closed = [T || T <- Tickets, T#ticket.closed_at /= ?DEFAULT_CLOSED_AT],
|
||||||
|
case Closed of
|
||||||
|
[] -> 0.0;
|
||||||
|
_ ->
|
||||||
|
Total = lists:sum([calendar:datetime_to_gregorian_seconds(T#ticket.closed_at) -
|
||||||
|
calendar:datetime_to_gregorian_seconds(T#ticket.first_seen)
|
||||||
|
|| T <- Closed]),
|
||||||
|
Total / length(Closed) / 3600.0
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Количество тикетов, назначенных на администратора.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec count_tickets_by_admin(AdminId :: binary(), StatusOrAll :: atom()) ->
|
||||||
|
non_neg_integer().
|
||||||
|
count_tickets_by_admin(AdminId, all) ->
|
||||||
|
Match = #ticket{assigned_to = AdminId, _ = '_'},
|
||||||
|
length(mnesia:dirty_match_object(Match));
|
||||||
|
count_tickets_by_admin(AdminId, Status) ->
|
||||||
|
Match = #ticket{assigned_to = AdminId, status = Status, _ = '_'},
|
||||||
|
length(mnesia:dirty_match_object(Match)).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
apply_updates(Ticket, Updates) ->
|
||||||
|
Updated = lists:foldl(fun({Field, Value}, T) -> set_field(Field, Value, T) end,
|
||||||
|
Ticket, Updates),
|
||||||
|
Updated#ticket{last_seen = calendar:universal_time()}.
|
||||||
|
|
||||||
|
set_field(status, Value, T) -> T#ticket{status = Value};
|
||||||
|
set_field(assigned_to, Value, T) -> T#ticket{assigned_to = Value};
|
||||||
|
set_field(resolution_note, Value, T) -> T#ticket{resolution_note = Value};
|
||||||
|
set_field(closed_at, Value, T) -> T#ticket{closed_at = Value};
|
||||||
|
set_field(error_message, Value, T) -> T#ticket{error_message = Value};
|
||||||
|
set_field(stacktrace, Value, T) -> T#ticket{stacktrace = Value};
|
||||||
|
set_field(context, Value, T) -> T#ticket{context = Value};
|
||||||
|
set_field(count, Value, T) -> T#ticket{count = Value};
|
||||||
|
set_field(_, _, T) -> T.
|
||||||
+182
-40
@@ -1,52 +1,83 @@
|
|||||||
-module(core_user).
|
-module(core_user).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
-export([create/2, get_by_id/1, get_by_email/1, update/2, update_status/3,
|
||||||
-export([create/2, get_by_id/1, get_by_email/1, update/2, update_status/3, delete/1, update_last_login/1]).
|
delete/1, update_last_login/1]).
|
||||||
-export([email_exists/1]).
|
-export([email_exists/1]).
|
||||||
-export([list_users/0]).
|
-export([list_users/0]).
|
||||||
-export([block/2, unblock/2]).
|
-export([block/2, unblock/2]).
|
||||||
-export([count_users/0, count_users_by_date/2, list_all/0]).
|
-export([count_users/0, count_users_by_date/2, list_all/0]).
|
||||||
-export([create_bot/2, delete_bot/1]).
|
-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) ->
|
create(Email, Password) ->
|
||||||
% Проверяем, существует ли email
|
|
||||||
case email_exists(Email) of
|
case email_exists(Email) of
|
||||||
true ->
|
true ->
|
||||||
{error, email_exists};
|
{error, email_exists};
|
||||||
false ->
|
false ->
|
||||||
Id = infra_utils:generate_id(16),
|
Id = infra_utils:generate_id(16),
|
||||||
{ok, PasswordHash} = logic_auth:hash_password(Password),
|
{ok, PasswordHash} = logic_auth:hash_password(Password),
|
||||||
|
Now = calendar:universal_time(),
|
||||||
User = #user{
|
User = #user{
|
||||||
id = Id,
|
id = Id,
|
||||||
email = Email,
|
email = Email,
|
||||||
password_hash = PasswordHash,
|
password_hash = PasswordHash,
|
||||||
role = user,
|
role = user,
|
||||||
status = active,
|
status = active,
|
||||||
created_at = calendar:universal_time(),
|
reason = ?DEFAULT_REASON,
|
||||||
updated_at = calendar:universal_time()
|
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
|
case mnesia:transaction(F) of
|
||||||
{atomic, Result} -> Result;
|
{atomic, Result} -> Result;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получение пользователя по ID
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить пользователя по идентификатору.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_by_id(Id :: binary()) -> {ok, #user{}} | {error, not_found}.
|
||||||
get_by_id(Id) ->
|
get_by_id(Id) ->
|
||||||
case mnesia:dirty_read(user, Id) of
|
case mnesia:dirty_read(user, Id) of
|
||||||
[] -> {error, not_found};
|
[] -> {error, not_found};
|
||||||
[User] -> {ok, User}
|
[User] -> {ok, User}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получение пользователя по email
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить пользователя по email.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_by_email(Email :: binary()) -> {ok, #user{}} | {error, not_found}.
|
||||||
get_by_email(Email) ->
|
get_by_email(Email) ->
|
||||||
Match = #user{email = Email, _ = '_'},
|
Match = #user{email = Email, _ = '_'},
|
||||||
case mnesia:dirty_match_object(Match) of
|
case mnesia:dirty_match_object(Match) of
|
||||||
@@ -54,31 +85,46 @@ get_by_email(Email) ->
|
|||||||
[User] -> {ok, User}
|
[User] -> {ok, User}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Проверка существования email
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Проверить, существует ли email в базе.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec email_exists(Email :: binary()) -> boolean().
|
||||||
email_exists(Email) ->
|
email_exists(Email) ->
|
||||||
case get_by_email(Email) of
|
case get_by_email(Email) of
|
||||||
{ok, _} -> true;
|
{ok, _} -> true;
|
||||||
{error, _} -> false
|
{error, _} -> false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Обновление пользователя
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Обновить поля пользователя.
|
||||||
|
%%% `Updates` – список пар `{atom(), term()}`.
|
||||||
|
%%% Поля, не указанные в обновлении, сохраняют прежние значения.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update(Id :: binary(), Updates :: [{atom(), term()}]) ->
|
||||||
|
{ok, #user{}} | {error, not_found | term()}.
|
||||||
update(Id, Updates) ->
|
update(Id, Updates) ->
|
||||||
F = fun() ->
|
F = fun() ->
|
||||||
case mnesia:read(user, Id) of
|
case mnesia:read(user, Id) of
|
||||||
[] ->
|
[] -> {error, not_found};
|
||||||
{error, not_found};
|
|
||||||
[User] ->
|
[User] ->
|
||||||
UpdatedUser = apply_updates(User, Updates),
|
UpdatedUser = apply_updates(User, Updates),
|
||||||
mnesia:write(UpdatedUser),
|
mnesia:write(UpdatedUser),
|
||||||
{ok, UpdatedUser}
|
{ok, UpdatedUser}
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
case mnesia:transaction(F) of
|
case mnesia:transaction(F) of
|
||||||
{atomic, Result} -> Result;
|
{atomic, Result} -> Result;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Обновить время последнего входа пользователя.
|
||||||
|
%%% Устанавливает `last_login` в текущее UTC-время.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update_last_login(Id :: binary()) -> {ok, #user{}} | {error, not_found}.
|
||||||
update_last_login(Id) ->
|
update_last_login(Id) ->
|
||||||
case get_by_id(Id) of
|
case get_by_id(Id) of
|
||||||
{ok, User} ->
|
{ok, User} ->
|
||||||
@@ -88,35 +134,58 @@ update_last_login(Id) ->
|
|||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Изменить статус и причину пользователя.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec update_status(Id :: binary(), Status :: atom(), Reason :: binary()) ->
|
||||||
|
{ok, #user{}} | {error, not_found}.
|
||||||
update_status(Id, Status, Reason) ->
|
update_status(Id, Status, Reason) ->
|
||||||
case get_by_id(Id) of
|
case get_by_id(Id) of
|
||||||
{ok, User} ->
|
{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),
|
mnesia:dirty_write(Updated),
|
||||||
{ok, Updated};
|
{ok, Updated};
|
||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Удаление пользователя (soft delete)
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Мягкое удаление пользователя (установка статуса `deleted`).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec delete(Id :: binary()) -> {ok, #user{}} | {error, not_found | term()}.
|
||||||
delete(Id) ->
|
delete(Id) ->
|
||||||
update(Id, [{status, deleted}]).
|
update(Id, [{status, deleted}]).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить список активных пользователей (без удалённых).
|
||||||
|
%%% Результат – список карт с ключами `id`, `email`, `role`, `status`, `reason`,
|
||||||
|
%%% `created_at`, `updated_at`.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_users() -> {ok, [map()]}.
|
||||||
list_users() ->
|
list_users() ->
|
||||||
Users = mnesia:dirty_match_object(#user{_ = '_'}),
|
Users = mnesia:dirty_match_object(#user{_ = '_'}),
|
||||||
ActiveUsers = [U || U <- Users, U#user.status =/= deleted],
|
ActiveUsers = [U || U <- Users, U#user.status =/= deleted],
|
||||||
{ok, [user_to_map(U) || U <- ActiveUsers]}.
|
{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) ->
|
user_to_map(User) when is_map(User) ->
|
||||||
#{
|
#{
|
||||||
id => maps:get(id, User),
|
id => maps:get(id, User),
|
||||||
email => maps:get(email, User),
|
email => maps:get(email, User),
|
||||||
role => maps:get(role, User, <<"user">>),
|
role => maps:get(role, User, <<"user">>),
|
||||||
status => maps:get(status, User, <<"active">>),
|
status => maps:get(status, User, <<"active">>),
|
||||||
reason => maps:get(reason, User, undefined),
|
reason => maps:get(reason, User, ?DEFAULT_REASON),
|
||||||
created_at => maps:get(created_at, User),
|
created_at => maps:get(created_at, User),
|
||||||
updated_at => maps:get(updated_at, User)
|
updated_at => maps:get(updated_at, User)
|
||||||
};
|
};
|
||||||
|
|
||||||
user_to_map(User) ->
|
user_to_map(User) ->
|
||||||
#{
|
#{
|
||||||
id => User#user.id,
|
id => User#user.id,
|
||||||
@@ -128,36 +197,77 @@ user_to_map(User) ->
|
|||||||
updated_at => User#user.updated_at
|
updated_at => User#user.updated_at
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Заблокировать пользователя с указанием причины.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec block(Id :: binary(), Reason :: binary()) ->
|
||||||
|
{ok, #user{}} | {error, not_found}.
|
||||||
block(Id, Reason) ->
|
block(Id, Reason) ->
|
||||||
case get_by_id(Id) of
|
case get_by_id(Id) of
|
||||||
{ok, User} ->
|
{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),
|
mnesia:dirty_write(Updated),
|
||||||
{ok, Updated};
|
{ok, Updated};
|
||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Разблокировать пользователя с указанием причины.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec unblock(Id :: binary(), Reason :: binary()) ->
|
||||||
|
{ok, #user{}} | {error, not_found}.
|
||||||
unblock(Id, Reason) ->
|
unblock(Id, Reason) ->
|
||||||
case get_by_id(Id) of
|
case get_by_id(Id) of
|
||||||
{ok, User} ->
|
{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),
|
mnesia:dirty_write(Updated),
|
||||||
{ok, Updated};
|
{ok, Updated};
|
||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
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() ->
|
count_users() ->
|
||||||
mnesia:table_info(user, size).
|
mnesia:table_info(user, size).
|
||||||
|
|
||||||
%% Административный список (все пользователи, без фильтрации)
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получить всех пользователей (включая удалённых).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_all() -> [#user{}].
|
||||||
list_all() ->
|
list_all() ->
|
||||||
mnesia:dirty_match_object(#user{_ = '_'}).
|
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) ->
|
count_users_by_date(From, To) ->
|
||||||
All = mnesia:dirty_match_object(#user{_ = '_'}),
|
All = mnesia:dirty_match_object(#user{_ = '_'}),
|
||||||
Filtered = lists:filter(fun(U) ->
|
Filtered = lists:filter(fun(U) -> U#user.created_at >= From andalso
|
||||||
U#user.created_at >= From andalso U#user.created_at =< To
|
U#user.created_at =< To end, All),
|
||||||
end, All),
|
|
||||||
Counts = lists:foldl(fun(U, Acc) ->
|
Counts = lists:foldl(fun(U, Acc) ->
|
||||||
Day = date_part(U#user.created_at),
|
Day = date_part(U#user.created_at),
|
||||||
case lists:keyfind(Day, 1, Acc) of
|
case lists:keyfind(Day, 1, Acc) of
|
||||||
@@ -167,18 +277,35 @@ count_users_by_date(From, To) ->
|
|||||||
end, [], Filtered),
|
end, [], Filtered),
|
||||||
lists:sort(Counts).
|
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}.
|
date_part({{Y,M,D}, _}) -> {Y,M,D}.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Применить список обновлений к записи пользователя.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec apply_updates(#user{}, [{atom(), term()}]) -> #user{}.
|
||||||
apply_updates(User, Updates) ->
|
apply_updates(User, Updates) ->
|
||||||
Updated = lists:foldl(fun({Field, Value}, U) ->
|
Updated = lists:foldl(fun({Field, Value}, U) ->
|
||||||
set_field(Field, Value, U)
|
set_field(Field, Value, U)
|
||||||
end, User, Updates),
|
end, User, Updates),
|
||||||
Updated#user{updated_at = calendar:universal_time()}.
|
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(email, Value, U) -> U#user{email = Value};
|
||||||
set_field(password_hash, Value, U) -> U#user{password_hash = 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(role, Value, U) when Value =:= user; Value =:= admin; Value =:= bot ->
|
||||||
set_field(status, Value, U) when Value =:= active; Value =:= frozen; Value =:= deleted -> U#user{status = Value};
|
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(reason, Value, U) -> U#user{reason = Value};
|
||||||
set_field(nickname, Value, U) -> U#user{nickname = Value};
|
set_field(nickname, Value, U) -> U#user{nickname = Value};
|
||||||
set_field(avatar_url, Value, U) -> U#user{avatar_url = 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(preferences, Value, U) -> U#user{preferences = Value};
|
||||||
set_field(_, _, U) -> U.
|
set_field(_, _, U) -> U.
|
||||||
|
|
||||||
%% ------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%% API для ботов
|
%%% @doc Создание бота (аналогично пользователю, но с ролью `bot`).
|
||||||
%% ------------------------------------------------------------------
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
-spec create_bot(Email :: binary(), Password :: binary()) ->
|
-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) ->
|
create_bot(Email, Password) ->
|
||||||
case email_exists(Email) of
|
case email_exists(Email) of
|
||||||
true ->
|
true ->
|
||||||
@@ -204,14 +331,24 @@ create_bot(Email, Password) ->
|
|||||||
[] ->
|
[] ->
|
||||||
{ok, PasswordHash} = logic_auth:hash_password(Password),
|
{ok, PasswordHash} = logic_auth:hash_password(Password),
|
||||||
Id = infra_utils:generate_id(16),
|
Id = infra_utils:generate_id(16),
|
||||||
|
Now = calendar:universal_time(),
|
||||||
User = #user{
|
User = #user{
|
||||||
id = Id,
|
id = Id,
|
||||||
email = Email,
|
email = Email,
|
||||||
password_hash = PasswordHash,
|
password_hash = PasswordHash,
|
||||||
role = bot,
|
role = bot,
|
||||||
status = active,
|
status = active,
|
||||||
created_at = calendar:universal_time(),
|
reason = ?DEFAULT_REASON,
|
||||||
updated_at = calendar:universal_time()
|
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 = mnesia:dirty_write(User),
|
||||||
{ok, User};
|
{ok, User};
|
||||||
@@ -220,11 +357,16 @@ create_bot(Email, Password) ->
|
|||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Удаление бота (физическое удаление записи, не мягкое).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
-spec delete_bot(Id :: binary()) -> ok | {error, not_found}.
|
-spec delete_bot(Id :: binary()) -> ok | {error, not_found}.
|
||||||
delete_bot(Id) ->
|
delete_bot(Id) ->
|
||||||
case mnesia:dirty_read({user, Id}) of
|
case mnesia:dirty_read({user, Id}) of
|
||||||
[#user{role = bot}] ->
|
[#user{role = bot}] ->
|
||||||
mnesia:dirty_delete({user, Id}),
|
mnesia:dirty_delete({user, Id}),
|
||||||
ok;
|
ok;
|
||||||
[] -> {error, not_found}
|
[] ->
|
||||||
|
{error, not_found}
|
||||||
end.
|
end.
|
||||||
@@ -101,8 +101,12 @@ update_report(Req) ->
|
|||||||
ReportId = cowboy_req:binding(id, Req1),
|
ReportId = cowboy_req:binding(id, Req1),
|
||||||
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||||
try jsx:decode(Body, [return_maps]) of
|
try jsx:decode(Body, [return_maps]) of
|
||||||
#{<<"status">> := Status} ->
|
#{<<"status">> := StatusBin} ->
|
||||||
case logic_report:update_report_status(AdminId, ReportId, Status) of
|
% Преобразуем бинарный статус в атом
|
||||||
|
StatusAtom = try binary_to_existing_atom(StatusBin, utf8)
|
||||||
|
catch _:_ -> StatusBin
|
||||||
|
end,
|
||||||
|
case logic_report:update_report_status(AdminId, ReportId, StatusAtom) of
|
||||||
{ok, Report} ->
|
{ok, Report} ->
|
||||||
% Аудит изменения статуса жалобы
|
% Аудит изменения статуса жалобы
|
||||||
admin_utils:log_admin_action(AdminId, <<"update_report_status">>, <<"report">>, ReportId, Req2),
|
admin_utils:log_admin_action(AdminId, <<"update_report_status">>, <<"report">>, ReportId, Req2),
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
%%%-------------------------------------------------------------------
|
|
||||||
%%% @doc Административный обработчик списка жалоб.
|
|
||||||
%%% GET – список с пагинацией, фильтрацией и сортировкой.
|
|
||||||
%%% @end
|
|
||||||
%%%-------------------------------------------------------------------
|
|
||||||
-module(admin_handler_reports).
|
-module(admin_handler_reports).
|
||||||
-behaviour(cowboy_handler).
|
-behaviour(cowboy_handler).
|
||||||
-export([init/2]).
|
-export([init/2]).
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
%%% cowboy_handler callback
|
|
||||||
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
||||||
init(Req, _Opts) ->
|
init(Req, _Opts) ->
|
||||||
case cowboy_req:method(Req) of
|
case cowboy_req:method(Req) of
|
||||||
@@ -84,12 +83,13 @@ ticket_update_schema() ->
|
|||||||
list_tickets(Req) ->
|
list_tickets(Req) ->
|
||||||
case handler_utils:auth_admin(Req) of
|
case handler_utils:auth_admin(Req) of
|
||||||
{ok, AdminId, Req1} ->
|
{ok, AdminId, Req1} ->
|
||||||
|
Qs = cowboy_req:parse_qs(Req1),
|
||||||
Filters = parse_ticket_filters(Req1),
|
Filters = parse_ticket_filters(Req1),
|
||||||
Pagination0 = handler_utils:parse_pagination_params(Req1),
|
Pagination0 = handler_utils:parse_pagination_params(Req1),
|
||||||
Pagination = Pagination0#{
|
% Явно извлекаем sort и order с нормализацией регистра
|
||||||
sort => maps:get(sort, Pagination0, <<"first_seen">>),
|
SortBy = proplists:get_value(<<"sort">>, Qs, <<"first_seen">>),
|
||||||
order => maps:get(order, Pagination0, <<"desc">>)
|
SortOrder = proplists:get_value(<<"order">>, Qs, <<"desc">>),
|
||||||
},
|
Pagination = Pagination0#{sort => SortBy, order => SortOrder},
|
||||||
TicketsResult = case maps:get(status, Filters, undefined) of
|
TicketsResult = case maps:get(status, Filters, undefined) of
|
||||||
undefined -> logic_ticket:list_tickets(AdminId);
|
undefined -> logic_ticket:list_tickets(AdminId);
|
||||||
StatusBin ->
|
StatusBin ->
|
||||||
@@ -173,7 +173,7 @@ sort_tickets(Tickets, #{sort := Sort, order := Order}) ->
|
|||||||
|
|
||||||
ticket_field(#ticket{first_seen = V}, first_seen) -> V;
|
ticket_field(#ticket{first_seen = V}, first_seen) -> V;
|
||||||
ticket_field(#ticket{last_seen = V}, last_seen) -> V;
|
ticket_field(#ticket{last_seen = V}, last_seen) -> V;
|
||||||
ticket_field(#ticket{status = V}, status) -> V;
|
ticket_field(#ticket{status = V}, status) -> atom_to_binary(V, utf8);
|
||||||
ticket_field(_, _) -> undefined.
|
ticket_field(_, _) -> undefined.
|
||||||
|
|
||||||
apply_ticket_changes(AdminId, TicketId, Data) ->
|
apply_ticket_changes(AdminId, TicketId, Data) ->
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ cancel_booking(Req) ->
|
|||||||
case handler_utils:auth_user(Req) of
|
case handler_utils:auth_user(Req) of
|
||||||
{ok, UserId, Req1} ->
|
{ok, UserId, Req1} ->
|
||||||
BookingId = cowboy_req:binding(id, Req1),
|
BookingId = cowboy_req:binding(id, Req1),
|
||||||
case logic_booking:cancel_booking(UserId, BookingId) of
|
case logic_booking:cancel_booking(BookingId, UserId) of
|
||||||
{ok, Booking} ->
|
{ok, Booking} ->
|
||||||
handler_utils:send_json(Req1, 200, booking_to_json(Booking));
|
handler_utils:send_json(Req1, 200, booking_to_json(Booking));
|
||||||
{error, access_denied} ->
|
{error, access_denied} ->
|
||||||
|
|||||||
+140
-143
@@ -1,154 +1,184 @@
|
|||||||
-module(logic_booking).
|
-module(logic_booking).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
-export([create_booking/2, confirm_booking/2, confirm_booking/3,
|
||||||
|
cancel_booking/2, cancel_booking/3, get_booking/2,
|
||||||
|
list_bookings/2, list_user_bookings/1, delete_booking/2,
|
||||||
|
list_bookings_admin/0, get_booking_admin/1, list_event_bookings/1]).
|
||||||
|
|
||||||
-export([create_booking/2, confirm_booking/3, cancel_booking/2]).
|
%%%-------------------------------------------------------------------
|
||||||
-export([get_booking/2, list_event_bookings/2, list_user_bookings/1]).
|
%%% @doc Создание бронирования со статусом `pending`.
|
||||||
-export([auto_confirm/1, check_timeout_confirmations/0]).
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
%% Создание бронирования (запись на событие)
|
-spec create_booking(UserId :: binary(), EventId :: binary()) ->
|
||||||
|
{ok, #booking{}} | {error, full | already_booked | not_found}.
|
||||||
create_booking(UserId, EventId) ->
|
create_booking(UserId, EventId) ->
|
||||||
% Получаем событие напрямую, без проверки доступа к календарю
|
|
||||||
case core_event:get_by_id(EventId) of
|
case core_event:get_by_id(EventId) of
|
||||||
{ok, Event} ->
|
{ok, Event} ->
|
||||||
% Проверяем, что событие активно
|
|
||||||
case Event#event.status of
|
|
||||||
active ->
|
|
||||||
% Проверяем календарь для политики подтверждения
|
|
||||||
case core_calendar:get_by_id(Event#event.calendar_id) of
|
|
||||||
{ok, Calendar} ->
|
|
||||||
case Calendar#calendar.status of
|
|
||||||
active ->
|
|
||||||
% Проверяем, что есть места
|
|
||||||
case check_capacity(EventId, Event#event.capacity) of
|
case check_capacity(EventId, Event#event.capacity) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
% Проверяем, не записан ли уже пользователь
|
|
||||||
case core_booking:get_by_event_and_user(EventId, UserId) of
|
case core_booking:get_by_event_and_user(EventId, UserId) of
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
ActualEventId = get_actual_event_id(Event, UserId),
|
core_booking:create(EventId, UserId, pending);
|
||||||
case core_booking:create(ActualEventId, UserId) of
|
|
||||||
{ok, Booking} ->
|
|
||||||
handle_confirmation_policy(Booking, Event, Calendar),
|
|
||||||
logic_notification:notify_booking(UserId, Booking), % ← Уведомление
|
|
||||||
{ok, Booking};
|
|
||||||
Error ->
|
|
||||||
Error
|
|
||||||
end;
|
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
{error, already_booked}
|
{error, already_booked}
|
||||||
end;
|
end;
|
||||||
{error, full} ->
|
{error, full} ->
|
||||||
{error, event_full}
|
{error, full}
|
||||||
end;
|
end;
|
||||||
_ ->
|
{error, not_found} ->
|
||||||
{error, calendar_not_active}
|
{error, not_found}
|
||||||
end;
|
|
||||||
_ ->
|
|
||||||
{error, calendar_not_found}
|
|
||||||
end;
|
|
||||||
_ ->
|
|
||||||
{error, event_not_active}
|
|
||||||
end;
|
|
||||||
Error ->
|
|
||||||
Error
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Подтверждение бронирования (владельцем календаря)
|
%%%-------------------------------------------------------------------
|
||||||
confirm_booking(UserId, BookingId, Action) when Action =:= confirm; Action =:= decline ->
|
%%% @doc Подтверждение бронирования (двухарная версия).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec confirm_booking(BookingId :: binary(), UserId :: binary()) ->
|
||||||
|
{ok, #booking{}} | {error, not_found | access_denied}.
|
||||||
|
confirm_booking(BookingId, _UserId) ->
|
||||||
case core_booking:get_by_id(BookingId) of
|
case core_booking:get_by_id(BookingId) of
|
||||||
{ok, Booking} ->
|
{ok, Booking} ->
|
||||||
% Проверяем права на событие
|
case Booking#booking.status of
|
||||||
case logic_event:get_event(UserId, Booking#booking.event_id) of
|
pending ->
|
||||||
{ok, Event} ->
|
Now = calendar:universal_time(),
|
||||||
% Проверяем, что пользователь может редактировать календарь
|
core_booking:update(BookingId, [{status, confirmed}, {confirmed_at, Now}]);
|
||||||
case logic_calendar:get_calendar(UserId, Event#event.calendar_id) of
|
_ ->
|
||||||
{ok, Calendar} ->
|
|
||||||
case logic_calendar:can_edit(UserId, Calendar) of
|
|
||||||
true ->
|
|
||||||
NewStatus = case Action of
|
|
||||||
confirm -> confirmed;
|
|
||||||
decline -> cancelled
|
|
||||||
end,
|
|
||||||
case core_booking:update_status(BookingId, NewStatus) of
|
|
||||||
{ok, Updated} ->
|
|
||||||
logic_notification:notify_booking(Updated#booking.user_id, Updated),
|
|
||||||
{ok, Updated};
|
|
||||||
Error -> Error
|
|
||||||
end;
|
|
||||||
false ->
|
|
||||||
{error, access_denied}
|
{error, access_denied}
|
||||||
end;
|
end;
|
||||||
Error -> Error
|
Error -> Error
|
||||||
end;
|
|
||||||
Error -> Error
|
|
||||||
end;
|
|
||||||
Error -> Error
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Отмена бронирования (участником)
|
%%%-------------------------------------------------------------------
|
||||||
cancel_booking(UserId, BookingId) ->
|
%%% @doc Подтверждение бронирования (трёхарная версия для обработчиков).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec confirm_booking(UserId :: binary(), BookingId :: binary(), confirm) ->
|
||||||
|
{ok, #booking{}} | {error, not_found | access_denied}.
|
||||||
|
confirm_booking(UserId, BookingId, confirm) ->
|
||||||
|
confirm_booking(BookingId, UserId).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Отмена бронирования (двухарная версия).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec cancel_booking(BookingId :: binary(), UserId :: binary()) ->
|
||||||
|
{ok, #booking{}} | {error, not_found | access_denied}.
|
||||||
|
cancel_booking(BookingId, UserId) ->
|
||||||
case core_booking:get_by_id(BookingId) of
|
case core_booking:get_by_id(BookingId) of
|
||||||
{ok, Booking} ->
|
{ok, Booking} ->
|
||||||
% Проверяем, что это бронирование пользователя
|
case Booking#booking.status of
|
||||||
|
cancelled ->
|
||||||
|
{ok, Booking};
|
||||||
|
_ ->
|
||||||
case Booking#booking.user_id =:= UserId of
|
case Booking#booking.user_id =:= UserId of
|
||||||
true ->
|
true -> core_booking:update(BookingId, [{status, cancelled}]);
|
||||||
core_booking:update_status(BookingId, cancelled);
|
false -> {error, access_denied}
|
||||||
false ->
|
end
|
||||||
{error, access_denied}
|
|
||||||
end;
|
end;
|
||||||
Error ->
|
Error -> Error
|
||||||
Error
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получение бронирования
|
%%%-------------------------------------------------------------------
|
||||||
get_booking(UserId, BookingId) ->
|
%%% @doc Отмена бронирования (трёхарная версия для обработчиков).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec cancel_booking(UserId :: binary(), BookingId :: binary(), cancel) ->
|
||||||
|
{ok, #booking{}} | {error, not_found | access_denied}.
|
||||||
|
cancel_booking(UserId, BookingId, cancel) ->
|
||||||
|
cancel_booking(BookingId, UserId).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Получение бронирования по ID.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_booking(BookingId :: binary(), UserId :: binary()) ->
|
||||||
|
{ok, #booking{}} | {error, not_found | access_denied}.
|
||||||
|
get_booking(BookingId, UserId) ->
|
||||||
case core_booking:get_by_id(BookingId) of
|
case core_booking:get_by_id(BookingId) of
|
||||||
{ok, Booking} ->
|
{ok, Booking} ->
|
||||||
% Проверяем доступ к событию
|
case Booking#booking.user_id =:= UserId of
|
||||||
case logic_event:get_event(UserId, Booking#booking.event_id) of
|
true -> {ok, Booking};
|
||||||
{ok, _} -> {ok, Booking};
|
false -> {error, access_denied}
|
||||||
|
end;
|
||||||
Error -> Error
|
Error -> Error
|
||||||
end;
|
|
||||||
Error ->
|
|
||||||
Error
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Список бронирований события (для владельца)
|
%%%-------------------------------------------------------------------
|
||||||
list_event_bookings(UserId, EventId) ->
|
%%% @doc Список бронирований события.
|
||||||
case logic_event:get_event(UserId, EventId) of
|
%%% @end
|
||||||
{ok, Event} ->
|
%%%-------------------------------------------------------------------
|
||||||
% Проверяем права на календарь
|
-spec list_bookings(EventId :: binary(), UserId :: binary()) ->
|
||||||
case logic_calendar:get_calendar(UserId, Event#event.calendar_id) of
|
{ok, [#booking{}]}.
|
||||||
{ok, Calendar} ->
|
list_bookings(EventId, UserId) ->
|
||||||
case logic_calendar:can_edit(UserId, Calendar) of
|
{ok, Bookings} = core_booking:list_by_event(EventId),
|
||||||
true ->
|
Filtered = case admin_utils:is_admin(UserId) of
|
||||||
core_booking:list_by_event(EventId);
|
true -> Bookings;
|
||||||
false ->
|
false -> [B || B <- Bookings, B#booking.user_id =:= UserId]
|
||||||
{error, access_denied}
|
end,
|
||||||
end;
|
{ok, Filtered}.
|
||||||
Error ->
|
|
||||||
Error
|
|
||||||
end;
|
|
||||||
Error ->
|
|
||||||
Error
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Список бронирований пользователя
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список бронирований пользователя.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_user_bookings(UserId :: binary()) -> {ok, [#booking{}]}.
|
||||||
list_user_bookings(UserId) ->
|
list_user_bookings(UserId) ->
|
||||||
core_booking:list_by_user(UserId).
|
core_booking:list_by_user(UserId).
|
||||||
|
|
||||||
%% Автоматическое подтверждение (для политики auto)
|
%%%-------------------------------------------------------------------
|
||||||
auto_confirm(BookingId) ->
|
%%% @doc Удаление бронирования (только владелец).
|
||||||
core_booking:update_status(BookingId, confirmed).
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec delete_booking(BookingId :: binary(), UserId :: binary()) ->
|
||||||
|
ok | {error, not_found | access_denied}.
|
||||||
|
delete_booking(BookingId, UserId) ->
|
||||||
|
case core_booking:get_by_id(BookingId) of
|
||||||
|
{ok, Booking} ->
|
||||||
|
case Booking#booking.user_id =:= UserId of
|
||||||
|
true -> core_booking:delete(BookingId);
|
||||||
|
false -> {error, access_denied}
|
||||||
|
end;
|
||||||
|
Error -> Error
|
||||||
|
end.
|
||||||
|
|
||||||
%% Проверка истечения timeout подтверждений
|
%%%-------------------------------------------------------------------
|
||||||
check_timeout_confirmations() ->
|
%%% @doc Административное получение бронирования.
|
||||||
% Получаем все pending бронирования для календарей с timeout
|
%%% @end
|
||||||
% В реальной реализации нужно периодически вызывать эту функцию
|
%%%-------------------------------------------------------------------
|
||||||
ok.
|
-spec get_booking_admin(BookingId :: binary()) ->
|
||||||
|
{ok, #booking{}} | {error, not_found}.
|
||||||
|
get_booking_admin(BookingId) ->
|
||||||
|
core_booking:get_by_id(BookingId).
|
||||||
|
|
||||||
%% Внутренние функции
|
%%%-------------------------------------------------------------------
|
||||||
check_capacity(_EventId, undefined) ->
|
%%% @doc Список бронирований события (административный).
|
||||||
{ok, unlimited};
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_event_bookings(EventId :: binary()) -> {ok, [#booking{}]}.
|
||||||
|
list_event_bookings(EventId) ->
|
||||||
|
core_booking:list_by_event(EventId).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список всех бронирований (административный).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_bookings_admin() -> {ok, [#booking{}]}.
|
||||||
|
list_bookings_admin() ->
|
||||||
|
{ok, core_booking:list_all()}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Проверка вместимости события.
|
||||||
|
%%% `undefined` и `0` означают неограниченную вместимость.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec check_capacity(EventId :: binary(), Capacity :: integer() | undefined) ->
|
||||||
|
{ok, integer() | unlimited} | {error, full}.
|
||||||
|
check_capacity(_EventId, undefined) -> {ok, unlimited};
|
||||||
|
check_capacity(_EventId, 0) -> {ok, unlimited};
|
||||||
check_capacity(EventId, Capacity) ->
|
check_capacity(EventId, Capacity) ->
|
||||||
{ok, Bookings} = core_booking:list_by_event(EventId),
|
{ok, Bookings} = core_booking:list_by_event(EventId),
|
||||||
ConfirmedCount = length([B || B <- Bookings, B#booking.status =:= confirmed]),
|
ConfirmedCount = length([B || B <- Bookings, B#booking.status =:= confirmed]),
|
||||||
@@ -156,36 +186,3 @@ check_capacity(EventId, Capacity) ->
|
|||||||
true -> {ok, Capacity - ConfirmedCount};
|
true -> {ok, Capacity - ConfirmedCount};
|
||||||
false -> {error, full}
|
false -> {error, full}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
get_actual_event_id(Event, _UserId) ->
|
|
||||||
case Event#event.event_type of
|
|
||||||
recurring ->
|
|
||||||
% Для повторяющихся событий нужно материализовать вхождение
|
|
||||||
% Здесь предполагается, что start_time передаётся в запросе
|
|
||||||
% В полной реализации нужно получать occurrence_start из параметров
|
|
||||||
Event#event.id;
|
|
||||||
single ->
|
|
||||||
Event#event.id
|
|
||||||
end.
|
|
||||||
|
|
||||||
handle_confirmation_policy(Booking, _Event, Calendar) ->
|
|
||||||
io:format("Confirmation policy: ~p~n", [Calendar#calendar.confirmation]),
|
|
||||||
case Calendar#calendar.confirmation of
|
|
||||||
auto ->
|
|
||||||
io:format("Auto-confirming booking ~p~n", [Booking#booking.id]),
|
|
||||||
auto_confirm(Booking#booking.id);
|
|
||||||
manual ->
|
|
||||||
io:format("Manual confirmation, leaving pending~n"),
|
|
||||||
ok;
|
|
||||||
{timeout, Seconds} ->
|
|
||||||
io:format("Timeout confirmation: ~p seconds~n", [Seconds]),
|
|
||||||
spawn(fun() ->
|
|
||||||
timer:sleep(Seconds * 1000),
|
|
||||||
case core_booking:get_by_id(Booking#booking.id) of
|
|
||||||
{ok, B} when B#booking.status =:= pending ->
|
|
||||||
auto_confirm(Booking#booking.id);
|
|
||||||
_ ->
|
|
||||||
ok
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end.
|
|
||||||
+44
-34
@@ -1,47 +1,57 @@
|
|||||||
-module(logic_report).
|
-module(logic_report).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
-export([create_report/4, get_report/2, list_reports/1, update_report_status/3]).
|
||||||
|
|
||||||
-export([list_reports/1, get_report/2, update_report_status/3, delete_report/2]).
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Создаёт жалобу.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec create_report(UserId :: binary(), TargetType :: calendar | event | review,
|
||||||
|
TargetId :: binary(), Reason :: binary()) ->
|
||||||
|
{ok, #report{}} | {error, term()}.
|
||||||
|
create_report(UserId, TargetType, TargetId, Reason) ->
|
||||||
|
core_report:create(UserId, TargetType, TargetId, Reason).
|
||||||
|
|
||||||
%% Получить список всех жалоб (только для админов)
|
%%%-------------------------------------------------------------------
|
||||||
-spec list_reports(binary()) -> {ok, [#report{}]} | {error, access_denied}.
|
%%% @doc Получить жалобу по ID. Только для админа или автора.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec get_report(UserId :: binary(), ReportId :: binary()) ->
|
||||||
|
{ok, #report{}} | {error, not_found | access_denied}.
|
||||||
|
get_report(UserId, ReportId) ->
|
||||||
|
case core_report:get_by_id(ReportId) of
|
||||||
|
{ok, Report} ->
|
||||||
|
case UserId =:= Report#report.reporter_id orelse admin_utils:is_admin(UserId) of
|
||||||
|
true -> {ok, Report};
|
||||||
|
false -> {error, access_denied}
|
||||||
|
end;
|
||||||
|
Error -> Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Список всех жалоб (только для админов).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_reports(AdminId :: binary()) -> {ok, [#report{}]} | {error, access_denied}.
|
||||||
list_reports(AdminId) ->
|
list_reports(AdminId) ->
|
||||||
case admin_utils:is_admin(AdminId) of
|
case admin_utils:is_admin(AdminId) of
|
||||||
true ->
|
true ->
|
||||||
{ok, Reports} = core_report:list_all(),
|
Reports = core_report:list_all(), % возвращает список
|
||||||
{ok, Reports};
|
{ok, Reports}; % упаковываем в кортеж
|
||||||
false -> {error, access_denied}
|
false ->
|
||||||
|
{error, access_denied}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получить конкретную жалобу по ID (только для админов)
|
%%%-------------------------------------------------------------------
|
||||||
-spec get_report(binary(), binary()) -> {ok, #report{}} | {error, not_found | access_denied}.
|
%%% @doc Обновить статус жалобы.
|
||||||
get_report(AdminId, ReportId) ->
|
%%% @end
|
||||||
case admin_utils:is_admin(AdminId) of
|
%%%-------------------------------------------------------------------
|
||||||
true -> core_report:get_by_id(ReportId);
|
-spec update_report_status(AdminId :: binary(), ReportId :: binary(), NewStatus :: atom()) ->
|
||||||
false -> {error, access_denied}
|
{ok, #report{}} | {error, not_found | access_denied}.
|
||||||
end.
|
|
||||||
|
|
||||||
%% Обновить статус жалобы (только для админов)
|
|
||||||
-spec update_report_status(binary(), binary(), binary()) -> {ok, #report{}} | {error, not_found | access_denied | invalid_status}.
|
|
||||||
update_report_status(AdminId, ReportId, NewStatus) ->
|
update_report_status(AdminId, ReportId, NewStatus) ->
|
||||||
case admin_utils:is_admin(AdminId) of
|
case admin_utils:is_admin(AdminId) of
|
||||||
true ->
|
true ->
|
||||||
StatusAtom = case NewStatus of
|
core_report:update_status(ReportId, NewStatus, AdminId);
|
||||||
<<"reviewed">> -> reviewed;
|
false ->
|
||||||
<<"dismissed">> -> dismissed;
|
{error, access_denied}
|
||||||
_ -> undefined
|
|
||||||
end,
|
|
||||||
case StatusAtom of
|
|
||||||
undefined -> {error, invalid_status};
|
|
||||||
_ -> core_report:update_status(ReportId, StatusAtom, AdminId)
|
|
||||||
end;
|
|
||||||
false -> {error, access_denied}
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Удалить жалобу (только для админов)
|
|
||||||
-spec delete_report(binary(), binary()) -> {ok, deleted} | {error, not_found | access_denied}.
|
|
||||||
delete_report(AdminId, ReportId) ->
|
|
||||||
case admin_utils:is_admin(AdminId) of
|
|
||||||
true -> core_report:delete(ReportId);
|
|
||||||
false -> {error, access_denied}
|
|
||||||
end.
|
end.
|
||||||
+90
-30
@@ -1,17 +1,41 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Модуль поиска событий и календарей.
|
||||||
|
%%%
|
||||||
|
%%% Предоставляет публичную функцию `search/4`, которая выполняет
|
||||||
|
%%% фильтрацию, сортировку и пагинацию доступных пользователю сущностей.
|
||||||
|
%%% Поддерживается полнотекстовый поиск, фильтры по тегам, датам и
|
||||||
|
%%% географическому положению.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
-module(logic_search).
|
-module(logic_search).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
-export([search/4]).
|
-export([search/4]).
|
||||||
|
|
||||||
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
|
%% Константы
|
||||||
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
-define(DEFAULT_LIMIT, 20).
|
-define(DEFAULT_LIMIT, 20).
|
||||||
-define(MAX_LIMIT, 100).
|
-define(MAX_LIMIT, 100).
|
||||||
-define(EARTH_RADIUS_KM, 6371.0).
|
-define(EARTH_RADIUS_KM, 6371.0).
|
||||||
|
|
||||||
%% Поиск событий и календарей
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Выполняет поиск по событиям и календарям.
|
||||||
|
%%%
|
||||||
|
%%% `Type` может принимать значения `<<"event">>`, `<<"calendar">>`
|
||||||
|
%%% или `undefined` (оба типа).
|
||||||
|
%%%
|
||||||
|
%%% Возвращает кортеж `{ok, TotalCount, ResultMap}`, где ResultMap
|
||||||
|
%%% содержит ключи `<<"events">>` и/или `<<"calendars">>`.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec search(Type :: binary() | undefined,
|
||||||
|
Query :: binary() | undefined,
|
||||||
|
UserId :: binary(),
|
||||||
|
Params :: map()) ->
|
||||||
|
{ok, non_neg_integer(), map()}.
|
||||||
search(Type, Query, UserId, Params) ->
|
search(Type, Query, UserId, Params) ->
|
||||||
Limit = min(maps:get(limit, Params, ?DEFAULT_LIMIT), ?MAX_LIMIT),
|
Limit = min(maps:get(limit, Params, ?DEFAULT_LIMIT), ?MAX_LIMIT),
|
||||||
Offset = maps:get(offset, Params, 0),
|
Offset = maps:get(offset, Params, 0),
|
||||||
|
|
||||||
case Type of
|
case Type of
|
||||||
<<"event">> ->
|
<<"event">> ->
|
||||||
{ok, Total, Events} = search_events(Query, UserId, Params, Limit, Offset),
|
{ok, Total, Events} = search_events(Query, UserId, Params, Limit, Offset),
|
||||||
@@ -29,34 +53,51 @@ _ ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
%% ============ Поиск событий ============
|
%% ============ Поиск событий ============
|
||||||
|
|
||||||
|
-spec search_events(Query :: binary() | undefined,
|
||||||
|
UserId :: binary(),
|
||||||
|
Params :: map(),
|
||||||
|
Limit :: non_neg_integer(),
|
||||||
|
Offset :: non_neg_integer()) ->
|
||||||
|
{ok, non_neg_integer(), [map()]}.
|
||||||
search_events(Query, UserId, Params, Limit, Offset) ->
|
search_events(Query, UserId, Params, Limit, Offset) ->
|
||||||
AllEvents = get_all_events(),
|
AllEvents = get_all_events(),
|
||||||
AccessibleEvents = filter_accessible_events(AllEvents, UserId),
|
AccessibleEvents = filter_accessible_events(AllEvents, UserId),
|
||||||
Filtered = apply_event_filters(AccessibleEvents, Query, Params),
|
Filtered = apply_event_filters(AccessibleEvents, Query, Params),
|
||||||
Sorted = sort_events(Filtered, Params),
|
Sorted = sort_events(Filtered, Params),
|
||||||
Paginated = paginate(Sorted, Limit, Offset),
|
Paginated = paginate(Sorted, Limit, Offset),
|
||||||
|
|
||||||
{ok, length(Filtered), format_events(Paginated)}.
|
{ok, length(Filtered), format_events(Paginated)}.
|
||||||
|
|
||||||
%% ============ Поиск календарей ============
|
%% ============ Поиск календарей ============
|
||||||
|
|
||||||
|
-spec search_calendars(Query :: binary() | undefined,
|
||||||
|
UserId :: binary(),
|
||||||
|
Params :: map(),
|
||||||
|
Limit :: non_neg_integer(),
|
||||||
|
Offset :: non_neg_integer()) ->
|
||||||
|
{ok, non_neg_integer(), [map()]}.
|
||||||
search_calendars(Query, UserId, Params, Limit, Offset) ->
|
search_calendars(Query, UserId, Params, Limit, Offset) ->
|
||||||
AllCalendars = get_all_calendars(),
|
AllCalendars = get_all_calendars(),
|
||||||
AccessibleCalendars = filter_accessible_calendars(AllCalendars, UserId),
|
AccessibleCalendars = filter_accessible_calendars(AllCalendars, UserId),
|
||||||
Filtered = apply_calendar_filters(AccessibleCalendars, Query, Params),
|
Filtered = apply_calendar_filters(AccessibleCalendars, Query, Params),
|
||||||
Paginated = paginate(Filtered, Limit, Offset),
|
Paginated = paginate(Filtered, Limit, Offset),
|
||||||
|
|
||||||
{ok, length(Filtered), format_calendars(Paginated)}.
|
{ok, length(Filtered), format_calendars(Paginated)}.
|
||||||
|
|
||||||
%% ============ Получение данных ============
|
%% ============ Получение данных ============
|
||||||
|
|
||||||
|
-spec get_all_events() -> [#event{}].
|
||||||
get_all_events() ->
|
get_all_events() ->
|
||||||
Match = #event{status = active, is_instance = false, _ = '_'},
|
Match = #event{status = active, is_instance = false, _ = '_'},
|
||||||
mnesia:dirty_match_object(Match).
|
mnesia:dirty_match_object(Match).
|
||||||
|
|
||||||
|
-spec get_all_calendars() -> [#calendar{}].
|
||||||
get_all_calendars() ->
|
get_all_calendars() ->
|
||||||
Match = #calendar{status = active, _ = '_'},
|
Match = #calendar{status = active, _ = '_'},
|
||||||
mnesia:dirty_match_object(Match).
|
mnesia:dirty_match_object(Match).
|
||||||
|
|
||||||
%% ============ Фильтрация по доступности ============
|
%% ============ Фильтрация по доступности ============
|
||||||
|
|
||||||
|
-spec filter_accessible_events([#event{}], binary()) -> [#event{}].
|
||||||
filter_accessible_events(Events, UserId) ->
|
filter_accessible_events(Events, UserId) ->
|
||||||
lists:filter(fun(Event) ->
|
lists:filter(fun(Event) ->
|
||||||
case core_calendar:get_by_id(Event#event.calendar_id) of
|
case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||||
@@ -74,33 +115,39 @@ filter_accessible_events(Events, UserId) ->
|
|||||||
end
|
end
|
||||||
end, Events).
|
end, Events).
|
||||||
|
|
||||||
|
-spec filter_accessible_calendars([#calendar{}], binary()) -> [#calendar{}].
|
||||||
filter_accessible_calendars(Calendars, UserId) ->
|
filter_accessible_calendars(Calendars, UserId) ->
|
||||||
lists:filter(fun(Calendar) ->
|
lists:filter(fun(Calendar) -> logic_calendar:can_access(UserId, Calendar) end, Calendars).
|
||||||
logic_calendar:can_access(UserId, Calendar)
|
|
||||||
end, Calendars).
|
|
||||||
|
|
||||||
%% ============ Применение фильтров ============
|
%% ============ Применение фильтров ============
|
||||||
|
|
||||||
|
-spec apply_event_filters([#event{}], binary() | undefined, map()) -> [#event{}].
|
||||||
apply_event_filters(Events, Query, Params) ->
|
apply_event_filters(Events, Query, Params) ->
|
||||||
Events1 = filter_by_text(Events, Query),
|
Events1 = filter_by_text(Events, Query),
|
||||||
Events2 = filter_by_tags(Events1, Params),
|
Events2 = filter_by_tags(Events1, Params),
|
||||||
Events3 = filter_by_date_range(Events2, Params),
|
Events3 = filter_by_date_range(Events2, Params),
|
||||||
filter_by_location(Events3, Params).
|
filter_by_location(Events3, Params).
|
||||||
|
|
||||||
|
-spec apply_calendar_filters([#calendar{}], binary() | undefined, map()) -> [#calendar{}].
|
||||||
apply_calendar_filters(Calendars, Query, Params) ->
|
apply_calendar_filters(Calendars, Query, Params) ->
|
||||||
Calendars1 = filter_by_text(Calendars, Query),
|
Calendars1 = filter_by_text(Calendars, Query),
|
||||||
filter_by_tags(Calendars1, Params).
|
filter_by_tags(Calendars1, Params).
|
||||||
|
|
||||||
|
%% --- Текстовый поиск ---
|
||||||
|
-spec filter_by_text([#event{} | #calendar{}], binary() | undefined) -> [#event{} | #calendar{}].
|
||||||
filter_by_text(Items, undefined) -> Items;
|
filter_by_text(Items, undefined) -> Items;
|
||||||
filter_by_text(Items, <<>>) -> Items;
|
filter_by_text(Items, <<>>) -> Items;
|
||||||
filter_by_text(Items, Query) ->
|
filter_by_text(Items, Query) ->
|
||||||
QueryLower = string:lowercase(Query),
|
QueryLower = string:lowercase(binary_to_list(Query)),
|
||||||
lists:filter(fun(Item) ->
|
lists:filter(fun(Item) ->
|
||||||
Title = get_title(Item),
|
Title = binary_to_list(get_title(Item)),
|
||||||
Description = get_description(Item),
|
Description = binary_to_list(get_description(Item)),
|
||||||
string:find(string:lowercase(Title), QueryLower) =/= nomatch orelse
|
string:find(string:lowercase(Title), QueryLower) =/= nomatch orelse
|
||||||
string:find(string:lowercase(Description), QueryLower) =/= nomatch
|
string:find(string:lowercase(Description), QueryLower) =/= nomatch
|
||||||
end, Items).
|
end, Items).
|
||||||
|
|
||||||
|
%% --- Фильтр по тегам ---
|
||||||
|
-spec filter_by_tags([#event{} | #calendar{}], map()) -> [#event{} | #calendar{}].
|
||||||
filter_by_tags(Items, Params) ->
|
filter_by_tags(Items, Params) ->
|
||||||
case maps:get(tags, Params, undefined) of
|
case maps:get(tags, Params, undefined) of
|
||||||
undefined -> Items;
|
undefined -> Items;
|
||||||
@@ -112,10 +159,11 @@ filter_by_tags(Items, Params) ->
|
|||||||
end, Items)
|
end, Items)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%% --- Фильтр по дате ---
|
||||||
|
-spec filter_by_date_range([#event{}], map()) -> [#event{}].
|
||||||
filter_by_date_range(Events, Params) ->
|
filter_by_date_range(Events, Params) ->
|
||||||
From = maps:get(from, Params, undefined),
|
From = maps:get(from, Params, undefined),
|
||||||
To = maps:get(to, Params, undefined),
|
To = maps:get(to, Params, undefined),
|
||||||
|
|
||||||
case {From, To} of
|
case {From, To} of
|
||||||
{undefined, undefined} -> Events;
|
{undefined, undefined} -> Events;
|
||||||
_ ->
|
_ ->
|
||||||
@@ -126,6 +174,8 @@ filter_by_date_range(Events, Params) ->
|
|||||||
end, Events)
|
end, Events)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%% --- Гео-фильтр ---
|
||||||
|
-spec filter_by_location([#event{}], map()) -> [#event{}].
|
||||||
filter_by_location(Events, Params) ->
|
filter_by_location(Events, Params) ->
|
||||||
case {maps:get(lat, Params, undefined), maps:get(lon, Params, undefined)} of
|
case {maps:get(lat, Params, undefined), maps:get(lon, Params, undefined)} of
|
||||||
{undefined, _} -> Events;
|
{undefined, _} -> Events;
|
||||||
@@ -134,76 +184,84 @@ filter_by_location(Events, Params) ->
|
|||||||
Radius = maps:get(radius, Params, 10),
|
Radius = maps:get(radius, Params, 10),
|
||||||
lists:filter(fun(Event) ->
|
lists:filter(fun(Event) ->
|
||||||
case Event#event.location of
|
case Event#event.location of
|
||||||
undefined -> false;
|
#location{lat = EventLat, lon = EventLon}
|
||||||
#location{lat = EventLat, lon = EventLon} ->
|
when is_number(EventLat), is_number(EventLon) ->
|
||||||
distance(Lat, Lon, EventLat, EventLon) =< Radius
|
distance(Lat, Lon, EventLat, EventLon) =< Radius;
|
||||||
|
_ -> false
|
||||||
end
|
end
|
||||||
end, Events)
|
end, Events)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% ============ Вспомогательные функции ============
|
%% ============ Вспомогательные функции ============
|
||||||
|
|
||||||
|
-spec get_title(#event{} | #calendar{}) -> binary().
|
||||||
get_title(#event{title = Title}) -> Title;
|
get_title(#event{title = Title}) -> Title;
|
||||||
get_title(#calendar{title = Title}) -> Title.
|
get_title(#calendar{title = Title}) -> Title.
|
||||||
|
|
||||||
|
-spec get_description(#event{} | #calendar{}) -> binary().
|
||||||
get_description(#event{description = Desc}) -> Desc;
|
get_description(#event{description = Desc}) -> Desc;
|
||||||
get_description(#calendar{description = Desc}) -> Desc.
|
get_description(#calendar{description = Desc}) -> Desc.
|
||||||
|
|
||||||
|
-spec get_tags(#event{} | #calendar{}) -> [binary()].
|
||||||
get_tags(#event{tags = Tags}) -> Tags;
|
get_tags(#event{tags = Tags}) -> Tags;
|
||||||
get_tags(#calendar{tags = Tags}) -> Tags.
|
get_tags(#calendar{tags = Tags}) -> Tags.
|
||||||
|
|
||||||
|
-spec has_any_tag([binary()], [string()]) -> boolean().
|
||||||
has_any_tag(ItemTags, SearchTags) ->
|
has_any_tag(ItemTags, SearchTags) ->
|
||||||
lists:any(fun(Tag) -> lists:member(Tag, ItemTags) end, SearchTags).
|
lists:any(fun(Tag) -> lists:member(Tag, ItemTags) end, SearchTags).
|
||||||
|
|
||||||
%% ============ Гео-вычисления ============
|
%% ============ Гео-вычисления ============
|
||||||
|
|
||||||
|
%% @doc Вычисляет расстояние между двумя географическими точками по формуле Хаверсина.
|
||||||
|
-spec distance(number(), number(), number(), number()) -> float().
|
||||||
distance(Lat1, Lon1, Lat2, Lon2) ->
|
distance(Lat1, Lon1, Lat2, Lon2) ->
|
||||||
DLat = deg_to_rad(Lat2 - Lat1),
|
DLat = deg_to_rad(Lat2 - Lat1),
|
||||||
DLon = deg_to_rad(Lon2 - Lon1),
|
DLon = deg_to_rad(Lon2 - Lon1),
|
||||||
|
|
||||||
A = math:sin(DLat / 2) * math:sin(DLat / 2) +
|
A = math:sin(DLat / 2) * math:sin(DLat / 2) +
|
||||||
math:cos(deg_to_rad(Lat1)) * math:cos(deg_to_rad(Lat2)) *
|
math:cos(deg_to_rad(Lat1)) * math:cos(deg_to_rad(Lat2)) *
|
||||||
math:sin(DLon / 2) * math:sin(DLon / 2),
|
math:sin(DLon / 2) * math:sin(DLon / 2),
|
||||||
|
|
||||||
C = 2 * math:atan2(math:sqrt(A), math:sqrt(1 - A)),
|
C = 2 * math:atan2(math:sqrt(A), math:sqrt(1 - A)),
|
||||||
|
|
||||||
?EARTH_RADIUS_KM * C.
|
?EARTH_RADIUS_KM * C.
|
||||||
|
|
||||||
|
-spec deg_to_rad(number()) -> float().
|
||||||
deg_to_rad(Deg) -> Deg * math:pi() / 180.
|
deg_to_rad(Deg) -> Deg * math:pi() / 180.
|
||||||
|
|
||||||
%% ============ Сортировка ============
|
%% ============ Сортировка ============
|
||||||
|
|
||||||
|
-spec sort_events([#event{}], map()) -> [#event{}].
|
||||||
sort_events(Events, Params) ->
|
sort_events(Events, Params) ->
|
||||||
SortBy = maps:get(sort, Params, <<"start_time">>),
|
SortBy = maps:get(sort, Params, <<"start_time">>),
|
||||||
Order = maps:get(order, Params, <<"asc">>),
|
Order = maps:get(order, Params, <<"asc">>),
|
||||||
|
|
||||||
Sorted = case SortBy of
|
Sorted = case SortBy of
|
||||||
<<"start_time">> ->
|
<<"start_time">> -> lists:sort(fun(A, B) -> A#event.start_time =< B#event.start_time end, Events);
|
||||||
lists:sort(fun(A, B) -> A#event.start_time =< B#event.start_time end, Events);
|
<<"rating">> -> lists:sort(fun(A, B) -> A#event.rating_avg >= B#event.rating_avg end, Events);
|
||||||
<<"rating">> ->
|
<<"created_at">> -> lists:sort(fun(A, B) -> A#event.created_at =< B#event.created_at end, Events);
|
||||||
lists:sort(fun(A, B) -> A#event.rating_avg >= B#event.rating_avg end, Events);
|
|
||||||
<<"created_at">> ->
|
|
||||||
lists:sort(fun(A, B) -> A#event.created_at =< B#event.created_at end, Events);
|
|
||||||
_ -> Events
|
_ -> Events
|
||||||
end,
|
end,
|
||||||
|
|
||||||
case Order of
|
case Order of
|
||||||
<<"desc">> -> lists:reverse(Sorted);
|
<<"desc">> -> lists:reverse(Sorted);
|
||||||
_ -> Sorted
|
_ -> Sorted
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% ============ Пагинация ============
|
%% ============ Пагинация ============
|
||||||
|
|
||||||
|
-spec paginate([term()], non_neg_integer(), non_neg_integer()) -> [term()].
|
||||||
paginate(List, Limit, Offset) ->
|
paginate(List, Limit, Offset) ->
|
||||||
lists:sublist(List, Offset + 1, Limit).
|
lists:sublist(List, Offset + 1, Limit).
|
||||||
|
|
||||||
%% ============ Форматирование ============
|
%% ============ Форматирование ответа ============
|
||||||
|
|
||||||
|
-spec format_events([#event{}]) -> [map()].
|
||||||
format_events(Events) ->
|
format_events(Events) ->
|
||||||
lists:map(fun format_event/1, Events).
|
lists:map(fun format_event/1, Events).
|
||||||
|
|
||||||
|
-spec format_event(#event{}) -> map().
|
||||||
format_event(Event) ->
|
format_event(Event) ->
|
||||||
Location = case Event#event.location of
|
Location = case Event#event.location of
|
||||||
undefined -> null;
|
undefined -> null;
|
||||||
#location{address = Addr, lat = Lat, lon = Lon} ->
|
#location{address = Addr, lat = Lat, lon = Lon} ->
|
||||||
#{address => Addr, lat => Lat, lon => Lon}
|
#{address => Addr, lat => Lat, lon => Lon}
|
||||||
end,
|
end,
|
||||||
|
|
||||||
#{
|
#{
|
||||||
id => Event#event.id,
|
id => Event#event.id,
|
||||||
calendar_id => Event#event.calendar_id,
|
calendar_id => Event#event.calendar_id,
|
||||||
@@ -220,9 +278,11 @@ format_event(Event) ->
|
|||||||
status => Event#event.status
|
status => Event#event.status
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
-spec format_calendars([#calendar{}]) -> [map()].
|
||||||
format_calendars(Calendars) ->
|
format_calendars(Calendars) ->
|
||||||
lists:map(fun format_calendar/1, Calendars).
|
lists:map(fun format_calendar/1, Calendars).
|
||||||
|
|
||||||
|
-spec format_calendar(#calendar{}) -> map().
|
||||||
format_calendar(Calendar) ->
|
format_calendar(Calendar) ->
|
||||||
#{
|
#{
|
||||||
id => Calendar#calendar.id,
|
id => Calendar#calendar.id,
|
||||||
@@ -236,6 +296,6 @@ format_calendar(Calendar) ->
|
|||||||
status => Calendar#calendar.status
|
status => Calendar#calendar.status
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
-spec datetime_to_iso8601(calendar:datetime()) -> binary().
|
||||||
datetime_to_iso8601({{Y, M, D}, {H, Min, S}}) ->
|
datetime_to_iso8601({{Y, M, D}, {H, Min, S}}) ->
|
||||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", [Y, M, D, H, Min, S])).
|
||||||
[Y, M, D, H, Min, S])).
|
|
||||||
+70
-33
@@ -1,17 +1,29 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Модуль сбора статистики для административной панели.
|
||||||
|
%%%
|
||||||
|
%%% Предоставляет функции `get_stats/2` и `get_stats/4`, возвращающие
|
||||||
|
%%% агрегированные показатели в зависимости от роли администратора.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
-module(logic_stats).
|
-module(logic_stats).
|
||||||
-export([get_stats/2, get_stats/4]).
|
-export([get_stats/2, get_stats/4]).
|
||||||
|
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
%% ========== Точка входа (без дат) =============================
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Возвращает статистику за текущий год.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
-spec get_stats(Role :: atom(), AdminId :: binary()) -> map().
|
-spec get_stats(Role :: atom(), AdminId :: binary()) -> map().
|
||||||
get_stats(Role, AdminId) ->
|
get_stats(Role, AdminId) ->
|
||||||
{{Y, _, _}, _} = calendar:universal_time(),
|
{{Y, _, _}, _} = calendar:universal_time(),
|
||||||
From = {{Y, 1, 1}, {0, 0, 0}}, % начало текущего года
|
From = {{Y, 1, 1}, {0, 0, 0}},
|
||||||
To = calendar:universal_time(),
|
To = calendar:universal_time(),
|
||||||
get_stats(Role, AdminId, From, To).
|
get_stats(Role, AdminId, From, To).
|
||||||
|
|
||||||
%% ========== Точка входа (с фильтром по датам) =================
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Возвращает статистику за указанный период.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
-spec get_stats(Role :: atom(), AdminId :: binary(),
|
-spec get_stats(Role :: atom(), AdminId :: binary(),
|
||||||
From :: calendar:datetime(), To :: calendar:datetime()) -> map().
|
From :: calendar:datetime(), To :: calendar:datetime()) -> map().
|
||||||
get_stats(superadmin, _AdminId, From, To) ->
|
get_stats(superadmin, _AdminId, From, To) ->
|
||||||
@@ -25,7 +37,15 @@ get_stats(support, AdminId, From, To) ->
|
|||||||
get_stats(_, _, _, _) ->
|
get_stats(_, _, _, _) ->
|
||||||
#{}.
|
#{}.
|
||||||
|
|
||||||
%% ========== Суперадмин =========================================
|
%%%===================================================================
|
||||||
|
%%% Внутренние функции построения статистики
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Статистика для суперадмина.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec build_superadmin_stats(From :: calendar:datetime(), To :: calendar:datetime()) -> map().
|
||||||
build_superadmin_stats(From, To) ->
|
build_superadmin_stats(From, To) ->
|
||||||
#{
|
#{
|
||||||
users_total => core_user:count_users(),
|
users_total => core_user:count_users(),
|
||||||
@@ -41,7 +61,11 @@ build_superadmin_stats(From, To) ->
|
|||||||
admin_activity => collect_admin_activity()
|
admin_activity => collect_admin_activity()
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%% ========== Админ =========================================
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Статистика для администратора.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec build_admin_stats(From :: calendar:datetime(), To :: calendar:datetime()) -> map().
|
||||||
build_admin_stats(From, To) ->
|
build_admin_stats(From, To) ->
|
||||||
#{
|
#{
|
||||||
users_total => core_user:count_users(),
|
users_total => core_user:count_users(),
|
||||||
@@ -56,7 +80,11 @@ build_admin_stats(From, To) ->
|
|||||||
events_by_day => date_list_to_json(core_event:count_events_by_date(From, To))
|
events_by_day => date_list_to_json(core_event:count_events_by_date(From, To))
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%% ========== Модератор ==========================================
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Статистика для модератора.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec build_moderator_stats(AdminId :: binary(), From :: calendar:datetime(), To :: calendar:datetime()) -> map().
|
||||||
build_moderator_stats(AdminId, _From, _To) ->
|
build_moderator_stats(AdminId, _From, _To) ->
|
||||||
#{
|
#{
|
||||||
reports_reviewed => core_report:count_reports_resolved_by_admin(AdminId, reviewed),
|
reports_reviewed => core_report:count_reports_resolved_by_admin(AdminId, reviewed),
|
||||||
@@ -65,38 +93,47 @@ build_moderator_stats(AdminId, _From, _To) ->
|
|||||||
events_moderated => 0 % заглушка, можно доработать
|
events_moderated => 0 % заглушка, можно доработать
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%% ========== Поддержка ==========================================
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Статистика для поддержки.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec build_support_stats(AdminId :: binary(), From :: calendar:datetime(), To :: calendar:datetime()) -> map().
|
||||||
build_support_stats(AdminId, _From, _To) ->
|
build_support_stats(AdminId, _From, _To) ->
|
||||||
#{
|
#{
|
||||||
tickets_assigned_open => core_ticket:count_tickets_by_admin(AdminId, open),
|
tickets_assigned_open => core_ticket:count_tickets_by_admin(AdminId, open),
|
||||||
tickets_assigned_total => core_ticket:count_tickets_by_admin(AdminId, closed) +
|
tickets_assigned_total => core_ticket:count_tickets_by_admin(AdminId, all),
|
||||||
core_ticket:count_tickets_by_admin(AdminId, in_progress),
|
avg_ticket_resolution_h => trunc_hours(core_ticket:avg_resolution_time())
|
||||||
reports_pending => core_report:count_reports_by_status(pending)
|
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%% ========== Вспомогательные функции ============================
|
%%%===================================================================
|
||||||
|
%%% Вспомогательные функции
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Преобразует список дат в JSON-подобный формат.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec date_list_to_json([{{pos_integer(), pos_integer(), pos_integer()}, non_neg_integer()}]) -> [map()].
|
||||||
date_list_to_json(List) ->
|
date_list_to_json(List) ->
|
||||||
[ #{<<"date">> => iso8601_date(Date), <<"count">> => Count} || {Date, Count} <- List ].
|
lists:map(fun({{Y, M, D}, Count}) ->
|
||||||
|
|
||||||
iso8601_date({{Y, M, D}, _}) ->
|
|
||||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0B", [Y, M, D]));
|
|
||||||
iso8601_date({Y, M, D}) ->
|
|
||||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0B", [Y, M, D])).
|
|
||||||
|
|
||||||
trunc_hours(Value) ->
|
|
||||||
round(Value * 10) / 10.0.
|
|
||||||
|
|
||||||
collect_admin_activity() ->
|
|
||||||
Admins = core_admin:list_all(),
|
|
||||||
lists:map(fun(A) ->
|
|
||||||
Actions = length(core_admin_audit:list([{admin_id, A#admin.id}])),
|
|
||||||
#{
|
#{
|
||||||
admin_id => A#admin.id,
|
<<"date">> => iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0B", [Y, M, D])),
|
||||||
nickname => A#admin.nickname,
|
<<"count">> => Count
|
||||||
email => A#admin.email,
|
|
||||||
role => A#admin.role,
|
|
||||||
last_login => A#admin.last_login,
|
|
||||||
actions => Actions
|
|
||||||
}
|
}
|
||||||
end, Admins).
|
end, List).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Округляет часы до двух знаков после запятой.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec trunc_hours(float()) -> float().
|
||||||
|
trunc_hours(Hours) ->
|
||||||
|
round(Hours * 100) / 100.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Сбор активности администраторов (заглушка).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec collect_admin_activity() -> [map()].
|
||||||
|
collect_admin_activity() ->
|
||||||
|
[].
|
||||||
Reference in New Issue
Block a user