找回密码
 立即注册
首页 业界区 业界 多租户下的系统基础表设计

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

颛孙中 前天 15:00
多租户下的系统基础表设计

在设计 多租户进销存系统(SaaS) 时,核心是 租户隔离 + 权限控制 + 组织结构。
一般推荐的设计是 “租户 → 机构 → 角色 → 用户” 的层级结构,同时所有业务数据都带 tenant_id。
租户表(Tenant)
  1. sys_tenant
  2. ------
  3. id              bigint PK
  4. tenant_code     varchar(50) unique -- 租户编码
  5. tenant_name     varchar(200)  -- 租户名称
  6. contact_name    varchar(100)  -- 联系人姓名
  7. contact_phone   varchar(50)   -- 联系人电话
  8. contact_email   varchar(100)  -- 联系人邮箱
  9. expire_time     datetime      -- 过期时间
  10. status          int           -- 状态, 1启用, 0禁用
  11. remark          varchar(255)  -- 备注
  12. created_at      datetime      -- 创建时间
  13. 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
别人几个月后根本看不懂。
所以业务表推荐:用 业务状态枚举。
例如订单:
  1. DRAFT
  2. SUBMITTED
  3. APPROVED
  4. FINISHED
  5. CANCELLED
复制代码
示例:
status含义DRAFT草稿SUBMITTED已提交APPROVED已审核FINISHED完成CANCELLED作废优点:

  • 可读性强
  • 调试方便
  • API清晰
成熟系统一般这样设计:
  1. status        业务状态
  2. is_deleted    逻辑删除
复制代码
因此:
系统表 vs 业务表总结:
类型status设计系统表0禁用 1启用用户表0禁用 1正常 2锁定业务表业务枚举字符串机构表(Organization)
  1. sys_organization
  2. -------------
  3. id            bigint PK
  4. tenant_id     bigint
  5. org_code      varchar(50) unique -- 机构编码
  6. org_name      varchar(200) -- 机构名称
  7. pid     bigint -- 父节点
  8. path          varchar(500) -- 层级路径
  9. org_type      varchar(50)   -- 机构类型,如: company/department/store
  10. sort          varchar(50) -- 排序
  11. status        int -- 状态, 1启用, 0禁用
  12. remark        varchar(200) -- 备注
  13. is_deleted    int -- 逻辑删除, 1删除, 0未删除
  14. created_at      datetime
  15. updated_at      datetime
复制代码
说明:
  1. tenant
  2.   └── 总公司
  3.     ├── 财务部
  4.     ├── 销售部
  5.     └── 门店A
复制代码

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

  • 优点:查询非常快, SQL简单
  • 缺点:移动机构需要更新 path
ERP 中 机构移动很少,所以这是一个很好的方案。
用户表(User)
  1. sys_user
  2. -----
  3. id            bigint PK
  4. tenant_id     bigint
  5. username      varchar(100)     -- 用户名
  6. password      varchar(255)     -- 密码哈希
  7. salt          varchar(50)     -- 密码盐
  8. real_name     varchar(100)     -- 真实姓名
  9. nickname      varchar(100)     -- 昵称
  10. gender        varchar(10)      -- 性别
  11. avatar        varchar(200)    -- 头像
  12. mobile         varchar(50)     -- 手机号
  13. email         varchar(100)    -- 邮箱
  14. org_id        bigint          -- 机构ID
  15. position_id   bigint          -- 岗位ID
  16. login_count   int             -- 登录次数
  17. last_login_time datetime        -- 最后登录时间
  18. last_login_ip  varchar(50)     -- 最后登录IP
  19. is_super      int           -- 是否超级管理员, 1超级管理员, 0普通用户
  20. is_deleted       int            -- 逻辑删除, 1删除, 0未删除
  21. status        int            -- 状态, 1启用, 0禁用
  22. remark        varchar(200) -- 备注
  23. created_at      datetime -- 创建时间
  24. updated_at      datetime -- 更新时间
复制代码
说明:

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

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

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

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

