导航菜单

  • 1.langchain.intro
  • 2.langchain.chat_models
  • 3.langchain.prompts
  • 4.langchain.example_selectors
  • 5.output_parsers
  • 6.Runnable
  • 7.memory
  • 8.document_loaders
  • 9.text_splitters
  • 10.embeddings
  • 11.tool
  • 12.retrievers
  • 13.optimize
  • 14.项目介绍
  • 15.启动HTTP
  • 16.数据与模型
  • 17.权限管理
  • 18.知识库管理
  • 19.设置
  • 20.文档管理
  • 21.聊天
  • 22.API文档
  • 23.RAG优化
  • 24.索引时优化
  • 25.检索前优化
  • 26.检索后优化
  • 27.系统优化
  • 28.GraphRAG
  • 29.图
  • 30.为什么选择图数据库
  • 31.什么是 Neo4j
  • 32.安装和连接 Neo4j
  • 33.Neo4j核心概念
  • 34.Cypher基础
  • 35.模式匹配
  • 36.数据CRUD操作
  • 37.GraphRAG
  • 38.查询和过滤
  • 39.结果处理和聚合
  • 40.语句组合
  • 41.子查询
  • 42.模式和约束
  • 43.日期时间处理
  • 44.Cypher内置函数
  • 45.Python操作Neo4j
  • 46.neo4j
  • 47.py2neo
  • 48.Streamlit
  • 49.Pandas
  • 50.graphRAG
  • 51.deepdoc
  • 52.deepdoc
  • 53.deepdoc
  • 55.deepdoc
  • 54.deepdoc
  • Pillow
  • 1.本章目标
  • 2.目录结构
  • 3.用户注册
    • 3.1. init.py
    • 3.2. auth.py
    • 3.3. base_service.py
    • 3.4. user_service.py
    • 3.5. base.html
    • 3.6. home.html
    • 3.7. register.html
    • 3.8. init.py
  • 4.用户登录
    • 4.1. login.html
    • 4.2. auth.py
    • 4.3. user_service.py
    • 4.4. base.html
    • 4.5. home.html
  • 5.退出登录
    • 5.1. auth.py
    • 5.2. init.py
    • 5.3. auth.py
    • 5.4. base_service.py
    • 5.5. user_service.py
    • 5.6. base.html

1.本章目标 #

  1. 介绍项目的权限管理功能与整体架构,帮助读者了解用户身份识别与访问控制的实现方式。
  2. 说明主页、导航栏、登录、注册、登出等涉及权限和身份态管理的前端页面及其行为。
  3. 梳理认证相关蓝图和服务(如 app/blueprints/auth.py, app/services/user_service.py),说明用户注册、登录流程和会话管理的后端逻辑。
  4. 展示如何通过模板上下文(如 current_user)实现细粒度的界面控制,实现“未登录只能访问部分功能,已登录显示更多入口及用户信息”。

2.目录结构 #

# 项目根目录
rag-lite/
    # 源代码文件夹
    ├── app/
        # 蓝图(视图)目录
        │   ├── blueprints/
        # 蓝图初始化文件
        │   │   ├── __init__.py
        # 认证相关路由
        │   │   └── auth.py
        # 数据模型目录
        │   ├── models/
        # 数据模型初始化文件
        │   │   ├── __init__.py
        # 基础模型定义
        │   │   ├── base.py
        # 聊天消息模型
        │   │   ├── chat_message.py
        # 聊天会话模型
        │   │   ├── chat_session.py
        # 文档模型
        │   │   ├── document.py
        # 知识库模型
        │   │   ├── knowledgebase.py
        # 设置模型
        │   │   ├── settings.py
        # 用户模型
        │   │   └── user.py
        # 服务层目录
        │   ├── services/
        # 基础服务类
        │   │   ├── base_service.py
        # 用户服务类
        │   │   └── user_service.py
        # 静态文件目录
        │   ├── static/
        # 模板目录
        │   ├── templates/
        # 基础模板
        │   │   ├── base.html
        # 首页模板
        │   │   ├── home.html
        # 登录页模板
        │   │   ├── login.html
        # 注册页模板
        │   │   └── register.html
        # 工具类目录
        │   ├── utils/
        # 认证工具
        │   │   ├── auth.py
        # 数据库辅助
        │   │   ├── db.py
        # 日志工具
        │   │   └── logger.py
        # app初始化文件
        │   ├── __init__.py
        # app 配置文件
        │   └── config.py
    # 日志目录
    ├── logs/
        # 项目日志文件
        │   └── rag_lite.log
    # 项目入口文件
    ├── main.py
    # python项目配置文件
    └── pyproject.toml

3.用户注册 #

3.1. init.py #

app/blueprints/init.py

"""
蓝图模块
"""
from app.blueprints import auth

__all__ = ['auth']

3.2. auth.py #

app/blueprints/auth.py

# 认证相关路由(视图)
"""
认证相关路由(视图)
"""
# 导入 Flask 所需模块和方法
from flask import Blueprint, render_template, request, redirect, url_for, session, flash
# 导入用户服务
from app.services.user_service import user_service
# 导入日志模块
import logging

# 获取当前模块的 logger
logger = logging.getLogger(__name__)

# 创建名为 'auth' 的 Blueprint 实例
bp = Blueprint('auth', __name__)

# 定义根路径的路由和视图函数
@bp.route('/')
def home():
    # 首页视图,返回 home.html 模板
    """首页"""
    return render_template('home.html')

# 定义注册页面路由,支持 GET 和 POST 方法
@bp.route('/register', methods=['GET', 'POST'])
def register():
    # 注册页面视图
    """注册页面"""
    # 判断请求方法是否为 POST
    if request.method == 'POST':
        # 获取用户输入的用户名并去除首尾空格
        username = request.form.get('username', '').strip()
        # 获取用户输入的密码
        password = request.form.get('password', '')
        # 获取用户输入的确认密码
        password_confirm = request.form.get('password_confirm', '')
        # 获取用户输入的邮箱,并去空格,如果为空则为 None
        email = request.form.get('email', '').strip() or None

        # 验证两次输入的密码是否一致
        if password != password_confirm:
            # 如果不一致,提示错误信息并渲染注册页面
            flash('两次输入的密码不一致', 'error')
            return render_template('register.html')

        try:
            # 调用用户服务进行注册
            user = user_service.register(username, password, email)
            # 注册成功提示并跳转到首页
            flash('注册成功!请登录', 'success')
            return redirect(url_for('auth.home'))
        except ValueError as e:
            # 注册时业务逻辑报错,提示具体信息
            flash(str(e), 'error')
        except Exception as e:
            # 捕获其他异常并写入日志,提示注册失败
            logger.error(f"Registration error: {e}")
            flash('注册失败,请稍后重试', 'error')

    # 如果是 GET 或注册失败则渲染注册页面
    return render_template('register.html')


3.3. base_service.py #

app/services/base_service.py

# 基础服务类
"""
基础服务类
"""
# 导入日志库
import logging
# 导入可选类型、泛型、类型变量和类型别名
from typing import Optional, TypeVar, Generic, Dict, Any
# 导入数据库会话和事务管理工具
from app.utils.db import db_session, db_transaction

# 创建日志记录器
logger = logging.getLogger(__name__)

# 定义泛型的类型变量T
T = TypeVar('T')


# 定义基础服务类,支持泛型
class BaseService(Generic[T]):
    # 基础服务类,提供通用的数据库操作方法

    # 初始化方法
    def __init__(self):
        # 初始化服务的日志记录器
        self.logger = logging.getLogger(self.__class__.__name__)

    # 数据库会话上下文管理器(只读)
    def session(self):
        """
        数据库会话上下文管理器(只读操作,不自动提交)

        使用示例:
            with self.session() as db:
                result = db.query(Model).all()
                # 不需要手动关闭 session
        """
        # 返回数据库会话
        return db_session()

    # 数据库事务上下文管理器(自动提交)
    def transaction(self):
        """
        数据库事务上下文管理器(自动提交,出错时回滚)

        使用示例:
            with self.transaction() as db:
                obj = Model(...)
                db.add(obj)
                # 自动提交,出错时自动回滚
        """
        # 返回数据库事务
        return db_transaction()

