第三章 数据库模型设计(上):用户与租户表设计、订阅计划表设计、订阅实例表设计

好,咱们直接进入正题。数据库模型设计,说白了就是给整个计费系统打地基。地基不稳,后面盖多少层楼都得塌。我见过太多项目,上线后因为表结构设计不合理,改得死去活来。今天咱们就把这三张核心表聊透。

3.1 用户与租户表设计:多租户的基石

先说说租户。SaaS 产品里,租户就是你的客户。一个公司、一个团队,甚至一个人,都可以是一个租户。用户呢?用户是租户下的具体使用者。一个租户可以有多个用户,一个用户也可以属于多个租户(比如一个人同时在两家公司用你的产品)。

嗯,这里要注意:用户和租户是多对多的关系。千万别设计成一对多,否则后面做权限管理、切换租户时,你会哭的。

3.1.1 租户表(Tenant)

我个人习惯,租户表字段尽量精简。核心字段如下:

字段名 类型 说明
id bigint 主键,自增
tenant_id varchar(32) 业务唯一标识,UUID 或雪花算法生成
name varchar(128) 租户名称,比如公司名
status tinyint 状态:0-试用,1-活跃,2-冻结,3-已删除
created_at datetime 创建时间
updated_at datetime 更新时间
我的小技巧:tenant_id 用 varchar 类型,别用 int。为什么?因为后面做数据隔离、分库分表时,字符串比数字更灵活。我在项目中吃过 int 的亏,后来全改成 varchar 了。

3.1.2 用户表(User)

用户表稍微复杂点。除了基本信息,还要考虑跨租户的场景。

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(32) NOT NULL COMMENT '用户唯一标识',
  `email` varchar(255) DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  `password_hash` varchar(255) NOT NULL COMMENT '密码哈希',
  `display_name` varchar(128) DEFAULT NULL COMMENT '显示名称',
  `avatar_url` varchar(512) DEFAULT NULL COMMENT '头像地址',
  `status` tinyint(4) DEFAULT '1' COMMENT '状态:0-禁用,1-启用',
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_id` (`user_id`),
  UNIQUE KEY `uk_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

你看,用户表本身不存租户信息。那用户和租户怎么关联?靠中间表。

3.1.3 用户租户关联表(UserTenant)

这张表很关键。它记录了用户属于哪个租户,以及在该租户下的角色。

字段名 类型 说明
id bigint 主键
user_id varchar(32) 用户 ID
tenant_id varchar(32) 租户 ID
role varchar(32) 角色:admin、member、viewer 等
joined_at datetime 加入时间
避坑指南:我曾经设计时把 role 字段直接放到了 user 表里,结果一个用户在不同租户下需要不同角色,改得我头皮发麻。记住,角色是跟租户绑定的,不是跟用户绑定的

3.2 订阅计划表(Plan)设计:定价的核心

订阅计划,就是你的产品卖什么、怎么卖。说白了,就是定价模型。我见过最简单的 Plan 表只有三四个字段,也见过几十个字段的。我的建议是:保持核心,扩展用配置

3.2.1 核心字段设计

字段名 类型 说明
id bigint 主键
plan_id varchar(32) 计划唯一标识
name varchar(128) 计划名称,如「基础版」「专业版」
description text 计划描述
billing_cycle varchar(16) 计费周期:monthly、yearly、quarterly
price decimal(10,2) 价格,单位元
currency varchar(3) 货币类型,如 CNY、USD
status tinyint 状态:0-下架,1-上架
sort_order int 排序,越小越靠前

你想想看,为什么要有 sort_order?因为前端展示时,你肯定希望「基础版」「专业版」「企业版」按顺序排,而不是按创建时间。这个字段虽然小,但很实用。

3.2.2 功能配置:用 JSON 还是用关联表?

这是个大问题。每个 Plan 包含哪些功能?比如存储空间、API 调用次数、团队成员数等。

