第 4 章:数据库设计
学习目标
- 设计 RBAC 数据库表结构
- 编写规范的建表 SQL
- 理解索引、字符集、引擎的选择策略
一、表清单
TaskFlow 涉及 9 张表,分三类:
| 类型 | 表名 | 作用 |
|---|---|---|
| 业务表 | sys_user / sys_role / sys_menu / sys_dept / sys_dict | 核心数据 |
| 关系表 | sys_user_role / sys_role_menu / sys_role_dept | 多对多关联 |
| 审计表 | sys_operation_log / sys_login_log | 日志审计 |
⚠️ 坑 1:数据库设计的第一原则是"满足业务,不预留"。不要在第一版就预留"以后用得上"的字段,后期重构的成本远低于现在空字段的维护成本。
二、用户表(sys_user)
sql
CREATE TABLE `sys_user` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` VARCHAR(50) NOT NULL COMMENT '登录名',
`password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt)',
`nickname` VARCHAR(50) COMMENT '昵称',
`email` VARCHAR(100) COMMENT '邮箱',
`phone` VARCHAR(20) COMMENT '手机号',
`avatar` VARCHAR(255) COMMENT '头像URL',
`gender` TINYINT DEFAULT 0 COMMENT '0=未知 1=男 2=女',
`status` TINYINT DEFAULT 1 COMMENT '0=禁用 1=正常 2=锁定',
`dept_id` BIGINT COMMENT '部门ID',
`last_login_time` DATETIME COMMENT '最后登录时间',
`last_login_ip` VARCHAR(50) COMMENT '最后登录IP',
`create_by` BIGINT COMMENT '创建人',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` BIGINT COMMENT '更新人',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` TINYINT DEFAULT 0 COMMENT '逻辑删除',
`version` INT DEFAULT 0 COMMENT '乐观锁',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`, `is_deleted`),
KEY `idx_dept` (`dept_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';关键设计:
password长度VARCHAR(100)够 BCrypt 存($2a$10$xxx,60 字符)- 唯一键包含
is_deleted是软删场景必踩坑:不带上,删了的用户名无法重复使用 version字段用于 MyBatis Plus 乐观锁,防止并发覆盖
java
// 软删字段踩坑示例
// 错误:删除用户后,无法重建同名用户
ALTER TABLE sys_user DROP INDEX uk_username;
// 正确:联合唯一键包含 is_deleted
UNIQUE KEY `uk_username` (`username`, `is_deleted`)三、角色表(sys_role)
sql
CREATE TABLE `sys_role` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL COMMENT '角色名',
`code` VARCHAR(50) NOT NULL COMMENT '角色编码,ROLE_XXX',
`description` VARCHAR(255) COMMENT '描述',
`sort` INT DEFAULT 0 COMMENT '排序',
`status` TINYINT DEFAULT 1 COMMENT '0=禁用 1=正常',
`data_scope` TINYINT DEFAULT 4 COMMENT '数据权限:1-全部 2-本部门及下级 3-本部门 4-仅本人 5-自定义',
`create_by` BIGINT,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_by` BIGINT,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`, `is_deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';角色编码约束(code 字段):
- 必须
ROLE_前缀(ROLE_ADMIN、ROLE_USER) - 校验注解:
@Pattern(regexp = "^ROLE_[A-Z_]+$")
四、菜单表(sys_menu)
sql
CREATE TABLE `sys_menu` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`parent_id` BIGINT DEFAULT 0 COMMENT '父菜单ID,0=顶级',
`name` VARCHAR(50) NOT NULL COMMENT '菜单名',
`type` TINYINT NOT NULL COMMENT '1=目录 2=菜单 3=按钮',
`permission` VARCHAR(100) COMMENT '权限标识(按钮才有)',
`path` VARCHAR(200) COMMENT '路由路径',
`component` VARCHAR(200) COMMENT '前端组件路径',
`icon` VARCHAR(50) COMMENT '图标',
`sort` INT DEFAULT 0 COMMENT '排序',
`visible` TINYINT DEFAULT 1 COMMENT '0=隐藏 1=显示',
`status` TINYINT DEFAULT 1 COMMENT '0=禁用 1=正常',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_parent` (`parent_id`),
KEY `idx_type` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单权限表';菜单三类型说明:
| type | 含义 | 示例 |
|---|---|---|
| 1 | 目录 | "系统管理"(左侧一级) |
| 2 | 菜单 | "用户管理"(/system/user) |
| 3 | 按钮 | "用户新增"(permission=user:create) |
五、部门表(sys_dept)
sql
CREATE TABLE `sys_dept` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`parent_id` BIGINT DEFAULT 0,
`name` VARCHAR(50) NOT NULL,
`code` VARCHAR(50) COMMENT '部门编码',
`leader` VARCHAR(50) COMMENT '负责人',
`phone` VARCHAR(20),
`email` VARCHAR(100),
`sort` INT DEFAULT 0,
`status` TINYINT DEFAULT 1,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_parent` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门表';树形结构:通过 parent_id 自关联实现。parent_id=0 表示顶级。
⚠️ 坑 2:不要在同一张表里存"父级路径"(如
path=/1/2/3/)。这违背范式,且每次部门变动都要更新所有祖先节点。直接用parent_id递归查就行,效率问题加缓存解决。
六、关系表(中间表)
sql
-- 用户-角色
CREATE TABLE `sys_user_role` (
`user_id` BIGINT NOT NULL,
`role_id` BIGINT NOT NULL,
PRIMARY KEY (`user_id`, `role_id`),
KEY `idx_role` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关系';
-- 角色-菜单
CREATE TABLE `sys_role_menu` (
`role_id` BIGINT NOT NULL,
`menu_id` BIGINT NOT NULL,
PRIMARY KEY (`role_id`, `menu_id`),
KEY `idx_menu` (`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关系';
-- 角色-部门(数据权限自定义时用)
CREATE TABLE `sys_role_dept` (
`role_id` BIGINT NOT NULL,
`dept_id` BIGINT NOT NULL,
PRIMARY KEY (`role_id`, `dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色部门关系';中间表设计原则:
- 联合主键
(user_id, role_id)防重复 - 反向索引
idx_role用于"查这个角色下的所有用户"
七、日志表
sql
CREATE TABLE `sys_operation_log` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT COMMENT '操作用户',
`username` VARCHAR(50) COMMENT '用户名(冗余便于查询)',
`module` VARCHAR(50) COMMENT '模块',
`action` VARCHAR(100) COMMENT '操作',
`method` VARCHAR(200) COMMENT '方法签名',
`request_url` VARCHAR(255),
`request_method` VARCHAR(10) COMMENT 'GET/POST',
`request_params` TEXT COMMENT '请求参数JSON',
`ip` VARCHAR(50),
`cost` BIGINT COMMENT '耗时ms',
`status` TINYINT DEFAULT 1 COMMENT '1=成功 0=失败',
`error_msg` TEXT,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`),
KEY `idx_module` (`module`),
KEY `idx_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志';
CREATE TABLE `sys_login_log` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL,
`ip` VARCHAR(50),
`user_agent` VARCHAR(500),
`status` TINYINT NOT NULL COMMENT '1=成功 0=失败',
`message` VARCHAR(255) COMMENT '失败原因',
`login_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_username` (`username`),
KEY `idx_time` (`login_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='登录日志';日志表特点:
- 只插入,不更新不删除(后期清理用
DELETE WHERE create_time < ?) - 不强制外键(性能 + 分表方便)
- 用 TEXT 存 JSON 参数,免去动态字段
⚠️ 坑 3:日志表千万别用外键约束。日志后期会按月分表,加了外键分表 SQL 写起来更麻烦,而且插入性能下降 10-20%。
八、必备审计字段
每张业务表都加这四个字段:
java
public abstract class BaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime; // 创建时间
@TableField(fill = FieldFill.INSERT)
private Long createBy; // 创建人
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime; // 更新时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy; // 更新人
@TableLogic
@TableField(select = false)
private Integer deleted; // 逻辑删除
}九、本章小结
| 要点 | 关键 |
|---|---|
| 业务表 | user / role / menu / dept / dict |
| 关系表 | user_role / role_menu / role_dept |
| 日志表 | operation_log / login_log |
| 必备字段 | id / 4 审计字段 / 软删 / 乐观锁 |
| 字符集 | utf8mb4(支持 emoji) |
| 引擎 | InnoDB(支持事务、行锁) |
| 软删 | is_deleted 联合唯一键 |
动手练习
- 写建表 SQL:在本地 MySQL 执行本章所有建表脚本,验证表都建成功
- 设计字典表:为 "性别"、"用户状态" 两个枚举,设计
sys_dict数据 - 加索引:对
sys_operation_log添加(user_id, create_time)复合索引,解释为什么
下一章:第 5 章:搭建脚手架 →