feat(data): 异步仓储接口 + ChartLoad/GridLoad 句柄(抽象基+Api实现) + 离线单测

This commit is contained in:
gaozheng 2026-06-11 20:19:32 +08:00
parent e980ddd346
commit bb602e2011
7 changed files with 219 additions and 2 deletions

View File

@ -6,8 +6,9 @@ add_library(geopro_data STATIC
dto/NavDto.cpp dto/NavDto.cpp
dto/DatasetChartDto.cpp dto/DatasetChartDto.cpp
api/ApiProjectRepository.cpp api/ApiProjectRepository.cpp
api/ApiDatasetRepository.cpp) api/ApiDatasetRepository.cpp
api/DatasetLoadHandles.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)
set_target_properties(geopro_data PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF) set_target_properties(geopro_data PROPERTIES AUTOMOC ON AUTOUIC OFF AUTORCC OFF)

View File

@ -0,0 +1,66 @@
#include "api/DatasetLoadHandles.hpp"
#include <stdexcept>
namespace geopro::data {
namespace {
QString reasonOf(const geopro::net::ApiResponse& r) {
return r.msg.isEmpty() ? r.rawError : r.msg;
}
} // namespace
ApiChartLoad::ApiChartLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent)
: ChartLoad(parent), batch_(batch), parse_(std::move(parse)) {
QObject::connect(batch, &geopro::net::ApiBatch::succeeded, this,
[this](const QList<geopro::net::ApiResponse>& resps) {
if (aborted_) return; // §5.0
try {
emit done(parse_(resps));
} catch (const std::exception& e) {
emit failed(QString::fromUtf8(e.what()));
}
deleteLater();
});
QObject::connect(batch, &geopro::net::ApiBatch::failed, this,
[this](int, const geopro::net::ApiResponse& r) {
if (aborted_) return;
emit failed(reasonOf(r));
deleteLater();
});
}
void ApiChartLoad::abort() {
if (aborted_) return;
aborted_ = true;
if (batch_) batch_->abort();
deleteLater();
}
ApiGridLoad::ApiGridLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent)
: GridLoad(parent), batch_(batch), parse_(std::move(parse)) {
QObject::connect(batch, &geopro::net::ApiBatch::succeeded, this,
[this](const QList<geopro::net::ApiResponse>& resps) {
if (aborted_) return;
try {
emit done(parse_(resps));
} catch (const std::exception& e) {
emit failed(QString::fromUtf8(e.what()));
}
deleteLater();
});
QObject::connect(batch, &geopro::net::ApiBatch::failed, this,
[this](int, const geopro::net::ApiResponse& r) {
if (aborted_) return;
emit failed(reasonOf(r));
deleteLater();
});
}
void ApiGridLoad::abort() {
if (aborted_) return;
aborted_ = true;
if (batch_) batch_->abort();
deleteLater();
}
} // namespace geopro::data

View File

@ -0,0 +1,60 @@
#pragma once
#include <functional>
#include <QList>
#include <QObject>
#include <QPointer>
#include <QString>
#include "ApiBatch.hpp"
#include "DatasetLoads.hpp"
namespace geopro::data {
// ── 抽象句柄(可测试缝,类比 IApiCall仓储返回基类指针控制器/测试只依赖它 ──
class ChartLoad : public QObject {
Q_OBJECT
public:
using QObject::QObject;
~ChartLoad() override = default;
virtual void abort() = 0;
signals:
void done(const geopro::data::ChartParts& parts);
void failed(const QString& message);
};
class GridLoad : public QObject {
Q_OBJECT
public:
using QObject::QObject;
~GridLoad() override = default;
virtual void abort() = 0;
signals:
void done(const geopro::data::GridParts& parts);
void failed(const QString& message);
};
// ── Api 实现:包一个 ApiBatch + 注入的解析函数。batch.succeeded→解析→donefailed→failed ──
class ApiChartLoad : public ChartLoad {
Q_OBJECT
public:
using Parser = std::function<ChartParts(const QList<geopro::net::ApiResponse>&)>;
ApiChartLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent = nullptr);
void abort() override;
private:
QPointer<geopro::net::ApiBatch> batch_;
Parser parse_;
bool aborted_ = false;
};
class ApiGridLoad : public GridLoad {
Q_OBJECT
public:
using Parser = std::function<GridParts(const QList<geopro::net::ApiResponse>&)>;
ApiGridLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent = nullptr);
void abort() override;
private:
QPointer<geopro::net::ApiBatch> batch_;
Parser parse_;
bool aborted_ = false;
};
} // namespace geopro::data

