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

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

24 KiB
Raw Permalink Blame History

考培练系统后端质量保证机制

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已更新
- [ ] 部署文档已更新

### 配置
- [ ] 环境变量已配置
- [ ] 数据库迁移已准备
- [ ] 依赖版本已锁定

### 回滚计划
- [ ] 数据库备份已完成
- [ ] 回滚脚本已准备
- [ ] 回滚流程已测试

### 通知
- [ ] 相关团队已通知
- [ ] 维护窗口已安排
- [ ] 监控告警已配置