docs(spec): 对齐实现(buildStructTree/StructTreeNode + structureLoaded 扁平节点 + 防重入/URL编码)

This commit is contained in:
gaozheng 2026-06-09 12:17:11 +08:00
parent 601706d120
commit 1f1cf5cd3c
1 changed files with 25 additions and 14 deletions

View File

@ -140,17 +140,20 @@ public:
**`api/ApiProjectRepository.{hpp,cpp}`** — 实现:持有 `net::ApiClient&` **`api/ApiProjectRepository.{hpp,cpp}`** — 实现:持有 `net::ApiClient&`
按 §3 路径发请求,把 `ApiResponse` 交给 `dto/` 映射;网络/业务码错误 → `RepoResult{ok=false, error=msg}` 按 §3 路径发请求,把 `ApiResponse` 交给 `dto/` 映射;网络/业务码错误 → `RepoResult{ok=false, error=msg}`
判定成功:`httpStatus==200 && code==<成功码>`(成功码沿用登录约定,实现时核对)。 判定成功:`code==200`(沿用登录 `AuthService` 的约定业务码即成功标志。id 进 URL 路径/查询前
`QUrl::toPercentEncoding` 百分号编码(不可信后端数据:防 `? # & /` 空格 破坏 URL
**`dto/NavDto.{hpp,cpp}`** — 纯函数映射(**无网络、可单测** **`dto/NavDto.{hpp,cpp}`** — 纯函数映射(**无网络、可单测**
- `parseWorkspaces(QJsonArray) -> vector<Workspace>``isCurTenant==1 → isCurrent`)。 - `parseWorkspaces(QJsonArray) -> vector<Workspace>``isCurTenant==1 → isCurrent`)。
- `parseProjects(QJsonObject) -> {vector<ProjectSummary>, bool hasNextPage}` - `parseProjects(QJsonObject) -> {vector<ProjectSummary>, bool hasNextPage}`
- `parseStructNodes(QJsonArray) -> vector<StructNode>` - `parseStructNodes(QJsonArray) -> vector<StructNode>`
- `parseDatasets(QJsonArray) -> vector<DsNode>``ddCode→ddType`)。 - `parseDatasets(QJsonArray) -> vector<DsNode>``ddCode→ddType`)。
- `buildProjectTree(vector<StructNode>, projectName) -> Project`:扁平→树。 - `buildStructTree(vector<StructNode>) -> vector<StructTreeNode>`:扁平→**通用树**(不强塞 `Project/Gs/Tm` 刚性模型,
- 以 `parentId` 归并;`parentId` 为空或不在集合内的节点挂到合成"项目根"。 以适配任意层级 + TM 直挂项目)。`StructTreeNode{StructNode node; bool isTm; vector<StructTreeNode> children}`。
- **叶子节点判定为 TM**(进 `TmNode`,携带 `confCode`/真实 id 作 tmObjectId非叶子为 GS。 - 以 `parentId` 归并;`parentId` 为空或不在集合内(孤儿)的节点为根层。
- TM 的 `dss` 本轮留空DS 懒加载)。 - **叶子节点判定为 TM**`isTm=true``node.id` 即 tmObjectId非叶子为 GS。
- `visited` 集防环:不可信后端数据(多节点环 / 重复 id也不会无限递归规约永不信任外部数据
- 纯函数、可单测树→QTreeWidget 的填充由 `ObjectTreePanel` 调用本函数完成(见 §5.5)。
### 5.4 逻辑层 `controller/WorkbenchNavController`QObject ### 5.4 逻辑层 `controller/WorkbenchNavController`QObject
唯一持有导航状态;不碰 widget经信号把模型推给 UI、经槽接收用户意图。 唯一持有导航状态;不碰 widget经信号把模型推给 UI、经槽接收用户意图。
@ -168,17 +171,23 @@ public slots:
signals: signals:
void workspacesLoaded(const std::vector<data::Workspace>&, QString currentId); void workspacesLoaded(const std::vector<data::Workspace>&, QString currentId);
void projectsLoaded(const std::vector<data::ProjectSummary>&, QString currentId); void projectsLoaded(const std::vector<data::ProjectSummary>&, QString currentId);
void structureLoaded(const data::Project&); // 已建好的树 // 发出项目名 + 扁平结构节点建树buildStructTree在 ObjectTreePanel 内完成。
void structureLoaded(const QString& projectName, const std::vector<data::StructNode>&);
void datasetsLoaded(const QString& tmObjectId, const std::vector<data::DsNode>&); void datasetsLoaded(const QString& tmObjectId, const std::vector<data::DsNode>&);
void loadFailed(const QString& stage, const QString& message); // 出错→UI 空/错状态 void loadFailed(const QString& stage, const QString& message); // 出错→UI 空/错状态
void busyChanged(bool busy); // 同步阻塞期间置 WaitCursor void busyChanged(bool busy); // 同步阻塞期间置 WaitCursor
private: private:
void loadProjectsAndStructure(); // start + switchWorkspace 共用
data::IProjectRepository& repo_; data::IProjectRepository& repo_;
QString currentWorkspaceId_, currentProjectId_, currentCrsCode_; std::vector<data::ProjectSummary> lastProjects_; // 供 switchProject 查 name/crsCode
std::string currentWorkspaceId_, currentProjectId_, currentProjectName_, currentCrsCode_;
bool busy_ = false; // 重入保护:同步请求期间拒绝再次进入
}; };
``` ```
编排逻辑:`start()` → `listWorkspaces`(选 isCurrent/首个)→ `listProjects`(选首个)→ `loadStructure`建树 编排逻辑:`start()` → `listWorkspaces`(选 isCurrent/首个)→ `listProjects`(选首个)→ `loadStructure`发扁平节点
切空间/项目按 §6 时序。每个阶段失败 emit `loadFailed(stage,msg)` 并停在该阶段。 切空间/项目按 §6 时序。每个阶段失败 emit `loadFailed(stage,msg)` 并停在该阶段。
**重入保护**:每个公共操作入口 `if (busy_) return;`,并用 RAII guard 在置忙/复位时配平 `busyChanged`
(同步 HTTP 会泵 Qt 事件循环,快速二次点击可能重入并污染状态)。
### 5.5 UI 层 `app`(被动视图,数据驱动) ### 5.5 UI 层 `app`(被动视图,数据驱动)
@ -188,9 +197,10 @@ private:
- 移除硬编码"个人工作空间 / 青海湖项目";用户区暂留静态。 - 移除硬编码"个人工作空间 / 青海湖项目";用户区暂留静态。
- `buildMenuBar` 不变(静态菜单本轮不接)。 - `buildMenuBar` 不变(静态菜单本轮不接)。
**`app/panels/ObjectTreePanel`**(新增;或先以构建函数落在 main二选一见 §11—— 被动: **`app/panels/ObjectTreePanel`**(新增)—— 被动:`setStructure(projectName, vector<StructNode>)` 内部调
`setProject(const data::Project&)` 重建 `QTreeWidget`项目根→GS→TMTM 可勾选、存 tmObjectId `dto::buildStructTree` 重建 `QTreeWidget`项目根→GS→TM叶子=TM 可勾选、`UserRole` 存 tmObjectId
信号 `tmClicked(QString tmObjectId)` / `tmCheckToggled(...)`。空/错状态:树区显示占位 label。 `showMessage(msg)` 显示空/错占位。信号 `tmClicked(QString tmObjectId)` / `tmCheckToggled(...)`
(后者为前瞻钩子,本轮无消费者)。
**`app/panels/DatasetListPanel`**(已有)—— `datasetsLoaded``populateDatasetList`;空时显示"暂无数据集"。 **`app/panels/DatasetListPanel`**(已有)—— `datasetsLoaded``populateDatasetList`;空时显示"暂无数据集"。
@ -205,7 +215,7 @@ private:
controller.start(): controller.start():
listWorkspaces → emit workspacesLoaded → TopBar.setWorkspaces listWorkspaces → emit workspacesLoaded → TopBar.setWorkspaces
listProjects(empty) → emit projectsLoaded → TopBar.setProjects listProjects(empty) → emit projectsLoaded → TopBar.setProjects
loadStructure(currentProject) → buildProjectTree → emit structureLoaded → ObjectTreePanel.setProject loadStructure(currentProject) → emit structureLoaded(name,nodes) → ObjectTreePanel.setStructure(→buildStructTree)
切空间: TopBar.workspaceSwitchRequested(id) 切空间: TopBar.workspaceSwitchRequested(id)
→ controller.switchWorkspace: switchWorkspace(id) → listProjects → 选首个 → loadStructure → controller.switchWorkspace: switchWorkspace(id) → listProjects → 选首个 → loadStructure
@ -225,7 +235,8 @@ private:
- controller 任一阶段失败 → `loadFailed(stage, msg)`UI 在对应面板显示空/错状态 label + 状态栏提示,**不回退本地样本**。 - controller 任一阶段失败 → `loadFailed(stage, msg)`UI 在对应面板显示空/错状态 label + 状态栏提示,**不回退本地样本**。
- 空数据(无空间 / 无项目 / 无结构 / 无 DS→ 各面板显示"暂无…"占位(识别优于回忆)。 - 空数据(无空间 / 无项目 / 无结构 / 无 DS→ 各面板显示"暂无…"占位(识别优于回忆)。
- token 过期(业务码 401 类)→ `loadFailed` 文案提示重新登录(本轮先提示,自动跳登录留后续)。 - token 过期(业务码 401 类)→ `loadFailed` 文案提示重新登录(本轮先提示,自动跳登录留后续)。
- 输入边界:`tmObjectId` / `projectId` 为空时短路不发请求。 - 输入边界:`tmObjectId` / `projectId` 为空时短路不发请求URL 中的 id 一律百分号编码(见 §5.3)。
- 重入:同步请求期间 `busy_` 拒绝再次进入(避免快速点击重入污染状态,见 §5.4)。
## 8. 渲染解耦 ## 8. 渲染解耦
@ -274,7 +285,7 @@ void rebuildCentralScene(geopro::render::Scene& scene, vtkRenderer* renderer,
依既有无测试桩 + 依赖 live 服务器的现实,聚焦**纯逻辑单测**GoogleTest + CTest 依既有无测试桩 + 依赖 live 服务器的现实,聚焦**纯逻辑单测**GoogleTest + CTest
- `dto/NavDto` 映射:喂样本 JSON取自 OpenAPI example / 手造)验证 - `dto/NavDto` 映射:喂样本 JSON取自 OpenAPI example / 手造)验证
`parseWorkspaces / parseProjects / parseStructNodes / parseDatasets` 字段与 `ddCode→ddType`、`isCurTenant→isCurrent`。 `parseWorkspaces / parseProjects / parseStructNodes / parseDatasets` 字段与 `ddCode→ddType`、`isCurTenant→isCurrent`。
- `buildProjectTree` 扁平→树:覆盖 项目根→GS→TM、TM 直挂项目(无 GS、孤儿 parentId、空列表 等场景。 - `buildStructTree` 扁平→树:覆盖 项目根→GS→TM、TM 直挂项目(无 GS、孤儿 parentId、空列表、防环 等场景。
- 不做 live 集成 / E2E无桩、依赖真实后端。控制器/UI 信号联动靠手动联调验证。 - 不做 live 集成 / E2E无桩、依赖真实后端。控制器/UI 信号联动靠手动联调验证。
- 目标纯逻辑文件dto + tree builder覆盖率优先达标UI/网络 IO 不计入。 - 目标纯逻辑文件dto + tree builder覆盖率优先达标UI/网络 IO 不计入。