租户模型设计:从实体定义到层级关系

好,咱们今天来聊聊租户模型设计。这是多租户系统的地基,地基没打好,后面盖什么楼都晃悠。我在好几个项目里都吃过租户模型设计不清晰的亏,所以这块我特别想跟你好好说说。

租户实体定义:到底什么是“租户”?

先别笑,这个问题真得想清楚。很多人一上来就写代码,结果租户和用户、租户和组织混成一锅粥。

我个人习惯把租户定义为:一个独立的业务运营单元。它拥有自己的数据、配置、用户和资源。说白了,租户就是一个“小王国”,里面的人、权限、数据都是隔离的。

来看一个最简的租户实体定义:

// 租户核心实体
public class Tenant {
    private String tenantId;      // 租户唯一标识
    private String tenantName;    // 租户名称
    private String tenantCode;    // 租户编码(业务用)
    private String tenantType;    // 租户类型
    private String status;        // 状态
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

嗯,这里要注意:tenantIdtenantCode 是两回事。前者是系统内部用的 UUID,后者是业务上可读的编码,比如“ACME_CORP”。我见过有人把这两个混用,结果业务方要求改编码时,整个系统都得跟着改,那叫一个酸爽。

租户属性设计:别只盯着那几个字段

租户的属性远不止名字和编码。你想想看,一个租户需要配置多少东西?

我在项目中遇到过最头疼的情况:租户属性写死在代码里,每次新增属性都要改表结构、改代码、重新部署。后来我学乖了,用扩展属性来解决。

推荐两种方案:

方案 适用场景 优缺点
固定字段 + JSON扩展 核心属性固定,少量扩展属性 查询方便,但JSON字段不好做索引
EAV模型(实体-属性-值) 属性频繁变化,每个租户属性差异大 灵活,但查询复杂,性能差一些

我个人更倾向第一种。核心属性用固定字段,比如:

public class Tenant {
    // 核心字段
    private String tenantId;
    private String tenantName;
    private String tenantCode;
    private String status;
    
    // 扩展属性(JSON格式)
    private String extraProperties;
    // 示例:{"logoUrl":"xxx","contactPhone":"138xxxx","industry":"IT"}
}
小技巧:JSON扩展字段里,建议把常用的查询条件单独拎出来建索引。比如租户的“行业”属性经常用来做筛选,那就单独加个 industry 字段。

租户状态管理:别让租户“死”得不明不白

租户的状态管理,说白了就是租户的生命周期。我见过最粗暴的做法:只有“启用”和“禁用”两个状态。结果租户欠费了、到期了、被投诉了,全都混在一起处理。

我建议至少设计这几个状态:

  • 待激活:刚注册,还没完成配置
  • 试用中:可以正常使用,但有时间或功能限制
  • 已激活:正式付费用户,所有功能开放
  • 已冻结:欠费或违规,暂时限制访问
  • 已注销:租户主动退出,数据保留一段时间
  • 已删除:数据彻底清理(通常是软删除)

状态流转图大概是这样的:

待激活 → 试用中 → 已激活 → 已冻结 → 已注销 → 已删除
                ↑        ↓
                └────────┘  (解冻后回到已激活)
避坑指南:我曾经在项目里把“已冻结”和“已注销”的状态处理逻辑写在一起,结果冻结的租户数据被自动清理了。嗯,那次事故让我记住了:每个状态都要有独立的处理逻辑,别偷懒。

租户层级关系:单层还是多层?

这个问题其实取决于你的业务场景。我见过三种常见的层级模型:

  1. 扁平模型:所有租户平级,没有父子关系。适合SaaS工具类产品。
  2. 树形模型:租户有父子关系,比如集团-子公司-部门。适合企业级应用。
  3. 混合模型:既有平级租户,也有层级租户。适合平台型产品。

我个人习惯用树形模型 + 路径枚举的方式。为什么?因为查询某个租户的所有子租户时,性能特别好。

来看具体实现:

public class Tenant {
    private String tenantId;
    private String parentId;       // 父租户ID,根租户为null
    private String tenantPath;     // 路径枚举,如 "/root/child/grandchild"
    private Integer level;         // 层级深度,根为0
    private String status;
}

// 查询某个租户的所有子租户
SELECT * FROM tenant 
WHERE tenant_path LIKE CONCAT(#{tenantPath}, '/%')
AND status = 'ACTIVATED';

路径枚举的好处是:查询时只需要一次 LIKE 操作,不需要递归。坏处是:移动租户时需要更新所有子租户的路径。不过在实际业务中,租户移动的场景很少,所以这个代价可以接受。

核心要点:

  • 租户实体要区分内部ID和业务编码
  • 扩展属性用JSON字段,别动不动就改表结构
  • 状态管理要细化,每个状态有独立逻辑
  • 层级关系推荐路径枚举,查询性能好

好了,租户模型设计这块就聊到这儿。下一章咱们会讲租户的创建流程和初始化策略,到时候你会看到这些模型是怎么在实际代码里跑起来的。有什么问题欢迎随时交流。