Compare commits

..

10 Commits

Author SHA1 Message Date
9d27ba3a6a fix 2025-04-16 16:24:28 +08:00
24019f4ceb fix 2025-04-16 16:12:55 +08:00
ed5c6bc3cf fix 2025-04-16 14:38:58 +08:00
03ba9ced25 fix 2025-04-07 15:54:51 +08:00
abbd6e2f70 fix 2025-04-07 15:41:21 +08:00
9237371034 fix pusher 2025-04-07 15:36:19 +08:00
49d251f95f fix 2025-04-07 15:33:55 +08:00
b725499dfe 增加api接口 2025-04-07 15:04:09 +08:00
53ba52712d fix apns 2025-04-07 11:58:14 +08:00
318e3b2f79 fix apns 2025-04-03 15:52:17 +08:00
11 changed files with 318 additions and 74 deletions

View 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()
}).

View File

@ -0,0 +1,107 @@
%%%-------------------------------------------------------------------
%%% @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(<<"success">>)};
{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", _, Notifications) ->
{ok, Pid} = dimension_apn_worker:start_link(),
dimension_apn_worker:push(Pid, Notifications),
{ok, 200, dimension_utils:json_data(<<"success">>)};
handle_request(_, Path, _, _) ->
Path1 = list_to_binary(Path),
{ok, 200, dimension_utils:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.

View File

@ -13,6 +13,7 @@
ssl, ssl,
jiffy, jiffy,
apns, apns,
mnesia,
kernel, kernel,
stdlib stdlib
]}, ]},

View File

@ -10,6 +10,12 @@
-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),
%% mnesia
ok = mnesia:start(),
%% http服务
start_http_server(), start_http_server(),
dimension_apn_sup:start_link(). dimension_apn_sup:start_link().
@ -29,10 +35,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, []}
]} ]}
]), ]),

View File

@ -12,8 +12,9 @@
-behaviour(gen_server). -behaviour(gen_server).
%% API %% API
-export([start_link/1]). -export([start_link/0]).
-export([push/4]). -export([push/2]).
-export([test/0]).
%% gen_server callbacks %% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
@ -29,15 +30,37 @@
%%% API %%% API
%%%=================================================================== %%%===================================================================
-spec push(Pid :: pid(), DeviceToken :: binary(), Title :: binary(), Body :: binary()) -> no_return(). test() ->
push(Pid, DeviceToken, Title, Body) when is_pid(Pid), is_binary(DeviceToken), is_binary(Title), is_binary(Body) -> {ok, Pid} = start_link(),
gen_server:call(Pid, {push, DeviceToken, Title, Body}). UserId = <<"9df4dbb1-aff7-4caa-9adb-cb426a7dbcca">>,
DeviceToken = <<"45fb2f5c960ab3c0b235088a87bfa5cddcb1109aa50f70c9b0fdd3d9482ec10a">>,
Title = <<"动物狂响曲"/utf8>>,
Body = <<"第7集(校服与被毛更深处),bilibili已更新"/utf8>>,
mnesia_device_token:insert(UserId, DeviceToken, dimension_utils:current_time()),
push(Pid, [
#{
<<"user_id">> => UserId,
<<"title">> => Title,
<<"body">> => Body,
<<"custom_data">> => #{
<<"target">> => <<"detail">>,
<<"params">> => #{
<<"drama_id">> => 1234
}
}
}
]).
-spec push(Pid :: pid(), Notifications :: [map()]) -> no_return().
push(Pid, Notifications) when is_pid(Pid), is_list(Notifications) ->
gen_server:cast(Pid, {push, Notifications}).
%% @doc Spawns the server and registers the local name (unique) %% @doc Spawns the server and registers the local name (unique)
-spec(start_link(Opts :: list()) -> -spec(start_link() ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}). {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link(Props) when is_list(Props) -> start_link() ->
gen_server:start_link(?MODULE, [Props], []). gen_server:start_link(?MODULE, [], []).
%%%=================================================================== %%%===================================================================
%%% gen_server callbacks %%% gen_server callbacks
@ -48,7 +71,8 @@ start_link(Props) when is_list(Props) ->
-spec(init(Args :: term()) -> -spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore). {stop, Reason :: term()} | ignore).
init([Props]) -> init([]) ->
{ok, Props} = application:get_env(dimension_apn, apns),
ConnectionOpts0 = proplists:get_value(connection_opts, Props), ConnectionOpts0 = proplists:get_value(connection_opts, Props),
Headers0 = proplists:get_value(headers, Props), Headers0 = proplists:get_value(headers, Props),
@ -73,21 +97,7 @@ init([Props]) ->
{noreply, NewState :: #state{}, timeout() | hibernate} | {noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}). {stop, Reason :: term(), NewState :: #state{}}).
handle_call({push, DeviceToken, Title, Body}, _From, State = #state{apns_pid = ApnsPid, headers = Headers}) -> handle_call(_Request, _From, State) ->
Notification = #{
aps => #{
alert => #{
title => Title,
body => Body
},
%
sound => <<"default">>,
% App
badge => 1
}
},
PushResult = apns:push_notification(ApnsPid, DeviceToken, Notification, Headers),
lager:debug("[dimension_apn_pusher] push result is: ~p", [PushResult]),
{reply, ok, State}. {reply, ok, State}.
%% @private %% @private
@ -96,6 +106,13 @@ handle_call({push, DeviceToken, Title, Body}, _From, State = #state{apns_pid = A
{noreply, NewState :: #state{}} | {noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} | {noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}). {stop, Reason :: term(), NewState :: #state{}}).
handle_cast({push, Notifications}, State = #state{apns_pid = ApnsPid, headers = Headers}) ->
lists:foreach(fun(#{<<"user_id">> := UserId, <<"title">> := Title, <<"body">> := Body, <<"custom_data">> := CustomData}) ->
PushResult = push_task(ApnsPid, UserId, Title, Body, CustomData, Headers),
lager:debug("[dimension_apn_pusher] push result is: ~p", [PushResult])
end, Notifications),
{noreply, State};
handle_cast(_Request, State) -> handle_cast(_Request, State) ->
{noreply, State}. {noreply, State}.
@ -130,6 +147,31 @@ code_change(_OldVsn, State = #state{}, _Extra) ->
%%% Internal functions %%% Internal functions
%%%=================================================================== %%%===================================================================
push_task(ApnsPid, UserId, Title, Body, CustomData, Headers)
when is_pid(ApnsPid), is_binary(UserId), is_binary(Title), is_binary(Body), is_map(CustomData), is_map(Headers) ->
case mnesia_device_token:get_token(UserId) of
error ->
ok;
{ok, DeviceToken} ->
Notification = #{
aps => #{
alert => #{
title => Title,
body => Body
},
'mutable-content' => 1,
%
sound => <<"default">>,
% App
badge => 1
% category => <<"HUB_MESSAGE">>
},
custom_data => CustomData
},
apns:push_notification(ApnsPid, DeviceToken, Notification, Headers)
end.
-spec parse_headers(Headers :: list()) -> map(). -spec parse_headers(Headers :: list()) -> map().
parse_headers(Headers) -> parse_headers(Headers) ->
parse_headers(Headers, #{}). parse_headers(Headers, #{}).

View File

@ -0,0 +1,27 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 07. 4 2025 15:47
%%%-------------------------------------------------------------------
-module(dimension_mnesia_manager).
-author("anlicheng").
%% API
-export([init_database/0]).
%%
init_database() ->
%% schema
mnesia:stop(),
mnesia:delete_schema([node()]),
%% schema
ok = mnesia:create_schema([node()]),
ok = mnesia:start(),
%%
mnesia_device_token:create_table(),
ok.

View File

@ -1,25 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 03. 4 2025 15:41
%%%-------------------------------------------------------------------
-module(dimension_spn_pusher).
-author("anlicheng").
%% API
-export([push/3]).
-export([test/1]).
test(DeviceToken) ->
Title = <<"这是一个消息通知"/utf8>>,
Body = jiffy:encode(#{<<"id">> => 1234, <<"name">> => <<"英雄第二季"/utf8>>}, [force_utf8]),
push(DeviceToken, Title, Body).
-spec push(DeviceToken :: binary(), Title :: binary(), Body :: binary()) -> no_return().
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).

View 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]).

View 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(fun() -> mnesia:write(device_token, Record, write) end) 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.

View File

@ -3,17 +3,14 @@
%% http服务器 %% http服务器
{http_server, [ {http_server, [
{port, 18080}, {port, 18084},
{acceptors, 500}, {acceptors, 500},
{max_connections, 10240}, {max_connections, 10240},
{backlog, 10240} {backlog, 10240}
]}, ]},
{pools, [ %% 推送配置
%% 推送设置 {apns, [
{apns_pool,
[{size, 1}, {max_overflow, 1}, {worker_module, dimension_apn_worker}],
[
{connection_opts, [ {connection_opts, [
{apple_host, "api.sandbox.push.apple.com"}, {apple_host, "api.sandbox.push.apple.com"},
{apple_port, 443}, {apple_port, 443},
@ -28,8 +25,9 @@
{apns_topic, "com.jihe.dimensionhub"}, {apns_topic, "com.jihe.dimensionhub"},
{apns_push_type, "alert"} {apns_push_type, "alert"}
]} ]}
] ]},
}
{pools, [
]} ]}
]}, ]},

View File

@ -2,5 +2,9 @@
-setcookie dimension_apn_cookie -setcookie dimension_apn_cookie
-mnesia dir '"/usr/local/var/mnesia/dimension"'
-mnesia dump_log_write_threshold 5000
-mnesia dc_dump_limit 40
+K true +K true
+A30 +A30