Contacts Schema

Status boundary:本文的 Contacts inventory 是 2026-06 snapshot;实施前必须重新核查 live schema。原 §3/§4 的 “Final / Delta” 是历史 proposal,尤其 task_progress_events、global closeResult 和旧 Task Decision contract 不再是 Target。

Snapshot source: callytics-common/src/db/schema/contacts.ts (verified 2026-06-01) Task V3 selected Target: Task V3 Product / Domain Contract Current rollout evidence boundary: Task V3 Rollout 状态快照 Historical Task V2+ audit: 工程审计与设计建议 表名: contacts复合主键: (phone, storeId) (State C 终态,callytics-infrastructure#599) 阅读顺序:2026-06 snapshot (§2) → historical proposed target (§3) → historical delta (§4)。

Name trust scoring:firstNameTrustScore 决定多写入方 UPSERT 时谁赢,详见 name-trust.md


1. 表说明

Contacts 是中心化客户档案表,聚合来自 calls / messages / Lead pipeline / AI 分析的数据。一个号码在不同门店下是独立客户记录(跨品牌隔离由 storeId UUID 自动保证)。

  • State C 终态:复合主键 (phone, storeId),franchiseId / accountId 降级为审计字段(superseded by storeId for isolation)
  • 所有枚举字段都是 text 列 + 注释里写约束,无 DB CHECK,无 const 数组 — 跟 tasks 不同(tasks 有 DB CHECK + UI 常量强制)。contacts 枚举值靠写入方(AI prompt + Lambda 代码)自觉遵守注释,数据库不拦非法值
  • 写入者分类:
    • code = 数据管道自动写入(通话管道 / Lead 管道 / 温度衰减规则)
    • ai = AI 分析写入(Per-Call / Daily Batch / On-Demand)
    • ai+code = AI 分析 + 规则映射共同决定
    • ai+staff = AI 可写入,员工可通过 UI 覆盖

2. Current Schema(2026-06-01 live)

2.1 contacts 表全字段(30 字段)

按代码存储顺序排列(PK + 隔离键在前,身份 → 生命周期 → 运营 → 活动 → 行动 → Lead 状态 → 障碍 → 分析 → 画像 → Contact Analysis 追踪 → 风险 → 时间戳)。复杂枚举(firstNameTrustScore / leadStatus)在 §2.2 展开。删除线 + ⚠️ 标记的字段在 Final 退役,详见 §4.1。

#Field (TS)SQL 列TypeNullableEnum / ValuesWriter用途
1phonephonetextNOT NULL, PKcodeE.164 格式电话号码,隔离键之一
2franchiseIdfranchise_idtextNOT NULLcode所属品牌(审计字段,已降级,不再作为隔离键)
3accountIdaccount_idtextNOT NULLcodeRC providerAccountId(审计字段,已降级)
4storeIdstore_idtextNOT NULL, PKcodeDynamoDB StoresV2 store UUID,终态隔离键。由 ai-analysis-processor / message-processor / contacts-analyzer 经 resolvePhoneIdentity() 从 PhoneStoreAssignments 解析后写入
5firstNamefirst_nametextNULLcode
6lastNamelast_nametextNULLcode
7firstNameTrustScorefirst_name_trust_scoresmallintDEFAULT 0(6 值)— 见 §2.2.1code名字来源的信任分。UPSERT 时高分覆盖低分。详见 name-trust.md
8firstNameUpdatedAtfirst_name_updated_attimestamptzNULLcodefirst_name 最后被任何来源更新的时间(审计)
9lifecycleStagelifecycle_stagetextNULLlead | member | churned | unknownai+code客户生命周期阶段(4 值)
10lifecycleStatelifecycle_statetextNULLactive | paused | terminalai生命周期状态(3 值)。不可派生:churned stage 对应 active / terminal 两值,active 需要 AI 从 transcripts 判断 "customer-initiated re-engagement" 语义信号(prompt-builder.ts:154-157, 181-195)。lead_declined close result 触发条件依赖该字段
11notesnotestextNULLai+staff备注
12doNotContactdo_not_contactbooleanNULLtrue / falseai+staffDNC 标记,全渠道停止触达。Sticky — AI 不可反转,代码保证
13hasCardOnFilehas_card_on_filebooleanNULLtrue / falsecode客户是否有信用卡存档(来自 POS/CRM 同步)
14lastActivityAtlast_activity_attimestamptzNULLcode最后活动时间(任何互动都更新)。Forward-only:只能向前推进,不可倒退
15actionNeededaction_neededbooleanNULLtrue / falseai跨通话综合判断是否需要跟进。⚠️ Final 退役:= EXISTS(open task for this contact),由代码从 task 派生。详见 §4.1
16actionNeededReasonaction_needed_reasontextNULLai为什么需要行动
17suggestedActionssuggested_actionsjsonbNULLArray<{ action, reason, priority: 'high'|'medium'|'low', priorityReason }>aiAI 建议的下一步操作列表。⚠️ 见 §6 Open Question
18leadStatuslead_statustextNULL(12 值)— 见 §2.2.2aiLead 漏斗状态。Lead 状态存在 contacts 表,不在 leads 表。前端漏斗与展示口径见 漏斗与状态
19leadStatusReasonlead_status_reasontextNULLai为什么是这个状态
20leadObjectionslead_objectionsjsonbNULLstring[]ai犹豫中的顾虑标签
21leadRejectionReasonslead_rejection_reasonsjsonbNULLstring[]ai明确拒绝的原因标签
22purchaseIntentpurchase_intenttextNULLhigh | medium | lowai购买意向等级(3 值)
23purchaseIntentReasonpurchase_intent_reasontextNULLai为什么是这个意向等级
24goalsgoalsjsonbNULLArray<{ goal: string; reason: string }>ai客户目标列表
25customerSummarycustomer_summarytextNULLai综合所有通话和 SMS 的整体画像摘要
26lastContactAnalysisAtlast_contact_analysis_attimestamptzNULLcodeContact Analysis 最后一次分析完成的时间。用于 Contact Analysis cooldown gate + Circuit Breaker(per-contact 防风暴层)
27doNotContactUpdatedBydo_not_contact_updated_bytextNULLstaff | ai | system (3 值)ai+codeDNC 标记来源,用于 TCPA 合规审计追踪
28hasOpenComplainthas_open_complaintbooleanNULLtrue / falseai+code当前是否有未解决的投诉
29createdAtcreated_attimestamptzNOT NULLDEFAULT NOW()code首次创建时间
30updatedAtupdated_attimestamptzNOT NULLDEFAULT NOW(), $onUpdatecode最后更新时间(Drizzle db.update() 时自动刷新)

2.2 枚举清单(Current)

2.2.1 firstNameTrustScore — 6 levels

UPSERT 时按 score 比较,高分覆盖低分;同分时新的非空值赢。详见 name-trust.md

ScoreSource含义
100staff员工手动输入(最权威)
80leadLead 表来源(报名表 / 表单填写)
60rc_apiRingCentral 联系人 API
40ai_transcriptAI 通话转录推断
20webhook第三方 webhook
0legacy / unknown历史数据 / 来源未知

2.2.2 leadStatus — 12 values

Lead 漏斗状态(ai 写入)。前端只展示其中 9 个 — showed / trialed / converted 依赖线下数据,V1 不展示。完整映射见 lead-funnel-status.md

Value含义
new新 lead,尚未联系
attempted已尝试联系,未接通
connected已接通并对话
booked已预约到店 / 体验课
showed已到店(依赖线下数据)
trialed已体验(依赖线下数据)
converted已转化为会员(依赖线下数据)
unreachable多次尝试无法触达
lost_contact失联(原本接通过但断联)
neglected长时间未跟进
bad_timing时机不对(暂缓)
not_interested明确不感兴趣

2.3 Indexes(Current)

IndexColumns用途
Primary Key (复合)(phone, storeId)State C 终态主键,同一号码在不同门店是独立记录
idx_contacts_franchise(franchiseId)审计
idx_contacts_site(accountId)审计
idx_contacts_franchise_site(franchiseId, accountId)审计
idx_contacts_lead_status(franchiseId, accountId, leadStatus)Lead 漏斗查询
idx_contacts_last_activity(franchiseId, accountId, lastActivityAt)活跃度查询
idx_contacts_action_needed(franchiseId, accountId, actionNeeded)行动建议查询。⚠️ Final 移除(随 actionNeeded 字段一并退役,见 §4.1)
idx_contacts_store_id(storeId)partial query pattern(PK 已含,显式 index 备用)

3. Historical proposed schema(原 Final)

3.1 contacts 表全字段(29 字段,退役 1 个派生字段)

设计原则:Code as Guardrail, AI as Judgment —— AI 只输出不可派生的语义判断,可派生的值由代码派生(详见 §3.2)。从 Current 移除 1 个派生冗余字段:actionNeeded(#15)。lifecycleState 保留(churned re-engage 等需要 AI 语义判断,见 §3.2)。

#Field (TS)SQL 列TypeNullableEnum / ValuesWriter用途
1phonephonetextNOT NULL, PKcodeE.164 格式电话号码,隔离键之一
2franchiseIdfranchise_idtextNOT NULLcode所属品牌(审计字段)
3accountIdaccount_idtextNOT NULLcodeRC providerAccountId(审计字段)
4storeIdstore_idtextNOT NULL, PKcodeDynamoDB StoresV2 store UUID,终态隔离键
5firstNamefirst_nametextNULLcode
6lastNamelast_nametextNULLcode
7firstNameTrustScorefirst_name_trust_scoresmallintDEFAULT 0(6 值)— 见 §2.2.1code名字来源的信任分
8firstNameUpdatedAtfirst_name_updated_attimestamptzNULLcodefirst_name 最后更新时间
9lifecycleStagelifecycle_stagetextNULLlead | member | churned | unknownai客户生命周期阶段(4 值)
10lifecycleStatelifecycle_statetextNULLactive | paused | terminalai生命周期状态(3 值)。churned re-engage 等需 AI 语义判断,不可纯派生。lead_declined close result 触发条件依赖该字段
11notesnotestextNULLai+staff备注
12doNotContactdo_not_contactbooleanNULLtrue / falseai+staffDNC 标记,Sticky
13hasCardOnFilehas_card_on_filebooleanNULLtrue / falsecode信用卡存档
14lastActivityAtlast_activity_attimestamptzNULLcode最后活动时间,Forward-only
15actionNeededReasonaction_needed_reasontextNULLai为什么需要行动(保留 reason 文本,boolean 退役)
16suggestedActionssuggested_actionsjsonbNULLArray<{ action, reason, priority, priorityReason }>aiAI 建议的下一步操作列表
17leadStatuslead_statustextNULL(12 值)— 见 §2.2.2aiLead 漏斗状态
18leadStatusReasonlead_status_reasontextNULLai为什么是这个状态
19leadObjectionslead_objectionsjsonbNULLstring[]ai犹豫中的顾虑标签
20leadRejectionReasonslead_rejection_reasonsjsonbNULLstring[]ai明确拒绝的原因标签
21purchaseIntentpurchase_intenttextNULLhigh | medium | lowai购买意向等级
22purchaseIntentReasonpurchase_intent_reasontextNULLai为什么是这个意向等级
23goalsgoalsjsonbNULLArray<{ goal, reason }>ai客户目标列表
24customerSummarycustomer_summarytextNULLai综合画像摘要
25lastContactAnalysisAtlast_contact_analysis_attimestamptzNULLcodeContact Analysis 最后一次分析完成时间
26doNotContactUpdatedBydo_not_contact_updated_bytextNULLstaff | ai | systemai+codeDNC 标记来源(TCPA 审计)
27hasOpenComplainthas_open_complaintbooleanNULLtrue / falseai+code是否有未解决投诉
28createdAtcreated_attimestamptzNOT NULLDEFAULT NOW()code首次创建时间
29updatedAtupdated_attimestamptzNOT NULLDEFAULT NOW(), $onUpdatecode最后更新时间

3.2 退役字段的真值源(派生方式)

退役字段真值源(从哪派生)派生时机
actionNeededEXISTS(SELECT 1 FROM tasks WHERE contact_phone=? AND store_id=? AND status='open')现查(KPI / badge 改成 JOIN tasks)或 task 写入时维护 denormalized count

为什么 lifecycleState 不能退役(2026-06-02 engineer feedback):prompt-builder.ts:154-157 真值表显示 churned stage 对应 active / terminal 两值,active 需要 AI 从 transcripts 判断 "customer-initiated re-engagement" 语义信号 —— 代码无法纯派生。f(lifecycleStage, leadStatus) 公式 incomplete。此外,lead_declined close result 设计依赖该字段作为触发信号(见 docs/product-design/v1/prompt-improvement/2026-05-prompt-open-issues.md:125)—— 它有 downstream consumer,不是单纯的派生 cache。

为什么 actionNeeded 退役:历史上 task 系统不完整(closeResult 混乱、无清晰 open/closed 语义),没法可靠用"有无 open task"代表"要不要跟进",所以 AI 单独输出 boolean 当缓存。task 4 层 model + Code as Guardrail 原则下,变成冗余。

跨表关联:历史设计里的 tasks.actionNeeded 是同类冗余 signal 的“另一半”;V3 Target 用简单 lifecycle 与 derived projection 分开表达,见 Task V3 Product / Domain Contract §5.1。历史演进原则见 unified-pipeline-final.md「两个 actionNeeded 都应退役」。

仍真需要 AI 输出的字段(不退役,对照):lifecycleStage(churned/unknown 带 leadStatus 表达不了的独立信息)、leadStatuspurchaseIntenthasOpenComplaint(有状态机的跨通话聚合)、customerSummarygoals 等 —— 这些是不可派生的语义判断。


4. Historical delta(2026-06 snapshot → 旧 proposed Final)

4.1 Removed(字段退役 + 索引一并移除)

RemovedWhere由什么派生(真值源)PhaseReason
contacts.actionNeeded(Current #15)contacts 表EXISTS(SELECT 1 FROM tasks WHERE contact_phone=? AND store_id=? AND status='open')(见 §3.2)分 2 phasestudio-api 真在读这列(routes/v3/contacts.ts contacts list + routes/v3/leads.ts:209 KPI summary COUNT(*) FILTER (WHERE c.action_needed = true))。退役不是零成本,需先迁移读路径
idx_contacts_action_neededindexesPhase 2actionNeeded 字段一并移除(不再需要这个 index)

lifecycleState 不退役(2026-06-02 engineer feedback,撤销之前的退役决定):churned re-engage 等 case 不能纯派生,需要 AI 语义判断;lead_declined close result 设计依赖该字段作为触发信号。详见 §3.2。

contacts.actionNeeded 退役 Phase 拆解

Phase动作
Phase 1① 改 routes/v3/contacts.ts + routes/v3/leads.tsc.action_needed 替换成 EXISTS(SELECT 1 FROM tasks t WHERE t.contact_phone=c.phone AND t.store_id=c.store_id AND t.status='open') 子查询;② Leads KPI 改成"数有 open task 的 contact" — COUNT(DISTINCT c.phone) FILTER (WHERE EXISTS ...);③ contact_analyzer 暂时仍写该字段(保险,observe-only);④ 不动 schema
Phase 1 observation period1-2 周 staging 上 parallel run 新旧 query 对比 KPI / contacts list 数据,verify EXISTS 口径无漂移
Phase 2observe 通过 → DROP COLUMN action_needed + 停 contact_analyzer 写入 + 删 idx_contacts_action_needed index

EXISTS perf 担心:tasks 表 idx_tasks_contact_status 现已 cover (contactPhone, franchiseId, accountId, status),Phase 1 改名后改为 (contactPhone, storeId, status)(随 tasks.store_id NOT NULL migration 同步)。EXISTS 子查询能走 index,dashboard 高频查也 OK。无需 denormalized counter。

4.2 Modified

无字段名变或语义改的 modify 项(枚举值无变化,沿用 §2.2)。

4.3 Added

无新增字段或新表(Final 是从 Current 删 1 字段 actionNeeded,不加新东西)。

4.4 Unchanged(明确保留,reviewer 不用 verify)

phone / franchiseId / accountId / storeId / firstName / lastName / firstNameTrustScore / firstNameUpdatedAt / lifecycleStage / lifecycleState / notes / doNotContact / hasCardOnFile / lastActivityAt / actionNeededReason / suggestedActions / leadStatus / leadStatusReason / leadObjections / leadRejectionReasons / purchaseIntent / purchaseIntentReason / goals / customerSummary / lastContactAnalysisAt / doNotContactUpdatedBy / hasOpenComplaint / createdAt / updatedAt(共 29 字段)。

枚举(firstNameTrustScore 6 值 + leadStatus 12 值)、其余 7 个 indexes 均不变(沿用 §2.2 / §2.3)。

actionNeededReason / suggestedActions 仍保留 — 它们是承载内容的字段(reason 文本被前端实际显示、suggestedActions 是 AI 给出的具体建议),不是冗余 boolean。但它们最终归属 contacts 还是并入 task 实体待定,见 §6 Open Questions。


5. Prompt Output ↔ Schema 字段对照(给 reviewer 用)

Reviewer 用本表逐一比对 prompt 输出 JSON 字段是否能写入 contacts 表。 Prompt 全文存在 ../unified-pipeline/prompts/

5.1 Current(Phase 1)— Contact Analyzer 一个大 prompt

Prompt输出关键字段写哪张表Phase 1 状态
01 Triage / 02 Classify / 03 Verify / 04 Coachingcall routing / 33 calls 字段 / coaching写 calls 表(见 calls-schema.md)n/a for contacts
06 Contact AnalyzerlifecycleStage / lifecycleState / leadStatus + leadStatusReason / leadObjections / leadRejectionReasons / purchaseIntent + purchaseIntentReason / goals / customerSummary / actionNeeded + actionNeededReason / suggestedActions / hasOpenComplaint / doNotContact + doNotContactUpdatedBy + Task Decision (taskDecisions[])contacts 表 + tasks 表 + contact_timeline一次 AI 调用同时输出 18 个 contact 分析字段 + task decisions。Phase 1 不拆 prompt,只改下游(走 Contact Writer / Task Orchestrator)
07 Task Playbookread-only,不写库n/an/a

5.2 Historical Target — 拆成 Contact Profile + Task Decision 两个 prompt

unified-pipeline-final.md §1 把 Contact Analyzer 拆成 Stage A + Stage B,两个 stage 在同一次 Lambda 调用里跑,共享内存:

StagePrompt输出关键字段写哪张表Schema 影响
A05 Contact Profile(Final 新拆)lifecycleStage / lifecycleState / leadStatus / leadStatusReason / customerSummary / goals / purchaseIntent / purchaseIntentReason / leadObjections / leadRejectionReasons / hasOpenComplaint / doNotContact / actionNeededReason / suggestedActionscontacts 表(本文档)不改 schema — 输出字段对应 §3.1 Final 29 字段。actionNeeded 已退役,prompt 不再输出该字段(由 EXISTS open task 派生,见 §3.2)
B06 Task Decision(Final 新拆)taskDecisions[] (action ∈ create_open / create_closed / close / update / record_progress / reopen / no_op,plus taskId / typeCategory / progressType / closeResult / evidence / confidence)tasks + task_progress_events(见 tasks-schema.md)n/a for contacts

关键判断:Contact Profile prompt 拆分是 prompt architecture refactor(让 contact 画像和 task decision 分别迭代),不是 schema migration。reviewer 验:① prompt JSON 字段落在 §3.1 Final 29 字段内 + 枚举值符合 §2/§3 注释;② 退役字段(actionNeeded)不在 prompt 输出中。

5.3 Reviewer checklist

检查项怎么验
Prompt 输出字段都在 §2 表里?grep prompt 文件的 JSON schema,逐字段对照
lifecycleStage 输出值 ∈ {lead, member, churned, unknown}?prompt 例子 + Lambda 校验代码
lifecycleState 输出值 ∈ {active, paused, terminal}?同上
leadStatus 输出值 ∈ §2.2.2 的 12 值?同上
purchaseIntent 输出值 ∈ {high, medium, low}?同上
goals[] 每项有 {goal, reason} 两字段?JSON shape
suggestedActions[] 每项有 {action, reason, priority, priorityReason} 四字段?JSON shape
doNotContact = true 后 AI 是否能改回 false?不能 — Contact Writer 保证 sticky

6. Open Questions(待产品拍板)

来源 unified-pipeline-final.md §4 Open Questions。本表不计入 Final scope,产品决定后再 update §2。

问题背景影响 schema?
contacts.actionNeeded 何时 deprecate? 已决EXISTS(open task) 派生。Phase 1 改读路径,Phase 2 DROP COLUMN。详见 §3.2 + §4.1Final 移除;actionNeededReason / suggestedActions 是否并入 task 实体仍待定
contacts.lifecycleState 退役? 撤销退役决定(2026-06-02 engineer feedback)churned re-engage 等 case 需 AI 语义判断,不可纯派生;lead_declined close result 设计依赖该字段。保留,与 leadStatus / purchaseIntent 同等地位保留字段,无 schema 改动
Contact Writer Phase 1 不统一全部 18 个 AI 字段Phase 1 只收口 identity / DNC / lastActivityAt;AI profile fields(lifecycle / leadStatus / goals / customerSummary 等)留 Phase 2不改 schema,影响 writer 路径
Phase 1 doNotContactUpdatedBy 是否包含 ai_agent?当前 3 值 staff / ai / system;tool calling 上线后需要区分 ai_agent(autonomous tool)vs ai(passive prompt output)可能新增 1 值

7. Appendix A — Re-verification commands

2026-06-01 verified。文档 stale(>30 天)时重跑下方命令并比对。

# 1. contacts 表所有 column + 注释里的枚举
grep -E "^\s*[a-zA-Z]+:|text\(|integer\(|timestamp\(|boolean\(|jsonb\(|smallint\(" \
  ../../../../../callytics-common/src/db/schema/contacts.ts

# 2. 复合主键 + index
grep -E "primaryKey|index\(" \
  ../../../../../callytics-common/src/db/schema/contacts.ts

# 3. Contact Profile prompt 是否独立(Final 拆分状态)
ls ../../../../../callytics-infrastructure/lambda/contacts-analyzer/src/core/
# 当前预期: prompt-builder.ts (1 个未拆) — verified 2026-06-01
# Final 预期: contact-profile-prompt.ts + task-decision-prompt.ts (2 个) — 拆分后更新

# 4. Final design 是否动 contacts schema
grep -i "contacts\." \
  ../../unified-pipeline/unified-pipeline-final.md
# 期望: 只 reference,不修改 schema(verified 2026-06-01 — 仅 prompt 拆分)

# 5. Codex task pipeline final design 是否动 contacts schema
grep -i "contacts\." \
  ../../tasks-feature/design/task-pipeline-deliverable-codex.md
# 期望: 只 reference,不修改 contacts schema(verified 2026-06-01)

8. Cross-References