我个人习惯:核心功能用字段,扩展功能用 JSON。比如:

-- 核心字段
`max_users` int DEFAULT NULL COMMENT '最大用户数',
`max_storage_gb` int DEFAULT NULL COMMENT '最大存储空间(GB)',
`api_calls_limit` int DEFAULT NULL COMMENT 'API调用次数限制',

-- 扩展配置
`features` json DEFAULT NULL COMMENT '功能配置,如 {"export_csv": true, "custom_domain": false}'

为什么这么搞?因为核心功能是计费逻辑里经常用到的,比如判断用户数是否超限。如果放在 JSON 里,每次都要解析,性能差不说,代码也难看。而一些开关型功能,用 JSON 存着,灵活又方便。

重点:Plan 表一旦发布,尽量不要修改价格和核心配置。真要改,建议下架旧 Plan,创建新 Plan。否则历史订阅数据会乱套。我在项目中就遇到过改价格导致续费金额对不上的惨案。

3.3 订阅实例表(Subscription)设计:计费的核心

订阅实例,就是用户买了哪个 Plan,什么时候到期,现在什么状态。这张表是计费系统的核心,所有计费逻辑都围着它转。

3.3.1 核心字段

字段名 类型 说明
id bigint 主键
subscription_id varchar(32) 订阅唯一标识
tenant_id varchar(32) 租户 ID
plan_id varchar(32) 订阅的计划 ID
status varchar(16) 状态:active、expired、canceled、paused、trialing
start_at datetime 订阅开始时间
end_at datetime 订阅结束时间
trial_end_at datetime 试用期结束时间(如有)
canceled_at datetime 取消时间
auto_renew tinyint 是否自动续费:0-否,1-是

3.3.2 状态机设计

订阅状态的变化是有规律的。你不能从 active 直接跳到 canceled,中间可能还有 grace period(宽限期)。我建议用状态机来约束:

状态流转:
trialing -> active (试用转正)
active -> expired (到期未续费)
active -> canceled (用户主动取消)
active -> paused (暂停订阅)
paused -> active (恢复订阅)
expired -> active (过期后重新激活)
我曾经踩过的坑:一开始没做状态机,代码里到处 if-else 判断状态。结果有个场景:用户取消订阅后,在有效期内又恢复了,状态直接变成了 active,但 end_at 没更新。导致用户多用了半个月没付钱。后来我加了状态机,每个流转都校验,再没出过这种问题。

3.3.3 计费周期与快照

订阅实例里还有一个重要概念:计费周期快照。什么意思?就是用户下单时,Plan 的价格是 100 元/月。一个月后 Plan 涨价到 120 元/月。那用户续费时按哪个价格?

答案是:按订阅时的价格。所以 Subscription 表里需要存一份价格快照:

-- 价格快照字段
`plan_price_at_subscription` decimal(10,2) COMMENT '订阅时的计划价格',
`billing_cycle_at_subscription` varchar(16) COMMENT '订阅时的计费周期'

这样,无论 Plan 怎么变,用户的订阅价格都不受影响。直到他主动升级或降级 Plan,才会使用新价格。

3.4 三张表的关系总结

来,咱们捋一捋这三张表的关系:

  • Tenant 是客户,User 是使用者,通过 UserTenant 关联
  • Plan 是产品定价,Subscription 是客户买了哪个 Plan
  • 一个 Tenant 可以有多个 Subscription(比如买了基础版,又买了附加包)
  • 一个 Subscription 只能关联一个 Plan

说白了,就是:谁(Tenant)买了什么(Plan),什么时候到期(Subscription)。就这么简单。

最后说一句:数据库设计没有银弹。我给你的这套方案,是我在多个 SaaS 项目里打磨出来的。但你的业务场景可能不一样,该加字段就加,该拆表就拆。核心原则就一个:可扩展、可追溯、不丢数据

下一章,咱们聊订单表和支付记录表的设计。那才是真正跟钱打交道的部分,到时候再细说。