DynamoDB 表关系图

Audit 历史
  • 2026-04-26: 9 个 CDK stack deployed(StorageStack / SqsStack / IamStack / LambdaStack / CognitoStack / ContactsStack / MonitoringStack / EventBridgeStack / MessageSystemStack + 条件性 GitHubOidcStack)
  • 2026-04-23: deep audit(AWS CLI 实查 + CDK 代码交叉验证)
  • 2026-03-02: 上次全量 audit
  • call-analysis / call-events / LeadTracking-v2 仍是 DDB-primary;calls / messages / leads 已切换到 Neon-primary read(DDB 表保留作 backward-compat)
状态含义行动
🟢 ACTIVE有活跃代码引用,正在生产使用正常维护
🟡 LEGACY功能已被新表替代,但仍有残留代码引用排查引用后迁移
🔴 DEAD零活跃代码引用,业务读写接近零(≤16 RCU/7d)列入 Archive 候选。archive 前必须 CloudTrail 追踪 2-16 RCU/7d 的实际来源

Production (us-east-1) — 26 张表

全部 PAY_PER_REQUEST(按需计费)。16 个 CDK-managed + 6 个孤儿表 + 4 个 Legacy 表。

28 vs 26 — 为什么少了 2 张?

MessageStore 和 Conversations 已部署到 test (us-west-2),尚未部署到 prod。部署后 prod 将有 28 张。

::: CDK Stack 所有权(5 stacks · 4 repos)

CDK StackRepo表数表名前缀
CallAnalytics-Prod-Storagecallytics-infrastructure3call-analytics-prod-*
OrangeTheoryStack-prodstudio-website-monorepo10orangetheory-* / studio-*
RCSubscription-prod-StorageringcentralSubscriptionService2rc-subscription-prod-*
LeadTrackingStacklead-tracking1LeadTracking-v2-*
(已删除) RingCentralOAuthStack-prod(已归档) ringcentralService6ConnectionService-*-prod

Call Analytics — 通话分析 (3) CDK: CallAnalytics-Prod-Storage

::: info Neon-primary read 已切换 (2026-04-26) call-analysis 表仍接收 ai-analysis-processor 的 dual-write(legacy 兼容),但 Neon calls 已是 primary read source。studio-api 查询通话数据走 Neon SQL(含 store_id filter)。call-events 仍 DDB-primary(无 Neon 镜像)。call-analysis-config 仍 DDB-only(per-client AI 配置,operational table,不迁 Neon)。