3.4. user_service.py #

app/services/user_service.py

# 用户服务
"""
用户服务
"""
# 导入哈希库
import hashlib
# 导入用户模型
from app.models.user import User
# 导入基础服务类
from app.services.base_service import BaseService

# 定义用户服务类,继承自基础服务
class UserService(BaseService[User]):
    # 用户服务的文档说明
    """用户服务"""

    # 静态方法,用于密码哈希
    @staticmethod
    def hash_password(password: str) -> str:
        # 对密码进行哈希处理
        """
        对密码进行哈希处理

        Args:
            password: 原始密码

        Returns:
            哈希后的密码
        """
        # 使用 SHA256 进行哈希(实际生产环境建议使用 bcrypt)
        return hashlib.sha256(password.encode('utf-8')).hexdigest()

    # 注册新用户方法
    def register(self, username: str, password: str, email: str = None) -> dict:
        # 注册新用户
        """
        注册新用户

        Args:
            username: 用户名
            password: 密码
            email: 邮箱(可选)

        Returns:
            用户信息字典

        Raises:
            ValueError: 如果用户名已存在或其他验证错误
        """
        # 检查用户名和密码是否为空
        if not username or not password:
            raise ValueError("用户名和密码不能为空")

        # 检查用户名长度是否小于3
        if len(username) < 3:
            raise ValueError("用户名至少需要3个字符")

        # 检查密码长度是否小于6
        if len(password) < 6:
            raise ValueError("密码至少需要6个字符")

        # 开启事务
        with self.transaction() as session:
            # 检查用户名是否已存在
            existing_user = session.query(User).filter_by(username=username).first()
            # 如果用户名已存在则抛出异常
            if existing_user:
                raise ValueError("用户名已存在")

            # 如果邮箱提供了,检查邮箱是否已被注册
            if email:
                existing_email = session.query(User).filter_by(email=email).first()
                # 如果邮箱已存在则抛出异常
                if existing_email:
                    raise ValueError("邮箱已被注册")

            # 对密码进行哈希处理
            password_hash = self.hash_password(password)
            # 创建新用户对象
            user = User(
                username=username,
                email=email,
                password_hash=password_hash,
                is_active=True
            )

            # 添加新用户到会话
            session.add(user)
            # 刷新会话,获取用户ID
            session.flush()
            # 刷新用户实例
            session.refresh(user)

            # 日志记录注册信息
            self.logger.info(f"User registered: {username}")
            # 返回用户信息字典
            return user.to_dict()

# 创建用户服务实例
user_service = UserService()

3.5. base.html #

app/templates/base.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}RAG Lite - RAG 系统{% endblock %}</title>

    <!-- Bootstrap CSS -->
    <link href="https://static.docs-hub.com/bootstrapmin_1767018057480.css" rel="stylesheet">
    <!-- Bootstrap Icons -->
    <link href="https://static.docs-hub.com/bootstrapicons_1767018066482.css" rel="stylesheet">

    <style>
        body {
            background-color: #f8f9fa;
        }
        .navbar-brand {
            font-weight: 600;
        }
        .card {
            border: none;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            transition: transform 0.2s, box-shadow 0.2s;
        }
        .card:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0,0,0,0.15);
        }
        .chat-message {
            padding: 1rem;
            margin-bottom: 1rem;
            border-radius: 0.5rem;
        }
        .chat-question {
            background-color: #e3f2fd;
        }
        .chat-answer {
            background-color: #f5f5f5;
        }
        .source-item {
            padding: 0.5rem;
            margin-bottom: 0.5rem;
            background-color: #fff;
            border-left: 3px solid #0d6efd;
            border-radius: 0.25rem;
        }
        .markdown-content {
            line-height: 1.6;
        }
        .markdown-content h1, .markdown-content h2, .markdown-content h3,
        .markdown-content h4, .markdown-content h5, .markdown-content h6 {
            margin-top: 1.5rem;
            margin-bottom: 1rem;
            font-weight: 600;
        }
        .markdown-content h1 { font-size: 2rem; }
        .markdown-content h2 { font-size: 1.75rem; }
        .markdown-content h3 { font-size: 1.5rem; }
        .markdown-content h4 { font-size: 1.25rem; }
        .markdown-content p {
            margin-bottom: 1rem;
        }
        .markdown-content ul, .markdown-content ol {
            margin-bottom: 1rem;
            padding-left: 2rem;
        }
        .markdown-content code {
            background-color: #f4f4f4;
            padding: 0.2rem 0.4rem;
            border-radius: 0.25rem;
            font-size: 0.9em;
        }
        .markdown-content pre {
            background-color: #f4f4f4;
            padding: 1rem;
            border-radius: 0.5rem;
            overflow-x: auto;
            margin-bottom: 1rem;
        }
        .markdown-content pre code {
            background-color: transparent;
            padding: 0;
        }
        .markdown-content blockquote {
            border-left: 4px solid #0d6efd;
            padding-left: 1rem;
            margin-left: 0;
            color: #6c757d;
            margin-bottom: 1rem;
        }
        .markdown-content table {
            width: 100%;
            border-collapse: collapse;
            margin-bottom: 1rem;
        }
        .markdown-content table th,
        .markdown-content table td {
            border: 1px solid #dee2e6;
            padding: 0.5rem;
        }
        .markdown-content table th {
            background-color: #f8f9fa;
            font-weight: 600;
        }
        .markdown-content a {
            color: #0d6efd;
            text-decoration: none;
        }
        .markdown-content a:hover {
            text-decoration: underline;
        }
        /* 打字机效果优化 */
        .markdown-content {
            word-wrap: break-word;
            overflow-wrap: break-word;
        }
        /* 平滑滚动 */
        #chatArea {
            scroll-behavior: smooth;
        }
    </style>

    {% block extra_css %}{% endblock %}
</head>
<body>
    <!-- 导航栏 -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container-fluid">
            <a class="navbar-brand" href="/">
                <i class="bi bi-book"></i> RAG Lite
            </a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav me-auto">
                    <li class="nav-item">
                        <a class="nav-link {% if request.endpoint == 'auth.home' %}active{% endif %}" href="/">
                            <i class="bi bi-house"></i> 首页
                        </a>
                    </li>
                    {% if current_user %}
                    <li class="nav-item">
                        <a class="nav-link {% if request.endpoint == 'knowledgebase.kb_list' or request.endpoint == 'knowledgebase.kb_detail' %}active{% endif %}" href="/kb">
                            <i class="bi bi-database"></i> 知识库
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link {% if request.endpoint == 'chat.chat' %}active{% endif %}" href="/chat">
                            <i class="bi bi-chat-dots"></i> 聊天
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link {% if request.endpoint == 'settings.settings' %}active{% endif %}" href="/settings">
                            <i class="bi bi-gear"></i> 设置
                        </a>
                    </li>
                    {% endif %}
                </ul>
                <ul class="navbar-nav">
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('auth.home') }}">
                            <i class="bi bi-box-arrow-in-right"></i> 登录
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('auth.register') }}">
                            <i class="bi bi-person-plus"></i> 注册
                        </a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>

    <!-- 主要内容 -->
    <div class="container-fluid mt-4">
        <!-- Flash 消息 -->
        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
                <div class="mb-3">
                    {% for category, message in messages %}
                        <div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
                            {{ message }}
                            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
                        </div>
                    {% endfor %}
                </div>
            {% endif %}
        {% endwith %}

        {% block content %}{% endblock %}
    </div>

    <!-- Bootstrap JS -->
    <script src="https://static.docs-hub.com/bootstrapbundlemin_1767018104304.js"></script>
    <!-- Marked.js for Markdown rendering -->
    <script src="https://static.docs-hub.com/markedmin_1767018117982.js"></script>
    {% block extra_js %}{% endblock %}
