第三章 数据库模型设计(上):用户与租户表设计、订阅计划表设计、订阅实例表设计
好,咱们直接进入正题。数据库模型设计,说白了就是给整个计费系统打地基。地基不稳,后面盖多少层楼都得塌。我见过太多项目,上线后因为表结构设计不合理,改得死去活来。今天咱们就把这三张核心表聊透。
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 | 更新时间 |
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 | 加入时间 |
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 存着,灵活又方便。
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 (过期后重新激活)
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)。就这么简单。
下一章,咱们聊订单表和支付记录表的设计。那才是真正跟钱打交道的部分,到时候再细说。