05-mock-domain-adapter.md 8.9 KB

05: Mock 领域适配层

What to build: 建立驾驶舱领域适配层,让 mock 数据与真实接口使用同一契约。页面组件只消费领域对象,不直接依赖后端字段。

Blocked by: 04

Status: resolved Labels: ready-for-agent

  • 存在可切换的 mock 数据源;默认 mock,真实接口由部署配置显式开启。
  • 真实模式下适配层统一附加部署层注入的 Authorization 请求头。
  • 接口字段到领域对象的转换集中在适配层。
  • 页面和 store 不直接处理后端裸字段。
  • mock 数据覆盖成功、空数据、字段缺失、失败和过期样例。
  • 401 或凭据类失败只把受影响分区标为数据不可用,保留已成功分区与地图骨架。
  • 有适配器转换单测。

进度:100%

已完成并关闭(2026-09-18)。 逻辑闸门 178/178、真机走查 10/10,维护者已开启服务并要求跑测(当时撞上沙箱 spawn EPERM,经批准后放行执行)。

交付边界(与维护者确认,2026-09-18)

Q1 裁定 (B) 接缝 + 共享详情域:票 05 交出「领域词汇 + 转换机制 + common 详情域适配器」,16 个面板级聚合留给 17–20(它们各自的验收原句就写着「有面板数据适配和联动单测」)。 其余裁定:Q2 分区状态词汇照搬后端四态 + 前端特有 unavailable;Q3 mock 照抄后端真实字段名;Q4 做「纯逻辑 + 最小可演示」;Q5 不新开 ADR。

改动

领域词汇与工具(domain/,纯函数、可脱浏览器断言,不 import service)

  • sectionState.js:分区状态词汇。后端 CockpitSectionState 四态(SUCCESS/EMPTY/STALE/FAILED)1:1 映射,另加前端特有 UNAVAILABLE(凭据/链路类失败,对应票 04 的 isCredentialFailure() 与统一文案)。STALE(有值但过期,retained:true)与 EMPTY(确实没有值)严格分开 —— 这是票 16 两条验收原句的落点。sectionFromResult() 是唯一收口,sectionView() 给色键与文案,overallState() 让 partial 可与全坏区分。
  • requestReason.js:REQUEST_REASON 从 services/cockpitApi.js 提到 domain。原因:sectionState 要按失败原因判「数据不可用」还是「加载失败」,而 domain 反向 import service 会层次倒挂。cockpitApi.js 仍转出该常量,票 04 的脚本与调用点零改动。
  • fields.js:字段读取器。核心是区分「本该有却没有」(记 missing,界面显示占位符)与「本来就允许为空」(静默 null);缺失一律回 null,绝不用 0 冒充,而后端真给的 0 是有效数据。
  • mapSummary.js(票 07/08 的输入)、pointDetail.js(票 09/10)、equipmentDetail.js(票 11)、workOrder.js(票 11/12)、events.js(票 13/14/19)、freshness.js(票 16):逐字段转换,字段名以 E:\pipe-ner-server\docs\cockpit\*.html 与 research/panel-field-coverage.md 为准。

