#include "WorkbenchNavController.hpp" #include #include #include #include #include "api/NavLoads.hpp" #include "api/NavRequest.hpp" #include "dto/NavDto.hpp" #include "repo/IAsyncProjectRepository.hpp" namespace geopro::controller { namespace { // 数据页树形:一次取全的大 pageSize(远超单 TM 实际 DS 数;超出会日志告警)+ 每页根节点数。 constexpr int kFetchAllPageSize = 1000; constexpr int kDataRootPageSize = 5; } // namespace using data::DsPage; using data::DsRow; using data::DynamicForm; using data::ExceptionRow; using data::NavRequest; using data::ProjectListPage; using data::ProjectSummary; using data::StructNode; using data::Workspace; WorkbenchNavController::WorkbenchNavController(data::IAsyncProjectRepository& repo, QObject* parent) : QObject(parent), repo_(repo) {} WorkbenchNavController::~WorkbenchNavController() { abortAll(); } bool WorkbenchNavController::anyInflight() const { if (startStepReq_ || structReq_ || selDataReq_ || selFileReq_ || selDetailReq_ || moreFilesReq_ || datasetReq_) return true; for (const auto& h : checkedInflight_) if (h) return true; return false; } void WorkbenchNavController::emitBusyIfChanged() { const bool now = anyInflight(); if (now != lastBusy_) { lastBusy_ = now; emit busyChanged(now); } } void WorkbenchNavController::abortAll() { if (startStepReq_) startStepReq_->abort(); if (structReq_) structReq_->abort(); if (selDataReq_) selDataReq_->abort(); if (selFileReq_) selFileReq_->abort(); if (selDetailReq_) selDetailReq_->abort(); if (moreFilesReq_) moreFilesReq_->abort(); if (datasetReq_) datasetReq_->abort(); for (const auto& h : checkedInflight_) if (h) h->abort(); checkedInflight_.clear(); } void WorkbenchNavController::resetSelectionState() { tmExceptionCache_.clear(); currentParentId_.clear(); currentParentConfType_ = 0; allDataRows_.clear(); dataRootsShown_ = 0; dataTotal_ = 0; } // ── start / switchWorkspace 依赖链:listWorkspaces → pageProjects → loadStructure ── // 用 NavRequest 续延(每级 done 内用业务值构造下一级),startStepReq_ 跟踪当前在飞级。 // abort-and-replace:start/switchWorkspace 入口 abort 旧 startStepReq_ → 自然丢弃旧链迟到信号。 void WorkbenchNavController::start() { if (startStepReq_) startStepReq_->abort(); NavRequest* req = repo_.listWorkspacesAsync(); startStepReq_ = req; emitBusyIfChanged(); QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) { if (req != startStepReq_) return; // §5.0 身份比对 startStepReq_.clear(); const auto ws = qvariant_cast>(v); QString cur; for (const auto& w : ws) if (w.isCurrent) cur = QString::fromStdString(w.id); if (cur.isEmpty() && !ws.empty()) cur = QString::fromStdString(ws.front().id); currentWorkspaceId_ = cur.toStdString(); emit workspacesLoaded(ws, cur); runProjectsAndStructure(); }); QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) { if (req != startStepReq_) return; startStepReq_.clear(); emit loadFailed(QStringLiteral("workspaces"), msg); emitBusyIfChanged(); }); } void WorkbenchNavController::switchWorkspace(const QString& tenantId) { if (tenantId.isEmpty()) return; if (startStepReq_) startStepReq_->abort(); NavRequest* req = repo_.switchWorkspaceAsync(tenantId.toStdString()); startStepReq_ = req; emitBusyIfChanged(); const std::string tid = tenantId.toStdString(); QObject::connect(req, &NavRequest::done, this, [this, req, tid](const QVariant&) { if (req != startStepReq_) return; startStepReq_.clear(); currentWorkspaceId_ = tid; runProjectsAndStructure(); }); QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) { if (req != startStepReq_) return; startStepReq_.clear(); emit loadFailed(QStringLiteral("switchWorkspace"), msg); emitBusyIfChanged(); }); } void WorkbenchNavController::runProjectsAndStructure() { NavRequest* req = repo_.pageProjectsAsync(std::string(), std::string(), 1, 10); // 下拉首页 10 startStepReq_ = req; emitBusyIfChanged(); QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) { if (req != startStepReq_) return; startStepReq_.clear(); const auto page = qvariant_cast(v); lastProjects_ = page.rows; resetSelectionState(); QString curP; if (!page.rows.empty()) { const auto& first = page.rows.front(); curP = QString::fromStdString(first.id); currentProjectId_ = first.id; currentProjectName_ = first.name; currentCrsCode_ = first.crsCode; } else { currentProjectId_.clear(); currentProjectName_.clear(); currentCrsCode_.clear(); } emit projectsLoaded(page.rows, curP, page.total); if (curP.isEmpty()) { lastStructNodes_.clear(); emit structureLoaded(QString(), {}); // 暂无项目 → 空树 emitBusyIfChanged(); return; } NavRequest* st = repo_.loadStructureAsync(currentProjectId_); startStepReq_ = st; emitBusyIfChanged(); QObject::connect(st, &NavRequest::done, this, [this, st](const QVariant& sv) { if (st != startStepReq_) return; startStepReq_.clear(); const auto nodes = qvariant_cast>(sv); lastStructNodes_ = nodes; emit structureLoaded(QString::fromStdString(currentProjectName_), nodes); emitBusyIfChanged(); }); QObject::connect(st, &NavRequest::failed, this, [this, st](const QString& msg) { if (st != startStepReq_) return; startStepReq_.clear(); emit loadFailed(QStringLiteral("structure"), msg); emitBusyIfChanged(); }); }); QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) { if (req != startStepReq_) return; startStepReq_.clear(); emit loadFailed(QStringLiteral("projects"), msg); emitBusyIfChanged(); }); } // ── switchProject:单请求 loadStructure ── void WorkbenchNavController::switchProject(const QString& projectId) { if (projectId.isEmpty()) return; currentProjectId_ = projectId.toStdString(); for (const auto& p : lastProjects_) if (p.id == currentProjectId_) { currentProjectName_ = p.name; currentCrsCode_ = p.crsCode; } if (structReq_) structReq_->abort(); // abort-and-replace NavRequest* req = repo_.loadStructureAsync(currentProjectId_); structReq_ = req; emitBusyIfChanged(); QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) { if (req != structReq_) return; // §5.0 身份比对 structReq_.clear(); const auto nodes = qvariant_cast>(v); lastStructNodes_ = nodes; resetSelectionState(); emit structureLoaded(QString::fromStdString(currentProjectName_), nodes); emitBusyIfChanged(); }); QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) { if (req != structReq_) return; structReq_.clear(); emit loadFailed(QStringLiteral("structure"), msg); emitBusyIfChanged(); }); } // ── selectObject:三并发(data 行 / file 行 / 对象详情),各自身份比对、独立 emit ── void WorkbenchNavController::selectObject(const QString& objectId, int confType) { if (objectId.isEmpty()) return; if (selDataReq_) selDataReq_->abort(); // abort-and-replace 三路 if (selFileReq_) selFileReq_->abort(); if (selDetailReq_) selDetailReq_->abort(); currentParentId_ = objectId.toStdString(); currentParentConfType_ = confType; const std::string pid = currentProjectId_; dataPageNo_ = 1; filePageNo_ = 1; allDataRows_.clear(); dataRootsShown_ = 0; // 数据页:一次取全(大 pageSize),再按根客户端分页——保证树完整(子节点不会跨服务端分页丢失父)。 NavRequest* dReq = repo_.loadRowsAsync(pid, currentParentId_, confType, 3, 1, kFetchAllPageSize); selDataReq_ = dReq; NavRequest* fReq = repo_.loadRowsAsync(pid, currentParentId_, confType, 1, filePageNo_); selFileReq_ = fReq; NavRequest* detReq = repo_.loadObjectDetailAsync(currentParentId_, confType); selDetailReq_ = detReq; emitBusyIfChanged(); QObject::connect(dReq, &NavRequest::done, this, [this, dReq, objectId](const QVariant& v) { if (dReq != selDataReq_) return; selDataReq_.clear(); const auto page = qvariant_cast(v); if (static_cast(page.rows.size()) < page.total) qWarning() << "[nav] data/page 未取全:listCount=" << page.rows.size() << " total=" << page.total << " → 树可能不完整(pageSize 不足)"; allDataRows_ = page.rows; // 全量缓存;按根分页由 emitNextDataRootPage 切 dataRootsShown_ = 0; emitNextDataRootPage(false); // 首页(append=false) emitBusyIfChanged(); }); QObject::connect(dReq, &NavRequest::failed, this, [this, dReq](const QString& msg) { if (dReq != selDataReq_) return; selDataReq_.clear(); emit loadFailed(QStringLiteral("datasets"), msg); emitBusyIfChanged(); }); QObject::connect(fReq, &NavRequest::done, this, [this, fReq, objectId](const QVariant& v) { if (fReq != selFileReq_) return; selFileReq_.clear(); const auto page = qvariant_cast(v); fileTotal_ = page.total; emit filesLoaded(objectId, page.rows, page.total, false); emitBusyIfChanged(); }); QObject::connect(fReq, &NavRequest::failed, this, [this, fReq](const QString& msg) { if (fReq != selFileReq_) return; selFileReq_.clear(); emit loadFailed(QStringLiteral("files"), msg); emitBusyIfChanged(); }); QObject::connect(detReq, &NavRequest::done, this, [this, detReq, objectId](const QVariant& v) { if (detReq != selDetailReq_) return; selDetailReq_.clear(); emit objectDetailLoaded(objectId, qvariant_cast(v)); emitBusyIfChanged(); }); QObject::connect(detReq, &NavRequest::failed, this, [this, detReq](const QString& msg) { if (detReq != selDetailReq_) return; selDetailReq_.clear(); emit loadFailed(QStringLiteral("objectDetail"), msg); emitBusyIfChanged(); }); } // ── 数据页树形分页:从 allDataRows_ 按根切下一页(同步,无请求)── // 根 = parentId 为空或不在本 TM 全量集合内(其父是源文件节点,不在 data/page 返回里)。 // 每页取 kDataRootPageSize 个根 + 各自整棵子树;行序保持后端原序(便于稳定显示)。 void WorkbenchNavController::emitNextDataRootPage(bool append) { const QString parent = QString::fromStdString(currentParentId_); // 本 TM 全部行 id 集合(判定谁是根)。 std::unordered_set ids; ids.reserve(allDataRows_.size()); for (const auto& r : allDataRows_) ids.insert(r.id); // 根索引(按原序)+ parentId→子索引表。 std::vector rootIdx; std::unordered_map> kids; for (std::size_t i = 0; i < allDataRows_.size(); ++i) { const std::string& p = allDataRows_[i].parentId; if (p.empty() || ids.find(p) == ids.end()) rootIdx.push_back(i); else kids[p].push_back(i); } const int rootCount = static_cast(rootIdx.size()); dataTotal_ = rootCount; // 取本页根 [shown, end) 的整棵子树(DFS 收集索引),再按原序输出保稳定。 const int end = std::min(dataRootsShown_ + kDataRootPageSize, rootCount); std::unordered_set picked; for (int k = dataRootsShown_; k < end; ++k) { std::vector stack{rootIdx[k]}; while (!stack.empty()) { const std::size_t cur = stack.back(); stack.pop_back(); if (!picked.insert(cur).second) continue; auto it = kids.find(allDataRows_[cur].id); if (it != kids.end()) for (std::size_t c : it->second) stack.push_back(c); } } std::vector out; out.reserve(picked.size()); for (std::size_t i = 0; i < allDataRows_.size(); ++i) if (picked.count(i)) out.push_back(allDataRows_[i]); dataRootsShown_ = end; emit datasetsLoaded(parent, out, rootCount, append); } // ── loadMoreData:数据页树形——同步切下一页根(无请求)。loadMoreFiles:文件页服务端分页 ── void WorkbenchNavController::loadMoreData() { if (currentParentId_.empty()) return; if (dataRootsShown_ >= dataTotal_) return; // 无更多根 emitNextDataRootPage(true); } void WorkbenchNavController::loadMoreFiles() { if (currentParentId_.empty()) return; if (moreFilesReq_) moreFilesReq_->abort(); NavRequest* req = repo_.loadRowsAsync(currentProjectId_, currentParentId_, currentParentConfType_, 1, ++filePageNo_); moreFilesReq_ = req; const QString parent = QString::fromStdString(currentParentId_); emitBusyIfChanged(); QObject::connect(req, &NavRequest::done, this, [this, req, parent](const QVariant& v) { if (req != moreFilesReq_) return; moreFilesReq_.clear(); const auto page = qvariant_cast(v); fileTotal_ = page.total; emit filesLoaded(parent, page.rows, page.total, true); emitBusyIfChanged(); }); QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) { if (req != moreFilesReq_) return; moreFilesReq_.clear(); --filePageNo_; // 回滚:本请求失败,页号复位,避免下次 loadMoreFiles 跳页 emit loadFailed(QStringLiteral("files"), msg); emitBusyIfChanged(); }); } // ── selectDataset:单请求 ── void WorkbenchNavController::selectDataset(const QString& dsObjectId) { if (dsObjectId.isEmpty()) return; if (datasetReq_) datasetReq_->abort(); NavRequest* req = repo_.loadDatasetFormAsync(dsObjectId.toStdString()); datasetReq_ = req; emitBusyIfChanged(); QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) { if (req != datasetReq_) return; datasetReq_.clear(); emit datasetDetailLoaded(qvariant_cast(v)); emitBusyIfChanged(); }); QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) { if (req != datasetReq_) return; datasetReq_.clear(); emit loadFailed(QStringLiteral("datasetDetail"), msg); emitBusyIfChanged(); }); } // ── setCheckedTms:未命中缓存项并发拉取,全到齐后组装;新勾选 abort 旧批(以最后一次为准)── void WorkbenchNavController::setCheckedTms(const QStringList& tmObjectIds) { for (const auto& h : checkedInflight_) // abort-and-replace 旧批 if (h) h->abort(); checkedInflight_.clear(); // 入口去重:保留首次出现顺序,剔除重复 id(避免重复请求 + 异常树重复 group)。 QStringList deduped; QSet seen; for (const QString& tmQ : tmObjectIds) { if (seen.contains(tmQ)) continue; seen.insert(tmQ); deduped.push_back(tmQ); } QStringList missing; for (const QString& tmQ : deduped) { const std::string tm = tmQ.toStdString(); if (tmExceptionCache_.find(tm) == tmExceptionCache_.end()) missing.push_back(tmQ); } if (missing.isEmpty()) { // 全命中缓存:同步组装,busyChanged 不抖动 assembleAndEmitExceptionTree(deduped); emitBusyIfChanged(); return; } // 并发拉取未命中项;每个 done 写缓存、清自身槽,全到齐后组装。计数用 shared 计数器。 auto remaining = std::make_shared(missing.size()); auto failedFlag = std::make_shared(false); for (const QString& tmQ : missing) { const std::string tm = tmQ.toStdString(); NavRequest* req = repo_.loadExceptionsByTmAsync(tm); checkedInflight_.push_back(req); QObject::connect(req, &NavRequest::done, this, [this, req, tm, remaining, failedFlag, deduped](const QVariant& v) { // 身份比对:req 仍在当前批中才接受(旧批已 abort + 从 vector 移除)。 bool inCurrent = false; for (const auto& h : checkedInflight_) if (h == req) inCurrent = true; if (!inCurrent) return; tmExceptionCache_[tm] = qvariant_cast>(v); if (--(*remaining) == 0 && !*failedFlag) { checkedInflight_.clear(); assembleAndEmitExceptionTree(deduped); emitBusyIfChanged(); } }); QObject::connect(req, &NavRequest::failed, this, [this, req, remaining, failedFlag](const QString& msg) { bool inCurrent = false; for (const auto& h : checkedInflight_) if (h == req) inCurrent = true; if (!inCurrent) return; if (*failedFlag) return; // 仅首个失败发一次 *failedFlag = true; for (const auto& h : checkedInflight_) // abort 其余在飞 if (h && h != req) h->abort(); checkedInflight_.clear(); emit loadFailed(QStringLiteral("exceptions"), msg); emitBusyIfChanged(); }); } emitBusyIfChanged(); } void WorkbenchNavController::assembleAndEmitExceptionTree(const QStringList& tmObjectIds) { auto nameOf = [this](const std::string& id) -> std::string { for (const auto& n : lastStructNodes_) if (n.id == id) return n.name; return id; }; std::vector groups; int total = 0; for (const QString& tmQ : tmObjectIds) { const std::string tm = tmQ.toStdString(); auto it = tmExceptionCache_.find(tm); if (it == tmExceptionCache_.end()) continue; // 防御:理论上此时全命中 auto grouped = data::dto::groupExceptionsByConsortium(it->second); data::ObjectExceptionGroup g; g.objectId = tm; g.objectName = nameOf(tm); g.consortia = std::move(grouped.consortia); g.looseExceptions = std::move(grouped.loose); total += static_cast(it->second.size()); groups.push_back(std::move(g)); } emit exceptionTreeLoaded(groups, total); } } // namespace geopro::controller