> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# Stores Schema

> **Source of truth**:
>
> - Postgres: `callytics-common/src/db/schema/rc-stores.ts` · `store-config.ts` · `staff.ts` · `user-stores.ts` (verified 2026-06-15)
> - DynamoDB: `studio-website-monorepo/apps/api/infra/lib/stack.ts` (CDK Table) + `apps/api/src/types/domain.ts` (item shape) + `apps/api/src/repositories/stores.ts` (writers)
>
> **隔离架构**: 唯一隔离键 `store_id` (UUID) — 详见 [Store-Level 隔离](/system-design/store-level-isolation.md)。Postgres `rc_stores.id` 与 DynamoDB `StoresV2.storeId` 是同一个 UUID,跨数据库一致。
> **不在本文档**: 多门店聚合/激活/通知邮箱的运行时计算口径在 [stores-calculations.md](/product-design/v2/stores-feature/stores-calculations.md);页面控件与卡片展示在 [stores-display.md](/product-design/v2/stores-feature/stores-display.md)。

***

## 1. 表说明与分工

Stores 域 6 张表分两组:**Postgres**(callytics-common Drizzle schema)+ **DynamoDB**(legacy + 跨服务共享)。

| 表                 | 位置       | 谁维护                                                  | 用途                                                                                                                 |
| ----------------- | -------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `rc_stores`       | Postgres | code(studio-api `syncStoresToNeon()`)                | 镜像 RC Site 结构。**全系统隔离键 `store_id` = `rc_stores.id`**                                                               |
| `rc_store_phones` | Postgres | code(随 rc\_stores sync 全量替换)                         | phone → store 物理映射(store 级过滤的核心查询)                                                                                 |
| `store_config`    | Postgres | user(studio-api `PATCH /v3/setup/stores/:id`)        | 用户配置(alias / 激活 / leadEmails / pricing)。RC sync 永不覆盖                                                               |
| `staff`           | Postgres | code(studio-api `syncStaffToNeon()`,replace pattern) | 员工身份(关闭任务下拉、AI 识别员工名、V2 PIN 登录)                                                                                    |
| `store_members`   | Postgres | user(studio-api access-management routes)            | 多门店共享访问(non-owner 才记录,owner 隐含在 `rc_stores.user_id`)                                                               |
| `StoresV2` (DDB)  | DynamoDB | code+user(studio-api `stores.ts` repository)         | 跨服务共享:`storeId → businessConfig` 查询(callytics-infrastructure 多个 Lambda 通过 IAM 读),含 staff / schedule / pricing 全量配置 |

> ⚠️ **Postgres `rc_stores` 与 DDB `StoresV2` 共享同一个 `storeId` UUID**。Postgres 表是 v3 setup/dashboard 数据源;DDB 表是 callytics Lambda 跨服务读 businessConfig 的入口。改隔离逻辑时两边都要看。

写入者列说明:

- **code** = 自动写入(RC sync / Lambda pipeline / studio-api 系统级操作)
- **user** = 用户通过 UI 写入

***

## 2. Postgres `rc_stores` — 门店主表(RC 同步)

由 RingCentral Site 结构自动创建。`syncStoresToNeon()` 在 OAuth 连接和手动 Sync 时按 `(user_id, connection_id, rc_site_id)` upsert。