角色是租户级的。
  1. sys_role
  2. -----
  3. id           bigint PK
  4. tenant_id    bigint
  5. role_code    varchar(50) unique -- 角色编码
  6. role_name    varchar(200) -- 角色名称
  7. role_type    varchar(50) -- 角色类型
  8. data_scope   varchar(50)   -- 数据权限
  9. sort         varchar(50) -- 排序
  10. status       int -- 状态, 1启用, 0禁用
  11. is_deleted   int -- 逻辑删除, 1删除, 0未删除
  12. created_at  datetime -- 创建时间
  13. updated_at  datetime -- 更新时间
复制代码
常见角色:

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

  • ALL              全部数据
  • ORG              本机构
  • ORG_CHILD        本机构及下级
  • SELF             仅自己
  • CUSTOM           指定机构
SQL示例:
1、data_scope = ALL 时;
  1. WHERE tenant_id = ?
复制代码
2、data_scope = ORG 时;
  1. WHERE tenant_id = ?
  2. AND org_id = current_org
复制代码
3、data_scope = ORG_CHILD 时;
  1. WHERE tenant_id = ?
  2. AND org_id IN (子机构列表)
复制代码
4、data_scope = SELF 时;
  1. WHERE tenant_id = ?
  2. AND created_by = current_user
复制代码
5、data_scope = CUSTOM 时;
  1. WHERE tenant_id = ?
  2. AND org_id IN (role_org)
复制代码
ERP 实际 SQL 拼接
  1. SELECT *
  2. FROM sales_order
  3. WHERE tenant_id = ?
  4. AND (
  5.       created_by = :user_id
  6.    OR org_id IN (:org_ids)
  7. )
复制代码
岗位表(Position)
  1. sys_position
  2. ----------
  3. id bigint PK
  4. tenant_id bigint
  5. position_code varchar(50) -- 岗位编码
  6. position_name varchar(200) -- 岗位名称
  7. org_id bigint -- 所属机构
  8. status int -- 状态, 1启用, 0禁用
  9. created_at datetime -- 创建时间
  10. updated_at datetime -- 更新时间
复制代码
角色主要解决 权限问题, 岗位主要解决 组织职责问题。岗位通常是 组织结构的一部分。
岗位通常是“一人一岗”(主岗位):

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

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

  • 兼职职责用 角色 解决。
权限表(Permission)

权限通常是菜单 + 按钮。
  1. sys_permission
  2. -----------
  3. id bigint PK
  4. system_code varchar(50) -- 系统类型
  5. perm_code varchar(50) PK -- 权限编码
  6. perm_name varchar(200) -- 权限名称
  7. perm_type  varchar(50)   -- 权限类型,如: menu/button/api
  8. pid bigint -- 父节点
  9. path varchar(500) -- 层级路径
  10. api_path varchar(200) -- API路径
  11. scope varchar(50) -- 权限范围,如:SYSTEM/TENANT
  12. module_code varchar(50) -- 模块编码
  13. resource_code varchar(50) -- 资源编码
  14. action_code varchar(50) -- 操作编码
  15. sort  varchar(50) -- 排序
  16. status int -- 状态, 1启用, 0禁用
  17. created_at datetime -- 创建时间
  18. 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:
  1. sys_permission   全局
  2. sys_menu         全局
复制代码
module_code + resource_code + action_code
这是 工业级权限编码拆分设计,后期非常好用。
例如:
module_coderesource_codeaction_codeperm_codesysuserviewsys:user:viewsysuseraddsys:user:addordersales_orderapproveorder:sales_order:approve比单纯 perm_code 更利于:

  • 代码生成
  • 权限树归类
  • 批量授权
  • 模块迁移
权限编码必须 统一规范:
推荐:模块:资源:操作
例如:
  1. user:list
  2. user:add
  3. user:update
  4. user:delete
  5. order:create
  6. order:approve
  7. order:cancel
复制代码
最终模型:
  1. Menu (导航)
  2. Permission (功能)
  3. User
  4. └─ Role
  5.       └─ Permission
  6.            ├─ Menu
  7.            ├─ Button
  8.            └─ API
复制代码
数据库表:
  1. sys_user
  2. sys_role
  3. sys_user_role
  4. sys_permission
  5. sys_role_permission
  6. sys_menu
  7. sys_menu_permission