表名状态PKGSI说明
call-analytics-prod-call-analysis-us-east-1🟢telephonySessionId11AI 分析结果(最大表,Neon calls 是 primary read,DDB 仅 dual-write 兼容
call-analytics-prod-call-events-us-east-1🟢telephonySessionId2原始通话事件(DDB-only,无 Neon 镜像)
call-analytics-prod-call-analysis-config-us-east-1🟢client_id0每客户分析配置(operational,不迁 Neon
call-analysis 11 个 GSI(全部

ProjectionType: ALL

GSI 名称Partition KeySort Key用途
franchise-callStartTime-indexfranchisecallStartTime按品牌查通话
franchise-siteId-indexfranchisesiteId按品牌+门店查
siteId-callStartTime-indexsiteIdcallStartTime按门店查通话
fromPhoneNumber-callStartTime-indexfromPhoneNumbercallStartTime按来电号码查
toPhoneNumber-callStartTime-indextoPhoneNumbercallStartTime按被叫号码查
callDirection-callStartTime-indexcallDirectioncallStartTime按方向查(呼入/呼出)
staff_performance...name-callStartTime-indexstaff...namecallStartTime按员工姓名查
call_categorization...classification-callStartTime-indexclassificationcallStartTime按主分类查
call_categorization.follow_up_type-callStartTime-indexfollow_up_typecallStartTime按跟进类型查
call_outcome.outcome_category-callStartTime-indexoutcome_categorycallStartTime按结果类别查
transcriptionJobName-indextranscriptionJobName转录任务关联

call-events 有 2 个 GSI:transcriptionJobName-indexsessionId-index(RC session 关联)。

⚠ 所有 11 个 GSI 均 ALL projection,写入成本 12 倍。其中 3 个 GSI 记录数为 0,建议排查后删除或改为 KEYS_ONLYTODO:2026-03-02 审计未记录具体是哪 3 个 GSI。候选(按 feature status 推断,未 verify):call_outcome.outcome_category-callStartTime-index / call_categorization.follow_up_type-callStartTime-index / staff_performance...name-callStartTime-index。下次 audit 需 per-GSI scan --projection-expression 确认。

ConnectionService — 孤儿表 (6) ⚠ 全部 KMS + Deletion Protection

RingCentralOAuthStack-prod CDK stack 于 2025-12-29 删除,表因 RETAIN + DP 保留。功能已被 orangetheory-* 的 user+store-centric 模型替代。主应用无运行时依赖USE_V2_CONFIG_LOADER=false,code path short-circuit),但 lib/stacks/lambda-stack.ts:440-447, 471-479 仍 hardcode CONFIG_TABLE_V2 / ORG_TABLE_V2 / CONN_TABLE_V2 3 个 env vars 到 AnalyticsGenerator + ForceRefresh 2 个 Lambda 的 env(值含 -test 硬后缀,指向在 us-west-2 不存在的 phantom 表)——未启用但未移除。追踪见 infra#658

ConnectionService → orangetheory-* 新旧表映射
ConnectionService (老)orangetheory-* (新)说明
Organizations (orgId)StoresV2 (storeId)数据围墙从 org → store
Connections (orgId+provider)UserConnections (userId+connectionId)RC 连接改为按用户
UserOrganizations (userId+orgId)UserStore (userId+storeId)成员权限改为按门店
OrgConfigurations (orgId)StoresV2.businessConfig配置合并到门店表
SubAccountLinksSubAccounts同样功能,不同 schema
CallAnalysisConfigurationscall-analysis-config直接替代(CDK 管理)
表名状态PKSKGSIStreams说明
ConnectionService-CallAnalysisConfigurations-prod🟢client_id0Legacy Lambda 仍在读(RCU ~450/7d)
ConnectionService-Organizations-prod🟡orgId0✅ NEW_AND_OLDV2 loader 存在但 USE_V2_CONFIG_LOADER=false
ConnectionService-Connections-prod🟡orgIdprovider1✅ NEW_AND_OLDV2 loader 存在但未启用
ConnectionService-OrgConfigurations-prod🟡orgId0✅ NEW_AND_OLDV2 loader 存在但未启用
ConnectionService-UserOrganizations-prod🔴userIdorgId1✅ NEW_AND_OLD零代码引用
ConnectionService-SubAccountLinks-prod🔴parentUserIdchildUserId1✅ NEW_AND_OLD零代码引用

Phone Management — 电话管理 (3) CDK: OrangeTheoryStack-prod

表名状态PKSKGSI说明
orangetheory-UserConnections-prod🟢userIdconnectionId4RC OAuth 连接(KMS 加密)
orangetheory-PhoneNumbers-prod🟢providerAccountIdphoneNumber2RC 电话线路与分机
orangetheory-PhoneStoreAssignments-prod🟢storeIdphoneNumber1电话→门店映射

Store & Operations — 门店运营 (7) CDK: OrangeTheoryStack-prod

表名状态PKSKGSI说明
orangetheory-StoresV2-prod🟢storeId1门店信息(GSI: userId-index)
orangetheory-UserStore-prod🟢userIdstoreId1用户-门店权限 OWNER/EDITOR/VIEWER
orangetheory-SubAccounts-prod🟢childUserId1子账号关系
orangetheory-OAuthStates-prod🟢stateId0OAuth 瞬态(TTL: ttl
studio-Cache-prod🟢cacheKey0通用缓存(TTL 30d: ttl
studio-BlackoutPeriods-prod🟢storeIdblackoutId1禁呼时段
studio-OperatingHours-prod🟢storeIdsk0营业时间(SK: SCHEDULE#date#ts / OVERRIDE#date

Lead Tracking — 线索追踪 (1) CDK: LeadTrackingStack

Neon-primary read 已切换 (2026-04-26)

LeadTracking-v2 仍接收 lead-tracking Lambda 的 dual-write(legacy 兼容),但 Neon leads 已是 primary read source,并已带 store_id(lead-tracking #88)。studio-api 查询线索数据走 Neon SQL。

表名状态PKGSI说明
LeadTracking-v2-us-east-1🟢id4线索记录(Neon leads 是 primary read,DDB 仅 dual-write 兼容
LeadTracking 4 个 GSI
GSI 名称Partition KeySort Key用途
email-receivedAt-indexleadEmailreceivedAt按客户邮箱查
emailFrom-receivedAt-indexemailFromreceivedAt按来源邮箱查
phone-receivedAt-indexphonereceivedAt按电话号码查(E.164)
forwardedOriginalFrom-receivedAt-indexforwardedOriginalFromreceivedAt按转发源查

RC Subscription — Webhook 订阅 (2) CDK: RCSubscription-prod-Storage

表名状态PKGSI说明
rc-subscription-prod-webhook-subscriptions🟢accountId1Webhook 订阅状态
rc-subscription-prod-idempotency🟢id0Lambda Powertools 幂等性去重(TTL: expiration

Legacy 遗留表 (4) Pre-CDK,仅 prod

表名状态说明
call-analysis🟡CDK 前旧表,production-resources.ts 仍引用 ARN
call-events🟡CDK 前旧表,production-resources.ts 仍引用 ARN
CallAnalysisMetadata🔴更早期格式,零代码引用
client-configurations🔴旧客户配置,零代码引用

Test (us-west-2) — 19 张表

Prod 的镜像子集。无 ConnectionService 孤儿表、无 Legacy 表、无 LeadTracking。另有 2 张 prod 尚未部署的新表。

#表名CDK Stack备注
1call-analytics-test-call-analysis-us-west-2CallAnalytics-Test-Storage
2call-analytics-test-call-events-us-west-2CallAnalytics-Test-Storage
3call-analytics-test-call-analysis-config-us-west-2CallAnalytics-Test-Storage
4call-analytics-test-message-store-us-west-2MessageSystemStackProd 未部署
5call-analytics-test-conversations-us-west-2MessageSystemStackProd 未部署
6orangetheory-UserConnections-testOrangeTheoryStack-test
7orangetheory-PhoneNumbers-testOrangeTheoryStack-test
8orangetheory-PhoneStoreAssignments-testOrangeTheoryStack-test
9orangetheory-StoresV2-testOrangeTheoryStack-test
10orangetheory-UserStore-testOrangeTheoryStack-test
11orangetheory-SubAccounts-testOrangeTheoryStack-test
12orangetheory-OAuthStates-testOrangeTheoryStack-test
13orangetheory-BlackoutPeriods-testOrangeTheoryStack-test⚠ 与 studio- 重复
14orangetheory-OperatingHours-testOrangeTheoryStack-test⚠ 与 studio- 重复
15studio-Cache-testOrangeTheoryStack-test
16studio-BlackoutPeriods-testOrangeTheoryStack-test
17studio-OperatingHours-testOrangeTheoryStack-test
18rc-subscription-test-webhook-subscriptionsRCSubscription-test-Storage
19rc-subscription-test-idempotencyRCSubscription-test-Storage
Test 环境差异
  • 无 Deletion Protection、无 PITR、全部 removalPolicy: DESTROY — test 环境允许随时重建
  • BlackoutPeriods / OperatingHours 同时存在 orangetheory-*studio-* 两个版本。Prod 已清理仅保留 studio-*,test 的 orangetheory-* 版本应在下次部署时清理。⚠️ 2026-04-23 live 发现orangetheory-BlackoutPeriods-test29 items(2026-01-10 创建),studio-BlackoutPeriods-test 只有 1 item——legacy 表的数据量反而更多。下次部署清理前必须先把 29 items 迁移到 studio-*,否则丢数据。
  • 缺少: LeadTracking(CDK 未部署到 test)、ConnectionService(孤儿表,从未创建)、Legacy 4 张(Pre-CDK 手动创建)

:::

实体关系

全局关系图

完整的 DynamoDB 全量表关系图(21 张表 · 36 GSIs · 色彩分组 + 关系箭头),源文件: diagrams/dynamodb.drawio

DynamoDB 全量表关系图

数据隔离模型

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

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

  └── 门店 B (storeId-B)
        ├── +1978489xxxx ──→ 门店 B 的 SMS / 通话 / 线索
        └── +1978489xxxx ──→ 门店 B 的 SMS / 通话 / 线索

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

Phone Management 数据流

核心问题:哪个电话号码属于哪个门店?


注意事项

::: danger siteId ≠ storeId — 两个完全不同的字段!

  • call-analysis.siteId = providerAccountIdnumeric,RingCentral 账号 ID,如 277275028
  • StoresV2.storeId = UUID(如 18a16bde-8f20-4442-9b95-35f9d144b0e7
  • 两者不可互换! 要从通话记录关联到门店:phoneNumber → PhoneStoreAssignments → storeId
  • call-analysis.clientId = franchise-providerAccountId(如 orangeTheory-277275028
  • call-analysis.franchise = 品牌名(如 orangeTheory)—— GSI franchise-callStartTime-index / franchise-siteId-index 用此字段

::: ::: info ⚠️ siteId / providerAccountId 即将 rename → accountId siteId 已统一 rename 为 account_id(callytics-common 0.22.0,migration 0016,2026-04-25 完成)。GSI 索引名 franchise-siteId-index 等不会变(DDB 索引名不能改名),但 item 内字段名已切换。详见 identity-fields.md §1.3 + docs/archive/identity-naming-history.md 演变史。

::: 表名前缀

前缀RepoCDK Stack
call-analytics-prod-*callytics-infrastructureCallAnalytics-Prod-Storage
ConnectionService-*-prod(已归档) ringcentralService(已删除) ⚠ 孤儿表
orangetheory-*-prodstudio-website-monorepoOrangeTheoryStack-prod(旧品牌前缀)
studio-*-prodstudio-website-monorepoOrangeTheoryStack-prod(新前缀,勿用 orangetheory-)
LeadTracking-v2-*lead-trackingLeadTrackingStack
rc-subscription-prod-*ringcentralSubscriptionServiceRCSubscription-prod-Storage

PhoneNumbers 表的 extensionId 字段类型不一致:有分机的号码为 Number 类型,无分机的为空 String。代码需兼容两种类型。


Archive 候选清单 { #archive }

审计日期: 2026-04-23 · AWS CLI 实查 + CDK 代码引用 + 全仓库 grep 交叉验证 2026-03-02 首次 audit;2026-04-23 deep re-audit 发现 RCU 数据与前次结论的 drift(见各节 live 提示)。

🔴 DEAD — archive 前必须 CloudTrail 追踪读源

::: warning 2026-04-23 live 发现:DEAD 表并非完全零读 aws cloudwatch get-metric-statistics --metric-name ConsumedReadCapacityUnits --period 604800 显示所有 4 张 DEAD 表在过去 7 天都有 2-16 RCU 的读取。读量极低,但 "零业务读写" 的旧结论已被证伪。archive 前必须用 CloudTrail 定位读源,否则冒 "删掉仍在 low-volume 被读的表" 风险。

另:client-configurationsaws lambda get-function-configuration RingCentralTokenRefresher-Multi-Tenant 验证,运行时 env var CLIENT_CONFIG_TABLE_V1=client-configurations 被设置(而非 doc 之前推断的空串)——这是 legacy Python Lambda,属于 ConnectionService-CallAnalysisConfigurations-prod 的同批退役范围。

表名数据量DP7d RCU前置步骤说明
CallAnalysisMetadata401 条 / 117KB16CloudTrail 追源 → 确认后删除更早期格式,零活跃引用
client-configurations2 条 / 1.5KB2退役 RingCentralTokenRefresher-Multi-Tenant(Python,非 CDK)后删除运行时被 CLIENT_CONFIG_TABLE_V1 env var 引用
ConnectionService-UserOrganizations-prod62 条2.5CloudTrail 追源 → 关闭 DP → 删除零活跃代码引用,新模型用 UserStore
ConnectionService-SubAccountLinks-prod13 条2CloudTrail 追源 → 关闭 DP → 删除零活跃代码引用,新模型用 SubAccounts

🟡 LEGACY — 需完成前置步骤后 archive

ConnectionService 低活跃表(3 张) — 功能已被 orangetheory-* 替代。callytics-infrastructure 中有 V2 config loader 引用,但 USE_V2_CONFIG_LOADER=false(未启用)。

表名DP替代表前置步骤
ConnectionService-Organizations-prodorangetheory-StoresV2-prod① 确认 V2 loader 不再需要 ② 关闭 DP
ConnectionService-Connections-prodorangetheory-UserConnections-prod① 同上 ② 关闭 DP
ConnectionService-OrgConfigurations-prodStoresV2.businessConfig① 同上 ② 关闭 DP

ConnectionService-CallAnalysisConfigurations-prod(🟢 仍活跃)

属性
DP
7d RCU~450 ← 仍有大量读取(2026-04-23 确认)
替代表call-analytics-prod-call-analysis-config-us-east-1(8 条,CDK 管理)
仍在读取的 LambdaRingCentralTokenRefresher-Multi-Tenant(Legacy Python,非 CDK 管理),env var CONFIG_TABLE_NAME=ConnectionService-CallAnalysisConfigurations-prod
关联 DEAD 读源同一 Legacy Lambda env var CLIENT_CONFIG_TABLE_V1=client-configurations —— 同批退役
前置步骤① 退役 Legacy Lambda(CDK 替代: orangetheory-token-refresh-prod,已在运行) ② 切换 USE_V2_CONFIG_LOADER 到新表 ③ 关闭 DP

Legacy 通话表(2 张)callytics-infrastructure/lib/config/production-resources.ts 中硬编码了 ARN(fromTableArn 引用)。

2026-04-23 live 发现:Legacy 表仍有显著读取流量

这两张表不是 quiet legacy —— 7 天 RCU Sum 分别为 call-analysis=3,482call-events=8,819。"前置步骤 ②(确认零业务读写)" 目前不满足,archive 前必须 CloudTrail 追源并迁移 caller。

表名数据量DP7d RCU前置步骤
call-analysis20,487 条 / 27MB3,482① 移除 production-resources.ts 中的 ARN 引用 ② CloudTrail 追源,迁移 caller 到 -prod- 新表 ③ 确认零业务读写 ④ 关闭 DP
call-events24,550 条 / 69MB8,819① 同上

Test 环境重复表(2 张)

表名环境说明
orangetheory-BlackoutPeriods-testus-west-2studio-BlackoutPeriods-test 重复
orangetheory-OperatingHours-testus-west-2studio-OperatingHours-test 重复

Archive 优先级

Phase 1 (立即): CallAnalysisMetadata, client-configurations,
                ConnectionService-UserOrganizations, ConnectionService-SubAccountLinks
              → 零引用零读写,风险最低

Phase 2 (退役 Legacy Lambda 后): ConnectionService-CallAnalysisConfigurations-prod
              → 需先退役 RingCentralTokenRefresher-Multi-Tenant

Phase 3 (清理 V2 loader 引用后): ConnectionService-Organizations/Connections/OrgConfigurations
              → 需确认 USE_V2_CONFIG_LOADER 代码路径可移除

Phase 4 (清理 production-resources.ts 后): call-analysis(legacy), call-events(legacy)
              → 需移除 callytics-infrastructure 中的 hardcoded ARN 引用

Phase 5 (Test 清理): orangetheory-BlackoutPeriods-test, orangetheory-OperatingHours-test
              → 下次 CDK deploy 时清理

删除操作步骤

# Step 1: 关闭 Deletion Protection(需要的表)
aws dynamodb update-table \
  --table-name <TABLE_NAME> \
  --no-deletion-protection \
  --region us-east-1

# Step 2: 删除表
aws dynamodb delete-table \
  --table-name <TABLE_NAME> \
  --region us-east-1
删除前 checklist
  • 确认 7 天 CloudWatch RCU/WCU = 0(或已确认来源并切换)
  • grep 所有活跃代码仓库确认 0 引用
  • 确认数据不需要归档(如需要,先导出到 S3)
  • 在 test 环境先执行删除,观察 7 天无报错
  • 最后在 prod 执行

:::

安全现状

保护状态总览

每张 prod 持久表都有三层保护:DP(Deletion Protection,防 API/Console 误删)、RETAINremovalPolicy,防 cdk destroy 误删)、PITR(Point-in-Time Recovery,防数据损坏,可恢复 35 天内任意时间点)。

::: danger ⚠️ 2026-04-23 live 审计发现:Call Analytics 3 张 prod 表 DP + PITR 未启用 CDK code 中 lib/stacks/storage-stack.ts:131-138, 161-168, 279-286 intent 正确(deletionProtection: envConfig.environmentName === 'prod' + pointInTimeRecoverySpecification),但 CallAnalytics-Prod-Storage stack LastUpdatedTime = 2026-02-04,CDK 保护代码 2026-03-02 才 commit(SHA c129c1d)——从未 deploy 到 prod。52 天 risk window。

$ aws dynamodb describe-table --table-name call-analytics-prod-call-analysis-us-east-1 \
    --region us-east-1 --query 'Table.DeletionProtectionEnabled'
false

$ aws dynamodb describe-continuous-backups --table-name call-analytics-prod-call-analysis-us-east-1 \
    --region us-east-1 \
    --query 'ContinuousBackupsDescription.PointInTimeRecoveryDescription.PointInTimeRecoveryStatus'
"DISABLED"

同样问题影响 call-analytics-prod-call-events-us-east-1 + call-analytics-prod-call-analysis-config-us-east-1。另:rc-subscription-prod-webhook-subscriptions DP 也是 False(PITR ✅,单项漂移)。

Ops 追踪见 infra#656 — 需要 out-of-band aws dynamodb update-table --deletion-protection-enabled + update-continuous-backups,然后 deploy-prod.yml 让 CF 模板对齐。Deploy cadence meta 追踪见 infra#659

分组(表数)Prod DPProd RETAINProd PITRTest DPTest RETAINTest PITR
Call Analytics (3)⚠️ intent=✅ / live=❌⚠️ intent=✅ / live=❌
Studio 持久表 (8)
LeadTracking (1)
RC webhook-subscriptions (1)⚠️ intent=✅ / live=❌
Message System (2)
瞬态表 (3: OAuthStates, Cache, idempotency)

Prod: ⚠️ 4 张应 ✅✅✅ 的持久表实际 DP=False(3 张 Call Analytics + 1 张 webhook-subscriptions),pending ops 修复。Studio 持久表 + LeadTracking 验证 ✅✅✅。瞬态表(TTL 自动过期)无需保护。 Test: 全部 ❌❌❌,允许随时重建。 = 该环境无此表。 ConnectionService 孤儿表和 Legacy 表的保护状态见 Archive 候选

其他配置

配置项当前状态备注
计费模式全部 PAY_PER_REQUEST当前规模合适
KMS 加密仅 ConnectionService(6 张,共用 1 个 KMS key)含 PII 的表应启用
DynamoDB StreamsConnectionService 5 张表(NEW_AND_OLD_IMAGEScall-analysis 表待启用 NEW_IMAGE
TTLOAuthStates (ttl)、Cache (ttl)、idempotency (expiration) = 3 张瞬态数据表
CDK Tagscallytics 3 + ConnectionService 6 + RC Subscription 2 = 11 张orangetheory-、studio-、LeadTracking 缺少 tags