docs(spec): 对齐实现(buildStructTree/StructTreeNode + structureLoaded 扁平节点 + 防重入/URL编码)
This commit is contained in:
parent
601706d120
commit
1f1cf5cd3c
|
|
@ -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→TM,TM 可勾选、存 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 不计入。
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue