Browse Source

first commit

zhangwl 1 day ago
commit
c920e6610f
8 changed files with 1126 additions and 0 deletions
  1. 10 0
      README.md
  2. 0 0
      app/__init__.py
  3. 0 0
      app/config/__init__.py
  4. 16 0
      app/config/config.py
  5. 0 0
      app/routers/__init__.py
  6. 510 0
      app/routers/chat.py
  7. 544 0
      app/routers/users.py
  8. 46 0
      main.py

+ 10 - 0
README.md

@@ -0,0 +1,10 @@
+pip install passlib
+pip install PyJWT
+pip install python-multipart
+# 服务器
+pip install "uvicorn[standard]"
+pip install bcrypt==4.3.0
+pip install fastapi
+pip install openai
+# 火山引擎的openAI
+pip install 'volcengine-python-sdk[ark]'

+ 0 - 0
app/__init__.py


+ 0 - 0
app/config/__init__.py


+ 16 - 0
app/config/config.py

@@ -0,0 +1,16 @@
+from dotenv import load_dotenv
+import os
+
+load_dotenv()
+
+class Config:
+    # 使用阿里云DashScope API
+    API_KEY = os.getenv("DASHSCOPE_API_KEY")
+    BASE_URL = os.getenv("DASHSCOPE_BASE_URL")
+    MODEL_NAME = "doubao-seed-2-0-mini-260215"
+    MAX_TOKENS = 2000
+    TEMPERATURE = 0.7
+
+    secret_key: str = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"  # 开发环境默认值
+    algorithm: str = "HS256"
+    access_token_expire_minutes: int = 30

+ 0 - 0
app/routers/__init__.py


+ 510 - 0
app/routers/chat.py

