feat/dataset-detail-chart #5

Merged
gaozheng merged 74 commits from feat/dataset-detail-chart into main 2026-06-13 17:30:37 +08:00
6 changed files with 149 additions and 1 deletions
Showing only changes of commit 4beb7a9523 - Show all commits

View File

@ -7,7 +7,8 @@ add_library(geopro_data STATIC
dto/DatasetChartDto.cpp dto/DatasetChartDto.cpp
api/ApiProjectRepository.cpp api/ApiProjectRepository.cpp
api/ApiDatasetRepository.cpp api/ApiDatasetRepository.cpp
api/DatasetLoadHandles.cpp) api/DatasetLoadHandles.cpp
api/NavRequest.cpp)
target_include_directories(geopro_data PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 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_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) target_compile_features(geopro_data PUBLIC cxx_std_17)

14
src/data/api/NavLoads.hpp Normal file
View File

@ -0,0 +1,14 @@
#pragma once
#include <vector>
#include <QMetaType>
#include "repo/RepoTypes.hpp"
// 导航异步返回类型经 QVariant 承载:同线程直连仅需 Q_DECLARE_METATYPE无需 qRegisterMetaType
Q_DECLARE_METATYPE(std::vector<geopro::data::Workspace>)
Q_DECLARE_METATYPE(geopro::data::ProjectListPage)
Q_DECLARE_METATYPE(std::vector<geopro::data::ProjectType>)
Q_DECLARE_METATYPE(std::vector<geopro::data::StructNode>)
Q_DECLARE_METATYPE(geopro::data::DsPage)
Q_DECLARE_METATYPE(geopro::data::DynamicForm)
Q_DECLARE_METATYPE(std::vector<geopro::data::ExceptionRow>)
// bool 已内置 QMetaType。

View File

@ -0,0 +1,47 @@
#include "api/NavRequest.hpp"
#include <stdexcept>
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

View File

@ -0,0 +1,39 @@
#pragma once
#include <functional>
#include <QObject>
#include <QPointer>
#include <QString>
#include <QVariant>
#include "IApiCall.hpp"
namespace geopro::data {
// 单请求异步句柄抽象基可测试缝payload 经 QVariant 承载,控制器侧 qvariant_cast<T> 取出。
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<QVariant(const geopro::net::ApiResponse&)>;
using Predicate = std::function<bool(const geopro::net::ApiResponse&)>;
ApiNavRequest(geopro::net::IApiCall* call, Parser parse, Predicate isFailure,
QObject* parent = nullptr); // 接管 call
void abort() override;
private:
QPointer<geopro::net::IApiCall> call_;
Parser parse_;
Predicate isFailure_;
bool aborted_ = false;
};
} // namespace geopro::data

View File

@ -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_nav_dto.cpp)
target_sources(geopro_tests PRIVATE data/test_dataset_chart_dto.cpp) target_sources(geopro_tests PRIVATE data/test_dataset_chart_dto.cpp)
target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.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) target_link_libraries(geopro_tests PRIVATE geopro_data)
# net RSA OpenSSL / find_package # net RSA OpenSSL / find_package

View File

@ -0,0 +1,45 @@
#include <gtest/gtest.h>
#include <QSignalSpy>
#include <QVariant>
#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<DsPage>(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);
}