#include "web_server.h" #include #include #include "config/config_manager.h" #include "nlohmann/json.hpp" #include "spdlog/spdlog.h" WebServer::WebServer(SystemMonitor::SystemMonitor& monitor, DeviceManager& deviceManager, LiveDataCache& liveDataCache, AlarmService& alarm_service, LightController& lightController, DoorController& doorController, VideoServiceManager& videoManager, // [修复 5.2] 对应头文件添加参数 uint16_t port) : crow::Crow(), m_monitor(monitor), m_device_manager(deviceManager), m_live_data_cache(liveDataCache), m_alarm_service(alarm_service), m_light_controller(lightController), // <--- 初始化成员 m_door_controller(doorController), m_video_manager(videoManager), m_port(port) { auto& cors = this->get_middleware(); cors.global() .origin("*") .headers("Content-Type", "Authorization") .methods("GET"_method, "POST"_method, "OPTIONS"_method); this->loglevel(crow::LogLevel::Warning); setup_routes(); } WebServer::~WebServer() { stop(); } void WebServer::start() { if (m_thread.joinable()) { spdlog::warn("Web server is already running."); return; } m_thread = std::thread([this]() { spdlog::info("Starting Web server on port {}", m_port); this->bindaddr("0.0.0.0").port(m_port).run(); spdlog::info("Web server has stopped."); }); } void WebServer::stop() { crow::Crow::stop(); if (m_thread.joinable()) { m_thread.join(); } } void WebServer::set_shutdown_handler(std::function handler) { m_shutdown_handler = handler; } bool WebServer::validate_video_config(const std::string& json_string, std::string& error_message) { try { auto config = nlohmann::json::parse(json_string); if (!config.is_object()) { error_message = "Root is not an object."; return false; } // 1. 验证 'video_service' if (!config.contains("video_service") || !config["video_service"].is_object()) { error_message = "Missing 'video_service' object."; return false; } if (!config["video_service"].contains("enabled") || !config["video_service"]["enabled"].is_boolean()) { error_message = "'video_service' must have 'enabled' (boolean)."; return false; } // 2. 验证 'video_streams' if (!config.contains("video_streams") || !config["video_streams"].is_array()) { error_message = "Missing 'video_streams' array."; return false; } // 3. 验证 'video_streams' 中的每个元素 for (const auto& stream : config["video_streams"]) { if (!stream.is_object()) { error_message = "Item in 'video_streams' is not an object."; return false; } // 检查必需的键 for (const char* key : {"id", "input_url", "module_type"}) { if (!stream.contains(key) || !stream[key].is_string() || stream[key].get().empty()) { error_message = "Stream missing or invalid key: '" + std::string(key) + "'."; return false; } } if (!stream.contains("enabled") || !stream["enabled"].is_boolean()) { error_message = "Stream missing or invalid key: 'enabled' (boolean)."; return false; } if (!stream.contains("module_config") || !stream["module_config"].is_object()) { error_message = "Stream missing 'module_config' (object)."; return false; } // 4. 验证 'module_config' 的关键字段 const auto& mod_cfg = stream["module_config"]; for (const char* key : {"model_path", "label_path"}) { if (!mod_cfg.contains(key) || !mod_cfg[key].is_string() || mod_cfg[key].get().empty()) { error_message = "module_config missing or empty key: '" + std::string(key) + "'."; return false; } } if (!mod_cfg.contains("class_num") || !mod_cfg["class_num"].is_number_integer() || mod_cfg["class_num"].get() <= 0) { error_message = "module_config missing or invalid 'class_num' (must be " "integer > 0)."; return false; } if (!mod_cfg.contains("rknn_thread_num") || !mod_cfg["rknn_thread_num"].is_number_integer()) { error_message = "module_config missing 'rknn_thread_num' (integer)."; return false; } } return true; // 所有检查通过 } catch (const nlohmann::json::parse_error& e) { error_message = "Invalid JSON: " + std::string(e.what()); return false; } catch (const std::exception& e) { error_message = "Validation error: " + std::string(e.what()); return false; } } bool WebServer::validate_main_config(const std::string& json_string, std::string& error_message) { try { auto config = nlohmann::json::parse(json_string); if (!config.is_object()) { error_message = "Root is not an object."; return false; } // 1. 必需的字符串类型 for (const char* key : {"device_id", "config_base_path", "mqtt_broker", "mqtt_client_id_prefix", "data_storage_db_path", "data_cache_db_path", "log_level", "alarm_rules_path", "piper_executable_path", "piper_model_path", "video_config_path"}) { if (!config.contains(key) || !config[key].is_string() || config[key].get().empty()) { error_message = "Missing or invalid/empty string key: '" + std::string(key) + "'."; return false; } } // 2. 必需的整数类型 if (!config.contains("web_server_port") || !config["web_server_port"].is_number_integer()) { error_message = "Missing or invalid 'web_server_port' (must be integer)."; return false; } // 3. 必需的数组类型 if (!config.contains("tcp_server_ports") || !config["tcp_server_ports"].is_array()) { error_message = "Missing or invalid 'tcp_server_ports' (must be array)."; return false; } for (const auto& port : config["tcp_server_ports"]) { if (!port.is_number_integer()) { error_message = "Item in 'tcp_server_ports' is not an integer."; return false; } } return true; // 所有检查通过 } catch (const nlohmann::json::parse_error& e) { error_message = "Invalid JSON: " + std::string(e.what()); return false; } catch (const std::exception& e) { error_message = "Validation error: " + std::string(e.what()); return false; } } void WebServer::setup_routes() { CROW_ROUTE((*this), "/api/system/id").methods("GET"_method)([this] { auto deviceID = ConfigManager::getInstance().getDeviceID(); crow::json::wvalue response; response["deviceID"] = deviceID; return response; }); CROW_ROUTE((*this), "/api/system/status").methods("GET"_method)([this] { auto cpu_util = m_monitor.getCpuUtilization(); auto mem_info = m_monitor.getMemoryInfo(); crow::json::wvalue response; response["cpu_usage_percentage"] = cpu_util.totalUsagePercentage; response["memory_total_kb"] = mem_info.total_kb; response["memory_free_kb"] = mem_info.available_kb; response["memory_usage_percentage"] = (mem_info.total_kb > 0) ? (1.0 - static_cast(mem_info.available_kb) / mem_info.total_kb) * 100.0 : 0.0; return response; }); CROW_ROUTE((*this), "/api/devices").methods("GET"_method)([this] { auto devices_info = m_device_manager.get_all_device_info(); std::vector devices_json; for (const auto& info : devices_info) { crow::json::wvalue device_obj; device_obj["id"] = info.id; device_obj["type"] = info.type; device_obj["is_running"] = info.is_running; crow::json::wvalue details_obj; for (const auto& pair : info.connection_details) { details_obj[pair.first] = pair.second; } device_obj["connection_details"] = std::move(details_obj); devices_json.push_back(std::move(device_obj)); } auto res = crow::response(crow::json::wvalue(devices_json)); res.set_header("Content-Type", "application/json"); return res; }); CROW_ROUTE((*this), "/api/data/latest").methods("GET"_method)([this] { auto latest_data_map = m_live_data_cache.get_all_data(); crow::json::wvalue response; for (const auto& pair : latest_data_map) { response[pair.first] = crow::json::load(pair.second); } auto res = crow::response(response); res.set_header("Content-Type", "application/json"); return res; }); CROW_ROUTE((*this), "/api/alarms/active").methods("GET"_method)([this] { try { auto json_string = m_alarm_service.getActiveAlarmsJson().dump(); auto res = crow::response(200, json_string); res.set_header("Content-Type", "application/json"); return res; } catch (const std::exception& e) { spdlog::error("Error processing /api/alarms/active: {}", e.what()); crow::json::wvalue error_resp; error_resp["error"] = "Failed to retrieve active alarms."; auto res = crow::response(500, error_resp.dump()); res.set_header("Content-Type", "application/json"); return res; } }); CROW_ROUTE((*this), "/api/alarms/history") .methods("GET"_method)([this](const crow::request& req) { int limit = 100; if (req.url_params.get("limit")) { try { limit = std::stoi(req.url_params.get("limit")); } catch (const std::exception&) { /* ignore invalid */ } } if (limit <= 0) limit = 100; try { auto json_string = m_alarm_service.getAlarmHistoryJson(limit).dump(); auto res = crow::response(200, json_string); res.set_header("Content-Type", "application/json"); return res; } catch (const std::exception& e) { spdlog::error("Error processing /api/alarms/history: {}", e.what()); crow::json::wvalue error_resp; error_resp["error"] = "Failed to retrieve alarm history."; auto res = crow::response(500, error_resp.dump()); res.set_header("Content-Type", "application/json"); return res; } }); CROW_ROUTE((*this), "/api/alarms/reload").methods("POST"_method)([this]() { spdlog::info("Web API: Received request to reload alarm rules..."); bool success = m_alarm_service.reload_rules(); if (success) { crow::json::wvalue response_json; response_json["status"] = "success"; response_json["message"] = "Alarm rules reloaded successfully."; auto res = crow::response(200, response_json.dump()); res.set_header("Content-Type", "application/json"); return res; } else { crow::json::wvalue error_json; error_json["status"] = "error"; error_json["message"] = "Failed to reload alarm rules. Check service logs for details."; auto res = crow::response(500, error_json.dump()); res.set_header("Content-Type", "application/json"); return res; } }); CROW_ROUTE((*this), "/api/alarms/clear") .methods("POST"_method)([this](const crow::request& req) { crow::json::rvalue j_body; try { j_body = crow::json::load(req.body); } catch (const std::exception& e) { spdlog::warn("Failed to parse request body for /api/alarms/clear: {}", e.what()); auto res = crow::response(400, "{\"error\":\"Invalid JSON body.\"}"); res.set_header("Content-Type", "application/json"); return res; } if (!j_body.has("rule_id") || !j_body.has("device_id")) { auto res = crow::response(400, "{\"error\":\"Missing 'rule_id' or 'device_id'.\"}"); res.set_header("Content-Type", "application/json"); return res; } std::string rule_id = j_body["rule_id"].s(); std::string device_id = j_body["device_id"].s(); bool success = m_alarm_service.manually_clear_alarm(rule_id, device_id); if (success) { auto res = crow::response(200, "{\"status\":\"success\", \"message\":\"Alarm cleared.\"}"); res.set_header("Content-Type", "application/json"); return res; } else { auto res = crow::response(404, "{\"status\":\"error\", \"message\":\"Alarm not " "found or not active.\"}"); res.set_header("Content-Type", "application/json"); return res; } }); CROW_ROUTE((*this), "/api/alarms/config").methods("GET"_method)([this]() { std::string rules_json_string = m_alarm_service.get_rules_as_json_string(); auto res = crow::response(200, rules_json_string); res.set_header("Content-Type", "application/json"); return res; }); CROW_ROUTE((*this), "/api/alarms/config") .methods("POST"_method)([this](const crow::request& req) { const std::string& new_rules_content = req.body; bool success = m_alarm_service.save_rules_from_json_string(new_rules_content); if (success) { crow::json::wvalue response_json; response_json["status"] = "success"; response_json["message"] = "Rules saved successfully. A reload is required to apply."; auto res = crow::response(200, response_json.dump()); res.set_header("Content-Type", "application/json"); return res; } else { crow::json::wvalue error_json; error_json["status"] = "error"; error_json["message"] = "Failed to save rules. Invalid JSON format or server error."; auto res = crow::response(400, error_json.dump()); res.set_header("Content-Type", "application/json"); return res; } }); /** * @brief GET /api/devices/config * 获取设备配置 (devices.json) 的原始 JSON 字符串 */ CROW_ROUTE((*this), "/api/devices/config").methods("GET"_method)([this]() { std::string rules_json_string = m_device_manager.get_config_as_json_string(); auto res = crow::response(200, rules_json_string); res.set_header("Content-Type", "application/json"); return res; }); /** * @brief POST /api/devices/config * 保存设备配置 (devices.json) 的原始 JSON 字符串 */ CROW_ROUTE((*this), "/api/devices/config") .methods("POST"_method)([this](const crow::request& req) { const std::string& new_rules_content = req.body; // save_config_from_json_string 内部包含 JSON 格式和 Schema 校验 bool success = m_device_manager.save_config_from_json_string(new_rules_content); if (success) { crow::json::wvalue response_json; response_json["status"] = "success"; response_json["message"] = "Device config saved successfully. A " "reload is required to apply."; auto res = crow::response(200, response_json.dump()); res.set_header("Content-Type", "application/json"); return res; } else { crow::json::wvalue error_json; error_json["status"] = "error"; error_json["message"] = "Failed to save rules. Invalid JSON format " "or schema. Check service logs."; auto res = crow::response(400, error_json.dump()); res.set_header("Content-Type", "application/json"); return res; } }); /** * @brief POST /api/devices/reload * 通知后端从磁盘重载 devices.json 并应用 */ CROW_ROUTE((*this), "/api/devices/reload").methods("POST"_method)([this]() { spdlog::info("Web API: Received request to reload device rules..."); bool success = m_device_manager.reload_config_from_file(); if (success) { crow::json::wvalue response_json; response_json["status"] = "success"; response_json["message"] = "Device rules reload posted successfully."; auto res = crow::response(200, response_json.dump()); res.set_header("Content-Type", "application/json"); return res; } else { crow::json::wvalue error_json; error_json["status"] = "error"; error_json["message"] = "Failed to post device rules reload. Check service logs."; auto res = crow::response(500, error_json.dump()); res.set_header("Content-Type", "application/json"); return res; } }); /** * @brief GET /api/video_config * 获取 video_config.json 的原始 JSON 字符串 */ CROW_ROUTE((*this), "/api/video_config").methods("GET"_method)([this]() { std::string config_path; try { config_path = ConfigManager::getInstance().getVideoConfigPath(); } catch (const std::exception& e) { spdlog::error("Failed to get video config path from ConfigManager: {}", e.what()); auto res = crow::response(500, "{\"error\":\"Server configuration error: " "cannot determine config path.\"}"); res.set_header("Content-Type", "application/json"); return res; } std::ifstream ifs(config_path); if (!ifs.is_open()) { spdlog::error("Failed to open video config file for reading: {}", config_path); auto res = crow::response(404, "{\"error\":\"Video config file not found.\"}"); res.set_header("Content-Type", "application/json"); return res; } std::string content((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())); auto res = crow::response(200, content); res.set_header("Content-Type", "application/json"); return res; }); /** * @brief POST /api/video_config * 验证并保存 video_config.json */ CROW_ROUTE((*this), "/api/video_config") .methods("POST"_method)([this](const crow::request& req) { const std::string& new_config_content = req.body; std::string error_msg; if (!validate_video_config(new_config_content, error_msg)) { spdlog::warn("Web API: Failed to save video_config: {}", error_msg); crow::json::wvalue error_json; error_json["status"] = "error"; error_json["message"] = "Invalid JSON format or schema: " + error_msg; auto res = crow::response(400, error_json.dump()); res.set_header("Content-Type", "application/json"); return res; } std::string config_path = ConfigManager::getInstance().getVideoConfigPath(); std::ofstream ofs(config_path); if (!ofs.is_open()) { spdlog::error("Failed to open video config file for writing: {}", config_path); auto res = crow::response(500, "{\"error\":\"Failed to write config file on server.\"}"); res.set_header("Content-Type", "application/json"); return res; } auto json_data = nlohmann::json::parse(new_config_content); ofs << json_data.dump(4); // 格式化写入 ofs.close(); // 4. 成功响应 spdlog::info("Video config successfully updated via API at {}", config_path); crow::json::wvalue response_json; response_json["status"] = "success"; response_json["message"] = "Video config saved. Send POST to /api/service/reload to apply."; auto res = crow::response(200, response_json.dump()); res.set_header("Content-Type", "application/json"); return res; }); CROW_ROUTE((*this), "/api/service/reload").methods("POST"_method)([this]() { if (m_shutdown_handler) { spdlog::info("Web API: Received request to reload service..."); m_shutdown_handler(); auto res = crow::response(202, "{\"status\":\"restarting\", " "\"message\":\"Service is restarting...\"}"); res.set_header("Content-Type", "application/json"); return res; } spdlog::error( "Web API: /api/service/reload called, but shutdown handler " "is not set!"); auto res = crow::response(500, "{\"error\":\"Shutdown handler not configured on server.\"}"); res.set_header("Content-Type", "application/json"); return res; }); /** * @brief GET /api/config * 获取主 config.json 的原始 JSON 字符串 */ CROW_ROUTE((*this), "/api/config").methods("GET"_method)([this]() { std::string config_path; try { // 1. 从 ConfigManager 获取路径 config_path = ConfigManager::getInstance().getConfigFilePath(); if (config_path.empty()) { throw std::runtime_error("ConfigManager returned an empty path."); } } catch (const std::exception& e) { spdlog::error("Failed to get main config path from ConfigManager: {}", e.what()); auto res = crow::response(500, "{\"error\":\"Server configuration error: " "cannot determine config path.\"}"); res.set_header("Content-Type", "application/json"); return res; } // 2. 读取文件 std::ifstream ifs(config_path); if (!ifs.is_open()) { spdlog::error("Failed to open main config file for reading: {}", config_path); auto res = crow::response(404, "{\"error\":\"Main config file not found.\"}"); res.set_header("Content-Type", "application/json"); return res; } // 3. 返回内容 std::string content((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())); auto res = crow::response(200, content); res.set_header("Content-Type", "application/json"); return res; }); /** * @brief POST /api/config * 验证并保存主 config.json */ CROW_ROUTE((*this), "/api/config").methods("POST"_method)([this](const crow::request& req) { const std::string& new_config_content = req.body; std::string error_msg; if (!validate_main_config(new_config_content, error_msg)) { spdlog::warn("Web API: Failed to save main config: {}", error_msg); crow::json::wvalue error_json; error_json["status"] = "error"; error_json["message"] = "Invalid JSON format or schema: " + error_msg; auto res = crow::response(400, error_json.dump()); res.set_header("Content-Type", "application/json"); return res; } std::string config_path = ConfigManager::getInstance().getConfigFilePath(); if (config_path.empty()) { auto res = crow::response(500, "{\"error\":\"Failed to get config file path on server.\"}"); res.set_header("Content-Type", "application/json"); return res; } std::ofstream ofs(config_path); if (!ofs.is_open()) { spdlog::error("Failed to open main config file for writing: {}", config_path); auto res = crow::response(500, "{\"error\":\"Failed to write config file on server.\"}"); res.set_header("Content-Type", "application/json"); return res; } try { auto json_data = nlohmann::json::parse(new_config_content); ofs << json_data.dump(4); ofs.close(); } catch (const std::exception& e) { spdlog::error("Failed to re-parse and dump main config: {}", e.what()); auto res = crow::response(500, "{\"error\":\"Failed to serialize config for writing.\"}"); res.set_header("Content-Type", "application/json"); return res; } spdlog::info("Main config successfully updated via API at {}", config_path); crow::json::wvalue response_json; response_json["status"] = "success"; response_json["message"] = "Main config saved. A full service restart (e.g., via " "/api/service/reload) is required to apply changes."; auto res = crow::response(200, response_json.dump()); res.set_header("Content-Type", "application/json"); return res; }); /** * @brief 灯光控制接口 * @method POST * @url /api/light/control * @body JSON { "device_id": "351", "action": "0" } * - device_id: 灯控设备的ID * - action: "0" (关) 或 "1" (开) */ CROW_ROUTE((*this), "/api/light/control") .methods("POST"_method)([this](const crow::request& req) { crow::json::rvalue j_body; try { j_body = crow::json::load(req.body); } catch (const std::exception& e) { return crow::response(400, "{\"error\":\"Invalid JSON format.\"}"); } if (!j_body.has("device_id") || !j_body.has("action")) { return crow::response(400, "{\"error\":\"Missing 'device_id' or 'action'.\"}"); } std::string req_deviceId = j_body["device_id"].s(); std::string action = j_body["action"].s(); spdlog::info("Web API: Light Control. Device: {}, Action: {}", req_deviceId, action); // 1. 定义要控制的设备列表 (将前端的联动逻辑移到这里) std::vector target_devices; // 如果是总控开关 351,则添加所有相关设备 if (req_deviceId == "351") { // 顺序很重要,建议把不太重要的放后面 target_devices = {"353", "350", "351", "352", "354", "349"}; } else { // 普通设备,只控制自己 target_devices.push_back(req_deviceId); } // 2. [关键优化] 启动后台线程执行物理控制 (Fire-and-Forget) // 这样前端会立刻收到 200 OK,切换页面也不会断连 // 复制必要的变量到 lambda LightController* ctrl = &m_light_controller; std::string act = action; std::vector thread_targets = target_devices; // 拷贝一份 std::thread([ctrl, thread_targets, act]() { for (const auto& id : thread_targets) { // 依次控制,防止网关拥堵 bool ok = ctrl->ControlLight(id, act); if (!ok) { // 失败稍微停顿一下 std::this_thread::sleep_for(std::chrono::milliseconds(50)); } // 成功也稍微停顿一下,让网关喘口气 (例如 20ms) std::this_thread::sleep_for(std::chrono::milliseconds(20)); } spdlog::info("Async Light Control Finished for {} devices.", thread_targets.size()); }).detach(); // 分离线程 // 3. 处理"智能模式"冲突 (保持之前的逻辑,但要针对列表里的所有设备) // 也就是:如果我手动关了灯,那么关联这个灯的摄像头必须退出智能模式 try { std::string config_path = ConfigManager::getInstance().getVideoConfigPath(); std::ifstream ifs(config_path); if (ifs.is_open()) { nlohmann::json json_config; ifs >> json_config; ifs.close(); bool config_changed = false; std::vector affected_streams; if (json_config.contains("video_streams") && json_config["video_streams"].is_array()) { for (auto& stream : json_config["video_streams"]) { if (!stream.contains("module_config")) continue; auto& mc = stream["module_config"]; bool is_associated = false; // 检查该摄像头的 light_device_ids 是否包含我们这次操作的 *任何一个* 灯 // 这样无论是单控还是群控,只要涉及到了,就关闭智能模式 std::vector cam_lights; if (mc.contains("light_device_ids") && mc["light_device_ids"].is_array()) { cam_lights = mc["light_device_ids"].get>(); } else if (mc.contains("light_device_id") && mc["light_device_id"].is_string()) { cam_lights.push_back(mc["light_device_id"].get()); } for (const auto& target_id : target_devices) { for (const auto& cam_light_id : cam_lights) { if (target_id == cam_light_id) { is_associated = true; break; } } if (is_associated) break; } // 如果关联且处于智能模式,强制关闭 if (is_associated) { int current_mode = mc.value("mode", 1); if (current_mode == 2) { mc["mode"] = 1; config_changed = true; std::string stream_id = stream.value("id", ""); if (!stream_id.empty()) { auto service = m_video_manager.getService(stream_id); if (service) { service->set_analysis_mode(1); affected_streams.push_back(stream_id); } } } } } } if (config_changed) { std::ofstream ofs(config_path); if (ofs.is_open()) { ofs << json_config.dump(4); ofs.close(); spdlog::info("Manual Override: Disabled Smart Mode for: {}", fmt::join(affected_streams, ", ")); } } } } catch (...) { // 忽略非关键错误 } crow::json::wvalue response_json; response_json["status"] = "success"; response_json["message"] = "Command received. Executing in background."; return crow::response(200, response_json); }); /** * @brief 门禁控制接口 * @method POST * @url /api/door/control * @body JSON { "action": "open" } * - action: "open" (开门), "close" (关门), "alwaysOpen" (常开), "alwaysClose" (常闭) */ CROW_ROUTE((*this), "/api/door/control") .methods("POST"_method)([this](const crow::request& req) { crow::json::rvalue j_body; try { j_body = crow::json::load(req.body); } catch (const std::exception& e) { spdlog::warn("Invalid JSON in door control request: {}", e.what()); return crow::response(400, "{\"error\":\"Invalid JSON format.\"}"); } // 参数校验 if (!j_body.has("action")) { return crow::response(400, "{\"error\":\"Missing 'action' field.\"}"); } std::string action = j_body["action"].s(); // 简单的安全检查,防止注入 if (action != "open" && action != "close" && action != "alwaysOpen" && action != "alwaysClose") { return crow::response(400, "{\"error\":\"Invalid action. Use 'open', 'close', etc.\"}"); } spdlog::info("Web API: Requesting door control. Action: {}", action); // 调用控制器 bool success = m_door_controller.ControlDoor(action); crow::json::wvalue response_json; if (success) { response_json["status"] = "success"; response_json["message"] = "Door control command sent."; return crow::response(200, response_json); } else { response_json["status"] = "error"; response_json["message"] = "Failed to send control command to Hikvision device."; return crow::response(502, response_json); } }); /** * @brief POST /api/video/zone * 单独修改指定视频流的 intrusion_zone (多边形区域) * @body JSON: * { * "stream_id": "cam_01_intrusion", * "intrusion_zone": [[100, 100], [500, 100], [500, 500], [100, 500]] * } */ CROW_ROUTE((*this), "/api/video/zone").methods("POST"_method)([this](const crow::request& req) { crow::json::rvalue j_body; try { j_body = crow::json::load(req.body); } catch (const std::exception& e) { spdlog::warn("Invalid JSON in update zone request: {}", e.what()); return crow::response(400, "{\"error\":\"Invalid JSON format.\"}"); } // 1. 参数校验 if (!j_body.has("stream_id") || !j_body.has("intrusion_zone")) { return crow::response(400, "{\"error\":\"Missing 'stream_id' or 'intrusion_zone'.\"}"); } std::string target_stream_id = j_body["stream_id"].s(); const auto& new_zone = j_body["intrusion_zone"]; // 验证 intrusion_zone 是否为数组,且至少有3个点(构成多边形) if (new_zone.t() != crow::json::type::List || new_zone.size() < 3) { return crow::response( 400, "{\"error\":\"'intrusion_zone' must be an array with at least 3 points.\"}"); } // 2. 读取当前配置文件 std::string config_path = ConfigManager::getInstance().getVideoConfigPath(); std::ifstream ifs(config_path); if (!ifs.is_open()) { spdlog::error("Failed to open video config file: {}", config_path); return crow::response(500, "{\"error\":\"Config file not found.\"}"); } nlohmann::json json_config; try { ifs >> json_config; } catch (const std::exception& e) { ifs.close(); spdlog::error("Failed to parse video config file: {}", e.what()); return crow::response(500, "{\"error\":\"Existing config file is corrupt.\"}"); } ifs.close(); // 3. 查找并更新目标流 bool found = false; if (json_config.contains("video_streams") && json_config["video_streams"].is_array()) { for (auto& stream : json_config["video_streams"]) { if (stream.contains("id") && stream["id"] == target_stream_id) { // 确保 module_config 存在 if (!stream.contains("module_config")) { stream["module_config"] = nlohmann::json::object(); } // 转换 crow::json 到 nlohmann::json // 注意:这里需要手动转换一下格式,或者直接解析 req.body 为 nlohmann::json try { auto body_nlohmann = nlohmann::json::parse(req.body); stream["module_config"]["intrusion_zone"] = body_nlohmann["intrusion_zone"]; found = true; } catch (const std::exception& e) { return crow::response( 400, "{\"error\":\"Failed to parse intrusion_zone data.\"}"); } break; } } } if (!found) { return crow::response(404, "{\"error\":\"Stream ID not found.\"}"); } // 4. 写回文件 std::ofstream ofs(config_path); if (!ofs.is_open()) { spdlog::error("Failed to open video config file for writing: {}", config_path); return crow::response(500, "{\"error\":\"Failed to save config file.\"}"); } ofs << json_config.dump(4); // 4空格缩进 ofs.close(); spdlog::info("Updated intrusion_zone for stream: {}", target_stream_id); crow::json::wvalue response_json; response_json["status"] = "success"; response_json["message"] = "Zone updated. Call /api/service/reload to apply changes."; return crow::response(200, response_json); }); /** * @brief 切换检测模式接口 * @method POST * @url /api/video/mode * @body JSON { "stream_id": "cam_01", "mode": 1 } * - mode: 1 (普通报警), 2 (快速响应) */ CROW_ROUTE((*this), "/api/video/mode").methods("POST"_method)([this](const crow::request& req) { crow::json::rvalue j_body; try { j_body = crow::json::load(req.body); } catch (const std::exception& e) { return crow::response(400, "{\"error\":\"Invalid JSON.\"}"); } if (!j_body.has("stream_id") || !j_body.has("mode")) { return crow::response(400, "{\"error\":\"Missing 'stream_id' or 'mode'.\"}"); } std::string stream_id = j_body["stream_id"].s(); int mode = j_body["mode"].i(); // 1. 【运行时更新】 立即生效 auto service = m_video_manager.getService(stream_id); if (service) { // 调用内存中的对象修改状态 bool result = service->set_analysis_mode(mode); if (!result) { spdlog::warn("Failed to set runtime mode for stream {}", stream_id); // 注意:这里即使运行时失败,只要配置能保存,也可以继续往下走,或者直接返回错误 // return crow::response(400, "{\"status\":\"error\", \"message\":\"Failed to switch // runtime mode.\"}"); } } else { // 如果服务没运行(比如禁用了),我们依然允许修改配置文件,这样下次启动就生效了 spdlog::warn("Stream {} not running, but config will be updated.", stream_id); } // 2. 【持久化更新】 保存到 JSON 文件 std::string config_path = ConfigManager::getInstance().getVideoConfigPath(); std::ifstream ifs(config_path); if (!ifs.is_open()) { return crow::response(500, "{\"error\":\"Config file not found.\"}"); } nlohmann::json json_config; try { ifs >> json_config; } catch (...) { return crow::response(500, "{\"error\":\"Config file corrupt.\"}"); } ifs.close(); bool found_config = false; if (json_config.contains("video_streams") && json_config["video_streams"].is_array()) { for (auto& stream : json_config["video_streams"]) { if (stream.contains("id") && stream["id"] == stream_id) { // 找到对应流,修改 module_config 中的 mode if (!stream.contains("module_config")) { stream["module_config"] = nlohmann::json::object(); } stream["module_config"]["mode"] = mode; // [关键] 写入 mode found_config = true; break; } } } if (!found_config) { return crow::response(404, "{\"error\":\"Stream ID not found in config.\"}"); } // 写回文件 std::ofstream ofs(config_path); if (!ofs.is_open()) { return crow::response(500, "{\"error\":\"Failed to save config.\"}"); } ofs << json_config.dump(4); ofs.close(); spdlog::info("Updated mode to {} for stream {} (Saved to file)", mode, stream_id); return crow::response(200, "{\"status\":\"success\", \"message\":\"Mode switched and saved.\"}"); }); }