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

# Tenant Onboarding:新客户怎么获得 tenant,存量客户怎么桥接

:::warning 一句话现状(2026-07-01 代码实测)
**今天一个新客户 onboard,从头到尾不会碰 tenant。** Signup 只有 Google OAuth(无公司名表单),3 步 onboarding(连 RingCentral → 配店 → 看 dashboard)没有任何一步创建 tenant;建店时的 mapping 同步在「user 没有 tenant」时静默 skip,且调用方不看返回值。全 repo 只有两个地方能创建 tenant:手动 seed 脚本和内部 admin 后台。唯一真实 tenant(orangeTheory)就是 2026-06-11 手动 seed 的。
:::

这份文档回答三个问题:现状链路断在哪(实测)/ V1 新客户流程怎么设计(提案)/ 存量客户怎么桥接(一次性动作)。配对 [`v1-gaps.md`](/system-design/multi-tenant/v1-gaps.md)(缺口 6 指向本文)和 [`prod-rollout-checklist.md`](/system-design/multi-tenant/prod-rollout-checklist.md)(桥接的 prod 执行时点)读。

## 一、现状实测:链路与断点

新用户从注册到用上产品的完整链路,和每一步与 tenant 的关系(证据来自 studio-website-monorepo 2026-07-01 main):

| 步骤                    | 做什么                                                                                 | 与 tenant 的关系                                                                                                                                    |
| --------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| 1. Signup             | Google OAuth(`sign-up.vue` 只有一个 `GoogleButton`)                                     | 无。没有公司名/组织名表单,不落任何 control plane 记录                                                                                                             |
| 2. 登录进入               | `tenant-context` middleware 查 `tenant_members`                                      | **fail-open**:0 条 membership → `logger.warn` + 放行(`tenant-context.ts:78-87`,`tenantGuardEnforce` 默认关)                                           |
| 3. 连 RingCentral + 建店 | `POST /v3/setup/sync` / `suggestions/confirm` / `stores` → 建 `rc_stores` + activate | 建店后调 `ensureTenantMappingsForStores`,但 user 无 active tenant 时**静默 skip**(`control-plane.ts:157-168`,skipReason `no_active_tenant`),三个调用方都不检查返回值 |
| 4. 日常使用               | `store_id` 过滤一切照常                                                                   | 业务不受影响(V1 隔离仍是 store\_id),但 control plane 里该客户**三无**:无 `tenants` 行、无 `tenant_members` 行、无 `tenant_store_mappings` 行                             |

**仅有的两个 tenant 创建路径**(都是手动):

| 路径                                                       | 能做什么                                                                                     | 边界                                                                                  |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| seed 脚本 `apps/api/scripts/seed-tenant-store-mappings.ts` | 从 `rc_stores.user_id` ownership 推导并建 tenant + member + mapping;默认 dry-run,`--apply` 才写   | 不在任何 CI/cron 里,纯手动跑                                                                 |
| `apps/control-plane-admin`(内部后台,Cloudflare Access 门禁)    | 建 tenant(`POST /api/tenants`,status 固定 `provisioning`)、加 member(DB 强制 one-active-tenant) | **不能**建 mapping、不能删/改任何东西;mapping 只能靠建店时的 `ensureTenantMappingsForStores` 或 seed 脚本 |

**断点的本质**:tenant 建档是运营动作,建店是客户动作,两者没有顺序保证也没有对账兜底——客户先建店,mapping 就永远缺(ensure 只在建店那一刻跑);没人建 tenant,客户就永远「三无」且无人知晓(fail-open 静默)。

## 二、V1 设计(提案):admin-led 建档 + 对账 auto-heal 兜底

**为什么不做 self-serve 自动建 tenant**:当前获客是 sales-led,客户总数个位数。让用户 signup 填公司名 = 为不存在的 self-serve 通道建产品面,还会制造垃圾 tenant(control plane 里 `test` / `test b` 两个脏 tenant 就是这么来的)。

**V1 流程**(顺序无关,靠机制兜底):

```text
运营侧(任意时点):在 control-plane-admin 建 tenant(公司名) + 加 owner member(真实 Cognito user_id)
客户侧(任意时点):照常走 3 步 onboarding,全程不感知 tenant
机制兜底(自动):
  ├─ tenant 先建、店后建 → 建店时 ensureTenantMappingsForStores 直接写 mapping(已有,#516)
  ├─ 店先建、tenant 后建 → 对账 job auto-heal 补 mapping(v1-gaps 缺口 4 的 job,unmapped store → derived owner → 唯一 active tenant → insert)
  └─ 历史业务行的 tenant_id → 回填脚本补(apps/api/scripts/backfill-tenant-ids.ts,已存在,定时化见缺口 3)
```

