From 8691412ec65facef0972b8428b30a800870270f2 Mon Sep 17 00:00:00 2001 From: DjangoAdmin Dev Date: Fri, 7 Aug 2026 12:14:51 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20P1=20=E5=B7=A5=E7=A8=8B=E5=8C=96?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pyproject.toml:pytest/black/isort/mypy 统一配置 - requirements-dev.txt:测试与代码规范开发依赖 - .flake8 / .editorconfig:PEP8 与编辑器统一 - .pre-commit-config.yaml:提交前钩子 - Makefile:常用命令集 - .env.example:环境变量模板(76 键) - tests/:pytest 骨架 + R 响应格式冒烟测试 Co-Authored-By: Claude --- .editorconfig | 27 ++++++ .env.example | 125 ++++++++++++++++++++++++++ .flake8 | 31 +++++++ .pre-commit-config.yaml | 39 ++++++++ Makefile | 69 ++++++++++++++ pyproject.toml | 63 +++++++++++++ requirements-dev.txt | 19 ++++ tests/__init__.py | 1 + tests/conftest.py | 19 ++++ tests/unit/__init__.py | 1 + tests/unit/test_core/__init__.py | 1 + tests/unit/test_core/test_response.py | 53 +++++++++++ 12 files changed, 448 insertions(+) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml create mode 100644 Makefile create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/test_core/__init__.py create mode 100644 tests/unit/test_core/test_response.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3eedbb7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +# ============================================================ +# 编辑器统一配置 +# ============================================================ +root = true + +[*] +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.py] +indent_size = 4 +max_line_length = 120 + +[*.{yml,yaml}] +indent_size = 2 + +[*.json] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..df0de9b --- /dev/null +++ b/.env.example @@ -0,0 +1,125 @@ +# ============================================================ +# DjangoAdmin Flask+EleVue 环境变量模板 +# ============================================================ +# 使用方法: +# 1. 复制为 .env:cp .env.example .env +# 2. 填写 <占位符> 标记的敏感值(严禁将真实 .env 提交到版本库) +# 说明:.env 已被 .gitignore 忽略 +# ============================================================ + +# ============================================================ +# 应用基础配置(config/app.py) +# ============================================================ +FLASK_NAME=Flask+EleVue旗舰版 +FLASK_VERSION=v2.8.0 +FLASK_APP=app.py +FLASK_ENV=development # development / production +FLASK_DEBUG=True # 生产环境必须为 False +FLASK_USE_RELOADER=False +FLASK_HOST=127.0.0.1 +FLASK_PORT=8021 +FLASK_SECRET_KEY=<必填,长度至少16字符,如生成随机串> +FLASK_DEMO=False # True=演示模式,拦截写操作 +# 站点域名(定时任务等内部 HTTP 调用使用) +# FLASK_SITE_DOMAIN=http://127.0.0.1:8021 + +# 跨域白名单(多个用英文逗号分隔;配置后 CORS 允许携带凭证) +CORS_ALLOWED_ORIGINS=http://localhost:8001 + +# 上传配置 +FLASK_UPLOAD_DIR=./uploads +FLASK_FILE_URL=http://images.flask.elevue +UPLOAD_ALLOWED_EXTS=.jpg,.jpeg,.png,.gif,.bmp,.webp,.svg,.ico,.txt,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4,.zip,.rar,.7z +UPLOAD_MAX_SIZE_MB=10 + +# 反向代理层数(0=无代理;1=前面有 1 层 Nginx) +PROXY_NUM=0 + +# ============================================================ +# 数据库配置(config/database.py) +# ============================================================ +DB_DRIVER=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=djangoadmin.flask.elevue +DB_USERNAME=root +DB_PASSWORD=<必填> +DB_PREFIX=flask_ +DB_DEBUG=True +DB_SSL=False +# 若开启 DB_SSL=True,配置以下证书路径 +# DB_SSL_CA= +# DB_SSL_CERT= +# DB_SSL_KEY= + +# ============================================================ +# Redis 缓存(config/redis.py) +# ============================================================ +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +REDIS_PASSWORD=<如有则填写,否则留空> +REDIS_INDEX=0 +REDIS_AUTH=True # 是否启用密码认证 +REDIS_DECODE_RESPONSES=True + +# ============================================================ +# JWT / 权限 / 白名单(config/auth.py) +# ============================================================ +JWT_SALT=<必填,JWT 签名密钥,建议随机长串> +JWT_EXPIRE_MINUTES=20 +LOGIN_FAIL_MAX=5 +LOGIN_LOCK_SECONDS=300 +PERMISSION_ENABLED=True # 权限校验总开关(开发可关) + +# 内部调用密钥(定时任务等无用户场景,通过 X-Scheduler-Key / X-Internal-API-Key 传递) +INTERNAL_API_KEY=<必填> +INTERNAL_ALLOWED_IPS=127.0.0.1 + +# 登录白名单(以下路径跳过登录校验) +LOGIN_WHITELIST_EXACT=/login,/captcha,/favicon.ico +LOGIN_WHITELIST_PREFIX=/job/sendSms,/public,/static,/api/webhook +LOGIN_WHITELIST_WILDCARD=/job/*/send*,/api/v1/*/public* +LOGIN_WHITELIST_DEBUG=True +LOGIN_WHITELIST_LOG_ACCESS=True + +# 验证码万能码(仅 FLASK_DEMO=True 时生效) +CAPTCHA_BYPASS_CODE= + +# ============================================================ +# 邮件(config/mail.py) +# ============================================================ +MAIL_MAILER=smtp +MAIL_SERVER=smtp.163.com +MAIL_PORT=465 +MAIL_USE_SSL=True +MAIL_ENCRYPTION= +MAIL_USERNAME=<必填> +MAIL_PASSWORD=<必填> +MAIL_FROM_NAME= +MAIL_FROM_ADDRESS= + +# ============================================================ +# 短信(config/sms.py,阿里云短信) +# ============================================================ +SMS_PROVIDER=aliyun +SMS_ALIYUN_ACCESS_KEY_ID=<必填> +SMS_ALIYUN_ACCESS_KEY_SECRET=<必填> +SMS_ALIYUN_SIGN_NAME=<必填> +SMS_ALIYUN_ENDPOINT=dysmsapi.aliyuncs.com +SMS_ALIYUN_CONNECT_TIMEOUT=10000 +SMS_ALIYUN_READ_TIMEOUT=10000 + +# ============================================================ +# 验证码图片(config/captcha.py) +# ============================================================ +CAPTCHA_LENGTH=6 +CAPTCHA_EXPIRE=300 +CAPTCHA_RATE_LIMIT=10 +CAPTCHA_RATE_WINDOW=60 +CAPTCHA_CHARS=ABCDEFGHJKLMNPQRSTUVWXYZ2346789 +CAPTCHA_FONT_PATH=static/fonts/Vera.ttf +CAPTCHA_IMAGE_WIDTH=160 +CAPTCHA_IMAGE_HEIGHT=50 +CAPTCHA_FONT_SIZE=28 +CAPTCHA_NOISE_LINES=6 +CAPTCHA_NOISE_DOTS=30 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..429afa5 --- /dev/null +++ b/.flake8 @@ -0,0 +1,31 @@ +# ============================================================ +# flake8 配置(PEP8 检查) +# 与 black(line-length=120) / isort(profile=black) 保持协调 +# ============================================================ +[flake8] +# 行宽:与 black 一致;中文注释/长 URL 场景已由 black 统一管理格式 +max-line-length = 120 +# 圈复杂度上限(存量服务函数普遍偏高,新代码应低于此值) +max-complexity = 20 + +# 排除目录 +exclude = + .git, + __pycache__, + .venv, + venv, + migrations, + static, + uploads, + ui, + document, + .idea, + .claude, + build, + dist + +# 与 black/isort 冲突项(必须忽略,否则与自动格式化互相打架) +extend-ignore = + E203, # 切片冒号前空格(black 风格) + W503, # 二元运算符换行(black 风格) + E501, # 行过长(交由 black 处理) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3e4118a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +# ============================================================ +# pre-commit 钩子配置 +# 安装:pip install -r requirements-dev.txt && pre-commit install +# 手动全量运行:pre-commit run --all-files +# ============================================================ +repos: + # ---- 通用钩子 ---- + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-merge-conflict + - id: debug-statements + - id: detect-private-key + # ip2region.xdb 约 10MB,放行大文件 + - id: check-added-large-files + args: ["--maxkb=12000"] + + # ---- Python 格式化:black ---- + - repo: https://github.com/psf/black + rev: 24.10.0 + hooks: + - id: black + args: ["--line-length", "120"] + + # ---- import 排序:isort ---- + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + + # ---- 静态检查:flake8 ---- + - repo: https://github.com/PyCQA/flake8 + rev: 7.1.1 + hooks: + - id: flake8 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..60dc19d --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +# ============================================================ +# DjangoAdmin Flask+EleVue 常用命令集 +# 用法:make help / make lint / make test / ... +# ============================================================ + +PYTHON ?= python +PIP ?= $(PYTHON) -m pip +FLASK ?= $(PYTHON) -m flask + +.PHONY: help install install-dev dev lint format type-check test test-cov \ + db-migrate db-upgrade db-downgrade docker-up docker-down clean + +help: ## 显示全部命令帮助 + @grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | sort | \ + awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' + +# ---- 依赖 ---- +install: ## 安装生产依赖 + $(PIP) install -r requirements.txt + +install-dev: ## 安装开发依赖(含生产依赖 + 测试/规范工具) + $(PIP) install -r requirements-dev.txt + +# ---- 运行 ---- +dev: ## 启动开发服务器(127.0.0.1:8021) + $(PYTHON) app.py + +# ---- 代码质量 ---- +lint: ## 静态检查(flake8 + black + isort) + flake8 application utils extensions config src tests scripts + black --check --line-length 120 application utils extensions config src tests scripts + isort --check-only application utils extensions config src tests scripts + +format: ## 自动格式化(black + isort) + black --line-length 120 application utils extensions config src tests scripts + isort application utils extensions config src tests scripts + +type-check: ## 类型检查(mypy) + mypy application src + +# ---- 测试 ---- +test: ## 运行全部测试 + pytest + +test-cov: ## 带覆盖率运行测试 + pytest --cov=application --cov-report=term-missing + +# ---- 数据库迁移 ---- +db-migrate: ## 生成迁移脚本(模型变更后) + $(FLASK) db migrate + +db-upgrade: ## 应用迁移到最新 + $(FLASK) db upgrade + +db-downgrade: ## 回滚一次迁移 + $(FLASK) db downgrade + +# ---- Docker ---- +docker-up: ## 启动 Docker 服务 + cd docker && docker-compose up -d + +docker-down: ## 停止 Docker 服务 + cd docker && docker-compose down + +# ---- 清理 ---- +clean: ## 清理缓存与临时文件 + find . -type d -name __pycache__ -not -path "*/ui/*" -exec rm -rf {} + + find . -type f -name "*.pyc" -not -path "*/ui/*" -delete + rm -rf .pytest_cache .mypy_cache diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7877192 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,63 @@ +# ============================================================ +# DjangoAdmin Flask+EleVue 项目管理配置 +# ============================================================ +# 说明: +# 1. 本项目为应用工程(非库),运行依赖由 requirements.txt 管理, +# 此处不重复声明(与既有部署约定保持一致)。 +# 2. 本文件承载各开发工具(pytest/black/isort/mypy)统一配置。 + +[project] +name = "djangoadmin-flask-elevue" +version = "3.0.0" +description = "DjangoAdmin Flask+EleVue 企业数字化开发平台(后端)" +readme = "README.md" +requires-python = ">=3.9" +license = {text = "Apache-2.0"} +authors = [ + {name = "一米阳光团队", email = "djangoadmin@126.com"}, +] +dependencies = [] + +# ============================================================ +# pytest +# ============================================================ +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-ra --strict-markers" +python_files = ["test_*.py"] +filterwarnings = [ + # 存量依赖(Flask-SQLAlchemy 3.x 等)DeprecationWarning 噪音较多,暂统一忽略 + "ignore::DeprecationWarning", +] + +# ============================================================ +# black(代码格式化) +# ============================================================ +[tool.black] +line-length = 120 +target-version = ["py39", "py310", "py311", "py312"] +skip-string-normalization = false + +# ============================================================ +# isort(import 排序) +# ============================================================ +[tool.isort] +profile = "black" +line_length = 120 +skip_gitignore = true +known_first_party = ["application", "config", "extensions", "src", "utils"] + +# ============================================================ +# mypy(类型检查) +# ============================================================ +[tool.mypy] +python_version = "3.9" +ignore_missing_imports = true +warn_unused_configs = true +exclude = ["migrations/", "ui/", "static/"] + +# ============================================================ +# coverage(pytest-cov) +# ============================================================ +[tool.coverage.run] +omit = ["migrations/*", "ui/*", "static/*", "tests/*", "scripts/*"] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..eca4321 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,19 @@ +# ============================================================ +# 开发环境依赖 +# 安装:pip install -r requirements-dev.txt +# ============================================================ +# 先引入全部生产依赖(保证开发环境与生产一致) +-r requirements.txt + +# ---- 测试 ---- +pytest~=8.0 +pytest-cov~=6.0 + +# ---- 代码质量 ---- +flake8~=7.0 +black~=24.0 +isort~=5.13 +mypy~=1.14 + +# ---- 工程化 ---- +pre-commit~=3.8 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e3038a8 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""测试包:pytest 通过此包将项目根目录加入 sys.path。""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7a6298f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +""" +pytest 全局配置 +============================================================= + +P1 阶段仅搭建骨架,保证 `pytest` 可空跑。 +测试 App 构建、数据库/Redis fixture 在 P8(测试完善阶段)补充: +- app fixture:create_app() + 测试库 DATABASE_URL 覆盖 +- db fixture:事务内建表/清库 +- client fixture:Flask test_client +""" + +import os +import sys + +# 项目根目录加入 sys.path(tests/ 已有 __init__.py 时 pytest 通常自动处理, +# 此处显式补充,兼容从任意目录启动 pytest 的场景) +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if ROOT not in sys.path: + sys.path.insert(0, ROOT) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..128173b --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1 @@ +"""单元测试包。""" diff --git a/tests/unit/test_core/__init__.py b/tests/unit/test_core/__init__.py new file mode 100644 index 0000000..7249ac0 --- /dev/null +++ b/tests/unit/test_core/__init__.py @@ -0,0 +1 @@ +"""core 层单元测试。""" diff --git a/tests/unit/test_core/test_response.py b/tests/unit/test_core/test_response.py new file mode 100644 index 0000000..8081ecf --- /dev/null +++ b/tests/unit/test_core/test_response.py @@ -0,0 +1,53 @@ +""" +R 统一响应格式单元测试 + +验证 `R.ok()` / `R.failed()` 返回的 JSON 响应结构符合前后端约定: +{code, data, msg, ok}(code=0 成功,非 0 失败)。 +""" + +from utils import R + + +class TestOk: + """成功响应""" + + def test_ok_default_format(self): + resp = R.ok() + body = resp.get_json() + + assert body["code"] == 0 + assert body["ok"] is True + assert body["data"] is None + assert body["msg"] + + def test_ok_with_data(self): + resp = R.ok(data={"id": 1, "name": "张三"}) + body = resp.get_json() + + assert body["code"] == 0 + assert body["data"] == {"id": 1, "name": "张三"} + + def test_ok_with_kwargs(self): + resp = R.ok(data=[], total=100) + body = resp.get_json() + + assert body["total"] == 100 + + +class TestFailed: + """失败响应""" + + def test_failed_default_format(self): + resp = R.failed() + body = resp.get_json() + + assert body["code"] != 0 + assert body["ok"] is False + assert body["data"] is None + + def test_failed_custom_code(self): + resp = R.failed(msg="未授权访问", code=401) + body = resp.get_json() + + assert body["code"] == 401 + assert body["msg"] == "未授权访问"