| #  | Field (TS)          | SQL 列                 | Type        | Nullable | Enum / 默认                          | Writer | 用途                                                                         |
| -- | ------------------- | --------------------- | ----------- | -------- | ---------------------------------- | ------ | -------------------------------------------------------------------------- |
| 1  | `id`                | `id`                  | uuid (PK)   | NOT NULL | `defaultRandom()`                  | code   | 门店唯一标识。**全系统隔离键 `store_id` 就是它**                                           |
| 2  | `userId`            | `user_id`             | text        | NOT NULL | —                                  | code   | Cognito userId — 门店所有者(owner 隐含)                                           |
| 3  | `connectionId`      | `connection_id`       | text        | NOT NULL | —                                  | code   | FK → DDB UserConnections.connectionId                                      |
| 4  | `providerAccountId` | `provider_account_id` | text        | NOT NULL | —                                  | code   | RC account ID                                                              |
| 5  | `rcSiteId`          | `rc_site_id`          | text        | NOT NULL | —                                  | code   | RC Extension List API 的 site.id,在单个 RC account 内唯一                         |
| 6  | `name`              | `name`                | text        | NOT NULL | —                                  | code   | RC site.name,从 RC 派生的展示名                                                   |
| 7  | `timezone`          | `timezone`            | text        | NULL     | —                                  | code   | IANA 时区,取自 RC `SiteInfo.regionalSettings.timezone.name`(e.g. `US/Eastern`) |
| 8  | `type`              | `type`                | text        | NULL     | `store` \| `group`,default `store` | code   | 门店类型:`store`(实体)/ `group`(虚拟分组)。对齐 DDB `StoresV2.type`                     |
| 9  | `franchise`         | `franchise`           | text        | NULL     | —                                  | code   | 品牌标识(e.g. `orangeTheory`)。并非所有门店都属于某品牌                                     |
| 10 | `createdAt`         | `created_at`          | timestamptz | NOT NULL | `defaultNow()`                     | code   | 创建时间                                                                       |
| 11 | `syncedAt`          | `synced_at`           | timestamptz | NOT NULL | `defaultNow()`                     | code   | 最近一次 RC sync 时间                                                            |

**索引**

| 索引                            | 列                                             | 作用                                    |
| ----------------------------- | --------------------------------------------- | ------------------------------------- |
| `uq_rc_stores_user_conn_site` | `(user_id, connection_id, rc_site_id)` UNIQUE | 同一用户在同一 RC 连接下,同 site 只一条             |
| `idx_rc_stores_user`          | `user_id`                                     | 按 user 查我的所有门店(setup / dashboard 主查询) |
| `idx_rc_stores_provider`      | `provider_account_id`                         | 按 RC account 反查                       |

> **为什么 3 列联合唯一**: 一个用户可有多个 RC 连接(不同 providerAccountId),不同连接可能共用同一个合成 siteId(e.g. 单站点账户都叫 `main`)。

***

## 3. Postgres `rc_store_phones` — 电话 ↔ 门店映射(RC 同步)

phone→store 的物理基础。studio-api 用它做 store-level filtering(把通话号码映射到具体门店)。从 RC Extension→Site 关系派生,**每次 sync 全量替换**(按 store DELETE + INSERT)。**无用户编辑入口** — 改门店或电话要去 RingCentral 改完再 re-sync。

| # | Field (TS)      | SQL 列            | Type | Nullable | Enum / 默认                           | Writer | 用途                                                  |
| - | --------------- | ---------------- | ---- | -------- | ----------------------------------- | ------ | --------------------------------------------------- |
| 1 | `storeId`       | `store_id`       | uuid | NOT NULL | FK→`rc_stores.id` ON DELETE CASCADE | code   | 所属门店                                                |
| 2 | `phoneNumber`   | `phone_number`   | text | NOT NULL | —                                   | code   | E.164 格式(e.g. `+15551234567`)                       |
| 3 | `extensionId`   | `extension_id`   | text | NULL     | —                                   | code   | 该电话所属的 RC extension ID                              |
| 4 | `extensionName` | `extension_name` | text | NULL     | —                                   | code   | RC extension 展示名(e.g. `Front Desk` / `Ardmore OTF`) |
| 5 | `phoneType`     | `phone_type`     | text | NOT NULL | `MAIN` \| `DIRECT` \| `TOLLFREE`    | code   | 从 RC `usageType` 映射                                 |

**索引**

| 索引                               | 列                                 | 作用                                 |
| -------------------------------- | --------------------------------- | ---------------------------------- |
| `uq_rc_store_phones_store_phone` | `(store_id, phone_number)` UNIQUE | 一店一号去重                             |
| `idx_rc_store_phones_number`     | `phone_number`                    | 按号码反查所属门店(**store-level 过滤的核心查询**) |

***

## 4. Postgres `store_config` — 门店配置(用户设置)

用户控制的门店配置,与 `rc_stores` 分离,**RC sync 永不覆盖**。1:1 关系,首次写配置时惰性创建。