@@ -0,0 +1,510 @@
+from fastapi import APIRouter, HTTPException, Depends
+from fastapi.responses import StreamingResponse
+from volcenginesdkarkruntime import Ark
+from pydantic import BaseModel
+from typing import List, Optional, Dict, Any, Annotated
+from datetime import datetime
+import json
+import asyncio
+from ..config.config import Config
+from ..routers.users import get_current_active_user, User
+
+# =====================================================
+# 全局变量和配置
+# =====================================================
+
+# 内存存储用户聊天历史
+# 结构: {username: [ChatMessage, ChatMessage, ...]}
+# 注意: 生产环境中应该使用数据库存储,如Redis或PostgreSQL
+chatHistory = {}
+
+# 创建配置实例
+# 从配置文件中读取API密钥、模型名称等设置
+config = Config()
+
+# 创建OpenAI客户端实例
+# 使用配置中的API密钥和基础URL
+client = Ark(api_key=config.API_KEY, base_url=config.BASE_URL)
+
+# 创建FastAPI路由器实例
+# 所有的聊天相关路由都会注册到这个路由器上
+router = APIRouter()
+
+
+# =====================================================
+# Pydantic数据模型定义
+# =====================================================
+
+class ChatMessage(BaseModel):
+    """
+    聊天消息数据模型
+
+    用于表示对话中的单条消息,包含角色、内容和时间戳
+    """
+    role: str  # 消息角色: "user"(用户), "assistant"(AI助手), "system"(系统)
+    content: str  # 消息的文本内容
+    timestamp: Optional[datetime] = None  # 消息创建时间戳(可选)
+    response_id: Optional[str] = None  # API响应ID,用于多轮对话的上下文关联
+
+
+class ChatRequest(BaseModel):
+    """
+    客户端聊天请求数据模型
+
+    定义了客户端发送聊天请求时需要包含的所有参数
+    注意: 移除了userId字段,改为从JWT认证中获取用户身份
+    """
+    messages: List[ChatMessage]  # 对话历史消息列表,包含用户和助手的所有消息
+    model: Optional[str] = config.MODEL_NAME  # 要使用的AI模型名称,默认使用配置中的模型
+    temperature: Optional[float] = config.TEMPERATURE  # 创造性温度值(0-2),控制回答的随机性
+    max_tokens: Optional[int] = config.MAX_TOKENS  # 最大生成token数,限制回答长度
+    stream: Optional[bool] = False  # 是否启用流式输出,True时会实时返回生成的内容
+
+
+class ChatResponse(BaseModel):
+    """
+    服务端聊天响应数据模型(非流式)
+
+    用于非流式请求的响应,包含完整的AI回答和使用统计
+    """
+    message: ChatMessage  # AI助手的回复消息
+    model: str  # 实际使用的模型名称
+    usage: Optional[Dict[str, Any]] = None  # token使用情况统计
+    response_id: Optional[str] = None  # API响应ID,用于后续多轮对话
+
+
+class StreamResponse(BaseModel):
+    """
+    流式响应数据模型
+
+    用于流式输出时的每个数据块,支持Server-Sent Events (SSE)
+    """
+    content: str  # 本次返回的内容片段
+    finished: bool  # 是否为最后一个片段,True表示流式响应结束
+    model: str  # 使用的模型名称
+    timestamp: datetime  # 当前片段的时间戳
+
+
+# =====================================================
+# 消息处理工具函数
+# =====================================================
+
+def convert_messages_for_api(messages: List[ChatMessage]) -> List[Dict[str, str]]:
+    """
+    将自定义ChatMessage转换为OpenAI API需要的格式
+
+    OpenAI API需要的消息格式是字典列表,每个字典包含role和content字段
+
+    Args:
+        messages (List[ChatMessage]): 自定义的消息对象列表
+
+    Returns:
+        List[Dict[str, str]]: OpenAI API格式的消息列表
+
+    Example:
+        输入: [ChatMessage(role="user", content="你好")]
+        输出: [{"role": "user", "content": "你好"}]
+    """
+    return [{"role": msg.role, "content": msg.content} for msg in messages]
+
+
+async def generate_stream_response(request: ChatRequest, username: str):
+    """
+    生成流式响应的异步生成器
+
+    这个函数处理流式AI响应,将OpenAI的流式输出转换为SSE格式
+
+    Args:
+        request (ChatRequest): 聊天请求对象
+        username (str): 当前用户名
+
+    Yields:
+        str: 格式化的SSE数据,每行以"data: "开头
+
+    Note:
+        使用Server-Sent Events (SSE) 协议进行实时数据传输
+        客户端需要使用EventSource或类似技术接收流式数据
+    """
+    try:
+        # 转换消息格式为OpenAI API需要的格式
+        api_messages = convert_messages_for_api(request.messages)
+
+        # 调用OpenAI流式API
+        # stream=True 启用流式输出,API会返回一个迭代器
+        stream = client.chat.completions.create(
+            model=request.model or config.MODEL_NAME,  # 使用指定模型或默认模型
+            messages=api_messages,  # 对话历史
+            max_tokens=request.max_tokens,  # 最大token数
+            temperature=request.temperature,  # 创造性温度
+            stream=True  # 启用流式输出
+        )
+
+        # 用于累积完整的回答内容
+        accumulated_content = ""
+
+        # 遍历流式响应的每个数据块
+        for chunk in stream:
+            # 检查数据块是否包含有效内容
+            if chunk.choices and chunk.choices[0].delta.content:
+                # 提取本次数据块的内容
+                chunk_content = chunk.choices[0].delta.content
+
+                # 累积到完整内容中
+                accumulated_content += chunk_content
+
+                # 构建流式响应数据对象
+                response_data = StreamResponse(
+                    content=chunk.choices[0].delta.content,  # 本次片段内容
+                    finished=False,  # 标记为未完成
+                    model=request.model or config.MODEL_NAME,  # 使用的模型
+                    timestamp=datetime.now()  # 当前时间戳
+                )
+
+                # 格式化为SSE格式并发送
+                # SSE格式: "data: {json_data}\n\n"
+                yield f"data: {response_data.model_dump_json()}\n\n"
+
+                # 异步让出控制权,避免阻塞事件循环
+                # 这对于处理大量并发请求很重要
+                await asyncio.sleep(0.01)
+
+        # 流式响应结束后的处理
+        if accumulated_content:
+            # 构建结束信号响应
+            final_response = StreamResponse(
+                content='',  # 结束信号不包含内容
+                finished=True,  # 标记为已完成
+                model=request.model or config.MODEL_NAME,
+                timestamp=datetime.now()
+            )
+
+            # 将完整的AI回复保存到用户的聊天历史中
+            chatHistory[username].append(
+                ChatMessage(
+                    role="assistant",
+                    content=accumulated_content,
+                    timestamp=datetime.now()
+                )
+            )
+
+            # 发送结束信号
+            yield f"data: {final_response.model_dump_json()}\n\n"
+
+    except Exception as e:
+        # 流式响应过程中的错误处理
+        # 构建错误响应并发送给客户端
+        error_response = {
+            "error": str(e),  # 错误信息
+            "finished": True,  # 标记为结束
+            "timestamp": datetime.now().isoformat()  # 错误发生时间
+        }
+
+        # 发送错误信息
+        yield f"data: {json.dumps(error_response)}\n\n"
+
+
+# =====================================================
+# API路由端点定义
+# =====================================================
+
+@router.post("/chat", response_model=ChatResponse)
+async def chat(
+        request: ChatRequest,
+        current_user: Annotated[User, Depends(get_current_active_user)]
+):
+    """
+    聊天对话接口 - 需要登录认证
+
+    这是核心的聊天接口,支持流式和非流式两种模式:
+    - 非流式: 等待AI完整回答后一次性返回
+    - 流式: 实时返回AI生成的内容片段
+
+    安全特性:
+    - 需要有效的JWT令牌
+    - 自动配额检查和限制
+    - 用户数据隔离
+
+    Args:
+        request (ChatRequest): 聊天请求数据
+        current_user (User): 通过JWT认证获取的当前用户信息
+
+    Returns:
+        ChatResponse: 非流式模式的完整响应
+        StreamingResponse: 流式模式的SSE响应
+
+    Raises:
+        HTTPException:
+            - 429: 配额已用完
+            - 500: AI模型调用失败或其他服务器错误
+    """
+    try:
+        # 从认证信息中获取用户名,确保数据安全
+        username = current_user.username
+
+        # 初始化用户的聊天历史记录(如果不存在)
+        if username not in chatHistory:
+            chatHistory[username] = []
+
+        # 根据请求类型处理:流式 vs 非流式
+        if request.stream:
+            # ===== 流式输出处理 =====
+
+            # 将用户的最新消息添加到历史记录
+            user_message = ChatMessage(
+                role=request.messages[-1].role,
+                content=request.messages[-1].content,
+                timestamp=datetime.now()
+            )
+            # chatHistory[username].append(user_message)
+
+            # 返回流式响应
+            # StreamingResponse 用于处理SSE协议
+            return StreamingResponse(
+                generate_stream_response(request, username),  # 异步生成器
+                media_type="text/plain",  # 媒体类型
+                headers={
+                    "Cache-Control": "no-cache",  # 禁用缓存
+                    "Connection": "keep-alive",  # 保持连接
+                    "Content-Type": "text/event-stream",  # SSE内容类型
+                }
+            )
+
+        else:
+            # ===== 非流式输出处理 =====
+
+            # 将用户消息添加到历史记录
+            user_message = ChatMessage(
+                role=request.messages[-1].role,
+                content=request.messages[-1].content,
+                timestamp=datetime.now()
+            )
+            chatHistory[username].append(user_message)
+
+            # 转换消息格式为OpenAI API需要的格式
+            # api_messages = convert_messages_for_api(request.messages)
+            api_messages = [{"role": request.messages[-1].role, "content": request.messages[-1].content}]
+
+            # 调用OpenAI API获取完整响应
+            # response = client.chat.completions.create(
+            #     model=request.model or config.MODEL_NAME,
+            #     messages=api_messages,
+            #     max_tokens=request.max_tokens,
+            #     temperature=request.temperature,
+            #     stream=False  # 非流式模式
+            # )
+            tools = [{
+                "type": "web_search",
+                "max_keyword": 2,  # 可选参数,用于限制一轮搜索的最大关键词数量
+            }]
+            response = client.responses.create(
+                model=config.MODEL_NAME,
+                input=api_messages,
+                tools=tools,
+                stream=False,
+            )
+
+            # 检查API响应是否有效
+            if response.output and len(response.output) > 0:
+                # 从output中找到最后一条消息(ResponseOutputMessage类型)
+                last_message = None
+                for item in reversed(response.output):
+                    if hasattr(item, 'type') and item.type == 'message':
+                        last_message = item
+                        break
+
+                if last_message and hasattr(last_message, 'content'):
+                    # 提取消息内容
+                    message_content = ""
+                    if isinstance(last_message.content, list):
+                        # content是列表,提取所有文本内容
+                        for content_item in last_message.content:
+                            if hasattr(content_item, 'text'):
+                                message_content += content_item.text
+                    else:
+                        message_content = str(last_message.content)
+
+                    # 构建AI助手的回复消息,包含response_id用于多轮对话
+                    assistant_message = ChatMessage(
+                        role="assistant",
+                        content=message_content or "",
+                        timestamp=datetime.now(),
+                        response_id=response.id  # 保存response_id用于后续多轮对话
+                    )
+
+                    # 将AI回复添加到用户的聊天历史
+                    chatHistory[username].append(assistant_message)
+
+                    # 构建完整的响应对象
+                    chat_response = ChatResponse(
+                        message=assistant_message,  # AI回复消息
+                        model=response.model,  # 实际使用的模型
+                        usage=response.usage.model_dump() if response.usage else None,  # token使用统计
+                        response_id=response.id  # 返回response_id供客户端保存
+                    )
+
+                    return chat_response
+            else:
+                # API返回空响应的错误处理
+                raise HTTPException(
+                    status_code=500,
+                    detail="AI模型返回了空响应"
+                )
+
+    except HTTPException:
+        # 重新抛出HTTP异常(如配额限制)
+        raise
+    except Exception as e:
+        # 捕获所有其他异常并转换为HTTP异常
+        error_message = f"处理聊天请求时发生错误: {str(e)}"
+        raise HTTPException(status_code=500, detail=error_message)
+
+
+@router.get("/models")
+async def get_models(
+        current_user: Annotated[User, Depends(get_current_active_user)]
+):
+    """
+    获取可用的AI模型列表 - 需要登录
+
+    返回当前可用的AI模型列表,用户可以在聊天时选择使用
+
+    Args:
+        current_user (User): 通过JWT认证获取的当前用户信息
+
+    Returns:
+        dict: 包含模型列表和默认模型的字典
+
+    Note:
+        如果无法获取模型列表,会返回默认配置
+    """
+    try:
+        # 尝试从OpenAI API获取可用模型列表
+        models = client.models.list()
+
+        return {
+            "models": [model.id for model in models.data],  # 模型ID列表
+            "default_model": config.MODEL_NAME,  # 默认模型
+            "user": current_user.username  # 请求用户
+        }
+    except Exception as e:
+        # 如果获取模型列表失败,返回默认配置
+        return {
+            "models": [config.MODEL_NAME],  # 只返回默认模型
+            "default_model": config.MODEL_NAME,
+            "note": "使用默认模型配置",  # 提示信息
+            "user": current_user.username
+        }
+
+
+@router.get("/history")
+async def get_user_history(
+        current_user: Annotated[User, Depends(get_current_active_user)]
+) -> List[ChatMessage]:
+    """
+    获取当前用户的聊天历史 - 安全版本
+
+    只返回当前认证用户的聊天历史,确保数据隐私
+
+    Args:
+        current_user (User): 通过JWT认证获取的当前用户信息
+
+    Returns:
+        List[ChatMessage]: 用户的历史消息列表
+
+    Security:
+        用户只能访问自己的聊天历史,无法访问他人数据
+    """
+    username = current_user.username
+
+    # 如果用户没有聊天历史,返回空列表
+    if username not in chatHistory:
+        return []
+
+    # 返回用户的完整聊天历史
+    # 创建新的ChatMessage对象确保数据一致性
+    return [
+        ChatMessage(
+            role=msg.role,
+            content=msg.content,
+            timestamp=msg.timestamp
+        )
+        for msg in chatHistory[username]
+    ]
+
+
+@router.delete("/history")
+async def clear_user_history(
+        current_user: Annotated[User, Depends(get_current_active_user)]
+):
+    """
+    清空当前用户的聊天历史
+
+    删除用户的所有聊天记录,此操作不可逆
+
+    Args:
+        current_user (User): 通过JWT认证获取的当前用户信息
+
+    Returns:
+        dict: 操作确认信息
+
+    Warning:
+        此操作会永久删除用户的聊天历史,无法恢复
+    """
+    username = current_user.username
+
+    # 如果用户有聊天历史,则删除
+    if username in chatHistory:
+        # 获取删除前的消息数量用于统计
+        message_count = len(chatHistory[username])
+
+        # 删除用户的聊天历史
+        del chatHistory[username]
+
+        return {
+            "message": "聊天历史已清空",
+            "user": username,
+            "deleted_messages": message_count,  # 删除的消息数量
+            "timestamp": datetime.now()
+        }
+    else:
+        # 用户没有聊天历史
+        return {
+            "message": "用户没有聊天历史",
+            "user": username,
+            "deleted_messages": 0,
+            "timestamp": datetime.now()
+        }
+
+
+@router.get("/health")
+async def health_check():
+    """
+    健康检查接口 - 无需认证
+
+    用于监控服务状态,通常被负载均衡器或监控系统调用
+
+    Returns:
+        dict: 服务状态信息
+
+    Note:
+        此接口不需要认证,可被任何人访问
+    """
+    return {
+        "status": "healthy",  # 服务状态
+        "timestamp": datetime.now(),  # 当前时间
+        "version": "1.0.0",  # 服务版本
+        "model": config.MODEL_NAME,  # 默认AI模型
+    }
+
+
+# =====================================================
+# 路由器配置和元数据
+# =====================================================
+
+# 为路由器添加标签和元数据,用于API文档生成
+router.tags = ["聊天服务"]
+router.responses = {
+    401: {"description": "未授权 - 需要有效的JWT令牌"},
+    429: {"description": "请求过多 - 配额已用完"},
+    500: {"description": "服务器内部错误"}
+}

