4. 权限核心表结构实现:用MySQL把权限表建扎实

好,咱们进入实战环节。前面聊了那么多理论,现在该动手了。

权限系统的核心,说白了就是几张表。表结构设计得好,后面开发省一半的力。设计得不好……嗯,我见过一个项目,权限表改了七版,最后干脆重写了。

4.1 五张核心表,一个都不能少

我个人习惯,权限系统至少要有这五张表:

  • 用户表(sys_user)—— 谁在用系统
  • 角色表(sys_role)—— 什么身份
  • 权限表(sys_permission)—— 能干什么
  • 用户角色关联表(sys_user_role)—— 用户和角色的关系
  • 角色权限关联表(sys_role_permission)—— 角色和权限的关系

你想想看,这其实就是经典的RBAC模型。用户不直接跟权限挂钩,中间隔了一层角色。这样做的好处是啥?灵活。一个销售总监换部门了,改个角色绑定就行,不用一条条改权限。

核心原则:用户→角色→权限,三层结构。千万别让用户直接关联权限,否则后期维护会哭的。

4.2 字段类型选择:别小看这个

字段类型选不对,后面全是坑。我直接给结论,你们照着用就行。

字段 推荐类型 为什么这么选
用户ID BIGINT UNSIGNED 用户量大了,INT不够用。我见过一个CRM系统用户刚过20万,INT就溢出了……
角色ID INT UNSIGNED 角色一般不会超过几万个,INT够用
权限编码 VARCHAR(100) 用字符串做权限标识,比如"user:create",可读性强
状态字段 TINYINT(1) 0禁用1启用,别用VARCHAR存"enable"/"disable"
创建时间 DATETIME(3) 精确到毫秒,排查问题时有奇效
排序字段 SMALLINT 权限菜单排序用,够用就行

我的小技巧:所有表都加上 create_time、update_time、deleted 三个字段。deleted用TINYINT,做逻辑删除。为什么?物理删除了数据,后面想追溯都找不到。

4.3 建表SQL,直接拿去用

废话不多说,上代码。这是我经过多个项目打磨后的版本。

-- 用户表
CREATE TABLE sys_user (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL COMMENT '用户名',
    password VARCHAR(255) NOT NULL COMMENT '密码(加密后)',
    real_name VARCHAR(50) COMMENT '真实姓名',
    email VARCHAR(100) COMMENT '邮箱',
    phone VARCHAR(20) COMMENT '手机号',
    status TINYINT(1) DEFAULT 1 COMMENT '状态:1启用 0禁用',
    create_time DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
    update_time DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
    deleted TINYINT(1) DEFAULT 0,
    UNIQUE KEY uk_username (username),
    INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 角色表
CREATE TABLE sys_role (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    role_name VARCHAR(50) NOT NULL COMMENT '角色名称',
    role_code VARCHAR(50) NOT NULL COMMENT '角色编码',
    description VARCHAR(200) COMMENT '角色描述',
    sort_order SMALLINT DEFAULT 0 COMMENT '排序',
    status TINYINT(1) DEFAULT 1,
    create_time DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
    update_time DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
    deleted TINYINT(1) DEFAULT 0,
    UNIQUE KEY uk_role_code (role_code),
    INDEX idx_sort (sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

-- 权限表
CREATE TABLE sys_permission (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    parent_id INT UNSIGNED DEFAULT 0 COMMENT '父权限ID',
    perm_name VARCHAR(50) NOT NULL COMMENT '权限名称',
    perm_code VARCHAR(100) NOT NULL COMMENT '权限编码',
    perm_type TINYINT(1) DEFAULT 1 COMMENT '类型:1菜单 2按钮 3API',
    icon VARCHAR(50) COMMENT '图标',
    path VARCHAR(200) COMMENT '路由路径',
    sort_order SMALLINT DEFAULT 0,
    status TINYINT(1) DEFAULT 1,
    create_time DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
    update_time DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
    deleted TINYINT(1) DEFAULT 0,
    UNIQUE KEY uk_perm_code (perm_code),
    INDEX idx_parent (parent_id),
    INDEX idx_type (perm_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';

-- 用户角色关联表
CREATE TABLE sys_user_role (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL,
    role_id INT UNSIGNED NOT NULL,
    create_time DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
    UNIQUE KEY uk_user_role (user_id, role_id),
    INDEX idx_role_id (role_id),
    CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE,
    CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

-- 角色权限关联表
CREATE TABLE sys_role_permission (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    role_id INT UNSIGNED NOT NULL,
    permission_id INT UNSIGNED NOT NULL,
    create_time DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
    UNIQUE KEY uk_role_perm (role_id, permission_id),
    INDEX idx_perm_id (permission_id),
    CONSTRAINT fk_role_perm_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
    CONSTRAINT fk_role_perm_perm FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限关联表';

4.4 索引设计:别乱加,也别不加

索引这东西,加多了写操作慢,加少了查询慢。怎么平衡?

我总结了几条经验:

  • 唯一索引一定要加:用户名、角色编码、权限编码这些,业务上就不允许重复
  • 外键字段必须加索引:user_id、role_id、permission_id 这些,关联查询全靠它们
  • 状态字段加普通索引:经常要查"启用的用户"、"禁用的角色"
  • 排序字段加索引:sort_order 这种,列表排序时有用
  • 联合索引用起来:用户角色关联表里,(user_id, role_id) 联合唯一索引,既能保证不重复,又能加速查询

注意:别给每个字段都加索引。我曾经接手过一个项目,一张表20个字段,建了18个索引。插入一条数据要等2秒……后来砍掉了一半索引,写入性能提升了3倍。

4.5 外键约束:用还是不用?

这是个老生常谈的问题。我的观点是:小项目用,大项目不用

为什么?

  • 用外键的好处:数据一致性有保障,删了用户,关联的角色记录自动清理
  • 不用外键的好处:性能更好,分库分表没障碍,代码里控制逻辑更灵活

我个人习惯,在CRM这种企业级系统里,关联表用外键,主表之间不用。你看上面的SQL,sys_user_role 和 sys_role_permission 我都加了外键。为什么?因为这两个表就是做关联的,数据一致性最重要。

但 sys_user 和 sys_role 之间,我没加外键。用户和角色是独立实体,用代码逻辑控制更合适。

避坑指南:我曾经在一个项目里,所有表都加了外键。后来要做分库分表,外键成了最大的障碍。改了一个星期才搞定。从那以后,我就只在关联表上用外键了。

4.6 性能权衡:几个关键点

表结构设计好了,性能问题也得考虑。我列几个常见的坑:

  1. 字段长度别贪多:VARCHAR(255) 和 VARCHAR(50) 在索引性能上差别很大。够用就行。
  2. 用 INT 别用 VARCHAR 存ID:我见过有人用 VARCHAR 存用户ID,说是"方便扩展"。结果关联查询慢得像蜗牛。
  3. 时间字段用 DATETIME 别用 TIMESTAMP:TIMESTAMP 有2038年问题,CRM系统数据要存好多年的。
  4. 逻辑删除用 TINYINT:别用 CHAR(1) 存 'Y'/'N',TINYINT 在索引和存储上都更高效。
  5. 关联表用自增ID做主键:虽然联合唯一索引也能做主键,但自增ID在 InnoDB 里性能更好。

嗯,表结构这块就聊这么多。下一章咱们聊聊权限数据的初始化,怎么把菜单和权限点灌进去。