Skip to content
··阅读时间5分钟

空白屏会摧毁信任。我有 31 个。

我在自己的 SaaS 里发现了 31 个空白屏问题——根因是我忘了多租户不只是数据访问,还包括 URL 上下文。下面是我如何在 Claude Code 帮助下一晚修完。

11 月 1 日,早上 8:47。我收到一封邮件。

“Hey Chandler - 我点了 ‘View’,然后是空白页。我是不是把所有 interview 数据都弄丢了?”

我胃一沉。空白页不是普通 bug,它是信任杀手。

我打开 dev console。检查路由。一个 agency 用户,查看 client “Acme Corp”,点了按钮,然后……URL 从 /clients/acme-corp/agents/agent-name/results/123 变成了 /agent-name/results/123

客户上下文丢了。React Router 找不到对应路由。空白屏。

“好吧,这算一个 bug,”我当时想,“修完继续做别的。”

我让 Claude Code 去扫描代码库里相似模式,然后我照常过周六:吃饭、办事、陪孩子,日常安排。

下午晚些时候我回来查看。Claude Code 确实找到了模式——而且情况很难看。

到晚上 7:42,我修了 14 个 bug。7:48 又找到 8 个。7:56,总数来到 31 个

根因全部相同。修法全部相同。全因为我做多租户时漏了一件事:导航不只是数据问题,它是上下文问题。

---

到底有多糟?

我得坦白这 31 个 bug 的实际意义:

对用户:

- Agency 用户频繁撞空白页(看起来像系统坏了,不像小 bug)

- 会话中途丢失工作流连续性

- “这个平台稳定到能给我们客户用吗?”

- 每一个空白页,都是一次更接近取消试用

对我:

- 每天 5+ 个 bug 反馈(全是导航相关)

- 每个单独排查要 2-3 小时

- 新功能根本发不出去(一直在救火)

- 真实恐惧:“会不会还有别的地方也坏了,只是我还没发现?”

存在性问题是: 如果连导航都做不稳,别人为什么要把客户营销策略交给 STRAŦUM?

空白屏摧毁信任的速度,比任何问题都快。

---

一切从这个 bug 开始

我直接展示发生了什么。

用户路径(本应正常):

1. 打开 /clients/acme-corp/agents/analysis

2. 点击 “Start Session”

3. 完成分析

4. 点击 “View Results”

5. 在 /clients/acme-corp/agents/analysis/results/123 看到结果

实际发生:

1. ✅ /clients/acme-corp/agents/analysis(正常)

2. ✅ 开始会话(正常)

3. ✅ 完成分析(数据已保存)

4. ❌ 点 “View Results” → 空白页

5. ❌ URL 变成 /analysis/results/123(client 上下文丢失)

React Router 去找 /analysis/results/123 这条路由。对 agency 用户根本不存在,于是渲染为空。

用户看到的就是:白屏。没有错误提示,没有 loading,什么都没有。

---

开始深挖后(严格说是 Claude Code 在挖)我看到了什么

这是坏掉代码的样子:

```typescript
// AgentPage.tsx (SANITIZED - the broken pattern)
import \{ useParams, useNavigate \} from 'react-router-dom';

export function AgentPage() \{
  const { clientSlug \} = useParams<\{ clientSlug: string \}>();
  const navigate = useNavigate();

  const handleViewResults = (sessionId: string) => \{
    // Problem: Hardcoded route, no client context
    navigate(`/agent-name/results/${sessionId\}`);
  };

  return (
    // ... component
  );
}
```

问题看到了吗?

我从 URL 里取了 clientSlug,也用它拉了数据。但在导航时,我完全忘记了它。

Agency 路由是:/clients/acme-corp/agents/agent-name

SME 路由是:/agent-name

而我把 SME 模式写死了。Agency 用户自然崩。

---

关键认知:我有 31 个这种问题

下午 2:15。我修了第一个 bug,提交,感觉不错。

3:42。另一个 agent 页面,完全同样模式,修掉。

4:18。又一个 agent。还是一样。

5:30。我盯着屏幕发呆了整整 10 分钟。

每个 agent 页面都有同类 bug。 每个带导航的嵌套组件几乎都中招。每个“Go to...”按钮的共享 UI 也有同类风险。

我可以一个个修,花两天。也可以先抽象模式,再系统化一次修完。

我选了后者。

---

修复方案:上下文感知导航(Context-Aware Navigation)

我没再让每个组件都直接 useParams(),而是让 Claude Code 先做一个 Context provider:

