增加api接口
This commit is contained in:
parent
53ba52712d
commit
b725499dfe
15
apps/dimension_apn/include/dimension_tables.hrl
Normal file
15
apps/dimension_apn/include/dimension_tables.hrl
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author anlicheng
|
||||||
|
%%% @copyright (C) 2025, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 07. 4月 2025 14:34
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-author("anlicheng").
|
||||||
|
|
||||||
|
-record(device_token, {
|
||||||
|
user_id :: binary(),
|
||||||
|
token :: binary(),
|
||||||
|
timestamp = 0 :: integer()
|
||||||
|
}).
|
||||||
115
apps/dimension_apn/src/api_handler.erl
Normal file
115
apps/dimension_apn/src/api_handler.erl
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author licheng5
|
||||||
|
%%% @copyright (C) 2020, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 26. 4月 2020 3:36 下午
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(api_handler).
|
||||||
|
-author("licheng5").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([init/2]).
|
||||||
|
|
||||||
|
init(Req0, Opts) ->
|
||||||
|
Method = binary_to_list(cowboy_req:method(Req0)),
|
||||||
|
Path = binary_to_list(cowboy_req:path(Req0)),
|
||||||
|
GetParams0 = cowboy_req:parse_qs(Req0),
|
||||||
|
GetParams = maps:from_list(GetParams0),
|
||||||
|
{ok, PostParams, Req1} = parse_body(Req0),
|
||||||
|
|
||||||
|
try handle_request(Method, Path, GetParams, PostParams) of
|
||||||
|
{ok, StatusCode, Resp} ->
|
||||||
|
lager:debug("[http_protocol] request path: ~p, get_params: ~p, post_params: ~p, response: ~ts",
|
||||||
|
[Path, GetParams, PostParams, Resp]),
|
||||||
|
AcceptEncoding = cowboy_req:header(<<"accept-encoding">>, Req1, <<>>),
|
||||||
|
Req2 = case iolist_size(Resp) >= 1024 andalso supported_gzip(AcceptEncoding) of
|
||||||
|
true ->
|
||||||
|
Resp1 = zlib:gzip(Resp),
|
||||||
|
cowboy_req:reply(StatusCode, #{
|
||||||
|
<<"Content-Type">> => <<"application/json;charset=utf-8">>,
|
||||||
|
<<"Content-Encoding">> => <<"gzip">>
|
||||||
|
}, Resp1, Req1);
|
||||||
|
false ->
|
||||||
|
cowboy_req:reply(StatusCode, #{
|
||||||
|
<<"Content-Type">> => <<"application/json;charset=utf-8">>
|
||||||
|
}, Resp, Req1)
|
||||||
|
end,
|
||||||
|
{ok, Req2, Opts}
|
||||||
|
catch
|
||||||
|
throw:Error ->
|
||||||
|
ErrResp = dimension_utils:json_error(-1, Error),
|
||||||
|
Req2 = cowboy_req:reply(404, #{
|
||||||
|
<<"Content-Type">> => <<"application/json;charset=utf-8">>
|
||||||
|
}, ErrResp, Req1),
|
||||||
|
{ok, Req2, Opts};
|
||||||
|
_:Error:Stack ->
|
||||||
|
lager:warning("[http_handler] get error: ~p, stack: ~p", [Error, Stack]),
|
||||||
|
Req2 = cowboy_req:reply(500, #{
|
||||||
|
<<"Content-Type">> => <<"text/html;charset=utf-8">>
|
||||||
|
}, <<"Internal Server Error">>, Req1),
|
||||||
|
{ok, Req2, Opts}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 判断是否支持gzip
|
||||||
|
supported_gzip(AcceptEncoding) when is_binary(AcceptEncoding) ->
|
||||||
|
binary:match(AcceptEncoding, <<"gzip">>) =/= nomatch.
|
||||||
|
|
||||||
|
parse_body(Req0) ->
|
||||||
|
ContentType = cowboy_req:header(<<"content-type">>, Req0),
|
||||||
|
case ContentType of
|
||||||
|
<<"application/json", _/binary>> ->
|
||||||
|
{ok, Body, Req1} = read_body(Req0),
|
||||||
|
{ok, catch jiffy:decode(Body, [return_maps]), Req1};
|
||||||
|
<<"application/x-www-form-urlencoded">> ->
|
||||||
|
{ok, PostParams0, Req1} = cowboy_req:read_urlencoded_body(Req0),
|
||||||
|
PostParams = maps:from_list(PostParams0),
|
||||||
|
{ok, PostParams, Req1};
|
||||||
|
_ ->
|
||||||
|
{ok, #{}, Req0}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 读取请求体
|
||||||
|
read_body(Req) ->
|
||||||
|
read_body(Req, <<>>).
|
||||||
|
read_body(Req, AccData) ->
|
||||||
|
case cowboy_req:read_body(Req) of
|
||||||
|
{ok, Data, Req1} ->
|
||||||
|
{ok, <<AccData/binary, Data/binary>>, Req1};
|
||||||
|
{more, Data, Req1} ->
|
||||||
|
read_body(Req1, <<AccData/binary, Data/binary>>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% helper methods
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
%% 更新token信息
|
||||||
|
handle_request("POST", "/api/device_token", _, #{<<"user_id">> := UserId, <<"token">> := Token}) ->
|
||||||
|
case mnesia_device_token:insert(UserId, Token, dimension_utils:current_time()) of
|
||||||
|
ok ->
|
||||||
|
{ok, 200, dimension_utils:json_data(<<"OK">>)};
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:notice("[api_handler] insert user_id: ~p, token: ~p, error: ~p", [UserId, Token, Reason]),
|
||||||
|
{ok, 200, dimension_utils:json_error(-1, <<"更新token失败"/utf8>>)}
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% 向用户推送数据
|
||||||
|
handle_request("POST", "/api/push", _, PushList) ->
|
||||||
|
|
||||||
|
Title = <<"动物狂响曲"/utf8>>,
|
||||||
|
Body = <<"第7集(校服与被毛更深处),bilibili已更新"/utf8>>,
|
||||||
|
push(DeviceToken, Title, Body).
|
||||||
|
|
||||||
|
case mnesia_device_token:insert(UserId, Token, dimension_utils:current_time()) of
|
||||||
|
ok ->
|
||||||
|
{ok, 200, dimension_utils:json_data(<<"OK">>)};
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:notice("[api_handler] insert user_id: ~p, token: ~p, error: ~p", [UserId, Token, Reason]),
|
||||||
|
{ok, 200, dimension_utils:json_error(-1, <<"更新token失败"/utf8>>)}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_request(_, Path, _, _) ->
|
||||||
|
Path1 = list_to_binary(Path),
|
||||||
|
{ok, 200, dimension_utils:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||||
@ -13,6 +13,7 @@
|
|||||||
ssl,
|
ssl,
|
||||||
jiffy,
|
jiffy,
|
||||||
apns,
|
apns,
|
||||||
|
mnesia,
|
||||||
kernel,
|
kernel,
|
||||||
stdlib
|
stdlib
|
||||||
]},
|
]},
|
||||||
|
|||||||
@ -10,6 +10,11 @@
|
|||||||
-export([start/2, stop/1]).
|
-export([start/2, stop/1]).
|
||||||
|
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
|
io:setopts([{encoding, unicode}]),
|
||||||
|
%% 加速内存的回收
|
||||||
|
erlang:system_flag(fullsweep_after, 16),
|
||||||
|
|
||||||
|
start_mnesia(),
|
||||||
start_http_server(),
|
start_http_server(),
|
||||||
dimension_apn_sup:start_link().
|
dimension_apn_sup:start_link().
|
||||||
|
|
||||||
@ -29,10 +34,6 @@ start_http_server() ->
|
|||||||
Dispatcher = cowboy_router:compile([
|
Dispatcher = cowboy_router:compile([
|
||||||
{'_', [
|
{'_', [
|
||||||
{"/api/[...]", http_protocol, [api_handler]},
|
{"/api/[...]", http_protocol, [api_handler]},
|
||||||
{"/host/[...]", http_protocol, [host_handler]},
|
|
||||||
{"/device/[...]", http_protocol, [device_handler]},
|
|
||||||
{"/totalizator/[...]", http_protocol, [totalizator_handler]},
|
|
||||||
{"/test/[...]", http_protocol, [test_handler]},
|
|
||||||
{"/ws", ws_channel, []}
|
{"/ws", ws_channel, []}
|
||||||
]}
|
]}
|
||||||
]),
|
]),
|
||||||
@ -47,3 +48,12 @@ start_http_server() ->
|
|||||||
{ok, Pid} = cowboy:start_clear(http_listener, TransOpts, #{env => #{dispatch => Dispatcher}}),
|
{ok, Pid} = cowboy:start_clear(http_listener, TransOpts, #{env => #{dispatch => Dispatcher}}),
|
||||||
|
|
||||||
lager:debug("[dimension_apn] the http server start at: ~p, pid is: ~p", [Port, Pid]).
|
lager:debug("[dimension_apn] the http server start at: ~p, pid is: ~p", [Port, Pid]).
|
||||||
|
|
||||||
|
%% 启动内存数据库
|
||||||
|
start_mnesia() ->
|
||||||
|
%% 启动数据库
|
||||||
|
ok = mnesia:start(),
|
||||||
|
Tables = mnesia:system_info(tables),
|
||||||
|
%% 创建数据库表
|
||||||
|
not lists:member(device_token, Tables) andalso mnesia_device_token:create_table(),
|
||||||
|
ok.
|
||||||
|
|||||||
@ -4,23 +4,123 @@
|
|||||||
%%% @doc
|
%%% @doc
|
||||||
%%%
|
%%%
|
||||||
%%% @end
|
%%% @end
|
||||||
%%% Created : 03. 4月 2025 15:41
|
%%% Created : 07. 4月 2025 14:53
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(dimension_apn_pusher).
|
-module(dimension_apn_pusher).
|
||||||
-author("anlicheng").
|
-author("anlicheng").
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([push/3]).
|
-export([start_link/0]).
|
||||||
-export([test/1, test/0]).
|
-export([push/1]).
|
||||||
|
-export([test/0]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
test() ->
|
test() ->
|
||||||
test(<<"3ea61b396cc2455069df01f874f0ffeeb2cfa4937adba5f3af743b08148c8eb0">>).
|
UserId = <<"">>,
|
||||||
|
|
||||||
test(DeviceToken) ->
|
|
||||||
Title = <<"动物狂响曲"/utf8>>,
|
Title = <<"动物狂响曲"/utf8>>,
|
||||||
Body = <<"第7集(校服与被毛更深处),bilibili已更新"/utf8>>,
|
Body = <<"第7集(校服与被毛更深处),bilibili已更新"/utf8>>,
|
||||||
push(DeviceToken, Title, Body).
|
push([
|
||||||
|
#{
|
||||||
|
<<"user_id">> => UserId,
|
||||||
|
<<"title">> => Title,
|
||||||
|
<<"body">> => Body
|
||||||
|
}
|
||||||
|
]).
|
||||||
|
|
||||||
-spec push(DeviceToken :: binary(), Title :: binary(), Body :: binary()) -> Response :: apns:response().
|
-spec push(NotificationList :: list()) -> no_return().
|
||||||
push(DeviceToken, Title, Body) when is_binary(DeviceToken), is_binary(Title), is_binary(Body) ->
|
push(NotificationList) when is_list(NotificationList) ->
|
||||||
poolboy:transaction(apns_pool, fun(WorkerPid) -> dimension_apn_worker:push(WorkerPid, DeviceToken, Title, Body) end).
|
gen_server:cast(?SERVER, {push, NotificationList}).
|
||||||
|
|
||||||
|
%% @doc Spawns the server and registers the local name (unique)
|
||||||
|
-spec(start_link() ->
|
||||||
|
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% gen_server callbacks
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Initializes the server
|
||||||
|
-spec(init(Args :: term()) ->
|
||||||
|
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term()} | ignore).
|
||||||
|
init([]) ->
|
||||||
|
{ok, #state{}}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling call messages
|
||||||
|
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||||
|
State :: #state{}) ->
|
||||||
|
{reply, Reply :: term(), NewState :: #state{}} |
|
||||||
|
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_call(_Request, _From, State = #state{}) ->
|
||||||
|
{reply, ok, State}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling cast messages
|
||||||
|
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_cast({push, NotificationList}, State = #state{}) ->
|
||||||
|
lists:foreach(fun(#{<<"user_id">> := UserId, <<"title">> := Title, <<"body">> := Body}) ->
|
||||||
|
case mnesia_device_token:get_token(UserId) of
|
||||||
|
error ->
|
||||||
|
ok;
|
||||||
|
{ok, DeviceToken} ->
|
||||||
|
poolboy:transaction(apns_pool, fun(WorkerPid) -> dimension_apn_worker:push(WorkerPid, DeviceToken, Title, Body) end)
|
||||||
|
end
|
||||||
|
end, NotificationList),
|
||||||
|
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling all non call/cast messages
|
||||||
|
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_info(_Info, State = #state{}) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc This function is called by a gen_server when it is about to
|
||||||
|
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||||
|
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||||
|
%% with Reason. The return value is ignored.
|
||||||
|
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||||
|
State :: #state{}) -> term()).
|
||||||
|
terminate(_Reason, _State = #state{}) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Convert process state when code is changed
|
||||||
|
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||||
|
Extra :: term()) ->
|
||||||
|
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||||
|
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Internal functions
|
||||||
|
%%%===================================================================
|
||||||
|
|||||||
31
apps/dimension_apn/src/dimension_utils.erl
Normal file
31
apps/dimension_apn/src/dimension_utils.erl
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author anlicheng
|
||||||
|
%%% @copyright (C) 2025, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 07. 4月 2025 14:40
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(dimension_utils).
|
||||||
|
-author("anlicheng").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([current_time/0]).
|
||||||
|
-export([json_data/1, json_error/2]).
|
||||||
|
|
||||||
|
-spec current_time() -> integer().
|
||||||
|
current_time() ->
|
||||||
|
{Mega, Seconds, _Micro} = os:timestamp(),
|
||||||
|
Mega * 1000000 + Seconds.
|
||||||
|
|
||||||
|
json_data(Data) ->
|
||||||
|
Json = jiffy:encode(#{<<"result">> => Data}, [force_utf8]),
|
||||||
|
iolist_to_binary(Json).
|
||||||
|
|
||||||
|
json_error(ErrCode, ErrMessage) when is_integer(ErrCode), is_binary(ErrMessage) ->
|
||||||
|
jiffy:encode(#{
|
||||||
|
<<"error">> => #{
|
||||||
|
<<"code">> => ErrCode,
|
||||||
|
<<"message">> => ErrMessage
|
||||||
|
}
|
||||||
|
}, [force_utf8]).
|
||||||
42
apps/dimension_apn/src/mnesia_device_token.erl
Normal file
42
apps/dimension_apn/src/mnesia_device_token.erl
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author anlicheng
|
||||||
|
%%% @copyright (C) 2025, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 07. 4月 2025 14:33
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(mnesia_device_token).
|
||||||
|
-author("anlicheng").
|
||||||
|
-include("dimension_tables.hrl").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([create_table/0, insert/3, get_token/1]).
|
||||||
|
|
||||||
|
create_table() ->
|
||||||
|
%% id生成器
|
||||||
|
mnesia:create_table(device_token, [
|
||||||
|
{attributes, record_info(fields, device_token)},
|
||||||
|
{record_name, device_token},
|
||||||
|
{disc_copies, [node()]},
|
||||||
|
{type, set}
|
||||||
|
]).
|
||||||
|
|
||||||
|
-spec insert(UserId :: binary(), DeviceToken :: binary(), Timestamp :: integer()) -> ok | {error, Reason :: any()}.
|
||||||
|
insert(UserId, DeviceToken, Timestamp) when is_binary(UserId), is_binary(DeviceToken), is_integer(Timestamp) ->
|
||||||
|
Record = #device_token{user_id = UserId, token = DeviceToken, timestamp = Timestamp},
|
||||||
|
case mnesia:transaction(mnesia:write(device_token, Record, write)) of
|
||||||
|
{'atomic', Res} ->
|
||||||
|
Res;
|
||||||
|
{'aborted', Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec get_token(UserId :: binary()) -> error | {ok, Token :: binary()}.
|
||||||
|
get_token(UserId) when is_binary(UserId) ->
|
||||||
|
case mnesia:dirty_read(device_token, UserId) of
|
||||||
|
[] ->
|
||||||
|
error;
|
||||||
|
[#device_token{token = Token} | _] ->
|
||||||
|
{ok, Token}
|
||||||
|
end.
|
||||||
Loading…
x
Reference in New Issue
Block a user