25 KiB
设计:ApiClient 异步化(DatasetDetail 路径试点)
日期 2026-06-11。范围:把数据集详情路径从同步阻塞改为异步非阻塞,作为全 App 异步化的模式样板。 后续会按此模式铺开到导航/登录路径(本期不做)。
状态更新(2026-06-11)
DatasetDetail 试点:✅ 已完成并通过评审。 实现计划 plans/2026-06-11-apiclient-async-datasetdetail.md(8 任务,逐任务 spec+质量双评审 + 整体评审)。测试 75 → 89(+14 离线用例)全绿。落地原语:IApiCall/ApiCall/ApiBatch(net)、ChartLoad/GridLoad/IAsyncDatasetRepository(data)、控制器 abort-and-replace + 句柄身份比对 + 退出契约、LoadingOverlay 网格懒加载遮罩。核心收益落地:详情路径不冻 UI、慢请求可 abort 不回灌、多请求并发 + fail-fast。
铺开进展(2026-06-12 更新):
- 导航路径 ✅(计划
plans/2026-06-11-apiclient-async-rollout.mdPart A):新增ApiChain(串行依赖链原语)、NavRequest(单请求句柄,QVariant payload)、IAsyncProjectRepository;WorkbenchNavController全异步(NavRequest 续延 + 并发计数 + abort-and-replace + 身份比对,删 busy_/drain,busyChanged=在飞存在性)。 - 登录路径 ✅(同计划 Part B:B1/B2/B4):
AuthService异步(CaptchaLoad/LoginLoad+ApiChain编排 verify→RSA→login2);LoginWindow不冻 + 可取消(析构 abort);test_authlive 异步化。 - 测试 89 → 116。每块逐任务 spec+质量双评审 + 整体评审通过。
技术债清除 ✅(2026-06-12): ProjectListDialog 已迁到 IAsyncProjectRepository(NavRequest + abort-and-replace + 身份比对 + 析构 abort),随即删除同步 IProjectRepository、RepoResult、ApiProjectRepository 9 个同步方法、ApiClient 同步 get/postJson+await(A6+B3 解锁完成)。全 App 网络层现已 100% 异步,无 QEventLoop 阻塞、无过渡双接口债。 测试 116/116。
结论:异步化主题完成。 数据详情(试点)+ 导航(Part A)+ 登录(Part B)+ 项目列表弹窗全部异步;同步路径彻底移除。
可选 follow-up(评审建议,非阻断,纯整洁):删 DatasetDetailController::ChartData.grid/gridScale 死字段;如未来引入 cross-thread 再补 qRegisterMetaType<QList<ApiResponse>>()。
铺开实现计划:
plans/2026-06-11-apiclient-async-rollout.md(Part A/B + 债务清除均已落地)。
1. 背景
geopro 现网络栈三层全同步阻塞:
ApiClient(src/net/):单个QNetworkAccessManager,get/postJson内部用QNetworkReply + QEventLoop死等响应。共享 cookie / JSESSIONID。- Repository(
ApiDatasetRepository、ApiProjectRepository):同步调api_.get/postJson,解析后返回 typed 值或抛std::runtime_error。 - Controller(
DatasetDetailController、WorkbenchNavController):在 slot 内同步调 repo,再emit结果信号。
核心问题:每次请求都用 QEventLoop 阻塞 UI 线程 → 全 App 冻结。其中数据集详情「网格数据」页签的 dd/ert/inversion/rows/{id} 请求因服务端网格化波动 1–4s,期间界面完全卡死。
QEventLoop 阻塞的副作用是可重入:阻塞期间 Qt 仍泵事件,用户的双击/切换会重入 slot,因此现有控制器到处用 busy_ 守卫挡重入——这些守卫本身就是同步阻塞架构的产物。
QNetworkAccessManager 本就是异步设计;当前架构用 QEventLoop 把它人为变同步,是反模式。本设计回归 Qt 原生异步。
2. 目标与非目标
目标
- 数据集详情路径(原数据 + 网格数据)全程不阻塞 UI 线程。
- 慢请求在飞时切换/关闭数据集页签可主动 abort 过期请求,结果不回灌。
- 同一逻辑加载内的多个独立请求并发发出(原数据 2 个、网格 3 个),缩短总等待。
- 加载期间给用户轻量"加载中"反馈(不再因冻结而"看起来在响应")。
- 产出可复用的异步原语(
ApiCall/ApiBatch/ 句柄模式),供后续导航/登录路径照搬。 - 现有测试不退化;新增异步路径的单测可离线运行。
非目标(本期不做)
- 不改
WorkbenchNavController/ApiProjectRepository/AuthService/ 登录流程——它们继续走同步ApiClient.get/postJson。 - 不做后台工作线程:网络是 I/O 密集,
QNetworkAccessManager原生异步即可,不引入QThread/QtConcurrent。 - 不追求与原版 loading 动画像素级一致(轻量提示即可,细抠后续单独做)。
- 不改
LocalSampleRepository(它实现旧同步IDatasetRepository,但不经详情控制器使用)。
3. 已定决策(及理由)
| 决策 | 选择 | 理由 |
|---|---|---|
| 范围 | 仅 DatasetDetail 路径 | 风险小、最痛点(1–4s rows)先解决、产出可复用模式 |
| 取消语义 | 主动 abort 过期请求 | 体验最好;句柄对象使 abort 成为一等公民 |
| 加载反馈 | 轻量"加载中"遮罩 + 禁用交互 | 不阻塞后必须给反馈;先跑通机制 |
| 异步机制 | 方案 A:每请求一个 QObject 句柄(信号 + abort) | 纯 Qt 信号、贴合现有信号驱动代码库;abort 用具体句柄最干净(QFuture 的 cancel 不中断底层 reply,反而要再加句柄);测试用 QSignalSpy 顺手 |
| 多请求汇聚 | fail-fast with abort(任一项失败立即回报并 abort 其余在飞 call) | 慢的 rows 1–4s,若 colorScale 200ms 先失败,gather-all 会让用户干等 1–4s 才看到失败(失败比成功还慢,违背"及时反馈")。fail-fast 依赖下方「abort 闸门」机制杜绝迟到回灌,原"避免部分-abort 竞态"的顾虑已被该机制消解。成功路径仍需全部到齐 |
| abort 闸门(核心安全机制) | 链路级 aborted_ 标志 + 句柄身份比对 |
见 §5.1/§5.3:仅靠 disconnect 挡不住「已 emit、正在事件队列等待派发」的迟到信号;必须每层入口判 aborted_ + 控制器比对句柄身份,才能兑现"abort 后绝不回灌"的核心承诺 |
4. 架构
DatasetDetailController (QObject,状态/编排)
│ 调 repo.loadChartAsync(id) / loadGridAsync(id),持有返回的 *Load 句柄
│ 切换/关页 → 句柄.abort()
▼
IAsyncDatasetRepository(纯抽象) ←── ApiDatasetRepository 实现
│ 每次 loadXxxAsync 创建一个 *Load 句柄,内部用 ApiBatch 并发 N 个 ApiCall
│ batch 全部完成 → 逐项校验+解析 → emit done(Parts) / failed(msg)
▼
ApiClient.getAsync(path) → ApiCall(包一个 QNetworkReply,实现 IApiCall)
(ApiClient 保留同步 get/postJson 供 Nav/登录继续使用,共享同一 QNetworkAccessManager)
信号面保持不变:DatasetDetailController 对外仍发 chartReady(ChartData) / gridReady(GridData) / loadFailed(dsId,msg) / focusRequested(dsId),新增 loadStarted(dsId, Phase)。因此 main.cpp 接线与 DatasetDetailPanel 几乎不动,仅接 loadStarted 显示遮罩。
5. 组件设计
5.0 核心安全不变量:abort 后绝不回灌(贯穿全链路)
这是本设计成败的关键。
disconnect只能阻止「将来才发」的信号,挡不住「已经 emit、已转成QMetaCallEvent在事件队列中等待派发」的迟到信号(尤其在共享 NAM 同步嵌套QEventLoop期间,见 5.1)。因此 disconnect 仅作"尽力而为",真正的权威闸门是下面两条:
- 每层
aborted_标志 + 入口守卫:ApiCall/ApiBatch/ChartLoad/GridLoad各持一个bool aborted_。所有finished/done/failed槽入口第一行if (aborted_) return;。abort()先置aborted_=true再断连/中断。聚合 emit 前同样判aborted_。 - 控制器句柄身份比对:
DatasetDetailController在done/failed槽内校验「发信号的句柄 == 当前持有的chartLoad_/gridLoad_」(用sender()或 lambda 捕获句柄指针比对),过期句柄的迟到信号直接丢弃。与 abort-and-replace 形成纵深防御。 - 销毁一律
deleteLater:ApiCall/ApiBatch/*Load/QNetworkReply在 abort 或完成后一律deleteLater,禁止任何同步delete——避免「abort 一个正在自己回调栈中的对象」导致 use-after-free。
5.1 net 层
IApiCall(新增,抽象,可测试缝)
// src/net/IApiCall.hpp
class IApiCall : public QObject {
Q_OBJECT
public:
using QObject::QObject;
virtual void abort() = 0; // 中断;不再 emit finished;deleteLater
signals:
void finished(const geopro::net::ApiResponse& resp); // 成功/错误均经此(错误写 rawError)
};
存在意义:ApiBatch 只依赖 IApiCall,单测可注入假 call(不碰真实网络),离线测 gather/abort。
ApiCall(新增,实现 IApiCall)
- 持
bool aborted_=false(见 §5.0)。 - 构造接管一个
QNetworkReply*;连接reply->finished→ 槽入口判if (aborted_) return;→ 解析为ApiResponse(复用现parseBody)→ emitfinished→deleteLater()自删。 abort():aborted_=true→disconnect自身与 reply 的连接 →reply->abort()→reply->deleteLater()→this->deleteLater();禁止同步 delete。reply->abort()本身会再触发一次finished(OperationCanceledError),已被 disconnect +aborted_双重挡掉。- 跨
QueuedConnection传ApiResponse需qRegisterMetaType<geopro::net::ApiResponse>()(应用启动时注册一次)。
ApiClient(扩展,不破坏现有)
net::IApiCall* getAsync(const QString& path); // 立即返回,不阻塞
net::IApiCall* postJsonAsync(const QString& path, const QJsonObject& body);
// 保留:ApiResponse get(path); ApiResponse postJson(path, body); // Nav/登录继续用
getAsync/postJsonAsync用同一nam_发起,返回new ApiCall(reply)。token 注入、buildRequest复用。- 已知限制(非"无害",受 §5.0 三条约束兜底):Nav 的同步嵌套
QEventLoop(ApiClient::get的loop.exec())期间,详情的异步 reply 若完成会被一并泵出,其整条槽链(ApiCall→ApiBatch→*Load→Controller→chartReady→detailPanel->openOrUpdate)会在 Nav 同步调用栈、嵌套循环里重入执行,即详情 UI 重建可能发生在 Nav 网络调用返回前。这不是"互不写状态"能涵盖的,但有 §5.0 兜底:(a) 一律deleteLater杜绝栈内同步释放;(b) 各层aborted_入口守卫;(c) 控制器句柄身份比对。本期 Nav 仍同步、窗口期有限;全异步后该同步路径与本限制一并消失。可选缓解(YAGNI,暂不做):详情完成回调改QueuedConnection投递,避免在嵌套栈内驱动 UI。
ApiBatch(新增,汇聚原语,fail-fast)
// src/net/ApiBatch.hpp
class ApiBatch : public QObject {
Q_OBJECT
public:
// calls 接管所有权;predicate 由 repo 注入,判定单个 ApiResponse 是否业务失败
ApiBatch(QList<net::IApiCall*> calls,
std::function<bool(const net::ApiResponse&)> isFailure,
QObject* parent=nullptr);
void abort(); // aborted_=true,abort 全部未完成子 call,self deleteLater,不 emit
signals:
void succeeded(const QList<geopro::net::ApiResponse>& responses); // 全部成功,按下标对齐
void failed(int index, const geopro::net::ApiResponse& resp); // 首个失败项
};
- 所有权契约:内部用
QList<QPointer<net::IApiCall>>持有子 call;子 call 正常finished后自删(QPointer自动置空)。abort()遍历时跳过空指针。子 call parent 不设为 batch(各自deleteLater自管),靠QPointer+aborted_防悬垂。 - 持
bool aborted_(§5.0)。每个子 callfinished槽入口判aborted_。 - fail-fast:每个子 call
finished到达时立即用isFailure(resp)判定——失败则emit failed(i, resp)+aborted_=true+ abort 其余在飞子 call +deleteLater;成功则记入responses_[i],全部到齐 →emit succeeded(responses)+deleteLater。 - 业务/传输/HTTP 三类失败统一由
isFailure判定(见 §7)。
5.2 data 层
data 层结果载体(中性,避免 data→controller 反向依赖)
// src/data/api/DatasetLoads.hpp
struct ChartParts { core::ScatterField scatter; core::ColorScale scatterScale; };
struct GridParts { core::Grid grid{1,1}; core::ColorScale gridScale; std::vector<core::Anomaly> anomalies; };
*Load 句柄(每次加载一个,承载 typed 结果 + abort)
class ChartLoad : public QObject {
Q_OBJECT
public:
void abort(); // aborted_=true,转发给内部 ApiBatch
signals:
void done(const geopro::data::ChartParts& parts);
void failed(const QString& message);
};
class GridLoad : public QObject { /* done(GridParts) / failed(QString) / abort() */ };
- 持
bool aborted_(§5.0);连ApiBatch::succeeded槽入口判aborted_后解析→ emitdone→deleteLater;连ApiBatch::failed→ 取msg(见 §7)→ emitfailed→deleteLater。abort()置aborted_并转发给内部QPointer<ApiBatch>。 - 用句柄对象做"每次逻辑加载"的天然关联点 + abort 目标(避免请求 id 簿记)。
旧同步方法去向(接口改形审计)
ApiDatasetRepository 不再继承 IDatasetRepository。旧 6 个同步方法去向:
| 旧方法 | 去向 |
|---|---|
loadStructure() |
丢弃(详情路径不用;现实现即返回 {} 占位;结构树走 LocalSampleRepository/Nav) |
loadScatter + loadScatterColorScale |
合并进 loadChartAsync |
loadGrid + loadColorScale + loadAnomalies |
合并进 loadGridAsync |
LocalSampleRepository 继续实现同步 IDatasetRepository,二接口并存;buildWorkbench 的 repo 形参类型不变。已核对:无任何处把 ApiDatasetRepository 当 IDatasetRepository* 用于 loadStructure,丢弃安全。
IAsyncDatasetRepository(新增抽象,详情控制器依赖它)
// src/data/repo/IAsyncDatasetRepository.hpp
class IAsyncDatasetRepository {
public:
virtual ~IAsyncDatasetRepository() = default;
virtual data::ChartLoad* loadChartAsync(const std::string& dsId) = 0; // scatter + scatterScale(type1)
virtual data::GridLoad* loadGridAsync(const std::string& dsId) = 0; // grid(rows) + scale(type2) + anomalies
};
- 复合方法(而非旧 6 个原子
loadXxx):原子方法把"该发哪几个请求"泄漏给了控制器;汇聚+abort 是核心复杂度,集中放 repo(它本就知道请求集)是正确归属,控制器保持轻薄。
ApiDatasetRepository(改造)
- 不再继承同步
IDatasetRepository;改实现IAsyncDatasetRepository。 loadChartAsync:建ApiBatch{ getScatter, getScatterScale },注入isFailure(见 §7)→succeeded时复用现有 DTO 解析(DatasetChartDto等)填ChartParts并done;failed→ 取错误原因并failed。loadGridAsync:ApiBatch{ getRows, getColorScale, getException }→ 同构填GridParts。- 现有同步解析/校验逻辑(DTO、v 行数校验、markType 钳制)与
must()的判定(code==200)原样搬入,不重写——must()的判定提炼为 batch 的isFailure谓词。 - 空数据非失败:异常列表可能为空——业务
code==200且 data 为空走成功(空GridParts.anomalies),与现状一致。
5.3 controller 层
DatasetDetailController(改造)
enum class Phase { Chart, Grid };
signals:
void loadStarted(const QString& dsId, Phase phase); // 新增:驱动加载遮罩
// 不变:chartReady(ChartData)/gridReady(GridData)/loadFailed(dsId,msg)/focusRequested(dsId)
private:
IAsyncDatasetRepository& repo_;
QPointer<data::ChartLoad> chartLoad_; // 当前在飞(QPointer 防悬垂)
QPointer<data::GridLoad> gridLoad_;
openDataset(dsId, ddCode):ddCode 非dd_inversion_data→loadFailed;否则 若chartLoad_在飞先abort(),repo_.loadChartAsync取新句柄存入chartLoad_,emitloadStarted(dsId, Chart),连:done→ 句柄身份比对(捕获的句柄指针 == 当前chartLoad_?否则丢弃迟到信号,见 §5.0)→ 组ChartData→chartReady→ 清空chartLoad_。failed→ 同样身份比对 →loadFailed→ 清空chartLoad_。
loadGridData(dsId, ddCode):同构,针对gridLoad_与gridReady。- 移除
busy_守卫:由 abort-and-replace + 句柄身份比对取代(新请求来→abort 旧→发新;旧句柄的迟到信号被身份比对丢弃)。 - 句柄完成/失败后清空对应
QPointer。控制器析构时 abort 所有在飞句柄(见 §7 退出契约)。 - 现有
catch(...)兜底不再需要(无同步抛出路径);错误统一经failed信号传递。
5.4 UI 层(加载反馈)
- 新增
LoadingOverlay(QWidget,半透明 + "加载中…" + 可选 busy 指示),可贴在任一视图区上层。 DatasetDetailPanel:- 接
loadStarted(dsId, Grid)→ 在对应 ds 页的网格视图上显示遮罩 + 禁用交互;gridReady/loadFailed→ 隐藏。 - 原数据初次加载(~0.8s):页面仍于
chartReady创建;加载期用 busy 光标 + 状态栏"加载中…"(最小改动)。网格遮罩是高价值项(页已存在、等待 1–4s)。
- 接
- 像素级对齐原版 loading 留后续。
6. 数据流(时序)
原数据(双击数据集)
detailCtrl.openDataset(id, "dd_inversion_data")→ emitloadStarted(id,Chart)(UI 置 busy)。- abort 旧
chartLoad_(若有)→repo.loadChartAsync(id)返回ChartLoad*。 - repo 内
ApiBatch{getScatter, getScatterScale}并发;UI 线程保持响应。 - 两 reply 全成功到齐 →
batch.succeeded→ repo 解析 →ChartLoad::done(ChartParts)。 - controller 身份比对通过 → 组
ChartData→chartReady→detailPanel->openOrUpdate(清 busy)。- 任一项失败(fail-fast):首个失败 →
batch.failed+ abort 其余在飞 →ChartLoad::failed→loadFailed→ 状态栏提示(清 busy),不等其余请求。
- 任一项失败(fail-fast):首个失败 →
网格数据(首次切到网格页签)
gridDataNeeded→loadGridData(id, ...)→ emitloadStarted(id,Grid)(网格视图遮罩)。- abort 旧
gridLoad_→repo.loadGridAsync(id)。 ApiBatch{getRows(慢1–4s), getColorScale, getException}并发。- 全成功到齐 → 解析 →
done(GridParts)→gridReady→setGridData(隐遮罩)。若 colorScale/exception 先失败 → fail-fast 立即failed+ abort rows,不干等 1–4s。 - 期间用户切走/关页 → controller abort
gridLoad_→ 子 reply 全 abort +aborted_闸门 → 即便有迟到信号也被丢弃,无回灌。
7. 错误处理与边界
- 失败判定三要素(
isFailure谓词,由现must()提炼):一个ApiResponse视为失败当:(1) 传输错误!rawError.isEmpty();或 (2) HTTP 异常(必要时查httpStatus);或 (3) 业务码code != 200(服务端常返回 HTTP 200 但code=500,这是现must()的判定口径,不能只看 httpStatus)。失败原因取msg(空则rawError)。 - abort 竞态(核心,见 §5.0):单靠 disconnect 不足以挡「已入队的迟到信号」。权威闸门 = 各层
aborted_入口守卫 + 控制器句柄身份比对 + 一律deleteLater。ApiCall::abort置aborted_后 disconnect+reply->abort()。 - 重复触发:abort-and-replace + 身份比对天然幂等(新覆旧,旧迟到信号丢弃)。
- 控制器析构(退出契约):abort 所有在飞句柄。
ApiCall/ApiBatch/*Load不得在析构或deleteLater回调里访问nam/reply(reply 在 abort 时已deleteLater)。main.cpp 栈对象析构逆序为detailCtrl→datasetRepo→api,确保控制器先 abort、ApiClient 最后毁;句柄不持有对nam的长期引用。 - 共享 NAM 同步/异步并存:见 §5.1,已知限制(非"无害"),受 §5.0 三约束兜底,全异步后消除。
- 空/缺数据:异常列表可能为空——业务
code==200走done(空),非failed,与现状一致。
8. 测试策略
ApiBatch(离线单测):注入假IApiCall(受测可控 emitfinished/记录abort)。- 全成功 →
succeeded一次、下标对齐。 - fail-fast:某子 call 先返回失败 →
failed(i,resp)一次 + 其余在飞子 call 被abort(且不再等待)。 - abort 闸门:batch
abort()后,未完成子 call 均被 abort;之后即便假 call 延迟 emitfinished,batch 也不 emit(aborted_守卫)。
- 全成功 →
DatasetDetailController(离线单测,QSignalSpy + 事件循环):用StubAsyncRepo返回假*Load,可控 emitdone/failed(即时或QMetaObject::invokeMethod(..., QueuedConnection)模拟迟到)。异步桩非即时返回,断言需spy.wait()/QTest::qWaitspin 事件循环。done→ 一次chartReady/gridReady;failed→ 一次loadFailed;非dd_inversion_data→ 直接loadFailed。- 在飞时再
openDataset→ 旧句柄被abort(stub 记录)。 - 回灌防护(回归原 bug 的核心用例):① 句柄 abort 后延迟 emit
done→ 控制器零chartReady(身份比对 + aborted_ 生效);②openDataset(A)在飞 →openDataset(B)→ A 句柄迟到 emitdone→ 仅 B 的数据被chartReady,A 丢弃。
ApiCall/getAsync(可选 live test):依现有AuthLiveTest先例,标记为 live,不计入离线覆盖门槛。- 现有 75 个测试中,
DatasetDetailController.*两个改为异步桩版本(加事件循环 spin);其余不动。LocalRepo.*不受影响(同步接口保留)。 - 目标:异步新增逻辑(batch/controller)离线单测覆盖 ≥ 80%。
9. 迁移步骤(供 writing-plans 细化)
- net:
IApiCall+ApiCall+ApiClient::getAsync/postJsonAsync(保留同步)。parseBody抽为可复用。 - net:
ApiBatch+ 离线单测。 - data:
DatasetLoads.hpp(ChartParts/GridParts)+ChartLoad/GridLoad+IAsyncDatasetRepository。 - data:
ApiDatasetRepository改实现异步接口(搬入现有 DTO 解析)。 - controller:
DatasetDetailController改异步 + abort-and-replace +loadStarted;改其单测。 main.cpp装配换IAsyncDatasetRepository+ 启动注册qRegisterMetaType<ApiResponse>()。- UI:
LoadingOverlay+DatasetDetailPanel接loadStarted。此步与异步内核无依赖,可在步骤 1–6 通过测试后再做/并行,以减小试点 PR 体积(surgical)。 - 全量构建 + 测试(dev-build / dev-test);手动验证:切换/关页 abort、网格遮罩、并发加载、UI 不冻、失败 fail-fast 即时报错。
10. 风险
| 风险 | 缓解 |
|---|---|
| 已入队的迟到信号回灌(最危险) | §5.0 闸门:各层 aborted_ 入口守卫 + 控制器句柄身份比对 + 一律 deleteLater;§8 专项回归用例 |
| abort 后回调悬垂/二次释放 | QPointer + abort 先置 aborted_+disconnect + 禁止同步 delete,仅 deleteLater |
| 句柄/batch 生命周期泄漏 | 完成或 abort 后统一 deleteLater;ApiBatch 用 QPointer 持子 call |
| 同步/异步共用 NAM 嵌套重入 | 已知限制(非无害);§5.0 三约束兜底;全异步后消除 |
| 退出期 UAF(句柄 deleteLater 晚于 ApiClient 析构) | 退出契约:句柄不在析构/回调访问 nam/reply;栈析构逆序保证 ctrl→repo→api |
| 失败比成功慢(gather-all 缺陷) | 改 fail-fast + abort 其余在飞 call |
| DTO 解析逻辑搬迁出错 | 原样搬入、不重写;must()→isFailure 谓词;保留现有解析单测 |
| 接口改形(原子→复合)波及面 | 仅详情控制器消费该接口;§5.2 旧方法去向表;LocalSampleRepository 不碰 |
11. 相关文件
- 改:
src/net/ApiClient.{hpp,cpp}、src/data/api/ApiDatasetRepository.{hpp,cpp}、src/controller/DatasetDetailController.{hpp,cpp}、src/app/main.cpp、src/app/panels/.../DatasetDetailPanel.*、tests/controller/test_dataset_detail_controller.cpp、各CMakeLists.txt。 - 新:
src/net/IApiCall.hpp、src/net/ApiCall.{hpp,cpp}、src/net/ApiBatch.{hpp,cpp}、src/data/api/DatasetLoads.hpp、src/data/api/DatasetLoadHandles.{hpp,cpp}(ChartLoad/GridLoad)、src/data/repo/IAsyncDatasetRepository.hpp、src/app/.../LoadingOverlay.{hpp,cpp}、tests/net/test_api_batch.cpp。 - 不动:
src/data/repo/IDatasetRepository.hpp(同步,留给 LocalSampleRepository)、Nav/Auth/Project 路径。