优化更新
This commit is contained in:
@@ -192,16 +192,23 @@ def register_v1(app):
|
||||
|
||||
```python
|
||||
# src/middleware/__init__.py
|
||||
from src.middleware.auth import register_auth
|
||||
from src.middleware.demo import register_demo
|
||||
from src.middleware.auth import check_login, is_whitelisted, register_middleware
|
||||
from src.middleware.demo import check_demo
|
||||
from src.middleware.transaction import register_transaction, transactional, TransactionContext
|
||||
from src.middleware.operation_log import register_operation_log, operation_log
|
||||
from src.middleware.errors import register_error_handlers
|
||||
```
|
||||
|
||||
各中间件在 `src/app.py` 的 `create_app()` 中按顺序注册:
|
||||
|
||||
def register_middleware(app):
|
||||
"""注册中间件"""
|
||||
register_auth(app) # 登录校验 + 白名单
|
||||
register_demo(app) # 演示模式拦截
|
||||
register_error_handlers(app) # 全局异常处理
|
||||
```python
|
||||
# src/app.py - create_app()
|
||||
register_operation_log(app) # 操作日志记录
|
||||
register_transaction(app) # AOP 事务管理
|
||||
register_v1(app) # 路由注册
|
||||
register_extends(app) # 扩展初始化
|
||||
register_middleware(app) # 登录校验 + 白名单
|
||||
register_error_handlers(app) # 全局异常处理
|
||||
```
|
||||
|
||||
中间件执行顺序(请求进入时):
|
||||
|
||||
@@ -55,10 +55,10 @@
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ BaseRepository (src/common/base_repository.py) │
|
||||
│ ─────────────────────────────────────────────────────────────── │
|
||||
│ 查询: get_by_id / get_by_ids / get_one / get_all / count │
|
||||
│ 分页: paginate → (items, total) │
|
||||
│ 查询: get_by_id / get_by_ids / get_all / first_by / count_by │
|
||||
│ 分页: find_page / find_list │
|
||||
│ 写操作: create / update / batch_delete │
|
||||
│ 条件: filter (原始) / filter_by (等值+软删) │
|
||||
│ 条件: exists / exists_by (等值+软删) │
|
||||
│ 软删除: 等值方法自动追加 is_delete=0 │
|
||||
└──────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
@@ -104,29 +104,39 @@
|
||||
### 通用字段
|
||||
|
||||
```python
|
||||
class BaseModel(Base):
|
||||
# src/common/base_model.py
|
||||
from src.core.database import db
|
||||
from sqlalchemy import text, func
|
||||
|
||||
def now():
|
||||
"""返回当前时间(北京时间)"""
|
||||
from datetime import datetime
|
||||
return datetime.now()
|
||||
|
||||
class BaseModel(db.Model):
|
||||
__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 = db.Column(db.Integer, primary_key=True, autoincrement=True, comment="主键ID")
|
||||
# 创建人:记录创建者,由业务层通过 get_realname() 设置
|
||||
create_user = db.Column(db.String(64), default='', server_default=text("''"), comment="创建人")
|
||||
# 创建时间:记录创建时间,创建后不再变化
|
||||
create_time = db.Column(db.DateTime, default=now, server_default=func.now(), comment="创建时间")
|
||||
# 更新人:记录最后修改者
|
||||
update_user = db.Column(db.String(64), default='', server_default=text("''"), comment="更新人")
|
||||
# 更新时间:记录最后修改时间,更新时自动刷新
|
||||
update_time = db.Column(db.DateTime, default=now, onupdate=now, server_default=func.now(), comment="最后更新时间")
|
||||
# 逻辑删除标记:0=正常,1=已删除
|
||||
is_delete = db.Column(db.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 自动触发) |
|
||||
| `create_user` | String(64) | 创建人(realname),默认空字符串 |
|
||||
| `create_time` | DateTime | 创建时间,server_default=func.now() |
|
||||
| `update_user` | String(64) | 更新人(realname),默认空字符串 |
|
||||
| `update_time` | DateTime | 更新时间,onupdate 自动触发 |
|
||||
| `is_delete` | Integer | 软删除标识:0=正常,1=已删除 |
|
||||
|
||||
### 表注释钩子
|
||||
@@ -203,30 +213,29 @@ class BaseDB:
|
||||
# | 说明: 统一处理前端传入的空字符串 id → None 等通用校验
|
||||
# +======================================================================
|
||||
|
||||
from wtforms import Form, IntegerField
|
||||
from wtforms.validators import Optional
|
||||
from flask_wtf import FlaskForm
|
||||
from src.common.forms.fields import ArrayField
|
||||
|
||||
|
||||
class BaseForm(Form):
|
||||
class BaseForm(FlaskForm):
|
||||
"""
|
||||
表单基类
|
||||
自定义表单基类,增强对 JSON 数组的支持
|
||||
|
||||
所有创建/编辑表单应继承此类,自动获得:
|
||||
1. id 字段:Optional,前端传空字符串时自动转为 None
|
||||
重写 process 方法,确保 ArrayField 能正确处理数组数据。
|
||||
"""
|
||||
|
||||
id = IntegerField('ID', validators=[Optional()])
|
||||
def process(self, formdata=None, obj=None, data=None, **kwargs):
|
||||
"""重写 process 方法,支持 ArrayField 处理数组数据"""
|
||||
# ... 处理逻辑
|
||||
```
|
||||
|
||||
### 代码解析
|
||||
|
||||
**可选 `id` 字段**:
|
||||
**继承 `FlaskForm`**:`BaseForm` 继承自 `flask_wtf.FlaskForm`(而非 `wtforms.Form`),获得 CSRF 保护等 Flask 集成能力。
|
||||
|
||||
```python
|
||||
id = IntegerField('ID', validators=[Optional()])
|
||||
```
|
||||
**ArrayField 支持**:重写 `process` 方法,确保 `ArrayField` 能正确处理前端传入的 JSON 数组数据。
|
||||
|
||||
`id` 默认为 `None`。创建记录时前端不传 `id`,WTForms 自动赋默认值;编辑记录时前端传入 `id`,Service 层据此判断是新增还是更新。
|
||||
**id 字段由子类自行定义**:`BaseForm` 本身不定义 `id` 字段,各业务表单类根据需要自行声明(通常为 `IntegerField` + `Optional()`)。
|
||||
|
||||
### 子类示例
|
||||
|
||||
@@ -301,7 +310,7 @@ class PositionForm(BaseForm):
|
||||
### 软删除约定
|
||||
|
||||
:::warning 重要设计约定
|
||||
- **等值便捷方法**(get_by_id / get_one / get_all / count / filter_by / exists / exists_by_field / paginate)自动追加 `is_delete=0` 过滤,调用方无需关心
|
||||
- **等值便捷方法**(get_by_id / get_by_ids / get_all / first_by / count_by / exists / exists_by / find_page / find_list)自动追加 `is_delete=0` 过滤,调用方无需关心
|
||||
- **原始出口** `filter(*criterion)` **不做**软删过滤,由调用方自行追加 `Model.is_delete == 0`,用于 `in_` / `!=` / `and_` / 区间等复杂条件
|
||||
:::
|
||||
|
||||
@@ -352,13 +361,17 @@ WTForms Schema 可能用 camelCase(如 `parentId`),而模型列是 snake_c
|
||||
|
||||
| 方法 | 说明 | 返回值 |
|
||||
|:---|:---|:---|
|
||||
| `get_page()` | 分页查询 | `R.ok(data=[...], count=total)` |
|
||||
| `get_list()` | 不分页查询 | `R.ok(data=[...])` |
|
||||
| `get_page(params)` | 分页查询 | `dict`(含 records/total 等分页数据) |
|
||||
| `get_list(params)` | 不分页查询 | `list`(记录列表) |
|
||||
| `get_detail(obj_id)` | 详情查询 | `dict` 或 `None` |
|
||||
| `add(data)` | 新增记录 | `R.ok(msg='添加成功')` |
|
||||
| `update(data)` | 编辑记录 | 含表单校验 + 存在性校验 |
|
||||
| `remove(ids)` | 批量软删除 | 逗号分隔 ID 字符串 |
|
||||
| `update_status(data)` | 状态切换 | 含状态表单校验 |
|
||||
| `add(data)` | 新增记录 | `None`(成功无返回,失败 raise BizError) |
|
||||
| `update(data)` | 编辑记录 | `None`(成功无返回,失败 raise BizError) |
|
||||
| `remove(ids)` | 批量软删除 | `int`(删除数量) |
|
||||
| `update_status(data)` | 状态切换 | `None`(成功无返回,失败 raise BizError) |
|
||||
|
||||
:::warning Service 不返回 R 对象
|
||||
Service 层入参为数据字典、出参为纯数据(或 None),业务失败通过 `raise BizError` 表达。Router 层负责封装 `R.ok()` / `R.failed()` 响应。
|
||||
:::
|
||||
|
||||
### 钩子方法
|
||||
|
||||
|
||||
@@ -181,7 +181,6 @@ def get_redis():
|
||||
| **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 存储 |
|
||||
@@ -207,27 +206,9 @@ def incr_with_expire(redis_client, key: str, seconds: int) -> int:
|
||||
```
|
||||
|
||||
:::tip 为什么需要 Lua 脚本?
|
||||
`incr` 和 `expire` 分开执行时,两步之间若进程崩溃,key 会以无 TTL 的状态永久残留,导致基于 Redis 的限流/登录锁定永久封禁。Lua 脚本保证原子性,消除此风险。
|
||||
`incr` 和 `expire` 分开执行时,两步之间若进程崩溃,key 会以无 TTL 的状态永久残留。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 突发问题。
|
||||
|
||||
## 缓存策略对比
|
||||
|
||||
@@ -236,7 +217,7 @@ return effective
|
||||
| **Cache-Aside** | 先查缓存,未命中查 DB,写回缓存 | 读多写少 | 数据字典、权限列表 |
|
||||
| **Write-Through** | 写操作同时更新缓存 | 数据一致性要求高 | 字典项变更时主动失效 |
|
||||
| **Write-Behind** | 写操作只更新缓存,异步写 DB | 写入性能要求高 | 未使用 |
|
||||
| **TTL 过期** | 缓存自动过期 | 允许短暂不一致 | 登录锁、限流计数 |
|
||||
| **TTL 过期** | 缓存自动过期 | 允许短暂不一致 | 登录锁 |
|
||||
| **主动失效** | 数据变更时主动清除缓存 | 数据一致性要求高 | 字典缓存、权限缓存 |
|
||||
|
||||
## 缓存失效策略
|
||||
@@ -263,7 +244,6 @@ invalidate_perm_cache(user_id)
|
||||
```
|
||||
数据字典缓存:TTL 300 秒(5 分钟)
|
||||
登录失败计数:TTL 300 秒(5 分钟)
|
||||
滑动窗口限流:TTL 2 倍窗口时长
|
||||
验证码:TTL 120 秒(2 分钟)
|
||||
```
|
||||
|
||||
|
||||
@@ -21,13 +21,12 @@ src/
|
||||
│ ├── response.py # 统一响应:R.ok() / R.failed() / R.page()
|
||||
│ ├── exceptions.py # 自定义异常:BizError
|
||||
│ ├── permission.py # 权限装饰器:check_permission
|
||||
│ ├── logger.py # 日志配置:结构化日志、文件输出
|
||||
│ ├── mail.py # 邮件发送
|
||||
│ └── scheduler.py # 定时任务调度器
|
||||
│
|
||||
├── middleware/ # HTTP 中间件层
|
||||
│ ├── __init__.py # 统一导出
|
||||
│ ├── auth.py # JWT 认证中间件:login_required
|
||||
│ ├── cors.py # CORS 跨域中间件
|
||||
│ ├── demo.py # 演示模式中间件:check_demo
|
||||
│ ├── errors.py # 全局异常处理
|
||||
│ ├── operation_log.py # 操作日志中间件
|
||||
@@ -57,7 +56,7 @@ src/
|
||||
│ ├── json.py # JSON 工具
|
||||
│ ├── model_utils.py # 模型工具
|
||||
│ ├── regular.py # 正则工具
|
||||
│ ├── request.py # 请求工具:parse_request_body / parse_id_list
|
||||
│ ├── request.py # 请求工具:parse_request_body / parse_batch_ids
|
||||
│ ├── rich_text.py # 富文本处理
|
||||
│ ├── time_utils.py # 时间工具
|
||||
│ └── user_agent.py # User-Agent 解析
|
||||
|
||||
@@ -139,8 +139,8 @@ update_training_status = TrainingService.update_status
|
||||
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
|
||||
from src.core import response as R
|
||||
from src.common.utils.request import parse_request_body, parse_batch_ids
|
||||
|
||||
training_blue = Blueprint('training', __name__, url_prefix='/training')
|
||||
|
||||
@@ -197,7 +197,7 @@ def status():
|
||||
@check_permission('sys:training:batchDelete')
|
||||
def batch_delete():
|
||||
from src.modules.system.training.service import batch_delete_trainings
|
||||
id_list = parse_id_list(request)
|
||||
id_list = parse_batch_ids(request)
|
||||
return batch_delete_trainings(id_list)
|
||||
```
|
||||
|
||||
@@ -307,7 +307,7 @@ def _normalize_driver(driver: str) -> str:
|
||||
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_DATABASE = os.getenv('DB_DATABASE', 'djangoadmin.flask.naivevue')
|
||||
DB_USERNAME = os.getenv('DB_USERNAME', 'root')
|
||||
DB_PASSWORD = os.getenv('DB_PASSWORD', '')
|
||||
|
||||
@@ -336,7 +336,7 @@ def _build_database_url(driver: str = None) -> str:
|
||||
| 驱动 | 连接串格式 | Python 包 |
|
||||
|:----|:----|:----|
|
||||
| mysql | `mysql+pymysql://user:pass@host:port/db?charset=utf8mb4` | PyMySQL |
|
||||
| postgresql | `postgresql+psycopg://user:pass@host:port/db` | psycopg[binary] |
|
||||
| postgresql | `postgresql+psycopg2://user:pass@host:port/db` | psycopg2-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` | 内置 |
|
||||
@@ -376,7 +376,7 @@ DB_PASSWORD=your_password
|
||||
```
|
||||
|
||||
:::warning 驱动切换注意事项
|
||||
1. 切换驱动后需安装对应的 Python 包(如 `psycopg[binary]` for PostgreSQL)
|
||||
1. 切换驱动后需安装对应的 Python 包(如 `psycopg2-binary` for PostgreSQL)
|
||||
2. 数据迁移使用 `scripts/migrate_db.py` 工具
|
||||
3. SQLite 不支持并发写入,仅适用于开发/测试环境
|
||||
4. Oracle 需要额外的客户端库配置
|
||||
@@ -409,13 +409,17 @@ python scripts/migrate_db.py --src-driver mysql --src-host ... --dst-driver post
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
|
||||
# CORS + ProxyFix(最外层)
|
||||
CORS(app, ...)
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, ...)
|
||||
|
||||
# 先注册 → 内层(最后处理请求)
|
||||
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) # 全局异常处理器
|
||||
register_v1(app) # 路由注册
|
||||
register_extends(app) # 扩展初始化
|
||||
register_middleware(app) # 登录校验 + 白名单
|
||||
register_error_handlers(app) # 全局异常处理器
|
||||
|
||||
# 请求计时(先注册→内层,统计业务耗时)
|
||||
@app.before_request
|
||||
@@ -429,8 +433,7 @@ def create_app():
|
||||
response.headers["X-Process-Time"] = str(round(process_time, 5))
|
||||
return response
|
||||
|
||||
register_rate_limit(app) # 滑动窗口限流
|
||||
register_cors(app) # CORS(最后注册→最外层)
|
||||
CORS(app, ...) # CORS(最后注册→最外层)
|
||||
|
||||
return app
|
||||
```
|
||||
@@ -470,13 +473,13 @@ def create_app():
|
||||
### 中间件执行顺序
|
||||
|
||||
```
|
||||
请求进入 → CORS → 限流 → 上传限制 → 计时 → 操作日志 → DB会话 → Redis注入 → 路由处理
|
||||
请求进入 → CORS → 上传限制 → 计时 → 操作日志 → DB会话 → Redis注入 → 路由处理
|
||||
│
|
||||
响应返回 ← CORS ← 限流 ← 上传限制 ← 计时 ← 操作日志 ← DB会话 ← Redis注入 ← 路由处理 ←┘
|
||||
响应返回 ← CORS ← 上传限制 ← 计时 ← 操作日志 ← DB会话 ← Redis注入 ← 路由处理 ←┘
|
||||
```
|
||||
|
||||
:::warning 中间件注册顺序
|
||||
Flask 中间件按注册顺序执行。`register_cors` 最后注册 → 最外层(最先处理请求)。`register_redis` 最先注册 → 最内层(最后处理请求)。
|
||||
Flask 中间件按注册顺序执行。CORS 最先配置 → 最外层(最先处理请求)。`register_middleware` 最后注册 → 最内层(最后处理请求)。
|
||||
:::
|
||||
|
||||
## 四、装饰器扩展
|
||||
|
||||
@@ -85,8 +85,8 @@ from flask import g, request
|
||||
from src.core.jwt import decode_token
|
||||
|
||||
|
||||
def register_auth(app):
|
||||
"""注册登录校验中间件"""
|
||||
def register_middleware(app):
|
||||
"""注册登录校验中间件(实际位于 src/middleware/auth.py)"""
|
||||
|
||||
@app.before_request
|
||||
def login_required():
|
||||
|
||||
@@ -70,8 +70,8 @@
|
||||
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
|
||||
from src.core import response as R
|
||||
from src.common.utils.request import parse_request_body, parse_batch_ids
|
||||
|
||||
# ============================================================
|
||||
# 创建 Blueprint 实例
|
||||
@@ -199,9 +199,9 @@ def batch_delete():
|
||||
|
||||
:return: 批量删除结果响应
|
||||
"""
|
||||
from src.modules.system.position.service import batch_delete_positions
|
||||
id_list = parse_id_list(request)
|
||||
return batch_delete_positions(id_list)
|
||||
from src.modules.system.position.service import delete_positions
|
||||
id_list = parse_batch_ids(request)
|
||||
return delete_positions(id_list)
|
||||
```
|
||||
|
||||
:::tip 装饰器顺序
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Browser (Vue3 + ElementPlus) │
|
||||
│ Browser (Vue3 + NaiveUI) │
|
||||
│ Axios HTTP Request / Vite Dev Proxy │
|
||||
└──────────────────────────────┬──────────────────────────────────────────┘
|
||||
│
|
||||
@@ -52,7 +52,7 @@
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────┐ ┌──────────────────────────────────────────┐ │
|
||||
│ │ SQLAlchemy 2.0 │ │ Redis │ │
|
||||
│ │ ORM → SQL 生成 │ │ Token 黑名单 · 登录锁 · 限流计数 · 缓存 │ │
|
||||
│ │ ORM → SQL 生成 │ │ Token 黑名单 · 登录锁 · 缓存 │ │
|
||||
│ └──────────┬──────────┘ └──────────────────────────────────────────┘ │
|
||||
└─────────────┼───────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
@@ -92,19 +92,18 @@
|
||||
|
||||
```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 会话管理
|
||||
CORS(app, ...) # CORS(最外层)
|
||||
ProxyFix(app.wsgi_app, ...) # 可信代理
|
||||
register_operation_log(app) # 操作日志记录
|
||||
register_transaction(app) # AOP 事务管理
|
||||
register_auth(app) # JWT 认证
|
||||
register_v1(app) # 路由注册
|
||||
register_extends(app) # 扩展初始化
|
||||
register_middleware(app) # 认证中间件
|
||||
register_v1(app) # 路由注册
|
||||
register_exception(app) # 全局异常处理
|
||||
```
|
||||
|
||||
外层中间件(CORS、限流)先拦截,内层中间件(DB 会话、Redis 注入)后处理,响应沿反向链返回。限流被置于 CORS 内层,被限流的请求不进入 DB 会话与操作日志,避免 DoS 攻击时日志表与 DB 连接不降反增。
|
||||
外层中间件(CORS)先拦截,内层中间件(DB 会话、Redis 注入)后处理,响应沿反向链返回。
|
||||
|
||||
### 5. 全局异常处理
|
||||
|
||||
@@ -177,16 +176,17 @@ def create_app():
|
||||
app = Flask(__name__)
|
||||
# 1. 验证配置
|
||||
validate_config()
|
||||
# 2. 初始化扩展
|
||||
register_extends(app)
|
||||
# 2. CORS + ProxyFix
|
||||
CORS(app, ...)
|
||||
app.wsgi_app = ProxyFix(app.wsgi_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)
|
||||
register_extends(app)
|
||||
register_middleware(app)
|
||||
register_error_handlers(app)
|
||||
# 4. 依赖注入
|
||||
register_v1(app)
|
||||
# 5. 注册异常处理
|
||||
register_exception(app)
|
||||
@@ -202,7 +202,7 @@ def create_app():
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Frontend (Vue3) │
|
||||
│ ElementPlus · Pinia · Vue Router · Axios │
|
||||
│ NaiveUI · Pinia · Vue Router · Axios │
|
||||
└──────────────────────────┬───────────────────────────────────┘
|
||||
│ HTTP / JSON
|
||||
┌──────────────────────────┴───────────────────────────────────┐
|
||||
@@ -249,4 +249,4 @@ def create_app():
|
||||
|
||||
## 总结
|
||||
|
||||
项目采用经典的前后端分离 + 三层架构设计,通过中间件链实现横切关注点(认证、限流、日志、事务)的统一处理,通过基类模板方法消除重复 CRUD 代码,通过 WTForms 实现请求参数的声明式校验。整体架构在保持简洁的同时,具备良好的可扩展性和可维护性,适合企业级后台管理系统的快速开发与长期演进。
|
||||
项目采用经典的前后端分离 + 三层架构设计,通过中间件链实现横切关注点(认证、日志、事务)的统一处理,通过基类模板方法消除重复 CRUD 代码,通过 WTForms 实现请求参数的声明式校验。整体架构在保持简洁的同时,具备良好的可扩展性和可维护性,适合企业级后台管理系统的快速开发与长期演进。
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
## 请求链路总览
|
||||
|
||||
:::tip 链路概览
|
||||
一个典型的 API 请求经过以下阶段:浏览器 → Nginx → CORS → 限流 → Redis 注入 → DB 会话 → 操作日志 → 路由匹配 → JWT 认证 → RBAC 鉴权 → WTForms 校验 → Router → Service → Repository → DB → 响应序列化 → 中间件反向链 → 浏览器
|
||||
一个典型的 API 请求经过以下阶段:浏览器 → Nginx → CORS → Redis 注入 → DB 会话 → 操作日志 → 路由匹配 → JWT 认证 → RBAC 鉴权 → WTForms 校验 → Router → Service → Repository → DB → 响应序列化 → 中间件反向链 → 浏览器
|
||||
:::
|
||||
|
||||
以下以 `POST /api/v1/position/add` 添加岗位为例,逐步拆解完整链路。
|
||||
@@ -49,37 +49,25 @@ Nginx 将请求代理到 Flask 服务,同时附加客户端真实 IP 到 `X-Fo
|
||||
|
||||
Flask 的中间件通过 `before_request` / `after_request` 钩子实现。请求从最外层进入,逐层穿透到路由处理函数,响应沿反向链返回。
|
||||
|
||||
### 3. CORS 跨域中间件(最外层)
|
||||
### 3. CORS 跨域配置(最外层)
|
||||
|
||||
```python
|
||||
# src/middleware/cors.py
|
||||
register_cors(app) # 注册 CORS 中间件
|
||||
# src/app.py - create_app()
|
||||
from flask_cors import CORS
|
||||
from config import CORS_ALLOWED_ORIGINS
|
||||
|
||||
if CORS_ALLOWED_ORIGINS:
|
||||
allowed_origins = [origin.strip() for origin in CORS_ALLOWED_ORIGINS.split(',') if origin.strip()]
|
||||
CORS(app, resources={r"/*": {"origins": allowed_origins}}, supports_credentials=True)
|
||||
else:
|
||||
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=False)
|
||||
```
|
||||
|
||||
- 检查 `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 上下文注入中间件
|
||||
### 4. Redis 上下文注入中间件
|
||||
|
||||
```python
|
||||
# src/core/redis_client.py
|
||||
@@ -351,8 +339,7 @@ R.ok(msg='添加成功')
|
||||
2. **操作日志**:记录写操作日志到数据库
|
||||
3. **DB 会话**:检测 `code==0`,执行 `db.session.commit()`
|
||||
4. **Redis 上下文**:无操作(g 对象自动清理)
|
||||
5. **限流**:无操作(响应直接透传)
|
||||
6. **CORS**:附加 `Access-Control-Allow-*` 响应头
|
||||
5. **CORS**:附加 `Access-Control-Allow-*` 响应头
|
||||
|
||||
### 20. 浏览器接收响应
|
||||
|
||||
@@ -381,10 +368,6 @@ R.ok(msg='添加成功')
|
||||
[CORS 中间件] ── 跨域校验通过
|
||||
│
|
||||
▼
|
||||
[限流中间件] ── 滑动窗口计数(Redis)
|
||||
│ ├─ 超限 → 直接返回失败
|
||||
│ └─ 通过 ▼
|
||||
▼
|
||||
[DB 会话中间件] ── 标记事务活跃
|
||||
│
|
||||
▼
|
||||
@@ -421,7 +404,7 @@ R.ok(msg='添加成功')
|
||||
└─ 业务失败(code≠0)→ rollback 回滚事务
|
||||
│
|
||||
▼
|
||||
[限流] → [CORS] → [Nginx]
|
||||
[CORS] → [Nginx]
|
||||
│
|
||||
▼
|
||||
[浏览器] ← JSON 响应({ code, data, msg, ok })
|
||||
@@ -443,4 +426,4 @@ R.ok(msg='添加成功')
|
||||
|
||||
## 总结
|
||||
|
||||
一个 API 请求从浏览器到数据库经过 7 个阶段、20+ 个处理节点。中间件链负责横切关注点(安全、限流、日志、事务),路由层负责认证鉴权,WTForms 负责参数校验,三层架构负责业务逻辑与数据访问。这种分层设计使得每一层职责单一、可独立测试,系统在高并发场景下仍能保持稳定的性能和可靠的事务一致性。
|
||||
一个 API 请求从浏览器到数据库经过 7 个阶段、20+ 个处理节点。中间件链负责横切关注点(安全、日志、事务),路由层负责认证鉴权,WTForms 负责参数校验,三层架构负责业务逻辑与数据访问。这种分层设计使得每一层职责单一、可独立测试,系统在高并发场景下仍能保持稳定的性能和可靠的事务一致性。
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
|
||||
**授权(Authorization)**:基于 RBAC(基于角色的访问控制)模型设计,权限控制粒度细化至接口级。用户通过角色继承权限节点(如 `sys:position:add`),由 `@check_permission` 装饰器在路由层统一拦截校验,权限数据采用 Redis 缓存加速查询,管理员(`userId == 1`)自动放行。权限变更时,相关缓存主动失效,确保权限更新的实时性。
|
||||
|
||||
**防护(Protection)**:系统从多维度构建安全防护体系。限流中间件基于 Redis 滑动窗口算法,有效防御 CC 攻击和接口滥用;参数校验通过 WTForms Schema 自动拦截非法输入,防止 SQL 注入和 XSS 攻击;敏感密码采用 bcrypt 加盐哈希存储,即使数据库泄露也无法还原明文;演示模式下 `@check_demo` 装饰器统一拦截写操作,保护测试数据不被污染;跨域配置精确控制可信域名,防止 CSRF 攻击。
|
||||
**防护(Protection)**:系统从多维度构建安全防护体系。参数校验通过 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 关闭、限流开启),如需放宽需显式配置
|
||||
安全默认:框架默认配置为安全模式(如 DEBUG 关闭),如需放宽需显式配置
|
||||
:::
|
||||
|
||||
## 安全架构总览
|
||||
@@ -29,7 +29,7 @@
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 网络层防护 │ │
|
||||
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
|
||||
│ │ │ CORS │ │ 限流 │ │ XSS 防护 │ │ 上传体积限制 │ │ │
|
||||
│ │ │ CORS │ │ XSS 防护 │ │ 上传体积限制 │ │ │
|
||||
│ │ │ 跨域控制 │ │ 滑动窗口 │ │ 输入清洗 │ │ DoS 防御 │ │ │
|
||||
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
@@ -316,7 +316,7 @@ def verify_password(password: str, salt: str, hashed: str) -> bool:
|
||||
# src/core/permission.py
|
||||
from functools import wraps
|
||||
from flask import request
|
||||
from src.core.response import R
|
||||
from src.core import response as R
|
||||
from src.core.security import get_user_id
|
||||
|
||||
def check_permission(permission: str):
|
||||
@@ -369,7 +369,7 @@ def invalidate_perm_cache(user_id: int):
|
||||
# src/middleware/demo.py
|
||||
from functools import wraps
|
||||
from flask import current_app
|
||||
from src.core.response import R
|
||||
from src.core import response as R
|
||||
|
||||
def check_demo(func):
|
||||
"""演示模式拦截装饰器,放在 @check_permission 之后、路由函数之前"""
|
||||
@@ -390,39 +390,20 @@ def check_demo(func):
|
||||
### CORS 跨域控制
|
||||
|
||||
```python
|
||||
# src/middleware/cors.py
|
||||
# src/app.py - create_app()
|
||||
from flask_cors import CORS
|
||||
from config import CORS_ALLOWED_ORIGINS
|
||||
|
||||
def register_cors(app):
|
||||
CORS(app,
|
||||
origins=CORS_ALLOWED_ORIGINS,
|
||||
supports_credentials=True,
|
||||
methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["Content-Type", "Authorization"])
|
||||
if CORS_ALLOWED_ORIGINS:
|
||||
allowed_origins = [origin.strip() for origin in CORS_ALLOWED_ORIGINS.split(',') if origin.strip()]
|
||||
CORS(app, resources={r"/*": {"origins": allowed_origins}}, supports_credentials=True)
|
||||
else:
|
||||
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=False)
|
||||
```
|
||||
|
||||
- 生产环境应配置具体的 `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
|
||||
@@ -450,17 +431,16 @@ def sanitize_html(content: str) -> str:
|
||||
### 上传体积限制
|
||||
|
||||
```python
|
||||
# src/middleware/upload_size.py
|
||||
from flask import request
|
||||
# src/modules/upload/service.py
|
||||
from config import UPLOAD_MAX_SIZE_MB
|
||||
|
||||
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="上传文件超出大小限制")
|
||||
max_size_bytes = UPLOAD_MAX_SIZE_MB * 1024 * 1024
|
||||
if file_size > max_size_bytes:
|
||||
raise BizError(f"文件大小超过限制,最大允许 {UPLOAD_MAX_SIZE_MB}MB")
|
||||
```
|
||||
|
||||
- 按 `Content-Length` 提前拒绝超大请求体
|
||||
- 避免超大 multipart 落临时盘后再由业务层校验
|
||||
- 在业务层校验文件大小,通过 `UPLOAD_MAX_SIZE_MB` 环境变量配置上限
|
||||
- 前端配合 `n-upload` 组件的 `before-upload` 钩子做前置校验
|
||||
|
||||
## 四、数据层防护
|
||||
|
||||
@@ -548,9 +528,6 @@ def operation_log():
|
||||
| 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 | 锁定写操作 |
|
||||
@@ -567,4 +544,4 @@ def operation_log():
|
||||
|
||||
## 总结
|
||||
|
||||
项目的安全架构涵盖认证(JWT + 验证码 + 登录锁定 + Token 黑名单)、授权(RBAC + check_permission + check_demo)、防护(CORS + 限流 + XSS + 上传限制 + SQL 注入防护)、审计(操作日志 + 登录日志)四大领域。通过纵深防御策略,在网络层、认证层、授权层、数据层分别设置安全屏障,确保系统在面对各类安全威胁时具备足够的防护能力。生产部署时需特别关注 JWT 密钥强度、CORS 配置、HTTPS 启用等关键安全项。
|
||||
项目的安全架构涵盖认证(JWT + 验证码 + 登录锁定 + Token 黑名单)、授权(RBAC + check_permission + check_demo)、防护(CORS + XSS + 上传限制 + SQL 注入防护)、审计(操作日志 + 登录日志)四大领域。通过纵深防御策略,在网络层、认证层、授权层、数据层分别设置安全屏障,确保系统在面对各类安全威胁时具备足够的防护能力。生产部署时需特别关注 JWT 密钥强度、CORS 配置、HTTPS 启用等关键安全项。
|
||||
|
||||
@@ -63,30 +63,29 @@ SQLAlchemy 是 Python 生态中最成熟、功能最完备的 ORM 框架,2.0
|
||||
|
||||
## 前端技术栈
|
||||
|
||||
### Vue3 + Vite + ElementPlus
|
||||
### Vue3 + Vite + NaiveUI
|
||||
|
||||
| 组件 | 版本 | 选型理由 |
|
||||
|:---|:---|:---|
|
||||
| **Vue3** | 3.x | Composition API、更好的 TypeScript 支持、Teleport/Suspense 等新特性 |
|
||||
| **Vite** | 5.x | 极速 HMR、ESM 原生支持、开箱即用的 TypeScript 支持 |
|
||||
| **ElementPlus** | 2.x | Vue3 生态最成熟的 UI 组件库,企业级后台管理系统首选 |
|
||||
| **NaiveUI** | 2.x | Vue3 生态高质量 UI 组件库,TypeScript 友好,主题定制灵活 |
|
||||
| **Pinia** | 2.x | Vue3 官方推荐的状态管理方案,替代 Vuex |
|
||||
| **Vue Router** | 4.x | Vue3 官方路由,支持动态路由、路由守卫 |
|
||||
| **Axios** | 1.x | HTTP 客户端,支持请求/响应拦截器、取消请求 |
|
||||
| **Axios** | 0.27.x | HTTP 客户端,支持请求/响应拦截器、取消请求 |
|
||||
|
||||
## 缓存与消息中间件
|
||||
|
||||
### Redis
|
||||
|
||||
:::tip Redis 在项目中的角色
|
||||
Redis 在项目中承担多个关键职责:JWT 令牌黑名单、登录失败锁定、请求限流计数、数据字典缓存、权限缓存等。采用同步客户端与 Flask 的同步模型无缝集成。
|
||||
Redis 在项目中承担多个关键职责:JWT 令牌黑名单、登录失败锁定、数据字典缓存、权限缓存等。采用同步客户端与 Flask 的同步模型无缝集成。
|
||||
:::
|
||||
|
||||
| 用途 | 数据结构 | 过期策略 |
|
||||
|:---|:---|:---|
|
||||
| JWT 黑名单 | String | 等于 Token 剩余有效期 |
|
||||
| 登录失败锁 | String | 指定锁定时长 |
|
||||
| 滑动窗口限流 | String (Lua) | 2 倍窗口时长 |
|
||||
| 数据字典缓存 | String (JSON) | TTL 可配置 |
|
||||
| 权限列表缓存 | String (JSON) | 变更时主动失效 |
|
||||
|
||||
@@ -211,9 +210,9 @@ Redis 在项目中承担多个关键职责:JWT 令牌黑名单、登录失败
|
||||
|
||||
| 依赖 | 版本 | 用途 |
|
||||
|:---|:---|:---|
|
||||
| `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` |
|
||||
| `Flask` | >= 3.1.3 | Web 框架,`src/app.py` 通过 `create_app()` 创建应用 |
|
||||
| `Flask-JWT-Extended` | >= 4.7.1 | JWT 令牌签发与验证,`src/core/jwt.py` |
|
||||
| `bcrypt` | >= 4.3.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()` |
|
||||
@@ -230,4 +229,4 @@ Redis 在项目中承担多个关键职责:JWT 令牌黑名单、登录失败
|
||||
|
||||
## 总结
|
||||
|
||||
项目的技术选型以「稳定可靠、轻量灵活、可扩展」为核心原则。Flask + SQLAlchemy 2.0 + WTForms 构成后端铁三角,Vue3 + Vite + ElementPlus 提供现代化前端体验,Redis 承担缓存与中间件职责。各组件版本经过充分测试验证,形成稳定的技术底座。选型时优先考虑组件的独立性和可替换性,避免强耦合,为未来的架构演进预留空间。
|
||||
项目的技术选型以「稳定可靠、轻量灵活、可扩展」为核心原则。Flask + SQLAlchemy 2.0 + WTForms 构成后端铁三角,Vue3 + Vite + NaiveUI 提供现代化前端体验,Redis 承担缓存与中间件职责。各组件版本经过充分测试验证,形成稳定的技术底座。选型时优先考虑组件的独立性和可替换性,避免强耦合,为未来的架构演进预留空间。
|
||||
|
||||
Reference in New Issue
Block a user