跳转至

DynamoDB 表关系图

1. 总览

Production 环境 (us-east-1) 的 DynamoDB 表按功能分为 6 组(外加 Legacy 遗留表),全部使用 PAY_PER_REQUEST(按需计费)。Test (us-west-2) 和 Pre (us-east-2) 是 prod 的复刻,schema 相同、数据量较少。

Call Analytics — 通话分析

表名 PK GSI 数 数据量 说明
call-analytics-prod-call-analysis-us-east-1 telephonySessionId 11 44,854 条 / 175MB AI 分析结果(最大表)
call-analytics-prod-call-events-us-east-1 telephonySessionId 2 27,482 条 / 131MB 原始通话事件
call-analytics-prod-call-analysis-config-us-east-1 client_id 0 8 条 每客户分析配置

ConnectionService — 连接管理(全部启用 KMS 加密 + Deletion Protection)

表名 PK SK GSI 数 Streams 数据量 说明
ConnectionService-Organizations-prod orgId 0 ✅ NEWANDOLD_IMAGES 20 条 组织注册
ConnectionService-Connections-prod orgId provider 1 ✅ NEWANDOLD_IMAGES 14 条 RC 连接(providerAccountId GSI)
ConnectionService-OrgConfigurations-prod orgId 0 ✅ NEWANDOLD_IMAGES 16 条 组织配置
ConnectionService-UserOrganizations-prod userId orgId 1 ✅ NEWANDOLD_IMAGES 62 条 用户-组织成员关系
ConnectionService-SubAccountLinks-prod parentUserId childUserId 1 ✅ NEWANDOLD_IMAGES 13 条 父子用户关系
ConnectionService-CallAnalysisConfigurations-prod client_id 0 ❌ 未启用 14 条 通话分析配置

Phone Management — 电话管理

表名 PK SK GSI 数 数据量 说明
orangetheory-UserConnections-prod userId connectionId 4 8 条 RC OAuth 连接(含加密 token)
orangetheory-PhoneNumbers-prod providerAccountId phoneNumber 2 82 条 RC 电话线路与分机
orangetheory-PhoneStoreAssignments-prod storeId phoneNumber 1 78 条 电话 → 门店映射

Store & Operations — 门店运营

