数据模型关系总图(Entity Relationship Map)
为什么有这份文档:2026-07-02 会议上 Peter 指出的系列问题(电话挪店历史全错、删店重建 lead 绑定全丢、contact 归属层级、tenant transfer 撞约束)有一个共同病根——所有归属关系(phone→store→tenant,以及事件行上的 stamp)都被当成"不会变的事实"写死,但它们全都会变,而系统没有"归属变更后历史数据怎么办"的统一答案。本文把全部表、全部关系、每条关系的变更策略摆在一张桌上,multi-tenant 相关的设计讨论以这里为共同语言。
数据口径:表清单来自 2026-07-02 callytics-test 库 information_schema 实测(22 张)+ control plane retaintive-identity(4 张)+ DDB(7 张)。schema 字段细节以 callytics-common/src/db/schema/ 与 control-plane/schema/tenants.ts 为准,本文不复制 DDL。
一、边的四种类型(先看图例)
二、身份与归属层(identity chain)
三个框的边界:
三、业务事实表:全部 stamp 归属
图中 tenant_id 边列的是 2026-07-01 test 实测已有 tenant_id 的 10 张表。其余 store-scoped 表(同样 stamp store_id,但 V1 不重复存 tenant_id 或暂不在主图展开):staff、blackout_periods、operating_schedules、operating_overrides。
历史审计列(不再是隔离键,只读不写新逻辑):franchise_id、account_id、site_id、store_phone。
⚠️ 代码与库的漂移(2026-07-03 codex review + 代码验证收口):
task_progress_events不是缺 migration——设计上已并入contact_timeline事件(task-progress.ts:"progress is no longer its own table")task_playbooks已从 final schema 移除(ai-output-tables.test.ts明确断言不导出)- 两者在 tasks-schema.md 中仍被描述为 live 表——该文档过时,待修
calendar_events:studio-api(calendar-events-neon.ts)把它当 Neon SoT 直接 INSERT/SELECT,但 test 库 information_schema 里没有这张表——待建或待查(与 backfill 脚本引用已删除表同族:代码期望的表和库不一致)
三.5 全量 schema 精读补充发现(2026-07-02,26 个 schema 文件逐一读)
设计基线:final Neon-only。 DDB 的 7 张表在 Neon 已全部有对应(rc_stores/rc_store_phones/user_connections/store_members/phone_numbers/oauth_states/sub_accounts),schema 层退役已完成;pipeline 的 phone 反查也已切 Neon(phone-identity 0.36.0+,DDB PhoneStoreAssignments 不再被该模块读取),运行时残留主要是 v2 API 读 DDB call-analysis——迁移工作,不影响设计。
精读发现(文件级证据):
- store 身份的 unique key 含 user_id:
rc_stores唯一键 =(user_id, connection_id, rc_site_id)— owner 换人或 OAuth 重连,同一物理店 sync 即新 UUID。删店重建只是断链的一种触发方式,这是更深的根 - 同号可挂两店是 schema 允许的:
rc_store_phones唯一键 =(store_id, phone_number);同号可同时有 A 店rc_sync行 + B 店user_manual行,靠查询侧 "user_manual 优先" 约定兜底(schema 注释自认) - 挪号 = DELETE + INSERT(
assigned_at刷新)— 归属历史物理归零 - lead email 配置双轨:
user_connections.leadEmails(用户级)与store_config.leadEmails(店级)并存 - 成员关系三轨并存:
sub_accounts(父子账号)+store_members(店级 EDITOR/VIEWER)+tenant_members(客户级 4 角色)— D2 的统一设计必须三轨一起收 task_progress_events表不存在:task-progress.ts明确 "progress is no longer its own table",进contact_timeline事件;tasks-schema.md 对此描述已过时(待修)- 好骨头(目标策略的本土先例):
operating_schedules已是effective_date时间轴模式;staff.is_active软删除;task_suggestions/ai_feedbackappend-only + supersede;contact_timeline审计列完备(多态 actor + AI 取证 + 幂等);tasks有 evidence 链(source_entity_*)与完整 CHECK 纪律 — "归属历史表"是把 repo 已有模式推广到 phone→store,不是引入新范式
四、核心:每条"会变的归属关系"的变更策略
这张表是本文档的重点。每一行都是一个已经发生过或必然发生的变更场景:
五、Open decisions(需要 Max 拍板,ER 图定型的前置)
六、冻结令(会议共识,已生效)
Peter(01:53):"我们先把这个事情想明白,再做 analytics。先不操作,越弄越麻烦。"
在第四节 #1/#2/#3 的目标策略定稿前:
- 不做店的删除/重建/电话挪动(有纠错需求先记录,攒批处理)
- 不基于 store_id 口径新增 analytics 功能
- Dashboard 各数字的取数表(contacts vs tasks vs leads)不再逐个打补丁——等 lead↔task 桥接和本图 #1 定稿后统一改
七、目标模型(Final 草案 — 把所有数据连上的那张图)
DRAFT。基于 §五 决策的推荐答案画出(D1 放开多 membership / D2 保留两层 / D3 门牌号只作匹配信号 / D4 person 层上 prod 前 / D5 开评估型口子)——决策拍板后只改标 ⟨D?⟩ 的地方,不重画。表名用 Final 词汇(locations / phone_lines);概念可以先落地在现有表名上(如 phone_line_assignments 直接引用 rc_stores.id),改名是 Final 阶段的机械活。
逐表说明书(图里每个框:干什么、靠什么连、现在有没有)
状态图例:✅ 已有(基本不动)/ 🔨 改造现有表 / 🆕 新建 / 🕐 future(有触发条件才建)。
管理面(谁是客户、谁能进)
身份区(店、电话、邮箱的归属)
顾客层(人,不是号码)
事实层(发生过的事,永不改写)
工作层(派生的待办和 AI 产出)
与现状的差异,就是五个洞的补法(全部有本土先例,见 §三.5 第 7 条):
逻辑走查(2026-07-03,9 条主干流程在目标模型上从头走到尾)
另两个走查时明确"先不做、记下来"的:号码易主/回收(同一号码换了主人)→ V1.5 用现有 needs_review 冲突机制兜,person_channels 不上时间轴;派生表不绑 assignment_id(codex F8,已收窄)。
相关文档
redesign-requirements.md— DB redesign 需求分析(23 场景清单 + 查询清单 + 口径冲突)why-tenant.md— 为什么需要 tenant 层(决策说明)v1-gaps.md— V1 已知缺口与补齐路线prod-rollout-checklist.md— 上 prod 铺开 checklist- 会议原始记录:2026-07-02 sync(1h55m,Lark Minutes
objpkpo636fwztfyrh4v8z4v) - codex 审计 brief:
.claude/specs/2026-07-02-codex-table-relationship-audit.md