From 4beb7a9523bd7f23e710ef8abadd906a14954fae Mon Sep 17 00:00:00 2001 From: gaozheng Date: Fri, 12 Jun 2026 07:38:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(data):=20NavRequest=20=E5=8D=95=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E5=BC=82=E6=AD=A5=E5=8F=A5=E6=9F=84(QVariant=20payloa?= =?UTF-8?q?d,=20abort=E9=97=B8=E9=97=A8)=20+=20=E5=85=83=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=20+=20=E7=A6=BB=E7=BA=BF=E5=8D=95=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/CMakeLists.txt | 3 ++- src/data/api/NavLoads.hpp | 14 ++++++++++ src/data/api/NavRequest.cpp | 47 +++++++++++++++++++++++++++++++++ src/data/api/NavRequest.hpp | 39 +++++++++++++++++++++++++++ tests/CMakeLists.txt | 2 ++ tests/data/test_nav_request.cpp | 45 +++++++++++++++++++++++++++++++ 6 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/data/api/NavLoads.hpp create mode 100644 src/data/api/NavRequest.cpp create mode 100644 src/data/api/NavRequest.hpp create mode 100644 tests/data/test_nav_request.cpp diff --git a/src/data/CMakeLists.txt b/src/data/CMakeLists.txt index 9319112..86f09c5 100644 --- a/src/data/CMakeLists.txt +++ b/src/data/CMakeLists.txt @@ -7,7 +7,8 @@ add_library(geopro_data STATIC dto/DatasetChartDto.cpp api/ApiProjectRepository.cpp api/ApiDatasetRepository.cpp - api/DatasetLoadHandles.cpp) + api/DatasetLoadHandles.cpp + api/NavRequest.cpp) target_include_directories(geopro_data PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(geopro_data PUBLIC geopro_core geopro_net Qt6::Core PRIVATE nlohmann_json::nlohmann_json) target_compile_features(geopro_data PUBLIC cxx_std_17) diff --git a/src/data/api/NavLoads.hpp b/src/data/api/NavLoads.hpp new file mode 100644 index 0000000..e4238d6 --- /dev/null +++ b/src/data/api/NavLoads.hpp @@ -0,0 +1,14 @@ +#pragma once +#include +#include +#include "repo/RepoTypes.hpp" + +// 导航异步返回类型经 QVariant 承载:同线程直连仅需 Q_DECLARE_METATYPE(无需 qRegisterMetaType)。 +Q_DECLARE_METATYPE(std::vector) +Q_DECLARE_METATYPE(geopro::data::ProjectListPage) +Q_DECLARE_METATYPE(std::vector) +Q_DECLARE_METATYPE(std::vector) +Q_DECLARE_METATYPE(geopro::data::DsPage) +Q_DECLARE_METATYPE(geopro::data::DynamicForm) +Q_DECLARE_METATYPE(std::vector) +// bool 已内置 QMetaType。 diff --git a/src/data/api/NavRequest.cpp b/src/data/api/NavRequest.cpp new file mode 100644 index 0000000..5b3eb57 --- /dev/null +++ b/src/data/api/NavRequest.cpp @@ -0,0 +1,47 @@ +#include "api/NavRequest.hpp" +#include + +namespace geopro::data { + +namespace { +QString reasonOf(const geopro::net::ApiResponse& r) { + return r.msg.isEmpty() ? r.rawError : r.msg; +} +} // namespace + +ApiNavRequest::ApiNavRequest(geopro::net::IApiCall* call, Parser parse, Predicate isFailure, + QObject* parent) + : NavRequest(parent), call_(call), parse_(std::move(parse)), isFailure_(std::move(isFailure)) { + QObject::connect(call, &geopro::net::IApiCall::finished, this, + [this](const geopro::net::ApiResponse& resp) { + if (aborted_) return; // §5.0 入口守卫 + if (isFailure_(resp)) { + emit failed(reasonOf(resp)); + deleteLater(); + return; + } + QVariant out; + try { + out = parse_(resp); // 仅解析在 try 内(下游 done 处理器抛出不误报) + } catch (const std::exception& e) { + emit failed(QString::fromUtf8(e.what())); + deleteLater(); + return; + } catch (...) { + emit failed(QStringLiteral("解析失败:未知异常")); + deleteLater(); + return; + } + emit done(out); + deleteLater(); + }); +} + +void ApiNavRequest::abort() { + if (aborted_) return; + aborted_ = true; + if (call_) call_->abort(); + deleteLater(); +} + +} // namespace geopro::data diff --git a/src/data/api/NavRequest.hpp b/src/data/api/NavRequest.hpp new file mode 100644 index 0000000..9859275 --- /dev/null +++ b/src/data/api/NavRequest.hpp @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include +#include +#include +#include "IApiCall.hpp" + +namespace geopro::data { + +// 单请求异步句柄(抽象基,可测试缝):payload 经 QVariant 承载,控制器侧 qvariant_cast 取出。 +class NavRequest : public QObject { + Q_OBJECT +public: + using QObject::QObject; + ~NavRequest() override = default; + virtual void abort() = 0; +signals: + void done(const QVariant& value); + void failed(const QString& message); +}; + +// Api 实现:包一个 IApiCall + 注入的解析器(ApiResponse → QVariant)+ 失败谓词。 +class ApiNavRequest : public NavRequest { + Q_OBJECT +public: + using Parser = std::function; + using Predicate = std::function; + ApiNavRequest(geopro::net::IApiCall* call, Parser parse, Predicate isFailure, + QObject* parent = nullptr); // 接管 call + void abort() override; +private: + QPointer call_; + Parser parse_; + Predicate isFailure_; + bool aborted_ = false; +}; + +} // namespace geopro::data diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 36c31c5..dd4d1e9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,6 +39,8 @@ target_sources(geopro_tests PRIVATE data/test_local_repo.cpp) target_sources(geopro_tests PRIVATE data/test_nav_dto.cpp) target_sources(geopro_tests PRIVATE data/test_dataset_chart_dto.cpp) target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.cpp) +# NavRequest 离线单测(QVariant payload: done/failed/abort 闸门)。 +target_sources(geopro_tests PRIVATE data/test_nav_request.cpp) target_link_libraries(geopro_tests PRIVATE geopro_data) # net 层:RSA 加密器。测试需直接用 OpenSSL 生成/解密密钥,故显式 find_package diff --git a/tests/data/test_nav_request.cpp b/tests/data/test_nav_request.cpp new file mode 100644 index 0000000..1beb902 --- /dev/null +++ b/tests/data/test_nav_request.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include "api/NavRequest.hpp" +#include "api/NavLoads.hpp" +#include "net/FakeApiCall.hpp" + +using namespace geopro::data; +using geopro::net::ApiResponse; +using geopro::net::test::FakeApiCall; + +namespace { +ApiResponse ok() { ApiResponse r; r.code = 200; r.httpStatus = 200; return r; } +ApiResponse bad() { ApiResponse r; r.code = 500; r.httpStatus = 200; r.msg = QStringLiteral("boom"); return r; } +auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); }; +} // namespace + +TEST(NavRequest, EmitsDoneWithParsedPayload) { + auto* call = new FakeApiCall; + auto* req = new ApiNavRequest(call, + [](const ApiResponse&) { return QVariant::fromValue(DsPage{{}, 42}); }, isFailure); + QSignalSpy doneSpy(req, &NavRequest::done); + call->fire(ok()); + ASSERT_EQ(doneSpy.count(), 1); + const auto page = qvariant_cast(doneSpy.takeFirst().at(0)); + EXPECT_EQ(page.total, 42); +} + +TEST(NavRequest, EmitsFailedOnBusinessError) { + auto* call = new FakeApiCall; + auto* req = new ApiNavRequest(call, [](const ApiResponse&) { return QVariant(); }, isFailure); + QSignalSpy failSpy(req, &NavRequest::failed); + call->fire(bad()); + EXPECT_EQ(failSpy.count(), 1); +} + +TEST(NavRequest, AbortSuppressesLateDone) { + auto* call = new FakeApiCall; + auto* req = new ApiNavRequest(call, [](const ApiResponse&) { return QVariant(); }, isFailure); + QSignalSpy doneSpy(req, &NavRequest::done); + req->abort(); + EXPECT_TRUE(call->aborted); + call->fire(ok()); + EXPECT_EQ(doneSpy.count(), 0); +}