复制代码
核心思想:

  • 菜单控制导航
  • 权限控制行为
  • 角色负责授权
前端菜单生成逻辑:
流程:
  1. 用户登录
  2. 获取角色
  3. 获取权限
  4. 根据权限加载菜单
复制代码
SQL示例:
  1. SELECT m.*
  2. FROM sys_menu m
  3. JOIN sys_menu_permission mp ON m.id = mp.menu_id
  4. JOIN sys_role_permission rp ON mp.permission_id = rp.permission_id
  5. WHERE rp.role_id IN (...)
复制代码
菜单表(Menu)
  1. sys_menu
  2. ------
  3. id bigint PK
  4. pid bigint -- 父节点
  5. system_code varchar(50) -- 系统类型
  6. menu_code varchar(50) PK -- 菜单编码
  7. menu_name varchar(200) -- 菜单名称
  8. tag varchar(50) -- 标签
  9. path varchar(200) -- 路由路径
  10. redirect varchar(200) -- 重定向路径
  11. is_iframe int -- 是否内嵌窗口,1内嵌窗口, 0不内嵌窗口
  12. out_link varchar(200) -- 外链地址
  13. is_keep_alive int -- 是否缓存,1缓存, 0不缓存
  14. is_affix int -- 是否固定,1固定, 0不固定
  15. is_expand int -- 是否展开
  16. url varchar(200) -- 界面Url地址
  17. is_eav_menu int -- 是否EAV菜单
  18. entity_type_id bigint -- 实体类型ID
  19. component varchar(200) -- 组件路径
  20. icon varchar(50) -- 图标
  21. sort varchar(50) -- 排序
  22. status int -- 状态, 1启用, 0禁用
  23. is_visible int -- 是否可见,1可见,0不可见
  24. created_at  datetime -- 创建时间
  25. updated_at datetime -- 更新时间
复制代码
系统类型表(SystemType)
  1. sys_system_type
  2. ------------
  3. id bigint PK
  4. system_code varchar(50) -- 系统类型编码
  5. system_name varchar(200) -- 系统类型名称
  6. remark varchar(200) -- 备注
  7. status int -- 状态, 1启用, 0禁用
  8. created_at datetime -- 创建时间
复制代码
系统类型表,用于区分不同系统之间的资源,如系统菜单、权限功能点等。
用户角色表(UserRole)
  1. sys_user_role
  2. ----------
  3. id bigint PK
  4. tenant_id bigint
  5. user_id bigint
  6. role_id bigint
复制代码
唯一约束:
  1. UNIQUE (tenant_id, user_id, role_id)
复制代码
企业级系统,中间表建议带 tenant_id

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

当使用 CUSTOM 时,需要指定机构:
  1. sys_role_org
  2. ---------
  3. id bigint PK
  4. tenant_id    bigint
  5. role_id bigint
  6. org_id bigint
复制代码
唯一性约束:
  1. UNIQUE (tenant_id, role_id, org_id)
复制代码
角色权限表(RolePermission)
  1. sys_role_permission
  2. ----------------
  3. id bigint PK
  4. tenant_id bigint
  5. role_id bigint
  6. perm_id bigint
复制代码
唯一性约束:
  1. UNIQUE (tenant_id, role_id, perm_id)
复制代码
菜单权限关系表:sys_menu_permission
  1. sys_menu_permission
  2. -
  3. id bigint PK
  4. tenant_id
  5. menu_id bigint
  6. perm_id bigint
复制代码
唯一性约束:
  1. UNIQUE (tenant_id, menu_id, perm_id)
复制代码
这样用户拿到角色权限后,就能推导出可见菜单。
菜单显示逻辑:用户登录后
  1. User
  2. -> UserRole
  3. -> RolePermission
  4. -> Permission
  5. -> MenuPermission
  6. -> Menu
