From cfcdaac3e5153bfae68e85955a6e309801f91512 Mon Sep 17 00:00:00 2001 From: Tian jianyong <11429339@qq.com> Date: Tue, 24 Dec 2024 09:58:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 4 + config/system_config.json | 7 +- docs/{vehicle_api.md => official_api.md} | 23 +++- src/collector/DataSourceConfig.h | 3 + src/config/SystemConfig.h | 9 ++ src/core/System.cpp | 6 +- src/network/HTTPDataSource.cpp | 130 +++++++++++++++++++++-- src/network/HTTPDataSource.h | 14 ++- tests/BasicCollisionTest.cpp | 6 +- tools/mock_server.py | 77 +++++++++++++- 10 files changed, 257 insertions(+), 22 deletions(-) rename docs/{vehicle_api.md => official_api.md} (73%) diff --git a/CMakeLists.txt b/CMakeLists.txt index e9e02af..31cdbdf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,10 @@ if(NOT APPLE) # 在 CentOS 上使用 Boost 1.69 set(BOOST_ROOT "/usr/include/boost169") set(BOOST_LIBRARYDIR "/usr/lib64/boost169") +else() + # 在 macOS 上使用 Homebrew 安装的 Boost + set(BOOST_ROOT "/opt/homebrew/Cellar/boost/1.87.0") + set(BOOST_LIBRARYDIR "/opt/homebrew/Cellar/boost/1.87.0/lib") endif() find_package(Boost 1.69.0 REQUIRED COMPONENTS system filesystem thread) diff --git a/config/system_config.json b/config/system_config.json index 6733ca4..7e2e51d 100644 --- a/config/system_config.json +++ b/config/system_config.json @@ -18,12 +18,17 @@ {"point": "T8", "longitude": 120.08676664, "latitude": 36.35004529}, {"point": "T9", "longitude": 120.08520616, "latitude": 36.34964473}, {"point": "T10", "longitude": 120.08710569, "latitude": 36.34917893}, - {"point": "T11", "longitude": 120.0873865, "latitude": 36.3509885} + {"point": "T11", "longitude": 120.0873865, "latitude": 36.3509885}, + {"point": "T12", "longitude": 120.08603613, "latitude": 36.35190217}, + {"point": "T13", "longitude": 120.08509148, "latitude": 36.35041247} ] }, "data_source": { "host": "localhost", "port": 8081, + "username": "dianxin", + "password": "dianxin@123", + "auth_path": "/api/login", "aircraft_path": "/api/getCurrentFlightPositions", "vehicle_path": "/api/getCurrentVehiclePositions", "traffic_light_path": "/api/getTrafficLightSignals", diff --git a/docs/vehicle_api.md b/docs/official_api.md similarity index 73% rename from docs/vehicle_api.md rename to docs/official_api.md index 2961403..c7ec88b 100644 --- a/docs/vehicle_api.md +++ b/docs/official_api.md @@ -1,14 +1,27 @@ -# 位置数据接口对接方案 +# 数据接口对接方案 ## 第1章 位置数据接口对接方案 -### 1.1 航空器位置数据接入 +### 1.1 登录认证 + +1. 登录接口: +2. 请求方式:post +3. 参数:username、password +4. 示例: +5、返回值 data 为返回的鉴权token,后续接口需要再header中携带,data所有的数据是一个token,不要截断 +示例:{ +    "status": 200, +    "msg": "登入成功", +    "data": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY" +} + +### 1.2 航空器位置数据接入 数据来源:接入并转发从空管接收到的融合数据 1. 接口地址: -2. 请求方式:get +2. 请求方式:get,需要在 Header 中携带认证信息,字段名为 Authorization,值为认证接口返回的token 3. 返回格式:以 JSON 格式返回数据,一次请求返回List集合对象 @@ -23,13 +36,13 @@ | 5 | altitude | 海拔高度 | double | 否 | | 6 | trackNumber | 航迹号 | String | 否 | -### 1.2 车辆位置数据接入 +### 1.3 车辆位置数据接入 数据来源:仅传递目前机场已接入的车辆位置数据 1. 接口地址: -2. 请求方式:get +2. 请求方式:get,需要在 Header 中携带认证信息,字段名为 Authorization,值为认证接口返回的token 3. 返回格式:以 JSON 格式返回数据,一次请求返回List集合对象 diff --git a/src/collector/DataSourceConfig.h b/src/collector/DataSourceConfig.h index 38c5ce6..e4f44a4 100644 --- a/src/collector/DataSourceConfig.h +++ b/src/collector/DataSourceConfig.h @@ -9,6 +9,9 @@ struct DataSourceConfig { std::string aircraft_path; std::string vehicle_path; std::string traffic_light_path; + std::string auth_path; // 认证接口路径 + std::string username; // 用户名 + std::string password; // 密码 int refresh_interval_ms; int timeout_ms; // 连接超时时间, 单位: 毫秒 int read_timeout_ms; // 读取超时时间, 单位: 毫秒, 小于连接超时时间 diff --git a/src/config/SystemConfig.h b/src/config/SystemConfig.h index 6068be6..2daa5a6 100644 --- a/src/config/SystemConfig.h +++ b/src/config/SystemConfig.h @@ -51,6 +51,9 @@ public: std::string aircraft_path; std::string vehicle_path; std::string traffic_light_path; + std::string auth_path; // 认证接口路径 + std::string username; // 用户名 + std::string password; // 密码 int refresh_interval_ms; int timeout_ms; int read_timeout_ms; @@ -210,6 +213,9 @@ inline void to_json(json& j, const SystemConfig::DataSource& p) { {"aircraft_path", p.aircraft_path}, {"vehicle_path", p.vehicle_path}, {"traffic_light_path", p.traffic_light_path}, + {"auth_path", p.auth_path}, + {"username", p.username}, + {"password", p.password}, {"refresh_interval_ms", p.refresh_interval_ms}, {"timeout_ms", p.timeout_ms}, {"read_timeout_ms", p.read_timeout_ms} @@ -222,6 +228,9 @@ inline void from_json(const json& j, SystemConfig::DataSource& p) { j.at("aircraft_path").get_to(p.aircraft_path); j.at("vehicle_path").get_to(p.vehicle_path); j.at("traffic_light_path").get_to(p.traffic_light_path); + j.at("auth_path").get_to(p.auth_path); + j.at("username").get_to(p.username); + j.at("password").get_to(p.password); j.at("refresh_interval_ms").get_to(p.refresh_interval_ms); j.at("timeout_ms").get_to(p.timeout_ms); j.at("read_timeout_ms").get_to(p.read_timeout_ms); diff --git a/src/core/System.cpp b/src/core/System.cpp index a56d771..cdad92f 100644 --- a/src/core/System.cpp +++ b/src/core/System.cpp @@ -69,8 +69,12 @@ bool System::initialize() { system_config.data_source.aircraft_path, system_config.data_source.vehicle_path, system_config.data_source.traffic_light_path, + system_config.data_source.auth_path, + system_config.data_source.username, + system_config.data_source.password, system_config.data_source.refresh_interval_ms, - system_config.data_source.timeout_ms + system_config.data_source.timeout_ms, + system_config.data_source.read_timeout_ms }; WarnConfig warnConfig{ diff --git a/src/network/HTTPDataSource.cpp b/src/network/HTTPDataSource.cpp index 78e3147..2fa4089 100644 --- a/src/network/HTTPDataSource.cpp +++ b/src/network/HTTPDataSource.cpp @@ -14,7 +14,8 @@ HTTPDataSource::HTTPDataSource(const DataSourceConfig& config) : config_(config) , host_(config.host) , port_(std::to_string(config.port)) - , last_health_check_(std::chrono::steady_clock::now()) { + , last_health_check_(std::chrono::steady_clock::now()) + , is_authenticated_(false) { curl_ = curl_easy_init(); if (!curl_) { Logger::error("Failed to initialize CURL"); @@ -36,7 +37,9 @@ bool HTTPDataSource::connect() { return false; } } - return true; + + // 进行认证 + return authenticate(); } void HTTPDataSource::disconnect() { @@ -115,12 +118,20 @@ bool HTTPDataSource::ensureConnected() { return tryReconnect(); } -bool HTTPDataSource::sendRequest(const std::string& path, std::string& response) { +bool HTTPDataSource::sendRequest(const std::string& path, std::string& response, HttpMethod method) { if (!curl_) { Logger::error("CURL not initialized"); return false; } + // 检查认证状态 + if (!is_authenticated_) { + Logger::error("Not authenticated"); + if (!authenticate()) { + return false; + } + } + std::string url = "http://" + host_ + ":" + port_ + path; Logger::debug("Sending request to: ", url); @@ -132,10 +143,24 @@ bool HTTPDataSource::sendRequest(const std::string& path, std::string& response) curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT, config_.timeout_ms / 1000); curl_easy_setopt(curl_, CURLOPT_NOSIGNAL, 1L); + // 根据方法设置不同的选项 + if (method == HttpMethod::GET) { + curl_easy_setopt(curl_, CURLOPT_HTTPGET, 1L); + } else if (method == HttpMethod::POST) { + curl_easy_setopt(curl_, CURLOPT_POST, 1L); + } + // 添加 HTTP 头 struct curl_slist* headers = nullptr; headers = curl_slist_append(headers, "Accept: application/json"); headers = curl_slist_append(headers, "Cache-Control: no-cache"); + + // 添加认证 token + if (!auth_token_.empty()) { + std::string auth_header = "Authorization: " + auth_token_; + headers = curl_slist_append(headers, auth_header.c_str()); + } + curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers); CURLcode res = curl_easy_perform(curl_); @@ -148,6 +173,17 @@ bool HTTPDataSource::sendRequest(const std::string& path, std::string& response) long http_code = 0; curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &http_code); + + // 如果返回 401,尝试重新认证 + if (http_code == 401) { + Logger::warning("Token expired, trying to re-authenticate"); + if (authenticate()) { + // 重新发送请求 + return sendRequest(path, response, method); + } + return false; + } + if (http_code != 200) { Logger::error("HTTP request failed with code: ", http_code); return false; @@ -164,7 +200,7 @@ bool HTTPDataSource::fetchAircraftData(std::vector& aircraft) { } std::string response; - if (!sendRequest(config_.aircraft_path, response)) { + if (!sendRequest(config_.aircraft_path, response, HttpMethod::GET)) { return false; } @@ -179,7 +215,7 @@ bool HTTPDataSource::fetchVehicleData(std::vector& vehicles) { } std::string response; - if (!sendRequest(config_.vehicle_path, response)) { + if (!sendRequest(config_.vehicle_path, response, HttpMethod::GET)) { return false; } @@ -194,7 +230,7 @@ bool HTTPDataSource::fetchTrafficLightSignals(std::vector& s } std::string response; - if (!sendRequest(config_.traffic_light_path, response)) { + if (!sendRequest(config_.traffic_light_path, response, HttpMethod::GET)) { return false; } @@ -363,4 +399,86 @@ bool HTTPDataSource::parseAircraftResponse(const std::string& response, std::vec Logger::error("Error processing aircraft response: ", e.what()); return false; } +} + +bool HTTPDataSource::authenticate() { + std::lock_guard lock(auth_mutex_); + + if (!curl_) { + Logger::error("CURL not initialized"); + return false; + } + + // 构造认证请求URL + std::string url = "http://" + host_ + ":" + port_ + config_.auth_path; + + // 构造认证请求体 + nlohmann::json request = { + {"username", config_.username}, + {"password", config_.password} + }; + + std::string request_body = request.dump(); + std::string response; + + Logger::debug("Sending authentication request with username: ", config_.username); + Logger::debug("Request body: ", request_body); + + // 设置CURL选项 + curl_easy_reset(curl_); + curl_easy_setopt(curl_, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl_, CURLOPT_POST, 1L); + curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, request_body.c_str()); + curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, static_cast(request_body.length())); + curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response); + + // 设置请求头 + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers); + + // 发送请求 + CURLcode res = curl_easy_perform(curl_); + curl_slist_free_all(headers); + + if (res != CURLE_OK) { + Logger::error("Failed to authenticate: ", curl_easy_strerror(res)); + return false; + } + + // 检查响应状态码 + long http_code = 0; + curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code != 200) { + Logger::error("Authentication failed with HTTP code: ", http_code); + return false; + } + + try { + Logger::debug("Authentication response: ", response); + + // 解析响应获取 token + nlohmann::json j = nlohmann::json::parse(response); + if (!j.contains("status") || !j.contains("msg") || !j.contains("data")) { + Logger::error("Authentication response missing required fields"); + return false; + } + + if (j["status"].get() != 200) { + Logger::error("Authentication failed: ", j["msg"].get()); + return false; + } + + auth_token_ = j["data"].get(); + is_authenticated_ = true; + Logger::debug("Stored token: ", auth_token_); + Logger::info("Successfully authenticated: ", j["msg"].get()); + return true; + } + catch (const std::exception& e) { + Logger::error("Failed to parse authentication response: ", e.what()); + return false; + } } \ No newline at end of file diff --git a/src/network/HTTPDataSource.h b/src/network/HTTPDataSource.h index db4f2a2..7591e44 100644 --- a/src/network/HTTPDataSource.h +++ b/src/network/HTTPDataSource.h @@ -13,6 +13,11 @@ class HTTPDataSource : public DataSource { public: + enum class HttpMethod { + GET, + POST + }; + explicit HTTPDataSource(const DataSourceConfig& config); ~HTTPDataSource() override; @@ -40,11 +45,18 @@ private: static constexpr int HEALTH_CHECK_INTERVAL = 60; // 健康检查间隔(秒) std::atomic is_reconnecting_{false}; // 重连状态标志 + std::string auth_token_; // 认证 token + bool is_authenticated_; // 认证状态 + std::mutex auth_mutex_; // 认证相关的互斥锁 + + bool authenticate(); // 认证方法 + bool refreshToken(); // 刷新 token + bool tryReconnect(); bool ensureConnected(); bool checkConnectionHealth(); // 检查连接健康状况 - bool sendRequest(const std::string& path, std::string& response); + bool sendRequest(const std::string& path, std::string& response, HttpMethod method = HttpMethod::GET); bool parseAircraftResponse(const std::string& response, std::vector& aircraft); bool parseVehicleResponse(const std::string& response, std::vector& vehicles); bool parseTrafficLightResponse(const std::string& response, std::vector& signals); diff --git a/tests/BasicCollisionTest.cpp b/tests/BasicCollisionTest.cpp index b23052a..1b4500b 100644 --- a/tests/BasicCollisionTest.cpp +++ b/tests/BasicCollisionTest.cpp @@ -401,7 +401,7 @@ TEST_F(BasicCollisionTest, QN002AircraftCrossing) { // 记录详细的测试信息 Logger::debug("QN002-AC001 碰撞测试结果:"); - Logger::debug("碰��类型: ", static_cast(result.type)); + Logger::debug("碰撞类型: ", static_cast(result.type)); Logger::debug("最小距离: ", result.minDistance, "m"); Logger::debug("碰撞时间: ", result.timeToCollision, "s"); Logger::debug("碰撞点: (", result.collisionPoint.x, ",", result.collisionPoint.y, ")"); @@ -460,7 +460,7 @@ TEST_F(BasicCollisionTest, CrossingBeforeAndAfter) { Logger::debug("无人车位置: (", vehicle.position.x, ",", vehicle.position.y, ")"); Logger::debug("交叉点位置: (", crossPoint.x, ",", crossPoint.y, ")"); - ASSERT_FALSE(risks.empty()) << "交叉前应该检测到��撞风险"; + ASSERT_FALSE(risks.empty()) << "交叉前应该检测到碰撞风险"; EXPECT_EQ(risks[0].level, RiskLevel::WARNING) << "交叉前应该是预警级别"; // 2. 测试交叉过程中 @@ -477,7 +477,7 @@ TEST_F(BasicCollisionTest, CrossingBeforeAndAfter) { risks = detector_->detectCollisions(); // 记录详细的测试信息 - Logger::debug("交叉过程��碰撞检测:"); + Logger::debug("交叉过程碰撞检测:"); Logger::debug("航空器位置: (", aircraft.position.x, ",", aircraft.position.y, ")"); Logger::debug("无人车位置: (", vehicle.position.x, ",", vehicle.position.y, ")"); diff --git a/tools/mock_server.py b/tools/mock_server.py index 97b6a9a..10ce7e9 100644 --- a/tools/mock_server.py +++ b/tools/mock_server.py @@ -67,6 +67,9 @@ POINT_T11 = {"longitude": 120.0873865, "latitude": 36.3509885} # QN002 新起点(距离 T6 路口 150 米) POINT_T12 = {"longitude": 120.08603613, "latitude": 36.35190217} # T4 和 T6 之间,距 T6 150米 +# AC001 新起点(距离 T6 路口 200 米) +POINT_T13 = {"longitude": 120.08509148, "latitude": 36.35041247} # T6向南 200 米 + # 飞机和车辆尺寸(半径 米) AIRCRAFT_SIZE_M = 30.0 VEHICLE_SIZE_M = 10.0 @@ -551,7 +554,7 @@ def update_aircraft_position(aircraft, elapsed_time): reached_target = update_position_with_vector( aircraft, POINT_T11, # 目标点 - POINT_T7, # 起点 + POINT_T13, # 起点 aircraft["speed"], elapsed_time, return_to_start=True # 航空器需要返回起点 @@ -735,8 +738,22 @@ last_aircraft_update_time = time.time() last_vehicle_update_time = time.time() last_light_switch_time = time.time() -@app.route('/api/getCurrentFlightPositions') +def check_auth(): + auth_header = request.headers.get('Authorization') + if not auth_header or auth_header != AUTH_TOKEN: + return False + return True + +@app.route('/api/getCurrentFlightPositions', methods=['GET', 'OPTIONS']) def get_flight_positions(): + if request.method == 'OPTIONS': + return '', 204 + if not check_auth(): + return jsonify({ + "status": 401, + "msg": "认证失败", + "data": None + }), 401 global last_aircraft_update_time current_time = time.time() elapsed_time = current_time - last_aircraft_update_time @@ -773,8 +790,16 @@ def switch_traffic_light_state(): if old_state != traffic_light_data[1]["state"]: print(f"东路口红绿灯状态切换为: {'绿灯' if traffic_light_data[1]['state'] == 1 else '红灯'}") -@app.route('/api/getCurrentVehiclePositions') +@app.route('/api/getCurrentVehiclePositions', methods=['GET', 'OPTIONS']) def get_vehicle_positions(): + if request.method == 'OPTIONS': + return '', 204 + if not check_auth(): + return jsonify({ + "status": 401, + "msg": "认证失败", + "data": None + }), 401 global last_vehicle_update_time current_time = time.time() elapsed_time = current_time - last_vehicle_update_time @@ -796,8 +821,16 @@ def get_vehicle_positions(): print(f"Error in get_vehicle_positions: {str(e)}") return jsonify({"error": str(e)}), 500 -@app.route('/api/getTrafficLightSignals') +@app.route('/api/getTrafficLightSignals', methods=['GET', 'OPTIONS']) def get_traffic_light_signals(): + if request.method == 'OPTIONS': + return '', 204 + if not check_auth(): + return jsonify({ + "status": 401, + "msg": "认证失败", + "data": None + }), 401 current_time = time.time() # 更新时间戳 @@ -812,8 +845,9 @@ def get_traffic_light_signals(): @app.after_request def add_cors_headers(response): response.headers['Access-Control-Allow-Origin'] = '*' - response.headers['Access-Control-Allow-Headers'] = 'Content-Type' + response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS' + response.headers['Access-Control-Max-Age'] = '3600' return response def check_vehicle_states(): @@ -821,5 +855,38 @@ def check_vehicle_states(): for vehicle_no, state in vehicle_states.items(): state.log_state() +# 认证相关配置 +AUTH_USERNAME = "dianxin" +AUTH_PASSWORD = "dianxin@123" +AUTH_TOKEN = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY" + +@app.route('/api/login', methods=['POST', 'OPTIONS']) +def login(): + if request.method == 'OPTIONS': + return '', 204 + data = request.get_json() + if not data: + return jsonify({ + "status": 400, + "msg": "请求格式错误", + "data": None + }), 400 + + username = data.get('username') + password = data.get('password') + + if username == AUTH_USERNAME and password == AUTH_PASSWORD: + return jsonify({ + "status": 200, + "msg": "登入成功", + "data": AUTH_TOKEN + }) + else: + return jsonify({ + "status": 401, + "msg": "用户名或密码错误", + "data": None + }), 401 + if __name__ == '__main__': app.run(host='localhost', port=8081, debug=True) \ No newline at end of file