优化更新

This commit is contained in:
2026-09-13 23:01:19 +08:00
parent 0c67b3e8b5
commit 23bd737b82
88 changed files with 2214 additions and 2503 deletions
@@ -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()` 响应。
:::
### 钩子方法
+2 -22
View File
@@ -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 承担缓存与中间件职责。各组件版本经过充分测试验证,形成稳定的技术底座。选型时优先考虑组件的独立性和可替换性,避免强耦合,为未来的架构演进预留空间。