刚换到一家做企业服务的公司时,我在第一次需求评审会上就遇到了一场争论。业务方说"客户要能登录系统查看报表",研发问"这里的客户是指签合同的公司,还是公司里实际点鼠标的那个人",业务方愣了,说"这有什么差别"。结果会开了半小时,就是在理清"客户"和"用户"到底是不是一回事。后来系统上线,又冒出"租户"这个词,整个团队彻底陷入了术语焦虑。
这个问题听起来基础,但几乎所有做SaaS、做B2B系统、做平台类产品的人,早晚都会碰到。更麻烦的是,在不同的语境下,这三个词的含义会偏移。CRM里说的"客户",到了IAM系统里可能叫"租户",而"用户"又经常跟"账号""联系人"混在一起。如果不把这三者的边界和关系定死,后面的权限模型、数据隔离、计费逻辑、统计报表全都会跟着乱。这篇就把我这些年在这上面的理解、建模思路和踩坑经历一次性讲清楚。
1. 为什么这三个词总是被混在一起说
1.1 从一次需求沟通说起
我先讲一个特别典型的需求:"客户登录后可以看到自己的工单进度。"
这句话在业务眼里没有任何问题。但在系统设计上,它同时涉及了三个不同维度的概念:客户(跟你有商务往来、付钱的那个组织)、用户(实际输入账号密码、点击操作的人)、以及系统里为这个客户分配的独立数据空间(也就是租户)。业务方说"客户登录",实际含义是"属于某客户组织下的某个人,登录进该客户专属的系统空间,并看到这个空间内的数据"。
这不是咬文嚼字。如果设计者把"客户"当成"用户"来做,做成一张大表,每个客户联系人一行记录,账号共用一套权限,那当这个客户公司有三个人都要登录,或者客户A的人不小心登录进了客户B的空间时,问题就来了。
1.2 术语在不同岗位的"歧义存量"
另一个把三者搅浑的原因是:不同岗位对"客户"的定义依赖各自的业务语境。
- 销售和市场部门:客户等于"商机、订单、开票、回款"的主体,关注的是这个公司跟我们的交易金额。
- 客服部门:客户可能等于"某个打电话来的人",哪怕这个人只是终端用户里的一线操作员。
- 财务部门:客户等于"挂账主体",看的是应收应付。
- 研发部门:客户往往直接映射为系统里的一个逻辑容器,也就是租户。
同一个词,四个部门四种理解,如果产品需求文档里不加限定直接写"客户",那开发出来的东西大概率是部门利益博弈的结果,而不是一套严谨的业务模型。
1.3 历史遗留系统的"一张表走天下"
还有一个历史原因。早期很多企业内部系统、单机软件,根本没有多组织概念。系统使用者就是公司内部员工,外部客户跟系统不直接发生关系。那时候"用户表"即"员工表","客户表"是外部联系人花名册,两者没有交集,也不会混淆。
但到了SaaS时代、平台互联时代,外部客户要直接登录你的系统、你还要为他们划分隔离空间,旧模型就撑不住了。很多老系统的做法是在"客户表"里加一个"联系人"字段,然后联系人登录时就校验客户ID。这种设计在客户只有一个联系人时勉强能用,一旦客户组织变大、角色变多,就会迅速崩坏。
2. 概念边界:客户、用户、租户各自到底指什么
要理清关系,先得把三个词切到不同维度去看。我的经验是:客户和租户是组织级概念,用户是个体级概念;客户强调商务关系,租户强调系统资源边界。
2.1 客户:商务合同的签约与付费主体
客户是跟你发生交易关系的组织或个人。它回答的问题是:谁给我们钱?谁拥有合同上的权利?
客户具备几个本质属性:
- 商务属性:有账户档案、有信用等级、有合同编号、有计费周期。
- 组织属性:客户通常是一个公司、一个机构,哪怕是个体客户,也应被视为一个"最小组织"。
- 权益归属:服务等级协议(SLA)、数据所有权、合同权益,都挂在客户头上。
- 生命周期:从商机、签约、启用、续费到退订,客户是有生命周期的。
在某CRM系统里,客户表的落库字段往往长这样:
CREATE TABLE customer ( customer_id BIGINT PRIMARY KEY, customer_name VARCHAR(200) NOT NULL, customer_type TINYINT NOT NULL, -- 1=企业 2=个人 3=渠道伙伴 industry VARCHAR(50), contract_no VARCHAR(50), sales_owner_id BIGINT, credit_level TINYINT, status TINYINT NOT NULL, -- 1=潜在 2=签约 3=停用 created_at DATETIME, updated_at DATETIME );注意一点:客户表里不出现登录密码、不出现角色权限、不出现个人手机号。那些是用户域和管理域的数据,不该混进来。
2.2 用户:实际操作系统和触达数据的人
用户是系统中可被识别的最小活动主体。它回答的问题是:谁在操作?谁在看数据?
用户本质上是"身份+凭证+权限"的载体:
- 身份属性:姓名、工号、邮箱、手机号,用来唯一标识一个人。
- 凭证属性:密码、密钥、证书,用来证明"是他本人"。
- 权限属性:角色、组织归属、数据范围,用来决定"他能看到什么、能做什么"。
- 行为属性:登录记录、操作日志、偏好设置,归属到个人。
但请注意,"用户"不等于"自然人"。同一个自然人,在客户公司里可能有多个身份(既是采购对接人,又是系统管理员),在一家公司离职后又以另一家公司身份来登录,那就是两个用户记录。所以在建模时必须把"自然人"与"用户账号"分开,账号属于系统,自然人不属于系统。
2.3 租户:系统内部的隔离与资源边界
租户是多租户架构里的核心概念,它回答的问题是:这份数据、这部分功能,是归属于哪个独立运营空间?
租户的本质是一个"逻辑容器":
- 数据隔离:租户之间的数据不可互相访问(至少业务层面不可见)。
- 配置隔离:每个租户有自己的界面配置、流程配置、权限模板。
- 资源配额:用户数、存储量、API调用次数,按租户计费和控制。
- 身份边界:认证发生在租户上下文内,用户在A租户的账号不能直接访问B租户的资源。
最常见的落地方式有三种:
| 隔离方案 | 数据存储方式 | 适用规模 | 优点 | 缺点 |
|---|---|---|---|---|
| 独立数据库 | 每个租户一个DB | 大客户、私有化 | 隔离彻底、备份恢复方便 | 成本高、运维复杂 |
| 共享数据库独立Schema | 每个租户一个Schema | 中型SaaS | 隔离较好、成本适中 | 连接管理复杂 |
| 共享库共享表 | 业务表加tenant_id列 | 中小型SaaS | 成本最低、开发最快 | 隔离弱、需严防漏写条件 |
我见过不少团队一上来就选第三种,因为省事。但如果目标客户里有大型企业,对方往往会对数据隔离提出硬性要求,到那时再从共享表迁成独立库,痛苦程度极高。我的建议是:默认按共享表设计,但预留tenant_id维度作为全局强制条件;如果明确获知客户有强隔离要求,再评估独立库方案。
2.4 三者维度定位一览
一句话总结三者的关系维度:
| 概念 | 维度 | 核心属性 | 生命周期管理者 | 典型问题 |
|---|---|---|---|---|
| 客户 | 商务/组织 | 合同、账单、权益 | CRM、财务系统 | 谁付钱、谁享受权益 |
| 用户 | 个体/身份 | 凭证、角色、行为 | IAM、用户中心 | 谁登录、谁能干什么 |
| 租户 | 系统/资源 | 隔离、配额、配置 | 平台系统 | 数据放在谁的空间 |
这三者不是三个层级,而是三个坐标轴。一个系统要运作顺畅,就要把每个操作者、每份数据、每张账单,都挂到正确的坐标上。
3. 关系模式拆解:几组常见映射和典型案例
3.1 客户和租户的关系:通常一对一,但不绝对
最常见的模型:一个客户对应一个租户。客户签约后,系统自动开通一个租户空间,该公司所有员工、所有数据、所有配置都装进这个空间里。
但这个模型在现实中有几个变种:
- 一个客户对应多个租户:比如集团客户,母公司签了合同,但子公司之间数据必须隔离,各自有各自的租户。这时客户和租户就是一对多。计费时看整个合同的金额,数据隔离按子公司的租户走。
- 多个客户共用一个租户:这在渠道场景中很常见。比如各地经销商都向总部下单,总部签总合同,但每个经销商被视为一个"客户",在系统里共享同一个平台租户,只是通过不同的客户代码来区分。这种情况下租户内还要做一层"逻辑客户"隔离。
我在一个供应链系统里就踩过这样的坑:每个经销商都想要独立空间,如果真给每个经销商开一个租户,那经销商登录时需要频繁切换租户上下文,而且系统内"串货""跨店比价"的问题就更难监控。最后我们采用了"共享平台租户+客户代码行级隔离"的方案,单独建了一张"客户映射表",把客户ID和租户ID的关系解耦。
3.2 用户和租户的关系:用户必须归属于某个租户
一个用户能否不属于任何租户?可以,但仅限于平台运维人员、超级管理员这类系统级用户。普通业务用户必须归属于某个租户,否则他连"自己在哪个数据空间里"都无法确定,权限模型无从谈起。
用户与租户常见模式:
- 用户归属于一个租户:这是最标准的模式。用户在租户内分配角色,数据范围默认就是本租户。
- 用户归属于多个租户:一个人既在集团总部工作,又被指派为子公司的业务顾问,需要登录两个租户。此时系统要支持"同一账号绑定多个租户身份",登录时选择当前上下文租户。
- 跨租户借调:用户主体在A租户,但短期内需要在B租户协作。这不能直接给他在B租户建一个同样密码的账号,那会造成账号孤岛。正确的做法是"租户内身份分配":同一个user_id,通过user_tenant_rel表绑定多个租户,密码和MFA凭证只维护一份。
这里特别提醒一个常见误解:"用户归属于租户"不等于"用户的数据权限只有本租户"。比如某些报表需要跨租户聚合,那由服务端统一做授权,不要靠给用户开多个账号来解决。
3.3 用户和客户的关系:用户是客户组织的成员
从商务视角看,用户是客户组织下的"人员"。客户与用户之间是"组织-成员"关系,用户属于客户。但由于前面说了客户和租户可能解耦,所以"用户属于客户"和"用户属于租户"这两句话要分别建模。
更清晰的建模方式是引入"组织"这一中间概念:
- 客户是外部商务概念,核心是合同和权益;
- 组织(或公司/部门)是内部结构概念,承载人员归属和汇报线;
- 租户是系统隔离单元;
- 用户是成员账号。
例如一个B2B产品中:
CREATE TABLE org ( org_id BIGINT PRIMARY KEY, customer_id BIGINT NOT NULL, -- 绑定客户 tenant_id BIGINT NOT NULL, -- 绑定租户 org_name VARCHAR(200) NOT NULL, parent_org_id BIGINT -- 支持层级组织 ); CREATE TABLE user_account ( user_id BIGINT PRIMARY KEY, username VARCHAR(50) NOT NULL, password_hash VARCHAR(255) NOT NULL, status TINYINT NOT NULL, created_at DATETIME ); CREATE TABLE user_org_rel ( rel_id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, org_id BIGINT NOT NULL, role_id BIGINT NOT NULL, is_primary TINYINT NOT NULL DEFAULT 0 );通过关系表,用户在一个客户下的不同组织里可以有不同角色。合同挂在customer上,数据隔离看tenant,操作权限看user_org_rel。三层各司其职。
3.4 B2C场景里没有租户吗
很多人觉得租户是B2B的专利,C端产品不存在租户。实际不是。淘宝是平台,每个商家就是一个租户空间;外卖平台里每个店铺也是一个租户,虽然C端用户看到的是一个统一的App,但商家后台、数据统计、商品管理,都是严格按店铺(租户)隔离的。
C端消费者则几乎不感知租户。消费者注册后直接进入一个"平台公共租户"或者按地域、按业务线划分的逻辑分区。所以B2C系统通常是一个超大的共享租户,内部再通过业务属性(比如店铺、社群)做二次隔离。理解了这一点,就不会再问"我们C端产品没有客户,是不是不需要租户"这类问题了。
4. 系统落地的核心:租户隔离与归属链设计
4.1 全链路租户上下文传递
概念理清了,真正难的是落地。多租户系统最容易出的问题,就是"有一个地方忘了加租户条件",导致数据串租户。
我见过的一次严重故障:某SaaS后台在一次性批量导出报表时,SQL里带了全量订单查询,没有按租户过滤。结果是A公司管理员导出的报表里,出现了B公司的订单明细。事后排查发现,是因为代码里有个公共查询方法,默认不带tenant_id条件,只有上层两个调用方加了过滤,第三个调用方忘了。
为了避免这类问题,我的做法是三条硬约束:
- 后端过滤器强制注入:在服务层统一拦截,从登录态拿到当前租户ID,自动拼入查询条件,业务代码里不允许写"我手动加一下租户条件"这种靠自觉的逻辑。
- 数据库表设计时,租户ID设为非空:凡属租户级业务表,tenant_id列一律NOT NULL,索引必须带tenant_id前缀。从源头上杜绝"无租户数据"。
- 联表查询时租户条件双写:主表条件和关联表条件都要带tenant_id,因为关联表可能是跨租户共享的字典表,也可能本身是租户隔离表,不双写就可能把另一租户的关联数据带出来。
4.2 归属链:客户-租户-组织-用户-角色
一个完整的归属链可以这样设计:
客户 (Customer) → 租户 (Tenant) → 组织 (Org/Dept) → 用户 (User) → 角色 (Role)这条链的价值在于:
- 报表统计:按客户维度算账、按租户维度算资源用量、按组织维度算部门绩效、按用户维度算个人工作量。每一层都有明确的分析主体。
- 权限继承:用户进入系统后,先解析其当前租户上下文,再解析组织归属,再合并角色权限,最后得出数据范围。
- 数据归属:每一笔业务数据,直接挂tenant_id + org_id + user_id,上层怎么分析都能找到归属。
在实际项目中,这条链未必都要建全。一个轻量SaaS可能只需要"客户-租户-用户"三层。但一旦需要在租户内做多部门、多分支的数据隔离,组织层就绕不开。我的建议是:在系统第一版就设计出这五层表结构,但允许中间层为空(比如组织层可以不启用),这样后续扩展成本低。
4.3 跨租户协作的边界控制
跨租户需求总会来。常见场景包括:
- 平台运营人员需要看所有租户的统计数据;
- 集团总部需要汇总各子公司的报表;
- 商家A的员工需要在商家B的店铺上架活动。
这些需求的共性是"用户在一个租户上下文内工作,但需要读取另一个租户的数据"。此时不能手一抖把所有租户数据查出来,而是走"显式授权"通道。
我推荐用一个单独的授权模型:跨租户授权表,记录source_tenant_id、target_tenant_id、resource_type、expire_time。每次跨租户访问都必须先校验这张表。这样平台级权限和租户内权限被彻底分开,审计时也一目了然。
5. 实际建模的五个典型错误和对应解法
5.1 错误一:把客户ID当成租户ID
团队早期如果只做过CRM,很容易把"客户"和"租户"当成同义词表。客户A的数据就全挂在customer_id下面。短期没问题,但客户A一旦拆分成了A1、A2两个内部单位,或者客户A和客户B合并了,你就只能做数据迁移,因为客户ID变了,所有业务数据的归属都要跟着改。
解法是:客户ID和租户ID永远分开,两者之间用关系表关联。租户ID是系统内部的稳定键,客户ID是商务侧的稳定键,就算客户被合并了,租户空间还可以保持独立,只是客户生命周期状态改一下。
5.2 错误二:用户表缺少租户维度
常见于老系统改造:原本用户表是全局唯一的,后来加了客户A的人、客户B的人,于是给用户表加了个company_id字段。看起来解决了,但用户登录时如果没强制校验company_id,A客户的人可以通过改URL参数看到B客户的数据。
解法是:用户表不直接放tenant_id,而是通过user_tenant_rel关系表实现用户与租户的多对多。登录后用户信息里同时携带当前默认租户和可切换租户列表,服务端解析权限时一律走租户上下文,不依赖请求参数里的客户ID。
5.3 错误三:天然人数据与账号数据不分
这是"用户"这个概念的经典坑。天然人是生物意义上的人,账号是系统逻辑意义上的主体。一位顾问同时服务于多个客户,他在系统里有多个账号;项目经理离职后又入职竞品公司,系统里是另一个账号,但天然人还是那一位。
如果账号表里直接塞姓名、身份证、手机号,而不单独建person表,就会出很多问题:同一人多个账号无法关联、数据画像分裂、审计时很难追溯"这到底是不是同一个人干的"。
解法是:建person表作为自然人的主数据,用户账号表只存身份凭证,person和user_account是1对N关系。如果系统对实名要求不高,可以推迟到有需要时再引入person表,但账号表不要承担"人的主数据"职责。
5.4 错误四:权限模型没有区分数据范围和组织范围
租户解决了"能进哪个空间",但没解决"空间内能看到哪些数据"。很多系统在租户内做了角色,却忽视数据范围。
举个例子:某部门经理角色,他的数据范围应该是本部门,而不是整个租户。如果角色的权限只配了"查看工单"的模块权限,没配数据范围,这个经理就能看到全租户所有部门的工单。
解法是:权限模型至少分两层:资源权限(能操作哪些菜单/接口)+ 数据权限(能看到哪些组织/人员/区域的数据)。数据权限通常挂组织树节点,角色上存allowed_org_ids或者"本人/本部门/本部门及以下/全租户"四种级别。
5.5 错误五:租户上下文用ThreadLocal存,却忘记清理
租户上下文在Java等后端里常用的做法是:过滤器里从请求头解析租户ID,存入ThreadLocal,供后面代码使用。坑在于线程池复用时,ThreadLocal不清理,下一个请求就会读到上一个请求的租户ID,造成数据错乱。
我在生产环境排查过一次:"为什么A租户的报表偶尔混进B租户的数据?"查了大半天,最后发现是异步任务线程池里ThreadLocal没有remove,任务A没执行完,租户上下文被任务B读取了。
解法是:使用完租户上下文后必须在finally里clear;异步任务则在提交任务时显式传入tenantId,不依赖调用线程的上下文;日志和监控里打印租户ID,方便追踪。
6. 一些实操层面可以照抄的设计建议
6.1 表结构模板:租户中心与用户中心的分工
如果从零开始设计,我建议至少建这几张基础表:
| 表名 | 职责 | 核心字段 | 注意点 |
|---|---|---|---|
| tenant | 租户主表 | tenant_id, tenant_code, tenant_type, status, plan_type | tenant_code唯一且不随意变更 |
| customer | 客户主表 | customer_id, customer_name, sales_owner, contract_status | 商务侧数据,不与租户强耦合 |
| tenant_customer_rel | 租户客户关联 | rel_id, tenant_id, customer_id, relation_type | 一对多关系放这里 |
| person | 自然人主表 | person_id, full_name, phone | 用户账号的归属人 |
| user_account | 用户账号表 | user_id, person_id, username, password_hash | 凭证与身份分离 |
| user_tenant_rel | 用户租户关联 | rel_id, user_id, tenant_id, default_flag | 用户与租户N对N |
| org | 组织架构表 | org_id, tenant_id, parent_org_id, org_name | 租户下的组织结构 |
| user_org_rel | 用户组织关系 | rel_id, user_id, org_id, role_id, is_primary | 一个用户可多组织多角色 |
| permission | 权限表 | perm_id, perm_code, tenant_id_nullable | 内置权限和租户自定义权限 |
中间关系表全部用BIGINT自增主键,关联字段建联合唯一索引(如user_tenant_rel的user_id + tenant_id),避免重复绑定。
6.2 登录流程中的租户判定
一个实用的登录逻辑是:
- 用户输入账号密码,先做身份认证,拿到user_id;
- 根据user_id查user_tenant_rel,得到该用户可访问的租户列表;
- 如果只有一个租户,直接选取为当前上下文;如果有多个,则返回租户选择页或记忆最近使用租户;
- 服务端根据当前租户上下文,再查用户的角色、组织、数据权限;
- 后续所有API请求都在请求头携带X-Tenant-Id,网关层统一校验该租户ID是否在用户的授权租户列表中。
这套流程的核心是:租户ID不能由前端随意传,必须在服务端根据登录态和用户租户关系表推导并校验。前端传的租户ID只能作为上下文切换的意图,不能作为权限凭证。
6.3 一条判断"当前数据该挂在哪一层"的经验法则
写代码时经常要问"这个字段我该传tenant_id还是customer_id还是user_id"。我的经验法则是:
- 如果这个数据属于一个独立运营空间,必须按租户隔离,挂tenant_id。
- 如果这个数据只跟合同、账单、售后有关,不涉及隔离,挂customer_id。
- 如果这个数据记录的是某个人做了什么、看什么,挂user_id和org_id。
- 如果一条数据同时要求"按租户隔离"和"按客户归集",则tenant_id和customer_id都挂,但二者要通过rel表保证一致性,不能互相推导后随便填。
这条法则帮我处理过很多模棱两可的需求。比如一个订单数据,它既要在租户内独立查询,又要在合同维度对账,那就tenant_id和customer_id都要有。服务层不能只取其一。
6.4 命名规范上的建议
最后说个容易被忽视的细节:代码里的命名能反过来强化团队认知。
- 实体类名用Customer表示商务客户,用Tenant表示系统租户,用UserAccount表示用户账号;
- 属性名里不要混用,比如User类里不要出现customerId字段(若确实需要,要先想想是不是应该走UserTenantRel);
- API路径里明确区分:/api/{tenantId}/xxx代表租户域接口,/api/customer/xxx代表商务域接口;
- 数据库表名前缀也分开,比如t_tenant_、t_customer_、t_user_,方便一眼看出数据属于哪个域。
命名统一之后,新人接手项目时踩坑的概率会低很多。毕竟这种三概念纠缠的问题,大多数时候不是技术难度高,而是团队内部的语言没有统一。
回到开头那句话,客户、用户、租户之间的关系,说穿了就是一句话:客户是给钱的,用户是干活的,租户是装东西的。把商务、身份、资源这三个维度分开建模、分开管理,再通过关联关系组合起来,系统的边界就会清晰很多。做企业应用的人,迟早都要在这上面做一次取舍,早点把模型想清楚,后面就能少折腾几次。