#include "Adminive_Config.h" #include "Global.h" #include "adminive/adminive.hpp" #include "adminive/adapters/drogon.hpp" #include "adminive/adapters/magic_enum.hpp" #include "adminive/adapters/nlohmann_json.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace ecap::adminive_config { enum class Data_Source_Type { Serial_Data_Source, TCP_Client_Data_Source, File_Data_Source, Dll_Data_Source, Shared_Memory_Data_Source }; enum class Data_Feed_Type { Data_Feed_TCP_Server, Data_Feed_UDP_Server, Data_Feed_TCP_Client, Data_Feed_UDP_Client }; struct Data_Source_Row { std::string key; Data_Source_Type type = Data_Source_Type::TCP_Client_Data_Source; bool enable{}; Data_Source_Data config; std::string ip; std::uint16_t port{}; std::string port_name; Baud_Rate_Type baud_rate{}; std::string file_path; File_Data_Type data_type = File_Data_Type::BIN_Blank_Text; Play_Mode play_mode = Play_Mode::one; std::string library_path; std::string function_name; std::size_t buffer_size{}; std::string shared_memory_name; std::uint64_t shared_memory_size{}; std::string state; }; struct Data_Feed_Row { std::string key; Data_Feed_Type type = Data_Feed_Type::Data_Feed_TCP_Server; bool enable{}; std::string source_key; Output_Format output_format; Output_Data_Format format = Output_Data_Format::BIN; std::uint16_t port{}; std::string url; std::size_t connect_user_buffer_size = 409600; std::size_t connect_system_buffer_size = 409600; std::string state; }; struct External_Resource_Row { std::string name; std::string description; std::string download_url; std::uint64_t update_interval_seconds{}; std::int64_t last_updated_at{}; std::size_t row_count{}; std::string state; }; } namespace adminive { template <> struct Type_Descriptor { static auto get() { using T = ecap::adminive_config::External_Resource_Row; return object("external_resource", "外部数据源管理", ADMINIVE_FIELD_LABEL(T, name, "数据表"), ADMINIVE_FIELD_LABEL(T, description, "说明"), ADMINIVE_FIELD_LABEL(T, download_url, "在线来源"), ADMINIVE_FIELD_LABEL(T, update_interval_seconds, "建议更新周期(秒)"), ADMINIVE_FIELD_LABEL(T, last_updated_at, "上次更新时间"), ADMINIVE_FIELD_LABEL(T, row_count, "行数"), ADMINIVE_FIELD_LABEL(T, state, "状态")); } }; template <> struct Type_View_Descriptor { static Table_View table() { using T = ecap::adminive_config::External_Resource_Row; return table_view(column<&T::name>("数据表").search(), column<&T::description>("说明"), column<&T::download_url>("在线来源"), column<&T::row_count>("行数").sort(), column<&T::last_updated_at>("上次更新时间").sort(), column<&T::update_interval_seconds>("建议更新周期(秒)"), column<&T::state>("状态")).titled("外部数据源管理"); } }; template <> struct Type_Descriptor { static auto get() { using T = ecap::adminive_config::Data_Source_Row; using E = ecap::adminive_config::Data_Source_Type; auto type = ADMINIVE_FIELD_LABEL(T, type, "数据源类型").creatable().required().select_input(); type = type.enum_label("串口数据源") .enum_label("TCP 客户端数据源") .enum_label("文件数据源") .enum_label("DLL 数据源") .enum_label("共享内存数据源"); return object("data_source", "数据源", ADMINIVE_FIELD_LABEL(T, key, "名称").creatable().required().text_input(), type, ADMINIVE_FIELD_LABEL(T, enable, "启用").creatable().editable().boolean_input(), ADMINIVE_FIELD_LABEL(T, config, "公共与地图配置").creatable().editable(), ADMINIVE_FIELD_LABEL(T, ip, "IP 地址").creatable().editable().text_input().visible_on("${$self.type == 'TCP_Client_Data_Source'}"), ADMINIVE_FIELD_LABEL(T, port, "端口").creatable().editable().number_input().visible_on("${$self.type == 'TCP_Client_Data_Source'}"), ADMINIVE_FIELD_LABEL(T, port_name, "串口名称").creatable().editable().text_input().visible_on("${$self.type == 'Serial_Data_Source'}"), ADMINIVE_FIELD_LABEL(T, baud_rate, "波特率").creatable().editable().select_input().visible_on("${$self.type == 'Serial_Data_Source'}"), ADMINIVE_FIELD_LABEL(T, file_path, "文件路径").creatable().editable().text_input().visible_on("${$self.type == 'File_Data_Source'}"), ADMINIVE_FIELD_LABEL(T, data_type, "数据格式").creatable().editable().select_input().visible_on("${$self.type == 'File_Data_Source' || $self.type == 'Dll_Data_Source' || $self.type == 'Shared_Memory_Data_Source'}"), ADMINIVE_FIELD_LABEL(T, play_mode, "播放模式").creatable().editable().select_input().visible_on("${$self.type == 'File_Data_Source'}"), ADMINIVE_FIELD_LABEL(T, library_path, "库路径").creatable().editable().text_input().visible_on("${$self.type == 'Dll_Data_Source'}"), ADMINIVE_FIELD_LABEL(T, function_name, "函数名").creatable().editable().text_input().visible_on("${$self.type == 'Dll_Data_Source'}"), ADMINIVE_FIELD_LABEL(T, buffer_size, "读取缓冲区大小").creatable().editable().number_input().visible_on("${$self.type == 'Dll_Data_Source'}"), ADMINIVE_FIELD_LABEL(T, shared_memory_name, "共享内存名称").creatable().editable().text_input().visible_on("${$self.type == 'Shared_Memory_Data_Source'}"), ADMINIVE_FIELD_LABEL(T, shared_memory_size, "共享内存大小").creatable().editable().number_input().visible_on("${$self.type == 'Shared_Memory_Data_Source'}"), ADMINIVE_FIELD_LABEL(T, state, "运行状态").text_input()); } }; template <> struct Type_View_Descriptor { static Table_View table() { using T = ecap::adminive_config::Data_Source_Row; return table_view(column<&T::key>("名称").search(), column<&T::type>("类型").filter(), column<&T::enable>("启用").filter(), column<&T::state>("状态")) .titled("数据源") .id_title("序号") .create_title("添加数据源") .edit_title("编辑") .delete_title("删除") .delete_confirmation("确定删除该数据源?仍有数据馈送连接时后端会拒绝删除。") .reorderable() .create_layout(vertical(grid(2, use<&T::key>(), use<&T::type>(), use<&T::enable>()), use<&T::config>(), grid(2, use<&T::ip>(), use<&T::port>(), use<&T::port_name>(), use<&T::baud_rate>(), use<&T::file_path>(), use<&T::data_type>(), use<&T::play_mode>(), use<&T::library_path>(), use<&T::function_name>(), use<&T::buffer_size>(), use<&T::shared_memory_name>(), use<&T::shared_memory_size>()))) .edit_layout(vertical(grid(2, use<&T::key>(), use<&T::type>(), use<&T::enable>(), use<&T::state>()), use<&T::config>(), grid(2, use<&T::ip>(), use<&T::port>(), use<&T::port_name>(), use<&T::baud_rate>(), use<&T::file_path>(), use<&T::data_type>(), use<&T::play_mode>(), use<&T::library_path>(), use<&T::function_name>(), use<&T::buffer_size>(), use<&T::shared_memory_name>(), use<&T::shared_memory_size>()))); } }; template <> struct Type_Descriptor { static auto get() { using T = ecap::adminive_config::Data_Feed_Row; using E = ecap::adminive_config::Data_Feed_Type; auto type = ADMINIVE_FIELD_LABEL(T, type, "数据馈送类型").creatable().required().select_input(); type = type.enum_label("TCP 服务端") .enum_label("UDP 服务端") .enum_label("TCP 客户端") .enum_label("UDP 客户端"); return object("data_feed", "数据馈送", ADMINIVE_FIELD_LABEL(T, key, "名称").creatable().required().text_input(), type, ADMINIVE_FIELD_LABEL(T, enable, "启用").creatable().editable().boolean_input(), ADMINIVE_FIELD_LABEL(T, source_key, "连接的数据源").creatable().editable().select_input(), ADMINIVE_FIELD_LABEL(T, output_format, "输出格式").creatable().editable(), ADMINIVE_FIELD_LABEL(T, format, "数据格式").select_input(), ADMINIVE_FIELD_LABEL(T, port, "端口").creatable().editable().number_input(), ADMINIVE_FIELD_LABEL(T, url, "IP 地址").creatable().editable().text_input().visible_on("${$self.type == 'Data_Feed_TCP_Client' || $self.type == 'Data_Feed_UDP_Client'}"), ADMINIVE_FIELD_LABEL(T, connect_user_buffer_size, "TCP 用户缓冲区").creatable().editable().number_input().visible_on("${$self.type == 'Data_Feed_TCP_Server'}"), ADMINIVE_FIELD_LABEL(T, connect_system_buffer_size, "TCP 系统缓冲区").creatable().editable().number_input().visible_on("${$self.type == 'Data_Feed_TCP_Server'}"), ADMINIVE_FIELD_LABEL(T, state, "运行状态").text_input()); } }; template <> struct Type_View_Descriptor { static Table_View table() { using T = ecap::adminive_config::Data_Feed_Row; return table_view(column<&T::key>("名称").search(), column<&T::type>("类型").filter(), column<&T::source_key>("连接的数据源").filter(), column<&T::format>("输出格式").filter(), column<&T::port>("端口").sort(), column<&T::enable>("启用").filter(), column<&T::state>("状态")) .titled("数据馈送") .id_title("序号") .create_title("添加数据馈送") .edit_title("编辑") .delete_title("删除") .delete_confirmation("确定删除该数据馈送?对应拓扑连接会一并删除。") .reorderable() .create_layout(vertical(grid(2, use<&T::key>(), use<&T::type>(), use<&T::enable>(), use<&T::source_key>()), use<&T::output_format>(), grid(2, use<&T::url>(), use<&T::port>(), use<&T::connect_user_buffer_size>(), use<&T::connect_system_buffer_size>()))) .edit_layout(vertical(grid(2, use<&T::key>(), use<&T::type>(), use<&T::enable>(), use<&T::source_key>(), use<&T::state>()), use<&T::output_format>(), grid(2, use<&T::url>(), use<&T::port>(), use<&T::connect_user_buffer_size>(), use<&T::connect_system_buffer_size>()))); } }; template <> struct Type_View_Descriptor { static Form_View edit() { using T = Mode_ACS_Config_Data; return edit_form(vertical( group("基础时序", grid(2, use<&T::timeout_seconds>(), use<&T::air_pos_timeout>(), use<&T::surface_pos_timeout>())), group("过滤参数", grid(2, use<&T::max_speed_m_s>(), use<&T::time_space_filter>(), use<&T::speed_filter>(), use<&T::aircraft_change_list_adsb_range_filter>(), use<&T::aircraft_change_list_adsb_range_factor>(), use<&T::adsb_theoretical_target_altitude_meters>())), group("容量与上报", grid(2, use<&T::max_track_point_size>(), use<&T::mode_s_max_num>(), use<&T::mode_other_max_num>(), use<&T::report_data_feed_msg_mum>(), use<&T::default_min_aircraft_list_num>(), use<&T::aircraft_change_list_min_position_points>())), group("运行开关", grid(2, use<&T::wait_process_msg>(), use<&T::monitor_msg_live>())))).submit("保存 Mode ACS 配置"); } }; template <> struct Type_View_Descriptor { static Form_View edit() { using T = Cesium_Graphics_Config_Data; return edit_form(vertical( group("渲染质量", grid(2, use<&T::debug_show_frames_per_second>(), use<&T::target_frame_rate>(), use<&T::use_browser_recommended_resolution>(), use<&T::resolution_scale>(), use<&T::msaa_samples>(), use<&T::fxaa>(), use<&T::maximum_screen_space_error>())), group("光照与阴影", grid(2, use<&T::shadows>(), use<&T::enable_lighting>(), use<&T::solar_lighting>(), use<&T::solar_light_intensity>())), group("地形", grid(2, use<&T::terrain_enabled_in_3d>(), use<&T::terrain_exaggeration>(), use<&T::terrain_limit_maximum_level>(), use<&T::terrain_maximum_level>(), use<&T::terrain_cache_tiles>(), use<&T::terrain_occlusion_enabled>(), use<&T::occlusion_sample_spacing_meters>(), use<&T::occlusion_clearance_margin_meters>())), group("辅助显示", grid(2, use<&T::surface_navigation_reference_visible>(), use<&T::view_axes_visible>(), use<&T::performance_panel_x>(), use<&T::performance_panel_y>(), use<&T::view_axes_panel_x>(), use<&T::view_axes_panel_y>(), use<&T::view_axes_panel_size>())))).submit("保存 Cesium 图形配置"); } }; template <> struct Type_View_Descriptor { static Form_View edit() { using T = Data_feed_Config_Data; return edit_form(grid(2, use<&T::packet_byte_size>())).submit("保存数据馈送全局配置"); } }; template <> struct Type_View_Descriptor { static Form_View edit() { using T = MLAT_Config_Data; return edit_form(grid(2, use<&T::merge>())).submit("保存 MLAT 配置"); } }; } namespace ecap::adminive_config { namespace { using Json = nlohmann::json; const std::string data_topology_path = "/api/adminive/config/data_topology"; class Conflict_Error final : public std::runtime_error { public: using std::runtime_error::runtime_error; }; class Bad_Request_Error final : public std::runtime_error { public: using std::runtime_error::runtime_error; }; template adminive::Resource_Transaction value_section_transaction(Config_Section section) { adminive::Resource_Transaction transaction; transaction.commit = [section](const Model& value, const adminive::Request_Context&) { Config::save(section, value.to_base_json()); }; transaction.rollback = transaction.commit; return transaction; } adminive::Resource_Transaction mode_acs_transaction() { adminive::Resource_Transaction transaction; const auto persist = [](const Mode_ACS_Config_Data& value, const adminive::Request_Context&) { SSR::Monitor_Mode_ACS_Message_Num = value.monitor_msg_live; Config::merge(Config_Section::mode_acs, value.to_base_json()); }; transaction.commit = persist; transaction.rollback = persist; return transaction; } adminive::Resource_Transaction data_feed_settings_transaction() { adminive::Resource_Transaction transaction; const auto persist = [](const Data_feed_Config_Data& value, const adminive::Request_Context&) { auto patch = Psc::JSON::object(); patch.append({"data_feed", value.to_base_json()}); Config::merge(Config_Section::mode_acs, patch); }; transaction.commit = persist; transaction.rollback = persist; return transaction; } adminive::Resource_Transaction map_view_transaction(Global* global) { adminive::Resource_Transaction transaction; transaction.prepare = [global](const Map_View_Config& value, const adminive::Request_Context&) { if(!Map_View_Config::is_scene_mode(value.scene_mode)) throw adminive::Field_Validation_Error("scene_mode", "场景模式必须为 2d 或 3d"); global->map_resources_config.read([&](const auto& resources) { if(!resources.is_imagery_key(value.map2d.current_imagery_key)) throw adminive::Field_Validation_Error("map2d.current_imagery_key", "影像源不存在"); if(!resources.is_terrain_key(value.map2d.current_terrain_key)) throw adminive::Field_Validation_Error("map2d.current_terrain_key", "地形源不存在"); if(!resources.is_imagery_key(value.map3d.current_imagery_key)) throw adminive::Field_Validation_Error("map3d.current_imagery_key", "影像源不存在"); if(!resources.is_terrain_key(value.map3d.current_terrain_key)) throw adminive::Field_Validation_Error("map3d.current_terrain_key", "地形源不存在"); }); }; const auto persist = [global](const Map_View_Config& value, const adminive::Request_Context&) { global->map_resources_config.write([&](auto& resources) { resources.current_imagery_key = value.scene_mode == "2d" ? value.map2d.current_imagery_key : value.map3d.current_imagery_key; }); Config::save(Config_Section::map_view, value.to_base_json()); Config::save(Config_Section::map_resources); }; transaction.commit = persist; transaction.rollback = persist; return transaction; } void save_data_sources(Global* global) { Config::save(Config_Section::mode_acs, global->mode_acs.to_base_json()); } void save_data_feeds(Global* global) { Config::save(Config_Section::mode_acs, global->mode_acs.to_base_json()); } template adminive::Http_Response business_response(Function&& function) noexcept { try { return std::forward(function)(); } catch(const Bad_Request_Error& error) { return adminive::make_http_error(400, error.what()); } catch(const Conflict_Error& error) { return adminive::make_http_error(409, error.what()); } catch(const std::out_of_range& error) { return adminive::make_http_error(404, error.what()); } catch(const std::invalid_argument& error) { return adminive::make_http_error(422, error.what()); } catch(const std::exception& error) { return adminive::make_http_error(500, error.what()); } catch(...) { return adminive::make_http_error(500, "unknown server error"); } } Json read_request_body(std::string_view body) { try { auto input = adminive::parse_json(body); adminive::Json_Adapter::erase(input, "id"); return input; } catch(const std::exception& error) { throw Bad_Request_Error(error.what()); } } Data_Topology_View_Config read_topology_view(Json input, Global* global) { if(input.value("protocol", std::string{}) != "ecap.data-topology-view" || input.value("protocol_version", 0) != 1) throw std::invalid_argument("数据拓扑视图协议或版本不受支持"); const auto finite_number = [&input](std::string_view name) { const auto field = input.find(std::string(name)); if(field == input.end() || !field->is_number()) throw std::invalid_argument("数据拓扑视图缺少数值字段: " + std::string(name)); const auto value = field->get(); if(!std::isfinite(value)) throw std::invalid_argument("数据拓扑视图字段不是有限数值: " + std::string(name)); return value; }; Data_Topology_View_Config result; result.initialized = true; result.zoom = finite_number("zoom"); result.pan_x = finite_number("pan_x"); result.pan_y = finite_number("pan_y"); if(result.zoom <= 0.0 || result.zoom > 100.0) throw std::invalid_argument("数据拓扑视图参数无效"); std::map valid_nodes; for(const auto& source : global->mode_acs.data_source_config.map.list()) valid_nodes.emplace("data_source:" + source->key, true); for(const auto& feed : global->mode_acs.data_feed_config.map.list()) valid_nodes.emplace("data_feed:" + feed->key, true); const auto positions = input.find("node_positions"); if(positions == input.end() || !positions->is_object()) throw std::invalid_argument("节点位置必须是对象"); for(const auto& [id, position] : positions->items()) { if(!valid_nodes.contains(id) || !position.is_object()) continue; const auto x_field = position.find("x"); const auto y_field = position.find("y"); if(x_field == position.end() || y_field == position.end() || !x_field->is_number() || !y_field->is_number()) throw std::invalid_argument("节点位置缺少数值坐标: " + id); const auto x = x_field->get(); const auto y = y_field->get(); if(!std::isfinite(x) || !std::isfinite(y)) throw std::invalid_argument("节点位置不是有限数值: " + id); result.node_positions.emplace(id, Data_Topology_Node_Position{x, y}); } return result; } template void complete_business_request(std::function& callback, Function&& function) noexcept { callback(adminive::make_drogon_response(business_response(std::forward(function)))); } adminive::Collection_Query read_collection_query(const drogon::HttpRequestPtr& request) { adminive::Collection_Query query; query.page = adminive::read_drogon_size_parameter(request, "page", 1); query.per_page = adminive::read_drogon_size_parameter(request, "perPage", 20); query.order_by = request->getParameter("orderBy"); query.order_dir = request->getParameter("orderDir"); for(const auto& [name, value] : request->getParameters()) { if(name != "page" && name != "perPage" && name != "orderBy" && name != "orderDir") query.fields.insert_or_assign(name, value); } return query; } std::vector read_order_ids(std::string_view body) { const Json input = adminive::parse_json(body); if(!input.contains("ids")) throw std::invalid_argument("missing ids"); const auto& value = input.at("ids"); std::vector result; if(value.is_array()) { result.reserve(value.size()); for(const auto& item : value) result.push_back(item.is_string() ? std::stoull(item.get()) : item.get()); return result; } const std::string text = value.get(); std::size_t begin{}; while(begin < text.size()) { const auto end = text.find(',', begin); const auto token = std::string_view(text).substr(begin, end == std::string::npos ? text.size() - begin : end - begin); std::uint64_t id{}; const auto [position, error] = std::from_chars(token.data(), token.data() + token.size(), id); if(error != std::errc{} || position != token.data() + token.size()) throw std::invalid_argument("invalid order id"); result.push_back(id); if(end == std::string::npos) break; begin = end + 1; } return result; } template std::shared_ptr item_by_id(const std::vector>& list, std::uint64_t id) { if(id == 0 || id > list.size()) throw std::out_of_range("record not found"); return list[id - 1]; } template std::vector ordered_keys(const std::vector>& list, const std::vector& ids) { if(ids.size() != list.size()) throw std::invalid_argument("order must contain every record"); std::vector used(list.size()); std::vector keys; keys.reserve(ids.size()); for(const auto id : ids) { if(id == 0 || id > list.size() || used[id - 1]) throw std::invalid_argument("invalid order id"); used[id - 1] = true; keys.push_back(list[id - 1]->key); } return keys; } std::string source_type_name(Data_Source_Type value) { return std::string(magic_enum::enum_name(value)); } std::string feed_type_name(Data_Feed_Type value) { return std::string(magic_enum::enum_name(value)); } Data_Source_Type source_type(std::string_view value) { const auto result = magic_enum::enum_cast(value); if(!result) throw std::invalid_argument("未知数据源类型: " + std::string(value)); return *result; } Data_Feed_Type feed_type(std::string_view value) { const auto result = magic_enum::enum_cast(value); if(!result) throw std::invalid_argument("未知数据馈送类型: " + std::string(value)); return *result; } Data_Source_Row source_row(const std::shared_ptr& source) { Data_Source_Row row; row.key = source->key; row.type = source_type(source->type); row.enable = source->enabled(); source->settings.read([&](const auto& value) { row.config = value; }); row.state = Psc::to_string(source->state); if(const auto source_value = dynamic_cast(source.get())) { source_value->specific.read([&](const auto& value) { row.ip = value.ip; row.port = value.port; }); } else if(const auto source_value = dynamic_cast(source.get())) { source_value->specific.read([&](const auto& value) { row.port_name = value.port_name; row.baud_rate = value.baud_rate; }); } else if(const auto source_value = dynamic_cast(source.get())) { source_value->specific.read([&](const auto& value) { row.file_path = value.file_path; row.data_type = value.data_type; row.play_mode = value.play_mode; }); } else if(const auto source_value = dynamic_cast(source.get())) { source_value->specific.read([&](const auto& value) { row.library_path = value.library_path; row.function_name = value.function_name; row.data_type = value.data_type; row.buffer_size = value.buffer_size; }); } else if(const auto source_value = dynamic_cast(source.get())) { source_value->specific.read([&](const auto& value) { row.shared_memory_name = value.shared_memory_name; row.shared_memory_size = value.shared_memory_size; row.data_type = value.data_type; }); } return row; } void apply_source_row(const std::shared_ptr& source, const Data_Source_Row& row) { source->set_enabled(row.enable); source->settings.write([&](auto& value) { value = row.config; }); if(auto source_value = dynamic_cast(source.get())) { source_value->specific.write([&](auto& value) { value.ip = row.ip; value.port = row.port; }); } else if(auto source_value = dynamic_cast(source.get())) { source_value->specific.write([&](auto& value) { value.port_name = row.port_name; value.baud_rate = row.baud_rate; }); } else if(auto source_value = dynamic_cast(source.get())) { source_value->specific.write([&](auto& value) { value.file_path = row.file_path; value.data_type = row.data_type; value.play_mode = row.play_mode; }); } else if(auto source_value = dynamic_cast(source.get())) { source_value->specific.write([&](auto& value) { value.library_path = row.library_path; value.function_name = row.function_name; value.data_type = row.data_type; value.buffer_size = row.buffer_size; }); source_value->buffer.resize(row.buffer_size); } else if(auto source_value = dynamic_cast(source.get())) { source_value->specific.write([&](auto& value) { value.shared_memory_name = row.shared_memory_name; value.shared_memory_size = row.shared_memory_size; value.data_type = row.data_type; }); } } Data_Feed_Row feed_row(const std::shared_ptr& feed) { Data_Feed_Row row; row.key = feed->key; row.type = feed_type(feed->type); row.enable = feed->enabled(); row.source_key = feed->source_key; feed->output_format.read([&](const auto& value) { row.output_format = value; row.format = value.type; }); row.state = Psc::to_string(feed->state); if(const auto feed_value = dynamic_cast(feed.get())) { feed_value->specific.read([&](const auto& value) { row.port = value.port; row.connect_user_buffer_size = value.connect_user_buffer_size; row.connect_system_buffer_size = value.connect_system_buffer_size; }); } else if(const auto feed_value = dynamic_cast(feed.get())) { feed_value->specific.read([&](const auto& value) { row.port = value.port; }); } else if(const auto feed_value = dynamic_cast(feed.get())) { feed_value->specific.read([&](const auto& value) { row.url = value.url; row.port = value.port; }); } else if(const auto feed_value = dynamic_cast(feed.get())) { feed_value->specific.read([&](const auto& value) { row.url = value.url; row.port = value.port; }); } return row; } void apply_feed_row(const std::shared_ptr& feed, const Data_Feed_Row& row) { feed->set_enabled(row.enable); feed->source_key = row.source_key; feed->output_format.write([&](auto& value) { value = row.output_format; }); if(auto feed_value = dynamic_cast(feed.get())) { feed_value->specific.write([&](auto& value) { value.port = row.port; value.connect_user_buffer_size = row.connect_user_buffer_size; value.connect_system_buffer_size = row.connect_system_buffer_size; }); } else if(auto feed_value = dynamic_cast(feed.get())) { feed_value->specific.member<&Data_Feed_UDP_Server_Data::port>().write([&](auto& value) { value = row.port; }); } else if(auto feed_value = dynamic_cast(feed.get())) { feed_value->specific.write([&](auto& value) { value.url = row.url; value.port = row.port; }); } else if(auto feed_value = dynamic_cast(feed.get())) { feed_value->specific.write([&](auto& value) { value.url = row.url; value.port = row.port; }); } } template adminive::Http_Response list_response(std::string path, std::vector rows, adminive::Collection_Query query) { adminive::Collection_Service collection(std::move(path), std::move(rows)); return collection.list_response(std::move(query)); } template adminive::Http_Response item_response(std::string path, std::vector rows, std::uint64_t id) { adminive::Collection_Service collection(std::move(path), std::move(rows)); return collection.item_response(id); } template void bind_collection_read_handlers(drogon::HttpAppFramework& app, std::string path, Descriptor_Factory descriptor_factory, Rows_Factory rows_factory, Schema_Factory schema_factory, bool bind_item) { const auto get = adminive::make_drogon_constraints(drogon::Get, {}); app.registerHandler(path + "/descriptor", [descriptor_factory](const drogon::HttpRequestPtr&, std::function&& callback) { callback(adminive::make_drogon_response(adminive::make_http_success(descriptor_factory()))); }, get); app.registerHandler(path + "/view", [](const drogon::HttpRequestPtr&, std::function&& callback) { callback(adminive::make_drogon_response(adminive::make_http_success(adminive::to_view_json(adminive::describe_table_view())))); }, get); app.registerHandler(path + "/amis", [schema_factory](const drogon::HttpRequestPtr&, std::function&& callback) { callback(adminive::make_drogon_response(adminive::make_http_success(schema_factory()))); }, get); app.registerHandler(path, [path, rows_factory](const drogon::HttpRequestPtr& request, std::function&& callback) { callback(adminive::make_drogon_response(list_response(path, rows_factory(), read_collection_query(request)))); }, get); if(bind_item) { app.registerHandler(path + "/{1:id}", [path, rows_factory](const drogon::HttpRequestPtr&, std::function&& callback, std::uint64_t id) { callback(adminive::make_drogon_response(item_response(path, rows_factory(), id))); }, get); } } std::vector source_rows(Global* global) { std::vector rows; for(const auto& source : global->mode_acs.data_source_config.map.list()) rows.push_back(source_row(source)); return rows; } std::vector feed_rows(Global* global) { std::vector rows; for(const auto& feed : global->mode_acs.data_feed_config.map.list()) rows.push_back(feed_row(feed)); return rows; } Json data_topology_view(Global* global) { auto result = global->mode_acs.data_topology_view.read([](const auto& value) { return adminive::parse_json(value.to_json().to_json_string()); }); result["protocol"] = "ecap.data-topology-view"; result["protocol_version"] = 1; return result; } Json data_topology(Global* global) { const auto sources = global->mode_acs.data_source_config.map.list(); const auto feeds = global->mode_acs.data_feed_config.map.list(); std::map> source_by_key; Json nodes = Json::array(); Json edges = Json::array(); for(std::size_t index = 0; index < sources.size(); ++index) { const auto& source = sources[index]; const auto id = "data_source:" + source->key; source_by_key.emplace(source->key, source); nodes.push_back(Json{{"id", id}, {"kind", "data_source"}, {"entity_id", index + 1}, {"key", source->key}, {"label", source->key}, {"type", source->type}, {"enabled", source->enabled()}, {"status_api", data_topology_path + "/status/data_source/" + std::to_string(index + 1)}}); } for(std::size_t index = 0; index < feeds.size(); ++index) { const auto& feed = feeds[index]; const auto id = "data_feed:" + feed->key; nodes.push_back(Json{{"id", id}, {"kind", "data_feed"}, {"entity_id", index + 1}, {"key", feed->key}, {"label", feed->key}, {"type", feed->type}, {"enabled", feed->enabled()}, {"status_api", data_topology_path + "/status/data_feed/" + std::to_string(index + 1)}}); const auto source = source_by_key.find(feed->source_key); if(source != source_by_key.end()) { edges.push_back(Json{{"id", "source-feed:" + feed->key}, {"source", "data_source:" + feed->source_key}, {"target", id}, {"active", source->second->enabled() && feed->enabled()}}); } } return Json{{"protocol", "ecap.data-topology"}, {"protocol_version", 1}, {"nodes", std::move(nodes)}, {"edges", std::move(edges)}, {"view", data_topology_view(global)}}; } std::vector external_resource_rows(Global* global) { std::map status_by_name; for(auto status : global->external_resources_manager.external_database_status()) status_by_name.emplace(status.name, std::move(status)); const auto config = global->external_resources_manager.to_base_json(); const auto* list = config.get("list"); std::vector rows; if(!list) return rows; rows.reserve(list->children.size()); for(const auto& item : list->children) { External_Resources_Data value; value.from_base_json(&item, false); External_Resource_Row row; row.name = value.name; row.description = value.description; row.download_url = value.download_url; row.update_interval_seconds = value.update_interval_seconds; row.last_updated_at = static_cast(value.last_updated_at); const auto status = status_by_name.find(value.name); if(status != status_by_name.end()) { row.row_count = status->second.row_count; row.state = status->second.message; } rows.push_back(std::move(row)); } return rows; } Json source_amis_schema() { return adminive::to_amis_table_schema("/api/adminive/config/data_sources", adminive::describe_table_view()); } void set_select_options(Json& value, std::string_view field_name, const Json& options) { if(value.is_object()) { const auto name = value.find("name"); if(name != value.end() && name->is_string() && name->get() == field_name) { value["type"] = "select"; value["options"] = options; value["clearable"] = true; value["placeholder"] = "未连接"; } for(auto& [key, child] : value.items()) { static_cast(key); set_select_options(child, field_name, options); } } else if(value.is_array()) { for(auto& child : value) set_select_options(child, field_name, options); } } Json feed_amis_schema(Global* global) { auto schema = adminive::to_amis_table_schema("/api/adminive/config/data_feeds", adminive::describe_table_view()); Json options = Json::array(); for(const auto& source : global->mode_acs.data_source_config.map.list()) options.push_back(Json{{"label", source->key}, {"value", source->key}}); set_select_options(schema, "source_key", options); return schema; } Json data_topology_schema(Global* global) { return Json{ {"title", "数据源与数据馈送拓扑"}, {"data_api", data_topology_path}, {"view_api", data_topology_path + "/view"}, {"graph", Json{ {"height", 620}, {"layout", Json{{"type", "dagre"}, {"rankdir", "LR"}, {"align", "UL"}, {"nodesep", 34}, {"ranksep", 100}}}, {"behaviors", Json::array({"drag-canvas", "zoom-canvas", "drag-element"})}, {"active_edge_color", "#52c41a"}, {"inactive_edge_color", "#bfbfbf"} }}, {"node_kinds", Json::array({ Json{{"kind", "data_source"}, {"title", "数据源"}, {"fill", "#e6f4ff"}, {"stroke", "#1677ff"}}, Json{{"kind", "data_feed"}, {"title", "数据馈送"}, {"fill", "#f6ffed"}, {"stroke", "#52c41a"}} })}, {"status", Json{{"title", "运行状态"}, {"poll_interval_ms", 1000}, {"collapsed_hint", "展开后才向后台轮询,收起立即停止"}}}, {"editors", Json::array({ Json{{"kind", "data_source"}, {"title", "数据源管理"}, {"button_label", "管理数据源"}, {"schema", source_amis_schema()}}, Json{{"kind", "data_feed"}, {"title", "数据馈送管理"}, {"button_label", "管理数据馈送"}, {"schema", feed_amis_schema(global)}} })} }; } Json external_resource_amis_schema() { auto schema = adminive::to_amis_table_schema("/api/adminive/config/external_resources", adminive::describe_table_view()); auto& crud = schema["body"][0]; const std::string crud_id = "external_resource_table_crud"; crud["headerToolbar"] = Json::array({Json{{"type", "columns-toggler"}, {"draggable", true}}, "reload", Json{{"type", "button"}, {"label", "全部后端下载并导入"}, {"level", "primary"}, {"actionType", "ajax"}, {"api", Json{{"method", "post"}, {"url", "/api/refresh_external_databases"}}}, {"reload", crud_id}}}); auto& columns = crud["columns"]; if(!columns.empty()) columns.erase(columns.end() - 1); const auto refresh_button = Json{{"type", "button"}, {"label", "后端下载并导入"}, {"level", "link"}, {"actionType", "ajax"}, {"api", Json{{"method", "post"}, {"url", "/api/refresh_external_database"}, {"data", Json{{"name", "${name}"}}}}}, {"reload", crud_id}}; const auto download_button = Json{{"type", "button"}, {"label", "前端下载"}, {"level", "link"}, {"actionType", "url"}, {"url", "${download_url}"}, {"blank", true}, {"disabledOn", "!this.download_url"}}; const auto upload_button = Json{{"type", "button"}, {"label", "离线导入"}, {"level", "link"}, {"actionType", "dialog"}, {"dialog", Json{{"title", "离线导入 ${name}"}, {"body", Json{{"type", "input-file"}, {"name", "file"}, {"label", "数据文件"}, {"accept", ".csv,.gz,.zip,.txt"}, {"receiver", Json{{"method", "post"}, {"url", "/api/upload_external_database"}, {"data", Json{{"name", "${name}"}}}}}}}}}}; const auto clear_button = Json{{"type", "button"}, {"label", "清空数据表"}, {"level", "link"}, {"className", "text-danger"}, {"actionType", "ajax"}, {"confirmText", "确定清空 ${name}?"}, {"api", Json{{"method", "post"}, {"url", "/api/clear_external_database_table"}, {"data", Json{{"name", "${name}"}}}}}, {"reload", crud_id}}; columns.push_back(Json{{"type", "operation"}, {"label", "操作"}, {"buttons", Json::array({refresh_button, download_button, upload_button, clear_button})}}); return schema; } Json backend_config_schema() { return Json{{"title", "后端完整配置(只读)"}, {"data_api", "/api/adminive/config/backend_config"}}; } Json archive_operations_schema() { const auto formats = Json::array({Json{{"label", "ZIP (.zip)"}, {"value", "zip"}, {"format", "zip"}, {"filter", ""}, {"extension", "zip"}}, Json{{"label", "TAR (.tar)"}, {"value", "tar"}, {"format", "pax"}, {"filter", ""}, {"extension", "tar"}}, Json{{"label", "TAR.GZ (.tar.gz)"}, {"value", "tar.gz"}, {"format", "pax"}, {"filter", "gzip"}, {"extension", "tar.gz"}}, Json{{"label", "TAR.XZ (.tar.xz)"}, {"value", "tar.xz"}, {"format", "pax"}, {"filter", "xz"}, {"extension", "tar.xz"}}, Json{{"label", "TAR.ZST (.tar.zst)"}, {"value", "tar.zst"}, {"format", "pax"}, {"filter", "zstd"}, {"extension", "tar.zst"}}, Json{{"label", "TAR.BZ2 (.tar.bz2)"}, {"value", "tar.bz2"}, {"format", "pax"}, {"filter", "bzip2"}, {"extension", "tar.bz2"}}, Json{{"label", "TAR.LZ4 (.tar.lz4)"}, {"value", "tar.lz4"}, {"format", "pax"}, {"filter", "lz4"}, {"extension", "tar.lz4"}}, Json{{"label", "7Z (.7z)"}, {"value", "7z"}, {"format", "7zip"}, {"filter", ""}, {"extension", "7z"}}}); return Json{{"title", "导入导出"}, {"formats", std::move(formats)}, {"exports", Json::array({Json{{"name", "版本"}, {"api", "/api/export_version"}, {"allow_version_selection", true}}, Json{{"name", "日志"}, {"api", "/api/export_logs"}, {"allow_version_selection", false}}})}, {"import_api", "/api/import_version"}, {"import_accept", ".tar.zstd,.tar.zst,.tar.gz,.tgz,.tar.xz,.txz,.tar.bz2,.tbz2,.tar.lz4,.zip,.7z,.rar,.tar,.cpio,.iso,.xar"}}; } Json data_source_descriptor() { auto descriptor = adminive::to_descriptor_json(); const auto tile_view_descriptor = adminive::to_descriptor_json(); const auto map_view_descriptor = adminive::to_descriptor_json(); const auto graphics_descriptor = adminive::to_descriptor_json(); descriptor["sidebar"] = Json{ {"source", Json{ {"title", "数据源配置"}, {"sections", Json::array({ Json{{"layout", Json::array({Json{{"kind", "cards"}, {"fields", Json::array({"update_form_gps", "base_station_has_valid_position", "keep_mode", "ignore_msg_time"})}}})}}, Json{{"layout", Json::array({Json{{"kind", "vertical"}, {"fields", Json::array({"lat", "lon", "alt"})}}})}}, Json{ {"title", "2D显示配置"}, {"modes", Json::array({"map2d"})}, {"object_path", "map_display.map2d"}, {"layout", Json::array({ Json{{"kind", "cards"}, {"fields", Json::array({"aircraft_show", "base_station_show", "show_icao", "show_call_sign", "show_fly_status"})}}, Json{{"kind", "vertical"}, {"fields", Json::array({"aircraft_pixel_size", "base_station_pixel_size", "track_point_pixel_size", "color", "track_point_color", "base_station_color"})}} })} }, Json{ {"title", "3D显示配置"}, {"modes", Json::array({"map3d"})}, {"object_path", "map_display.map3d"}, {"layout", Json::array({ Json{{"kind", "cards"}, {"fields", Json::array({"aircraft_show", "base_station_show", "constant_screen_size", "show_icao", "show_call_sign", "show_fly_status"})}}, Json{{"kind", "vertical"}, {"fields", Json::array({"aircraft_scale", "base_station_scale", "track_point_pixel_size", "color", "track_point_color", "base_station_color"})}} })} } })}, {"rules", Json::array({ Json{{"field", "base_station_has_valid_position"}, {"disabled_when", Json{{"field", "update_form_gps"}, {"equals", true}}}}, Json{{"field", "lat"}, {"disabled_when", Json{{"field", "update_form_gps"}, {"equals", true}}}}, Json{{"field", "lon"}, {"disabled_when", Json{{"field", "update_form_gps"}, {"equals", true}}}}, Json{{"field", "alt"}, {"disabled_when", Json{{"field", "update_form_gps"}, {"equals", true}}}} })} }}, {"tiles", Json{ {"title", "瓦片显示"}, {"layout", Json::array({Json{{"kind", "vertical"}, {"fields", Json::array({"tile_display_maximum_level"})}}})}, {"descriptor_fields", tile_view_descriptor.at("fields")} }}, {"view3d", Json{ {"graphics", Json{ {"title", "辅助显示"}, {"layout", Json::array({Json{{"kind", "cards"}, {"fields", Json::array({"surface_navigation_reference_visible", "view_axes_visible"})}}})}, {"descriptor_fields", graphics_descriptor.at("fields")} }}, {"map_view", Json{ {"title", "定位视图"}, {"layout", Json::array({Json{{"kind", "vertical"}, {"fields", Json::array({"base_station_fly_to_height_offset_meters"})}}})}, {"descriptor_fields", map_view_descriptor.at("fields")} }} }} }; return descriptor; } Json cesium_models_schema(Global* global) { Json aircraft_types = Json::array(); global->map_model_config.read([&](const auto& config) { for(const auto& [key, value] : config.aircraft_models) { static_cast(value); auto label = key; for(std::uint8_t raw = 1; raw <= 21; ++raw) { const auto type = static_cast(raw); if(SSR::Vortex_Type_to_key(type) == key) { label = SSR::Vortex_Type_to_label(type); break; } } aircraft_types.push_back(Json{{"key", key}, {"label", label}}); } }); const auto item_descriptor = adminive::to_descriptor_json(); return Json{ {"title", "Cesium 模型设置"}, {"data_api", "/map/models"}, {"upload_accept", ".glb"}, {"item_descriptor", item_descriptor}, {"groups", Json::array({ Json{{"title", "默认飞机模型"}, {"path", "aircraft_model"}, {"upload", Json{{"model_type", "aircraft"}, {"label", "上传飞机模型"}, {"success_message", "默认飞机模型已更新"}}}}, Json{{"title", "基站模型"}, {"path", "base_station_model"}, {"upload", Json{{"model_type", "base_station"}, {"label", "上传基站模型"}, {"success_message", "基站模型已更新"}}}}, Json{{"title", "设备模型"}, {"path", "device_model"}, {"upload", Json{{"model_type", "device"}, {"label", "上传设备模型"}, {"success_message", "设备模型已更新"}}}} })}, {"collections", Json::array({ Json{{"title", "按涡流/目标类型选择飞机模型"}, {"path", "aircraft_models"}, {"upload_model_type", "aircraft_type"}, {"upload_label", "上传 GLB"}, {"upload_success_message", "分类飞机模型已更新"}, {"items", std::move(aircraft_types)}} })} }; } Json system_actions_schema() { return Json{{"type", "panel"}, {"title", "设备操作"}, {"body", Json::array({Json{{"type", "button"}, {"label", "重启 FPGA 设备"}, {"level", "primary"}, {"actionType", "ajax"}, {"confirmText", "确定重启 FPGA 设备?"}, {"api", Json{{"method", "post"}, {"url", "/api/restart_device"}}}}})}}; } class Adminive_Config_Resources { public: explicit Adminive_Config_Resources(Global* global) : global_(global), mode_acs_resource_(global->mode_acs.settings, mode_acs_path, mode_acs_transaction()), data_feed_settings_resource_(global->mode_acs.data_feed_config.settings, data_feed_settings_path, data_feed_settings_transaction()), map_view_resource_(global->map_view_config, map_view_path, map_view_transaction(global)), mlat_resource_(global->mlat.settings, mlat_path, value_section_transaction(Config_Section::mlat)), cesium_graphics_resource_(global->cesium_graphics_config.settings, cesium_graphics_path, value_section_transaction(Config_Section::cesium_graphics)) { } void bind() { auto& app = drogon::app(); mode_acs_resource_.bind(app); data_feed_settings_resource_.bind(app); map_view_resource_.bind(app); mlat_resource_.bind(app); cesium_graphics_resource_.bind(app); bind_data_sources(app); bind_data_feeds(app); bind_data_topology(app); bind_external_resources(app); bind_backend_config(app); app.registerHandler(manifest_path, [this](const drogon::HttpRequestPtr&, std::function&& callback) { callback(adminive::make_drogon_response(adminive::make_http_success(manifest()))); }, {drogon::Get}); } private: void bind_data_topology(drogon::HttpAppFramework& app) { const auto get = adminive::make_drogon_constraints(drogon::Get, {}); const auto put = adminive::make_drogon_constraints(drogon::Put, {}); app.registerHandler(data_topology_path + "/schema", [this](const drogon::HttpRequestPtr&, std::function&& callback) { complete_business_request(callback, [this] { std::lock_guard lock(topology_mutex_); return adminive::make_http_success(data_topology_schema(global_)); }); }, get); app.registerHandler(data_topology_path, [this](const drogon::HttpRequestPtr&, std::function&& callback) { complete_business_request(callback, [this] { std::lock_guard lock(topology_mutex_); return adminive::make_http_success(data_topology(global_)); }); }, get); app.registerHandler(data_topology_path + "/view", [this](const drogon::HttpRequestPtr&, std::function&& callback) { complete_business_request(callback, [this] { std::lock_guard lock(topology_mutex_); return adminive::make_http_success(data_topology_view(global_)); }); }, get); app.registerHandler(data_topology_path + "/view", [this](const drogon::HttpRequestPtr& request, std::function&& callback) { complete_business_request(callback, [this, body = std::string(request->getBody())] { std::lock_guard lock(topology_mutex_); auto value = read_topology_view(read_request_body(body), global_); global_->mode_acs.data_topology_view.write([&](auto& current) { current = std::move(value); }); Config::save(Config_Section::mode_acs, global_->mode_acs.to_base_json()); return adminive::make_http_success(data_topology_view(global_), "saved"); }); }, put); app.registerHandler(data_topology_path + "/status/data_source/{1:id}", [this](const drogon::HttpRequestPtr&, std::function&& callback, std::uint64_t id) { complete_business_request(callback, [this, id] { std::lock_guard lock(topology_mutex_); const auto source = item_by_id(global_->mode_acs.data_source_config.map.list(), id); return adminive::make_http_success(adminive::parse_json(source->get_state().to_json_string())); }); }, get); app.registerHandler(data_topology_path + "/status/data_feed/{1:id}", [this](const drogon::HttpRequestPtr&, std::function&& callback, std::uint64_t id) { complete_business_request(callback, [this, id] { std::lock_guard lock(topology_mutex_); const auto feed = item_by_id(global_->mode_acs.data_feed_config.map.list(), id); return adminive::make_http_success(adminive::parse_json(feed->get_state_json().to_json_string())); }); }, get); } void bind_backend_config(drogon::HttpAppFramework& app) { const auto get = adminive::make_drogon_constraints(drogon::Get, {}); app.registerHandler(backend_config_path, [this](const drogon::HttpRequestPtr&, std::function&& callback) { callback(adminive::make_drogon_response(adminive::make_http_success(Json::parse(global_->toJson().to_json_string())))); }, get); } void bind_external_resources(drogon::HttpAppFramework& app) { bind_collection_read_handlers(app, external_resources_path, [] { return adminive::to_descriptor_json(); }, [this] { return external_resource_rows(global_); }, [] { return external_resource_amis_schema(); }, false); } void bind_data_sources(drogon::HttpAppFramework& app) { const auto post = adminive::make_drogon_constraints(drogon::Post, {}); const auto put = adminive::make_drogon_constraints(drogon::Put, {}); const auto patch = adminive::make_drogon_constraints(drogon::Patch, {}); const auto remove = adminive::make_drogon_constraints(drogon::Delete, {}); bind_collection_read_handlers(app, data_sources_path, [] { return data_source_descriptor(); }, [this] { return source_rows(global_); }, [] { return source_amis_schema(); }, true); app.registerHandler(data_sources_path, [this](const drogon::HttpRequestPtr& request, std::function&& callback) { complete_business_request(callback, [this, body = std::string(request->getBody())] { std::lock_guard lock(topology_mutex_); auto input = read_request_body(body); Data_Source_Row row; const auto result = adminive::apply_frontend_create(row, input); if(!result.success) return adminive::make_http_error(422, result); if(global_->mode_acs.data_source_config.map.get(row.key)) return adminive::make_http_error(409, "数据源名称已存在: " + row.key); auto source = create_data_source_from_type(source_type_name(row.type)); source->key = row.key; apply_source_row(source, row); global_->mode_acs.data_source_config.map.push_back(source); Data_feed_Config::set_connections_need_refresh(); save_data_sources(global_); return adminive::make_http_success(adminive::to_frontend_json(source_row(source)), "created"); }); }, post); const auto update = [this](const drogon::HttpRequestPtr& request, std::function&& callback, std::uint64_t id) { complete_business_request(callback, [this, id, body = std::string(request->getBody())] { std::lock_guard lock(topology_mutex_); const auto list = global_->mode_acs.data_source_config.map.list(); auto source = item_by_id(list, id); auto input = read_request_body(body); auto row = source_row(source); const auto result = adminive::apply_frontend_patch(row, input); if(!result.success) return adminive::make_http_error(422, result); apply_source_row(source, row); Data_feed_Config::set_connections_need_refresh(); save_data_sources(global_); return adminive::make_http_success(adminive::to_frontend_json(source_row(source)), result.message); }); }; app.registerHandler(data_sources_path + "/{1:id}", update, put); app.registerHandler(data_sources_path + "/{1:id}", update, patch); app.registerHandler(data_sources_path + "/{1:id}", [this](const drogon::HttpRequestPtr&, std::function&& callback, std::uint64_t id) { complete_business_request(callback, [this, id] { std::lock_guard lock(topology_mutex_); const auto list = global_->mode_acs.data_source_config.map.list(); auto source = item_by_id(list, id); if(global_->mode_acs.data_feed_config.source_has_connection(source->key)) throw Conflict_Error("数据源仍被数据馈送连接,不能删除: " + source->key); source->async_stop(); if(!global_->mode_acs.data_source_config.map.remove(id - 1)) throw std::out_of_range("record not found"); Data_feed_Config::set_connections_need_refresh(); save_data_sources(global_); return adminive::make_http_success(adminive::json_object(), "deleted"); }); }, remove); app.registerHandler(data_sources_path + "/order", [this](const drogon::HttpRequestPtr& request, std::function&& callback) { complete_business_request(callback, [this, body = std::string(request->getBody())] { std::lock_guard lock(topology_mutex_); const auto list = global_->mode_acs.data_source_config.map.list(); const auto keys = ordered_keys(list, read_order_ids(body)); if(!global_->mode_acs.data_source_config.map.set_order(keys)) throw std::invalid_argument("invalid order"); Data_feed_Config::set_connections_need_refresh(); save_data_sources(global_); return adminive::make_http_success(adminive::json_object(), "reordered"); }); }, post); } void bind_data_feeds(drogon::HttpAppFramework& app) { const auto post = adminive::make_drogon_constraints(drogon::Post, {}); const auto put = adminive::make_drogon_constraints(drogon::Put, {}); const auto patch = adminive::make_drogon_constraints(drogon::Patch, {}); const auto remove = adminive::make_drogon_constraints(drogon::Delete, {}); bind_collection_read_handlers(app, data_feeds_path, [] { return adminive::to_descriptor_json(); }, [this] { return feed_rows(global_); }, [this] { return feed_amis_schema(global_); }, true); app.registerHandler(data_feeds_path, [this](const drogon::HttpRequestPtr& request, std::function&& callback) { complete_business_request(callback, [this, body = std::string(request->getBody())] { std::lock_guard lock(topology_mutex_); auto input = read_request_body(body); Data_Feed_Row row; const auto result = adminive::apply_frontend_create(row, input); if(!result.success) return adminive::make_http_error(422, result); if(global_->mode_acs.data_feed_config.map.get(row.key)) return adminive::make_http_error(409, "数据馈送名称已存在: " + row.key); if(!row.source_key.empty() && !global_->mode_acs.data_source_config.map.get(row.source_key)) throw std::invalid_argument("连接的数据源不存在: " + row.source_key); auto feed = create_data_feed_from_type(feed_type_name(row.type)); feed->key = row.key; apply_feed_row(feed, row); global_->mode_acs.data_feed_config.map.push_back(feed); Data_feed_Config::set_connections_need_refresh(); save_data_feeds(global_); return adminive::make_http_success(adminive::to_frontend_json(feed_row(feed)), "created"); }); }, post); const auto update = [this](const drogon::HttpRequestPtr& request, std::function&& callback, std::uint64_t id) { complete_business_request(callback, [this, id, body = std::string(request->getBody())] { std::lock_guard lock(topology_mutex_); const auto list = global_->mode_acs.data_feed_config.map.list(); auto feed = item_by_id(list, id); auto input = read_request_body(body); auto row = feed_row(feed); const auto result = adminive::apply_frontend_patch(row, input); if(!result.success) return adminive::make_http_error(422, result); if(!row.source_key.empty() && !global_->mode_acs.data_source_config.map.get(row.source_key)) throw std::invalid_argument("连接的数据源不存在: " + row.source_key); apply_feed_row(feed, row); Data_feed_Config::set_connections_need_refresh(); save_data_feeds(global_); return adminive::make_http_success(adminive::to_frontend_json(feed_row(feed)), result.message); }); }; app.registerHandler(data_feeds_path + "/{1:id}", update, put); app.registerHandler(data_feeds_path + "/{1:id}", update, patch); app.registerHandler(data_feeds_path + "/{1:id}", [this](const drogon::HttpRequestPtr&, std::function&& callback, std::uint64_t id) { complete_business_request(callback, [this, id] { std::lock_guard lock(topology_mutex_); const auto list = global_->mode_acs.data_feed_config.map.list(); auto feed = item_by_id(list, id); feed->async_stop(); if(!global_->mode_acs.data_feed_config.map.remove(id - 1)) throw std::out_of_range("record not found"); Data_feed_Config::set_connections_need_refresh(); save_data_feeds(global_); return adminive::make_http_success(adminive::json_object(), "deleted"); }); }, remove); app.registerHandler(data_feeds_path + "/order", [this](const drogon::HttpRequestPtr& request, std::function&& callback) { complete_business_request(callback, [this, body = std::string(request->getBody())] { std::lock_guard lock(topology_mutex_); const auto list = global_->mode_acs.data_feed_config.map.list(); const auto keys = ordered_keys(list, read_order_ids(body)); if(!global_->mode_acs.data_feed_config.map.set_order(keys)) throw std::invalid_argument("invalid order"); Data_feed_Config::set_connections_need_refresh(); save_data_feeds(global_); return adminive::make_http_success(adminive::json_object(), "reordered"); }); }, post); } Json manifest() const { const auto view = adminive::composition_view("ecap_config", adminive::compose::frontend(adminive::compose::slot("mode_acs"), adminive::compose::slot("data_feed_settings"), adminive::compose::slot("mlat"), adminive::compose::slot("map_view"), adminive::compose::slot("cesium_graphics"), adminive::compose::slot("external_resources"), adminive::compose::slot("archive_operations"), adminive::compose::slot("cesium_models"), adminive::compose::slot("backend_config"), adminive::compose::slot("system_actions"))).titled("ECAP 配置"); const std::vector contracts = { {"mode_acs", "amis_schema", mode_acs_path + "/descriptor", mode_acs_path + "/view", mode_acs_path + "/data", mode_acs_path + "/amis"}, {"data_feed_settings", "amis_schema", data_feed_settings_path + "/descriptor", data_feed_settings_path + "/view", data_feed_settings_path + "/data", data_feed_settings_path + "/amis"}, {"mlat", "amis_schema", mlat_path + "/descriptor", mlat_path + "/view", mlat_path + "/data", mlat_path + "/amis"}, {"map_view", "amis_schema", map_view_path + "/descriptor", map_view_path + "/view", map_view_path + "/data", map_view_path + "/amis"}, {"cesium_graphics", "amis_schema", cesium_graphics_path + "/descriptor", cesium_graphics_path + "/view", cesium_graphics_path + "/data", cesium_graphics_path + "/amis"}, {"external_resources", "amis_schema", external_resources_path + "/descriptor", external_resources_path + "/view", external_resources_path, external_resources_path + "/amis"}, {"archive_operations", "ecap_archive_operations", {}, {}, {}, {}}, {"cesium_models", "ecap_cesium_models", {}, {}, "/map/models", {}}, {"backend_config", "ecap_backend_config", {}, {}, backend_config_path, {}}, {"system_actions", "amis_schema", {}, {}, {}, {}} }; const std::map schemas = { {"mode_acs", mode_acs_resource_.amis_schema()}, {"data_feed_settings", data_feed_settings_resource_.amis_schema()}, {"mlat", mlat_resource_.amis_schema()}, {"map_view", map_view_resource_.amis_schema()}, {"cesium_graphics", cesium_graphics_resource_.amis_schema()}, {"external_resources", external_resource_amis_schema()}, {"archive_operations", archive_operations_schema()}, {"cesium_models", cesium_models_schema(global_)}, {"backend_config", backend_config_schema()}, {"system_actions", system_actions_schema()} }; return adminive::to_composition_manifest_json(view, contracts, schemas); } static inline const std::string mode_acs_path = "/api/adminive/config/mode_acs"; static inline const std::string data_feed_settings_path = "/api/adminive/config/data_feed_settings"; static inline const std::string map_view_path = "/api/adminive/config/map_view"; static inline const std::string data_sources_path = "/api/adminive/config/data_sources"; static inline const std::string data_feeds_path = "/api/adminive/config/data_feeds"; static inline const std::string external_resources_path = "/api/adminive/config/external_resources"; static inline const std::string backend_config_path = "/api/adminive/config/backend_config"; static inline const std::string mlat_path = "/api/adminive/config/mlat"; static inline const std::string cesium_graphics_path = "/api/adminive/config/cesium_graphics"; static inline const std::string manifest_path = "/api/adminive/config/manifest"; Global* global_; std::mutex topology_mutex_; adminive::Drogon_Resource mode_acs_resource_; adminive::Drogon_Resource data_feed_settings_resource_; adminive::Drogon_Resource map_view_resource_; adminive::Drogon_Resource mlat_resource_; adminive::Drogon_Resource cesium_graphics_resource_; }; } void register_adminive_config(Global* global) { static auto resources = std::make_unique(global); resources->bind(); } }