| # | Field (TS)    | SQL 列          | Type        | Nullable | Enum / 默认                           | Writer | 用途                                                           |
| - | ------------- | -------------- | ----------- | -------- | ----------------------------------- | ------ | ------------------------------------------------------------ |
| 1 | `storeId`     | `store_id`     | uuid (PK)   | NOT NULL | FK→`rc_stores.id` ON DELETE CASCADE | code   | 关联门店。RC site 被删时配置自动清理                                       |
| 2 | `alias`       | `alias`        | text        | NULL     | —                                   | user   | 用户自定义展示名。NULL 时回退用 `rc_stores.name`                          |
| 3 | `activatedAt` | `activated_at` | timestamptz | NULL     | —                                   | user   | 门店"加入"用户活跃工作区(侧栏 / Dashboard)的时间戳。**NULL = 未激活**,非空 = 在该时刻激活 |
| 4 | `leadEmails`  | `lead_emails`  | text\[]     | NULL     | default `[]`                        | user   | 门店级 Lead 通知邮箱列表                                              |
| 5 | `pricing`     | `pricing`      | jsonb       | NULL     | default `{}`                        | user   | 定价配置(intro offers / class packs / membership tiers)          |
| 6 | `updatedAt`   | `updated_at`   | timestamptz | NOT NULL | `defaultNow()`,`$onUpdate`          | code   | 最后更新时间                                                       |

> **`activatedAt` 是 timestamp 不是 boolean**: 它决定门店是否出现在用户的活跃工作区。Dashboard 的 multi-store 聚合用 `WHERE sc.activated_at IS NOT NULL` 过滤 — 只统计已激活门店,同时免费拿到激活时间线。(Gemini 设计建议,2026-04-30)

***

## 5. Postgres `staff` — 员工身份

V1.5:Close Task dialog 的 "Who are you?" 下拉。V2:完整员工管理(PIN 登录、角色权限)。Isolation key = `store_id` (NOT NULL);`account_id` 作为 legacy 审计字段保留。

| #  | Field (TS)    | SQL 列          | Type        | Nullable | Enum / 默认                  | Writer | 用途                                                       |
| -- | ------------- | -------------- | ----------- | -------- | -------------------------- | ------ | -------------------------------------------------------- |
| 1  | `id`          | `id`           | uuid (PK)   | NOT NULL | `defaultRandom()`          | code   | 员工唯一标识                                                   |
| 2  | `franchiseId` | `franchise_id` | text        | NOT NULL | —                          | code   | 品牌标识(e.g. `orangeTheory`),保留 NOT NULL 给未来 brand-level 查询 |
| 3  | `accountId`   | `account_id`   | text        | NULL     | —                          | code   | RC providerAccountId。**已被 storeId 取代为隔离键**,保留为审计列        |
| 4  | `storeId`     | `store_id`     | text        | NOT NULL | —                          | code   | **DynamoDB StoresV2 store UUID — 终态隔离键**                 |
| 5  | `name`        | `name`         | text        | NOT NULL | —                          | code   | 员工姓名                                                     |
| 6  | `pin`         | `pin`          | text        | NULL     | —                          | code   | V2 存 bcrypt hash,绝不存明文                                   |
| 7  | `role`        | `role`         | text        | NULL     | `staff` \| `manager`       | code   | V1.5 为 NULL;V2 用于权限控制                                    |
| 8  | `isActive`    | `is_active`    | boolean     | NOT NULL | default `true`             | code   | 软删除。停用员工保留 FK 完整性,用于历史归因                                 |
| 9  | `createdAt`   | `created_at`   | timestamptz | NOT NULL | `defaultNow()`             | code   | 创建时间                                                     |
| 10 | `updatedAt`   | `updated_at`   | timestamptz | NOT NULL | `defaultNow()`,`$onUpdate` | code   | 最后更新时间                                                   |

**索引**

| 索引                   | 列                       | 作用                                                                                   |
| -------------------- | ----------------------- | ------------------------------------------------------------------------------------ |
| `idx_staff_store_id` | `store_id`              | 主查询:按 store 列出员工                                                                     |
| `uq_staff_id_store`  | `(id, store_id)` UNIQUE | 给 V2 复合 FK 用:`tasks(closed_by_staff_id, store_id) → staff(id, store_id)` 强制 store 一致 |

