Skip to content

想象一个场景:一家公司注册了你的 SaaS,14 天试用期内录了 500 条数据,到期后没付费。三个月过去,这 500 条数据还躺在共享数据库里占着地方,他的租户还能登录吗?数据该删吗?如果第四个月他又回来了呢?

这就是租户生命周期管理的问题:租户从注册到离开,每个阶段“能干什么、数据怎么处理”,得有一套明确的状态机管着。隔离解决的是“租户之间看不见”,生命周期解决的是“一个租户的时间轴上的每个阶段怎么对待”。

一、先立状态机

一个典型的租户生命周期长这样:

租户生命周期状态机

五个业务状态 + 一个终点:

状态含义登录数据
试用中免费体验,有到期日✅(可限功能)正常读写
正式生效付费客户正常读写
冻结欠费 / 违规临时处理❌ 拦截原样保留
注销申请客户主动离开或长期欠费❌ 拦截进入倒计时
数据保留期宽限窗口❌(或只读导出)保留,期满删除
已删除终点清除

设计里最关键的两个决策,一个一个说。

二、决策一:冻结 ≠ 删数据

客户欠费了,最直觉的冲动是把他的数据清掉“释放资源”。千万别。理由很实际:

  1. 客户会回来。欠费冻结的客户有相当比例会续费——数据原样躺着,续费完立刻恢复,体验是“什么都没发生过”。数据没了,这个客户基本就丢了。
  2. 对账和纠纷需要数据。客户欠费金额有争议时,业务数据是凭证。
  3. 删除是不可逆动作,凡不可逆,就要配最重的防护(见决策三)。

所以冻结的正确实现是只禁入口,不动数据

java
// 登录链路上的租户校验(三查:存在 / 停用 / 过期)
private void checkTenant(String tenantId) {
    SysTenant tenant = tenantMapper.selectById(tenantId);
    if (tenant == null) {
        throw new TenantException("租户不存在");
    }
    if (TenantStatus.FROZEN.eq(tenant.getStatus())) {
        throw new TenantException("租户已冻结,请联系管理员续费");
    }
    if (tenant.getExpireTime() != null
            && tenant.getExpireTime().isBefore(LocalDateTime.now())) {
        throw new TenantException("租户已过期,请续费后使用");
    }
}

这段逻辑挂在登录流程里——校验不过直接拦在门外,比让用户登录成功后到处点到处报错体面得多。RuoYi-Vue-Plus 的租户模块就是这么做的(《多租户数据隔离》一文里 checkTenant 的完整链路有展开)。

到期日判定有个细节:到期当天应该还算有效(含尾)。用户看到“9 月 10 日到期”,9 月 10 日当天被拦在门外,投诉是必然的。判定写成 expireTime.isBefore(now) 而不是 !expireTime.isAfter(now),一天之差,体验全不同。

三、决策二:到期不靠人盯,靠定时任务

试用到期、订阅到期、长期欠费转注销——这些时间驱动的状态迁移,不能指望管理员记得去点按钮。标准做法是定时任务扫描

java
// 每天凌晨扫描:试用/订阅到期的租户批量冻结
@Scheduled(cron = "0 30 2 * * ?")
public void freezeExpiredTenants() {
    // 到期日早于今天的租户 → 批量置为冻结
    // 只改 status 字段,不碰业务数据
    tenantMapper.updateStatusByExpireTime(TenantStatus.FROZEN, LocalDateTime.now());
}

两个实现要点:

  • 幂等:任务重跑、多实例并发跑,结果必须一致(update ... where status = 正式 and expire_time < now 天然幂等)。
  • 配套动作一起做:冻结时往往要停掉该租户的调度任务、撤销其 API 凭证,这些可以走事件(发布“租户被冻结”事件,订阅方各自处理),避免扫描任务里塞满业务逻辑。

这套“定时扫描 + 事件通知”的组合,和《分布式任务调度:从 @Scheduled 到 SnailJob》里讲的多实例防重问题是同一件事——多实例部署时扫描任务要加分布式锁或改用调度平台。

四、决策三:注销要留“后悔药”

注销流程的核心是一个缓冲期——数据保留期

注销申请 → 数据保留期(如 30 天)→ 期满物理删除

保留期内做三件事:

  1. 数据导出包。给客户生成一份他的全部业务数据(数据库导出 + 附件打包),既是对客户的交代,也是对自己免责。
  2. 允许反悔。政策上可以规定“保留期内恢复,数据原样回来”——成本几乎为零(只是把状态改回来),但能救回冲动注销的客户。
  3. 期满删除走审计。真到删除那一步,记录谁、什么时候、删了哪个租户的哪些数据。物理删除前二次确认,删除动作本身幂等(重跑不报错)。

还有一个容易漏的角色:租户内的用户怎么办。注销后这些用户的登录会话要全部失效(他们的 token 里还绑着这个租户 ID,见多租户文章里的会话绑定机制),否则可能出现“注销租户的幽灵用户还能调接口”。

五、常见误区

① 试用不限功能只限时间。 试用期能用旗舰版全部功能,转正后功能缩水——落差感会让转化率崩掉。更好的姿势是试用期就用目标套餐的配置,让客户按“续费后”的体验做决策。

② 冻结只改了登录校验。 忘了冻结租户还有 API 凭证、开放接口、消息推送这些旁路入口——网页进不去,但他的集成程序还在正常调你的 API。所有入口都要过同一个租户状态校验。

③ 到期判定时区混乱。 数据库存 UTC、代码用本地时间比对,到期日就变成了“随缘过期”。统一一种时区(一般就存服务器本地时间或统一 UTC),在系统里从一开始就定死。

④ 把“降级套餐”和“冻结”混为一谈。 套餐降级是功能开关问题(见《SaaS 套餐功能开关设计》),冻结是账号状态问题,两条线正交:冻结的客户降级无意义,降级的客户随时能登录。

系列导航