签入版本

This commit is contained in:
2026-09-16 13:28:20 +08:00
parent 6cd5e80de0
commit c97d87847f
410 changed files with 68726 additions and 0 deletions
@@ -0,0 +1,478 @@
# 数据库设计
本章详细描述 的数据库设计,包括核心表结构、通用字段规范、ER 关系和命名约定。
:::tip 设计原则
所有业务表继承 `base_model` 基类,自动获得 6 个通用字段(id / create_user / create_time / update_user / update_time / is_delete)。表名使用 `DB_PREFIX` 前缀(默认 `tornado_`),软删除通过 `is_delete` 字段实现。
:::
## 通用字段规范
所有业务表共享以下通用字段,由 `base_model` 基类自动提供:
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|:---|:---|:---|:---|:---|
| `id` | INT | PRIMARY KEY, AUTO_INCREMENT | — | 主键自增 |
| `create_user` | VARCHAR(50) | NULLABLE | NULL | 创建人(realname) |
| `create_time` | DATETIME | NOT NULL | CURRENT_TIMESTAMP | 创建时间(UTC) |
| `update_user` | VARCHAR(50) | NULLABLE | NULL | 更新人(realname) |
| `update_time` | DATETIME | NOT NULL | CURRENT_TIMESTAMP | 更新时间(UTC,onupdate) |
| `is_delete` | INT | NOT NULL, INDEX | 0 | 软删除:0=正常,1=已删除 |
## 核心表结构
### 系统管理模块
#### 用户表 `tornado_user`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 用户ID |
| username | VARCHAR(50) | NOT NULL, UNIQUE | 登录账号 |
| password | VARCHAR(200) | NOT NULL | 密码(bcrypt 哈希) |
| salt | VARCHAR(50) | NULLABLE | 密码盐值 |
| realname | VARCHAR(50) | NULLABLE | 真实姓名 |
| avatar | VARCHAR(500) | NULLABLE | 头像地址 |
| email | VARCHAR(100) | NULLABLE | 邮箱 |
| mobile | VARCHAR(20) | NULLABLE | 手机号 |
| gender | INT | DEFAULT 0 | 性别:0=未知 1=男 2=女 |
| dept_id | INT | NULLABLE, INDEX | 部门ID(外键) |
| position_id | INT | NULLABLE, INDEX | 岗位ID(外键) |
| level_id | INT | NULLABLE, INDEX | 职级ID(外键) |
| status | INT | DEFAULT 1 | 状态:1=正常 2=禁用 |
| create_user | VARCHAR(50) | NULLABLE | 通用字段 |
| create_time | DATETIME | DEFAULT NOW() | 通用字段 |
| update_user | VARCHAR(50) | NULLABLE | 通用字段 |
| update_time | DATETIME | DEFAULT NOW() | 通用字段 |
| is_delete | INT | DEFAULT 0, INDEX | 通用字段 |
#### 角色表 `tornado_role`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 角色ID |
| name | VARCHAR(100) | NOT NULL, INDEX | 角色名称 |
| code | VARCHAR(100) | NOT NULL, UNIQUE | 角色编码 |
| status | INT | DEFAULT 1 | 状态:1=正常 2=禁用 |
| sort | INT | DEFAULT 0 | 排序 |
| note | VARCHAR(255) | NULLABLE | 备注 |
| + 通用字段 | | | |
#### 菜单表 `tornado_menu`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 菜单ID |
| parent_id | INT | DEFAULT 0, INDEX | 父菜单ID(0=顶级) |
| name | VARCHAR(100) | NOT NULL | 菜单名称 |
| type | INT | NOT NULL | 类型:1=目录 2=菜单 3=按钮 |
| path | VARCHAR(200) | NULLABLE | 路由路径 |
| component | VARCHAR(200) | NULLABLE | 前端组件路径 |
| permission | VARCHAR(200) | NULLABLE | 权限标识(如 sys:user:add) |
| icon | VARCHAR(100) | NULLABLE | 菜单图标 |
| sort | INT | DEFAULT 0 | 排序 |
| visible | INT | DEFAULT 1 | 是否可见:1=是 2=否 |
| status | INT | DEFAULT 1 | 状态:1=正常 2=禁用 |
| + 通用字段 | | | |
#### 部门表 `tornado_dept`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 部门ID |
| parent_id | INT | DEFAULT 0, INDEX | 父部门ID(0=顶级) |
| name | VARCHAR(100) | NOT NULL | 部门名称 |
| leader | VARCHAR(50) | NULLABLE | 负责人 |
| mobile | VARCHAR(20) | NULLABLE | 联系电话 |
| email | VARCHAR(100) | NULLABLE | 邮箱 |
| sort | INT | DEFAULT 0 | 排序 |
| status | INT | DEFAULT 1 | 状态:1=正常 2=禁用 |
| + 通用字段 | | | |
#### 岗位表 `tornado_position`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 岗位ID |
| name | VARCHAR(255) | NOT NULL, INDEX | 岗位名称 |
| status | INT | DEFAULT 0, INDEX | 状态:1=在用 2=停用 |
| sort | INT | DEFAULT 0 | 排序 |
| + 通用字段 | | | |
#### 职级表 `tornado_level`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 职级ID |
| name | VARCHAR(255) | NOT NULL, INDEX | 职级名称 |
| status | INT | DEFAULT 0, INDEX | 状态:1=在用 2=停用 |
| sort | INT | DEFAULT 0 | 排序 |
| + 通用字段 | | | |
### 关联表
#### 用户角色关联表 `tornado_user_role`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 主键 |
| user_id | INT | NOT NULL, INDEX | 用户ID |
| role_id | INT | NOT NULL, INDEX | 角色ID |
| + 通用字段 | | | |
#### 角色菜单关联表 `tornado_role_menu`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 主键 |
| role_id | INT | NOT NULL, INDEX | 角色ID |
| menu_id | INT | NOT NULL, INDEX | 菜单ID |
| + 通用字段 | | | |
### 数据字典模块
#### 字典表 `tornado_dict`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 字典ID |
| name | VARCHAR(200) | NOT NULL | 字典名称 |
| code | VARCHAR(200) | NOT NULL, UNIQUE | 字典编码 |
| status | INT | DEFAULT 1 | 状态 |
| note | VARCHAR(255) | NULLABLE | 备注 |
| + 通用字段 | | | |
#### 字典项表 `tornado_dict_item`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 字典项ID |
| dict_code | VARCHAR(200) | NOT NULL, INDEX | 所属字典编码 |
| label | VARCHAR(200) | NOT NULL | 显示名 |
| value | VARCHAR(200) | NOT NULL | 字典值 |
| sort | INT | DEFAULT 0 | 排序 |
| status | INT | DEFAULT 1 | 状态 |
| note | VARCHAR(255) | NULLABLE | 备注 |
| + 通用字段 | | | |
### 内容管理模块
#### 文章表 `tornado_article`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 文章ID |
| category_id | INT | NULLABLE, INDEX | 分类ID |
| title | VARCHAR(200) | NOT NULL | 文章标题 |
| cover | VARCHAR(500) | NULLABLE | 封面图片 |
| summary | VARCHAR(500) | NULLABLE | 摘要 |
| content | TEXT | NULLABLE | 正文(富文本) |
| author | VARCHAR(50) | NULLABLE | 作者 |
| source | VARCHAR(200) | NULLABLE | 来源 |
| view_count | INT | DEFAULT 0 | 浏览次数 |
| is_top | INT | DEFAULT 0 | 是否置顶 |
| status | INT | DEFAULT 1 | 状态 |
| sort | INT | DEFAULT 0 | 排序 |
| + 通用字段 | | | |
#### 文章分类表 `tornado_category`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 分类ID |
| parent_id | INT | DEFAULT 0, INDEX | 父分类ID |
| name | VARCHAR(100) | NOT NULL | 分类名称 |
| sort | INT | DEFAULT 0 | 排序 |
| status | INT | DEFAULT 1 | 状态 |
| + 通用字段 | | | |
#### 通知公告表 `tornado_notice`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 通知ID |
| title | VARCHAR(200) | NOT NULL | 标题 |
| content | TEXT | NULLABLE | 内容 |
| type | INT | DEFAULT 1 | 类型:1=通知 2=公告 |
| status | INT | DEFAULT 1 | 状态 |
| + 通用字段 | | | |
### 日志模块
#### 登录日志表 `tornado_login_log`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 日志ID |
| username | VARCHAR(50) | NULLABLE | 登录账号 |
| ip | VARCHAR(50) | NULLABLE | 登录IP |
| location | VARCHAR(200) | NULLABLE | 登录地点 |
| browser | VARCHAR(100) | NULLABLE | 浏览器 |
| os | VARCHAR(100) | NULLABLE | 操作系统 |
| status | INT | DEFAULT 1 | 状态:1=成功 2=失败 |
| msg | VARCHAR(500) | NULLABLE | 提示消息 |
| + 通用字段 | | | |
#### 操作日志表 `tornado_operation_log`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 日志ID |
| username | VARCHAR(50) | NULLABLE | 操作人 |
| module | VARCHAR(100) | NULLABLE | 操作模块 |
| action | VARCHAR(100) | NULLABLE | 操作动作 |
| method | VARCHAR(10) | NULLABLE | 请求方法 |
| url | VARCHAR(500) | NULLABLE | 请求URL |
| params | TEXT | NULLABLE | 请求参数 |
| ip | VARCHAR(50) | NULLABLE | 操作IP |
| status | INT | DEFAULT 1 | 状态 |
| duration | INT | NULLABLE | 耗时(毫秒) |
| + 通用字段 | | | |
### 定时任务模块
#### 任务表 `tornado_job`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 任务ID |
| name | VARCHAR(200) | NOT NULL | 任务名称 |
| job_group | VARCHAR(100) | NULLABLE | 任务分组 |
| invoke_target | VARCHAR(500) | NOT NULL | 调用目标 |
| cron_expression | VARCHAR(200) | NOT NULL | Cron 表达式 |
| misfire_policy | INT | DEFAULT 3 | 执行策略 |
| concurrent | INT | DEFAULT 0 | 是否并发 |
| status | INT | DEFAULT 1 | 状态:1=运行 2=暂停 |
| note | VARCHAR(255) | NULLABLE | 备注 |
| + 通用字段 | | | |
#### 任务日志表 `tornado_job_log`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 日志ID |
| job_id | INT | NOT NULL, INDEX | 任务ID |
| job_name | VARCHAR(200) | NULLABLE | 任务名称 |
| invoke_target | VARCHAR(500) | NULLABLE | 调用目标 |
| job_message | VARCHAR(500) | NULLABLE | 执行结果 |
| status | INT | DEFAULT 1 | 状态 |
| exception_info | TEXT | NULLABLE | 异常信息 |
| start_time | DATETIME | NULLABLE | 开始时间 |
| end_time | DATETIME | NULLABLE | 结束时间 |
| + 通用字段 | | | |
## ER 关系图
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ tornado │ │ tornado │ │ tornado │
│ _user │ │ _user_role │ │ _role │
│──────────────│ │──────────────│ │──────────────│
│ id (PK) │←───│ user_id (FK) │ │ id (PK) │
│ username │ │ role_id (FK) │───→│ name │
│ password │ └──────────────┘ │ code │
│ realname │ └──────┬───────┘
│ dept_id (FK) │───→┌──────────────┐ │
│ position_id │ │ tornado │ │
│ (FK) │ │ _dept │ │
│ level_id (FK)│ │──────────────│ │
│ │ │ id (PK) │ │
│ │ │ parent_id │ │
│ │ │ name │ │
│ │ └──────────────┘ │
│ │ │
│ │ ┌──────────────┐ │
│ │ │ tornado │ │
│ │ │ _role_menu │ │
│ │ │──────────────│ │
│ │ │ role_id (FK) │←───────────┘
│ │ │ menu_id (FK) │───→┌──────────────┐
│ │ └──────────────┘ │ tornado │
└──────────────┘ │ _menu │
│──────────────│
┌──────────────┐ │ id (PK) │
│ tornado │ │ parent_id │
│ _position │ │ name │
│──────────────│ │ permission │
│ id (PK) │ │ type │
│ name │ └──────────────┘
└──────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ tornado │ │ tornado │ │ tornado │
│ _dict │ │ _dict_item │ │ _config │
│──────────────│ │──────────────│ │──────────────│
│ id (PK) │←───│ dict_code │ │ id (PK) │
│ name │ │ label │ │ name │
│ code │ │ value │ │ code │
└──────────────┘ └──────────────┘ └──────────────┘
┌──────────────┐ ┌──────────────┐
│ tornado │ │ tornado │
│ _article │ │ _category │
│──────────────│ │──────────────│
│ id (PK) │ │ id (PK) │
│ category_id │───→│ parent_id │
│ title │ │ name │
│ content │ └──────────────┘
└──────────────┘
```
## 命名约定
| 约定项 | 规范 | 示例 |
|:---|:---|:---|
| 表名 | `{DB_PREFIX}{module_name}` | `tornado_position` |
| 字段名 | 小写下划线(snake_case) | `create_user`、`dict_code` |
| 主键 | `id`,INT,AUTO_INCREMENT | — |
| 外键 | `{关联表名}_id` | `user_id`、`dept_id` |
| 索引 | 外键字段、高频查询字段 | `status`、`username` |
| 布尔状态 | INT 类型,0/1 或 1/2 | `is_delete: 0/1`、`status: 1/2` |
| 时间字段 | DATETIME 类型 | `create_time`、`update_time` |
:::warning 外键约束
不使用数据库层面的外键约束(FOREIGN KEY),而是通过应用层代码维护引用完整性。这样做的好处是:简化跨库迁移、避免级联删除风险、提升写入性能。删除关联数据时,由 Service 层的 `_before_delete` 钩子检查引用关系。
:::
## 多数据库驱动支持
通过 `DB_DRIVER` 环境变量切换数据库驱动,`src/core/database.py` 的 `build_engine()` 函数按驱动构建方言感知的 SQLAlchemy 引擎:
| 驱动 | DB_DRIVER | 连接 URL 格式 | 特殊处理 |
|:---|:---|:---|:---|
| MySQL | `mysql`(默认) | `mysql+pymysql://user:pass@host:3306/db` | `CLIENT.FOUND_ROWS` 避免重复提交时 rowcount=0 误判 |
| PostgreSQL | `postgresql` | `postgresql+psycopg://user:pass@host:5432/db` | `options="-c timezone=UTC"` 确保 naive UTC 一致 |
| SQL Server | `mssql` | `mssql+pymssql://user:pass@host:1433/db` | `login_timeout` 连接超时 |
| SQLite | `sqlite` | `sqlite:///./db.sqlite3` | `check_same_thread=False`,跳过连接池参数 |
| Oracle | `oracle` | `oracle+oracledb://user:pass@host:1521/db` | thin 模式,UTF-8 编码 |
```python
# src/core/database.py - 引擎构建核心逻辑
# ============================================================
# 引擎构建
# ============================================================
def build_engine(driver: str | None = None, database_url: str | None = None):
"""按 DB_DRIVER 构建方言感知的 SQLAlchemy 引擎
- mysql: connect_timeout + client_flag FOUND_ROWS(UPDATE rowcount=匹配行数,
避免重复提交相同内容时 rowcount=0 被误判"更新失败");pymysql 懒加载
- postgresql: connect_timeout + options="-c timezone=UTC"(aware-UTC 默认值落库为
naive UTC,与 base_model.py 的 naive-UTC 读取约定一致,防时区偏移)
- mssql: login_timeout(pymssql 连接超时参数)
- oracle: oracledb 懒加载(thin 模式,UTF-8);connect_args 传 encoding/nencoding
- sqlite: 仅 check_same_thread=False,跳过连接池容量参数(QueuePool 默认即可)
"""
from core.config import (
SQLALCHEMY_DATABASE_URL, DB_DRIVER, TORNADO_DEBUG,
DB_POOL_SIZE, DB_MAX_OVERFLOW, DB_POOL_RECYCLE,
DB_POOL_TIMEOUT, DB_CONNECT_TIMEOUT,
)
from core.config.database import _normalize_driver
drv = _normalize_driver(driver or DB_DRIVER)
url = database_url or SQLALCHEMY_DATABASE_URL
common = {"pool_pre_ping": True, "echo": TORNADO_DEBUG, "echo_pool": TORNADO_DEBUG}
if drv == 'mysql':
# 懒加载:仅 mysql 分支需要 pymysql,sqlite/pg/mssql 无需安装
from pymysql.constants import CLIENT
return create_engine(
url,
pool_size=DB_POOL_SIZE,
pool_recycle=DB_POOL_RECYCLE,
pool_timeout=DB_POOL_TIMEOUT,
max_overflow=DB_MAX_OVERFLOW,
pool_use_lifo=True,
connect_args={
"connect_timeout": DB_CONNECT_TIMEOUT,
"client_flag": CLIENT.FOUND_ROWS,
},
**common,
)
if drv == 'postgresql':
return create_engine(
url,
pool_size=DB_POOL_SIZE,
pool_recycle=DB_POOL_RECYCLE,
pool_timeout=DB_POOL_TIMEOUT,
max_overflow=DB_MAX_OVERFLOW,
pool_use_lifo=True,
connect_args={
"connect_timeout": DB_CONNECT_TIMEOUT,
"options": "-c timezone=UTC",
},
**common,
)
if drv == 'mssql':
return create_engine(
url,
pool_size=DB_POOL_SIZE,
pool_recycle=DB_POOL_RECYCLE,
pool_timeout=DB_POOL_TIMEOUT,
max_overflow=DB_MAX_OVERFLOW,
pool_use_lifo=True,
connect_args={"login_timeout": DB_CONNECT_TIMEOUT},
**common,
)
if drv == 'oracle':
# 懒加载:仅 oracle 分支需要 oracledb(thin 模式,无需 Oracle Instant Client)
import oracledb # noqa: F401
return create_engine(
url,
pool_size=DB_POOL_SIZE,
pool_recycle=DB_POOL_RECYCLE,
pool_timeout=DB_POOL_TIMEOUT,
max_overflow=DB_MAX_OVERFLOW,
pool_use_lifo=True,
connect_args={"encoding": "UTF-8", "nencoding": "UTF-8"},
**common,
)
# sqlite
return create_engine(url, connect_args={"check_same_thread": False}, **common)
```
业务代码无需修改,SQLAlchemy 自动处理方言差异(如分页语法、自增策略等)。
### 会话管理
```python
# src/core/database.py
from sqlalchemy.orm import scoped_session, sessionmaker
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
db = scoped_session(SessionLocal)
```
`scoped_session` 基于 thread-local 实现线程级隔离,每个线程持有独立的数据库会话。Repository 层通过 `self.db` 使用会话,写操作内部自动 `db.commit()`,Service 层 `try/finally` 确保 `repo.close()` 释放会话。
try:
independent_session.add(obj)
independent_session.commit()
except Exception:
independent_session.rollback()
raise
finally:
independent_session.close()
```
:::tip 约束命名约定
`database.py` 中定义了自定义约束命名约定,索引前缀使用 `idx_` 而非默认的 `ix_`:
```python
convention = {
"ix": "idx_%(table_name)s_%(column_0_name)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
metadata = MetaData(naming_convention=convention)
Base = declarative_base(metadata=metadata)
```
:::
## 总结
数据库设计以「通用字段 + 软删除 + 应用层外键」为核心策略。所有业务表继承 `BaseModel` 获得统一的通用字段,通过 `is_delete` 实现软删除,通过应用层代码维护引用完整性。核心表覆盖系统管理(用户/角色/菜单/部门/岗位/职级)、数据字典、内容管理(文章/分类/通知)、日志管理、定时任务等功能域。多数据库驱动通过 `src/core/database.py` 按方言构建引擎,业务代码与具体数据库解耦。会话通过 `scoped_session` 实现线程级隔离。