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
5 changed files with 161 additions and 0 deletions
Showing only changes of commit 22a7f2339e - Show all commits

53
src/net/ApiChain.cpp Normal file
View File

@ -0,0 +1,53 @@
#include "ApiChain.hpp"
#include <stdexcept>
namespace geopro::net {
ApiChain::ApiChain(QList<StepFactory> steps, Predicate isFailure, QObject* parent)
: QObject(parent), steps_(std::move(steps)), isFailure_(std::move(isFailure)) {
Q_ASSERT(!steps_.isEmpty()); // 契约:至少一步(空链永不发 succeeded
Q_ASSERT(isFailure_);
runNext();
}
void ApiChain::runNext() {
if (aborted_) return;
if (index_ >= steps_.size()) { // 全部完成
emit succeeded(responses_);
deleteLater();
return;
}
IApiCall* call = nullptr;
try {
call = steps_[index_](responses_); // 工厂可抛(如 RSA 失败):转 failed
} catch (const std::exception& e) {
ApiResponse r;
r.rawError = QString::fromUtf8(e.what()); // 保留原因;登录 RSA 文案在 AuthService 层包装
aborted_ = true;
emit failed(index_, r);
deleteLater();
return;
}
current_ = call;
QObject::connect(call, &IApiCall::finished, this, [this](const ApiResponse& resp) {
if (aborted_) return; // §5.0 入口守卫
if (isFailure_(resp)) {
aborted_ = true;
emit failed(index_, resp);
deleteLater();
return;
}
responses_.append(resp);
++index_;
runNext(); // 链式推进
});
}
void ApiChain::abort() {
if (aborted_) return;
aborted_ = true;
if (current_) current_->abort(); // abort 当前在飞步骤
deleteLater();
}
} // namespace geopro::net

35
src/net/ApiChain.hpp Normal file
View File

@ -0,0 +1,35 @@
#pragma once
#include <functional>
#include <QList>
#include <QPointer>
#include <QObject>
#include "IApiCall.hpp"
namespace geopro::net {
// 顺序执行 N 个步骤(依赖链):每步工厂用既往响应构造下一 IApiCall工厂可抛 std::exception
// 任一步失败 → fail-fastfailed(index,resp) + abort 当前在飞 + deleteLater。
// 全部成功 → succeeded(按序响应)。安全不变量见 spec §5.0aborted_ 闸门 + 一律 deleteLater
// 与 ApiBatch 对称:同 Predicate、同信号面、同安全约束。
class ApiChain : public QObject {
Q_OBJECT
public:
// 工厂:入参为已完成步骤的响应(按序),返回本步 IApiCall接管所有权。可抛 std::exception。
using StepFactory = std::function<IApiCall*(const QList<ApiResponse>& prior)>;
using Predicate = std::function<bool(const ApiResponse&)>;
ApiChain(QList<StepFactory> steps, Predicate isFailure, QObject* parent = nullptr);
void abort();
signals:
void succeeded(const QList<geopro::net::ApiResponse>& responses);
void failed(int index, const geopro::net::ApiResponse& resp);
private:
void runNext(); // 构造并连接下一步(工厂抛出 → emit failed
QList<StepFactory> steps_;
Predicate isFailure_;
QList<ApiResponse> responses_;
QPointer<IApiCall> current_;
int index_ = 0;
bool aborted_ = false;
};
} // namespace geopro::net

View File

