--- status: accepted --- # 联系人图谱 demo 接真实 API(contact-graph 票 06) ## Context customer-contact-graph effort 票 06([spec](../../.scratch/customer-contact-graph/spec.md) §三/§七修订⑧):原型 demo(`prototype-contact-graph.html`)此前数据内联(mock 9 人 + 内置字典 + auto 边引擎),票 03/04/05 后端已落地(图谱/增强/导入三组端点 + 字典零新端点)。票面四条:①API 接线清单 + 修订⑧回写 ②mock→API 替换 ③导入 UI 三段式 ④验证(node --check + 静态路径对账 + 既有冒烟不回归;图谱交互 Playwright 归票 08)。 修订⑧(spec §七冻结)四条必须先回写 demo 再谈接线:①砍自动生成边引擎 ②公司边派生化(不可删、摘线=降级)③无否决/tombstone 交互 ④level 来源标记在页内编辑-职务变更联动上体现。 ## Considered Options - **数据层形态**:mock/真机双轨开关 vs **单一 fetch 层(选中)**——统一 `API_BASE = URL ?api=` 覆盖、`apiGet/apiPost/apiDownload` 单入口、Result 信封 `{code,success,message,data}` code==0 成功解析收敛在 `apiEnvelope`。双轨会留下两套数据语义(mock 修复不等于真机修复),票 06 的目的就是消灭 mock 死代码。 - **auto 边引擎**:demo 保留模拟 vs **随修订⑧删除(选中)**——`autoParentsOf/rebuildAutoEdges/vetoed tombstone` 全删;`edges` 收敛为纯手动边 `{id:parent+'>'+child}`;公司边由 `derivedCompanyEdges()` 派生渲染(虚线浅蓝、无 ×、不入 save 快照)。保留模拟会让 demo 与 spec D6/D8 冻结口径分叉,验收者看到的规则面板与后端行为不一致。 - **层级一致性清理**:保留 auto 重建 + 清理 vs **`sanitizeManualEdges` 单职责(选中)**——层级变化(拖格子定级/职务变更联动)后只做「非法边清理」(父层级 ≤ 子层级的 committed/pending 边移除并记日志),不再生成任何边;非法边落库由 save 67018 兜底(后端口径:batchEdit 不动边)。 - **自检策略**:起真服 HTTP 冒烟 vs **Playwright route 内存后端桩(选中)**——`pg.route('http://mock-api/**')` 接管全部 /api/**,页址带 `?api=http://mock-api`;桩实现 14 端点含状态变更(graph/save 版本 CAS、quickAdd 追加、batchEdit 四列联动 bump、导入三段式固定预检)。起服冒烟归票 07 E2E,本票自检只验证 demo 自身行为契约;桩把 method 语义钉住(template 必须 GET,否则 405)防止同类回归。 - **二进制端点 method**:apiDownload 一律 POST vs **按后端映射(选中)**——`import/template` 后端是 `@GetMapping`(读操作),`export` 是 `@PostMapping`;apiDownload 增加 method 参数(默认 POST),模板调用显式传 GET。静态对账时发现的原型 bug(实机 405),桩不校验 method 时 Playwright 冒烟暴露不了。 - **页内编辑-职务变更(D1/D2 体现)**:jobFull option value 用类别 code vs **职务 code(选中)**——option value = 职务字典 code(`applyCellEdit` 按 `j.code === v` 反查联动四列 + levelSource=1),提交 `jobTitleCode` 与后端契约一致;排查中发现 value 误写类别 code 导致联动静默失效,静态对账 + 冒烟步骤 8 双重暴露。 - **脱敏回显提交口径**:phone 一律 11 位校验 vs **含 \* 放行(选中)**——`saveListAll` 前端校验对脱敏回显形态(`131****3272`,loadGraph 重拉后的未修改值)放行提交,后端口径「脱敏形态回传=不改,明文=修改」;否则职务变更保存被脱敏回显误拦(冒烟实测暴露)。 ## Decision 1. demo 数据层收敛为单一 fetch 层:`API_BASE` 从 URL `?api=` 读(默认同源),`loadDicts`(job_title 两级树 + contact_source 启用项)/`loadGraph`(graph/detail 全量 → contacts/edges/graphVersion)/`applyGraph` 落地;JacksonConfig 全局 Long→String,demo 侧 id 统一 `String()` 防精度丢失。初始化 `async init()`:字典+图谱加载失败 toast 提示 `?api=` 用法。 2. 修订⑧回写:auto 边引擎/否决交互全删;公司边派生视图渲染(`edge-company` class 虚线浅蓝、无 ×,`.edge-del` 仅渲染在手动边上);撤级检查豁免公司边(`childEdges` 过滤 `child !== COMPANY_ID`);`tryConnect` 无向 pair 查重覆盖 committed+pending;删手动边直接进 pendingDeletes(无确认弹窗);`saveGraph` 全量快照 POST(nodes 全员等级终态 + 手动边终态集),67017 冲突提示并 `loadGraph` 重拉重置基准。 3. 导入三段式 modal(impStep1 模板+上传 / impStep2 预检表 / impStep3 结果轮询+失败明细),六端点中接线五端点(template/upload/confirm/result/failures;page 为列表侧分页不在 demo 范围);轮询 1s,status=2/3 终态停表并 `loadGraph().then(renderAll)`(导入 bump 后版本重拉)。 4. 自检(`selfcheck.py`)改造:内存后端桩接管 14 端点 + 修订⑧断言适配(边计数口径 = 1 manual + 3 derived company 与原 4 auto 边数值巧合保持;收尾A 改为「无幽灵边自由降级」断言;步骤 8 断言 4 = 后端不动边拉回非法边;步骤 9 重写为公司边无 × + 手动边删免确认 + save 版本推进;新增步骤 11 导入三段式冒烟)——59 步全绿。 ## Consequences - demo 与后端单一事实源对齐:字典/节点/边/版本全部来自 API,mock 配置死代码清零;后续 UI 演进(票 08 Playwright 图谱交互)在同一 fetch 层上做,无需再碰数据层。 - 桩与真机的剩余差异(登录态 cookie、数据权限、真实 xlsx 解析)由票 07 E2E 起服验证承接:三段式 HTTP 冒烟 + 票 02 enabled-list 断言 + 图谱 save CAS。 - 静态路径对账结论:demo 14 端点 ↔ 后端四 Controller(Graph/Enhance/Contact/ContactImport)+ DictItem 映射全对齐,无幽灵路径;`import/page` demo 未接(列表分页后续票)。 - 三个原型缺陷在冒烟/对账中暴露并修复(jobFull value 错绑类别 code、脱敏回显误拦保存、模板下载 method 405)——「桩把后端契约钉住」的自检形态在后续原型票复用。