颛孙中 发表于 6 天前

多租户下的系统基础表设计

多租户下的系统基础表设计

在设计 多租户进销存系统(SaaS) 时,核心是 租户隔离 + 权限控制 + 组织结构。
一般推荐的设计是 “租户 → 机构 → 角色 → 用户” 的层级结构,同时所有业务数据都带 tenant_id。
租户表(Tenant)

sys_tenant
------
id            bigint PK
tenant_code   varchar(50) unique -- 租户编码
tenant_name   varchar(200)-- 租户名称
contact_name    varchar(100)-- 联系人姓名
contact_phone   varchar(50)   -- 联系人电话
contact_email   varchar(100)-- 联系人邮箱
expire_time   datetime      -- 过期时间

status          int         -- 状态, 1启用, 0禁用
remark          varchar(255)-- 备注

created_at      datetime      -- 创建时间
updated_at      datetime      -- 更新时间说明:

[*]一个租户 = 一个企业
[*]所有业务表都要带 tenant_id 字段,用来标识当前数据所属的租户
业务表统一规范:

[*]id
[*]tenant_id
[*]created_by
[*]created_at
[*]updated_by
[*]updated_at
[*]is_deleted
[*]org_id
status 字段用来标识当前数据是否有效。
系统表通常只需要表达 是否可用,状态很少变化。这些对象只有两件事:是否可用,是否禁用。用户有时需要 锁定状态,如密码输错次数过多。
因此,状态设计:

[*]0:禁用
[*]1:正常
[*]2: 锁定
业务表的 status 设计,业务单据通常有 生命周期。
例如订单:
草稿 → 提交 → 审核 → 完成 → 作废
如果用一个简单 status:

[*]0
[*]1
[*]2
[*]3
别人几个月后根本看不懂。
所以业务表推荐:用 业务状态枚举。
例如订单:
DRAFT
SUBMITTED
APPROVED
FINISHED
CANCELLED示例:
status含义DRAFT草稿SUBMITTED已提交APPROVED已审核FINISHED完成CANCELLED作废优点:

[*]可读性强
[*]调试方便
[*]API清晰
成熟系统一般这样设计:
status      业务状态
is_deleted    逻辑删除因此:
系统表 vs 业务表总结:
类型status设计系统表0禁用 1启用用户表0禁用 1正常 2锁定业务表业务枚举字符串机构表(Organization)

sys_organization
-------------
id            bigint PK
tenant_id   bigint

org_code      varchar(50) unique -- 机构编码
org_name      varchar(200) -- 机构名称

pid   bigint -- 父节点
path          varchar(500) -- 层级路径
org_type      varchar(50)   -- 机构类型,如: company/department/store

sort          varchar(50) -- 排序
status      int -- 状态, 1启用, 0禁用
remark      varchar(200) -- 备注
is_deleted    int -- 逻辑删除, 1删除, 0未删除
created_at      datetime
updated_at      datetime说明:
tenant
  └── 总公司
    ├── 财务部
    ├── 销售部
    └── 门店A

[*]机构表中,org_type 字段用来标识当前机构的类型,如:company/department/store
[*]机构表中,pid 字段用来标识当前机构的父节点,path 字段用来标识当前机构的层级路径
path 字段用来标识当前机构的层级路径。
idpath1121/231/341/2/451/2/561/3/6如果用户机构:org_id = 2, 查询:
SELECT id
FROM sys_organization
WHERE path LIKE '1/2/%'
   OR id = 2;

[*]优点:查询非常快, SQL简单
[*]缺点:移动机构需要更新 path
ERP 中 机构移动很少,所以这是一个很好的方案。
用户表(User)

sys_user
-----
id            bigint PK
tenant_id   bigint

username      varchar(100)   -- 用户名
password      varchar(255)   -- 密码哈希
salt          varchar(50)   -- 密码盐

real_name   varchar(100)   -- 真实姓名
nickname      varchar(100)   -- 昵称
gender      varchar(10)      -- 性别
avatar      varchar(200)    -- 头像
mobile         varchar(50)   -- 手机号
email         varchar(100)    -- 邮箱

org_id      bigint          -- 机构ID
position_id   bigint          -- 岗位ID
login_count   int             -- 登录次数
last_login_time datetime      -- 最后登录时间
last_login_ipvarchar(50)   -- 最后登录IP

is_super      int         -- 是否超级管理员, 1超级管理员, 0普通用户
is_deleted       int            -- 逻辑删除, 1删除, 0未删除
status      int            -- 状态, 1启用, 0禁用