</body>
</html>

3.6. home.html #

app/templates/home.html

{% extends "base.html" %}

{% block title %}首页 - RAG Lite{% endblock %}

{% block content %}
{% if not current_user %}
<div class="alert alert-info" role="alert">
    <h5 class="alert-heading"><i class="bi bi-info-circle"></i> 提示</h5>
    <p class="mb-0">访问知识库管理和聊天功能需要先 <a href="{{ url_for('auth.home') }}" class="alert-link">登录</a> 或 <a href="{{ url_for('auth.register') }}" class="alert-link">注册</a>。</p>
</div>
{% endif %}
<div class="row">
    <div class="col-12">
        <!-- 项目介绍 -->
        <div class="card mb-4">
            <div class="card-body text-center py-5">
                <h1 class="display-4 mb-4">
                    <i class="bi bi-book"></i> RAG Lite
                </h1>
                <p class="lead mb-4">
                    RAG(检索增强生成)系统
                </p>
                <p class="text-muted mb-4">
                    基于 LangChain 和 DeepSeek API 构建的轻量级知识库问答系统
                </p>
                <div class="d-flex justify-content-center gap-3">
                    <a href="/kb" class="btn btn-primary btn-lg">
                        <i class="bi bi-collection"></i> 知识库管理
                    </a>
                    <a href="/chat" class="btn btn-outline-primary btn-lg">
                        <i class="bi bi-chat-dots"></i> 开始聊天
                    </a>
                </div>
            </div>
        </div>

        <!-- 功能特性 -->
        <div class="row mb-4">
            <div class="col-md-4 mb-4">
                <div class="card h-100">
                    <div class="card-body text-center">
                        <div class="mb-3">
                            <i class="bi bi-file-earmark-text" style="font-size: 3rem; color: #0d6efd;"></i>
                        </div>
                        <h5 class="card-title">文档管理</h5>
                        <p class="card-text text-muted">
                            支持 PDF、DOCX、TXT、MD 等多种格式文档上传和管理
                        </p>
                    </div>
                </div>
            </div>
            <div class="col-md-4 mb-4">
                <div class="card h-100">
                    <div class="card-body text-center">
                        <div class="mb-3">
                            <i class="bi bi-search" style="font-size: 3rem; color: #0d6efd;"></i>
                        </div>
                        <h5 class="card-title">智能检索</h5>
                        <p class="card-text text-muted">
                            基于向量相似度的智能文档检索,快速找到相关内容
                        </p>
                    </div>
                </div>
            </div>
            <div class="col-md-4 mb-4">
                <div class="card h-100">
                    <div class="card-body text-center">
                        <div class="mb-3">
                            <i class="bi bi-robot" style="font-size: 3rem; color: #0d6efd;"></i>
                        </div>
                        <h5 class="card-title">智能问答</h5>
                        <p class="card-text text-muted">
                            基于文档内容的智能问答,支持流式输出和 Markdown 渲染
                        </p>
                    </div>
                </div>
            </div>
        </div>

        <!-- 快速开始 -->
        <div class="card mt-4">
            <div class="card-header">
                <h5 class="mb-0"><i class="bi bi-lightning-charge"></i> 快速开始</h5>
            </div>
            <div class="card-body">
                <ol>
                    <li class="mb-2">
                        <strong>创建知识库</strong>
                        <p class="text-muted small mb-0">在知识库管理页面创建新的知识库,设置分块参数和检索参数</p>
                    </li>
                    <li class="mb-2">
                        <strong>上传文档</strong>
                        <p class="text-muted small mb-0">在知识库详情页面上传文档,系统会自动解析、分块和向量化</p>
                    </li>
                    <li class="mb-2">
                        <strong>开始问答</strong>
                        <p class="text-muted small mb-0">在聊天页面选择知识库,输入问题即可获得基于文档内容的智能回答</p>
                    </li>
                </ol>
            </div>
        </div>
    </div>
</div>
{% endblock %}

3.7. register.html #

app/templates/register.html

{% extends "base.html" %}

{% block title %}注册 - RAG Lite{% endblock %}

{% block content %}
<div class="row justify-content-center">
    <div class="col-md-5 col-lg-4">
        <div class="card shadow">
            <div class="card-header bg-success text-white">
                <h4 class="mb-0"><i class="bi bi-person-plus"></i> 用户注册</h4>
            </div>
            <div class="card-body">
                <form method="POST" action="{{ url_for('auth.register') }}">
                    <div class="mb-3">
                        <label for="username" class="form-label">用户名 <span class="text-danger">*</span></label>
                        <input type="text" class="form-control" id="username" name="username" 
                               placeholder="至少3个字符" required autofocus minlength="3">
                        <div class="form-text">用户名将用于登录,至少需要3个字符</div>
                    </div>

                    <div class="mb-3">
                        <label for="email" class="form-label">邮箱</label>
                        <input type="email" class="form-control" id="email" name="email" 
                               placeholder="example@email.com">
                        <div class="form-text">邮箱为可选,用于找回密码等</div>
                    </div>

                    <div class="mb-3">
                        <label for="password" class="form-label">密码 <span class="text-danger">*</span></label>
                        <input type="password" class="form-control" id="password" name="password" 
                               placeholder="至少6个字符" required minlength="6">
                        <div class="form-text">密码至少需要6个字符</div>
                    </div>

                    <div class="mb-3">
                        <label for="password_confirm" class="form-label">确认密码 <span class="text-danger">*</span></label>
                        <input type="password" class="form-control" id="password_confirm" name="password_confirm" 
                               placeholder="请再次输入密码" required minlength="6">
                    </div>

                    <div class="d-grid gap-2">
                        <button type="submit" class="btn btn-success">
                            <i class="bi bi-person-plus"></i> 注册
                        </button>
                    </div>
                </form>

                <hr>

                <div class="text-center">
                    <p class="mb-0">已有账号? <a href="{{ url_for('auth.home') }}">立即登录</a></p>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

3.8. init.py #

app/init.py

# RAG Lite 应用模块说明
"""
RAG Lite Application
"""

# 导入操作系统相关模块
import os
# 从 Flask 包导入 Flask 应用对象
from flask import Flask
# 导入 Flask 跨域资源共享支持
from flask_cors import CORS
# 导入应用配置类
from app.config import Config
# 导入日志工具,用于获取日志记录器
from app.utils.logger import get_logger
# 导入数据库初始化函数
from app.utils.db import init_db
# 导入蓝图模块
+from app.blueprints import auth

# 定义创建 Flask 应用的工厂函数
def create_app(config_class=Config):
    # 获取日志记录器,名称为当前模块名
    logger = get_logger(__name__)
    # 尝试初始化数据库
    try:
        # 输出日志,表示即将初始化数据库
        logger.info("初始化数据库...")
        # 执行数据库初始化函数
        init_db()
        # 输出日志,表示数据库初始化成功
        logger.info("数据库初始化成功")
    # 捕获任意异常
    except Exception as e:
        # 输出警告日志,提示数据库初始化失败,并输出异常信息
        logger.warning(f"数据库初始化失败: {e}")
        # 输出警告日志,提示检查数据库是否已存在,并建议手动创建数据表
        logger.warning("请确认数据库已存在,或手动创建数据表")

    # 创建 Flask 应用对象,并指定模板和静态文件目录
    base_dir = os.path.abspath(os.path.dirname(__file__))
    # 创建 Flask 应用对象,并指定模板和静态文件目录
    app = Flask(
        __name__,
        # 指定模板文件目录
        template_folder=os.path.join(base_dir, 'templates'),
        # 指定静态文件目录
        static_folder=os.path.join(base_dir, 'static')
    )
    # 从给定配置类加载配置信息到应用
    app.config.from_object(config_class)

    # 启用跨域请求支持
    CORS(app)

    # 记录应用创建日志信息
    logger.info("Flask 应用已创建")
    # 注册蓝图
