签入版本

This commit is contained in:
2026-09-12 23:12:15 +08:00
commit 91998056ca
1841 changed files with 341325 additions and 0 deletions
@@ -0,0 +1,285 @@
# 应用工厂与扩展初始化
:::tip 本章概要
Flask 应用工厂模式(`create_app()`)是项目的核心初始化机制。本文详解工厂函数的初始化顺序、扩展注册方式、依赖注入机制,帮助开发者理解应用启动全流程。
:::
## 应用工厂模式
项目采用 Flask 标准的应用工厂模式,通过 `create_app()` 函数创建和配置应用实例:
```python
# src/app.py
from flask import Flask
from flask_cors import CORS
from werkzeug.middleware.proxy_fix import ProxyFix
from src.middleware import register_middleware, register_transaction, register_error_handlers, register_operation_log
from src.routes.v1 import register_v1
from src.extensions import register_extends
from config import DatabaseConfig, REDIS_URL
from src.common.utils.json import JSONEncoder
def create_app():
"""
创建并配置 Flask 应用实例
配置流程:
1. 创建 Flask 核心对象
2. 配置 Redis 连接
3. 加载数据库配置
4. 设置 JSON 序列化器
5. 配置跨域资源共享
6. 注册操作日志与事务拦截器
7. 注册 API v1 路由
8. 初始化第三方扩展
9. 注册中间件
10. 注入依赖
Returns:
Flask: 配置完成的应用实例
"""
# 1. 创建 Flask 核心对象
app = Flask(__name__, template_folder='templates', static_folder='../static')
# 2. 配置 Redis
app.config['REDIS_URL'] = REDIS_URL
# 3. 加载数据库配置
app.config.from_object(DatabaseConfig)
# 4. 自定义 JSON 序列化器
app.json = JSONEncoder(app)
# 5. 配置 CORS
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=False)
# 6. 注册操作日志 + AOP 事务拦截器
register_operation_log(app)
register_transaction(app)
# 7. 注册 API v1 路由
register_v1(app)
# 8. 初始化扩展(数据库、Redis、调度器等)
register_extends(app)
# 9. 注册中间件(登录校验、演示模式、异常处理)
register_middleware(app)
# 10. 注入依赖
_inject_dependencies(app)
return app
```
## 初始化顺序的重要性
初始化顺序经过精心设计,各步骤之间存在依赖关系:
```
Flask 核心对象
↓
配置加载(数据库、Redis)
↓
JSON 序列化器
↓
CORS 跨域
↓
操作日志 + 事务拦截器(须在路由之前)
↓
路由注册(Blueprint 挂载)
↓
扩展初始化(db.init_app、redis 连接)
↓
中间件注册(登录校验、演示模式)
↓
依赖注入(权限提供方、操作日志写入器)
```
:::warning 关键顺序
- `register_operation_log` 必须在 `register_transaction` 之前,确保操作日志在事务提交前记录
- `register_v1`(路由注册)必须在 `register_middleware`(登录校验)之前,否则中间件无法拦截已注册的路由
- `register_extends`(扩展初始化)必须在路由处理请求之前完成
:::
## 扩展注册(init_app 模式)
Flask 扩展采用 `init_app` 模式,将扩展实例与应用绑定:
```python
# src/extensions.py
from src.core.database import db
from src.core.redis_client import redis
from src.core.scheduler import scheduler
def register_extends(app):
"""初始化第三方扩展"""
# Flask-SQLAlchemy
db.init_app(app)
# Redis 连接
redis.init_app(app)
# APScheduler 定时任务
scheduler.init_app(app)
```
```python
# src/core/database.py
from flask_sqlalchemy import SQLAlchemy
# 创建扩展实例(不绑定应用)
db = SQLAlchemy()
```
这种模式的优势:
1. **延迟初始化**:扩展实例在模块加载时创建,但绑定应用发生在 `create_app()` 中
2. **测试友好**:测试时可以创建不同的应用实例,扩展自动绑定到测试应用
3. **避免循环导入**:扩展实例定义在 `src/core/`,应用工厂在 `src/app.py`,解耦清晰
## 路由注册(Blueprint 模式)
所有业务蓝图通过 `src/routes/v1.py` 统一注册到 `/api/v1` 前缀:
```python
# src/routes/v1.py
from flask import Blueprint
from src.modules.system.position.router import position_blue
from src.modules.system.user.router import user_blue
from src.modules.system.role.router import role_blue
# ... 其他模块导入
# 全部业务蓝图(按领域分组排序)
BLUEPRINTS = [
# 系统管理模块
user_blue, role_blue, menu_blue, dept_blue, position_blue, level_blue,
# 内容管理模块
article_blue, category_blue, link_blue,
# 数据管理模块
dict_blue, config_blue, notice_blue, param_blue, city_blue,
# 监控管理模块
job_blue, job_log_blue, login_log_blue, operation_log_blue,
# 认证模块
auth_blue,
# 文件上传
upload_blue,
]
def register_v1(app):
"""
注册 API v1 路由(所有业务蓝图挂载到 /api/v1)
Args:
app: Flask 应用实例
"""
v1 = Blueprint('v1', __name__, url_prefix='/api/v1')
for blue in BLUEPRINTS:
v1.register_blueprint(blue)
app.register_blueprint(v1)
```
## 中间件注册
中间件通过 `before_request` / `after_request` / `teardown_request` 钩子实现:
```python
# src/middleware/__init__.py
from src.middleware.auth import register_auth
from src.middleware.demo import register_demo
from src.middleware.errors import register_error_handlers
def register_middleware(app):
"""注册中间件"""
register_auth(app) # 登录校验 + 白名单
register_demo(app) # 演示模式拦截
register_error_handlers(app) # 全局异常处理
```
中间件执行顺序(请求进入时):
```
请求进入
↓
login_required(检查 JWT Token)
↓
check_permission(RBAC 权限校验,装饰器层面)
↓
check_demo(演示模式拦截,装饰器层面)
↓
路由函数(Router)
↓
Service → Repository → 数据库
↓
返回响应
```
## 依赖注入
`_inject_dependencies` 将权限查询函数注入到核心模块,解耦业务模块与核心模块:
```python
# src/app.py
def _inject_dependencies(app):
"""
注入依赖(权限提供方 / 操作日志写入器)
通过 app.extensions 字典传递依赖,避免核心模块直接导入业务模块。
"""
from src.modules.system.menu.service import get_user_permissions
from src.modules.operation_log.service import save_operation_log
app.extensions['permission_provider'] = get_user_permissions
app.extensions['operation_log_writer'] = save_operation_log
```
```python
# src/core/permission.py
from flask import current_app
def check_permission(permission: str):
"""权限校验装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 通过 current_app 获取注入的权限查询函数
permission_provider = current_app.extensions.get('permission_provider')
if permission_provider:
user_perms = permission_provider(user_id)
if permission not in user_perms:
return R.failed(msg="权限不足", code=403)
return func(*args, **kwargs)
return wrapper
return decorator
```
## 入口文件
```python
# app.py(项目根目录)
from src.app import create_app
app = create_app()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8021, debug=True)
```
生产环境使用 Gunicorn 启动:
```bash
# 4 个工作进程,每个进程 4 个线程
gunicorn -w 4 -t 4 "app:app" --bind 0.0.0.0:8021
```
## 总结
项目采用 Flask 标准的应用工厂模式,通过 `create_app()` 按序完成配置加载、扩展初始化、路由注册、中间件注册、依赖注入。扩展采用 `init_app` 模式延迟绑定,路由通过 Blueprint 模块化组织,中间件通过 `before_request` / `after_request` 钩子链式处理。这种设计确保了模块解耦、测试友好、初始化顺序可控。
@@ -0,0 +1,509 @@
# 核心基类体系
本章详细描述项目的核心基类体系,包括 `BaseModel`、`BaseDB`、`BaseForm`、`BaseRepository`、`BaseService` 五大基类的设计理念、继承关系和使用方式。以岗位(Position)模块为实例贯穿说明。
## 基类继承关系
:::tip 基类体系总览
五大基类各司其职,覆盖从数据模型到请求校验再到业务逻辑的完整链路。子类只需声明差异点,即可获得标准化的增删改查能力。
:::
```
┌──────────────────────────────────────────────────────────────────┐
│ SQLAlchemy Base (declarative_base) │
└──────────────────────────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ BaseModel (src/common/base_model.py) │
│ ─────────────────────────────────────────────────────────────── │
│ 通用字段: id / create_user / create_time / update_user / │
│ update_time / is_delete │
│ 序列化: to_dict() → 驼峰字段名 + UTC 转本地 + 排除敏感字段 │
└──────────────────────────┬───────────────────────────────────────┘
│ 多继承
▼
┌──────────────────────────────────────────────────────────────────┐
│ BaseDB (src/common/base_db.py) │
│ ─────────────────────────────────────────────────────────────── │
│ 持久化: save() → db.session.add + db.session.flush(不 commit) │
│ 事务: 由 AOP 事务中间件统一管理 │
└──────────────────────────────────────────────────────────────────┘
│
▼
Position(BaseModel, BaseDB)
┌──────────────────────────┐
│ __tablename__ │
│ name / status / sort │
└──────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ BaseForm (src/common/forms/base_form.py) │
│ ─────────────────────────────────────────────────────────────── │
│ WTForms 表单基类 │
│ 可选 id 字段: Optional,创建时可省略 │
│ 空字符串容错: '' / 'null' / 'undefined' → None │
└──────────────────────────────────────────────────────────────────┘
│
▼
PositionForm(BaseForm)
┌──────────────────────────┐
│ name / status / sort │
│ 继承可选 id │
└──────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ BaseRepository (src/common/base_repository.py) │
│ ─────────────────────────────────────────────────────────────── │
│ 查询: get_by_id / get_by_ids / get_one / get_all / count │
│ 分页: paginate → (items, total) │
│ 写操作: create / update / batch_delete │
│ 条件: filter (原始) / filter_by (等值+软删) │
│ 软删除: 等值方法自动追加 is_delete=0 │
└──────────────────────────┬───────────────────────────────────────┘
│
▼
PositionRepository(BaseRepository)
┌──────────────────────────┐
│ model = Position │
│ _repository = 单例 │
└──────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ BaseService (src/common/base_service.py) │
│ ─────────────────────────────────────────────────────────────── │
│ 查询: get_page / get_list / get_detail / get_info │
│ 写操作: add / update / remove / update_status │
│ 差异点: repository / form / status_form / model_name │
│ 钩子: before_add / before_update / before_delete │
│ after_add / after_update / after_delete │
│ _enrich_page_records / _detail_dict │
│ _validate_form / _clean_form_data │
└──────────────────────────┬───────────────────────────────────────┘
│
▼
PositionService(BaseService)
┌──────────────────────────────────┐
│ repository = PositionRepository │
│ form = PositionForm │
│ status_form = PositionStatusForm │
│ model_name = "岗位" │
│ before_delete() → 引用检查 │
│ 模块级代理函数 │
└──────────────────────────────────┘
```
## BaseModel — 模型基类
**文件位置**:`src/common/base_model.py`
### 设计目标
为所有业务模型提供统一的通用字段和序列化方法,消除重复定义。
### 通用字段
```python
class BaseModel(Base):
__abstract__ = True # 抽象基类,不会在数据库中创建对应的表
# 主键:自增整数,所有业务表的唯一标识
id = Column(Integer, primary_key=True, autoincrement=True, comment="主键ID")
# 创建人:记录该条记录的创建者用户名,由 Service 层自动填充
create_user = Column(String(50), nullable=True, comment="创建人用户名")
# 创建时间:记录创建时的 UTC 时间,数据库默认自动生成
create_time = Column(DateTime, default=_UTC_NOW, server_default=func.now(), comment="创建时间")
# 更新人:记录最后修改该条记录的用户名,由 Service 层自动填充
update_user = Column(String(50), nullable=True, comment="最后更新人用户名")
# 更新时间:记录最后修改时间,更新时自动刷新,数据库默认自动生成
update_time = Column(DateTime, default=_UTC_NOW, onupdate=_UTC_NOW, server_default=func.now(), comment="最后更新时间")
# 逻辑删除标记:0 表示正常记录,1 表示已删除,物理删除不使用,保证数据可恢复;添加索引以提升查询性能,Repository 层默认过滤 is_delete=0
is_delete = Column(Integer, default=0, server_default=text('0'), index=True, comment="逻辑删除标识:0-正常,1-已删除")
```
| 字段 | 类型 | 说明 |
|:---|:---|:---|
| `id` | Integer, PK, AI | 主键自增 |
| `create_user` | String(50) | 创建人(realname) |
| `create_time` | DateTime | 创建时间(UTC,server_default) |
| `update_user` | String(50) | 更新人(realname) |
| `update_time` | DateTime | 更新时间(UTC,onupdate 自动触发) |
| `is_delete` | Integer | 软删除标识:0=正常,1=已删除 |
### 表注释钩子
```python
# src/common/base_model.py
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
comment = getattr(cls, '__table_comment__', None)
if not comment:
return
if not getattr(cls, '__tablename__', None):
return
existing = getattr(cls, '__table_args__', {})
if isinstance(existing, dict):
merged = {**existing}
elif isinstance(existing, tuple) and existing:
merged = dict(existing[-1]) if isinstance(existing[-1], dict) else {}
else:
merged = {}
if 'comment' not in merged:
merged['comment'] = comment
cls.__table_args__ = merged
```
子类只需声明 `__table_comment__ = "岗位表"`,基类通过 `__init_subclass__` 钩子自动将其写入 `__table_args__`,无需手动配置 `Table(comment=...)`。
### 序列化方法
关键设计点:
1. **敏感字段排除**:`is_delete`、`password`、`salt` 不会出现在序列化结果中
2. **时区处理**:MySQL 的 `DATETIME` 读回为 naive UTC,统一转 aware UTC 后再转本地时间
3. **驼峰命名**:数据库下划线字段名自动转为前端习惯的驼峰命名
## BaseDB — 数据库操作基类
**文件位置**:`src/common/base_db.py`
### 设计目标
提供模型实例的持久化方法,事务边界由中间件统一管理。
```python
# src/common/base_db.py
class BaseDB:
def save(self):
# 将当前模型实例添加到 SQLAlchemy 会话的持久化上下文中
db.session.add(self)
# 立即发送 INSERT SQL 到数据库,获取自增 ID,但不提交事务(事务由中间件统一控制)
db.session.flush()
# 返回当前实例,支持链式调用,便于后续操作
return self
```
:::tip 为什么 save() 不 commit?
事务提交由 `src/middleware/transaction.py` 的 `register_transaction` 统一管理:响应体 `code==0` 时 commit,否则 rollback。这样业务代码无需关心事务边界,也避免了部分操作成功、部分失败导致的数据不一致。审计日志等需要独立提交的场景,可使用 `commit_independently(obj)` 绕过请求事务。
:::
## BaseForm — 表单基类
**文件位置**:`src/common/forms/base_form.py`
### 设计目标
所有创建/编辑表单的 WTForms 基类,提供统一的可选 `id` 字段和前端空字符串容错处理。
### 完整代码
```python
# src/common/forms/base_form.py
# +======================================================================
# | 模块: 表单基类
# | 说明: 统一处理前端传入的空字符串 id → None 等通用校验
# +======================================================================
from wtforms import Form, IntegerField
from wtforms.validators import Optional
class BaseForm(Form):
"""
表单基类
所有创建/编辑表单应继承此类,自动获得:
1. id 字段:Optional,前端传空字符串时自动转为 None
"""
id = IntegerField('ID', validators=[Optional()])
```
### 代码解析
**可选 `id` 字段**:
```python
id = IntegerField('ID', validators=[Optional()])
```
`id` 默认为 `None`。创建记录时前端不传 `id`,WTForms 自动赋默认值;编辑记录时前端传入 `id`,Service 层据此判断是新增还是更新。
### 子类示例
```python
# src/modules/system/position/schema.py
# +======================================================================
# | 模块: 岗位表单验证
# | 说明: 岗位创建/编辑/状态更新的请求数据校验
# +======================================================================
from src.common.forms.base_form import BaseForm
class PositionForm(BaseForm):
"""岗位创建/编辑表单"""
name = StringField('岗位名称', validators=[...])
status = IntegerField('岗位状态', validators=[...])
sort = IntegerField('岗位排序', validators=[...])
```
`PositionForm` 继承 `BaseForm` 后自动获得可选 `id` 字段。创建时 JSON 请求体无需包含 `id`:
```json
{ "name": "测试岗位", "status": 1, "sort": 0 }
```
编辑时传入 `id`:
```json
{ "id": 1, "name": "修改后", "status": 1, "sort": 1 }
```
### 使用场景
| 场景 | 是否传 id | BaseForm 行为 |
|:----|:----|:----|
| 新增 | 不传 | `id = None`,Service 走 `add()` 分支 |
| 编辑 | 传入 | `id = 123`,Service 走 `update()` 分支 |
| 特定操作 | 不需要 id | 继承 `Form` 而非 `BaseForm`(如 `PositionStatusForm`) |
:::tip 何时继承 BaseForm vs Form
- **创建/编辑共用的表单**:继承 `BaseForm`,自动获得可选 `id`
- **特定操作的表单**(如状态更新、排序调整):直接继承 `Form`,只定义必要字段
:::
## BaseRepository — 仓库基类
**文件位置**:`src/common/base_repository.py`
### 设计目标
封装各模型的数据访问层公共操作,提供统一的 CRUD 接口和软删除约定。
### 核心方法一览
| 方法 | 签名 | 说明 | 软删过滤 |
|:---|:---|:---|:---:|
| `get_by_id` | `(obj_id) → Optional[M]` | 按 ID 获取单条记录 | 是 |
| `get_by_ids` | `(ids) → List[M]` | 按 ID 列表批量获取 | 是 |
| `first_by` | `(filters) → Optional[M]` | 等值条件获取单条 | 是 |
| `exists_by` | `(filters, exclude_id) → bool` | 唯一性校验 | 是 |
| `count_by` | `(filters) → int` | 等值条件计数 | 是 |
| `exists` | `(obj_id) → bool` | 检查记录是否存在 | 是 |
| `count_children` | `(parent_id) → int` | 统计子记录数 | 是 |
| `find_page` | `(...) → (items, total, pages)` | 统一分页查询 | 是 |
| `find_list` | `(...) → List[M]` | 统一不分页查询 | 是 |
| `create` | `(**kwargs) → M` | 创建记录 | — |
| `update_by_id` | `(obj_id, data) → int` | 按 ID 更新 | — |
| `update_status` | `(obj_id, status) → int` | 更新状态字段 | — |
| `delete_by_id` | `(obj_id) → bool` | 软删除单条 | — |
| `batch_delete` | `(ids) → int` | 批量软删除 | — |
| `batch_restore` | `(ids) → int` | 批量恢复 | — |
### 软删除约定
:::warning 重要设计约定
- **等值便捷方法**(get_by_id / get_one / get_all / count / filter_by / exists / exists_by_field / paginate)自动追加 `is_delete=0` 过滤,调用方无需关心
- **原始出口** `filter(*criterion)` **不做**软删过滤,由调用方自行追加 `Model.is_delete == 0`,用于 `in_` / `!=` / `and_` / 区间等复杂条件
:::
### 数据清洗
```python
# src/common/base_service.py
@classmethod
def _clean_form_data(cls, form):
"""
过滤表单数据
仅保留模型真实列字段,防止伪字段进入 update_by_id 触发 SQL 错误。
camelCase 字段名自动转为 snake_case 后与模型列集合比对。
"""
if form is None:
return {}
columns = {c.name for c in cls.repository.model.__table__.columns}
return {k: v for k, v in form.data.items() if convert_snake_case(k) in columns}
```
WTForms Schema 可能用 camelCase(如 `parentId`),而模型列是 snake_case(如 `parent_id`)。`_clean_form_data` 在 Service 层统一转换,同时过滤掉模型中不存在的字段,业务模块无需各自处理。
## BaseService — 服务基类
**文件位置**:`src/common/base_service.py`
### 设计目标
通过模板方法模式封装通用 CRUD 流程。子类只需声明差异点,即可获得分页、详情、添加、编辑、删除、批量删除、状态切换等标准接口。
### 差异点声明
| 差异点 | 类型 | 说明 |
|:---|:---|:---|
| `repository` | `BaseRepository` | **必填**,数据仓库类 |
| `model` | `type[M]` | **必填**,ORM 模型类 |
| `repository` | Repository 类 | 数据访问层类 | `PositionRepository` |
| `form` | Form 类 | 新增/更新表单校验类 | `PositionForm` |
| `status_form` | Form 类 | 状态更新表单校验类(可选) | `PositionStatusForm` |
| `model_name` | str | 业务对象名称,用于错误提示 | `"岗位"` |
| `serialize_maps` | dict | 枚举显示名映射(可选) | `{'type': 'operation_type'}` |
| `serialize_maps` | `Dict[str, Dict]` | 枚举显示名映射 |
| `file_fields` | `Tuple[str, ...]` | 文件字段(add/update 时自动迁移临时文件,serialize 时自动补全 URL) |
| `rich_text_fields` | `Tuple[str, ...]` | 富文本字段(含嵌入图片迁移 + XSS 清洗) |
### 标准接口
| 方法 | 说明 | 返回值 |
|:---|:---|:---|
| `get_page()` | 分页查询 | `R.ok(data=[...], count=total)` |
| `get_list()` | 不分页查询 | `R.ok(data=[...])` |
| `get_detail(obj_id)` | 详情查询 | `dict` 或 `None` |
| `add(data)` | 新增记录 | `R.ok(msg='添加成功')` |
| `update(data)` | 编辑记录 | 含表单校验 + 存在性校验 |
| `remove(ids)` | 批量软删除 | 逗号分隔 ID 字符串 |
| `update_status(data)` | 状态切换 | 含状态表单校验 |
### 钩子方法
| 钩子 | 触发时机 | 默认行为 |
|:---|:---|:---|
| `before_add(form)` | 新增前 | 空操作,可返回额外字段字典 |
| `after_add(obj)` | 新增后 | 空操作 |
| `before_update(form, obj)` | 编辑前 | 空操作,可返回额外字段字典 |
| `after_update(obj)` | 编辑后 | 空操作 |
| `before_delete(ids)` | 删除前 | 空操作,抛 BizError 中止 |
| `after_delete(count)` | 删除后 | 空操作 |
| `_enrich_page_records(records)` | 分页富化 | 应用 serialize_maps |
| `_detail_dict(obj)` | 详情序列化 | model_to_dict + serialize_maps |
| `_validate_form(data)` | 表单校验 | 调用 form.validate() |
| `_clean_form_data(form)` | 表单数据过滤 | 保留模型列字段 |
## 实例:Position 模块的基类使用
### Model(继承 BaseModel + BaseDB)
```python
# src/modules/system/position/models.py
from sqlalchemy import Column, String, Integer, text
from src.common.base_db import BaseDB
from src.common.base_model import BaseModel
from config.database import DB_PREFIX
class Position(BaseModel, BaseDB):
"""岗位模型类"""
__tablename__ = DB_PREFIX + "position"
__table_comment__ = "岗位表"
name = Column(String(255), nullable=False, index=True, comment="岗位名称")
status = Column(Integer, default=0, server_default=text('0'), index=True, comment="岗位状态:1-在用 2-停用")
sort = Column(Integer, default=0, server_default=text('0'), comment="岗位排序")
def __repr__(self):
return '岗位:{}'.format(self.name)
```
获得的能力:
- 6 个通用字段(id / create_user / create_time / update_user / update_time / is_delete)
- `to_dict()` 序列化方法
- `save()` 持久化方法
- 表注释自动写入
### Repository(继承 BaseRepository)
```python
# src/modules/system/position/repository.py
from src.common.base_repository import BaseRepository
from src.common.constants.common import PAGE_SIZE
from src.modules.system.position.models import Position
class PositionRepository(BaseRepository):
"""岗位数据仓储"""
model = Position
@classmethod
def page(cls, params: dict):
"""岗位分页查询"""
return cls.find_page(
page=int(params.get('pageNo', 1)),
page_size=int(params.get('pageSize', PAGE_SIZE)),
filters={'status': params.get('status')},
like_filters={'name': params.get('name')},
order_by='sort',
)
@classmethod
def list_enabled(cls, params=None):
"""查询启用的岗位列表"""
params = params or {}
return cls.find_list(
filters={'status': 1},
like_filters={'name': params.get('name')},
order_by='sort',
)
```
获得的能力:
- 全部通用 CRUD 方法(classmethod)
- 软删除自动过滤
- camelCase → snake_case 数据清洗
### Service(继承 BaseService)
```python
# src/modules/system/position/service.py
from src.common.base_service import BaseService
from src.core.exceptions import BizError
from src.modules.system.position.schema import PositionForm, PositionStatusForm
from src.modules.system.position.repository import PositionRepository
from src.modules.system.user.repository import UserRepository
class PositionService(BaseService):
"""岗位服务类"""
repository = PositionRepository
form = PositionForm
status_form = PositionStatusForm
model_name = "岗位"
@classmethod
def before_delete(cls, ids):
"""删除前校验:存在用户引用该岗位时禁止删除"""
for pid in ids:
if UserRepository.count_by({'position_id': pid}) > 0:
raise BizError(f"岗位ID {pid} 已被用户引用,无法删除")
# 模块级代理函数
get_position_page = PositionService.get_page
get_position_detail = PositionService.get_detail
get_info = PositionService.get_info
add_position = PositionService.add
update_position = PositionService.update
delete_positions = PositionService.remove
update_position_status = PositionService.update_status
get_position_list = PositionService.get_list
```
获得的能力:
- 8 个标准接口
- 唯一性自动校验
- 分页查询自动构建
- 序列化自动处理
:::tip 极简 CRUD
对于最简单的模块(如 Level 职级),Repository 和 Service 只需声明差异点,无需编写任何自定义方法,即可获得完整的增删改查能力。
:::
## 总结
五大基类通过模板方法模式和声明式配置,将通用 CRUD 流程固化为标准实现:
| 基类 | 职责 | 核心能力 |
|:----|:----|:----|
| `BaseModel` | 模型基类 | 通用字段、to_dict() 序列化 |
| `BaseDB` | 持久化基类 | save() 方法 |
| `BaseForm` | 表单基类 | 可选 id、空字符串容错 |
| `BaseRepository` | 仓库基类 | 12 个 CRUD 方法、软删除自动过滤 |
| `BaseService` | 服务基类 | 8 个标准接口、钩子方法、声明式配置 |
子类只需声明差异点(过滤字段、排序规则、唯一性约束等),即可获得企业级的增删改查能力。以岗位模块为例,完整实现只需约 80 行有效代码(不含版权头),其中 Repository 几乎零代码。这种设计使得新模块的开发效率提升 3-5 倍,同时保证了代码风格和行为的一致性。
@@ -0,0 +1,307 @@
# 缓存架构设计
本章详细描述项目的缓存架构设计,包括两级缓存策略(进程内缓存 + Redis 缓存)、缓存应用场景、失效策略和代码实现。
## 两级缓存架构
:::tip 缓存分层设计
项目采用两级缓存架构:第一级为进程内字典缓存(`dict_util`),第二级为 Redis 分布式缓存。两级缓存配合使用,在保证数据一致性的同时最大化查询性能。
:::
```
┌─────────────────────────────────────────────────────────────────┐
│ 请求处理流程 │
│ │
│ ┌──────────┐ 命中 ┌──────────┐ 命中 ┌───────────┐ │
│ │ 业务代码 │ ─────────→ │ L1 缓存 │ ────────→ │ 返回结果 │ │
│ │ │ │ (进程内) │ │ │ │
│ └──────────┘ └──────────┘ └───────────┘ │
│ │ │ 未命中 │
│ │ ▼ │
│ │ ┌──────────┐ 命中 ┌───────────┐ │
│ │ │ L2 缓存 │ ────────→ │ 写回 L1 │ │
│ │ │ (Redis) │ │ 返回结果 │ │
│ │ └──────────┘ └───────────┘ │
│ │ │ 未命中 │
│ │ ▼ │
│ │ ┌──────────┐ ┌───────────┐ │
│ │ │ 数据库 │ ────────→ │ 写回 L1 │ │
│ │ │ (MySQL) │ │ 写回 L2 │ │
│ │ └──────────┘ └───────────┘ │
│ │ │
│ ┌──────────┐ │
│ │ 缓存失效 │ → 清除 L1 + 删除 L2 │
│ └──────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### 缓存层级对比
| 维度 | L1 进程内缓存 | L2 Redis 缓存 |
|:---|:---|:---|
| **存储位置** | Python 进程内存 | Redis 服务器 |
| **访问速度** | 纳秒级(字典查找) | 毫秒级(网络 I/O) |
| **数据共享** | 仅当前进程 | 所有进程/实例共享 |
| **持久化** | 进程重启丢失 | 持久化可配置 |
| **容量限制** | 受进程内存限制 | 受 Redis 内存限制 |
| **适用场景** | 高频读取、低变更数据 | 跨进程共享、分布式场景 |
## L1 缓存:进程内字典缓存
### 实现原理
`dict_util` 模块实现了带 TTL 的进程内字典缓存。数据首次查询时从 Redis 加载到进程内存,后续查询直接从内存读取,TTL 过期后自动重新加载。
```python
# src/common/utils/dict_util.py
import time
import json
from typing import Dict
# 进程内缓存:{cache_key: (data, expire_timestamp)}
_cache: Dict[str, tuple] = {}
CACHE_TTL = 300 # 默认 5 分钟
def get_dict_map(dict_code: str) -> Dict[str, str]:
"""获取数据字典的 {value: label} 映射(进程内缓存 + Redis 二级缓存)"""
cache_key = f"dict:{dict_code}"
now = time.time()
# L1: 进程内缓存命中
if cache_key in _cache:
data, expire_at = _cache[cache_key]
if now < expire_at:
return data
del _cache[cache_key]
# L2: Redis 缓存
from src.core.redis_client import get_redis
redis = get_redis()
cached = redis.get(cache_key)
if cached:
data = json.loads(cached)
_cache[cache_key] = (data, now + CACHE_TTL)
return data
# 数据库查询(L3 回源)
from src.modules.dictionary.dict_item.repository import DictItemRepository
items = DictItemRepository.get_all(dict_code=dict_code)
data = {str(item.value): item.label for item in items}
# 写回两级缓存
_cache[cache_key] = (data, now + CACHE_TTL)
redis.set(cache_key, json.dumps(data, ensure_ascii=False), ex=CACHE_TTL)
return data
def invalidate_dict_cache(dict_code: str):
"""失效指定字典的缓存(L1 + L2)"""
cache_key = f"dict:{dict_code}"
# 清除 L1(进程内缓存)
_cache.pop(cache_key, None)
# 清除 L2(Redis 缓存)
from src.core.redis_client import get_redis
redis = get_redis()
redis.delete(cache_key)
```
### 使用场景
| 场景 | 调用方式 | 说明 |
|:---|:---|:---|
| 枚举显示名 | `get_dict_map('status_type')` | 序列化时将值转为显示名 |
| 表单下拉 | `get_dict_map('gender')` | 前端获取字典项列表 |
| 业务校验 | `value in get_dict_map('xxx')` | 校验值是否在字典范围内 |
### 缓存失效
字典数据变更时(新增/编辑/删除字典项),主动清除两级缓存:
```python
# src/modules/dictionary/dict_item/service.py
class DictItemService(BaseService):
def add(self, data) -> R:
result = super().add(data)
if result.code == 0:
invalidate_dict_cache(data['dict_code']) # 新增成功后失效缓存
return result
def update(self, data) -> R:
result = super().update(data)
if result.code == 0:
invalidate_dict_cache(data['dict_code']) # 更新成功后失效缓存
return result
@classmethod
def before_delete(cls, ids):
"""删除前钩子:失效缓存"""
for obj_id in ids:
item = cls.repository.get_by_id(obj_id)
if item:
invalidate_dict_cache(item.dict_code)
return result
```
## L2 缓存:Redis 缓存
### Redis 连接管理
```python
# src/core/redis_client.py
import redis
# Redis 连接池
_pool = None
def get_redis():
"""获取 Redis 客户端"""
global _pool
if _pool is None:
_pool = redis.ConnectionPool(
host=REDIS_HOST,
port=REDIS_PORT,
password=REDIS_PASSWORD,
db=REDIS_DB,
decode_responses=True
)
return redis.Redis(connection_pool=_pool)
```
- 连接池在应用启动时创建,所有请求共享
- 同步客户端与 Flask 同步模型无缝集成
### 缓存应用场景
| 应用场景 | 数据结构 | Key 格式 | TTL | 说明 |
|:---|:---|:---|:---|:---|
| **JWT 黑名单** | String | `jwt:blacklist:{token_hash}` | Token 剩余有效期 | 登出时将 Token 加入黑名单 |
| **登录失败锁** | String | `login:lock:{username}` | 600 秒 | 连续 5 次失败后锁定 |
| **登录失败计数** | String (Lua) | `login:fail:{username}` | 300 秒 | 原子递增 + 首次设置 TTL |
| **滑动窗口限流** | String (Lua) | `rate:{ip}:{window_index}` | 2 倍窗口时长 | 滑动窗口计数器 |
| **数据字典缓存** | String (JSON) | `dict:{dict_code}` | 300 秒 | 字典项 {value: label} 映射 |
| **权限列表缓存** | String (JSON) | `perm:user:{user_id}` | 变更时失效 | 用户权限字符串列表 |
| **在线用户** | String | `online:user:{user_id}` | Token 有效期 | 在线用户 Token 存储 |
| **验证码** | String | `captcha:{uuid}` | 120 秒 | 图形验证码文本 |
### Lua 脚本
#### 原子递增 + 过期
```python
# src/core/redis_client.py
_INCR_EXPIRE_LUA = """
local count = redis.call('incr', KEYS[1])
if count == 1 then
redis.call('expire', KEYS[1], ARGV[1])
end
return count
"""
def incr_with_expire(redis_client, key: str, seconds: int) -> int:
"""原子执行 incr,并在 key 首次创建时一并设置过期时间"""
return redis_client.eval(_INCR_EXPIRE_LUA, 1, key, seconds)
```
:::tip 为什么需要 Lua 脚本?
`incr` 和 `expire` 分开执行时,两步之间若进程崩溃,key 会以无 TTL 的状态永久残留,导致基于 Redis 的限流/登录锁定永久封禁。Lua 脚本保证原子性,消除此风险。
:::
#### 滑动窗口限流
```python
_SLIDING_WINDOW_LUA = """
local cur = redis.call('incr', KEYS[1])
if cur == 1 then
redis.call('expire', KEYS[1], ARGV[1])
end
local prev = tonumber(redis.call('get', KEYS[2]) or '0')
local elapsed = tonumber(ARGV[3]) - tonumber(ARGV[2]) * tonumber(ARGV[1])
local f = elapsed / tonumber(ARGV[1])
if f < 0 then f = 0 elseif f > 1 then f = 1 end
local effective = prev * (1 - f) + cur
return effective
"""
```
滑动窗口限流通过当前窗口与上一窗口的计数按时间比例加权,消除固定窗口边界处的 2x 突发问题。
## 缓存策略对比
| 策略 | 说明 | 适用场景 | 应用 |
|:---|:---|:---|:---|
| **Cache-Aside** | 先查缓存,未命中查 DB,写回缓存 | 读多写少 | 数据字典、权限列表 |
| **Write-Through** | 写操作同时更新缓存 | 数据一致性要求高 | 字典项变更时主动失效 |
| **Write-Behind** | 写操作只更新缓存,异步写 DB | 写入性能要求高 | 未使用 |
| **TTL 过期** | 缓存自动过期 | 允许短暂不一致 | 登录锁、限流计数 |
| **主动失效** | 数据变更时主动清除缓存 | 数据一致性要求高 | 字典缓存、权限缓存 |
## 缓存失效策略
### 主动失效
数据变更时主动清除相关缓存,保证数据一致性:
```python
# 字典项变更 → 清除字典缓存
invalidate_dict_cache(dict_code)
# 角色菜单变更 → 清除该角色下所有用户的权限缓存
invalidate_perm_cache(user_id)
# 用户角色变更 → 清除该用户的权限缓存
invalidate_perm_cache(user_id)
```
### TTL 自动过期
对于允许短暂不一致的数据,使用 TTL 自动过期:
```
数据字典缓存:TTL 300 秒(5 分钟)
登录失败计数:TTL 300 秒(5 分钟)
滑动窗口限流:TTL 2 倍窗口时长
验证码:TTL 120 秒(2 分钟)
```
### 缓存穿透防护
:::warning 缓存穿透
当查询一个不存在的数据时,缓存和数据库都不会命中,导致每次请求都穿透到数据库。对于字典查询,如果 `dict_code` 不存在,应缓存空结果(空字典),避免反复查询数据库。
:::
```python
def get_dict_map(dict_code: str) -> Dict[str, str]:
# ... 缓存查询逻辑 ...
items = DictItemRepository.get_all(dict_code=dict_code)
data = {str(item.value): item.label for item in items}
# 即使 data 为空字典,也写入缓存,防止穿透
_cache[cache_key] = (data, now + CACHE_TTL)
return data
```
## 缓存监控
Redis 缓存的运行状态可通过以下方式监控:
1. **Redis INFO**:连接数、内存使用、命中率
2. **应用日志**:缓存命中/未命中日志(DEBUG 级别)
3. **Redis TTL**:检查 key 的剩余有效期
```bash
# 查看 Redis 缓存统计
redis-cli INFO stats | grep -E "keyspace_hits|keyspace_misses"
# 查看指定 key 的 TTL
redis-cli TTL "dict:status_type"
# 查看所有字典缓存 key
redis-cli KEYS "dict:*"
```
## 总结
项目采用进程内缓存(L1)+ Redis 缓存(L2)的两级缓存架构。L1 缓存提供纳秒级访问速度,适合高频读取的字典数据;L2 缓存提供跨进程数据共享,适合分布式场景。缓存失效采用主动失效 + TTL 过期的混合策略,数据变更时主动清除缓存保证一致性,允许短暂不一致的场景使用 TTL 自动过期。Lua 脚本保证 Redis 操作的原子性,防止进程崩溃导致的缓存异常。
@@ -0,0 +1,400 @@
# 数据库设计
本章详细描述项目的数据库设计,包括核心表结构、通用字段规范、ER 关系和命名约定。
:::tip 设计原则
所有业务表继承 `BaseModel` 基类,自动获得 6 个通用字段(id / create_user / create_time / update_user / update_time / is_delete)。表名使用 `DB_PREFIX` 前缀(默认 `flask_`),软删除通过 `is_delete` 字段实现。
:::
## 通用字段规范
所有业务表共享以下通用字段,由 `BaseModel` 基类自动提供:
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|:---|:---|:---|:---|:---|
| `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=已删除 |
## 核心表结构
### 系统管理模块
#### 用户表 `flask_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 | 通用字段 |
#### 角色表 `flask_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 | 排序 |
| remark | VARCHAR(500) | NULLABLE | 备注 |
| + 通用字段 | | | |
#### 菜单表 `flask_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=禁用 |
| + 通用字段 | | | |
#### 部门表 `flask_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=禁用 |
| + 通用字段 | | | |
#### 岗位表 `flask_position`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 岗位ID |
| name | VARCHAR(255) | NOT NULL, INDEX | 岗位名称 |
| status | INT | DEFAULT 0, INDEX | 状态:1=在用 2=停用 |
| sort | INT | DEFAULT 0 | 排序 |
| + 通用字段 | | | |
#### 职级表 `flask_level`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 职级ID |
| name | VARCHAR(255) | NOT NULL, INDEX | 职级名称 |
| status | INT | DEFAULT 0, INDEX | 状态:1=在用 2=停用 |
| sort | INT | DEFAULT 0 | 排序 |
| + 通用字段 | | | |
### 关联表
#### 用户角色关联表 `flask_user_role`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 主键 |
| user_id | INT | NOT NULL, INDEX | 用户ID |
| role_id | INT | NOT NULL, INDEX | 角色ID |
| + 通用字段 | | | |
#### 角色菜单关联表 `flask_role_menu`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 主键 |
| role_id | INT | NOT NULL, INDEX | 角色ID |
| menu_id | INT | NOT NULL, INDEX | 菜单ID |
| + 通用字段 | | | |
### 数据字典模块
#### 字典表 `flask_dict`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 字典ID |
| name | VARCHAR(200) | NOT NULL | 字典名称 |
| code | VARCHAR(200) | NOT NULL, UNIQUE | 字典编码 |
| status | INT | DEFAULT 1 | 状态 |
| remark | VARCHAR(500) | NULLABLE | 备注 |
| + 通用字段 | | | |
#### 字典项表 `flask_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 | 状态 |
| remark | VARCHAR(500) | NULLABLE | 备注 |
| + 通用字段 | | | |
### 内容管理模块
#### 文章表 `flask_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 | 排序 |
| + 通用字段 | | | |
#### 文章分类表 `flask_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 | 状态 |
| + 通用字段 | | | |
#### 通知公告表 `flask_notice`
| 字段名 | 类型 | 约束 | 说明 |
|:---|:---|:---|:---|
| id | INT | PK | 通知ID |
| title | VARCHAR(200) | NOT NULL | 标题 |
| content | TEXT | NULLABLE | 内容 |
| type | INT | DEFAULT 1 | 类型:1=通知 2=公告 |
| status | INT | DEFAULT 1 | 状态 |
| + 通用字段 | | | |
### 日志模块
#### 登录日志表 `flask_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 | 提示消息 |
| + 通用字段 | | | |
#### 操作日志表 `flask_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 | 耗时(毫秒) |
| + 通用字段 | | | |
### 定时任务模块
#### 任务表 `flask_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=暂停 |
| remark | VARCHAR(500) | NULLABLE | 备注 |
| + 通用字段 | | | |
#### 任务日志表 `flask_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 关系图
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ flask │ │ flask │ │ flask │
│ _user │ │ _user_role │ │ _role │
│──────────────│ │──────────────│ │──────────────│
│ id (PK) │←───│ user_id (FK) │ │ id (PK) │
│ username │ │ role_id (FK) │───→│ name │
│ password │ └──────────────┘ │ code │
│ realname │ └──────┬───────┘
│ dept_id (FK) │───→┌──────────────┐ │
│ position_id │ │ flask │ │
│ (FK) │ │ _dept │ │
│ level_id (FK)│ │──────────────│ │
│ │ │ id (PK) │ │
│ │ │ parent_id │ │
│ │ │ name │ │
│ │ └──────────────┘ │
│ │ │
│ │ ┌──────────────┐ │
│ │ │ flask │ │
│ │ │ _role_menu │ │
│ │ │──────────────│ │
│ │ │ role_id (FK) │←───────────┘
│ │ │ menu_id (FK) │───→┌──────────────┐
│ │ └──────────────┘ │ flask │
└──────────────┘ │ _menu │
│──────────────│
┌──────────────┐ │ id (PK) │
│ flask │ │ parent_id │
│ _position │ │ name │
│──────────────│ │ permission │
│ id (PK) │ │ type │
│ name │ └──────────────┘
└──────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ flask │ │ flask │ │ flask │
│ _dict │ │ _dict_item │ │ _config │
│──────────────│ │──────────────│ │──────────────│
│ id (PK) │←───│ dict_code │ │ id (PK) │
│ name │ │ label │ │ name │
│ code │ │ value │ │ code │
└──────────────┘ └──────────────┘ └──────────────┘
┌──────────────┐ ┌──────────────┐
│ flask │ │ flask │
│ _article │ │ _category │
│──────────────│ │──────────────│
│ id (PK) │ │ id (PK) │
│ category_id │───→│ parent_id │
│ title │ │ name │
│ content │ └──────────────┘
└──────────────┘
```
## 命名约定
| 约定项 | 规范 | 示例 |
|:---|:---|:---|
| 表名 | `{DB_PREFIX}{module_name}` | `flask_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` 环境变量切换数据库驱动,`config/database.py` 的 `_build_database_url()` 函数按驱动构建方言感知的 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 编码 |
业务代码无需修改,SQLAlchemy 自动处理方言差异(如分页语法、自增策略等)。
### 请求级会话管理
Flask 使用 `g` 对象和 `before_request` / `after_request` 钩子管理数据库会话:
```python
# src/middleware/transaction.py
from flask import request, g
from src.core.database import db
def register_transaction(app):
@app.before_request
def _transaction_begin():
g.transaction_active = True
@app.after_request
def _transaction_commit(response):
if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
if response.status_code == 200:
body = response.get_json()
if body and body.get('code') == 0:
db.session.commit()
else:
db.session.rollback()
return response
@app.teardown_request
def _transaction_rollback(exception):
if exception:
db.session.rollback()
db.session.remove()
```
:::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` 实现软删除,通过应用层代码维护引用完整性。核心表覆盖系统管理(用户/角色/菜单/部门/岗位/职级)、数据字典、内容管理(文章/分类/通知)、日志管理、定时任务等功能域。多数据库驱动通过 `config/database.py` 的 `_build_database_url()` 按方言构建引擎,业务代码与具体数据库解耦。请求级会话通过 Flask 的 `g` 对象和 `before_request` / `after_request` 钩子实现,审计数据可通过 `commit_independently()` 绕过请求事务独立提交。
@@ -0,0 +1,269 @@
# 目录结构设计
本章详细说明项目目录结构设计,包括后端 `src/` 和前端 `ui/src/` 的完整目录树及各目录的设计意图。
:::tip 设计原则
目录结构遵循「按功能分组、按职责分层」的原则。后端 `src/` 按技术层次组织(core / middleware / routes / modules),前端 `ui/src/` 按功能领域组织(api / views / components / store)。每个目录有明确的边界和单一职责。
:::
## 后端目录结构
```
src/
├── app.py # 应用入口:create_app() 组装中间件、路由、异常处理
├── core/ # 核心基础设施层
│ ├── __init__.py # 统一导出
│ ├── database.py # 数据库引擎:SQLAlchemy 初始化
│ ├── redis_client.py # Redis 客户端:连接池、Lua 脚本
│ ├── jwt.py # JWT 工具:签发 / 解析 / 续签 / 黑名单
│ ├── password.py # 密码工具:bcrypt 哈希 + 盐值
│ ├── security.py # 安全工具:get_user_id / get_username / get_realname
│ ├── response.py # 统一响应:R.ok() / R.failed() / R.page()
│ ├── exceptions.py # 自定义异常:BizError
│ ├── permission.py # 权限装饰器:check_permission
│ ├── logger.py # 日志配置:结构化日志、文件输出
│ └── scheduler.py # 定时任务调度器
│
├── middleware/ # HTTP 中间件层
│ ├── __init__.py # 统一导出
│ ├── auth.py # JWT 认证中间件:login_required
│ ├── cors.py # CORS 跨域中间件
│ ├── demo.py # 演示模式中间件:check_demo
│ ├── errors.py # 全局异常处理
│ ├── operation_log.py # 操作日志中间件
│ └── transaction.py # AOP 事务管理中间件
│
├── common/ # 公共模块层
│ ├── __init__.py # 统一导出
│ ├── base_model.py # 模型基类:通用字段 + to_dict() 序列化
│ ├── base_db.py # 数据库操作基类:save() 持久化方法
│ ├── base_repository.py # 仓库基类:通用 CRUD + 软删除 + 分页
│ ├── base_service.py # 服务基类:模板方法 CRUD + 差异点钩子
│ ├── forms/ # 表单基类
│ │ ├── __init__.py # 统一导出
│ │ ├── base_form.py # BaseForm 基类
│ │ └── fields.py # 自定义字段
│ ├── constants/ # 常量与枚举
│ │ ├── __init__.py # 统一导出
│ │ └── common.py # 通用常量
│ └── utils/ # 工具函数
│ ├── __init__.py # 统一导出
│ ├── captcha.py # 验证码生成
│ ├── case_converter.py # 驼峰/下划线互转
│ ├── excel_utils.py # Excel 导入导出
│ ├── file.py # 文件工具
│ ├── ip.py # IP 工具:客户端 IP 提取
│ ├── ip2region.py # IP 地理位置解析
│ ├── json.py # JSON 工具
│ ├── model_utils.py # 模型工具
│ ├── regular.py # 正则工具
│ ├── request.py # 请求工具:parse_request_body / parse_id_list
│ ├── rich_text.py # 富文本处理
│ ├── time_utils.py # 时间工具
│ └── user_agent.py # User-Agent 解析
│
├── routes/ # 路由装配层
│ ├── __init__.py # 统一导出
│ └── v1.py # API v1 路由注册中心
│
├── modules/ # 业务模块层(纯业务逻辑)
│ ├── auth/ # 认证模块
│ │ ├── router.py # 登录/登出/验证码/Token 刷新
│ │ ├── schema.py # 登录表单校验
│ │ └── service.py # 登录/登出/验证码/锁定逻辑
│ ├── system/ # 系统管理模块组
│ │ ├── position/ # 岗位管理
│ │ │ ├── models.py # Position 模型
│ │ │ ├── repository.py # PositionRepository
│ │ │ ├── schema.py # PositionForm / PositionStatusForm
│ │ │ ├── service.py # PositionService
│ │ │ └── router.py # 路由定义
│ │ ├── level/ # 职级管理(同结构)
│ │ ├── dept/ # 部门管理(支持树形)
│ │ ├── role/ # 角色管理
│ │ ├── menu/ # 菜单管理(支持树形)
│ │ └── user/ # 用户管理(复杂模块)
│ ├── association/ # 关联模块组
│ │ ├── role_menu/ # 角色菜单关联
│ │ └── user_role/ # 用户角色关联
│ ├── dictionary/ # 字典模块组
│ │ ├── dict/ # 字典
│ │ └── dict_item/ # 字典项
│ ├── configuration/ # 配置模块组
│ │ ├── config/ # 配置
│ │ └── config_item/ # 配置项
│ ├── cms/ # 内容管理模块组
│ │ ├── article/ # 文章管理
│ │ ├── category/ # 文章分类
│ │ └── link/ # 友链管理
│ ├── city/ # 城市/行政区划
│ ├── notice/ # 通知公告
│ ├── param/ # 参数管理
│ ├── job/ # 定时任务(含调度器)
│ ├── job_log/ # 任务执行日志
│ ├── login_log/ # 登录日志
│ ├── operation_log/ # 操作日志
│ ├── upload/ # 文件上传
│ ├── file_template/ # 文件模板
│ ├── generator/ # 代码生成器
│ └── example/ # 案例演示
│
├── models_registry.py # 模型注册表
│
└── migrations/ # Flask-Migrate 迁移
├── env.py # 迁移环境配置
└── versions/ # 迁移版本文件
└── 0001_baseline.py # 基线迁移
```
### 目录设计意图
| 目录 | 设计意图 |
|:---|:---|
| `core/` | 基础设施层,提供所有模块共用的工具和配置。不包含业务逻辑 |
| `middleware/` | 中间件独立于 core,职责单一,便于按需注册和测试 |
| `common/` | 公共模块层,提供基类、常量和工具函数 |
| `routes/` | 路由装配层,将所有模块的 Blueprint 集中注册 |
| `modules/` | 业务模块层,按功能域分组(system / dictionary / configuration / cms),每个模块包含 models + repository + schema + service + router |
| `modules/{group}/` | 模块组:将关联模块归组(如 system 下的 user / role / menu / dept) |
## 前端目录结构
```
ui/src/
├── main.ts # 应用入口
├── App.vue # 根组件
├── api/ # API 接口层(按后端模块对应)
│ ├── system/ # 系统管理 API
│ │ ├── position.ts # 岗位管理 API
│ │ ├── level.ts # 职级管理 API
│ │ ├── dept.ts # 部门管理 API
│ │ ├── role.ts # 角色管理 API
│ │ ├── menu.ts # 菜单管理 API
│ │ └── user.ts # 用户管理 API
│ ├── content/ # 内容管理 API
│ │ ├── article.ts # 文章管理 API
│ │ ├── category.ts # 文章分类 API
│ │ ├── notice.ts # 通知公告 API
│ │ └── link.ts # 友链管理 API
│ ├── data/ # 数据管理 API
│ │ ├── dict.ts # 字典管理 API
│ │ ├── config.ts # 配置管理 API
│ │ └── param.ts # 参数管理 API
│ ├── common/ # 公共 API
│ │ └── index.ts # 登录/登出/验证码
│ ├── dashboard/ # 仪表盘 API
│ ├── file/ # 文件管理 API
│ ├── monitor/ # 监控 API
│ ├── region/ # 行政区划 API
│ ├── setting/ # 设置 API
│ └── tool/ # 工具 API
│
├── views/ # 页面视图(按功能域分组)
│ ├── login/ # 登录页
│ ├── dashboard/ # 仪表盘
│ ├── system/ # 系统管理页面
│ │ ├── position/ # 岗位管理页面
│ │ ├── level/ # 职级管理页面
│ │ ├── dept/ # 部门管理页面
│ │ ├── role/ # 角色管理页面
│ │ ├── menu/ # 菜单管理页面
│ │ └── user/ # 用户管理页面
│ ├── content/ # 内容管理页面
│ ├── data/ # 数据管理页面
│ ├── monitor/ # 监控页面
│ ├── setting/ # 设置页面
│ ├── tool/ # 工具页面
│ ├── file/ # 文件管理页面
│ ├── exception/ # 异常页面(404/403/500)
│ ├── about/ # 关于页面
│ ├── iframe/ # 内嵌页面
│ └── redirect/ # 重定向页面
│
├── components/ # 全局公共组件
│ ├── Table/ # 表格组件
│ ├── Form/ # 表单组件
│ ├── Modal/ # 弹窗组件
│ ├── Upload/ # 上传组件
│ ├── Editor/ # 富文本编辑器
│ ├── Excel/ # Excel 导入导出
│ ├── Select/ # 选择器组件
│ ├── Cropper/ # 图片裁剪
│ ├── Qrcode/ # 二维码
│ ├── Page/ # 分页组件
│ ├── Password/ # 密码组件
│ ├── Authority/ # 权限组件
│ ├── CheckToken/ # Token 检查
│ ├── ChinaArea/ # 中国行政区划
│ ├── Websocket/ # WebSocket 组件
│ ├── Lockscreen/ # 锁屏
│ ├── CountTo/ # 数字动画
│ ├── Render/ # 自定义渲染
│ ├── Application/# 应用组件
│ ├── Region/ # 区域选择
│ ├── TableSelect/# 表格选择
│ ├── icon/ # 图标组件
│ ├── importFile/ # 文件导入
│ ├── numberInput/# 数字输入
│ └── pagination/ # 分页组件
│
├── store/ # Pinia 状态管理
│ └── modules/ # 状态模块
│ ├── user.ts # 用户状态(Token/用户信息/权限)
│ ├── app.ts # 应用状态(主题/布局/语言)
│ ├── permission.ts # 权限状态(动态路由)
│ ├── tabs.ts # 标签页状态
│ └── dict.ts # 字典缓存
│
├── router/ # Vue Router 路由
│ ├── index.ts # 路由配置入口
│ └── menus/ # 菜单路由
│
├── hooks/ # Composition API Hooks
│ ├── core/ # 核心 Hooks
│ ├── event/ # 事件 Hooks
│ ├── setting/ # 设置 Hooks
│ └── web/ # Web Hooks
│
├── layout/ # 布局组件
│ └── components/ # 布局子组件(侧边栏/头部/标签页)
│
├── directives/ # 自定义指令
├── enums/ # 前端枚举
├── plugins/ # 插件
├── settings/ # 应用设置
├── styles/ # 全局样式
│ ├── theme/ # 主题样式
│ └── transition/ # 过渡动画
├── utils/ # 工具函数
│ ├── http/ # Axios 封装
│ ├── file/ # 文件工具
│ ├── helper/ # 辅助函数
│ ├── is/ # 类型判断
│ └── lib/ # 第三方库封装
└── assets/ # 静态资源
├── icons/ # 图标
└── images/ # 图片
```
### 前后端目录对应关系
| 前端目录 | 后端目录 | 说明 |
|:---|:---|:---|
| `api/system/position.ts` | `modules/system/position/router.py` | API 接口定义一一对应 |
| `views/system/position/` | `modules/system/position/` | 页面视图对应业务模块 |
| `store/modules/user.ts` | `modules/auth/service.py` | 用户状态对应认证服务 |
| `components/` | `common/utils/` + `core/` | 公共组件对应后端工具 |
## 目录规范
:::warning 目录命名规范
1. **后端**:目录名使用小写下划线(snake_case),与 Python 模块命名一致
2. **前端**:目录名使用小写 kebab-case 或 camelCase,与 Vue 生态习惯一致
3. **模块文件**:每个模块固定包含 `models.py` / `repository.py` / `schema.py` / `service.py` / `router.py` 五个文件
4. **路由文件**:每个模块对应一个路由文件,文件名与模块名一致
:::
## 总结
目录结构是项目可维护性的基础。后端按技术层次(core / middleware / common / routes / modules)组织,前端按功能领域(api / views / components / store)组织。每个目录有明确的边界和单一职责,模块内部遵循固定的文件结构(models + repository + schema + service + router)。这种设计使得新开发者能快速定位代码,新模块能按既定模式快速创建。
@@ -0,0 +1,584 @@
# 扩展性设计
本章详细描述系统的扩展性设计,包括模块可插拔、数据库驱动可切换、中间件可插拔三大扩展维度。
:::tip 扩展性设计原则
遵循开闭原则(OCP):对扩展开放,对修改关闭。新功能通过添加新模块实现,而非修改现有代码。核心框架通过基类、装饰器、中间件等机制提供扩展点,业务模块只需声明差异即可接入。
:::
## 一、模块可插拔设计
### 模块标准结构
每个业务模块遵循固定的五文件结构,新增模块只需创建这五个文件即可接入系统:
```
src/modules/{group}/{name}/
├── models.py # ORM 模型
├── repository.py # 数据访问层
├── schema.py # 请求校验
├── service.py # 业务逻辑层
└── router.py # 路由定义
```
### 新增模块步骤
以新增「培训管理(Training)」模块为例。
**步骤一:创建模块目录和文件**
创建 `src/modules/system/training/` 目录,包含以下五个文件:
`models.py` — ORM 模型:
```python
# src/modules/system/training/models.py
from sqlalchemy import Column, String, Integer, text
from src.common.base_db import BaseDB
from src.common.base_model import BaseModel
from config.database import DB_PREFIX
class Training(BaseModel, BaseDB):
__tablename__ = DB_PREFIX + "training"
__table_comment__ = "培训记录表"
title = Column(String(200), nullable=False, comment="培训标题")
content = Column(String(2000), nullable=True, comment="培训内容")
trainer = Column(String(50), nullable=True, comment="培训讲师")
status = Column(Integer, default=1, server_default=text('1'), comment="状态")
sort = Column(Integer, default=0, server_default=text('0'), comment="排序")
```
`repository.py` — 数据访问层:
```python
# src/modules/system/training/repository.py
from src.modules.system.training.models import Training
from src.common.base_repository import BaseRepository
class TrainingRepository(BaseRepository):
model = Training
_training_repository = TrainingRepository()
```
`schema.py` — 请求校验:
```python
# src/modules/system/training/schema.py
from wtforms import StringField, IntegerField
from wtforms.validators import DataRequired, Length, NumberRange
from src.common.forms.base_form import BaseForm
class TrainingForm(BaseForm):
title = StringField('培训标题', validators=[
DataRequired(message='培训标题不能为空'),
Length(min=1, max=200, message='培训标题长度必须在1-200个字符之间')
])
content = StringField('培训内容', validators=[
Length(max=2000, message='培训内容长度不能超过2000个字符')
])
trainer = StringField('培训讲师', validators=[
Length(max=50, message='培训讲师长度不能超过50个字符')
])
status = IntegerField('状态', validators=[
DataRequired(message='状态不能为空'),
NumberRange(min=1, max=2, message='状态必须是1或2')
])
sort = IntegerField('排序', validators=[
DataRequired(message='排序不能为空'),
NumberRange(min=0, max=99999, message='排序必须在0-99999之间')
])
class TrainingStatusForm(BaseForm):
id = IntegerField('培训ID', validators=[
DataRequired(message='培训ID不能为空'),
NumberRange(min=1, message='培训ID必须大于0')
])
status = IntegerField('状态', validators=[
DataRequired(message='状态不能为空'),
NumberRange(min=1, max=2, message='状态必须是1或2')
])
```
`service.py` — 业务逻辑层:
```python
# src/modules/system/training/service.py
from src.common.base_service import BaseService
from src.modules.system.training.models import Training
from src.modules.system.training.repository import TrainingRepository
class TrainingService(BaseService):
repository = TrainingRepository
form = TrainingForm
status_form = TrainingStatusForm
model_name = "培训"
# 模块级代理函数
get_training_page = TrainingService.get_page
get_training_detail = TrainingService.get_detail
add_training = TrainingService.add
update_training = TrainingService.update
delete_trainings = TrainingService.remove
update_training_status = TrainingService.update_status
```
`router.py` — 路由定义:
```python
# src/modules/system/training/router.py
from flask import Blueprint, request
from src.core.permission import check_permission
from src.core.response import R
from src.common.utils.request import parse_request_body, parse_id_list
training_blue = Blueprint('training', __name__, url_prefix='/training')
@training_blue.route('/page', methods=['GET'])
@check_permission('sys:training:page')
def page():
from src.modules.system.training.service import get_training_page
return get_training_page()
@training_blue.route('/detail/<int:id>', methods=['GET'])
@check_permission('sys:training:detail')
def detail(id):
from src.modules.system.training.service import get_training_detail
return R.ok(data=get_training_detail(id))
@training_blue.route('/add', methods=['POST'])
@check_permission('sys:training:add')
def add():
from src.modules.system.training.service import add_training
from src.modules.system.training.schema import TrainingForm
data = parse_request_body(request, TrainingForm)
return add_training(data)
@training_blue.route('/update', methods=['PUT'])
@check_permission('sys:training:update')
def update():
from src.modules.system.training.service import update_training
from src.modules.system.training.schema import TrainingForm
data = parse_request_body(request, TrainingForm)
return update_training(data)
@training_blue.route('/delete/<int:id>', methods=['DELETE'])
@check_permission('sys:training:delete')
def delete(id):
from src.modules.system.training.service import delete_training
return delete_training(id)
@training_blue.route('/status', methods=['PUT'])
@check_permission('sys:training:status')
def status():
from src.modules.system.training.service import update_training_status
from src.modules.system.training.schema import TrainingStatusForm
data = parse_request_body(request, TrainingStatusForm)
return update_training_status(data)
@training_blue.route('/batchDelete', methods=['DELETE'])
@check_permission('sys:training:batchDelete')
def batch_delete():
from src.modules.system.training.service import batch_delete_trainings
id_list = parse_id_list(request)
return batch_delete_trainings(id_list)
```
**步骤二:注册路由**
在 `src/routes/v1.py` 中添加:
```python
# src/routes/v1.py
from src.modules.system.training.router import training_blue
BLUEPRINTS = [
# ... 其他模块
training_blue,
]
```
路由注册后,该模块的所有端点自动挂在 `/api/v1/training` 前缀下,并通过 `src/routes/v1.py` 的 `register_v1()` 统一挂载到 Flask 应用实例。
**步骤三:配置菜单权限**
在数据库 `flask_menu` 表中插入对应的菜单和权限节点记录。权限标识格式为 `sys:{module}:{action}`,如 `sys:training:add`、`sys:training:page`。
### 模块解耦
模块之间通过以下方式解耦:
| 解耦方式 | 说明 | 示例 |
|:---|:---|:---|
| **Repository 单例** | 模块级单例,通过 import 引用 | `from src.modules.system.user.repository import UserRepository` |
| **Service 代理函数** | 模块级代理函数,通过 import 引用 | `from src.modules.system.user.service import get_user_page` |
| **跨模块引用** | 通过 Repository 引用其他模块的模型 | PositionService 引用 User 模型检查引用 |
| **事件机制** | 暂未实现(未来可通过事件总线解耦) | — |
### 复杂模块扩展
对于业务逻辑复杂的模块(如 User、Role、Menu),在继承 `BaseService` 的基础上添加自定义方法:
```python
# src/modules/system/user/service.py
class UserService(BaseService):
repository = UserRepository
form = UserForm
status_form = UserStatusForm
model_name = "用户"
@classmethod
def add(cls, data: dict) -> None:
"""新增用户(覆写基类方法)"""
form = cls._validate_form(data)
# 密码加密
pwd = form.data.get("password") or DEFAULT_PASSWORD
salt = generate_salt()
create_data = cls._clean_form_data(form)
create_data["password"] = encrypt_password(pwd, salt)
create_data["salt"] = salt
create_data["create_user"] = get_realname()
user = cls.repository.create(**create_data)
user.code = f"{user.id:06d}"
@classmethod
def _detail_dict(cls, obj):
"""详情序列化:剔除密码字段,补充关联数据"""
data = model_to_dict(obj)
if data:
data.pop("password", None)
return _enrich_user_detail(data)
@classmethod
def before_delete(cls, ids):
"""删除前校验:超管禁止删除"""
if 1 in ids:
raise BizError("超级管理员不能删除")
```
## 二、数据库驱动可切换
### 驱动注册机制
项目通过 `_SUPPORTED_DRIVERS` 声明支持的驱动,`_normalize_driver()` 处理别名映射和校验,`_build_database_url()` 按驱动构建连接串:
```python
# config/database.py(简化)
import os
from urllib.parse import quote_plus
# 支持的数据库驱动
_SUPPORTED_DRIVERS = ("mysql", "postgresql", "mssql", "sqlite", "oracle")
def _normalize_driver(driver: str) -> str:
"""归一化 DB_DRIVER:别名映射 + 校验"""
name = str(driver).strip().lower()
alias_map = {
'postgres': 'postgresql',
'pg': 'postgresql',
'sqlserver': 'mssql',
'sql_server': 'mssql',
'sqlite3': 'sqlite',
}
name = alias_map.get(name, name)
if name not in _SUPPORTED_DRIVERS:
raise ValueError(f"不支持的 DB_DRIVER: {driver!r}")
return name
DB_DRIVER = _normalize_driver(os.getenv('DB_DRIVER', 'mysql'))
DB_HOST = os.getenv('DB_HOST', '127.0.0.1')
DB_PORT = int(os.getenv('DB_PORT', '3306'))
DB_DATABASE = os.getenv('DB_DATABASE', 'djangoadmin.flask.elevue')
DB_USERNAME = os.getenv('DB_USERNAME', 'root')
DB_PASSWORD = os.getenv('DB_PASSWORD', '')
def _build_database_url(driver: str = None) -> str:
"""按驱动构建 SQLAlchemy 连接串"""
drv = _normalize_driver(driver if driver is not None else DB_DRIVER)
if drv == 'sqlite':
return 'sqlite:///./' + DB_DATABASE
base = quote_plus(DB_USERNAME) + ':' + quote_plus(DB_PASSWORD) + '@' + DB_HOST + ':' + str(DB_PORT)
if drv == 'mysql':
return 'mysql+pymysql://' + base + '/' + DB_DATABASE + '?charset=utf8mb4'
if drv == 'postgresql':
return 'postgresql+psycopg://' + base + '/' + DB_DATABASE
if drv == 'oracle':
return 'oracle+oracledb://' + base + '/?service_name=' + quote_plus(DB_DATABASE)
# mssql
return 'mssql+pymssql://' + base + '/' + DB_DATABASE + '?charset=utf8'
```
各驱动的连接串格式:
| 驱动 | 连接串格式 | Python 包 |
|:----|:----|:----|
| mysql | `mysql+pymysql://user:pass@host:port/db?charset=utf8mb4` | PyMySQL |
| postgresql | `postgresql+psycopg://user:pass@host:port/db` | psycopg[binary] |
| mssql | `mssql+pymssql://user:pass@host:port/db?charset=utf8` | pymssql |
| oracle | `oracle+oracledb://user:pass@host:port/?service_name=db` | oracledb |
| sqlite | `sqlite:///./path/to/db` | 内置 |
### 切换方式
只需修改 `.env` 文件中的 `DB_DRIVER` 和对应的连接参数:
```bash
# MySQL(默认)
DB_DRIVER=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=djangoadmin
DB_USERNAME=root
DB_PASSWORD=your_password
# PostgreSQL
DB_DRIVER=postgresql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=djangoadmin
DB_USERNAME=postgres
DB_PASSWORD=your_password
# SQLite
DB_DRIVER=sqlite
DB_DATABASE=./data.db
# SQL Server
DB_DRIVER=mssql
DB_HOST=127.0.0.1
DB_PORT=1433
DB_DATABASE=djangoadmin
DB_USERNAME=sa
DB_PASSWORD=your_password
```
:::warning 驱动切换注意事项
1. 切换驱动后需安装对应的 Python 包(如 `psycopg[binary]` for PostgreSQL)
2. 数据迁移使用 `scripts/migrate_db.py` 工具
3. SQLite 不支持并发写入,仅适用于开发/测试环境
4. Oracle 需要额外的客户端库配置
:::
### 跨库数据迁移
```bash
# 干跑:核对迁移计划
python scripts/migrate_db.py --src-driver mysql --src-host ... --dst-driver postgresql --dst-host ... --dry-run
# 执行迁移
python scripts/migrate_db.py --src-driver mysql --src-host ... --dst-driver postgresql --dst-host ...
# 幂等重跑(先删除目标库数据)
python scripts/migrate_db.py --src-driver mysql --src-host ... --dst-driver postgresql --dst-host ... --drop-target-first
```
## 三、中间件可插拔
### 中间件注册机制
所有中间件在 `src/app.py` 的 `create_app()` 中按顺序注册。新增中间件只需:
1. 在 `src/middleware/` 目录创建中间件文件
2. 在 `create_app()` 中注册
```python
# src/app.py - create_app() 中间件注册顺序
def create_app():
app = Flask(__name__)
# 先注册 → 内层(最后处理请求)
register_redis(app) # Redis 上下文注入
register_db_session(app) # DB 会话管理(commit/rollback)
register_operation_log(app) # 操作日志记录
register_transaction(app) # AOP 事务管理
register_v1(app) # 路由注册(login_required 依赖)
register_exception(app) # 全局异常处理器
# 请求计时(先注册→内层,统计业务耗时)
@app.before_request
def start_timer():
g.start_time = time.time()
@app.after_request
def add_process_time_header(response):
if hasattr(g, 'start_time'):
process_time = time.time() - g.start_time
response.headers["X-Process-Time"] = str(round(process_time, 5))
return response
register_rate_limit(app) # 滑动窗口限流
register_cors(app) # CORS(最后注册→最外层)
return app
```
### 新增中间件示例
```python
# src/middleware/custom_middleware.py
from flask import request, g
def register_custom(app):
"""注册自定义中间件"""
@app.before_request
def custom_before():
"""请求前逻辑"""
g.custom_data = "xxx"
@app.after_request
def custom_after(response):
"""响应后逻辑"""
response.headers["X-Custom-Header"] = "value"
return response
```
```python
# src/app.py
from src.middleware.custom_middleware import register_custom
def create_app():
# ... 其他中间件 ...
register_custom(app)
# ...
```
### 中间件执行顺序
```
请求进入 → CORS → 限流 → 上传限制 → 计时 → 操作日志 → DB会话 → Redis注入 → 路由处理
│
响应返回 ← CORS ← 限流 ← 上传限制 ← 计时 ← 操作日志 ← DB会话 ← Redis注入 ← 路由处理 ←┘
```
:::warning 中间件注册顺序
Flask 中间件按注册顺序执行。`register_cors` 最后注册 → 最外层(最先处理请求)。`register_redis` 最先注册 → 最内层(最后处理请求)。
:::
## 四、装饰器扩展
### 自定义装饰器
通过 `check_permission` 和 `check_demo` 的模式,可以创建自定义装饰器:
```python
# src/common/decorators.py
import time
from functools import wraps
def log_operation(module: str, action: str):
"""
操作日志记录装饰器
自动记录接口调用的模块、操作类型、执行耗时
Args:
module: 所属模块名称,如 "培训管理"
action: 操作类型,如 "新增"、"编辑"、"删除"
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 记录开始时间,用于计算接口执行耗时
start = time.time()
# 执行原业务函数
result = func(*args, **kwargs)
# 计算耗时(秒)
duration = time.time() - start
# 记录操作日志到数据库(包含模块、操作、耗时等信息)
save_log(module, action, duration)
return result
return wrapper
return decorator
# src/modules/system/training/router.py
# 使用示例:多个装饰器叠加使用
# 执行顺序:从下往上执行(先 log_operation,再 check_permission)
@training_blue.route('/add', methods=['POST'])
@check_permission('sys:training:add') # 权限校验:需要培训新增权限
@log_operation("培训管理", "新增") # 操作日志记录:记录本次操作
def add():
"""新增培训记录接口"""
from src.modules.system.training.service import add_training
from src.modules.system.training.schema import TrainingForm
data = parse_request_body(request, TrainingForm)
return add_training(data)
```
## 五、配置扩展
### 环境变量驱动
所有可配置项通过 `.env` 文件管理,新增配置项只需:
1. 在 `.env` 文件中添加变量
2. 在 `config/` 对应模块中读取
3. 在业务代码中引用
```bash
# .env
CUSTOM_FEATURE_ENABLED=true
CUSTOM_TIMEOUT=30
```
```python
# config/custom.py
import os
CUSTOM_FEATURE_ENABLED = os.getenv("CUSTOM_FEATURE_ENABLED", "false").lower() == "true"
CUSTOM_TIMEOUT = int(os.getenv("CUSTOM_TIMEOUT", "30"))
```
## 六、前端扩展
### 新增页面
1. 在 `ui/src/api/` 创建 API 文件
2. 在 `ui/src/views/` 创建页面组件
3. 在数据库 `flask_menu` 表中配置菜单路由
### 新增组件
公共组件放在 `ui/src/components/` 目录,遵循 Vue3 组件规范。
## 扩展性矩阵
| 扩展维度 | 扩展方式 | 修改范围 | 复杂度 |
|:---|:---|:---|:---|
| 新增 CRUD 模块 | 创建 5 文件 + 注册路由 | 新增文件,不改现有代码 | 低 |
| 新增复杂模块 | 继承基类 + 自定义方法 | 新增文件,不改现有代码 | 中 |
| 切换数据库驱动 | 修改 .env 配置 | 仅改配置 | 低 |
| 新增中间件 | 创建中间件 + 注册 | 新增文件 + 改 app.py | 低 |
| 新增装饰器 | 创建装饰器 + 应用 | 新增文件 | 低 |
| 新增配置项 | .env + config 模块 | 新增配置 | 低 |
| 新增前端页面 | API + Views + Menu | 新增文件 + 改数据库 | 中 |
## 总结
通过分层架构、基类模板方法、装饰器、中间件、环境变量配置等机制,实现了高度的可扩展性。新增 CRUD 模块只需 5 个标准文件 + 1 行路由注册,不修改任何现有代码。数据库驱动通过环境变量切换,中间件通过注册机制插拔。这种设计使得系统在功能持续增长的同时,保持代码结构的清晰和稳定。
@@ -0,0 +1,198 @@
# Flask g 对象与请求上下文
:::tip 本章概要
Flask 的 `g` 对象是请求级别的全局变量容器,用于在同一请求的不同函数间共享数据。本文介绍 `g` 对象的使用方式、请求上下文机制,以及项目中 `g` 对象的实际应用场景。
:::
## 什么是 g 对象
`g`(global 的缩写)是 Flask 提供的请求级全局变量对象。它的生命周期与请求绑定:
```
请求进入 → 创建 g 对象
↓
路由处理 → 通过 g 存取数据
↓
响应返回 → 销毁 g 对象
```
:::warning 线程安全
每个请求有独立的 `g` 对象,不同请求之间的 `g` 互不影响。在多线程环境下,每个线程处理的请求都有自己的 `g`,不存在数据竞争。
:::
## 基本用法
```python
from flask import g
# 在请求开始时设置
g.user_id = 1
g.username = 'admin'
# 在请求处理过程中获取
user_id = g.user_id
```
## 项目中的实际应用
### 1. 事务状态管理
AOP 事务拦截器通过 `g` 对象管理事务状态:
```python
# src/middleware/transaction.py
from flask import g, request
from src.core.database import db
def register_transaction(app):
"""注册 AOP 事务拦截器"""
@app.before_request
def begin_transaction():
"""请求开始:记录开始时间,开启事务标记"""
g.request_start_time = time.time()
g.transaction_active = True
@app.after_request
def commit_transaction(response):
"""请求结束:自动提交事务"""
if request.method in ("POST", "PUT", "DELETE", "PATCH"):
if 200 <= response.status_code < 300:
data = response.get_json(silent=True)
if data and data.get("code", 0) == 0:
db.session.commit()
else:
db.session.rollback()
return response
@app.teardown_request
def cleanup_transaction(exception=None):
"""请求结束:清理资源"""
if exception:
db.session.rollback()
g.pop("transaction_active", None)
g.pop("request_start_time", None)
```
### 2. 当前用户信息
登录校验中间件将当前用户信息存入 `g` 对象,业务代码通过 `get_user_id()` 等函数获取:
```python
# src/middleware/auth.py
from flask import g, request
from src.core.jwt import decode_token
def register_auth(app):
"""注册登录校验中间件"""
@app.before_request
def login_required():
"""登录校验:解析 JWT Token,注入用户信息到 g 对象"""
# 白名单路径跳过校验
if _is_whitelist_path(request.path):
return
# 提取 Token
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return R.failed(msg="未登录", code=401)
# 解析 Token
payload = decode_token(token)
if not payload:
return R.failed(msg="Token 无效", code=401)
# 注入用户信息到 g 对象
g.user_id = payload.get('user_id')
g.username = payload.get('username')
g.realname = payload.get('realname')
```
```python
# src/core/security.py
from flask import g
def get_user_id():
"""获取当前登录用户ID"""
return getattr(g, 'user_id', None)
def get_username():
"""获取当前登录用户名"""
return getattr(g, 'username', None)
def get_realname():
"""获取当前登录用户真实姓名"""
return getattr(g, 'realname', None)
```
业务代码通过这些函数获取当前用户信息:
```python
# src/modules/system/position/service.py
from src.core.security import get_realname
class PositionService(BaseService):
@classmethod
def add(cls, data: dict) -> None:
"""新增岗位"""
form = cls._validate_form(data)
create_data = cls._clean_form_data(form)
create_data["create_user"] = get_realname() # 从 g 对象获取
create_data["update_user"] = get_realname()
cls.repository.create(**create_data)
```
### 3. 性能监控
事务拦截器通过 `g` 记录请求开始时间,用于计算响应耗时:
```python
# src/middleware/transaction.py
@app.after_request
def commit_transaction(response):
elapsed = time.time() - g.get("request_start_time", time.time())
logger.debug(
f"请求结束, method: {request.method}, path: {request.path}, "
f"status: {response.status_code}, elapsed: {elapsed:.3f}s"
)
return response
```
## g 对象 vs 其他存储方式
| 存储方式 | 生命周期 | 线程安全 | 适用场景 |
|:---|:---|:---|:---|
| `g` | 单个请求 | 是 | 请求级数据(当前用户、事务状态) |
| `session` | 用户会话 | 是 | 用户级数据(登录状态、偏好设置) |
| `current_app.config` | 应用生命周期 | 是 | 应用级配置(数据库 URI、密钥) |
| 模块级变量 | 进程生命周期 | 否 | 单例对象(扩展实例) |
:::warning 注意事项
- `g` 对象只能在请求上下文中访问,不能在请求外使用
- `g` 对象在请求结束后自动销毁,不要用于持久化数据
- 需要跨请求共享的数据应使用 Redis 或数据库
:::
## 请求上下文
Flask 的请求上下文机制确保 `g`、`request`、`session` 等对象在请求处理期间可用:
```python
# 在请求上下文中可以访问 g
with app.test_request_context():
g.user_id = 1
print(g.user_id) # 正常访问
# 在请求上下文外访问 g 会报错
# print(g.user_id) # RuntimeError: Working outside of application context.
```
## 总结
`g` 对象是 Flask 请求级全局变量容器,项目中用于存储当前用户信息(`user_id` / `username` / `realname`)、事务状态(`transaction_active`)、请求开始时间(`request_start_time`)等请求级数据。通过 `get_user_id()` / `get_realname()` 等安全函数封装访问,业务代码无需直接操作 `g` 对象。
@@ -0,0 +1,514 @@
# 分层架构设计
本章详细描述项目的三层架构设计,包括各层的职责边界、依赖方向、通信方式,并以岗位(Position)模块为实例贯穿说明。
## 三层架构概览
:::tip 架构分层原则
采用经典的三层架构,核心原则是:**依赖单向、职责单一、接口隔离**。上层只依赖下层接口,下层不感知上层存在。
:::
```
┌─────────────────────────────────────────────────────────────┐
│ HTTP 层 (Router) │
│ modules/system/position/router.py │
│ 职责:路由定义 · 权限装饰器 · 参数接收 · 响应返回 │
│ 原则:不含业务逻辑,仅做请求转发 │
└──────────────────────────┬──────────────────────────────────┘
│ 调用
▼
┌─────────────────────────────────────────────────────────────┐
│ 业务层 (Service) │
│ modules/system/position/service.py │
│ 职责:业务逻辑 · 校验规则 · 字段组装 · 序列化加工 │
│ 原则:不直接操作数据库,通过 Repository 访问数据 │
└──────────────────────────┬──────────────────────────────────┘
│ 调用
▼
┌─────────────────────────────────────────────────────────────┐
│ 数据层 (Repository) │
│ modules/system/position/repository.py │
│ 职责:数据库 CRUD · 查询条件组装 · 软删除过滤 │
│ 原则:只做数据访问原语,不做业务校验和响应封装 │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 数据库 (MySQL / PostgreSQL / etc.) │
└─────────────────────────────────────────────────────────────┘
```
## 各层详细职责
### 第一层:HTTP 层(Router)
**目录位置**:`src/modules/{group}/{name}/router.py`
**核心职责**:
1. **路由定义**:声明 HTTP 方法、URL 路径、请求参数类型
2. **权限控制**:通过 `@check_permission` 装饰器声明所需权限
3. **参数接收**:通过 `parse_request_body` 函数解析请求体
4. **响应返回**:直接返回 Service 层的 `R` 对象
**设计原则**:
- 不包含任何业务逻辑
- 不直接访问数据库
- 仅做请求转发和响应返回
- 权限字符串遵循 `sys:{module}:{action}` 格式
**代码示例**(岗位模块):
```python
# src/modules/system/position/router.py
# +======================================================================
# | 模块: 岗位路由层
# | 说明: 提供岗位信息的增删改查、状态管理、列表获取等 API 接口
# +======================================================================
from flask import Blueprint, request
from src.core.permission import check_permission
from src.core.response import R
from src.common.utils.request import parse_request_body, parse_id_list
# ============================================================
# 创建 Blueprint 实例
# ============================================================
position_blue = Blueprint('position', __name__, url_prefix='/position')
# ============================================================
# 查询分页数据
# ============================================================
@position_blue.route('/page', methods=['GET'])
@check_permission('sys:position:page')
def page():
"""
获取岗位分页列表
:return: 分页数据响应
"""
from src.modules.system.position.service import get_position_page
return get_position_page()
# ============================================================
# 查询岗位详情
# ============================================================
@position_blue.route('/detail/<int:id>', methods=['GET'])
@check_permission('sys:position:detail')
def detail(id):
"""
根据岗位ID查询详细信息
:param id: 岗位ID(路径参数)
:return: 岗位详情数据
"""
from src.modules.system.position.service import get_position_detail
return R.ok(data=get_position_detail(id))
# ============================================================
# 获取岗位列表
# ============================================================
@position_blue.route('/list', methods=['GET'])
@check_permission('sys:position:list')
def get_position_list():
"""
获取所有岗位列表(无分页,用于下拉选择)
:return: 岗位列表数据
"""
from src.modules.system.position.service import get_position_list
return R.ok(data=get_position_list())
# ============================================================
# 添加岗位
# ============================================================
@position_blue.route('/add', methods=['POST'])
@check_permission('sys:position:add')
def add():
"""
新增岗位
:return: 添加结果响应
"""
from src.modules.system.position.service import add_position
from src.modules.system.position.schema import PositionForm
data = parse_request_body(request, PositionForm)
return add_position(data)
# ============================================================
# 更新岗位
# ============================================================
@position_blue.route('/update', methods=['PUT'])
@check_permission('sys:position:update')
def update():
"""
更新岗位信息
:return: 更新结果响应
"""
from src.modules.system.position.service import update_position
from src.modules.system.position.schema import PositionForm
data = parse_request_body(request, PositionForm)
return update_position(data)
# ============================================================
# 删除岗位
# ============================================================
@position_blue.route('/delete/<int:id>', methods=['DELETE'])
@check_permission('sys:position:delete')
def delete(id):
"""
根据ID删除单个岗位
:param id: 岗位ID(路径参数)
:return: 删除结果响应
"""
from src.modules.system.position.service import delete_position
return delete_position(id)
# ============================================================
# 设置岗位状态
# ============================================================
@position_blue.route('/status', methods=['PUT'])
@check_permission('sys:position:status')
def status():
"""
修改岗位状态(启用/禁用)
:return: 状态更新结果响应
"""
from src.modules.system.position.service import update_position_status
from src.modules.system.position.schema import PositionStatusForm
data = parse_request_body(request, PositionStatusForm)
return update_position_status(data)
# ============================================================
# 批量删除岗位
# ============================================================
@position_blue.route('/batchDelete', methods=['DELETE'])
@check_permission('sys:position:batchDelete')
def batch_delete():
"""
批量删除岗位(请求体需传入 ID 数组)
Request Body 示例:
[1, 2, 3] # 岗位ID数组
:return: 批量删除结果响应
"""
from src.modules.system.position.service import batch_delete_positions
id_list = parse_id_list(request)
return batch_delete_positions(id_list)
```
:::tip 装饰器顺序
装饰器按从上到下执行:先 `@position_blue.route()`(路由),再 `@check_permission`(鉴权)。
:::
### 第二层:业务层(Service)
**目录位置**:`src/modules/{group}/{name}/service.py`
**核心职责**:
1. **业务逻辑编排**:组合多个数据操作完成业务流程
2. **唯一性校验**:通过 Schema 的 `validate_` 方法 + `Repository.exists_by()` 实现
3. **表单校验**:`_validate_form` 自动校验,`_clean_form_data` 过滤无效字段
4. **序列化加工**:`_detail_dict` / `_enrich_page_records` 加工响应数据
5. **文件处理**:文件字段迁移、URL 补全
6. **前置/后置钩子**:`before_add` / `before_update` / `before_delete` / `after_add` / `after_update` / `after_delete` 扩展点
**设计原则**:
- 通过 Repository 访问数据库,不直接使用 `db.query()`
- 返回 `R` 对象,不做 jsonify 封装
- 通过声明差异点定制通用 CRUD,减少重复代码
**代码示例**(岗位模块):
```python
# src/modules/system/position/service.py
# +======================================================================
# | 模块: 岗位业务逻辑层
# | 说明: 岗位的增删改查、唯一性校验、状态管理等业务处理
# +======================================================================
from typing import Optional
from src.common.base_service import BaseService
from src.modules.system.position.models import Position
from src.modules.system.position.repository import PositionRepository
from src.modules.system.user.models import User
from src.modules.system.user.repository import UserRepository
class PositionService(BaseService):
"""
岗位服务类
继承 BaseService 获得标准 CRUD 能力,并扩展删除前用户引用校验。
"""
repository = PositionRepository
form = PositionForm
status_form = PositionStatusForm
model_name = "岗位"
@classmethod
def before_delete(cls, ids):
"""删除前校验:存在用户引用该岗位时禁止删除"""
for pid in ids:
if UserRepository.count_by({'position_id': pid}) > 0:
raise BizError(f"岗位ID {pid} 已被用户引用,无法删除")
# ============================================================
# 模块级代理函数:保持 router 调用兼容
# ============================================================
get_position_page = PositionService.get_page
get_position_detail = PositionService.get_detail
get_info = PositionService.get_info
add_position = PositionService.add
update_position = PositionService.update
delete_positions = PositionService.remove
update_position_status = PositionService.update_status
get_position_list = PositionService.get_list
```
:::tip Service 层的两种模式
简单 CRUD 模块继承 `BaseService`,只需声明 `repository` / `form` / `model_name` 并覆写钩子方法。复杂模块(如用户)在继承基础上覆写 `add` / `update` 等方法。
:::
### 第三层:数据层(Repository)
**目录位置**:`src/modules/{group}/{name}/repository.py`
**核心职责**:
1. **CRUD 操作**:`create` / `update` / `batch_delete` / `get_by_id` 等
2. **查询构建**:`filter` / `filter_by` / `get_one` / `get_all` / `count`
3. **分页查询**:`paginate` 返回 `(items, total)` 元组
4. **软删除过滤**:等值方法自动追加 `is_delete=0`
5. **数据清洗**:`_clean_form_data` 过滤无效字段,`convert_camel_to_snake` 转命名
**设计原则**:
- 只做数据访问原语,不做 R 封装
- 等值方法自动软删过滤,`filter()` 为原始出口
- 删除的 R 响应由 Service 层统一封装
**代码示例**(岗位模块):
```python
# src/modules/system/position/repository.py
from src.common.base_repository import BaseRepository
from src.common.constants.common import PAGE_SIZE
from src.modules.system.position.models import Position
class PositionRepository(BaseRepository):
"""岗位数据仓储"""
model = Position
@classmethod
def page(cls, params: dict):
"""岗位分页查询"""
return cls.find_page(
page=int(params.get('pageNo', 1)),
page_size=int(params.get('pageSize', PAGE_SIZE)),
filters={'status': params.get('status')},
like_filters={'name': params.get('name')},
order_by='sort',
)
@classmethod
def list_enabled(cls, params=None):
"""查询启用的岗位列表"""
params = params or {}
return cls.find_list(
filters={'status': 1},
like_filters={'name': params.get('name')},
order_by='sort',
)
```
:::tip Repository 基类提供的方法
`BaseRepository` 提供以下通用方法,所有方法均为 `classmethod`:
- **查询**:`get_by_id` / `get_by_ids` / `first_by` / `exists_by` / `count_by` / `exists`
- **分页**:`find_page` 返回 `(items, total, pages)`
- **列表**:`find_list` 返回实例列表
- **写操作**:`create` / `update_by_id` / `update_status` / `delete_by_id` / `batch_delete` / `batch_restore`
:::
## 依赖方向
```
HTTP 层 ──依赖──→ Service 层 ──依赖──→ Repository 层 ──依赖──→ Model
│ │ │
│ │ │
▼ ▼ ▼
R (响应封装) R (响应封装) BaseRepository (基类)
WTForms BaseRepository db.session
check_permission BaseService SQLAlchemy ORM
```
:::warning 依赖规则
- HTTP 层只依赖 Service 层,不直接调用 Repository
- Service 层通过 Repository 访问数据库,不直接使用 `db.query()`
- Repository 层只依赖 Model 和数据库会话
- 下层不感知上层存在,不调用上层方法
:::
## 模型层(Model)
**目录位置**:`src/modules/{group}/{name}/models.py`
模型层定义数据库表结构,继承 `BaseModel`(通用字段)和 `BaseDB`(持久化方法)。
```python
# src/modules/system/position/models.py
from sqlalchemy import Column, String, Integer, text
from src.common.base_db import BaseDB
from src.common.base_model import BaseModel
from config.database import DB_PREFIX
# ============================================================
# 岗位模型
# ============================================================
class Position(BaseModel, BaseDB):
"""岗位模型类"""
# ============================================================
# 表名配置
# ============================================================
__tablename__ = DB_PREFIX + "position"
__table_comment__ = "岗位表"
# ============================================================
# 字段定义
# ============================================================
# 岗位名称
name = Column(String(255), nullable=False, index=True, comment="岗位名称")
# 岗位状态:1-在用 2-停用
status = Column(Integer, default=0, server_default=text('0'), index=True, comment="岗位状态:1-在用 2-停用")
# 岗位排序
sort = Column(Integer, default=0, server_default=text('0'), comment="岗位排序")
# ============================================================
# 内置方法
# ============================================================
def __repr__(self):
"""返回岗位名称作为字符串表示"""
return '岗位:{}'.format(self.name)
```
## Schema 层(校验)
**目录位置**:`src/modules/{group}/{name}/schema.py`
Schema 层定义请求参数的校验规则,使用 WTForms 的表单类 + validators。
```python
# src/modules/system/position/schema.py
# +======================================================================
# | 模块: 岗位表单验证
# | 说明: 岗位创建/编辑/状态更新的请求数据校验
# +======================================================================
from wtforms import StringField, IntegerField
from wtforms.validators import DataRequired, Length, NumberRange
from src.common.forms.base_form import BaseForm
# ============================================================
# 岗位表单类
# ============================================================
class PositionForm(BaseForm):
"""岗位创建/编辑表单"""
name = StringField('岗位名称', validators=[
DataRequired(message='岗位名称不能为空'),
Length(min=1, max=150, message='岗位名称长度必须在1-150个字符之间')
])
status = IntegerField('岗位状态', validators=[
DataRequired(message='岗位状态不能为空'),
NumberRange(min=1, max=2, message='岗位状态必须是1或2')
])
sort = IntegerField('岗位排序', validators=[
DataRequired(message='岗位排序不能为空'),
NumberRange(min=0, max=99999, message='岗位排序必须在0-99999之间')
])
# ============================================================
# 设置状态表单类
# ============================================================
class PositionStatusForm(BaseForm):
"""岗位状态更新表单"""
id = IntegerField('岗位ID', validators=[
DataRequired(message='岗位ID不能为空'),
NumberRange(min=1, message='岗位ID必须大于0')
])
status = IntegerField('岗位状态', validators=[
DataRequired(message='岗位状态不能为空'),
NumberRange(min=1, max=2, message='岗位状态必须是1或2')
])
```
## 完整调用链路示例
以添加岗位为例,展示三层之间的调用关系:
```
POST /api/v1/position/add
│
├─ 1. Router (position.py)
│ @check_permission('sys:position:add')
│ def add():
│ data = parse_request_body(request, PositionForm)
│ return add_position(data)
│
├─ 2. Service (service.py)
│ @classmethod
│ def add(cls, data):
│ form = cls._validate_form(data) # 表单校验
│ extra = cls.before_add(form) or {} # 前置钩子
│ create_data = cls._clean_form_data(form)
│ create_data.update(extra)
│ obj = cls.repository.create(**create_data)
│ cls.after_add(obj) # 后置钩子
│
├─ 3. Repository (base_repository.py)
│ @classmethod
│ def create(cls, **kwargs):
│ obj = cls.model(**kwargs)
│ obj.save() # add + flush
│ return obj
│
├─ 4. Model (base_db.py)
│ def save(self):
│ db.session.add(self)
│ db.session.flush() # SQL 发送到数据库
│
└─ 5. Database
INSERT INTO flask_position (name, status, sort, create_user, ...)
VALUES ('高级工程师', 1, 10, '管理员', ...)
```
## 各层文件职责一览
| 文件 | 层次 | 职责 | 是否包含业务逻辑 |
|:---|:---:|:---|:---:|
| `position/router.py` | HTTP | 路由、权限、参数接收 | 否 |
| `position/schema.py` | HTTP | 请求参数校验规则 | 否 |
| `position/service.py` | 业务 | 业务逻辑、校验、序列化 | 是 |
| `position/repository.py` | 数据 | 数据库 CRUD、查询构建 | 否 |
| `position/models.py` | 数据 | 表结构定义、字段映射 | 否 |
## 总结
三层架构通过依赖单向和职责单一实现了良好的关注点分离。HTTP 层只关心请求接收和响应返回,Service 层只关心业务逻辑编排,Repository 层只关心数据访问。这种分层使得每一层都可以独立测试、独立替换,系统在模块数量增长时仍能保持清晰的代码结构。以岗位模块为例,完整实现一个 CRUD 模块只需 5 个文件,其中 Repository 和 Model 几乎零代码,开发者只需关注 Service 层的业务差异点。
@@ -0,0 +1,252 @@
# 整体架构图
本章从全局视角阐述系统的整体技术架构,涵盖前端、后端、数据库及基础设施等核心组成部分,旨在帮助开发者快速建立对系统全貌的清晰认知,为后续的开发和运维工作奠定基础。
## 架构总览
下图展示了从用户浏览器发起请求到最终返回响应的完整链路:
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Browser (Vue3 + ElementPlus) │
│ Axios HTTP Request / Vite Dev Proxy │
└──────────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Nginx (反向代理) │
│ 静态资源 / SSL 终止 / 负载均衡 / gzip 压缩 │
└──────────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Flask Application │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Middleware Chain (请求/响应钩子) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌───────┐ │ │
│ │ │ CORS │→│ Redis │→│ RateLimit │→│ DBSession│→│OpLog │ │ │
│ │ │ 跨域处理 │ │ 上下文 │ │ 滑动窗口 │ │ 会话管理 │ │操作日志│ │ │
│ │ └─────────┘ └─────────┘ └──────────┘ └─────────┘ └───────┘ │ │
│ └───────────────────────────────┬───────────────────────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Blueprint Router (路由分发) │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │
│ │ │login_required│→│check_permis- │→│ Router (HTTP 层) │ │ │
│ │ │ JWT 认证 │ │ sion RBAC │ │ @blueprint.route() │ │ │
│ │ │ │ │ 鉴权 │ │ WTForms 校验 │ │ │
│ │ └─────────────┘ └──────────────┘ └────────────┬─────────────┘ │ │
│ └───────────────────────────────────────────────────┼───────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Service Layer (业务逻辑层) │ │
│ │ BaseService / 自定义 Service │ │
│ │ 唯一性校验 · 字段组装 · 序列化 · 文件处理 · 缓存管理 │ │
│ └───────────────────────────────┬───────────────────────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Repository Layer (数据访问层) │ │
│ │ BaseRepository │ │
│ │ CRUD · 软删除 · 分页 · 等值查询 · 原始条件查询 │ │
│ └───────────────────────────────┬───────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────┐ ┌──────────────────────────────────────────┐ │
│ │ SQLAlchemy 2.0 │ │ Redis │ │
│ │ ORM → SQL 生成 │ │ Token 黑名单 · 登录锁 · 限流计数 · 缓存 │ │
│ └──────────┬──────────┘ └──────────────────────────────────────────┘ │
└─────────────┼───────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Database (MySQL / PostgreSQL / SQL Server / SQLite / Oracle) │
│ 表前缀: flask_ · 软删除: is_delete │
└─────────────────────────────────────────────────────────────────────────┘
```
## 架构特征
:::tip 核心设计原则
遵循以下架构设计原则,确保系统在长期演进中保持高质量和可维护性:
:::
### 1. 前后端分离
前端 Vue3 应用通过 HTTP API 调用后端 Flask 服务,两者独立开发、独立部署。前端开发阶段通过 Vite 的 `proxy` 配置将 `/api` 请求代理到后端 `http://127.0.0.1:8021`,生产环境由 Nginx 统一反向代理。
### 2. 分层架构
后端严格分为三层,每层职责单一、依赖单向:
| 层次 | 目录 | 职责 |
|:---:|:---:|:---|
| HTTP 层 | `modules/{group}/{name}/router.py` | 路由定义、权限装饰器、参数接收、响应返回 |
| 业务层 | `modules/{group}/{name}/service.py` | 业务逻辑、校验规则、序列化加工 |
| 数据层 | `modules/{group}/{name}/repository.py` | 数据库 CRUD、查询条件组装 |
### 3. 模板方法模式
通过 `BaseRepository` 和 `BaseService` 基类,将通用 CRUD 流程固化为模板方法。子类只需声明差异点(过滤字段、排序规则、唯一性校验等),即可获得完整的增删改查能力,极大减少重复代码。
### 4. 中间件链式处理
请求处理采用 Flask 的 `before_request` / `after_request` 钩子,形成链式处理。中间件在 `src/app.py` 的 `create_app()` 中按以下顺序注册:
```python
# src/app.py - create_app() 中间件注册顺序
register_cors(app) # CORS(最外层)
register_rate_limit(app) # 滑动窗口限流
register_upload_size_limit(app) # 上传体积预拦截
register_redis(app) # Redis 上下文注入
register_db_session(app) # DB 会话管理
register_operation_log(app) # 操作日志记录
register_transaction(app) # AOP 事务管理
register_auth(app) # JWT 认证
register_v1(app) # 路由注册
register_exception(app) # 全局异常处理
```
外层中间件(CORS、限流)先拦截,内层中间件(DB 会话、Redis 注入)后处理,响应沿反向链返回。限流被置于 CORS 内层,被限流的请求不进入 DB 会话与操作日志,避免 DoS 攻击时日志表与 DB 连接不降反增。
### 5. 全局异常处理
`register_exception(app)` 在 `src/app.py` 中注册全局异常处理器,所有异常均返回 HTTP 200 状态码,通过 `code` 字段区分业务成功(0)与失败(1):
| 异常类型 | 触发场景 | 响应格式 |
|:---|:---|:---|
| `BizError` | 业务逻辑错误 | `{"code": 1, "msg": "..."}` |
| `ValidationError` | WTForms 校验失败 | `{"code": 1, "msg": "field: message/..."}` |
| `Exception` | 未捕获异常 | `{"code": 1, "msg": "服务器内部错误"}` |
### 6. 统一响应格式
所有 API 端点返回统一的 JSON 结构:
```json
{
"code": 0,
"data": {},
"msg": "操作成功",
"ok": true
}
```
成功响应 `code=0`,失败响应 `code=1`,前端无需针对不同接口做差异化解析。
### 7. 软删除策略
所有业务表通过 `is_delete` 字段实现软删除(0=正常,1=已删除)。`BaseRepository` 的等值查询方法自动追加 `is_delete=0` 过滤,业务代码无需关心已删除数据。
### 8. AOP 事务管理
数据库事务由 `src/middleware/transaction.py` 的 `register_transaction` 统一管理:`before_request` 标记事务活跃,`after_request` 在 POST/PUT/DELETE/PATCH 请求且 HTTP 2xx 且 `code==0` 时自动 commit,否则 rollback,`teardown_request` 在异常时自动 rollback。业务代码只需调用 `model.save()` 完成 `add + flush`,无需手动提交事务。
```python
# src/middleware/transaction.py 核心逻辑
def register_transaction(app):
@app.before_request
def _transaction_begin():
g.transaction_active = True
@app.after_request
def _transaction_commit(response):
if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
if response.status_code == 200:
body = response.get_json()
if body and body.get('code') == 0:
db.session.commit()
else:
db.session.rollback()
return response
@app.teardown_request
def _transaction_rollback(exception):
if exception:
db.session.rollback()
```
### 9. 多数据库驱动
通过 `DB_DRIVER` 环境变量切换数据库驱动(MySQL / PostgreSQL / SQL Server / SQLite / Oracle),`SQLAlchemy` 的方言层屏蔽了底层差异,业务代码无需修改。
### 10. 应用生命周期管理
`src/app.py` 通过 `create_app()` 管理应用启动和关闭流程:
```python
# src/app.py
def create_app():
app = Flask(__name__)
# 1. 验证配置
validate_config()
# 2. 初始化扩展
register_extends(app)
# 3. 注册中间件
register_cors(app)
register_redis(app)
register_db_session(app)
register_operation_log(app)
register_transaction(app)
register_auth(app)
# 4. 注册路由
register_v1(app)
# 5. 注册异常处理
register_exception(app)
# 6. 初始化定时任务
init_scheduler(app)
return app
```
启动顺序:`validate_config()` → `register_extends()` → 中间件注册 → 路由注册 → 异常处理 → 定时任务初始化。
## 技术分层图
```
┌──────────────────────────────────────────────────────────────┐
│ Frontend (Vue3) │
│ ElementPlus · Pinia · Vue Router · Axios │
└──────────────────────────┬───────────────────────────────────┘
│ HTTP / JSON
┌──────────────────────────┴───────────────────────────────────┐
│ Flask Application │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│ │ Middleware │ │ Router │ │ Exception Handler │ │
│ │ ───────── │ │ ───────── │ │ ───────────────── │ │
│ │ CORS │ │ auth │ │ BizError │ │
│ │ Redis │ │ v1 (login │ │ ValidationError │ │
│ │ RateLimit │ │ required) │ │ HTTP │ │
│ │ DBSession │ │ │ │ Generic │ │
│ │ OpLog │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └──────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐│
│ │ Routers (modules/{group}/{name}/router.py) ││
│ │ @check_permission · @check_demo · WTForms Form ││
│ └──────────────────────────────────────────────────────────┘│
│ │ ││
│ ┌──────────────────────────────────────────────────────────┐│
│ │ Services (modules/{group}/{name}/service.py) ││
│ │ BaseService · 自定义 Service ││
│ └──────────────────────────────────────────────────────────┘│
│ │ ││
│ ┌──────────────────────────────────────────────────────────┐│
│ │ Repositories (modules/{group}/{name}/repository.py) ││
│ │ BaseRepository · 自定义 Repository ││
│ └──────────────────────────────────────────────────────────┘│
│ │ ││
│ ┌──────────────────────────────────────────────────────────┐│
│ │ Models (modules/{group}/{name}/models.py) ││
│ │ BaseModel + BaseDB → SQLAlchemy ORM ││
│ └──────────────────────────────────────────────────────────┘│
└──────────────────────────┬───────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌────────────┐ ┌─────────────┐
│ Database │ │ Redis │
│ (MySQL等) │ │ │
└────────────┘ └─────────────┘
```
## 总结
项目采用经典的前后端分离 + 三层架构设计,通过中间件链实现横切关注点(认证、限流、日志、事务)的统一处理,通过基类模板方法消除重复 CRUD 代码,通过 WTForms 实现请求参数的声明式校验。整体架构在保持简洁的同时,具备良好的可扩展性和可维护性,适合企业级后台管理系统的快速开发与长期演进。
@@ -0,0 +1,446 @@
# 请求完整链路
本章以一个具体的 API 请求为例,详细描述从浏览器发起 HTTP 请求到返回响应的完整链路,帮助开发者理解每一层的职责和数据流转方式。
## 请求链路总览
:::tip 链路概览
一个典型的 API 请求经过以下阶段:浏览器 → Nginx → CORS → 限流 → Redis 注入 → DB 会话 → 操作日志 → 路由匹配 → JWT 认证 → RBAC 鉴权 → WTForms 校验 → Router → Service → Repository → DB → 响应序列化 → 中间件反向链 → 浏览器
:::
以下以 `POST /api/v1/position/add` 添加岗位为例,逐步拆解完整链路。
## 第一阶段:网络层
### 1. 浏览器发起请求
```javascript
// 前端 Axios 调用
const res = await request({
url: '/api/v1/position/add',
method: 'post',
data: {
name: '高级工程师',
status: 1,
sort: 10
}
});
```
前端通过 Axios 发起 HTTP POST 请求,携带:
- `Authorization: Bearer <JWT Token>` 请求头
- `Content-Type: application/json` 请求头
- JSON 请求体
### 2. Nginx 反向代理(生产环境)
```nginx
location /api/ {
proxy_pass http://127.0.0.1:8021;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
Nginx 将请求代理到 Flask 服务,同时附加客户端真实 IP 到 `X-Forwarded-For` 头。
## 第二阶段:中间件链(请求/响应钩子)
Flask 的中间件通过 `before_request` / `after_request` 钩子实现。请求从最外层进入,逐层穿透到路由处理函数,响应沿反向链返回。
### 3. CORS 跨域中间件(最外层)
```python
# src/middleware/cors.py
register_cors(app) # 注册 CORS 中间件
```
- 检查 `Origin` 头是否在允许列表中
- 预检请求(OPTIONS)直接返回 CORS 响应头
- 普通请求附加 `Access-Control-Allow-*` 响应头
### 4. 请求限流中间件
```python
# src/middleware/rate_limit.py
@app.before_request
def rate_limit():
if not RATE_LIMIT_ENABLED:
return
# 滑动窗口计数器(Redis Lua 原子操作)
count = sliding_window_incr(redis, current_key, prev_key, ...)
if count > RATE_LIMIT_LIMIT:
return R.failed("请求过于频繁,请稍后再试")
```
- 基于客户端 IP 的滑动窗口计数器
- 使用 Redis Lua 脚本保证原子性
- 超限直接返回统一 JSON 响应,不进入后续链路
- Redis 不可用时降级放行(fail-open)
### 5. Redis 上下文注入中间件
```python
# src/core/redis_client.py
@app.before_request
def redis_context():
# 将 Redis 客户端注入到 g 对象
g.redis = redis_client
```
- 将 Redis 客户端注入 `g` 对象
- 后续业务代码通过 `get_redis()` 获取当前请求的 Redis 客户端
### 6. 数据库会话中间件
```python
# src/middleware/transaction.py
@app.before_request
def _transaction_begin():
"""请求开始前:标记事务活跃"""
g.transaction_active = True
@app.after_request
def _transaction_commit(response):
"""请求结束后:根据响应状态决定提交或回滚"""
if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
if response.status_code == 200:
body = response.get_json()
if body and body.get('code') == 0:
db.session.commit()
else:
db.session.rollback()
return response
@app.teardown_request
def _transaction_rollback(exception):
"""请求异常时:回滚事务"""
if exception:
db.session.rollback()
db.session.remove()
```
- 为每个请求管理数据库会话
- 写操作根据响应体 `code` 字段自动 commit/rollback
- 异常时自动 rollback,finally 中关闭会话
### 7. 操作日志中间件
```python
# src/middleware/operation_log.py
@app.after_request
def operation_log(response):
if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
# 记录操作日志到 flask_operation_log 表
# 包括:操作人、操作IP、请求路径、请求参数、响应状态、耗时等
...
return response
```
- 在 DB 会话外层,记录写操作日志
- 包含请求方法、URL、参数、响应码、耗时、操作人等信息
### 8. 请求计时中间件
```python
# src/app.py
@app.before_request
def start_timer():
g.start_time = time.time()
@app.after_request
def add_process_time_header(response):
if hasattr(g, 'start_time'):
process_time = time.time() - g.start_time
response.headers["X-Process-Time"] = str(round(process_time, 5))
return response
```
- 记录请求处理耗时
- 通过 `X-Process-Time` 响应头返回
## 第三阶段:路由与认证
### 9. 路由匹配
```python
# src/routes/v1.py
def register_v1(app):
v1 = Blueprint('v1', __name__, url_prefix='/api/v1')
for blue in BLUEPRINTS:
v1.register_blueprint(blue)
app.register_blueprint(v1)
```
- Flask 根据 HTTP 方法 + URL 路径匹配到 `POST /api/v1/position/add` 端点
- 该端点挂载在 `v1` 蓝图上
- `v1` 蓝图在 `src/routes/v1.py` 中集中注册所有模块路由
### 10. JWT 认证(login_required)
```python
# src/middleware/auth.py
@app.before_request
def login_required():
# 白名单路径跳过认证
if request.path in LOGIN_WHITELIST:
return
# 1. 从 Authorization 头提取 Bearer Token
# 2. 验证 Token 签名、有效期
# 3. 检查 Token 是否在 Redis 黑名单中
# 4. 解析出 user_id、username、realname
# 5. 注入到 g 对象
g.user_id = user_id
g.username = username
g.realname = realname
```
- 认证失败返回 `{"code": 401, "msg": "..."}` 响应
- 认证通过后,用户信息注入 `g` 对象供后续使用
### 11. RBAC 鉴权(check_permission)
```python
# src/modules/system/position/router.py
@position_blue.route('/add', methods=['POST'])
@check_permission('sys:position:add')
def add():
from src.modules.system.position.service import add_position
from src.modules.system.position.schema import PositionForm
data = parse_request_body(request, PositionForm)
return add_position(data)
```
```python
# src/core/permission.py
def check_permission(permission: str):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
user_id = get_user_id()
if user_id == 1:
return func(*args, **kwargs)
from src.modules.system.menu.service import get_permissions_list
permission_list = get_permissions_list(user_id)
if permission not in permission_list:
return R.failed("权限不足")
return func(*args, **kwargs)
return wrapper
return decorator
```
- 权限字符串格式:`sys:{module}:{action}`(如 `sys:position:add`)
- 用户 ID=1(超级管理员)跳过所有权限检查
- 使用懒加载 `from src.modules.system.menu.service import get_permissions_list` 避免 core → 业务模块顶层循环依赖
## 第四阶段:参数校验
### 12. WTForms 请求校验
```python
# src/modules/system/position/schema.py
class PositionForm(BaseForm):
name = StringField('岗位名称', validators=[
DataRequired(message='岗位名称不能为空'),
Length(min=1, max=150, message='岗位名称长度必须在1-150个字符之间')
])
status = IntegerField('岗位状态', validators=[
DataRequired(message='岗位状态不能为空'),
NumberRange(min=1, max=2, message='岗位状态必须是1或2')
])
sort = IntegerField('岗位排序', validators=[
DataRequired(message='岗位排序不能为空'),
NumberRange(min=0, max=99999, message='岗位排序必须在0-99999之间')
])
```
- `parse_request_body` 函数解析 JSON 请求体
- WTForms 按 `validators` 约束校验每个字段
- 校验失败返回:
```json
{"code": 1, "data": null, "msg": "岗位名称不能为空"}
```
- 校验通过后,`data` 参数为已验证的字典
## 第五阶段:业务处理
### 13. Router(HTTP 层)
```python
# src/modules/system/position/router.py
def add():
from src.modules.system.position.service import add_position
from src.modules.system.position.schema import PositionForm
data = parse_request_body(request, PositionForm)
return add_position(data)
```
- HTTP 层仅做请求转发,不含业务逻辑
- 直接调用 Service 层函数
### 14. Service(业务逻辑层)
```python
# src/modules/system/position/service.py
class PositionService(BaseService):
repository = PositionRepository
form = PositionForm
status_form = PositionStatusForm
model_name = "岗位"
@classmethod
def before_delete(cls, ids):
"""删除前校验"""
for pid in ids:
if UserRepository.count_by({'position_id': pid}) > 0:
raise BizError(f"岗位ID {pid} 已被用户引用,无法删除")
# 模块级代理函数
add_position = PositionService.add
```
### 15. Repository(数据访问层)
```python
# src/common/base_repository.py
class BaseRepository:
def create(self, **fields):
# 清洗数据:camelCase → snake_case,过滤非模型字段
obj = cls.model(**cls._clean_form_data(fields))
obj.save() # add + flush(不 commit)
return obj
```
### 16. Model(模型层)
```python
# src/common/base_db.py
class BaseDB:
def save(self):
db.session.add(self)
db.session.flush() # 发送 SQL 到数据库,获取自增 ID
return self
```
### 17. 数据库执行
- SQLAlchemy 将 ORM 操作转换为 SQL:`INSERT INTO flask_position (name, status, sort, create_user, ...) VALUES (...)`
- `flush()` 将 SQL 发送到数据库,获取自增主键
- 事务由中间件在响应返回后统一 commit
## 第六阶段:响应返回
### 18. 响应序列化
```python
# Service 返回 R.ok(msg='添加成功')
# src/core/response.py
R.ok(msg='添加成功')
# → jsonify({"code": 0, "data": None, "msg": "添加成功", "ok": True})
```
### 19. 中间件反向链
响应沿中间件链反向返回:
1. **请求计时**:附加 `X-Process-Time` 响应头
2. **操作日志**:记录写操作日志到数据库
3. **DB 会话**:检测 `code==0`,执行 `db.session.commit()`
4. **Redis 上下文**:无操作(g 对象自动清理)
5. **限流**:无操作(响应直接透传)
6. **CORS**:附加 `Access-Control-Allow-*` 响应头
### 20. 浏览器接收响应
```json
{
"code": 0,
"data": null,
"msg": "添加成功",
"ok": true
}
```
前端 Axios 拦截器统一处理响应,`code===0` 时展示成功提示,否则展示错误信息。
## 完整链路流程图
```
请求链路(自上而下)
[浏览器]
│ POST /api/v1/position/add
▼
[Nginx] ── 反向代理转发
│
▼
[CORS 中间件] ── 跨域校验通过
│
▼
[限流中间件] ── 滑动窗口计数(Redis)
│ ├─ 超限 → 直接返回失败
│ └─ 通过 ▼
▼
[DB 会话中间件] ── 标记事务活跃
│
▼
[操作日志中间件] ── 记录写操作(不影响主流程)
│
▼
[路由层] ── 匹配 /api/v1/position/add
│
▼
[认证中间件 login_required] ── JWT 验证 + 黑名单检查(Redis)
│ ├─ 失败 → 返回 401
│ └─ 通过 → 解析 user_id/username 注入 g 对象 ▼
▼
[Router 层]
├─ @check_permission ── 权限校验(Redis 缓存)
├─ parse_request_body ── WTForms 参数校验
└─ 调用 Service ▼
▼
[Service 层]
├─ _validate_form ── WTForms 表单校验
├─ before_add ── 前置扩展钩子
├─ _clean_form_data ── 过滤无效字段
└─ 调用 Repository.create ▼
▼
[Repository 层]
└─ model(**kwargs).save() ── add + flush(获取自增 ID,不提交)▼
▼
[数据库] ── 执行 INSERT,返回自增 ID
│
▼ 逐层返回响应
[Repository] → [Service] → [Router] → [OpLog] → [DBSession]
│
├─ 业务成功(code=0)→ commit 提交事务
└─ 业务失败(code≠0)→ rollback 回滚事务
│
▼
[限流] → [CORS] → [Nginx]
│
▼
[浏览器] ← JSON 响应({ code, data, msg, ok })
```
## 异常处理链路
当请求链路中发生异常时,全局异常处理器统一捕获并返回标准响应:
| 异常类型 | 触发场景 | 响应 |
|:---|:---|:---|
| `BizError` | 业务逻辑错误 | `{"code": 1, "msg": "..."}` |
| `ValidationError` | WTForms 校验失败 | `{"code": 1, "msg": "field: message"}` |
| `Exception` | 未捕获异常 | `{"code": 1, "msg": "服务器内部错误"}` |
:::warning 异常处理原则
所有异常均返回 HTTP 200 状态码,通过 `code` 字段区分业务成功(0)与失败(1)。这是前后端约定,前端统一按 `code` 判断,无需处理 HTTP 状态码差异。
:::
## 总结
一个 API 请求从浏览器到数据库经过 7 个阶段、20+ 个处理节点。中间件链负责横切关注点(安全、限流、日志、事务),路由层负责认证鉴权,WTForms 负责参数校验,三层架构负责业务逻辑与数据访问。这种分层设计使得每一层职责单一、可独立测试,系统在高并发场景下仍能保持稳定的性能和可靠的事务一致性。
@@ -0,0 +1,570 @@
# 安全架构设计
本章详细阐述系统的安全架构设计,围绕认证、授权、防护、审计四大安全领域展开,旨在帮助开发者全面理解系统的安全保障体系,确保在开发与部署过程中遵循安全规范,有效防范各类安全风险。
**认证(Authentication)**:系统采用基于 JWT(JSON Web Token)的无状态认证机制,用户通过用户名密码登录后获取 Token,后续请求通过 `Authorization: Bearer <token>` 头传递,由 `login_required` 中间件统一拦截并验证 Token 的有效性(签名校验、有效期检查、Redis 黑名单过滤)。同时,系统内置登录失败锁定策略,连续失败 5 次即锁定账号 10 分钟,有效防御暴力破解攻击。
**授权(Authorization)**:基于 RBAC(基于角色的访问控制)模型设计,权限控制粒度细化至接口级。用户通过角色继承权限节点(如 `sys:position:add`),由 `@check_permission` 装饰器在路由层统一拦截校验,权限数据采用 Redis 缓存加速查询,管理员(`userId == 1`)自动放行。权限变更时,相关缓存主动失效,确保权限更新的实时性。
**防护(Protection)**:系统从多维度构建安全防护体系。限流中间件基于 Redis 滑动窗口算法,有效防御 CC 攻击和接口滥用;参数校验通过 WTForms Schema 自动拦截非法输入,防止 SQL 注入和 XSS 攻击;敏感密码采用 bcrypt 加盐哈希存储,即使数据库泄露也无法还原明文;演示模式下 `@check_demo` 装饰器统一拦截写操作,保护测试数据不被污染;跨域配置精确控制可信域名,防止 CSRF 攻击。
**审计(Audit)**:系统提供完备的操作审计能力。操作日志中间件自动记录所有写操作(POST/PUT/DELETE/PATCH)的请求路径、参数、操作人、IP 地址及执行结果,存入 `flask_operation_log` 表;登录日志独立记录每次登录尝试(含 IP、User-Agent、登录状态),便于安全事件追溯与异常行为分析;定时任务每次执行记录至 `flask_job_log` 表,确保后台任务的执行过程可审计。
:::tip 安全设计原则
纵深防御:认证、授权、限流、参数校验多层防护,单一防线失效时仍有其他机制兜底
最小权限:用户仅能访问已授权接口,角色权限按需分配,遵循"够用即可"原则
数据不落地:敏感信息(Token、密码)不在日志中明文记录,密码哈希不可逆
安全默认:框架默认配置为安全模式(如 DEBUG 关闭、限流开启),如需放宽需显式配置
:::
## 安全架构总览
```
┌─────────────────────────────────────────────────────────────────────┐
│ 安全防御层次 │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 网络层防护 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ CORS │ │ 限流 │ │ XSS 防护 │ │ 上传体积限制 │ │ │
│ │ │ 跨域控制 │ │ 滑动窗口 │ │ 输入清洗 │ │ DoS 防御 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 认证层 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ JWT 认证 │ │ 验证码 │ │ 登录锁定 │ │ Token 黑名单 │ │ │
│ │ │ Bearer │ │ 图形验证 │ │ 失败计数 │ │ 登出失效 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 授权层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │
│ │ │ RBAC 权限 │ │ 演示模式保护 │ │ 超级管理员跳过 │ │ │
│ │ │ check_ │ │ check_demo │ │ user_id=1 bypass │ │ │
│ │ │ permission │ │ │ │ │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 数据层 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ 密码加密 │ │ 参数校验 │ │ SQL 注入 │ │ 操作日志 │ │ │
│ │ │ bcrypt │ │ WTForms │ │ ORM 防护 │ │ 审计追踪 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
## 一、认证机制
### JWT 令牌认证
:::tip JWT 工作流程
用户登录成功后,服务端签发 JWT Token 返回给前端。前端将 Token 存储在本地,后续每次请求通过 `Authorization: Bearer <token>` 头携带。服务端验证 Token 签名和有效期,解析出用户信息。
:::
1. 工作流程时序图
```
[前端] [后端] [Redis]
│ │ │
│── POST /login ─────────→│ │
│ {username, pwd, │ │
│ captcha, uuid} │ │
│ │── 验证码校验 (uuid) ──→│
│ │←─ captcha_text ────────│
│ │ │
│ │── 密码校验 │
│ │── 签发JWT │
│ │── 存储Token ──────────→│
│←─ {token} ─────────────│ │
│ │ │
│── GET /api/v1/xxx ────→│ │
│ Authorization: │ │
│ Bearer <token> │ │
│ │── 验证JWT │
│ │── 检查黑名单 ─────────→│
│ │←─ exists ─────────────│
│←─ {data} ──────────────│ │
```
:::tip 流程说明
1. 前端提交用户名、密码、验证码及 UUID
2. 后端校验验证码,验证密码,生成 JWT 并缓存至 Redis
3. 前端后续请求携带 JWT,后端校验 Token 有效性并检查黑名单
4. 校验通过后返回业务数据
:::
#### JWT 配置
```python
# config/auth.py
# ============================================================
# JWT 配置
# ============================================================
# JWT签名密钥(必须通过环境变量设置,不能使用默认值上线)
JWT_SALT = os.getenv('JWT_SALT', '')
# JWT令牌过期时间(分钟)
JWT_EXPIRE_MINUTES = int(os.getenv('JWT_EXPIRE_MINUTES', '20'))
JWT_ALGORITHM = "HS256" # 签名算法
```
#### Token 签发
```python
# src/core/jwt.py
def create_token(payload, timeout=None):
"""
生成JWT令牌
参数:
payload (dict): JWT载荷数据,通常包含用户ID、用户名等信息
timeout (int): 令牌过期时间(分钟),默认使用DEFAULT_TIMEOUT_MINUTES
返回:
str: 生成的JWT令牌字符串
"""
try:
if timeout is None:
timeout = DEFAULT_TIMEOUT_MINUTES
payload_copy = payload.copy()
current_time = datetime.datetime.now(tz=datetime.timezone.utc)
payload_copy['exp'] = current_time + datetime.timedelta(minutes=timeout)
payload_copy['iat'] = current_time
headers = {
"typ": "JWT",
"alg": JWT_ALGORITHM
}
token = jwt.encode(
payload=payload_copy,
key=JWT_SALT,
algorithm=JWT_ALGORITHM,
headers=headers
)
logger.debug(f"生成JWT令牌成功,过期时间:{timeout}分钟")
return token
except Exception as e:
logger.error(f"生成JWT令牌失败: {str(e)}")
raise
```
#### Token 验证
```python
# src/middleware/auth.py
def login_required():
"""登录验证中间件:提取并校验 Token,校验用户有效,写入 g 对象"""
from flask import request, g
success, access_token, msg = get_access_token()
if not success:
return R.failed(msg=msg, code=401)
if is_token_blacklisted(access_token):
return R.failed(msg="登录已注销", code=401)
result = parse_payload(access_token)
if result['code'] != 0:
return R.failed(msg=result['msg'], code=401)
data = result['data']
user_id = int(data.get('userId', 0))
# 用户有效状态校验
from src.modules.system.user.repository import UserRepository
user = UserRepository.get_by_id(user_id)
if not user or user.status != 1:
return R.failed(msg="用户已被禁用或删除,请联系管理员", code=401)
g.user_id = user_id
g.username = data.get('username', '')
g.realname = data.get('realname', '')
```
#### Token 黑名单
用户登出时,将当前 Token 加入 Redis 黑名单,TTL 等于 Token 剩余有效期:
```python
# src/modules/auth/router.py
@auth_blue.route('/logout', methods=['GET'])
def logout():
"""用户退出登录"""
success, access_token, _ = get_access_token()
if success and access_token:
add_token_to_blacklist(access_token)
# 记录退出登录日志
username = get_username()
realname = get_realname()
record_login_log(username, 2, 0, 0, method='v1.logout',
request_method=request.method, create_user=realname, result="退出成功")
return R.ok(msg="注销成功")
```
### 登录验证码
:::tip 验证码防暴力破解
每次登录前需先获取验证码,验证码存储在 Redis 中(TTL 120 秒),登录时校验验证码正确性。验证码使用 UUID 作为 key,避免被猜测。
:::
```python
# 获取验证码
GET /api/v1/captcha
Response: { "key": "xxx", "captcha": "data:image/png;base64,iVBORw0KG..." }
# 登录时校验
POST /api/v1/login
Body: { "username": "admin", "password": "123456", "code": "a3Xb", "key": "xxx" }
```
### 登录失败锁定
连续 5 次登录失败后,账号自动锁定 10 分钟:
```python
# src/modules/auth/service.py
def login(data):
redis = get_redis()
# 检查锁定状态
lock_key = f"login:lock:{data['username']}"
is_locked = redis.get(lock_key)
if is_locked:
return R.failed("账号已锁定,请10分钟后再试")
# 验证用户名密码
user = UserRepository.first_by({'username': data['username']})
if not user or not verify_password(data['password'], user.password):
# 原子递增失败计数
fail_key = f"login:fail:{data['username']}"
count = incr_with_expire(redis, fail_key, 300)
if count >= 5:
redis.set(lock_key, 1, ex=600) # 锁定 10 分钟
return R.failed(f"用户名或密码错误,剩余{5-count}次机会")
# 登录成功,清除失败计数
redis.delete(f"login:fail:{data['username']}")
```
### 密码安全
```python
# src/core/password.py
import secrets
import bcrypt
BCRYPT_ROUNDS = 12
def generate_salt(length: int = 10) -> str:
"""生成随机盐值"""
return secrets.token_urlsafe(length)[:length]
def encrypt_password(password: str, salt: str) -> str:
"""使用 bcrypt 加密密码(外部 salt 双重加盐)"""
combined = (password + salt).encode('utf-8')
hashed = bcrypt.hashpw(combined, bcrypt.gensalt(rounds=BCRYPT_ROUNDS))
return hashed.decode('utf-8')
def verify_password(password: str, salt: str, hashed: str) -> bool:
"""验证密码是否正确"""
combined = (password + salt).encode('utf-8')
return bcrypt.checkpw(combined, hashed.encode('utf-8'))
```
- 使用 bcrypt 算法,每次哈希自动生成随机盐
- 密码不可逆,数据库泄露后无法还原明文
- 默认密码在首次登录时强制修改
## 二、授权机制
### RBAC 权限模型
```
用户(User) ──N:M──→ 角色(Role) ──N:M──→ 菜单/权限(Menu)
│ │ │
│ user_role │ role_menu │ permission
│ 关联表 │ 关联表 │ 权限标识
└────────────────────┘ │
▼
sys:user:add
sys:user:page
sys:role:update
...
```
权限标识格式:`sys:{module}:{action}`
- `sys`:系统前缀
- `{module}`:模块名(user / role / menu / position / level 等)
- `{action}`:操作名(page / list / detail / add / update / delete / status / batchDelete / export / import)
### check_permission 装饰器
```python
# src/core/permission.py
from functools import wraps
from flask import request
from src.core.response import R
from src.core.security import get_user_id
def check_permission(permission: str):
"""节点权限鉴权装饰器,userId==1(管理员)直接放行"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
user_id = get_user_id()
if user_id == 1:
return func(*args, **kwargs)
from src.modules.system.menu.service import get_permissions_list
permission_list = get_permissions_list(user_id)
if permission not in permission_list:
return R.failed("权限不足")
return func(*args, **kwargs)
return wrapper
return decorator
```
### 权限缓存
```python
# src/modules/system/menu/service.py
def get_permissions_list(user_id: int) -> list:
"""获取用户权限列表(走 Redis 缓存)"""
cache_key = f"perm:user:{user_id}"
redis = get_redis()
cached = redis.get(cache_key)
if cached:
return json.loads(cached)
# 从数据库查询
permissions = query_user_permissions(user_id)
redis.set(cache_key, json.dumps(permissions), ex=3600)
return permissions
def invalidate_perm_cache(user_id: int):
"""失效用户权限缓存(角色/菜单变更时调用)"""
from src.core.redis_client import get_redis
redis = get_redis()
redis.delete(f"perm:user:{user_id}")
```
### check_demo 装饰器
```python
# src/middleware/demo.py
from functools import wraps
from flask import current_app
from src.core.response import R
def check_demo(func):
"""演示模式拦截装饰器,放在 @check_permission 之后、路由函数之前"""
@wraps(func)
def wrapper(*args, **kwargs):
if current_app.config.get('FLASK_DEMO'):
return R.failed("演示环境,暂无操作权限")
return func(*args, **kwargs)
return wrapper
```
- 保护所有写操作(add / update / delete / status / batchDelete / import)
- 在 `check_permission` 之后应用
- `FLASK_DEMO=True` 时,所有写操作返回失败
## 三、网络层防护
### CORS 跨域控制
```python
# src/middleware/cors.py
from flask_cors import CORS
def register_cors(app):
CORS(app,
origins=CORS_ALLOWED_ORIGINS,
supports_credentials=True,
methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization"])
```
- 生产环境应配置具体的 `origins`,避免使用 `*`
- 开发环境可配置为 `http://localhost:8001`
### 请求限流
```python
# src/middleware/rate_limit.py
from flask import request
def rate_limit():
# 基于客户端 IP 的滑动窗口限流
client_ip = get_client_ip()
count = sliding_window_incr(redis, current_key, prev_key, ...)
if count > RATE_LIMIT_LIMIT:
return R.failed(msg="请求过于频繁,请稍后再试")
```
- 默认关闭,通过 `RATE_LIMIT_ENABLED=True` 启用
- 使用 Redis 滑动窗口计数器,消除固定窗口边界 2x 突发
- Redis 不可用时降级放行(fail-open)
- CORS 预检请求不计数
### XSS 防护
```python
# src/common/utils/rich_text.py
import bleach
def sanitize_html(content: str) -> str:
"""XSS 清洗:移除危险标签和属性"""
allowed_tags = [
'p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'a', 'img', 'table', 'tr', 'td', 'th', 'tbody', 'thead'
]
allowed_attrs = {
'a': ['href', 'title'],
'img': ['src', 'alt', 'width', 'height'],
}
return bleach.clean(content, tags=allowed_tags, attributes=allowed_attrs)
```
- 富文本内容入库前进行 XSS 清洗
- 使用 `bleach` 库白名单过滤危险标签和属性
- 图片 `src` 属性只允许合法 URL
### 上传体积限制
```python
# src/middleware/upload_size.py
from flask import request
def upload_size_limit():
content_length = request.headers.get('content-length')
if content_length and int(content_length) > MAX_UPLOAD_SIZE:
return R.failed(msg="上传文件超出大小限制")
```
- 按 `Content-Length` 提前拒绝超大请求体
- 避免超大 multipart 落临时盘后再由业务层校验
## 四、数据层防护
### SQL 注入防护
:::tip ORM 天然防注入
SQLAlchemy ORM 使用参数化查询(Parameterized Query),所有用户输入通过参数绑定传递,不直接拼接 SQL 字符串,从根本上防止 SQL 注入。
:::
```python
# SQLAlchemy 参数化查询示例
UserRepository.first_by({'username': 'admin'})
# 实际执行:SELECT * FROM flask_user WHERE username = :username AND is_delete = 0
# 参数绑定:{'username': 'admin'}
```
### 参数校验
```python
# WTForms 声明式校验
class PositionForm(BaseForm):
name = StringField('岗位名称', validators=[
DataRequired(message='岗位名称不能为空'),
Length(min=1, max=150, message='岗位名称长度必须在1-150个字符之间')
])
status = IntegerField('岗位状态', validators=[
DataRequired(message='岗位状态不能为空'),
NumberRange(min=1, max=2, message='岗位状态必须是1或2')
])
sort = IntegerField('岗位排序', validators=[
DataRequired(message='岗位排序不能为空'),
NumberRange(min=0, max=99999, message='岗位排序必须在0-99999之间')
])
```
- 所有请求参数通过 WTForms 声明式校验
- 自动拒绝不符合约束的请求
- 校验错误统一返回格式
### 敏感字段保护
- 密码使用 bcrypt 哈希存储,不可逆
- `to_dict()` 序列化时自动排除 `is_delete`、`password`、`salt`
- JWT 密钥通过环境变量配置,不硬编码
## 五、审计追踪
### 操作日志
```python
# src/middleware/operation_log.py
from flask import request, g
def operation_log():
if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
# 记录操作日志
log_data = {
'username': g.username,
'module': extract_module(request.path),
'action': request.method,
'url': request.url,
'params': get_request_params(request),
'ip': get_client_ip(),
'status': 1 if is_success else 2,
'duration': int(duration * 1000),
}
save_operation_log(log_data)
```
自动记录所有写操作的日志,包括:
- 操作人、操作模块、操作动作
- 请求 URL、请求参数
- 操作 IP、操作时间
- 响应状态、执行耗时
### 登录日志
每次登录/登出自动记录到 `flask_login_log` 表,包括登录账号、IP、浏览器、操作系统、登录状态等。
## 安全配置清单
| 配置项 | 环境变量 | 默认值 | 说明 |
|:---|:---|:---|:---|
| JWT 密钥 | `JWT_SALT` | 必须配置 | HS256 签名密钥 |
| JWT 过期时间 | `JWT_EXPIRE_MINUTES` | 120 | Token 有效期(分钟) |
| 密码错误锁定 | — | 5 次/10 分钟 | 连续失败后锁定 |
| 验证码有效期 | — | 120 秒 | 验证码 Redis TTL |
| 限流开关 | `RATE_LIMIT_ENABLED` | False | 滑动窗口限流 |
| 限流阈值 | `RATE_LIMIT_LIMIT` | 100 | 每窗口最大请求数 |
| 限流窗口 | `RATE_LIMIT_WINDOW_SECONDS` | 60 | 窗口时长(秒) |
| CORS 源 | `CORS_ALLOWED_ORIGINS` | * | 允许的跨域源 |
| 上传限制 | `UPLOAD_MAX_SIZE_MB` | 10MB | 最大上传体积 |
| 演示模式 | `FLASK_DEMO` | False | 锁定写操作 |
| 数据库密码 | `DB_PASSWORD` | — | 数据库连接密码 |
| Redis 密码 | `REDIS_PASSWORD` | — | Redis 连接密码 |
:::warning 生产环境安全检查
1. `JWT_SALT` 必须使用强随机字符串,不能使用默认值
2. `CORS_ALLOWED_ORIGINS` 必须配置具体的域名,不能使用 `*`
3. `FLASK_DEMO` 在生产环境应为 `False`
4. 数据库和 Redis 必须设置密码
5. 启用 HTTPS(由 Nginx/负载均衡器处理)
:::
## 总结
项目的安全架构涵盖认证(JWT + 验证码 + 登录锁定 + Token 黑名单)、授权(RBAC + check_permission + check_demo)、防护(CORS + 限流 + XSS + 上传限制 + SQL 注入防护)、审计(操作日志 + 登录日志)四大领域。通过纵深防御策略,在网络层、认证层、授权层、数据层分别设置安全屏障,确保系统在面对各类安全威胁时具备足够的防护能力。生产部署时需特别关注 JWT 密钥强度、CORS 配置、HTTPS 启用等关键安全项。
@@ -0,0 +1,240 @@
# 同步架构设计
## 概述
Flask 是一个基于 WSGI 的同步 Web 框架,采用同步编程模型。本文档介绍项目的同步架构设计及其优势。
:::tip 为什么选择同步架构?
1. **成熟稳定**:Flask 的同步模型经过长期验证,稳定可靠
2. **易于理解**:同步编程模型更直观,易于调试和维护
3. **工具完善**:Flask 生态完善,各种扩展和工具支持良好
4. **部署简单**:WSGI 标准,支持多种部署方式
:::
## 同步模型特性
| 特性 | 说明 |
|:-----|:-----|
| 编程模型 | 同步 `def` |
| 并发处理 | 多线程/多进程(由 WSGI 服务器管理) |
| I/O 等待 | 阻塞当前线程,其他线程不受影响 |
| 学习曲线 | 平缓,标准 Python 编程 |
| 调试难度 | 简单,标准 pdb 调试 |
| 生态支持 | 完善,Flask 扩展生态丰富 |
## Flask 同步模型
### 请求处理流程
```
客户端请求
↓
WSGI 服务器(Gunicorn)
↓
Flask 应用
↓
中间件链(before_request)
↓
路由分发(Blueprint)
↓
视图函数(Router)
↓
Service 层
↓
Repository 层
↓
SQLAlchemy ORM
↓
数据库
↓
返回响应
```
### 多线程/多进程
Flask 通过 WSGI 服务器(如 Gunicorn)实现并发处理:
```bash
# 使用 Gunicorn 启动,4 个工作进程,每个进程 4 个线程
gunicorn -w 4 -t 4 app:app
```
| 参数 | 说明 |
|:-----|:-----|
| `-w 4` | 4 个工作进程 |
| `-t 4` | 每个进程 4 个线程 |
| `app:app` | Flask 应用实例 |
## 同步代码示例
### Service 层
```python
# src/modules/system/position/service.py
from src.common.base_service import BaseService
from src.core.exceptions import BizError
from src.modules.system.position.schema import PositionForm, PositionStatusForm
from src.modules.system.position.repository import PositionRepository
from src.modules.system.user.repository import UserRepository
class PositionService(BaseService):
"""
岗位服务类
所有方法均为同步 classmethod,通过 Repository 访问数据库。
"""
repository = PositionRepository
form = PositionForm
status_form = PositionStatusForm
model_name = "岗位"
@classmethod
def before_delete(cls, ids):
"""删除前校验:检查用户引用"""
for pid in ids:
if UserRepository.count_by({'position_id': pid}) > 0:
raise BizError(f"岗位ID {pid} 已被用户引用,无法删除")
```
### Repository 层
```python
# src/modules/system/position/repository.py
from src.common.base_repository import BaseRepository
from src.common.constants.common import PAGE_SIZE
from src.modules.system.position.models import Position
class PositionRepository(BaseRepository):
"""岗位数据仓储"""
model = Position
@classmethod
def page(cls, params: dict):
"""分页查询:同步调用基类 find_page"""
return cls.find_page(
page=int(params.get('pageNo', 1)),
page_size=int(params.get('pageSize', PAGE_SIZE)),
filters={'status': params.get('status')},
like_filters={'name': params.get('name')},
order_by='sort',
)
@classmethod
def list_enabled(cls, params=None):
"""查询启用的岗位列表"""
params = params or {}
return cls.find_list(
filters={'status': 1},
like_filters={'name': params.get('name')},
order_by='sort',
)
```
## 同步架构优势
### 1. 代码简洁
同步代码更直观,易于理解和维护:
```python
# 同步代码(本项目)
from src.modules.system.position.service import get_position_page
def page():
"""同步调用 service"""
return R.ok(data=get_position_page(request.args))
```
### 2. 调试简单
同步代码的调试更简单,标准 Python 调试工具直接可用:
```python
# 同步调试:直接使用 pdb
import pdb; pdb.set_trace()
```
### 3. 生态完善
Flask 生态完善,各种扩展支持良好:
| 扩展 | 用途 |
|:-----|:-----|
| Flask-SQLAlchemy | ORM 集成 |
| Flask-Migrate | 数据库迁移 |
| Flask-CORS | 跨域资源共享 |
| Flask-WTF | 表单校验(WTForms 集成) |
| redis-py | Redis 同步客户端 |
| APScheduler | 定时任务调度 |
### 4. 部署简单
WSGI 标准,支持多种部署方式:
```bash
# Gunicorn(推荐)
gunicorn -w 4 -t 4 app:app
# uWSGI
uwsgi --http 0.0.0.0:8021 --wsgi-file app.py --callable app
# Flask 内置服务器(仅开发)
python app.py
```
## 性能优化
### 1. 连接池
SQLAlchemy 使用连接池管理数据库连接:
```python
from sqlalchemy import create_engine
engine = create_engine(
DB_URL,
pool_size=10, # 连接池大小
max_overflow=20, # 最大溢出连接数
pool_recycle=3600, # 连接回收时间(秒)
)
```
### 2. 缓存
使用 Redis 缓存热点数据:
```python
def get_position_list(self):
"""获取岗位列表(带缓存)"""
cache_key = "position:list"
cached = redis.get(cache_key)
if cached:
return json.loads(cached)
# 查询数据库
positions = cls.repository.find_list(filters={'status': 1})
result = [p.to_dict() for p in positions]
# 写入缓存
redis.set(cache_key, json.dumps(result), ex=3600)
return result
```
### 3. 定时任务
项目使用 APScheduler 实现定时任务调度,任务定义存储在数据库中:
```python
# src/core/scheduler.py
from apscheduler.schedulers.background import BackgroundScheduler
scheduler = BackgroundScheduler()
```
定时任务在独立线程中执行,不阻塞主请求处理流程。支持 Cron、Interval、Date 三种触发器,通过 Redis 实现多实例互斥,避免重复执行。
## 总结
项目采用 Flask 同步架构,具有成熟稳定、易于理解、工具完善、部署简单等优势。对于后台管理系统这类以 CRUD 为主的场景,同步模型完全够用,代码更简洁,调试更方便。通过连接池、Redis 缓存、APScheduler 定时任务等技术,可以进一步优化性能。
@@ -0,0 +1,169 @@
# 同步编程模型
:::tip 核心原则
Flask 基于 WSGI 同步框架,所有视图函数和业务代码均为同步 `def`。通过多线程/多进程实现并发,代码简洁直观,调试方便。
:::
## Flask 的执行模型
Flask 采用同步编程模型,所有请求处理在独立的线程中执行:
```
客户端请求
↓
WSGI 服务器(Gunicorn / Flask 内置)
↓
分配线程处理请求
↓
Flask 路由分发 → 视图函数 → Service → Repository → 数据库
↓
返回响应(同步等待完成)
```
所有业务代码均为同步 `def`,数据库操作通过 SQLAlchemy 同步 ORM 完成,Redis 操作通过同步客户端完成。
## 为什么选择同步模型
| 考量 | 说明 |
|:---|:---|
| **成熟稳定** | Flask 同步模型经过长期验证,生态完善 |
| **代码简洁** | 直接调用,无异步包装开销 |
| **调试方便** | 标准 Python 调试工具(pdb)直接可用 |
| **学习曲线** | 同步编程更直观,团队上手快 |
| **后台管理** | CRUD 为主的场景,同步模型完全够用 |
## 代码示例:全同步模式
### Router 层
```python
# src/modules/system/position/router.py
from flask import Blueprint, request
from src.core.permission import check_permission
from src.modules.system.position import service as position
from src.common.utils.request import parse_request_body
from src.core import response as R
position_blue = Blueprint('position', __name__, url_prefix='/position')
@position_blue.route('/page', methods=["GET"])
@check_permission("sys:position:page")
def page():
"""
查询岗位分页数据
同步函数:直接调用 service,无需 await。
"""
return R.ok(data=position.get_position_page(request.args))
@position_blue.route('/add', methods=["POST"])
@check_permission("sys:position:add")
def add():
"""
添加岗位
同步函数:解析请求体 → 调用 service → 返回结果。
"""
json_data, err = parse_request_body()
if err:
return R.failed(msg=err)
position.add_position(json_data)
return R.ok(msg="添加成功")
```
### Service 层
```python
# src/modules/system/position/service.py
from src.common.base_service import BaseService
from src.core.exceptions import BizError
from src.modules.system.position.schema import PositionForm, PositionStatusForm
from src.modules.system.position.repository import PositionRepository
from src.modules.system.user.repository import UserRepository
class PositionService(BaseService):
"""
岗位服务类
所有方法均为同步 classmethod,直接调用 Repository 访问数据库。
"""
repository = PositionRepository
form = PositionForm
status_form = PositionStatusForm
model_name = "岗位"
@classmethod
def before_delete(cls, ids):
"""删除前校验:检查用户引用"""
for pid in ids:
if UserRepository.count_by({'position_id': pid}) > 0:
raise BizError(f"岗位ID {pid} 已被用户引用,无法删除")
# 模块级代理函数
get_position_page = PositionService.get_page
add_position = PositionService.add
```
### Repository 层
```python
# src/modules/system/position/repository.py
from src.common.base_repository import BaseRepository
from src.common.constants.common import PAGE_SIZE
from src.modules.system.position.models import Position
class PositionRepository(BaseRepository):
"""岗位数据仓储,所有查询方法均为同步 classmethod"""
model = Position
@classmethod
def page(cls, params: dict):
"""分页查询:直接调用基类 find_page,同步执行"""
return cls.find_page(
page=int(params.get('pageNo', 1)),
page_size=int(params.get('pageSize', PAGE_SIZE)),
filters={'status': params.get('status')},
like_filters={'name': params.get('name')},
order_by='sort',
)
```
:::tip 全链路同步
从 Router → Service → Repository → 数据库,全链路均为同步调用,代码简洁直观。
:::
## Redis 操作也是同步的
项目使用 `redis-py` 同步客户端,所有 Redis 操作也是同步的:
```python
# src/core/redis_client.py
from redis import Redis
redis = Redis.from_url(REDIS_URL, decode_responses=True)
```
```python
# 使用示例(同步)
from src.core.redis_client import redis
# 存储验证码
redis.set(f"captcha:{key}", code, ex=300)
# 查询验证码
cached_code = redis.get(f"captcha:{key}")
# 登录失败计数
count = redis.incr(f"login:fail:{ip}:{username}")
redis.expire(f"login:fail:{ip}:{username}", 300)
```
## 总结
项目采用 Flask 同步架构,全链路(Router → Service → Repository → Redis → 数据库)均为同步 `def`,代码简洁直观。通过 WSGI 服务器的多线程/多进程模型实现并发处理,对于后台管理系统场景完全够用。
@@ -0,0 +1,233 @@
# 技术选型说明
本章详细阐述系统各技术组件的选型理由、与同类主流方案的对比分析,以及版本约束说明,旨在帮助开发者理解技术选型的依据与权衡,确保团队在技术决策上达成共识,并为后续的技术演进提供参考依据。
## 后端框架选型
### Flask
:::tip 为什么选择 Flask?
Flask 是 Python 生态中最轻量、最灵活的 Web 框架,基于 WSGI 同步模型,成熟稳定,易于调试和部署。对于后台管理系统这类以 CRUD 为主的场景,Flask 的简洁性和可扩展性表现优异。
:::
| 特性 | 说明 |
|:---|:---|
| **轻量灵活** | 核心概念少,学习曲线平缓,易于上手 |
| **成熟稳定** | 基于 WSGI 同步模型,经过长期验证,稳定可靠 |
| **扩展丰富** | Flask 扩展生态完善,支持各种功能扩展 |
| **蓝图机制** | Blueprint 实现模块化路由组织,便于大型项目管理 |
| **中间件支持** | 支持请求/响应钩子,便于实现认证、日志等横切关注点 |
| **多数据库支持** | 通过 SQLAlchemy ORM 支持多种数据库方言 |
| **易于部署** | WSGI 标准,支持 Gunicorn、uWSGI 等多种服务器 |
### 选择 Flask 的核心理由
1. **成熟稳定**:Flask 是 Python Web 框架中最成熟的选择之一,社区活跃,文档完善
2. **轻量灵活**:核心简洁,不强制绑定特定组件,便于根据项目需求定制
3. **蓝图机制**:Blueprint 实现模块化路由组织,便于大型项目管理
4. **同步模型**:同步编程模型更易于理解和调试,适合后台管理系统场景
5. **易于部署**:WSGI 标准,支持多种部署方式,生产环境部署简单
## ORM 选型
### SQLAlchemy 2.0
:::tip 为什么选择 SQLAlchemy?
SQLAlchemy 是 Python 生态中最成熟、功能最完备的 ORM 框架,2.0 版本统一了 Core 和 ORM 的 API 风格,同时保持了对多种数据库方言的原生支持。
:::
| 特性 | 说明 |
|:---|:---|
| **声明式模型** | 基于 `declarative_base()` 定义模型类,字段与数据库列一一映射 |
| **多数据库方言** | 通过 `DB_DRIVER` 切换 MySQL / PostgreSQL / SQL Server / SQLite / Oracle |
| **会话管理** | `db.session` + Flask 请求级会话隔离(AOP 事务中间件自动管理) |
| **连接池** | 内置连接池(`QueuePool`),支持连接池配置 |
| **软删除** | 通过 `is_delete` 字段 + Repository 基类自动过滤实现 |
| **事务控制** | AOP 中间件自动 commit/rollback,业务代码无需手动管理 |
版本约束:`SQLAlchemy >= 2.0.51`
## 请求校验选型
### WTForms
| 特性 | 说明 |
|:---|:---|
| **声明式校验** | 通过 `BaseForm` + `validators` 声明字段约束,自动校验 |
| **自定义验证器** | `validate_` 方法支持复杂的业务校验逻辑(如唯一性检查) |
| **错误聚合** | 自动收集所有校验错误,返回统一错误信息 |
| **易于集成** | 与 Flask 无缝集成,社区支持完善 |
| **类型安全** | 支持各种字段类型:StringField、IntegerField、SelectField 等 |
版本约束:`WTForms >= 3.0.0`
## 前端技术栈
### Vue3 + Vite + ElementPlus
| 组件 | 版本 | 选型理由 |
|:---|:---|:---|
| **Vue3** | 3.x | Composition API、更好的 TypeScript 支持、Teleport/Suspense 等新特性 |
| **Vite** | 5.x | 极速 HMR、ESM 原生支持、开箱即用的 TypeScript 支持 |
| **ElementPlus** | 2.x | Vue3 生态最成熟的 UI 组件库,企业级后台管理系统首选 |
| **Pinia** | 2.x | Vue3 官方推荐的状态管理方案,替代 Vuex |
| **Vue Router** | 4.x | Vue3 官方路由,支持动态路由、路由守卫 |
| **Axios** | 1.x | HTTP 客户端,支持请求/响应拦截器、取消请求 |
## 缓存与消息中间件
### Redis
:::tip Redis 在项目中的角色
Redis 在项目中承担多个关键职责:JWT 令牌黑名单、登录失败锁定、请求限流计数、数据字典缓存、权限缓存等。采用同步客户端与 Flask 的同步模型无缝集成。
:::
| 用途 | 数据结构 | 过期策略 |
|:---|:---|:---|
| JWT 黑名单 | String | 等于 Token 剩余有效期 |
| 登录失败锁 | String | 指定锁定时长 |
| 滑动窗口限流 | String (Lua) | 2 倍窗口时长 |
| 数据字典缓存 | String (JSON) | TTL 可配置 |
| 权限列表缓存 | String (JSON) | 变更时主动失效 |
版本约束:`redis >= 5.0.0`
## 定时任务
### APScheduler
| 特性 | 说明 |
|:---|:---|
| **调度策略** | 支持 Cron、Interval、Date 三种触发器 |
| **持久化** | 任务定义存储在数据库,重启后自动恢复 |
| **分布式锁** | 通过 Redis 实现多实例互斥,避免重复执行 |
| **日志记录** | 每次执行记录到 `flask_job_log` 表 |
### Cron 表达式格式
项目支持 **5 字段**和 **6 字段**两种 Cron 表达式,6 字段首字段为秒:
| 字段 | 5 字段格式 | 6 字段格式 | 允许值 | 允许的特殊字符 |
|:----|:----|:----|:----|:----|
| 秒 | - | 第 1 位 | 0-59 | `, - * /` |
| 分钟 | 第 1 位 | 第 2 位 | 0-59 | `, - * /` |
| 小时 | 第 2 位 | 第 3 位 | 0-23 | `, - * /` |
| 日 | 第 3 位 | 第 4 位 | 1-31 | `, - * / ? L W` |
| 月 | 第 4 位 | 第 5 位 | 1-12 | `, - * /` |
| 星期 | 第 5 位 | 第 6 位 | 0-7(0 和 7 均为周日) | `, - * / ? L #` |
### 特殊字符说明
| 字符 | 含义 | 示例 |
|:----|:----|:----|
| `*` | 所有值 | `* * * * *` = 每分钟 |
| `?` | 不指定(用于日和星期互斥) | `0 0 0 * * ?` = 每天 0 点,星期不指定 |
| `-` | 范围 | `1-5` = 1 到 5 |
| `,` | 列举 | `1,3,5` = 1、3、5 |
| `/` | 步长 | `*/5` = 每隔 5 个单位 |
| `L` | 最后(Last) | `L` 在日字段 = 月末;`5L` 在星期字段 = 最后一个周五 |
| `W` | 最近工作日(Weekday) | `15W` = 离 15 号最近的工作日 |
| `#` | 第几个星期几 | `6#3` = 第 3 个周五(6=周五) |
### 常用 Cron 表达式示例
**基础频率**
| 表达式 | 说明 |
|:----|:----|
| `* * * * *` | 每分钟执行 |
| `*/5 * * * *` | 每 5 分钟执行 |
| `0 * * * *` | 每小时整点执行 |
| `0 */2 * * *` | 每 2 小时执行 |
| `0 0 * * *` | 每天凌晨 0 点执行 |
| `0 0 */1 * *` | 每天凌晨 0 点(同上) |
**每天定时**
| 表达式 | 说明 |
|:----|:----|
| `0 8 * * *` | 每天早上 8:00 |
| `0 12 * * *` | 每天中午 12:00 |
| `0 18 * * *` | 每天下午 18:00 |
| `0 0 * * *` | 每天凌晨 0:00 |
| `30 9 * * *` | 每天早上 9:30 |
| `0 9,18 * * *` | 每天 9:00 和 18:00(上下班提醒) |
| `0 8-18 * * *` | 每天 8:00 到 18:00 整点(工作时间每小时) |
**每周定时**
| 表达式 | 说明 |
|:----|:----|
| `0 0 * * 0` | 每周日凌晨 0:00 |
| `0 9 * * 1` | 每周一早上 9:00 |
| `0 9 * * 1-5` | 工作日(周一到周五)早上 9:00 |
| `0 9 * * 1,3,5` | 周一、周三、周五早上 9:00 |
| `0 0 * * 6L` | 每月最后一个周六凌晨 0:00 |
**每月定时**
| 表达式 | 说明 |
|:----|:----|
| `0 0 1 * *` | 每月 1 号凌晨 0:00 |
| `0 0 15 * *` | 每月 15 号凌晨 0:00 |
| `0 0 L * *` | 每月最后一天凌晨 0:00 |
| `0 9 1,15 * *` | 每月 1 号和 15 号早上 9:00 |
| `0 0 1 1,4,7,10 *` | 每季度首月 1 号凌晨(季度报表) |
**秒级触发(6 字段)**
| 表达式 | 说明 |
|:----|:----|
| `*/30 * * * * *` | 每 30 秒执行 |
| `0 */5 * * * *` | 每 5 分钟整点执行(等同 5 字段 `*/5 * * * *`) |
| `0 0 9 * * *` | 每天 9:00:00 整秒执行 |
| `0 30 8 * * 1-5` | 工作日 8:30:00 执行 |
| `*/10 * 8-18 * * *` | 工作时间每 10 秒执行(高频监控) |
**业务场景示例**
| 场景 | 表达式 | 说明 |
|:----|:----|:----|
| 数据库备份 | `0 2 * * *` | 每天凌晨 2:00 备份 |
| 日志清理 | `0 3 * * 0` | 每周日凌晨 3:00 清理过期日志 |
| 月度报表 | `0 8 1 * *` | 每月 1 号早上 8:00 生成报表 |
| 心跳检测 | `*/5 * * * *` | 每 5 分钟检测服务状态 |
| 缓存刷新 | `0 */6 * * *` | 每 6 小时刷新一次缓存 |
| 工作日签到提醒 | `0 9 * * 1-5` | 工作日早上 9:00 发送提醒 |
| 订单超时检查 | `*/30 * * * *` | 每 30 秒检查超时未支付订单 |
| 定时推送 | `0 20 * * *` | 每天晚上 8:00 推送消息 |
### 常见错误
| 错误写法 | 问题 | 正确写法 |
|:----|:----|:----|
| `0 0 0 * * * *` | 7 个字段,非法 | `0 0 0 * * *`(6 字段)或 `0 0 * * *`(5 字段) |
| `0 0 * * 7` | 星期 7 超出范围 | `0 0 * * 0`(0 和 7 均为周日,但推荐用 0) |
| `0 0 32 * *` | 日 32 超出范围 | `0 0 1-31 * *` |
| `0 0 * 13 *` | 月 13 超出范围 | `0 0 * 1-12 *` |
| `*/0 * * * *` | 步长不能为 0 | `*/1 * * * *` |
## 其他关键依赖
| 依赖 | 版本 | 用途 |
|:---|:---|:---|
| `Flask` | >= 3.1.1 | Web 框架,`src/app.py` 通过 `create_app()` 创建应用 |
| `PyJWT` | >= 2.13.0 | JWT 令牌签发与验证(HS256 算法),`src/core/jwt.py` |
| `bcrypt` | >= 4.0.0 | 密码哈希(带随机盐),`src/core/password.py` |
| `openpyxl` | >= 3.1.0 | Excel 导入导出,文件上传/下载模块使用 |
| `Pillow` | >= 10.0.0 | 验证码图片生成,`src/common/utils/captcha.py` |
| `python-dotenv` | >= 1.0.0 | `.env` 配置文件加载,`config/__init__.py` 首先调用 `load_dotenv()` |
| `Flask-Migrate` | >= 4.0.0 | 数据库迁移,`migrations/` 目录,`scripts/setup_db.py` 末尾调用 `flask db stamp head` |
| `APScheduler` | >= 3.10.0 | 定时任务调度,`src/core/scheduler.py`,在 `create_app()` 中初始化 |
| `bleach` | >= 6.0.0 | 富文本 XSS 清洗,`src/common/utils/rich_text.py` 的 `sanitize_html()` |
| `Gunicorn` | >= 22.0.0 | WSGI 服务器,生产环境部署使用 |
## 版本锁定策略
:::warning 版本管理
所有依赖版本在 `requirements.txt` 中锁定最低版本(`>=x.y.z`),确保兼容性的同时允许安全补丁升级。生产环境建议使用 `pip freeze` 生成精确版本锁文件。
:::
## 总结
项目的技术选型以「稳定可靠、轻量灵活、可扩展」为核心原则。Flask + SQLAlchemy 2.0 + WTForms 构成后端铁三角,Vue3 + Vite + ElementPlus 提供现代化前端体验,Redis 承担缓存与中间件职责。各组件版本经过充分测试验证,形成稳定的技术底座。选型时优先考虑组件的独立性和可替换性,避免强耦合,为未来的架构演进预留空间。