remark      varchar(200) -- 备注
created_at      datetime -- 创建时间
updated_at      datetime -- 更新时间说明:

[*]用户表中,tenant_id 字段用来标识当前用户所属的租户
[*]org_id 字段用来标识当前用户所属的机构
is_super 表示:
系统超级管理员,不受任何权限控制
if user.is_super:
    允许所有操作
else:
    按 RBAC 权限判断避免误操作:

[*]如果超级管理员只是角色, 管理员可能在 UI 中误删:
[*]结果:系统没有管理员
[*]这个字段通常:不允许 UI 修改,只能数据库修改,安全性更高。
is_super 的作用:

[*]1️⃣ 绕过权限系统
[*]2️⃣ 防止系统锁死
[*]3️⃣ 提高权限判断性能
[*]4️⃣ 防止误删管理员角色
[*]5️⃣ 系统逃生通道
用户有时需要 锁定状态,如密码输错次数过多。
因此,状态设计:

[*]0:禁用
[*]1:正常
[*]2: 锁定
角色表(Role)

角色是租户级的。
sys_role
-----
id         bigint PK
tenant_id    bigint

role_code    varchar(50) unique -- 角色编码
role_name    varchar(200) -- 角色名称
role_type    varchar(50) -- 角色类型

data_scope   varchar(50)   -- 数据权限

sort         varchar(50) -- 排序
status       int -- 状态, 1启用, 0禁用
is_deleted   int -- 逻辑删除, 1删除, 0未删除

created_atdatetime -- 创建时间
updated_atdatetime -- 更新时间常见角色:

[*]管理员
[*]采购
[*]销售
[*]仓库
[*]财务
角色通常需要:data_scope,例如:

[*]ALL            全部数据
[*]ORG            本机构
[*]ORG_CHILD      本机构及下级
[*]SELF             仅自己
[*]CUSTOM         指定机构
SQL示例:
1、data_scope = ALL 时;
WHERE tenant_id = ?2、data_scope = ORG 时;
WHERE tenant_id = ?
AND org_id = current_org3、data_scope = ORG_CHILD 时;
WHERE tenant_id = ?
AND org_id IN (子机构列表)4、data_scope = SELF 时;
WHERE tenant_id = ?
AND created_by = current_user5、data_scope = CUSTOM 时;
WHERE tenant_id = ?
AND org_id IN (role_org)ERP 实际 SQL 拼接
SELECT *
FROM sales_order
WHERE tenant_id = ?
AND (
      created_by = :user_id
   OR org_id IN (:org_ids)
)岗位表(Position)

sys_position
----------
id bigint PK
tenant_id bigint

position_code varchar(50) -- 岗位编码
position_name varchar(200) -- 岗位名称

org_id bigint -- 所属机构

status int -- 状态, 1启用, 0禁用
created_at datetime -- 创建时间
updated_at datetime -- 更新时间角色主要解决 权限问题, 岗位主要解决 组织职责问题。岗位通常是 组织结构的一部分。
岗位通常是“一人一岗”(主岗位):

[*]在很多 ERP / OA / HR 系统里,岗位通常设计为“一人一个主岗位”,因此直接在用户表中放 position_id,而不是做多对多。
优点:

[*]表结构简单
[*]查询快
[*]UI简单
[*]符合大多数企业组织结构
有些企业确实存在 兼职岗位:

[*]兼职职责用 角色 解决。
权限表(Permission)

权限通常是菜单 + 按钮。
sys_permission
-----------
id bigint PK
system_code varchar(50) -- 系统类型

perm_code varchar(50) PK -- 权限编码
perm_name varchar(200) -- 权限名称
perm_typevarchar(50)   -- 权限类型,如: menu/button/api

pid bigint -- 父节点
path varchar(500) -- 层级路径
api_path varchar(200) -- API路径
scope varchar(50) -- 权限范围,如:SYSTEM/TENANT
module_code varchar(50) -- 模块编码
resource_code varchar(50) -- 资源编码
action_code varchar(50) -- 操作编码

sortvarchar(50) -- 排序
status int -- 状态, 1启用, 0禁用

created_at datetime -- 创建时间
updated_at datetime -- 更新时间说明:
perm_type 枚举:

[*]menu:菜单
[*]button:按钮
[*]api:API
例如:
perm_codeperm_typeuser:add按钮user:delete按钮/api/user/listAPI这样可以:

[*]控制前端菜单
[*]控制按钮
[*]控制接口权限
scope 权限作用范围:

[*]SYSTEM:系统级
[*]TENANT:租户级
例如:
perm_codescopetenant:createSYSTEMuser:addTENANT权限表通常 不带 tenant_id:
sys_permission   全局
sys_menu         全局module_code + resource_code + action_code
这是 工业级权限编码拆分设计,后期非常好用。
例如:
module_coderesource_codeaction_codeperm_codesysuserviewsys:user:viewsysuseraddsys:user:addordersales_orderapproveorder:sales_order:approve比单纯 perm_code 更利于:

[*]代码生成
[*]权限树归类
[*]批量授权
[*]模块迁移
权限编码必须 统一规范:
推荐:模块:资源:操作
例如:
user:list
user:add
user:update
user:delete

order:create
order:approve
order:cancel最终模型:
Menu (导航)
Permission (功能)

User
└─ Role
      └─ Permission
         ├─ Menu
         ├─ Button
         └─ API数据库表:
sys_user
sys_role
sys_user_role
sys_permission
sys_role_permission
sys_menu
sys_menu_permission核心思想:

[*]菜单控制导航
[*]权限控制行为
[*]角色负责授权
前端菜单生成逻辑:
流程:
用户登录

获取角色

获取权限

根据权限加载菜单SQL示例:
SELECT m.*
FROM sys_menu m
JOIN sys_menu_permission mp ON m.id = mp.menu_id
JOIN sys_role_permission rp ON mp.permission_id = rp.permission_id
WHERE rp.role_id IN (...)菜单表(Menu)

sys_menu
------
id bigint PK
pid bigint -- 父节点
system_code varchar(50) -- 系统类型

menu_code varchar(50) PK -- 菜单编码
menu_name varchar(200) -- 菜单名称
tag varchar(50) -- 标签

path varchar(200) -- 路由路径
redirect varchar(200) -- 重定向路径
is_iframe int -- 是否内嵌窗口,1内嵌窗口, 0不内嵌窗口
out_link varchar(200) -- 外链地址
is_keep_alive int -- 是否缓存,1缓存, 0不缓存
is_affix int -- 是否固定,1固定, 0不固定
is_expand int -- 是否展开
url varchar(200) -- 界面Url地址

is_eav_menu int -- 是否EAV菜单
entity_type_id bigint -- 实体类型ID

component varchar(200) -- 组件路径
icon varchar(50) -- 图标

sort varchar(50) -- 排序
status int -- 状态, 1启用, 0禁用
is_visible int -- 是否可见,1可见,0不可见

created_atdatetime -- 创建时间
updated_at datetime -- 更新时间系统类型表(SystemType)

sys_system_type
------------
id bigint PK

system_code varchar(50) -- 系统类型编码
system_name varchar(200) -- 系统类型名称
remark varchar(200) -- 备注

status int -- 状态, 1启用, 0禁用
created_at datetime -- 创建时间系统类型表,用于区分不同系统之间的资源,如系统菜单、权限功能点等。
用户角色表(UserRole)

sys_user_role
----------
id bigint PK
tenant_id bigint

user_id bigint
role_id bigint唯一约束:
UNIQUE (tenant_id, user_id, role_id)企业级系统,中间表建议带 tenant_id

[*]避免跨租户脏数据
[*]查询更高效
[*]索引优化更直接
tenant_id 可以冗余,但利大于弊。
角色机构表(RoleOrg)

当使用 CUSTOM 时,需要指定机构:
sys_role_org
---------
id bigint PK
tenant_id    bigint

role_id bigint
org_id bigint唯一性约束:
UNIQUE (tenant_id, role_id, org_id)角色权限表(RolePermission)

sys_role_permission
----------------
id bigint PK
tenant_id bigint

role_id bigint
perm_id bigint唯一性约束:
UNIQUE (tenant_id, role_id, perm_id)菜单权限关系表:sys_menu_permission

sys_menu_permission
-
id bigint PK
tenant_id

menu_id bigint
perm_id bigint唯一性约束:
UNIQUE (tenant_id, menu_id, perm_id)这样用户拿到角色权限后,就能推导出可见菜单。
菜单显示逻辑:用户登录后
User
-> UserRole
-> RolePermission
-> Permission
-> MenuPermission
-> MenuSQL思路:
SELECT DISTINCT m.*
FROM sys_menu m
JOIN sys_menu_permission mp ON mp.menu_id = m.id
JOIN sys_role_permission rp ON rp.permission_id = mp.permission_id
JOIN sys_user_role ur ON ur.role_id = rp.role_id
WHERE ur.user_id = :user_id
AND m.status = 1
AND m.visible = TRUE操作日志表(OperationLog)

