feat(data): 异步仓储接口 + ChartLoad/GridLoad 句柄(抽象基+Api实现) + 离线单测
This commit is contained in:
parent
e980ddd346
commit
bb602e2011
|
|
@ -6,8 +6,9 @@ add_library(geopro_data STATIC
|
|||
dto/NavDto.cpp
|
||||
dto/DatasetChartDto.cpp
|
||||
api/ApiProjectRepository.cpp
|
||||
api/ApiDatasetRepository.cpp)
|
||||
api/ApiDatasetRepository.cpp
|
||||
api/DatasetLoadHandles.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)
|
||||
set_target_properties(geopro_data PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF)
|
||||
set_target_properties(geopro_data PROPERTIES AUTOMOC ON AUTOUIC OFF AUTORCC OFF)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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→解析→done;failed→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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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_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)
|
||||
target_link_libraries(geopro_tests PRIVATE geopro_data)
|
||||
|
||||
# net 层:RSA 加密器。测试需直接用 OpenSSL 生成/解密密钥,故显式 find_package
|
||||
|
|
|
|||
|
|
@ -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_ 双闸门
|
||||
}
|
||||
Loading…
Reference in New Issue