+   app.register_blueprint(auth.bp)

    # 定义首页路由
    @app.route('/')
    def index():
        return "Hello, World!"

    # 返回已配置的 Flask 应用对象
    return app

4.用户登录 #

4.1. login.html #

app/templates/login.html

{% extends "base.html" %}

{% block title %}登录 - RAG Lite{% endblock %}

{% block content %}
<div class="row justify-content-center">
    <div class="col-md-5 col-lg-4">
        <div class="card shadow">
            <div class="card-header bg-primary text-white">
                <h4 class="mb-0"><i class="bi bi-box-arrow-in-right"></i> 用户登录</h4>
            </div>
            <div class="card-body">
                <form method="POST" action="{{ url_for('auth.login') }}">
                    {% if next_url %}
                    <input type="hidden" name="next" value="{{ next_url }}">
                    {% endif %}

                    <div class="mb-3">
                        <label for="username" class="form-label">用户名</label>
                        <input type="text" class="form-control" id="username" name="username" 
                               placeholder="请输入用户名" required autofocus>
                    </div>

                    <div class="mb-3">
                        <label for="password" class="form-label">密码</label>
                        <input type="password" class="form-control" id="password" name="password" 
                               placeholder="请输入密码" required>
                    </div>

                    <div class="d-grid gap-2">
                        <button type="submit" class="btn btn-primary">
                            <i class="bi bi-box-arrow-in-right"></i> 登录
                        </button>
                    </div>
                </form>

                <hr>

                <div class="text-center">
                    <p class="mb-0">还没有账号? <a href="{{ url_for('auth.register') }}">立即注册</a></p>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

4.2. auth.py #

app/blueprints/auth.py

# 认证相关路由(视图)
"""
认证相关路由(视图)
"""
# 导入 Flask 所需模块和方法
from flask import Blueprint, render_template, request, redirect, url_for, session, flash
# 导入用户服务
from app.services.user_service import user_service
# 导入日志模块
import logging

# 获取当前模块的 logger
logger = logging.getLogger(__name__)

# 创建名为 'auth' 的 Blueprint 实例
bp = Blueprint('auth', __name__)

# 定义根路径的路由和视图函数
@bp.route('/')
def home():
    # 首页视图,返回 home.html 模板
    """首页"""
    return render_template('home.html')

# 定义注册页面路由,支持 GET 和 POST 方法
@bp.route('/register', methods=['GET', 'POST'])
def register():
    # 注册页面视图
    """注册页面"""
    # 判断请求方法是否为 POST
    if request.method == 'POST':
        # 获取用户输入的用户名并去除首尾空格
        username = request.form.get('username', '').strip()
        # 获取用户输入的密码
        password = request.form.get('password', '')
        # 获取用户输入的确认密码
        password_confirm = request.form.get('password_confirm', '')
        # 获取用户输入的邮箱,并去空格,如果为空则为 None
        email = request.form.get('email', '').strip() or None

        # 验证两次输入的密码是否一致
        if password != password_confirm:
            # 如果不一致,提示错误信息并渲染注册页面
            flash('两次输入的密码不一致', 'error')
            return render_template('register.html')

        try:
            # 调用用户服务进行注册
            user = user_service.register(username, password, email)
            # 注册成功提示并跳转到首页
            flash('注册成功!请登录', 'success')
            return redirect(url_for('auth.home'))
        except ValueError as e:
            # 注册时业务逻辑报错,提示具体信息
            flash(str(e), 'error')
        except Exception as e:
            # 捕获其他异常并写入日志,提示注册失败
            logger.error(f"Registration error: {e}")
            flash('注册失败,请稍后重试', 'error')

    # 如果是 GET 或注册失败则渲染注册页面
    return render_template('register.html')


# 定义 '/login' 路由,支持 GET 和 POST 方法
+@bp.route('/login', methods=['GET', 'POST'])
# 定义登录视图函数
+def login():
    # 登录页面说明
+   """登录页面"""
    # 如果请求方法为 POST,表示尝试登录
+   if request.method == 'POST':
        # 获取表单中填写的用户名,并去除首尾空格
+       username = request.form.get('username', '').strip()
        # 获取表单中填写的密码
+       password = request.form.get('password', '')
        # 获取登录后重定向的目标页面(优先表单,其次URL参数)
+       next_url = request.form.get('next') or request.args.get('next')

+       try:
            # 调用用户服务进行登录校验,返回用户信息
+           user = user_service.login(username, password)
            # 将用户ID保存到会话
+           session['user_id'] = user['id']
            # 将用户名保存到会话
+           session['username'] = user['username']
            # 设置会话为永久有效(默认31天)
+           session.permanent = True  

            # 登录成功的提示信息
+           flash('登录成功!', 'success')

            # 登录成功后重定向到“next_url”或知识库列表页面
+           return redirect(next_url or url_for('auth.home'))
        # 如果抛出 ValueError,提示具体错误(如用户名或密码不正确)
+       except ValueError as e:
+           flash(str(e), 'error')
        # 捕获其他异常,记录日志并提示通用登录失败
+       except Exception as e:
+           logger.error(f"Login error: {e}")
+           flash('登录失败,请稍后重试', 'error')

    # 如果是 GET 请求或者登录失败,获取 URL 参数中的 next
+   next_url = request.args.get('next')
    # 渲染登录页面模板,并传递 next_url 变量
+   return render_template('login.html', next_url=next_url)

4.3. user_service.py #

app/services/user_service.py

# 用户服务
"""
用户服务
"""
# 导入哈希库
import hashlib
# 导入用户模型
from app.models.user import User
# 导入基础服务类
from app.services.base_service import BaseService

# 定义用户服务类,继承自基础服务
class UserService(BaseService[User]):
    # 用户服务的文档说明
    """用户服务"""

    # 静态方法,用于密码哈希
    @staticmethod
    def hash_password(password: str) -> str:
        # 对密码进行哈希处理
        """
        对密码进行哈希处理

        Args:
            password: 原始密码

        Returns:
            哈希后的密码
        """
        # 使用 SHA256 进行哈希(实际生产环境建议使用 bcrypt)
        return hashlib.sha256(password.encode('utf-8')).hexdigest()

    # 注册新用户方法
    def register(self, username: str, password: str, email: str = None) -> dict:
        # 注册新用户
        """
        注册新用户

        Args:
            username: 用户名
            password: 密码
            email: 邮箱(可选)

        Returns:
            用户信息字典

        Raises:
            ValueError: 如果用户名已存在或其他验证错误
        """
        # 检查用户名和密码是否为空
        if not username or not password:
            raise ValueError("用户名和密码不能为空")

        # 检查用户名长度是否小于3
        if len(username) < 3:
            raise ValueError("用户名至少需要3个字符")

        # 检查密码长度是否小于6
        if len(password) < 6:
            raise ValueError("密码至少需要6个字符")

        # 开启事务
        with self.transaction() as session:
            # 检查用户名是否已存在
            existing_user = session.query(User).filter_by(username=username).first()
            # 如果用户名已存在则抛出异常
            if existing_user:
                raise ValueError("用户名已存在")

            # 如果邮箱提供了,检查邮箱是否已被注册
            if email:
                existing_email = session.query(User).filter_by(email=email).first()
                # 如果邮箱已存在则抛出异常
                if existing_email:
                    raise ValueError("邮箱已被注册")

            # 对密码进行哈希处理
            password_hash = self.hash_password(password)
            # 创建新用户对象
            user = User(
                username=username,
                email=email,
                password_hash=password_hash,
                is_active=True
            )

            # 添加新用户到会话
            session.add(user)
            # 刷新会话,获取用户ID
            session.flush()
            # 刷新用户实例
            session.refresh(user)

            # 日志记录注册信息
            self.logger.info(f"User registered: {username}")
            # 返回用户信息字典
            return user.to_dict()
    # 验证密码方法,比较原始密码和哈希密码是否一致
