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-index 和 sessionId-index(RC session 关联)。
注意 sessionId-index 存在于 call-events 表,而非 call-analysis 表。
GSI 优化空间
所有 11 个 GSI 都使用 ProjectionType: ALL(完整复制每条记录),写入成本是基表的 12 倍。
其中 call_outcome.outcome_category、call_categorization.primary_category.classification、call_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-1 有 4 个 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 的上限 |