> **Writers**: studio-api `syncStaffToNeon()`(replace pattern,store 级全量替换)。
> **Readers**: callytics-infrastructure transcribe-processor(keywords)/ ai-analysis-processor(staff name 识别)/ studio-api GET store(merge)。

***

## 6. Postgres `store_members` — 多门店共享访问

替代 DDB `orangetheory-UserStore-{env}` 给 v3 用。**只记 non-owner**(EDITOR / VIEWER);owner 隐含在 `rc_stores.user_id`,不在此表冗余存储。

| # | Field (TS)  | SQL 列        | Type        | Nullable | Enum / 默认                                      | Writer | 用途                       |
| - | ----------- | ------------ | ----------- | -------- | ---------------------------------------------- | ------ | ------------------------ |
| 1 | `userId`    | `user_id`    | text        | NOT NULL | PK 复合                                          | user   | Cognito user ID,接收共享访问的人 |
| 2 | `storeId`   | `store_id`   | uuid        | NOT NULL | PK 复合,FK→`rc_stores.id` ON DELETE CASCADE      | user   | 被授权访问的门店                 |
| 3 | `role`      | `role`       | text        | NOT NULL | `EDITOR` \| `VIEWER`(CHECK 强制)— **OWNER 不在此表** | user   | 角色                       |
| 4 | `grantedBy` | `granted_by` | text        | NOT NULL | —                                              | user   | 授权者 userId(通常是 owner)    |
| 5 | `createdAt` | `created_at` | timestamptz | NOT NULL | `defaultNow()`                                 | code   | 创建时间                     |
| 6 | `updatedAt` | `updated_at` | timestamptz | NOT NULL | `defaultNow()`,`$onUpdate`                     | code   | 最后更新时间                   |

**约束 / 索引**

| 名称                           | 类型                                   | 作用                               |
| ---------------------------- | ------------------------------------ | -------------------------------- |
| `(user_id, store_id)`        | PRIMARY KEY 复合                       | 同一用户在同一门店只一条共享记录                 |
| `idx_store_members_store_id` | INDEX                                | 按 store 查所有共享成员(grant/revoke UI) |
| `chk_store_members_role`     | CHECK `role IN ('EDITOR', 'VIEWER')` | 数据库层杜绝 OWNER 写入此表                |

> **v3 授权流程**(`shared.ts:getStoreContext`):
>
> 1. `rc_stores.user_id === userId` → OWNER(零查询)
> 2. `store_members WHERE user_id = ? AND store_id = ?` → EDITOR / VIEWER
> 3. 都不是 → `ForbiddenError`

***

## 7. DynamoDB `orangetheory-StoresV2-{env}` — 跨服务 store + businessConfig

**为什么独立于 Postgres**: callytics-infrastructure 多个 Lambda(transcribe-processor / ai-analysis-processor / contacts-analyzer)需要 `storeId → businessConfig` 低延迟查询。这张 DDB 表是跨服务共享入口,IAM cross-account 授权访问(见 `iam-stack.ts`)。Postgres `rc_stores` + `store_config` 是 v3 setup / dashboard 视图,DDB `StoresV2` 是 Lambda runtime view。两者通过同一 `storeId` UUID 对齐。

**Table 配置**(`studio-website-monorepo/apps/api/infra/lib/stack.ts:240-254`)

| 配置                  | 值                                                                         |
| ------------------- | ------------------------------------------------------------------------- |
| TableName           | `orangetheory-StoresV2-{env}` (env ∈ `test` / `pre` / `preprod` / `prod`) |
| Partition Key       | `storeId` (STRING)                                                        |
| Sort Key            | 无(单键表)                                                                    |
| GSI: `userId-index` | PK `userId` (STRING) + SK `createdAt` (STRING),ProjectionType ALL         |
| Billing Mode        | `PAY_PER_REQUEST`                                                         |
| PITR                | prod 开启                                                                   |
| Deletion Protection | prod 开启                                                                   |
| Removal Policy      | prod `RETAIN` / 非 prod `DESTROY`                                          |

