多租户下的系统基础表设计
在设计 多租户进销存系统(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 字段用来标识当前数据是否有效。
系统表通常只需要表达 是否可用,状态很少变化。这些对象只有两件事:是否可用,是否禁用。用户有时需要 锁定状态,如密码输错次数过多。
因此,状态设计:
业务表的 status 设计,业务单据通常有 生命周期。
例如订单:
草稿 → 提交 → 审核 → 完成 → 作废
如果用一个简单 status:
别人几个月后根本看不懂。
所以业务表推荐:用 业务状态枚举。
例如订单:- DRAFT
- SUBMITTED
- APPROVED
- FINISHED
- CANCELLED
复制代码 示例:
status含义DRAFT草稿SUBMITTED已提交APPROVED已审核FINISHED完成CANCELLED作废优点:
成熟系统一般这样设计:- 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_ip varchar(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️⃣ 系统逃生通道
用户有时需要 锁定状态,如密码输错次数过多。
因此,状态设计:
角色表(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_at datetime -- 创建时间
- updated_at datetime -- 更新时间
复制代码 常见角色:
角色通常需要:data_scope,例如:
- ALL 全部数据
- ORG 本机构
- ORG_CHILD 本机构及下级
- SELF 仅自己
- CUSTOM 指定机构
SQL示例:
1、data_scope = ALL 时;2、data_scope = ORG 时;- WHERE tenant_id = ?
- AND org_id = current_org
复制代码 3、data_scope = ORG_CHILD 时;- WHERE tenant_id = ?
- AND org_id IN (子机构列表)
复制代码 4、data_scope = SELF 时;- WHERE tenant_id = ?
- AND created_by = current_user
复制代码 5、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_type varchar(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) -- 操作编码
- sort varchar(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 权限作用范围:
例如:
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_at datetime -- 创建时间
- 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
- -> Menu
复制代码 SQL思路:- 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_system int -- 是否系统字典
- status int -- 状态, 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) -- 备注
- sort varchar(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_time
复制代码 ERP系统推荐ID方案
大多数 ERP 系统推荐:主键ID使用分布式ID:生成 64bit BIGINT:特点:
因此使用BIGINT + Snowflake 方案。
结构:生成:
优点:
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 数据合并
例如:来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |