diff --git a/apps/iot/include/iot.hrl b/apps/iot/include/iot.hrl index 3c334ec..dc26448 100644 --- a/apps/iot/include/iot.hrl +++ b/apps/iot/include/iot.hrl @@ -90,7 +90,7 @@ %% id生成器 -record(id_generator, { - name :: string(), + name :: atom(), seq = 0 :: integer() }). diff --git a/apps/iot/src/http_handler/http_host_handler.erl b/apps/iot/src/http_handler/http_host_handler.erl index 79dc9fb..20f27c7 100644 --- a/apps/iot/src/http_handler/http_host_handler.erl +++ b/apps/iot/src/http_handler/http_host_handler.erl @@ -48,7 +48,7 @@ handle_request("POST", "/host/create", _, HostInfo) -> end; %% 批量导入 -handle_request("POST", "/host/batch_create", _, HostInfos) -> +handle_request("POST", "/host/batch_import", _, HostInfos) -> lager:debug("[host_handler] batch_create post params: ~p", [HostInfos]), %% serial_number是必填项 diff --git a/apps/iot/src/http_handler/http_terminal_handler.erl b/apps/iot/src/http_handler/http_terminal_handler.erl new file mode 100644 index 0000000..597391e --- /dev/null +++ b/apps/iot/src/http_handler/http_terminal_handler.erl @@ -0,0 +1,206 @@ +%%%------------------------------------------------------------------- +%%% @author licheng5 +%%% @copyright (C) 2020, +%%% @doc +%%% +%%% @end +%%% Created : 26. 4月 2020 3:36 下午 +%%%------------------------------------------------------------------- +-module(http_terminal_handler). +-author("licheng5"). +-include("iot.hrl"). + +%% API +-export([handle_request/4]). + +%% 添加终端 +handle_request("POST", "/terminal/create", _, TerminalInfo) -> + lager:debug("[terminal_handler] create post params: ~p", [TerminalInfo]), + + case valid_terminal_info(TerminalInfo) of + true -> + TerminalId = id_generator_model:new_terminal_id(), + Terminal = convert_terminal_info(TerminalId, TerminalInfo), + + case terminal_model:add_terminal(Terminal) of + ok -> + {ok, 200, iot_util:json_data(TerminalId)}; + {error, Reason} when is_binary(Reason) -> + lager:warning("[host_handler] get a error: ~p", [Reason]), + {ok, 200, iot_util:json_error(-1, Reason)}; + {error, Reason} when is_binary(Reason) -> + {ok, 200, iot_util:json_error(-1, <<"database error">>)} + end; + false -> + Error = terminal_error(TerminalInfo), + {ok, 200, iot_util:json_error(-1, Error)} + end; + +%% 批量导入 +handle_request("POST", "/terminal/batch_import", _, TerminalInfos) -> + lager:debug("[terminal_handler] batch_import post params: ~p", [TerminalInfos]), + + %% serial_number是必填项 + case lists:any(fun(Info) -> not is_map_key(<<"serial_number">>, Info) orelse maps:get(<<"serial_number">>, Info) == <<"">> end, TerminalInfos) of + true -> + {ok, 200, iot_util:json_error(-1, <<"serial_number missed">>)}; + false -> + case lists:all(fun valid_terminal_info/1, TerminalInfos) of + true -> + ImportResult = lists:map(fun(TerminalInfo = #{<<"serial_number">> := SerialNumber}) -> + TerminalId = id_generator_model:new_terminal_id(), + Terminal = convert_terminal_info(TerminalId, TerminalInfo), + case terminal_model:add_terminal(Terminal) of + ok -> + {SerialNumber, TerminalId}; + {error, Reason} when is_binary(Reason) -> + lager:debug("[host_handler] add_host get error: ~p", [Reason]), + {SerialNumber, Reason}; + {error, Reason} -> + {SerialNumber, <<"failed">>} + end + end, TerminalInfos), + + {ok, 200, iot_util:json_data(ImportResult)}; + false -> + %% 反馈错误信息 + InvalidTerminalInfos = lists:filter(fun(Info) -> not valid_terminal_info(Info) end, TerminalInfos), + ErrorInfos = lists:map(fun(Info = #{<<"serial_number">> := SerialNumber}) -> {SerialNumber, terminal_error(Info)} end, InvalidTerminalInfos), + {ok, 200, iot_util:json_error(-1, maps:from_list(ErrorInfos))} + end + end; + +%% 获取终端的统计信息 +handle_request("GET", "/terminal/stat", _, _) -> + case terminal_model:get_stat() of + {ok, {TotalNum, Stat}} -> + StatInfo = #{ + <<"total_num">> => TotalNum, + <<"stat">> => lists:map(fun({Status, Num}) -> #{<<"status">> => Status, <<"num">> => Num} end, maps:to_list(Stat)) + }, + {ok, 200, iot_util:json_data(StatInfo)}; + {error, Reason} -> + lager:warning("[host_handler] get a error: ~p", [Reason]), + {ok, 200, iot_util:json_error(404, <<"database error">>)} + end; + +%% 获取终端的列表信息 +handle_request(_, "/terminal/list", GetParams, PostParams) -> + Page0 = maps:get(<<"page">>, GetParams, <<"1">>), + Size0 = maps:get(<<"size">>, GetParams, <<"10">>), + Page = binary_to_integer(Page0), + Size = binary_to_integer(Size0), + + true = Page > 0 andalso Size > 0, + Start = (Page - 1) * Size, + + %% 处理查询条件 + Model = maps:get(<<"model">>, PostParams, <<"">>), + CellId = maps:get(<<"cell_id">>, PostParams, <<"">>), + CellId1 = case CellId =/= <<>> of + true -> binary_to_integer(CellId); + false -> 0 + end, + + case terminal_model:find_terminals([{model, Model}, {cell, CellId1}], Start, Size) of + {ok, Terminals, TotalNum} -> + Response = #{ + <<"total_num">> => TotalNum, + <<"terminals">> => lists:map(fun terminal_model:to_map/1, Terminals) + }, + + lager:debug("resp is: ~p", [Response]), + + {ok, 200, iot_util:json_data(Response)}; + {error, Reason} -> + lager:warning("[host_handler] get a error: ~p", [Reason]), + {ok, 200, iot_util:json_error(404, <<"database error">>)} + end; + +%% 获取终端详情 +handle_request("GET", "/terminal/detail", #{<<"terminal_id">> := TerminalId}, _) -> + lager:debug("[terminal_handler] terminal detail id is: ~p", [TerminalId]), + case terminal_model:get_terminal(TerminalId) of + undefined -> + {ok, 200, iot_util:json_error(404, <<"terminal not found">>)}; + {ok, Terminal} -> + TerminalInfo = terminal_model:to_map(Terminal), + %% TODO 获取终端的数据信息 + + {ok, 200, iot_util:json_data(TerminalInfo)} + end; + +%% 更新状态信息 +handle_request("POST", "/terminal/change_status", _, #{<<"terminal_id">> := TerminalId, <<"status">> := Status}) when is_integer(TerminalId), is_integer(Status) -> + lager:debug("[terminal_handler] change_status id is: ~p", [TerminalId]), + case terminal_model:change_status(TerminalId, Status) of + {error, Reason} when is_binary(Reason) -> + {ok, 200, iot_util:json_error(-1, Reason)}; + ok -> + {ok, 200, iot_util:json_data(<<"success">>)} + end; + +%% 更新终端信息 +handle_request("POST", "/terminal/update", _, Fields0 = #{<<"terminal_id">> := TerminalId}) -> + lager:debug("[terminal_handler] update terminal params is: ~p", [Fields0]), + + Fields = maps:remove(<<"terminal_id">>, Fields0), + case terminal_model:update_terminal(TerminalId, Fields) of + ok -> + {ok, 200, iot_util:json_data(<<"success">>)}; + {error, Reason} when is_binary(Reason) -> + {ok, 200, iot_util:json_error(-1, Reason)} + end; + +handle_request(_, Path, _, _) -> + Path1 = list_to_binary(Path), + {ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% helper methods +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% 将map转换成record +convert_terminal_info(TerminalId, TerminalInfo) when is_integer(TerminalId), is_map(TerminalInfo) -> + #terminal{ + terminal_id = TerminalId, + %% 关联主机 + host_id = maps:get(<<"host_id">>, TerminalInfo, <<"">>), + %% 序列号 + serial_number = maps:get(<<"serial_number">>, TerminalInfo, <<"">>), + %% 名称 + name = maps:get(<<"name">>, TerminalInfo, <<"">>), + %% 设备编码 + code = maps:get(<<"code">>, TerminalInfo, <<"">>), + %% 接入协议 + access_protocol = maps:get(<<"access_protocol">>, TerminalInfo, <<"">>), + %% 产品ID,枚举类型 + product_id = maps:get(<<"product_id">>, TerminalInfo, 0), + %% 厂商ID,枚举类型 + vendor_id = maps:get(<<"vendor_id">>, TerminalInfo, 0), + %% 型号 + model = maps:get(<<"model">>, TerminalInfo, <<"">>), + %% 所在单元ID,管理系统负责 + cell_id = maps:get(<<"cell_id">>, TerminalInfo, 0), + %% 终端状态 + status = ?TERMINAL_STATUS_OFFLINE, + %% 最后上线时间 + update_ts = 0 + }. + +%% 检查是否是合法的主机信息 +valid_terminal_info(#{<<"name">> := Name, <<"model">> := Model, <<"cell_id">> := CellId, <<"serial_number">> := SerialNumber}) -> + Name =/= <<"">> andalso Model =/= <<"">> andalso CellId > 0 andalso SerialNumber =/= <<"">>. + +%% 获取错误信息 +terminal_error(M) when is_map(M) -> + if + not is_map_key(<<"name">>, M) orelse map_get(<<"name">>, M) == <<>> -> + <<"name is empty">>; + not is_map_key(<<"model">>, M) orelse map_get(<<"model">>, M) == <<>> -> + <<"model is empty">>; + not is_map_key(<<"serial_number">>, M) orelse map_get(<<"serial_number">>, M) == <<>> -> + <<"serial_number is empty">>; + true -> + <<"unknown error">> + end. \ No newline at end of file diff --git a/apps/iot/src/model/id_generator_model.erl b/apps/iot/src/model/id_generator_model.erl index 8a72b2b..1010272 100644 --- a/apps/iot/src/model/id_generator_model.erl +++ b/apps/iot/src/model/id_generator_model.erl @@ -11,14 +11,13 @@ -include("iot.hrl"). %% API --export([generate/1]). +-export([generate/1, new_terminal_id/0]). %% 生成对应的自增id --spec generate(Name :: string()) -> {ok, Id :: integer()} | {error, Reason :: any()}. -generate(Name) when is_list(Name) -> - case mnesia:transaction(fun() -> mnesia:dirty_update_counter(id_generator, Name, 1) end) of - {atomic, Id} -> - {ok, Id}; - {aborted, Reason} -> - {error, Reason} - end. \ No newline at end of file +-spec generate(Name :: atom()) -> Id :: integer(). +generate(Name) when is_atom(Name) -> + mnesia:dirty_update_counter(id_generator, Name, 1). + +-spec new_terminal_id() -> Id :: integer(). +new_terminal_id() -> + generate(terminal). \ No newline at end of file diff --git a/apps/iot/src/model/terminal_model.erl b/apps/iot/src/model/terminal_model.erl index b7d8fe6..22638c1 100644 --- a/apps/iot/src/model/terminal_model.erl +++ b/apps/iot/src/model/terminal_model.erl @@ -14,8 +14,17 @@ -define(TAB_NAME, terminal). %% API --export([get_all_terminals/0, get_host_terminals/1, get_status_stat/0, table_size/0]). --export([change_status/2, delete/1, to_map/1, add_terminal/1]). +-export([get_terminal/1, get_all_terminals/0, get_host_terminals/1, find_terminals/3, get_stat/0, table_size/0, match_spec/1]). +-export([change_status/2, delete/1, to_map/1, add_terminal/1, update_terminal/2]). + +-spec get_terminal(TerminalId :: integer()) -> {ok, #terminal{}} | undefined. +get_terminal(TerminalId) when is_integer(TerminalId) -> + case mnesia:dirty_read(?TAB_NAME, TerminalId) of + [Terminal] -> + {ok, Terminal}; + _ -> + undefined + end. %% 获取app信息 -spec get_all_terminals() -> {ok, Terminals :: [#terminal{}]} | {error, Reason :: any()}. @@ -44,14 +53,30 @@ get_host_terminals(HostId) when is_binary(HostId) -> {error, Error} end. +%% 获取app信息 +-spec find_terminals(Matches :: [{Name :: atom(), Val :: any()}], Start :: integer(), Limit :: integer()) -> + {ok, Terminals :: list(), TotalNum :: integer()} | + {error, Reason :: any()}. +find_terminals(Matches, Start, Limit) when is_list(Matches), is_integer(Limit), is_integer(Start), Start >= 0, Limit > 0 -> + MatchSpec = terminal_model:match_spec(Matches), + Terminals0 = mnesia:dirty_select(?TAB_NAME, [MatchSpec]), + Terminals = sort(Terminals0), + Len = length(Terminals), + case Len >= Start + 1 of + true -> + {ok, lists:sublist(Terminals, Start + 1, Limit), Len}; + false -> + {ok, [], Len} + end. + %% 获取状态分组统计信息 --spec get_status_stat() -> {ok, Stat :: #{}} | {error, Reason :: any()}. -get_status_stat() -> +-spec get_stat() -> {ok, {Total :: integer(), Stat :: #{}}} | {error, Reason :: any()}. +get_stat() -> Fun = fun() -> - mnesia:foldl(fun(#terminal{status = Status}, Acc) -> + mnesia:foldl(fun(#terminal{status = Status}, {Total, Acc}) -> Num = maps:get(Status, Acc, 0), - Acc#{Status => Num + 1} - end, #{}, ?TAB_NAME) + {Total + 1, Acc#{Status => Num + 1}} + end, {0, #{}}, ?TAB_NAME) end, case mnesia:transaction(Fun) of @@ -70,6 +95,46 @@ add_terminal(Terminal = #terminal{}) -> {error, Error} end. +-spec change_status(TerminalId :: integer(), Fields :: #{}) -> ok | {error, Reason :: any()}. +update_terminal(TerminalId, Fields) when is_integer(TerminalId), is_map(Fields) -> + Fun = fun() -> + case mnesia:read(?TAB_NAME, TerminalId) of + [] -> + mnesia:abort(<<"terminal not found">>); + [Terminal] -> + NTerminal = lists:foldl(fun(E, Terminal0) -> + case E of + {<<"name">>, Name} when is_binary(Name) -> + Terminal0#terminal{name = Name}; + {<<"serial_number">>, SerialNumber} when is_binary(SerialNumber) -> + Terminal0#terminal{serial_number = SerialNumber}; + {<<"code">>, Code} when is_binary(Code) -> + Terminal0#terminal{code = Code}; + {<<"access_protocol">>, AccessProtocol} when is_binary(AccessProtocol) -> + Terminal0#terminal{access_protocol = AccessProtocol}; + {<<"product_id">>, ProductId} when is_integer(ProductId) -> + Terminal0#terminal{product_id = ProductId}; + {<<"vendor_id">>, VendorId} when is_integer(VendorId) -> + Terminal0#terminal{vendor_id = VendorId}; + {<<"model">>, Model} when is_binary(Model) -> + Terminal0#terminal{model = Model}; + {<<"cell_id">>, CellId} when is_integer(CellId) -> + Terminal0#terminal{cell_id = CellId}; + {Name, _} -> + mnesia:abort(<<"invalid: ", Name/binary>>) + end + end, Terminal, maps:to_list(Fields)), + + mnesia:write(?TAB_NAME, NTerminal, write) + end + end, + case mnesia:transaction(Fun) of + {atomic, ok} -> + ok; + {aborted, Reason} -> + {error, Reason} + end. + -spec change_status(TerminalId :: integer(), Status :: integer()) -> ok | {error, Reason :: any()}. change_status(TerminalId, Status) when is_integer(TerminalId), is_integer(Status) -> Fun = fun() -> @@ -100,6 +165,24 @@ delete(TerminalId) when is_integer(TerminalId) -> table_size() -> mnesia:table_info(host, size). +%% 获取查询的过滤条件 +match_spec(Specs) when is_list(Specs) -> + MatchHead = #host{model = '$1', cell_id = '$2', _ = '_'}, + Guard = guard(Specs), + Result = ['$_'], + {MatchHead, Guard, Result}. + +guard(Specs) when is_list(Specs) -> + guard(Specs, []). +guard([], Guard) -> + Guard; +guard([{model, Model}|Tail], Guard) when Model =/= <<"">> -> + guard(Tail, [{'=:=', '$1', Model}|Guard]); +guard([{cell, CellId}|Tail], Guard) when CellId > 0 -> + guard(Tail, [{'=:=', '$2', CellId}|Guard]); +guard([_|Tail], Guard) -> + guard(Tail, Guard). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% helper methods %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%