**Item shape — 全字段平铺**(`Store` / `BusinessConfig` / `StaffMember` / `DefaultDaySchedule` / `DateSchedule` interfaces,`apps/api/src/types/domain.ts:89-150`)

下表把嵌套对象按 `Path` 列展平,reviewer 一张表看完所有字段。`Path` 列约定:

- 单段(无 `.`)= 顶层 Store 字段(e.g. `storeId` / `name`)
- `businessConfig.X` = 顶层 `businessConfig` 下的二级字段(单值或数组容器)
- `businessConfig.staff[].X` = `staff` 数组每个 `StaffMember` 元素的字段
- `businessConfig.defaultSchedule[].X` = `defaultSchedule` 数组每个 `DefaultDaySchedule` 元素的字段
- `businessConfig.schedules[].X` = `schedules` 数组每个 `DateSchedule` 元素的字段
- `businessConfig.pricing.classPacks[].X` 同理(嵌套 2 层)

| #  | Path                                         | Type                                             | Required \| Optional | Notes                                                                         |
| -- | -------------------------------------------- | ------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------- |
| 1  | `storeId`                                    | string (PK)                                      | Required             | UUID,**与 Postgres `rc_stores.id` 相同**;表的 partition key                        |
| 2  | `userId`                                     | string                                           | Required             | Owner Cognito userId;GSI `userId-index` 的 PK                                  |
| 3  | `providerAccountId`                          | string                                           | Required             | 绑定的 RC account ID                                                             |
| 4  | `connectionId`                               | string                                           | Required             | FK → DDB UserConnections.connectionId                                         |
| 5  | `name`                                       | string                                           | Required             | 门店名                                                                           |
| 6  | `type`                                       | `StoreType`                                      | Required             | Enum `'store'` \| `'group'`;`'store'` = 实体门店,`'group'` = 虚拟分组                 |
| 7  | `franchise`                                  | string                                           | Required             | 品牌标识(e.g. `orangeTheory`)                                                     |
| 8  | `timezone`                                   | string \| null                                   | Required(可为 null)    | IANA 时区,可显式 `null`                                                            |
| 9  | `leadEmails`                                 | string\[]                                        | Required             | 门店级 Lead 通知邮箱;与 Postgres `store_config.leadEmails` 同步                         |
| 10 | `businessConfig`                             | `BusinessConfig` 对象                              | Required(container)  | 容器字段;子字段见下方 13–40                                                             |
| 11 | `createdAt`                                  | string (ISO)                                     | Required             | 创建时间                                                                          |
| 12 | `updatedAt`                                  | string (ISO)                                     | Required             | 最后更新时间                                                                        |
| 13 | `businessConfig.staff`                       | `string \| StaffMember[]`                        | Optional             | **Legacy**: newline-separated 姓名字符串;**新格式**: 结构化数组(元素字段见 17–23)               |
| 14 | `businessConfig.defaultSchedule`             | `DefaultDaySchedule[]`                           | Optional             | 默认周课表数组(元素字段见 24–28)                                                          |
| 15 | `businessConfig.schedules`                   | `DateSchedule[]`                                 | Optional             | 单日 override 课表数组(元素字段见 29–34)                                                 |
| 16 | `businessConfig.pricing`                     | object                                           | Optional             | 定价容器;子字段见 35–40                                                               |
| 17 | `businessConfig.staff[].id`                  | string                                           | Required             | 员工 ID(数组元素内)                                                                  |
| 18 | `businessConfig.staff[].name`                | string                                           | Required             | 员工姓名                                                                          |
| 19 | `businessConfig.staff[].email`               | string                                           | Optional             | 邮箱                                                                            |
| 20 | `businessConfig.staff[].phoneExtension`      | string                                           | Optional             | 分机号                                                                           |
| 21 | `businessConfig.staff[].role`                | `StaffRole`                                      | Required             | Enum `'general'` \| `'front_desk'` \| `'head_coach'` \| `'manager'`           |
| 22 | `businessConfig.staff[].color`               | string                                           | Required             | Tailwind color class(e.g. `green-500`)                                        |
| 23 | `businessConfig.staff[].isActive`            | boolean                                          | Required             | 是否在职                                                                          |
| 24 | `businessConfig.defaultSchedule[].dayOfWeek` | number                                           | Required             | `0=Sun, 1=Mon, ..., 6=Sat`                                                    |
| 25 | `businessConfig.defaultSchedule[].isOpen`    | boolean                                          | Required             | 当天是否营业                                                                        |
| 26 | `businessConfig.defaultSchedule[].openTime`  | string                                           | Optional             | `HH:mm` 格式                                                                    |
| 27 | `businessConfig.defaultSchedule[].closeTime` | string                                           | Optional             | `HH:mm` 格式                                                                    |
| 28 | `businessConfig.defaultSchedule[].staffIds`  | string\[]                                        | Required             | 当天值班员工 ID(关联 `staff[].id`)                                                    |
| 29 | `businessConfig.schedules[].date`            | string                                           | Required             | `YYYY-MM-DD` 格式(单日 override)                                                  |
| 30 | `businessConfig.schedules[].isOpen`          | boolean                                          | Required             | 当天是否营业                                                                        |
| 31 | `businessConfig.schedules[].openTime`        | string                                           | Optional             | `HH:mm` 格式                                                                    |
| 32 | `businessConfig.schedules[].closeTime`       | string                                           | Optional             | `HH:mm` 格式                                                                    |
| 33 | `businessConfig.schedules[].staffIds`        | string\[]                                        | Required             | 当天值班员工 ID                                                                     |
| 34 | `businessConfig.schedules[].note`            | string                                           | Optional             | e.g. `Holiday` / `Special event`                                              |
| 35 | `businessConfig.pricing.introOffer`          | string                                           | Optional             | 体验价文案                                                                         |
| 36 | `businessConfig.pricing.classPacks`          | `Array<{ name, numClasses, price }>`             | Optional             | 课包列表;元素字段:`name: string` / `numClasses: number` / `price: number`             |
| 37 | `businessConfig.pricing.membershipTiers`     | `Array<{ name, classesPerMonth, monthlyPrice }>` | Optional             | 会员等级;元素字段:`name: string` / `classesPerMonth: string` / `monthlyPrice: number` |
| 38 | `businessConfig.pricing.lateCancelFee`       | number                                           | Optional             | 临取消费                                                                          |
| 39 | `businessConfig.pricing.hrmPurchase`         | number                                           | Optional             | 心率带购买价                                                                        |
| 40 | `businessConfig.pricing.hrmRentalMonthly`    | number                                           | Optional             | 心率带月租                                                                         |

