Skip to main content

第五篇:从 1 个客户到一群客户 — 陪跑、多租户与需求收敛

进入信号:新客户自己找上来了——不是你去推,是客户推荐来的。

本篇解决的问题:怎么从服务 1 个客户升级到服务 10 个客户?陪跑的真实节奏是什么?不同客户的需求开始打架了怎么处理?多租户到底怎么做?


L 推荐了新客户,然后需求开始失控

L 在一次行业饭局上提了你在帮她做对账工具的事。三天后,另一家公司的财务总监 G 加了你微信。

G:「听 L 说你们的工具可以自动对账 + 通知销售?我们公司也想用,能看看吗?」

你给他开了试用账号。他用了两天,发来一份需求清单:

  1. 我们不用飞书,用企业微信发通知
  2. 我们的银行流水格式是 CSV,但字段名不一样,能不能支持自定义映射?
  3. 我们财务部有 3 个人,希望设置不同权限——一个人只能看自己负责的客户的对账,经理看全部
  4. 能不能加审批?差异大于 5000 的要财务总监批
  5. 能不能自动生成对账报告,格式要和我们现在的月度报告一样

5 条需求。而 L 之前还提过:「能不能支持支付宝流水」「能不能自动发邮件给客户催款」。

从 1 个客户到 3 个客户,需求开始爆炸。


陪跑的正确姿势:坐在旁边看他用

你决定在写代码之前,先去「陪跑」——这里不是比喻,是真的物理意义上的陪跑。

陪跑是什么?

陪跑是你坐在客户旁边,看着他操作你的产品,记录他皱眉、停顿、绕路、放弃的时刻。 不是发个链接等反馈,不是线上问卷调研。

陪跑的价值在于:客户说出来的需求常常是假的,但操作中的行为不会撒谎。

陪跑 G 的真实记录

你专门去了 G 的办公室,坐在他旁边看他操作。

他打开你的对账工具,上传了 CSV。文件上传成功,但到了「匹配」环节卡住了。

他盯着屏幕 8 秒,鼠标从「开始对账」按钮移到旁边的「设置」按钮,又移回去,如此反复了 3 次。

你没说话,继续观察。他终于点了「开始对账」。匹配结果显示 80 笔差异——显然格式映射有问题。

他叹了口气:「每次都要调格式,好麻烦。」

他说的需求是「自定义映射」,但他的真实问题是「我不想每次都要调格式」——这两个是不一样的。自定义映射是给他一个配置工具,让他自己调;自动化格式识别是检测到是某银行的格式自动映射。

你设计的功能应该是「自动识别格式」而不是「自定义字段映射」。

陪跑的三个层次

层次方式能得到什么会错过什么
看数据看活跃度、留存率用没用为什么用/为什么不用
看反馈发问卷、做访谈客户想什么客户做什么(说着想用≠真的会用)
陪跑坐在旁边看操作客户怎么用、哪里卡、哪里绕

前两个层次可以规模化(10 个以上客户时),但前 10 个客户必须物理陪跑——因为你需要建立最根本的直觉:这个产品在使用中的真实摩擦在哪里。


需求收敛:共性做进产品,个例给 Workaround

10 个客户之后,你会收到至少 50 条不同的需求。怎么处理?

需求判断框架:三问法

每收到一个需求,问三个问题:

1. 这个需求背后是同一个 Job 吗?

  • L 要飞书通知、G 要企业微信通知 → 背后是同一个 Job:「自动通知相关人员」
  • 解决方案不是做一个支持所有 IM 的万能通知系统(成本爆炸),而是做一个「通知渠道」的抽象层,先支持最常见的 2 个

2. 不做这个需求,客户会流失吗?

  • 「能不能自动发邮件给客户催款」——不做,L 的对账能照常跑
  • 「差异能不能不走邮件,直接在系统里确认」——不做,G 的销售根本不会打开系统