@ -7,6 +7,7 @@ add_library(geopro_net STATIC
IApiCall.cpp
ApiCall.cpp
ApiBatch.cpp
ApiChain.cpp
AuthService.cpp)
target_include_directories(geopro_net PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(geopro_net PUBLIC OpenSSL::SSL OpenSSL::Crypto Qt6::Core Qt6::Network)

View File

@ -50,6 +50,8 @@ target_sources(geopro_tests PRIVATE net/test_rsa.cpp)
target_sources(geopro_tests PRIVATE net/test_auth.cpp)
# ApiBatch 线QSignalSpy Qt6::Test
target_sources(geopro_tests PRIVATE net/test_api_batch.cpp)
# ApiChain 线//abort/
target_sources(geopro_tests PRIVATE net/test_api_chain.cpp)
target_link_libraries(geopro_tests PRIVATE geopro_net OpenSSL::SSL OpenSSL::Crypto Qt6::Core Qt6::Network Qt6::Test)
# geopro_data Qt6::Core exe gtest Qt6Core.dll

View File

@ -0,0 +1,70 @@
#include <gtest/gtest.h>
#include <stdexcept>
#include <QSignalSpy>
#include "ApiChain.hpp"
#include "net/FakeApiCall.hpp"
using namespace geopro::net;
using geopro::net::test::FakeApiCall;
namespace {
ApiResponse ok(int v = 0) { ApiResponse r; r.code = 200; r.httpStatus = 200; r.data = QJsonObject{{"v", v}}; 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(ApiChain, RunsStepsInOrderAndPassesPriorResponses) {
auto* s1 = new FakeApiCall;
auto* s2 = new FakeApiCall;
int seenPrior = -1;
QList<ApiChain::StepFactory> steps{
[&](const QList<ApiResponse>&) -> IApiCall* { return s1; },
[&](const QList<ApiResponse>& prior) -> IApiCall* {
seenPrior = prior.size(); // 第二步能看到第一步响应
return s2;
}};
auto* chain = new ApiChain(steps, isFailure);
QSignalSpy okSpy(chain, &ApiChain::succeeded);
s1->fire(ok(11)); // 第一步完成 → 触发第二步工厂
EXPECT_EQ(seenPrior, 1);
EXPECT_EQ(okSpy.count(), 0); // 还差第二步
s2->fire(ok(22));
EXPECT_EQ(okSpy.count(), 1);
const auto resps = okSpy.takeFirst().at(0).value<QList<ApiResponse>>();
EXPECT_EQ(resps.size(), 2);
}
TEST(ApiChain, FailFastShortCircuitsRemainingSteps) {
auto* s1 = new FakeApiCall;
bool secondBuilt = false;
QList<ApiChain::StepFactory> steps{
[&](const QList<ApiResponse>&) -> IApiCall* { return s1; },
[&](const QList<ApiResponse>&) -> IApiCall* { secondBuilt = true; return new FakeApiCall; }};
auto* chain = new ApiChain(steps, isFailure);
QSignalSpy failSpy(chain, &ApiChain::failed);
s1->fire(bad()); // 第一步失败
EXPECT_EQ(failSpy.count(), 1);
EXPECT_FALSE(secondBuilt); // 后续步骤不再构造
}
TEST(ApiChain, AbortGateSuppressesLateSignals) {
auto* s1 = new FakeApiCall;
QList<ApiChain::StepFactory> steps{[&](const QList<ApiResponse>&) -> IApiCall* { return s1; }};
auto* chain = new ApiChain(steps, isFailure);
QSignalSpy okSpy(chain, &ApiChain::succeeded);
chain->abort();
EXPECT_TRUE(s1->aborted); // 在飞步骤被 abort
s1->fire(ok()); // 迟到
EXPECT_EQ(okSpy.count(), 0); // aborted_ 闸门
}
TEST(ApiChain, StepFactoryThrowBecomesFailed) {
auto* s1 = new FakeApiCall;
QList<ApiChain::StepFactory> steps{
[&](const QList<ApiResponse>&) -> IApiCall* { return s1; },
[&](const QList<ApiResponse>&) -> IApiCall* { throw std::runtime_error("rsa fail"); }};
auto* chain = new ApiChain(steps, isFailure);
QSignalSpy failSpy(chain, &ApiChain::failed);
s1->fire(ok()); // 触发第二步工厂 → 抛 → failed
EXPECT_EQ(failSpy.count(), 1);
}