# 数据库设计 本章详细描述 的数据库设计,包括核心表结构、通用字段规范、ER 关系和命名约定。 :::tip 设计原则 所有业务表继承 `base_model` 基类,自动获得 6 个通用字段(id / create_user / create_time / update_user / update_time / is_delete)。表名使用 `DB_PREFIX` 前缀(默认 `fastapi_`),软删除通过 `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=已删除 | ## 核心表结构 ### 系统管理模块 #### 用户表 `fastapi_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 | 通用字段 | #### 角色表 `fastapi_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 | 备注 | | + 通用字段 | | | | #### 菜单表 `fastapi_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=禁用 | | + 通用字段 | | | | #### 部门表 `fastapi_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=禁用 | | + 通用字段 | | | | #### 岗位表 `fastapi_position` | 字段名 | 类型 | 约束 | 说明 | |:---|:---|:---|:---| | id | INT | PK | 岗位ID | | name | VARCHAR(255) | NOT NULL, INDEX | 岗位名称 | | status | INT | DEFAULT 0, INDEX | 状态:1=在用 2=停用 | | sort | INT | DEFAULT 0 | 排序 | | + 通用字段 | | | | #### 职级表 `fastapi_level` | 字段名 | 类型 | 约束 | 说明 | |:---|:---|:---|:---| | id | INT | PK | 职级ID | | name | VARCHAR(255) | NOT NULL, INDEX | 职级名称 | | status | INT | DEFAULT 0, INDEX | 状态:1=在用 2=停用 | | sort | INT | DEFAULT 0 | 排序 | | + 通用字段 | | | | ### 关联表 #### 用户角色关联表 `fastapi_user_role` | 字段名 | 类型 | 约束 | 说明 | |:---|:---|:---|:---| | id | INT | PK | 主键 | | user_id | INT | NOT NULL, INDEX | 用户ID | | role_id | INT | NOT NULL, INDEX | 角色ID | | + 通用字段 | | | | #### 角色菜单关联表 `fastapi_role_menu` | 字段名 | 类型 | 约束 | 说明 | |:---|:---|:---|:---| | id | INT | PK | 主键 | | role_id | INT | NOT NULL, INDEX | 角色ID | | menu_id | INT | NOT NULL, INDEX | 菜单ID | | + 通用字段 | | | | ### 数据字典模块 #### 字典表 `fastapi_dict` | 字段名 | 类型 | 约束 | 说明 | |:---|:---|:---|:---| | id | INT | PK | 字典ID | | name | VARCHAR(200) | NOT NULL | 字典名称 | | code | VARCHAR(200) | NOT NULL, UNIQUE | 字典编码 | | status | INT | DEFAULT 1 | 状态 | | note | VARCHAR(255) | NULLABLE | 备注 | | + 通用字段 | | | | #### 字典项表 `fastapi_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 | 备注 | | + 通用字段 | | | | ### 内容管理模块 #### 文章表 `fastapi_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 | 排序 | | + 通用字段 | | | | #### 文章分类表 `fastapi_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 | 状态 | | + 通用字段 | | | | #### 通知公告表 `fastapi_notice` | 字段名 | 类型 | 约束 | 说明 | |:---|:---|:---|:---| | id | INT | PK | 通知ID | | title | VARCHAR(200) | NOT NULL | 标题 | | content | TEXT | NULLABLE | 内容 | | type | INT | DEFAULT 1 | 类型:1=通知 2=公告 | | status | INT | DEFAULT 1 | 状态 | | + 通用字段 | | | | ### 日志模块 #### 登录日志表 `fastapi_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 | 提示消息 | | + 通用字段 | | | | #### 操作日志表 `fastapi_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 | 耗时(毫秒) | | + 通用字段 | | | | ### 定时任务模块 #### 任务表 `fastapi_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 | 备注 | | + 通用字段 | | | | #### 任务日志表 `fastapi_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 关系图 ``` ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ fastapi │ │ fastapi │ │ fastapi │ │ _user │ │ _user_role │ │ _role │ │──────────────│ │──────────────│ │──────────────│ │ id (PK) │←───│ user_id (FK) │ │ id (PK) │ │ username │ │ role_id (FK) │───→│ name │ │ password │ └──────────────┘ │ code │ │ realname │ └──────┬───────┘ │ dept_id (FK) │───→┌──────────────┐ │ │ position_id │ │ fastapi │ │ │ (FK) │ │ _dept │ │ │ level_id (FK)│ │──────────────│ │ │ │ │ id (PK) │ │ │ │ │ parent_id │ │ │ │ │ name │ │ │ │ └──────────────┘ │ │ │ │ │ │ ┌──────────────┐ │ │ │ │ fastapi │ │ │ │ │ _role_menu │ │ │ │ │──────────────│ │ │ │ │ role_id (FK) │←───────────┘ │ │ │ menu_id (FK) │───→┌──────────────┐ │ │ └──────────────┘ │ fastapi │ └──────────────┘ │ _menu │ │──────────────│ ┌──────────────┐ │ id (PK) │ │ fastapi │ │ parent_id │ │ _position │ │ name │ │──────────────│ │ permission │ │ id (PK) │ │ type │ │ name │ └──────────────┘ └──────────────┘ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ fastapi │ │ fastapi │ │ fastapi │ │ _dict │ │ _dict_item │ │ _config │ │──────────────│ │──────────────│ │──────────────│ │ id (PK) │←───│ dict_code │ │ id (PK) │ │ name │ │ label │ │ name │ │ code │ │ value │ │ code │ └──────────────┘ └──────────────┘ └──────────────┘ ┌──────────────┐ ┌──────────────┐ │ fastapi │ │ fastapi │ │ _article │ │ _category │ │──────────────│ │──────────────│ │ id (PK) │ │ id (PK) │ │ category_id │───→│ parent_id │ │ title │ │ name │ │ content │ └──────────────┘ └──────────────┘ ``` ## 命名约定 | 约定项 | 规范 | 示例 | |:---|:---|:---| | 表名 | `{DB_PREFIX}{module_name}` | `fastapi_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, FASTAPI_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": FASTAPI_DEBUG, "echo_pool": FASTAPI_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 # 请求级数据库 session 上下文变量 _db_ctx: ContextVar = ContextVar('_db_ctx') def get_db_session(): """获取当前请求的数据库 session(仅限请求上下文内调用)""" try: return _db_ctx.get() except LookupError: raise RuntimeError( "get_db_session() 只能在请求上下文内调用(缺少 db_session 中间件注入)。" "后台线程/启动阶段请使用 SessionLocal() 创建独立会话。" ) from None # ============================================================ # 独立提交 # ============================================================ def commit_independently(obj): """ 使用独立session提交对象,不受请求事务影响 日志等审计数据无论业务成功或失败都应持久化, 因此需要绕过 AOP 事务中间件的 commit/rollback 机制。 """ independent_session = SessionLocal() 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) ``` ::: ## 总结 数据库设计以「通用字段 + 软删除 + 应用层外键」为核心策略。所有业务表继承 `base_model` 获得统一的通用字段,通过 `is_delete` 实现软删除,通过应用层代码维护引用完整性。核心表覆盖系统管理(用户/角色/菜单/部门/岗位/职级)、数据字典、内容管理(文章/分类/通知)、日志管理、定时任务等功能域。多数据库驱动通过 `src/core/database.py` 的 `build_engine()` 按方言构建引擎,业务代码与具体数据库解耦。请求级会话通过 `contextvars` + `db_session_middleware` 实现,审计数据可通过 `commit_independently()` 绕过请求事务独立提交。