> ⚠️ **Update 行为踩坑提醒**: `updateStore()`(`apps/api/src/repositories/stores.ts:141-249`)对 `businessConfig` 做**字段级合并** — 只更新发送来的二级子字段(e.g. 只更新 `staff` 不会清空 `schedules` / `pricing`),实现是 DDB `SET businessConfig.staff = :val` 这种 nested path update。**如果 `businessConfig` 整个字段本身还不存在**,DDB 会拒绝 nested SET 抛 `ValidationException: document path`,此时降级为整对象 SET(`stores.ts:213-247` 的 catch 块处理)。
>
> ⚠️ **DDB `businessConfig.staff[]` 与 Postgres `staff` 表是两份独立数据**:前者给 calendar / scheduling 用,后者给 AI 识别员工 + Close Task 归因用。改员工时两边都要写。

***

## 8. 跨表关系图

```
┌──────────────────────────────────────────────────────────────┐
│                     storeId (UUID) — 全系统隔离键              │
└──────────────────────────────────────────────────────────────┘
                              │
       ┌──────────────────────┼──────────────────────┐
       │                      │                      │
   Postgres              Postgres              DynamoDB
       │                      │                      │
  ┌────▼─────┐         ┌──────▼──────┐         ┌─────▼──────┐
  │rc_stores │◄────FK──┤store_config │         │ StoresV2   │
  │  .id     │         │ .store_id   │         │ .storeId   │
  └────┬─────┘         └─────────────┘         │            │
       │                                       │ + GSI      │
       │ FK CASCADE                            │  userId-   │
       │                                       │  index     │
   ┌───▼──────────┐    ┌──────────────┐        └────────────┘
   │rc_store_     │    │store_members │
   │ phones       │    │  .store_id   │
   │ .store_id    │    └──────────────┘
   └──────────────┘
                       ┌──────────────┐
   (staff.store_id 是  │   staff      │
    text,不强制 FK,    │  .store_id   │
    跨服务共用)         └──────────────┘
```

