fix: 代码评审整改(控制器防重入 + URL 百分号编码 + 测试/注释完善)

This commit is contained in:
gaozheng 2026-06-09 12:15:04 +08:00
parent 405fb2ae4f
commit 601706d120
6 changed files with 44 additions and 18 deletions

View File

@ -498,6 +498,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
.arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax) .arg(name).arg(g.nx()).arg(g.ny()).arg(g.vmin).arg(g.vmax)
.arg(anomalies.size())); .arg(anomalies.size()));
}; };
(void)loadDataset; // 暂未触发:保留待下一轮真实 DS 详情渲染复用
// ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)── // ── 单击左下数据列表的采集批次(DS) → 占位(真实剖面/反演渲染下一阶段接 dd 接口)──
QObject::connect(datasetList, &QListWidget::itemClicked, datasetList, QObject::connect(datasetList, &QListWidget::itemClicked, datasetList,

View File

@ -20,6 +20,7 @@ public:
signals: signals:
void tmClicked(const QString& tmObjectId); void tmClicked(const QString& tmObjectId);
// 前瞻钩子:勾选驱动中央渲染留待下一轮接真实 DS本轮暂无消费者
void tmCheckToggled(const QString& tmObjectId, bool checked); void tmCheckToggled(const QString& tmObjectId, bool checked);
private: private:

View File

@ -8,11 +8,27 @@ using data::Workspace;
WorkbenchNavController::WorkbenchNavController(data::IProjectRepository& repo, QObject* parent) WorkbenchNavController::WorkbenchNavController(data::IProjectRepository& repo, QObject* parent)
: QObject(parent), repo_(repo) {} : QObject(parent), repo_(repo) {}
namespace {
// RAII进入公共导航操作时置忙驱动等待光标任何返回路径都复位——保证 busyChanged 配平。
struct BusyGuard {
WorkbenchNavController* self;
bool* busy;
BusyGuard(WorkbenchNavController* s, bool* b) : self(s), busy(b) {
*busy = true;
emit self->busyChanged(true);
}
~BusyGuard() {
*busy = false;
emit self->busyChanged(false);
}
};
} // namespace
void WorkbenchNavController::start() { void WorkbenchNavController::start() {
emit busyChanged(true); if (busy_) return;
BusyGuard guard(this, &busy_);
const auto ws = repo_.listWorkspaces(); const auto ws = repo_.listWorkspaces();
if (!ws.ok) { if (!ws.ok) {
emit busyChanged(false);
emit loadFailed(QStringLiteral("workspaces"), QString::fromStdString(ws.error)); emit loadFailed(QStringLiteral("workspaces"), QString::fromStdString(ws.error));
return; return;
} }
@ -22,9 +38,7 @@ void WorkbenchNavController::start() {
if (cur.isEmpty() && !ws.value.empty()) cur = QString::fromStdString(ws.value.front().id); if (cur.isEmpty() && !ws.value.empty()) cur = QString::fromStdString(ws.value.front().id);
currentWorkspaceId_ = cur.toStdString(); currentWorkspaceId_ = cur.toStdString();
emit workspacesLoaded(ws.value, cur); emit workspacesLoaded(ws.value, cur);
loadProjectsAndStructure(); loadProjectsAndStructure();
emit busyChanged(false);
} }
void WorkbenchNavController::loadProjectsAndStructure() { void WorkbenchNavController::loadProjectsAndStructure() {
@ -61,22 +75,20 @@ void WorkbenchNavController::loadProjectsAndStructure() {
} }
void WorkbenchNavController::switchWorkspace(const QString& tenantId) { void WorkbenchNavController::switchWorkspace(const QString& tenantId) {
if (tenantId.isEmpty()) return; if (tenantId.isEmpty() || busy_) return;
emit busyChanged(true); BusyGuard guard(this, &busy_);
const auto r = repo_.switchWorkspace(tenantId.toStdString()); const auto r = repo_.switchWorkspace(tenantId.toStdString());
if (!r.ok) { if (!r.ok) {
emit busyChanged(false);
emit loadFailed(QStringLiteral("switchWorkspace"), QString::fromStdString(r.error)); emit loadFailed(QStringLiteral("switchWorkspace"), QString::fromStdString(r.error));
return; return;
} }
currentWorkspaceId_ = tenantId.toStdString(); currentWorkspaceId_ = tenantId.toStdString();
loadProjectsAndStructure(); loadProjectsAndStructure();
emit busyChanged(false);
} }
void WorkbenchNavController::switchProject(const QString& projectId) { void WorkbenchNavController::switchProject(const QString& projectId) {
if (projectId.isEmpty()) return; if (projectId.isEmpty() || busy_) return;
emit busyChanged(true); BusyGuard guard(this, &busy_);
currentProjectId_ = projectId.toStdString(); currentProjectId_ = projectId.toStdString();
for (const auto& p : lastProjects_) for (const auto& p : lastProjects_)
if (p.id == currentProjectId_) { if (p.id == currentProjectId_) {
@ -85,19 +97,16 @@ void WorkbenchNavController::switchProject(const QString& projectId) {
} }
const auto st = repo_.loadStructure(currentProjectId_); const auto st = repo_.loadStructure(currentProjectId_);
if (!st.ok) { if (!st.ok) {
emit busyChanged(false);
emit loadFailed(QStringLiteral("structure"), QString::fromStdString(st.error)); emit loadFailed(QStringLiteral("structure"), QString::fromStdString(st.error));
return; return;
} }
emit structureLoaded(QString::fromStdString(currentProjectName_), st.value); emit structureLoaded(QString::fromStdString(currentProjectName_), st.value);
emit busyChanged(false);
} }
void WorkbenchNavController::selectTm(const QString& tmObjectId) { void WorkbenchNavController::selectTm(const QString& tmObjectId) {
if (tmObjectId.isEmpty()) return; if (tmObjectId.isEmpty() || busy_) return;
emit busyChanged(true); BusyGuard guard(this, &busy_);
const auto ds = repo_.loadDatasetsOfTm(tmObjectId.toStdString()); const auto ds = repo_.loadDatasetsOfTm(tmObjectId.toStdString());
emit busyChanged(false);
if (!ds.ok) { if (!ds.ok) {
emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(ds.error)); emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(ds.error));
return; return;

View File

@ -35,6 +35,7 @@ private:
void loadProjectsAndStructure(); // start + switchWorkspace 共用 void loadProjectsAndStructure(); // start + switchWorkspace 共用
data::IProjectRepository& repo_; data::IProjectRepository& repo_;
bool busy_ = false;
std::vector<data::ProjectSummary> lastProjects_; std::vector<data::ProjectSummary> lastProjects_;
std::string currentWorkspaceId_, currentProjectId_, currentProjectName_, currentCrsCode_; std::string currentWorkspaceId_, currentProjectId_, currentProjectName_, currentCrsCode_;
}; };

View File

@ -3,6 +3,7 @@
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QString> #include <QString>
#include <QUrl>
#include "ApiClient.hpp" #include "ApiClient.hpp"
#include "dto/NavDto.hpp" #include "dto/NavDto.hpp"
@ -19,6 +20,11 @@ std::string errorOf(const net::ApiResponse& r, const char* fallback) {
if (!r.rawError.isEmpty()) return r.rawError.toStdString(); if (!r.rawError.isEmpty()) return r.rawError.toStdString();
return fallback; return fallback;
} }
// 后端 id 进 URL 前做百分号编码(不可信外部数据:防 ? # & / 空格 破坏路径/查询)。
QString enc(const std::string& s) {
return QString::fromUtf8(QUrl::toPercentEncoding(QString::fromStdString(s)));
}
} // namespace } // namespace
ApiProjectRepository::ApiProjectRepository(net::ApiClient& api) : api_(api) {} ApiProjectRepository::ApiProjectRepository(net::ApiClient& api) : api_(api) {}
@ -32,7 +38,7 @@ RepoResult<std::vector<Workspace>> ApiProjectRepository::listWorkspaces() {
RepoResult<bool> ApiProjectRepository::switchWorkspace(const std::string& tenantId) { RepoResult<bool> ApiProjectRepository::switchWorkspace(const std::string& tenantId) {
const QString path = const QString path =
QStringLiteral("/business/system/tenant/enterprise/switch/%1").arg(QString::fromStdString(tenantId)); QStringLiteral("/business/system/tenant/enterprise/switch/%1").arg(enc(tenantId));
const net::ApiResponse r = api_.postJson(path, QJsonObject{}); const net::ApiResponse r = api_.postJson(path, QJsonObject{});
if (!ok(r)) return {false, false, errorOf(r, "switchWorkspace failed")}; if (!ok(r)) return {false, false, errorOf(r, "switchWorkspace failed")};
return {true, true, {}}; return {true, true, {}};
@ -41,7 +47,8 @@ RepoResult<bool> ApiProjectRepository::switchWorkspace(const std::string& tenant
RepoResult<std::vector<ProjectSummary>> ApiProjectRepository::listProjects( RepoResult<std::vector<ProjectSummary>> ApiProjectRepository::listProjects(
const std::string& lastProjectId) { const std::string& lastProjectId) {
const QString path = QStringLiteral("/business/project/queryByUser?lastProjectId=%1") const QString path = QStringLiteral("/business/project/queryByUser?lastProjectId=%1")
.arg(QString::fromStdString(lastProjectId)); .arg(enc(lastProjectId));
// 本轮仅取首页hasNextPage 暂不跟进(分页"加载更多"留下一轮)。
const net::ApiResponse r = api_.get(path); const net::ApiResponse r = api_.get(path);
if (!ok(r)) return {false, {}, errorOf(r, "listProjects failed")}; if (!ok(r)) return {false, {}, errorOf(r, "listProjects failed")};
return {true, dto::parseProjects(r.data).projects, {}}; return {true, dto::parseProjects(r.data).projects, {}};
@ -57,7 +64,7 @@ RepoResult<std::vector<StructNode>> ApiProjectRepository::loadStructure(const st
RepoResult<std::vector<DsNode>> ApiProjectRepository::loadDatasetsOfTm(const std::string& tmObjectId) { RepoResult<std::vector<DsNode>> ApiProjectRepository::loadDatasetsOfTm(const std::string& tmObjectId) {
const QString path = QStringLiteral("/business/projectWorkbench/queryDsByTmObjectId/%1") const QString path = QStringLiteral("/business/projectWorkbench/queryDsByTmObjectId/%1")
.arg(QString::fromStdString(tmObjectId)); .arg(enc(tmObjectId));
const net::ApiResponse r = api_.get(path); const net::ApiResponse r = api_.get(path);
if (!ok(r)) return {false, {}, errorOf(r, "loadDatasetsOfTm failed")}; if (!ok(r)) return {false, {}, errorOf(r, "loadDatasetsOfTm failed")};
return {true, dto::parseDatasets(r.data.value(QStringLiteral("value")).toArray()), {}}; return {true, dto::parseDatasets(r.data.value(QStringLiteral("value")).toArray()), {}};

View File

@ -119,3 +119,10 @@ TEST(NavDto, BuildStructTreeHandlesCycleWithoutInfiniteRecursion) {
ASSERT_EQ(roots.size(), 1u); ASSERT_EQ(roots.size(), 1u);
EXPECT_EQ(roots[0].node.id, "R"); EXPECT_EQ(roots[0].node.id, "R");
} }
TEST(NavDto, ParseProjectsEmptyAndMissingListGraceful) {
EXPECT_TRUE(dto::parseProjects(objOf(R"({})")).projects.empty());
EXPECT_FALSE(dto::parseProjects(objOf(R"({"hasNextPage":false})")).hasNextPage);
const auto p = dto::parseProjects(objOf(R"({"projectList":[]})"));
EXPECT_TRUE(p.projects.empty());
}