geopro/docs/superpowers/specs/2026-06-09-real-api-navigat...

17 KiB
Raw Blame History

接入真实导航(工作空间 / 项目 / 对象树)— 设计文档

  • 日期2026-06-09
  • 分支feat/m1-finishing建议拉子分支 feat/real-api-navigation
  • 状态:已与需求方确认范围,待 spec 评审

1. 背景与目标

当前应用已搭好骨架:登录链路(net::AuthService + net::ApiClient共享会话、token 注入)可用; 工作台(src/app/main.cpp::buildWorkbench)用本地静态样本仓储 data::LocalSampleRepository 渲染三维/二维示例;顶部 app::TopBar 的"工作空间切换 / 项目"为静态视觉壳(硬编码下拉项)。

本轮目标:把顶层导航壳接到真实后端接口,逐步替换静态数据:

  1. 工作空间(=企业租户/空间)列表与切换;
  2. 项目列表与切换;
  3. 对象显示栏的树形结构(项目 → GS → TM+ 选中 TM 后其 DS 列表。

中央三维/二维渲染与"数据详情"的真实剖面/反演数据走另一批 dd/ert 接口,本轮不接

2. 范围(已确认决策)

In Scope

  • 工作空间列表 / 切换(真实接口)。
  • 项目列表 / 切换(真实接口)。
  • 对象树:按真实结构显示 GS 层(项目根 → GS → TMTM 在左下"数据真实显示栏"列出其 DS。
  • 真实接口失败(断网 / token 过期 / 无数据)→ 显示错误 / 空状态不回退本地样本
  • 项目 referenceCRSCode 存入导航状态,供下一轮替换硬编码 EPSG:4547(本轮不改渲染)。

不做Out of Scope留下一轮

  • 中央 2D/3D 视图、数据详情的真实数据渲染(dd/ert/gpr 接口)。 → 点击真实 DS 时中央/详情显示占位"待接入"render/*LocalSampleRepository 代码保留不删
  • 异步仓储QFuture/回调)—— 本轮同步阻塞 + WaitCursor与登录一致异步留 M1.5。
  • 用户头像 / 姓名接真实 auth/getUserInfo(本轮先留静态)。
  • 项目分页"加载更多"的滚动 UI接口支持 hasNextPage,本轮先取首页;翻页留待后续)。

3. 接口确认结论

网关:http://tenant.geomative.cn/pop-api(现有 ApiClient 基址business 与 admin 两份 OpenAPI 同一上游。 token 已由登录注入(geomativeauthorization 头),下列接口直接复用现有会话。

能力 方法 路径 关键返回
工作空间列表 GET /business/system/tenant/enterprise/joined/list [{id, name, ownerType(1个人/2企业), isCurTenant(0/1), logoPath}]
切换工作空间 POST /business/system/tenant/enterprise/switch/{tenantId} 信封 code/msg
项目列表 GET /business/project/queryByUser?lastProjectId= {hasNextPage, projectList:[{id, projectName, projectTypeName, referenceCRSCode, referenceCRSName, status, ...}]}(游标分页,首页传空 lastProjectId
项目结构 POST /business/projectWorkbench/queryProjectStruct body {projectId}data {projectStructList:[{id, name, parentId, type, typeId, typeName, confCode}]}
TM 下 DS GET /business/projectWorkbench/queryDsByTmObjectId/{tmObjectId} [{id, name, ddCode, typeName}]

层级确认(修正需求方假设):真实结构不是 项目→tm→ds,而是 项目 → GS(工区) → TM(测线) → DS

  • queryProjectStruct 返回一个扁平 parent-child 列表(仅含 GS + TM 两类节点,不含 DS),客户端按 parentId 自建树。
  • DS 不在结构列表里,按 TM 单独拉取(queryDsByTmObjectId)。
  • 项目不能直接挂 DSDS 永远挂在 TM 下。但由于是 parentId 扁平结构,TM 可直接挂在项目下(无中间 GS——这是"项目直接挂"印象的来源,但叶子仍是 TM→DS。

节点判定:结构列表只含 GS+TMTM = 该节点在结构列表中无子节点(叶子);非叶子 = GS。 type(integer) / confCode 一并保留为辅助信号,待见到 live 数据后可固化判定规则。

4. 架构分层

遵循仓库既有四层(见各层 README本轮新增组件按层就位依赖方向单向向下UI 不直接碰 ApiClient

┌─────────────────────────────────────────────────────────────┐
│ UI 层  (src/app, 目标 src/view) — 被动视图,只渲染模型 + 发用户意图信号 │
│   TopBar(数据驱动)   ObjectTreePanel   DatasetListPanel(已有)        │
└───────────────▲──────────────────────────┬──────────────────┘
            信号(用户意图)              槽(模型数据)
┌───────────────┴──────────────────────────▼──────────────────┐
│ 逻辑层 (src/controller) — 编排状态机,无 widget                      │
│   WorkbenchNavController : QObject                              │
│     state: 当前 workspaceId / projectId / project.crsCode         │
│     slots: switchWorkspace / switchProject / selectTm            │
│     signals: workspacesLoaded / projectsLoaded /                 │
│              structureLoaded / datasetsLoaded / loadFailed       │
└───────────────────────────┬──────────────────────────────────┘
                       IProjectRepository(同步契约)
┌───────────────────────────▼──────────────────────────────────┐
│ 数据访问层 (src/data)                                            │
│   repo/IProjectRepository.hpp   ← 导航仓储抽象(Result<T>)         │
│   api/ApiProjectRepository.{h,cpp} ← 用 ApiClient 实现            │
│   dto/NavDto.{h,cpp}            ← 后端 JSON → 模型 纯映射 + 扁平→树   │
└───────────────────────────┬──────────────────────────────────┘
                       net::ApiClient(原始 HTTP)
┌───────────────────────────▼──────────────────────────────────┐
│ 接口层 (src/net) — 复用,无改动                                    │
│   ApiClient(共享会话/token) ; AuthService(登录)                   │
└───────────────────────────────────────────────────────────────┘

模型层 (src/data/repo/RepoTypes.hpp & 新增 NavTypes) — 纯结构,被各层共享
   Workspace / ProjectSummary / StructNode / Project,GsNode,TmNode,DsNode(已有)

依赖规则:net 不依赖任何上层;data 依赖 net + 模型;controller 依赖 data + 模型(不依赖 UI app/view 依赖 controller + 模型(不依赖 data/net 具体类型,只经 controller 信号拿模型)。

5. 各层组件详细设计

5.1 接口层 net(复用,不改)

ApiClient::get(path) / postJson(path, body) 返回 ApiResponse{httpStatus, code, data, msg, rawError}。 本轮所有业务接口经它发出token 已注入,会话已共享。

5.2 模型层(纯结构,无 Qt / 无 VTK

src/data/repo/RepoTypes.hpp 已有 Project/GsNode/TmNode/DsNode。新增导航模型(同文件或 NavTypes.hpp

struct Workspace { std::string id, name; int ownerType = 0; bool isCurrent = false; };
struct ProjectSummary {
    std::string id, name, typeName, crsCode, crsName;
    int status = 0;
};
// 项目结构扁平节点GS / TM客户端按 parentId 建树。
struct StructNode {
    std::string id, name, parentId, typeName, confCode;
    int type = 0;
};

DsNode{id,name,ddType} 复用;映射时 ddCode → ddType

5.3 数据访问层 data

repo/IProjectRepository.hpp — 导航仓储抽象(同步,呼应既有 IDatasetRepository 风格; 但网络可失败,故用显式 Result 而非抛异常,便于 UI 出错误/空状态):

template <class T>
struct RepoResult { bool ok = false; T value{}; std::string error; };

class IProjectRepository {
public:
    virtual ~IProjectRepository() = default;
    virtual RepoResult<std::vector<Workspace>>      listWorkspaces() = 0;
    virtual RepoResult<bool>                         switchWorkspace(const std::string& tenantId) = 0;
    virtual RepoResult<std::vector<ProjectSummary>> listProjects(const std::string& lastProjectId) = 0;
    virtual RepoResult<std::vector<StructNode>>      loadStructure(const std::string& projectId) = 0;
    virtual RepoResult<std::vector<DsNode>>          loadDatasetsOfTm(const std::string& tmObjectId) = 0;
};

api/ApiProjectRepository.{hpp,cpp} — 实现:持有 net::ApiClient& 按 §3 路径发请求,把 ApiResponse 交给 dto/ 映射;网络/业务码错误 → RepoResult{ok=false, error=msg}。 判定成功:httpStatus==200 && code==<成功码>(成功码沿用登录约定,实现时核对)。

dto/NavDto.{hpp,cpp} — 纯函数映射(无网络、可单测

  • parseWorkspaces(QJsonArray) -> vector<Workspace>isCurTenant==1 → isCurrent)。
  • parseProjects(QJsonObject) -> {vector<ProjectSummary>, bool hasNextPage}
  • parseStructNodes(QJsonArray) -> vector<StructNode>
  • parseDatasets(QJsonArray) -> vector<DsNode>ddCode→ddType)。
  • buildProjectTree(vector<StructNode>, projectName) -> Project:扁平→树。
    • parentId 归并;parentId 为空或不在集合内的节点挂到合成"项目根"。
    • 叶子节点判定为 TM(进 TmNode,携带 confCode/真实 id 作 tmObjectId非叶子为 GS。
    • TM 的 dss 本轮留空DS 懒加载)。

5.4 逻辑层 controller/WorkbenchNavControllerQObject

唯一持有导航状态;不碰 widget经信号把模型推给 UI、经槽接收用户意图。

class WorkbenchNavController : public QObject {
    Q_OBJECT
public:
    explicit WorkbenchNavController(data::IProjectRepository& repo, QObject* parent=nullptr);
    void start();                                   // 启动:拉空间→项目→结构
public slots:
    void switchWorkspace(const QString& tenantId);  // 切空间→重载项目→重载结构
    void switchProject(const QString& projectId);   // 切项目→重载结构(清 DS/详情)
    void selectTm(const QString& tmObjectId);       // 选 TM→拉其 DS
signals:
    void workspacesLoaded(const std::vector<data::Workspace>&, QString currentId);
    void projectsLoaded(const std::vector<data::ProjectSummary>&, QString currentId);
    void structureLoaded(const data::Project&);     // 已建好的树
    void datasetsLoaded(const QString& tmObjectId, const std::vector<data::DsNode>&);
    void loadFailed(const QString& stage, const QString& message); // 出错→UI 空/错状态
    void busyChanged(bool busy);                    // 同步阻塞期间置 WaitCursor
private:
    data::IProjectRepository& repo_;
    QString currentWorkspaceId_, currentProjectId_, currentCrsCode_;
};

编排逻辑:start()listWorkspaces(选 isCurrent/首个)→ listProjects(选首个)→ loadStructure→建树。 切空间/项目按 §6 时序。每个阶段失败 emit loadFailed(stage,msg) 并停在该阶段。

5.5 UI 层 app(被动视图,数据驱动)

app/TopBar —— 由"自由函数返回静态 QWidget"升级为数据驱动类QWidget 子类):

  • setWorkspaces(list, currentId) / setProjects(list, currentId) 重建下拉项。
  • 信号 workspaceSwitchRequested(QString id) / projectSwitchRequested(QString id)
  • 移除硬编码"个人工作空间 / 青海湖项目";用户区暂留静态。
  • buildMenuBar 不变(静态菜单本轮不接)。

app/panels/ObjectTreePanel(新增;或先以构建函数落在 main二选一见 §11—— 被动: setProject(const data::Project&) 重建 QTreeWidget项目根→GS→TMTM 可勾选、存 tmObjectId 信号 tmClicked(QString tmObjectId) / tmCheckToggled(...)。空/错状态:树区显示占位 label。

app/panels/DatasetListPanel(已有)—— datasetsLoadedpopulateDatasetList;空时显示"暂无数据集"。

中央/详情:移除"启动自动渲染本地 demo"DS 点击 → 详情面板与中央视图显示占位文案 "该数据集渲染将在下一阶段接入 dd 接口"。渲染代码保留。

6. 数据流 / 交互时序

启动(登录后):
  main 构造 ApiClient → ApiProjectRepository → WorkbenchNavController → TopBar/ObjectTreePanel
  controller.start():
    listWorkspaces → emit workspacesLoaded → TopBar.setWorkspaces
    listProjects(empty) → emit projectsLoaded → TopBar.setProjects
    loadStructure(currentProject) → buildProjectTree → emit structureLoaded → ObjectTreePanel.setProject

切空间: TopBar.workspaceSwitchRequested(id)
  → controller.switchWorkspace: switchWorkspace(id) → listProjects → 选首个 → loadStructure
  → emit projectsLoaded + structureLoaded清空 DS 列表/详情占位

切项目: TopBar.projectSwitchRequested(id)
  → controller.switchProject: loadStructure(id) → emit structureLoaded清空 DS 列表/详情占位

选 TM: ObjectTreePanel.tmClicked(tmObjectId)
  → controller.selectTm: loadDatasetsOfTm → emit datasetsLoaded → DatasetListPanel 填充

点 DS: DatasetListPanel → 中央/详情显示占位"待接入"(本轮不渲染真实数据)

7. 错误处理与边界

  • 仓储层捕获网络错误(rawError)与业务错误码,归一为 RepoResult.error
  • controller 任一阶段失败 → loadFailed(stage, msg)UI 在对应面板显示空/错状态 label + 状态栏提示,不回退本地样本
  • 空数据(无空间 / 无项目 / 无结构 / 无 DS→ 各面板显示"暂无…"占位(识别优于回忆)。
  • token 过期(业务码 401 类)→ loadFailed 文案提示重新登录(本轮先提示,自动跳登录留后续)。
  • 输入边界:tmObjectId / projectId 为空时短路不发请求。

8. 渲染解耦

现状:对象树(本地 grid1/grid2…直接驱动中央与数据详情。本轮真实树 id 与本地样本对不上,故:

  • 启动不再自动渲染本地 demo。
  • 真实 DS 点击 → 中央/详情显示占位文案。
  • render/*LocalSampleRepositoryVoxelFromScatters 等全部保留,待下轮按 dd/ert 接口复用。
  • 项目 crsCode 由 controller 存住,下一轮替换 main.cpp 中硬编码 EPSG:4547

9. 测试策略

依既有无测试桩 + 依赖 live 服务器的现实,聚焦纯逻辑单测GoogleTest + CTest

  • dto/NavDto 映射:喂样本 JSON取自 OpenAPI example / 手造)验证 parseWorkspaces / parseProjects / parseStructNodes / parseDatasets 字段与 ddCode→ddTypeisCurTenant→isCurrent
  • buildProjectTree 扁平→树:覆盖 项目根→GS→TM、TM 直挂项目(无 GS、孤儿 parentId、空列表 等场景。
  • 不做 live 集成 / E2E无桩、依赖真实后端。控制器/UI 信号联动靠手动联调验证。
  • 目标纯逻辑文件dto + tree builder覆盖率优先达标UI/网络 IO 不计入。

10. 线程 / 性能

  • 同步阻塞 UI 线程(ApiClient 用 QEventLoop+ busyChangedQt::WaitCursor,与现有登录一致。
  • 切空间/项目可能稍慢但可接受MVP。异步QFuture/取消)留 M1.5,届时 IProjectRepository 契约可平滑改造。

11. 文件清单

新增

  • src/data/repo/IProjectRepository.hpp(含 RepoResult<T>、导航模型 Workspace/ProjectSummary/StructNode,或拆 NavTypes.hpp
  • src/data/api/ApiProjectRepository.{hpp,cpp}
  • src/data/dto/NavDto.{hpp,cpp}
  • src/controller/WorkbenchNavController.{hpp,cpp}
  • src/app/panels/ObjectTreePanel.{hpp,cpp}(若不抽,则树构建函数留在 main但 TopBar 必抽)
  • 测试:tests/(或既有测试目录)NavDtoTest.cppBuildProjectTreeTest.cpp

改造

  • src/app/TopBar.{hpp,cpp} — 升级为数据驱动类 + 信号
  • src/app/main.cpp — 构造 repo/controller、接线信号移除启动自动渲染 demoDS 点击改占位
  • 各层 CMakeLists.txt — 新增源文件 + controller 目标接入构建controller 需 Q_OBJECTAUTOMOC ON

保留不删LocalSampleRepositoryrender/*VoxelFromScatters、现有详情/中央渲染代码。

12. 未决 / 下一轮

  • dd/ert/gpr 真实剖面/反演/雷达数据渲染(替换占位)。
  • 项目 crsCode 替换硬编码 EPSG:4547,重建 GeoLocalFrame
  • 异步仓储QFuture + 取消 + 分页"加载更多")。
  • 用户头像/姓名接 auth/getUserInfotoken 过期自动跳登录。
  • 顶部菜单(视图/项目管理/业务工具/设备)接真实页面。