+ 544 - 0
app/routers/users.py

@@ -0,0 +1,544 @@
+from datetime import datetime, timedelta, timezone
+from typing import Optional, Annotated
+import jwt
+from fastapi import APIRouter, Depends, HTTPException, status
+from fastapi.security import OAuth2PasswordBearer
+from passlib.context import CryptContext
+from pydantic import BaseModel
+
+# =====================================================
+# JWT和安全配置
+# =====================================================
+
+# JWT密钥配置 - 生产环境中应该使用环境变量或密钥管理服务
+SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"  # 警告:生产环境请使用强密钥并通过环境变量管理
+ALGORITHM = "HS256"  # JWT签名算法
+ACCESS_TOKEN_EXPIRE_MINUTES = 30  # Token过期时间(分钟)
+
+# 密码加密上下文配置
+# schemes: 支持的密码哈希方案,bcrypt是目前推荐的安全哈希算法
+# deprecated: 标记为已弃用的方案(用于向后兼容)
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+# OAuth2密码Bearer令牌方案
+# tokenUrl: 获取token的端点URL,必须与实际的token端点路径匹配
+# 这告诉FastAPI和前端客户端在哪里获取访问令牌
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/users/token")
+
+# 创建路由器实例
+# 这个路由器将包含所有用户相关的路由
+router = APIRouter()
+
+
+# =====================================================
+# Pydantic数据模型定义
+# =====================================================
+
+class LoginRequest(BaseModel):
+    """
+        用户登录输入的模型
+    """
+    username: str
+    password: str
+
+
+class Token(BaseModel):
+    """
+    访问令牌响应模型
+    用于登录成功后返回JWT令牌
+    """
+    message: str
+    access_token: str  # JWT访问令牌
+    token_type: str  # 令牌类型,通常是"bearer"
+    username: str
+
+
+class TokenData(BaseModel):
+    """
+    令牌数据模型
+    用于解析JWT令牌中的用户信息
+    """
+    username: Optional[str] = None  # 用户名(可选)
+
+
+class User(BaseModel):
+    """
+    用户基础信息模型
+    定义用户的公开信息(不包含密码等敏感信息)
+    """
+    username: str  # 用户名(必需)
+    email: Optional[str] = None  # 邮箱(可选)
+    full_name: Optional[str] = None  # 全名(可选)
+    disabled: Optional[bool] = None  # 是否禁用(可选)
+
+
+class UserInDB(User):
+    """
+    数据库中的用户模型
+    继承User模型,添加了密码哈希字段
+    """
+    hashed_password: str  # 哈希后的密码
+
+
+class UserCreate(BaseModel):
+    """
+    用户创建请求模型
+    用于用户注册时接收前端传来的数据
+    """
+    username: str  # 用户名(必需)
+    password: str  # 明文密码(必需)
+    email: Optional[str] = None  # 邮箱(可选)
+    full_name: Optional[str] = None  # 全名(可选)
+
+
+class UserUpdate(BaseModel):
+    """
+    用户更新请求模型
+    用于更新用户信息时接收前端传来的数据
+    """
+    email: Optional[str] = None  # 新邮箱(可选)
+    full_name: Optional[str] = None  # 新全名(可选)
+
+
+# =====================================================
+# 模拟数据库
+# =====================================================
+
+# 模拟用户数据库 - 生产环境中应该使用真实的数据库(如PostgreSQL、MySQL等)
+# 这里使用字典来模拟数据库存储,包含一个默认管理员账户
+fake_users_db = {
+    "root": {
+        "username": "root",
+        "full_name": "Administrator",
+        "email": "admin@example.com",
+        # 这是"admin123"的bcrypt哈希值
+        "hashed_password": "$2b$12$p2v617r0nPHKa4LVd6j7puYqR0lD8xivcwvtCp9UBziF5c2dRhFe.",
+        "disabled": False,
+    }
+}
+
+
+# =====================================================
+# 工具函数
+# =====================================================
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+    """
+    验证密码是否正确
+
+    Args:
+        plain_password (str): 用户输入的明文密码
+        hashed_password (str): 数据库中存储的密码哈希
+
+    Returns:
+        bool: 密码是否匹配
+    """
+    return pwd_context.verify(plain_password, hashed_password)
+
+
+def get_password_hash(password: str) -> str:
+    """
+    生成密码的哈希值
+
+    Args:
+        password (str): 明文密码
+
+    Returns:
+        str: 密码的bcrypt哈希值
+    """
+    return pwd_context.hash(password)
+
+
+def get_user(db: dict, username: str) -> Optional[UserInDB]:
+    """
+    从数据库中获取用户信息
+
+    Args:
+        db (dict): 用户数据库
+        username (str): 用户名
+
+    Returns:
+        Optional[UserInDB]: 用户信息对象,如果用户不存在则返回None
+    """
+    if username in db:
+        user_dict = db[username]
+        return UserInDB(**user_dict)
+    return None
+
+
+def authenticate_user(fake_db: dict, username: str, password: str) -> Optional[UserInDB]:
+    """
+    验证用户身份
+
+    Args:
+        fake_db (dict): 用户数据库
+        username (str): 用户名
+        password (str): 明文密码
+
+    Returns:
+        Optional[UserInDB]: 验证成功返回用户对象,失败返回False
+    """
+    # 首先获取用户信息
+    user = get_user(fake_db, username)
+    if not user:
+        return False
+
+    # 验证密码是否正确
+    if not verify_password(password, user.hashed_password):
+        return False
+
+    return user
+
+
+def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
+    """
+    创建JWT访问令牌
+
+    Args:
+        data (dict): 要编码到令牌中的数据(通常包含用户标识)
+        expires_delta (Optional[timedelta]): 令牌过期时间,如果不提供则使用默认值
+
+    Returns:
+        str: 编码后的JWT令牌
+    """
+    # 复制数据以避免修改原始数据
+    to_encode = data.copy()
+
+    # 计算过期时间,没有设置过期时间,就默认设置15分钟
+    if expires_delta:
+        expire = datetime.now(timezone.utc) + expires_delta
+    else:
+        expire = datetime.now(timezone.utc) + timedelta(minutes=15)
+
+    # 添加过期时间到令牌数据中
+    to_encode.update({"exp": expire})
+
+    # 使用密钥和算法对数据进行编码
+    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
+    return encoded_jwt
+
+
+# =====================================================
+# 依赖函数(用于路由中的依赖注入)
+# =====================================================
+
+async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> UserInDB:
+    """
+    从JWT令牌中获取当前用户信息
+
+    这是一个依赖函数,会被其他需要用户身份验证的路由使用
+
+    Args:
+        token (str): 从请求头中提取的Bearer令牌
+
+    Returns:
+        UserInDB: 当前用户信息
+
+    Raises:
+        HTTPException: 如果令牌无效或用户不存在
+    """
+    # 定义认证异常,当令牌验证失败时抛出
+    credentials_exception = HTTPException(
+        status_code=status.HTTP_401_UNAUTHORIZED,
+        detail="Could not validate credentials",  # 无法验证凭据
+        headers={"WWW-Authenticate": "Bearer"},  # 告诉客户端使用Bearer认证
+    )
+
+    try:
+        # 解码JWT令牌
+        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+
+        # 从令牌中提取用户名(sub是JWT标准字段,表示subject/主题)
+        username: str = payload.get("sub")
+        if username is None:
+            raise credentials_exception
+
+        # 创建令牌数据对象
+        token_data = TokenData(username=username)
+
+    except jwt.PyJWTError:
+        # JWT解码失败(令牌无效、过期等)
+        raise credentials_exception
+
+    # 从数据库中获取用户信息
+    user = get_user(fake_users_db, username=token_data.username)
+    if user is None:
+        raise credentials_exception
+
+    return user
+
+
+async def get_current_active_user(
+        current_user: Annotated[User, Depends(get_current_user)]
+) -> User:
+    """
+    获取当前活跃用户
+
+    这是另一个依赖函数,确保用户不仅通过了身份验证,而且账户是活跃的
+
+    Args:
+        current_user (User): 从get_current_user依赖中获取的当前用户
+
+    Returns:
+        User: 活跃的用户信息
+
+    Raises:
+        HTTPException: 如果用户账户被禁用
+    """
+    if current_user.disabled:
+        raise HTTPException(status_code=400, detail="Inactive user")
+    return current_user
+
+
+# =====================================================
+# API路由端点
+# =====================================================
+
+@router.post("/token", response_model=Token, summary="用户登录", description="使用用户名和密码获取JWT访问令牌")
+async def login_for_access_token(
+        login_data: LoginRequest
+) -> Token:
+    """
+    用户登录端点
+
+    接受用户名和密码,返回JWT访问令牌
+    使用OAuth2PasswordRequestForm来接收表单数据(username、password字段)
+
+    Args:
+        login_data : 包含用户名和密码的json数据
+
+    Returns:
+        Token: 包含访问令牌和令牌类型的对象
+
+    Raises:
+        HTTPException: 如果用户名或密码不正确
+    """
+    # 验证用户身份
+    user = authenticate_user(fake_users_db, login_data.username, login_data.password)
+    if not user:
+        # 认证失败,返回401未授权状态码
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail="用户名或密码错误",  # 用户名或密码错误
+            headers={"WWW-Authenticate": "Bearer"},
+        )
+
+    # 设置令牌过期时间
+    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+
+    # 创建访问令牌,将用户名作为subject存储在令牌中
+    access_token = create_access_token(
+        data={"sub": user.username},
+        expires_delta=access_token_expires
+    )
+
+    # 返回令牌和令牌类型
+    return {
+        "message": "登录成功",
+        "access_token": access_token,
+        "token_type": "bearer",
+        "username": user.username
+    }
+
+
+@router.post("/register", response_model=User, summary="用户注册", description="创建新用户账户")
+async def register_user(user: UserCreate) -> User:
+    """
+    用户注册端点
+
+    创建新的用户账户,密码会被自动哈希加密存储
+
+    Args:
+        user (UserCreate): 包含用户注册信息的对象
+
+    Returns:
+        User: 创建成功的用户信息(不包含密码)
+
+    Raises:
+        HTTPException: 如果用户名已存在
+    """
+    # 检查用户名是否已经存在
+    if user.username in fake_users_db:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail="Username already registered"  # 用户名已被注册
+        )
+
+    # 对密码进行哈希加密
+    hashed_password = get_password_hash(user.password)
+
+    # 创建用户数据字典
+    user_dict = {
+        "username": user.username,
+        "full_name": user.full_name,
+        "email": user.email,
+        "hashed_password": hashed_password,
+        "disabled": False  # 新用户默认为启用状态
+    }
+
+    # 将用户数据保存到"数据库"
+    fake_users_db[user.username] = user_dict
+
+    # 返回用户信息(不包含密码哈希)
+    return User(**user_dict)
+
+
+@router.post("/logout", summary="用户退出", description="用户退出登录")
+async def logout(
+        current_user: Annotated[User, Depends(get_current_active_user)]
+) -> dict:
+    """
+    用户退出登录端点
+
+    由于JWT是无状态的,服务端不需要做特殊处理
+    主要是返回成功消息,让前端清除本地存储的token
+
+    Args:
+        current_user (User): 通过依赖注入获取的当前用户信息
+
+    Returns:
+        dict: 退出成功的消息
+    """
+    print(current_user.username)
+    return {
+        "message": "退出登录成功",
+        "username": current_user.username,
+        "logout_time": datetime.now(timezone.utc).isoformat()
+    }
+
+
+@router.get("/me", response_model=User, summary="获取用户信息", description="获取当前登录用户的个人信息")
+async def read_users_me(
+        current_user: Annotated[User, Depends(get_current_active_user)]
+) -> User:
+    """
+    获取当前用户信息端点
+
+    需要有效的JWT令牌才能访问
+
+    Args:
+        current_user (User): 通过依赖注入获取的当前用户信息
+
+    Returns:
+        User: 当前用户的信息
+    """
+    return current_user
+
+
+@router.put("/me", response_model=User, summary="更新用户信息", description="更新当前登录用户的个人信息")
+async def update_user_me(
+        user_update: UserUpdate,
+        current_user: Annotated[User, Depends(get_current_active_user)]
+) -> User:
+    """
+    更新当前用户信息端点
+
+    允许用户更新自己的邮箱和全名信息
+
+    Args:
+        user_update (UserUpdate): 包含要更新的用户信息
+        current_user (User): 通过依赖注入获取的当前用户信息
+
+    Returns:
+        User: 更新后的用户信息
+    """
+    # 只更新非None的字段
+    if user_update.email is not None:
+        fake_users_db[current_user.username]["email"] = user_update.email
+    if user_update.full_name is not None:
+        fake_users_db[current_user.username]["full_name"] = user_update.full_name
+
+    # 获取并返回更新后的用户信息
+    updated_user = get_user(fake_users_db, current_user.username)
+    return User(**updated_user.model_dump())
+
+
+@router.get("/protected", summary="受保护的路由示例", description="演示需要身份验证才能访问的路由")
+async def protected_route(
+        current_user: Annotated[User, Depends(get_current_active_user)]
+) -> dict:
+    """
+    受保护的路由示例
+
+    这个端点演示了如何创建需要身份验证的路由
+    只有提供有效JWT令牌的用户才能访问
+
+    Args:
+        current_user (User): 通过依赖注入获取的当前用户信息
+
+    Returns:
+        dict: 包含欢迎消息的字典
+    """
+    return {
+        "message": f"Hello {current_user.username}, this is a protected route!",
+        "user_info": {
+            "username": current_user.username,
+            "email": current_user.email,
+            "full_name": current_user.full_name
+        },
+        "access_time": datetime.now(timezone.utc).isoformat()
+    }
+
+
+@router.get("/all", summary="获取所有用户", description="获取系统中所有用户的列表(需要管理员权限)")
+async def get_all_users(
+        current_user: Annotated[User, Depends(get_current_active_user)]
+) -> dict:
+    """
+    获取所有用户列表端点
+
+    返回系统中所有用户的信息(不包含密码)
+    注意:在实际应用中,这个功能应该有权限控制
+
+    Args:
+        current_user (User): 通过依赖注入获取的当前用户信息
+
+    Returns:
+        dict: 包含用户列表和总数的字典
+    """
+    users = []
+    for username, user_data in fake_users_db.items():
+        # 创建User对象(不包含密码哈希)
+        user_info = {k: v for k, v in user_data.items() if k != 'hashed_password'}
+        users.append(User(**user_info))
+
+    return {
+        "users": users,
+        "total": len(users),
+        "requested_by": current_user.username
+    }
+
+
+@router.delete("/me", summary="删除用户账户", description="删除当前登录用户的账户")
+async def delete_user_account(
+        current_user: Annotated[User, Depends(get_current_active_user)]
+) -> dict:
+    """
+    删除当前用户账户端点
+
+    允许用户删除自己的账户
+    注意:在实际应用中,可能需要额外的确认步骤
+
+    Args:
+        current_user (User): 通过依赖注入获取的当前用户信息
+
+    Returns:
+        dict: 删除成功的确认消息
+
+    Raises:
+        HTTPException: 如果用户不存在(理论上不会发生)
+    """
+    if current_user.username in fake_users_db:
+        # 从数据库中删除用户
+        del fake_users_db[current_user.username]
+        return {
+            "message": "User account deleted successfully",
+            "deleted_user": current_user.username,
+            "deleted_at": datetime.now(timezone.utc).isoformat()
+        }
+    else:
+        # 这种情况理论上不会发生,因为用户已经通过了身份验证
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail="User not found"
+        )