sys_operation_log
--------------
id bigint PK
tenant_id bigint
user_id bigint

module varchar(250) -- 模块
action varchar(50) -- 操作

content varchar(2000) -- 内容

ip varchar(50) -- IP地址
created_at datetime -- 创建时间登录日志表(LoginLog)

sys_login_log
----------
id bigint PK
tenant_id bigint
user_id bigint

content varchar(2000) -- 内容
ip varchar(50) -- IP地址
created_at字典类型表

sys_dict_type
---------------
id bigint PK
pid bigint

type_code varchar(50) -- 类型编码
type_name varchar(200) -- 类型名称
sort varchar(50) -- 排序
remark varchar(200) -- 备注

is_deleted int -- 逻辑删除, 1删除, 0未删除
is_systemint -- 是否系统字典
statusint   -- 状态, 1启用, 0禁用

created_at datetime -- 创建时间字典项目表

sys_dict_data
---------------
id   bigint PK
type_id bigint -- 类型ID

item_name varchar(200) -- 项目名称
item_value varchar(200) -- 项目值
remark varchar(200) -- 备注

sortvarchar(50) -- 排序
status int -- 状态, 1启用, 0禁用
is_deleted int -- 逻辑删除, 1删除, 0未删除

created_at datetime -- 创建时间参数表(Parameter)

sys_parameter
----------
id
tenant_id

param_code
param_name

param_value

status

created_at
updated_at

## 进销存业务表建议

核心业务表:

product
category
warehouse
inventory
supplier
customer
purchase_order
purchase_order_item
sales_order
sales_order_item
stock_in
stock_out

所有表都带 tenant_id 字段,用来标识当前数据所属的租户。
对于中间关联表,如UserRole设计,需要增加tenant_id字段。

``` sql
user_role
----------
id
tenant_id

user_id
role_id查询,需要根据租户过滤,如:
SELECT *
FROM user_role
WHERE tenant_id = ?避免跨租户脏数据,可以加唯一索引,逻辑更安全:
CREATE UNIQUE INDEX idx_user_role_tenant_id_user_id_role_id ON user_role (tenant_id, user_id, role_id);SaaS ORM自动过滤更容易:
query.filter(Model.tenant_id == current_tenant)删除租户数据更容易:
DELETE FROM user_role WHERE tenant_id = ?大多数企业系统 全部中间表都会带 tenant_id。例如:

[*]user_role
[*]role_permission
[*]user_org
[*]role_org
[*]user_position
多租户系统设计原则:
只要是业务表,一律带 tenant_id 字段,并且查询时需要根据租户过滤。
什么时候可以不加 tenant_id?只有一种情况:全局表,这种是 平台共享数据,不属于某个租户吗,如:租户表,字典表、参数表、系统配置表、菜单表等。。

[*]tenant
[*]dictionary
[*]parameter
[*]permission
[*]menu
[*]country
[*]currency
除了租户基础表外,租户还需要包括:租户套餐, 租户套餐关联。
租户套餐:
tenant_package
--------------
id
package_name
user_limit
storage_limit
price租户套餐关联:
tenant_package_rel
-------------------
tenant_id
package_id
start_time
end_timeERP系统推荐ID方案

大多数 ERP 系统推荐:
主键ID:BIGINT
业务编码:VARCHAR主键ID使用分布式ID:
Snowflake
Leaf
Sonyflake生成 64bit BIGINT:
178923741239123特点:

[*]全局唯一
[*]有时间顺序
[*]仍然是 BIGINT
因此使用BIGINT + Snowflake 方案。
结构:
id BIGINT PRIMARY KEY生成:

[*]Snowflake ID
优点:

[*]分布式
[*]高性能
[*]顺序索引
ERP数据库标准结构:
典型表:
id BIGINT PRIMARY KEY
tenant_id BIGINT
created_at DATETIME
updated_at DATETIME不要在id中,把 GUID 存 VARCHAR。
例如:
550e8400-e29b-41d4-a716-446655440000
什么时候用 GUID ?
1 微服务跨系统ID
例如:
订单服务
支付服务
物流服务2 离线客户端
例如:
移动端
离线同步3 数据合并
例如:
多数据库合并
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 多租户下的系统基础表设计