```typescript
// contexts/ClientContext.tsx
import \{ createContext, useContext \} from 'react';
import \{ useParams \} from 'react-router-dom';

interface ClientContextValue \{
  clientSlug: string | null;
\}

const ClientContext = createContext<ClientContextValue | null>(null);

export function ClientContextProvider(\{ children \}: \{ children: React.ReactNode \}) \{
  // Extract clientSlug ONCE at the layout level
  const { clientSlug \} = useParams<\{ clientSlug: string \}>();

  return (
    <ClientContext.Provider value=\{{ clientSlug: clientSlug || null \}}>
      \{children\}
    </ClientContext.Provider>
  );
}

export function useClientContext() \{
  const context = useContext(ClientContext);
  if (!context) {
    throw new Error('useClientContext must be used within ClientContextProvider');
  \}
  return context;
}
```

然后把 agency 路由包在 provider 里:

```typescript
// App.tsx
<Route path="/clients/:clientSlug/*" element={
  <ClientContextProvider>
    <ClientLayout />
  </ClientContextProvider>
}>
  <Route path="agents/analysis" element={<AnalysisAgent />} />
  <Route path="agents/strategy" element={<StrategyAgent />} />
  \{/* ... all client-scoped routes */\}
</Route>
```

这样内部组件都能通过 context 拿到 clientSlug,不用每层传 params。

再加一个路由 helper(因为同样 if/else 写 20 次没意义):

```typescript
// hooks/useContextRoute.ts
import \{ useClientContext \} from '@/contexts/ClientContext';

export function useContextRoute() \{
  const { clientSlug \} = useClientContext();

  const buildRoute = (route: string) => \{
    if (clientSlug) {
      // Agency route: /clients/acme-corp/agents/...
      const cleanRoute = route.startsWith('/') ? route.slice(1) : route;
      return `/clients/${clientSlug\}/${cleanRoute\}`;
    }
    // SME route: /agent-name/...
    return route;
  };

  return \{ buildRoute, clientSlug \};
}
```

使用方式(干净很多):

```typescript
// AgentPage.tsx (SANITIZED - the fixed pattern)
import \{ useClientContext \} from '@/contexts/ClientContext';
import \{ useNavigate \} from 'react-router-dom';
import \{ useContextRoute \} from '@/hooks/useContextRoute';

export function AgentPage() \{
  const { clientSlug \} = useClientContext();
  const \{ buildRoute \} = useContextRoute();
  const navigate = useNavigate();

  const handleViewResults = (sessionId: string) => \{
    // This works for BOTH SME and Agency users
    navigate(buildRoute(`agents/analysis/results/${sessionId\}`));
  };

  return (
    // ... component
  );
}
```

一个 helper,带上下文,SME 和 Agency 都能走通。

---

系统化修复:一天,31 个文件

有了模式后,后面就变成了机械执行。

2025 年 11 月 1 日 - 冲刺日

下午(2 PM - 7 PM) - 发现并分类:

- 在所有 agent 页面发现共性模式

- 搭好 Context provider 和 routing helper

- 在第一个 agent 上验证方案

晚上(7:00 PM - 8:00 PM) - 批量修复:

7:42 PM - 第一波(14 个 bug):

```
fix(multi-tenant): fix 14 navigation bugs and refactor all agents to useClientContext()

Frontend Changes (8 files):
- Refactored 6 agents to use useClientContext() hook
- Fixed 14 navigation bugs that lost client context across multiple agents

Backend Changes (5 files):
- Added client_id parameter to all save operations
- Updated base classes to extract client_id properly

Files changed: 14
Insertions: +732
Deletions: -164
```

7:48 PM - 第二波(再 8 个):

```
fix(multi-tenant): fix 8 navigation bugs in strategy page

- Fixed 8 navigation buttons that lost client context
- Total bugs fixed: 22 (14 + 8)

Files changed: 1
Insertions: +71
Deletions: -23
```

7:56 PM - 最后一波(再 9 个):

```
fix(multi-tenant): fix 9 navigation bugs in interview and tool pages

- Fixed remaining navigation issues in nested components
- Total bugs fixed: 31 bugs across entire application

Files changed: 2
Insertions: +46
Deletions: -10
```

我把所有 agent 流程全跑了一遍。SME 用户、Agency 用户、每个按钮都点。没有空白页。

总变更规模: 17 个文件,新增 849 行,删除 197 行。

投入时间: 与 Claude Code 协作、非连续约 6 小时(下午 2 点发现,到晚上 8 点最终提交)。

节省时间: 估算至少 30+ 小时的逐个修 bug 与用户支持成本。

---

我学到的(硬课)

1. 多租户导航比数据隔离更难

org_id 过滤数据并不难。

难的是导航。你对同一个功能要同时支持 两套合法 URL 结构

```
SME:    /agent-name/session/123
Agency: /clients/acme-corp/agents/agent-name/session/123
```

