1.本章目标 #
- 介绍项目的权限管理功能与整体架构,帮助读者了解用户身份识别与访问控制的实现方式。
- 说明主页、导航栏、登录、注册、登出等涉及权限和身份态管理的前端页面及其行为。
- 梳理认证相关蓝图和服务(如
app/blueprints/auth.py,app/services/user_service.py),说明用户注册、登录流程和会话管理的后端逻辑。 - 展示如何通过模板上下文(如
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.toml3.用户注册 #
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 app4.用户登录 #
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>