+   def verify_password(self, password: str, password_hash: str) -> bool:
+       """
+       验证密码

+       Args:
+           password: 原始密码
+           password_hash: 哈希后的密码

+       Returns:
+           是否匹配
+       """
        # 对输入的原始密码进行哈希,并与数据库中保存的哈希值进行比较,返回是否一致
+       return self.hash_password(password) == password_hash        

    # 用户登录验证方法
+   def login(self, username: str, password: str) -> dict:
+       """
+       用户登录验证

+       Args:
+           username: 用户名
+           password: 密码

+       Returns:
+           用户信息字典

+       Raises:
+           ValueError: 如果用户名或密码错误
+       """
        # 检查用户名和密码是否为空,如果为空抛出异常
+       if not username or not password:
+           raise ValueError("用户名和密码不能为空")

        # 开启 SQLAlchemy 会话
+       with self.session() as session:
            # 根据用户名查询用户对象
+           user = session.query(User).filter_by(username=username).first()

            # 如果没有查到用户,则抛出用户名或密码错误的异常
+           if not user:
+               raise ValueError("用户名或密码错误")

            # 如果用户账户被禁用,则抛出账户已被禁用异常
+           if not user.is_active:
+               raise ValueError("账户已被禁用")

            # 校验密码,若不匹配则抛出异常
+           if not self.verify_password(password, user.password_hash):
+               raise ValueError("用户名或密码错误")

            # 登录成功,记录日志
+           self.logger.info(f"User logged in: {username}")
            # 返回用户信息字典
+           return user.to_dict()        

# 创建用户服务实例
user_service = UserService()

4.4. base.html #

app/templates/base.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}RAG Lite - RAG 系统{% endblock %}</title>

    <!-- Bootstrap CSS -->
    <link href="https://static.docs-hub.com/bootstrapmin_1767018057480.css" rel="stylesheet">
    <!-- Bootstrap Icons -->
    <link href="https://static.docs-hub.com/bootstrapicons_1767018066482.css" rel="stylesheet">

    <style>
        body {
            background-color: #f8f9fa;
        }
        .navbar-brand {
            font-weight: 600;
        }
        .card {
            border: none;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            transition: transform 0.2s, box-shadow 0.2s;
        }
        .card:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0,0,0,0.15);
        }
        .chat-message {
            padding: 1rem;
            margin-bottom: 1rem;
            border-radius: 0.5rem;
        }
        .chat-question {
            background-color: #e3f2fd;
        }
        .chat-answer {
            background-color: #f5f5f5;
        }
        .source-item {
            padding: 0.5rem;
            margin-bottom: 0.5rem;
            background-color: #fff;
            border-left: 3px solid #0d6efd;
            border-radius: 0.25rem;
        }
        .markdown-content {
            line-height: 1.6;
        }
        .markdown-content h1, .markdown-content h2, .markdown-content h3,
        .markdown-content h4, .markdown-content h5, .markdown-content h6 {
            margin-top: 1.5rem;
            margin-bottom: 1rem;
            font-weight: 600;
        }
        .markdown-content h1 { font-size: 2rem; }
        .markdown-content h2 { font-size: 1.75rem; }
        .markdown-content h3 { font-size: 1.5rem; }
        .markdown-content h4 { font-size: 1.25rem; }
        .markdown-content p {
            margin-bottom: 1rem;
        }
        .markdown-content ul, .markdown-content ol {
            margin-bottom: 1rem;
            padding-left: 2rem;
        }
        .markdown-content code {
            background-color: #f4f4f4;
            padding: 0.2rem 0.4rem;
            border-radius: 0.25rem;
            font-size: 0.9em;
        }
        .markdown-content pre {
            background-color: #f4f4f4;
            padding: 1rem;
            border-radius: 0.5rem;
            overflow-x: auto;
            margin-bottom: 1rem;
        }
        .markdown-content pre code {
            background-color: transparent;
            padding: 0;
        }
        .markdown-content blockquote {
            border-left: 4px solid #0d6efd;
            padding-left: 1rem;
            margin-left: 0;
            color: #6c757d;
            margin-bottom: 1rem;
        }
        .markdown-content table {
            width: 100%;
            border-collapse: collapse;
            margin-bottom: 1rem;
        }
        .markdown-content table th,
        .markdown-content table td {
            border: 1px solid #dee2e6;
            padding: 0.5rem;
        }
        .markdown-content table th {
            background-color: #f8f9fa;
            font-weight: 600;
        }
        .markdown-content a {
            color: #0d6efd;
            text-decoration: none;
        }
        .markdown-content a:hover {
            text-decoration: underline;
        }
        /* 打字机效果优化 */
        .markdown-content {
            word-wrap: break-word;
            overflow-wrap: break-word;
        }
        /* 平滑滚动 */
        #chatArea {
            scroll-behavior: smooth;
        }
    </style>

    {% block extra_css %}{% endblock %}
</head>
<body>
    <!-- 导航栏 -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container-fluid">
            <a class="navbar-brand" href="/">
                <i class="bi bi-book"></i> RAG Lite
            </a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav me-auto">
                    <li class="nav-item">
                        <a class="nav-link {% if request.endpoint == 'auth.home' %}active{% endif %}" href="/">
                            <i class="bi bi-house"></i> 首页
                        </a>
                    </li>
                    {% if current_user %}
                    <li class="nav-item">
                        <a class="nav-link {% if request.endpoint == 'knowledgebase.kb_list' or request.endpoint == 'knowledgebase.kb_detail' %}active{% endif %}" href="/kb">
                            <i class="bi bi-database"></i> 知识库
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link {% if request.endpoint == 'chat.chat' %}active{% endif %}" href="/chat">
                            <i class="bi bi-chat-dots"></i> 聊天
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link {% if request.endpoint == 'settings.settings' %}active{% endif %}" href="/settings">
                            <i class="bi bi-gear"></i> 设置
                        </a>
                    </li>
                    {% endif %}
                </ul>
                <ul class="navbar-nav">
                    <li class="nav-item">
+                       <a class="nav-link" href="{{ url_for('auth.login') }}">
                            <i class="bi bi-box-arrow-in-right"></i> 登录
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('auth.register') }}">
                            <i class="bi bi-person-plus"></i> 注册
                        </a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>

    <!-- 主要内容 -->
    <div class="container-fluid mt-4">
        <!-- Flash 消息 -->
        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
                <div class="mb-3">
                    {% for category, message in messages %}
                        <div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
                            {{ message }}
                            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
                        </div>
                    {% endfor %}
                </div>
            {% endif %}
        {% endwith %}

        {% block content %}{% endblock %}
    </div>

    <!-- Bootstrap JS -->
    <script src="https://static.docs-hub.com/bootstrapbundlemin_1767018104304.js"></script>
    <!-- Marked.js for Markdown rendering -->
    <script src="https://static.docs-hub.com/markedmin_1767018117982.js"></script>
    {% block extra_js %}{% endblock %}
</body>
</html>

4.5. home.html #

app/templates/home.html

{% extends "base.html" %}

{% block title %}首页 - RAG Lite{% endblock %}

{% block content %}
{% if not current_user %}
<div class="alert alert-info" role="alert">
    <h5 class="alert-heading"><i class="bi bi-info-circle"></i> 提示</h5>