每次 navigate() 都必须知道当前该用哪套模式。错一次,用户就看到白屏。

2. useParams() 会“骗你”

```typescript
const \{ clientSlug \} = useParams();
```

它看起来可用……直到路由变化。然后 clientSlug 变成 undefined,你下一次导航就炸。

React Context 不会“骗你”。它始终可用、始终一致。

3. 不要修到一半就宣布胜利

我一开始以为只有 14 个。后来又挖出 8 个。再后来又 9 个。

教训: 不要只看你怀疑有问题的几个文件。先 grep 整个代码库。

4. 同类 bug 出现两次,就该停下来抽象模式

逐个修: 31 个 bug 可能要一周

系统化修: 找模式 → 做 helper → 一次改完 = 6 小时

那天 5:30 PM 我盯屏幕发呆的 10 分钟,给我省了几天。

5. 导航 bug 是生存级威胁

我们总在讨论状态管理、数据拉取、API 优化。

但导航坏了 = 白屏 = “平台坏了”。

用户不会关心你 RLS 策略有多漂亮,也不会关心多租户架构多优雅。他们只关心点了 “View Results” 能不能看到结果。同样这个“技术上可用”和“产品上完成”之间的差距,我在 用 AI 构建原生 iOS 应用 时也再次遇到:Claude Code 能很快生成骨架,但真正让用户信任产品的打磨,仍是那 40% 人工工作。

---

这套模式(给你的多租户应用)

如果你在做分层 URL 的多租户 SaaS:

✅ 第 1 步: 在 layout 层创建 Context provider

```typescript
<ClientContextProvider>
  \{/* All tenant-scoped routes */\}
</ClientContextProvider>
```

✅ 第 2 步: 做一个路由 helper

```typescript
const \{ buildRoute \} = useContextRoute();
navigate(buildRoute('agents/analysis/session/123'));
```

✅ 第 3 步: 禁止硬编码路由

```typescript
// ❌ BAD
navigate('/analysis/session/123');

// ✅ GOOD
navigate(buildRoute('agents/analysis/session/123'));
```

✅ 第 4 步: 把所有导航调用全搜出来

```bash
# Find every navigate() call
grep -r "navigate(" src/ > navigation_audit.txt

# Find hardcoded routes
grep -r "navigate('/" src/ | grep -v "buildRoute"
```

✅ 第 5 步: 用所有用户类型做回归

SME 流程。Agency 流程。每个按钮。每个链接。不能有白屏。

---

结果

11 月 1 日之前:

- 有 31 个潜伏导航 bug

- 每天 5+ 个 bug 反馈

- Agency 用户质疑平台稳定性

- 我自己不敢放心发新功能

11 月 1 日之后:

- 导航 bug 归零

- 导航相关支持工单归零

- 未来开发有了稳定模式

- 我重新有信心继续发新 agent 路由

开发速度变化:

- 新增路由:5 分钟(以前 30 分钟 + “会不会又坏?”)

- bug 反馈:0(以前每天 5+)

- 用户信任:恢复(“平台现在稳定了”)

这 6 小时修复,按支持成本和信任恢复来算,至少回报了 10 倍。

---

为什么我要分享这个

多租户导航不性感。没人会像庆祝“发了一个新功能”那样庆祝“6 小时修了 31 个 bug”。

但如果你和我一样在做多租户 SaaS,比如 https://stratum.chandlernguyen.com/ 这样的 agency 营销 AI 平台——你迟早会遇到这个问题。也许不是 31 个,也许只是 5 个,但一定会遇到。

遇到时请记住:

1. 导航状态里,Context > Params

2. 修复策略里,系统化 > 逐个修

3. 一定要 grep 全库(你会比想象中发现更多)

4. 在宣布修好前,用所有用户类型走完整测试

如果你也曾盯着空白屏,纳闷上下文到底去哪了,知道你不是一个人。我也一样。 :)

你上线后最痛的 bug 是什么?那种只能靠真实用户反馈才暴露的问题。我真的很想听。

致敬,

Chandler

STRAŦUM 架构系列: 这次导航危机,是更大多租户旅程的一部分。它开始于 Day 2 就做多租户,在 Day 67 重建整套 schema 时升级,最后收尾于我发现数据库 逻辑正确但慢了 296 倍

---

Still coding, still learning, still finding bugs in groups of 31.

申请 alpha: https://stratum.chandlernguyen.com/request-invitation

---

P.S. - 报告第一个 bug 的那位用户?他们的 interview 数据并没有丢。数据一直在数据库里,只是坏掉的路由让他们看不到。晚上 7:42 修完后,他们所有成果都还在。这个小小的“还好没丢”让那 6 小时非常值得。

---

继续阅读

我的旅程
联系
语言
偏好设置