**关键不变式**:

1. **Postgres `rc_stores.id` ≡ DDB `StoresV2.storeId`** — 同一个 UUID,跨数据库表达同一个门店
2. **`store_id` 在所有跨表查询中都必须出现**(contacts / calls / tasks 全部带 `store_id` guard,见 [calls-schema.md](/product-design/v2/calls-feature/calls-schema.md) / [tasks-schema.md](/product-design/v2/tasks-feature/tasks-schema.md) / [contacts-schema.md](/product-design/v2/contacts-feature/contacts-schema.md))
3. **owner 不在 `store_members` 表里** — owner = `rc_stores.user_id`,授权流程先查 owner 命中就返回
4. **DDB `businessConfig.staff[]` 与 Postgres `staff` 表是两份独立数据** — 用途不同,改一边不会自动同步另一边

***

## 9. Appendix A — Re-verification commands

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

```bash
# 1. Postgres 4 张 stores 域表
for f in rc-stores store-config staff user-stores; do
  echo "=== $f ==="
  grep -E "pgTable|text\(|integer\(|timestamp\(|uuid\(|boolean\(|jsonb\(|notNull\(|references\(|primaryKey\(|uniqueIndex\(|index\(|check\(" \
    ../../../../../callytics-common/src/db/schema/$f.ts
done

# 2. DDB StoresV2 CDK 定义
grep -n -A 15 "StoresV2-\${environment}\b" \
  ../../../../../studio-website-monorepo/apps/api/infra/lib/stack.ts

# 3. DDB Store item shape
grep -n -A 20 "^export interface Store\b\|^export interface BusinessConfig\b\|^export interface StaffMember\b\|^export interface DefaultDaySchedule\b" \
  ../../../../../studio-website-monorepo/apps/api/src/types/domain.ts

# 4. Task pipeline design 是否动 stores 域
grep -iE "rc_stores|store_config|staff\.ts|store_members|StoresV2" \
  ../../tasks-feature/design/task-pipeline-deliverable-codex.md
# 期望: 只 reference storeId 做隔离 guard,不动 schema

# 5. Unified pipeline 是否动 stores 域
grep -iE "rc_stores|store_config|staff\.ts|store_members|StoresV2" \
  ../../unified-pipeline/unified-pipeline-final.md
# 期望: 只 reference storeId 做隔离 guard,不动 schema
```

***

## 10. Cross-References

- 隔离架构(全 repo 看哪): `docs/system-design/store-level-isolation.md`
- 多门店运行时计算口径: [stores-calculations.md](/product-design/v2/stores-feature/stores-calculations.md)
- 页面展示规范: [stores-display.md](/product-design/v2/stores-feature/stores-display.md)
- 上游表(都带 `storeId` 做 store guard):
  - [calls-schema.md](/product-design/v2/calls-feature/calls-schema.md) — `calls.storeId` (text, NULL until Phase 3 rollout)
  - [tasks-schema.md](/product-design/v2/tasks-feature/tasks-schema.md) — `tasks.storeId` (text, NULL)
  - [contacts-schema.md](/product-design/v2/contacts-feature/contacts-schema.md) — `contacts` PK 已含 `storeId`
- Unified pipeline: [../unified-pipeline/unified-pipeline-final.md](/product-design/v2/unified-pipeline/unified-pipeline-final.md)
- Live schema:
  - Postgres: `callytics-common/src/db/schema/rc-stores.ts` · `store-config.ts` · `staff.ts` · `user-stores.ts`
  - DynamoDB CDK: `studio-website-monorepo/apps/api/infra/lib/stack.ts`
  - DDB item shape: `studio-website-monorepo/apps/api/src/types/domain.ts`
  - DDB repository: `studio-website-monorepo/apps/api/src/repositories/stores.ts`
