From 22a7f2339e0223bde0927db9f250fe1a5b28e7d9 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Fri, 12 Jun 2026 07:36:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(net):=20ApiChain=20=E9=A1=BA=E5=BA=8F?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E9=93=BE=E5=8E=9F=E8=AF=AD(fail-fast+abort?= =?UTF-8?q?=E9=97=B8=E9=97=A8+=E5=B7=A5=E5=8E=82=E5=8F=AF=E6=8A=9B)=20+=20?= =?UTF-8?q?=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/net/ApiChain.cpp | 53 +++++++++++++++++++++++++++ src/net/ApiChain.hpp | 35 ++++++++++++++++++ src/net/CMakeLists.txt | 1 + tests/CMakeLists.txt | 2 ++ tests/net/test_api_chain.cpp | 70 ++++++++++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+) create mode 100644 src/net/ApiChain.cpp create mode 100644 src/net/ApiChain.hpp create mode 100644 tests/net/test_api_chain.cpp diff --git a/src/net/ApiChain.cpp b/src/net/ApiChain.cpp new file mode 100644 index 0000000..fa8909f --- /dev/null +++ b/src/net/ApiChain.cpp @@ -0,0 +1,53 @@ +#include "ApiChain.hpp" +#include + +namespace geopro::net { + +ApiChain::ApiChain(QList 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 diff --git a/src/net/ApiChain.hpp b/src/net/ApiChain.hpp new file mode 100644 index 0000000..8575b87 --- /dev/null +++ b/src/net/ApiChain.hpp @@ -0,0 +1,35 @@ +#pragma once +#include +#include +#include +#include +#include "IApiCall.hpp" + +namespace geopro::net { + +// 顺序执行 N 个步骤(依赖链):每步工厂用既往响应构造下一 IApiCall(工厂可抛 std::exception)。 +// 任一步失败 → fail-fast:failed(index,resp) + abort 当前在飞 + deleteLater。 +// 全部成功 → succeeded(按序响应)。安全不变量见 spec §5.0(aborted_ 闸门 + 一律 deleteLater)。 +// 与 ApiBatch 对称:同 Predicate、同信号面、同安全约束。 +class ApiChain : public QObject { + Q_OBJECT +public: + // 工厂:入参为已完成步骤的响应(按序),返回本步 IApiCall(接管所有权)。可抛 std::exception。 + using StepFactory = std::function& prior)>; + using Predicate = std::function; + ApiChain(QList steps, Predicate isFailure, QObject* parent = nullptr); + void abort(); +signals: + void succeeded(const QList& responses); + void failed(int index, const geopro::net::ApiResponse& resp); +private: + void runNext(); // 构造并连接下一步(工厂抛出 → emit failed) + QList steps_; + Predicate isFailure_; + QList responses_; + QPointer current_; + int index_ = 0; + bool aborted_ = false; +}; + +} // namespace geopro::net diff --git a/src/net/CMakeLists.txt b/src/net/CMakeLists.txt index 64c9341..d529f39 100644 --- a/src/net/CMakeLists.txt +++ b/src/net/CMakeLists.txt @@ -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) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 98ee36e..36c31c5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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 等运行时 diff --git a/tests/net/test_api_chain.cpp b/tests/net/test_api_chain.cpp new file mode 100644 index 0000000..7c7f70a --- /dev/null +++ b/tests/net/test_api_chain.cpp @@ -0,0 +1,70 @@ +#include +#include +#include +#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 steps{ + [&](const QList&) -> IApiCall* { return s1; }, + [&](const QList& 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>(); + EXPECT_EQ(resps.size(), 2); +} + +TEST(ApiChain, FailFastShortCircuitsRemainingSteps) { + auto* s1 = new FakeApiCall; + bool secondBuilt = false; + QList steps{ + [&](const QList&) -> IApiCall* { return s1; }, + [&](const QList&) -> 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 steps{[&](const QList&) -> 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 steps{ + [&](const QList&) -> IApiCall* { return s1; }, + [&](const QList&) -> 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); +}