复制代码
SQL思路:
  1. SELECT DISTINCT m.*
  2. FROM sys_menu m
  3. JOIN sys_menu_permission mp ON mp.menu_id = m.id
  4. JOIN sys_role_permission rp ON rp.permission_id = mp.permission_id
  5. JOIN sys_user_role ur ON ur.role_id = rp.role_id
  6. WHERE ur.user_id = :user_id
  7.   AND m.status = 1
  8.   AND m.visible = TRUE
复制代码
操作日志表(OperationLog)
  1. sys_operation_log
  2. --------------
  3. id bigint PK
  4. tenant_id bigint
  5. user_id bigint
  6. module varchar(250) -- 模块
  7. action varchar(50) -- 操作
  8. content varchar(2000) -- 内容
  9. ip varchar(50) -- IP地址
  10. created_at datetime -- 创建时间
复制代码
登录日志表(LoginLog)
  1. sys_login_log
  2. ----------
  3. id bigint PK
  4. tenant_id bigint
  5. user_id bigint
  6. content varchar(2000) -- 内容
  7. ip varchar(50) -- IP地址
  8. created_at
复制代码
字典类型表
  1. sys_dict_type
  2. ---------------
  3. id bigint PK
  4. pid bigint
  5. type_code varchar(50) -- 类型编码
  6. type_name varchar(200) -- 类型名称
  7. sort varchar(50) -- 排序
  8. remark varchar(200) -- 备注
  9. is_deleted int -- 逻辑删除, 1删除, 0未删除
  10. is_system  int -- 是否系统字典
  11. status  int     -- 状态, 1启用, 0禁用
  12. created_at datetime -- 创建时间
复制代码
字典项目表
  1. sys_dict_data
  2. ---------------
  3. id     bigint PK
  4. type_id bigint -- 类型ID
  5. item_name varchar(200) -- 项目名称
  6. item_value varchar(200) -- 项目值
  7. remark varchar(200) -- 备注
  8. sort  varchar(50) -- 排序
  9. status int -- 状态, 1启用, 0禁用
  10. is_deleted int -- 逻辑删除, 1删除, 0未删除
  11. created_at datetime -- 创建时间
复制代码
参数表(Parameter)
  1. sys_parameter
  2. ----------
  3. id
  4. tenant_id
  5. param_code
  6. param_name
  7. param_value
  8. status
  9. created_at
  10. updated_at
  11. ## 进销存业务表建议
  12. 核心业务表:
  13. product  
  14. category  
  15. warehouse  
  16. inventory  
  17. supplier  
  18. customer  
  19. purchase_order  
  20. purchase_order_item  
  21. sales_order  
  22. sales_order_item  
  23. stock_in  
  24. stock_out  
  25. 所有表都带 tenant_id 字段,用来标识当前数据所属的租户。
  26. 对于中间关联表,如UserRole设计,需要增加tenant_id字段。
  27. ``` sql
  28. user_role
  29. ----------
  30. id
  31. tenant_id
  32. user_id
  33. role_id
复制代码
查询,需要根据租户过滤,如:
  1. SELECT *
  2. FROM user_role
  3. WHERE tenant_id = ?
复制代码
避免跨租户脏数据,可以加唯一索引,逻辑更安全:
  1. CREATE UNIQUE INDEX idx_user_role_tenant_id_user_id_role_id ON user_role (tenant_id, user_id, role_id);
复制代码
SaaS ORM自动过滤更容易:
  1. query.filter(Model.tenant_id == current_tenant)
复制代码
删除租户数据更容易:
  1. 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
除了租户基础表外,租户还需要包括:租户套餐, 租户套餐关联。
租户套餐:
  1. tenant_package
  2. --------------
  3. id
  4. package_name
  5. user_limit
  6. storage_limit
  7. price
复制代码
租户套餐关联:
  1. tenant_package_rel
  2. -------------------
  3. tenant_id
  4. package_id
  5. start_time
  6. end_time
复制代码
ERP系统推荐ID方案

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

  • 全局唯一
  • 有时间顺序
  • 仍然是 BIGINT
因此使用BIGINT + Snowflake 方案。
结构:
  1. id BIGINT PRIMARY KEY
复制代码
生成:

  • Snowflake ID
优点:

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

相关推荐

您需要登录后才可以回帖 登录 | 立即注册