优先级 = 不做会流失 > 不做会不满 > 不做也无所谓。

3. 做这个需求,是打开了一类客户还是一扇门?

  • 支持「自定义映射」→ 打开一扇门(所有用非标格式的客户都能接),做
  • 支持「L 公司的特殊报告格式」→ 只打开一个客户,给 Workaround

应对个例需求的策略

Workaround 原则:给客户一个不优雅但能解决问题的方案,而不是把个例需求硬编码进产品。

比如 G 要的月度报告格式很特殊——这不是一个通用需求,其他客户不会用到。你的做法不是在他的账号上单独加一个「特殊报告」功能,而是:

  1. 给他导出一个标准格式的 CSV
  2. 告诉他「你可以用这个 CSV 在 Excel 里套你原来的模板」
  3. 在 Roadmap 里标记「自定义报告模板 → 5 个以上客户反馈后开始设计」

一句话原则:如果 3 个以上客户提了同一个需求,开始设计方案。如果只有 1 个客户提,给 Workaround。


多租户的第二次扩展:从 2 个到 10 个

之前的状态

在第四篇时,你刚加了 TeamUser 表。每个 API Route 里加了 WHERE team_id = ?。这对 2 个客户够用。

10 个客户时的新问题

当客户数从 2 个涨到 10 个后,出现了新问题:

问题 1:每次写查询都要手动加 teamId,忘了就出事

你在某个新功能里忘了加 teamId 过滤,L 偶然看到了一次不属于她公司的数据——虽然那是一条没什么敏感信息的测试数据,但这暴露了一个根本问题:靠人工加 teamId 是不可靠的。

解决方案:Prisma 中间件自动注入。

// lib/prisma.ts
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// 用 Prisma 扩展自动注入 teamId
export function getTeamPrisma(teamId: string) {
return prisma.$extends({
query: {
$allModels: {
// 所有查询自动带上 teamId
async findMany({ args, query }) {
args.where = { ...args.where, teamId };
return query(args);
},
async findFirst({ args, query }) {
args.where = { ...args.where, teamId };
return query(args);
},
// create 时自动写入 teamId
async create({ args, query }) {
args.data = { ...args.data, teamId };
return query(args);
},
},
},
});
}

这样业务代码里不用每次手动加 teamId,中间件自动处理。一次配置,全系统生效。

ToC 对照:ToC 产品不存在这个问题——user_id 隔离天生在查询里。但如果你是 ToC 想做「团队版」,就会突然撞上这个挑战。

问题 2:不同客户的数据量差异开始显现

L 的公司每周 30 笔对账,G 的公司每周 200 笔。某天 G 的对账跑了一分钟还没出结果——数据库没有索引。

-- 找慢查询
SELECT * FROM Reconciliation WHERE status = 'pending' AND team_id = 'xxx';
-- 这条查询全表扫描,因为 team_id 和 status 都没有索引
// schema.prisma 加索引
model Reconciliation {
// ...
@@index([teamId, status]) // 复合索引,覆盖最常用的查询
@@index([bankTransactionId])
@@index([orderId])
}

在 10 个客户的规模下,加几个索引就能解决问题。不需要读写分离、不需要缓存——那是 200 个客户以后的事。

问题 3:部署方式开始尴尬

你一直是 git push 到 GitHub,然后 SSH 到服务器 git pull && npm run build && pm2 restart。手动部署 1 个客户时还好,10 个客户后经常一天部署 3~5 次,每次都要中断工作去操作一遍。

这个问题现在还不够痛,但你已经感觉快了。在第七篇「上云」之前,先忍一忍。


数据库 Schema 的演化:3 张表 → 12 张表

第一版 Demo 时你只有 3 张表。10 个客户后,Schema 变成了:

model Team { ... }
model User { ... }
model BankTransaction { ... }
model Order { ... }
model Reconciliation {
// 之前的状态只有 matched/unmatched
// 现在多了:pending_confirmation(待确认)
// confirmed(已确认)
// disputed(有争议)
// resolved(已解决)
}
model Notification { // 新表:通知记录
id, teamId, userId, type, channel, content, status, sentAt
}
model AuditLog { // 新表:操作日志(谁什么时候做了什么)
id, teamId, userId, action, entityType, entityId, detail, createdAt
}
model MatchRule { // 新表:匹配规则(客户可自定义)
id, teamId, name, conditions(JSON), priority, enabled
}
model ImportTemplate { // 新表:导入格式模板
id, teamId, name, fieldMapping(JSON)
}
model ApprovalRule { // 新表:审批规则(>5000 要经理批)
id, teamId, condition(JSON), approvers(JSON)
}
model Report { // 新表:月度报告
id, teamId, month, data(JSON), generatedAt
}
model Invitation { // 新表:邀请成员
id, teamId, email, role, token, status, expiresAt
}

从 3 张表到 12 张表,这个过程不是一天完成的。是陪跑过程中,每当客户说「能不能……」,你评估后决定「这个值得做」,然后加一张表。

关键原则:永远让真实需求驱动 Schema 扩展,不要预判「未来的客户会需要什么」。


本周迭代节奏

陪跑 + 多客户阶段,你的迭代节奏从「一周一次」加速到了「每天一次」。

周一:G 反馈匹配格式问题 → 晚 上写「自动格式识别」
周二:L 说确认状态看不清楚 → 晚上改 UI 加状态图标
周三:G 说需要审批 → 评估后决定「3 个以上客户再补」,先记下来
周四:新客户 H 导入时报 500 → 加了文件大小限制 + 友好错误提示
周五:H 说「你应该做移动端」→ 优先级低,先放 Roadmap

这个阶段的铁律:判断力 > 执行力。 你不可能在一周内满足所有客户的所有需求。必须学会说「这个先不做」——而且要说得让客户接受。


什么时候进入下一阶段?

你已经验证了:

  • ✅ 产品能服务 10 个不同客户(不是只有熟人能用)
  • ✅ 陪跑帮你建立了需求判断直觉
  • ✅ 多租户架构成型,数据隔离有保障

但危机也在累积:

  • 改一个功能,另一个不相关的功能挂了——你完全不知道为什么
  • API Route 文件里,业务逻辑、数据库查询、通知发送全混在一起——一个文件 300 行
  • 前端组件膨胀:Reconciliation.tsx 从 100 行变成 500 行,里面塞了文件上传、匹配结果、通知面板、审批面板
  • 没有测试——每次改了代码,只能手动点一遍确认没挂
  • 你开始害怕改代码

进入下一阶段的信号:以下任一出现——

  • 改功能 A 导致功能 B 崩了,你完全不知道为什么
  • 一个组件超过 300 行,一个 API Route 超过 200 行
  • 你开始害怕改代码

如果这三个信号同时出现,没有别的选择——必须停下来重构。这是第六篇的内容。


本篇总结

做了什么花了多久
陪跑 3 个首批客户每客户 2-3 次,每次 1-2 小时
需求判断框架 + Workaround 策略持续应用
Prisma 中间件自动注入 teamId1 小时
加索引、Schema 从 3 张表到 12 张表持续迭代
每日迭代节奏每天 1-2 次部署

当前系统形态: 单体应用,能服务 10 个不同客户。多租户通过 Prisma 中间件保障数据隔离。但代码开始失控——组件膨胀、没有测试、不敢改。

本月账单:$0(Supabase 免费层 500MB 快不够用了,但还能撑)

AI 在这个阶段的角色: AI 帮你快速加新功能——写一个新通知模块只要 30 分钟。但它不知道你之前的架构决策,每次都是「在现有代码上缝合」。当缝合的层数超过 5 层,代码就开始崩溃。


下一步

你开始害怕改代码——进入第六篇:从屎山到可维护,第一次有意识的重构。