From 48b599509aaa0fc5da9df4e9e9df50714ac2cd71 Mon Sep 17 00:00:00 2001 From: tatianna <443403589@qq.com> Date: Tue, 16 Dec 2025 16:21:23 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E5=9B=A2=E9=98=9F?= =?UTF-8?q?=E8=B4=9F=E8=B4=A3=E4=BA=BA=E6=9D=83=E9=99=90=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=EF=BC=8C=E5=B0=86=E7=9F=A5=E8=AF=86=E5=BA=93=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=BD=92=E5=B1=9E=E5=9B=A2=E9=98=9F=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98(#235)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- management/server/app.py | 30 +- management/server/routes/files/routes.py | 18 +- .../server/routes/knowledgebases/routes.py | 9 + management/server/routes/teams/routes.py | 11 +- management/server/routes/users/routes.py | 54 ++- management/server/services/auth/__init__.py | 1 + management/server/services/auth/auth_utils.py | 47 +++ management/server/services/files/service.py | 11 +- .../server/services/knowledgebases/service.py | 35 +- management/server/services/teams/service.py | 12 +- management/server/services/users/service.py | 315 ++++++++++-------- management/server/utils.py | 8 +- management/web/src/router/index.ts | 111 +++--- 13 files changed, 440 insertions(+), 222 deletions(-) create mode 100644 management/server/services/auth/__init__.py create mode 100644 management/server/services/auth/auth_utils.py diff --git a/management/server/app.py b/management/server/app.py index 3525c18..9d319e0 100644 --- a/management/server/app.py +++ b/management/server/app.py @@ -7,6 +7,7 @@ from flask import Flask, request from flask_cors import CORS from routes import register_routes +from services.users.service import authenticate_user # 加载环境变量 load_dotenv(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "docker", ".env")) @@ -41,12 +42,19 @@ # 生成token -def generate_token(username): +def generate_token(user_info): # 设置令牌过期时间(例如1小时后过期) expire_time = datetime.utcnow() + timedelta(hours=1) - # 生成令牌 - token = jwt.encode({"username": username, "exp": expire_time}, JWT_SECRET, algorithm="HS256") + # 生成令牌,包含用户ID、用户名、角色和租户ID + payload = { + "user_id": user_info['id'], + "username": user_info['username'], + "role": user_info['role'], + "tenant_id": user_info.get('tenant_id'), + "exp": expire_time + } + token = jwt.encode(payload, JWT_SECRET, algorithm="HS256") return token @@ -58,19 +66,17 @@ def login(): username = data.get("username") password = data.get("password") - # 创建用户名和密码的映射 - valid_users = {ADMIN_USERNAME: ADMIN_PASSWORD} + if not username or not password: + return {"code": 1, "message": "用户名和密码不能为空"}, 400 - # 验证用户名是否存在 - if not username or username not in valid_users: - return {"code": 1, "message": "用户名不存在"}, 400 + # 从数据库验证用户,只允许超级管理员和团队负责人登录 + success, user_info, error_message = authenticate_user(username, password) - # 验证密码是否正确 - if not password or password != valid_users[username]: - return {"code": 1, "message": "密码错误"}, 400 + if not success: + return {"code": 1, "message": error_message}, 400 # 生成token - token = generate_token(username) + token = generate_token(user_info) return {"code": 0, "data": {"token": token}, "message": "登录成功"} diff --git a/management/server/routes/files/routes.py b/management/server/routes/files/routes.py index 1eff273..feac0bb 100644 --- a/management/server/routes/files/routes.py +++ b/management/server/routes/files/routes.py @@ -3,6 +3,7 @@ from flask import current_app, jsonify, request, send_file from services.files.service import batch_delete_files, delete_file, download_file_from_minio, get_file_info, get_files_list, handle_chunk_upload, merge_chunks, upload_files_to_server from services.files.utils import FileType +from services.auth import get_current_user_from_token, is_admin from .. import files_bp @@ -19,8 +20,12 @@ def upload_file(): if "files" not in request.files: return jsonify({"code": 400, "message": "未选择文件", "data": None}), 400 + # 获取当前用户信息 + current_user = get_current_user_from_token() + user_id = current_user.get('user_id') if current_user else None + files = request.files.getlist("files") - upload_result = upload_files_to_server(files) + upload_result = upload_files_to_server(files, user_id=user_id) # 返回标准格式 return jsonify({"code": 0, "message": "上传成功", "data": upload_result["data"]}) @@ -33,13 +38,22 @@ def get_files(): return "", 200 try: + # 获取当前用户信息 + current_user = get_current_user_from_token() + current_page = int(request.args.get("currentPage", 1)) page_size = int(request.args.get("size", 10)) name_filter = request.args.get("name", "") sort_by = request.args.get("sort_by", "create_time") sort_order = request.args.get("sort_order", "desc") - result, total = get_files_list(current_page, page_size, name_filter, sort_by, sort_order) + # 根据用户角色过滤文件 + # 超级管理员可以看到所有文件,团队负责人只能看到自己上传的文件 + user_id = None + if current_user and not is_admin(current_user): + user_id = current_user.get('user_id') + + result, total = get_files_list(current_page, page_size, name_filter, sort_by, sort_order, user_id) return jsonify({"code": 0, "data": {"list": result, "total": total}, "message": "获取文件列表成功"}) diff --git a/management/server/routes/knowledgebases/routes.py b/management/server/routes/knowledgebases/routes.py index 54a00d2..1f46e7a 100644 --- a/management/server/routes/knowledgebases/routes.py +++ b/management/server/routes/knowledgebases/routes.py @@ -2,6 +2,7 @@ from flask import request from services.knowledgebases.service import KnowledgebaseService +from services.auth import get_current_user_from_token, is_admin from utils import error_response, success_response from .. import knowledgebase_bp @@ -11,6 +12,9 @@ def get_knowledgebase_list(): """获取知识库列表""" try: + # 获取当前用户信息 + current_user = get_current_user_from_token() + params = { "page": int(request.args.get("currentPage", 1)), "size": int(request.args.get("size", 10)), @@ -18,6 +22,11 @@ def get_knowledgebase_list(): "sort_by": request.args.get("sort_by", "create_time"), "sort_order": request.args.get("sort_order", "desc"), } + + # 如果是团队负责人,只返回其租户的知识库 + if current_user and not is_admin(current_user): + params["tenant_id"] = current_user.get("tenant_id") + result = KnowledgebaseService.get_knowledgebase_list(**params) return success_response(result) except ValueError: diff --git a/management/server/routes/teams/routes.py b/management/server/routes/teams/routes.py index e81ac0b..aa51ef1 100644 --- a/management/server/routes/teams/routes.py +++ b/management/server/routes/teams/routes.py @@ -1,11 +1,15 @@ from flask import jsonify, request from services.teams.service import get_teams_with_pagination, get_team_by_id, delete_team, get_team_members, add_team_member, remove_team_member +from services.auth import get_current_user_from_token, is_admin from .. import teams_bp @teams_bp.route('', methods=['GET']) def get_teams(): """获取团队列表的API端点,支持分页和条件查询""" try: + # 获取当前用户信息 + current_user = get_current_user_from_token() + # 获取查询参数 current_page = int(request.args.get('currentPage', 1)) page_size = int(request.args.get('size', 10)) @@ -13,8 +17,13 @@ def get_teams(): sort_by = request.args.get("sort_by", "create_time") sort_order = request.args.get("sort_order", "desc") + # 如果是团队负责人,只返回其自己的团队 + tenant_id = None + if current_user and not is_admin(current_user): + tenant_id = current_user.get("tenant_id") + # 调用服务函数获取分页和筛选后的团队数据 - teams, total = get_teams_with_pagination(current_page, page_size, team_name, sort_by, sort_order) + teams, total = get_teams_with_pagination(current_page, page_size, team_name, sort_by, sort_order, tenant_id) # 返回符合前端期望格式的数据 return jsonify({ diff --git a/management/server/routes/users/routes.py b/management/server/routes/users/routes.py index c81eeee..a1b322b 100644 --- a/management/server/routes/users/routes.py +++ b/management/server/routes/users/routes.py @@ -1,7 +1,11 @@ +import os +import jwt from flask import jsonify, request -from services.users.service import get_users_with_pagination, delete_user, create_user, update_user, reset_user_password +from services.users.service import get_users_with_pagination, delete_user, create_user, update_user, reset_user_password, get_user_info_by_id from .. import users_bp +JWT_SECRET = os.getenv("MANAGEMENT_JWT_SECRET", "your-secret-key") + @users_bp.route('', methods=['GET']) def get_users(): """获取用户的API端点,支持分页和条件查询""" @@ -78,14 +82,46 @@ def update_user_route(user_id): @users_bp.route('/me', methods=['GET']) def get_current_user(): - return jsonify({ - "code": 0, - "data": { - "username": "admin", - "roles": ["admin"] - }, - "message": "获取用户信息成功" - }) + """获取当前登录用户信息""" + try: + # 从请求头获取token + auth_header = request.headers.get('Authorization') + if not auth_header or not auth_header.startswith('Bearer '): + return jsonify({"code": 401, "message": "未提供有效的认证令牌"}), 401 + + token = auth_header.split(' ')[1] + + # 解析token + try: + payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"]) + except jwt.ExpiredSignatureError: + return jsonify({"code": 401, "message": "令牌已过期"}), 401 + except jwt.InvalidTokenError: + return jsonify({"code": 401, "message": "无效的令牌"}), 401 + + user_id = payload.get('user_id') + + # 从数据库获取用户信息 + user_info = get_user_info_by_id(user_id) + + if not user_info: + return jsonify({"code": 401, "message": "用户不存在或无权限"}), 401 + + return jsonify({ + "code": 0, + "data": { + "username": user_info['username'], + "roles": user_info['roles'], + "role": user_info['role'], + "tenant_id": user_info.get('tenant_id') + }, + "message": "获取用户信息成功" + }) + except Exception as e: + return jsonify({ + "code": 500, + "message": f"获取用户信息失败: {str(e)}" + }), 500 @users_bp.route('//reset-password', methods=['PUT']) def reset_password_route(user_id): diff --git a/management/server/services/auth/__init__.py b/management/server/services/auth/__init__.py new file mode 100644 index 0000000..16810b2 --- /dev/null +++ b/management/server/services/auth/__init__.py @@ -0,0 +1 @@ +from .auth_utils import get_current_user_from_token, is_admin, is_team_owner diff --git a/management/server/services/auth/auth_utils.py b/management/server/services/auth/auth_utils.py new file mode 100644 index 0000000..6baca47 --- /dev/null +++ b/management/server/services/auth/auth_utils.py @@ -0,0 +1,47 @@ +import os +import jwt +from flask import request + +JWT_SECRET = os.getenv("MANAGEMENT_JWT_SECRET", "your-secret-key") + + +def get_current_user_from_token(): + """ + 从请求头的JWT token中获取当前用户信息 + 返回: dict 包含 user_id, username, role, tenant_id 或 None + """ + try: + auth_header = request.headers.get('Authorization') + if not auth_header or not auth_header.startswith('Bearer '): + return None + + token = auth_header.split(' ')[1] + + try: + payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"]) + return { + 'user_id': payload.get('user_id'), + 'username': payload.get('username'), + 'role': payload.get('role'), + 'tenant_id': payload.get('tenant_id') + } + except jwt.ExpiredSignatureError: + return None + except jwt.InvalidTokenError: + return None + except Exception: + return None + + +def is_admin(user_info): + """检查用户是否是超级管理员""" + if not user_info: + return False + return user_info.get('role') == 'admin' + + +def is_team_owner(user_info): + """检查用户是否是团队负责人""" + if not user_info: + return False + return user_info.get('role') == 'team_owner' diff --git a/management/server/services/files/service.py b/management/server/services/files/service.py index 471dccc..7a1a48c 100644 --- a/management/server/services/files/service.py +++ b/management/server/services/files/service.py @@ -49,15 +49,17 @@ def filename_type(filename): return FileType.OTHER.value -def get_files_list(current_page, page_size, name_filter="", sort_by="create_time", sort_order="desc"): +def get_files_list(current_page, page_size, name_filter="", sort_by="create_time", sort_order="desc", user_id=None): """ 获取文件列表 Args: current_page: 当前页码 page_size: 每页大小 - parent_id: 父文件夹ID name_filter: 文件名过滤条件 + sort_by: 排序字段 + sort_order: 排序顺序 + user_id: 用户ID(如果提供,则只返回该用户上传的文件) Returns: tuple: (文件列表, 总数) @@ -77,6 +79,11 @@ def get_files_list(current_page, page_size, name_filter="", sort_by="create_time if name_filter: where_clause += " AND f.name LIKE %s" params.append(f"%{name_filter}%") + + # 如果提供了user_id,则只返回该用户上传的文件 + if user_id: + where_clause += " AND f.created_by = %s" + params.append(user_id) # 验证排序字段 valid_sort_fields = ["name", "size", "type", "create_time", "create_date"] diff --git a/management/server/services/knowledgebases/service.py b/management/server/services/knowledgebases/service.py index ef4e832..ee29c10 100644 --- a/management/server/services/knowledgebases/service.py +++ b/management/server/services/knowledgebases/service.py @@ -24,8 +24,11 @@ def _get_db_connection(cls): return mysql.connector.connect(**DB_CONFIG) @classmethod - def get_knowledgebase_list(cls, page=1, size=10, name="", sort_by="create_time", sort_order="desc"): - """获取知识库列表""" + def get_knowledgebase_list(cls, page=1, size=10, name="", sort_by="create_time", sort_order="desc", tenant_id=None): + """ + 获取知识库列表 + tenant_id: 如果提供,则只返回该租户相关的知识库 + """ conn = cls._get_db_connection() cursor = conn.cursor(dictionary=True) @@ -49,16 +52,26 @@ def get_knowledgebase_list(cls, page=1, size=10, name="", sort_by="create_time", k.language, k.permission, k.created_by, + k.tenant_id, u.nickname FROM knowledgebase k - LEFT JOIN user u ON k.created_by = u.id -- 获取创建者的昵称 + LEFT JOIN user u ON k.created_by = u.id """ params = [] + where_clauses = [] if name: - query += " WHERE k.name LIKE %s" + where_clauses.append("k.name LIKE %s") params.append(f"%{name}%") + # 如果提供了tenant_id,则只返回该租户的知识库 + if tenant_id: + where_clauses.append("k.tenant_id = %s") + params.append(tenant_id) + + if where_clauses: + query += " WHERE " + " AND ".join(where_clauses) + # 添加查询排序条件 query += f" {sort_clause}" @@ -87,10 +100,18 @@ def get_knowledgebase_list(cls, page=1, size=10, name="", sort_by="create_time", result['nickname'] = "未知用户" # 获取总数 - count_query = "SELECT COUNT(*) as total FROM knowledgebase" + count_query = "SELECT COUNT(*) as total FROM knowledgebase k" + count_params = [] + count_where_clauses = [] if name: - count_query += " WHERE name LIKE %s" - cursor.execute(count_query, params[:1] if name else []) + count_where_clauses.append("k.name LIKE %s") + count_params.append(f"%{name}%") + if tenant_id: + count_where_clauses.append("k.tenant_id = %s") + count_params.append(tenant_id) + if count_where_clauses: + count_query += " WHERE " + " AND ".join(count_where_clauses) + cursor.execute(count_query, count_params) total = cursor.fetchone()["total"] cursor.close() diff --git a/management/server/services/teams/service.py b/management/server/services/teams/service.py index 1dcf0c7..087a459 100644 --- a/management/server/services/teams/service.py +++ b/management/server/services/teams/service.py @@ -3,8 +3,11 @@ from utils import generate_uuid from database import DB_CONFIG -def get_teams_with_pagination(current_page, page_size, name='', sort_by="create_time",sort_order="desc"): - """查询团队信息,支持分页和条件筛选""" +def get_teams_with_pagination(current_page, page_size, name='', sort_by="create_time", sort_order="desc", tenant_id=None): + """ + 查询团队信息,支持分页和条件筛选 + tenant_id: 如果提供,则只返回该租户的团队 + """ try: conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor(dictionary=True) @@ -17,6 +20,11 @@ def get_teams_with_pagination(current_page, page_size, name='', sort_by="create_ where_clauses.append("t.name LIKE %s") params.append(f"%{name}%") + # 如果提供了tenant_id,则只返回该租户的团队 + if tenant_id: + where_clauses.append("t.id = %s") + params.append(tenant_id) + # 组合WHERE子句 where_sql = "WHERE " + (" AND ".join(where_clauses) if where_clauses else "1=1") diff --git a/management/server/services/users/service.py b/management/server/services/users/service.py index 9b50bee..0b796a9 100644 --- a/management/server/services/users/service.py +++ b/management/server/services/users/service.py @@ -1,9 +1,183 @@ +import os import mysql.connector import pytz from datetime import datetime -from utils import generate_uuid, encrypt_password +from utils import generate_uuid, encrypt_password, verify_password from database import DB_CONFIG +# 从环境变量获取超级管理员配置 +ADMIN_USERNAME = os.getenv("MANAGEMENT_ADMIN_USERNAME", "admin") +ADMIN_PASSWORD = os.getenv("MANAGEMENT_ADMIN_PASSWORD", "12345678") + + +def authenticate_user(username: str, password: str): + """ + 验证用户登录: + 1. 超级管理员(admin)使用环境变量验证 + 2. 团队负责人(owner)使用数据库验证 + username: 可以是email或nickname + 返回: (success: bool, user_info: dict, error_message: str) + """ + # 首先检查是否是超级管理员(使用环境变量验证) + if username == ADMIN_USERNAME: + if password == ADMIN_PASSWORD: + return True, { + 'id': 'admin', + 'username': ADMIN_USERNAME, + 'role': 'admin', + 'is_superuser': True, + 'tenant_id': None + }, None + else: + return False, None, "密码错误" + + # 其他用户使用数据库验证 + try: + conn = mysql.connector.connect(**DB_CONFIG) + cursor = conn.cursor(dictionary=True) + + # 查询用户信息 - 支持email或nickname登录 + query = """ + SELECT id, nickname, email, password, is_superuser + FROM user + WHERE email = %s OR nickname = %s + """ + cursor.execute(query, (username, username)) + user = cursor.fetchone() + + if not user: + cursor.close() + conn.close() + return False, None, "用户名或邮箱不存在" + + # 验证密码 + if not verify_password(password, user['password']): + cursor.close() + conn.close() + return False, None, "密码错误" + + user_id = user['id'] + is_superuser = user['is_superuser'] + + # 检查是否是数据库中标记的超级管理员 + if is_superuser: + cursor.close() + conn.close() + return True, { + 'id': user_id, + 'username': user['nickname'], + 'role': 'admin', + 'is_superuser': True, + 'tenant_id': None + }, None + + # 检查是否是团队负责人 (owner) + owner_query = """ + SELECT tenant_id + FROM user_tenant + WHERE user_id = %s AND role = 'owner' + """ + cursor.execute(owner_query, (user_id,)) + owner_record = cursor.fetchone() + + cursor.close() + conn.close() + + if owner_record: + return True, { + 'id': user_id, + 'username': user['nickname'], + 'role': 'team_owner', + 'is_superuser': False, + 'tenant_id': owner_record['tenant_id'] + }, None + + # 既不是超级管理员也不是团队负责人 + return False, None, "无权限登录管理系统,只有超级管理员和团队负责人可以登录" + + except mysql.connector.Error as err: + print(f"数据库错误: {err}") + return False, None, f"数据库错误: {str(err)}" + + +def get_user_info_by_id(user_id: str): + """ + 根据用户ID获取用户信息和角色 + """ + # 如果是环境变量配置的超级管理员 + if user_id == 'admin': + return { + 'id': 'admin', + 'username': ADMIN_USERNAME, + 'role': 'admin', + 'roles': ['admin'], + 'is_superuser': True, + 'tenant_id': None + } + + try: + conn = mysql.connector.connect(**DB_CONFIG) + cursor = conn.cursor(dictionary=True) + + # 查询用户信息 + query = """ + SELECT id, nickname, is_superuser + FROM user + WHERE id = %s + """ + cursor.execute(query, (user_id,)) + user = cursor.fetchone() + + if not user: + cursor.close() + conn.close() + return None + + username = user['nickname'] + is_superuser = user['is_superuser'] + + # 检查是否是数据库中标记的超级管理员 + if is_superuser: + cursor.close() + conn.close() + return { + 'id': user_id, + 'username': username, + 'role': 'admin', + 'roles': ['admin'], + 'is_superuser': True, + 'tenant_id': None + } + + # 检查是否是团队负责人 + owner_query = """ + SELECT tenant_id + FROM user_tenant + WHERE user_id = %s AND role = 'owner' + """ + cursor.execute(owner_query, (user_id,)) + owner_record = cursor.fetchone() + + cursor.close() + conn.close() + + if owner_record: + return { + 'id': user_id, + 'username': username, + 'role': 'team_owner', + 'roles': ['team_owner'], + 'is_superuser': False, + 'tenant_id': owner_record['tenant_id'] + } + + return None + + except mysql.connector.Error as err: + print(f"数据库错误: {err}") + return None + + def get_users_with_pagination(current_page, page_size, username='', email='', sort_by="create_time",sort_order="desc"): """查询用户信息,支持分页和条件筛选""" try: @@ -107,48 +281,13 @@ def delete_user(user_id): def create_user(user_data): """ - 创建新用户,并加入最早用户的团队,并使用相同的模型配置。 + 创建新用户(仅创建用户记录,不自动创建团队和配置) 时间将以 UTC+8 (Asia/Shanghai) 存储。 """ try: conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor(dictionary=True) - # 检查用户表是否为空 - check_users_query = "SELECT COUNT(*) as user_count FROM user" - cursor.execute(check_users_query) - user_count = cursor.fetchone()['user_count'] - - # 如果有用户,则查询最早的tenant和用户配置 - if user_count > 0: - # 查询最早创建的tenant配置 - query_earliest_tenant = """ - SELECT id, llm_id, embd_id, asr_id, img2txt_id, rerank_id, tts_id, parser_ids, credit - FROM tenant - WHERE create_time = (SELECT MIN(create_time) FROM tenant) - LIMIT 1 - """ - cursor.execute(query_earliest_tenant) - earliest_tenant = cursor.fetchone() - - # 查询最早创建的用户ID - query_earliest_user = """ - SELECT id FROM user - WHERE create_time = (SELECT MIN(create_time) FROM user) - LIMIT 1 - """ - cursor.execute(query_earliest_user) - earliest_user = cursor.fetchone() - - # 查询最早用户的所有tenant_llm配置 - query_earliest_user_tenant_llms = """ - SELECT llm_factory, model_type, llm_name, api_key, api_base, max_tokens, used_tokens - FROM tenant_llm - WHERE tenant_id = %s - """ - cursor.execute(query_earliest_user_tenant_llms, (earliest_user['id'],)) - earliest_user_tenant_llms = cursor.fetchall() - # 开始插入 user_id = generate_uuid() # 获取基本信息 @@ -158,20 +297,15 @@ def create_user(user_data): # 加密密码 encrypted_password = encrypt_password(password) - # --- 修改时间获取和格式化逻辑 --- - # 获取当前 UTC 时间 + # 获取当前 UTC 时间并转换为 UTC+8 utc_now = datetime.utcnow().replace(tzinfo=pytz.utc) - # 定义目标时区 (UTC+8) target_tz = pytz.timezone('Asia/Shanghai') - # 将 UTC 时间转换为目标时区时间 local_dt = utc_now.astimezone(target_tz) - # 使用转换后的时间 - create_time = int(local_dt.timestamp() * 1000) # 使用本地化时间戳 - current_date = local_dt.strftime("%Y-%m-%d %H:%M:%S") # 使用本地化时间格式化 - # --- 时间逻辑修改结束 --- + create_time = int(local_dt.timestamp() * 1000) + current_date = local_dt.strftime("%Y-%m-%d %H:%M:%S") - # 插入用户表 + # 仅插入用户表 user_insert_query = """ INSERT INTO user ( id, create_time, create_date, update_time, update_date, access_token, @@ -186,97 +320,12 @@ def create_user(user_data): ) """ user_data_tuple = ( - user_id, create_time, current_date, create_time, current_date, None, # 使用修改后的时间 + user_id, create_time, current_date, create_time, current_date, None, username, encrypted_password, email, None, "Chinese", "Bright", "UTC+8 Asia/Shanghai", - current_date, 1, 1, 0, "password", # last_login_time 也使用 UTC+8 时间 + current_date, 1, 1, 0, "password", 1, 0 ) cursor.execute(user_insert_query, user_data_tuple) - - # 插入租户表 - tenant_insert_query = """ - INSERT INTO tenant ( - id, create_time, create_date, update_time, update_date, name, - public_key, llm_id, embd_id, asr_id, img2txt_id, rerank_id, tts_id, - parser_ids, credit, status - ) VALUES ( - %s, %s, %s, %s, %s, %s, - %s, %s, %s, %s, %s, %s, %s, - %s, %s, %s - ) - """ - - if user_count > 0: - # 如果有现有用户,复制其模型配置 - tenant_data = ( - user_id, create_time, current_date, create_time, current_date, username + "'s Kingdom", # 使用修改后的时间 - None, str(earliest_tenant['llm_id']), str(earliest_tenant['embd_id']), - str(earliest_tenant['asr_id']), str(earliest_tenant['img2txt_id']), - str(earliest_tenant['rerank_id']), str(earliest_tenant['tts_id']), - str(earliest_tenant['parser_ids']), str(earliest_tenant['credit']), 1 - ) - else: - # 如果是第一个用户,模型ID使用空字符串 - tenant_data = ( - user_id, create_time, current_date, create_time, current_date, username + "'s Kingdom", # 使用修改后的时间 - None, '', '', '', '', '', '', - '', "1000", 1 - ) - cursor.execute(tenant_insert_query, tenant_data) - - # 插入用户租户关系表(owner角色) - user_tenant_insert_owner_query = """ - INSERT INTO user_tenant ( - id, create_time, create_date, update_time, update_date, user_id, - tenant_id, role, invited_by, status - ) VALUES ( - %s, %s, %s, %s, %s, %s, - %s, %s, %s, %s - ) - """ - user_tenant_data_owner = ( - generate_uuid(), create_time, current_date, create_time, current_date, user_id, # 使用修改后的时间 - user_id, "owner", user_id, 1 - ) - cursor.execute(user_tenant_insert_owner_query, user_tenant_data_owner) - - # 只有在存在其他用户时,才加入最早用户的团队 - if user_count > 0: - # 插入用户租户关系表(normal角色) - user_tenant_insert_normal_query = """ - INSERT INTO user_tenant ( - id, create_time, create_date, update_time, update_date, user_id, - tenant_id, role, invited_by, status - ) VALUES ( - %s, %s, %s, %s, %s, %s, - %s, %s, %s, %s - ) - """ - user_tenant_data_normal = ( - generate_uuid(), create_time, current_date, create_time, current_date, user_id, # 使用修改后的时间 - earliest_tenant['id'], "normal", earliest_tenant['id'], 1 - ) - cursor.execute(user_tenant_insert_normal_query, user_tenant_data_normal) - - # 为新用户复制最早用户的所有tenant_llm配置 - tenant_llm_insert_query = """ - INSERT INTO tenant_llm ( - create_time, create_date, update_time, update_date, tenant_id, - llm_factory, model_type, llm_name, api_key, api_base, max_tokens, used_tokens - ) VALUES ( - %s, %s, %s, %s, %s, - %s, %s, %s, %s, %s, %s, %s - ) - """ - - # 遍历最早用户的所有tenant_llm配置并复制给新用户 - for tenant_llm in earliest_user_tenant_llms: - tenant_llm_data = ( - create_time, current_date, create_time, current_date, user_id, # 使用修改后的时间 - str(tenant_llm['llm_factory']), str(tenant_llm['model_type']), str(tenant_llm['llm_name']), - str(tenant_llm['api_key']), str(tenant_llm['api_base']), str(tenant_llm['max_tokens']), 0 - ) - cursor.execute(tenant_llm_insert_query, tenant_llm_data) conn.commit() cursor.close() diff --git a/management/server/utils.py b/management/server/utils.py index 220072c..852b635 100644 --- a/management/server/utils.py +++ b/management/server/utils.py @@ -4,7 +4,7 @@ from Cryptodome.Cipher import PKCS1_v1_5 from Cryptodome.PublicKey import RSA from flask import jsonify -from werkzeug.security import generate_password_hash +from werkzeug.security import generate_password_hash, check_password_hash # 生成随机的 UUID 作为 id @@ -30,6 +30,12 @@ def encrypt_password(raw_password: str) -> str: return generate_password_hash(base64_password) +# 验证密码 +def verify_password(raw_password: str, hashed_password: str) -> bool: + base64_password = base64.b64encode(raw_password.encode()).decode() + return check_password_hash(hashed_password, base64_password) + + # 标准响应格式 def success_response(data=None, message="操作成功", code=0): return jsonify({"code": code, "message": message, "data": data}) diff --git a/management/web/src/router/index.ts b/management/web/src/router/index.ts index b0bb10e..d603910 100644 --- a/management/web/src/router/index.ts +++ b/management/web/src/router/index.ts @@ -45,20 +45,43 @@ export const constantRoutes: RouteRecordRaw[] = [ meta: { hidden: true } - }, + } +] + +/** + * @name 动态路由 + * @description 用来放置有权限 (Roles 属性) 的路由 + * @description 必须带有唯一的 Name 属性 + */ +export const dynamicRoutes: RouteRecordRaw[] = [ { path: "/", component: Layouts, - redirect: "/dashboard", + redirect: "/file", + name: "Root", + meta: { + roles: ["admin", "team_owner"] + }, + children: [] + }, + { + path: "/dashboard", + component: Layouts, + redirect: "/dashboard/index", + name: "Dashboard", + meta: { + roles: ["admin"] + }, children: [ { - path: "dashboard", + path: "index", component: () => import("@/pages/user-management/index.vue"), name: "UserManagement", meta: { title: "用户管理", svgIcon: "user-management", - affix: true + affix: true, + roles: ["admin"] } } ] @@ -67,6 +90,10 @@ export const constantRoutes: RouteRecordRaw[] = [ path: "/team", component: Layouts, redirect: "/team/index", + name: "TeamManagement", + meta: { + roles: ["admin", "team_owner"] + }, children: [ { path: "index", @@ -76,7 +103,8 @@ export const constantRoutes: RouteRecordRaw[] = [ title: "团队管理", svgIcon: "team-management", affix: false, - keepAlive: true + keepAlive: true, + roles: ["admin", "team_owner"] } } ] @@ -85,6 +113,10 @@ export const constantRoutes: RouteRecordRaw[] = [ path: "/config", component: Layouts, redirect: "/config/index", + name: "ConfigManagement", + meta: { + roles: ["admin"] + }, children: [ { path: "index", @@ -94,7 +126,8 @@ export const constantRoutes: RouteRecordRaw[] = [ title: "用户配置", svgIcon: "user-config", affix: false, - keepAlive: true + keepAlive: true, + roles: ["admin"] } } ] @@ -103,6 +136,10 @@ export const constantRoutes: RouteRecordRaw[] = [ path: "/file", component: Layouts, redirect: "/file/index", + name: "FileManagement", + meta: { + roles: ["admin", "team_owner"] + }, children: [ { path: "index", @@ -112,7 +149,8 @@ export const constantRoutes: RouteRecordRaw[] = [ title: "文件管理", svgIcon: "file", affix: false, - keepAlive: true + keepAlive: true, + roles: ["admin", "team_owner"] } } ] @@ -121,6 +159,10 @@ export const constantRoutes: RouteRecordRaw[] = [ path: "/knowledgebase", component: Layouts, redirect: "/knowledgebase/index", + name: "KnowledgeBaseManagement", + meta: { + roles: ["admin", "team_owner"] + }, children: [ { path: "index", @@ -130,7 +172,8 @@ export const constantRoutes: RouteRecordRaw[] = [ title: "知识库管理", svgIcon: "kb", affix: false, - keepAlive: true + keepAlive: true, + roles: ["admin", "team_owner"] } } ] @@ -139,65 +182,27 @@ export const constantRoutes: RouteRecordRaw[] = [ path: "/conversation", component: Layouts, redirect: "/conversation/index", + name: "ConversationManagement", + meta: { + roles: ["admin"] + }, children: [ { path: "index", component: () => import("@/pages/conversation/index.vue"), - name: "conversation", + name: "Conversation", meta: { title: "用户会话管理", svgIcon: "conversation", affix: false, - keepAlive: true + keepAlive: true, + roles: ["admin"] } } ] } ] -/** - * @name 动态路由 - * @description 用来放置有权限 (Roles 属性) 的路由 - * @description 必须带有唯一的 Name 属性 - */ -export const dynamicRoutes: RouteRecordRaw[] = [ - // { - // path: "/permission", - // component: Layouts, - // redirect: "/permission/page-level", - // name: "Permission", - // meta: { - // title: "权限演示", - // elIcon: "Lock", - // // 可以在根路由中设置角色 - // roles: ["admin", "editor"], - // alwaysShow: true - // }, - // children: [ - // { - // path: "page-level", - // component: () => import("@/pages/demo/permission/page-level.vue"), - // name: "PermissionPageLevel", - // meta: { - // title: "页面级", - // // 或者在子路由中设置角色 - // roles: ["admin"] - // } - // }, - // { - // path: "button-level", - // component: () => import("@/pages/demo/permission/button-level.vue"), - // name: "PermissionButtonLevel", - // meta: { - // title: "按钮级", - // // 如果未设置角色,则表示:该页面不需要权限,但会继承根路由的角色 - // roles: undefined - // } - // } - // ] - // } -] - /** 路由实例 */ export const router = createRouter({ history: routerConfig.history, From 14434de4ad3cb66afdadd5dd5fd7b2fa76376e66 Mon Sep 17 00:00:00 2001 From: tatianna <443403589@qq.com> Date: Wed, 17 Dec 2025 14:30:32 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=E8=AF=AF=E5=88=A0=E4=BA=86=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E6=96=B0=E7=94=A8=E6=88=B7=EF=BC=8C=E5=B9=B6=E5=8A=A0?= =?UTF-8?q?=E5=85=A5=E6=9C=80=E6=97=A9=E7=94=A8=E6=88=B7=E7=9A=84=E5=9B=A2?= =?UTF-8?q?=E9=98=9F=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- management/server/services/users/service.py | 139 +++++++++++++++++++- 1 file changed, 132 insertions(+), 7 deletions(-) diff --git a/management/server/services/users/service.py b/management/server/services/users/service.py index 0b796a9..594afb0 100644 --- a/management/server/services/users/service.py +++ b/management/server/services/users/service.py @@ -281,13 +281,48 @@ def delete_user(user_id): def create_user(user_data): """ - 创建新用户(仅创建用户记录,不自动创建团队和配置) + 创建新用户,并加入最早用户的团队,并使用相同的模型配置。 时间将以 UTC+8 (Asia/Shanghai) 存储。 """ try: conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor(dictionary=True) + # 检查用户表是否为空 + check_users_query = "SELECT COUNT(*) as user_count FROM user" + cursor.execute(check_users_query) + user_count = cursor.fetchone()['user_count'] + + # 如果有用户,则查询最早的tenant和用户配置 + if user_count > 0: + # 查询最早创建的tenant配置 + query_earliest_tenant = """ + SELECT id, llm_id, embd_id, asr_id, img2txt_id, rerank_id, tts_id, parser_ids, credit + FROM tenant + WHERE create_time = (SELECT MIN(create_time) FROM tenant) + LIMIT 1 + """ + cursor.execute(query_earliest_tenant) + earliest_tenant = cursor.fetchone() + + # 查询最早创建的用户ID + query_earliest_user = """ + SELECT id FROM user + WHERE create_time = (SELECT MIN(create_time) FROM user) + LIMIT 1 + """ + cursor.execute(query_earliest_user) + earliest_user = cursor.fetchone() + + # 查询最早用户的所有tenant_llm配置 + query_earliest_user_tenant_llms = """ + SELECT llm_factory, model_type, llm_name, api_key, api_base, max_tokens, used_tokens + FROM tenant_llm + WHERE tenant_id = %s + """ + cursor.execute(query_earliest_user_tenant_llms, (earliest_user['id'],)) + earliest_user_tenant_llms = cursor.fetchall() + # 开始插入 user_id = generate_uuid() # 获取基本信息 @@ -297,15 +332,20 @@ def create_user(user_data): # 加密密码 encrypted_password = encrypt_password(password) - # 获取当前 UTC 时间并转换为 UTC+8 + # --- 修改时间获取和格式化逻辑 --- + # 获取当前 UTC 时间 utc_now = datetime.utcnow().replace(tzinfo=pytz.utc) + # 定义目标时区 (UTC+8) target_tz = pytz.timezone('Asia/Shanghai') + # 将 UTC 时间转换为目标时区时间 local_dt = utc_now.astimezone(target_tz) - create_time = int(local_dt.timestamp() * 1000) - current_date = local_dt.strftime("%Y-%m-%d %H:%M:%S") + # 使用转换后的时间 + create_time = int(local_dt.timestamp() * 1000) # 使用本地化时间戳 + current_date = local_dt.strftime("%Y-%m-%d %H:%M:%S") # 使用本地化时间格式化 + # --- 时间逻辑修改结束 --- - # 仅插入用户表 + # 插入用户表 user_insert_query = """ INSERT INTO user ( id, create_time, create_date, update_time, update_date, access_token, @@ -320,12 +360,97 @@ def create_user(user_data): ) """ user_data_tuple = ( - user_id, create_time, current_date, create_time, current_date, None, + user_id, create_time, current_date, create_time, current_date, None, # 使用修改后的时间 username, encrypted_password, email, None, "Chinese", "Bright", "UTC+8 Asia/Shanghai", - current_date, 1, 1, 0, "password", + current_date, 1, 1, 0, "password", # last_login_time 也使用 UTC+8 时间 1, 0 ) cursor.execute(user_insert_query, user_data_tuple) + + # 插入租户表 + tenant_insert_query = """ + INSERT INTO tenant ( + id, create_time, create_date, update_time, update_date, name, + public_key, llm_id, embd_id, asr_id, img2txt_id, rerank_id, tts_id, + parser_ids, credit, status + ) VALUES ( + %s, %s, %s, %s, %s, %s, + %s, %s, %s, %s, %s, %s, %s, + %s, %s, %s + ) + """ + + if user_count > 0: + # 如果有现有用户,复制其模型配置 + tenant_data = ( + user_id, create_time, current_date, create_time, current_date, username + "'s Kingdom", # 使用修改后的时间 + None, str(earliest_tenant['llm_id']), str(earliest_tenant['embd_id']), + str(earliest_tenant['asr_id']), str(earliest_tenant['img2txt_id']), + str(earliest_tenant['rerank_id']), str(earliest_tenant['tts_id']), + str(earliest_tenant['parser_ids']), str(earliest_tenant['credit']), 1 + ) + else: + # 如果是第一个用户,模型ID使用空字符串 + tenant_data = ( + user_id, create_time, current_date, create_time, current_date, username + "'s Kingdom", # 使用修改后的时间 + None, '', '', '', '', '', '', + '', "1000", 1 + ) + cursor.execute(tenant_insert_query, tenant_data) + + # 插入用户租户关系表(owner角色) + user_tenant_insert_owner_query = """ + INSERT INTO user_tenant ( + id, create_time, create_date, update_time, update_date, user_id, + tenant_id, role, invited_by, status + ) VALUES ( + %s, %s, %s, %s, %s, %s, + %s, %s, %s, %s + ) + """ + user_tenant_data_owner = ( + generate_uuid(), create_time, current_date, create_time, current_date, user_id, # 使用修改后的时间 + user_id, "owner", user_id, 1 + ) + cursor.execute(user_tenant_insert_owner_query, user_tenant_data_owner) + + # 只有在存在其他用户时,才加入最早用户的团队 + if user_count > 0: + # 插入用户租户关系表(normal角色) + user_tenant_insert_normal_query = """ + INSERT INTO user_tenant ( + id, create_time, create_date, update_time, update_date, user_id, + tenant_id, role, invited_by, status + ) VALUES ( + %s, %s, %s, %s, %s, %s, + %s, %s, %s, %s + ) + """ + user_tenant_data_normal = ( + generate_uuid(), create_time, current_date, create_time, current_date, user_id, # 使用修改后的时间 + earliest_tenant['id'], "normal", earliest_tenant['id'], 1 + ) + cursor.execute(user_tenant_insert_normal_query, user_tenant_data_normal) + + # 为新用户复制最早用户的所有tenant_llm配置 + tenant_llm_insert_query = """ + INSERT INTO tenant_llm ( + create_time, create_date, update_time, update_date, tenant_id, + llm_factory, model_type, llm_name, api_key, api_base, max_tokens, used_tokens + ) VALUES ( + %s, %s, %s, %s, %s, + %s, %s, %s, %s, %s, %s, %s + ) + """ + + # 遍历最早用户的所有tenant_llm配置并复制给新用户 + for tenant_llm in earliest_user_tenant_llms: + tenant_llm_data = ( + create_time, current_date, create_time, current_date, user_id, # 使用修改后的时间 + str(tenant_llm['llm_factory']), str(tenant_llm['model_type']), str(tenant_llm['llm_name']), + str(tenant_llm['api_key']), str(tenant_llm['api_base']), str(tenant_llm['max_tokens']), 0 + ) + cursor.execute(tenant_llm_insert_query, tenant_llm_data) conn.commit() cursor.close()