+   <p class="mb-0">访问知识库管理和聊天功能需要先 <a href="{{ url_for('auth.login') }}" class="alert-link">登录</a> 或 <a href="{{ url_for('auth.register') }}" class="alert-link">注册</a>。</p>
</div>
{% endif %}
<div class="row">
    <div class="col-12">
        <!-- 项目介绍 -->
        <div class="card mb-4">
            <div class="card-body text-center py-5">
                <h1 class="display-4 mb-4">
                    <i class="bi bi-book"></i> RAG Lite
                </h1>
                <p class="lead mb-4">
                    RAG(检索增强生成)系统
                </p>
                <p class="text-muted mb-4">
                    基于 LangChain 和 DeepSeek API 构建的轻量级知识库问答系统
                </p>
                <div class="d-flex justify-content-center gap-3">
                    <a href="/kb" class="btn btn-primary btn-lg">
                        <i class="bi bi-collection"></i> 知识库管理
                    </a>
                    <a href="/chat" class="btn btn-outline-primary btn-lg">
                        <i class="bi bi-chat-dots"></i> 开始聊天
                    </a>
                </div>
            </div>
        </div>

        <!-- 功能特性 -->
        <div class="row mb-4">
            <div class="col-md-4 mb-4">
                <div class="card h-100">
                    <div class="card-body text-center">
                        <div class="mb-3">
                            <i class="bi bi-file-earmark-text" style="font-size: 3rem; color: #0d6efd;"></i>
                        </div>
                        <h5 class="card-title">文档管理</h5>
                        <p class="card-text text-muted">
                            支持 PDF、DOCX、TXT、MD 等多种格式文档上传和管理
                        </p>
                    </div>
                </div>
            </div>
            <div class="col-md-4 mb-4">
                <div class="card h-100">
                    <div class="card-body text-center">
                        <div class="mb-3">
                            <i class="bi bi-search" style="font-size: 3rem; color: #0d6efd;"></i>
                        </div>
                        <h5 class="card-title">智能检索</h5>
                        <p class="card-text text-muted">
                            基于向量相似度的智能文档检索,快速找到相关内容
                        </p>
                    </div>
                </div>
            </div>
            <div class="col-md-4 mb-4">
                <div class="card h-100">
                    <div class="card-body text-center">
                        <div class="mb-3">
                            <i class="bi bi-robot" style="font-size: 3rem; color: #0d6efd;"></i>
                        </div>
                        <h5 class="card-title">智能问答</h5>
                        <p class="card-text text-muted">
                            基于文档内容的智能问答,支持流式输出和 Markdown 渲染
                        </p>
                    </div>
                </div>
            </div>
        </div>

        <!-- 快速开始 -->
        <div class="card mt-4">
            <div class="card-header">
                <h5 class="mb-0"><i class="bi bi-lightning-charge"></i> 快速开始</h5>
            </div>
            <div class="card-body">
                <ol>
                    <li class="mb-2">
                        <strong>创建知识库</strong>
                        <p class="text-muted small mb-0">在知识库管理页面创建新的知识库,设置分块参数和检索参数</p>
                    </li>
                    <li class="mb-2">
                        <strong>上传文档</strong>
                        <p class="text-muted small mb-0">在知识库详情页面上传文档,系统会自动解析、分块和向量化</p>
                    </li>
                    <li class="mb-2">
                        <strong>开始问答</strong>
                        <p class="text-muted small mb-0">在聊天页面选择知识库,输入问题即可获得基于文档内容的智能回答</p>
                    </li>
                </ol>
            </div>
        </div>
    </div>
</div>
{% endblock %}

5.退出登录 #

5.1. auth.py #

app/utils/auth.py

# 认证工具
"""
认证工具
"""
# 从 Flask 导入 session 和 g(全局对象)
from flask import session, g
# 导入日志库 logging
import logging
# 导入 user_service 服务
from app.services.user_service import user_service
# 获取当前模块的日志记录器实例
logger = logging.getLogger(__name__)

# 定义获取当前登录用户信息的函数
def get_current_user():
    """
    获取当前登录用户信息
    使用 Flask 的 g 对象缓存,避免重复查询

    Returns:
        用户信息字典,如果未登录则返回 None
    """
    # 如果 g 对象没有 current_user 属性
    if not hasattr(g, 'current_user'):
        # 如果会话中有 user_id 字段
        if 'user_id' in session:
            # 通过 user_id 获取用户信息并缓存到 g.current_user
            g.current_user = user_service.get_by_id(session['user_id'])
        else:
            # 如果没有登录,将 g.current_user 设为 None
            g.current_user = None
    # 返回当前用户信息
    return g.current_user

5.2. init.py #

app/init.py

# RAG Lite 应用模块说明
"""
RAG Lite Application
"""

# 导入操作系统相关模块
import os
# 从 Flask 包导入 Flask 应用对象
from flask import Flask
# 导入 Flask 跨域资源共享支持
from flask_cors import CORS
# 导入应用配置类
from app.config import Config
# 导入日志工具,用于获取日志记录器
from app.utils.logger import get_logger
# 导入数据库初始化函数
from app.utils.db import init_db
# 导入蓝图模块
from app.blueprints import auth
# 导入获取当前用户信息函数
+from app.utils.auth import get_current_user
# 定义创建 Flask 应用的工厂函数
def create_app(config_class=Config):
    # 获取日志记录器,名称为当前模块名
    logger = get_logger(__name__)
    # 尝试初始化数据库
    try:
        # 输出日志,表示即将初始化数据库
        logger.info("初始化数据库...")
        # 执行数据库初始化函数
        init_db()
        # 输出日志,表示数据库初始化成功
        logger.info("数据库初始化成功")
    # 捕获任意异常
    except Exception as e:
        # 输出警告日志,提示数据库初始化失败,并输出异常信息
        logger.warning(f"数据库初始化失败: {e}")
        # 输出警告日志,提示检查数据库是否已存在,并建议手动创建数据表
        logger.warning("请确认数据库已存在,或手动创建数据表")

    # 创建 Flask 应用对象,并指定模板和静态文件目录
    base_dir = os.path.abspath(os.path.dirname(__file__))
    # 创建 Flask 应用对象,并指定模板和静态文件目录
    app = Flask(
        __name__,
        # 指定模板文件目录
        template_folder=os.path.join(base_dir, 'templates'),
        # 指定静态文件目录
        static_folder=os.path.join(base_dir, 'static')
    )
    # 从给定配置类加载配置信息到应用
    app.config.from_object(config_class)

    # 启用跨域请求支持
    CORS(app)

    # 记录应用创建日志信息
    logger.info("Flask 应用已创建")

    # 注册上下文处理器,使 current_user 在所有模板中可用
+   @app.context_processor
+   def inject_user():
        # 返回当前用户信息字典
        # 使用 get_current_user 获取当前用户信息,并将其添加到上下文字典中
        # 这样在模板中可以直接使用 current_user 变量
+       return dict(current_user=get_current_user())

    # 注册蓝图
    app.register_blueprint(auth.bp)

    # 定义首页路由
    @app.route('/')
    def index():
        return "Hello, World!"

    # 返回已配置的 Flask 应用对象
    return app

5.3. auth.py #

app/blueprints/auth.py

# 认证相关路由(视图)
"""
认证相关路由(视图)
"""
# 导入 Flask 所需模块和方法
from flask import Blueprint, render_template, request, redirect, url_for, session, flash
# 导入用户服务
from app.services.user_service import user_service
# 导入日志模块
import logging

# 获取当前模块的 logger
logger = logging.getLogger(__name__)

# 创建名为 'auth' 的 Blueprint 实例
bp = Blueprint('auth', __name__)

# 定义根路径的路由和视图函数
@bp.route('/')
def home():
    # 首页视图,返回 home.html 模板
    """首页"""
    return render_template('home.html')

