- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
24 KiB
24 KiB
考培练系统后端质量保证机制
1. 代码质量保证
1.1 自动化代码检查
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
language_version: python3.8
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8
args: ["--max-line-length", "88", "--extend-ignore", "E203"]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0
hooks:
- id: mypy
additional_dependencies: [types-all]
1.2 代码复杂度控制
# setup.cfg
[flake8]
max-line-length = 88
max-complexity = 10 # 圈复杂度限制
extend-ignore = E203, W503
exclude = .git,__pycache__,venv,migrations
[mypy]
python_version = 3.8
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_no_return = True
warn_unreachable = True
strict_equality = True
2. 测试策略
2.1 测试金字塔
/\
/ \ 端到端测试 (10%)
/ \ - 完整业务流程测试
/ \- UI自动化测试
/--------\
/ \ 集成测试 (30%)
/ \- API测试
/ \- 数据库集成测试
/ \- 第三方服务集成测试
/------------------\
单元测试 (60%)
- 业务逻辑测试
- 工具函数测试
- 数据验证测试
2.2 单元测试规范
# tests/unit/test_user_service.py
import pytest
from unittest.mock import Mock, AsyncMock
from app.services.user_service import UserService
from app.models.user import User
from app.schemas.user import UserCreate
class TestUserService:
"""用户服务单元测试"""
@pytest.fixture
def mock_db(self):
"""模拟数据库会话"""
return AsyncMock()
@pytest.fixture
def mock_logger(self):
"""模拟日志器"""
return Mock()
@pytest.fixture
def user_service(self, mock_db, mock_logger):
"""创建用户服务实例"""
return UserService(db=mock_db, logger=mock_logger)
async def test_create_user_success(self, user_service, mock_db):
"""测试成功创建用户"""
# 准备测试数据
user_data = UserCreate(
username="testuser",
email="test@example.com",
password="securepassword"
)
# 配置mock
mock_db.execute.return_value.scalar_one_or_none.return_value = None
mock_db.commit.return_value = None
# 执行测试
result = await user_service.create_user(user_data)
# 验证结果
assert result is not None
assert result.username == user_data.username
assert result.email == user_data.email
mock_db.commit.assert_called_once()
async def test_create_user_duplicate_username(self, user_service, mock_db):
"""测试用户名重复的情况"""
# 准备测试数据
user_data = UserCreate(
username="existinguser",
email="new@example.com",
password="password"
)
# 配置mock - 模拟用户已存在
mock_db.execute.return_value.scalar_one_or_none.return_value = User(
id=1,
username="existinguser"
)
# 执行测试并验证异常
with pytest.raises(ValueError, match="用户名已存在"):
await user_service.create_user(user_data)
# 验证未提交事务
mock_db.commit.assert_not_called()
2.3 集成测试规范
# tests/integration/test_auth_flow.py
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from app.main import app
from app.models.user import User
@pytest.mark.integration
class TestAuthFlow:
"""认证流程集成测试"""
async def test_complete_auth_flow(
self,
async_client: AsyncClient,
db_session: AsyncSession
):
"""测试完整的认证流程"""
# 1. 注册新用户
register_data = {
"username": "newuser",
"email": "newuser@example.com",
"password": "StrongPassword123!"
}
response = await async_client.post(
"/api/v1/auth/register",
json=register_data
)
assert response.status_code == 201
user_data = response.json()["data"]
assert user_data["username"] == register_data["username"]
# 2. 登录
login_data = {
"username": register_data["username"],
"password": register_data["password"]
}
response = await async_client.post(
"/api/v1/auth/login",
json=login_data
)
assert response.status_code == 200
tokens = response.json()["data"]
assert "access_token" in tokens
assert "refresh_token" in tokens
# 3. 访问受保护的端点
headers = {"Authorization": f"Bearer {tokens['access_token']}"}
response = await async_client.get(
"/api/v1/users/me",
headers=headers
)
assert response.status_code == 200
assert response.json()["data"]["username"] == register_data["username"]
# 4. 刷新Token
refresh_data = {"refresh_token": tokens["refresh_token"]}
response = await async_client.post(
"/api/v1/auth/refresh",
json=refresh_data
)
assert response.status_code == 200
new_tokens = response.json()["data"]
assert new_tokens["access_token"] != tokens["access_token"]
2.4 性能测试
# tests/performance/test_api_performance.py
import pytest
import asyncio
import time
from httpx import AsyncClient
from statistics import mean, stdev
@pytest.mark.performance
class TestAPIPerformance:
"""API性能测试"""
async def test_login_performance(self, async_client: AsyncClient):
"""测试登录接口性能"""
login_data = {
"username": "perftest",
"password": "password123"
}
# 预热
for _ in range(5):
await async_client.post("/api/v1/auth/login", json=login_data)
# 性能测试
response_times = []
concurrent_requests = 10
total_requests = 100
async def make_request():
start_time = time.time()
response = await async_client.post(
"/api/v1/auth/login",
json=login_data
)
end_time = time.time()
return end_time - start_time, response.status_code
# 执行并发请求
for _ in range(total_requests // concurrent_requests):
tasks = [make_request() for _ in range(concurrent_requests)]
results = await asyncio.gather(*tasks)
for response_time, status_code in results:
assert status_code == 200
response_times.append(response_time)
# 分析结果
avg_response_time = mean(response_times)
std_response_time = stdev(response_times)
max_response_time = max(response_times)
min_response_time = min(response_times)
# 性能断言
assert avg_response_time < 0.1 # 平均响应时间小于100ms
assert max_response_time < 0.5 # 最大响应时间小于500ms
print(f"\n性能测试结果:")
print(f"平均响应时间: {avg_response_time*1000:.2f}ms")
print(f"标准差: {std_response_time*1000:.2f}ms")
print(f"最小响应时间: {min_response_time*1000:.2f}ms")
print(f"最大响应时间: {max_response_time*1000:.2f}ms")
3. 持续集成/持续部署 (CI/CD)
3.1 GitHub Actions配置
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: Install dependencies
run: |
pip install -r requirements/dev.txt
- name: Run linters
run: |
make lint
make type-check
test:
runs-on: ubuntu-latest
needs: lint
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: testpassword
MYSQL_DATABASE: testdb
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
ports:
- 3306:3306
redis:
image: redis:7
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: Install dependencies
run: |
pip install -r requirements/dev.txt
- name: Run migrations
env:
DATABASE_URL: mysql+aiomysql://root:testpassword@localhost:3306/testdb
run: |
alembic upgrade head
- name: Run tests
env:
DATABASE_URL: mysql+aiomysql://root:testpassword@localhost:3306/testdb
REDIS_URL: redis://localhost:6379/0
run: |
pytest tests/ -v --cov=app --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: true
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run security checks
uses: pyupio/safety@v1
with:
api-key: ${{ secrets.SAFETY_API_KEY }}
- name: Run Bandit
run: |
pip install bandit
bandit -r app/ -f json -o bandit-report.json
- name: Upload security report
uses: actions/upload-artifact@v3
with:
name: security-reports
path: bandit-report.json
3.2 代码覆盖率配置
# .coveragerc
[run]
source = app
omit =
*/tests/*
*/migrations/*
*/__init__.py
*/config/*
[report]
precision = 2
show_missing = True
skip_covered = False
[html]
directory = htmlcov
[xml]
output = coverage.xml
4. 代码审查流程
4.1 Pull Request模板
<!-- .github/pull_request_template.md -->
## 描述
简要描述这个PR的目的和所做的更改。
## 更改类型
- [ ] Bug修复
- [ ] 新功能
- [ ] 性能优化
- [ ] 代码重构
- [ ] 文档更新
- [ ] 测试更新
## 检查清单
- [ ] 代码符合项目的编码规范
- [ ] 已添加/更新相关测试
- [ ] 所有测试通过
- [ ] 已更新相关文档
- [ ] 代码已经过自我审查
- [ ] 没有注释掉的代码
- [ ] 没有console.log或print调试语句
## 测试说明
描述如何测试这些更改。
## 相关Issue
Closes #(issue号)
## 截图(如适用)
如果有UI更改,请添加截图。
4.2 代码审查标准
# scripts/code_review_checklist.py
"""代码审查检查工具"""
import ast
import sys
from pathlib import Path
from typing import List, Dict
class CodeReviewChecker(ast.NodeVisitor):
"""代码审查自动检查器"""
def __init__(self):
self.issues = []
self.current_file = None
def check_file(self, file_path: Path) -> List[Dict[str, any]]:
"""检查单个文件"""
self.current_file = file_path
self.issues = []
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 检查文件级别的问题
self._check_file_issues(content)
# 解析AST并检查
try:
tree = ast.parse(content)
self.visit(tree)
except SyntaxError as e:
self.issues.append({
"type": "syntax_error",
"line": e.lineno,
"message": str(e)
})
return self.issues
def _check_file_issues(self, content: str):
"""检查文件级别的问题"""
lines = content.split('\n')
for i, line in enumerate(lines, 1):
# 检查行长度
if len(line) > 88:
self.issues.append({
"type": "line_too_long",
"line": i,
"message": f"行长度 {len(line)} 超过88字符"
})
# 检查TODO/FIXME
if 'TODO' in line or 'FIXME' in line:
self.issues.append({
"type": "todo_found",
"line": i,
"message": "发现待处理的TODO/FIXME"
})
# 检查print语句
if 'print(' in line and not line.strip().startswith('#'):
self.issues.append({
"type": "print_statement",
"line": i,
"message": "使用了print语句,应使用日志"
})
def visit_FunctionDef(self, node):
"""检查函数定义"""
# 检查函数复杂度
complexity = self._calculate_complexity(node)
if complexity > 10:
self.issues.append({
"type": "high_complexity",
"line": node.lineno,
"message": f"函数 {node.name} 复杂度 {complexity} 过高"
})
# 检查是否有文档字符串
if not ast.get_docstring(node):
self.issues.append({
"type": "missing_docstring",
"line": node.lineno,
"message": f"函数 {node.name} 缺少文档字符串"
})
self.generic_visit(node)
def _calculate_complexity(self, node) -> int:
"""计算圈复杂度"""
complexity = 1
for child in ast.walk(node):
if isinstance(child, (ast.If, ast.While, ast.For)):
complexity += 1
elif isinstance(child, ast.ExceptHandler):
complexity += 1
return complexity
def main():
"""主函数"""
checker = CodeReviewChecker()
# 检查所有Python文件
app_dir = Path("app")
all_issues = []
for py_file in app_dir.rglob("*.py"):
issues = checker.check_file(py_file)
if issues:
all_issues.append({
"file": str(py_file),
"issues": issues
})
# 输出结果
if all_issues:
print("代码审查发现以下问题:")
for file_issues in all_issues:
print(f"\n文件: {file_issues['file']}")
for issue in file_issues['issues']:
print(f" 行 {issue['line']}: [{issue['type']}] {issue['message']}")
sys.exit(1)
else:
print("代码审查通过!")
if __name__ == "__main__":
main()
5. 安全保证
5.1 安全扫描配置
# .github/workflows/security.yml
name: Security Scan
on:
schedule:
- cron: '0 0 * * *' # 每天运行
push:
branches: [main]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
- name: OWASP Dependency Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'kaopeilian-backend'
path: '.'
format: 'ALL'
5.2 安全最佳实践检查
# app/core/security_checks.py
"""运行时安全检查"""
import os
import re
from typing import List, Dict
from pathlib import Path
class SecurityChecker:
"""安全检查器"""
@staticmethod
def check_env_variables() -> List[str]:
"""检查环境变量安全性"""
issues = []
# 检查敏感配置
sensitive_vars = [
'SECRET_KEY',
'DATABASE_URL',
'COZE_API_TOKEN',
'DIFY_API_KEY'
]
for var in sensitive_vars:
value = os.getenv(var)
if not value:
issues.append(f"缺少必要的环境变量: {var}")
elif var == 'SECRET_KEY' and len(value) < 32:
issues.append("SECRET_KEY 长度不足32字符")
return issues
@staticmethod
def check_sql_injection_patterns(query: str) -> bool:
"""检查SQL注入模式"""
dangerous_patterns = [
r';\s*DROP\s+TABLE',
r';\s*DELETE\s+FROM',
r'UNION\s+SELECT',
r'OR\s+1\s*=\s*1',
r'--\s*$'
]
for pattern in dangerous_patterns:
if re.search(pattern, query, re.IGNORECASE):
return True
return False
@staticmethod
def check_file_upload(filename: str, content: bytes) -> Dict[str, any]:
"""检查文件上传安全性"""
issues = []
# 检查文件扩展名
allowed_extensions = {'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.png', '.jpg', '.jpeg'}
ext = Path(filename).suffix.lower()
if ext not in allowed_extensions:
issues.append(f"不允许的文件类型: {ext}")
# 检查文件大小
max_size = 10 * 1024 * 1024 # 10MB
if len(content) > max_size:
issues.append(f"文件大小超过限制: {len(content)/1024/1024:.2f}MB > 10MB")
# 检查文件内容魔术字节
magic_bytes = {
b'\x89PNG': '.png',
b'\xFF\xD8\xFF': '.jpg',
b'%PDF': '.pdf'
}
file_type_valid = False
for magic, expected_ext in magic_bytes.items():
if content.startswith(magic) and ext == expected_ext:
file_type_valid = True
break
if not file_type_valid and ext in {'.png', '.jpg', '.pdf'}:
issues.append("文件内容与扩展名不匹配")
return {
"valid": len(issues) == 0,
"issues": issues
}
6. 监控与告警
6.1 健康检查端点
# app/api/v1/health.py
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_db
from app.services.ai.coze.client import CozeClient
from app.services.ai.dify.client import DifyClient
import redis.asyncio as redis
from datetime import datetime
router = APIRouter()
@router.get("/health")
async def health_check():
"""基础健康检查"""
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"version": "1.0.0"
}
@router.get("/health/detailed")
async def detailed_health_check(
db: AsyncSession = Depends(get_db)
):
"""详细健康检查"""
health_status = {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"checks": {}
}
# 检查数据库
try:
await db.execute("SELECT 1")
health_status["checks"]["database"] = {
"status": "up",
"response_time_ms": 5
}
except Exception as e:
health_status["status"] = "unhealthy"
health_status["checks"]["database"] = {
"status": "down",
"error": str(e)
}
# 检查Redis
try:
r = redis.from_url("redis://localhost:6379")
await r.ping()
health_status["checks"]["redis"] = {
"status": "up",
"response_time_ms": 2
}
except Exception as e:
health_status["status"] = "degraded"
health_status["checks"]["redis"] = {
"status": "down",
"error": str(e)
}
# 检查Coze连接
try:
coze = CozeClient()
# 执行简单的API调用测试
health_status["checks"]["coze"] = {
"status": "up",
"response_time_ms": 50
}
except Exception as e:
health_status["status"] = "degraded"
health_status["checks"]["coze"] = {
"status": "down",
"error": str(e)
}
return health_status
6.2 性能监控指标
# app/core/metrics.py
from prometheus_client import Counter, Histogram, Gauge, generate_latest
from functools import wraps
import time
# 定义指标
http_requests_total = Counter(
'http_requests_total',
'Total HTTP requests',
['method', 'endpoint', 'status']
)
http_request_duration_seconds = Histogram(
'http_request_duration_seconds',
'HTTP request duration',
['method', 'endpoint']
)
active_connections = Gauge(
'active_connections',
'Number of active connections'
)
ai_api_calls_total = Counter(
'ai_api_calls_total',
'Total AI API calls',
['platform', 'operation', 'status']
)
def track_request_metrics(endpoint: str):
"""请求指标追踪装饰器"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
start_time = time.time()
method = kwargs.get('request', args[0]).method
try:
response = await func(*args, **kwargs)
status = response.status_code
http_requests_total.labels(
method=method,
endpoint=endpoint,
status=status
).inc()
return response
finally:
duration = time.time() - start_time
http_request_duration_seconds.labels(
method=method,
endpoint=endpoint
).observe(duration)
return wrapper
return decorator
7. 发布管理
7.1 版本控制策略
# 版本号格式:MAJOR.MINOR.PATCH
# MAJOR: 不兼容的API更改
# MINOR: 向后兼容的功能添加
# PATCH: 向后兼容的错误修复
# 自动版本号管理脚本
#!/bin/bash
# scripts/bump_version.sh
CURRENT_VERSION=$(cat VERSION)
IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION"
case $1 in
major)
NEW_VERSION="$((VERSION_PARTS[0] + 1)).0.0"
;;
minor)
NEW_VERSION="${VERSION_PARTS[0]}.$((VERSION_PARTS[1] + 1)).0"
;;
patch)
NEW_VERSION="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.$((VERSION_PARTS[2] + 1))"
;;
*)
echo "Usage: $0 {major|minor|patch}"
exit 1
;;
esac
echo $NEW_VERSION > VERSION
echo "Version bumped from $CURRENT_VERSION to $NEW_VERSION"
7.2 发布检查清单
## 发布前检查清单
### 代码质量
- [ ] 所有测试通过
- [ ] 代码覆盖率达标(>80%)
- [ ] 无安全漏洞警告
- [ ] 性能测试通过
### 文档
- [ ] API文档已更新
- [ ] CHANGELOG已更新
- [ ] README已更新
- [ ] 部署文档已更新
### 配置
- [ ] 环境变量已配置
- [ ] 数据库迁移已准备
- [ ] 依赖版本已锁定
### 回滚计划
- [ ] 数据库备份已完成
- [ ] 回滚脚本已准备
- [ ] 回滚流程已测试
### 通知
- [ ] 相关团队已通知
- [ ] 维护窗口已安排
- [ ] 监控告警已配置