Files
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

926 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 考培练系统后端质量保证机制
## 1. 代码质量保证
### 1.1 自动化代码检查
```yaml
# .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 代码复杂度控制
```python
# 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 单元测试规范
```python
# 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 集成测试规范
```python
# 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 性能测试
```python
# 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配置
```yaml
# .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 代码覆盖率配置
```ini
# .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模板
```markdown
<!-- .github/pull_request_template.md -->
## 描述
简要描述这个PR的目的和所做的更改。
## 更改类型
- [ ] Bug修复
- [ ] 新功能
- [ ] 性能优化
- [ ] 代码重构
- [ ] 文档更新
- [ ] 测试更新
## 检查清单
- [ ] 代码符合项目的编码规范
- [ ] 已添加/更新相关测试
- [ ] 所有测试通过
- [ ] 已更新相关文档
- [ ] 代码已经过自我审查
- [ ] 没有注释掉的代码
- [ ] 没有console.log或print调试语句
## 测试说明
描述如何测试这些更改。
## 相关Issue
Closes #(issue号)
## 截图(如适用)
如果有UI更改请添加截图。
```
### 4.2 代码审查标准
```python
# 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 安全扫描配置
```yaml
# .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 安全最佳实践检查
```python
# 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 健康检查端点
```python
# 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 性能监控指标
```python
# 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 版本控制策略
```bash
# 版本号格式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 发布检查清单
```markdown
## 发布前检查清单
### 代码质量
- [ ] 所有测试通过
- [ ] 代码覆盖率达标(>80%
- [ ] 无安全漏洞警告
- [ ] 性能测试通过
### 文档
- [ ] API文档已更新
- [ ] CHANGELOG已更新
- [ ] README已更新
- [ ] 部署文档已更新
### 配置
- [ ] 环境变量已配置
- [ ] 数据库迁移已准备
- [ ] 依赖版本已锁定
### 回滚计划
- [ ] 数据库备份已完成
- [ ] 回滚脚本已准备
- [ ] 回滚流程已测试
### 通知
- [ ] 相关团队已通知
- [ ] 维护窗口已安排
- [ ] 监控告警已配置
```