View File

@ -0,0 +1,21 @@
#pragma once
#include <vector>
#include "model/Field.hpp"
#include "model/ColorScale.hpp"
#include "model/Anomaly.hpp"
namespace geopro::data {
// 原数据加载结果scatter + 散点色阶(type1)。
struct ChartParts {
geopro::core::ScatterField scatter;
geopro::core::ColorScale scatterScale;
};
// 网格数据加载结果grid(rows) + 网格色阶(type2) + 异常。
struct GridParts {
geopro::core::Grid grid{1, 1}; // Grid 无默认构造;占位
geopro::core::ColorScale gridScale;
std::vector<geopro::core::Anomaly> anomalies;
};
} // namespace geopro::data

View File

@ -0,0 +1,17 @@
#pragma once
#include <string>
namespace geopro::data {
class ChartLoad;
class GridLoad;
// 数据集详情异步仓储抽象。返回自管理句柄(完成/失败后 deleteLater
class IAsyncDatasetRepository {
public:
virtual ~IAsyncDatasetRepository() = default;
virtual ChartLoad* loadChartAsync(const std::string& dsId) = 0; // scatter + 散点色阶(type1)
virtual GridLoad* loadGridAsync(const std::string& dsId) = 0; // grid(rows) + 色阶(type2) + 异常
};
} // namespace geopro::data

View File

@ -38,6 +38,7 @@ target_sources(geopro_tests PRIVATE data/test_parsers.cpp)
target_sources(geopro_tests PRIVATE data/test_local_repo.cpp) 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_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,51 @@
#include <gtest/gtest.h>
#include <QSignalSpy>
#include "api/DatasetLoadHandles.hpp"
#include "net/FakeApiCall.hpp"
using namespace geopro::data;
using geopro::net::ApiBatch;
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(); };
}
TEST(DatasetLoadHandles, ChartLoadEmitsDoneOnSuccess) {
auto* a = new FakeApiCall;
auto* b = new FakeApiCall;
auto* batch = new ApiBatch({a, b}, isFailure);
bool parsed = false;
auto* load = new ApiChartLoad(batch, [&](const QList<ApiResponse>& resps) {
parsed = (resps.size() == 2);
return ChartParts{};
});
QSignalSpy doneSpy(load, &ChartLoad::done);
a->fire(ok());
b->fire(ok());
EXPECT_EQ(doneSpy.count(), 1);
EXPECT_TRUE(parsed);
}
TEST(DatasetLoadHandles, ChartLoadEmitsFailedOnBatchFailure) {
auto* a = new FakeApiCall;
auto* b = new FakeApiCall;
auto* batch = new ApiBatch({a, b}, isFailure);
auto* load = new ApiChartLoad(batch, [](const QList<ApiResponse>&) { return ChartParts{}; });
QSignalSpy failSpy(load, &ChartLoad::failed);
a->fire(bad());
EXPECT_EQ(failSpy.count(), 1);
}
TEST(DatasetLoadHandles, GridLoadAbortSuppressesLateDone) {
auto* a = new FakeApiCall;
auto* batch = new ApiBatch({a}, isFailure);
auto* load = new ApiGridLoad(batch, [](const QList<ApiResponse>&) { return GridParts{}; });
QSignalSpy doneSpy(load, &GridLoad::done);
load->abort();
a->fire(ok()); // 迟到
EXPECT_EQ(doneSpy.count(), 0); // batch.aborted_ + load.aborted_ 双闸门
}