服务层(ADR 0005 的领域拆分)

  • domainRequest.js:三个领域模块共用的「拼查询串 → 走票 04 的 requestCockpitJson → 收成一个分区」。
  • mapApi.js / detailApi.js / runtimeApi.js:领域接口。每个分区各自成败,调用方拿到的是一组并列 section() 而不是共用成败的详情对象。
  • cockpitApi.js(改):新增 findMockTransport() 通配匹配,让带路径参数的接口(/equipments/*/detail-aggregate)也能 mock;mock 处理器可在异常时声明失败原因,但只认白名单取值。
  • mocks/contractMocks.js + mocks/index.js:契约化 mock。字段名与嵌套形状照抄后端演示载荷,值用边界值构造(坐标 VALID/UNVERIFIED/INVALID/ZERO、指标真 0、未结案 null)。场景开关走宿主全局键,不改代码不重新构建即可演示五类样例与 401。

最小可演示(Q4=B)

  • components/SectionStateTag.vue + stores/detailStore.js(ADR 0004 的 detailStore)+ CockpitShell.vue 筛选条右侧的分区状态板:三条共享分区各带 data-section-state / data-section-name / data-section-tone / data-section-retained,走查可直接断言。只有 mock 模式才自动取数:real 模式后端未就位,自动取数只会刷 404(沿用票 04「没有显式配置就不发请求」的立场)。

验证证据

  • 纯逻辑:node .scratch/cockpit-shell-panel-layout/verify/t05-adapter.mjs → 178/178 通过。四段:A 分区状态(四态映射、401 归类、过期 vs 空、部分可用、视图映射、界面文案零原因码泄漏);B 领域转换(用后端演示载荷的真实字段名喂入,含 0 值/缺失/null-time/超范围坐标/七态/回退动作/阈值只在报警/未知配色);C 契约化 mock(五类样例逐一取数、401 与前缀隔离、real 分支同一适配层且带凭据、假 401、坏载荷、查询串、缺编码不发请求、WS 增量帧两个字段名、注册幂等);D 静态守卫(10 个页面/store 文件零后端裸字段、零凭据、零登录跳转;8 个领域模块不自己发请求、不 import service)。
  • 真机走查:node .scratch/cockpit-shell-panel-layout/verify/t05-adapter-walkthrough.mjs → 10/10 通过(yarn dev @ localhost:8000,维护者开启并要求跑测)。逐条:默认三条分区 success+lv4、引擎 ready、宿主唯一、四面板齐全、零页面级报错;空数据样例显示「无数据」中性色且不误报失败;过期样例 stale+lv2+retained=true 且文案含「已过期」;失败样例三分区 failed 而外壳/四面板/地图骨架全保留;401 只标受影响分区(设备详情 unavailable+lv1+「数据不可用」+「凭据未通过校验(由部署层注入)」,地图摘要与新鲜度仍 success)、地图骨架未被牵连、不跳登录页、零页面级报错。截图归档在 verify/walkthrough/t05-sections-{success,401}.png(走查产物,gitignore)。
  • 回归:t01-panel-catalog / t03-camera-contract / t04-config / t06-assets / t21-terrain / t29-basemap 全部通过(exit 0)(票 04 的凭据守卫与既有断言未受影响)。
  • 布局:分区板接入后实测筛选条仍为 34px 行高、scrollWidth === clientWidth(无横向溢出)、文档无横向滚动。
  • 编译:@vue/compiler-sfc 对 CockpitShell.vue 与 SectionStateTag.vue 的 template 与 <script setup> 均解析通过,绑定无悬空。
  • 单测直接证到的关键行为:档案分区失败时指标分区仍是成功(票 11 验收第 3 条);401 只打掉被选中的那一个分区,同一次加载里地图摘要与新鲜度照常成功、overallState 为 partial;过期分区 retained:true 且保留旧值;空分区 data:null;未注册接口回 mock-handler-missing 而不是静默成功。

真机走查抓到的两个真 bug(第一轮 7/10 → 修复后 10/10)

这正是「渲染侧不能只靠纯逻辑」的实证——两条逻辑闸门全绿时它们照样存在:

  1. 分区标签把状态词顶掉了(真 UI bug):SectionStateTag 原先用 labelOverride 让分区名替换状态文案,结果过期分区只显示「显示最后一次成功数据,不是实时值」、不可用分区只显示「凭据未通过校验」——肉眼与断言都读不出「已过期 / 数据不可用」,而成功态与失败态长得几乎一样。修法:分区名与状态词两者都渲染(label + sectionView().label),状态词永不被顶掉。修在组件里,所有分区同享。
  2. 界面漏出内部原因码:失败分区的 detail 直接回传 reason,截图上出现「数据加载失败unavailable」。修法:sectionState.js 新增 reasonDetail() 字典,六种原因各有中文说明、认不出的原因回空串(宁可只说「数据加载失败」,也不把接口代号甩给值班人员),并补 A9-no-reason-code-in-ui 静态断言防回归。

另有一条属走查脚本自身的时序问题(非产品缺陷):A1-map-skeleton-alive 在引擎仍 booting 时就读断言(engine=booting → 误判红)。修法与 t04/t29 一致——先等 dataset.mapEngine 落到终态再断言。

边界与遗留

  • runtimeApi.js 只交 REST 侧与增量帧→领域事件的转换;连接、重连、按 serverUpdatedAt 丢弃旧消息归票 15(其票面原句),本票不持有连接。
  • 事件的地理位置仍是契约缺口(后端只有文本 location,无经纬度):转换层已留 longitude/latitude 位并如实回 null,补契约见票 25。
  • 面板级(16 个二级面板)领域适配按 Q1=B 留给 17–20;本票只交共享详情域。
  • PanelPlaceholder 仍是各面板体内占位 —— 面板内容归 17–20,本票只把共享分区状态接上界面。
  • 本票改动未提交(工作区另有票 03/30 会话的在途改动)。