表名 PK SK GSI 数 数据量 说明
orangetheory-StoresV2-prod storeId 1 13 条 门店信息(名称、员工、定价)
orangetheory-UserStore-prod userId storeId 1 55 条 用户-门店权限(OWNER/EDITOR/VIEWER)
orangetheory-SubAccounts-prod childUserId 1 2 条 子账号关系
orangetheory-OAuthStates-prod stateId 0 OAuth 瞬态(TTL 已启用,属性: ttl
studio-Cache-prod cacheKey 0 通用缓存(TTL 已启用,属性: ttl,含 SMS 10 分钟缓存)
studio-BlackoutPeriods-prod storeId blackoutId 1 禁呼时段
studio-OperatingHours-prod storeId sk 0 营业时间

Lead Tracking — 线索追踪

表名 PK GSI 数 数据量 说明
LeadTracking-v2-us-east-1 id 4 3,695 条 / 9MB 邮件线索(phone/email/forwardedFrom GSIs)

RC Subscription — Webhook 订阅

表名 PK GSI 数 数据量 说明
rc-subscription-prod-webhook-subscriptions accountId 1 少量 Webhook 订阅状态
rc-subscription-prod-idempotency id 0 幂等性去重(TTL 已启用,属性: expiration

Legacy 遗留表(仅 prod)

表名 数据量 Deletion Protection 说明
call-analysis 20,487 条 / 27MB CDK 之前的旧通话分析表
call-events 24,550 条 / 69MB CDK 之前的旧通话事件表
CallAnalysisMetadata 401 条 更早期格式
client-configurations 2 条 旧客户配置

2. 核心实体关系

2.1 全局关系图

graph TD
    subgraph "用户与连接"
        U["👤 User<br/>(Cognito userId)"]
        UC["🔗 UserConnections<br/>PK: userId + connectionId<br/>providerAccountId, mainNumber"]
    end

    subgraph "电话与门店"
        PN["📱 PhoneNumbers<br/>PK: providerAccountId + phoneNumber<br/>extensionId, extensionName"]
        PSA["🔀 PhoneStoreAssignments<br/>PK: storeId + phoneNumber<br/>GSI: phoneNumber-index"]
        ST["🏬 StoresV2<br/>PK: storeId<br/>name, franchise, timezone"]
        US["👥 UserStore<br/>PK: userId + storeId<br/>OWNER / EDITOR / VIEWER"]
    end

    subgraph "业务数据"
        CA["📊 call-analysis<br/>PK: telephonySessionId<br/>11 GSIs, 44K 条"]
        LT["📋 LeadTracking-v2<br/>PK: id<br/>phone (E.164), leadEmail"]
    end

    subgraph "未来新增"
        MS["💬 MessageStore<br/>PK: messageId<br/>storeId, phoneNumber"]
        PB["📒 PhoneBook<br/>PK: phoneNumber (E.164)<br/>contactName, grade"]
    end

    U -->|"1:N"| UC
    U -->|"1:N"| US
    UC -->|"providerAccountId"| PN
    PN -->|"phoneNumber"| PSA
    PSA -->|"storeId"| ST
    US -->|"storeId"| ST

    PN -.->|"fromPhone / toPhone"| CA
    PN -.->|"收发 SMS"| MS
    PN -.->|"E.164 关联"| PB
    LT -.->|"leadId (Phase 1)"| CA
    ST -.->|"franchise + siteId"| CA
    ST -.->|"leadEmails"| LT

    style MS stroke-dasharray: 5 5
    style PB stroke-dasharray: 5 5

2.2 数据隔离模型

storeId 是数据围墙 — 每个门店只能看到自己电话号码关联的数据:

老板 (userId)
  ├── 门店 A (storeId-A)
  │     ├── +1914265xxxx ──→ 门店 A 的 SMS / 通话 / 线索
  │     └── +1914265xxxx ──→ 门店 A 的 SMS / 通话 / 线索
  └── 门店 B (storeId-B)
        ├── +1978489xxxx ──→ 门店 B 的 SMS / 通话 / 线索
        └── +1978489xxxx ──→ 门店 B 的 SMS / 通话 / 线索

❌ 门店 A 看不到门店 B 的数据
❌ 同一个客户在两家店 = 两条独立记录
✅ 老板可以切换门店分别查看

3. 分组详解

3.1 📱 Phone Management(电话管理)

这三张表回答了一个核心问题:哪个电话号码属于哪个门店?

graph TD
    UC["UserConnections (8 条)<br/>━━━━━━━━━━━━━━━<br/>PK: userId + connectionId<br/>━━━━━━━━━━━━━━━<br/>providerAccountId<br/>mainNumber<br/>accountName<br/>encryptedTokens (KMS)<br/>status: ACTIVE"]

    PN["PhoneNumbers (82 条)<br/>━━━━━━━━━━━━━━━<br/>PK: providerAccountId<br/>SK: phoneNumber (E.164)<br/>━━━━━━━━━━━━━━━<br/>extensionId<br/>extensionName (101/102/...)<br/>type: DIRECT / MAIN<br/>━━━━━━━━━━━━━━━<br/>GSI: storeId-index<br/>GSI: connectionId-index"]

    PSA["PhoneStoreAssignments (78 条)<br/>━━━━━━━━━━━━━━━<br/>PK: storeId<br/>SK: phoneNumber<br/>━━━━━━━━━━━━━━━<br/>connectionId<br/>providerAccountId<br/>assignedBy<br/>assignedAt<br/>━━━━━━━━━━━━━━━<br/>GSI: phoneNumber-index"]

    ST["StoresV2 (13 条)<br/>━━━━━━━━━━━━━━━<br/>PK: storeId<br/>━━━━━━━━━━━━━━━<br/>name: White Plains<br/>franchise: orangeTheory<br/>timezone<br/>businessConfig (staff, pricing)<br/>leadEmails<br/>━━━━━━━━━━━━━━━<br/>GSI: userId-index"]

    UC -->|providerAccountId| PN
    PN -->|phoneNumber| PSA
    PSA -->|storeId| ST

实际数据示例

UserConnection:
  providerAccountId: 193365026
  mainNumber: +16105724789
  accountName: "Alan OTF"
  status: ACTIVE
PhoneNumbers (属于这个 RC 账号的线路):
  +19142654371  ext:101  type:DIRECT
  +19142654467  ext:102  type:DIRECT
  +19142654576  (无分机)  type:DIRECT
  +19147290996           type:MAIN
PhoneStoreAssignments (每条线分配到门店):
  +19142654371 → storeId: 53f49dbe-...  (White Plains)
  +19142654467 → storeId: 53f49dbe-...  (White Plains)
  +19142654576 → storeId: 53f49dbe-...  (White Plains)

已知问题:extensionId 类型不一致

PhoneNumbers 表中 extensionId 字段有两种类型:

  • 有分机的号码:{"N": "62733082007"}(Number 类型)
  • 无分机的号码:{"S": ""}(String 类型)

应用代码需要兼容两种类型,建议统一转为 string 处理。


3.2 🔵 Call Analytics — GSI 详解

call-analysis 表有 11 个 GSI(全部 ProjectionType: ALL):

GSI 名称 Partition Key Sort Key 用途
franchise-callStartTime-index franchise callStartTime 按品牌查通话
franchise-siteId-index franchise siteId 按品牌+门店查
siteId-callStartTime-index siteId callStartTime 按门店查通话
fromPhoneNumber-callStartTime-index fromPhoneNumber callStartTime 按来电号码查
toPhoneNumber-callStartTime-index toPhoneNumber callStartTime 按被叫号码查
callDirection-callStartTime-index callDirection callStartTime 按方向查(呼入/呼出)
staff_performance.staff_identification.name-callStartTime-index staffperformance.staffidentification.name callStartTime 按员工姓名查
call_categorization.primary_category.classification-callStartTime-index callcategorization.primarycategory.classification callStartTime 按主分类查
call_categorization.follow_up_type-callStartTime-index callcategorization.followup_type callStartTime 按跟进类型查
call_outcome.outcome_category-callStartTime-index calloutcome.outcomecategory callStartTime 按结果类别查
transcriptionJobName-index transcriptionJobName 转录任务关联

call-events 表的 GSI

call-events 表有 2 个 GSI:transcriptionJobName-indexsessionId-index(RC session 关联)。 注意 sessionId-index 存在于 call-events 表,而非 call-analysis 表。

GSI 优化空间

所有 11 个 GSI 都使用 ProjectionType: ALL(完整复制每条记录),写入成本是基表的 12 倍。 其中 call_outcome.outcome_categorycall_categorization.primary_category.classificationcall_categorization.follow_up_type 三个 GSI 记录数为 0, 可能是 dead code 或数据未填充这些字段。建议排查后删除或改为 KEYS_ONLY projection。

Phase 1 新增字段(PR #397 合并后):

  • leadId — 链接到 LeadTracking 记录
  • customerPhone — E.164 格式客户电话
  • DynamoDB Streams 已启用(NEW_IMAGE)→ 触发 lead-outcome-inference Lambda

3.3 📋 Lead Tracking — GSI 与 Phase 1 变化

LeadTracking-v2-us-east-14 个 GSI

GSI 名称 Partition Key Sort Key 用途
email-receivedAt-index leadEmail receivedAt 按客户邮箱查
emailFrom-receivedAt-index emailFrom receivedAt 按来源邮箱查
phone-receivedAt-index phone receivedAt 按电话号码查(E.164)
forwardedOriginalFrom-receivedAt-index forwardedOriginalFrom receivedAt 按转发源查

Phase 1 新增字段(PR #12 + #397 合并后):

  • phone 格式从 7328561597+17328561597(E.164)
  • duplicateCount / lastDuplicateAt — 去重统计
  • outcome / outcomeUpdatedBy / outcomeUpdatedAt / outcomeHistory — AI 自动推断结果
  • lastContactTime / followUpNeeded / lastCallRef — 最近联系信息

3.4 已知问题

ConnectionService 是设计参考标杆

ConnectionService 是系统中安全性和设计成熟度最高的表组(KMS 加密 + Streams + Deletion Protection)。 新增表(如 PhoneBook、MessageStore)应参考其模式。

表名前缀说明

系统中存在多种表名前缀,对应不同的 CDK Stack / 仓库:

前缀 来源 说明
call-analytics-prod-* callytics-infrastructure 通话分析核心表
ConnectionService-*-prod connectionService CDK 连接管理
orangetheory-*-prod studio-api CDK Phone/Store 管理(旧品牌前缀)
studio-*-prod studio-api CDK 缓存和运营表(新前缀)
LeadTracking-v2-* lead-tracking CDK 线索追踪
rc-subscription-prod-* ringcentralSubscriptionService Webhook 订阅

历史上 BlackoutPeriods 和 OperatingHours 同时存在 orangetheory-*studio-* 两个版本, 但 orangetheory-* 版本已从 prod 中移除,目前仅保留 studio-* 前缀版本。

siteId 与 storeId

call-analysis 表使用 siteId 作为 GSI 分区键,其他表(PhoneStoreAssignments、StoresV2 等)使用 storeId。 两者指向同一个值(门店唯一标识),是历史命名原因导致的不一致。查询时 siteId = storeId


4. 未来新增表(规划中)

4.1 PhoneBook(Phase 1.5 — 优先级 1)

以 E.164 电话号码为核心的客户档案表,建立"人"的概念。完整 schema 参见 PhoneBook 数据库设计

PhoneBook(简化版,完整字段见专项文档)
  PK: phone (E.164)
  orgId, siteId, contactName, grade (hot/warm/cold/A-D/ungraded)
  smsCount, callCount, lastSeenAt

EmailMapping
  PK: email → phone 反查

Alias
  PK: aliasPhone → primaryPhone 映射

4.2 MessageStore + Conversations(Phase 1.5 / Issue #1)

SMS/VoiceMail/Fax 的永久存储,替代当前的 10 分钟 TTL 缓存。完整分析参见 SMS 持久化可行性分析。Schema 尚未定稿,以下为草案。

MessageStore
  PK: messageId
  GSI: storeId-creationTime (⚠️ 设计中需确认 storeId 还是 accountId)
  GSI: fromPhoneNumber-creationTime
  GSI: toPhoneNumber-creationTime
  GSI: conversationId-creationTime

Conversations
  PK: conversationId
  GSI: storeId-lastMessageTime
  GSI: otherPartyPhone-lastMessageTime

4.3 查询路径

SMS webhook 到达后的数据关联路径:

SMS Webhook payload
  │  包含: accountId (RC), fromNumber, toNumber
PhoneStoreAssignments 查询 (phoneNumber-index GSI)
  │  输入: fromNumber 或 toNumber
  │  输出: storeId
写入 MessageStore + Conversations
  │  带上: storeId, phoneNumber
PhoneBook 关联 (未来)
  │  输入: customerPhone (E.164)
  │  输出: contactName, grade, 历史记录
前端展示: 门店 A 的 SMS 收件箱

5. 配置与安全现状

配置项 当前状态 建议
计费模式 全部 PAYPERREQUEST 当前规模合适,无需改动
KMS 加密 仅 ConnectionService(6 张表) 含 PII 的表(PhoneNumbers、LeadTracking)应启用
DynamoDB Streams ConnectionService 5 张表(NEW_AND_OLD_IMAGES);CallAnalysisConfigurations 未启用 call-analysis 表 Phase 1 后启用;MessageStore 新表也应启用
Deletion Protection ConnectionService 6 张表 + Legacy 2 张表(call-analysis、call-events) 所有核心业务表应启用
TTL OAuthStates(ttl)、Cache(ttl)、idempotency(expiration)已启用 其他瞬态数据表如有需要也应启用
总数据量 ~450MB(最大表 175MB) 远低于 DynamoDB 和 D1 的上限