电商平台用户系统API设计

目录

  • 电商平台用户系统API设计:构建高可用、安全的用户服务
    • [1. 引言](#1. 引言)
    • [2. 需求分析与功能规划](#2. 需求分析与功能规划)
      • [2.1 核心功能需求](#2.1 核心功能需求)
      • [2.2 非功能需求](#2.2 非功能需求)
    • [3. 系统架构设计](#3. 系统架构设计)
      • [3.1 整体架构](#3.1 整体架构)
      • [3.2 技术栈选择](#3.2 技术栈选择)
    • [4. 数据模型设计](#4. 数据模型设计)
      • [4.1 实体关系图](#4.1 实体关系图)
      • [4.2 核心数据表设计](#4.2 核心数据表设计)
        • [4.2.1 用户表 (users)](#4.2.1 用户表 (users))
    • [5. API接口设计](#5. API接口设计)
      • [5.1 RESTful API设计原则](#5.1 RESTful API设计原则)
      • [5.2 API端点设计](#5.2 API端点设计)
        • [5.2.1 认证相关API](#5.2.1 认证相关API)
        • [5.2.2 用户管理API](#5.2.2 用户管理API)
    • [6. 安全设计与实现](#6. 安全设计与实现)
      • [6.1 密码安全策略](#6.1 密码安全策略)
      • [6.2 JWT令牌机制](#6.2 JWT令牌机制)
      • [6.3 限流与防刷机制](#6.3 限流与防刷机制)
    • [7. 性能优化策略](#7. 性能优化策略)
      • [7.1 缓存策略](#7.1 缓存策略)
      • [7.2 数据库优化](#7.2 数据库优化)
    • [8. 完整代码实现](#8. 完整代码实现)
      • [8.1 项目结构](#8.1 项目结构)
      • [8.2 核心代码实现](#8.2 核心代码实现)
        • [8.2.1 配置管理 (app/config.py)](#8.2.1 配置管理 (app/config.py))
        • [8.2.2 数据库模型 (app/models/user.py)](#8.2.2 数据库模型 (app/models/user.py))
        • [8.2.3 Pydantic模型 (app/schemas/user.py)](#8.2.3 Pydantic模型 (app/schemas/user.py))
        • [8.2.4 安全模块 (app/core/security.py)](#8.2.4 安全模块 (app/core/security.py))
        • [8.2.5 认证路由 (app/api/v1/auth.py)](#8.2.5 认证路由 (app/api/v1/auth.py))
        • [8.2.6 用户管理路由 (app/api/v1/users.py)](#8.2.6 用户管理路由 (app/api/v1/users.py))
        • [8.2.7 主应用文件 (app/main.py)](#8.2.7 主应用文件 (app/main.py))
    • [9. 测试策略](#9. 测试策略)
      • [9.1 单元测试](#9.1 单元测试)
      • [9.2 性能测试](#9.2 性能测试)
    • [10. 部署与监控](#10. 部署与监控)
      • [10.1 Docker部署](#10.1 Docker部署)
      • [10.2 监控指标](#10.2 监控指标)
    • [11. 代码自查与优化](#11. 代码自查与优化)
      • [11.1 代码自查清单](#11.1 代码自查清单)
      • [11.2 已知问题与改进方向](#11.2 已知问题与改进方向)
    • [12. 总结](#12. 总结)

电商平台用户系统API设计:构建高可用、安全的用户服务

1. 引言

在当今数字化时代,电商平台的核心竞争力很大程度上依赖于其用户系统的健壮性和用户体验。用户系统不仅是用户身份验证和管理的基石,更是实现个性化推荐、精准营销、订单管理等高级功能的基础。一个设计良好的用户系统API能够显著提升平台的可扩展性、安全性和开发效率。

本博客将深入探讨电商平台用户系统的API设计,涵盖从需求分析、架构设计到具体实现的完整流程。我们将使用Python作为主要开发语言,结合FastAPI框架构建高性能的RESTful API,并详细讨论安全策略、数据模型设计、性能优化等关键问题。

2. 需求分析与功能规划

2.1 核心功能需求

一个完整的电商用户系统应包含以下核心功能:

  1. 用户注册与认证:支持多种注册方式(邮箱、手机号、第三方登录)
  2. 用户信息管理:个人资料的增删改查
  3. 安全机制:密码加密、JWT令牌、验证码、二次验证
  4. 权限管理:角色和权限控制
  5. 地址管理:用户收货地址的CRUD操作
  6. 会话管理:登录状态、设备管理
  7. 用户统计:用户行为分析、活跃度统计

2.2 非功能需求

  1. 性能:API响应时间应小于200ms
  2. 可用性:系统可用性目标99.9%
  3. 安全性:符合OWASP安全标准
  4. 可扩展性:支持千万级用户规模
  5. 兼容性:支持Web、移动端等多平台

3. 系统架构设计

3.1 整体架构

客户端

Web/移动端
API网关
负载均衡器
用户服务
认证服务
地址服务
用户数据库
令牌存储
地址数据库
缓存层 Redis
消息队列
分析服务
数据仓库

3.2 技术栈选择

  • 后端框架: FastAPI (高性能,自动生成API文档)
  • 数据库: PostgreSQL (主数据存储) + Redis (缓存和会话)
  • 消息队列: RabbitMQ/Kafka (异步任务)
  • 容器化: Docker + Kubernetes
  • 监控: Prometheus + Grafana
  • 日志: ELK Stack

4. 数据模型设计

4.1 实体关系图

has
maintains
assigned
has
records
USERS
uuid
id
PK
string
username
UK
string
email
UK
string
phone
UK
string
password_hash
boolean
is_active
boolean
is_verified
timestamp
created_at
timestamp
updated_at
timestamp
last_login
USER_ADDRESSES
uuid
id
PK
uuid
user_id
FK
string
recipient_name
string
phone
string
province
string
city
string
district
string
detail_address
string
postal_code
boolean
is_default
USER_SESSIONS
uuid
id
PK
uuid
user_id
FK
string
session_token
string
device_info
string
ip_address
timestamp
login_at
timestamp
expires_at
boolean
is_active
USER_ROLES
ROLES
ROLE_PERMISSIONS
LOGIN_HISTORY

4.2 核心数据表设计

4.2.1 用户表 (users)
字段名 类型 说明
id UUID 主键
username VARCHAR(50) 用户名,唯一
email VARCHAR(255) 邮箱,唯一
phone VARCHAR(20) 手机号,唯一
password_hash VARCHAR(255) 加密后的密码
is_active BOOLEAN 账户是否激活
is_verified BOOLEAN 邮箱/手机是否验证
last_login TIMESTAMP 最后登录时间
created_at TIMESTAMP 创建时间
updated_at TIMESTAMP 更新时间

5. API接口设计

5.1 RESTful API设计原则

我们遵循以下RESTful设计原则:

  1. 资源导向:使用名词而不是动词
  2. HTTP方法对应CRUD操作:
    • GET: 获取资源
    • POST: 创建资源
    • PUT: 更新资源
    • DELETE: 删除资源
  3. 使用合适的HTTP状态码
  4. 版本控制:API版本包含在URL中

5.2 API端点设计

5.2.1 认证相关API
复制代码
POST   /api/v1/auth/register       # 用户注册
POST   /api/v1/auth/login          # 用户登录
POST   /api/v1/auth/logout         # 用户登出
POST   /api/v1/auth/refresh        # 刷新令牌
POST   /api/v1/auth/verify-email   # 邮箱验证
POST   /api/v1/auth/forgot-password # 忘记密码
POST   /api/v1/auth/reset-password  # 重置密码
5.2.2 用户管理API
复制代码
GET    /api/v1/users/{user_id}     # 获取用户信息
PUT    /api/v1/users/{user_id}     # 更新用户信息
DELETE /api/v1/users/{user_id}     # 删除用户(软删除)

GET    /api/v1/users/{user_id}/addresses      # 获取地址列表
POST   /api/v1/users/{user_id}/addresses      # 添加地址
PUT    /api/v1/users/{user_id}/addresses/{address_id} # 更新地址
DELETE /api/v1/users/{user_id}/addresses/{address_id} # 删除地址

6. 安全设计与实现

6.1 密码安全策略

用户密码的安全存储至关重要。我们采用以下策略:

  1. 密码哈希: 使用bcrypt算法,工作因子为12
  2. 盐值: 每个密码使用唯一盐值
  3. 密码策略 :
    • 最小长度8位
    • 必须包含大小写字母、数字和特殊字符
    • 密码不得与历史密码重复

密码哈希的数学表示:

hash = bcrypt ( password , salt , cost ) \text{hash} = \text{bcrypt}(\text{password}, \text{salt}, \text{cost}) hash=bcrypt(password,salt,cost)

其中cost参数决定计算复杂度,提高暴力破解难度。

6.2 JWT令牌机制

我们采用JWT(JSON Web Token)进行身份验证:

python 复制代码
# JWT令牌结构示例
header = {
    "alg": "HS256",
    "typ": "JWT"
}

payload = {
    "user_id": "12345",
    "username": "john_doe",
    "exp": 1609459200,  # 过期时间
    "iat": 1609455600,   # 签发时间
    "roles": ["user"],
    "permissions": ["read:profile", "write:address"]
}

signature = HMAC-SHA256(
    base64UrlEncode(header) + "." + 
    base64UrlEncode(payload),
    secret_key
)

令牌生成公式:

token = Base64URL ( header ) ⋅ Base64URL ( payload ) ⋅ Base64URL ( signature ) \text{token} = \text{Base64URL}(\text{header}) \cdot \text{Base64URL}(\text{payload}) \cdot \text{Base64URL}(\text{signature}) token=Base64URL(header)⋅Base64URL(payload)⋅Base64URL(signature)

6.3 限流与防刷机制

为防止恶意请求,我们实现以下防护措施:

  1. 令牌桶算法限流:每个用户每分钟最多60次请求
  2. 滑动窗口计数:检测异常请求模式
  3. IP黑名单:自动封禁恶意IP

令牌桶算法的数学模型:

可用令牌数 = min ⁡ ( 桶容量 , 当前令牌数 + Δ t × 填充速率 ) \text{可用令牌数} = \min(\text{桶容量}, \text{当前令牌数} + \Delta t \times \text{填充速率}) 可用令牌数=min(桶容量,当前令牌数+Δt×填充速率)

其中 Δ t \Delta t Δt是上次请求到现在的时间差。

7. 性能优化策略

7.1 缓存策略

命中
未命中
API请求
缓存检查
返回缓存数据
查询数据库
写入缓存
返回数据
数据更新
删除缓存

缓存策略:

  1. 用户信息缓存:TTL 5分钟,LRU淘汰策略
  2. 会话信息缓存:TTL 30分钟
  3. 热点数据:永久缓存,主动更新

7.2 数据库优化

  1. 索引策略

    • 为查询频繁的字段创建索引
    • 使用复合索引优化多条件查询
    • 定期分析查询性能,优化索引
  2. 查询优化

    • 使用分页避免全表扫描
    • 使用EXPLAIN分析查询计划
    • 避免N+1查询问题

8. 完整代码实现

8.1 项目结构

复制代码
ecommerce-user-system/
├── app/
│   ├── __init__.py
│   ├── main.py              # 应用入口
│   ├── config.py            # 配置管理
│   ├── database.py          # 数据库连接
│   ├── models/              # 数据模型
│   │   ├── __init__.py
│   │   ├── user.py
│   │   ├── address.py
│   │   └── session.py
│   ├── schemas/             # Pydantic模型
│   │   ├── __init__.py
│   │   ├── user.py
│   │   └── auth.py
│   ├── api/                 # API路由
│   │   ├── __init__.py
│   │   ├── v1/
│   │   │   ├── __init__.py
│   │   │   ├── auth.py
│   │   │   ├── users.py
│   │   │   └── addresses.py
│   ├── core/                # 核心功能
│   │   ├── __init__.py
│   │   ├── security.py      # 安全相关
│   │   ├── auth.py          # 认证逻辑
│   │   └── cache.py         # 缓存管理
│   └── utils/               # 工具函数
│       ├── __init__.py
│       ├── validators.py    # 验证器
│       └── helpers.py       # 辅助函数
├── tests/                   # 测试文件
├── requirements.txt         # 依赖包
└── .env.example            # 环境变量示例

8.2 核心代码实现

8.2.1 配置管理 (app/config.py)
python 复制代码
import os
from typing import List, Optional
from pydantic import BaseSettings, validator
from functools import lru_cache


class Settings(BaseSettings):
    """应用配置类"""
    
    # 应用配置
    APP_NAME: str = "Ecommerce User System"
    APP_VERSION: str = "1.0.0"
    DEBUG: bool = False
    
    # API配置
    API_V1_STR: str = "/api/v1"
    PROJECT_NAME: str = "Ecommerce User API"
    
    # 安全配置
    SECRET_KEY: str
    ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
    REFRESH_TOKEN_EXPIRE_DAYS: int = 7
    
    # 密码安全
    BCRYPT_ROUNDS: int = 12
    PASSWORD_MIN_LENGTH: int = 8
    
    # CORS配置
    BACKEND_CORS_ORIGINS: List[str] = ["http://localhost:3000"]
    
    @validator("BACKEND_CORS_ORIGINS", pre=True)
    def assemble_cors_origins(cls, v):
        """解析CORS origins"""
        if isinstance(v, str):
            return [i.strip() for i in v.split(",")]
        return v
    
    # 数据库配置
    POSTGRES_SERVER: str
    POSTGRES_USER: str
    POSTGRES_PASSWORD: str
    POSTGRES_DB: str
    DATABASE_URL: Optional[str] = None
    
    @validator("DATABASE_URL", pre=True)
    def assemble_db_connection(cls, v, values):
        """构建数据库连接URL"""
        if v:
            return v
        return f"postgresql://{values.get('POSTGRES_USER')}:{values.get('POSTGRES_PASSWORD')}@{values.get('POSTGRES_SERVER')}/{values.get('POSTGRES_DB')}"
    
    # Redis配置
    REDIS_HOST: str = "localhost"
    REDIS_PORT: int = 6379
    REDIS_DB: int = 0
    REDIS_PASSWORD: Optional[str] = None
    
    # 缓存配置
    CACHE_TTL: int = 300  # 5分钟
    SESSION_TTL: int = 1800  # 30分钟
    
    # 限流配置
    RATE_LIMIT_PER_MINUTE: int = 60
    RATE_LIMIT_BURST: int = 10
    
    class Config:
        env_file = ".env"
        case_sensitive = True


@lru_cache()
def get_settings() -> Settings:
    """获取配置单例"""
    return Settings()


# 全局配置实例
settings = get_settings()
8.2.2 数据库模型 (app/models/user.py)
python 复制代码
import uuid
from datetime import datetime
from typing import Optional
from sqlalchemy import Boolean, Column, DateTime, String, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base


class User(Base):
    """用户模型"""
    
    __tablename__ = "users"
    
    # 主键
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
    
    # 用户身份信息
    username = Column(String(50), unique=True, index=True, nullable=False)
    email = Column(String(255), unique=True, index=True, nullable=False)
    phone = Column(String(20), unique=True, index=True, nullable=True)
    
    # 安全信息
    password_hash = Column(String(255), nullable=False)
    is_active = Column(Boolean, default=True)
    is_verified = Column(Boolean, default=False)
    verification_token = Column(String(100), nullable=True)
    
    # 用户资料
    first_name = Column(String(50), nullable=True)
    last_name = Column(String(50), nullable=True)
    avatar_url = Column(String(500), nullable=True)
    bio = Column(Text, nullable=True)
    
    # 时间戳
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    last_login = Column(DateTime, nullable=True)
    
    # 关系
    addresses = relationship("UserAddress", back_populates="user", cascade="all, delete-orphan")
    sessions = relationship("UserSession", back_populates="user", cascade="all, delete-orphan")
    roles = relationship("UserRole", back_populates="user", cascade="all, delete-orphan")
    
    def __repr__(self) -> str:
        return f"<User(id={self.id}, username={self.username}, email={self.email})>"
    
    def to_dict(self) -> dict:
        """转换为字典"""
        return {
            "id": str(self.id),
            "username": self.username,
            "email": self.email,
            "phone": self.phone,
            "first_name": self.first_name,
            "last_name": self.last_name,
            "avatar_url": self.avatar_url,
            "is_active": self.is_active,
            "is_verified": self.is_verified,
            "created_at": self.created_at.isoformat() if self.created_at else None,
            "last_login": self.last_login.isoformat() if self.last_login else None
        }


class UserAddress(Base):
    """用户地址模型"""
    
    __tablename__ = "user_addresses"
    
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    user_id = Column(UUID(as_uuid=True), index=True, nullable=False)
    
    # 收件人信息
    recipient_name = Column(String(100), nullable=False)
    phone = Column(String(20), nullable=False)
    
    # 地址信息
    country = Column(String(50), default="中国")
    province = Column(String(50), nullable=False)
    city = Column(String(50), nullable=False)
    district = Column(String(50), nullable=False)
    detail_address = Column(String(500), nullable=False)
    postal_code = Column(String(10), nullable=True)
    
    # 地址标签和状态
    address_label = Column(String(50), nullable=True)  # 如:家、公司
    is_default = Column(Boolean, default=False)
    
    # 时间戳
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    # 关系
    user = relationship("User", back_populates="addresses")
    
    def __repr__(self) -> str:
        return f"<UserAddress(id={self.id}, user_id={self.user_id}, recipient={self.recipient_name})>"


class UserSession(Base):
    """用户会话模型"""
    
    __tablename__ = "user_sessions"
    
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    user_id = Column(UUID(as_uuid=True), index=True, nullable=False)
    
    # 会话信息
    session_token = Column(String(500), unique=True, index=True, nullable=False)
    refresh_token = Column(String(500), unique=True, index=True, nullable=False)
    device_info = Column(String(500), nullable=True)
    ip_address = Column(String(45), nullable=True)  # 支持IPv6
    user_agent = Column(String(500), nullable=True)
    
    # 状态和时间
    is_active = Column(Boolean, default=True)
    login_at = Column(DateTime, default=datetime.utcnow)
    last_activity = Column(DateTime, default=datetime.utcnow)
    expires_at = Column(DateTime, nullable=False)
    
    # 关系
    user = relationship("User", back_populates="sessions")
    
    def __repr__(self) -> str:
        return f"<UserSession(id={self.id}, user_id={self.user_id}, is_active={self.is_active})>"
8.2.3 Pydantic模型 (app/schemas/user.py)
python 复制代码
from typing import Optional, List
from datetime import datetime
from pydantic import BaseModel, EmailStr, Field, validator
import re


# 基础模型
class UserBase(BaseModel):
    """用户基础模型"""
    username: str = Field(..., min_length=3, max_length=50, description="用户名")
    email: EmailStr = Field(..., description="邮箱地址")
    phone: Optional[str] = Field(None, description="手机号")


class UserCreate(UserBase):
    """用户创建模型"""
    password: str = Field(..., min_length=8, description="密码")
    password_confirm: str = Field(..., description="确认密码")
    
    @validator('username')
    def validate_username(cls, v):
        """验证用户名格式"""
        if not re.match(r'^[a-zA-Z0-9_]+$', v):
            raise ValueError('用户名只能包含字母、数字和下划线')
        return v
    
    @validator('password')
    def validate_password(cls, v):
        """验证密码强度"""
        if not re.search(r'[A-Z]', v):
            raise ValueError('密码必须包含至少一个大写字母')
        if not re.search(r'[a-z]', v):
            raise ValueError('密码必须包含至少一个小写字母')
        if not re.search(r'\d', v):
            raise ValueError('密码必须包含至少一个数字')
        if not re.search(r'[!@#$%^&*(),.?":{}|<>]', v):
            raise ValueError('密码必须包含至少一个特殊字符')
        return v
    
    @validator('password_confirm')
    def passwords_match(cls, v, values):
        """验证两次输入的密码是否一致"""
        if 'password' in values and v != values['password']:
            raise ValueError('两次输入的密码不一致')
        return v


class UserUpdate(BaseModel):
    """用户更新模型"""
    first_name: Optional[str] = Field(None, max_length=50, description="名")
    last_name: Optional[str] = Field(None, max_length=50, description="姓")
    phone: Optional[str] = Field(None, description="手机号")
    avatar_url: Optional[str] = Field(None, description="头像URL")
    bio: Optional[str] = Field(None, description="个人简介")


class UserInDB(UserBase):
    """数据库中的用户模型"""
    id: str
    first_name: Optional[str]
    last_name: Optional[str]
    avatar_url: Optional[str]
    bio: Optional[str]
    is_active: bool
    is_verified: bool
    created_at: datetime
    updated_at: datetime
    last_login: Optional[datetime]
    
    class Config:
        orm_mode = True


class UserPublic(UserInDB):
    """公开的用户信息"""
    pass


# 地址模型
class AddressBase(BaseModel):
    """地址基础模型"""
    recipient_name: str = Field(..., max_length=100, description="收件人姓名")
    phone: str = Field(..., description="联系电话")
    province: str = Field(..., description="省份")
    city: str = Field(..., description="城市")
    district: str = Field(..., description="区县")
    detail_address: str = Field(..., max_length=500, description="详细地址")
    postal_code: Optional[str] = Field(None, description="邮政编码")
    address_label: Optional[str] = Field(None, description="地址标签")
    is_default: bool = Field(False, description="是否默认地址")


class AddressCreate(AddressBase):
    """地址创建模型"""
    pass


class AddressUpdate(BaseModel):
    """地址更新模型"""
    recipient_name: Optional[str] = None
    phone: Optional[str] = None
    province: Optional[str] = None
    city: Optional[str] = None
    district: Optional[str] = None
    detail_address: Optional[str] = None
    postal_code: Optional[str] = None
    address_label: Optional[str] = None
    is_default: Optional[bool] = None


class AddressInDB(AddressBase):
    """数据库中的地址模型"""
    id: str
    user_id: str
    created_at: datetime
    updated_at: datetime
    
    class Config:
        orm_mode = True


# 响应模型
class UserResponse(BaseModel):
    """用户响应模型"""
    success: bool = True
    message: str = "操作成功"
    data: Optional[UserPublic] = None


class AddressListResponse(BaseModel):
    """地址列表响应模型"""
    success: bool = True
    message: str = "操作成功"
    data: List[AddressInDB]
    total: int
    page: int
    size: int
8.2.4 安全模块 (app/core/security.py)
python 复制代码
import bcrypt
import jwt
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
from fastapi import HTTPException, status
from app.config import settings


class SecurityManager:
    """安全管理器"""
    
    @staticmethod
    def hash_password(password: str) -> str:
        """使用bcrypt哈希密码
        
        公式:hash = bcrypt(password, salt, cost)
        其中cost参数为工作因子,决定计算复杂度
        """
        # 生成盐值并哈希密码
        salt = bcrypt.gensalt(rounds=settings.BCRYPT_ROUNDS)
        password_bytes = password.encode('utf-8')
        hashed = bcrypt.hashpw(password_bytes, salt)
        return hashed.decode('utf-8')
    
    @staticmethod
    def verify_password(plain_password: str, hashed_password: str) -> bool:
        """验证密码
        
        对比明文密码和哈希后的密码是否匹配
        """
        try:
            plain_bytes = plain_password.encode('utf-8')
            hashed_bytes = hashed_password.encode('utf-8')
            return bcrypt.checkpw(plain_bytes, hashed_bytes)
        except Exception:
            return False
    
    @staticmethod
    def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
        """创建访问令牌
        
        JWT令牌结构:header.payload.signature
        """
        to_encode = data.copy()
        
        # 设置过期时间
        if expires_delta:
            expire = datetime.utcnow() + expires_delta
        else:
            expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
        
        to_encode.update({"exp": expire, "iat": datetime.utcnow()})
        
        # 编码JWT令牌
        encoded_jwt = jwt.encode(
            to_encode,
            settings.SECRET_KEY,
            algorithm=settings.ALGORITHM
        )
        
        return encoded_jwt
    
    @staticmethod
    def create_refresh_token(data: Dict[str, Any]) -> str:
        """创建刷新令牌"""
        to_encode = data.copy()
        
        # 刷新令牌有效期更长
        expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
        to_encode.update({"exp": expire, "iat": datetime.utcnow(), "type": "refresh"})
        
        encoded_jwt = jwt.encode(
            to_encode,
            settings.SECRET_KEY,
            algorithm=settings.ALGORITHM
        )
        
        return encoded_jwt
    
    @staticmethod
    def verify_token(token: str) -> Dict[str, Any]:
        """验证并解码JWT令牌"""
        try:
            # 解码令牌
            payload = jwt.decode(
                token,
                settings.SECRET_KEY,
                algorithms=[settings.ALGORITHM]
            )
            return payload
        except jwt.ExpiredSignatureError:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="令牌已过期",
                headers={"WWW-Authenticate": "Bearer"},
            )
        except jwt.JWTError:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="无效的令牌",
                headers={"WWW-Authenticate": "Bearer"},
            )
    
    @staticmethod
    def generate_verification_code(length: int = 6) -> str:
        """生成验证码"""
        import random
        import string
        return ''.join(random.choices(string.digits, k=length))
    
    @staticmethod
    def validate_phone_number(phone: str) -> bool:
        """验证手机号格式"""
        import re
        # 简单的手机号验证正则
        pattern = r'^1[3-9]\d{9}$'
        return bool(re.match(pattern, phone))
    
    @staticmethod
    def validate_email(email: str) -> bool:
        """验证邮箱格式"""
        import re
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        return bool(re.match(pattern, email))


# 安全工具实例
security = SecurityManager()
8.2.5 认证路由 (app/api/v1/auth.py)
python 复制代码
from datetime import timedelta
from typing import Any, Dict
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
import redis

from app import schemas, models, crud
from app.api import deps
from app.core import security, auth
from app.core.cache import get_redis_client
from app.utils.validators import validate_email, validate_phone

router = APIRouter()

# OAuth2密码流
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login")


@router.post("/register", response_model=schemas.UserResponse, status_code=status.HTTP_201_CREATED)
async def register(
    *,
    db: Session = Depends(deps.get_db),
    user_in: schemas.UserCreate,
    request: Request
) -> Any:
    """用户注册
    
    参数:
    - user_in: 用户注册信息
    - request: 请求对象,用于获取客户端信息
    
    返回:
    - 注册成功的用户信息
    """
    
    # 检查用户是否已存在
    user_by_email = crud.user.get_by_email(db, email=user_in.email)
    if user_by_email:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="该邮箱已被注册"
        )
    
    user_by_username = crud.user.get_by_username(db, username=user_in.username)
    if user_by_username:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="该用户名已被使用"
        )
    
    if user_in.phone:
        user_by_phone = crud.user.get_by_phone(db, phone=user_in.phone)
        if user_by_phone:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="该手机号已被注册"
            )
    
    # 创建用户
    user = crud.user.create(db, obj_in=user_in)
    
    # 生成验证码(实际项目中应通过邮件或短信发送)
    verification_code = security.generate_verification_code()
    
    # 将验证码存入Redis,有效期10分钟
    redis_client = get_redis_client()
    redis_key = f"verify:{user.id}"
    redis_client.setex(redis_key, 600, verification_code)
    
    # 记录注册日志
    crud.user.create_activity_log(
        db,
        user_id=user.id,
        activity_type="register",
        ip_address=request.client.host,
        user_agent=request.headers.get("user-agent")
    )
    
    return {
        "success": True,
        "message": "注册成功,请验证邮箱",
        "data": user
    }


@router.post("/login", response_model=Dict[str, Any])
async def login(
    db: Session = Depends(deps.get_db),
    form_data: OAuth2PasswordRequestForm = Depends(),
    request: Request = None
) -> Any:
    """用户登录
    
    支持用户名/邮箱/手机号登录
    
    参数:
    - form_data: OAuth2密码表单数据
    - request: 请求对象
    
    返回:
    - 访问令牌和刷新令牌
    """
    
    # 识别登录类型(邮箱、手机号或用户名)
    login_field = form_data.username
    
    # 根据输入格式判断登录方式
    if "@" in login_field:
        # 邮箱登录
        user = crud.user.authenticate_by_email(db, email=login_field, password=form_data.password)
    elif login_field.isdigit() and len(login_field) == 11:
        # 手机号登录(简单判断)
        user = crud.user.authenticate_by_phone(db, phone=login_field, password=form_data.password)
    else:
        # 用户名登录
        user = crud.user.authenticate_by_username(db, username=login_field, password=form_data.password)
    
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="用户名或密码错误",
            headers={"WWW-Authenticate": "Bearer"},
        )
    
    if not user.is_active:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="账户已被禁用"
        )
    
    # 更新最后登录时间
    user.last_login = datetime.utcnow()
    db.commit()
    
    # 创建访问令牌
    access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = security.create_access_token(
        data={"sub": str(user.id), "username": user.username},
        expires_delta=access_token_expires
    )
    
    # 创建刷新令牌
    refresh_token = security.create_refresh_token(
        data={"sub": str(user.id)}
    )
    
    # 创建会话记录
    device_info = request.headers.get("user-agent", "")
    ip_address = request.client.host if request.client else "0.0.0.0"
    
    session_data = {
        "user_id": user.id,
        "access_token": access_token,
        "refresh_token": refresh_token,
        "device_info": device_info,
        "ip_address": ip_address,
        "expires_at": datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
    }
    
    crud.session.create(db, obj_in=session_data)
    
    # 记录登录日志
    crud.user.create_activity_log(
        db,
        user_id=user.id,
        activity_type="login",
        ip_address=ip_address,
        user_agent=device_info
    )
    
    return {
        "access_token": access_token,
        "refresh_token": refresh_token,
        "token_type": "bearer",
        "expires_in": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
        "user": user.to_dict()
    }


@router.post("/refresh", response_model=Dict[str, Any])
async def refresh_token(
    *,
    db: Session = Depends(deps.get_db),
    refresh_token: str
) -> Any:
    """刷新访问令牌"""
    
    # 验证刷新令牌
    try:
        payload = security.verify_token(refresh_token)
        user_id = payload.get("sub")
        token_type = payload.get("type")
        
        if not user_id or token_type != "refresh":
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="无效的刷新令牌"
            )
        
        # 检查令牌是否在会话中
        session = crud.session.get_by_refresh_token(db, refresh_token=refresh_token)
        if not session or not session.is_active:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="令牌已失效"
            )
        
        # 获取用户
        user = crud.user.get(db, id=user_id)
        if not user or not user.is_active:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="用户不存在或已被禁用"
            )
        
        # 创建新的访问令牌
        access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
        new_access_token = security.create_access_token(
            data={"sub": str(user.id), "username": user.username},
            expires_delta=access_token_expires
        )
        
        # 更新会话中的访问令牌
        session.access_token = new_access_token
        session.last_activity = datetime.utcnow()
        db.commit()
        
        return {
            "access_token": new_access_token,
            "token_type": "bearer",
            "expires_in": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
        }
        
    except jwt.ExpiredSignatureError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="刷新令牌已过期"
        )
    except jwt.JWTError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="无效的刷新令牌"
        )


@router.post("/logout")
async def logout(
    *,
    db: Session = Depends(deps.get_db),
    current_user: models.User = Depends(deps.get_current_user),
    token: str = Depends(oauth2_scheme)
) -> Any:
    """用户登出"""
    
    # 使当前令牌失效
    crud.session.deactivate_by_token(db, access_token=token)
    
    # 记录登出日志
    crud.user.create_activity_log(
        db,
        user_id=current_user.id,
        activity_type="logout"
    )
    
    return {
        "success": True,
        "message": "登出成功"
    }


@router.post("/verify-email")
async def verify_email(
    *,
    db: Session = Depends(deps.get_db),
    user_id: str,
    code: str
) -> Any:
    """验证邮箱"""
    
    # 检查验证码
    redis_client = get_redis_client()
    redis_key = f"verify:{user_id}"
    stored_code = redis_client.get(redis_key)
    
    if not stored_code or stored_code.decode() != code:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="验证码错误或已过期"
        )
    
    # 更新用户验证状态
    user = crud.user.get(db, id=user_id)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在"
        )
    
    user.is_verified = True
    db.commit()
    
    # 删除验证码
    redis_client.delete(redis_key)
    
    return {
        "success": True,
        "message": "邮箱验证成功"
    }


@router.post("/forgot-password")
async def forgot_password(
    *,
    db: Session = Depends(deps.get_db),
    email: str
) -> Any:
    """忘记密码 - 发送重置邮件"""
    
    user = crud.user.get_by_email(db, email=email)
    if not user:
        # 出于安全考虑,即使用户不存在也返回成功
        return {
            "success": True,
            "message": "如果邮箱存在,重置链接已发送"
        }
    
    # 生成重置令牌
    reset_token = security.create_access_token(
        data={"sub": str(user.id), "type": "reset_password"},
        expires_delta=timedelta(hours=1)
    )
    
    # 存储重置令牌到Redis,有效期1小时
    redis_client = get_redis_client()
    redis_key = f"reset:{user.id}"
    redis_client.setex(redis_key, 3600, reset_token)
    
    # TODO: 实际项目中应发送重置邮件
    # send_reset_email(user.email, reset_token)
    
    # 记录操作日志
    crud.user.create_activity_log(
        db,
        user_id=user.id,
        activity_type="request_password_reset"
    )
    
    return {
        "success": True,
        "message": "重置链接已发送到您的邮箱",
        "reset_token": reset_token  # 开发环境返回,生产环境不返回
    }


@router.post("/reset-password")
async def reset_password(
    *,
    db: Session = Depends(deps.get_db),
    token: str,
    new_password: str,
    confirm_password: str
) -> Any:
    """重置密码"""
    
    if new_password != confirm_password:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="两次输入的密码不一致"
        )
    
    # 验证令牌
    try:
        payload = security.verify_token(token)
        user_id = payload.get("sub")
        token_type = payload.get("type")
        
        if not user_id or token_type != "reset_password":
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="无效的重置令牌"
            )
        
        # 检查令牌是否有效
        redis_client = get_redis_client()
        redis_key = f"reset:{user_id}"
        stored_token = redis_client.get(redis_key)
        
        if not stored_token or stored_token.decode() != token:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="重置令牌已失效"
            )
        
        # 更新密码
        user = crud.user.get(db, id=user_id)
        if not user:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="用户不存在"
            )
        
        crud.user.update_password(db, user=user, new_password=new_password)
        
        # 删除所有活跃会话(强制重新登录)
        crud.session.deactivate_all_user_sessions(db, user_id=user_id)
        
        # 删除重置令牌
        redis_client.delete(redis_key)
        
        # 记录操作日志
        crud.user.create_activity_log(
            db,
            user_id=user.id,
            activity_type="password_reset"
        )
        
        return {
            "success": True,
            "message": "密码重置成功"
        }
        
    except jwt.ExpiredSignatureError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="重置令牌已过期"
        )
    except jwt.JWTError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="无效的重置令牌"
        )
8.2.6 用户管理路由 (app/api/v1/users.py)
python 复制代码
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session

from app import schemas, models, crud
from app.api import deps
from app.core.cache import cache

router = APIRouter()


@router.get("/me", response_model=schemas.UserResponse)
@cache(ttl=300, key_prefix="user")
async def get_current_user_info(
    current_user: models.User = Depends(deps.get_current_user),
) -> Any:
    """获取当前用户信息"""
    return {
        "success": True,
        "data": current_user
    }


@router.get("/{user_id}", response_model=schemas.UserResponse)
@cache(ttl=300, key_prefix="user")
async def get_user_by_id(
    user_id: str,
    db: Session = Depends(deps.get_db),
    current_user: models.User = Depends(deps.get_current_user),
) -> Any:
    """根据ID获取用户信息(需要管理员权限)"""
    
    # 检查权限:普通用户只能查看自己的信息
    if str(current_user.id) != user_id and not current_user.is_superuser:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="权限不足"
        )
    
    user = crud.user.get(db, id=user_id)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在"
        )
    
    return {
        "success": True,
        "data": user
    }


@router.put("/{user_id}", response_model=schemas.UserResponse)
async def update_user(
    *,
    db: Session = Depends(deps.get_db),
    user_id: str,
    user_in: schemas.UserUpdate,
    current_user: models.User = Depends(deps.get_current_user),
) -> Any:
    """更新用户信息"""
    
    # 检查权限:只能更新自己的信息
    if str(current_user.id) != user_id and not current_user.is_superuser:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="只能修改自己的信息"
        )
    
    user = crud.user.get(db, id=user_id)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在"
        )
    
    # 如果更新手机号,需要验证手机号是否已被使用
    if user_in.phone and user_in.phone != user.phone:
        existing_user = crud.user.get_by_phone(db, phone=user_in.phone)
        if existing_user and existing_user.id != user.id:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="该手机号已被其他用户使用"
            )
    
    user = crud.user.update(db, db_obj=user, obj_in=user_in)
    
    # 清除缓存
    cache.clear_key(f"user:{user_id}")
    
    return {
        "success": True,
        "message": "用户信息更新成功",
        "data": user
    }


@router.delete("/{user_id}")
async def delete_user(
    *,
    db: Session = Depends(deps.get_db),
    user_id: str,
    current_user: models.User = Depends(deps.get_current_user),
) -> Any:
    """删除用户(软删除)"""
    
    # 检查权限:只能删除自己的账户或管理员操作
    if str(current_user.id) != user_id and not current_user.is_superuser:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="只能删除自己的账户"
        )
    
    user = crud.user.get(db, id=user_id)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在"
        )
    
    # 软删除:将用户标记为不活跃
    user.is_active = False
    db.commit()
    
    # 清除用户的所有活跃会话
    crud.session.deactivate_all_user_sessions(db, user_id=user_id)
    
    # 清除缓存
    cache.clear_key(f"user:{user_id}")
    
    # 记录操作日志
    crud.user.create_activity_log(
        db,
        user_id=user.id,
        activity_type="account_deletion"
    )
    
    return {
        "success": True,
        "message": "账户已成功删除"
    }


@router.get("/{user_id}/addresses", response_model=schemas.AddressListResponse)
@cache(ttl=300, key_prefix="user_addresses")
async def get_user_addresses(
    user_id: str,
    db: Session = Depends(deps.get_db),
    current_user: models.User = Depends(deps.get_current_user),
    skip: int = Query(0, ge=0, description="跳过记录数"),
    limit: int = Query(20, ge=1, le=100, description="每页记录数"),
) -> Any:
    """获取用户地址列表"""
    
    # 检查权限:只能查看自己的地址
    if str(current_user.id) != user_id and not current_user.is_superuser:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="权限不足"
        )
    
    addresses, total = crud.address.get_by_user(
        db, 
        user_id=user_id, 
        skip=skip, 
        limit=limit
    )
    
    return {
        "success": True,
        "data": addresses,
        "total": total,
        "page": skip // limit + 1 if limit > 0 else 1,
        "size": limit
    }


@router.post("/{user_id}/addresses", response_model=schemas.UserResponse, status_code=status.HTTP_201_CREATED)
async def create_address(
    *,
    db: Session = Depends(deps.get_db),
    user_id: str,
    address_in: schemas.AddressCreate,
    current_user: models.User = Depends(deps.get_current_user),
) -> Any:
    """添加用户地址"""
    
    # 检查权限:只能为自己添加地址
    if str(current_user.id) != user_id and not current_user.is_superuser:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="只能为自己的账户添加地址"
        )
    
    # 检查用户是否存在
    user = crud.user.get(db, id=user_id)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在"
        )
    
    # 如果设置为默认地址,需要先取消其他默认地址
    if address_in.is_default:
        crud.address.unset_default_addresses(db, user_id=user_id)
    
    address = crud.address.create_with_user(db, obj_in=address_in, user_id=user_id)
    
    # 清除地址缓存
    cache.clear_key(f"user_addresses:{user_id}")
    
    return {
        "success": True,
        "message": "地址添加成功",
        "data": address
    }


@router.put("/{user_id}/addresses/{address_id}", response_model=schemas.UserResponse)
async def update_address(
    *,
    db: Session = Depends(deps.get_db),
    user_id: str,
    address_id: str,
    address_in: schemas.AddressUpdate,
    current_user: models.User = Depends(deps.get_current_user),
) -> Any:
    """更新用户地址"""
    
    # 检查权限:只能更新自己的地址
    if str(current_user.id) != user_id and not current_user.is_superuser:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="只能修改自己的地址"
        )
    
    # 获取地址
    address = crud.address.get(db, id=address_id)
    if not address or str(address.user_id) != user_id:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="地址不存在"
        )
    
    # 如果设置为默认地址,需要先取消其他默认地址
    if address_in.is_default and not address.is_default:
        crud.address.unset_default_addresses(db, user_id=user_id)
    
    address = crud.address.update(db, db_obj=address, obj_in=address_in)
    
    # 清除地址缓存
    cache.clear_key(f"user_addresses:{user_id}")
    
    return {
        "success": True,
        "message": "地址更新成功",
        "data": address
    }


@router.delete("/{user_id}/addresses/{address_id}")
async def delete_address(
    *,
    db: Session = Depends(deps.get_db),
    user_id: str,
    address_id: str,
    current_user: models.User = Depends(deps.get_current_user),
) -> Any:
    """删除用户地址"""
    
    # 检查权限:只能删除自己的地址
    if str(current_user.id) != user_id and not current_user.is_superuser:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="只能删除自己的地址"
        )
    
    # 获取地址
    address = crud.address.get(db, id=address_id)
    if not address or str(address.user_id) != user_id:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="地址不存在"
        )
    
    crud.address.remove(db, id=address_id)
    
    # 清除地址缓存
    cache.clear_key(f"user_addresses:{user_id}")
    
    return {
        "success": True,
        "message": "地址删除成功"
    }
8.2.7 主应用文件 (app/main.py)
python 复制代码
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
import logging

from app.api.v1 import api_router
from app.config import settings
from app.database import engine
from app import models

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('app.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# 创建数据库表
models.Base.metadata.create_all(bind=engine)

# 初始化限流器
limiter = Limiter(key_func=get_remote_address)

# 创建FastAPI应用
app = FastAPI(
    title=settings.PROJECT_NAME,
    version=settings.APP_VERSION,
    openapi_url=f"{settings.API_V1_STR}/openapi.json",
    docs_url="/docs",
    redoc_url="/redoc"
)

# 添加限流器
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

# 设置CORS
if settings.BACKEND_CORS_ORIGINS:
    app.add_middleware(
        CORSMiddleware,
        allow_origins=settings.BACKEND_CORS_ORIGINS,
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

# 添加信任主机中间件
app.add_middleware(
    TrustedHostMiddleware,
    allowed_hosts=["*"] if settings.DEBUG else settings.BACKEND_CORS_ORIGINS
)

# 添加Gzip压缩中间件
app.add_middleware(GZipMiddleware, minimum_size=1000)

# 添加请求日志中间件
@app.middleware("http")
async def log_requests(request, call_next):
    """记录请求日志"""
    logger.info(f"Request: {request.method} {request.url}")
    response = await call_next(request)
    logger.info(f"Response: {response.status_code}")
    return response

# 包含API路由
app.include_router(api_router, prefix=settings.API_V1_STR)

# 健康检查端点
@app.get("/health")
@limiter.limit("10/minute")
async def health_check():
    """健康检查"""
    return {
        "status": "healthy",
        "service": settings.PROJECT_NAME,
        "version": settings.APP_VERSION
    }

@app.get("/")
async def root():
    """根路径"""
    return {
        "message": f"欢迎使用{settings.PROJECT_NAME} API",
        "version": settings.APP_VERSION,
        "docs": "/docs",
        "redoc": "/redoc"
    }

# 应用启动事件
@app.on_event("startup")
async def startup_event():
    """应用启动时执行"""
    logger.info("应用启动中...")
    # 初始化Redis连接池等
    
@app.on_event("shutdown")
async def shutdown_event():
    """应用关闭时执行"""
    logger.info("应用关闭中...")
    # 清理资源

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "app.main:app",
        host="0.0.0.0",
        port=8000,
        reload=settings.DEBUG,
        log_level="info"
    )

9. 测试策略

9.1 单元测试

python 复制代码
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from app.main import app
from app.database import Base, get_db
from app.core.security import hash_password

# 测试数据库
SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_TEST_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# 覆盖依赖
def override_get_db():
    try:
        db = TestingSessionLocal()
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_get_db

client = TestClient(app)

def setup_module():
    """测试模块设置"""
    Base.metadata.create_all(bind=engine)

def teardown_module():
    """测试模块清理"""
    Base.metadata.drop_all(bind=engine)

def test_register_user():
    """测试用户注册"""
    user_data = {
        "username": "testuser",
        "email": "test@example.com",
        "password": "Test123!@#",
        "password_confirm": "Test123!@#"
    }
    
    response = client.post("/api/v1/auth/register", json=user_data)
    assert response.status_code == 201
    data = response.json()
    assert data["success"] == True
    assert data["data"]["username"] == "testuser"
    assert data["data"]["email"] == "test@example.com"

def test_login():
    """测试用户登录"""
    login_data = {
        "username": "testuser",
        "password": "Test123!@#"
    }
    
    response = client.post("/api/v1/auth/login", data=login_data)
    assert response.status_code == 200
    data = response.json()
    assert "access_token" in data
    assert "refresh_token" in data
    return data["access_token"]

def test_get_current_user():
    """测试获取当前用户信息"""
    # 先登录获取令牌
    login_data = {
        "username": "testuser",
        "password": "Test123!@#"
    }
    
    response = client.post("/api/v1/auth/login", data=login_data)
    token = response.json()["access_token"]
    
    # 使用令牌获取用户信息
    headers = {"Authorization": f"Bearer {token}"}
    response = client.get("/api/v1/users/me", headers=headers)
    
    assert response.status_code == 200
    data = response.json()
    assert data["success"] == True
    assert data["data"]["username"] == "testuser"

9.2 性能测试

python 复制代码
import asyncio
import time
from typing import List
import statistics

class PerformanceTester:
    """性能测试器"""
    
    @staticmethod
    async def test_concurrent_requests(
        client, 
        endpoint: str, 
        headers: dict, 
        num_requests: int = 100
    ) -> dict:
        """测试并发请求性能"""
        
        async def make_request():
            start = time.time()
            response = await client.get(endpoint, headers=headers)
            elapsed = time.time() - start
            return elapsed, response.status_code
        
        # 创建并发任务
        tasks = [make_request() for _ in range(num_requests)]
        results = await asyncio.gather(*tasks)
        
        # 分析结果
        response_times = [r[0] for r in results]
        status_codes = [r[1] for r in results]
        
        return {
            "total_requests": num_requests,
            "successful_requests": sum(1 for code in status_codes if code == 200),
            "avg_response_time": statistics.mean(response_times),
            "median_response_time": statistics.median(response_times),
            "p95_response_time": sorted(response_times)[int(0.95 * num_requests)],
            "max_response_time": max(response_times),
            "min_response_time": min(response_times)
        }

10. 部署与监控

10.1 Docker部署

dockerfile 复制代码
# Dockerfile
FROM python:3.9-slim

# 设置工作目录
WORKDIR /app

# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/app

# 安装系统依赖
RUN apt-get update \
    && apt-get install -y --no-install-recommends gcc libpq-dev \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# 安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY . .

# 创建非root用户
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

# 暴露端口
EXPOSE 8000

# 启动命令
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

10.2 监控指标

关键监控指标:

  1. API性能指标

    • 请求响应时间(P50, P95, P99)
    • 请求成功率
    • API调用量
  2. 系统资源指标

    • CPU使用率
    • 内存使用率
    • 磁盘I/O
    • 网络流量
  3. 业务指标

    • 用户注册/登录成功率
    • 用户活跃度
    • API错误率

11. 代码自查与优化

11.1 代码自查清单

在完成代码实现后,我们进行了以下自查:

  1. 安全性检查

    • 所有用户输入都经过验证和清理
    • 密码使用bcrypt加密存储
    • JWT令牌有过期时间和刷新机制
    • 实现了API限流和防刷机制
    • 敏感信息不在日志中记录
  2. 性能检查

    • 数据库查询都使用索引
    • 频繁访问的数据使用缓存
    • 实现了数据库连接池
    • 异步处理耗时操作
  3. 代码质量检查

    • 遵循PEP8代码规范
    • 函数和类有清晰的文档字符串
    • 错误处理完善
    • 单元测试覆盖核心功能
  4. API设计检查

    • 遵循RESTful设计原则
    • 使用合适的HTTP状态码
    • 响应格式统一
    • API版本控制

11.2 已知问题与改进方向

  1. 当前实现的局限性

    • 第三方登录(微信、支付宝)需要额外集成
    • 分布式会话管理需要Redis集群支持
    • 需要更完善的审计日志系统
  2. 后续优化方向

    • 实现GraphQL API作为RESTful API的补充
    • 添加API网关进行统一管理
    • 实施服务网格进行微服务治理
    • 集成实时通知系统(WebSocket)

12. 总结

本文详细介绍了电商平台用户系统API的设计与实现。我们从需求分析出发,设计了合理的系统架构,实现了包括用户认证、信息管理、地址管理在内的核心功能。通过采用FastAPI框架,我们构建了高性能的RESTful API,并实施了多层次的安全防护措施。

关键设计要点总结:

  1. 安全性优先:使用bcrypt哈希密码、JWT令牌、API限流等多重安全机制
  2. 性能优化:通过缓存、数据库索引、异步处理提升系统性能
  3. 可扩展性:采用微服务架构思想,便于系统水平扩展
  4. 开发友好:自动生成API文档,统一的错误处理,完善的日志系统

本实现可作为电商平台用户系统的基础框架,根据具体业务需求进行扩展和优化。随着业务的发展,可以考虑引入更多高级功能,如用户行为分析、个性化推荐引擎等,进一步提升用户体验和平台价值。


注意:本文提供的代码为示例实现,实际生产环境中需要根据具体需求进行调整和优化。部署前请确保进行充分的安全测试和性能测试。

相关推荐
悟空码字几秒前
SpringBoot + Redis分布式锁深度剖析,性能暴涨的秘密全在这里
java·spring boot·后端
奋进的芋圆2 分钟前
Spring Boot中实现定时任务
java·spring boot·后端
山沐与山2 分钟前
【Python】深入理解Python Web框架:从Flask到FastAPI的并发之路
python·flask·fastapi
Moresweet猫甜3 分钟前
Ubuntu LVM引导丢失紧急救援:完整恢复指南
linux·运维·数据库·ubuntu
yumgpkpm7 分钟前
Cloudera CDH5、CDH6、CDP7现状及替代方案
数据库·人工智能·hive·hadoop·elasticsearch·数据挖掘·kafka
BD_Marathon8 分钟前
Spring——容器
java·后端·spring
松涛和鸣10 分钟前
48、MQTT 3.1.1
linux·前端·网络·数据库·tcp/ip·html
晓时谷雨12 分钟前
达梦数据库适配方案及总结
数据库·达梦·数据迁移
武子康14 分钟前
大数据-206 用 NumPy 矩阵乘法手写多元线性回归:正规方程、SSE/MSE/RMSE 与 R²
大数据·后端·机器学习
LaLaLa_OvO14 分钟前
spring boot2.0 里的 javax.validation.Constraint 加入 service
java·数据库·spring boot