# 定义注册页面路由,支持 GET 和 POST 方法
@bp.route('/register', methods=['GET', 'POST'])
def register():
    # 注册页面视图
    """注册页面"""
    # 判断请求方法是否为 POST
    if request.method == 'POST':
        # 获取用户输入的用户名并去除首尾空格
        username = request.form.get('username', '').strip()
        # 获取用户输入的密码
        password = request.form.get('password', '')
        # 获取用户输入的确认密码
        password_confirm = request.form.get('password_confirm', '')
        # 获取用户输入的邮箱,并去空格,如果为空则为 None
        email = request.form.get('email', '').strip() or None

        # 验证两次输入的密码是否一致
        if password != password_confirm:
            # 如果不一致,提示错误信息并渲染注册页面
            flash('两次输入的密码不一致', 'error')
            return render_template('register.html')

        try:
            # 调用用户服务进行注册
            user = user_service.register(username, password, email)
            # 注册成功提示并跳转到首页
            flash('注册成功!请登录', 'success')
            return redirect(url_for('auth.home'))
        except ValueError as e:
            # 注册时业务逻辑报错,提示具体信息
            flash(str(e), 'error')
        except Exception as e:
            # 捕获其他异常并写入日志,提示注册失败
            logger.error(f"Registration error: {e}")
            flash('注册失败,请稍后重试', 'error')

    # 如果是 GET 或注册失败则渲染注册页面
    return render_template('register.html')


# 定义 '/login' 路由,支持 GET 和 POST 方法
@bp.route('/login', methods=['GET', 'POST'])
# 定义登录视图函数
def login():
    # 登录页面说明
    """登录页面"""
    # 如果请求方法为 POST,表示尝试登录
    if request.method == 'POST':
        # 获取表单中填写的用户名,并去除首尾空格
        username = request.form.get('username', '').strip()
        # 获取表单中填写的密码
        password = request.form.get('password', '')
        # 获取登录后重定向的目标页面(优先表单,其次URL参数)
        next_url = request.form.get('next') or request.args.get('next')

        try:
            # 调用用户服务进行登录校验,返回用户信息
            user = user_service.login(username, password)
            # 将用户ID保存到会话
            session['user_id'] = user['id']
            # 将用户名保存到会话
            session['username'] = user['username']
            # 设置会话为永久有效(默认31天)
            session.permanent = True  

            # 登录成功的提示信息
            flash('登录成功!', 'success')

            # 登录成功后重定向到“next_url”或知识库列表页面
            return redirect(next_url or url_for('auth.home'))
        # 如果抛出 ValueError,提示具体错误(如用户名或密码不正确)
        except ValueError as e:
            flash(str(e), 'error')
        # 捕获其他异常,记录日志并提示通用登录失败
        except Exception as e:
            logger.error(f"Login error: {e}")
            flash('登录失败,请稍后重试', 'error')

    # 如果是 GET 请求或者登录失败,获取 URL 参数中的 next
    next_url = request.args.get('next')
    # 渲染登录页面模板,并传递 next_url 变量
    return render_template('login.html', next_url=next_url)

+@bp.route('/logout')
+def logout():
+   """登出"""
+   session.clear()
+   flash('已成功登出', 'success')
+   return redirect(url_for('auth.home'))    

5.4. base_service.py #

app/services/base_service.py

# 基础服务类
"""
基础服务类
"""
# 导入日志库
import logging
# 导入可选类型、泛型、类型变量和类型别名
from typing import Optional, TypeVar, Generic, Dict, Any
# 导入数据库会话和事务管理工具
from app.utils.db import db_session, db_transaction

# 创建日志记录器
logger = logging.getLogger(__name__)

# 定义泛型的类型变量T
T = TypeVar('T')


# 定义基础服务类,支持泛型
class BaseService(Generic[T]):
    # 基础服务类,提供通用的数据库操作方法

    # 初始化方法
    def __init__(self):
        # 初始化服务的日志记录器
        self.logger = logging.getLogger(self.__class__.__name__)

    # 数据库会话上下文管理器(只读)
    def session(self):
        """
        数据库会话上下文管理器(只读操作,不自动提交)

        使用示例:
            with self.session() as db:
                result = db.query(Model).all()
                # 不需要手动关闭 session
        """
        # 返回数据库会话
        return db_session()

    # 数据库事务上下文管理器(自动提交)
    def transaction(self):
        """
        数据库事务上下文管理器(自动提交,出错时回滚)

        使用示例:
            with self.transaction() as db:
                obj = Model(...)
                db.add(obj)
                # 自动提交,出错时自动回滚
        """
        # 返回数据库事务
        return db_transaction()
+   def get_by_id(self, model_class: type, entity_id: str) -> Optional[T]:
+       """
+       根据ID获取实体(通用方法)

+       Args:
+           model_class: 模型类
+           entity_id: 实体ID

+       Returns:
+           实体对象,如果不存在则返回 None
+       """
+       with self.session() as session:
+           try:
+               return session.query(model_class).filter(model_class.id == entity_id).first()
+           except Exception as e:
+               self.logger.error(f"Error getting {model_class.__name__} by id {entity_id}: {e}")
+               return None    

5.5. user_service.py #

app/services/user_service.py

# 用户服务
"""
用户服务
"""
# 导入哈希库
import hashlib
# 导入用户模型
from app.models.user import User
# 导入基础服务类
from app.services.base_service import BaseService

# 定义用户服务类,继承自基础服务
class UserService(BaseService[User]):
    # 用户服务的文档说明
    """用户服务"""

    # 静态方法,用于密码哈希
    @staticmethod
    def hash_password(password: str) -> str:
        # 对密码进行哈希处理
        """
        对密码进行哈希处理

        Args:
            password: 原始密码

        Returns:
            哈希后的密码
        """
        # 使用 SHA256 进行哈希(实际生产环境建议使用 bcrypt)
        return hashlib.sha256(password.encode('utf-8')).hexdigest()

    # 注册新用户方法
    def register(self, username: str, password: str, email: str = None) -> dict:
        # 注册新用户
        """
        注册新用户

        Args:
            username: 用户名
            password: 密码
            email: 邮箱(可选)

        Returns:
            用户信息字典

        Raises:
            ValueError: 如果用户名已存在或其他验证错误
        """
        # 检查用户名和密码是否为空
        if not username or not password:
            raise ValueError("用户名和密码不能为空")

        # 检查用户名长度是否小于3
        if len(username) < 3:
            raise ValueError("用户名至少需要3个字符")

        # 检查密码长度是否小于6
        if len(password) < 6:
            raise ValueError("密码至少需要6个字符")

        # 开启事务
        with self.transaction() as session:
            # 检查用户名是否已存在
            existing_user = session.query(User).filter_by(username=username).first()
            # 如果用户名已存在则抛出异常
            if existing_user:
                raise ValueError("用户名已存在")

            # 如果邮箱提供了,检查邮箱是否已被注册
            if email:
                existing_email = session.query(User).filter_by(email=email).first()
                # 如果邮箱已存在则抛出异常
                if existing_email:
                    raise ValueError("邮箱已被注册")

            # 对密码进行哈希处理
            password_hash = self.hash_password(password)
            # 创建新用户对象
            user = User(
                username=username,
                email=email,
                password_hash=password_hash,
                is_active=True
            )

            # 添加新用户到会话
            session.add(user)
            # 刷新会话,获取用户ID
            session.flush()
            # 刷新用户实例
            session.refresh(user)

            # 日志记录注册信息
            self.logger.info(f"User registered: {username}")
            # 返回用户信息字典
            return user.to_dict()
    # 验证密码方法,比较原始密码和哈希密码是否一致
    def verify_password(self, password: str, password_hash: str) -> bool:
        """
        验证密码

        Args:
            password: 原始密码
            password_hash: 哈希后的密码

        Returns:
            是否匹配
        """
        # 对输入的原始密码进行哈希,并与数据库中保存的哈希值进行比较,返回是否一致
        return self.hash_password(password) == password_hash        

    # 用户登录验证方法
    def login(self, username: str, password: str) -> dict:
        """
        用户登录验证

        Args:
            username: 用户名
            password: 密码

        Returns:
            用户信息字典

        Raises:
            ValueError: 如果用户名或密码错误
        """
        # 检查用户名和密码是否为空,如果为空抛出异常
        if not username or not password:
            raise ValueError("用户名和密码不能为空")

        # 开启 SQLAlchemy 会话
        with self.session() as session:
            # 根据用户名查询用户对象
            user = session.query(User).filter_by(username=username).first()

            # 如果没有查到用户,则抛出用户名或密码错误的异常
            if not user:
                raise ValueError("用户名或密码错误")

            # 如果用户账户被禁用,则抛出账户已被禁用异常
            if not user.is_active:
                raise ValueError("账户已被禁用")

            # 校验密码,若不匹配则抛出异常
            if not self.verify_password(password, user.password_hash):
                raise ValueError("用户名或密码错误")

            # 登录成功,记录日志
            self.logger.info(f"User logged in: {username}")
            # 返回用户信息字典
