chore: P1 工程化工具链
- 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
15f2a5e097
commit
8691412ec6
@@ -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
|
||||||
+125
@@ -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
|
||||||
@@ -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 处理)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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/*"]
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""测试包:pytest 通过此包将项目根目录加入 sys.path。"""
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""单元测试包。"""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""core 层单元测试。"""
|
||||||
@@ -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"] == "未授权访问"
|
||||||
Reference in New Issue
Block a user