+ 46 - 0
main.py

@@ -0,0 +1,46 @@
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+from app.routers import users, chat
+
+# 创建FastAPI应用实例
+app = FastAPI(title="聊天机器人", version="1.0.0", description="基于fastapi+VUE的聊天机器人")
+
+# 添加CORS中间件,允许前端跨域访问
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],  # 生产环境建议指定具体域名
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+# 包含用户路由模块
+# prefix: 为该路由模块添加URL前缀,所有用户相关路由都会以/users开头
+# tags: 在API文档中用于分组显示,便于组织和查看
+app.include_router(users.router, prefix="/users", tags=["用户管理"])
+app.include_router(chat.router, prefix="/chat", tags=["聊天管理"])
+
+
+
+# ==================== 应用启动配置 ====================
+
+if __name__ == "__main__":
+    import uvicorn
+
+    # 打印启动信息
+    print("=" * 50)
+    print("聊天机器人服务启动中...")
+    print("=" * 50)
+    print(f"Web界面: http://localhost:8000")
+    print(f"API文档: http://localhost:8000/docs")
+    print("=" * 50)
+
+    # 启动服务器
+    uvicorn.run(
+        "main:app",  # 应用模块路径
+        host="0.0.0.0",  # 监听所有网络接口
+        port=8000,  # 端口号
+        reload=True,  # 开发模式热重载
+        log_level="info"  # 日志级别
+    )