这个设计的关键洞察:**缺口 4 的对账 job 同时就是 onboarding 的兜底机制**——它不只是防漂移,还让「先建店后建档」这个最常见的真实顺序自动收敛。所以对账 job 的优先级比单看缺口 4 更高。

auto-heal 的边界要写死:只有 derived owner 恰好有 1 个 active tenant 时才自动补 mapping;owner 无 tenant、多 active tenant、或跨 tenant 冲突时只进日报/告警,不自动写。

**要补的两个小件**(都很小):

1. 「有店但 owner 无 tenant」进对账 job 的日报(现在这种状态完全不可见;fail-open 的 `logger.warn` 没人看)
2. status 生命周期接线:onboarding 完成的判据(mapping 齐 + 写入带 tenant\_id 验证过)→ 运营在 admin 后台把 `provisioning` 翻 `active`。admin 后台当前没有改 status 的操作,要加一个

## 三、存量客户桥接(一次性,现有 4 个店主,其中 3 个待桥接)

2026-07-01 实测,所谓「存量客户」就这么多(reproducer:`SELECT user_id, count(*) FROM rc_stores GROUP BY user_id` 分别在 test / prod 库跑):

| 库    | 店主 user\_id(前 8 位) | 店数 | tenant 现状                                                |
| ---- | ------------------ | -- | -------------------------------------------------------- |
| test | `e43874d8`         | 11 | **已有**(orangeTheory,2026-06-11 seed;11 家店中 8 家有 mapping) |
| test | `644854c8`         | 3  | 无                                                        |
| test | `d4a8d448`         | 1  | 无                                                        |
| test | `b4d86488`         | 1  | 无                                                        |
| prod | `d4a8d448`         | 8  | 无                                                        |
| prod | `644854c8`         | 3  | 无                                                        |
| prod | `b4d86488`         | 2  | 无                                                        |

去重后**只有 3 个 user 没有 tenant**(`644854c8` / `d4a8d448` / `b4d86488`,test 和 prod 是同一批人)。桥接动作(每人):

1. control-plane-admin 建 tenant + 加 owner member(或直接跑 seed 脚本,它就是为这个场景写的)
2. mapping:seed 脚本 `--apply`,或等对账 job 上线自动补
3. 历史行回填:`backfill-tenant-ids.ts --apply`(mapping 齐了之后跑)

prod 侧的执行时点已固化在 [`prod-rollout-checklist.md`](/system-design/multi-tenant/prod-rollout-checklist.md) Step 2-3;test 侧随时可做。

## 四、Self-serve 的触发条件(现在不做)

当获客通道产品化(自助注册付费)时,再把 tenant 创建挪进产品流:signup 后首次 setup 让用户填公司名 → 自动建 tenant(`provisioning`)+ owner member → 接 `tenant_provisioning_jobs` 账本(见 [`best-practices.md`](/system-design/multi-tenant/best-practices.md) §Silo 自动化必备的 7 件事)。触发信号:运营手动建档成为瓶颈,或第一个非 sales 触达的客户出现。在那之前,admin-led 是正确的懒。

## 五、待拍板(3 个)

1. **V1 方向**:admin-led 建档 + 对账 auto-heal 兜底,这个方向 OK?(替代方案:signup 即自动建 tenant——不推荐,理由见 §二)
2. **status 生命周期**:`provisioning` → `active` 的判据用「mapping 齐 + 新写入带 tenant\_id 验证通过」、由运营在 admin 后台手动翻,可以吗?
3. **存量名单**:`644854c8` / `d4a8d448` / `b4d86488` 这 3 个 user 分别是谁(真实客户/团队号)?真实客户才建档,测试号不建。

## Source of Truth

- `studio-website-monorepo/apps/api/src/middleware/tenant-context.ts` — fail-open 行为
- `studio-website-monorepo/apps/api/src/services/control-plane.ts` — `ensureTenantMappingsForStores` 与 skip 逻辑
- `studio-website-monorepo/apps/api/scripts/seed-tenant-store-mappings.ts` / `backfill-tenant-ids.ts` — 手动建档与回填工具
- `studio-website-monorepo/apps/control-plane-admin/` — 内部 admin 后台能力边界
- [`v1-gaps.md`](/system-design/multi-tenant/v1-gaps.md) — 机制缺口总账(缺口 6 = 本文)