+           return user.to_dict()    
    # 定义通过用户ID获取用户信息的方法
+   def get_by_id(self, user_id: str) -> dict:
+       """
+       根据ID获取用户

+       Args:
+           user_id: 用户ID

+       Returns:
+           用户信息字典,如果不存在则返回 None
+       """
        # 调用父类的get_by_id方法查找用户
+       user = super().get_by_id(User, user_id)
        # 如果查找到用户
+       if user:
            # 返回将用户对象转换成字典后的信息
+           return user.to_dict()
        # 如果用户不存在,返回None
+       return None            

# 创建用户服务实例
user_service = UserService()

5.6. base.html #

app/templates/base.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}RAG Lite - RAG 系统{% endblock %}</title>

    <!-- Bootstrap CSS -->
    <link href="https://static.docs-hub.com/bootstrapmin_1767018057480.css" rel="stylesheet">
    <!-- Bootstrap Icons -->
    <link href="https://static.docs-hub.com/bootstrapicons_1767018066482.css" rel="stylesheet">

    <style>
        body {
            background-color: #f8f9fa;
        }
        .navbar-brand {
            font-weight: 600;
        }
        .card {
            border: none;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            transition: transform 0.2s, box-shadow 0.2s;
        }
        .card:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0,0,0,0.15);
        }
        .chat-message {
            padding: 1rem;
            margin-bottom: 1rem;
            border-radius: 0.5rem;
        }
        .chat-question {
            background-color: #e3f2fd;
        }
        .chat-answer {
            background-color: #f5f5f5;
        }
        .source-item {
            padding: 0.5rem;
            margin-bottom: 0.5rem;
            background-color: #fff;
            border-left: 3px solid #0d6efd;
            border-radius: 0.25rem;
        }
        .markdown-content {
            line-height: 1.6;
        }
        .markdown-content h1, .markdown-content h2, .markdown-content h3,
        .markdown-content h4, .markdown-content h5, .markdown-content h6 {
            margin-top: 1.5rem;
            margin-bottom: 1rem;
            font-weight: 600;
        }
        .markdown-content h1 { font-size: 2rem; }
        .markdown-content h2 { font-size: 1.75rem; }
        .markdown-content h3 { font-size: 1.5rem; }
        .markdown-content h4 { font-size: 1.25rem; }
        .markdown-content p {
            margin-bottom: 1rem;
        }
        .markdown-content ul, .markdown-content ol {
            margin-bottom: 1rem;
            padding-left: 2rem;
        }
        .markdown-content code {
            background-color: #f4f4f4;
            padding: 0.2rem 0.4rem;
            border-radius: 0.25rem;
            font-size: 0.9em;
        }
        .markdown-content pre {
            background-color: #f4f4f4;
            padding: 1rem;
            border-radius: 0.5rem;
            overflow-x: auto;
            margin-bottom: 1rem;
        }
        .markdown-content pre code {
            background-color: transparent;
            padding: 0;
        }
        .markdown-content blockquote {
            border-left: 4px solid #0d6efd;
            padding-left: 1rem;
            margin-left: 0;
            color: #6c757d;
            margin-bottom: 1rem;
        }
        .markdown-content table {
            width: 100%;
            border-collapse: collapse;
            margin-bottom: 1rem;
        }
        .markdown-content table th,
        .markdown-content table td {
            border: 1px solid #dee2e6;
            padding: 0.5rem;
        }
        .markdown-content table th {
            background-color: #f8f9fa;
            font-weight: 600;
        }
        .markdown-content a {
            color: #0d6efd;
            text-decoration: none;
        }
        .markdown-content a:hover {
            text-decoration: underline;
        }
        /* 打字机效果优化 */
        .markdown-content {
            word-wrap: break-word;
            overflow-wrap: break-word;
        }
        /* 平滑滚动 */
        #chatArea {
            scroll-behavior: smooth;
        }
    </style>

    {% block extra_css %}{% endblock %}
</head>
<body>
    <!-- 导航栏 -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container-fluid">
            <a class="navbar-brand" href="/">
                <i class="bi bi-book"></i> RAG Lite
            </a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav me-auto">
                    <li class="nav-item">
                        <a class="nav-link {% if request.endpoint == 'auth.home' %}active{% endif %}" href="/">
                            <i class="bi bi-house"></i> 首页
                        </a>
                    </li>
                    {% if current_user %}
                    <li class="nav-item">
                        <a class="nav-link {% if request.endpoint == 'knowledgebase.kb_list' or request.endpoint == 'knowledgebase.kb_detail' %}active{% endif %}" href="/kb">
                            <i class="bi bi-database"></i> 知识库
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link {% if request.endpoint == 'chat.chat' %}active{% endif %}" href="/chat">
                            <i class="bi bi-chat-dots"></i> 聊天
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link {% if request.endpoint == 'settings.settings' %}active{% endif %}" href="/settings">
                            <i class="bi bi-gear"></i> 设置
                        </a>
                    </li>
                    {% endif %}
                </ul>
                <ul class="navbar-nav">
+                   {% if current_user %}
+                   <li class="nav-item dropdown">
+                       <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
+                           <i class="bi bi-person-circle"></i> {{ current_user.username }}
+                       </a>
+                       <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
+                           <li><a class="dropdown-item" href="{{ url_for('auth.logout') }}"><i class="bi bi-box-arrow-right"></i> 登出</a></li>
+                       </ul>
+                   </li>
+                   {% else %}
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('auth.login') }}">
                            <i class="bi bi-box-arrow-in-right"></i> 登录
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('auth.register') }}">
                            <i class="bi bi-person-plus"></i> 注册
                        </a>
                    </li>
+                   {% endif %}
                </ul>
            </div>
        </div>
    </nav>

    <!-- 主要内容 -->
    <div class="container-fluid mt-4">
        <!-- Flash 消息 -->
        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
                <div class="mb-3">
                    {% for category, message in messages %}
                        <div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
                            {{ message }}
                            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
                        </div>
                    {% endfor %}
                </div>
            {% endif %}
        {% endwith %}

        {% block content %}{% endblock %}
    </div>

    <!-- Bootstrap JS -->
    <script src="https://static.docs-hub.com/bootstrapbundlemin_1767018104304.js"></script>
    <!-- Marked.js for Markdown rendering -->
    <script src="https://static.docs-hub.com/markedmin_1767018117982.js"></script>
    {% block extra_js %}{% endblock %}
</body>
</html>

← 上一节 16.数据与模型 下一节 18.知识库管理 →

访问验证

请输入访问令牌

Token不正确,请重新输入