From 74f41f651f9c3c99999f361fcc9425462d8dfbea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=9A=82=E8=9A=81=E4=BA=91?= Date: Sun, 13 Sep 2026 20:29:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.vitepress/config/shared.ts | Bin 2142 -> 2169 bytes .../.vitepress/config/shared.ts | Bin 2140 -> 2167 bytes .../.vitepress/config/shared.ts | Bin 2140 -> 2171 bytes .../.vitepress/config/shared.ts | Bin 2142 -> 2169 bytes .../.vitepress/config/shared.ts | Bin 2142 -> 2165 bytes .../.vitepress/config/zh.ts | 14 +- DjangoAdmin_Flask_EleVue/zh/changelog.md | 5 +- DjangoAdmin_Flask_EleVue/zh/course.md | 13 +- DjangoAdmin_Flask_EleVue/zh/faq.md | 1 - DjangoAdmin_Flask_EleVue/zh/introduction.md | 8 +- DjangoAdmin_Flask_EleVue/zh/struct.md | 27 +- .../zh/volume1/backend.md | 30 +- .../zh/volume1/database.md | 18 +- DjangoAdmin_Flask_EleVue/zh/volume1/docker.md | 2 +- .../zh/volume1/env-vars.md | 71 +++-- .../zh/volume1/environment.md | 2 +- .../zh/volume1/frontend.md | 2 +- .../zh/volume2/blueprint.md | 4 +- .../zh/volume2/code-list.md | 11 +- DjangoAdmin_Flask_EleVue/zh/volume2/faq.md | 2 +- DjangoAdmin_Flask_EleVue/zh/volume2/model.md | 239 ++++++++++----- DjangoAdmin_Flask_EleVue/zh/volume2/schema.md | 247 ++++++++------- .../zh/volume3/app-factory.md | 21 +- .../zh/volume3/base-classes.md | 91 +++--- DjangoAdmin_Flask_EleVue/zh/volume3/cache.md | 24 +- .../zh/volume3/directory.md | 5 +- .../zh/volume3/extensibility.md | 31 +- .../zh/volume3/flask-g-object.md | 4 +- .../zh/volume3/layer-arch.md | 10 +- .../zh/volume3/overview.md | 32 +- .../zh/volume3/request-flow.md | 47 +-- .../zh/volume3/security.md | 65 ++-- .../zh/volume3/tech-stack.md | 11 +- .../zh/volume4/business/user.md | 4 +- .../zh/volume4/core/auth-flow.md | 8 +- .../zh/volume4/core/cors.md | 154 ++++++---- .../zh/volume4/core/demo-mode.md | 2 +- .../zh/volume4/core/exception.md | 2 +- .../zh/volume4/core/idempotent.md | 21 +- .../zh/volume4/core/jwt.md | 2 +- .../zh/volume4/core/operation-log.md | 66 ++-- .../zh/volume4/core/rate-limit.md | 145 --------- .../zh/volume4/core/rbac.md | 103 +++++-- .../zh/volume4/core/transaction.md | 2 +- .../zh/volume4/core/validator.md | 2 +- .../zh/volume4/frontend/setup.md | 2 +- .../zh/volume4/integration/api-contract.md | 2 +- .../zh/volume4/tools/ip-ua.md | 111 ++----- .../zh/volume4/tools/logger.md | 124 -------- .../zh/volume4/tools/request-util.md | 16 +- DjangoAdmin_Flask_EleVue/zh/volume5/demo.md | 4 +- .../zh/volume6/db-migrate.md | 22 +- DjangoAdmin_Flask_EleVue/zh/volume6/docker.md | 2 +- .../zh/volume6/env-plan.md | 12 +- .../zh/volume6/monitoring.md | 12 +- DjangoAdmin_Flask_EleVue/zh/volume6/nginx.md | 5 +- .../zh/volume6/supervisor.md | 6 +- .../zh/volume6/upgrade.md | 4 +- .../zh/volume7/api-response.md | 4 +- .../zh/volume7/deletion.md | 8 +- .../zh/volume7/doc-standard.md | 2 +- .../zh/volume7/endpoint.md | 16 +- .../zh/volume7/pydantic.md | 190 ------------ DjangoAdmin_Flask_EleVue/zh/volume7/router.md | 288 +++++++++++------- DjangoAdmin_Flask_EleVue/zh/why.md | 4 +- 65 files changed, 1061 insertions(+), 1321 deletions(-) delete mode 100644 DjangoAdmin_Flask_EleVue/zh/volume4/core/rate-limit.md delete mode 100644 DjangoAdmin_Flask_EleVue/zh/volume4/tools/logger.md delete mode 100644 DjangoAdmin_Flask_EleVue/zh/volume7/pydantic.md diff --git a/DjangoAdmin_Django_AntdVue/.vitepress/config/shared.ts b/DjangoAdmin_Django_AntdVue/.vitepress/config/shared.ts index 9ad31b0e96405067017a565c5d19bd2eafc65089..6211781d8bb318f238d72c4c03d87c11ccdf8c47 100644 GIT binary patch delta 57 zcmca7@Kazy32#iP0TAedD8sag<>ow(8sow(8sow(8sow(8sMi682EA|oqO-dYu delta 47 zcmew<;F B5nuoS diff --git a/DjangoAdmin_Flask_EleVue/.vitepress/config/shared.ts b/DjangoAdmin_Flask_EleVue/.vitepress/config/shared.ts index f123406a1b69034452eb9c35dbfeb84c49d1f1d0..606f2b47251a40be47e59749d3e0154a2c8a1ca0 100644 GIT binary patch delta 76 zcmca7@Ksow(8s = { collapsed: false, items: [ { - text: "5.1.14 请求限流", - link: "/zh/volume4/core/rate-limit.html", - }, - { - text: "5.1.15 防重复提交", + text: "5.1.14 防重复提交", link: "/zh/volume4/core/idempotent.html", }, ], @@ -511,11 +507,11 @@ export const zhConfig: LocaleSpecificConfig = { collapsed: false, items: [ { - text: "5.1.16 AOP 事务管理", + text: "5.1.15 AOP 事务管理", link: "/zh/volume4/core/transaction.html", }, { - text: "5.1.17 事务装饰器", + text: "5.1.16 事务装饰器", link: "/zh/volume4/core/transaction-decorator.html", }, ], @@ -624,10 +620,6 @@ export const zhConfig: LocaleSpecificConfig = { text: "5.3.6 密码加密与验证", link: "/zh/volume4/tools/password.html", }, - { - text: "5.3.7 日志系统", - link: "/zh/volume4/tools/logger.html", - }, ], }, // 🧩 辅助工具 diff --git a/DjangoAdmin_Flask_EleVue/zh/changelog.md b/DjangoAdmin_Flask_EleVue/zh/changelog.md index 9a11a91..0c09072 100644 --- a/DjangoAdmin_Flask_EleVue/zh/changelog.md +++ b/DjangoAdmin_Flask_EleVue/zh/changelog.md @@ -16,7 +16,7 @@ | 2.5.0 | 2025-06-09 | Vue3 + ElementPlus + TypeScript + Vite | | 2.6.0 | 2025-09-22 | 代码生成器 + Excel 导入导出 | | 2.7.0 | 2025-12-16 | 定时任务调度 + 操作日志 + 登录日志 | -| 2.8.0 | 2026-03-28 | 接口限流 + IP 地理位置 + 安全防护增强 | +| 2.8.0 | 2026-03-28 | IP 地理位置 + UA 解析 + 安全防护增强 | | 2.9.0 | 2026-06-07 | 技术文档体系 + 契约测试 + 缓存优化 | | **3.0.0** | **2026-09-01**** | **模块化分层 + Repository 层 + AOP 事务 + 数据迁移工具** | @@ -86,13 +86,12 @@ ## [2.8.0] - 2026-03-28 -**新增接口限流与 IP 地理位置查询,安全防护全面增强。** +**新增 IP 地理位置查询与 UA 解析,安全防护全面增强。** ### 新增 - **IP 地理位置查询**:基于离线 GeoIP 数据库,服务启动时加载内存,查询零延迟,无外部 API 依赖 - **User-Agent 解析**:自动识别客户端操作系统类型与版本、浏览器名称与版本 -- **接口限流**:基于 Redis 滑动窗口的按 IP 限流策略,支持按接口独立配置阈值,防刷防滥用 - **操作日志自动记录**:所有写操作(POST / PUT / DELETE)自动捕获请求参数与响应结果,无需手动埋点 - **验证码防刷**:按 IP 频率控制,单 IP 每分钟最多获取 5 次验证码,超限拒绝生成 diff --git a/DjangoAdmin_Flask_EleVue/zh/course.md b/DjangoAdmin_Flask_EleVue/zh/course.md index acaf800..4238824 100644 --- a/DjangoAdmin_Flask_EleVue/zh/course.md +++ b/DjangoAdmin_Flask_EleVue/zh/course.md @@ -23,7 +23,7 @@ |------|----------|------| | Python | 3.12+ | 后端运行环境 | | MySQL | 8.0+ | 数据库 | -| Redis | 3.2+ | 缓存(验证码、JWT黑名单、限流) | +| Redis | 3.2+ | 缓存(验证码、JWT黑名单) | | Node.js | 22+ | 前端构建环境 | | pnpm | 最新版 | 前端包管理器 | | Git | 最新版 | 版本控制 | @@ -130,7 +130,7 @@ python scripts/init_db.py ``` :::tip 温馨提示 -以上命令是推荐的直接执行方式,适用于所有操作系统。文档中出现的 `make` 命令(如 `make db-init`)是 Linux / macOS 系统的便捷别名,Windows 用户直接使用对应的原始命令即可(如 `python scripts/init_db.py`)。 +以上命令是推荐的直接执行方式,适用于所有操作系统。文档中出现的 `make` 命令(如 `make db-migrate`)是 Linux / macOS 系统的便捷别名,Windows 用户直接使用对应的原始命令即可(如 `flask db migrate`)。 ::: 初始化完成后,数据库中包含以下默认数据: @@ -230,7 +230,8 @@ pnpm dev **models.py** — 定义数据模型: ```python -from sqlalchemy import Column, String, Integer, text +from sqlalchemy import text +from src.core.database import db from src.common.base_db import BaseDB from src.common.base_model import BaseModel from config.database import DB_PREFIX @@ -242,11 +243,11 @@ class Position(BaseModel, BaseDB): __table_comment__ = "岗位表" # 岗位名称 - name = Column(String(255), nullable=False, index=True, comment="岗位名称") + name = db.Column(db.String(255), nullable=False, index=True, comment="岗位名称") # 岗位状态:1-在用 2-停用 - status = Column(Integer, default=0, server_default=text('0'), index=True, comment="岗位状态:1-在用 2-停用") + status = db.Column(db.Integer, default=0, server_default=text('0'), index=True, comment="岗位状态:0-停用 1-启用") # 岗位排序 - sort = Column(Integer, default=0, server_default=text('0'), comment="岗位排序") + sort = db.Column(db.Integer, default=0, server_default=text('0'), comment="岗位排序") ``` **schema.py** — 定义表单校验: diff --git a/DjangoAdmin_Flask_EleVue/zh/faq.md b/DjangoAdmin_Flask_EleVue/zh/faq.md index 2c03539..78e4801 100644 --- a/DjangoAdmin_Flask_EleVue/zh/faq.md +++ b/DjangoAdmin_Flask_EleVue/zh/faq.md @@ -63,7 +63,6 @@ | 问题 | 说明 | 详细文档 | |------|------|----------| | IP 地理位置显示"未知" | xdb 文件不存在或 IP 格式问题 | [IP/UA 解析](/zh/volume4/tools/ip-ua) | -| 限流不生效 | RATE_LIMIT_ENABLED 未开启 | [IP/UA 解析](/zh/volume4/tools/ip-ua) | | 富文本 XSS 被过滤 | bleach 清洗规则过严 | [富文本过滤](/zh/volume4/tools/rich-text) | | 文件上传被拒绝 | 扩展名不在白名单 | [上传配置](/zh/volume4/tools/upload-guide) | diff --git a/DjangoAdmin_Flask_EleVue/zh/introduction.md b/DjangoAdmin_Flask_EleVue/zh/introduction.md index 7169ce0..95b39f0 100644 --- a/DjangoAdmin_Flask_EleVue/zh/introduction.md +++ b/DjangoAdmin_Flask_EleVue/zh/introduction.md @@ -31,7 +31,7 @@ + **模块可插拔**:业务模块位于 `src/modules/`,按需增删不影响核心框架 + **多数据库支持**:通过 `DB_DRIVER` 环境变量一键切换 MySQL / PostgreSQL / SQL Server / SQLite / Oracle + **AOP 事务管理**:自定义事务拦截器,业务代码无需手动 commit/rollback -+ **中间件可扩展**:CORS、限流、日志、事务通过中间件链式处理,按需注册 ++ **中间件可扩展**:CORS、日志、事务通过中间件链式处理,按需注册 + **RBAC 权限**:后端装饰器 + 前端指令双重校验,精确到按钮级 + **代码生成器**:根据数据库表结构一键生成前后端 CRUD 代码 @@ -72,9 +72,9 @@ | Django+AntDesign前后端分离版 | 基于 Django、Django ORM、Vue3、AntDesign、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Django_AntdVue) | | Django+NaiveUI前后端分离版 | 基于 Django、Django ORM、Vue3、NaiveUI、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Django_NaiveVue) | | Flask+Layui混编版 | 基于 Flask、Jinja2、Layui 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Flask_Layui) | -| Flask+ElementPlus前后端分离版 | 基于 Flask、SQLAlchemy、Pydantic、Vue3、ElementPlus、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Flask_EleVue) | -| Flask+AntDesign前后端分离版 | 基于 Flask、SQLAlchemy、Pydantic、Vue3、AntDesign、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Flask_AntdVue) | -| Flask+NaiveUI前后端分离版 | 基于 Flask、SQLAlchemy、Pydantic、Vue3、NaiveUI、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Flask_NaiveVue) | +| Flask+ElementPlus前后端分离版 | 基于 Flask、SQLAlchemy、WTForms、Vue3、ElementPlus、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Flask_EleVue) | +| Flask+AntDesign前后端分离版 | 基于 Flask、SQLAlchemy、WTForms、Vue3、AntDesign、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Flask_AntdVue) | +| Flask+NaiveUI前后端分离版 | 基于 Flask、SQLAlchemy、WTForms、Vue3、NaiveUI、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Flask_NaiveVue) | | FastAPI+Layui混编版 | 基于 FastAPI、Jinja2、Layui 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Fastapi_Layui) | | FastAPI+ElementPlus前后端分离版 | 基于 FastAPI、SQLAlchemy、Pydantic、Vue3、ElementPlus、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Fastapi_EleVue) | | FastAPI+AntDesign前后端分离版 | 基于 FastAPI、SQLAlchemy、Pydantic、Vue3、AntDesign、TypeScript、Vite 等技术栈,支持 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等数据库 | [Gitee地址](https://gitee.com/djangoadmin/DjangoAdmin_Fastapi_AntdVue) | diff --git a/DjangoAdmin_Flask_EleVue/zh/struct.md b/DjangoAdmin_Flask_EleVue/zh/struct.md index a0ff6e1..6f286fd 100644 --- a/DjangoAdmin_Flask_EleVue/zh/struct.md +++ b/DjangoAdmin_Flask_EleVue/zh/struct.md @@ -14,20 +14,28 @@ ``` ├── src/ // 后端源码 ├── ui/ // 前端源码(Vue3 + ElementPlus) +├── config/ // 配置文件(base / database / redis / auth) +├── docker/ // Docker 部署配置(Dockerfile / docker-compose.yml) ├── document/ // 项目文档(SQL脚本、迁移日志等) ├── migrations/ // Alembic 数据库迁移脚本 -├── scripts/ // 脚本工具(init_db、migrate_db、seed 等) +├── scripts/ // 脚本工具(setup_db / migrate_db 等) ├── tests/ // 测试代码(contract / unit / integration) -├── deploy/ // 部署配置(nginx.conf、supervisor.conf) ├── wiki/ // VitePress 文档站点 ├── static/ // 静态资源(验证码字体等) +├── uploads/ // 上传文件存储目录 +├── app.py // 后端启动入口 +├── gunicorn.conf.py // Gunicorn 生产环境配置 +├── generator.py // 代码生成器 CLI 入口 +├── config.json // 代码生成器配置 ├── .env.example // 环境变量模板 ├── .env // 环境变量(不提交到版本控制) -├── docker-compose.yml // Docker 编排配置 -├── Dockerfile // 后端镜像构建文件 ├── Makefile // 快捷命令(db-init、migrate-*、db-migrate) +├── pyproject.toml // 项目元数据与工具配置 ├── requirements.txt // Python 依赖清单 -├── alembic.ini // Alembic 配置 +├── requirements-dev.txt // 开发依赖清单 +├── .flake8 // Flake8 代码检查配置 +├── .pre-commit-config.yaml // Pre-commit 钩子配置 +├── .dockerignore // Docker 构建忽略规则 └── README.md // 项目说明 ``` @@ -60,7 +68,7 @@ src/ │ ├── constants/ // 常量定义 │ │ └── common.py // PAGE_SIZE / DEFAULT_PASSWORD 等 │ └── utils/ // 工具函数 -│ ├── request.py // parse_request_body / parse_batch_ids / parse_id_list +│ ├── request.py // parse_request_body / parse_batch_ids / parse_batch_ids │ ├── captcha.py // 验证码生成(Pillow) │ ├── file.py // 文件工具 │ ├── ip.py // IP 地址获取 @@ -141,7 +149,9 @@ ui/src/ │ ├── tool/ // 工具页面(generator / example) │ ├── setting/ // 设置页面(profile / configWeb) │ ├── exception/ // 异常页面(404 / 500) -│ └── about/ // 关于页面 +│ ├── about/ // 关于页面 +│ ├── iframe/ // 内嵌页面 +│ └── redirect/ // 重定向页面 ├── components/ // 公共组件 │ ├── Table/ // BasicTable 表格组件 │ ├── Form/ // BasicForm 表单组件 @@ -157,6 +167,9 @@ ui/src/ │ ├── Select/ // 下拉选择组件 │ ├── CountTo/ // 数字动画组件 │ ├── Authority/ // 权限组件 +│ ├── CheckToken/ // Token 检查组件 +│ ├── Application/ // 应用组件 +│ ├── Websocket/ // WebSocket 组件 │ ├── Lockscreen/ // 锁屏组件 │ ├── Password/ // 密码强度组件 │ ├── Region/ // 区域选择组件 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume1/backend.md b/DjangoAdmin_Flask_EleVue/zh/volume1/backend.md index e85a789..f338e68 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume1/backend.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume1/backend.md @@ -37,16 +37,16 @@ cd djangoadmin │ ├── core/ # 核心模块(数据库、缓存、认证、响应等) │ ├── routes/ # 路由层(HTTP 接口定义) │ ├── modules/ # 业务模块(模型、仓库、服务) -│ ├── middleware/ # 中间件(CORS、限流、日志、操作记录等) -│ ├── common/ # 公共模块(基类、常量、工具函数等) -│ └── utils/ # 工具函数 +│ ├── middleware/ # 中间件(认证、事务、日志、操作记录等) +│ └── common/ # 公共模块(基类、常量、工具函数等) ├── ui/ # 前端源码(独立仓库或子目录) +├── config/ # 配置文件(base / database / redis / auth) +├── docker/ # Docker 部署配置(Dockerfile / docker-compose.yml) ├── scripts/ # 脚本工具(数据库初始化、数据迁移等) ├── migrations/ # Flask-Migrate 数据库迁移文件 ├── tests/ # 测试代码 ├── .env.example # 环境变量示例文件 ├── requirements.txt # Python 依赖清单 -├── docker-compose.yml # Docker 编排文件 └── Makefile # 常用命令集合 ``` @@ -72,7 +72,7 @@ pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ ```bash # 检查 Flask 是否安装成功 python -c "import flask; print(flask.__version__)" -3.1.1 +3.1.3 # 检查 SQLAlchemy 是否安装成功 python -c "import sqlalchemy; print(sqlalchemy.__version__)" @@ -265,14 +265,20 @@ curl http://127.0.0.1:8021/api/v1/captcha | 命令 | 对应命令 | 说明 | |:----|:----|:----| -| `make install` | `pip install -r requirements.txt` | 安装运行时 + 开发依赖 | +| `make install` | `pip install -r requirements.txt` | 安装生产依赖 | +| `make install-dev` | `pip install -r requirements-dev.txt` | 安装开发依赖(含测试/规范工具) | | `make dev` | `python app.py` | 启动开发服务 | -| `make test` | `pytest` | 运行全部测试(默认跳过 integration) | -| `make test-contract` | `pytest tests/contract` | 运行契约快照测试 | -| `make test-unit` | `pytest tests/unit` | 运行单元测试 | -| `make test-integration` | `pytest tests/integration` | 运行集成测试(需真实 MySQL/Redis) | -| `make db-init` | `python scripts/setup_db.py` | 初始化数据库 | -| `make seed-dict` | `python scripts/seed_dict.py` | 初始化业务字典数据 | +| `make lint` | `flake8 + black --check + isort --check-only` | 静态检查 | +| `make format` | `black + isort` | 自动格式化 | +| `make type-check` | `mypy src` | 类型检查 | +| `make test` | `pytest` | 运行全部测试 | +| `make test-cov` | `pytest --cov=src` | 带覆盖率运行测试 | +| `make db-migrate` | `flask db migrate` | 生成迁移脚本 | +| `make db-upgrade` | `flask db upgrade` | 应用迁移到最新 | +| `make db-downgrade` | `flask db downgrade` | 回滚一次迁移 | +| `make docker-up` | `cd docker && docker-compose up -d` | 启动 Docker 服务 | +| `make docker-down` | `cd docker && docker-compose down` | 停止 Docker 服务 | +| `make clean` | 清理 `__pycache__` / `.pyc` / `.pytest_cache` | 清理缓存与临时文件 | ## 常见问题 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume1/database.md b/DjangoAdmin_Flask_EleVue/zh/volume1/database.md index add347d..2e13eb4 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume1/database.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume1/database.md @@ -294,13 +294,13 @@ $ python scripts/setup_db.py --drop-first | 数据库 | DB_DRIVER 值 | 默认端口 | 依赖包 | 脚本目录 | |:----|:----|:----|:----|:----| | MySQL | `mysql` | 3306 | PyMySQL | `document/mysql/` | -| PostgreSQL | `postgresql` | 5432 | psycopg[binary] | `document/postgresql/` | +| PostgreSQL | `postgresql` | 5432 | psycopg2-binary | `document/postgresql/` | | SQL Server | `mssql` | 1433 | pymssql | `document/sqlserver/` | | SQLite | `sqlite` | - | 内置 | `document/sqlite/` | | Oracle | `oracle` | 1521 | oracledb | `document/oracle/` | :::tip 温馨提示 -切换数据库驱动后,需要安装对应的驱动包(如 `pip install psycopg[binary]`),然后按照上述步骤重新初始化数据库。 +切换数据库驱动后,需要安装对应的驱动包(如 `pip install psycopg2-binary`),然后按照上述步骤重新初始化数据库。 ::: ## 种子数据 @@ -380,13 +380,13 @@ flask db stamp head | 命令 | 对应命令 | 说明 | |:----|:----|:----| -| `make db-init` | `python scripts/setup_db.py` | 初始化数据库(建库 + 建表 + 种子数据 + flask db stamp) | -| `make seed-dict` | `python scripts/seed_dict.py` | 初始化业务字典数据(幂等,按 code 判重) | -| `make migrate-revision` | `flask db migrate` | 生成增量迁移脚本 | -| `make migrate-upgrade` | `flask db upgrade` | 应用所有待执行的迁移 | -| `make migrate-stamp` | `flask db stamp head` | 标记当前数据库为最新版本 | -| `make db-migrate ARGS="..."` | `python scripts/migrate_db.py ...` | 跨库数据迁移 | -| `make db-migrate-dry-run ARGS="..."` | `python scripts/migrate_db.py ... --dry-run` | 跨库迁移干跑(只核对计划,不写入目标库) | +| `make db-migrate` | `flask db migrate` | 生成增量迁移脚本 | +| `make db-upgrade` | `flask db upgrade` | 应用迁移到最新 | +| `make db-downgrade` | `flask db downgrade` | 回滚一次迁移 | + +:::tip 数据库初始化 +数据库初始化使用 `python scripts/setup_db.py`(非 Makefile 命令),详见上方「方式二:脚本初始化」章节。跨库数据迁移使用 `python scripts/migrate_db.py`。 +::: ## 数据库备份与恢复 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume1/docker.md b/DjangoAdmin_Flask_EleVue/zh/volume1/docker.md index 596b2d4..c0b6756 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume1/docker.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume1/docker.md @@ -88,7 +88,7 @@ python -c "import secrets; print(secrets.token_urlsafe(48))" ## Docker Compose 配置 -项目的 `docker-compose.yml` 文件定义了后端服务的容器编排: +项目的 `docker/docker-compose.yml` 文件定义了后端服务的容器编排: ```yaml # +====================================================================== diff --git a/DjangoAdmin_Flask_EleVue/zh/volume1/env-vars.md b/DjangoAdmin_Flask_EleVue/zh/volume1/env-vars.md index 0967072..5e521c2 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume1/env-vars.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume1/env-vars.md @@ -17,12 +17,14 @@ | FLASK_HOST | 0.0.0.0 | 应用监听地址。`127.0.0.1` 仅本地访问;容器/生产建议 `0.0.0.0` | | FLASK_PORT | 8021 | 应用监听端口 | | FLASK_APP | app.py | 应用启动文件名 | -| FLASK_ENV | production | 运行环境:`development` / `production` / `testing` | -| FLASK_DEBUG | False | 是否开启调试模式。`True` 时输出详细错误堆栈和 SQL 日志,生产环境务必设为 `False` | +| FLASK_ENV | development | 运行环境:`development` / `production` / `testing` | +| FLASK_DEBUG | True | 是否开启调试模式。`True` 时输出详细错误堆栈和 SQL 日志,生产环境务必设为 `False` | | FLASK_DEMO | False | 是否开启演示模式。`True` 时所有写操作(add/update/delete/status/batchDelete)返回"演示环境,暂无操作权限" | -| FLASK_SECRET_KEY | (空) | Flask 应用密钥,用于 Session 加密和 CSRF 保护,**必填**且至少 16 字符 | -| FLASK_FILE_URL | http://file.flask.elevue | 文件访问域名,用于拼接上传文件的访问 URL。`BaseService._enrich_file_urls()` 会自动为文件字段补此前缀 | -| UPLOAD_ALLOWED_EXTS | .jpg,.jpeg,.png,.gif,...,.zip | 允许上传的文件扩展名,逗号分隔(带点号)。`upload_size_limit_middleware` 按 `Content-Length` 提前拦截超大请求 | +| FLASK_USE_RELOADER | False | 是否禁用自动重启。仅在 `FLASK_DEBUG=True` 时生效 | +| FLASK_SECRET_KEY | `<必填>` | Flask 应用密钥,用于 Session 加密和 CSRF 保护,**必填**且至少 16 字符 | +| FLASK_UPLOAD_DIR | ./uploads | 文件上传存储目录 | +| FLASK_FILE_URL | http://file.flask.elevue | 文件访问域名,用于拼接上传文件的访问 URL | +| UPLOAD_ALLOWED_EXTS | .jpg,.jpeg,.png,.gif,...,.zip | 允许上传的文件扩展名,逗号分隔(带点号) | | UPLOAD_MAX_SIZE_MB | 10 | 上传文件大小上限(MB) | :::tip 安全提示 @@ -40,7 +42,11 @@ | DB_USERNAME | root | 数据库账号(`sqlite` 时忽略) | | DB_PASSWORD | CHANGE_ME_DB_PASSWORD | 数据库密码,**必填**(`sqlite` 除外) | | DB_PREFIX | flask_ | 数据表前缀,所有业务表名自动添加此前缀。模型中通过 `DB_PREFIX + "position"` 拼接表名 | -| DB_DEBUG | False | 是否开启 SQL 调试日志(对应 SQLAlchemy `echo` 参数)。生产环境务必设为 `False` | +| DB_DEBUG | True | 是否开启 SQL 调试日志(对应 SQLAlchemy `echo` 参数)。生产环境务必设为 `False` | +| DB_SSL | False | 是否启用数据库 SSL 加密连接 | +| DB_SSL_CA | (空) | SSL CA 证书路径(验证服务端证书) | +| DB_SSL_CERT | (空) | SSL 客户端证书路径(双向认证) | +| DB_SSL_KEY | (空) | SSL 客户端私钥路径(双向认证) | :::tip 多数据库支持 `DB_DRIVER` 支持别名:`postgres` → `postgresql`、`sqlserver` → `mssql`、`sqlite3` → `sqlite`。切换驱动后需安装对应的 Python 驱动包。 @@ -58,17 +64,32 @@ | REDIS_DECODE_RESPONSES | True | 是否自动解码响应(bytes → str) | :::tip 温馨提示 -Redis 用于存储验证码、JWT 令牌黑名单和登录限流数据,是项目正常运行的必需服务。 +Redis 用于存储验证码、JWT 令牌黑名单和登录锁定数据,是项目正常运行的必需服务。 ::: ## JWT 令牌配置 | 变量名 | 默认值 | 说明 | |:----|:----|:----| -| JWT_SALT | (空) | JWT 签名密钥,**必填**。至少 16 字符强随机值,留空应用会拒绝启动 | -| JWT_EXPIRE_MINUTES | 20 | JWT 令牌过期时间(分钟) | +| JWT_SALT | `<必填>` | JWT 签名密钥,**必填**。至少 16 字符强随机值,留空应用会拒绝启动 | +| JWT_EXPIRE_MINUTES | 20 | 滑动会话空闲超时(分钟),无请求则过期,活跃自动续期 | +| JWT_ABSOLUTE_TIMEOUT_MINUTES | 1440 | 令牌绝对有效期(分钟),防止无限续期,须 ≥ JWT_EXPIRE_MINUTES | | LOGIN_FAIL_MAX | 5 | 登录失败次数上限,超过后锁定账号(防暴力破解) | | LOGIN_LOCK_SECONDS | 300 | 登录锁定时间(秒) | +| PERMISSION_ENABLED | True | 权限校验总开关。`False` 时所有请求放行(开发调试可用) | +| INTERNAL_API_KEY | `<必填>` | 内部调用密钥(定时任务等无用户场景,通过 `X-Scheduler-Key` / `X-Internal-API-Key` 传递) | +| INTERNAL_ALLOWED_IPS | 127.0.0.1 | 内部调用白名单 IP,逗号分隔 | + +## 登录白名单配置 + +| 变量名 | 默认值 | 说明 | +|:----|:----|:----| +| LOGIN_WHITELIST_EXACT | /api/v1/login,/api/v1/captcha,/favicon.ico,/health | 精确匹配白名单,逗号分隔 | +| LOGIN_WHITELIST_PREFIX | /api/v1/job/sendSms,/public,/static,/api/webhook | 前缀匹配白名单,逗号分隔 | +| LOGIN_WHITELIST_WILDCARD | /api/v1/job/*/send*,/api/v1/*/public* | 通配符匹配白名单,支持 `*` 和 `?` | +| LOGIN_WHITELIST_DEBUG | True | 是否启用白名单调试日志 | +| LOGIN_WHITELIST_LOG_ACCESS | True | 白名单路径是否记录访问日志 | +| CAPTCHA_BYPASS_CODE | (空) | 万能验证码,仅 `FLASK_DEMO=True` 时生效 | :::danger 重要提示 `JWT_SALT` 是安全关键配置,必须设置。推荐使用以下命令生成: @@ -101,8 +122,7 @@ python -c "import secrets; print(secrets.token_urlsafe(48))" | 变量名 | 默认值 | 说明 | |:----|:----|:----| | CORS_ALLOWED_ORIGINS | http://localhost:8001 | CORS 允许的源,多个用逗号分隔 | -| TRUSTED_PROXY_COUNT | 0 | 可信代理层数。`0` = 不信任 X-Forwarded-For;部署在 Nginx 等反代后设为 `>=1` | -| TRUSTED_PROXY_IPS | (空) | 可信代理 IP 白名单,逗号分隔 | +| PROXY_NUM | 0 | 可信代理层数。`0` = 不信任 X-Forwarded-For;部署在 Nginx 等反代后设为 `1` | :::tip 温馨提示 本地开发时,`CORS_ALLOWED_ORIGINS` 需包含前端开发服务器地址(默认 `http://localhost:8001`)。如果有多个前端地址,用逗号分隔: @@ -130,31 +150,22 @@ CORS_ALLOWED_ORIGINS=http://localhost:8001,http://192.168.1.100:8001 邮件功能为预留扩展功能,暂未实现。 ::: -## 短信配置 +## 短信配置(阿里云) | 变量名 | 默认值 | 说明 | |:----|:----|:----| -| SMS_URL | (空) | 短信服务 API 地址 | -| SMS_API_KEY | (空) | 短信服务 API 密钥 | -| SMS_SIGN | (空) | 短信签名 | -| SMS_TEMPLATE_ID | (空) | 短信模板 ID | +| SMS_PROVIDER | aliyun | 短信服务商(当前仅支持 aliyun) | +| SMS_ALIYUN_ACCESS_KEY_ID | `<必填>` | 阿里云 AccessKey ID | +| SMS_ALIYUN_ACCESS_KEY_SECRET | `<必填>` | 阿里云 AccessKey Secret | +| SMS_ALIYUN_SIGN_NAME | `<必填>` | 短信签名(需在阿里云控制台审核通过) | +| SMS_ALIYUN_ENDPOINT | dysmsapi.aliyuncs.com | 短信服务接入点 | +| SMS_ALIYUN_CONNECT_TIMEOUT | 10000 | 连接超时(毫秒) | +| SMS_ALIYUN_READ_TIMEOUT | 10000 | 读取超时(毫秒) | :::tip 温馨提示 -短信功能为预留扩展功能,暂未实现。 +短信功能基于阿里云短信服务,需在阿里云控制台开通短信服务并获取 AccessKey。 ::: -## 请求限流配置 - -| 变量名 | 默认值 | 说明 | -|:----|:----|:----| -| RATE_LIMIT_ENABLED | False | 是否启用请求限流。在 `.env.example` 中默认被注释,需手动取消注释并设为 `True` | -| RATE_LIMIT_LIMIT | 100 | 窗口期内最大请求数 | -| RATE_LIMIT_WINDOW_SECONDS | 60 | 限流窗口时间(秒) | -| RATE_LIMIT_KEY_PREFIX | rl: | Redis 限流键前缀 | - -:::tip 温馨提示 -请求限流功能默认关闭。启用后由 `src/middleware/rate_limit.py` 基于 Redis 滑动窗口计数器实现,按客户端 IP 维度统计。超出限制后直接返回 `{"code": 1, "msg": "请求过于频繁"}` 响应,不进入后续中间件链(避免 DoS 攻击时日志表与 DB 连接不降反增)。Redis 不可用时降级放行(fail-open)。生产环境建议启用此功能以防止 API 滥用。 -::: ## 环境变量配置示例 @@ -228,4 +239,4 @@ CORS_ALLOWED_ORIGINS=http://localhost:8001 # 允许跨域访问的前端地址 ## 总结 -本章节汇总了项目所有可配置的环境变量,涵盖基础配置、数据库、Redis、JWT、验证码、跨域、邮件、短信和限流等功能模块。通过合理配置这些变量,可以灵活调整项目的运行参数,适应不同的开发、测试和生产环境需求。建议首次使用时以 `.env.example` 为模板,仅修改必需项,其余保持默认值即可快速启动项目。 +本章节汇总了项目所有可配置的环境变量,涵盖基础配置、数据库、Redis、JWT、验证码、跨域、邮件、短信等功能模块。通过合理配置这些变量,可以灵活调整项目的运行参数,适应不同的开发、测试和生产环境需求。建议首次使用时以 `.env.example` 为模板,仅修改必需项,其余保持默认值即可快速启动项目。 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume1/environment.md b/DjangoAdmin_Flask_EleVue/zh/volume1/environment.md index 5f78255..3a143e6 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume1/environment.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume1/environment.md @@ -199,7 +199,7 @@ SHOW DATABASES; ## 安装 Redis -`Redis` 是必需的缓存服务,用于验证码存储、JWT 令牌黑名单、登录限流等功能。要求 `3.2` 或更高版本。 +`Redis` 是必需的缓存服务,用于验证码存储、JWT 令牌黑名单、登录锁定等功能。要求 `3.2` 或更高版本。 + 下载软件 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume1/frontend.md b/DjangoAdmin_Flask_EleVue/zh/volume1/frontend.md index 30291ac..8796ecc 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume1/frontend.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume1/frontend.md @@ -23,7 +23,7 @@ ui/ │ ├── utils/ # 工具函数 │ ├── views/ # 页面视图 │ ├── App.vue # 根组件 -│ └── main.js # 入口文件 +│ └── main.ts # 入口文件 ├── .env.development # 开发环境变量 ├── .env.production # 生产环境变量 ├── vite.config.ts # Vite 构建配置 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume2/blueprint.md b/DjangoAdmin_Flask_EleVue/zh/volume2/blueprint.md index 1ea7486..3db027b 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume2/blueprint.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume2/blueprint.md @@ -15,8 +15,8 @@ Flask 使用 `Blueprint` 机制实现模块化路由注册。每个业务模块 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 # 参数说明: diff --git a/DjangoAdmin_Flask_EleVue/zh/volume2/code-list.md b/DjangoAdmin_Flask_EleVue/zh/volume2/code-list.md index 36eee21..be7ac1d 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume2/code-list.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume2/code-list.md @@ -14,7 +14,8 @@ 岗位模型定义,包含名称、排序、状态等属性。 """ -from sqlalchemy import Column, String, Integer, text +from sqlalchemy import text +from src.core.database import db from src.common.base_db import BaseDB from src.common.base_model import BaseModel @@ -36,11 +37,11 @@ class Position(BaseModel, BaseDB): # 字段定义 # ============================================================ # 岗位名称 - 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-停用") + name = db.Column(db.String(255), nullable=False, index=True, comment="岗位名称") + # 岗位状态:0-停用 1-启用 + status = db.Column(db.Integer, default=0, server_default=text('0'), index=True, comment="岗位状态:0-停用 1-启用") # 岗位排序 - sort = Column(Integer, default=0, server_default=text('0'), comment="岗位排序") + sort = db.Column(db.Integer, default=0, server_default=text('0'), comment="岗位排序") # ============================================================ # 内置方法 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume2/faq.md b/DjangoAdmin_Flask_EleVue/zh/volume2/faq.md index b75c303..f43ef34 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume2/faq.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume2/faq.md @@ -56,7 +56,7 @@ flask db upgrade ### Q6: 批量删除报 500 错误 -**原因**:`parse_id_list` 函数未正确导入或使用。 +**原因**:`parse_batch_ids` 函数未正确导入或使用。 **解决**:确保正确导入并使用: diff --git a/DjangoAdmin_Flask_EleVue/zh/volume2/model.md b/DjangoAdmin_Flask_EleVue/zh/volume2/model.md index c23362f..7ebcb10 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume2/model.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume2/model.md @@ -10,31 +10,147 @@ src/modules/system/position/models.py ## 完整代码 +以下为 `src/modules/system/position/models.py` 的核心代码(省略版权头): + ```python -from sqlalchemy import Column, String, Integer, text +""" +岗位模型模块 +============================================================= + +提供岗位管理的数据模型定义,用于定义员工岗位信息。 + +企业级特性: + 1. 使用枚举类管理状态常量,确保类型安全 + 2. 状态文本自动转换,便于前端展示 + 3. 便捷的查询方法,提高开发效率 + 4. 继承 BaseModel 和 BaseDB,复用通用功能 + 5. __init__ 由 BaseModel 统一管理,无需重复定义 +""" + +from enum import IntEnum +from typing import Optional, List from src.common.base_db import BaseDB from src.common.base_model import BaseModel -from config.database import DB_PREFIX +from config import DB_PREFIX +from sqlalchemy import text +from src.core.database import db +from src.common.utils.request import like_contains +# ============================================================ +# 岗位状态枚举 +# ============================================================ + +class PositionStatus(IntEnum): + """ + 岗位状态枚举 + + 使用 IntEnum 确保类型安全,避免魔法数字。 + 可直接用于数据库查询和比较。 + """ + DISABLED = 0 # 停用 + ENABLED = 1 # 启用 + + +# ============================================================ +# 岗位模型 +# ============================================================ + class Position(BaseModel, BaseDB): - """岗位模型类""" - # 表名配置 + """ + 岗位模型 + + 用于存储员工岗位信息,如:技术总监、产品经理等。 + 继承自 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) + # 岗位名称:必填字段,最长255字符 + name = db.Column( + db.String(255), + nullable=False, + index=True, + comment="岗位名称" + ) + + # 岗位状态:使用枚举确保类型安全 + # 默认值为停用,创建后需手动启用 + status = db.Column( + db.Integer, + index=True, + default=PositionStatus.DISABLED, + server_default=text('0'), + comment="岗位状态:0-停用 1-启用" + ) + + # 岗位排序:数值越小越靠前 + sort = db.Column( + db.Integer, + default=0, + server_default=text('0'), + comment="岗位排序" + ) + + # ============================================================ + # 注意:__init__ 方法在 BaseModel 中已实现,无需重复定义 + # BaseModel 已处理驼峰转换、字段过滤等逻辑 + # ============================================================ + + # ============================================================ + # 便捷属性 + # ============================================================ + + @property + def status_text(self) -> str: + """获取岗位状态文本:将数字状态转换为可读的中文文本""" + status_map = { + PositionStatus.DISABLED: '停用', + PositionStatus.ENABLED: '启用' + } + return status_map.get(self.status, '未知') + + @property + def is_enabled(self) -> bool: + """判断岗位是否启用""" + return self.status == PositionStatus.ENABLED + + @property + def is_disabled(self) -> bool: + """判断岗位是否停用""" + return self.status == PositionStatus.DISABLED + + # ============================================================ + # 自定义查询方法 + # ============================================================ + + @classmethod + def get_enabled(cls) -> List['Position']: + """获取所有启用的岗位,按排序值升序排列""" + return cls.query.filter( + cls.is_delete == 0, + cls.status == PositionStatus.ENABLED + ).order_by(cls.sort.asc()).all() + + @classmethod + def get_by_name(cls, name: Optional[str]) -> Optional['Position']: + """根据岗位名称精确查询""" + if not name: + return None + return cls.query.filter( + cls.name == name, + cls.is_delete == 0 + ).first() + + def __repr__(self) -> str: + return f'岗位:{self.name}' ``` ## 代码解析 @@ -48,7 +164,34 @@ class Position(BaseModel, BaseDB): | 基类 | 来源 | 提供能力 | |:----|:----|:----| | `BaseModel` | `src/common/base_model.py` | 通用字段:id、create_user、create_time、update_user、update_time、is_delete | -| `BaseDB` | `src/common/base_db.py` | `save()` 持久化方法、`delete()` 删除方法、`to_dict()` 序列化方法 | +| `BaseDB` | `src/common/base_db.py` | `save()` 持久化方法(仅 flush,不 commit)、`to_dict()` 序列化方法 | + +### 状态枚举 + +项目推荐使用 `IntEnum` 管理状态常量,避免魔法数字: + +```python +class PositionStatus(IntEnum): + DISABLED = 0 # 停用 + ENABLED = 1 # 启用 +``` + +| 值 | 含义 | +|:----|:----| +| 0 | 停用 / 禁用 | +| 1 | 启用 / 在用 | + +软删除字段 `is_delete` 由基类提供:`0` = 正常,`1` = 已删除。 + +### 字段定义风格 + +项目使用 `db.Column`(从 `src.core.database` 导入的 `db` 实例),而非直接导入 `Column`: + +```python +from src.core.database import db + +name = db.Column(db.String(255), nullable=False, index=True, comment="岗位名称") +``` ### 表名配置 @@ -56,71 +199,17 @@ class Position(BaseModel, BaseDB): __tablename__ = DB_PREFIX + "position" ``` -`DB_PREFIX` 从 `.env` 读取(默认 `flask_`),最终表名为 `flask_position`。所有业务表都使用此前缀,便于区分框架表与业务表。 - -### 字段定义 - -```python -name = Column(String(255), nullable=False, index=True, comment="岗位名称") -``` - -| 参数 | 说明 | -|:----|:----| -| `String(255)` | 字段类型,VARCHAR(255) | -| `nullable=False` | 非空约束 | -| `index=True` | 创建索引,加速查询 | -| `comment` | 字段注释,同步到数据库 | - -```python -status = Column(Integer, default=0, server_default=text('0'), index=True, comment="岗位状态:1-在用 2-停用") -``` - -`default=0` 是 Python 端默认值,`server_default=text('0')` 是数据库端默认值。两者都设置可确保无论通过 ORM 还是直接 SQL 插入,都能正确赋默认值。 - -### 枚举值约定 - -项目中状态字段统一使用整数枚举: - -| 值 | 含义 | -|:----|:----| -| 1 | 启用 / 在用 | -| 2 | 停用 / 禁用 | - -软删除字段 `is_delete` 由基类提供:`0` = 正常,`1` = 已删除。 - -## 数据表 DDL(参考) - -模型定义对应的 MySQL 建表语句: - -```sql -CREATE TABLE `flask_position` ( - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '岗位名称', - `status` int DEFAULT '0' COMMENT '岗位状态:1-在用 2-停用', - `sort` int DEFAULT '0' COMMENT '岗位排序', - `id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `create_user` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人', - `create_time` datetime DEFAULT (now()) COMMENT '创建时间', - `update_user` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人', - `update_time` datetime DEFAULT (now()) COMMENT '更新时间', - `is_delete` int DEFAULT '0' COMMENT '删除标识:0-正常 1-已删除', - PRIMARY KEY (`id`) USING BTREE, - KEY `idx_flask_position_name` (`name`) USING BTREE, - KEY `idx_flask_position_status` (`status`) USING BTREE, - KEY `idx_flask_position_is_delete` (`is_delete`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='岗位表'; -``` - -:::tip 温馨提示 -实际开发中不需要手写 DDL。模型定义完成后,框架会通过 `Base.metadata.create_all()` 自动建表,或通过 `flask db migrate` 生成迁移脚本。 -::: +`DB_PREFIX` 从 `.env` 读取(默认 `flask_`),最终表名为 `flask_position`。 ## 开发要点 1. **业务字段仅定义差异部分**:通用字段(id、时间、软删除等)由基类自动提供 2. **表名必须加 `DB_PREFIX` 前缀**:保持命名一致性 -3. **合理使用索引**:查询频繁的字段(如 name、status)设置 `index=True` -4. **同时设置 `default` 和 `server_default`**:确保 ORM 和直 SQL 两种场景都能正确赋默认值 +3. **使用 `db.Column` 而非直接导入 `Column`**:保持与项目一致的导入风格 +4. **状态字段使用 IntEnum**:避免魔法数字,提高代码可读性 +5. **同时设置 `default` 和 `server_default`**:确保 ORM 和直 SQL 两种场景都能正确赋默认值 +6. **`__init__` 由 BaseModel 统一管理**:无需在子类中重复定义 ## 总结 -数据模型继承 `BaseModel`(公共字段)和 `BaseDB`(save/delete 方法),表名必须加 `DB_PREFIX` 前缀。查询频繁的字段设置索引,同时设置 `default` 和 `server_default` 确保 ORM 和直 SQL 两种场景都能正确赋默认值。 +数据模型继承 `BaseModel`(公共字段)和 `BaseDB`(save 方法),表名必须加 `DB_PREFIX` 前缀。状态字段使用 `IntEnum` 枚举类管理,字段定义使用 `db.Column` 风格。`__init__` 由 BaseModel 统一处理驼峰转换和字段过滤,子类无需重复定义。 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume2/schema.md b/DjangoAdmin_Flask_EleVue/zh/volume2/schema.md index f532648..8f111d1 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume2/schema.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume2/schema.md @@ -10,115 +10,173 @@ src/modules/system/position/schema.py ## 完整代码 +以下为 `src/modules/system/position/schema.py` 的核心代码(省略版权头): + ```python -from wtforms import StringField, IntegerField +""" +岗位表单验证模块 +============================================================= + +提供岗位相关的表单验证功能,包括: +- 岗位添加/更新表单验证 +- 岗位状态设置表单验证 +""" + +from wtforms import ValidationError, StringField, IntegerField from wtforms.validators import DataRequired, Length, NumberRange, Optional from src.common.forms.base_form import BaseForm +from src.modules.system.position.repository import PositionRepository # ============================================================ -# 岗位表单类 +# 岗位表单验证(添加和更新共用) # ============================================================ + 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之间') - ]) + """ + 岗位表单验证类 + + 用于添加和更新岗位时的数据验证。 + 添加时 id 为 None,更新时 id 必须有值。 + """ + + # 岗位ID:更新时必填,添加时可选 + id = IntegerField( + label='岗位ID', + validators=[ + Optional(), # 允许为空(添加时) + NumberRange(min=1, message='岗位ID必须大于0') + ], + description='岗位唯一标识,更新时必填,添加时自动生成' + ) + + # 岗位名称:必填,最长150字符,不可重复 + name = StringField( + label='岗位名称', + validators=[ + DataRequired(message='岗位名称不能为空'), + Length(max=150, message='岗位名称长度不得超过150个字符') + ], + description='岗位显示名称,如:技术总监、产品经理' + ) + + # 岗位状态:必填,1-在用,2-停用 + status = IntegerField( + label='岗位状态', + validators=[ + DataRequired(message='岗位状态不能为空'), + NumberRange(min=1, max=2, message='岗位状态值在1~2之间') + ], + description='岗位状态:1-在用,2-停用' + ) + + # 岗位排序:可选,0-99999之间的整数 + sort = IntegerField( + label='岗位排序', + validators=[ + Optional(), + NumberRange(min=0, max=99999, message='岗位排序值在0~99999之间') + ], + description='排序值,数值越小越靠前' + ) + + # ============================================================ + # 自定义验证方法 + # ============================================================ + + def validate_name(self, field): + """ + 自定义验证:岗位名称不能重复 + + 验证逻辑: + 1. 添加时:检查数据库中是否存在同名记录 + 2. 更新时:排除当前记录自身,检查其他记录是否同名 + """ + if not field.data: + return + + # 获取当前记录的ID(更新时存在,添加时为None) + current_id = self.id.data if self.id.data else None + + # 唯一性校验(软删过滤 + 更新时排除自身) + if PositionRepository.exists_by({'name': field.data}, exclude_id=current_id): + raise ValidationError("岗位名称已存在") # ============================================================ -# 设置状态表单类 +# 状态设置表单 # ============================================================ + 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') - ]) + """岗位状态设置表单验证类""" + + id = IntegerField( + label='岗位ID', + validators=[ + DataRequired(message='岗位ID不能为空'), + NumberRange(min=1, message='岗位ID必须大于0') + ], + description='要修改状态的岗位ID' + ) + + status = IntegerField( + label='岗位状态', + validators=[ + DataRequired(message='岗位状态不能为空'), + NumberRange(min=1, max=2, message='岗位状态值在1~2之间') + ], + description='目标状态:1-在用,2-停用' + ) ``` ## 代码解析 ### 基类选择 -| 基类 | 用途 | 包含字段 | +| 基类 | 用途 | 说明 | |:----|:----|:----| -| `BaseForm` | 创建/编辑通用表单 | 自带可选 `id` 字段(`Optional`),编辑时传入 | -| 特定操作表单 | 无额外字段,按需定义 | +| `BaseForm` | 创建/编辑通用表单 | 继承 `flask_wtf.FlaskForm`,支持 ArrayField 处理 | +| `PositionStatusForm` | 状态更新专用 | 只包含 `id` 和 `status` 两个字段 | -创建和编辑共用 `PositionForm`:创建时不传 `id`,编辑时传入 `id`。`BaseForm` 的 `id` 字段定义为 `IntegerField('ID', validators=[Optional()])`,创建时可省略。 +### id 字段 + +创建和编辑共用 `PositionForm`:创建时不传 `id`,编辑时传入 `id`。`id` 字段定义为 `Optional()`,创建时可省略: + +```python +id = IntegerField( + label='岗位ID', + validators=[Optional(), NumberRange(min=1, message='岗位ID必须大于0')], + description='岗位唯一标识,更新时必填,添加时自动生成' +) +``` ### Validator 约束 -```python -name = StringField('岗位名称', validators=[ - DataRequired(message='岗位名称不能为空'), - Length(min=1, max=150, message='岗位名称长度必须在1-150个字符之间') -]) -``` +| Validator | 含义 | 示例 | +|:----|:----|:----| +| `DataRequired` | 必填字段,不允许空值 | `DataRequired(message='岗位名称不能为空')` | +| `Length(max=150)` | 字符串长度限制 | `Length(max=150, message='...')` | +| `NumberRange(min=1, max=2)` | 数值范围限制 | `NumberRange(min=1, max=2, message='...')` | +| `Optional` | 可选字段 | `Optional()` | -| Validator | 含义 | -|:----|:----| -| `DataRequired` | 必填字段,不允许空值 | -| `Length(min=1, max=150)` | 字符串长度限制 | -| `NumberRange(min=1, max=2)` | 数值范围限制 | -| `Optional` | 可选字段 | +### 自定义校验器 + +通过定义 `validate_{field_name}` 方法实现自定义校验: ```python -status = IntegerField('岗位状态', validators=[ - DataRequired(message='岗位状态不能为空'), - NumberRange(min=1, max=2, message='岗位状态必须是1或2') -]) +def validate_name(self, field): + """自定义验证:岗位名称不能重复""" + if not field.data: + return + current_id = self.id.data if self.id.data else None + if PositionRepository.exists_by({'name': field.data}, exclude_id=current_id): + raise ValidationError("岗位名称已存在") ``` -| 参数 | 含义 | -|:----|:----| -| `min=1` | 最小值(大于等于) | -| `max=2` | 最大值(小于等于) | -| `message` | 校验失败时的错误信息 | - -```python -sort = IntegerField('岗位排序', validators=[ - DataRequired(message='岗位排序不能为空'), - NumberRange(min=0, max=99999, message='岗位排序必须在0-99999之间') -]) -``` - -排序字段允许 0 到 99999 的范围。 - -### 状态更新单独建类 - -```python -# ============================================================ -# 设置状态表单类 -# ============================================================ -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') - ]) -``` - -状态更新接口只需要 `id` 和 `status` 两个字段,与创建/编辑表单分开定义,接口语义更清晰。 +- 方法名必须为 `validate_{field_name}` +- 校验失败抛出 `ValidationError(错误消息)` +- 唯一性校验通过 `Repository.exists_by(filters, exclude_id)` 实现,自动过滤软删除记录 ## 如何在 Router 中使用 @@ -141,7 +199,7 @@ def add(): return R.ok(msg="添加成功") ``` -`parse_request_body` 函数自动解析请求体 JSON。校验失败时,Service 层抛出 `BizError`,由异常处理器自动返回标准错误响应: +校验失败时,由框架自动返回标准错误响应: ```json { @@ -151,36 +209,15 @@ def add(): } ``` -## 自定义校验器 - -如果需要自定义校验逻辑,可以在表单类中定义 `validate_` 开头的方法: - -```python -class PositionForm(BaseForm): - """岗位创建/编辑表单""" - name = StringField('岗位名称', validators=[ - DataRequired(message='岗位名称不能为空'), - Length(max=150, message='岗位名称长度不得超过150个字符') - ]) - - def validate_name(self, field): - """自定义校验:检查岗位名称是否已存在""" - from src.modules.system.position.repository import PositionRepository - if not field.data: - return - current_id = self.id.data if self.id.data else None - if PositionRepository.exists_by({'name': field.data}, exclude_id=current_id): - raise ValidationError('岗位名称已存在') -``` - ## 开发要点 -1. **创建/编辑共用一个表单类**:继承 `BaseForm`,利用其内置的可选 `id` 字段 -2. **特殊操作单独建类**:如状态更新、排序调整等,只包含必要的字段 +1. **创建/编辑共用一个表单类**:继承 `BaseForm`,显式定义 `id` 字段(`Optional`) +2. **特殊操作单独建类**:如状态更新,只包含必要的字段 3. **合理设置 Validator**:`DataRequired` / `Length` / `NumberRange` 等,让框架自动校验 4. **message 必填**:用于返回给前端的错误信息,方便调试 5. **唯一性校验**:通过自定义 `validate_` 方法实现,使用 Repository 层的 `exists_by(filters, exclude_id)` 方法 +6. **可选字段用 `Optional()`**:如 `sort` 排序字段,不传时使用数据库默认值 ## 总结 -WTForms Schema 通过 `BaseForm` 继承公共字段(id 等),使用 `validators` 声明约束,自定义 `validate_` 方法实现唯一性校验。唯一性校验使用 `Repository.exists_by({'field': value}, exclude_id=current_id)` 方法,自动过滤软删除记录并支持排除自身。特殊操作单独建类,`message` 必填用于返回给前端的错误信息。 +WTForms Schema 通过 `BaseForm` 继承公共能力,使用 `validators` 声明约束,自定义 `validate_` 方法实现唯一性校验。唯一性校验使用 `Repository.exists_by({'field': value}, exclude_id=current_id)` 方法,自动过滤软删除记录并支持排除自身。特殊操作单独建类,`message` 必填用于返回给前端的错误信息。 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume3/app-factory.md b/DjangoAdmin_Flask_EleVue/zh/volume3/app-factory.md index 2f5b6b8..0e06108 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume3/app-factory.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume3/app-factory.md @@ -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) # 全局异常处理 ``` 中间件执行顺序(请求进入时): diff --git a/DjangoAdmin_Flask_EleVue/zh/volume3/base-classes.md b/DjangoAdmin_Flask_EleVue/zh/volume3/base-classes.md index dda0d6a..f562831 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume3/base-classes.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume3/base-classes.md @@ -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()` 响应。 +::: ### 钩子方法 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume3/cache.md b/DjangoAdmin_Flask_EleVue/zh/volume3/cache.md index f11f970..3790495 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume3/cache.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume3/cache.md @@ -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 分钟) ``` diff --git a/DjangoAdmin_Flask_EleVue/zh/volume3/directory.md b/DjangoAdmin_Flask_EleVue/zh/volume3/directory.md index 68c7da5..4877c33 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume3/directory.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume3/directory.md @@ -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 解析 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume3/extensibility.md b/DjangoAdmin_Flask_EleVue/zh/volume3/extensibility.md index 5843ff1..cb2a468 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume3/extensibility.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume3/extensibility.md @@ -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) ``` @@ -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` 最后注册 → 最内层(最后处理请求)。 ::: ## 四、装饰器扩展 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume3/flask-g-object.md b/DjangoAdmin_Flask_EleVue/zh/volume3/flask-g-object.md index 6ef2d21..c139e0a 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume3/flask-g-object.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume3/flask-g-object.md @@ -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(): diff --git a/DjangoAdmin_Flask_EleVue/zh/volume3/layer-arch.md b/DjangoAdmin_Flask_EleVue/zh/volume3/layer-arch.md index 0e9f9e3..c2c4769 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume3/layer-arch.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume3/layer-arch.md @@ -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 装饰器顺序 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume3/overview.md b/DjangoAdmin_Flask_EleVue/zh/volume3/overview.md index 2abbd30..75882be 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume3/overview.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume3/overview.md @@ -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) @@ -249,4 +249,4 @@ def create_app(): ## 总结 -项目采用经典的前后端分离 + 三层架构设计,通过中间件链实现横切关注点(认证、限流、日志、事务)的统一处理,通过基类模板方法消除重复 CRUD 代码,通过 WTForms 实现请求参数的声明式校验。整体架构在保持简洁的同时,具备良好的可扩展性和可维护性,适合企业级后台管理系统的快速开发与长期演进。 +项目采用经典的前后端分离 + 三层架构设计,通过中间件链实现横切关注点(认证、日志、事务)的统一处理,通过基类模板方法消除重复 CRUD 代码,通过 WTForms 实现请求参数的声明式校验。整体架构在保持简洁的同时,具备良好的可扩展性和可维护性,适合企业级后台管理系统的快速开发与长期演进。 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume3/request-flow.md b/DjangoAdmin_Flask_EleVue/zh/volume3/request-flow.md index 33b1711..130678c 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume3/request-flow.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume3/request-flow.md @@ -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 负责参数校验,三层架构负责业务逻辑与数据访问。这种分层设计使得每一层职责单一、可独立测试,系统在高并发场景下仍能保持稳定的性能和可靠的事务一致性。 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume3/security.md b/DjangoAdmin_Flask_EleVue/zh/volume3/security.md index 2d34178..26bbcf1 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume3/security.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume3/security.md @@ -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` 环境变量配置上限 +- 前端配合 `el-upload` 组件的 `beforeUpload` 钩子做前置校验 ## 四、数据层防护 @@ -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 启用等关键安全项。 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume3/tech-stack.md b/DjangoAdmin_Flask_EleVue/zh/volume3/tech-stack.md index c877a10..1c81b16 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume3/tech-stack.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume3/tech-stack.md @@ -72,21 +72,20 @@ SQLAlchemy 是 Python 生态中最成熟、功能最完备的 ORM 框架,2.0 | **ElementPlus** | 2.x | Vue3 生态最成熟的 UI 组件库,企业级后台管理系统首选 | | **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()` | diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/business/user.md b/DjangoAdmin_Flask_EleVue/zh/volume4/business/user.md index e1131c7..f9891d6 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/business/user.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/business/user.md @@ -1,5 +1,5 @@ :::tip 用户管理 -用户管理是系统的核心模块,属于复杂业务模块。包含用户 CRUD、角色关联、密码加密、行政区划、Excel 导入导出等功能。与简单 CRUD 模块不同,用户管理使用自定义 service 函数而非继承 BaseService 的类方法。 +用户管理是系统的核心模块,属于复杂业务模块。包含用户 CRUD、角色关联、密码加密、行政区划、Excel 导入导出等功能。用户管理继承 `BaseService` 获得标准 CRUD 能力,并通过覆写钩子方法实现头像保存、密码加密、角色关联等定制逻辑。 ::: ## 模块结构 @@ -237,7 +237,7 @@ def _enrich_user_records(records): role_map = user_role.get_user_roles_batch(user_ids) for data in records: - data["genderName"] = gender_map.get(str(data.get("gender"))) + data["genderText"] = gender_map.get(str(data.get("gender"))) data["deptName"] = dept_map.get(data.get("deptId")) data["levelName"] = level_map.get(data.get("levelId")) data["positionName"] = position_map.get(data.get("positionId")) diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/core/auth-flow.md b/DjangoAdmin_Flask_EleVue/zh/volume4/core/auth-flow.md index 9a34545..a2ba2bd 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/core/auth-flow.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/core/auth-flow.md @@ -117,15 +117,13 @@ Content-Type: application/json "code": 0, "msg": "登录成功", "data": { - "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", - "must_change_password": false + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." }, "ok": true } ``` - `access_token`:JWT 令牌,后续请求需携带 -- `must_change_password`:是否需要修改密码(检测到默认密码时为 `true`) ### 失败响应 @@ -134,10 +132,10 @@ Content-Type: application/json {"code": 1, "msg": "验证码不正确", "data": null, "ok": false} // 账号或密码错误 -{"code": 1, "msg": "用户名或密码不正确", "data": null, "ok": false} +{"code": 1, "msg": "用户不存在", "data": null, "ok": false} // 账号被锁定 -{"code": 1, "msg": "登录失败次数过多,请280秒后再试", "data": null, "ok": false} +{"code": 1, "msg": "登录失败次数过多,请5分钟后再试", "data": null, "ok": false} ``` ### 登录流程详解 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/core/cors.md b/DjangoAdmin_Flask_EleVue/zh/volume4/core/cors.md index 761b989..e17be84 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/core/cors.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/core/cors.md @@ -1,96 +1,118 @@ -:::tip CORS跨域 -CORS(Cross-Origin Resource Sharing)跨域资源共享是前后端分离架构必须处理的问题。通过 `CORS_ALLOWED_ORIGINS` 环境变量配置允许的跨域源。中间件位于 `src/middleware/cors.py`。 +# 跨域资源共享(CORS) + +:::tip CORS 跨域 +CORS(Cross-Origin Resource Sharing)跨域资源共享是前后端分离架构必须处理的问题。项目通过 `flask-cors` 扩展实现 CORS 支持,配置入口在 `src/app.py` 的 `create_app()` 中,通过 `CORS_ALLOWED_ORIGINS` 环境变量配置允许的跨域源。 ::: ## 配置项 | 环境变量 | 默认值 | 说明 | |---------|--------|------| -| `CORS_ALLOWED_ORIGINS` | 空(允许所有源) | 允许的跨域源(逗号分隔) | -| `TRUSTED_PROXY_COUNT` | `0` | 可信代理层数 | -| `TRUSTED_PROXY_IPS` | 空 | 可信代理 IP 白名单(逗号分隔) | +| `CORS_ALLOWED_ORIGINS` | `http://localhost:8001` | 允许的跨域源(逗号分隔),留空则允许所有源 | +| `PROXY_NUM` | `0` | 可信代理层数。`0` = 不信任 X-Forwarded-For;部署在 Nginx 后设为 `1` | + +## 实现原理 + +CORS 配置位于 `src/app.py` 的 `create_app()` 中: + +```python +# src/app.py +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 + ) +``` + +- 配置了 `CORS_ALLOWED_ORIGINS` 时:仅允许指定源,支持携带凭证(Cookie 等) +- 未配置时:允许所有源,但不携带凭证 ## 配置示例 ```bash # .env -# 允许所有源(开发环境) -CORS_ALLOWED_ORIGINS= +# 开发环境:允许前端开发服务器 +CORS_ALLOWED_ORIGINS=http://localhost:8001 -# 允许指定源(生产环境) +# 生产环境:允许指定域名(多个用逗号分隔) CORS_ALLOWED_ORIGINS=https://admin.example.com,https://www.example.com - -# 信任一层反向代理(Nginx) -TRUSTED_PROXY_COUNT=1 -TRUSTED_PROXY_IPS=127.0.0.1 ``` :::warning 生产环境 生产环境务必配置 `CORS_ALLOWED_ORIGINS` 为具体域名,避免允许所有源带来的安全风险。 ::: -## CORS 中间件注册 +## 可信代理配置 -Flask 使用 `flask-cors` 扩展实现 CORS 支持: +当应用部署在 Nginx 等反向代理后,需要配置代理信任以获取真实客户端 IP: ```python -# src/middleware/cors.py -from flask_cors import CORS -from config.app import CORS_ALLOWED_ORIGINS +# src/app.py +from werkzeug.middleware.proxy_fix import ProxyFix -def register_cors(app): - if CORS_ALLOWED_ORIGINS: - origins = [o.strip() for o in CORS_ALLOWED_ORIGINS.split(',') if o.strip()] - CORS(app, origins=origins, supports_credentials=True) - else: - # 未配置时允许所有源但不携带凭证 - CORS(app, origins="*") +# ============================================================ +# 可信代理配置(反向代理环境下获取真实客户端IP) +# ============================================================ +proxy_num = int(os.getenv('PROXY_NUM', 0)) +if proxy_num > 0: + app.wsgi_app = ProxyFix(app.wsgi_app, x_for=proxy_num, x_proto=proxy_num) ``` -## 中间件顺序 +`PROXY_NUM` 说明: -CORS 中间件在 `src/app.py` 中注册为最外层: +| 值 | 说明 | +|----|------| +| `0` | 不信任 X-Forwarded-For,使用 request.remote_addr | +| `1` | 信任一层反向代理(如 Nginx) | +| `2` | 信任两层代理 | + +:::tip 安全提示 +仅当应用部署在反向代理后才设置 `PROXY_NUM>=1`。如果应用被直接暴露(如 Docker bridge 映射端口),设置代理信任会导致客户端伪造 XFF 绕过安全校验。 +::: + +## 中间件注册顺序 + +CORS 在 `src/app.py` 中最先注册,确保所有响应都带 CORS 头: ```python -# 最先注册→最外层:确保限流等直接返回的响应也带 CORS 头 -register_cors(app) +# src/app.py - create_app() 中间件注册顺序 +# 1. CORS 配置(最先注册→最外层) +# 2. ProxyFix 可信代理 +# 3. register_operation_log(app) # 操作日志 +# 4. register_transaction(app) # AOP 事务 +# 5. register_v1(app) # 路由注册 +# 6. register_extends(app) # 扩展初始化 +# 7. register_middleware(app) # 认证中间件 +# 8. register_error_handlers(app) # 异常处理 ``` 中间件执行顺序(从外到内): ``` -CORS → 限流 → 上传体积限制 → 操作日志 → DB会话 → Redis → 业务逻辑 +CORS → 操作日志 → AOP 事务 → 认证 → 路由 → 业务逻辑 ``` -## 代理头处理 - -当应用部署在 Nginx 等反向代理后,需要配置代理信任: - -```bash -# 信任一层代理 -TRUSTED_PROXY_COUNT=1 -TRUSTED_PROXY_IPS=127.0.0.1 -``` - -`TRUSTED_PROXY_COUNT` 说明: - -| 值 | 说明 | -|----|------| -| `0` | 不信任 X-Forwarded-For,使用 request.remote_addr | -| `1` | 信任一层反向代理 | -| `2` | 信任两层代理 | - -:::tip 安全提示 -仅当应用部署在反向代理后才设置 `TRUSTED_PROXY_COUNT>=1`。如果应用被直接暴露(如 Docker bridge 映射端口),设置代理信任会导致客户端伪造 XFF 绕过限流。 -::: - ## Flask-CORS 配置选项 ```python from flask_cors import CORS -CORS(app, +CORS(app, origins=["https://admin.example.com"], # 允许的源 methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], # 允许的方法 allow_headers=["Content-Type", "Authorization"], # 允许的头 @@ -107,14 +129,24 @@ CORS(app, | `supports_credentials` | 允许携带凭证 | `False` | | `max_age` | 预检请求缓存时间 | `None` | +## 常见问题 + ++ 跨域请求被拒绝 + +``` +1. 检查 .env 中 CORS_ALLOWED_ORIGINS 是否包含前端地址 +2. 检查前端请求的 Origin 头是否与配置一致 +3. 生产环境检查 Nginx 是否正确传递 Origin 头 +``` + ++ 携带凭证(Cookie)失败 + +``` +1. CORS_ALLOWED_ORIGINS 必须配置具体域名,不能用 * +2. 前端请求需设置 withCredentials: true +3. 确保 supports_credentials=True +``` + ## 总结 -CORS 跨域配置具备以下特点: - -``` -1. 环境变量配置:通过 CORS_ALLOWED_ORIGINS 灵活配置允许的源 -2. 开发/生产分离:开发环境允许所有源,生产环境指定域名 -3. 代理头支持:TRUSTED_PROXY_COUNT 配置代理信任层数 -4. 安全默认:未配置时允许所有源但不携带凭证 -5. 最外层注册:确保所有响应都带 CORS 头 -``` +CORS 跨域配置通过 `flask-cors` 扩展在 `src/app.py` 中实现,通过 `CORS_ALLOWED_ORIGINS` 环境变量灵活配置允许的源。开发环境允许前端开发服务器(`http://localhost:8001`),生产环境应配置具体域名。反向代理场景通过 `PROXY_NUM` 配置代理信任层数,确保获取真实客户端 IP。 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/core/demo-mode.md b/DjangoAdmin_Flask_EleVue/zh/volume4/core/demo-mode.md index 98aae5f..769e2ea 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/core/demo-mode.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/core/demo-mode.md @@ -40,7 +40,7 @@ def add(): ```python 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 之后、路由函数之前""" diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/core/exception.md b/DjangoAdmin_Flask_EleVue/zh/volume4/core/exception.md index 665034d..9d9bd15 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/core/exception.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/core/exception.md @@ -7,7 +7,7 @@ `src/core/response.py` 提供统一的响应生成函数: ```python -from src.core.response import R +from src.core import response as R # 成功响应 R.ok(data={"id": 1}, msg="操作成功") diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/core/idempotent.md b/DjangoAdmin_Flask_EleVue/zh/volume4/core/idempotent.md index 1774a6d..23c2af4 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/core/idempotent.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/core/idempotent.md @@ -9,10 +9,10 @@ 前端提供 `useLockFn` 钩子函数,实现请求级别的防重复提交: ```javascript -import { useLockFn } from '@/utils/lock' +import { useLockFn } from '@/utils/useLockFn' // 包装提交函数,防止重复点击 -const handleSubmit = useLockFn(async () => { +const { lockFn: handleSubmit } = useLockFn(async () => { await axios.post('/api/v1/level/add', formData) ElMessage.success('添加成功') }) @@ -21,17 +21,20 @@ const handleSubmit = useLockFn(async () => { `useLockFn` 原理: ```javascript -function useLockFn(fn) { - let isLocked = false - return async (...args) => { - if (isLocked) return - isLocked = true +import { ref } from 'vue' + +export function useLockFn(fn) { + const isLock = ref(false) + const lockFn = async (...args) => { + if (isLock.value) return + isLock.value = true try { - return await fn(...args) + await fn(...args) } finally { - isLocked = false + isLock.value = false } } + return { isLock, lockFn } } ``` diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/core/jwt.md b/DjangoAdmin_Flask_EleVue/zh/volume4/core/jwt.md index 87b1f87..ea3a013 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/core/jwt.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/core/jwt.md @@ -214,7 +214,7 @@ def login(data): return R.ok(msg="登录成功", data={ "access_token": access_token, - "must_change_password": is_default_pwd + "access_token": access_token }) ``` diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/core/operation-log.md b/DjangoAdmin_Flask_EleVue/zh/volume4/core/operation-log.md index 0080e49..d9d212f 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/core/operation-log.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/core/operation-log.md @@ -17,50 +17,42 @@ 以下路径不记录操作日志: ```python -_EXCLUDE_PATHS = ( - '/api/v1/login', - '/api/v1/logout', - '/api/v1/captcha', - '/api/v1/upload', - '/api/v1/loginlog', - '/api/v1/operationlog', - '/api/v1/index/menu', - '/api/v1/index/user', -) +# src/middleware/operation_log.py +_EXCLUDED_PATH_SEGMENTS = ("/login", "/captcha", "/logout", "/sendSms") ``` +排除逻辑使用 `endswith` 匹配路径末段,而非精确匹配完整路径。 + ## 操作类型自动推断 -中间件根据 URL 路径末段自动推断操作类型,映射表定义在 `_TYPE_MAP` 中: +中间件根据 HTTP 方法和 URL 路径自动推断操作类型: ```python -_TYPE_MAP = { - 'add': 1, # 新增 - 'update': 2, # 修改 - 'delete': 3, # 删除 - 'batchdelete': 4, # 批量删除 - 'status': 5, # 状态设置 - 'resetpwd': 6, # 重置密码 - 'updatepassword': 7, # 修改密码 - 'import': 8, # 导入 - 'export': 9, # 导出 - 'kickout': 10, # 强退 - 'generate': 11, # 生成代码 - 'clear': 12, # 清空数据 - 'auth': 13, # 授权 - 'refresh': 14, # 缓存 - 'openaccount': 15, # 开账号 +# src/middleware/operation_log.py + +# HTTP 方法 → 日志类型 +_METHOD_LOG_TYPE = { + "POST": 3, # 新增 + "PUT": 4, # 修改 + "DELETE": 5, # 删除 } -def _infer_type(request) -> int: - """从 URL 路径末段推断操作类型,匹配不到返回 0(其它)""" - path = request.path.rstrip('/') - last_segment = path.split('/')[-1].lower() - # 跳过纯数字路径参数(如 /delete/123 → 取 delete) - if last_segment.isdigit(): - parts = path.split('/') - last_segment = parts[-2].lower() if len(parts) >= 2 else '' - return _TYPE_MAP.get(last_segment, 0) +# URL 路径关键字 → 日志类型(优先级高于方法) +_PATH_ACTION_TYPE = { + "status": 4, # 状态变更 + "import": 3, # 导入 + "export": 5, # 导出 + "batchDelete": 5, # 批量删除 +} + +def _infer_log_type(request) -> int: + """从 HTTP 方法和 URL 路径推断操作类型""" + # 先按路径关键字匹配 + for keyword, log_type in _PATH_ACTION_TYPE.items(): + if keyword in request.path: + return log_type + # 再按 HTTP 方法匹配 + return _METHOD_LOG_TYPE.get(request.method, 18) # 18=其它 ``` | 路径末段 | 操作类型 | 类型ID | @@ -136,7 +128,7 @@ def register_operation_log(app): return response # 排除特定路径 - if request.path in _EXCLUDE_PATHS: + if any(seg in request.path for seg in _EXCLUDED_PATH_SEGMENTS): return response # 计算耗时 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/core/rate-limit.md b/DjangoAdmin_Flask_EleVue/zh/volume4/core/rate-limit.md deleted file mode 100644 index 43f44d3..0000000 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/core/rate-limit.md +++ /dev/null @@ -1,145 +0,0 @@ -:::tip 请求限流 -请求限流中间件基于 Redis 实现 IP 级别的滑动窗口限流,防止恶意请求和 DoS 攻击。默认关闭,可通过环境变量开启。源码位于 `src/middleware/rate_limit.py`,滑动窗口 Lua 脚本位于 `src/core/redis_client.py`。 -::: - -## 配置项 - -| 环境变量 | 默认值 | 说明 | -|---------|--------|------| -| `RATE_LIMIT_ENABLED` | `False` | 是否启用限流 | -| `RATE_LIMIT_LIMIT` | `100` | 单窗口内每个 IP 最大请求数 | -| `RATE_LIMIT_WINDOW_SECONDS` | `60` | 统计窗口时长(秒) | -| `RATE_LIMIT_KEY_PREFIX` | `rl:` | Redis 计数 key 前缀 | - -:::warning 生产环境建议 -生产环境建议开启限流(`RATE_LIMIT_ENABLED=True`),根据实际流量调整 `RATE_LIMIT_LIMIT` 和 `RATE_LIMIT_WINDOW_SECONDS`。 -::: - -## 工作原理 - -``` -1. 请求进入 → 检查 RATE_LIMIT_ENABLED 开关 -2. 获取客户端 IP(支持 X-Forwarded-For 代理头) -3. 使用滑动窗口算法计算当前窗口请求数 -4. 超过限制 → 返回 {"code":1, "msg":"请求过于频繁,请 X 秒后再试"} -5. 未超过 → 放行请求 -``` - -## 滑动窗口算法 - -采用滑动窗口计数器,按当前窗口与上一窗口的计数时间加权,消除固定窗口边界处的 2x 突发问题。 - -### 中间件实现 - -```python -# src/middleware/rate_limit.py -from flask import request, g -import time - -def register_rate_limit(app): - """注册限流中间件""" - - @app.before_request - def rate_limit(): - """按客户端 IP 做滑动窗口限流,超限直接返回统一 200 + code 响应""" - from flask import current_app - - if not current_app.config.get('RATE_LIMIT_ENABLED'): - return - - # CORS 预检请求不计数,避免浏览器预检占用限流额度 - if request.method == 'OPTIONS': - return - - # Redis 未就绪时降级放行 - redis = getattr(g, 'redis', None) - if redis is None: - return - - client_ip = get_client_ip() - now = time.time() - window_index = int(now // RATE_LIMIT_WINDOW_SECONDS) - current_key = f"{RATE_LIMIT_KEY_PREFIX}{client_ip}:{window_index}" - prev_key = f"{RATE_LIMIT_KEY_PREFIX}{client_ip}:{window_index - 1}" - - # 原子滑动窗口计数 - try: - count = sliding_window_incr( - redis, current_key, prev_key, RATE_LIMIT_WINDOW_SECONDS, window_index, now) - except Exception as e: - app.logger.warning(f"限流计数异常,降级放行: {e}") - return - - # 直接返回而非抛异常 - if count > RATE_LIMIT_LIMIT: - try: - ttl = redis.ttl(current_key) - except Exception: - ttl = RATE_LIMIT_WINDOW_SECONDS - from src.core.response import R - return R.failed(msg=f"请求过于频繁,请 {ttl} 秒后再试") -``` - -### Lua 脚本 - -滑动窗口的核心在 `src/core/redis_client.py` 中的 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 -""" - -def sliding_window_incr(redis, current_key, prev_key, - window_seconds, window_index, now) -> float: - """滑动窗口限流计数:返回当前滑动窗口内的加权请求数(近似精确)""" - return redis.eval( - _SLIDING_WINDOW_LUA, 2, current_key, prev_key, - window_seconds, window_index, now) -``` - -算法原理:当前窗口计数 `cur` + 上一窗口计数 `prev` 按时间比例加权(`prev * (1 - f)`),窗口边界处请求数平滑过渡,消除固定窗口在边界整体重置导致的 2x 突发。 - -## 降级策略 - -``` -1. Redis 未连接:直接放行,不阻塞请求 -2. Redis 运行期故障:降级放行,记录警告日志 -3. 限流计数异常:降级放行,避免全站不可用 -``` - -:::tip fail-open 策略 -限流采用 fail-open 策略:Redis 不可用时放行所有请求,优先保证服务可用性。这是安全与可用性的权衡。 -::: - -## 中间件位置 - -限流中间件在 `src/app.py` 中注册为最外层(CORS 内层): - -```python -# 最外层:限流(被限流的请求不进入 DB 会话与操作日志) -register_rate_limit(app) -# CORS(确保限流响应也带 CORS 头) -register_cors(app) -``` - -## 总结 - -请求限流模块具备以下特点: - -``` -1. 滑动窗口:消除固定窗口边界处的 2x 突发问题 -2. IP 维度:按客户端 IP 限流,支持代理头 -3. 默认关闭:opt-in 模式,按需开启 -4. 降级策略:Redis 故障时 fail-open 放行 -5. 最外层拦截:限流请求不进入 DB 会话和操作日志 -6. Lua 原子操作:计数和过期设置原子执行,避免并发问题 -``` diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/core/rbac.md b/DjangoAdmin_Flask_EleVue/zh/volume4/core/rbac.md index 9380baf..a65f751 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/core/rbac.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/core/rbac.md @@ -118,59 +118,108 @@ def add(): 校验逻辑(完整实现): ```python +# src/core/permission.py +import fnmatch +import hmac +import logging from functools import wraps -from flask import request, g -from src.core.response import R -from src.core.security import get_user_id +from typing import List, Protocol -def check_permission(permission: str): - """节点权限鉴权装饰器,userId==1(管理员)直接放行""" +from flask import request, g, current_app +from src.core import response as R +from src.core.security import get_user_id +from config import PERMISSION_ENABLED, INTERNAL_API_KEY, INTERNAL_ALLOWED_IPS + +logger = logging.getLogger(__name__) + + +class PermissionProvider(Protocol): + """权限数据提供方接口:core 只认接口,不认实现。""" + + def get_user_permissions(self, user_id: int) -> List[str]: + """返回用户拥有的权限节点列表。""" + + +def get_provider() -> "PermissionProvider": + """获取注入的权限提供方(应用工厂注入)。""" + return current_app.extensions.get("permission_provider") + + +def check_permission(permission: str, log: bool = False): + """ + 权限验证装饰器 + + 权限验证规则: + 1. 定时任务等内部调用 → 跳过权限验证,直接放行 + 2. 超级管理员(user_id=1) → 拥有所有权限 + 3. 普通用户 → 通过注入的 PermissionProvider 验证权限 + """ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): - # 获取当前用户ID - user_id = get_user_id() + # 定时任务等内部调用:跳过权限验证 + if is_internal_call(): + return func(*args, **kwargs) - # 管理员免检 + # 获取用户ID + user_id = getattr(g, "user_id", None) + if user_id is None: + try: + user_id = get_user_id() + except Exception: + user_id = None + + # 超级管理员放行(user_id=1 为系统内置管理员) 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 user_id: + provider = get_provider() + if provider is None: + return R.failed(msg="权限验证失败", code=500) - # 检查权限 - if permission not in permission_list: - return R.failed("权限不足") + permission_list = provider.get_user_permissions(user_id) + if permission not in permission_list: + return R.failed(msg="权限不足!", code=403) + else: + return R.failed(msg="请先登录", code=401) return func(*args, **kwargs) return wrapper return decorator ``` -:::warning Request 提取 -装饰器通过 `flask.request` 获取当前请求对象,无需显式传递 request 参数。 +:::tip 依赖注入设计 +权限校验采用依赖注入设计(解决 core → modules 依赖倒置): +- core 定义 `PermissionProvider` 协议,不 import 任何业务模块 +- 应用工厂启动时注入 menu 服务的实现(见 `src/app.py` 的 `_inject_dependencies`) +- `check_permission` 通过 `current_app.extensions['permission_provider']` 获取权限数据 ::: :::tip 管理员免检 用户ID为1的管理员账号(admin)自动跳过所有权限校验,拥有全部权限。 ::: -### 权限缓存 +### 权限数据注入 -权限列表通过 Redis 缓存,提高查询性能: +权限数据通过应用工厂注入,避免 core 层直接依赖 modules 层: ```python -# 获取用户的权限节点列表(带 Redis 缓存) -def get_permissions_list(user_id: int) -> list: - # 1. 先查 Redis 缓存 - cache_key = f"user:permissions:{user_id}" - cached = redis.get(cache_key) - if cached: - return json.loads(cached) +# src/app.py - _inject_dependencies() +from src.modules.system.menu.service import get_user_permissions - # 2. 缓存未命中 → 查 DB(user → role → menu where type=1) - permission_list = _query_permissions_from_db(user_id) +class MenuPermissionProvider: + """权限数据提供方(适配 menu 服务)。""" + def get_user_permissions(self, user_id): + return get_user_permissions(user_id) + +app.extensions['permission_provider'] = MenuPermissionProvider() +``` + +### 权限缓存 + +权限列表通过 Redis 缓存,提高查询性能。缓存在角色菜单变更时主动失效。 # 3. 写入缓存 redis.set(cache_key, json.dumps(permission_list), ex=3600) diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/core/transaction.md b/DjangoAdmin_Flask_EleVue/zh/volume4/core/transaction.md index 7925cbf..1706af7 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/core/transaction.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/core/transaction.md @@ -98,7 +98,7 @@ def add_position(data): ### 批量操作 ```python -def batch_delete_positions(id_list): +def delete_positions(id_list): """批量删除岗位""" # 批量软删除(仅 flush,不 commit) PositionRepository.batch_delete(id_list) diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/core/validator.md b/DjangoAdmin_Flask_EleVue/zh/volume4/core/validator.md index 4c779d8..f647de6 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/core/validator.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/core/validator.md @@ -123,7 +123,7 @@ from wtforms import Form, IntegerField from wtforms.validators import Optional -class BaseForm(Form): +class BaseForm(FlaskForm): """ 表单基类 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/frontend/setup.md b/DjangoAdmin_Flask_EleVue/zh/volume4/frontend/setup.md index 13e12ec..95e122d 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/frontend/setup.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/frontend/setup.md @@ -13,7 +13,7 @@ | Vite | 3.x | 下一代前端构建工具 | | Pinia | 2.x | 状态管理(Vuex 替代方案) | | Vue Router | 4.x | 路由管理 | -| Axios | 1.x | HTTP 请求库 | +| Axios | 0.27.x | HTTP 请求库 | | TypeScript | 5.x | 类型安全的 JavaScript 超集 | | Tailwind CSS | 3.x | 原子化 CSS 框架 | | SCSS | - | CSS 预处理器 | diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/integration/api-contract.md b/DjangoAdmin_Flask_EleVue/zh/volume4/integration/api-contract.md index 7e8a3b9..d7b11c9 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/integration/api-contract.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/integration/api-contract.md @@ -74,7 +74,7 @@ class R: ### 使用示例 ```python -from src.core.response import R +from src.core import response as R # 成功 return R.ok(data=user_info) diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/tools/ip-ua.md b/DjangoAdmin_Flask_EleVue/zh/volume4/tools/ip-ua.md index db77de8..aacb6b7 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/tools/ip-ua.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/tools/ip-ua.md @@ -1,7 +1,7 @@ # IP/UA 解析 :::tip 说明 -项目提供客户端 IP 获取、IP 地理位置查询和 User-Agent 解析三大能力。IP 获取位于 `src/common/utils/ip.py`,地理位置查询基于 ip2region 离线数据库(`src/common/utils/ip2region.py`),UA 解析位于 `src/common/utils/user_agent.py`。三者组合使用,为登录日志、操作日志、限流、登录锁定等场景提供客户端身份信息。 +项目提供客户端 IP 获取、IP 地理位置查询和 User-Agent 解析三大能力。IP 获取位于 `src/common/utils/ip.py`,地理位置查询基于 ip2region 离线数据库(`src/common/utils/ip2region.py`),UA 解析位于 `src/common/utils/user_agent.py`。三者组合使用,为登录日志、操作日志、登录锁定等场景提供客户端身份信息。 ::: ## 架构总览 @@ -21,8 +21,8 @@ ┌─────┴─────┐ ▼ ▼ ┌─────────┐ ┌──────────────────────────────────┐ -│ 限流 │ │ get_ip_region(ip) │ -│ 登录锁定 │ │ ├─ 内网/保留 IP → "内网IP" │ +│ 登录锁定 │ │ get_ip_region(ip) │ +│ 验证码防刷│ │ ├─ 内网/保留 IP → "内网IP" │ │ 验证码防刷│ │ ├─ ip2region.xdb 离线查询 │ └─────────┘ │ └─ 异常降级 → "未知" │ └──────────┬───────────────────────┘ @@ -49,49 +49,40 @@ ### get_client_ip() ```python -def get_client_ip() -> str: +# src/common/utils/ip.py +def get_client_ip(): """ 获取客户端真实IP地址 - 仅当直连对端命中 TRUSTED_PROXY_IPS 白名单且 TRUSTED_PROXY_COUNT>0 时才信任 - X-Forwarded-For 头: - - TRUSTED_PROXY_COUNT==0 或 TRUSTED_PROXY_IPS 未配置 / 对端不命中:不信任代理头, - 直接使用 request.remote_addr(fail-closed,防止被直接暴露时伪造 XFF) - - 命中白名单:从 X-Forwarded-For 右侧取倒数第 N 个 IP 作为真实客户端IP + 通过Flask ProxyFix中间件由PROXY_NUM配置自动设置 + request.remote_addr为真实客户端IP。未配置ProxyFix时, + request.remote_addr即为直连IP,避免客户端伪造。 """ + from flask import request + return request.remote_addr ``` **返回值**:客户端 IP 地址字符串(如 `"1.2.3.4"`)。 ### 代理链解析原理 -当应用部署在 Nginx/HAProxy 等反向代理之后,`request.remote_addr` 拿到的是代理 IP 而非真实客户端 IP。此时需要从 `X-Forwarded-For`(XFF)头提取真实 IP。 +当应用部署在 Nginx/HAProxy 等反向代理之后,`request.remote_addr` 拿到的是代理 IP 而非真实客户端 IP。项目通过 Flask 的 `ProxyFix` 中间件自动处理,在 `src/app.py` 中通过 `PROXY_NUM` 配置: -``` -客户端 (1.2.3.4) - │ - ▼ -外层代理 (10.0.0.1) ← 追加 XFF: 1.2.3.4 - │ - ▼ -内层代理 (10.0.0.2) ← 追加 XFF: 1.2.3.4, 10.0.0.1 - │ - ▼ -Flask 应用 - request.remote_addr = 10.0.0.2 - X-Forwarded-For = "1.2.3.4, 10.0.0.1" -``` +```python +# src/app.py - create_app() +from werkzeug.middleware.proxy_fix import ProxyFix -**取值逻辑**:XFF 最右侧由受信代理追加,客户端可控的伪造条目只可能出现在左侧。跳过最右侧 `TRUSTED_PROXY_COUNT` 个代理条目,取真实客户端 IP。 +proxy_num = int(os.getenv('PROXY_NUM', 0)) +if proxy_num > 0: + app.wsgi_app = ProxyFix(app.wsgi_app, x_for=proxy_num, x_proto=proxy_num) +``` ### 安全设计:fail-closed | 场景 | 行为 | |------|------| -| `TRUSTED_PROXY_COUNT=0` | 不信任任何代理头,直接返回 `request.remote_addr` | -| `TRUSTED_PROXY_IPS` 未配置或为空 | 同上,fail-closed | -| 直连对端不在白名单中 | 同上(防止应用被直接暴露时客户端伪造 XFF) | -| 对端命中白名单 + XFF 条目少于代理层数 | 回退到 XFF 最右侧条目(避免拿到代理 IP) | +| `PROXY_NUM=0`(默认) | 不信任任何代理头,直接返回 `request.remote_addr` | +| `PROXY_NUM>=1` | 信任 N 层代理,从 XFF 右侧取真实客户端 IP | | `remote_addr` 为 None | 回退到 `127.0.0.1` | ### 代理配置 @@ -100,15 +91,15 @@ Flask 应用 ```bash # 信任的代理层数(0 = 不信任代理头) -TRUSTED_PROXY_COUNT=1 +PROXY_NUM=1 # 可信代理IP白名单(逗号分隔) -TRUSTED_PROXY_IPS=127.0.0.1 +PROXY_NUM=127.0.0.1 ``` **常见部署配置**: -| 部署方式 | TRUSTED_PROXY_COUNT | TRUSTED_PROXY_IPS | +| 部署方式 | PROXY_NUM | PROXY_NUM | |:---------|:----|:----| | 直连(无代理) | `0` | 留空 | | 单层 Nginx | `1` | `127.0.0.1`(本机)或 Nginx 内网 IP | @@ -116,7 +107,7 @@ TRUSTED_PROXY_IPS=127.0.0.1 | Docker bridge 映射端口 | `0` | 留空(容器外为客户端直连) | :::warning 安全提示 -如果应用被直接暴露在公网(如 Docker bridge 映射端口、未走 Nginx),务必保持 `TRUSTED_PROXY_COUNT=0` 或 `TRUSTED_PROXY_IPS` 留空。否则客户端可随意伪造 XFF 首部,绕过基于 IP 的登录锁定和请求限流。 +如果应用被直接暴露在公网(如 Docker bridge 映射端口、未走 Nginx),务必保持 `PROXY_NUM=0`。否则客户端可随意伪造 XFF 首部,绕过基于 IP 的登录锁定。 ::: ## 二、IP 地理位置查询 @@ -339,29 +330,7 @@ os_name, browser_name = parse_user_agent(ua) | `os` | `VARCHAR(100)` | 操作系统 | | `browser` | `VARCHAR(100)` | 浏览器 | -### 3. 请求限流 - -基于 IP 的 Redis 滑动窗口限流,防止接口被刷: - -```python -# src/middleware/rate_limit.py -client_ip = get_client_ip() -current_key = f"rl:{client_ip}:{window_index}" -count = sliding_window_incr(redis, current_key, ...) -if count > RATE_LIMIT_LIMIT: - return R.failed(msg="请求过于频繁,请 N 秒后再试") -``` - -**配置项**(`.env`): - -| 变量 | 默认值 | 说明 | -|:-----|:-------|:-----| -| `RATE_LIMIT_ENABLED` | `False` | 是否启用限流 | -| `RATE_LIMIT_LIMIT` | `100` | 单窗口内每 IP 最大请求数 | -| `RATE_LIMIT_WINDOW_SECONDS` | `60` | 窗口时长(秒) | -| `RATE_LIMIT_KEY_PREFIX` | `rl:` | Redis key 前缀 | - -### 4. 登录失败锁定 +### 3. 登录失败锁定 按 IP + 用户名维度统计登录失败次数,超过阈值自动锁定: @@ -380,7 +349,7 @@ if locked: | 失败计数 | `login:fail:{ip}:{username}` | 累计失败次数 | | 锁定状态 | `login:lock:{ip}:{username}` | 锁定时自动设置 TTL | -### 5. 验证码防刷 +### 4. 验证码防刷 按 IP 统计验证码请求频率,超过阈值拒绝生成新验证码: @@ -402,21 +371,7 @@ if count and int(count) >= CAPTCHA_RATE_LIMIT: # 可信代理配置 # ============================================================ # 信任的代理层数(0 = 不信任代理头,直接使用 request.remote_addr) -TRUSTED_PROXY_COUNT=1 -# 可信代理IP白名单(逗号分隔,仅白名单内的直连对端才信任 XFF) -TRUSTED_PROXY_IPS=127.0.0.1 - -# ============================================================ -# 请求限流配置 -# ============================================================ -# 是否启用请求限流(默认关闭) -RATE_LIMIT_ENABLED=False -# 单窗口内每个 IP 允许的最大请求数 -RATE_LIMIT_LIMIT=100 -# 统计窗口时长(秒) -RATE_LIMIT_WINDOW_SECONDS=60 -# Redis 计数 key 前缀 -RATE_LIMIT_KEY_PREFIX=rl: +PROXY_NUM=1 ``` ## 六、常见问题 @@ -425,7 +380,7 @@ RATE_LIMIT_KEY_PREFIX=rl: **原因**:本地直连无代理,`request.remote_addr` 就是 `127.0.0.1`。 -**说明**:这是正常行为。生产环境配置 `TRUSTED_PROXY_COUNT` 和 `TRUSTED_PROXY_IPS` 后即可获取真实客户端 IP。 +**说明**:这是正常行为。生产环境配置 `PROXY_NUM` 后即可获取真实客户端 IP。 ### 地理位置显示为"未知" @@ -443,14 +398,6 @@ RATE_LIMIT_KEY_PREFIX=rl: **说明**:常见于开发环境或内网部署。生产环境通过反代获取真实外网 IP 即可。 -### 限流不生效 - -**排查步骤**: - -1. 检查 `.env` 中 `RATE_LIMIT_ENABLED=True` -2. 检查 Redis 连接是否正常 -3. 检查 `TRUSTED_PROXY_COUNT` 配置——如果限流基于 `request.remote_addr`(代理 IP),所有请求会共享同一 IP,导致限流提前触发或形同虚设 - ## 七、总结 | 模块 | 文件 | 核心能力 | 降级策略 | @@ -459,4 +406,4 @@ RATE_LIMIT_KEY_PREFIX=rl: | 地理位置 | `src/common/utils/ip2region.py` | 基于 ip2region.xdb 离线查询,零网络依赖 | 异常/畸形/内网统一降级返回 `"未知"` 或 `"内网IP"` | | UA 解析 | `src/common/utils/user_agent.py` | 正则匹配提取操作系统和浏览器 | UA 为空时返回 `('', '')` | -三者组合为登录日志、操作日志、限流、登录锁定、验证码防刷等场景提供完整的客户端身份信息链路。所有接口均采用降级策略,保证日志落库等核心流程不受异常影响。 +三者组合为登录日志、操作日志、登录锁定、验证码防刷等场景提供完整的客户端身份信息链路。所有接口均采用降级策略,保证日志落库等核心流程不受异常影响。 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/tools/logger.md b/DjangoAdmin_Flask_EleVue/zh/volume4/tools/logger.md deleted file mode 100644 index 5f74411..0000000 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/tools/logger.md +++ /dev/null @@ -1,124 +0,0 @@ -# 日志系统 - -:::tip 说明 -项目通过 Python 标准库 `logging` 实现结构化日志,支持控制台 + 文件双输出,日志文件按大小自动轮转。配置入口位于 `src/core/logger.py`。 -::: - -## 配置入口 - -日志配置位于 `src/core/logger.py`: - -```python -import logging -import sys -from logging.handlers import RotatingFileHandler -from pathlib import Path - -# 日志目录 -LOG_DIR = Path(__file__).resolve().parents[2] / "logs" -LOG_DIR.mkdir(parents=True, exist_ok=True) - -# 日志格式 -_FORMAT = "%(asctime)s | %(levelname)-7s | %(name)s | %(message)s" - -# 轮转配置 -LOG_MAX_BYTES = 10 * 1024 * 1024 # 10MB -LOG_BACKUP_COUNT = 5 -``` - -## setup_logging() - -```python -def setup_logging(level: int = logging.INFO, log_file: str = "app.log") -> None: - """ - 配置全局日志:root logger 输出到控制台与文件 - - Args: - level: 日志级别(默认 INFO) - log_file: 日志文件名(位于 logs/ 目录下) - """ -``` - -### 控制台输出 - -```python -console = logging.StreamHandler(sys.stdout) -console.setFormatter(formatter) -root.addHandler(console) -``` - -### 文件输出(按大小轮转) - -```python -file_handler = RotatingFileHandler( - LOG_DIR / log_file, - maxBytes=LOG_MAX_BYTES, # 单文件上限 10MB - backupCount=LOG_BACKUP_COUNT, # 保留 5 份备份 - encoding="utf-8", -) -``` - -## 日志轮转 - -当日志文件达到 10MB 时自动轮转: - -``` -logs/app.log ← 当前日志 -logs/app.log.1 ← 最近一次轮转 -logs/app.log.2 ← 倒数第二次 -logs/app.log.3 -logs/app.log.4 -logs/app.log.5 ← 最早的备份(超过自动删除) -``` - -## 日志格式 - -``` -2025-03-06 14:30:25,123 | INFO | modules.system.user.service | 用户登录成功 -``` - -格式说明: - -| 字段 | 说明 | -|------|------| -| `asctime` | 时间戳 | -| `levelname` | 日志级别(右对齐7字符) | -| `name` | 模块名称 | -| `message` | 日志内容 | - -## 使用方式 - -在各模块中获取 logger: - -```python -import logging - -logger = logging.getLogger(__name__) - -logger.info("操作成功") -logger.warning("配置缺失") -logger.error(f"异常信息: {str(e)}") -``` - -## 框架日志集成 - -Flask 和 SQLAlchemy 的默认日志统一走 root logger: - -```python -for name in ("flask", "sqlalchemy"): - lg = logging.getLogger(name) - lg.handlers.clear() - lg.propagate = True -``` - -:::tip 温馨提示 -`setup_logging()` 会检查是否已添加 handler(通过 `_djangoadmin` 标记),避免重复添加。可在应用启动时调用一次。 -::: - -## 操作日志 - -除了应用日志外,还通过 `src/middleware/operation_log.py` 中间件自动记录用户的写操作日志,存储到数据库 `flask_operation_log` 表中。详见 [操作日志](/zh/volume4/core/operation-log) 章节。 - -## 总结 - -日志系统通过 `RotatingFileHandler` 实现按大小轮转,单文件上限 10MB 保留 5 份备份,防止日志无限增长撑爆磁盘。控制台和文件双输出,格式统一包含时间戳、级别、模块名和消息内容。 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume4/tools/request-util.md b/DjangoAdmin_Flask_EleVue/zh/volume4/tools/request-util.md index 03ac130..cf8d11e 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume4/tools/request-util.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume4/tools/request-util.md @@ -4,22 +4,22 @@ `src/common/utils/request.py` 提供请求数据解析相关的工具函数,包括批量 ID 解析、SQL LIKE 安全处理、分页参数解析等。这些工具函数在各业务模块中广泛使用。 ::: -## parse_id_list +## parse_batch_ids 从请求体解析批量删除的 ID 数组,校验并转为整数列表。 ```python from flask import request -from src.common.utils.request import parse_id_list +from src.common.utils.request import parse_batch_ids -def parse_id_list(request) -> list: +def parse_batch_ids(request) -> list: """ 从请求体解析 ID 数组 请求体格式: [1, 2, 3] 返回: [1, 2, 3] 示例: - >>> parse_id_list(request) + >>> parse_batch_ids(request) [1, 2, 3] """ ids = request.get_json() @@ -38,9 +38,9 @@ def parse_id_list(request) -> list: @position_blue.route('/batchDelete', methods=['DELETE']) @check_permission('sys:position:batchDelete') def batch_delete(): - 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) ``` ## parse_request_body @@ -207,4 +207,4 @@ def get_client_ip() -> str: ## 总结 -请求解析工具函数覆盖了批量操作、模糊查询安全、分页参数等常见场景。`parse_id_list` 和 `parse_pagination` 通过防御性校验避免畸形参数导致 500 错误,`like_escape` 防止 LIKE 通配符注入,`parse_request_body` 使用 WTForms 进行参数校验。 +请求解析工具函数覆盖了批量操作、模糊查询安全、分页参数等常见场景。`parse_batch_ids` 和 `parse_pagination` 通过防御性校验避免畸形参数导致 500 错误,`like_escape` 防止 LIKE 通配符注入,`parse_request_body` 使用 WTForms 进行参数校验。 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume5/demo.md b/DjangoAdmin_Flask_EleVue/zh/volume5/demo.md index d80e6f3..d2328bf 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume5/demo.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume5/demo.md @@ -256,8 +256,8 @@ update_example_status = _example_service.update_status ```python 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 example_blue = Blueprint('example', __name__, url_prefix='/example') diff --git a/DjangoAdmin_Flask_EleVue/zh/volume6/db-migrate.md b/DjangoAdmin_Flask_EleVue/zh/volume6/db-migrate.md index aa50af6..fb4d511 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume6/db-migrate.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume6/db-migrate.md @@ -15,11 +15,7 @@ 首次部署时,使用初始化脚本一键完成建库、建表、Flask-Migrate stamp: ```bash -# 方式一:直接运行脚本 python scripts/setup_db.py - -# 方式二:Make 命令 -make db-init ``` :::tip 初始化流程 @@ -40,7 +36,7 @@ make db-init flask db migrate -m "描述变更内容" # 或使用 Make 命令 -make migrate-revision +flask db migrate ``` :::tip 注意事项 @@ -56,7 +52,7 @@ make migrate-revision flask db upgrade # 或使用 Make 命令 -make migrate-upgrade +flask db upgrade ``` ### 回滚迁移 @@ -87,7 +83,7 @@ flask db history flask db stamp head # 或使用 Make 命令 -make migrate-stamp +flask db stamp head ``` ## 跨库数据迁移 @@ -111,7 +107,7 @@ make migrate-stamp | 目标库 | 驱动包 | 说明 | |:----|:----|:----| -| PostgreSQL | psycopg[binary] | psycopg3 | +| PostgreSQL | psycopg2-binary | psycopg2 | | SQL Server | pymssql | - | | SQLite | 内置 | 无需额外驱动 | | Oracle | oracledb | thin 模式,无需安装 Oracle Client | @@ -362,14 +358,14 @@ CREATE SEQUENCE flask_user_id_seq START WITH ; | 场景 | 命令 | |------|------| -| 全新安装 | `make db-init` | -| 模型变更后生成迁移 | `make migrate-revision` | -| 应用迁移 | `make migrate-upgrade` | +| 全新安装 | `python scripts/setup_db.py` | +| 模型变更后生成迁移 | `flask db migrate` | +| 应用迁移 | `flask db upgrade` | | 回滚一次迁移 | `flask db downgrade -1` | -| 标记已有库为最新 | `make migrate-stamp` | +| 标记已有库为最新 | `flask db stamp head` | | 跨库迁移干跑 | `make db-migrate-dry-run ARGS="..."` | | 跨库迁移执行 | `make db-migrate ARGS="..."` | ## 总结 -数据库迁移分为结构迁移(Flask-Migrate / Alembic)和数据迁移(`migrate_db.py`)两部分。全新安装用 `make db-init`,模型变更用 Flask-Migrate 自动生成迁移脚本,跨库切换用 `migrate_db.py` 数据泵。建议每次迁移前先干跑确认,生产环境务必做好备份。 +数据库迁移分为结构迁移(Flask-Migrate / Alembic)和数据迁移(`migrate_db.py`)两部分。全新安装用 `python scripts/setup_db.py`,模型变更用 Flask-Migrate 自动生成迁移脚本,跨库切换用 `migrate_db.py` 数据泵。建议每次迁移前先干跑确认,生产环境务必做好备份。 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume6/docker.md b/DjangoAdmin_Flask_EleVue/zh/volume6/docker.md index 7f0662b..10f3cd4 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume6/docker.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume6/docker.md @@ -12,7 +12,7 @@ ## Dockerfile 详解 -项目根目录 `Dockerfile` 完整内容: +`docker/Dockerfile` 完整内容: ```dockerfile # 基础镜像:Python 3.11-slim diff --git a/DjangoAdmin_Flask_EleVue/zh/volume6/env-plan.md b/DjangoAdmin_Flask_EleVue/zh/volume6/env-plan.md index c6fd91b..79e15a8 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume6/env-plan.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume6/env-plan.md @@ -113,18 +113,18 @@ Docker 部署时,`docker-compose.yml` 通过 `env_file: .env` 注入环境变 | DB_DRIVER | 数据库 | 依赖包 | 连接串格式 | |-----------|--------|--------|-----------| | mysql(默认) | MySQL 8.x | PyMySQL | `mysql+pymysql://user:pass@host:3306/db` | -| postgresql | PostgreSQL 14+ | psycopg[binary] | `postgresql+psycopg://user:pass@host:5432/db` | +| postgresql | PostgreSQL 14+ | psycopg2-binary | `postgresql+psycopg2://user:pass@host:5432/db` | | mssql | SQL Server 2019+ | pymssql | `mssql+pymssql://user:pass@host:1433/db` | | sqlite | SQLite 3 | 内置 | `sqlite:///path/to/db.sqlite3` | | oracle | Oracle 12c+ | oracledb | `oracle+oracledb://user:pass@host:1521/db` | :::tip 切换数据库后 -切换数据库驱动后,需要重新初始化数据库结构。项目 Makefile 提供常用命令: +切换数据库驱动后,需要重新初始化数据库结构。常用命令: ```bash -make db-init # 初始化数据库(建库 + SQL 导入 + 补表 + flask db stamp) -make migrate-revision # 生成增量迁移脚本 -make migrate-upgrade # 应用迁移 -make migrate-stamp # 标记当前库已到基线 +python scripts/setup_db.py # 初始化数据库(建库 + SQL 导入 + 补表 + flask db stamp) +flask db migrate # 生成增量迁移脚本 +flask db upgrade # 应用迁移 +flask db stamp head # 标记当前库已到基线 make db-migrate ARGS="..." # 跨库数据迁移 ``` ::: diff --git a/DjangoAdmin_Flask_EleVue/zh/volume6/monitoring.md b/DjangoAdmin_Flask_EleVue/zh/volume6/monitoring.md index 91b8de8..0f3ad63 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume6/monitoring.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume6/monitoring.md @@ -18,16 +18,14 @@ Flask 应用提供健康检查路由,可用于负载均衡器或容器编排的探针配置: ```bash -# 基础健康检查 -GET /api/v1/health +# 基础健康检查(无需认证,已在登录白名单中) +GET /health # 响应示例 { "code": 0, "data": { - "status": "healthy", - "version": "1.0.0", - "uptime": 3600 + "status": "ok" }, "msg": "操作成功", "ok": true @@ -38,7 +36,7 @@ GET /api/v1/health 在 Dockerfile 中配置 HEALTHCHECK: ```dockerfile HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ - CMD curl -f http://localhost:8021/api/v1/health || exit 1 + CMD curl -f http://localhost:8021/health || exit 1 ``` ::: @@ -79,7 +77,7 @@ HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ ``` 2026-09-05 10:30:15 [INFO] src.app: Application startup complete -2026-09-05 10:30:16 [WARNING] src.middleware.rate_limit: Rate limit exceeded for 192.168.1.100 +2026-09-05 10:30:16 [WARNING] src.middleware.auth: Login attempt locked for user admin from 192.168.1.100 2026-09-05 10:30:17 [ERROR] src.modules.auth.service: Login failed for user admin ``` diff --git a/DjangoAdmin_Flask_EleVue/zh/volume6/nginx.md b/DjangoAdmin_Flask_EleVue/zh/volume6/nginx.md index 55b85c4..395fdf8 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume6/nginx.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume6/nginx.md @@ -15,7 +15,7 @@ Nginx 作为反向代理服务器,承担静态资源服务、API 请求转发 ## 基础配置 -项目提供了 Nginx 配置模板 `deploy/nginx.conf`,按需修改即可使用: +以下为推荐的 Nginx 配置模板,按需修改后保存为 `/etc/nginx/conf.d/djangoadmin.conf`: ```nginx upstream flask_backend { @@ -101,8 +101,7 @@ cd ui && pnpm build mkdir -p /data/apps/djangoadmin/frontend cp -r ui/dist /data/apps/djangoadmin/frontend/ -# 3. 拷贝 Nginx 配置 -cp deploy/nginx.conf /etc/nginx/conf.d/djangoadmin.conf +# 3. 将上方配置保存为 /etc/nginx/conf.d/djangoadmin.conf # 4. 修改配置中的实际值(server_name、root 等) diff --git a/DjangoAdmin_Flask_EleVue/zh/volume6/supervisor.md b/DjangoAdmin_Flask_EleVue/zh/volume6/supervisor.md index ce0e16e..4eea92b 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume6/supervisor.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume6/supervisor.md @@ -12,7 +12,7 @@ Supervisor 是一个 Python 编写的进程管理工具,用于在 Linux 服务 ## 配置文件示例 -项目提供了 Supervisor 配置模板 `deploy/supervisor.conf`,完整内容: +以下为推荐的 Supervisor 配置模板,按需修改后保存到 `/etc/supervisor/conf.d/`: ```ini ; Supervisor 进程管理示例配置 @@ -47,7 +47,7 @@ environment=PYTHONUNBUFFERED=1 # 1. 修改 command 中的虚拟环境路径 # 2. 修改 directory 为项目实际路径 # 3. 修改 stdout_logfile 为日志实际路径 -cp deploy/supervisor.conf /etc/supervisor/conf.d/djangoadmin-flask.conf +# 将上方配置保存为 /etc/supervisor/conf.d/djangoadmin-flask.conf ``` ::: @@ -192,4 +192,4 @@ startsecs=10 ## 总结 -Supervisor 是 Linux 环境下管理 Flask 进程的可靠方案,提供自动重启、日志轮转、进程监控等能力。配置文件位于 `deploy/supervisor.conf`,修改路径后复制到 `/etc/supervisor/conf.d/` 即可使用。生产环境建议配合 systemd 管理 Supervisor 自身的启动。 +Supervisor 是 Linux 环境下管理 Flask 进程的可靠方案,提供自动重启、日志轮转、进程监控等能力。按上方模板创建配置文件,修改路径后复制到 `/etc/supervisor/conf.d/` 即可使用。生产环境建议配合 systemd 管理 Supervisor 自身的启动。 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume6/upgrade.md b/DjangoAdmin_Flask_EleVue/zh/volume6/upgrade.md index fe7ee3b..6fac56c 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume6/upgrade.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume6/upgrade.md @@ -94,8 +94,6 @@ flask db history # 执行迁移 flask db upgrade -# 或使用 Make 命令 -make migrate-upgrade ``` :::warning 迁移前确认 @@ -124,7 +122,7 @@ sudo systemctl restart djangoadmin-flask sudo supervisorctl status djangoadmin-flask # 2. 检查健康端点 -curl -s http://127.0.0.1:8021/api/v1/health | python -m json.tool +curl -s http://127.0.0.1:8021/health | python -m json.tool # 3. 检查日志 tail -f /data/apps/logs/supervisor.out.log diff --git a/DjangoAdmin_Flask_EleVue/zh/volume7/api-response.md b/DjangoAdmin_Flask_EleVue/zh/volume7/api-response.md index 9c99fbf..75556c3 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume7/api-response.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume7/api-response.md @@ -51,7 +51,7 @@ ### R.ok() 成功响应 ```python -from src.core.response import R +from src.core import response as R # 基础成功响应 return R.ok() @@ -114,7 +114,7 @@ return R.page(data=records, total=100, current=1, size=10) ## 在 Router 中使用 ```python -from src.core.response import R +from src.core import response as R # 查询详情:service 返回 dict,router 包 R.ok @position_blue.route('/detail/', methods=['GET']) diff --git a/DjangoAdmin_Flask_EleVue/zh/volume7/deletion.md b/DjangoAdmin_Flask_EleVue/zh/volume7/deletion.md index 72d88de..268d6cf 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume7/deletion.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume7/deletion.md @@ -51,9 +51,9 @@ Content-Type: application/json @check_permission('sys:position:batchDelete') def batch_delete(): """批量删除岗位""" - 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) ``` ## 删除流程 @@ -61,7 +61,7 @@ def batch_delete(): ``` 1. Router 接收请求 ↓ -2. parse_id_list(request) 解析 ID 数组 +2. parse_batch_ids(request) 解析 ID 数组 ↓ 3. Service.batch_delete(id_list) ↓ diff --git a/DjangoAdmin_Flask_EleVue/zh/volume7/doc-standard.md b/DjangoAdmin_Flask_EleVue/zh/volume7/doc-standard.md index 0f0b5d2..4d9f1ee 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume7/doc-standard.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume7/doc-standard.md @@ -136,7 +136,7 @@ wiki/ # VitePress 文档站 │ ├── 3. 模块开发实战/ # volume2/(Model→Schema→Repo→Service→Router→前端) │ ├── 4. 架构设计/ # volume3/(分层、基类、缓存、安全) │ ├── 5. 开发指南/ # volume4/ -│ │ ├── 5.1 后端核心功能/ # core/(JWT、RBAC、字典、日志、限流、事务) +│ │ ├── 5.1 后端核心功能/ # core/(JWT、RBAC、字典、日志、事务) │ │ ├── 5.2 后端业务模块/ # business/(用户、角色、菜单、文章、任务) │ │ ├── 5.3 后端通用工具/ # tools/(上传、Excel、IP/UA、密码、富文本) │ │ ├── 5.4 前端开发/ # frontend/(组件、路由、Store、构建) diff --git a/DjangoAdmin_Flask_EleVue/zh/volume7/endpoint.md b/DjangoAdmin_Flask_EleVue/zh/volume7/endpoint.md index 0542e06..926ca7e 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume7/endpoint.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume7/endpoint.md @@ -96,9 +96,9 @@ def status(): @position_blue.route('/batchDelete', methods=['DELETE']) @check_permission('sys:position:batchDelete') def batch_delete(): - 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) ``` ## 权限标识规范 @@ -126,8 +126,8 @@ def batch_delete(): 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 实例 position_blue = Blueprint('position', __name__, url_prefix='/position') @@ -191,9 +191,9 @@ def status(): @position_blue.route('/batchDelete', methods=['DELETE']) @check_permission('sys:position:batchDelete') def batch_delete(): - 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) ``` ## 路由注册 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume7/pydantic.md b/DjangoAdmin_Flask_EleVue/zh/volume7/pydantic.md deleted file mode 100644 index e429d18..0000000 --- a/DjangoAdmin_Flask_EleVue/zh/volume7/pydantic.md +++ /dev/null @@ -1,190 +0,0 @@ -# WTForms Schema规范 - -## 概述 - -项目使用 WTForms 进行请求数据校验。所有请求体参数通过 WTForms 表单类定义,框架在调用 Router 前自动完成解析和验证。 - -:::tip 核心原则 -- 请求体参数必须声明为 WTForms 表单类(Form 类) -- 字段约束使用 `validators` 定义 -- 自定义校验使用 `validate_` 方法 -- 唯一性校验在 Schema 中查询数据库 -::: - -## 命名规范 - -| 类型 | 命名格式 | 示例 | -|------|---------|------| -| 创建/编辑表单 | `{模块}Form` | `PositionForm` | -| 状态更新表单 | `{模块}StatusForm` | `PositionStatusForm` | -| 查询参数表单 | `{模块}QueryForm` | `PositionQueryForm` | - -:::tip 继承 BaseForm -创建/编辑表单继承 `src.common.forms.base_form.BaseForm`,自动获得 `id` 字段(编辑时必填)。 -::: - -## PositionForm 示例 - -以岗位模块为例,`src/modules/system/position/schema.py` 完整内容: - -```python -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') - ]) -``` - -:::tip BaseForm 基类 -`BaseForm`(定义在 `src/common/forms/base_form.py`)提供 `id` 字段和数据清洗功能,自动将前端传入的空字符串 `''`/`'null'`/`'undefined'` 转为 `None`,避免 WTForms 解析报错。 -::: - -## Validator 约束 - -| Validator | 类型 | 说明 | 示例 | -|------|------|------|------| -| `DataRequired` | any | 必填字段 | `DataRequired(message='不能为空')` | -| `Length` | str | 字符串长度 | `Length(min=1, max=150)` | -| `NumberRange` | int/float | 数值范围 | `NumberRange(min=0, max=99999)` | -| `Optional` | any | 可选字段 | `Optional()` | -| `Email` | str | 邮箱格式 | `Email(message='邮箱格式不正确')` | -| `URL` | str | URL 格式 | `URL(message='URL格式不正确')` | -| `Regexp` | str | 正则表达式 | `Regexp(r'^[a-zA-Z]+$')` | - -:::tip Validator 说明 -- `DataRequired`:必填字段,不允许空值 -- `Length`:字符串长度限制 -- `NumberRange`:数值范围限制 -- `Optional`:可选字段 -::: - -## 自定义校验器 - -### 唯一性校验 - -```python -from wtforms import ValidationError -from src.modules.system.position.repository import PositionRepository - - -class PositionForm(BaseForm): - """岗位创建/编辑表单""" - name = StringField('岗位名称', validators=[ - DataRequired(message='岗位名称不能为空'), - Length(min=1, max=150, message='岗位名称长度必须在1-150个字符之间') - ]) - - def validate_name(self, field): - """岗位名称唯一性校验""" - exclude_id = self.id.data if self.id.data else None - if PositionRepository.exists_by([Position.name == field.data], exclude_id): - raise ValidationError('岗位名称不能重复') -``` - -### 格式校验 - -```python -from wtforms import ValidationError -import re - - -class UserForm(BaseForm): - """用户创建/编辑表单""" - username = StringField('用户名', validators=[ - DataRequired(message='用户名不能为空'), - Length(min=3, max=50, message='用户名长度必须在3-50个字符之间') - ]) - email = StringField('邮箱', validators=[ - DataRequired(message='邮箱不能为空'), - Length(max=100, message='邮箱长度不能超过100个字符') - ]) - phone = StringField('手机号', validators=[ - Optional(), - Length(max=20, message='手机号长度不能超过20个字符') - ]) - - def validate_username(self, field): - """用户名只允许字母、数字、下划线""" - if not re.match(r'^[a-zA-Z0-9_]+$', field.data): - raise ValidationError('用户名只允许字母、数字、下划线') - - def validate_email(self, field): - """邮箱格式校验""" - if not re.match(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$', field.data): - raise ValidationError('邮箱格式不正确') - - def validate_phone(self, field): - """手机号格式校验(可选字段,有值时校验)""" - if field.data and field.data != '': - if not re.match(r'^1[3-9]\d{9}$', field.data): - raise ValidationError('手机号格式不正确') -``` - -:::tip validator 规则 -- 方法名必须为 `validate_{field_name}` -- 校验失败抛出 `ValidationError(错误消息)` -- 可选字段需判断 `None` 值 -::: - -## 在 Router 中使用 - -```python -@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) -``` - -:::tip 数据访问 -Service 层通过 `data['field']` 直接访问字段。WTForms 在进入 Router 之前已完成校验,Service 无需重复验证。 -::: - -## 错误响应 - -WTForms 校验失败时,由框架自动转换: - -```json -{ - "code": 1, - "data": null, - "msg": "岗位名称不能为空", - "ok": false -} -``` - -## 总结 - -WTForms Schema 规范要求:表单类命名 `{Module}Form`,继承 `BaseForm`;字段约束用 `validators` 定义长度/范围;自定义校验用 `validate_{field_name}` 方法抛 `ValidationError`;唯一性校验通过 `Repository.exists_by(filters, exclude_id)` 实现。框架自动解析和验证,Service 通过 `form.data['field']` 访问已校验的数据。 diff --git a/DjangoAdmin_Flask_EleVue/zh/volume7/router.md b/DjangoAdmin_Flask_EleVue/zh/volume7/router.md index bbd964f..35ec65e 100644 --- a/DjangoAdmin_Flask_EleVue/zh/volume7/router.md +++ b/DjangoAdmin_Flask_EleVue/zh/volume7/router.md @@ -2,134 +2,234 @@ ## 概述 -Router 层是 HTTP 接口层,负责定义路由、权限装饰器和请求参数绑定。每个业务模块对应一个 router 文件,所有业务逻辑委托给 Service 层处理。 +Router 层是 HTTP 接口层,负责定义路由、权限装饰器和请求参数解析。每个业务模块对应一个 router 文件,所有业务逻辑委托给 Service 层处理。 :::tip 设计原则 -- Router 层不含业务逻辑,仅做路由定义、权限校验和参数绑定 -- 装饰器顺序固定为:路由 → 权限 -- Service 函数使用延迟导入,避免循环引用 +- Router 层不含业务逻辑,仅做路由定义、权限校验和参数解析 +- 装饰器顺序固定为:路由 → 权限 → 演示模式 → 操作日志 +- Service 模块在文件顶部统一导入(`from src.modules.system.position import service as position`) +- Router 负责调用 `R.ok()` / `R.failed()` 封装响应,Service 不返回 R 对象 ::: ## 完整示例 -以岗位模块为例,`src/modules/system/position/router.py` 完整内容: +以岗位模块为例,以下为 `src/modules/system/position/router.py` 的核心代码(省略版权头): ```python +""" +岗位管理控制器模块 +============================================================= + +提供岗位(Position)相关的API接口,包括: +- 岗位分页查询 +- 岗位详情查询 +- 岗位添加、更新、删除 +- 岗位状态设置 +- 岗位数据列表获取 +""" from flask import Blueprint, request +from src.middleware import check_demo, operation_log 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.modules.system.position import service as position +from src.common.utils.request import parse_batch_ids, parse_request_body + +from src.core import response as R -# ============================================================ -# 创建 Blueprint 实例 -# ============================================================ position_blue = Blueprint('position', __name__, url_prefix='/position') # ============================================================ -# 查询分页数据 +# 岗位分页查询 # ============================================================ -@position_blue.route('/page', methods=['GET']) -@check_permission('sys:position:page') + +@position_blue.route('/page', methods=["GET"]) +@check_permission("sys:position:page") +@operation_log("岗位管理", "查询") def page(): - """获取岗位分页列表""" - from src.modules.system.position.service import get_position_page - return get_position_page() + """查询岗位分页数据""" + return R.ok(data=position.get_position_page(request.args)) # ============================================================ -# 查询岗位详情 +# 岗位详情查询 # ============================================================ -@position_blue.route('/detail/', methods=['GET']) -@check_permission('sys:position:detail') + +@position_blue.route('/detail/', methods=["GET"]) +@check_permission("sys:position:detail") +@operation_log("岗位管理", "详情") def detail(id): - """根据岗位ID查询详细信息""" - from src.modules.system.position.service import get_position_detail - return R.ok(data=get_position_detail(id)) + """查询岗位详情""" + data = position.get_position_detail(id) + return R.ok(data=data) # ============================================================ -# 获取岗位列表 +# 岗位添加 # ============================================================ -@position_blue.route('/list', methods=['GET']) -@check_permission('sys:position:list') -def get_position_list(): - """获取所有岗位列表(无分页,用于下拉选择)""" - 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') +@position_blue.route('/add', methods=["POST"]) +@check_permission("sys:position:add") +@check_demo +@operation_log("岗位管理", "新增") 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) + """添加岗位""" + json_data, err = parse_request_body() + if err: + return R.failed(msg=err) + position.add_position(json_data) + return R.ok(msg="添加成功") # ============================================================ -# 更新岗位 +# 岗位更新 # ============================================================ -@position_blue.route('/update', methods=['PUT']) -@check_permission('sys:position:update') + +@position_blue.route('/update', methods=["PUT"]) +@check_permission("sys:position:update") +@check_demo +@operation_log("岗位管理", "修改") def update(): - """更新岗位信息""" - 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) + """更新岗位""" + json_data, err = parse_request_body() + if err: + return R.failed(msg=err) + position.update_position(json_data) + return R.ok(msg="更新成功") # ============================================================ -# 删除岗位 +# 岗位删除 # ============================================================ -@position_blue.route('/delete/', methods=['DELETE']) -@check_permission('sys:position:delete') + +@position_blue.route('/delete/', methods=["DELETE"]) +@check_permission("sys:position:delete") +@check_demo +@operation_log("岗位管理", "删除") def delete(id): - """根据ID删除单个岗位""" - from src.modules.system.position.service import delete_position - return delete_position(id) + """删除岗位(支持逗号分隔的多个ID)""" + count = position.delete_positions(id) + return R.ok(msg=f"本次共删除{count}条数据") # ============================================================ -# 设置岗位状态 +# 岗位批量删除 # ============================================================ -@position_blue.route('/status', methods=['PUT']) -@check_permission('sys:position:status') -def status(): - """修改岗位状态(启用/禁用)""" - 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') +@position_blue.route('/batchDelete', methods=["DELETE"]) +@check_permission("sys:position:batchDelete") +@check_demo +@operation_log("岗位管理", "批量删除") def batch_delete(): """批量删除岗位""" - from src.modules.system.position.service import batch_delete_positions - id_list = parse_id_list(request) - return batch_delete_positions(id_list) + id_str, err = parse_batch_ids() + if err: + return err + count = position.delete_positions(id_str) + return R.ok(msg=f"本次共删除{count}条数据") + + +# ============================================================ +# 岗位状态设置 +# ============================================================ + +@position_blue.route('/status', methods=["PUT"]) +@check_permission("sys:position:status") +@check_demo +@operation_log("岗位管理", "状态变更") +def status(): + """设置岗位状态""" + json_data, err = parse_request_body() + if err: + return R.failed(msg=err) + position.update_position_status(json_data) + return R.ok(msg="设置成功") + + +# ============================================================ +# 岗位列表获取(无分页) +# ============================================================ + +@position_blue.route('/list', methods=["GET"]) +@check_permission("sys:position:list") +@operation_log("岗位管理", "查询") +def get_position_list(): + """获取岗位数据列表(无分页,用于下拉选择)""" + result = position.get_position_list(request.args) + return R.ok(data=result) ``` ## 装饰器说明 -| 装饰器 | 作用 | 位置 | +| 装饰器 | 作用 | 适用场景 | |:----|:----|:----| -| `@position_blue.route()` | 定义 HTTP 方法和路径 | 最外层 | -| `@check_permission('sys:position:page')` | RBAC 权限校验 | 路由装饰器之后 | +| `@position_blue.route()` | 定义 HTTP 方法和路径 | 所有接口 | +| `@check_permission('sys:position:page')` | RBAC 权限校验 | 所有接口 | +| `@check_demo` | 演示模式拦截(禁止写操作) | 写操作(add/update/delete/status/batchDelete) | +| `@operation_log("岗位管理", "查询")` | 操作日志自动记录 | 所有接口 | -**装饰器顺序必须严格遵守**:路由 → 权限。顺序错误会导致功能异常。 +**装饰器顺序必须严格遵守**:路由 → 权限 → 演示模式 → 操作日志。 + +## 导入规范 + +```python +# 模块顶部统一导入 +from src.middleware import check_demo, operation_log +from src.core.permission import check_permission +from src.modules.system.position import service as position +from src.common.utils.request import parse_batch_ids, parse_request_body +from src.core import response as R +``` + +| 导入项 | 来源 | 说明 | +|:----|:----|:----| +| `check_demo` | `src.middleware` | 演示模式装饰器 | +| `operation_log` | `src.middleware` | 操作日志装饰器 | +| `check_permission` | `src.core.permission` | 权限校验装饰器 | +| `service as position` | 业务模块 | Service 层代理函数 | +| `parse_request_body` | `src.common.utils.request` | 解析请求体 JSON | +| `parse_batch_ids` | `src.common.utils.request` | 解析批量删除的 ID 数组 | +| `R` | `src.core` | 统一响应封装 | + +## 请求参数解析 + +```python +# 解析请求体(返回元组:数据, 错误信息) +json_data, err = parse_request_body() +if err: + return R.failed(msg=err) + +# 解析批量删除的ID数组(返回元组:ID字符串, 错误信息) +id_str, err = parse_batch_ids() +if err: + return err +``` + +| 函数 | 作用 | 返回值 | +|:----|:----|:----| +| `parse_request_body()` | 从请求体解析 JSON | `(dict, None)` 或 `(None, error_msg)` | +| `parse_batch_ids()` | 从请求体解析 ID 数组 | `(id_str, None)` 或 `(None, R.failed(...))` | + +## 响应封装 + +Router 层负责调用 `R.ok()` / `R.failed()` 封装响应: + +```python +# 查询类:Service 返回数据,Router 用 R.ok(data=...) 包装 +return R.ok(data=position.get_position_page(request.args)) + +# 写操作类:Service 执行成功后无返回值,Router 返回成功消息 +position.add_position(json_data) +return R.ok(msg="添加成功") + +# 删除类:Service 返回删除数量,Router 格式化消息 +count = position.delete_positions(id) +return R.ok(msg=f"本次共删除{count}条数据") +``` + +:::warning Service 不返回 R 对象 +Service 层入参为数据字典、出参为纯数据(或 None),业务失败通过 `raise BizError` 表达。Router 层负责封装 `R.ok()` / `R.failed()` 响应。 +::: ## 权限字符串命名规范 @@ -148,28 +248,6 @@ sys:{module}:{action} | `batchDelete` | DELETE | 批量删除 | | `status` | PUT | 状态变更 | -## 请求参数解析 - -```python -from src.common.utils.request import parse_request_body, parse_id_list - -# 解析请求体 -data = parse_request_body(request, PositionForm) - -# 解析ID列表 -id_list = parse_id_list(request) -``` - -| 函数 | 作用 | -|:----|:----| -| `parse_request_body(request, FormClass)` | 从请求体解析 JSON 并校验 | -| `parse_id_list(request)` | 从请求体解析 ID 数组 | - -## 响应封装 - -- **Service 返回 R 对象的**(如 `add_position`、`update_position`):直接 `return` -- **Service 返回原始数据的**(如 `get_position_detail`):用 `R.ok(data=...)` 包装 - ## 模块注册 在 `src/routes/v1.py` 中注册模块: @@ -186,13 +264,13 @@ BLUEPRINTS = [ ## 开发要点 -1. **Router 层不含业务逻辑**:仅做路由定义、权限校验和参数绑定 -2. **装饰器顺序**:路由 → 权限 -3. **使用 `parse_request_body` 解析请求体**:自动执行 WTForms 校验 -4. **使用 `parse_id_list` 解析批量操作的 ID 数组** -5. **Service 函数使用延迟导入**:避免循环引用 -6. **权限字符串与代码完全一致**:确保权限校验正确 +1. **Router 层不含业务逻辑**:仅做路由定义、权限校验和参数解析 +2. **装饰器顺序**:路由 → 权限 → 演示模式 → 操作日志 +3. **Service 模块顶部统一导入**:`from src.modules.system.position import service as position` +4. **使用 `parse_request_body()` 解析请求体**:返回 `(data, err)` 元组 +5. **使用 `parse_batch_ids()` 解析批量操作的 ID 数组** +6. **Router 负责封装响应**:Service 不返回 R 对象 ## 总结 -Router 层是 HTTP 请求的入口,负责路由定义、权限校验和参数绑定,不含业务逻辑。装饰器顺序固定为路由 → 权限,使用 `parse_request_body` 解析请求体,使用 `parse_id_list` 解析批量操作的 ID 数组。Service 函数使用延迟导入,避免循环引用。 +Router 层是 HTTP 请求的入口,负责路由定义、权限校验和参数解析,不含业务逻辑。装饰器顺序固定为路由 → 权限 → 演示模式 → 操作日志。Service 模块在文件顶部统一导入,Router 负责调用 `R.ok()` / `R.failed()` 封装响应,Service 不返回 R 对象。 diff --git a/DjangoAdmin_Flask_EleVue/zh/why.md b/DjangoAdmin_Flask_EleVue/zh/why.md index 1134241..90c0234 100644 --- a/DjangoAdmin_Flask_EleVue/zh/why.md +++ b/DjangoAdmin_Flask_EleVue/zh/why.md @@ -44,7 +44,7 @@ router.py → 路由控制器(Blueprint) - **后端三层分离**:Endpoint(HTTP)→ Service(业务)→ Repository(数据),职责清晰 - **基类复用**:BaseRepository / BaseService 提供 30+ 通用方法,子类只需声明差异 - **模块可插拔**:业务模块位于 `src/modules/`,按需增删不影响框架 -- **中间件可扩展**:CORS、限流、日志、事务按需注册,链式处理 +- **中间件可扩展**:CORS、日志、事务按需注册,链式处理 ### 3. 多数据库支持,数据可迁移 @@ -66,7 +66,7 @@ router.py → 路由控制器(Blueprint) - **认证安全**:JWT 令牌 + 黑名单机制,密码 bcrypt 加盐加密 - **登录防护**:图形验证码防暴力破解、失败自动锁定、密码强度校验 -- **接口防护**:Redis 滑动窗口限流、防重复提交(幂等性) +- **接口防护**:防重复提交(幂等性)、WTForms 参数校验 - **文件上传**:扩展名白名单 + 魔术字节校验 + 路径遍历防护 + 高危格式黑名单 - **RBAC 权限**:后端装饰器 + 前端指令双重校验,精确到按钮级,管理员自动绕过 - **数据安全**:软删除机制、敏感字段序列化排除、XSS 清洗