签入版本
This commit is contained in:
@@ -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 承担缓存与中间件职责。各组件版本经过充分测试验证,形成稳定的技术底座。选型时优先考虑组件的独立性和可替换性,避免强耦合,为未来的架构演进预留空间。
|
||||
Reference in New Issue
Block a user