From b725499dfe9cead94c56f6b671394c41eda9ed70 Mon Sep 17 00:00:00 2001 From: anlicheng <244108715@qq.com> Date: Mon, 7 Apr 2025 15:04:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0api=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../include/dimension_tables.hrl | 15 +++ apps/dimension_apn/src/api_handler.erl | 115 +++++++++++++++++ apps/dimension_apn/src/dimension_apn.app.src | 1 + apps/dimension_apn/src/dimension_apn_app.erl | 20 ++- .../src/dimension_apn_pusher.erl | 120 ++++++++++++++++-- apps/dimension_apn/src/dimension_utils.erl | 31 +++++ .../dimension_apn/src/mnesia_device_token.erl | 42 ++++++ 7 files changed, 329 insertions(+), 15 deletions(-) create mode 100644 apps/dimension_apn/include/dimension_tables.hrl create mode 100644 apps/dimension_apn/src/api_handler.erl create mode 100644 apps/dimension_apn/src/dimension_utils.erl create mode 100644 apps/dimension_apn/src/mnesia_device_token.erl diff --git a/apps/dimension_apn/include/dimension_tables.hrl b/apps/dimension_apn/include/dimension_tables.hrl new file mode 100644 index 0000000..2c001c9 --- /dev/null +++ b/apps/dimension_apn/include/dimension_tables.hrl @@ -0,0 +1,15 @@ +%%%------------------------------------------------------------------- +%%% @author anlicheng +%%% @copyright (C) 2025, +%%% @doc +%%% +%%% @end +%%% Created : 07. 4月 2025 14:34 +%%%------------------------------------------------------------------- +-author("anlicheng"). + +-record(device_token, { + user_id :: binary(), + token :: binary(), + timestamp = 0 :: integer() +}). \ No newline at end of file diff --git a/apps/dimension_apn/src/api_handler.erl b/apps/dimension_apn/src/api_handler.erl new file mode 100644 index 0000000..f151fec --- /dev/null +++ b/apps/dimension_apn/src/api_handler.erl @@ -0,0 +1,115 @@ +%%%------------------------------------------------------------------- +%%% @author licheng5 +%%% @copyright (C) 2020, +%%% @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, <>, Req1}; + {more, Data, Req1} -> + read_body(Req1, <>) + 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">>)}. \ No newline at end of file diff --git a/apps/dimension_apn/src/dimension_apn.app.src b/apps/dimension_apn/src/dimension_apn.app.src index 55bf54a..7107762 100644 --- a/apps/dimension_apn/src/dimension_apn.app.src +++ b/apps/dimension_apn/src/dimension_apn.app.src @@ -13,6 +13,7 @@ ssl, jiffy, apns, + mnesia, kernel, stdlib ]}, diff --git a/apps/dimension_apn/src/dimension_apn_app.erl b/apps/dimension_apn/src/dimension_apn_app.erl index 5ad22f5..999a628 100644 --- a/apps/dimension_apn/src/dimension_apn_app.erl +++ b/apps/dimension_apn/src/dimension_apn_app.erl @@ -10,6 +10,11 @@ -export([start/2, stop/1]). start(_StartType, _StartArgs) -> + io:setopts([{encoding, unicode}]), + %% 加速内存的回收 + erlang:system_flag(fullsweep_after, 16), + + start_mnesia(), start_http_server(), dimension_apn_sup:start_link(). @@ -29,10 +34,6 @@ start_http_server() -> Dispatcher = cowboy_router:compile([ {'_', [ {"/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, []} ]} ]), @@ -46,4 +47,13 @@ start_http_server() -> {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]). \ No newline at end of file + 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. diff --git a/apps/dimension_apn/src/dimension_apn_pusher.erl b/apps/dimension_apn/src/dimension_apn_pusher.erl index 6706795..cad265d 100644 --- a/apps/dimension_apn/src/dimension_apn_pusher.erl +++ b/apps/dimension_apn/src/dimension_apn_pusher.erl @@ -4,23 +4,123 @@ %%% @doc %%% %%% @end -%%% Created : 03. 4月 2025 15:41 +%%% Created : 07. 4月 2025 14:53 %%%------------------------------------------------------------------- -module(dimension_apn_pusher). -author("anlicheng"). +-behaviour(gen_server). + %% API --export([push/3]). --export([test/1, test/0]). +-export([start_link/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(<<"3ea61b396cc2455069df01f874f0ffeeb2cfa4937adba5f3af743b08148c8eb0">>). - -test(DeviceToken) -> + UserId = <<"">>, Title = <<"动物狂响曲"/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(). -push(DeviceToken, Title, Body) when is_binary(DeviceToken), is_binary(Title), is_binary(Body) -> - poolboy:transaction(apns_pool, fun(WorkerPid) -> dimension_apn_worker:push(WorkerPid, DeviceToken, Title, Body) end). \ No newline at end of file +-spec push(NotificationList :: list()) -> no_return(). +push(NotificationList) when is_list(NotificationList) -> + 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 +%%%=================================================================== diff --git a/apps/dimension_apn/src/dimension_utils.erl b/apps/dimension_apn/src/dimension_utils.erl new file mode 100644 index 0000000..e44ecc0 --- /dev/null +++ b/apps/dimension_apn/src/dimension_utils.erl @@ -0,0 +1,31 @@ +%%%------------------------------------------------------------------- +%%% @author anlicheng +%%% @copyright (C) 2025, +%%% @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]). \ No newline at end of file diff --git a/apps/dimension_apn/src/mnesia_device_token.erl b/apps/dimension_apn/src/mnesia_device_token.erl new file mode 100644 index 0000000..03afa1f --- /dev/null +++ b/apps/dimension_apn/src/mnesia_device_token.erl @@ -0,0 +1,42 @@ +%%%------------------------------------------------------------------- +%%% @author anlicheng +%%% @copyright (C) 2025, +%%% @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. \ No newline at end of file